Next.js 16 App Router에 어려움을 겪고 있나요? 더 빠르고 스마트하게 마이그레이션하세요
Source: Dev.to
Next.js 16에서 실제로 바뀐 점은?
Next.js 16은 애플리케이션 구조에 영향을 주는 여러 패러다임 변화를 도입했습니다.
1. Async params와 searchParams
Next.js 15 및 이전 버전에서는 params와 searchParams가 동기 객체였습니다. 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: params와 searchParams를 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