← Writing

Capacitor로 PWA를 iOS 앱으로 출시하기

Plan Do!는 React로 만든 PWA였는데, 사용자들이 iOS 앱을 찾기 시작하면서 네이티브 출시를 고민하게 됐습니다.

선택지는 둘이었어요.

  • React Native로 다시 짜기 — 네이티브에 가까운 성능, 다만 코드베이스가 둘로 갈라짐
  • Capacitor로 기존 웹을 감싸기 — 한 코드베이스 유지, 네이티브 기능은 플러그인으로

1인 개발 환경에서 두 개의 코드베이스를 유지할 자신이 없어서 Capacitor로 갔습니다. 지금 시점에서 보면 잘한 결정이었어요.

첫 셋업

npm install @capacitor/core @capacitor/ios
npx cap init
npx cap add ios

명령어 자체는 단순한데, 실제로 App Store에 올라가는 빌드가 나오기까지 며칠 걸렸습니다. Capacitor 8.3 기준으로 진행했고, 플러그인은 필요한 것만 골라서 넣었어요.

  • @capacitor/push-notifications — APNs 토큰 발급
  • @capacitor/geolocation — 위치 기반 알림용
  • @capacitor/keyboard — iOS 키보드 높이 추적
  • @capacitor/app — 앱 백그라운드/포그라운드 전환 감지
  • @capacitor/browser — 외부 링크
  • @capacitor/haptics — 터치 피드백
  • @capacitor-community/apple-sign-in — Apple 로그인

플러그인을 늘릴수록 빌드 시간과 리뷰 리스크가 같이 올라가서, 안 쓰는 건 깔지 않으려고 했어요.

Safe-area와 키보드 — 가장 시간 많이 잡아먹은 부분

데스크탑 Safari에서 잘 보이던 화면이 iOS에서는 미묘하게 어긋났습니다. 노치 아래로 콘텐츠가 잘리거나, 키보드가 올라올 때 입력창이 가려지는 식이었어요.

해결은 두 군데에서 했습니다.

  • capacitor.config.json에서 contentInset: "never" + resize: "native"
  • CSS에서 env(safe-area-inset-*)를 일관되게 적용

특히 키보드 처리는 resize 옵션을 잘못 잡으면 viewport가 두 번 줄어드는 현상이 있어서 useKeyboardOffset 같은 훅으로 한 번에 컨트롤하게 했습니다.

푸시 알림에서 특정 화면으로 이동시키려면 Deep Link가 필요했어요. plando:// 스킴을 Info.plistCFBundleURLTypes에 잡아두고, React 쪽에서는 DeepLinkHandler 컴포넌트 하나에서 모든 진입을 처리합니다.

  • 워크스페이스 초대(/join)
  • 특정 todo로 바로 이동
  • 위젯에서 들어오는 진입

여기서 한 번 잘 깔아두니까 위젯에서 들어오든 푸시에서 들어오든 같은 코드가 처리하게 돼서 한참 후에 위젯 기능을 붙일 때 추가 작업이 거의 없었어요.

백그라운드 복귀 — 의외로 작은 디테일이 큰 차이

iOS는 앱이 백그라운드에서 한참 있다가 돌아오면, 화면은 그대로인데 서버 상태와 어긋난 상황이 흔히 생깁니다. todo가 다른 기기에서 바뀌었거나, 새 DM이 도착했거나.

Capacitor.App.addListener('appStateChange')에 훅을 걸어서 포그라운드 복귀 시점에 가벼운 재동기화를 트리거하게 했습니다. 사용자가 돌아왔을 때 자연스럽게 최신 상태를 보는 인상이 이게 결정합니다.

결정 — 단순함을 더 우선

빌드 워크플로우와 코드 구조에서 일관되게 적용한 원칙들:

  • WebView 전용 화면을 따로 두지 않기 — 같은 라우트를 그대로, Capacitor 환경만 분기 처리
  • 네이티브 플러그인은 최소화 — 카메라·푸시·위치·키보드 정도. 나머진 웹 표준
  • platformUtils.js 한 곳에 분기 모으기Capacitor.getPlatform() 호출이 코드에 흩어지지 않게

정리

같은 코드 한 줄이 웹과 iOS 양쪽에 영향을 주는 구조라 변경의 무게는 커졌습니다. 하지만 그만큼 한 사람이 양쪽을 동시에 끌고 갈 수 있게 되었어요.

App Store 심사는 한 번에 통과하진 않았고, 권한 설명 문구를 다듬는 과정에서 몇 번의 왕복이 있었습니다. 필요한 권한만, 왜 필요한지 명확히 적는 게 가장 효과적이었던 것 같아요.

Capacitor가 React Native만큼 빠르지는 않지만, 혼자 만드는 사람에게는 충분히 합리적인 선택이라는 게 지금까지의 결론입니다.