---
term: '교차 출처 리소스 공유 (CORS)'
seoTitle: 'CORS란? 오류, 프리플라이트, 해결 방법 총정리'
headline: 'CORS(교차 출처 리소스 공유)란?'
slug: cors-cross-origin-resource-sharing
category: api-realtime
shortDefinition: 'CORS는 서버가 자신을 호출할 수 있는 다른 출처를 선언하게 해 동일 출처 정책을 의도적으로 완화하는 브라우저 메커니즘입니다.'
relatedTerms:
  - api-key-security
  - api-gateway-architecture
  - cross-site-scripting-xss-prevention
contrastsWith:
  - cross-site-scripting-xss-prevention
faq:
  - question: 'CORS를 쉽게 설명하면 무엇입니까?'
    answer: '사이트 간 API 호출을 위한 브라우저의 허가 체계입니다. 기본적으로 한 출처의 스크립트는 다른 출처의 응답을 읽을 수 없습니다. 이것이 동일 출처 정책입니다. CORS는 서버가 이를 명시적으로 허용하는 방법으로, 어떤 출처, 메서드, 헤더를 받아들이는지 응답 헤더로 선언합니다. 강제는 브라우저가 하고, 선언은 서버가 하며, 프론트엔드 코드는 전달자일 뿐입니다.'
  - question: '출처(origin)란 정확히 무엇입니까?'
    answer: '스킴, 호스트, 포트의 세 요소입니다. https://app.example.com과 https://api.example.com은 호스트가 다르므로 서로 다른 출처입니다. 한 사이트의 http 버전과 https 버전도 스킴이 다르므로 다른 출처이고, 개발 환경의 :3000과 :8080도 포트가 다르므로 마찬가지입니다. 모든 CORS 판단은 이 세 부분을 비교하며, URL의 나머지는 전혀 상관이 없습니다.'
  - question: '전형적인 CORS 오류는 왜 발생합니까?'
    answer: '브라우저가 다른 출처를 호출했는데, 응답에 현재 출처와 일치하는 Access-Control-Allow-Origin 헤더가 없어서 브라우저가 스크립트의 응답 읽기를 차단한 것입니다. 요청 자체는 서버에 정상적으로 도달한 경우가 많습니다. 차단은 클라이언트 측에서, 읽기 단계에서 일어납니다. 그래서 해결책은 언제나 서버 설정이며, 결코 프론트엔드 코드가 아닙니다.'
  - question: '프리플라이트 요청이란 무엇입니까?'
    answer: '단순하지 않은 요청에 대해 브라우저가 미리 하는 허가 확인입니다. PUT이나 DELETE, 또는 인증 토큰 같은 커스텀 헤더가 붙은 요청을 보내기 전에, 브라우저는 "보내도 됩니까?"라고 묻는 OPTIONS 요청을 먼저 보냅니다. 서버의 헤더가 허용되는 메서드, 헤더, 출처를 답하고, 그 뒤에야 실제 요청이 전송됩니다. 프리플라이트 결과는 Access-Control-Max-Age로 캐시할 수 있습니다.'
  - question: '왜 curl에서는 되는 요청이 브라우저에서는 실패합니까?'
    answer: 'CORS는 서버의 거부가 아니라 브라우저의 강제이기 때문입니다. curl 같은 도구와 네이티브 모바일 앱에는 동일 출처 정책이 없으므로 응답을 문제없이 읽습니다. 브라우저는 사용자를 대신해 이 정책을 적용합니다. 지금 보이는 차이는 강제 지점의 차이이며, 동시에 CORS가 접근 제어가 아니라는 증거이기도 합니다.'
  - question: 'Access-Control-Allow-Origin: *는 안전합니까?'
    answer: '자격 증명이 필요 없는 진정한 공개 API라면 안전합니다. 자격 증명 모드에서는 와일드카드가 금지되어 있으며, 브라우저는 설계상 와일드카드에 대해 쿠키 전송을 거부합니다. 그리고 자격 증명을 허용하면서 임의의 출처를 그대로 반사하는 설정은 CORS를 방패에서 구멍으로 바꾸는 전형적인 잘못된 구성입니다. 비공개 API는 출처를 명시적으로 나열해야 합니다.'
  - question: 'CORS를 비활성화해서 오류를 해결할 수 있습니까?'
    answer: '연기 감지기를 떼어 내서 화재를 해결한다는 의미에서만 가능합니다. 브라우저 플래그와 무엇이든 허용하는 프록시는 내 컴퓨터에서 증상만 가릴 뿐, 문제를 모든 사용자에게 그대로 배포합니다. 올바른 해결책은 몇 분이면 됩니다. 서버(또는 플랫폼 대시보드)에서 호출을 허용해야 할 출처를 허용하도록 설정하는 것입니다.'
  - question: 'CORS, CSRF, CSP의 차이는 무엇입니까?'
    answer: '하는 일이 전혀 다릅니다. CORS는 어떤 출처가 서버의 응답을 읽을 수 있는지를 관할합니다. CSRF는 공격입니다. 사용자의 쿠키에 편승해 요청을 위조하며, CORS가 아니라 토큰과 SameSite 쿠키로 막습니다. CSP는 페이지가 무엇을 로드하고 실행할 수 있는지 제한하는 페이지 자체의 정책으로, XSS 방어 도구입니다. 이웃한 약어일 뿐 서로 독립적인 메커니즘입니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'CORS — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS'
  - name: 'Fetch Standard — CORS protocol (WHATWG)'
    url: 'https://fetch.spec.whatwg.org/#http-cors-protocol'
  - name: 'Same-origin policy — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy'
  - name: 'Back4app documentation'
    url: 'https://www.back4app.com/docs'
  - name: 'Cross-origin resource sharing — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/Cross-origin_resource_sharing'
