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

핵심 UI

거의 모든 화면이 쓰는 akanjs/ui 컴포넌트 다섯 개입니다. 아래 섹션마다 props를 정리하고, 실제로 동작하는 예시 하나로 마무리합니다.import { Image, Layout, Link, Load, Model } from "akanjs/ui";
Link
앱 내부 route 사이를 이동합니다. 내부 링크는 전부 Link입니다.
Image
업로드한 파일이나 URL을 그리고, Akan 이미지 최적화기로 크기를 맞춥니다.
Layout
페이지의 틀입니다. 콘텐츠 컨테이너, 위아래 chrome, 헤더와 서랍으로 나뉩니다.
Load
fetch 결과를 목록, 상세, 폼 화면으로 바꾸고, 그 밖의 promise도 기다렸다가 그립니다.
Model
model마다 자동으로 만들어지는 store에 연결된 생성·수정·조회·삭제 셸입니다.
이 페이지에서 쓰는 말
slice
model의 목록 조회에 이름을 붙인 것입니다(productInShop). 컴포넌트에는 fetch.slice.<name>으로 넘깁니다.
initviewedit
fetch.init*, fetch.view*, fetch.edit*가 돌려주는 값입니다. 데이터와 store가 받아 쓸 정보가 함께 들어 있습니다.
hydrate
서버에서 받은 데이터를 클라이언트 store에 채우는 일입니다. 그래야 페이지 넘김 같은 자동 생성 action이 그 데이터로 동작합니다.
Suspense boundary
데이터를 기다리는 섹션 하나만 대체 화면으로 가리고, 나머지 페이지는 먼저 보여 주는 React 경계입니다.
chrome
스크롤 본문 위아래에 고정된 막대입니다. navbar나 하단 탭 바가 여기에 속합니다.
trigger
사용자가 눌러서 modal이나 확인 창을 여는 요소입니다.
draft
저장하지 않은 폼 입력값입니다. 기기에 남겨 두었다가 폼을 다시 열 때 복구를 제안합니다.

Image

업로드한 파일이나 URL을 이미지로 그립니다. 서버 렌더링 페이지에서는 Akan 이미지 최적화기(/_akan/image)를 거쳐, 크기에 맞게 줄인 사본을 받습니다.
Props
srcstring
직접 지정하는 이미지 URL입니다. file.url보다 우선합니다.
fileProtoLightFile | { url, imageSize, abstractData? } | null
File model 값, 또는 url과 imageSize를 가진 객체입니다.
width / heightnumber기본값 file.imageSize
그려질 크기입니다. 비워 두면 file.imageSize에서 가져옵니다.
altstring기본값 "image"
대체 텍스트입니다. 기본값은 image라는 단어뿐이니 실제 설명을 넘기세요.
abstractDatastring | null
저화질 미리보기 데이터입니다. file.abstractData보다 우선합니다.
qualitynumber기본값 75
최적화기가 쓰는 화질입니다.
priority / preloadboolean
지연 없이 높은 우선순위로 불러오고, 서버 렌더링 페이지에서는 미리 불러옵니다.
unoptimizedboolean
최적화기를 건너뛰고 원본 URL을 그대로 씁니다.
예시
사용자가 올린 이미지로 만든 48px 아바타입니다:
apps/shop/lib/user/User.Unit.tsx
  • 1x·2x 사본. width만 있고 sizes가 없으면 두 가지를 모두 만듭니다. 이 아바타는 48px과 96px 파일을 받습니다.
  • 최적화하지 않는 것: SVG 파일과 data: / blob: URL은 그대로 내보냅니다.
  • CSR 번들(모바일 앱)은 원본 URL을 그립니다. 최적화기는 서버에서 돌기 때문입니다.

Layout

