Next.js 15에서 params와 searchParams 비동기 처리하기

2024. 11. 23. 16:15·FrontEnd/Next.js

Next.js 15를 공부하다가 params랑 searchParams를 예전처럼 동기 코드처럼 쓰다가 경고를 하나 맞았다.

처음에는 그냥 타입스크립트 경고 정도겠거니 했는데, 에러 메시지를 끝까지 읽어 보니 이제 Next.js 15에서는 요청 관련 API들이 비동기로 바뀌었다는 걸 알게 됐다.

  • 내가 실제로 만난 에러 상황 정리
  • 왜 이런 경고가 뜨는지
  • Codemod로 어떻게 자동 변환했는지
  • 최종적으로 코드를 어떻게 정리했는지

1. 처음 본 에러 메시지

서버 콘솔에 딱 이 에러가 찍혔다.

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

 

요약하면

  • "/shop/[id]" 라우트에서 params.id를 동기적으로 바로 썼고
  • 이제는 params를 먼저 await해서 풀어쓴 다음에 속성을 써야 한다는 이야기

공식 문서 링크를 타고 들어가 보니, Next.js 15부터는 몇 가지 요청 관련 API들이 비동기로 동작하는 동적 API로 묶여 있다는 걸 확인했다.


2. 왜 이런 경고가 나오는지

문서 기준으로 정리하면, Next.js 15에서는 아래 동적 API들이 비동기로 동작한다.

  • params, searchParams
    페이지, 레이아웃, 메타데이터 API, 라우트 핸들러에 props로 들어오는 애들
  • cookies(), draftMode(), headers()
    next/headers에서 제공하는 함수들

즉, 이제는 이 값들이 바로 값이 아니라 Promise로 래핑된 상태로 들어온다고 보면 된다.

공식 문서의 "Good to know" 부분에는 이런 문장이 있다.

실제로 값이 필요해질 때까지는 await이나 React.use로 Promise를 바로 풀지 말고 늦게까지 들고 있어라. 그러면 Next.js가 페이지의 더 많은 부분을 정적으로 렌더링할 수 있다.

 

요지는

  • 동적 API를 비동기로 유지해 두면
  • Next.js가 렌더링을 더 유연하게 최적화할 수 있고
  • 정적 렌더링 가능한 부분도 더 늘릴 수 있다

라는 느낌이다.


3. 문제였던 코드 정리

경고가 터졌던 코드는 대략 이런 형태였다.

3-1. 서버 컴포넌트에서 params 직접 사용

// /shop/[id]/page.tsx

export default function ShopDetail({ params }: { params: { id: string } }) {
  const detail = getShopDetail(params.id);
  return <div>{params.id}</div>;
}

Next.js 13, 14 기준으로 익숙한 패턴이라 그냥 이렇게 썼는데, 15에서는 params가 Promise 기준으로 바뀌어서 경고가 난다.

3-2. 서버 컴포넌트에서 searchParams 직접 사용

export default function Upload({
  searchParams,
}: {
  searchParams: { message: string };
}) {
  return (
    <div>
      {/* ... */}
    </div>
  );
}

여기도 마찬가지로, searchParams가 이제 Promise라서 그대로 쓰면 안 되는 상황

3-3. 라우트 핸들러에서 params 직접 구조 분해

// /shop/[id]/route.ts

import { NextRequest, NextResponse } from "next/server";

export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } },
) {
  const { id } = params;
  console.log("id:", id);

  return NextResponse.json({ message: "error" });
}

여기서도 params를 동기로 구조 분해해서 쓰고 있어서, 같은 경고를 보게 된다.


4. Codemod로 일단 자동 변환

문서를 계속 보다 보니, Next.js에서 공식으로 제공하는 Codemod가 있었다.

next-async-request-api라는 이름이고, 동적 API 관련 코드를 한 번에 바꿔 주는 툴이다.

설치는 이렇게 했다.

pnpm add -D @next/codemod@canary

프로젝트 루트에서 아래 명령으로 실행.

pnpm dlx @next/codemod@canary next-async-request-api .

실행하면

  • 서버 컴포넌트
  • 라우트 핸들러

등에서 params, searchParams, headers() 같은 동적 API들을 찾아서 Promise를 기준으로 쓰는 코드로 자동 변환해 준다.

물론 모든 케이스를 100퍼센트 커버해 주진 않아서, Codemod 돌린 뒤에 코드를 한 번씩 훑어 보면서 손을 봐야 한다.


5. Codemod 결과와 구조 분해 정리

Codemod를 돌렸더니 우선 이런 형태로 바뀌었다.

5-1. ShopDetail – Codemod 결과

export default async function ShopDetail(props: {
  params: Promise<{ id: string }>;
}) {
  const params = await props.params;
  const detail = getShopDetail(params.id);
  return <div>{params.id}</div>;
}

