"웹 개발에서 CORS 오류는 마치 갑자기 나타난 벽과 같습니다. 그러나 이 벽은 우리를 보호하기 위한 것이며, 올바른 문을 찾아 통과하는 방법을 배우는 것이 중요합니다."
목차
- CORS란 무엇인가?
- 동일 출처 정책(Same-Origin Policy)
- CORS가 필요한 이유
- CORS 작동 방식
- CORS 구성 방법
- 일반적인 CORS 오류와 해결 방법
- 보안 고려사항
- 실제 구현 예제
- 결론
CORS란 무엇인가?
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 웹 브라우저에서 실행 중인 웹 애플리케이션이 다른 출처(도메인, 프로토콜, 포트)의 리소스에 접근할 수 있도록 허용하는 보안 메커니즘입니다.
"출처(Origin)"란 URL의 프로토콜, 도메인, 포트 번호의 조합을 의미합니다. 예를 들어, https://example.com:443
에서:
- 프로토콜:
https://
- 도메인:
example.com
- 포트:
443
(HTTPS의 기본 포트)
다음과 같은 경우 두 URL은 "다른 출처"로 간주됩니다:
- 프로토콜이 다른 경우:
http://example.com
vshttps://example.com
- 도메인이 다른 경우:
https://example.com
vshttps://api.example.com
- 포트가 다른 경우:
https://example.com
vshttps://example.com:8080
CORS는 이러한 다른 출처 간의 안전한 통신을 가능하게 합니다.
동일 출처 정책(Same-Origin Policy)
웹 브라우저는 기본적으로 동일 출처 정책(Same-Origin Policy, SOP)을 따릅니다. 이 정책은 웹 페이지가 동일한 출처의 리소스만 자유롭게 접근할 수 있도록 제한합니다.
SOP가 방어하는 주요 공격
1. CSRF(Cross-Site Request Forgery) 공격
CSRF 공격은 악성 웹사이트가 사용자의 인증 정보(쿠키 등)를 이용하여 사용자 모르게 다른 웹사이트에 요청을 보내는 공격입니다.
공격 시나리오:
- 사용자가 은행 웹사이트(
bank.com
)에 로그인하고 인증 쿠키를 받음 - 로그아웃하지 않은 상태에서 악성 웹사이트(
evil.com
)를 방문 - 악성 웹사이트는 사용자 모르게 은행 웹사이트로 송금 요청을 보냄
- 브라우저는 자동으로 은행 웹사이트에 대한 인증 쿠키를 함께 전송
- 은행 웹사이트는 요청이 인증된 사용자로부터 온 것으로 판단하고 송금을 실행
SOP는 이러한 공격을 방지합니다. evil.com
에서 bank.com
으로의 요청은 가능하지만, 응답에 접근할 수 없기 때문에 공격자는 공격의 성공 여부를 확인하기 어렵고, 추가 공격을 위한 정보를 얻을 수 없습니다.
2. XSS(Cross-Site Scripting)와의 시너지 효과
SOP는 XSS 공격이 발생해도 그 영향을 제한합니다. 악성 스크립트가 삽입되더라도 다른 출처의 응답 데이터에 접근할 수 없어 데이터 유출 범위가 제한됩니다.
SOP의 문제점
SOP는 보안을 강화하지만, 현대 웹 애플리케이션 개발에 제약을 가져옵니다. 특히:
- 프론트엔드와 백엔드가 분리된 아키텍처(예: React 프론트엔드 + REST API 백엔드)
- 마이크로서비스 아키텍처
- 제3자 API 통합
- CDN(Content Delivery Network) 사용
이러한 상황에서는 서로 다른 출처 간의 통신이 필수적이며, 이를 가능하게 하는 것이 CORS입니다.
CORS가 필요한 이유
현대 웹 개발에서 CORS가 필요한 주요 이유는 다음과 같습니다:
1. 분리된 프론트엔드와 백엔드
최신 웹 개발에서는 프론트엔드와 백엔드를 분리하는 것이 일반적입니다:
- 프론트엔드:
https://app.example.com
(React, Vue, Angular 등) - 백엔드 API:
https://api.example.com
(Node.js, Spring, Django 등)
이러한 구조에서는 프론트엔드에서 백엔드 API로의 요청이 교차 출처 요청이 되므로 CORS가 필요합니다.
2. 제3자 서비스 통합
많은 웹 애플리케이션이 제3자 서비스를 통합합니다:
- 결제 게이트웨이 (Stripe, PayPal 등)
- 소셜 미디어 API (Facebook, Twitter 등)
- 클라우드 서비스 (AWS, Google Cloud 등)
이러한 서비스들은 서로 다른 도메인에 호스팅되므로 CORS 없이는 접근할 수 없습니다.
3. 마이크로 프론트엔드 아키텍처
여러 개의 독립적인 프론트엔드 애플리케이션이 하나의 통합된 웹사이트를 구성하는 마이크로 프론트엔드 아키텍처에서도 CORS가 필요합니다.
4. 개발 환경과 프로덕션 환경의 분리
개발 중에는 프론트엔드가 로컬 환경(예: http://localhost:3000
)에서 실행되지만, 백엔드 API는 다른 출처(예: 개발 서버 또는 스테이징 환경)에 있을 수 있습니다.
CORS 작동 방식
CORS는 HTTP 헤더를 통해 작동합니다. 브라우저와 서버 간에 추가적인 HTTP 헤더를 교환하여 교차 출처 요청이 허용되는지 확인합니다.
간단한 요청(Simple Request)
다음 조건을 충족하는 요청은 "간단한 요청"으로 분류됩니다:
- HTTP 메서드가 GET, HEAD, POST 중 하나
- 자동으로 설정되는 헤더(예:
Connection
,User-Agent
) 외에Accept
,Accept-Language
,Content-Language
,Content-Type
헤더만 사용 Content-Type
헤더가application/x-www-form-urlencoded
,multipart/form-data
,text/plain
중 하나인 경우
간단한 요청의 경우:
- 브라우저가 서버에 요청을 보낼 때
Origin
헤더를 포함 - 서버는 응답에
Access-Control-Allow-Origin
헤더를 포함 - 브라우저는
Origin
과Access-Control-Allow-Origin
을 비교하여 접근 허용 여부 결정
// 요청 헤더
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
// 응답 헤더
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
프리플라이트 요청(Preflight Request)
간단한 요청이 아닌 경우(예: PUT, DELETE 메서드 사용, 사용자 정의 헤더 포함 등), 브라우저는 본 요청을 보내기 전에 "프리플라이트" 요청을 보냅니다:
- 브라우저가 OPTIONS 메서드로 프리플라이트 요청을 보냄
- 서버는 허용되는 메서드, 헤더 등을 포함한 응답을 반환
- 브라우저는 응답을 확인하고, 허용되면 본 요청을 보냄
// 프리플라이트 요청
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
// 프리플라이트 응답
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: Content-Type, Authorization
Access-Control-Max-Age: 86400
// 본 요청 (프리플라이트 성공 후)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123
// 본 요청에 대한 응답
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
인증 정보를 포함한 요청
기본적으로 브라우저는 교차 출처 요청에 쿠키나 HTTP 인증과 같은 인증 정보를 포함하지 않습니다. 인증 정보를 포함하려면:
- 클라이언트 측에서
credentials: 'include'
설정 - 서버 측에서
Access-Control-Allow-Credentials: true
헤더 포함 - 서버 측에서
Access-Control-Allow-Origin
에 와일드카드(*
)가 아닌 구체적인 출처 지정
// 클라이언트 측 (fetch API 사용)
fetch('https://api.example.com/data', {
credentials: 'include' // 인증 정보 포함
})
// 서버 응답 헤더
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
CORS 구성 방법
다양한 서버 환경에서 CORS를 구성하는 방법을 알아보겠습니다.
Express.js (Node.js)
// 방법 1: cors 미들웨어 패키지 사용
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 출처 허용
app.use(cors());
// 특정 출처만 허용
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
// 방법 2: 직접 헤더 설정
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.status(204).send();
}
next();
});
Spring Boot (Java)
// 방법 1: 어노테이션 사용
@CrossOrigin(origins = "https://app.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = {"Content-Type", "Authorization"},
allowCredentials = "true")
@RestController
public class ApiController {
// ...
}
// 방법 2: 전역 설정
@Configuration
public class WebConfig implements 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);
}
}
Django (Python)
# settings.py
INSTALLED_APPS = [
# ...
'corsheaders',
# ...
]
MIDDLEWARE = [
# ...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
# ...
]
# 모든 출처 허용
CORS_ALLOW_ALL_ORIGINS = True
# 또는 특정 출처만 허용
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
]
CORS_ALLOW_METHODS = [
'GET',
'POST',
'PUT',
'DELETE',
'OPTIONS',
]
CORS_ALLOW_HEADERS = [
'Authorization',
'Content-Type',
]
CORS_ALLOW_CREDENTIALS = True
Nginx
server {
# ...
location /api/ {
# ...
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
}
}
일반적인 CORS 오류와 해결 방법
개발 중 자주 마주치는 CORS 오류와 해결 방법을 알아보겠습니다.
1. "Access to fetch at 'X' from origin 'Y' has been blocked by CORS policy"
원인: 서버가 적절한 CORS 헤더를 제공하지 않음
해결 방법:
- 서버 측에서 적절한
Access-Control-Allow-Origin
헤더 설정 - 개발 환경에서는 프록시 서버 사용 (Create React App, Vue CLI 등에서 제공)
2. "Request header field X is not allowed by Access-Control-Allow-Headers"
원인: 사용자 정의 헤더를 사용하지만 서버가 이를 허용하지 않음
해결 방법:
- 서버 측에서
Access-Control-Allow-Headers
에 해당 헤더 추가
3. "Method X not allowed by Access-Control-Allow-Methods"
원인: 사용하려는 HTTP 메서드가 서버에서 허용되지 않음
해결 방법:
- 서버 측에서
Access-Control-Allow-Methods
에 해당 메서드 추가
4. "Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is 'false'"
원인: 인증 정보를 포함한 요청을 보냈지만 서버가 이를 허용하지 않음
해결 방법:
- 서버 측에서
Access-Control-Allow-Credentials: true
설정 Access-Control-Allow-Origin
에 와일드카드(*
) 대신 구체적인 출처 지정
개발 환경에서의 임시 해결책
⚠️ 주의: 아래 방법은 개발 환경에서만 사용해야 하며, 프로덕션 환경에서는 절대 사용하지 마세요!
- 브라우저 CORS 보안 비활성화: Chrome을
--disable-web-security
플래그와 함께 실행 - CORS 브라우저 확장 프로그램: "CORS Unblock"과 같은 확장 프로그램 사용
- 프록시 서버 사용: 개발 서버에서 API 요청을 프록시
// Create React App의 package.json에서 프록시 설정
{
"name": "my-app",
"version": "0.1.0",
"proxy": "https://api.example.com"
}
보안 고려사항
CORS를 구성할 때 주의해야 할 보안 사항:
1. 와일드카드 사용 자제
Access-Control-Allow-Origin: *
모든 출처를 허용하는 와일드카드는 개발 환경이나 공개 API에서만 사용하고, 인증이 필요한 API에서는 사용하지 마세요.
2. 필요한 출처만 허용
가능한 한 구체적인 출처만 허용하세요:
Access-Control-Allow-Origin: https://app.example.com
여러 출처를 허용해야 하는 경우, 요청의 Origin
헤더를 확인하고 동적으로 응답하세요:
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
// ...
next();
});
3. 인증 요청 처리 주의
인증 정보가 포함된 요청(credentials: 'include'
)을 허용할 때:
Access-Control-Allow-Origin
에 와일드카드(*
)를 사용할 수 없음- 구체적인 출처를 지정해야 함
Access-Control-Allow-Credentials: true
설정 필요
4. 필요한 메서드와 헤더만 허용
모든 메서드와 헤더를 허용하는 대신 필요한 것만 명시적으로 허용하세요:
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
5. CSRF 보호
CORS는 CSRF 공격을 완전히 방지하지 못합니다. 다음과 같은 추가 보호 조치를 고려하세요:
- CSRF 토큰 사용
- SameSite 쿠키 속성 설정
- 중요한 작업에 추가 인증 요구
실제 구현 예제
React + Express 애플리케이션에서의 CORS 구현
프론트엔드 (React)
// 인증 정보 없는 요청
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// 인증 정보 포함 요청
fetch('https://api.example.com/protected-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
credentials: 'include', // 쿠키 포함
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
백엔드 (Express)
const express = require('express');
const cors = require('cors');
const app = express();
// CORS 설정
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = ['https://app.example.com', 'https://www.example.com'];
// 개발 환경에서는 origin이 undefined일 수 있음 (예: Postman)
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('CORS policy violation'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 프리플라이트 캐싱 (24시간)
};
app.use(cors(corsOptions));
app.use(express.json());
// 공개 API
app.get('/data', (req, res) => {
res.json({ message: 'Public data' });
});
// 보호된 API
app.post('/protected-data', authenticateToken, (req, res) => {
res.json({ message: 'Protected data', user: req.user });
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401);
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.listen(3000, () => {
console.log('Server running on port 3000');
});
결론
CORS는 웹 보안의 중요한 부분으로, 웹 개발자가 반드시 이해해야 하는 개념입니다. 동일 출처 정책은 웹을 보호하는 중요한 메커니즘이지만, 현대 웹 애플리케이션의 복잡한 아키텍처에서는 제약이 될 수 있습니다. CORS는 이러한 제약을 안전하게 완화하는 표준 방법을 제공합니다.
CORS를 효과적으로 구현하기 위한 핵심 원칙:
- 최소 권한 원칙 적용: 필요한 출처, 메서드, 헤더만 허용
- 보안과 편의성 균형 유지: 과도한 제한은 사용성을 해치지만, 과도한 허용은 보안을 약화시킴
- 계층적 방어 전략 사용: CORS만으로는 완벽한 보안을 보장할 수 없으므로 CSRF 토큰, 적절한 인증 체계 등 추가 보안 조치 필요
CORS 관련 문제는 웹 개발에서 자주 발생하지만, 그 개념과 작동 방식을 이해하면 효과적으로 해결할 수 있습니다. 올바르게 구성된 CORS 정책은 애플리케이션의 보안을 유지하면서도 필요한 교차 출처 통신을 가능하게 합니다.
참고 자료
'Frontend Development' 카테고리의 다른 글
script 태그의 성능 최적화: async와 defer 속성 완벽 가이드 (0) | 2025.05.28 |
---|---|
React 성능 최적화: 메모이제이션 기법으로 불필요한 리렌더링 방지하기 (4) | 2025.05.28 |
React의 Error Boundary: 안정적인 프론트엔드 애플리케이션을 위한 필수 요소 (2) | 2025.05.28 |
JavaScript Promise 완벽 가이드: resolve()와 fulfilled 상태 이해하기 (0) | 2025.05.28 |
자바스크립트의 함수 정의 방식: 선언식 vs 표현식 완벽 가이드 (0) | 2025.05.28 |