들어가며
센디에는 라이트 · 스탠다드 · 비즈니스 · 엔터프라이즈 네 가지 플랜이 있다. 플랜마다 차량 옵션·가격 정책·서비스 범위가 다른데, 정작 이 구조가 사용자에게 직접 노출되지 않았다. 신규 가입자는 스탠다드로 기본 할당됐고, 비즈니스·엔터프라이즈로 이동하는 경로는 영업·고객센터 유입에만 의존하고 있었다.
이 글은 플랜 전환 경로를 제품 안에 만들기 위해 운송 플랜 온보딩 화면을 RN으로 구현한 과정, 그리고 신규 소셜 가입자 한정으로 A/B 실험을 돌리고 결과를 A로 확정하기까지의 기록이다. 뒤이어 운송 리뷰 기능도 같은 구조로 RN으로 확장한 이야기까지 묶는다.
진행 배경
플랜 구조가 보이지 않는 상태
내부 OKR 중 하나는 "스탠다드 고객의 비즈니스·엔터프라이즈 전환율을 높이는 것"이었다. ARPU(Average Revenue Per User)는 플랜이 올라갈수록 크게 차이 난다. 비즈니스·엔터프라이즈 고객은 월간 운송 건수도, 건당 단가도 스탠다드보다 훨씬 높다.
그런데 앱 안에서 사용자가 "나에게 맞는 플랜"을 스스로 탐색할 수 있는 경로가 없었다. 영업 채널을 통한 전환은 꾸준히 들어왔지만, 세일즈 리소스에 병목이 걸렸다. 앱 내에서 사용자가 자기 운송 목적을 선택하고 그에 맞는 플랜으로 자연스럽게 유입되는 구조가 필요했다.
문제 정의
두 가지를 문제로 정의했다.
- 플랜 구조의 가시성: 신규 사용자가 플랜의 존재 자체를 모른다. 가입 직후 적절한 온보딩이 없어 스탠다드에 머무른다
- 운송 목적에 따른 분기: 같은 스탠다드 안에서도 "개인 이사", "사무실 이전", "정기 화물" 같은 목적이 섞여 있는데, 목적별로 맞는 플랜 안내가 다르다
이 두 문제를 한 온보딩 화면에 담기로 했다. 운송 목적을 선택하면 → 목적에 맞는 플랜을 안내하고 → 바로 오더폼으로 연결.
개선 방안 도출: 네이티브 vs RN
대안은 세 가지였다.
| 대안 | 평가 |
|---|---|
| 네이티브로 구현 (UIKit + SwiftUI) | 기존 방식. 안정적이지만 이터레이션 속도가 느림. 디자인 변경 시 배포 주기 필요 |
| 웹뷰(WKWebView)에 웹 페이지 로드 | 디자인 반복은 빠르지만 네이티브 네비게이션·상태와의 통합 비용이 큼. 딥링크 엔트리가 복잡해짐 |
| React Native로 구현 (채택) | 디자인 반복이 빠르고 네이티브 네비게이션과의 통합 비용도 웹뷰보다 낮음. 센디는 RN 브라운필드 구조가 이미 갖춰져 있었음 |
RN을 택한 실질적 이유는 두 가지였다.
- 온보딩 화면은 디자인 반복 빈도가 높고 실패 시 롤백 비용이 싸다 — 핵심 결제나 인증이 아니라 진입 직후 단 한 번 보이는 화면
- 센디는 이미 RN 브라운필드 구조가 준비되어 있어 첫 프로덕트 소비자로 운송 플랜 온보딩을 선택하면 RN TF의 성과 사례로도 쓸 수 있었다
문제 해결 1단계: RN으로 온보딩 화면 구현
네이티브 엔트리와 RN 컴포넌트 경계
네이티브 측에서는 RN 화면을 띄우는 ViewController 하나만 있으면 된다.
final class RNTransportPlanOnboardingScreenViewController: RNBaseScreenViewController {
override var moduleName: String { "TransportPlanOnboarding" }
override var initialProps: [String: Any] { [:] }
}46줄 수준의 VC가 전부. 화면 내용(목적 선택 UI, 플랜별 설명, 가이드)은 모두 RN 쪽 TransportPlanOnboardingScreen 컴포넌트가 렌더링한다.
// ReactNativeDelegate에 모듈 등록
func viewName(for moduleName: String) -> String? {
switch moduleName {
case "TransportPlanOnboarding": return "TransportPlanOnboardingScreen"
// ...
}
}실험 진입 조건: 신규 소셜 가입자 한정
Hackle에서 transport_plan_onboarding_experiment (key 55) 실험을 정의했다. A/B 분배를 받되 실험 모수를 신규 소셜 가입자로 한정했다. 이유는 두 가지였다.
- 기존 사용자는 이미 플랜 맥락을 알고 있어서 온보딩 노출이 방해 요소가 될 수 있음
- 신규 소셜 가입자는 가입 직후 진입하므로 퍼널이 깨끗하고 효과 측정이 용이
enum TransportPlanOnboardingExperiment: String, CaseIterable {
case A // 온보딩 노출
case B // 기존 플로우 (온보딩 없음)
}이벤트 로깅: RN과 네이티브 이벤트 일원화
RN 쪽에서 온보딩 플로우 내 각 스텝을 로깅한다.
// RN
void Analytics.trackEvent('plan_onboarding_step', { step });
void Analytics.trackEvent('plan_onboarding_complete', { ... });
void Analytics.trackEvent('business_plan_conversion', { ... });네이티브 쪽에서는 실험 variant 노출과 콘텐츠 타입 이벤트를 로깅한다.
// Native
sendTransportPlanOnboardingExperimentEvent(variant: .A)
sendOnboardingContentTypeEvent(type: .PERSONAL_MOVE)중요한 설계 결정은 RN 이벤트가 네이티브 브릿지(NativeSendy.logEvent)를 거쳐 단일 Mixpanel 인스턴스로 전송되게 한 것이다. RN과 네이티브가 각자 Mixpanel SDK를 초기화하면 같은 사용자가 다른 distinct_id로 분리 집계될 수 있다. 온보딩 → 오더폼 퍼널이 끊기면 실험 결과 해석 자체가 불가능해진다.
완료 후 오더폼 바로 이동 딥링크
온보딩 완료 후 메인 홈으로 떨어뜨리는 대신 바로 오더폼으로 이동하도록 설계했다. 온보딩에서 선택한 운송 목적을 prefill 상태로 넣어 사용자가 "선택한 목적 → 바로 입력"의 흐름을 끊기지 않게 했다.
fix: 메인 화면 이동 딥링크 fallback 온보딩 타입 제거
feat: 홈화면에서 운송플랜 온보딩을 끝내면, 오더폼 페이지로 이동한다
feat: 메인 홈 화면 딥링크를 추가하고 사용자가 선택한 운송 목적을 로컬에 저장한다
개선 효과 1단계
실험 기간 동안의 핵심 지표는 채울 빈칸(Mixpanel 기준):
- 실험 모수: variant A · B 각 유저 수 (
transport_plan_onboarding_experimentproperties.variant) - 온보딩 진입률:
plan_onboarding_step(step 1) / 신규 가입 완료 이벤트 - 온보딩 완료율:
plan_onboarding_complete/plan_onboarding_step(step 1) - 스탠다드→비즈니스 플랜 전환율:
business_plan_conversion/plan_onboarding_complete - 온보딩 후 오더 시작율 (7일 윈도우):
plan_onboarding_complete→order_form_launch
결과적으로 A(온보딩 노출)가 확정됐다. 주요 근거는 두 가지였다. 첫째, 온보딩 완료자의 플랜 전환율이 유의미하게 높았다. 둘째, 온보딩 완료 후 오더 시작까지 이어지는 비율이 B(온보딩 미노출)보다 높았다. 즉 온보딩이 플랜 전환 경로를 만드는 효과 + 오더 시작까지의 흐름을 오히려 매끄럽게 만드는 부수 효과를 모두 냈다.
문제 해결 2단계: 네이티브 온보딩 제거, RN으로 확정
실험 종료 후 결정
A 확정 후 기존 네이티브 온보딩 경로 자체를 제거했다. A/B가 끝난 뒤 네이티브 화면을 실험 variant 분기로만 남겨두는 건 레거시가 되기 쉽고, RN 온보딩만 유지하는 것이 운영 복잡도를 낮췄다.
enum TransportPlanOnboardingExperiment: String, CaseIterable {
case A // A 확정 (RN 온보딩)
/// A: 네이티브 온보딩 표시하지 않음 (실험 종료, A 확정)
var shouldPresentNativeOnboarding: Bool { false }
var shouldShowContentSheet: Bool { false }
}코드 주석에 실험 종료 맥락을 남긴 것이 의도적이다. 실험이 끝난 뒤에도 "왜 이 enum이 하나만 남아 있는가"를 모르면 나중에 불필요한 정리가 일어날 수 있다.
운송 리뷰로 확장
RN 온보딩이 안정화된 뒤 같은 패턴으로 운송 리뷰 화면도 RN으로 교체했다.
네이티브 운송 리뷰 화면: 기존 82줄
↓
RNTransportReviewScreenViewController: 30줄로 축소
+ RN 측 TransportReviewScreen 컴포넌트
운송 리뷰는 온보딩과 달리 네이티브 네비게이션 스택 내부에서 push로 들어가는 화면이라 엔트리가 다르다. RN ↔ 네이티브 네비게이션 인터롭 설계를 여기서 확장했다 (RN 브라운필드 글의 "남은 과제"와 직결).
개선 효과 2단계
- 네이티브 온보딩 경로 제거: 변종(variant) 분기 코드와 네이티브 UI를 제거하면서 유지보수 표면이 줄었다
- RN 도입의 프로덕트 임팩트 증명: "기술 스택 도입"이 아니라 "플랜 전환율 개선"이라는 실제 비즈니스 수치로 RN TF의 기여가 기록됐다
- 두 번째 소비자(운송 리뷰) 확장: 첫 사례가 성공한 뒤 두 번째 화면으로 확장할 때 네이티브 측 진입 VC가 30줄 수준으로 경량화됐다. 구조 재사용 가능성 확인
- 이벤트 브릿지 일원화의 효과: 온보딩·리뷰 플로우 모두에서 Mixpanel 퍼널이 끊기지 않아 분석 결과 신뢰도가 유지됨
단점과 예상치 못한 변수
1. RN 첫 진입 시 JS 번들 로딩 지연.
신규 가입 직후 RN 온보딩으로 진입할 때 JS 번들 초기 로드가 추가 비용이다. 프리로드를 일부 적용했지만 체감 지연이 0은 아니다. 네이티브 온보딩이면 즉시 렌더링됐을 것. 이 trade-off는 감수했다 — RN의 이터레이션 속도 이점이 첫 진입 지연을 상쇄한다고 판단.
2. 실험 모수 한정의 함정.
신규 소셜 가입자 한정은 깨끗한 퍼널을 줬지만, 기존 사용자군에서 온보딩 효과가 어떨지는 측정 범위 밖으로 남았다. 확정 후 기존 사용자에게도 적용할지는 후속 실험이 필요한 영역.
3. 이벤트 브릿지가 SPOF가 될 수 있다.
모든 RN 이벤트가 NativeSendy.logEvent 하나로 모이는 구조라, 이 브릿지가 실패하면 RN 쪽 로깅 전체가 사라진다. 브릿지 레벨의 fallback(로컬 큐, 재시도)을 어느 수준까지 둘지 설계 숙제가 남았다.
4. 네이티브 코드 주석 유지보수.
실험 종료 후 TransportPlanOnboardingExperiment enum에 A만 남는 이상한 형태가 됐다. "왜 하나만 있는가"를 코드 주석으로 설명했지만, 시간이 지나면 주석이 휘발되기 쉽다. 실험 종료 시 enum 자체를 제거하고 단순 bool/호출로 대체하는 것이 장기적으로 나을 수 있다.
이후 방향성
- 기존 사용자군으로 확장: 신규 소셜 가입자에 국한됐던 온보딩을 기존 사용자군에도 제안할지 후속 실험으로 검증 예정. 단, 기존 사용자는 맥락이 다르므로 "설명형"이 아닌 "리마인드형" 버전이 필요할 수 있음
- RN 네비게이션 인터롭 정리: 운송 리뷰 확장에서 드러난 네이티브↔RN 네비게이션 충돌을 구조적으로 정리하는 작업이 예정 (RN 브라운필드 글의 "남은 과제")
- 이벤트 브릿지 안정화: 로컬 큐 + 재시도 + 드롭 메트릭 추가해 SDK 소비자 관점에서 로깅 신뢰도 보강
- 세 번째 소비자 후보: 기획·디자인 반복 빈도가 높으면서 롤백 비용이 싼 화면을 계속 발굴 중
이 이니셔티브의 진짜 성과는 기술적 통합 자체가 아니라 "RN을 통해 프로덕트 임팩트를 만들 수 있다"는 증명이었다. 기술 선택은 수단이고 목적은 플랜 전환율. 두 축을 분리해서 기록하는 것이 이후 다른 기술 도입을 설득할 때도 유효했다.