사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
앱 & 라이브러리▾
도메인▾
스칼라▾
Model.View.tsx
View 파일은 레코드 하나를 빠짐없이 그립니다. 상세 page나 상세 섹션의 본문이 바로 View입니다. full 모델을 prop으로 받아 그리기만 합니다.
full 모델을 받습니다
ticket: cnst.Ticket목록에서는 빠지는 긴 본문과 중첩 데이터까지 모든 필드가 들어 있습니다.
그리기만 합니다
Ticket.Util.* · Ticket.Unit.*Unit, Util, Zone과 자체 하위 컴포넌트를 조합할 수 있습니다. 저장과 판단은 다른 파일에서 합니다.
General을 export합니다
Ticket.View.General주 export는
General입니다. 화면이 길면 이름 붙인 섹션을 옆에 더 둡니다.Zone을 거쳐 그려집니다
renderView={(ticket) => …}상세 Zone이 서버에서 받은 모델을
Load.View로 넘겨 줍니다.이 페이지에서 쓰는 말
용어설명
full model
cnst.Ticket처럼 constant가 선언한 필드를 모두 가진 완전한 모델 클래스입니다.light model
cnst.LightTicket처럼 목록에 필요한 필드만 담은 가벼운 클래스입니다.view payload
fetch.viewTicket(id)가 ticketView로 돌려주는 값으로, 레코드 하나를 순수 데이터로 담습니다.hydrate
서버가 이미 가져온 데이터로 클라이언트 store를 채우는 일입니다. 같은 요청을 다시 보내지 않습니다.
View와 Unit의 차이
두 파일 모두 모델을 그리기만 합니다. 다른 점은 모델을 얼마나 받는지, 그리고 page에서 어떤 역할을 하는지입니다.
View · 레코드 하나를 자세히상세 page나 상세 섹션 하나에 씁니다.
- 받는 모델
- cnst.Ticket
- 대표 export
- Ticket.View.General
- props 타입
- GeneralProps
- 그리는 쪽
- Load.View → renderView
Unit · 여럿 중 하나목록의 행, 카드, 짧은 요약에 씁니다.
- 받는 모델
- cnst.LightTicket
- 대표 export
- Ticket.Unit.Card
- props 타입
- ModelProps<"ticket", cnst.LightTicket>
- 그리는 쪽
- Load.Units → renderItem
- 레코드 하나를 자세히 보여 주면 View입니다. 긴 본문 같은 필드가 필요하므로 full 모델을 받습니다.
- 같은 모양이 반복되면 Unit입니다. 목록은 레코드를 한꺼번에 많이 보내므로 행마다 light 모델을 받습니다.
View 파일의 기본 형태
View 파일은 모두 같은 뼈대에서 시작합니다. 티켓의 View 파일 전체입니다:
apps/koyo/lib/ticket/Ticket.View.tsx
- 주 export는
General입니다. page와 Zone은Ticket.View.General로 씁니다. - props는 full 모델과 className입니다.
GeneralProps는 컴포넌트 바로 위에 두고,className을 먼저, 이어서ticket: cnst.Ticket을 씁니다. - 호출한 쪽의 클래스는 마지막에 합칩니다.
cn("…", className)으로 page나 Zone이 너비와 간격을 조정할 수 있습니다. - 라벨은 모두 dictionary를 거칩니다. 필드 이름은
l("ticket.status")로 가져옵니다. enum 값의 키는 enum 이름으로 시작하므로,"active"는l("ticketStatus.active")로 읽습니다.
full 모델을 그리는 패턴
View는 light 요약이 아니라 full 모델을 받으므로, constant가 full 모델에 선언한 필드는 무엇이든 그릴 수 있습니다. 평범한 텍스트 필드는 마크업에 그대로 넣습니다:
apps/blog/lib/article/Article.View.tsx
enum은 dictionary 라벨을 거치고, 숫자는 그리는 자리에서 형식을 맞춥니다:
apps/koyo/lib/order/Order.View.tsx
- 함께 쓰는 표시 로직은 Light 모델에 둡니다. 한 번만 쓰는
toLocaleString()은 View에 둡니다. Unit에서도 필요한 형식이라면 full 모델이 물려받는LightOrder의 메서드로 만듭니다. - 긴 화면은 이름 붙인 섹션으로 나눕니다.
libs/shared의User.View는 거대한 컴포넌트 하나 대신General과Discord를 export합니다. - 안에 든 버튼은 Util입니다.
User.View.General은User.Util.ChangePassword를 그립니다. 자리는 View가 잡고, 클릭은 Util이 처리합니다.
page에서 View 쓰기
상세 page는
fetch.view<Model>(id)로 요청을 시작하고 view payload를 Zone에 넘깁니다. 호출을 await하느냐에 따라 섹션이 도착하는 시점이 달라집니다:구조 분해 — 스트리밍
const { ticketView } = fetch.viewTicket(id)query가 실행되는 동안 page 마크업이 먼저 나갑니다. 섹션은 자기만의 boundary 뒤에서 채워집니다.
await — 셸에 넣기
await Promise.all([fetch.viewTicket(id)])page가 모델을 직접 읽을 때 씁니다. 제목, 링크를 만들 id, redirect 판단이 그렇습니다.
스트리밍
보통의 상세 page는 await하지 않고 promise를 그대로 넘깁니다:
apps/koyo/page/ticket/[ticketId]/_index.tsx
async도await도 없습니다. render callback은 본문이 await할 때만async를 붙입니다.- Zone은 promise를 그대로 받습니다.
ClientView는 payload와 그 promise를 모두 받고,Load.View는 도착할 때까지 스켈레톤을 보여 줍니다.
await하기
page가 레코드를 직접 써야 하면 호출을 await합니다. 결과는
ticket과 ticketView를 담은 객체입니다:apps/koyo/page/ticket/[ticketId]/_index.tsx
ticketView는 여전히 Zone에 넘깁니다. 이미 도착한 값이므로 로딩 상태 없이 첫 HTML에 그려집니다.ticket은 page 안에서만 씁니다. hydrate된 모델 인스턴스로, 링크나 제목, redirect 판단에 씁니다.- page가
Load.View를 직접 부르지는 않습니다.renderView는 함수인데, 서버 page는 클라이언트 컴포넌트에 함수를 넘길 수 없습니다. 그래서 둘 사이에 Zone을 둡니다.



