크로스 오리진 리소스 공유(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
- 그리고 다음과 같이 사용할 수 있어: