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테스트
    이전설정다음Deep Links

    푸시 설정

    권한 창이 뜨고, 토큰이 돌아오고, 서버 로그에는 발송이 찍힙니다. 그런데 폰에는 아무것도 오지 않습니다.
    푸시는 클라이언트 API usePushNotification() 하나와 서버의 발송기 둘로 이루어집니다. iOS는 APNs, Android와 웹은 FCM입니다. 발송기 인증 정보가 없는 토큰은 로그 한 줄만 남기고 건너뛰니, 아래 표에서 해당하는 줄을 빠짐없이 준비하세요.
    이 페이지에서 쓰는 말
    용어설명
    FCM
    Firebase Cloud Messaging입니다. Akan은 Android 앱과 브라우저에 이것으로 보냅니다.
    APNs
    Apple의 푸시 서비스입니다. 서버가 iOS 앱에 이것으로 직접 보내며, Firebase는 거치지 않습니다.
    푸시 토큰
    앱 설치 하나의 주소입니다. register()가 이 주소로 배달하는 provider와 함께 돌려줍니다.
    provider
    iOS는 apns, Android와 웹은 fcm입니다. 서버는 이 값으로 발송기를 고릅니다.
    deviceId
    앱이 자기 저장소에 두는 임의의 설치 id입니다. 토큰이 바뀌면 이전 토큰을 대신합니다.
    VAPID 키
    웹 푸시용 키 쌍입니다. 공개 키를 client env의 vapidKey에 넣습니다.
    서비스 계정
    서버가 FCM에 발송할 때 쓰는 Firebase Admin 인증 정보입니다. 클라이언트로 가지 않습니다.
    APNs 인증 키
    서버가 APNs 요청에 서명하는 .p8 키입니다. 키 하나로 두 APNs 환경을 모두 씁니다.
    aps-environment
    앱의 토큰이 APNs development와 production 중 어느 쪽 것인지 정하는 iOS entitlement입니다.
    준비할 것
    항목
    웹
    Android
    iOS
    콘솔에서
    Firebase 앱
    ✓
    ✓
    Firebase 프로젝트 하나에 웹 앱과 Android 앱을 등록합니다.
    VAPID 키
    ✓
    Firebase의 Cloud Messaging 설정에서 만드는 Web Push 인증서 키 쌍입니다.
    Push 기능
    ✓
    Apple Developer의 App ID에서 Push Notifications를 켭니다. 그래야 프로파일에 entitlement가 들어갑니다.
    APNs 인증 키 (.p8)
    ✓
    Apple Developer의 Keys에서 만들고, Key ID와 Team ID를 함께 적어 둡니다. 서버에 넣습니다.
    앱 폴더에
    env.client.*
    ✓
    firebase 아래에 공개 Firebase 웹 설정과 vapidKey를 넣습니다.
    google-services.json
    ✓
    Android용 Firebase 설정 파일이며, akan.config.ts의 native.android.googleServices로 지정합니다.
    permissions: ["push"]
    ✓
    ✓
    네이티브 푸시 플러그인을 넣습니다. akan.config.ts의 native에 적습니다.
    서버에
    pushNoti.firebase
    ✓
    ✓
    env.server.*에 둡니다. 서버가 FCM에 발송할 때 쓰는 서비스 계정입니다.
    pushNoti.apns
    ✓
    env.server.*에 둡니다. APNs 키와 Key ID, Team ID, 앱의 bundle id입니다.
    ✓필요필요 없음

    네이티브 플러그인 하나

    네이티브 앱의 푸시는 런타임의 push 플러그인이 맡고, native의 permissions: ["push"]만으로 들어갑니다. 설치할 패키지는 없습니다. 플러그인은 플랫폼마다 그 플랫폼의 서비스를 씁니다:
    iOS · APNs
    Firebase SDK 없이 APNs에 직접 등록합니다. 알림 탭과 앱이 앞에 있을 때 온 메시지는 셸의 알림 라우터로 들어옵니다.
    push.register() → { provider: "apns" }
    Android · FCM
    런타임과 함께 고정된 FCM 모듈입니다. 빌드가 google-services.json을 직접 읽으므로 Gradle 플러그인이 필요 없습니다.
    push.register() → { provider: "fcm" }
    usePushNotification()이 그 차이를 감춥니다. 네이티브 셸에서는 플러그인을, 브라우저에서는 Firebase를 부르고, 어느 쪽이든 같은 모양의 PushToken을 돌려줍니다. 어떤 권한이 어떤 플러그인을 넣는지는 설정 문서에서 다룹니다.

    웹 푸시

    웹 푸시에는 네이티브 프로젝트가 전혀 필요 없습니다. Firebase 웹 앱을 등록하고, 공개 설정값을 client env에 옮기면 됩니다.
    1. Firebase Console에서 웹 앱을 만들거나 기존 웹 앱을 엽니다.
    2. 공개 설정값을 env.client.*의 firebase 아래에 넣습니다.
    3. Web Push 인증서 키 쌍을 만들고, 공개 키를 vapidKey에 넣습니다.
    그러면 client env 파일은 이렇게 됩니다:
    apps/myapp/env/env.client.local.ts
    • 공개해도 되는 값만 넣습니다. env.client.*는 브라우저로 전달됩니다. 서버의 서비스 계정은 env.server.*에 둡니다.
    • 필수 필드는 네 개입니다. apiKey, projectId, messagingSenderId, appId 중 하나라도 없으면 웹에서 register()가 undefined를 돌려줍니다.
    • 환경마다 파일이 하나씩 있습니다. env.client.ts가 AKAN_PUBLIC_ENV에 따라 env.client.<env>.ts를 고르므로, 배포하는 모든 환경에 채워 둡니다.
    • 서비스 워커는 생성됩니다. client env에 firebase가 있으면 akan sync가 환경마다 public/firebase-messaging-sw.js를 써 줍니다.

    Android 푸시

    Android 푸시는 패키지 이름이 native.appId와 정확히 같은 Firebase Android 앱 등록, 그리고 native.android.googleServices가 지정하는 설정 파일 하나로 끝납니다.
    1. Firebase Console에서 프로젝트를 엽니다.
    2. Android 앱을 추가합니다.
    3. native.appId와 같은 패키지 이름을 입력합니다.
    4. google-services.json을 내려받습니다.
    5. apps/myapp/secrets/google-services.json에 둡니다.
    그리고 akan.config.ts의 native.android에서 지정합니다:
    apps/myapp/akan.config.ts
    • 빌드가 파일을 직접 변환합니다. 패키지 이름이 타깃의 appId인 client를 고르고(디버그 빌드도 그것을 씁니다), 그 앱이 없는 파일이면 들어 있는 이름을 알려 주며 빌드를 멈춥니다.
    • public/이 아니라 secrets/에 둡니다. public/의 파일은 모든 방문자에게 그대로 제공됩니다. secrets에 등록한 파일은 git에서 빠지고, akan upload-env와 akan download-env로 함께 옮겨집니다.
    • permissions: ["push"]가 앱에 푸시 플러그인과 POST_NOTIFICATIONS 권한을 넣습니다.
    google-services.json은 서버 인증 정보가 아닙니다. Android 앱용 Firebase 설정 파일이지, Firebase Admin 서비스 계정 JSON이 아닙니다. 서버 인증 정보는 마지막 섹션처럼 env.server.*에 둡니다.
    Android 알림 표시 설정
    알림이 어떻게 보이는지는 앱이 앞에 있는지에 따라 다릅니다:
    • 포그라운드. 앱이 열려 있을 때 온 푸시도 보이도록 프레임워크가 플러그인에 요청합니다(배너, 목록, 소리, 배지). 그래서 다른 알림처럼 누를 수 있습니다.
    • 백그라운드. 앱이 앞에 없으면 FCM이 알림을 직접 그립니다. 누르면 앱이 열리고 푸시의 url로 이동합니다.
    • 채널, 아이콘, 색. Firebase는 기본 채널에 런처 아이콘으로 올리고, 상태 표시줄은 그 아이콘을 회색 사각형으로 그립니다. native.android.push로 channel({ id, name, importance? }), smallIcon(앱 폴더 안의 흰색·투명 PNG), 강조 color를 대신 정합니다.

    iOS 푸시

    iOS 푸시에는 Firebase가 전혀 필요 없습니다. 앱은 APNs에 등록하고, 서버도 APNs로 직접 보냅니다. 직접 챙길 것은 App ID의 기능 설정과 서버가 서명할 키입니다.
    1. Apple Developer의 Identifiers에서 native.appId와 같은 App ID를 골라 Push Notifications를 켭니다.
    2. Keys에서 Apple Push Notifications service를 켠 키를 만들고 .p8을 내려받습니다. 한 번만 받을 수 있으니, Key ID와 Team ID도 함께 적어 둡니다.
    3. 셋을 서버의 pushNoti.apns에 넣습니다. 마지막 섹션에서 봅니다.
    4. native에 permissions: ["push"]를 추가합니다.
    설정에는 그 밖에 더 넣을 것이 없습니다:
    apps/myapp/akan.config.ts
    • GoogleService-Info.plist도, firebase-ios-sdk도 필요 없습니다. UIBackgroundModes와 aps-environment는 푸시 플러그인이 앱에 직접 넣습니다.
    • iOS 토큰은 APNs 기기 토큰입니다. provider: "apns"로 오며, FCM은 이 토큰을 받지 않으므로 서버가 직접 APNs로 보냅니다.
    • xcrun simctl push에는 서버가 필요 없습니다. 시뮬레이터에 payload를 바로 넘겨 탭과 라우팅을 시험합니다. 서버처럼 url은 aps 옆, 최상위에 둡니다.

    빌드한 APNs 환경 확인하기

    aps-environment는 직접 쓰지 않습니다. 푸시 플러그인이 development를 선언하고, 프로비저닝 프로파일로 서명한 빌드는 프로파일의 값을 씁니다. 이 값이 기기 토큰이 어느 APNs 환경의 것인지를 정합니다.
    명령aps-environment용도
    akan start-iosdevelopment시뮬레이터와 development로 서명한 iPhone 실행이며, APNs 샌드박스를 씁니다.
    akan build-iosdevelopment시뮬레이터 빌드입니다.
    akan release-iosproductionApp Store 프로파일입니다. TestFlight와 App Store에 씁니다.
    akan release-ios --adHocproductionad hoc 프로파일입니다.
    • 서버가 두 곳을 모두 시도합니다. environment를 비워 두면 production에 먼저 보내고, APNs가 BadDeviceToken으로 답하면(development 빌드의 토큰) 샌드박스로 보냅니다. 하나로 고정하려면 environment를 적습니다.
    • 키 하나로 둘 다 됩니다. APNs 인증 키는 환경에 묶이지 않으므로, development 실행과 TestFlight 빌드에 서버 설정을 따로 둘 필요가 없습니다.
    • 어느 환경도 모르는 토큰은 지웁니다. 410이나, 마지막으로 시도한 환경의 BadDeviceToken이면 그 토큰을 주인에게서 지웁니다.

    클라이언트 등록

    libs/shared를 쓰는 앱은 직접 짤 코드가 없습니다. 로그인한 사용자의 레이아웃에 Notification.Zone.Initialize를 한 번 둡니다. 방문할 때마다, 그리고 네이티브 셸이 토큰을 바꿀 때마다 기기를 다시 등록하며, 권한은 묻지 않습니다.
    apps/myapp/page/(user)/_layout.tsx
    권한 요청은 사용자 동작에서 해야 합니다. 동작 없이 요청하면 Chrome은 무시하고 iOS는 거절합니다. Notification.Util.PushSetting이 그 스위치입니다. 직접 만든 버튼이라면 register()를 부르고 받은 PushToken을 스토어에 넘깁니다:
    apps/myapp/ui/EnablePush.tsx
    registerPushToken은 libs/shared에 들어 있습니다. 쓰지 않는다면 PushToken을 직접 만든 엔드포인트에 넘기면 됩니다. 필드는 다음 섹션의 DeviceToken과 하나씩 맞습니다.
    usePushNotification()이 돌려주는 것
    @libs/util/webkit에서 가져옵니다. 대부분의 화면은 register()만 있으면 됩니다.
    메서드설명
    register()
    권한을 요청한 뒤 PushToken을 돌려줍니다. 거부되거나 지원하지 않으면 undefined입니다.
    getToken()
    묻지 않고 토큰을 돌려줍니다. 등록 자체는 창을 띄우지 않으므로 먼저 getPermission()을 확인합니다.
    getPermission()
    현재 권한 상태를 읽습니다.
    requestPermission()
    권한 요청 창을 띄우고 결과를 돌려줍니다.
    isSupported()
    지금 푸시를 쓸 수 있는지 알려 줍니다. 셸에서는 네이티브 플러그인, 브라우저에서는 Firebase 웹 설정을 봅니다.
    onTokenChange(listener)
    네이티브 토큰은 저절로 바뀝니다. 바뀔 때마다 새 PushToken을 리스너에 넘기고, 해제 함수를 돌려줍니다.
    initClickBridge()
    • PushToken에는 token, platform(web | android | ios), provider(apns | fcm), 그리고 getPushDeviceId()가 앱 저장소에 두는 설치 id인 deviceId가 들어 있습니다.
    • 저장은 내장입니다. libs/shared를 쓰면 st.do.registerPushToken(pushToken)이 로그인한 사용자에게 저장합니다. 어디에 두는지는 다음 섹션에서 봅니다.
    • 클릭 라우팅. url을 보내면 탭했을 때 CSR router로 그 경로를 엽니다. 네이티브 셸에서는 앱을 띄운 탭까지 포함해 프레임워크가 부팅 때부터 라우팅하고, 브라우저에서는 서비스 워커가 열린 탭에 넘깁니다. 앱 안의 경로만 따라갑니다.

    토큰이 저장되는 곳

    libs/shared는 기기마다의 토큰을 그 주인에게 둡니다. user.notiInfo.deviceTokens에 설치 하나당 DeviceToken 하나씩입니다. secret 필드라 서버 밖으로 나가지 않습니다.
    푸시 토큰 생명주기
    예예
    클라이언트: register()
    서버: addNotiDeviceTokenOfSelf
    user.notiInfo.deviceTokens
    서버: push(userIds)
    수신 설정이 받는가?
    provider별 sendEach
    APNs
    FCM
    사용자 기기
    사라진 토큰인가?
    서버: 토큰 삭제
    클라이언트: register()
    서버: addNotiDeviceTokenOfSelf
    user.notiInfo.deviceTokens
    서버: push(userIds)
    수신 설정이 받는가?
    예
    deviceToken.constant.ts
    스칼라에는 register()가 돌려준 값과, 서버가 저장한 시각을 담습니다:
    libs/shared/lib/__scalar/deviceToken/deviceToken.constant.ts
    • 설치 하나에 항목 하나입니다. 같은 token이나 같은 deviceId로 다시 등록하면 그 항목을 바꾸므로, 바뀐 토큰이 쌓이지 않습니다.
    • updatedAt은 서버가 씁니다. 토큰을 등록할 때 기록하며, 클라이언트가 보낸 값은 쓰지 않습니다.
    • 로그아웃하면 이 기기를 지웁니다. signoutUser가 설치의 deviceId를 보내므로, 물려받은 폰이 앞 사람의 알림을 받지 않습니다.
    • 예전 토큰은 건너뜁니다. 이 모양 이전에 문자열로 저장된 토큰은 읽지 않으며, Notification.Zone.Initialize가 다음 방문 때 기기를 다시 등록합니다.
    엔드포인트
    셋 모두 user 시그널에 있는 User 가드 엔드포인트이며, notification 스토어의 registerPushToken, unregisterPushToken, loadPushState가 부릅니다:
    엔드포인트설명
    addNotiDeviceTokenOfSelf(deviceToken)
    이 기기의 DeviceToken을 호출한 사용자에게 저장하고, 이전 항목을 바꿉니다.
    subNotiDeviceTokenOfSelf(token)
    호출한 사용자에게서 토큰 하나를 지웁니다. 푸시 스위치를 끈 경우입니다.
    hasNotiDeviceTokenOfSelf(token)
    이 기기가 등록되어 있는지 알려 줍니다. 스위치가 보여 주는 상태입니다.
    • 소유자는 Self가 넘겨줍니다. 그래서 클라이언트가 다른 사람 계정으로 토큰을 등록할 수 없습니다.
    • MCP에는 올리지 않습니다. 에이전트에게는 기기가 없으므로 토큰 엔드포인트는 mcp: false입니다.

    발송과 죽은 토큰 정리

    도메인 서비스가 부르는 것은 notificationService.push(userIds, payload) 하나입니다. 받는 사람마다 수신 설정을 읽고, 받아 준 기기마다 그 기기의 provider로 보내며, APNs나 FCM이 사라졌다고 답한 토큰을 지웁니다.
    서버 인증 정보
    두 발송기의 인증 정보를 각 서버 env 파일의 pushNoti 아래에 넣습니다:
    apps/myapp/env/env.server.local.ts
    • firebase는 서비스 계정입니다. Firebase Console의 프로젝트 설정 → 서비스 계정에서 받고, 내려받은 JSON에서 위 다섯 필드를 옮겨 적습니다. Android와 웹에 필요합니다.
    • apns는 .p8 키입니다. privateKey는 파일의 텍스트이고(\n 이스케이프도 됩니다), keyId와 teamId는 Apple Developer에서, bundleId는 앱의 native.appId입니다. iOS에 필요합니다.
    • 어느 것도 google-services.json이 아닙니다. 그 파일은 Android 앱 설정이고, 이것들은 모든 발송에 서명합니다.
    apps/myapp/lib/order/order.service.ts
    • 수신 설정은 함수 하나가 판정합니다. NotificationService.accepts입니다. block과 disagree는 전부 막고, fewer는 actionRequired와 essential만 통과시키며, pauseUntil이 미래면 전부 막고, 토큰이 없는 사용자는 건너뜁니다.
    • 죽은 토큰은 바로 지웁니다. APNs의 410이나 BadDeviceToken, FCM의 messaging/registration-token-not-registered를 받으면 같은 호출 안에서 주인에게서 지웁니다.
    • 오류를 던지지 않습니다. 푸시는 최선을 다할 뿐입니다. push()는 닿은 범위(targetUserIds, tokenNum, successCount, prunedTokens)를 돌려주고, 발송 실패가 호출한 쪽의 일을 실패시키지 않습니다.
    • 전체 발송도 같은 판정을 거칩니다. 관리자가 쓴 type: "all" 알림은 모든 활성 사용자에게 500명씩 나가며, 다른 푸시처럼 accepts를 거칩니다.
    push()가 받는 값
    titlestring필수
    알림 제목입니다.
    levelcnst.NotiLevel필수
    actionRequired, notice, essential, suggestion, advertise 중 하나입니다. 수신 설정 판정이 읽습니다.
    contentstring
    알림 본문입니다.
    contentKey

    이 페이지

    푸시 설정
    네이티브 플러그인 하나
    웹 푸시
    Android 푸시
    iOS 푸시
    빌드한 APNs 환경 확인하기
    클라이언트 등록
    토큰이 저장되는 곳
    발송과 죽은 토큰 정리
    .p8은 서버에만 둡니다. 이 키는 팀의 모든 앱에 푸시를 서명합니다. env.server.*에 두고, env.client.*나 public/에는 절대 두지 않습니다.
    브라우저의 알림 클릭을 라우팅합니다. 훅이 마운트될 때 실행하며, 네이티브 셸에서는 할 일이 없습니다.
    provider별 sendEach
    APNs
    FCM
    사라진 토큰인가?
    예
    사용자 기기
    서버: 토큰 삭제
    인증 정보가 없는 발송기는 아무것도 보내지 않고, 오류도 던지지 않습니다. pushNoti.apns is not configured 같은 warn 로그 한 줄만 남기고 그 토큰들을 건너뛰며, 실패로 셉니다.
    서비스에서 보내기
    불러오고, 저장한 뒤 알립니다. 푸시는 기다리지 않고 보냅니다:
    string
    본문 대신 쓰는 사전 키입니다. 앱의 기본 로케일로 풀어 씁니다.
    urlstring
    알림을 눌렀을 때 열 경로입니다. 앱 안의 경로를 씁니다.
    tagstring
    합치기 키입니다. 같은 tag의 두 번째 푸시가 첫 번째를 대신합니다.
    imageUrlstring
    알림에 보여 줄 이미지입니다.
    badgenumber
    앱 아이콘의 배지 숫자입니다.
    토픽은 쓰지 않습니다. 토픽은 APNs 토큰을 담지 못하고, 사람마다의 수신 설정을 물을 수 없으며, 죽은 토큰도 알려 주지 않습니다. 그래서 모든 발송은 저장된 토큰으로 나갑니다. libs/shared 없이 쓴다면 @libs/util/srvkit의 PushNotificationServer.sendEach(targets, message)를 { token, provider } 목록으로 부르고, 돌려받은 invalidTokens는 더 이상 저장하지 않습니다.