Next.js → CloudFront — 정적 빌드와 SSR 격리
output:export, SSR 3파일 패턴, React 18 고정, OAC 보안
Next.js → CloudFront
Next.js 앱을 S3 + CloudFront에 정적 배포하는 원리와 제약사항
정적 빌드(output: 'export')란
Next.js의 output: 'export'는 빌드 시 모든 페이지를 HTML 파일로 미리 생성합니다. S3에 업로드하고 CloudFront로 서빙하면 됩니다. 서버가 없어도 됩니다.
| 방식 | 서버 필요 | 우리 선택 이유 |
|---|---|---|
| SSR (서버 렌더링) | 필요 (Node.js 서버) | Lambda에 Node 서버 올리면 복잡해짐 |
| 정적 빌드 | 불필요 | S3 + CloudFront만으로 서빙 가능, 비용 최소화 |
📋 next.config.js — 정적 빌드 설정
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // 정적 HTML/JS/CSS로 빌드
trailingSlash: true, // /page → /page/ (S3 폴더 구조에 맞춤)
images: {
unoptimized: true, // 이미지 최적화 서버 없음 — 원본 그대로
},
}
module.exports = nextConfig
out/ 폴더에 생성됩니다. pnpm build를 실행하면 out/index.html, out/_next/static/ 등의 파일이 생성됩니다. 이 폴더 전체를 S3에 업로드합니다.API Routes 사용 불가 — 왜 /api/를 만들 수 없는가
Next.js의 app/api/ Route Handlers는 Node.js 서버에서 실행됩니다. 정적 빌드에서는 서버가 없으므로 API Routes를 사용할 수 없습니다.
app/api/login/route.ts정적 빌드 시 빌드 오류 또는 런타임에 404. "API routes cannot be used with
output: 'export'" 에러.
NEXT_PUBLIC_API_URL=https://api.execute-api.../prod로 환경변수에 API URL 저장 후 fetch.
SSR 격리 3파일 패턴 — MSAL이 서버에서 실행되면 안 된다
MSAL은 window, localStorage, sessionStorage에 의존합니다. 이것들은 브라우저에만 존재하는 객체입니다. Next.js는 정적 빌드 시에도 서버(Node.js)에서 페이지를 한 번 렌더링(pre-render)합니다. MSAL이 이 때 실행되면 window is not defined 오류가 납니다.
'use client'MSAL PublicClientApp 생성
MsalProvider로 감쌈
모든 인증 로직 여기에
next/dynamic으로
ClientOnlyAuth 로드
SSR 비활성화
'use client'AuthProvider를 실제로 사용
이 파일을 dynamic으로 로드
📋 page.tsx — ClientOnlyAuth를 dynamic으로 로드
import dynamic from 'next/dynamic'
// ssr: false → 브라우저에서만 로드됨. 서버 pre-render 시 건너뜀.
const ClientOnlyAuth = dynamic(
() => import('./ClientOnlyAuth'),
{ ssr: false }
)
export default function HomePage() {
return (
)
}
dynamic을 쓰면 안 됩니다. dynamic(() => import('@azure/msal-react'), {ssr:false})처럼 사용하면 동작하지 않습니다 — msal-react는 default export가 없는 패키지입니다. 반드시 위의 3파일 패턴을 사용하세요: AuthProvider.tsx → ClientOnlyAuth.tsx → page.tsx의 dynamic import.React 18 고정 — React 19에서 빌드가 실패하는 이유
React 19가 릴리즈되면서 output: 'export'와 함께 사용 시 pre-render 단계에서 특정 훅 사용에 대한 경고가 빌드 오류로 격상되는 문제가 생겼습니다.
📋 package.json — React 18 고정
{
"dependencies": {
"react": "18.3.1", // 18.x로 고정 — 19.x는 output:export와 충돌
"react-dom": "18.3.1",
"next": "14.2.x" // Next.js 14.x와 함께 사용
}
}
package.json의 "pnpm" 필드에 "overrides": {"react": "18.3.1", "react-dom": "18.3.1"}를 추가하면 의존성 트리 전체에서 React 18을 사용하도록 강제할 수 있습니다.OAC — S3 버킷을 잠그고 CloudFront만 허용
OAC(Origin Access Control)는 CloudFront가 S3에 접근할 수 있도록 하면서 S3 버킷은 인터넷에서 직접 접근 불가하도록 만드는 설정입니다.
📋 S3 버킷 정책 — CloudFront OAC만 허용
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-frontend-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789:distribution/ABCDE123"
}
}
}]
}
| 방식 | 차이 | 권장 |
|---|---|---|
| OAC (Origin Access Control) | 신형. IAM 기반. 서명 버전 4. | ✅ 사용 |
| OAI (Origin Access Identity) | 구형. 레거시. 신규 기능 없음. | ❌ 쓰지 않음 |
캐시 전략 — HTML과 JS/CSS를 다르게 캐싱하는 이유
정적 빌드에서 _next/static/ 폴더 내 파일들은 파일명에 콘텐츠 해시가 포함됩니다 (_next/static/abc123.js). 코드가 바뀌면 파일명이 바뀝니다. 따라서 이 파일들은 영구 캐싱이 안전합니다. 반면 index.html은 파일명이 변하지 않으므로 캐싱하면 변경사항이 반영되지 않습니다.
📋 CloudFront 배포 후 HTML 캐시 무효화
# 코드 배포 후 반드시 HTML 파일 캐시 무효화
aws cloudfront create-invalidation \
--distribution-id E1234567890 \
--paths "/*.html" "/index.html" "/*"
# 또는 전체 무효화 (느리지만 확실)
aws cloudfront create-invalidation \
--distribution-id E1234567890 \
--paths "/*"
Azure Portal 리다이렉트 URI 등록 — 필수 사전 작업
Azure AD는 로그인 후 사용자를 특정 URL로 리다이렉트합니다. 이 URL이 Azure Portal에 사전 등록되어 있지 않으면 "AADSTS50011: 응답 URL이 일치하지 않습니다" 오류가 발생합니다.
| 환경 | 등록해야 할 URI |
|---|---|
| 로컬 개발 | http://localhost:3000 |
| 프로덕션 (CloudFront) | https://xxxx.cloudfront.net |
| 커스텀 도메인 (있을 경우) | https://app.example.com |
등록 경로: Azure Portal → Azure Active Directory → 앱 등록 → 해당 앱 → 인증 → 리다이렉트 URI 추가 → 저장