왜 정적 웹사이트를 S3 + CloudFront로 배포할까?
여러분이 포트폴리오 사이트나 회사 소개 페이지를 만들었다고 합시다. 서버 없이 HTML, CSS, JavaScript 파일만으로 구성된 사이트입니다.

이걸 배포하려면 어떻게 해야 할까요?
방법 1: EC2 인스턴스를 띄우고 Nginx를 설치한다. → 서버 관리가 필요하고, 월 $8~15 비용이 발생합니다. → 한국에서만 빠르고, 미국이나 유럽에서는 느립니다.
방법 2: S3에 파일을 올리고 CloudFront로 전 세계에 배포한다. → 서버 관리가 필요 없고, 월 $0~1로 거의 무료입니다. → 전 세계 400개 이상의 Edge Location에서 캐시되어 어디서든 빠릅니다.
당연히 방법 2가 낫겠죠? 이것이 바로 정적 웹사이트 호스팅의 정석입니다.
실제로 Netflix, Airbnb 같은 대형 서비스도 프론트엔드를 S3 + CloudFront로 배포합니다. React, Next.js, Vue.js로 빌드한 결과물을 S3에 올리고 CloudFront로 배포하는 것은 업계 표준입니다.
이 실습을 마치면 여러분은:
- S3 버킷을 웹사이트 오리진으로 설정할 수 있습니다
- CloudFront CDN으로 전 세계에 배포할 수 있습니다
- OAC로 S3 직접 접근을 차단하는 보안 설정을 할 수 있습니다
- 캐시 무효화를 통해 배포 업데이트를 할 수 있습니다
이 조합은 실무에서 가장 많이 사용되는 정적 배포 아키텍처입니다. 비용이 거의 들지 않으면서도 글로벌 성능과 보안을 확보할 수 있습니다.
실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. S3와 CloudFront는 글로벌 서비스이며, ACM 인증서는 us-east-1 (버지니아 북부) 리전에서 발급해야 합니다.
아키텍처 개요