cta:
  title: '그냥 동작하는 교차 출처 호출'
  text: 'Back4app API는 기본적으로 프리플라이트에 응답하고 올바른 CORS 헤더를 보냅니다. 웹 앱은 허용한 어떤 출처에서든 백엔드를 호출할 수 있고, 네이티브 앱은 이 절차를 아예 거치지 않습니다. 프록시 꼼수는 전혀 필요 없습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-30'
translationKey: cors-cross-origin-resource-sharing
---

**CORS는 서버가 자신을 호출할 수 있는 다른 출처를 선언하게 해 동일 출처 정책을 의도적으로 완화하는 브라우저 메커니즘입니다.** 관점 두 가지만 바꾸면 혼란의 대부분이 사라집니다. CORS를 강제하는 것은 서버가 아니라 *브라우저*이고(그래서 curl은 동작합니다), CORS를 고치는 것은 프론트엔드가 아니라 *서버*입니다(그래서 JavaScript를 아무리 고쳐도 소용이 없습니다). 나머지는 전부 헤더의 문제입니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 기반 | 동일 출처 정책: 기본적으로 스크립트는 다른 출처의 응답을 읽을 수 없음 |
| 출처 | 스킴 + 호스트 + 포트. 셋 모두 일치해야 함 |
| CORS | 특정 출처를 허용하는 서버 헤더. 강제는 브라우저가 담당 |
| 프리플라이트 | 단순하지 않은 요청 전에 보내는 OPTIONS "보내도 됩니까?" |
| 변하지 않는 진실 | CORS 오류는 서버에서 고침. 예외 없음 |

## 전송 구간에서 본 프로토콜 전체

```text
# 프리플라이트: 인증 헤더가 붙은 PUT을 보내기 전에 브라우저가 먼저 묻습니다
OPTIONS /classes/Product HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-parse-session-token

# 서버가 내주는 허가증
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: x-parse-session-token
Access-Control-Max-Age: 7200         ← 이 응답을 캐시해 반복 프리플라이트를 생략

# 그다음에야 비로소 실제 요청이 전송됩니다.
```

애플리케이션 코드에서는 이렇게 보입니다. 코드 탭이 보여 주듯, CORS는 *브라우저*의 문제이며 네이티브 앱은 이를 마주칠 일이 없습니다:

**JavaScript:**

