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

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

  • Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
    사람함께에이전트▾
    사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
    함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
    에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
    소개▾
    시작하기기본 개념실습하기
    튜토리얼▾
    상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
    핵심 개념▾
    폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
    시스템 아키텍처▾
    아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
    사람함께에이전트▾
    사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
    함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
    에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
    소개▾
    시작하기기본 개념실습하기
    튜토리얼▾
    상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
    핵심 개념▾
    폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
    시스템 아키텍처▾
    아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
    이전UI 아키텍처다음비즈니스 서비스

    UI 구성

    주문 목록 화면 하나를 떠올려 보세요. 목록 자체 말고도 여섯 가지가 더 필요합니다:
    • 불러오는 동안 보여 줄 스켈레톤
    • 비었을 때 보여 줄 자리표시자
    • 하단의 페이지 이동 컨트롤
    • 새 주문을 만드는 모달
    • 주문을 고치는 두 번째 모달
    • 무언가를 지우기 전의 확인창
    배열 하나를 둘러싼 상태가 여섯 개인데, 그중 어느 것도 여러분의 제품은 아닙니다. akanjs/ui는 여섯 가지를 모두 제공하고, 생성된 store에 이미 연결해 두었습니다. 여러분은 행 하나와 상세 화면 하나만 쓰면 되고, 그 둘레의 셸이 loading·empty·paging·refresh와 CRUD 모달을 맡습니다.
    행은 내가 쓰고, 나머지는 셸이 그립니다
    주문 목록 화면에서 직접 쓰는 것은 행 컴포넌트 하나뿐입니다. Load.Units가 그 행을 반복하고, 아래에 페이지 이동 컨트롤을 그리고, 로딩과 빈 상태를 처리합니다. New 버튼은 Model.New 셸입니다.
    이 문서는 그 셸들의 목록과 조합 규칙입니다. 클라이언트 경계가 어디에 그어지는지는 앞 문서 UI 아키텍처에서 다룹니다.
    이 페이지에서 쓰는 말
    용어설명
    shell
    내 컴포넌트 둘레에 로딩, 빈 상태, 페이지 이동, 모달을 그려 주는 akanjs/ui의 완성된 컴포넌트입니다.
    slice
    icecreamOrderInPublic처럼 이름이 붙은 모듈의 목록 query입니다. 셸은 이것을 slice prop으로 받습니다.
    handle
    fetch.init, fetch.view, fetch.edit이 돌려주는 값입니다. await하거나, field마다 promise 하나씩 구조 분해합니다.
    Suspense boundary
    먼저 fallback을 보여 주고, 데이터가 도착하면 내용을 스트리밍해 채우는 자리입니다.
    akanjs/ui가 주는 것
    Export설명
    Load.UnitsLoad.ViewLoad.Edit
    fetch handle로 store를 채우고 loading·empty·list 상태를 그리는 데이터 셸입니다.
    Load.Stream
    promise 하나를 자체 Suspense 경계 뒤에서 기다립니다. 이미 해소된 값은 바로 그립니다.
    Model.NewModel.EditModel.SureToRemove
    생성된 store action에 연결된 생성·수정·삭제 모달입니다.
    Field
    모든 모델 field 타입의 컨트롤입니다. 모델 field에 맨 input을 쓰지 않습니다.
    TabLayoutLinkImageEmpty
    조합용 기본 요소입니다. Tab은 패널 본문을 서버에 남기고, Link는 locale 접두어를 붙입니다.
    cn
    akanjs/client의 유일한 클래스 병합 함수입니다. 호출자의 className을 마지막에 넘깁니다.
    직접 쓰는 컴포넌트는 어디에 두나
    직접 쓰는 쪽도 모델 하나에 묶이는지를 기준으로 똑같이 나뉩니다:
    lib/<model>/
    모델 하나에 묶인 것입니다. 모듈이 자기 행, 상세 화면, 폼, 액션을 직접 소유합니다.
    ui/
    여러 모델에서 재사용되고, 어느 하나에도 묶이지 않는 것입니다.
    둘 다 필요해 보이는 컴포넌트는 사실 컴포넌트 둘입니다. 각자 제자리에 하나씩 둡니다.
    서드파티 패키지는 lib를 거쳐 씁니다. page/**, barrel, 모듈 component 파일에서는 서드파티 패키지를 import할 수 없으므로, 먼저 lib에서 re-export합니다. libs/shared/ui/Field.tsx가 프레임워크의 Field에 Rich, Img, Map을 더해 확장하는 것도 그래서이며, 덕분에 각 앱이 에디터를 직접 import하지 않습니다.

    모델 화면의 모양

    모델 하나는 거의 언제나 같은 네 화면을 만들어 냅니다. 사용자는 목록을 훑고, 레코드를 만들고, 하나를 열어 보고, 다시 돌아와 고칩니다. 아래 화살표는 모두 이미 있는 셸이므로, 네 화면은 네 개의 workflow가 아니라 네 개의 작은 파일입니다.
    목록, 생성, 상세, 수정
    시작
    목록Load.Units
    생성Model.New
    상세Load.View
    수정Load.Edit
    끝
    생성
    제출
    Unit 선택
    수정
    제출
    뒤로
    Model.SureToRemove
    시작
    목록Load.Units
    생성Model.New
    상세Load.View
    수정Load.Edit
    끝
    시작 → 목록
    목록 → 생성생성
    생성 → 상세제출
    목록 → 상세Unit 선택
    상세 → 수정수정
    화면마다 맡은 일이 하나 있고, 그 일을 대신해 주는 셸이 하나 있습니다:
    목록 — 찾기
    Load.Units
    탐색을 위한 화면입니다. 검색하고, 훑고, 페이지를 넘기고, 하나를 고릅니다.
    생성 — 만들기
    Model.New
    Template 하나와 submit action으로 입력을 받습니다.
    상세 — 보기
    Load.View
    레코드 하나를 명확히 보여 준 뒤, 이어서 할 수 있는 동작을 제시합니다.
    수정 — 고치기
    Load.Edit
    생성과 같은 Template에 레코드의 현재 값을 채워 고칩니다.
    모든 화면 아래의 같은 스택
    네 화면 모두의 아래에서는 route부터 데이터베이스까지 같은 층이 같은 순서로 돕니다:
    route에서 데이터베이스까지
    page()route 진입점
    Unit · Viewserver component
    Zone · Template · Utilclient component
    fetch에 닿는 길은 둘, 규칙은 하나. route는 fetch를 직접 부르고, client component는 store action을 통해서만 닿습니다. 이 한 가지 규칙이 두 경로가 어긋나지 않게 합니다.
    매일 쓰는 표면은 생성된 helper 넷이 전부입니다:

    Load 셸

    loading, empty, list 상태를 직접 만들지 마세요. Load 셸이 세 가지를 대신해 줍니다:
    • route가 가져온 handle을 받습니다. 그래서 데이터는 page가 전송되기 전부터 이미 오는 중입니다.
    • 그 handle로 client store를 채웁니다. 그래서 hydration 이후에도 생성된 pagination·query·sort·refresh action이 계속 동작합니다.
    • 여러분의 행 컴포넌트 둘레에 loading, empty, list 세 가지 상태를 그립니다.
    셸설명
    Load.Units
    fetch.init<Model><Suffix>의 init을 받습니다. renderItem은 행 하나, renderList는 목록 전체를 그립니다.
    Load.View
    fetch.view<Model>의 view를 받습니다. renderView는 필수이고 empty가 자리표시자입니다.
    Load.Edit
    fetch.edit<Model>의 edit 또는 partial을 받습니다. slice는 필수이고 type으로 modal·form을 고릅니다.
    Load.Stream
    of는 promise나 값을 받고 children이 그립니다. slice의 x<Model>List<Suffix>를 받습니다.
    Load.PaginationLoad.Page
    페이지 이동 컨트롤 단독, 그리고 SSR·CSR 공용 page loader입니다.

    CRUD 모달

    레코드를 만들고, 고치고, 지우는 일은 모든 모델에 필요하지만 어떤 모델도 직접 구현할 필요가 없는 세 가지 workflow입니다. 각 셸은 다룰 slice를 받아 모듈의 Template을 열고, 제출하면 생성된 store action을 호출합니다.
    Model.New: trigger가 열고, children이 채웁니다
    trigger prop은 페이지에서 모달을 여는 버튼입니다. Model.New의 children은 버튼 라벨이 아니라 모달 안의 폼 필드이고, 모달 자체와 제출 버튼은 셸이 그립니다.
    apps/koyo/lib/icecreamOrder/IcecreamOrder.Util.tsx
    Util의 export는 endpoint 동사에서 모델 명사를 뺀 이름을 씁니다. 그래서 이 파일은 NewIcecreamOrder가 아니라 New와 Remove를 내보냅니다. 이 셸을 쓰기 전에 알아 둘 prop이 셋 있습니다:
    trigger
    모달을 여는 기본 버튼을 대체합니다. Model.New와 Model.Edit에서 children은 라벨이 아니라 모달 안에 들어갈 폼 본문이고, 둘 다 className을 받지 않습니다. Model.SureToRemove는 children을 아예 받지 않으므로 trigger가 컨트롤 전체입니다.

    폼은 store가 움직입니다

    모듈의 폼 컴포넌트인 Template은 자기 상태를 갖지 않습니다. 모든 컨트롤이 store의 <model>Form에서 key 하나를 읽고, 생성된 setter로 다시 씁니다. 여기서 두 가지가 따라옵니다:
    • Template에는 useState가 하나도 없습니다.
    • 폼 전체가 한곳에 있으므로, 저장해 둔 draft를 그대로 되돌려 넣을 수 있습니다.
    apps/koyo/lib/icecreamOrder/IcecreamOrder.Template.tsx

    하나의 어휘, 여러 화면

    사용자가 읽는 모든 문구는 모듈의 dictionary를 거칩니다. field 라벨, enum 값, 오류 메시지, 모델 이름까지 [en, ko] 쌍으로 한 번 선언하고, component는 key로 읽습니다.
    그래서 어휘는 그것을 표시하게 된 component들에 흩어지지 않고, 모델 바로 옆에 모여 있습니다.
    apps/koyo/lib/icecreamOrder/icecreamOrder.dictionary.ts
    component는 세 가지 방법 중 하나로 문구를 읽습니다:
    호출설명
    l("icecreamOrder.size")
    field 라벨입니다. 모델의 dictionary에서 읽습니다.
    l("icecreamOrder.modelName")

    이 페이지

    UI 구성
    모델 화면의 모양
    Load 셸
    CRUD 모달
    폼은 store가 움직입니다
    하나의 어휘, 여러 화면
    헬퍼설명
    fetch
    endpoint마다 함수 하나, slice마다 init·view·edit handle입니다. 서버 쪽에서 호출합니다.
    st
    st.use.*로 읽고 st.do.*로 씁니다. CRUD action은 생성됩니다.
    <Model>.*
    모듈의 Unit·View·Zone·Template·Util입니다. 역할로 이름 짓습니다: IcecreamOrder.Unit.Card.
    usePage
    l, l.trans, page context입니다. server component에서도 씁니다.
    가장 느린 query를 기다리지 않기
    route는 handle을 await하지 않고 구조 분해합니다. init field는 Zone에 넘기고, 남는 list promise는 Load.Stream에 넘깁니다:
    apps/koyo/page/(public)/icecreamOrder/_index.tsx
    • Load.Stream은 목록이 도착할 때까지 스켈레톤을 보여 주고, 도착하면 그것으로 합계를 그립니다.
    • Zone은 init을 받아 자기만의 경계 뒤에서 행을 그립니다.
    • 둘은 각자 자기 데이터가 도착하는 대로 그려지므로, page가 가장 느린 query를 기다릴 일이 없습니다.
    list와 insight는 Zone에 넘기지 않습니다. x<Model>List<Suffix>와 x<Model>Insight<Suffix>는 메서드를 가진 클래스 객체, 즉 hydrate된 모델 인스턴스로 해소됩니다. 서버가 client component에 prop을 넘기는 형식인 React Flight는 이를 거부합니다. server component 안이나 Load.Stream 안에서 소비하세요. 경계를 넘도록 만들어진 field는 init입니다.
    client 파일에서 fetch.init*을 호출하지 마세요. route에서는 첫 바이트 전에 해소되지만, hydration 이후에는 브라우저가 이미 그린 화면을 위해 왕복 두 번을 더 치릅니다. 클라이언트에서 다시 불러올 때는 생성된 st.do.init<Model><Suffix>()를 쓰세요.
    draft
    폼 복구이며 기본으로 켜져 있습니다. 사용자가 입력하는 동안 폼 전체를 저장했다가 다음에 열 때 돌려줍니다. scope는 edit이면 레코드 id, new면 seed와 route이고, 로그인한 사용자별로 나뉩니다. secret과 hidden 값은 저장하지 않습니다.
    name
    Model.SureToRemove가 확인창에 보여 주는 값입니다. typeNameToRemove를 켜면, 삭제 버튼이 활성화되기 전에 사용자가 이 값을 직접 입력해야 합니다.
    폼 값을 직접 저장하지 마세요. 예전의 field별 cache와 cacheKey prop은 deprecated이며 아무것도 저장하지 않습니다. 컨트롤 다섯 종류만 다뤘고, 번역된 라벨을 키로 썼고, 서버 데이터 위에 덮어썼기 때문입니다. draft={false}는 복구를 끄고, draft="<scope>"는 맥락이 id에도 seed에도 없을 때 scope를 직접 지정합니다.
    setter는 참조로 넘기세요. onChange={(v) => st.do.setSizeOnIcecreamOrder(v)}도 똑같이 동작하지만, 화살표 함수는 매번 새로 만들어지는 익명 클로저입니다. 그러면 컨트롤이 data-akan-action을 내보내지 않고 그 field의 에이전트 tool도 publish되지 않아서, 인페이지 에이전트와 E2E selector와 외부 브라우저 에이전트가 조용히 그 field를 잃습니다. 값 변환이 필요하면 컨트롤의 transform prop을 쓰세요.
    중첩된 행과 파일
    중첩된 행
    중첩된 값 하나는 경로로 쓰고, 행을 더하거나 뺄 때는 생성된 add<Field>OnX, sub<Field>OnX action을 씁니다.
    st.do.writeOnIcecreamOrder("toppings.3.name", value)
    이미지와 파일 field
    이미지나 파일 field는 File 모델에 대한 relation이고, store가 업로드 action을 생성합니다. data-URL 대체 구현을 직접 만들지 마세요.
    upload<Field>On<Model>(fileList)
    store action이 하는 일과 하지 않는 일
    하는 일
    • state를 읽고 fetch를 호출합니다.
    • loading·list·form 상태를 갱신합니다.
    • 결과가 있으면 this.set({ ... })으로 state에 씁니다.
    하지 않는 일
    • 값을 반환하지 않습니다. 모든 action은 st.do.<action>()으로 디스패치되고 void 타입이라 반환값에 아무도 닿지 못합니다.
    • 에러를 catch하지 않습니다. Err는 프레임워크가 토스트로 띄웁니다.
    • 비즈니스 규칙을 반복하지 않습니다. 비밀번호, 권한, 재고, 결제 규칙은 service에 남습니다.
    모델 자신의 이름입니다.
    l.trans({ en, ko })
    어느 모델에도 속하지 않는 일회성 문장입니다.
    usePage()가 셋 모두를 서버와 클라이언트 양쪽에서 해결하므로, 완전히 다국어인 화면도 문구 때문에 client 경계를 만들 일이 없습니다.
    같은 어휘, 같은 fetch, 같은 store가 고객 웹사이트와 관리자 콘솔과 파트너 포털과 모바일 앱을 함께 받칩니다. 이들은 별개의 앱이 아니라 한 앱의 client 표면이고, 어떤 화면이 어디에 속하는지는 인프라 결정이기 전에 제품 결정입니다. basePath가 각 표면에 자기 route와 layout과 permission을 어떻게 주는지는 다중 클라이언트 문서에서 다룹니다.
    수정 → 상세제출
    상세 → 목록뒤로
    상세 → 끝Model.SureToRemove
    store
    st.use · st.do
    fetch생성된 endpoint 호출
    signalendpoint · slice
    service
    document
    page()route 진입점
    Unit · Viewserver component
    Zone · Template · Utilclient component
    storest.use · st.do
    fetch생성된 endpoint 호출
    signalendpoint · slice
    service
    document