본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 10장: 실전 프로젝트 - Deno 2 풀스택 애플리케이션
2026년 8월 5일·프로그래밍·

10장: 실전 프로젝트 - Deno 2 풀스택 애플리케이션

Fresh 프론트엔드와 Deno.serve API, Deno KV를 결합한 풀스택 할 일 관리 애플리케이션을 구축합니다. 인증, 배포, 모니터링까지 실전 프로젝트의 전체 과정을 다룹니다.

22분2,579자10개 섹션
typescriptperformancetoolingsecuritydeveloper-experience
공유
deno-runtime10 / 10
12345678910
이전9장: Deno로 REST API 구축하기

실전 프로젝트 개요

이 시리즈의 마지막 장에서는 지금까지 배운 모든 내용을 종합하여 풀스택 애플리케이션을 구축합니다. Fresh 프론트엔드, Deno.serve 기반 API, Deno KV 데이터베이스를 결합한 할 일 관리 애플리케이션(TaskFlow) 을 만들겠습니다.

기술 스택

핵심 기능

TaskFlow 기능 목록
text
인증
  - 회원가입 / 로그인
  - JWT 기반 세션 관리
  - 쿠키 기반 인증 유지
 
할 일 관리
  - 할 일 생성 / 조회 / 수정 / 삭제
  - 완료 상태 토글
  - 우선순위 설정 (높음, 중간, 낮음)
  - 마감일 설정
 
대시보드
  - 통계 (전체, 완료, 미완료)
  - 우선순위별 분류

프로젝트 초기 설정

프로젝트 구조

TaskFlow 프로젝트 구조
text
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

설정 파일

deno.json
json
{
  "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"
  }
}

공유 타입 정의

lib/types.ts
typescript
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;
}

인증 시스템 구현

JWT 인증 유틸리티

lib/auth.ts
typescript
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;
}
Warning

이 예제에서는 학습 목적으로 Web Crypto API를 사용한 간단한 JWT 구현을 보여줍니다. 프로덕션 환경에서는 jose 같은 검증된 JWT 라이브러리를 사용하고, 비밀번호 해싱에는 bcrypt나 argon2 같은 전용 알고리즘을 사용해야 합니다.

데이터베이스 계층

lib/db.ts
typescript
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,
  };
}

API 라우트 구현

인증 API

routes/api/auth/register.ts
typescript
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 });
    }
  },
};
routes/api/auth/login.ts
typescript
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 });
    }
  },
};

할 일 API

routes/api/tasks/index.ts
typescript
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 });
  },
};

프론트엔드 Island 컴포넌트

할 일 목록 Island

islands/TaskList.tsx
typescript
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>
  );
}

할 일 생성 폼 Island

islands/TaskForm.tsx
typescript
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>
  );
}

대시보드 페이지

routes/dashboard.tsx
typescript
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>
  );
});
Info

대시보드 페이지에서 Islands Architecture의 장점이 잘 드러납니다. 헤더, 통계 바, 레이아웃은 모두 서버에서 HTML로 렌더링되어 전송됩니다. 클라이언트 JavaScript는 TaskForm과 TaskList Island에만 로드됩니다. 이를 통해 초기 로딩이 빠르면서도 필요한 인터랙션은 유지됩니다.

Deno Deploy 배포

배포 준비

배포 프로세스
bash
# 1. 빌드 확인
deno task build
 
# 2. Deno Deploy에 배포
deployctl deploy --project=taskflow --prod main.ts
 
# 3. 환경 변수 설정 (Deno Deploy 대시보드에서)
# JWT_SECRET=your-production-secret-key

배포 후 모니터링

모니터링을 위한 헬스 체크 엔드포인트
typescript
// 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,
      },
    });
  },
};
Tip

Deno Deploy 대시보드에서 요청 로그, 에러 추적, 대역폭 사용량 등을 모니터링할 수 있습니다. 프로덕션 환경에서는 외부 모니터링 서비스(Uptime Robot, Better Stack 등)와 연동하여 헬스 체크 엔드포인트를 주기적으로 확인하는 것을 권장합니다.

시리즈를 마치며

이 시리즈를 통해 Deno 2의 핵심 개념부터 실전 프로젝트까지 전 과정을 살펴보았습니다.

시리즈 요약
text
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 개발의 기반 지식으로 계속 가치를 가질 것입니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

9장: Deno로 REST API 구축하기

Deno.serve()를 활용한 HTTP 서버 구축부터 Hono 프레임워크, PostgreSQL 및 Deno KV 데이터베이스 연동, 테스트 작성, 배포까지 REST API 개발 전 과정을 다룹니다.

2026년 8월 2일·16분
프로그래밍

8장: 서버리스와 엣지 배포

Deno Deploy를 중심으로 서버리스 및 엣지 배포 전략을 다룹니다. 엣지 컴퓨팅 개념, 콜드 스타트 성능, Deno KV를 활용한 엣지 상태 관리, Cloudflare Workers 호환성을 분석합니다.

2026년 7월 30일·19분
프로그래밍

7장: Fresh 프레임워크 - Deno 네이티브 웹 개발

Deno의 공식 웹 프레임워크 Fresh를 심층 분석합니다. Islands Architecture, Preact 기반 컴포넌트, 라우팅, 미들웨어, 데이터 페칭 등 핵심 기능을 다룹니다.

2026년 7월 28일·16분
이전 글9장: Deno로 REST API 구축하기

댓글

목차

약 22분 남음
  • 실전 프로젝트 개요
    • 기술 스택
    • 핵심 기능
  • 프로젝트 초기 설정
    • 프로젝트 구조
    • 설정 파일
  • 공유 타입 정의
  • 인증 시스템 구현
    • JWT 인증 유틸리티
  • 데이터베이스 계층
  • API 라우트 구현
    • 인증 API
    • 할 일 API
  • 프론트엔드 Island 컴포넌트
    • 할 일 목록 Island
    • 할 일 생성 폼 Island
  • 대시보드 페이지
  • Deno Deploy 배포
    • 배포 준비
    • 배포 후 모니터링
  • 시리즈를 마치며