Skip to content

Documentation / @finchart/react

@finchart/react

@finchart/core 위의 React 컴포넌트.

설치

아직 npm에 발행 전이다 — 스코프 선점 전이고 버전은 0.0.1이다. 지금 써 보려면 저장소를 클론해 pnpm buildpnpm pack으로 tarball을 만들어 설치한다(절차는 저장소 루트 README의 "시작하기"에 있다). 아래는 발행 뒤의 모습이다.

sh
pnpm add @finchart/react @finchart/core @finchart/dom react react-dom

@finchart/core·@finchart/dom은 peer다 — 함께 설치한다. 아래 60초 예제가 둘에서 직접 import하므로 소비자의 직접 의존이다.

60초 — 실서비스 차트 하나

캔들 + 거래량 pane + 이동평균 + 십자선·툴팁·레전드 + 실시간 + 반응형. 훅 0개, 배선 한 줄:

tsx
import { histogramSeries, timeTicks } from "@finchart/core";
import type { HistogramPoint, LineDataPoint, OHLC } from "@finchart/core";
import { browserDeps } from "@finchart/dom";
import {
  ChartCandles, ChartContainer, ChartLine, ChartPane, ChartSeries,
  Crosshair, Legend, Tooltip, XAxis, YAxis,
} from "@finchart/react";

declare const bars: OHLC[];              // 네가 들고 오는 것
declare const volume: HistogramPoint[];

const deps = browserDeps({ autoSize: true });   // 배선은 명시가 계약이다

// 원본 전체에서 그릴 점을 만든다. 앞 19개가 null인 것이 절반이다 —
// 점을 빼면 선이 구멍을 가로질러 이어진다.
const sma20 = (source: OHLC[]): LineDataPoint[] =>
  source.map((bar, i) => ({
    x: bar.x,
    y: i < 19
      ? null
      : source.slice(i - 19, i + 1).reduce((sum, b) => sum + b.close, 0) / 20,
  }));

<ChartContainer deps={deps} data={bars} style={{ height: 480 }}>
  <XAxis ticks={timeTicks()} />
  <YAxis position="right" />

  <Crosshair magnet />
  <Tooltip />
  <Legend />

  <ChartPane>
    <ChartCandles name="가격" />
    <ChartLine derive={sma20} deriveKey={[20]} color="#f59e0b" width={1.5} pointRadius={0} />
  </ChartPane>

  <ChartPane flex={0.25} minHeight={48}>
    <ChartSeries series={histogramSeries()} data={volume} name="거래량" />
  </ChartPane>
</ChartContainer>

선이 그릴 점을 얻는 길은 셋이다data(이미 배열이 있다), derive+deriveKey(원본에서 만든다, 위 예제), input(남이 만든 계산 노드를 잇는다). 실전에서는 셋째가 흔하다: @finchart/indicatorsmovingAverage(source, { period: 20 }).out.ma가 정확히 input의 모양이고, 그러면 밴드와 중심선이 한 계산을 나눠 먹는다.

실시간은 data에 새 배열을 주면 끝이다 — 명령도 ref도 없다(뷰포트 유지·새 봉 따라가기 의미는 명령형과 같고, 100k 봉에서 비용 차가 잡음 밑이다). pane 높이는 flex가 정한다 — 픽셀 산술을 소비자가 하지 않는다.

이 예제는 기계가 지킨다: 같은 코드가 src/__tests__/minimal-service.types.tsx에서 컴파일되고, apps/examplesminimal.html이 브라우저에서 그대로 돈다. 한 곳만 다르다 — 픽스처는 패키지 이라 '../components'에서 가져온다.

전에는 sma20이 픽스처에서 declare const ma로 지워져 있었다. JSX는 지켜졌지만 소비자가 막힌 자리는 정확히 "그래서 ma를 어디서 얻나"였다 (36차 콜드 컨슈머). declare가 지우는 자리가 문서의 구멍이다 — 데이터는 소비자가 들고 오는 것이라 지워도 되지만, 파생은 예제가 가르쳐야 하는 지식이다.

