Develop

[nodejs] CORS Preflight 완전 정복 - 브라우저가 먼저 “실례합니다!”

너드나무 2025. 6. 4. 15:16
반응형
웹 개발을 하다 보면,
콘솔에 뜬 Response to preflight request doesn't pass access control check 경고가
우리를 좌절시키곤 합니다.

오늘은 이 프리플라이트(preflight) 요청이 대체 무엇이며,
왜 생기고, 어떻게 다뤄야 하는지 깔끔하게 정리해 봅니다.

1. 프리플라이트가 뭐길래?

비유 실제 동작
택배를 보내기 전 “이 짐, 위험 물질 없죠?” 라고 묻는 통관 절차 브라우저가 실제 요청 전에 서버에게 OPTIONS 메서드로 “이 출처(origin)에서 이런 헤더·메서드 써도 괜찮아요?”하고 묻는 과정
 

왜 필요할까?

  1. 보안
    스크립트가 사용자를 속여 악성 요청을 다른 사이트로 보낼 수 있기 때문입니다.
  2. 사전 합의
    서버가 미리 허락한 메서드·헤더·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. 디버깅 꿀팁

  1. 네트워크 탭
    Method = OPTIONS’ 요청이 있는지 먼저 확인.
  2. Response Headers
    Access-Control-... 값이 정확히 돌아왔는지 체크.
  3. 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
반응형