사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
커스터마이즈
akanjs/ui 컴포넌트는 fork하지 않고도 라우트 단위로 모양을 바꿀 수 있습니다. 앱의 ui/에 교체 컴포넌트를 만들고 page/**/_overrides.tsx 매니페스트에서 슬롯에 연결하면 됩니다. 그러면 그 폴더 아래의 모든 <Modal>, <Button>, <Table>이 호출부를 한 줄도 고치지 않고 여러분의 컴포넌트로 그려집니다.매니페스트는 레이아웃처럼 라우트 트리를 따라 내려가고, 가장 가까운 것이 우선합니다.
이 페이지에서 쓰는 말
용어설명
슬롯
프레임워크 컴포넌트를 갈아 끼울 수 있는 이름 붙은 자리입니다.
Modal, InputPassword 같은 것입니다._overrides.tsx
page/ 폴더에 두는 매니페스트입니다. 그 폴더 아래 모든 라우트의 슬롯을 연결합니다.교체 컴포넌트
여러분이 만든 대체품입니다. 원래 컴포넌트와 같은 props를 받으므로 호출부는 그대로입니다.
headless 부품
동작은 있고 자기 모양은 없는 부품입니다.
Dialog.Modal이 그 예입니다.recipe
variant에 맞는 className을 돌려주는 함수입니다.
buttonRecipe({ variant: "primary" })처럼 씁니다.슬롯의 두 종류
한 매니페스트에 두 종류를 함께 적습니다. 모양만 문제라면 recipe를, 마크업이 문제라면 컴포넌트를 바꿉니다.
컴포넌트 슬롯
슬롯 46개 · 타입은 AkanUiOverrides
컴포넌트 전체(마크업, 클래스, 재사용하지 않은 동작)를
apps/<app>/ui/에 만든 컴포넌트로 바꿉니다. 모든 호출부와 그 props는 그대로입니다.override({ Modal: BrandModal })레시피 슬롯
슬롯 3개 · 타입은 AkanUiRecipes
클래스만
apps/<app>/ui/Recipe/의 recipe로 바꿉니다. 호출부, 마크업, async 상태, 포커스 처리, 접근성은 그대로입니다.override({ recipes: { button: neonButtonRecipe } })

