사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
앱 & 라이브러리▾
도메인▾
스칼라▾
Model.Unit.tsx
Unit 파일은 모델의 레코드 하나를 그립니다. 카드, 한 줄짜리 행, 아바타, 갤러리 타일, 테이블 열을 그리는 helper가 대표적이며, 이 모델을 보여 주는 목록과 관계 화면은 모두 이 export를 가져다 씁니다.
목록에 새 모양이 필요하거나 행에 필드를 하나 더 보여 줘야 할 때 이 파일을 엽니다. Unit은 그리기만 하고, 나머지 일은 각자 맡는 파일이 있습니다:
무엇을
Unit
Util
Template
Store
page
Unit이 직접 하는 일
Light 모델의 필드
✓
제목, 상태, 날짜 같은 값을 카드, 행, 타일로 그립니다.
usePage · l()
✓
번역은 서버에서도 되므로, 라벨 때문에 클라이언트 코드를 쓸 일이 없습니다.
Link · href
✓
이동은 Unit이 맡고, 어디로 갈지는 호출하는 쪽이 정합니다.
다른 파일에 맡기는 일
onClick
✓
수정, 삭제 같은 작은 동작은 Unit이 렌더링하는 Util이 맡습니다.
Field.*
✓
폼은 Template이 맡고, 목록 항목 안에는 넣지 않습니다.
st.use · st.do
✓
✓
규모가 큰 상호작용은 Util이 시작하고 store 액션이 처리합니다.
fetch.*
✓
데이터는 page가 불러오고, 레코드를 하나씩 prop으로 Unit에 넘깁니다.
✓여기에 둠여기가 아님
이 페이지에서 쓰는 말
용어설명
cnst.Light<Model>
모델의 가벼운 버전입니다. constant가 목록용으로 고른 필드와 표시용 메서드만 가집니다.
서버 컴포넌트
"use client"가 없는 컴포넌트입니다. 서버에서 HTML이 되고, JavaScript는 브라우저로 가지 않습니다.
slice
inProject처럼 이름이 붙은 목록 query입니다. 이 이름이 자동으로 생기는 이름 끝의 <Suffix> 자리에 들어갑니다.hydrate
서버가 이미 불러온 데이터를 브라우저의 store에 채우는 일입니다. 같은 데이터를 두 번 불러오지 않습니다.
ModelProps와 Light 모델
Unit의 props 타입은
ModelProps<"article", cnst.LightArticle>로 씁니다. 모델 이름을 딴 레코드 prop에 className, href, 목록 컴포넌트가 채워 주는 prop 몇 개가 더해집니다.Unit은 목록에서 여러 번 렌더링되므로 Light 모델을 받습니다. 가장 작은 완성형 Unit 파일입니다:
apps/koyo/lib/article/Article.Unit.tsx
Layout.Unit이 기본 컨테이너입니다. 안쪽 여백이 있는 세로 레이아웃이며,href가 있으면 링크가 되고 없으면 평범한div가 됩니다.- Light 필드만 읽습니다. Light 모델에는 constant가 목록용으로 고른 필드만 있으므로, 전체 모델의 필드가 있다고 가정하지 않습니다.
- 표시 로직은 Light 클래스에 둡니다. 라벨이나 조건 판단은
admin.label()같은 메서드로 만들고, Unit은 호출만 합니다. - prop을 더하려면 ModelProps를 확장합니다.
interface MiniProps extends ModelProps<"article", cnst.LightArticle>를 컴포넌트 바로 위에 선언합니다.
ModelProps가 주는 props
마지막 세 prop은 Unit을
renderItem으로 바로 받는 Data.ListContainer가 채워 줍니다:articlecnst.LightArticle필수
그릴 레코드입니다. prop 이름은 첫 번째 타입 인자가 정합니다.
classNamestring
호출하는 쪽이 주는 추가 class입니다.
cn으로 맨 뒤에 합칩니다.hrefstring
Unit이 연결할 주소입니다. 없으면
Layout.Unit과 Link는 평범한 div를 그립니다.onClick(model: L) => unknown
클라이언트 부모가 넘길 수 있는 클릭 콜백입니다.
sliceSliceMeta
목록이 속한 slice입니다.
Data.ListContainer가 넘깁니다.actionsDataAction[]
행 동작(
edit, view, remove 또는 요소)입니다. Data.ListContainer가 넘깁니다.columnsDataColumn<L>[]
보여 줄 필드 목록입니다.
Data.ListContainer가 넘깁니다.Unit 변형
Unit 파일 하나가 같은 모델의 여러 모양을 export하고, 각각 용도에 맞는 이름을 붙입니다. 모델 이름은 namespace가 붙여 주므로
ArticleCard가 아니라 <Article.Unit.Card />입니다.export설명
Card
목록과 그리드에 쓰는 기본 카드입니다.
MiniRow
빽빽한 목록에 쓰는 한 줄짜리 행입니다.
Admin.Unit.Row는 동작 버튼도 함께 둡니다.Abstract
피드나 목록 미리보기에 쓰는 짧은 요약입니다.
Gallery
이미지 그리드에 쓰는, 이미지가 중심인 타일입니다.
Avatar
User.Unit.Avatar처럼 레코드를 작은 그림으로 보여 줍니다.같은 파일에 있는 한 줄짜리 행과 이미지 타일입니다:
apps/koyo/lib/article/Article.Unit.tsx
- flag보다 변형이 낫습니다.
Card에isCompactflag를 다는 것보다Mini를 하나 더 만드는 편이 간단합니다. - 동작은 Util이 맡습니다.
Mini는Article.Util.Remove를 렌더링하면서 id만 넘깁니다. 이유는 다음 섹션에서 설명합니다. Image에는 파일을 넘깁니다.file={article.cover}로 넘기면File관계에서 URL, 크기, 흐린 미리보기를 읽습니다.
Unit 안의 동작
Unit에는 삭제, 복사, 상세 보기 같은 작은 동작 버튼을 둘 수 있습니다. Unit은 작은 Util 컴포넌트를 배치만 하고, 브라우저 동작은 Util이 맡습니다.
Unit은 버튼을 모서리에 두되, 링크 안이 아니라 링크 옆에 둡니다:
apps/koyo/lib/article/Article.Unit.tsx
Util은 클라이언트 컴포넌트입니다. 모델이 아니라 id를 받습니다:
apps/koyo/lib/article/Article.Util.tsx
- JavaScript로 가는 것은 버튼뿐입니다. page가 카드를 렌더링하면 나머지는 서버에서 그린 HTML로 남습니다.
- Util은 모델이 아니라 id를 받습니다. 모델 prop은 클래스 인스턴스째로 서버와 클라이언트의 경계를 넘게 되므로,
RemoveProps는articleId: string을 받습니다. - 버튼은 링크 밖에 둡니다.
<a>안을 누르면 링크로도 이동하고, 그 안의 버튼은 올바르지 않은 HTML이라 예제에서는 둘을 형제로 둡니다. - 폼과 비동기 흐름은 Unit 밖에 둡니다. 폼은 Template에, 여러 단계짜리 흐름은 store 액션에 둡니다.



