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

use() hook — 인라인 async 읽기

~13 min · use, react-19, suspense

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
use() 는 작은 hook 이지만 폭발 반경 큼. 컴포넌트가 promise 를 동기 값처럼 읽게 해주고 Suspense 가 wait 처리. 승리: success 경로에서 컴포넌트가 위에서 아래로 렌더; 통과시킬 로딩 state 없음.

모양

function MyComp({ promise }: { promise: Promise<T> }) {
  const data = use(promise);
  return <p>{data.value}</p>;
}

Promise pending 이면 use() suspend — React 가 가장 가까운 Suspense fallback 표시. Reject 면 use() throw — 가장 가까운 error boundary 잡음. Resolve 됐으면 use() 가 값 반환. 컴포넌트 body 는 success 케이스만 봄.

기본 규칙 — render 간 같은 promise

use() 가 promise 를 reference identity 로 인식. 매 render 마다 컴포넌트 안에 새 promise 만들면 use() 가 매번 다른 (여전히 pending) promise 봄, 매번 suspend, 무한 로딩 state.

고침: promise 를 컴포넌트 에 생성 (또는 부모에서 만들어 prop 으로 전달, 또는 ref / context 에 캐시, 또는 캐싱 처리하는 TanStack Query 같은 라이브러리).

안정적 promise 출처 셋

  1. 부모 prop — 부모가 promise 생성 (한 번 또는 캐시된 팩토리로), 자식이 use() 로 소비.
  2. 모듈 레벨 상수 — one-off 부트스트래핑 데이터에 OK.
  3. 캐시된 팩토리 — React 의 cache 헬퍼의 cache((id: string) => fetch(id).then(j)) 같은 함수 (또는 본인의 Map 기반 memoization).

Context 와 use()

use() 가 Context 도 읽음 — useContext 와 달리 조건부 호출 가능. if (someCondition) { const value = use(MyCtx); } 가 use() 로는 합법, useContext 로는 불법. 일부 branch 가 필요 없는 Context 있을 때 편함.

use() 가 async 가 사는 자리의 책임을 이동. use() 전엔 각 컴포넌트가 자기 로딩 state 운반. use() 후엔 wait state 가 위쪽 Suspense 경계에, 컴포넌트는 값이 이미 거기 있는 것처럼 읽음. Leaf 가 단순해짐; 부모가 promise-생성 책임 짊어짐.

Code

부모-생성 promise 가진 use()·tsx
import { use, Suspense } from "react";

async function fetchUser(id: string): Promise<{ name: string }> {
  const r = await fetch(`/api/users/${id}`);
  if (!r.ok) throw new Error(r.statusText);
  return r.json();
}

function UserName({ userPromise }: { userPromise: Promise<{ name: string }> }) {
  const user = use(userPromise);
  return <p>{user.name}</p>;
}

function UserPage({ userId }: { userId: string }) {
  // 부모가 promise 생성. 같은 userId 의 UserPage 매 render 가
  // 같은 promise 산출해야 — 캐시 사용 또는 useState/useRef 로 통과.
  const promise = fetchUser(userId); // 단순화 — 진짜 사용엔 아래 캐시 패턴
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <UserName userPromise={promise} />
    </Suspense>
  );
}
캐시 패턴 — render 간 안정적 promise·tsx
import { use, Suspense } from "react";

// Map 기반 캐시 — 같은 userId 가 항상 같은 promise 반환.
const userCache = new Map<string, Promise<{ name: string }>>();

function getUserPromise(userId: string): Promise<{ name: string }> {
  if (!userCache.has(userId)) {
    userCache.set(
      userId,
      fetch(`/api/users/${userId}`).then((r) => r.json())
    );
  }
  return userCache.get(userId)!;
}

function UserName({ userId }: { userId: string }) {
  const user = use(getUserPromise(userId));
  return <p>{user.name}</p>;
}

function UserPage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <UserName userId={userId} />
    </Suspense>
  );
}

// 프로덕션에선 TanStack Query 또는 React 의 `cache()` 헬퍼 써서
// invalidation, 재시도, dedup 와 함께 같은 효과 얻음.
Context 와 use() — 조건부 읽기·tsx
import { use, createContext } from "react";

const AuthCtx = createContext<{ user: { name: string } } | null>(null);

function MaybeGreeting({ greet }: { greet: boolean }) {
  // 조건부 — useContext 불가, use() 가능.
  if (greet) {
    const auth = use(AuthCtx);
    if (!auth) return null;
    return <p>Hello, {auth.user.name}</p>;
  }
  return null;
}

External links

Exercise

보여준 캐시 패턴 쓰는 <UserPage userId={...}> 빌드. 확인: (1) 초기 로드가 suspend 하고 Suspense fallback 보임; (2) 로드 후 데이터 나타남; (3) 다른 userId 로 네비게이트하면 또 fetch + suspension 트리거; (4) 원래 userId 로 BACK 네비게이트하면 re-fetch 안 함 (캐시 hit 가 resolve 된 promise 반환). 캐시가 네비게이션을 즉시처럼 느끼게.
Hint
반복 방문이 re-fetch 면 캐시 잘못. 확인: Map 이 모듈 레벨 (render 간 보존) 인가 컴포넌트-로컬 (재생성) 인가? 모듈 레벨이 원하는 것.

Progress

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

댓글 0

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

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