model.dictionary.ts

<model>.dictionary.ts는 모듈의 언어 레이어입니다. 필드, insight 값, filter, 정렬 순서, enum 값, slice, endpoint, 에러, 모듈 전용 UI 문구까지 사용자가 읽는 모든 이름에 언어별 레이블을 붙입니다.
코드는 문구를 직접 쓰지 않고 l("ticket.title")처럼 key로 꺼내 씁니다. key는 constant, filter, slice, endpoint의 타입을 따르므로, 레이블 없이 필드를 추가하면 컴파일 에러가 납니다.
이 페이지에서 쓰는 말
label
이름 하나에 대해 사용자가 읽는 문구입니다. 언어마다 한 번씩 씁니다: t(["Title", "제목"]).
desc
.desc([en, ko])로 덧붙이는 긴 설명입니다. 폼에서는 툴팁으로 보입니다.
key
코드가 레이블을 꺼낼 때 쓰는, 점으로 이은 경로입니다. ticket.title 같은 모양입니다.
언어 배열
언어마다 항목 하나씩, builder에 넘긴 언어 순서대로 적은 배열입니다: ["Title", "제목"].
단계
체인의 호출 하나입니다. .model(), .error()처럼 한 종류의 이름을 맡습니다.

modelDictionary 기본 형태

데이터베이스 모듈은 modelDictionary를 씁니다. 단계 순서는 고정이고, 붙일 레이블이 없는 단계도 빈 채로 적어서 모든 dictionary가 같은 모양으로 읽히게 합니다.
constant, document 문서에 나온 ticket 모듈의 dictionary 전체입니다:
apps/koyo/lib/ticket/ticket.dictionary.ts
  • t는 레이블을, fn은 인자가 있는 레이블을 만듭니다. .query(), .slice(), .endpoint()는 fn을 주고, 그 .arg()로 filter나 signal이 선언한 인자마다 레이블을 붙입니다.
  • 빠뜨릴 수 없습니다. 각 단계는 타입이 선언한 필드, query, 값, slice, endpoint를 하나도 빠짐없이 채워야 하고, 커스텀 endpoint가 받는 skip, limit, sort도 포함됩니다. 하나라도 빠지면 타입 에러입니다.
  • 레이블에는 거의 항상 .desc()를 붙입니다. 레이블을 되풀이하는 설명이어도 괜찮습니다. 폼은 툴팁으로 보여주고, API 문서와 AI 에이전트는 endpoint 설명을 읽습니다.
단계별 레이블과 key
단계마다 레이블이 들어가는 key 자리가 정해져 있습니다. 예시 칸은 위 파일에서 생기는 key입니다:
.of()
모듈 자체의 이름과 설명입니다.
.model()
모델의 모든 필드입니다. id, createdAt, updatedAt, removedAt은 이미 레이블이 있습니다.
.insight()
모든 insight 필드입니다. 기본 제공되는 count는 이미 레이블이 있습니다.
.query()
모든 filter query와 그 인자입니다. 기본 제공되는 any는 이미 레이블이 있습니다.
.sort()
모든 정렬 순서입니다. latest, oldest, relevance는 이미 레이블이 있습니다.
.enum()
enumOf 하나의 모든 값입니다. key는 모델 이름이 아니라 enum 이름으로 시작합니다.
.slice()
이름 있는 모든 slice입니다. list key와 insight key가 하나씩 생기고, root slice는 이미 레이블이 있습니다.
.endpoint()
모든 커스텀 endpoint와 그 인자입니다. createTicket 같은 생성 CRUD는 이미 레이블이 있습니다.
.error()
에러 메시지입니다. 언어 배열을 그대로 적습니다.
.translate()
토스트, 버튼 문구처럼 모듈이 보여주는 그 밖의 문구입니다.

dictionary 꺼내 쓰기

코드는 헬퍼 세 개로 dictionary를 읽습니다. 셋 모두 key를 받고, 사용자는 그 문구를 자기 언어로 읽습니다.
l(key)
server와 client component 어디서든 화면에 보일 레이블을 꺼냅니다. l은 usePage()에서 얻습니다.
new Err(key)
document나 service에서 던지는 에러입니다. Err는 ../dict에서 import합니다.
msg.success(key)
store 액션이 띄우는 토스트입니다. msg는 ../useClient에서 import합니다.
화면에서
Template은 필드의 레이블을 key로 읽고, 설명은 그 key에 .desc를 붙여 읽습니다:
apps/koyo/lib/ticket/Ticket.Template.tsx
  • usePage()는 서버에서도 동작합니다. 번역 때문에 "use client"를 달 일은 없습니다. 이 Template에 붙은 것은 store 때문입니다.
  • desc는 도움말 아이콘이 됩니다. 레이블 옆에 붙고, 설명은 툴팁으로 뜹니다.
  • l()은 존재하는 key만 받으므로 오타는 타입 에러가 됩니다. l("key", { name })은 문구의 {name} 자리를 채웁니다.
서버 코드에서
document 메서드는 허용되지 않는 상태 변경을 만나면 key로 에러를 던집니다:
apps/koyo/lib/ticket/ticket.document.ts
  • 문장이 아니라 key가 전달됩니다. 클라이언트는 key와 데이터를 받으므로, 사용자마다 자기 언어로 메시지를 읽습니다.
  • 두 번째 인자는 자리 표시자를 채웁니다. new Err("ticket.error.overdue", { days })는 {days} 자리에 숫자를 넣습니다.
  • 하위 클래스로 HTTP 상태를 고릅니다. Err는 400이고, Err.BadRequest, Err.Unauthorized, Err.Forbidden, Err.NotFound, Err.Conflict는 각각 400, 401, 403, 404, 409로 응답합니다.
store에서
store 액션은 호출 앞뒤로 로딩 토스트와 성공 토스트를 띄웁니다:
apps/koyo/lib/ticket/ticket.store.ts
  • 같은 key를 쓰면 토스트가 바뀝니다. 성공 토스트가 로딩 토스트 아래에 쌓이지 않고 그 자리를 대신합니다.
  • msg.error는 .error() key도 받습니다. 서버를 부르기 전에 막는 검사라면 msg.error("ticket.error.cannotOpen")를 띄우고 바로 return합니다.
  • 옵션이 두 개 더 있습니다. 초 단위 duration(기본값 3)과 자리 표시자를 채우는 data입니다.

라이브러리 모델 확장하기

앱은 libs/shared의 user처럼 라이브러리가 이미 가진 모델을 확장할 수 있습니다. 이때 dictionary는 라이브러리의 것에서 출발하고, 앱은 자기가 더한 것만 적습니다.
라이브러리의 dictionary를 modelDictionary의 언어 목록 바로 뒤에 넘깁니다:
apps/koyo/lib/user/user.dictionary.ts
  • user.dictionaries는 생성 파일에서 옵니다. ../__lib/lib.dictionary는 앱과 라이브러리가 함께 가진 모델마다 항목 하나를 export합니다. import만 하고 수정하지 않습니다.
  • 라이브러리의 레이블은 그대로 남습니다. 필드, query, 에러, translate() 항목이 유지되므로, .model<User>()는 여기 있는 githubInfo처럼 라이브러리가 붙이지 않은 필드만 요구합니다.

scalar와 service dictionary

builder는 모듈의 종류로 고릅니다. builder마다 어울리는 단계만 있어서, scalar에는 query가 없고 service에는 필드가 없습니다.
  • modelDictionary는 lib/<model>/의 데이터베이스 모듈에 씁니다. 모든 단계를 쓸 수 있습니다.
  • scalarDictionary는 lib/__scalar/<name>/의 내장 값에 씁니다. 보통 필드, enum 값, 에러, 약간의 문구면 충분합니다.
  • serviceDictionary는 lib/_<name>/의 service 모듈이나, 어느 모델에도 속하지 않는 앱 수준 문구에 씁니다.
단계
모델
10단계
스칼라
5단계
서비스
3단계
모듈과 값의 이름
.of()
✓
✓
.model()
✓
✓
.enum()
✓
✓
데이터베이스 조회의 이름
.insight()
✓
.query()
✓
.sort()
✓
API의 이름
.slice()
✓
.endpoint()
✓
✓
메시지
.error()
✓
✓
✓
.translate()
✓
✓
✓
✓쓸 수 있음없음
scalar는 자기 필드와 enum 값에 레이블을 붙입니다:
libs/util/lib/__scalar/coordinate/coordinate.dictionary.ts
자기 endpoint가 없는 service 모듈은 공용 UI 문구만 담을 수도 있습니다:
libs/util/lib/_util/util.dictionary.ts
  • service의 endpoint도 같은 방식으로 .endpoint<OauthEndpoint>()에 레이블을 붙이고, key는 <service>.signal.<endpoint> 아래에 생깁니다.

에러, UI 문구, 언어 목록

.error()에는 무언가 잘못됐을 때의 메시지를, .translate()에는 모듈이 보여주는 그 밖의 문구를 담습니다. 둘 다 t()나 .desc() 없이 언어 배열을 그대로 받습니다.
중괄호로 감싼 단어는 자리 표시자이고, key와 함께 넘긴 데이터로 채워집니다:
apps/koyo/lib/ticket/ticket.dictionary.ts
  • key 위치: 에러는 error 아래(ticket.error.overdue), 문구는 모듈 바로 아래(ticket.openTicketLoading)에 생깁니다.
  • 표기 규칙: 영어 레이블은 Title Case로, 한국어는 평소 쓰는 도메인 용어로 씁니다. 한국어 .error() 문장은 다.로 끝냅니다.
언어 목록
builder에 넘기는 배열이 언어 목록이고, 모든 언어 배열은 그 순서를 따릅니다. 언어는 두 개보다 많아도 됩니다:
apps/koyo/lib/_koyo/koyo.dictionary.ts
  • 컴파일러는 개수만 검사하고 순서는 검사하지 않습니다. ["제목", "Title"]도 컴파일되고, 영어 사용자에게 한국어가 보입니다.
  • 어떤 언어를 제공할지는 앱이 정합니다. akan.config.ts의 i18n: { defaultLocale, locales }에 적고, 기본값은 ["en", "ko"] 중 en입니다.
  • 없는 언어는 기본 언어로 대신합니다. dictionary가 선언하지 않은 언어는 기본 언어 문구를 보여주고, 어느 언어에도 없는 key는 key 문자열이 그대로 보입니다.

한눈에 보는 규칙

  • key는 타입을 따릅니다. 임의의 문자열을 만들지 말고 constant, filter, slice, endpoint에 맞춥니다.
  • 레이블은 사람이 읽습니다. 변수 이름 due보다 "기한"이 낫습니다.
  • .desc()를 붙입니다. 레이블이 폼, 툴팁, API 문서, AI 에이전트 중 한 곳에라도 보일 수 있다면 필요합니다.
  • 자리마다 헬퍼가 정해져 있습니다. UI는 usePage()의 l(), 서버 로직은 Err, store는 msg를 씁니다.
  • 더하기 전에 확장합니다. 라이브러리에 이미 있는 모델은 ...model.dictionaries에서 출발한 뒤 앱의 레이블을 더합니다.
  • 언어 배열은 언어 목록을 따릅니다. 순서도 개수도 같아야 합니다.
자주 하는 실수
이렇게 쓰지 말고
↳ 이렇게 씁니다
l("ticket.status.active")
enum key는 enum 이름으로 시작합니다: l("ticketStatus.active").
JSX에 직접 적은 문구
보이는 문구는 모두 l("ticket.modelName")이나 l.trans({ en, ko })를 거칩니다.
비어 있는 단계를 지우기
.insight<TicketInsight>((t) => ({}))처럼 빈 채로 둡니다.
.desc() 없는 endpoint 레이블
설명을 적습니다. AI 에이전트는 endpoint 설명을 보고 툴을 고릅니다.
이어서 읽기

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

내 AI에 이 문서 연결하기

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