Hooks in Depth

Contents

  • Introduction
  • Basic Hooks

    • useState
    • useEffect
    • useContext
  • Hooks in-Depth

    • useRef
    • useReducer
    • useMemo
    • useCallback
    • useLayoutEffect
    • useImperativeHandle

Introduction

React v.16.8에 도입된 Hooks는 함수형 컴포넌트에서도 기존 클래스 컴포넌트에서만 사용할 수 있었던 Component 내 local state, life cycle 메소드의 기능을 사용할 수 있게 해줍니다. 이로 인해서, render props, hoc와 같은 조금 복잡한 개념을 사용하지 않고도 stateful logic을 컴포넌트 간 공유할 수 있게 되었고, 여러 문제를 해결할 수 있습니다. Hooks에 대한 규칙 및 Hooks가 어떠한 문제를 해결하는지 좀 더 알아보고 싶으면 저의 다른 포스팅에서도 확인할 수 있습니다. :)

Basic Hooks

useState, useEffect, useContext, useRef 이 네 가지 built-in hooks가 아마도 Hooks 사용 비중의 90% 이상을 차지할 것입니다. 하지만 나머지 작은 비중의 툴이 어떠한 문제를 해결할 수 있는지 확인해 보는 것 또한 중요합니다.

useState

useState는 기존에 클래스 컴포넌트에서만 사용할 수 있던 state를 함수형 컴포넌트에서도 사용할 수 있게 해주며 좀 더 유연한 컴포넌트가 되도록 해 줍니다. 아래의 예에서 h1 태그를 클릭할 때마다 color가 변경됩니다.

Hooks 사용시 가장 중요한 규칙은 반드시 최상위 단에서 호출이 되어야 하고 조건에 따른 호출이 이루어 져서는 안된다는 것입니다.

useEffect

Effects는 함수형 컴포넌트에서 componentDidMount, componentDidUpdate 그리고 componentDidUnmount와 같은 리액트의 lifecycle method와 같은 기능을 사용할 수 있게 하며, 이를 이용해 ajax 요청과 같은 작업을 할 수 있습니다.

아래의 예에서 우리는 시간을 화면에 보여주기 위해서 Effect 안에서 setTimeout을 사용할 것입니다. setTimeout이 callback을 실행하면, callback은 state를 업데이트합니다. 그리고 렌더가 다시 되고, 다시 일정 시간 이후에 다음의 Effect가 실행되도록 합니다.

useEffect의 두 번째 인자로 빈 배열을 전달할 수도 있습니다. 이 경우 React에게 우리의 Effect가 props나 state의 어떠한 값에도 의존하지 않는다고 말하는 것과 같으며, 때문에 리렌더가 발생하지 않습니다.

아래의 경우 매 렌더마다 Effect가 작동해야 하기 때문에 두 번째 인자를 전달하지 않습니다.

위 코드에서 useEffect의 두 번째 인자로 빈 배열을 추가해 보세요. 그러면 리렌더가 되지 않기 때문에 UI 상 시간 또한 변경되지 않습니다.

추가로, useEffect가 또 다른 함수를 반환하고 있습니다. 이 함수는 컴포넌트가 unmount 되기 전 호출이 되는데, 위의 경우는 setTimeout을 clear하고 있습니다. 만약 특정 이벤트의 핸들러를 삭제하고 싶으면, 이곳에서 할 수 있습니다.

useContext

React의 초기 문제점 중 하나로 "data tunneling" 또는 "prop drilling"이라 불리는 문제가 있습니다. 최상단에 있는 컴포넌트에서 한참 아래에 있는 자식 컴포넌트에 데이터를 전달해야 할 경우가 있습니다. 데이터를 전달할 수는 있지만, 중간에 해당 데이터의 존재를 몰라도 되는 컴포넌트까지 거쳐서 가야 할 경우가 생깁니다.

