React.js

6. 리액트 기본훅

JadeStone 2023. 1. 17. 18:45

훅(HOOK) 이란?

  1. 리액트 컴포넌트는 클래스형 컴포넌트(Class component)와 함수형 컴포넌트(Functional component)로 나뉩니다.
  2. 리액트 훅은 새로운 기능으로 React 16.8버전에 새로 추가된 기능
  3. 함수형태의 컴포넌트에서 사용되는 몇가지 기술을 Hook이라고 부른다. (useState, userEffect 등)
  4. 리액트 훅은 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해주는 기능이다.

훅의 규칙

  • 최상위 에서만 Hook을 호출해야 한다  -> (함수형 컴포넌트의 중괄호 바로 아래에서만 호출해야 된다는 뜻.)
    • 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 안된다.
    • 이 규칙을 따르면 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
  • 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.

반드시 알아야 할 기본 훅

useState(초기값) 

useState() : 배열반환 
첫번째 배열의 요소에는 현재값을, 두번째 요소는 상태를 변경하는 (setter) 를 반환합니다.

const [data, setData] = useState('초기값')

 

useEffect(실행시킬 콜백함수, 값에 따른 렌더링 지정 ) 

userEffect의 첫번째 매개변수는 실행시킬 콜백함수

userEffect의 두번째 매개변수는 배열[]을 사용하여 특정값이 update 될 때만 실행시켜 줄 수 있습니다.

 

useEffect() 는 컴포넌트의 라이프 사이클을 다룹니다.
리액트 컴포넌트가 mount, mount이후, unmount때 마다 특정작업을 수행합니다.

 

(mount는 화면이 렌더링 된 후 화면이 브라우저에 올라가는것(생성)을 말합니다.)

 

라이프 사이클

 

#mount 이후 - 컴포넌트가 마운트 됨, 즉 컴포넌트의 첫번째 렌더링이 마치면 호출되는 메서드 입니다.

클래스형 componentDidMount() 대체 

->  렌더링 이후 데이터를 가져와야하는 일이 제일 많기 때문에 이걸 제일 많이 사용함.

 

함수형 훅

    //useEffect(함수) - 화면이 mount된 이후에 동작됩니다
    useEffect(() => {
        console.log(`렌더링완료. state값${name}`);
    });

 

# mount 이후 업데이트 될 때는 실행되지 않으려면, 두번째 매개변수 배열를 줍니다.

    //useEffect(함수, []) - 화면이 첫번째 mount에서만 실행됩니다.
    useEffect(()=>{
        console.log('처음만 실행됩니다')
    },[]);

 

#update 이후 - 특정값에 의해 컴포넌트가 업데이트 되고 난 후 발생합니다.

클래스형 componentDidUpdate() 대체

함수형 훅

import { useEffect, useState } from "react";

const HookEffect = () => {
    //useState 훅
    const [name, setName] = useState('');
    const [age, setAge] = useState('');

    const handleName = (e) => {
        setName(e.target.value);
    }
    const handleAge = (e) => {
        setAge(e.target.value);
    }
    
    //useEffect(함수, [state]) - 특정값이 렌더링 될때만 실행됩니다.
    useEffect(() => {
        console.log('age가 변경될 때 실행됩니다')
    },[age, name])

    return (
        <>
            이름:<input type="text" onChange={handleName}/><br/>
            나이:<input type="number" onChange={handleAge}/><br/>

            이름: {name}, 나이 {age} 
        </>
    )

}

export default HookEffect;

 

#unmount직전 - 컴포넌트가 화면에서 사라지기 직전에 호출됩니다.

-화면에 렌더링 되기 직전에 기존화면이 지워집니다. 이 순간을 unmount라고 합니다.

componentWillUnMount() 대체하는 useEffect는 unmount 직전에 실행되는 것입니다.

 

-클래스형 componentWillUnMount() 대체

    //componentWillUnMount() 대체하는 useEffect
    useEffect( () => {
        console.log("name이 변경될 때 return 됩니다");

        //컴포넌트가 unmount될 때 실행됩니다.
        return () => {
            console.log('unmount됩니다'); //렌더링이 그려지면, 기존화면은 지워집니다.
            console.log(`update전 값:${name}`); //state는 직전값이 나옵니다.
        }

    },[name]);

-위에 코드를 보면 return 문 안에 함수에서 내용은

화면이 렌더링 되기 직전에 실행됩니다. 그래서 현재 state값이 변경되었다면 

변경된 state값이 화면에 다시 그려지기전에 return문 안에 내용이 실행되기 때문에.

state값은 변경되었지만 그 이전에 값이 출력됩니다. 

