본문으로 건너뛰기

SOP와 CORS: 브라우저의 교차 출처 접근 제어

프론트엔드에서 API를 연결하다 보면 서버는 정상인데 데이터를 가져오지 못하는 순간이 있다. Postman에서는 응답이 잘 오고, 서버 로그에도 문제가 없다. 그런데 브라우저 콘솔에는 CORS 오류가 표시된다.

const response = await fetch('https://api.example.com/products/1');
const product = await response.json();

코드만 보면 평범한 상품 조회 요청이다. 하지만 이 코드가 실행되는 페이지의 주소가 https://app.example.com이라면 브라우저는 다른 출처의 API에 접근하는 요청으로 처리한다. 이때는 서버가 응답하는 것만으로 충분하지 않다. 브라우저가 그 응답을 JavaScript에 공개해도 되는지까지 확인해야 한다.

SOP와 CORS를 이해하려면 이 차이에서 출발하는 것이 좋다. 상품을 조회하고, 등록하고, 로그인한 사용자의 정보를 가져오는 흐름을 따라가며 브라우저와 서버가 각각 무엇을 확인하는지 살펴보자.

출처(Origin)의 정의와 동일 출처 판단 기준​

일반적인 HTTP(S) URL에서 브라우저는 두 URL의 스킴, 호스트, 포트가 모두 같을 때 같은 출처라고 판단한다. 이 세 요소의 조합을 출처(Origin) 라고 한다. 경로나 쿼리 문자열은 출처 판단에 포함되지 않는다.

가령 현재 페이지가 https://app.example.com/products라면 다음과 같이 구분된다.

요청 주소출처 판단이유
https://app.example.com/orders동일 출처경로만 다름
https://app.example.com/products?page=2동일 출처쿼리만 다름
https://app.example.com:443/orders동일 출처HTTPS 기본 포트가 443
http://app.example.com/products교차 출처스킴이 다름
https://api.example.com/products교차 출처호스트가 다름
https://app.example.com:8443/products교차 출처포트가 다름

같은 회사가 운영하는 서비스인지, 물리적으로 같은 서버에 있는지는 이 판단에 영향을 주지 않는다. 로컬 개발에서도 프론트엔드가 http://localhost:5173, API가 http://localhost:8080이라면 포트가 달라 교차 출처가 된다. localhost와 127.0.0.1도 호스트가 다르므로 같은 출처가 아니다.

SOP의 목적과 접근 제한 범위​

사용자가 어떤 서비스에 로그인한 뒤 악성 사이트를 방문했다고 생각해 보자. 그 사이트의 JavaScript가 사용자의 로그인 상태를 이용해 서비스의 개인정보 API를 호출하고 응답까지 자유롭게 읽을 수 있다면, 다른 사이트를 방문하는 것만으로 정보가 유출될 수 있다.

이런 접근을 제한하는 브라우저 보안 정책이 SOP(Same-Origin Policy, 동일 출처 정책) 다. 한 출처에서 실행되는 문서나 스크립트가 다른 출처의 리소스와 상호작용할 수 있는 범위를 제한한다.

예시: 페이지 출처에 따른 계좌 API 응답 접근 제한

SOP는 개발자가 함수처럼 호출하는 기능이 아니다. 브라우저가 기본적으로 적용하는 규칙이다. 사용자가 은행에 로그인한 상황에서, 은행 페이지와 악성 페이지가 같은 계좌 조회 API를 호출한다고 생각해 보자.

은행 페이지는 동일 출처의 계좌 응답을 읽을 수 있지만, 악성 페이지는 은행 서버가 응답해도 브라우저 안에서 JavaScript의 응답 읽기가 차단되는 예시

그림에서는 차단 지점을 비교하기 위해 인증 쿠키의 전송 조건을 만족하는 단순 GET 요청이며, 은행 서버가 악성 사이트에 CORS를 허용하지 않았다 고 가정한다. 실제로는 SameSite나 브라우저의 쿠키 정책에 따라 인증 쿠키 자체가 전송되지 않을 수도 있다.

위쪽은 사용자가 https://bank.example에서 자신의 잔액을 확인하는 상황이다. 은행 페이지의 JavaScript가 같은 출처의 API를 호출하므로, 인증이 정상적으로 처리되면 응답을 읽어 화면에 잔액을 표시할 수 있다.

