6분 읽기
웹을 Expo Web으로 바꾸지 않고, Expo + React Native를 별도 앱으로 선택한 이유

이 글은 eolma 앱 아키텍처 문서를 바탕으로 정리한 초안입니다. 구현 진행에 따라 저장소·배포 경계는 달라질 수 있으며, 이 글은 당시의 의사결정 맥락을 기록합니다.

모바일 앱을 추가할 때 가장 먼저 떠오른 선택지는 “기존 웹을 최대한 재사용할 수 있는가”였다. 하지만 재사용의 범위를 잘못 정하면 데스크톱 웹과 모바일 앱 모두에서 불편한 결과가 나온다.

eolma는 개인 가계부 서비스다. 데스크톱에서는 밀도 높은 표, CSV/XLSX 가져오기·내보내기, 브라우저 기반 파일 흐름이 중요하다. 모바일에서는 네이티브 내비게이션, 안전한 토큰 저장, 플랫폼 로그인, 터치에 맞는 입력 흐름이 중요하다. 두 표면이 같은 API를 쓴다고 해서 UI까지 같은 방식으로 만들어야 하는 것은 아니었다.

문제를 “한 코드베이스”가 아니라 “두 사용자 경험”으로 정의했다

당시 비교한 방향은 다음과 같았다.

방향장점eolma에서의 한계
기존 React 웹을 반응형으로 확장하나의 UI 코드와 배포 흐름을 유지네이티브 인증·보안 저장소·모바일 제스처에 맞는 경험을 별도로 해결해야 한다
기존 웹을 Expo Web으로 교체단일 React Native 계열 UI를 지향할 수 있다데스크톱의 표·SEO·파일 워크플로를 새 렌더링 모델에 맞춰 다시 검증해야 한다
웹과 네이티브 UI를 분리하고 비UI 코드만 공유각 플랫폼에 맞는 UX를 설계할 수 있다두 UI를 유지해야 하고 공유 경계를 의도적으로 관리해야 한다

선택은 세 번째였다. apps/web은 React + Vite 기반의 데스크톱 웹으로 유지하고, apps/mobile은 Expo + React Native로 Android·iOS를 담당한다. Expo Web으로 기존 웹을 대체하지 않는 이유는 “기술 하나로 통일”하는 것보다 이미 검증된 데스크톱 흐름을 보존하는 편이 더 중요했기 때문이다.

flowchart TB
  API["eolma API"]
  WEB["apps/web\nReact + Vite\n데스크톱 웹"]
  MOBILE["apps/mobile\nExpo + React Native\nAndroid · iOS"]
  CONTRACTS["packages/contracts\nDTO · API 계약"]
  CORE["packages/api-core\n인증 · API 공통부"]
  DOMAIN["packages/domain\n순수 도메인 규칙"]
  QUERY["packages/query\nQuery key · option"]
  TOKENS["packages/design-tokens\n시각 토큰"]

  WEB --> API
  MOBILE --> API
  WEB --> CONTRACTS
  WEB --> CORE
  WEB --> DOMAIN
  WEB --> QUERY
  WEB --> TOKENS
  MOBILE --> CONTRACTS
  MOBILE --> CORE
  MOBILE --> DOMAIN
  MOBILE --> QUERY
  MOBILE --> TOKENS

무엇을 공유하고, 무엇을 공유하지 않았나

공유 여부는 “코드가 비슷해 보이는가”가 아니라 플랫폼 종속성이 있는가로 판단했다.

공유 대상이유
API 응답·DTO 계약웹과 모바일이 같은 서버 계약을 사용해야 한다
인증·HTTP 같은 API 공통부플랫폼별 저장소 adapter를 경계로 두면 요청 흐름을 재사용할 수 있다
가계부 도메인 타입·순수 규칙UI나 런타임에 의존하지 않는 로직이다
TanStack Query key와 옵션같은 데이터가 플랫폼마다 다른 캐시 키로 분리되는 일을 줄인다
색상·간격 등 디자인 토큰브랜드와 의미론적 값은 공유하되, 실제 컴포넌트 구현은 각 플랫폼에 맞춘다

