Engineering/javascript

CORS (교차 출처 리소스 공유) 오류 설명

부스 boos 2025. 6. 26. 18:21
728x90

 웹 브라우저는 보안을 위해 동일 출처 정책(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 문제를 해결하는 이유는 다음과 같다.

  1. 브라우저의 CORS 제한: CORS 정책은 브라우저에 의해 적용되는 보안 메커니즘이다. 브라우저는 사용자의 안전을 위해 웹페이지가 악의적으로 다른 도메인의 리소스를 사용하는 것을 막는다. 따라서 http://localhost:3000 에서 실행되는 HTML 파일이 직접 https://www.koreaexim.go.kr로 요청을 보내려 하면 브라우저가 막는 것입니다.
  2. 서버 간 통신 (Server-to-Server Communication): Node.js 프록시 서버는 서버는 웹 브라우저와 달리 동일 출처 정책의 제약을 받지 않습니다. 서버 간의 통신에는 CORS와 같은 보안 제약이 적용되지 않는다. Node.js 서버는 https://www.koreaexim.go.kr 에 직접 HTTP 요청을 보내고 응답을 받을 수 있다.
  3. 프록시의 역할:
    • 클라이언트(브라우저) -> 프록시 서버: 브라우저는 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