아래쪽은 사용자가 은행에 로그인한 상태로 https://evil.example을 방문한 상황이다. 이 페이지의 JavaScript가 다음 코드를 실행한다고 해보자.

// https://evil.example 페이지에서 실행되는 코드
try {
const response = await fetch('https://bank.example/api/account', {
credentials: 'include',
});
const account = await response.json();
console.log(account.balance);
} catch (error) {
console.error('계좌 응답을 읽을 수 없습니다.', error);
}

브라우저는 API 주소만 보는 것이 아니라 이 코드를 실행한 페이지의 출처 도 확인한다. 코드는 evil.example에서 실행됐고 응답은 bank.example에서 왔다. 서로 다른 출처이며 은행 서버의 CORS 허용도 없으므로, 브라우저는 응답을 악성 페이지의 JavaScript에 넘기지 않는다. 이 코드에서는 fetch가 실패해 catch로 넘어가고, 잔액을 출력하는 부분까지 실행되지 않는다.

그림의 차단 표시가 서버 앞이 아니라 브라우저 안에 있는 이유가 이것이다. 요청과 응답이 네트워크를 오갔더라도, 그 내용을 악성 페이지의 코드가 읽을 수 있는지는 별도로 제한된다. 은행 탭을 함께 열어 두었거나 은행에 로그인했다는 사실이 다른 탭의 악성 코드에 은행 출처의 권한을 주지는 않는다.

요청 전송과 응답 읽기의 차이​

여기서 주의할 점은 요청을 보내는 것과 응답을 읽는 것이 서로 다른 동작이라는 사실이다. 다른 출처로 링크를 따라 이동하거나 폼을 제출하는 일은 일반적으로 가능하다. 다른 출처의 이미지를 화면에 표시할 수도 있다. 반면 fetch로 가져온 교차 출처 응답을 JavaScript에서 읽거나 다른 출처 iframe의 DOM에 접근하는 일에는 제한이 있다.

이미지를 표시할 수 있다고 해서 픽셀 데이터까지 자유롭게 읽을 수 있는 것도 아니다. CORS 허용 없이 가져온 교차 출처 이미지를 canvas에 그리면 canvas가 오염되어 getImageData() 같은 읽기 작업이 제한될 수 있다. 브라우저는 리소스를 보여 주는 행위와 그 내용을 스크립트에 공개하는 행위를 구분한다.

이 차이는 CSRF를 이해할 때도 중요하다. CSRF는 사용자의 인증 정보를 이용해 의도하지 않은 요청을 실행시키는 공격이다. 공격자가 응답을 읽지 못하더라도 요청이 서버에 도달해 상태를 변경하면 공격이 성립할 수 있다. 따라서 SOP만으로 CSRF를 방어할 수는 없으며, 서버는 CSRF 토큰 검증이나 적절한 SameSite 쿠키 설정, 요청 출처 검증 등을 별도로 고려해야 한다.

CORS의 동작 원리와 응답 헤더​

프론트엔드를 https://app.example.com, API를 https://api.example.com에서 운영하는 구조는 자연스럽다. 두 주소는 교차 출처지만 프론트엔드가 API 응답을 읽을 수 있어야 서비스가 동작한다.

이때 사용하는 메커니즘이 CORS(Cross-Origin Resource Sharing) 다. 서버는 HTTP 응답 헤더로 허용 범위를 알리고, 브라우저는 그 정보를 확인해 응답을 JavaScript에 공개할지 결정한다.

처음의 상품 조회 요청을 다시 보자. 브라우저는 요청을 시작한 출처를 Origin 헤더로 전달한다.

GET /products/1 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

서버가 해당 출처의 접근을 허용한다면 다음과 같이 응답할 수 있다.

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example.com

{"id":1,"name":"키보드"}

브라우저는 Access-Control-Allow-Origin을 확인한 뒤 프론트엔드 코드가 응답을 사용할 수 있게 한다. 이 헤더가 없거나 허용 출처가 맞지 않으면 서버가 200 OK를 반환했더라도 JavaScript는 응답을 읽지 못한다.

Postman에서는 성공하고 브라우저에서는 실패하는 이유도 여기에 있다. Postman은 브라우저의 SOP와 CORS 제한을 같은 방식으로 적용하지 않는다. CORS가 API 인증이나 인가를 대신할 수 없는 이유이기도 하다. 브라우저 밖의 클라이언트도 API를 호출할 수 있으므로 서버는 요청자의 권한을 별도로 검증해야 한다.

