React Hooks

React Hooks는 React 16.8에 새로 추가된 기능이다. React 클래스 컴포넌트를 작성하지 않고도 상태 및 리액트의 기능을 사용할 수 있게 된 것이다. 물론 리액트 팀에서도 밝혔지만, 리액트에서 클래스를 제거할 계획은 없기 때문에 훅이 도입되었다고 해서 모든 것을 마이그레이션 할 필요는 없다.

Golden Rule: Don't use them until absolutely necessary!

React Hooks

위에서도 말했지만, React Hooks가 도입됨으로서, 리액트의 클래스 컴포넌트를 작성하지 않고도 상태 및 리액트의 기능을 사용할 수 있게 됐다. 그렇다고 React Hooks는 기존 React의 기본 개념에 대한 부분을 대체하진 않는다. 다만 이미 알고 있는 props, state, context, ref, life cycle 등의 개념을 보다 직관적인 API를 제공해 준다.

[참고 - 공식문서]

useState

클래스에서 상태 관리를 위해 사용했던 this.setState() 대신 사용 가능한 훅이다. 함수 컴포넌트에서 상태를 쉽게 처리할 수 있도록 해준다. 또한 여러 사용도 가능하다.

function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  const tick = () => {
    setSeconds(state => state + 1)
  }
  
  return ...
}

useEffect

함수 컴포넌트에서 데이터 패칭, 구독, DOM 변경 등의 사이드 이펙트 작업을 수행할 때 사용한다. 이는 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount와 동일한 목적으로 사용되며, 클래스 컴포넌트의 경우 3가지 함수로 제공되나, 함수 컴포넌트에서는 useEffect라는 단일 API로 제공된다.

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

function Example({ friend }) {
  const [count, setCount] = useState(0);
  const [data, setData] = useState();
  
  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  }, []);
  
  // 데이터 패칭 
  useEffect(() => {
    axios.get('https://...').then(res => setData(res))  
  },[]);
  
  // 구독과 취
  useEffect(() => {
    const handleStatusChange = () => {...};
    
    ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange);
    };
  }, []);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );

클래스 컴포넌트의 함수들과는 다르게 useEffect 또한 여러번 사용 가능하며, 이로 인해 관심사에 따른 사이드 이펙트의 분리를 만들 수 있다. 또한 내부에서 선택적으로 함수를 반환하여 Clean up하는 방법을 지정할 수 있다.

만약 조건부로 효과를 발생시키고 싶다면, useEffect의 두번째 인자에 있는 종속성 배열에 종속성을 추가하면 된다. 여기에 추가된 값 중 하나라도 변경되는 경우에도 Hook이 실행된다.

memo

React.memo는 고차 컴포넌트(HOC)이다.

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

만약 컴포넌트가 **동일한 props**를 받아 동일한 결과를 렌더링하는 경우, React.memo를 사용하여 몇몇 경우에서 메모화된 결과를 이용하여 성능 향상을 이룰 수 있다. 이 경우 React는 컴포넌트 렌더링을 건너 뛰고 마지막으로 렌더링된 결과를 재사용한다.

이때 React.memo는 오직 props의 변화만 얕은 비교(shallow compare)를 한다. 문서에도 나와있지만 이 함수는 성능 최적화 용도로만 사용해야지, 렌더링 방지용으로는 사용하지 말자. 버그가 발생할 수 있으니 정말 앱이 느려지는 것을 발견하거나 아주 많은 목록을 렌더링할 때만 고려해 보고 도입하자.

+ 실제로 메모를 사용하여 성능 향상을 느낄 수 있는 부분은 상당히 적지만, 또 하나의 장점이 존재한다. 바로 props를 비교 함수를 통해 직접 비교하여 컴포넌트의 렌더링 시기를 정확하게 선택할 수 있다는 점이다. 물론 이것을 위해 memo를 쓰진 말자. 그냥 참고용이다.

고차 컴포넌트(HOC)란?

고차 컴포넌트란 컴포넌트를 받아서 새 컴포넌트를 반환하는 함수를 말한다. 컴포넌트의 로직을 재사용하기 위해 사용하는 기술이며, React에서는 일반적으로 HOC에는 접두사로 with를 많이 사용한다.

