Akan.js
Docs
문서컨벤션레퍼런스Cheatsheet
Akan.js
문서컨벤션레퍼런스Cheatsheet
Akan.js

MIT 라이선스 하에 배포되었습니다.

Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
이전실시간다음Push Notifications

모바일 설정 흐름

Akan 모바일 앱은 akanjs의 자체 런타임이 iOS·Android용으로, 그리고 macOS·Windows·Linux용으로도 만들어 내는 네이티브 셸 안에서 CSR 웹 앱을 실행한 것입니다. 페이지와 비즈니스 로직은 웹 앱이 맡습니다. 패키지 ID, 기기 권한, 플러그인, 네이티브 파일, 서명, 스토어 빌드는 셸이 맡으며, 모두 akan.config.ts에 선언합니다.
이 페이지에서 쓰는 말
용어설명
네이티브 런타임
akanjs 안에 들어 있는 @akanjs/native입니다. 웹 앱을 WebView 안에서 실행하고, 플러그인으로 기기 기능을 쓰게 해 줍니다.
CSR 번들
앱을 한 페이지짜리 웹 앱으로 빌드한 결과물입니다. 네이티브 앱에 이 번들이 들어가므로 web.csr을 끄면 안 됩니다.
target
Akan 앱 하나에서 만드는 네이티브 앱 하나입니다. native.targets의 키가 곧 --target에 넘기는 값입니다.
appId
앱의 고정 ID입니다. Android에서는 package name, iOS에서는 bundle ID가 됩니다.
.akan/native/<target>
실행할 때마다 target의 웹 루트와 빌드를 쓰는 곳입니다. 생성되고 git에서 제외되며, 고칠 Xcode 프로젝트는 없습니다.
플러그인
카메라, 푸시 같은 네이티브 런타임 모듈입니다. 권한이나 native.plugins가 이름을 대면 앱에 들어갑니다.
네 단계
1. native 설정
akan.config.ts에서 앱 이름과 appId를 정하고, target과 권한을 고릅니다.
2. 네이티브 플러그인
권한이 제 플러그인을 가져오고, 그 밖의 플러그인은 native.plugins에 적습니다.
3. Android · iOS · Desktop
개발 도구를 설치하고 기기에서 앱을 띄운 뒤, 서명과 스토어 빌드를 준비합니다.
4. 확인
빌드 성공에서 멈추지 말고, 실제 기기에서 기능을 하나씩 확인합니다.
푸시 알림과 딥링크는 선택 기능입니다. 이 페이지를 끝낸 뒤, 앱에 필요할 때만 설정하세요.

native 설정

