React 19 use() 훅 — Suspense와 통합된 데이터 페칭 패턴
Promise와 Context를 조건부로 읽을 수 있는 유일한 훅, use(). 기존 useEffect 기반 페칭과 무엇이 다른지 정리.
다른 훅과 다른 점
useState, useEffect 같은 기존 훅은 컴포넌트 최상위에서만 호출할 수 있고, 조건문이나 반복문 안에서는 호출할 수 없다. use()는 이 규칙에서 예외다 — 조건문 안에서 호출해도 된다.
1function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) { 2 if (!commentsPromise) return null; 3 // 조건문 통과 후에 use() 호출 — 기존 훅이었다면 규칙 위반 4 const comments = use(commentsPromise); 5 return <CommentList comments={comments} />; 6}
Promise를 직접 읽는다
use()는 Promise를 인자로 받아서, 그 Promise가 resolve될 때까지 컴포넌트 렌더링을 Suspense 경계까지 올려서 중단시킨다. useEffect + useState 조합으로 로딩 상태를 손으로 관리하던 걸 컴포넌트 트리 차원에서 대신 처리하는 셈이다.
1// 기존 방식 2function Profile({ userId }: { userId: string }) { 3 const [user, setUser] = useState<User | null>(null); 4 const [loading, setLoading] = useState(true); 5 6 useEffect(() => { 7 fetchUser(userId).then((u) => { 8 setUser(u); 9 setLoading(false); 10 }); 11 }, [userId]); 12 13 if (loading) return <Spinner />; 14 return <div>{user?.name}</div>; 15} 16 17// use() 방식 — 로딩 상태를 Suspense가 대신 처리 18function Profile({ userPromise }: { userPromise: Promise<User> }) { 19 const user = use(userPromise); 20 return <div>{user.name}</div>; 21} 22 23// 상위에서 24<Suspense fallback={<Spinner />}> 25 <Profile userPromise={fetchUser(userId)} /> 26</Suspense>
주의할 점 — 매 렌더마다 새 Promise를 넘기면 안 된다
fetchUser(userId)를 Profile 컴포넌트 렌더링 도중에 직접 호출하면, 리렌더링될 때마다 새 Promise가 생성돼서 무한 로딩에 빠진다. Promise는 상위 컴포넌트(주로 Server Component)나 캐시된 함수에서 생성해서 props로 내려줘야 한다. Next.js App Router에서 Server Component가 Promise를 만들고 Client Component가 use()로 읽는 패턴이 이 때문에 자리 잡았다.
Context도 조건부로 읽을 수 있다
use()는 Context도 읽을 수 있는데, useContext와 달리 조건문 뒤에서 호출 가능하다.
1function Button({ theme }: { theme?: Theme }) { 2 if (theme) return <StyledButton theme={theme} />; 3 const contextTheme = use(ThemeContext); // 조건문 통과 후 호출 가능 4 return <StyledButton theme={contextTheme} />; 5}
정리
use()는 새로운 데이터 페칭 라이브러리가 아니라, Promise/Context를 렌더링 도중 읽는 방법을 하나 추가한 것에 가깝다. 실제 캐싱, 재요청 전략은 여전히 React Query 같은 라이브러리나 Next.js의 fetch 캐싱이 담당한다.