Context API를 사용하면 불필요하게 중간 컴포넌트를 거치지 않고고 데이터를 필요한 컴포넌트에만 전달할 수 있습니다. useContext는 Context object가 인자로 주어지면 데이터를 추출할 수 있습니다.

Level 2, 3, 4는 건너뛰고 LevelFive 컴포넌트에서 최상위 컴포넌트에서 전달한 데이터를 사용하고 있습니다.

Hooks in Depth

useRef

useRef는 가변적 객체를 반환하며 current 속성에 초깃값으로 전달받은 값을 간직하고 있습니다. useRef는 여러 경우에 유용하며, 이 글에서는 두 가지 주요 사용 케이스에 대해 알아보도록 하겠습니다. useRef의 일반적인 사용 케이스 중 하나는 DOM 요소에 명시적 접근을 하는 것입니다.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // 'current' points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

하지만 ref attribute로 사용하는 것 외에도, class의 instance field와 같이 useRef에는 어떠한 가변적인 값을 할당할 수 있습니다.

refs가 왜 유용한지 알기 위해서는 우선 자바스크립트의 closure가 어떻게 작동하는지에 대해 알아야 합니다. 아래의 예시 컴포넌트에서, 유저가 클릭하면 state와 ref의 숫자가 1초 뒤 log 되도록 timeout을 겁니다. 한 가지 유념해야 할 것은 state와 ref의 숫자는 동시에 업데이트가 되고 항상 같다는 점입니다.

하지만, log를 setTimeout에서 1초 지연하기 때문에 새로운 값을 alert를 통해 출력할 때 state와 ref의 값이 다르게 출력됩니다. state는 setTimeout을 처음 호출했을 때의 값이 출력됩니다. closure가 그 값을 지니고 있기 때문입니다. 하지만 ref의 값은 항상 현재의 값을 출력합니다. ref의 값은 React가 전달하는 동일한 object의 값을 출력하기 때문입니다. 같은 객체의 property 값으로 숫자가 할당돼 있고, closure scope의 대상이 아니기 때문에 이 숫자는 항상 최신의 값을 유지합니다.

ref의 이러한 점은 유용한 것일까요? setInterval이나 setTimeout의 ID를 가지고 있다가, 이후 clear 하는 작업에 유용합니다. 또한, 가변적인 state가 있지만, re-render를 원하지 않을 때도 사용할 수 있습니다. useRef는 안의 내용이 변경 되도 렌더가 다시 되지 않습니다.

useReducer

useReducer는 useState를 대신해서 사용할 수 있는 built-in Hook입니다. Redux에 익숙하다면 reducer가 어떻게 작동하는지 이미 알고 계실 거라 생각합니다. useReducer는 Redux 스타일의 reducer를 Hooks 안에서 사용할 수 있도록 해 줍니다.

useReducer는 여러 하위 value가 있는 state logic을 다루거나, 다음 state가 이전 state에 의존성을 가질 때 사용하기 적합합니다.

useMemo

useMemo는 memoized된 값을 반환하면, useMemo 함수는 callback과 dependencies 배열을 인자로 받습니다. useMemo에 전달된 함수는 렌더링 시 호출이 되기 때문에, 일반적인 렌더링 시 하지 않는 작업을 똑같이 useMemo에서 하지 않습니다. 그리고 보통 선제적으로 useMemo를 사용하기보다는 성능적인 문제가 생기면 그때 사용하는 것이 권해집니다.

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

아래의 예에서 재귀로 구현된 fibonacci의 결과를 UI에 출력하고 있습니다. 재귀로 구현된 fibonacci 함수를 호출하는 것은 매우 비싼 작업입니다. 만약 useMemo를 사용하지 않았다면, 타이틀의 색 변경을 위해 h1을 클릭할 때마다 fibonacci 함수를 다시 호출할 것입니다. 하지만 useMemo를 사용했기 때문에 useMemo의 두번째 인자인 배열의 요소가 변경될 때마다 useMemo의 callback이 호출됩니다.

