라떼군 이야기


Next.js 15 동적 라우트 에러: 'params should be awaited' 해결 방법

Problem

Next.js 15에서 App Router를 사용하여 다국어 지원(/[locale])이나 동적 라우트를 구현할 때, params 객체에 접근하려고 하면 Error: Route "/[locale]" used params.locale. params should be awaited before using its properties.라는 에러가 발생합니다. 이전 버전(Next.js 14 이하)처럼 컴포넌트의 props에서 params를 동기적으로 구조 분해 할당({ params: { locale } })하여 사용하던 코드가 더 이상 작동하지 않아, 버전 업그레이드 후 많은 개발자들이 겪게 되는 흔한 문제입니다.

Background

Next.js 15부터는 서버 렌더링의 성능과 유연성을 향상시키기 위해 요청(Request)과 관련된 동적 API들의 동작 방식이 크게 변경되었습니다. 기존에는 params, searchParams, cookies, headers 등의 값들이 동기적으로 제공되었으나, 이제는 렌더링 시점에 비동기적으로 평가되는 Promise 형태로 제공됩니다. 이는 서버 컴포넌트가 요청 시점의 데이터에 의존할 때, 불필요하게 렌더링을 차단하지 않고 더 효율적으로 스트리밍(Streaming)을 수행할 수 있도록 하기 위한 아키텍처 개선입니다. 따라서 이러한 동적 값들을 사용하려면 반드시 Promise를 해제(unwrap)하는 과정이 필요해졌습니다.

Solution

이 문제를 해결하기 위해서는 두 가지 측면을 확인해야 합니다. 첫 번째는 코드 상에서 params를 비동기적으로 처리하는 것이고, 두 번째는 특정 파일의 위치로 인한 버그를 수정하는 것입니다.

1. paramsawait로 비동기 처리하기

가장 직접적인 해결책은 params의 타입을 Promise로 변경하고, 컴포넌트 내부에서 await를 사용하여 값을 추출하는 것입니다.

// app/[locale]/layout.tsx
import React from "react";

// params의 타입을 Promise로 정의합니다.
type RootLayoutProps = {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
};

// 컴포넌트를 async 함수로 선언합니다.
export default async function RootLayout({ children, params }: RootLayoutProps) {
  // await를 사용하여 Promise 객체인 params에서 locale 값을 비동기적으로 가져옵니다.
  const { locale } = await params;

  return (
    <html lang={locale}>
      <body>
        {children}
      </body>
    </html>
  );
}

이 방식을 적용하면 Next.js 15의 비동기 동적 API 요구사항을 충족하게 되어 에러가 사라집니다.

2. 정적 파일(icon.png 등) 위치 확인하기

코드를 올바르게 수정했음에도 불구하고 경고나 에러가 지속된다면, 프로젝트 구조 내의 메타데이터 파일 위치를 확인해야 합니다. 동적 라우트 폴더 내부에 icon.png와 같은 파일이 있을 경우, Next.js가 이를 처리하는 과정에서 잘못된 경고를 발생시키는 이슈가 있습니다.

문제가 발생하는 구조 (경고 발생):

/src
├── /app
│   ├── /[lang]
│   │    ├── layout.tsx
│   │    └── icon.png  <-- 동적 라우트 폴더 안에 위치함

해결된 구조 (경고 없음):

/src
├── /app
│   ├── icon.png       <-- app 디렉토리 최상단으로 이동
│   ├── /[lang]
│   │    └── layout.tsx

icon.png, apple-icon.png 등의 정적 메타데이터 파일은 동적 라우트 폴더(/[locale]) 내부가 아닌, app 디렉토리의 루트나 정적인 경로에 배치하는 것이 좋습니다.

Deep Dive

서버 컴포넌트에서는 async/await를 쉽게 사용할 수 있지만, 클라이언트 컴포넌트("use client")에서 동적 params를 다뤄야 할 때는 async 컴포넌트를 사용할 수 없습니다. 이 경우에는 React 19에서 새롭게 도입된 React.use() 훅을 사용하여 Promise를 처리해야 합니다. 또한, 기존 Next.js 14 프로젝트를 15로 대규모 마이그레이션하는 경우, Next.js 팀에서 제공하는 @next/codemodnext-async-request-api 코드모드를 실행하면 프로젝트 전체의 동기적 API 호출을 비동기 방식으로 자동 변환해 주어 매우 유용합니다.

Conclusion

Next.js 15에서는 렌더링 최적화를 위해 params를 비롯한 동적 API들이 비동기(Promise)로 변경되었습니다. 따라서 서버 컴포넌트에서는 await를, 클라이언트 컴포넌트에서는 React.use()를 사용하여 값을 추출해야 하며, 예상치 못한 에러가 발생할 경우 동적 라우트 폴더 내의 정적 에셋 위치를 점검하는 것이 좋습니다.

References

협업 및 후원 연락하기 →