사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
UI 레퍼런스▾

akanjs/webkit

akanjs/webkit은 브라우저나 네이티브 앱에서만 도는 React hook과 helper를 모은 모듈입니다. 타이머, 지연 로딩, promise 상태, 기기 기능, CSR 라우터 상태가 여기에 있습니다.
import { lazy, useDebounce, useInterval } from "akanjs/webkit";
이 페이지에서 다루는 것
컴포넌트 코드를 필요할 때 받아 옵니다. ssr: false면 서버 렌더링을 건너뜁니다.
호출이 잠시 멈춘 뒤에 콜백을 한 번 실행합니다.
콜백을 일정한 간격으로 실행하고, 언마운트되면 멈춥니다.
콜백을 바로 실행하고, 잠시 동안 이어지는 호출은 무시합니다.
클라이언트 컴포넌트에 promise가 끝났는지와 그 값을 알려 줍니다.
네이티브 런타임의 플러그인으로 카메라와 위치를 씁니다. 브라우저 대체 경로와 권한 요청까지 처리합니다.
이 모듈에는 없습니다. 푸시 hook은 @libs/util/webkit에 있습니다.
네이티브 iap 플러그인으로 인앱 결제를 하고, 서버가 검증합니다. akanjs/webkit/usePurchase에서 가져옵니다.
CSR 라우터가 쓰는 위치 해석기와 history 스택입니다.
shared store의 login action이 받는 인자 타입입니다.
그 밖의 export
useBodyScrollLock(active)
active인 동안 document.body 스크롤을 막습니다. 여러 오버레이가 카운터 하나를 함께 씁니다.
useEscapeKey(active, onEscape)
active인 동안 Escape를 누르면 onEscape를 부릅니다. 겹쳐 열린 것 중 맨 위 하나만 받습니다.
usePageFocusEffect(effect, deps)
사용자가 이 페이지에 있는 동안 effect를 돌리고, 떠나면 정리합니다. 스와이프 뒤로가기를 위해 현재 페이지 아래 남은 페이지는 마운트된 채라 평범한 effect는 계속 돕니다.
usePageLocation()
이 페이지의 { pathname, params, searchParams }입니다. st.use.searchParams()는 화면에 보이는 페이지를 따르므로, 준비 중이거나 현재 페이지 아래 남은 페이지는 여기서 자기 값을 읽습니다.
usePageActivity()
"current" | "prev" | "pending" | "hidden" — CSR 스택에서 이 페이지의 위치이며, CSR 밖에서는 늘 current입니다.
usePageTooluseScreenScope
페이지 넘김과 화면 속 항목을 인페이지 에이전트에게 알립니다. Load.Units와 Load.View가 이미 부릅니다.
createRobotPagecreateSitemapPage
robots 규칙(기본 disallow: "/admin/")과 sitemap 항목 목록을 만듭니다.
bootCsrreplacePagesuseCsrValues
CSR(모바일) 번들을 띄우고, dev에서는 라우트 모듈을 그 자리에서 바꾸며, 라우터 상태를 쥡니다. 자동 생성된 진입 파일이 부르므로 앱 코드에서는 부르지 않습니다.
LoginAuthScreenScopeItem
타입입니다. "user" | "admin" | "public", 그리고 화면 속 항목 하나인 { id, label? }입니다.
  • hook은 클라이언트 파일에서 부릅니다. React hook이라 "use client"가 붙은 파일이 필요합니다. lazy()는 ui/<Folder>/index_.tsx 경계 파일에 둡니다.
  • 푸시와 음성은 util 라이브러리에서 가져옵니다. usePushNotification과 useSpeech는 여기가 아니라 @libs/util/webkit에서 import합니다.

lazy

