1. "로컬에서는 잘 되는데요?"
프론트엔드 개발자가 흔히 겪는 공포의 순간이 있습니다.
npm run dev로 로컬에서 개발할 때는 아무 문제가 없습니다. 메뉴를 클릭해도 페이지가 잘 바뀌고, 뒤로 가기도 잘 되고, 특정 페이지(/about)에서 새로고침을 해도 아주 잘 뜹니다.
"완벽해!" 하고 빌드를 합니다(npm run build).
그리고 결과물(dist 폴더)을 AWS S3나 Nginx 서버에 올립니다.
접속해보니 메인 페이지(index.html)는 잘 나옵니다.
그런데 상단 메뉴를 눌러서 /mypage로 이동한 뒤, 새로고침(F5)을 누르는 순간...
404 Not Found 하얀 화면에 차가운 에러 메시지만 덩그러니 뜹니다.
이건 버그가 아닙니다. SPA(Single Page Application)의 작동 원리를 이해하지 못해서 생긴 서버 설정(Server Configuration) 문제입니다. 코드를 100번 수정해도 해결되지 않습니다. 범인은 코드가 아니라 서버 설정이니까요.
2. 범인은 "가짜 주소" (Client-Side Routing)
이 현상을 이해하려면 전통적인 웹(MPA)과 현대적인 웹(SPA)의 라우팅 차이를 알아야 합니다.
2.1. 전통적인 방식 (Server-Side Routing)
과거의 웹사이트는 파일 기반이었습니다. PHP, JSP, ASP가 이에 해당합니다.
example.com/about요청 -> 서버 하드 디스크에 있는about.html또는about.php파일을 찾아서 줍니다.example.com/contact요청 ->contact.html파일을 찾아서 줍니다.- 파일이 없으면? 당연히 404 Not Found입니다.
2.2. SPA 방식 (Client-Side Routing)
React, Vue, Angular는 Single Page, 즉 페이지가 index.html 하나뿐입니다.
- 사용자가
example.com에 접속 -> 서버는index.html과bundle.js를 줍니다. - 사용자가 메뉴에서
/about을 클릭 -> 브라우저의 주소창만 자바스크립트(history.pushState)로 바꿉니다. - 서버에 요청을 보내지 않습니다. 그리고 자바스크립트가 화면을 다시 그립니다.
- 이때까지 서버는 아무것도 모릅니다. 사용자가
/about에 있는지/contact에 있는지 알 길이 없습니다.
2.3. 문제는 "새로고침"
사용자가 /about 페이지를 보고 있다가 새로고침을 누릅니다.
새로고침은 "브라우저야, 지금 주소창에 있는 example.com/about 페이지를 서버에서 다시 받아와!"라는 명령입니다.
요청을 받은 서버(Nginx, S3 등)는 당황합니다.
"어? 내 하드 디스크에는 index.html밖에 없는데? about이라는 파일이나 폴더는 눈 씻고 찾아봐도 없는데?"
그래서 정직하게 404 Not Found를 돌려주는 것입니다.
3. 해결책 - "모로 가도 서울만 가면 된다" (Fallback)
해결 방법은 간단합니다. 서버에게 거짓말을 시키는 것입니다.
"사용자가 어떤 주소(/about, /contact, /user/123)를 요청하든 간에, 파일이 없으면 404 에러 대신 무조건 index.html을 내려줘!"
그러면 브라우저는 일단 index.html을 받고, 그 안에 포함된 React/Vue 자바스크립트가 실행됩니다.
자바스크립트 라우터(React Router, Vue Router)가 깨어나서 주소창(window.location.pathname)을 확인합니다.
"어? 현재 주소가 /about이네? 그렇다면 나는 About 컴포넌트를 렌더링해야겠다!"
이렇게 해서 정상적으로 화면이 뜨게 되는 것입니다.
이것을 서버 설정 용어로 Rewrite 또는 Fallback이라고 합니다.
4. 서버별 설정 가이드 (Best Practice)
여러분이 쓰는 배포 환경에 딱 맞는 설정을 복사해서 쓰세요.
4.1. Nginx
실제로 가장 많이 쓰는 웹 서버입니다. nginx.conf 또는 default.conf의 location / 블록을 수정합니다.
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html; # 빌드 파일 경로
index index.html;
location / {
# 1. 파일($uri)이 있으면 그걸 줌 (이미지, JS, CSS 등)
# 2. 없으면 폴더($uri/)를 찾아봄
# 3. 그래도 없으면 /index.html을 줌 (Fallback)
try_files $uri $uri/ /index.html;
}
}
try_files $uri $uri/ /index.html; 이 한 줄이 핵심입니다.
4.2. AWS S3 + CloudFront
S3만 쓸 때와 CloudFront(CDN)를 붙일 때가 다릅니다. (HTTPS 때문에 보통 CloudFront를 같이 씁니다).
-
S3 정적 웹 사이트 호스팅:
- S3 버킷 -> 속성 -> 정적 웹 사이트 호스팅 편집
- 오류 문서(Error Document):
index.html로 설정합니다. - (404가 200 OK로 바뀌면서
index.html이 나갑니다.)
-
CloudFront:
- CloudFront 콘솔 -> 배포(Distribution) 선택 -> 오류 페이지(Error Pages) 탭
- 사용자 정의 오류 응답 생성(Create Custom Error Response)
- HTTP 오류 코드: 403 Forbidden (S3는 파일 없으면 403을 줍니다) 또는 404 Not Found
- 오류 캐싱 최소 TTL: 0 (권장)
- 응답 사용자 정의: 예(Yes)
- 응답 페이지 경로:
/index.html - HTTP 응답 코드: 200 OK
4.3. GitHub Pages
GitHub Pages는 기본적으로 SPA 리다이렉트를 지원하지 않습니다. 그래서 꼼수(Hack)를 써야 합니다.
- 프로젝트 루트(public 폴더)에
404.html파일을 만듭니다. index.html의 내용을 그대로 복사해서404.html에 붙여넣습니다.- 그러면 404 에러가 날 때
404.html(=index.html)이 실행되면서 React/Vue가 구동됩니다.
4.4. Netlify / Vercel
이 서비스들은 SPA를 위해 태어났기 때문에 설정이 매우 쉽습니다.
- Netlify:
public/_redirects파일 생성/* /index.html 200 - Vercel:
vercel.json설정{ "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] }
4.5. Apache
프로젝트 루트 폴더에 .htaccess 파일을 만들고 다음 내용을 붙여넣습니다.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
5. Hash Router: 설정하기 귀찮을 때의 꼼수
서버 설정을 건드릴 권한이 없거나 너무 귀찮다면, Hash Router를 쓰는 방법도 있습니다.
주소에 샵(#)을 붙이는 방식입니다.
example.com/#/about
브라우저는 # 뒤에 오는 부분(Fragment)은 서버에 보내지 않습니다.
즉, 사용자가 example.com/#/about을 요청해도 서버는 example.com만 봅니다.
그래서 무조건 index.html을 받아오게 됩니다.
하지만 URL이 못생겨지고, 검색 엔진 최적화(SEO)에 불리하기 때문에 실제로는 거의 쓰지 않습니다. (관리자 페이지 같은 내부용 툴에는 괜찮습니다).
6. Next.js (SSR)에서는 왜 이런 일이 없나요?
Next.js나 Nuxt.js 같은 Server-Side Rendering (SSR) 프레임워크를 쓰면 이 고생을 안 해도 됩니다.
SSR은 서버(Node.js)가 돌아가고 있습니다.
사용자가 /about을 요청하면, Node.js 서버가 받아서 "아, About 페이지 달라는 거구나" 하고 그 자리에서 HTML을 만들어서(Render) 줍니다.
즉, 모든 URL에 대해 진짜 파일(HTML)을 주는 것과 같은 효과를 냅니다.
하지만 정적 사이트(Static Site)로 배포하는 경우(Next.js의 output: export)에는 React와 똑같은 404 문제가 발생하므로, 위에서 설명한 try_files 설정이 필요합니다.
7. 요약
배포 후 404 에러는 여러분의 코드가 잘못된 게 아닙니다. "없는 파일을 요청하면 index.html을 줘라"라는 규칙을 서버에 알려주지 않았을 뿐입니다.
- 원인: SPA는 가짜 주소를 쓰는데, 서버는 진짜 파일만 찾으려고 해서.
- 해결: 모든 404 요청을
index.html로 돌려보내는 Fallback 설정 추가. - 도구별: Nginx(
try_files), S3(Error Document), Apache(.htaccess).
이제 자신 있게 배포하세요!