
이번 신한투자증권 프로디지털아카데미 프로젝트에서 구축한 실시간 차트 구현에서 초당 요청 거래 제한을 회피하는 방법과 성능 개선을 이야기하고자 한다.


// 한투 API 요청 함수
const makeHantuRequest = async () => {
const queryParams = new URLSearchParams(cacheParams).toString();
const myGetToken = await getPeriodToken();
if (!myGetToken) {
throw new Error("토큰이 없습니다.");
}
const getToken = myGetToken.access_token;
const response = await fetch(
`https://openapivts.koreainvestment.com:29443/uapi/overseas-price/v1/quotations/dailyprice?${queryParams}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${getToken}`,
"content-type": "application/json",
appKey: appKey,
appSecret: appSecret,
tr_id: "HHDFS76240000",
},
}
);
const data = await response.json();
// 성공적인 응답인 경우에만 캐시에 저장 (BYMD가 있을 때만)
if ((data.rt_cd === "0" || data.rt_cd === 0) && shouldCache) {
saveCachedData("daily", cacheParams, data);
}
return data;
};
// 큐를 통해 요청 처리
const data = await hantuQueue.addRequest(makeHantuRequest);
-> hantuQueue에 요청을 대기시킨다.
class HantuRequestQueue {
constructor() {
this.queue = [];
this.processing = false;
this.lastRequestTime = 0;
this.minInterval = 200; // 최소 200ms 간격 (초당 5회 제한으로 조정)
}
async addRequest(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const { requestFn, resolve, reject } = this.queue.shift();
try {
// 요청 간격 조절
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minInterval) {
const delay = this.minInterval - timeSinceLastRequest;
await new Promise((resolve) => setTimeout(resolve, delay));
}
this.lastRequestTime = Date.now();
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
// 메모리 기반 캐시 객체
const chartCache = new Map();
// 실시간 데이터 임시 캐시 (5초)
const realtimeCache = new Map();
...
function generateCacheKey(type, params) {
const sortedParams = Object.keys(params)
.sort()
.map((key) => `${key}:${params[key]}`)
.join("|");
return `chart_${type}_${sortedParams}`;
}
// 캐시에서 데이터 조회
function getCachedData(type, params) {
const cacheKey = generateCacheKey(type, params);
const cached = chartCache.get(cacheKey);
if (cached) {
console.log(`Returning cached ${type} chart data`);
return cached;
}
return null;
}
// 캐시에 데이터 저장
function saveCachedData(type, params, data) {
const cacheKey = generateCacheKey(type, params);
chartCache.set(cacheKey, data);
console.log(`Cached ${type} chart data`);
}
// 실시간 1분봉 데이터 캐시 조회
function getRealtimeCache(symbol, params) {
const cacheKey = `realtime_${symbol}_${JSON.stringify(params)}`;
const cached = realtimeCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 5000) {
// 5초 유효
console.log(`Returning realtime cache for ${symbol}`);
return cached.data;
}
return null;
}
// 실시간 1분봉 데이터 캐시 저장
function saveRealtimeCache(symbol, params, data) {
const cacheKey = `realtime_${symbol}_${JSON.stringify(params)}`;
realtimeCache.set(cacheKey, {
data,
timestamp: Date.now(),
});
console.log(`Cached realtime data for ${symbol}`);
}
즉, 사용자가 많아지는 경우 발생하는 문제점을 개선하기 위해서는 종목별 차트 데이터를 DB에 저장하는 방식이 사실상 유일하다고 생각합니다. 그렇기에 소규모 프로젝트 단위에서는 캐싱과 큐를 통해 해결하는 방법을 추천합니다.