lazy는 서버 렌더링 스위치가 붙은 React lazy입니다. ssr: false를 주면 서버에서 그리지 않으므로, import하는 순간 window를 건드리는 지도, 차트, 3D 라이브러리에 씁니다.
lazy(loader, { ssr, suspense, loading })
ssrboolean기본값 true
false면 서버 렌더링을 건너뜁니다. 서버는 loading만 보내고, 청크는 마운트 뒤에 받습니다.
suspenseboolean기본값 false
컴포넌트를 전용 Suspense 경계로 감싸, 청크를 기다리는 동안 이 자리만 기다리게 합니다.
loading() => ReactNode
자리 표시입니다. ssr: false나 suspense: true일 때만 보입니다.
호출 방식별 렌더링
호출서버 HTML
↳ 청크를 받는 동안
옵션 없음컴포넌트를 그대로 그립니다.
가장 가까운 경계(보통 route 전체)가 fallback을 보여 줍니다.
suspense: true컴포넌트를 그립니다. stream 모드에서는 shell 뒤에 따로 도착합니다.
이 자리만 loading을 보여 줍니다.
ssr: falseloading만 보내고, 없으면 아무것도 보내지 않습니다.
마운트되고 청크가 도착할 때까지 loading을 보여 줍니다.
브라우저가 있어야 그려지는 지구본을 lazy 경계 파일 뒤에 둡니다:
apps/myapp/ui/Globe/index_.tsx
  • 파일 쌍을 지킵니다. "use client"와 lazy()는 index_.tsx에 두고, 옆의 index.tsx는 서버에서도 import할 수 있게 둡니다. 둘을 합치면 서버 렌더링이 깨집니다.
  • 불러오는 파일은 컴포넌트를 default로 내보냅니다. 패키지의 default export처럼 loader가 컴포넌트를 바로 돌려줘도 됩니다.
  • suspense: true는 나중에 열리는 것에만 씁니다. 모달 본문, 드롭다운, 에디터가 그렇습니다. 페이지 본문에 쓰면 SEO 스냅샷과 사전 렌더링이 읽는 shell에서 마크업이 빠집니다.

useDebounce

useDebounce는 호출이 멈춘 뒤에만 실행되는 콜백을 돌려줍니다. 입력이 멈추면 한 번 검색하는 검색창이 대표적이고, 이미지 편집기나 비용이 큰 필드 갱신도 사용자가 드래그하거나 입력하는 동안 이것으로 작업을 미룹니다.
useDebounce(callback, states = [], wait = 100)
callback(...args) => unknown필수
마지막 호출의 인자로 한 번 실행됩니다.
statesunknown[]기본값 []
안쪽 useCallback의 의존성 배열입니다. 콜백이 읽는 prop과 state를 모두 넣습니다.
waitnumber기본값 100
마지막 호출 뒤에 기다리는 시간(ms)입니다.
마지막 입력 300ms 뒤에 slice를 다시 불러오는 검색창입니다:
apps/myapp/lib/product/Product.Util.tsx
  • 콜백이 읽는 값을 states에 넣습니다. []로 두면 첫 렌더링의 콜백이 계속 실행되어 예전 shopId를 봅니다.
  • 인자 순서가 useThrottle과 다릅니다. 여기서는 의존성이 두 번째, 대기 시간이 세 번째입니다.

useInterval

useInterval은 delay ms마다 콜백을 실행하고, 언마운트되면 타이머를 정리합니다. 대시보드, 게임 상태, 빌드 로그를 주기적으로 다시 불러올 때 씁니다.
useInterval(callback, delay)
callback() => void | Promise<void>필수
틱마다 실행됩니다. 가장 최근 렌더링에서 넘긴 콜백이 실행됩니다.
delaynumber필수
틱 사이의 간격(ms)입니다. 값이 바뀌면 타이머를 다시 시작합니다.
주문 목록을 3초마다 새로 불러오는 Zone입니다:
apps/myapp/lib/order/Order.Zone.tsx
  • 렌더링마다 새 콜백을 넘겨도 됩니다. 타이머는 그대로 돌면서 최신 콜백을 실행하고, delay가 바뀔 때만 다시 시작합니다.
  • 틱은 서로를 기다리지 않습니다. async 콜백이 delay보다 오래 걸리면 다음 틱과 겹쳐 실행됩니다.
  • 바뀌는 즉시 받아야 하면 .live() slice를 선언합니다. 폴링은 타이머에 맞춰 다시 불러올 뿐이고, live slice는 변경을 구독자에게 바로 보냅니다.

useThrottle

useThrottle은 바로 실행한 뒤 delay ms가 지날 때까지 호출을 버리는 콜백을 돌려줍니다. 너무 자주 발생하는 scroll, pointer, resize, drag 핸들러에 씁니다.
useThrottle(func, delay = 200, deps = [])
func(...args) => unknown필수
구간마다 첫 호출에서 바로 실행됩니다.
delaynumber기본값 200
실행 뒤 이어지는 호출을 버리는 시간(ms)입니다.
depsunknown[]기본값 []
추가 의존성입니다. func와 delay는 이미 들어 있습니다.
useDebounce와 비교
호출
↳ 실행 시점
useDebounce(callback, states = [], wait = 100)
호출이 wait ms 동안 멈춘 뒤 한 번 실행합니다.
useThrottle(func, delay = 200, deps = [])
바로 실행하고, delay ms 동안 이어지는 호출은 버립니다.
점 위치를 최대 100ms마다 한 번만 바꾸는 드래그 패드입니다:
apps/myapp/ui/DragPad.tsx
  • 첫 호출은 항상 실행됩니다. 구간 안의 호출은 쌓아 두지 않고 버리므로, 빠른 드래그의 마지막 위치는 빠질 수 있습니다.
  • 마지막 값이 꼭 필요하면 useDebounce를 씁니다. 마지막 호출의 인자로 한 번 실행됩니다.

