TIL

배포방법 고민 - EC2 + NginX vs CloudFront + S3

inz1234 2025. 1. 11. 00:39

새로운 사이드 프로젝트를 하는 도중 프론트 배포를 무엇으로 할지 고민에 빠졌다.

우리의 서비스는 웹소켓을 이용한 투표 서비스이고, 정적파일을 저장할 일이 아직까지는 많지 않다.

제목의 EC2 + NginX 와 CloudFront + S3 두 방법의 특징과 장단점, 현재 프로젝트에 무엇이 더 적합할지 알아보았다.


CloudFront + S3

  • Amazon S3(Simple Storage Service)
    • 정적파일(HTML, CSS, JS, 이미지, 폰트 등)을 저장하는 스토리지
    • 정적파일이란?
    • 더보기
      서버의 가공 없이 그대로 제공할 수 있는 자원
      HTML, CSS파일, JS파일, 이미지, 폰트, 비디오/오디오  
    • 장점
      • 서버리스로 서버 관리가 필요 없음
      • 무제한 확장성
      • 내구성(데이터 유실 위험 거의 X)
    • 단점
      • 웹소켓 지원 X
      • 트래픽이 많아지면 직접 트래픽 감당해야함
  • Amazon CloudFront(CDN, Content Delivery Network)
    • S3에 저장된 정적파일을 전 세계에 빠르고 안전하게 배포
    • 장점
      • 전 세계에 분포한 Edge Location(엣지 서버)를 통해 클라이언트와 가장 가까운 서버에서 빠르게 데이터 제공
      •  자주 요청되는 파일 캐싱 - 서버 부하 down, 빠른 응답
      • 정적파일을 자동으로 압축 전송 - 네트워크 대역폭 절감
      • 보안강화
        • SSL/TLS 인증서 적용으로 HTTPS 적용가능
        • AWS WAF(웹 방화벽)와 연동해 DDos 공경, 악성 트래픽 차단 가능
        • Signed URL, Signed Cookie로 리소스 접근 제어
      • 비용절감
    • 단점
      • 웹소켓, API 호출 직접 처리 불가 - 서버나 다른 API Gateway 서비스와 연결해야 함
      • 캐시로 인해 변경 사항 반영이 다소 느릴 수 있음
      • HTTP-GET 요청만 가능
      • 이미지 업로드/수정/삭제(POST, PUT, DELETE) 자체적으로는 불가
        클라이언트 → 별도의 API 서버(EC2) S3 필요
        Presigned URL(클라이언트 → S3 직접 수정 가능 임시 접근 권한) 발급 후에는 직접 수정 가능
        But, 초기 Presigned URL을 요청 시에는 EC2 필요
  • 흐름도
  • 더보기
    클라이언트    CloudFront    S3에서 파일 가져옴 + CloudFront에 캐시 저장    클라이언트

                ㄴ 캐시 있으면? 바로 클라이언트로 응답

