2026. 7. 2.

Next.js 15 동적 라우트가 멈출 때, params 에러 3분 수정 순서

Next.js 15로 올린 뒤 `params should be awaited`나 Promise 타입 에러가 보이면 동적 라우트 파일에서 `params`를 먼저 await하고 타입을 Promise로 맞추면 됩니다. 전체 페이지를 다시 만들기 전에 3분 안에 확인할 순서를 정리했습니다.

5 min read
Next.js 15 동적 라우트가 멈출 때, params 에러 3분 수정 순서 대표 이미지

Next.js 15로 올린 뒤 app/[slug]/page.tsx가 갑자기 멈추면 처음에는 코드 전체가 틀어진 것처럼 보입니다. 에러에는 params should be awaited가 보이거나, TypeScript가 paramsthen, catch, finally가 없다고 말합니다. 어제까지 되던 동적 라우트라면 더 헷갈립니다.

먼저 결론부터 보면 됩니다. Next.js 15의 App Router에서 동적 라우트 params는 이제 동기 객체처럼 바로 꺼내 쓰는 값이 아니라 Promise로 다뤄야 합니다. 그래서 목표는 페이지 전체를 다시 만드는 것이 아니라 params를 await하고 타입을 Promise로 맞추는 것입니다.

Next.js 15 params 에러가 발생한 동적 라우트와 수정 위치를 보여주는 전후 비교 이미지

nextjs.org의 Dynamic Segments 문서는 params prop이 Promise이며 async/await 또는 React use로 값을 읽어야 한다고 안내합니다. 같은 nextjs.org의 Next.js 15 업그레이드 가이드도 page.js, layout.js, route.js, generateMetadata 같은 요청 시점 API에서 params가 비동기로 바뀌었다고 설명합니다. 따라서 이 글은 업그레이드 전체가 아니라, 동적 라우트 한 곳을 먼저 살리는 순서에만 집중합니다.

30초 안에 에러가 난 파일을 좁힙니다

에러가 보이면 먼저 app 폴더 안의 대괄호 경로를 찾습니다. 보통 문제는 이런 파일 중 하나에서 시작됩니다.

app/blog/[slug]/page.tsx
app/blog/[slug]/layout.tsx
app/api/posts/[id]/route.ts
app/blog/[slug]/generateMetadata 안의 params 사용 코드

초보자가 가장 자주 고치는 곳은 page.tsx입니다. 예전 코드나 AI가 만든 코드에는 아래처럼 params를 바로 구조분해하는 패턴이 남아 있을 수 있습니다.

type PageProps = {
  params: { slug: string };
};

export default async function Page({ params }: PageProps) {
  const { slug } = params;
  return <div>{slug}</div>;
}

Next.js 15에서는 이 줄이 문제의 중심입니다.

const { slug } = params;

여기서 확인할 것은 많지 않습니다. 동적 세그먼트 이름이 [slug]인지, 그 값을 params.slug처럼 바로 읽고 있는지만 먼저 봅니다. [id]라면 id, [productId]라면 productId가 대상입니다.

1분 안에 await params로 먼저 살립니다

가장 작은 수정은 params를 Promise로 받고, 안에서 await한 뒤 값을 꺼내는 방식입니다.

type PageProps = {
  params: Promise<{ slug: string }>;
};

export default async function Page({ params }: PageProps) {
  const { slug } = await params;
  return <div>{slug}</div>;
}

핵심은 두 줄입니다.

params: Promise<{ slug: string }>;
const { slug } = await params;

이미 컴포넌트가 async라면 위처럼 바로 바꿉니다. 컴포넌트가 async가 아니라면 서버 컴포넌트에서는 async를 붙이는 쪽이 보통 더 단순합니다. 이때 unrelated UI, fetch 로직, CSS를 같이 바꾸지 않습니다. 에러를 없애는 첫 수정 범위는 params 타입과 await 한 줄입니다.

Next.js 15 동적 라우트 params 에러를 고치는 3단계 체크리스트 이미지

generateMetadata에서도 같은 원칙을 씁니다.

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  return {
    title: slug,
  };
}

Route Handler에서는 두 번째 인자로 들어오는 context.params를 기다리는 형태가 자주 나옵니다.

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  return Response.json({ id });
}

여기까지 바꾼 뒤 npm run dev 또는 프로젝트에서 쓰는 개발 서버 명령을 다시 실행합니다. 에러가 사라지면 그 파일은 끝입니다. 같은 에러가 다른 파일에서 다시 나오면 동일한 패턴만 반복합니다.

2분째에는 타입 에러 문장을 그대로 읽습니다

Vercel 빌드나 next build에서 아래와 비슷한 TypeScript 에러가 보일 수 있습니다.

Type '{ slug: string }' is missing the following properties
from type 'Promise<any>': then, catch, finally

이 문장은 slug가 틀렸다는 뜻이 아닙니다. params 타입을 아직 예전 방식의 객체로 적었다는 뜻에 가깝습니다.

잘못된 타입은 보통 이렇게 생겼습니다.

type PageProps = {
  params: { slug: string };
};

