PromleeBlog
sitemap
aboutMe

posting thumbnail
아키텍처와 캐싱 시스템 - Next.js 16 특징 2부
Architecture and Caching System - Next.js 16 Features Part 2

📅

🚀

들어가기 전에 🔗

안녕하세요.
지난 1부에서는 Turbopack이 가져온
속도의 혁신
에 대해 다뤘습니다.

이번 2부에서는 Next.js 16의 심장부라고 할 수 있는
아키텍처
캐싱 시스템
의 근본적인 변화를 코드 레벨에서 깊이 있게 파헤쳐 보겠습니다.
단순히 API가 바뀐 것을 넘어, 왜 Next.js가
비동기
명시적 캐싱
을 선택했는지 그 원리를 이해한다면 마이그레이션 작업이 훨씬 수월해질 것입니다.

🚀

1. 동기(Sync)에서 비동기(Async)로: I/O의 진화 🔗

Next.js 16에서 가장 개발자들을 당황하게 만들 수 있는 변화는
동적 API의 비동기화
입니다.
서버 컴포넌트에서 런타임 정보인 쿠키(Cookies), 헤더(Headers), URL 파라미터(Params) 등에 접근할 때, 이제는 반드시 await 를 사용해야 합니다.

왜 이렇게 바뀌었나요? 🔗

기존의 동기 방식은 데이터를 가져오는 동안 서버의 메인 스레드가 멈추는
블로킹(Blocking)
현상을 유발했습니다.
Node.js는
이벤트 루프(Event Loop)
기반으로 동작하므로, I/O 작업은 비동기로 처리해야 CPU 자원을 효율적으로 사용할 수 있습니다.
Next.js 팀은 이를 위해 모든 런타임 데이터 접근을
Promise
로 변경하여, 서버가 렌더링 준비 중에 다른 작업을 병렬로 처리할 수 있도록 최적화했습니다.

변경 전후 코드 비교 🔗

app/blog/[slug]/page.tsx
import { cookies } from 'next/headers';
 
// [변경 포인트 1] Props 타입 정의가 Promise로 감싸져야 합니다.
// 이전: { params: { slug: string } }
// 이후: { params: Promise<{ slug: string }> }
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  
  // [변경 포인트 2] params는 이제 Promise 객체이므로 await로 풀어야 합니다.
  // 만약 await 없이 params.slug로 접근하면 에러가 발생합니다.
  const { slug } = await params;
 
  // [변경 포인트 3] cookies() 함수 호출 결과도 Promise입니다.
  // 데이터베이스 조회 등 다른 비동기 작업과 병렬로 처리될 수 있습니다.
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme');
  
  return (
    <div className={theme?.value}>
      <h1>Post Slug: {slug}</h1>
    </div>
  );
}

🚀

2. 캐싱의 패러다임 변화: Cache Components 🔗

그동안 Next.js의 캐싱은 "어떤 건 자동이고 어떤 건 수동인지 헷갈린다"는 의견이 많았습니다.
16 버전에서는 이를 해결하기 위해
명시적(Explicit)
인 캐싱 제어 방식인 'use cache' 디렉티브를 도입했습니다.

'use cache' 사용법 🔗

복잡한 next.config.js 설정이나 fetch 옵션을 건드릴 필요가 없습니다.
캐싱하고 싶은 함수나 컴포넌트 상단에 한 줄만 추가하면 됩니다.
app/components/StockPrice.tsx
import { db } from '@/lib/db';
 
// symbol(종목코드)을 인자로 받아 가격을 조회하는 함수
export async function StockPrice({ symbol }: { symbol: string }) {
  // [핵심] 이 디렉티브를 선언하면 함수 실행 결과가 자동으로 캐싱됩니다.
  'use cache'; 
  
  // 캐시 키(Cache Key)는 자동으로 관리됩니다.
  // 인자로 들어온 'symbol' 값이 같다면, 아래 DB 조회 로직은 실행되지 않고
  // 저장된 캐시 값을 즉시 반환합니다. (Memoization 원리)
  const price = await db.stock.findUnique({ 
    where: { symbol },
    select: { price: true }
  });
 
  return (
    <div className="p-4 border rounded">
      <h3>{symbol}</h3>
      <p>Current Price: ${price}</p>
    </div>
  );
}
이 방식은 우리가 자료구조/알고리즘 시간에 배우는
메모이제이션(Memoization)
과 원리가 같습니다.
동일한 입력(symbol)에 대해
비싼 연산
(DB 조회)을 반복하지 않음으로써 성능을 극대화합니다.

🚀

3. 정교해진 데이터 갱신 (Revalidation) 🔗

