크로스 오리진 리소스 공유(CORS) 이해와 구현 🌐🔒

콘텐츠 대표 이미지 - 크로스 오리진 리소스 공유(CORS) 이해와 구현 🌐🔒

 

 

안녕, 친구들! 오늘은 웹 개발자들 사이에서 자주 언급되는 크로스 오리진 리소스 공유(CORS)에 대해 재미있게 알아볼 거야. 😎 CORS가 뭔지, 왜 필요한지, 그리고 어떻게 구현하는지 쉽고 상세하게 설명해줄게. 우리 재능넷 사이트처럼 다양한 리소스를 공유하는 플랫폼을 만들 때 특히 중요한 개념이니까 잘 따라와봐!

🚀 CORS의 세계로 떠나기 전에: 이 글을 읽고 나면, 너도 CORS 전문가가 될 거야! 웹 보안의 비밀을 함께 파헤쳐보자고!

1. CORS란 뭘까? 🤔

CORS는 Cross-Origin Resource Sharing의 약자야. 한국어로 하면 "교차 출처 리소스 공유"라고 할 수 있지. 뭔가 복잡해 보이지? 걱정 마, 쉽게 설명해줄게!

imagine 네가 엄청 맛있는 쿠키를 만들었다고 생각해봐. 🍪 그런데 이 쿠키는 특별해서 너의 집(도메인)에서만 먹을 수 있어. 어느 날 친구가 와서 "나도 그 쿠키 먹고 싶어!"라고 하는 거야. 하지만 규칙상 네 집 밖으로 쿠키를 가져갈 수 없어. 이게 바로 웹에서의 '동일 출처 정책(Same-Origin Policy)'이야.

CORS는 이런 상황에서 등장한 영웅이야! CORS를 사용하면, 너의 특별한 쿠키를 다른 친구들의 집(다른 도메인)에서도 먹을 수 있게 해주는 거지. 물론 너가 허락한 친구들에게만!

🍪 쿠키 비유 정리:

  • 너의 집 = 너의 웹사이트 도메인
  • 특별한 쿠키 = 웹 리소스 (데이터, 이미지 등)
  • 친구들의 집 = 다른 도메인
  • 쿠키를 나눠 먹는 것 = 리소스 공유

자, 이제 CORS가 뭔지 대충 감이 왔지? 그럼 이제 좀 더 자세히 들어가볼까?

1.1 CORS의 탄생 배경 📚

옛날옛날 웹이 처음 만들어졌을 때는, 모든 웹사이트가 자기 집에서만 놀았어. 다른 집에 있는 리소스를 가져다 쓰는 일이 거의 없었지. 하지만 웹이 점점 발전하면서, 여러 사이트의 정보를 한 곳에서 보여주는 게 필요해졌어.

예를 들어, 우리 재능넷 같은 사이트에서 다른 사이트의 날씨 정보나 주식 정보를 가져와서 보여주고 싶다고 생각해봐. 이런 걸 가능하게 하려면 다른 도메인의 리소스를 안전하게 가져올 수 있는 방법이 필요했던 거야.

그래서 등장한 게 바로 CORS! 웹 브라우저와 서버가 서로 대화를 나누면서 "이 리소스를 공유해도 될까요?"라고 물어보고, "네, 괜찮아요!"라고 대답하는 과정을 만든 거지.

CORS 작동 원리 브라우저 서버 1. 리소스 요청 2. CORS 헤더와 함께 응답

1.2 CORS가 없다면? 😱

CORS가 없는 세상을 상상해봐. 그건 마치 모든 집의 문이 항상 열려있고, 누구나 들어와서 물건을 가져갈 수 있는 것과 같아. 무서운 일이지, 그렇지? 웹에서도 마찬가지야.

CORS 없이 모든 도메인 간 요청이 허용된다면:

  • 악의적인 웹사이트가 너의 개인 정보를 쉽게 훔쳐갈 수 있어 😈
  • 해커들이 너도 모르는 사이에 다른 사이트에 요청을 보낼 수 있지 🕵️‍♂️
  • 중요한 데이터가 엉뚱한 곳으로 새어나갈 수 있어 💧

