블로그 목록
프론트엔드React

React 19 use() 훅으로 Suspense 데이터 페칭 바꾸기

React 19가 나오고 나서 가장 자주 쓰게 된 건 use() 훅이다.

기존에 비동기 데이터를 컴포넌트에서 가져오려면 패턴이 뻔했다. useEffect로 API 호출하고, useState로 로딩/에러/데이터 상태 관리하고, 조건부 렌더링으로 처리하는 구조. 간단한 컴포넌트도 코드가 금방 길어진다.

use()는 이 흐름을 바꾼다. Promise를 직접 컴포넌트 안에서 "읽을" 수 있게 해준다.

기본 사용법

import { use, Suspense } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <div>{user.name}</div>;
}

export default function Page() {
  const userPromise = fetchUser(1);

  return (
    <Suspense fallback={<div>로딩 중...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

use(promise)를 호출하면 Promise가 pending 상태일 때 자동으로 Suspense에 제어를 넘긴다. 컴포넌트는 Promise가 resolve됐을 때만 렌더링된다. ErrorBoundary를 감싸면 reject 처리도 외부에서 할 수 있다.

기존 useEffect 패턴과 비교

// 기존 방식
function OldUserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>에러 발생</div>;
  return <div>{user?.name}</div>;
}

로딩·에러·데이터 상태를 각각 관리해야 해서 코드가 길다. use()는 이걸 Suspense와 ErrorBoundary로 외부에서 처리하게 만들어서 컴포넌트 자체는 훨씬 단순해진다.

Context도 읽을 수 있다

use()의 특이한 점은 Context도 읽을 수 있다는 거다.

function ThemeButton() {
  const theme = use(ThemeContext);
  return <button className={theme.buttonClass}>버튼</button>;
}

기존 useContext와 달리 조건문 안에서도 호출할 수 있다. 훅 규칙(최상단에서만 호출)을 따르지 않아도 된다는 게 use()의 독특한 특성이다.

Next.js App Router에서 쓰는 패턴

서버 컴포넌트에서 Promise를 만들어 클라이언트 컴포넌트로 내려주는 방식이 자연스럽게 맞아떨어진다.

// app/users/[id]/page.tsx (서버 컴포넌트)
export default function Page({ params }: { params: { id: string } }) {
  const userPromise = fetchUser(Number(params.id));

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

서버에서 Promise를 생성하면 데이터 페칭이 서버에서 시작되고, 클라이언트는 그 결과를 받아서 렌더링하는 구조가 된다. 워터폴 없이 병렬 페칭도 쉽게 처리할 수 있다.

주의할 점

Promise는 컴포넌트 렌더 함수 밖에서 생성해야 한다. 렌더 함수 안에서 만들면 매 렌더마다 새 Promise가 생겨서 무한 루프에 빠진다. use(promise) 앞에 const promise = useMemo(() => fetch(...), [id]) 같은 메모이제이션이 필요하다.


아직 팀마다 도입 속도가 다르긴 한데, Suspense 기반 데이터 페칭으로 넘어가는 흐름은 확실한 것 같다. React Query도 내부적으로 이 방향으로 맞춰가고 있고, use()를 먼저 익혀두면 이후 생태계 변화를 따라가기 훨씬 편하다.