[React] 리액트 훅(Hook)에 대한 개념 정리
Web Frontend Developer

[React] 리액트 훅(Hook)에 대한 개념 정리

이번 포스팅에서는 React Hook에 대해서 개념을 정리해 보려고 한다. 리액트 훅은 v16.8에 새로 도입되었으며 함수형 컴포넌트에서 기존에 라이프사이클 메서드가 없어서 사용할 수 없었던 기능들을 사용할 수 있게 만들어 주었다.

리액트 훅을 도입하게 된 목적은 여러가지가 있다. 먼저 컴포넌트에서 상태관련 로직을 사용할 때 레이어 변화 없이 재사용할 수 있게하기 위함이 첫번째 목적이다. 기존에는 여러가지 레이어로 둘러 쌓여있어서 구조가 복잡했기 때문이다. 두 번째 목적은 기존의 라이프사이클 메서드 기반이 아닌 로직 기반으로 나눌 수 있어서 컴포넌트를 함수 단위로 잘게 쪼갤 수 있다는 이점 때문이다. 그 외에도 클래스 기반 컴포넌트를 지양하고자 하는 목적 등도 있다.

그러면 지금부터 자주 사용하는 리액트 훅 위주로 정리를 해 보려고 한다.

 

useState

useState는 가장 기본적인 Hook이며, 함수형 컴포넌트에서 가변적인 상태를 지닐 수 있게 해 준다. 함수형 컴포넌트에서 상태를 관리해야 할 때 사용한다.

state는 원시타입 뿐만 아니라 객체로 사용할 수도 있다. 여러개의 useState를 사용할 수도 있지만, 이와 같이 하나의 state에 여러 프로퍼티를 추가해서 두 가지 이상의 상태를 관리할 수도 있다.

 

useEffect

useEffect는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. 클래스형 컴포넌트의 componentDidMount와 componentDidUpdate, componentWillUnmount를 합친 형태로 보아도 된다. 이 훅을 통해서 함수형 컴포넌트에서 사이드 이펙트(side effect)를 수행할 수 있는데, 여기서 사이드 이펙트는 데이터 가져오기, 구독 설정, 수동으로 DOM 조작 등을 말한다.

useEffect는 리액트에서 컴포넌트 렌더링 이후 어떠한 일을 수행해야 하는지 말해준다. 우리가 넘긴 함수(effect라고 부름)를 기억했다가, DOM 업데이트 이후 불러온다. 이렇게 컴포넌트 안에서 불러오게 될 경우 effect를 통해 state나 props에 접근할 수 있게 된다. useEffect는 컴포넌트의 첫 번째 렌더링과 그 이후 모든 업데이트에서 수행이 된다.

만약에 useEffect에 설정한 함수를 매번 업데이트마다 수행시키지 않으려면 어떻게 해야 할까? 업데이트 될 때 실행하지 않으려면 함수의 두 번째 파라미터로 비어 있는 배열을 넣어 주면 된다. 그리고 만약 특정 값이 업데이트 될 때만 useEffect를 실행하고 싶다면 두 번째 파라미터 배열에 해당 값들을 넣어주면 된다.

// 첫 렌더링 때만 useEffect 실행
useEffect(() => {
  console.log('마운트 될 때만 실행');
}, []);

// 특정 값(name)이 바뀔 때만 useEffect 실행
useEffect(() => {
  console.log(`${name}이 바뀔 때만 실행`);
}, [name]);

 

컴포넌트가 언마운트 되기 전이나 업데이트 되기 직전에 어떠한 작업을 수행하고 싶다면 useEffect에서 뒷정리 함수(clean-up)를 반환해 주어야 한다. 예를 들면 외부 데이터에 구독(subscription) 설정을 해야 하는 경우 메모리 누수가 발생하지 않도록 clean-up을 해 주어야 한다.