useFetch / useFetchFn

이 두 hook은 클라이언트 컴포넌트 안에서 promise를 { fulfilled, value }로 바꿔 줍니다. 브라우저에만 있는 값이거나, 값을 그리기만 하는 게 아니라 클라이언트 코드가 값 자체를 써야 할 때 씁니다.
useFetch(promiseOrValue, { onError })
렌더링마다 넘겨받은 promise를 따라갑니다. promise가 아닌 값이면 바로 fulfilled: true로 돌려줍니다.
useFetchFn(factory, deps = [], { onError })
factory를 useMemo 안에서 불러, deps가 바뀔 때만 요청이 새로 나갑니다.
반환값과 옵션
fulfilledboolean
지금 promise가 끝나면 true입니다. 실패했거나 새로 넘겨받은 promise는 false입니다.
valueT | null
지금 promise가 끝난 값입니다. 그 전에는 null입니다.
onError(err: string) => void
옵션입니다. promise가 실패하면 "Error: <message>" 문자열로 불립니다.
저장 공간 사용량은 브라우저에만 있으므로, 이 컴포넌트는 lazy(…, { ssr: false })로 불러옵니다:
apps/myapp/ui/StorageUsage/StorageUsage.tsx
  • promise를 그리기만 한다면 <Load.Stream of={promise}>가 먼저입니다. 서버가 실제 마크업을 스트리밍합니다. useFetch는 effect에서 기다리므로 첫 HTML에는 대체 화면만 들어갑니다.
  • 마운트할 때 fetch.*를 부르지 않습니다. 서버 데이터는 route에서 불러 prop이나 await하지 않은 promise로 넘깁니다.
  • factory는 렌더링 중에 실행되며, 서버에서도 실행됩니다. 클라이언트 컴포넌트도 서버에서 한 번 렌더링되는데 그곳에는 navigator.storage가 없습니다. 그래서 브라우저 전용 호출은 위처럼 ssr: false 경계 뒤에 둡니다.
  • 렌더링마다 넘겨받은 promise를 따라갑니다. 새 promise를 넘기거나 useFetchFn의 deps가 바뀌면, 결과는 그 promise가 끝날 때까지 fulfilled: false로 돌아가고 이전 promise의 결과는 버려집니다.

useCamera

useCamera는 네이티브 런타임의 camera 플러그인으로 사진을 찍거나 앨범에서 고릅니다. 네이티브 앱에서는 먼저 카메라 권한을 요청하고, 거부된 상태면 앱 설정 화면을 엽니다. 브라우저에서는 파일에서 고릅니다.
useCamera({ promptLabels } = {})
getPhoto(src = "prompt")
사진 한 장을 찍거나 골라 똑바로 선 JPEG { dataUrl }로 돌려줍니다. "prompt"는 네이티브 앱에서 카메라·앨범 선택 시트를 띄우고, 취소하면 undefined입니다.
pickImage({ limit })
앨범에서 여러 장을 골라 각각 { dataUrl }로 돌려줍니다.
permissions
{ camera } 권한 상태입니다. 네이티브 앱에서 마운트할 때 읽고, 그 전에는 "prompt"입니다.
checkPermission()
카메라 권한을 요청하고, 거부됐으면 앱 설정을 엽니다.
옵션
promptLabels{ header?, photo?, picture?, cancel? }기본값 {}
네이티브 선택 시트의 문구입니다. 빠진 것은 base 사전에서 현재 언어로 가져옵니다.
사진을 찍어 미리 보여 주는 버튼입니다:
apps/myapp/ui/TakePhoto.tsx
  • 권한을 선언합니다. native.permissions에 "camera"를 넣으면 camera 플러그인과 사용 목적 문구가 들어갑니다. 설치할 패키지는 없습니다.
  • 브라우저에서는 파일에서 고릅니다. 네이티브 앱 밖에서는 어떤 source든 앨범 선택이 되므로, 따로 확인하지 않아도 같은 호출이 웹에서 동작합니다.
  • 사진은 한 번만 읽습니다. 런타임이 넘긴 파일 참조를 hook이 data URL로 바꾸고 바로 놓아 주므로, 따로 정리할 것이 없습니다.

