30개의 금융사를 지원하기 위한 SDK 표준화 설계

첫 금융사에 약정 화면이 올라가고 약정률이 30% 가까이 개선되자 영업이 풀리기 시작했다. 성공 사례가 하나 생기니 다음 금융사와의 대화가 빨라졌고, 곧바로 두 번째 금융사 작업이 들어왔다.
작업 방식은 단순했다. 첫 금융사 코드를 복사한 뒤 다른 부분을 고쳤다. 고쳐야 할 화면은 스물몇 개였다.
4편까지는 금융사 한 곳을 성립시키는 문제를 다뤘다. 스타일이 깨지지 않게 하고, 스크립트가 제때 로드되게 하고, 배포가 금융사마다 엉키지 않게 하고, 화면 스물몇 개를 두 달 안에 올리는 일이었다.
두 번째 금융사부터는 질문이 달라졌다.
한 곳에서 동작하는 코드를 어떻게 서른 곳까지 늘릴 것인가.
당시 조건은 다음과 같았다.
- 금융사는 1곳에서 2곳, 3곳으로 늘고 있고 영업 목표는 30곳이다.
- 금융사당 화면은 20~30개다.
- 지금 코드는 첫 금융사 코드를 복사해서 시작했다.
- 우리는 을이고, 금융사의 비즈니스 요구는 대체로 수용해야 한다.
- 앞으로 어떤 요구가 들어올지는 모른다.
네 번째와 다섯 번째 조건이 문제를 어렵게 만들었다. 요구를 제한하기 어렵고 그 내용도 예측할 수 없는데, 금융사 수는 열 배로 늘려야 했다.
표준화는 보통 차이를 줄이는 일로 설명된다. 하지만 이 프로젝트에서는 반대 방향에서 출발해야 했다. 차이를 없애는 것이 아니라, 앞으로 들어올 차이를 계속 수용할 수 있는 구조를 만드는 일이었다.
화면 25개에 금융사 30곳을 곱해봤다
대출 퍼널은 화면이 많다.
약관 동의, 본인 인증, 정보 입력, 추가 정보, 심사 대기, 상품 선택, 계약, 결과. 단계 이름만 보면 여덟 개지만 실제로는 각 단계가 여러 화면으로 나뉜다. 동의 항목이 많으면 약관이 두 화면이 되고, 소득 정보와 직장 정보를 따로 받으면 입력이 세 화면이 된다.
한 금융사당 대략 스물에서 서른 개였다. 웹 서비스 하나라면 그저 화면이 많은 서비스다. 문제는 금융사마다 한 벌씩 생긴다는 것이었다.
두 번째 금융사를 작업하면서 계산해봤다. 금융사 한 곳당 화면을 25개로 잡으면 30곳은 750개다. 모두 서로 다른 화면이라는 뜻은 아니다. 복사한 코드를 금융사별 프로젝트에서 따로 관리하면 최대 750개의 화면 인스턴스를 각각 확인하고 배포해야 한다는 뜻이었다.
flowchart LR
A["금융사 1곳<br/>화면 약 25개"] --> B["금융사 30곳<br/>화면 인스턴스 최대 750개"]
B --> C["공통 버그 발견"]
C --> D["30개 프로젝트의<br/>영향 범위 확인"]
D --> E["복사 이후 생긴 차이 때문에<br/>확인·수정·배포 절차도 분기"]
개별 문구 변경이라면 해당 금융사의 화면만 고치고 배포하면 된다. 정상적인 개별 요구다. 공통 로직에서 버그가 발견되면 이야기가 달라진다.
복사해서 시작한 코드에서는 버그가 어디까지 퍼졌는지 바로 알 수 없다. 열 곳이면 열 곳을 모두 확인해야 한다. 복사 이후 코드가 각자 달라졌다면 같은 버그를 확인하는 방법도 조금씩 달라진다.
확인할 곳은 금융사 수만큼 늘어나고, 코드의 차이는 확인 절차까지 갈라놓는다. 두 곳일 때는 아직 감당할 수 있었다. 하지만 30곳까지 같은 방식으로 갈 수는 없었다.
실험이 제품이 되면서 계산이 달라졌다
금융사가 두세 곳으로 늘어나는 사이, 조직 안에서 이 SDK의 위치도 바뀌었다. MVP에서 공식 제품으로 승격됐다.
실험 단계의 요구사항에는 유효기간이 있다. 되는지 확인하기 위해 만들기 때문에 방향이 틀렸다면 코드를 버릴 수 있다. 제품이 되면 다르다. 협의한 내용은 지켜야 하고, 한 번 올라간 화면은 계속 유지해야 한다. 운영 종료 시점도 정해져 있지 않았다.
복사해서 시작한 코드를 계속 복사해도 되는가. 제품이 된 뒤에는 이 질문의 무게가 달라졌다.
그렇다고 금융사 30곳에서 나올 요구를 미리 예상해 범용 구조부터 만들겠다는 뜻은 아니었다. 아직 확인하지 않은 차이를 추측해 옵션과 계층을 늘리는 것 역시 위험했다. 먼저 지금까지 받은 기획서를 나란히 놓고, 실제로 반복되는 업무와 금융사마다 달라지는 부분을 구분해보기로 했다.
세 번째 금융사의 기획서를 받았을 때 이 문제를 공론화하기로 했다. 세 금융사의 기획서를 나란히 놓고 화면을 하나씩 대응시켰다. 순서와 문구가 다르고 인증 방식도 달랐다. 어떤 금융사에는 추가 동의 단계가 있었고, 다른 금융사에는 없었다.
그런데 화면의 생김새가 아니라 대출 과정에서 맡는 역할을 기준으로 놓고 보니 겹치는 쪽이 훨씬 컸다. 대략 80% 이상은 공통 구조로 묶을 수 있었다. 나머지도 완전히 새로운 화면이라기보다 문구가 다르거나 요소 하나가 더 있는 경우가 많았다.
세 금융사를 겹쳤는데 이 정도가 같다면 우연이라기보다 대출이라는 업무가 요구하는 공통 구조에 가까웠다. 금융사는 달라도 대출 과정에서 해야 할 일은 상당 부분 같았다.
우리는 그 같은 부분을 세 번 복사해 관리하고 있었다.
표준화는 요구를 거절하기 위한 일이 아니었다
표준화 이야기를 꺼냈을 때 가장 강하게 걱정한 사람은 이 SDK로 영업하던 담당자였다.
금융사가 요구하면 우리는 들어줘야 한다. 아직 만나지도 않은 금융사들이 무엇을 요구할지 모르는데, 어떻게 지금 틀을 정할 수 있느냐는 걱정이었다.
반박하기 어려운 지적이었다. 표준화는 지금 만드는 틀이고, 요구는 나중에 온다. 남은 27곳이 무엇을 요구할지 우리는 정말로 몰랐다. 계약 관계에서 우리는 을이었고, 금융사가 비즈니스 요구를 제시하면 대체로 수용해야 하는 쪽이었다. “표준화를 위해 금융사 요구를 거절하겠다”는 논리로는 이 일을 시작할 수 없었다.
그래서 주장을 뒤집었다.
요구는 모두 수용한다. 다만 지금처럼 코드를 복사해서 고치는 방식으로는 금융사 30곳의 요구를 계속 받을 수 없다. 금융사가 늘어날수록 한 번의 공통 수정에도 확인하고 배포할 곳이 늘어난다. 코드가 서로 달라지면 각각의 차이까지 이해해야 한다.
표준화는 요구를 덜 받기 위한 장치가 아니라, 더 많은 요구를 계속 받기 위한 조건이었다.
세 금융사의 화면을 대응시킨 결과도 근거가 됐다. 공통으로 묶을 수 있는 부분이 80% 이상이었고, 다른 부분도 문구나 일부 요소의 차이인 경우가 많았다. 앞으로 어떤 요구가 올지는 몰라도, 차이가 주로 들어오는 자리는 세 번의 사례로 어느 정도 좁힐 수 있었다.
표준화의 목표는 금융사 사이의 차이를 없애는 것이 아니라, 차이가 들어올 자리를 정해두는 것이었다.
이 내용을 OKR로 올렸다. 문서만 돌리지 않고 조직장, 영업팀, 경영진을 한 명씩 만나 화이트보드에 그렸다. 금융사 축과 화면 축을 그리고 칸을 채우면 복사 방식의 곱셈이 바로 보였다.
반대를 꺾어 통과시킨 것은 아니었다. 영업 담당자의 걱정을 정당한 전제로 인정하고, 그 걱정을 해결하는 방식으로 표준화를 다시 설명했다. 필요하다는 공감대가 생기면서 일이 시작됐다.
코드보다 먼저 대출이라는 업무를 맞췄다
승인 뒤 예상하지 못한 일이 있었다. 디자인팀 리드가 이 문제에 강하게 공감하면서 깊게 참여하기로 한 것이다. 디자이너 한 명이 추가로 배정됐고, 프론트엔드 개발자 2명과 디자이너 2명이 함께 작업하게 됐다.
처음에는 코드 문제라고 생각했다. 컴포넌트를 잘 나누고 props를 잘 설계하면 된다고 봤다. 막상 시작해보니 코드에서 혼자 정할 수 없는 질문이 먼저 나왔다.
- 어떤 화면들을 같은 종류로 묶을 것인가.
- 어떤 차이를 표현 유형(
variant)으로 둘 것인가. - 어디까지를 하나의 화면 실행 단위(Page)가 책임질 것인가.
이 질문에 답하려면 코드 구조보다 먼저 화면의 의미를 정해야 했다. 금융사마다 모양과 문구가 달라도 대출 과정에서 같은 일을 하는 화면인지 판단해야 했다.
세 금융사의 기획서를 나란히 놓고 비교했을 때 화면 이름과 순서는 조금씩 달랐지만, 대출이라는 업무가 진행되는 큰 흐름은 대부분 같았다.
flowchart LR
TERMS["약관 동의<br/>(TERMS_AGREEMENT)"] --> ID["신분증 인증<br/>(IDENTITY_VERIFICATION)"]
ID -->|"인증 성공"| INFO["고객 정보 입력<br/>(CUSTOMER_INFORMATION)"]
ID -->|"촬영 실패 시 별도 처리"| MANUAL["신분증 정보 수기 입력<br/>(IDENTITY_MANUAL_INPUT)"]
MANUAL --> INFO
INFO --> UW["대출 심사<br/>(UNDERWRITING)"]
UW --> RESULT["심사 결과 안내<br/>(RESULT)"]
RESULT --> CONDITION["대출 조건 확인<br/>(LOAN_CONDITION)"]
CONDITION --> DONE["대출 실행 완료<br/>(COMPLETED)"]
CONDITION --> CONSULT["상담 연결<br/>(CONSULTATION)"]
이 그림은 모든 금융사가 반드시 따라야 하는 하나의 런타임 순서를 뜻하지 않는다. 세 금융사의 화면을 같은 언어로 비교하기 위해 뽑아낸 업무 지도에 가까웠다. 어떤 금융사는 신분증 인증 이후 바로 고객 정보를 받았고, 어떤 금융사는 촬영 실패 상황을 별도 화면으로 분리했다. 약관을 하나의 긴 화면으로 제공하는 금융사가 있는 반면, 필수 약관과 선택 약관을 나눠 여러 화면으로 제공하는 금융사도 있었다.
하지만 화면이 맡는 역할을 기준으로 보면 대부분 같은 업무 안에 있었다.
디자이너가 새 금융사의 화면을 기존 variant와 무관하게 설계하고, 개발자가 그것을 받기 위해 조건 분기를 하나 더 넣는 일이 반복되면 표준화 전으로 돌아간다. 반대로 코드에만 존재하는 variant는 다음 디자인에서 재사용되지 않는다.
피그마의 variant와 코드의 variant가 같은 차이를 가리켜야 했다. 표준화 작업의 절반은 디자인과 코드가 함께 사용할 언어를 맞추는 일이었다.
여기서 정의한 화면 실행 단위(Page), 업무 유형(type), 표현 유형(variant)은 금융사 개발자에게 전달하는 스펙이 아니었다. 금융사 개발자가 이 모델을 사용해 화면을 조합하는 구조도 아니었다. 우리는 금융사의 요구를 받은 뒤 SDK를 빌드하기 전에 금융사별 Page와 type, variant를 우리 스펙에 맞게 미리 구성했고, SDK는 그 결과에 따라 화면을 렌더링했다.
따라서 표준화의 대상은 금융사의 요구가 아니라, 서로 다른 요구를 SDK로 옮기는 우리 내부의 제작 방식이었다. 금융사에는 각자의 요구에 맞는 화면을 제공하되, 우리 안에서는 그 차이를 데이터와 표현 유형(variant), 새로운 화면(Page), 금융사 전용 화면(Custom Page) 중 어디에 둘지 같은 기준으로 판단하려 했다.
업무 이름은 화면(Page)의 업무 유형(type)으로 뒀다
작업의 출발점은 화면에 공통 이름을 붙이는 것이었다.
컴포넌트 이름부터 정하지는 않았다. 금융사마다 무엇이라고 부르는지, 화면이 어떻게 생겼는지보다 대출 과정에서 그 화면이 무슨 일을 하는지를 먼저 봤다.
약관 동의 (TERMS_AGREEMENT)
본인 인증 (IDENTITY_VERIFICATION)
고객 정보 입력 (CUSTOMER_INFORMATION)
추가 정보 입력 (ADDITIONAL_INFORMATION)
대출 심사 (UNDERWRITING)
대출 조건 제안 (LOAN_OFFER)
계약 (CONTRACT)
성공 (SUCCESS)
실패 (FAILURE)이름이 생기자 세 금융사의 기획서를 같은 언어로 이야기할 수 있게 됐다. A사의 3번 Page와 B사의 2번 Page가 같은 역할을 한다는 말을 화면 번호 대신 type: IDENTITY_VERIFICATION으로 표현할 수 있었다.
이때 이 업무 이름을 화면(Page)과 별개의 업무 단계(step) 계층으로 둘지도 고민했다.
flowchart LR
subgraph TWO["가정한 2층 구조: 업무 단계(step) + 화면(Page)"]
direction TB
STEP["약관 동의<br/>업무 단계(step): TERMS_AGREEMENT"]
STEP --> REQUIRED["필수 약관 화면<br/>(Page)"]
STEP --> OPTIONAL["선택 약관 화면<br/>(Page)"]
STEP -.-> STATE["필요할 때 소유하는 책임<br/>단계 진행 상태·되돌리기·트랜잭션"]
end
subgraph ONE["실제로 선택한 1층 구조: 화면(Page)"]
direction TB
REQUIRED_ONE["필수 약관 화면 (Page)<br/>업무 유형(type): TERMS_AGREEMENT"]
OPTIONAL_ONE["선택 약관 화면 (Page)<br/>업무 유형(type): TERMS_AGREEMENT"]
end
왼쪽의 업무 단계(step)는 그 자체가 렌더링되는 화면이 아니다. 필수 약관 화면과 선택 약관 화면처럼 같은 업무 목적을 가진 여러 화면(Page)을 하나로 묶는 상위 계층이다. 필요하다면 단계 전체의 진행 상태, 되돌리기, 트랜잭션을 이 계층이 책임질 수 있다.
당시에는 오른쪽의 1층 구조를 선택했다. 별도의 부모 계층을 만들지 않고, 각 화면(Page)이 같은 업무 유형(type: TERMS_AGREEMENT)을 갖게 했다.
판단 기준은 그 계층이 런타임에 자기 상태를 가져야 하는지였다. 업무 단계가 별도 상태를 가져야 하는 경우는 단계 단위로 되돌리거나 단계 전체에 트랜잭션을 걸어야 할 때다. 하지만 뒤로가기는 Page 단위로 처리했고, 트랜잭션은 Page 안의 action 단위였다. 업무 단계가 따로 관리할 상태가 없었다.
금융사 사이의 비교를 위해서도 별도 계층은 필요하지 않았다. 관측 데이터를 조회할 때 Page의 type으로 묶으면 됐다. 집계 기준이 필요하다는 사실이 런타임 상태를 하나 더 만들어야 한다는 뜻은 아니었다.
계층을 두면 Page와 업무 단계의 진행 상태를 각각 관리하고 둘을 맞춰야 한다. 당시 요구를 해결하기보다 동기화해야 할 상태만 하나 더 생기는 구조였다.
최종적으로 화면 실행 단위(Page)는 다음 세 축을 가졌다.
flowchart TB
PAGE["화면 실행 단위 (Page)<br/>플로우 그래프의 실행 노드"]
PAGE --> TYPE["업무 유형 (type)<br/>어떤 업무인가<br/>비교·관측의 기준"]
PAGE --> VARIANT["표현 유형 (variant)<br/>같은 책임 안의<br/>표현 차이"]
PAGE --> ACTIONS["전환 규칙 (actions)<br/>화면 안의 업무와<br/>다음 이동 규칙"]
업무 유형(type)은 화면(Page)이 대출 과정에서 무슨 일을 하는지 나타냈다. 표현 유형(variant)은 의미와 책임을 유지한 채 달라질 수 있는 표현을 담았다. 전환 규칙(actions)은 Page 안에서 발생하는 업무와 전환 규칙을 정의했다.
표현 유형(variant) 폭발과 flow 중복은 다른 문제였다
표준화를 진행하며 구분해야 했던 문제가 하나 있었다. Page 내부 표현의 조합이 늘어나는 문제와 금융사마다 비슷한 flow 선언이 반복되는 문제는 서로 다른 계층의 문제였다.
예를 들어 여러 금융사가 다음 흐름을 반복해서 선언한다면 step 같은 상위 개념이 중복을 줄이는 데 도움이 될 수 있다.
금융사 A: 약관 동의(TERMS) → 본인 인증(IDENTITY) → 정보 입력(INFORMATION) → 계약(CONTRACT)
금융사 B: 약관 동의(TERMS) → 본인 인증(IDENTITY) → 정보 입력(INFORMATION) → 계약(CONTRACT)하지만 약관 Page 안에서 설명 영역이 있거나 없고, 동의 방식과 CTA 모양이 달라지는 문제는 step을 추가해도 해결되지 않는다. 이 문제를 Page 전체의 variant만으로 풀려고 하면 조합이 늘어난다.
TermsAgreementPage
├─ default
├─ compact
├─ with-description
├─ compact-with-description
└─ ...금융사별 조건을 Boolean prop으로 계속 추가하는 방식도 마찬가지였다.
<Page
bankA
hideDescription
showExtraConsent
openBottomSheetBeforeSubmit
useLegacyFlow
specialVerification
/>여섯 개를 서로 독립적인 Boolean prop으로 취급하면 이론상 64가지 조합이 생긴다. 실제로 쓰는 조합이 여섯 가지뿐이라면 나머지 58개가 허용되는지, 우연히 만들어진 잘못된 상태인지 코드만 보고 알기 어렵다. 공통 컴포넌트는 각 금융사에서 생긴 예외의 역사를 모두 기억하게 되고, 표준화했는데 변경은 오히려 더 위험해질 수 있다.
그래서 Page를 더 작은 책임 단위로 나눴다.
TermsAgreementPage
├─ TermsHeader
├─ TermsDescription
├─ TermsList
├─ AgreementControl
└─ PageCTA문구와 약관 목록처럼 구조를 바꾸지 않는 값은 데이터로 다루고, 화면 안의 영역별 차이는 책임이 작은 컴포넌트와 그 컴포넌트의 variant로 나눴다. Page 전체의 변형을 계속 늘리기보다 각 영역이 자신의 변화만 책임지게 했다.
다만 컴포지션이라는 이름만 붙인다고 조합 폭발이 사라지는 것은 아니다. 금융사 프로젝트가 공통 Page 안에 무엇이든 임의로 삽입할 수 있게 두면, Boolean prop이 만들던 불확실한 조합이 다른 형태로 돌아온다. 조합 가능한 부품과 허용된 상태를 피그마와 코드에서 함께 이름 붙이고, Storybook에서 확인할 수 있는 유한한 목록으로 유지하는 것이 중요했다.
이 구분 덕분에 판단할 수 있었다. 지금 겪는 문제가 Page 안의 표현 조합이라면 컴포넌트와 variant의 책임을 다시 나눠야 한다. 여러 금융사가 같은 흐름을 반복해서 선언하는 문제라면 그때 step이나 flow 추상화를 검토해야 한다. 성격이 다른 두 문제를 하나의 계층으로 해결하려 하지 않았다.
차이가 들어올 자리를 세 곳으로 제한했다
모든 차이를 variant로 만들지는 않았다. 요구의 성격에 따라 차이가 들어갈 자리를 세 가지로 나눴다.
| 차이의 성격 | 들어갈 자리 |
|---|---|
같은 업무 유형(type)과 책임을 유지하는 문구·데이터·표현 차이 |
데이터, 컴포넌트 조합, 표현 유형(variant) |
| 독립적인 성공·실패·재시도와 전환을 갖는 업무 단위 | 새로운 화면(Page) |
| 공통 구조에 넣으면 다른 금융사의 코드까지 복잡해지는 요구 | 금융사 전용 화면(Custom Page) |
표를 만드는 것보다 어려운 일은 실제 요구가 들어왔을 때 어느 칸에 넣을지 판단하는 것이었다.
flowchart TD
START["새 금융사 요구"] --> SAME{"기존 화면(Page)과<br/>같은 업무와 책임인가?"}
SAME -- "아니오" --> NEW["새로운 화면<br/>(Page)"]
SAME -- "예" --> TX{"독립적인 업무·서버 요청(API 호출)·실패·재시도·<br/>결과에 따른 전환이 있는가?"}
TX -- "예" --> NEW
TX -- "아니오" --> DATA{"값만 바뀌는가?"}
DATA -- "예" --> CONTENT["데이터로 주입"]
DATA -- "아니오" --> COMMON{"공통 구조에 넣어도<br/>다른 금융사가 복잡해지지 않는가?"}
COMMON -- "예" --> VARIANT["컴포넌트 조합 또는<br/>표현 유형 (variant)"]
COMMON -- "아니오" --> CUSTOM["금융사 전용 화면<br/>(Custom Page)"]
앞의 질문들은 새로운 Page가 필요한지를 판단하고, 마지막 질문은 공통 구조와 Custom Page 사이의 경계를 정한다. 표현 차이를 잘못 나누면 컴포넌트를 다시 쪼개면 된다. Page의 책임을 잘못 나누면 공통 API와 actions가 알아야 할 업무까지 늘어난다.
하단 시트(BottomSheet) 하나는 표현 유형(variant)인가, 새로운 화면(Page)인가
판정이 갈렸던 사례가 있었다.
한 금융사는 CTA를 누르면 바로 다음 화면으로 넘어갔다. 다른 금융사는 CTA를 누르면 BottomSheet가 열렸고, 그 안에서 추가 법적 동의를 받아야 했다.
모양만 보면 variant였다. 기존 Page에 BottomSheet 컴포넌트 하나를 붙이면 되는 것처럼 보였다.
하지만 시트 안에서 일어나는 일을 보면 달랐다. 새로운 법적 동의를 받고, 그 내용을 서버로 보내는 독립적인 API 호출이 있었다. 호출에 실패하면 그 부분만 다시 시도해야 했고, 결과에 따라 다음 경로도 달라졌다.
이 요구를 기존 Page의 variant로 넣으면 variant의 계약 안에 동의 요청, 대기, 실패, 재시도, 다음 경로의 판단까지 들어온다. 기존 Page가 원래 맡지 않았던 업무 트랜잭션을 알아야 하는 것이다.
따라서 Page인지 아닌지는 화면을 얼마나 크게 차지하는지로 판단하지 않았다. 플로우 그래프에서 독립적인 업무와 전환을 책임지는 노드가 필요한지를 봤다. BottomSheet처럼 보여도 독립된 상태와 전환이 있다면 새로운 Page로 나눴다.
flowchart LR
CURRENT["현재 화면<br/>(Page)"] -->|"버튼 클릭 (CTA)"| CONSENT["추가 법적 동의<br/>새로운 화면 실행 단위 (Page)<br/>표현: 하단 시트 (BottomSheet)"]
CONSENT -->|"요청 성공"| NEXT["다음 화면<br/>(Page)"]
CONSENT -->|"요청 실패"| RETRY["해당 업무만 재시도"]
RETRY --> CONSENT
UI의 모양이 아니라 업무 트랜잭션과 전환의 경계가 Page를 결정했다.
임의의 확장점은 두지 않았다
variant와 Custom Page 사이에 확장점을 하나 더 둘 수도 있었다. 공통 Page가 자리만 비워두고 그 안의 내용은 금융사별 프로젝트가 채우는 방식이다. 요소 하나가 추가되는 정도의 차이에는 적합해 보였다.
하지만 두지 않았다.
variant는 이름이 붙은 유한한 목록이다. 피그마와 코드에서 같은 이름으로 관리하고 Storybook에 모든 경우를 올려 검증할 수 있다. 반면 무엇이든 들어올 수 있는 확장점은 가능한 결과를 목록으로 만들기 어렵다. 앞에서 Boolean prop 조합을 경계한 문제가 다른 모습으로 되돌아온다.
피그마와의 대응도 이유였다. 임의의 확장점은 디자이너와 개발자가 함께 정한 variant 경계 밖으로 나가는 문이다. 다음 금융사의 디자인이 그 문으로 들어오기 시작하면 화면의 최종 모습을 누가 책임지고 검증하는지 다시 불분명해진다.
갈래를 variant, 새로운 Page, Custom Page로 유지한 것은 단순화를 위한 생략이 아니었다. 검증 가능한 조합만 공통 구조 안에 남기려는 선택이었다.
새 Page를 만들기로 정하면 판단이 하나 더 남았다. 기존 type을 재사용할 것인지, 새로운 type을 만들 것인지다.
필수 동의와 선택 동의를 두 Page로 나눈 금융사라면 Page는 둘이어도 둘 다 type: TERMS_AGREEMENT로 묶을 수 있다. 반면 한 금융사에만 있는 별도 확인 절차라면 새로운 type이 필요하다.
두 선택의 비용은 달랐다. 기존 type을 재사용하면 Page 수만 늘어난다. 새로운 type을 만들면 금융사끼리 비교하던 관측 축에 한 곳에만 있는 항목이 추가된다. 그래서 Page를 나눌 때는 기존 type 안에 들어갈 수 있는지를 먼저 봤다.
금융사 전용 화면(Custom Page)은 공통화의 실패가 아니었다
어떤 요구는 어느 칸에도 들어가지 않았다.
한 금융사만의 사정에서 나온 Page이고, 공통 구조로 흡수하면 그 금융사의 사정을 다른 금융사의 코드까지 모두 알아야 하는 종류였다. 이런 요구는 해당 금융사 프로젝트의 Custom Page로 남겼다.
표준화를 하면서 예외 자리를 공식화하는 일이어서 처음에는 타협이라고 생각했다. 지금 돌아보면 이 자리가 영업팀에 한 약속을 실제로 지키는 곳이었다. 요구를 모두 수용하겠다는 말이 빈말이 아니려면 공통 구조에 들어오지 않는 요구도 받을 수 있어야 한다. 그 자리가 없으면 공통 컴포넌트가 모든 예외를 떠안고, 결국 아무도 건드리기 어려운 코드가 된다.
Custom Page는 공통 코어를 지키기 위해 의도적으로 열어둔 문이었다.
Custom Page의 개수만으로 표준화의 성공과 실패를 판단하지도 않았다. 공통 레이아웃, 입력 컴포넌트, 액션 처리, 검증 로직 같은 기본 요소를 재사용하면서 금융사별 조합만 달라지는 구조라면 Custom Page가 여러 개여도 변경 범위는 격리된다. 반대로 공통 Page 하나에 금융사별 조건문과 예외 prop이 계속 추가된다면, Custom Page가 없어도 위험한 구조다.
다만 그 문을 언제 열지는 계속 판단해야 했다. 한 금융사에서만 나온 요구는 우선 해당 금융사 안에 두었다. 같은 의미의 요구가 다른 금융사에서도 반복되면 그때 공통 구조로 올렸다. 한 번 나온 요구를 미리 공통화하면 틀린 추상화를 만들 가능성이 컸다.
업무 단계(step) 계층은 필요해지는 시점까지 미뤘다
별도의 step 계층을 두지 않았다는 것은 영원히 필요 없다고 결론 내렸다는 뜻이 아니었다. 당시 해결해야 할 문제와 도입 비용을 비교해 우선순위를 정한 것이었다.
Page의 type, variant, 컴포넌트 구조는 이미 존재하는 디자인과 화면 구현 방식에 직접 영향을 준다. 잘못된 추상화를 만들면 디자이너와 개발자가 다시 언어를 맞춰야 하고, 이미 배포된 금융사 화면도 검증해야 한다. 먼저 변경 비용이 높은 영역부터 정리했다.
- 화면이 맡는 업무의 의미를 정의한다.
- 금융사 사이의 공통 역할을 찾는다.
- 데이터·컴포넌트·
variant·Page의 경계를 정한다. - 공통 구조에 들어오지 않는 요구는 Custom Page로 격리한다.
step은 그다음에 실제 신호가 생기면 도입할 수 있는 내부 추상화였다.
- 하나의 업무 단계가 여러 Page로 반복해서 분리된다.
- 금융사마다 flow 선언이 거의 같은데 Page 노드 때문에 중복된다.
- 단계 단위의 되돌리기나 트랜잭션 관리가 필요해진다.
이런 요구가 없다면 type으로 비교하고 Page 단위로 상태와 전환을 관리하는 것으로 충분했다. 필요해졌을 때도 금융사 개발자에게 Page, type, variant 변경을 요구하지 않고 SDK 내부에서 먼저 검토할 수 있었다.
추상화는 많이 만드는 것이 목표가 아니었다. 변경의 성격이 다른 문제를 같은 계층에서 해결하지 않는 것이 여러 금융사를 운영할 수 있는 구조를 만드는 기준이었다.
화면(Page)의 업무 유형(type)은 관측의 기준이 됐다
이 작업을 시작한 목적은 코드 재사용이었다. 복사된 750개의 화면을 따로 관리하는 구조를 만들지 않는 것.
작업이 끝나자 예상하지 못한 효과가 하나 따라왔다.
Page에 공통 type이 생기면서 금융사마다 구현이 달라도 같은 기준으로 비교할 수 있는 바탕이 생겼다. A사의 type: IDENTITY_VERIFICATION과 B사의 type: IDENTITY_VERIFICATION은 UI와 구현이 달라도 둘 다 본인 인증 단계였다.
type만 정한다고 지표가 생기는 것은 아니다. 하지만 이벤트와 에러에 같은 type을 실을 수 있게 된다. 이후 에러 처리와 모니터링을 설계할 때 이를 공통 필드로 사용했고, 어느 업무 단계에서 이탈과 지연, 실패가 발생했는지를 금융사 사이에서 같은 축으로 볼 수 있게 됐다.
화면 번호만 있을 때는 이런 비교가 어려웠다. A사의 3번 Page와 B사의 2번 Page가 같은 업무 단계라는 공통 축이 없었기 때문이다.
표준화가 코드와 디자인뿐 아니라 관측의 언어까지 만든 셈이었다. 처음부터 의도한 결과는 아니었다.
차이를 관리할 구조가 필요했다
세 번째 금융사의 기획서를 앞의 두 곳과 겹쳐보면서 시작한 일이었다.
화면 역할의 80% 이상을 공통 구조로 묶을 수 있다는 것을 확인했다. 복사 방식을 30곳까지 늘렸을 때 생기는 곱셈을 그림으로 설명했고, 표준화를 요구를 거절하는 장치가 아니라 더 많은 요구를 받기 위한 조건으로 제안했다.
실행 단계에서 이것은 코드만의 문제가 아니었다. 피그마와 코드가 같은 variant를 가리켜야 했고, 화면의 생김새보다 업무상 역할을 먼저 정의해야 했다. 다만 이 모델을 금융사 개발자에게 노출하지는 않았다. 표준화한 것은 금융사의 요구가 아니라, 서로 다른 요구를 금융사별 SDK로 옮기는 우리 내부의 제작 방식이었다.
표준화의 기본 단위는 플로우 그래프의 실행 노드인 Page였다. Page에는 업무와 관측의 축인 type, 표현 차이인 variant, 전환 규칙인 actions가 있었다. 문구와 목록은 데이터로, Page 안의 제한된 표현 차이는 컴포넌트 조합과 variant로 다뤘다. 독립적인 업무와 전환이 생기면 새로운 Page로 나눴고, 공통 구조를 복잡하게 만드는 금융사 고유 요구는 Custom Page로 격리했다.
별도의 step 계층은 당시 필요한 상태와 책임이 없었기 때문에 미뤘다. 임의의 확장점도 두지 않았다. 가능한 조합을 피그마와 Storybook에서 유한한 목록으로 검증하기 위해서였다.
결과물은 공통 컴포넌트만이 아니었다. Page의 구조, 차이를 판정하는 규칙, 금융사를 비교할 수 있는 공통 type이 함께 남았다.
Page 모델을 정하고 나니 다음 질문이 생겼다. actions를 누가, 어떤 결과에 따라 실행할 것인가.
TERMS_AGREEMENT 다음에 IDENTITY_VERIFICATION이 오는지 ADDITIONAL_INFORMATION이 오는지는 심사 결과와 사용자 조건에 따라 달라진다. 그 판단에 필요한 정보는 금융사 서버만 갖고 있었다. SDK는 어떤 Page가 렌더됐는지는 기록할 수 있지만, 금융사가 왜 그 Page를 선택했는지와 그 전환이 유효한지는 알 수 없었다.
다음 편에서는 그 이야기를 다룬다. 타입 검증을 기대하기 어려운 환경에서 금융사 개발자와 어떤 actions 계약을 맺었는지, 그리고 그 계약을 한 번 크게 바꿔야 했던 이유를 정리한다.