https://www.youtube.com/watch?v=ED2H_y6dmC8
08:59:36
Create "Editor" component → "Editor" 컴포넌트 생성
- Add react-flow
→react-flow 추가 - Add initial nodes
→초기 노드 추가
React Flow를 활용한 워크플로우 에디터 구축
React Flow는 노드 기반 인터페이스를 구축하기 위한 강력한 라이브러리다.
주요 특징:
- 워크플로우, 다이어그램, 마인드맵 등 시각적 프로그래밍 도구 구현
- 드래그, 줌, 패닝, 노드 연결 등 완전한 인터랙티브 기능
- 노드와 엣지의 스타일 및 동작을 자유롭게 커스터마이징 가능
npm install @xyflow/react
1단계: 기본 Editor 컴포넌트 생성
- Node (노드): 플로우차트의 박스/요소
- Edge (엣지): 노드 간의 연결선
- Position: 캔버스 상의 위치 좌표
사용자가 노드를 드래그하는 경우:
- 사용자 액션: 노드 'n1'을 (100, 150)으로 드래그
- React Flow 내부: 변경 이벤트 생성
- onNodesChange 호출
- applyNodeChanges: 현재 노드 배열에서 'n1'의 위치 업데이트
- 상태 업데이트: setNodes로 새 배열 설정
- 리렌더링: React Flow가 새 위치에 노드 표시
사용자가 노드를 연결하는 경우:
- 사용자 액션: 'n1'에서 'n2'로 연결선 드래그
- React Flow 내부: 연결 파라미터 생성
- onConnect 호출:
- addEdge: 새 엣지 객체 생성 및 중복 체크
- 상태 업데이트: setEdges로 새 배열 설정
- 리렌더링: React Flow가 연결선 표시
Editor 컴포넌트 구현
src/features/editor/components/editor.tsx
"use client"
import { useState, useCallback } from 'react';
import {
ReactFlow, applyNodeChanges, applyEdgeChanges, addEdge,
type Node, // 노드 타입
type Edge, // 엣지 타입
type NodeChange, // 노드 변경 이벤트 타입
type EdgeChange, // 엣지 변경 이벤트 타입
type Connection, // 연결 파라미터 타입
Background,
Controls,
MiniMap
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { ErrorView, LoadingView } from "@/components/entity-components"
import { useSuspenseWorkflow } from "@/features/workflows/hooks/use-workflows"
export const EditorLoading = () => {
return <LoadingView message="Loading editor..."/>
}
export const EditorError = () => {
return <ErrorView message="Error loading editor..."/>
}
// 플로우차트의 박스/요소
const initialNodes = [
{
id: 'n1', // 고유 식별자
position: { x: 0, y: 0 }, // 캔버스상 위치
data: { label: 'Node 1' } // 노드에 표시할 데이터
},
{
id: 'n2',
position: { x: 0, y: 100 },
data: { label: 'Node 2' }
},
];
// 노드 간의 연결 선
const initialEdges = [{
id: 'n1-n2', // 고유 식별자
source: 'n1', // 시작 노드 ID
target: 'n2' // 도착 노드 ID
}];
export const Editor = ({workflowId}:{workflowId: string}) => {
const {data: workflow} = useSuspenseWorkflow(workflowId)
// 상태 관리
const [nodes, setNodes] = useState<Node[]>(initialNodes); // 현재 캔버스에 있는 모든 노드들
const [edges, setEdges] = useState<Edge[]>(initialEdges); // 현재 캔버스에 있는 모든 연결선들
// onNodesChange - 노드 변경 처리(노드 드래그, 노드 선택/해제, 노드 삭제, 노드의 크기 변경시점)
const onNodesChange = useCallback(
// useCallback 사용이유
// React Flow는 매 렌더링마다 props 변경을 체크
// 함수가 매번 새로 생성되면 불필요한 리렌더링 발생
// useCallback으로 함수를 메모이제이션해서 성능 최적화
(changes:NodeChange[]) => setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot)),
[],
);
// onEdgesChange - 엣지 변경 처리(엣지 선택/해제, 엣지 삭제, 엣지 스타일 변경시점)
const onEdgesChange = useCallback(
(changes:EdgeChange[]) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
[],
);
// onConnect - 새 연결 생성( 노드의 핸들을 드래그해서 다른 노드에 연결할때 호출 )
const onConnect = useCallback(
// params:Connection 구조
// {
// source: 'n1', // 시작 노드
// target: 'n2', // 끝 노드
// sourceHandle: null, // 시작 핸들 ID (옵션)
// targetHandle: null // 끝 핸들 ID (옵션)
// }
(params:Connection) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)), // addEdge는 중복 체크를 하고 새 엣지를 배열에 추가
[],
);
return(
<div className='size-full'>
<ReactFlow
nodes={nodes} // 표시할 노드들
edges={edges} // 표시할 엣지들
onNodesChange={onNodesChange} // 노드 변경 핸들러
onEdgesChange={onEdgesChange} // 엣지 변경 핸들러
onConnect={onConnect} // 연결 생성 핸들러
fitView // 초기 로드시 모든 노드가 보이도록 자동 줌
>
<Background/>
<Controls/>
<MiniMap/>
</ReactFlow>
</div>
)
}
Update schema → 스키마 업데이트
- Add "Node" table
→"Node" 테이블 추가 - Add "Connection" table
→"Connection" 테이블 추가
Load default editor state → 기본 에디터 상태 로드
2단계: 데이터베이스 스키마 업데이트
prisma/schema.prisma
...
model Workflow{
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 관계 추가
nodes Node[]
conections Connection[]
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum NodeType{
INITIAL
}
model Node{
id String @id @default(cuid())
workflowId String
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
name String
type NodeType
position Json
data Json @default("{}")
outputConnections Connection[] @relation("FromNode")
inputConnections Connection[] @relation("ToNode")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Connection{
id String @id @default(cuid())
workflowId String
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
fromNodeId String
fromNode Node @relation("FromNode", fields: [fromNodeId], references: [id], onDelete: Cascade)
toNodeId String
toNode Node @relation("ToNode", fields: [toNodeId], references: [id], onDelete: Cascade)
fromOutput String @default("main")
toInput String @default("main")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([fromNodeId, toNodeId, fromOutput, toInput])
}
마이그레이션 실행:
npx prisma migrate dev
# name: react-flow-tables
3단계: API 라우터 수정
src/features/workflows/server/routers.ts
- 워크플로우 생성 시 초기 노드를 자동으로 생성하고, 조회 시 React Flow 형식으로 데이터를 변환
// src/features/workflows/server/routers.ts
create: premiumProcedure.mutation(({ctx}) => {
return prisma.workflow.create({
data: {
name: generateSlug(3),
userId: ctx.auth.user.id,
nodes: {
create: {
type: NodeType.INITIAL,
position: {x: 0, y: 0},
name: NodeType.INITIAL,
}
}
}
})
}),
getOne: protectedProcedure
.input(z.object({id: z.string()}))
.query(async({ctx, input}) => {
const workflow = await prisma.workflow.findUniqueOrThrow({
where: { id: input.id, userId: ctx.auth.user.id },
include: { nodes: true, connections: true }
})
// Prisma 데이터를 React Flow 형식으로 변환
const nodes: Node[] = workflow.nodes.map((node) => ({
id: node.id,
type: node.type,
position: node.position as {x: number, y: number},
data: (node.data as Record<string, unknown>) || {},
}))
const edges: Edge[] = workflow.connections.map((connection) => ({
id: connection.id,
source: connection.fromNodeId,
target: connection.toNodeId,
sourceHandle: connection.fromOutput,
targetHandle: connection.toInput,
}))
return {
id: workflow.id,
name: workflow.name,
nodes,
edges
}
}),
src/features/editor/components/editor.tsx 수정
// 상태 관리
const [nodes, setNodes] = useState<Node[]>(workflow.nodes); // 현재 캔버스에 있는 모든 노드들
const [edges, setEdges] = useState<Edge[]>(workflow.edges); // 현재 캔버스에 있는 모든 연결선들

참고
https://reactflow.dev/ui/components/placeholder-node
Placeholder Node - React Flow
A custom node that can be clicked to create a new node.
reactflow.dev
4단계: 커스텀 노드 컴포넌트 생성
npx shadcn@3.3.1 add https://ui.reactflow.dev/placeholder-node
설치된 파일들을 정리:
- src/components/react-flow 디렉토리 생성
- base-node.tsx와 placeholder-node.tsx를 이동
----
src/components/initial-node.tsx
"use client"
import { memo } from "react"
import { NodeProps } from "@xyflow/react"
import { PlaceholderNode } from "./react-flow/placeholder-node"
import { PlusIcon } from "lucide-react"
export const InitialNode = memo((props:NodeProps) => {
return(
<PlaceholderNode
{...props}
>
<div className="cursor-pointer flex items-center justify-center">
<PlusIcon className="size-4"/>
</div>
</PlaceholderNode>
)
})
InitialNode.displayName = "InitialNode"
노드 컴포넌트 등록
src/config/node-components.ts
1. NodeTypes 타입:
- React Flow가 요구하는 노드 타입 맵 구조
- 키는 노드 타입 문자열, 값은 React 컴포넌트
2. satisfies 키워드:
- TypeScript 4.9+에서 도입된 기능
- nodeComponents가 NodeTypes 인터페이스를 만족하는지 검증하면서도 정확한 타입을 유지
3. RegisteredNodeType:
- 등록된 노드 타입들의 유니온 타입을 추출
- 타입 안전성을 제공하여 존재하지 않는 노드 타입 사용을 방지
import { InitialNode } from "@/components/initial-node";
import { NodeType } from '@/generated/prisma/enums';
import type { NodeTypes } from "@xyflow/react"
export const nodeComponents = {
[NodeType.INITIAL] : InitialNode,
} as const satisfies NodeTypes;
export type RegisteredNodeType = keyof typeof nodeComponents;
src/features/editor/components/editor.tsx
return(
<div className='size-full'>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
// 추가
nodeTypes={nodeComponents}
fitView
>
<Background/>
<Controls/>
<MiniMap/>
</ReactFlow>
</div>
)
화면에 node 가 1개 생김.

WorkflowNode 래퍼 컴포넌트
src/components/workflow-node.tsx
- 모든 워크플로우 노드에 공통 기능을 제공하는 래퍼 컴포넌트만들기
1. NodeToolbar:
- React Flow에서 제공하는 노드별 툴바 컴포넌트
- 노드를 선택했을 때 설정 및 삭제 버튼을 표시
- position prop으로 툴바 위치를 지정할 수 있다
2. 조건부 렌더링:
- showToolbar: true일 때만 상단 툴바를 표시
- name: 존재할 때만 하단에 이름과 설명을 표시
"use client"
import { NodeToolbar, Position } from "@xyflow/react";
import { ReactNode } from "react";
import { Button } from "./ui/button";
import { SettingsIcon, TrashIcon } from "lucide-react";
interface WorkflowNodeProps {
children : ReactNode;
showToolbar?: boolean;
onDelete?: () => void;
onSettings?: () => void;
name?: string;
description?: string;
}
export function WorkflowNode({
children,
showToolbar,
onDelete,
onSettings,
name,
description,
}: WorkflowNodeProps) {
return(
<>
{showToolbar && (
<NodeToolbar>
<Button size="sm" variant="ghost" onClick={onSettings}>
<SettingsIcon/>
</Button>
<Button size="sm" variant="ghost" onClick={onDelete}>
<TrashIcon/>
</Button>
</NodeToolbar>
)}
{children}
{name && (
<NodeToolbar
position={Position.Bottom}
isVisible
className="max-w-[200px] text-center"
>
<p className="font-medium">
{name}
</p>
{description && (
<p className="text-muted-foreground truncate text-sm">
{description}
</p>
)}
</NodeToolbar>
)}
</>
)
}
-----
PlaceholderNode 커스터마이징
src/components/react-flow/placeholder-node.tsx. // onClick 추가 후 코드 수정
1. Props 타입:
- Partial<NodeProps>: React Flow의 기본 노드 props를 선택적으로 받음
- onClick: 클릭 이벤트 핸들러를 추가
- 2. Handle 컴포넌트:
- type="target": 다른 노드로부터 연결을 받을 수 있는 입력 포인트
- type="source": 다른 노드로 연결을 시작할 수 있는 출력 포인트
- visibility: "hidden": 핸들을 숨기지만 기능은 유지
- isConnectable={false}: PlaceholderNode는 연결할 수 없도록 설정
"use client";
import React, {type ReactNode } from "react";
import {
Handle,
Position,
type NodeProps,
} from "@xyflow/react";
import { BaseNode } from "./base-node";
export type PlaceholderNodeProps = Partial<NodeProps> & {
children?: ReactNode;
onClick?: ()=> void
};
export function PlaceholderNode({ children, onClick }: PlaceholderNodeProps) {
return (
<BaseNode
className="w-auto h-auto border-dashed border-gray-400 bg-card p-4 text-center text-gray-400 shadow-none cursor-pointer hover:border-gray-500 hover:bg-gray-50"
onClick={onClick}
>
{children}
<Handle
type="target"
style={{ visibility: "hidden" }}
position={Position.Top}
isConnectable={false}
/>
<Handle
type="source"
style={{ visibility: "hidden" }}
position={Position.Bottom}
isConnectable={false}
/>
</BaseNode>
);
}
src/components/initial-node.tsx
1. memo 최적화:
- React.memo로 컴포넌트를 감싸서 props가 변경되지 않으면 리렌더링을 방지
- 많은 노드가 있을 때 성능 향상에 도움이 됨
2. displayName:
- React DevTools에서 컴포넌트 이름을 명확하게 표시하기 위해 설정
- memo로 감싸면 displayName이 사라지므로 명시적으로 설정
3. 컴포넌트 구조:
- WorkflowNode로 감싸지만 툴바는 표시하지 않는다 (showToolbar={false})
- PlaceholderNode 내부에 + 아이콘을 표시하여 새 노드 추가를 암시
"use client"
import { memo } from "react"
import { NodeProps } from "@xyflow/react"
import { PlaceholderNode } from "./react-flow/placeholder-node"
import { PlusIcon } from "lucide-react"
import { WorkflowNode } from "./workflow-node"
export const InitialNode = memo((props:NodeProps) => {
return(
<WorkflowNode showToolbar={false}>
<PlaceholderNode
{...props}
onClick={()=>{}}
>
<div className="cursor-pointer flex items-center justify-center">
<PlusIcon className="size-4"/>
</div>
</PlaceholderNode>
</WorkflowNode>
)
})
InitialNode.displayName = "InitialNode"
5단계: AddNodeButton 컴포넌트
src/features/editor/components/add-node-button.tsx // 새 노드를 추가할 수 있는 버튼
- onClick 핸들러가 비어있어 아직 기능이 구현되지 않았음
"use client"
import { memo } from "react"
import { Button } from "@/components/ui/button"
import { PlusIcon } from "lucide-react"
export const AddNodeButton = memo(()=>{
return(
<Button
onClick={()=>{}}
size="icon"
variant="outline"
className="bg-background"
>
<PlusIcon/>
</Button>
)
})
AddNodeButton.displayName = "AddNodeButton";
src/features/editor/components/editor.tsx // AddNodeButton 추가
return(
<div className='size-full'>
<ReactFlow
nodes={nodes} // 표시할 노드들
edges={edges} // 표시할 엣지들
onNodesChange={onNodesChange} // 노드 변경 핸들러
onEdgesChange={onEdgesChange} // 엣지 변경 핸들러
onConnect={onConnect} // 연결 생성 핸들러
nodeTypes={nodeComponents}
fitView // 초기 로드시 모든 노드가 보이도록 자동 줌
>
<Background/>
<Controls/>
<MiniMap/>
<Panel position='top-right'>
<AddNodeButton/>
</Panel>
</ReactFlow>
</div>
)

이건 잘 모르겠다. 패스패스
'Clone Coding' 카테고리의 다른 글
| *잠시 중단* N8N & Zapier - Editor State (0) | 2026.01.26 |
|---|---|
| N8N & Zapier - Node selector (0) | 2026.01.22 |
| N8N & Zapier - Workflow Page (0) | 2026.01.17 |
| N8N & Zapier - Workflows UI (1) | 2026.01.16 |
| N8N & Zapier - Workflows Pagination (0) | 2026.01.15 |