반대로 화면 컴포넌트, 레이아웃, 네비게이션은 공유하지 않았다. DOM의 테이블·폼과 React Native의 View·Text·네이티브 navigation은 표면적으로 비슷해도 접근성, 입력, 레이아웃 제약이 다르다. 공통 UI를 억지로 만들면 공유층이 두 플랫폼의 예외를 모두 떠안는 또 하나의 복잡한 제품이 된다.

모바일의 첫 범위를 의도적으로 작게 잡았다

“모든 기능을 모바일에 넣는다”가 첫 목표는 아니었다. 사용 빈도가 높은 가계부·자산·목표·계정 관리는 앱에 포함하되, CSV/XLSX 가져오기·내보내기와 브라우저 확장 프로그램 연동은 데스크톱 웹에 남겼다.

이 경계에는 두 가지 효과가 있다.

  1. 모바일에서 파일 처리 경험을 무리하게 복제하지 않는다.
  2. 사용자는 파일 기반 작업이 필요할 때 데스크톱 워크플로를 안내받고, 모바일은 일상적인 조회·입력·관리 흐름에 집중한다.

또한 첫 모바일 앱은 온라인 전용으로 두었다. 가계부와 자산 데이터의 로컬 영속 캐시를 먼저 만들면 동기화 충돌, 삭제 요청, 암호화, 오프라인 편집 규칙을 함께 설계해야 한다. 서버를 원본으로 두고 앱 프로세스 종료·로그아웃 시 데이터를 유지하지 않는 선택은 기능을 포기한 것이 아니라, 동기화 정책을 검증하기 전의 명확한 안전 경계다.

인증 저장소는 공유 코드의 경계를 시험한다

웹과 모바일 모두 같은 계정 모델과 API를 사용하지만, 토큰을 저장하는 방식은 같을 수 없다. 웹은 웹용 인증 저장소 adapter 뒤에 구현하고, 모바일은 암호화된 secure storage를 사용한다. 이 차이는 공통 API 계층이 “저장소가 어디에 있는지”를 직접 알지 않도록 만들었다.

플랫폼의 보안 기능을 활용하려면 네이티브 런타임이 필요하다. Expo는 Android·iOS를 포함한 여러 플랫폼을 위한 도구와 API를 제공하고, 그 위에서 필요한 네이티브 기능을 선택적으로 사용할 수 있다. 자세한 시작점은 Expo 공식 문서에서 확인할 수 있다.

릴리스는 하나의 버전 번호로 끝나지 않는다

웹과 백엔드는 컨테이너 태그를 맞춰 배포할 수 있다. 하지만 Android와 iOS는 스토어 심사와 사용자의 업데이트 시점이 달라서 같은 순간에 교체되지 않는다. 그래서 모바일의 versionCodebuildNumber은 별도로 증가시키고, 서버 API는 지원 중인 설치 버전과 호환되어야 한다.

이는 monorepo를 만들었다고 해서 출시가 단일화되는 것은 아니라는 뜻이다. monorepo가 해결하는 것은 계약·순수 로직·검증 명령의 공유이지, 플랫폼별 배포 속도와 사용자 업데이트 속도까지 같게 만드는 일은 아니다.

이 결정에서 얻은 기준

이번 선택을 한 문장으로 요약하면 이렇다.

공유는 제품의 규칙과 계약까지, UI는 각 플랫폼의 사용자 경험까지.

웹과 모바일을 전부 따로 만들자는 이야기도, 모든 것을 하나의 컴포넌트로 만들자는 이야기도 아니다. 서버 계약과 도메인 규칙은 하나여야 하지만, 그 규칙을 보여 주고 입력받는 방법은 플랫폼에 맞아야 한다. eolma의 구조는 그 경계를 코드로 강제하려는 시도다.

참고 자료