계약(Contract)을 먼저 작성하고 코드를 나중에 구현하는 방식: 저희가 기능을 두 번이나 재구축하는 것을 막아준 워크플로우
요약
기능 구현 전 '계약(Contract)'을 먼저 작성하여 제품 관점의 요구사항을 명확히 하는 워크플로우를 소개합니다. 이를 통해 기술적 구현 단계에서 발생할 수 있는 불필요한 재구축을 방지하고 UX 결정을 선제적으로 내릴 수 있습니다.
핵심 포인트
- 코드 작성 전 기능의 트리거, 출력, 상호작용을 정의하는 계약서 작성
- 기술적 구현과 제품 결정(UX) 사이의 간극을 조기에 발견
- 불필요한 코드 재구축 및 설계 변경 방지
- 정적 프로토타입을 통해 실제 컴포넌트 트리에서의 피드백 비용 절감
한 줄 변경처럼 보였던 요청
기능 요청이 들어왔는데, 종이에만 보면 계획하기에는 너무 간단해 보이는 수준이었습니다. "치료사들이 차트를 열고, 특정 객체(objective)를 클릭한 다음, 그 목표(target)와 단계(step)로 상세하게 들여다볼 수 있게 해주세요."
기술적으로는 기본적으로 한 줄짜리 작업이었습니다. 데이터 모델에는 이미 모든 것이 갖춰져 있었습니다—목표, 단계, 세션별 점수까지 모두 연결되어 있었죠. 차트 라이브러리도 코드베이스에 존재했습니다. 클릭 가능한 대상(click target)은 단 하나의 속성만 추가하면 바로 작동할 수 있는 상태였습니다. 만약 제가 이 작업을 "몇 개의 파일이 변경되는지"로 측정했다면, 하루가 걸릴 것이고 어쩌면 그보다 적을 거라고 말했을 겁니다.
하지만 "기술적으로 간단하다"는 것과 "처음 시도에 올바르다"는 것은 기능의 무관한 속성입니다. 그리고 이 기능은 전적으로 사용자 경험(UX)에 의해 결정되는 부분이었습니다. 알 수 없는 수의 목표들을 어떻게 나란히 배치해야 너무 작은 차트들의 벽으로 붕괴되지 않을까요? 섹션이 스크롤을 지나가도 고정되어 있어야 할까요, 아니면 그저 화면 공간만 차지하게 될까요? 치료사가 다섯 명의 아이들에 대한 세션을 검토할 때 실제로 중요한 것을 알아차리려면, 차트가 보여주는 것 외에 무엇을 말해야 할까요?
이런 것들은 종이에 아무리 많이 적어도 제대로 결정되지 않습니다. 그것은 직접 보고, 클릭해보고, 눈앞의 것에 솔직하게 반응하면서 결정됩니다—그리고 이것이야말로 정적인 HTML 파일에서는 얻기 쉽지만, 실제 상태(state), 실제 i18n 키(i18n keys), 그리고 연결된 실제 테스트 스위트가 있는 실제 컴포넌트 트리에서는 얻기 비용이 많이 드는 피드백입니다.
그래서 이번에는 제가 다르게 접근했습니다. 기능 자체가 아니라 HTML 파일로 풀 리퀘스트(pull request)를 열었습니다. 전체 과정과—더 중요한 것은—제품 관점에서 실제로 무엇을 되찾아 왔는지에 대해 말씀드리겠습니다.
1단계: 편집기를 열기 전에 계약서를 작성하기
어떤 코드를 건드리기 전에, 저는 문제의 형태를 일반 언어로 문서화합니다. 단순히 "이 기능을 만드세요"가 아니라 실제 계약서처럼요. 무엇이 이 기능을 트리거하는지, 어떤 데이터가 필요한지, '완료' 상태가 어떻게 보이는지, 그리고 결정적으로 아직 무엇을 모르는지를 명시합니다.
이 기능의 경우, 계약서는 대략 다음과 같았습니다:
- 트리거 (Trigger): 기존 그래프 화면에서 목표 (objective)의 제목을 클릭함.
- 출력 (Output): 모달 (modal) — 대상 (target)당 하나의 영역 차트 (area chart, 절대값, 나란히 배치), 그리고 그 아래에 단계 (step)당 하나의 누적 막대 차트 (stacked bar chart)를 포함하는 대상당 하나의 카드.
- 상호작용 (Interaction): 대상의 제목을 클릭하면 모달이 해당 대상의 단계 카드 위치로 스크롤됨.
- 명시적 비목표 (Explicit non-goal): 스키마 (schema) 변경 없음. 기존 테이블들이 이미 이를 올바르게 모델링하고 있었음; 유일한 간극은 현재 쿼리 (query)가 대상/단계의 세분성 (granularity)을 유지하는 대신 모든 것을 목표 수준으로 평탄화 (flatten)했다는 점임.
에디터를 열기 전에 계약서를 작성하는 것은 구현 중간이 아니라 초기에 간극을 드러내 줍니다. 이 과정에서 두 가지 실제 제품 관련 질문이 즉시 도출되었습니다: 대상이 두세 개보다 많아지면 대상 행 (target row)이 어떻게 동작하는가, 그리고 아래의 단계 카드들을 스크롤할 때 해당 행이 계속 보이는가 아니면 전체가 함께 스크롤되는가? 이 질문들에 대한 "정답"인 기술적 답변은 없습니다. 이것들은 제품 결정 (product decisions) 사항이며, 계약서의 역할은 3주 뒤에 CSS를 작성하는 사람이 실수로 결정하게 두는 대신, 누군가가 의도적으로 결정할 수 있도록 이 문제들을 수면 위로 끌어올리는 것입니다.
2단계: 단계를 도구에 맞추지 말고, 도구를 단계에 맞추세요
AI 페어 (AI pair)와 함께 작업하면 처음부터 끝까지 동일한 방식, 즉 "기능을 작성하게 시키기"라는 유혹에 빠지기 쉽습니다. UX가 중요한 작업에서는 정확히 반대로 해야 합니다. 실제로 효과적이었던 방법은 각 단계를 완전히 다른 태세 (posture)로 접근하는 것이었습니다:
- 탐색 (Exploration) — 사실만 기록합니다. 오직 이미 존재하는 것을 매핑하는 것이 유일한 임무였던 읽기 전용(read-only) 검토였습니다. 클릭 대상이 어디인지, 어떤 차트 라이브러리가 이미 사용 중인지, 쿼리 레이어는 오늘날 어떤 모습인지, 코드베이스의 다른 어느 곳에 유사한 상호작용(모달, 스택형 차트, 유사한 드릴다운 등)에 대한 선행 사례가 있는지 등을 파악했습니다. 이 단계에서는 의견이 없습니다 — 단지 파일 참조와 함께 사실만 기록할 뿐입니다. 왜냐하면 사실을 알아내기 전에 형성된 모든 의견은 자신감 있는 목소리를 입은 추측이기 때문입니다.
- 설계 (Design) — 그 사실에 기반합니다. 탐색 검토를 마친 후에야 구체적인 구현 접근 방식을 요청했습니다: 어떤 기존 함수를 재사용할지, 실제로 변경해야 하는 파일이 무엇인지, 그리고 새로운 쿼리가 어떻게 생겼는지 등을요. 새로 발명한 패턴에 의존하는 것이 아니라 실제 존재하는 것에 기반했기 때문에 아무도 먼저 확인하지 않은 실수를 막을 수 있었습니다.
- 명확화 (Clarification) — 추측하지 말고 질문합니다. 계약(contract)에서 나온 두 가지 열린 질문(대상 행 레이아웃, 고정 여부)은 가정하는 대신 직접 질문했습니다. 여기서 잘못된 추측은 10초를 비용으로 만들지 않습니다 — 발견하고 수정하는 데는 전체 검토 주기와 또 다른 시간이 소요됩니다. 질문하는 것은 10초밖에 들지 않습니다.
- 제작 (Craft) — 목업(mockup)만, 그것도 오직 목업일 때. 마지막 단계이자 이 게시글 전체가 정말로 다루고자 하는 부분은 실제로 클릭할 수 있는 무언가를 만드는 것이었습니다. 이는 앱의 실제 디자인 토큰(기존 CSS 사용자 정의 속성에서 바로 가져온 색상들, 발명한 것이 아님), 실제 콘텐츠, 그리고 결국에는
이것은 전체 프로세스에서 가장 영향력이 큰 단일 결정이며, 이번에 제대로 실행하기 전까지 제가 과소평가했던 부분입니다. 바로 목업(Mockup)에 플레이스홀더(Placeholder) 콘텐츠가 아닌 제품의 실제 시드 데이터(Seed data)를 사용했다는 점입니다. 데모 데이터셋에서 가져온 실제 객관적인 제목들, 실제 단계별 진행 문구 — 즉, 일반적인 "단계 1 / 단계 2" 라벨이 아니라 실제 ESDM 프롬프트 페이딩(Prompt-fading) 레벨("전적인 신체적 지원과 함께", "부분적인 신체적 지원과 함께")을 사용했습니다. 실제 세션 날짜들. 그리고 누군가가 나중에 디자인 시스템(Design system)과 대조해야 하는 "예쁜 파란색"을 임의로 고르는 대신, 앱의 자체 CSS 변수에서 직접 가져온 실제 디자인 토큰(Design tokens)들을 사용했습니다.
또한 이것은 빈 페이지에 홀로 떠 있는 차트가 아니었습니다. 차트가 실제로 존재하는 대시보드를 완전히 재구성하여 그 안에 배치했습니다. 동일한 헤더, 동일한 수직 공간을 차지하기 위해 경쟁하는 주변 섹션들, 동일한 카드 크롬(Card chrome)을 적용했습니다. 고립된 상태에서 멋져 보이는 차트와, 주의를 끌기 위해 경쟁하는 다른 네 개의 섹션 아래에 놓였을 때 멋져 보이는 차트는 완전히 다른 두 가지 디자인 문제입니다. 그리고 위젯(Widget)만 만드는 것이 아니라 컨테이너(Container)까지 구축할 만큼 정직하게 작업해야만 두 번째 문제를 발견할 수 있습니다.
저는 또한 실제 차트 라이브러리(Charting library)를 연결했습니다. 이를 대신하는 수제 SVG 도형이 아니라, "이게 맞게 보이는가"라는 질문이 실제로 배포될 것과 동일한 렌더링 엔진(Rendering engine)을 기준으로 평가될 수 있는 의미 있는 질문이 되도록 하기 위함이었습니다. 자신의 렌더링 엔진을 가짜로 만드는 목업은 그 놀라움(문제)을 나중으로 미룰 뿐입니다.
4단계: 풀 리퀘스트(Pull request)가 아닌 목업에서 반복(Iterate)하기
이 부분이 실제로 비용을 지불하는(가치를 창출하는) 단계입니다. 목업이 클릭 가능해지는 순간, 사전 계약(Contract) 작성만으로는 절대 잡아낼 수 없었던 두 가지 사항이 드러났습니다. 왜냐하면 이 두 가지 모두 실제 결과물이 움직이는 것을 보기 전까지는 보이지 않기 때문입니다:
서류상으로는 "분명히 옳은" 선택이었던 스티키 헤더(Sticky header)는 실제로는 매우 나쁜 선택이었습니다. 대상 비교 행(target-comparison row)을 고정하는 것은 추상적인 관점에서는 완전히 말이 되는 일이었습니다. 즉, 아래의 상세 내용을 스크롤하는 동안 비교 대상이 되는 항목을 항상 화면에 보이게 유지하는 것이죠. 하지만 실제 차트 높이(첫 번째 패스에서의 압축된 플레이스홀더 높이가 아닌 실제 높이)가 적용되는 순간, 고정된 섹션이 모달(modal)의 수직 공간 대부분을 차지해 버렸고, 정작 비교를 도와주어야 할 콘텐츠를 위한 공간은 거의 남지 않게 되었습니다. 이것을 일반 스크롤 방식으로 되돌리는 데는 눈에 보이는 즉시 약 2분 정도가 걸렸습니다. 만약 이것이 먼저 배포되었다면, 디자인 리뷰와 후속 티켓(ticket), 그리고 두 번째 PR(Pull Request)이 필요했을 것입니다.**
임의로 만들어낸 단계 이름들은 말 그대로 만들어낸 것처럼 느껴졌습니다. 일반적인 하위 기술(sub-skill) 레이블을 시드 데이터(seed data)에 있는 실제 프롬프트 페이딩(prompt-fading) 어휘로 교체하자, "어떤 단계가 실제로 추가적인 지원 부하를 유발하는가"라는 강조 사항이 단순한 UI 기능에서 임상적으로 정확한 사실로 읽히게 되었습니다. 이것은 단순한 미적 차이가 아닙니다. 이해관계자가 예의 바르게 고개를 끄덕이는 것과, 이해관계자가 "아, _바로 저 아이_가 제가 생각하던 아이예요, 네, 정확히 저 패턴이에요"라고 말하는 것 사이의 차이입니다.
이 두 가지 모두 코드 리뷰(code review)에서 잡아낼 수 있는 결함이 아닙니다. 코드 리뷰는 코드가 티켓(ticket)에 명시된 대로 작동하는지를 확인합니다. 티켓 자체가 옳았는지에 대해서는 의견을 내지 않습니다. 오직 결과물이 움직이는 것을 직접 봐야만 이를 알 수 있습니다. 그리고 "그것"이 훅(hooks), 쿼리 키(query keys), 번역 문자열(translation strings)이 이미 연결된 컴포넌트 트리(component tree)가 되기 전, 스크래치 파일(scratch file) 안의 <div> 상태일 때 이를 발견하는 것이 훨씬 더 극적으로 저렴합니다.
실제로 중요한 부분: 이것이 실제 제품에서 절약해 주는 것
제가 구체적으로 말씀드리고 싶은 부분은 바로 여기입니다. 왜냐하면 "구축하기 전에 검증하라"는 말은 실제 비용이 어디에서 발생하는지를 보기 전까지는 당연한 소리처럼 들리기 때문입니다.
실제 구현 이후의 재작업(Rework)은 변경 사항의 크기에 비례하지 않습니다. 정적인 목업(Mockup)에서의 한 줄짜리 CSS 수정은 그냥 한 줄짜리 CSS 수정일 뿐입니다. 하지만 기능이 실제 컴포넌트(Component)를 통해 배포된 후에 발견된 동일한 한 줄짜리 수정은 다음과 같은 의미를 갖습니다: 상태(State)와 프롭스(Props)가 흐르는 컴포넌트를 건드려야 하고, 아마도 훅(Hook)을 건드려야 할 수도 있으며, 쿼리 키(Query key)를 건드려야 할 수도 있고, 반드시 타입 체커(Type checker)를 다시 실행해야 하며, 아마도 두 개의 로케일(Locale) 파일에 있는 번역 문자열을 건드려야 할 수도 있고, 반드시 다시 리뷰(Review) 과정을 거쳐야 하며, 이미 완료(Done)로 표시된 플로우(Flow)를 다시 테스트해야 할 수도 있습니다. 수정 사항의 크기는 변하지 않았습니다. 하지만 수정을 수행하는 데 따르는 영향 범위(Blast radius)가 변한 것입니다.
그렇게 피한 재작업의 매 단계는 다른 곳에 투입될 수 있는 엔지니어링 시간입니다. 하나의 기능에만 몰두하다 보면 놓치기 쉬운 부분이 바로 이것입니다. 이미 배포된 컴포넌트를 다시 논쟁하며 소비하지 않은 시간은 단순히 "절약"되는 것이 아니라, 다른 곳으로 "재배치"됩니다. 즉, 백로그(Backlog)의 다음 항목으로, 두 스프린트(Sprint) 동안 방치되었던 버그로, 혹은 이번 기능 다음에 올 기능으로 재배치되는 것입니다. 한 분기(Quarter)를 기준으로 볼 때, "구축하기 전에 검증한다"와 "일단 구축하고 틀린 것을 수정한다"의 차이는 단순한 반올림 오차 수준이 아닙니다. 그것은 로드맵(Roadmap)을 그대로 완수하느냐, 아니면 로드맵의 대부분을 완수하면서 동시에 "처음에 거의 맞았던" 것들에 대한 후속 티켓(Follow-up tickets)의 흔적을 남기느냐의 차이입니다.
솔직히 말해 모형(Mockup)에 불과한 풀 리퀘스트(Pull Request, PR)가 더 정직한 피드백을 더 빠르게 이끌어냅니다. 이는 다소 추상적으로 들릴 수 있지만, 리뷰 지연 시간(Review latency)에 미치는 실질적이고 측정 가능한 효과입니다. 리뷰어가 해당 PR에 프로덕션 코드(Production code)가 전혀 포함되어 있지 않다는 것을 알게 되면,
순서를 뒤집으세요. 목업(Mockup)을 먼저 만들고, 저렴하게 반복(Iterate)한 다음, 그다음에 진짜 것을 작성하십시오. 그러면 동일한 이견이 발생하더라도, 동일한 횟수의 검토 과정을 거치면서도 비용은 이전의 아주 일부분만 소모됩니다. 기능도 같고, 피드백도 같으며, "아니요, 다시 해보세요"라는 말의 횟수도 같습니다. 단지 프로덕션 컴포넌트(Production-component) 가격이 아닌, 스크래치 파일(Scratch-file) 가격으로 지불했을 뿐입니다.
그것이 핵심 비결입니다. 실수를 줄이는 것이 아니라, 동일한 수의 실수를 더 저렴한 곳에서 잡아내는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기