Next.js 16 App Router에 어려움을 겪고 있나요? 더 빠르고 스마트하게 마이그레이션하세요

발행: (2025년 11월 30일 오전 07:07 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Next.js 16에서 실제로 바뀐 점은?

Next.js 16은 애플리케이션 구조에 영향을 주는 여러 패러다임 변화를 도입했습니다.

1. Async paramssearchParams

Next.js 15 및 이전 버전에서는 paramssearchParams가 동기 객체였습니다. Next.js 16에서는 Promise가 되며 await해야 합니다.

Before (Next.js 15):

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  const { slug } = params; // ✅ Synchronous access
  return <h1>Post: {slug}</h1>;
}

After (Next.js 16):

// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params; // ⚠️ Now async!
  return <h1>Post: {slug}</h1>;
}

왜 이런 변화가 필요했을까? 스트리밍과 병렬 데이터 페칭이 개선됩니다. Next.js는 params가 해결되는 동안에도 렌더링을 시작할 수 있습니다.

2. Dynamic API가 이제 Async

cookies(), headers(), draftMode()와 같은 함수가 이제 Promise를 반환합니다.

Before:

import { cookies } from 'next/headers';

export async function getUser() {
  const cookieStore = cookies(); // Sync
  const token = cookieStore.get('auth-token');
  return fetchUser(token);
}

After:

import { cookies } from 'next/headers';

export async function getUser() {
  const cookieStore = await cookies(); // Now async
  const token = cookieStore.get('auth-token');
  return fetchUser(token);
}

3. Cache Components: 명시적 사용으로 전환

이전에는 페이지가 기본적으로 캐시되어 “stale data” 문제가 자주 제기되었습니다. Next.js 16에서는 다음과 같이 바뀝니다:

  • 모든 페이지가 기본적으로 동적 (요청당 렌더링)
  • 캐시를 원한다면 "use cache" 지시자를 명시적으로 사용

예시: 서버 컴포넌트 캐시하기

// app/blog/page.tsx
'use cache'; // Explicitly cache this component

export default async function BlogList() {
  const posts = await fetch('https://api.example.com/posts')
    .then((r) => r.json());

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  );
}

설정 파일에서 캐시 컴포넌트를 활성화합니다:

// next.config.ts
const nextConfig = {
  cacheComponents: true,
};

export default nextConfig;

4. 정교해진 Caching API

revalidateTag()는 이제 stale‑while‑revalidate 동작을 위한 cacheLife 프로필을 요구합니다.

Before:

import { revalidateTag } from 'next/cache';

revalidateTag('blog-posts'); // Simple invalidation

After:

import { revalidateTag } from 'next/cache';

// 대부분의 경우 'max' 사용을 권장
revalidateTag('blog-posts', 'max');

// 내장 프로필
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');

// 인라인 커스텀 프로필
revalidateTag('products', { expire: 3600 });

사용자 행동 직후와 같이 즉시 업데이트가 필요할 때는 새로운 updateTag() API를 사용합니다:

import { updateTag } from 'next/cache';

// Server Action 내부
export async function createPost(formData: FormData) {
  // Create post...
  updateTag('blog-posts'); // Immediate cache invalidation
}

단계별 마이그레이션 체크리스트

기존 Next.js 앱을 실용적으로 마이그레이션하는 방법입니다.

Step 1: 의존성 업데이트

npm install next@16 react@19 react-dom@19

또는 공식 codemod 실행:

npx @next/codemod@canary upgrade latest

Step 2: paramssearchParams를 Async로 만들기

params 또는 searchParams에 접근하는 모든 페이지 컴포넌트를 검색합니다:

# Find all pages accessing params
grep -r "params:" app/

각 컴포넌트를 다음과 같이 업데이트합니다:

// Before
export default function Page({ params, searchParams }: PageProps) {
  const { id } = params;
  const { filter } = searchParams;
}

// After
export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ filter?: string }>;
}) {
  const { id } = await params;
  const { filter } = await searchParams;
}

Step 3: Dynamic API 호출 업데이트

cookies(), headers(), draftMode() 호출에 await를 추가합니다:

// Before
const cookieStore = cookies();
const headersList = headers();

// After
const cookieStore = await cookies();
const headersList = await headers();

Step 4: Middleware를 Proxy로 마이그레이션

Next.js 16에서는 middleware.ts를 명확성을 위해 proxy.ts로 이름을 바꿉니다:

mv middleware.ts proxy.ts

내보내기를 업데이트합니다:

// Before (middleware.ts)
export default function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url));
}

// After (proxy.ts)
export default function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url));
}

Step 5: 캐싱 전략 검토

캐시가 필요한 페이지를 찾아 "use cache" 지시자를 추가합니다:

// For static content (blog, marketing pages)
'use cache';

export default async function MarketingPage() {
  // This component is now cached
}

흔히 발생하는 마이그레이션 실수 피하기

Pitfall 1: Sync와 Async 패턴 혼용

문제: 일부 위치에서 await를 빼먹고, 다른 곳에서는 정상적으로 사용함.

해결책: TypeScript의 strict 모드를 활성화해 컴파일러가 이런 오류를 잡게 합니다.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

Pitfall 2: 과도한 캐시 또는 캐시 부족

문제: 언제 "use cache"를 써야 할지 모름.

원칙:

  • "use cache" 를 사용하세요: 마케팅 페이지, 블로그 포스트, 제품 리스트, 문서 페이지 등 정적 콘텐츠
  • 동적 렌더링을 유지하세요: 사용자 대시보드, 실시간 데이터, 개인화 콘텐츠, 폼 등

Pitfall 3: Turbopack 무시

Next.js 16에서는 Turbopack이 기본 번들러가 되었습니다. 프로덕션 빌드가 2–5배 빠릅니다. 커스텀 Webpack 설정이 있다면 여전히 옵션을 선택할 수 있습니다:

next dev --webpack
Back to Blog

관련 글

더 보기 »

Bf-트리: 페이지 장벽을 깨다

안녕하세요, 저는 Maneshwar입니다. 저는 FreeDevTools – 온라인 오픈‑소스 허브를 개발하고 있습니다. 이 허브는 dev tools, cheat codes, 그리고 TLDRs를 한 곳에 모아 쉽게 이용할 수 있게 합니다.