그래서 CORS는 웹의 안전을 지키는 경비원 같은 역할을 하는 거야. "잠깐만요! 당신, 이 리소스를 가져갈 자격이 있나요?"라고 물어보는 거지.

2. CORS는 어떻게 작동할까? 🔍

자, 이제 CORS가 어떻게 작동하는지 자세히 알아볼 차례야. 마치 비밀 요원들이 암호를 주고받는 것처럼 복잡해 보이지만, 걱정 마! 차근차근 설명해줄게.

2.1 CORS의 기본 흐름 🌊

CORS의 작동 과정은 크게 세 단계로 나눌 수 있어:

  1. 프리플라이트 요청 (Preflight Request): 본격적인 요청 전에 "미리 확인"하는 단계
  2. 실제 요청 (Actual Request): 진짜로 원하는 데이터를 요청하는 단계
  3. 서버 응답 (Server Response): 서버가 데이터와 함께 CORS 관련 헤더를 보내는 단계

이게 뭔 소리냐고? 걱정 마, 하나씩 자세히 설명해줄게! 😉

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

프리플라이트 요청은 마치 파티에 가기 전에 전화해서 "제가 이 파티에 참석해도 될까요?"라고 물어보는 것과 비슷해. 브라우저가 서버에게 "이봐요, 제가 이런 요청을 보내도 괜찮을까요?"라고 미리 물어보는 거지.

🎭 프리플라이트 요청의 특징:

  • HTTP 메서드로 OPTIONS를 사용해
  • 실제 요청에 대한 정보를 담은 특별한 헤더들이 포함돼
  • 브라우저가 자동으로 보내는 거라 개발자가 직접 코드를 작성할 필요는 없어

프리플라이트 요청에 포함되는 주요 헤더들을 살펴볼까?

  • Origin: 요청을 보내는 출처(도메인)를 나타내
  • Access-Control-Request-Method: 실제로 보낼 요청의 HTTP 메서드를 알려줘
  • Access-Control-Request-Headers: 실제 요청에 포함될 특별한 헤더들을 나열해

