Shioaji Pro
에이전트를 위한 거래 터미널: 코딩 에이전트가 전부 구축, UI 100% 오픈소스, 데스크톱에 AI 에이전트와 전문 주문 패널 내장
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
업비트 공개 API로 암호화폐 실시간 시세를 보여주는 웹앱입니다. 안내 — UI 레이아웃은 토스증권 데스크톱 화면을 참고한 학습용 클론입니다. 토스증권과는 무관하며, 시세 데이터는 업비트 공개 API(공포·탐욕 지수만 alternative.me)에서 가져옵니다. 1. 프로젝트 개요 2. 주요 기능 3. 기술 스택과 선택 이유 4. 아키텍처 • 데이터 공급과 소비의 분리 • 소켓 구성 • API 프록시 • 폴더 구조 5. 성능 최적화 • 배운 점 6. 접근성 7. 실행 방법 • 업비트 KRW 마켓의 코인 목록과 시세를 REST로 한 번 받아 오고, 이후 WebSocket으로 실시간 갱신합니다. • 퍼블리셔에서 프론트엔드 개발자로 전환하는 과정에서 만든 개인 포트폴리오입니다. • 데스크톱 전용(최소 1280px)이며 모바일 대응은 하지 않았습니다. • KRW 마켓 전체 시세 테이블 — 현재가 · 등락률 · 고가 · 저가 · 일중 위치 · 거래대금 · 거래량 • 등락률이 바뀌면 셀 배경이 상승/하락색으로 깜빡임 (가격엔 색을 입히지 않고 등락률에만 — 토스 규칙) • 상단 시장 요약 — 대표 코인 스파크라인, 상승/하락 종목 수, 공포·탐욕 지수README 전문 보기 ↗
업비트 공개 API로 암호화폐 실시간 시세를 보여주는 웹앱입니다.
안내 — UI 레이아웃은 토스증권 데스크톱 화면을 참고한 학습용 클론입니다. 토스증권과는 무관하며, 시세 데이터는 업비트 공개 API(공포·탐욕 지수만 alternative.me)에서 가져옵니다.
홈
검색
/ 키 또는 GNB 버튼으로 여는 검색 팝업관심 · 최근 본
코인 상세
공통
| 분류 | 사용 기술 |
|---|---|
| 빌드 | Vite 8 |
| UI | React 19, TypeScript 6 |
| 라우팅 | react-router-dom 7 (선언형 라우팅) |
| 전역 상태 | Zustand 5 |
| 스타일 | CSS Modules + CSS 변수 디자인 토큰 |
| 차트 | lightweight-charts 5 |
| 폰트 | Pretendard (npm 패키지로 자체 호스팅) |
| 포맷터 | Prettier (홑따옴표·세미콜론 없음·4칸) |
Next.js가 아닌 Vite 실시간 시세는 브라우저가 WebSocket으로 받아 그리는 클라이언트 중심 앱이라 SSR이 필요하지 않았습니다. 설정이 가벼운 Vite SPA로 충분하다고 판단했습니다.
Context가 아닌 Zustand Context는 값이 바뀌면 그 Context를 구독하는 컴포넌트가 전부 리렌더됩니다. 시세는 초당 수십 번 갱신되기 때문에, 컴포넌트가 필요한 값만 골라 구독하는 선택적 구독이 필요했습니다.
// 각 행은 자기 코인의 시세만 구독
const live = useMarketStore((s) => s.liveMap[market])
Tailwind가 아닌 CSS Modules
퍼블리셔로 쌓아 온 CSS 설계 역량을 드러내고 싶었습니다. 색상·간격 등은 tokens.css의 CSS 변수로 관리하고, 다크/라이트 테마는 data-theme 속성으로 토큰 값을 바꿉니다.
AppLayout
└─ useMarketFeed() ← 앱 전체에서 한 번만 호출
├─ REST 스냅샷 (1회) : 목록 · 순서 · 한글명
└─ 스트림 ① ticker : KRW 전체 시세, 200ms 배칭
│
▼
Zustand 스토어 (market)
│ 읽기만 함
┌────────┬─────────┬──────────────┬─────────────────┬─────────────┐
Home MarketRow TickerBar MarketSummary 관심·최근 사이드바 검색 팝업
CoinDetail (/coins/:market) ← 상세 페이지에 있는 동안만
├─ 스토어 liveMap[market] → 스트림 ①에서 이 코인만 구독
└─ useUpbitTrades(market) → REST 최근 100건 + 스트림 ② trade : 체결 내역 (200ms 배칭)
AppLayout에서 useMarketFeed()를 한 번 호출합니다. 이 훅이 REST 스냅샷을 1회 받고 스트림 ①을 구독해 스토어에 기록합니다.초기에는 시세가 필요한 컴포넌트마다 각자 WebSocket을 열어서, 같은 데이터를 받는 소켓이 3개 동시에 떠 있었습니다. 그래서 데이터를 받아 오는 곳(공급) 과 그리는 곳(소비) 을 나눠, 시세는 스트림 ① 하나를 스토어로 공유하도록 통합했습니다.
| 스트림 | 대상 | 반영 방식 | 수명 |
|---|---|---|---|
| ① ticker | KRW 전체 | 200ms 배칭 → 스토어 (상세 페이지도 여기서 자기 코인만 구독) | 앱 전체 |
| ② trade | 상세 코인 체결 | 200ms 배칭 → 최근 100건, 페이지 로컬 상태 | 상세 페이지 |
물리적 연결은 1개입니다. 업비트는 웹사이트 Origin으로 들어온 브라우저 소켓을 동시에 1개 정도만 허용해서(localhost는 예외라 배포 후에야 드러남), 스트림마다 소켓을 열면 상세 페이지에서 연결 직후 끊겼습니다. 그래서 upbitSocket.ts가 연결 하나를 공유하고,
type·code가 맞는 구독자에게만 전달합니다브라우저에서 업비트 REST를 직접 호출하면 요청에 Origin 헤더가 붙습니다. 업비트는 Origin이 있는 요청을 별도 그룹으로 분류해 초당 약 1회로 제한하는데, 코인 목록(200개 이상)과 실시간 시세를 동시에 받아야 하는 이 앱에서는 한계가 금방 옵니다. api/upbit.ts(Vercel 서버 함수)를 프록시로 두어 서버에서 Origin 없이 업비트를 호출하고 응답을 그대로 넘기는 방식으로 이를 우회했습니다.
브라우저 → /upbit/:path → api/upbit.ts → api.upbit.com/v1/:path
(Origin 없음)
설계에서 신경 쓴 세 가지입니다.
열린 프록시 방지
경로를 그대로 포워딩하면 누구나 업비트의 임의 엔드포인트를 이 서버를 통해 호출할 수 있습니다. ALLOWED 정규식으로 이 앱이 실제로 쓰는 경로만 허용하고, 그 외 요청은 400으로 거부합니다.
const ALLOWED = /^(market\/all|ticker|trades\/ticks|candles\/(minutes\/(1|15|60)|days|weeks|months))$/
엔드포인트별 엣지 캐시 분리 모든 응답에 같은 캐시 정책을 적용하면 시세가 낡거나 마켓 목록이 과도하게 재요청됩니다. 변경 주기가 다른 엔드포인트를 다르게 처리했습니다.
| 엔드포인트 | s-maxage | 이유 |
|---|---|---|
market/all | 3600s | 마켓 목록은 하루에도 거의 안 바뀜 |
| 시세 · 캔들 | 1s | 실시간성이 중요하므로 거의 무효화 |
여기까지가 README 앞부분입니다. 나머지는 원문에서 읽을 수 있습니다. README 전문 보기 ↗