개발/React

리액트 추가 Hook 살펴보기

차얀 2022. 2. 8. 18:35
    목차

리액트 훅이란?

함수형 컴포넌트를 클래스형 컴포넌트처럼 기능을 구현할 수 있게 해주는 기능입니다.

즉, 기존에 단순 view 역할밖에 수행하지 못했던 함수형 컴포넌트에서 상태를 관리할 수 있고(useState),

라이프 사이클에 따른 동작(useEffect)를 구현할 수도 있게 해 줬습니다.

 

이번 글에선 리액트 공식문서에서 보여주는 기본 Hooks가 아닌, 추가 Hooks에 대해 알아보겠습니다.

 

 

 

1. useReducer

useState의 대체 함수로, state를 reducer의 형태로 관리하게 해 줍니다.

즉 (state, action)이 주어지면, 주어진 action에 따라 state를 newState로 만들어 반환해줍니다.

 

// useReducer 사용 예제
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>
    </>
  );
}

 

간단한 기능에 대해선 useState를 사용하는 게 낫지만, 보다 복잡한 상태 관리에 대해 useReducer를 사용하면

reducer 메서드만을 통해 state 변화를 관리할 수 있습니다.

 

 

 

2. useCallback

렌더링 성능 개선을 위해 제공되는 훅으로,

리렌더링 될 때마다 특정 함수를 새로 만들지 않고 재사용하기 위해 사용하는 훅입니다.

shouldComponentUpdate를 사용해 참조하고 있는 값이 동일하면,

앞서 메모리라이즈 되어있는 콜백 함수를 반환합니다.

 

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

 

 

 

3. useMemo

렌더링 성능 개선을 위해 제공되는 훅으로,

앞서 useCallback이 함수에 대한 최적화를 제공하듯, useMemo는 값에 대한 최적화를 제공합니다.

즉, 참조하고 있는 값이 동일하면, 앞서 메모리라이즈 되어있는 값을 반환합니다.

리렌더링마다 수행되는 고비용 연산을 방지해줍니다.

 

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

 

추가로 공식문서에서 useMemo를 너무 신뢰하지 말라고 적혀있습니다.

이는 해당 기능이 deprecate 되거나 오프 스크린 컴포넌트에 대한 메모리 해제로의 방향성을 가짓수 있기에, 

useMemo를 사용하지 않고도 동작할 수 있도록 코드를 작성하고 최적화하기를 권장하고 있습니다.

 

 

 

4. useRef

크게 두 가지 경우에서 사용됩니다.

 

우선, DOM 요소에 직접 접근하기 위해서 사용됩니다.

리액트는 사용자가 DOM 요소에 직접 접근해 조작하는 것을 지양하지만

결국 DOM을 직접 참조해야 되는 경우가 존재합니다. (ex. input에 focus를 적용하기)

 

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 에서 요소를 담고 있습니다.
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

 

다음으로, 리렌더링과 무관한 값을 가지고 있기 위해 사용됩니다.

timer와 같은 정보를 가지고 있기에 유용합니다.

 

function AppWithTimer() {
  const [number, setNumber] = useState(0);
  const timerId = useRef(0);
  
  useEffect(() => {
  	timerId.current = setInterval(() => setNumber(number + 1))}, 1000);
  }, [];
  
  const claerTimer = () => clearInterval(timerId.current);
  ...
}

 

 

 

5. useImperativeHandle

부모 컴포넌트에서 forwardRef 방식으로 자식 컴포넌트에 전달해준 ref 조작 로직을
해당 자식 컴포넌트에서 구현할 수 있게 해 줍니다.

이를 통해, 자식 컴포넌트의 ref에 저장되어있는 값(DOM 등)을 외부에서 직접 조작하지 않아,

자식 컴포넌트의 상태 조작 로직을 격리할 수 있습니다

 

const ChildInput = React.forwardRef(props, ref) {  // 부모 컴포넌트에서 ref를 내려준다.
  const inputRef = useRef();
  
  
  useImperativeHandle(ref, () => ({
   // 부모 컴포넌트는 단순히 ref.current.focus로 접근해 사용한다.
    focus: () => {
      // 부모 컴포넌트는 이 내부 로직를 알지 못한다.
      inputRef.current.focus();
    }
  }));
  
  return <input ref={inputRef} ... />;
  
}


// 부모 컴포넌트
const ref = useRef();
...
ref.current.focus();  // 자식 컴포넌트에서 구현한 로직을 사용한다.
...
<ChildInput ref={ref} />

 

 

 

6. useLayoutEffect

useEffect Hook은 DOM의 레이아웃 배치와 페인트가 종료된 후 호출합니다.

그래서 보통 useEffect를 통해 변화되는 state의 경우,

화면에 초기 state가 출력된 후에 변화되는 현상을 본 적이 있을 겁니다.

 

useLayoutEffect는 이런 문제를 해결하기 위해 생겨났고,

useEffect와 동작이 동일하되, DOM 변경 후 페인트 되기 전에 실행됩니다.

즉, 렌더링 과정에서 동기적으로 작동하게 됩니다.

 

렌더링 순서 비교
useEffect : DOM update > Painting => useEffect
useLayoutEffect : DOM update => useLayoutEffect => Painting 

 

그렇다고 무조건 useLayoutEffect를 사용하는 건 지양해야 합니다.

만약 useLayoutEffect에서 수행되는 내용이 오래 소요될 경우,

사용자들은 페인트 되지도 않은 화면을 보고 있어야 합니다.

(useEffect를 사용해 loading View를 렌더링 후 오래 소요되는 내용을 실행하는 게 더 적절합니다)

 

 

 

7. useDebugValue

React DevTools에서 사용자 Hook label을 표시하는 데 사용됩니다.

 

useDebugValue(isOnline ? 'Online' : Offline');