사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
앱 & 라이브러리▾
도메인▾
스칼라▾
service.dictionary.ts
<service>.dictionary.ts는 service 모듈이 보여 주는 endpoint, 에러, 그 밖의 문구에 언어별 레이블을 붙입니다. endpoint를 추가하거나, 새 에러를 던지거나, 새 문구를 화면에 띄울 때 이 파일을 엽니다.model dictionary는 필드에 이름을 붙이는 것으로 시작합니다. service 모듈에는 필드가 없어서 한 단계 뒤인 endpoint부터 시작하고, 첫 줄부터 다릅니다.
modelDictionary가 아니라 serviceDictionary입니다.이 페이지에서 쓰는 말
용어설명
label
사람이 읽는 이름입니다. 언어마다 하나씩 적습니다:
fn(["Disconnect App", "앱 연결 끊기"])..desc()
레이블 옆에 붙는 긴 설명입니다. AI 에이전트는 이 설명을 읽고 툴을 고릅니다.
key
코드가 문구를 꺼낼 때 쓰는, 점으로 이은 경로입니다.
oauth.consentTitle 같은 모양입니다.언어 배열
언어마다 문자열 하나씩,
serviceDictionary(["en", "ko"])에 적은 언어 순서대로 쓴 배열입니다.세 단계
단계담는 것
.endpoint<XEndpoint>((fn) => ({}))
signal endpoint마다 항목 하나입니다. 레이블,
.desc(), 인자마다 붙이는 .arg()로 이루어집니다..error({})
service가
new Err("<service>.error.<key>")로 던지는 모든 key입니다. 한국어는 다.로 끝냅니다..translate({})
endpoint도 에러도 아닌 나머지 문구입니다. 모듈 이름 바로 아래의 key로 읽습니다.
- 모든 단계는 선택입니다. 담을 것이 있는 단계만 씁니다.
_security는.endpoint()만 쓰고,_localFile에는.translate()가 없습니다. - 순서는 자유지만 관례는 하나입니다. 각 단계가 같은 builder를 돌려주므로 어떤 순서든 동작합니다. 그래도 읽는 사람이 찾는 순서인 endpoint → error → translate로 씁니다.
- 배열이 언어 목록입니다.
serviceDictionary(["en", "ko"])가 파일 안 모든 언어 배열의 순서를 정합니다.
endpoint에 이름 붙이기
.endpoint()에 signal의 endpoint class를 타입 인자로 넘기고, endpoint마다 레이블과 .desc()를 붙입니다. endpoint가 하나뿐인 _localFile 모듈의 파일 전체입니다:libs/util/lib/_localFile/localFile.dictionary.ts
- key는 endpoint class를 따릅니다. 콜백이 endpoint마다 항목을 하나씩 돌려줘야 하므로, endpoint 이름을 바꾸거나 새로 추가하면 첫 렌더가 아니라 컴파일 시점에 dictionary가 깨집니다.
- class는
import type으로 가져옵니다. dictionary는 공유 계약 파일이라, 값으로 import하면 signal의 runtime 그래프가 뒤에 딸려 들어옵니다. - 빠뜨리면 안 되는 것이 둘 있습니다. 레이블 옆의
.desc(), 그리고localFile.service.ts가 이름으로 던지는 error key입니다.
어떤 문구를 누가 읽는지
레이블은 사람이, 설명은 모델이 읽습니다. 각 문구가 보이는 곳은 다음과 같습니다:
문구
API 탐색기
OpenAPI
MCP
endpoint
label
✓
✓
✓
API 탐색기의 제목, OpenAPI의
summary, MCP 툴의 title이 됩니다..desc()
✓
✓
✓
에이전트가 툴을 고를 때 읽는 설명입니다. API 탐색기에서는 레이블 아래에 보입니다.
.arg() 안의 인자
label
✓
API 탐색기에서 식별자 옆에만 보입니다.
.desc()
✓
✓
✓
MCP input schema와 OpenAPI의 path·query 파라미터에서 그 인자의 설명이 됩니다.
✓여기에 보임쓰지 않음


