CORS 완벽 가이드: 웹 개발자가 반드시 알아야 할 교차 출처 리소스 공유

2025. 5. 28. 10:07·Frontend Development
반응형

"웹 개발에서 CORS 오류는 마치 갑자기 나타난 벽과 같습니다. 그러나 이 벽은 우리를 보호하기 위한 것이며, 올바른 문을 찾아 통과하는 방법을 배우는 것이 중요합니다."

목차

  • CORS란 무엇인가?
  • 동일 출처 정책(Same-Origin Policy)
  • CORS가 필요한 이유
  • CORS 작동 방식
  • CORS 구성 방법
  • 일반적인 CORS 오류와 해결 방법
  • 보안 고려사항
  • 실제 구현 예제
  • 결론

CORS란 무엇인가?

CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 웹 브라우저에서 실행 중인 웹 애플리케이션이 다른 출처(도메인, 프로토콜, 포트)의 리소스에 접근할 수 있도록 허용하는 보안 메커니즘입니다.

"출처(Origin)"란 URL의 프로토콜, 도메인, 포트 번호의 조합을 의미합니다. 예를 들어, https://example.com:443에서:

  • 프로토콜: https://
  • 도메인: example.com
  • 포트: 443 (HTTPS의 기본 포트)

다음과 같은 경우 두 URL은 "다른 출처"로 간주됩니다:

  • 프로토콜이 다른 경우: http://example.com vs https://example.com
  • 도메인이 다른 경우: https://example.com vs https://api.example.com
  • 포트가 다른 경우: https://example.com vs https://example.com:8080

CORS는 이러한 다른 출처 간의 안전한 통신을 가능하게 합니다.


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

웹 브라우저는 기본적으로 동일 출처 정책(Same-Origin Policy, SOP)을 따릅니다. 이 정책은 웹 페이지가 동일한 출처의 리소스만 자유롭게 접근할 수 있도록 제한합니다.

SOP가 방어하는 주요 공격

1. CSRF(Cross-Site Request Forgery) 공격

CSRF 공격은 악성 웹사이트가 사용자의 인증 정보(쿠키 등)를 이용하여 사용자 모르게 다른 웹사이트에 요청을 보내는 공격입니다.

공격 시나리오:

  1. 사용자가 은행 웹사이트(bank.com)에 로그인하고 인증 쿠키를 받음
  2. 로그아웃하지 않은 상태에서 악성 웹사이트(evil.com)를 방문
  3. 악성 웹사이트는 사용자 모르게 은행 웹사이트로 송금 요청을 보냄
  4. 브라우저는 자동으로 은행 웹사이트에 대한 인증 쿠키를 함께 전송
  5. 은행 웹사이트는 요청이 인증된 사용자로부터 온 것으로 판단하고 송금을 실행

SOP는 이러한 공격을 방지합니다. evil.com에서 bank.com으로의 요청은 가능하지만, 응답에 접근할 수 없기 때문에 공격자는 공격의 성공 여부를 확인하기 어렵고, 추가 공격을 위한 정보를 얻을 수 없습니다.

2. XSS(Cross-Site Scripting)와의 시너지 효과

SOP는 XSS 공격이 발생해도 그 영향을 제한합니다. 악성 스크립트가 삽입되더라도 다른 출처의 응답 데이터에 접근할 수 없어 데이터 유출 범위가 제한됩니다.

SOP의 문제점

SOP는 보안을 강화하지만, 현대 웹 애플리케이션 개발에 제약을 가져옵니다. 특히:

  • 프론트엔드와 백엔드가 분리된 아키텍처(예: React 프론트엔드 + REST API 백엔드)
  • 마이크로서비스 아키텍처
  • 제3자 API 통합
  • CDN(Content Delivery Network) 사용

이러한 상황에서는 서로 다른 출처 간의 통신이 필수적이며, 이를 가능하게 하는 것이 CORS입니다.


CORS가 필요한 이유

현대 웹 개발에서 CORS가 필요한 주요 이유는 다음과 같습니다:

1. 분리된 프론트엔드와 백엔드

최신 웹 개발에서는 프론트엔드와 백엔드를 분리하는 것이 일반적입니다:

  • 프론트엔드: https://app.example.com (React, Vue, Angular 등)
  • 백엔드 API: https://api.example.com (Node.js, Spring, Django 등)

