본문 바로가기
C.W.K.
Stream
Lesson 04 of 07 · published

use() 훅 — 인라인 비동기 읽기

~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()를 쓰면 컴포넌트가 프로미스를 동기 값처럼 읽을 수 있고, 기다림은 Suspense가 맡아. 대신 렌더링 사이에 같은 프로미스를 유지해야 해.

기본 모양

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

프로미스가 대기 중이면 use()가 렌더링을 중단하고 React는 가장 가까운 Suspense의 대체 UI를 보여 줘. 프로미스가 거부되면 use()가 오류를 던지고 가장 가까운 오류 경계가 처리해. 이행된 뒤에는 값을 반환하므로 컴포넌트 본문에는 성공 경로만 남아.

핵심 규칙: 렌더링 사이에 같은 프로미스를 유지해

use()는 프로미스를 참조 동일성으로 구분해. 컴포넌트 안에서 렌더링할 때마다 새 프로미스를 만들면 React는 매번 새로운 대기 작업으로 판단하고 다시 중단해. 그러면 로딩 화면이 끝나지 않을 수 있어.

프로미스는 컴포넌트 밖에서 만들거나, 부모에서 만들어 prop으로 넘기거나, ref나 Context에 보관하거나, TanStack Query처럼 캐시를 관리하는 도구에서 가져와야 해.

안정적인 프로미스를 얻는 세 곳

  1. 부모 prop: 부모가 한 번 만들었거나 캐시에서 가져온 프로미스를 자식에게 넘기고, 자식은 use()로 읽어.
  2. 모듈 수준 상수: 앱 시작 때 한 번만 읽는 데이터에 적합해.
  3. 캐시된 팩토리: 같은 인자를 받으면 같은 프로미스를 돌려주는 함수야. React의 cache 헬퍼나 직접 만든 Map 기반 캐시를 사용할 수 있어.

Context와 use()

use()는 Context도 읽을 수 있어. useContext와 달리 조건부 호출이 허용되므로 if (someCondition) { const value = use(MyCtx); } 같은 코드도 합법이야. 일부 분기에서만 Context 값이 필요할 때 유용해.

use()는 비동기 책임의 위치를 바꿔. 이전에는 각 컴포넌트가 로딩 상태를 직접 들고 있었지만, use()를 쓰면 기다림은 위쪽 Suspense 경계로 올라가. 말단 컴포넌트는 값이 준비된 것처럼 단순하게 읽을 수 있고, 대신 부모나 캐시 계층이 안정적인 프로미스를 제공할 책임을 져.

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 }) {
  // 부모가 프로미스 생성. 같은 userId를 받은 UserPage가 렌더링될 때마다
  // 같은 프로미스를 반환해야 해. 캐시하거나 useState/useRef에 보관해.
  const promise = fetchUser(userId); // 단순화 — 진짜 사용엔 아래 캐시 패턴
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <UserName userPromise={promise} />
    </Suspense>
  );
}
캐시 패턴: 렌더링 간 안정적 프로미스·tsx
import { use, Suspense } from "react";

// Map 기반 캐시 — 같은 userId는 항상 같은 프로미스를 반환해.
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()` 헬퍼로
// 무효화, 재시도, 중복 제거까지 함께 처리할 수 있어..
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

레슨의 캐시 패턴을 사용해 userId에 맞는 UserPage를 만들어. 처음 열 때는 Suspense의 대체 UI가 보이고, 요청이 끝나면 사용자 이름이 나타나야 해. 다른 userId로 이동하면 새 요청을 보내고, 이전 userId로 돌아오면 이행된 프로미스를 캐시에서 읽어 다시 요청하지 않는지도 확인해.
Hint
userId를 key로 삼는 Map에 프로미스를 저장해. 같은 userId를 다시 받았을 때 새 fetch를 만들지 않고 기존 프로미스를 반환해야 해.

Progress

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

댓글 0

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

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