loading 오류 수정

This commit is contained in:
Macbook
2026-05-31 23:15:19 +09:00
parent 7f33640b86
commit 5adb22721e
6 changed files with 296 additions and 20 deletions
+3 -2
View File
@@ -243,11 +243,12 @@ function App() {
const priorityMarketsRef = useRef<string[]>([symbol]);
const getPriorityMarkets = useCallback(() => priorityMarketsRef.current, []);
/** 마켓 패널기 전에는 우선 종목만 — PC·모바일 공통 (ticker 429 방지) */
/** lite=모바일(청크 REST 금지). loadFull=마켓 패널 열림 시 백엔드 일괄 ticker */
const marketTickerOpts = useMemo(() => ({
lite: isMobile,
loadFull: showMarketPanel,
getPriorityMarkets,
}), [showMarketPanel, getPriorityMarkets]);
}), [isMobile, showMarketPanel, getPriorityMarkets]);
const { tickers: marketTickers, loading: marketLoading, usdRate } = useMarketTicker(marketTickerOpts);
// ── 레이아웃 상태 ─────────────────────────────────────────────────────
@@ -5,6 +5,7 @@ import {
toggleFavorite,
FAVORITES_CHANGED_EVENT,
} from '../utils/marketStorage';
import { API_BASE } from '../utils/backendApi';
import { UPBIT_API } from '../utils/upbitApi';
import { fetchTickersThrottled } from '../utils/upbitTickerFetch';
import { safeToFixed } from '../utils/safeFormat';
@@ -98,7 +99,11 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
// ── 1. 마켓 목록 로드 ─────────────────────────────────────────────────────
useEffect(() => {
setLoading(true);
fetch(`${UPBIT_API}/market/all?isDetails=false`)
fetch(`${API_BASE}/markets/all?isDetails=false`)
.then(r => {
if (!r.ok) return fetch(`${UPBIT_API}/market/all?isDetails=false`);
return r;
})
.then(r => {
if (!r.ok) throw new Error(`market/all ${r.status}`);
return r.json();
+28 -9
View File
@@ -8,6 +8,7 @@
*/
import { useState, useEffect, useRef, useCallback } from 'react';
import { setMarketNames } from '../utils/marketNameCache';
import { API_BASE } from '../utils/backendApi';
import { UPBIT_API } from '../utils/upbitApi';
import { fetchTickersThrottled, type UpbitTickerRaw } from '../utils/upbitTickerFetch';
import { subscribeUpbitWs } from '../utils/upbitWsBroker';
@@ -66,10 +67,17 @@ function getMarketCode(raw: UpbitTickerRaw): string {
}
async function fetchAllMarkets(): Promise<MarketInfo[]> {
const res = await fetch(`${UPBIT_API}/market/all?isDetails=true`);
if (!res.ok) throw new Error(`market/all ${res.status}`);
const data: UpbitMarketAll[] = await res.json();
const krw = data.filter(m => m.market.startsWith('KRW-'));
let data: UpbitMarketAll[] | null = null;
try {
const res = await fetch(`${API_BASE}/markets/all?isDetails=true`);
if (res.ok) data = await res.json();
} catch { /* backend fallback */ }
if (!data) {
const res = await fetch(`${UPBIT_API}/market/all?isDetails=true`);
if (!res.ok) throw new Error(`market/all ${res.status}`);
data = await res.json();
}
const krw = (data ?? []).filter(m => m.market.startsWith('KRW-'));
const nameMap: Record<string, string> = {};
krw.forEach(m => { nameMap[m.market] = m.korean_name; });
setMarketNames(nameMap);
@@ -138,6 +146,7 @@ function subscribeTickerWs(
export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
const loadFull = opts.loadFull ?? false;
const isMobileLite = opts.lite ?? false;
const priorityList = mergePriorityMarkets(opts.getPriorityMarkets?.() ?? opts.priorityMarkets);
const priorityKey = priorityList.join(',');
@@ -205,12 +214,21 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
const runFullTickerLoad = useCallback(async () => {
const all = marketListRef.current;
if (all.length === 0 || fullLoadedRef.current) return;
if (isMobileLite) {
await loadTickersFor([]);
if (!mountedRef.current) return;
fullLoadedRef.current = true;
attachWs(wsTickerMarkets(all, priorityList));
return;
}
await loadTickersFor(all);
if (!mountedRef.current) return;
fullLoadedRef.current = true;
attachWs(wsTickerMarkets(all, priorityList));
startPoll(all);
}, [loadTickersFor, attachWs, startPoll, priorityList]);
}, [isMobileLite, loadTickersFor, attachWs, startPoll, priorityList]);
// 1) 마켓 목록 + 우선 종목 ticker (마운트 1회)
useEffect(() => {
@@ -238,7 +256,7 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
initDoneRef.current = true;
attachWs(wsTickerMarkets(allMarkets, priorityList));
if (loadFull) {
if (loadFull && !isMobileLite) {
setTimeout(() => { void runFullTickerLoad(); }, FULL_LOAD_DEFER_MS);
}
})();
@@ -252,14 +270,15 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
// eslint-disable-next-line react-hooks/exhaustive-deps -- init only
}, []);
// 2) 마켓 패널 열림 → 전 종목 (지연·스로틀)
// 2) 마켓 패널 열림 → 전 종목 (모바일: 백엔드 1회, PC: 지연 후 일괄)
useEffect(() => {
if (!loadFull || !initDoneRef.current) return;
if (fullLoadedRef.current) return;
const t = setTimeout(() => { void runFullTickerLoad(); }, FULL_LOAD_DEFER_MS);
const delay = isMobileLite ? 300 : FULL_LOAD_DEFER_MS;
const t = setTimeout(() => { void runFullTickerLoad(); }, delay);
return () => clearTimeout(t);
}, [loadFull, runFullTickerLoad]);
}, [isMobileLite, loadFull, runFullTickerLoad]);
// 3) 우선 종목 변경 (lite) — 전체 재시작 없이 보강
useEffect(() => {
+48 -8
View File
@@ -1,6 +1,7 @@
/**
* 업비트 /ticker REST — 청크·지연·429 백오프 (공유)
* KRW ticker 조회 — 백엔드 /api/markets/tickers 우선 (1회·캐시), 실패 시 /upbit-api 폴백
*/
import { API_BASE } from './backendApi';
import { UPBIT_API } from './upbitApi';
export interface UpbitTickerRaw {
@@ -17,16 +18,38 @@ export interface UpbitTickerRaw {
change?: 'RISE' | 'FALL' | 'EVEN';
}
const CHUNK_SIZE = 60;
const CHUNK_DELAY_MS = 150;
const MAX_CHUNK_ATTEMPTS = 5;
const CHUNK_SIZE = 40;
const CHUNK_DELAY_MS = 200;
const MAX_CHUNK_ATTEMPTS = 4;
function sleep(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
/** 중복 제거 후 청크 단위 ticker 조회 (429 시 백오프) */
export async function fetchTickersThrottled(markets: string[]): Promise<UpbitTickerRaw[]> {
/** 백엔드 일괄 ticker (권장) */
async function fetchTickersFromBackend(markets: string[]): Promise<UpbitTickerRaw[] | null> {
try {
const qs = markets.length > 0
? `?markets=${encodeURIComponent(markets.join(','))}`
: '';
const res = await fetch(`${API_BASE}/markets/tickers${qs}`);
if (res.status === 429) {
await sleep(1500);
const retry = await fetch(`${API_BASE}/markets/tickers${qs}`);
if (!retry.ok) return null;
const data = (await retry.json()) as UpbitTickerRaw[];
return Array.isArray(data) ? data : null;
}
if (!res.ok) return null;
const data = (await res.json()) as UpbitTickerRaw[];
return Array.isArray(data) ? data : null;
} catch {
return null;
}
}
/** 레거시: nginx /upbit-api 직접 (청크·지연) */
async function fetchTickersFromUpbitDirect(markets: string[]): Promise<UpbitTickerRaw[]> {
const unique = [...new Set(markets.filter(m => m?.startsWith('KRW-')))];
if (unique.length === 0) return [];
@@ -38,7 +61,7 @@ export async function fetchTickersThrottled(markets: string[]): Promise<UpbitTic
try {
const res = await fetch(`${UPBIT_API}/ticker?markets=${chunk.join(',')}`);
if (res.status === 429) {
await sleep(500 * (attempt + 1));
await sleep(800 * (attempt + 1));
continue;
}
if (res.ok) {
@@ -47,10 +70,27 @@ export async function fetchTickersThrottled(markets: string[]): Promise<UpbitTic
}
done = true;
} catch {
await sleep(300 * (attempt + 1));
await sleep(400 * (attempt + 1));
}
}
if (i + CHUNK_SIZE < unique.length) await sleep(CHUNK_DELAY_MS);
}
return results;
}
/**
* ticker 조회 — 전 종목은 백엔드 1회, 소량은 markets 쿼리
*/
export async function fetchTickersThrottled(markets: string[]): Promise<UpbitTickerRaw[]> {
const unique = [...new Set(markets.filter(m => m?.startsWith('KRW-')))];
const fromBackend = await fetchTickersFromBackend(unique);
if (fromBackend != null && fromBackend.length > 0) return fromBackend;
if (unique.length === 0) {
const all = await fetchTickersFromBackend([]);
if (all != null && all.length > 0) return all;
}
return fetchTickersFromUpbitDirect(unique);
}