제품 · 협업
대표 글인보이스 PDF — PD와 FE가 함께 쓴 UX 상세 동작 정의
팀에 애자일 프로세스가 자리 잡으면서 일하는 방식이 바뀌었다. PO가 문제와 방향을 가져오면, 그걸 화면에서 어떻게 동작시킬지는 PD와 FE가 함께 상세 동작 정의로 만들어 구현까지 가져간다. 인보이스 PDF 등록이 그 방식으로 만든 대표 기능이다 — 그리고 이 기능에서 처음으로, 늘 구현 중에야 드러나던 것들(글자수 제한, 에러 문구, 값 충돌 규칙)을 시작 전에 전부 정의하고 들어갔다.
PO의 문제 정의에서 출발
PO가 가져온 문제는 명확했다. AI 기능의 답변 품질이 기대에 못 미치는데, 원인은 모델이 아니라 데이터 부족 — 딜러들이 인보이스를 잘 등록하지 않았다. 등록이 어려우니까. 모든 필드를 수기로 입력해야 했으니까.
해결책 한 줄: "PDF를 올리면 인보이스가 등록되게 한다." AI가 파싱해 채우고, 사용자는 더블체크만. 여기까지가 PO의 몫이고, 이제 이 한 줄을 화면의 동작으로 바꾸는 게 PD와 FE의 몫이었다.
미션은 한 단어 — "쉽게"
PD와 내가 받은 미션은 결국 한 단어였다. 쉽게. 주 사용자가 4~50대 미국 창고 브로커라, 화려한 인터랙션보다 헤맬 구석이 없는 흐름이 우선이었다.
그래서 UI가 나오기 전에 내가 먼저 움직였다. 아이디어가 갈리는 지점마다 프로토타입 화면을 직접 만들어 들고 갔다. 그중 하나가 AI 바이어 매칭 영역 — "매칭 확률을 구간별로 다르게 보여주고, 후보가 없으면 그 화면에서 바로 신규 등록까지 이어지게 하자"는 흐름을 제안했고, 채택됐다. PD는 그 골격을 훨씬 그럴듯한 UI로 다듬어 돌려줬다. 서로 의견이 다른 순간도 있었지만, 주고받을수록 결과물이 나아졌다.
이 핑퐁의 진짜 수확은 따로 있었다. 프로토타입을 들고 가면 대화의 단위가 "느낌"이 아니라 동작이 된다. 그래서 개발자 눈에만 보이는 엣지케이스 — "AI가 후보를 못 찾으면?", "같은 바이어가 이미 있으면?", "필드가 비어 오면?" — 를 기획 단계에서 같이 정의하고 출발할 수 있었다. 보통 이런 케이스는 구현 중에야 발견돼 기획에 되묻고 흐름을 끊는데, 이번엔 그 왕복이 통째로 사라졌다.
유저 저니부터 그렸다
상세 동작에 들어가기 전에 전체 흐름을 한 장으로 합의했다.
인보이스 목록 ─ Upload PDF → 업로드 다이얼로그 ─ PDF 선택 → AI 스캐닝 (4단계 진행바)
│
┌─────────────────────────────────┘
▼
Parse 페이지 (검토/편집)
│
┌─ 거래처 후보 있음 → 추천 카드(최대 3) + 검색 + Add Buyer
└─ 후보 없음 → "Add a buyer" 카드
│
Line Items / Payments 검토
│
Save 성공 → 목록으로 / 이탈 시도 → "Discard this invoice?" 가드저니가 한 장 있으니 이후의 모든 세부 논의가 "이 흐름의 어느 칸 얘기인지"로 정리됐다.
상세 동작 정의 — 구현 중에 물어볼 것을 미리 없애기
PD와 함께 케이스를 하나씩 표로 못 박았다. 핵심 몇 가지:
| 케이스 | 정의한 동작 |
|---|---|
| 거래처 카드 선택 시 값 채움 | 거래처 값 우선 → 없으면 AI 추출값 → 둘 다 없으면 빈값 (이전 거래처 잔재 차단) |
| 주소 셋트 (Address + Detail) | 거래처에 하나라도 있으면 둘 다 거래처 값으로 — 한쪽만 갈아끼우지 않는다 |
| 선택 해제 | buyer 영역을 AI 추출 원본값으로 복원 |
| 정확도 뱃지 | 점수 기준치 이상만 노출, 최대 3건, 확률 구간별 색(회색→보라→파랑→진파랑), 95% 이상은 95%로 캡(과신 방지) |
| 실패 5종 (파일 오류·파싱 실패·진입 실패·저장 실패·이탈) | 각각 토스트/재시도 버튼/가드 다이얼로그로 정의 |
"AI가 후보를 20건 주면?"(FE가 정렬해 상위 3건만), "선택을 바꿨는데 새 후보에 그 필드가 없으면?" (기존 값 유지, 빈값으로 만들지 않기) 같은 질문들이 구현 중이 아니라 정의 단계에서 소거됐다.
기다림을 설계하다 — 진행바의 비하인드
Figma 시안엔 진행바가 "있다"까지만 있었다. 부드럽게 흐르는지, 단계마다 멈추는지, 실제 진행과 동기화되는지 — 어떻게 움직이는지는 정의돼 있지 않았다.
처음 검토한 건 백엔드에서 SSE나 소켓으로 실제 파싱 단계를 받아 동기화하는 안이었다. 정확하긴 하지만, 이 한 화면을 위해 실시간 채널을 까는 건 오버엔지니어링이라고 판단했다 — 빠르게 검증하자는 애자일 의도와도 어긋났다. 대신 AI 엔지니어에게 평균 응답 시간을 물었고, 7~10초라는 답을 기준으로 진행바를 "시간"으로 설계했다.
- 단계는 백엔드와 무관하게 FE가 임의로 표시 — 4단계(업로드 → 문서 읽기 → 거래처 매칭 → 품목 추출)
- 리듬은 PD와 합의한 채움 1초 + 대기 1초 — 단계마다 차오르다 멈추는 호흡이 "일하고 있다"는 안정감을 준다
- 4단계 × 2초 = 기본 8초를 먼저 소화하고, 실제 응답이 더 걸리면 마지막 단계에서 in-progress로 정지(스피너·펄스는 계속) — 평균 7~10초 응답이면 사용자가 "멈춘 진행바"를 보는 시간은 길어야 2~3초다
기다림 자체를 줄일 수 없다면, 기다림의 체감을 설계한다 — 이 결정의 효과는 출시 후 지표에서 확인된다.
늘 놓치던 것들을 이번엔 먼저 — 글자수·에러 문구
이전 폼들에서 구현 후에야 발견되던 두 가지를 이번엔 정의에 포함시켰다.
- 글자수 제한 — 백엔드 스키마의
varchar(N)을 그대로 받아 전 필드를 zod 스키마 + input maxLength로 명문화했다. 백엔드가 무제한인 Note는 그냥 두지 않고, 레퍼런스 회계 SaaS의 정책을 찾아 4000자 제한을 제안해 맞췄다 - 에러 메시지 통일 — 필드 단위("This field is required" / "Maximum N characters") · 라인 단위 (한 라인의 누락 필드를 한 줄로 합쳐 "A, B are required") · 섹션 단위(결제 합이 총액 초과 시 Save 비활성 + 헬퍼)의 3계층으로 정리하고, 메시지 상수를 단일 진입점으로 모았다. 스키마 회귀 테스트 25케이스로 규칙을 박제했다
FE가 질문을 먼저 꺼내다 — Open question
정의 단계의 FE 역할 중 하나는 UX 영향 관점으로 문제를 먼저 제기하는 것이었다. PRD에 Open question 섹션을 두고 이런 걸 올렸다.
- 바이어를 추가했는데 값을 지우면 "Add buyer" 배너가 다시 뜬다 → 시스템 상태(생성 완료)와 UI가 어긋나 "내가 방금 한 게 잘못됐나?" 오해 유발
- 필드를 클릭해야 에러가 보인다 → 문제 필드를 빠르게 인지할 수 없다
- 같은 바이어를 중복 추가할 수 있다 → 리스트 신뢰도 저하, "어떤 게 진짜지?" 인지 부담
- 방금 추가한 항목이 정렬 때문에 3~4페이지로 가서 안 보인다 → "추가된 거 맞나?" 불안
전부 이번 스코프 밖이지만, 지금 적어두면 다음 스코프의 결정이 빨라진다. 거꾸로 디자인에 시안 두 개를 만들어 의견을 구하기도 했고("이 안내 문구, 검색 필드 옆 vs 아래?"), 동료의 "빨리 업로드하고 싶은 경험을 해친다"는 피드백으로 문구를 빼는 결정이 나기도 했다. 정의는 한 방향 전달이 아니라 핑퐁이었다.
확정본을 들고 백엔드로 — 병렬 개발
UX 흐름·프로토타입이 확정되자 그 문서를 그대로 백엔드에 공유했고, 백엔드는 화면이 완성되길 기다리는 대신 명세(스웨거)부터 빠르게 내려줬다. FE는 그 명세로 Orval 타입을 생성하고 MSW 목서버를 붙여, 실제 API가 나오기 전부터 병렬로 화면을 만들었다. 기획 → 디자인 → FE → BE가 일렬로 기다리는 대신, 확정된 문서 하나를 축으로 같이 움직인 것이다.
결과 — 지표를 회수하다
출시 직후, PO가 미국 자회사의 내부 유저들과 테스트를 돌려 PRD에 적어둔 지표를 실제로 회수했다.
| 지표 | 목표 | 결과 |
|---|---|---|
| 핵심 필드 인식 성공률 | 90% 이상 | 99% |
| 실사용 평균 등록 시간 | 2분 이내 | 52.4초 (수정 없으면 38초) |
| 정성 평가 (AI 정확도·수정 UX, 10점 척도) | 각 7점 이상 | 평균 9.75점 |
| 등록 로딩 시간 | 10초 이내 | 10.89초 — 소폭 미달 |
숫자 밖의 비교가 더 컸다. 수기 입력으로는 한 건에 평균 5~10분이 걸리던 일이 1분 안쪽으로 내려왔고, "등록이 정말 간편해졌다"는 피드백이 따라왔다.
로딩은 목표를 못 맞췄다. 다만 같은 인터뷰에서 "단계별 진행바 덕에 지루하지 않았고, 오래 걸린다고 체감하지 못했다"는 답을 받았다 — 위에서 시간으로 설계해둔 진행바가 미달을 체감에서 흡수한 것이다. 미달은 미달대로 기록했다. 지표는 달성했을 때보다 못 맞췄을 때 더 배울 게 많다.
남긴 것
- 상세 동작 정의를 PD와 FE가 같이 쓰면 구현 중 멈춤이 사라진다. "이럴 땐 어떻게 돼요?"가 정의 단계로 앞당겨지고, 구현은 표를 코드로 옮기는 일이 된다
- 글자수·에러 문구처럼 늘 구현 후에 발견되던 것일수록 정의에 넣을 가치가 크다 — 이번에 만든 3계층 에러 규칙은 이후 폼들의 기본값이 됐다
- 실시간 동기화가 항상 정답은 아니다. 숫자 하나(평균 응답 7~10초)를 물어보는 것이 SSE 채널 하나보다 싼 정답일 때가 있다
- 지표는 적는 것보다 회수하는 게 일이다. 출시 직후 측정까지 끝내야 "기능을 냈다"가 아니라 "문제를 풀었다"라고 말할 수 있다