model.Zone.tsx

Zone은 page 섹션에서 클라이언트 쪽을 맡는 조각입니다. page가 데이터를 가져오면, Zone은 그 데이터를 store에 넣고 레코드마다 Unit이나 View에 넘겨 그리게 합니다.
page에 목록이나 상세 섹션을 새로 붙일 때, 또는 섹션에 모달이나 실시간 갱신이 필요할 때 이 파일을 엽니다. 섹션 하나는 네 단계로 완성됩니다:
  1. page가 fetch를 시작합니다. 목록은 fetch.init<Model><Suffix>(), 레코드 하나는 fetch.view<Model>(id)로 가져와 init이나 view로 넘깁니다.
  2. Zone이 그것을 Load.Units나 Load.View에 넘깁니다. 이 둘이 store를 채우고 로딩 화면과 빈 화면을 그립니다.
  3. 레코드는 서버 컴포넌트가 그립니다. 행 하나는 Unit이, 상세 화면은 View가 그립니다.
  4. 동작과 폼은 각자의 파일에 둡니다. 버튼은 Util, 폼은 Template에 두고, 상태와 액션은 store가 맡습니다.
이 페이지에서 쓰는 말
init
fetch.init<Model><Suffix>()가 주는 <model>Init<Suffix> 필드이며, promise 그대로든 await한 값이든 ClientInit입니다.
view
fetch.view<Model>(id)가 주는 <model>View 필드이며, promise 그대로든 await한 값이든 레코드 하나를 담은 ClientView입니다.
hydrate
서버가 준 payload를 클라이언트 store에 옮겨 담아, 화면과 store가 같은 데이터를 갖게 하는 일입니다.
fetch.slice.<name>
래퍼나 컨트롤에게 어떤 모델의 어떤 목록을 다루는지 알려 줍니다.
Suspense boundary
promise의 결과가 올 때까지 그 자리에만 대체 화면을 보여 주고, 나머지 page는 기다리지 않게 하는 경계입니다.

파일 규칙과 props

Zone 파일은 언제나 "use client"로 시작합니다. props는 서버에서 클라이언트로 건너갈 수 있는 값이어야 합니다. init이나 view payload, id, className이 그렇습니다.
경로
apps/<app>/lib/<model>/<Model>.Zone.tsx
데이터베이스 모듈과 서비스 모듈에 둘 수 있고, 스칼라 모듈에는 두지 않습니다.
첫 줄
"use client";
언제나 import보다 먼저, 파일 첫 줄에 씁니다.
목록 Zone의 props
className · init · slice · <parent>Id
init은 ClientInit입니다. slice는 안쪽의 래퍼와 컨트롤에 넘깁니다.
상세 Zone의 props
className · view · <parent>Id
view는 ClientView입니다. 로그인한 사용자는 prop이 아니라 st.use.self()로 읽습니다.
새 모듈은 이 Zone으로 시작합니다. 목록용 Card와 상세용 View를 하나씩 export합니다:
apps/koyo/lib/icecreamOrder/IcecreamOrder.Zone.tsx
  • export 이름은 역할 이름입니다. 모델 이름은 네임스페이스가 붙여 주므로 page에서는 <IcecreamOrder.Zone.Card />로 쓰고, IcecreamOrderCard라고 짓지 않습니다.
  • ClientInit, ClientView, ClientEdit는 두 모양을 다 받습니다. page가 await로 받아 둔 payload를 넘기든 fetch가 준 promise를 넘기든, Zone 코드는 그대로입니다.
  • interface <Name>Props는 컴포넌트 바로 위에 선언합니다. 첫 필드는 className?입니다.

목록 Zone과 Load.Units

목록 섹션은 받은 init을 Load.Units에 넘깁니다. Load.Units는 행을 store에 채우고, 로딩 화면과 빈 화면을 그리며, 행마다 render 함수를 부릅니다.
page는 query를 시작하고, await하지 않은 promise를 그대로 넘깁니다:
apps/koyo/page/devApp/[devAppId]/dbBackup.tsx
Zone은 Load.Units에 행을 그리는 함수와 빈 화면을 넘깁니다:
apps/koyo/lib/dbBackup/DbBackup.Zone.tsx
  • renderItem은 행 하나를 그립니다. 보통 Unit.Card나 Unit.Abstract에 넘깁니다.
  • renderEmpty는 빈 화면입니다. Model.NewWrapper나 링크 모양의 안내 버튼을 자주 둡니다. Model.NewWrapper는 트리거만 그리므로, 폼은 옆에 둔 Model.EditModal이 그립니다.
  • await하지 않은 promise는 스트리밍됩니다. Load.Units가 자기만의 Suspense boundary 뒤에서 loading을 보여 주고, page의 나머지는 기다리지 않고 먼저 전송됩니다.
