이미지 로딩 중...

CORS와 교차 출처 리소스 공유 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 24. · 5 Views

CORS와 교차 출처 리소스 공유 완벽 가이드

웹 개발에서 가장 많이 마주치는 CORS 오류, 이제 완벽하게 이해하고 해결해보세요. Same-Origin Policy부터 Preflight 요청, CORS 헤더 설정까지 실무에서 바로 활용할 수 있는 모든 것을 다룹니다.


목차

  1. CORS란 무엇인가?
  2. Same-Origin Policy 이해하기
  3. Preflight 요청의 역할
  4. CORS 헤더 종류와 설정
  5. 실전 CORS 문제 해결
  6. 프론트엔드-백엔드 CORS 설정 예제

1. CORS란 무엇인가?

시작하며

여러분이 프론트엔드 개발을 하다가 "Access to fetch has been blocked by CORS policy"라는 빨간 에러 메시지를 본 적 있나요? 분명히 API는 정상적으로 작동하는데, 브라우저에서만 접근이 막히는 이 황당한 상황 말이죠.

이런 문제는 특히 프론트엔드와 백엔드를 분리해서 개발할 때 거의 매번 발생합니다. localhost:3000에서 실행 중인 React 앱이 localhost:5000의 백엔드 API를 호출하려고 하면 브라우저가 이를 차단해버립니다.

심지어 Postman에서는 잘 되는데 브라우저에서만 안 되니 더 혼란스럽죠. 바로 이럴 때 필요한 것이 CORS(Cross-Origin Resource Sharing)에 대한 이해입니다.

CORS를 제대로 이해하면 이런 에러를 근본적으로 해결할 수 있을 뿐만 아니라, 웹 보안의 핵심 원리까지 알게 됩니다.

개요

간단히 말해서, CORS는 브라우저가 다른 출처(origin)의 리소스를 안전하게 공유할 수 있도록 허락하는 메커니즘입니다. 여기서 '다른 출처'란 프로토콜, 도메인, 포트 중 하나라도 다른 것을 의미합니다.

웹 브라우저는 기본적으로 보안상의 이유로 다른 출처의 리소스 요청을 차단합니다. 만약 이런 제한이 없다면, 악의적인 웹사이트가 여러분이 로그인한 은행 사이트의 정보를 몰래 가져갈 수 있겠죠?

하지만 현대 웹 개발에서는 프론트엔드와 백엔드를 분리하거나, 외부 API를 사용하는 경우가 많아서 이런 교차 출처 요청이 필수적입니다. 기존에는 같은 서버에서 모든 것을 처리해야 했다면, 이제는 CORS를 통해 안전하게 다른 출처의 리소스를 사용할 수 있습니다.

서버가 "이 출처는 내 리소스에 접근해도 괜찮아"라고 명시적으로 허락하는 방식이죠. CORS의 핵심 특징은 첫째, 브라우저 레벨에서 작동한다는 점(서버 간 통신에는 적용되지 않음), 둘째, 서버가 명시적으로 허용해야 한다는 점, 셋째, 안전하지 않은 요청에는 사전 확인(Preflight) 과정을 거친다는 점입니다.

이러한 특징들이 웹 애플리케이션의 보안을 유지하면서도 유연한 개발을 가능하게 합니다.

코드 예제

// 클라이언트 측: fetch API로 다른 출처에 요청
fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
  // CORS 에러가 여기서 발생할 수 있습니다
  console.error('CORS 에러:', error);
});

설명

이것이 하는 일: 위 코드는 프론트엔드에서 외부 API에 데이터를 요청하는 가장 기본적인 형태입니다. 하지만 이 간단한 요청이 성공하려면 서버 측에서 CORS를 허용해야 합니다.

첫 번째로, fetch 함수가 실행되면 브라우저는 요청의 출처(origin)를 확인합니다. 만약 현재 페이지가 https://myapp.com에서 실행 중이고, 요청 대상이 https://api.example.com이라면 출처가 다르므로 CORS 검사가 시작됩니다.

브라우저는 자동으로 Origin 헤더를 요청에 추가하여 "나는 myapp.com에서 왔어요"라고 알려줍니다. 그 다음으로, 서버가 응답을 보낼 때 Access-Control-Allow-Origin 헤더를 포함해야 합니다.

이 헤더가 없거나 현재 출처를 포함하지 않으면 브라우저는 응답을 차단합니다. 서버에서 "Access-Control-Allow-Origin: https://myapp.com" 또는 "Access-Control-Allow-Origin: *"를 응답 헤더에 포함하면 브라우저가 이를 확인하고 접근을 허용합니다.

마지막으로, then 블록이 실행되면서 정상적으로 데이터를 받아 처리합니다. 만약 CORS 정책 위반으로 요청이 차단되면 catch 블록으로 이동하여 에러가 발생합니다.

중요한 점은 이 모든 과정이 브라우저에 의해 자동으로 처리된다는 것입니다. 여러분이 이 코드를 사용하면 외부 API와 안전하게 통신할 수 있습니다.

하지만 CORS 에러를 만나면 당황하지 말고, 서버 측 설정을 확인하거나 백엔드 개발자와 협업하여 적절한 CORS 헤더를 추가해야 합니다. 또한 개발 환경에서는 프록시를 사용하여 CORS 문제를 우회할 수도 있습니다.

실전 팁

💡 Postman이나 curl에서는 잘 되는데 브라우저에서만 안 된다면 100% CORS 문제입니다. 이런 도구들은 브라우저가 아니므로 CORS 정책을 적용하지 않기 때문입니다.

