본문으로 바로가기

8. Hooks

category React/리액트를 다루는 기술 2021. 1. 22. 14:46

Hook은 클래스형 컴포넌트에 비해 기능이 모자란 함수형 컴포넌트에서도 여러가지 기능을 사용할 수 있게 해준다

8-1. useState


3장에서도 한번 다루었는데 사용할 상태값과 상태값을 관리해 줄 함수를 받아온 뒤 사용할 수 있는 함수

const Counter = ()=>{
    const [value,setValue] = useState(0);
    return(
        <div>
            <h1>{value}</h1>
            <button onClick={()=>setValue(value+1)}>+</button>
            <button onClick={()=>setValue(value-1)}>-</button>
        </div>
    )
}

만약 관리해야할 상태값이 여러개라면 아래와 같이 변수들을 객체로 묶어서 한 state에서 관리해 줄 수도 있다

const Info =()=>{
    const [names, setNames] = useState({
        name : '',
        nickname : '',
    });
    const {name, nickname} = names;

    const handleChange =(e) =>{
        const nextNames = {
            ...names,
            [e.target.name] : e.target.value,
        }
        setNames(nextNames);
    }

    return(
        <div>
            <input type="text" name={'name'} value={name} onChange={handleChange}/>
            <input type="text" name={'nickname'} value={nickname} onChange={handleChange}/>
            <h2> 이름 : {name}</h2>
            <h2> 닉네임 : {nickname}   </h2>
        </div>
    )
}

8-2. useEffect


직전에 배웠던 클래스형 컴포넌트에서 사용할 수 있는 componentDidMount와 componentDidUpdate 메서드를 합친 형태와 비슷하다

위에서 사용한 Info 컴포넌트에 useEffect를 추가해서 컴포넌트가 렌더링(리렌더링)될 때마다 콘솔창에 로그를 표시하도록 해보자

const Info =()=>{
        ...
    useEffect(()=>{
        console.log(`when i effected`);
    });
        ...
}

name과 nickname의 값이 바뀔 때마다 콘솔창에 입력이되는 것을 볼 수 있다

8-2-1. 조건부 useEffect


우리는 useEffect가 조건부로 실행되게 할 수도 있다

위에서는 name과 nickname이 바뀔(리렌더링될) 때마다 출력하게 했지만 이번엔 name의 값이 바뀔 때만 useEffect가 실행되도록 해보자

useEffect(()=>{
        console.log(`When name change`);
    },[]);

이렇게 useEffect의 두번째 파라미터의 (첫번째는 콜백함수) 배열에 name을 추가해주게되면 name의 값이 바뀌었을 때만 useEffect가 실행되게 된다

  • 참고

8-2-2. DidMount


우리는 이제 조건에 따라서 useEffect를 실행시킬 수 있게 되었다

처음에 렌더링될 때만 useEffeect를 실행시키려면 어떻게 해야할까?

useEffect(()=>{
        console.log(`When name change`);
    },[]);

두번째 파라미터로 빈 배열을 주면된다

8-2-3. WillUnMount, WillMount


언마운트되기 직전과 마운트되기 직전에만 실행하고 싶을 때는 어떻게 할까?

// UnMount, Mount
function App(){
    const [visible, setVisible] = useState(true);

    return <div>
        <button onClick={()=>setVisible(!visible)}> { visible ? `hide`: `show`} </button>
        {visible && <Info/>}
    </div>
}
    useEffect(()=>{
        return ()=>{
            console.log(`WillUnMount`);
            console.log(`WillMount`);
        }
    });

useEffect의 리턴값으로 함수를 전달해주면 그 함수가 언마운트,마운트 직전에 호출된다

여기서 또 응용이되는데

이상태에서 두번째 파라미터에 빈 배열을 넣게되면 WillUnMount 즉, 언마운트되기 직전에만 리턴값으로 반환한 함수가 호출된다

8-3. useReducer