Load.Units의 props
initClientInit<"model", LightModel>필수
page가 넘겨준 목록 payload나 그 promise입니다.
renderItem(item, idx) => ReactNode
행 하나를 그리며, renderList를 넘기지 않으면 필수입니다.
renderList(list: DataList) => ReactNode
그룹, 탭, 보드, 직접 정한 순서가 필요할 때 목록 전체를 그립니다.
renderEmpty(() => ReactNode) | false기본값 <Empty />
행이 없을 때의 화면을 그리며, renderList와 함께 false를 주면 빈 목록을 그대로 그립니다.
emptyReactNode
renderEmpty보다 우선하는, 미리 만든 빈 상태 요소입니다.
loadingReactNode기본값 Loading.Skeleton
promise로 받은 init을 기다리는 동안과 목록을 다시 불러오는 동안 보입니다.
paginationboolean기본값 true
데스크톱에서는 페이지 번호를, 모바일에서는 무한 스크롤을 붙입니다.
classNamestring
행을 감싸는 div의 클래스로, 그리드 레이아웃 등을 여기에 줍니다.

상세 Zone과 Load.View

상세 섹션은 받은 view를 Load.View에 넘깁니다. Load.View는 레코드를 store에 넣은 뒤 full 모델을 renderView에 건넵니다.
아직 도착하지 않은 view promise는 자기만의 boundary를 가지므로, 느린 상세 화면이 주변 레이아웃을 붙잡지 않습니다:
apps/koyo/lib/ticket/Ticket.Zone.tsx
  • page는 ticketView를 넘깁니다. fetch.viewTicket(ticketId)는 ticketView와 ticket을 줍니다. 모델 인스턴스인 ticket은 서버에서만 씁니다.
  • 로그인한 사용자는 store에서 읽습니다. self prop은 경계를 넘는 cnst 모델이 되므로, 대신 st.use.self()를 씁니다.
Load.View의 props
viewClientView<"model", Model>필수
page가 넘겨준 상세 payload나 그 promise입니다.
renderView(model) => ReactNode필수
full 모델을 그리며, 보통 <Model>.View.General을 씁니다.
loadingReactNode기본값 Loading.Skeleton
promise로 받은 view를 기다리는 동안 보입니다.
emptyReactNode기본값 <Empty />
레코드가 비어서 돌아왔을 때 보입니다.
classNamestring
감싸는 div의 클래스입니다.
noDivboolean
감싸는 div 없이 renderView만 그립니다.

여러 조각을 엮는 Zone

어떤 Zone은 섹션 전체를 조립합니다. 필터, 목록, 생성 버튼, 모달이 한자리에 모입니다. Zone은 이들을 엮기만 하고, 각 조각은 여전히 자기 파일에 있습니다.
renderList로 만드는 보드
renderList는 목록 전체를 받으므로, Zone이 행을 열별로 묶고 그 둘레에 컨트롤을 둘 수 있습니다:
apps/koyo/lib/ticket/Ticket.Zone.tsx
  • 필터는 Util입니다. 컨트롤은 Ticket.Util.QueryMakerInSelf가 맡고, Zone은 자리만 잡아 줍니다.
  • Model.New는 생성 버튼과 폼을 한 번에 그립니다. partial로 새 티켓에 현재 프로젝트를 미리 채웁니다.
  • renderEmpty={false}로 보드를 유지합니다. 티켓이 하나도 없어도 빈 열과 생성 버튼이 그대로 보입니다.
모달을 여는 카드
Model.ViewWrapper는 카드를 누르면 그 레코드를 열고, Model.ViewEditModal 하나가 편집 버튼과 함께 보여 줍니다:
apps/koyo/lib/dessert/Dessert.Zone.tsx
  • 모달 하나가 모든 카드를 맡습니다. Model.ViewWrapper는 id로 레코드를 열기만 하고, 그 slice의 Model.ViewEditModal 하나가 화면을 그립니다.
  • renderTemplate은 필수입니다. 모달의 편집 버튼을 누르면 View가 이 폼으로 바뀝니다.
  • 로컬 UI 상태는 최소로 둡니다. useState는 모달 열림, 입력 중인 초안, 드래그 상태에만 쓰고 서버 데이터에는 쓰지 않습니다.
  • 모드 전환은 akanjs/ui의 Tab으로 합니다. Tab은 page나 View에 둡니다. Tab.Panel은 children을 그대로 그리므로, 안에 넣은 서버 View는 서버에서 그려집니다.

