명령형 API

스크롤과 배율, 전체 보기 줌, 상세 패널, 작업 생성을 앱의 컨트롤에서 직접 호출해요.

차트는 타임라인을 그리고, 작업 목록은 켜져 있을 때 그려요. 툴바와 배율 선택기는 그리지 않으니 앱에서 만들고 ref로 차트를 호출해요.

툴바 만들기

ReactGanttChart는 forwardRef로 감싸져 있어요. useRef<GanttHandle>를 연결하면 차트가 마운트된 뒤 핸들에 메서드 9개가 담겨요.

GanttToolbar.tsx
import { useRef, useState } from 'react';
import {
  ReactGanttChart,
  type GanttHandle,
  type GanttScaleKey,
  type Task,
} from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';

const SCALES: GanttScaleKey[] = ['day', 'week', 'month', 'quarter', 'year'];

const initialTasks: Task[] = [
  { id: 'design', name: 'Design', parentId: null, sequence: '1',
    startDate: '2026-09-01', endDate: '2026-09-08' },
  { id: 'build', name: 'Build', parentId: null, sequence: '2',
    startDate: '2026-09-08', endDate: '2026-09-18' },
];

export function GanttToolbar() {
  const ganttRef = useRef<GanttHandle>(null);
  const [tasks, setTasks] = useState(initialTasks);
  const [scale, setScale] = useState<GanttScaleKey>('month');

  return (
    <>
      <div>
        <button onClick={() => ganttRef.current?.scrollToToday()}>Today</button>
        <button onClick={() => ganttRef.current?.zoomToFit()}>Fit</button>
        <button onClick={() => ganttRef.current?.openDetail('build')}>Build details</button>

        <select
          aria-label="Timeline scale"
          value={scale}
          onChange={(e) => ganttRef.current?.setScale(e.target.value as GanttScaleKey)}
        >
          {SCALES.map((s) => (
            <option key={s} value={s}>{s}</option>
          ))}
        </select>
      </div>

      <ReactGanttChart
        ref={ganttRef}
        tasks={tasks}
        onTasksChange={setTasks}
        defaultScale="month"
        onScaleChange={setScale}
        showDetail
        height={420}
      />
    </>
  );
}

차트가 마운트되기 전까지 ref는 null이라 호출마다 옵셔널 체이닝을 써요. 버튼은 상태를 갖지 않아요. 메서드가 호출되는 순간의 차트 상태를 읽기 때문이에요. 타입 선언은 GanttHandle에 있어요.

타임라인 스크롤하기

scrollToDate와 scrollToToday는 가로축만 움직여요. scrollToTask는 두 축을 모두 움직이고, 행이 화면 밖에 있을 때만 세로로 행을 가운데에 놓아요.

ganttRef.current?.scrollToDate('2026-09-01', { smooth: false, align: 'start' });
ganttRef.current?.scrollToTask('build');

스크롤 메서드는 모두 같은 옵션을 받아요. smooth는 리터럴 false를 넘길 때만 애니메이션을 꺼요. align은 리터럴 'start'를 넘길 때만 타임라인 영역의 왼쪽 끝에 맞추고, 그 외에는 가운데에 맞춰요.

가운데에 맞출 때는 뷰포트 너비에서 고정된 작업 목록 패널을 뺀 영역을 기준으로 재요. scrollToTask는 가로로 align: 'start'면 막대의 왼쪽 끝을, 그 외에는 막대의 중간을 맞춰요. align은 세로축에 영향을 주지 않고, 두 경우 모두 목표 위치가 0보다 작아지지 않아요.

스크롤 메서드는 모두 예외도 콘솔 출력도 없이 반환해요. 렌더링 범위 밖의 날짜나 알 수 없는 작업 id로, 또는 마운트되기 전에 호출해도 안전해요.

scrollToTask는 렌더링된 행에서만 찾아서, 접힌 부모 아래의 작업은 펼치기 전까지 닿지 않아요.

첫 스크롤에는 ref가 필요 없어요. initialScrollTo에 "today"나 날짜 문자열을 넘기면 첫 렌더링 뒤에 그 위치로 한 번 이동하고, 이후 데이터가 바뀌어도 스크롤은 그대로예요.

