운영 콘솔 아키텍처 시리즈 3/9
React로 만든 관리자 페이지를 배포한다고 하면 자연스럽게 웹 서버를 떠올리기 쉽다. EC2 인스턴스를 만들고, Nginx를 설치하고, 빌드된 파일을 복사하고, HTTPS 인증서를 붙이는 식이다.
하지만 React SPA의 빌드 결과물은 대부분 정적 파일이다. 서버가 매 요청마다 HTML을 렌더링하지 않는다면, 24시간 떠 있는 웹 서버가 꼭 필요하지 않을 수 있다.
문제 상황
운영 콘솔은 React SPA였다. SEO가 중요하지 않았고, 서버 사이드 렌더링도 필요하지 않았다. 브라우저가 index.html과 JavaScript 번들을 받은 뒤, 이후 화면 전환은 클라이언트 라우터가 처리했다.
필요한 것은 다음 정도였다.
- 커스텀 도메인
- HTTPS
- 정적 파일 캐싱
- SPA 라우팅 fallback
- 배포 후 캐시 무효화
- 낮은 운영 비용
이 조건에서 EC2와 Nginx를 유지하는 것은 과했다.
선택지
첫 번째 선택지는 EC2와 Nginx였다. 익숙하고 제어 범위가 넓다. 하지만 OS 패치, 프로세스 관리, 보안 업데이트, 장애 복구까지 직접 책임져야 한다.
두 번째 선택지는 정적 호스팅 플랫폼이었다. 배포 경험은 좋지만, 조직의 클라우드 계정과 도메인 관리 정책에 따라 맞지 않을 수 있다.
세 번째 선택지는 S3와 CloudFront였다. 빌드 산출물은 S3에 두고, CloudFront가 HTTPS와 CDN 캐시, 커스텀 도메인을 담당한다.
운영 콘솔에는 세 번째 방식이 가장 잘 맞았다.
최종 결정
React SPA는 S3에 업로드하고, CloudFront를 앞단에 둔다.

S3는 정적 파일 저장소 역할만 한다. CloudFront는 사용자와 가까운 엣지에서 파일을 캐싱하고, HTTPS 연결을 종단하며, 커스텀 도메인을 제공한다.
구현 구조
빌드 결과물은 보통 다음과 같은 파일로 구성된다.
build/
├── index.html
├── static/js/app.hash.js
├── static/css/app.hash.css
└── assets/*
이 파일들을 static-assets-bucket에 업로드한다. 사용자는 S3 URL이 아니라 console.example.com으로 접근한다. CloudFront는 요청을 받아 S3 오리진에서 파일을 가져오거나 캐시된 파일을 응답한다.
SPA 라우팅은 별도 처리가 필요하다. 사용자가 /reports/123 같은 경로로 바로 접근하면 S3에는 그 파일이 없다. 이때 CloudFront 또는 S3 오류 응답 설정에서 index.html을 돌려주도록 구성해야 한다. 그러면 React Router가 브라우저에서 실제 라우트를 해석한다.
정적 자산과 HTML의 캐시 정책도 분리했다.
| 파일 | 캐시 전략 |
|---|---|
index.html |
짧게 캐시하거나 배포 후 무효화 |
| hash가 붙은 JS/CSS | 길게 캐시 |
| 자주 바뀌는 JSON | 짧게 캐시 |
| 공개 이미지/정적 리소스 | 길게 캐시 가능 |
얻은 것
운영 부담이 크게 줄었다. 웹 서버 인스턴스를 운영하지 않으니 OS 패치, Nginx 설정, 프로세스 재시작, 디스크 용량 관리 같은 일이 사라졌다.
비용도 낮아졌다. 사내 운영 콘솔은 트래픽이 제한적이고, 정적 파일은 CDN 캐시에 잘 맞는다. 트래픽이 없는 시간에도 웹 서버 비용을 계속 내는 구조보다 S3와 CloudFront가 더 적합했다.
배포 단위도 단순해졌다. 새 버전을 빌드하고 S3에 동기화한 뒤 CloudFront invalidation을 실행하면 된다. 롤백도 이전 빌드 산출물을 다시 올리는 방식으로 처리할 수 있다.
포기한 것
서버에서 요청별로 동적 HTML을 만들 수는 없다. 하지만 운영 콘솔은 SEO가 중요하지 않고, 로그인 후 사용하는 도구였기 때문에 문제가 되지 않았다.
CloudFront 캐시 정책을 잘못 잡으면 배포한 버전이 바로 보이지 않을 수 있다. 특히 index.html이 오래 캐시되면 사용자가 오래된 JavaScript 번들을 계속 받을 수 있다. 그래서 배포 과정에서 invalidation을 명시적으로 포함해야 했다.
다시 설계한다면
정적 자산과 데이터 JSON의 캐시 정책을 처음부터 더 엄격히 분리하겠다. SPA 번들은 긴 캐시가 유리하지만, 운영 데이터는 최신성이 중요할 수 있다. 같은 S3를 쓰더라도 경로별 캐시 정책은 별도로 생각해야 한다.
또한 배포 스크립트에 검증 단계를 넣겠다. 업로드 후 index.html 접근, 주요 JS 파일 접근, CloudFront 응답 헤더 확인 정도는 자동화할 만하다.
정리
React SPA 배포에 항상 웹 서버가 필요한 것은 아니다. 서버가 HTML을 동적으로 렌더링하지 않고 정적 파일만 제공한다면 S3와 CloudFront만으로도 충분하다.
중요한 것은 "어디에 올릴까"보다 "어떤 파일을 얼마나 캐시할까", "SPA 라우팅을 어떻게 fallback할까", "배포 직후 사용자가 새 버전을 받게 할까"를 함께 설계하는 것이다.
함께 볼 GitHub 저장소
'성장과 기술 > 시스템 설계' 카테고리의 다른 글
| 같은 관리자 페이지처럼 보여도, 도메인이 다르면 콘솔을 나눠야 한다 (0) | 2026.07.07 |
|---|---|
| 작은 운영 콘솔을 안정적으로 굴리기 위한 아키텍처 결정들 (0) | 2026.07.06 |
| 다시 설계한다면 무엇을 바꿀까 (0) | 2026.07.05 |
| 운영 복잡도를 비용처럼 계산하기 (0) | 2026.07.04 |
| 처음부터 완벽한 공통화를 하지 않은 이유 (0) | 2026.07.03 |
글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.