리액트 프로젝트 배포를 어디에 올릴까?

코린·2025년 10월 13일

리액트

목록 보기
23/23
post-thumbnail

리액트를 배포할 수 있는 툴과 플랫폼은 매우 많습니다.
이때 어떤 것을 어떻게 골라야할지에 대해서 알아보겠습니다.

들어가기에 앞서서...

SPA

Single Page Application단일 페이지 모던 웹 애플리케이션을 뜻합니다.
다양한 웹 사이트를 하나의 페이지로 담습니다.사용자가 처음 웹 사이트에 접속하면 핵심 정적 리소스(HTML,CSS,JavaScript)를 다운로드합니다.사용자가 다른 페이지로 이동하면, 페이지에서 변경이 필요한 부분만 로딩하고 전체 페이지를 다시 로딩하지 않습니다.이때 클라언트 사이드 라우팅, 즉 클라이언트 측에서 JavaScript로 라우팅을 처리합니다.

반대되는 개념으로는 MPA(Multi Page Application)가 있습니다.페이지를 이동하면 서버에서 새로운 페이지를 새로 렌더링해서 전송해주는 방식입니다.페이지 변경 시, 매번 서버에 요청을 보내야 한다는 단점이 있습니다.이 단점을 보완하기 위해 SPA가 등장하게 된 것 입니다..!

SSR 과 CSR

SSR


