사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
UI 레퍼런스▾

akanjs/constant

akanjs/constant는 Akan의 스키마 층입니다. 모든 .constant.ts 파일은 이 패키지의 export 두 개로 만듭니다. class를 선언하는 via, 그리고 via가 넘겨 주는 builder field입니다.
import { via } from "akanjs/constant";
나머지는 모두 그 둘을 돕는 도구입니다. getDefault부터는 직접 호출하기보다 읽게 되는 helper입니다.
모든 constant class를 선언합니다. 넘기는 인자가 어떤 class가 될지 정합니다.
via가 넘겨 주는 builder입니다. 한 번 부를 때마다 저장되는 필드 하나를 선언합니다.
field.visualfield.hiddenfield.secret
값을 에이전트, 클라이언트, 기본 조회에서 빼 두는 field의 변형입니다.
저장하지 않고 서버가 응답마다 계산하는 필드를 선언합니다.
새 레코드나 폼이 시작하는 빈 객체를 만듭니다.
DocumentModelDefaultOfQueryOfPurifiedModelProtoFileProtoLightFile
저장되는 모양, 기본값 모양, query, purify한 값, 파일을 나타내는 타입입니다.
crystalizemakePurify
원시 데이터를 model 값으로, model을 검사를 마친 일반 객체로 바꿉니다.
serializedeserialize
경계를 넘나드는 payload와 런타임 값 사이를 변환합니다.
런타임에 model class, refName, enum을 찾아 줍니다.
이 페이지에서 쓰는 말
refName
모듈이 등록된 camelCase 이름입니다. 예: banner
scalar
자기 테이블 없이 다른 문서 안에 통째로 들어가는 값 객체입니다.
relation
타입이 다른 데이터베이스 model인 필드입니다. 그 문서의 id를 저장합니다.
projection
읽어 올 필드를 적는 조회 옵션입니다. 예: { password: true }
모듈 하나를 이루는 다섯 class
데이터베이스 모듈은 아래 다섯 class를 항상 이 순서로 선언합니다. 이 페이지는 banner 모듈을 예로 듭니다:
BannerInput
배너를 만들거나 고칠 때 받는 필드입니다.
BannerObject
Input에 서버가 관리하는 필드와 id, createdAt, updatedAt, removedAt을 더합니다.
LightBanner
목록에 필요한 필드만 고른 class입니다. 표시·판별 메서드를 여기에 둡니다.
Banner
full model입니다. 모든 필드와 resolve가 계산하는 필드를 가집니다.
BannerInsight
목록 전체에 대한 집계 값입니다. count가 기본으로 들어 있고, 비어 있어도 작성합니다.
  • 순서는 정해져 있습니다. 맨 위에 enumOf class를 두고, 그 아래로 Input, Object, Light, full, Insight 순서로 씁니다. Insight class는 비어 있어도 작성합니다.
  • 공유 로직은 Light에 둡니다. 서버와 클라이언트가 모두 Light class를 갖고 있으므로 isNew()나 canWrite(user)는 util 모듈이 아니라 여기에 둡니다.
  • scalar는 class 하나입니다. via((field) => ({ … })) 하나로 선언하며, 위치는 lib/__scalar/<name>/입니다.

via

via는 namespace가 아니라 overload된 함수 하나입니다. via.model이나 via.scalar는 없고, 넘기는 인자가 어떤 class가 될지 정합니다.
인자
↳ 만드는 class
(field) => ({ … })
BannerInput 또는 scalar입니다. builder callback 하나만 넘기면 둘 중 하나가 됩니다.
BannerInput, (field) => ({ … })
BannerObject입니다. Input의 필드에 새 필드를 더합니다.
BannerObject, ["title", …] as const, (resolve) => ({ … })
LightBanner입니다. 이름을 적은 필드와 resolve 필드만 가집니다.
BannerObject, LightBanner, (resolve) => ({ … })
Banner(full model)입니다. Object와 Light를 합치고 resolve 필드를 더합니다.
Banner, (field) => ({ … })
BannerInsight입니다. 기본 count에 새 필드를 더합니다.
libs/shared의 banner 모듈을 줄인 것입니다:
libs/shared/lib/banner/banner.constant.ts
  • callback의 매개변수가 builder를 알려 줍니다. Input, Object, Insight는 field를 받습니다. Light와 full은 계산 필드만 더하므로 resolve를 받습니다.
  • Light의 필드 목록에는 as const를 붙입니다. Light가 고르는 필드 이름은 항상 리터럴 튜플로 씁니다.
  • lib의 class를 확장하려면 맨 뒤에 넘깁니다. 모든 형태가 제 인자 뒤에 class를 더 받으므로, 앱은 lib model의 class를 끝에 넘겨 필드를 더합니다.