위의 코드에서 useMemo를 사용하지 않고, 클릭해 num을 40 정도로 만든 후 h1 태그를 클릭해보세요. 타이틀의 색 변경이 매우 느릴것입니다.

useCallback

useCallback은 useMemo와 매우 비슷합니다. memoized된 callback을 반환하며, 두 번째 인자로 받은 요소가 변경될 때만 호출이 됩니다.

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

리액트 앱에서 일반적으로 상단에 위치한 컴포넌트에서 변화를 감지하면, 그 하위에 위치한 것들이 모두 렌더가 다시 됩니다. 때때로 일부 컴포넌트가 특별한 이유 없이 렌더가 다시 되면 성능상 이슈가 발생할 수 있습니다.

아래의 예시에서 ExpensiveComputationComponent를 오직 필요할 때만 렌더가 다시 되도록 하고자 합니다. React.memo를 사용했는데, React.memo는 pureComponent와 마찬가지로 이전의 props와 새로운 props를 shallow comparison 한 후, 변화가 있을 때만 렌더를 다시합니다.

이점을 고려하면, ExpensiveComputationComponent에 prop으로 전달하는 함수는 항상 동일해야 합니다. useCallback을 이용해 항상 같은 fibonacci 함수를 전달하도록 하고, count 숫자가 변경될 때만 렌더가 다시 되도록 했습니다.

useCallback을 사용하지 않으면, count가 40 정도 됐을 때 매초 UI 업데이트가 느리게 진행됩니다.

useLayoutEffect

useLayoutEffect는 useEffect와 같지만, 다른 점은 모든 DOM의 조작이 완료된 후 동기적으로 실행이 된다는 것입니다. useLayoutEffect은 componentDidMount, componentDidUpdate와 같은 시점에 작동합니다. useLayoutEffect를 사용해야 할 때는 애니메이션과 같은 효과를 주기 위해 DOM nodes의 측정이 필요한 경우입니다. 아래의 예시에서 textarea를 클릭할 때마다 textarea를 측정합니다. 이것은 렌더를 두 번 하게 하지만 정확한 측정값을 얻을 수 있습니다.

useImperativeHandle

useImperativeHandle은 아마도 직접 사용할 케이스가 많지 않지만, 아마도 우리가 사용하는 라이브러리에서 사용하고 있을 수 있습니다. useImperativeHandle은 ref 사용 시 부모 컴포넌트에 노출되는 instance value를 커스터마이징 할 수 있습니다.

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

<Fancyinput ref={fancyInput} />을 렌더하는 부모 컴포넌트에서 fancyInputRef.current.focus()를 호출할 수 있습니다.

아래의 폼 예시에서, 폼의 input 값이 유효하지 않은 경우 유효하지 않은 첫 번째 필드에 focus가 됩니다. 하지만, ElaborateInput 컴포넌트는 자식 컴포넌트이고 부모 컴포넌트에서 그 안의 요소에 직접 접근을 할 수 없습니다. 어떻게 구현할 수 있을까요?

우리는 useImperativeHandle을 사용할 수 있습니다. useImperativeHandle을 사용하면 부모 컴포넌트에서 useRef를 통해 생성된 객체에 커스텀 메소드를 추가할 수 있습니다. ElaborateInput 컴포넌트 내에는 두 개의 ref가 있습니다. 하나는 컴포넌트 내부에서 생성해 input 태그에 직접 추가한 ref입니다. 다른 하나는 forwardRef를 통해 부모 컴포넌트로부터 전달받은 ref입니다. ElaborateInput 컴포넌트 함수의 두 번째 인자로 ref를 받습니다. 그리고 부모 컴포넌트에서 자식 컴포넌트에서 생성한 커스텀 메소드를 사용할 수 있습니다.

앞서 언급한 바와 같이, 일반적으로 이 Hook을 사용하지 않고 prop과 같은 다른 방식을 사용하는 것이 좋습니다.