C.W.K.
Stream
Lesson 07 of 07 · published

Rules of Hooks — 그리고 use() 가 드디어 풀어준 것

~13 min · rules-of-hooks, use, react-19

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
Rules of Hooks 가 엄격했던 건 reconciler 가 그게 필요했기 때문. React 19 의 use() 는 깨지지 않고 휘게 설계된 첫 hook. 어떤 규칙이 여전히 적용되고 어떤 규칙이 정확히 적용 안 되는지 아는 게 이 레슨의 요점.

클래식 두 규칙

  1. Hook 을 상단에서 호출. 조건 안 아님, 루프 안 아님, nested 함수 안 아님. Hook 이 매 render 같은 순서로 돌아야.
  2. Hook 을 React 함수에서 호출. 함수 컴포넌트 또는 커스텀 hook (이름이 use 로 시작하는 함수) — 평범한 이벤트 핸들러나 유틸 함수에서 절대 아님.

이유: React 가 hook state 를 호출 순서로 추적. 본인 함수의 첫 hook useState(0) 가 이 컴포넌트 인스턴스에 영구적으로 '슬롯 0'. 조건이 hook 스킵하면 다음 render 가 다른 슬롯 매핑 봄 — 한 hook 의 state 가 다른 hook 으로 들어감. 시스템 전체가 조용히 깨짐.

use() 가 뭐

Promise 또는 Context 의 값을 인라인으로 읽는 React 19 hook. Context 넘기면 useContext 처럼 보임 (같이 동작), 다만 돌파구는 조건부 + 루프 안 에서 호출 가능하다는 것.

use() 가 특별한 이유

React 가 재설계되어 use() 의 호출 지점이 reconciler 에 안 중요. Suspense 를 resume 메커니즘으로 사용: use() 가 pending promise 만나면 React 가 컴포넌트 unwind 하고 promise resolve 시 재실행. 다른 hook 들의 동력인 '슬롯 추적' 이 적용 안 됨.

그래서 다음이 가능:

if (someCondition) {
  const data = use(somePromise); // OK!
}

또는:

for (const promise of promises) {
  const value = use(promise); // OK!
}

Trade-off

use() 는 위에 Suspense 경계 필요 (promise 용) + 호출 컴포넌트가 promise resolve 시 처음부터 재실행. 그 재실행이 비용. 대부분 데이터 페칭엔 승리 — 코드가 위에서 아래로 읽히고 Suspense 가 로딩 상태 선언적으로 처리. Track 5 에서 운동.

다른 hook 들은 여전히 규칙 따름. useState, useEffect, useRef, useContext, useReducer, 커스텀 hook — 다 여전히 상단에서 무조건 호출. 이 레슨 읽고 hook 을 조건 안에 숨기기 시작하지 마. use() 만 완화야.

Code

클래식 규칙이 금지하는 것 (설명 포함)·tsx
// WRONG — 조건 안의 hook.
function Broken({ showCounter }: { showCounter: boolean }) {
  if (showCounter) {
    const [count, setCount] = useState(0); // Rules of Hooks 위반
    return <p>{count}</p>;
  }
  return null;
}

// 패턴 fail 이유: 첫 render (showCounter=true) 가 useState 를 슬롯 0 에
// 둠; 다음 render (showCounter=false) 가 hook 스킵 → 슬롯 0 이 이제 비어있음
// — React 의 tracker 깨짐.

// RIGHT — hook 은 상단, 조건은 return 에.
function Fixed({ showCounter }: { showCounter: boolean }) {
  const [count, setCount] = useState(0);
  if (!showCounter) return null;
  return <p>{count}</p>;
}
use() — 예외·tsx
import { use, Suspense } from "react";

function Profile({ userPromise }: { userPromise: Promise<{ name: string }> }) {
  // 조건부 use() — 합법!
  const showName = window.location.hash === "#named";
  if (showName) {
    const user = use(userPromise);
    return <p>Hello, {user.name}</p>;
  }
  return <p>Anonymous</p>;
}

// use() 가 promise pending 일 때 suspend, 부모 Suspense 가
// resolution 까지 fallback 표시.
function Page() {
  const promise = fetch("/api/me").then((r) => r.json());
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Profile userPromise={promise} />
    </Suspense>
  );
}
ESLint config — linter 가 규칙 강제하게·json
{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

External links

Exercise

useEffect + useState 로 데이터 페치하는 컴포넌트 가져와서 use() + Suspense 로 다시 짜 (Track 5 lesson 4 가 완전히 빌드 — 여기서 시작). 차이 관찰: 로딩 상태 useState 없음, isLoading 플래그 없음, 컴포넌트에 에러 상태 없음 (error boundary 로). 두 버전 나란히 비교.
Hint
패턴: 부모가 promise 만들어서 prop 으로 전달, 자식이 use(promise) 호출. Suspense 경계는 부모에. 로딩과 에러 상태는 더는 자식 문제 아님.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.