
프리플라이트 요청(Preflight Request)이란 무엇일까요? (CORS 심층 분석)
안녕하세요!
웹 개발을 하다 보면 POST(포스트), GET(겟), PUT(풋), DELETE(딜리트) 같은 다양한 HTTP(Hypertext Transfer Protocol) 요청 메서드(method)들을 사용하게 되는데요.
그런데 혹시, 우리가 직접 요청하지 않았는데도 브라우저(browser)가 알아서 보내는 OPTIONS(옵션스) 라는 특별한 요청을 보신 적 있으신가요?
일반적으로 이 OPTIONS(옵션스) 요청을 바로 프리플라이트 요청(Preflight Request) 이라고 부릅니다.
이는 브라우저(browser)가 앞으로 보낼 실제 요청이 서버(server)에 예측하지 못한 영향을 줄 수도 있다고 판단될 때, 본 요청을 보내기 전에 먼저 보내는 예비 요청인데요.
이 프리플라이트 요청(Preflight Request)을 통해 브라우저(browser)는 서버(server)가 다가올 실제 요청을 허용하는지 미리 확인할 수 있습니다. 서버(server)로부터 '허락'을 받아야만 브라우저(browser)는 비로소 실제 요청을 실행하게 된답니다.
대부분의 경우, 이 프리플라이트 요청(Preflight Request)은 사용자가 직접 관리하거나 신경 쓸 필요 없이 브라우저(browser)와 서버(server) 사이에서 자동으로 처리되는데요.
프리플라이트 요청(Preflight Request)의 헤더(header)는 보통 다음과 같은 모습을 하고 있습니다.
Access-Control-Request-Headers: x-requested-with
Access-Control-Request-Method: POST
Origin: http://preflight.example.com
이 요청에서 중요한 필드(field) 세 가지는 다음과 같은데요.
Origin(오리진): 요청을 보내는 출처(주소)를 나타냅니다.Access-Control-Request-Method(액세스-컨트롤-리퀘스트-메서드): 앞으로 보낼 실제 요청이 어떤 HTTP(Hypertext Transfer Protocol) 메서드(method)를 사용할 것인지를 명시합니다. (예: POST)Access-Control-Request-Headers(액세스-컨트롤-리퀘스트-헤더스): 실제 요청에 어떤 커스텀(custom) 요청 헤더(header)들이 포함될 것인지를 쉼표로 구분하여 나열합니다.
이에 대한 서버(server)의 프리플라이트 응답(response)은 대략 이런 모습입니다.
Access-Control-Allow-Headers: Content-Type, Content-Length, Authorization, Accept, X-Requested-With
Access-Control-Allow-Origin: http://preflight.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
이 응답에서 가장 중요한 필드(field)들은 다음과 같습니다.
Access-Control-Allow-Origin(액세스-컨트롤-얼라우-오리진): 어떤 출처(origin)의 접근을 허용할 것인지를 명시합니다. 위 예시는http://preflight.example.com만 허용한다는 의미입니다.Access-Control-Allow-Headers(액세스-컨트롤-얼라우-헤더스): 서버(server)가 허용하는 커스텀(custom) 요청 헤더(header)들의 목록입니다.Access-Control-Allow-Methods(액세스-컨트롤-얼라우-메서즈): 서버(server)가 허용하는 HTTP(Hypertext Transfer Protocol) 메서드(method)들의 목록입니다.
만약 실제 보내려는 요청의 출처(origin), 메서드(method), 또는 헤더(header) 중 어느 하나라도 서버(server)가 이 프리플라이트 응답(response)에서 허용한 범위에 속하지 않는다면, 브라우저(browser)는 실제 요청 전송을 자동으로 취소하고 콘솔(console)에 씨오알에스(CORS - Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 오류를 발생시킵니다.
마지막 필드인 Access-Control-Max-Age (액세스-컨트롤-맥스-에이지)는 이 프리플라이트 요청(Preflight Request)의 '수명'을 정의합니다.
여기에 명시된 시간(초 단위) 동안 브라우저(browser)는 동일한 요청에 대해 또 다른 프리플라이트 요청(Preflight Request)을 보내지 않고 이전에 받은 허용 결과를 재사용합니다.
성능을 위한 캐싱(caching) 기능이라고 볼 수 있습니다.
언제 프리플라이트 요청(Preflight Request)이 발생할까요?
프리플라이트 요청(Preflight Request)은 씨오알에스(CORS) 명세의 일부이며, 모든 최신 브라우저(browser)들은 이를 구현하고 있습니다. (일부 브라우저(browser)는 표준 외 추가 기능을 구현하기도 합니다.)
엠디엔(MDN - Mozilla Developer Network) 문서에 따르면, 특정 조건을 모두 만족하는 요청은 "단순 요청(Simple Request)"으로 간주되어 프리플라이트 요청(Preflight Request)을 보내지 않습니다.
하지만 이 조건 중 단 하나라도 만족하지 못하면, 브라우저(browser)는 예측 불가능한 서버(server) 동작을 방지하기 위해 실제 요청 전에 반드시 프리플라이트 요청(Preflight Request)을 먼저 보냅니다.
프리플라이트 요청(Preflight Request)을 피하기 위한 5가지 조건은 다음과 같습니다(즉, 이 조건들을 모두 만족해야 '단순 요청'입니다).
- 요청 메서드 제한: 요청 메서드(method)는 반드시
GET(겟),POST(포스트),HEAD(헤드) 중 하나여야 합니다. (PUT,DELETE등 다른 메서드는 프리플라이트 요청을 유발합니다.) - 요청 헤더 제한: 요청에 포함될 수 있는 헤더(header)는 아래 9가지로 엄격히 제한됩니다. 이 외의 헤더(예:
Authorization)가 포함되면 프리플라이트 요청이 필요합니다.Accept(억셉트)Accept-Language(억셉트-랭귀지)Content-Language(콘텐트-랭귀지)Content-Type(콘텐트-타입) (단, 아래 3번 조건의 값만 허용)DPR(디피알)Downlink(다운링크)Save-Data(세이브-데이터)Viewport-Width(뷰포트-위드)Width(위드)
Content-Type(콘텐트-타입) 헤더 값 제한:Content-Type(콘텐트-타입) 헤더(header)는 다음 세 가지 값 중 하나여야만 합니다. (예:application/json은 프리플라이트 요청을 유발합니다.)text/plain(텍스트/플레인)multipart/form-data(멀티파트/폼-데이터)application/x-www-form-urlencoded(애플리케이션/x-www-폼-urlencoded)
- XMLHttpRequestUpload 객체 제한: 요청을 생성하는 데 사용된
XMLHttpRequestUpload(엑스엠엘에이치티티피리퀘스트업로드) 객체에 어떤 이벤트 리스너(event listener)도 등록되어 있지 않아야 합니다. - ReadableStream 객체 제한: 요청에
ReadableStream(리더블스트림) 객체가 사용되지 않아야 합니다.
대부분의 개발자에게 있어 프리플라이트 요청(Preflight Request)이 발생하는 가장 흔한 원인은 주로 첫 세 가지 규칙 때문입니다. 즉,
- 허용된 목록 외의 커스텀(custom) 요청 헤더(header)를 사용할 때 (예: 인증 토큰(token)을 위한
Authorization헤더) - 허용된 세 가지 외의
Content-Type(콘텐트-타입)을 사용할 때 (예:application/json으로 데이터를 보낼 때) PUT(풋),DELETE(딜리트) 등 단순 요청 메서드(method) 외의 메서드(method)를 사용할 때
프리플라이트 요청(Preflight Request)이 발생하게 됩니다.
프리플라이트 요청(Preflight Request)은 왜 존재할까요?
프리플라이트 요청(Preflight Request)이 무엇이고 언제 발생하는지 알았으니, 이제 왜 이런 메커니즘(mechanism)이 설계되었는지 그 이유를 탐구해 볼 시간입니다.
프리플라이트 요청(Preflight Request)은 씨오알에스(CORS)의 핵심적인 부분이며, 교차 출처(cross-origin) 접근을 제한하여 웹 보안을 강화하는 중요한 목적을 가지고 있습니다.
만약 씨오알에스(CORS)가 없다면 어떤 일이 벌어질까요? 명시적으로 제한하지 않는 한, 모든 웹 리소스(resource)는 어떤 웹사이트(website)에서든 접근 가능하게 됩니다. 예를 들어,
- A 웹사이트(website)가 자바스크립트(JavaScript)를 이용해 B 웹사이트(website)의 쿠키(cookie)나 개인 데이터에 접근할 수 있습니다.
- 반대로 B 웹사이트(website)도 A 웹사이트(website)에 똑같은 작업을 할 수 있습니다.
씨오알에스(CORS)는 기본적으로 허가되지 않은 교차 출처(cross-origin) 요청을 막습니다. 오직 서버(server)가 허용한 출처(origin), 메서드(method), 헤더(header) (화이트리스트(whitelist) 방식)만이 보호된 리소스(resource)에 접근할 수 있도록 합니다. 이러한 보안 모델(security model)은 보호가 필요한 웹사이트(website)는 접근을 제어하면서도, 교차 출처(cross-origin) 접근이 필요한 경우에는 유연성을 유지할 수 있도록 보장합니다.
프리플라이트 요청(Preflight Request)은 다음과 같은 방식으로 보안 위험을 예방하는 데 도움을 줍니다.
- 서버(server)가 실제 요청을 실행하기 전에, 해당 교차 출처(cross-origin) 요청을 명시적으로 허용하는지 미리 확인합니다.
- 씨오알에스(CORS)를 지원하지 않는 구형 서버(server)의 경우, 프리플라이트 요청(OPTIONS(옵션스) 요청)을 이해하지 못해 제대로 응답하지 못할 가능성이 높습니다. 이 경우 브라우저(browser)는 실제 요청(잠재적으로 민감한 데이터 포함)을 보내지 않음으로써, 해당 서버(server)가 의도치 않게 요청을 처리하여 민감한 정보가 노출되는 것을 방지합니다.
씨오알에스(CORS) 보안 메커니즘(mechanism)에 대해 더 깊이 알고 싶으시다면, 엠디엔(MDN)의 관련 문서를 참조하시면 큰 도움이 될 것입니다.
프리플라이트 요청(Preflight Request)을 제대로 지원하는 방법 (백엔드 관점)
만약 여러분이 백엔드(backend) 개발자라면, 서버(server)가 프리플라이트 요청(Preflight Request)을 적절하게 처리하도록 설정해야 할 수 있는데요. 어떻게 해야 할까요?
프리플라이트 요청(Preflight Request)을 지원하기 위해서는 주로 다음 세 가지 응답 헤더(response header) 설정에 집중해야 합니다.
Access-Control-Allow-Origin(액세스-컨트롤-얼라우-오리진)- 리소스(resource)에 접근을 허용할 출처(origin)를 명시합니다.
- 만약 API(Application Programming Interface)가 완전히 공개된 것(예: 이미지 업로드 서비스)이라면, 이 값을
"*"(모든 출처 허용)로 설정할 수도 있습니다. - 하지만 보안 수준을 낮추기 때문에
"*"사용은 일반적으로 권장되지 않습니다. 꼭 필요한 특정 출처(origin)만 명시하는 것이 좋습니다.
Access-Control-Allow-Headers(액세스-컨트롤-얼라우-헤더스)- 허용할 커스텀(custom) 요청 헤더(header)를 명시합니다.
- 서버(server)는 지원하는 헤더(header)들만 명시적으로 나열해야 합니다. 역시
"*"사용은 지양하는 것이 좋습니다.
Access-Control-Allow-Methods(액세스-컨트롤-얼라우-메서즈)- 허용할 HTTP(Hypertext Transfer Protocol) 메서드(method)를 명시합니다.
- 마찬가지로
"*"대신 필요한 메서드(method)들만 명시하는 것이 더 안전합니다.
권장되는 서버(Server) 설정 예시 (예: Koa 프레임워크 사용 시)
// 특정 허용된 오리진으로 교체해야 합니다.
ctx.set('Access-Control-Allow-Origin', 'http://allowed.example.com');
// 만약 쿠키(cookie) 등 신용 정보(credential) 전송이 필요하다면 true로 설정합니다.
ctx.set('Access-Control-Allow-Credentials', true);
// 프리플라이트 응답 캐싱 시간 (예: 24시간)
ctx.set('Access-Control-Max-Age', 86400000);
// OPTIONS 외에 실제 허용할 메서드를 명시합니다.
ctx.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
// 실제 필요한 헤더들을 명시합니다.
ctx.set(
'Access-Control-Allow-Headers',
'Content-Type, Content-Length, Authorization, Accept, X-Requested-With, your-custom-header'
);
추가적으로, 만약 백엔드(backend) 프레임워크(framework)에서 router.post()나 router.get()과 같이 특정 메서드(method)에 대한 핸들러(handler)만 등록하는 방식을 사용하고 있다면, 반드시 OPTIONS 메서드(method) 요청을 명시적으로 처리하는 로직(logic)을 추가해야 합니다.
그렇지 않으면 브라우저(browser)가 보낸 프리플라이트 요청(Preflight Request)을 서버(server)가 제대로 처리하지 못할 수 있습니다.
일반적인 접근 방식은 OPTIONS 요청에 대해 HTTP(Hypertext Transfer Protocol) 상태 코드(status code) 200 (OK) 또는 204 (No Content)를 반환하는 것입니다.
if (ctx.request.method === 'OPTIONS') {
// 필요한 Access-Control-Allow-* 헤더들을 위에서 설정한 후,
ctx.response.status = 204; // 내용 없이 성공 상태만 알림
// 또는 ctx.response.status = 200;
} else {
// 다른 실제 요청 처리 로직
}
참고: 2xx 범위의 어떤 상태 코드(status code)든 기술적으로는 허용되지만, 204 No Content(내용 없음)가 가장 일반적으로 사용되는 선택지입니다.
프리플라이트 요청(Preflight Request)과 씨오알에스(CORS)의 관계 요약
엠디엔(MDN)에 따르면, 프리플라이트 요청(Preflight Request)은 씨오알에스(CORS) 표준의 일부입니다.
그리고 중요한 것은, 프리플라이트 요청(Preflight Request)은 오직 요청이 교차 출처(cross-origin)일 경우에만 발생할 수 있다는 점입니다.
이는 다음을 의미합니다.
- 교차 출처(Cross-origin) 요청이라고 해서 항상 프리플라이트 요청(Preflight Request)을 보내는 것은 아닙니다. ("단순 요청" 조건 충족 시 생략)
- 만약 프리플라이트 요청(Preflight Request)이 발생했다면, 그 요청은 분명히 교차 출처(cross-origin) 요청입니다.
이는 합리적인 설계인데요.
프리플라이트 요청(Preflight Request) 자체가 교차 출처(cross-origin) 상호작용의 보안을 위해 존재하기 때문입니다.
같은 출처(same-origin) 내에서는 프론트엔드(frontend) 개발자와 백엔드(backend) 개발자 간에 보통 API(Application Programming Interface) 보안에 대한 상호 이해가 있다고 가정하므로, 굳이 프리플라이트 확인 절차를 거칠 필요가 없는 것입니다.
이는 웹 개발에서 보안과 편의성 사이의 균형을 맞추려는 전형적인 예시라고 할 수 있습니다.
즉, 개발을 너무 제약하지 않으면서도 보안을 향상시키는 절충안을 선택한 것이죠.
이제 프리플라이트 요청(Preflight Request)에 대해 확실히 이해하셨기를 바랍니다!
'Javascript' 카테고리의 다른 글
| 세션, JWT, SSO, OAuth 2.0 전격 비교: 장단점부터 사용 사례까지 완벽 분석! (1) | 2025.05.06 |
|---|---|
| RESTful API 완벽 이해: 원칙부터 디자인, 베스트 프랙티스까지 (0) | 2025.05.05 |
| 플레이라이트(Playwright) vs 퍼펫티어(Puppeteer): 지금 갈아타야 할까요? (마이그레이션 가이드) (0) | 2025.05.05 |
| 자바스크립트 샌드박스 완벽 파헤치기: 안전한 코드 실행 환경 만들기 (0) | 2025.05.05 |
| 노드JS에서 멀티스레딩 완벽 정복! 성능 향상의 비밀 풀기 (1) | 2025.04.28 |