Sik.limited Logo

웹뷰 앱은 포장지가 아니다: SvelteKit/Capacitor가 깨지는 순간들

SvelteKit과 Capacitor로 앱을 만들 때 진짜 문제는 화면 구현보다 브라우저와 네이티브 경계 조건을 해석하는 일입니다. 카메라, 푸시, 로컬 저장소, YouTube iframe, iOS 정책까지 실무자가 먼저 확인해야 할 기준을 정리했습니다.

· 9분 읽기

웹뷰 앱은 포장지가 아니다: SvelteKit/Capacitor가 깨지는 순간들

부제: 웹뷰 앱에서 카메라, 푸시, 로컬 저장소, YouTube iframe, iOS 정책까지

웹 개발자가 Capacitor로 앱을 만들 때 처음 드는 생각은 단순합니다. “SvelteKit으로 만든 웹을 빌드하고, Capacitor로 감싸면 iOS/Android 앱이 되는 거 아닌가?”

반은 맞고, 반은 틀립니다. 화면은 그렇게 올라옵니다. 라우팅도 되고, CSS도 먹고, API도 호출됩니다. 그런데 실제 앱으로 배포하려는 순간부터 문제의 종류가 달라져요.

카메라는 브라우저 API가 아니라 권한과 네이티브 플러그인의 문제가 됩니다. 저장소는 localStorage 하나로 끝나지 않습니다. YouTube iframe은 어느 날 Error 153을 내고, iOS의 WKWebView는 origin, referrer, local file, safe area를 전혀 다른 방식으로 다룹니다. 푸시는 APNs와 FCM을 통과해야 하고, App Store 심사는 “웹사이트를 앱으로 포장한 것”을 싫어합니다.

핵심은 이겁니다.

웹 기술로 앱을 만드는 건 “웹을 감싸는 것”이 아니라, 브라우저와 네이티브 사이의 경계 조건을 계속 해결하는 일이다.

이 글은 SvelteKit + Capacitor로 모바일 앱을 만들 때 웹 개발자가 실제로 부딪히는 문제를 정리한 글입니다. 채용 HR이 보기에는 “이 사람이 앱을 출시 가능한 수준으로 생각해봤구나” 정도의 신호가 되고, 프로덕트/엔지니어링 시니어가 보기에는 “웹뷰 앱의 리스크를 감으로만 말하지 않는구나” 정도의 글이 되면 좋겠습니다.


Capacitor 앱은 모바일 웹이 아니라, 로컬 origin을 가진 앱이다

Capacitor 앱은 웹 번들을 네이티브 앱 안의 WebView에서 실행합니다. 이 말만 들으면 “그냥 모바일 웹”처럼 보이지만, 실제 실행 환경은 다릅니다.

웹사이트는 보통 https://example.com에서 실행됩니다. 반면 Capacitor iOS 앱은 내부적으로 capacitor://localhost 같은 origin을 사용하고, Android는 기본적으로 http://localhost 계열의 로컬 서버를 사용합니다. Capacitor 설정에는 server.hostname, iosScheme, androidScheme 같은 항목이 있고, 이 값은 URL, 쿠키, iframe, OAuth, CORS, referrer 동작에 영향을 줍니다.

이 차이를 가볍게 보면 이후 문제가 이상하게 보입니다.

  • 웹에서는 되던 iframe이 앱에서만 깨집니다.
  • 브라우저에서는 유지되던 저장소가 WebView에서는 기대와 다르게 동작합니다.
  • OAuth redirect URI가 웹 도메인과 앱 origin 사이에서 어긋납니다.
  • 로컬 파일 경로를 <img><audio>에 넣었는데 WebView가 읽지 못합니다.

Capacitor의 장점은 웹 기술로 네이티브 기능을 호출할 수 있다는 점입니다. 하지만 그 장점은 “웹과 네이티브의 차이를 숨겨준다”는 뜻이 아닙니다. 오히려 차이가 나타나는 지점을 플러그인과 설정으로 다룰 수 있게 해준다는 쪽에 가깝습니다.