💡 개발 중에는 브라우저 확장 프로그램으로 CORS를 우회할 수 있지만, 프로덕션에서는 반드시 서버 측에서 올바르게 설정해야 합니다.

💡 CORS 에러 메시지를 자세히 읽어보세요. "No 'Access-Control-Allow-Origin' header is present"라면 서버 설정 문제이고, "The value of the header is not the requested origin"이라면 헤더는 있지만 값이 맞지 않는 것입니다.

💡 개발 환경에서는 Create React App의 proxy 설정이나 Vite의 proxy 기능을 활용하면 CORS 없이 개발할 수 있습니다.

💡 네트워크 탭에서 요청을 확인할 때 상태 코드가 200이어도 CORS 에러가 날 수 있습니다. 응답은 성공했지만 브라우저가 접근을 차단한 것이기 때문입니다.


2. Same-Origin Policy 이해하기

시작하며

여러분이 온라인 뱅킹 사이트에 로그인한 상태에서, 다른 탭에서 악의적인 웹사이트를 방문했다고 상상해보세요. 만약 브라우저에 보안 정책이 없다면 그 악의적인 사이트가 여러분의 뱅킹 정보를 몰래 가져갈 수 있을까요?

이런 위험을 막기 위해 브라우저는 Same-Origin Policy(동일 출처 정책)라는 강력한 보안 메커니즘을 가지고 있습니다. 이것은 웹 보안의 가장 기본이 되는 원칙으로, 1995년 Netscape Navigator 2.0부터 도입되었습니다.

거의 30년 가까이 웹을 지켜온 핵심 보안 정책이죠. CORS를 이해하려면 먼저 Same-Origin Policy를 알아야 합니다.

CORS는 사실 Same-Origin Policy를 안전하게 우회하는 방법이기 때문입니다.

개요

간단히 말해서, Same-Origin Policy는 한 출처에서 로드된 문서나 스크립트가 다른 출처의 리소스와 상호작용하는 것을 제한하는 브라우저의 보안 정책입니다. 여기서 '같은 출처'란 프로토콜(scheme), 호스트(host), 포트(port)가 모두 같은 경우를 의미합니다.

이 정책이 필요한 이유는 명확합니다. 만약 example.com에서 실행 중인 악의적인 스크립트가 bank.com의 쿠키나 로컬 스토리지에 접근할 수 있다면 큰 보안 문제가 발생합니다.

사용자가 은행 사이트에 로그인한 상태라면, 그 세션 정보를 이용해 계좌 정보를 훔치거나 송금을 할 수도 있겠죠. 기존에는 모든 리소스를 같은 서버에서 제공해야 했다면, 이제는 CORS를 통해 필요한 경우에만 선택적으로 다른 출처의 접근을 허용할 수 있습니다.

하지만 기본은 여전히 "모두 차단"이며, 명시적으로 허용한 것만 통과시킵니다. 출처 비교의 핵심 특징은 첫째, 매우 엄격하다는 점(포트가 하나만 달라도 다른 출처), 둘째, 브라우저가 자동으로 검사한다는 점, 셋째, 일부 태그(img, script, link 등)는 예외적으로 허용한다는 점입니다.

이러한 특징들이 웹의 보안과 유연성 사이의 균형을 맞춰줍니다.

코드 예제

// 같은 출처 판단 예제
const currentOrigin = 'https://example.com:443';

// 같은 출처인 경우들
const sameOrigins = [
  'https://example.com:443/page1',  // 경로만 다름 → 같은 출처
  'https://example.com:443/api/data' // 경로만 다름 → 같은 출처
];

// 다른 출처인 경우들
const differentOrigins = [
  'http://example.com:443',   // 프로토콜 다름 (http vs https)
  'https://example.com:8080', // 포트 다름 (443 vs 8080)
  'https://api.example.com',  // 호스트 다름 (서브도메인 포함)
  'https://example.org'       // 도메인 다름
];

// 브라우저가 자동으로 이런 검사를 수행합니다
console.log('같은 출처:', sameOrigins);
console.log('다른 출처 (CORS 필요):', differentOrigins);

설명

이것이 하는 일: 위 코드는 브라우저가 어떤 기준으로 출처를 비교하는지 보여줍니다. 실제로는 개발자가 직접 이런 비교를 할 필요는 없지만, 어떤 경우에 CORS가 필요한지 이해하는 데 도움이 됩니다.

첫 번째로, currentOrigin을 기준으로 다양한 URL들을 비교해봅니다. 브라우저는 요청을 보낼 때마다 자동으로 이런 비교를 수행합니다.

프로토콜(https), 호스트(example.com), 포트(443) 세 가지를 모두 확인하여 하나라도 다르면 "다른 출처"로 판단합니다. 그 다음으로, sameOrigins 배열의 경우들을 보면 경로(/page1, /api/data)만 다를 뿐 프로토콜, 호스트, 포트는 동일합니다.

이런 경우는 Same-Origin으로 간주되어 아무런 제약 없이 자유롭게 리소스에 접근할 수 있습니다. 쿠키, 로컬 스토리지, DOM 접근 등이 모두 가능하죠.

마지막으로, differentOrigins 배열을 보면 각각 다른 이유로 "다른 출처"로 판단됩니다. http://example.com은 프로토콜이 다르고(https가 아닌 http), https://example.com:8080은 포트가 다릅니다(443이 아닌 8080).

