서버리스 환경에서 풀스택 앱을 만들어보고 싶었다. AWS Lambda는 설정이 번거롭고, Vercel은 DB 연동이 따로 필요하고… 그러다가 Cloudflare Workers + D1 조합을 발견했다. Workers에서 바로 SQL 쿼리를 날릴 수 있고, 배포도 wrangler deploy 한 줄이면 끝이다. 여기에 OpenAI API를 연동해서 할 일을 자동으로 분류해주는 기능까지 넣어봤다.
프로젝트 초기화
먼저 Wrangler CLI가 필요하다:
npm install -g wrangler
wrangler login
프로젝트를 생성한다:
npm create cloudflare@latest ai-todo -- --type=hello-world
cd ai-todo
D1 데이터베이스 생성
Cloudflare D1은 SQLite 기반의 서버리스 데이터베이스다. 콘솔에서 만들 수도 있지만 CLI가 더 편하다:
wrangler d1 create ai-todo-db
실행하면 database_id가 출력된다. 이걸 wrangler.toml에 넣어줘야 한다:
name = "ai-todo"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "ai-todo-db"
database_id = "여기에-출력된-ID"
테이블을 만들자. schema.sql 파일을 작성한다:
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
category TEXT DEFAULT 'uncategorized',
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);
wrangler d1 execute ai-todo-db --file=./schema.sql
Worker 코드 작성
src/index.ts를 작성한다. 일단 기본적인 CRUD부터:
export interface Env {
DB: D1Database;
OPENAI_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// CORS 헤더
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
if (path === '/api/todos' && request.method === 'GET') {
const results = await env.DB.prepare(
'SELECT * FROM todos ORDER BY created_at DESC'
).all();
return Response.json(results.results, { headers: corsHeaders });
}
if (path === '/api/todos' && request.method === 'POST') {
const body = await request.json() as { content: string };
// AI로 카테고리 자동 분류
const category = await classifyTodo(body.content, env.OPENAI_API_KEY);
const result = await env.DB.prepare(
'INSERT INTO todos (content, category) VALUES (?, ?)'
).bind(body.content, category).run();
return Response.json(
{ id: result.meta.last_row_id, content: body.content, category },
{ headers: corsHeaders }
);
}
if (path.startsWith('/api/todos/') && request.method === 'PUT') {
const id = path.split('/').pop();
const body = await request.json() as { completed: number };
await env.DB.prepare(
'UPDATE todos SET completed = ? WHERE id = ?'
).bind(body.completed, id).run();
return Response.json({ success: true }, { headers: corsHeaders });
}
if (path.startsWith('/api/todos/') && request.method === 'DELETE') {
const id = path.split('/').pop();
await env.DB.prepare('DELETE FROM todos WHERE id = ?').bind(id).run();
return Response.json({ success: true }, { headers: corsHeaders });
}
return new Response('Not Found', { status: 404 });
} catch (error) {
return Response.json(
{ error: 'Internal Server Error' },
{ status: 500, headers: corsHeaders }
);
}
},
};
AI 카테고리 분류 함수
OpenAI API를 호출해서 할 일의 카테고리를 자동으로 분류한다:
async function classifyTodo(content: string, apiKey: string): Promise<string> {
const categories = ['업무', '개인', '공부', '운동', '쇼핑', '기타'];
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content: `다음 할 일을 카테고리로 분류해주세요. 카테고리: ${categories.join(', ')}. 카테고리 이름만 답해주세요.`,
},
{ role: 'user', content },
],
max_tokens: 10,
temperature: 0,
}),
});
const data = await response.json() as any;
const result = data.choices[0].message.content.trim();
return categories.includes(result) ? result : '기타';
} catch {
return '기타';
}
}
API 키는 시크릿으로 관리한다:
wrangler secret put OPENAI_API_KEY
프론트엔드
간단하게 static 폴더에 HTML을 하나 만들었다. Workers에서 정적 파일도 서빙할 수 있지만, 나는 프론트엔드를 Cloudflare Pages에 따로 올리는 방식을 선택했다. API만 Workers에서 처리하고, 프론트는 분리하면 나중에 React나 Vue로 갈아끼우기도 쉽다.
핵심 로직만 보면:
async function addTodo() {
const input = document.getElementById('todo-input');
const content = input.value.trim();
if (!content) return;
const res = await fetch(`${API_URL}/api/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
});
const todo = await res.json();
renderTodo(todo);
input.value = '';
}
카테고리별로 색상을 다르게 표시해서 한눈에 구분이 되게 했다. 업무는 파란색, 개인은 초록색, 공부는 보라색 이런 식으로.
배포
배포는 정말 간단하다:
wrangler deploy
이 한 줄이면 Workers에 배포가 완료된다. D1 데이터베이스도 자동으로 연결되고, 시크릿도 이미 설정해뒀으니 바로 동작한다.
느낀 점
Cloudflare Workers + D1 조합은 소규모 프로젝트에 정말 좋다. 무료 티어가 넉넉해서 개인 프로젝트로는 비용이 거의 안 든다. Workers는 하루 10만 요청, D1은 5GB 저장공간이 무료다. AI 기능을 넣으면 OpenAI API 비용이 들긴 하지만, gpt-3.5-turbo로 짧은 분류 작업만 하면 거의 무시할 수준이다.
다만 D1이 아직 베타인 점은 감안해야 한다. 프로덕션 서비스에 쓰기엔 좀 이르고, 개인 프로젝트나 프로토타입용으로 적합하다고 본다. SQLite 기반이라 복잡한 쿼리나 대용량 데이터 처리에는 한계가 있다.
전체적으로 서버리스 풀스택 개발 경험이 상당히 매끄러웠다. Wrangler CLI 하나로 DB 생성부터 배포까지 다 되니까 DevOps에 시간 쓸 필요가 없었다. 다음에는 Durable Objects를 써서 실시간 동기화 기능도 넣어볼 생각이다.