배율 선택기 동기화하기

setScale은 타임라인 가운데 날짜를 그대로 두고 배율을 바꿔요. onScaleChange는 확정된 변경을 다시 알려줘요. 위 예제처럼 네이티브 <select>를 둘에 연결하면 select는 자체 상태가 필요 없어요.

휠 줌과 키보드 단축키, zoomToFit()도 모두 onScaleChange로 알려주니 select가 추가 코드 없이 따라와요. option 텍스트는 앱이 직접 쓰니 라벨도 나머지 화면과 함께 번역해요. 자세한 내용은 로케일과 날짜 형식에서 확인해요.

프로젝트 전체 보기

zoomToFit()은 렌더링된 작업에서 가장 이른 시작일과 가장 늦은 종료일을 읽어요. 배율 5개를 촘촘한 쪽부터 시도해서 구간 전체가 타임라인 너비에 들어가는 첫 배율로 바꾸고, 어느 배율에도 들어가지 않으면 year를 써요. 가장 이른 날짜는 왼쪽 끝에 고정해요.

들어가는지는 배율별 평균 밀리초당 픽셀로 재요. 눈금 길이가 고정된 배율에서는 이 값이 정확해요. quarter와 year는 달력 길이를 따르는 셀이라, 구간 길이가 뷰포트 너비와 몇 퍼센트 안쪽으로 차이 나면 옆 배율이 골라질 수 있어요.

zoomToFit()은 옵션을 받지 않고 스크롤은 항상 즉시 일어나요. 작업이 없거나 차트가 마운트되지 않았거나 쓸 수 있는 시작일과 종료일이 없으면 오류 없이 반환해요.

zoomIn과 zoomOut은 없어요. 배율 키 5개를 담은 배열을 직접 만들어 setScale로 한 단계씩 옮기거나, 타임라인에서 설명하는 휠과 키보드 단축키에 맡겨요.

패널 열고 작업 추가하기

openDetail(taskId)는 옆에 붙은 상세 패널을 그 작업으로 열고 closeDetail()은 닫아요. 패널이 꺼져 있으면 둘 다 아무 일도 하지 않으니 버튼에 가드를 두지 않아도 돼요.

openDetail은 접힌 행까지 포함해 차트가 가진 모든 작업에서 id를 찾아요. 이미 닫힌 패널에 closeDetail()을 호출하면 onDetailChange도 발생하지 않아요. detailTaskId를 넘기면 차트는 제어 모드가 되고, 두 메서드는 요청만 알리고 패널은 앱 상태를 따라가요. 자세한 내용은 상세 패널에서 확인해요.

addTask()는 화면이 아니라 데이터에 관한 유일한 멤버예요. Add task 버튼과 같은 초안을 만들어 onTaskCreate에 넘겨요. 초안은 오늘 위치의 눈금 하나예요. 아무것도 쓰지 않으니 행은 핸들러가 그 작업을 tasks에 넣을 때 생겨요.

onTaskCreate가 없거나 생성이 꺼져 있으면 addTask()도 아무 일도 하지 않아요. 이 버튼에도 가드는 필요 없어요. 자세한 내용은 작업 생성에서 확인해요.

적용 전에 확인하기

  • addTask()는 초안을 onTaskCreate에 넘길 뿐 아무것도 쓰지 않아요. 작업을 수정하거나 옮기거나 지우는 메서드는 없어서 모든 변경은 tasks와 onTasksChange를 지나요.
  • getScrollElement()는 스크롤 노드를 그대로 반환하니 앱에서 직접 다뤄요. scrollToRow도 스크롤 위치 getter도 스크롤 이벤트 콜백도 없어요.
  • 어떤 메서드도 실패를 알리지 않아요. 범위 밖 날짜, 알 수 없는 id나 배율 키, 패널이 없는 차트에서 부른 상세 호출은 밖에서 보면 성공과 같아요.
  • setScale에는 짝이 되는 getter가 없어서 현재 배율은 앱 상태에 있거나 어디에도 없어요.

prop과 기본값은 GanttProps를 참고해요.