Zone에는
ticket이 아니라 ticketView를 넘깁니다. ticket은 클래스 인스턴스이고, React Flight는 클래스 인스턴스를 클라이언트 prop으로 받지 않습니다.Load.View로 store hydrate하기
Load.View는 view payload의 레코드를 클라이언트 store에 넣은 뒤, full 모델로 renderView를 호출합니다. 상세 Zone은 사실상 이 호출 하나입니다:apps/koyo/lib/ticket/Ticket.Zone.tsx
- 서버에서 가져온 view 데이터를 store에 넣어야 하는 곳이면 씁니다. 상세 Zone, 탭 레이아웃, 재사용 섹션 모두 이렇게 View를 감쌉니다.
- 대기와 빈 상태는 이미 들어 있습니다. 기다리는 동안은
loading(기본은 스켈레톤)을, payload가 비어 있으면empty(기본은<Empty />)를 보여 줍니다.
store에 쓰는 값
View를 그리기 전에
Load.View는 모델에 대해 키 네 개를 설정합니다:store 키설명
<model>
payload의
<model>Obj로 만든 full 모델 인스턴스입니다.<model>Loading
false로 두어 View가 로딩 상태 없이 바로 그려지게 합니다.<model>Modal
"view"로 두어 모달 래퍼가 편집 폼이 아닌 보기 화면을 열게 합니다.<model>ViewAt
서버가 payload에 찍은
Date로, store에 든 값과 어느 쪽이 최신인지 비교할 때 씁니다.- store 쪽이 더 최신이면 store 값을 씁니다. store가 이 레코드를 더 나중의
<model>ViewAt으로 이미 들고 있으면,Load.View는 오래된 payload로 덮어쓰지 않습니다. - 저장한 뒤 뒤로 가면 레코드를 다시 불러옵니다. 탐색 캐시가 저장 전 payload를 다시 꺼내면,
Load.View가st.do.view<Model>(id)로 레코드를 새로 가져옵니다.
실전 규칙
View에 두는 것과, 나머지를 맡는 파일을 한눈에 정리했습니다:
할 일
View
*.View.tsx
Util
*.Util.tsx
Zone
*.Zone.tsx
page
page/**
그리기 — View의 일
필드와 마크업
✓
full 모델의 제목, 본문, 중첩 데이터, 형식을 맞춘 숫자를 그립니다.
l() · l.trans()
✓
필드 이름, enum 값, 제목은 dictionary에서 가져옵니다.
General · Discord
✓
큰 View는 거대한
General 하나 대신 User.View처럼 이름 붙인 섹션으로 나눕니다.<Model>.Unit · <Model>.Util
✓
View 안에서 Unit, Util, Zone을 그려도 됩니다. 각자 자기 일을 그대로 합니다.
동작 — 다른 파일의 일
onClick · submit
✓
버튼과 동작은 View가 그려 주는 Util입니다.
User.Util.ChangePassword가 그 예입니다.useState · useEffect
✓
✓
hook은 브라우저가 필요하므로 Util이나 Zone에 둡니다.
st.use · st.do
✓
✓
store를 읽고 씁니다. 실제 변경은 store, signal, service가 맡습니다.
Load.View
✓
view payload로 store를 hydrate하고 모델을 View에 넘깁니다.
fetch.view<Model>
✓
route에서 부르므로 첫 바이트가 나가기 전에 query가 시작됩니다.
✓여기에 둡니다여기에 두지 않습니다


View는 서버 파일이고, lint가 이를 검사합니다.
*.View.tsx에 "use client" 줄, useState 같은 React hook import, st import 중 하나라도 있으면 akan lint가 실패합니다.함께 볼 페이지