CORS 오류를 해결하려고 프론트엔드 요청에 Access-Control-Allow-Origin을 추가하는 경우가 있지만, 이 헤더는 서버가 보내는 응답 헤더다. 요청에 넣어도 응답을 읽을 권한이 생기지 않고, 오히려 추가 헤더 때문에 사전 요청이 발생할 수 있다.

응답 헤더 공개와 Access-Control-Expose-Headers​

CORS가 허용된 응답이라도 모든 응답 헤더를 JavaScript에서 읽을 수 있는 것은 아니다. 예를 들어 서버가 반환한 X-Request-Id를 코드에서 사용하려면 서버가 이를 추가로 공개해야 한다.

Access-Control-Expose-Headers: X-Request-Id
const response = await fetch('https://api.example.com/products/1');
console.log(response.headers.get('X-Request-Id'));

Set-Cookie처럼 JavaScript에 공개할 수 없는 응답 헤더도 있다. Access-Control-Expose-Headers에 추가해도 response.headers로 읽을 수 없으며, 쿠키 저장은 브라우저가 처리한다.

개발자 도구의 Network 탭에서 헤더가 보이는 것과 JavaScript에서 읽을 수 있는 것은 다르다. Access-Control-Expose-Headers는 읽을 수 있는 응답 헤더 를 지정한다. 뒤에서 살펴볼 Access-Control-Allow-Headers는 보낼 수 있는 요청 헤더 를 지정하므로 역할을 구분해야 한다.

단순 요청과 프리플라이트의 발생 조건​

조회가 정상적으로 동작해 상품 등록 기능을 추가했다고 하자. 이번에는 JSON 본문을 담아 POST 요청을 보낸다.

fetch('https://api.example.com/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '키보드',
price: 50000,
}),
});

Network 탭을 보면 POST에 앞서 OPTIONS 요청이 나타날 수 있다. 브라우저가 실제 요청을 보내기 전에 서버의 허용 여부를 확인하는 프리플라이트(preflight) 다.

모든 교차 출처 요청이 이 과정을 거치는 것은 아니다. 일반적으로 단순 요청이라고 부르는 요청은 사전 확인 없이 전송된다. 주요 조건은 메서드가 GET, HEAD, POST 중 하나이고, 개발자가 설정한 헤더가 CORS 안전 목록과 값 제한을 만족하는 것이다. Content-Type을 지정했다면 매개변수를 제외한 미디어 타입이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야 한다. 예를 들어 text/plain;charset=UTF-8도 다른 안전 목록 조건을 만족하면 해당한다. XHR 업로드 이벤트 리스너나 스트림 본문 등 API 사용 방식에 따른 추가 조건도 있다.

위 상품 등록 요청의 application/json은 이 Content-Type 목록에 포함되지 않는다. Authorization 같은 헤더를 추가하거나 PUT, PATCH, DELETE 메서드를 사용할 때도 프리플라이트가 필요하다. 따라서 GET이라도 Authorization 헤더가 있으면 사전 확인을 거칠 수 있고, POST라도 폼 형식 등 단순 요청 조건을 만족하면 바로 전송될 수 있다.

단순 요청 역시 응답을 JavaScript에 공개할 때는 CORS 검사를 받는다. 생략되는 것은 실제 요청 전의 사전 확인이다.

OPTIONS 사전 확인과 실제 요청 처리 과정​

브라우저가 OPTIONS 사전 요청과 허용 응답을 확인한 뒤 실제 POST를 보내고, 실제 응답도 CORS 검사 후 JavaScript에 공개하는 과정

사전 확인과 실제 요청은 각각 요청·응답으로 구성된다. 브라우저는 ②의 허용 응답을 검사한 뒤 ③의 실제 요청을 전송하고, ④의 실제 응답도 검사한 뒤 JavaScript에 공개한다.

유효한 프리플라이트 캐시가 없다면 브라우저는 상품 등록 전에 다음 요청을 보낸다.

OPTIONS /products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

Access-Control-Request-Method와 Access-Control-Request-Headers는 실제 요청에서 사용할 메서드와 헤더를 알린다. 서버는 이에 대한 허용 범위를 응답한다.

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600

브라우저는 성공 상태 코드뿐 아니라 허용 출처·메서드·헤더가 요청 조건에 맞는지도 확인한다. 검사를 통과하면 실제 POST 요청을 보내고, 서버는 상품을 등록한 뒤 응답한다.

