**CORS(Cross-Origin Resource Sharing)**๋Š” HTTP ํ—ค๋” ๊ธฐ๋ฐ˜ ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ, ์„œ๋ฒ„๊ฐ€ ์ž์‹ ์˜ ์ถœ์ฒ˜(origin)์™€ ๋‹ค๋ฅธ ์ถœ์ฒ˜์—์„œ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ์„ ํ—ˆ์šฉํ• ์ง€ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Same-Origin Policy(๋™์ผ ์ถœ์ฒ˜ ์ •์ฑ…)๋ฅผ ์ ์šฉํ•˜์—ฌ ๋‹ค๋ฅธ ์ถœ์ฒ˜๋กœ์˜ HTTP ์š”์ฒญ์„ ์ฐจ๋‹จํ•˜๋Š”๋ฐ, CORS๋Š” ์ด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์™„ํ™”ํ•˜๋Š” ํ‘œ์ค€ํ™”๋œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

**์ถœ์ฒ˜(Origin)**๋Š” ํ”„๋กœํ† ์ฝœ + ๋„๋ฉ”์ธ + ํฌํŠธ์˜ ์กฐํ•ฉ์ž…๋‹ˆ๋‹ค:

  • https://example.com:443 โ†’ ํ•˜๋‚˜์˜ ์ถœ์ฒ˜
  • http://example.com:80 โ†’ ๋‹ค๋ฅธ ์ถœ์ฒ˜ (ํ”„๋กœํ† ์ฝœ ๋‹ค๋ฆ„)
  • https://api.example.com:443 โ†’ ๋‹ค๋ฅธ ์ถœ์ฒ˜ (๋„๋ฉ”์ธ ๋‹ค๋ฆ„)