특히 주의할 점은 api.example.com처럼 서브도메인이 추가되어도 완전히 다른 출처로 간주된다는 것입니다. 여러분이 이 원리를 이해하면 왜 CORS 에러가 발생하는지 명확히 알 수 있습니다.

localhost:3000에서 개발 중인 프론트엔드가 localhost:5000의 백엔드 API를 호출할 때 포트가 다르므로 CORS가 필요한 것이죠. 또한 프로덕션에서 www.myapp.com과 api.myapp.com을 분리하여 운영할 때도 호스트가 다르므로 CORS 설정이 필수입니다.

실전 팁

💡 HTTPS에서 HTTP 리소스를 요청하면 CORS 이전에 Mixed Content 에러가 발생합니다. 보안상 더 강력한 제약이므로 CORS로도 해결할 수 없습니다.

💡 localhost와 127.0.0.1은 기술적으로 다른 호스트입니다. 개발 중 하나는 되는데 다른 하나는 안 된다면 이것 때문일 수 있습니다.

💡 포트를 명시하지 않으면 기본 포트가 사용됩니다(http는 80, https는 443). https://example.com과 https://example.com:443은 같은 출처입니다.

💡 IE 11 이하 버전은 포트를 출처 비교에 포함하지 않는 버그가 있습니다. 레거시 브라우저 지원 시 주의하세요.

💡 file:// 프로토콜로 연 HTML 파일은 모든 다른 파일을 다른 출처로 간주합니다. 로컬 개발 시 반드시 로컬 서버를 사용해야 하는 이유입니다.


3. Preflight 요청의 역할

시작하며

여러분이 DELETE 메서드로 API를 호출했는데, 네트워크 탭을 보니 같은 URL로 OPTIONS 메서드 요청이 먼저 보내진 것을 본 적 있나요? "나는 DELETE를 보냈는데 왜 OPTIONS가 먼저 가지?"라고 의아해하셨을 겁니다.

이것은 버그가 아니라 브라우저가 안전을 위해 자동으로 수행하는 "사전 확인" 과정입니다. 특히 데이터를 변경하거나 삭제하는 요청의 경우, 브라우저는 먼저 "이 요청을 보내도 되는지" 서버에게 물어봅니다.

실제 요청을 보내기 전에 미리 허가를 받는 것이죠. 바로 이것이 Preflight 요청입니다.

모든 CORS 요청에 Preflight가 발생하는 것은 아니지만, 서버의 데이터를 변경할 수 있는 "위험한" 요청에는 반드시 Preflight 과정을 거칩니다.

개요

간단히 말해서, Preflight 요청은 실제 요청을 보내기 전에 OPTIONS 메서드로 서버에게 "이런 요청을 보내도 되나요?"라고 물어보는 사전 확인 과정입니다. 서버가 "괜찮아요"라고 답하면 그때야 실제 요청을 보냅니다.

모든 요청에 Preflight가 발생하는 것은 아닙니다. Simple Request(단순 요청)라고 불리는 조건을 만족하면 Preflight 없이 바로 요청을 보냅니다.

하지만 커스텀 헤더를 사용하거나, PUT/DELETE/PATCH 같은 메서드를 사용하거나, application/json 같은 Content-Type을 사용하면 Preflight가 발생합니다. 실무에서는 대부분의 API 요청이 이 조건에 해당하므로 Preflight를 자주 보게 됩니다.

기존에는 서버가 단순히 요청에 응답만 하면 됐다면, 이제는 OPTIONS 요청에도 적절히 응답해야 합니다. Preflight 응답이 제대로 오지 않으면 실제 요청은 아예 보내지지도 않습니다.

Preflight의 핵심 특징은 첫째, 자동으로 발생한다는 점(개발자가 코드로 만들지 않음), 둘째, OPTIONS 메서드를 사용한다는 점, 셋째, 캐시될 수 있다는 점입니다(Access-Control-Max-Age 헤더 사용). 이러한 특징들이 보안을 유지하면서도 성능을 최적화할 수 있게 해줍니다.

코드 예제

// 프론트엔드: Preflight를 발생시키는 요청 예제
fetch('https://api.example.com/users/123', {
  method: 'DELETE',  // Simple Request가 아닌 메서드
  headers: {
    'Content-Type': 'application/json',  // Simple Request가 아닌 Content-Type
    'Authorization': 'Bearer token123'    // 커스텀 헤더
  }
})
.then(response => response.json())
.then(data => console.log('삭제 성공:', data));

// 브라우저가 자동으로 먼저 보내는 Preflight 요청:
// OPTIONS /users/123
// Origin: https://myapp.com
// Access-Control-Request-Method: DELETE
// Access-Control-Request-Headers: content-type, authorization

설명

이것이 하는 일: 위 코드는 DELETE 요청을 보내는 간단한 예제지만, 실제로는 두 번의 HTTP 요청이 발생합니다. 브라우저가 자동으로 Preflight를 먼저 보내고, 서버의 허가를 받은 후에야 실제 DELETE 요청을 보내기 때문입니다.

첫 번째로, fetch를 호출하면 브라우저는 이 요청이 Simple Request 조건을 만족하는지 확인합니다. DELETE 메서드는 GET/POST/HEAD가 아니므로 조건을 만족하지 않습니다.

Authorization 헤더도 Simple Request에서 허용되지 않는 커스텀 헤더입니다. 따라서 브라우저는 "이 요청은 위험할 수 있으니 먼저 확인해야겠다"고 판단합니다.