Next.js 15 방식에 맞추면 이렇게 됩니다.

type PageProps = {
  params: Promise<{ slug: string }>;
};

searchParams도 같이 쓰는 페이지라면 같은 원칙으로 봅니다. Next.js의 동적 API 안내 문서는 params, searchParams, cookies(), headers(), draftMode()가 비동기 API라고 설명합니다. 다만 지금 글의 범위는 동적 라우트 params입니다. cookies()headers()까지 한 번에 고치려다 보면 문제 위치가 넓어집니다.

한 파일에서 params 에러가 사라진 뒤에야 다음 동적 API로 넘어갑니다. 그래야 AI가 만든 코드든 직접 쓴 코드든 어느 수정이 실제로 에러를 없앴는지 확인할 수 있습니다.

3분째에는 AI에게 전체 재작성을 막고 요청합니다

AI 코딩 도구에게 "이 에러 고쳐줘"라고만 말하면 페이지 전체를 다시 쓰거나, 잘 돌아가던 데이터 패칭까지 바꿀 수 있습니다. 요청은 작게 적어야 합니다.

Next.js 15 params 에러를 AI에게 최소 변경으로 고치게 요청하는 프롬프트 카드 이미지

아래 프롬프트를 그대로 붙여 넣고, 그 아래에 문제 파일 코드를 넣습니다.

Next.js 15 App Router에서 `params should be awaited` 에러가 납니다.

요청:
- 전체 페이지를 다시 작성하지 말고 params 관련 줄만 확인해 주세요.
- params 타입을 Promise 형태로 바꾸고, 값을 읽는 부분에 await를 추가해 주세요.
- page.tsx, layout.tsx, route.ts, generateMetadata 중 현재 파일에 해당하는 방식으로만 수정해 주세요.
- fetch 로직, CSS, 컴포넌트 구조, UI 문구는 바꾸지 마세요.
- 수정 전후 차이를 짧게 설명해 주세요.

문제 코드:
[여기에 파일 전체를 붙여 넣기]

AI가 수정안을 주면 먼저 이 두 가지를 봅니다.

1. params 타입이 Promise<{ ... }>로 바뀌었는가
2. params 값을 꺼내는 줄에 await가 들어갔는가

두 가지가 맞으면 저장하고 실행합니다. AI가 파일 구조를 크게 바꿨다면 그대로 적용하지 말고, params 관련 줄만 골라서 반영합니다. 이번 문제의 목표는 좋은 리팩터링이 아니라 빌드가 막힌 한 줄을 정확히 푸는 것입니다.

파일이 많으면 codemod를 쓰되 결과를 확인합니다

Next.js 15 공식 블로그와 업그레이드 가이드는 비동기 Request API 전환을 위한 codemod를 안내합니다. 여러 파일에서 같은 에러가 반복되면 아래 명령이 출발점이 될 수 있습니다.

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

다만 초보자 프로젝트에서는 먼저 한 파일을 손으로 고쳐보는 편이 좋습니다. 그러면 codemod가 바꾸는 내용도 이해할 수 있습니다. 바로 전체 변환을 돌리면 어떤 변경이 필요한 변경이고 어떤 변경이 검토 대상인지 구분하기 어렵습니다.

선택 기준은 간단합니다.

동적 라우트 한두 개만 깨짐: 손으로 수정
프로젝트 전체에서 반복됨: codemod 실행 후 diff 확인
AI가 만든 코드가 많음: 한 파일 수동 수정 후 같은 패턴만 적용

codemod를 돌린 뒤에는 git diff로 변경 파일을 봅니다.

git diff --name-only
git diff

바뀐 파일 중 params, searchParams, cookies, headers와 관련 없는 큰 UI 변경이 섞였다면 멈추고 다시 확인합니다. 공식 도구라도 프로젝트의 커스텀 타입과 생성 코드까지 완벽히 대신 판단하지는 않습니다.

마지막으로 실행 결과 세 가지만 봅니다

수정 후에는 코드만 보지 말고 같은 화면을 다시 엽니다.

1. 개발 서버에서 `params should be awaited` 경고가 사라졌는가
2. `npm run build` 또는 배포 빌드에서 Promise 타입 에러가 사라졌는가
3. `/blog/hello`처럼 실제 동적 주소가 정상 렌더링되는가

셋 중 하나라도 실패하면 다시 파일을 좁힙니다. page.tsx를 고쳤는데 generateMetadata가 남아 있거나, [slug]와 타입의 키 이름이 맞지 않는 경우가 많습니다.

오늘 기억할 기준은 하나입니다. Next.js 15 App Router에서 동적 라우트가 params 때문에 멈추면 params를 바로 꺼내지 말고 await params로 읽습니다. 그다음 타입을 Promise<{ slug: string }>처럼 맞추고, 같은 패턴이 남은 파일만 차례로 정리하면 됩니다.

참고 출처

다음으로 읽을 기사

같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.

댓글 0

이 글을 읽은 독자들의 생각을 나눠보세요.

비밀번호(선택)

첫 번째 댓글을 남겨보세요

여러분의 생각이 다른 독자에게 도움이 됩니다.