프로그래밍/Python

🚫 프론트엔드 개발자의 악몽: "Access to fetch has been blocked by CORS policy"

Tiboong 2025. 10. 1. 11:28
728x90
반응형

"API는 잘 되는데 브라우저에서만 안 돼요!"

월요일 아침, 프론트엔드 개발자가 다급하게 찾아왔습니다.

 

"백엔드 API를 Postman에서 테스트하면 완벽하게 작동하는데, React에서 호출하면 계속 에러가 나요! 빨간색으로 'CORS policy'라고 뜨는데 이게 뭔가요?"

 

콘솔을 열어보니 그 유명한 에러 메시지가 보입니다:

Access to fetch at 'http://api.mysite.com/users/' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource.

 

이것이 바로 현대 웹 개발에서 프론트엔드와 백엔드를 분리하는 순간 거의 100%가 마주치는 문제, CORS (Cross-Origin Resource Sharing) 문제입니다.

 

27년간 수많은 웹 프로젝트를 경험하면서 확신하는 것은, CORS가 **"귀찮은 장애물이 아니라 필수적인 보안 메커니즘"**이라는 것입니다.

 

오늘은 CORS가 무엇인지, 왜 필요한지, 그리고 Django에서 어떻게 올바르게 설정하는지 알아보겠습니다.

🤔 CORS가 도대체 뭘까요?

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

먼저 CORS를 이해하려면 동일 출처 정책부터 알아야 합니다.

출처(Origin)란? 출처는 프로토콜 + 도메인 + 포트의 조합입니다.

 

같은 출처 예시:

https://mysite.com/page1
https://mysite.com/page2
→ 같은 출처! (프로토콜, 도메인, 포트 모두 동일)

 

다른 출처 예시:

https://mysite.com        (HTTPS)
http://mysite.com         (HTTP) → 프로토콜 다름!

https://mysite.com:443    (포트 443)
https://mysite.com:8000   (포트 8000) → 포트 다름!

https://mysite.com        (메인 도메인)
https://api.mysite.com    (서브도메인) → 도메인 다름!

 

왜 이렇게 엄격할까요? 브라우저는 사용자를 보호하기 위해 기본적으로 다른 출처의 리소스 접근을 차단합니다.

실생활 비유: 은행의 보안 시스템

CORS를 은행의 보안 시스템에 비유해보겠습니다.

 

상황 1: 같은 출처 (동일 은행 지점 간)

당신: A지점에서 계좌 개설
나중에: A지점에서 계좌 조회
은행: "같은 지점이니까 OK!"

 

상황 2: 다른 출처 (다른 은행)

당신: A은행 고객
악의적인 사이트: B은행 역할을 하며 당신의 A은행 정보 요청
브라우저(경비원): "잠깐! B은행이 A은행 정보를 요청하네? 위험해!"
브라우저: 요청 차단!

만약 이런 보안이 없다면?

악성 사이트: "은행 사이트처럼 보이는 페이지를 만들자"
악성 사이트: 실제 은행 API를 몰래 호출
브라우저: (보안 없으면) 그냥 허용
결과: 사용자 모르게 계좌 정보 탈취, 돈 이체 등

CORS의 탄생: 필요악의 해결책

문제 상황:

  • 보안을 위해 다른 출처 접근을 차단하는 것은 좋음
  • 하지만 현대 웹은 여러 서버를 사용해야 함 (프론트엔드 서버 + API 서버)
  • 모든 것을 막으면 정상적인 개발이 불가능

CORS의 해결책:

  • 기본적으로는 차단
  • 하지만 서버가 명시적으로 허용하면 접근 가능
  • "이 출처는 신뢰할 수 있어"라고 서버가 선언하는 메커니즘

💥 CORS 에러가 발생하는 실제 상황들

상황 1: 프론트엔드와 백엔드 분리 개발

전통적인 방식 (CORS 문제 없음):

Django가 모든 것을 담당:
- 템플릿 렌더링
- API 제공
- 정적 파일 제공

주소: https://mysite.com
→ 모든 것이 같은 출처!

 

현대적인 방식 (CORS 문제 발생):

프론트엔드: http://localhost:3000 (React, Vue 개발 서버)
백엔드: http://localhost:8000 (Django API)

→ 포트가 다르므로 다른 출처!
→ CORS 에러 발생!

 

왜 이렇게 개발할까요?

  • 기술 스택 자유: 프론트엔드는 React, 백엔드는 Django
  • 팀 분리: 프론트엔드 팀과 백엔드 팀이 독립적으로 작업
  • 확장성: 모바일 앱, 다른 서비스에서도 같은 API 사용
  • 성능: 정적 사이트 생성, CDN 활용 등

상황 2: 개발 환경과 운영 환경의 차이

개발 중:

프론트엔드: http://localhost:3000
백엔드: http://localhost:8000
→ CORS 설정 필요

 

배포 후:

프론트엔드: https://mysite.com
백엔드: https://api.mysite.com
→ 여전히 다른 출처이므로 CORS 설정 필요

 

일시적으로 같은 출처로 만들 수도 있음:

리버스 프록시 사용:
https://mysite.com/        → 프론트엔드
https://mysite.com/api/    → 백엔드 API
→ 같은 도메인이므로 CORS 불필요

상황 3: 외부 API 호출

써드파티 API 사용:

내 사이트: https://mysite.com
외부 API: https://api.external.com

브라우저에서 직접 호출:
→ 다른 출처이므로 CORS 에러!

해결책:
1. 서버에서 대신 호출 (프록시)
2. 외부 API가 CORS를 허용해야 함

🔍 CORS의 작동 원리 상세 분석

Simple Request: 간단한 요청

조건이 맞으면 바로 요청:

  • HTTP 메서드: GET, HEAD, POST 중 하나
  • 헤더: 기본 헤더만 사용
  • Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded

작동 과정:

1. 브라우저: "서버님, 데이터 주세요!"
   GET http://api.mysite.com/users/
   Origin: http://localhost:3000

2. 서버: "이 출처는 허용하지 않아"
   (Access-Control-Allow-Origin 헤더 없음)

3. 브라우저: "안전하지 않네, 차단!"
   → CORS 에러 발생

 

서버가 허용한다면:

1. 브라우저: 요청 전송
2. 서버: 응답 + 헤더
   Access-Control-Allow-Origin: http://localhost:3000
3. 브라우저: "서버가 허용했네, 통과!"
   → 정상 작동

Preflight Request: 사전 확인 요청

복잡한 요청은 먼저 확인:

  • HTTP 메서드: PUT, DELETE, PATCH 등
  • 커스텀 헤더 사용: Authorization, X-Custom-Header 등
  • Content-Type: application/json 등

작동 과정 (2단계):

1단계: Preflight (사전 확인)
브라우저: "이런 요청 보내도 돼요?"
OPTIONS http://api.mysite.com/users/
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

서버: "네, 괜찮아요!"
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type

2단계: 실제 요청
브라우저: "확인했으니 실제 요청 보낼게요"
POST http://api.mysite.com/users/
(실제 데이터 전송)

 

왜 이렇게 복잡할까요?

  • 서버 보호: 위험한 요청이 실제로 실행되기 전에 차단 가능
  • 비용 절감: 큰 데이터를 보내기 전에 허용 여부 확인
  • 보안 강화: 서버가 명시적으로 허용한 요청만 처리

인증 정보와 CORS

쿠키, 인증 토큰을 포함한 요청:

// 프론트엔드 코드
fetch('http://api.mysite.com/users/', {
  credentials: 'include',  // 쿠키 포함!
  headers: {
    'Authorization': 'Bearer token123'
  }
})

 

더 엄격한 검증:

서버가 이렇게 응답하면 에러:
Access-Control-Allow-Origin: *  // 모든 출처 허용

브라우저: "인증 정보 포함 시 *는 안 돼!"

올바른 응답:
Access-Control-Allow-Origin: http://localhost:3000  // 명시적 출처
Access-Control-Allow-Credentials: true  // 인증 정보 허용

 

왜 더 엄격할까요?

  • 쿠키 탈취 방지: 모든 사이트가 인증 쿠키를 볼 수 있으면 위험
  • 세션 하이재킹 방지: 특정 출처만 인증 정보에 접근 가능

🛠️ Django에서 CORS 올바르게 설정하기

django-cors-headers 패키지 사용

설치 및 기본 설정:

pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
    # ...
    'corsheaders',
    # ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 최대한 위쪽에!
    'django.middleware.common.CommonMiddleware',
    # ...
]

 

