본문 바로가기

CORS 문제 해결 완전 가이드 : 프론트/백엔드 분리 시 자주 터지는 보안 정책

📑 목차

    CORS 문제 해결 흐름은 프런트와 백엔드가 분리된 환경에서 "요청은 갔는데 브라우저가 응답을 막는" 상황을 순서대로 좁히는 데 초점이 있다.

    예를 들어 프런트는 https://app.example.com, API는 https://api.example.com처럼 도메인이 갈라져 있을 때, 개발자 도구 콘솔에 빨간 오류가 뜨고 화면은 멈춘 것처럼 보이기도 한다.

    서버 로그에는 200이 찍혔는데 브라우저는 실패로 처리하는 경우가 많다. 이때 문제는 네트워크 단절이 아니라 "브라우저의 보안 정책이 응답을 읽지 못하게 차단"하는 쪽일 가능성이 커진다.

    처음엔 "뭐가 문제지?" 싶어서 서버를 계속 건드렸다. 하지만 CORS 개념을 배우니까 브라우저가 막는 이유가 명확해졌다. 브라우저는 정책을 지키는 것이었다.

    CORS가 터지는 전형적인 장면부터 정리한다

    이 현상은 프런트와 백엔드 출처가 다를 때 항상 발생 가능한 상황이다. 특히 개발 단계와 배포 단계에서 URL 구조가 달라질 때 자주 재발한다.

    핵심은 "서버는 응답했지만 브라우저가 읽지 못했다"는 신호를 빠르게 캐치하는 것이다.

    핵심 개념 1: 동일 출처 정책과 CORS가 맡는 역할

    브라우저는 기본적으로 동일 출처 정책(Same-Origin Policy)으로 다른 출처의 리소스 접근을 제한한다.

    여기서 출처는 보통 스킴(https) + 호스트 + 포트의 조합이다. 프런트와 백엔드가 분리되면 출처가 달라지는 경우가 흔하다.

    CORS는 이 제한을 무조건 푸는 기능이 아니라, "어떤 출처에서 어떤 방식의 요청을 어떤 조건으로 허용할지"를 서버가 응답 헤더로 명시할 수 있게 만든 규칙 집합이다.

    주의점은 CORS가 서버 방화벽처럼 모든 공격을 막는 정책이 아니라는 점이다. CORS는 주로 브라우저가 응답을 읽어도 되는지 판단하는 규칙이며, 서버 자체의 인증/인가를 대신하지 않는다.

    핵심 개념 2: 프리플라이트(OPTIONS)와 응답 헤더가 결정하는 허용 범위

    일부 요청은 브라우저가 실제 요청 전에 "미리 물어보는 요청"을 보내는데, 이것이 프리플라이트(Preflight)다.

    프리플라이트는 주로 OPTIONS 메서드로 날아가며, 브라우저는 "이 출처에서 이런 메서드/헤더로 요청해도 되나"를 서버에 확인한다.

    서버가 적절한 CORS 응답 헤더를 주지 못하면, 실제 요청이 서버에 도달하기 전 단계에서 막히거나, 실제 요청이 성공해도 브라우저가 응답을 폐기한다.

    주의점은 프리플라이트가 401/403/404/500처럼 실패로 끝나거나, 200이어도 필요한 헤더가 없으면 동일하게 차단될 수 있다는 점이다. 특히 리버스 프락시(Nginx)나 API 게이트웨이가 OPTIONS를 다른 위치로 라우팅해 의도치 않게 막히는 경우가 자주 나온다.

    CORS 문제 해결 흐름: 프론트/백엔드가 분리될 때 자주 터지는 보안 정책
    CORS 동작 구조와 프리플라이트 흐름

    원인별로 증상이 갈리는 지점을 비교표로 나눈다

    같은 "CORS 오류"로 보이더라도 실제 고치는 지점은 다르다. 아래 표는 문제를 분류하기 위한 기준이다.

    항목 프리플라이트가 실패하는 경우 실제 요청은 성공하지만 브라우저가 차단하는 경우 쿠키/인증을 쓰다가 막히는 경우
    대표 증상 Network 탭에 OPTIONS가 보이고 상태코드가 4xx/5xx로 끝난다. 서버 로그는 200인데 콘솔에 CORS 차단 문구가 뜬다. 로그인은 되는데 API 호출만 실패하거나, 특정 브라우저에서만 재현된다.
    주요 원인 OPTIONS 라우팅/인증 처리 오류, Allow-Methods/Headers 누락 Access-Control-Allow-Origin 값 불일치 또는 헤더 미포함 Credentials 설정 불일치, 와일드카드 사용 제약, SameSite 쿠키
    수정 위치 프록시/게이트웨이/백엔드의 OPTIONS 처리 백엔드 응답 헤더 구성(또는 프록시에서 헤더 추가) 프론트 요청 옵션 + 서버의 Allow-Credentials/Origin 정책

    YES/NO 체크로 원인 후보를 빠르게 좁힌다

    • YES: 브라우저 콘솔에 "CORS policy" 문구가 직접 출력되는가?
    • YES: Network 탭에서 OPTIONS 요청이 먼저 나가는가?
    • YES: OPTIONS 응답에 Access-Control-Allow-Origin이 존재하는가?
    • YES: 프런트가 Authorization 같은 커스텀 헤더를 보내는가?
    • YES: 쿠키 기반 인증을 쓰며 요청에 credentials 옵션이 들어가는가?

    여기서 YES가 많을수록 "서버 설정을 바꿔야 하는 CORS"에 가깝다. 반대로 콘솔에 CORS가 아닌 네트워크 오류가 뜨면(타임아웃, DNS, 5xx) 먼저 연결 문제부터 확인하는 편이 효율적이다.

    실전 점검 절차: 개발자 도구로 원인을 단계별로 파악한다

    CORS 문제의 핵심은 "서버가 뭘 줬고, 브라우저가 뭘 요구했는가"를 명확히 보는 것이다.

    1. 콘솔 메시지를 먼저 확보한 다경로: 브라우저 개발자 도구 → Console
    2. 체크 포인트: CORS policy 문구와 대상 URL이 무엇인지 기록한다.
    3. Network에서 요청 단위를 분리한 다경로: 개발자 도구 → Network
    4. 체크 포인트: OPTIONS가 있는지, 실제 GET/POST가 있는지, 상태코드가 무엇인지 확인한다.
    5. 요청의 Origin과 요청 URL을 비교한 다경로: Network → 요청 Headers
    6. 체크 포인트: Origin 값과 요청 대상 호스트가 다른지 확인한다.
    7. 응답 헤더를 확인한 다경로: Network → Response Headers
    8. 체크 포인트: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 유무와 값이 일치하는지 확인한다.
    9. 프락시/백엔드 중 어디서 헤더를 붙이는지 결정한 다경로: 배포 구성도 또는 프락시 설정 파일
    10. 체크 포인트: OPTIONS가 프락시에서 차단되는지, 백엔드까지 도달하는지 확인한다.
    11. 수정 후 재현 환경을 고정해 재검증한 다경로: 캐시 무효화 후 재시도 + 시크릿 창 + 다른 브라우저
    12. 체크 포인트: 동일 요청이 통과하는지, OPTIONS와 실제 요청 모두 헤더가 유지되는지 확인한다.

    이 이미지는 CORS 오류를 프리플라이트부터 헤더 확인까지 6단계로 점검하는 흐름을 설명한다
    CORS 문제 해결 6단계 점검 흐름

    실전 예시: 로컬 개발에서 프런트/백엔드가 포트가 다를 때

    상황: 프런트 개발 서버는 http://localhost:5173, API는 http://localhost:8080일 때 CORS가 터진다.

    나도 처음 겪을 때 "포트만 다른데 왜 막아?" 싶었다. 하지만 브라우저는 포트도 출처의 일부로 본다는 걸 배웠다.

    프런트에서 fetch를 호출하면 다음과 같은 문구가 화면에 보일 수 있다(샘플).

    Access to fetch at 'http://localhost:8080/api/items'
    from origin 'http://localhost:5173' has been blocked by CORS policy:
    No 'Access-Control-Allow-Origin' header is present on the requested resource.

    이 문구는 "서버가 응답은 했지만, 브라우저가 읽을 수 있는 CORS 헤더를 받지 못했다"는 의미로 해석하는 편이 정확하다.

    적합한 선택은 백엔드(또는 프락시)에서 http://localhost:5173을 허용 출처로 명시하고, 필요한 메서드/헤더를 함께 허용하는 구성이다.

    잘못된 선택은 프런트에서만 우회하려고 플러그인이나 브라우저 설정을 만지는 방식이다. 그 방식은 개발자 개인 환경에서는 넘어갈 수 있지만 배포 환경에서는 동일 문제가 재발하는 경우가 많다.

    배포·운영 체크리스트: CORS 설정을 안정적으로 관리하기

    • 허용 출처 리스트화: 개발/스테이징/운영 환경별로 허용할 프런트 도메인을 명시한다.
    • OPTIONS 라우팅 확인: 프락시/게이트웨이가 OPTIONS를 올바르게 백엔드로 라우팅 하는지 확인한다.
    • 필요한 헤더만 허용: Authorization, Content-Type 같은 필요한 커스텀 헤더를 명시한다.
    • 쿠키 정책 일치: 쿠키 인증을 쓸 경우 Credentials와 SameSite 정책을 일관되게 설정한다.
    • 정기 모니터링: CORS 차단 로그를 주기적으로 확인해 의도치 않은 차단이 없는지 본다.

    결론

    CORS 문제 해결 흐름은 "콘솔 문구 확보 → OPTIONS 유무 확인 → 응답 헤더 일치 확인"으로 고정하면 흔들리지 않는다.

    핵심은 브라우저가 막는 이유를 서버 응답 헤더로 입증하는 것이며, 프락시/게이트웨이를 포함해 OPTIONS와 실제 요청의 헤더를 모두 점검하는 것이다.

    이제 CORS 오류가 떠도 당황하지 않고 개발자 도구로 원인을 파악할 수 있어서, 무분별한 설정 변경을 피할 수 있다.

    쿠키 기반 인증이나 커스텀 헤더를 쓰는 순간 프리플라이트와 credentials 조건이 얽히므로, 허용 출처를 구체적으로 관리하는 쪽이 재발을 줄인다.

    다음 글에서는 "CSRF 공격과 CORS의 차이", "SameSite 쿠키로 CSRF 방어하기", "API 게이트웨이에서 CORS를 일괄 관리하는 전략"을 다룰 예정이다. API 보안을 더 깊게 이해하려면 꼭 읽어보길 추천한다!

    FAQ

    Q1. CORS가 서버 보안을 강화해 주는 기능인가?

    CORS는 브라우저가 응답을 읽어도 되는지 판단하는 규칙에 가깝다.

    서버의 인증/인가나 접근 통제를 대신하지 않으므로, API 보안은 별도로 구현되어 있어야 한다.

    Q2. 서버 로그가 200인데도 프런트에서 실패로 나오는 이유는 무엇인가?

    서버가 성공 응답을 주더라도 CORS 응답 헤더가 없거나 값이 맞지 않으면 브라우저가 응답 내용을 차단한다.

    이 경우 프런트는 실제 데이터에 접근하지 못하므로 실패처럼 보일 수 있다.

    Q3. 프리플라이트 OPTIONS가 자꾸 실패할 때 가장 먼저 볼 것은 무엇인가?

    Network 탭에서 OPTIONS 응답 상태코드와 응답 헤더를 먼저 확인하는 편이 빠르다.

    라우팅/인증 미스매치로 OPTIONS가 막히거나, Allow-Methods/Allow-Headers가 빠져 실패하는 경우가 흔하다.

    Q4. 와일드카드(*)로 모든 출처를 허용하면 CORS가 다 해결되는가?

    와일드카드로 모든 출처를 허용하면 CORS 오류는 사라지지만, 쿠키 기반 인증을 쓸 때는 불가능하다.

    프로덕션 환경에서는 정확한 허용 출처 리스트를 관리하는 편이 보안과 운영성 모두에 나쁘지 않다.