캐시된 데이터를 언제, 어떻게 업데이트할지에 대한 API도 훨씬 구체적으로 진화했습니다.
상황에 따라 적절한 도구를 선택하는 것이 중요합니다.

상황별 API 선택 가이드 🔗

Next.js 16은 데이터의 성격에 따라 3가지 방식을 제안합니다.
  1. revalidateTag
    : "조금 늦게 업데이트돼도 괜찮아" (SWR 전략)
  2. updateTag
    : "수정 즉시 반영돼야 해" (Read-Your-Writes)
  3. refresh
    : "서버 데이터는 그대로 두고 화면만 새로고침해"
app/actions/productActions.ts
'use server';
 
import { revalidateTag, updateTag, refresh } from 'next/cache';
import { db } from '@/lib/db';
 
// 상황 1: 관리자가 상품 정보를 대량으로 수정했을 때
// 사용자는 1~2초 정도 옛날 가격을 봐도 큰 문제가 없는 경우
export async function updateBulkProducts() {
  await db.product.updateMany({ ... });
  
  // [SWR] 백그라운드에서 캐시를 갱신합니다. 
  // 'max'는 캐시 수명을 최대로 설정하겠다는 프로필입니다.
  revalidateTag('all-products', 'max'); 
}
 
// 상황 2: 사용자가 자신의 프로필을 수정했을 때
// 저장 버튼을 누르자마자 바뀐 이름이 보여야 함
export async function updateProfile(userId: string, data: any) {
  await db.user.update({ where: { id: userId }, data });
 
  // [Read-Your-Writes] 캐시를 즉시 만료(Purge)시키고 새로 데이터를 가져옵니다.
  updateTag(`user-${userId}`); 
}
 
// 상황 3: 사용자가 알림을 읽었을 때
// 알림 목록 데이터 자체는 갱신할 필요가 없고, 헤더의 '뱃지 숫자'만 바꾸고 싶을 때
export async function readNotification(id: string) {
  await db.notification.markAsRead(id);
 
  // 서버 캐시를 건드리지 않고, 클라이언트 라우터만 새로고침(Refresh)합니다.
  refresh(); 
}

🚀

4. Middleware에서 Proxy로 🔗

프로젝트 루트에 있던 middleware.ts 파일의 이름이
proxy.ts
로 변경되었습니다.
단순한 이름 변경 같지만, 여기에는 "네트워크 경계를 명확히 하겠다"는 아키텍처 철학이 담겨 있습니다.
프록시 패턴 (Proxy Pattern)
프록시는 클라이언트와 실제 서버 사이에서 중계자 역할을 하는 디자인 패턴입니다.
Next.js의
Proxy
는 모든 요청의 최전선(Edge)에서 인증을 확인하거나, 경로를 재작성(Rewrite)하는
보안 문지기
역할을 수행합니다.
proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
// [변경] 파일명이 middleware.ts -> proxy.ts로 변경되었습니다.
// [변경] 함수 이름도 export function middleware -> proxy로 변경해야 합니다.
export function proxy(request: NextRequest) {
  
  // 기존 미들웨어 로직은 그대로 사용 가능합니다.
  // 예: /dashboard로 들어오는 요청에 인증 토큰이 없으면 로그인 페이지로 리다이렉트
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!request.cookies.has('token')) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
 
  return NextResponse.next();
}

🚀

5. Parallel Routes의 엄격해진 규칙 🔗

병렬 라우트(Parallel Routes)
를 사용할 때의 규칙도 강화되었습니다.
이전에는 슬롯(@folder)에 대응하는 UI가 없으면 404 에러가 나거나 암묵적으로 처리되었지만, 이제는 default.js 파일이 없으면
빌드 자체가 실패
합니다.
app/@analytics/default.tsx
// 슬롯에 보여줄 기본 컨텐츠가 없는 경우라도 파일은 반드시 존재해야 합니다.
export default function Default() {
  // 아무것도 렌더링하지 않음을 명시적으로 선언합니다.
  return null;
}

🚀

결론 🔗

Next.js 16의 아키텍처 변화를 한마디로 요약하면
명확성(Clarity)
입니다.
비동기 API
로 성능 병목의 원인을 없애고, 'use cache' 로 개발자의 캐싱 의도를 코드에 명확히 드러내며, Proxy 로 네트워크 역할을 정의했습니다.
이러한 변화들은 처음에는 코드를 수정해야 해서 번거로울 수 있지만, 장기적으로는 애플리케이션의
유지보수성
예측 가능성
을 크게 높여줄 것입니다.
다음 3부에서는 이 모든 변경 사항을 실제 프로젝트에 적용하는 마이그레이션 가이드로 찾아오겠습니다.

참고 🔗