본문으로 건너뛰기

React Gantt의 데이터 바인딩 및 상태 관리

React Gantt는 두 가지 데이터 바인딩 패턴을 지원합니다:

  1. 진실의 원천으로서의 React 상태 - 대부분의 React 앱에 권장됩니다.
  2. Gantt를 진실의 원천으로 삼기 - 특수한 경우에 유용합니다.

두 가지 접근 방식 모두 유효하지만, 예기치 않은 동작을 피하기 위해 하나를 선택하고 일관되게 따라야 합니다.

이 문서는 두 모드를 설명하고 각 모드의 기본 예제를 제공합니다.

아직 기본 차트를 렌더링하지 않았다면 빠른 시작에서 시작하세요.

데이터 모델​

진실의 원천으로서의 React 상태 (권장)​

이 모델에서:

  • tasks, links, resources, resourceAssignments를 React 상태나 상태 관리 라이브러리에 보관합니다
  • 이를 <Gantt>에 props로 전달합니다
  • 사용자가 무언가를 변경하면 ReactGantt가 Ваш data.save 또는 data.batchSave 콜백을 호출합니다
  • React 상태를 업데이트 -> React가 재렌더링 -> ReactGantt가 새 props를 다시 읽습니다

페이지에 Gantt와 동일한 데이터를 보아야 하는 다른 React UI가 있거나, 애플리케이션이 동일한 데이터를 사용하는 추가 컴포넌트나 상태 관리자를 사용하는 경우에 적합합니다.

다만 Gantt의 재해석(re-parsing)이나 재렌더링이 더 잦아질 수 있습니다.

Gantt를 진실의 원천으로 삼기​

이 접근 방식에서 ReactGantt와 백엔드를 데이터의 주 소유자로 다룹니다:

  • ReactGantt는 초기 데이터 집합을 data.load를 통해, props를 통해, 또는 명령형 API 호출을 통해 로드합니다
  • ReactGantt는 사용자 변경을 내부적으로 적용하거나 서버로 전송합니다
  • 당신은 모든 작업/링크의 미러 복사본을 React 상태에 상시로 유지하지 않습니다(프로프스에 계속 피드백되는 방식이 아닙니다)

주된 차이점은 전체 루프의 부재입니다 - 사용자 변경이 React 상태를 업데이트하지 않으며 매 변경 후 업데이트된 props를 재적용하지 않습니다.

이 모델은 데이터 집합이 매우 클 때 유용합니다. Gantt 데이터가 변경될 때 React 상태를 지속적으로 업데이트하는 오버헤드를 줄이고, 대량 배치 작업(예: 자동 일정 수립)을 반복적인 재렌더링 없이 단순화합니다.

다른 한편으로는 Gantt 데이터와 React 상태 간의 직접적 동기화가 끊깁니다. 그리고 React 상태에 작업/링크를 저장하는 경우 Gantt의 내부 상태를 의도치 않게 덮어쓰지 않도록 주의해야 합니다.

진실의 원천으로서의 React 상태​

이 패턴에서는 모든 핵심 컬렉션을 상태로 보유하고 이를 Props(tasks, links, resources, resourceAssignments)로 전달합니다. 사용자가 Gantt 내부에서 작업이나 링크를 수정할 때(예: 작업을 만들거나 삭제), Gantt는 콜백을 트리거합니다. 이 콜백에서 새 데이터로 React 상태를 업데이트합니다. 상태가 업데이트되면 React가 ReactGantt 컴포넌트를 재렌더링하고, 최신 상태로부터 업데이트된 props를 읽게 됩니다.

상태를 정의할 때는 작업에는 SerializedTask, 링크에는 SerializedLink를 사용합니다. 이 타입은 사용자에게 표시되는 데이터 구조를 나타내며, 날짜 필드는 Date | string을 허용하고, 내부에 $ 접두사가 붙은 속성은 없습니다. 데이터 내부에서 작업/링크를 다룰 때는 Gantt 이벤트 핸들러 안에서 Gantt가 이미 데이터를 파싱한 상태이므로 Task와 Link를 사용합니다.

React 상태를 사용한 최소 예제​

import { useState } from 'react';
import Gantt, {
Task,
Link
} from "@dhtmlx/trial-react-gantt";
import "@dhtmlx/trial-react-gantt/dist/react-gantt.css";

import { demoData } from "./demoData";

