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()를 먼저 익혀두면 이후 생태계 변화를 따라가기 훨씬 편하다.