사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
소개▾
튜토리얼▾
핵심 개념▾
시스템 아키텍처▾

데이터 레이어

데이터 레이어는 비즈니스 데이터 정의가 서버 로직과 화면 사용으로 이어지는 길입니다. 상품, 주문, 사용자, 예약, 청구서 같은 기능을 만들 때 비즈니스 형태가 실제 애플리케이션 동작이 되는 구간입니다.
Akan은 이 흐름을 모델 폴더 가까이에 모아둡니다. 예를 들어 상품 기능은 상품이 어떤 데이터인지, 어떻게 저장되는지, 재고와 가격 규칙이 어떻게 동작하는지, 페이지가 어떻게 상품 데이터를 불러오는지를 하나의 모듈에서 다룰 수 있습니다.
모듈 하나, 데이터베이스에서 화면까지
lib/product 폴더 하나가 전체 경로를 담습니다. document가 데이터를 저장하고, service가 비즈니스 규칙을 실행하고, signal이 그것을 페이지에 열고, API 너머에서는 store가 UI를 위한 클라이언트 상태를 들고 있습니다. constant 파일은 모든 단계 아래에 깔려 있습니다. 각 단계가 그 형태를 다시 쓰기 때문입니다.

모델 형태

constant 파일은 비즈니스 객체의 설계도입니다. 상품에는 어떤 필드가 있는지, 어떤 값이 허용되는지, 가벼운 목록에서는 어떤 필드만 보여줄지 같은 질문에 답합니다.
상품 예시에서는 이름, 설명, 이미지 주소, 가격, 재고, 판매 상태 같은 카탈로그 정보를 모델에 둡니다. 이 정의는 서버와 클라이언트가 함께 이해할 수 있는 공통 기준입니다.
apps/shop/lib/product/product.constant.ts
Input: 데이터를 생성하거나 수정할 때 입력할 수 있는 필드입니다.
Object: 다른 모델 형태를 만들 때 기준이 되는 기본 객체 형태입니다.
Light: 목록, 카드, 연결된 데이터에 쓰기 좋은 가벼운 형태입니다.
Full: 상세 조회가 돌려주는 전체 레코드입니다. constant 파일에는 언제나 이 순서로 다섯 클래스를 모두 씁니다.
Insight: 목록 쿼리가 행과 함께 돌려주는 집계 값입니다. 비어 있어도 선언합니다.
다섯 클래스가 서로를 쌓는 방식
Input은 폼이 보내는 값입니다. Object는 시스템이 관리하는 저장 필드를 더합니다. Light는 목록이나 카드에 필요한 필드 몇 개만 고르고 공유 로직을 담습니다. Model은 Object와 Light를 합친 전체 레코드이고, Insight는 그 목록을 세는 집계입니다.
각 클래스가 나타나는 곳
생성·수정 폼은 Input을 보내고, 카드 목록은 Light를 보여주고, 상세 화면은 전체 Model을 보여주고, 목록 위의 합계는 Insight를 읽습니다.

Document와 Service

document 파일은 모델 형태를 저장 가능한 데이터로 바꿉니다. 데이터베이스에서 사용할 모델과, 앱이 데이터를 검색하거나 정렬할 때 쓰는 필터 형태를 정의합니다.
apps/shop/lib/product/product.document.ts
service 파일은 비즈니스 동작을 두는 곳입니다. 이 간단한 예시에서는 document가 자신의 재고를 늘리는 방법을 알고, service가 어떤 상품을 불러와 저장할지 결정합니다.
apps/shop/lib/product/product.service.ts
service는 결정하고, document는 스스로 바뀝니다
service가 데이터베이스에서 document를 불러오고, document가 스스로 검증하고 바꾼 뒤 자기 자신을 돌려주면, service가 다시 저장합니다.

필터가 만들어 주는 것