예를 들어, 재능넷(https://www.jaenung.net)에서 다른 사이트의 API를 호출하려고 할 때, 프리플라이트 요청은 이렇게 생겼을 거야:

OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://www.jaenung.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

이 요청은 "안녕하세요, api.example.com! 저는 재능넷에서 왔어요. POST 요청을 보내고 싶은데, Content-Type과 Authorization 헤더도 함께 보낼 거예요. 괜찮을까요?" 라고 물어보는 거야.

2.1.2 서버의 프리플라이트 응답 🏰

서버는 이 프리플라이트 요청을 받고 "음, 이 요청을 받아들일 수 있을지" 판단해. 그리고 그 결과를 담아 응답을 보내지.

긍정적인 응답은 이런 식이야:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.jaenung.net
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

이 응답은 이렇게 해석할 수 있어:

  • "재능넷(https://www.jaenung.net)에서 오는 요청은 환영이야!"
  • "POST, GET, OPTIONS 메서드는 사용해도 돼."
  • "Content-Type과 Authorization 헤더도 문제없어."
  • "이 허가는 24시간(86400초) 동안 유효해. 그동안은 다시 물어보지 않아도 돼."

만약 서버가 요청을 허용하지 않는다면, Access-Control-Allow-Origin 헤더를 보내지 않거나 다른 출처를 지정할 거야. 그러면 브라우저는 "앗, 안 되는구나!" 하고 실제 요청을 보내지 않아.

2.1.3 실제 요청 (Actual Request) 📨

프리플라이트 요청이 성공적으로 끝나면, 이제 진짜 원하는 요청을 보낼 차례야! 이 단계는 우리가 원래 하고 싶었던 그 요청을 보내는 거야.

예를 들어, 재능넷에서 사용자 프로필 정보를 가져오는 요청은 이렇게 생겼을 거야:

POST /api/user-profile HTTP/1.1
Host: api.example.com
Origin: https://www.jaenung.net
Content-Type: application/json
Authorization: Bearer token123456

{
  "userId": "12345"
}

이 요청에는 프리플라이트 때 확인받은 대로 Content-Type과 Authorization 헤더가 포함되어 있어. 그리고 요청 본문에는 실제로 필요한 데이터(여기서는 userId)가 들어있지.

2.1.4 서버 응답 (Server Response) 📬

서버는 이 요청을 처리하고, 응답과 함께 필요한 CORS 헤더를 다시 보내줘. 성공적인 응답은 이런 모습이야:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.jaenung.net
Content-Type: application/json

{
  "name": "김재능",
  "email": "jaenung@example.com",
  "skills": ["웹개발", "디자인", "마케팅"]
}

여기서 중요한 건 Access-Control-Allow-Origin 헤더야. 이 헤더가 있어야 브라우저가 "오케이, 이 응답은 우리가 사용해도 돼!"라고 판단하지.

⚠️ 주의할 점: Access-Control-Allow-Origin: *처럼 와일드카드(*)를 사용하면 모든 도메인에서의 접근을 허용하는 거야. 이건 보안상 위험할 수 있으니 주의해서 사용해야 해!

2.2 CORS 시나리오 예시 🎭

CORS가 어떻게 작동하는지 더 쉽게 이해하기 위해, 재능넷을 예로 들어 몇 가지 시나리오를 살펴볼까?

2.2.1 간단한 GET 요청 📗

재능넷 사용자가 다른 사이트의 공개 API에서 정보를 가져오려고 해. 이건 "단순 요청"이라고 불리는 경우야.

시나리오: 재능넷(https://www.jaenung.net)에서 공개 날씨 API(https://api.weatherapp.com)의 정보를 가져오려고 해.

// 프론트엔드 JavaScript 코드
fetch('https://api.weatherapp.com/current?city=seoul')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

이 경우, 프리플라이트 요청 없이 바로 GET 요청이 날아가.

서버 응답:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.jaenung.net
Content-Type: application/json

{
  "city": "Seoul",
  "temperature": 25,
  "condition": "Sunny"
}

여기서 Access-Control-Allow-Origin 헤더가 재능넷의 도메인을 허용하고 있어서, 브라우저는 이 응답을 안전하게 사용할 수 있어.

2.2.2 복잡한 POST 요청 📘

이번엔 좀 더 복잡한 상황을 볼게. 재능넷 사용자가 자신의 프로필을 외부 서비스에 업데이트하려고 해.

시나리오: 재능넷 사용자가 연동된 포트폴리오 서비스(https://api.portfolio.com)에 자신의 새 기술을 추가하려고 해.

먼저, 브라우저는 프리플라이트 요청을 보내:

OPTIONS /api/update-skills HTTP/1.1
Host: api.portfolio.com
Origin: https://www.jaenung.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

서버의 프리플라이트 응답:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.jaenung.net
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

이제 실제 POST 요청을 보내:

POST /api/update-skills HTTP/1.1
Host: api.portfolio.com
Origin: https://www.jaenung.net
Content-Type: application/json
Authorization: Bearer user_token_123

{
  "userId": "jaenung_kim",
  "newSkill": "CORS 마스터"
}

서버의 최종 응답:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.jaenung.net
Content-Type: application/json

{
  "status": "success",
  "message": "Skill updated successfully",
  "updatedSkills": ["웹개발", "디자인", "마케팅", "CORS 마스터"]
}

이런 복잡한 요청에서도 CORS가 잘 작동해서, 재능넷 사용자의 정보를 안전하게 다른 서비스와 주고받을 수 있어!

2.3 CORS 에러와 해결 방법 🚑

CORS를 처음 접하면 많은 개발자들이 에러 때문에 머리를 쥐어뜯곤 해. 하지만 걱정 마! 흔한 CORS 에러들과 그 해결 방법을 알아보자.

2.3.1 "Access to fetch at 'URL' from origin 'Origin' has been blocked by CORS policy" 에러

이 에러는 CORS 정책 위반으로 요청이 차단되었다는 뜻이야. 주로 다음과 같은 이유로 발생해:

  • 서버가 Access-Control-Allow-Origin 헤더를 보내지 않았거나
  • 허용된 오리진 목록에 요청을 보낸 도메인이 포함되어 있지 않을 때

해결 방법:

  1. 서버 측에서 적절한 CORS 헤더를 설정해야 해. 예를 들어, Express.js를 사용하는 Node.js 서버라면:
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
  origin: 'https://www.jaenung.net'
}));

