사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
적은 코드로 CRUD 만들기
CRUD는 보통 가장 먼저 만드는 화면입니다. 목록을 보고, 하나를 열고, 새로 만들고, 고치고, 지웁니다. Akan에는 이 다섯 가지에 쓸 store 액션과 컴포넌트가 이미 있어서, 아래 세 조각과 이를 배치할 page만 직접 쓰면 됩니다.
직접 쓰는 것설명
post.signal.ts
slice입니다. 이 화면이 어떤 레코드를 읽고 고칠 수 있는지 정합니다.
Post.Template.tsx
폼 필드를 그립니다. 생성과 수정이 같은 Template을 씁니다.
Post.Zone.tsx
Load와 Model 컴포넌트로 slice를 화면 동작에 연결합니다.할 일마다 쓰는 컴포넌트와, 자동으로 생기는 store 액션은 다음과 같습니다:
| 할 일 | store 키 · 액션 |
|---|---|
| ↳ 컴포넌트 | |
| 목록 | postListInAdmin |
| Load.Units | |
| 열기 | viewPost(id) |
| Model.ViewWrapper + Model.ViewEditModal | |
| 생성 | newPost → submitPost |
| Load.Edit · Model.New | |
| 수정 | editPost(id) → submitPost |
| Load.Edit · Model.Edit | |
| 삭제 | removePost(id) |
| Model.Remove | |
Slice부터 시작하기
Slice는 모델을 바라보는 이름 붙은 창, 즉 한 화면이 보여 줄 목록입니다.
inPublic, inAdmin, inProject처럼 화면 목적이 드러나는 이름을 붙이세요.1. document에 filter 선언하기
"공개"가 무엇인지 document에 적습니다. filter마다 service에
query<Name>() 메서드가 생깁니다:apps/blog/lib/post/post.document.ts
2. signal에 slice로 내보내기
그다음 signal에 화면마다 키 하나씩 slice로 적습니다:
apps/blog/lib/post/post.signal.ts
queryInPublic()은 filter에서 생깁니다.queryAny()는 모든 모델에 있는anyfilter에서 생깁니다.- 키가 fetch 메서드 이름이 됩니다.
inAdmin에서fetch.initPostInAdmin()과fetch.slice.postInAdmin이 생기고, 다음 단계에서 이것을 씁니다. - query는 반환만 하고 다듬지 않습니다. 정렬과 페이지 크기는 query의
.sort()가 아니라 fetch 옵션({ sort, page, limit })으로 정합니다.
누가 무엇을 부를 수 있나
guards 옵션은 자동으로 생기는 endpoint를 지키고, 이름 있는 slice는 자기 guard를 따로 가집니다:키지키는 것
root
어떤 filter든 실행할 수 있는 root slice
initPost(queryKey, args)이므로, 항상 Admin으로 둡니다.get
레코드 하나를 읽는
fetch.post(id)이며, viewPost와 editPost도 이것을 부릅니다.cru
createPost, updatePost, removePost이며, create, update, remove 키로 하나씩 따로 덮어쓸 수 있습니다.init({ guards })
이름 있는 slice의 목록이며, 위의
guards 옵션은 여기에 닿지 않습니다.