document 파일에 선언한 query 하나는 메서드 하나가 아닙니다. Akan은 필터 키를 붙여 열네 개를 만듭니다. byOwner를 선언하면 listByOwner, countByOwner, updateOneByOwner를 비롯한 열네 개가 model과 service 양쪽에 생깁니다.
apps/shop/lib/product/product.document.ts
필터 하나, 메서드 열넷
byOwner 같은 필터 하나가 읽기 아홉 개, slice용 query descriptor 하나, hook을 실행하지 않는 쓰기 네 개를 만듭니다.
열넷 중 아홉은 읽기이고, 하나는 query descriptor를 만들 뿐이며, 나머지 넷이 쓰기입니다. 조심해야 하는 쪽은 이 넷입니다. 각각 데이터베이스에 원자적 문장 하나를 보내므로 모델의 document hook이 전혀 실행되지 않습니다:
list<Filter>
읽기, hook 없음. hydrate된 도큐먼트를 최신순으로 돌려주며 skip·limit·sort·select 옵션을 받습니다.
listIds<Filter>
읽기, hook 없음. id만 돌려줍니다. 같은 옵션을 받되 select는 무시합니다.
find<Filter>
읽기, hook 없음. 가장 최근에 맞는 하나 또는 null입니다.
findId<Filter>
읽기, hook 없음. 그 하나의 id이거나 null입니다.
pick<Filter>
읽기, hook 없음. find와 같지만 없으면 예외를 냅니다. 행이 있다고 아는 호출에서 씁니다.
pickId<Filter>
읽기, hook 없음. 그 id이거나 예외입니다.
exists<Filter>
읽기, hook 없음. boolean이 아니라 맞는 id 또는 null입니다. 조건문에서는 boolean처럼 읽힙니다.
count<Filter>
읽기, hook 없음. 조건에 맞는 행의 개수입니다.
insight<Filter>
읽기, hook 없음. 모델의 Insight 집계를 plain record로 돌려줍니다. hydrate된 도큐먼트가 아닙니다.
query<Filter>
읽기도 쓰기도 아닙니다. slice의 exec이 돌려주는 query descriptor이며, 동기이고 데이터베이스에 닿지 않습니다.
remove<Filter>
쓰기, hook 없음. 맞는 모든 행을 원자적 soft delete 한 번으로 지우고 개수를 돌려줍니다.
removeOne<Filter>
쓰기, hook 없음. 같은 동작을 가장 최근 하나에만 합니다. 큐 항목을 집는 용도가 아니라 많아야 하나인 행에 씁니다.
update<Filter>
쓰기, hook 없음. 체인입니다. 수정할 값은 마지막 .set()에 넘기고, 체인을 만드는 것만으로는 아무 일도 없습니다.
updateOne<Filter>
쓰기, hook 없음. 같은 체인을 가장 최근 하나로 좁힙니다.
모든 모델에는 any 필터가 이미 있어서, 아무것도 선언하지 않아도 listAny와 countAny가 존재합니다.

Signal에서 UI까지

signal은 서버 동작을 페이지에서 사용할 수 있게 여는 레이어입니다. slice는 페이지가 목록이나 대시보드 관점의 데이터를 필요로 할 때 좋고, endpoint는 상품 재고 추가처럼 특정 동작을 실행해야 할 때 좋습니다.
apps/shop/lib/product/product.signal.ts
custom endpoint는 각각 자기 guards 배열을 선언하고, slice는 verb별로 guard를 지정합니다. guards는 MCP 노출 여부까지 결정합니다. guards를 선언하지 않은 endpoint는 agent 카탈로그에서 거부되므로, guards 배열을 빠뜨리면 권한뿐 아니라 노출까지 잃습니다.
slice: 공개 목록, 관리자 목록, 대시보드, 검색 결과처럼 데이터를 보여주는 관점에 사용합니다.
endpoint: 주문 취소, 요청 승인, 메시지 전송, 결제 완료처럼 동작을 실행할 때 사용합니다.
internal: 스케줄, 반복 작업, 큐, 유지보수 작업처럼 서버 내부에서 실행되는 일에 사용합니다.
밖으로 난 문 둘, 안에서 도는 일 하나
slice와 endpoint는 페이지가 닿을 수 있는 두 개의 문이고 각각 guard 뒤에 있습니다. internal signal은 서버 안에서 작업을 돌리며 문이 아예 없습니다.

Fetch와 Store 인스턴스