HTTP/1.1 201 Created
Content-Type: application/json
Access-Control-Allow-Origin: https://app.example.com

{"id":1,"name":"키보드","price":50000}

실제 응답에도 Access-Control-Allow-Origin이 있다는 점이 중요하다. 프리플라이트에서 허용받았더라도 실제 응답의 CORS 검사는 별도로 수행된다.

사전 확인이 매번 반복되는 것은 아니다. 서버는 Access-Control-Max-Age로 확인 결과의 캐시 시간을 안내할 수 있고, 브라우저는 유효한 캐시가 있으면 프리플라이트를 생략할 수 있다. 이 캐시는 일반 HTTP 응답 캐시와 구분되며, 브라우저별 상한이 적용될 수 있다.

CORS 실패 단계와 서버 처리 여부​

프리플라이트 검사에서 실패했다면 브라우저는 실제 POST를 보내지 않는다. 하지만 POST까지 전송된 뒤 실제 응답의 CORS 검사에서 실패했다면 상황이 다르다. 서버는 이미 상품을 등록했는데 프론트엔드만 결과를 읽지 못한 상태일 수 있다.

단순 요청도 마찬가지다. CORS 사전 확인 없이 전송될 수 있으므로 응답을 읽는 데 실패했다고 해서 서버 작업까지 취소되는 것은 아니다. 화면에 오류가 표시됐다는 이유만으로 등록 요청을 반복하면 중복 데이터가 생길 수 있다. 재시도하기 전에 실제 요청의 전송 여부와 서버 처리 결과를 확인해야 하는 이유다.

교차 출처 쿠키 인증 설정​

상품 조회와 등록까지 동작하더라도 로그인한 사용자의 정보를 조회할 때 다시 문제가 생길 수 있다. 쿠키 기반 인증을 사용한다면 CORS 설정에 더해 쿠키가 실제로 전송되는 조건을 확인해야 한다.

fetch의 credentials 기본값은 same-origin이므로 교차 출처 요청에는 기본적으로 쿠키가 포함되지 않는다. 교차 출처에도 쿠키를 포함하려면 다음과 같이 설정한다.

fetch('https://api.example.com/me', {
credentials: 'include',
});

서버는 자격 증명 모드가 include인 요청의 응답을 공개하도록 명시적인 출처와 자격 증명 허용 헤더를 반환해야 한다.

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

이 경우 Access-Control-Allow-Origin: *는 사용할 수 없다. 클라이언트의 credentials 설정과 서버의 허용 설정이 함께 맞아야 한다.

credentials는 기존 쿠키를 보내는 것뿐 아니라 응답의 Set-Cookie를 처리할지에도 영향을 준다. 교차 출처 로그인 요청에서 새 쿠키를 저장하려는 경우에도 include 설정과 쿠키 정책을 함께 확인해야 한다.

다만 include를 설정했다고 모든 쿠키가 전송되는 것은 아니다. 쿠키의 호스트 또는 Domain, Path가 요청 대상과 맞아야 하며 Secure, SameSite 조건과 브라우저의 쿠키 정책도 만족해야 한다. HttpOnly는 JavaScript가 쿠키를 직접 읽는 것을 제한하는 속성이므로, 전송 조건을 만족하는 HttpOnly 쿠키는 요청에 포함될 수 있다.

Origin과 Site의 차이 및 SameSite 설정​

여기서 CORS의 출처와 쿠키의 SameSite가 사용하는 사이트 개념을 구분할 필요가 있다. 일반적인 HTTPS 웹 주소에서 사이트는 스킴과 등록 가능한 도메인을 기준으로 판단한다.

https://app.example.com과 https://api.example.com은 호스트가 달라 교차 출처지만, 스킴과 등록 가능한 도메인인 example.com이 같아 동일 사이트다. 서브도메인이 다르다는 이유만으로 SameSite=None이 필요한 것은 아니다.

교차 사이트 요청에 쿠키를 보내야 하는 경우에는 다음과 같은 설정을 검토한다.

Set-Cookie: sessionId=example; Path=/; HttpOnly; Secure; SameSite=None

이 설정도 브라우저의 서드파티 쿠키 제한을 무효화하지는 않는다. 쿠키가 보이지 않는다면 CORS 응답 헤더뿐 아니라 쿠키 자체의 전송 조건도 함께 살펴야 한다.

프리플라이트와 실제 요청의 인증 처리​