알아야 하는 것 — 개념 넷, 차선 둘

개념무엇
컨테이너무대 하나. 데이터·크기·상태의 문<ChartContainer>
pane값 축을 공유하는 영역. 높이는 flex<ChartPane>
시리즈그리는 것<ChartCandles>·<ChartLine>·<ChartSeries>
동승자무대에 얹히는 것 — 축·도구·장식<XAxis>·<Crosshair>·<Tooltip>·<PriceLine>

차선 둘 — 무대 위에 존재하는 것은 자식으로, 명령은 훅으로:

언제
usePlugin플러그인을 설치·해제한다 (drawingTools·paneMaximize·attach*)
usePluginState플러그인의 상태를 React 상태로 구독한다 (도구 모드·선택)
useChartPlot컨테이너 에서 무대에 명령한다 (옵션·설치)

plotRef는 이벤트 핸들러(fitDomains() 버튼), onPlot은 컨테이너 사이의 배선(<SyncX>) — 아래 "컨테이너 사이" 참고.

배선은 명시가 계약이다

deps필수다. 그 한 줄이 번들에 무엇이 들어오는지를 정한다 — 배선에서 빼면 코드도 안 들어온다 (PRINCIPLES 21). 편의를 위해 기본값을 두면 이 패키지가 browserDeps를 정적으로 import하게 되고, 그러면 lean하게 배선한 소비자·headless·worker까지 브라우저 셸 전체를 싣는다 (실측 +21.6KB raw / +6.5KB gzip — 34차에 넣었다가 같은 날 되돌린 이유다).

tsx
// 봉 번호 x축 + 관성 스크롤이 필요하면 그것도 여기서 고른다
const deps = browserDeps({ autoSize: true, createXMapping: barIndexX });

보는 상태는 state/onStateChange로 밖에 둘 수 있다(URL·undo·동기화).

컴포넌트는 DOM을 렌더하지 않는다

DOM은 <ChartContainer>의 div 하나뿐이고, 나머지 컴포넌트는 마운트가 등록, 언마운트가 해제, prop 변경이 반영이다 — 캔버스 시리즈는 DOM 요소가 아니므로 DevTools에 요소가 없고, CSS 선택자로 시리즈 하나를 집을 수 없다. 그래서 테마(모든 라인의 기본색)는 CSS 변수로, "이 지표는 주황" 같은 개별 색은 color prop으로 준다. 시리즈의 신원은 React의 key 판정 그대로다 — 같은 자리면 같은 시리즈다.

이름 규칙: Chart* 접두는 구조와 시리즈(ChartContainer·ChartPane· ChartLine…), 무접두는 동승자(XAxis·Crosshair·Tooltip·PriceLine…).

장식(PriceLine·Markers·Watermark·Span)은 값이 같으면 다시 세우지 않는다 — 인라인으로 써도 된다. Crosshair·Tooltip·Legend도 인라인이어도 된다(그쪽은 applyOptions로 옵션만 다시 넣는다).

51차 전까지 여기 *"참조가 바뀌면 떼었다 붙이므로 인라인 객체는 밖으로 빼거나 useMemo로 고정한다"*고 적혀 있었다. 처방이 성립하지 않았다 — 고정할 대상이 소비자가 만드는 객체가 아니라 JSX가 렌더마다 새로 만드는 props 객체라, 소비자 쪽에 손잡이가 없었다. 계약을 소비자에게 넘기는 대신 컴포넌트가 값으로 비교하게 고쳤다.

중첩 객체(PriceLinestyle)도 값으로 본다 — 한 겹 더 들어간다.

예외가 둘 남는다.

  • Markersitems: 배열은 원소 신원까지 본다. 안정된 데이터에서 만들면 그대로 걸리지만, 매 렌더 items의 원소를 새로 지으면 useMemo가 필요하다 — 깊은 비교는 큰 목록에서 그리기보다 비싸진다.
  • 함수 prop(PriceLineformat): 두 클로저가 같은 일을 하는지는 판정할 수 없어 신원으로 떨어진다. 인라인 format={(v) => …}은 렌더마다 새 함수라 그때마다 다시 세우므로, useCallback이나 모듈 상수로 고정한다.

