식자재 유통사를 위한 B2B SaaS 플랫폼 마켓봄(구 마켓봄 프로)의 프론트엔드를 담당했습니다.
네이티브 WebView 안에서 동작하는 인앱 하이브리드 앱을 개발하며 배포 버전 불일치 진단·금액 계산 정합성 등 운영 이슈를 해결했고, 웹 서비스·정산 관리자·디자인 시스템까지 전 레이어에 걸쳐 개발했습니다.
React Vue.js Nuxt.js TypeScript GraphQL Apollo Client Emotion Storybook Datadog ag-Grid Vite
개요
담당 범위
거래처 주문 앱 — 초기 설계·개발을 단독 수행
마켓봄 웹 — 거래전표 등록, 자료 일괄 등록, 세금계산서 대조 화면
유통사 전용 앱 — 수주 관리 화면과 네이티브 기능 연동
통합 운영 관리자 — 세 서비스 운영 화면을 신규 구축
사내 디자인 시스템 — MUI 의존 제거와 모바일 컴포넌트 개발
B2B 식자재 유통 플랫폼 — 유통사 관점
마켓봄 프로대기 중
📋
🚚
거래처 A
📋
🚚
거래처 B
📋
🚚
거래처 C
📋
🚚
거래처 D
발주 수신
발주 확인
출고 처리
발주 수신
새벽 6시, 수십 개 거래처의 발주가 한꺼번에 들어온다
전화·팩스 시대에는 직원이 한 통씩 받아 수기로 적었습니다. 마감 시간이 지나도 전화는 계속 왔고, 받아 적다 빠뜨린 품목이 배송 당일에야 발견되곤 했습니다. 마켓봄 프로는 이 모든 발주를 실시간 디지털 데이터로 전환합니다.
음식점 사장님이 앱에서 직접 발주
유통사는 실시간으로 발주 수신
전화·팩스 없이 자동 기록
발주 확인
한 화면에서 모든 거래처의 발주를 한눈에
유통사 담당자는 웹이나 모바일 앱 어느 환경에서도 발주를 확인하고 출고 수량을 수정할 수 있습니다. 거래처별·상품별로 정렬된 발주 목록은 창고 출고 준비의 첫 번째 신호입니다.
거래처별 발주 목록 실시간 조회
출고 수량·단가 인라인 수정
자동발주 처리로 반복 주문 부담 절감
출고 처리
출고 지시서 한 장으로 배송 시작
발주 확인 → 출고 지시 → 배송 → 세금계산서 발행. 전 과정이 하나의 플랫폼 안에서 이어집니다. 수기 장부와 전화 통화로 채워졌던 유통사의 하루가, 데이터로 관리되는 워크플로우로 전환됩니다.
출고 지시서 자동 생성·정렬
배송 상태 실시간 추적
정산·세금계산서 자동 연동
스크롤하여 탐색↓
고지: 전 직장의 영업비밀 보호를 위해 내부 식별자·파일 경로·연동 규격 등 일부 항목은 생략하거나 실제와 다르게 표기했습니다.
배경
마켓봄(구 마켓봄 프로)은 식자재 유통사(도매상)를 위한 B2B SaaS 플랫폼입니다. 유통사가 다수의 거래처(음식점)를 관리하고, 발주를 수신·처리하며, 정산까지 처리하는 올인원 플랫폼입니다.
단일 서비스가 아니라 여러 프로덕트가 연계된 에코시스템입니다. 웹 서비스, 두 개의 하이브리드 앱, 통합 운영 관리자, 디자인 시스템이 각각 독립 저장소로 운영되며, 프론트엔드 전 레이어에 걸쳐 개발과 코드리뷰를 담당했습니다.
마켓봄 에코시스템 ├── 마켓봄 웹 (Nuxt.js) — 유통사 업무 화면 ├── 거래처 주문 앱 (WebView) — 음식점 발주 ├── 유통사 전용 앱 (WebView) — 현장 수주 관리 ├── 통합 운영 관리자 (React) — 세 서비스 운영 도구 └── 디자인 시스템 — 위 전부의 공통 UI
이 구조 때문에 같은 도메인 규칙이 서로 다른 기술 스택 위에서 반복 구현되는 상황이 계속 생겼고, 금액 계산 규칙과 공통 UI를 어디에 두어야 하는지가 반복되는 판단 대상이었습니다.
해결한 문제
각 문제의 상황·제약·접근·결과·한계는 아래 탭의 문서에 케이스 단위로 정리했습니다.
문제
어디에
반올림하는 순서 때문에 금액이 틀어졌다 — 제가 작성한 로직에서 발생한 정산 사고. 핫픽스 후 Big.js로 라이브러리 레벨 차단까지
마켓봄 웹 탭
기능을 추가할수록 기능 추가가 어려워졌다 — 리팩터링 기간을 별도로 확보해 거래전표 검증 구조를 재설계
마켓봄 웹 탭
고친 이슈의 CS가 계속 들어왔다 — WebView 캐시로 구버전 번들이 남는 문제를 배포 버전 비교로 자동 판별
거래처 주문 앱 탭
네이티브가 넘겨준 인증 URL, 그리고 그 URL이 남긴 것 — 주어진 진입 규격에 딸려 온 히스토리 오염과, 덮지 못하고 남긴 판단
거래처 주문 앱 탭
네트워크가 불안정한 현장, 그리고 캐싱의 대가 — 자산 캐싱으로 얻은 것과 내준 것
거래처 주문 앱 탭
데스크탑에서는 재현되지 않는 이슈들 — 모바일 전용 모니터링 뷰 분리
유통사 전용 앱 탭
컴포넌트 하나 쓰려고 MUI 전체를 지고 갔다 — 소비측 코드를 깨지 않고 의존성 교체
디자인 시스템 탭
운영 화면이 서비스 수만큼 흩어져 있었다 — 세 서비스 운영 도구를 0→1로 통합
통합 운영 관리자 탭
이 프로젝트에서 반복적으로 다룬 축은 금액 정합성(유통사별 부가세·반올림 정책을 클라이언트가 계산하고, 표시 금액이 어긋나면 곧바로 거래 분쟁이 되는 영역 — Big.js로 부동소수점 오차 차단, 클라이언트 선검증 + 서버 재검증 이중 구조), 인앱 WebView 환경(배포 버전 불일치 자동 판별, 히스토리 조작, 자산 캐싱, JS Bridge 네이티브 연동), 레거시 의존성 제거(MUI 걷어내기), 그리고 운영 도구 통합(흩어진 세 서비스 관리 화면을 0→1로)이었습니다.
WebView 대응 경험을 확장해 앱 6종의 네이티브 Shell을 단일 코드베이스로 통합하는 설계를 진행했습니다 → 하이브리드 앱 통합 프레임워크
전체 기술 스택
레이어
기술
마켓봄 웹
Nuxt.js, Vue.js, TypeScript, GraphQL, Apollo Client, RealGrid
거래처 주문 앱 (웹뷰)
Nuxt.js, Vue.js, TypeScript, GraphQL, Apollo Client, Big.js, Workbox
유통사 전용 앱 (웹뷰)
React, TypeScript, Emotion, GraphQL, Apollo Client
유통사(도매상)가 거래처 관리, 발주 수신·처리, 세금계산서·정산 업무까지 처리하는 B2B 웹 서비스입니다. 마켓봄 에코시스템의 메인 프로덕트로, 이미 운영 중이던 서비스를 인수해 개발·운영했습니다.
담당 범위
거래전표 등록 화면 — 설계부터 개발, 이후 구조 리팩터링까지
자료 일괄 등록 — 엑셀 서식 버전 관리와 유효성 검증
세금계산서·거래금액 대조 화면
발주 수신·처리, 거래처별 단가 관리, 배송 일정
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
반올림하는 순서 때문에 금액이 틀어졌다
상황
거래전표는 유통사와 거래처 사이의 실제 거래 금액을 확정하는 문서입니다. 여기서 금액이 틀리면 화면 버그로 끝나지 않고 곧바로 정산 문제가 됩니다.
특정 조건에서 전표 금액이 어긋나는 문제가 발생했습니다. 원인은 반올림을 적용하는 순서였습니다.
원인
금액 계산에는 반올림을 적용할 수 있는 지점이 여러 곳입니다.
항목별 금액 → 반올림 → 합산합산 → 반올림
둘은 다른 값을 만듭니다. 항목마다 발생한 미세한 반올림 오차가 항목 수만큼 누적되기 때문입니다. 항목이 적으면 차이가 드러나지 않다가, 특정 조건에서만 눈에 보이는 금액 차이로 나타납니다.
이 로직은 제가 작성한 것이었습니다.
대응
즉시 핫픽스로 계산 순서를 바로잡았습니다. 이미 발생한 건은 기술로 되돌릴 수 없어, 유통사와 거래처 사이의 조율을 통해 정산으로 해결됐습니다.
이후 — 라이브러리 레벨 차단
이 문제만 있었던 것은 아닙니다. 금액 계산에서 어긋나는 문제가 반복적으로 발생했고, 개별 대응으로는 끝나지 않는다고 판단했습니다.
모든 금액 연산을 Big.js로 옮겼습니다. 단가 × 수량 × 부가세율이 중첩되면 Number 연산의 부동소수점 오차가 실제 원 단위 차이로 나타납니다. 개별 코드에서 조심하는 방식으로는 언젠가 다시 뚫린다고 보고, 라이브러리 레벨에서 차단하는 쪽을 택했습니다.
한계
당시에는 핫픽스로 끝냈습니다. 반올림 규칙을 공통 함수로 일원화하거나, 해당 케이스를 회귀 테스트로 고정하는 작업은 하지 않았습니다. Big.js 도입이 결과적으로 구조적 대응이 되었지만, 이 사건 직후에 재발 방지를 설계한 것은 아니었습니다.
지금이라면 최소한 그 조건을 재현하는 테스트 한 건은 남기겠습니다. 같은 곳이 다시 틀렸을 때 배포 전에 걸리도록 하는 것이, 사고를 겪은 사람이 남겨야 할 최소한이라고 생각합니다.
금액을 다루는 화면에서 배운 것은 “조심해서 짠다”가 대책이 될 수 없다는 점이었습니다. 이후 유통사별 부가세·반올림 정책 처리, 클라이언트 선검증과 서버 재검증의 역할 분리 같은 판단도 같은 기준에서 나왔습니다.
기능을 추가할수록 기능 추가가 어려워졌다
상황
거래전표 등록은 유통사가 매일 쓰는 화면입니다. 기획·운영팀의 요구로 검증 규칙과 예외 처리가 계속 덧붙었고, 그때마다 코드 복잡도가 올라갔습니다.
결과적으로 기능 하나를 추가하는 비용이 계속 커지는 상태가 됐습니다. 어디를 건드리면 무엇이 깨지는지 파악하는 데 드는 시간이 실제 구현 시간을 넘어서기 시작했습니다.
이건 버그가 아니라 구조의 문제라 기능 개발 틈틈이 고칠 수 있는 성질이 아니었습니다. 리팩터링 기간을 별도로 요구해 확보하고, 직접 수행했습니다.
제약
검증을 전부 백엔드로 넘길 수 없었습니다. 거래전표는 사용자가 입력하는 값이 많고, 그 값들이 연쇄적으로 계산됩니다.
수량 변경 → 합계금액 변경 → 부가세 포함 여부 → 합계금액 재계산
이 흐름 중간중간에 입력 즉시 확인해야 하는 검증이 있었습니다. 재고, 일일 최대 주문 수량·금액, 거래처별 거래 불가 상품, 미수금 한도 같은 항목들입니다. 등록 버튼을 누른 뒤에야 알려주면, 사용자는 한참 입력한 전표를 되돌려야 합니다.
문제는 이 검증들이 항목별로 따로 호출되고 있었다는 점입니다. 일부는 실제로 의존 관계가 있었고, 일부는 개발 편의로 그렇게 굳어진 것이었습니다.
선택지와 결정
백엔드에 케이스별 통합 검증 API를 요청하는 안을 회의에서 논의했습니다. 결론은 그렇게 가지 않는 것이었고, 근거는 이랬습니다.
입력값의 조합이 너무 많아 케이스마다 전용 API를 만드는 것은 현실적이지 않았습니다.
더 중요한 이유는 사용자 경험이었습니다. 입력할 때마다 로딩바를 보며 기다리게 할 수는 없다 — 이게 회의에서 합의된 핵심 근거였습니다. 전표 입력은 빠른 연속 작업이라, 왕복이 늘어나면 업무 자체가 느려집니다.
대신 최종 등록 시점에 전체 검증을 한 번 수행하는 안전망을 두어, 개별 검증이 놓친 상태를 서버가 잡도록 했습니다.
접근
필요한 검증만 묶어 병렬 호출로 전환 — 서로 의존하지 않는 check 계열 mutation을 Promise.all로 통합했습니다. 의존 관계가 있는 것들은 순서를 유지하되, 단위 순수 함수로 분리·추상화해 복잡도를 낮췄습니다. 병렬화가 목적이 아니라 읽을 수 있는 구조로 되돌리는 것이 목적이었습니다.
기획의 두 등급을 API 선택에 그대로 매핑 — 검증 실패의 의미가 두 가지였습니다.
기획 정의
처리
사용한 API
하나라도 걸리면 전표 발행 불가
즉시 중단
Promise.all
경고만 노출하고 진행 가능
전부 수집 후 안내
allSettled
allSettled를 쓴 이유는 경고 등급에서는 하나가 실패해도 나머지 결과가 필요하기 때문입니다. 경고가 여러 건이면 하나를 닫으면 다음이 뜨는 방식으로 순차 노출했습니다.
호출 시점 정리 — 검증은 포커스가 빠질 때 발동하도록 하고, 연쇄 계산 중 발생하는 중복 호출은 디바운스로 억제했습니다.
결과
측정 기록은 없습니다. 성능 지표를 남기지 않았습니다.
다만 리팩터링 이후 같은 화면을 다루는 동료 개발자들로부터 로직이 간결해졌다는 평가를 받았습니다. 주관적 평가이고 정량 근거는 아닙니다.
한계
애초에 목표가 복잡도 감소였음에도 개선 전후를 측정 가능한 형태로 정의하지 않았습니다. 지금이라면 최소한 변경 범위·순환 복잡도·기능 추가 소요 시간 중 하나는 기록해 두겠습니다. 리팩터링의 효과를 “느낌”으로만 남긴 것이 이 작업의 가장 큰 아쉬움입니다.
그 밖의 담당 기여
핵심 업무 기능
발주 수신 및 처리 — 거래처로부터 들어오는 발주를 실시간으로 수신하고, 확인·처리·취소 워크플로우 구현
거래처별 단가 관리 — 거래처마다 다른 상품 단가를 설정하고 일괄 적용하는 가격 관리 인터페이스
배송 일정 관리 — 날짜별·지역별 배송 일정 조회 및 도서산간 배송비 그리드 연동
출고 지시서 관리 — 출고 지시서 정렬·필터링 및 출고완료 상태 처리
동시 편집으로 사라진 상품 처리
사용자가 상품을 전표 화면에 올려둔 사이 다른 담당자가 그 상품을 삭제하면, 화면에는 남아 있지만 저장하는 순간 존재하지 않는 상품이 됩니다. 여러 담당자가 동시에 쓰는 B2B 도구에서 클라이언트가 미리 검증해도 원천적으로 막을 수 없는 종류의 상태입니다.
클라이언트에서 막으려 하지 않고 서버 저장 시점의 에러 코드를 기준으로 처리했습니다. 안내창을 띄우는 데서 끝내지 않고 문제가 된 행을 붉은색으로 표시해, 항목이 많은 전표에서 사용자가 어느 줄을 지워야 하는지 직접 찾지 않아도 되게 했습니다. 클라이언트 검증은 UX를 위한 것이고 신뢰의 근거는 서버에 둔다는 원칙을 여기서도 지켰습니다.
세금계산서 & 거래금액 대조 화면
유통사가 세금계산서와 실제 거래금액을 대조·검증하는 화면을 구현했습니다. 조회 기간 설정, 결제 수단별(가상계좌·신용카드) 금액 분류, 매입처 탭의 지급금액 컬럼 구성, 조회 기준 툴팁 등 세부 UI를 담당했습니다.
이 화면의 요구사항은 사내 유통 업무 전문가와 영업팀을 통해 기획으로 전달받은 것입니다. 현장에서 어떤 대조 업무가 있는지에 대한 정보는 그 경로로 확보했습니다.
자료 일괄 등록 — 엑셀 서식 버전 관리
유통사는 상품·단가·거래처 정보를 엑셀 파일로 일괄 등록하는데, 서식이 계속 변경됐습니다. 예전에 받아둔 서식으로 업로드하면 등록이 실패하지만 사용자는 이유를 알 수 없었습니다.
서식 버전을 서버에서 관리하고 구버전 업로드 시 업로드 시점에 안내하도록 해, 실패한 뒤 원인을 문의하는 흐름을 업로드 직후 안내로 앞당겼습니다. 버전 관리를 둔 이유는 변경된 서식을 반영하기 위해서, 그리고 에러가 났을 때 어떤 서식으로 올린 것인지 추적하기 위해서입니다.
업로드 불가 확장자, 단가 그룹 개수 제한 초과 등 엣지 케이스의 프론트엔드 예외 처리
파일 처리 결과에 업로드한 사용자 정보를 포함해 이력 추적 가능
Apollo Client + GraphQL 데이터 레이어
Apollo Client를 통해 GraphQL API와 통신합니다. 쿼리 캐싱 전략으로 페이지 전환 시 네트워크 요청을 줄이고, optimisticResponse로 UI 응답성을 개선했습니다.
RealGrid 기반 대용량 테이블
대용량 발주·단가·정산 데이터를 처리하는 화면에서 RealGrid를 활용합니다. 그리드 내 인라인 편집, 다중 선택, 컬럼 고정 등 스프레드시트 수준의 인터랙션을 제공합니다.
음식점 사장님(거래처)이 모바일·PC에서 식자재를 발주하는 전용 웹 앱입니다. 네이티브 WebView Shell 안에서 실행되는 웹 레이어로, 초기 설계와 개발을 혼자 진행한 뒤 인원이 합류했습니다.
담당 범위
초기 아키텍처와 Apollo Link 체인 설계
배포 버전 불일치 대응 — 원인 자동 판별 구조 구현
WebView 인증·히스토리 처리
유통사별 가격 표기와 주문 유효성 검증
이후 추가된 화면·기능 — 팀 공동 개발
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
고친 이슈의 CS가 계속 들어왔다
상황
이미 수정해서 배포한 이슈인데, 같은 증상의 CS가 계속 인입됐습니다. 코드에는 문제가 없었고 재현도 되지 않았습니다.
원인을 추적하다 확인한 것은, 일부 사용자가 캐시된 이전 버전의 번들로 앱에 진입하고 있다는 것이었습니다. 고친 코드가 그 사용자들에게는 아직 도달하지 않았던 겁니다.
WebView 앱이라는 점이 문제를 키웠습니다. 사용자는 자신이 옛 화면을 보고 있다는 사실을 알 방법이 없고, 브라우저처럼 강제 새로고침을 시도할 수도 없습니다. 개발자에게는 “고쳤는데 왜 안 고쳐졌냐”로, 사용자에게는 “여전히 안 된다”로 보이는 상태가 이어졌습니다.
제약
사용자에게 “새로고침 해보세요”를 요구할 수 없었습니다. WebView에는 그 조작이 없고, 요구 자체가 서비스의 실패입니다.
클라이언트가 자기 버전이 낡았는지 스스로 알 방법이 없었습니다. 비교 대상이 필요했습니다.
접근과 결정
서버 응답에 배포 버전을 싣고, 클라이언트가 매 응답마다 자기 버전과 비교하도록 했습니다. 백엔드와 협의해 응답 헤더에 버전과 빌드 번호를 담았고, Apollo Link 체인에 versionCheckLink를 추가해 이 비교를 요청 파이프라인에서 처리했습니다.
불일치가 감지됐을 때만 CDN을 한 번 더 조회했습니다. 서버 헤더와 클라이언트 버전만 비교해도 판별은 되지만, 오탐을 줄이기 위해 최신 배포본의 실제 버전을 CDN에서 한 번 더 확인하는 단계를 뒀습니다. 이 조회 자체가 캐시되면 낡은 값을 읽게 되므로 쿼리스트링으로 캐시를 우회했습니다.
클라이언트 vs 서버
CDN 조회 결과
판정
불일치
CDN이 서버와 동일
클라이언트가 옛 번들 사용 중 → 자동 갱신
불일치
CDN도 서버와 불일치
배포 상태 이상 → 개발팀 확인 대상
복구는 사용자에게 맡기지 않고 자동으로 처리했습니다. 옛 번들로 판정되면 쿼리스트링을 새로 붙여 재진입시켜 최신 번들을 받게 했습니다. 사용자 입장에서는 안내창도 조작도 없이 “화면이 조금 늦게 뜨네” 정도의 경험으로 끝납니다.
재진입해도 여전히 옛 버전이면 무한 리로드에 빠질 수 있습니다. 이 경우에는 자동 복구를 멈추고, 앱의 새 버전을 내려받아 달라는 안내를 노출했습니다. 웹 레이어에서 해결할 수 없는 상태를 네이티브 앱 업데이트로 넘긴 것입니다.
결과
도입 이후 버전 미반영으로 인한 문제는 발생하지 않았습니다.
고쳤는데도 반복되던 CS의 원인이 코드가 아니라 전달 경로에 있었다는 것을 확인했고, 같은 유형의 추적에 시간을 쓰지 않게 됐습니다.
한계
표의 두 번째 분기(배포 상태 이상)는 실제로 발동한 사례를 겪지 못했습니다. 오탐을 줄이려고 넣은 방어 장치이고, 실제로 해결한 문제는 첫 번째 분기입니다. 두 분기를 대등한 성과로 보기는 어렵습니다.
네이티브가 넘겨준 인증 URL, 그리고 그 URL이 남긴 것
상황
네이티브 WebView는 앱 진입 시 인증 파라미터가 포함된 URL을 엽니다. 웹 미들웨어가 이를 감지해 GraphQL로 로그인하고 토큰을 저장합니다. 웹 레이어가 정한 규격이 아니라, 네이티브 → 웹 진입 경로로 주어진 규격이었습니다.
이 규격에 두 가지가 딸려 왔습니다. 하나는 히스토리의 첫 엔트리가 인증 URL이 된다는 것, 다른 하나는 인증 값이 URL과 히스토리에 그대로 남는다는 것입니다.
앞의 것이 먼저 드러났습니다. 사용자가 뒤로가기를 누르면 인증 URL로 되돌아가 로그인 화면을 다시 보게 됐고, QA 과정에서 발견됐습니다.
접근과 결정
history.replaceState로 히스토리 첫 엔트리를 메인 URL로 교체한 뒤 이동하도록 처리했습니다. 인증 URL이 히스토리에 남지 않으므로 뒤로가기가 정상 동작합니다.
다만 이 처리만으로는 메인 화면에서 뒤로가기를 눌렀을 때의 동작이 남습니다. 웹 레이어 단독으로 정할 수 없는 영역이라 네이티브 쪽 back 버튼 처리와 협의해 함께 맞췄습니다. 진입 규격에서 비롯된 문제는 웹에서 끝나지 않는다는 걸 여기서 확인했습니다.
결과
뒤로가기로 인증 URL에 되돌아가는 경로가 사라졌고, 메인 화면의 back 동작은 네이티브와 웹이 같은 기준으로 처리하게 됐습니다.
남겨둔 판단
두 번째 문제는 그대로 남겼습니다. 인증 파라미터는 일회성 코드였고, URL에 실려 전달됐습니다. 값이 URL·히스토리에 남는다는 점은 인지하고 있었지만, 네이티브 → 웹 진입 경로의 제약상 그대로 진행했습니다.
일회성 코드라 재사용 위험은 제한적이지만, 더 나은 선택지가 있었느냐고 묻는다면 그렇다고 답하겠습니다. 히스토리 오염은 웹에서 덮을 수 있었고 실제로 덮었지만, 값이 URL로 오간다는 사실 자체는 진입 규격을 바꿔야 없어지는 것이었습니다.
네트워크가 불안정한 현장, 그리고 캐싱의 대가
상황
거래처는 주방·창고·이동 중에 앱을 씁니다. 연결이 끊기거나 느린 상태에서도 화면이 깨지지 않아야 했습니다.
접근과 결정
Workbox 서비스 워커로 정적 자산을 캐싱했습니다. 자산 성격에 따라 전략을 나눴습니다.
대상
전략
만료
근거
웹폰트 스타일시트
Stale-While-Revalidate
—
변경 시 빠르게 반영되되 대기시키지 않음
웹폰트 파일
Cache-First
1년
1년 내 폰트 교체 계획이 없었음
이미지
Cache-First
7일 / 최대 100개
한 번 업로드된 이미지는 거의 변경되지 않는 것을 확인
만료 기간은 관례값이 아니라 해당 자산이 실제로 얼마나 자주 바뀌는지를 확인하고 정했습니다.
대가
캐싱은 공짜가 아니었습니다. 도입 초기에 캐시가 과도하게 유지되어 생기는 오류가 있었습니다. 특히 나중에 합류한 개발자들이 이 캐싱의 존재를 모른 채 디버깅하다 며칠을 소모한 일이 있었습니다. 코드를 고쳐도 반영되지 않는 것처럼 보이기 때문입니다.
지금 다시 한다면 캐싱 정책과 로컬 개발 시 우회 방법을 문서화하고 팀에 공유하는 것을 도입과 동시에 하겠습니다. 기술 선택의 비용은 나 혼자가 아니라 팀이 함께 치른다는 걸 이때 배웠습니다.
그 밖의 담당 기여
Apollo Link 체인 — 공통 관심사의 파이프라인 분리
인증·버전 체크·에러 처리·로깅을 각각의 Link로 분리해 요청 파이프라인으로 구성했습니다. 페이지 컴포넌트는 데이터만 다루고, 횡단 관심사는 전부 이 체인에서 처리됩니다.
유통사 전용 앱을 위한 React 컴포넌트 라이브러리입니다. 사내 npm 패키지로 배포되며 ESM·CJS 듀얼 빌드를 지원합니다.
담당 범위
MUI 의존 제거와 Emotion 기반 자체 테마 시스템 전환
Bottom Navigation·DatePicker 등 모바일 핵심 컴포넌트 개발
Storybook 문서화와 Semantic Versioning 기반 패키지 배포
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
컴포넌트 하나 쓰려고 MUI 전체를 지고 갔다
상황
디자인 시스템은 초기에 MUI를 내부 의존성으로 사용했습니다. 빠르게 구색을 갖추는 데는 유리했지만, 문제는 소비하는 쪽에서 나타났습니다.
MUI 전체를 의존하면서 실제로 쓰는 컴포넌트는 일부뿐이라, 소비측 번들에 불필요한 코드가 딸려 들어갔습니다. 이 라이브러리를 쓰는 곳이 모바일 하이브리드 앱의 웹 레이어라는 점이 문제를 키웠습니다. 네트워크가 불안정한 현장에서 쓰는 앱인데, 쓰지도 않는 코드를 내려받게 하는 구조였습니다.
제약
이미 유통사 앱이 이 라이브러리를 쓰고 있었습니다. 즉 소비측 코드를 깨지 않으면서 내부 구현만 바꿔야 했습니다. 컴포넌트 API가 바뀌면 앱 쪽 화면을 전부 손봐야 합니다.
접근과 결정
MUI 의존을 걷어내고 Emotion 기반 자체 테마 시스템으로 전환했습니다. 필요한 컴포넌트를 직접 구현하되, 밖으로 드러나는 사용법은 그대로 유지하는 것이 조건이었습니다.
테마는 소비측 서비스가 Provider로 앱을 감싸기만 하면 전체 스타일이 자동 적용되는 구조로 설계했습니다. 버튼·DatePicker 등은 계속 선언적으로 쓰면 됩니다. 라이브러리를 갈아끼웠다는 사실이 소비측 코드에 드러나지 않아야 이런 교체가 실제로 가능해집니다.
Emotion을 고른 이유는 런타임 CSS-in-JS라 동적 테마 적용이 가능했기 때문입니다. 유통사마다 다른 브랜드 색을 적용해야 하는 요구가 있는 영역이었습니다.
결과
소비측 번들에서 실제 사용하지 않는 컴포넌트가 포함되던 문제가 해소됐습니다.
한계
번들 크기의 전후를 측정하지 않았습니다. MUI를 걷어냈으니 줄어든 것은 분명하지만, 얼마나 줄었는지, 그것이 실제 로딩 시간에 얼마나 반영됐는지는 확인하지 않았습니다. 원인을 찾아 고치는 데까지는 갔지만 개선의 크기를 숫자로 남기지 못한 작업입니다. 지금 다시 한다면 착수 전에 번들 크기를 먼저 기록해 두겠습니다.
소비측이 업데이트 시점을 스스로 정할 수 없다
상황
공유 라이브러리는 여러 저장소가 동시에 의존합니다. 라이브러리가 바뀔 때마다 소비측이 전부 따라 올려야 한다면, 라이브러리를 고치는 일 자체가 부담이 됩니다.
소비측 저장소들은 번들러 구성도 서로 달랐습니다.
접근과 결정
Semantic Versioning으로 패키지를 관리했습니다. 브레이킹 체인지 없는 업데이트는 패치·마이너를, API 변경이 있으면 메이저를 올려 소비측이 안전하게 업데이트 시점을 스스로 결정할 수 있게 했습니다.
번들러 차이는 Vite 라이브러리 모드로 ESM·CJS를 동시에 빌드해 해결했습니다. 소비측 번들러 종류에 관계없이 최적 모듈 형식이 자동 선택됩니다. TypeScript 타입 정의 파일도 함께 생성해 소비측 IDE 지원을 보장했습니다.
같은 문제를 다른 조건에서 정반대로 푼 사례가 인쇄 템플릿 라이브러리입니다. 거기서는 버전이 갈리는 것 자체가 사고(서비스마다 다른 라벨 출력)라서 단일 버전 강제를 택했습니다. 여기서는 UI 불일치가 사고로 이어지지 않으므로 소비측 선택권을 택했습니다.
결과
소비측이 번들러 종류와 무관하게 업데이트 시점을 스스로 결정할 수 있는 구조를 갖췄습니다.
그 밖의 담당 기여
핵심 컴포넌트 개발
유통사 앱의 모바일 UX에 필요한 컴포넌트들을 설계하고 구현했습니다.
Bottom Navigation — 모바일 앱의 하단 탭 내비게이션. 활성 탭 상태·아이콘·라벨 구성
DatePicker — 모바일 환경에 최적화된 날짜 선택. 터치 친화적 인터랙션 구현
Iconography / Logo — 서비스 전용 아이콘셋과 로고 컴포넌트 정의 및 추가
Dialog — 줄바꿈(linebreak) 처리 개선. 긴 메시지 표시 시 레이아웃 깨짐 수정
Storybook 문서화
모든 컴포넌트를 Storybook 8로 문서화했습니다. Props 명세, variant별 렌더링 예시, 인터랙션 테스트를 Storybook에서 확인할 수 있어, 디자이너와 개발자가 같은 컴포넌트 명칭과 명세를 기준으로 소통하게 됐습니다. 원래 협업에서 컴포넌트 명칭과 명세가 불일치하던 문제를 겨냥한 것입니다.