field

field는 via가 Input, Object, Insight callback에 넘겨 주는 builder입니다. 한 번 부를 때마다 저장되는 필드 하나를 선언하며, 타입을 먼저, 옵션을 그다음에 씁니다.
타입 쓰는 법
작성
↳ 뜻
field(String)
값 하나입니다. String, Boolean, Date, ID, Int, Float, Any를 씁니다.
field([String])
배열입니다. [[Float]]처럼 대괄호를 세 겹까지 겹칠 수 있습니다.
field(File)
다른 model 문서와의 relation입니다. 그 문서의 id로 저장됩니다.
field(Coordinate)
scalar를 이 문서 안에 통째로 넣습니다.
field(ProductStatus)
enumOf로 선언한 enum class입니다.
field(Map, { of: String })
문자열 key를 쓰는 Map입니다. of로 값 타입을 적으며, 빠뜨릴 수 없습니다.
field<T>(Any)
모양을 열어 둔 값입니다. 타입 인자로 TypeScript 타입을 유지합니다.
  • 숫자는 Int나 Float로 씁니다. field(Number)는 타입 검사를 통과하지 못합니다.
  • 바이트는 필드가 될 수 없습니다. Binary와 Upload는 signal에서만 씁니다. 파일은 field(File)로 저장합니다.
이 중 대부분을 쓰는 product input입니다:
apps/myapp/lib/product/product.constant.ts
옵션
두 번째 인자는 옵션 객체입니다. nullable, select, enum, meta는 옵션으로 쓰지 않습니다. .optional(), field.secret, enumOf 타입, .meta()가 대신 정합니다.
값과 검사
defaultT | ((doc: { id: string }) => T)
시작 값입니다. 함수로 주면 레코드마다 새로 실행됩니다.
validate(value, model) => boolean
직접 작성하는 검사로, purify와 문서 저장 때마다 실행됩니다. false면 값을 거부합니다.
immutableboolean기본값 false
문서 저장으로 값을 바꾸면 에러가 납니다. query 단위 쓰기는 검사하지 않습니다.
of
Map 필드의 값 타입입니다. String이나 scalar 등을 적으며, Map에는 꼭 필요합니다.
visualboolean기본값 false
field.visual로 선언한 것과 같습니다.
accumulatequery 객체
Insight 필드에만 씁니다. 이 카운터가 셀 조건이며, {}는 조회 결과 전부를 셉니다.
검색과 relation
text"title" | "desc" | "tag" | "thumb" | "filter"
그 역할로 전문 검색에 넣습니다. thumb는 표시용으로만 저장되고 검색되지 않습니다.
cascade"removeRef" | "removeWith" | "removeWithAny"
관련 문서를 함께 지웁니다. 값이 어느 쪽이 어느 쪽을 따라 지워지는지 정합니다.
refstring
ID 필드가 가리키는 model의 refName입니다. 예: { ref: "org", cascade: "removeWith" }
refPathstring
id가 어느 model을 가리키는지 담은 필드 이름입니다. enumOf 필드를 쓰고, removeWithAny일 때만 String을 씁니다.
  • text는 문자열 필드에 씁니다. title, desc, tag는 String만 받고, thumb와 filter는 ID나 relation도 받습니다. Map과 중첩 배열에는 역할을 줄 수 없습니다.
  • cascade의 값은 방향입니다. removeRef는 주인 쪽 relation에, removeWith는 자식이 주인을 가리키는 필드에 씁니다. 방향을 틀리면 엉뚱한 문서가 지워집니다.
문서와 샘플 전용
스키마 문서, API explorer, sampleOf()에 필드를 설명하는 옵션입니다. 값을 막지는 않으므로, 반드시 지켜야 하는 규칙은 validate에 둡니다.
minnumber
스키마 문서에 표시되는 하한입니다. sampleOf()는 이 값을 샘플로 씁니다.
maxnumber
상한입니다. min과 같은 방식으로 쓰입니다.
minlengthnumber
스키마 문서에 표시되는 최소 길이입니다. 배열이면 purify가 항목 수를 실제로 검사합니다.
maxlengthnumber
최대 길이입니다. minlength와 같은 방식으로 다룹니다.
type"email" | "password" | "url"
sampleOf()가 그럴듯한 이메일, 비밀번호, URL을 만들게 합니다.
exampleT
스키마 문서와 API explorer의 예시 요청에 쓰이는 샘플 값입니다.
refType"child" | "parent" | "relation"
스키마 문서가 relation에 붙여 보여 주는 이름표입니다.
이어 붙이는 메서드
.optional()
null을 허용합니다. default가 없으면 null에서 시작합니다.
.meta(obj)
필드에 자유 형식의 metadata를 붙입니다. 집계 카운터는 이것으로 자기가 세는 목록을 적습니다.

