초기로딩 부하문제 수정
This commit is contained in:
+25
-127
@@ -1,4 +1,5 @@
|
||||
import type { OHLCVBar, Timeframe } from '../types';
|
||||
import { subscribeUpbitTrade, subscribeUpbitWsConnection } from './upbitWsBroker';
|
||||
|
||||
// ─── Upbit REST API ────────────────────────────────────────────────────────
|
||||
// 개발: Vite proxy (/upbit-api) → https://api.upbit.com
|
||||
@@ -180,140 +181,37 @@ export interface UpbitWsOptions {
|
||||
onError: (err: string) => void;
|
||||
}
|
||||
|
||||
// 개발: Vite proxy (/upbit-ws) → wss://api.upbit.com/websocket/v1
|
||||
// 프로덕션: nginx proxy (/upbit-ws) → wss://api.upbit.com/websocket/v1
|
||||
// 항상 동일 오리진 상대 경로를 사용 → CORS / Origin 차단 모두 우회
|
||||
function getWsUrl(): string {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
return `${proto}://${window.location.host}/upbit-ws`;
|
||||
}
|
||||
|
||||
const INITIAL_DELAY = 80; // React StrictMode 이중마운트 방지 (cleanup < 80ms면 연결 안 함)
|
||||
const RECONNECT_BASE = 2000; // 최초 재연결 대기
|
||||
const RECONNECT_MAX = 30000; // 최대 재연결 대기
|
||||
const PING_INTERVAL = 20000; // 20초마다 PING (서버가 60초 내 응답 없으면 끊음)
|
||||
|
||||
/** @deprecated 직접 WebSocket 대신 upbitWsBroker 사용. 하위 호환 래퍼. */
|
||||
export class UpbitWebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private opts: UpbitWsOptions;
|
||||
private destroyed = false;
|
||||
private retryCount = 0;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(opts: UpbitWsOptions) {
|
||||
this.opts = opts;
|
||||
// 80ms 지연으로 React StrictMode 이중 마운트 방지
|
||||
// (cleanup 이 80ms 안에 호출되면 connect 가 취소됨)
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.connect();
|
||||
}, INITIAL_DELAY);
|
||||
}
|
||||
|
||||
private connect() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(getWsUrl());
|
||||
this.ws = ws;
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
this.retryCount = 0;
|
||||
|
||||
// ── 구독 메시지 ──────────────────────────────────────────────
|
||||
// format 생략 시 DEFAULT (긴 키명), SIMPLE이면 단축 키명
|
||||
ws.send(JSON.stringify([
|
||||
{ ticket: `react_chart_${Date.now()}` },
|
||||
{ type: 'trade', codes: [this.opts.market.toUpperCase()] },
|
||||
{ format: 'DEFAULT' },
|
||||
]));
|
||||
|
||||
// ── PING keepalive ─────────────────────────────────────────
|
||||
this.clearPing();
|
||||
this.pingTimer = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send('PING');
|
||||
}
|
||||
}, PING_INTERVAL);
|
||||
private destroyed = false;
|
||||
private unsubTrade: (() => void) | null = null;
|
||||
private unsubConn: (() => void) | null = null;
|
||||
|
||||
constructor(private opts: UpbitWsOptions) {
|
||||
this.unsubConn = subscribeUpbitWsConnection(connected => {
|
||||
if (this.destroyed) return;
|
||||
if (connected) {
|
||||
this.opts.onConnect();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// ① 텍스트 메시지: 서버 PING → PONG 응답, 상태 메시지 → 무시
|
||||
if (typeof event.data === 'string') {
|
||||
if (event.data === 'PING') ws.send('PONG');
|
||||
return;
|
||||
}
|
||||
|
||||
// ② 바이너리 메시지: 체결/시세 데이터
|
||||
let msg: UpbitMessage | null = null;
|
||||
try {
|
||||
const text = new TextDecoder('utf-8').decode(event.data as ArrayBuffer);
|
||||
if (text === 'PING') { ws.send('PONG'); return; }
|
||||
const parsed = JSON.parse(text) as UpbitMessage;
|
||||
if (!('type' in parsed)) return;
|
||||
msg = parsed;
|
||||
} catch {
|
||||
// JSON 파싱 실패 → 무시
|
||||
return;
|
||||
}
|
||||
// JSON 파싱과 onMessage 를 분리: 차트 업데이트 오류가 WS 메시지 처리를 중단시키지 않도록
|
||||
try {
|
||||
this.opts.onMessage(msg);
|
||||
} catch (err) {
|
||||
console.error('[UpbitWS] onMessage 처리 중 오류:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (ev) => {
|
||||
this.clearPing();
|
||||
if (!this.destroyed) {
|
||||
// 정상 종료(1000/1001)가 아닐 때만 오류 표시
|
||||
if (ev.code !== 1000 && ev.code !== 1001) {
|
||||
this.opts.onError(`연결 끊김 (code ${ev.code})`);
|
||||
}
|
||||
this.opts.onDisconnect();
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
// onerror 직후 onclose가 반드시 발생하므로 여기서는 로그만
|
||||
// (onclose에서 재연결 처리)
|
||||
console.warn('[UpbitWS] onerror 발생 → onclose 대기');
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
this.opts.onError(`WebSocket 초기화 실패: ${String(err)}`);
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private clearPing() {
|
||||
if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = null; }
|
||||
}
|
||||
|
||||
private scheduleReconnect() {
|
||||
if (this.destroyed) return;
|
||||
const delay = Math.min(RECONNECT_BASE * 2 ** this.retryCount, RECONNECT_MAX);
|
||||
this.retryCount++;
|
||||
console.info(`[UpbitWS] ${delay}ms 후 재연결 시도 (${this.retryCount}회차)`);
|
||||
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
||||
} else {
|
||||
this.opts.onDisconnect();
|
||||
}
|
||||
});
|
||||
this.unsubTrade = subscribeUpbitTrade(opts.market, msg => {
|
||||
if (this.destroyed) return;
|
||||
try {
|
||||
this.opts.onMessage(msg);
|
||||
} catch (err) {
|
||||
console.error('[UpbitWS] onMessage 처리 중 오류:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
this.clearPing();
|
||||
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
||||
if (this.ws) {
|
||||
this.ws.onclose = null;
|
||||
this.ws.onerror = null;
|
||||
this.ws.close(1000, 'destroy');
|
||||
this.ws = null;
|
||||
}
|
||||
this.unsubTrade?.();
|
||||
this.unsubTrade = null;
|
||||
this.unsubConn?.();
|
||||
this.unsubConn = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user