티스토리 뷰
웹 개발 여정에서 개발자들을 끊임없이 괴롭히는 난관 중 하나는 바로 "CORS 에러"입니다. 컴퓨터 화면에 붉은색 "CORS Error" 메시지가 선명하게 뜨고, 매번 해결 방법을 찾아 헤매셨던 경험이 있으신가요? 이 가이드는 CORS가 무엇인지부터 왜 발생하며, 어떻게 효과적으로 해결하고 미래에 예방할 수 있는지에 대한 모든 것을 담고 있습니다.
이 글을 통해 프론트엔드 개발 입문자부터 숙련된 백엔드 개발자, 그리고 웹 서비스 기획자까지, 웹 개발 지식 수준에 관계없이 누구나 CORS를 명확히 이해하고 마스터할 수 있을 것입니다. 단순한 문제 해결을 넘어, 웹 보안의 핵심 원리를 이해하고 더 견고한 웹 서비스를 구축하는 데 필요한 지식을 얻게 되실 겁니다. 이제 더 이상 CORS 에러 앞에서 좌절하지 마세요. 함께 정복의 여정을 시작해봅시다!

CORS(교차 출처 리소스 공유)란 무엇인가요?
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 웹 애플리케이션이 다른 출처(Origin)를 가진 리소스에 안전하게 접근할 수 있도록 허용하는 메커니즘입니다. 이 개념을 이해하기 위해 먼저 웹 보안의 근간이 되는 동일 출처 정책(Same-Origin Policy, SOP)부터 살펴봅시다.
동일 출처 정책(Same-Origin Policy, SOP)의 이해
동일 출처 정책(SOP)은 웹 브라우저의 핵심적인 보안 메커니즘입니다. 웹 페이지는 자신과 동일한 출처에서 로드된 리소스만 자유롭게 접근할 수 있도록 제한합니다. 여기서 '출처(Origin)'는 세 가지 요소로 결정됩니다:
- 프로토콜(Protocol):
http://,https:// - 호스트(Host): 도메인 이름 (
www.example.com,api.example.com,localhost) - 포트(Port): 서버 포트 번호 (
:80,:443,:3000,:8080)
이 세 가지 요소 중 단 하나라도 다르면, 브라우저는 해당 리소스를 다른 출처(Cross-Origin)로 간주하고 접근을 제한합니다.
출처(Origin) 비교 예시:
| 요청 출처 (클라이언트) | 대상 출처 (서버) | 동일 출처 여부 | 이유 |
|---|---|---|---|
http://example.com:80/page |
http://example.com:80/data |
O (동일) | 프로토콜, 호스트, 포트 모두 동일 |
http://example.com:80/page |
http://api.example.com:80/data |
X (교차) | 호스트가 다름 (example.com vs api.example.com) |
http://example.com:80/page |
https://example.com:80/data |
X (교차) | 프로토콜이 다름 (http vs https) |
http://example.com:80/page |
http://example.com:8080/data |
X (교차) | 포트가 다름 (80 vs 8080) |
SOP가 존재하는 가장 큰 이유는 보안입니다. 만약 SOP가 없다면, 악의적인 웹사이트(evil.com)가 당신이 로그인한 은행 사이트(mybank.com)나 소셜 미디어(facebook.com)의 데이터를 마음대로 읽어가 개인 정보 유출, 세션 하이재킹 등 심각한 보안 문제로 이어질 수 있습니다.
왜 CORS가 필요한가요?
SOP는 강력한 보안을 제공하지만, 때로는 웹 애플리케이션의 유연성을 저해합니다. 현대 웹은 여러 서비스가 서로 연동되고 데이터를 주고받는 것이 일반적입니다. 예를 들어:
- 프론트엔드와 백엔드의 분리:
frontend.com에서 실행되는 자바스크립트 코드가api.backend.com의 API 서버와 통신해야 할 때. - 서드파티 서비스 통합:
yourshop.com에서 결제 서비스를 위해paymentgateway.com의 API를 호출해야 할 때. - CDN 사용:
yourwebsite.com에서 이미지나 스크립트를cdn.anotherdomain.com에서 로드해야 할 때.
이러한 정당한 교차 출처 요청들은 SOP에 의해 기본적으로 차단됩니다. 여기서 CORS가 등장합니다. CORS는 브라우저와 서버 간의 약속을 통해, 특정 교차 출처 요청을 안전하게 허용할 수 있도록 하는 표준화된 방법입니다. 서버는 Access-Control-Allow-Origin과 같은 특별한 HTTP 응답 헤더를 통해 "이 리소스는 특정 출처에서 접근하는 것을 허용한다"고 브라우저에게 알려주고, 브라우저는 이 헤더를 확인하여 요청을 차단할지, 허용할지를 결정합니다.
CORS의 동작 방식: Simple Request와 Preflight Request
CORS 요청은 크게 두 가지 방식으로 나뉩니다.
- Simple Request (단순 요청):
GET,HEAD,POST메서드 중 하나를 사용합니다.- 사용 가능한 헤더가 제한적입니다 (예:
Accept,Accept-Language,Content-Language,Content-Type중 특정 값). Content-Type은application/x-www-form-urlencoded,multipart/form-data,text/plain중 하나여야 합니다.- 이러한 조건을 만족하는 요청은 브라우저가 사전 확인 없이 바로 보냅니다. 서버는 응답에
Access-Control-Allow-Origin헤더를 포함해야 하고, 브라우저는 이 헤더를 보고 접근 허용 여부를 결정합니다.
- Preflight Request (사전 요청):
- 단순 요청의 조건을 만족하지 않는 모든 교차 출처 요청 (예:
PUT,DELETE메서드, 사용자 정의 헤더 사용,application/jsonContent-Type사용 등). - 브라우저는 실제 요청을 보내기 전에
OPTIONS메서드를 사용하여 사전 요청(Preflight Request)을 먼저 보냅니다. 이 요청은 서버에게 "내가 이런 방식으로 요청을 보낼 건데, 허용해줄 수 있니?"라고 묻는 것입니다. - 서버는 이
OPTIONS요청에 대해Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Max-Age등의 헤더를 포함하여 응답합니다. - 브라우저는 이 사전 응답을 확인하여 실제 요청을 보낼지, 아니면 바로 차단할지를 결정합니다. 사전 요청이 성공하면, 브라우저는 캐시된 정보를 기반으로 일정 시간 동안 다시 사전 요청 없이 실제 요청을 보낼 수 있습니다.
- 단순 요청의 조건을 만족하지 않는 모든 교차 출처 요청 (예:
요약하자면, CORS는 동일 출처 정책이라는 엄격한 보안 규칙 속에서, 합법적인 교차 출처 통신을 허용하기 위한 안전하고 제어된 문(門)을 제공하는 메커니즘입니다. 이 메커니즘을 제대로 이해하지 못하면, 우리는 원인을 알 수 없는 'CORS 에러'라는 벽에 계속 부딪히게 되는 것입니다.
왜 CORS 에러는 계속 발생할까요? 발생 원인 심층 분석
CORS 에러는 개발자들에게 가장 흔하고도 골치 아픈 문제 중 하나입니다. "분명히 서버는 잘 동작하고, 클라이언트 코드도 문제없어 보이는데 왜!"라는 탄식을 자아내게 합니다. 앞서 설명했듯이, CORS 에러는 서버의 문제가 아니라 브라우저의 보안 정책 위반 때문에 발생합니다. 서버가 응답을 제대로 보내지 않거나, 보내더라도 브라우저가 요구하는 특정 CORS 관련 헤더가 없거나 잘못 설정되어 있을 때 브라우저는 해당 응답을 차단하고 에러를 발생시킵니다.
그렇다면 구체적으로 어떤 상황에서 CORS 에러가 발생하는지, 그 원인을 심층적으로 분석해봅시다.
1. 가장 흔한 원인: Access-Control-Allow-Origin 헤더 부재 또는 불일치
이것이 CORS 에러의 90% 이상을 차지한다고 해도 과언이 아닙니다.
- 원인 1.1: 서버가
Access-Control-Allow-Origin헤더를 아예 보내지 않을 때- 클라이언트(브라우저)가
http://localhost:3000에서 실행 중인 웹 페이지라고 가정해봅시다. 이 페이지가http://localhost:8080/api/data와 같은 다른 출처의 API에 데이터를 요청합니다. - 서버는 요청을 성공적으로 처리하고 데이터를 응답으로 보냅니다. 하지만 이 응답에
Access-Control-Allow-Origin헤더가 전혀 포함되어 있지 않습니다. - 브라우저는 "이 응답은 다른 출처에서 왔는데, 서버가 나에게 이 출처의 리소스를 공유해도 좋다고 명시적으로 허락하지 않았군!"이라고 판단합니다. 즉, 서버가
Access-Control-Allow-Origin: http://localhost:3000과 같은 헤더를 보내지 않았기 때문에, 브라우저는 보안상의 이유로 해당 응답을 클라이언트 JavaScript 코드에 전달하지 않고 차단해버립니다.
- 클라이언트(브라우저)가
- 원인 1.2: 서버가
Access-Control-Allow-Origin헤더를 보냈지만, 클라이언트의 출처와 일치하지 않을 때- 이번에는 서버가
Access-Control-Allow-Origin: http://another-origin.com이라고 헤더를 보냈다고 가정해봅시다. - 그러나 실제 요청을 보낸 클라이언트의 출처는
http://localhost:3000입니다. - 브라우저는 서버가 허용한 출처(
http://another-origin.com)와 실제 요청 출처(http://localhost:3000)가 다르다는 것을 확인하고, 역시 응답을 차단합니다. 이는 서버가 특정 출처만 허용하고 있음을 의미합니다.
- 이번에는 서버가
2. Preflight Request (사전 요청) 실패
PUT, DELETE와 같은 HTTP 메서드를 사용하거나, Content-Type: application/json 이외의 복잡한 헤더를 포함하는 등 단순 요청(Simple Request)의 조건을 만족하지 않는 요청은 브라우저가 본 요청(actual request) 전에 OPTIONS 메서드로 사전 요청(Preflight Request)을 보냅니다.
- 원인 2.1: 서버가
OPTIONS요청에 응답하지 않거나, 잘못된 헤더를 보낼 때- 클라이언트가
PUT메서드로 데이터를 전송하려 할 때, 브라우저는 먼저OPTIONS요청을http://localhost:8080/api/resource로 보냅니다. - 서버는 이
OPTIONS요청에 대해 다음과 같은 CORS 관련 헤더를 포함하여 응답해야 합니다:Access-Control-Allow-Origin: 허용할 클라이언트 출처Access-Control-Allow-Methods: 허용할 HTTP 메서드 (예:GET, POST, PUT, DELETE)Access-Control-Allow-Headers: 허용할 요청 헤더 (예:Content-Type, Authorization)Access-Control-Max-Age: 사전 요청 결과를 캐시할 시간
- 만약 서버가
OPTIONS요청에 아예 응답하지 않거나 (404 Not Found), 필수 헤더 중 하나라도 누락시키거나 (예:Access-Control-Allow-Methods에PUT이 없음), 혹은 200 OK 또는 204 No Content 상태 코드를 반환하지 않으면, 브라우저는 사전 요청이 실패했다고 판단하고 본 요청을 보내지 않고 CORS 에러를 발생시킵니다.
- 클라이언트가
3. 인증 정보(Credentials) 관련 문제
클라이언트가 fetch API나 axios를 사용하여 요청을 보낼 때, 쿠키, HTTP 인증 헤더(Authorization), 클라이언트 SSL 인증서와 같은 인증 정보(credentials)를 포함해야 하는 경우가 있습니다.
- 원인 3.1:
Access-Control-Allow-Credentials헤더 부재 또는Allow-Origin: *과 함께 사용할 때- 클라이언트 코드에서
fetch('url', { credentials: 'include' })또는axios.defaults.withCredentials = true;와 같이 인증 정보를 포함하도록 설정한 경우, 서버는 반드시 응답 헤더에Access-Control-Allow-Credentials: true를 포함해야 합니다. - 또한,
Access-Control-Allow-Credentials: true헤더는Access-Control-Allow-Origin: *(와일드카드)와 함께 사용할 수 없습니다. 보안상의 이유로 와일드카드 출처 허용과 인증 정보 공유는 상충하기 때문입니다. 인증 정보가 포함된 요청의 경우, 서버는 반드시Access-Control-Allow-Origin에 구체적인 클라이언트 출처를 명시해야 합니다. - 이 조건을 위반하면 브라우저는 요청을 차단하고 CORS 에러를 발생시킵니다.
- 클라이언트 코드에서
4. 기타 간과하기 쉬운 원인들
- 리다이렉션: 서버가 요청에 대해 리다이렉션(예: 301, 302 상태 코드) 응답을 보낼 때, 리다이렉트된 최종 URL의 출처가 원래 CORS 정책을 따르지 않으면 문제가 발생할 수 있습니다.
- 방화벽 또는 프록시 설정: 네트워크 환경이나 서버 앞단의 방화벽, 로드 밸런서, API 게이트웨이 등이 CORS 헤더를 제거하거나 수정하는 경우.
- 오류 응답: 서버에서 에러가 발생하여 예상치 못한 응답(예: 500 Internal Server Error)을 보낼 때, 이 오류 응답 자체에 CORS 헤더가 포함되지 않아 브라우저가 이를 CORS 에러로 오인할 수 있습니다. 실제로는 서버의 로직 문제인데, 브라우저가 CORS 문제로 표시하는 것입니다.
- 브라우저 캐시: Preflight Request의
Access-Control-Max-Age설정 때문에 변경된 CORS 정책이 브라우저에 즉시 반영되지 않고, 이전 설정이 캐시되어 에러가 발생하기도 합니다. 브라우저 캐시를 지우거나Access-Control-Max-Age를 짧게 설정하여 테스트해야 합니다.
이처럼 CORS 에러는 단순히 "서버가 말을 듣지 않는다"는 문제가 아니라, 웹 보안을 위한 브라우저의 엄격한 규칙과 서버의 응답 간의 불일치에서 비롯됩니다. 문제를 해결하려면 클라이언트와 서버, 그리고 그 사이의 네트워크 통신을 전체적으로 이해하고 분석해야 합니다.
흔히 마주치는 CORS 에러 메시지 유형과 의미
CORS 에러가 발생하면, 웹 브라우저의 개발자 도구(Console 탭)에 붉은색 에러 메시지가 표시됩니다. 이 메시지들은 단순히 에러가 났다는 것을 넘어, 무엇이 문제인지에 대한 중요한 힌트를 담고 있습니다. 각 메시지의 의미를 정확히 파악하면 문제 해결 시간을 크게 단축할 수 있습니다.
여기서 몇 가지 대표적인 CORS 에러 메시지 유형과 그 의미를 살펴보겠습니다.
1. "No 'Access-Control-Allow-Origin' header is present on the requested resource."
- 에러 메시지 전문 예시:
Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.- 의미:
이것은 가장 흔하게 접하는 CORS 에러입니다. 요청을 보낸 서버가 응답 헤더에Access-Control-Allow-Origin헤더를 아예 포함하지 않았다는 뜻입니다. 브라우저는 클라이언트(http://localhost:3000)가http://localhost:8080이라는 다른 출처의 리소스에 접근하려 하는데, 서버가 이를 허용하는 명시적인 선언(즉,Access-Control-Allow-Origin헤더)을 하지 않았기 때문에, 보안 정책에 따라 응답을 차단한 것입니다. - 해결책 방향:
서버 측에서Access-Control-Allow-Origin헤더를 적절하게 설정해야 합니다. 클라이언트의 출처(http://localhost:3000)를 이 헤더의 값으로 지정해주어야 합니다.
2. "The 'Access-Control-Allow-Origin' header has a value '...' that is not equal to the supplied origin."
- 에러 메시지 전문 예시:
Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'http://another-frontend.com' that is not equal to the supplied origin.- 의미:
이 에러는 서버가Access-Control-Allow-Origin헤더를 보내기는 했지만, 그 값(http://another-frontend.com)이 실제 요청을 보낸 클라이언트의 출처(http://localhost:3000)와 일치하지 않는다는 의미입니다. 서버가 특정 출처만 허용하도록 설정되어 있는데, 당신의 클라이언트 출처는 그 목록에 없다는 뜻입니다. - 해결책 방향:
서버 측에서Access-Control-Allow-Origin헤더의 값을 현재 클라이언트의 출처로 수정하거나, 여러 출처를 허용하도록 설정을 확장해야 합니다.
3. "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Headers' header is present on the requested resource." 또는 유사한 preflight 에러
- 에러 메시지 전문 예시:
Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status. Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Headers' header is present on the requested resource.- 의미:
이 에러는 브라우저가 실제 요청을 보내기 전에 보낸OPTIONS사전 요청(Preflight Request)이 실패했다는 것을 의미합니다.- 첫 번째 예시(
No 'Access-Control-Allow-Headers'): 서버가OPTIONS요청에 대한 응답으로Access-Control-Allow-Headers헤더를 포함하지 않았거나, 클라이언트가 보내려는 사용자 정의 헤더가 이 목록에 없다는 뜻입니다. - 두 번째 예시(
It does not have HTTP ok status): 서버가OPTIONS요청에 대해 200 OK 상태 코드(HTTP 204 No Content도 가능)로 응답하지 않았다는 의미입니다. 이는 서버에OPTIONS메서드를 처리하는 라우터나 미들웨어가 없거나, 네트워크/방화벽 문제로OPTIONS요청 자체가 서버에 도달하지 못했을 가능성도 있습니다.
- 첫 번째 예시(
- 해결책 방향:
서버 측에서OPTIONS메서드에 대한 라우터를 설정하고,Access-Control-Allow-Headers,Access-Control-Allow-Methods,Access-Control-Allow-Origin,Access-Control-Max-Age와 같은 CORS 관련 헤더들을 올바르게 포함하여 200 OK (또는 204 No Content) 응답을 보내도록 설정해야 합니다.
4. "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'."
- 에러 메시지 전문 예시:
Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.- 의미:
클라이언트 코드에서credentials: 'include'옵션을 사용하여 쿠키나 인증 헤더(예:Authorization헤더)를 요청에 포함시켰는데, 서버가Access-Control-Allow-Origin: *(와일드카드)와 함께Access-Control-Allow-Credentials: true헤더를 보냈다는 의미입니다. 보안상의 이유로, 브라우저는 와일드카드 출처 허용과 인증 정보 공유를 동시에 허용하지 않습니다. - 해결책 방향:
서버 측에서Access-Control-Allow-Origin헤더에*대신 구체적인 클라이언트 출처(예:http://localhost:3000)를 명시해야 합니다. 그리고Access-Control-Allow-Credentials: true헤더도 함께 보내야 합니다.
5. "CORS policy: It does not have HTTP ok status." (서버 에러가 CORS 에러로 보이는 경우)
- 에러 메시지 전문 예시:또는 (실제 요청이 서버 에러로 인해 실패할 때)(개발자 도구 콘솔에서 이 에러와 함께 "has been blocked by CORS policy" 메시지가 동반될 수 있습니다.)
Failed to load resource: the server responded with a status of 500 (Internal Server Error)Access to XMLHttpRequest at 'http://localhost:8080/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request does not have HTTP ok status.- 의미:
이 메시지는 사전 요청이 200 OK 상태 코드로 응답받지 못했을 때 나타납니다. 하지만 때로는 실제 서버 로직에서 에러(예: 404 Not Found, 500 Internal Server Error)가 발생하여 서버가OPTIONS요청을 제대로 처리하지 못하거나, 본 요청에 대한 응답이 CORS 헤더를 포함하지 않은 채 에러 상태 코드로 돌아올 때도 발생합니다. 이 경우, 본질적으로는 서버의 로직 에러임에도 불구하고 브라우저가 이를 CORS 정책 위반으로 해석하여 표시할 수 있습니다. - 해결책 방향:
서버 로그를 확인하여 실제 어떤 에러가 발생했는지 파악해야 합니다.OPTIONS요청 처리 로직에 문제가 없는지, 또는 API 엔드포인트 자체가 유효한지 확인하고, 에러 응답에도 최소한의 CORS 헤더가 포함되도록 설정하는 것이 좋습니다.
CORS 에러 메시지를 마주쳤을 때 당황하지 마세요. 각 메시지는 문제를 해결할 수 있는 소중한 단서입니다. 메시지를 주의 깊게 읽고, 위에서 설명한 원인 및 해결책 방향과 연결시켜보면 의외로 쉽게 답을 찾을 수 있을 것입니다.
CORS 에러 해결 마스터하기: 클라이언트/서버별 실전 해결책
CORS 에러는 클라이언트 단독, 서버 단독, 또는 둘 다의 설정 문제로 발생할 수 있습니다. 가장 이상적인 해결책은 서버에서 CORS 정책을 올바르게 설정하는 것이지만, 개발 환경이나 특정 상황에서는 클라이언트 측에서 임시적인 해결책을 사용하기도 합니다. 여기서는 클라이언트와 서버 양측에서 CORS 에러를 해결하는 실전적인 방법과 코드 예시를 제공합니다.
1. 클라이언트 측 해결책 (주로 개발 환경에서 활용)
클라이언트 측 해결책은 브라우저의 CORS 정책을 우회하거나, 브라우저가 요청을 동일 출처로 인식하게 만드는 방법입니다. 운영 환경에서는 서버 측 해결책을 사용하는 것이 훨씬 바람직합니다.
1.1. 프록시(Proxy) 서버 사용
가장 보편적이고 안정적인 클라이언트 측 해결책입니다. 프록시 서버는 클라이언트(브라우저)와 실제 API 서버 사이에 위치하여, 브라우저의 요청을 대신 API 서버로 전달하고, API 서버의 응답을 다시 브라우저로 전달하는 중개자 역할을 합니다.
브라우저 입장에서는 요청이 자신의 웹 서버(동일 출처)로 가는 것처럼 보이므로 CORS 정책 위반이 발생하지 않습니다. 실제 API 요청은 프록시 서버가 백엔드 서버로 보내게 됩니다.
- 프론트엔드 개발 환경에서의 프록시 (Webpack Dev Server, Vite 등)
React 프로젝트의create-react-app이나 Vue 프로젝트에서vue-cli를 통해 생성된 프로젝트는 개발 서버에 프록시 기능을 쉽게 설정할 수 있습니다.// package.json { "name": "my-frontend-app", "version": "0.1.0", "private": true, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:8080" // <--- 이 부분이 핵심입니다! }"proxy": "http://localhost:8080"설정을 추가하면,http://localhost:3000에서 실행되는 프론트엔드 개발 서버는http://localhost:3000/api/data와 같은 요청이 들어오면 이를http://localhost:8080/api/data로 대신 전달해줍니다. 브라우저는http://localhost:3000으로 요청했다고 생각하므로 CORS 에러가 발생하지 않습니다.
- 예시:
package.json에 프록시 설정 (React, Vue CLI) - 운영 환경에서의 프록시 (Nginx, Apache)
운영 환경에서는 Nginx나 Apache와 같은 웹 서버를 리버스 프록시(Reverse Proxy)로 설정하여 CORS 문제를 해결할 수 있습니다.# nginx.conf (또는 sites-available/default 등) server { listen 80; server_name your-frontend.com; location / { # 프론트엔드 정적 파일 서빙 root /var/www/your-frontend; index index.html; try_files $uri $uri/ /index.html; } location /api/ { # /api 경로로 들어오는 요청을 백엔드 서버로 프록시 proxy_pass http://localhost:8080; # 백엔드 서버 주소 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }- 이 설정은
your-frontend.com/api/로 들어오는 요청을http://localhost:8080(백엔드 서버)로 전달합니다. 브라우저는your-frontend.com으로 요청했다고 생각하므로 CORS 에러가 발생하지 않습니다.
- 이 설정은
- 예시: Nginx 설정
1.2. 크롬 확장 프로그램 (개발 시에만)
개발 단계에서 급하게 CORS 문제를 우회해야 할 때, Allow CORS: Access-Control-Allow-Origin과 같은 크롬 확장 프로그램을 사용할 수 있습니다. 그러나 이는 브라우저의 보안 정책을 강제로 해제하는 것이므로, 절대 운영 환경에서 사용해서는 안 됩니다. 또한, 팀원 간의 개발 환경을 통일하기 어렵고, 실제 사용자 환경에서는 작동하지 않으므로 근본적인 해결책이 될 수 없습니다.
2. 서버 측 해결책 (가장 중요하고 권장되는 방법)
서버에서 CORS 정책을 명시적으로 설정하여 브라우저에게 "이 출처에서의 접근을 허용한다"고 알려주는 것이 CORS 에러의 가장 올바르고 안정적인 해결책입니다.
2.1. Access-Control-Allow-Origin 헤더 설정
서버는 HTTP 응답 헤더에 Access-Control-Allow-Origin을 포함하여 특정 출처를 허용해야 합니다.
- 특정 출처 허용:
가장 안전한 방법입니다. 요청을 보내는 클라이언트의 정확한 출처를 명시합니다. Access-Control-Allow-Origin: http://localhost:3000- 여러 출처 허용 (동적 처리):
여러 클라이언트 출처를 허용해야 할 경우, 요청 헤더의Origin값을 확인하여 허용된 목록에 있는 경우에만 해당Origin값을Access-Control-Allow-Origin헤더로 설정합니다. // Node.js (Express) 예시: 동적으로 여러 출처 허용 const allowedOrigins = ['http://localhost:3000', 'https://www.my-prod-app.com']; app.use((req, res, next) => { const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Allow-Credentials', 'true'); // 인증 정보 포함 시 next(); });- 모든 출처 허용 (와일드카드
*)Access-Control-Allow-Origin: *로 설정하면 모든 출처의 요청을 허용합니다. 보안상 매우 위험하며, 민감한 데이터가 포함된 API에는 절대 사용해서는 안 됩니다. 주로 공개 API나 개발 단계에서만 제한적으로 사용됩니다. 또한,Access-Control-Allow-Credentials: true와는 함께 사용할 수 없습니다. Access-Control-Allow-Origin: *
2.2. Preflight Request (OPTIONS) 처리
PUT, DELETE, 사용자 정의 헤더 등을 사용하는 복잡한 요청의 경우, 브라우저가 OPTIONS 사전 요청을 먼저 보냅니다. 서버는 이 OPTIONS 요청에 대해 다음과 같은 헤더를 포함하여 응답해야 합니다.
Access-Control-Allow-Origin: 허용할 클라이언트 출처Access-Control-Allow-Methods: 허용할 HTTP 메서드 (예:GET, POST, PUT, DELETE, OPTIONS)Access-Control-Allow-Headers: 허용할 요청 헤더 (예:Content-Type, Authorization, X-Custom-Header)Access-Control-Max-Age: 사전 요청 결과를 캐시할 시간(초). 이 시간 동안은OPTIONS요청을 다시 보내지 않습니다.
대부분의 웹 프레임워크는 이러한 OPTIONS 요청 처리를 위한 미들웨어 또는 설정 기능을 제공합니다.
2.3. 프레임워크별 CORS 설정 방법 및 코드 예시
각 서버 프레임워크는 CORS를 쉽게 설정할 수 있는 기능을 제공합니다.
Node.js (Express)
cors npm 패키지를 사용하는 것이 가장 일반적이고 간편합니다.
// app.js (Express 서버)
const express = require('express');
const cors = require('cors'); // CORS 미들웨어 임포트
const app = express();
const port = 8080;
// Option 1: 모든 출처 허용 (개발 환경에서만 사용 권장, 보안에 주의!)
// app.use(cors());
// Option 2: 특정 출처만 허용
const corsOptions = {
origin: 'http://localhost:3000', // 프론트엔드 애플리케이션의 주소
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 허용할 HTTP 메서드
allowedHeaders: ['Content-Type', 'Authorization'], // 허용할 요청 헤더
credentials: true, // 인증 정보(쿠키, Authorization 헤더 등) 허용 여부
optionsSuccessStatus: 200 // 일부 레거시 브라우저(IE11, SmartTV 등)는 204에 문제가 있을 수 있어 200으로 설정
};
app.use(cors(corsOptions));
// Option 3: 여러 출처를 동적으로 허용 (보안성 높음)
// const allowedOrigins = ['http://localhost:3000', 'https://www.my-prod-app.com'];
// app.use(cors({
// origin: function (origin, callback) {
// // origin이 없는 요청 허용 (예: Postman, curl, 모바일 앱)
// if (!origin) return callback(null, true);
// if (allowedOrigins.indexOf(origin) === -1) {
// const msg = `The CORS policy for this site does not allow access from the specified Origin: ${origin}`;
// return callback(new Error(msg), false);
// }
// return callback(null, true);
// },
// methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// allowedHeaders: ['Content-Type', 'Authorization'],
// credentials: true,
// optionsSuccessStatus: 200
// }));
// API 라우트
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from Node.js API!' });
});
app.post('/api/items', (req, res) => {
// Post 요청 처리 로직
res.status(201).json({ message: 'Item created!' });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Python (Flask)
Flask-CORS 확장 패키지를 사용하여 CORS를 설정할 수 있습니다.
# app.py (Flask 서버)
from flask import Flask, jsonify, request
from flask_cors import CORS # Flask-CORS 임포트
app = Flask(__name__)
# Option 1: 모든 출처 허용 (개발 환경에서만 사용 권장)
# CORS(app)
# Option 2: 특정 출처만 허용 (전역 설정)
# CORS(app, origins="http://localhost:3000")
# Option 3: 특정 경로에 대해 특정 출처 허용
# CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
# Option 4: 여러 출처를 동적으로 허용 (더욱 안전한 운영 환경 설정)
allowed_origins = ["http://localhost:3000", "https://www.my-prod-app.com"]
def is_origin_allowed(origin, _): # Flask-CORS의 origins 인자는 origin과 credentials 인자를 받음
if not origin: # Postman, curl, 모바일 앱 등 Origin 헤더가 없는 요청 허용
return True
return origin in allowed_origins
CORS(app, origins=is_origin_allowed, supports_credentials=True)
@app.route('/api/data', methods=['GET'])
def get_data():
return jsonify({'message': 'Hello from Flask API!'})
@app.route('/api/items', methods=['POST'])
def create_item():
# Post 요청 처리 로직
return jsonify({'message': 'Item created!'}), 201
if __name__ == '__main__':
app.run(port=8080)
Java (Spring Boot)
Spring Boot는 @CrossOrigin 어노테이션이나 WebMvcConfigurer를 통해 CORS를 유연하게 설정할 수 있습니다.
// MyController.java (Spring Boot 컨트롤러)
package com.example.demo;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
// Option 1: 컨트롤러 전체에 CORS 설정 적용
// @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
public class MyController {
// Option 2: 특정 메서드에만 CORS 설정 적용
@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
@GetMapping("/api/data")
public String getData() {
return "Hello from Spring Boot API!";
}
@CrossOrigin(origins = "http://localhost:3000", methods = { "POST", "OPTIONS" }, allowCredentials = "true")
@PostMapping("/api/items")
public String createItem() {
return "Item created!";
}
}
// CorsConfig.java (Spring Boot 전역 CORS 설정 - 더 유연하고 권장됨)
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // CORS를 적용할 경로 패턴
.allowedOrigins("http://localhost:3000", "https://www.my-prod-app.com") // 허용할 출처 목록
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
.allowedHeaders("*") // 모든 헤더 허용
.allowCredentials(true) // 인증 정보(쿠키, Authorization 헤더 등) 허용
.maxAge(3600); // Preflight 요청 결과를 3600초(1시간) 동안 캐시
}
};
}
}
CORS 에러를 해결하는 핵심은 결국 서버가 브라우저의 CORS 정책을 이해하고, 그에 맞는 응답 헤더를 적절하게 보내주는 것입니다. 클라이언트 측 해결책은 개발 편의를 위한 임시 방편으로 생각하고, 최종적으로는 서버에서 견고한 CORS 설정을 구축하는 것이 중요합니다.
CORS 에러 방지를 위한 모범 사례 및 설계 전략
CORS 에러는 대부분 개발 초기 단계에서 제대로 고려하지 않아 발생하는 경우가 많습니다. "일단 기능부터 만들고 보자"는 생각으로 진행하다가, 막상 배포 단계나 복잡한 기능 구현 시점에 CORS 문제에 부딪히면 해결이 더 어려워집니다. 개발 초기부터 CORS를 염두에 두고 설계하면 많은 시간과 노력을 절약할 수 있습니다. 다음은 CORS 에러를 효과적으로 방지하기 위한 모범 사례 및 설계 전략입니다.
1. API 설계 단계에서 출처 일관성 유지
가능하다면, 클라이언트 애플리케이션과 API 서버가 동일한 출처(Same-Origin)를 사용하도록 설계하는 것이 가장 좋습니다. 이는 브라우저의 동일 출처 정책을 자연스럽게 만족시켜 CORS 문제를 원천적으로 방지합니다.
- 가장 이상적인 예시:
- 프론트엔드:
https://yourdomain.com(또는https://www.yourdomain.com) - 백엔드 API:
https://yourdomain.com/api(리버스 프록시를 통해 내부 백엔드 서버로 라우팅)
이처럼 프론트엔드와 백엔드 API가 동일한 프로토콜, 호스트, 포트를 사용하도록 구성하면 CORS 에러를 원천적으로 방지할 수 있습니다.
- 프론트엔드:
- 서브도메인 사용 시 주의:
- 프론트엔드:
https://www.yourdomain.com - 백엔드 API:
https://api.yourdomain.com
이 경우,www.yourdomain.com에서api.yourdomain.com으로의 요청은 '호스트' 요소(www와api)가 다르므로 교차 출처(Cross-Origin) 요청으로 간주됩니다. 따라서 이 경우에도 CORS 설정이 필요하며, API 게이트웨이나 서버 측에서Access-Control-Allow-Origin: https://www.yourdomain.com과 같이 명시적으로 허용해야 합니다.document.domain을 이용한 방식은 보안상 취약점이 있어 최신 웹 환경에서는 권장되지 않습니다.
- 프론트엔드:
2. API 게이트웨이 또는 리버스 프록시를 통한 중앙 집중식 관리
모든 API 요청이 단일 진입점(예: Nginx, Apache, AWS API Gateway)을 통과하도록 설계하고, 이 게이트웨이 또는 프록시 단에서 CORS 헤더를 일괄적으로 관리하는 것은 매우 효과적인 전략입니다.
- 장점:
- 단일 책임 원칙: 각 마이크로서비스나 백엔드 서버는 CORS 설정을 신경 쓸 필요 없이 비즈니스 로직에만 집중할 수 있습니다.
- 일관된 정책: 모든 API에 대해 일관된 CORS 정책을 적용할 수 있습니다.
- 쉬운 관리: CORS 정책 변경 시 게이트웨이 설정만 수정하면 됩니다.
- 보안 강화: 잘못된 CORS 설정으로 인한 보안 취약점 발생 가능성을 줄입니다.
- 예시 (Nginx 리버스 프록시):
server { listen 443 ssl; server_name api.yourdomain.com; # SSL 설정 ... location / { # CORS 헤더 추가 (모든 API 응답에 적용) add_header 'Access-Control-Allow-Origin' 'https://www.yourdomain.com' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Max-Age' 3600 always; # OPTIONS 요청 처리 (Preflight Request) if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' 'https://www.yourdomain.com' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Max-Age' 3600 always; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 204; # 204 No Content로 응답 } proxy_pass http://your_backend_service:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
3. 개발 환경과 운영 환경 CORS 정책 분리
개발 환경에서는 여러 개발자의 로컬 환경이나 테스트 서버에서 API를 호출해야 하므로, 비교적 유연한 CORS 정책을 적용할 수 있습니다. 하지만 운영 환경에서는 보안을 최우선으로 하여 매우 엄격한 정책을 적용해야 합니다.
- 개발 환경:
Access-Control-Allow-Origin: http://localhost:3000, http://127.0.0.1:3000등 개발용 출처들을 허용하거나, 필요시*(와일드카드)를 제한적으로 사용할 수 있습니다. - 운영 환경:
Access-Control-Allow-Origin: https://www.yourdomain.com과 같이 정확하게 허용할 도메인만 명시해야 합니다.*는 절대 사용하지 않아야 합니다.
대부분의 프레임워크는 환경 변수(예: NODE_ENV, SPRING_PROFILES_ACTIVE)를 기반으로 설정을 다르게 로드하는 기능을 제공합니다. 이를 활용하여 환경별로 다른 CORS 정책을 적용하세요.
4. 인증 방식(JWT, 세션 쿠키 등)과 CORS의 연동 시 주의사항
API 요청에 사용자 인증 정보(예: JWT 토큰을 담은 Authorization 헤더, 세션 쿠키)가 포함될 경우, CORS 설정에 특히 주의해야 합니다.
Access-Control-Allow-Credentials: true헤더를 반드시 설정해야 합니다. 이 헤더가 없으면 브라우저는 교차 출처 요청에 쿠키나 인증 헤더를 포함시키지 않습니다.Access-Control-Allow-Credentials: true를 사용할 경우,Access-Control-Allow-Origin헤더에*(와일드카드)를 사용할 수 없습니다. 반드시 허용할 출처를 명시적으로 지정해야 합니다. 이는 보안상의 이유로 중요한 제약 사항입니다.
5. API 문서화 및 테스트
API 개발 시 CORS 정책을 명확히 문서화해야 합니다.
- 각 API 엔드포인트가 어떤 CORS 정책을 따르는지, 어떤 출처를 허용하는지, 어떤 HTTP 메서드와 헤더를 지원하는지를 명시합니다.
- CORS 테스트 케이스를 포함하여, 다양한 시나리오(동일 출처, 허용된 교차 출처, 허용되지 않은 교차 출처 등)에서 API가 올바르게 동작하는지 확인합니다. Postman이나
curl같은 도구로OPTIONS요청을 직접 보내서 서버 응답 헤더를 확인하는 것도 좋은 방법입니다.
6. 최소 권한의 원칙 (Principle of Least Privilege)
CORS 정책을 설정할 때도 최소 권한의 원칙을 따르는 것이 좋습니다. 필요한 최소한의 출처, 메서드, 헤더만 허용하고, 그 외의 모든 것은 기본적으로 차단합니다. 이는 잠재적인 보안 위협을 줄이는 가장 기본적인 방법입니다.
CORS는 단순히 "성가신 에러"가 아니라, 웹 보안의 필수적인 부분입니다. 위의 모범 사례와 설계 전략을 따른다면, CORS 에러로 인한 좌절을 줄이고 더욱 견고하고 안전한 웹 서비스를 구축할 수 있을 것입니다.
결론: CORS, 이제는 이해하고 안전하게 활용하자!
지금까지 악명 높은 CORS 에러의 정체부터 시작하여, 그 근본적인 원인인 동일 출처 정책(SOP)을 이해하고, 다양한 에러 메시지의 의미를 파악하는 방법을 알아보았습니다. 또한, 클라이언트와 서버 양측에서 CORS 에러를 해결하는 실질적인 코드 예시와 함께, 개발 초기 단계부터 CORS 에러를 방지할 수 있는 모범 사례 및 설계 전략까지 심도 있게 다루었습니다.
CORS 에러는 더 이상 당신을 좌절시키는 존재가 아닙니다. 이는 웹 애플리케이션의 보안을 강화하기 위한 브라우저의 중요한 경고 신호이며, 서버가 응답 헤더를 통해 브라우저와 "안전하게 리소스를 공유할 방법을 의논"하는 과정입니다. 이 과정을 이해하고 올바르게 설정하는 것은 현대 웹 개발자에게 필수적인 역량입니다.
이 가이드에서 제시된 지식과 실전 해결책들을 통해, 이제 당신은 CORS 에러를 두려워하지 않고 능숙하게 대처하며, 나아가 웹 보안을 고려한 견고한 아키텍처를 설계할 수 있는 역량을 갖추게 되었을 것입니다. 복잡해 보이는 문제도 원리를 이해하고 단계적으로 접근하면 해결할 수 있다는 자신감을 얻으셨기를 바랍니다.
웹 개발의 여정에서 CORS는 이제 당신의 든든한 조력자가 될 것입니다. 이 글이 당신의 웹 개발 여정에 큰 도움이 되었기를 바라며, 궁금한 점이나 추가적인 해결책이 있다면 언제든지 댓글로 공유해주세요!
참고 자료:
'DEV' 카테고리의 다른 글
| Stateful vs Stateless: 개발 비전공자도 완벽 이해하는 핵심 개념 (0) | 2026.01.27 |
|---|---|
| 크롬 PNA와 CORS: 웹 보안 이해부터 완벽 해결까지 마스터하기 (0) | 2026.01.27 |
| CORS 보안 취약점 예방: 안전하고 견고한 웹 서비스 구축을 위한 완벽 가이드 (0) | 2026.01.27 |
| REST API 완벽 가이드: 구성, 특징부터 실전 통신까지 (비전공자 눈높이 설명) (0) | 2026.01.27 |
| 잦은 로그인 풀림? 장바구니 유실? 웹 서비스 세션 불일치 문제 완벽 해결 가이드 (0) | 2026.01.27 |
- Total
- Today
- Yesterday
- SEO최적화
- 개발생산성
- AI기술
- 개발자성장
- 개발자가이드
- 데이터베이스
- LLM
- 마이크로서비스
- 생성형AI
- 웹보안
- 인공지능
- springai
- Java
- 프론트엔드개발
- 배민
- 미래ai
- 클라우드컴퓨팅
- 성능최적화
- n8n
- restapi
- 업무자동화
- 자바개발
- 로드밸런싱
- 개발가이드
- 프롬프트엔지니어링
- 백엔드개발
- 클린코드
- 웹개발
- AI반도체
- AI
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