field.visual / field.hidden / field.secret

field의 변형 세 가지입니다. 셋 다 보통 필드처럼 저장되고, 저장한 값이 어디까지 갈 수 있는지만 다릅니다. 아래 표의 조회는 projection 없는 서버 조회, 초안은 저장해 둔 폼 초안입니다.
필드
조회
페이지
에이전트
초안
검색
페이지로 가는 필드
field
✓
✓
✓
✓
✓
보통 필드입니다. 어디서나 읽습니다.
field.visual
✓
✓
✓
✓
페이지에는 그려지고, 에이전트가 읽는 값에서는 빠집니다.
서버에 남는 필드
field.hidden
✓
서버 코드는 읽고, 클라이언트는 null을 받습니다.
field.secret
이름을 적은 projection으로만 읽힙니다.
✓값이 닿을 수 있음닿지 않음
하나씩 모두 쓴 profile입니다:
apps/myapp/lib/profile/profile.constant.ts
  • visual은 비밀이 아니라 비용의 문제입니다. blur placeholder나 렌더링한 HTML 본문은 화면에는 필요하지만 에이전트가 질문에 답하는 데는 쓰이지 않습니다. 저장, 검색, 폼, 페이지는 그대로 동작합니다.
  • hidden의 타입은 여전히 string입니다. 타입에 null이 들어 있는 secret과 달리 string으로 적혀 있지만, 클라이언트는 null을 읽습니다.
  • secret은 조회조차 되지 않습니다. pickById(id, { password: true })처럼 이름을 적은 projection으로만 읽히며, 이때는 적은 필드만 돌아옵니다. 그렇게 읽어도 endpoint 응답에서는 빠집니다.
  • hidden과 secret에는 text 역할을 줄 수 없습니다. 검색 색인은 평문이므로 둘에 text를 쓰면 타입 에러가 납니다.

resolve

resolve는 Light와 full callback이 받는 builder입니다. 저장하지 않는 필드를 선언하며, 서버가 응답을 만들 때마다 값을 계산합니다.
먼저 model에 선언합니다:
apps/myapp/lib/order/order.constant.ts
그다음 모듈의 Internal class에 같은 이름의 resolveField를 두어 계산합니다. 저장된 order 문서가 인자로 들어오며, 여기서는 unitPrice와 quantity 필드가 있다고 가정합니다:
apps/myapp/lib/order/order.signal.ts
  • resolve 필드마다 resolveField가 있어야 합니다. Internal의 타입이 resolve 필드를 모두 요구하므로, 하나라도 빠지면 타입 검사에서 막힙니다.
  • optional은 양쪽에 함께 씁니다. resolve(Int).optional()에는 resolveField(Int, { nullable: true })가 짝입니다.
  • this로 service를 부릅니다. endpoint처럼 exec를 function으로 쓰면, 조회가 필요할 때 this.orderService를 호출할 수 있습니다.
  • text 역할은 없습니다. 계산한 값은 검색 색인에 들어가지 않으므로 resolve는 이 옵션을 받지 않습니다.

getDefault

getDefault는 새 레코드나 폼이 시작하는 빈 객체를 만듭니다. 보통은 model을 통해 Model.getDefault()로 부릅니다.
필드
↳ 시작 값
field.hidden · field.secret
항상 null입니다.
default: () => …
함수의 반환값입니다. 호출마다 다시 실행합니다.
배열 필드
default를 새로 복사한 배열입니다. 없으면 []입니다.
그 밖의 default
그 값 자체입니다. 이 값으로 만든 객체가 모두 같은 값을 나눠 씁니다.
default 없는 .optional()
null
넣어 둔 scalar
그 scalar의 기본값 객체입니다.
relation
null
그 밖의 타입
타입의 빈 값입니다. 예: "", 0, false
두 가지 호출 방법을 테스트로 보면 다음과 같습니다:
libs/shared/lib/banner/banner.test.ts
  • Model.getDefault()는 한 번만 만듭니다. 첫 호출에서 객체를 만들고 이후에는 얕은 복사본을 돌려줍니다. 그래서 여기서는 () => dayjs() default도 첫 값 그대로입니다.
  • field map으로 부르면 함수를 매번 다시 실행합니다. getDefault(Model[FIELD_META])는 호출할 때마다 default 함수를 모두 실행합니다.

DocumentModel / DefaultOf / QueryOf