useGeoLocation

useGeoLocation은 네이티브 런타임의 geolocation 플러그인으로, 브라우저에서는 navigator.geolocation으로 현재 위치를 읽습니다. 권한이 거부되면 사용자를 앱 설정으로 보냅니다.
useGeoLocation()
getPosition({ enableHighAccuracy })
Position을 돌려줍니다. 권한이 거부되면 앱 설정을 열고 undefined를 돌려줍니다.
checkPermission()
권한을 요청하고 { location, precise }를 돌려줍니다.
지도를 어디에 맞출지 찾는 앱 hook입니다:
apps/myapp/webkit/useMapCenter.tsx
  • undefined를 확인합니다. 권한이 거부되어 이미 설정 화면을 연 상태라는 뜻입니다.
  • 위치 값은 평평합니다. latitude, longitude, accuracy, altitude, heading, speed, timestamp가 coords 아래가 아니라 값 자체에 있습니다.
  • precise는 정확한 위치인지 알려 줍니다. false면 사용자가 대략적인 위치만 허용한 것이고, 웹은 null로 답합니다.
  • 권한을 선언합니다. native.permissions에 "location"을 넣으면 geolocation 플러그인과 사용 목적 문구가 들어갑니다.

usePushNotification

푸시는 akanjs/webkit이 아니라 @libs/util/webkit에 있습니다. 네이티브 셸에서는 런타임의 push 플러그인(iOS는 APNs, Android는 FCM)을, 브라우저에서는 Firebase를 부르고, 어느 쪽이든 같은 모양의 PushToken을 돌려줍니다.
import { type PushToken, usePushNotification } from "@libs/util/webkit";
register()
권한을 요청한 뒤 PushToken을 돌려줍니다. 거부되거나 지원하지 않으면 undefined입니다.
getToken()
묻지 않고 토큰을 돌려줍니다. 등록 자체는 창을 띄우지 않으므로 먼저 권한을 확인합니다.
getPermission()requestPermission()
권한 상태를 읽거나, 권한 요청 창을 띄우고 결과를 돌려줍니다.
isSupported()
지금 런타임에서 푸시를 쓸 수 있는지 알려 줍니다.
onTokenChange(listener)
네이티브 셸이 바꾼 토큰을 리스너에 넘깁니다. 해제 함수를 돌려줍니다.
initClickBridge()
브라우저의 알림 클릭을 앱 안 이동으로 잇습니다. 훅이 마운트될 때 실행하며, 네이티브 탭은 할 일이 없습니다.
register()가 권한 창을 띄울 수 있으므로 버튼에서 등록합니다:
apps/myapp/ui/EnablePush.tsx
  • PushToken은 앱 설치 하나의 주소입니다. token, platform(web | ios | android), provider(apns | fcm), 앱 저장소에 두는 설치 id인 deviceId를 담습니다.
  • 알림을 누르면 푸시의 url로 이동합니다. 네이티브 셸에서는 앱을 띄운 탭까지 프레임워크가 부팅 때부터 라우팅하며, 앱 안의 경로만 따라갑니다.
  • registerPushToken은 libs/shared에 들어 있습니다. notification 스토어가 토큰을 로그인한 사용자에게 저장하고, Notification.Zone.Initialize가 최신으로 유지합니다.

usePurchase