리액트에서 뒷정리를 하는 시점은 컴포넌트가 마운트를 해제하는 시점이다. effect는 렌더링이 실행될 때마다 실행되는데, 그렇기 때문에 다음 effect를 실행하기 전에 이전 렌더링에서 파생된 effect를 정리해 주어야 할 필요가 있다.

 

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트 해주고 싶을 때 사용하는 Hook이다. (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 state를 반환한다. 하윗값이 복잡한 정적 로직을 만들거나, 다음 state가 이전 state에 의존적인 경우 보통 useState 대신 useReducer를 사용한다. 또한 useReducer는 자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화 할 수 있는데, 이것은 Callback 대신 dispatch를 전달할 수 있기 때문이다.

useReducer의 첫 번째 파라미터에는 리듀서 함수를 넣고, 두 번째 파라미터에는 해당 리듀서의 기본값을 넣어준다. 이 Hook을 사용하면 state값과 dispatch 함수를 받아온다. 여기서 state는 현재 가리키고 있는 상태고, dispatch는 액션을 발생시키는 함수이다. dispatch(action)과 같은 형태로 함수 안에 파라미터로 액션 값을 넣어주면 리듀서 함수가 호출되는 구조이다.

초기화를 조금 지연할 수도 있는데 init 함수를 세 번째 인자로 전달하면 된다. 이는 reducer 외부에서 초기 state를 계산하는 로직을 추출할 수 있도록 한다. 또한 어떤 행동에 대한 대응으로 나중에 state를 재설정하는 데에도 유용하다.

 

useMemo

useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있다. 이 Hook은 메모이제이션 된 값을 반환한다. useMemo는 의존성이 변경되었을 때만 메모이제이션 된 값을 다시 계산한다. 이 최적화는 모든 렌더링 시 고비용 계산을 방지하게 해 준다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo로 전달된 함수는 렌더링 중에 실행이 된다. 따라서 렌더링 중에 하지 않는 작업은 이 함수 내에서 할 수 없다. 예를 들어 사이드 이펙트에 대한 처리는 useEffect에서 해 주는 식으로 말이다. useMemo가 성능 최적화를 위해서 사용하는 것은 맞지만, 가능하면 useMemo를 사용하지 않고도 동작할 수 있도록 코드를 작성하는 것이 더 바람직하다.

 

useCallback

useCallback은 메모이제이션 된 콜백을 반환한다. 주로 렌더링 성능을 최적화 해야 하는 상황에서 사용하는데, 이 Hook을 통해서 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다.

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

인라인 콜백과 그것의 의존성 값의 배열을 전달하면 useCallback은 콜백의 메모이제이션된 버전을 반환한다. 그 메모이제이션된 버전은 콜백의 의존성이 변경되었을 때만 변경된다. 이는 불필요한 렌더링을 방지하기 위해 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할 때 유용하다. useMemo와 비슷한 역할을 하고 useCallback은 결국 useMemo로 함수를 반환하는 상황에서 더 편하게 사용할 수 있는 훅이다. 숫자, 문자열, 객체 처럼 일반 값을 재사용하려면 useMemo를 사용하고, 함수를 재사용하려면 useCallback을 사용한다.

 

useRef

const refContainer = useRef(initialValue);

useRef는 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해준다. useRef는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환한다.

본질적으로 useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 상자와 같다. 일반적으로 DOM의 접근하는 방법으로 refs를 익숙해 한다. 만약 리액트에서 ref 객체를 전달한다면 리액트는 모드가 변경될 때 마다 변경된 DOM 노드에 .current 프로퍼티를 설정할 것이다.

ref 속성을 사용하는 것보다 useRef() 훅을 사용하는게 더 유용한데, 그 이유는 useRef()가 순수 자바스크립트 객체를 생성하기 때문이다. useRef()와 {current: ...} 객체를 생성하는 것의 차이점은 useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공한다는 점이다.

하지만 useRef는 내용이 변경될 때 그것을 알려주지는 않는다. .current 프로퍼티를 변형하는 것이 리렌더링을 발생시키지는 않기 때문이다. 만약 리액트가 DOM 노드에 ref를 attach 하거나 detach할 때 어떤 코드를 실행하고 싶다면 callback ref를 사용하는 것을 권장한다.

 

useContext

const value = useContext(MyContext);

useContext는 context 객체(React.createContext)를 받아 그 context의 현재 값을 반환한다. context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트의 가장 가까이에 있는 <MyContext.Provider>의 value prop에 의해 결정된다.

컴포넌트에서 가장 가까운 <MyContext.Provider>가 갱신되면 useContext는 <MyContext.Provider>에게 전달된 가장 가까운 context value를 사용하여 렌더러를 트리거 한다.상위 컴포넌트에서 React.memo나 shouldComponentUpdate를 사용하더라도 useContext를 사용하고 있는 컴포넌트 자체에서부터 다시 렌더링이 된다. 항상 인자는 context 객체 그 자체여야 한다.

useContext를 호출한 컴포넌트는 context 값이 변경되는 항상 리렌더링 된다. 따라서 이 비용이 많이 들면 메모이제이션을 통해 최적화를 할 수도 있다.

 

참고자료

  • 리액트 공식 문서
  • 리액트를 다루는 기술, 김민준(Velopert) 저