안녕하세요! 길고 길었던 [실무 웹 인증과 보안 완벽 가이드]의 대장정이 드디어 마지막 장에 도착했습니다. 지난 6편에서 우리는 해커들의 악랄한 공격(XSS, CSRF)을 막아내는 방법을 배웠습니다.
"자, 이제 프론트와 백엔드 세팅 끝! 로그인 API 호출해 볼까?" 자신 있게 버튼을 누르는 순간, 브라우저 콘솔 창이 시뻘겋게 물들며 개발자들의 영혼을 파괴하는 그 유명한 에러가 튀어나옵니다.
Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy:
백엔드 개발자에게 달려가 물어봅니다. "API가 안 돼요!" 백엔드 개발자는 Postman으로 찔러보고 대답합니다. "어? 포스트맨에서는 잘 되는데요?"
도대체 포스트맨에서는 잘만 되는 API가 왜 내 브라우저에서만 빨간 에러를 뱉어내는 걸까요? 오늘, 수많은 주니어들을 밤새우게 만들었던 CORS(교차 출처 리소스 공유)의 정체와 이를 깔끔하게 해결하는 실무 노하우를 전부 공개합니다.
1. 포스트맨은 되고 브라우저는 안 되는 이유: SOP (동일 출처 정책)
우리가 겪는 이 고통의 진짜 범인은 백엔드 서버가 아닙니다. 범인은 바로 '여러분이 쓰고 있는 크롬 브라우저'입니다.
브라우저에는 SOP (Same-Origin Policy, 동일 출처 정책) 이라는 아주 깐깐한 기본 보안 규칙이 있습니다. 쉽게 말해 "데이터를 요청한 곳(프론트엔드)과 데이터를 주는 곳(백엔드)의 '출처(Origin)'가 다르면, 브라우저가 중간에서 데이터를 뺏어버리는 정책"입니다.
- 출처(Origin)란 무엇일까요? URL을 구성하는 [프로토콜] + [도메인(호스트)] + [포트 번호] 이 세 가지가 완벽하게 똑같아야만 '같은 출처'로 인정합니다. 예를 들어 프론트엔드가 http://localhost:3000 에서 실행 중인데, 백엔드 API 서버는 http://localhost:8080 이라면? 포트 번호가 다르죠! 출처가 다르다고 판단한 브라우저는 백엔드가 정성껏 보내준 응답 데이터를 여러분이 보지 못하게 콱 막아버립니다.
- 브라우저는 왜 이렇게 우릴 귀찮게 할까요? 우리를 괴롭히려는 게 아니라 지켜주기 위해서입니다. 6편에서 배운 CSRF 공격 기억하시나요? 해커의 악성 사이트(hacker.com)에서 몰래 우리은행(wooribank.com) API를 호출해서 내 돈을 빼가려고 할 때, 브라우저가 "어딜 남의 출처에서 함부로 데이터를 요청해!" 하고 컷(Cut) 해주는 방어막이 바로 SOP이기 때문입니다.

2. 예외를 허락해 주는 VIP 티켓: CORS 의 등장
하지만 시대가 변했습니다. 옛날에는 프론트와 백엔드가 한 서버에서 같이 돌았지만, 요즘은 React(프론트) 서버와 Spring(백엔드) 서버를 완전히 분리해서 개발하잖아요? 즉, 태생적으로 프론트와 백엔드의 출처가 다를 수밖에 없는 구조가 되었습니다.
SOP 때문에 아예 개발을 할 수 없게 되자, 브라우저가 타협안을 내놓았습니다. "알았어, 출처가 달라도 백엔드 서버가 '얘는 나랑 아는 애니까 데이터 줘도 돼!' 하고 헤더에 명시적으로 허락증을 써서 보내면, 내가 특별히 통과시켜 줄게!"
이 특별한 허락증 제도가 바로 CORS (Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 입니다. 즉, CORS는 에러 이름이 아니라, 서로 다른 출처 간에 안전하게 통신하기 위해 브라우저가 열어준 합법적인 뒷문(규칙)입니다. 우리가 흔히 말하는 "CORS 에러"는 사실 "너희 둘이 통신하려면 CORS 규칙을 지켜야 하는데, 백엔드 서버가 허락증을 안 줬잖아!" 하고 브라우저가 화를 내는 것입니다.
3. 노크부터 하고 들어갑니다! 예비 요청 (Preflight)
CORS의 통신 방식을 파고들다 보면, 프론트엔드 네트워크 탭에 내가 보낸 적도 없는 이상한 요청이 하나 먼저 찍혀있는 걸 보게 됩니다. 바로 메서드가 OPTIONS로 되어 있는 Preflight(예비 요청) 입니다.
왜 브라우저는 본 요청(POST나 PUT)을 보내기 전에 OPTIONS라는 녀석을 먼저 쓱 날려볼까요?
만약 여러분이 "게시글 100개 한 번에 삭제하기(DELETE)" 버튼을 눌렀다고 칩시다. 출처가 달라서 브라우저가 막아야 하는데, 백엔드 서버가 그걸 받아서 이미 DB에서 게시글 100개를 지워버렸다면? 나중에 브라우저가 에러를 띄워봤자 이미 데이터는 다 날아간 뒤입니다. (이를 '부수 효과(Side Effect)'라고 부릅니다.)
그래서 브라우저는 무거운 본 요청을 보내기 전에, 아주 가벼운 OPTIONS 요청을 먼저 백엔드로 톡 던져봅니다. "똑똑똑, 저기요 백엔드 님. 제가 지금부터 http://localhost:3000에서 온 DELETE 요청을 하나 보낼 건데요. 혹시 이거 받아주실 수 있나요?" 서버가 "오케이, 받아줄게" 하고 응답하면, 그제야 안심하고 진짜 DELETE 본 요청을 날립니다. 이 똑똑한 매커니즘 덕분에 돌이킬 수 없는 서버 데이터의 훼손을 미리 막을 수 있는 것이죠!

4. 실전! CORS 에러를 박살 내는 양측의 해결 방법
원리를 알았으니 이제 에러를 고쳐야겠죠? 개발 환경과 운영 환경에 따라 프론트와 백엔드가 사이좋게 해결하는 방법이 있습니다.
🛠️ 프론트엔드에서의 꼼수: 개발 환경 Proxy (프록시) 설정
로컬에서 당장 개발해야 하는데 백엔드 개발자가 바빠서 CORS 세팅을 못 해준다면? 프론트엔드 개발자가 직접 '브라우저를 속이는 마법'을 부릴 수 있습니다. React(Webpack)나 Vite 설정 파일에서 Proxy(프록시) 기능을 켜는 것입니다. 프론트엔드 코드에서는 API를 호출할 때 http://localhost:8080/api 대신 그냥 /api 라고만 적습니다. 그러면 브라우저는 "오, 같은 포트(3000)로 요청하네? 통과!" 하고 보내줍니다. 하지만 그 뒤에서 프록시 서버가 몰래 목적지를 8080 포트로 쓱 바꿔치기해서 백엔드로 보내는 기가 막힌 우회 기법입니다. (단, 이 방법은 배포 환경에서는 동작하지 않으니 개발할 때만 쓰셔야 합니다!)
🛡️ 백엔드에서의 정석: CORS 허용 헤더(Header) 추가
실제 서비스를 배포(운영 환경)할 때는 반드시 백엔드 서버에서 정식으로 허락증을 발급해 주어야 합니다. 백엔드 프레임워크(Spring, Node.js 등)의 설정 파일에 들어가서, 응답 헤더에 Access-Control-Allow-Origin 값을 추가해 줍니다.
- Access-Control-Allow-Origin: 나의 프론트 도메인
- 이렇게 적어주면 브라우저가 응답을 보고 "오, 백엔드가 프론트 도메인을 VIP 명단에 넣어놨네. 통과!" 하고 에러를 없애줍니다.
🚨 [경고] 귀찮다고 * (와일드카드) 쓰지 마세요! CORS 설정하기 귀찮다고 백엔드 코드에 Access-Control-Allow-Origin: * (아무나 다 들어오세요) 라고 적어놓는 경우가 있습니다. 이렇게 하면 우리 서버를 향한 해커의 모든 요청을 브라우저가 통과시켜 주기 때문에 심각한 보안 구멍이 생깁니다. 또한, 우리가 4편에서 배운 쿠키(Cookie)를 실어 보낼 때는 브라우저가 * 설정을 절대 허락하지 않습니다. 반드시 허용할 프론트엔드의 정확한 도메인 주소만 콕 집어서 설정해 주는 습관을 들이셔야 합니다!

시리즈 연재를 마치며: 이제 당신은 보안 마스터입니다!
드디어 7편에 걸친 [실무 웹 인증과 보안 완벽 가이드] 대장정이 끝났습니다. 축하합니다! 🎉
처음엔 그저 "로그인 기능 구현하기" 정도로만 생각하셨겠지만, 1편의 쿠키와 세션을 시작으로 2~3편의 JWT와 토큰 분리 전략, 4편의 아찔했던 토큰 저장소 논쟁, 5편의 우아한 소셜 로그인 통신 흐름, 6편의 방패와 창 같은 XSS/CSRF 방어, 그리고 오늘 7편의 CORS 에러 격파까지!
이제 여러분은 단순히 코드를 복사 붙여넣기 하는 코더가 아니라, "왜 이런 아키텍처를 선택했고, 어떻게 사용자의 데이터를 안전하게 지켜내는지"를 명확하게 설명할 수 있는 훌륭한 엔지니어의 시야를 가지게 되셨습니다.
이 시리즈가 여러분의 실무 프로젝트나 다가올 면접에서 강력한 무기가 되기를 진심으로 바랍니다. 그동안 긴 시리즈를 함께해 주셔서 감사합니다. 앞으로도 블로그에서 더 트렌디하고 유익한 웹 개발 이야기로 찾아오겠습니다! 모두 에러 없는 행복한 코딩 하세요! 💻✨