전화·팩스로 이뤄지던 식자재 발주를 온라인으로 옮긴 B2B 커머스 식봄의 프론트엔드를 담당했습니다.
레거시 PHP 웹과 Next.js 신규 웹이 같은 도메인에서 병행 운영되는 구조라, 양쪽을 함께 다루며 검색·상세·주문·쿠폰 등 핵심 사용자 흐름을 개발하고 대형 장바구니 성능을 측정 기반으로 최적화했습니다. 관리자 웹과 인쇄 템플릿 라이브러리까지 여러 레이어를 함께 맡았습니다.
음식점 사장님이 식자재를 검색·비교하고 발주하는 메인 소비자 채널입니다. 레거시 PHP 웹을 기능 단위로 Next.js로 현대화하며 팀으로 공동 개발했고, 이 저장소의 주요 기여자로 참여했습니다.
담당 범위
장바구니 성능 개선 — 측정·원인 분석·개선·재발 방지 전 과정
로깅·모니터링 고도화 — 로그 레벨 정의, 공통 로거, 알림 조건과 대시보드 재설계
장바구니 쿠폰 적용 — 기존 쿠폰 기능 중 장바구니 영역을 설계부터 구현까지
팀 개발·테스트 기준 정리 — 커스텀 ESLint, 테스트 계층 분리
상품 상세·추가주문·업종 정보 개발
그 외 커머스 플로우 — 팀 공동 개발
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
재현되지 않는 “장바구니가 느리다”
상황
“장바구니가 너무 느리다”는 피드백이 들어왔습니다. 그런데 직접 사용해 보니 재현되지 않았습니다. 개발 환경에서도, 일반적인 장바구니 상태에서도 문제가 없었습니다.
체감으로는 진행할 수 없다고 판단해, Datadog RUM에 특정 조건의 로그를 심어 실사용자 데이터를 관찰했습니다. 처음에는 전체를 대상으로 보고, 원인을 좁혀 가며 타깃 범위를 점차 줄이는 방식이었습니다.
여기서 두 종류의 사용자군이 드러났습니다.
장바구니에 상품을 300개 이상 담아 둔 사용자가 소수 존재
API 통신 자체가 느린 환경의 사용자
식봄은 판매자별로 장바구니가 나뉘는 구조입니다. 음식점 사장님이 여러 유통사에서 동시에 발주하기 때문에, 판매자 수와 상품 수가 함께 늘어납니다. 문제는 이 조건에서만 드러났습니다.
재현되지 않는 문제를 “느낌”이 아니라 계측 대상으로 바꾼 것이 이 작업의 출발점이었습니다.
재현 조건 고정
관찰로 얻은 조건을 개발 환경에서 재현 가능한 형태로 고정했습니다.
케이스
구성
A
판매자 10 × 상품 10
B
판매자 1 × 상품 100
개발 PC 기준 상품 100개 조건에서 장바구니 진입에 약 7~8초가 걸렸습니다.
실사용자의 최악 케이스는 이보다 더 무거웠지만, 위 조건에서 이미 문제가 충분히 드러나 이 조건으로 진행했습니다.
측정 방법
단일 수치로 판단하지 않기 위해, 액션을 나누고 반복 측정했습니다.
액션 4종 — 페이지 로드, 장바구니 해제, 체크, 수량 변경
각 액션당 10회 측정 후 평균
지표는 LCP와 상품 목록이 화면에 모두 그려지는 시점(직접 계측) 을 함께 확인 — 두 값은 비슷한 수준이었습니다
측정 도구로는 React DevTools Profiler(리렌더 범위), Chrome DevTools Performance(메인 스레드 점유·Long Task)를 함께 사용했습니다.
원인
① 초기 렌더의 크리티컬 패스에 필수적이지 않은 요청이 함께 있었습니다.
대체 상품 영역, 쿠폰 영역, 배너 정보처럼 장바구니 목록을 보는 데 필요하지 않은 데이터가 첫 로드에 함께 묶여 있었습니다.
② 파생 상태가 매 렌더마다 재계산됐습니다.
판매자별 소계와 쿠폰 적용가가 대표적입니다. 개별 연산은 짧지만 호출 횟수가 판매자·상품 수에 비례해 늘어나면서 합계가 커졌습니다.
③ 가장 큰 Long Task는 상품 조회였고, 이건 제거할 수 없었습니다.
Performance에서 확인한 최대 Long Task는 상품 목록을 불러오는 구간이었습니다. 장바구니 화면의 존재 이유 자체이므로 제거 대상이 될 수 없었고, 그래서 이걸 건드리는 대신 주변 비용을 줄이는 방향으로 계획을 세웠습니다.
접근과 결정
필수적이지 않은 요청을 크리티컬 패스에서 분리 — 대체 상품·쿠폰 영역을 별도 컴포넌트로 추출하고 비동기로 조회해, 첫 화면 렌더 이후에 그려지도록 했습니다.
검증 로직은 “미룬” 것이 아니라 시점을 바로잡은 것 — 실시간 재고처럼 값이 계속 변하는 항목은 초기 로드 시점에 확인해도 의미가 없습니다. 사용자가 장바구니 화면을 오래 띄워 두는 경우가 많기 때문입니다. 각 액션 시점에 검증하고 최종 확인 시점에 한 번 더 확인하는 구조로 두었습니다. 성능을 위해 정확성을 양보한 것이 아니라, 값의 수명에 맞는 시점에 검증을 배치한 것입니다.
파생 상태 메모이제이션 — 판매자별 소계·쿠폰 적용가 등 파생 값을 useMemo·useCallback으로 감싸, 의존하는 값이 바뀐 판매자만 재계산되도록 범위를 좁혔습니다.
결과
LCP 기준 페이지 로드 시간이 단축되었습니다. 앞의 재현 조건을 고정한 채, 액션 4종을 각각 10회 측정한 평균을 변경 전후로 비교해 확인했습니다.
개선율 수치는 전 직장의 내부 성과 지표에 해당해 공개 문서에는 적지 않았습니다. 측정 조건과 방법은 위에 기재한 그대로입니다.
한계와 남은 과제
개선 후에도 절대적인 로드 시간은 여전히 길었습니다. 줄인 것은 제거 가능한 비용이었고, 남은 시간의 바닥값은 상품 조회 API였습니다.
이 구간은 프론트엔드 단독으로 해결할 수 없었습니다. 조회 자체를 가볍게 하거나 페이지네이션·부분 로딩으로 바꾸려면 API 설계 변경이 필요했고, 백엔드와 함께 진행해야 하는 과제로 남겨 두었습니다.
정직하게 남기면 이렇습니다 — 프론트 영역에서 줄일 수 있는 것은 줄였고, 근본 개선은 하지 못했습니다.
재발 방지
일회성 개선으로 끝나면 다음 기능 추가에서 같은 문제가 돌아옵니다.
금액 계산 로직 일부를 컴포넌트 외부 순수 함수로 분리 — 메모이제이션 의존성이 명시적으로 드러나, 이후 기능 추가 시 의존성 누락을 코드에서 확인 가능
분리한 계산 로직 일부에 단위 테스트 작성 — 리팩터링 이후에도 계산 결과가 동일한지 자동 검증
Datadog RUM 상시 관찰 — 애초에 이 문제를 발견한 채널이므로, 이후에도 로컬에서 재현되지 않는 성능 저하를 지표로 감지
알림이 너무 많아서, 정작 장애를 놓쳤다
상황
식봄 웹은 초기부터 속도를 우선해 기능을 붙여 온 서비스였고, 로깅과 알림은 그 속도를 따라가지 못한 채 남아 있었습니다. Datadog은 이미 쓰고 있었지만 로그에 레벨 구분이 없었고, 에러가 발생하면 그 사실이 Slack으로 전부 흘러들어왔습니다.
대부분은 사람이 대처할 필요가 없는 것들이었습니다. 그래서 채널을 습관적으로 넘기게 되었고, 실제로 대응이 필요한 에러를 제때 감지하지 못해 대처가 늦어진 케이스가 발생했습니다.
알림이 많아서 불편한 것이 문제가 아니라, 알림이 많아서 알림이 작동하지 않게 된 것이 문제였습니다.
원인 — 알림이 아니라 로그에 등급이 없었다
알림 조건만 조이는 것으로는 해결되지 않는 상태였습니다. 알림을 걸 기준 자체가 로그에 없었기 때문입니다.
로그에 레벨이 없어 “이건 사람이 봐야 하는가”를 로그만 보고 판단할 수 없었습니다
에러가 나면 종류와 메시지는 알 수 있었지만, 누가 / 어느 세션에서 / 코드의 어느 지점에서 겪은 것인지는 알 수 없었습니다
그래서 알림을 받아도 재현부터 시작해야 했고, 이 비용이 다시 “일단 넘기기”를 부추겼습니다
알림 조건을 손보기 전에 로그를 먼저 신뢰할 수 있게 만들어야 했습니다.
접근과 결정
① 레벨을 심각도가 아니라 “필요한 행동”으로 정의했습니다.
“이게 얼마나 심각한가”는 사람마다 다르게 판단합니다. 그래서 기준을 “이 로그를 보면 무엇을 해야 하는가” 로 바꿨습니다.
레벨
정의
도착지
error
즉각적인 대처가 필요한 상황
Slack 알림
warn
수정이 필요하지만 지금 당장은 아닌 상황
대시보드
info
상황 공유만 필요한 상황
대시보드
이 정의가 그대로 알림 조건이 됩니다. 알림을 보낼지 말지는 모니터 쪽 조건식이 아니라 로그를 남기는 시점에 결정되고, 그 판단 근거가 코드에 남습니다. 사용자 플로우에 영향을 주지 않고 클라이언트 콘솔에만 뜨는 에러는 알림 대상에서 제외했습니다.
② 공통 로거로 필수 필드를 타입으로 강제했습니다.
레벨을 정의만 해두면 지켜지지 않습니다. 로깅을 공통 로거 함수로 단일화하고, 인자 타입으로 필수 필드를 강제해 필드를 빠뜨린 로그는 애초에 컴파일되지 않도록 했습니다.
사용자 식별값·sessionId — 누가, 어느 세션에서 겪은 일인지 특정하기 위한 값
실행 위치 — 호출 시점에 직접 기재하도록 했습니다. 스택 트레이스 자동 수집이 아니라 수기 기재라 정확도가 작성자에게 달려 있지만, 대신 Next.js에서 같은 코드가 서버와 클라이언트 중 어느 쪽에서 돈 것인지처럼 자동 수집으로는 모호해지는 맥락을 로그에 그대로 남길 수 있었습니다
③ 대시보드는 “찾아가서 보는 곳”이 아니라 “매일 아침 도착하는 것”으로 두었습니다.
알림에서 걷어낸 것을 대시보드로 옮기기만 하면, 아무도 보지 않는 곳으로 옮긴 것과 같습니다. 그래서 매일 오전 9시에 프론트엔드 개발자들에게 자동 공유되도록 파이프라인을 구성했습니다.
사용자 접속 현황
error / warn 발생 건수
API 응답 속도
API 호출 순위
출근해서 Slack을 열면 어제의 상태가 이미 도착해 있는 형태입니다.
결과
알림 건수가 50% 이상 줄었습니다. 그리고 중요한 건 줄어든 알림이 아니라, 남은 알림이 실제로 작동했다는 쪽입니다.
로그인 관련 이슈를 운영팀 문의보다 먼저 발견해 핫픽스를 배포했습니다. 이전 구조였다면 알림에 묻혀 CS로 올라온 뒤에야 인지했을 사례입니다
아침 대시보드가 생기면서 “어제와 다른가”를 매일 확인하는 루틴이 프론트엔드 팀에 자리 잡았습니다
한계와 남은 과제
대응 시간은 측정하지 않았습니다. 알림 건수 감소는 확인했지만, 감지에서 대응까지 걸린 시간이 실제로 줄었는지는 수치로 남기지 않았습니다. 위의 로그인 이슈는 사례이지 지표가 아닙니다.
“놓친 알림”을 확인할 수단이 없었습니다. 조건을 좁힌 뒤 실제로 놓친 사례는 겪지 않았지만, 이건 놓치지 않았다는 증거라기보다 놓친 것을 알 방법이 없었다는 쪽에 가깝습니다. warn으로 내려간 항목이 사실 error였어야 했는지 주기적으로 되돌아보는 절차까지는 만들지 못했습니다.
요청 단위 추적(traceId)은 도입하지 못했습니다. 사용자와 세션까지는 특정할 수 있지만, 하나의 요청이 프론트엔드를 떠난 뒤의 흐름은 이어지지 않습니다. 이 작업은 프론트엔드 범위에서 혼자 진행했고 — 로그 규격 정의, 공통 로거 구현, Datadog 대시보드·모니터 설정, 일일 공유 파이프라인까지 — 백엔드에는 필요성을 공유하고 동일한 관리를 요청드리는 선까지였습니다. 경계를 넘는 추적은 양쪽이 같은 규격을 쓸 때만 완성되므로, 아직 절반입니다.
그 밖의 담당 기여
장바구니 쿠폰 적용
장바구니에 적용된 쿠폰은 사용자가 화면을 보고 있는 동안에도 수량 소진·유효기간 만료·최소 주문 금액 미달·쿠폰 우선순위 규칙으로 언제든 무효가 됩니다. 그때마다 화면에 표시된 금액과 실제 결제 금액이 어긋납니다.
할인 계산 정책의 주인은 서버였기 때문에, 클라이언트가 같은 계산을 다시 구현하지 않고 서버가 내려준 쿠폰 적용가와 할인 배분값을 그대로 표시했습니다. 필요한 필드는 GraphQL 스키마를 추가·수정해 확보했습니다. 프론트엔드가 책임진 것은 계산이 아니라 “언제 다시 물어볼 것인가” — 다운로드 직후 잔여 수량 refetch, 최소 주문 금액 미달 시 자동 해제, 결제 시점 최종 검증 등 무효화 시점의 관리였습니다.
쿠폰 적용 상태를 제거하면 금액이 자동으로 재계산되는 파생 구조로 두어, 어떤 경로로 무효화되든 별도의 금액 보정 로직이 필요 없게 했습니다. 무효화 경로마다 금액을 직접 되돌리는 코드를 두었다면 하나만 빠뜨려도 금액이 틀어졌을 겁니다.
앱 전용 쿠폰 배지(웹에서 구분 노출 후 앱 설치 유도), 추가주문 플로우에서의 쿠폰 기준 금액 대응도 함께 처리했습니다. 쿠폰 관련 CS 추이나 사용률 변화는 측정하지 않았습니다.
아래 항목들은 아직 위와 같은 수준으로 사실 검증을 마치지 않아, 기존 서술을 유지합니다. 순차적으로 케이스 형태로 정리할 예정입니다.
상품 상세 — 가격 정보 모바일 레이아웃 재설계
시세·가격 비교·대량 판매가 한 화면에 겹치면서 모바일에서 가격 판단이 어려웠던 문제를 해결했습니다. 가격 레코드 섹션을 전면 재설계하여 해상도별로 어떤 가격 정보를 우선 노출할지 재정의했습니다. 시세 차트의 컬러 시스템도 이때 함께 정리했습니다.
추가주문 2차 기능
기존 주문 컨텍스트를 유지한 채 새 상품을 선택하고 결제까지 이어지는 UI 플로우를 구현했습니다. 기존 주문의 배송지·결제 조건을 승계하면서 신규 상품만 추가되는 구조라, 상태 소유권을 분명히 하는 것이 관건이었습니다.
업종 정보 수집
신규 가입 사용자의 업종 정보 수집·저장 기능과, 기존 사용자 업종 조회 최적화를 구현했습니다.
팀 개발·테스트 기준 정리
코드 리뷰에서 반복적으로 지적되던 규칙을 사람이 아니라 도구가 잡도록 옮겼습니다.
커스텀 ESLint제작 — Relay fragment 작성 규칙 강제, ts-pattern 엄격 사용 강제. 리뷰어가 매번 같은 코멘트를 남기지 않아도 되고, 규칙이 코드로 문서화됩니다
테스트 계층 분리 — 금액 계산 등 복잡한 로직은 단위 테스트로 검증하고, 핵심 플로우는 E2E 계층으로 분리하는 기준을 정리
Bun 기반 자동화 스크립트 — 미사용 모듈 탐지, GraphQL 스키마 변경 영향도 분석, 환경 변수 포맷 검증, 파일명 규칙 감지
···
참고 — 아키텍처와 기술 선택
컴포넌트 구조
Atomic Design 계층으로 컴포넌트를 관리합니다.
components/├── atom/ 원자 컴포넌트 (버튼, 인풋, 배지)├── ds/ 디자인 시스템 토큰 기반 컴포넌트├── molecules/ 조합 컴포넌트 (검색바, 상품 카드)├── organisms/ 페이지 단위 섹션 (헤더, 장바구니 시트)└── pages/ 레이아웃 컴포넌트
페이지 영역은 탐색·주문·마이페이지 등 커머스의 일반적인 도메인 단위로 나뉩니다.
기술 선택 이유
Relay — GraphQL 데이터 의존성을 컴포넌트 레벨 fragment로 선언합니다. Relay Compiler가 빌드 타임에 스키마와 쿼리를 검증하므로 런타임 타입 불일치가 배포 전에 걸립니다. 오버페칭 없이 컴포넌트가 필요한 필드만 요청합니다.
Jotai — 장바구니·쿠폰처럼 여러 컴포넌트가 공유하는 상태를 원자 단위로 관리합니다. Context API 대비 리렌더링 범위가 좁아, 항목이 많은 장바구니에서 유리합니다.
GrowthBook — 세그먼트별 기능 플래그와 A/B 테스트를 코드 재배포 없이 제어합니다. 결제·쿠폰처럼 되돌리기 비용이 큰 영역을 점진 롤아웃하고, 문제 발생 시 배포 없이 즉시 끌 수 있습니다.
Datadog RUM — 실사용자 환경의 Core Web Vitals와 에러를 상시 수집합니다. 로컬에서 재현되지 않는 이슈의 근거를 확보하기 위한 채널이며, 실제로 위 장바구니 문제를 발견한 경로입니다. 로그 레벨 정의와 알림 조건, 일일 공유 대시보드도 이 위에 올렸습니다.
식봄 소비자 채널의 레거시 구현체입니다. CodeIgniter 3 기반 PHP 애플리케이션으로, 프론트엔드 팀에서 아무도 맡으려 하지 않던 화면들을 퍼블리싱부터 PHP 컨트롤러까지 직접 열어 개선한 저장소입니다.
담당 범위
검색·검색 필터 — 리디자인 이전 레거시 화면을 주도적으로 개발
방치되던 퍼블리싱 — 담당자가 정해지지 않던 작업을 자원해 처리
알림센터 — 프론트엔드부터 PHP 컨트롤러까지 직접 신규 구현
최근 본 상품 — 로컬 스토리지 기반 재구매 동선 구현
기획전 노출 기간 — 코드 하드코딩을 DB 기반 운영으로 이관
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
아무도 맡으려 하지 않는 화면이 남아 있었다
상황
식봄은 레거시 PHP 웹과 Next.js 신규 웹이 같은 도메인에서 동시에 운영되는 서비스입니다. 전환이 끝난 기능은 신규 웹으로, 아직 아닌 기능은 레거시 웹으로 흘러갑니다. 사용자는 그 경계를 인지하지 못합니다.
경계를 인지하는 것은 개발자 쪽이었습니다. 프론트엔드 팀의 주 저장소는 Next.js 웹이었고, PHP 저장소는 사실상 담당자가 정해지지 않은 영역이었습니다. CodeIgniter 템플릿과 PHP 컨트롤러는 “프론트엔드가 할 일”로 잘 분류되지 않았고, 그중에서도 제가 맡은 검색은 리디자인 이전 버전 — 곧 대체될 예정인 화면이었습니다.
그래서 이런 상태가 만들어져 있었습니다.
곧 사라질 화면이라 개선 우선순위에서 계속 밀림
PHP라서 자원해서 잡는 사람이 없음
그 결과, 담당자가 애매한 퍼블리싱 건들이 처리되지 않은 채로 남음
문제는 이 화면이 여전히 사용자에게 노출되고 있었다는 것입니다. 사라질 예정이라는 사실은 개발 로드맵의 사정이지, 지금 그 화면에서 상품을 찾고 있는 사장님과는 아무 관계가 없습니다.
검색은 특히 그랬습니다. 커머스에서 이탈이 가장 잘 일어나는 구간이라, 작은 입력 UX 결함도 그대로 “못 찾겠다”로 이어집니다.
제약
새로 짤 수 없었습니다. 곧 대체될 화면에 큰 투자를 정당화할 수 없었고, 리디자인 이전 버전이라 신규 웹의 디자인 시스템·컴포넌트를 가져다 쓸 수도 없었습니다. 레거시 템플릿의 규칙 안에서, 운영을 멈추지 않고 고쳐야 했습니다.
프론트엔드 경계에서 끝나지 않았습니다. 마크업만 만져서 되는 일이 아니라, 무엇을 언제 그릴지가 PHP 컨트롤러와 조회 로직에 들어 있는 경우가 많았습니다.
접근과 결정
① 도구가 아니라 문제를 기준으로 맡았습니다.
판단 기준을 “내가 PHP를 좋아하는가”가 아니라 “지금 이 화면이 사용자에게 어떤가” 로 잡았습니다. 언어와 프레임워크는 문제를 푸는 도구지, 일의 경계를 정하는 기준이 아니라고 봤습니다. React를 쓰는 화면이든 CodeIgniter 템플릿이든, 사용자가 겪는 결함의 무게는 같습니다.
② 아무도 안 하던 퍼블리싱부터 자원했습니다.
담당이 애매한 채로 남아 있던 퍼블리싱 건들을 먼저 가져왔습니다. 난이도가 높은 일이라서가 아니라, 아무도 가져가지 않으면 영영 남는 종류의 일이었기 때문입니다. 이 과정에서 레거시 템플릿 구조와 컨트롤러의 렌더링 흐름을 파악한 것이, 이후 검색 작업의 준비가 됐습니다.
③ 검색과 검색 필터를 주도적으로 개발했습니다.
레거시 검색의 결과 페이지와 입력 UX 전반을 맡아, 사용자가 실제로 걸려 넘어지는 지점들을 케이스 단위로 처리했습니다.
작업
다룬 문제
PC/모바일 상품 리스트 차별화
화면 크기에 따라 노출 상품 구성을 분기
검색어 볼드 강조
결과 목록에서 “왜 이 상품이 나왔는지”가 보이지 않던 문제
입력창 커서 위치 이슈
검색어를 수정하려 할 때 커서가 의도한 곳에 놓이지 않던 문제
뒤로가기 시 검색어 초기화
상품을 보고 돌아오면 검색어가 사라져 처음부터 다시 입력해야 했던 문제
이벤트성 Placeholder
검색창 포커스 시점을 이벤트 노출 지점으로 활용
뒤로가기 케이스가 이 작업의 성격을 가장 잘 보여줍니다. 커머스에서 “검색 → 상품 확인 → 돌아와서 다음 상품”은 가장 흔한 동선인데, 돌아올 때마다 검색어가 비어 있으면 사용자는 매번 처음부터 시작해야 합니다. 기능이 없는 것이 아니라 동선이 끊기는 종류의 결함이고, 곧 없어질 화면이라는 이유로 계속 미뤄져 있던 것이었습니다.
④ 필요하면 PHP 컨트롤러를 직접 열었습니다.
프론트엔드 쪽에서 끝나지 않는 건은 백엔드에 넘기고 기다리는 대신 컨트롤러와 조회 로직을 직접 확인했습니다.
결과
전환이 끝날 때까지, 레거시 검색은 방치된 화면이 아니라 계속 고쳐지는 화면이었습니다. 담당자가 애매하던 영역에 담당자가 생겼고, 밀려 있던 퍼블리싱 건과 검색 UX 결함이 순차적으로 처리됐습니다.
개인적으로 남은 것은 이쪽이 더 큽니다 — 일의 경계를 저장소나 언어가 아니라 문제로 잡는 방식이 이후 작업의 기준이 됐습니다. 신규 웹에서 성능 문제를 잡을 때 프론트 코드 바깥까지 확인한 것도, 관리자 웹에서 레거시와의 병행 운영을 전제로 전환에 참여한 것도 같은 기준에서 나왔습니다.
한계
지표로 남기지 않았습니다. 검색 이탈률이나 결과 클릭률의 전후 비교를 하지 않았습니다. 개별 결함이 사라진 것은 확인할 수 있지만, 그것이 지표를 움직였는지는 말할 수 없습니다. “이탈이 잦은 구간”이라는 것은 검색이라는 화면의 일반적 성격이지, 제 작업의 효과를 측정한 결과가 아닙니다.
검색 품질 자체는 다루지 않았습니다. 제가 손댄 것은 결과 페이지와 입력 UX였고, 무엇이 검색되고 어떤 순서로 나오는지 — 색인과 랭킹 — 는 Elasticsearch 쪽 영역으로 백엔드가 소유했습니다. “찾는 경험”은 고쳤지만 “찾아지는 정확도”는 건드리지 않았습니다.
구조를 남기지는 못했습니다. 이 화면들은 이후 Next.js로 대체됐고, 제가 고친 코드는 그대로 사라지는 것이 예정된 결과였습니다. 그보다 아쉬운 것은 다음 사람이 다시 PHP 저장소를 기피하지 않도록 만든 것이 없다는 점입니다. 제가 파악한 레거시 템플릿 구조와 컨트롤러 흐름을 문서로 남기거나 팀에 옮기는 데까지는 가지 못했고, 결국 한 사람이 자원해서 메운 자리로 끝났습니다.
그 밖의 담당 기여
알림센터
알림센터 메인 페이지를 프론트엔드부터 PHP 컨트롤러까지 직접 신규 구현했습니다. 미로그인 접근 시 로그인 리다이렉트 처리, 알림 타입별 아이콘 애니메이션, 알림 JS 모듈 분리 및 구조화를 함께 진행했습니다.
최근 본 상품
모바일·PC 공통으로 최근 본 상품을 노출하는 기능을 구현했습니다. 로컬 스토리지 기반으로 세션 간 히스토리를 유지합니다. “자주 구매하는 상품”과 함께 재구매 동선을 만드는 기능으로, 장바구니 담기 전환 개선에 기여했습니다. 지표로 확인한 것이고, 이 기능만 분리해 측정한 통제된 실험은 아닙니다.
기획전 노출 기간 관리 — 운영 이관
기획전 노출 조건이 코드에 하드코딩되어 있어, 노출 기간을 바꾸려면 매번 배포가 필요했습니다. 이를 DB 기반 동적 관리로 전환해 운영자가 관리자 화면에서 시작·종료일을 직접 설정하도록 했습니다. 개발자를 거치지 않고 기획전을 운영할 수 있게 되었습니다.
···
참고 — 저장소 성격과 기술 스택
이 저장소에서 얻은 것
레거시 코드베이스에서 운영 중인 서비스를 중단 없이 개선하는 방법을 실전으로 익혔습니다. 새 코드를 짜는 것보다, 이미 돌아가는 코드의 영향 범위를 파악하고 안전하게 바꾸는 일이 대부분이었습니다. 프론트엔드 경계를 넘어 서버 로직까지 직접 확인하는 습관도 여기서 생겼습니다.
서비스 규모
웹 컨트롤러 다수 — 상품 탐색, 장바구니, 주문, 결제, 마이페이지 등 소비자 플로우 전반
식봄 서비스 운영팀이 사용하는 내부 관리 도구입니다. 레거시 관리 도구를 React + TanStack Router 기반 SPA로 현대화한 프로젝트입니다.
담당 범위
쿠폰 관리 — 목록 조회와 쿠폰 템플릿 관리 구현
레거시 관리 도구의 기능 단위 React 전환
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
운영을 멈추지 않고 관리 도구를 옮기기
상황
레거시 관리 도구를 React로 현대화해야 했지만, 관리자 웹은 운영팀이 매일 쓰는 업무 도구입니다. 한 번에 교체하는 방식은 선택지가 아니었습니다. 전환 중 어느 시점에도 운영 업무가 멈추면 안 됐습니다.
제약
전환은 한 번에 끝나지 않습니다. 즉 상당 기간 동안 신규 React 화면과 레거시 화면이 공존해야 하고, 그 사이에도 새 기능 요구는 계속 들어옵니다.
접근과 결정
기능 단위로 독립 React 모듈로 분리하는 방식으로 참여했습니다. 각 기능이 자체 라우트와 데이터 레이어를 가지므로 독립적으로 개발·배포되고, 전환이 끝나지 않은 기능은 레거시에 그대로 남습니다.
전환 순서를 정하는 기준은 “코드가 낡은 순서”가 아니라 “바뀔 일이 많은 순서” 였습니다. 어차피 손대야 하는 화면부터 옮기면 전환 비용과 기능 개발 비용이 겹쳐 한 번에 회수됩니다.
TanStack Router를 고른 것도 이 맥락입니다. URL 파라미터·쿼리 스트링이 TypeScript 타입으로 강제되므로, 화면이 하나씩 늘어나는 동안 라우트 파라미터 누락이 컴파일 단계에서 걸립니다. 화면이 점진적으로 붙는 구조에서는 이 검출 시점이 실제로 중요합니다.
결과
레거시 관리 도구를 운영 중단 없이 기능 단위로 전환했습니다. 미전환 기능과 병행 운영되는 상태를 유지했습니다.
한계
전환 속도나 운영팀의 업무 시간 변화를 측정하지 않았습니다. “중단 없이 전환했다”는 것은 사고가 없었다는 뜻이지, 전환이 빨랐거나 도구가 더 나아졌다는 근거는 아닙니다.
그 밖의 담당 기여
쿠폰 관리 기능
쿠폰 목록 조회와 쿠폰 템플릿 관리 기능을 개발했습니다. 소비자 웹의 장바구니 쿠폰 기능(→ 식봄 웹 탭)과 같은 GraphQL 스키마를 공유하므로, 운영자가 관리자에서 발행한 쿠폰이 소비자 화면에서 어떻게 계산되는지 양쪽을 모두 이해한 상태로 구현할 수 있었습니다. 두 서비스 간 쿠폰 정책이 어긋날 여지를 스키마 수준에서 없앤 셈입니다.
Relay로 쿠폰 목록의 페이지네이션·필터링을 서버 주도로 처리
TanStack Table로 다양한 정렬·필터 조합을 지원하는 테이블 UI 구성
···
참고 — 기술 선택과 기술 스택
기술
선택 이유
TanStack Router
URL 파라미터·쿼리 스트링이 TypeScript 타입으로 강제됨 — 라우트 이동 시 파라미터 누락·타입 불일치를 컴파일 단계에서 검출
Shadcn/ui
접근성(ARIA)이 내장된 헤드리스 컴포넌트 위에 스타일만 얹는 패턴. 모달·드롭다운·팝오버의 키보드·스크린리더 대응을 직접 구현하지 않아도 됨
Relay
소비자 웹과 동일한 GraphQL 타입 시스템 공유. fragment 기반 데이터 선언으로 오버페칭 방지
식봄 물류 현장에서 사용하는 배송 라벨과 거래명세서를 React 컴포넌트로 구현한 인쇄 전용 라이브러리입니다.
담당 범위
설계부터 개발·배포까지 단독 수행
기술 스택과 무관한 단일 배포 구조 설계
배송 라벨·거래명세서 6종 템플릿과 mm 단위 그리드 레이아웃 구현
인쇄물 변경의 실물 프린터·스캐너 검증 절차 수립
문제와 해결
이 저장소에서 실제로 부딪힌 문제를 케이스로 정리했습니다. 각 케이스는 어떤 상황이었고 무엇이 제약이었는지에서
시작해, 그래서 어떻게 판단했고 무엇이 남았는지까지 적었습니다.
같은 라벨이 서비스마다 다르게 인쇄된다
상황
식봄은 Next.js 신규 웹과 PHP 레거시 웹이 병행 운영되고, 관리자 웹은 또 다른 React SPA입니다. 세 서비스 모두에서 같은 배송 라벨을 출력할 수 있어야 했습니다.
같은 배송 라벨이 세 서비스에서 서로 다르게 인쇄되면 물류 현장에서 곧바로 사고가 됩니다. 화면 UI가 조금 어긋나는 것과 달리, 인쇄물은 그대로 상자에 붙어 나갑니다.
제약
세 서비스는 프레임워크도 번들러도 서로 다릅니다. npm 패키지로 배포하면 각 서비스가 제각기 다른 시점에 버전을 올리게 되고, 그 시차 동안 서로 다른 라벨이 출력됩니다. 서비스마다 인쇄 로직을 각자 구현하는 것은 애초에 이 문제의 원인입니다.
접근과 결정
라이브러리를 UMD 번들로 빌드해 사내 배포 채널에 올리는 방식을 택했습니다. 각 서비스는 자신의 번들러·프레임워크와 무관하게 스크립트 한 줄로 동일한 버전을 불러오고, 전역에 노출된 두 개의 API만 호출합니다.
빌드 → UMD 번들 (.umd.cjs)배포 → 사내 배포 채널 (전 서비스 공통 버전)사용 → template(payload) — 템플릿 DOM 생성 print(payload) — 인쇄 다이얼로그 실행
핵심은 번들 포맷이 아니라 버전이 하나뿐이라는 점입니다. 소비하는 쪽이 버전을 고를 수 없으므로, “어떤 서비스가 아직 안 올렸나”를 관리할 필요 자체가 사라집니다. 소비측 인터페이스를 함수 두 개로 좁게 유지한 것도 같은 이유입니다 — 표면이 좁아야 세 서비스를 한꺼번에 바꿔도 깨지지 않습니다.
결과
세 개의 서로 다른 기술 스택(Next.js · PHP · React SPA)에서 동일한 인쇄 결과를 보장하고, 서비스별 인쇄 로직 중복을 제거했습니다.
한계
단일 버전 전략은 롤백 단위도 하나라는 뜻입니다. 한 서비스에서만 문제가 생겨도 되돌리면 전체가 되돌아갑니다. 서비스별 버전 고정 수단은 두지 않았습니다.
인쇄물 오류는 배포 후 현장에서 발견된다
상황
배송 라벨의 바코드는 물류 시스템 스캐너와 1:1로 대응하는 고유 값을 인코딩합니다. 바코드 사양이 바뀌면 스캐너 연동이 즉시 깨지고, 이는 배포 후에야 현장에서 발견됩니다.
브라우저 미리보기로는 잡히지 않습니다. 화면에서 멀쩡해 보이는 바코드가 실제 프린터에서 인쇄되면 해상도·여백 때문에 판독되지 않을 수 있습니다.
접근과 결정
변경 건마다 실제 프린터로 출력하고 실제 스캐너로 판독하는 검증 단계를 배포 전 필수 절차로 두었습니다. 자동화할 수 없는 종류의 검증이라, 자동화 대신 절차로 고정하는 쪽을 택했습니다.
검증을 앞당기기 위해 Playground 개발 도구를 함께 만들었습니다. json-edit-react 기반으로, MOCK 데이터를 실시간 편집하면 템플릿 미리보기가 즉시 갱신되어 백엔드 연동 없이 인쇄 결과를 확인할 수 있습니다. QA가 백엔드 배포를 기다리지 않아도 되므로, 검증 리드타임이 배포 주기에서 분리됩니다.
결과
물류팀 요청으로 바코드 구성을 조정할 때도 이 절차를 거쳐 연동 장애 없이 반영했습니다.
한계
절차는 사람이 지켜야 유지됩니다. 실물 검증을 건너뛰어도 배포를 막는 장치는 없어서, 담당자가 바뀌면 유실될 수 있는 형태의 대책입니다.
그 밖의 담당 기여
그리드 기반 레이아웃 시스템
인쇄물은 화면과 달리 물리적 치수가 곧 스펙입니다. 픽셀 좌표로 배치하면 프린터·드라이버마다 결과가 달라집니다.
각 템플릿을 mm 단위 행·열 배열로 용지를 격자 분할하고, 각 셀에 필드를 이름으로 배치하는 방식으로 설계했습니다. 셀 병합은 인접 셀에 같은 필드명을 반복해 표현합니다. 레이아웃 변경이 배열 숫자 수정만으로 끝나고, 절대 좌표 계산이 코드에서 사라집니다.
행 높이(mm) = [10, 12, 10, ...] ← 용지를 가로로 분할열 너비(mm) = [11, 48, 11, 22] ← 용지를 세로로 분할배치 맵 = [ ['바코드', '납품처명', '납품처명', 'QR'], ← 같은 이름 반복 = 셀 병합 ['바코드', '품명', '품명', 'QR'], ...]
필드는 데이터를 받아 렌더링 구조를 반환하는 Provider 함수로 정의하고, 템플릿은 Provider 조합으로 완성됩니다. 바코드·QR·텍스트·이미지 등 필드 종류가 늘어도 템플릿 코드는 그대로입니다.
저장품 라벨 신규 템플릿 추가
축산물 이력제 의무 표시 규정에 대응해 저장품 라벨을 세 유형(이력·비축산, 이력·축산물, 비이력·비축산)으로 분리 구현했습니다. 유형별로 의무 표시 항목(원산지, 이력 번호, 이력 QR 등)이 달라 하나의 템플릿에 조건 분기를 넣는 대신 별도 템플릿으로 분리했습니다. 규정이 바뀌는 유형만 독립적으로 수정할 수 있습니다.
한글 도메인 코드 (유비쿼터스 언어)
파일명·변수명·함수명을 백엔드 도메인 용어와 동일한 한글로 작성했습니다.
templates/일반배송_라벨utils/바코드utils/날짜포맷팅utils/원산지
인쇄물 사양의 최종 결정권자는 개발자가 아니라 물류·운영 담당자입니다. 코드가 그들의 용어를 그대로 쓰면 “원산지 필드 위치를 바꿔달라”는 요청이 코드의 어느 부분인지 번역 없이 대응됩니다.
···
참고 — 템플릿 목록과 기술 선택
템플릿 목록
템플릿
용지
용도
일반배송 라벨
100×100 mm
일반 식자재 배송 라벨
저장품 라벨 (비이력·비축산)
100×100 mm
저장품 라벨 (비이력 추적, 비축산)
저장품 라벨 (이력·비축산)
100×100 mm
저장품 라벨 (이력 추적, 비축산)
저장품 라벨 (이력·축산물)
100×100 mm
저장품 라벨 (이력 추적, 축산물)
거래명세서 (일반배송)
A4
일반 배송 거래명세서
거래명세서 (저장품배송)
A4
저장품 배송 거래명세서
기술 선택 이유
기술
선택 이유
UMD 번들
소비측 서비스의 프레임워크·번들러에 의존하지 않는 유일한 배포 형식
React
템플릿을 선언적 컴포넌트 트리로 표현 — 필드 조합·재사용이 용이
Tailwind CSS
mm 단위 임의 값(arbitrary value)을 클래스로 직접 표현해 인쇄 치수를 코드에서 즉시 확인