개발자 윤찬

웹 개발자 윤찬의 프로필 사진개발자 윤찬
2025. 6. 7.

라이브러리 커스터마이징(Rete)

VienceReact.jsOpen Source
"내가 원하는 조립"

라이브러리를 쓴다는 것

프론트엔드 개발을 하다 보면 수많은 라이브러리를 접하게 된다. 내가 생각하기에 라이브러리를 사용한다는 건 크게 세 가지 방식으로 나눌 수 있다.

  • 그대로 가져다 쓰는 경우
  • 오픈소스에 직접 기여하는 경우
  • 서비스에 맞게 커스터마이징해서 응용하는 경우

개인적으로는 세 번째 방식이 가장 흥미롭다. 단순히 가져다 쓰는 걸로 끝나는 게 아니라, 서비스 맥락에 맞게 확장하거나 재해석할 수 있기 때문이다. 그렇게 되면 단순히 도구로 쓰는 게 아니라, 마치 일부처럼 녹여낼 수 있다. 특히 프론트엔드 쪽에서는 재미있고 창의적인 라이브러리가 많아서 더 매력적으로 느껴진다. 단순 사용에서 멈추지 않고, 조금만 응용하면 라이브러리의 가능성을 훨씬 더 크게 활용할 수 있다. 이 과정 자체가 개발자로서 굉장히 즐겁다.

회사 서비스에서는 현재 rete.js라는 시각적 에디터 라이브러리를 사용하고 있다. 원래 rete.js도 꽤 강력한 기능을 제공하지만, 서비스에서는 여기에 UI적인 고도화와 커스터마이징을 거쳐서 workspace라는 전체 서비스 맥락에 맞게 적용하고 있다. 오늘은 rete.js 라이브러리를 커스터마이징 한 내용 및 과정에 대한 글이다.


Rete.js 커스터마이징

rete 자체는 엔진을 가지고 있다. 이것에 대해서는 여러번 포스팅을 진행했었다.

Workspace 안정화 작업 상태 일관성

이 중에서도 중요한 라이브러리의 커스텀 작업 기준은 크게 두 가지라고 생각한다.

  • UI 고도화 라이브러리가 기본적으로 제공해주는 UI를 서비스에 맞게 변형하고 확장하는 작업. 예를 들어, 사용자 경험(UX)을 고려해 기본 노드 UI를 재디자인하거나, 서비스 컨셉에 맞는 인터랙션을 추가하는 방식.
  • 기능적(속성) 고도화 라이브러리가 제공하는 속성(객체의 기본값이나 기본 기능)에 직접 로직을 덧붙이거나, 필요한 경우 상속을 통해 새로운 동작을 정의하는 작업. 즉, 단순히 화면만 바꾸는 것이 아니라 동작 자체를 서비스에 맞게 바꿔 나가는 것

Rete.js의 UI 고도화

rete는 위의 사진 디폴트 UI이다. 이것을 최종적으로 UI 고도화 한 것이 아래의 사진과 같다.

rete에서는 저런 사각형 모양을 node라고 부른다. 우리 회사에서도 동일하게 node라는 용어를 사용하며 소통한다. 각 node마다 고유의 기능을 가지고 있기 때문에, 사용자 입장에서는 어떤 기능을 수행하는지 직관적으로 구분할 수 있어야 한다. 그래서 단순히 기능만 추가하는 것이 아니라, UI적으로도 기능의 성격을 잘 드러낼 수 있도록 고도화 작업이 필요했다.

예를 들어, 데이터셋을 선택하는 Data Selector 노드와 이미지 처리를 담당하는 Image Processing 노드는 기본적으로 같은 사각형 모양이지만, 사용자가 한눈에 구분할 수 있도록 색상, 아이콘, 라벨링 방식을 차별화했다. 이렇게 하면 단순히 기능을 연결하는 도구를 넘어, 시각적으로도 워크플로우를 이해하기 쉽게 만드는 효과가 있다.

또한 기본 rete의 UI는 어디까지나 시작점에 불과했기 때문에, 우리 서비스에 맞는 디자인 시스템을 반영하는 것도 중요한 과제였다. 내부에서 사용하는 색상 팔레트, 폰트, 여백 규칙 등을 그대로 노드 UI에 녹여냈고, 그 결과 rete의 노드가 단순히 외부 라이브러리의 느낌이 아니라 우리 서비스 고유의 UI 컴포넌트처럼 보이게 만들 수 있었다.

이런 UI 고도화 과정을 거치면서, 사용자들은 복잡한 워크플로우를 직관적으로 파악할 수 있었고, 노드를 연결하는 과정에서 혼동이 줄어들었다. 나아가 단순히 “노드를 연결하는 툴”이 아니라, 서비스 전체의 경험을 확장하는 핵심 UI 요소가 될 수 있었다.

특히 이중에서도 각 노드별 처리 현황 등을 나타낼 수 있는 상태 UI는 기존 rete 코드에서, 상태를 따로 분리시켜 덮어쓰는 형식으로 확장시킬 수 있다. 또한 소켓이라는 개념이 rete에는 있는데, 이는 다음노드로 데이터를 전달할때 사용하는 기능이다. 즉, output, input이 있다는 것이고 input의 입장에서 여러 output이 들어올 때, 동적으로 ui가 바뀌도록 할 수 있도록

위와 같이 input이 받는 output의 개수에 따라 노드 UI가 자동으로 확장/축소되도록 구현했다. 이 과정에서 포트 간 간격이나 정렬 상태가 무너지지 않도록 레이아웃을 동적으로 계산했다. 덕분에 많은 노드가 연결되어도 UI가 어지럽지 않고, 플로우의 가독성을 유지할 수 있었다.


Rete.js의 기능적 고도화

기능적 고도화는 사실 내용이 좀 많긴하다.

노드 삭제, 추가 버튼대신 keyEvent로 적용
노드 정렬을, keyEvnet로 적용
데이터 타입 검증 로직
소켓 이동마다 데이터 현황표시
등등..

이중에서도 제일 핵심적인 것은 노드의 생성과 저장로직에 있었다. 생성과, 저장에서는 노드의 종류 저장할 데이터 전달할 데이터 모든것을 관리하고 있으며, rete엔진의 파이프라인에 맞춰서 동작하도록 설계했다.

예를들어, 새로운 노드가 생성되면, 엔진 파이프라인에 해당 노드가 자동으로 등록된다. 이후, 저장된 데이터를 불러오면, 엔진이 그대로 실행 파이프라인을 재구성하고 이를 통해 단순히 그림을 그려주는 툴이 아니라, 실제로 동작하는 데이터 파이프라인 도구가 될 수 있었다. 즉 쉽게 기본 예제 코드와 커스텀 코드는 다음과 같다.

  • 기본 노드: 단순 정의한 기능 흘려보내기
  • 커스텀 노드: “동적 처리 + UI/상태 반영 + 파이프라인 연결”

노드의 종류마다 다르지만 약식으로 rete 노드 생성자 코드를 보면,

// 노드 클래스 정의
export class MyNode extends ClassicPreset.Node<
  { [key in string]: ClassicPreset.Socket }, // Input Sockets
  { [key in string]: ClassicPreset.Socket }, // Output Sockets
  {
    [key in string]:
      | ButtonControl
      | ProgressControl
      | ClassicPreset.Control
      | ClassicPreset.InputControl<"number">
      | ClassicPreset.InputControl<"text">;
  }
> {
  constructor(id?: NodeId) {
    super("My Node", id); // 노드 이름
  }

  // builder: 노드 초기 구조
  async builder(node: ClassicPreset.Node) {
    const socket = new ClassicPreset.Socket("Number");

    // 출력 소켓 추가
    node.addOutput("out", new ClassicPreset.Output(socket));

    // 숫자 입력 컨트롤
    const inputControl = new ClassicPreset.InputControl("number", {
      initial: 0,
      change(value) {
        console.log("Input changed:", value);
      }
    });

    // 버튼 컨트롤
    const buttonControl = new ButtonControl("Randomize", () => {
      const val = Math.round(Math.random() * 100);
      inputControl.setValue(val);
    });

    // 컨트롤 등록
    node.addControl("input", inputControl);
    node.addControl("button", buttonControl);
  }

  // worker: 노드 처리 로직
  async worker(node: any, inputs: any, outputs: any) {
    const val = inputs["in"]?.[0] || 0;
    outputs["out"] = val;
  }
}
  • ClassicPreset.Node 상속 → 커스텀 노드 클래스 정의
  • builder()에서 입출력, 컨트롤 구성
  • worker()에서 실제 데이터 처리 작성
  • 나중에 에디터에 등록: await editor.addNode(new MyNode())

이렇게 코드가 구성이 되어있다. 여기서 실제 서비스에 적용하면서 여러부분을 바꿨는데,

약식으로 나타내면, 위와 같다. 크게 바뀐부분은 어떤 것에 중점을 두고 작업했냐는 것인데, 기본 예제 코드(MyNode)의 경우에는 주로 UI 중심, 에디터 화면에서의 상호작용에 초점을 맞췄다.

  • builder()에서 노드의 입력/출력 소켓과 컨트롤을 구성
  • worker()에서 입력 데이터를 받아 출력으로 전달하는 구조

즉, 화면에서 사용자가 입력을 변경하거나 버튼을 클릭하면 그 결과가 즉시 노드 출력으로 반영되도록 설계되어 있다. 컨트롤 자체는 주로 화면 렌더링 역할을 하고, 상태 관리보다는 사용자 이벤트 처리 중심이다.

반면, 실제 서비스 적용을 위해 수정한 TempNode의 경우에는 데이터 처리와 상태 관리 중심으로 설계가 바뀌었다.

  • 생성자에서 바로 입력/출력 소켓과 컨트롤을 등록하고
  • data() 메서드에서 입력 데이터를 처리하면서 컨트롤의 상태와 값을 직접 갱신한다.
  • 컨트롤은 값과 상태를 관리하며, 변경이 있을 때 콜백을 호출하도록 구성했다.

이렇게 하면 노드 내부 로직이 화면 렌더링과 독립적으로 동작할 수 있으며, 데이터 처리 흐름에 집중할 수 있다.

MyNode(기존 예제): UI 중심, 사용자가 보는 에디터 상호작용에 초점 TempNode(기능적 변경): 데이터 처리 + 상태 관리 중심, 서비스 로직과 노드 로직 결합에 초점

이러한 구조 변경은 실제 서비스 환경에서 노드가 화면 중심이 아니라, 데이터 처리 파이프라인 역할을 하도록 만들기 위해 필요한 설계 조정이다. 노드 자체가 다른 작업을 하고 있더라도, 독립적으로 데이터를 처리하고, 다음 노드로 전달을 해야하기 때문에, 트리거 발동을 최소화 시켜 직접 데이터를 전달하는 data() 함수에 상태를 넣어 노드만의 상태와 데이터를 컨트롤이 관리하도록 할 수 있다. 이후, UI 로직은 따로 컴포넌트를 분리해서 관리를 하게 되었다. 이결과로 각 노드의 상태들만 집합시켜 다음과 같은 구성을 이룰 수 있었다.

입력 확인과 상태 관리

  • 입력이 없으면 컨트롤 상태를 'init'로, 입력이 있으면 'completed'로 설정

effector 객체 구성

  • 입력 데이터(img_paths)와 컨트롤 옵션을 합쳐서 노드 처리 결과 객체(effector) 생성

전역 상태 저장

  • useWorkspaceStore에 effector 저장 → 다른 노드나 시스템에서 참조 가능

컨트롤 업데이트

  • 이전 값과 비교해 변경된 경우만 컨트롤에 값 설정 → 불필요한 트리거 최소화

출력 반환

  • out 객체로 img_paths와 effector를 반환 → 다음 노드로 전달

FSD 구조에서, ENTITIES로 분리한 노드 초기 구성을 비즈니스 데이터로 처리를 시켰고 하나의 노드 객체가 데이터를 독립적으로 처리할 수 있도록 구현하였다.

이 구조를 통해 생성된 노드는 Rete 엔진이 가동될 때, 흐름에 맞춰 독립적으로 데이터를 처리하고, React 렌더링까지 수행할 수 있다. 또한, 처리된 데이터를 백엔드로 보내고 받거나, 컨트롤 객체(ctrl)에서 종합적으로 관리할 수 있으며, 들어오는 데이터의 형식을 통일하고 예외를 분기하는 등의 작업도 보다 수월하게 수행할 수 있다.

Profile Picture

CHAN

과정은 복잡하되, 결과는 단순하게

Thank You for Visiting My Blog, Have a Good Day 😆
ⓒYoonchan Cho