이러한 구조에서는 프론트엔드에서 백엔드 API로의 요청이 교차 출처 요청이 되므로 CORS가 필요합니다.

2. 제3자 서비스 통합

많은 웹 애플리케이션이 제3자 서비스를 통합합니다:

  • 결제 게이트웨이 (Stripe, PayPal 등)
  • 소셜 미디어 API (Facebook, Twitter 등)
  • 클라우드 서비스 (AWS, Google Cloud 등)

이러한 서비스들은 서로 다른 도메인에 호스팅되므로 CORS 없이는 접근할 수 없습니다.

3. 마이크로 프론트엔드 아키텍처

여러 개의 독립적인 프론트엔드 애플리케이션이 하나의 통합된 웹사이트를 구성하는 마이크로 프론트엔드 아키텍처에서도 CORS가 필요합니다.

4. 개발 환경과 프로덕션 환경의 분리

개발 중에는 프론트엔드가 로컬 환경(예: http://localhost:3000)에서 실행되지만, 백엔드 API는 다른 출처(예: 개발 서버 또는 스테이징 환경)에 있을 수 있습니다.


CORS 작동 방식

CORS는 HTTP 헤더를 통해 작동합니다. 브라우저와 서버 간에 추가적인 HTTP 헤더를 교환하여 교차 출처 요청이 허용되는지 확인합니다.

간단한 요청(Simple Request)

다음 조건을 충족하는 요청은 "간단한 요청"으로 분류됩니다:

  • HTTP 메서드가 GET, HEAD, POST 중 하나
  • 자동으로 설정되는 헤더(예: Connection, User-Agent) 외에 Accept, Accept-Language, Content-Language, Content-Type 헤더만 사용
  • Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나인 경우

간단한 요청의 경우:

  1. 브라우저가 서버에 요청을 보낼 때 Origin 헤더를 포함
  2. 서버는 응답에 Access-Control-Allow-Origin 헤더를 포함
  3. 브라우저는 Origin과 Access-Control-Allow-Origin을 비교하여 접근 허용 여부 결정
// 요청 헤더
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

// 응답 헤더
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

프리플라이트 요청(Preflight Request)

간단한 요청이 아닌 경우(예: PUT, DELETE 메서드 사용, 사용자 정의 헤더 포함 등), 브라우저는 본 요청을 보내기 전에 "프리플라이트" 요청을 보냅니다:

  1. 브라우저가 OPTIONS 메서드로 프리플라이트 요청을 보냄
  2. 서버는 허용되는 메서드, 헤더 등을 포함한 응답을 반환
  3. 브라우저는 응답을 확인하고, 허용되면 본 요청을 보냄
// 프리플라이트 요청
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

// 프리플라이트 응답
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

// 본 요청 (프리플라이트 성공 후)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123

// 본 요청에 대한 응답
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

인증 정보를 포함한 요청

기본적으로 브라우저는 교차 출처 요청에 쿠키나 HTTP 인증과 같은 인증 정보를 포함하지 않습니다. 인증 정보를 포함하려면:

  1. 클라이언트 측에서 credentials: 'include' 설정
  2. 서버 측에서 Access-Control-Allow-Credentials: true 헤더 포함
  3. 서버 측에서 Access-Control-Allow-Origin에 와일드카드(*)가 아닌 구체적인 출처 지정
// 클라이언트 측 (fetch API 사용)
fetch('https://api.example.com/data', {
  credentials: 'include' // 인증 정보 포함
})
// 서버 응답 헤더
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

CORS 구성 방법

다양한 서버 환경에서 CORS를 구성하는 방법을 알아보겠습니다.

Express.js (Node.js)

// 방법 1: cors 미들웨어 패키지 사용
const express = require('express');
const cors = require('cors');
const app = express();

// 모든 출처 허용
app.use(cors());

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

// 방법 2: 직접 헤더 설정
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://app.example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');

  if (req.method === 'OPTIONS') {
    return res.status(204).send();
  }
  next();
});

Spring Boot (Java)

