인증과 Next.js (1): CORS 에러 제대로 이해하기 with Next.js
시리즈 목차: 인증과 Next.js
- (1편) CORS 에러 제대로 이해하기 with Next.js ← 현재 글
- (2편) 쿠키(Cookie) 완전 정복 — Secure, HttpOnly, SameSite의 진짜 의미
- (3편) Axios + 쿠키 인증 — withCredentials와 CORS의 콜라보
- (4편) BFF 패턴 & Proxy Handler — Next.js를 API 게이트웨이로

(1) CORS 에러 제대로 이해하기 with Next.js
"Access to XMLHttpRequest at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy."
모든 프론트엔드 개발자가 한 번쯤 마주치는 그 에러. 백엔드, 더 나아가 풀스택 개발 중에도 많이 마주친다.
하지만 구글에서 찾은 대로 헤더 하나 추가해서 해결했지만, 정작 왜 이런 일이 생기는지 는 모르고 넘어갈 수 있다.
이제 그 "왜"를 알아볼 시간입니다.
CORS 에러의 뿌리에는 브라우저가 강제하는 **Same-Origin Policy(동일 출처 정책)**가 있고,
그걸 이해하면 Next.js에서 기본적인 axios 사용법 부터, 협업 시 백엔드와 어떻게 소통해야 할지 , 더 나아가 직접 어떻게 구현해야 하는지 자연스럽게 이해할 수 있을 것입니다.
이제 하나씩 알아보도록 합시다.
1. Next.js의 두 가지 실행 환경
CORS를 이해하려면 먼저 Next.js가 어디서 코드를 실행하는지 구분해야 합니다.
(참고: Next가 없는 순수 React는 단순히 클라이언트에 모든 자바스크립트 번들을 가져와 코드를 실행하죠)
mermaid
이렇게 Next.js 기반 서비스는 코드 실행 환경이 두 곳입니다. 서버 환경은 배포된 클라우드 서버 내부에 있을 것이고, 클라이언트 환경은 브라우저에 있을 것입니다.
그리고 이 두 환경의 차이가 CORS 문제의 핵심입니다.
- 서버 환경: Node.js에서 직접 외부 API를 호출한다. 브라우저가 없으므로 CORS 정책이 적용되지 않습니다.(우회)
- 클라이언트 환경: 브라우저에서 외부 API를 호출한다. 브라우저가 SOP를 강제하므로 CORS 정책이 적용됩니다.
즉, "use client" 가 붙은 Client Component에서 axios로 외부 API를 직접 호출하는 순간,
우리는 CORS 정책의 세계로 들어오게 된다고 할 수 있습니다.
핵심 요약 CORS는 브라우저가 강제하는 정책이다. 서버 → 서버 요청에는 CORS가 존재하지 않는다.
그래서 프록시 서버 '우회'라는 말이 나오지 않았을까..
1-2. Next.js의 환경변수 설정(서버 vs 클라이언트)
Next.js 기반 서비스는 두 가지 코드 실행 환경이 있다고 했었죠? (서버 vs 클라이언트)
Next.js 환경변수를 설정할 때는 이를 잘 생각해야 합니다. (제가 경험함..)
Next.js의 환경변수는 기본적으로 서버에서만 접근 가능합니다.
하지만 NEXT_PUBLIC_ prefix가 붙은 환경변수만 빌드 시 클라이언트 번들에 포함됩니다.
mermaid
때문에, 환경변수를 .env 파일 내에 생성할 때 무작정 NEXT_PUBLIC_ prefix를 붙이지 말고 '이 환경변수가 어디에 쓰일 건지'에 대해 충분히 고민해보아야겠죠?
외부 API URL을 클라이언트 컴포넌트에서 직접 호출해야 한다면 NEXT_PUBLIC_이 필요하지만,
뒤에서 살펴볼 BFF 패턴을 사용하면 이 URL을 클라이언트에 노출하지 않아도 됩니다.
더 나아가 생각해보면 보안 상으로 꼭 숨겨야 하는 환경변수는 서버 측에 코드를 위치시키고 NEXT_PUBLIC_을 안쓰면 되곘네요.
2. Same-Origin Policy(SOP) — CORS의 뿌리
브라우저는 기본적으로 SOP를 따릅니다. SOP란 무엇일까요?
Same-Origin Policy(SOP, 동일 출처 정책)는 웹 브라우저의 핵심 보안 메커니즘으로, 한 웹사이트(출처)에서 불러온 문서나 스크립트가 다른 출처의 리소스에 접근하거나 상호작용하는 것을 제한하는 규칙입니다.
브라우저는 왜 이런 제약을 만들었을까요?
그 이유를 Origin부터 차근차근 알아봅시다.
2-1. Origin과 Same/Cross-Origin?
Origin = Scheme + Host + Port 의 조합입니다.
mermaid
그리고 Same-Origin은 Origin이 서로 같을 때를 의미하고, **Cross-Origin(교차 출처)**은 두 origin이 다를 때를 의미합니다.
언제 Cross-Origin일지 표로 정리해볼까요?
| URL | Origin | 비교 |
|---|---|---|
https://api.example.com | https://api.example.com | - 비교군 |
https://www.example.com | https://www.example.com | ❌ host 다름 |
http://api.example.com | http://api.example.com | ❌ scheme(프로토콜, http or https) 다름 |
https://api.example.com:8080 | https://api.example.com:8080 | ❌ port 다름 |
보시다시피 셋 중 하나라도 다르면 Cross-Origin 입니다.
예시로, 흔히 로컬 개발 환경에서 localhost:3000에서 localhost:8080을 호출해도 Cross-Origin입니다. 포트가 다르기 때문이죠.
2-2. SOP가 없다면 어떤 일이 생길까?
위에서 언급했다시피 브라우저는 기본적으로 SOP를 따른다고 했습니다.
왜 그런지, SOP가 만약 없다면 어떤 위험이 있을지 알아볼까요?
mermaid
위 다이어그램을 보면 evil.com이 나 대신 bank.com에 요청을 보내고 있죠?
SOP가 없다면, 공격자는 단순히 요청을 보내는 것에 그치지 않고, 서버가 돌려준 나의 개인정보나 인증 토큰 등을 자바스크립트로 가로채서 읽을 수 있게 됩니다. 내 브라우저 안에서 내 정보를 자유롭게 훔쳐가는 '스파이'가 생기는 셈이죠.
SOP는 바로 이 지점에서 다른 출처(evil.com)의 스크립트가 내 응답 데이터(bank.com)에 접근하는 것을 원천 봉쇄합니다.
SOP는 이런 시나리오를 막기 위해 존재한다고 보면 됩니다. 브라우저가 다른 Origin의 응답을 JavaScript에 노출시키지 않음으로써, 악성 스크립트가 다른 사이트의 데이터를 훔쳐가거나 사용자 대신 요청을 보내는 것을 방지합니다.
결국 SOP는 공격을 막기 위한 브라우저 단의 안전장치입니다.
서버 입장에서는 요청을 거부하는 게 아니라, 브라우저가 응답 결과를 JavaScript에 전달하는 것을 막는 것입니다. 이 차이를 이해하는 것이 중요합니다.
중요: SOP는 "요청"이 아니라 "응답"을 막는다
브라우저는 서버로 HTTP 요청을 정상적으로 보낸다. 서버도 요청을 처리하고 응답을 되돌려준다. 하지만 브라우저가 이 응답을 검사했을 때 CORS 정책을 위반한다고 판단하면, 그 응답 내용을 JavaScript 코드(
axios.then,fetch().then)에게 전달하지 않고 CORS 에러로 막는다. 즉, 데이터는 이미 서버에서 넘어왔지만 브라우저 문턱(SOP)에서 막힌 것이다.특히
POST,PUT,DELETE처럼 서버 상태를 변경하는 요청은 잘못된 Origin에서 무분별하게 호출되지 않도록Preflight(OPTIONS)단계에서 한 번 더 검증을 거치게 된다.
3. CORS(Cross-Origin Resource Sharing) — SOP의 예외를 허용하는 메커니즘
일단 Cross-Origin 뭔지, SOP가 왜 Cross-Origin 간의 요청/응답 시나리오를 막는지는 다들 이해하셨죠?
그런데 실제 웹 서비스에서는 다른 Origin과의 통신이 필요한 경우가 많습니다.
프론트엔드 서버(frontend.com)가 백엔드 서버(api.backend.com)를 호출하는 건 아주 흔한 패턴이죠.
이 패턴을 쓰기 위한 메커니즘이 CORS(Cross-Origin Resource Sharing) 입니다. 서버가 특정 헤더를 응답에 포함시켜 "이 Origin에서의 요청은 허용한다"고 명시적으로 선언하는 방식이죠. 다르게 설명하면 특정 Origin에 대해서만 SOP 보호를 벗어날 '문'을 제공해준다고 볼 수 있습니다.
서버에게 주도권이 있다: CORS 에러는 프론트엔드에서 나지만, 해결의 열쇠(헤더)는 서버가 쥐고 있다.
3-1. CORS 핵심: 서버 응답 헤더 검사
서버 응답 헤더:
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
브라우저는 응답을 받으면 이 헤더를 검사합니다.
현재 페이지의 Origin이 Access-Control-Allow-Origin에 포함되어 있으면
응답 데이터를 JavaScript에 전달하고, 포함되어 있지 않으면 응답을 차단하고 콘솔에 CORS 에러를 출력합니다.
3-2. 요청: Simple Request vs Preflight Request
응답에서 어떻게 CORS를 확인하는지는 확인했습니다. 이제 CORS 요청에 대해 알아봅시다.
CORS 요청에는 두 종류가 있습니다.
1) Simple Request (단순 요청) 아래 조건을 모두 만족하면 바로 요청을 보내고 응답 헤더만 확인합니다.
- Method:
GET,POST,HEAD중 하나 Content-Type:application/x-www-form-urlencoded,multipart/form-data,text/plain중 하나- 커스텀 헤더 없음
여기서 "커스텀 헤더"란 개발자가 직접 추가하는 인증 헤더나 특수 헤더뿐 아니라, 사양에서 허용한 일부 헤더(예:
Accept,Accept-Language,Content-Language)를 제외한 나머지 대부분의 추가 헤더를 의미한다. 예를 들어Authorization,X-Request-Id같은 헤더를 붙이는 순간 이 요청은 Simple Request가 아니게 되고, Preflight(OPTIONS) 가 발생한다.
2) Preflight Request (사전 요청)
위 조건을 벗어나면 실제 요청을 보내기 전에 브라우저가 OPTIONS 요청을 먼저 보냅니다.
mermaid
자주 놓치는 부분 axios로
application/json형태의 POST 요청을 보내면Content-Type: application/json이 자동으로 설정되는데, 이 Content-Type은 Simple Request 조건에 해당하지 않는다. 따라서 axios로 JSON POST를 보내면 무조건 Preflight가 발생한다.
이걸 모르면 "네트워크 탭에 OPTIONS 요청이 왜 두 번씩 보내지지?"라고 당황할 수 있습니다. 하지만 그건 브라우저가 정상적으로 동작하고 있는 겁니다.
팁: Preflight(OPTIONS)도 캐시할 수 있다
매 요청마다
OPTIONSPreflight가 발생하면 네트워크 비용이 쌓인다. 서버에서Access-Control-Max-Age헤더를 설정해두면, 브라우저가 Preflight 응답을 일정 시간 동안 캐시해서 같은 Origin/메서드/헤더 조합에 대해 매번 OPTIONS를 보내지 않아도 된다.예:
Access-Control-Max-Age: 600→ 10분간 Preflight 결과 재사용
4. 대표적인 CORS 에러 해결 방법 3가지
지금까지 정리했던 내용을 바탕으로, CORS 에러를 마주쳤을 때 해결할 수 있는 방법 3가지를 Next.js 환경임을 조합하여 살펴보겠습니다.
4-1. 백엔드 서버에서 CORS 헤더 설정
가장 직접적인 해결책입니다. 직접 API 서버를 제어할 수 있다면 아래와 같이 수정하세요.
혹시, 팀프로젝트 시에는 백엔드 팀원에게 CORS 헤더를 설정했는지 확인해보고, 설정되어 있지 않다면 아래와 같이 설정해달라고 요청하세요.
협업 시 CS 지식의 필요성이다.
# 서버 응답 헤더
Access-Control-Allow-Origin: https://my-frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true # 쿠키 포함 시 필요
주의(서버측): withCredentials +
Access-Control-Allow-Origin: *는 함께 쓸 수 없다브라우저에서
withCredentials: true(또는credentials: "include")로 요청을 보내면 서버는Access-Control-Allow-Credentials: true와 함께 구체적인 Origin 값 만을Access-Control-Allow-Origin에 설정해야 한다.
- ✅ 올바른 예:
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Credentials: true- ❌ 잘못된 예:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true표준상 와일드카드(
*)는 Credential(쿠키, Authorization 헤더 등)과 함께 사용할 수 없다. 실무에서 CORS가 "가끔은 되고, 가끔은 안 되는" 미묘한 버그를 만들 수 있는 부분이라 꼭 주의해야 한다.
4-2. Next.js Route Handler로 프록시
Next.js 기반 프로젝트일 때 사용되는 방법 중 하나입니다.
브라우저 → Next.js 서버는 Same-Origin이므로 CORS가 없고,
Next.js 서버 → 외부 API는 서버 간 통신이라 역시 CORS가 없게 되므로 우회가 가능해집니다.
백엔드에서 CORS 에러를 헤더로 열어두지 않아도 에러가 발생하지 않겠네요.
mermaid
Next.js app router 환경에서 다음과 같이 작성하면 우회할 proxy route가 생성할 수 있습니다. 클라이언트에서 proxy route로 요청하면 됩니다.
ts// app/api/proxy/route.ts export async function GET() { const res = await fetch("https://external-api.com/data", { headers: { "API-Key": process.env.API_SECRET! }, }); const data = await res.json(); return Response.json(data); }
정리하자면, 브라우저는 https://my-app.com/api/proxy를 호출하는 것이므로 Same-Origin. 외부 API 키도 서버에만 존재(NEXT_PUBLIC_ 없음)해서 클라이언트에 노출되지 않습니다.
이것이 BFF 패턴의 기본 원리입니다. (4편에서 자세히 다룸)
4-3. next.config.js rewrites
이 또한 클라이언트가 Next.js 기반 프로젝트일 때 사용 가능한 방법 중 하나로, route handler 방식보다 제한적이지만 구현은 간단합니다. next.config.ts에서 rewrites로 간단한 정적 프록시 설정을 하면 됩니다.
실무에서는 도메인이 다른 API 서버를 마치 Same-Origin 내부 API처럼 "속이기" 위한 꼼수이자 기술로 자주 쓰입니다. 다만 요청/응답을 가공하거나 인증 정보를 주입할 수 없다는 한계가 있다.
ts// next.config.ts const nextConfig = { async rewrites() { return [ { source: "/api/:path*", destination: "https://external-api.com/:path*", }, ]; }, };
결과는 다음과 같습니다. 브라우저에서 /api/users를 호출하면 내부적으로 https://external-api.com/users로 연결됩니다.
mermaid
4-4. CORS 에러 해결 방법 정리
3가지 방식을 정리하면 다음과 같습니다.
| 방법 | 구현 복잡도 | 유연성 | 적합한 상황 |
|---|---|---|---|
| 백엔드 CORS 헤더 | 낮음 | — | API 서버를 직접 제어하거나 요청할 수 있을 때 |
| Route Handler 프록시 | 중간 | 높음 | 인증 주입, 데이터 가공 필요 시 |
| rewrites 설정 | 낮음 | 낮음 | 단순 URL 포워딩 |
5. 정리
이 글에서 다룬 내용을 정리하면 다음과 같습니다
- CORS는 브라우저의 Same-Origin Policy(SOP)에서 비롯됩니다. 서버-서버 통신에는 CORS가 없습니다.
- SOP는 악의적인 사이트가 다른 사이트의 리소스에 접근하는 것을 막기 위해 존재합니다. 공격이 아닌 보안 메커니즘입니다.
- CORS는 서버가 "이 Origin은 허용한다"고 선언하는 메커니즘입니다. 서버가 특정 헤더를 응답에 포함시켜야 합니다.
- CORS 해결의 핵심은 요청 경로 설계이며. 서버를 통한 프록시로 대부분의 CORS 문제를 우회할 수 있다. 이는 Next.js에서 가능한 부분이다.
다음 2편에서는 인증 상태를 유지하는 핵심 메커니즘인 쿠키를 깊게 알아보도록 하겠습니다.
HttpOnly, Secure, SameSite 같은 옵션들이 왜 존재하는지,
각각 어떤 공격을 막기 위한 것인지를 정리해볼게요.
아직 댓글이 없습니다
첫 번째 댓글을 작성해보세요!