Menu

Express CORS 에러 해결 (cors 패키지 사용과 미사용)

CORS 에러는 응답이 페이지의 출처를 허용하지 않아서 브라우저가 페이지가 API 응답을 읽지 못하게 막았다는 뜻입니다. 직접 쓰거나 cors 패키지로 Access-Control-Allow-Origin을 보내고 프리플라이트 OPTIONS 요청에 응답해 서버에서 고치세요.

이 페이지에는 실행 가능한 에디터가 있습니다. 편집하고 실행하면 결과를 바로 볼 수 있습니다.

CORS 에러("No 'Access-Control-Allow-Origin' header is present")는 http://localhost:5173 같은 한 출처의 웹 페이지가 다른 출처의 API를 호출했는데, 응답이 그 출처를 허용한다고 말하지 않아서 브라우저가 페이지가 응답을 읽지 못하게 막았다는 뜻입니다. 해결은 서버에서 합니다. 페이지의 출처를 담은 Access-Control-Allow-Origin을 보내고, 브라우저의 프리플라이트 OPTIONS 요청에는 받아들일 메서드와 헤더로 응답하세요. Express에서는 미들웨어 몇 줄이나 cors 패키지의 app.use(cors())면 됩니다.

응답은 상품 목록이고, Headers 탭에 access-control-allow-origin: https://app.example.com이 보입니다. 그 출처의 페이지는 응답을 읽어도 된다는 뜻입니다. 이제 Origin: https://app.example.com과 Access-Control-Request-Method: PUT 헤더를 붙여 OPTIONS /api/products/1을 보내 보세요. 브라우저가 PUT 전에 보내는 프리플라이트이고, 응답은 허용된 메서드와 헤더를 담은, 본문 없는 204입니다.

브라우저가 확인하는 것

같은 출처의 페이지는 아무것도 필요 없습니다. 교차 출처 요청에서 브라우저는 다음 응답 헤더를 읽습니다.

헤더의미
Access-Control-Allow-Origin응답을 읽어도 되는 출처 하나, 또는 모두라면 *
Access-Control-Allow-Methods실제 요청에서 허용되는 메서드(프리플라이트에만)
Access-Control-Allow-Headers페이지가 보낼 수 있는 요청 헤더, 예를 들어 Authorization(프리플라이트에만)
Access-Control-Allow-Credentials페이지가 쿠키를 보내고 응답을 읽어도 되면 true
Access-Control-Max-Age브라우저가 프리플라이트 응답을 캐시해도 되는 초
Vary: Origin응답이 Origin 요청 헤더에 따라 달라진다고 캐시에 알림

단순한 GET이나 폼 형식의 POST는 서버로 바로 가고, 브라우저는 응답의 Access-Control-Allow-Origin을 확인합니다. 서버는 어쨌든 요청을 실행합니다. CORS는 페이지가 결과를 볼 수 있는지만 정합니다. JSON 본문, Authorization 헤더, PUT이나 DELETE 같은 메서드가 있는 요청 앞에는 프리플라이트 OPTIONS 요청이 가고, 프리플라이트 응답이 허용할 때만 실제 요청이 전송됩니다. 그래서 CORS 미들웨어는 어떤 라우트나 인증 검사보다 먼저 OPTIONS에 응답해야 합니다.

여러 출처를 쿠키와 함께 허용하기

Access-Control-Allow-Origin에는 출처 하나나 *만 들어가고 목록은 안 됩니다. 여러 개를 허용하려면 요청의 Origin 헤더를 허용 목록과 비교해서, 일치하면 그대로 돌려주세요.

첫 요청에는 curl이나 같은 출처 페이지에서의 호출처럼 Origin 헤더가 없어서, {"origin":null}을 받고 CORS 헤더는 붙지 않습니다. Origin: http://localhost:5173 헤더를 붙여 GET /api/me를 보내면, Headers 탭에 그 출처가 그대로 돌아온 것과 access-control-allow-credentials: true가 보입니다. 헤더를 Origin: https://evil.example로 바꾸면 두 CORS 헤더가 모두 사라집니다. 서버는 여전히 응답하지만, 브라우저라면 페이지가 그것을 읽지 못하게 막습니다. Access-Control-Allow-Credentials: true는 페이지가 쿠키를 보낼 수 있게 하는데(fetch(url, { credentials: 'include' })로), 브라우저는 이것과 *의 조합을 거부하므로 쿠키를 쓰려면 항상 정확한 출처가 필요합니다.

cors 패키지

cors 패키지는 같은 일을 옵션으로 합니다. npm install cors로 설치하세요.

const cors = require('cors');

// Any origin, no cookies: fine for a public read-only API.
app.use(cors());

// Only your frontend, with cookies.
app.use(cors({
    origin: ['https://app.example.com', 'http://localhost:5173'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

// One route only.
app.get('/api/public', cors(), handler);

옵션 없이 호출하면 cors()는 Access-Control-Allow-Origin: *을 보내고 모든 프리플라이트에 응답합니다. *는 로그인이 필요 없는 공개 데이터에만 쓰세요.

흔한 실수

라우트나 인증 미들웨어 뒤에 CORS를 추가하는 것. 프리플라이트 OPTIONS 요청에는 Authorization 헤더가 없어서, 먼저 놓인 인증 검사가 401로 응답하고 브라우저는 CORS 에러를 보고합니다. CORS 미들웨어를 먼저 등록하세요.

쿠키와 함께 *를 설정하는 것. 브라우저가 응답을 거부합니다. 대신 특정 출처를 그대로 돌려주세요.

프론트엔드에서 CORS를 고치려는 것. mode: 'no-cors' 같은 요청 옵션은 제한을 풀어 주지 않습니다. 읽을 수 없는 불투명한 응답을 줄 뿐입니다. 서버의 헤더만이 고칠 수 있습니다.

app.use()와 미들웨어 순서의 동작은 Express 미들웨어를 참고하세요.

자주 묻는 질문

API가 Postman이나 curl에서는 되는데 브라우저에서는 안 되는 이유는?

CORS는 브라우저만 강제합니다. Postman, curl, 다른 서버는 CORS 헤더를 무시하므로 어쨌든 응답을 읽습니다. 브라우저는 서버가 그 출처를 허용하지 않는 한, 다른 출처의 페이지 스크립트에 응답을 넘겨주지 않습니다.

CORS는 내 서버를 위한 보안 기능인가요?

서버가 아니라 사용자를 보호합니다. 악의적인 페이지가 사용자의 쿠키로 여러분의 API 응답을 읽는 것을 막습니다. 스크립트나 서버에서는 누구나 여전히 API를 호출할 수 있으므로, 인증과 권한 확인은 여전히 여러분의 몫입니다.

무엇이 다른 출처로 간주되나요?

출처는 스킴, 호스트, 포트를 합친 것입니다. http://localhost:5173과 http://localhost:3000은 포트가 달라서 다른 출처이고, https://example.com과 https://api.example.com은 호스트가 다릅니다.

개발 중에 CORS를 피할 수 있나요?

네, 요청을 같은 출처로 만들면 됩니다. 프론트엔드 개발 서버(예를 들어 Vite의 server.proxy)가 /api를 Express 서버로 전달하도록 설정하세요. 그러면 브라우저는 한 출처와만 통신하므로 CORS 헤더가 필요 없습니다.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기