iOS WKWebView는 referrer와 origin에서 자주 발목을 잡는다

웹뷰 앱에서 가장 까다로운 환경은 여전히 iOS입니다. iOS 앱의 웹 콘텐츠는 WKWebView 위에서 돌아가고, 이 환경은 Safari와 비슷하지만 같지 않습니다.

특히 origin과 referrer가 민감합니다. 웹에서 외부 서비스를 iframe으로 붙이면, 서비스 제공자는 보통 “어디서 요청이 왔는지”를 확인합니다. 웹 도메인이 명확하면 판단이 쉽습니다. 그런데 Capacitor 앱은 웹 도메인이 아니라 앱 내부 scheme에서 실행됩니다. capacitor://localhost 같은 origin은 일반적인 웹사이트가 아닙니다.

이 차이는 다음 기능에서 바로 튀어나옵니다.

  • YouTube iframe, 결제 위젯, 지도, 광고 스크립트
  • OAuth나 SSO redirect
  • 쿠키 기반 세션
  • referrer 기반 접근 제한
  • SameSite, Secure, Content-Security-Policy 정책

문제는 이게 “코드 한 줄 실수”처럼 보인다는 점입니다. iframe URL도 맞고, 네트워크도 정상이고, 브라우저에서는 잘 됩니다. 그런데 앱에서만 빈 화면이 나오거나, 특정 provider가 요청을 거절합니다. 이때는 컴포넌트 문제가 아니라 실행 origin 문제부터 봐야 합니다.

YouTube iframe Error 153은 작은 버그가 아니라 경계 조건의 사례다

YouTube iframe에서 Error 153을 만난 적이 있습니다. 겉으로는 단순한 재생 오류처럼 보입니다. 하지만 실제 원인은 보통 referrer 또는 origin 정보가 기대와 다르게 전달되는 쪽에 가깝습니다.

YouTube IFrame Player API 문서에는 origin 파라미터가 보안 측면에서 권장된다고 나옵니다. 웹에서는 보통 배포 도메인을 넣으면 됩니다. 예를 들어 https://myapp.com입니다.

문제는 Capacitor 앱입니다. 앱 내부에서는 capacitor://localhost에서 페이지가 실행되는데, YouTube 입장에서는 이 값이 일반적인 웹 origin이 아닙니다. 또 어떤 WebView 환경에서는 referrer가 비어 있거나, provider가 기대하는 HTTP referrer와 다르게 처리됩니다.

이 상황에서 해볼 수 있는 조치는 대략 이렇습니다.

  1. iframe URL에 origin을 명시합니다.
  2. Capacitor의 server.hostname과 scheme 설정을 확인합니다.
  3. referrerpolicy를 무심코 no-referrer로 두지 않았는지 봅니다.
  4. YouTube embed를 직접 넣을지, 별도 웹 페이지를 열지, 네이티브 브라우저로 넘길지 판단합니다.
  5. 앱 내부 origin을 외부 provider가 받아들이지 않는다면 기능 설계를 바꿉니다.

여기서 중요한 건 “YouTube 에러를 고치는 법” 자체가 아닙니다. 외부 웹 서비스를 WebView 안에 넣는 순간, 서비스 제공자의 보안 정책과 앱 내부 origin이 충돌할 수 있다는 점입니다. 결제, 영상, 로그인, 지도도 같은 종류의 문제를 만들 수 있어요.

로컬 파일, 오디오, 자막, STT는 웹처럼 다루면 늦게 깨진다

웹 개발자는 파일을 URL로 생각하는 데 익숙합니다. 하지만 앱에서 로컬 파일은 URL 이전에 권한, 위치, lifecycle의 문제입니다.