Unit 파일에서는 클라이언트 전용 기능을 쓰지 않습니다. Unit 안의
"use client", useState 같은 React hook, st import는 lint가 막습니다. onClick도 page가 서버에서 그 Unit을 렌더링하는 순간 깨집니다.Load.Units와 직접 렌더링
Unit 목록이 화면에 나오는 길은 세 가지입니다. page가 무엇을 가지고 있는지에 따라 고릅니다:
| page가 가진 것 | 렌더링 방법 |
|---|---|
| ↳ 결과 | |
Zone에 넘긴 init | Load.Units |
| 로딩, 페이지 이동, 새로고침, 빈 화면을 처리하고 store도 hydrate합니다. | |
| await한 목록 | list.map(…) |
| 첫 응답에 들어가는 서버 HTML입니다. 서버에서 렌더링하는 page에서 흔히 씁니다. | |
await하지 않은 <model>List<Suffix> | Load.Stream |
| 목록이 자기 boundary 뒤에서 렌더링되므로 route 전체가 기다리지 않습니다. | |
Zone 안의 Load.Units
slice가 로딩, 페이지 이동, 새로고침, 빈 화면을 관리해야 하면
Load.Units를 씁니다. page가 넘긴 init을 받는 Zone 안에 둡니다:apps/koyo/lib/article/Article.Zone.tsx
renderItem은 Unit으로 행 하나를 그립니다.href는 여기서 넘겨야 Unit 자체를 여러 곳에서 다시 쓸 수 있습니다.renderEmpty는 빈 화면입니다.Model.NewWrapper는 감싼 버튼이 새 폼을 열게 하고, 그 폼은Model.EditModal이 그립니다.
Load.Units가 store에 채우는 값
Load.Units는 slice를 클라이언트 store에 hydrate합니다. 덕분에 첫 렌더링 뒤에도 자동으로 생긴 페이지 이동, query, 정렬, 새로고침, insight helper가 계속 동작합니다. 각 값은 st.use.<key>()로 읽습니다:store 키설명
<model>List<Suffix>
Load.Units가 그리는 목록입니다. 지금 화면에 보이는 그대로입니다.<model>InitList<Suffix>
서버가 처음 보낸 목록입니다. 초기화하거나 비교할 때 씁니다.
<model>InitAt<Suffix>
서버가 그 첫 목록을 만든 시각입니다.
<model>ListLoading<Suffix>
목록이 hydrate되면
false가 되고, 다시 불러오는 동안에는 true가 됩니다.<model>Insight<Suffix>
slice와 함께 온 insight입니다.
count나 요약 값이 들어 있습니다.pageOf<Model><Suffix>lastPageOf<Model><Suffix>limitOf<Model><Suffix>
init 객체에서 가져온 페이지 상태입니다.
hasMoreOf<Model><Suffix>isCumulativeOf<Model><Suffix>
뒤에 행이 더 있는지, 목록이
loadMoreOf<Model><Suffix>()로 행을 이어 붙인 상태인지를 나타냅니다.queryArgsOf<Model><Suffix>
slice를 불러올 때 쓴 필터 인자입니다.
sortOf<Model><Suffix>
slice를 불러올 때 쓴 정렬 키입니다.
서버에서 직접 렌더링하기
page가 이미 목록을 가지고 있다면 map으로 바로 Unit을 그립니다. hydrate할 것이 없고, 행은 첫 응답에 들어갑니다:
apps/koyo/page/project/[projectId]/_index.tsx
배열 대신 await하지 않은
<model>List<Suffix> promise를 가지고 있다면, map을 Load.Stream으로 감쌉니다. 목록이 route를 붙잡지 않고 자기 boundary 뒤에서 렌더링됩니다:apps/koyo/page/project/[projectId]/_index.tsx
<model>List<Suffix>에는 모델 인스턴스가 들어 있습니다. 서버 컴포넌트에만 넘기고, Zone에는<model>Init<Suffix>를 넘깁니다.- 스트리밍은 작고 변하지 않는 목록에만 씁니다. 같은 slice를 Zone에도 넘기면 서버의
Load.Stream과 hydrate 뒤의Load.Units가 행을 두 번 만듭니다. 큰 목록은init으로 Zone에만 넘깁니다.
실전 규칙
Unit을 여러 곳에서 다시 쓸 수 있게 하는 여섯 가지 규칙입니다:
- 목록에는 Light 모델을 씁니다. 행마다 반복해서 그리는 것은 전체 모델이 아니라 Light 모델을 받습니다.
className과href를 받습니다. 그래야 다른 레이아웃과 다른 링크에서도 같은 Unit을 쓸 수 있습니다.cn으로 합칩니다. 호출하는 쪽의 class를 맨 뒤에 둡니다:cn("rounded-lg border", className).- 클릭할 수 있는 카드와 행은
Layout.Unit이나Link로 만듭니다. - 폼은 Template에, 복잡한 비동기 동작은 Util이나 Store에 둡니다. Unit에는 둘 다 두지 않습니다.
- flag 대신 변형을 export합니다.
Card하나에 flag를 쌓지 말고, 보여 주는 목적마다 변형을 하나씩 만듭니다.
자주 하는 실수
| 실수와 고치는 법 |
|---|
| ↳ 이렇게 합니다 |
| Util.Remove article={article} |
Util은 id를 받으므로 articleId={article.id}를 넘깁니다. |
| <button onClick={…}> |
| 핸들러를 Util로 옮기고, Unit에서는 그 Util을 렌더링합니다. |
| export const ArticleCard |
Card로 export합니다. 모델 이름은 namespace가 붙여 줍니다: <Article.Unit.Card />. |
| article.content |
| Light 모델에는 constant가 고른 필드만 있습니다. 거기에 필드를 추가하거나 View에서 그립니다. |
| await fetch.viewArticle(id) |
| Unit은 데이터를 불러오지 않습니다. page에서 불러와 레코드를 prop으로 넘깁니다. |