_overrides.tsx에는 로직을 넣지 않습니다. "use client"도 필요 없고, import와 export default override({ … }) 하나만 둡니다.동작 방식
파일 두 개면 됩니다.
ui/의 컴포넌트 하나와, 슬롯을 그 컴포넌트에 연결하는 page/의 매니페스트 하나입니다.1. 교체 컴포넌트 작성
Dialog의 headless 부품으로 조립하면 포커스 가두기, Escape 닫기, 스크롤 잠금, portal이 그대로 동작합니다:apps/<app>/ui/BrandModal.tsx
- 슬롯 타입을 붙입니다.
AkanModalComponent(다른 슬롯은AkanUiOverrides["<Slot>"])가 모든 prop에 타입을 주고, 그대로 끼울 수 있는 컴포넌트인지 검사합니다. - 모든 prop을 넘깁니다.
Model.New,Model.EditModal,Model.Remove는 버튼을action으로 넘깁니다. 이것을 버리는 교체본은 그 버튼을 잃습니다.
2. 매니페스트에 연결
적용할 라우트가 있는 폴더의
_overrides.tsx에서 슬롯을 여러분의 컴포넌트에 연결합니다:apps/<app>/page/_overrides.tsx
override()는 타입만 검사합니다. 맵을 그대로 돌려줍니다. 키는 PascalCase 슬롯 이름이고, 각 값은 그 슬롯의 props와 맞는지 검사됩니다.- 슬롯 이름을 잘못 쓰면 타입 오류입니다. 조용히 아무 일도 안 하는 연결이 되지 않습니다.
- 다른 곳은 고칠 것이 없습니다. 이제
page/아래의 모든<Modal>이BrandModal로 그려집니다.
akanjs/ui에서 가져오는 것
export설명
override
_overrides.tsx가 export하는 매니페스트를 만듭니다. 인자를 그대로 돌려주고 타입만 검사합니다.AkanUiOverrides
슬롯 이름마다 컴포넌트 타입을 매핑합니다. 교체 컴포넌트에
AkanUiOverrides["Table"]처럼 붙입니다.AkanModalComponent
AkanUiOverrides["Modal"]의 줄임 타입입니다.AkanUiRecipes
레시피 슬롯(
button, badge, input)마다 className 팩토리 타입을 매핑합니다.AkanUiOverrideManifestAkanUiOverrideName
매니페스트 전체의 모양, 그리고 슬롯 이름의 union입니다.
Dialog
Modal 교체본을 조립하는 headless 부품(.Modal, .Title, .Content, .Action, .Trigger)입니다.DefaultApprovalDefaultBubbleDefaultCodeDefaultComposerDefaultLauncherDefaultMarkdownDefaultAgentMenuDefaultQuestionDefaultQueuedDefaultStepsDefaultToolCardDefaultToastDefaultToastItem
채팅 부품 슬롯 열한 개와 Toast 슬롯 두 개의 기본 구현입니다. 다시 쓰지 않고 감싸서 쓸 수 있습니다.
agentAttrs
기본 컨트롤이 에이전트용으로 다는
data-akan-* 속성입니다. 교체 컴포넌트에 펼쳐 넣습니다.triggerSlot
Dropdown의 클릭과 aria 상태를 교체 컴포넌트가 그리는 트리거에 직접 붙입니다.UiOverrideProvider
원하는 하위 트리에 override 맵을 직접 마운트합니다. 라우트 매니페스트 위에 합쳐집니다.
useUiOverrideuseUiRecipe
이 하위 트리에서 슬롯에 연결된 컴포넌트나 recipe를 읽습니다. 없으면
undefined입니다.createOverridable
컴포넌트를 감싸 이름 붙은 슬롯을 거쳐 결정되게 하고, 연결이 없으면 기본값을 씁니다.
적용 범위
_overrides.tsx를 어디에 두느냐가 적용되는 라우트를 정합니다. route group 폴더든 일반 segment 폴더든 됩니다.파일적용 범위
page/_overrides.tsx
앱의 모든 라우트
page/(admin)/_overrides.tsx
(admin) 그룹 안의 라우트만page/settings/_overrides.tsx
/settings와 그 아래 모든 라우트매니페스트는 겹쳐 쌓입니다. 안쪽 매니페스트가 앱 전역 매니페스트를 좁힙니다:
apps/<app>/page/_overrides.tsx · apps/<app>/page/(admin)/_overrides.tsx
- 가장 가까운 매니페스트가 우선합니다.
(admin)안에서는<Modal>이BrandModal이 아니라AdminModal로 그려집니다. - 적지 않은 슬롯은 물려받습니다. 슬롯 단위로 합쳐지므로, 안쪽 매니페스트가 적지 않은 슬롯은 위의 연결을 그대로 씁니다.
- recipe도 같은 방식으로 합쳐집니다.
button만 바꾼 하위 매니페스트도 상위의badge교체는 유지합니다. - 레이아웃도 적용됩니다. 매니페스트와 같은 폴더의
_layout.tsx와 그 아래의 모든 레이아웃은 루트 레이아웃이든 아니든 매니페스트 안에서 그려지므로, 거기에 마운트한<Modal>이나<Agent.Chat />도 교체본으로 바뀝니다. 매니페스트 폴더보다 위의 레이아웃은 바깥 라우트와 함께 쓰이므로 위의 연결을 그대로 씁니다.
교체할 수 있는 슬롯
프레임워크의 슬롯은 아래 46개이고, 목록에 없는 컴포넌트는 교체할 수 없습니다. 복합 컴포넌트의 leaf는 이름을 이어 붙입니다.
Input.Password는 InputPassword입니다.슬롯설명
BadgeModalEmptyPaginationPopconfirmDropdownTableMenuTooltipUnauthorized
단독 컴포넌트입니다. 렌더하는 이름이 곧 키입니다.
<Modal>은 Modal입니다.ButtonSelect
제네릭 컴포넌트입니다. 교체 컴포넌트는 제네릭 없이 씁니다. 아래 제네릭 컴포넌트 절을 보세요.
InputInputTextAreaInputPasswordInputEmailInputNumberInputCheckbox
Input과 leaf 다섯 개(Input.TextArea부터 Input.Checkbox까지)입니다.RadioRadioItem
Radio와 Radio.Item입니다.DatePickerDatePickerRangePickerDatePickerTimePicker
DatePicker와 .RangePicker, .TimePicker입니다.ToggleSelectToggleSelectMulti
제네릭
ToggleSelect와 그 .Multi leaf입니다.LoadingSpinLoadingSkeletonLoadingProgressBarLoadingButtonLoadingInputLoadingArea
Loading.* 멤버 하나하나입니다. Loading 자체는 슬롯이 없는 단순 네임스페이스입니다.ToastToastItem
토스트 묶음, 그리고 그 안의 토스트 카드 하나입니다.
DraftBar
편집 셸이 복구한 폼에 띄우는 배너입니다. 복원과 버리기 동작은 그대로 연결돼 있습니다.
AgentChatAgentLauncherAgentBubbleAgentStepsAgentComposerAgentApprovalAgentQuestionAgentQueuedAgentMenuAgentMarkdownAgentToolCardAgentCode
인페이지 채팅입니다.
AgentChat은 패널 전체를, 나머지 열한 개는 각자 한 부분을 바꿉니다.슬롯이 아닌 것
컴포넌트설명
PortalInfiniteScrollClientSide
다른 동작 전용 컴포넌트처럼 배선만 맡고 자기 모양이 없어서, 바꿀 것이 없습니다.
Messages
System 안의 토스트 스택입니다. 토스트 모양을 바꾸려면 대신 Toast와 ToastItem을 연결합니다.Messages는 msg.* 배선, store 읽기, body 수준 portal, 자동 닫힘 타이머를 직접 들고 있습니다. Toast 슬롯을 쓰면 토스트가 언제 뜨고 사라지는지는 다시 만들지 않고 모양만 바꿀 수 있습니다.제네릭 컴포넌트
Button, Select, ToggleSelect는 제네릭이고, 호출부의 타입 추론은 그대로 유지됩니다. 슬롯은 가장 넓은 타입으로 저장되므로 교체 컴포넌트에는 제네릭이 필요 없습니다.슬롯 타입을 붙인
Button 교체 컴포넌트입니다:apps/<app>/ui/BrandButton.tsx
- 호출부의 타입은 그대로입니다.
<Select<MyEnum, true> … />와<Button<Todo> onSuccess={…} />는 여전히 value, onChange, 결과 타입을 추론합니다. - 가장 넓은 props로 작성합니다.
Button슬롯의 타입은ButtonProps<unknown>이라서,onClick은unknown을 돌려주고onSuccess가 그 값을 받습니다. - variant props는 DOM에 넘기지 않습니다.
variant,size,shape,outline은 recipe에 넘기고,<button>이 모르는loadingMode,showError는 뺍니다. agentAttrs(onClick)를 펼쳐 넣습니다. 기본 버튼에는 인페이지 에이전트가 읽는data-akan-*표시가 붙어 있으므로, 교체본도 이를 다시 붙여야 합니다.
복합 컴포넌트
하위 부품이 있는 컴포넌트는 leaf마다 슬롯이 하나씩 있고, 이름은
<Base><Sub>입니다. 원하는 leaf만 바꾸면 나머지는 기본 그대로입니다.렌더하는 것슬롯 키
<Input.Password />
InputPassword<Input.Checkbox />
InputCheckbox<Radio.Item />
RadioItem<DatePicker.RangePicker />
DatePickerRangePicker<ToggleSelect.Multi />
ToggleSelectMulti<Toast.Item />
ToastItem<Loading.Spin />
LoadingSpinleaf 하나만 연결하면 형제 leaf는 그대로입니다:
apps/<app>/page/_overrides.tsx
- 형제 leaf는 기본 그대로입니다. 이제
<Input.Checkbox />는BrandCheckbox로 그려지고,<Input />과<Input.Password />는 프레임워크 모양을 유지합니다. - 점 표기 접근은 그대로입니다.
Input.Password,Loading.Spin은 그 자리에 있고, 그려지는 것만 바뀝니다. Field.*도 leaf를 따라갑니다.Field.Text,Field.Email,Field.Password,Field.Number는Inputleaf를 그리므로, 그 leaf의 교체가 여기에도 적용됩니다.
레시피 슬롯
구조는 맞고 모양만 바꾸고 싶다면 컴포넌트 대신 recipe를 바꿉니다.
AkanUiRecipes로 타입이 정해진 recipes 키는 className 팩토리만 교체하고, async 상태, 포커스 처리, 접근성은 건드리지 않습니다.button(variants?: ButtonVariants, className?: ClassValue) => string
Button, 그리고 Popconfirm, Dropdown, Menu, Pagination, ToggleSelect 안의 버튼입니다.badge(variants?: BadgeVariants, className?: ClassValue) => string
Badge, 그리고 Field.Tags가 그리는 태그 칩입니다.input(variants?: InputSurfaceVariants, className?: ClassValue) => string
Input과 그 텍스트 입력 leaf, 그리고 채팅 입력창이 쓰는 필드 껍데기입니다.교체 recipe는 프레임워크 recipe의 variant 계약을 전부 받아야 합니다. 기존 호출부가 그 값을 그대로 넘기기 때문입니다:
apps/<app>/ui/Recipe/neonButton.ts
recipes 아래에 연결합니다. 컴포넌트 슬롯과 한 매니페스트에 함께 둘 수 있습니다:apps/<app>/page/(brand)/_overrides.tsx
- 축을 더해도 어휘가 넓어지지 않습니다. 타입상 허용되지만, 그 축은 교체 recipe의 타입을 아는 코드에서만 닿습니다.
- 어휘를 넓히려면 프레임워크 recipe에 축을 더하거나,
apps/<app>/ui/Recipe/아래에 앱 recipe를 만듭니다.


recipe 슬롯은 클라이언트에서, 라우트 범위로 다시 칠하는 장치입니다.
useUiRecipe(...)로 recipe를 찾는 프레임워크 클라이언트 컴포넌트에만 닿습니다. 앱 JSX에 직접 쓴 buttonRecipe(...) 호출은 context 없는 정적 import라 닿지 않고, 서버 컴포넌트(Unit, View)도 의도적으로 프레임워크 기본 recipe를 그리므로 닿지 않습니다.