그 다음으로, 브라우저가 자동으로 OPTIONS 요청을 먼저 보냅니다. 이 요청에는 실제로 사용할 메서드(DELETE)와 헤더(content-type, authorization)가 무엇인지 알려주는 특별한 헤더들이 포함됩니다.

서버는 이 정보를 보고 "이 출처에서 이런 메서드와 헤더를 사용해도 되는지" 판단하여 응답합니다. 마지막으로, 서버가 적절한 CORS 헤더(Access-Control-Allow-Methods, Access-Control-Allow-Headers 등)와 함께 200 OK를 응답하면, 브라우저는 "허가를 받았구나"라고 판단하고 실제 DELETE 요청을 보냅니다.

만약 Preflight 응답이 적절하지 않으면 여기서 멈추고 실제 요청은 보내지지 않습니다. 이것이 서버를 보호하는 핵심 메커니즘입니다.

여러분이 이 과정을 이해하면 왜 네트워크 탭에 두 번의 요청이 보이는지 알 수 있습니다. 또한 서버 개발 시 OPTIONS 메서드에 대한 라우트도 반드시 처리해야 한다는 것을 알게 됩니다.

Preflight 응답을 캐싱하면(Access-Control-Max-Age 사용) 매번 Preflight를 보내지 않아도 되므로 성능도 개선할 수 있습니다.

실전 팁

💡 Simple Request 조건: GET/POST/HEAD 메서드, Accept/Accept-Language/Content-Language/Content-Type(application/x-www-form-urlencoded, multipart/form-data, text/plain만) 헤더만 사용, 커스텀 헤더 없음

💡 실무에서는 거의 모든 API 요청이 application/json을 사용하므로 Preflight가 발생합니다. 이것은 정상이며 피할 수 없습니다.

💡 OPTIONS 요청이 실패하면 실제 요청은 보내지지 않으므로, 서버 로그에서 실제 요청을 찾을 수 없을 수 있습니다. OPTIONS 요청부터 확인하세요.

💡 Access-Control-Max-Age를 86400(24시간)으로 설정하면 같은 종류의 요청에 대해 24시간 동안 Preflight를 다시 보내지 않습니다.

💡 개발 환경에서 Preflight 요청이 너무 많아 느리다면, 캐싱을 활용하거나 개발 중에만 프록시를 사용하여 우회할 수 있습니다.


4. CORS 헤더 종류와 설정

시작하며

여러분이 백엔드 개발자에게 "CORS 좀 열어주세요"라고 요청했을 때, "어떤 헤더를 어떻게 설정해야 하나요?"라는 질문을 받은 적 있나요? Access-Control-Allow-Origin만 설정하면 될 줄 알았는데, 막상 해보니 여러 가지 헤더를 설정해야 한다는 것을 알게 됩니다.

CORS는 생각보다 많은 헤더를 사용하며, 각 헤더마다 명확한 역할이 있습니다. Origin을 허용하는 헤더, 메서드를 허용하는 헤더, 헤더를 허용하는 헤더, 쿠키를 허용하는 헤더 등 다양합니다.

상황에 따라 필요한 헤더가 다르므로 각각을 정확히 이해해야 합니다. 바로 이 섹션에서는 실무에서 사용하는 모든 CORS 헤더를 상세히 알아보고, 어떤 상황에 어떤 헤더를 사용해야 하는지 배웁니다.

개요

간단히 말해서, CORS 헤더는 서버가 브라우저에게 "어떤 출처에서, 어떤 메서드로, 어떤 헤더를 사용하여 접근해도 되는지"를 알려주는 응답 헤더들입니다. 이 헤더들을 통해 Same-Origin Policy의 제약을 안전하게 완화할 수 있습니다.

가장 중요한 헤더는 Access-Control-Allow-Origin입니다. 이것이 없으면 다른 모든 헤더가 있어도 소용없습니다.

하지만 이것만으로는 부족한 경우가 많습니다. 예를 들어 DELETE 메서드를 사용하려면 Access-Control-Allow-Methods도 필요하고, Authorization 헤더를 보내려면 Access-Control-Allow-Headers도 필요합니다.

쿠키를 주고받으려면 Access-Control-Allow-Credentials도 true로 설정해야 하죠. 기존에는 복잡한 보안 설정을 직접 구현해야 했다면, 이제는 CORS 헤더 몇 개로 간단히 해결할 수 있습니다.

Express의 cors 미들웨어나 Spring의 @CrossOrigin 어노테이션처럼 프레임워크가 제공하는 도구를 사용하면 더욱 쉽습니다. CORS 헤더의 핵심 특징은 첫째, 서버 응답에 포함된다는 점(클라이언트가 아닌 서버가 설정), 둘째, 와일드카드(*)를 지원하지만 제약이 있다는 점(credentials 사용 시 *불가), 셋째, Preflight와 실제 요청에 각각 필요한 헤더가 다르다는 점입니다.

이러한 특징들을 이해하면 정확한 CORS 설정이 가능합니다.

코드 예제

// Node.js Express 서버의 CORS 설정 예제
const express = require('express');
const app = express();

// 모든 요청에 CORS 헤더 추가 (미들웨어 방식)
app.use((req, res, next) => {
  // 1. 허용할 출처 설정 (가장 중요!)
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');

  // 2. 허용할 HTTP 메서드 설정
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');

  // 3. 허용할 요청 헤더 설정
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // 4. 쿠키/인증 정보 허용 (credentials 사용 시 필수)
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  // 5. Preflight 캐싱 시간 (초 단위, 최대 24시간)
  res.setHeader('Access-Control-Max-Age', '86400');

  // 6. 클라이언트에 노출할 응답 헤더 설정
  res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count');

  // Preflight 요청(OPTIONS)은 여기서 바로 응답
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

// 실제 API 라우트
app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS가 적용된 데이터' });
});

