EAI 에서는 연계 작업 단위를 하나의 인터페이스 라고 부릅니다. 또, 그 인터페이스를 구성하는 더 작은 단위를 프로세스 라고 하고, 각각의 프로세스에는 연계에 필요한 20여개의 어댑터 라는 것이 존재합니다. 그리고 이 프로세스들이 각각의 부모 프로세스 및 자식 프로세스를 가지며 이 정보를 토대로 서버 단에서 서로 다른 host 간의 데이터 연계를 돕습니다.

불과 5일전에 저에게 인터페이스를 GUI로 만들어내는 Drag and Drop 연계 워크플로우 에디터를 새로 만들어야 한다는 task가 내려왔습니다.

워크플로우 에디터는 화면 위에 노드를 올리고, 노드끼리 선으로 잇고, 줌과 팬으로 캔버스를 움직이고, 어댑터를 드래그해서 붙이고, 그렇게 그린 흐름을 저장하면 실제 연계 설정으로 변환 되는 기능입니다.

현재는 프로젝트가 v2 차세대 버전으로 재배포가 완료된 상황이였고, v1 때 존재하던 사내 바닐라 자바스크립트로 만든 워크플로우 에디터 라이브러리를 활용한 기능이 v2에는 여러가지 이유로 아직 해당 기능이 구현되지 않았던 상황이였습니다.

아래는 그 이유들입니다.

  1. 팀 내부에서 사용하지 않고 있는 peerDependencies가 줄줄이 딸려 왔고, css 파일이 지나치게 커서 프로젝트 번들 사이즈를 끌어올렸습니다.
  2. 애초에 다른 팀의 데이터 규격에 맞춰 설계된 라이브러리라, 우리 팀 데이터를 억지로 끼워 맞춰야 하는 수고스러움이 존재했습니다.
  3. 억지로 맞추다 보니 예상치 못한 사이드 이펙트가 뒤따랐습니다.
  4. v2 프로젝트의 디자인 컨셉과도 맞지 않았습니다.
  5. 무엇보다 커스텀이 어려웠습니다. 손볼 곳이 생길 때마다 라이브러리 안쪽과 싸워야 했죠.

결국 남의 규격에 맞춰진 도구를 우리 데이터에 욱여넣는 대신, 우리 팀 규격과 디자인에 맞는 에디터를 직접 만들기로 v2 프로젝트를 처음 만들기 시작했을 때 약속했습니다.

처음 일정을 가늠해봤을 때 머릿속에 떠오른 숫자는 6개월이었습니다.

머릿속으로 작업을 펼쳐보니 챙겨야 할 게 한둘이 아니었습니다.

먼저 오픈소스 캔버스 라이브러리를 검토하는 시간이 필요했습니다. 사전에 살펴봤을 때 react-flow라는 라이브러리가 있긴 했지만, 상용으로 쓰려면 라이선스가 필요했습니다. 그러면 결국 캔버스를 직접 구현하는 길로 가야 하는데, 거기에 딸린 결정이 또 줄줄이 이어졌습니다.

  1. 캔버스를 어떤 방식으로 구현할지, 좌표는 어떻게 계산할지
  2. 엘리먼트끼리 겹쳤을 때의 충돌 정책
  3. 어댑터별로 다른 폼과 유효성 검증
  4. 저장할 때의 유효성 검증
  5. 기존에 쌓여 있던 워크플로우 데이터를 어떤 좌표와 어떤 정책으로 다시 불러올지

등등..

여기에 노드 추가와 수정 같은 편집 기능까지 더하면, 6개월이라는 숫자가 그렇게 과한 추정은 아니었습니다.

그런데 결론부터 말하면, 동작하는 첫 버전을 단 3일 만에 만들었습니다.

완성된 워크플로우 에디터 전체 화면
구현 완료된 워크플로우 에디터 전체 화면

설계부터 시작해서 캔버스, 엣지라인, 충돌 계산 등 모든 로직을 거의 바닥부터 개발했습니다. 빨라진 이유는 제가 코드를 짠 방식이 아니라, 코드를 짜게 만든 방식이 달랐기 때문이라고 생각합니다.

