CORS가 뭐야?

seungjun.dev·2025년 10월 24일

Node.js

목록 보기
12/13

Cross-Origin Resource Sharing(교차 출처 리소스 공유)

하나의 출처(Origin)에서 실행 중인 웹 앱이 다른 출처의 자원(Resource)에 접근할 수 있도록 허용하는 메커니즘

출처(Origin)란?

Protocol, Host, Post 세 가지로 구성된다.
이 중 하나라도 다르면 다른 출처로 간주한다.

예를 들어서 https://example.com:80 이라는 주소가 있다고 가정한다.

URL출처결과이유
https://example.com:80/apphttps://example.com:80동일 출처경로(Path)만 다름
http://example.com:80https://example.com:80다른 출처프로토콜(http vs https)이 다름
https://example.com:80https://www.example.com:80다른 출처호스트(example.com vs www.example.com)가 다름
https://example.com:80https://example.com:8080다른 출처포트(:80 vs :8080)가 다름

CORS는 왜 필요한가?

SOP의 필요성

웹 브라우저는 기본적으로 SOP(Same-Origin Policy, 동일 출처 정책)을 따른다.

이는 보안상의 이유로, 다른 출처의 리소스를 함부로 가져오지 못하게 막는 정책이다.

만약 이 정책이 없다면, 악의적인 사이트가 사용자의 개인 정보를 쉽게 탈취할 수 있을 것이다.

문제

그러나 요즘 웹 앱은 API 서버, 이미지 서버 등 다양한 출처의 리소스를 사용하는 경우가 많다.

이때 SOP 때문에 리소스를 가져올 수 없는 문제가 생긴다.

해결책

따라서 CORS는 서버에 특정 설정을 추가함으로써, 신뢰할 수 있는 다른 출처의 요청을 허용해 준다.

CORS는 어떻게 동작하나?

HTTP 헤더를 통해 동작한다.

  1. 클라이언트: 다른 출처의 서버로 리소스를 요청한다. 이때 요청 헤더에 Origin이라는 필드를 추가하여 자신의 출처를 알린다.
  2. 서버: 이 요청을 받고, Origin 헤더를 확인한다.
  3. 서버: 만약 허용된 출처라면, 응답 헤더에 Access-Control-Allow-Origin 필드를 추가하여 응답한다.
  4. 클라이언트: 서버의 응답 헤더에 Access-Control-Allow-Origin이 있고, 그 값이 자신의 출처와 일치하면 리소스를 정상적으로 처리한다. 만약 이 헤더가 없거나 출처가 일치하지 않으면, 브라우저는 보안 오류를 발생시키고 리소스를 차단한다.

실제 구현

Express

정말 간단하다. 이미 미들웨어로 구현이 되어있어서 가져다 쓰기만 하면 된다.

import express from 'express';
import cors from 'cors';

const app = express();

// cors 미들웨어를 사용
app.use(cors());

// 특정 출처만 허용하고 싶을 경우
/*
const corsOptions = {
  origin: 'https://allowed-domain.com', // 허용할 출처
  optionsSuccessStatus: 200 
};
app.use(cors(corsOptions));
*/

// API 엔드포인트를 정의
app.get('/api/data', (req, res) => {
  res.json({ message: 'CORS 요청 성공!' });
});

// 서버를 3000번 포트에서 실행
app.listen(3000, () => {
  console.log('서버가 3000번 포트에서 실행 중입니다.');
});

net 모듈 사용

내부 동작을 확인하기 위해 Node.js의 net 모듈을 사용해서 low level에서 직접 구현한다.

프리플라이트 요청을 처리하고, HTTP 프로토콜을 직접 해석하고 적절한 CORS 헤더를 포함한 응답을 수동으로 만들어야 한다.

프리플라이트(Preflight) 요청

브라우저가 본 요청(e.g. PUT, DELETE)을 보내기 전에, 서버가 이 요청을 안전하게 처리할 수 있는지 확인하기 위해 먼저 보내는 OPTIONS 메서드 요청이다.

일종의 "사전 탐색"이라고 생각할 수 있다.

프리플라이트 요청은 언제 발생하는가?

브라우저는 단순 요청(Simple Request)이 아닐 경우에 프리플라이트 요청을 보낸다.
아래 조건 중 하나라도 해당하면 비-단순 요청으로 간주되어 프리플라이트가 필요하다.

  • 메서드: GET, HEAD, POST가 아닌 다른 메서드 (e.g. PUT, DELETE, PATCH 등)
  • 헤더: 기본 헤더 외에 다른 헤더가 포함된 경우 (e.g. Authorization, Content-Type: application/json)
  • Content-Type 헤더: application/x-www-form-urlencoded, multipart/form-data, text/plain 이외의 값을 가지는 경우

예를 들어 프론트엔드에서 비단순 요청을 보내면, 브라우저는 해당 요청을 보내기 전에 먼저 OPTIONS 메서드 요청을 서버로 보낸다.

프리플라이트 요청은 CORS 설정과 관계 없이 단순 요청이 아닐 때 항상 발생한다.

처리 흐름

  1. TCP 소켓으로 데이터 수신
  2. HTTP 요청 파싱: 받은 데이터를 문자열로 변환하고 첫 줄을 분석해 OPTIONS 메서드인지 확인
  3. OPTIONS 요청 응답
    • 만약 OPTIONS 요청이면, HTTP 204 No Content 상태 코드와 함께 CORS 허용 헤더들을 응답으로 보낸다.
    • 이 응답을 받은 브라우저는 서버가 요청을 허용한다고 판단하고, 이어서 원래 보내려 했던 본 요청을 보낸다.
const startLine = lines[0]; // 첫 번째 줄 (e.g. GET / HTTP/1.1)
if (!startLine) return;

// 첫 줄을 공백 기준으로 나눠서 메서드와 경로 추출
const [method, requestPath, version] = startLine.split(' ');

if (method === 'OPTIONS') {
  // CORS 프리플라이트 요청 처리
  const response = [
    `${version} 200 OK`,
    'Access-Control-Allow-Origin: *', // 모든 도메인에서의 접근 허용
    'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS', // 허용된 HTTP 메서드
    'Access-Control-Allow-Headers: Content-Type, Authorization', // 허용된 요청 헤더
    'Access-Control-Max-Age: 86400', // 24시간 동안 프리플라이트 결과 캐시
    'Content-Length: 0',
    '',
  ].join('\r\n');

  socket.write(response);
}
  1. 본 요청 응답
    • OPTIONS가 아닌 본 요청이 들어오면, 이 응답에도 반드시 Access-Control-Allow-Origin 헤더를 포함해서 보낸다.
else if (method === 'GET' && requestPath === '/index') {
    const body = loadTemplate('index.html');

    // HTTP 응답 헤더를 규칙에 맞게 작성 (CORS 헤더 포함)
    const response = [
      `${version} 200 OK`,
      'Content-Type: text/html; charset=utf-8',
      `Content-Length: ${Buffer.byteLength(body)}`,
      'Access-Control-Allow-Origin: *',
      'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers: Content-Type, Authorization',
      '', // 헤더와 본문 구분
      body,
    ].join('\r\n');

    socket.write(response);
}
profile
Frontend Engineer | Microsoft Student Ambassadors Alumni

0개의 댓글