이보다 자세한 내용은 공식문서를 참고하자.

useMemo

useMemo의 경우 React 컴포넌트 내부에서 사용되며, 메모된 값을 반환한다. useMemoReact.memo와 달리 종속성을 처리한다. 즉 React.memo는 두번째 인자로 비교 함수를 받지만, useMemo의 경우 함수 대신 종속성 배열을 받아, 종속성 배열의 변경 사항을 체크하여 값이 변경된 경우에만 함수를 다시 실행하며 새 메모화된 값을 가져온다.

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

메모제이션이란?

함수가 실행되면 결과를 얻기 위해 다시 함수를 실행하는 것이 아니라, 처음 실행했을 때의 결과를 기억한다는 의미이다. 그리고 동일한 인자로 함수가 실행된다면 메모한 결과값을 반환한다.

useCallback

값을 메모하는 useMemo와 달리 함수를 메모한다. useMemo의 경우 인자를 받아 값을 반환하는 비용이 많이 드는 코드가 있는 경우 사용하여, 렌더링 간이 계산 비용이 많이 드는 코드를 다시 실행하지 않고 해당 값을 계속 참조할 수 있다.

반면 useCallback은 렌더링 간에 지속되는 함수가 필요한 경우 함수의 해당 인스턴스를 기억할 수 있게 하기 위해 필요한다. 이는 컴포넌트 밖에서 함수를 만드는 것과 비슷하다고 생각할 수 있으며, 코드가 리렌더링될 때, 함수도 유지되게 된다.

아래 예제를 보면서 차이점이 무엇인지 생각해 보자.

const [seconds, setSeconds] = useState(0);

const nextSeconds1 = useMemo(() => seconds + 1, [seconds]);
const nextSeconds2 = useCallback(() => seconds + 1, [seconds]);

여기서 각각 메모되고 있는 값은 무엇일까?

nextSeconds1에서 useMemo가 기억하고 있는 것은 1이며, nextSeconds2에서 useCallback이 기억하고 있는 것은 () => seconds + 1이다. nextSeconds1은 종속성이 변경될때까지 함수의 결과 값을 기억하고 반환할 것이다. nextSeconds2는 종속성이 변경될때까지 함수를 재생성하지 않으며 제공한 함수를 기억할 것이다.

useCallback의 참조 동등성

참조 동등성이란 쉽게 말해 () => {} === () => {} 의 결과는 참조 동등하지 않기 때문에 false이지만, const func = ()= > {}라고 정의하고 func === func를 비교한다면 참조 동등하기 때문에 true의 결과가 나오는 것을 말한다.

useCallback 또한 렌더링 간의 참조 동등성을 제공한다.

useMemo와 useCallback은 항상 성능이 향상될까?

만약 위의 문장이 명제라면, React에서는 저 두 함수를 기본값으로 사용했을 것이다. 하지만 그렇지 않다. 왜냐하면 함수나 값을 메모하는 것과 관련된 코드와 업데이트가 필요한지 비교하는 코드는 훅에 전달하는 자체 함수의 코드보다 비용이 훨씬 크고 비쌀 수 있기 때문이다.

useLayoutEffect

useEffect와 대부분의 기능이나 문법은 동일하지만, 다른 점은 useLayoutEffect의 경우 DOM의 변형이 끝난 후에 동기적으로 실행된다.

대부분의 모든 경우에서는 useEffect를 사용하면 되나, 브라우저가 페인트하기 전에 직접 DOM노드를 조작해야 한다면 그때 사용을 고려해 보자.

useImperativeHandle

useImperativeHandle의 경우 forwardRef와 같이 사용되며, 부모 컴포넌트에서 ref를 사용할 때 노출될 인스턴스 값을 커스터마이징하기 위해 사용된다.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

useReducer

useState에 대한 대안이다. 복잡한 상태 로직을 가질때 사용하면 유용하다. redux와 거의 동일하기 때문에 많은 설명은 공식 예제 코드로 대신하겠지만, 중요 차이점은 useReducer은 컴포넌트와 해당 컴포넌트의 자식의 컨텍스트로만 제한된 다는 것이다(redux는 전역 저장소를 전체 앱에서 접근할 수 있다).

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

Last updated