크로스 오리진 리소스 공유(CORS) 이해와 구현 🌐🔒
안녕, 친구들! 오늘은 웹 개발자들 사이에서 자주 언급되는 크로스 오리진 리소스 공유(CORS)에 대해 재미있게 알아볼 거야. 😎 CORS가 뭔지, 왜 필요한지, 그리고 어떻게 구현하는지 쉽고 상세하게 설명해줄게. 우리 재능넷 사이트처럼 다양한 리소스를 공유하는 플랫폼을 만들 때 특히 중요한 개념이니까 잘 따라와봐!
🚀 CORS의 세계로 떠나기 전에: 이 글을 읽고 나면, 너도 CORS 전문가가 될 거야! 웹 보안의 비밀을 함께 파헤쳐보자고!
1. CORS란 뭘까? 🤔
CORS는 Cross-Origin Resource Sharing의 약자야. 한국어로 하면 "교차 출처 리소스 공유"라고 할 수 있지. 뭔가 복잡해 보이지? 걱정 마, 쉽게 설명해줄게!
imagine 네가 엄청 맛있는 쿠키를 만들었다고 생각해봐. 🍪 그런데 이 쿠키는 특별해서 너의 집(도메인)에서만 먹을 수 있어. 어느 날 친구가 와서 "나도 그 쿠키 먹고 싶어!"라고 하는 거야. 하지만 규칙상 네 집 밖으로 쿠키를 가져갈 수 없어. 이게 바로 웹에서의 '동일 출처 정책(Same-Origin Policy)'이야.
CORS는 이런 상황에서 등장한 영웅이야! CORS를 사용하면, 너의 특별한 쿠키를 다른 친구들의 집(다른 도메인)에서도 먹을 수 있게 해주는 거지. 물론 너가 허락한 친구들에게만!
🍪 쿠키 비유 정리:
- 너의 집 = 너의 웹사이트 도메인
- 특별한 쿠키 = 웹 리소스 (데이터, 이미지 등)
- 친구들의 집 = 다른 도메인
- 쿠키를 나눠 먹는 것 = 리소스 공유
자, 이제 CORS가 뭔지 대충 감이 왔지? 그럼 이제 좀 더 자세히 들어가볼까?
1.1 CORS의 탄생 배경 📚
옛날옛날 웹이 처음 만들어졌을 때는, 모든 웹사이트가 자기 집에서만 놀았어. 다른 집에 있는 리소스를 가져다 쓰는 일이 거의 없었지. 하지만 웹이 점점 발전하면서, 여러 사이트의 정보를 한 곳에서 보여주는 게 필요해졌어.
예를 들어, 우리 재능넷 같은 사이트에서 다른 사이트의 날씨 정보나 주식 정보를 가져와서 보여주고 싶다고 생각해봐. 이런 걸 가능하게 하려면 다른 도메인의 리소스를 안전하게 가져올 수 있는 방법이 필요했던 거야.
그래서 등장한 게 바로 CORS! 웹 브라우저와 서버가 서로 대화를 나누면서 "이 리소스를 공유해도 될까요?"라고 물어보고, "네, 괜찮아요!"라고 대답하는 과정을 만든 거지.
1.2 CORS가 없다면? 😱
CORS가 없는 세상을 상상해봐. 그건 마치 모든 집의 문이 항상 열려있고, 누구나 들어와서 물건을 가져갈 수 있는 것과 같아. 무서운 일이지, 그렇지? 웹에서도 마찬가지야.
CORS 없이 모든 도메인 간 요청이 허용된다면:
- 악의적인 웹사이트가 너의 개인 정보를 쉽게 훔쳐갈 수 있어 😈
- 해커들이 너도 모르는 사이에 다른 사이트에 요청을 보낼 수 있지 🕵️♂️
- 중요한 데이터가 엉뚱한 곳으로 새어나갈 수 있어 💧
그래서 CORS는 웹의 안전을 지키는 경비원 같은 역할을 하는 거야. "잠깐만요! 당신, 이 리소스를 가져갈 자격이 있나요?"라고 물어보는 거지.
2. CORS는 어떻게 작동할까? 🔍
자, 이제 CORS가 어떻게 작동하는지 자세히 알아볼 차례야. 마치 비밀 요원들이 암호를 주고받는 것처럼 복잡해 보이지만, 걱정 마! 차근차근 설명해줄게.
2.1 CORS의 기본 흐름 🌊
CORS의 작동 과정은 크게 세 단계로 나눌 수 있어:
- 프리플라이트 요청 (Preflight Request): 본격적인 요청 전에 "미리 확인"하는 단계
- 실제 요청 (Actual Request): 진짜로 원하는 데이터를 요청하는 단계
- 서버 응답 (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
헤더를 보내지 않았거나 - 허용된 오리진 목록에 요청을 보낸 도메인이 포함되어 있지 않을 때
해결 방법:
- 서버 측에서 적절한 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'));
- 프록시 서버를 사용하는 방법도 있어. 개발 환경에서는 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: *
로 응답했을 때 발생해.
해결 방법:
- 클라이언트 측에서 credentials 옵션을 설정할 때:
fetch('https://api.example.com/data', {
credentials: 'include'
})
- 서버 측에서는 와일드카드(*) 대신 구체적인 오리진을 지정해야 해:
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를 구현할 수 있어.
- 먼저,
cors
패키지를 설치해:
npm install cors
- 그리고 서버 코드에서 다음과 같이 사용할 수 있어:
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 확장을 사용하면 돼.
- 먼저 Flask-CORS를 설치해:
pip install flask-cors
- 그리고 다음과 같이 사용할 수 있어:
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 설정부터 시작해서 점진적으로 복잡한 설정을 추가해나가.
- 환경 차이 고려: 개발 환경과 프로덕션 환경의 CORS 설정이 다를 수 있으니 주의해.
이렇게 CORS를 구현하고 관리하면, 재능넷 같은 복잡한 웹 애플리케이션에서도 안전하고 효율적으로 리소스를 공유할 수 있어. CORS는 처음에는 복잡해 보이지만, 이해하고 나면 웹의 보안과 유연성을 동시에 높여주는 강력한 도구라는 걸 알 수 있을 거야! 🚀
4. CORS의 미래와 대안 기술 🔮
CORS는 현재 웹 개발에서 중요한 역할을 하고 있지만, 웹 기술은 계속 발전하고 있어. CORS의 미래와 함께 등장하고 있는 대안 기술들에 대해서도 알아보자!
4.1 CORS의 진화 🦋
CORS는 계속해서 발전하고 있어. 몇 가지 주목할 만한 변화와 제안들을 살펴볼게:
- Vary 헤더의 중요성 증가: 캐싱과 관련해
Vary: Origin
헤더의 사용이 더욱 강조되고 있어. - 새로운 CORS 헤더 제안: 예를 들어,
Access-Control-Allow-Origin-Regex
같은 새로운 헤더가 제안되고 있어. 이를 통해 더 유연한 오리진 설정이 가능해질 거야. - 보안 강화: CORS와 관련된 보안 이슈를 더욱 세밀하게 다루기 위한 새로운 명세들이 논의되고 있어.
4.2 대안 기술들 🔄
CORS 외에도 교차 출처 리소스 공유를 위한 다양한 기술들이 있어. 이들 중 일부는 CORS와 함께 사용되거나, 특정 상황에서 CORS를 대체할 수 있어.
4.2.1 JSONP (JSON with Padding)
JSONP는 CORS가 널리 지원되기 전에 많이 사용되던 기술이야. <script>
태그가 교차 출처 요청을 할 수 있다는 점을 이용해.
<script>
function handleResponse(data) {
console.log(data);
}
</script>
<script src="https://api.example.com/data?callback=handleResponse"></script>
장점: 오래된 브라우저에서도 작동해.
단점: GET 요청만 가능하고, 보안 이슈가 있을 수 있어.
4.2.2 WebSockets
WebSocket은 실시간, 양방향 통신을 위한 프로토콜이야. CORS의 제한을 받지 않아.
const socket = new WebSocket('wss://api.example.com');
socket.onopen = function(event) {
socket.send('Hello Server!');
};
socket.onmessage = function(event) {
console.log('Message from server:', event.data);
};
장점: 실시간 통신이 가능하고, CORS 제한이 없어.
단점: 서버 설정이 복잡할 수 있고, 모든 시나리오에 적합하지는 않아.
4.2.3 서버 간 통신
클라이언트에서 직접 다른 출처의 API를 호출하는 대신, 자체 서버를 통해 요청을 프록시하는 방법이야.