프론트엔드를 위한 UI Flow Specification 작성법(제안)
들어가며
이번달 15일에 SQLD 자격증 시험을 위해 교보문고에 갔다가 우연히 Alistair Cockburn의 “Writing Effective Use Cases”를 발견했습니다. 20년이 넘은 책이지만, 몇 페이지를 넘겨보는 순간 이것이 현재 나에게 필요하다고 느꼈습니다. AI의 도움으로 관련 내용을 공부하는 중이며, 저자가 Use Case 3.0까지 발전시켜 무료로 제공하고 있다는 것도 알게 되었습니다.
실무의 문제점
작은 조직에서는 디자이너가 디자인과 기획을 함께 수행하는 경우가 많습니다. 제가 겪은 어려움은 다음과 같습니다:
- 기능에 대한 기획과 디자인을 동시에 제공하지만, 기존 구현되어 있는 것과 통일성이 떨어지는 경우가 있음
- 디자인은 업데이트되지만 화면설계서는 그대로인 경우가 많음
- 설계서만 보고 사용자 플로우나 상호작용을 이해하기 어려움
- 개발 중에 “에러 나면?”, “로딩 중에는?” 같은 질문이 계속 발생
이런 이슈들은 화면설계서(정적 산출물)와 구현(동적 상태/전환) 사이의 간극에서 비롯됩니다.
Use Case와 구현 사이의 간극
기획서(Use Case)는 이렇게 작성됩니다:
1
2
3
1. 사용자가 상품을 선택한다
2. 시스템이 재고를 확인한다
3. 사용자가 결제한다
하지만 실제 구현할 때는 이런 질문들이 나옵니다:
- 재고 확인 중에 버튼은 어떻게 되나요?
- 재고가 없으면 어떤 메시지를 보여주나요?
- 네트워크가 느리면 어떻게 하나요?
- 결제 중에 뒤로가기를 누르면?
이 간극을 메우는 문서가 필요합니다. 이것이 바로 “UI Flow Specification”입니다.
용어 주석: 본 글에서 사용하는 ‘UI Flow Specification’은 업계 표준 용어가 아니라, 필자가 Use Case와 구현 사이의 간극을 메우기 위해 제안하는 실무 템플릿입니다. 유사 산출물로는 UI/UX Spec, Interaction Spec, Statechart, Acceptance Criteria 등이 있습니다.
이 글에서는 Use Case를 대체하는 것이 아니라, Use Case를 프론트엔드 관점에서 구체화하는 방법을 제안합니다.
왜 UI Flow Specification이 필요한가?
1. User Story는 프론트엔드에서 가장 구체적으로 드러난다
현대 프론트엔드는 과거와 비교할 수 없을 만큼 복잡해졌습니다:
- 상태 관리의 복잡성: Redux, MobX, Zustand, Jotai, Recoil 등 상태 관리 라이브러리가 계속 개발되고 있습니다
- 비동기 처리: 로딩, 에러, 재시도, 낙관적 업데이트 등 고려해야 할 상태가 많습니다
- UI 상태: 모달, 토글, 폼 검증, 애니메이션 등 순수하게 UI만을 위한 상태도 관리해야 합니다
- 사용자 경험: 단순히 기능이 동작하는 것을 넘어, 매끄러운 사용자 경험을 제공해야 합니다
이러한 복잡도 속에서 User Story의 핵심 상호작용은 프론트엔드에서 가장 구체적으로 드러납니다. 사용자가 경험하는 많은 순간이 프론트엔드 코드로 표현됩니다.
2. UI 구현 디테일은 프론트엔드가 가장 잘 안다
제 경험상, 보통 디자인은 에러 처리 없이, 로딩 화면 없이, 단순 성공된 화면만 제공하는 경우가 많았습니다. 그러면 프론트엔드 개발자는 개발 중에 “재고가 없을 때는?”, “네트워크가 느릴 때는?”, “에러가 나면?” 같은 질문을 하게 됩니다.
이 질문들은 Use Case의 UI Flow/Acceptance Criteria에 해당합니다. 그리고 이 질문에 대한 구체화를 프론트엔드가 맡는 것이 자연스럽습니다. 그렇다면 처음부터 프론트엔드가 Use Case를 기반으로 한 UI Flow Spec을 함께 작성하는 것이 더 효율적이지 않을까요?
3. 작은 조직에서의 필요성
작은 조직이나 스타트업에서는 하나의 목표를 향해 달려가기 때문에 모든 팀원이 프로덕트에 대한 높은 이해도를 가져야 합니다. 프론트엔드 개발자가 Use Case를 작성하면:
- 백엔드 개발자는 프론트엔드에서 어떤 상태가 필요한지 명확히 알 수 있습니다
- 기획자/디자이너는 놓친 엣지 케이스를 발견할 수 있습니다
- 전체 팀이 같은 그림을 그리며 align할 수 있습니다
API 계약과 데이터 모델은 사용자 경험을 뒷받침합니다. 프론트엔드는 UI Flow Spec으로 필요한 상태와 상호작용을 명확히 제시하고, 백엔드는 이에 맞춰 API와 비즈니스 로직을 설계하는 식으로 협업하는 것이 자연스럽습니다.
4. User Story의 상호작용은 프론트엔드에서 가장 구체적으로 드러난다
User Story를 분해해보면 모든 순간이 프론트엔드와 연결되어 있습니다:
- “사용자가 버튼을 클릭한다” → onClick 이벤트 핸들러
- “시스템이 로딩 상태를 표시한다” → isLoading 상태 관리
- “에러가 발생하면 메시지를 보여준다” → Error Boundary, Toast
- “성공하면 다음 화면으로 이동한다” → router.push
프론트엔드 개발자는 UI Flow Specification을 가장 구체적으로 작성할 수 있는 포지션입니다.
Use Case가 의도적으로 다루지 않는 영역 (UI Flow Spec이 보완하는 부분)
Use Case는 본래 구현 독립적이며, 액터-시스템의 상호작용과 사용자 목표(비즈니스 가치)를 기술합니다. 다만 실무에서는 종종 시스템 내부 흐름이나 백엔드 API 설계 문서처럼 오해되기도 합니다. 아래 항목들은 Use Case의 범위 밖(Out of Scope)이며, UI Flow Spec으로 보완하는 것이 적절합니다:
- UI 상태 상세: 로딩 중일 때 버튼 비활성화/스피너 표시 등
- 에러 표현/피드백: “재고 확인 실패” 시 사용자에게 보이는 메시지와 행동 유도
- 비동기 시 UI 상호작용 규칙: API 호출 중 입력 잠금, 재시도, 낙관적 업데이트 등
- 구현 지침/상태 다이어그램: 컴포넌트 수준 상태 전환, 모달 열림/닫힘 규칙 등
UI Flow Specification 작성법(제안)
핵심 원칙 (가설)
UI Flow Spec의 핵심 원칙은 다음과 같습니다:
- UI 상태를 명시적으로 정의한다
- 각 상태에서 가능한 전환을 명확히 한다
- 성공/실패 시나리오를 모두 작성한다
- 사용자가 보는 것과 할 수 있는 것을 구체적으로 서술한다
예제: 상품 구매하기 UI Flow
전통적인 방식 대신, 프론트엔드 관점에서 다음과 같이 작성할 수 있을 것 같습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* UI Flow Specification: 상품 구매 (Based on Use Case #12)
* Component: ProductPurchase.tsx
* States: IDLE | LOADING | PAYMENT_READY | PROCESSING | SUCCESS | ERROR | PAYMENT_FAILED
*/
/* ============================================
* [IDLE 상태] - 초기 상태
* ============================================ */
화면 요소:
- "구매하기" 버튼 (활성화)
- 상품 정보 표시
- 재고 수량 표시
사용자 액션:
1. 사용자가 "구매하기" 버튼 클릭
시스템 반응:
→ UI 상태: IDLE → LOADING
→ 재고 확인 API 호출 시작
→ 버튼 비활성화
→ 버튼 텍스트 변경: "구매하기" → "확인 중..."
→ 로딩 스피너 표시
→ 다른 상호작용 방지 (버튼 중복 클릭 방지)
/* ============================================
* [LOADING 상태] - 재고 확인 중
* ============================================ */
화면 요소:
- "구매하기" 버튼 (비활성화, 로딩 스피너)
- 상품 정보 표시 (약간 흐리게)
시나리오 2a: 재고 확인 성공
시스템 반응:
→ UI 상태: LOADING → PAYMENT_READY
→ 결제 모달 오픈 (fade-in 애니메이션)
→ 결제 폼 초기화
→ 포커스를 카드번호 입력 필드로 이동
→ body 스크롤 잠금 (모달 열림)
→ ESC 키로 모달 닫기 가능
시나리오 2b: 재고 없음
시스템 반응:
→ UI 상태: LOADING → ERROR
→ Toast 메시지 표시: "죄송합니다. 해당 상품의 재고가 부족합니다."
→ 토스트는 3초 후 자동으로 사라짐
→ UI 상태: ERROR → IDLE로 자동 복귀
→ 버튼 다시 활성화
→ 재고 수량 업데이트 (0개로 표시)
→ 버튼 텍스트 변경: "품절"
→ 버튼 스타일 변경 (회색, 비활성화)
시나리오 2c: 네트워크 에러 / 타임아웃
시스템 반응:
→ UI 상태: LOADING → ERROR
→ Toast 메시지: "네트워크 오류가 발생했습니다."
→ "재시도" 버튼 표시
→ 버튼 활성화 상태로 복귀
사용자 액션 (재시도 버튼 클릭):
→ 시나리오 1번으로 돌아감 (IDLE → LOADING)
/* ============================================
* [PAYMENT_READY 상태] - 결제 준비
* ============================================ */
화면 요소:
- 결제 모달 (배경 딤 처리)
- 카드번호 입력 필드
- 유효기간 입력 필드
- CVC 입력 필드
- "결제하기" 버튼 (초기에는 비활성화)
- "취소" 버튼
사용자 액션 3-1: 결제 정보 입력
시스템 반응:
→ 실시간 유효성 검사 수행
→ 카드번호: 16자리 숫자, 4자리마다 하이픈 자동 삽입
→ 유효기간: MM/YY 형식, 과거 날짜 체크
→ CVC: 3자리 숫자
→ 각 필드마다 에러 메시지 실시간 표시
→ 모든 필드 valid하면 "결제하기" 버튼 활성화
→ 하나라도 invalid하면 "결제하기" 버튼 비활성화 유지
사용자 액션 3-2: "취소" 버튼 클릭 또는 ESC 키 입력
시스템 반응:
→ 확인 다이얼로그 표시: "결제를 취소하시겠습니까?"
→ "예" 선택 시:
- 모달 닫기 (fade-out 애니메이션)
- UI 상태: PAYMENT_READY → IDLE
- body 스크롤 잠금 해제
- 입력한 결제 정보 메모리에서 제거 (보안)
→ "아니오" 선택 시:
- 다이얼로그 닫기
- PAYMENT_READY 상태 유지
사용자 액션 3-3: "결제하기" 버튼 클릭 (모든 입력 valid한 경우)
시스템 반응:
→ UI 상태: PAYMENT_READY → PROCESSING
→ 결제 API 호출 시작
→ 버튼 비활성화
→ 버튼 텍스트 변경: "결제하기" → "처리 중..."
→ 로딩 오버레이 표시 (전체 모달 덮음)
→ 모든 입력 필드 비활성화
→ 뒤로가기 방지 (beforeunload 이벤트 리스너 등록)
→ 페이지 새로고침 방지 알림
/* ============================================
* [PROCESSING 상태] - 결제 처리 중
* ============================================ */
화면 요소:
- 결제 모달 (모든 컨트롤 비활성화)
- 전체를 덮는 로딩 오버레이
- "결제 처리 중입니다. 잠시만 기다려주세요." 메시지
- 로딩 애니메이션
주의사항:
- 이 상태에서는 사용자가 아무런 액션도 할 수 없음
- 브라우저 뒤로가기 시도 시 경고 메시지
- 탭 닫기 시도 시 경고 메시지
시나리오 5a: 결제 성공
시스템 반응:
→ UI 상태: PROCESSING → SUCCESS
→ 로딩 오버레이 제거
→ 성공 아이콘 애니메이션 재생 (체크마크)
→ "결제가 완료되었습니다!" 메시지 표시
→ 주문번호 표시
→ 2초 대기
→ 주문 완료 페이지로 자동 이동 (router.push)
→ beforeunload 이벤트 리스너 제거
→ 결제 정보 메모리에서 안전하게 제거
시나리오 5b: 결제 실패 (카드사 거부, 한도 초과 등)
시스템 반응:
→ UI 상태: PROCESSING → PAYMENT_FAILED
→ 로딩 오버레이 제거
→ 에러 아이콘 표시 (X 마크)
→ 실패 사유 메시지 표시:
- "카드 한도가 부족합니다"
- "유효하지 않은 카드입니다"
- "카드사 승인이 거부되었습니다"
→ "다른 카드로 시도" 버튼 활성화
→ "취소" 버튼 활성화
→ beforeunload 이벤트 리스너 제거
사용자 액션 (다른 카드로 시도):
→ UI 상태: PAYMENT_FAILED → PAYMENT_READY
→ 결제 폼 초기화 (기존 입력값 제거)
→ 카드번호 입력 필드에 포커스
시나리오 5c: 네트워크 에러 / 타임아웃
시스템 반응:
→ UI 상태: PROCESSING → ERROR
→ 에러 메시지: "일시적인 오류가 발생했습니다. 결제가 처리되었는지 확인 중입니다..."
→ 백그라운드에서 결제 상태 확인 API 호출 (polling, 최대 3회)
→ 결제 확인 완료 시:
- 시나리오 5a 또는 5b로 분기
→ 결제 확인 실패 시:
- "결제 상태를 확인할 수 없습니다. 고객센터로 문의해주세요." 메시지
- 고객센터 전화번호 표시
- "내 주문 확인" 버튼 제공
→ beforeunload 이벤트 리스너 제거
이렇게 작성하면 좋을 것 같은 이유
1. 개발자-디자이너-기획자 간 명확한 커뮤니케이션
“재고 확인 실패 시 어떻게 해야 하나요?”라는 질문에 대해:
- 기존: “에러 메시지 보여주면 돼요”
- 이 방식: 위 시나리오 2b를 정확히 참조 가능
이렇게 하면 팀원 간 커뮤니케이션이 더 명확해질 것 같습니다.
2. 엣지 케이스 사전 발견 가능
작성 과정에서 자연스럽게 다음과 같은 질문들을 먼저 생각해볼 수 있습니다:
- “결제 처리 중에 사용자가 뒤로가기를 누르면?”
- “네트워크가 끊겼다가 다시 연결되면?”
- “같은 버튼을 빠르게 두 번 클릭하면?”
3. 테스트 시나리오 자동 도출
UI Flow Spec이 곧 테스트 케이스로 이어질 수 있습니다:
1
2
3
4
5
6
7
8
9
10
describe('상품 구매하기', () => {
it('[시나리오 2b] 재고 없음 시 에러 토스트를 표시한다', async () => {
mockAPI.getStock.mockRejectedValue({ code: 'OUT_OF_STOCK' });
await user.click(screen.getByRole('button', { name: '구매하기' }));
expect(await screen.findByText('해당 상품의 재고가 부족합니다')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '품절' })).toBeDisabled();
});
});
4. 상태 관리 설계의 가이드
UI Flow Spec을 작성하면서 필요한 상태를 미리 정의할 수 있을 것 같습니다:
1
2
3
4
5
6
7
8
type PurchaseState =
| { status: 'IDLE' }
| { status: 'LOADING' }
| { status: 'PAYMENT_READY'; formData: PaymentForm }
| { status: 'PROCESSING' }
| { status: 'SUCCESS'; orderId: string }
| { status: 'ERROR'; error: Error; canRetry: boolean }
| { status: 'PAYMENT_FAILED'; reason: string };
실무에 적용한다면?
실무에 이 방법을 적용한다면 다음과 같이 해볼 수 있을 것 같습니다.
1. 모든 화면에 대해 작성할 필요는 없을 것 같습니다
복잡한 사용자 플로우나 중요한 비즈니스 로직이 있는 화면에 집중하면 좋을 것 같습니다:
- 결제 프로세스
- 회원가입/로그인
- 복잡한 폼 작성
- 다단계 프로세스
단순한 조회 화면이나 정적 페이지는 생략해도 될 것 같습니다.
2. 점진적으로 구체화하기
처음부터 완벽하게 작성하려 하지 않고, 단계적으로 발전시키면 좋을 것 같습니다:
- 1단계: 주요 흐름만 작성 (Happy Path)
- 2단계: 에러 케이스 추가
- 3단계: UI 디테일 추가
- 4단계: 개발하면서 발견한 엣지 케이스 추가
3. 팀 내 템플릿 만들기
팀의 상황에 맞는 템플릿을 만들어 사용하면 좋을 것 같습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## UI Flow Spec: [기능명]
### 메타데이터
- 화면: [컴포넌트명]
- 주요 상태: [상태 목록]
- 관련 API: [API 엔드포인트들]
### [상태명] 상태
**화면 요소:**
-
**사용자 액션:**
-
**시스템 반응:**
-
4. 코드와 함께 관리하기
UI Flow Spec 문서를 코드베이스에 함께 관리하면 좋을 것 같습니다:
1
2
3
4
5
6
7
src/
features/
purchase/
PurchasePage.tsx
PurchasePage.uiflow.md ← 여기!
PurchasePage.test.tsx
usePurchaseState.ts
이렇게 하면 다음과 같은 이점이 있을 것 같습니다:
- PR 리뷰 시 Use Case도 함께 검토 가능
- 화면 수정 시 Use Case도 함께 업데이트 가능
- 히스토리 추적 가능
마치며: Use Case와 코드 사이를 잇는 UI Flow Spec
Use Case vs UI Flow Spec
- Use Case: “무엇을 하는가”(사용자 목표, 비즈니스 가치). 구현 독립적.
- UI Flow Spec: “사용자는 무엇을 보고, 어떻게 상호작용하는가”(상태/전환/피드백). 프론트엔드 구현 지향.
화면설계서가 스크린샷이라면, UI Flow Spec은 동영상에 가깝습니다. 정적인 화면이 아니라 시간에 따른 상태 변화와 상호작용을 담습니다.
협업 관점
- 기획자는 비즈니스 목적과 시나리오(Use Case)를 정의하고,
- 프론트엔드는 해당 시나리오의 UI 구현 디테일(UI Flow Spec)을 구체화하며,
- 백엔드는 이를 뒷받침하는 API/비즈니스 로직을 설계합니다.
학습 중인 제안
Writing Effective Use Cases의 핵심 원칙을 학습하면서, 프론트엔드가 기여할 수 있는 문서 형태를 정리해 보았습니다. 아직 학습 중이며, 작은 조직에서부터 점진적으로 시도해 보려는 제안입니다.
또한 본 글의 ‘UI Flow Specification’ 용어와 템플릿은 필자가 제안한 작업 용어로, 유사 표준 산출물들을 실무에 맞춰 통합한 접근입니다.
다음 글 예고
다음 글에서는 UI Flow Spec을 코드로 연결하는 실전 패턴(XState 상태 머신, TanStack Query 연계, Storybook 상태별 스토리, Playwright E2E)과 적용 체크리스트를 정리할 예정입니다.