CORS?
서버 보안 기능이 아니라 브라우저 정책!
CORS(Cross-Origin Resource Sharing)의 본질은 브라우저의 동일 출처 정책(Same-Origin Policy SOP)을 예외적으로 완화하기 위한 규칙입니다.
보통 개발자는 "프론트에서 API호출이 실패했다"라고 이해하고, console창에 "blocked by CORS policy"라고 뜨기 때문에, 서버가 막았다고 생각할 수 있습니다.
실제로는 브라우저가 응답을 JS에 전달하지 않은 것인데, 실패 지점이 백엔드처럼 보입니다.
또한, 해당 에러는 Access-Control-Allow-Origin 같은 헤더를 추가하면 문제가 해결되기 때문에 오해할 수 있습니다.
즉 CORS는 서버가 막는게 아니라 브라우저가 막는 것이고, 서버는 이 Origin에게는 읽을 수 있게 해도 된다는 허가증(헤더)를 발급할 뿐이고, 그 허가증을 확인해서 통과시킬지 말지 결정하는 문지기는 브라우저입니다.
그렇다면, SOP은 왜 있는걸까?
서로 다른 사이트들이 한 브라우저 안에서 같이 돌아가는 환경이기 때문입니다.
기본적으로 다른 사이트에서 사용자가 로그인해둔 사이트의 권한을 가로채지 못하게 격리하기 위해서입니다.
웹은 기본적으로 악성사이트를 방문할 수 있다는 위협 모델이 있습니다. 사용자는 여러 사이트에 로그인한 상태로 웹 서핑을 합니다. 이 때 사용자가 하나라도 악성사이트를 방문하게 되면 그 페이지의 JS가 사용자의 브라우저에서 실행됩니다.
SOP가 없다면, 악성사이트의 JS가 사용자의 쿠키/세션이 실린 요청을 보내고 응답내용을 자기의 서버로 빼돌릴 수 있습니다.
즉, 다른 출처의 응답을 스크립트가 마음대로 읽지 못하게 해서 데이터의 탈취를 막습니다.
브라우저는 자동 첨부 권한인 쿠키/세션으로 인해 편의성이 있지만, 격리가 없다면 권한위임/탈취의 문제가 됩니다. SOP은 자동권한에 있어서도 안전장치를 제공할 수 있습니다.
웹은 광고/위/서드파티스크립트/여러탭/여러도메인 링크로 자유롭게 이동하는 구조이기 때문에 기본적으로 격리되어야합니다. SOP이 웹 보안의 기본 격리 모델입니다.
그렇지만, 요즘의 웹앱은 프론트와 API가 나뉘는 경우가 많습니다. 그래서 SOP의 예외 규칙이 필요합니다. 여기서 CORS가 예외규칙입니다.
Origin 판단 기준
- schema : http/https
- host : example.com
- port : 80/443/3000 등
3가지 모두 같은지로 판단 됩니다.
내 요청은 왜 두 번 날아갈까? (Preflight와 Simple Request)
개발을 하다 보면 네트워크 탭에 분명 API를 한 번 호출했는데, 요청이 두 번 잡히는 경우를 볼 수 있습니다. 자세히 보면 첫 번째 요청의 메서드가 GET이나 POST가 아니라 OPTIONS로 되어 있죠.
이것이 바로 Preflight(예비 요청)입니다.
브라우저는 요청을 크게 두 가지로 나눕니다.
1) 단순 요청 (Simple Request) 브라우저가 보기에 "이 정도는 안전하다"고 판단하는 요청입니다.
- 메서드가 GET, POST, HEAD 중 하나이고,
- Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 같은 기본적인 것들일 때입니다.
- 커스텀 헤더가 없어야 합니다.
- 이때는 예비 검사 없이 서버에 바로 요청을 보냅니다. (물론 SOP 위반 시 응답은 브라우저가 가립니다.)
2) 예비 요청이 필요한 경우 (Preflight) 하지만 요즘 우리는 대부분 JSON(application/json)으로 통신하거나, Authorization 헤더에 토큰을 실어 보냅니다. 브라우저 입장에서 이건 "단순하지 않은, 서버 데이터에 부작용을 일으킬 수 있는 요청"입니다.
그래서 본 요청을 보내기 전에 OPTIONS 메서드로 서버를 먼저 찔러봅니다.
브라우저: "나 application/json 데이터 보낼 건데, 받아줄래?" (OPTIONS 요청)
서버: "어, 그 출처(Origin)라면 허용해. POST 메서드 써도 돼." (200 OK)
브라우저: "확인했어. 이제 진짜 데이터 보낼게." (본 요청 POST)
CORS 에러는 주로 이 예비 요청 단계에서 서버가 허가증(헤더)을 제대로 주지 않아 발생합니다. 네트워크 탭에서 OPTIONS 요청이 빨간색으로 실패했다면, 서버 설정을 먼저 확인해야 합니다.
로그인이 왜 안될까(쿠키가 안넘어감)
CORS 설정을 다 했는데 쿠키가 안넘어갈 수 있는데, 이것도 SOP의 목적과 관련있습니다.
기본적으로 브라우저는 다른 출처로 요청을 보낼 때, 쿠키/인증헤더 같은 민감한 정보는 함부로 담지 않습니다.
이를 해결하기 위해서는 클라이언트와 서버 양쪽에 명시해야합니다.
1) 클라이언트 (프론트) : 요청을 보낼 때 인증정보도 함께 보낸다고 명시해야합니다.
- axios의 경우: withCredentials: true 옵션 필수
- fetch의 경우: credentials: 'include' 옵션 필수
2) 서버 (백엔드) : 응답 헤더에 인증정보를 받아도 된다고 허락해야합니다.
- Access-Control-Allow-Credentials: true
참고) Credentials를 true로 설정하는 순간,
Access-Control-Allow-Origin 헤더에는 절대로 와일드카드(*)를 쓸 수 없습니다.
이 경우 와일드카드(*) 대신 정확한 출처(Origin)를 명시해서 내려줘야 합니다.
Access-Control-Allow-Origin: * (모든 곳 허용)
퍼블릭 API 라면 상관없지만, 민감한 정보를 다루는 서비스라면 매우 위험합니다.
SOP라는 방어막을 서버가 스스로 무력화 시킬 수 있습니다.
서버간 통신이 유리한 이유
지금까지 CORS의 원리와 안전하게 설정하는 법을 알아봤습니다.
하지만, 보안 관점에서 "꼭 브라우저에서 그 API를 직접 호출해야만 할까요?"
서버끼리는 SOP가 없습니다.
SOP은 브라우저의 보안 정책입니다. 즉, api 서버 -> api 서버를 호출할때는 CORS 검사가 아예 발생하지 않습니다. preflight (두번 호출) 로 인한 네트워크 낭비도 없습니다.
가장 중요한 이유는 보안 입니다.
만약 프론트엔드에서 직접 타사 API를 호출한다면 필연적으로 API key나 인증 토큰이 브라우저에 노출됩니다. 개발자 도구만 열어도 누구나 그 키를 볼 수 있습니다.
하지만 proxy 방식을 사용하면
1. 프론트엔드 : 같은 도메인에 요청을 보냅니다.
2. 백엔드 : 숨겨진 비밀 키를 사용해 API 서버와 통신합니다.
3. response : 받은 데이터만 프론트에 전달합니다.
Admin API나 결제 API처럼 민감한 권한이 필요한경우
API key가 외부에 절대 노출되면 안되는경우
CORS설정 권한이 없는 외부 서버와 통신해야할 때 는 서버간 통신(proxy)를 권장합니다.
그럼에도 불구하고 CORS를 써야하는 경우
클라이언트가 직접 CDN이나 퍼블릭 API 데이터를 가져와야할 때
서버를 거치지 않고 빠른 속도로 대용량 파일을 다운로드/업로드 할 때
결국 CORS는 브라우저의 보안을 잠시 꺼주는 예외 규칙일 뿐입니다. 보안이 중요한 기능이라면 억지로 CORS를 뚫으려고 하기 보다는 백엔드에서 처리하는 서버간 통신을 우선 고려하는 것이 좋습니다.
'Frontend · UI' 카테고리의 다른 글
| wsl2로 rocky9 환경으로 react 프로젝트 만들기 (0) | 2025.03.06 |
|---|---|
| next.js 모듈 분리에 관한 고민 (0) | 2025.02.21 |