배포 흐름
비용 예측
비용 계산기
GB당 $0.023
GB
시간
* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.
Step 1: S3 버킷 생성 및 파일 업로드
S3(Simple Storage Service)는 AWS의 객체 스토리지 서비스입니다. 파일을 "버킷"이라는 컨테이너에 저장합니다.
여기서 중요한 점은 퍼블릭 액세스를 차단한 상태로 두는 것입니다. "정적 웹사이트인데 왜 퍼블릭을 막아?" 라고 생각할 수 있지만, CloudFront의 OAC(Origin Access Control)를 통해 CloudFront만 S3에 접근하도록 설정합니다. 이렇게 하면 S3 URL로 직접 접근하는 것을 차단하여 보안이 강화됩니다.
먼저 간단한 HTML 파일을 준비합니다. 텍스트 편집기에서 아래 내용으로 index.html을 만드세요:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Static Website</title>
<style>
body { font-family: 'Pretendard', sans-serif; text-align: center; padding: 50px; }
h1 { color: #232f3e; }
.badge { background: #ff9900; color: white; padding: 8px 16px; border-radius: 4px; }
</style>
</head>
<body>
<h1>CloudFront로 배포된 정적 웹사이트</h1>
<p class="badge">S3 + CloudFront + OAC</p>
<p>전 세계 어디서든 빠르게 로드됩니다!</p>
</body>
</html>에러 페이지용 error.html도 만듭니다:
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>404</title></head>
<body style="text-align:center;padding:50px;">
<h1>404 - 페이지를 찾을 수 없습니다</h1>
<a href="/">홈으로 돌아가기</a>
</body>
</html>- AWS 콘솔 상단 검색창에서 S3 검색 → 클릭
- 버킷 만들기 클릭
- 버킷 이름:
lab-static-site-{고유ID}입력- 예:
lab-static-site-20260414(전 세계에서 고유해야 합니다) - 이미 존재하는 이름이면 에러가 나므로, 날짜나 랜덤 숫자를 붙이세요
- 예:
- AWS 리전: ap-northeast-2 (서울) 선택
- 모든 퍼블릭 액세스 차단: 체크된 상태 그대로 유지합니다 (이것이 핵심!)
- 버전 관리: 비활성화 상태 유지 (실습에서는 불필요)
- 버킷 만들기 클릭
- 생성된 버킷 클릭 → 객체 탭 → 업로드 클릭
- 준비한
index.html과error.html파일을 드래그 앤 드롭으로 업로드 - 업로드 클릭 → 성공 확인
# 버킷 생성
aws s3 mb s3://lab-static-site-$(date +%s) --region ap-northeast-2
# 파일 업로드
aws s3 cp index.html s3://lab-static-site-XXXXX/ --content-type "text/html"
aws s3 cp error.html s3://lab-static-site-XXXXX/ --content-type "text/html"resource "aws_s3_bucket" "website" {
bucket = "lab-static-site-${random_id.suffix.hex}"
tags = { Name = "lab-static-site" }
}
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.website.id
key = "index.html"
source = "index.html"
content_type = "text/html"
}
resource "aws_s3_object" "error" {
bucket = aws_s3_bucket.website.id
key = "error.html"
source = "error.html"
content_type = "text/html"
}S3 버킷 이름은 전 세계에서 고유해야 합니다.
자신만의 접두사나 날짜를 포함하여 충돌을 방지하세요.
예: lab-static-site-홍길동-20260414
주의: 이 시점에서 S3 URL(https://lab-static-site-xxx.s3.amazonaws.com/index.html)로 직접 접속하면 Access Denied 에러가 납니다. 이것이 정상입니다! 퍼블릭 액세스를 차단했으니까요. CloudFront를 통해서만 접근할 수 있도록 설정하겠습니다.
Step 2: CloudFront 배포 생성
CloudFront는 AWS의 CDN(Content Delivery Network) 서비스입니다. 전 세계 400개 이상의 Edge Location에 콘텐츠를 캐시하여, 사용자와 가장 가까운 위치에서 콘텐츠를 제공합니다.
한국 사용자가 접속하면 서울의 Edge Location에서, 미국 사용자가 접속하면 미국의 Edge Location에서 콘텐츠를 받습니다. 원본 S3(Origin)까지 매번 갈 필요가 없으므로 응답 속도가 매우 빨라집니다.
- 1AWS 콘솔에서 CloudFront 검색 → 클릭
- 2배포 생성 클릭
- 3원본(Origin) 설정: 원본 도메인: 검색창에서 위에서 만든 S3 버킷 선택 이름: 자동 입력됩니다
- 4원본 액세스 설정 (중요!): 원본 액세스 제어 설정(권장) 선택 제어 설정 생성 클릭 이름: lab-oac (기본값 유지해도 됩니다) 서명 동작: 항상 서명 선택 생성 클릭
- 5기본 캐시 동작 설정: 뷰어 프로토콜 정책: Redirect HTTP to HTTPS 선택 허용된 HTTP 메서드: GET, HEAD 선택 캐시 정책: CachingOptimized 선택 (기본값)
- 6설정 섹션: 기본 루트 객체: index.html 입력 (반드시 입력해야 합니다!) 가격 등급: 모든 Edge Location 사용 (기본값)
- 7배포 생성 클릭
- 8중요! 상단에 노란색 배너가 표시됩니다: "S3 버킷 정책을 업데이트해야 합니다" 정책 복사 버튼을 클릭하여 정책을 복사합니다 이 정책은 다음 단계에서 사용합니다
- 9배포 상태가 배포 중에서 활성화됨으로 바뀔 때까지 3~5분 기다립니다
기본 루트 객체를 빼먹으면: CloudFront 도메인으로 접속했을 때
index.html이 아닌 XML 에러 페이지가 표시됩니다.
반드시 index.html을 입력하세요.
Step 3: OAC 버킷 정책 설정
이전 단계에서 복사한 정책을 S3 버킷에 적용합니다. 이 정책은 "이 CloudFront 배포에서 오는 요청만 S3 객체 읽기를 허용한다"는 내용입니다.
- S3 콘솔 → 위에서 만든 버킷 클릭
- 권한 탭 클릭
- 버킷 정책 섹션 → 편집 클릭
- 이전 단계에서 복사한 정책을 붙여넣기합니다
- 변경 사항 저장 클릭
- 이제 CloudFront 도메인으로 접속을 테스트합니다:
- CloudFront 콘솔 → 배포 선택 → 배포 도메인 이름 복사 (예: d111111abcdef.cloudfront.net)
- 브라우저에서 해당 도메인으로 접속
- "CloudFront로 배포된 정적 웹사이트" 페이지가 표시되면 성공!
- S3 URL로 직접 접속하면 여전히 Access Denied인지도 확인해 보세요
aws s3api put-bucket-policy --bucket lab-static-site-XXXXX \
--policy '{
"Version":"2012-10-17","Statement":[{
"Effect":"Allow",
"Principal":{"Service":"cloudfront.amazonaws.com"},
"Action":"s3:GetObject",
"Resource":"arn:aws:s3:::lab-static-site-XXXXX/*",
"Condition":{"StringEquals":{"AWS:SourceArn":"arn:aws:cloudfront::ACCOUNT:distribution/DIST_ID"}}
}]
}'resource "aws_cloudfront_origin_access_control" "oac" {
name = "lab-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}Step 4: 캐시 무효화 테스트
CloudFront는 콘텐츠를 Edge Location에 캐시합니다. 따라서 S3의 파일을 업데이트해도 바로 반영되지 않습니다. 캐시가 만료될 때까지 이전 버전이 계속 표시됩니다.
이때 캐시 무효화(Invalidation)를 사용하면 강제로 캐시를 제거할 수 있습니다.
- 1index.html을 수정합니다. 예를 들어 제목을 "업데이트된 정적 웹사이트"로 변경
- 2S3 콘솔 → 버킷 → 업로드 → 수정된 index.html 업로드 (덮어쓰기됨)
- 3CloudFront 도메인으로 접속 → 아직 이전 콘텐츠가 표시됩니다 (캐시가 남아 있음)
- 4CloudFront 콘솔 → 배포 선택 → 무효화 탭 클릭
- 5무효화 생성 클릭
- 6객체 경로에 /* 입력 (모든 파일의 캐시를 제거한다는 뜻)
- 7무효화 생성 클릭
- 8상태가 완료됨이 될 때까지 1~2분 대기
- 9브라우저에서 CloudFront 도메인을 새로고침 → 변경된 콘텐츠가 표시되면 성공!
캐시 무효화는 월 1,000건까지 무료입니다.
배포 자동화 시 CI/CD 파이프라인에 무효화 단계를 포함하세요.
/* 대신 /index.html처럼 특정 파일만 무효화하면 더 효율적입니다.
실무 팁: 매번 캐시 무효화를 하는 것보다 더 좋은 방법이 있습니다.
파일명에 해시를 포함하는 것입니다. 예: app.abc123.js
파일 내용이 바뀌면 파일명도 바뀌므로, CloudFront가 새 파일로 인식합니다.
React, Next.js 등의 빌드 도구는 이 방식을 자동으로 지원합니다.
핵심 개념 확인
직접 설명해 보기
본인의 말로 설명해 보세요
CloudFront의 캐시 HIT와 MISS가 무엇인지, 그리고 각 경우에 콘텐츠가 어디서 오는지 설명해 보세요.
💡 브라우저 → Edge Location → S3(Origin)의 흐름에서, 캐시가 있을 때와 없을 때를 각각 설명해 보세요.
완성 후 테스트 가이드
배포가 완료되었다면, 다음 테스트를 통해 정상 동작을 확인하세요.
- 1CloudFront 접속 테스트: 브라우저에서 CloudFront 도메인(d111111abcdef.cloudfront.net)으로 접속 → 웹페이지가 정상 표시되는지 확인
- 2HTTPS 테스트: URL이 https://로 시작하는지 확인. http://로 접속하면 자동으로 https://로 리다이렉트되는지 확인
- 3S3 직접 접근 차단 테스트: S3 URL(lab-static-site-xxx.s3.amazonaws.com/index.html)로 접속 → Access Denied가 나오면 OAC 설정 성공
- 4캐시 확인: 브라우저 개발자 도구(F12) → Network 탭 → 페이지 새로고침 → 응답 헤더에서 X-Cache: Hit from cloudfront 확인
- 5에러 페이지 테스트: 존재하지 않는 경로(예: /없는페이지)로 접속 → CloudFront가 S3에서 403을 받으면 커스텀 에러 응답을 반환하는지 확인
- 6캐시 무효화 테스트: 파일 수정 → S3 재업로드 → 캐시 무효화 → 변경 반영 확인
트러블슈팅
CloudFront 도메인으로 접속 시 Access Denied:
- S3 버킷 정책에 CloudFront OAC 정책이 적용되었는지 확인
- CloudFront 배포에서 OAC가 제대로 설정되었는지 확인
- 기본 루트 객체에
index.html이 입력되어 있는지 확인 - 배포 상태가 활성화됨인지 확인 (배포 중이면 아직 사용 불가)
캐시 무효화 후에도 이전 콘텐츠가 표시되는 경우:
- 무효화 상태가 완료됨인지 확인 (진행 중이면 대기)
- 브라우저 캐시를 삭제하고 강력 새로고침(Ctrl+Shift+R)
- 무효화 경로가 올바른지 확인 (예:
/*또는/index.html) - S3에 새 파일이 정상적으로 업로드되었는지 확인
CloudFront 배포가 비활성화되지 않아 삭제할 수 없는 경우: CloudFront 배포를 삭제하려면 먼저 비활성화해야 합니다. 배포 선택 → 편집 → 비활성화 → 저장. 비활성화에 5~10분이 소요됩니다. 비활성화 완료 후에야 삭제가 가능합니다.
확장 아이디어
- 커스텀 도메인 연결: Route 53에 호스팅 영역을 만들고, CloudFront에 커스텀 도메인(예: www.mysite.com)을 연결해 보세요. ACM 인증서(us-east-1)가 필요합니다.
- 에러 페이지 커스터마이즈: CloudFront 설정에서 에러 응답을 커스텀하여, 403/404 에러 시
error.html을 보여주도록 설정해 보세요. - CI/CD 파이프라인: GitHub Actions로 코드 푸시 시 자동으로 S3 업로드 + CloudFront 무효화를 실행하는 파이프라인을 구축해 보세요.
- Lambda@Edge: CloudFront의 Edge Location에서 실행되는 Lambda 함수로, URL 리다이렉트나 헤더 조작 등을 해 보세요.
- CloudFront Functions: Lambda@Edge보다 가볍고 빠른 함수로, 간단한 URL 리라이팅이나 캐시 키 조작을 해 보세요.
학습 정리
핵심 치트시트
이 프로젝트에서는 S3에 정적 파일을 호스팅하고, CloudFront CDN으로 전 세계에 배포하는 아키텍처를 구축했습니다. OAC로 S3 직접 접근을 차단하고, 캐시 무효화로 배포 업데이트를 처리합니다.
리소스 정리
실습 완료 후 반드시 아래 순서대로 리소스를 정리하여 불필요한 과금을 방지하세요. CloudFront 배포는 비활성화 후 삭제해야 하며, 비활성화에 수 분이 소요됩니다.
- CloudFront 배포 비활성화 (배포 편집 → 비활성화 → 저장, 5~10분 소요)
- 비활성화 완료 후 CloudFront 배포 삭제
- OAC(Origin Access Control) 삭제
- S3 버킷 내 객체 모두 삭제 (버킷 → 비우기)
- S3 버킷 삭제
- (Route 53 호스팅 영역, ACM 인증서는 필요 시 유지)