Situation

이마트앱 프론트엔드에서 네이티브 앱과의 통신(79개 인터페이스)을 담당하는 코드가 utils/native.ts 단일 파일에 집중되어 있었습니다. 워크이마트, 웰컴체크인 등 네이티브 연동 서비스가 늘어나면서 유지보수 한계에 도달했습니다.

Problem

이 파일에는 scheme 실행, 콜백 등록, 스트림 수신, 타입 정의가 모두 혼재되어 있었습니다. Pinia store에서 native 유틸을 import하고, native에서도 store를 import하면서 순환 의존이 발생했습니다.

콜백 등록이 AppCallbackMap에 하드코딩되어 있어 새 인터페이스를 추가할 때마다 여러 파일을 수정해야 했습니다. 타입은 3개 파일(emart.d.ts, nativeCallbacks.d.ts, nativeInterface.d.ts)에 산재해 있어 원하는 타입을 찾기 어려웠습니다. 또한 콜백 응답에 대한 타임아웃이 구현되어 있지 않아, 네이티브가 응답하지 않으면 무한 대기 상태에 빠지는 문제도 있었습니다.

Approach

AS-IS

장점

  • 단일 파일이라 전체 코드 한눈에 파악 가능

고려할 점

  • 순환 의존
  • 타입 산재(3파일)
  • 인터페이스 추가 시 다수 파일 수정
  • 타임아웃 없음

TO-BE

장점

  • 역할별 모듈 분리
  • 타입 단일 파일
  • 3패턴 API 명시화
  • 자동 정리/타임아웃 내장

고려할 점

  • 기존 20+파일 import 경로 변경 필요 (기계적 마이그레이션)

핵심 아이디어는 네이티브 통신을 3가지 패턴으로 명시화하는 것이었습니다.

  1. request (단발 응답)
  2. subscribe (스트림)
  3. send (fire-and-forget)

기존 소비자(20+파일)는 import 경로만 변경하면 되도록 설계하여 점진적 마이그레이션이 가능하게 했습니다.

Implementation

모듈 구조는 다음과 같이 역할별로 분리했습니다.

composables/native/
  • nativeBridge.ts
    • request/subscribe/send 3패턴 API (141 lines)
  • executeScheme.ts
    • 앱스킴 실행 (store 의존은 잔존)
  • helpers.ts
    • 유니크 콜백명 생성, 타임아웃 관리
  • useNativeScope.ts
    • Vue lifecycle 자동 연동 (onUnmounted 정리)

타입 역시 단일 파일로 통합했습니다.

types (AS-IS)

3파일 산재:

  • emart.d.ts
  • nativeCallbacks.d.ts
  • nativeInterface.d.ts → 찾기 어렵고, 중복 선언 발생

types (TO-BE)

단일 파일 통합:

  • types/native.d.ts (160 lines)
  • NativeSchemeName 리터럴 유니온 → 타입 추가만으로 자동 완성 동작

소비자 전환은 16개 컴포넌트/스토어 파일의 import 경로를 변경하는 방식으로 진행했습니다. 검증을 위해 nativeBridge.test.ts(100 lines)로 단위 테스트를 작성했습니다.

Result

🧩

역할별 모듈 분리

scheme 실행 / 콜백 / 스트림 / 타입 경계 확립

⚡

인터페이스 추가 간소화

native.d.ts 타입만 추가하면 자동 완성 동작

⏱️

타임아웃 내장

네이티브 미응답 시 graceful 에러 처리

📉

코드량 70% 축소

useNativeStream으로 워크이마트 걸음수 구독 간소화

리팩토링 직후 웰컴체크인 개발에서도 동일 API를 즉시 활용할 수 있었습니다.

미달성: 순환 의존은 끊지 못했다. 모듈 분리 첫 커밋부터 executeScheme.ts가 store를 import하며, 19189의 authGuard 추가로 사이클이 하나 더 늘었다. 호출이 전부 함수 본문 안이라 런타임 영향은 없다. 상세는 emart-native-bridge 참조.

Learnings

  • utils/(순수함수) ↔ composables/(상태) 경계만으로는 순환 의존이 끊기지 않습니다 — 실제로 남았습니다. 끊으려면 store→native 참조 방향 자체를 단방향으로 설계해야 합니다
  • "콜백 레지스트리"를 string key 범용으로 만들면 새 기능 추가가 타입 선언 하나로 끝납니다
  • 20+파일 마이그레이션도 import 경로만 바꾸는 설계면 기계적으로 처리 가능합니다 (리스크 낮음)
  • 리팩토링은 새 서비스 개발과 동시에 하면 "쓰면서 검증"이 가능합니다 — 워크이마트가 첫 소비자 역할을 했습니다