모든 endpoint에
.desc()를 적습니다. 에이전트는 설명을 읽고 툴을 고르며, 레이블만 있으면 이름 말고는 아무것도 모릅니다. MCP는 option.setMcp({ language })로 바꾸지 않는 한 영어 항목을 읽으므로, 영어 설명만으로 뜻이 통해야 합니다.모든 인자에 이름 붙이기
.arg()로 endpoint가 선언한 인자마다 이름을 붙입니다. 커스텀 endpoint가 받는 skip, limit, sort도 포함되며, 하나라도 빠지면 dictionary가 컴파일되지 않습니다:libs/shared/lib/_oauth/oauth.dictionary.ts
- 값을 어디서 얻는지 적습니다. 이 설명은 session ID가 무엇인지가 아니라, 호출하는 쪽이 그 값을 어디서 얻는지(연결된 앱 목록)를 알려 줍니다.
- 이 한 문장이 타입보다 값집니다. MCP input schema에서 인자 설명이 되는 문장입니다. 없으면 에이전트에게는 식별자 철자밖에 없어서 값을 찍어 맞힙니다.
- 레이블은 사람을 위한 것입니다. API 탐색기에서
sessionId옆에 "세션 ID"로 보입니다.
에러와 문구
에러 key는 throw의 나머지 절반입니다. service가
new Err("oauth.error.notSignedIn")을 던지면, 그 key가 사람이 읽을 문장이 되는 곳은 .error()뿐입니다..translate()에는 모듈이 보여 주는 그 밖의 문구를 담습니다. 두 단계 모두 t()나 .desc() 없이 언어 배열을 그대로 받습니다:libs/shared/lib/_oauth/oauth.dictionary.ts
Err는 등록된 key만 받습니다. 오타나.error()에 없는 key는 타입 에러입니다. 읽는 사람의 언어에도 기본 언어에도 문구가 없는 key는 key 문자열 그대로 보입니다.- 중괄호는 호출하는 쪽이 채우는 자리입니다.
l("oauth.connectedAt", { at })이{at} 연결을 채우고,new Err(key, { days })도 같은 방식으로 에러 문구를 채웁니다. - 값을 넘겼는지는 아무도 검사하지 않습니다. 빠지면
{at}이 문구에 그대로 남으므로, 자리 이름을 뻔하게 짓습니다.
어투
문장 끝은 그 문장이 누구에게 말하는지를 따릅니다. 위 파일의 두 어투는 모두 의도된 것입니다:
단계쓰는 법
.error()
무엇이 잘못됐는지 서술하고
다.로 끝냅니다. 사과하는 문장이 아닙니다..translate()
다.는 단순한 서술일 때만 쓰고, consentScope처럼 사용자에게 말을 거는 문구는 습니다로 끝냅니다.label
영어는 Title Case로, 한국어는 평소 쓰는 도메인 용어로 씁니다.
key로 문구 읽기
모든 key는 service 이름 아래에 놓이고, 그다음 자리는 단계가 정합니다. endpoint 레이블에는 문구와 섞이지 않도록
signal 구간이 붙고, 문구에는 아무것도 붙지 않습니다.key적는 곳과 읽는 방법
<service>.signal.<endpoint>
.endpoint()에 적은 endpoint 레이블입니다. l()로 읽습니다.<service>.signal.<endpoint>.desc
그 레이블의
.desc()입니다. 레이블 key 뒤에 .desc를 붙여 l()로 읽습니다.<service>.signal.<endpoint>.arg.<arg>
.arg()에 적은 인자 레이블입니다. l()로 읽습니다.<service>.error.<key>
.error()에 적은 에러입니다. 서버에서는 new Err()로 던지고, 클라이언트에서는 msg.error()로 띄웁니다.<service>.<key>
.translate()에 적은 문구입니다. l()로 읽습니다.OAuth 동의 페이지는 서버에서 이렇게 문구를 읽습니다. 마크업은 줄여서 옮겼습니다:
libs/shared/page/oauth/consent/_index.tsx
usePage()는 서버에서도 동작합니다. 동의 페이지는 server component이므로 레이블 때문에 client 경계가 생기지 않습니다.- 던진 에러는 따로 읽을 필요가 없습니다. store action이
Err로 실패하면 store가 그 문구를 읽는 사람의 언어로 바꿔 토스트로 띄웁니다. 클라이언트에서 막는 검사라면msg.error("oauth.error.notSignedIn")를 띄우고 return합니다. - 한 번 쓰는 문구는 화면의 것입니다. 한 화면에서 한 번만 쓰는 문구는 그 component에서
l.trans({ en, ko })로 씁니다. component 하나만 읽는 key는 괜히 누군가 계속 맞춰 줘야 하는 key입니다.



l()은 error key를 받지 않습니다. l("oauth.error.notSignedIn")은 타입 에러입니다. error key는 서버에서는 Err로, 클라이언트에서는 msg.error()로 씁니다.자주 하는 실수
| 이렇게 쓰지 말고 |
|---|
| ↳ 이렇게 씁니다 |
import { OauthEndpoint } from "./oauth.signal" |
import type으로 씁니다. 값 import는 signal의 runtime 그래프를 dictionary로 끌어옵니다. |
.desc() 없는 endpoint 레이블 |
| 설명을 적습니다. 에이전트는 설명을 보고 툴을 고릅니다. |
| 이름을 되풀이하는 인자 설명: "세션 ID" |
| 호출하는 쪽이 값을 어디서 얻는지 적습니다: "연결된 앱 목록의 세션 ID". |
component 하나만 읽는 .translate() key |
그 component 안에서 l.trans({ en, ko })로 씁니다. |
이어서 읽기