본문 바로가기
Clone Coding

N8N & Zapier - Editor Setup

by zzuny-code 2026. 1. 20.
반응형

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를 활용한 워크플로우 에디터 구축

<ReactFlow /> 란?

React Flow는 노드 기반 인터페이스를 구축하기 위한 강력한 라이브러리다.

 

주요 특징:

  • 워크플로우, 다이어그램, 마인드맵 등 시각적 프로그래밍 도구 구현
  • 드래그, 줌, 패닝, 노드 연결 등 완전한 인터랙티브 기능
  • 노드와 엣지의 스타일 및 동작을 자유롭게 커스터마이징 가능
npm install @xyflow/react

 

 

 

1단계: 기본 Editor 컴포넌트 생성

 

  • Node (노드): 플로우차트의 박스/요소
  • Edge (엣지): 노드 간의 연결선
  • Position: 캔버스 상의 위치 좌표
더보기

사용자가 노드를 드래그하는 경우:

  1. 사용자 액션: 노드 'n1'을 (100, 150)으로 드래그
  2. React Flow 내부: 변경 이벤트 생성
  3. onNodesChange 호출
  4. applyNodeChanges: 현재 노드 배열에서 'n1'의 위치 업데이트
  5. 상태 업데이트: setNodes로 새 배열 설정
  6. 리렌더링: React Flow가 새 위치에 노드 표시

사용자가 노드를 연결하는 경우:

  1. 사용자 액션: 'n1'에서 'n2'로 연결선 드래그
  2. React Flow 내부: 연결 파라미터 생성
  3. onConnect 호출:
  4. addEdge: 새 엣지 객체 생성 및 중복 체크
  5. 상태 업데이트: setEdges로 새 배열 설정
  6. 리렌더링: 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