두 차선 — 선언과 명령

무대 위에 존재하는 것은 자식으로, 명령은 훅으로 (28차):

  • 선언 차선 — 시리즈·pane·장식·도구 설치처럼 "있다/없다"가 의미인 것은 컴포넌트다. 조건부·목록·순서·신원을 React가 판정한다.
  • 명령 차선 — 플러그인의 api 핸들, fitDomains() 같은 한 번의 명령, 구독은 훅과 ref다: usePlugin(설치·해제 수명), useChartPlot(컨테이너 안의 설정), plotRef(이벤트 핸들러).
tsx
const tools = usePlugin((plot, pane) => pane.use(drawingTools({ plot })), []);
// tools?.begin("trend") — api는 커밋 뒤에 온다

한 pane의 주인은 하나다. <ChartSeries>가 하나라도 있는 pane의 시리즈 목록은 선언이 소유한다 — 거기에 addSeriesattach* 지표(내부가 addSeries다)를 명령형으로 얹으면 다음 리렌더에서 조용히 사라진다. pane 전체를 명령형으로 쓰는 것은 된다(시리즈 컴포넌트를 안 두면 목록을 건드리지 않는다). 자기 pane을 만드는 attach*(RSI·MACD류)는 <ChartPane> 과 섞으면 state 저장·복원의 pane 인덱스가 어긋날 수 있다 — 상태 왕복을 쓰는 화면에서는 pane 구조를 한쪽 차선으로 통일한다.

컨테이너 사이: onPlot<SyncX>

컨테이너 의 어휘는 무대 하나를 전제한다. 무대 둘 이상을 잇는 배선(심볼 비교의 x 동기 등)은 밖에 살고, 재료는 ref가 아니라 상태다: plotRef는 effect를 못 깨우므로 차트가 서고 지는 것에 반응하지 못한다. onPlot으로 무대를 상태에 올리고, 고정 길이 배열(빈 자리 null)로 <SyncX>에 준다:

tsx
const [a, setA] = useState<Plot | null>(null);
const [b, setB] = useState<Plot | null>(null);
<ChartContainer onPlot={setA} …/>
<ChartContainer onPlot={setB} …/>
<SyncX plots={[a, b]} />

역할 구분 — plotRef는 이벤트 핸들러(누를 때는 무대가 정착해 있다), onPlot은 배선(무대의 등장·퇴장에 반응해야 한다). onPlot 콜백은 참조가 안정해야 한다(useState setter면 충분하다).

명령형 설정: useChartPlot

무대에 도구를 설치하거나 옵션을 얹는 effect는 컨테이너 안의 컴포넌트에서 useChartPlot()으로 한다. 부모의 effect에서 plotRef를 읽으면 key 리마운트(StrictMode 이중 마운트)에서 버려지는 인스턴스를 잡는다 — 계약은 chart-context.ts의 JSDoc을 본다. plotRef는 이벤트 핸들러 (fitDomains() 버튼 같은 것)에는 여전히 맞다.

고급: usePlot

<ChartContainer>가 내부적으로 쓰는 저수준 훅이다. 시리즈 하나를 명령형 손잡이(SeriesHandle)로 직접 갈아 끼우고 싶을 때만 쓴다 — 대부분은 필요 없다. 계약은 use-chart.ts의 JSDoc을 본다.

지원 범위

  • Node 18+ — SSR·worker·테스트가 지나는 headless 경로.
  • 브라우저 — Chrome 98+ · Edge 98+ · Firefox 94+ · Safari 15.4+ (2022-03). 하한을 정하는 것은 structuredClone·Object.hasOwn·Array.prototype.at이고, 폴리필은 싣지 않는다 — 더 낮은 곳을 지원하려면 소비자가 넣는다.
  • 저장소의 개발 환경(Node 24 · pnpm 11)은 이보다 높다. 그것은 기여자의 하한이지 소비자의 것이 아니다.

문서

Interfaces

Type Aliases

Functions