쿠키를 포함한다는 사실만으로 프리플라이트가 발생하지는 않는다. 앞의 GET 요청은 다른 조건에 문제가 없다면 credentials: 'include'여도 사전 확인 없이 전송될 수 있다.

한편 프리플라이트가 필요한 요청에서는 표준상 교차 출처 OPTIONS에 로그인 쿠키 같은 자격 증명이 포함되지 않는다. 인증 미들웨어가 OPTIONS에도 로그인 쿠키를 요구해 401을 반환하면 브라우저는 실제 요청을 보내지 못한다.

실제 요청의 credentials가 include라면, 프리플라이트 응답에도 명시적인 Access-Control-Allow-Origin과 Access-Control-Allow-Credentials: true가 필요하다. OPTIONS 자체에는 쿠키가 없더라도, 이후 자격 증명을 포함한 실제 요청을 허용한다는 응답은 있어야 한다. 실제 응답에도 이 두 헤더를 반환해야 한다.

서버에서는 프리플라이트의 출처·메서드·헤더 허용 여부를 처리하고, 실제 요청에서 필요한 인증과 인가를 수행하도록 구성해야 한다.

CORS 오류 진단과 해결 방법​

CORS 관련 오류 메시지가 항상 허용 헤더 누락을 뜻하지는 않는다. DNS 조회 실패, 연결 거부, TLS 오류 등으로 요청 자체가 실패한 경우도 있으므로 Network 탭에서 응답이 실제로 도착했는지 먼저 확인한다.

CORS 오류를 만났을 때는 먼저 현재 페이지와 실제 요청 URL의 출처를 비교한다. 이후 Console의 오류 메시지와 Network 탭을 함께 보면 어느 단계에서 문제가 생겼는지 좁힐 수 있다.

OPTIONS가 실패했고 실제 요청이 없다면 프리플라이트 응답을 살펴본다. 401이면 인증 미들웨어가 사전 요청을 막고 있는지 확인하고, 성공 상태 코드가 왔다면 허용 출처·메서드·헤더가 요청과 맞는지 확인한다.

실제 요청까지 전송됐다면 그 응답의 CORS 헤더를 확인한다. 성공 응답에는 헤더가 있지만 오류 응답에는 빠지는 경우도 있다. 이때는 브라우저의 CORS 오류가 원래 서버 오류를 가릴 수 있으므로 서버나 게이트웨이 로그까지 확인해야 한다. 쿠키 인증 요청이라면 credentials 설정과 쿠키의 전송 여부도 함께 본다.

HTTP 오류와 CORS 실패도 구분해야 한다. 실제 응답이 401이나 500이어도 CORS 조건을 만족하면 fetch는 Response를 반환하므로 response.ok나 response.status를 확인해야 한다. 반면 CORS 검사 실패는 fetch의 Promise를 거부하며, JavaScript만으로는 상세 원인을 알기 어려워 Console과 Network 탭을 함께 확인해야 한다.

서버의 출처 허용 목록과 CORS 헤더 설정​

API 서버나 게이트웨이에서는 서비스에 필요한 출처·메서드·헤더를 허용한다. 예를 들어 프리플라이트 응답에 다음과 같은 범위를 지정할 수 있다.

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

여러 출처를 허용할 때 Access-Control-Allow-Origin에 주소를 쉼표로 나열하지는 않는다. 서버가 요청의 Origin을 허용 목록과 비교하고, 허용된 경우 해당 출처 하나를 반환한다. 출처에 따라 응답이 달라진다면 캐시가 이를 구분하도록 Vary: Origin도 설정한다.

Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

요청의 Origin을 검증 없이 그대로 반환하면 의도하지 않은 사이트에도 응답을 공개할 수 있다. 특히 쿠키 인증을 사용하는 응답은 허용 목록을 명확히 관리해야 한다.

동일 출처 프록시 구성​

프록시는 클라이언트의 요청을 다른 서버로 전달하고, 받은 응답을 다시 클라이언트에 돌려주는 중계 역할을 한다. “프론트에서 프록시로 CORS 문제를 해결했다”는 말은 보통 프론트 프로젝트의 개발 서버에 중계 설정을 추가했다는 뜻이다. 브라우저에서 실행되는 JavaScript가 직접 CORS 제한을 해제하는 것은 아니다.

예시: 백엔드 직접 요청과 개발 서버 프록시 요청 비교

