에러 처리

Next.js 15 dynamic route params 사용시 에러

Next.js 15 dynamic route params 사용시 에러

평소와 같이 app/[id]/page.tsx 에서 아래와 같이 params를 사용하다 다음과 같은 에러가 발생한 사람이 많을 것 같습니다.

function Page({ params }) {
  // direct access of `params.id`.
  return <p>ID: {params.id}</p>
}

Error: Route "/posts/[slugAndId]" used params.slugAndId. params should be awaited before using its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis

현재 Next.js가 params동기처럼 쓰되 내부적으로 Promise로 감싸도록 설계했기 때문에,TS에서는 이걸 그대로 Promise<{ slugAndId: string }>로 강제함. 해당페이지의 내용은 다음과 같습니다. next.js 15버전에서는 아래와 같은 dynamic api들이 비동기적으로 만들어졌다고 한다.

  • pages, layouts, metadata APIs, and route handlers에 제공되는 params, searchParams props들.

  • next/headers에서cookies()draftMode(), and headers()

 

즉, params, searchParams, cookies, headers 같은 객체들이 더 이상 즉시 동기적으로 접근할 수 있는 값이 아니고, Promise 형태로 감싸진 비동기 값이 되었음을 의미합니다.

공식문서에서는 다음과 같은 해결책을 제안하고 있습니다.

해결책

1. codemod로 자동 수정

npx @next/codemod@canary next-async-request-api .

👉 의미:

  • Next.js에서 바뀐 API들 (params, cookies, headers 등)이 이제 Promise 기반이기 때문에, 이를 자동으로 await 또는 React.use() 등으로 감싸도록 코드를 변환해줌.

  • 해당 codemod가 아래의 2번, 3번을 수행해주는데, 다만 완벽하진 않아서, 일부 코드는 수동으로 고쳐야 함.

2. 서버 컴포넌트에서는 await 사용

async function Page({ params }) { 
	// asynchronous access of `params.id`.
	const { id } = await params 
	return <p>ID: {id}</p>
}

👉 의미:

  • params는 이제 Promise 객체가 될 수 있으므로, 서버 컴포넌트에서는 반드시 await params 해야 함.

  • 즉, params.id 바로 쓰면 안 됨. const { id } = await params처럼 써야 정상 작동함.

  • 마찬가지로 cookies(), headers() 같은 것도 마찬가지로 await 필요.

3. 클라이언트 컴포넌트에서는 React.use() 사용

'use client'
import * as React from 'react'
 
function Page({ params }) {
  // asynchronous access of `params.id`.
  const { id } = React.use(params)
  return <p>ID: {id}</p>
}

👉 의미:

  • 클라이언트 컴포넌트는 async function을 사용할 수 없으니, 대신 React.use()로 감싼다.

  • 이는 params가 Promise일 수 있음을 고려한 Suspense 기반의 unwrap 방식임.

4. 마이그레이션 불가 코드 (Unmigratable Cases)

export function MyCookiesComponent() {
  const c =
    /* @next-codemod-error Manually await this call and refactor the function to be async */
    cookies()
  return c.get('name')
}

👉 의미:

  • codemod가 적합한 migrate를 수행하지 못할때, 위의 코드처럼 주석을 남겨놓는다.

  • 예를 들어서, cookies()는 Promise인데, 함수가 동기적으로 작성되어 있어서 마이그레이션이 불가능.

  • 따라서 MyCookiesComponent 자체를 async function으로 바꾸고 await cookies()를 써야 한다는 경고.

  • codemod가 자동으로 고칠 수 없으므로, 주석이 붙고 빌드 에러가 발생함

5. Linter로 강제 적용

- /* @next-codemod-error <suggested message> */
+ /* @next-codemod-ignore */`

👉 의미:

  • codemod가 자동 수정 못 한 곳엔 @next-codemod-error 주석이 붙음.

  • 이 주석이 남아있으면 빌드할 때 에러 발생.

  • 직접 수정하거나, 정말 무시해도 되는 경우에는 @next-codemod-ignore로 바꿔야 함.

💡 보너스 팁: "필요할 때만 await"

  • await params필요할 때까지 지연해서 사용하는 것이 바람직함.

  • 그래야 Next.js가 더 많은 부분을 정적으로 렌더링(SSG) 할 수 있음.

  • 예: 헤더나 쿠키가 필요 없는 부분은 먼저 렌더링 가능 → 성능 최적화.


여기까지는 단순히 에러를 없앨 수 있는 방법이다. 근데 대체 왜 dynamic api들을 promise구조로 바꾼 것일까?

결론부터 말하자면 다음과 같습니다.

Next.js가 모든 데이터를 비동기적으로 조합해, 가장 늦게까지 정적으로 만들고, 필요한 시점에만 동적으로 처리하는 렌더링 엔진으로 진화하고 있기 때문이다.

다음 포스트에서는 이와 관련된 내용을 자세하게 다루려고 합니다.