usePurchase는 네이티브 런타임의 iap 플러그인으로 인앱 상품을 팝니다. iOS는 StoreKit 2, Android는 Play Billing입니다. 앱이 지급하기 전에 서버가 거래마다 검증하고, 브라우저에서는 팔지 않습니다.
import { usePurchase } from "akanjs/webkit/usePurchase";
옵션
platform"ios" | "android" | "all"필수
앱이 판매하는 스토어입니다. 다른 플랫폼의 네이티브 셸에서는 상품을 보여 주지 않습니다.
productInfo{ id, type: "consumable" | "nonConsumable" | "subscription" }[]필수
스토어 상품 id와 그 종류입니다. 목록에 없는 상품은 소비하지 않고 완료만 합니다.
urlstring필수
검증 서버의 origin입니다. POST <url>/billing/verifyBilling에 답해야 합니다.
onPay(transaction, verified) => void | Promise<void>
소비성·비소비성 상품을 지급합니다. verified는 서버가 돌려준 JSON입니다.
onSubscribe(transaction, verified) => void | Promise<void>
구독 상품에 대해 같은 일을 합니다.
products
productInfo에 대한 스토어의 IapProduct 목록입니다. 제목, displayPrice, 가격, 통화, 할인 조건이 들어 있습니다.
isLoading
상품과 완료되지 않은 거래를 불러올 때까지 true입니다.
purchaseProduct(product, offerToken?)
스토어 결제 창을 열고 "purchased", "pending", "cancelled", "unverified" 중 하나로 답합니다.
restorePurchases()
지금 가진 구매 목록을 돌려주고, 아직 확인되지 않은 Android 구매는 그 김에 검증하고 완료합니다.
서버가 구매를 받아 준 뒤에 코인을 지급하는 구매 버튼입니다:
apps/myapp/ui/BuyCoins.tsx
  • 네이티브 앱에 플러그인을 넣습니다. iap 플러그인은 권한이 아니므로, akan.config.ts에 native: { plugins: ["iap"] }로 적습니다.
  • 서버는 본문 하나를 받습니다: { data }. data는 { platform, packageName, productId, receipt, transactionId }입니다. iOS의 receipt는 거래의 서명된 JWS이고(app receipt와 account id는 없습니다), Android는 purchase token과 패키지 이름입니다. 2xx면 받아 준 것이고, 그 JSON이 onPay나 onSubscribe에 verified로 옵니다.
  • 지급한 뒤에만 완료합니다. 서버가 받아 주고 콜백이 끝난 뒤에 거래를 완료합니다. 거절되거나 콜백이 던지면 완료하지 않고, 스토어가 다시 넘겨줍니다. iOS는 다음 실행 때 다시 오고, Google은 확인하지 않은 구매를 3일 뒤 환불합니다.
  • 남은 거래는 마운트할 때 정리합니다. hook이 완료되지 않은 거래를 불러오고 나중에 오는 거래(Ask to Buy, 보류된 결제)도 듣습니다. 같은 거래가 두 번 와도 한 번만 지급합니다.

useLocation / useHistory

CSR 라우터는 이 두 hook 위에서 돕니다. 하나는 href를 route 상태로 바꾸고, 다른 하나는 사용자가 지나온 곳을 기억합니다. 캐시된 페이지 전환, 스크롤 복원, 뒤로/앞으로 가기 판별이 여기서 나옵니다.
useLocation({ rootRouteGuide })
getLocation(href)를 돌려줍니다. href를 route 트리에 맞춰 봅니다.
getLocation(href)
pathname, search, hash, params, searchParams, 맞춘 pathRoute를 돌려줍니다.
useHistory(locations)
방문한 위치, 현재 index, 페이지별 스크롤 위치를 ref에 담아 둡니다.
setHistoryForwardsetHistoryBack
push, replace, pop을 기록하고, 떠나는 페이지의 스크롤 위치를 저장합니다.
getPrevLocationgetCurrentLocationgetNextLocation
현재 항목의 앞뒤를 읽습니다. 뒤로 가기와 앞으로 가기를 이것으로 구분합니다.
getScrollTop(location)
되돌릴 스크롤 위치입니다. 저장된 값이나, #hash 요소의 위치입니다.
라우터는 처음 연 페이지에서 시작해 한 번만 준비합니다:
pkgs/akanjs/webkit/useCsrValues.ts
  • 앱 코드는 router로 이동합니다. akanjs/client의 router.push와 router.back이 이 hook들을 대신 거칩니다.
  • config에 cache를 켠 페이지는 보관됩니다. history가 그 위치를 기억해 두므로, 뒤로 가면 다시 만들지 않고 보여 줍니다.

LoginForm

LoginForm은 shared store의 login action이 받는 인자입니다. 로그인 뒤 어떤 계정을 불러올지, 성공하거나 실패하면 어디로 보낼지를 담습니다.
auth"user" | "admin" | "public"필수
"admin"이면 관리자 계정을 불러옵니다. 그 밖의 값이면 getSelf로 사용자를 불러옵니다.
redirectstring
계정을 불러온 뒤 router.push로 이동할 경로입니다.
unauthorizestring
계정을 불러오지 못했을 때 이동할 경로입니다.
jwtstring | null
무엇이든 불러오기 전에 setAuth로 저장할 토큰입니다. 로그인 직후처럼 토큰을 막 받았을 때 넘깁니다.
로그인한 사용자를 불러온 뒤 홈으로 보내고, 실패하면 로그인 화면으로 돌려보내는 버튼입니다:
apps/myapp/ui/Continue.tsx
  • 로그인 호출 직후에는 jwt를 넘깁니다. admin store가 이렇게 합니다. 로그인한 뒤 { auth: "admin", jwt, redirect }로 login을 부릅니다.
  • 지금은 "public"도 "user"와 같은 방식으로 불러옵니다. "admin"만 다른 길로 갑니다.

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

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman