기존 React + Vite 웹앱을 Capacitor 로 iOS/Android 앱으로 배포하기 위한 실전 순서.
- ✅ 1. Capacitor 설치 & 초기화
- ✅ 2. 네이티브 플랫폼 추가 (iOS/Android)
- ✅ 3. 개발 워크플로우 스크립트
- ✅ 4.
package.json스크립트 (app:sync,app:ios,app:android) - ✅ 5. 필수 플러그인 9종 설치 +
npx cap sync - ✅ 7. CORS (백엔드) —
capacitor://localhost,http://localhost허용 - ✅ 9. 반응형·안전영역 —
viewport-fit=cover+env(safe-area-inset-*)로 노치/홈 인디케이터 대응 - ✅ 웹/앱 UI 분리 —
Capacitor.isNativePlatform()기반is-native/is-webbody 클래스 부여. CSS 에서.is-native셀렉터로 앱 전용 스타일 적용 (창 크기 무관) - ✅ 사이드바 → 드로어 UI — 앱에서 사이드바를 fixed off-screen 으로 두고 헤더의 MODI 로고 버튼 탭 시 슬라이드-인. 백드롭 오버레이·자동 닫기 지원
- ✅ iOS 시뮬레이터 빌드·실행 검증 (iPhone 17 Pro, xcodebuild + SPM)
- ✅ 로그인 · JWT 인증 흐름 검증 (앱에서 실제 로그인 성공)
- ✅ 모바일 UI 튜닝 (하위 태스크):
- 자산 비교 탭: 헤더 세로 스택+중앙 정렬, 필터 select
-webkit-appearance:none+ 커스텀 화살표 (년/월 렌더 이슈 해결), 금액 슬라이더 카드 안 가둠, 공개 사용자 카드 헤더 세로 스택, 평균 비교 카드 전체 콘텐츠 중앙정렬 + 카드 폭 통일 - 예산 관리 탭:
budget-header-actions중앙 정렬,CompactStat폰트/패딩 축소 (계획합계·실제지출 한 줄) - Q&A · 노하우 탭: HOT 게시글 그리드 앱에서 1열 스택, 게시글 카드 2열 → 1열, 페이지 헤더 아이콘 제거 + 타이틀 정중앙
- 알림 탭: 상단 3열 요약 → 1열
- 공통:
.cardpadding 24→16, 좌우 여백 32→12,dashboard-view-header세로 스택 + 중앙 정렬
- 자산 비교 탭: 헤더 세로 스택+중앙 정렬, 필터 select
- ⏳ 5-1. 토큰 저장소 안전화 — 현재 웹뷰 localStorage 그대로 사용 중.
Preferences어댑터로 감싸는 async 리팩터 필요 (호출부:tokenStorage.ts,client.ts,LoginView.tsx,App.tsx초기 부팅 렌더링 순서 조정). 배포 전 필수 (앱 삭제 시 세션 유실 방지, 백업 노출 방지) - ⏳ 5-2. FCM 푸시 알림 — 백엔드 인프라 존재. 앱에서
PushNotifications.register()+GoogleService-Info.plist/google-services.json발급 + Firebase Console + Xcode Capability 설정 필요 - ⏳ 5-3. 이미지 업로드 (Camera 플러그인) —
KnowhowWriteView,QnaWriteView등에서Camera.getPhoto()로 대체 - ⏳ 6. OAuth2 딥링크 — 카카오/구글 로그인 앱 지원용. 커스텀 스킴
com.jointliving.app://oauth2/redirect를 Info.plist / AndroidManifest.xml / 각 OAuth 콘솔 / Spring Security whitelist 에 등록 필요 - ⏳ 8. 스플래시 & 아이콘 — 현재 Capacitor 기본. MODI 로고로 교체 필요 (
resources/icon.png1024²,resources/splash.png2732²) - ⏳ 10. 배포 — Apple Developer ($99/년) + Play Console ($25) 계정 필요. TestFlight / Play 내부테스트 트랙 업로드
- ⏳ 11. OTA 업데이트 — Optional.
@capgo/capacitor-updater등
"쓸 만한 앱 배포"까지 남은 크리티컬 패스: 토큰저장소 → OAuth 딥링크 → 아이콘 → TestFlight 업로드 (약 3~5일 예상)
- Node 20+, npm/pnpm
- iOS 빌드: macOS + Xcode 15+ + CocoaPods (
brew install cocoapods) - Android 빌드: Android Studio + JDK 17
- Apple Developer 계정 ($99/년, TestFlight 등록 필요), Google Play Console ($25 일회성)
프론트 프로젝트 루트에서:
cd frontend
npm i @capacitor/core @capacitor/cli
npx cap init "JointLiving" "com.jointliving.app" --web-dir=distcom.jointliving.app= bundle ID. 나중에 못 바꾸니 신중히.--web-dir=dist= Vite 빌드 산출물 위치.vite.config.ts의build.outDir이 다르면 맞춰야 함.
생성된 capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.jointliving.app',
appName: 'JointLiving',
webDir: 'dist',
server: {
androidScheme: 'https',
// 개발 중엔 아래 주석 풀어서 dev 서버 붙이기
// url: 'http://192.168.0.10:5173',
// cleartext: true,
},
};
export default config;npm i @capacitor/ios @capacitor/android
npm run build # dist/ 생성
npx cap add ios
npx cap add androidios/, android/ 폴더 생성 — 커밋 필요 (네이티브 프로젝트 파일).
.gitignore 추가 항목:
ios/Pods/
ios/App/Podfile.lock # 팀 정책 따라
android/.gradle/
android/build/
android/app/build/
android/local.properties빌드 → 네이티브 동기화 → 실행:
npm run build # Vite 빌드
npx cap sync # dist/ 를 네이티브 앱에 복사 + 플러그인 반영
npx cap open ios # Xcode 열기 → ▶ 실행
npx cap open android # Android Studio 열기 → ▶ 실행핫리로드 팁: capacitor.config.ts 의 server.url 을 로컬 dev 서버 IP로 지정하면 웹처럼 저장 즉시 반영. 배포 빌드 전엔 반드시 주석 처리.
{
"scripts": {
"build": "tsc && vite build",
"app:sync": "npm run build && npx cap sync",
"app:ios": "npm run app:sync && npx cap open ios",
"app:android": "npm run app:sync && npx cap open android"
}
}npm i @capacitor/preferences \
@capacitor/push-notifications \
@capacitor/camera \
@capacitor/filesystem \
@capacitor/app \
@capacitor/status-bar \
@capacitor/splash-screen \
@capacitor/browser \
@capacitor/keyboard
npx cap synclocalStorage 는 웹뷰에서 앱 삭제 시 날아가고 백업에 노출됨. Preferences 로 어댑터:
// src/api/tokenStorage.ts
import { Capacitor } from '@capacitor/core';
import { Preferences } from '@capacitor/preferences';
const isNative = Capacitor.isNativePlatform();
async function setItem(k: string, v: string) {
if (isNative) await Preferences.set({ key: k, value: v });
else localStorage.setItem(k, v);
}
// getItem/removeItem 동일 패턴localStorage.getItem) → async 로딩 완료 후 앱 렌더 구조로 감싸야 함.
백엔드 FCM 인프라 존재. 앱에서 토큰 등록만:
import { PushNotifications } from '@capacitor/push-notifications';
import { Capacitor } from '@capacitor/core';
export async function initPush() {
const perm = await PushNotifications.requestPermissions();
if (perm.receive !== 'granted') return;
await PushNotifications.register();
PushNotifications.addListener('registration', async (token) => {
await authFetch('/api/v1/users/me/devices', {
method: 'POST',
body: JSON.stringify({
fcmToken: token.value,
platform: Capacitor.getPlatform(),
}),
});
});
PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
const boardId = action.notification.data?.boardId;
if (boardId) window.location.hash = `/qa?post=${boardId}`;
});
}iOS Xcode 설정:
- Signing & Capabilities → Push Notifications, Background Modes → Remote notifications 추가
- Firebase Console 에서
GoogleService-Info.plist받아 프로젝트 추가 - APNs 인증서/키를 Firebase Console 에 업로드
Android 설정:
android/app/google-services.json배치android/build.gradle:classpath 'com.google.gms:google-services:4.4.0'android/app/build.gradle하단:apply plugin: 'com.google.gms.google-services'
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
const photo = await Camera.getPhoto({
quality: 80,
resultType: CameraResultType.Uri,
source: CameraSource.Prompt, // 카메라/갤러리 선택
});
const blob = await fetch(photo.webPath!).then(r => r.blob());
const form = new FormData();
form.append('file', blob, 'image.jpg');
await authFetch('/api/files/upload', { method: 'POST', body: form });웹의 /oauth2/redirect → 앱에선 브라우저 열고 콜백을 앱이 가로채야 함.
com.jointliving.app://oauth2/redirect 를 redirect URI 로 등록
- iOS:
ios/App/App/Info.plist에CFBundleURLTypes추가 - Android:
android/app/src/main/AndroidManifest.xmlMainActivity 에<intent-filter>추가 - 카카오/구글 콘솔에도 이 URI 등록
- Spring Security redirect URI whitelist 에도 추가
https://jointliving.com/oauth2/redirect 를 앱이 가로채기. 도메인 인증(apple-app-site-association, assetlinks.json) 필요. 더 깔끔하지만 셋업 복잡.
브라우저 열기:
import { Browser } from '@capacitor/browser';
await Browser.open({ url: 'https://api.jointliving.com/oauth2/authorization/kakao' });콜백 처리:
import { App } from '@capacitor/app';
App.addListener('appUrlOpen', ({ url }) => {
// com.jointliving.app://oauth2/redirect?accessToken=...&refreshToken=...
const params = new URLSearchParams(new URL(url).search);
tokenStorage.setTokens(params.get('accessToken')!, params.get('refreshToken')!, ...);
Browser.close();
});앱 fetch 의 origin:
- iOS:
capacitor://localhost - Android:
http://localhost
백엔드 CORS 에 추가:
config.setAllowedOrigins(List.of(
"http://localhost:5173",
"https://jointliving.vercel.app",
"capacitor://localhost", // iOS
"http://localhost" // Android
));npm i -D @capacitor/assets
# 소스: resources/icon.png (1024x1024), resources/splash.png (2732x2732)
npx capacitor-assets generate- 노치/펀치홀: CSS 에
env(safe-area-inset-top)사용.app-layout { padding-top: env(safe-area-inset-top); }
- 상태바 색:
@capacitor/status-bar로 앱 부팅 시 스타일 지정 - 키보드:
@capacitor/keyboard로 인풋 포커스 시 화면 안 가려지게
- Xcode → Signing & Capabilities → 팀 선택, bundle ID 확인
- Product → Archive → Distribute App → App Store Connect
- App Store Connect → TestFlight → 내부/외부 테스터 초대
cd android && ./gradlew bundleRelease
# 산출물: android/app/build/outputs/bundle/release/app-release.aabPlay Console → 내부 테스트 트랙 업로드
서명 키 잃어버리면 앱 업데이트 불가 — android/app/keystore/ 백업 필수.
- 웹 코드만 바뀐 경우:
npm run app:sync후 재빌드·재제출 필요 (기본 OTA 없음) - OTA 원하면:
@capgo/capacitor-updater같은 커뮤니티 플러그인 (자체 CDN 필요)
- 인라인
<a href>/window.location:App.tsx의 hash 라우팅은 그대로 동작. 외부 링크는Browser.open()으로 열어야 앱 안에서 안 갇힘 - 파일 다운로드: 거래내역 export CSV 같은 건 웹뷰에서 동작 안 함 →
@capacitor/filesystem+ share sheet 로 우회 vercel.jsonSPA 리라이트: 앱에선 무의미 (파일 자체는 해 없음)- Chart.js / recharts: 대시보드 차트는 웹뷰에서 정상 렌더링
# 1. 설치
cd frontend
npm i @capacitor/core @capacitor/cli @capacitor/ios @capacitor/android
npx cap init "JointLiving" "com.jointliving.app" --web-dir=dist
# 2. 빌드 + 플랫폼 추가
npm run build && npx cap add ios && npx cap add android
# 3. Xcode / Android Studio 로 열어서 실행 테스트
npx cap open ios
# 4. 플러그인 추가 → sync
npm i @capacitor/preferences @capacitor/push-notifications @capacitor/camera @capacitor/app
npx cap sync
# 5. 토큰 저장소 어댑터·푸시 초기화·OAuth 딥링크 코드 반영
# 6. 아이콘/스플래시 생성
# 7. 서명 → 스토어 업로드예상 기간: 첫 실행까지 반나절, TestFlight/내부테스트 배포까지 2~3일.