왜 미들웨어 순서가 중요한가?

  • CORS 체크는 다른 모든 처리보다 먼저 해야 함
  • 나중에 하면 이미 요청이 처리된 후라 의미 없음
  • 보안 검사는 가장 먼저 하는 것이 원칙

개발 환경 설정

개발 중에는 모든 출처 허용 (편의성):

# settings/development.py
CORS_ALLOW_ALL_ORIGINS = True  # 개발 중에만!

# 또는 특정 출처만
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",  # React 개발 서버
    "http://localhost:8080",  # Vue 개발 서버
    "http://127.0.0.1:3000",
]

 

왜 개발 중에는 느슨하게?

  • 빠른 개발: 출처를 계속 추가하는 번거로움 제거
  • 테스트 용이: 다양한 환경에서 쉽게 테스트
  • 학습 곡선: CORS 때문에 개발이 막히는 것 방지

주의사항:

  • 개발 환경에서만! 절대 운영 환경에서는 사용 금지

운영 환경 설정

명시적이고 제한적인 설정:

# settings/production.py
CORS_ALLOWED_ORIGINS = [
    "https://mysite.com",
    "https://www.mysite.com",
    "https://app.mysite.com",
]

# 인증 정보 허용 (필요한 경우만)
CORS_ALLOW_CREDENTIALS = True

# 허용할 HTTP 메서드
CORS_ALLOW_METHODS = [
    'GET',
    'POST',
    'PUT',
    'PATCH',
    'DELETE',
    'OPTIONS',
]

# 허용할 헤더
CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'origin',
    'user-agent',
]

# Preflight 요청 결과 캐싱 시간 (초)
CORS_PREFLIGHT_MAX_AGE = 86400  # 24시간

 

각 설정의 의미:

CORS_ALLOWED_ORIGINS:

  • 화이트리스트 방식: 정확히 나열된 출처만 허용
  • 보안 최우선: 알지 못하는 출처는 모두 차단
  • 유지보수: 새로운 도메인 추가 시 명시적으로 설정

CORS_ALLOW_CREDENTIALS:

  • 인증 쿠키 허용: 로그인 세션이 필요한 경우
  • JWT 토큰: Authorization 헤더 사용 시에도 필요
  • 보안 강화: CORS_ALLOWED_ORIGINS와 함께 사용 필수

CORS_PREFLIGHT_MAX_AGE:

  • 성능 최적화: Preflight 요청 횟수 감소
  • 24시간 캐싱: 같은 요청이 반복되어도 한 번만 확인
  • 네트워크 절약: 불필요한 OPTIONS 요청 제거

환경별 동적 설정

환경 변수 활용:

# settings.py
import os

# 환경 변수에서 허용할 출처 읽기
CORS_ALLOWED_ORIGINS = os.getenv(
    'CORS_ALLOWED_ORIGINS',
    'http://localhost:3000'
).split(',')

# .env 파일 (개발)
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080

# .env 파일 (운영)
CORS_ALLOWED_ORIGINS=https://mysite.com,https://www.mysite.com

 

장점:

  • 코드 수정 없이 환경별 설정 가능
  • 보안 향상: 민감한 설정을 코드에 하드코딩하지 않음
  • 배포 유연성: 환경 변수만 바꾸면 됨

🚨 CORS 설정 시 흔한 실수들

실수 1: "일단 모든 출처 허용하고 나중에..."

위험한 설정:

# ❌ 절대 운영 환경에서 사용 금지!
CORS_ALLOW_ALL_ORIGINS = True

 

왜 위험한가?

  • 모든 웹사이트가 당신의 API를 호출 가능
  • 악의적인 사이트가 사용자 정보를 탈취 가능
  • CSRF 공격에 완전 무방비
  • "나중에"는 영원히 오지 않음

실제 피해 사례:

해커 사이트: "당신의 은행 사이트 같은 UI"
사용자: 로그인 시도
해커 사이트: 실제 은행 API 호출 (CORS 허용되어 있어서 가능)
결과: 사용자 인증 정보, 계좌 정보 모두 탈취

실수 2: Credentials와 Wildcard 함께 사용

작동하지 않는 조합:

# ❌ 에러 발생!
CORS_ALLOW_ALL_ORIGINS = True  # 또는 '*'
CORS_ALLOW_CREDENTIALS = True

 

