학습 자료

훅의 규칙 — 왜 if 문 안에서 부르면 안 되나


Article

useStateuseEffect를 쓰면서 이런 경고를 봤을 것이다.

콘솔
  • React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.

규칙은 두 줄이다.

규칙무슨 뜻인가
최상위에서만 부른다조건문 · 반복문 · 중첩 함수 안에서 부르지 않는다
React 함수 안에서만 부른다컴포넌트나 다른 훅 안에서만 부른다

외우기는 쉽다. 왜 그런지를 알면 영원히 안 잊는다.

React 는 훅을 이름으로 기억하지 않는다

이런 컴포넌트가 있다고 하자.

function Form() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [agreed, setAgreed] = useState(false);
  ...
}

React는 name, email, agreed라는 이름을 모른다. 그건 우리가 구조분해로 붙인 변수명일 뿐이다. useState에 넘긴 인자는 초기값 하나뿐이었다.

그럼 세 번의 useState 호출을 어떻게 구별할까.

부른 순서로 구별한다.

React 내부에 쌓이는 훅
  • 2번 — useState(false)agreed
  • 1번 — useState("")email
  • 0번 — useState("")name

Hook List

React 는 "0번 훅", "1번 훅" 이라고만 안다. 이름은 우리 것이다

첫 렌더링에서 순서대로 자리를 만들고, 다음 렌더링에서도 같은 순서로 부를 것이라 믿고 그 자리의 값을 꺼내준다.

순서가 바뀌면

조건문 안에 넣어본다.

function Form({ showEmail }) {
  const [name, setName] = useState("");

  if (showEmail) {
    const [email, setEmail] = useState("");
  }

  const [agreed, setAgreed] = useState(false);
}

showEmailtrue일 때는 훅이 세 번 불린다. false가 되면 두 번만 불린다.

showEmail 이 true 에서 false 로 바뀌면
  1. 01

    0번을 부른다

    useState("") → React 가 0번 자리의 값을 준다. name 이다. 정상.

  2. 02

    if 를 건너뛴다

    useState("") 가 실행되지 않는다.

  3. 03

    다음 useState 가 1번이 되어버린다

    우리는 agreed 를 원했는데, React 는 1번 자리를 준다. 그건 `email` 의 값이다.

    const [agreed] = useState(false);  // agreed 에 "" 가 들어온다
  4. 04

    값이 한 칸씩 밀린다

    boolean 자리에 문자열이 들어온다. 11강에서 index 를 key 로 썼을 때와 똑같은 종류의 사고다.

11강의 key 문제와 구조가 완전히 같다. 자리 번호로 기억하는데 내용물이 옮겨갔다.

그럼 조건부로 처리하려면

훅은 항상 부르고, 그 안에서 조건을 처리한다.

function Form({ showEmail }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [agreed, setAgreed] = useState(false);

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      {showEmail && <input value={email} onChange={e => setEmail(e.target.value)} />}
    </form>
  );
}

훅은 셋 다 부르고, 화면에 그릴지 말지를 조건으로 정한다. 7강의 조건부 렌더링이다.

useEffect도 같다. 훅 자체를 조건에 넣지 말고, 안에서 조건을 건다.

useEffect(() => {
  if (!userId) return;
  fetchUser(userId);
}, [userId]);

early return 과 훅

7강에서 early return을 권했는데, 훅과 만나면 함정이 된다.

function Profile({ user }) {
  if (!user) return <Empty />;

  const [isEditing, setIsEditing] = useState(false);
}

user가 없으면 useState가 안 불린다. 순서가 깨진다.

훅을 전부 부른 뒤에 early return한다.

function Profile({ user }) {
  const [isEditing, setIsEditing] = useState(false);

  if (!user) return <Empty />;

  return <div></div>;
}

반복문 안에서도 안 된다

function List({ items }) {
  items.forEach(item => {
    const [selected, setSelected] = useState(false);
  });
}

items의 길이가 바뀌면 훅의 개수가 바뀐다. 같은 이유로 금지된다.

이런 게 필요하다면 컴포넌트를 나눈다. 항목마다 자기 상태를 갖게 한다.

function List({ items }) {
  return items.map(item => <Item key={item.id} item={item} />);
}

function Item({ item }) {
  const [selected, setSelected] = useState(false);
}

8강에서 본 대로 컴포넌트마다 state가 따로 생긴다.

커스텀 훅 — 로직을 이름으로 묶는다

두 번째 규칙이 "React 함수 안에서만"이었다. 다른 훅 안도 포함된다.

이름이 use로 시작하는 함수를 만들면 그 안에서 훅을 부를 수 있다. 이걸 커스텀 훅이라 한다.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}

쓰는 쪽은 한 줄이다.

function Header() {
  const width = useWindowWidth();
  return <p>{width}px</p>;
}

컴포넌트가 아니다. JSX를 반환하지 않고 값을 반환한다. 화면이 아니라 로직을 재사용한다.

컴포넌트와 커스텀 훅

컴포넌트

화면을 재사용

  • 대문자로 시작
  • JSX 를 반환
  • <Button /> 으로 쓴다

커스텀 훅

로직을 재사용

  • use 로 시작
  • 아무 값이나 반환
  • useWidth() 로 부른다

이름을 use로 시작해야 하는 이유는 린트 도구가 알아보게 하기 위해서다. getWindowWidth라고 지으면 린트가 그냥 함수로 보고 훅 규칙을 검사하지 않는다.

언제 커스텀 훅으로 빼나

4강에서 컴포넌트를 나누는 기준을 이야기했다. 같은 감각이다.

빼야 할 신호
  1. 01

    같은 useEffect 를 두 곳에서 쓴다

    복사해 붙이고 있다면 훅으로 뺀다.

  2. 02

    이름을 붙일 수 있다

    useDebounce, useLocalStorage 처럼 이름이 자연스럽게 떠오르면 그건 독립된 로직이다.

  3. 03

    컴포넌트가 훅 코드로 뒤덮였다

    화면을 그리는 코드보다 로직이 길다면 분리한다. 컴포넌트는 무엇을 그리는지만 보이게.

정리하면

  • React는 훅을 이름이 아니라 순서로 기억한다. 이게 모든 규칙의 근거다.
  • 그래서 훅은 항상 같은 순서로, 같은 횟수만큼 불려야 한다.
  • 조건문·반복문·중첩 함수 안에서 부르지 않는다. 훅을 부른 뒤 안에서 조건을 건다.
  • early return은 훅을 전부 부른 뒤에 한다.
  • 항목마다 상태가 필요하면 컴포넌트를 나눈다.
  • use로 시작하는 함수를 만들면 커스텀 훅이다. 화면이 아니라 로직을 재사용한다.
  • 커스텀 훅은 코드를 공유할 뿐 값을 공유하지 않는다.

다음 강의에서는 렌더링과 무관한 값을 담는 useRef와, 그것으로 DOM을 직접 다루는 법을 본다.

훅의 규칙 — 왜 if 문 안에서 부르면 안 되나 — 디코드랩(DCODELAB)