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

커스터마이즈

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 } })

동작 방식

파일 두 개면 됩니다. 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에서 가져오는 것
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 />
LoadingSpin
leaf 하나만 연결하면 형제 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는 Input leaf를 그리므로, 그 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를 만듭니다.

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

내 AI에 이 문서 연결하기

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