정적 호스팅 배포
빌드 산출물을 Netlify에 배포하고 Git 연동 자동 배포·도메인 연결·SPA 경로 새로고침 문제를 해결합니다.
이 글에서는 React SPA(Single Page Application)를 배포할 때 가장 흔히 쓰는 방식 가운데 하나인 정적 호스팅 서비스(Static Hosting Service)로 배포하는 흐름을 정리합니다. 앞 글에서 만든 dist 산출물을 호스팅 서비스에 올리고, Git 연동 자동 배포와 도메인 연결, SPA 경로의 새로고침 문제를 차례로 다룹니다.
정적 호스팅 서비스란?
정적 호스팅 서비스는 HTML, CSS, JavaScript, 이미지 같은 정적인 파일(Static Files)을 저장해 웹으로 제공하는 서비스입니다. React 같은 SPA는 빌드하면 결국 이런 정적 파일의 집합이 되므로 정적 호스팅에 배포하기 알맞습니다.
정적 호스팅 서비스는 대부분 GitHub, GitLab, Bitbucket 같은 Git 저장소와 연동해 자동 배포를 쉽게 설정할 수 있고, CDN(Content Delivery Network)으로 사용자에게 가까운 서버에서 파일을 제공해 로딩이 빠릅니다. 파일을 제공하는 인프라는 서비스가 운영하므로 별도의 서버 관리 없이도 쉽게 확장할 수 있습니다. 다만 서비스마다 요금과 사용량 한도, 기능 조건이 다르므로 프로젝트에 맞는 플랜을 확인하고, 무료라는 이유만으로 제한 없이 운영할 수 있다고 가정하지 않습니다. 사용량 한도나 장애, 외부 API의 지연까지 없어지는 것은 아니므로 자신의 요청 경로도 확인합니다.
React 개발자들이 많이 쓰는 정적 호스팅 서비스는 다음과 같습니다. 이 글의 실습 대상인 Netlify는 Git 연동 빌드와 정적 파일 배포를 중심으로 설명하며, 도메인과 인증서, 함수, 폼 같은 추가 기능은 이용 플랜과 현재 서비스 문서의 조건을 확인합니다. 이 글에서 고른 서비스일 뿐 모든 프로젝트에 같은 도구가 필요한 것은 아닙니다. Next.js를 만든 회사의 서비스인 Vercel은 Netlify와 비슷한 배포 기능을 제공하며 Next.js 프로젝트에 특히 잘 맞습니다. GitHub Pages는 GitHub 저장소의 정적 파일을 호스팅하는 간단한 서비스로 개인 프로젝트나 포트폴리오 사이트에 알맞지만, 빌드와 배포 과정은 직접 구성해야 합니다. Google Firebase 플랫폼의 Firebase Hosting은 인증이나 데이터베이스 같은 Firebase의 다른 서비스와 함께 쓰기 좋고, AWS S3에 정적 파일을 저장하고 CloudFront로 CDN을 구성하는 방식은 가장 유연하지만 설정이 복잡하고 비용 관리가 필요합니다.
배포하기 전에는 빌드 산출물과 배포할 브랜치, 출력 경로를 먼저 정합니다. 계정별 요금과 사용량, 권한, 화면의 버튼 이름은 바뀔 수 있으므로 실제 대시보드에서 확인하며, 아래의 화면 이름은 설정 항목을 찾기 위한 예입니다.
Netlify를 이용한 배포 (GitHub 연동)
Netlify로 React 앱을 배포하는 과정은 간단하며, 대부분의 단계가 자동으로 이루어집니다. 먼저 React 프로젝트의 코드가 GitHub, GitLab, Bitbucket 같은 Git 원격 저장소에 푸시되어 있어야 합니다. 저장소를 만들고 커밋을 원격에 올리는 방법이 낯설다면 Git 기초와 협업 워크플로우에서 GitHub에 푸시하는 과정을 먼저 따라 해 보세요. Vite의 기본 산출물인 dist는 빌드할 때 만들어지므로, 프로젝트의 .gitignore에서 이 폴더를 제외했는지도 확인합니다. 그다음 Netlify 웹사이트에서 GitHub 계정 등으로 가입하고 로그인합니다.
Netlify로 배포하는 과정
Netlify의 현재 문서는 사이트(site)를 프로젝트(project)라고 부르므로, 화면에서도 두 이름을 함께 볼 수 있습니다(Netlify 문서의 저장소에서 배포하기). 로그인한 뒤 팀 대시보드에서 Add new project를 누르고 Import an existing project를 고릅니다. 프로젝트가 있는 Git 공급자를 골라 연결하며, 처음 연결할 때는 저장소에 접근할 권한을 묻는 화면이 나올 수 있습니다. 연결된 저장소 목록에서 배포할 React 프로젝트의 저장소를 고릅니다.
이어서 빌드 설정(Build settings)을 정합니다. Netlify는 프레임워크를 감지해 알맞은 설정을 제안하며, 배포할 Git 브랜치(Branch to deploy, 보통 main), 빌드 명령을 실행할 디렉터리(Base directory, 보통 프로젝트 루트라서 비워 둠), 프로젝트를 빌드하는 명령(Build command), 빌드된 정적 파일이 만들어지는 디렉터리(Publish directory)를 확인합니다. Vite 프로젝트라면 Build command는 npm run build, Publish directory는 dist로 두면 대부분 바로 배포됩니다.
설정을 확인한 뒤 배포를 시작하는 버튼(현재 문서에는 Publish로 적혀 있음)을 누르면, Netlify가 Git 저장소에서 코드를 가져와 빌드 명령을 실행하고 Publish directory의 내용을 CDN에 배포합니다. 진행 상황은 대시보드에서 실시간으로 볼 수 있으며, 배포가 끝나면 https://your-random-name.netlify.app/ 같은 임의의 주소가 만들어져 그 주소로 React 앱에 들어갈 수 있습니다.
자동 배포 (Continuous Deployment)
Netlify의 큰 장점 가운데 하나는 자동 배포(Continuous Deployment)입니다. 한 번 설정해 두면 지정한 브랜치(main 등)에 코드를 푸시할 때마다 Netlify가 이를 감지해 자동으로 빌드하고 배포합니다. 그래도 빌드와 배포가 성공했는지, 실제 주소가 제대로 응답하는지는 확인해야 하며, 별도의 테스트 통과를 배포의 조건으로 삼으려면 그 순서도 따로 구성해야 합니다.
호스트의 요청 분기와 브라우저 라우팅을 나눈다 — 상태 추적
하나의 직접 URL 요청에서 호스트 분류/파일 또는 rewrite/브라우저 router 매칭의 현재 위치와 HTTP/view 결과가 달라진다. 더 엄격한 자산 404는 별도 규칙임을 명시한다.
STATIC HOST / SPA REWRITE / ROUTER
호스트의 요청 분기와 브라우저 라우팅을 나눈다
아래는 실제 파일을 먼저 제공하고 허용한 앱 경로만 index.html로 rewrite하는 운영 설계 예시다. 앱 셸과 JS 실행 이후 React Router가 원래 URL의 화면을 선택한다.
HOST / REAL FILE
존재하는 자산은 직접 제공한다
예: /assets/app-hash.js. Netlify의 기본 rewrite는 실제 배포 파일을 먼저 제공한다. 해시 자산은 내용과 캐시 정책을 맞추고, HTML은 최신 버전 재검증을 구성한다.
HOST → BROWSER / APP ROUTE
깊은 URL도 앱 셸을 받는다
예: /projects/42 직접 진입. 호스트가 index.html을 200으로 응답하며 주소창 URL은 유지된다. 브라우저가 번들을 실행하면 React Router가 원래 path를 매칭한다.
POLICY / MISSING FILE
엄격한 404는 직접 구성한다
예: 존재하지 않는 /assets/missing.js나 앱 밖 경로. 자산·API·앱 경로를 나누는 호스트 규칙을 설정해야 위 404 분기를 구현할 수 있다. 클라이언트 Router만으로 HTTP 응답을 정하지 않는다.
LESSON / WILDCARD RULE
본문의 한 줄은 범위가 넓다
Netlify의 /* /index.html 200은 실제 파일이 없으면 앱 밖 경로나 누락 JS·CSS도 HTML 200으로 응답할 수 있다. 이것만으로 위 엄격한 분기가 완성됐다고 판단하지 않는다.
Vite base는 배포 하위 경로의 자산 URL을 조정한다. BrowserRouter의 basename이나 호스트의 깊은 URL 새로고침 규칙을 자동으로 설정하지 않는다. _redirects는 Netlify 방식이므로 다른 호스트는 해당 규칙을 사용한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 브라우저 요청에서 존재 파일·허용 앱 경로·누락 자산 또는 앱 밖의 세 결과로 각각 갈라진다. 세 결과를 세로에 놓았지만 서로 이어지는 실행 순서가 아니다. 허용 앱 경로만 HTML과 JS 실행 뒤 Router 매칭으로 이어진다. 같은 다섯 노드와 네 간선이며 엄격한 404는 추가 호스트 정책이다. 본문 wildcard 한 줄만으로 이 정책이 자동 완성되지 않는다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 호스트 요청 | host: 파일/app/앱 밖 판정 / router: 실행 전 | 브라우저 document 요청이 host에서 분류된다 | BrowserRouter보다 호스트 응답이 먼저다 |
| 응답 경계 | app: index.html 응답 / 누락 자산: 별도 404 규칙 필요 | 파일은 직접 제공하고 허용 app 경로는 URL 유지 rewrite를 한다 | 단순 wildcard 예제는 엄격한 자산 분리를 완성하지 않는다 |
| router 실행 | URL: 원 pathname / UI: 매칭 결과 | 받은 문서에서 JS가 pathname을 읽어 화면을 고른다 | client 화면 선택이 반환 HTTP 상태를 바꾸지 않는다 |
호스트의 요청 분기와 브라우저 라우팅을 나눈다 — 상태 추적
하나의 직접 URL 요청에서 호스트 분류/파일 또는 rewrite/브라우저 router 매칭의 현재 위치와 HTTP/view 결과가 달라진다. 더 엄격한 자산 404는 별도 규칙임을 명시한다.
STATIC HOST / SPA REWRITE / ROUTER
호스트의 요청 분기와 브라우저 라우팅을 나눈다
아래는 실제 파일을 먼저 제공하고 허용한 앱 경로만 index.html로 rewrite하는 운영 설계 예시다. 앱 셸과 JS 실행 이후 React Router가 원래 URL의 화면을 선택한다.
HOST / REAL FILE
존재하는 자산은 직접 제공한다
예: /assets/app-hash.js. Netlify의 기본 rewrite는 실제 배포 파일을 먼저 제공한다. 해시 자산은 내용과 캐시 정책을 맞추고, HTML은 최신 버전 재검증을 구성한다.
HOST → BROWSER / APP ROUTE
깊은 URL도 앱 셸을 받는다
예: /projects/42 직접 진입. 호스트가 index.html을 200으로 응답하며 주소창 URL은 유지된다. 브라우저가 번들을 실행하면 React Router가 원래 path를 매칭한다.
POLICY / MISSING FILE
엄격한 404는 직접 구성한다
예: 존재하지 않는 /assets/missing.js나 앱 밖 경로. 자산·API·앱 경로를 나누는 호스트 규칙을 설정해야 위 404 분기를 구현할 수 있다. 클라이언트 Router만으로 HTTP 응답을 정하지 않는다.
LESSON / WILDCARD RULE
본문의 한 줄은 범위가 넓다
Netlify의 /* /index.html 200은 실제 파일이 없으면 앱 밖 경로나 누락 JS·CSS도 HTML 200으로 응답할 수 있다. 이것만으로 위 엄격한 분기가 완성됐다고 판단하지 않는다.
Vite base는 배포 하위 경로의 자산 URL을 조정한다. BrowserRouter의 basename이나 호스트의 깊은 URL 새로고침 규칙을 자동으로 설정하지 않는다. _redirects는 Netlify 방식이므로 다른 호스트는 해당 규칙을 사용한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 브라우저 요청에서 존재 파일·허용 앱 경로·누락 자산 또는 앱 밖의 세 결과로 각각 갈라진다. 세 결과를 세로에 놓았지만 서로 이어지는 실행 순서가 아니다. 허용 앱 경로만 HTML과 JS 실행 뒤 Router 매칭으로 이어진다. 같은 다섯 노드와 네 간선이며 엄격한 404는 추가 호스트 정책이다. 본문 wildcard 한 줄만으로 이 정책이 자동 완성되지 않는다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 호스트 요청 | host: 파일/app/앱 밖 판정 / router: 실행 전 | 브라우저 document 요청이 host에서 분류된다 | BrowserRouter보다 호스트 응답이 먼저다 |
| 응답 경계 | app: index.html 응답 / 누락 자산: 별도 404 규칙 필요 | 파일은 직접 제공하고 허용 app 경로는 URL 유지 rewrite를 한다 | 단순 wildcard 예제는 엄격한 자산 분리를 완성하지 않는다 |
| router 실행 | URL: 원 pathname / UI: 매칭 결과 | 받은 문서에서 JS가 pathname을 읽어 화면을 고른다 | client 화면 선택이 반환 HTTP 상태를 바꾸지 않는다 |
위 그림은 배포한 뒤 브라우저의 요청을 호스트가 나누는 방식과, 그 뒤에 브라우저에서 일어나는 라우팅을 구분해 보여 줍니다. 그림의 요약처럼 이것은 실제 파일을 먼저 제공하고 허용한 앱 경로만 index.html로 재작성하는 운영 설계의 예이며, 앱 셸과 JS가 실행된 뒤에 React Router가 원래 URL의 화면을 고릅니다. 브라우저의 URL 요청은 호스트에서 먼저 분류되어, 존재하는 파일이면 200으로 그 파일을 직접 주고, 허용한 앱 경로라면 200으로 index.html을 주며, 누락된 자산이나 앱 밖의 경로라면 별도의 규칙으로 404를 줍니다. 세 결과는 세로로 놓여 있지만 차례로 이어지는 실행 순서가 아니며, 허용한 앱 경로만 JS 실행과 Router 매칭으로 이어집니다.
그림 아래의 네 칸은 각 분기를 예로 풉니다. /assets/app-hash.js 같은 존재하는 자산은 Netlify의 기본 재작성에서도 실제 파일이 먼저 제공되며, 해시 자산은 내용과 캐시 정책을 맞추고 HTML은 최신 버전을 다시 확인하도록 구성합니다. /projects/42로 바로 들어오는 깊은 URL은 호스트가 index.html을 200으로 주고 주소창의 URL은 그대로 남으며, 번들이 실행되면 React Router가 원래 경로를 맞춥니다. 존재하지 않는 /assets/missing.js나 앱 밖의 경로에 엄격한 404를 주려면 자산과 API, 앱 경로를 나누는 호스트 규칙을 직접 설정해야 하며, 클라이언트의 Router만으로 HTTP 응답을 정할 수는 없습니다. 마지막 칸이 짚듯 뒤에서 볼 Netlify의 /* /index.html 200 한 줄은 실제 파일이 없으면 앱 밖의 경로나 누락된 JS·CSS도 HTML 200으로 응답할 수 있으므로, 이것만으로 위의 엄격한 분기가 완성되지는 않습니다. 그림의 마지막 설명처럼 Vite의 base는 하위 경로에 배포할 때 자산 URL을 조정할 뿐 BrowserRouter의 basename이나 호스트의 깊은 URL 새로고침 규칙을 설정해 주지 않으며, _redirects는 Netlify의 방식이므로 다른 호스트에서는 그 서비스의 규칙을 씁니다.
커스텀 도메인 설정
임의의 Netlify 주소 대신 www.myawesomeapp.com 같은 자체 도메인을 연결할 수 있습니다. 대시보드에서 해당 프로젝트를 고르고, 왼쪽 메뉴의 Domain management에서 Production domains로 들어가 Add a domain을 누른 뒤, 이미 가진 도메인을 추가하는 항목을 골라 도메인 이름을 입력합니다. 그다음 Netlify가 보여 주는 DNS 설정(CNAME이나 A 레코드)을 가비아나 GoDaddy 같은 도메인 등록 업체에 적용합니다. CNAME과 A 레코드가 각각 이름을 어디로 연결하는지는 DNS 레코드와 실습에서 다룹니다. DNS를 연결한 뒤에는 대시보드에서 인증서의 발급 상태와 HTTPS 접속을 확인하며, 도메인 소유 확인이나 DNS 오류, 현재 플랜의 조건 때문에 끝나지 않을 수 있으므로 설정을 입력한 것만으로 발급에 성공했다고 단정하지 않습니다.
라우팅 문제 해결 (SPA Fallback)
React SPA는 React Router 같은 클라이언트 측 라우팅을 씁니다. 그래서 /about이나 /products/123처럼 클라이언트 라우터가 맡는 앱 경로는, 주소창으로 직접 요청해도 index.html을 받을 수 있어야 하며, 실제 자산과 API 경로는 따로 처리합니다. 하지만 웹 서버는 기본적으로 요청한 경로에 해당하는 파일이 없으면 404 Not Found를 돌려줍니다.
이 문제를 해결하려고 Netlify 같은 정적 호스팅 서비스는 재작성 규칙을 설정해, 파일이 없는 경로의 요청을 index.html로 돌려주는 기능을 제공합니다. Netlify에서는 프로젝트의 public 폴더 안에 _redirects 파일을 만들어 이 규칙을 더하며, 이 파일은 public 폴더에 있어야 빌드할 때 dist로 복사됩니다.
/* /index.html 200이 기본 규칙은 실제 배포 파일이 있으면 그 파일을 먼저 주고, 나머지는 URL을 유지한 채 index.html을 200으로 응답하는 재작성(rewrite)입니다. 이렇게 하면 React Router가 경로를 처리할 수 있게 됩니다. Netlify 문서의 rewrite와 proxy가 설명하듯 200 rewrite는 원래 URL을 유지한 채 다른 파일의 내용을 응답하는 동작이며, 브라우저를 다른 주소로 이동시키는 3xx 리디렉션과 구별합니다. 다만 누락된 JS·CSS 요청과 앱 밖의 경로도 HTML 200으로 가릴 수 있으므로, 그림의 엄격한 404 분기는 이 한 줄만으로 구현되지 않습니다.
파일 요청과 앱 경로를 따로 확인하기
배포한 HTML이 assets/app.js를 참조하고 라우터에 /about이 있다고 가정합니다. 브라우저의 Network 탭에서 다음 네 요청의 URL과 상태, Content-Type, 응답의 첫 내용을 나란히 확인합니다. 파일 이름은 실제 빌드에 맞춰 바꿉니다.
| 요청 | 확인할 결과 | 해석할 때 주의할 점 |
|---|---|---|
/assets/app.js | 실제 JS 파일의 내용 | 200이어도 HTML이라면 JS를 받은 것이 아님 |
/about 직접 열기 | 앱 HTML을 받은 뒤 About 화면 | 링크 클릭의 성공과 서버에 직접 요청한 성공을 구별 |
/assets/missing.js | 누락된 자산임을 알 수 있는 응답 | 위 와일드카드만 쓰면 HTML 200이 될 수 있으며, 엄격한 처리는 추가 정책 |
/unknown 직접 열기 | 라우터의 찾을 수 없음 화면이나 서버의 404 | SPA의 찾을 수 없음 화면이 보여도 HTTP 상태가 404라는 뜻은 아님 |
이 표는 실제 운영 응답을 이미 확인했다는 보고가 아니라 독자가 해 볼 확인 방법입니다. 정적 호스팅은 회원가입 API나 로컬 json-server를 대신 실행해 주지 않으므로, API를 쓰는 앱이라면 브라우저가 접근할 수 있는 별도의 API 주소와 권한, CORS 조건도 확인합니다.
더 깊이: Netlify가 재작성 규칙을 고르는 순서
Netlify는 규칙을 위에서부터 읽어 처음 맞는 규칙 하나만 적용하고, _redirects 파일의 규칙을 netlify.toml의 규칙보다 언제나 먼저 처리합니다(Netlify 문서의 리디렉션 개요). 그래서 /* /index.html 200을 맨 위에 두면 그 아래의 규칙은 적용될 기회가 없으며, Netlify 문서도 이 SPA 규칙을 보통 마지막에 둔다고 안내합니다. 예를 들어 CORS를 지원하지 않는 API를 /api/* https://api.example.com/:splat 200처럼 Netlify의 CDN을 거쳐 프록시하려면, 이 규칙을 SPA 규칙보다 위에 적어야 합니다.
앞에서 본 “실제 파일을 먼저 준다”는 동작에도 이름과 예외가 있습니다. Netlify는 실제로 있는 URL을 재작성 규칙이 가리지(shadow) 못하게 하므로, /* 규칙이 있어도 실제로 있는 파일의 요청에는 그 파일을 줍니다. 파일이 있어도 반드시 재작성하려면 /app/* /app/index.html 200!처럼 상태 코드 뒤에 느낌표를 붙여 강제할 수 있습니다. 다만 이 형식을 /*에 쓰면 assets의 JavaScript 요청까지 HTML로 바뀌므로, SPA의 기본 규칙에는 쓰지 않습니다.
다른 정적 호스팅 서비스의 배포 (간략)
다른 주요 정적 호스팅 서비스도 Netlify와 비슷한 과정으로 배포합니다. Vercel은 Netlify와 거의 같은 흐름으로 Git 저장소 연동과 자동 배포를 지원하며, vercel CLI로 명령줄에서도 배포할 수 있고, Vite 프로젝트는 빌드 명령과 출력 폴더 dist를 프레임워크 설정으로 감지합니다.
GitHub Pages는 dist 폴더의 내용을 배포용 브랜치(gh-pages 등)나 GitHub Actions로 올리는 방식이 있습니다. Vite 문서의 정적 배포 안내는 GitHub Actions 워크플로를 예로 들며, 여기서는 gh-pages 패키지로 gh-pages 브랜치에 올리는 방법을 봅니다. 먼저 프로젝트 폴더에서 npm install --save-dev gh-pages로 패키지를 설치하면 devDependencies에 gh-pages가 더해지며, 그다음 package.json의 scripts에 predeploy와 deploy 두 줄을 더합니다. 아래 코드는 파일 전체가 아니라 관련된 항목만 보여 주므로, 기존 dependencies와 vite 같은 다른 devDependencies는 지우지 않고 그대로 둡니다.
{
"name": "my-react-app",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
},
"devDependencies": {
"gh-pages": "^6.3.0"
}
}vite.config.js(TypeScript 템플릿이라면 vite.config.ts)에서 base: '/<YOUR_REPO_NAME>/'를 설정한 뒤 npm run deploy를 실행하면, predeploy가 먼저 빌드하고 gh-pages -d dist가 dist의 내용을 gh-pages 브랜치에 올립니다. base는 자산 경로의 설정일 뿐, BrowserRouter의 basename이나 GitHub Pages의 깊은 경로 새로고침 문제까지 해결해 주지는 않습니다.
Firebase Hosting은 Firebase CLI를 설치하고 firebase init으로 프로젝트를 초기화한 뒤 firebase deploy로 배포합니다. firebase.json 설정 파일에서 public 디렉터리를 dist로, rewrites 규칙의 대상을 index.html로 두어 SPA 라우팅 문제를 해결합니다. 어느 서비스를 쓰든, 사용자의 URL 요청이 호스트를 거쳐 React Router까지 어떻게 넘어가는지를 함께 이해해야 합니다.
정리
- React SPA의 빌드 결과는 정적 파일의 집합이므로 정적 호스팅 서비스에 배포하기 알맞습니다.
- Netlify에서는 Git 저장소를 연결하고 Build command를
npm run build, Publish directory를dist로 두면, 지정한 브랜치에 푸시할 때마다 자동으로 배포됩니다. - 깊은 앱 경로를 직접 열 수 있게 하려면
public/_redirects에/* /index.html 200같은 재작성 규칙을 둡니다. - 이 한 줄의 규칙은 누락된 자산이나 앱 밖의 경로도 HTML 200으로 응답할 수 있으므로, 엄격한 404가 필요하면 호스트 규칙을 더 구성합니다.
- Vite의
base는 자산 URL만 조정하며, 라우터의basename과 호스트의 재작성은 따로 설정합니다.
연습 문제
1. 하위 경로 배포
사이트를 https://example.test/learn/ 아래에 배포했고, 자산 app.js가 /learn/assets/app.js에 있다고 가정합니다. HTML이 /assets/app.js를 요청하면 어디를 찾을까요? Vite의 base를 /learn/에 맞추면 BrowserRouter의 basename과 /learn/about 직접 열기도 저절로 해결될까요?
해설 보기
슬래시로 시작하는 /assets/app.js는 사이트의 루트인 https://example.test/assets/app.js를 요청하므로, 의도한 /learn/assets/app.js와 다릅니다. base는 빌드된 자산의 URL을 배포 경로에 맞추는 설정입니다.
BrowserRouter가 앱 경로를 해석하는 기준과 서버의 깊은 경로 재작성은 따로 설정해야 합니다. base만 바꾼 뒤 모두 해결되었다고 판단하지 말고, HTML이 실제로 요청하는 자산과 /learn/about 직접 열기를 각각 확인합니다. 이 문제의 example.test와 파일 이름은 조건을 설명하기 위한 예일 뿐 실제 배포 주소가 아닙니다.
2. 200이지만 JS가 아닌 응답
Netlify에 /* /index.html 200 규칙만 두고 배포했는데, 새 버전을 배포하는 도중에 이전 HTML이 assets/old-abc.js를 요청했고 그 파일은 이미 없습니다. 이 요청의 상태와 내용은 어떻게 될까요? 콘솔에는 어떤 문제가 나타날 수 있을까요?
해설 보기
실제 파일이 없으므로 와일드카드 규칙이 적용되어, 상태는 200이지만 내용은 index.html의 HTML이 됩니다. 브라우저는 JavaScript를 기대했는데 HTML을 받으므로, 모듈 스크립트의 MIME 형식이 맞지 않는다는 오류가 나며 그 화면이 실행되지 않을 수 있습니다.
상태 코드만 보면 성공처럼 보이므로, Network 탭에서 Content-Type과 응답 내용까지 확인해야 합니다. 이런 문제를 줄이려면 이전 청크를 얼마 동안 남길지 정하거나, 자산 경로에는 재작성을 적용하지 않는 호스트 규칙을 따로 구성합니다.
3. 링크로는 되는데 새로고침하면 404
_redirects 파일을 프로젝트 루트에 만들고 배포했더니, 앱 안의 링크로 /about에 가면 잘 보이지만 그 주소에서 새로고침하면 404가 나옵니다. 무엇이 문제일까요?
해설 보기
링크로 이동할 때는 서버에 요청하지 않고 React Router가 브라우저 안에서 화면을 바꾸므로 잘 보입니다. 새로고침하면 브라우저가 서버에 /about을 직접 요청하는데, _redirects가 프로젝트 루트에 있어서 빌드 결과인 dist에 복사되지 않았으므로 Netlify가 규칙을 모르고 404를 돌려줍니다.
_redirects를 public 폴더로 옮기면 빌드할 때 dist의 루트로 복사되어 규칙이 적용됩니다. 배포한 뒤에는 dist에 파일이 들어 있는지와 /about 직접 열기를 다시 확인합니다.
다음 글 CI/CD 기초 개념 소개에서는 지속적 통합과 제공, 배포의 차이를 구분하고, React 앱의 변경을 검증하고 배포하는 과정을 자동화하는 파이프라인을 알아봅니다.