akan.config.ts의 native 블록이 네이티브 앱의 이름, ID, 버전, 권한을 정하고, 한 플랫폼만 읽는 값은 ios, android, desktop 아래에 둡니다. 네이티브 앱을 하나만 내는 앱은 이것으로 충분합니다:
apps/myapp/akan.config.ts
appNamestring기본값 앱 이름
홈 화면 아이콘 아래에 보이는 이름입니다. 스토어 목록에는 다른 이름이 보일 수 있습니다.
appIdstring기본값 com.<repo>.<app>
Android package name이자 iOS bundle ID입니다. 스토어 콘솔과 Firebase에 등록할 때도 똑같이 씁니다.
versionstring기본값 0.0.1
사용자에게 보이는 버전입니다. Android versionName과 iOS CFBundleShortVersionString에 들어갑니다.
buildNumnumber기본값 1
스토어 빌드 번호입니다. Android versionCode와 iOS CFBundleVersion에 들어가며, 스토어에 올릴 때마다 올립니다.
permissionsNativePermission[]
준비할 기기 기능입니다. camera, contacts, location, push, speech 다섯 가지뿐입니다.
pluginsstring[]
더 싣는 런타임 플러그인이며, iap 같은 내장 id나 절대 경로 폴더로 적습니다.
indexPathstring기본값 /
앱의 홈 route입니다. 딥링크는 그 위에 열리고, Android 뒤로 가기는 앱을 닫기 전에 여기로 돌아옵니다.
basePathstring
다중 클라이언트 앱에서 열 클라이언트이며, routes에 선언한 basePath입니다. basePath가 없으면 적지 않습니다.
ios{ teamId?, infoPlist?, entitlements?, privacy?, files? }
iOS 전용입니다. universal link용 팀, Info.plist·entitlements 키, 개인정보 매니페스트, 번들 파일을 적습니다.
android{ googleServices?, push?, autoplay?, files?, manifest?, … }
Android 전용입니다. FCM용 googleServices, 푸시 표시, 앱 링크 fingerprint, 파일, manifest XML을 적습니다.
desktop{ server?, recovery?, window?, screenCapture? }
데스크톱 전용입니다. server는 앱의 서버를 싣고, 나머지는 지키는 사람이 없는 앱을 계속 돌게 합니다.
targetsRecord<string, AkanNativeSettings>기본값 { default: {} }
네이티브 앱마다 항목 하나이며, 키가 곧 --target에 넘기는 이름입니다. 각각 위 필드를 받습니다.
아이콘, 스플래시 이미지, 딥링크도 native 필드입니다. 설정과 딥링크 문서를 보세요.
권한마다 들어가는 네이티브 설정
권한을 적으면 다음 실행 때 그 기능의 플러그인이 들어가고 네이티브 설정이 쓰입니다. 음성은 런타임에 아직 플러그인이 없어, 빌드가 그렇게 알리고 없이 빌드합니다. 그 권한을 맡은 lib는 자기 항목만 더합니다.
권한플러그인AndroidiOS
cameracamera없음. 시스템 카메라와 사진 선택기는 권한이 필요 없습니다카메라·사진 보관함 사용 안내 문구
contactscontacts (읽기 전용)READ_CONTACTS연락처 사용 안내 문구
locationgeolocationACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION위치 사용 안내 문구 (항상 / 사용 중)
pushpushPOST_NOTIFICATIONS와 FCM 모듈 (가 필요합니다)
앱 하나로 네이티브 앱 여러 개 만들기
한 저장소에서 고객용, 관리자용, 파트너용 앱을 따로 낸다면 basePath로 클라이언트를 나누고 targets에 앱마다 키를 하나씩 둡니다. target은 native와 같은 필드를 받고, target이 적은 값이 이깁니다. ios, android, desktop, deepLinks, updates 같은 객체는 키마다 합치고, 목록과 나머지 값은 통째로 바꿉니다. 그래서 target의 permissions는 native의 목록에 더해지지 않고 그 목록을 대신합니다. targets가 없으면 앱에는 default라는 target 하나가 있습니다. appId를 따로 정한 target은 별개의 스토어 앱이 됩니다:
apps/myapp/akan.config.ts
  • basePath는 routes에 있어야 합니다. 클라이언트를 먼저 선언하세요. 방법은 다중 클라이언트 문서에 있습니다.
  • 실제 appId를 쓰세요. example, myapp, test 같은 단어가 들어간 ID는 Apple 포털에서 대개 이미 쓰이고 있어 실기기 서명이 실패합니다. akan doctor --ios가 이런 ID를 짚어 줍니다.
  • CSR 번들을 켜 두세요. 네이티브 앱에 이 번들이 들어가므로 web: { csr: false }와 native 블록은 함께 쓸 수 없습니다.

네이티브 플러그인

런타임은 target이 요청한 플러그인만 앱에 넣습니다. 모든 Akan 페이지가 쓰는 기본 묶음은 항상 들어가고, permissions가 그 기능의 플러그인을 가져오며, 그 밖의 것은 native.plugins에 적습니다. 앱에 없는 플러그인을 호출하면 UNSUPPORTED로 거부됩니다.
패키지
항상
권한으로
permissions
이름으로
native.plugins
모든 Akan 페이지가 쓰는 것
app
✓
앱 정보, Android 뒤로 가기, 딥링크 이벤트, 앱 종료를 다룹니다.
app-state
✓
앱이 앞·뒤로 오가는 변화를 알립니다.
device
✓
플랫폼, 기종, 기기 언어를 읽습니다.
keyboard
✓
키보드 높이를 알려 주어 화면이 따라 움직이게 합니다.
preferences
✓
기기 저장소이며, 로그인 토큰을 여기에 둡니다.
secure-storage
✓
비밀 값을 두는 키체인·키스토어입니다.
browser · opener
✓
앱 안 브라우저로 페이지를 열거나, 링크를 시스템에 넘깁니다.
auth-session
✓
OAuth 로그인이 여는 시스템 로그인 창입니다.
dialog · haptics
✓
시스템 알림창·액션 시트와 진동 피드백입니다.
기능별
camera
✓
카메라와 사진 선택입니다. camera가 가져옵니다.
geolocation
✓
현재 위치와 위치 추적입니다. location이 가져옵니다.
push
✓
iOS는 APNs, Android는 FCM입니다. 가 가져옵니다.
✓해당해당 없음
기본 묶음과 권한 밖의 플러그인은 앱이 실제로 호출하는 것만 적습니다. 예를 들어 인앱 결제에는 따로 권한이 없습니다:
apps/myapp/akan.config.ts
  • package.json에는 아무것도 적지 않습니다. 플러그인은 akanjs 안에 들어 있으므로, 앱이 네이티브 패키지를 설치하거나 버전을 따로 고정하지 않습니다.
  • lib가 권한을 맡을 수 있습니다. native 블록을 가진 <name>.plugin.ts가 권한, 그 플러그인, 사용 안내 문구, Android 권한을 적으면 그 lib를 쓰는 모든 앱이 이를 받고, 그 권한의 기본 항목을 대신합니다.
  • 바꾼 뒤에는 다시 실행하세요. 권한이나 native를 바꾼 뒤에는 start-ios, start-android, 빌드 명령 중 하나를 다시 실행합니다. 앱은 매번 새로 만들어집니다.
앱이 가진 플러그인
빌트인에 없는 장치 기능(키오스크의 부팅 수신, Windows 레지스트리 설정 같은 것)은 앱의 native/ 폴더에 플러그인으로 둡니다. 플러그인 하나가 폴더 하나이고, 폴더 이름은 그 id입니다. 어디에도 적지 않습니다. 앱의 모든 타깃에 들어가고, 플랫폼마다 무엇이 도는지는 manifest가 정합니다. lib의 native/ 플러그인은 그 lib에 의존하는 앱에 들어가며, 같은 id를 앱도 가지면 앱 것이 쓰입니다.
apps/myapp/native/kiosk
definePlugin은 akanjs/client/native에서, defineDesktopPlugin은 akanjs/native/desktop에서 가져옵니다. 런타임 패키지 자체는 앱의 작업 공간에 설치되지 않습니다. 플러그인 API는 webkit/ 훅이 ../native/kiosk/src에서 가져오고, 페이지는 그 훅을 부릅니다.

Android 설정

Android 앱을 에뮬레이터나 폰에서 띄우는 과정입니다. 꼭 맞춰야 할 값은 하나입니다. native.appId가 Android의 applicationId가 됩니다.
준비물
  • build-tools 35 이상이 설치된 Android SDK, 그리고 에뮬레이터나 USB 디버깅을 켠 폰. Android Studio가 둘 다 설치해 줍니다.
  • JDK 17 이상. Android Studio에 들어 있는 것을 쓰며, 다른 JDK는 JAVA_HOME으로 고릅니다. Kotlin 컴파일러는 첫 빌드 때 받아 옵니다.
  • com.acme.shop처럼 바뀌지 않을 native.appId.
기기에서 실행하기
  1. SDK가 ~/Library/Android/sdk에 없다면 터미널이 그 위치를 쓰도록 설정합니다:
    Terminal
  2. native.appId가 확정됐는지 확인하고(위 native 설정 참고) 개발 서버를 켭니다. --release 없이 실행하면 앱이 이 서버에서 화면을 불러옵니다:
    Terminal
  3. 두 번째 터미널에서 에뮬레이터나 연결한 폰으로 앱을 실행합니다:
    Terminal
  4. 앱이 target의 indexPath로 열리고, 저장하면 다시 빌드하지 않아도 반영되면 성공입니다. 폰은 USB 연결로 개발 서버에 닿습니다.

iOS 설정

bundle ID, 서명, 시뮬레이터 실행, 스토어 빌드를 준비하는 과정입니다. Xcode 프로젝트는 없습니다. 런타임이 Xcode의 도구로 앱을 컴파일하고, Xcode가 둔 서명을 읽습니다. 먼저 시뮬레이터에서 실행하고, 기기 전용 기능은 폰에서 확인합니다.
준비물
  • Xcode 26 이상과 iOS 26 시뮬레이터 런타임 (Xcode › Settings › Components).
  • bundle ID로 쓸, 바뀌지 않을 native.appId.

데스크톱

같은 target은 데스크톱 앱으로도 실행되며, 브라우저 밖에서 변경을 가장 빨리 확인하는 방법입니다. akan start-desktop은 데스크톱 앱이 자기 OS에서만 빌드되므로 지금 컴퓨터의 OS용으로 빌드하고, 폰 명령처럼 akan start에서 화면을 불러옵니다:
Terminal
rustup으로 설치한 Rust(빌드가 고정한 toolchain을 rustup이 받습니다)와 OS별 네이티브 빌드 도구가 필요합니다:

설정 확인

빌드 성공이 끝이 아닙니다. 실제 기기에서 플러그인이 불러와지는지, 네이티브 파일이 제자리에 있는지, 권한 창이 뜨는지, 푸시가 도착해 맞는 화면을 여는지 확인하세요.
증상
↳ 확인할 것
camera.takePhoto() is not supported on ios
target에 그 플러그인이 들어 있지 않습니다. 권한을 적거나 native.plugins에 이름을 적고 다시 실행합니다.
No dev server answers on …
개발 빌드는 akan start myapp에서 화면을 불러옵니다. 먼저 켜거나 --release를 줍니다.
저장하면 그 자리에서 바뀌지 않고 앱 전체가 다시 뜸
컴포넌트·store·페이지·레이아웃 수정은 state를 유지한 채 그 자리에서 바뀌고, *.constant.ts가 바뀌거나 라우트가 추가·삭제되거나 npm 의존성이 새로 들어오면 다시 뜹니다. akan start에 AKAN_DEV_CSR=artifact를 주면 저장할 때마다 다시 뜨는 예전 단일 파일 dev 번들로 돌아갑니다.
권한 창이 뜨지 않거나, iOS에서 처음 쓸 때 앱이 꺼짐
permissions에 기능을 적고 다시 실행해, 사용 안내 문구와 네이티브 설정이 들어가게 합니다.
네이티브 파일이 없음
ios.files의 키는 앱 번들 안 경로, 의 키는 나 이고, 값은 앱 폴더 기준 경로입니다.

이 페이지

모바일 설정 흐름
native 설정
네이티브 플러그인
Android 설정
iOS 설정
데스크톱
설정 확인
명령과 스토어 빌드
명령기본 --env
↳ 결과
start-androidlocal
에뮬레이터나 폰에서 실행합니다. --release를 주면 개발 서버 대신 웹 빌드를 넣습니다.
build-androiddebug
앱이 빌드되는지 확인하는, 로컬 debug 키로 서명한 APK를 만듭니다.
release-androidmain
업로드 키로 서명한 Play Store용 AAB를 만듭니다. --assemble-type apk면 APK입니다.
앱이 빌드되는지 확인한 뒤, Play Store에 올릴 AAB를 main 백엔드로 만듭니다:
Terminal
release-android는 환경 변수에서 읽은 업로드 키로 서명합니다. 이름 세 개를 두고, 키에 비밀번호가 따로 있으면 네 번째도 둡니다. 하나라도 없으면 빌드 전에 멈춥니다:
Terminal
  • MYAPP_RELEASE_STORE_FILE은 앱 폴더 기준 경로입니다. keystore 파일은 secrets/에 두고, public/에는 두지 마세요.
  • 비밀번호는 git에 올리지 마세요. CI에서는 같은 이름을 secret으로 넣습니다. 비밀번호는 명령줄이나 로그가 아니라 환경 변수로만 서명 도구에 전달됩니다.
  • 결과물 위치. apps/myapp/.akan/native/default/build/android에 생기며, release-android가 경로를 출력합니다.