ํ•ด๋‹น ๊ฐœ๋…์ด ํ•„์š”ํ•œ ์ด์œ 

  • Same-Origin Policy์˜ ์ œ์•ฝ ๊ทน๋ณต: ๋ธŒ๋ผ์šฐ์ €๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. CORS๋ฅผ ํ†ตํ•ด ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ถœ์ฒ˜์— ํ•œํ•ด ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • API ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๋ถ„๋ฆฌ: ์ตœ๊ทผ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํ”„๋ก ํŠธ์—”๋“œ(https://app.example.com)์™€ ๋ฐฑ์—”๋“œ API(https://api.example.com)๋ฅผ ๋ณ„๋„ ๋„๋ฉ”์ธ์—์„œ ์šด์˜ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. CORS ์—†์ด๋Š” ์ด๋Ÿฌํ•œ ๊ตฌ์กฐ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค
  • ์จ๋“œํŒŒํ‹ฐ API ํ†ตํ•ฉ: ์™ธ๋ถ€ API๋ฅผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•  ๋•Œ CORS ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค (Google Maps API, ๊ฒฐ์ œ API ๋“ฑ)

AS-IS: Same-Origin Policy๋กœ ์ธํ•œ ์ฐจ๋‹จ

sequenceDiagram
    autonumber
    participant Browser
    participant Frontend as Frontend<br/>(https://app.example.com)
    participant API as API Server<br/>(https://api.example.com)

    Browser->>Frontend: ํŽ˜์ด์ง€ ๋กœ๋“œ
    Frontend->>Browser: fetch('https://api.example.com/data')
    Browser->>API: GET /data<br/>Origin: https://app.example.com
    API->>Browser: 200 OK (CORS ํ—ค๋” ์—†์Œ)
    Browser->>Frontend: โŒ CORS ์—๋Ÿฌ<br/>"Access-Control-Allow-Origin header missing"

    Note over Browser,API: Same-Origin Policy๋กœ ์ธํ•ด ์‘๋‹ต ์ฐจ๋‹จ

TO-BE: CORS ํ—ค๋”๋กœ ์ ‘๊ทผ ํ—ˆ์šฉ

sequenceDiagram
    autonumber
    participant Browser
    participant Frontend as Frontend<br/>(https://app.example.com)
    participant API as API Server<br/>(https://api.example.com)

    Browser->>Frontend: ํŽ˜์ด์ง€ ๋กœ๋“œ
    Frontend->>Browser: fetch('https://api.example.com/data')
    Browser->>API: GET /data<br/>Origin: https://app.example.com
    API->>Browser: 200 OK<br/>Access-Control-Allow-Origin: https://app.example.com
    Browser->>Frontend: โœ… ์‘๋‹ต ์ „๋‹ฌ ์„ฑ๊ณต

    Note over Browser,API: CORS ํ—ค๋”๋กœ ์ ‘๊ทผ ํ—ˆ์šฉ

CORS ๋™์ž‘ ์›๋ฆฌ

CORS๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค:

  1. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ cross-origin ์š”์ฒญ์— Origin ํ—ค๋”๋ฅผ ํฌํ•จํ•˜์—ฌ ์ „์†ก
  2. ์„œ๋ฒ„๊ฐ€ Access-Control-* ํ—ค๋”๋กœ ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ์‘๋‹ต
  3. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‘๋‹ต ํ—ค๋”๋ฅผ ํ™•์ธํ•˜์—ฌ ์ ‘๊ทผ ํ—ˆ์šฉ ๋˜๋Š” ์ฐจ๋‹จ
  4. ํŠน์ • ์š”์ฒญ์˜ ๊ฒฝ์šฐ ์‹ค์ œ ์š”์ฒญ ์ „์— **Preflight(์‚ฌ์ „ ์š”์ฒญ)**์„ ๋จผ์ € ์ „์†ก

Simple Request vs Preflighted Request

CORS ์š”์ฒญ์€ ๋‘ ๊ฐ€์ง€ ์œ ํ˜•์œผ๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.

Simple Request (๋‹จ์ˆœ ์š”์ฒญ)

Preflight ์—†์ด ๋ฐ”๋กœ ์‹ค์ œ ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋‹ค์Œ ์กฐ๊ฑด์„ ๋ชจ๋‘ ๋งŒ์กฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

์กฐ๊ฑด 1: ํ—ˆ์šฉ๋œ HTTP ๋ฉ”์„œ๋“œ๋งŒ ์‚ฌ์šฉ

  • GET
  • HEAD
  • POST

์กฐ๊ฑด 2: ํ—ˆ์šฉ๋œ ํ—ค๋”๋งŒ ์‚ฌ์šฉ (CORS-safelisted headers)

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (ํŠน์ • ๊ฐ’๋งŒ ํ—ˆ์šฉ)
  • Range (๋‹จ์ผ ๋ฒ”์œ„๋งŒ)

์กฐ๊ฑด 3: Content-Type์ด ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Simple Request ์˜ˆ์‹œ

// ํด๋ผ์ด์–ธํŠธ
fetch("https://api.example.com/public-data")
  .then(response => response.json())
  .then(data => console.log(data));
// ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ „์†ก
GET /public-data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Accept: application/json
 
// ์„œ๋ฒ„ ์‘๋‹ต
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
 
{"message": "Hello"}

Preflighted Request (์‚ฌ์ „ ์š”์ฒญ)

์‹ค์ œ ์š”์ฒญ ์ „์— OPTIONS ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์ „ ์š”์ฒญ์„ ๋ณด๋‚ด ์„œ๋ฒ„๊ฐ€ ํ—ˆ์šฉํ•˜๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

Preflight๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ

  • GET, HEAD, POST ์™ธ์˜ HTTP ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ (PUT, DELETE, PATCH ๋“ฑ)
  • ์ปค์Šคํ…€ ํ—ค๋” ํฌํ•จ (์˜ˆ: X-Custom-Header, Authorization)
  • Content-Type์ด ํ—ˆ์šฉ๋œ 3๊ฐ€์ง€ ์™ธ์˜ ๊ฐ’ (์˜ˆ: application/json)
  • XMLHttpRequest.upload์— ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
  • ReadableStream ์‚ฌ์šฉ

Preflighted Request ์˜ˆ์‹œ

// ํด๋ผ์ด์–ธํŠธ
fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",  // preflight ํŠธ๋ฆฌ๊ฑฐ
    "X-Custom-Header": "value"           // preflight ํŠธ๋ฆฌ๊ฑฐ
  },
  body: JSON.stringify({ name: "Alice" })
});
// Step 1: Preflight ์š”์ฒญ (OPTIONS)
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-custom-header
 
// Step 2: Preflight ์‘๋‹ต
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400  // 24์‹œ๊ฐ„ ๋™์•ˆ preflight ๊ฒฐ๊ณผ ์บ์‹ฑ
 
// Step 3: ์‹ค์ œ ์š”์ฒญ (preflight ์„ฑ๊ณต ์‹œ์—๋งŒ)
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
X-Custom-Header: value
 
{"name": "Alice"}
 
// Step 4: ์‹ค์ œ ์‘๋‹ต
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
 
{"id": 123, "name": "Alice"}
sequenceDiagram
    autonumber
    participant Browser
    participant Server

    Note over Browser,Server: Preflight ๋‹จ๊ณ„
    Browser->>Server: OPTIONS /users<br/>Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: content-type
    Server->>Browser: 204 No Content<br/>Access-Control-Allow-Methods: POST<br/>Access-Control-Allow-Headers: content-type

    Note over Browser,Server: ์‹ค์ œ ์š”์ฒญ ๋‹จ๊ณ„
    Browser->>Server: POST /users<br/>Content-Type: application/json
    Server->>Browser: 200 OK<br/>Access-Control-Allow-Origin: https://app.example.com

CORS ์ฃผ์š” ํ—ค๋”

์„œ๋ฒ„ โ†’ ๋ธŒ๋ผ์šฐ์ € ์‘๋‹ต ํ—ค๋”

ํ—ค๋”์„ค๋ช…์˜ˆ์‹œ
Access-Control-Allow-Originํ—ˆ์šฉํ•  ์ถœ์ฒ˜ ์ง€์ •https://app.example.com ๋˜๋Š” *
Access-Control-Allow-Methodsํ—ˆ์šฉํ•  HTTP ๋ฉ”์„œ๋“œPOST, GET, OPTIONS
Access-Control-Allow-Headersํ—ˆ์šฉํ•  ์š”์ฒญ ํ—ค๋”Content-Type, Authorization
Access-Control-Allow-Credentials์ธ์ฆ ์ •๋ณด(์ฟ ํ‚ค) ํ—ˆ์šฉ ์—ฌ๋ถ€true
Access-Control-Max-AgePreflight ๊ฒฐ๊ณผ ์บ์‹ฑ ์‹œ๊ฐ„ (์ดˆ)86400 (24์‹œ๊ฐ„)
Access-Control-Expose-HeadersJS์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ํ—ค๋”X-Custom-Header

Access-Control-Allow-Origin

๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ—ค๋”๋กœ, ์–ด๋–ค ์ถœ์ฒ˜์˜ ์š”์ฒญ์„ ํ—ˆ์šฉํ• ์ง€ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.

# ํŠน์ • ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉ
Access-Control-Allow-Origin: https://app.example.com
 
# ๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ (์ธ์ฆ ์ •๋ณด ์‚ฌ์šฉ ๋ถˆ๊ฐ€)
Access-Control-Allow-Origin: *

์ค‘์š”ํ•œ ์ œ์•ฝ:

  • ์ธ์ฆ ์ •๋ณด(์ฟ ํ‚ค, Authorization ํ—ค๋”)๋ฅผ ํฌํ•จํ•˜๋Š” ์š”์ฒญ์˜ ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ ์ •ํ™•ํ•œ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•˜๋ฉฐ, *๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
# โŒ ์ธ์ฆ ์ •๋ณด ํฌํ•จ ์š”์ฒญ์—์„œ ์™€์ผ๋“œ์นด๋“œ ์‚ฌ์šฉ - ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฐจ๋‹จ
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
 
# โœ… ์ •ํ™•ํ•œ ์ถœ์ฒ˜ ๋ช…์‹œ ํ•„์š”
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Access-Control-Allow-Credentials

์ธ์ฆ ์ •๋ณด(์ฟ ํ‚ค, HTTP ์ธ์ฆ, TLS ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ์„œ)๋ฅผ ์š”์ฒญ์— ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

Access-Control-Allow-Credentials: true

์ด ํ—ค๋”๊ฐ€ true๋กœ ์„ค์ •๋˜๋ฉด:

  • ํด๋ผ์ด์–ธํŠธ๋Š” credentials: 'include' ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ
  • Access-Control-Allow-Origin์€ ๋ฐ˜๋“œ์‹œ ์ •ํ™•ํ•œ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•จ (* ๋ถˆ๊ฐ€)

Access-Control-Max-Age

Preflight ์š”์ฒญ์˜ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•  ์‹œ๊ฐ„(์ดˆ)์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์บ์‹ฑ ๊ธฐ๊ฐ„ ๋™์•ˆ์€ ๋™์ผํ•œ ์š”์ฒญ์— ๋Œ€ํ•ด preflight๋ฅผ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค.

Access-Control-Max-Age: 86400  # 24์‹œ๊ฐ„ (86400์ดˆ)

๋ธŒ๋ผ์šฐ์ € โ†’ ์„œ๋ฒ„ ์š”์ฒญ ํ—ค๋”

ํ—ค๋”์„ค๋ช…์˜ˆ์‹œ
Origin์š”์ฒญ์˜ ์ถœ์ฒ˜https://app.example.com
Access-Control-Request-Method์‹ค์ œ ์š”์ฒญ์—์„œ ์‚ฌ์šฉํ•  HTTP ๋ฉ”์„œ๋“œ (Preflight์—์„œ๋งŒ)POST
Access-Control-Request-Headers์‹ค์ œ ์š”์ฒญ์—์„œ ํฌํ•จํ•  ํ—ค๋” ๋ชฉ๋ก (Preflight์—์„œ๋งŒ)Content-Type, X-Custom-Header

์ธ์ฆ ์ •๋ณด(Credentials) ํฌํ•จ ์š”์ฒญ

๊ธฐ๋ณธ์ ์œผ๋กœ cross-origin ์š”์ฒญ์€ ์ฟ ํ‚ค๋‚˜ Authorization ํ—ค๋”๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ ค๋ฉด ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๋ชจ๋‘ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์„ค์ •

Fetch API

fetch("https://api.example.com/user-data", {
  credentials: "include"  // ์ฟ ํ‚ค ๋ฐ ์ธ์ฆ ์ •๋ณด ํฌํ•จ
})
  .then(response => response.json())
  .then(data => console.log(data));

credentials ์˜ต์…˜ ๊ฐ’:

  • "omit": ์ธ์ฆ ์ •๋ณด ํฌํ•จ ์•ˆ ํ•จ (๊ธฐ๋ณธ๊ฐ’)
  • "same-origin": ๋™์ผ ์ถœ์ฒ˜ ์š”์ฒญ์—๋งŒ ์ธ์ฆ ์ •๋ณด ํฌํ•จ
  • "include": ํ•ญ์ƒ ์ธ์ฆ ์ •๋ณด ํฌํ•จ

XMLHttpRequest

const xhr = new XMLHttpRequest();
xhr.withCredentials = true;  // ์ธ์ฆ ์ •๋ณด ํฌํ•จ
xhr.open("GET", "https://api.example.com/user-data");
xhr.send();

์„œ๋ฒ„ ์„ค์ •

์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์— ๋Œ€ํ•ด ์„œ๋ฒ„๋Š” ๋‹ค์Œ์„ ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

Access-Control-Allow-Origin: https://app.example.com  # ์ •ํ™•ํ•œ ์ถœ์ฒ˜ ๋ช…์‹œ (* ๋ถˆ๊ฐ€)
Access-Control-Allow-Credentials: true

์ธ์ฆ ์ •๋ณด ํฌํ•จ ์š”์ฒญ ์ „์ฒด ํ๋ฆ„

// ํด๋ผ์ด์–ธํŠธ
fetch("https://api.example.com/protected-data", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ id: 123 })
});
// Preflight ์š”์ฒญ
OPTIONS /protected-data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
 