```javascript
// Browser JavaScript — Back4app JS SDK
// A cross-origin call that just works: the platform answers the
// preflight and sends the CORS headers, so the browser lets it through
Parse.initialize('APP_ID', 'JS_KEY');
Parse.serverURL = 'https://parseapi.back4app.com'; // different origin than your site

const products = await new Parse.Query('Product').find();
// No proxy hacks, no "disable CORS" — the server side is configured correctly.
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Native apps have no same-origin policy — CORS is a browser concern.
// Flutter Web, however, DOES enforce it: same browser rules apply there.
final query = QueryBuilder<ParseObject>(ParseObject('Product'));
final response = await query.query();
// Works identically on mobile and web because the server sends CORS headers.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// No browser, no same-origin policy: CORS never applies to native iOS.
// The same backend serves browsers (with CORS headers) and apps alike.
let query = Product.query()
query.find { result in
  if case .success(let products) = result { render(products) }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// No browser, no same-origin policy: CORS never applies to native Android.
// The same backend serves browsers (with CORS headers) and apps alike.
val query = ParseQuery.getQuery<ParseObject>("Product")
query.findInBackground { products, e ->
  if (e == null) render(products)
}
```

## 브라우저의 판단 흐름

```mermaid
flowchart TB
  accTitle: 브라우저가 교차 출처 요청을 판단하는 방식
  accDescr: 동일 출처 요청은 그대로 진행됩니다. 단순한 교차 출처 요청은 전송된 뒤 응답에 출처 허용 헤더가 있는지 확인됩니다. 단순하지 않은 요청은 먼저 OPTIONS 프리플라이트를 발생시키며, 헤더가 없거나 일치하지 않으면 브라우저가 페이지의 응답 접근을 차단합니다.
  R["스크립트가 요청을 보냄"] --> S{"같은 출처인가?"}
  S -- 예 --> OK["그대로 진행, CORS 개입 없음"]
  S -- 아니요 --> T{"단순 요청인가?<br/>(GET/HEAD/POST, 안전 목록 헤더)"}
  T -- 예 --> D["전송 후 응답에서<br/>Access-Control-Allow-Origin 확인"]
  T -- 아니요 --> P["먼저 OPTIONS 프리플라이트"]
  P --> D
  D -->|"헤더가 출처와 일치"| OK2["응답 읽기 가능"]
  D -->|"누락 / 불일치"| B["브라우저가 차단<br/>(콘솔 오류)"]
```