모바일 명령 플래그
--targetstring
native.targets의 키나 all입니다. target이 하나뿐이면 자동으로 고르며, start-*는 한 번에 하나만 실행합니다.
--envlocal | debug | develop | main
앱이 연결할 백엔드 환경입니다. 기본값은 위 표처럼 명령마다 다릅니다.
--releaseboolean기본값 falsestart-*
웹 빌드를 담은 릴리스 빌드로 실행하므로 개발 서버가 필요 없습니다.
--devicestringstart-iosstart-android
시뮬레이터, 에뮬레이터, 기기의 id나 이름입니다. 페어링한 iPhone 이름을 주면 서명한 폰 빌드를 만듭니다.
-T, --teamstringstart-iosrelease-ios
Mac에 여러 팀의 프로필이 있을 때 서명에 쓸 Apple 팀 id입니다.
--debugboolean기본값 falsebuild-*
릴리스 대신 디버그 빌드를 만듭니다.
--ad-hocboolean기본값 falserelease-ios
App Store 프로필 대신 ad-hoc 프로필로 서명합니다.
--assemble-typeaab | apk기본값 aabrelease-android
aab는 Play Store 업로드용, apk는 파일을 직접 설치할 때 씁니다.
-l, --allow-local-releaseboolean기본값 falserelease-*
릴리스 빌드에서 --env local을 허용합니다. 로컬 테스트용입니다.
  • 폰 실행과 출시에 쓸 Apple 개발자 팀.
  • 기기에서 실행하기
    akan start myapp을 켜 둔 채 시뮬레이터에서 앱을 실행하거나, 페어링한 iPhone 이름을 줍니다:
    Terminal
    • --device로 기기를 고릅니다. 시뮬레이터 이름이나 UDID, 페어링한 iPhone 이름을 받습니다. 생략하면 켜져 있는 iPhone 시뮬레이터를 쓰고, 없으면 가장 최신 것을 띄웁니다.
    • 서명은 만들지 않고 찾습니다. 폰 실행과 출시는 맞는 인증서와 프로필을 골라 쓰고, 무엇을 썼는지 출력합니다. 여러 팀이 맞으면 --team으로 고릅니다.
    • 맞는 것이 없을 때. 오류가 그 bundle ID의 프로필마다 맞지 않는 이유를 보여 줍니다. 만료, 다른 팀, 푸시 같은 capability 누락, 폰이 빠진 프로필 등입니다.
    서명에서 확인할 것
    1. Xcode(Settings › Accounts)에서 팀에 로그인하고 프로필을 내려받아, Mac에 Apple Development 인증서와 이 App ID의 프로필이 있게 합니다. 런타임은 Xcode의 프로필 폴더를 읽으며, 따로 열 프로젝트는 없습니다.
    2. 프로필의 App ID가 native.appId와 같아야 합니다. 와일드카드 ID는 앱이 푸시도 associated domains도 요청하지 않을 때만 씁니다.
    3. 폰에서 실행하려면 development 프로필에 그 폰이 들어 있어야 합니다. 폰을 한 번 등록하고(Xcode에서 그 폰으로 빌드하거나 개발자 사이트에서 추가) 프로필을 다시 내려받습니다. 출시에는 Apple Distribution 인증서와 App Store(또는 ad-hoc) 프로필이 필요합니다.
    4. 먼저 시뮬레이터에서 실행하고, 기기 전용 기능은 폰으로 옮겨 확인합니다.
    명령과 스토어 빌드
    명령기본 --env
    ↳ 결과
    start-ioslocal
    시뮬레이터나 폰에서 실행합니다. --release를 주면 개발 서버 대신 웹 빌드를 넣습니다.
    build-iosdebug
    앱이 빌드되는지 확인하는 시뮬레이터용 앱을 만듭니다.
    release-iosmain
    App Store용으로 서명한 iPhone 앱과 그 .ipa를 만듭니다.
    앱이 빌드되는지 확인한 뒤, App Store용 빌드를 main 백엔드로 만듭니다. release-ios는 기본으로 main을 씁니다:
    Terminal
    OS
    설명
    macOS
    Xcode command line tools (xcode-select --install).
    Windows
    Visual Studio 2022 Build Tools의 "Desktop development with C++".
    Linux
    C 컴파일러, pkg-config, WebKitGTK 4.1·GTK 3·libsoup 3 개발 패키지.
    데스크톱 앱에 앱의 서버를 넣을 수도 있습니다. 다른 곳에 백엔드 없이 컴퓨터 한 대에서 동작합니다. native에 desktop: { server: true }를 주면(한 타깃에만 주면 그 앱만 서버를 싣습니다) akan start-desktop은 떠 있는 개발 서버가 없을 때 같은 명령에서 akan start를 띄우고, akan build-desktop과 akan publish-update는 서버를 넣은 앱을 빌드합니다. 이 서버는 창과 함께 loopback 포트로 떠서 API만 서빙하고, SQLite 데이터를 앱 데이터 폴더의 server/에 둡니다(Windows는 %LOCALAPPDATA% 아래, --debug 빌드는 따로 server-debug/). 운영체제가 믿는 인증서를 믿고, 페이지처럼 사용자 세션의 프록시 변수를 따릅니다. 이 컴퓨터의 다른 프로그램도 그 포트를 부를 수 있으므로, 엔드포인트는 네트워크 서버처럼 가드합니다. 포트는 대개 지난번과 같지만 보장되지 않습니다. 그래서 redirect URI가 정확히 같아야 하는 로그인 공급자는 앱에 넣은 서버가 아니라 클라우드 서버의 adapter로 받습니다. 설치된 앱은 서버를 더하거나 빼는 업데이트를 받지 않으므로, 이미 배포한 앱에서 바꾸려면 다시 설치해야 합니다. 다시 설치해도 데이터는 옮겨지지 않습니다. 서버를 더하면 앱이 빈 로컬 데이터베이스로 시작하고, 빼면 페이지가 빌드에 적힌 백엔드를 부릅니다.
    basePath가 없는 앱은 basePath를 적지 않고, 앱을 하나만 내면 targets도 필요 없으므로 서버를 싣는 가장 짧은 설정은 이렇습니다. 첫 페이지가 /가 아니면 desktop 옆에 indexPath를 더합니다:
    apps/myapp/akan.config.ts
    Terminal
    서버를 넣으려면 database.modes에 single이 있어야 합니다. 앱에는 서버의 private/ 폴더(lib의 것도 private/libs/<lib>로), 빌드할 때의 --env에 해당하는 env.server.<env>.ts 하나, 앱이 쓰는 lib이 서버 env로 내보내는 기본값(lib의 env.server.testing.ts)이 평문으로 실립니다. 앱을 가진 사람은 누구나 그 파일과 값을 모두 읽을 수 있으니 클라우드 키 같은 배포용 비밀과 라이선스 파일은 두지 마세요. 앱에 넣은 서버에는 public/이 없고 작업 폴더는 데이터 폴더이므로, 실행 중에 읽는 파일은 process.cwd()가 아니라 앱 폴더(AKAN_APP_DIR, 없으면 Bun.main의 폴더) 기준으로 읽습니다.
    start-desktop은 개발과 테스트용이고, build-desktop은 이 컴퓨터용 앱을 만듭니다. macOS는 ad hoc 또는 개발용 인증서로 서명하고, Windows와 Linux는 서명하지 않습니다. 배포 서명과 공증은 아직 akan 명령에 없고, Windows에서는 --installer가 서명하지 않은 현재 사용자용 설치 프로그램을 만듭니다.
    앱에 넣은 서버에는 이미지의 docker 단계가 하나도 들어가지 않습니다. 서버나 네이티브 플러그인이 실행하는 ffmpeg 같은 실행 파일은 akan.config.ts의 bin에 적습니다. 플랫폼마다 sha256로 확인하는 다운로드나 설정 파일 옆의 파일을 적습니다. 서버가 있든 없든 모든 데스크톱 빌드·실행·개발이 이 파일을 싣습니다. 빌드가 이 컴퓨터용 파일을 앱에 넣고 그 폴더를 앱의 PATH 맨 앞에 두므로, 서버의 spawn("ffmpeg")가 그 파일을 실행하고 사용자는 아무것도 설치하지 않으며, 네이티브 플러그인은 ctx.binDir에서 찾습니다. 설치하면서 스스로 빌드하는 패키지는 trustedDependencies에 적습니다.
    akan.config.ts
    정적 LGPL 빌드를 넣으세요. 공유 라이브러리를 따로 불러오는 빌드는 만든 컴퓨터에서만 돌고, --enable-nonfree로 빌드한 것(npm ffmpeg-static이 받는 macOS 파일)은 재배포할 수 없습니다.
    사용자가 고른 파일은 복사본이나 경로가 아니라 허가(grant)로 서버에 전달됩니다. 그래서 수 GB 영상도 복사하거나 업로드하지 않습니다. native.plugins에 file-picker를 추가하고 forServer: true로 고른 뒤, grant를 엔드포인트에 넘기면 서버가 NativeFile로 경로를 받습니다. 서버는 사용자가 고른 파일만 얻습니다.
    page → server
    장치는 서버가 아니라 네이티브 플러그인이 다룹니다. 디스플레이와 그 변경(screen), 디스플레이에 놓는 창(window), 시스템 볼륨과 음소거(volume, Android는 미디어 볼륨), 전역 단축키, 절전 막기, 로그인 시 실행이 있습니다. 각각 native.plugins에 추가합니다. 빌트인 플러그인의 API는 모두 akanjs/client/native/<id>(akanjs/client/native/window, …/screen)에서, volume과 filePicker는 akanjs/client/native에서도 가져옵니다.
    지키는 사람이 없는 앱
    키오스크나 전광판에는 새로고침을 누를 사람이 없습니다. desktop.recovery: "reload"는 프로세스가 끝난 페이지를 매번 다시 불러오되 연달아 끝날수록 오래 기다리고, webview 브라우저 프로세스가 끝나면 앱을 다시 띄웁니다. desktop.window는 주 창을 첫 프레임부터 전체화면, 작업 표시줄 버튼 없이 열고, app.relaunch()는 데스크톱과 Android에서 앱을 새 프로세스로 다시 시작합니다. Windows에서 원격 지원을 하려면 desktop.screenCapture: "auto"가 getDisplayMedia()에 선택 창도 터치도 없이 첫 화면으로 답합니다. 모든 미디어 요청에 적용되므로 카메라나 마이크를 요청하는 앱에서는 켜지 않습니다.
    apps/board/akan.config.ts
    Windows에 설치하기
    Windows에서 akan build-desktop myapp --installer true --env main을 실행하면 앱 폴더 옆에 설치 프로그램이 생깁니다(NSIS: winget install NSIS.NSIS). 현재 사용자로 설치하므로 업데이트가 관리자 권한 없이 앱을 바꿉니다. /S는 무인 설치, /RUN은 설치 뒤 실행으로, 원격 설치가 넘기는 인자입니다. WebView2 Runtime이 없는 PC에는 함께 설치합니다. /D= 없이 다시 실행하면 앱이 이미 있는 폴더에 설치하고, 다른 설치 프로그램이 도는 동안 띄운 것은 시작하지 않습니다. 아직 코드 서명이 없어서, 브라우저로 받은 파일은 SmartScreen 경고를 만납니다.
    빌드는 그것을 실행하는 Bun의 CPU를 따릅니다. 그래서 ARM64 Windows에서도 rustup target add x86_64-pc-windows-msvc 뒤 x64 Bun(bun-windows-x64-baseline, AVX2가 없는 CPU에서도 도는 빌드)으로 akan을 실행하면 x64 PC용 앱이 나옵니다.
    업데이트
    설치된 앱은 직접 서명한 릴리스로 스스로 업데이트합니다. akan update-keygen이 키를 한 번 만들고 native.updates에 넣을 공개 키를 출력합니다. akan publish-update는 릴리스(데스크톱은 앱 전체, 폰은 웹 번들)를 .akan/native/<target>/updates에 빌드합니다. 그 폴더에는 updates.url에 올릴 것만 있으며, manifest를 마지막에 올립니다. 새 릴리스는 첫 페이지가 마운트될 때까지 시험 실행입니다. 폰은 시작할 때와 앞으로 돌아올 때마다 새 웹 번들을 스스로 찾아 받고 다음 콜드 스타트부터 씁니다. 데스크톱은 릴리스가 앱 전체이고 재실행이 따르므로 언제 확인·다운로드·적용할지는 앱이 정합니다. akan pack-update는 키를 다른 곳에 두는 서명자를 위해 폰 업데이트를 서명 없이 씁니다.
    updates.url에 닿는 누구나 그 아래의 모든 것을 읽을 수 있고, 업데이터는 인증 정보를 보내지 않습니다. 데스크톱 릴리스는 앱 전체이므로 앱에 넣은 서버의 private/(앱과 lib의 것)와 env 파일도 들어 있습니다. 설치된 앱에도 두면 안 되는 것은 여기에도 두지 마세요.
    앱은 updates.channel이 정한 채널을, 없으면 빌드할 때의 --env를 따릅니다. 그래서 updates.channel이 없으면 자기 env로 게시한 릴리스만 받습니다. build-desktop의 기본값은 debug, publish-update는 main이므로 둘에 같은 --env를 줍니다. publish-update의 --channel은 쓸 매니페스트 이름만 정합니다. 안에 든 릴리스는 빌드할 때의 채널을 그대로 가지므로 받은 앱은 그 뒤로 그 채널을 따릅니다. 그래서 pilot 그룹에는 updates.channel이 pilot인 타깃을 따로 둡니다. publish-update는 채널의 직전 릴리스와 서버 유무가 다른 데스크톱 릴리스를 빌드하기 전에 거부합니다. updates.channel로 다른 채널에 게시하거나, 출력 폴더에서 그 <channel>.json을 지워 채널을 새로 시작합니다. 설치 폴더, 제거 항목, 데이터 폴더, 한 번에 하나만 뜨는 인스턴스, 업데이트 상태처럼 데스크톱 앱을 그 앱이게 하는 것은 env가 아니라 타깃의 appId와 이름에서 나옵니다. 그래서 한 타깃의 두 env를 한 컴퓨터에 두면 이것을 모두 함께 씁니다. 나란히 설치하려면 env마다 appId가 다른 타깃을 따로 둡니다.
    릴리스가 시험 실행인 동안 updates.check()는 그 릴리스에 available: false로 답하고, updates.apply()는 NOT_ALLOWED로 거부합니다. 적용하면 시험 실행이 실패했을 때 돌아갈 앱을 바꾸기 때문입니다. 서버를 싣는 앱은 그 서버가 응답하고 5초 동안 떠 있어야, 그리고 그때 떠 있어야 시험 실행을 확정하며, readyTimeout 시계도 그때 시작합니다. 서버가 포기하거나 시작하고 120초 안에 뜨지 않으면 곧바로 되돌립니다. 그 릴리스는 받은 채로 남아 다음 적용 때 다시 시도하고, 이렇게 세 번째 실패하면 더는 받지 않습니다. 서버를 더하거나 빼는 릴리스는 매니페스트만 보고 아무것도 받기 전에 거부하며, 앱을 다시 설치하면 이전 설치가 거부한 릴리스 목록이 비워집니다.
    webkit/useAppUpdates.tsx
    CDN으로 서빙할 때 app/과 files/ 아래 파일은 해시로 이름이 붙으므로 오래 캐시해도 됩니다. 하지만 <channel>.json과 <channel>.json.sig는 캐시하지 않거나 둘을 함께 무효화해야 합니다. manifest가 다른 릴리스의 서명과 짝지어지면 검증에 실패하고, 캐시가 만료될 때까지 모든 앱이 업데이트를 멈춥니다. app/과 files/를 먼저 올리고, 두 파일은 마지막에 함께 올립니다.
    android.files
    res/…
    assets/…
    알림을 누르면 엉뚱한 화면이 열림
    데이터에 url: "/some/path"를 넣어 보내고, 누르면 그 CSR route가 열리는지 확인합니다.
    플랫폼별 푸시 점검
    Android 푸시
    • package name이 Firebase에 등록한 Android 앱과 같습니다.
    • native.android.googleServices가 그 앱의 google-services.json을 가리킵니다.
    • 폰에서 알림 권한을 허용했습니다.
    • 서버의 Firebase 인증 정보가 같은 프로젝트의 것입니다.
    iOS 푸시
    • 실기기에서 테스트합니다.
    • 프로필이 푸시를 허용합니다. aps-environment는 프로필을 따라 폰 실행에서는 development, 출시에서는 production입니다.
    • 서버에 이 bundle ID의 APNs 키(팀 ID, 키 ID, .p8 파일)가 있습니다. iOS 푸시는 Firebase를 거치지 않습니다.
    다음 단계
    푸시 알림→
    플랫폼별 APNs, FCM 설정과 클라이언트 API입니다.
    딥링크→
    커스텀 URL scheme과 검증된 HTTPS 앱 링크입니다.
    native 필드 전체→
    아이콘, 스플래시 이미지, 파일, ios·android·desktop 섹션까지 모두 봅니다.
    CLI 레퍼런스→
    모바일 명령의 모든 플래그입니다.
    native.android.googleServices
    원격 알림 백그라운드 모드와 aps-environment entitlement
    speech아직 없음. 없이 빌드됩니다lib 플러그인이 선언한 것lib 플러그인이 선언한 것
    출시한 뒤에는 appId를 바꾸지 마세요. Android와 iOS는 appId가 다르면 완전히 다른 앱으로 봅니다.
    push
    iap
    ✓
    인앱 결제입니다. StoreKit 2와 Play Billing을 씁니다.
    share · biometric · …
    ✓
    런타임의 다른 플러그인은 id로, 직접 만든 플러그인 폴더는 절대 경로로 적습니다.