app.listen(5000, () => console.log('서버 시작: http://localhost:5000'));

설명

이것이 하는 일: 위 코드는 Express 서버에서 CORS를 완벽하게 설정하는 실무 예제입니다. 모든 요청에 대해 적절한 CORS 헤더를 추가하여 크로스 오리진 요청을 안전하게 허용합니다.

첫 번째로, Access-Control-Allow-Origin 헤더로 허용할 출처를 명시합니다. 'https://myapp.com'만 허용하므로 다른 출처의 요청은 차단됩니다.

'*'를 사용하면 모든 출처를 허용하지만, 보안상 위험하고 credentials와 함께 사용할 수 없으므로 특정 출처를 명시하는 것이 좋습니다. 여러 출처를 허용하려면 요청의 Origin 헤더를 확인하여 동적으로 설정해야 합니다.

그 다음으로, Access-Control-Allow-Methods와 Access-Control-Allow-Headers로 허용할 메서드와 헤더를 지정합니다. Preflight 요청에서 브라우저가 "DELETE 메서드와 Authorization 헤더를 사용하겠다"고 알려오면, 서버는 이 헤더들을 확인하여 허용 여부를 판단합니다.

여기에 명시되지 않은 메서드나 헤더를 사용하면 요청이 차단됩니다. 세 번째로, Access-Control-Allow-Credentials를 'true'로 설정하면 쿠키나 Authorization 헤더를 포함한 인증 정보를 주고받을 수 있습니다.

이것을 설정할 때는 Allow-Origin을 '*'로 할 수 없으며 반드시 구체적인 출처를 명시해야 합니다. 클라이언트 측에서도 fetch의 credentials 옵션을 'include'로 설정해야 합니다.

네 번째로, Access-Control-Max-Age는 Preflight 응답을 캐싱할 시간을 초 단위로 지정합니다. 86400초(24시간)로 설정하면 같은 종류의 요청에 대해 24시간 동안 Preflight를 다시 보내지 않아 성능이 향상됩니다.

브라우저마다 최대값이 다르므로 너무 큰 값은 의미가 없습니다. 마지막으로, OPTIONS 메서드로 들어온 Preflight 요청은 실제 데이터 처리 없이 204 No Content로 즉시 응답합니다.

이것은 "네, 그 요청 보내셔도 됩니다"라는 의미입니다. Access-Control-Expose-Headers는 클라이언트 JavaScript에서 접근할 수 있는 응답 헤더를 지정합니다(기본적으로 일부 헤더만 접근 가능).

여러분이 이 코드를 사용하면 완벽한 CORS 설정을 할 수 있습니다. 실무에서는 cors 패키지를 사용하면 더 간단하게 설정할 수 있지만, 내부적으로는 위와 같은 헤더를 설정하는 것입니다.

보안을 위해 절대 '*'를 남발하지 말고, 필요한 출처와 메서드만 최소한으로 허용하세요.

실전 팁

💡 Access-Control-Allow-Origin에 여러 도메인을 직접 나열할 수 없습니다. 동적으로 확인하여 설정하거나, 여러 번 setHeader를 호출해도 마지막 값만 적용되므로 주의하세요.

💡 실무에서는 환경 변수로 허용할 출처 목록을 관리하고, 요청의 Origin 헤더와 비교하여 동적으로 설정하는 패턴을 많이 사용합니다.

💡 Access-Control-Expose-Headers를 설정하지 않으면 커스텀 응답 헤더를 JavaScript에서 읽을 수 없습니다. X-Total-Count 같은 메타데이터를 전달할 때 필수입니다.

💡 nginx나 Apache를 사용한다면 서버 레벨에서 CORS 헤더를 추가할 수도 있습니다. 애플리케이션 코드보다 앞단에서 처리되므로 성능상 유리합니다.

💡 OPTIONS 요청에 인증 미들웨어를 적용하면 Preflight가 실패할 수 있습니다. OPTIONS는 인증 없이 통과시켜야 합니다.


5. 실전 CORS 문제 해결

시작하며

여러분이 프로젝트를 배포했는데 갑자기 CORS 에러가 발생한 적 있나요? 로컬에서는 잘 되던 것이 프로덕션에서만 안 되거나, 반대로 프로덕션은 되는데 로컬에서만 안 되는 경우도 있습니다.

CORS 에러 메시지도 다양해서 어디서부터 해결해야 할지 막막합니다. 실무에서 CORS 문제는 정말 자주 발생하며, 각 상황마다 원인과 해결법이 다릅니다.

"Access to fetch has been blocked", "Response to preflight request doesn't pass", "The value of the header is not the requested origin" 등 다양한 에러 메시지가 있고, 각각 다른 문제를 의미합니다. 바로 이 섹션에서는 실무에서 자주 만나는 CORS 문제들을 상황별로 진단하고 해결하는 방법을 배웁니다.

에러 메시지를 읽고 정확히 파악하는 법, 네트워크 탭 활용법, 그리고 단계별 해결 전략까지 모두 다룹니다.

개요

간단히 말해서, CORS 문제 해결의 핵심은 에러 메시지를 정확히 읽고, 요청과 응답을 자세히 분석하며, 클라이언트와 서버 양쪽을 모두 확인하는 것입니다. CORS는 서버 설정 문제인 경우가 95% 이상이지만, 클라이언트의 잘못된 요청 때문인 경우도 있습니다.