export default function ReactStateGantt() {
const [tasks, setTasks] = useState<Task[]>(demoData.tasks);
const [links, setLinks] = useState<Link[]>(demoData.links);


return (
<div style={{ height: "100vh" }}>
<Gantt
tasks={tasks}
links={links}
data={{
save: (entity, action, item, id) => {
// 아래 패턴 참조: React 상태를 여기서 업데이트
console.log("Change:", { entity, action, id, item });
},
}}
/>
</div>
);
}

이 예제는 기본 시작점을 제공합니다 - React가 props를 통해 렌더링할 내용을 제어하고, Gantt가 save 콜백을 통해 변경사항을 보고하며, React를 데이터의 권위적 소유자로 만듭니다.

다음 섹션에서는 해당 콜백 안에서 구현하는 일반적인 패턴을 보여줍니다.

data.save로 변경 처리​

data.save를 제공하면 ReactGantt는 사용자가 변경하는 모든 변경마다 이를 호출합니다:

(entity: string, action: string, item: any, id: string|number) => {...}

다음은 간단하게 React 상태를 직접 업데이트하는 예제입니다:

function handleSave(entity, action, item, id) {
if (entity === "task") {
setTasks((prev) => {
if (action === "create") return [...prev, item];
if (action === "update") return prev.map((t) => (t.id === id ? item : t));
if (action === "delete") return prev.filter((t) => t.id !== id);
return prev;
});
}

if (entity === "link") {
setLinks((prev) => {
if (action === "create") return [...prev, item];
if (action === "update") return prev.map((l) => (l.id === id ? item : l));
if (action === "delete") return prev.filter((l) => l.id !== id);
return prev;
});
}

// 필요하면 resources / assignments에 대한 동일 아이디어를 적용
}

실제 애플리케이션에서는 이 로직을 거의 인라인으로 유지하지 않습니다:

  • Redux Toolkit에서는 이것이 리듀서나 썽크가 됩니다
  • Zustand/Jotai/MobX/Valtio에서는 스토어에 저장됩니다
  • 서버 통합을 위해 여기서 API를 호출할 수도 있습니다

상태 관리 튜토리얼은 이 패턴에 기반해 구성되며, 이 예제는 시작점을 보여주는 것일 뿐입니다.

data.batchSave를 통한 대량 업데이트​

data.save가 사용자 변경을 가장 편리하게 캡처하는 진입점이지만 한 가지 단점이 있습니다 - 대규모 프로젝트에서 많은 작업을 드래그로 이동시키는 등 자동 일정 수립과 같은 대량 변경이 수백 또는 수천 건의 변경을 생성할 수 있습니다.

이 경우 애플리케이션에서 그럴 가능성이 있다면 data.save 대신 data.batchSave를 제공하여 배치 모드로 전환할 수 있습니다. 이 모드에서 ReactGantt는 묶인 변경 사항을 제공합니다:

type GanttBatchChanges = {
tasks?: Array<DataCallbackChange<Task>>;
links?: Array<DataCallbackChange<Link>>;
resources?: Array<DataCallbackChange<Resource>>;
resourceAssignments?: Array<DataCallbackChange<ResourceAssignment>>;
};

interface DataCallbackChange<T> {
entity: string;
action: string;
data: T;
id: number | string;
}

다음은 최소 사용 예제입니다:

<ReactGantt
// ...tasks/links/resources 등 props
data={{
batchSave: (changes) => {
console.log("Batch changes:", changes);

if (changes.tasks) {
setTasks((prev) => applyTaskBatch(prev, changes.tasks));
}

if (changes.links) {
setLinks((prev) => applyLinkBatch(prev, changes.links));
}

// 필요시 resources / assignments에 대해서도 동일 처리
},
}}
/>

여기서 applyTaskBatch/applyLinkBatch는 { action, data, id }를 순회하며 업데이트된 배열을 반환하는 작은 헬퍼들입니다.

일반 원칙으로서,

  • 한 번에 많은 변경이 예상되거나 백엔드에 모든 변경을 한 번의 요청으로 보내고 싶을 때는 batchSave를 사용
  • 대부분의 편집이 단일 작업/링크에 해당하고 가능한 간단한 통합을 원한다면 save를 사용

React 상태로 데이터를 로드하기​

React 주도 모델에서 Gantt는 React 상태를 통해 데이터를 받습니다. 그 상태가 어디에서 오는지는 애플리케이션 구조에 달려 있습니다.

개발자가 상태를 채우는 세 가지 일반적인 방법은 다음과 같습니다:

로컬 컴포넌트 상태​

빠른 데모, 프로토타입 또는 소규모 애플리케이션에 유용합니다.

