본문으로 건너뛰기
개발 머꼬
개발 노트Next.js
hohyeon.dev30

generateMetadata를 썼는데 페이지 데이터를 두 번 fetch하는 것처럼 보인 이유

  • #Engineering Note
  • #Next.js
  • #Performance

문제 발생

generateMetadata와 페이지 컴포넌트가 둘 다 같은 게시글 데이터를 조회하도록 작성했는데, 서버 로그에 같은 쿼리가 요청마다 두 번씩 찍히는 것처럼 보여서 중복 조회를 걱정했습니다.

async function getPost(slug: string) {
  return db.query.posts.findFirst({ where: eq(posts.slug, slug) });
}

export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug); // 1번째 호출
  return { title: post?.title };
}

export default async function Page({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug); // 2번째 호출처럼 보임
  return <Article post={post} />;
}

원인 분석

Next.js는 fetch 요청에 대해 자동 메모이제이션을 제공합니다 — 같은 렌더링 패스(하나의 요청을 처리하는 동안) 안에서 동일한 URL과 옵션으로 fetch를 여러 번 호출하면, 실제 네트워크 요청은 한 번만 나가고 나머지는 그 결과를 재사용합니다. generateMetadata와 페이지 컴포넌트가 같은 데이터가 필요할 때 각자 fetch를 호출해도 되도록 설계된 이유가 바로 이것입니다.

중요한 전제: 이 자동 메모이제이션은 fetch 함수 자체에 적용되는 것이지, 위 예시처럼 ORM(Drizzle 등)이나 DB 드라이버를 직접 호출하는 코드에는 자동으로 적용되지 않습니다. 그래서 위 코드는 실제로 데이터베이스에 쿼리를 두 번 보냅니다 — 로그에 두 번 찍힌 것은 착각이 아니라 사실입니다.

해결 방안

DB 쿼리처럼 fetch가 아닌 데이터 소스에 대해 같은 효과를 얻으려면 React의 cache() 함수로 직접 감싸야 합니다.

import { cache } from "react";

const getPost = cache(async (slug: string) => {
  return db.query.posts.findFirst({ where: eq(posts.slug, slug) });
});

cache()로 감싼 함수는 같은 인자로 같은 렌더링 패스 안에서 호출되면, 실제 함수 본문을 다시 실행하지 않고 이전 결과를 그대로 반환합니다 — generateMetadata와 페이지 컴포넌트가 각자 getPost(slug)를 호출해도 실제 쿼리는 한 번만 나갑니다.

  1. cache()의 메모이제이션은 하나의 요청(렌더링 패스) 안에서만 유효합니다 — 다른 요청 사이에 캐싱되는 것이 아니므로, 여러 요청에 걸친 캐싱이 필요하다면 "use cache"/cacheTag 같은 별도 메커니즘이 필요합니다(요청 범위 중복 제거와 요청 간 캐싱은 다른 문제입니다).
  2. 데이터 페칭 함수를 domain/data-access 레이어의 한 곳에 정의해두고 generateMetadata와 페이지 양쪽에서 그 함수를 그대로 재사용하는 구조를 기본으로 삼으면, 이런 중복 조회를 처음부터 피하기 쉬워집니다.
  3. cache()는 React 자체가 제공하는 기능(Next.js 전용이 아님)이지만, 실제로는 거의 항상 Next.js App Router의 서버 컴포넌트 데이터 페칭 패턴과 함께 쓰입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.