가장 흔한 문제는 서버에서 Access-Control-Allow-Origin 헤더를 아예 설정하지 않은 경우입니다. 두 번째로 흔한 것은 헤더는 있지만 값이 요청 출처와 일치하지 않는 경우입니다.

세 번째는 Preflight 요청에 제대로 응답하지 않는 경우이고, 네 번째는 credentials를 사용하면서 '*'를 허용한 경우입니다. 기존에는 에러가 발생하면 무작정 인터넷을 검색하거나 라이브러리를 추가했다면, 이제는 체계적으로 문제를 진단하고 정확한 지점을 고칠 수 있습니다.

브라우저 개발자 도구의 네트워크 탭, 콘솔 메시지, 서버 로그를 종합적으로 활용하면 대부분의 CORS 문제를 10분 안에 해결할 수 있습니다. CORS 디버깅의 핵심 특징은 첫째, 네트워크 탭이 가장 중요한 도구라는 점(요청/응답 헤더를 모두 확인), 둘째, OPTIONS 요청부터 확인해야 한다는 점(Preflight 실패 시 실제 요청은 보내지지 않음), 셋째, 서버 로그도 함께 봐야 한다는 점입니다.

이러한 도구들을 활용하면 문제를 빠르게 찾을 수 있습니다.

코드 예제

// CORS 문제 진단을 위한 체크리스트 코드
// 1. 브라우저 콘솔에서 정확한 에러 메시지 확인
console.log('1단계: 에러 메시지 유형 파악');

// 2. 네트워크 탭에서 요청 확인
// - OPTIONS 요청이 있는가? → Preflight 발생
// - OPTIONS 요청의 상태 코드는? → 200/204가 아니면 문제
// - OPTIONS 응답 헤더에 Access-Control-* 헤더들이 있는가?

// 3. 클라이언트 요청 코드 검증
fetch('https://api.example.com/data', {
  method: 'POST',
  credentials: 'include',  // 쿠키 포함 시 서버에서도 credentials: true 필요
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'  // 커스텀 헤더는 서버에서 명시적 허용 필요
  },
  body: JSON.stringify({ data: 'test' })
})
.then(res => {
  // 4. 응답 헤더 확인 (개발자 도구에서)
  console.log('4단계: 응답 헤더에서 Access-Control-Allow-Origin 확인');
  return res.json();
})
.catch(err => {
  console.error('CORS 에러 발생:', err);
  // 5. 에러 메시지별 해결법
  // "No 'Access-Control-Allow-Origin'" → 서버에 헤더 추가
  // "not the requested origin" → 서버의 Allow-Origin 값 확인
  // "preflight request doesn't pass" → OPTIONS 요청 처리 확인
  // "credentials mode is 'include'" → * 대신 구체적 출처 설정
});

// 6. 임시 해결책: 개발 중 프록시 사용 (Create React App 예시)
// package.json에 추가:
// "proxy": "http://localhost:5000"
// 이제 /api/data로 요청하면 자동으로 localhost:5000/api/data로 프록시됨

설명

이것이 하는 일: 위 코드는 CORS 문제를 체계적으로 진단하는 단계별 체크리스트입니다. 실제로 실행하는 코드라기보다는 문제 해결 프로세스를 보여주는 가이드입니다.

첫 번째로, 브라우저 콘솔의 정확한 에러 메시지를 확인합니다. "Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com' has been blocked by CORS policy" 같은 메시지가 나오는데, 이 메시지 끝 부분이 핵심입니다.

"No 'Access-Control-Allow-Origin' header is present"라면 서버에서 아예 헤더를 안 보낸 것이고, "The value of the header is 'https://other.com' but it must be 'https://myapp.com'"이라면 헤더는 있지만 값이 틀린 것입니다. 그 다음으로, 네트워크 탭을 열어 OPTIONS 요청이 있는지 확인합니다.

Preflight가 발생하는 요청인데 OPTIONS가 보이지 않는다면 서버에 도달하기 전에 차단된 것입니다(예: 네트워크 문제, HTTPS/HTTP 혼용). OPTIONS 요청이 보인다면 상태 코드를 확인하세요.

200이나 204가 아니라 404나 500이면 서버에서 OPTIONS 요청을 처리하는 라우트가 없거나 에러가 발생한 것입니다. 세 번째로, OPTIONS 응답 헤더를 자세히 봅니다.

Access-Control-Allow-Methods에 여러분이 사용하려는 메서드(예: DELETE)가 포함되어 있나요? Access-Control-Allow-Headers에 여러분이 보내는 커스텀 헤더(예: Authorization)가 포함되어 있나요?

하나라도 빠지면 실제 요청이 차단됩니다. 네 번째로, credentials: 'include'를 사용한다면 서버의 Access-Control-Allow-Origin이 구체적인 출처여야 합니다.

'*'로 설정되어 있으면 "The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'" 같은 에러가 발생합니다. 마지막으로, 개발 환경에서만 CORS 문제를 겪는다면 프록시를 사용하는 것도 좋은 해결책입니다.

Create React App은 package.json의 proxy 설정으로, Vite는 vite.config.js의 server.proxy로 간단히 설정할 수 있습니다. 이렇게 하면 개발 서버가 중간에서 요청을 전달하므로 브라우저 입장에서는 같은 출처 요청이 됩니다.

여러분이 이 체크리스트를 따라가면 대부분의 CORS 문제를 빠르게 해결할 수 있습니다. 핵심은 "어디서" 막혔는지 정확히 파악하는 것입니다.