실시간 Zone과 대시보드 Zone

섹션 전체가 store 상태, 구독, 클라이언트 전용 레이아웃을 따라 움직인다면 Zone은 대시보드나 실시간 섹션이 될 수도 있습니다.
대시보드는 요약 모델 위의 Load.View입니다:
apps/koyo/lib/summary/Summary.Zone.tsx
실시간 섹션은 effect 안에서 구독하고, effect의 cleanup에서 구독을 끊습니다:
apps/koyo/lib/chatRoom/ChatRoom.Zone.tsx
  • 실시간 목록에는 effect가 필요 없습니다. slice에 .live()를 선언하면 Load.Units가 알아서 room을 열고 변경분을 반영합니다.
  • useEffect는 구독과 cleanup에만 씁니다. mount 시점에 데이터를 불러오는 effect는 서버가 이미 한 왕복을 되풀이합니다. akan quality ssr은 이것을 client-mount-load로 알려 줍니다.
  • 로딩 분기를 직접 만들지 마세요. 대기 화면과 빈 화면은 Load.View와 Load.Units가 이미 그리고, 데이터는 route가 첫 바이트 전에 가져왔습니다.

Zone을 쓰는 경우

화면의 조각마다 자리가 정해져 있습니다. 섹션이 store를 써야 할 때 Zone을 만들고, 그리기만 하는 것은 서버에 둡니다.
파일
서버
클라이언트
가져오거나 그리는 파일
page/**/*.tsx
✓
param을 읽고 fetch.*를 시작해 결과를 아래로 넘기는 route 셸입니다.
<Model>.Unit.tsx
✓
light 모델로 행이나 카드 하나를 그립니다.
<Model>.View.tsx
✓
레코드 하나의 상세 화면을 그립니다.
상태나 동작을 가진 파일
<Model>.Zone.tsx
✓
Load 래퍼, store 읽기, 모달로 page 섹션을 조립합니다.
<Model>.Template.tsx
✓
store에 묶인 폼 필드와 폼 조각입니다.
<Model>.Util.tsx
✓
필터나 삭제 버튼 같은 작은 동작, 도구 모음, 헬퍼입니다.
<model>.store.ts
✓
클라이언트 번들에만 들어가는 상태와 액션입니다.
✓여기서 실행여기가 아님

실전 규칙

Zone을 작게 유지하는 다섯 가지 규칙입니다:
  • page는 얇게 둡니다. 섹션을 page에서 직접 만들지 말고, 서버의 init이나 view 데이터를 Zone에 넘깁니다.
  • 목록에는 Load.Units, 상세에는 Load.View를 씁니다.
  • 그리는 일은 Unit과 View에 맡깁니다. 행은 Unit, 상세 화면은 View가 그리므로 Zone 자체의 마크업은 거의 없습니다.
  • 동작은 Util에 둡니다. Zone 안의 버튼과 컨트롤은 Util 컴포넌트입니다.
  • 비즈니스 규칙은 render 코드에 두지 않습니다. service, document, store, constant에 둡니다.
자주 하는 실수
실수와 고치는 법
↳ 이렇게 합니다
useEffect(() => { fetch… }, [])
route에서 fetch하고 결과를 init이나 view로 넘깁니다.
fetch.initXInY()
클라이언트 파일에서는 lint가 막으므로, 다시 불러올 때는 st.do.initXInY()를 씁니다.
init={fetch.initXInY(id)}
handle 전체가 아니라 필드를 넘깁니다: init={xInitInY}.
<X.Zone.Card list={xListInY} />
xListInY에는 클라이언트 prop이 거부하는 모델 인스턴스가 들어 있으므로 xInitInY를 넘깁니다.
self: cnst.User
lint가 모델 prop을 막으므로 st.use.self()로 읽거나 id를 받습니다.
useState<Mode>(…)
패널이 서버에서 그려지도록 page나 View에서 Tab으로 전환합니다.
isLoading ? <Spinner /> : …
Load.Units와 Load.View의 loading, empty prop을 씁니다.

MIT 라이선스 하에 배포되었습니다.

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman