S3 정적 웹사이트 호스팅 완전 가이드: HTML/CSS 사이트를 무료로 배포하는 법
간단한 HTML/CSS 포트폴리오나 랜딩 페이지를 만들었는데, 서버 없이 배포할 방법을 찾고 있다면 S3 정적 웹사이트 호스팅이 가장 현실적인 선택이다. EC2 인스턴스를 띄울 필요도 없고, Nginx 설정을 건드릴 필요도 없다. S3 버킷 하나로 수백만 요청을 처리할 수 있다.
TL;DR — S3 정적 웹사이트 호스팅 핵심 요약
| 단계 | 작업 | 주의사항 |
|---|---|---|
| 1 | S3 버킷 생성 (퍼블릭 액세스 차단 해제) | 버킷 이름은 변경 불가 |
| 2 | 정적 웹사이트 호스팅 활성화 | 인덱스/에러 문서 지정 필수 |
| 3 | 버킷 정책으로 퍼블릭 읽기 허용 | ACL 방식은 권장하지 않음 |
| 4 | 파일 업로드 | Content-Type 자동 감지 확인 |
| 5 | S3 웹사이트 엔드포인트로 접근 | HTTPS는 CloudFront 필요 |
S3 정적 웹사이트 호스팅의 동작 원리
S3에는 두 가지 엔드포인트가 존재한다. 하나는 REST API 엔드포인트(s3.amazonaws.com), 다른 하나는 웹사이트 엔드포인트(s3-website-{region}.amazonaws.com)다. 정적 웹사이트 호스팅을 활성화하면 웹사이트 엔드포인트가 활성화되고, 이 엔드포인트는 HTTP GET 요청에 대해 인덱스 문서를 반환하거나 404 시 커스텀 에러 페이지를 반환하는 웹 서버처럼 동작한다.
REST 엔드포인트와 웹사이트 엔드포인트의 결정적 차이는 루트 경로 처리 방식이다. REST 엔드포인트에서 /를 요청하면 버킷 목록 XML이 반환되거나 403이 뜬다. 웹사이트 엔드포인트는 설정된 인덱스 문서(index.html)를 자동으로 반환한다.
중요한 제약이 하나 있다. S3 웹사이트 엔드포인트는 HTTP만 지원한다. HTTPS가 필요하다면 CloudFront를 앞단에 배치해야 한다. 이 포스트에서는 S3 단독 설정에 집중하고, CloudFront 연동은 별도로 다룬다.
HTTP only"] WebEndpoint --> PolicyCheck{"버킷 정책 평가
s3:GetObject 허용?"} PolicyCheck -->|허용| ObjectStore["S3 객체 저장소"] PolicyCheck -->|거부| Err403["403 Forbidden"] ObjectStore -->|객체 존재| Response["HTML/CSS/JS 반환"] ObjectStore -->|객체 없음| ErrorDoc["에러 문서 반환
error.html"]
- 브라우저 요청: 사용자가 S3 웹사이트 엔드포인트 URL로 HTTP 요청을 보낸다.
- 웹사이트 호스팅 처리: S3가 요청 경로를 분석해 해당 객체를 조회한다. 루트 경로면 인덱스 문서를 반환한다.
- 버킷 정책 평가:
s3:GetObject허용 여부를 버킷 정책에서 확인한다. 퍼블릭 읽기가 허용되어 있어야 응답이 반환된다. - 객체 반환: HTML, CSS, JS, 이미지 파일이 HTTP 응답으로 전달된다.
- 404 처리: 객체가 없으면 설정된 에러 문서를 반환한다.
S3 정적 웹사이트 호스팅 설정 단계별 가이드
1단계: S3 버킷 생성
버킷 이름은 전 세계적으로 고유해야 한다. 도메인 이름을 사용할 계획이라면 버킷 이름을 도메인과 동일하게 설정하는 것이 Route 53 연동 시 유리하다. 버킷 생성 시 기본적으로 활성화되어 있는 '퍼블릭 액세스 차단'을 해제해야 외부에서 접근 가능하다.
# 버킷 생성
aws s3api create-bucket \
--bucket my-static-website-example \
--region us-east-1
# us-east-1 외 리전은 LocationConstraint 파라미터 필요
# aws s3api create-bucket \
# --bucket my-static-website-example \
# --region ap-northeast-2 \
# --create-bucket-configuration LocationConstraint=ap-northeast-2
버킷 생성 후 퍼블릭 액세스 차단 설정을 해제한다. 이 설정은 버킷 정책이나 ACL을 통한 퍼블릭 접근을 계정 수준에서 막는 안전장치다. 정적 웹사이트 호스팅을 위해서는 이 차단을 해제해야 한다.
aws s3api put-public-access-block \
--bucket my-static-website-example \
--public-access-block-configuration \
'BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false'
2단계: 정적 웹사이트 호스팅 활성화
이 단계가 핵심이다. 웹사이트 호스팅을 활성화하면 S3가 웹 서버 모드로 전환되어 인덱스 문서와 에러 문서를 처리할 수 있게 된다. 인덱스 문서는 루트 경로(/) 또는 디렉터리 경로 요청 시 반환할 파일명이다.
aws s3api put-bucket-website \
--bucket my-static-website-example \
--website-configuration '{
"IndexDocument": {"Suffix": "index.html"},
"ErrorDocument": {"Key": "error.html"}
}'
설정이 적용됐는지 확인한다.
aws s3api get-bucket-website \
--bucket my-static-website-example
3단계: 버킷 정책으로 퍼블릭 읽기 허용
퍼블릭 액세스 차단을 해제했다고 해서 자동으로 공개되는 게 아니다. 실제 읽기 권한은 버킷 정책으로 명시적으로 부여해야 한다. ACL 방식(public-read)은 AWS가 권장하지 않으며, 버킷 정책 방식이 표준이다.
아래 정책은 모든 사용자(*)에게 버킷 내 모든 객체에 대한 s3:GetObject 권한을 부여한다. 이것이 정적 웹사이트 호스팅에서 필요한 최소 권한이다.
# policy.json 파일 생성
cat > policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-static-website-example/*"
}
]
}
EOF
# 버킷 정책 적용
aws s3api put-bucket-policy \
--bucket my-static-website-example \
--policy file://policy.json
버킷 정책의
Principal: "*"는 인터넷의 모든 사용자를 의미한다. 이는 정적 웹사이트 호스팅의 의도된 설정이지만, 민감한 데이터가 포함된 객체는 절대 이 버킷에 업로드하면 안 된다. 웹사이트 파일 전용 버킷을 별도로 운영하는 이유가 여기에 있다.
4단계: 웹사이트 파일 업로드
로컬 디렉터리 전체를 S3에 동기화한다. --delete 옵션을 사용하면 로컬에서 삭제된 파일이 S3에서도 제거된다. 처음 배포 시에는 --delete 없이 실행하는 것이 안전하다.
# 단일 파일 업로드
aws s3 cp index.html s3://my-static-website-example/
# 디렉터리 전체 동기화 (권장)
aws s3 sync ./my-website/ s3://my-static-website-example/ \
--exclude '.DS_Store' \
--exclude '.git/*'
업로드 후 파일 목록과 Content-Type이 올바르게 설정됐는지 확인한다. Content-Type이 잘못 설정되면 브라우저가 HTML 파일을 다운로드하거나 CSS가 적용되지 않는 문제가 발생한다. AWS CLI는 파일 확장자를 기반으로 Content-Type을 자동 감지한다.
aws s3api head-object \
--bucket my-static-website-example \
--key index.html
5단계: 웹사이트 엔드포인트 확인 및 접속
S3 웹사이트 엔드포인트 URL 형식은 리전마다 다르다. 아래 명령으로 현재 설정된 웹사이트 구성을 확인하고, 엔드포인트를 직접 조회한다.
aws s3api get-bucket-location \
--bucket my-static-website-example
리전 확인 후 웹사이트 엔드포인트 URL을 구성한다. 예를 들어 us-east-1 리전이라면 엔드포인트는 http://my-static-website-example.s3-website-us-east-1.amazonaws.com이다. ap-northeast-2(서울)라면 http://my-static-website-example.s3-website.ap-northeast-2.amazonaws.com이다.
엔드포인트 형식이 리전마다 다르다는 점을 주의해야 한다. us-east-1은 하이픈(s3-website-us-east-1) 형식이고, 다른 리전은 점(s3-website.ap-northeast-2) 형식을 사용한다. 공식 AWS 문서에서 리전별 엔드포인트를 반드시 확인하라.
# curl로 응답 확인
curl -I http://my-static-website-example.s3-website-us-east-1.amazonaws.com
자주 겪는 문제와 실제 진단 방법
403 Forbidden — 가장 흔한 실수
배포 후 403이 뜨면 대부분 세 가지 중 하나다. 퍼블릭 액세스 차단이 여전히 활성화되어 있거나, 버킷 정책이 없거나, 버킷 정책의 리소스 ARN이 잘못됐다. 순서대로 확인한다.
# 퍼블릭 액세스 차단 현황 확인
aws s3api get-public-access-block \
--bucket my-static-website-example
# 버킷 정책 확인
aws s3api get-bucket-policy \
--bucket my-static-website-example
버킷 정책이 없다면 NoSuchBucketPolicy 에러가 반환된다. 정책이 있는데도 403이 뜬다면 리소스 ARN에서 버킷 이름 오타나 /* 누락을 확인한다. arn:aws:s3:::my-bucket은 버킷 자체에 대한 권한이고, 객체 읽기에는 arn:aws:s3:::my-bucket/*이 필요하다.
404 Not Found — 인덱스 문서 설정 문제
버킷에 index.html이 분명히 존재하는데 루트 경로에서 404가 뜨는 경우가 있다. 웹사이트 호스팅 설정에서 인덱스 문서를 지정하지 않았거나, 파일이 버킷 루트가 아닌 하위 폴더에 업로드된 경우다.
# 버킷 내 파일 목록 확인
aws s3 ls s3://my-static-website-example/
# 웹사이트 설정 재확인
aws s3api get-bucket-website \
--bucket my-static-website-example
CSS/JS가 적용되지 않는 경우 — Content-Type 문제
HTML은 정상 표시되는데 스타일이 전혀 없는 상태로 보인다면, CSS 파일의 Content-Type을 확인한다. text/plain으로 설정된 경우 브라우저가 스타일시트로 인식하지 않는다.
aws s3api head-object \
--bucket my-static-website-example \
--key styles.css
Content-Type이 잘못됐다면 올바른 MIME 타입을 명시해서 재업로드한다.
aws s3 cp styles.css s3://my-static-website-example/styles.css \
--content-type 'text/css'
실제 운영에서 겪은 함정 — 퍼블릭 액세스 차단의 계층 구조
처음 설정할 때 가장 많이 막히는 지점이 여기다. 버킷 정책을 완벽하게 작성했는데도 403이 계속 뜨는 상황. 버킷 수준 퍼블릭 액세스 차단을 해제했다고 확신했는데 여전히 안 된다.
원인은 계정 수준 퍼블릭 액세스 차단이었다. AWS 계정에는 버킷 수준과 별개로 계정 전체에 적용되는 퍼블릭 액세스 차단 설정이 존재한다. 계정 수준 설정이 활성화되어 있으면 버킷 수준에서 아무리 해제해도 퍼블릭 접근이 차단된다.
# 계정 수준 퍼블릭 액세스 차단 확인
aws s3control get-public-access-block \
--account-id 123456789012
계정 수준 차단이 활성화되어 있다면, 특정 버킷만 예외로 두는 방법은 없다. 계정 수준 설정을 해제하거나, 퍼블릭 접근이 필요 없는 아키텍처(CloudFront + OAC)로 전환해야 한다. 프로덕션 환경에서는 계정 수준 차단을 유지하고 CloudFront를 사용하는 것이 보안상 더 나은 선택이다.
퍼블릭 액세스 차단"] -->|차단 해제 필요| BucketBlock["버킷 수준
퍼블릭 액세스 차단"] BucketBlock -->|차단 해제 필요| BucketPolicy["버킷 정책
s3:GetObject Allow"] BucketPolicy -->|정책 적용 완료| PublicAccess["퍼블릭 접근 허용"] AccountBlock -->|차단 활성화| Blocked1["접근 차단 — 버킷 설정 무관"] BucketBlock -->|차단 활성화| Blocked2["접근 차단 — 버킷 정책 무관"]
- 계정 수준 차단: 가장 높은 우선순위. 활성화되어 있으면 버킷 설정과 무관하게 퍼블릭 접근 차단.
- 버킷 수준 차단: 계정 수준이 해제된 경우에만 유효. 버킷별로 독립적으로 설정 가능.
- 버킷 정책: 두 수준의 차단이 모두 해제된 경우에만 실제 권한 부여 효과 발생.
비용과 한계 — 알고 쓰는 것과 모르고 쓰는 것의 차이
S3 정적 웹사이트 호스팅은 저렴하지만 완전히 무료는 아니다. S3 스토리지 비용, GET 요청 비용, 데이터 전송 비용이 발생한다. 트래픽이 거의 없는 개인 프로젝트라면 AWS 프리 티어 범위 내에서 운영 가능하다. 정확한 요금은 AWS S3 요금 페이지에서 확인하라.
기능적 한계도 명확하다. 서버 사이드 렌더링, 동적 콘텐츠 처리, 서버 사이드 리다이렉트 로직은 S3 단독으로 구현할 수 없다. SPA(Single Page Application)에서 클라이언트 사이드 라우팅을 사용한다면, 직접 URL 접근 시 404가 발생하는 문제가 있다. 이 경우 에러 문서를 index.html로 설정하는 우회 방법이 있지만, CloudFront의 커스텀 에러 응답 기능을 사용하는 것이 더 깔끔하다.
S3 정적 웹사이트 호스팅 다음 단계
S3 단독 설정으로 기본 배포는 완료됐다. 실제 서비스 수준으로 올리려면 추가 작업이 필요하다.
- HTTPS 적용: CloudFront 배포를 생성하고 S3 버킷을 오리진으로 설정한다. OAC(Origin Access Control)를 사용하면 S3 버킷을 퍼블릭으로 열지 않아도 된다.
- 커스텀 도메인: Route 53에서 도메인을 구매하거나 기존 도메인을 연결한다. CloudFront 배포에 SSL 인증서(ACM)를 연결하고 Route 53 A 레코드를 Alias로 설정한다.
- 배포 자동화: GitHub Actions에서
aws s3 sync와 CloudFront 캐시 무효화(aws cloudfront create-invalidation)를 조합해 CI/CD 파이프라인을 구성한다.
공식 문서: AWS S3 정적 웹사이트 호스팅 공식 문서
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| S3 웹사이트 엔드포인트 | 정적 웹사이트 호스팅 활성화 시 생성되는 HTTP 전용 URL. REST 엔드포인트와 별개로 동작하며 인덱스 문서 처리를 지원한다. |
| 퍼블릭 액세스 차단 (Block Public Access) | S3 버킷 또는 AWS 계정 전체에서 퍼블릭 접근을 차단하는 설정. 계정 수준과 버킷 수준 두 레이어로 구성된다. |
| 버킷 정책 (Bucket Policy) | S3 버킷에 연결되는 리소스 기반 IAM 정책. 특정 Principal에게 버킷 및 객체에 대한 권한을 부여하거나 거부한다. |
| 인덱스 문서 (Index Document) | 루트 경로 또는 디렉터리 경로 요청 시 S3가 반환하는 기본 파일. 일반적으로 index.html을 사용한다. |
| OAC (Origin Access Control) | CloudFront가 S3 버킷에 접근할 때 사용하는 인증 메커니즘. 버킷을 퍼블릭으로 열지 않고도 CloudFront를 통한 접근을 허용한다. |
댓글
댓글 쓰기