Preflight에서 막혔는지, 실제 요청에서 막혔는지, 서버 설정 문제인지, 클라이언트 요청 문제인지 구분할 수 있어야 합니다.

실전 팁

💡 curl이나 Postman으로는 되는데 브라우저에서만 안 된다면 100% CORS 문제입니다. 이런 도구들은 브라우저가 아니므로 CORS를 검사하지 않습니다.

💡 크롬 확장 프로그램 "CORS Unblock"이나 "Allow CORS"는 개발 중 임시 해결에만 사용하세요. 프로덕션에서는 반드시 서버 설정으로 해결해야 합니다.

💡 "ERR_FAILED" 같은 에러는 CORS가 아니라 네트워크 문제입니다. 서버가 아예 응답하지 않는 것이므로 서버 상태부터 확인하세요.

💡 프로덕션에서 갑자기 CORS 에러가 발생했다면 최근 배포한 코드에서 CORS 설정이 빠졌거나, nginx/CloudFront 같은 프록시 설정이 변경된 것일 수 있습니다.

💡 여러 도메인을 허용해야 한다면 서버에서 요청의 Origin 헤더를 확인하여 화이트리스트에 있을 때만 해당 출처를 응답 헤더에 설정하는 패턴을 사용하세요.


6. 프론트엔드-백엔드 CORS 설정 예제

시작하며

여러분이 React + Node.js로 풀스택 프로젝트를 시작할 때, 로컬에서 프론트엔드는 localhost:3000, 백엔드는 localhost:5000으로 실행하죠? 그리고 첫 API 호출에서 바로 CORS 에러를 만나게 됩니다.

"이제 뭘 해야 하지?" 이것은 모든 풀스택 개발자가 겪는 통과의례입니다. 프론트엔드와 백엔드를 각각 설정해야 하고, 개발 환경과 프로덕션 환경의 설정도 달라야 합니다.

로컬 개발 시에는 프록시를 사용할지, CORS를 직접 설정할지도 고민해야 하죠. 바로 이 섹션에서는 실전 프로젝트에서 바로 사용할 수 있는 완전한 CORS 설정 예제를 프론트엔드와 백엔드 양쪽 모두 제공합니다.

React, Vue, Express, NestJS 등 다양한 스택에 적용할 수 있습니다.

개요

간단히 말해서, 프론트엔드-백엔드 CORS 설정은 클라이언트가 올바른 요청을 보내고, 서버가 적절한 헤더로 응답하도록 양쪽을 모두 설정하는 것입니다. 개발 환경에서는 편의성을, 프로덕션에서는 보안을 우선시해야 합니다.

프론트엔드에서는 credentials 옵션, 요청 헤더, 그리고 개발 중 프록시 설정을 신경 써야 합니다. fetch나 axios 같은 HTTP 클라이언트를 사용할 때 올바른 옵션을 설정해야 쿠키나 인증 토큰이 제대로 전달됩니다.

백엔드에서는 CORS 미들웨어를 적용하고, 환경별로 다른 설정을 사용하며, OPTIONS 요청을 적절히 처리해야 합니다. 기존에는 각 프레임워크마다 CORS 설정법이 달라 매번 문서를 찾아봐야 했다면, 이제는 핵심 원리를 이해했으므로 어떤 스택이든 쉽게 적용할 수 있습니다.

Express든 Django든 Spring Boot든 결국 같은 HTTP 헤더를 설정하는 것이기 때문입니다. 풀스택 CORS 설정의 핵심 특징은 첫째, 환경 변수로 출처를 관리한다는 점(하드코딩 금지), 둘째, 개발 환경에서는 유연하게 프로덕션에서는 엄격하게 설정한다는 점, 셋째, 프론트엔드와 백엔드 설정이 서로 일치해야 한다는 점입니다.

이러한 특징들을 고려하면 안전하고 유지보수하기 쉬운 설정을 만들 수 있습니다.

코드 예제

// ===== 백엔드 (Node.js Express) =====
// server.js
const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();

// CORS 설정 객체
const corsOptions = {
  // 환경에 따라 다른 출처 허용
  origin: function (origin, callback) {
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [
      'http://localhost:3000',  // 개발 환경
      'https://myapp.com'       // 프로덕션
    ];

    // origin이 undefined인 경우는 같은 출처 요청 (허용)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS 정책에 의해 차단됨'));
    }
  },
  credentials: true,  // 쿠키/인증 정보 허용
  optionsSuccessStatus: 200  // IE11 등 레거시 브라우저 지원
};

app.use(cors(corsOptions));
app.use(express.json());

// API 라우트
app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS 설정 완료', data: [1, 2, 3] });
});

app.listen(5000, () => console.log('서버: http://localhost:5000'));

// ===== 프론트엔드 (React) =====
// api.js - API 클라이언트 설정
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000';