예를 들어 사용자가 영상을 내려받고, 자막 파일을 저장하고, 오디오를 재생하고, STT를 돌리는 앱을 만든다고 해봅시다. 웹에서는 fetch, Blob, URL.createObjectURL, <audio>, <video> 조합으로 어느 정도 됩니다. 앱에서는 질문이 바뀝니다.

  • 파일을 어디에 저장할 것인가?
  • iOS에서 해당 디렉터리는 앱 업데이트 후에도 유지되는가?
  • Android 권한 정책은 버전별로 어떻게 다른가?
  • WebView가 로컬 파일 경로를 직접 읽을 수 있는가?
  • 파일 URI를 WebView에서 접근 가능한 URI로 변환해야 하는가?
  • STT는 브라우저 API로 할 것인가, 네이티브 플러그인이나 서버로 넘길 것인가?

Capacitor의 Filesystem 문서를 보면 파일 경로를 WebView에서 보여주기 위해 convertFileSrc 같은 변환이 필요할 수 있습니다. 이건 사소한 구현 디테일처럼 보이지만, 앱에서는 핵심입니다. 경로가 틀리면 이미지, 오디오, 자막이 모두 조용히 실패합니다.

STT도 마찬가지입니다. 브라우저의 Web Speech API는 플랫폼과 브라우저 지원이 일정하지 않습니다. 앱 품질을 요구한다면 네이티브 음성 인식, 서버 기반 STT, 오프라인 모델, 권한 안내, 실패 fallback을 같이 설계해야 합니다. “웹에서 됐으니 앱에서도 되겠지”는 이 영역에서 꽤 위험한 가정입니다.

Push, Safe Area, Storage는 앱의 기본기지만 웹의 기본기는 아니다

하이브리드 앱에서 푸시 알림은 대표적인 착각 포인트입니다. 웹에서는 Service Worker와 Web Push를 떠올리지만, iOS/Android 앱에서는 APNs, FCM, 네이티브 권한, 토큰 발급, 앱 lifecycle을 봐야 합니다. Capacitor Push Notifications 플러그인은 이 연결점을 만들어주지만, 제품 요구사항은 여전히 앱 관점으로 풀어야 합니다.

예를 들어 이런 질문이 생깁니다.

  • 권한 요청은 언제 띄울 것인가?
  • 토큰 갱신은 어디서 처리할 것인가?
  • 사용자가 알림을 눌렀을 때 어떤 화면으로 보낼 것인가?
  • 앱이 foreground일 때와 background일 때 처리는 어떻게 다를 것인가?
  • Android 13 이후 알림 권한은 어떻게 받을 것인가?

Safe Area도 비슷합니다. 웹에서는 env(safe-area-inset-top) 정도로 끝나는 것처럼 보이지만, 실제 앱에서는 상태바, 하단 홈 인디케이터, 키보드, 풀스크린 여부, 화면 회전까지 같이 봐야 합니다. 디자인 QA에서 “상단이 조금 잘린다”는 말이 나오면 CSS 문제가 아니라 네이티브 viewport 설정까지 봐야 할 수 있습니다.

Storage는 더 조심해야 합니다. 브라우저의 localStorage는 간단하지만 앱 데이터 저장소로는 약합니다. Capacitor Preferences는 iOS의 UserDefaults, Android의 SharedPreferences 계열 저장소에 값을 저장합니다. 민감한 데이터라면 또 다른 보안 저장소가 필요합니다. 대량 데이터, 오프라인 캐시, 검색 가능한 로컬 DB가 필요하면 SQLite 계열을 고려하는 게 낫습니다.

정리하면 이렇습니다.

  • 작은 설정값: Preferences
  • 세션성 웹 상태: 웹 storage, 단 삭제 가능성을 감안
  • 대량/관계형 로컬 데이터: SQLite 계열
  • 민감 정보: secure storage 계열
  • 파일: Filesystem + WebView 접근 변환

저장소를 하나로 뭉뚱그리면 나중에 마이그레이션이 어려워집니다. 앱은 한 번 배포되면 사용자의 기기 안에서 오래 살아남기 때문입니다.

SvelteKit 빌드는 SPA가 아니라 앱 번들 전략으로 봐야 한다

SvelteKit을 Capacitor에 붙일 때 자주 쓰는 선택지는 @sveltejs/adapter-static입니다. 앱 안에 정적 파일을 넣어야 하니까 자연스러운 선택입니다. 하지만 여기서도 함정이 있습니다.

SvelteKit은 기본적으로 서버 렌더링과 라우팅을 강하게 지원하는 프레임워크입니다. 그런데 Capacitor 앱에 넣는 순간, 서버가 없는 정적 환경에서 라우팅이 돌아가야 합니다. 그래서 prerender, fallback, client-side routing 전략을 명확히 해야 합니다.

대략 체크리스트는 이렇습니다.

  • 모든 페이지를 prerender할 수 있는가?
  • 로그인 이후 동적 라우트는 fallback으로 처리할 것인가?
  • API 호출은 상대경로가 아니라 배포 API origin을 명확히 바라보는가?
  • base 경로나 asset path가 앱 내부 경로에서 깨지지 않는가?
  • SSR에서만 가능한 코드를 클라이언트 번들에 넣고 있지 않은가?
  • dev server에서는 되는데 production static build에서 깨지는 라우트가 없는가?

SvelteKit은 좋은 선택입니다. 다만 Capacitor 앱에서는 “웹사이트 배포”가 아니라 “네이티브 앱에 들어갈 웹 런타임을 빌드한다”는 관점으로 봐야 합니다. 빌드 산출물이 앱의 일부가 되기 때문입니다.

성능 문제는 FPS보다 배터리와 메모리에서 먼저 드러난다

웹뷰 앱의 성능을 이야기할 때 흔히 FPS만 봅니다. 물론 중요합니다. 하지만 실제 사용성에서는 배터리, 메모리, 백그라운드 복귀, 스크롤 안정성이 더 빨리 문제로 드러납니다.

웹뷰는 브라우저 엔진 위에서 돌아갑니다. 여기에 네이티브 shell, 플러그인 bridge, 이미지 디코딩, 영상 재생, 네트워크, 저장소 I/O가 같이 붙습니다. 모바일 기기에서는 데스크톱 크롬에서 괜찮던 코드가 그대로 괜찮지 않을 수 있어요.

특히 이런 패턴은 위험합니다.

  • 긴 리스트를 가상화 없이 렌더링
  • 큰 이미지를 원본 그대로 WebView에 표시
  • WebView와 네이티브 사이 bridge 호출을 너무 자주 발생시킴
  • 오디오/영상 리소스를 화면 전환 후에도 해제하지 않음
  • 백그라운드 복귀 시 데이터를 중복 fetch
  • IndexedDB/localStorage에 큰 payload를 반복 저장

하이브리드 앱에서 좋은 성능은 “웹 최적화 + 앱 lifecycle 이해”의 합입니다. Svelte 컴포넌트 최적화만으로는 부족하고, 앱이 pause/resume될 때 무엇을 멈추고 무엇을 다시 시작할지 정해야 합니다.

iOS 정책은 기술 문제가 아니라 제품 포지셔닝 문제다

Apple App Review Guidelines에는 “웹사이트를 단순히 다시 포장한 앱”이나 충분한 기능이 없는 앱에 대한 기준이 있습니다. 여기서 중요한 건 Apple이 웹 기술을 싫어한다는 뜻이 아닙니다. 앱으로 제출한다면 앱다운 가치가 있어야 한다는 뜻입니다.

Capacitor 앱을 제출할 때는 다음 질문에 답할 수 있어야 합니다.

  • 왜 이 기능은 웹사이트가 아니라 앱이어야 하는가?
  • 앱 설치 후 사용자가 얻는 명확한 효용은 무엇인가?
  • 푸시, 오프라인, 카메라, 파일, 로컬 저장소 같은 앱 기능이 제품 가치와 연결되는가?
  • 단순 링크 모음이나 웹페이지 wrapper로 보이지 않는가?
  • 네이티브 UX 기본기, 권한 안내, 오류 상태가 충분히 갖춰졌는가?

이건 엔지니어링 이전에 제품 판단입니다. 좋은 Capacitor 앱은 “웹으로 만들었지만 앱처럼 느껴지는 제품”이지, “웹을 앱스토어에 넣은 제품”이 아닙니다.

언제 Capacitor가 좋고, 언제 Swift/네이티브가 나은가

Capacitor는 좋은 도구입니다. 특히 웹 개발 조직이 빠르게 모바일 앱을 출시해야 할 때 강합니다. 기존 웹 코드와 UI 자산을 재사용할 수 있고, SvelteKit 같은 프레임워크와도 잘 맞습니다. 관리자 도구, 콘텐츠 앱, 커뮤니티, 학습 앱, 미디어 소비 앱, 간단한 생산성 앱은 충분히 현실적인 후보입니다.

Capacitor가 특히 좋은 상황은 이렇습니다.

  • 웹 제품이 이미 있고 모바일 앱을 빠르게 검증해야 한다.
  • UI 대부분이 웹으로 충분하고, 네이티브 기능은 카메라/푸시/저장소 정도다.
  • iOS와 Android를 동시에 운영할 팀 여력이 제한적이다.
  • 앱의 핵심 경쟁력이 네이티브 UI 성능보다 콘텐츠, 워크플로우, 배포 속도에 있다.
  • 웹 프론트엔드 팀이 제품 의사결정까지 빠르게 밀고 가야 한다.

반대로 Swift/네이티브가 나은 경우도 명확합니다.

  • 카메라, 영상 편집, 실시간 오디오, AR, 센서처럼 네이티브 성능이 핵심이다.
  • 오프라인 데이터와 동기화가 앱의 중심이다.
  • 복잡한 백그라운드 작업이 많다.
  • 앱 UI가 플랫폼 네이티브 패턴에 깊게 의존한다.
  • 배터리, 메모리, 프레임 안정성이 제품의 핵심 품질이다.

이 판단은 도구 취향의 문제가 아닙니다. 제품의 병목이 어디에 있느냐의 문제입니다. 병목이 화면과 콘텐츠 운영이면 Capacitor가 빠를 수 있습니다. 병목이 플랫폼 성능과 네이티브 경험이면 Swift/네이티브가 더 정직합니다.

웹뷰 앱 개발자는 “프론트엔드 + 모바일 런타임”을 같이 봐야 한다

SvelteKit + Capacitor로 앱을 만든다는 건 React Native나 Swift를 모른다는 뜻이 아닙니다. 오히려 웹 기술을 기반으로 모바일 런타임의 제약을 다루겠다는 선택입니다.

그래서 좋은 하이브리드 앱 개발자는 컴포넌트만 잘 만드는 사람이 아닙니다. 다음을 같이 봅니다.

  • WebView origin과 외부 provider 정책
  • iOS/Android 권한 모델
  • 정적 빌드와 앱 번들 구조
  • 로컬 파일과 저장소 전략
  • 푸시 알림 lifecycle
  • safe area와 keyboard interaction
  • 성능, 배터리, 메모리
  • App Store/Play Store 심사 기준

웹뷰 앱은 쉬운 길처럼 보이지만, 출시 가능한 품질까지 가면 꽤 현실적인 엔지니어링이 됩니다. 그래서 이 분야의 경험은 프론트엔드 엔지니어에게 좋은 신호가 됩니다. 화면을 만드는 능력에서 끝나지 않고, 제품이 사용자의 기기 안에서 어떻게 살아야 하는지까지 생각했다는 뜻이니까요.

Capacitor/SvelteKit 앱을 제대로 만든다는 건 결국 이 문장으로 정리됩니다.

웹을 앱 안에 넣는 게 아니라, 앱이라는 환경에서 웹 런타임이 살아남도록 설계하는 일이다.

최신 글