Develop
[nodejs] CORS Preflight 완전 정복 - 브라우저가 먼저 “실례합니다!”
너드나무
2025. 6. 4. 15:16
반응형
웹 개발을 하다 보면,
콘솔에 뜬 Response to preflight request doesn't pass access control check 경고가
우리를 좌절시키곤 합니다.
오늘은 이 프리플라이트(preflight) 요청이 대체 무엇이며,
왜 생기고, 어떻게 다뤄야 하는지 깔끔하게 정리해 봅니다.
1. 프리플라이트가 뭐길래?
비유 | 실제 동작 |
택배를 보내기 전 “이 짐, 위험 물질 없죠?” 라고 묻는 통관 절차 | 브라우저가 실제 요청 전에 서버에게 OPTIONS 메서드로 “이 출처(origin)에서 이런 헤더·메서드 써도 괜찮아요?”하고 묻는 과정 |
왜 필요할까?
- 보안
스크립트가 사용자를 속여 악성 요청을 다른 사이트로 보낼 수 있기 때문입니다. - 사전 합의
서버가 미리 허락한 메서드·헤더·Credentials 범위 내에서만 요청을 허용하도록 표준이 정했습니다.
2. 언제 프리플라이트가 발생하나?
조건 | 예시 |
비-단순(non-simple) 요청 | Content-Type: application/json, PUT, PATCH, DELETE 등 |
withCredentials 가 true | 쿠키·Authorization 헤더 포함 |
커스텀 헤더 존재 | X-Requested-With, Authorization 등 |
단순 요청(Simple Request) = GET / POST + Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 이고 커스텀 헤더가 없는 경우.
이때는 프리플라이트 없이 바로 본 요청이 간다.
3. 프리플라이트 흐름 살펴보기
1) 브라우저 → 서버 OPTIONS /api/login
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
Origin: https://myfrontend.com
2) 서버 → 브라우저 200 OK
Access-Control-Allow-Origin: https://myfrontend.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
3) 브라우저 → 서버 실제 POST /api/login
서버가 2단계에서 허락 헤더를 정확히 주지 못하면?
→ 3단계 요청 자체가 차단되고, 콘솔에 CORS 에러가 뜹니다.
4. “내 서버에서” 처리하는 법
(1) 전통적인 Express 예시
const express = require('express');
const app = express();
const ALLOW_ORIGIN = 'https://myfrontend.com';
app.use((_, res, next) => {
res.setHeader('Access-Control-Allow-Origin', ALLOW_ORIGIN);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
// OPTIONS fast-path
app.options('*', (_, res) => {
res.set({
'Access-Control-Allow-Origin': ALLOW_ORIGIN,
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Access-Control-Allow-Credentials': 'true',
});
res.sendStatus(200);
});
(2) AWS Lambda + Lambda URL 패턴
const serverless = require('serverless-http');
const app = express();
const expressHandler = serverless(app);
const CORS_HEADERS = {/* …위와 동일… */};
module.exports.handler = async (event, context) => {
if (event.requestContext.http.method === 'OPTIONS') {
return { statusCode: 200, headers: CORS_HEADERS, body: '' };
}
const res = await expressHandler(event, context);
res.headers = { ...res.headers, ...CORS_HEADERS };
return res;
};
포인트
- Lambda URL은 ALB나 APIGW가 없어 미들웨어가 OPTIONS를 캐치하지 못할 수 있습니다.
따라서 람다 진입 직후 직접 200 응답을 돌려주는 것이 안전합니다.
5. 흔한 실수 & 해결책
증상 | 원인 | 해결 |
403, 405, ERR_FAILED | 서버가 OPTIONS 라우트를 안 만듦 | fast-path 추가 |
허락했는데도 계속 차단 | Access-Control-Allow-Origin: * + withCredentials: true 조합 | 크리덴셜 쓰면 ‘*’ 못 씀 — 꼭 정확한 Origin 지정 |
로컬만 잘 되고 배포만 실패 | 배포 URL이 ALLOW_ORIGIN 목록에 빠짐 | 환경변수로 관리하거나 와일드카드 서브도메인 검토 |
6. 디버깅 꿀팁
- 네트워크 탭
‘Method = OPTIONS’ 요청이 있는지 먼저 확인. - Response Headers
Access-Control-... 값이 정확히 돌아왔는지 체크. - curl 테스트
curl -X OPTIONS -H "Origin: https://myfrontend.com" \
-H "Access-Control-Request-Method: POST" \
https://my-api.com/api/login -i
7. Best Practice 한눈에
- 단일 상수로 ALLOW_ORIGIN·CORS_HEADERS 관리
- 프리플라이트 패스트패스: 200 OK + 헤더만 주고 일찌감치 종료
- 모든 응답에 헤더 추가: 미들웨어 or 핸들러 마지막 단계
- Credentials 필요 없으면 쿠키/Authorization 제거 → 단순 요청으로 다운그레이드
- 캐시: Access-Control-Max-Age 헤더로 프리플라이트 재사용(Chrome 최대 2시간)
728x90
반응형