React를 다루는 기술 09 - Hooks

6 minute read

Hooks

Hooks는 React 16.8버전에 도입된 기능으로 함수형 컴포넌트에서 할 수 없었던 상태관리를 지원하는 기능이다.

useState

함수형 컴포넌트에서 state를 사용할 수 있게 해주는 함수.

import React, { useState } from 'react';

const App = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>
        현재 카운터 값은 <b>{value}</b>입니다.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  )
}

위와 같이 useState를 import하여 사용하며 인자로 넣은 값은 해당 state의 기본값이 된다.
useState는 호출 시 1개의 변수와 1개의 함수를 반환하는데 이는 각각 state변수와 해당 변수의 상태를 설정하는 함수이다.
해당 함수에 파라미터를 넣어서 호출하면 state의 값을 변경할 수 있다.

기존 클래스 컴포넌트에서의 state는 하나의 Map에 담아서 여러개의 state를 정의했지만 useState는 한 번에 1개의 state만 정의할 수 있다.
때문에 여러개의 state를 정의하기 위해선 여러번 사용해야한다.

useEffect

useEffect는 함수형 컴포넌트에서 클래스 컴포넌트의 라이프사이클 함수를 사용하는 것과 같은 상태관리를 할 수 있게 해주는 함수이다.

import React, { useState, useEffect } from 'react';

const App = () => {
  const [value, setValue] = useState(0);
  const [nickName, setNickName] = useState('');

  useEffect(() => {
    console.log('렌더링이 완료되었습니다.');
    console.log({
      name,
      nickName
    })
  });

  const onChangeValue = e => {
    setValue(e.target.value);
  }

  const onChangeNickName = e => {
    setNickName(e.target.value);
  }

  return (
    ...
  )
}

위의 값을 실행할 경우 컴포넌트가 마운트 될 때 useEffect 내의 함수가 실행되어 콘솔에 값이 표시되고 state값이 update되어 재렌더링 될 때도 실행된다.
만약 useEffect를 업데이트가 아닌 마운트 시에만 실행되게 하고 싶다면 2번째 파라미터로 빈 배열을 넣어주면 된다.

  useEffect(() => {
    console.log('렌더링이 완료되었습니다.');
    console.log({
      name,
      nickName
    })
  }, []);

특정 값이 업데이트 될 때만 실행되게 하고 싶다면 위의 배열에 해당되는 변수를 넣어주면 된다.

  useEffect(() => {
    console.log('렌더링이 완료되었습니다.');
    console.log({
      name,
      nickName
    })
  }, [name]);

이렇게 입력하면 name state변수가 변경될 때만 useEffect의 함수가 호출된다.
useEffect를 이용하여 컴포넌트가 언마운트 될 때 기능이 호출되게 하기 위해서는 해당 기능을 반환해주면 된다.

  useEffect(() => {
    console.log('렌더링이 완료되었습니다.');
    console.log({
      name,
      nickName
    })

    return (
      console.log('return');
    )
  }, []);

단순히 언마운트 될 때만이 아니라 useEffect를 통해 실행한 함수의 클린업 함수를 실행해야 할 때도 해당 함수를 반환해주면 된다.

useReducer

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트하고 싶을 때 사용한다.
Reducer는 현재 상태(state)와 업데이트를 위해 필요한 정보를 담은 액션(action)을 전달받아 새로운 상태를 반환하는 함수이다.

import React, { useReducer } from 'react';

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

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

  return (
    <div>
      <p>
        현재 카운터 값은 <b>{state.value}</b>입니다.
      </p>
      <button onClick={() => dispatch({type: 'INCREMENT'})}>+1</button>
      <button onClick={() => dispatch({type: 'DECREMENT'})}>-1</button>
    </div>
  )
}

useReducer 함수에는 첫번째 인자로 reducer함수를 두번째 인자로 state 초기값을 넣어준다. state변수와 해당 변수의 상태관리함수를 반환했던 useState와 달리 useReducer는 state와 dispatch를 반환하며 state는 useReducer에서 2번째 인자로 넣었던 값이며 dispatch는 action을 발생시키는 함수이다.

이러한 reducer를 사용하는 가장 큰 이유는 컴포넌트의 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 점이다.
이 reducer를 통해 여러개의 input을 useState보다 더 편하게 관리할 수 있다.
정의해야 할 state가 늘어날 때마다 useState의 갯수도 늘어나는 것이 useState의 단점이다.
이를 useReducer를 이용하면 클래스 컴포넌트에서 name을 이용해 각각 관리하던 방식으로 관리가 가능하다.

import React, { useReducer } from 'react';

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value
  };
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, { value: 0, name: '' }); 
  const { value, name } = state;
  const onChange = e => {
    dispatch(e.target);
  }

  return (
    <div>
      <div>
        <input name="value" value={value} onChange={onChange} />
        <input name="name" value={name} onChange={onChange} />
      </div>
      <div>
        <div>
          <b>값:</b> {value}
        </div>
        <div>
          <b>이름:</b> {name}
        </div>
      </div>
    </div>
  )
}

기본적인 원리는 클래스 컴포넌트에서 하던 것과 동일하다.
하나의 state 변수에 input의 name값을 key값으로 한 map을 등록하고 해당 값과 input값을 연결시켜준다.
그리고 값이 변경될 때마다 reducer함수를 호출하여 action 인자로 해당 DOM을 넣어준다.
이는 useReducer의 action인자로 어떠한 값을 넣어주건 상관없다는 점을 이용하여 DOM을 넘겨준 것으로 인자를 받은 reducer함수가 state를 새로 생성하는 것으로 마무리된다.

