CAKE20

AI Web PaaS

페이지와 레이아웃

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