export async function fetchData() {
  try {
    const response = await fetch(`${API_BASE_URL}/api/data`, {
      method: 'GET',
      credentials: 'include',  // 쿠키 포함 (서버의 credentials: true와 짝)
      headers: {
        'Content-Type': 'application/json',
        // 인증 토큰이 있다면
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('API 요청 실패:', error);
    throw error;
  }
}

// ===== 개발 환경 프록시 (선택사항) =====
// package.json (Create React App)
// "proxy": "http://localhost:5000"
// 이제 /api/data로 요청하면 자동으로 localhost:5000/api/data로 전달됨

설명

이것이 하는 일: 위 코드는 실무에서 바로 사용할 수 있는 완전한 프론트엔드-백엔드 CORS 설정 예제입니다. 개발과 프로덕션 환경 모두에서 안전하게 작동합니다.

첫 번째로, 백엔드에서 cors 미들웨어를 사용하여 CORS를 설정합니다. origin 옵션에 함수를 전달하여 동적으로 출처를 검증합니다.

환경 변수 ALLOWED_ORIGINS에 쉼표로 구분된 출처 목록을 설정하면, 그 중 하나와 일치할 때만 접근을 허용합니다. 개발 환경에서는 localhost:3000을, 프로덕션에서는 실제 도메인을 포함하면 됩니다.

그 다음으로, credentials: true 설정으로 쿠키와 인증 정보를 허용합니다. 이것을 설정하면 프론트엔드에서 credentials: 'include'로 보낸 쿠키나 HTTP 인증 정보를 받을 수 있습니다.

JWT를 쿠키에 저장하여 사용하는 경우 필수 설정입니다. 이 옵션을 사용할 때는 origin을 '*'로 할 수 없으므로 반드시 구체적인 출처를 지정해야 합니다.

세 번째로, 프론트엔드에서 API_BASE_URL을 환경 변수로 관리합니다. 개발 환경에서는 localhost:5000을, 프로덕션에서는 https://api.myapp.com 같은 실제 API 주소를 사용합니다.

.env 파일에 REACT_APP_API_URL을 설정하면 빌드 시 자동으로 교체됩니다. 네 번째로, fetch 요청에 credentials: 'include'를 설정하여 쿠키를 포함합니다.

또한 Authorization 헤더로 JWT 토큰을 전달합니다. 서버에서 이 헤더를 허용하려면 Access-Control-Allow-Headers에 'Authorization'이 포함되어야 하는데, cors 미들웨어가 자동으로 처리해줍니다.

마지막으로, 개발 환경에서는 package.json의 proxy 설정을 사용할 수도 있습니다. 이렇게 하면 /api/data로 요청해도 개발 서버가 자동으로 localhost:5000/api/data로 프록시해줍니다.

브라우저 입장에서는 같은 출처 요청이므로 CORS 문제가 아예 발생하지 않습니다. 프로덕션 빌드에서는 이 설정이 무시되므로, 프로덕션에서는 CORS 설정이 필요합니다.

여러분이 이 코드를 사용하면 로컬 개발부터 프로덕션 배포까지 CORS 문제 없이 진행할 수 있습니다. 환경 변수를 잘 관리하고, 프론트엔드와 백엔드 설정이 일치하는지 확인하세요.

특히 credentials 설정은 양쪽이 모두 일치해야 작동합니다.

실전 팁

💡 .env 파일에 ALLOWED_ORIGINS="http://localhost:3000,https://myapp.com,https://www.myapp.com" 형식으로 여러 출처를 관리하면 편리합니다.

💡 개발 중에는 프록시를 사용하고, 프로덕션에서만 CORS를 설정하는 것이 가장 간단한 방법입니다. 하지만 프로덕션과 동일한 환경에서 테스트하려면 개발 환경에서도 CORS를 설정하세요.

💡 Vite를 사용한다면 vite.config.js에서 server.proxy를 설정할 수 있습니다: server: { proxy: { '/api': 'http://localhost:5000' } }

💡 Next.js의 경우 next.config.js의 rewrites를 사용하거나, API Routes를 중간 프록시로 활용할 수 있습니다.

💡 nginx를 사용한다면 프록시와 CORS 헤더를 nginx 설정 파일에서 한 번에 처리할 수 있어 애플리케이션 코드가 더 깔끔해집니다.


#JavaScript#CORS#API#HTTP#WebSecurity#API,CORS,보안

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

WebSocket과 Server-Sent Events 실시간 통신 완벽 가이드

웹 애플리케이션에서 실시간 데이터 통신을 구현하는 핵심 기술인 WebSocket과 Server-Sent Events를 다룹니다. 채팅, 알림, 실시간 업데이트 등 현대 웹 서비스의 필수 기능을 구현하는 방법을 배워봅니다.

API 테스트 전략과 자동화 완벽 가이드

API 개발에서 필수적인 테스트 전략을 단계별로 알아봅니다. 단위 테스트부터 부하 테스트까지, 실무에서 바로 적용할 수 있는 자동화 기법을 익혀보세요.

효과적인 API 문서 작성법 완벽 가이드

API 문서는 개발자와 개발자 사이의 가장 중요한 소통 수단입니다. 이 가이드에서는 좋은 API 문서가 갖춰야 할 조건부터 Getting Started, 엔드포인트 설명, 에러 코드 문서화, 인증 가이드, 변경 이력 관리까지 체계적으로 배워봅니다.

API 캐싱과 성능 최적화 완벽 가이드

웹 서비스의 응답 속도를 획기적으로 개선하는 캐싱 전략과 성능 최적화 기법을 다룹니다. HTTP 캐싱부터 Redis, 데이터베이스 최적화, CDN까지 실무에서 바로 적용할 수 있는 핵심 기술을 초급자 눈높이에서 설명합니다.

OAuth 2.0과 소셜 로그인 완벽 가이드

OAuth 2.0의 핵심 개념부터 구글, 카카오 소셜 로그인 구현까지 초급 개발자를 위해 쉽게 설명합니다. 인증과 인가의 차이점, 다양한 Flow의 특징, 그리고 보안 고려사항까지 실무에 바로 적용할 수 있는 내용을 다룹니다.