Service.Zone.tsx

Zone은 검색 콘솔, 업로드 패널, 장비 대시보드처럼 page가 통째로 끼워 넣는 화면 구획입니다. service module에는 대개 필요 없습니다. 이 워크스페이스의 service module 여덟 개 중 Zone을 가진 것은 하나도 없고, Util도 마찬가지입니다.
나열할 레코드가 없기 때문입니다. service module의 UI는 보통 구획이 아니라 자기만의 화면이거나, 다른 화면 속 버튼 하나입니다.
이 페이지에서 쓰는 말
service module
자기 model 없이 service, signal, dictionary, 그리고 흔히 store로 이루어진 lib/_<name> 폴더입니다.
page가 통째로 끼워 넣는 구획을 맡는 파일 역할입니다. 언제나 클라이언트 컴포넌트입니다.
"use client"가 없는 파일입니다. 서버에서 실행되어 HTML로 도착합니다.
첫 줄이 "use client"인 파일입니다. HTML로 한 번, 브라우저가 다시 실행하는 JS로 한 번 더 도착합니다.
slot
header 같은 ReactNode prop입니다. page가 서버에서 내용을 그려 넘겨줍니다.
UI 종류별 자리
UI의 모습
page/
ui/
.Zone.tsx
Zone이 아닌 경우
자기만의 화면
✓
libs/shared/page/oauth의 OAuth 동의 화면처럼 route 하나입니다.
다른 화면 속 버튼 하나
✓
service store가 움직이는 ui/ 컴포넌트입니다. 드물게 Service.Util.tsx가 됩니다.
route 하나에만 나오는 구획
✓
그 구획이 곧 그 route입니다. page 안에 바로 씁니다.
Zone인 경우
여러 route에서 다시 쓰는 구획
✓
store state를 함께 쓰는 컨트롤 여러 개가 한 덩어리로 배치된 것입니다.
✓여기에 둡니다여기가 아닙니다
구획이 Zone이 되려면 아래 세 가지를 모두 만족해야 합니다:
  • 컨트롤이 여러 개입니다. 모두 같은 store state를 읽습니다.
  • 한 덩어리로 배치됩니다. 화면 여기저기 흩어진 조각이 아니라 한 구획을 이룹니다.
  • route 두 곳 이상에서 다시 씁니다. 정확히 한 route에만 나온다면, 그 구획이 곧 그 route입니다.

마크업은 담지 않는다

model module의 Zone은 store를 읽고, 그리기는 같은 폴더의 서버 컴포넌트인 View에 맡깁니다. service module에는 View가 없으므로, 그리기를 맡길 컴포넌트는 ui/에 둡니다.
아래 Zone은 Zone이 할 수 있는 두 가지를 모두 합니다. store state를 ui/ 컴포넌트에 prop으로 건네고, 서버 콘텐츠는 slot으로 돌려받습니다:
apps/koyo/lib/_receipt/Receipt.Zone.tsx
  • store 읽기 둘, 감싸는 엘리먼트 하나. Zone이 직접 그리는 것은 <section>뿐입니다. 사용자가 보는 것은 ReceiptPreview와 header slot에 있습니다.
  • 번들에서 빠지는 것은 slot뿐입니다. header는 page가 서버에서 그려 완성된 채로 넘깁니다. ReceiptPreview는 "use client"가 없지만, Zone이 import하므로 그 코드는 Zone의 JS 청크에 함께 실립니다.
  • children보다 이름 있는 slot을 씁니다. slot이 있으면 page가 서버 콘텐츠를 익명 자리 하나가 아니라 이름 붙은 자리에 놓을 수 있습니다. Layout.Navbar가 title, back, left, right, children 다섯 개를 받는 이유입니다.
경계를 작게 유지하는 법
  • UI가 아니라 인터랙션을 감쌉니다. 쓸모 있는 가장 작은 클라이언트 컴포넌트는 동작 하나만 더하고 children은 그대로 그립니다. 그러면 안쪽 마크업은 번들에 들어가지 않습니다.
  • 경계를 말단까지 내립니다. key 셋을 읽고 엘리먼트 마흔 개를 그리는 Zone은, key 셋을 읽는 Zone과 마흔 개를 그리는 서버 컴포넌트로 나눕니다.

데이터는 route에서 채운다

마운트될 때 불러오고 싶은 마음을 참으세요. useEffect(…, [])는 빈 껍데기를 그리고 hydrate한 뒤에야, 서버가 첫 바이트 전에 답할 수 있었던 질문을 서버에 던집니다. akan quality ssr은 이것을 akan.ssr.client-mount-load로 알려 줍니다.
대신 page가 fetch하고 await한 뒤, Zone은 그 결과를 prop으로 받습니다:
apps/koyo/page/receipt/_index.tsx
  • 불러오기는 page가 합니다. fetch.listReceiptTemplates()는 첫 바이트 전에 끝나므로, 콘솔은 이미 채워진 채로 도착합니다.
  • 클라이언트 컴포넌트는 fetch.*를 부르지 않습니다. 읽기는 st.use.*, 쓰기는 st.do.*로 합니다.
  • 특히 fetch.init*는 부르지 않습니다. 이것은 Load.*의 init prop만 읽는 hydration 스냅샷입니다. 클라이언트에서 부르면 왕복 두 번을 더 들여 아무도 읽지 않는 값을 받습니다.
데이터가 오는 곳
바로 필요한 서버 데이터
page에서 await하고 prop으로 내려보냅니다.
기다려도 되는 서버 데이터
await하지 않은 promise를 <Load.Stream of={…}>에 넘깁니다. 자기 경계 뒤에서 채워집니다.
클릭이 요청하는 것
st.do.*로 부르는 store action입니다.

아니면 그냥 page를 쓴다

libs/shared의 _oauth가 따라 할 만한 예입니다. 동의 화면, 승인·거부 버튼, 연결된 앱 목록이 필요했지만 Util도 Zone도 없습니다.
동의 화면
route 하나입니다: libs/shared/page/oauth/consent/_index.tsx
승인 · 거부 버튼
쿠키 세션이 인증하는 평범한 <form method="post">입니다.
연결된 앱 목록
앱이 자기 page에서 부르는 endpoint listOAuthConnections입니다.
module이 내보내는 것
endpoint 10개, 컴포넌트 0개입니다.
  • form post에는 스크립트가 필요 없습니다. 동의 화면은 HTML로 전송되고, 어떤 번들이 도착하기 전에도 동작합니다.
  • 이 화면에서는 그것이 핵심입니다. 다른 애플리케이션이 내 이름으로 행동하도록 허락하는 화면이라, 스크립트 없이 동작하는 것은 최적화가 아니라 목표입니다.
  • 인자는 경로에 실립니다. 두 endpoint 모두 .param("requestId", String)을 받습니다. 각 form은 API prefix 아래 approveOAuthConsent/<requestId>나 denyOAuthConsent/<requestId>로 post합니다.
route는 lib에도 둘 수 있다
lib는 module뿐 아니라 route도 가질 수 있습니다. libs/<lib>/page는 앱의 page/와 같은 규칙을 따르고, 앱은 akan.config.ts의 syncPageLibs로 가져옵니다:
apps/koyo/akan.config.ts
true
앱이 의존하는 lib 중 page 폴더가 있는 것 전부입니다.
["shared"]
나열한 lib만 가져옵니다.
false
기본값입니다. lib route를 가져오지 않습니다.
lib route는 경로가 바뀌지 않습니다. libs/shared/page/oauth/consent/_index.tsx는 이 route를 가져온 모든 앱에서 /oauth/consent로 열립니다.

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

내 AI에 이 문서 연결하기

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