안녕하세요! 지난번 배포 환경의 미스터리, 환경변수(.env)의 함정에서는 무사히 탈출하셨나요? 드디어 서버에 내 코드를 띄우셨다니 정말 축하드립니다! 자, 오늘은 프론트엔드와 백엔드가 처음으로 만나서 데이터를 주고받을 때, 무조건 100% 확률로 마주치게 되는 붉은색 에러의 대명사에 대해 이야기해 볼까 합니다.
프론트엔드 개발자가 열심히 만든 화면에서 백엔드 API로 첫 로그인을 시도합니다. 그런데 콘솔창에 무시무시한 빨간 글씨가 도배됩니다. Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy. 등골이 서늘해지죠. 프론트엔드 개발자는 억울합니다. "어? 백엔드 개발자님이 주신 API 주소, 포스트맨(Postman)으로 찌르면 응답 엄청 잘 오는데요? 제 코드가 문젠가요?"
며칠 밤을 지새우게 만드는 이 끔찍한 'CORS 에러'가 도대체 왜 발생하는지, 그리고 프론트엔드와 백엔드에서 각각 어떻게 대처해야 하는지 속 시원하게 파헤쳐 보겠습니다.

1. 내 도메인과 네 도메인은 다르잖아! CORS의 진짜 정체
CORS(Cross-Origin Resource Sharing)는 사실 에러나 버그가 아닙니다. 브라우저가 여러분을 지키기 위해 켜둔 '강력한 보안 방패'입니다.
우리가 사용하는 크롬, 사파리 같은 웹 브라우저에는 기본적으로 '동일 출처 정책(SOP, Same-Origin Policy)'이라는 규칙이 있습니다. 쉽게 말해 "내 웹사이트(도메인 A)에서 돌아가는 스크립트는, 오직 내 웹사이트 서버(도메인 A)하고만 데이터를 주고받을 수 있어!"라는 깐깐한 규칙입니다. 만약 이 규칙이 없다면, 나쁜 해커가 만든 웹사이트에 무심코 접속했을 때 내 브라우저가 해커의 스크립트를 실행해서 내 은행 서버로 돈을 송금하라는 요청을 몰래 보낼 수도 있겠죠.
그래서 브라우저는 출처(Origin, 도메인+포트번호)가 다른 백엔드 서버로 API 요청을 보내면, "잠깐! 너네 둘이 주소가 다른데 내가 어떻게 믿고 데이터를 넘겨줘?" 하면서 통신을 콱 막아버리는 것입니다.
2. 프론트엔드의 억울함: "포스트맨(Postman)에서는 잘 되는데요?"
이때 프론트엔드 개발자들이 가장 많이 하는 오해가 있습니다. "포스트맨에서는 데이터가 잘 오는데 왜 브라우저에서만 에러가 날까요? 서버는 멀쩡한 것 같아요!"
정답은 'CORS는 브라우저만 검사하는 보안 규칙'이기 때문입니다. 포스트맨은 브라우저가 아니라 단순한 API 테스트 프로그램이라서 이런 깐깐한 보안 정책을 따르지 않습니다. 서버는 프론트엔드가 요청하든 포스트맨이 요청하든 똑같이 정상적인 데이터를 내려줍니다. 단지, 그 데이터를 받아서 화면에 보여주기 직전에 프론트엔드의 웹 브라우저가 자체적으로 문을 걸어 잠그고 붉은 에러를 뱉어내는 것이죠.

3. 완벽한 해결책: 백엔드 서버에서 "얘는 내 친구야"라고 허락해 주기
그렇다면 이 방패를 뚫고 정상적으로 통신하려면 어떻게 해야 할까요? 가장 근본적이고 완벽한 해결책은 백엔드 서버에서 브라우저에게 "이 도메인에서 오는 요청은 내가 허락한 안전한 친구니까 막지 마!"라고 응답 헤더(Header)에 명시해 주는 것입니다.
백엔드 개발자에게 커피 한 잔을 사주면서 이렇게 부탁해 보세요. "개발자님, 서버 응답 헤더에 Access-Control-Allow-Origin 값을 프론트엔드 도메인(http://localhost:3000 등)으로 열어주세요!"
Spring Boot라면 @CrossOrigin 어노테이션이나 WebMvcConfigurer를 통해 전역 설정을 할 수 있고, Node.js(Express)라면 cors 미들웨어 패키지를 설치해서 단 세 줄이면 해결할 수 있습니다. 백엔드에서 이 설정만 켜주면 브라우저는 안심하고 데이터를 프론트엔드에 넘겨주게 됩니다.
4. 프론트엔드의 임시 방편: 로컬 개발 환경의 구원자, 프록시(Proxy)
하지만 백엔드 개발자가 당장 바빠서 코드를 수정해 줄 수 없거나, 내가 제어할 수 없는 외부 오픈 API를 사용해야 할 때는 어떨까요? 이때 프론트엔드가 쓸 수 있는 훌륭한 속임수가 바로 '프록시(Proxy)'입니다.
React(Vite)나 Next.js 설정 파일에서 프록시를 세팅하면, 브라우저에게 "나 지금 다른 서버로 요청 보내는 거 아니고, 그냥 내 로컬 서버(localhost:3000)한테 보내는 거야~"라고 거짓말을 칠 수 있습니다. 그러면 프론트엔드 개발 서버가 중간에 그 요청을 가로채서 백엔드 서버로 대신 전달해 줍니다. 서버와 서버 간의 통신에서는 CORS 에러가 발생하지 않기 때문에, 로컬 개발 환경에서만큼은 에러 없이 쾌적하게 API를 테스트할 수 있게 됩니다.
마무리하며
오늘은 프론트엔드와 백엔드의 평화를 위협하는 붉은 악마, CORS 에러의 정체와 해결 방법에 대해 알아보았습니다. 브라우저가 우리를 지켜주기 위해 깐깐하게 구는 것일 뿐, 절대 서버나 코드가 망가진 것이 아니니 앞으로는 빨간 글씨를 보더라도 당황하지 마시고 우아하게 헤더와 프록시 설정을 만져주시면 되겠습니다!
그리고 여러분, 혹시 눈치채셨나요? 오늘 포스팅을 마지막으로 장장 12부작에 걸쳐 쉼 없이 달려온 "실무에서 복붙해 쓰는 에러 해결 및 트러블슈팅 일지" 시리즈가 드디어 대단원의 막을 내리게 되었습니다!
처음 다루었던 지긋지긋한 undefined 에러부터 무한 렌더링, Git 충돌, 메모리 누수, N+1 쿼리 폭탄, 환경변수 함정, 그리고 오늘의 CORS까지... 실무에서 우리를 멘붕에 빠뜨렸던 수많은 붉은 에러들을 하나하나 파헤쳐 보았는데요. 처음 코딩을 시작했을 때는 붉은색 콘솔창만 봐도 가슴이 철렁 내려앉고 두려우셨겠지만, 이 시리즈를 끝까지 함께 완주하신 여러분이라면 이제 어떤 에러 로그를 마주해도 침착하게 원인을 분석하고 단서를 찾아내는 튼튼한 '트러블슈팅 근육'이 생기셨을 거라 확신합니다.
기억하세요! 에러는 결코 여러분의 코딩 실력이 부족해서 나는 것이 아닙니다. 더 깊은 원리를 깨닫고 훌륭한 개발자로 성장하기 위해 거쳐가는 아주 자연스럽고 소중한 과정일 뿐입니다.
그동안 에러 해결 시리즈를 사랑해 주시고 함께 삽질의 눈물을 흘려주신 모든 분들께 진심으로 감사드립니다. 다음번에는 새로운 시리즈로 다시 여러분을 찾아뵙겠습니다. 새로운 시리즈도 많은 기대 부탁드립니다!