기술별 실행 예제
Cake20이 기본 제공하는 기능을 짧은 소스와 실제 실행 결과로 확인합니다.
Cake20 View 페이지와 컴포넌트
// app/components/GreetingCard.tsx
export type Props = { name: string };
export default ({ name }: Props) => <Card>안녕하세요, {name}님</Card>;
// app/pages/index.tsx
export default () => <GreetingCard name="카페" />;실행 결과
홈 화면에 ‘안녕하세요, 카페님’ 카드가 표시됩니다. 컴포넌트 import는 필요 없습니다.
Cake20 UI와 Tailwind CSS
export default () => (
<Button icon="i-lucide-save" class="mt-4" onClick={save}>저장</Button>
);실행 결과
Lucide 저장 아이콘이 있는 Cake20 UI 버튼이 표시됩니다. U 접두어와 패키지 설치가 필요 없습니다.
WYSIWYG Editor
export const data = {
content: "<h2>오늘의 기록</h2><p>내용을 적어 보세요.</p>",
};
export default () => (
<Editor bind={data.content} image={false} mention={false} />
);실행 결과
Tiptap 기반 서식 편집기가 표시되며 HTML 문자열과 양방향으로 동기화됩니다.
DragList 드래그 정렬
export default () => (
<DragList bind={data.cards} group="tasks" onEnd={saveOrder}>
{data.cards.map((card) => <Card key={card.id}>{card.title}</Card>)}
</DragList>
);실행 결과
카드 전체를 잡아 순서를 바꾸거나 같은 group의 다른 목록으로 옮길 수 있습니다.
웹사이트 공개 설정
// package.json
{
"title": "동네 카페",
"description": "오늘의 메뉴를 소개합니다.",
"timezone": "Asia/Seoul",
"lang": "ko"
}실행 결과
문서 제목과 공개 메타 정보가 적용되고 서버 시간 계산은 서울 기준을 사용합니다.
반응형 Store
// app/stores/cart.store.ts
export const config = { persist: "local" } as const;
export default {
count: 0,
add() { this.count += 1; }
};
// app/pages/index.tsx
export default () => (
<Button onClick={() => store.cart.add()}>{store.cart.count}</Button>
);실행 결과
버튼을 누르면 숫자가 증가하며 브라우저를 다시 열어도 localStorage에서 복구됩니다.
하위 컴포넌트에 Context 제공
// app/components/ThemePanel.tsx
export const data = { theme: "dark" as "light" | "dark" };
export const context = { theme: () => data.theme };
export default () => <ThemeLabel />;
// app/components/ThemeLabel.tsx
const theme = useContext<"light" | "dark">("theme");
export default () => <p>현재 테마: {theme()}</p>;실행 결과
하위 컴포넌트가 prop 전달 없이 가장 가까운 상위 context 값을 사용합니다. 특정 UI 계층의 문맥은 context, 전역·영속 상태는 store에 둡니다.
API와 입력 검증
// server/api/memos.post.ts
export const config = {
filter: { title: z.string().trim().min(1).max(80) },
sample: { title: "첫 메모" }
};
export default async (data: Input<typeof config.filter>) =>
db.memo.create({ data });실행 결과
POST /api/memos가 생성되고 잘못된 입력은 DB 실행 전에 400 응답으로 차단됩니다.
문의 폼 보관함
// server/api/contact.post.ts
export default defineApi(async (input, { request }) =>
form.submit(request, {
name: "contact",
consent: input.consent === true,
data: { email: input.email, message: input.message }
})
);실행 결과
문의가 웹사이트 전용 보관함에 저장되고 속도 제한·용량 제한·동의 정보가 함께 적용됩니다.
Prisma 데이터베이스 모델
// server/db/memo.db.ts
export const Memo = {
id: z.id(),
title: z.string().max(80),
createdAt: z.date().defaultNow().timestamp()
};실행 결과
Memo 테이블과 Prisma 접근 객체 db.memo가 생성됩니다. 테이블마다 db.ts 파일 하나를 사용합니다.
초기 데이터와 Preview 샘플
// server/db/memo.db.ts
export const Memo = {
id: z.string().id().max(30),
title: z.string().max(100)
};
export const seed = async () => [
{ id: "first", title: "첫 메모" },
{ id: "shopping", title: "장보기" }
];실행 결과
현재 반환 row로 Preview JSON을 만들고, 신규·변경된 seed만 운영 DB에 upsert합니다.
재사용 SQL 함수
// server/db/memo.sql.ts
export const recentMemos = db.sql(
z.object({ id: z.string(), title: z.string() }),
() => sql`SELECT id, title FROM "Memo" ORDER BY "createdAt" DESC LIMIT 5`
);
const rows = await db.sql.recentMemos();실행 결과
검증된 최근 메모 배열이 반환되며 API·Hook·Task에서 같은 함수를 재사용할 수 있습니다.
로그인과 권한
// server/api/me.get.ts
export default async (_data: unknown, context: ApiContext) => {
const user = await auth.require(context.request, { roles: ["admin"] });
return { email: user.email };
};실행 결과
관리자는 이메일을 받고, 미로그인은 401, 권한이 없는 사용자는 403 응답을 받습니다.
Route, WebSocket과 SSE
// server/routes/notice.sse.ts
export default () => ({
event: "notice",
data: { text: "주문이 준비됐습니다." }
});
// server/routes/chat.ws.ts
export default { onMessage(peer, message) { peer.publish("room", message); } };실행 결과
브라우저는 /notice에서 서버 알림을 받고 /chat에서 같은 방의 메시지를 실시간 수신합니다.
Hook, Task, Job과 스케줄
// server/hooks/order.hook.ts
export default (order: Order) => console.log(order.id);
// server/tasks/cleanup.task.ts
export default async () => ({ removed: await db.memo.deleteMany() });
// server/tasks/daily.job.ts
export const config = { cron: "0 0 * * *" };
export default async () => db.report.create({ data: { day: textDate(now()) } });실행 결과
이벤트 Hook, 수동 Task와 매일 자정 Job이 서로 독립된 파일과 실행 이력으로 관리됩니다.
Redis 기반 비동기 Job
// server/api/report.post.ts
const work = await job.report({ month: "2026-08" });
return { id: work.id };
// 상태 조회
const state = await job.$get(work.id);실행 결과
API는 즉시 작업 ID를 반환하고 서버 Job은 웹사이트 전용 Redis Queue에서 재시도·상태를 관리합니다.
알람, 이메일과 결제
await mail.send({ to: "[email protected]", subject: "가입 완료", text: "환영합니다." });
if (telegram.active) await telegram.send("새 주문이 등록되었습니다.");
if (gmail.active) {
await gmail.send({ subject: "새 주문", text: "새 주문이 등록되었습니다." });
}
const ready = await payment.request({
orderId: "ORDER-1",
orderName: "딸기 케이크",
amount: 12000,
returnUrl: "https://example.com/pay/success",
cancelUrl: "https://example.com/pay/fail"
});실행 결과
사이트 관리자 Telegram·Gmail과 SMTP로 알림을 보내고 결제 요청 정보를 반환합니다.
파일과 Object Storage
const file = await storage.put("images/menu.webp", body);
return { url: storage.url(file.path) };실행 결과
로컬과 Object Storage 설정에 관계없이 공개 가능한 이미지 URL이 반환됩니다.
Excel XLSX 생성
const rows = [["주문"], ["딸기 케이크"]];
return excel.download("orders.xlsx", rows);실행 결과
단순 행 데이터를 storage에 남기지 않고 XLSX 다운로드 Response로 바로 반환합니다.
실행 가능한 테스트
test("메모 목록", async () => {
const data = await fetchGet<{ items: unknown[] }>("/api/memos");
expect(data.items.length).toBeGreaterThan(0);
});실행 결과
검수 빌드에서 API를 실제로 호출하고 통과·실패 결과가 에디터 테스트 콘솔에 표시됩니다.