이미지 출처

  1. 사용자가 웹사이트에 요청을 보냄
    (https://어쩌구저쩌구 사이트 방문)

  2. 서버는 HTML 문자열을 미리 생성 (renderToString() 또는 renderToPipeableStream() 사용)
    (JS를 실행하지 않고도 페이지 구조와 텍스트를 완성)

  3. 브라우저는 빠르게 HTML을 렌더링
    (단, 이 사이트는 상호작용 못함, JS 파일을 다운로드 하지 않았기 때문)

  4. 브라우저 JS 다운로드

  5. 사용자는 콘텐츠를 볼 수 있고, 조작이 기록될 수 있음
    (SEO에 유리)

  6. 브라우저 JS 프레임워크 실행
    (Hydration 시작, 서버가 생성한 HTML을 다시 읽고 내부 상태와 이벤트를 연결)

  7. 기록된 사용자 조작 실행, 페이지 상호작용 가능해짐

CSR


이미지 출처

  1. 사용자가 웹사이트에 요청을 보냄
    (https://어쩌구저쩌구 사이트 방문)

  2. CDN이 빠르게 JS와 연결된 HTML 파일을 제공
    (index.html 반환 (내용은 거의 비어있음))

  3. 브라우저는 HTML 다운로드 후 JS를 다운로드
    (이때 동안, 사용자는 아무것도 볼 수 없음)

  4. 브라우저 JS 다운로드

  1. JS 실행(가상 DOM 생성), API 데이터 요청, 사용자는 placeholder(임시값)를 볼 수 있음
    (placeholder의 예시는 스피너, 스켈레톤(회색박스,텍스트 모양의 틀) 등등이 있음)
  1. 서버는 API에 의해 요청받은 데이터를 응답

  2. API로 부터 받은 데이터는 placeholder를 채우고 페이지는 상호작용이 가능해짐

간단하게 보자면,

CSR은 브라우저가 화면을 그리는 방식(JS 실행 후 렌더링), SSR은 서버가 미리 화면을 그려서 HTML로 보내는 방식 이라고 보면 됩니다.

이 둘의 차이를 왜 배포관점에서 알아야하죠?

CSR과 SSR은 렌더링 위치 (브라우저 vs 서버)가 다르기 때문에, 배포 시 필요한 인프라 구조,호스팅 방식,SEO 전략이 달라집니다.

CSR은 빌드 결과물을 CDN에 올리면 되지만, SSR은 서버 코드도 함께 배포해야 합니다.

리액트 배포

배포에서 알아야 할 개념

빌드

개발용 코드를 브라우저가 이해할 수 있는 형태로 변환 및 최적화 하는 과정

프론트엔드에서는 webpack,vite와 같은 빌드툴이 있습니다.

JSX/TypeScript -> JavaScript로 변환
코드 압축, 번들링(파일 합치기)

npm run build

결과물은 /build 혹은 /dist 폴더에 생성됩니다.

브라우저는 이 폴더의 index.html,main.js,style.css 만 실행합니다.

CDN(Content Delivery Network)

웹 콘텐츠를 세계 곳곳에 있는 여러 서버에 분산하여 저장하는 분산 서버 네트워크 시스템 입니다.

  • 오리진 서버

    • 웹사이트의 원본 콘텐츠를 저장하고 있는 중앙 서버
  • 엣지 서버

    • 전 세계 곳곳 위치한 분산 서버
    • 오리진 서버의 콘텐츠를 캐싱, 캐싱 정책에 따라 콘텐츠를 업데이트
  • DNS 서버

    • CDN에서 오리진 서버와 엣지 서버를 연결해주는 역할

  1. DNS 서버는 사용자의 위치를 기준으로 가장 가까운 엣지 서버(캐싱 서버)의 IP 주소를 반환
  2. 엣지 서버는 요청된 콘텐츠가 캐싱되어 있는지 확인 (캐싱되어 있지 않거나 만료되었다면, 오리진 서버에서 최신 콘텐츠를 가져옴)
  3. 엣지 서버는 사용자에게 콘텐츠 제공

배포 구조

CSR

브라우저 -> CDN -> index.html + JS/CSS 반환

SSR

브라우저 -> 서버(Node.js, Lamda) -> HTML 생성(renderTostring) -> JS 로드 후 Hydration

이 외에도 알아야 할 것 들이 있지만 너무 길어질 것 같아 생략합니다..!

저는 가장 대표적인 배포 방식 3가지를 추렸습니다.

AWS amplify, Netlify, S3 + CDN

각각이 어떻게 다른지, 어디에 적합한지 확인해 보겠습니다.

AWS Amplify

풀스택 애플리케이션 개발을 지원하는 개발 플랫폼으로 완전 관리형 입니다.

  • 배포 자동화
    • Git 연동 시 CI/CD + 빌드 + 배포 자동화
  • SSR/ISR 지원
  • 글로벌 배포
  • 리디렉트/Rewrite 설정
  • 확장성 관리

Netlify

정적 웹 사이트를 배포를 위한 플랫폼입니다.

  • Git 연결 시 배포 가능
  • PR 시 미리보기 URL 제공
  • 서버리스 함수 + Edge 기능
    • Netlify Functions
    • 제한적임
  • 정적 자산이 글로벌 CDN을 통해 즉시 배포

S3 + CDN

S3 버킷을 정적 웹 사이트 호스팅용으로 사용하며 CloudFront(CDN)으로 전 세계 엣지 노드에서 콘텐츠를 제공합니다.

  • 단순하며 비용 효율적인 정적 배포 구조
  • 완전한 제어
  • 확장성이 높음

각 플랫폼 비교

구분AWS AmplifyNetlifyS3 + CloudFront(CDN)
특징AWS 기반 풀스택 호스팅 (CI/CD + 백엔드 통합)정적 사이트 + Jamstack 배포 플랫폼정적 파일 저장 + CDN 조합
강점 (장점)✅ SSR/ISR 지원 (Next.js 등)
✅ 자동 빌드·배포 (Git 연동)
✅ 글로벌 CDN(CloudFront)
✅ AWS 서비스와 통합 용이
✅ 손쉬운 배포 (Git 연결만으로 즉시 배포)
✅ PR Preview(미리보기 URL)
✅ 내장 Functions / Edge 기능
✅ 기본 CDN 내장
✅ 가장 저렴하고 단순
✅ 완전한 제어 가능 (보안·정책 직접 설정)
✅ 매우 빠른 정적 콘텐츠 제공
✅ 의존성 적음
약점 (단점)⚠️ 설정 복잡 (AWS 초심자에 어려움)
⚠️ CloudFront 직접 수정 불가
⚠️ 비용 구조 복합
⚠️ SSR 한계 (정적 중심)
⚠️ 고급 로깅/통합 어려움
⚠️ 트래픽 증가 시 비용 상승
⚠️ CI/CD, SSL, Redirect 수동 설정
⚠️ SSR 불가 (정적만 가능)
⚠️ 운영 자동화 기능 부족
추천 사용 상황 (CSR)SEO가 덜 중요한 SPA 앱
+ AWS 리소스 통합이 필요한 경우
빠른 배포·개발 생산성 우선인 프로젝트정적 사이트 / 랜딩 페이지 / 블로그
추천 사용 상황 (SSR)SSR/ISR 필요 (Next.js, 개인화 콘텐츠 등)간단한 Edge SSR (한정적)
SSR보단 CSR + Prerender 중심
❌ SSR 불가 (CloudFront + Lambda로만 흉내 가능)

CSR, SSR 관점에서 어떤 플랫폼을 사용하는 것이 좋을까요?

여러 시나리오에서 비교를 해보겠습니다.

시나리오 / 요구사항CSR 방식 적합 플랫폼SSR 방식 적합 플랫폼이유 요약
마케팅 페이지 / 블로그 / 콘텐츠 중심 (정적인 페이지 많음)S3 + CloudFront 또는 Netlify 또는 Amplify Static일부 SSR이 필요한 경우 Amplify SSR 또는 Netlify Edge 가능정적 HTML 위주라 CSR → 프리렌더 또는 정적 생성 접근이 유리
SPA앱 + 일부 SEO 중요 경로 (예: 유저 프로필, 상품 상세)Amplify Static + 프리렌더 / Lambda@Edge 혼합Amplify SSR Hosting 또는 Next.js 배포정적은 비용/운영 낮고, SEO 경로는 SSR 또는 Edge 처리
개인화 / 로그인 중심 플랫폼 (SNS,이커머스,금융/투자)가능하지만 SSR 쪽이 강세SSR 또는 ISR 기반 플랫폼 (Amplify SSR, 서버리스 SSR)사용자별 콘텐츠가 많고 초기 렌더링 속도/SEO 중요
짧은 개발 시간 / 빠른 배포 우선Netlify 또는 Amplify StaticAmplify SSR (자동 감지)개발자 경험이 우선일 때 Netlify나 Amplify가 설정 부담 낮음
예산/운영 최소화S3 + CloudFront (정적 위주)SSR은 비용 증가하므로 제한적으로서버리스 실행, 요청 수 등에 따라 추가 비용 발생

CSR 프로젝트를 Amplify를 이용해버렸어요..

CSR 프로젝트를 Amplify를 이용해서 배포를 진행했습니다. 사실 크게 문제될 것은 없지만(정상적으로 작동하니까요..?ㅎㅎ) Amplify의 장점 중 하나인 서버리스/함수 부분을 챙기지 못한다는 것이 왠지 마음에 걸립니다.

이럴 경우에 해결책은 크게 2가지 방법으로 볼 수 있을 것 같습니다.

  1. S3 + CDN으로 재배포를 진행한다.
  2. 서버리스/함수 부분을 활용해보자. (사실 억지로 끼워 맞춘 느낌도 없지 않아 있습니다.)

저는 이미 Amplify의 편리함에 익숙해진 사람이기 때문에 2번 방향으로 진행해보겠습니다.

저의 경우 CSR 프로젝트지만 SEO 가 필요한 상황입니다.

앞서 보았듯이 CSR 프로젝트는 SEO 가 되지 않는다는 단점이 있습니다!

그렇다면 CSR 에 프리렌더 되는 HTML을 내려준다면.. 그게 SSR이면서 SEO를 가능하게 할 수 있지 않을까? 라는 생각이 들었습니다.

프리렌더링(Pre-rendering)

페이지를 사전에 생성하여, 브라우저가 초기 로드 시 바로 콘텐츠를 표시할 수 있도록 하는 기술입니다.

즉, HTML을 미리 생성하여 클라이언트에 제공하는 방식을 의미합니다.
-> SEO 개선 가능!

대표적인 도구 중 하나인, react-snap의 사용방식을 살펴보겠습니다.

react-snap은 Puppeteer를 사용해 페이지를 크롤링하고 정적 HTML을 생성합니다.

사용방법

  1. package.json을 변경합니다.
"scripts": {
  "postbuild": "react-snap"
}
  1. src/index.js를 변경합니다.
import { hydrate, render } from "react-dom";

const rootElement = document.getElementById("root");
if (rootElement.hasChildNodes()) {
  hydrate(<App />, rootElement);
} else {
  render(<App />, rootElement);
}

끝! 이지만

굳이굳이 Amplify를 활용하는 방법도 알아보겠습니다.

Amplify + react-snap

우선 Amplify (나의 앱)/호스팅/빌드설정 탭에서 빌드 사양 YML 파일 관리가 가능합니다.

  1. package.json 을 변경합니다.
{
  "scripts": {
    "build": "react-scripts build",
    "preview:static": "npx serve -s build -l 4173",
    "postbuild": "concurrently -k \"npm:preview:static\" \"sleep 2 && npx react-snap\""
  },
  "reactSnap": {
    "crawl": true,
    "inlineCss": true,
    "source": "http://localhost:4173",
    "saveAs": "html",
    "include": ["/", "/about", "/products"]
  },
  "devDependencies": {
    "concurrently": "^8",
    "react-snap": "^1",
    "serve": "^14"
  }
}

한 줄 한 줄 다시 살펴보겠습니다.

    "preview:static": "npx serve -s build -l 4173",
  • react-snap 실행을 위한 임시 서버 구동 명령어
    - serve 패키지를 사용해 build 폴더를 로컬에서 띄움

    • -1 4173 으로 포트 번호를 4173으로 지정
    • react-snap이 서버http://localhost:4173를 크롤링해서 HTML을 생성
      • 로컬 서버 괜찮은가..?
        • 빌드 서버내에서만 실행할 것 이라 괜찮습니다. (Amplify 내부 빌드 환경에서 4173을 사용하지 않는 다는 가정하에)
       "postbuild": "concurrently -k \"npm:preview:static\" \"sleep 2 && npx react-snap\""
  • concurrently는 여러 명령어를 동시에 실행하는 유틸리티

  • -k는 한 프로세스가 종료되면 나머지도 모두 종료 시킴

  • 위에서 만든 임시 서버를 실행

  • sleep 2 && npx react-snap 2초 기다린 후 react-snap이 크롤링 시작

  • 결과적으로, react-snap이 서버에 접속해 페이지별 HTML을 미리 만들어 build 폴더에 저장

     },
     "reactSnap": {
       "crawl": true,
       "inlineCss": true,
       "source": "http://localhost:4173",
       "saveAs": "html",
       "include": ["/", "/about", "/products"]
     },
- react-snap 설정 부분
	- `"crawl": true` react-snap이 HTML 내부의 `<a>` 링크를 자동 추적하여 추가 페이지를 렌더링합니다.
  - `"inlineCss": true` CSS 를 HTML에 인라인으로 삽입하여 초기 렌더 속도를 개선합니다.
  - `"source": "http://localhost:4173"` react-snap이 어떤 서버를 기준으로 크롤링 할지 결정
  - `"saveAs": "html"` 결과물을 HTML 파일로 저장
  - `include": [...]` 프리렌더 할 경로(라우트) 명시
  
2. `amplify.yml` 설정

```yaml
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build       # CRA 빌드 -> build/ 생성
        - npm run postbuild   # react-snap 프리렌더 실행
  artifacts:
    baseDirectory: build      # CRA는 build/, (Vite면 dist/)
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
  • postbuild 를 이용해 react-snap 프리렌더를 실행

한ㄱㅖ..

  1. 동적 페이지는 한계 존재

데이터나 URL이 API 응답에 따라 달라지는 페이지라면, 계속해서 재빌드 하지 않는 이상은 SEO가 불가능합니다.

왜냐?

react-snap은 빌드 시점에서만 작동합니다.

따라서 해당 시점에는, /product/1,product/2와 같은 동적 라우트 목록을 알 수가 없습니다.

그럼 동적페이지에 SEO를 하려면..?

가장 명쾌한 방법은 Next.js 와 같은 SSR/SSG 프레임워크로 마이그레이션 하는 것 이지만 아주 큰 공사가 됩니다.

그럼에도 CSR을 유지하면서 SEO를 확보하는 방식에 대해서 알아보겠습니다.

AWS CloudFront + Lambda@Edge

"특정 경로"만 Lambda@Edge에서 즉석으로 HTML을 만들어 변환할 수 있습니다.

  • 일반 사용자는 S3에서 React 정적 파일을 받습니다 -> CSR
  • 봇(구글,네이버,트위터 등)은 Lamda@Edge에서 만들어낸 HTML을 받습니다 -> SSR

예시

/product/123 같은 상품 상세 페이지를 SSR처럼 보이게 한다고 가정

  1. CloudFront 설정
  • 오리진: Amplify 혹은 S3 정적 사이트
  • Lamda@Edge 연결 (Viewer Request)
  1. Lambda@Edge 코드
const fetch = require("node-fetch");

exports.handler = async (event, context, callback) => {
  const req = event.Records[0].cf.request;
  const ua = req.headers['user-agent']?.[0]?.value || '';
  const isBot = /Googlebot|Bingbot|Twitterbot|LinkedInBot/i.test(ua);

  // 봇만 SSR 흉내 HTML 생성
  if (!isBot) return callback(null, req);

  // 예: 상품 정보 가져오기
  const match = req.uri.match(/^\/product\/(\d+)/);
  if (!match) return callback(null, req);
  const productId = match[1];
  
  // API에서 데이터 가져오기
  const apiRes = await fetch(`https://api.myapp.com/products/${productId}`);
  const product = await apiRes.json();

  // SSR 흉내 HTML 생성
  const html = `
    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="UTF-8" />
      <title>${product.name} | MyShop</title>
      <meta name="description" content="${product.summary}">
      <meta property="og:image" content="${product.image}">
    </head>
    <body>
      <h1>${product.name}</h1>
      <p>${product.summary}</p>
      <img src="${product.image}" alt="${product.name}" />
    </body>
    </html>
  `;

  callback(null, {
    status: '200',
    statusDescription: 'OK',
    headers: {
      'content-type': [{ key: 'Content-Type', value: 'text/html; charset=utf-8' }],
      'cache-control': [{ key: 'Cache-Control', value: 'public, max-age=300' }]
    },
    body: html
  });
};

위와 같이 하면, 동적 페이지에서도 SEO 가능한 HTML을 선택적으로 내려줄 수 있습니다.

주의할 점

  • Lambda@Edge는 원본 배포가 us-east-1 필요
    - Lamda@Edge는 CloudFront의 확장 기능으로, "중앙 제어 리전"인 us-east-1에 등록 필요

  • SEO 안정성: 콘텐츠 동일성(Paritiy)로 "클로킹" 방지
    - 검색봇과 일반 사용자에게 의도적으로 다른 내용을 주면 검색엔진 가이드 위반(클로킹)으로 패널티 위험이 있음

    • 봇 전용 HTML과 CSR 최종 렌더 결과를 샘플 경로별로 차이점을 비교 필요
    • 메타 데이터(타이틀/디스크립션/OG) 양쪽 동일하게 관리

결론..

프레임워크를 선택할 땐...신중히....

출처

SPA
SSR vs CSR
빌드
CDN
프리렌더링
react-snap

profile
안녕하세요 코린입니다!

0개의 댓글