동일 출처 정책(Same-Origin Policy) 이해하기
웹 브라우저는 보안을 위해 동일 출처 정책(Same-Origin Policy)을 기본으로 적용합니다. 이 정책은 웹 애플리케이션이 한 출처(origin)에서 로드된 리소스만 접근할 수 있도록 제한합니다. 여기서 '출처'란 프로토콜(protocol), 호스트(host), 포트(port)가 모두 일치해야 합니다.
예를 들어, http://example.com:8080 에서 실행 중인 자바스크립트는 https://example.com:8080 또는 http://example.com:3000 같은 다른 출처의 API에 직접 XMLHttpRequest나 fetch로 요청을 보낼 수 없습니다. 이러한 시도는 브라우저에서 차단되며 콘솔에 Access-Control-Allow-Origin 관련 오류 메시지가 출력됩니다.
동일 출처 정책이 제한하는 주요 동작은 다음과 같습니다:
- 다른 도메인의 쿠키, localStorage, sessionStorage 접근 불가
- 다른 도메인의 DOM 요소 조작 불가
- XMLHttpRequest 및 Fetch API를 통한 비동기 요청 제한
크로스 도메인 문제 해결 전략
1. CORS (Cross-Origin Resource Sharing)
CORS는 W3C 표준으로, 서버 측에서 특정 HTTP 헤더를 추가함으로써 클라이언트의 크로스 도메인 요청을 허용하는 방법입니다. 브라우저는 크로스 도메인 요청을 감지하면 자동으로 Origin 헤더를 포함해 요청을 보내고, 서버가 적절한 응답 헤더(예: Access-Control-Allow-Origin)를 반환하면 요청을 성공적으로 처리합니다.
서버 예제 (Node.js + Koa):
const { successBody } = require('../utils');
class CrossDomainController {
static async corsHandler(ctx) {
const query = ctx.request.query;
// 모든 도메인 허용 (보안상 * 대신 특정 도메인 지정 권장)
ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
ctx.set('Access-Control-Allow-Credentials', 'true');
ctx.cookies.set('tokenId', 'abc123', { httpOnly: true });
ctx.body = successBody({ message: query.msg }, 'success');
}
}
module.exports = CrossDomainController;
클라이언트 요청 예시:
fetch('http://localhost:9871/api/cors?msg=hello')
.then(response => response.json())
.then(data => console.log(data));
2. JSONP (JSON with Padding)
JSONP은 스크립트 태그의 src 속성이 동일 출처 정책의 영향을 받지 않는 점을 이용한 구형 기법입니다. 서버는 클라이언트가 지정한 콜백 함수 이름을 사용해 자바스크립트 함수 호출 형태로 데이터를 반환합니다.
사용 예시:
function handleResponse(data) {
const list = document.getElementById('result');
let items = '';
data.results.forEach(item => {
items += <li>${item}</li>;
});
list.innerHTML = items;
}
document.getElementById('search').addEventListener('input', function(e) {
const script = document.createElement('script');
script.src = https://suggest.example.com/search?term=${e.target.value}&callback=handleResponse;
document.head.appendChild(script);
});
jQuery에서는 dataType: 'jsonp' 옵션으로 간편하게 사용 가능합니다:
$.ajax({
url: 'https://suggest.example.com/search',
data: { term: 'query' },
dataType: 'jsonp',
success: function(res) {
console.log(res);
}
});
3. 서버 사이드 프록시(Server Proxy)
프론트엔드가 직접 외부 API를 호출하는 대신, 자신의 백엔드 서버를 거쳐 요청을 전달하는 방식입니다. 프론트엔드 → 백엔드(API 게이트웨이) → 외부 서비스 구조로, 백엔드는 네트워크 계층에서 자유롭게 외부 리소스에 접근할 수 있으므로 크로스 도메인 문제가 발생하지 않습니다.
예: Nginx 리버스 프록시 설정
location /api/external/ {
proxy_pass https://external-api.com/;
proxy_set_header Host external-api.com;
}
4. WebSocket을 통한 실시간 통신
WebSocket은 HTTP와 달리 동일 출처 정책의 제약을 덜 받으며, 서버와 클라이언트 간 양방향 실시간 통신을 지원합니다. 서버가 연결 요청을 수락하면 이후 메시지는 자유롭게 주고받을 수 있습니다.
클라이언트 코드:
<input type="text" id="userInput" placeholder="메시지를 입력하세요">
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io('http://backend-server.com:8080');
socket.on('connect', () => {
console.log('서버에 연결됨');
socket.on('message', (msg) => {
console.log('수신:', msg);
});
});
document.getElementById('userInput').addEventListener('blur', function() {
socket.send(this.value);
});
</script>
서버 코드 (Node.js + Socket.IO):
const http = require('http');
const socketIo = require('socket.io');
const server = http.createServer();
const io = socketIo(server, {
cors: {
origin: "http://frontend.com",
methods: ["GET", "POST"]
}
});
io.on('connection', (client) => {
client.on('message', (data) => {
console.log('클라이언트로부터:', data);
client.send('echo: ' + data);
});
client.on('disconnect', () => {
console.log('클라이언트 연결 종료');
});
});
server.listen(8080, () => {
console.log('Socket 서버 8080번 포트에서 실행 중');
});