Capacitor 완벽 가이드 — 웹에서 네이티브 하이브리드 앱으로
이 글의 핵심
Capacitor는 빌드된 웹 자산을 네이티브 컨테이너에 싣고, JavaScript와 OS API 사이를 얇고 예측 가능한 브리지로 연결합니다. 이 글에서는 네이티브 브리지·플러그인 디스패치·WebView 수명 주기 같은 내부 동작, iOS/Android 차이, 프로덕션 배포 패턴까지 포함해 아키텍처, 프로젝트 구조, 공식·커스텀 플러그인, 푸시·백그라운드, 성능, Cordova 비교를 한 흐름으로 다룹니다.
이 글의 핵심
Capacitor는 Ionic 팀이 만든 크로스플랫폼 런타임으로, Vue·React·Angular·Svelte 등 임의의 웹 프론트엔드 빌드 산출물을 iOS·Android·웹(PWA) 로 배포할 수 있게 합니다. 하이브리드 앱에서 중요한 것은 “웹 기술로 UI를 그린다”는 사실보다, 네이티브 수명 주기·권한·스토어 정책 안에서 안전하게 브리지를 유지하는 것입니다.
이 가이드는 (1) 아키텍처와 핵심 개념, (1-α) 네이티브 브리지·플러그인 내부·WebView 수명 주기·플랫폼 차이·프로덕션 배포 패턴(심화), (2) iOS/Android 네이티브 프로젝트 구성, (3) 공식 플러그인 API 활용, (4) 커스텀 플러그인 개발 흐름, (5) 푸시·백그라운드의 현실적인 한계와 패턴, (6) 성능 최적화 체크리스트, (7) Cordova와의 비교 순으로 정리합니다.
1. Capacitor의 핵심 개념
1.1 네이티브 셸과 웹 자산
Capacitor 앱은 단순히 “URL을 여는 브라우저”가 아니라, Xcode/Android Studio가 생성하는 네이티브 프로젝트 안에 정적 웹 자산(webDir) 이 복사되고, WKWebView(iOS) 또는 Android System WebView가 이를 로드합니다. 개발 중에는 server.url로 로컬 개발 서버에 붙을 수 있어 핫 리로드에 가까운 경험을 만들 수 있습니다.
1.2 Capacitor 런타임과 브리지
JavaScript 측에서는 @capacitor/core가 플러그인 레지스트리와 메시지 패싱을 담당합니다. 네이티브 측에서는 Capacitor Runtime이 동일한 플러그인 이름·메서드 이름에 대응하는 구현을 호출합니다. 이 계약이 명확하기 때문에 TypeScript 정의가 잘 맞고, 신규 플러그인 추가 시에도 패턴이 반복됩니다.
1.3 npx cap sync의 의미
sync는 (1) 웹 빌드를 android/app/src/main/assets/public 등 올바른 위치로 복사하고, (2) 네이티브 의존성·플러그인 훅을 Gradle/CocoaPods 쪽과 맞추는 작업입니다. “웹만 다시 빌드했다”고 해서 스토어 빌드에 반영되지 않는 이유가 여기에 있습니다. 릴리스 빌드 전에는 반드시 웹 빌드 → cap sync → 네이티브 빌드 순서를 지켜야 합니다.
1.4 설정의 중심: capacitor.config
앱 ID(appId), 앱 표시 이름(appName), 웹 자산 경로(webDir), 개발 서버 URL, iOS/Android별 세부 옵션을 한곳에서 관리합니다. 팀 규모가 커질수록 환경별 설정(스테이징 API, 로깅 레벨)을 capacitor.config.ts에서 분기하거나, 빌드 파이프라인에서 파일을 치환하는 패턴이 흔합니다.
// capacitor.config.ts (예시 구조)
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'MyApp',
webDir: 'dist',
server: {
// 개발 시에만 사용 — 프로덕션 빌드에서는 제거하거나 빌드 스크립트로 비활성화
// url: 'http://192.168.0.10:5173',
cleartext: true,
},
ios: {
contentInset: 'automatic',
},
android: {
allowMixedContent: false,
},
};
export default config;
위 예시에서 webDir는 Vite·Webpack·Angular CLI 등 프로덕션 빌드 출력 폴더와 정확히 일치해야 합니다. server.url을 켠 채로 스토어 제출용 아카이브를 만들면 외부 네트워크에 의존하는 앱이 되어 심사에서 문제가 될 수 있으므로, CI에서는 별도의 프로덕션 설정 파일을 쓰는 방식을 권장합니다.
1.5 보안 모델을 이해하기
하이브리드 앱은 파일 스킴(capacitor:// 등)과 혼합 콘텐츠, 디버그 빌드에서의 HTTP 허용 같은 주제와 마주칩니다. 운영 빌드에서는 HTTPS만, 불필요한 cleartext 비활성화, 신뢰할 수 있는 오리진만 allowNavigation에 추가하는 식으로 공격 표면을 줄입니다. 또한 민감한 토큰을 WebView 저장소에 그대로 두지 않고 네이티브 키체인/Keystore 래퍼 플러그인을 쓰는 사례가 많습니다.
1-α. 네이티브 브리지 아키텍처(심화)
표면적으로는 import { Foo } from '@capacitor/foo' 한 줄이지만, 런타임 관점에서는 (A) JS 번들이 Capacitor 코어와 통신하고, (B) 코어가 플랫폼별 브리지로 직렬화된 메시지를 넘기며, (C) 네이티브 런타임이 플러그인 레지스트리에서 구현체를 찾아 디스패치하는 3단 구조입니다. 이 계층을 이해하면 “왜 sync가 필요한지”, “왜 특정 플러그인만 깨지는지”를 원인 분석하기 쉬워집니다.
1-α.1 메시지 경로와 직렬화 경계
JS 측 @capacitor/core는 플러그인 호출을 이름 기반 디스패치(플러그인 식별자 + 메서드 이름 + 인자 객체)로 표현합니다. 네이티브로 넘어갈 때 인자는 JSON 직렬화 가능한 구조(문자열·숫자·불리언·객체·배열 등)로 제한되는 것이 일반적이며, 이 경계를 넘는 값(파일 디스크립터, 순환 참조, 클로저)은 설계 단계에서 차단됩니다. 그 결과 TypeScript 타입 정의가 “실제로 브리지를 통과할 수 있는 형태”와 강하게 정렬됩니다.
iOS에서는 WKWebView의 스크립트 메시지 채널(예: WKScriptMessageHandler)을 통해 네이티브가 메시지를 수신하고, Android에서는 Chromium 기반 WebView의 브리지 인터페이스를 통해 동일한 계약을 구현합니다(플랫폼별 템플릿·버전에 따라 클래스 이름과 등록 지점은 다를 수 있으나, “JS → 네이티브 단일 진입점 → Capacitor 런타임”이라는 큰 그림은 동일합니다). 개발 모드에서 server.url로 원격 번들을 로드할 때와, capacitor:// 혹은 로컬 파일 스킴으로 패키징된 자산을 로드할 때 오리진·CORS·혼합 콘텐츠 정책이 달라지므로, 브리지는 동일해도 네트워크·보안 경계는 별개로 봐야 합니다.
1-α.2 콜백 상관관계와 비동기 모델
플러그인 메서드는 대부분 Promise로 노출되지만, 네이티브 구현은 전통적인 요청 ID(콜백 핸들) 패턴으로 응답을 되돌립니다. 즉 JS는 “한 번의 호출”으로 읽히지만, 런타임 내부에서는 요청 단위로 네이티브 응답이 매칭됩니다. 이 때문에 다음이 중요합니다.
- 동일 메서드의 중첩 호출·재진입을 허용할지(잠금·큐·취소 토큰) 앱 수준에서 정의할 것
- 타임아웃·취소가 필요한 작업(블루투스 스캔, 위치 구독)은 플러그인 API에 맞춰 생명 주기를 설계할 것
- 에러 객체를 플랫폼 공통 형태로 매핑해 JS에서 분기 가능하게 할 것
1-α.3 스레드 모델: “Promise”와 “메인 스레드”의 오해
JS의 비동기성과 달리, 네이티브 쪽 작업은 UI 스레드 규약(iOS 메인 런루프, Android 메인 스레드)과 백그라운드 실행 정책의 지배를 받습니다. Capacitor 플러그인 구현체가 메인 스레드에서 무거운 I/O를 수행하면 스크롤 버벅임·터치 지연·ANR로 이어집니다. 반대로 백그라운드에서 UI 터치를 유발하는 API를 호출하면 플랫폼별로 예외가 나거나 레이스가 납니다. 실무에서는 작업을 백그라운드로 보내고, 결과만 메인에서 UI/콜백으로 전달하는 패턴이 기본선입니다.
1-β. 플러그인 시스템 내부(심화)
플러그인은 “npm 패키지”이기 이전에 네이티브 바이너리에 등록된 모듈입니다. @capacitor/cli의 sync는 단순 파일 복사가 아니라, Gradle 의존성·CocoaPods 구성·플랫폼 템플릿에 선언된 로더 훅까지 일치시키는 과정입니다.
1-β.1 등록·탐색·디스패치
런타임은 기동 시점에 플러그인 클래스 목록을 구성합니다(iOS/Android 각각의 등록 메커니즘이며, 템플릿 버전에 따라 자동 스캔·명시 등록 방식이 다를 수 있음). JS에서 Foo.bar()를 호출하면:
- 코어가 플러그인 ID와 메서드 이름으로 메시지를 구성
- 네이티브 브리지가 메시지를 수신
- Capacitor 런타임이 해당 네이티브 플러그인 인스턴스의 메서드로 라우팅
- 결과 또는 예외를 직렬화해 JS로 반환
여기서 “Plugin ... is not implemented” 류의 오류는 대개 (a) npm 설치만 하고 sync가 안 됐거나, (b) 네이티브 쪽 의존성/등록이 빠졌거나, (c) 플랫폼 빌드 변형(debug/release)에서만 존재하는 조건부 의존성 중 하나입니다.
1-β.2 @CapacitorPlugin / 메서드 바인딩과 리플렉션 비용
네이티브 측은 선언적 어노테이션(예: 메서드 노출)과 리플렉션 기반 디스패치를 사용하는 경우가 있어, 콜드 경로 비용은 대부분 무시 가능하지만, 초기 기동 시 플러그인 로딩 순서나 예외 처리 경로에서 디버깅 난이도가 달라집니다. 커스텀 플러그인을 작성할 때는 공식 템플릿을 벗어나지 말고, 메서드 이름·시그니처·스레드 규약을 문서화하는 것이 장기적으로 이득입니다.
1-β.3 Web 폴백과 registerPlugin
registerPlugin의 web 구현은 PWA·브라우저 개발을 위한 동일 인터페이스 스텁입니다. 모바일 전용 기능이라면 Web에서 명시적 실패를 반환해 조기에 잘못된 가정을 깨는 편이 안전합니다. 반대로 Filesystem·Preferences처럼 웹에서도 의미 있는 대체 구현이 가능한 영역은 Web 구현 품질이 개발 생산성을 좌우합니다.
// registerPlugin 패턴의 의도: 동일 인터페이스로 플랫폼 분기 은닉
import { registerPlugin } from '@capacitor/core';
export interface SecureStoragePlugin {
get(options: { key: string }): Promise<{ value: string | null }>;
set(options: { key: string; value: string }): Promise<void>;
}
export const SecureStorage = registerPlugin<SecureStoragePlugin>('SecureStorage', {
web: () => import('./secure-storage-web').then((m) => new m.SecureStorageWeb()),
});
위 패턴에서 운영 이슈는 “Web 폴백이 보안상 취약한 저장소를 쓰는지”입니다. 모바일에서는 Keychain/Keystore 래퍼를 쓰더라도, 웹 폴백이 localStorage라면 위협 모델이 달라지므로 문서에 명시해야 합니다.
1-γ. WebView 수명 주기 관리(심화)
하이브리드 앱의 크래시의 상당수는 WebView가 “브라우저 탭”처럼 동작하지 않는다는 전제 오류에서 비롯됩니다. 특히 iOS의 WKWebView는 별도 렌더 프로세스를 사용하며, 메모리 압박 시 프로세스가 종료될 수 있습니다.
1-γ.1 iOS: WKWebView와 렌더 프로세스 종료
iOS는 WebKit이 웹 콘텐츠 프로세스를 분리해 운영합니다. 앱이 백그라운드에 있거나 메모리가 부족하면 이 프로세스가 죽고, WKWebView는 빈 문서로 복구되거나 재로딩될 수 있습니다. 이 순간 JS 힙의 모든 상태는 소멸합니다. 따라서 “SPA 상태는 메모리에만” 두는 설계는 하이브리드에서 취약합니다.
실무 대응:
- 복구 가능한 단일 진실 공급원: 인증 세션·폼 초안·라우트는
Preferences·파일·서버 세션 등으로 재기동에도 살아남는 저장소와 조합 - 네이티브 훅에서 종료 감지:
webViewWebContentProcessDidTerminate에서 로깅·복구 루틴(필요 시reload)을 연결 - 무한 리로드 루프 방지: 동일 실패가 반복되면 서킷 브레이커로 사용자에게 안내
1-γ.2 Android: Activity·프로세스·WebView 일시정지
Android는 Activity 생명 주기와 연동해 WebView의 타이머·미디어·렌더링을 제어해야 합니다. 백그라운드에서 불필요한 JS 타이머가 돌면 배터리·백그라운드 실행 제한과 충돌합니다. 일반적으로:
onPause/onResume에서WebView.onPause()/onResume()호출 패턴을 검토(템플릿·버전별로 앱 구조가 다름)- 하드웨어 가속·레이어 타입 이슈로 특정 기기에서만 깜빡임이 생기면 디바이스 매트릭스로 확인
- WebView 업데이트(Chrome/WebView 시스템 컴포넌트) 편차로 기능이 갈리면 최소 WebView 버전을 운영 정책으로 둠
1-γ.3 로드 소스: 로컬 패키지 vs 개발 서버
개발 중 server.url을 사용하면 네트워크 의존·CORS·캐시가 프로덕션과 달라집니다. 릴리스 빌드는 로컬 정적 자산 로딩이 기본이며, 이때 라우터의 base, 절대 경로 자산, 서비스 워커(사용 시) 캐시 키가 어긋나면 “첫 화면만 뜨고 이후 네비게이션이 깨짐”이 발생합니다. 수명 주기 관점에서는 첫 페인트 이후 라우터 초기화 순서를 idempotent하게 유지하는 것이 핵심입니다.
1-δ. iOS와 Android 플랫폼 차이(심화)
같은 Capacitor API라도 권한 UX·백그라운드 정책·파일 접근·브라우저 엔진 업데이트 방식에서 체감이 크게 달라집니다.
1-δ.1 브라우저 엔진과 기능 편차
iOS는 앱 내 WKWebView가 시스템 WebKit을 사용하며, JIT 관련 제약·스토리지·일부 API는 Safari 계열 정책의 영향을 받습니다. Android는 시스템 WebView(Chromium) 가 Play 스토어를 통해 갱신되어 기능 롤아웃 속도가 기기·업데이트 상태에 좌우됩니다. 따라서 “특정 Web API가 크롬 최신만 지원” 같은 전제는 하이브리드에서 위험합니다. 폴리필·프로그레시브 인핸스먼트·기능 탐지가 기본 전략입니다.
1-δ.2 권한·프라이버시 UX
Android 13+ 알림 권한, iOS의 사진 라이브러리 제한된 선택(Photos picker)·위치 정밀/대략 같은 변화는 “플러그인 한 줄”로 끝나지 않습니다. 권한 거부·재요청·설정 화면 이동까지 포함한 상태 머신을 UI에 반영해야 합니다.
1-δ.3 파일·URI·스코프드 스토리지
Android는 스코프드 스토리지 이후 공용 저장소 접근이 제약되고, iOS는 샌드박스·보안 스코프드 북마크·파일 프로바이더와의 상호운용이 이슈가 됩니다. Filesystem으로 추상화되어도, 외부 앱이 공유한 콘텐츠 URI·다운로드 폴더·미디어 스토어처럼 플랫폼 고유 개념이 등장하면 커스텀 네이티브 코드가 필요해집니다.
1-δ.4 딥링크·백 스택·제스처
iOS의 스와이프 뒤로 가기 부재(앱 네비게이션은 앱 구현에 의존)와 Android의 시스템 백(예측 가능한 백 내비게이션) 차이는 웹 라우터와 충돌하기 쉽습니다. 히스토리 동기화(Capacitor App 플러그인의 뒤로 가기 이벤트 등)를 설계하지 않으면 “웹에서는 되는데 앱만 이상한” 문제가 반복됩니다.
1-ε. 프로덕션 배포 패턴(심화)
프로덕션은 웹 배포 파이프라인과 모바일 스토어 파이프라인이 결합됩니다. 핵심은 재현 가능한 빌드와 환경 분리입니다.
1-ε.1 CI에서의 “정확한 순서”
팀마다 스크립트 이름은 다르지만, 불변 조건은 동일합니다.
- 프론트엔드 프로덕션 빌드(환경 변수 주입 포함)
npx cap sync(웹 자산 복사 + 네이티브 종속성 동기화)- 네이티브 서명 빌드(iOS:
xcodebuild또는 Fastlane, Android: Gradlebundle/assemble) - 아티팩트 보관(AAB/IPA, 매핑 파일, dSYM)
sync를 생략하면 “코드는 바뀌었는데 앱은 예전 dist” 같은 사고가 납니다. CI에서는 웹 산출물 해시를 로그로 남기거나, webDir 내용을 아티팩트로 저장해 추적 가능하게 합니다.
1-ε.2 환경별 capacitor.config와 비밀 관리
스테이징·프로덕션 API, 로깅 레벨, 기능 플래그는 capacitor.config.ts 분기 또는 빌드 시 파일 치환으로 관리합니다. 모바일에서는 .env가 웹 빌드에 번들되는 방식이므로, 시크릿이 클라이언트에 노출된다는 전제를 깔고 설계합니다(토큰·클라이언트 시크릿은 결국 추출 가능). 운영 비밀은 서버·단기 토큰· attestation 등으로 보완합니다.
1-ε.3 Android: Play App Signing, AAB, 난독화
Play는 AAB 제출이 기본이며, 서명 키 정책(Play App Signing)을 이해하고 업로드 키를 안전하게 보관합니다. R8/Proguard 규칙은 플러그인·리플렉션 사용 시 깨지기 쉬우므로, 릴리스 빌드에서만 재현되는 크래시를 의심합니다. 매핑 파일을 크래시 리포터에 업로드하는 절차를 자동화합니다.
1-ε.4 iOS: 서명, 프로비저닝, dSYM
Xcode의 서명 자동화는 팀 규모가 커질수록 Fastlane match 같은 중앙화된 인증서 관리로 수렴합니다. Bitcode 사용 여부·dSYM 생성·크래시 심볼리케이션은 모니터링 품질에 직결됩니다. Capacitor 자체 이슈가 아니라도, 스토어 제출 실패의 상당수는 서명·버전·권한 문자열에서 발생합니다.
1-ε.5 OTA 업데이트(선택)와 정책 리스크
일부 팀은 웹 번들만 원격 갱신하는 솔루션(예: Ionic Appflow, 자체 CDN + 검증)을 도입합니다. 이는 배포 속도를 높이지만, 스토어 정책(특히 iOS의 코드 실행·업데이트 관련 가이드) 과 충돌하지 않도록 범위를 명확히 해야 합니다. 기능을 네이티브 플러그인으로 확장하면 스토어 제출이 다시 필요해지는 경우가 많습니다.
1-ε.6 관측 가능성: 네이티브 + JS 스택
Sentry·Firebase Crashlytics 등은 네이티브 크래시와 JS 오류를 통합하려면 소스맵·심볼 업로드가 필수입니다. 하이브리드 앱은 오류가 어느 층에서 났는지(브리지, 플러그인, 웹 라우터) 태깅하는 것이 장애 대응 속도를 좌우합니다.
# 예: CI 단계(개념) — 실제 이름은 GitHub Actions / GitLab CI 등에 맞게 조정
# 1) npm ci && npm run build
# 2) npx cap sync
# 3) android: ./gradlew bundleRelease
# 4) ios: xcodebuild -workspace ... archive (또는 fastlane lane)
# 5) 아티팩트 업로드 + 매핑/dSYM 퍼블리시
2. iOS/Android 프로젝트 구성
2.1 초기 설치와 프로젝트 생성 흐름
일반적인 흐름은 다음과 같습니다.
- 웹 프로젝트에서 프로덕션 빌드가 가능한 상태로 만든다.
npm install @capacitor/core @capacitor/cli후npx cap init으로 앱 ID와webDir를 지정한다.npx cap add ios,npx cap add android로 네이티브 폴더를 생성한다.npx cap sync로 자산을 복사하고 네이티브 종속성을 맞춘다.npx cap open ios또는npx cap open android로 IDE를 연다.
팀원마다 Xcode·CocoaPods·Android SDK·JDK 버전이 다르면 “내 컴퓨터에서는 되는데” 문제가 생깁니다. 버전 고정 문서(예: Xcode 16.x, Android Gradle Plugin x.x)를 두는 것이 좋습니다.
2.2 iOS: Xcode 프로젝트에서 봐야 할 것
- 번들 ID·서명(Signing & Capabilities): 푸시, 백그라운드 모드, 앱 그룹 등은 여기서 켭니다.
- Info.plist: 카메라·마이크·사진 라이브러리 사용 목적 문자열(
NSCameraUsageDescription등)은 스토어 심사 필수입니다. - AppDelegate / Scene 생명 주기: 딥링크, 푸시 토큰 갱신, 백그라운드 진입과 연동됩니다.
- WKWebView: iOS 버전에 따라 동작 차이가 있으므로, 최소 지원 OS를 정하고 테스트 매트릭스를 잡습니다.
2.3 Android: Gradle과 매니페스트
- applicationId는
capacitor.config의appId와 일관되게 유지합니다. - minSdkVersion / targetSdkVersion: Play 정책은 상승하는 편이므로 주기적으로 올려야 합니다.
- AndroidManifest.xml: 권한 선언,
android:usesCleartextTraffic, 백그라운드 서비스·워커 선언이 여기에 모입니다. - Proguard/R8: 릴리스 빌드에서 난독화 규칙이 플러그인 클래스를 깨뜨리지 않는지 확인합니다.
2.4 폴더 구조의 mental model
웹 저장소 루트에는 src/(프론트 소스), dist/(빌드), ios/, android/가 공존합니다. 네이티브 코드를 직접 수정한 경우 Capacitor 업그레이드 시 충돌이 날 수 있으므로, 커스텀 네이티브 변경은 플러그인으로 격리하거나, 문서화된 패치 파일을 두는 편이 안전합니다.
flowchart LR
subgraph web [웹 빌드]
A[소스 TS/Vue/React] --> B[번들러 빌드]
B --> C[webDir 산출물]
end
subgraph cap [Capacitor]
C --> D[npx cap sync]
D --> E[iOS/Android 자산 복사]
end
subgraph native [네이티브]
E --> F[Xcode / Android Studio 빌드]
F --> G[IPA / AAB·APK]
end
3. 네이티브 플러그인 API 활용
공식 @capacitor/* 패키지는 네이티브 기능을 Promise 기반 API로 감쌉니다. 패턴은 대부분 import { PluginName } from '@capacitor/...'; 후 PluginName.method() 호출입니다. 권한 거부·취소·타임아웃은 반드시 UI와 함께 설계합니다.
3.1 Camera
@capacitor/camera는 사진 촬영·갤러리 피커를 제공합니다. 권한 문구·프라이버시, 대용량 이미지 메모리, EXIF 메타데이터까지 고려해야 합니다. 업로드 전 리사이즈·압축은 웹에서 처리할지, 네이티브에서 처리할지 팀 역량에 따라 갈립니다.
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
export async function pickPhoto() {
const image = await Camera.getPhoto({
quality: 85,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Prompt, // 카메라/갤러리 선택 UI
});
// image.webPath — 웹뷰에서 표시 가능한 경로
return image;
}
실무에서는 resultType을 Base64로 받아 바로 업로드하는 대신 파일 URI 스트림으로 올리는 편이 메모리 측면에서 유리한 경우가 많습니다. 또한 iOS에서 Photos 권한 범위(제한된 라이브러리) 정책을 고려해 UX를 설계합니다.
3.2 Filesystem
@capacitor/filesystem은 DATA·CACHE·DOCUMENTS 등 저장소 구획을 추상화합니다. 오프라인 캐시, 로그 파일, 사용자 생성 콘텐츠 저장에 쓰입니다. 경로 문자열을 직접 조합하기보다 API가 제공하는 URI·디렉터리 상수를 사용하면 OS 차이를 흡수하기 쉽습니다.
대용량 파일은 청크 읽기/쓰기와 진행률 UI를 함께 설계하고, Android의 스코프드 스토리지 정책과 충돌하지 않는지 확인합니다.
3.3 기타 자주 쓰는 공식 플러그인
| 영역 | 대표 플러그인 | 메모 |
|---|---|---|
| 기기 정보 | App, Device | 모델명·OS 버전은 분석·호환성 분기에 사용 |
| 네트워크 | Network | 오프라인 배너, 동기 큐 재시도 트리거 |
| 저장소 | Preferences | 소량 KV, 민감 정보는 추가 암호화 검토 |
| UI | Toast, Dialog, Status Bar, Splash Screen | 네이티브 룩앤필과 일관성 |
| 하드웨어 | Haptics, Geolocation | 권한·배터리 소모 주의 |
플러그인마다 최소 OS 버전과 권한 선언이 다르므로, 스토어 제출 전 실기기 매트릭스로 검증합니다.
4. 커스텀 플러그인 개발
4.1 언제 커스텀인가
공식·커뮤니티 플러그인으로 해결되지 않는 사내 SDK, 특수 주변기기, 보안 정책상 네이티브에만 둘 코드가 있을 때 커스텀 플러그인을 만듭니다. Capacitor는 Swift/Objective-C(iOS) 와 Kotlin/Java(Android) 로 각각 메서드를 구현하고, TypeScript에서 동일한 식별자로 호출합니다.
4.2 설계 체크리스트
- 플러그인 이름·메서드 이름을 팀 규칙에 맞게 고정한다.
- 입출력은 JSON 직렬화 가능한 타입 위주로 유지해 브리지 비용을 예측 가능하게 한다.
- 메인 스레드 규칙: UI 갱신은 메인, 무거운 작업은 백그라운드 큐로 넘긴 뒤 결과만 브리지한다.
- 에러 코드를 플랫폼 공통 열거형으로 매핑해 JS에서 분기 처리한다.
4.3 TypeScript 측 스텁 예시
import { registerPlugin } from '@capacitor/core';
export interface EchoPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
}
export const Echo = registerPlugin<EchoPlugin>('Echo', {
web: () => import('./echo-web').then((m) => new m.EchoWeb()),
});
웹(PWA) 타깃까지 고려하면 web 구현을 제공해 동일한 인터페이스로 개발할 수 있습니다. 모바일 전용이면 web에서 명확한 no-op 또는 에러를 반환합니다.
4.4 네이티브 측의 역할
iOS에서는 CAPPlugin 서브클래스에 @objc 메서드를 노출하고, Android에서는 Plugin 클래스에 @PluginMethod를 붙입니다. 실제 코드는 플랫폼별 가이드(Swift Concurrency, Kotlin Coroutine)에 맞춰 작성하되, Capacitor의 콜백/에러 전달 규약을 지키는 것이 핵심입니다.
팀에 네이티브 경험이 적다면, 기능을 최소 단위로 쪼개 플러그인 수를 늘리기보다 하나의 응집된 플러그인으로 묶어 유지보수 비용을 관리합니다.
5. 푸시 알림과 백그라운드 작업
5.1 푸시 알림: FCM / APNs
@capacitor/push-notifications는 로컬 알림 스케줄링과 원격 푸시 토큰 수신을 다루는 데 도움이 됩니다. 그러나 서버에서 실제로 보내는 인프라(FCM HTTP v1, APNs 인증 키·인증서)와 토큰 등록 API는 앱 백엔드와 함께 설계해야 합니다.
실무에서는 다음을 반복 점검합니다.
- 토큰 갱신 시 서버 동기화
- 포그라운드 수신 시 인앱 표시 정책
- 딥링크 payload와 라우팅 매핑
- Android 채널, iOS 카테고리 등 OS별 UX
5.2 백그라운드 작업의 현실
모바일 OS는 배터리·프라이버시를 이유로 백그라운드 실행을 엄격히 제한합니다. Capacitor만으로 “데스크톱처럼 항상 도는 워커”를 기대하면 실패하기 쉽습니다.
- iOS:
Background Tasks,BGAppRefresh, 위치 업데이트, 푸시 기반 깨우기 등 사용 사례별 프레임이 정해져 있습니다. - Android: WorkManager, 포그라운드 서비스(알림 필수) 등 정책이 자주 변하므로 공식 문서를 추적해야 합니다.
@capacitor/background-runner 같은 실험적·제한적 API는 문서의 지원 범위를 확인하고 도입합니다. 주기적 동기화가 비즈니스 핵심이면 네이티브 설계 검토가 선행되어야 합니다.
5.3 설계 권장
- 가능하면 푸시 트리거 + 짧은 작업 패턴
- 오프라인 큐는 앱 재개 시 처리
- 위치 추적이 필요하면 포그라운드 서비스·배터리 고지 등 정책 준수
6. 성능 최적화
6.1 웹뷰 병목을 먼저 의심
프로파일링 결과가 JS 실행·레이아웃·페인트에 쏠리면, 네이티브 튜닝보다 프론트엔드 최적화가 우선입니다. 긴 목록은 가상 스크롤, 이미지는 지연 로딩·적절한 해상도, 메인 스레드 작업은 청크 분할·Web Worker를 검토합니다.
6.2 시작 성능
- 스플래시·초기 번들 크기 줄이기
- 라우트 단위 코드 스플리팅
- 필수 API만 초기 로드, 나머지는 지연
import
6.3 네이티브 측
- WKWebView 프로세스 재시작 이슈 대비 상태 복구
- 하드웨어 가속, 디버그 WebView로 성능 추적
- Android 과도한 오버드로우·섀도 줄이기
6.4 메모리
카메라·파일·지도처럼 대용량 객체를 다룰 때 메모리 스파이크가 납니다. 스트리밍 업로드, Object URL 해제, 이미지 다운샘플을 습관화합니다.
7. Cordova vs Capacitor
7.1 철학과 구조
Cordova는 오랜 역사와 방대한 플러그인 생태계를 가졌고, Capacitor는 네이티브 프로젝트를 소스로 보존하고 npm 기반 툴링에 맞춘 현대적 설계를 지향합니다. Capacitor는 Cordova 호환 레이어를 통해 일부 Cordova 플러그인을 사용할 수 있지만, 모든 플러그인이 무조건 동작하는 것은 아닙니다.
7.2 선택 가이드
| 기준 | Cordova에 가까운 경우 | Capacitor에 가까운 경우 |
|---|---|---|
| 기존 자산 | 대규모 Cordova 플러그인 의존 | 최신 웹 스택·번들러 중심 |
| 네이티브 커스터마이징 | 유지보수 최소 | Xcode/Gradle 수정이 잦음 |
| 팀 스킬 | 레거시 위주 | TypeScript·모던 프론트 강함 |
| 장기 로드맵 | 유지 모드 프로젝트 | 신규 기능·스토어 정책 대응 |
새 프로젝트는 대개 Capacitor가 유리하고, 레거시는 비용 산정 후 점진 이전이 현실적입니다.
8. CLI 명령과 일상적인 작업 흐름
아래는 팀 온보딩 문서에 그대로 넣기 좋은 명령 요약입니다. 정확한 플래그는 npx cap --help와 프로젝트에 설치된 @capacitor/cli 버전을 기준으로 합니다.
| 명령 | 용도 |
|---|---|
npx cap init | 앱 ID·이름·webDir로 Capacitor 메타데이터 초기화 |
npx cap add ios / android | 네이티브 프로젝트 폴더 생성(이미 있으면 덮어쓰지 않도록 주의) |
npx cap sync | 웹 빌드 복사 + 네이티브 종속성 동기화 |
npx cap copy | 자산만 복사(sync의 일부; 플러그인 Gradle/Pods까지 필요하면 sync) |
npx cap open ios / android | Xcode/Android Studio 실행 |
npx cap run ios / android | CLI에서 빌드·실행(환경에 따라 유틸 설치 필요) |
일상 루프는 (1) 웹에서 기능 개발 → (2) npm run build → (3) npx cap sync → (4) IDE에서 실행입니다. 릴리스 브랜치에서는 server.url이 꺼져 있는지, 올바른 환경 변수가 주입되는지 CI에서 검증하는 편이 안전합니다.
9. 트러블슈팅: 자주 막히는 지점
9.1 흰 화면·index.html 없음
webDir가 실제 빌드 출력과 다르거나, base 경로가 잘못되어 에셋이 404인 경우가 많습니다. 브라우저 개발자 도구의 네트워크 탭으로 첫 HTML·JS 로드 성공 여부를 확인하고, Vite라면 base와 Capacitor의 루트 로딩 경로가 일치하는지 봅니다.
9.2 "Plugin ... is not implemented on android"
JS에서는 플러그인을 호출하지만 네이티브에 패키지 등록·Gradle 의존성이 빠졌을 때 흔합니다. npm install @capacitor/... 후 npx cap sync를 다시 했는지, Android 쪽 MainActivity 생성자에 플러그인이 로드되는지(버전별 템플릿 차이)를 확인합니다.
9.3 iOS에서만 API가 실패
권한 문자열 누락, Capabilities 미설정, 시뮬레이터와 실기기 차이 등이 원인입니다. 특히 카메라·마이크·위치는 실기기와 Info.plist를 함께 봐야 합니다.
9.4 CORS·혼합 콘텐츠
웹뷰에서 원격 API를 부를 때는 브라우저와 동일하게 CORS 제약을 받습니다. 해결은 (1) API 서버 헤더 수정, (2) 앱 전용 게이트웨이, (3) 네이티브에서 프록시하는 커스텀 플러그인 등이 있습니다. 운영 빌드에서 HTTP를 열어두지 않도록 합니다.
10. 딥링크·유니버설 링크(개념)
앱이 특정 URL을 열었을 때 웹이 아니라 앱 화면으로 라우팅하려면 iOS Universal Links, Android App Links 설정이 필요합니다. Capacitor 자체가 모든 것을 대신해 주는 것은 아니고, 도메인 인증 파일(AASA, Digital Asset Links)과 네이티브 매니페스트/Associated Domains 구성이 선행됩니다. 구현 후에는 실제 기기에서 링크 탭 → 앱 전환, 뒤로 가기 동작까지 시나리오별로 검증합니다.
11. 운영 체크리스트
- 환경 분리: dev/stage/prod API, 로깅,
server.url차단 - 버전 정책: 최소 OS, WebView 버전, Play/App 심사 노트
- 보안: 혼합 콘텐츠, 인증서 핀닝(필요 시), 저장소 암호화
- 관측: 크래시 리포팅(Sentry 등), 네이티브·JS 스택 통합
- CI: 웹 빌드 →
cap sync→ 네이티브 빌드 아티팩트까지 자동화
12. 정리
Capacitor는 웹 개발 속도와 스토어 배포를 잇는 실용적인 다리입니다. 성공적인 하이브리드 앱은 “웹 기술을 썼다”보다 플랫폼 수명 주기·권한·성능·보안을 네이티브 앱처럼 다뤘는지로 평가됩니다. 운영 난이도를 낮추려면 브리지 메시지 경계·플러그인 디스패치·WebView 프로세스 종료에 따른 상태 복구·iOS/Android 정책 차이·CI 기반 재현 빌드까지 같은 설계 안에 넣는 것이 좋습니다. 이 글의 흐름대로 내부 구조 심화 → 구성 → 공식 플러그인 → 필요 시 커스텀 → 푸시·백그라운드 현실 인지 → 성능·Cordova 비교를 설계에 반영하면, 초기 설계 단계에서 큰 비용 낭비를 줄일 수 있습니다.
배포 전에는 git add, git commit, git push 후 npm run deploy를 실행하는 워크플로를 따르십시오.
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Capacitor 완벽 가이드 — 웹에서 네이티브 하이브리드 앱으로」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Capacitor 완벽 가이드 — 웹에서 네이티브 하이브리드 앱으로」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.