Fresh 완전 가이드 | Deno 기반 0 JavaScript 웹 프레임워크
이 글의 핵심
Deno 전용 차세대 웹 프레임워크 Fresh. 기본적으로 0 JavaScript를 전송하고, Islands 아키텍처로 부분 Hydration을 지원합니다. 빌드 없이 TypeScript를 바로 실행하며, Lighthouse 점수 100점을 쉽게 달성합니다.
이 글의 핵심
Fresh는 Deno 전용 0 JavaScript 웹 프레임워크입니다. Islands 아키텍처로 부분 Hydration을 지원하고, 빌드 없이 TypeScript를 바로 실행하며, Preact 컴포넌트로 초고속 웹사이트를 구축합니다. Lighthouse 100점을 쉽게 달성합니다.
목차
Fresh란?
Fresh는 2022년 Luca Casonato (Deno 팀)가 개발한 Deno 전용 웹 프레임워크입니다.
🚀 핵심 특징
1. 0 JavaScript 기본
// 정적 페이지 (JavaScript 0KB)
export default function Home() {
return (
<div>
<h1>Hello Fresh!</h1>
<p>No JavaScript sent to the client</p>
</div>
);
}
2. Islands 아키텍처
// 필요한 컴포넌트만 Hydration
<Counter /> {/* JavaScript 전송됨 */}
<p>Static content</p> {/* JavaScript 없음 */}
3. 빌드 없음
Next.js:
- TypeScript → Babel → Webpack → 출력
- 빌드 시간: 30초
Fresh:
- TypeScript → 바로 실행
- 빌드 시간: 0초
4. Deno 네이티브
// Deno 표준 라이브러리 직접 사용
import { serve } from 'https://deno.land/std/http/server.ts';
Fresh 시작하기
1️⃣ Deno 설치
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# 버전 확인
deno --version
2️⃣ Fresh 프로젝트 생성
# Fresh 프로젝트 생성
deno run -A -r https://fresh.deno.dev my-fresh-app
cd my-fresh-app
# 개발 서버 실행
deno task start
프로젝트 구조
my-fresh-app/
├── routes/
│ ├── index.tsx # 홈페이지
│ ├── about.tsx # /about
│ └── api/hello.ts # API 라우트
├── islands/
│ └── Counter.tsx # 인터랙티브 컴포넌트
├── static/
│ └── logo.svg
├── deno.json
└── fresh.gen.ts # 자동 생성
라우팅
파일 기반 라우팅
// routes/index.tsx (홈페이지)
export default function Home() {
return (
<div>
<h1>Welcome to Fresh</h1>
<p>0 JavaScript by default</p>
</div>
);
}
// routes/about.tsx (/about)
export default function About() {
return <h1>About Page</h1>;
}
// routes/blog/[slug].tsx (/blog/:slug)
import { PageProps } from '$fresh/server.ts';
export default function BlogPost(props: PageProps) {
const { slug } = props.params;
return <h1>Post: {slug}</h1>;
}
Islands (인터랙티브 컴포넌트)
Island 생성
// islands/Counter.tsx
import { useState } from 'preact/hooks';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Island 사용
// routes/index.tsx
import Counter from '../islands/Counter.tsx';
export default function Home() {
return (
<div>
<h1>Fresh Islands Demo</h1>
{/* 정적 HTML (JavaScript 없음) */}
<p>This is static content</p>
{/* Island (JavaScript 전송됨) */}
<Counter />
{/* 다시 정적 HTML */}
<p>More static content</p>
</div>
);
}
Handlers (서버 로직)
GET 요청
// routes/users.tsx
import { Handlers, PageProps } from '$fresh/server.ts';
interface User {
id: number;
name: string;
}
export const handler: Handlers<User[]> = {
async GET(_req, ctx) {
// API 호출 또는 DB 쿼리
const users = await fetchUsers();
return ctx.render(users);
},
};
export default function UsersPage(props: PageProps<User[]>) {
const users = props.data;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
POST 요청
// routes/users/new.tsx
import { Handlers } from '$fresh/server.ts';
export const handler: Handlers = {
async POST(req, ctx) {
const form = await req.formData();
const name = form.get('name') as string;
const email = form.get('email') as string;
// DB에 저장
await createUser({ name, email });
// 리다이렉트
return new Response('', {
status: 303,
headers: { Location: '/users' },
});
},
};
export default function NewUser() {
return (
<form method="POST">
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit">Create</button>
</form>
);
}
API 라우트
// routes/api/hello.ts
import { HandlerContext } from '$fresh/server.ts';
export const handler = (req: Request, ctx: HandlerContext) => {
return new Response(JSON.stringify({ message: 'Hello API!' }), {
headers: { 'Content-Type': 'application/json' },
});
};
// routes/api/users/[id].ts
export const handler = async (req: Request, ctx: HandlerContext) => {
const { id } = ctx.params;
if (req.method === 'GET') {
const user = await db.users.findUnique({ where: { id } });
return Response.json(user);
}
if (req.method === 'DELETE') {
await db.users.delete({ where: { id } });
return new Response(null, { status: 204 });
}
return new Response('Method Not Allowed', { status: 405 });
};
Deno KV (Database)
// routes/todos.tsx
import { Handlers, PageProps } from '$fresh/server.ts';
const kv = await Deno.openKv();
interface Todo {
id: string;
title: string;
completed: boolean;
}
export const handler: Handlers<Todo[]> = {
async GET(_req, ctx) {
const todos = [];
const iter = kv.list<Todo>({ prefix: ['todos'] });
for await (const entry of iter) {
todos.push(entry.value);
}
return ctx.render(todos);
},
async POST(req, ctx) {
const form = await req.formData();
const title = form.get('title') as string;
const id = crypto.randomUUID();
await kv.set(['todos', id], {
id,
title,
completed: false,
});
return new Response('', {
status: 303,
headers: { Location: '/todos' },
});
},
};
export default function Todos(props: PageProps<Todo[]>) {
const todos = props.data;
return (
<div>
<h1>Todos</h1>
<form method="POST">
<input name="title" placeholder="New todo..." />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
Fresh vs Astro vs Next.js
| 기능 | Fresh | Astro | Next.js |
|---|---|---|---|
| 런타임 | Deno만 | Node.js | Node.js |
| 초기 JS | 0KB | 0KB | 85KB |
| 빌드 | ❌ 없음 | ✅ 필요 | ✅ 필요 |
| Islands | ✅ Preact | ✅ 다중 프레임워크 | ❌ |
| TypeScript | ✅ 네이티브 | ⚠️ 컴파일 | ⚠️ 컴파일 |
| 배포 | Deno Deploy | Vercel·Cloudflare | Vercel |
| 생태계 | 🌱 새로움 | 🌿 성장 중 | 🌳 성숙 |
핵심 정리
✅ Fresh의 장점
- 0 JavaScript: 기본적으로 정적 HTML만 전송
- 빌드 없음: TypeScript를 바로 실행
- Islands 아키텍처: 부분 Hydration
- Deno 네이티브: 안전하고 빠른 런타임
- Lighthouse 100점: 쉽게 달성 가능
🚀 다음 단계
- Fresh 공식 문서에서 심화 학습
- Deno Deploy에 배포
- Discord에서 커뮤니티 참여
시작하기:
deno run -A -r https://fresh.deno.dev로 5분 만에 프로젝트를 시작하고, 0 JavaScript 웹사이트를 만드세요! 🚀