웹 브라우저는 보안을 위해 동일 출처 정책(Same-Origin Policy)이라는 규칙을 따릅니다. 이 규칙은 웹페이지가 로드된 출처(Origin)와 다른 출처의 리소스에 접근하는 것을 제한한다. 여기서 "출처"는 프로토콜(http, https 등), 호스트(도메인 이름), 포트 번호를 조합한 것을 의미한다.
예를 들어, http://mywebsite.com:8080 에서 로드된 웹페이지는 http://anotherwebsite.com 에 있는 데이터나 https://mywebsite.com 에 있는 데이터에 바로 접근할 수 없다. 이는 악의적인 웹사이트가 사용자 모르게 다른 웹사이트의 정보를 빼가는 것을 막기 위한 보안 조치이다.
- exchange.html
<!DOCTYPE html>
<html>
<head>
<title>실시간 달러 환율</title>
<style>
#exchangeRate {
font-size: 24px;
font-weight: bold;
}
#lastUpdated {
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<h1>실시간 달러 환율</h1>
<div id="exchangeRate">로딩 중...</div>
<div id="lastUpdated"></div>
<script>
function getExchangeRate() {
const url = 'https://www.koreaexim.go.kr/site/program/financial/exchangeJSON';
const params = {
authkey: '', // 발급받은 API 키를 여기에 입력하세요 (선택 사항)
searchdate: getCurrentDate(),
curcd: 'USD'
};
const queryString = Object.keys(params)
.map(key => key + '=' + params[key])
.join('&');
fetch(`${url}?${queryString}`)
.then(response => response.json())
.then(data => {
if (data.length > 0) {
const rateInfo = data[0];
const exchangeRate = parseFloat(rateInfo.tts).toFixed(2);
const lastUpdatedTime = new Date();
document.getElementById("exchangeRate").innerText = exchangeRate + " 원/달러";
document.getElementById("lastUpdated").innerText = "최근 업데이트: " + formatDate(lastUpdatedTime);
} else {
document.getElementById("exchangeRate").innerText = "환율 정보를 가져올 수 없습니다.";
}
})
.catch(error => {
console.error('환율 정보를 가져오는 중 오류 발생:', error);
document.getElementById("exchangeRate").innerText = "오류 발생";
});
}
function getCurrentDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// 페이지 로드 시 환율 정보를 가져옵니다.
getExchangeRate();
// 10초마다 환율 정보를 갱신합니다.
setInterval(getExchangeRate, 10000);
</script>
</body>
</html>
실시간으로 환율 조회를 위한 기능을 위해 위의 exchange.html 페이지를 만들고 ,이를 double click 해서 실행해서 개발자 페이지 도구에서 ' from origin 'null' has been blocked by CORS policy...." 이라는 에러를 보게 된다. (물론 API 키가 없어 실제로도 오류가 발생하긴 한다.)
HTML 파일을 더블클릭해서 브라우저에서 열면, 해당 파일은 file:// 프로토콜로 로드되면서 이 file:// 출처는 어떤 웹 서버 출처와도 다르기 때문에, 한국수출입은행( https://www.koreaexim.go.kr )와 같은 외부 API 서버에 데이터를 요청하면 브라우저는 이를 "다른 출처의 리소스에 대한 요청"으로 간주한다.
한국수출입은행 API 서버 가 이러한 교차 출처 요청(Cross-Origin Request)을 명시적으로 허용하지 않으면, 브라우저는 보안상의 이유로 해당 요청을 차단하고 CORS 오류를 발생시킨다. 즉, API 서버는 file:// 에서 온 요청을 신뢰할 수 없다고 판단하여 데이터를 주지 않는 것이다.
이 문제를 해결하기 위해서 웹 서버(예: Node.js 프록시 서버)를 사용해서 HTML 파일을 제공하거나, API 서버에서 file:// 출처를 허용하도록 설정해야 하지만, 후자는 일반적으로 불가능하다.
Node.js 프록시 서버에서 한국수출입은행 API 서버를 호출하는 것은 API 서버 입장에서는 여전히 다른 도메인에서 온 요청입니다. 한국수출입은행 API 서버는 Node.js 서버의 IP 주소(또는 도메인)를 요청의 출처로 인식한다.
그럼에도 불구하고 이 방법이 CORS 문제를 해결하는 이유는 다음과 같다.
- 브라우저의 CORS 제한: CORS 정책은 브라우저에 의해 적용되는 보안 메커니즘이다. 브라우저는 사용자의 안전을 위해 웹페이지가 악의적으로 다른 도메인의 리소스를 사용하는 것을 막는다. 따라서 http://localhost:3000 에서 실행되는 HTML 파일이 직접 https://www.koreaexim.go.kr로 요청을 보내려 하면 브라우저가 막는 것입니다.
- 서버 간 통신 (Server-to-Server Communication): Node.js 프록시 서버는 서버는 웹 브라우저와 달리 동일 출처 정책의 제약을 받지 않습니다. 서버 간의 통신에는 CORS와 같은 보안 제약이 적용되지 않는다. Node.js 서버는 https://www.koreaexim.go.kr 에 직접 HTTP 요청을 보내고 응답을 받을 수 있다.
- 프록시의 역할:
- 클라이언트(브라우저) -> 프록시 서버: 브라우저는 http://localhost:3000/exchange와 같이 자신과 동일한 출처(또는 CORS 정책에 의해 허용된 출처)인 프록시 서버로 요청을 보낸다. 우리의 server.js 파일에서는 app.use(cors());를 사용하여 http://localhost:3000이 모든 출처의 요청을 허용하도록 설정했으므로, file://에서 로드된 HTML 파일의 요청을 받아들일 수 있다.
- 프록시 서버 -> 외부 API 서버: 프록시 서버는 브라우저의 요청을 받아서, 그 요청을 기반으로 자신이 직접 https://www.koreaexim.go.kr API 서버에 요청을 보낸다. 이 과정은 서버 내부에서 일어나므로 브라우저의 CORS 제한을 우회하는 것이다.
- 외부 API 서버 -> 프록시 서버: API 서버는 Node.js 프록시 서버로부터의 요청을 받아서 처리하고 응답을 보낸다. API 서버는 Node.js 서버를 일반적인 클라이언트 중 하나로 인식하며, CORS 정책은 서버 간 통신에는 적용되지 않는다.
- 프록시 서버 -> 클라이언트(브라우저): 프록시 서버는 API 서버로부터 받은 응답을 브라우저에 다시 전달한다. 브라우저 입장에서는 자신이 http://localhost:3000 (프록시 서버)로부터 데이터를 받은 것이므로 CORS 오류가 발생하지 않는다.
- index.html
<!DOCTYPE html>
<html>
<head>
<title>실시간 달러 환율</title>
<style>
#exchangeRate {
font-size: 24px;
font-weight: bold;
}
#lastUpdated {
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<h1>실시간 달러 환율</h1>
<div id="exchangeRate">로딩 중...</div>
<div id="lastUpdated"></div>
<script>
function getExchangeRate() {
// 로컬 프록시 서버의 URL로 변경
const url = 'http://localhost:3000/exchange';
const params = {
searchdate: getCurrentDate(),
curcd: 'USD'
};
const queryString = Object.keys(params)
.map(key => key + '=' + params[key])
.join('&');
fetch(`<span class="math-inline">\{url\}?</span>{queryString}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.length > 0) {
const rateInfo = data[0];
// `ttb` 또는 `tts` 필드를 사용하세요.
// 'ttb'는 송금보낼때 (매입률), 'tts'는 송금받을때 (매도율) 입니다.
// 여기서는 예시로 'deal_bas_r' (매매기준율) 또는 'ttb'를 사용할 수 있습니다.
// API 응답 구조에 따라 적절한 필드를 선택해야 합니다.
// 일반적으로 매매기준율이 표시되는 경우가 많으므로, 여기서는 'deal_bas_r'을 사용하겠습니다.
// 만약 'deal_bas_r'이 없다면 'tts' 또는 'ttb'를 사용하세요.
const exchangeRate = parseFloat(rateInfo.deal_bas_r.replace(',', '')).toFixed(2); // 콤마 제거 후 숫자 변환
const lastUpdatedTime = new Date();
document.getElementById("exchangeRate").innerText = exchangeRate + " 원/달러";
document.getElementById("lastUpdated").innerText = "최근 업데이트: " + formatDate(lastUpdatedTime);
} else {
document.getElementById("exchangeRate").innerText = "환율 정보를 가져올 수 없습니다.";
}
})
.catch(error => {
console.error('환율 정보를 가져오는 중 오류 발생:', error);
document.getElementById("exchangeRate").innerText = "오류 발생";
});
}
function getCurrentDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `<span class="math-inline">\{year\}</span>{month}${day}`;
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `<span class="math-inline">\{year\}\-</span>{month}-${day} <span class="math-inline">\{hour\}\:</span>{minute}:${second}`;
}
// 페이지 로드 시 환율 정보를 가져옵니다.
getExchangeRate();
// 10초마다 환율 정보를 갱신합니다.
setInterval(getExchangeRate, 10000);
</script>
</body>
</html>
- server.js (Node 로 실행)
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000; // 프록시 서버가 실행될 포트
// CORS 허용
app.use(cors());
// 환율 정보를 가져오는 API 엔드포인트
app.get('/exchange', async (req, res) => {
const authkey = 'YOUR_API_KEY_HERE'; // 여기에 발급받은 API 키를 입력하세요!
const searchdate = req.query.searchdate;
const curcd = req.query.curcd;
const url = `https://www.koreaexim.go.kr/site/program/financial/exchangeJSON?authkey=<span class="math-inline">\{authkey\}&searchdate\=</span>{searchdate}&curcd=${curcd}`;
try {
const response = await fetch(url);
const data = await response.json();
res.json(data);
} catch (error) {
console.error('프록시 서버에서 오류 발생:', error);
res.status(500).json({ error: '환율 정보를 가져오는 데 실패했습니다.' });
}
});
app.listen(port, () => {
console.log(`프록시 서버가 http://localhost:${port} 에서 실행 중입니다.`);
console.log('브라우저에서 HTML 파일을 열어 확인하세요.');
});
출처 : Gemini 2.5 Flash Pro
'Engineering > javascript' 카테고리의 다른 글
DataTable 에서 AJAX 로 pagination 처리 (0) | 2016.06.29 |
---|---|
자바스크립트용 ipcalc (0) | 2011.03.17 |