service.store.ts

<service>.store.ts는 service module의 클라이언트 상태와 액션을 담습니다. 대부분의 service module은 이 파일을 비워 두므로, 폴더에서 가장 손댈 일이 적은 파일입니다.
여러 컴포넌트가 같은 값을 함께 쓰거나, 화면에 module의 endpoint를 부르는 액션이 필요할 때만 채웁니다.
이 페이지에서 쓰는 말
state
store가 들고 있는 값입니다. key를 읽는 컴포넌트는 그 key가 바뀌면 다시 렌더링됩니다.
action
store 클래스의 method입니다. 컴포넌트는 st.do.<action>()으로 호출합니다.
st.usest.do
클라이언트 컴포넌트가 key를 읽고(st.use.<key>()) 액션을 실행하는(st.do.<action>()) 통로입니다.
model store
model의 signal에 묶인 store, store(sig.<model>, …)입니다. 목록, 폼, CRUD가 자동으로 생깁니다.
service store
이름 하나에 묶인 store, store("<name>" as const, …)입니다. model에서 생성되는 것이 없습니다.
store를 가진 service module은 몇 개인가
이 워크스페이스의 service module 여덟 개 중 store가 있는 것은 넷이고, 그중 둘은 아직 빈 스캐폴드입니다:
libs/util/lib/_util
지도 viewport와 알림 권한, 그리고 지도 액션 두 개입니다.
libs/shared/lib/_shared
상태는 없고, login과 logout 액션만 있습니다.
apps/akan/lib/_akan
빈 스캐폴드 그대로입니다.
apps/minimal/lib/_minimal
빈 스캐폴드 그대로입니다.
나머지 넷인 _doc, _localFile, _security, _oauth에는 store 파일이 아예 없습니다.
service store에 없는 것
model store는 signal의 slice로 만들어지므로 목록, 폼, CRUD 상태가 코드 없이 생깁니다. service store는 model이 아니라 이름에 묶이므로, 직접 선언한 것과 key마다 따라오는 읽기 함수와 setter만 있습니다.
생기는 것
model store
service store
model의 slice에서 생성됨
<model>List · <model>Insight
✓
slice마다 목록과 insight가 하나씩 생깁니다.
pageOf<Model> · limitOf<Model>
✓
slice마다 페이지네이션 상태가 생깁니다.
<model>Form · set<Field>On<Model>
✓
편집 폼과, 필드마다 setter 하나가 생깁니다.
create<Model> · remove<Model>
✓
자동 생성된 endpoint를 부르는 CRUD 액션입니다.
선언한 key마다 따라옴
st.use.<key>()
✓
✓
컴포넌트를 그 key 하나에 구독시킵니다.
st.do.set<Key>(value)
✓
✓
그 key의 setter입니다. search·computed key이거나 같은 이름의 액션이 있으면 생기지 않습니다.
직접 작성
<action>()
✓
✓
class 본문의 method입니다. service store에는 key setter 말고는 이것이 액션의 전부입니다.
✓있음없음
기본 뼈대
akan create-service <name>은 store를 이 파일과 똑같이 빈 채로 만듭니다:
apps/minimal/lib/_minimal/minimal.store.ts
  • 주석 두 줄은 남깁니다. 상태는 factory 안에, 액션은 class 본문에 둡니다. 빈 파일이 다음 사람에게 주는 단서는 이 두 줄뿐이라, 지우면 아무것도 남지 않습니다.
  • 이름에 묶입니다. 첫 인자는 module 이름 "minimal" as const입니다. model store라면 이 자리에 sig.<model>이 옵니다.

화면이 함께 쓰는 상태

여기에 둘 만한 상태는 컴포넌트 둘 이상이 읽는 값입니다. 지도의 viewport가 가장 좋은 예입니다. 지도는 그 값을 그리고, 컨트롤 패널은 바꾸고, 목록은 그 값으로 거르지만, 어느 컴포넌트도 주인이 아닙니다.
util 라이브러리는 지도 viewport를 service store에 둡니다:
libs/util/lib/_util/util.store.ts
  • key마다 따로 구독합니다. st.use.mapZoom()을 부른 클라이언트 컴포넌트는 mapZoom이 바뀔 때만 다시 렌더링됩니다.
  • key마다 setter도 생깁니다. 지도는 st.do.setMapZoom(zoom)으로 값을 되돌려 씁니다. 값을 그대로 쓰는 데는 액션이 필요 없습니다.
  • 파생 계산은 scalar에 둡니다. Coordinate.getBounds는 constant의 static이라 서버에서도 부를 수 있습니다. store는 언제 실행할지만 정합니다.
  • model store의 상태 빌더도 그대로 씁니다. persist, session, search, computed는 model store에서와 똑같이 동작합니다. 자세한 내용은 페이지 맨 아래에 링크한 model.store.ts에 있습니다.

액션은 값을 돌려주지 않습니다