// 방법 1: 어노테이션 사용
@CrossOrigin(origins = "https://app.example.com", 
             methods = {RequestMethod.GET, RequestMethod.POST}, 
             allowedHeaders = {"Content-Type", "Authorization"},
             allowCredentials = "true")
@RestController
public class ApiController {
    // ...
}

// 방법 2: 전역 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Content-Type", "Authorization")
                .allowCredentials(true);
    }
}

Django (Python)

# settings.py
INSTALLED_APPS = [
    # ...
    'corsheaders',
    # ...
]

MIDDLEWARE = [
    # ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

# 모든 출처 허용
CORS_ALLOW_ALL_ORIGINS = True

# 또는 특정 출처만 허용
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
]

CORS_ALLOW_METHODS = [
    'GET',
    'POST',
    'PUT',
    'DELETE',
    'OPTIONS',
]

CORS_ALLOW_HEADERS = [
    'Authorization',
    'Content-Type',
]

CORS_ALLOW_CREDENTIALS = True

Nginx

server {
    # ...

    location /api/ {
        # ...

        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Allow-Credentials' 'true';

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 86400;
            return 204;
        }
    }
}

일반적인 CORS 오류와 해결 방법

개발 중 자주 마주치는 CORS 오류와 해결 방법을 알아보겠습니다.

1. "Access to fetch at 'X' from origin 'Y' has been blocked by CORS policy"

원인: 서버가 적절한 CORS 헤더를 제공하지 않음

해결 방법:

  • 서버 측에서 적절한 Access-Control-Allow-Origin 헤더 설정
  • 개발 환경에서는 프록시 서버 사용 (Create React App, Vue CLI 등에서 제공)

2. "Request header field X is not allowed by Access-Control-Allow-Headers"

원인: 사용자 정의 헤더를 사용하지만 서버가 이를 허용하지 않음

해결 방법:

  • 서버 측에서 Access-Control-Allow-Headers에 해당 헤더 추가

3. "Method X not allowed by Access-Control-Allow-Methods"

원인: 사용하려는 HTTP 메서드가 서버에서 허용되지 않음

해결 방법:

  • 서버 측에서 Access-Control-Allow-Methods에 해당 메서드 추가

4. "Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is 'false'"

원인: 인증 정보를 포함한 요청을 보냈지만 서버가 이를 허용하지 않음

해결 방법:

  • 서버 측에서 Access-Control-Allow-Credentials: true 설정
  • Access-Control-Allow-Origin에 와일드카드(*) 대신 구체적인 출처 지정

개발 환경에서의 임시 해결책

⚠️ 주의: 아래 방법은 개발 환경에서만 사용해야 하며, 프로덕션 환경에서는 절대 사용하지 마세요!

  1. 브라우저 CORS 보안 비활성화: Chrome을 --disable-web-security 플래그와 함께 실행
  2. CORS 브라우저 확장 프로그램: "CORS Unblock"과 같은 확장 프로그램 사용
  3. 프록시 서버 사용: 개발 서버에서 API 요청을 프록시
// Create React App의 package.json에서 프록시 설정
{
  "name": "my-app",
  "version": "0.1.0",
  "proxy": "https://api.example.com"
}

보안 고려사항

CORS를 구성할 때 주의해야 할 보안 사항:

1. 와일드카드 사용 자제

Access-Control-Allow-Origin: *

모든 출처를 허용하는 와일드카드는 개발 환경이나 공개 API에서만 사용하고, 인증이 필요한 API에서는 사용하지 마세요.

2. 필요한 출처만 허용

가능한 한 구체적인 출처만 허용하세요:

Access-Control-Allow-Origin: https://app.example.com

여러 출처를 허용해야 하는 경우, 요청의 Origin 헤더를 확인하고 동적으로 응답하세요:

const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  // ...
  next();
});

3. 인증 요청 처리 주의

인증 정보가 포함된 요청(credentials: 'include')을 허용할 때:

  • Access-Control-Allow-Origin에 와일드카드(*)를 사용할 수 없음
  • 구체적인 출처를 지정해야 함
  • Access-Control-Allow-Credentials: true 설정 필요

4. 필요한 메서드와 헤더만 허용

모든 메서드와 헤더를 허용하는 대신 필요한 것만 명시적으로 허용하세요:

Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization

5. CSRF 보호