이 글은 그 “방식”에 대한 기록입니다.


6개월이라는 숫자의 정체

먼저 이 기능이 왜 작지 않은지부터 짚고 싶습니다.

노드 기반 에디터는 화면 하나처럼 보이지만, 안을 열어보면 서로 다른 문제들이 겹쳐 있습니다.

  • 노드를 배치하고 줌과 팬으로 움직이는 캔버스
  • 노드와 노드를 잇는 선(엣지)의 경로를 계산하고, 노드가 움직이면 따라서 갱신하기
  • 노드끼리 겹치지 않게, 드롭 위치가 유효한지 판정하는 충돌 처리
  • 트리 형태의 데이터를 화면 좌표로 펼치는 레이아웃
  • 어댑터를 끌어다 캔버스에 떨어뜨리는 DnD 팔레트
  • 왼쪽 패널의 인터페이스 트리
  • 노드를 클릭하면 뜨는 어댑터 설정 폼과 유효성 검사
  • 그리다 만 상태를 들고 있다가, 저장할 때 서버 요청 형태로 변환하는 드래프트 상태
  • 인터페이스 규격에 맞는 도형 엘리먼트 별 상호작용 정책을 고려한 예외처리
노드와 엣지가 연결된 캔버스 상세

각각은 할 만합니다. 문제는 이게 한 화면 안에서 전부 맞물려 돌아가야 한다는 점입니다. 엣지는 노드 좌표를 알아야 하고, 충돌은 레이아웃을 알아야 하고, 저장은 드래프트 전체를 알아야 합니다. 하나를 건드리면 다른 하나가 깨지는, 상태가 잔뜩 얽힌 종류의 작업입니다.

이런 작업이 오래 걸리는 진짜 이유는 코드 양이 아니라 맥락의 양이라고 생각합니다. 어디를 고치면 어디가 영향받는지, 지금 짜는 함수가 기존 컨벤션과 맞는지, 이 타입이 다른 도메인에서 사용하는 중복타입이진 않은지. 사람이 머리에 담을 수 있는 맥락에는 한계가 있고, 그 한계를 넘으면 속도가 급격히 떨어집니다.

그래서 이번엔 맥락을 사람 한 명이 다 들고 있지 않는 구조를 먼저 제대로 만들기로 했습니다.


하네스 먼저, 코드는 나중에

저는 Claude Code를 쓰고 있었지만, “AI한테 시키면 빨라진다” 는 단순한 기대는 이미 여러 번 배신당한 적이 있었습니다. 프롬프트를 계속 할 수록 어느 순간 AI에게 아니 그니까 내말은 이라고 프롬프트하는 제 자신을 볼 수 있었습니다.

큰 작업을 통째로 던지면 AI는 금세 길을 잃습니다. 앞에서 정한 컨벤션을 뒤에서 어기고, 없는 파일 경로를 지어내고, 대화가 길어질수록 처음 맥락을 잊습니다. 이를 Context Rot 이라고 칭합니다.

그래서 이번엔 기능을 짜기 전에 AI가 일하는 환경 자체를 먼저 설계했습니다. 이걸 하네스(harness)라고 부르는데, 말 그대로 모델에 씌우는 마구입니다. 모델이 멋대로 달리지 않도록, 일을 어떻게 나누고 무엇을 지키며 어떤 순서로 진행할지를 저장소 안에 글로 박아두는 것입니다.

Reference

이 하네스의 뼈대는 superpowers라는 스킬 모음과 Claude Code의 서브에이전트 기능을 조합해 만들었습니다.

핵심은 세 가지였습니다.

1. 일하는 절차를 스킬로 못 박기

superpowers는 “무엇을 만들기 전에 먼저 브레인스토밍해라”, “구현 전에 테스트부터 써라”, “버그는 추측하지 말고 체계적으로 디버깅해라” 같은 작업 절차 자체를 스킬로 강제합니다.

