안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 공식 문서 요약 - Hook 규칙

피그브라더 2020. 9. 12. 15:56
 

Rules of Hooks – React

A JavaScript library for building user interfaces

reactjs.org

* React 공식 문서를 꼼꼼히 읽으면서 개인적으로 요약한 내용입니다. 잘못된 내용이 있다면 지적 부탁드립니다.

 

1. 컴포넌트의 최상위(Top Level)에서만 Hook을 호출해야 한다.

반복문, 조건문, 혹은 중첩된 함수 내에서 Hook을 호출하면 안 된다. 대신 React 함수형 컴포넌트의 최상위에서만 호출해야 한다. 이 규칙을 따르면 해당 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 여러 개의 Hook이 호출되는 것을 보장할 수 있다. 이는 React가 여러 개의 Hook이 호출되는 경우에도 각 Hook의 상태를 렌더링 간에 제대로 유지할 수 있도록 한다. 이에 대해서는 아래에서 자세히 설명한다.

 

2. 오직 React 함수 내에서 Hook을 호출해야 한다.

일반적인 JavaScript 함수 내에서 Hook을 호출하면 안 된다. 대신 React 함수형 컴포넌트 안에서 호출하거나, 직접 만든 커스텀 Hook 안에서만 호출하도록 하자. 커스텀 Hook과 관련한 자세한 내용은 '나만의 Hook 만들기' 포스팅을 참고하자. 이 규칙을 따르면 해당 컴포넌트의 상태와 관련한 모든 로직들을 소스 코드 상에서 명확하게 보이도록 할 수 있다.

 

3. ESLint 플러그인 (eslint-plugin-react-hooks 패키지)

Hook을 사용할 때 위와 같은 두 가지의 규칙을 반드시 지키도록 강제하고 싶다면 ESLint 플러그인을 프로젝트에 추가하자. npm 혹은 yarn으로 eslint-plugin-react-hooks 패키지를 설치하고 ESLint 설정 파일을 다음과 같이 구성하면 된다. 참고로 create-react-app으로 만들어진 프로젝트에는 이 플러그인이 기본적으로 설정되어 있으므로 추가적인 설정이 필요하지 않다. 그리고 "react-hooks/exhaustive-deps" 설정은 앞선 포스팅에서 설명했듯이 useEffect() 함수를 사용할 때 의존성 배열을 올바르게 지정할 수 있도록 돕는 설정이다.

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",  // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn"  // Checks effect dependencies
  }
}

 

4. React는 어떤 상태가 어떤 Hook에 대응하는지 어떻게 알 수 있을까?

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

 

앞서 설명했듯 한 컴포넌트에서도 Hook을 여러 번 호출할 수 있다. 바로 위 예시처럼 말이다. 그렇다면 React는 어떤 상태가 어떤 Hook 호출에 대응하는지 어떻게 알 수 있을까? 즉, 각각의 Hook 호출이 기억되어 있는 이전의 Hook 상태들 중 어떤 상태를 참조해야 하는지 어떻게 알 수 있을까? 정답은 바로 Hook 호출의 순서이다. 즉 호출의 순서대로 각각의 Hook 호출이 무엇인지 식별하는 것이다. 위 예시의 경우 매 렌더링마다 Hook 호출의 순서가 항상 같기 때문에 올바르게 동작한다. 다음 그림을 보자. Hook의 호출 순서가 매 렌더링마다 동일하다면 React는 지역적으로 기억해둔 상태들을 각각 알맞은 Hook에 대응시킬 수 있다.

// ------------
// First render
// ------------
useState('Mary')  // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm)  // 2. Add an effect for persisting the form
useState('Poppins')  // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle)  // 4. Add an effect for updating the title

// -------------
// Second render
// -------------
useState('Mary')  // 1. Read the name state variable (argument is ignored)
useEffect(persistForm)  // 2. Replace the effect for persisting the form
useState('Poppins')  // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle)  // 4. Replace the effect for updating the title

// ...

 

그렇다면 Hook을 조건문 안에서 호출하면 어떻게 될까? 그러면 조건이 만족되지 않을 때는 해당 Hook이 호출되지 않으므로 이전 렌더링 때와는 Hook 호출 순서가 달라지게 된다. 다음 예시처럼 말이다. 원래 호출되던 useEffect(persistForm)이 호출되지 않았다.

useState('Mary')  // 1. Read the name state variable (argument is ignored)
// useEffect(persistForm)  // 🔴 This Hook was skipped!
useState('Poppins')  // 🔴 2 (but was 3). Fail to read the surname state variable
useEffect(updateTitle)  // 🔴 3 (but was 4). Fail to replace the effect

 

이렇게 되면 React는 각각의 Hook 호출을 잘못 해석하게 된다. useState('Poppins') 호출의 경우 두 번째 호출된 Hook이기 때문에 이전 렌더링에서의 useEffect(persistForm) 호출에 대응되는 것으로 기대할 것이다. 그러나 결과적으로 그렇지 않고, 과거 useEffect(persistForm) 호출에 의해서는 상태 변수가 특별히 저장되지 않았기 때문에 useState('Poppins') 호출을 통해 저장되어 있는 상태 변수를 읽어오려 할 때 문제가 발생한다. 이때부터 Hook의 호출 순서는 하나씩 밀려서 전체적으로 버그를 만들게 된다. 이것이 컴포넌트의 최상위에서만 Hook을 호출해야 하는 진짜 이유이다. 만약 조건부로 이펙트를 수행하고 싶다면 effect 함수 내에 조건문을 넣자. 제공된 lint 플러그인을 잘 사용한다면 이러한 문제들은 전혀 걱정할 필요가 없을 것이다.