// Preflight ์‘๋‹ต
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com  # * ๋ถˆ๊ฐ€
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true  # ํ•„์ˆ˜
 
// ์‹ค์ œ ์š”์ฒญ
POST /protected-data HTTP/1.1
Origin: https://app.example.com
Cookie: sessionId=abc123  # ์ฟ ํ‚ค ํฌํ•จ
Content-Type: application/json
 
{"id": 123}
 
// ์‹ค์ œ ์‘๋‹ต
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com  # * ๋ถˆ๊ฐ€
Access-Control-Allow-Credentials: true  # ํ•„์ˆ˜
Set-Cookie: sessionId=xyz789
 
{"status": "success"}

์„œ๋ฒ„ ์ธก CORS ๊ตฌํ˜„ ์˜ˆ์‹œ

Node.js/Express

const express = require('express');
const app = express();
 
// ๋ชจ๋“  ์š”์ฒญ์— CORS ํ—ˆ์šฉ (๊ณต๊ฐœ API)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});
 
// ํŠน์ • ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์ธ์ฆ ์ •๋ณด ํฌํ•จ
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
 
  // Preflight ์š”์ฒญ ์ฒ˜๋ฆฌ
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});
 
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello CORS' });
});
 
app.listen(3000);

Spring Boot (Java)