왜 안 될까?

  • 브라우저가 명시적으로 거부
  • "모든 사이트"가 인증 정보를 가져가는 것은 너무 위험
  • 보안을 위한 의도적인 제한

올바른 설정:

# ✅ 정확한 출처 명시
CORS_ALLOWED_ORIGINS = ['https://mysite.com']
CORS_ALLOW_CREDENTIALS = True

실수 3: 프로토콜이나 포트 빠뜨림

미묘한 차이가 문제:

# 설정
CORS_ALLOWED_ORIGINS = [
    "https://mysite.com"  # HTTPS
]

# 실제 프론트엔드
http://mysite.com  # HTTP

# 결과: CORS 에러! (프로토콜이 다름)

 

또 다른 예시:

# 설정
CORS_ALLOWED_ORIGINS = [
    "https://mysite.com:443"  # 포트 명시
]

# 브라우저 요청
Origin: https://mysite.com  # 포트 생략 (기본 포트)

# 결과: 대부분의 브라우저는 OK, 일부는 에러

 

안전한 방법:

# 명확하게 모두 나열
CORS_ALLOWED_ORIGINS = [
    "https://mysite.com",
    "https://www.mysite.com",
    "http://localhost:3000",  # 개발용
]

실수 4: 미들웨어 순서 무시

잘못된 순서:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ... 많은 미들웨어들
    'corsheaders.middleware.CorsMiddleware',  # ❌ 너무 늦음!
]

 

올바른 순서:

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # ✅ 가장 위에!
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

 

왜 순서가 중요한가?

  • CORS는 가장 먼저 체크되어야 함
  • 다른 미들웨어가 먼저 실행되면 이미 요청이 처리된 후
  • 에러가 발생해도 CORS 헤더가 없어서 브라우저가 에러 메시지조차 못 봄

🔒 CORS와 보안의 균형

CORS는 보안이 아니라 보안 메커니즘

중요한 사실: CORS는 브라우저의 보안 기능이지, 서버의 보안이 아닙니다.

의미:

Postman이나 curl로 API 호출:
→ CORS 체크 없음! (브라우저가 아니므로)
→ 정상 작동

브라우저에서 API 호출:
→ CORS 체크 실행
→ 허용되지 않으면 차단

 

결론:

  • CORS는 브라우저에서의 악용 방지
  • 서버 자체의 보안은 별도로 구현 필요
  • 인증, 권한, 입력 검증 등은 CORS와 무관하게 필수

다층 방어 전략

CORS만으로는 부족합니다:

 

1. 인증 (Authentication):

  • JWT 토큰, 세션 쿠키 등
  • "너는 누구냐?"

2. 인가 (Authorization):

  • 권한 체크, 역할 기반 접근 제어
  • "너는 이걸 할 수 있냐?"

3. CORS:

  • 출처 기반 접근 제어
  • "너는 어디서 왔냐?"

4. CSRF 보호:

  • 토큰 기반 요청 검증
  • "이 요청이 정말 내 사이트에서 온 거냐?"

5. Rate Limiting:

  • 요청 빈도 제한
  • "너무 많이 요청하는 거 아냐?"

실전 보안 체크리스트

운영 환경 배포 전 필수 확인:

  • [ ] CORS_ALLOW_ALL_ORIGINS = False 확인
  • [ ] CORS_ALLOWED_ORIGINS에 정확한 도메인만 등록
  • [ ] HTTPS만 허용 (HTTP는 개발 환경만)
  • [ ] 불필요한 HTTP 메서드는 제거
  • [ ] CORS_PREFLIGHT_MAX_AGE 적절히 설정
  • [ ] 인증이 필요한 엔드포인트는 반드시 인증 체크
  • [ ] CORS 로그 모니터링 설정

🎯 실제 해결 사례: 핀테크 스타트업

문제 발견과 초기 상황

서비스 특성:

  • React 프론트엔드 + Django REST API 백엔드
  • 금융 데이터 처리 및 결제 시스템
  • 모바일 앱과 웹 모두 지원
  • 엄격한 보안 요구사항

발생한 문제들:

  • 개발 환경에서는 완벽히 작동
  • 배포 후 모든 API 호출이 CORS 에러
  • 급하게 CORS_ALLOW_ALL_ORIGINS = True 설정
  • 보안 감사에서 심각한 취약점 지적