document, store, test에서 model의 다른 모양을 가리킬 때 쓰는 타입입니다. 타입으로만 import합니다:
import type { DefaultOf, DocumentModel, PurifiedModel, QueryOf } from "akanjs/constant";
DocumentModel<T>
저장되는 모양입니다. relation은 id 문자열로, relation 목록은 string[]로 바뀝니다.
DefaultOf<T>
getDefault()가 돌려주는 모양입니다. 메서드는 빠지고, relation 필드는 null일 수 있습니다.
QueryOf<T>
안을 들여다볼 수 없는 query 값이며, 타입은 any입니다. slice의 exec가 이것을 돌려줍니다.
PurifiedModel<T>
purify가 돌려주는 모양입니다. relation은 id가 되고, 날짜는 Dayjs 타입 그대로입니다.
ProtoFileProtoLightFile
File과 LightFile의 모양입니다. 파일을 prop으로 받는 UI 코드가 씁니다.

crystalize / purify

이 둘은 원시 데이터와 model 인스턴스 사이에서 값을 서로 반대 방향으로 옮깁니다. 이름으로 부르기보다 model을 통해 씁니다.
crystalize — 원시 값에서 model로
필드 하나의 원시 값을 바꿉니다. 날짜 문자열은 Dayjs로, 중첩 객체는 그 class로 바뀝니다. 생성자와 set()도 같은 변환기를 씁니다.
crystalize(field.getProps(), value)
purify — model에서 일반 객체로
모든 필드를 검사하고(필수 값, enum, validate, 배열 길이) relation을 id로 바꿉니다. 검사에 실패하면 null을 돌려줍니다.
Model.purify(value) · makePurify(Model)
실제로는 생성자로 만들고, model의 purify로 검사합니다:
libs/shared/lib/banner/banner.test.ts
  • purify는 이름으로 export되지 않습니다. 모든 class가 static Model.purify로 갖고 있고, makePurify(Model)가 같은 함수를 만듭니다.
  • 빈 문자열인 필수 필드는 통과하지 못합니다. 필수 String이나 ID가 "" 그대로면 실패하므로, 위의 첫 purify는 null입니다.
  • store는 보내기 전에 purify를 거칩니다. 생성된 create·update action은 Input class로 폼을 purify하고, 결과가 null이면 아무것도 보내지 않습니다.

serialize / deserialize

런타임 값과, document나 전송 경계를 넘는 payload 사이를 변환합니다.
serialize — 런타임 값에서 payload로
model의 필드를 따라가며 Dayjs는 Date로, Map은 일반 객체로 바꿉니다.
deserialize — payload에서 런타임 값으로
primitive를 하나씩 해석해 날짜 문자열을 Dayjs로 바꾸고, 안에 넣은 scalar까지 따라 들어갑니다.
serialize(ref, arrDepth, value, type?, opts)
type의 기본값은 "object"이고, "input"이면 relation을 id로 보냅니다. opts는 { nullable?, key? }입니다.
deserialize(ref, arrDepth, value, opts)
opts는 { nullable?, key?, enum?, convertFn? }입니다. enum을 주면 목록에 없는 값은 에러를 던집니다.
ConstantRegistry.serializeConstantRegistry.deserialize
짧은 형태로, (ref, value, nullable?)를 받습니다. 목록이면 [Ref]를 넘기며, primitive, Map, 등록된 model을 다룹니다.
날짜 하나가 나갔다가 돌아오는 모습입니다:
libs/shared/lib/banner/banner.test.ts
  • deserialize는 데이터베이스 model을 relation으로 봅니다. primitive와 scalar만 변환하고, model의 값은 받은 그대로 돌려줍니다. 인스턴스는 new cnst.X(value)로 만듭니다.
  • 필수 값이 없으면 에러를 던집니다. nullable이 없고 타입도 Any가 아니면, 둘 다 null과 undefined를 거부합니다.

ConstantRegistry

ConstantRegistry는 런타임에 model class와 refName을 이어 줍니다. 모든 모듈의 class, scalar, enum이 여기에 등록됩니다.
import { ConstantRegistry } from "akanjs/constant";
getRefName(Model)
model의 refName입니다. 등록되지 않은 class면 { allowEmpty: true }가 없는 한 에러를 던집니다.
getModelName(Model)
역할에 맞는 class 이름입니다. 예: BannerInput, LightBanner
getModelRef(refName, modelType?)
refName과 역할에 맞는 class입니다. 역할을 빼면 "Int" 같은 primitive를 찾습니다.
getDatabase(refName)getScalar(refName)
등록된 모듈 항목입니다. model은 다섯 class, scalar는 class 하나를 담습니다. allowEmpty가 없으면 에러를 던집니다.
has(Model)
그 class가 등록되어 있는지 알려 줍니다.
isFullisLightisObjectisInsightisScalar
class가 어떤 역할인지 확인합니다.
serializedeserialize
위의 serialize / deserialize 절에서 설명한 짧은 형태입니다.

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

내 AI에 이 문서 연결하기

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