페이지의 틀입니다. 멤버는 놓을 자리로 고릅니다. 모듈 파일 안, 스크롤 본문의 위아래, 그리고 페이지 위에 겹치는 자리입니다.
콘텐츠 컨테이너
Layout.Template{ className?, children? }
모듈 Template에 맞는 간격을 가진 세로 폼 컨테이너입니다.
Layout.Unit{ className?, children, href? }
목록·카드 항목입니다. href를 주면 항목 전체가 Link 하나가 됩니다.
Layout.View{ className?, children }
상세 페이지 컨테이너이며, 폭은 max-w-5xl까지입니다.
Layout.Zone{ className?, children }
zone과 페이지 블록을 담는 섹션 컨테이너이며, 폭 제한은 같습니다.
위아래 chrome
Navbar, TopInset, BottomInset, BottomTab은 자기 높이를 route에 등록합니다. 그래서 스크롤 본문이 그 뒤에 가려지지 않습니다.
Layout.Navbar{ className?, children?, height?, back? }기본값 height 48
children을 상단 inset으로 옮겨 그립니다. back은 true면 기본 화살표, 아니면 직접 넘긴 요소입니다.
Layout.TopInset{ className?, children, estimatedHeight? }기본값 estimatedHeight 48
navbar가 아닌 상단 chrome입니다. estimatedHeight만큼 자리를 잡아 둡니다.
Layout.TopLeftAction{ className?, children }
상단 inset의 왼쪽 모서리로, navbar의 back이 놓이는 자리입니다. 다른 모서리 버튼도 여기에 둡니다.
Layout.BottomInset{ className?, children, keyboardSticky?, role?, estimatedHeight? }기본값 estimatedHeight 60
하단 chrome입니다. keyboardSticky면 키보드 위에 붙고, role로 상시 chrome과 키보드 액세서리를 나눕니다.
Layout.BottomTab{ className?, tabs, height?, renderTab? }기본값 height 64
앱의 하단 탭 바입니다. 탭마다 { name, icon, activeIcon?, notiCount?, href }를 넘깁니다.
헤더와 서랍
이 넷은 페이지 위에 그려지며, 높이를 등록하지 않습니다.
Layout.Header{ className?, children?, type?: "hide" | "static" }기본값 "hide"
화면 위에 고정된 웹 헤더입니다. hide는 md 이상 화면에서 아래로 스크롤하면 숨기고, static은 계속 보여 줍니다.
Layout.Sider{ className?, bgClassName?, trigger?, header?, close?, children? }
열림 상태를 스스로 갖고 route가 바뀌면 닫히는 서랍입니다. trigger, header, close로 부품을 바꿉니다.
Layout.LeftSider{ open, onCancel, children, width?, close?, className? }
open으로 제어하는 왼쪽 서랍입니다. close={false}면 닫기 버튼을 그리지 않습니다.
Layout.RightSider{ open, onCancel, children, title?, width?, close?, className? }
open으로 제어하는 오른쪽 서랍이며, 왼쪽에는 없는 title 자리가 있습니다.
  • 닫기 표시. back을 켜면, route 전환이 bottomUp, scaleOut, fade일 때는 화살표 대신 ✕를 그립니다.
  • renderTab은 탭 본문 전체를 그립니다. 배지도 포함입니다. framework는 링크와 활성 판정만 맡으므로 notiCount는 직접 그립니다.
예시
navbar에 뒤로 가기 버튼과 수정 링크를 둔 상세 페이지입니다:
apps/shop/page/order/[orderId]/_index.tsx
  • 트리 어디에 두어도 됩니다. navbar는 내용을 상단 inset으로 옮겨 그리므로, page는 본문 바로 옆에 두면 됩니다.
  • page는 서버에 그대로 남습니다. children과 back은 평범한 prop이라 page에 "use client"가 필요 없습니다.
누르면 주문 상세로 가는 목록 행입니다:
apps/shop/lib/order/Order.Unit.tsx
  • href가 없으면 평범한 컨테이너일 뿐 누를 수 있는 곳이 없습니다.

Load