이름 있는 slice는 자기
init({ guards })로만 지켜집니다. get과 cru는 여기에 닿지 않으므로, guard 없는 inAdmin: init()은 누가 호출하든 모든 post를 내줍니다. None은 endpoint를 모두에게 닫습니다.목록과 열기
page가 목록의 첫 페이지를 서버에서 불러옵니다. Zone이
Load.Units로 목록을 그리고, 목록 옆의 Model.ViewEditModal이 상세 보기와 수정 폼을 함께 처리합니다.1. page에서 불러오기
page는 slice query를 시작하고 그 promise를 Zone에 넘깁니다:
apps/blog/page/admin/post/_index.tsx
2. Zone에서 그리기
카드마다 자기 post를 열고, 모달 하나가 지금 열린 post를 보여 줍니다:
apps/blog/lib/post/Post.Zone.tsx
- Zone에 두는 이유.
renderItem,renderView,renderTemplate은 함수인데, 서버 page는 클라이언트 컴포넌트에 함수를 넘길 수 없습니다. - 모달 하나가 모든 카드를 맡습니다.
Model.ViewWrapper가st.do.viewPost(id)를 부르면, 모달이renderView를 그립니다. - 수정 기능이 들어 있습니다. 모달의 편집 버튼을 누르면
renderTemplate으로 바뀌고, 저장하면 상세 보기로 돌아옵니다. ⋮ 메뉴에는 삭제가 있고,menu={false}로 숨깁니다. - 빈 목록과 페이지 처리는 필요 없습니다.
Load.Units가 행이 없으면<Empty />를 보여 주고, 기본으로 페이지를 나눕니다.
생성하고 수정하기
생성과 수정에는 같은 Template을 씁니다. 폼을 감싸는 셸 컴포넌트가
postForm을 준비하고 저장하므로 Template은 필드만 신경 쓰면 됩니다. 폼에 id가 없으면 새로 만들고, 있으면 수정합니다.폼이 나타날 자리에 맞춰 셸을 고릅니다:
셸두는 곳과 여는 방법
Load.Edit
new.tsx 같은 전용 page에 두며, 그 경로에 들어오면 폼이 바로 열립니다.Model.New
어디든 두면 새로 만들기 버튼을 직접 그리고, 누르면
st.do.newPost()를 부릅니다.Model.Edit
어디든 두면 편집 버튼을 직접 그리고, 누르면
st.do.editPost(id)를 부릅니다.Model.ViewEditModal
Zone 안 목록 옆에 두며, 상세 보기 안의 편집 버튼이 폼을 엽니다.
생성 page
생성 page는 새 post가 가질 초깃값으로 폼을 채웁니다:
apps/blog/page/admin/post/new.tsx
수정 page
수정 page는 레코드를 서버에서 불러와 같은 셸에 넘깁니다:
apps/blog/page/admin/post/[postId]/edit.tsx
type="form"은 폼을 그 자리에 그립니다. 저장 버튼도 함께 그리며, 기본값인"modal"은 폼을 모달로 엽니다.slice는 새 post가 들어갈 목록을 정합니다.postInAdmin으로 만든 post는postListInAdmin맨 위에 들어가고, 수정은 이미 불러온 모든 목록에 반영됩니다.onSubmit과onCancel에는 경로를 넘깁니다."back"은 뒤로 가고,onSubmit경로 속[postId]는 저장된 post의 id로 바뀝니다.- 실수로 닫아도 폼이 남습니다. 셸은 입력하는 동안 폼을 저장해 두었다가 다음에 열 때 되살려 줍니다.
draft={false}로 끕니다.
모달에서 수정하기
화면을 떠나지 않고 고치려면,
Model.Edit이 편집 버튼과 모달을 함께 그립니다:apps/blog/lib/post/Post.Util.tsx
- 누르면 레코드를 불러옵니다.
st.do.editPost(postId)가postForm을 채우고 모달을 엽니다. - 버튼과 모달을 따로 둬야 하나요?
Model.Edit은Model.EditWrapper(트리거)와Model.EditModal id={postId}(모달)를 합친 것입니다. 모달만 두면editPost가 불리기 전까지 아무것도 열리지 않습니다. - 생성용 짝은
Model.New입니다.modelId대신partial로 새 폼의 초깃값을 넘깁니다.
삭제는 Util에 두기
삭제 버튼은 카드, 상세 보기, 메뉴처럼 여러 곳에 나타나곤 합니다.
Post.Util.tsx의 Edit 옆에 Remove 하나를 두면 Unit, View, Zone 파일이 단순해집니다:apps/blog/lib/post/Post.Util.tsx
- 먼저 확인을 받습니다. 누르면 확인 모달이 열리고, 확인하면
st.do.removePost(postId)가 불린 뒤 성공 토스트가 뜹니다. - 모든 목록에서 행이 빠집니다. 삭제는 id로 하므로 이 모델의 어떤 slice를 넘겨도 됩니다.
fetch.slice.post는 root slice입니다. - 상세 page라면
redirect="back"이나 경로를 넘겨, 삭제한 뒤 다른 곳으로 이동합니다.name은 확인 문구를 채우고,title,description,action은 모달의 각 부분을 바꿉니다.
꿀팁
- Slice 이름은 DB query가 아니라 화면 목적에 맞춥니다.
inAdmin은 목록을 어떻게 가져오는지가 아니라 누가 보는지를 말합니다. - Template은 단순하게 둡니다.
postForm을 읽고 필드를 그리며, setter는onChange={st.do.setTitleOnPost}처럼 그대로 넘깁니다. - 반복되는 동작은 Util로 뺍니다. 삭제, 발행, 승인, 대화상자 열기는
Post.Util.tsx에 한 번 쓰고 어디에든 둡니다.
더 읽기