본문으로 건너뛰기

리액트 스케줄러와 Starhive 통합

이 자습서는 React Scheduler를 Starhive NoSQL 백엔드에 Next.js Route Handlers를 통해 연결합니다. Starhive는 타입이 지정된 스키마와 생성된 TypeScript 클라이언트를 제공하므로 API 계층은 최소화됩니다: 이벤트와 리소스를 로드하는 하나의 엔드포인트와 생성/수정/삭제를 처리하는 또 다른 엔드포인트가 있습니다.

다음과 같은 구성을 구축합니다:

  • 클라이언트 컴포넌트에 Scheduler를 호스팅하는 Next.js 페이지
  • /api/load - 첫 렌더링 시 Starhive에서 이벤트와 리소스를 가져옵니다
  • /api/event (POST) 및 /api/event/[id] (PUT, DELETE) - Scheduler의 dataBridge가 사용하는 쓰기 경로
노트

전체 소스 코드는 GitHub에서 이용 가능합니다.

전제 조건​

  • Next.js + React + TypeScript 기초 지식
  • Node.js 18+
  • Starhive 계정(30일 체험으로 충분)

1단계. 프로젝트 생성​

npx create-next-app@latest react-scheduler-starhive-demo
cd react-scheduler-starhive-demo

React Scheduler를 React Scheduler 설치 가이드에 따라 설치합니다. 평가용으로:

npm install @dhtmlx/trial-react-scheduler

이미 Professional 패키지를 사용 중이라면 명령 및 가져오기에 있는 @dhtmlx/trial-react-scheduler를 @dhx/react-scheduler로 바꿉니다.

생성된 Starhive TypeScript 클라이언트의 피어 의존성이 필요한 경우도 있으므로 axios도 필요합니다.

npm install axios

2단계. Starhive 공간 설정​

로그인 후 우측 상단 모서리에 있는 + Create를 클릭하고 공간 이름을 Scheduler로 설정합니다.

공간 내부에서 두 가지 타입을 만듭니다: Resources와 Events. Resources는 타임라인의 행을 보유하고(팀, 사람, 방 등), Events는 하나의 Resources를 참조합니다.

다음 속성을 + Attribute 버튼을 통해 추가합니다. Starhive는 각 항목의 id를 자동으로 생성하므로 직접 선언할 필요가 없습니다.

Resources type

FieldType
labelText

Events type

FieldType
textText
start_dateDate & Time
end_dateDate & Time
resource_idReference → Resources

3단계. 샘플 데이터 가져오기​

scheduler_resources.csv를 생성합니다:

label
"Frontend Team"
"Backend Team"
"QA Team"
"DevOps"
"Security Team"

그리고 scheduler_events.csv를 생성합니다:

text,start_date,end_date,resource_id
"Development","2026-04-01T08:00:00","2026-04-01T10:30:00","Frontend Team"
"Code Review","2026-04-01T09:00:00","2026-04-01T11:30:00","Backend Team"
"QA Testing","2026-04-01T10:00:00","2026-04-01T13:00:00","QA Team"
"Deployment","2026-04-01T11:00:00","2026-04-01T13:30:00","DevOps"
"Incident Response","2026-04-01T12:00:00","2026-04-01T15:00:00","DevOps"
"Maintenance Window","2026-04-01T08:30:00","2026-04-01T11:00:00","Backend Team"
"Security Scan","2026-04-01T13:00:00","2026-04-01T15:30:00","Security Team"

Starhive UI에서 각 파일의 타입을 열고 CSV import를 클릭합니다.

4단계. 스키마 생성 및 복사​

Settings → API Connectors로 이동합니다. Scheduler 공간을 선택하고 언어를 TypeScript로 설정한 다음 Generate, 그리고 Download를 클릭합니다.

압축을 풀고 project/src/io/ 아래의 starhive 폴더를 찾은 뒤, Next.js 프로젝트의 lib/starhive/로 복사합니다. 생성된 파일은 작업공간별 UUID를 포함하고 있어 스키마가 변경되거나 워크스페이스를 전환할 때마다 이 단계를 반복해야 합니다.

