Next.js 15 params should be awaited 에러 원인과 해결 가이드

Next.js 15 params should be awaited 에러 원인과 해결 가이드

3초 요약: 이렇게 고치세요
  • 원인: Next.js 15부터 params·searchParams가 Promise로 바뀌었는데, 예전처럼 params.id를 동기적으로 바로 읽어서 발생
  • 해결: 서버 컴포넌트·Route Handler·generateMetadataconst { id } = await params; / 클라이언트 컴포넌트 → const { id } = use(params);
  • 주의: 공식 codemod가 대부분 자동 변환해주지만, params를 조건부로 읽거나 다른 함수에 그대로 넘기는 패턴은 놓칠 수 있어 직접 확인 필요
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
}
↓ 왜 이런 에러가 나는지 원리 보기

Next.js 14 프로젝트를 15로 올리자마자 params.id를 쓰던 코드에서 "params should be awaited" 같은 경고나 타입 에러를 마주친 적 있으실 겁니다. 원인은 Next.js 15부터 동적 라우트의 paramssearchParams가 더 이상 일반 객체가 아니라 Promise로 바뀌었기 때문인데, 코드 몇 줄만 고치면 되는 문제지만 프로젝트 전체에 흩어져 있으면 손이 꽤 갑니다.


params가 갑자기 Promise가 된 이유

Next.js 14까지는 page.tsx가 받는 params·searchParams가 렌더링 시점에 이미 값이 채워진 평범한 객체였습니다. Next.js 15는 이 값들을 포함해 cookies(), headers(), draftMode()처럼 요청이 와야만 알 수 있는 값들을 전부 비동기 API로 통일했습니다. 정적으로 미리 렌더링할 수 있는 부분(레이아웃, 고정 텍스트 등)과 요청마다 달라지는 동적인 부분을 명확히 분리해서, 동적인 값이 실제로 필요한 지점까지는 페이지 셸을 먼저 그려두고 나머지를 스트리밍으로 채워 넣을 수 있게 하기 위한 구조 변경입니다.

그래서 예전처럼 params.id를 동기적으로 바로 읽으면, Next.js는 "이 값은 이제 Promise니까 await 없이 읽으면 안 된다"는 경고나 타입 에러를 띄웁니다.

최근 외주로 유지보수를 맡고 있는 프로젝트가 있는데, 버전을 계속 미뤄오다가 다른 패키지 업데이트 때문에 더는 미룰 수 없어서 Next.js 14에서 15로 한 번에 올리는 작업을 했습니다. 빌드는 문제없이 끝나길래 별일 아닌 줄 알고 넘어갔습니다. 그런데 다음 날 다른 작업자가 그 페이지에서 cookies()를 새로 쓰는 코드를 추가하다가 타입 에러를 만났고, 그제서야 콘솔에 계속 쌓이고 있던 params 관련 경고를 제대로 들여다봤습니다. Next.js가 당장은 이전 방식의 동기 접근도 어느 정도 호환되게 처리해주면서 경고만 띄우는 구간이 있어서 눈에 잘 안 띄었던 건데, 그땐 공식 codemod가 있는 줄도 모르고 프로젝트에 params.id를 쓰는 곳을 페이지·Route Handler 합쳐서 열 군데 넘게 직접 찾아 하나씩 await로 바꿨습니다. 나중에서야 자동 변환 도구가 있었다는 걸 알고 좀 허탈했습니다.

헷갈리기 쉬운 지점: codemod가 전부 잡아주지는 않는다

Next.js는 이 변경을 위한 공식 codemod(npx @next/codemod@latest next-async-request-api .)를 제공합니다. params.id처럼 곧바로 구조 분해하는 흔한 패턴은 대부분 자동으로 await 형태로 바꿔주지만, params를 그대로 다른 함수에 넘기거나 조건문 안에서만 접근하는 것처럼 조금만 패턴이 달라져도 놓치는 경우가 있습니다. codemod를 돌린 뒤에도 params·searchParams를 쓰는 곳을 한 번은 직접 검색해서 확인하는 게 안전합니다.


실전 해결 코드: 위치별 처리 방법

서버 컴포넌트 (page.tsx / layout.tsx)

// 이전 (Next.js 14 방식, 15부터 경고/에러)
export default function BlogPost({ params }: { params: { slug: string } }) {
  const { slug } = params;
  return <h1>{slug}</h1>;
}

// 이후 (Next.js 15)
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}

Route Handler

// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  return Response.json({ id });
}

클라이언트 컴포넌트

클라이언트 컴포넌트는 async 함수로 만들 수 없기 때문에 await 대신 React의 use로 Promise를 언래핑합니다.

'use client';
import { use } from 'react';

export default function BlogPostClient({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = use(params);
  return <h1>{slug}</h1>;
}

searchParams도 동일한 방식입니다. 서버 컴포넌트·Route Handler에서는 await searchParams, 클라이언트 컴포넌트에서는 use(searchParams)로 처리하면 됩니다. generateMetadata 함수도 같은 params를 받기 때문에 빠뜨리기 쉬운데, 여기도 동일하게 await로 풀어줘야 합니다.


위치별 처리 방법 정리

코드가 실행되는 위치에 따라 처리 방식이 다릅니다.

위치 처리 방법 비고
서버 컴포넌트 await params 컴포넌트 함수를 async로 선언해야 함
Route Handler await params GET/POST 등 핸들러 함수 자체가 이미 async
클라이언트 컴포넌트 use(params) 클라이언트 컴포넌트는 async로 선언할 수 없어 await 대신 use 사용
generateMetadata await params 서버 컴포넌트와 동일하지만 빠뜨리기 쉬움
공식 참고 레퍼런스

비동기 API 전환의 배경과 전체 마이그레이션 범위는 공식 문서에서 확인할 수 있습니다.

호마다의 웹개발 팁

Next.js의 비동기 렌더링과 스트리밍 구조를 좀 더 자세히 다룬 글은 [호마다의 IT 개발 입문 블로그] 에서 볼 수 있습니다.

댓글

이 블로그의 인기 게시물

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

Next.js 하이드레이션 오류 원인과 해결법 완벽 정리

Next.js App Router에서 getServerSideProps 대체 및 데이터 페칭법