사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
akanjs/fetch
akanjs/fetch에는 route가 가져온 데이터를 컴포넌트까지 나르는 타입과, 그 호출을 실제로 보내는 client가 들어 있습니다. Zone 파일은 여기서 타입만 import type으로 가져옵니다.이 페이지에서 쓰는 말
용어설명
handle
fetch.init*, fetch.view*, fetch.edit*가 돌려주는 값입니다. await하거나, 필드 하나만 꺼내 씁니다.payload
userInitInOrg처럼 Zone이 받는 일반 데이터입니다. 서버에서 클라이언트로 넘길 수 있습니다.hydrated instance
cnst.User나 DataList 같은 모델 class 인스턴스입니다. 서버 컴포넌트 안에서만 씁니다.slice
모델 하나에 이름을 붙인 목록 query입니다.
user의 inOrg slice에서 fetch.initUserInOrg가 나옵니다.Zone
모듈의 클라이언트 컴포넌트입니다. payload로 store를 채우고 화면에 그립니다.
export 목록
export설명
InitHandleViewHandleEditHandle
fetch.init*, fetch.view*, fetch.edit*의 반환 타입입니다. await하거나 필드별로 나눠 씁니다.Zone
init prop의 타입입니다. 목록 payload 또는 그 promise입니다.ClientViewClientEdit
레코드 하나를 받는 Zone
view, edit prop의 타입입니다.ServerInitServerViewServerEdit
위 세 타입에서 promise를 뺀, 이미 해소된 payload입니다.
컴포넌트가 다루는 slice를 가리킵니다.
refName, sliceName, argLength 세 필드입니다.목록을 불러올 때의 옵션입니다. page, limit, sort, insight, 기본값, invalidate를 정합니다.
QuerySetting
{ queryKey, args } 형태로, root slice 목록 컴포넌트가 실행할 filter를 고릅니다.로그인 토큰에 담긴 계정 정보입니다.
모든 앱의
fetch 뒤에서 실제로 요청을 보내는 client입니다.getRequestheaderscookies
지금 렌더링 중인 page의 요청을 읽습니다. 서버 전용입니다.
그 밖의 export
HttpClient, WsClient, AgentTurn, getRequestTheme 같은 request store helper, 생성된 client가 쓰는 타입입니다.InitHandle / ViewHandle / EditHandle
route에서 부르는
fetch.init*, fetch.view*, fetch.edit*는 handle을 돌려줍니다. await하면 예전과 같은 객체가 나오고, 필드 하나를 꺼내면 그 필드만의 promise가 나옵니다.그래서 섹션마다 자기 데이터가 도착하는 대로 그려지고, page 전체가 가장 느린 query를 기다리지 않습니다.
handle 세 가지
| handle | 돌려주는 호출 |
|---|---|
| ↳ 필드 | |
InitHandle | fetch.init<Model><Suffix>(...args, option?) |
| <model>Init<Suffix> · <model>List<Suffix> · <model>Insight<Suffix> | |
ViewHandle | fetch.view<Model>(id, option?) |
| <model> · <model>View | |
EditHandle | fetch.edit<Model>(id, option?) |
| <model> · <model>Edit | |
page 나누기
handle을 await하지 말고 구조 분해해서, 필드마다 그것을 그리는 섹션에 넘깁니다:
apps/myapp/page/org/[orgId]/_index.tsx
- 요청은 호출하는 순간 모두 출발합니다. 결과를 나눠 받아도 query가 차례로 실행되지 않습니다.
- 목록이 먼저 도착합니다.
<model>List<Suffix>는 행이 오면 바로 해소됩니다.<model>Init<Suffix>는lastPageOf<Model>을 담아야 해서 개수까지 기다립니다. - 첫 HTML에 꼭 있어야 하는 것은 await합니다.
await하면 전체 객체가 나오고 그 섹션이 shell에 들어가므로, SEO 스냅샷과 prerender가 읽을 수 있습니다. - payload만 필요하다면
fetch.get<Model>Init<Suffix>,fetch.get<Model>View,fetch.get<Model>Edit가 그것만 일반 promise로 돌려줍니다.
필드별로 넘기는 곳
필드
Zone
init · view · edit
서버
Unit · View · Load.Stream
일반 payload
<model>Init<Suffix>
✓
목록 payload입니다. Zone의
init prop에 넘깁니다.<model>View · <model>Edit
✓
레코드 하나의 payload입니다. Zone의
view나 edit prop에 넘깁니다.hydrate된 인스턴스
<model>List<Suffix>
✓
Light 모델의
DataList입니다. 개수를 셀 목록 같은 데 씁니다.<model>Insight<Suffix>
✓
집계 결과를 담은 Insight 모델 인스턴스입니다.
<model>
✓
레코드 하나의 full 모델 인스턴스입니다.
✓여기로 넘김넘기지 않음


hydrate된 인스턴스는 Zone에 넘기지 않습니다. React Flight는 class 인스턴스를 클라이언트 컴포넌트 prop으로 받지 않습니다. 그래서
<model>List<Suffix>, <model>Insight<Suffix>, <model>는 서버 컴포넌트에서만 씁니다.ClientInit
ClientInit은 Zone init prop의 타입입니다. 해소된 목록 payload도, init handle이 주는 <model>Init<Suffix> promise도 받습니다. 아직 대기 중인 promise는 Zone 자체의 Suspense 경계 뒤에서 그려집니다.목록 Zone은 이렇게 선언합니다:
apps/myapp/lib/user/User.Zone.tsx
타입 인자
보통 두 개면 됩니다. ref name과 Light 모델만 적고, 나머지 셋은 기본값
any로 둡니다.RefNamestring
"user" 같은 모델의 ref name입니다. payload의 key 이름이 이것을 따릅니다.Light
각 행의 Light 모델입니다. 예:
cnst.LightUser.Insight기본값 any
집계 결과의 Insight 모델입니다.
QueryArgs기본값 any
slice가 받는 인자 tuple입니다.
Filter기본값 any
모델의 filter class입니다. sort key의 타입을 정합니다.
payload에 든 것
앞의 세 key를 빼면 모든 key 이름에 모델 이름이 들어갑니다.
user라면 행 목록은 userObjList입니다:key설명
refNamesliceNameargLength
목록이 나온 slice입니다.
SliceMeta와 같은 세 필드입니다.<model>ObjList
일반 객체로 된 행 목록입니다.
<model>ObjInsight
집계 결과를 담은 일반 객체입니다.
insight: false로 불러왔다면 null입니다.pageOf<Model>limitOf<Model>lastPageOf<Model>
현재 페이지, 페이지 크기, 그리고 개수로 계산한 마지막 페이지입니다.
hasMoreOf<Model>
다음 묶음이 있는지 여부입니다. 개수가 아니라 받아 온 묶음의 크기로 판단합니다.
queryArgsOf<Model>sortOf<Model>
목록을 불러올 때 쓴 인자와 sort key입니다.
<model>InitAt
목록을 불러온 시각입니다.
ClientView / ClientEdit
ClientView와 ClientEdit은 레코드 하나를 받는 Zone prop의 타입입니다. 둘 다 해소된 payload와, view·edit handle이 주는 promise를 모두 받습니다.| 타입 | handle 필드 |
|---|---|
| ↳ 받는 곳 | |
ClientView | fetch.view<Model>(id) → <model>View |
Load.View의 view prop입니다. | |
ClientEdit | fetch.edit<Model>(id) → <model>Edit |
Load.Edit의 edit prop입니다. | |
두 payload는 같은 key 세 개를 가집니다:
key설명
refName
모델의 ref name입니다.
<model>Obj
일반 객체로 된 레코드입니다.
<model>ViewAt
레코드를 불러온 시각입니다. edit payload도 같은 key를 씁니다.
ticket 하나를 보여 주고 고치는 Zone입니다:
apps/myapp/lib/ticket/Ticket.Zone.tsx
- 새 레코드에는 요청이 필요 없습니다.
Load.Edit과Model.EditModal의editprop은 일부만 채운 모델도 받습니다. 그래서 새 레코드 page는 payload 대신 기본값만 넘깁니다. - full 모델은 따로 있는 필드입니다. 같은 handle의
<model>은 서버 컴포넌트용 hydrate된 인스턴스이고, Zone은 payload만 받습니다.
SliceMeta
SliceMeta는 컴포넌트가 다루는 slice를 가리킵니다. Model.*, Data.* 컴포넌트는 이것을 slice prop으로 받아, 저장한 뒤 어느 store의 어느 목록을 고칠지 압니다.필드설명
refName
"ticket" 같은 모델의 ref name입니다.sliceName
ref name에 slice 접미사를 붙인 이름입니다. 예:
ticketInProject. root slice는 ref name 그대로입니다.argLength
slice가 받는 query 인자 개수입니다.
fetch.slice에서 하나를 꺼내 쓰고, 컴포넌트에서는 선택 prop으로 받습니다:apps/myapp/lib/ticket/Ticket.Util.tsx
fetch.slice에는 slice마다 하나씩 들어 있습니다. key는sliceName이고, 타입은 앱의 signal에서 나옵니다.- 목록 payload에도 같은 세 필드가 들어 있습니다. 그래서
Load.Units는init만 보고 자기 slice를 찾습니다.
FetchInitForm
FetchInitForm은 목록을 불러올 때 쓰는 옵션 객체입니다. 몇 페이지를, 몇 행씩, 어떤 순서로 불러올지와 개수를 셀지 정합니다. fetch.init<Model><Suffix>()와 st.do.init<Model><Suffix>()의 마지막 인자로 넘깁니다.pagenumber기본값 1
불러올 페이지입니다. 1부터 셉니다.
limitnumber기본값 20
한 페이지의 행 수입니다.
sortExtractSort<Filter>기본값 "latest"
filter의 sort key 중 하나입니다.
latest, oldest, relevance는 항상 있습니다.insightboolean기본값 true
false면 집계 query를 보내지 않습니다. <model>ObjInsight는 null이고 총계도 없습니다.defaultPartial<DefaultOf<Input>>st.do.init*
slice의 form이 처음에 채우는 값이며, 저장할 때마다 이 값으로 돌아갑니다.
invalidateboolean기본값 falsest.do.init*
false면 같은 인자, page, limit, sort로 이미 불러온 목록을 다시 씁니다.타입 인자
Input과 Filter가 default와 sort의 타입을 정합니다. st.do.init* 표시가 붙은 필드는 store만 읽습니다. 위 기본값은 fetch.init* 기준이고, st.do.init*는 page, limit, sort를 빼면 목록의 현재 값을 그대로 씁니다.총계를 보여 주지 않는 멤버 목록은 개수를 세지 않고 불러옵니다:
apps/myapp/page/org/[orgId]/member.tsx
- 총계도 페이지 이동도 없는 화면이면
insight: false를 넘깁니다. 그러면 손에 든 행이 곧 전체 개수입니다. - 같은 객체에 호출 단위 옵션도 넣습니다.
fetch.init*는token,timeout같은FetchPolicy필드도 함께 받습니다. - 목록 컴포넌트는 이것을 init prop으로 받습니다.
Data.CardList,Data.TableList,Data.ListContainer가 그대로st.do.init*에 넘깁니다.
Account
Account는 로그인 토큰에 담긴 계정 정보입니다. appName과 environment는 항상 있고, 타입 인자로 앱만의 claim을 더합니다.현재 계정은
akanjs/client의 getAccount()로 읽습니다. page 렌더링 중에도, 브라우저에서도 됩니다:apps/myapp/webkit/cookie.ts
- 다른 앱의 토큰은 로그아웃 상태로 읽힙니다. 토큰이 다른 앱이나 환경에서 발급됐다면
getAccount()는{ appName, environment }만 돌려줍니다. getDefaultAccount()가 그 로그아웃 상태의 값입니다. 현재 env의appName과environment만 담습니다.- 서버도 같은 모양으로 풉니다.
libs/shared의AccountMiddleware가 호출마다 이 값을 붙이고, guard는context.get("account")로 읽습니다.
FetchClient
FetchClient는 앱의 signal 정보를 타입이 붙은 HTTP·WebSocket 함수로 바꿉니다. 앱이 import하는 fetch는 인스턴스 하나를 감싼 proxy라서, 인스턴스 메서드를 fetch에서 바로 부를 수 있습니다.멤버설명
new FetchClient(origin)
getEnv().serverHttpUri 같은 API origin으로 client를 만듭니다. origin에는 prefix까지 들어갑니다.setJwt(jwt)
이후의 모든 호출에 이 토큰을 붙입니다. HTTP와 WebSocket 모두입니다.
null이면 지웁니다.clone({ origin, jwt, connect })
같은 endpoint를 가진 복사본을 다른 origin이나 사용자용으로 만듭니다.
connect 기본값은 true입니다.setTimeout(ms)
호출도 endpoint도 제한 시간을 정하지 않았을 때 쓰는 기본값입니다. 기본 30초이며,
false면 제한이 없습니다.connect()disconnect()
pubsub, message endpoint가 쓰는 WebSocket을 열거나 닫습니다.fetch.instance
앱의
fetch proxy 안에 든 FetchClient입니다.FetchClient.fromFetchClient.build
생성된
lib/sig.ts와 lib/useClient.ts에서 앱의 fetch를 만듭니다.signal 테스트는
clone으로 로그인한 사용자처럼 서버를 부릅니다:apps/myapp/lib/user/user.signal.spec.ts
- 사용자마다 복사본 하나. 복사본마다 자기 토큰을 들고 있어서, 두 사용자가 같은 서버를 나란히 부를 수 있습니다.
connect: false면 WebSocket을 열지 않습니다. HTTP만 부르는 복사본에 씁니다. API explorer가 이렇게 복사합니다.- API prefix를 직접 적지 않습니다.
akanjs/base의getEnv().serverHttpUri가 이미 prefix까지 담고 있습니다.
getRequest / headers / cookies
이 함수들은 지금 렌더링 중인 page의 요청을 읽습니다.
akanjs/fetch는 클라이언트 코드를 끌어오지 않으므로, 서버 컴포넌트에서 부담 없이 import할 수 있습니다.getRequest()Request | undefined
지금 렌더링 중인 요청입니다.
headers()Map<string, string>
요청 헤더이며 key는 소문자입니다. 부를 때마다 새 Map을 만듭니다.
cookies()Map<string, { name, value }>
Cookie 헤더를 파싱한 값입니다. j:로 시작하는 값은 JSON으로 풀어 줍니다.getRequestStore()AkanRequestStore | undefined
요청마다 하나인 store 전체입니다. 요청, theme, query cache가 들어 있습니다.
page는 렌더링하는 동안 이 값을 읽을 수 있습니다:
apps/myapp/page/_index.tsx
- page를 렌더링하는 동안에만 요청이 보입니다. 그 밖에서는 Map이 비어 있고
getRequest()는undefined입니다. - endpoint는 다른 방법으로 호출자를 읽습니다. signal에서는
.with(Self), guard에서는context.get("account")를 씁니다. - 양쪽에서 도는 코드는
akanjs/client를 씁니다. 그쪽의getCookie(key)는 서버에서는 요청을, 브라우저에서는document.cookie를 읽습니다.