useState의 대체 함수입니다. (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환합니다

리액트 공식 문서에서 useState의 대체 함수라고 설명하고 있다

읽어보면 대충 어떻게 쓰는 건지 감이 오지만 역시 확실한 건 써 보는 것

8-3-1. useReducer를 이용한 Counter 컴포넌트


8-1에서 useState로 연습해 본 Counter컴포넌트를 useState대신 useReducer으로 바꿔보자

const ReduceCounter = ()=>{
    const [state,dispatch] = useReducer(reducer, {value:0});
        ...
}

useReducer 훅을 사용하면 state와 dispatch를 받아오는데

  • state는 우리가 흔히 사용해왔던 현재 컴포넌트가 가지고 있는 상태값이고, useReducer의 두번째 인자로 초기화된다
  • dispatch는 액션값을 받아서 state의 상태값을 변경해주는 함수 즉, useState의 set~~ 함수와 비슷한 역할을 수행하는 함수이다
  • dispatch는 useReducer의 첫번째 인자로 받은 콜백 함수와 대응된다

이제 dispatch와 대응된 reducer함수를 알아보자

const reducer = (state,action)=>{
    switch (action.type){
        case 'INCREMENT' :
            return {value : state.value +1};
        case 'DECREMENT' :
            return {value : state.value -1};
        default: return state;
    }
}

reducer함수는 state와 action객체를 파라미터로 받고 action객체의 type값에 따라서 value를 수정하고

불변성을 지키면서 업데이트한 state를 반환한다

const reducer = (state,action)=>{
    switch (action.type){
        case 'INCREMENT' :
            return {value : state.value +1};
        case 'DECREMENT' :
            return {value : state.value -1};
        default: return state;
    }
}

const ReduceCounter = ()=>{
    const [state,dispatch] = useReducer(reducer, {value:0});

    return(
        <div>
            <h1>{state.value}</h1>
            <button onClick={()=>dispatch({type:'INCREMENT'})}>+</button>
            <button onClick={()=>dispatch({type:'DECREMENT'})}>-</button>
        </div>
    )
}

이렇게 useReducer로 작성한 컴포넌트의 장점은 state 업데이트 로직을 컴포넌트 밖으로 뺄 수 있다는 것

8-3-2. useReducer를 이용한 Info 컴포넌트


const infoReducer = (state, action) =>{
    const nextState = {
        ...state,
        [action.name] : action.value
    }
    return nextState;
}

export const ReducerInfo = ()=>{
    const [state,dispatch] = useReducer(infoReducer,{
        name : 'name',
        nickname : 'nickname',
    });
    const handleChange = (e) =>{
        dispatch(e.target);
    }
    return(
        <div>
            <input type="text" name={'name'} value={state.name} onChange={handleChange} />
            <input type="text" name={'nickname'} value={state.nickname} onChange={handleChange}/>
            <h2> 이름 : {state.name}</h2>
            <h2> 닉네임 : {state.nickname}</h2>
        </div>
    )
}

이런식으로 이벤트 핸들러 안에서 dispatch 함수를 불러오고 이벤트 객체를 넘겨줘서 처리도 가능하다

8-4. useMemo


useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있다

숫자를 입력받고 입력받은 수들의 평균을 구하는 Average 컴포넌트를 만들었다

const calAver = (list)=>{
        console.log(`when i excute?`);
    return list.length ? list.reduce((acc,v)=>acc+v)/list.length : 0;
}

const Average = ()=>{
    const [list, setList] = useState([]);
    const [number,setNumber] = useState(0);

        const forRender =()=>{
        return list.map((v,i)=><li key={i}>{v}</li>);
    };
    const handleClick= ()=> {
        const nextList = list.concat([Number(number)]);
        setList(nextList);
    }
    const handleChange =(e)=>{
        setNumber(e.target.value);
    }

    return (
        <div>
            <input type="text" name={'number'} value={number} onChange={handleChange}/>
            <button onClick={handleClick}>등록</button>
            <h1>평균값 : {calAver(list)}</h1>
            <ul>{forRender}</ul>
        </div>
    )
}

이 컴포넌트의 문제점은 number input의 값이 바뀌어서 리렌더링될 때마다 연산을 하지도 않는 calAver가 호출된다는 것

calAver 함수 내부에 console.log로 로그를 띄워놓고 input을 입력해 보면 알 수 있다

const Average = ()=>{
        ...
    const avg = useMemo(()=>calAver(list), [list]);
        ...
    return (
                        ...
            <h1>평균값 : {avg}</h1>
                        ...
}

useMemo는 함수를 받아 메모이제이션하고 배열로 받는 파라미터가 변경되었을 때만 함수를 실행시켜 반환값을 반환한다

배열이 비어있다면 매번 새로운 함수를 만들어 호출

8-5. useCallback


useCallback은 이전에 만들어 놓았던 함수를 재사용한다는 점에서 useMemo와 상당히 유사하다

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

첫번째 인자로 콜백 함수를 받아 메모이제이션 해놓고 두번째 인자로 배열의 값들이 변경될 때 함수를 재생성한다

공식 문서와 리다기를 동시에 보고 있는데 굉장히 의아한 점한가지..

useCallback(fn, deps)은 useMemo(() => fn, deps)와 같습니다.

라고 하는데 이부분이 머리가 꽤 아팠다

이말에 의하면 위에서 만든 Average컴포넌트에 적용하면 같은 결과가 나와야한다

...
//const avg = useMemo(()=>calAver(list),[list]);
const avg = useCallback(calAver(list),[list]);
...

하지만 실행해보면 memo는 log가 출력되지 않는데 callback은 input값이 바뀔 때마다 마구마구 출력되는 것을 볼 수 있다

둘의 콜백 함수가 메모이제이션되는 건 같지만

  • Memo는 리렌더링 시 의존 변수가 바뀌지않으면 실행을 막아주고 바뀌면 메모이제이션된 함수를 실행
  • Callback은 리렌더링 시 의존 변수가 바뀌지않으면 메모이제이션된 함수를 실행, 바뀌면 메모이제이션된 함수를 대체할 새로운 함수를 생성 후 실행

으로 작동하는 듯하다

참고


useMemo는 성능 최적화를 위해 사용할 수는 있지만 의미상으로 보장이 있다고 생각하지는 마세요. (...) 미래에는 메모이제이션된 값들의 일부를 “잊어버리고” 다음 렌더링 시에 그것들을 재계산하는 방향을 택할지도 (...) useMemo를 사용하지 않고도 동작할 수 있도록 코드를 작성하고 그것을 추가하여 성능을 최적화하세요.

리액트 공식 페이지에선 이렇게 써놨으니 개발할 땐 useMemo를 쓰지않고 개발한 뒤 차후에 최적화를 시키도록하자

8-6. useRef


8-6-1. 포커스 바꾸기


위의 Average 컴포넌트에서 버튼을 클릭하면 포커스가 인풋으로 옮겨가게끔 해보자


const AverageRef = ()=>{
        ...
    **const inputEl = useRef(null);**
        ...
    const handleClick= ()=> {
        const nextList = list.concat([Number(number)]);
        **inputEl.current.focus();**
        setList(nextList);
    }
        ...
    return (
        <div>
            <input type="text" name={'number'} value={number} onChange={handleChange} **ref={inputEl}**/>
                        ...
}

8-6-2. 로컬 변수 사용하기


export const LocalVar = ()=>{
    const variable = useRef(1);
    variable.current = 10;
    console.log(variable); // current : 10
    return (
        <div>
        </div>
    )
}

useRef로 컴포넌트 내에 로컬 변수도 선언이 가능하다

이 변수가 바뀌어도 렌더링되지 않는다는 점에 주의하자

'React > 리액트를 다루는 기술' 카테고리의 다른 글

10. 일정 관리 웹 애플리케이션 만들기  (0) 2021.01.30
9. 컴포넌트 스타일링  (0) 2021.01.25
7. 라이프사이클 메서드  (0) 2021.01.20
6. 컴포넌트 반복  (0) 2021.01.17
5. ref: DOM에 이름 달기  (0) 2021.01.15