-return문 밖에 출력문은 화면이 렌더링 되고나서 나타납니다.

 

위에 두 차이점을 잘 구분해 놓읍시다.

 

★★ useEffect는 여러개여도 됩니다 ★★
단, 너무 많이 사용하는 것은 권장되지 않습니다.
 
#위에 모든 내용 코드
*App.js
import { useState } from "react";
import HookEffect from "./hook/HookEffect";
import HookQ from "./hook/HookQ";
import HookQ2 from "./hook/HookQ2";
import HookRef from "./hook/HookRef";

const App = () => {

    /*
    1.필수훅
    useState()
    컴포넌트에서 상태값을 제어하는 가장기본이 되는 hook

    useEffect()
    컴포넌트의 라이프사이클(생명주기)를 다룹니다.
    mount, mount이후, state변경될 때, unmount이전에 특정작업을 수행할 수 있습니다.
    */
    const[visible, setvisible] = useState(true); 

    const handleClick = () => {
        // console.log(visible);
        setvisible(!visible); //visible이 가진 값의 반대
    }

    return(

        <>
            {/* effect훅 */}
            <button onClick={handleClick}>{visible ? "숨기기" : "보이기"}</button>
            <br/>
            {visible ? <HookEffect/> : null}

            <hr/>
            {/* ref훅 */}
            <HookRef/>

            <hr/>
            {/* 실습 */}
            <HookQ/>

            <hr/>
            <HookQ2/>
        </>

    )

}

export default App;

 

*HookEffect.js

import { useEffect, useState } from "react";

const HookEffect = () => {
    //useState 훅
    const [name, setName] = useState('');
    const [age, setAge] = useState('');

    const handleName = (e) => {
        setName(e.target.value);
    }
    const handleAge = (e) => {
        setAge(e.target.value);
    }

    //useEffect(함수) - 화면이 mount된 이후에 동작됩니다
    // useEffect(() => {
    //     console.log(`렌더링완료. state값${name}`);
    // });
    

    //useEffect(함수, []) - 화면이 첫번째 mount에서만 실행됩니다.
    useEffect(()=>{
        console.log('처음만 실행됩니다')
    },[]);
    

    //useEffect(함수, [state]) - 특정값이 렌더링 될때만 실행됩니다.
    // useEffect(() => {
    //     console.log('age가 변경될 때 실행됩니다')
    // },[age, name])


    //componentWillUnMount() 대체하는 useEffect
    useEffect( () => {
        console.log("name이 변경될 때 return 됩니다");

        //컴포넌트가 unmount될 때 실행됩니다.
        return () => {
            console.log('unmount됩니다'); //렌더링이 그려지면, 기존화면은 지워집니다.
            console.log(`update전 값:${name}`); //state는 직전값이 나옵니다.
        }

    },[name]);

    //★★useEffect는 여러개여도 됩니다.★★
    //하지만 너무 많이 사용하는 것은 권장하지 않습니다.

    return (
        <>
            이름:<input type="text" onChange={handleName}/><br/>
            나이:<input type="number" onChange={handleAge}/><br/>

            이름: {name}, 나이 {age} 
        </>
    )

}

export default HookEffect;
-----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

#특정 태그에 이름달기 useRef()

 

* useRef() 라는 훅을 무분별하게 막 사용하는 것은 권장하지 않습니다.

 기본적으로 데이터는 state로 관리를 하되 직접적으로 태그를 핸들링 해야하는 경우에만 useRef()를 사용하도록 합시다.

useRef(초기값) 
const 사용할이름 = useRef(null);

 

이벤트를 사용하다 보면 특정 태그에 접근해서 핸들링 하는 경우가 생깁니다.

arrow function에 event 매개변수를 이용해서, 자신 태그에는 접근할 수 있지만, 다른태그는 핸들링 하기가 어렵습니다.

이런경우 useRef() 훅을 이용해서 특정태그에 이름을 지정하고 핸들링 할 수 있습니다.

 

import { useRef, useState } from "react";

const HookRef = () => {
    //사용자 입력값 data, 화면에 출력값 result
    const[form, setForm] = useState({data: '', result:''})

    const handleChange = (e) => {
        setForm({...form,["data"]:e.target.value}) //state값을 객체로 관리할 때 나오는 문장
    }

    //등록
    const handleClick = () => {

        setForm({data:'', result:form.data})

        //Ref의 사용
        // console.log(inputTag);
        // console.log(inputTag.current);
        console.log(inputTag.current.value);
        inputTag.current.focus();

    }

    //특정 태그에 이름달기 useRef()
    //반환된 이름을 사용할 태그에 ref속성에 넣습니다.
    const inputTag = useRef(null);
    // console.log(inputTag); //특정 객체를 주고 안에 current는 속성이 있음을 확인

    return (

        <>
            내용: <input type="text" onChange={handleChange} value={form.data} ref={inputTag}/>
            <button onClick={handleClick}>등록하기</button>
            <br/>
            결과: {form.result}
        </>

    )

}