// 라우트 및 기타 서버 로직...

app.listen(3000, () => console.log('Server running on port 3000'));
  1. 프록시 서버를 사용하는 방법도 있어. 개발 환경에서는 Create React App이나 Vue CLI 같은 도구들이 제공하는 프록시 기능을 활용할 수 있지.

2.3.2 "Method not allowed by CORS" 에러

이 에러는 서버가 특정 HTTP 메서드를 허용하지 않을 때 발생해.

해결 방법:

서버의 CORS 설정에서 허용할 메서드를 명시적으로 추가해줘야 해:

app.use(cors({
  origin: 'https://www.jaenung.net',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
}));

2.3.3 "Request header field [header-name] is not allowed by Access-Control-Allow-Headers in preflight response" 에러

이 에러는 요청에 포함된 특정 헤더가 서버에서 허용되지 않았을 때 발생해.

해결 방법:

서버 측에서 Access-Control-Allow-Headers에 필요한 헤더를 추가해줘야 해:

app.use(cors({
  origin: 'https://www.jaenung.net',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

2.3.4 "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'" 에러

이 에러는 credentials를 포함한 요청을 보냈는데, 서버가 Access-Control-Allow-Origin: *로 응답했을 때 발생해.

해결 방법:

  1. 클라이언트 측에서 credentials 옵션을 설정할 때:
fetch('https://api.example.com/data', {
  credentials: 'include'
})
  1. 서버 측에서는 와일드카드(*) 대신 구체적인 오리진을 지정해야 해:
app.use(cors({
  origin: 'https://www.jaenung.net',
  credentials: true
}));

이렇게 CORS 에러들을 해결하면서, 너도 어느새 CORS 전문가가 되어 있을 거야! 🎓 재능넷 같은 플랫폼을 개발할 때 이런 지식들이 정말 유용하게 쓰일 거야.

3. CORS 구현하기 🛠️

자, 이제 CORS에 대해 꽤 많이 알게 됐지? 그럼 이제 실제로 CORS를 구현하는 방법을 알아볼 차례야. 여러 가지 환경에서 CORS를 어떻게 설정하는지 자세히 살펴보자!

3.1 서버 측 CORS 구현 🖥️

서버 측에서 CORS를 구현하는 방법은 사용하는 기술 스택에 따라 조금씩 다를 수 있어. 주요한 몇 가지 환경에서의 구현 방법을 알아볼게.

3.1.1 Node.js + Express

Node.js와 Express를 사용하는 경우, cors 미들웨어를 사용하면 아주 쉽게 CORS를 구현할 수 있어.

  1. 먼저, cors 패키지를 설치해:
npm install cors
  1. 그리고 서버 코드에서 다음과 같이 사용할 수 있어:
const express = require('express');
const cors = require('cors');
const app = express();

// 모든 라우트에 CORS 적용
app.use(cors());

// 또는 특정 옵션으로 CORS 설정
app.use(cors({
  origin: 'https://www.jaenung.net',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.get('/api/data', (req, res) => {
  res.json({ message: "This is CORS-enabled data" });
});

app.listen(8080, () => {
  console.log('CORS-enabled server running on port 8080');
});

이렇게 하면 모든 라우트에 CORS가 적용돼. 특정 라우트에만 CORS를 적용하고 싶다면 이렇게 할 수 있어:

app.get('/api/special-data', cors(), (req, res) => {
  res.json({ message: "This route has its own CORS policy" });
});

3.1.2 Python + Flask

Python과 Flask를 사용한다면, Flask-CORS 확장을 사용하면 돼.

  1. 먼저 Flask-CORS를 설치해:
pip install flask-cors
  1. 그리고 다음과 같이 사용할 수 있어:
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # 모든 라우트에 CORS 적용

@app.route("/api/data")
def get_data():
    return {"message": "This is CORS-enabled data"}

if __name__ == "__main__":
    app.run(port=8080)

특정 라우트에만 CORS를 적용하고 싶다면:

from flask import Flask
from flask_cors import cross_origin

app = Flask(__name__)

@app.route("/api/special-data")
@cross_origin()
def get_special_data():
    return {"message": "This route has its own CORS policy"}

3.1.3 Java + Spring Boot

Java와 Spring Boot를 사용하는 경우, @CrossOrigin 어노테이션이나 WebMvcConfigurer를 사용해 CORS를 구현할 수 있어.

@CrossOrigin 어노테이션 사용:

import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin(origins = "https://www.jaenung.net")
public class DataController {

    @GetMapping("/api/data")
    public String getData() {
        return "This is CORS-enabled data";
    }
}

전역 CORS 설정을 위해 WebMvcConfigurer 사용:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.jaenung.net")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

3.2 클라이언트 측 CORS 처리 💻

클라이언트 측에서는 CORS를 직접 "구현"하지는 않아. 대신, CORS를 지원하는 방식으로 요청을 보내야 해. 몇 가지 주요 방법을 살펴볼게.

3.2.1 Fetch API 사용

Fetch API를 사용할 때는 다음과 같이 CORS를 고려할 수 있어:

fetch('https://api.example.com/data', {
  method: 'GET',
  mode: 'cors', // CORS 모드 명시
  credentials: 'include', // 필요한 경우 credentials 포함
  headers: {
    'Content-Type': 'application/json'
    // 필요한 다른 헤더들...
  }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

3.2.2 Axios 라이브러리 사용

Axios를 사용하면 CORS 관련 설정을 좀 더 쉽게 할 수 있어:

import axios from 'axios';

axios.get('https://api.example.com/data', {
  withCredentials: true, // 필요한 경우 credentials 포함
  headers: {
    'Content-Type': 'application/json'
    // 필요한 다른 헤더들...
  }
})
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));

3.2.3 프록시 서버 사용

개발 환경에서 CORS 문제를 우회하기 위해 프록시 서버를 사용할 수 있어. Create React App을 사용하는 경우, package.json에 다음과 같이 설정할 수 있지:

{
  "name": "my-app",
  "version": "0.1.0",
  "proxy": "https://api.example.com"
}

이렇게 하면 개발 서버가 API 요청을 프록시해주어 CORS 문제를 피할 수 있어.

3.3 CORS 보안 고려사항 🔒

CORS를 구현할 때는 보안도 꼭 고려해야 해. 몇 가지 중요한 포인트를 살펴볼게:

  • 와일드카드 사용 자제: Access-Control-Allow-Origin: *의 사용은 가능한 피하고, 구체적인 오리진을 지정해.
  • 필요한 메서드만 허용: 모든 HTTP 메서드를 열어두지 말고, 필요한 메서드만 허용해.
  • 중요한 작업은 서버에서: 클라이언트에서의 CORS 설정은 우회될 수 있으므로, 중요한 보안 검사는 항상 서버에서 수행해.
  • Credentials 주의: Access-Control-Allow-Credentials: true를 설정할 때는 특히 주의해. 이 설정과 함께 와일드카드 오리진을 사용하면 안 돼.
  • 헤더 제한: Access-Control-Allow-Headers를 사용해 필요한 헤더만 허용해.

3.4 CORS 디버깅 팁 🐛

CORS 관련 문제를 디버깅할 때 도움이 될 만한 팁들이야:

  • 브라우저 개발자 도구 활용: Network 탭에서 요청과 응답 헤더를 자세히 살펴봐.
  • 서버 로그 확인: 서버 측 로그를 통해 CORS 관련 설정이 제대로 적용되고 있는지 확인해.
  • 테스트 도구 사용: Postman 같은 도구로 API를 직접 테스트해보면 CORS 문제인지 다른 문제인지 구분하는 데 도움이 돼.
  • 단계적 접근: 가장 기본적인 CORS 설정부터 시작해서 점진적으로 복잡한 설정을 추가해나가.