본문으로 건너뛰기

리액트 스케줄러

노트

리액트 스케줄러는 상용, 엔터프라이즈 및 얼티밋 라이선스 하에 제공됩니다. 스케줄러의 개인(Individual) 또는 GPL 에디션을 사용하는 경우, React용 dhtmlxScheduler를 사용하시기 바랍니다.

개요

DHTMLX React Scheduler는 DHTMLX Scheduler의 공식 React 래퍼입니다. 고급 제어가 필요할 때 기본 Scheduler 인스턴스를 노출하면서도 Scheduler를 렌더링하고 구성하기 위한 선언적 API를 제공합니다.

주요 기능:

  • 프롭스로 events, view, 및 date 전달
  • configtemplates로 동작을 커스터마이즈
  • 사용자의 변경을 data.save 또는 data.batchSave를 통해 처리
  • Scheduler API 메서드에 직접 접근하려면 ref 사용

DHTMLX Scheduler에 익숙하지 않다면, 기능 개요를 보려면 DHTMLX Scheduler 문서를 참고하세요.

설치 및 npm 접근

평가판 및 전문가용 패키지 설치에 대한 내용은 아래를 참고하세요:

버전 요구사항

  • 리액트 18+

기본 사용법

import { useMemo, useRef } from "react";
import ReactScheduler, {
type Event,
type ReactSchedulerRef,
type SchedulerConfig,
type SchedulerTemplates,
} from "@dhtmlx/trial-react-scheduler";
import "@dhtmlx/trial-react-scheduler/dist/react-scheduler.css";

const events: Event[] = [
{
id: 1,
text: "Product Strategy Hike",
classname: "blue",
start_date: new Date("2025-12-08T02:00:00Z"),
end_date: new Date("2025-12-08T10:20:00Z"),
},
];

export default function BasicSchedulerDemo() {
const schedulerRef = useRef<ReactSchedulerRef>(null);

const templates: SchedulerTemplates = useMemo(
() => ({
event_class: (_start, _end, event) => event.classname || "",
}),
[]
);

const config: SchedulerConfig = useMemo(
() => ({
first_hour: 6,
last_hour: 22,
hour_size_px: 60,
}),
[]
);

return (
<div style={{ height: "100vh" }}>
<ReactScheduler
ref={schedulerRef}
events={events}
view="week"
date={new Date("2025-12-08T00:00:00Z")}
templates={templates}
config={config}
/>
</div>
);
}

데이터 바인딩

리액트 스케줄러는 두 가지 데이터 바인딩 모델을 지원합니다.

핵심 원천으로서의 React 상태(권장)

이 모델에서 React(또는 상태 관리 도구)가 이벤트 데이터를 소유합니다:

  • Scheduler는 프롭스로 이벤트를 읽습니다
  • 사용자의 변경은 data.save 콜백을 호출합니다
  • 콜백이 React 상태를 업데이트합니다
  • React가 업데이트된 프롭으로 Scheduler를 다시 렌더링합니다
import { useMemo, useState } from "react";
import ReactScheduler, { type Event } from "@dhtmlx/trial-react-scheduler";

export default function ReactDrivenExample({ seedEvents }: { seedEvents: Event[] }) {
const [events, setEvents] = useState<Event[]>(seedEvents);

const data = useMemo(
() => ({
save: (entity: string, action: string, item: Event, id: string | number) => {
if (entity !== "event") return;

if (action === "create") {
setEvents((prev) => [...prev, item]);
return;
}

if (action === "update") {
setEvents((prev) => prev.map((e) => (e.id === id ? item : e)));
return;
}

if (action === "delete") {
setEvents((prev) => prev.filter((e) => e.id !== id));
}
},
}),
[]
);

return <ReactScheduler events={events} data={data} />;
}

이 모델은 다른 React UI가 Scheduler 데이터와 동기화되어 있어야 할 때 가장 적합합니다.

Scheduler를 진실의 원천으로

이 모델에서 Scheduler는 내부 상태를 관리하고 수정사항을 백엔드로 전달합니다.

<ReactScheduler
data={{
load: "/api/scheduler/load",
save: "/api/scheduler/save",
}}
/>

