결제완료 화면의 카드 발급 유도 팝업을 이벤트 기간에만 막아 달라는 요청을 받았다. 기간을 payload 로 받는 피처 플래그를 설계했다. AI 에이전트와 함께 검증 항목을 먼저 세우고, 변경 전후를 브라우저로 대조하고, 실제 플래그를 단계별로 켜고 끄고, 장애 상황까지 만들어 확인했다. PR 을 연 지 세 시간 남짓 만에 운영에 나갔고, 운영에서도 의도대로 도는 것까지 봤다. 빠르고, 안정적이고, 증거가 남았다. 잘 만들었다.
그런데, 싸늘하다. 가슴에 비수가 날아와 꽂힌다.
이게 잘된 구현일까?
요청은 “기간 동안 꺼 주세요” 한 줄이었다. 마케터가 캠페인 일정을 바꾸면 몇 분이면 끝났을 일이다. 그 한 줄이 내게 오기까지 팝업의 주인을 찾는 데 일주일이 걸렸고, 기존 스위치로는 꺼지지 않았고, 나는 결국 그 팝업을 제품 코드에 더 깊이, 더 단단하게 박아 넣었다. 하지 말아야 할 구현을 아주 잘 해낸 것이다.
이 글은 그 싸늘함의 정체를 따라간다. 결론은 단순하다. 거래를 끝내는 기능(코어)과 다시 오게 만드는 기능(부수)을 가르지 않고 같은 제품 코드에 넣으면, 부수 기능이 코어 앞에 서고 모든 요청이 코드로 들어온다. 요구는 프론트가 거절할 수 없는 모양으로 오고, 실험은 근거 없이 들어와 끝나지 않고 남는다. 지금 속도로 쌓이면 제품은 부수 기능의 무게를 버티지 못한다.
TL;DR
- 잘 만들었다. 기간을 받는 피처 플래그, 검증 항목 12개, 변경 전후 브라우저 대조, 실제 플래그 단계별 확인과 장애 시뮬레이션, 운영 검증까지 갔다. AI 와 함께한 덕에 PR 을 연 뒤 운영까지 세 시간 남짓이었다.
- 그런데 하지 말아야 할 구현이었다. 이 팝업은 기획서에서 우선순위 낮은 부수 기능이었는데 제품 코드로 만들어졌다. 같은 카드를 함께 운영하는 다른 서비스는 같은 팝업을 CRM 도구로 띄웠다. 이번 플래그는 그 잘못된 자리를 더 단단하게 고정했다.
- 그 뒤 반년 동안 이 카드 기능의 변경은 전부 프론트 코드로 들어왔다. 머지된 PR 31건 중 18건이 수정이고, 6건은 문구만 바꿨다.
- 요구는 거절할 수 없는 모양으로 왔다. 날짜가 먼저 정해졌고, 해법(“결제완료에 바텀시트”)이 요구 안에 들어 있었고, 마케팅과 백엔드를 거친 요청은 마지막에 화면을 가진 프론트로 모였다.
- 실험은 근거 없이 들어와 남는다. 결제완료 화면 전체가 실험 하나의 로딩을 기다리고, 기한이 2년도 더 지난 플래그가 앱 세 곳에 복제돼 있고, 1년 전 “실험”으로 들어온 버튼은 지금 브랜드 제외 목록이 박힌 규칙이 됐다.
- 한 번의 사고가 아니다. 핵심 화면 네 곳(장바구니·주문서·결제완료·상품 상세)에 거래와 무관한 노출이 45개, 켜고 끄는 방법이 11가지였다. 같은 팝업 구조는 다른 제휴카드로 복제되고 있었다.
- 해법은 “CRM 도구를 쓰자”가 아니라 “노출 판단까지 넘기자”다. 렌더만 옮기면 빈도 하나 바꾸는 것도 코드 수정이 된다.
여기까지만 읽어도 된다. 아래는 근거와 기준이다.
1. 자랑부터 하자: PR 을 연 지 세 시간 남짓 만에 운영까지
이벤트 기획서에는 결제완료 화면에 이벤트 팝업을 띄우고, 기존 카드 발급 팝업과는 동시에 노출하지 않는다는 조항이 있었다. 이벤트 본편 기간 내내 카드 팝업을 내려야 한다는 뜻이다. 기간은 정해져 있고, 기간이 끝나면 다시 떠야 한다.
그래서 기간을 받는 피처 플래그를 만들었다.
| 플래그 상태 | 동작 |
|---|---|
| OFF | 지금과 같이 노출 |
| ON + 기간(payload) | 그 기간에만 차단 |
| ON + 기간 없음 | 기간 없이 차단 |
| ON + 기간 형식 오류 | 차단하고 에러 로그 |
| 플래그 조회 실패 | 3초 뒤 OFF 로 판정 |
형식이 틀렸을 때 노출이 아니라 차단으로 정한 이유도 하나 적어 둔다. 비슷한 선례(쿠폰 랜딩 덮어쓰기)는 형식이 틀리면 덮어쓰기를 무시한다. 거기서는 무시가 곧 현행 유지라 안전했다. 여기서는 무시가 “막아 달라는 걸 안 막은 상태”라서 그게 사고다. 같은 모양의 설정이라도 실패 방향은 요청의 목적이 정한다.
검증은 AI 에이전트와 함께 사다리처럼 올라갔다.
- 검증 항목부터. 에이전트에게 티켓과 기획서를 읽혀 검증 항목 12개를 먼저 세웠다. 새로 생기는 동작 7개, 지켜야 할 기존 동작 5개. 그중 6개는 브라우저로, 6개는 단위 테스트로 봤다.
- 변경 전후 대조. 같은 브라우저 스모크를 변경 전 코드와 변경 후 코드에 각각 돌렸다. 새 동작은 변경 전에 실패하고 변경 후에 통과하며, 기존 동작은 양쪽 다 통과한다는 걸 화면 녹화로 남겼다.
- 실제 플래그로 단계별. 개발 환경 플래그를 OFF → 기간 안 → 기간 밖으로 바꿔 가며, 확인한 시각과 그때 브라우저가 받은 플래그 값을 같이 기록했다.
- 장애 시뮬레이션. 브라우저에서 플래그 요청을 막아 조회 실패 상황을 만들었다.
- 리뷰. 자동 리뷰 봇의 리뷰를 세 라운드 거치고 코드 오너 승인을 받았다. 정적 분석 신규 위반은 0.
- QA 환경. 배포 뒤 같은 단계를 QA 환경에서 다시 밟았다.
- 운영. 페이지가 내려주는 빌드 ID 가 바뀌는 것으로 롤아웃을 확인하고, 새 번들에 이번 코드가 들어 있는지 봤다. 운영 플래그를 잠깐 켜서 저장된 값이 실제 판정 코드로 의도대로 읽히는지 보고, 운영 결제완료 화면은 내 브라우저 탭 안에서만 조건을 채워 확인했다.
사람이 한 건 판단과 플래그 토글이었고, 나머지는 에이전트가 돌렸다. PR 을 연 뒤 운영 롤아웃까지 세 시간 남짓 걸렸고, 운영 화면 확인까지 그날 오후에 끝냈다. 동작은 의도대로다. 지금 다시 해도 이보다 잘 만들 자신은 없다.
2. 그런데, 싸늘하다: 팝업 하나를 끄는 데 든 것
비수는 구현 바깥에 있었다.
이 요청이 내게 오기까지의 일주일을 다시 보자. 이벤트를 맡은 기획 A 는 먼저 마케팅에 이 팝업을 끌 수 있는지 물었다. 마케팅은 처음엔 자기 쪽 팝업이 아니라고 했다가, 제품에서 직접 띄우는 팝업이라고 정정했다. 백엔드는 자기 쪽에서 만든 게 아니니 프론트에서 조건을 추가하는 게 낫겠다고 했다. 그사이 요청은 업무요청 채널을 거쳐 내게 왔다. 팝업 하나의 주인을 찾는 데 일주일이 걸렸다.
넘겨받고 보니 이미 카드 넛지 전용 플래그가 있었다. 그런데 그 플래그는 넛지 배너를 감싸는 컴포넌트에만 걸려 있었고, 팝업은 그 바깥에서 디자인 시스템 바텀시트를 직접 쓰고 있었다. 끄면 배너만 사라지고 문제의 팝업은 그대로 뜬다. 스위치가 있는데도 끌 수 없었다.
돌아보면 싸늘한 이유는 셋이다.
- 요청은 한 줄이었다. 처음 질문이 나온 날부터 운영 배포까지 2주, 그중 일주일은 주인을 찾는 데 썼다.
- 몇 분짜리 일이 배포가 됐다. 마케터가 캠페인 일정을 바꾸면 끝났을 일에 코드 7개 파일, 단위 테스트, 리뷰 세 라운드, QA 배포, 배포 승인, 운영 검증이 붙었다.
- 결과물은 해결이 아니라 고정이다. 팝업은 여전히 제품 코드에 있고, 이제 기간 판정 로직과 스위치를 하나 더 달고 있다. 다음 이벤트도 같은 길로 온다.
빠르고 안정적으로 만들 수 있다는 건 좋은 일이다. 그런데 그 속도가 “이걸 코드로 하는 게 맞나”를 물을 틈까지 없앴다. 1절의 검증 사다리는 “의도대로 도는가”를 끝까지 증명했지만, “이걸 만들어야 했는가”는 한 번도 묻지 않았다.
3. 제품 변경 라인: 반년 동안 무엇이 들어왔나
이 팝업은 제휴카드를 도입할 때 생겼다. 기획서에는 “결제완료 화면에 카드 발급 유도 바텀시트, 미발급자 대상, 하루 최대 1회, 우선순위 P2”라고 적혀 있었다. 처음부터 거래와 무관한, 우선순위 낮은 부수 기능이었다. 프론트 B 가 티켓을 만들어 코드로 구현했다. 노출 대상(미발급자)은 결제완료 API 응답 값으로, 하루 1회는 브라우저 저장소로 판정했다. CRM 도구를 검토한 흔적은 기획서에도 티켓에도 없다.
같은 시기, 같은 카드를 함께 운영하는 다른 서비스는 같은 바텀시트를 CRM 도구(Braze)로 띄웠다. 그쪽도 순탄하진 않았다. 직접 구현으로 바꿨다가 iOS 에서 카드 신청 화면으로 넘어가지 않는 문제가 생겨 다시 CRM 도구로 돌아갔다. 이 반례는 9절에서 다시 쓴다.
그 뒤로 이 카드 기능에 들어온 변경을 시점별로 펼치면 이렇다. 왼쪽은 요구, 오른쪽은 코드에 남은 것이다.
| 시점 | 들어온 요구 | 코드에 남은 것 |
|---|---|---|
| 출시 전 | 결제완료 바텀시트, 그리고 상품 상세·주문서·마이페이지·결제완료의 카드 넛지 | 넛지 공용 패키지, 카드 안내 페이지, 발급 연결 화면. 브랜치 커밋만 186개 |
| 출시 2주 전 | 사용 시나리오 변경, 일주일 간격으로 두 번 | 넛지·안내 화면 재작업 |
| 출시 직전 | QA 버그 30건가량이 한꺼번에 몰림 | 날짜별 QA 대응 티켓 일곱 개 |
| 출시 주간 | 금요일에 먼저 배포하고 월요일에 플래그를 켜는 출시 | 배포 여섯 번 |
| 출시 후 두 달 | 문구, 할인 매칭, 브랜드별 감춤, 카드 기억 동작 제거, 자동 등록 안내 시간 단축, 첫결제 할인 중복 차단 | 수정 PR 이 연달아. 같은 안내 시간 수정은 배포 브랜치와 main 에 한 번씩, 두 번 |
| 3개월 뒤 | 앱 웹뷰에서 뒤로 갈 수 없는 문제 | 앱과 웹이 같은 주에 따로 고쳤다. 웹에는 뒤로가기 버튼과 플래그가 남았다 |
| 4개월 뒤 | 팝업 문구 수정(같은 카드를 쓰는 서비스 네 곳이 동시에), 이틀 뒤 다른 화면 문구 수정, 발급 연결 화면 동작 변경 | 문구 PR, 3주 동안 열려 있던 PR |
| 4개월 반 뒤 | 다른 서비스가 콘텐츠 API 를 폐기 → 카드 안내 페이지 장애 | 경로 교체 핫픽스, 실패 로깅 |
| 같은 주 | 안내 페이지 헤더가 두 번 뜸. 원인은 3개월 전 뒤로가기 수정 | 조건 한 줄, 플래그 제거 |
| 같은 달 | 다른 제휴카드의 결제완료 바텀시트 | 이 카드 바텀시트 구조를 그대로 확장 |
| 5개월 뒤 | 이벤트 기간 동안 팝업 차단 | 이 글의 플래그 |
숫자로 줄이면 이렇다. 이 카드 기능이 주목적인 PR 이 31건 머지됐다. 그중 18건이 수정이고, 6건은 문구만 바꾼 PR 이다. 기능을 만든 PR 은 11건이다. 만든 것보다 고친 것이 많다.
표의 몇 칸은 따로 적어 둘 만하다.
- 팝업 문구 하나를 바꾸는 데 같은 카드를 쓰는 서비스 네 곳이 각자 티켓을 만들고 배포했다. 그 수정이 마지막이 아니었다는 건 6절에서 다시 쓴다.
- 카드 안내 페이지는 다른 서비스의 콘텐츠 API 를 직접 호출하다가, 그 API 가 사라지면서 장애가 났다. 신고가 들어오고서야 알았다(에러 화면은 있었는데 로그가 없었다).
- 발급 연결 화면 변경 PR 은 3주 동안 열려 있었다(거의 3주 동안 머지하지 못한 PR 을 살려두는 법).
- 출시 뒤 담당이 바뀌면서 후속 정리 티켓 대부분은 착수되지 못했다. 사람 탓이 아니다. 정리할 시간이 일정에 없었다는 뜻이다.
이 글을 쓰기 전에는 하나하나를 별개의 사건으로 봤다. 모아 놓고 보니 같은 이야기다. 리텐션 성격의 기능 변경이 매번 커머스 프론트의 코드·리뷰·배포 절차로 들어왔다.
4. 패턴: 이 팝업만의 문제가 아니다
혹시 이 팝업만 특이한 건 아닌지, 거래를 다루는 핵심 화면 네 곳을 전부 세어 봤다. 기준은 하나다. 이번 거래를 끝내는 데 필요하면 코어(상품, 가격, 배송, 결제수단, 필수 고지, 주문 결과), 추가 행동을 유도하면 부수(카드·쿠폰 유도, 혜택 안내, 앱 설치, 추천, 배지, 타이머, 마케팅 팝업)다.
| 화면 | 부수 기능 | 프론트 플래그로 끌 수 있는 것 | 코드를 고쳐야만 바뀌는 것 |
|---|---|---|---|
| 장바구니 | 7 | 1 | 2 |
| 주문서 | 8 | 4 | 0 (문구·상한 고정값 3곳) |
| 결제완료 | 5 | 4 | 1 (이번 팝업이 플래그 추가 전까지 여기 속했다) |
| 상품 상세 | 25 | 6 | 7 |
| 합계 | 45 | 15 | 10 |
나머지 20개는 서버 응답 값에 기대어 켜지고 꺼진다. 켜고 끄는 방법을 모아 보니 11가지였다. 피처 플래그, 플래그 payload 로 문구 관리, 패키지 단위 래퍼, A/B 실험 도구(키 상수가 앱마다 복제돼 있다), 서버 응답 필드, 서버 쪽 절약 모드, 저장소 패키지 기반 노출 기록, 저장소 직접 접근, 쿠키, 코드 고정값, 앱 네이티브 위임이다. 카드 기능 하나에만 스위치가 일곱 군데(플래그 둘, 브라우저 저장소 둘, 서버 응답 값 셋) 흩어져 있었고, 그중 어느 것도 결제완료 팝업만 골라 끄지 못했다.
같은 구조는 복제되고 있었다. 다른 제휴카드의 결제완료 바텀시트가 “기존 카드 바텀시트 구조 확장”으로 만들어졌다. 하루 1회 저장소 로직까지 그대로다. 제휴카드가 하나 늘 때마다 결제완료에 코드 팝업이 하나씩 쌓인다.
반대 방향의 혼선도 있었다. 기획자들이 무료배송·첫구매 팝업을 개발 산출물로 알고 있었는데, 실제로는 CRM 도구의 캠페인이었다. 코드 팝업은 마케팅 것인 줄 알고, 마케팅 팝업은 코드인 줄 안다. 화면에 무엇이 어디서 뜨는지 그린 지도가 없다.
하나 정정해 둔다. 처음에는 “툴팁이 남발되고 있다”고 생각했다. 세어 보니 장바구니·주문서의 툴팁 일곱 개는 전부 배송 시작일, 착불, 도서산간 배송비 같은 핵심 정보였다. 부수 성격의 툴팁은 상품 상세에만 있었다. 주문 화면의 부수 기능은 툴팁이 아니라 넛지·배너·바텀시트 모양으로 들어와 있다. 인상과 숫자는 다를 수 있으니, 주장하기 전에 세어 볼 일이다.
5. 우선순위가 뒤집히는 모습
부수 기능이 많은 것 자체보다 무서운 건, 그것들이 코어 흐름 앞에 서 있다는 점이다.
- 결제완료는 실험이 끝나야 주문 결과를 그린다. 브랜드 관련 실험 플래그 응답이 올 때까지 화면 전체가 로딩 바만 보여 준다. 이번 작업에서 플래그 서버 장애를 재현해 보다가 알았다. 플래그 서버가 응답하지 않으면 이번 변경과 상관없이 사용자는 주문이 끝났는지 볼 수 없다.
- 할인이 0원이면 혜택 요약 자리를 카드 넛지가 차지한다. “이번 주문으로 받은 혜택”이 있어야 할 자리에 “이 카드였다면 받았을 혜택”이 들어간다.
- 바로구매는 업셀 시트를 거쳐야 결제로 간다. 결제 화면으로 넘어가는 콜백 자체를 업셀 시트 트리거가 감싸고 있다. 끄는 프론트 플래그는 없다.
- 장바구니에 담으면 추천 시트가 뜬다. 코드에 고정돼 있다.
- 주문서의 최종 결제 요약 안에 카드 넛지가 있다. 적립률이 없으면 10% 로 보이게 기본값이 박혀 있고, 넛지를 누르면 결제수단과 할인이 결제 상태에 바로 반영된다.
- 부수 기능이 다른 기능의 검증을 막았다. 배송비 표기 작업(작은 티켓이 엿새 걸린 이유)의 E2E 는 결제완료에서 자꾸 실패했다. 카드 팝업이 늦게 떠서 클릭을 가로채고 있었다. 테스트 지원 코드에 “카드 발급 팝업이 클릭을 가로챈다”는 주석과 함께 팝업 닫기 함수가 들어갔다.
- 두 팝업 중 무엇을 먼저 보여 줄지 정할 곳이 없었다. 이벤트 팝업과 카드 팝업의 순서는 결국 코드 배포로 정했다.
부수 기능의 변경은 코어의 릴리스·리뷰·배포 절차를 탄다. 거꾸로 부수 기능의 문제는 코어를 흔든다. 우선순위가 뒤집혔다는 건 이 두 방향을 같이 말하는 것이다.
6. 요구는 프론트가 거절할 수 없는 모양으로 왔다
3절의 표를 보면 “프론트가 왜 그때 코드로 하지 말자고 하지 않았나”라는 질문이 나올 수 있다. 말할 틈이 없었다. 요구는 매번 이미 결정된 채로 왔다.
날짜가 먼저 정해졌다. 출시일은 카드사와 맞춘 사업 일정이었다. 개발 완료 일정이 “미정”이라고 적힌 채로 QA 요청서가 먼저 나갔고, 출시 검토 문서에는 기한이 지났다는 자동 리마인드가 열세 번 달렸다. 출시 2주 전까지 사용 시나리오가 두 번 바뀌었다. 이벤트 기간 차단도 마찬가지다. 이벤트 시작일은 바뀌지 않는다. 프론트에 남은 질문은 “할까 말까”나 “어디서 할까”가 아니라 “언제까지”뿐이었다.
해법이 요구 안에 들어 있었다. 기획서 조항은 “결제완료 화면에 바텀시트”, “이 기간에는 동시 노출 금지” 같은 화면 지시였다. 무엇을 할지뿐 아니라 어떻게 할지까지 정해진 상태로 왔다. 그 해법이 제품 구조와 맞지 않을 때도 있었다. 발급 연결 화면 변경 요구는 “웹뷰를 닫는다”였는데, 이 흐름은 한 웹뷰 안에서 페이지만 바뀌는 구조라 닫으면 주문서까지 같이 닫힌다. 요구가 구조를 모른 채 왔고, 프론트가 다른 방법을 찾아 맞췄다. 그 과정에서 출시 때부터 빠져 있던 유입 경로 표시까지 찾아 고쳤다.
요청은 결국 프론트로 모인다. 마케팅은 자기 것이 아니라고 하고, 백엔드는 프론트에서 조건을 추가하는 게 낫다고 한다. 화면에 보이는 문제는 화면을 가진 쪽이 마지막 수신자가 된다. 프론트가 넘길 곳은 없다.
“이게 마지막”은 지켜지지 않는다. 팝업 문구를 서비스 네 곳이 같이 고친 날, 더 이상의 수정은 없게 하겠다는 약속이 나왔다. 이틀 뒤 같은 카드의 다른 화면 문구를 바꿔 달라는 티켓이 왔다. 그 티켓은 문구 재검토로 두 번 멈췄다가 한 달 넘게 지나 배포됐다.
거절할 대안이 없다. “이건 CRM 도구로 하자”고 말하려면 대상 데이터를 도구에 넣을 사람, 캠페인을 설계할 사람, 그 캠페인을 책임질 사람이 있어야 한다. 그 자리가 비어 있으면 대안을 말하는 순간 그 일까지 떠안는다. 그러니 프론트가 코드로 하는 게 가장 빠르고, 그래서 늘 프론트가 코드로 한다.
거절의 비용은 비대칭이다. 거절하면 출시나 이벤트를 막는 사람이 된다. 받아들이면 비용은 나중에, 보이지 않게 쌓인다. 31건 중 18건이 수정이었다는 숫자가 그렇게 쌓인 비용이다. 누구도 그 비용을 결정한 적이 없다.
요청하는 사람이 잘못한 게 아니다. 웹 제품을 어떻게 다룰 수 있는지 모르는 쪽에서 보면, 화면에 보이는 건 전부 개발 산출물이다. 그래서 코드 팝업도 개발에 요청하고, CRM 캠페인 팝업도 개발 산출물로 안다. 같은 뿌리다. 그리고 받는 쪽인 개발도 “해 달라니까” 코드로 해 왔다. 나도 그랬다. 이번 플래그를 만든 것도 나다.
7. 실험이라는 이름으로 들어온 것들
부수 기능이 코드로 들어오는 또 하나의 길은 “실험”이다. 실험은 원래 가설과 지표와 끝나는 날이 있어야 실험이다. 셋 중 하나라도 빠지면, 확인하지 않은 기능이 실험이라는 이름으로 코어 화면에 들어온 것이다. 코드에서 찾은 흔적은 이렇다.
결제완료 화면이 실험 하나를 기다린다. 5절에서 적은 대로, 브랜드 관련 실험의 플래그 응답이 와야 주문 결과가 그려진다. 그 실험에는 앱 빌드 번호 하한까지 코드에 박혀 있다. 안드로이드와 iOS 의 하한이 다르다는 걸 테스트가 따로 지키고 있을 정도로 공을 들였다. 그런데 그 실험이, 주문 결과를 보여 준다는 이 화면의 존재 이유보다 앞에 서 있다.
주문 앱 하나에 실험이 이만큼 있다. A/B 실험 도구의 키가 여덟 개, 이름에 “실험”이 붙은 훅이 여섯 개, 실험 전용 페이지 라우트가 하나다. 그중 하나는 주석에 “제거 임팩트 확인 실험 #2”라고 적혀 있는데 키 이름 끝은 -3 이다. 같은 실험을 몇 번 다시 열었는지, 앞선 결과가 무엇이었는지 코드만으로는 알 수 없다.
실험 키 규칙에는 “끝”이 없다. 실험 키 상수 파일은 앱마다 따로 복제돼 있다. 파일 머리의 규칙은 “키를 추가한 사람과 적용 범위를 주석으로 남겨라”다. 언제 끝나는지, 결과가 어땠는지를 남기라는 칸은 없다. 넣는 절차는 있고 빼는 절차는 없다.
기한이 지난 플래그가 남아 있다. 주석에 “@until 24.04.11” 이라고 적힌 플래그 훅이 앱 세 곳에 똑같이 복제돼 아직 있다. 2년도 더 전에 끝났어야 할 코드다. 2025년 7월이 기한인 플래그도, 기한이 “미정”인 플래그도 있다.
실험이 규칙이 된다. 상품 상세 상단의 비슷한 상품 추천 버튼은 1년 전 “추천 티저 버튼 실험을 추가합니다”라는 커밋으로 들어왔다. 지금 그 코드에는 플래그가 없다. 대신 특정 브랜드 ID 다섯 개를 노출에서 빼는 목록이 “비즈니스 규칙”이라는 주석과 함께 박혀 있다. 실험이 결론을 남기고 규칙이 된 건지, 그냥 남아서 규칙처럼 굳은 건지 코드는 말해 주지 않는다.
카드 팝업도 다르지 않다. 이 팝업의 효과를 적은 내부 문서끼리도 숫자가 맞지 않는다. 효과가 합의되지 않은 채, 결제완료 화면에서 반년 가까이 하루 한 번씩 자동으로 떴다.
내가 보기에 이건 실험이 아니라 “일단 넣어 보기”다. 넣는 비용은 작아 보이고 빼는 비용은 누구의 것도 아니라서, 화면은 넣는 쪽으로만 기운다. 상품 상세의 부수 기능 25개는 2년이 채 안 되는 사이에 하나씩 더해졌고, 네 화면의 부수 기능 추가는 올해 4월에서 9월 사이에 몰려 있다. 실험이 많아서 문제가 아니라, 실험이 끝나지 않아서 문제다.
8. 왜 이렇게 되나: 요청을 받는 순간 정하는 단계가 없다
6절과 7절을 묶으면 원인은 하나다. 요청은 늘 화면의 언어로 들어온다. “결제완료에 팝업 띄워 주세요”, “이 기간에는 꺼 주세요”, “문구 바꿔 주세요”, “이 버튼 실험해 봐요”. 화면의 언어로 들어온 요청은 화면을 가진 사람, 즉 제품 코드로 떨어진다. 그 사이에 “이건 어디서 처리할 일인가”를 묻는 단계가 없다.
코어와 부수는 성질이 다르다.
| 코어 기능 | 부수 기능 (발급 유도, 가입 유도, 혜택 안내, 추천) | |
|---|---|---|
| 바뀌는 주기 | 제품 릴리스 단위 | 캠페인·이벤트 단위, 수시로 |
| 바꾸는 사람 | 개발 | 마케팅·CRM |
| 실패했을 때 | 절대 깨지면 안 된다 | 안 떠도 괜찮다 |
| 성과를 보는 곳 | 제품 지표 | CRM 지표 |
| 끝 | 없다 (계속 유지) | 있어야 한다 (기간·실험 종료) |
이 둘을 한 코드에 넣으면 이 다섯 가지가 서로 묶인다. 수시로 바뀌어야 할 것이 릴리스 주기를 타고, 안 떠도 될 것이 절대 깨지면 안 되는 화면을 흔들고, 끝나야 할 것이 끝나지 않는다.
이번 플래그도 그렇다. 피처 플래그의 분류(Feature Toggles)로 보면 릴리스 토글도, 실험 토글도, 운영 토글도 아니다. 운영자가 기간을 바꾸며 쓰는 캠페인 스케줄러를 피처 플래그와 JSON 으로 흉내 낸 것이다. 잘 동작하지만, 그 역할을 원래 하는 도구가 따로 있다.
9. CRM 도구로 옮기면 끝인가 — 아니다
3절의 다른 서비스 이야기를 다시 꺼낸다. 그 서비스는 바텀시트를 CRM 도구로 띄우면서 노출 쿨다운은 프론트 코드에 남겼다. 그래서 “다시 보여 주기까지 48시간 → 5일” 같은 변경도 코드 수정으로 처리됐다. 렌더는 도구가 하는데 판단은 코드가 한다. 같은 문제가 모양만 바꿔 반복된다.
넘겨야 하는 건 렌더가 아니라 노출 판단이다.
- 누구에게 (대상)
- 언제 (기간)
- 얼마나 자주 (빈도)
- 무엇 다음에 (다른 팝업과의 우선순위)
이번 경우 가장 어려워 보이는 건 대상, 즉 “카드 미발급자에게만”이다. 프론트는 카드가 발급됐는지 알 수 없다. 발급은 카드사 화면에서 끝난다. 하지만 발급 완료는 이미 데이터 파이프라인으로 흐른다. 그 지점에서 CRM 도구의 사용자 속성을 갱신했다면, 미발급자 대상·이벤트 기간 제외·하루 1회·이벤트 팝업 다음 순서는 전부 캠페인 설정이 된다(인앱 메시지, 이벤트 트리거 발송). 결제완료 화면은 이미 진입 이벤트를 CRM 도구로 보내고 있다. 이번 요청은 마케터가 캠페인 일정을 바꾸는 일로 끝났을 것이다.
하나 더. CRM 도구를 쓰자는 이유는 전환 효율이 아니다. CRM 도구의 팝업이 제품 UI 보다 잘 팔린다는 근거는 없고, 디자인 일관성은 오히려 손해일 수 있다. 이유는 격리다. 어느 채용 서비스의 공고 화면 하단 가입·로그인 유도도 CRM 도구로 뜬다. 그 배너가 안 떠도 공고는 보이고, 문구나 대상을 바꿔도 공고 화면은 재배포하지 않는다. 성질이 다른 부작용을 낼 수 있는 기능일수록 제품과 떼어 놓는 게 맞다. 실험도 같다. 끝나는 날과 결과를 도구가 들고 있으면, 실험이 끝났을 때 제품 코드에는 아무것도 남지 않는다.
10. 요청을 받을 때 먼저 던질 질문 네 가지
다음에 비슷한 요청이 오면 코드를 열기 전에 이 넷부터 묻는다.
- 이게 없으면 거래가 안 끝나나?
- 기간·대상·빈도를 바꿀 사람이 개발자가 아닌가?
- 안 떠도 괜찮나?
- 성과를 CRM 지표로 보나?
1번이 “아니오”이고 나머지 중 하나라도 “예”면, 제품 코드가 아니라 캠페인 도구로 보낸다. 그리고 대상 판단에 필요한 데이터부터 도구에 넣는다. 렌더만 옮기고 판단을 코드에 남기면 9절의 반례가 된다.
실험으로 들어오는 요청에는 하나를 더 묻는다. “이 실험은 언제, 무엇을 보고 끝나나요?” 답이 없으면 그건 실험이 아니라 기능 추가이고, 기능 추가라면 위의 네 질문을 다시 거친다.
당장 할 수 있는 건 작다.
- 새로 만드는 것부터 이 질문을 거친다. 기존 것을 한꺼번에 옮기자는 게 아니다.
- 손댈 일이 생긴 부수 기능부터 하나씩 옮긴다. 이번 카드 팝업이 첫 후보다.
- 실험 키에 끝나는 날을 적는 칸을 만든다. 넣는 절차만 있는 곳에 빼는 절차를 하나 더한다.
- 화면별 노출 지도를 한 장 만든다. 무엇이, 어디서(코드인지 도구인지), 무엇으로 켜고 끄는지. 주인을 찾는 데 일주일을 쓰지 않으려면 이것부터 필요하다.
마치며
이번 구현은 잘된 구현이었나. 코드로 보면 그렇다. 설계는 실패 방향까지 따졌고, 검증은 운영까지 갔다. 제품으로 보면 아니다. 하지 말아야 할 구현을 잘 해냈고, 잘 해냈기 때문에 이 길은 다음에도 열려 있다. 다음 이벤트도, 다음 제휴카드도, 다음 실험도 같은 길로 올 것이고, 그때마다 결제완료 화면에는 판단 하나와 스위치 하나가 더 쌓인다.
그 대가는 이미 치르고 있다. 문구 하나에 PR 이 붙고, 다른 기능의 테스트가 팝업에 막히고, 결제완료는 실험을 기다리고, 팝업 하나의 주인을 찾는 데 일주일이 간다.
처음에 빌려 온 「타짜」의 대사는 이렇게 이어진다. “하지만 걱정하지 마라. 손은 눈보다 빠르니까.” AI 와 함께하는 손은 정말 눈보다 빠르다. 이번에도 그랬다. 그래서 더 걱정해야 한다. 빠른 손은 하지 말아야 할 패도 빨리 돌린다. 검증이 아무리 단단해도, 검증은 “만들어야 했는가”를 대신 묻지 않는다.
다음에는 손보다 눈을 먼저 쓰겠다. 코드를 열기 전에 묻는다. “이건 어디서 처리할 일인가요?”
읽을 거리
- 작은 티켓이 엿새 걸린 이유 — 카드 팝업이 E2E 클릭을 가로챈 바로 그 작업
- 거의 3주 동안 머지하지 못한 PR 을 살려두는 법 — 같은 카드 기능의 발급 연결 화면 변경
- Feature Toggles (aka Feature Flags) — Pete Hodgson
- Braze — In-app messages
실제로 겪은 일이다. 서비스와 사람은 알아볼 수 없게 바꿔 적었다.