React, Vue, Angular와 같은 SPA(Single Page Application)를 빌드하여 Nginx 웹 서버에 배포한 후 가장 빈번하게 마주치는 문제가 있습니다. 바로 루트(/)에서는 페이지 이동이 잘 되는데, 특정 하위 경로(예: /mypage, /dashboard)에서 새로고침(F5)을 누르거나 주소를 직접 입력하면 404 Not Found 오류가 발생하는 현상입니다.

이 글에서는 이 문제가 발생하는 근본적인 원인과 Nginx의 try_files 지시어를 활용해 완벽하게 해결하는 방법을 정리합니다.


1. 문제 상황 및 증상

웹 애플리케이션을 빌드한 결과물(정적 파일)을 /var/www/html 또는 별도의 디렉토리에 두고 Nginx 기본 설정으로 서빙하는 경우를 예로 들어보겠습니다.

  • 메인 페이지(https://example.com/) 접속: 정상 동작
  • 메인 화면에서 버튼을 클릭해 /profile 페이지로 이동: 정상 동작 (URL도 바뀜)
  • 그 상태에서 새로고침(F5) 클릭 또는 새 브라우저 창에서 https://example.com/profile 직접 입력: 404 Not Found 오류 발생

2. 발생 원인: CSR(클라이언트 사이드 라우팅)의 동작 원리

이 현상은 Nginx의 버그가 아니라, SPA의 동작 방식과 전통적인 웹 서버의 파일 탐색 방식 간의 차이 때문에 발생합니다.

  1. SPA(React Router, Vue Router)의 라우팅:

    • SPA는 단 하나의 index.html 파일과 JavaScript 번들 파일로 구성됩니다.
    • 브라우저 주소창의 경로(/profile)는 실제로 서버에 존재하는 물리적 파일이나 폴더가 아니라, 브라우저의 JavaScript 라우터가 가상으로 처리하는 경로입니다.
    • 메인 페이지에서 버튼을 클릭해 이동할 때는 브라우저가 서버에 새 요청을 보내지 않고 HTML5 History API(pushState)를 이용해 클라이언트 내부에서 화면을 전환합니다.
  2. Nginx의 기본 동작:

    • 그러나 사용자가 주소창에 직접 https://example.com/profile을 치거나 새로고침을 누르면, 브라우저는 Nginx 웹 서버에 직접 /profile이라는 리소스를 달라고 HTTP GET 요청을 보냅니다.
    • Nginx는 서버의 루트 디렉토리에서 profile이라는 파일이나 디렉토리를 찾으려고 시도합니다.
    • 하지만 서버에는 index.html만 존재하고 profile이라는 물리적 파일은 없기 때문에, Nginx는 당연히 404 Not Found를 응답하게 됩니다.

3. 해결 방법: try_files 지시어 설정

이 문제를 해결하려면 Nginx에게 다음과 같이 명령해야 합니다:

“요청한 경로에 실제 파일이나 디렉토리가 있다면 그것을 서빙하고, 만약 없다면 무조건 index.html을 반환해서 브라우저의 프론트엔드 라우터가 처리하게 해라!”

이를 가능하게 해주는 것이 바로 Nginx의 try_files 지시어입니다.

3.1 기본 Nginx 설정 (nginx.conf 또는 가상 호스트 파일)

서버 블록(server) 내부의 location / 블록을 다음과 같이 수정합니다.

server {
    listen 80;
    server_name example.com;

    root /var/www/html;
    index index.html;

    location / {
        # 1) 요청한 파일($uri)이 존재하는지 확인
        # 2) 요청한 디렉토리($uri/)가 존재하는지 확인
        # 3) 둘 다 없으면 마지막 대체재로 /index.html을 반환!
        try_files $uri $uri/ /index.html;
    }

    # 정적 에셋 캐싱 설정 (선택 사항)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, no-transform";
    }
}

3.2 try_files $uri $uri/ /index.html; 동작 흐름

  1. $uri: 예를 들어 https://example.com/main.js 요청이 오면 실제 파일인 main.js를 바로 전달합니다.
  2. $uri/: 디렉토리 요청이 오면 해당 디렉토리의 index.html을 확인합니다.
  3. /index.html: 만약 https://example.com/profile 요청이 들어와서 $uri도 없고 $uri/도 없다면, 404를 내는 대신 **루트의 /index.html을 클라이언트에게 정상 응답(HTTP 200)**으로 내려줍니다.
  4. 브라우저는 index.html과 JS 번들을 로드한 뒤, 현재 주소창의 /profile 경로를 읽어 알맞은 React/Vue 컴포넌트를 화면에 렌더링합니다.

4. 서브 경로(Sub-path)에 SPA가 배포된 경우

만약 메인 도메인의 하위 경로(예: https://example.com/admin/)에 별도의 SPA 앱이 배포되어 있다면 다음과 같이 설정해야 합니다.

location /admin/ {
    alias /var/www/admin-app/;
    index index.html;
    try_files $uri $uri/ /admin/index.html;
}

5. 설정 적용 및 테스트

설정 파일을 수정한 후에는 반드시 Nginx 설정 문법을 검사하고 서비스를 재로드해야 안전합니다.

# 1. 설정 파일 문법 검사
sudo nginx -t

# 2. 검사 결과(syntax is ok) 확인 후 무중단 재로드
sudo systemctl reload nginx

6. 요약

  • SPA의 새로고침 404 에러는 물리적 파일이 없는 가상 경로를 Nginx가 서버에서 찾으려고 하기 때문에 발생합니다.
  • location / 내에 try_files $uri $uri/ /index.html; 구문을 추가하면 해결됩니다.
  • 이 설정을 통해 새로고침이나 직접 URL 접속 시에도 항상 index.html이 전달되어 프론트엔드 클라이언트 라우터가 정상 동작하게 됩니다.