Akan.js
Docs
문서컨벤션레퍼런스Cheatsheet
Akan.js
문서컨벤션레퍼런스Cheatsheet
Akan.js

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

Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
소개▾
시작하기기본 개념실습하기
튜토리얼▾
상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
핵심 개념▾
폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
시스템 아키텍처▾
아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
소개▾
시작하기기본 개념실습하기
튜토리얼▾
상세하게 보여주기상태 변경하기서비스 내에서 상호작용슬라이스로 표시하기페이지를 통한 UX스칼라 사용하기인사이트 사용하기데이터 연결하기
핵심 개념▾
폴더 규칙파일 규칙파일 기반 라우팅데이터 레이어앱 설정Akan 런타임다중 클라이언트
시스템 아키텍처▾
아키텍처 개요UI 아키텍처UI 구성비즈니스 서비스인페이지 에이전트CSS와 스타일링UI 레시피 레이어모바일 앱 아키텍처런타임과 인프라
이전CSS와 스타일링다음모바일 앱 아키텍처

레시피 레이어

primary 버튼 하나에는 클래스가 열 개쯤 필요합니다. 버튼마다 그 열 개를 적으면 조금씩 어긋나고, 모양을 바꾸려면 모든 사본을 고쳐야 합니다. recipe는 그 모양에 한 번 이름을 붙이고, 모든 버튼이 그 이름으로 가져다 쓰게 합니다.
기술적으로 recipe는 tailwind-variants 기반의 변형 팩토리이고, 토큰 계층과 컴포넌트 사이에 있습니다. 계층마다 질문 하나에 답하고, 바로 아래 계층만 압니다:
계층하는 일
tokens
styles.css의 CSS 변수입니다. 테마를 따르고 서버/클라이언트를 가리지 않습니다. 이 색은 무엇인가?
recipes
토큰을 조합하는 변형 팩토리입니다. 서버에서도 안전하고 use client가 없습니다. 어떻게 보이는가?
components
recipe를 쓰고 상호작용과 상태를 더합니다. use client는 필요할 때만. 어떻게 동작하는가?
이 페이지에서 쓰는 말
용어설명
recipe
모양 하나의 클래스 문자열을 돌려주는 함수입니다. recipe(tv({ … }))로 만듭니다.
variant
recipe의 이름 붙은 축 하나(variant, size, side)와 그 축이 가질 수 있는 값들입니다.
semantic token
값이 아니라 역할로 이름 붙인 색입니다. primary, background, destructive 같은 이름입니다.
slot
라우트의 _overrides.tsx가 recipe를 갈아 끼울 수 있는 이름 붙은 자리(button, badge, input)입니다.
recipe가 양쪽에서 동작하는 이유
recipe 하나를 양쪽에서 호출
buttonRecipe에는 use client가 없으므로 서버 컴포넌트와 클라이언트 컴포넌트가 모두 호출해 같은 클래스 문자열을 받습니다.
recipe 모듈에는 절대 "use client"를 붙이지 않습니다. className 문자열을 돌려주는 순수 함수라서 서버 컴포넌트와 클라이언트 컴포넌트 모두 호출할 수 있습니다. 서버 페이지도 raw <Link>나 <div>를 buttonRecipe()로 바로 꾸밀 수 있습니다.

프레임워크 레시피

akanjs/ui는 서버에서도 안전한 모듈에서 buttonRecipe, badgeRecipe, inputRecipe를 제공하고, Button, Badge, Input 컴포넌트도 안에서 같은 recipe를 씁니다. 그래서 recipe로 꾸민 raw 엘리먼트는 컴포넌트와 똑같이 보입니다.
첫 번째 인자로 variant를, 두 번째 인자로 추가 클래스를 넘깁니다. recipe가 tailwind-merge로 알아서 합쳐 주므로 cn()으로 감쌀 필요가 없습니다:
프레임워크 recipe가 받는 값
recipe축과 값
buttonRecipe
variantdefault · primary · secondary · accent · neutral · outline · ghost · destructive · success · warning · info · link
sizexs · sm · md · lg · icon
shapedefault · square · circle
outlinetrue
badgeRecipe
variantdefault · primary · secondary · accent · neutral · success · warning · info · error · outline
sizexs · sm · md · lg
outlinetrue
inputRecipe
kindfield · area
sizexs · sm · md · lg · xl
tonedefault · primary · error
굵은 값이 기본값입니다. outline은 플래그로, variant의 색을 유지한 채 외곽선으로 그립니다.
모든 variant 클래스는 시맨틱 토큰(bg-primary, text-success-foreground …)이라서, 어떤 값이든 자동으로 테마를 따라갑니다.
recipe를 별도 폴더(akanjs/ui/recipe/, 파일당 recipe 하나)에 두는 이유가 바로 client-only가 되지 않게 하려는 것입니다. 'use client' 컴포넌트 파일에서 export하면 서버 컴포넌트에서 호출할 때 'client-only export' 에러가 납니다. 폴더를 분리하면 그 경계가 사라집니다.

앱 레벨 레시피

앱에는 프레임워크가 모르는 반복 표면이 있습니다. 그라디언트 히어로, 아이콘 타일, 챗 버블 같은 것입니다. 같은 클래스 문자열을 곳곳에 인라인하지 말고, 각각에 앱 recipe를 만드세요:
  1. 앱의 ui/Recipe/ 아래에 recipe마다 파일 하나를 추가합니다. 앱 ui 폴더는 PascalCase라서, 프레임워크의 소문자 ui/recipe/와 달리 ui/Recipe/입니다.
  2. 팩토리는 recipe(tv({ base, variants }))로 만듭니다. recipe와 tv 모두 akanjs/ui에서 re-export됩니다.
  3. 이름은 <name>Recipe로 짓고, 파일에 'use client'를 넣지 않습니다.
  4. ui 배럴에서 import합니다. 폴더의 index.ts가 프레임워크 recipe도 재수출하므로 import 경로 하나로 둘 다 씁니다.