export default HookRef;

 

 

#useEffect(), useRef() 사용한 실습

 

*HookQ.js

import { useEffect, useRef, useState } from "react";


const HookQ = () => {

    const idTag = useRef(null)
    const pwTag = useRef(null)

    // 1.컴포넌트가 마운트된 이후 한번만 id태그에 포커스를 줍니다.

    useEffect(()=>{
        idTag.current.focus(); //처음 마운트 이후 id태그에 포커싱
    },[])
    
    // 2.id, pw는 state로 관리합니다
    // 로그인버튼 클릭시 공백이라면 공백인 태그에 포커스를 주세요.
    // 로그인버튼 클릭시 공백이 아니라면 경고창으로 id와 pw를 출력해주세요.

    const[login, setLogin] = useState({id:'',pw:''});

   const handleChange = (e) => {
        // console.log(e.target);

        setLogin({...login, [e.target.name]:e.target.value}) //tag의 name 속성 이용.

        // if(e.target.type === 'text'){
        //     setLogin({...login, ["id"] : e.target.value})
        // }
        // if(e.target.type === 'password'){
        //     setLogin({...login, ["pw"]: e.target.value})
        // }
   }

    const handleClick = () => {

        if(idTag.current.value === ''){
            idTag.current.focus(); //id태그
        } else if(pwTag.current.value === ''){
            pwTag.current.focus(); //pw태그
        } else {
            alert(idTag.current.value  + ',' + pwTag.current.value);
        }

        //state로 처리한것.
        // if(login.id === ''){
        //     idTag.current.focus();
        // } else if(login.pw === ''){
        //     pwTag.current.focus();
        // } else {
        //     // console.log(login.pw);
        //     alert(login.id + ',' + login.pw);
        //     // alert(login.id , login.pw); 
        //     //alert는 매개변수가 하나여서 ,로 여러개 표현하는게 안됨. console.log는 매개변수가 가변매개변수여서 ,로 여러개 표현 가능한것.
        // }
    }

    return (
        <div>
            <input type="text" name="id" placeholder="아이디" ref={idTag} onChange={handleChange}/>
            <input type="password" name="pw" placeholder="비밀번호" ref={pwTag} onChange={handleChange}/>
            <button onClick={handleClick}>로그인</button>
        </div>
    )

}

export default HookQ;

 

*HookQ2.js

import { useRef, useState } from "react";


const HookQ2 = () => {

    /*
    실습(할일목록 만들기)
    1.state는 {todo:'', list:[]}로 관리하세요
    2.할일 목록을 작성하고 클릭시, list에 인풋에 입력값을 추가하고 map을 통해서 화면을 그립니다.
    3.등록버튼 클릭 이후에는 ref를 활용해서 input태그에 포커싱을 줍니다.
    */
   
   const[todoList,setTodoList] = useState({todo:'', list:[]});
   const inputTag = useRef(null); //input태그
   
   
   //인풋데이터 핸들링
   const handleChange = (e) => {
       setTodoList({...todoList, [e.target.name]:e.target.value})
    }
    
    //추가하기
    const handleClick = () => {
        
        //여기서 concat대신에 push를 쓰면 안되는 이유는 push는 원본 값에 새로운 데이터를 넣는 것.
        //그래서 push를 쓰면 원본 값이 변경이 되는 것이기 때문에 사용하면 안됨. 
        //concat은 기존 list는 유지, 새롭게 합쳐진 리스트를 반환.
        setTodoList({...todoList,['todo']:'',['list']:todoList.list.concat(todoList.todo)}) 
        
        //inputTag에 포커싱 
        inputTag.current.focus(); 

    }

    //ul태그에 할일 목록 그리기.
    const newList = todoList.list.map((item, index)=> 
        <li style={{listStyle:"none", fontWeight:"600", fontSize:"20"}}key={index}>{index+1}. {item}</li>
    )
    
    return(
        <div>
            <h3>ref로 할일목록 만들기</h3>
            <input type="text" name="todo" placeholder="할일목록" onChange={handleChange} ref={inputTag} value={todoList.todo}/>
            <button type="button" onClick={handleClick}>등록하기</button>
            <ul>
                {newList}
            </ul>
        </div>
    )

}

