params와 searchParams가 왜 Promise가 되었는가
동기 접근은 이제 완전히 사라졌다
Next.js 14 후반부터 15에 걸쳐 params, searchParams, cookies(), headers()를 동기적으로 읽으면 경고가 떴습니다. 하지만 여전히 동작은 했습니다. 경고를 무시하고 배포한 프로젝트가 적지 않았을 거라고 생각합니다 — 저라도 급할 때는 그랬을 겁니다. 16에서는 그 유예 기간이 끝났습니다. 동기 접근은 더 이상 지원되지 않고, 값이 undefined가 되거나 타입 에러가 납니다.
// Next.js 15 이하 — 동기 접근 (경고는 뜨지만 동작함)
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// Next.js 16 — params는 Promise다
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}searchParams도 동일합니다. route.ts의 핸들러에서 두 번째 인자로 받는 params도 마찬가지고요.
왜 굳이 Promise로 감쌌는가
이해가 안 되면 그냥 외워서 고치게 되고, 외워서 고친 코드는 비슷한 실수를 반복합니다. 그러니 이유를 짚고 넘어가겠습니다.
App Router는 페이지의 일부를 정적으로 미리 만들어두고(Partial Prerendering) 나머지만 요청 시점에 채우는 방향으로 가고 있습니다. params, searchParams, cookies(), headers()는 전부 요청이 와야만 알 수 있는 값입니다. 이 값들이 동기 함수로 노출되어 있으면, 컴파일러 입장에서는 "이 컴포넌트가 정적으로 미리 렌더 가능한지"를 판단하기가 어렵습니다. 동기 API 뒤에서 무슨 일이 일어나는지 컴파일러가 알 방법이 없으니까요.
반면 Promise로 감싸두면 "이 값은 요청 시점 이전에는 존재하지 않는다"는 게 타입 시스템에 드러납니다. await 없이는 값을 못 읽으니, 정적으로 미리 만들 수 있는 부분과 요청을 기다려야 하는 부분이 코드 상에서 명확히 분리됩니다. 성능 최적화를 프레임워크가 자동으로 해주길 바란다면, 그 자동화가 가능하도록 API 모양을 바꾼 셈입니다.
실전에서 부딪히는 지점들
가장 많이 놓치는 건 클라이언트 컴포넌트입니다. "use client"가 붙은 컴포넌트는 async 함수가 될 수 없으므로 await params를 쓸 수 없습니다. 이 경우 React 19의 use() 훅으로 Promise를 언래핑합니다.
"use client";
import { use } from "react";
export default function ClientPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = use(params);
return <h1>{slug}</h1>;
}generateMetadata, generateStaticParams처럼 params를 받는 다른 함수들도 전부 같은 방식으로 고쳐야 합니다. 하나라도 빠뜨리면 그 함수만 조용히 깨지는데, 타입을 strict로 켜두지 않았다면 빌드는 성공하고 런타임에서야 발견됩니다.
# 공식 codemod가 대부분의 기계적 변환을 해준다
npx @next/codemod@canary next-async-request-api .codemod를 돌린 뒤에도 직접 눈으로 확인해야 할 것이 있습니다. codemod는 params를 쓰는 함수 시그니처는 잘 고치지만, 그 값을 컴포넌트 최상단이 아니라 조건문 안에서 늦게 읽던 코드는 놓치는 경우가 있습니다. 조건부 렌더링 안에 params.slug 같은 접근이 남아 있는지는 검색으로 한 번 더 훑는 걸 권합니다.