여기에 더해, 프로젝트 전용 스킬과 규칙을 .claude/ 아래에 직접 작성했습니다.

.claude/
  CLAUDE.md          ← 프로젝트 컨벤션, 폴더 구조
  rules/             ← 행동 원칙, 스킬 라우팅, 서브에이전트 라우팅
  skills/            ← 테스트/커밋/리뷰/컨벤션 등 작업별 가이드
  agents/            ← 서브에이전트 정의

덕분에 “테스트 짜줘”라고만 해도 알아서 우리 팀 테스트 컨벤션을 따르고, “커밋해줘”라고 하면 프론트엔드 파일만 골라 우리 커밋 메시지 형식으로 정리합니다. 매번 같은 잔소리를 반복하지 않아도 되는 셈입니다.

2. 사고를 서브에이전트로 분리하기

가장 효과가 컸던 부분입니다. 하나의 긴 대화에서 전부 처리하는 대신, 일을 역할이 분리된 서브에이전트들에게 나눠 맡겼습니다.

scanner    → 파일 구조만 훑어서 요약 (원문은 버림)
analyzer   → 원인 분석, 우선순위 결정
architect  → 모듈 경계 / 의존성 방향 설계
coder      → 실제 코드 작성 (diff만 반환)
test-agent → 테스트 작성
reviewer   → 변경된 코드 리뷰

여기서 중요한 규칙이 하나 있었습니다. 각 서브에이전트는 파일 원문을 메인 대화로 가져오지 않고, 요약과 결과만 돌려준다는 것입니다.

scanner가 파일 수십 개를 훑어도 메인 대화에는 “이런 구조이고 의존성은 이렇다”는 요약만 남습니다. 덕분에 메인 대화는 끝까지 가벼웠고, 처음 정한 방향을 잊지 않았습니다. 맥락을 사람 한 명이 다 드는 게 아니라, 에이전트별로 쪼개서 격리한 셈입니다.

3. 모델을 계층으로 쓰기

모든 일에 가장 비싼 모델을 쓸 필요는 없었습니다.

  • 구현과 분석은 Sonnet에 (빠르고 충분했습니다)
  • 구조 설계와 최종 리뷰는 Opus에 (판단이 중요한 곳에만)

코드를 찍어내는 일은 빠른 모델에 맡기고, “이 설계가 맞는가”, “이 변경이 안전한가” 같은 판단이 필요한 길목에만 더 신중한 모델을 세웠습니다.


그래서, 3일 동안 무슨 일이 있었나

구조를 깔아두니 실제 작업은 의외로 단조로웠습니다. 제가 한 일은 코드를 직접 치는 게 아니라 오케스트레이터(orchestrator) 역할에 가까웠습니다. 심지어 릴리즈 일정이 다가오고 있어, 기존 버전에 대한 개발자 QA를 진행하며 만들었습니다.

대략 이런 흐름이었습니다.

  1. 브레인스토밍. 무엇을 만들지부터 정리했습니다. 노드 에디터를 라이브러리로 갈지 직접 구현할지, 상태는 어디에 둘지, 저장 포맷은 어떻게 할지. 곧바로 코드로 들어가지 않고 의도와 요구사항을 먼저 발라냈습니다.
  2. 스펙과 플랜 작성. 발라낸 요구사항을 한 번 더 좁혀서 스펙 문서로 정리하고, 그 스펙을 다시 구현 플랜으로 옮겼습니다. 무엇을 어떤 순서로 만들지가 글로 남아 있어야, 에이전트도 저도 중간에 길을 잃지 않았습니다.
  3. 구조 설계. 캔버스, 엣지, 충돌, 레이아웃을 각각 어떤 상태와 함수로 나눌지 경계를 먼저 그었습니다.
  4. 조각별 구현. 한 번에 다 짜지 않고, 유틸 하나를 만들고 검증한 뒤 다음 유틸로 넘어갔습니다. 깨지면 어디서 깨졌는지 바로 알 수 있게요.
  5. 검증과 리뷰. 핵심 로직은 테스트로 묶고, 변경 덩어리가 커지면 리뷰 에이전트를 한 번 태웠습니다.