데이터는 종종 로컬 시더 파일에서 가져오지만, 계산되거나 파생될 수도 있습니다.

export default function GanttTemplatesDemo() {
const [tasks, setTasks] = useState(projectData.tasks);
const [links, setLinks] = useState(projectData.links);
const [resources, setResources] = useState(projectData.resources);
const [resourceAssignments, setResourceAssignments] =
useState(projectData.resourceAssignments);

return (
<div style={{height: '100vh'}}>
<ReactGantt
tasks={tasks}
links={links}
resources={resources}
resourceAssignments={resourceAssignments}
/>
</div>
);
};

이 패턴은 상태 관리자를 사용하는 것과 매우 유사합니다 - 유일한 차이점은 상태가 어디에 있는지 입니다.

상태 관리 도구(Redux Toolkit, Zustand, MobX, Jotai, XState, Valtio)​

많은 프로덕션 애플리케이션에서 Gantt 데이터는 컴포넌트 내부가 아닌 전역 저장소에 저장됩니다.

ReactGantt는 이들 라이브러리와 자연스럽게 통합됩니다. 값을 전달하려면 셀럭터나 저장소 훅을 통해 저장소의 데이터를 읽고, 이를 props로 <Gantt>에 전달합니다. 로컬 상태에서 하는 것과 똑같이 작동합니다.

다음은 일반적으로 어떻게 보이는지의 예시들입니다.

Redux Toolkit

const { tasks, links } = useSelector((state: RootState) => state.gantt);

Zustand

const tasks = useGanttStore((state) => state.tasks);

MobX

<Gantt tasks={store.tasks} links={store.links} />

각 라이브러리는 고유 API를 가지지만 통합 패턴은 동일합니다 - tasks, links, resources 등을 props로 제공하고, 사용자 업데이트는 data.save 또는 data.batchSave를 통해 처리하며, 유일한 차이점은 데이터가 어디서 originates하는지입니다.

상태 관리 도구의 튜토리얼은 각 라이브러리에서 동일한 패턴을 구현하는 방법을 보여줍니다:

API에서 데이터 로드​

현실적인 애플리케이션에서는 보통 백엔드에서 데이터를 로드하고 이를 React 상태(로컬 또는 글로벌)에 배치합니다.

다음은 일반적인 사용 방식을 반영한 보다 완전한 예제입니다:

import { useEffect, useState } from "react";
import Gantt, {
Task,
Link,
Resource,
ResourceAssignment,
Calendar,
} from "@dhtmlx/trial-react-gantt";

interface GanttData {
tasks: Task[];
links: Link[];
resources: Resource[];
resourceAssignments: ResourceAssignment[];
}

export default function GanttWithApi() {
const [data, setData] = useState<GanttData | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function load() {
try {
const response = await fetch("/api/gantt-data");
const raw = await response.json();

const nextData: GanttData = {
tasks: raw.tasks,
links: raw.links,
resources: raw.resources ?? [],
resourceAssignments: raw.resourceAssignments ?? []
};

setData(nextData);
} finally {
setIsLoading(false);
}
}

load();
}, []);

if (isLoading || !data) {
return <div>Loading Gantt...</div>;
}

return (
<ReactGantt
tasks={data.tasks}
links={data.links}
resources={data.resources}
resourceAssignments={data.resourceAssignments}
data={{
save: (entity, action, item, id) => {
// 백엔드와 동기화하고 상태를 업데이트
},
}}
/>
);
}

이 접근 방식은 상태 관리 도구와 clean하게 통합됩니다:

  • 효과(t thunk)에서 데이터를 로드하고,
  • Redux/Zustand/MobX 등 저장소에 저장하고,
  • props를 통해 \<Gantt\>에 전달하고,
  • save/batchSave를 통해 업데이트를 처리합니다.

React 앱에서의 Gantt를 진실의 원천으로​

두 번째 바인딩 모드는 Gantt를 진실의 원천으로 삼는 방식으로 Gantt(및 필요 시 백엔드)가 기본 데이터 보유자로 취급됩니다.

React는 컴포넌트를 렌더링하지만, 업데이트마다 Prop으로 흐르는 canonical한 상태로서의 tasks/links/resources를 유지하지 않습니다.

이 모델은 React 상태 - Gantt 루프를 완전히 제거합니다.

이 모델이 의미가 있을 때​

