사이트픽 블로그
← 목록으로

사이트 접근 주소 변경 (한 도메인에 4개 SPA 경로 재구성)

URL 경로 재구성: 한 도메인에 4개 SPA 깔끔하게 붙이기 (2026-07-07)

개요

한 도메인(mforet.kr) 아래에 브랜드/관리자/학원/수강생 4개의 React SPA를 서비스하는데, 기존 URL이 /piano, /piano-admin, /piano-academy, /piano-student 처럼 piano- 접두어가 붙어 지저분했다. 이걸 아래처럼 접두어 없는 깔끔한 경로로 재구성했다.

URL 재구성 매핑표

앱vite basereact-router basename디스크 경로접속 URL
브랜드 (brand)///var/www/piano/https://mforet.kr/
관리자 (admin)/admin//admin/var/www/piano/admin/https://mforet.kr/admin
학원 (academy)/academy//academy/var/www/piano/academy/https://mforet.kr/academy
수강생 (student)/student//student/var/www/piano/student/https://mforet.kr/student

핵심은 세 곳의 경로가 서로 일치해야 한다는 것:

  1. vite base — 빌드 결과물의 JS/CSS 에셋 참조 경로 (<script src="/academy/assets/...">)
  2. react-router basename — SPA 내부 라우팅 기준 경로
  3. nginx location + try_files — 서버가 파일을 찾고 SPA fallback을 주는 경로

이 셋 중 하나라도 어긋나면 → 흰 화면(에셋 404)이거나 라우팅이 깨진다.

왜 세 곳을 다 바꿔야 하나

1) vite base

base는 빌드된 index.html이 에셋을 참조하는 접두어다.

// academy/vite.config.ts
export default defineConfig({
  base: '/academy/',   // <script src="/academy/assets/index-xxx.js">
  ...
});

base가 틀리면 브라우저가 엉뚱한 경로에서 JS를 찾아 404 → 흰 화면.

2) react-router basename

SPA 내부 링크(<Link to="/login">)를 실제 URL(/academy/login)로 매핑하는 기준.

<Router basename="/academy">
  ...
</Router>

브랜드는 루트에 있으므로 basename="/".

3) nginx SPA 라우팅

# 하위 앱을 루트(브랜드)보다 먼저 매칭시켜야 한다
location /admin   { try_files $uri $uri/ /admin/index.html; }
location /academy { try_files $uri $uri/ /academy/index.html; }
location /student { try_files $uri $uri/ /student/index.html; }

# 브랜드 = 루트 SPA (나머지 전부)
location / { try_files $uri $uri/ /index.html; }

try_files의 마지막 인자를 /{app}/index.html로 줘야 딥링크(/academy/login 직접 접속·새로고침)에서 SPA fallback이 동작한다.

놓치기 쉬운 함정: index.html의 하드코딩된 favicon 경로

vite base는 번들 에셋(JS/CSS) 경로만 자동으로 바꿔준다. 하지만 index.html에 직접 써둔 정적 링크는 그대로 남는다.

<!-- 빌드해도 안 바뀜 → 옛 경로 그대로 -->
<link rel="icon" href="/piano-academy/favicon.ico" />

결과적으로 JS는 신규 경로(/academy/...)인데 favicon만 옛 경로(/piano-academy/...)를 가리키는 짬뽕 상태가 됐다. 각 앱 index.html의 favicon/apple-touch-icon 경로를 직접 신규 경로로 고쳐야 한다.

<link rel="icon" href="/academy/favicon.ico" />

기존 경로 호환: 301 리다이렉트

북마크·외부 링크가 깨지지 않게 옛 경로는 신규 경로로 301 영구 리다이렉트로 넘겼다.

location = /piano/       { return 301 /; }
location = /piano        { return 301 /; }
location /piano-admin    { return 301 /admin; }
location /piano-academy  { return 301 /academy; }
location /piano-student  { return 301 /student; }

트러블슈팅: nginx 설정 교체하다 HTTPS가 통째로 날아감

배포 중 nginx 설정 파일 전송이 불완전하게 끝나면서, 활성 설정에서 listen 443 ssl 블록(SSL 인증서 포함)이 사라진 사고가 있었다. nginx -t는 통과하는데(문법은 맞으니까) 443 리스너가 없어 HTTPS 접속이 전부 실패(curl 000) 하는 상황.

  • 증상: ss -tlnp에 nginx가 80 포트에만 떠 있음. 443 없음.
  • 원인: 새 설정 파일 업로드가 truncate되어 SSL 서버 블록이 유실.
  • 복구: 교체 직전 만들어 둔 백업(piano.conf.bak-prepath-*)에서 SSL 블록이 온전한 설정을 즉시 복원 → reload → HTTPS 복구.
  • 교훈:
    1. nginx 설정 교체 전 반드시 타임스탬프 백업을 남긴다.
    2. nginx -t 통과 ≠ 정상. reload 후 ss -tlnp로 443 리스너와 실제 HTTPS 응답까지 확인한다.
    3. 설정 파일 전송 후 wc -l / grep 'listen 443'으로 무결성 검증하고 나서 교체한다.

검증

curl -sL -o /dev/null -w "%{http_code}" https://mforet.kr/          # 200 (브랜드)
curl -sL -o /dev/null -w "%{http_code}" https://mforet.kr/admin     # 200
curl -sL -o /dev/null -w "%{http_code}" https://mforet.kr/academy   # 200
curl -sL -o /dev/null -w "%{http_code}" https://mforet.kr/student   # 200
curl -s -o /dev/null -w "%{http_code}" https://mforet.kr/academy/assets/index-xxx.js  # 200 (에셋)
curl -s -o /dev/null -w "%{http_code}" https://mforet.kr/academy/login                # 200 (SPA 딥링크 fallback)
curl -s -o /dev/null -w "%{redirect_url}" https://mforet.kr/piano-academy              # /academy (301)

결론 / 배운 점

  • 한 도메인 멀티 SPA에서 URL 경로를 바꿀 땐 vite base · router basename · nginx location 세 곳을 세트로 맞춰야 한다.
  • vite base가 안 건드리는 **index.html의 하드코딩 정적 링크(favicon 등)**를 잊지 말자.
  • nginx 설정 교체는 백업 → 전송 무결성 검증 → 교체 → reload → 실제 리스너/응답 확인의 안전 절차를 지키자. nginx -t 통과를 정상으로 착각하면 큰일 난다.

댓글 0

  • 첫 번째 댓글을 남겨보세요.