CORS는 CSRF 공격을 완전히 방지하지 못합니다. 다음과 같은 추가 보호 조치를 고려하세요:

  • CSRF 토큰 사용
  • SameSite 쿠키 속성 설정
  • 중요한 작업에 추가 인증 요구

실제 구현 예제

React + Express 애플리케이션에서의 CORS 구현

프론트엔드 (React)

// 인증 정보 없는 요청
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// 인증 정보 포함 요청
fetch('https://api.example.com/protected-data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  credentials: 'include',  // 쿠키 포함
  body: JSON.stringify({ key: 'value' })
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

백엔드 (Express)

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

// CORS 설정
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = ['https://app.example.com', 'https://www.example.com'];
    // 개발 환경에서는 origin이 undefined일 수 있음 (예: Postman)
    if (!origin || allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('CORS policy violation'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // 프리플라이트 캐싱 (24시간)
};

app.use(cors(corsOptions));
app.use(express.json());

// 공개 API
app.get('/data', (req, res) => {
  res.json({ message: 'Public data' });
});

// 보호된 API
app.post('/protected-data', authenticateToken, (req, res) => {
  res.json({ message: 'Protected data', user: req.user });
});

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) return res.sendStatus(401);

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

결론

CORS는 웹 보안의 중요한 부분으로, 웹 개발자가 반드시 이해해야 하는 개념입니다. 동일 출처 정책은 웹을 보호하는 중요한 메커니즘이지만, 현대 웹 애플리케이션의 복잡한 아키텍처에서는 제약이 될 수 있습니다. CORS는 이러한 제약을 안전하게 완화하는 표준 방법을 제공합니다.

CORS를 효과적으로 구현하기 위한 핵심 원칙:

  1. 최소 권한 원칙 적용: 필요한 출처, 메서드, 헤더만 허용
  2. 보안과 편의성 균형 유지: 과도한 제한은 사용성을 해치지만, 과도한 허용은 보안을 약화시킴
  3. 계층적 방어 전략 사용: CORS만으로는 완벽한 보안을 보장할 수 없으므로 CSRF 토큰, 적절한 인증 체계 등 추가 보안 조치 필요

CORS 관련 문제는 웹 개발에서 자주 발생하지만, 그 개념과 작동 방식을 이해하면 효과적으로 해결할 수 있습니다. 올바르게 구성된 CORS 정책은 애플리케이션의 보안을 유지하면서도 필요한 교차 출처 통신을 가능하게 합니다.


참고 자료

  • MDN Web Docs: 교차 출처 리소스 공유 (CORS)
  • OWASP: CORS 취약점
  • W3C CORS 스펙
  • Express CORS 미들웨어
  • Spring Framework CORS 지원
  • Django CORS Headers
반응형
저작자표시 비영리 변경금지 (새창열림)

'Frontend Development' 카테고리의 다른 글

script 태그의 성능 최적화: async와 defer 속성 완벽 가이드  (0) 2025.05.28
React 성능 최적화: 메모이제이션 기법으로 불필요한 리렌더링 방지하기  (4) 2025.05.28
React의 Error Boundary: 안정적인 프론트엔드 애플리케이션을 위한 필수 요소  (2) 2025.05.28
JavaScript Promise 완벽 가이드: resolve()와 fulfilled 상태 이해하기  (0) 2025.05.28
자바스크립트의 함수 정의 방식: 선언식 vs 표현식 완벽 가이드  (0) 2025.05.28
'Frontend Development' 카테고리의 다른 글
  • script 태그의 성능 최적화: async와 defer 속성 완벽 가이드
  • React 성능 최적화: 메모이제이션 기법으로 불필요한 리렌더링 방지하기
  • React의 Error Boundary: 안정적인 프론트엔드 애플리케이션을 위한 필수 요소
  • JavaScript Promise 완벽 가이드: resolve()와 fulfilled 상태 이해하기
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (100) N
      • Frontend Development (39) N
      • Backend Development (21) N
      • Algorithm (33) N
        • 백준 (11) N
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    frontend development
    tailwindcss
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
CORS 완벽 가이드: 웹 개발자가 반드시 알아야 할 교차 출처 리소스 공유
상단으로

티스토리툴바