노트

작성 시점의 Starhive TypeScript 생성기는 엄격한 TypeScript를 통과하지 않는 출력(Sla.ts 참조 누락, 인라인 AttributeVisitor 리터럴의 구현 누락, client.request<T> 호출이 any로 타입된 필드에 대해 발생하는 TS2347 등)을 만들 수 있습니다. 보조 데모 저장소에는 이러한 문제를 해결하기 위한 세 가지 최소 패치가 포함되어 있습니다; lib/starhive/PATCHES.md에서 차이점을 확인하고 스키마를 재생성할 때마다 동일한 패치를 다시 적용하십시오. Starhive가 수정 사항을 공개할 때까지 계속 적용합니다.

5단계. Starhive 클라이언트 구성​

lib/starhiveClient.ts를 만듭니다:

lib/starhiveClient.ts
import { StarhiveClient } from "./starhive/client/StarhiveClient";
import { JSON_DECODERS } from "./starhive/schema/JsonDecoders";

let starhiveClient: StarhiveClient | null = null;

export function getStarhiveClient() {
if (starhiveClient) return starhiveClient;

const workspaceId = process.env.STARHIVE_WORKSPACE_ID;
const apiKey = process.env.STARHIVE_API_TOKEN;

if (!workspaceId || !apiKey) {
throw new Error("Missing Starhive configuration (workspaceId or API token)");
}

starhiveClient = new StarhiveClient(apiKey, workspaceId, JSON_DECODERS);
return starhiveClient;
}

함수는 모듈 범위에서 클라이언트를 캐시하므로 라우트 핸들러가 하나의 인스턴스를 공유합니다.

루트 프로젝트에 .env.local(또는 .env)를 추가합니다:

.env.local
STARHIVE_API_TOKEN=your-api-token
STARHIVE_WORKSPACE_ID=your-workspace-id

API 토큰은 설정 → 개인 액세스 토큰에서 생성합니다. 워크스페이스 ID는 https://app.starhive.com/workspace/<workspace-id>/home 경로의 일부입니다.

6단계. 이벤트와 리소스 불러오기​

app/api/load/route.ts를 생성합니다:

app/api/load/route.ts
import { NextResponse } from 'next/server';
import { getStarhiveClient } from '@/lib/starhiveClient';
import { Events } from '@/lib/starhive/schema/Events';
import { Resources } from '@/lib/starhive/schema/Resources';

function normalizeEvents(events: Events[]) {
return events.map(ev => ({
id: ev.getId() || '',
text: ev.getText(),
start_date: ev.getStartDate(),
end_date: ev.getEndDate(),
resource_id: ev.getResourceId()?.[0] || null,
}));
}

export async function GET() {
try {
const client = getStarhiveClient();
const [events, resources] = await Promise.all([
client.search<Events>(Events.TYPE_ID, ""),
client.search<Resources>(Resources.TYPE_ID, "")
]);

return NextResponse.json({
events: normalizeEvents(events.result),
resources: resources.result.map((r) => ({
key: r.getId(),
label: r.getLabel(),
})),
});
} catch (error) {
return NextResponse.json({ error: 'Failed to load data' }, { status: 500 });
}
}

normalizeEvents는 각 Starhive 객체를 React Scheduler가 기대하는 형태로 평면화합니다: { id, text, start_date, end_date, resource_id }. Resources는 { key, label }로 축약되며 이는 타임라인 뷰의 y_unit에서 소비됩니다.

개발 서버를 시작한 후 http://localhost:3000/api/load를 방문해 JSON 형태를 확인합니다.

7단계. Scheduler 렌더링 및 이벤트 불러오기​

app/page.tsx를 생성합니다:

app/page.tsx
'use client';

import { useEffect, useMemo, useState } from 'react';
import ReactScheduler, {
type Event,
type SchedulerViewsProp,
} from '@dhtmlx/trial-react-scheduler';
import '@dhtmlx/trial-react-scheduler/dist/react-scheduler.css';

type Resource = { key: string; label: string };

export default function Scheduler() {
const [resources, setResources] = useState<Resource[]>([]);
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetch('/api/load')
.then((response) => response.json())
.then((data) => {
setResources(data.resources);
setEvents(data.events);
})
.catch((error) => {
console.error('Failed to load resources data:', error);
})
.finally(() => {
setLoading(false);
});
}, []);

const views: SchedulerViewsProp = useMemo(
() => ({
timeline: [
{
name: "timeline",
x_unit: "hour",
x_date: "%H:%i",
x_step: 1,
x_start: 8,
x_size: 13,
x_length: 13,
event_dy: 50,
event_min_dy: 50,
y_property: "resource_id",
render: "bar",
y_unit: resources,
},
],
}),
[resources]
);

if (loading) {
return <div>Loading...</div>;
}

return (
<div style={{ height: "100vh" }}>
<ReactScheduler
events={events}
date={new Date("2026-04-01T00:00:00Z")}
views={views}
view={"timeline"}
/>
</div>
);
}

로딩 플래그는 이벤트 수를 확인하는 것보다 Scheduler가 비어 있어도 로딩 화면에 머물지 않도록 하는 것이 좋습니다.

npm run dev를 실행하면 imported 이벤트가 리소스별로 그룹화된 타임라인이 표시됩니다.

8단계. CRUD 엔드포인트 구현​

Scheduler의 dataBridge는 생성은 POST, 업데이트는 PUT, 삭제는 DELETE의 세 엔드포인트를 호출하며 다음과 같은 응답 형태를 기대합니다:

HTTP 메서드엔드포인트응답
GET/api/load{ events, resources }
POST/api/event{ action: "inserted", tid: id }
PUT/api/event/{event_id}{ action: "updated" }
DELETE/api/event/{event_id}{ action: "deleted" }

POST 핸들러를 app/api/event/route.ts에 만듭니다:

app/api/event/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getStarhiveClient } from '@/lib/starhiveClient';
import { Events } from '@/lib/starhive/schema/Events';

export async function POST(req: NextRequest) {
try {
const { text, start_date, end_date, resource_id } = await req.json();
const client = getStarhiveClient();

const event = Events.builder()
.text(text)
.startDate(new Date(start_date))
.endDate(new Date(end_date))
.resourceId([resource_id])
.build();

const result = await client.createObject(event);
return NextResponse.json({ action: 'inserted', tid: result.getId() });
} catch (error) {
return NextResponse.json({ error: 'Create failed' }, { status: 500 });
}
}

그리고 동적 PUT/DELETE 핸들러를 app/api/event/[id]/route.ts에 추가합니다:

app/api/event/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Events } from "@/lib/starhive/schema/Events";
import { getStarhiveClient } from "@/lib/starhiveClient";

export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const body = await request.json();
const client = getStarhiveClient();

const existingEvent = await client.getObject(id, Events.TYPE_ID);
if (!existingEvent) {
return NextResponse.json({ error: 'Event not found' }, { status: 404 });
}

const updatedEvent = Events.builder()
.id(id)
.text(body.text)
.startDate(new Date(body.start_date))
.endDate(new Date(body.end_date))
.resourceId([body.resource_id])
.build();

await client.updateObject(updatedEvent);
return NextResponse.json({ action: 'updated' });
} catch (error: any) {
console.error('Update error:', error);
return NextResponse.json(
{ error: 'Update failed', details: error.message },
{ status: 500 }
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const { id } = await params;
const client = getStarhiveClient();
await client.deleteObjectsInBulk([id]);

return NextResponse.json({ action: 'deleted' });
} catch (error: any) {
console.error('Delete error:', error);
return NextResponse.json(
{ error: 'Delete failed', details: error.message },
{ status: 500 }
);
}
}
노트

Next.js 15+에서 동적 경로 핸들러의 params 인수는 Promise입니다. 값을 읽기 전에 반드시 Promise<{...}>로 타입을 지정하고 await하십시오. 일부 설정에서 Promise<> 래퍼를 생략하면 컴파일되지만 엄격 모드에서 실패합니다.

9단계. dataBridge 연결​

클라이언트 측의 작은 헬퍼를 services/scheduler.ts에 생성합니다:

services/scheduler.ts
import type { Event } from '@dhtmlx/trial-react-scheduler';

async function request<T>(url: string, options: RequestInit): Promise<T> {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}

export function createEvent(event: Event) {
return request('/api/event', {
method: 'POST',
body: JSON.stringify(event),
headers: { 'Content-Type': 'application/json' },
});
}

export function updateEvent(event: Event) {
return request(`/api/event/${event.id}`, {
method: 'PUT',
body: JSON.stringify(event),
headers: { 'Content-Type': 'application/json' },
});
}

export function deleteEvent(id: string | number) {
return request(`/api/event/${id}`, {
method: 'DELETE',
});
}

그다음 페이지에 dataBridge를 연결합니다. imports를 추가하고 <ReactScheduler>에 data 속성을 추가합니다:

app/page.tsx
import { createEvent, deleteEvent, updateEvent } from '@/services/scheduler';

// Scheduler 컴포넌트 내부:
const dataBridge = useMemo(() => ({
save: (entity: string, action: string, payload: Event, id: string | number) => {
if (entity !== "event") return;

switch (action) {
case "update":
return updateEvent(payload);
case "create":
return createEvent(payload);
case "delete":
return deleteEvent(id);
default:
console.warn(`Unknown action: ${action}`);
return;
}
},
}), []);

// 컴포넌트에 데이터 브리지 전달:
<ReactScheduler
events={events}
data={dataBridge}
date={new Date("2026-04-01T00:00:00Z")}
views={views}
view={"timeline"}
/>

테스트 방법

npm run dev

브라우저에서 http://localhost:3000 를 열고 이벤트를 드래그로 새 시간으로 옮기고, 텍스트를 편집하고 하나를 삭제해 보십시오. 각 변경 사항은 Starhive UI의 Events 타입 아래 즉시 반영됩니다.

Starhive 통합에 대한 참고사항​

  • 서버 측 자격 증명만 사용합니다. STARHIVE_API_TOKEN과 STARHIVE_WORKSPACE_ID는 Route Handler 내부에서 읽히며 브라우저 번들에 노출되지 않습니다. 토큰을 Client Component로 올리거나 NEXT_PUBLIC_* 변수로 노출하지 마십시오.
  • 스키마 재생성. Starhive의 속성을 추가하거나 이름을 바꿀 때는 TypeScript 스키마를 재생성하고 lib/starhive/를 교체합니다. 문제가 발생하면 lib/starhive/PATCHES.md 경로의 패치를 재적용하십시오.
  • 실시간 동기화 없음. Firebase 통합과 달리 Starhive는 연결된 클라이언트에 변경 내용을 푸시하지 않습니다. 여러 사용자가 동일한 Scheduler를 편집하면 서로의 변경 사항이 덮어씌워질 수 있습니다. 다중 사용자 시나리오의 경우 클라이언트 측 폴링을 추가하거나 Starhive 웹훅을 사용해 SSE/웹소켓으로 무효화 이벤트를 보내고 원격 변경 시 events 상태를 새로고침하십시오.
  • 대용량 데이터 세트의 동적 로딩. /api/load 경로는 워크스페이스의 모든 이벤트를 로드합니다. 프로덕션에서는 GET 핸들러에서 from / to 쿼리 매개변수를 받아 start_date / end_date를 필터링하고 클라이언트에서 scheduler.setLoadMode("day")를 호출하여 보이는 범위의 데이터만 가져오도록 하십시오.
  • 참조 속성은 배열을 담습니다. Events.getResourceId()는 string[] | undefined를 반환합니다. Starhive의 참조 속성은 다-valued이기 때문입니다. 데모는 ?.[0] || null로 평탄화합니다. 이벤트가 여러 리소스에 속하도록 허용하는 경우 타임라인 뷰의 y_property 해상도와 평탄화/빌더 호출을 이에 맞게 조정하십시오.

관련 페이지​

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.