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

Next.js 16에서 params와 cookies()가 Promise가 된 이유

  • #Engineering Note
  • #Next.js
  • #Upgrade

문제 발생

Next.js 15에서 경고만 뜨던 코드가 16으로 올린 뒤 실제로 깨졌습니다.

export default function Page({ params }: { params: { slug: string } }) {
  const post = getPost(params.slug);   // params가 Promise라 slug가 undefined
  return <article>{post.title}</article>;
}

cookies(), headers()도 같은 방식으로 값 대신 Promise를 돌려주기 시작했습니다.

원인 분석

이 API들은 인프라 수준에서 원래 비동기입니다. 요청이 도착하기 전에는 알 수 없는 값이고, 서버는 그 값을 스트림으로 받습니다. 그런데도 동기 함수처럼 보이게 하려면 내부적으로 우회가 필요했고, 그 우회가 부분 사전 렌더링(PPR)과 스트리밍에서 버그의 원인이 됐습니다. 렌더를 미리 시작해 두고 요청 정보가 필요한 지점에서만 기다리는 방식이 동기 API와는 맞지 않기 때문입니다.

그래서 15에서 비동기로 바꾸되 동기 접근을 한동안 허용하며 경고를 냈고, 16에서 동기 접근을 완전히 제거했습니다. 즉 16의 변경은 새 기능이 아니라 15에서 예고한 마감입니다.

영향 범위가 넓다는 점도 중요합니다. 페이지와 레이아웃뿐 아니라 라우트 핸들러, 그리고 opengraph-image, twitter-image, icon, apple-icon 같은 이미지 생성 함수까지 모두 해당됩니다.

해결 방안

  1. 함수를 async로 바꾸고 await 합니다.
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.title}</article>;
}
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value;
  1. codemod로 기계적인 부분을 먼저 처리합니다. 동기 params/searchParams 접근에 await을 붙이고 함수를 async로 바꿔 줍니다. 남는 것은 검토가 필요한 자리뿐입니다.
npx @next/codemod@latest next-async-request-api .
  1. 타입은 typegen으로 맞춥니다. 페이지 props 타입을 직접 적는 대신 생성된 타입을 쓰면 시그니처가 어긋나지 않습니다.
npx next typegen
  1. 한 번에 하나씩 옮깁니다. 15의 마지막 마이너에서 경고를 0으로 만든 뒤 16으로 올리면, 16에서 새로 발견되는 문제가 거의 없습니다. 경고를 남긴 채 올리면 런타임 오류로 바뀐 것을 배포 후에 만나게 됩니다.
  2. await을 어디에 둘지 신경 씁니다. 컴포넌트 맨 위에서 무조건 await cookies()를 하면, 그 지점부터 전체가 동적 렌더링으로 넘어갑니다. 실제로 필요한 경계 안쪽으로 밀어 넣으면 나머지는 정적으로 남습니다 — 이 API들을 비동기로 만든 목적이 바로 그 분리입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

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