동작은 문제 없지만, 매개변수에서 바로 구조 분해하는 패턴이 더 익숙해서 아래처럼 한 번 더 정리했다.

export default async function ShopDetail({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const paramsResolved = await params;
  const detail = await getShopDetail(paramsResolved.id);

  return <div>{paramsResolved.id}</div>;
}
  • 매개변수에서 params만 구조 분해
  • 안쪽에서는 paramsResolved로 한 번 풀어서 사용

이 패턴이 나중에 봐도 덜 헷갈렸다.

5-2. Upload – Codemod 결과와 정리

Codemod가 만든 기본 형태는 대략 이런 느낌이었다.

export default async function Upload(props: {
  searchParams: Promise<{ message: string }>;
}) {
  const searchParams = await props.searchParams;
  // ...
}

여기도 마찬가지로 구조 분해해서 아래처럼 정리했다.

export default async function Upload({
  searchParams,
}: {
  searchParams: Promise<{ message: string }>;
}) {
  const { message } = await searchParams;

  return (
    <div>
      {/* message로 뭔가 처리 */}
      <p>{message}</p>
    </div>
  );
}

5-3. 라우트 핸들러 – Codemod 결과와 정리

Codemod가 라우트 핸들러도 같이 바꿔 준다.

import { NextRequest, NextResponse } from "next/server";

export async function GET(
  req: NextRequest,
  props: { params: Promise<{ id: string }> },
) {
  const params = await props.params;
  const { id } = params;

  console.log("id:", id);

  return NextResponse.json({ message: "error" });
}

이것도 구조 분해 쪽이 읽기 편해서 아래처럼 다시 바꿨다.

import { NextRequest, NextResponse } from "next/server";

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  console.log("id:", id);

  return NextResponse.json({ message: "error" });
}

서버 컴포넌트와 라우트 핸들러 모두 같은 패턴으로 통일해 두니까, 나중에 볼 때도 "이거 Promise였지" 하는 게 바로 떠올랐다.


6. 정리 메모 – Next.js 15에서 기억해 둘 것

지금까지 정리한 내용을 나중에 다시 볼 때를 위해 한 번 더 정리하면 아래 정도다.

  • Next.js 15에서는 몇 가지 요청 API가 비동기 동적 API로 바뀌었다
    • params, searchParams
    • cookies(), draftMode(), headers()
  • 이 값들은 이제 바로 값이 아니라 Promise라서, 속성을 쓰기 전에 await로 한 번 풀어야 한다
  • 기존 동기 코드가 많다면 next-async-request-api Codemod를 돌려서 뼈대부터 자동 변환해 두는 게 편하다
  • Codemod 결과는 props 형태라 가독성이 애매할 수 있으니, 구조 분해로 한 번 더 정리해 두면 좋다 
  • 서버 컴포넌트
    • function Page(props: { params: Promise<...> })
    • → function Page({ params }: { params: Promise<...> })
  • 라우트 핸들러
    • function GET(req, props: { params: Promise<...> })
    • → function GET(req, { params }: { params: Promise<...> })

앞으로 Next.js 15에서 params나 searchParams를 볼 때는

  • "이건 Promise다"
  • "언제 await로 풀지, 어디까지는 그대로 들고 갈지"

이 두 가지를 먼저 생각하고 코드를 짜는 게 좋겠다.

'FrontEnd > Next.js' 카테고리의 다른 글

Next.js 서버사이드 다국어(SSR) + SEO 테스트 기록 (next-intl, Vercel)  (0) 2026.02.27
Vercel 자동 배포 실패 – ERR_PNPM_LOCKFILE_CONFIG_MISMATCH  (0) 2025.12.12
'FrontEnd/Next.js' 카테고리의 다른 글
  • Next.js 서버사이드 다국어(SSR) + SEO 테스트 기록 (next-intl, Vercel)
  • Vercel 자동 배포 실패 – ERR_PNPM_LOCKFILE_CONFIG_MISMATCH
프론트엔드 개발자 jbeat
프론트엔드 개발자 jbeat
프론트엔드 개발자 블로그인데 일상도 쪼그으믐
  • 프론트엔드 개발자 jbeat
    jbeat 님의 블로그
    프론트엔드 개발자 jbeat
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • FrontEnd (43)
        • TypeScript (6)
        • JavaScript (18)
        • Next.js (3)
        • React (1)
        • Testing (2)
        • Third Party (1)
        • web (10)
        • Tooling (1)
        • coding test (0)
        • A.I (1)
      • 일상 (1)
        • wedding (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 태그

    고차함수
    pick
    Utility
    TypeScript
    CrossOrigin
    yjs
    코테
    omit
    배열
    컬렉션
    Next.js
    CRDT
    타입스크립트
    WebSocket
    playwright
    Android
    preconnect
    주니어
    이터러블
    javascript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
프론트엔드 개발자 jbeat
Next.js 15에서 params와 searchParams 비동기 처리하기
상단으로

티스토리툴바