React.js 캐시 문제용 use() 훅
Source: Dev.to
대부분의 튜토리얼이 여기서 멈춥니다. 하지만 클라이언트 컴포넌트 내부에서 만든 Promise와 함께 use()를 사용하려고 하면 미묘하고 답답한 버그에 직면하게 됩니다.
// Bug: creates a new promise on every render
function UserProfile({ userId }: { userId: string }) {
const user = use(fetchUser(userId)); // new promise every render
return <div>{/* ... */}</div>;
}
fetchUser(userId)는 매 렌더링마다 새로운 Promise 객체를 반환합니다. React는 새로운 Promise를 보고 다시 suspend하고, 컴포넌트는 다시 렌더링되어 또 다른 새로운 Promise를 만들고, 또 suspend하는 무한 루프에 빠집니다.
use()는 데이터를 가져오는 것이 아니라 Promise를 읽는 것입니다. 그 Promise는 렌더링 사이에 안정적인 식별자를 가져야 합니다. 매 렌더링마다 새로운 Promise를 만들면 무한 suspend 루프가 발생합니다.
Promise를 안정화하는 방법
- 부모 컴포넌트 또는 서버 컴포넌트에서 Promise를 생성하기
// Server Component - promise created once, stable across renders
export default function UserPage({ params }: { params: { id: string } }) {
const userPromise = fetchUser(params.id);
return (
<UserProfile userPromise={userPromise} />
);
}
async/await은 필요하지 않습니다. Promise는 해결되지 않은 채로 내려보내고, 클라이언트 컴포넌트가 use()로 풀어줍니다. 서버 컴포넌트는 재렌더링되지 않으므로 Promise 참조는 자연스럽게 안정적입니다.
- 모듈 레벨 캐시 사용하기
const cache = new Map<string, Promise<User>>();
function fetchUserCached(id: string): Promise<User> {
if (!cache.has(id)) {
cache.set(id, fetchUser(id));
}
return cache.get(id)!;
}
function UserProfile({ userId }: { userId: string }) {
const user = use(fetchUserCached(userId));
return <div>{/* ... */}</div>;
}
같은 인자는 같은 Promise 참조를 반환하므로 무한 루프가 발생하지 않습니다.
캐시 래퍼에서 async 사용 금지
캐시 함수를 async로 선언하지 마세요. async 키워드는 항상 새로운 Promise를 만들기 때문에, 캐시된 값을 반환하더라도 새 Promise가 생성됩니다. 원본 Promise 객체를 저장하고 반환하는 동기 함수를 사용하세요.
-
데이터 패칭 라이브러리 사용하기
TanStack Query나 SWR 같은 라이브러리는 캐싱, 중복 제거, 재검증을 기본 제공하며use()가 등장하기 전부터 존재해 왔습니다. 다만 gzipped 기준 약 13KB의 추가 용량과 Provider 래퍼가 필요합니다. 단순히 “한 번 fetch하고 결과를 표시”하는 패턴이라면 위의 2번 옵션처럼 5줄짜리 캐시 함수만으로도 충분합니다. 라이브러리를 사용할 가치가 있는 경우는 UI에 오래 유지되는 클라이언트 상태가 있고, 탭 포커스 시 재fetch, 페이지네이션이 있는 리스트, 혹은 연관된 쿼리를 옵티미스틱하게 업데이트해야 하는 mutation 등이 있을 때입니다. -
서버 컴포넌트에서 React의
cache()사용하기
import { cache } from "react";
const getUser = cache(async (id: string): Promise<User> => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});
같은 렌더링 과정에서 여러 컴포넌트가 getUser("123")를 호출하면 하나의 fetch만 수행됩니다. cache()는 단일 서버 요청의 수명 동안 반환값을 메모이제이션합니다. 페이지가 새로 로드될 때마다 캐시는 초기화됩니다.
cache() vs. useMemo
두 함수 모두 메모이제이션을 수행하지만, cache()는 서버 렌더링 시 컴포넌트 간에 데이터를 공유(중복 제거)하는 반면, useMemo는 하나의 컴포넌트 내부에서 재렌더링 사이에 계산 결과를 저장합니다. cache()는 데이터 패칭용, useMemo는 순수 연산용으로 각각 다른 도구이며, 용도도 다릅니다.