Capacitor 완벽 가이드 — 웹에서 네이티브 하이브리드 앱으로
이 글의 핵심
Capacitor는 빌드된 웹 자산을 네이티브 컨테이너에 싣고, JavaScript와 OS API 사이를 얇고 예측 가능한 브리지로 연결합니다. 이 글에서는 아키텍처, iOS/Android 프로젝트 구조, Camera·Filesystem 등 공식 플러그인, 커스텀 네이티브 플러그인, 푸시·백그라운드 제약, 성능 튜닝, Cordova와의 선택 기준을 한 흐름으로 다룹니다.
이 글의 핵심
Capacitor는 Ionic 팀이 만든 크로스플랫폼 런타임으로, Vue·React·Angular·Svelte 등 임의의 웹 프론트엔드 빌드 산출물을 iOS·Android·웹(PWA) 로 배포할 수 있게 합니다. 하이브리드 앱에서 중요한 것은 “웹 기술로 UI를 그린다”는 사실보다, 네이티브 수명 주기·권한·스토어 정책 안에서 안전하게 브리지를 유지하는 것입니다.
이 가이드는 (1) 아키텍처와 핵심 개념, (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 래퍼 플러그인을 쓰는 사례가 많습니다.
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는 웹 개발 속도와 스토어 배포를 잇는 실용적인 다리입니다. 성공적인 하이브리드 앱은 “웹 기술을 썼다”보다 플랫폼 수명 주기·권한·성능·보안을 네이티브 앱처럼 다뤘는지로 평가됩니다. 이 글의 흐름대로 구성 → 공식 플러그인 → 필요 시 커스텀 → 푸시·백그라운드 현실 인지 → 성능·Cordova 비교를 설계에 반영하면, 초기 설계 단계에서 큰 비용 낭비를 줄일 수 있습니다.
배포 전에는 git add, git commit, git push 후 npm run deploy를 실행하는 워크플로를 따르십시오.