EC2 + NginX

  • NginX
    • 웹 서버이자 리버스 프록시 서버(중간 관리자)
      • 웹 서버(Web Server)
        클라이언트 요청(HTTP)을 받아서 정적 파일(HTML, CSS, JS, 이미지 등)을 전달
      • 리버스 프록시(Reverse Proxy)
        백엔드 서버를 외부에 노출시키지 않고,
        외부에서 들어오는 요청을 내부 서버(EC2)로 안전하게 전달
      • 로드 밸런서(Load Balancer)
        서버 부하를 분산 -> 트래픽이 몰릴 때까지 서버 다운 방지
      • 캐시 서버(Cache Server) - CloudFront처럼 캐싱 가능
      • 보안
        SSL 인증서, 보안헤더 설정가능
      • 동적/정적파일 모두 서빙 가능
        • EC2 서버에 정적 파일을 두고 바로 제공 가능
        • 더보기
          /var/www/html/   ← EC2 서버의 디렉토리
            ├── index.html
            ├── styles.css
            └── script.js
        • EC2 서버로 API 요청 / 웹소켓 요청 동적요청 안전하게 전달(NginX가 필수는 X)
  • 흐름도
    • 더보기
      [사용자] ──▶ [Nginx] ──▶ [정적 파일 (/var/www/html)]  → 정적 자원 응답
                 │
                 └──▶ [Node.js 서버 (EC2)] → 동적 요청(API, 웹소켓)
    • 더보기
      [클라이언트 (웹 브라우저)]
              │
              │ (HTTP 요청)
              ▼
      [Nginx (프론트엔드 EC2)]  ───▶ [정적 자원 제공 (React 앱)]
              │
              │ (웹소켓 연결 요청)
              ▼
      [Nginx 프록시 설정]
              │
              ▼
      [백엔드 EC2 (웹소켓 서버 + DB)]
              │
              │ (실시간 데이터 전송)
              ▼
      [클라이언트]
  • NginX가 요청의 헤더(Upgrade: websocket)을 보고 웹소켓 요청인지 HTTP 요청인지 구분하여 각 웹소켓 서버 / API 서버로 분배
  • ⚠️ 주의점
    • BrowserRouter: React에서 SPA를 구현할 때 사용하는 라우팅 도구
      • 사용자가 URL을 변경하면, 서버에 새로운 요청을 보내는 것이 아니라 React 내부에서 URL 경로에 따라 컴포넌트만 바꿔서 렌더링 해주는 기능
      • 새로고침을 하지 않아도 화면이 전환됨
      • 만약 새로고침을 누르면(ex. /about -> F5새로고침) 브라우저는 서버에 /about 경로의 리소스 요청
      •  /about  은 React 내부의 가상경로 = 서버에는 실제로 /about 이라는 경로의 리소스를 가지고 있지 않음 => 404 Error 
  • 해결방법 
    • 서버가 어떤 경로로 요청이 와도 항상 React 앱의 index.html을 반환하도록 설정해야함
    • 더보기
      // NginX 
      location / {
        try_files $uri /index.html;
      }

웹 소켓

  • HTTP와 웹소켓은 모두 TCP기반이지만 각각 HTTP, 웹소켓 프로토콜을 따름
  • 웹소켓 연결이 성립되면, 웹소켓 서버가 클라이언트 서버와 직접 통신 
  • 더보기
    // 응답흐름
    클라이언트 → Nginx(프록시) → 백엔드 EC2 (웹소켓 서버) → 클라이언트
  • 직접 소통한다? 그럼 만약 웹소켓으로 채팅 중에 이미지 업로드(HTTP 요청)를 하면 웹소켓 연결이 끊어지나?
  • 더보기

    1. 채팅 중 DB에 저장된 이미지를 친구에게 보내기

    1️⃣ 이미지 URL 요청 (HTTP)
    [클라이언트 A] → [Nginx(프론트)] → [백엔드 API 서버] → [S3](DB)

    2️⃣ 이미지 URL 전송 (웹소켓)
    [클라이언트 A] → [웹소켓 서버] → [클라이언트 B]

    3️⃣ 이미지 로딩 (HTTP)
    [클라이언트 B] → [CloudFront] → [S3] → 이미지 표시


    2. 채팅 중 이미지 업로드 후 친구에게 보내기
    1️⃣ 이미지 업로드 (HTTP)
    [클라이언트 A] → [Nginx(프론트)] → [백엔드 API 서버] → [S3]

    2️⃣ 업로드 완료 알림 (웹소켓)
    [백엔드 API 서버]  [웹소켓 서버] → [클라이언트 B] (이미지 URL 전송)
            ㄴ 이미지 url 또는 식별자 API 서버 - 웹소켓 서버로 직접 연결하여 넘김

    3️⃣ 이미지 로딩 (HTTP)
    [클라이언트 B] → [CloudFront] → [S3] → 이미지 표시

  • 웹소켓이 연결된 뒤에는 웹소켓 서버와 클라이언트가 직접 소통한다고 했는데, 그럼 프론트에서는 초기 요청은 프론트 NginX로,  연결된 후 요청은 바로 웹소켓 서버로 보내야 하는 건가?
    • No!
    • 더보기

      웹소켓 연결은 한 번만 하면 되고,
      이후 모든 통신은 자동으로 연결된 웹소켓 서버와 진행

      // 웹소켓 연결 후 통신 흐름
      1️⃣ 웹소켓 연결 요청 [클라이언트]  [프론트 Nginx]  [백엔드 Nginx]  [웹소켓 서버]
      2️⃣ 연결 성립 후 실시간 통신 [웹소켓 서버]  [클라이언트]

  • 그럼 웹소켓 서버는 클라이언트를 어떻게 기억하는 걸까?
    • 더보기

      웹소켓 서버는 연결이 성립되면, 클라이언트와의 연결을 소켓 객체(Socket)로 관리

      즉, 연결된 소켓 객체를 통해 클라이언트와 통신을 유지

  • 클라이언트 → 프론트 NginX(EC2) → 백엔드 EC2 의 요청에서 백엔드가 웹소켓 요청인지 API 요청인지 어떻게 구별할까?
    • 웹소켓 연결을 맺을 때 특별한 헤더를 포함
    • 더보기

      // 일반 API 요청 헤더

      POST /api/upload HTTP/1.1
      Host: example.com
      Content-Type: application/json
      Authorization: Bearer xxxxx

      // 클라이언트  서버 웹소켓 요청 헤더
      GET /ws/chat HTTP/1.1 
      Host: example.com
      Upgrade: websocket                             HTTP에서 웹소켓으로 연결 전환
      Connection: Upgrade                           프로토콜 전환을 명시
      Sec-WebSocket-Key: xxxxx
      Sec-WebSocket-Version: 13

      // 프론트에서 웹소켓 연결 시 (자동으로 Upgrade 헤더 추가)
      const socket = io('http://localhost:3000');

      // 서버  클라이언트 응답 헤더
      HTTP/1.1 101 Switching Protocols            프로토콜 전환(101 Switching Protocols)됨 의미
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: yyyyy

  • ⚠️ 주의점: 프로토콜(http vs https)
  • 🔒
    웹 사이트가 https라면 → 웹소켓도 wss(보안 웹소켓) 사용
    웹사이트가 http라면 → 웹소켓도 ws(일반 웹소켓) 사용
const socket = io('wss://frontend.example.com');

결론

이미지 등 정적파일 없이 웹소켓만 쓸 거면 NginX + EC2를 사용하는 게 좋을 것 같다.

하지만 현재 서비스는 투표 현황이나 아이템, 투표에 올릴 이미지 등 정적파일이 꽤나 필요할 것 같아서 
NginX + EC2 + S3를 사용할 예정이고, 배포는 CI/CD 자동화를 위해 CSR 서비스지만 Docker를 이용할 예정이다.

  • 웹소켓은 NginX 설정에서 → 백엔드 웹소켓 서버로
  • 정적파일은 NginX 설정에서 → 백엔드 API 서버로 

EC2 기본파일 구조

더보기

/
├── bin       # 기본 명령어 실행 파일 (ls, cp, mv 등)
├── boot      # 부팅 관련 파일
├── dev       # 장치 파일
├── etc       # 시스템 설정 파일 (Nginx, MySQL 설정 파일)
├── home      # 사용자 홈 디렉토리
│   └── ubuntu  # Ubuntu EC2의 기본 사용자 디렉토리(파일 업로드 후 이동) 
├── lib       # 시스템 라이브러리
├── media     # 미디어 장치 마운트
├── mnt       # 임시 마운트 디렉토리
├── opt       # 추가 패키지 설치 경로(확장)
├── root      # 루트 계정 홈 디렉토리
├── run       # 실행 중인 프로세스 정보
├── sbin      # 시스템 관리 명령어
├── srv       # 서비스 데이터(확장)
├── tmp       # 임시 파일 저장소
├── usr       # 사용자 프로그램 및 라이브러리
│   └── share/nginx/html  # ✅ Nginx의 기본 웹 파일 경로
├── var       # 로그 파일, 웹 서버 파일
│   ├── www   # 웹 서버 파일 경로 (Nginx, Apache)
│   └── log   # 로그 파일 (Nginx, 시스템 로그)

 

더보기

// NginX

server {
    listen 80;
    server_name your-domain.com;

    # ✅ API 요청은 백엔드 EC2로 프록시
    location /api/ {
        proxy_pass http://백엔드-EC2-IP:4000; #API 서버
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    # ✅ 웹소켓 요청은 백엔드 EC2로 프록시
    location /ws/ {
        proxy_pass http://백엔드-EC2-IP:5000;  # 웹소켓 서버
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'Upgrade';
    }

    # ✅ 정적 파일 요청은 Nginx가 직접 제공
    location / {
        root /usr/share/nginx/html;  # React 빌드 파일
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}

 

참고
https://suvera.tistory.com/79