물류 현장 작업자들이 PDA로 사용하는 WMS 모바일 웹을, 브라우저 접속 방식에서 설치형 하이브리드 앱으로 전환한 경험을 정리했다. 기술적으로 아주 복잡한 작업은 아니었지만, “서비스를 제공하는 방식 자체”를 바꾼 작업이라 과정과 판단 기준을 기록해두려고 한다.


1. 왜 앱이 필요했나

접속 경험: 완성된 서비스처럼 느껴지지 않았다

이전에는 작업자가 브라우저를 켜고, 주소창이나 탭을 거쳐 WMS에 들어가야 했다. 지금은 홈 화면의 앱 아이콘만 누르면 바로 WMS로 진입한다.

모바일 WMS를 개발하면서 PDA로 직접 테스트를 많이 했는데, 매번 브라우저를 열어 접속하는 과정 자체가 꽤 번거로웠다. 매일 쓰는 업무 도구를 이렇게 열어야 한다면, 작업자 입장에서는 완성된 서비스라기보다 임시로 쓰는 웹페이지처럼 느껴질 수 있겠다는 생각이 들었다.

이게 내 느낌만은 아니었다. 실제로 현장 초기 세팅 때 “모바일은 플레이스토어에서 받으면 되나요?”라는 문의가 있었다고 한다. 사용자들은 이미 앱을 기대하고 있었던 것이다.

화면: 좁은 PDA 화면을 브라우저 UI가 잠식했다

같은 화면인데, 브라우저로 접속하면 위에는 툴바, 아래에는 Android 하단 바가 붙는다. 가뜩이나 작은 PDA 화면에서 이 바들이 공간을 차지하다 보니 한 번에 보이는 정보량이 눈에 띄게 줄었다. 앱으로 전환한 뒤에는 위아래 바 없이 전체화면으로 뜨기 때문에, 그 공간이 모두 작업 영역이 된다.

여기에 부수적으로, 촬영할 때마다 카메라 권한을 다시 묻는 불편도 있었다. 브라우저 위에서는 해결할 방법이 없는 문제였다.

정리하면 목표는 두 가지였다.

  • 브라우저 기반이라는 걸 사용자가 의식하지 않는, 완결된 서비스 경험
  • 툴바와 하단 바가 없는 넓은 작업 화면

2. 검토한 대안

옵션 장점 한계 결론
PWA 가장 가볍고 기존 웹 기술만으로 도입 가능 브라우저 엔진 위라 전체화면·권한 등 OS 수준 제어가 제한됨 배제
React Native WebView 팀의 JS/TS 역량 재사용, 큰 생태계, iOS 확장성 앱 용량·런타임 오버헤드가 Capacitor보다 큼 채택
Capacitor WebView 래핑에 최적화, 안정적인 네이티브 플러그인 생태계가 작고 벤더 SDK 연동은 결국 직접 구현 필요 배제
Kotlin 네이티브 셸 가장 가볍고 제어권이 큼 팀의 Android 네이티브 경험 부족, iOS 확장성 없음 배제

PWA를 먼저 검토했다. 가장 가볍고 구현도 간단하지만, 결국 브라우저 위에서 동작하기 때문에 전체화면, 하단 바, 뒤로가기, 카메라 권한 같은 OS 레벨 동작을 웹에서 제어하는 데 한계가 있었다. 실기기에서도 문제가 이어졌고, 독립된 앱으로 제공하려는 목적과 맞지 않아 제외했다.

나머지 세 가지(Kotlin, Capacitor, React Native)는 모두 앱 안에 WebView를 띄우는 방식이고, 차이는 그 껍데기를 무엇으로 만드느냐였다.

  • Kotlin 네이티브는 가장 가볍지만, 프론트엔드 팀이 별도의 언어와 Android 개발 환경을 익혀야 해서 유지보수 부담이 컸다.
  • Capacitor와 React Native는 둘 다 JS/TypeScript를 쓰고 iOS로 확장할 수 있다는 점은 같았다.
  • RN은 앱 용량과 런타임 오버헤드가 더 큰 대신, 네이티브 모듈 생태계가 가장 넓고 Expo라는 빌드·배포 도구를 함께 쓸 수 있었다.

앱 쉘이 맡는 역할이 크지 않아 오버헤드는 감수할 만하다고 봤고, 확장성과 운영 편의를 우선해 RN을 선택했다.