export default HookQ2;
 

훅의 종류는 이뿐이 아니고 굉장히 많습니다.

 

-부가적인 훅-

 

# useReducer()

 

* useReducer() 란?

useState의 사용을 외부에서 사용할 수 있게 해주는 훅 (state의 변경을 외부에서 제어할 수 있습니다.)

 외부에서 제어할 수 있게됨으로서 state를 다루는 부분을 컴포넌트화 해서 재활용 할 수 있습니다.

 -> 컴포넌트화 해놓은 리듀서를 원하는 컴포넌트에 가져와서 state를 다루는 기능을 그대로 사용하면 됩니다.

 

*useReducer()의 생김새 

 const [현재값,리듀서를 업데이트할수있는 함수] = useReducer(외부에서 사용할 리듀서함수, 리듀서의 초기값)

*state의 값이 이동하는 순서

 

*HookReducer.js 

//리듀서를 사용하면 state값을 외부에서 사용할 수 있다.
//state 값을 외부에서 사용할 수 있다는 것의 장점은 재활용이 가능하다는 것.
//리듀서를 원하는 컴포넌트에 가져와서 기능을 그대로 사용할 수 있음. 

import { useEffect, useReducer } from "react";

//이렇게 리듀서를 사용하면 재활용이 가능함. 
import {myReducer} from './HookReducerComponent';


//리듀서 선언 (현재의 state, 업데이트에 필요한정보-객체임)
//action을 판단해서 state를 체인지
/*
const myReducer = (state, action) => {
    //action은 객체 ★
    console.log(state);
    console.log(action);

    if(action.type === 'increase'){
        state = {value : state.value + 1};
    } else if(action.type === 'decrease'){
        state = {value : state.value - 1};
    } else if(action.type === 'reset'){
        state = {value : 0}
    }
    return state; //return {value: state.value + 1}
}
*/

const HookReducer = () => {
    //const[현재값, 리듀서를 업데이트할수 있는 함수] = useReducer(외부에서 사용할 리듀서함수, 리듀서의 초기값)
    const [state, func] = useReducer(myReducer, {value: 0})
    /*
    useEffect(()=>{
        func({type: 'reset'}); //리듀서를 싱행시키고, MyReducer의 action으로 전달됩니다.
    },[])
    console.log(state)
    */

    const up = () => {
        func({type: "increase"});
    }

    return (
        <>
            <button onClick={up}>증가</button>
            <button onClick={()=> func({type:"decrease"})}>감소</button>
            <button onClick={()=> func({type: "reset"})}>초기화</button>
            결과{state.value}
        </>
    )

}

export default HookReducer;

 

*HookReducerComponent.js

HookReducer.js (위) 와 HookReducer2.js (아래) 의 리듀서를 컴포넌트화 해놓은 파일입니다.

const myReducer = (state, action) => {
    //action은 객체
    // console.log(state);
    // console.log(action);
    if(action.type === 'increase'){
        state = {value : state.value + 1};
    } else if(action.type === 'decrease'){
        state = {value : state.value - 1};
    } else if(action.type === 'reset'){
        state = {value : 0}
    }
    return state; //return {value: state.value + 1}
}


const nameReducer = (state, action) => {

    // console.log(action); //e.target
    
    /*
    if(action.name == 'name'){
        state = {...state, ['name']: action.value};
    } else if(action.name == 'age'){
        state = {...state, ['age']: action.value};
    }
    */ 
    state = {...state, [action.name]: action.value};

    return state;
}

//기본디폴트 모형
export {myReducer, nameReducer};

 

*HookReducer2.js

import { useReducer } from "react";

import { nameReducer } from "./HookReducerComponent";

//리듀서
/*
const nameReducer = (state, action) => {

    // console.log(action); //e.target
    
    
    // if(action.name == 'name'){
    //     state = {...state, ['name']: action.value};
    // } else if(action.name == 'age'){
    //     state = {...state, ['age']: action.value};
    // }
     
    state = {...state, [action.name]: action.value};

    return state;
}
*/

const HookReducer2 = () => {
    //[스테이트, 리듀서제어함수] = useReducer(리듀서, 초기값)
    const[state, func] = useReducer(nameReducer, {name: '', age: ''});

    const {name, age} = state; //스테이트 값 구조분해할당

    return (
        <>
            이름:<input type="text" name="name" onChange={(e) => func(e.target)}/>  
            나이:<input type="text" name="age" onChange={(e) => func(e.target)}/>        
            <br/>
            결과값:{name}<br/>
            결과값:{age}<br/>
        </>
    )

}

export default HookReducer2;