보안팀의 경고:

  • "모든 출처를 허용하면 피싱 사이트가 API 악용 가능"
  • "인증 쿠키가 노출되어 계좌 정보 탈취 위험"
  • "금융감독원 보안 기준 위배"

체계적인 해결 과정

1단계: 긴급 조치

# 즉시 수정
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
    'https://app.oursite.com',  # 프로덕션 웹
    'https://www.oursite.com',  # 메인 사이트
]
CORS_ALLOW_CREDENTIALS = True

 

2단계: 환경별 분리

# settings/base.py (공통)
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']

# settings/development.py
CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',
    'http://localhost:3001',
]

# settings/staging.py
CORS_ALLOWED_ORIGINS = [
    'https://staging.oursite.com',
]

# settings/production.py
CORS_ALLOWED_ORIGINS = [
    'https://app.oursite.com',
    'https://www.oursite.com',
]

 

3단계: 모바일 앱 대응

# 모바일 앱은 CORS 영향 없음 (네이티브)
# 하지만 하이브리드 앱(WebView)은 필요

# 앱 버전별 출처 관리
CORS_ALLOWED_ORIGINS = [
    # 웹
    'https://app.oursite.com',
    # 하이브리드 앱 (Capacitor/Cordova)
    'capacitor://localhost',
    'ionic://localhost',
]

 

4단계: 모니터링 시스템 구축

# 커스텀 미들웨어로 CORS 요청 로깅
class CorsLoggingMiddleware:
    def __call__(self, request):
        origin = request.META.get('HTTP_ORIGIN')
        if origin and origin not in settings.CORS_ALLOWED_ORIGINS:
            logger.warning(
                f"Blocked CORS request from {origin} to {request.path}"
            )
        return self.get_response(request)

해결 후 성과

보안 개선:

  • 보안 감사 통과: "심각" → "양호" 등급
  • 피싱 공격 시도 차단: 월 평균 50건 자동 차단
  • 금융감독원 보안 기준 완벽 충족
  • 고객 신뢰도 크게 향상

운영 효율:

  • CORS 관련 고객 문의: 주 20건 → 0건
  • 개발 생산성: 환경별 자동 설정으로 개발 시간 단축
  • 장애 대응: 모니터링으로 문제 사전 감지
  • 규정 준수: 자동화된 보안 체크

예상치 못한 효과:

  • 명확한 환경 분리로 전체 시스템 안정성 향상
  • 보안 의식 고취로 다른 영역의 보안도 개선
  • 체계적인 문서화로 신규 개발자 온보딩 용이

🎓 정리하며: 완벽한 CORS 관리 원칙

1. CORS는 보안의 한 부분일 뿐

CORS만으로 보안이 완성되지 않습니다. 인증, 인가, 입력 검증 등 다른 보안 메커니즘과 함께 사용하세요.

2. 개발 편의성과 보안의 균형

개발 환경에서는 편리하게, 운영 환경에서는 엄격하게 설정하세요. 환경별로 명확히 분리하는 것이 핵심입니다.

3. 명시적인 허용이 최선

*(와일드카드)보다는 정확한 도메인 목록을 관리하세요. 불편하더라도 안전합니다.

4. 정기적인 점검과 업데이트

도메인이 추가되거나 변경될 때마다 CORS 설정도 함께 업데이트하세요. 문서화도 필수입니다.

5. 모니터링과 로깅

차단된 CORS 요청을 로깅하여 실제 공격 시도나 설정 오류를 빠르게 발견하세요.

6. 팀 전체의 이해

프론트엔드와 백엔드 개발자 모두 CORS를 이해해야 효율적인 협업이 가능합니다.


💬 CORS 문제로 개발이 막혀 있나요?

"CORS 에러가 계속 나와서 개발을 못하겠어요", "보안은 지키면서 제대로 설정하고 싶어요"

27년 경력의 시니어 개발자가 여러분의 Django 프로젝트 CORS 설정을 완벽하게 구축해드립니다.

  • 🔍 현재 CORS 설정 분석 및 보안 취약점 진단
  • 🛡️ 환경별 최적화된 CORS 정책 수립
  • 📊 프론트엔드-백엔드 연동 아키텍처 설계
  • 🎓 팀 전체 CORS 이해도 향상 교육

➡️ Django CORS 전문가 상담받기

"CORS로 시간 낭비하지 마세요. 전문가와 함께 한 번에 제대로 설정하세요!"

728x90
반응형