@Configuration
public class CorsConfig {
 
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new 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)
                    .maxAge(3600);
            }
        };
    }
}

CORS ์—๋Ÿฌ ํ•ด๊ฒฐ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€์›์ธํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
Access-Control-Allow-Origin header missing์„œ๋ฒ„๊ฐ€ CORS ํ—ค๋”๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์Œ์„œ๋ฒ„์—์„œ Access-Control-Allow-Origin ํ—ค๋” ์ถ”๊ฐ€
Access-Control-Allow-Origin does not match์ถœ์ฒ˜๊ฐ€ ํ—ˆ์šฉ ๋ชฉ๋ก์— ์—†์Œ์„œ๋ฒ„์—์„œ ํ•ด๋‹น ์ถœ์ฒ˜๋ฅผ ํ—ˆ์šฉ ๋ชฉ๋ก์— ์ถ”๊ฐ€
Credentials mode is โ€˜includeโ€™ but Access-Control-Allow-Credentials is missing์ธ์ฆ ์ •๋ณด ํฌํ•จ ์š”์ฒญ์— credentials ํ—ค๋” ์—†์Œ์„œ๋ฒ„์—์„œ Access-Control-Allow-Credentials: true ์ถ”๊ฐ€
Method not found in Access-Control-Allow-MethodsHTTP ๋ฉ”์„œ๋“œ๊ฐ€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ์„œ๋ฒ„์—์„œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ Access-Control-Allow-Methods์— ์ถ”๊ฐ€
Header not found in Access-Control-Allow-Headers์ปค์Šคํ…€ ํ—ค๋”๊ฐ€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ์„œ๋ฒ„์—์„œ ํ•ด๋‹น ํ—ค๋”๋ฅผ Access-Control-Allow-Headers์— ์ถ”๊ฐ€

