사용자 브라우저
↓
CloudFront
↓
S3 Bucket
↓
index.html, JS, CSS, 이미지
Nginx 서버 직접 제공:
서버 관리 필요
서버 장애 시 프론트도 영향
S3 + CloudFront:
정적 파일은 CDN에서 제공
백엔드와 프론트 장애 범위 분리
| 구성요소 | 역할 |
|---|---|
| S3 | 정적 파일 원본 저장소 |
| CloudFront | CDN, 전 세계 엣지에서 파일 캐싱 |
| Route 53 | 도메인 DNS 연결 |
| ACM | HTTPS 인증서 |
| GitHub Actions | 빌드/업로드 자동화 |
| IAM | 배포 권한 제어 |
S3:
빌드 결과물 저장
예:
dist/index.html
dist/assets/index-abc123.js
dist/assets/index-def456.css
dist/images/banner.webp
CloudFront:
사용자와 가까운 엣지 서버에서 정적 파일 제공
장점:
속도 개선
S3 직접 접근 차단 가능
HTTPS 적용
캐시 정책 제어
ACM:
CloudFront에 연결할 HTTPS 인증서 관리
주의:
CloudFront용 ACM 인증서는 us-east-1 리전에 만들어야 함
npm run build를 실행하면 보통 dist 폴더가 생성됩니다.npm run build
dist/
index.html
assets/
index-B2k3f9.js
index-A8c2d1.css
images/
logo.webp
banner.webp
| 파일 | 특징 |
|---|---|
index.html | 앱의 진입점 |
| JS/CSS assets | hash가 붙는 경우 많음 |
| 이미지 | 상품/배너/로고 |
| favicon/manifest | 브라우저/앱 메타 정보 |
index.html:
항상 최신이어야 함
hashed JS/CSS:
파일명이 바뀌면 새 버전이므로 오래 캐시 가능
index.html과 assets를 같은 캐시 정책으로 두면 배포 후 문제가 생길 수 있습니다.1. 코드 수정
2. npm run lint
3. npm run test:run
4. npm run build
5. dist 파일 생성
6. S3로 업로드
7. CloudFront invalidation
8. 운영 URL Smoke Test
aws s3 sync dist/ s3://YOUR_BUCKET_NAME --delete
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
운영 URL 접속
새 버전 반영 확인
상품 상세 접속
상담 신청 모달 확인
관리자 로그인 확인
상담 목록 조회 확인
브라우저 Network 탭에서 API 주소 확인
사용자
↓
S3 Website Endpoint 직접 접근
장점:
설정이 단순함
빠르게 테스트 가능
단점:
S3가 직접 공개됨
CloudFront 우회 접근 가능
보안/캐시/도메인 관리가 애매해질 수 있음
사용자
↓
CloudFront
↓
S3 private bucket
장점:
S3 직접 접근 차단
CloudFront를 통해서만 파일 제공
HTTPS/캐시/도메인 관리 일원화
| 구분 | 의미 |
|---|---|
| OAI | Origin Access Identity |
| OAC | Origin Access Control |
OAI/OAC:
CloudFront는 S3에 접근 가능
일반 사용자는 S3 직접 접근 불가
S3 public access block 유지
CloudFront OAC 생성
S3 bucket policy에서 CloudFront distribution만 허용
사용자는 CloudFront 도메인 또는 커스텀 도메인으로 접속
/products/1, /admin/consults 같은 경로가 실제 파일이 아닙니다.사용자:
https://www.example.com/products/1 직접 접속
CloudFront/S3:
products/1 파일 찾음
결과:
파일 없음 → 404
없는 경로 요청
↓
index.html 반환
↓
React Router가 클라이언트에서 경로 처리
403 또는 404 응답 발생
↓
/index.html 반환
↓
HTTP status 200으로 응답
/index.html로 돌려주는 설정을 확인해야 합니다./api 요청과 SPA fallback 충돌 주의/api 경로를 API로 쓰는 경우, SPA fallback이 API 요청까지 index.html로 돌려버리면 안 됩니다.정상:
https://example.com/api/products → API 서버
문제:
https://example.com/api/products → index.html
서브도메인 분리:
www.example.com
api.example.com
경로 분리:
example.com/api
example.com/*
프론트 S3 + CloudFront:
www.example.com
백엔드 API:
api.example.com
장점:
CloudFront SPA fallback과 API 라우팅 충돌 감소
CORS/쿠키 정책만 명확히 관리하면 됨
| 파일 | 캐시 전략 |
|---|---|
index.html | 짧게 또는 no-cache |
| hashed JS/CSS | 길게 |
| 이미지 | 파일명 변경 기준으로 길게 |
| favicon/manifest | 상황에 따라 중간 |
| 설정 JSON | 짧게 |
index.html:
항상 최신이어야 함
assets/index-abc123.js:
파일명이 바뀌면 새 파일이므로 오래 캐시 가능
사용자 브라우저:
이전 index.html 캐시 보유
S3:
이전 JS 파일은 삭제됨
사용자:
이전 index.html이 이전 JS 파일 요청
결과:
Chunk Load Error 또는 흰 화면
index.html을 오래 캐시하는 것은 위험합니다.aws s3 sync로 업로드할 때 파일별 Cache-Control을 다르게 지정할 수 있습니다.aws s3 cp dist/index.html s3://YOUR_BUCKET_NAME/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"
aws s3 sync dist/assets/ s3://YOUR_BUCKET_NAME/assets/ \
--cache-control "public, max-age=31536000, immutable"
aws s3 sync dist/ s3://YOUR_BUCKET_NAME \
--exclude "index.html" \
--exclude "assets/*"
index.html과 hashed assets의 캐시 정책을 분리하는 것이 좋습니다.--delete 사용 주의aws s3 sync dist/ s3://bucket --delete는 dist에 없는 S3 파일을 삭제합니다.사용자가 이전 버전 페이지를 열어둠
↓
새 배포 시 --delete로 이전 JS 삭제
↓
사용자가 페이지 이동
↓
이전 JS chunk 요청
↓
파일 없음
↓
Chunk Load Error
이전 assets를 일정 기간 보존
index.html은 최신화
오래된 assets 정리는 주기적으로 수행
ChunkLoadError 발생 시 새로고침 안내
작은 서비스:
--delete 사용 가능, 문제 발생 시 새로고침 안내
관리자 작업 중단이 민감한 서비스:
assets 즉시 삭제 지양
배포 안정성 높이고 싶을 때:
버전별 prefix 배포 검토
--delete는 편하지만 운영 사용자가 페이지를 열어둔 상태를 고려해야 합니다.aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
장점:
간단함
모든 파일 최신화 가능
작은 서비스에서 관리 쉬움
단점:
불필요한 무효화가 많음
규모가 커지면 비용/효율 고려 필요
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/index.html" "/"
장점:
hashed assets는 파일명이 바뀌므로 그대로 장기 캐시 가능
무효화 범위가 작음
초기/작은 서비스:
전체 invalidation으로 단순 운영
캐시 전략 정리 후:
index.html, root 경로 중심 invalidation
이미지 파일명 변경 없이 덮어쓰기:
해당 이미지 경로도 invalidation 필요
s3://bucket/releases/2026-08-05-001/
index.html
assets/
s3://bucket/releases/2026-08-05-002/
index.html
assets/
이전 버전 보존
롤백 쉬움
assets 삭제로 인한 chunk error 감소
배포 이력 명확
설정 복잡
S3 용량 증가
CloudFront origin/path 관리 필요
배포 스크립트 복잡
이전 안정 커밋 checkout
↓
운영 env로 build
↓
S3 재업로드
↓
CloudFront invalidation
↓
Smoke Test
배포마다 dist artifact 저장
↓
문제 발생 시 이전 artifact를 S3에 재업로드
↓
CloudFront invalidation
백엔드 API 변경과 호환되는가?
환경변수 변경이 있었는가?
프론트만 롤백해도 되는가?
CloudFront 캐시 무효화가 필요한가?
사용자에게 강제 새로고침 안내가 필요한가?
main branch push
↓
checkout
↓
node setup
↓
install
↓
lint/test/build
↓
AWS credentials 설정
↓
S3 upload
↓
CloudFront invalidation
name: Frontend Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install
run: npm ci
- name: Verify
run: |
npm run lint
npm run test:run
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Upload assets
run: |
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_NAME }} --delete
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
GitHub Secrets에 AWS key 저장
운영 배포 branch 제한
PR에서는 build/test만 실행
main merge 후 배포
배포 실패 시 알림 필요
AWS 권한 최소화
S3:
대상 버킷 PutObject, DeleteObject, ListBucket
CloudFront:
CreateInvalidation
그 외:
가능하면 불필요한 권한 제거
AdministratorAccess 금지
개인 AWS key와 회사 배포 key 분리
GitHub Secrets 외부 노출 주의
퇴사/교체 시 key rotation
CloudTrail로 사용 기록 확인 가능하게
.env.production
↓
npm run build
↓
JS bundle에 값 포함
↓
S3 업로드
VITE_API_BASE_URL 변경 시 재빌드 필요
운영 빌드에 localhost 들어가면 치명적
프론트 env에 Secret 넣으면 노출
스테이징/운영 env 분리 필요
grep -R "localhost" dist/ || true
localhost가 들어가 있으면 거의 확실히 문제입니다.VITE_API_BASE_URL이 운영 API를 가리키는지 확인해야 합니다.www.example.com, API를 api.example.com으로 분리하면 브라우저 기준 origin이 달라집니다.Frontend:
https://www.example.com
API:
https://api.example.com
브라우저:
서로 다른 origin으로 판단
app.enableCors({
origin: ['https://www.example.com', 'https://admin.example.com'],
credentials: true,
});
credentials: true
Access-Control-Allow-Origin은 * 불가
쿠키 secure true
sameSite 설정 확인
API 요청 withCredentials 설정
export const apiClient = axios.create({
baseURL: env.apiBaseUrl,
withCredentials: true,
});
xxxxx.cloudfront.net 형태입니다.www.example.com, admin.example.com 같은 커스텀 도메인을 연결합니다.Route 53 또는 DNS
↓
www.example.com CNAME/Alias
↓
CloudFront Distribution
↓
S3 Origin
ACM 인증서
CloudFront Alternate domain name 설정
DNS record 설정
HTTPS 접속 확인
CloudFront용 ACM은 us-east-1
DNS 전파 시간 고려
www와 apex 도메인 처리 구분
인증서 SAN에 필요한 도메인 포함
www.example.com만 인증서에 있고 admin.example.com이 없으면 관리자 도메인 HTTPS가 실패할 수 있습니다.www.example.com
├─ /
├─ /products
└─ /admin
장점:
구조 단순
배포 한 번
공통 코드 공유 쉬움
단점:
관리자 코드가 고객 번들에 섞일 수 있음
권한/라우팅 관리 주의
고객/관리자 장애 범위가 같음
www.example.com:
고객 화면
admin.example.com:
관리자 화면
장점:
역할 분리 명확
번들 분리
권한/라우팅 관리 쉬움
관리자 장애와 고객 화면 분리 가능
단점:
배포 설정 2개
CloudFront/S3 버킷 관리 증가
공통 UI 공유 구조 필요
초기:
같은 앱으로 시작 가능
규모 커짐:
고객/관리자 별도 도메인과 별도 배포 검토
관리자 보안 중요:
admin.example.com 분리 권장
banner.png 교체
↓
파일명 동일
↓
CloudFront/브라우저는 이전 파일 캐시
↓
사용자에게 이전 배너 표시
파일명에 버전 포함:
banner-20260805.webp
파일명에 hash 포함:
banner-a8f3.webp
교체 시 invalidation:
해당 이미지 경로 무효화
배너:
파일명 버전 관리
상품 이미지:
업로드 시 고유 key 생성
수정 잦은 이미지:
짧은 캐시 또는 파일명 변경
로고:
변경 드물면 긴 캐시 가능
#!/bin/bash
set -e
BUCKET_NAME="YOUR_BUCKET_NAME"
DISTRIBUTION_ID="YOUR_DISTRIBUTION_ID"
echo "Checking branch..."
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "main" ]; then
echo "현재 브랜치가 main이 아닙니다: $CURRENT_BRANCH"
exit 1
fi
echo "Installing and verifying..."
npm run lint
npm run test:run
npm run build
echo "Checking build output..."
grep -R "localhost" dist/ && {
echo "dist 안에 localhost가 포함되어 있습니다. 환경변수를 확인하세요."
exit 1
} || true
echo "Uploading assets..."
aws s3 sync dist/assets/ s3://$BUCKET_NAME/assets/ \
--cache-control "public, max-age=31536000, immutable"
echo "Uploading index.html..."
aws s3 cp dist/index.html s3://$BUCKET_NAME/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"
echo "Uploading rest..."
aws s3 sync dist/ s3://$BUCKET_NAME \
--exclude "index.html" \
--exclude "assets/*"
echo "Invalidating CloudFront..."
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/index.html" "/"
echo "Deploy completed."
브랜치 확인
빌드 전 검증
운영 env 확인
dist 안 localhost 검색
S3 bucket 이름 확인
CloudFront distribution 확인
캐시 정책 분리
배포 후 Smoke Test
메인 페이지 접속
상품 목록 접속
상품 상세 접속
대표 이미지/배너 확인
상담 신청 모달 열기
필수값 검증 확인
API 요청 도메인 확인
모바일 화면 확인
관리자 로그인
상담 목록 조회
검색/필터 실행
상담 상세 열기
상태 변경 모달 열기
권한별 버튼 확인
엑셀 다운로드 버튼 확인
새로고침 후 최신 화면인가?
시크릿 모드에서도 정상인가?
Network 탭에서 index.html cache-control 확인
JS/CSS가 404 나지 않는가?
Chunk Load Error가 없는가?
가능한 원인:
CloudFront 캐시
브라우저 캐시
index.html 캐시가 너무 김
invalidation 누락
다른 S3 bucket에 업로드
다른 distribution을 무효화
확인:
CloudFront invalidation 상태
S3 index.html 수정 시간
브라우저 Network cache-control
배포 bucket/distribution ID
가능한 원인:
SPA fallback 미설정
CloudFront custom error response 미설정
S3 private bucket에서 403 반환
해결:
403/404를 index.html로 응답하도록 CloudFront 설정
React Router 경로 확인
API 경로와 fallback 충돌 확인
가능한 원인:
JS chunk 404
환경변수 오류
API Base URL undefined
브라우저 런타임 에러
이전 index.html과 새 assets 불일치
확인:
브라우저 Console
Network JS/CSS 404 여부
dist 안 환경변수
CloudFront invalidation
ErrorBoundary 로그
가능한 원인:
CORS 설정 오류
withCredentials 누락
secure cookie 설정
sameSite 설정
API 도메인 HTTPS 문제
X-Forwarded-Proto 누락
확인:
브라우저 Application Cookie
API 응답 Set-Cookie
CORS response header
axios withCredentials
백엔드 cookie option
index.html 캐시가 짧거나 no-cache인가?--delete 사용 시 이전 chunk 삭제 위험을 고려했는가?npm run lint가 통과했는가?npm run test:run이 통과했는가?npm run build가 통과했는가?localhost가 남아 있지 않은가?React + Vite 프론트엔드를 S3 + CloudFront로 배포하고 있어.
상황:
1. 빌드 결과물은 dist 폴더에 생성됨
2. S3 bucket은 private이고 CloudFront OAC로 접근함
3. 도메인은 www.example.com
4. API는 https://api.example.com 사용
5. React Router를 사용함
6. /products/1에서 새로고침하면 403 또는 404가 발생함
7. CloudFront custom error response 설정은 아래와 같음
8. S3 bucket policy는 아래와 같음
9. Network 탭과 CloudFront 응답 상태는 아래와 같음
요청:
- 가장 가능성 높은 원인
- CloudFront/S3에서 확인할 설정
- SPA fallback 설정 방법
- API 경로와 충돌 가능성
- 배포 후 확인할 Smoke Test
- 재발 방지 체크리스트
를 순서대로 정리해줘.
/products/1, /admin/consults 새로고침 시 실제 파일이 없으므로 CloudFront에서 403/404를 /index.html로 돌려주는 fallback 설정이 필요합니다.index.html과 hashed JS/CSS assets를 반드시 구분해야 합니다. index.html은 짧은 캐시, hashed assets는 긴 캐시가 기본입니다.aws s3 sync --delete는 깔끔하지만 이전 JS chunk를 삭제해 Chunk Load Error를 만들 수 있으므로 운영 사용자 상황에 따라 조심해야 합니다./* 전체 무효화로 단순하게 시작해도 되고, 캐시 전략이 정리되면 /index.html, / 중심으로 줄일 수 있습니다.VITE_API_BASE_URL 변경 후에는 반드시 재빌드가 필요하고, Secret은 절대 넣으면 안 됩니다.www.example.com, admin.example.com처럼 별도 배포를 검토할 수 있습니다.