소프트웨어 엔지니어
신혼부부의 장기 재무 설계를 자동화하는 AI 모바일 금융 서비스입니다. 일반적으로 재무 설계는 전문가 상담이 필요한 복잡하고 진입장벽이 높은 영역이지만, Finberry는 챗봇과의 자연스러운 대화 형태로 이를 풀어냈습니다.
사용자가 챗봇 온보딩을 통해 소득·자산·부채·소비 패턴 등을 입력하면, AI가 결혼·출산·내집마련·은퇴 등 생애주기 이벤트에 맞춰 맞춤형 재무 로드맵과 액션 플랜을 생성합니다. 실시간 스트리밍 응답, 음성 입력, 강력한 데이터 검증, 모바일 퍼스트 UX를 통해 복잡한 금융 도메인을 누구나 사용할 수 있는 인터페이스로 구현하는 것이 핵심 목표였습니다.
신혼부부 대상 AI 재무 상담 서비스 Finberry는 사용자의 금융 정보를 받아 AI가 맞춤형 재무 분석 리포트를 생성하는 구조입니다. 문제는 AI가 리포트를 한 번에 생성하는 방식에서는 응답까지 5~10초의 대기 시간이 발생해 사용자가 빈 화면을 응시하며 이탈하는 경우가 많았다는 점입니다. 특히 모바일 환경에서 키보드로 수십 개의 재무 정보를 입력해야 했기에 입력 피로도가 높아 온보딩 완료율이 저조했고, 재무 상담이라는 신뢰가 중요한 도메인에서 "AI가 멈춘 것 같은" 인상은 치명적이었습니다.
OpenAI Chat Completions API의 stream 옵션을 활용해 토큰 단위로 응답을 전송하고, SSE(Server-Sent Events) 프로토콜로 클라이언트에 실시간 푸시했습니다. EventSource API의 한계(POST 요청 미지원)를 우회하기 위해 fetch + ReadableStream을 사용한 커스텀 SSE 클라이언트를 구현했고, 네트워크 끊김 시 자동 재연결과 부분 응답 누적 처리를 핸들링했습니다. 입력 측면에서는 OpenAI Whisper API를 모바일 마이크와 연동해 음성→텍스트 변환 파이프라인을 구축했고, 60대 이상 사용자도 키보드 없이 정보를 입력할 수 있게 했습니다. 이 과정에서 AI 파트와 스트리밍 응답 포맷과 리포트 JSON 스키마 구조를 긴밀히 협의하며, 프론트엔드가 토큰 유실이나 파싱 오류 없이 안정적으로 처리할 수 있는 데이터 계약을 함께 설계했습니다.
첫 토큰 응답까지의 시간(TTFT)을 500ms 이하로 단축 했고, 사용자는 AI가 "실시간으로 생각하는" 듯한 체험을 통해 평균 8~10초의 응답 대기를 자연스럽게 받아들이게 됐습니다. 음성 입력 옵션은 모바일 입력 부담을 크게 줄여, 디지털 친숙도가 낮은 사용자층의 온보딩 완료율 개선에 기여했습니다.
UX 측면에서 "응답이 끝나는 속도"보다 "응답이 시작되는 속도"가 사용자 체감에 훨씬 큰 영향을 준다는 점이 인상 깊었고, AI 서비스에서 스트리밍은 단순한 기술 선택이 아니라 핵심 UX 자산이라는 인식을 갖게 됐습니다. 또한 AI 파트와 데이터 인터페이스를 사전에 합의하고 조율해 본 경험을 통해, AI 서비스의 안정성은 모델 성능만큼이나 프론트와 AI 간 데이터 계약을 얼마나 명확히 설계하느냐에 달려 있다는 점을 배웠습니다.
Finberry는 사용자가 입력한 금융 정보를 AI에게 전달해 재무 로드맵을 생성하는 서비스로, AI가 응답하는 JSON에는 재무 분석 결과·로드맵· 추천 액션 등 310개 이상의 동적 필드가 포함됐습니다. AI 응답은 본질적으로 비결정적이라 필드 누락, 타입 불일치(숫자 문자열, null 등), 예상치 못한 값이 빈번하게 발생했고, 이를 그대로 렌더링하면 화이트스크린이나 잘못된 정보 노출로 이어지는 치명적 문제였습니다. 기존의 단순 typeof 검사로는 중첩된 필드 간 의존성(예: "대출 있음" 체크 시 대출 금액 필수)을 검증할 수 없었습니다.
Zod를 도입해 모든 AI 응답과 사용자 입력에 스키마 기반 검증을 적용했습니다. 특히 superRefine과 discriminatedUnion을 활용해 크로스 필드 검증(예: 대출 보유자의 잔액·이율 필수, 자녀 계획자의 출산 시기 필수 등)을 선언적으로 표현했습니다. 실패 시 사용자 친화적 에러 메시지를 자동 생성하는 ErrorMap을 작성했고, AI 응답 검증 실패는 별도 로깅 채널로 모아 프롬프트 개선에 활용했습니다.
런타임 에러가 크게 감소했고, AI 통신 계층에서 발생하던 알 수 없는 크래시가 거의 사라졌습니다. 개발자는 검증된 데이터를 신뢰하고 다음 로직을 작성할 수 있어 디버깅 시간이 단축됐고, AI 응답 형식이 변경되더라도 스키마 한 곳만 수정해 전체 시스템 안정성을 유지할 수 있었습니다.
AI는 같은 질문에도 응답 형식이 조금씩 달라질 수 있기 때문에, 프로그램이 실행되는 중에도 데이터 구조를 계속 검증하는 안전장치가 반드시 필요하다는 것을 깨달았습니다. 또한 검증 규칙을 코드 여기저기에 나눠 작성하지 않고 스키마 한 곳에 모아두니, AI 응답 형식이 바뀌어도 스키마만 수정하면 전체에 반영되어 유지보수가 훨씬 수월했습니다.
Finberry는 신혼부부를 타겟으로 출시한 AI 재무 상담 서비스였습니다. 출시 초기 실제 사용자층과 사용 패턴에 대한 정량적 근거가 부족해 다음 제품 방향성을 정하기 어려웠고, 팀 내부 가설(20~30대·PC 위주)과 실제 사용자 행동 간 괴리가 있을 가능성이 높았습니다. 이를 검증할 데이터 분석 인프라가 전무했던 상태였습니다.
GA4에 플로우별 이벤트를 설계해 온보딩 단계별 이탈 지점, 디바이스, 연령대를 추적했고, Mixpanel로는 주요 단계별 전환율을 측정했습니다. 직접 이벤트 네이밍 컨벤션을 만들고 데이터 레이어를 구축해 분석 일관성을 확보했고, 분석 결과를 매주 팀에 공유하며 가설→실험→검증 사이클을 만들었습니다.
60대 이상 사용자가 예상보다 큰 비중을 차지한다는 사실과, 전체 접속의 95%가 모바일에서 발생한다는 점을 발견했습니다. 이는 기존 가설을 완전히 뒤집는 발견이었고, 이를 근거로 모바일 퍼스트 UX 전략으로 제품 방향을 피벗했습니다. 폰트 크기, 터치 영역, 음성 입력 등 시니어 친화적 요소를 우선순위화하는 의사결정으로 이어졌습니다.
"느낌"이 아니라 데이터로 의사결정하는 경험을 통해, 개발자가 단순히 코드를 작성하는 사람이 아니라 제품 방향성에 영향을 줄 수 있는 핵심 역할임을 체감했습니다. 또한 객관적인 데이터를 근거로 가지고 있으니 제품 회의에서 막연한 느낌이 아니라 구체적인 숫자로 확신을 가지고 의견을 제시할 수 있었고, 팀 내에서 제 제안의 설득력이 훨씬 높아지는 것을 경험했습니다. 무엇보다 제 의견에서 출발한 방향으로 제품이 만들어지다 보니 그 과정에서 자연스럽게 책임감이 더 커졌고, 작은 디테일 하나까지 세세하게 챙기려고 노력하게 됐습니다.
Finberry는 챗봇 기반 AI 재무 상담 서비스로, 금융 데이터·챗봇 플로우· UI 상태(모달·로딩·에러)라는 세 축의 상태가 컴포넌트 곳곳에 흩어져 있어 동일한 상태를 여러 곳에서 동기화하는 코드가 반복되고 있었습니다. 특히 "지금 챗봇이 몇 단계인지"와 "사용자가 입력한 값"이 한 덩어리로 얽혀 있어, 한 부분을 수정하면 관련 없어 보이던 다른 부분까지 망가지는 일이 잦았습니다. 또한 화면을 그리는 컴포넌트 안에 계산·검증 같은 로직까지 함께 들어 있어, 로직만 따로 테스트하거나 다른 화면에서 재사용하기가 어려웠습니다.
Recoil의 atom과 selector를 활용해 상태를 책임별로 분리했습니다. surveyState(140+ 필드)는 챗봇 UI 세션의 원본 상태(본인+배우자 입력, 챗봇 플로우, UI 가시성, 리포트 결과)를 담당하고, selfState(120+ 필드)는 백엔드와 LLM에 전달하기 위한 정규화된 본인 기준 데이터(지출·대출·자산을 세분화한 snake_case 포맷)를 담당하도록 역할을 명확히 나눠, 총 260개 이상의 필드를 중앙 관리했습니다. 상태 변경은 stateHandlerSelector(Redux-like 액션 패턴)와 selfSpouseSelector(본인/배우자 필드 동적 접근) 같은 selector를 통해 일관되게 처리했습니다. 컴포넌트에서 직접 atom을 구독하지 않고, 도메인별로 추상화된 40개 이상의 커스텀 훅(useChatFlow, useFinanceData, useUserProfile 등)으로 비즈니스 로직과 UI를 분리해, 훅 내부에서 파생 상태와 사이드 이펙트를 캡슐화했습니다.
같은 데이터를 사용하는 컴포넌트가 추가될 때마다 새로운 보일러플레이트 없이 훅 한 줄로 재사용할 수 있어 개발 속도가 크게 향상됐습니다. 비즈니스 로직 변경 시 훅만 수정하면 모든 사용처에 일관되게 반영돼 버그가 줄었고, 신규 기능 개발 시 기존 훅을 조합하는 방식으로 확장이 쉬워졌습니다.
대규모 상태 관리의 핵심은 라이브러리가 아니라 atom 설계, 네이밍 컨벤션, 그리고 책임 분리에 있음을 체감했습니다. 처음에는 atom을 잘게 쪼개는 것이 과해 보였지만, 시간이 지날수록 잘게 쪼갠 atom이 리렌더 최적화와 디버깅에 결정적 도움을 준다는 것을 경험했습니다.
Finberry는 스타트업 서비스로, 빠른 배포와 잦은 제품 업데이트를 통해 시장의 피드백을 신속하게 반영하는 것이 초기 성장의 핵심이었습니다. 그러나 AWS S3/CloudFront에 정적 프론트엔드를 배포하는 구조임에도 초기에는 로컬에서 빌드한 결과물을 수동으로 S3에 업로드하는 방식이었습니다. 빌드 환경이 개발자마다 달라 같은 코드에서 다른 결과물이 나오는 문제가 있었고, 배포 시간이 5~10분씩 소요되며 환경 변수 누락 같은 휴먼 에러가 잦았습니다.
GitHub Actions로 PR 머지 트리거 기반의 자동 배포 파이프라인을 구축했습니다. 환경별로 별도 워크플로우 (dev/production)를 작성하고, 각각 다른 환경 변수와 S3 버킷을 매핑했습니다. 특히 dev와 production 환경을 명확히 분리한 이유는, 개발 중인 코드가 곧바로 운영 환경에 올라가지 않도록 dev 환경에서 충분히 테스트하고 이슈를 수정한 뒤에만 production에 배포하는 안전장치를 두기 위함이었습니다. 이를 통해 스타트업 특유의 빠른 배포 속도를 유지하면서도 운영 안정성을 함께 확보할 수 있었습니다. 빌드 → S3 sync → CloudFront invalidation 순서로 구성하되, CloudFront 캐시 무효화를 빌드 결과물 hash 기반으로 처리해 캐시 갱신 누락을 방지했고, 빌드 결과는 워크플로우 상태와 함께 디스코드 알림으로 팀에 공유했습니다.
배포가 코드 머지와 동시에 자동화되어 휴먼 에러가 사라졌고, 환경별로 안정적인 분리가 가능해졌습니다. 배포 시간도 일정하게 관리되어 긴급 수정이 필요한 상황에서도 안전하게 대응할 수 있게 됐습니다. 또한 production 배포 전 dev 환경에서 충분한 테스트 기간을 확보할 수 있게 되면서, 개발자·디자이너·기획자가 함께 실제 동작을 확인하며 의견을 맞추는 협업 시간이 자연스럽게 생겼고, 운영에 반영되기 전 이견을 조율하거나 UX를 다듬을 수 있게 됐습니다.
처음에는 CI/CD를 단순히 "배포를 자동화해 시간을 아끼는 도구"로 생각했지만, 실제로 구축해보니 CI/CD는 팀이 속도와 안정성을 동시에 챙기기 위한 장치에 가깝다는 것을 배웠습니다. 특히 dev/production을 분리하면서 생긴 테스트 기간이 단순한 기술적 단계가 아니라 개발자·디자이너·기획자가 같은 화면을 보고 이야기할 수 있는 협업의 장이 된 경험이 인상 깊었습니다. 기술적인 결정 하나가 팀의 일하는 방식까지 바꿀 수 있다는 점을 체감했고, 앞으로 인프라·파이프라인을 설계할 때도 코드 관점뿐 아니라 팀의 협업 흐름까지 함께 고려 해야 한다는 기준을 갖게 됐습니다.
Finberry의 유료 AI 재무 컨설팅 상품을 판매하기 위해 결제 시스템을 연동해야 했습니다. 상품 선택부터 결제 완료까지 주문 → 결제로 이어지는 흐름을 구성하고, 각 단계마다 화면과 데이터가 올바르게 갱신되도록 관리해야 했습니다. 특히 결제 도중 사용자가 뒤로가기를 누르거나 창을 닫고 다시 돌아오는 경우, 결제가 이미 진행됐는지 / 실패했는지를 판단해 적절한 화면으로 복구해주는 처리가 까다로웠습니다. 게다가 런칭 직전 "심사 제출 후 환불 기능이 반드시 있어야 한다"는 요구사항이 추가로 들어와, 짧은 기간 안에 환불 플로우까지 함께 구현해야 했습니다.
Toss Payments SDK를 연동해 사용자가 선택한 상품을 주문으로 확정하고, 해당 주문을 결제 단계로 넘기는 흐름을 구현했습니다. 결제 후 사용자가 뒤로가기로 이탈했다 돌아온 경우에는, 서버에 현재 주문의 결제 상태를 조회해 미완료 상태면 재시도 안내를, 이미 완료된 결제면 완료 페이지로 이동시키는 복구 로직을 추가했습니다. 또한 런칭 임박 시점에 긴급하게 들어온 환불 요구사항에는 백엔드 팀과 긴밀히 협의해 환불 API를 연동하고, 사용자가 환불을 요청할 수 있는 UI를 짧은 기간 안에 함께 구현했습니다. 결제 금액 검증 같은 민감한 로직은 클라이언트가 아닌 서버에서 처리하도록 역할을 명확히 나눴습니다.
결제와 환불 플로우가 안정적으로 동작했고, 결제 중 이탈 후 재진입했을 때도 사용자가 혼란 없이 다음 액션을 안내받을 수 있었습니다. 특히 런칭 직전에 추가된 환불 요구사항을 일정 내에 성공적으로 대응해, 서비스 오픈 일정을 지연시키지 않고 예정대로 런칭할 수 있었습니다.
SDK를 붙이는 작업 자체보다 "결제 중 사용자가 이탈했다가 돌아왔을 때" 같은 엣지 케이스를 어떻게 일관되게 복구할지가 훨씬 더 어려운 문제였다는 것을 절감했습니다. 또한 런칭 임박 시점에 긴급 요구사항(환불 기능)이 들어왔을 때, 기존 결제 플로우와 충돌 없이 빠르게 대응하려면 백엔드 팀과의 긴밀한 협업과 책임 분리가 필수라는 것을 체감했습니다. 금전이 오가는 기능은 프론트엔드와 백엔드가 각자 어디까지 책임질지 명확히 나누는 것이 가장 중요한 설계 포인트라는 원칙을 얻었습니다.
CreditConnect는 이커머스 사업자를 위한 대출 중계 플랫폼 으로, 사용자 그룹에 따라 세 가지 오피스 구조로 설계되었습니다. FO(Front Office)는 쿠팡·네이버스토어 등을 운영하는 판매 사장님들이, BO(Back Office)는 회사 내부 운영 직원이, PO(Partner Office)는 금융사 직원이 사용합니다.
판매몰을 운영하는 셀러는 카드결제·할부 등의 이유로 판매금이 바로 지급되지 않고 정산까지 시간이 걸립니다. 고객이 '구매확정'을 하면 정산예정금이 잡히는데, CreditConnect는 이 정산예정금을 담보로 대출을 신청할 수 있도록 금융기관과 셀러를 연결해 줍니다. 일반적으로 금융기관이 증빙자료가 부족한 사업자에게 판매몰 자료를 일일이 요청하기 어렵다는 문제를 해결하기 위해, CreditConnect는 각 판매몰 어드민에 접속해 데이터를 자동 스크래핑하고 대출 시작부터 종료까지 매일 최신 자료를 금융기관에 제공하며 중간다리 역할을 수행합니다.
CreditConnect는 셀러(FO)·내부 운영(BO)·금융 파트너(PO) 3개 포탈로 구성된 금융 플랫폼으로, DevOps 정책상 각 포탈이 독립된 Git 레포지토리로 분리된 구조였습니다. 저는 프로젝트 초기부터 이 환경에서 작업에 참여했고, BO와 PO는 화면과 로직이 상당 부분 겹쳤지만 공통 코드를 추출할 수단 없이 동일한 구현을 각 레포에 반복 작성해야 했습니다. 그 결과 한쪽 레포에서 수정한 버그를 다른 레포에 반영하는 걸 놓치기 쉬웠고, 시간이 지날수록 각 레포의 스타일과 패턴이 조금씩 드리프트되는 문제가 있었습니다.
이 환경에서 일관성을 유지하기 위해 "패턴 기반 동기화" 전략을 취했습니다. 먼저 코드 컨벤션과 개발 가이드 문서를 3개 레포의 공통 기준 으로 삼아, 모든 레포가 동일한 폴더 구조·네이밍·상태 관리 패턴·API 호출 방식을 따르도록 정렬했습니다. 공통 UI 컴포넌트나 SearchTable 같은 재사용 패턴은 한 레포에서 먼저 설계한 뒤 동일한 형태로 나머지 레포에 전파했고, BO/PO처럼 유사한 포탈 간에는 기능 구현 시 양쪽을 한 번에 작업하는 루틴을 만들었습니다. 또한 PR 리뷰 시 "이 변경이 다른 레포에도 반영되어야 하는가"를 체크리스트 항목으로 포함시켜 누락을 줄였습니다.
공유 패키지 없이도 3개 레포의 코드·디자인 일관성을 유지할 수 있었고, BO와 PO는 마치 하나의 프로덕트처럼 동일한 UX와 코드 품질을 가졌습니다. 버그 수정 누락과 패턴 드리프트가 크게 줄었고, 새로 합류한 팀원도 한 레포에서 익힌 구조를 다른 레포에 그대로 적용할 수 있어 학습 비용이 낮아졌습니다.
이상적인 아키텍처(monorepo, shared package)가 없더라도 공유된 기준과 루틴만으로 상당한 일관성을 확보할 수 있다는 점을 체감했습니다. 다만 복붙 기반 구조는 명확한 기술 부채이며, 팀 규모나 레포 수가 늘어나면 결국 공유 패키지 구조로 이전이 필요하다는 점도 함께 배웠습니다. 제약 속에서 문제를 푸는 경험을 통해 "완벽한 아키텍처"보다 "상황에 맞는 실용적 규칙 설정" 이 실제 현장에서 더 중요한 경우가 많다는 점을 인식하게 됐습니다.
CreditConnect의 BO(Back Office)와 PO(Partner Office)는 관리자 도구 성격이 강한 포탈로, 30개 이상의 페이지가 대부분 "필터 조건 입력 → 검색 → 결과 테이블 → 행 선택 → 상세/수정"이라는 동일한 패턴을 반복하고 있었습니다. 페이지마다 비슷한 UI를 매번 새로 짜다 보니 일관성이 떨어지고 개발 시간도 누적되며 중복 코드가 많아졌고, 신규 페이지 추가에 평균 3~5일이 소요됐습니다.
먼저 86개 이상의 재사용 UI 컴포넌트(Input, Table, Modal, Dropdown, DatePicker 등)를 디자인 시스템으로 정리했습니다. 그 위에 config-driven SearchTable 패턴을 도입했는데, 이는 페이지마다 검색·테이블·페이지네이션 로직을 매번 새로 코드로 작성하던 기존 방식 대신, 각 페이지에서는 "어떤 컬럼을, 어떤 필터로, 어떤 API로 불러올지"만 설정(config) 객체로 선언하면 나머지 동작(검색·페이지네이션·정렬·행 선택·CSV 내보내기)은 공통 컴포넌트가 자동으로 처리해주는 방식입니다. 즉, 페이지 개발자는 columns, filters, fetch 함수만 객체로 넘기면 전체 화면이 조립되는 구조로 만든 것입니다. 또한 페이지마다 달라지는 커스텀 동작은 render prop과 hooks를 통해 확장 가능하도록 열어두어, 공통화의 이점과 페이지별 유연성을 동시에 확보했습니다.
30개 이상 페이지에서 개발 시간이 평균 60% 단축됐고, UI 일관성이 자동으로 유지돼 디자인-개발 간 커뮤니케이션 비용도 줄었습니다. 신규 페이지 추가가 1~2일 단위로 가능해졌고, 기존 페이지 수정도 패턴 변경 한 번으로 일괄 적용 가능해 유지보수성이 크게 향상됐습니다.
관리자 도구처럼 비슷한 화면이 계속 반복되는 서비스에서는, 공통 구조를 한 번만 잘 만들어두면 개발 속도가 정말 크게 빨라진다는 것을 직접 경험했습니다. 다만 너무 많은 것을 공통 구조에 한꺼번에 집어넣으면, 나중에 특정 페이지에서만 다르게 동작해야 할 때 오히려 수정이 까다로워진다는 점도 함께 배웠습니다. 그래서 공통 구조는 최대한 단순하게 유지하되, 페이지마다 다르게 만들어야 하는 부분은 언제든 쉽게 바꿀 수 있도록 열어두는 균형이 중요하다는 것을 깨달았습니다.
CreditConnect 프로젝트에서 만 2년이 되기 조금 전, 팀에 새로운 프론트엔드 멤버가 합류하면서 팀이 2명에서 3명으로 확장되었습니다. 이 시점에 팀 내부에 코드 컨벤션과 개발 가이드 문서가 전혀 없다는 문제가 드러났습니다. 다른 사람이 작성한 코드를 이어서 작업할 때 새 기능을 바로 추가하지 못하고 리팩토링부터 진행해야 하는 경우가 많았고, 작성자마다 다른 스타일과 패턴이 섞여 있어 인수인계 비용이 크게 증가하던 상황이었습니다.
먼저 팀 전체가 참조할 수 있는 코드 컨벤션 및 개발 가이드 문서를 작성했습니다. 컴포넌트 구조, 네이밍, 상태 관리 패턴, API 호출 방식, 에러 처리 등 반복적으로 의사결정이 필요한 지점을 정리해 "이 프로젝트에서는 이렇게 한다"를 명시했습니다. 그러나 컨벤션 문서만으로 여전히 자잘한 오류들이 발생했고, QA 건이 지나치게 많다는 팀 내 의견을 수렴해 승인 1명을 필수로 하는 PR 코드 리뷰 프로세스 를 도입했습니다. 리뷰의 기준은 앞서 작성한 컨벤션 문서로 삼았고, 이로써 리뷰가 단순한 의견 교환이 아니라 공통 기준에 기반한 정렬이 되도록 설계했습니다.
컨벤션 문서화 이후 다른 사람 코드를 맡았을 때의 리팩토링 시간이 약 30% 절감됐습니다. 이어서 컨벤션 기반 코드 리뷰 프로세스를 도입한 뒤에는 릴리스 이후 버그가 약 50% 감소했습니다(QA 기간 중 발생한 이슈 카드 개수 기준). 팀 전체의 코드 이해도와 일관성이 향상되어 누구의 코드든 누구나 수정할 수 있는 구조가 만들어졌고, 신규 인원이 합류하더라도 컨벤션 문서를 기준으로 빠르게 온보딩할 수 있는 토대가 마련됐습니다.
처음에는 코드 리뷰 자체가 품질의 핵심이라고 생각했지만, 실제로는 "무엇을 기준으로 리뷰할 것인가"가 더 본질적인 문제였습니다. 컨벤션 문서 없이 리뷰를 시작했다면 리뷰어마다 다른 취향이 부딪치며 소모적인 논쟁으로 흘러갔을 것입니다. 팀 단위의 품질은 개인의 성실함이 아니라 공유된 기준에서 나오며, 기준을 먼저 만들고 그 위에 프로세스를 얹어야 지속 가능한 문화가 된다는 점을 배웠습니다.
CreditConnect는 대출 중개 금융 서비스여서 보안상 로그인 토큰(JWT)의 유효 시간을 짧게 설정해야 했습니다. 문제는 사용자가 작업 중 토큰이 만료되면, 화면에서 동시에 호출되고 있던 여러 API가 한꺼번에 "인증 실패(401)"를 받아 화면이 깨지거나 세션이 갑자기 끊기는 일이 자주 발생했다는 점입니다. 단순히 "401이 오면 토큰을 새로 받자"고 처리하면 실패한 요청들이 각자 따로 토큰 갱신을 요청해, 한 번이면 될 작업이 여러 번 중복되고 서로 순서가 꼬여 세션이 완전히 끊기는 문제로 이어졌습니다.
Axios 인터셉터에 Request Queue 패턴(요청 대기열 방식)을 도입했습니다. 실패한 요청들을 각자 처리하지 않고 한 줄로 줄 세워서 처리하는 방식으로, 401 응답이 오면 가장 먼저 들어온 요청 하나만 토큰 갱신을 시도하고 나머지는 대기열(큐)에 잠시 보관합니다. 갱신이 성공하면 대기 중이던 요청들에 새 토큰을 붙여 자동으로 다시 시도하고, 실패하면 한꺼번에 에러로 처리합니다. 이 모든 과정을 공통 레이어에서 처리해, 각 페이지는 토큰 갱신을 전혀 신경 쓰지 않고 평소처럼 API만 호출하면 되도록 만들었습니다.
사용자는 토큰이 만료됐다는 사실조차 인지하지 못한 채 작업을 이어갈 수 있게 됐고, 금융 데이터 접근이 중단 없이 처리됐습니다. 중복 갱신 요청이 한 번으로 줄어들어 서버 부하도 감소했고, 401로 인한 화면 깨짐 이슈가 완전히 사라졌습니다.
인증 플로우에서 가장 위험한 건 단순한 코드 실수가 아니라 "동시에 여러 요청이 일어날 때 서로 꼬이는 상황"이라는 것을 직접 체감했습니다. 특히 금융 서비스에서는 세션이 끊기면 사용자 신뢰를 크게 잃기 때문에, "사용자가 전혀 눈치채지 못하는 자연스러운 처리"가 결국 가장 좋은 UX라는 것을 배웠습니다.
CreditConnect는 화면 단위(접근 가능 페이지), 메뉴 단위(보이는 메뉴), 요소 단위(버튼·필드 표시 여부)로 세밀한 접근 제어가 필요했습니다. 역할이 5개 이상이고 각 역할마다 권한 조합이 달랐으며, 권한 정책이 자주 변경되는 상황에서 코드 곳곳에 권한 체크 if문이 흩어지면 유지보수가 불가능했습니다. 또한 권한이 잘못 노출되면 금융사고로 직결되는 위험이 있었습니다.
권한 관리를 라우트·메뉴·요소의 3계층으로 분리했습니다. 라우트 레벨은 AuthRouteOutlet(로그인 필수 영역), ProtectedRoute(역할별 접근 제어), withoutAuth(비로그인 전용 영역)를 조합한 React Router v6 Outlet 기반 구조로 처리했습니다. 메뉴 레벨은 역할별 접근 가능 화면 리스트를 기반으로 사이드바를 동적으로 생성했고, 요소 레벨은 권한 훅으로 버튼·필드·액션 단위 가시성을 제어했습니다. 모든 권한 정책은 설정 파일 한 곳에 모아 관리해, 신규 권한 추가는 코드 변경 없이 설정만으로 가능하게 했습니다.
역할별 접근 제어가 안정적으로 동작했고, 권한 정책이 바뀔 때도 설정 파일 한 곳만 수정하면 되는 구조 덕분에 유지보수 비용이 크게 줄었습니다. 신규 역할이 추가되어도 기존 코드를 거의 건드리지 않고 확장 가능했고, 권한 체크 if문을 코드 곳곳에 뿌리지 않아도 되어 전체 코드의 가독성도 함께 향상됐습니다. 특히 "어떤 역할이 어떤 화면·메뉴·요소에 접근할 수 있는지"를 한 곳에서 명확하게 확인할 수 있어, 기획·운영 팀과 권한 정책을 논의할 때도 소통이 훨씬 수월해졌습니다.
권한 시스템은 초기에 구조를 얼마나 잘 잡느냐에 따라 이후 유지보수 난이도가 완전히 달라지는 영역이라는 것을 직접 체감했습니다. 권한 체크를 그때그때 코드 여기저기에 추가하면 단기적으로는 빠르지만, 시간이 지나면 if문이 곳곳에 뿌려져 어떤 역할이 어디에 접근 가능한지 아무도 확신하지 못하는 상황이 옵니다. 반대로 권한 정책을 설정 파일 한 곳에 모아두는 구조로 만들면, 새로운 요구사항이 들어와도 설정만 수정하면 되기 때문에 훨씬 안전하고 예측 가능합니다. 특히 금융 서비스처럼 권한 실수가 곧바로 사고로 이어질 수 있는 환경에서는, 누가 무엇에 접근 가능한지 한눈에 볼 수 있게 투명하게 관리하는 것이 가장 중요한 원칙이라는 것을 배웠습니다.
CreditConnect는 셀러의 정산예정금을 담보로 대출을 중개하는 플랫폼으로, 대출 적격 심사를 위해서는 쿠팡·네이버스토어 등 마켓플레이스의 셀러 매출 데이터 를 자동으로 수집해야 했습니다. 그러나 판매몰 연동 과정은 로그인 단계부터 2차 인증, 보안 문자, 데이터 수집까지 다양한 실패 지점이 존재했고, 사용자는 "왜 연동이 실패했는지" 알 수 없어 연동 실패가 곧바로 CS 문의로 이어지는 경우가 많았습니다. 특히 판매몰 측 정책 변경이나 일시적 차단으로 인한 실패도 잦아 상태 추적이 매우 까다로웠습니다.
백엔드 스크래핑 API와 연동해 판매몰 연동 프로세스를 5단계 상태로 모델링했습니다(로그인 시도 → 2차 인증 → 데이터 수집 → 검증 → 완료). 각 단계별로 50여 개의 에러 코드를 정의하고, 에러 코드마다 사용자가 스스로 해결할 수 있는 안내 메시지를 매핑했습니다. 진행 상황은 실시간으로 UI에 표시하고, 실패 시 재시도 플로우와 문의 가이드를 단계별로 노출하도록 설계했습니다.
사용자가 어느 단계에서 실패했는지, 다음에 무엇을 해야 하는지 명확하게 파악할 수 있게 되어 CS 문의가 약 30% 감소했습니다. 운영팀은 에러 코드로 문제를 즉시 특정할 수 있게 되어 대응 시간이 단축됐고, 셀러 입장에서도 대출 프로세스가 "블랙박스"에서 "단계별 가시화"로 바뀌어 서비스 신뢰도가 향상됐습니다.
이 작업은 셀러의 의견이 운영팀을 통해 직접적으로 전달되는 업무여서, 내가 만든 기능의 문제가 곧바로 고객의 불만으로 이어진다는 부담감이 컸습니다. 동시에 개발팀이 아닌 외부 팀(운영·CS)과 처음으로 긴밀하게 소통해본 경험이었는데, 같은 이슈라도 기술 언어와 사용자 언어 사이를 번역해주는 일이 얼마나 중요한지 체감했습니다. 무엇보다 외부 시스템과 연동되는 기능에서는 "어떤 상황에서도 에러가 나지 않는 안정성"을 추구하거나, 에러가 발생하더라도 원인을 빠르게 파악하고 고칠 수 있는 구조를 갖추는 것이 가장 중요한 원칙이라는 것을 배웠습니다. 실패를 숨기는 것이 아니라 잘 드러나게 만드는 설계가 결국 서비스 신뢰를 지키는 길이라는 인식을 얻었습니다.