useMemo

useMemo는 컴포넌트 내부에서 발생하는 연산을 최적화하기 위한 함수이다.

import React, {useState} from 'react';

const getAverage = (numbers) => {
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  
  return sum / numbers.length;
}

const App = () => {
  const [ list, setList ] = useState([]);
  const [ number, setNumber ] = useState('');

  const onChange = e => {
    setNumber(e.target.value);
  }
  const onInsert = e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  }

  return (
    ...
    <div>
      <b>평균값 : </b> {getAverage(list)}
    </div>
  )
}

위의 코드는 input에 숫자를 넣고 등록 버튼에 누르면 해당 숫자가 하나하나 등록이 되며 등록된 숫자들의 평균을 표시해주는 코드이다.
이 코드는 잘 작동하지만 등록버튼을 누르지 않고 input의 값이 변경되기만 해도 getAverage함수가 호출된다는 점이다.
이는 state가 변경됨으로써 재렌더링이 이루어지는 것으로 React의 당연한 성질이지만 우리는 등록버튼을 누르지 않을 때는 그런 동작을 하길 원하지 않는다.
이러한 불필요한 동작을 막기 위해서 useMemo를 사용한다.

import React, {useState, useMemo} from 'react';

const getAverage = (numbers) => {
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  
  return sum / numbers.length;
}

const App = () => {
  const [ list, setList ] = useState([]);
  const [ number, setNumber ] = useState('');

  const onChange = e => {
    setNumber(e.target.value);
  }
  const onInsert = e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  }

  const avg = useMemo(() => {
    getAverage(list)
  }, [list]);

  return (
    ...
    <div>
      <b>평균값 : </b> {avg}
    </div>
  )
}

이렇게 getAverage 함수를 useMemo 함수내에 넣고 useEffect와 같이 2번째 인자로 배열을 넣어주면 된다.
이로써 getAverage 함수는 list 변수가 업데이트 될 때만 호출되게 되어 불필요한 행동을 줄일 수 있게 되었다.

useCallback

useCallback 함수의 역할은 useMemo 함수와 동일하게 불필요한 행동을 줄이는 것이다.
다만 useMemo의 경우 특정 값을 재사용할 때 쓰이고 useCallback 함수는 특정 함수를 재사용할때 사용한다는 점에서 차이가 있다.

방금 useMemo를 설명하면서 작성한 코드에서 onChange함수와 onInsert함수를 정의해주었는데 이 경우 컴포넌트가 리렌더링 될 때마다 이 함수들이 재생성된다.
일반적인 경우 이로 인해 눈에 띄는 성능저하는 없지만 컴포넌트의 렌더링이 자주 발생하거나 컴포넌트의 갯수가 많을 경우 이러한 부분의 최적화가 필요하다.

import React, {useState, useCallbac} from 'react';

const getAverage = (numbers) => {
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  
  return sum / numbers.length;
}

const App = () => {
  const [ list, setList ] = useState([]);
  const [ number, setNumber ] = useState('');

  const onChange = onCallback(e => {
    setNumber(e.target.value);
  }, []); // 컴포넌트가 마운트 될 때만 생성

  const onInsert = onCallback(e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  }, [list, number]); // list, number state의 값이 업데이트 될 경우 생성

  const avg = useMemo(() => {
    getAverage(list)
  }, [list]);

  return (
    ...
    <div>
      <b>평균값 : </b> {avg}
    </div>
  )
}

위처럼 함수를 onCallback 함수로 감싸주고 두번째 인자로 배열을 넣어 어느 타이밍에 생성할지를 정해주면 된다.
여기서 주의할 점은 해당 함수가 내부에서 참조하는 값이 있을 경우 반드시 그 값을 배열에 넣어주어야 한다는 점이다.
onChange함수의 경우 참조값 없이 상태만 변경시키고 있기 때문에 상관없지만 onInsert의 경우 list, number 변수를 참조하고 있다.
이 상태에서 둘 중 하나를 빼먹을 경우 해당 state가 변경되어도 함수가 재생성되지 않아서 과거의 값을 참조하게 되어 문제가 발생한다.

useRef

클래스 컴포넌트에서 DOM에 직접 접근하기 위해 사용했던 ref의 함수형 컴포넌트 버전이다.
사용법은 동일하며 변수를 선언할 때 useRef 함수를 호출해주기만 하면 된다.

import React, {useState, useCallback, useRef} from 'react';

const getAverage = (numbers) => {
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  
  return sum / numbers.length;
}

const App = () => {
  const [ list, setList ] = useState([]);
  const [ number, setNumber ] = useState('');
  const inputEl = useRef(null);

  const onChange = onCallback(e => {
    setNumber(e.target.value);
  }, []); // 컴포넌트가 마운트 될 때만 생성

  const onInsert = onCallback(e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEl.current.focus();
  }, [list, number]); // list, number state의 값이 업데이트 될 경우 생성

  const avg = useMemo(() => {
    getAverage(list)
  }, [list]);

  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl}>
      <button onClick={onInsert}>등록</button>
    </div>
    <div>
      <b>평균값 : </b> {avg}
    </div>
  )
}

위 코드처럼 useRef함수로 inputEl이라는 변수를 선언해주고 이를 원하는 DOM에 ref로 등록한다.
그 후 함수에서 호출하여 사용하면 된다.

출처자료

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=209354690 (리액트를 다루는 기술(개정판))

Categories:

Updated: