Created on

Flowtask

개요

웹에서 반복적으로 수행하는 업무에는 페이지를 열고, 원하는 요소를 찾고, 필요한 정보를 추출해 다른 서비스로 전달하는 과정이 자주 등장합니다.

이러한 작업을 직접 자동화하려면 브라우저를 제어하는 코드를 작성하고, 여러 작업을 연결하거나 실행 과정을 확인하기 위한 구조도 필요합니다.

Flowtask는 이러한 브라우저 작업을 코드 대신 노드로 구성하고 실행할 수 있도록 만든 브라우저 자동화 워크플로우 서비스입니다.

이 글에서는 Flowtask의 전체 구조와 주요 기능을 소개하고, 각 기능을 구현하면서 고민했던 내용을 함께 정리합니다.

  • React Flow와 Liveblocks를 활용한 워크플로우 편집과 실시간 공동 편집
  • 이전 노드의 결과를 다음 노드에서 활용하기 위한 데이터 흐름
  • Trigger.dev를 활용한 백그라운드 워크플로우 실행과 실시간 상태 관리
  • Stagehand와 Browserbase를 활용한 브라우저 자동화와 실행 과정 확인
  • Clerk를 활용한 인증과 Organization 기반 협업 환경 구성
  • 기능을 확장하면서 진행한 구조 개선과 문제 해결

사이트: https://flowtask.kr

GitHub: https://github.com/GitHubGW/flowtask

프로젝트 소개

Flowtask 메인 페이지

Flowtask는 브라우저 작업을 노드로 구성하고 실행하며, 여러 사용자가 함께 편집할 수 있는 워크플로우 서비스입니다.

사용자는 코드를 작성하는 대신 필요한 작업을 Canvas에 추가하고 원하는 순서대로 연결해 하나의 자동화 흐름을 만들 수 있습니다.

현재 지원하는 노드는 다음과 같습니다.

노드역할
시작워크플로우 실행 시작
URL 열기지정한 웹페이지로 이동
요소 찾기자연어로 설명한 작업을 수행할 수 있는 요소 탐색
동작 실행자연어 명령을 기반으로 하나의 브라우저 동작 실행
정보 추출웹페이지에서 필요한 정보 추출
AI 에이전트목표를 달성하기 위한 여러 브라우저 작업을 자율적으로 실행
이메일 전송워크플로우에서 만들어진 내용을 이메일로 전송

예를 들어 특정 웹페이지에서 필요한 정보를 찾아 이메일로 보내고 싶다면 다음과 같은 워크플로우를 만들 수 있습니다.

Flowtask 워크플로우 실행 예시

노드는 단순히 실행 순서만 표현하지 않습니다.

앞 단계에서 찾은 요소를 다음 단계의 입력으로 사용하거나, 추출한 데이터를 이메일 내용으로 전달하는 것처럼 노드 사이에 실제 데이터가 흐를 수 있도록 구성했습니다.

또한 여러 사용자가 같은 워크플로우를 실시간으로 편집할 수 있으며, 실행 중에는 각 노드의 상태와 입출력, 오류, 실행 시간을 확인할 수 있습니다.

브라우저 작업이 끝난 뒤에는 원격 브라우저에서 실제로 어떤 과정으로 자동화가 진행되었는지도 Replay를 통해 확인할 수 있습니다.

기술 스택

프로젝트의 주요 기술 스택은 다음과 같습니다.

영역기술
프레임워크Next.js 16, React 19, TypeScript
워크플로우 UIReact Flow
실시간 협업Liveblocks
백그라운드 작업Trigger.dev
브라우저 자동화Stagehand
원격 브라우저Browserbase
인증 / 조직 / 결제Clerk
데이터베이스Neon PostgreSQL, Drizzle ORM
이메일Resend
모니터링Sentry
UITailwind CSS 4, shadcn/ui, Storybook
테스트Vitest

React Flow는 노드와 엣지를 이용한 워크플로우 편집 UI를 담당하고, Liveblocks는 여러 사용자가 하나의 그래프를 실시간으로 공유하도록 합니다.

Trigger.dev는 시간이 오래 걸릴 수 있는 워크플로우 실행을 백그라운드에서 처리하고, Stagehand는 자연어 명령을 실제 브라우저 작업으로 연결합니다.

Browserbase는 Stagehand가 사용할 원격 브라우저 환경과 실행 기록을 제공합니다.

Clerk는 인증과 Organization, 구독을 담당하며 Organization을 Flowtask의 Workspace 경계로 사용했습니다.

프로젝트 전체 구조

Flowtask는 크게 워크플로우를 만드는 편집 영역과 완성된 워크플로우를 실제로 실행하는 영역으로 나누어 구성했습니다.

Flowtask
│
├─ 워크플로우 편집
│  ├─ React Flow → 노드·연결 편집
│  ├─ Liveblocks → 실시간 공동 편집
│  └─ PostgreSQL → 워크플로우 저장
│
└─ 워크플로우 실행
   ↓
   Trigger.dev
   백그라운드 실행·단계별 상태 관리
   ↓
   Stagehand
   자연어 기반 브라우저 자동화
   ↓
   Browserbase
   원격 브라우저 실행·Replay
   ↓
   실행 상태·입출력·오류
   ↓
   Flowtask
   Canvas·Console·Replay

Clerk
└─ 전체 과정의 인증·조직·구독 관리

사용자가 워크플로우를 편집할 때는 React Flow가 노드와 연결을 표현하고, Liveblocks가 여러 사용자의 변경 사항을 실시간으로 공유합니다. 워크플로우 정보와 Graph는 PostgreSQL에 저장합니다.

실행 버튼을 누르면 실제 브라우저 자동화는 Next.js 요청 안에서 처리하지 않고 Trigger.dev의 Background Task로 전달합니다.

각 노드는 실행 순서에 따라 처리되고, 브라우저 작업이 필요한 경우 Stagehand가 자연어 명령을 실제 브라우저 동작으로 연결합니다. Browserbase는 이 과정에서 필요한 원격 브라우저 환경과 실행 기록을 제공합니다.

백그라운드에서 변경되는 단계별 실행 상태는 다시 Flowtask로 전달해 Canvas와 Console에 반영하고, 실행이 끝난 뒤에는 각 단계의 입출력과 오류, Replay를 확인할 수 있도록 구성했습니다.

Clerk의 Organization은 이 전체 과정에서 Workspace를 기준으로 사용하며 인증, 워크플로우 접근, 실시간 협업, Replay의 권한을 연결합니다.

이제 이 구조를 기준으로 Flowtask의 주요 기능과 구현 과정을 하나씩 살펴보겠습니다.

주요 기능

Flowtask의 주요 기능은 크게 아래와 같습니다.

  • 워크플로우 편집
  • 실시간 협업
  • 노드 간 데이터 전달
  • 백그라운드 실행
  • 브라우저 자동화
  • 실행 상태 추적
  • Replay

노드 기반 워크플로우 편집

워크플로우 편집 화면은 크게 세 영역으로 구성했습니다.

Flowtask 워크플로우 편집 화면

Canvas에서는 노드를 추가하고 이동하거나 연결할 수 있습니다.

오른쪽 설정 영역에서는 선택한 노드의 입력값을 설정하고, 하단 Console에서는 현재 실행 상태와 이전 실행 기록을 확인합니다.

각 영역의 크기도 직접 조절할 수 있도록 구성해 편집할 때는 Canvas를 넓게 사용하고, 실행 결과를 확인할 때는 Console을 넓게 사용할 수 있도록 했습니다.

Canvas는 React Flow를 기반으로 구현했습니다.

React Flow는 노드와 엣지를 기반으로 Interactive Flow를 구성하고 커스텀 노드를 정의할 수 있지만, Flowtask에서 어떤 Graph를 실행 가능한 워크플로우로 정할 것인지는 별도로 정의가 필요했습니다.

현재 Flowtask에서는 하나의 노드에 하나의 입력과 출력 연결만 허용합니다.

따라서 범용적인 분기형 보다는 하나의 실행 흐름이 이어지는 선형 구조에 가깝습니다.

새로운 엣지를 연결할 때는 다음 조건을 확인합니다.

새로운 연결 요청
↓
자기 자신을 연결하는가?
├─ Yes → 거부
Source에 이미 출력 연결이 있는가?
├─ Yes → 거부
Target에 이미 입력 연결이 있는가?
├─ Yes → 거부
새로운 연결이 순환 구조를 만드는가?
├─ Yes → 거부
↓
연결 허용

UI에서 잘못된 연결을 제한하는 것만으로 실행 가능성을 보장하지 않고, 실제 실행 요청 시에도 Graph를 다시 검증합니다.

Canvas
사용자 입력 단계의 연결 제한
↓
실행 요청
그래프 유효성 재검증
↓
유효한 Graph면 Background Task로 전달

현재 서버 실행 경계에서는 시작 노드와 액션 노드의 존재 여부, 연결되지 않은 액션 노드, Cycle 여부를 확인합니다.

여러 사용자가 하나의 워크플로우를 실시간으로 편집하기

React Flow만으로는 같은 워크플로우를 여러 사용자가 동시에 편집하는 상태를 공유할 수 없습니다.

이를 위해 Liveblocks를 함께 사용했습니다.

사용자 A의 React Flow
노드 이동 · 추가 · 연결 변경
↓
Liveblocks Room
↓
공유된 노드 / 엣지 상태 변경
↓
사용자 B의 React Flow
변경된 Graph 실시간 반영
Flowtask 실시간 공동 편집

한 사용자가 노드를 추가하거나 이동하고 연결을 변경하면 다른 사용자 화면에도 변경된 Graph가 실시간으로 반영됩니다.

그래서 다른 사용자의 위치는 Cursor로 표시하고, 현재 접속 중인 사용자는 Avatar Stack으로 보여주도록 구성했습니다.

워크플로우 ID는 Liveblocks Room ID로 사용합니다.

Workflow
id: abc
↓
Liveblocks Room
id: abc

워크플로우를 생성하면 PostgreSQL에 레코드를 생성하는 것과 함께 동일한 ID의 Liveblocks Room을 만들고, 해당 Organization Group에 편집 권한을 부여합니다.

이를 통해 하나의 워크플로우와 하나의 실시간 협업 공간이 동일한 식별자를 기준으로 연결됩니다.

이전 노드의 결과를 다음 노드에서 사용하기

노드를 연결하면 어떤 순서로 실행할지는 알 수 있지만, 실제 자동화를 만들려면 앞 단계에서 만든 결과를 다음 단계에서 사용할 수 있어야 합니다.

예를 들어 웹페이지에서 필요한 정보를 추출한 뒤 그 내용을 이메일로 보내는 워크플로우를 생각해 볼 수 있습니다.

정보 추출
"오늘의 환율은 1,390원입니다."
↓
이메일 전송
"오늘의 환율은 1,390원입니다."을 본문으로 사용

단순히 두 노드를 연결하는 것만으로는 정보 추출 노드에서 만들어진 값이 자동으로 이메일 전송 노드의 입력값이 되지는 않습니다.

따라서 Flowtask에서는 이전 노드의 Output을 다음 노드의 Input으로 선택해서 사용할 수 있도록 구성했습니다.

이전 노드 출력값 선택

예를 들어 이메일 전송 노드의 본문을 설정할 때 앞에서 실행되는 정보 추출 노드의 결과를 선택할 수 있습니다.

사용자가 이전 노드의 결과를 선택하면 입력값에는 실제 결과 대신 해당 값을 가리키는 표현식이 저장됩니다.

{{ extractNodeId.extraction }}

아직 워크플로우를 실행하지 않았기 때문에 정보 추출 노드가 어떤 값을 반환할지는 알 수 없습니다.

그래서 편집할 때는 실제 값을 저장하는 대신 이 노드가 실행되면 여기서 만들어진 결과를 사용한다는 참조만 저장합니다.

워크플로우를 편집할 때

정보 추출
↓
결과를 참조하도록 연결
↓
이메일 전송
본문: {{ extractNodeId.extraction }}

그리고 워크플로우가 실제로 실행되면 앞에서부터 각 노드를 순서대로 처리하면서 결과를 저장합니다.

정보 추출 실행
↓
결과 저장
"오늘의 환율은 1,390원입니다."
↓
이메일 전송 실행 직전에 {{ extractNodeId.extraction }} 값을 확인
↓
저장된 실제 결과로 변환 후, 이메일 본문으로 전달

즉, 사용자가 편집할 때 저장한 값과 실제 실행할 때 전달되는 값은 다음처럼 달라집니다.

편집할 때
{{ extractNodeId.extraction }}
↓
실행할 때
"오늘의 환율은 1,390원입니다."

이 변환은 각 노드를 실행하기 직전에 이루어집니다.

이전 노드의 결과는 노드 ID를 기준으로 저장하기 때문에 특정 노드의 어떤 Output을 사용할지도 구분할 수 있습니다.

예를 들어 요소 찾기 노드가 여러 정보를 반환한다면 다음처럼 중첩된 값도 참조할 수 있습니다.

{{ observeNodeId.matches[0].selector }}

이 경우에는 요소 찾기 노드의 결과 중 첫 번째 항목의 selector 값을 찾아 실제 입력값으로 변환합니다.

사용자가 이런 표현식을 직접 작성할 필요는 없습니다.

현재 노드보다 앞에 연결된 노드들을 탐색하고, 각 노드에서 사용할 수 있는 Output을 선택지로 보여주도록 구성했습니다.

현재 노드
↓
앞에 연결된 노드 확인
↓
사용할 수 있는 Output 확인
↓
사용자가 원하는 값 선택
↓
Input에 참조 표현식 저장

중첩된 객체와 배열의 값도 참조할 수 있으며, 객체나 배열 자체를 선택한 경우에는 JSON 문자열로 변환합니다.

값이 존재하지 않거나 null인 경우에는 빈 문자열로 처리합니다.

결과적으로 Flowtask에서는 두 가지 관계를 나누어 표현했습니다.

엣지
→ 어떤 노드 다음에 어떤 노드를 실행할 것인가

Output 참조
→ 앞 노드에서 만들어진 어떤 값을 사용할 것인가

덕분에 사용자는 자동화 코드를 직접 작성하지 않아도 앞 단계에서 만들어진 결과를 선택해 다음 단계의 입력으로 연결할 수 있습니다.

워크플로우를 백그라운드에서 실행하기

브라우저 자동화는 일반적인 데이터 변경 요청보다 실행 시간이 길어질 수 있습니다.

페이지를 열고, 요소를 찾고, 동작을 수행하고, 데이터를 추출하는 여러 단계가 이어지며 AI 에이전트는 하나의 목표를 위해 여러 브라우저 작업을 수행할 수도 있습니다.

이런 작업은 오래 걸릴 수 있기 때문에 하나의 Next.js 요청 안에서 처리하기보다 실행 요청과 실제 워크플로우 실행을 분리했습니다.

사용자
실행 버튼 클릭
↓
Next.js
Graph 검증 및 실행 요청
↓
Trigger.dev
백그라운드 실행
↓
각 노드 실행

사용자가 실행을 요청하면 현재 Graph를 검증하고 PostgreSQL에 저장한 뒤 Trigger.dev에 Background Task를 요청합니다.

여기서 저장되는 Graph는 사용자가 실행을 요청한 시점의 워크플로우 상태입니다.

워크플로우 실행 요청과 백그라운드 실행

Trigger.dev에서는 Graph를 실행 가능한 순서로 변환한 뒤 각 노드를 순차적으로 실행합니다.

이 구조를 통해 Next.js는 실행을 요청하는 경계, Trigger.dev는 오래 걸릴 수 있는 작업의 실행과 상태 관리를 담당하도록 역할을 나눴습니다.

AI 모델을 활용한 자연어 기반 브라우저 자동화

브라우저 자동화에서는 Stagehand와 Browserbase를 서로 다른 역할로 사용했습니다.

Stagehand
자연어 명령을 실제 브라우저 동작으로 변환
→ 무엇을 실행할지 담당

Browserbase
원격 Browser Session 제공 및 기록
→ 어디에서 실행할지 담당

Stagehand는 자연어를 이용해 페이지를 탐색하거나 동작을 수행하고 데이터를 추출하는 자동화로 사용했습니다.

Stagehand가 자연어 명령을 해석하고 브라우저 작업을 수행하기 위한 모델로는 Google Gemini 2.5 Flash를 적용했습니다.

const stagehand = new Stagehand({
  env: 'BROWSERBASE',
  apiKey: BROWSERBASE_API_KEY,
  model: 'google/gemini-2.5-flash',
});

브라우저 자동화에 사용할 모델을 선택할 때는 단순히 모델의 성능만 보기보다 작업의 복잡도와 응답 속도, 실행 비용을 함께 고려했습니다.

GPT나 Claude 계열의 모델처럼 복잡한 추론과 여러 단계의 자율적인 작업에 강점을 가진 모델도 사용할 수 있지만, Flowtask에서는 요소를 찾고 동작을 수행하거나 정보를 추출하는 비교적 짧은 브라우저 작업이 반복적으로 실행됩니다.

Gemini 2.5 Flash는 긴 Context를 지원하면서도 응답 속도와 비용 측면에서 부담이 비교적 적어, 이러한 작업을 처리하는 기본 모델로 적합하다고 판단했습니다.

하나의 워크플로우 안에서도 여러 노드가 각각 Stagehand를 사용할 수 있기 때문에, 모델을 한 번 호출했을 때의 성능뿐 아니라 워크플로우 전체에서 반복적으로 호출될 때의 비용과 응답 속도도 중요했습니다.

Flowtask에서는 브라우저 자동화 과정을 여러 노드로 나누었으며, 이 중 자연어 기반 작업이 필요한 노드에서 Stagehand를 활용했습니다.

노드역할
요소 찾기자연어로 설명한 작업을 수행할 수 있는 요소 탐색
동작 실행자연어 명령을 기반으로 하나의 브라우저 동작 실행
정보 추출웹페이지에서 필요한 정보 추출
AI 에이전트목표를 달성하기 위한 여러 브라우저 작업을 자율적으로 실행

Browserbase는 이러한 자동화가 실행되는 원격 Browser Session과 실행 기록을 제공합니다.

Stagehand를 이용한 브라우저 자동화 실행

백그라운드 실행 상태를 실시간으로 확인하기

워크플로우를 Background Task로 분리하면서 백그라운드에서 진행되는 상태를 다시 React UI에 연결해야 하는 문제가 생겼습니다.

사용자는 실행 버튼을 누른 뒤 현재 어떤 노드가 실행되고 있는지 확인할 수 있어야 합니다.

아래는 워크플로우 실행 상태를 표현한 예시입니다.

URL 열기   ✓ Done
↓
요소 찾기   ✓ Done
↓
동작 실행   ● Running
↓
정보 추출   ○ Pending
↓
이메일 전송 ○ Pending

각 Step의 상태를 다음과 같이 관리했습니다.

pending
↓
running
↓
done 또는 failed

상태뿐 아니라 실제 실행에 사용된 Input, Output, 실행 시간, 오류 정보도 함께 관리합니다.

interface RunStep {
  nodeId: string;
  type: WorkflowStepType;
  title: string;
  status: 'pending' | 'running' | 'done' | 'failed';
  input?: Record<string, string>;
  output?: unknown;
  durationMs?: number;
  error?: string;
}

input은 사용자가 입력한 원본 문자열이 아닌 Output Token 보간이 끝난 뒤 실제 Executor에 전달된 값입니다.

워크플로우 단계별 실행 상태

실행 중인 노드와 완료된 노드는 Canvas에서 바로 구분할 수 있으며 엣지에도 실행 결과를 반영합니다.

실행 결과와 Browser Replay 확인하기

실행이 끝났다고 해서 성공과 실패만 보여주면 여러 노드가 연결된 자동화에서 문제가 발생했을 때 원인을 찾기 어렵습니다.

그래서 하단 Console에서는 실행의 세부 정보를 확인할 수 있도록 구성했습니다.

Flowtask 실행 Console

Console에서는 다음 정보를 확인할 수 있습니다.

  • 과거 워크플로우 실행 기록
  • 전체 실행 상태
  • 노드별 실행 상태
  • 실제 Executor에 전달된 Input
  • 노드가 반환한 Output
  • 실행 시간
  • 오류 메시지
  • Browser Session Replay

Console의 Input과 Output이 데이터 관점의 실행 기록이라면 Browserbase Replay는 실제 브라우저 동작 관점의 실행 기록입니다.

Flowtask Browser Session Replay

Replay를 구현하면서는 워크플로우 실행 완료와 Replay 준비 완료의 시점이 다르다는 문제도 있었습니다.

워크플로우 실행 완료
↓
Browserbase Session 종료
↓
Replay 처리
↓
Replay 준비 완료

Replay가 아직 준비되지 않은 경우 이를 오류로 처리하지 않고 Route Handler에서 202 Accepted를 반환했습니다.

Replay 요청
↓
응답 상태 확인
├─ 200 OK
│  → Replay 준비 완료
│  → HLS 재생
│
├─ 202 Accepted
│  → Replay 준비 중
│  → 2초 후 다시 요청
│
└─ 그 외
   → Error

클라이언트에서는 2초 간격으로 다시 확인하고 최대 3분의 Timeout을 두었습니다.

Replay가 준비되면 HLS Playlist를 가져와 hls.js로 재생하고, Native HLS를 지원하는 환경에서는 브라우저의 기본 기능을 사용합니다.

여기서 HLS는 영상을 여러 구간으로 나누어 HTTP를 통해 스트리밍하는 방식입니다.

hls.js는 브라우저에서 HLS 재생을 지원하기 위한 JavaScript 라이브러리입니다.

HLS Playlist
↓
hls.js로 재생 가능한가?
├─ Yes → hls.js로 재생
│
└─ No
   ↓
   브라우저가 HLS를 직접 지원하는가?
   ├─ Yes → video src로 재생
   └─ No  → 재생 미지원

인증과 조직 기반 워크스페이스

Flowtask에서는 Clerk를 사용해 사용자 인증과 조직 기반의 워크스페이스를 구성했습니다.

사용자는 회원가입과 로그인 후 서비스를 이용할 수 있으며, 로그인한 사용자 정보를 기준으로 워크플로우 생성과 조회, 실행 같은 기능에 접근할 수 있습니다.

회원가입 · 로그인
↓
Clerk
사용자 인증
↓
Flowtask
워크플로우 생성 · 편집 · 실행

또한 여러 사용자가 하나의 워크플로우를 함께 편집할 수 있도록 Clerk의 Organization 기능을 활용했습니다.

Flowtask 조직 기반 워크스페이스

사용자는 자신이 속한 Organization에서 워크플로우를 생성하고 관리할 수 있으며, 사용자를 Organization에 초대해 같은 Organization의 워크플로우를 함께 편집할 수 있습니다.

Organization
├─ 사용자 A
├─ 사용자 B
│
└─ Workflows
   ├─ Workflow A
   ├─ Workflow B
   └─ Workflow C

워크플로우 역시 사용자 개인이 아니라 Organization을 기준으로 관리했습니다.

Database에서 워크플로우를 조회하거나 변경할 때도 현재 로그인한 사용자의 Organization ID를 함께 확인합니다.

db.select()
  .from(workflows)
  .where(and(eq(workflows.id, workflowId), eq(workflows.organizationId, organizationId)));

이를 통해 같은 서비스 안에서도 Organization별로 워크플로우를 분리하고, 현재 사용자가 속한 Organization의 데이터만 조회하고 변경할 수 있도록 구성했습니다.

실시간 공동 편집에서도 같은 Organization을 기준으로 사용자를 연결해, Clerk에서 관리하는 Organization과 실제 협업이 이루어지는 공간이 동일한 조직 단위를 사용하도록 구성했습니다.

결과적으로 Clerk를 사용자 인증과 Organization을 기반으로 협업 단위를 구성하는 역할로 활용했습니다.

문제 해결 및 구조 개선

프로젝트의 기능이 늘어나면서 새로운 기능을 추가하는 것뿐 아니라 기존 구조를 정리하고 구현 과정에서 발생한 문제를 해결하는 작업도 함께 진행했습니다.

실행 영역의 책임 분리

브라우저 노드와 실행 상태가 늘어나면서 하나의 Background Task에서 모든 처리를 담당하면 실행 흐름을 이해하기 어려워졌습니다.

그래서 워크플로우 실행 영역의 책임을 나눴습니다.

run-workflow-task
전체 실행 흐름 조율
↓
execute-workflow-step
입력 보간 · Executor 실행
↓
workflow-step-executors
노드별 실제 작업

실행 상태는 별도의 Tracker가 담당합니다.

create-run-step-tracker
Step 실행 상태 관리
├─ pending
├─ running
├─ done
└─ failed
↓
Trigger.dev Metadata에 반영

Browser Session 생성과 재사용, 정리 역시 별도의 책임으로 분리했습니다.

Browser Session 관리
↓
필요한 시점에 Session 생성
↓
같은 워크플로우에서 재사용
↓
실행 종료 시 Session 정리

결과적으로 전체 Task는 세부 구현보다 워크플로우의 전체 실행 순서를 조율하는 역할에 집중할 수 있게 되었습니다.

노드 정의와 실행 로직 분리

노드 종류가 늘어나면서 Label, Description, Input, Output 같은 정보가 여러 컴포넌트에 중복되지 않도록 workflowStepRegistry에서 공통으로 관리했습니다.

workflowStepRegistry
├─ Type
├─ Kind
├─ Label
├─ Description
├─ Icon
├─ Inputs
└─ Outputs

반면 실제 실행 함수는 workflowStepExecutors에서 별도로 관리합니다.

Node Type
├─ workflowStepRegistry
│  → UI · Input · Output 정의
│
└─ workflowStepExecutors
   → 실제 실행 로직

Canvas와 설정 UI에서는 실행 구현을 알 필요 없이 Registry를 사용하고, 실행기에서는 UI 구성을 알 필요 없이 Type에 맞는 Executor를 찾도록 역할을 나눴습니다.

Server Action의 Redirect가 실패처럼 처리된 문제

워크플로우 생성 이후 페이지를 이동하는 과정에서도 문제가 있었습니다.

처음에는 Server Action에서 워크플로우를 생성한 뒤 바로 redirect()를 호출했습니다.

Server Action
Database 생성
Liveblocks Room 생성
↓
redirect()

하지만 Client에서 Server Action 호출 전체를 try/catch로 처리하고 있었고, Next.js의 redirect()는 단순히 페이지를 이동하는 것이 아니라 현재 실행 흐름을 중단시키는 방식으로 동작하기 때문에, 워크플로우가 정상적으로 생성됐음에도 Client에서는 실패처럼 처리될 수 있었습니다.

Server Action
워크플로우 생성 성공
↓
redirect() 실행
↓
Server Action의 실행 흐름 중단
↓
Client에서 실패처럼 처리

그래서 Server Action에서는 생성된 워크플로우 ID만 반환하고, Client에서 성공 처리를 마친 뒤 Router로 이동하도록 변경했습니다.

Server Action
워크플로우 생성 성공
↓
워크플로우 ID 반환
↓
Client
성공 처리
↓
router.push()

이 문제를 통해 Server Action을 일반적인 비동기 함수처럼만 생각하기보다, redirect()와 같은 Framework 기능이 실행 흐름에 어떤 영향을 주는지도 함께 고려해야 한다는 점을 알 수 있었습니다.

여러 실행 영역의 오류 추적

Flowtask의 워크플로우 실행은 하나의 환경에서 끝나지 않습니다.

사용자는 Browser에서 워크플로우를 만들고 실행하지만, 요청은 Next.js Server를 거쳐 Trigger.dev의 Background Task로 전달되고 실제 브라우저 자동화는 별도의 원격 Browser에서 이루어집니다.

사용자
↓
Client
↓
Next.js Server
↓
Trigger.dev Background Task
↓
원격 Browser 자동화

따라서 문제가 발생했을 때 단순히 Client의 오류만 확인해서는 어느 실행 영역에서 문제가 발생했는지 파악하기 어려웠습니다.

이를 위해 Sentry를 Client와 Next.js Server뿐 아니라 Trigger.dev의 Background Task에도 연결했습니다.

Next.js에서는 실행 환경에 따라 Client, Node.js Server, Edge Runtime의 Sentry 설정을 분리하고, 전역에서 처리되지 않은 오류와 Request 처리 중 발생한 오류를 수집하도록 구성했습니다.

Flowtask
├─ Client
│  └─ UI 오류 · 전역 오류 · Session Replay
├─ Next.js Server
│  └─ Request · Server 오류
└─ Trigger.dev Background Task
   └─ 워크플로우 실행 오류
↓
Sentry

특히 워크플로우 실행은 Next.js에서 요청만 전달한 뒤 실제 처리가 Trigger.dev에서 계속되기 때문에, Background Task의 오류를 별도로 수집할 필요가 있었습니다.

Trigger.dev의 전역 실패 Hook에서 오류를 Sentry로 전달하고, 어떤 실행에서 발생한 문제인지 확인할 수 있도록 Task ID와 Run ID 등의 정보를 함께 기록했습니다.

tasks.onFailure(({ error, task, ctx }) => {
  Sentry.captureException(error, {
    tags: {
      'trigger.task_id': task,
      'trigger.run_id': ctx.run.id,
      'trigger.environment': ctx.environment.type,
    },
  });
});

이렇게 하면 단순히 오류 메시지만 확인하는 것이 아니라 어떤 Background Task의 어떤 Run에서 발생한 오류인지 함께 추적할 수 있습니다.

워크플로우 생성, 삭제, 실행 요청, 실행 중지처럼 주요 동작도 Sentry의 구조화된 로그로 기록했습니다.

예를 들어 워크플로우 실행을 요청할 때는 Workflow ID와 Organization ID, Trigger.dev에서 생성된 Run ID, 실행할 Node 수를 함께 남깁니다.

Sentry.logger.info(WORKFLOW_LOG_EVENTS.RUN_REQUESTED.message, {
  'event.name': WORKFLOW_LOG_EVENTS.RUN_REQUESTED.name,
  'workflow.id': workflowId,
  'organization.id': orgId,
  'trigger.run_id': handle.id,
  'workflow.node_count': graph.nodes.length,
});

이를 통해 오류 자체뿐 아니라 어떤 워크플로우와 Background Run에서 발생한 문제인지 추적할 수 있도록 구성했습니다.

테스트

모든 기능을 테스트 대상으로 만들기보다 입력과 출력이 명확하고 워크플로우 실행에 중요한 핵심 로직을 순수 함수로 분리해 단위 테스트를 작성했습니다.

주요 테스트 대상은 다음과 같습니다.

  • 초기 워크플로우 Graph 생성
  • 워크플로우 기본 이름 생성
  • Output Token 보간
  • 워크플로우 Graph 검증

특히 Output Token은 이전 노드의 실행 결과를 다음 노드의 입력으로 전달하는 데 사용되기 때문에 단순 문자열뿐 아니라 중첩된 객체와 배열의 경로도 올바르게 탐색하는지 확인했습니다.

{{ node-a.matches[0].selector }}
↓
이전 노드의 Output 탐색
↓
matches[0].selector 접근
↓
실제 값으로 변환

Graph Validation에서는 시작 노드나 액션 노드가 없는 경우, 연결되지 않은 액션 노드가 존재하는 경우, Cycle이 발생하는 경우처럼 실행할 수 없는 Graph를 올바르게 차단하는지 검증했습니다.

Flowtask Vitest Coverage 결과

워크플로우의 핵심 로직을 단위 테스트로 검증했으며, 테스트 대상으로 지정한 범위에서 Statements, Branches, Functions, Lines 모두 100% Coverage를 확인했습니다.

이번 프로젝트에서는 외부 서비스 의존성이 적은 핵심 비즈니스 로직을 분리하고, 해당 로직의 동작을 단위 테스트로 검증하는 데 집중했습니다.

향후 확장 방향

Flowtask는 프로젝트를 진행하면서 현재 구현 범위와 이후 확장할 수 있는 기능을 구분해 설계했습니다.

현재 Canvas에서는 하나의 노드에 하나의 입력과 출력 연결만 허용하기 때문에 하나의 흐름으로 이어지는 워크플로우를 기준으로 동작합니다.

현재
A
↓
B
↓
C
↓
D
확장 가능한 형태
  A
  ↓
  B
 ↙ ↘
C   D

현재 UI는 여러 경로로 나뉘는 구조가 아니라 하나의 흐름으로 이어지는 워크플로우로 설계되어 있습니다.

Graph Validation 역시 현재 워크플로우 실행에 필요한 조건을 중심으로 구현되어 있습니다.

향후 기능과 실행 구조를 확장한다면 다음과 같은 부분을 추가로 고려할 수 있을 것 같습니다.

  • 여러 Branch를 가지는 워크플로우
  • 조건에 따른 분기
  • 서로 의존하지 않는 노드의 동시 실행
  • 노드별 필수 Input에 대한 실행 전 검증
  • 실패한 Step부터 다시 실행하는 기능
  • 전체 사용자 시나리오에 대한 E2E 테스트

마치며

Flowtask는 React Flow를 이용해 노드를 만들고 연결하는 워크플로우 편집기에서 시작했습니다.

하지만 프로젝트를 구현하면서 실제로 더 많은 고민이 필요했던 부분은 워크플로우를 편집하는 것부터 함께 수정하고, 실행하고, 결과를 확인하는 과정까지 하나의 흐름으로 연결하는 것이었습니다.

워크플로우를 만든다
↓
다른 사용자와 함께 수정한다
↓
실행한다
↓
현재 진행 상황을 확인한다
↓
결과와 실제 실행 과정을 확인한다

편집 상태와 실행 상태를 분리하고, 오래 걸리는 작업을 Background Task로 넘긴 뒤 실행 상태를 다시 UI에 반영하는 과정에서 여러 실시간·비동기 시스템 사이의 경계를 직접 다뤄볼 수 있었습니다.

단순한 기능 구현을 넘어, 여러 실시간·비동기 시스템의 상태를 연결하고 일관된 사용자 경험으로 만드는 과정을 경험할 수 있었던 프로젝트였습니다.

참고 자료