이 글은 누구를 위한 것인가
- Node.js의 보안 문제와 복잡한 설정 없이 TypeScript를 바로 실행하고 싶은 팀
- Deno 2의 npm 호환성 수준을 파악하고 마이그레이션을 고려하는 개발자
- Fresh Islands 아키텍처로 빠른 풀스택 앱을 만들고 싶은 개발자
들어가며
Deno 2는 npm 완전 호환과 함께 Node.js의 대안으로 자리잡았다. 빌드 단계 없이 TypeScript를 바로 실행하고, 기본적으로 권한 없이 파일/네트워크 접근이 불가능한 보안 모델을 갖는다. Fresh는 Deno 위의 풀스택 프레임워크다.
이 글은 bluefoxdev.kr의 Deno 2 실전 가이드 를 참고하여 작성했습니다.
1. Deno 2 핵심 특성
[Deno 2 vs Node.js]
설정:
Node.js: package.json, tsconfig.json, .eslintrc 등 다수
Deno: deno.json 하나 (또는 없어도 됨)
TypeScript:
Node.js: ts-node/tsx/swc 필요
Deno: 기본 지원 (트랜스파일 내장)
패키지:
Node.js: npm, node_modules
Deno 2: npm 호환 + JSR(JavaScript Registry) + URL import
보안:
Node.js: 모든 권한 기본
Deno: 기본 권한 없음, --allow-* 플래그로 명시적 허용
런타임 API:
Node.js: CommonJS + ESM 혼재
Deno: Web Standard API 우선 (fetch, crypto, Web Streams)
[Deno KV]
내장 키-값 데이터베이스 (SQLite 기반 로컬)
Deno Deploy에서 글로벌 분산 복제
타입 안전한 API
사용 사례: 세션, 캐시, 설정 저장
[Fresh 프레임워크]
파일 기반 라우팅
Islands 아키텍처 (Astro와 유사)
미들웨어 지원
빌드 단계 없음 (JIT 컴파일)
Tailwind 내장
2. Fresh 풀스택 앱 구현
// deno.json
// {
// "tasks": {
// "dev": "deno run -A --watch=static/,routes/ dev.ts",
// "build": "deno run -A dev.ts build",
// "start": "deno run -A main.ts"
// },
// "imports": {
// "$fresh/": "https://deno.land/x/fresh@1.7.0/",
// "preact": "https://esm.sh/preact@10.22.0",
// "preact/": "https://esm.sh/preact@10.22.0/"
// }
// }
// routes/index.tsx - 페이지 라우트
import { Handlers, PageProps } from '$fresh/server.ts';
import { Head } from '$fresh/runtime.ts';
interface Product {
id: number;
name: string;
price: number;
}
// 서버 사이드 데이터 로딩
export const handler: Handlers<Product[]> = {
async GET(req, ctx) {
const products = await fetchProducts();
return ctx.render(products);
},
};
export default function ProductsPage({ data }: PageProps<Product[]>) {
return (
<>
<Head>
<title>상품 목록</title>
</Head>
<main>
<h1>상품 목록</h1>
<ProductList products={data} />
</main>
</>
);
}
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map(p => (
<li key={p.id}>
<a href={`/products/${p.id}`}>{p.name}</a>
<span>₩{p.price.toLocaleString()}</span>
</li>
))}
</ul>
);
}
async function fetchProducts(): Promise<Product[]> {
return [
{ id: 1, name: '상품 A', price: 10000 },
{ id: 2, name: '상품 B', price: 20000 },
];
}
// islands/AddToCart.tsx - 클라이언트 컴포넌트 (Island)
import { useState } from 'preact/hooks';
interface Props {
productId: number;
initialCount?: number;
}
export default function AddToCart({ productId, initialCount = 0 }: Props) {
const [count, setCount] = useState(initialCount);
const [loading, setLoading] = useState(false);
async function addToCart() {
setLoading(true);
try {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId, quantity: 1 }),
headers: { 'Content-Type': 'application/json' },
});
setCount(c => c + 1);
} finally {
setLoading(false);
}
}
return (
<div>
<span>장바구니: {count}개</span>
<button onClick={addToCart} disabled={loading}>
{loading ? '추가 중...' : '장바구니 추가'}
</button>
</div>
);
}
// routes/api/cart.ts - API 라우트
import { Handlers } from '$fresh/server.ts';
// Deno KV로 장바구니 저장
const kv = await Deno.openKv();
export const handler: Handlers = {
async GET(req) {
const sessionId = getSessionId(req);
const entries = kv.list<CartItem>({ prefix: ['cart', sessionId] });
const items: CartItem[] = [];
for await (const entry of entries) {
items.push(entry.value);
}
return Response.json(items);
},
async POST(req) {
const sessionId = getSessionId(req);
const { productId, quantity } = await req.json();
const key = ['cart', sessionId, productId];
const existing = await kv.get<CartItem>(key);
await kv.set(key, {
productId,
quantity: (existing.value?.quantity ?? 0) + quantity,
updatedAt: new Date().toISOString(),
});
return Response.json({ success: true });
},
};
interface CartItem { productId: number; quantity: number; updatedAt: string }
function getSessionId(req: Request): string {
return req.headers.get('x-session-id') ?? crypto.randomUUID();
}
// routes/_middleware.ts - 전역 미들웨어
import { FreshContext } from '$fresh/server.ts';
export async function handler(req: Request, ctx: FreshContext) {
const start = Date.now();
// 인증 체크
if (ctx.destination === 'route') {
const url = new URL(req.url);
const isProtected = url.pathname.startsWith('/dashboard');
if (isProtected) {
const session = req.headers.get('cookie');
if (!session || !isValidSession(session)) {
return Response.redirect(new URL('/login', req.url));
}
}
}
const resp = await ctx.next();
// 응답 시간 헤더 추가
resp.headers.set('X-Response-Time', `${Date.now() - start}ms`);
return resp;
}
function isValidSession(cookie: string): boolean {
return cookie.includes('session=');
}
# Deno 권한 모델
deno run \
--allow-net=api.example.com:443 \ # 특정 호스트만
--allow-read=/data \ # 특정 경로만
--allow-env=DATABASE_URL,PORT \ # 특정 환경변수만
server.ts
# npm 패키지 사용 (Deno 2)
deno run --allow-net npm:hono@latest server.ts
# Deno Deploy 배포
deployctl deploy --project=my-app main.ts
# 성능 벤치마크
deno bench benchmarks/http.ts