이 모델은 React가 매 업데이트를 즉시 반영할 필요가 없을 때 유용합니다.

데이터 로드

프롭스나 data.load 중 하나를 사용하여 데이터를 로드할 수 있습니다:

// Props 기반 로딩
<ReactScheduler events={eventsFromState} />

// 전송 기반 로딩
<ReactScheduler data={{ load: "/api/scheduler/load" }} />

데이터 형식 요구사항은 데이터 로딩을 참조하세요.

변경 사항 저장

data.save는 URL이 될 수 있으며 콜백일 수도 있습니다.

<ReactScheduler
data={{
save: async (entity, action, item, id) => {
if (entity !== "event") return;

if (action === "create") {
const response = await fetch("/api/events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item),
});

const created = await response.json();
return { id: created.id };
}

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

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

백엔드 동작에 대한 세부 내용은 Server Integration을 참조하세요.

라이트박스 교체

스케줄러에는 라이트박스라고 하는 내장 이벤트 편집기가 포함되어 있습니다.

이를 customLightbox를 사용하여 교체할 수 있습니다:

import React, { useState } from 'react';

export interface CustomLightboxProps {
data?: any;
onSave?: (event: any) => void;
onCancel?: () => void;
onDelete?: () => void;
}

const CustomLightbox: React.FC<CustomLightboxProps> = ({
data,
onSave,
onCancel,
onDelete
}) => {

let updatedEventText = data.text || "";

const handleSaveClick = () => {
if(onSave)
onSave({ ...data, text: updatedEventText });
};

function PaperComponent(props: any) {
const nodeRef = React.useRef(null);
return (
<Draggable
nodeRef={nodeRef"
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"], input,textarea'}
>
<Paper {...props} ref={nodeRef}/>
</Draggable>
);
}


function TextComponent() {
const [description, setDescription] = useState<string>(data.text || '');

return (
<TextField
id="event_text"
hiddenLabel
multiline
value="{description}"
autoFocus
onChange="{(e)" => {
updatedEventText = e.target.value;
setDescription(e.target.value)
}}
sx="{{" width: '100%', padding: '8px', marginTop: '10px' }}
/>
)
}


return (
<Dialog
open={true}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
className="lightbox"
onClose={onCancel}
>
<DialogTitle style={{ cursor: 'move' }} id="draggable-dialog-title">
Edit Event
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Description
</DialogContentText>

<TextComponent />

<DialogActions className='buttons'>
<Button variant="contained" onClick={handleSaveClick}>Save</Button>
<Button variant="contained" onClick={onCancel}>Cancel</Button>
<Button variant="contained" onClick={onDelete}>Delete</Button>
</DialogActions>
</DialogContent>
</Dialog>

);
};

export default CustomLightbox;

에디터 열기를 차단하는 훅도 제공됩니다(onBeforeLightbox):

import { useEffect, useRef } from 'react';
import ReactScheduler from "@dhx/react-scheduler";
import "@dhx/react-scheduler/dist/react-scheduler.css";
import { useNavigate } from 'react-router-dom';


export default function BasicInitDemo() {
const schedulerRef = useRef<any>(null);

const { events, handleSaveEvent, handleDeleteEvent, createEvent }
= useOutletContext<SchedulerEditorContext>();
const navigate = useNavigate();

const handleEventEdit = (id: any) => {
const schedulerInstance = schedulerRef.current?.instance;
navigate(`/editor/${id}`, { state: { task: schedulerInstance.getTask(id) } });
};

return (
<ReactScheduler
ref={schedulerRef}
tasks={events}
onBeforeLightbox={handleEventEdit} />
);
}

참고: onBeforeLightbox

내장 모달 교체

삭제 확인 다이얼로그는 modals를 통해 재정의할 수 있습니다.

<ReactScheduler
modals={{
onBeforeEventDelete: ({ event, callback, schedulerInstance }) => {
if (window.confirm(`Delete "${event.text}"?`)) {
callback(); // 콜백을 호출하면 이벤트가 삭제됩니다
}
},
}}
/>

반복 일정 확인 모달 커스터마이즈하기