다음과 같은 경우에 Gantt를 진실의 원천으로 삼는 방식을 사용합니다.

  • 데이터셋이 매우 큰 경우(수천 개의 작업)
  • 자동 일정 수립이나 대량 업데이트가 자주 발생하는 경우
  • React가 모든 업데이트를 실시간으로 반응할 필요가 없을 때
  • 페이지가 주로 "Gantt 중심"일 때

React는 레이아웃, 라우팅 및 주변 UI를 담당하고, Gantt가 데이터의 생명주기를 소유합니다.

초기 데이터 제공​

이 모델에서도 Gantt에 초기 데이터 세트를 제공할 수 있습니다. 중요한 차이는 변경을 지속적으로 React 상태로 반영하지 않는다는 점입니다.

다음과 같이 데이터를 로드하는 다양한 접근 방식으로 Gantt를 초기화할 수 있습니다: URL로 데이터 로딩, 커스텀 함수로 데이터 로딩, props를 통한 초기 스냅샷 전달. 아래의 세부 정보를 확인하세요.

URL로 데이터 로딩​

Gantt는 REST 엔드포인트를 사용해 백엔드에서 모든 것을 직접 로드할 수 있습니다:

<Gantt
data={{
load: "/api/gantt/load",
save: "/api/gantt/save",
}}
/>
  • data.load는 초기화 시 한 번 호출됩니다
  • 사용자가 작업/링크를 수정할 때마다 data.save가 트리거됩니다.

커스텀 함수로 데이터 로딩​

URL 대신 비동기 함수를 제공할 수 있습니다:

<Gantt
data={{
load: async () => {
const res = await fetch("/api/gantt/load");
return res.json();
},
save: async (entity, action, item, id) => {
// 아래 예제를 참조
},
}}
/>

props로 초기 스냅샷 전달(단방향)​

여전히 초기 데이터를 props로 제공할 수 있습니다:

<Gantt tasks={initialTasks} links={initialLinks} />

이 경우 props는 시작점일 뿐입니다. 초기화 후 Gantt는 자체 내부 저장소를 유지하고 거기서 계속 작동합니다. 매 편집 후에 props에 새로운 배열을 다시 피드하는 것이 아니라는 점은 이 모델에서 React가 데이터의 정식 소유주로 간주되지 않기 때문입니다.

업데이트가 작동하는 방식​

사용자가 데이터를 생성, 편집 또는 삭제할 때마다 Gantt는 save(또는 batchSave) 콜백을 트리거합니다.

Gantt는 생성된 레코드에 임시 ID를 처음으로 할당합니다. 백엔드는 이를 실제 데이터베이스 ID로 교체해야 합니다.

이 로직은 JS Gantt의 DataProcessor 모듈이 작동하는 방식(JS Docs의 Server-Side Integration 참조)과 일치합니다.

새로운 작업, 링크, 리소스 또는 어사인먼트가 생성되면 save 호출은 다음 형식의 Promise를 반환해야 합니다:

{ "id": "<database id>"}

예:

data.save = async (entity, action, item, id) => {
if (action === "create") {
const response = await fetch(`/api/${entity}`, {
method: "POST",
body: JSON.stringify(item),
headers: { "Content-Type": "application/json" }
});

const result = await response.json();
// 결과에는 { id: newDatabaseId }가 포함되어야 합니다
return { id: result.id };
}

if (action === "update") {
await fetch(`/api/${entity}/${id}`, {
method: "PUT",
body: JSON.stringify(item),
headers: { "Content-Type": "application/json" }
});
return {};
}

if (action === "delete") {
await fetch(`/api/${entity}/${id}`, { method: "DELETE" });
return {};
}
};

임시 ID를 영구 ID로 교체하도록 {id: newId}를 반환하는 것은 이후 업데이트/삭제 작업이 데이터베이스의 올바른 레코드를 대상으로 하도록 보장합니다.

batchSave 사용​

batchSave는 여러 변경을 하나의 콜백으로 묶습니다. 여러 새로운 레코드가 함께 나타날 수 있으므로 Gantt는 batchSave가 아무 것도 반환하길 기대하지 않습니다.

batchSave를 사용할 때는 백엔드에서 새 레코드를 생성하고 그들의 영구 ID를 얻은 뒤, 임시 ID를 대체하기 위해 명령형 API 호출로 Gantt를 업데이트해야 합니다:

gantt.changeTaskId(tempId, realId);
gantt.changeLinkId(tempId, realId);

다음 단계​

두 데이터 모델에 대해 명확히 이해했다면 특정 튜토리얼로 이동하면 됩니다.

상태 관리 도구:

또한 명령형 API와 서버 측 통신 사용에 대해 더 알아보기:

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.