주제:  SSR/ISR/RSC, 라우트 핸들러, 이미지·CDN 최적화
TL;DR (체크리스트)
- 렌더링 전략을 먼저 고정: Static(SSG) → ISR → SSR(스트리밍) 순서로 우선
- 데이터 패칭 표준: fetch(..., { next: { revalidate, tags }, cache })
- 변경 알림 표준: 서버 액션/웹훅 → revalidateTag()
- 이미지: next/image+sizes필수, remotePatterns 화이트리스트
- 클라이언트 JS 다이어트: RSC 기본, use client최소화, dynamic import
- 엣지 활용: 인증 제외 읽기 전용 API/경량 페이지는 runtime='edge'
- 관측성: instrumentation.ts+ Server-Timing/traceparent 전파
1) 성능 목표(웹 지표)
- TTFB p95 < 300ms(캐시 히트 시 100ms대)
- LCP p75 < 2.5s, CLS < 0.1, INP p75 < 200ms
- 페이지 JS < 200KB gz(초기 로드)
2) 렌더링 전략 맵
- 정적(SSG/ISR): 변동이 느린 컨텐츠(마케팅/문서/목록)
- SSR(스트리밍): 사용자별/세션 의존, 빠른 TTFB + 점진 렌더
- RSC(Server Components): 데이터와 컴포넌트 결합, 클라 JS 감소
// app/products/[id]/page.tsx (ISR + 태그 캐시)
export const revalidate = 120; // 초
export default async function Page({ params:{id} }:{
  params:{id:string}
}) {
  const data = await fetch(`${process.env.API}/products/${id}`, {
    next: { revalidate, tags: [`product:${id}`, 'products'] }
  }).then(r=>r.json());
  return <ProductView data={data} />;
}
3) 데이터 패칭 규칙(중요)
- 읽기: fetch에next: { revalidate, tags }또는cache:'no-store'
- 쓰기 후 무효화: 서버 액션/웹훅 → revalidateTag('product:123')
- BFF 라우트 핸들러에서 ETag/Cache-Control 헤더 부여
// app/actions/updateProduct.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, dto: any) {
  await fetch(`${process.env.API}/products/${id}`, { method:'PUT', body: JSON.stringify(dto) })
  revalidateTag(`product:${id}`)         // 목록/상세 동시 무효화
  revalidateTag('products')
}
// app/api/public/products/[id]/route.ts (BFF + 캐시 헤더)
import { NextResponse } from 'next/server'
export const runtime = 'nodejs' // 인증/서명 필요시 node 런타임
export async function GET(_:Request,{params}:{params:{id:string}}) {
  const r = await fetch(`${process.env.API}/products/${params.id}`, { cache:'no-store' })
  const data = await r.json()
  const res = NextResponse.json(data)
  res.headers.set('Cache-Control','public, max-age=60, stale-while-revalidate=120')
  return res
}
4) 스트리밍 SSR UX 패턴
- loading.tsx로 첫 스켈레톤
- 부분적 비싼 영역은 Suspense경계 분리(데이터 지연 컴포넌트 뒤로)
// app/app/page.tsx
import { Suspense } from 'react'
export default function Page(){
  return (
    <>
      <Hero/>
      <Suspense fallback={<ListSkeleton/>}>
        {/* 비싼 서버 컴포넌트 */}
        <TopProducts/>
      </Suspense>
    </>
  )
}
5) 이미지 & CDN 최적화
- next/image에 반드시- sizes지정(가로폭 조건식)
- 큰 리스트는 우선순위 한정(priority는 Fold 상단 1~2개만)
- 외부 이미지 사용 시 next.config.js→images.remotePatterns
import Image from 'next/image'
export default function Card({img}:{img:string}) {
  return (
    <div className="relative h-48">
      <Image
        src={img}
        alt=""
        fill
        sizes="(max-width:768px) 100vw, (max-width:1200px) 50vw, 33vw"
        placeholder="empty" // blurDataURL 준비 시 'blur'
      />
    </div>
  )
}
// next.config.js
module.exports = {
  images: {
    remotePatterns: [{ protocol:'https', hostname:'cdn.example.com' }]
  }
}
정적 자산은 Cache-Control: max-age=31536000, immutable (빌드 해시 파일).
6) 엣지 런타임 가이드
- 사용 추천: 읽기 전용 API, 퍼스널라이제이션 없는 공개 페이지
- 피해야: 비밀 접근, 대형 종속성, Node 전용 API 필요 시
export const runtime = 'edge' // 파일 상단
7) 클라이언트 JS 다이어트
- 기본은 서버 컴포넌트; 이벤트 필요 부분만 use client
- 무거운 위젯은 dynamic import(ssr:false가능)
- 상태관리 최소화(Zustand 등도 client boundary 안으로만)
// app/(client)/Chart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(()=>import('./_chart'), { ssr:false })
export default function ChartWrap(){ return <Chart/> }
8) 라우트 핸들러 운영 팁(BFF)
- 인증·세션 처리 후 서버→API 호출(서비스 토큰/HMAC)
- ETag/If-None-Match로 304 응답
- 레이트리밋 헤더(RFC 9230) 부여(6편 참고)
// 304 처리 예
import crypto from 'crypto'
export async function GET(req:Request) {
  const data = await getData()
  const etag = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex')
  if (req.headers.get('if-none-match')===etag) return new Response(null,{status:304})
  const res = new Response(JSON.stringify(data),{ headers:{ 'ETag': etag, 'Content-Type':'application/json' }})
  return res
}
9) SEO/메타데이터 & 국제화
- export const metadata = {...}로 타이틀/OG/Twitter 카드 설정
- 다국어는 도메인/서브패스 라우팅 + generateMetadata에서 locale별 태그
export const metadata = {
  title: 'EX | XR/AI Platform',
  openGraph: { title:'EX', type:'website' }
}
10) 에러/로딩/경계
- error.tsx(사용자 친화 메시지),- not-found.tsx
- API 실패는 서버 컴포넌트에서 throw → error.tsx로 위임
- 로그인 요구 페이지는 middleware.ts로 보호
11) 관측성(instrumentation.ts)
- Next.js OTel 지원으로 서버 요청 트레이스/메트릭 노출
- 응답에 Server-Timing·traceparent추가하여 백엔드와 연결
// instrumentation.ts
import { registerOTel } from '@vercel/otel' // (Vercel 사용 시)
export function register() {
  registerOTel({ serviceName: 'web' })
}
// app/api/_lib/withTrace.ts
export async function withTrace<T>(name:string, f:()=>Promise<T>) {
  const start = Date.now()
  try { return await f() }
  finally {
    const dur = Date.now()-start
    // 헤더 주입 (Route Handler 내에서)
    // res.headers.set('Server-Timing', `app;dur=${dur}`)
  }
}
12) 보안 헤더 & 미들웨어
- HSTS, CSP, Referrer-Policy, X-Frame-Options(=DENY)
- 봇/스크래핑 차단, 업로드 라우트에 바디 크기 제한
- 인증이 필요한 API는 cache:'no-store'강제
13) 빌드/배포 최적화
- next build후 analyze로 번들 크기 점검(중복 deps 제거)
- Edge/Node 런타임 혼합 시 라우트 분리로 콜드스타트 감소
- CI에서 lint/test → build → prerender 결과 확인 → 배포
14) 2주 액션 플랜
- 페이지/라우트 렌더링 전략 표 확정(SSG/ISR/SSR 구분)
- 상위 5개 페이지에 revalidate/tags적용, 서버 액션에서revalidateTag연결
- next/image전면 적용 +- sizes/remotePatterns 세팅
- 클라 JS 다이어트: use client제거/경계 재배치 + dynamic import
- 라우트 핸들러에 ETag/Cache-Control/RateLimit 헤더 도입
- instrumentation.ts로 OTEL 연결 + Server-Timing 노출
- Lighthouse CI/Playwright로 성능·회귀 테스트 파이프라인 추가
15) “복붙” 모음
A. 데이터 패칭 공통 래퍼
export async function api<T>(path:string, opt?:RequestInit & { revalidate?: number, tags?: string[] }) {
  const { revalidate, tags, ...init } = opt || {}
  const res = await fetch(`${process.env.API}${path}`, {
    ...init,
    next: { revalidate, tags },
    headers: { 'Accept':'application/json', ...(init.headers||{}) },
  })
  if (!res.ok) throw new Error(`API ${res.status}`)
  return res.json() as Promise<T>
}
B. 전역 이미지 컴포넌트 프리셋
import Image, { ImageProps } from 'next/image'
export function Img(props: ImageProps){
  return <Image placeholder="empty" sizes="(max-width:768px) 100vw, 50vw" {...props}/>
}
C. Edge용 공개 목록 API
// app/api/public/items/route.ts
export const runtime = 'edge'
export async function GET() {
  const r = await fetch(`${process.env.API}/items`, { next:{ revalidate: 60, tags:['items'] } })
  return new Response(await r.text(), {
    headers: { 'Content-Type':'application/json', 'Cache-Control':'public, max-age=60, stale-while-revalidate=120' }
  })
}
마무리
Next.js는 캐시·무효화·스트리밍 3가지를 제대로 쓰는 순간 스케일이 달라집니다. 브라우저에는 작은 JS, 엣지에는 빠른 캐시, 서버에는 견고한 BFF—이 조합을 습관화하세요.
다음은 8편. 가시성(Observability) — OpenTelemetry/로그/메트릭/트레이싱 & SLO로 갑니다.
답글 남기기