signal을 선언하면 Akan은 @apps/<app>/client에서 앱 전용 클라이언트 helper를 제공합니다. 이때 가장 자주 보게 되는 이름이 fetch와 st입니다.
서버 데이터를 호출하거나 Akan UI 컴포넌트에 slice 정보를 넘길 때는 fetch를 사용합니다. 클라이언트 컴포넌트가 현재 상태를 읽거나 store action을 실행해야 할 때는 st를 사용합니다.
fetch: 생성된 요청 인스턴스입니다. endpoint 호출, slice 초기화, view 로딩을 수행하고 fetch.slice.* 메타 정보를 제공합니다.
st: 생성된 클라이언트 store 인스턴스입니다. 상태를 읽는 st.use.* hook과 상태를 변경하는 st.do.* action을 제공합니다.
fetch를 부르는 쪽, st를 쥐는 쪽
서버 컴포넌트는 fetch를 직접 부릅니다. 클라이언트 컴포넌트는 st.use로 store를 읽고 st.do로 바꾸며, fetch를 부르는 것은 store의 action입니다.
Server action: call addStock with fetch
endpoint 인자는 선언 순서대로 위치 인자로 넘깁니다. 호출 결과는 endpoint가 선언한 반환값 그대로입니다. addStock은 cnst.Product를 반환하므로 await한 값도 감싸는 객체가 아니라 상품 자체입니다.
이 패턴은 페이지, action, 서버 측 helper가 비즈니스 동작을 실행해야 할 때 유용합니다. 생성된 fetch 인스턴스가 서버 endpoint를 호출하고 타입이 지정된 결과를 돌려줍니다.
Client zone: pass fetch.slice metadata to UI components
fetch.slice.product는 상품 데이터 자체가 아닙니다. Akan UI 컴포넌트가 어떤 모델 slice를 조회, 수정, 새로고침, 삭제해야 하는지 알 수 있게 해주는 slice 메타 정보입니다.
Client form: read and change state with st
클라이언트 컴포넌트에서 st.use.*는 현재 store 값을 읽고, st.do.*는 생성된 action을 실행합니다. 이렇게 하면 여러 화면에서 form 상태와 비즈니스 동작을 일관되게 유지할 수 있습니다.

페이지 데이터 스트리밍

fetch.init<Model><Suffix>, fetch.view<Model>, fetch.edit<Model>는 route가 화면 데이터를 불러올 때 쓰는 세 helper입니다. 각각 await도 되고 구조분해도 되는 handle을 반환합니다. await하면 payload 객체를 주고, field를 읽으면 그 field의 promise를 줍니다.
차이는 page가 어디서 기다리는지입니다. await한 호출은 query가 끝날 때까지 route 전체를 붙잡으므로 그 아래 아무것도 전송되지 않습니다. 반면 Zone이나 Load.Stream에 넘긴 promise는 그 component 안에서, 자체 Suspense boundary 뒤에서 await됩니다 — page의 나머지는 이미 전송된 상태이고 각 section은 자기 data가 도착하는 대로 채워집니다.
페이지가 기다리는 지점
브라우저
Route 렌더
서버 쿼리
GET /:lang/shop/:shopId
fetch.initProductInShop(shopId)
fetch.initOrderInShop(shopId)
shell HTML, 섹션마다 경계 하나
productInitInShop이 product zone을 채웁니다
orderInitInShop이 order zone을 채웁니다
productListInShop이 Load.Stream을 채웁니다
Server page: hand each promise to the section that renders it
x<Model>Init<Suffix>: plain list와 insight data입니다. client Zone의 prop으로 넘길 수 있는 유일한 field입니다.
x<Model>List<Suffix> / x<Model>Insight<Suffix>: hydrate된 model instance입니다. React Flight가 client prop으로 거부하므로 server component나 Load.Stream 안에서 사용합니다.
x<Model>View / x<Model>Edit: Load.View, Load.Edit에 넘기는 단일 model payload입니다. 형제 field인 x<Model>은 hydrate된 model이므로 같은 이유로 server 전용입니다.

자주 하는 판단

코드를 어디에 둘지 헷갈릴 때는 비즈니스 질문에서 시작하면 됩니다. 각 파일이 한 종류의 질문에 답한다고 생각하면 데이터 레이어를 설계하기 쉬워집니다.
어떤 필드를 가지나요?
model.constant.ts
어떤 필드가 텍스트 검색 대상인가요?
model.constant.ts
어떻게 저장하고 필터링하고 검색하나요?
model.document.ts
어떤 업무 규칙이 실행되나요?
model.service.ts
페이지가 무엇을 호출하고, 누가 호출할 수 있나요?
model.signal.ts
클라이언트에서 어떤 상태를 공유하나요?
model.store.ts
사용자에게 무엇을 보여주나요?
Model.View.tsx · Model.Zone.tsx

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

내 AI에 이 문서 연결하기

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