이 마지막 단계에서 기억에 남는 순간이 있었습니다. AI가 짠 코드가 겉보기에는 멀쩡히 돌아가는데, 들여다보면 as any로 타입을 눌러버리거나 그 위에 린트를 무효화하는 주석을 달아둔 경우가 종종 있었습니다. 화면도 정상이고 빌드도 통과하니, 대충 보면 그냥 넘어가기 딱 좋은 코드였죠.

우리 팀 컨벤션에는 any를 사용하면 7일동안 커피를 돌려야한다 라는 컨벤션이 있습니다. anyas any, 린트 무효화 주석으로 문제를 덮는 건 당장 동작만 할 뿐, 나중에 더 큰 비용으로 돌아오기 때문입니다. 그래서 리뷰에서 이런 코드를 마주칠 때마다 타입을 제대로 잡고, 우회 주석을 걷어내고 다시 짜게 했습니다. 빠르게 나온 코드일수록 동작한다맞다를 구분하는 눈이 필요하다는 걸 이때 제일 크게 느꼈습니다. 또한 커밋을 하면 해당 커밋의 author가 Claude로 올라가지 않습니다.

AI가 코드를 짰다 하더라도 그 코드의 책임은 결국 개발자 자기 자신에게 있다는 것을 깨닫게 되었습니다.

노드를 드래그해 연결하는 동작

회고, 그리고 솔직한 경계

6개월에서 3일 이라는 기간은 단순히 일정 산정과 결과를 같은 잣대로 비교한 숫자가 아닙니다. 6개월은 기존 방식으로 가늠한 추정이었고, 3일은 동작하는 첫 버전까지 걸린 시간입니다. 그 사이에는 다듬고, 버그를 잡고, 엣지 케이스를 메우는 시간이 당연히 더 들어갔습니다. 그러니 “AI 덕분에 90배 빨라졌다”는 식으로 읽히는 건 저도 경계하고 싶습니다.

그럼에도 분명히 달라진 게 있었습니다. 예전 같으면 큰 기능 앞에서 시작 자체가 부담이었습니다. 일정을 맞추며 요구사항을 충족하기 위해 어디서부터 손대야 할지 막막하다 라는 생각부터 했던 것 같습니다. 하네스를 깔아두니 그 막막함이 줄었습니다. 막힌 곳을 통째로 떠안는 대신, 작은 단위로 잘라 에이전트에게 넘기고 결과만 검수하면 됐으니까요.

반대로 한계도 또렷했습니다.

  • 하네스를 짜는 것도 결국 일입니다. 규칙과 스킬을 글로 정리하고, 에이전트가 자꾸 어기면 규칙을 보강하는 데 그 자체로 시간이 듭니다. 한 번 쓰고 버릴 기능이라면 이 투자가 오히려 손해일 수 있습니다.
  • 검수는 결국 사람 몫입니다. AI가 빠르게 짠 코드일수록 “이게 정말 맞나”를 확인하는 눈은 더 또렷해야 했습니다. 빠른 게 곧 정확한 건 아니었습니다.
  • 결과물이 곧 제 실력은 아닙니다. 직접 한 글자씩 친 코드가 아니다 보니, 나중에 그 코드를 온전히 제 것으로 설명할 수 있어야 한다는 숙제가 남습니다.

그래도 이번 경험을 한 줄로 남긴다면 이렇게 쓰고 싶습니다.

AI에게 일을 잘 시키는 방법은 더 똑똑한 프롬프트를 던지는 게 아니었습니다. AI가 길을 잃지 않을 환경을 먼저 만들어 두는 것이었습니다.

하네스라는 말이 원래 마구를 뜻하는 게 꽤 잘 어울린다고 생각했습니다. 어떻게 보면 빠르게 달리는 말에게 필요한 건 채찍이 아니라, 방향을 잡아주는 고삐였네요.

이상으로 글을 마치겠습니다.