fetch.* 결과를 화면으로 바꿉니다. 모든 멤버가 await한 값과 promise를 둘 다 받습니다. 아직 끝나지 않은 promise는 자기 Suspense 경계 안에서 기다리므로, 느린 섹션 하나가 페이지 전체를 붙잡지 않습니다.
멤버
Load.Units{ init, renderItem | renderList, … }
slice 목록을 그리고 store를 hydrate합니다. 그래서 자동 생성된 페이지 넘김과 새로고침이 그대로 동작합니다.
Load.View{ view, renderView, loading?, empty?, noDiv?, className? }
model 하나를 hydrate하고 renderView로 그립니다. noDiv면 감싸는 요소를 그리지 않습니다.
Load.Edit{ edit, slice, type?, modal?, loading?, draft?, onSubmit?, onCancel?, submitText?, renderSubmit?, … }기본값 type "modal"
edit에는 수정용 payload, 그 promise, 새 레코드용 seed를 넘깁니다. type은 modal, form, empty 중 하나입니다.
Load.Pagination{ init, className?, scrollToTop? }
목록의 init을 받는 독립 pager입니다. 한 페이지에 다 들어가면 아무것도 그리지 않습니다.
Load.Stream{ of, fallback?, children }
promise 하나를 자기 Suspense 경계 안에서 기다렸다가 그 값을 children에 넘깁니다.
Load.Page{ of, loader, render, loading?, noCache? }
SSR과 CSR 공용 route 로더입니다. of는 CSR이 마운트할 컴포넌트, loader는 둘이 함께 쓰는 fetch입니다.
Load.Units 옵션
renderItem / renderList(item, idx) => ReactNode / (list) => ReactNode
둘 중 하나는 꼭 넘깁니다. 행마다 그리거나, 목록 전체를 한 번에 그립니다.
empty / renderEmptyReactNode / () => ReactNode
행이 없을 때 보여 줍니다. 둘 다 주면 empty가 우선합니다.
loadingReactNode
init을 기다리는 동안과 다시 불러오는 동안 보여 줍니다.
paginationboolean기본값 true
데스크톱에서는 pager, 모바일에서는 무한 스크롤입니다. Load.Pagination을 따로 두려면 끕니다.
staleTimenumber (ms)
마운트할 때 다시 불러오지 않고 쓸 수 있는 seed 데이터의 최대 나이입니다. 0이면 항상 다시 불러옵니다.
from / tonumber
다시 불러오지 않고 renderItem이 그릴 행 범위만 자릅니다.
filter / sort / reverse(item, idx) => boolean / (a, b) => number / boolean
이미 불러온 행을 클라이언트에서 거르고, 정렬하고, 뒤집습니다.
멤버를 두는 곳
함수는 서버 page에서 클라이언트 컴포넌트로 넘어갈 수 없습니다. 그래서 렌더 함수를 받는 멤버는 Zone에 두고, page는 그 Zone에 promise를 넘깁니다.
멤버
page
Zone
렌더 함수를 받는 멤버
Load.Units
✓
renderItem과 renderList는 함수라서 서버 page에서 넘길 수 없습니다.
Load.View
✓
renderView도 함수입니다.
데이터만 받는 멤버
Load.Edit
✓
✓
edit, slice, 문자열, children은 모두 경계를 넘을 수 있습니다.
Load.Pagination
✓
✓
init과 플래그 하나만 받습니다.
Load.Stream
✓
✓
"use client"가 없어서 children 함수는 그려지는 쪽에서 그대로 실행됩니다.
route 전용
Load.Page
✓
of, loader, render는 page가 넘겨도 되는 함수 prop입니다.
✓여기에 둘 수 있습니다여기에는 두지 않습니다
예시: page와 Zone
page는 모든 query를 시작하고 promise를 나눠 줍니다:
apps/shop/page/shop/[shopId]/product/[productId]/_index.tsx
  • await한 값 대신 promise를 넘깁니다. fetch.initProductInShop(shopId)는 두 query를 동시에 띄우고, 섹션마다 자기 데이터가 도착하는 대로 그려집니다.
  • 목록 데이터는 서버에 둡니다. productListInShop과 productInsightInShop에는 hydrate된 model 인스턴스가 들어 있어, React가 클라이언트 prop으로 받지 않습니다. 여기의 Load.Stream처럼 서버 컴포넌트에서 읽고, Zone prop으로는 넘기지 않습니다.
Zone에는 렌더 함수를 받는 두 멤버를 둡니다:
apps/shop/lib/product/Product.Zone.tsx
  • pagination={false}는 아래의 Load.Pagination이 pager를 그리기 때문입니다. 기본값 그대로면 목록이 pager를 하나 더 그립니다.
  • Zone은 자기 마크업을 그리지 않습니다. 행은 Product.Unit.Card에, 상세는 Product.View.General에 맡깁니다.
폼 draft 복구
Load.Edit, Model.EditModal, Model.New, Model.Edit는 사용자가 입력하는 대로 폼을 저장해 두고, 다음에 열 때 복구를 제안합니다.
draftboolean | string기본값 true
false면 복구를 끄고, 문자열이면 scope를 직접 정합니다.
  • scope. 수정 폼은 레코드 id, 새 폼은 seed와 route로 나뉘며, 언제나 로그인한 사용자별로 따로 저장됩니다.
  • 저장하지 않는 값: field.secret과 field.hidden 값입니다.
  • 폼 값을 직접 저장하지 않습니다. 예전의 field별 cache / cacheKey prop은 없어졌습니다. 입력 컨트롤 다섯 종류만 다뤘고, 번역된 label을 key로 썼으며, 서버 데이터 위에 덮어썼기 때문입니다.

Model

model마다 자동으로 만들어지는 store에 연결된 생성·수정·조회·삭제 셸입니다. 그 store action을 바로 쓸 수 있는 모듈의 Util, View, Zone 파일에서 씁니다.
trigger와 modal을 한 줄로
Model.New{ slice, children, trigger?, partial?, renderTitle?, modal?, namespace?, draft? }
children은 폼 본문, partial은 초기값, trigger는 기본 신규 버튼을 대신할 요소입니다.
Model.Edit{ slice, modelId, children, trigger?, renderTitle?, modal?, draft? }
레코드 하나에 대한 같은 묶음입니다. trigger 기본값은 framework의 수정 버튼입니다.
wrapper: 감싼 요소가 trigger가 됩니다
Model.NewWrapper{ slice, children, partial?, setDefault?, modal?, resets?, namespace?, draft?, className? }
클릭하면 생성 폼을 엽니다. resets에 적은 model은 폼이 열릴 때 reset<Model>()이 실행됩니다.
Model.EditWrapper{ slice, modelId, children, modal?, disabled?, resets?, draft?, className? }
레코드 하나를 수정 폼으로 엽니다.
Model.ViewWrapper{ slice, modelId, children, modal?, resets?, className? }
레코드 하나를 상세 보기로 엽니다.
Model.RemoveWrapper{ slice, modelId, name, children, modal?, className? }
작은 확인 팝오버로 한 번 묻고 레코드를 삭제합니다.
trigger 없는 modal과 본문
Model.EditModal{ slice, children, edit?, type?, id?, renderTitle?, submitText?, renderSubmit?, onSubmit?, onCancel?, draft?, draftBarClassName?, … }
trigger 없는 수정 셸입니다. onSubmit / onCancel에는 "back", "reset", 경로, 콜백 중 하나를 넘깁니다.
Model.ViewModal{ id, slice, renderView, renderTitle?, renderAction?, modal?, modalClassName?, viewClassName? }
상세 보기를 modal로 띄우며, 제목과 action 자리가 있습니다.
Model.ViewEditModal{ slice, renderView, renderTemplate, renderTitle?, menu?, editLabel?, saveLabel? }
상세 보기와 폼을 오가는 modal 하나입니다. menu={false}면 케밥 메뉴와 그 안의 삭제 항목이 빠집니다.
Model.View{ model, render, modelLoading?, loading?, empty?, loadingWrapper?, className? }기본값 modelLoading true
store 쪽 Load.View입니다. model 하나로 불러온 상태, 로딩 중, 빈 상태를 그리며 store의 로딩 값을 꼭 넘깁니다.
Model.AdminPanel{ slice, components, columns?, actions?, tools?, summaryColumns?, insightColumns?, queryMap? }
생성된 Unit, Template, View namespace로 만드는 관리자 화면 전체입니다.
Model.LoadInit / Model.LoadView{ init } / { view }
fetch 결과로 클라이언트 store를 채우기만 하고, 아무것도 그리지 않습니다.
삭제
둘 다 redirect를 받습니다. "back"이나, 삭제 뒤에 열 경로를 넘깁니다.
Model.Remove{ slice, modelId, children, name?, title?, description?, action?, modal?, redirect? }
children을 누르면 확인 modal이 열립니다. action을 바꾸면 삭제도 그 안에서 직접 해야 합니다.
Model.SureToRemove{ slice, modelId, name, trigger?, title?, description?, confirmLabel?, typeNameToRemove?, redirect? }
더 신중한 삭제입니다. typeNameToRemove면 사용자가 name을 다시 입력할 때까지 버튼이 잠깁니다.
예시
상품 하나의 수정·삭제 버튼과, 문구를 바꾼 생성 버튼입니다:
apps/shop/lib/product/Product.Util.tsx
  • 여는 요소를 바꾸는 것은 trigger뿐입니다. Model.New와 Model.Edit의 children은 폼 본문이고 className도 없습니다. Model.SureToRemove는 children을 아예 받지 않습니다.
  • 같은 slice에 생성 버튼을 하나 더 두면 namespace가 필요합니다. 버튼이 인페이지 에이전트에 공개하는 tool 이름 뒤에 붙습니다.
  • AdminPanel은 역할마다 General export를 씁니다. 없으면 첫 export를 쓰고(Unit은 Card를 먼저 봅니다), export가 하나도 없는 역할은 건너뜁니다.
  • export마다 자기 Suspense 경계가 있습니다. 페이지가 그려지고 한참 뒤, 사용자가 조작할 때 마운트되기 때문입니다.

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

내 AI에 이 문서 연결하기

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