apps/myapp/ui/Recipe/chatBubble.ts
그러면 페이지는 클래스 문자열 반복을 멈추고 데이터에서 variant를 읽습니다:
apps/myapp/page/(home)/inbox/chat.tsx
호출도 프레임워크 recipe와 같습니다. xRecipe(변형, className?) — 두 번째 인자는 내부에서 병합되므로 cn()이 필요 없습니다.

언제 레시피를 쓸까

클래스 묶음이 재사용되거나, 조건에 따라 조합되거나, 서버 컴포넌트에서 필요할 때 recipe가 제값을 합니다. 일회성 클래스는 인라인으로 두세요. 내 경우를 왼쪽 열에서 찾아보세요:
상황
새 recipe
variant
인라인
안 그러면 반복하게 될 클래스 묶음
반복되거나 variant 같은 표면
✓
상태 pill, 히어로, 버블, 타일 같은 것입니다. recipe로 뽑아냅니다.
데이터로 고정된 집합에서 고르는 클래스
✓
tone, size, side, status가 클래스를 정합니다. recipe의 variant로 만듭니다.
서버 컴포넌트나 raw 엘리먼트 꾸미기
✓
recipe는 서버에서도 안전하므로 서버 페이지가 직접 호출할 수 있습니다.
한 번만 쓰는 클래스
정말 일회성인 className
✓
인라인으로 둡니다. 과하게 추상화하지 마세요.
✓이것을 씁니다이것이 아닙니다

레시피 오버라이드 — 재구현 없이 리스킨

앱의 한 구역만 모양이 달라야 할 때가 있습니다. 네온 스타일의 관리자 구역처럼요. 그래도 컴포넌트의 동작은 그대로여야 합니다. recipe override는 모양만 바꾸고 나머지는 건드리지 않습니다.
라우트의 _overrides.tsx는 recipe 슬롯(button, badge, input)을 교체할 수 있습니다. 그 recipe를 쓰는 프레임워크 클라이언트 컴포넌트는 라우트 서브트리 전체에서 새 모양이 되지만, 동작(async 상태, 포커스 트랩, a11y)은 프레임워크 그대로입니다. 바뀌는 것은 className 팩토리뿐입니다.
먼저, 교체할 recipe와 같은 variant 표면을 가진 recipe를 씁니다:
apps/myapp/ui/Recipe/neonButton.ts
그다음 그 구역의 _overrides.tsx에서 슬롯에 연결합니다. 그 아래 화면 코드는 한 줄도 바뀌지 않습니다:
apps/myapp/page/(section)/_overrides.tsx
교체가 닿는 곳
호출하는 쪽
새 모양
원래 모양
라우트 서브트리 안에서
프레임워크 클라이언트 컴포넌트
✓
Button, Badge, Input, Dropdown, Pagination …은 슬롯을 읽으므로 새 모양으로 바뀝니다.
서버 컴포넌트(Unit, View)
✓
canonical recipe를 그대로 씁니다.
buttonRecipe(...)
✓
내 JSX 안의 직접 호출은 canonical recipe를 씁니다. 거기서는 내 recipe를 직접 import합니다.
✓이 recipe를 씀이쪽이 아님
교체 recipe는 프레임워크 recipe의 variant 표면을 전부 받아야 모든 호출부가 계속 동작합니다. 교체는 슬롯을 읽는 컴포넌트에만 닿으므로, buttonRecipe(...)를 직접 부르는 곳에서는 내 recipe를 직접 import하세요.

세 질문, 하나의 불변

커스터마이징은 화면마다가 아니라 디자인 시스템을 셋업할 때 한 번 결정합니다. 디자인 스펙을 /lab 카탈로그와 한 번 대조한 뒤, 다른 점마다 아래 질문에 넣어 보세요.
Q1. 테마가 다른가?
색 · 각도 · 폰트
예 → 앱의 page/styles.css에서 토큰 값을 override합니다.
아니오 → akan 기본값을 씁니다.
Q2. 컴포넌트의 look이 다른가?
구조는 같고 겉모습만 다름
예 → 앱 recipe를 쓰고 _overrides.tsx의 recipes로 주입합니다.
아니오 → 그대로 씁니다.
Q3. 구조나 동작이 다른가?
예: 모달 → 드로어
예 → headless 부품을 다시 조립하는 component override를 씁니다.
아니오 → 필요 없습니다.
lib에 없는 표면인가?
챗 버블, 타일
예 → 앱 recipe를 새로 추가합니다. lib에 대응물이 없는 확장이라 충돌하지 않습니다.
아니오 → lib의 recipe를 씁니다.

이 페이지

레시피 레이어
프레임워크 레시피
앱 레벨 레시피
언제 레시피를 쓸까
레시피 오버라이드
커스터마이징 결정
변하지 않는 것: 답이 무엇이든 화면 코드, 즉 그냥 <Button>은 바뀌지 않습니다. 바뀌는 것은 설정 파일뿐입니다.
앱 recipe는 확장입니다. lib에 없는 표면을 더할 뿐, lib 컴포넌트를 병행해서 다시 정의하지 않습니다. lib 컴포넌트의 look을 바꾸려면 병행 버튼 recipe가 아니라 recipe override를 쓰세요. 같은 className 조정이 반복되면 recipe override(앱 전역)나 variant로 승격하세요.