직접 요청은 브라우저가 8080 백엔드를 호출하므로 CORS 검사를 받지만, 프록시 요청은 같은 출처인 5173 개발 서버에 요청하고 개발 서버가 백엔드와 통신하는 구조

프론트 페이지가 http://localhost:5173에서 열려 있고, 백엔드는 http://localhost:8080에서 실행 중이라고 가정하자. 다음 코드는 브라우저가 백엔드에 직접 요청하게 한다.

fetch('http://localhost:8080/products');

페이지와 요청 대상의 포트가 다르므로 교차 출처 요청이다. 단순 GET 요청이라면 백엔드가 정상적으로 응답해도 CORS 허용 헤더가 없을 때 브라우저는 JavaScript의 응답 읽기를 제한한다. 서버에 연결하지 못한 상황과 응답을 읽지 못한 상황은 다르다.

프록시를 사용할 때는 브라우저의 요청 대상을 바꾼다.

fetch('/api/products');
// 브라우저가 요청하는 URL:
// http://localhost:5173/api/products

이 상대 경로는 백엔드의 8080 포트가 아니라 현재 페이지와 같은 출처인 5173 포트로 향한다. 개발 서버에는 /api로 들어온 요청을 백엔드로 전달하도록 프록시를 설정한다. 이 예시에서는 /api 접두사를 제거하도록 설정한 경우를 가정한다.

브라우저 → 개발 서버: http://localhost:5173/api/products
개발 서버 → 백엔드: http://localhost:8080/products

브라우저에서 개발 서버까지는 동일 출처 통신이다. 개발 서버에서 백엔드까지는 서버 간 통신이므로 브라우저의 CORS 검사가 적용되지 않는다. 백엔드가 응답하면 개발 서버는 받은 데이터를 브라우저에 전달한다. 브라우저는 자신이 요청한 5173 개발 서버의 응답을 받는다.

여기서 개발 서버와 브라우저는 별개의 프로그램이다. 둘 다 프론트엔드 개발 과정에 등장하지만, 백엔드에 중계 요청을 보내는 주체는 개발 서버다. 프록시 설정 없이 상대 경로만 바꾸면 개발 서버가 API 요청을 처리하지 못해 404 등의 오류가 발생할 수 있다.

백엔드가 꺼져 있거나 개발 서버에서 백엔드로 연결할 수 없다면 프록시를 사용해도 실패한다. 프록시는 네트워크 연결 실패나 인증·인가 실패를 해결하지 않는다. 브라우저의 요청을 같은 출처에서 받아 서버가 중계하는 구조를 만드는 것이다.

프록시는 백엔드 응답을 직접 전달해야 한다. 외부 API 주소로 리다이렉션만 반환하면 브라우저가 다시 교차 출처 요청을 보내므로 CORS 검사가 적용될 수 있다.

개발 서버의 프록시는 일반적으로 개발 중에만 동작한다. 배포 후에도 같은 구조를 사용하려면 웹 서버나 게이트웨이에 /api를 처리할 리버스 프록시를 구성해야 한다. 또는 브라우저가 API를 직접 호출하도록 하고, API 서버에서 배포된 프론트엔드 출처를 허용할 수 있다.

no-cors 모드의 응답 접근 제한​

요청 옵션에 mode: 'no-cors'를 넣으면 해결될 것처럼 보일 수 있다. 하지만 교차 출처 요청에서 이 옵션을 사용하면 응답이 opaque 형태로 전달되어 JavaScript가 본문이나 헤더를 읽을 수 없다.

const response = await fetch('https://api.example.com/products', {
mode: 'no-cors',
});

// 교차 출처 opaque 응답의 JSON 본문은 읽을 수 없다.
const products = await response.json();

요청에 사용할 수 있는 메서드와 헤더에도 제한이 있다. API 데이터를 화면에 표시해야 한다면 서버의 CORS 설정을 맞추거나 동일 출처 프록시를 구성해야 한다.

처음의 상품 조회 오류로 돌아가 보면, 확인해야 할 것은 서버의 정상 응답 여부만이 아니다. 요청을 보낸 페이지의 출처가 무엇인지, 서버가 그 출처를 허용했는지, 브라우저가 어느 단계에서 응답 접근을 제한했는지까지 이어서 살펴야 한다. 이 흐름을 이해하면 프론트엔드의 요청 옵션과 백엔드의 응답 설정을 연결해서 문제를 찾을 수 있다.

참고 자료​