전체 목록
네트워크Medium#24

CORS(Cross-Origin Resource Sharing)란 무엇이며 어떻게 해결하나요?

#네트워크#CORS#보안#브라우저
힌트

Same-Origin Policy와 Preflight 요청을 생각해보세요.

정답 및 해설

CORS(Cross-Origin Resource Sharing)란 무엇이며 어떻게 해결하나요?

CORS는 브라우저의 **동일 출처 정책(Same-Origin Policy)**을 안전하게 완화하는 HTTP 헤더 기반 메커니즘입니다. 다른 출처(origin)의 리소스를 웹 페이지에서 요청할 수 있도록 서버가 명시적으로 허용하는 방식입니다.

Same-Origin Policy (동일 출처 정책)란?

브라우저는 보안을 위해 같은 출처(origin)에서 온 리소스만 기본적으로 접근할 수 있게 제한합니다. 출처(origin)는 프로토콜 + 호스트 + 포트 의 조합입니다.

https://example.com:443/page

프로토콜: https
호스트: example.com
포트: 443

동일 출처 / 다른 출처 판별

기준 URL: https://example.com/page

https://example.com/other          -- 동일 출처 (경로만 다름)
https://example.com:8080/page      -- 다른 출처 (포트 다름)
http://example.com/page            -- 다른 출처 (프로토콜 다름)
https://api.example.com/page       -- 다른 출처 (서브도메인 다름)
https://other.com/page             -- 다른 출처 (호스트 다름)

Same-Origin Policy가 없다면?

// 악성 사이트 evil.com에서의 코드
// 사용자가 bank.com에 로그인된 상태라면
fetch('https://bank.com/api/transfer', {
  method: 'POST',
  body: JSON.stringify({ to: 'hacker', amount: 1000000 }),
  credentials: 'include', // 쿠키(세션) 포함
});
// Same-Origin Policy가 없으면 이런 요청이 성공해버림!

SOP는 이런 CSRF(Cross-Site Request Forgery) 계열 공격을 방어합니다.

CORS란?

CORS는 서버가 HTTP 헤더를 통해 "이 출처에서의 요청을 허용한다"고 브라우저에 알려주는 메커니즘입니다. CORS 정책은 브라우저에서 강제되며, 서버 간 통신(Node.js에서 fetch 등)에는 적용되지 않습니다.

Preflight 요청

브라우저는 일부 요청 전에 실제 요청을 보내기 전 OPTIONS 메서드로 Preflight(사전 확인) 요청을 먼저 보냅니다.

단순 요청 (Simple Request) — Preflight 없음

다음 조건을 모두 만족하는 경우에만 Preflight 없이 바로 전송됩니다.

메서드: GET, POST, HEAD 중 하나
헤더: Accept, Content-Type(application/x-www-form-urlencoded, multipart/form-data, text/plain 만), ...

Preflight가 발생하는 경우

메서드: PUT, DELETE, PATCH 등
헤더: Authorization, Content-Type: application/json 등 커스텀 헤더 포함
-- Preflight 요청 --
OPTIONS /api/users HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

-- 서버의 Preflight 응답 --
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400   -- 24시간 동안 Preflight 캐시

Preflight가 성공하면 브라우저는 실제 요청을 보냅니다.

-- 실제 요청 --
POST /api/users HTTP/1.1
Origin: https://frontend.com
Authorization: Bearer token123
Content-Type: application/json

{"name": "홍길동", "email": "hong@example.com"}

-- 서버 응답 --
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://frontend.com
Content-Type: application/json

{"id": 1, "name": "홍길동"}

CORS 해결 방법

1. 서버에서 CORS 헤더 설정 (가장 기본적인 방법)

Express.js (Node.js)

const express = require('express');
const cors = require('cors');
const app = express();

// 모든 출처 허용 (개발 환경에서만!)
app.use(cors());

// 특정 출처만 허용
app.use(cors({
  origin: 'https://myfrontend.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true, // 쿠키/인증 헤더 허용
}));

// 여러 출처 허용
const allowedOrigins = [
  'https://myfrontend.com',
  'https://staging.myfrontend.com',
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
}));

수동으로 헤더 설정

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://myfrontend.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
  res.header('Access-Control-Allow-Credentials', 'true');

  // Preflight 요청 처리
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

2. 프록시 서버 사용

CORS는 브라우저에서만 적용됩니다. 프론트엔드 서버에서 프록시하면 브라우저 관점에서는 동일 출처 요청이 됩니다.

Next.js에서 API 라우트로 프록시

// pages/api/proxy/[...path].ts 또는 app/api/proxy/[...path]/route.ts

// App Router 방식
export async function GET(request: Request, { params }: { params: { path: string[] } }) {
  const path = params.path.join('/');
  const url = `https://external-api.com/${path}`;

  const response = await fetch(url, {
    headers: {
      Authorization: `Bearer ${process.env.API_SECRET}`,
    },
  });

  const data = await response.json();
  return Response.json(data);
}

// 클라이언트에서는 /api/proxy/users 로 요청
// → Next.js 서버가 external-api.com/users 로 프록시

Vite 개발 서버 프록시

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.backend.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

3. Next.js의 rewrites/headers 설정

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://external-api.com/:path*',
      },
    ];
  },

  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: '*' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Authorization, Content-Type' },
        ],
      },
    ];
  },
};

자격 증명(Credentials)과 CORS

쿠키나 Authorization 헤더를 포함한 요청은 특별한 설정이 필요합니다.

// 클라이언트 — credentials 포함 요청
fetch('https://api.example.com/user', {
  credentials: 'include', // 쿠키 포함
});

// 또는 axios
axios.get('https://api.example.com/user', {
  withCredentials: true,
});
// 서버 — credentials를 허용할 때 wildcard(*) 사용 불가
// 잘못된 설정:
res.header('Access-Control-Allow-Origin', '*');           // X
res.header('Access-Control-Allow-Credentials', 'true');   // X — 이 조합은 오류

// 올바른 설정:
res.header('Access-Control-Allow-Origin', 'https://myfrontend.com'); // 특정 출처
res.header('Access-Control-Allow-Credentials', 'true');

CORS 오류 디버깅

-- 콘솔 오류 메시지 --
Access to fetch at 'https://api.example.com/data' from origin 'https://myfrontend.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.

-- 확인 사항 --
1. Network 탭에서 Preflight(OPTIONS) 요청 확인
2. 응답 헤더에 Access-Control-Allow-Origin 존재 여부
3. 요청 Origin과 허용된 Origin 일치 여부
4. credentials 사용 시 wildcard(*) 설정 여부

정리

상황해결 방법
자체 API 서버 있음서버에 CORS 헤더 설정
외부 API 직접 호출프록시 서버 사용
Next.js 프로젝트API Routes 프록시 또는 rewrites
개발 환경Vite/webpack-dev-server 프록시
브라우저 없는 서버 간 통신CORS 불필요 (SOP는 브라우저 정책)

CORS는 서버가 아닌 브라우저가 강제하는 정책입니다. 따라서 근본적인 해결책은 항상 서버에서 적절한 헤더를 설정하는 것이며, 프록시는 이를 우회하는 방법입니다.