store 클래스의 method는 모두 st.do.<action>()으로 호출되고, 타입은 void나 Promise<void>입니다. return한 값은 호출한 쪽에 닿지 않으므로, 대신 this.set()으로 상태에 씁니다.
아래 첫 번째 액션은 흔한 실수와 그 수정이고, 두 번째는 같은 store의 실제 액션입니다:
libs/util/lib/_util/util.store.ts
  • 돌려주지 말고 상태에 씁니다. notiPermission이 필요한 컴포넌트는 st.use.notiPermission()으로 읽습니다.
  • 값 없는 조건 탈출은 괜찮습니다. if (!result) return;은 값 없이 액션을 일찍 끝내므로 린트 규칙이 허용합니다.
액션 안에서 읽고 쓰기
this.set(state)
값이 액션 밖으로 나가는 유일한 길입니다. 객체를 넘기면 얕게 병합하고, 함수를 넘기면 immer draft를 고칩니다.
this.get()
현재 상태를 돌려줍니다. 값이 없을 수도 있을 때 씁니다.
this.pick(...keys)
반드시 있어야 하는 key를 돌려줍니다. null, undefined, ""이면 에러를 던집니다.
허용되는 return
return 형태
허용
린트 에러
store 클래스 안에서
return value;
✓
액션이 돌려주는 값입니다. 어떤 호출자도 읽을 수 없습니다.
return;
✓
액션을 일찍 끝내는 값 없는 조건 탈출입니다.
(x) => { return … }
✓
안쪽 callback에 속한 return입니다.
get total() { … }
✓
getter는 액션이 아닙니다.
static helper() { … }
✓
static method도 액션이 아닙니다.
✓해당해당 없음
에러는 프레임워크에 맡깁니다
액션은 본문을 try/catch로 감싸지 않습니다. 실패는 두 종류이고, 각각 대응이 정해져 있습니다:
서버가 거절했을 때
서버가 던진 Err는 fetch를 거쳐 이미 dictionary 문구의 toast로 뜹니다. 그것을 삼키는 catch는 번역된 메시지를 지워 버립니다.
클라이언트 검증이 실패했을 때
msg.error("<key>"); return;
msg.error로 dictionary key의 문구를 띄우고 바로 return합니다. throw는 하지 않습니다.

endpoint 호출하기

store는 fetch.*를 부르는 유일한 클라이언트 파일입니다. 컴포넌트는 st.use.*로 읽고 st.do.*로 쓰므로, 같은 액션을 부르는 버튼 두 개가 서로 다르게 동작할 수 없습니다.
shared 라이브러리의 logout 액션 하나에 그 모양이 다 들어 있습니다:
libs/shared/lib/_shared/shared.store.ts
store 액션은 거의 모두 같은 세 단계를 따릅니다:
  1. await fetch.<endpoint>(…)로 endpoint를 부릅니다.
  2. this.set({ … })으로 결과를 저장합니다. logout은 토큰을 저장하므로 대신 setAuth를 부릅니다.
  3. msg.success("<key>")나 router.refresh()로 사용자나 router에 알립니다.
이보다 훨씬 긴 본문은 대개 service가 내렸어야 할 결정입니다.
store가 닿을 수 있는 것
store는 클라이언트 파일이라 브라우저 번들에 들어갑니다. 서버에는 생성된 fetch로만 닿습니다:
import·호출
허용
린트 에러
클라이언트에서 안전한 것
fetch.<endpoint>()
✓
"../useClient"의 생성된 client입니다. store가 서버에 닿는 유일한 길입니다.
../cnst · akanjs/client
✓
model 클래스와 router, setAuth 같은 브라우저 쪽 도구입니다.
import type { … }
✓
번들링 전에 지워지므로, 서버 파일의 타입이라도 괜찮습니다.
서버 전용이거나 route 전용인 것
*.service · *.signal · *.document · *.dictionary
✓
서버 module입니다. 값 import 하나로 그 의존성 전체가 브라우저 번들에 끌려옵니다.
srvkit/ · ../srv · ../db · ../sig · ../dict
✓
서버 전용 폴더와 barrel, 그리고 option, useServer, 모든 server 진입점입니다.
fetch.init<Model><Suffix>()
✓
route가 첫 바이트 전에 불러오는 초기 데이터입니다. 클라이언트는 st.do.init<Model><Suffix>()로 다시 불러옵니다.
✓해당해당 없음
  • 다른 module의 store에는 RootStore로 닿습니다. import type { RootStore } from "../st"를 적은 뒤, 위의 logout처럼 (this as unknown as RootStore)로 그 액션을 부르거나 .set({ … })로 상태를 씁니다.
  • import type으로만 가져옵니다. 실행 중에는 모든 store가 하나의 root로 합쳐지므로 캐스팅은 this가 이미 무엇인지 타입에 알려 줄 뿐입니다. st.ts를 값으로 import하면 순환이 생깁니다.
함께 볼 페이지

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

내 AI에 이 문서 연결하기

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