React + Nginx + Spring API에서 401이 발생한 이유
(Vite 로컬 환경과 Nginx Proxy의 차이 정리)
이번에 React(Vite) 프로젝트를 개발 서버에 배포하면서 이상한 현상을 하나 만났다.
로컬에서는 잘 되던 API 호출이 nginx를 통해 배포하니 401이 발생했다ㅜㅜ
처음에는 단순 인증 문제라고 생각했다.
하지만 파고들어 보니, 단순한 문제가 아니었다.
■ 상황 정리
- 프론트엔드: React + Vite
- 로컬 환경: Vite dev server 사용
- 개발 서버: Docker + nginx
- API 서버: 별도 Spring 서버
로컬에서는 /v1/orgChrt 호출이 잘 된다.
하지만 nginx를 거쳐 호출하면 401이 발생한다.
■ 처음에 의심했던 것들
- Authorization 헤더가 안 넘어가나?
- Cookie가 전달되지 않나?
- Host 헤더 mismatch 문제인가?
- vite.config.ts의 changeOrigin: true 때문인가?
■ 결정적 단서: 직접 fetch 해봤을 때
브라우저에서 API를 직접 호출해봤다.
fetch("http://ip:port/api/methodName", {
headers: {
Authorization: "Bearer ..."
}
})
그 결과: No 'Access-Control-Allow-Origin' header is present
=> 이건 인증 문제가 아니라 CORS 문제다.
■ 왜 로컬(Vite)에서는 문제가 없었을까?
여기서 중요한 차이가 있다.
// 로컬 환경 (Vite dev server 사용)
브라우저 → localhost:3000 (Vite)
localhost:3000 → API 서버
→ 브라우저 입장에서는 같은 origin이다. 즉, CORS 검사를 하지 않는다.
// nginx 배포 환경
브라우저 → localhost:3001 (nginx)
브라우저 → API 서버 (직접 요청 시)
→ 이때 브라우저는 Origin 헤더를 보낸다.
Spring 서버가 이를 허용하지 않으면 차단한다. 즉, 401처럼 보였지만 실제 원인은 CORS 정책이었다.
■ 깨달음
-Vite proxy는 개발 편의용 기능이다.
-changeOrigin: true는 Vite 전용 옵션이다.
-nginx는 자동으로 Origin을 바꿔주지 않는다.
-API 서버를 직접 호출하면 CORS가 발생한다.
■ 해결 방안
1. API 서버를 수정할 수 없는 상황이었다.
2. nginx proxy를 통해 CORS를 우회한다. (브라우저 → nginx → API 서버)
두가지 방법이 있지만 나는 2번을 통해서 문제를 해결했다.
최종 nginx.conf 설정
server {
listen 3001;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://ip:port;
proxy_set_header Host ip:port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization $http_authorization;
proxy_set_header Cookie $http_cookie;
proxy_pass_request_headers on;
}
location / {
try_files $uri $uri/ /index.html;
}
}
이렇게 하면 브라우저는 API 서버를 직접 알지 못한다.
CORS는 발생하지 않는다.
■ 추가 궁금증
왜 주소창에서 직접 API 치면 되는데, fetch는 CORS가 걸릴까?
브라우저 주소창에
http://ip:port/api/methodName
를 입력하면 정상 응답이 나온다.
그런데 콘솔에서 fetch로 호출하면 CORS가 뜬다.
왜 그럴까?
핵심 차이: “페이지 이동” vs “JS 요청”
1. 주소창 직접 접근
브라우저는 이를 페이지 이동(navigation) 으로 처리한다.
이 경우에는
- CORS 검사하지 않는다
- 그냥 응답을 화면에 렌더링한다
2. fetch / axios 요청
fetch("http://ip:port/api/methodName")
이거는 아래와 같이 요청하는 것이다.
현재 페이지(origin: http://localhost:3001) → 다른 서버(origin: http://10.0.0.5:8080)
이때 브라우저는 자동으로 다음 헤더를 붙인다.
Origin: http://localhost:3001
그렇게 되면 API 서버가 아래와 같이 받게 됨으로써 응답하지 않으면 브라우저가 차단한다.
Access-Control-Allow-Origin: http://localhost:3001
왜 이런 보안 정책이 존재하는 이유
악성 사이트가 사용자의 인증된 API를 몰래 호출하지 못하게 하기 위함.
예를 들어:
- 사용자가 은행 사이트에 로그인 상태
- 다른 악성 사이트가 JS로 은행 API 호출
- 세션 쿠키로 이체 시도
이걸 막기 위한 것이 CORS다.
■ 결론
- 401이 항상 인증 문제는 아니다.
- 브라우저 콘솔에 CORS 에러가 보이면 구조 문제다.
- Vite proxy와 nginx proxy는 완전히 다르다.
- 주소창 요청과 fetch 요청은 브라우저 동작 방식이 다르다.
- API 서버를 수정 못 하면 nginx proxy로 해결 가능하다.
이번 문제는 단순 해결이 아니라
프론트 → nginx → API 서버 통신 구조를 이해하게 만든 경험이었다.
'React' 카테고리의 다른 글
| [Next.js] force-dynamic를 쓰게 된 이유 (0) | 2025.12.23 |
|---|---|
| Next.js 경로 끝 슬래시(/) (feat. 308 redirect) (0) | 2025.05.30 |
| 전체 페이지가 새로고침 되는 이유 e.preventDefault() (0) | 2025.05.27 |
| [Next.js] 무료 에디터 Tiptap커스텀하기 (0) | 2025.03.26 |
| [TypeScript] 제네릭을 활용한 setDto 최적화 - any 대신 T를 쓰는 이유 (0) | 2025.03.13 |