-
Notifications
You must be signed in to change notification settings - Fork 0
Upbit Market API 연동 및 실시간 시세 조회 기능 구현 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 Upbit API를 활용하여 KRW 마켓의 실시간 암호화폐 시세 정보를 조회하고 사용자에게 표시하는 기능을 추가합니다. BFF(Backend for Frontend) 패턴을 적용하여 서버에서 데이터를 효율적으로 처리하고, React Query를 통해 클라이언트에서 주기적으로 데이터를 업데이트하여 실시간성을 확보했습니다. 이를 통해 사용자들은 최신 시세 정보를 편리하게 확인할 수 있습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
전반적으로 Upbit API 연동 및 실시간 시세 조회 기능 구현을 위한 훌륭한 PR입니다. BFF 패턴을 도입하여 서버와 클라이언트의 역할을 명확히 분리하고, React Query를 활용해 효율적인 데이터 페칭 및 상태 관리를 구현한 점이 인상적입니다. Feature-Sliced Design에 따라 프로젝트 구조를 체계적으로 구성한 것도 유지보수성을 높이는 좋은 시도입니다.
몇 가지 개선점을 제안드립니다.
- BFF 핸들러에서 마켓 정보와 시세 정보를 병합하는 로직의 시간 복잡도를 개선할 수 있습니다. 현재 O(N*M) 구조를 O(N+M)으로 최적화하면 더 효율적인 데이터 처리가 가능합니다.
- API 응답을 위한 공통 타입 정의가 실제 응답과 일치하지 않는 부분이 있어 수정이 필요합니다.
- 코드에 남아있는 주석 처리된 코드 블록들은 정리하는 것이 좋겠습니다.
자세한 내용은 각 파일에 남긴 리뷰 코멘트를 참고해주세요. PR 설명에 언급된 대로, 추후 Polling 방식을 WebSocket으로 전환하면 더욱 실시간에 가까운 사용자 경험을 제공하고 서버 부하도 줄일 수 있을 것입니다. 멋진 작업입니다!
| const KRWmarketList = marketData | ||
| .filter((item) => item.market.startsWith("KRW-")) | ||
| .map((item) => ({ | ||
| market: item.market, | ||
| korean_name: item.korean_name, | ||
| })); | ||
| // const marketName = KRWmarketList.map((item) => item.korean_name); | ||
|
|
||
| // const marketList = KRWMarkets.map((item) => item.market, item.korean_name); | ||
|
|
||
| const tickerData = await tickerAPI(KRWmarketList.map((item) => item.market)); | ||
| // const tickerData = await tickerAPI(["KRW-WAXP"]); | ||
|
|
||
| const result = tickerData.map((ticker) => { | ||
| const marketInfo = KRWmarketList.find((m) => m.market === ticker.market); | ||
|
|
||
| return { | ||
| market: ticker.market, | ||
| koreanName: marketInfo?.korean_name || "", | ||
| tradePrice: ticker.trade_price, | ||
| changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리 | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 구현은 tickerData.map 내부에서 KRWmarketList.find를 사용하여 O(N*M)의 시간 복잡도를 가집니다. KRW 마켓의 수가 200개가 넘어가면 서버 측에서 성능 저하를 유발할 수 있습니다.
KRWmarketList를 Map으로 미리 변환하여 조회하면, 시간 복잡도를 O(N+M)으로 개선하여 성능을 크게 향상시킬 수 있습니다. 또한, 불필요한 API 호출을 막기 위해 KRW 마켓 코드가 없을 경우 조기 반환하는 로직을 추가하는 것이 좋습니다.
const krwMarketMap = new Map<string, { korean_name: string }>();
marketData.forEach((item) => {
if (item.market.startsWith("KRW-")) {
krwMarketMap.set(item.market, { korean_name: item.korean_name });
}
});
const krwMarketCodes = Array.from(krwMarketMap.keys());
if (krwMarketCodes.length === 0) {
return [];
}
const tickerData = await tickerAPI(krwMarketCodes);
const result = tickerData.map((ticker) => {
const marketInfo = krwMarketMap.get(ticker.market);
return {
market: ticker.market,
koreanName: marketInfo?.korean_name || "",
tradePrice: ticker.trade_price,
changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리
};
});| export type APIResponse<T> = { | ||
| status: "success" | "error"; | ||
| data: T; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APIResponse<T> 타입이 API 에러 케이스를 정확하게 반영하지 못하고 있습니다. src/app/api/market/route.ts의 핸들러는 요청 실패 시 status: "error"와 함께 error 프로퍼티를 포함하는 객체를 반환합니다. 현재 타입 정의에는 이 error 필드가 누락되어 있어 클라이언트에서 타입 에러가 발생할 수 있습니다.
| export type APIResponse<T> = { | |
| status: "success" | "error"; | |
| data: T; | |
| }; | |
| export type APIResponse<T> = { | |
| status: "success" | "error"; | |
| data: T; | |
| error?: string; | |
| }; |
| } catch (error) { | ||
| console.error("Market API error:", error); | ||
| return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catch 블록에서 error를 직접 로깅하고 있습니다. error는 unknown 타입일 수 있으므로, Error 인스턴스인지 확인하여 더 구체적인 오류 메시지를 로깅하면 프로덕션 환경에서 디버깅할 때 더 유용합니다.
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Market API error:", errorMessage, error);
return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 });
}
src/features/home/apis/market.api.ts
Outdated
| // type MarketApiResponse = { | ||
| // success: boolean; | ||
| // data: MarketInfoType[]; | ||
| // error?: string; | ||
| // }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the coin info table from server-side rendering to client-side rendering with React Query, introducing real-time data polling and improved data management.
- Introduces React Query for client-side data fetching with 10-second polling
- Creates a new API route (
/api/market) that aggregates market and ticker data - Refactors
CoinInfoTablefrom async server component to client component with hooks
Reviewed Changes
Copilot reviewed 28 out of 31 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/types/api.type.ts | Adds generic APIResponse type for consistent API responses |
| src/shared/provider/* | Creates QueryProvider and AppProvider for React Query setup |
| src/shared/libs/query-client.ts | Instantiates QueryClient for React Query |
| src/features/home/hooks/useGetMarketInfo.ts | Custom hook for fetching market info with 10-second polling |
| src/features/home/components/features/coin-info/CoinInfoTable.tsx | Refactored to client component using React Query hook |
| src/features/home/apis/market.api.ts | New API client for fetching market data from internal API |
| src/entities/market/handler/market-info.handler.ts | Server-side handler combining market list and ticker data |
| src/entities/market/model/apis/* | API functions for external Upbit API calls |
| src/app/api/market/route.ts | Next.js API route exposing market info endpoint |
| src/app/layout.tsx | Wraps app with AppProvider for React Query context |
| src/app/globals.css | Adds new color variables for increase/decrease indicators |
| package.json & pnpm-lock.yaml | Adds React Query dependencies |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/shared/libs/query-client.ts
Outdated
| @@ -0,0 +1,3 @@ | |||
| import { QueryClient } from "@tanstack/react-query"; | |||
|
|
|||
| export const queryClient = new QueryClient(); | |||
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The QueryClient is instantiated without any default configuration options. In production applications, it's recommended to configure error handling, retry logic, and cache behavior. Consider adding default options like defaultOptions: { queries: { retry: 1, staleTime: 5000 } }.
| export const queryClient = new QueryClient(); | |
| export const queryClient = new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| retry: 1, | |
| staleTime: 5000, | |
| }, | |
| }, | |
| }); |
| return NextResponse.json({ status: "success", data }); | ||
| } catch (error) { | ||
| console.error("Market API error:", error); | ||
| return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 }); |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error response structure is inconsistent with the APIResponse<T> type definition which only has status and data fields. The response returns an error field that doesn't exist in the type. Consider updating the type to include an optional error field or adjust the response structure to match the defined type.
| return NextResponse.json({ status: "error", error: "Failed to fetch market data" }, { status: 500 }); | |
| return NextResponse.json({ status: "error", data: "Failed to fetch market data" }, { status: 500 }); |
| import { useGetMarketInfo } from "../../../hooks"; | ||
|
|
||
| export const CoinInfoTable = () => { | ||
| const { data: marketInfoData } = useGetMarketInfo(); |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The component doesn't handle loading and error states from the React Query hook. When marketInfoData is undefined during loading or after an error, the table will render empty. Consider destructuring isLoading and error from the hook and displaying appropriate UI feedback for these states.
| market: ticker.market, | ||
| koreanName: marketInfo?.korean_name || "", | ||
| tradePrice: ticker.trade_price, | ||
| changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리 |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using parseFloat after toFixed is redundant since toFixed returns a string. The value is then parsed back to a number and later used with .toFixed(2) again in the component. Consider returning the number directly as ticker.signed_change_rate * 100 and let the component handle formatting, avoiding unnecessary precision loss.
| changeRate: parseFloat((ticker.signed_change_rate * 100).toFixed(2)), // 퍼센트로 변환 & 소수 둘째자리 | |
| changeRate: ticker.signed_change_rate * 100, // 퍼센트로 변환, 소수 둘째자리 포맷은 컴포넌트에서 처리 |
📝 요약 (Summary)
Upbit API를 활용하여 KRW 마켓의 실시간 시세 정보를 조회하고 표시하는 기능을 구현했습니다. BFF(Backend for Frontend) 패턴을 적용하여 서버 사이드에서 데이터를 가공하고, React Query를 통해 클라이언트에서 1초 간격으로 폴링하여 실시간 업데이트를 제공합니다.
✅ 주요 변경 사항 (Key Changes)
/market/all)과 실시간 시세(/ticker) API 통합💻 상세 구현 내용 (Implementation Details)
1. Upbit API 통합 (
src/entities/market/model/apis/)market-all.api.ts/market/allAPI 호출하여 전체 마켓 목록 조회MarketAllItem(market, korean_name, english_name 등)ticker.api.ts/tickerAPI 호출하여 실시간 시세 데이터 조회TickerResponse(52개 필드 포함)markets쿼리 파라미터)2. BFF Layer - Market Info Handler (
src/entities/market/handler/)market-info.handler.ts서버 사이드에서 데이터 집계 및 가공 로직:
market.startsWith("KRW-"))signed_change_rate→ 퍼센트 변환 (소수점 둘째자리)korean_name→ camelCase 변환3. API Route Handler (
src/app/api/market/route.ts)Next.js Route Handler를 통한 RESTful API 엔드포인트 제공:
응답 포맷:
{ "status": "success", "data": [ { "market": "KRW-BTC", "koreanName": "비트코인", "tradePrice": 123000000, "changeRate": -1.10 } ] }4. React Query 통합 (
src/features/home/)Client API Wrapper (
apis/market.api.ts)Custom Hook (
hooks/useGetMarketInfo.ts)React Query Provider (
src/shared/provider/)5. UI 컴포넌트 (
src/features/home/components/features/coin-info/)CoinInfoTable.tsx실시간 시세 테이블 렌더링:
주요 기능:
6. 타입 정의 및 계약
src/entities/market/model/types/market-info.type.tssrc/shared/types/api.type.ts7. 프로젝트 구조 (Feature-Sliced Design)
🚀 트러블 슈팅 (Trouble Shooting)
1. React Server Components 직렬화 에러
문제:
원인:
QueryClient인스턴스(클래스 객체)를 모듈 레벨에서 생성 후 서버 컴포넌트에서 클라이언트 컴포넌트로 전달하려다 발생해결:
QueryProvider컴포넌트에"use client"지시어 추가QueryClient인스턴스를 전역이 아닌 컴포넌트 내부에서 생성하도록 변경 (초기 시도)queryClient를 사용하되, Provider 자체를 Client Component로 명시2. API 응답 타입 불일치
문제:
{ status, data }형태로 반환data필드 접근 필요해결:
3. 캐싱 전략
문제:
해결:
성능 최적화 고려사항
폴링 간격: 현재 10초로 설정되어 있어 API 호출 빈도가 높음
메모리 누수 방지: React Query는 자동으로 처리하지만, 컴포넌트 언마운트 시 폴링 중단 확인 필요
추후 개선 사항
WebSocket 전환
에러 핸들링 강화
페이지네이션/가상화
react-virtual등을 활용한 가상 스크롤 도입 검토검색/필터 기능
24시간 등락률 추가
change_rate_24h계산 로직 추가 필요 (candles API 활용)타입 안정성 더 강화
📸 스크린샷 (Screenshots)
실시간 시세 테이블
표시 정보:
주요 기능:
#️⃣ 관련 이슈 (Related Issues)
🔍 리뷰 포인트 (Review Points)