Hooks는 자바스크립트 함수지만 두 가지 규칙을 지키며 사용해야 합니다. 이런 규칙은 linter plugin을 통해 자동으로 적용할 수 있습니다.

Only Call Hooks at the Top Level

Hooks를 루프 내부, 조건(condition), 또는 중첩 함수 내부에서 호출하면 안됩니다. Hooks는 언제나 리액트 함수의 top-level에서 호출되어야 합니다. 이 규칙을 지키면 컴포넌트가 렌더링 될 때 마다 hooks가 동일한 순서로 호출되는 것을 보장할 수 있습니다. 그리고 이런 방식은 리액트가 정확하게 여러 hooks의 상태를 보존할 수 있도록 도와줍니다(useState, useEffect 등).

Only Call Hooks from React Functions

Hooks를 일반 자바스크립트 함수에서 호출하면 안됩니다. 대신

  • Hooks를 리액트 함수형 컴포넌트 내부에서 호출합니다.
  • Hooks를 custom Hooks에서 호출합니다.

이 규칙을 따르면 컴포넌트 내부의 stateful logic이 소스코드에서 명확하게 보일 수 있도록 보장합니다.

ESLint Plugin

리액트팀은 위 두 규칙을 강제해주는 eslint-plugin-react-hooks를 제공합니다. 아래와 같은 방식으로 프로젝트에 적용할 수 있습니다.

npm install eslint-plugin-react-hooks --save-dev

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

Explanation

위에서 본 것 처럼 우리는 하나의 컴포넌트에서 여러 상태와 이팩트를 사용할 수 있습니다.

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;
  });

  // ...
}

리액트는 어떻게 특정 상태가 해당하는 특정 useState call을 구분할 수 있을까요? 리액트는 Hooks가 call되는 순서에 의존합니다.

// ------------
// 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

// ...

각 렌더마다 Hooks의 call순서가 동일하다면 리액트는 특정 local state를 연관시킬 수 있습니다. 하지만 만약 호출 순서가 달라진다면 어떻게 될까요?

// 🔴 We're breaking the first rule by using a Hook in a condition
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

위의 특정 조건이 다음 렌더링에서 더 이상 참이 아니게 되었다고 가정합시다.
이제 hooks가 불리는 순서가 달라지게 됩니다.

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

리액트는 두 번째 useState call에서 무엇을 리턴해야 하는지 알 수 없게 됩니다. 리액트는 두 번째 call이 persistForm에 해당하는 effect call이라고 생각하고 있기 때문입니다. 따라서 특정 hooks가 스킵되면 버그가 발생하게 됩니다.

이것이 우리가 Hooks를 항상 컴포넌트의 top-level에서 호출해야 하는 이유입니다. 만약 조건에 따른 동작을 원한다면 Hook 내부에서 해결할 수 있습니다.

useEffect(function persistForm() {
    // 👍 We're not breaking the first rule anymore
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
});

출처

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글