사이트 접근 주소 변경 (한 도메인에 4개 SPA 경로 재구성)
URL 경로 재구성: 한 도메인에 4개 SPA 깔끔하게 붙이기 (2026-07-07)
개요
한 도메인(mforet.kr) 아래에 브랜드/관리자/학원/수강생 4개의 React SPA를 서비스하는데,
기존 URL이 /piano, /piano-admin, /piano-academy, /piano-student 처럼 piano- 접두어가 붙어 지저분했다.
이걸 아래처럼 접두어 없는 깔끔한 경로로 재구성했다.
URL 재구성 매핑표
| 앱 | vite base | react-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 |
핵심은 세 곳의 경로가 서로 일치해야 한다는 것:
- vite
base— 빌드 결과물의 JS/CSS 에셋 참조 경로 (<script src="/academy/assets/...">) - react-router
basename— SPA 내부 라우팅 기준 경로 - 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 복구. - 교훈:
- nginx 설정 교체 전 반드시 타임스탬프 백업을 남긴다.
nginx -t통과 ≠ 정상. reload 후ss -tlnp로 443 리스너와 실제 HTTPS 응답까지 확인한다.- 설정 파일 전송 후
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
- 첫 번째 댓글을 남겨보세요.