스타일과 UI
Cake20에 이미 설치된 Cake20 UI와 Tailwind CSS로 빠르게 개발합니다.
Cake20 UI
- Cake20 UI와 Tailwind CSS는 Cake20에 이미 설치·설정되어 있습니다. 별도 설치나 초기 설정 없이 기본 기능으로 사용합니다.
- 화면을 직접 구현하거나 UI 패키지를 추가하기 전에 Cake20 UI에 이미 있는 컴포넌트를 먼저 찾아 조합합니다. Form, Table, Modal, Slideover, NavigationMenu와 Page 계열도 바로 사용할 수 있습니다.
- AI도 Cake20 UI 전체 컴포넌트를 이미 제공되는 기능으로 간주하고, 고급 UI를 요청받으면 기존 컴포넌트 조합을 우선해 빠르게 구현합니다.
- Lucide 아이콘도 기본 설치되어 있습니다. Icon의 name이나 Button 같은 컴포넌트의 icon prop에 i-lucide-user 형식으로 바로 사용합니다.
- Icon 컴포넌트는 U 접두어 없이 작성하지만 아이콘 이름의 i-lucide- 접두어는 유지합니다. 다른 Iconify 컬렉션은 필요할 때만 의존성을 추가합니다.
- Card, Button, Input처럼 U 접두어 없는 PascalCase 태그로 Cake20 UI 컴포넌트를 사용합니다.
- Button은 Cake20 UI 컴포넌트고 button은 기본 HTML 요소로 대소문자를 구분합니다.
- Cake20 UI 공식 예제의 UButton, UCard는 Cake20에서 Button, Card로 바꿔 작성합니다. U 접두어 문법은 호환을 위해서만 지원합니다.
- 사용자 컴포넌트는 MyButton처럼 Cake20 UI 예약 이름과 겹치지 않게 작성합니다.
export const data = { name: "" };
export const save = () => console.log(data.name);
export default () => (
<Card>
<Input bind={data.name} placeholder="이름" />
<Button class="mt-3" onClick={save}>저장</Button>
</Card>
);Cake20 CDN 폰트
Cake20 웹사이트는 Cake20 Fonts 모듈을 따로 설치하지 않고 Cake20 CDN의 버전 고정 CSS를 연결해 웹폰트를 사용합니다. 폰트 파일은 Cake20 CDN에서 제공하므로 Google Fonts 같은 외부 서비스에 직접 요청하지 않습니다.
외부 @import는 app/assets/style.css의 다른 규칙보다 먼저 작성합니다. Cake20이 이를 문서 head의 stylesheet 링크로 옮겨 Preview와 운영 빌드에서 같은 경로를 사용합니다.
- Noto Sans KR은 한글·영문 본문, Noto Serif KR은 제목과 인용문에 적합합니다.
- Lilex는 코드와 숫자 표시에 사용하며 IBM Plex Sans도 /fonts/v1/ibm-plex-sans.css에서 제공합니다.
- Tailwind의 font-sans, font-serif, font-mono 클래스로 등록한 폰트를 화면마다 선택할 수 있습니다.
- 폰트 CSS와 WOFF2는 버전 경로를 유지합니다. 새 버전으로 바꿀 때만 CDN 경로의 v1을 변경합니다.
- 각 폰트의 저작권과 SIL OFL 1.1 전문은 CDN 카탈로그의 라이선스 링크에서 확인할 수 있습니다.
- CDN에 없는 전용 폰트는 public/fonts에 WOFF2를 두고 @font-face로 연결합니다. 해당 폰트의 웹 배포 라이선스는 사이트 소유자가 확인합니다.
@import url("https://cdn.cake20.com/fonts/v1/noto-sans-kr.css");
@import url("https://cdn.cake20.com/fonts/v1/noto-serif-kr.css");
@import url("https://cdn.cake20.com/fonts/v1/lilex.css");
@theme {
--font-sans: "Noto Sans KR Variable", sans-serif;
--font-serif: "Noto Serif KR Variable", serif;
--font-mono: "Lilex Variable", monospace;
}
body {
font-family: var(--font-sans);
}폰트 이름만 font-family에 작성하면 파일이 자동으로 생기는 Cake20 Fonts 모듈 방식과 다릅니다. Cake20에서는 사용할 CDN CSS를 명시적으로 연결합니다.
드래그 정렬 DragList
- DragList는 Cake20에 포함된 드래그 정렬 컴포넌트입니다. 별도 설치나 import 없이 사용합니다.
- 같은 group을 지정한 목록 사이에서 항목을 이동할 수 있어 칸반, 순서 편집, 갤러리에 적합합니다.
- 드래그 중인 항목과 놓일 자리를 기본 스타일로 표시하고, 주변 항목의 이동과 터치 입력도 지원합니다.
- 드래그 항목이 가까워질수록 주변 항목이 먼저 밀려나 자리를 비우며, 놓기 전에도 이동 방향을 확인할 수 있습니다.
- 마우스로 잡은 항목은 이동 임계값 없이 커서를 즉시 따라가며, 터치 입력에서만 오작동 방지 지연을 사용합니다.
- animation은 이동 시간이며 기본값은 380ms입니다. easing으로 최종 자리 이동의 움직임을 조절할 수 있습니다.
- swapThreshold와 invertedSwapThreshold는 0~1 범위에서 교체 반응 영역을 조절하며 기본값은 1입니다.
- handle을 생략하면 항목 전체를 잡아 이동합니다. fallbackTolerance 기본값은 0이라 마우스 이동을 즉시 따라갑니다.
- 각 항목에는 변하지 않는 고유한 key를 지정하고 onEnd에서 변경된 순서를 저장합니다.
- handle을 지정하면 카드 전체가 아니라 지정한 손잡이에서만 드래그를 시작합니다.
export const data = {
cards: [
{ id: 1, title: "디자인" },
{ id: 2, title: "검토" },
],
};
export const saveOrder = () => console.log(data.cards);
export default () => (
<DragList bind={data.cards} group="tasks" handle=".drag-handle" onEnd={saveOrder}>
{data.cards.map((card) => (
<article key={card.id}>
<button class="drag-handle">이동</button>
{card.title}
</article>
))}
</DragList>
);웹사이트 설정
- package.json은 유효한 JSON 객체로 작성하며 Cake20 설정과 의존성의 단일 원본입니다.
- package.json에 작성한 공개 설정은 별도 선언이나 import 없이 화면과 서버에서 project.title, project.pageSize처럼 접근합니다. 실제 속성을 기준으로 자동완성과 타입을 생성합니다.
- 브라우저의 project는 Cake20 View 반응형 객체입니다. project.pageSize = 10처럼 속성이나 중첩 값을 직접 바꾸면 이를 사용하는 화면과 document.title이 즉시 갱신됩니다. 구조 분해한 원시 값은 반응형 연결이 끊기므로 project 속성으로 직접 접근합니다.
- 브라우저에서 바꾼 project 값은 현재 페이지 메모리에만 유지되고 package.json에 저장되지는 않습니다. 새로고침 후에도 유지할 기본값은 package.json을 수정해 저장합니다. 서버의 project는 시작 설정이므로 읽기 전용으로 사용합니다.
- 기본 템플릿의 공지 화면과 API는 pageSize를 함께 사용합니다. 값을 바꾸면 한 페이지의 공지 개수가 양쪽에 동일하게 적용됩니다.
- project 값은 브라우저에도 포함되는 공개 설정입니다. 비밀번호와 API 키는 작성하지 않고 웹사이트 설정의 암호화 Secret에 저장합니다.
- mode는 auto, static, fullstack 중 하나입니다. 생략하거나 빈 값이면 auto이지만, 자체 API, 파일 저장소, Task, Queue, WebSocket, SSE, DB 또는 그 밖의 server 실행 코드가 없으면 static을 명시합니다.
- AI가 웹사이트를 생성하거나 수정할 때도 서버 기능이 없음을 확인하면 mode: "static"을 설정합니다. 서버 기능을 추가하면 fullstack으로 변경합니다. static은 별도 웹사이트 서버 프로세스를 실행하지 않습니다.
- data는 auto, local, server 중 하나를 package.json에 작성합니다. 생략하면 내부 project.data 기본값은 auto입니다.
- auto는 Cake20 서버에서 server, 로컬 CLI에서 local로 동작합니다. 명시한 local 또는 server 값은 실행 환경과 관계없이 그대로 적용합니다.
- name, private, type과 packageManager는 Cake20이 관리하며 package.json에 표시합니다. data도 같은 파일에 저장하고 dependencies는 비어 있으면 생략합니다.
- 기존 project.ts, website.ts, config.ts와 package.ts는 설정을 package.json에 병합한 뒤 저장이 성공한 경우에만 삭제합니다.
- timezone은 선택값이며 Asia/Seoul 같은 IANA 시간대를 사용합니다. 설정하면 Node 프로세스와 PostgreSQL 연결을 포함한 웹사이트 실행, 데이터베이스, 예약 작업과 로그에 공통 적용됩니다. 생략해도 시스템 로컬 시간대를 양쪽에 동일하게 적용합니다.
- device는 주대상 화면이며 desktop, tablet, mobile 중 하나를 사용합니다. 생략하면 desktop입니다. 에디터의 해상도 메뉴와 배포한 템플릿의 미리보기는 이 기기를 먼저 선택합니다.
- orientation은 landscape 또는 portrait입니다. 생략하면 desktop은 landscape, tablet과 mobile은 portrait를 사용합니다.
- title은 필수 1~200자 문자열입니다. 브라우저 탭, HTML title, Open Graph와 X Card 제목에 사용합니다.
- description은 선택 1~500자 문자열입니다. 검색 결과 요약과 링크 공유 설명에 사용하는 전역 기본값입니다.
- image는 선택 1~2,000자 문자열입니다. /로 시작하는 public 경로나 http(s) URL을 입력합니다.
- lang은 HTML 문서의 언어이며 ko 또는 ko-KR 형식으로 입력합니다.
- favicon은 브라우저 아이콘이며 /로 시작하는 public 경로나 http(s) URL을 입력합니다.
- public/social.jpg를 두고 image를 /social.jpg로 지정하면 Open Graph와 X Card의 대표 이미지로 사용합니다.
- social.jpg는 기본 제공하지 않습니다. 링크 공유 이미지가 필요할 때 사이트를 잘 나타내는 이미지를 직접 추가합니다.
- image가 있으면 X Card는 summary_large_image, 없으면 summary로 생성됩니다. 대표 이미지는 필수 SEO 요소가 아닙니다.
- 여러 페이지의 검색 최적화에는 각 페이지에 맞는 고유한 제목과 설명이 필요합니다.
- 새 웹사이트는 Cake20을 기본 title로 사용합니다.
- robots.txt와 sitemap.xml은 package.json이 아닌 public에서 별도로 관리합니다.
- app/index.html은 Cake20이 관리하며 에디터와 AI 도구에서 숨깁니다.
// package.json
{
"title": "우리 동네 케이크 가게",
"description": "매일 아침 직접 구운 케이크를 소개합니다.",
"mode": "static",
"data": "auto",
"timezone": "Asia/Seoul",
"device": "mobile",
"orientation": "portrait",
"image": "/social.jpg",
"lang": "ko",
"favicon": "/favicon.svg",
"pageSize": 5
}
// public/social.jpg → /social.jpg전역 CSS
웹사이트 전체 스타일은 app/assets/style.css에 작성합니다. assets의 이미지와 폰트는 Cake20 View나 CSS에서 import하면 Vite가 빌드하며, public 파일은 / 경로로 직접 참조할 수 있습니다.
Tailwind 기본 utility를 우선 사용하고, 반복되는 스타일은 Cake20 View 컴포넌트로 묶는 것이 좋습니다.
긴 Tailwind class 정리
한 Cake20 View 컴포넌트 안에서만 사용하는 긴 class는 짧은 문자열이나 배열 변수로 옮기고 class에 연결할 수 있습니다. 여러 화면에서 반복되거나 hero-fade처럼 디자인 역할이 분명한 조합은 app/assets/style.css의 @utility로 선언합니다.
- Cake20 Runtime이 Tailwind CSS를 먼저 불러오므로 style.css에서 @import "tailwindcss"를 다시 작성하거나 패키지를 설치하지 않습니다.
- 한 번만 쓰는 짧은 조합은 JSX에 그대로 두고, class가 길어 읽기 어렵거나 같은 조합이 반복될 때만 변수나 @utility로 이름을 붙입니다.
- Tailwind가 빌드할 때 찾을 수 있도록 class 이름은 완성된 문자열로 작성합니다. bg-${color}-500 같은 동적 조합 대신 완성된 문자열 map을 사용합니다.
- @utility는 app/assets/style.css 최상위에 선언하고, Tailwind 기본 utility 조합은 @apply로 재사용합니다.
// app/pages/index.tsx
const fade = [
"absolute inset-0 bg-gradient-to-b",
"from-orange-200/70 via-amber-100/40 to-transparent"
];
export default () => (
<>
<div class={fade} />
<div class="hero-fade" />
</>
);
/* app/assets/style.css */
@utility hero-fade {
@apply absolute inset-0 bg-gradient-to-b from-orange-200/70
via-amber-100/40 to-transparent;
}