디버깅 세션 대부분은 두 가지 세부 사항에서 갈립니다. **단순 요청**(안전 목록에 있는 헤더와 일반 콘텐츠 타입을 쓰는 GET/HEAD/POST)은 프리플라이트를 건너뜁니다. 그래서 `Authorization` 헤더를 추가하면 잘 되던 엔드포인트가 갑자기 "고장" 나는 것입니다. 그리고 **자격 증명이 포함된 요청**은 모든 조건을 강화합니다. [명세](https://fetch.spec.whatwg.org/#http-cors-protocol)에 따르면 쿠키는 `Access-Control-Allow-Credentials: true`에 *더해* 정확한 출처가 지정될 때만 전달되며, 와일드카드로는 절대 전달되지 않습니다.

## 흔한 CORS 오류 해결 방법(오류 → 원인 → 해결)

| 콘솔 메시지 | 실제로 일어난 일 | 해결책(항상 서버 측) |
| --- | --- | --- |
| No 'Access-Control-Allow-Origin' header | 서버가 해당 출처를 허용한 적이 없음 | 허용 목록에 출처 추가 |
| Origin not allowed by Access-Control-Allow-Origin | 허용 목록은 있지만 해당 출처가 없음 | 정확한 스킴+호스트+포트 추가 |
| Response to preflight… doesn't pass | OPTIONS를 처리하지 않거나 헤더가 불완전함 | 메서드/헤더를 담아 OPTIONS에 응답 |
| Wildcard '*' cannot be used with credentials | 쿠키 + `*`. 금지된 조합 | 출처를 명시적으로 나열 |
| Request header not allowed | 커스텀 헤더가 허용 목록에 없음 | Access-Control-Allow-Headers에 추가 |

그리고 짚어 둘 만한 잘못된 해결책이 있습니다. 브라우저 플래그로 CORS를 끄거나 무엇이든 허용하는 개발용 프록시를 쓰면 오류가 *내 컴퓨터에서만* 보이지 않게 됩니다. 배포하면 사용자 쪽에서는 여전히 고장 납니다. 진짜 해결책은 몇 분이면 끝나는 서버 설정입니다.

## 일반적인 사용 사례

- **서로 다른 출처의 SPA + API** – app.example.com이 api.example.com을 호출합니다. CORS가 존재하는 이유인 일상적인 경우입니다.
- **로컬 개발** – localhost:3000에서 실제 백엔드를 호출합니다. 방패를 끄지 말고 개발용 출처를 허용해야 합니다.
- **공개 API** – 와일드카드 출처, 자격 증명 없음. 진정한 공개 데이터라면 올바르고 안전한 구성입니다.
- **다중 프론트엔드 플랫폼** – 여러 앱이 하나의 백엔드를 쓰며, 환경별로 명시적인 출처 목록을 둡니다.
- **BaaS 백엔드** – 플랫폼이 프리플라이트에 응답하고 헤더를 보냅니다. 사용자가 할 일은 허용할 출처를 선언하는 것으로 줄어듭니다.

## 와일드카드 vs. 명시적 출처: 결정 매트릭스

| 구성 | 적합한 경우… | 절대 쓰면 안 되는 경우… |
| --- | --- | --- |
| 와일드카드 `*` | 자격 증명이 없는 공개 API | 쿠키나 사용자 세션이 존재함 |
| 명시적 출처 목록 | 비공개 API, 자격 증명을 쓰는 앱 | — (이것이 기본 답) |
| 요청 출처 반사 | 거의 없음 | 자격 증명과 결합할 때. 전형적인 보안 구멍 |
| 환경별 목록 | 개발/스테이징/프로덕션 환경 분리 | 프로덕션이 개발 환경의 localhost 항목을 물려받을 때 |

보안 관점의 재정리 하나로 매트릭스를 마무리합니다. CORS는 **접근 제어가 아닙니다**. 네이티브 앱과 서버는 CORS를 완전히 무시하므로, CORS는 호출자로부터 API를 지키는 것이 아니라, 악성 사이트로부터 *브라우저 사용자*를 지킵니다. API를 지키는 일은 인증과 [데이터 계층 권한](/glossary/ko/data-layer-vs-application-layer-security/)이 맡고, CORS는 어떤 웹사이트의 스크립트가 응답을 읽을 수 있는지만 결정합니다.

## 한계와 트레이드오프

- **브라우저만 관할합니다.** 브라우저가 아닌 것은 모두 그냥 지나갑니다. 출처 목록을 인가(권한 부여)로 착각해서는 안 됩니다.
- **프리플라이트는 왕복 한 번의 비용이 듭니다.** 단순하지 않은 요청에서 발생하며, `Access-Control-Max-Age` 캐싱이 저렴하지만 잊히기 쉬운 해결책입니다.
- **잘못된 구성은 요청을 차단하는 쪽으로, 눈에 띄게 실패합니다.** 안전에는 좋지만, 위의 오류→해결 표 없이 디버깅하기에는 가혹합니다.
- **출처 목록은 환경마다 다른 설정입니다.** 스테이징 출처가 프로덕션 설정으로 새어 들어가므로, 자격 증명을 다루듯 목록을 감사해야 합니다.
- **CORS ≠ CSRF ≠ CSP.** 이웃한 약어이지만 별개의 방어 수단입니다. 쿠키에는 여전히 SameSite/토큰 보호가, 페이지에는 여전히 콘텐츠 정책이 필요하며, CORS는 교차 출처 읽기만 담당합니다.

## Back4app에서의 CORS

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. CORS라는 번거로운 일은 처음부터 처리된 상태로 제공됩니다. 플랫폼 API가 프리플라이트에 응답하고 올바른 헤더를 보내므로, 브라우저 앱은 프록시 꼼수 없이 허용된 출처에서 백엔드를 호출합니다. 위 JavaScript 탭의 코드가 개발자가 할 일의 전부입니다. 한편 Flutter, Swift, Kotlin 탭은 네이티브 클라이언트가 이 절차를 아예 마주치지 않는다는 조용한 진실을 보여 줍니다. 실제 보안은 있어야 할 곳에 남아 있습니다. 세션, ACL, 클래스 수준 권한이 어느 출처에서 오든 모든 요청에 대해 서버 측에서 강제됩니다.
