Fresh 프론트엔드와 Deno.serve API, Deno KV를 결합한 풀스택 할 일 관리 애플리케이션을 구축합니다. 인증, 배포, 모니터링까지 실전 프로젝트의 전체 과정을 다룹니다.
이 시리즈의 마지막 장에서는 지금까지 배운 모든 내용을 종합하여 풀스택 애플리케이션을 구축합니다. Fresh 프론트엔드, Deno.serve 기반 API, Deno KV 데이터베이스를 결합한 할 일 관리 애플리케이션(TaskFlow) 을 만들겠습니다.
인증
- 회원가입 / 로그인
- JWT 기반 세션 관리
- 쿠키 기반 인증 유지
할 일 관리
- 할 일 생성 / 조회 / 수정 / 삭제
- 완료 상태 토글
- 우선순위 설정 (높음, 중간, 낮음)
- 마감일 설정
대시보드
- 통계 (전체, 완료, 미완료)
- 우선순위별 분류taskflow/
components/
TaskCard.tsx # 할 일 카드 (서버 컴포넌트)
Header.tsx # 헤더
StatsBar.tsx # 통계 바
islands/
TaskForm.tsx # 할 일 생성/수정 폼 (인터랙티브)
TaskList.tsx # 할 일 목록 (인터랙티브)
LoginForm.tsx # 로그인 폼 (인터랙티브)
routes/
_app.tsx # 앱 레이아웃
_middleware.ts # 전역 미들웨어
index.tsx # 메인 페이지 (로그인 리다이렉트)
login.tsx # 로그인 페이지
register.tsx # 회원가입 페이지
dashboard.tsx # 대시보드
api/
auth/
login.ts # 로그인 API
register.ts # 회원가입 API
logout.ts # 로그아웃 API
tasks/
index.ts # 할 일 CRUD API
[id].ts # 개별 할 일 API
lib/
auth.ts # 인증 유틸리티
db.ts # 데이터베이스 계층
types.ts # 공유 타입 정의
static/
styles.css # 전역 스타일
deno.json
fresh.config.ts{
"imports": {
"fresh": "jsr:@fresh/core@^2",
"@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^0.1",
"preact": "npm:preact@^10.19",
"@preact/signals": "npm:@preact/signals@^1.2",
"hono": "jsr:@hono/hono@^4",
"zod": "npm:zod@^3.22",
"@std/assert": "jsr:@std/assert@^1",
"@std/encoding": "jsr:@std/encoding@^1"
},
"tasks": {
"dev": "deno run -A --watch=static/,routes/,islands/,components/,lib/ dev.ts",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"test": "deno test -A"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}export interface User {
id: string;
email: string;
name: string;
passwordHash: string;
createdAt: string;
}
export type TaskPriority = "high" | "medium" | "low";
export type TaskStatus = "pending" | "completed";
export interface Task {
id: string;
userId: string;
title: string;
description: string;
priority: TaskPriority;
status: TaskStatus;
dueDate: string | null;
createdAt: string;
updatedAt: string;
}
export interface AuthPayload {
userId: string;
email: string;
exp: number;
}import type { AuthPayload, User } from "./types.ts";
const JWT_SECRET = Deno.env.get("JWT_SECRET") ?? "dev-secret-change-in-production";
const encoder = new TextEncoder();
async function createHmacKey(): Promise<CryptoKey> {
return await crypto.subtle.importKey(
"raw",
encoder.encode(JWT_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"],
);
}
function base64url(data: Uint8Array): string {
return btoa(String.fromCharCode(...data))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function base64urlDecode(str: string): Uint8Array {
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(padded);
return new Uint8Array([...binary].map((c) => c.charCodeAt(0)));
}
export async function createToken(user: User): Promise<string> {
const header = { alg: "HS256", typ: "JWT" };
const payload: AuthPayload = {
userId: user.id,
email: user.email,
exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // 24시간
};
const headerB64 = base64url(encoder.encode(JSON.stringify(header)));
const payloadB64 = base64url(encoder.encode(JSON.stringify(payload)));
const data = `${headerB64}.${payloadB64}`;
const key = await createHmacKey();
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
return `${data}.${base64url(new Uint8Array(signature))}`;
}
export async function verifyToken(token: string): Promise<AuthPayload | null> {
try {
const [headerB64, payloadB64, signatureB64] = token.split(".");
if (!headerB64 || !payloadB64 || !signatureB64) return null;
const data = `${headerB64}.${payloadB64}`;
const key = await createHmacKey();
const signature = base64urlDecode(signatureB64);
const valid = await crypto.subtle.verify(
"HMAC",
key,
signature,
encoder.encode(data),
);
if (!valid) return null;
const payload: AuthPayload = JSON.parse(
new TextDecoder().decode(base64urlDecode(payloadB64)),
);
// 만료 확인
if (payload.exp < Math.floor(Date.now() / 1000)) {
return null;
}
return payload;
} catch {
return null;
}
}
export async function hashPassword(password: string): Promise<string> {
const data = encoder.encode(password + JWT_SECRET);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64url(new Uint8Array(hash));
}
export async function verifyPassword(
password: string,
hash: string,
): Promise<boolean> {
const computed = await hashPassword(password);
return computed === hash;
}이 예제에서는 학습 목적으로 Web Crypto API를 사용한 간단한 JWT 구현을 보여줍니다. 프로덕션 환경에서는 jose 같은 검증된 JWT 라이브러리를 사용하고, 비밀번호 해싱에는 bcrypt나 argon2 같은 전용 알고리즘을 사용해야 합니다.
import type { Task, User } from "./types.ts";
let kv: Deno.Kv | null = null;
async function getKv(): Promise<Deno.Kv> {
if (!kv) {
kv = await Deno.openKv();
}
return kv;
}
// 사용자 관련 함수
export async function createUser(
data: Omit<User, "id" | "createdAt">,
): Promise<User> {
const db = await getKv();
const id = crypto.randomUUID();
const user: User = {
id,
...data,
createdAt: new Date().toISOString(),
};
const existingEmail = await db.get(["users_by_email", user.email]);
if (existingEmail.value) {
throw new Error("이미 등록된 이메일입니다");
}
const result = await db.atomic()
.check(existingEmail)
.set(["users", id], user)
.set(["users_by_email", user.email], id)
.commit();
if (!result.ok) {
throw new Error("회원가입 실패");
}
return user;
}
export async function getUserByEmail(email: string): Promise<User | null> {
const db = await getKv();
const idResult = await db.get<string>(["users_by_email", email]);
if (!idResult.value) return null;
const userResult = await db.get<User>(["users", idResult.value]);
return userResult.value;
}
// 할 일 관련 함수
export async function createTask(
data: Omit<Task, "id" | "createdAt" | "updatedAt">,
): Promise<Task> {
const db = await getKv();
const id = crypto.randomUUID();
const now = new Date().toISOString();
const task: Task = {
id,
...data,
createdAt: now,
updatedAt: now,
};
await db.set(["tasks", data.userId, id], task);
return task;
}
export async function getTasksByUser(userId: string): Promise<Task[]> {
const db = await getKv();
const tasks: Task[] = [];
const iter = db.list<Task>({ prefix: ["tasks", userId] });
for await (const entry of iter) {
tasks.push(entry.value);
}
// 생성일 기준 내림차순 정렬
return tasks.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
}
export async function getTask(
userId: string,
taskId: string,
): Promise<Task | null> {
const db = await getKv();
const result = await db.get<Task>(["tasks", userId, taskId]);
return result.value;
}
export async function updateTask(
userId: string,
taskId: string,
data: Partial<Pick<Task, "title" | "description" | "priority" | "status" | "dueDate">>,
): Promise<Task> {
const db = await getKv();
const existing = await db.get<Task>(["tasks", userId, taskId]);
if (!existing.value) {
throw new Error("할 일을 찾을 수 없습니다");
}
const updated: Task = {
...existing.value,
...data,
updatedAt: new Date().toISOString(),
};
const result = await db.atomic()
.check(existing)
.set(["tasks", userId, taskId], updated)
.commit();
if (!result.ok) {
throw new Error("업데이트 실패");
}
return updated;
}
export async function deleteTask(
userId: string,
taskId: string,
): Promise<void> {
const db = await getKv();
await db.delete(["tasks", userId, taskId]);
}
export async function getTaskStats(
userId: string,
): Promise<{ total: number; completed: number; pending: number }> {
const tasks = await getTasksByUser(userId);
const completed = tasks.filter((t) => t.status === "completed").length;
return {
total: tasks.length,
completed,
pending: tasks.length - completed,
};
}import { z } from "zod";
import { createUser } from "../../../lib/db.ts";
import { createToken, hashPassword } from "../../../lib/auth.ts";
import type { Handlers } from "fresh/server.ts";
const RegisterSchema = z.object({
name: z.string().min(2, "이름은 2글자 이상이어야 합니다"),
email: z.string().email("유효한 이메일을 입력하세요"),
password: z.string().min(8, "비밀번호는 8자 이상이어야 합니다"),
});
export const handler: Handlers = {
async POST(req) {
try {
const body = await req.json();
const validation = RegisterSchema.safeParse(body);
if (!validation.success) {
return Response.json(
{ error: "입력 검증 실패", details: validation.error.issues },
{ status: 400 },
);
}
const { name, email, password } = validation.data;
const passwordHash = await hashPassword(password);
const user = await createUser({ name, email, passwordHash });
const token = await createToken(user);
return new Response(
JSON.stringify({ user: { id: user.id, name: user.name, email: user.email } }),
{
status: 201,
headers: {
"Content-Type": "application/json",
"Set-Cookie":
`token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`,
},
},
);
} catch (error) {
if (error instanceof Error && error.message.includes("이미 등록된")) {
return Response.json({ error: error.message }, { status: 409 });
}
return Response.json({ error: "서버 오류" }, { status: 500 });
}
},
};import { z } from "zod";
import { getUserByEmail } from "../../../lib/db.ts";
import { createToken, verifyPassword } from "../../../lib/auth.ts";
import type { Handlers } from "fresh/server.ts";
const LoginSchema = z.object({
email: z.string().email(),
password: z.string(),
});
export const handler: Handlers = {
async POST(req) {
try {
const body = await req.json();
const validation = LoginSchema.safeParse(body);
if (!validation.success) {
return Response.json({ error: "입력 형식이 올바르지 않습니다" }, { status: 400 });
}
const { email, password } = validation.data;
const user = await getUserByEmail(email);
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return Response.json(
{ error: "이메일 또는 비밀번호가 올바르지 않습니다" },
{ status: 401 },
);
}
const token = await createToken(user);
return new Response(
JSON.stringify({ user: { id: user.id, name: user.name, email: user.email } }),
{
headers: {
"Content-Type": "application/json",
"Set-Cookie":
`token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`,
},
},
);
} catch {
return Response.json({ error: "서버 오류" }, { status: 500 });
}
},
};import { z } from "zod";
import { createTask, getTasksByUser } from "../../../lib/db.ts";
import { verifyToken } from "../../../lib/auth.ts";
import type { Handlers } from "fresh/server.ts";
const CreateTaskSchema = z.object({
title: z.string().min(1, "제목을 입력하세요"),
description: z.string().default(""),
priority: z.enum(["high", "medium", "low"]).default("medium"),
dueDate: z.string().nullable().default(null),
});
function getTokenFromCookie(req: Request): string | null {
const cookie = req.headers.get("Cookie") ?? "";
return cookie.match(/token=([^;]+)/)?.[1] ?? null;
}
export const handler: Handlers = {
async GET(req) {
const token = getTokenFromCookie(req);
if (!token) return Response.json({ error: "인증 필요" }, { status: 401 });
const auth = await verifyToken(token);
if (!auth) return Response.json({ error: "유효하지 않은 토큰" }, { status: 401 });
const tasks = await getTasksByUser(auth.userId);
return Response.json({ tasks });
},
async POST(req) {
const token = getTokenFromCookie(req);
if (!token) return Response.json({ error: "인증 필요" }, { status: 401 });
const auth = await verifyToken(token);
if (!auth) return Response.json({ error: "유효하지 않은 토큰" }, { status: 401 });
const body = await req.json();
const validation = CreateTaskSchema.safeParse(body);
if (!validation.success) {
return Response.json(
{ error: "입력 검증 실패", details: validation.error.issues },
{ status: 400 },
);
}
const task = await createTask({
userId: auth.userId,
...validation.data,
status: "pending",
});
return Response.json({ task }, { status: 201 });
},
};import { useSignal } from "@preact/signals";
import type { Task } from "../lib/types.ts";
interface TaskListProps {
initialTasks: Task[];
}
export default function TaskList({ initialTasks }: TaskListProps) {
const tasks = useSignal<Task[]>(initialTasks);
const filter = useSignal<"all" | "pending" | "completed">("all");
async function toggleStatus(task: Task) {
const newStatus = task.status === "completed" ? "pending" : "completed";
const response = await fetch(`/api/tasks/${task.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: newStatus }),
});
if (response.ok) {
const { task: updated } = await response.json();
tasks.value = tasks.value.map((t) => (t.id === task.id ? updated : t));
}
}
async function deleteTask(taskId: string) {
const confirmed = confirm("정말 삭제하시겠습니까?");
if (!confirmed) return;
const response = await fetch(`/api/tasks/${taskId}`, {
method: "DELETE",
});
if (response.ok) {
tasks.value = tasks.value.filter((t) => t.id !== taskId);
}
}
const filteredTasks = tasks.value.filter((task) => {
if (filter.value === "all") return true;
return task.status === filter.value;
});
const priorityColors = {
high: "border-l-red-500",
medium: "border-l-yellow-500",
low: "border-l-green-500",
};
return (
<div>
<div class="flex gap-2 mb-6">
{(["all", "pending", "completed"] as const).map((f) => (
<button
key={f}
onClick={() => (filter.value = f)}
class={`px-4 py-2 rounded text-sm font-medium transition-colors ${
filter.value === f
? "bg-blue-500 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{f === "all" ? "전체" : f === "pending" ? "진행 중" : "완료"}
</button>
))}
</div>
<div class="space-y-3">
{filteredTasks.length === 0 && (
<p class="text-gray-500 text-center py-8">
{filter.value === "all"
? "할 일이 없습니다. 새로운 할 일을 추가해보세요."
: "해당 상태의 할 일이 없습니다."}
</p>
)}
{filteredTasks.map((task) => (
<div
key={task.id}
class={`border-l-4 ${priorityColors[task.priority]} bg-white rounded-lg shadow-sm p-4 flex items-center gap-4`}
>
<button
onClick={() => toggleStatus(task)}
class={`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-colors ${
task.status === "completed"
? "bg-green-500 border-green-500 text-white"
: "border-gray-300 hover:border-green-400"
}`}
>
{task.status === "completed" && (
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<div class="flex-1">
<h3
class={`font-medium ${
task.status === "completed" ? "line-through text-gray-400" : "text-gray-900"
}`}
>
{task.title}
</h3>
{task.description && (
<p class="text-sm text-gray-500 mt-1">{task.description}</p>
)}
{task.dueDate && (
<p class="text-xs text-gray-400 mt-1">
마감: {new Date(task.dueDate).toLocaleDateString("ko-KR")}
</p>
)}
</div>
<button
onClick={() => deleteTask(task.id)}
class="text-red-400 hover:text-red-600 transition-colors"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
))}
</div>
</div>
);
}import { useSignal } from "@preact/signals";
import type { TaskPriority } from "../lib/types.ts";
interface TaskFormProps {
onCreated?: () => void;
}
export default function TaskForm({ onCreated }: TaskFormProps) {
const title = useSignal("");
const description = useSignal("");
const priority = useSignal<TaskPriority>("medium");
const dueDate = useSignal("");
const isSubmitting = useSignal(false);
const error = useSignal("");
async function handleSubmit(e: Event) {
e.preventDefault();
if (!title.value.trim()) {
error.value = "제목을 입력하세요";
return;
}
isSubmitting.value = true;
error.value = "";
try {
const response = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.value,
description: description.value,
priority: priority.value,
dueDate: dueDate.value || null,
}),
});
if (response.ok) {
title.value = "";
description.value = "";
priority.value = "medium";
dueDate.value = "";
// 페이지 새로고침으로 목록 업데이트
location.reload();
} else {
const data = await response.json();
error.value = data.error ?? "할 일 생성에 실패했습니다";
}
} catch {
error.value = "네트워크 오류가 발생했습니다";
} finally {
isSubmitting.value = false;
}
}
return (
<form onSubmit={handleSubmit} class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">새 할 일 추가</h2>
{error.value && (
<div class="bg-red-50 text-red-700 px-4 py-2 rounded mb-4 text-sm">
{error.value}
</div>
)}
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
제목
</label>
<input
type="text"
value={title}
onInput={(e) => (title.value = (e.target as HTMLInputElement).value)}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="할 일을 입력하세요"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
설명 (선택)
</label>
<textarea
value={description}
onInput={(e) => (description.value = (e.target as HTMLTextAreaElement).value)}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={2}
placeholder="상세 설명을 입력하세요"
/>
</div>
<div class="flex gap-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">
우선순위
</label>
<select
value={priority}
onChange={(e) => (priority.value = (e.target as HTMLSelectElement).value as TaskPriority)}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="low">낮음</option>
<option value="medium">중간</option>
<option value="high">높음</option>
</select>
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">
마감일 (선택)
</label>
<input
type="date"
value={dueDate}
onInput={(e) => (dueDate.value = (e.target as HTMLInputElement).value)}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<button
type="submit"
disabled={isSubmitting.value}
class="w-full py-2 px-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors font-medium"
>
{isSubmitting.value ? "추가 중..." : "할 일 추가"}
</button>
</div>
</form>
);
}import { page, type RouteContext } from "fresh";
import { verifyToken } from "../lib/auth.ts";
import { getTasksByUser, getTaskStats } from "../lib/db.ts";
import TaskList from "../islands/TaskList.tsx";
import TaskForm from "../islands/TaskForm.tsx";
export default page(async function Dashboard(_props: unknown, ctx: RouteContext) {
// 쿠키에서 토큰 추출
const cookie = ctx.req.headers.get("Cookie") ?? "";
const token = cookie.match(/token=([^;]+)/)?.[1];
if (!token) {
return new Response(null, {
status: 302,
headers: { Location: "/login" },
});
}
const auth = await verifyToken(token);
if (!auth) {
return new Response(null, {
status: 302,
headers: { Location: "/login" },
});
}
const tasks = await getTasksByUser(auth.userId);
const stats = await getTaskStats(auth.userId);
return (
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow-sm">
<div class="max-w-screen-md mx-auto px-4 py-4 flex justify-between items-center">
<h1 class="text-xl font-bold text-gray-900">TaskFlow</h1>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-600">{auth.email}</span>
<a
href="/api/auth/logout"
class="text-sm text-red-500 hover:text-red-700"
>
로그아웃
</a>
</div>
</div>
</header>
<main class="max-w-screen-md mx-auto px-4 py-8">
{/* 통계 */}
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white rounded-lg shadow-sm p-4 text-center">
<p class="text-3xl font-bold text-gray-900">{stats.total}</p>
<p class="text-sm text-gray-500">전체</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 text-center">
<p class="text-3xl font-bold text-green-600">{stats.completed}</p>
<p class="text-sm text-gray-500">완료</p>
</div>
<div class="bg-white rounded-lg shadow-sm p-4 text-center">
<p class="text-3xl font-bold text-orange-500">{stats.pending}</p>
<p class="text-sm text-gray-500">진행 중</p>
</div>
</div>
{/* 할 일 추가 폼 (Island) */}
<TaskForm />
{/* 할 일 목록 (Island) */}
<TaskList initialTasks={tasks} />
</main>
</div>
);
});대시보드 페이지에서 Islands Architecture의 장점이 잘 드러납니다. 헤더, 통계 바, 레이아웃은 모두 서버에서 HTML로 렌더링되어 전송됩니다. 클라이언트 JavaScript는 TaskForm과 TaskList Island에만 로드됩니다. 이를 통해 초기 로딩이 빠르면서도 필요한 인터랙션은 유지됩니다.
# 1. 빌드 확인
deno task build
# 2. Deno Deploy에 배포
deployctl deploy --project=taskflow --prod main.ts
# 3. 환경 변수 설정 (Deno Deploy 대시보드에서)
# JWT_SECRET=your-production-secret-key// routes/api/health.ts
import type { Handlers } from "fresh/server.ts";
export const handler: Handlers = {
async GET() {
const start = performance.now();
// KV 연결 확인
let kvStatus = "ok";
try {
const kv = await Deno.openKv();
await kv.get(["health-check"]);
} catch {
kvStatus = "error";
}
const duration = performance.now() - start;
return Response.json({
status: kvStatus === "ok" ? "healthy" : "degraded",
region: Deno.env.get("DENO_REGION") ?? "local",
timestamp: new Date().toISOString(),
checks: {
kv: kvStatus,
latencyMs: Math.round(duration * 100) / 100,
},
});
},
};Deno Deploy 대시보드에서 요청 로그, 에러 추적, 대역폭 사용량 등을 모니터링할 수 있습니다. 프로덕션 환경에서는 외부 모니터링 서비스(Uptime Robot, Better Stack 등)와 연동하여 헬스 체크 엔드포인트를 주기적으로 확인하는 것을 권장합니다.
이 시리즈를 통해 Deno 2의 핵심 개념부터 실전 프로젝트까지 전 과정을 살펴보았습니다.
1장: JavaScript 런타임 생태계와 Deno의 탄생 배경
2장: Deno 2의 아키텍처 (V8, Tokio, Rust, 내장 도구 체인)
3장: 권한 기반 보안 모델의 동작 원리와 실무 적용
4장: npm 호환과 JSR을 통한 패키지 관리 전략
5장: 웹 표준 API와 Deno 네이티브 API 활용
6장: Bun과의 체계적 비교와 선택 기준
7장: Fresh 프레임워크의 Islands Architecture
8장: Deno Deploy를 활용한 엣지 배포
9장: REST API 구축 실습 (Hono, Deno KV, 테스트)
10장: 풀스택 프로젝트 완성 (Fresh + API + KV)Deno 2는 "이상적인 JavaScript 런타임"에 한 걸음 더 다가간 결과물입니다. 보안, 웹 표준, 개발자 경험이라는 세 가지 축을 중심으로, npm 호환이라는 현실적인 요구까지 포용한 런타임입니다.
JavaScript 런타임 생태계는 Node.js, Deno, Bun의 경쟁 속에서 빠르게 발전하고 있습니다. 어떤 런타임을 선택하든, 이 시리즈에서 다룬 웹 표준 API, 보안 모델, 엣지 컴퓨팅 같은 개념들은 현대 JavaScript 개발의 기반 지식으로 계속 가치를 가질 것입니다.
이 글이 도움이 되셨나요?
Deno.serve()를 활용한 HTTP 서버 구축부터 Hono 프레임워크, PostgreSQL 및 Deno KV 데이터베이스 연동, 테스트 작성, 배포까지 REST API 개발 전 과정을 다룹니다.
Deno Deploy를 중심으로 서버리스 및 엣지 배포 전략을 다룹니다. 엣지 컴퓨팅 개념, 콜드 스타트 성능, Deno KV를 활용한 엣지 상태 관리, Cloudflare Workers 호환성을 분석합니다.
Deno의 공식 웹 프레임워크 Fresh를 심층 분석합니다. Islands Architecture, Preact 기반 컴포넌트, 라우팅, 미들웨어, 데이터 페칭 등 핵심 기능을 다룹니다.