사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
데이터 레이어
데이터 레이어는 비즈니스 데이터 정의가 서버 로직과 화면 사용으로 이어지는 길입니다. 상품, 주문, 사용자, 예약, 청구서 같은 기능을 만들 때 비즈니스 형태가 실제 애플리케이션 동작이 되는 구간입니다.
Akan은 이 흐름을 모델 폴더 가까이에 모아둡니다. 예를 들어 상품 기능은 상품이 어떤 데이터인지, 어떻게 저장되는지, 재고와 가격 규칙이 어떻게 동작하는지, 페이지가 어떻게 상품 데이터를 불러오는지를 하나의 모듈에서 다룰 수 있습니다.

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


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

필터가 만들어 주는 것
document 파일에 선언한 query 하나는 메서드 하나가 아닙니다. Akan은 필터 키를 붙여 열네 개를 만듭니다. byOwner를 선언하면 listByOwner, countByOwner, updateOneByOwner를 비롯한 열네 개가 model과 service 양쪽에 생깁니다.
apps/shop/lib/product/product.document.ts

열넷 중 아홉은 읽기이고, 하나는 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 없음. 같은 체인을 가장 최근 하나로 좁힙니다.


이 네 개의 쓰기는 삭제에 부수 효과가 없는 모델에만 씁니다. cascade가 있거나, 저장된 파일을 지우는 _postRemove가 있거나, 실시간 목록이 지켜보고 있는 모델은 remove<Model>(id)로 한 건씩 삭제해야 합니다. 원자적 UPDATE 하나로는 그중 무엇도 실행할 수 없습니다.
모든 모델에는 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: 스케줄, 반복 작업, 큐, 유지보수 작업처럼 서버 내부에서 실행되는 일에 사용합니다.

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을 제공합니다.

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 상태와 비즈니스 동작을 일관되게 유지할 수 있습니다.


st는 클라이언트 컴포넌트에서 사용합니다. st.use.*나 st.do.*를 쓰는 컴포넌트에는 "use client"를 선언하세요. 서버 페이지에서는 보통 fetch로 초기 데이터를 불러오는 방식이 적합합니다.
페이지 데이터 스트리밍
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을 채웁니다
브라우저
Route 렌더
서버 쿼리
브라우저 → Route 렌더GET /:lang/shop/:shopId
Route 렌더 → 서버 쿼리fetch.initProductInShop(shopId)
Route 렌더 → 서버 쿼리fetch.initOrderInShop(shopId)
Route 렌더 → 브라우저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 전용입니다.


page가 즉시 필요한 것은 await하고 나머지는 스트리밍하세요. shell은 SEO 스냅샷, prerendering, hydration 이전 E2E가 읽는 대상이므로, 첫 화면이 의존하는 값 — 인증 게이트, 제목, link를 만드는 데 쓰는 id — 은 await한 호출에 두어야 합니다.
자주 하는 판단
코드를 어디에 둘지 헷갈릴 때는 비즈니스 질문에서 시작하면 됩니다. 각 파일이 한 종류의 질문에 답한다고 생각하면 데이터 레이어를 설계하기 쉬워집니다.
질문설명
어떤 필드를 가지나요?
model.constant.ts어떤 필드가 텍스트 검색 대상인가요?
model.constant.ts어떻게 저장하고 필터링하고 검색하나요?
model.document.ts어떤 업무 규칙이 실행되나요?
model.service.ts페이지가 무엇을 호출하고, 누가 호출할 수 있나요?
model.signal.ts클라이언트에서 어떤 상태를 공유하나요?
model.store.ts사용자에게 무엇을 보여주나요?
Model.View.tsx · Model.Zone.tsx

페이지 파일은 사용자 경험에 집중시키는 것이 좋습니다. 다른 페이지, 모바일 앱, 관리자 화면에서도 같은 규칙이 필요하다면 보통 그 규칙은 데이터 레이어에 두는 것이 맞습니다.