웹 코드로 앱 만들기: Tauri·Electron·Capacitor 선택 가이드
웹 앱을 iOS·Android·Windows·macOS로 확장할 때 Tauri, Electron, Capacitor 중 무엇을 골라야 할지 제품 요구·권한·WebView·보안·배포 기준으로 정리했습니다.
프레임워크부터 고르면 질문을 놓친다
웹으로 먼저 만든 제품을 앱으로 넓힐 때, Tauri·Electron·Capacitor는 자주 한 줄 비교표로 만납니다. “가벼운 건 Tauri”, “성숙한 건 Electron”, “모바일은 Capacitor”라고 끝내기 쉽습니다. 하지만 실제 선택은 라이브러리 이름보다 사용자가 어느 화면에서, 어떤 위험을 감수하며, 어떤 속도로 업데이트를 받아야 하는가에 가깝습니다.
예를 들어 사내에서만 쓰는 파일 정리 도구와, iPhone의 카메라·푸시 알림을 매일 쓰는 소비자 서비스는 같은 웹 코드로 시작해도 운영의 중심이 다릅니다. 전자는 파일 권한과 데스크톱 배포가 먼저이고, 후자는 스토어 심사·알림 권한·오프라인 전환이 먼저예요. 앱의 외피를 고르는 일은 곧 제품의 약속을 고르는 일입니다.
이 글은 세 도구의 우열을 정하지 않습니다. 대신 제품 기획, 디자인 QA, 개발과 배포가 같은 문서에서 판단할 수 있게 기준을 세웁니다. 이미 Tauri vs Electron: SvelteKit 데스크톱 앱 선택 기준과 Capacitor vs Tauri: 하나의 웹 코드베이스를 모바일·데스크톱 앱으로 확장하는 선택 기준을 읽었다면, 이 글은 그 비교를 출시 계획으로 연결하는 허브입니다.
세 도구가 감싸는 경계는 다르다
먼저 “웹 앱을 네이티브 앱으로 만든다”는 말을 풀어야 합니다. 세 도구 모두 HTML·CSS·JavaScript로 만든 화면을 활용할 수 있습니다. 그렇다고 같은 종류의 런타임은 아닙니다.
| 질문 | Capacitor | Tauri | Electron |
|---|---|---|---|
| 출발점 | 모바일 웹 앱을 iOS·Android 앱으로 연결 | 웹 프런트엔드와 Rust 기반 네이티브 코어를 연결 | 웹 프런트엔드와 Chromium·Node.js 환경을 함께 배포 |
| 가장 자연스러운 무대 | iOS·Android | Windows·macOS·Linux 데스크톱 | Windows·macOS·Linux 데스크톱 |
| 화면 엔진의 관점 | 각 모바일 플랫폼 WebView | 운영체제의 WebView | 앱에 포함한 Chromium |
| 네이티브 기능의 접점 | 플러그인과 iOS/Android 프로젝트 | 명시한 command·plugin capability | main/preload/renderer와 IPC |
| 팀이 먼저 설계할 것 | 권한·플러그인·스토어 빌드 | 권한 범위·Rust 경계·플랫폼별 UI QA | IPC 경계·보안 설정·패키징과 업데이트 |
이 표는 “하나가 다른 하나를 대체한다”는 뜻이 아닙니다. Capacitor는 모바일 네이티브 프로젝트를 관리하는 흐름에 가깝고, Tauri와 Electron은 데스크톱 배포를 중심에 둔 선택입니다. Tauri도 모바일을 지원하고, Capacitor 기반 코드에 데스크톱 타깃을 더하는 조합도 존재합니다. 다만 출발점과 운영 비용을 같은 선상에서 보지 않으면, 초기에 아낀 시간이 QA·릴리스에서 다시 나갑니다.
구조를 더 단순하게 그리면 아래와 같습니다.
공유 가능한 영역
├─ 도메인 모델 · API 클라이언트 · 디자인 토큰 · 화면 컴포넌트
├─ 웹 배포물 (PWA / 브라우저)
└─ 플랫폼 어댑터
├─ Capacitor: iOS / Android 플러그인과 WebView
├─ Tauri: Rust command / capability / 시스템 WebView
└─ Electron: preload / IPC / Chromium + Node.js
여기서 가장 중요한 원칙은 공유 코드의 양이 아니라 공유해도 되는 책임의 양입니다. 결제, 카메라, 파일, 푸시, 백그라운드 작업처럼 플랫폼이 사용자의 기기와 직접 만나는 지점은 얇은 어댑터로 남겨야 해요. 화면 코드가 같다는 이유로 그 경계까지 숨기면, 오류가 생겼을 때 누가 어떤 권한으로 무엇을 호출했는지 찾기 어려워집니다.
제품 요구를 먼저 네 장으로 나눈다
기술 회의 전에 다음 네 장을 작성해보면 프레임워크 논쟁이 짧아집니다.
- 사용 환경: 사용자는 책상 앞의 큰 화면에서 일하는가, 이동 중 휴대폰을 여는가, 둘 다 필요한가.
- 기기 기능: 카메라·사진 보관함·푸시·생체 인증·공유 시트가 제품의 핵심인가. 아니면 파일 시스템·트레이·다중 창·전역 단축키가 핵심인가.
- 신뢰 경계: 로컬 파일, 자격증명, 회사 데이터, 사용자 생성 HTML 중 무엇을 읽고 쓰는가.
- 릴리스 속도: 서버에서 바꾸면 되는 콘텐츠와, 스토어 심사 또는 설치 프로그램 업데이트가 필요한 실행 코드를 분리했는가.
이 네 장은 기획 문서이면서 아키텍처 문서입니다. “모바일도 나중에 지원” 같은 문장은 아직 요구사항이 아닙니다. 모바일에서 필요한 동작을 적어야 합니다. 예컨대 사진 한 장을 촬영해 OCR로 보내고, 푸시로 결과를 받고, 오프라인에서 임시 저장한다면 Capacitor 쪽의 네이티브 플러그인·권한·복구 흐름이 제품 범위입니다. 반대로 회의 중 여러 파일을 끌어다 놓고, 메뉴 바에서 실행하며, 수백 개 항목을 키보드로 편집한다면 데스크톱이 단순한 화면 크기 확장이 아닙니다.
빠른 선택 질문
| 이 질문에 “예”라면 | 출발점 |
|---|---|
| iOS·Android 출시가 이번 분기의 핵심이고 카메라·푸시·공유가 기능의 일부인가 | Capacitor |
| 데스크톱 앱이 핵심이며, 로컬 권한을 최소한으로 선언하고 Rust 경계를 둘 수 있는가 | Tauri |
| 데스크톱에서 Chromium의 일관된 렌더링, Node.js 생태계 또는 기존 Electron 자산이 중요한가 | Electron |
| 모바일과 데스크톱 모두 필요한가 | Capacitor를 모바일 축으로 두고, 데스크톱 요구를 Tauri 또는 Electron으로 별도 검토 |
| 그 어느 쪽도 명확하지 않은가 | 먼저 반응형 웹/PWA로 핵심 행동을 검증하고 네이티브 요구를 기록 |
“모든 플랫폼을 한 번에”는 요구가 아니라 희망일 때가 많습니다. 처음에는 한 플랫폼에서 완결된 경험을 만들고, 다음 플랫폼은 공유할 것과 분리할 것을 확인한 뒤 여는 편이 낫습니다. 이 순서가 제품의 속도를 늦추는 게 아니라, 지원해야 할 예외의 수를 줄입니다.
Capacitor는 모바일 WebView의 문제를 제품 문제로 끌어온다
Capacitor는 웹 앱에 네이티브 기능을 덧붙이는 데 익숙한 팀에게 자연스럽습니다. 웹 자산을 빌드한 뒤 iOS·Android 프로젝트에 동기화하고, 플러그인을 통해 카메라·알림·파일 같은 기능을 호출하는 흐름입니다. 이 구조의 장점은 프런트엔드 팀이 기존 화면·상태 관리·API 레이어를 크게 버리지 않아도 된다는 점이에요.
하지만 “웹뷰라서 빠르다”는 이유만으로 고르면 안 됩니다. 모바일 앱이 되는 순간, 브라우저에서는 보이지 않던 상태가 등장합니다. 권한을 거절했을 때의 화면, 앱이 백그라운드로 갔다 돌아오는 순간, 느린 네트워크에서 업로드가 끊기는 순간, 앱 심사자가 로그인하지 못했을 때의 경로가 모두 제품입니다.
먼저 웹 레이어와 네이티브 레이어를 명시적으로 나눕니다.
// src/platform/camera.ts
export type CapturedPhoto = {
dataUrl: string;
format: 'jpeg' | 'png';
};
export async function capturePhoto(): Promise<CapturedPhoto> {
if (typeof window === 'undefined') {
throw new Error('브라우저 렌더링 중에는 카메라를 열 수 없습니다.');
}
const { Camera, CameraResultType } = await import('@capacitor/camera');
const result = await Camera.getPhoto({
quality: 85,
resultType: CameraResultType.DataUrl,
allowEditing: false
});
if (!result.dataUrl || !result.format) throw new Error('사진 데이터를 받지 못했습니다.');
return { dataUrl: result.dataUrl, format: result.format as 'jpeg' | 'png' };
}
위 코드의 핵심은 Camera API 자체가 아닙니다. 화면 컴포넌트가 곧바로 플러그인을 가져오지 않게 한 점입니다. 웹 미리보기에서는 대체 UI를 보이거나, 테스트에서는 같은 타입을 돌려주는 가짜 구현을 넣을 수 있어요. 이렇게 해두면 “웹에서 되는데 앱에서만 안 돼요”라는 문제가 한 컴포넌트 안에서 섞이지 않습니다.
빌드 단계도 제품 배포 단계로 취급합니다.
npm run build
npx cap sync
npx cap open ios
# 또는
npx cap open android
sync는 단순 복사가 아닙니다. 웹 빌드 결과와 플러그인 설정을 네이티브 프로젝트에 반영하는 경계입니다. CI에서 이 단계가 빠지면 로컬에서는 최신 화면을 보고, 스토어에 올린 바이너리에는 이전 웹 자산이 들어가는 일이 생깁니다. 그래서 릴리스 파이프라인은 웹 빌드 번호와 iOS/Android 버전 정보를 함께 출력해야 합니다.
Tauri는 작은 번들보다 작은 권한에서 의미가 생긴다
Tauri는 웹 프런트엔드와 Rust 코어를 연결하고, 각 운영체제의 WebView를 사용합니다. 번들 크기나 메모리 수치만 비교의 출발점으로 삼으면 실망하기 쉽습니다. 실제 가치는 프런트엔드가 운영체제에 무엇을 요청할 수 있는지 명시적으로 좁히는 구조에서 나옵니다.
파일을 저장해야 한다고 해서 모든 경로에 접근할 필요는 없습니다. 프런트엔드에서 호출 가능한 command와 scope를 구체적으로 설계해야 합니다. 다음은 사용자가 선택한 폴더 아래에만 내보내기를 허용하는 사고방식의 예시입니다.
// src-tauri/src/lib.rs
#[tauri::command]
fn export_report(destination: String, body: String) -> Result<(), String> {
if !destination.ends_with(".md") {
return Err("Markdown 파일만 내보낼 수 있습니다.".into());
}
std::fs::write(destination, body)
.map_err(|error| format!("파일을 저장하지 못했습니다: {error}"))
}
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-save",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/Documents/my-app/**" }]
}
]
}
실제 permission 식별자와 스키마는 사용 중인 플러그인 버전에 맞춰 공식 문서에서 확인해야 합니다. 이 예시는 “저장 버튼 하나”도 세 가지 결정을 포함한다는 것을 보여줍니다. 사용자가 파일을 고를 수 있는가, 앱은 어느 경로까지 쓸 수 있는가, command는 어떤 입력만 받는가. 이 셋을 한 번에 넓게 열지 않는 것이 중요합니다.
Tauri의 시스템 WebView는 장점과 검증 항목을 동시에 만듭니다. Windows·macOS·Linux에서 렌더링 엔진과 버전 관리 방식이 다를 수 있으므로, 디자인 QA는 운영체제별 실기기 또는 최소한 해당 엔진에서 해야 합니다. “Chrome에서 보인다”는 통과 기준이 아닙니다. 폰트 줄바꿈, 파일 드래그 앤 드롭, 미디어, 편집기, CSS의 최신 기능처럼 제품의 핵심인 화면을 먼저 테스트 목록으로 만들면, WebView 차이는 막연한 불안이 아니라 관리 가능한 비용이 됩니다.
Electron은 무거움의 반대말이 아니라 일관성의 선택이다
Electron은 Chromium과 Node.js를 앱과 함께 배포합니다. 그래서 설치물과 업데이트 대상으로 다뤄야 할 런타임이 분명합니다. 동시에 그 선택 덕분에 여러 운영체제에서 같은 Chromium 기반 화면을 테스트할 수 있고, JavaScript·Node.js에 익숙한 팀은 백엔드 경계를 Rust로 새로 만들지 않아도 됩니다.
이 장점은 보안 설계를 생략해도 된다는 뜻이 아닙니다. Electron에서는 renderer에 Node.js 권한을 통째로 주지 않고, preload에서 필요한 기능만 노출하는 패턴이 기본입니다. 원격 콘텐츠를 표시하는 앱일수록 더 엄격해야 합니다.
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktop', {
pickExportPath: () => ipcRenderer.invoke('dialog:pick-export-path'),
saveTextFile: (path: string, contents: string) =>
ipcRenderer.invoke('file:save-text', { path, contents })
});
// electron/main.ts
import { dialog, ipcMain } from 'electron';
import { writeFile } from 'node:fs/promises';
ipcMain.handle('dialog:pick-export-path', async () => {
const result = await dialog.showSaveDialog({
filters: [{ name: 'Markdown', extensions: ['md'] }]
});
return result.canceled ? null : result.filePath;
});
ipcMain.handle('file:save-text', async (_event, input: { path: string; contents: string }) => {
if (!input.path.endsWith('.md')) throw new Error('Markdown 파일만 저장할 수 있습니다.');
await writeFile(input.path, input.contents, 'utf8');
});
프런트엔드가 받는 것은 desktop.saveTextFile이라는 좁은 기능뿐입니다. require('fs')나 임의의 Node API를 화면에 노출하지 않습니다. 이 제한은 약간 번거롭지만, 화면의 XSS 문제를 곧바로 기기 권한 문제로 키우지 않는 핵심 경계예요. Electron 공식 보안 가이드가 context isolation, 제한적인 IPC, 최신 버전 유지, CSP를 강조하는 이유도 여기에 있습니다.
화면을 공유하되 기능을 억지로 공유하지 않는다
세 런타임을 함께 운영할 때 흔히 생기는 실패는 “코드베이스 하나”를 “동작 하나”로 오해하는 것입니다. 공유할 부분과 각 플랫폼에 남길 부분을 처음부터 나눠 두는 편이 좋습니다.
packages/
core/ # 도메인 타입, API, 유효성 검사
ui/ # 디자인 토큰, 화면 컴포넌트
web/ # 브라우저용 진입점
mobile/ # Capacitor 초기화, 모바일 권한·플러그인
desktop-tauri/ # Tauri command, capability, 번들 설정
desktop-electron/ # preload, IPC, 패키징 설정
core에는 “사진을 업로드한다”, “보고서를 내보낸다” 같은 제품 의도를 둡니다. mobile에는 카메라 권한을 확인하는 방법을, desktop-tauri와 desktop-electron에는 파일 저장이라는 의도를 운영체제 API로 바꾸는 방법을 둡니다. 디자인 시스템도 버튼의 모양만 공유하지 말고, 권한 거절·업데이트 대기·오프라인 실패 같은 상태를 공통 컴포넌트로 다루면 더 큰 이득이 납니다.
그렇다고 한 저장소가 정답이라는 뜻은 아닙니다. 팀이 작고 플랫폼별 출시 시점이 멀다면, 모노레포 관리 자체가 비용일 수 있어요. 공유할 도메인 로직이 실제로 생긴 뒤에 추출해도 늦지 않습니다. 중요한 것은 디렉터리 구조가 아니라 플랫폼 구현을 화면 곳곳에 흩뜨리지 않는 것입니다.
성능은 프레임워크가 아니라 작업의 이동 경로에서 나온다
Tauri와 Electron 비교에서 가장 자주 나오는 말은 설치 크기와 메모리입니다. 둘 다 사용자가 느끼는 품질에 영향을 주지만, 제품의 실제 병목은 다른 데 있을 때가 많습니다. 큰 이미지를 디코딩하는 위치, 목록을 가상화하지 않은 화면, 동기 API 호출, IPC를 너무 자주 오가는 상태, 백그라운드 작업이 UI를 막는 순간이 더 크게 체감됩니다.
성능 가설을 프레임워크 별칭으로 적지 말고, 사용자 행동으로 적어보세요.
| 사용자 행동 | 측정할 것 | 개선 방향 |
|---|---|---|
| 앱을 처음 연다 | 첫 화면이 상호작용 가능한 시점 | 초기 데이터·무거운 모듈을 지연 로드 |
| 파일 1,000개를 탐색한다 | 스크롤·검색 중 프레임과 응답 | 가상 목록, 인덱싱 작업의 분리 |
| 사진을 촬영·업로드한다 | 권한부터 완료 안내까지 | 실패 재시도·백그라운드 전환 상태 |
| 업데이트를 받는다 | 다운로드·재시작·롤백 | 릴리스 채널과 복구 경로 설계 |
특히 네이티브 경계를 넘는 호출은 API처럼 다뤄야 합니다. 한 글자 입력마다 Tauri command나 Electron IPC를 호출하지 말고, 화면의 입력은 로컬 상태에서 처리하고 저장·색인·파일 작업처럼 경계가 필요한 순간에만 묶어 호출합니다.
// 나쁜 예: 입력마다 데스크톱 경계를 넘는다.
editor.on('change', (value) => window.desktop.saveDraft(value));
// 더 나은 예: UI 상태와 영속화를 분리한다.
let pending: ReturnType<typeof setTimeout> | undefined;
editor.on('change', (value) => {
clearTimeout(pending);
pending = setTimeout(() => saveDraft(value), 700);
});
async function saveDraft(value: string) {
await draftRepository.save({ value, savedAt: new Date().toISOString() });
}
이런 설계는 어떤 런타임을 선택해도 남습니다. 성능을 “Tauri로 바꾸면 해결” 또는 “Electron이라 어쩔 수 없음”으로 설명하지 않아도 되는 이유입니다.
선택을 검증하는 작은 스파이크를 만든다
프레임워크를 정하기 전에 보름짜리 데모를 만들 필요는 없습니다. 다만 제품에서 가장 위험한 장면 하나를 골라, 후보 런타임에서 끝까지 통과시키는 작은 스파이크는 필요합니다. 이 스파이크의 목적은 예쁜 첫 화면을 만드는 것이 아니라, 나중에 되돌리기 어려운 가정을 일찍 깨는 것입니다.
파일 중심 데스크톱 제품이라면 다음 네 장면이 좋습니다. 사용자가 파일 선택 창에서 폴더를 고른다. 앱은 해당 폴더 안의 파일 목록을 읽는다. 한 파일을 수정해 저장한다. 앱을 재시작한 뒤에도 최근 작업을 안전하게 복원한다. Tauri에서는 command·capability·scope가 이 흐름을 과하게 넓히지 않는지, Electron에서는 preload·IPC가 필요한 기능만 노출하는지 확인할 수 있습니다. 어느 쪽이든 “파일을 읽었다”가 아니라 거절·취소·권한 없음·동시 수정까지 화면으로 확인해야 합니다.
모바일 제품이라면 카메라 또는 푸시처럼 웹 브라우저의 가정이 깨지는 기능을 택합니다. 권한을 처음 요청하는 타이밍, 권한을 거절한 뒤의 대체 경로, 앱이 백그라운드에 있을 때 완료된 작업을 다시 여는 흐름, 느린 네트워크의 재시도까지 이어 봅니다. Capacitor의 장점은 웹 코드 재사용만이 아니라 이런 기기 경계를 네이티브 프로젝트에서 실제로 확인할 수 있다는 데 있습니다.
스파이크의 결과는 “성공”과 “실패”가 아니라 체크리스트여야 합니다.
| 확인 항목 | 합격 조건 | 실패했을 때 남길 기록 |
|---|---|---|
| 빌드 | CI에서 깨끗한 환경으로 산출물 생성 | 사용한 SDK·런타임 버전과 오류 로그 |
| 렌더링 | 핵심 화면이 목표 OS에서 사용 가능 | OS, WebView/Chromium 버전, 스크린샷 |
| 권한 | 거절·취소 후에도 다음 행동이 보임 | 사용자의 막힌 단계와 문구 |
| 데이터 | 재시작·업데이트 뒤에도 초안이 보존 | 포맷 버전과 복구 방법 |
| 배포 | 설치·제거·재설치가 예상대로 동작 | 설치 경로, 서명, 남는 데이터 |
스파이크가 Tauri의 Rust 학습 난이도나 Electron의 설치물 크기만 확인하고 끝나면 의미가 작습니다. 그 비용은 이미 알려진 비용입니다. 실제로 확인해야 할 것은 팀의 디자인 시스템이 시스템 WebView에서 어떻게 보이는지, 지금 쓰는 인증·파일·오프라인 코드가 네이티브 경계에서 어떤 상태를 만드는지, 그리고 디자이너와 QA가 그 상태를 재현할 수 있는지예요.
이 기록은 다음 분기에 런타임을 바꾸더라도 버려지지 않습니다. 권한 카피, 테스트 기기 목록, API 호환 규칙, 오류 보고 형식은 Capacitor·Tauri·Electron 중 어느 것을 선택해도 제품 자산으로 남습니다. 반대로 이런 기록 없이 시작한 프로젝트는 문제가 생길 때마다 프레임워크 탓을 하게 됩니다.
출시 전에는 기능이 아니라 실패 경로를 리허설한다
앱을 마켓이나 다운로드 페이지에 올리기 전, 정상 동작만 데모하면 중요한 문제가 남습니다. 다음 표는 기획·디자인·개발이 함께 확인할 릴리스 리허설입니다.
| 장면 | 확인할 질문 |
|---|---|
| 권한 거절 | 왜 필요한지 설명하는가, 설정으로 돌아가는 길이 있는가 |
| 네트워크 단절 | 저장되지 않은 작업을 잃지 않는가, 재시도가 가능한가 |
| 앱 재시작 | 진행 중인 업로드·다운로드·초안이 어떤 상태가 되는가 |
| 업데이트 직후 | 데이터 마이그레이션 실패 시 되돌릴 수 있는가 |
| 오래된 OS | 지원 범위와 제한을 사용자에게 보여주는가 |
| 다른 WebView | 폰트·편집·미디어·드래그·키보드 조작이 의도대로인가 |
| 로그인/심사 | 심사자가 제품 핵심을 재현할 수 있는가 |
Tauri와 Electron의 데스크톱 업데이트, App Store와 Play Store의 모바일 업데이트는 같은 “새 버전”이지만 통제하는 주체가 다릅니다. 다음 글인 웹 앱의 업데이트 전략: App Store·Play Store·Tauri·Electron 배포를 어떻게 나눌까에서 이 차이를 릴리스 채널, 서명 키, 긴급 수정, 롤백까지 이어서 다룹니다.
선택은 한 번의 선언이 아니라 되돌릴 수 있는 결정이다
작은 팀이 가장 피해야 할 것은 미래의 모든 요구를 상상해 가장 복잡한 구조를 먼저 만드는 일입니다. 현재 제품의 핵심 행동 하나를 골라, 그 행동이 가장 자연스러운 플랫폼에서 끝까지 완성해보는 편이 낫습니다.
- 모바일의 카메라·푸시·공유가 핵심이면 Capacitor에서 권한과 스토어 흐름까지 완성합니다.
- 로컬 파일·창·키보드·백그라운드 작업이 핵심이면 Tauri 또는 Electron에서 설치·업데이트·복구까지 완성합니다.
- 팀이 이미 Electron을 운영하고 있다면 “새것이 더 가볍다”는 이유만으로 옮기지 말고, 해결하려는 비용을 수치와 재현 시나리오로 적습니다.
- Tauri를 고른다면 Rust를 도입했다는 사실보다, capability와 command를 얼마나 좁게 설계했는지로 품질을 판단합니다.
좋은 선택은 나중에 다른 런타임을 못 쓰게 만드는 선택이 아닙니다. 화면과 제품 규칙은 공유하되 기기 권한과 배포는 얇은 경계로 남겨, 다음 결정의 비용을 낮추는 선택입니다. 앱의 프레임워크는 사용자가 보지 않습니다. 다만 권한을 물을 때, 업데이트가 실패할 때, 오프라인에서 초안이 남아 있을 때는 그 설계가 그대로 드러납니다.