페이지와 레이아웃
Cake20의 파일 기반 라우팅으로 Cake20 View 화면을 구성합니다.
페이지 URL
- app/pages/index.tsx → /
- app/pages/about.tsx → /about
- app/pages/users/[id].tsx → /users/:id
- app/pages/docs/[...slug].tsx → /docs/*
- 파일이 많으면 app/pages/admin/users/*.tsx처럼 필요한 깊이의 폴더로 정리합니다.
Cake20 View 문법
Cake20 View는 TypeScript 조건식, 배열 map, 표준 이벤트와 bind로 화면 동작을 표현합니다.
- 스타일 class는 HTML과 동일하게 class로 작성합니다.
- 이벤트는 onClick, onInput처럼 표준 이벤트 이름을 사용합니다.
- 조건은 TypeScript 조건식, 반복은 배열 map을 사용합니다.
- map은 배열에만 사용합니다. 숫자 횟수 반복은 Array.from({ length: count }).map(...)으로 작성합니다.
- 양방향 연결은 bind를 사용합니다.
- 이벤트는 함수 자체를 전달하거나 실제로 호출합니다. 함수 이름만 참조하고 끝나는 콜백을 만들지 않습니다.
export const data = {
open: true,
name: "",
items: ["딸기", "초콜릿"]
};
export default () => (
<main class="space-y-3">
<Input bind={data.name} />
<Button onClick={() => (data.open = !data.open)}>전환</Button>
{data.open && <Panel />}
{data.items.map((item) => <p key={item}>{item}</p>)}
</main>
);저장과 컴파일
저장된 TSX는 @cake20/view가 인스턴스별 반응형 상태와 화면 코드로 컴파일합니다.
- data, computed, watch와 생명주기 export를 예약 문법으로 인식합니다.
- class, html, bind와 이벤트 옵션을 화면 실행 규칙으로 변환합니다.
- 문법 오류가 있으면 저장하거나 빌드하지 않고 정확한 위치를 안내합니다.
Cake20.js 변환 점검
- data와 다른 선언은 초기화되기 전에 참조하지 않습니다.
- state와 element 같은 일반 모듈 API는 문서에 지정된 경로에서 import합니다. 컴포넌트 자동 import와 혼동하지 않습니다.
- API 응답을 data에 반영하기 전에 배열과 객체 형태를 확인합니다. 빈 Preview 응답으로 의미 있는 초기값을 지우지 않습니다.
- Cake20 화면 이동에는 RouterLink를 사용합니다.
- 변환 후 첫 화면뿐 아니라 전체 경로, 주요 버튼과 입력, 양방향 연결, 로그인 복원과 API 오류를 확인합니다.
저장 시 코드 포맷
에디터에서 저장 버튼이나 저장 단축키를 누르면 지원 파일을 Cake20의 내부 Oxfmt 규칙에 맞춰 정리한 뒤 저장하며, 결과를 현재 탭에도 바로 반영합니다.
- TypeScript, TSX, JavaScript, HTML, CSS 계열과 JSON 계열 텍스트 파일을 지원합니다.
- 기존 변경 충돌 검사와 빌드 잠금을 그대로 거치며, 포맷에 실패하면 저장하지 않습니다.
- 웹사이트별 설정 파일은 사용하지 않습니다. 모든 웹사이트에 같은 Cake20 내부 규칙을 적용합니다.
- SVG, 이미지와 Prisma schema 파일은 코드 포맷 대상이 아닙니다.
레이아웃 지정
export const config = {
layout: "admin"
};
export default () => <h1>관리 화면</h1>;app/layouts/admin.tsx가 페이지를 감쌉니다. layout: false로 레이아웃을 끌 수 있습니다.
레이아웃 프리뷰
- 레이아웃 파일을 선택하면 실제 페이지 대신 slot이라고 표시된 가상 박스를 넣습니다.
- 레이아웃에 props가 있으면 sample에 프리뷰 값을 입력합니다.
- sample이 없으면 빈 props로 레이아웃 구조만 표시합니다.
export const sample = {
sidebar: true
};컴포넌트 자동 등록
- app/components의 Cake20 View 파일은 import 없이 페이지, 레이아웃과 다른 컴포넌트에서 사용합니다.
- *.client.ts와 *.client.tsx의 .client는 브라우저 전용 실행환경 표식이며 컴포넌트 이름에 포함하지 않습니다. Chart.client.tsx는 <Chart />로 사용합니다.
- 파일이 많으면 app/components/account/profile/*.tsx처럼 필요한 깊이의 폴더로 정리합니다.
- 그룹 폴더와 파일 이름을 PascalCase로 합쳐 컴포넌트 이름을 만듭니다.
- 같은 자동 등록 이름이 만들어지는 파일은 함께 둘 수 없습니다.
- 사용자 컴포넌트의 이름 있는 양방향 연결은 bindName을 사용하고 대상의 name과 onNameChange 또는 onUpdate:name 계약에 맞춥니다. 한 태그에 여러 bind를 사용할 수 있습니다.
// app/components/account/ProfileCard.tsx
export type Props = { children?: View };
export default (props: Props) => <Card>{props.children}</Card>;
// app/pages/index.tsx
export default () => <AccountProfileCard>프로필</AccountProfileCard>;페이지와 컴포넌트 분리
페이지와 레이아웃은 화면 전체의 흐름, 데이터 연결과 컴포넌트 조합을 담고, 독립적인 화면 영역과 반복되는 UI는 app/components의 Cake20 View 컴포넌트로 분리합니다.
- 여러 섹션, 카드, 목록, 폼, 모달처럼 역할이 구분되면 한 페이지 파일에 마크업과 로직을 모두 쌓지 말고 책임별 컴포넌트로 나눕니다.
- 같은 UI가 두 번 이상 반복되거나 자체 props, 상태 또는 이벤트를 가지면 컴포넌트로 분리하는 것을 기본으로 합니다.
- 페이지 파일은 라우팅과 화면 조합이 한눈에 보일 정도로 유지합니다. 단순한 태그 몇 개까지 의미 없이 잘게 나눌 필요는 없습니다.
- 화면 상태와 통신 로직이 길어지면 기본적으로 app/stores/<name>.store.ts로 옮기고, UI와 서버가 함께 쓰는 순수 함수와 타입은 shared/utils와 shared/types로 분리합니다.
- 재사용 화면 로직은 app/composables와 그 하위 폴더에서 자동 import할 수 있지만 Cake20 공식 템플릿은 store를 기본으로 합니다.
- 기존 파일이 이미 여러 책임을 가진 상태라면 기능을 더 쌓기 전에 관련 영역을 컴포넌트·store·공통 타입으로 먼저 정리합니다.
- 템플릿도 일회용 데모가 아니라 사용자가 내려받아 학습하고 AI가 이어서 수정하는 운영 소스로 간주하며 같은 분리 기준을 적용합니다.
- API 파일은 입력 검증, 권한 확인과 응답 연결에 집중하고 여러 API가 공유하는 순수 함수와 타입은 shared/utils와 shared/types로 분리합니다.
- 정적 HTML이 여러 화면 영역과 상호작용을 포함할 만큼 커지면 Cake20 View 페이지와 컴포넌트로 옮기고, HTML 파일은 작고 독립적인 정적 문서에 사용합니다.
// app/pages/orders.tsx: 데이터 연결과 화면 조합
export const data = { orders: [] as Order[] };
export const onServer = async () => {
data.orders = await fetchGet<Order[]>("/api/orders");
};
export default () => (
<>
<OrderSummary orders={data.orders} />
<OrderTable orders={data.orders} />
<OrderForm onSaved={refresh} />
</>
);
// app/components/OrderTable.tsx: 목록 표시만 담당
export type Props = { orders: Order[] };
export default (props: Props) => <Table data={props.orders} />;실행 결과
수정할 기능의 파일을 이름만으로 찾을 수 있고, AI가 관련 없는 화면을 읽거나 바꿀 필요가 줄어듭니다.
하위 컴포넌트에 Context 제공
- 상위 컴포넌트의 export const context는 하위 컴포넌트에 문맥을 제공합니다.
- 하위 컴포넌트는 useContext<Value>(name)로 가장 가까운 context 값을 찾습니다.
- useContext()는 getter를 반환하므로 theme()처럼 호출합니다. 변경되는 값은 () => data.theme 형태로 공유하면 반응형 갱신을 유지합니다.
- 중간 컴포넌트가 같은 이름의 context를 제공하면 그 아래에서는 더 가까운 값을 사용합니다.
- 직접 연결된 부모와 자식은 props를 사용하고, 특정 UI 계층의 Form, Tabs, Editor와 테마 문맥은 context를 사용합니다.
- 페이지와 무관한 전역 상태, 서로 다른 화면 영역의 공유 상태와 영속 상태는 store를 사용합니다.
- context는 컴포넌트 계층의 메모리에만 유지되므로 새로고침하거나 제공하는 컴포넌트가 종료되면 사라집니다.
// app/components/ThemePanel.tsx
export const data = {
theme: "dark" as "light" | "dark"
};
export const context = {
theme: () => data.theme
};
export default () => (
<section>
<Button onClick={() => {
data.theme = data.theme === "dark" ? "light" : "dark";
}}>테마 전환</Button>
<ThemeLabel />
</section>
);
// app/components/ThemeLabel.tsx
const theme = useContext<"light" | "dark">("theme");
export default () => (
<p>{theme() === "dark" ? "어두운 테마" : "밝은 테마"}</p>
);실행 결과
ThemeLabel은 prop을 전달받지 않아도 가장 가까운 ThemePanel의 반응형 테마를 사용합니다.
컴포넌트 프리뷰
- 컴포넌트 파일을 선택하면 페이지와 레이아웃 없이 컴포넌트만 표시합니다.
- export const sample을 컴포넌트 props로 전달합니다.
- 필수 prop이 있으면 sample에도 유효한 테스트값을 입력합니다.
- sample은 프리뷰, 타입 검증과 사용 예제를 함께 제공하는 실행 가능한 문서입니다.
export type Props = {
title: string;
count: number;
};
export const sample = {
title: "오늘 방문자",
count: 1280
} satisfies Props;
export default (props: Props) => (
<Card>{props.title}: {props.count.toLocaleString()}</Card>
);