특히 Expo의 EAS Build 덕분에 로컬 Xcode·Android Studio 없이 클라우드에서 환경별로 빌드되도록 구성할 수 있었다. 별도 환경 세팅이나 온보딩 비용 없이 팀원 누구나 유지보수할 수 있는 구조가 됐다.

판단 기준: 현장 기기에서 검증되는가, 그리고 작은 팀이 감당할 복잡도인가.
검증은 실제 운영 중인 산업용 PDA(Android 13)로 진행했다.


3. 구현 범위: 앱쉘은 얇게

구현 범위는 하나의 원칙으로 정했다.

화면과 업무 판단은 전부 웹에 두고, 웹이 할 수 없는 것만 앱이 맡는다.

그래서 웹은 거의 바꾸지 않았다. 앱은 지금 쓰는 모바일 웹을 그대로 띄우기 때문에, 기존처럼 웹만 배포하면 앱 재배포 없이도 항상 최신 화면이 반영된다. 웹에서 바뀐 부분은 실행 환경을 구분해 브라우저에서는 웹캠을, 앱에서는 네이티브 카메라를 쓰도록 분기한 정도다.

앱 쪽에서 만든 것은 다음과 같다.

작업 내용 효과
전체화면 WebView 앱쉘 배포된 웹 URL을 그대로 로드, 상태바 숨김·안전영역 처리 웹 콘텐츠와 배포 흐름은 그대로
환경별 빌드 프로필 dev·stg·prod마다 URL·패키지명·앱 이름 분리, EAS 클라우드 빌드, versionCode 자동 증가 한 기기에 세 환경 앱을 나란히 설치
진입 경로 기능 플래그 앱 시작 시 플래그를 한 번 조회해 ON이면 신규 웹, OFF·실패·3초 초과면 기존 웹 앱 재배포 없이 웹 이관 시점 전환
웹 ↔ 네이티브 브릿지 메시지 4종만 정의 (카메라 열기·닫기, 업데이트 다운로드, 설치 실행) 판단은 웹이, 네이티브는 실행만
자체 업데이트 설치 APK를 백그라운드로 받아두고 OS 설치 인텐트 실행 스토어 없이 업데이트 전파
Android 뒤로가기 카메라 열림 → 닫기, 웹 히스토리 → 뒤로, 더 없으면 앱 종료 배포 후 발견해 사전 테스트 범위에 추가

브릿지 메시지를 4종으로 한정한 게 개인적으로 가장 잘한 결정이라고 생각한다. 네이티브가 판단을 갖기 시작하면 웹과 앱 두 곳에 로직이 흩어지고, 그때부터는 웹 배포만으로 해결되지 않는 문제가 생기기 때문이다.

바코드 스캐너는 손대지 않았다. 기존에 스캔 입력을 키보드 입력(HID)으로 받아 처리하고 있어 WebView에서도 그대로 동작했고, 업무 흐름에 직접 영향을 주는 부분이라 굳이 바꿀 이유보다 리스크가 더 컸다.


4. 배포 방식: 스토어 대신 QR

B2B 물류 서비스라 앱을 누구나 받을 수 있게 플레이스토어에 올릴 필요가 없었고, 오히려 계약한 창고에만 노출되는 편이 맞다고 판단했다. 그래서 기존 데스크탑 WMS에 로그인하면 다운로드용 QR을 띄울 수 있게 하고, 현장에서는 PDA로 이 QR을 스캔해 설치하는 방식을 택했다.

  1. 웹 (로그인 필요): 로그인한 사용자가 계정 메뉴에서 QR을 발급한다. 10분 뒤 만료되는 1회용 토큰이 생성된다.
  2. PDA: 내장 스캐너로 QR을 스캔하면 다운로드 버튼만 있는 공개 페이지가 열린다. PDA에서는 따로 로그인할 필요가 없다.
  3. 서버: 버튼을 누르는 시점에 토큰이 서버로 전달되고, 서버는 만료·재사용·발급 한도를 검증한 뒤 토큰을 사용 처리한다.
  4. S3: 서버가 5분간 유효한 Presigned URL로 302 리다이렉트하고, 기기는 비공개 버킷에서 파일을 직접 내려받는다.

서버가 APK 파일 전송을 중계하지 않기 때문에 부하 부담이 없고, 발급 API와 이력 테이블을 별도로 두어 누가 언제 QR을 발급했는지 추적할 수 있도록 했다.

왜 Presigned URL을 QR에 바로 넣지 않았나

방식 QR에 담기는 값 한계 결정
고정 URL · 퍼블릭 배포 APK 주소 URL을 아는 누구나 다운로드, 로그인 강제 불가 배제
Presigned URL을 QR에 직접 1,608자 · 133×133 모듈 문자열이 너무 길어 QR이 촘촘해지고, 인식 가능한 최소 출력 크기가 커져 스캔이 잘 안 됨 배제
토큰 URL + 다운로드 페이지 약 90자 · 33×33 모듈 발급 API·이력 테이블 신설 (BE 협업) 채택

처음에는 Presigned URL을 그대로 QR에 넣는 게 가장 간단해 보였다. 그런데 서명 파라미터까지 포함하면 URL이 1,600자를 넘어가서 QR이 133×133 모듈까지 촘촘해졌고, PDA 스캐너가 제대로 인식하지 못했다. 짧은 토큰 URL을 QR에 담고 실제 Presigned URL은 버튼을 누르는 순간 발급하는 구조로 바꾸면서 QR이 33×33 모듈로 줄었다.

판단 기준: 유출·오남용 피해가 제한되는가.
토큰은 10분 만료 · 1회용 · 사용자당 10분에 5회 발급 한도를 두어, QR이 유출돼도 만료로 자동 무효화되도록 했다.

실제 설치 과정은 이렇다.
빠른 설정의 QR 스캔으로 코드를 읽으면 다운로드 페이지가 열리고, 버튼을 누르면 다운로드와 설치가 이어진다.

06_설치_영상.mp4
0.82MB

 

다운로드 페이지는 버튼 하나만 두었고, 다운로드가 시작되면 브라우저 알림에서 진행 상황을 확인하도록 안내한다. 토큰이 만료됐다면 페이지 안에서 바로 안내해준다.


5. 자동 업데이트

웹 변경은 배포만 하면 바로 반영되지만, 앱 쉘이 바뀌면 앱 자체를 업데이트해야 한다. 이때마다 QR로 다시 설치하게 하는 대신, 앱이 스스로 새 버전을 받아오도록 구현했다.

  1. 로그인하면 버전을 비교한다. 앱 쉘은 WebView 진입 URL의 쿼리 파라미터에 자기 versionCode를 담아 보내고, 웹이 이를 최신 빌드 번호와 비교한다.
  2. 새 빌드면 조용히 내려받는다. 웹이 브릿지 메시지로 앱에 알리면, 네이티브가 1회용 토큰 URL로 APK를 백그라운드에서 미리 받아둔다.
  3. 준비되면 모달이 뜬다. 사용자가 찾아갈 메뉴는 없고, “지금 설치” 또는 “나중에”만 고르면 된다.
  4. OS 설치 확인 한 번. 설치가 끝나고 앱을 다시 열면 새 버전이다. 설치 여부는 사용자가 정한다.
09_자동업데이트_영상.mp4
0.25MB

배포 파이프라인이 APK를 올릴 때 버전 파일도 함께 갱신하기 때문에, 현장에 따로 공지하지 않아도 다음 로그인에서 각 기기에 업데이트 알림이 자연스럽게 전달된다.


6. 결과

  Before After 효과
접속 브라우저를 거쳐 진입 앱 아이콘으로 바로 실행 완결된 서비스 경험, 교육·문의 감소
화면 툴바·하단 바에 잠식된 작업 영역 전체화면 작업 공간 한 화면에 더 많은 정보
업데이트 새 APK 공지 후 수동 재설치 다음 로그인에 모달로 자동 안내 버전 파편화 감소
촬영 매번 권한 재요청 최초 1회 허용 촬영 흐름 안정

마치며

처음에 정의한 두 가지 문제는 홈 화면 아이콘으로 바로 접속하고, 전체화면으로 작업 영역을 넓히는 것으로 해결했다. 여기에 더해 계약 창고에만 앱을 배포하는 체계를 갖췄고, 별도 공지 없이도 앱이 스스로 업데이트되는 구조를 만들었다.

돌아보면 앱 쉘을 씌우는 작업 자체는 복잡하지 않았다. 그보다는 PDA로 직접 테스트하면서 느낀 불편함을 그냥 넘기지 않고, 크지 않은 공수로 서비스를 제공하는 방식 자체를 바꾸자고 제안해 실제 사용자 경험을 개선했다는 점에서 개인적으로 뿌듯했던 작업이다.