사용자가 반복 이벤트를 편집하거나 끌어다 놓은 경우, 특정 발생건만 수정할지, 이후 이벤트까지 포함할지, 아니면 전체 시퀀스를 수정할지 물어보는 확인 모달이 나타납니다. 이 내장 대화상자를 modals.onRecurrenceConfirm으로 대체할 수 있습니다.

콜백은 컨텍스트 객체를 받고 결정을 반환해야 합니다(또는 이를 해결하는 Promise):

필드타입설명
origin"lightbox" | "dnd"라이트박스에서 트리거되었는지 여부 또는 드래그 앤 드롭으로 트리거되었는지 여부
occurrenceany편집 중인 특정 발생 건
seriesany부모 반복 이벤트
labelsobject지역화된 레이블: title, ok, cancel, occurrence, following, series
optionsstring[]사용 가능한 선택지 예: ["occurrence", "following", "series"]

Return value (RecurrenceDecision): "occurrence", "following", "series", 또는 취소를 의미하는 null.

예시:

import { useState, useCallback } from "react";

function App() {
const [recurrencePrompt, setRecurrencePrompt] = useState(null);

const onRecurrenceConfirm = useCallback((context) => {
return new Promise((resolve) => {
setRecurrencePrompt({ context, resolve });
});
}, []);

return (
<>
<ReactScheduler
modals={{ onRecurrenceConfirm }}
/>
{recurrencePrompt && (
<MyRecurrenceDialog
options={recurrencePrompt.context.options}
labels={recurrencePrompt.context.labels}
onSelect={(choice) => {
recurrencePrompt.resolve(choice);
setRecurrencePrompt(null);
}}
onCancel={() => {
recurrencePrompt.resolve(null);
setRecurrencePrompt(null);
}}
/>
)}
</>
);
}

필터링

표시되는 이벤트를 제어하려면 filter 프롭을 사용하세요:

import { useCallback, useState } from "react";

function FilteredScheduler({ events }: { events: any[] }) {
const [query, setQuery] = useState("");

const filterFn = useCallback(
(event: any) => {
if (!query.trim()) return true;
return event.text?.toLowerCase().includes(query.trim().toLowerCase());
},
[query]
);

return (
<>
<input
placeholder="Search events..."
value={query}
onChange={(e) =/> setQuery(e.target.value)}
/>
<ReactScheduler events={events} filter={filterFn} />
</>
);
}

기본 Scheduler API에 접근하기

프롭스만으로 부족할 때는 ref를 통해 Scheduler 인스턴스에 접근합니다:

import { useEffect, useRef } from "react";
import ReactScheduler, { type ReactSchedulerRef } from "@dhx/react-scheduler";

export function DirectRefExample({ events }: { events: any[] }) {
const schedulerRef = useRef<ReactSchedulerRef>(null);

useEffect(() => {
const scheduler = schedulerRef.current?.instance;
if (!scheduler) return;

console.log("Events:", scheduler.getEvents());
scheduler.setCurrentView(new Date());
}, []);

return <ReactScheduler ref={schedulerRef} events={events} />;
}

프로퍼티를 직접 수정하는 경우, 상태 드리프트를 피하기 위해 React 프롭스를 동기화해 두는 것이 좋습니다.

가능한 메서드 목록은 Scheduler Methods Overview를 참조하세요.

React 프롭스와의 충돌 방지

  • 코드에서 수동으로 scheduler.parse(( events ))를 호출하거나 scheduler.addEvent()를 호출하는 경우, React 프롭스를 동기화 상태로 유지해야 할 수 있습니다. 그렇지 않으면 다음 React 렌더링 시 수동 변경이 덮어써질 수 있습니다.
  • 권장되는 방법은 이벤트에 대해 래퍼의 프롭스에 의존하거나 React 상태에서 관리하는 것이며, 그런 다음 래퍼가 재파싱을 처리하게 두는 것입니다.

SSR 프레임워크(Next.js, Remix)와의 호환성

노트

리액트 스케줄러는 SSR 친화적입니다. 서버 렌더링 중에는 플레이스홀더 컨테이너를 출력하고 클라이언트에서 하이드레이션합니다.

세부 내용은 프레임워크별 가이드를 참고하세요:

다음 단계

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.