CORS๊ฐ€ ๋ณดํ˜ธํ•˜๋Š” ๊ฒƒ vs ๋ณดํ˜ธํ•˜์ง€ ์•Š๋Š” ๊ฒƒ

โœ… CORS๊ฐ€ ๋ณดํ˜ธํ•˜๋Š” ๊ฒƒ

  • ๋ฌด๋‹จ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋ฐฉ์ง€: ์•…์˜์ ์ธ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ์ฝ๋Š” ๊ฒƒ์„ ์ฐจ๋‹จ
  • ๋ช…์‹œ์  ๊ถŒํ•œ ๋ถ€์—ฌ: ์„œ๋ฒ„๊ฐ€ ํ—ˆ์šฉํ•œ ์ถœ์ฒ˜๋งŒ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ๋ฏผ๊ฐํ•œ ์ž‘์—… ๋ณดํ˜ธ: GET ์™ธ์˜ ์š”์ฒญ์€ preflight๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•ด์•ผ ํ•จ

โŒ CORS๊ฐ€ ๋ณดํ˜ธํ•˜์ง€ ์•Š๋Š” ๊ฒƒ

  • CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ: CORS๋Š” CSRF๋ฅผ ๋ฐฉ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ณ„๋„์˜ CSRF ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
  • ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(Man-in-the-Middle): CORS๋Š” ์ „์†ก ๊ณ„์ธต ๋ณด์•ˆ๊ณผ ๋ฌด๊ด€ํ•ฉ๋‹ˆ๋‹ค. HTTPS/TLS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
  • ์„œ๋ฒ„ ์ธก ์ทจ์•ฝ์ : CORS๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ๋งŒ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์ธก ๋ณด์•ˆ์€ ๋ณ„๋„๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

๋ณด์•ˆ Best Practices

# โŒ ๋‚˜์œ ์˜ˆ: ์ธ์ฆ ์ •๋ณด์™€ ์™€์ผ๋“œ์นด๋“œ ํ•จ๊ป˜ ์‚ฌ์šฉ ๋ถˆ๊ฐ€
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
 
# โœ… ์ข‹์€ ์˜ˆ: ํŠน์ • ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉ
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
 
# โœ… ์ข‹์€ ์˜ˆ: Preflight ์บ์‹ฑ ์ ์ ˆํžˆ ์„ค์ •
Access-Control-Max-Age: 3600  # 1์‹œ๊ฐ„
 
# โœ… ์ข‹์€ ์˜ˆ: ํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ๋งŒ ํ—ˆ์šฉ
Access-Control-Allow-Methods: GET, POST  # PUT, DELETE๋Š” ํ•„์š”ํ•  ๋•Œ๋งŒ

์ฐธ๊ณ  ๋ฌธ์„œ