들어가며
React.js 기반의 웹사이트를 배포하는 방법에는 다양한 방법이 있다. Vercel, Netlify, GitHub Pages 같은 정적 호스팅 서비스를 사용할 수도 있고 AWS, Google cloud Platform, Microsoft Azure같은 클라우드 플랫폼을 사용할 수도 있다.
전자는 GitHub 연동을 해두면 자동 배포, CDN, 모니터링 등 다양한 기능을 제공해주어 편리하다는 장점이 있다. 후자는 일일이 세팅해줘야한다는 점이 번거롭지만, 직접 커스텀하기 용이하다는 장점이 있다. 지금까지 개인 프로젝트를 진행하며 항상 나는 전자, 그 중에서 Vercel을 사용해왔다. 편리하다는 이유에서였다.
우테코에서는 정적 호스팅 서비스를 사용하지 않고 배포하는 것이 팀 프로젝트의 도전 과제였으며, 이번 기회에 AWS를 이용해서 직접 배포를 해볼 수 있게되었다. 아래는 AWS의 S3와 CloudFront를 이용해 배포를 하고, Code Pipeline과 Code Build를 이용해서 자동 배포를 구현한 과정이다.
배포
S3 + CloudFront
AWS S3와 CloudFront는 정적 웹사이트 호스팅에 효과적인 조합이다. S3를 이용해 리액트와 같은 SPA의 빌드 파일을 저장하고, CloudFront로 S3에 저장된 콘텐츠를 전 세계에 빠르게 제공을 할 수 있다.
S3
S3(Simple Storage Service)는 AWS의 객체 스토리지 서비스이다. S3를 활용해 정적 웹사이트 호스팅 외에도 백업 및 아키이빙, 이미지, 동영상, 음성파일 등의 미디어 저장소, 로그 수집 및 분석 등의 다양하게 활용할 수도 있다.
AWS S3에 들어가, 버킷을 만들고(위 이미지에서는 techcourse-project-2025), 빌드 결과물을 업로드하면 된다.
CloudFront
CloudFront는 AWS의 CDN이다. S3에 올려둔 빌드 파일을 사용자와 가까운 엣지 서버에 캐싱해서 내려준다. 우리가 CloudFront를 쓴 이유는 두 가지였다. S3 단독으로는 안 되는 HTTPS를 붙일 수 있고, 캐싱 덕분에 응답도 빨라진다.
- CloudFront에 들어가 원본 생성을 누르고
- 1번에서 만든 S3 버킷과 경로를 선택한다.
- 오류 페이지를 설정한다.
React.js는 index.html에서 라우팅을 모두 관리하므로 403/404에러 발생 시 index.html로 다시 이동하도록 설정한다.
추가로, S3에 변경된 빌드 파일을 업로드를 했을 때 새롭게 배포하고 싶을 때는 무효화 생성을 해주어야한다. 이 과정은 CloudFront(CDN)에 캐싱된 객체를 무효화하는 것으로, 이를 통해 사용자들은 CloudFront에서 새롭게 변경된 S3 빌드 파일을 가져올 수 있게 된다.
CD
아키텍쳐 소개
현재 구성할 아키텍처는 GitHub에서 코드가 푸시되면 AWS CodePipeline을 통해 자동으로 빌드와 배포 프로세스가 시작되는 구조이다. CodeBuild에서 React 애플리케이션을 빌드한 후, 결과물을 S3 버킷에 업로드하고, CloudFront를 통해 전 세계에 콘텐츠를 배포하는 방식이다.
S3 + CloudFront 배포 자동화의 가장 간단한 방법은 GitHub Actions를 사용하는 것이다. GitHub Actions에서 S3에 빌드 파일을 업로드하고 CloudFront 캐시를 무효화할 수 있다. 하지만 우리 팀은 우아한테크코스의 AWS IAM 계정을 사용하고 있었고, 크루 IAM으로는 시크릿 키를 발급받을 수 없었다. 따라서 대안으로 CodeBuild + CodePipeline 방식을 채택할 수밖에 없었다.
CodeBuild
AWS CodeBuild는 코드를 빌드·테스트해서 배포용 산출물을 만들어주는 AWS 서비스다.
- AWS CodeBuild에 들어가
프로젝트 구성을 클릭한다.
프로젝트 이름을 설정하고, 직접 설정할 것이기에 기본 프로젝트로 설정한다.
- 소스
우리가 빌드를 위해 사용할 소스를 지정한다. 빌드를 할 프로젝트의 레포지토리와, 소스 버전을 선택한다. 우리는 client브랜치에서 배포를 진행할 것이기에 refs/heads/client로 지정한다.
- 환경
- Buildspec
빌드할 파일은 직접 넣어줄 것이기에 위와 같이 설정한다.
# frontend/buildspec.yml
version: 0.2
phases:
install:
runtime-versions:
nodejs: 18
commands:
- echo "Installing dependencies..."
- cd frontend
- npm install
pre_build:
commands:
- echo "Running lint..."
- npm run lint
- echo "Running type-check..."
- npm run type-check
build:
commands:
- echo "Building project..."
- npm run build
artifacts:
base-directory: frontend/dist
files:
- '**/*'
buildspec.yml은 CodeBuild에게 주는 작업 지시서다. 흐름은 로컬에서 하던 것과 똑같다. 의존성 설치 → 린트/타입 체크 → 빌드. 다른 점은 artifacts 섹션인데, 여기에 명시한 frontend/dist가 다음 단계(S3 업로드)로 넘어간다.
- 아티팩트
빌드 후 업로드할 위치이다. S3에 업로드할 것이기에 해당 S3를 선택한다.
- 로그
빌드 출력 로그를 CloudWatch에 남기기 위한 설정이다.
- 서비스 역할 권한
CodePipeline
AWS CodePipeline은 소프트웨어 릴리스 프로세스를 자동화하는 완전 관리형 지속적 전달 서비스다. 한마디로 배포 컨베이어 벨트다. GitHub에 푸시가 일어나면 소스 가져오기 → 빌드 → S3 업로드까지 정해둔 단계를 알아서 돌린다.
CodePipeline에서 소스를 가져오고, 빌드하고, 배포하는 아래와 같은 파이프라인을 구성할 것이다.
- 소스
Code Pipeline을 트리거할 레포지토리와 브랜치를 선택한다.
- Build
1번 소스에서 레포지토리로부터 가져온 소스를 사용할 것이기에, 소스의 출력 아티펙트인 SourceArtifact를 선택한다.
작업 공급자는 AWS CodeBuild를 선택하고, CodeBuild에서 작성한 이름(여기선 bombom)으로 선택한다.
- Deploy
2번 Build에서 만들어진 빌드 결과물을 사용할 것이기에, Build의 입력 아티펙트인 BuildOutput을 선택한다.
그리고 S3에 저장할 것이기에 작업 공급자는 Amazon S3를, 버킷과 경로는 배포를 원하는 S3로 선택한다.
Lambda
AWS Lambda는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 서버리스 컴퓨팅 서비스다. Lambda는 서버 관리 없이 이벤트가 오면 코드를 실행해주는 서비스다. 이 프로젝트에서의 CloudFront 캐시 무효화와 같은 특정 작업을 자동화하는 데 Lambda를 활용할 수 있다.
빌드 폴더를 S3에 변경된 리소스가 올라가면, CloudFront를 무효화하기 위해 Lambda를 생성해야한다.
import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
import { CodePipelineClient, PutJobSuccessResultCommand, PutJobFailureResultCommand } from "@aws-sdk/client-codepipeline";
const cf = new CloudFrontClient({ region: "us-east-1" });
const cp = new CodePipelineClient({ region: "ap-northeast-2" });
export const handler = async (event) => {
const jobId = event["CodePipeline.job"].id;
console.log(`CloudFront 캐시 무효화 시작 - Job ID: ${jobId}`);
try {
const result = await cf.send(new CreateInvalidationCommand({
DistributionId: `${process.env.cloudFrontId}`,
InvalidationBatch: {
Paths: { Quantity: 1, Items: ["/*"] },
CallerReference: `invalidation-${Date.now()}`
}
}));
console.log(`무효화 성공 - ID: ${result.Invalidation.Id}`);
await cp.send(new PutJobSuccessResultCommand({ jobId }));
console.log("CodePipeline 작업 완료");
} catch (err) {
console.error("오류:", err);
await cp.send(new PutJobFailureResultCommand({
jobId,
failureDetails: {
message: err.message,
type: "JobFailed",
},
}));
}
};
트러블 슈팅
1. 권한 이슈
Code PipeLine에서 Lambda를 실행하면 위와 같이 권한 이슈가 발생하고는 했다
- 해결 방법
- 작업 공급자에서
배포의 AWS Lambda가 아니라,호출의 AWS Lambda를 선택해야한다.
- 작업 공급자에서
배포의 AWS Lambda는 Lambda 함수 코드를 AWS에 업로드하고 설정하는 과정이고, 호출의 AWS Labmda는 이미 배포된 Lambda 함수를 실행시키는 것이라고 한다. 다른게 아니라 권한 이슈가 발생한 이유는 현재 사용 중인 우아한테크코스 계정에서 크루 IAM에서 배포의 AWS Lamda를 제한해두었기 때문이다.
2. 배포된 웹사이트에서 새로고침 시, 403 에러 뜨는 이슈
- 해결 방법 : CloudFront에서 오류 페이지를 설정한다.
403/404 오류 코드를 받았을 때 index.html로 리다이렉트시키면된다. 왜냐하면 리액트는 전부 index.html에서 모든 라우팅이 클라이언트 사이드에서 처리되기 때문이다. 이렇게 설정하면 사용자가 직접 URL을 입력하거나 새로고침을 했을 때도 React 애플리케이션이 올바르게 로드된다.
맺으며
Vercel이나 Netlify 같은 자동 배포 서비스만 사용해오다가 AWS로 직접 CI/CD 파이프라인을 구축해보니 정말 신선한 경험이었다. 이런 복잡한 과정들을 기존 서비스들이 완벽하게 자동화해주고 있었다는 사실을 실감하며 감사함을 느꼈다. 또한 AWS의 각 기능들을 사용해보며 멀다고만 느껴졌던 AWS와 한 걸음 더 가까워진 기분이었다.
구축 과정은 복잡했지만, 직접 파이프라인을 만들면서 배포의 전체 사이클을 이해하게 됐다. 현재 구축한 시스템에는 효율적인 캐싱 처리나 GitHub에서 배포 상태를 모니터링하는 기능 등이 아직 부족하다. 이런 부분들은 쓰면서 하나씩 손봐야겠다.