unstable_cache 안에서 세션 조회 같은 동작이 지원되지 않는 이유

2025-05-22

Next.js의 unstable_cache를 사용하다가 아래와 같은 에러를 처음 마주했습니다.

Error: Route /roadmap/[externalId] used "headers" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "headers" outside of the cached function and pass the required dynamic data in as an argument.

unstable_cache 안에서는 headers(), cookies() 같은 dynamic api를 쓸 수 없다는 경고입니다.

왜 이런 제약이 있을까?

Next.js는 unstable_cache내부에서 실행된 함수의 결과를 정적으로 캐시합니다. 그런데 headers()처럼 매 요청마다 바뀔 수 있는 동적 데이터를 사용하면, 결과가 사용자마다 달라질 수 있어서 캐시의 의미가 사라지게 됩니다.

Accessing dynamic data sources such as headers or cookies inside a cache scope is not supported. If you need this data inside a cached function use headers outside of the cached function and pass the required dynamic data in as an argument.

https://nextjs.org/docs/app/api-reference/functions/unstable_cache

세션이 필요한 경우엔 캐시를 포기해야 할까?

pickroad의 상세 페이지에서는 하나의 로드맵을 보여주면서 아래 정보를 같이 표시해야 했습니다.

  • 제목, 설명, 아이템 리스트, 좋아요 개수 → 누구에게나 동일함
  • 로그인 사용자의 좋아요/북마크 여부 → 사용자에 따라 다름

처음엔 세션 처리를 위해 unstable_cache사용을 포기할까도 고민했지만, 트래픽이 많아질 것을 고려하면 정적 데이터는 반드시 캐시해야 한다는 판단이 들었습니다. 그래서 정적 데이터와 동적 데이터를 분리해서 처리하는 방식을 선택했습니다.

시도한 방법

공통적으로 모든 사용자에게 동일하게 제공되는 정적 데이터는 unstable_cache를 활용해 효율적으로 캐싱했습니다.

반면, 사용자마다 달라지는 좋아요 여부나 북마크 상태와 같은 맞춤형 정보는 함수 외부에서 세션을 조회해 처리한 뒤, 그 결과를 바탕으로 동적으로 추가하여 반환하는 방식으로 분리해서 구현했습니다.

export const getRoadmap = unstable_cache(
  async (externalId: Roadmap["externalId"]): Promise<Roadmap | null> => {
    const roadmap = await db.query.roadmaps.findFirst({
      where: eq(roadmaps.externalId, externalId),
      with: {
        category: true,
        author: true,
      },
    });

    if (!roadmap) return null;

    return roadmap
  },
);

export const getRoadmapWithSession = async (
  externalId: Roadmap["externalId"],
): Promise<Roadmap | null> => {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  const roadmap = await getRoadmap(externalId);

  if (!roadmap) return null;

  let isLiked = false;
  let isBookmarked = false;

  if (session) {
    const row = ... //like,bookmark여부 조회 query

    isLiked = row.isLiked || false;
    isBookmarked = row.isBookmarked || false;
  }

  return {
    ...roadmap,
    isLiked,
    isBookmarked,
  };
};