Ver3.0 ๐ Deno์ Oak ํ๋ ์์ํฌ๋ก ์์ํ๋ ์๋ฒ๋ฆฌ์ค API ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋

๐ Deno์ Oak ํ๋ ์์ํฌ๋ก ์์ํ๋ ์๋ฒ๋ฆฌ์ค API ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ์ฐจ์ธ๋ JavaScript ๋ฐํ์ ํ์ฉ๋ฒ
์๋
! ๐ ์ค๋์ ์์ฆ ๊ฐ๋ฐ์๋ค ์ฌ์ด์์ ํซํ ํ์ ์ธ Deno์ Oak ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ ์๋ฒ๋ฆฌ์ค API ๊ฐ๋ฐ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. Node.js์ ์ฐฝ์์๊ฐ ๋ง๋ ์ฐจ์ธ๋ ๋ฐํ์์ธ Deno, ๋ค์ด๋ ๋ดค๋๋ฐ ๋ง์ ์ด๋ป๊ฒ ์์ํด์ผ ํ ์ง ๋ง๋งํ๋ค๋ฉด? ์ด ๊ธ์ด ๋ฑ ๋๋ฅผ ์ํ ๊ฑฐ์ผ! ๐
์๋ฒ๋ฆฌ์ค ์ํคํ
์ฒ๋ ์ด์ ์ ํ์ด ์๋ ํ์๊ฐ ๋์ด๊ฐ๊ณ ์์ด. ํนํ ์คํํธ์
์ด๋ ๊ฐ์ธ ํ๋ก์ ํธ์์ ๋น ๋ฅด๊ฒ MVP๋ฅผ ๋ง๋ค์ด์ผ ํ ๋, Deno์ Oak์ ์กฐํฉ์ ์ ๋ง ๊ฐ๋ ฅํ ๋ฌด๊ธฐ๊ฐ ๋์ด์ค ๊ฑฐ์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ด๋ฐ ์ต์ ๊ธฐ์ ์คํ์ ํ์ฉํ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๋ ์ ๋ฌธ๊ฐ๋ค์ด ์ ์ ๋์ด๋๊ณ ์๋ค๋ ์ฌ์ค! ๐ฅ
๐ค Deno๊ฐ ๋ญ๊ธธ๋ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์, ๋จผ์ Deno๊ฐ ๋ญ์ง๋ถํฐ ์์๋ณด์. Deno๋ Node.js๋ฅผ ๋ง๋ ๋ผ์ด์ธ ๋ฌ(Ryan Dahl)์ด 2018๋
์ ๋ฐํํ ์๋ก์ด JavaScript/TypeScript ๋ฐํ์์ด์ผ. ๊ทธ๋ฐ๋ฐ ์ Node.js๋ฅผ ๋ง๋ ์ฌ๋์ด ๋ ๋ค๋ฅธ ๋ฐํ์์ ๋ง๋ค์์๊น? ๐คจ
๋ผ์ด์ธ ๋ฌ์ 2018๋
JSConf์์ "Node.js์ ๋ํ 10๊ฐ์ง ํํ"๋ผ๋ ์ ๋ชฉ์ ๋ฐํ๋ฅผ ํ์ด. ๊ทธ๋ Node.js๋ฅผ ๋ง๋ค๋ฉด์ ๋ณด์, ๋ชจ๋ ์์คํ
, package.json ๋ฑ ์ฌ๋ฌ ๋ถ๋ถ์์ ์์ฌ์ด ๊ฒฐ์ ์ ๋ด๋ ธ๋ค๊ณ ๊ณ ๋ฐฑํ์ง. ๊ทธ๋ฆฌ๊ณ ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด ์ฒ์๋ถํฐ ๋ค์ ์ค๊ณํ ๊ฒ ๋ฐ๋ก Deno์ผ!
๐ฏ Deno์ ํต์ฌ ํน์ง
1. ๋ณด์ ์ฐ์ (Secure by Default) ๐
Deno๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ผ, ๋คํธ์ํฌ, ํ๊ฒฝ๋ณ์ ์ ๊ทผ์ด ๋ชจ๋ ์ฐจ๋จ๋์ด ์์ด. ๋ช
์์ ์ผ๋ก ๊ถํ์ ๋ถ์ฌํด์ผ๋ง ์ ๊ทผํ ์ ์์ง. ์ด๊ฑด ์ ๋ง ํ์ ์ ์ธ ๋ณํ์ผ!
2. TypeScript ๋ค์ดํฐ๋ธ ์ง์ ๐
๋ณ๋์ ์ค์ ์์ด TypeScript๋ฅผ ๋ฐ๋ก ์คํํ ์ ์์ด. ํ์
์์ ์ฑ์ ์ํ๋ค๋ฉด Deno๊ฐ ์ต๊ณ ์ ์ ํ์ด์ง.
3. ES ๋ชจ๋ ํ์ค ์ฌ์ฉ ๐ฆ
CommonJS ๋์ ํ์ค ES ๋ชจ๋์ ์ฌ์ฉํด. URL๋ก ์ง์ ๋ชจ๋์ importํ ์ ์์ด์ npm ๊ฐ์ ์ค์ ํจํค์ง ๊ด๋ฆฌ์๊ฐ ํ์ ์์ด.
4. ๋ด์ฅ ๋๊ตฌ๋ค ๐ ๏ธ
ํฌ๋งคํฐ, ๋ฆฐํฐ, ํ
์คํธ ๋ฌ๋, ๋ฒ๋ค๋ฌ๊ฐ ๋ชจ๋ ๋ด์ฅ๋์ด ์์ด. ๋ณ๋์ ํจํค์ง ์ค์น๊ฐ ํ์ ์๋ค๋ ๊ฒ ์ผ๋ง๋ ํธํ์ง!
5. ์น ํ์ค API ๐
fetch, WebSocket ๋ฑ ๋ธ๋ผ์ฐ์ API๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ด. ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฌ๋ ์ปค๋ธ๊ฐ ๊ฑฐ์ ์์ง!
๐ณ Oak ํ๋ ์์ํฌ๋ ๋ ๋ญ์ผ?
Oak๋ Deno๋ฅผ ์ํ ๋ฏธ๋ค์จ์ด ํ๋ ์์ํฌ์ผ. Node.js์ Express๋ Koa๋ฅผ ์ฌ์ฉํด๋ดค๋ค๋ฉด ๊ธ๋ฐฉ ์ต์ํด์ง ๊ฑฐ์ผ. ์ค์ ๋ก Oak๋ Koa์์ ์๊ฐ์ ๋ฐ์ ๋ง๋ค์ด์ก๊ฑฐ๋ ! ๐ฒ
Oak์ ์ด๋ฆ๋ ์ฌ๋ฏธ์์ด. "Deno"๋ ๊ณต๋ฃก์ ์๋ฏธํ๋๋ฐ, ๊ณต๋ฃก์ด ์ด๋ ์๋์๋ ์ฐธ๋๋ฌด(Oak)๊ฐ ๋ง์๋ค๋ ์ปจ์
์์ ๋ฐ์จ ๊ฑฐ๋. ๊ท์ฝ์ง? ๐
๐ก Oak๋ฅผ ์ ํํด์ผ ํ๋ ์ด์
โข ๊ฐ๊ฒฐํ API - Express๋ณด๋ค ๋ ์ง๊ด์ ์ด๊ณ ๊น๋ํ ์ฝ๋ ์์ฑ์ด ๊ฐ๋ฅํด
โข ๊ฐ๋ ฅํ ๋ผ์ฐํ
- RESTful API ๊ตฌ์ถ์ ์ต์ ํ๋์ด ์์ด
โข ๋ฏธ๋ค์จ์ด ์ํ๊ณ - ์ธ์ฆ, ๋ก๊น
, CORS ๋ฑ ํ์ํ ๋ฏธ๋ค์จ์ด๊ฐ ์ ๊ฐ์ถฐ์ ธ ์์ด
โข TypeScript ์นํ์ - ํ์
์ ์๊ฐ ์๋ฒฝํ๊ฒ ๋์ด ์์ด์ ๊ฐ๋ฐ ๊ฒฝํ์ด ์ ๋ง ์ข์
โข ํ๋ฐํ ์ปค๋ฎค๋ํฐ - Deno ์ํ๊ณ์์ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์น ํ๋ ์์ํฌ์ผ
๐ Deno ์ค์น๋ถํฐ ์์ํด๋ณด์!
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ Deno๋ฅผ ์ค์นํด์ผ๊ฒ ์ง? ์ค์น๋ ์ ๋ง ๊ฐ๋จํด! ๐
๐ฅ ์ค์น ๋ฐฉ๋ฒ (์ด์์ฒด์ ๋ณ)
macOS / Linux (Shell)
curl -fsSL https://deno.land/x/install/install.sh | sh
Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
Homebrew (macOS)
brew install deno
Chocolatey (Windows)
choco install deno
์ค์น๊ฐ ๋๋ฌ์ผ๋ฉด ํฐ๋ฏธ๋์์ ํ์ธํด๋ณด์:
deno --version
๋ฒ์ ์ ๋ณด๊ฐ ๋์ค๋ฉด ์ฑ๊ณต! ๐ ๋๋ ํ์ฌ Deno 1.40.0์ ์ฌ์ฉํ๊ณ ์์ด. (2024๋ ๊ธฐ์ค)
โ ๏ธ ํ๊ฒฝ ๋ณ์ ์ค์ ์ฃผ์์ฌํญ
์ค์น ํ PATH ํ๊ฒฝ ๋ณ์๊ฐ ์๋์ผ๋ก ์ค์ ๋์ง ์์ ์ ์์ด. ๊ทธ๋ด ๋ ์ ์ค์ ํ์ผ(.bashrc, .zshrc ๋ฑ)์ ๋ค์์ ์ถ๊ฐํด์ค:
export DENO_INSTALL="/home/username/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
๊ทธ๋ฆฌ๊ณ source ~/.bashrc (๋๋ ํด๋น ์ ์ค์ ํ์ผ)๋ก ์ ์ฉํ๋ฉด ๋ผ!
๐จ ์ฒซ ๋ฒ์งธ Deno ํ๋ก๊ทธ๋จ ๋ง๋ค๊ธฐ
์ค์น๊ฐ ๋๋ฌ์ผ๋ ๊ฐ๋จํ ํ๋ก๊ทธ๋จ๋ถํฐ ๋ง๋ค์ด๋ณด์. Deno์ ๋งค๋ ฅ์ ๋๊ปด๋ณผ ์๊ฐ์ด์ผ! โจ
๐ถ Hello World ์์
hello.ts ํ์ผ์ ๋ง๋ค๊ณ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด๋ด:
// hello.ts
const greeting: string = "Hello, Deno! ๐ฆ";
console.log(greeting);
// ํ์ฌ ์๊ฐ๋ ์ถ๋ ฅํด๋ณด์
const now = new Date();
console.log(`ํ์ฌ ์๊ฐ: ${now.toLocaleString('ko-KR')}`);
์คํ์ ์ด๋ ๊ฒ:
deno run hello.ts
๋ณด์ด์ง? TypeScript ํ์ผ์ ๋ณ๋์ ์ปดํ์ผ ์์ด ๋ฐ๋ก ์คํํ์ด! ์ด๊ฒ Deno์ ์ฒซ ๋ฒ์งธ ๋งค๋ ฅ ํฌ์ธํธ์ผ. ๐
๐ ์น ์๋ฒ ๋ง๋ค์ด๋ณด๊ธฐ
์ด์ ๊ฐ๋จํ ์น ์๋ฒ๋ฅผ ๋ง๋ค์ด๋ณผ๊น? server.ts ํ์ผ์ ๋ง๋ค์ด:
// server.ts
const port = 8000;
const handler = (request: Request): Response => {
const body = `Your user-agent is:\n\n${
request.headers.get("user-agent") ?? "Unknown"
}`;
return new Response(body, {
status: 200,
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
};
console.log(`HTTP ์๋ฒ๊ฐ http://localhost:${port}/ ์์ ์คํ ์ค์
๋๋ค ๐`);
Deno.serve({ port }, handler);
์คํํ ๋๋ ๋คํธ์ํฌ ๊ถํ์ด ํ์ํด:
deno run --allow-net server.ts
๋ธ๋ผ์ฐ์ ์์ http://localhost:8000์ ์ ์ํ๋ฉด ๋น์ ์ User-Agent ์ ๋ณด๊ฐ ๋ณด์ผ ๊ฑฐ์ผ! ๐ฏ
์ฌ๊ธฐ์ ์ฃผ๋ชฉํ ์ ์ --allow-net ํ๋๊ทธ์ผ. ์ด๊ฒ ๋ฐ๋ก Deno์ ๋ณด์ ๋ชจ๋ธ์ด์ง. ๋ช
์์ ์ผ๋ก ๊ถํ์ ๋ถ์ฌํ์ง ์์ผ๋ฉด ๋คํธ์ํฌ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํด!
๐ฒ Oak ํ๋ ์์ํฌ๋ก ๋ณธ๊ฒฉ API ๊ฐ๋ฐ
์, ์ด์ Oak ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํด์ ์ ๋๋ก ๋ API๋ฅผ ๋ง๋ค์ด๋ณด์! Oak๋ ๋ณ๋ ์ค์น๊ฐ ํ์ ์์ด. URL๋ก ์ง์ importํ๋ฉด ๋ผ. ์ด๊ฒ Deno์ ๋ ๋ฒ์งธ ๋งค๋ ฅ ํฌ์ธํธ! ๐ฆ
๐ฌ ๊ธฐ๋ณธ Oak ์๋ฒ ๊ตฌ์ถ
oak-basic.ts ํ์ผ์ ๋ง๋ค์ด๋ณด์:
// oak-basic.ts
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const router = new Router();
// ๊ธฐ๋ณธ ๋ผ์ฐํธ
router.get("/", (context) => {
context.response.body = {
message: "Welcome to Oak API! ๐ฒ",
timestamp: new Date().toISOString()
};
});
// ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
router.get("/api/user/:id", (context) => {
const userId = context.params.id;
context.response.body = {
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`
};
});
// POST ์์ฒญ ์ฒ๋ฆฌ
router.post("/api/users", async (context) => {
const body = await context.request.body();
const userData = await body.value;
context.response.status = 201;
context.response.body = {
message: "์ฌ์ฉ์๊ฐ ์์ฑ๋์์ต๋๋ค",
data: userData
};
});
const app = new Application();
// ๋ก๊ฑฐ ๋ฏธ๋ค์จ์ด
app.use(async (context, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${context.request.method} ${context.request.url} - ${ms}ms`);
});
app.use(router.routes());
app.use(router.allowedMethods());
console.log("๐ Oak ์๋ฒ๊ฐ http://localhost:8000 ์์ ์คํ ์ค์
๋๋ค");
await app.listen({ port: 8000 });
์คํ:
deno run --allow-net oak-basic.ts
์ด์ API ํ ์คํธ๋ฅผ ํด๋ณด์! curl์ด๋ Postman์ ์ฌ์ฉํด๋ ๋๊ณ , ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ ์ํด๋ ๋ผ. ๐
๐ ์ฝ๋ ๋ถ์ํ๊ธฐ
์ ์ฝ๋์์ ์ฃผ๋ชฉํ ๋ถ๋ถ๋ค์ ์ดํด๋ณด์:
1. Import ๋ฐฉ์
URL๋ก ์ง์ ๋ชจ๋์ ๊ฐ์ ธ์. ๋ฒ์ ๋ URL์ ๋ช
์๋์ด ์์ด์ ์์กด์ฑ ๊ด๋ฆฌ๊ฐ ๋ช
ํํด.
2. Router ๊ฐ์ฒด
Express์ Router์ ๋น์ทํ์ง? HTTP ๋ฉ์๋๋ณ๋ก ๋ผ์ฐํธ๋ฅผ ์ ์ํ ์ ์์ด.
3. Context ๊ฐ์ฒด
request์ response๊ฐ ํ๋์ context ๊ฐ์ฒด๋ก ํตํฉ๋์ด ์์ด. Koa ์คํ์ผ์ด์ง.
4. ๋ฏธ๋ค์จ์ด ์ฒด์ธ
app.use()๋ก ๋ฏธ๋ค์จ์ด๋ฅผ ๋ฑ๋กํ๊ณ , await next()๋ก ๋ค์ ๋ฏธ๋ค์จ์ด๋ฅผ ํธ์ถํด.
5. ๋น๋๊ธฐ ์ฒ๋ฆฌ
๋ชจ๋ ํธ๋ค๋ฌ๊ฐ async ํจ์๋ก ์์ฑ๋์ด ์์ด. Promise ๊ธฐ๋ฐ์ ๊น๋ํ ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํด!
๐ฏ ์ค์ RESTful API ๊ตฌ์ถํ๊ธฐ
์ด์ ์ค์ ํ๋ก์ ํธ์์ ์ฌ์ฉํ ์ ์๋ ์์ค์ API๋ฅผ ๋ง๋ค์ด๋ณด์. ๊ฐ๋จํ ๋ธ๋ก๊ทธ API๋ฅผ ์์ ๋ก ์ผ์๊ฒ! ๐
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
๋จผ์ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์ก์๋ณด์:
blog-api/
โโโ src/
โ โโโ controllers/
โ โ โโโ postController.ts
โ โโโ models/
โ โ โโโ Post.ts
โ โโโ routes/
โ โ โโโ postRoutes.ts
โ โโโ middleware/
โ โ โโโ logger.ts
โ โโโ app.ts
โโโ deps.ts
โโโ mod.ts
Deno์์๋ deps.ts ํ์ผ์ ๋ชจ๋ ์ธ๋ถ ์์กด์ฑ์ ๋ชจ์๋๋ ๊ฒ ๊ด๋ก์ผ. ์ด๋ ๊ฒ ํ๋ฉด ์์กด์ฑ ๊ด๋ฆฌ๊ฐ ํจ์ฌ ์ฌ์์ ธ!
๐ฆ deps.ts - ์์กด์ฑ ๊ด๋ฆฌ
// deps.ts
export {
Application,
Router,
Context,
Status,
} from "https://deno.land/x/oak@v12.6.1/mod.ts";
export type {
RouterContext,
Middleware,
} from "https://deno.land/x/oak@v12.6.1/mod.ts";
์ด์ ๋ค๋ฅธ ํ์ผ์์๋ deps.ts์์ importํ๋ฉด ๋ผ. ๋ฒ์ ๊ด๋ฆฌ๊ฐ ํ ๊ณณ์์ ์ด๋ฃจ์ด์ง๋๊น ํธํ์ง? ๐ฏ
๐ ๋ชจ๋ธ ์ ์ - Post.ts
// src/models/Post.ts
export interface Post {
id: string;
title: string;
content: string;
author: string;
createdAt: Date;
updatedAt: Date;
tags: string[];
}
export class PostStore {
private posts: Map<string, Post> = new Map();
create(post: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>): Post {
const id = crypto.randomUUID();
const now = new Date();
const newPost: Post = {
...post,
id,
createdAt: now,
updatedAt: now,
};
this.posts.set(id, newPost);
return newPost;
}
findAll(): Post[] {
return Array.from(this.posts.values());
}
findById(id: string): Post | undefined {
return this.posts.get(id);
}
update(id: string, updates: Partial<Post>): Post | undefined {
const post = this.posts.get(id);
if (!post) return undefined;
const updatedPost: Post = {
...post,
...updates,
id: post.id,
createdAt: post.createdAt,
updatedAt: new Date(),
};
this.posts.set(id, updatedPost);
return updatedPost;
}
delete(id: string): boolean {
return this.posts.delete(id);
}
findByTag(tag: string): Post[] {
return this.findAll().filter(post =>
post.tags.includes(tag)
);
}
}
// ์ฑ๊ธํค ์ธ์คํด์ค
export const postStore = new PostStore();
์ฌ๊ธฐ์๋ ๊ฐ๋จํ๊ฒ ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ์ด. ์ค์ ํ๋ก๋์ ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐ๊ฒฐํด์ผ๊ฒ ์ง? ๋์ค์ MongoDB๋ PostgreSQL ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ๋ ์๋ ค์ค๊ฒ! ๐พ
๐ฎ ์ปจํธ๋กค๋ฌ ์์ฑ - postController.ts
// src/controllers/postController.ts
import { RouterContext, Status } from "../../deps.ts";
import { postStore } from "../models/Post.ts";
export const getAllPosts = (context: RouterContext<"/posts">) => {
const posts = postStore.findAll();
context.response.body = {
success: true,
data: posts,
count: posts.length,
};
};
export const getPostById = (context: RouterContext<"/posts/:id">) => {
const { id } = context.params;
const post = postStore.findById(id);
if (!post) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "ํฌ์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.body = {
success: true,
data: post,
};
};
export const createPost = async (context: RouterContext<"/posts">) => {
try {
const body = await context.request.body();
const postData = await body.value;
// ์ ํจ์ฑ ๊ฒ์ฌ
if (!postData.title || !postData.content || !postData.author) {
context.response.status = Status.BadRequest;
context.response.body = {
success: false,
message: "title, content, author๋ ํ์ ํญ๋ชฉ์
๋๋ค",
};
return;
}
const newPost = postStore.create({
title: postData.title,
content: postData.content,
author: postData.author,
tags: postData.tags || [],
});
context.response.status = Status.Created;
context.response.body = {
success: true,
data: newPost,
message: "ํฌ์คํธ๊ฐ ์์ฑ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const updatePost = async (context: RouterContext<"/posts/:id">) => {
const { id } = context.params;
try {
const body = await context.request.body();
const updates = await body.value;
const updatedPost = postStore.update(id, updates);
if (!updatedPost) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "ํฌ์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.body = {
success: true,
data: updatedPost,
message: "ํฌ์คํธ๊ฐ ์์ ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const deletePost = (context: RouterContext<"/posts/:id">) => {
const { id } = context.params;
const deleted = postStore.delete(id);
if (!deleted) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "ํฌ์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.status = Status.NoContent;
};
export const getPostsByTag = (context: RouterContext<"/posts/tag/:tag">) => {
const { tag } = context.params;
const posts = postStore.findByTag(tag);
context.response.body = {
success: true,
data: posts,
count: posts.length,
tag,
};
};
์ปจํธ๋กค๋ฌ์์๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํด. ์๋ฌ ํธ๋ค๋ง๋ ๊ผผ๊ผผํ๊ฒ ํด์คฌ์ด. ์ค์ ํ๋ก์ ํธ์์๋ ์ด๋ฐ ํจํด์ด ์ ๋ง ์ค์ํ๊ฑฐ๋ ! ๐ฏ
๐ฃ๏ธ ๋ผ์ฐํธ ์ค์ - postRoutes.ts
// src/routes/postRoutes.ts
import { Router } from "../../deps.ts";
import {
getAllPosts,
getPostById,
createPost,
updatePost,
deletePost,
getPostsByTag,
} from "../controllers/postController.ts";
export const postRouter = new Router();
postRouter
.get("/posts", getAllPosts)
.get("/posts/tag/:tag", getPostsByTag)
.get("/posts/:id", getPostById)
.post("/posts", createPost)
.put("/posts/:id", updatePost)
.delete("/posts/:id", deletePost);
๋ผ์ฐํธ๋ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ์ด. ์ฒด์ด๋ ๋ฐฉ์์ผ๋ก ์์ฑํ๋ฉด ๊ฐ๋ ์ฑ์ด ์ข์ง? ๐
๐ง ๋ฏธ๋ค์จ์ด ์์ฑ - logger.ts
// src/middleware/logger.ts
import { Context, Middleware } from "../../deps.ts";
export const logger: Middleware = async (context: Context, next) => {
const start = Date.now();
const { method, url } = context.request;
await next();
const ms = Date.now() - start;
const status = context.response.status;
// ์ํ ์ฝ๋์ ๋ฐ๋ผ ์์ ๋ณ๊ฒฝ (ํฐ๋ฏธ๋ ์ถ๋ ฅ์ฉ)
const statusColor = status >= 500 ? "๐ด"
: status >= 400 ? "๐ก"
: status >= 300 ? "๐ต"
: "๐ข";
console.log(
`${statusColor} ${method} ${url.pathname} - ${status} - ${ms}ms`
);
};
export const errorHandler: Middleware = async (context: Context, next) => {
try {
await next();
} catch (error) {
console.error("โ ์๋ฌ ๋ฐ์:", error);
context.response.status = 500;
context.response.body = {
success: false,
message: "์๋ฒ ๋ด๋ถ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const corsMiddleware: Middleware = async (context: Context, next) => {
context.response.headers.set("Access-Control-Allow-Origin", "*");
context.response.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
);
context.response.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
if (context.request.method === "OPTIONS") {
context.response.status = 204;
return;
}
await next();
};
๋ฏธ๋ค์จ์ด๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ก์ง์ ๋ด๋ ๊ณณ์ด์ผ. ๋ก๊น , ์๋ฌ ์ฒ๋ฆฌ, CORS ์ค์ ๋ฑ์ ๊น๋ํ๊ฒ ๋ถ๋ฆฌํ์ด! ๐ ๏ธ
๐ ๋ฉ์ธ ์ ํ๋ฆฌ์ผ์ด์ - app.ts
// src/app.ts
import { Application } from "../deps.ts";
import { postRouter } from "./routes/postRoutes.ts";
import { logger, errorHandler, corsMiddleware } from "./middleware/logger.ts";
const app = new Application();
// ์ ์ญ ๋ฏธ๋ค์จ์ด ๋ฑ๋ก
app.use(errorHandler);
app.use(logger);
app.use(corsMiddleware);
// ๋ผ์ฐํฐ ๋ฑ๋ก
app.use(postRouter.routes());
app.use(postRouter.allowedMethods());
// 404 ํธ๋ค๋ฌ
app.use((context) => {
context.response.status = 404;
context.response.body = {
success: false,
message: "์์ฒญํ์ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
path: context.request.url.pathname,
};
});
export default app;
๐ฌ ์ง์ ์ - mod.ts
// mod.ts
import app from "./src/app.ts";
const PORT = 8000;
const HOST = "localhost";
console.log(`
๐ฆ Deno Blog API Server
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ ์๋ฒ๊ฐ ์์๋์์ต๋๋ค!
๐ URL: http://${HOST}:${PORT}
๐ API ์๋ํฌ์ธํธ:
GET /posts - ๋ชจ๋ ํฌ์คํธ ์กฐํ
GET /posts/:id - ํน์ ํฌ์คํธ ์กฐํ
GET /posts/tag/:tag - ํ๊ทธ๋ณ ํฌ์คํธ ์กฐํ
POST /posts - ์ ํฌ์คํธ ์์ฑ
PUT /posts/:id - ํฌ์คํธ ์์
DELETE /posts/:id - ํฌ์คํธ ์ญ์
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
`);
await app.listen({ port: PORT, hostname: HOST });
์คํ:
deno run --allow-net mod.ts
์ง์! ๐ ์์ ํ RESTful API๊ฐ ์์ฑ๋์ด! ์ด์ Postman์ด๋ curl๋ก ํ ์คํธํด๋ณด์.
๐งช API ํ ์คํธํ๊ธฐ
API๋ฅผ ๋ง๋ค์์ผ๋ฉด ๋น์ฐํ ํ ์คํธ๋ฅผ ํด๋ด์ผ๊ฒ ์ง? curl ๋ช ๋ น์ด๋ก ๊ฐ๋จํ๊ฒ ํ ์คํธํด๋ณผ๊ฒ! ๐ป
๐ ํฌ์คํธ ์์ฑ (POST)
curl -X POST http://localhost:8000/posts \
-H "Content-Type: application/json" \
-d '{
"title": "Deno์ Oak๋ก API ๋ง๋ค๊ธฐ",
"content": "์ ๋ง ์ฝ๊ณ ์ฌ๋ฏธ์์ด์!",
"author": "๊ฐ๋ฐ์ ๊น์ฝ๋ฉ",
"tags": ["deno", "oak", "typescript"]
}'
์๋ต:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Deno์ Oak๋ก API ๋ง๋ค๊ธฐ",
"content": "์ ๋ง ์ฝ๊ณ ์ฌ๋ฏธ์์ด์!",
"author": "๊ฐ๋ฐ์ ๊น์ฝ๋ฉ",
"tags": ["deno", "oak", "typescript"],
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
},
"message": "ํฌ์คํธ๊ฐ ์์ฑ๋์์ต๋๋ค"
}
๐ ๋ชจ๋ ํฌ์คํธ ์กฐํ (GET)
curl http://localhost:8000/posts
์๋ต:
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Deno์ Oak๋ก API ๋ง๋ค๊ธฐ",
"content": "์ ๋ง ์ฝ๊ณ ์ฌ๋ฏธ์์ด์!",
"author": "๊ฐ๋ฐ์ ๊น์ฝ๋ฉ",
"tags": ["deno", "oak", "typescript"],
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
],
"count": 1
}
๐ ํน์ ํฌ์คํธ ์กฐํ (GET)
curl http://localhost:8000/posts/550e8400-e29b-41d4-a716-446655440000
โ๏ธ ํฌ์คํธ ์์ (PUT)
curl -X PUT http://localhost:8000/posts/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{
"title": "Deno์ Oak๋ก API ๋ง๋ค๊ธฐ (์์ ๋จ)",
"content": "์
๋ฐ์ดํธ๋ ๋ด์ฉ์
๋๋ค!"
}'
๐๏ธ ํฌ์คํธ ์ญ์ (DELETE)
curl -X DELETE http://localhost:8000/posts/550e8400-e29b-41d4-a716-446655440000
๐ท๏ธ ํ๊ทธ๋ก ๊ฒ์ (GET)
curl http://localhost:8000/posts/tag/deno
โ๏ธ ์๋ฒ๋ฆฌ์ค ๋ฐฐํฌํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ! ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. Deno๋ ์ฌ๋ฌ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ์ ์ง์ํด. ๋ํ์ ์ธ ๊ฒ๋ค์ ์ดํด๋ณผ๊ฒ! ๐
๐ Deno Deploy - ๊ณต์ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ
Deno Deploy๋ Deno ํ์ด ์ง์ ๋ง๋ ์๋ฒ๋ฆฌ์ค ํ๋ซํผ์ด์ผ. ์ ์ธ๊ณ 35๊ฐ ์ด์์ ์ฃ์ง ๋ก์ผ์ด์
์์ ์คํ๋์ด ์ด์ ์ง์ฐ์ ์๋ํ์ง! ๐
ํน์ง:
โข ๋ฌด๋ฃ ํฐ์ด ์ ๊ณต (์ 100๋ง ์์ฒญ)
โข GitHub ์ฐ๋์ผ๋ก ์๋ ๋ฐฐํฌ
โข ์ ์ญ CDN ๋ด์ฅ
โข ์ปค์คํ
๋๋ฉ์ธ ์ง์
โข ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
โข ์ค์๊ฐ ๋ก๊ทธ ํ์ธ
๐ Deno Deploy ๋ฐฐํฌ ๊ณผ์
1๋จ๊ณ: ๊ณ์ ์์ฑ
deno.com/deploy์ ์ ์ํด์ GitHub ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํด.
2๋จ๊ณ: ํ๋ก์ ํธ ์์ฑ
"New Project" ๋ฒํผ์ ํด๋ฆญํ๊ณ GitHub ์ ์ฅ์๋ฅผ ์ฐ๊ฒฐํด.
3๋จ๊ณ: ์ง์
์ ์ค์
๋ฐฐํฌํ ํ์ผ์ ์ ํํด. ์ฐ๋ฆฌ์ ๊ฒฝ์ฐ mod.ts๋ฅผ ์ ํํ๋ฉด ๋ผ.
4๋จ๊ณ: ํ๊ฒฝ ๋ณ์ ์ค์ (ํ์์)
ํ๋ก์ ํธ ์ค์ ์์ ํ๊ฒฝ ๋ณ์๋ฅผ ์ถ๊ฐํ ์ ์์ด.
5๋จ๊ณ: ๋ฐฐํฌ!
"Deploy Project" ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋! ๋ช ์ด ์์ ์ ์ธ๊ณ์ ๋ฐฐํฌ๋ผ. ๐
๋ฐฐํฌ๊ฐ ์๋ฃ๋๋ฉด https://your-project.deno.dev ๊ฐ์ URL์ด ์์ฑ๋ผ. ์ด์ ์ ์ธ๊ณ ์ด๋์๋ ๋น์ ์ API์ ์ ๊ทผํ ์ ์์ด!
โ๏ธ ๋ฐฐํฌ์ฉ ์ฝ๋ ์์
Deno Deploy์์๋ ํฌํธ๋ฅผ ์๋์ผ๋ก ํ ๋นํด์ค. ์ฝ๋๋ฅผ ์ฝ๊ฐ ์์ ํด์ผ ํด:
// mod.ts (๋ฐฐํฌ์ฉ)
import app from "./src/app.ts";
const PORT = parseInt(Deno.env.get("PORT") || "8000");
console.log(`๐ ์๋ฒ๊ฐ ํฌํธ ${PORT}์์ ์คํ ์ค์
๋๋ค`);
await app.listen({ port: PORT });
ํ๊ฒฝ ๋ณ์์์ PORT๋ฅผ ๊ฐ์ ธ์ค๋๋ก ์์ ํ์ด. ์ด๋ ๊ฒ ํ๋ฉด ๋ก์ปฌ๊ณผ ๋ฐฐํฌ ํ๊ฒฝ ๋ชจ๋์์ ์ ์๋ํด! ๐
โ ๏ธ ๋ฐฐํฌ ์ ์ฃผ์์ฌํญ
1. ํ์ผ ์์คํ
์ ๊ทผ ์ ํ
Deno Deploy๋ ํ์ผ ์์คํ
์ ๊ทผ์ด ์ ํ๋์ด ์์ด. ํ์ผ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ฌ์ฉํ ์ ์์ด.
2. ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ
API ํค๋ ๋น๋ฐ๋ฒํธ๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง๊ณ ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํด.
3. ๋ฉ๋ชจ๋ฆฌ ์ ํ
๋ฌด๋ฃ ํฐ์ด๋ ๋ฉ๋ชจ๋ฆฌ ์ ํ์ด ์์ด. ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ ์ฃผ์ํด์ผ ํด.
4. ์คํ ์๊ฐ ์ ํ
์์ฒญ๋น ์ต๋ ์คํ ์๊ฐ์ด ์์ด. ์ฅ์๊ฐ ์คํ๋๋ ์์
์ ์ ํฉํ์ง ์์.
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐํ๊ธฐ
์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ์๊ฒ ์ง? Deno์์ ์ฌ์ฉํ ์ ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ๋ค์ ์์๋ณด์! ๐พ
๐ MongoDB ์ฐ๊ฒฐ
MongoDB๋ Deno์ ๊ถํฉ์ด ์ ๋ง ์ข์. mongo ๋๋ผ์ด๋ฒ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ:
// deps.ts์ ์ถ๊ฐ
export { MongoClient } from "https://deno.land/x/mongo@v0.32.0/mod.ts";
// src/db/mongodb.ts
import { MongoClient } from "../../deps.ts";
const client = new MongoClient();
// MongoDB ์ฐ๊ฒฐ
await client.connect("mongodb://localhost:27017");
// ๋๋ MongoDB Atlas ์ฌ์ฉ
// await client.connect("mongodb+srv://username:password@cluster.mongodb.net");
const db = client.database("blog_api");
export const postsCollection = db.collection("posts");
console.log("โ
MongoDB ์ฐ๊ฒฐ ์ฑ๊ณต!");
์ปจํธ๋กค๋ฌ๋ฅผ MongoDB์ฉ์ผ๋ก ์์ :
// src/controllers/postController.ts (MongoDB ๋ฒ์ )
import { RouterContext, Status } from "../../deps.ts";
import { postsCollection } from "../db/mongodb.ts";
import { ObjectId } from "https://deno.land/x/mongo@v0.32.0/mod.ts";
export const getAllPosts = async (context: RouterContext<"/posts">) => {
const posts = await postsCollection.find({}).toArray();
context.response.body = {
success: true,
data: posts,
count: posts.length,
};
};
export const getPostById = async (context: RouterContext<"/posts/:id">) => {
const { id } = context.params;
try {
const post = await postsCollection.findOne({ _id: new ObjectId(id) });
if (!post) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "ํฌ์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.body = {
success: true,
data: post,
};
} catch (error) {
context.response.status = Status.BadRequest;
context.response.body = {
success: false,
message: "์๋ชป๋ ID ํ์์
๋๋ค",
};
}
};
export const createPost = async (context: RouterContext<"/posts">) => {
try {
const body = await context.request.body();
const postData = await body.value;
const insertId = await postsCollection.insertOne({
...postData,
createdAt: new Date(),
updatedAt: new Date(),
});
context.response.status = Status.Created;
context.response.body = {
success: true,
data: { id: insertId, ...postData },
message: "ํฌ์คํธ๊ฐ ์์ฑ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
๐ PostgreSQL ์ฐ๊ฒฐ
PostgreSQL์ ์ ํธํ๋ค๋ฉด postgres ๋๋ผ์ด๋ฒ๋ฅผ ์ฌ์ฉํด:
// deps.ts์ ์ถ๊ฐ
export { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
// src/db/postgres.ts
import { Client } from "../../deps.ts";
const client = new Client({
user: "your_username",
database: "blog_api",
hostname: "localhost",
port: 5432,
password: "your_password",
});
await client.connect();
// ํ
์ด๋ธ ์์ฑ
await client.queryArray(`
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author VARCHAR(100) NOT NULL,
tags TEXT[],
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log("โ
PostgreSQL ์ฐ๊ฒฐ ์ฑ๊ณต!");
export default client;
// ์ฌ์ฉ ์์
import client from "../db/postgres.ts";
export const getAllPosts = async (context: RouterContext<"/posts">) => {
const result = await client.queryObject(
"SELECT * FROM posts ORDER BY created_at DESC"
);
context.response.body = {
success: true,
data: result.rows,
count: result.rows.length,
};
};
๐ฅ Supabase ์ฐ๊ฒฐ (์ถ์ฒ!)
์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์๋ Supabase๊ฐ ์ ๋ง ์ข์ ์ ํ์ด์ผ. PostgreSQL ๊ธฐ๋ฐ์ด๋ฉด์ ์ค์๊ฐ ๊ธฐ๋ฅ๋ ์ ๊ณตํ๊ฑฐ๋ ! ๐
// deps.ts์ ์ถ๊ฐ
export { createClient } from "https://esm.sh/@supabase/supabase-js@2.38.4";
// src/db/supabase.ts
import { createClient } from "../../deps.ts";
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseKey = Deno.env.get("SUPABASE_KEY")!;
export const supabase = createClient(supabaseUrl, supabaseKey);
console.log("โ
Supabase ์ฐ๊ฒฐ ์ฑ๊ณต!");
// ์ฌ์ฉ ์์
import { supabase } from "../db/supabase.ts";
export const getAllPosts = async (context: RouterContext<"/posts">) => {
const { data, error } = await supabase
.from("posts")
.select("*")
.order("created_at", { ascending: false });
if (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: error.message,
};
return;
}
context.response.body = {
success: true,
data,
count: data.length,
};
};
export const createPost = async (context: RouterContext<"/posts">) => {
const body = await context.request.body();
const postData = await body.value;
const { data, error } = await supabase
.from("posts")
.insert([postData])
.select();
if (error) {
context.response.status = Status.BadRequest;
context.response.body = {
success: false,
message: error.message,
};
return;
}
context.response.status = Status.Created;
context.response.body = {
success: true,
data: data[0],
message: "ํฌ์คํธ๊ฐ ์์ฑ๋์์ต๋๋ค",
};
};
Supabase๋ ๋ฌด๋ฃ ํฐ์ด๋ ๋๋ํ๊ณ , Deno Deploy์ ์๋ฒฝํ๊ฒ ํธํ๋ผ. ์๋ฒ๋ฆฌ์ค API ๊ฐ๋ฐ์ ์ต์ ์ด์ผ! ๐
๐ ์ธ์ฆ๊ณผ ๋ณด์
API๋ฅผ ๋ง๋ค์์ผ๋ฉด ๋น์ฐํ ๋ณด์๋ ์ ๊ฒฝ ์จ์ผ๊ฒ ์ง? JWT ๊ธฐ๋ฐ ์ธ์ฆ์ ๊ตฌํํด๋ณด์! ๐
๐ซ JWT ์ธ์ฆ ๊ตฌํ
// deps.ts์ ์ถ๊ฐ
export { create, verify, getNumericDate } from "https://deno.land/x/djwt@v3.0.1/mod.ts";
export type { Payload } from "https://deno.land/x/djwt@v3.0.1/mod.ts";
// src/middleware/auth.ts
import { Context, Middleware, Status } from "../../deps.ts";
import { create, verify, getNumericDate } from "../../deps.ts";
import type { Payload } from "../../deps.ts";
const JWT_SECRET = Deno.env.get("JWT_SECRET") || "your-secret-key";
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(JWT_SECRET),
{ name: "HMAC", hash: "SHA-256" },
true,
["sign", "verify"]
);
export interface UserPayload extends Payload {
userId: string;
email: string;
role: string;
}
// JWT ํ ํฐ ์์ฑ
export async function generateToken(userId: string, email: string, role: string = "user"): Promise<string> {
const payload: UserPayload = {
userId,
email,
role,
exp: getNumericDate(60 * 60 * 24), // 24์๊ฐ
};
return await create({ alg: "HS256", typ: "JWT" }, payload, key);
}
// JWT ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
export const authMiddleware: Middleware = async (context: Context, next) => {
const authHeader = context.request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
context.response.status = Status.Unauthorized;
context.response.body = {
success: false,
message: "์ธ์ฆ ํ ํฐ์ด ํ์ํฉ๋๋ค",
};
return;
}
const token = authHeader.substring(7);
try {
const payload = await verify(token, key) as UserPayload;
context.state.user = payload;
await next();
} catch (error) {
context.response.status = Status.Unauthorized;
context.response.body = {
success: false,
message: "์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค",
error: error.message,
};
}
};
// ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด
export function requireRole(...roles: string[]): Middleware {
return async (context: Context, next) => {
const user = context.state.user as UserPayload;
if (!user || !roles.includes(user.role)) {
context.response.status = Status.Forbidden;
context.response.body = {
success: false,
message: "์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค",
};
return;
}
await next();
};
}
๐ค ์ฌ์ฉ์ ์ธ์ฆ ์ปจํธ๋กค๋ฌ
// src/controllers/authController.ts
import { RouterContext, Status } from "../../deps.ts";
import { generateToken } from "../middleware/auth.ts";
import { supabase } from "../db/supabase.ts";
export const register = async (context: RouterContext<"/auth/register">) => {
try {
const body = await context.request.body();
const { email, password, name } = await body.value;
// Supabase Auth ์ฌ์ฉ
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: { name }
}
});
if (error) {
context.response.status = Status.BadRequest;
context.response.body = {
success: false,
message: error.message,
};
return;
}
const token = await generateToken(data.user!.id, email, "user");
context.response.status = Status.Created;
context.response.body = {
success: true,
data: {
user: data.user,
token,
},
message: "ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const login = async (context: RouterContext<"/auth/login">) => {
try {
const body = await context.request.body();
const { email, password } = await body.value;
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
context.response.status = Status.Unauthorized;
context.response.body = {
success: false,
message: "์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค",
};
return;
}
const token = await generateToken(data.user.id, email, "user");
context.response.body = {
success: true,
data: {
user: data.user,
token,
},
message: "๋ก๊ทธ์ธ ์ฑ๊ณต",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const getProfile = async (context: RouterContext<"/auth/profile">) => {
const user = context.state.user;
context.response.body = {
success: true,
data: user,
};
};
๐ฃ๏ธ ์ธ์ฆ ๋ผ์ฐํธ ์ถ๊ฐ
// src/routes/authRoutes.ts
import { Router } from "../../deps.ts";
import { register, login, getProfile } from "../controllers/authController.ts";
import { authMiddleware } from "../middleware/auth.ts";
export const authRouter = new Router();
authRouter
.post("/auth/register", register)
.post("/auth/login", login)
.get("/auth/profile", authMiddleware, getProfile);
์ด์ ๋ณดํธ๋ ๋ผ์ฐํธ์ ์ธ์ฆ ๋ฏธ๋ค์จ์ด๋ฅผ ์ ์ฉํ ์ ์์ด:
// src/routes/postRoutes.ts ์์
import { authMiddleware, requireRole } from "../middleware/auth.ts";
postRouter
.get("/posts", getAllPosts) // ๊ณต๊ฐ
.get("/posts/:id", getPostById) // ๊ณต๊ฐ
.post("/posts", authMiddleware, createPost) // ์ธ์ฆ ํ์
.put("/posts/:id", authMiddleware, updatePost) // ์ธ์ฆ ํ์
.delete("/posts/:id", authMiddleware, requireRole("admin"), deletePost); // ๊ด๋ฆฌ์๋ง
๐ก ๋ณด์ ํ
1. ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ
JWT_SECRET ๊ฐ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ๋ง๊ณ ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌํด.
2. HTTPS ์ฌ์ฉ
ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ฐ๋์ HTTPS๋ฅผ ์ฌ์ฉํด. Deno Deploy๋ ์๋์ผ๋ก HTTPS๋ฅผ ์ ๊ณตํด.
3. Rate Limiting
๋ฌด์ฐจ๋ณ ๋์
๊ณต๊ฒฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์์ฒญ ์ ํ์ ๊ตฌํํด.
4. ์
๋ ฅ ๊ฒ์ฆ
๋ชจ๋ ์ฌ์ฉ์ ์
๋ ฅ์ ์ฒ ์ ํ ๊ฒ์ฆํ๊ณ sanitizeํด์ผ ํด.
5. CORS ์ค์
ํ์ํ ๋๋ฉ์ธ๋ง ํ์ฉํ๋๋ก CORS๋ฅผ ์ ํ์ ์ผ๋ก ์ค์ ํด.
๐ ์ฑ๋ฅ ์ต์ ํ์ ๋ชจ๋ํฐ๋ง
API๋ฅผ ๋ง๋ค์์ผ๋ฉด ์ฑ๋ฅ๋ ์ ๊ฒฝ ์จ์ผ๊ฒ ์ง? ๋ช ๊ฐ์ง ์ต์ ํ ๊ธฐ๋ฒ์ ์์๋ณด์! โก
๐ ์บ์ฑ ๊ตฌํ
// src/middleware/cache.ts
import { Context, Middleware } from "../../deps.ts";
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
class SimpleCache {
private cache: Map<string, CacheEntry> = new Map();
set(key: string, data: any, ttl: number = 60000): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
}
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
clear(): void {
this.cache.clear();
}
delete(key: string): void {
this.cache.delete(key);
}
}
export const cache = new SimpleCache();
export function cacheMiddleware(ttl: number = 60000): Middleware {
return async (context: Context, next) => {
const key = `${context.request.method}:${context.request.url.pathname}`;
// GET ์์ฒญ๋ง ์บ์ฑ
if (context.request.method === "GET") {
const cached = cache.get(key);
if (cached) {
context.response.headers.set("X-Cache", "HIT");
context.response.body = cached;
return;
}
}
await next();
// ์๋ต์ ์บ์์ ์ ์ฅ
if (context.request.method === "GET" && context.response.status === 200) {
cache.set(key, context.response.body, ttl);
context.response.headers.set("X-Cache", "MISS");
}
};
}
์ฌ์ฉ ์์:
// ํน์ ๋ผ์ฐํธ์ ์บ์ฑ ์ ์ฉ
postRouter.get(
"/posts",
cacheMiddleware(300000), // 5๋ถ ์บ์ฑ
getAllPosts
);
๐ Rate Limiting ๊ตฌํ
// src/middleware/rateLimit.ts
import { Context, Middleware, Status } from "../../deps.ts";
interface RateLimitEntry {
count: number;
resetTime: number;
}
class RateLimiter {
private limits: Map<string, RateLimitEntry> = new Map();
check(ip: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now();
const entry = this.limits.get(ip);
if (!entry || now > entry.resetTime) {
this.limits.set(ip, {
count: 1,
resetTime: now + windowMs,
});
return true;
}
if (entry.count >= maxRequests) {
return false;
}
entry.count++;
return true;
}
getRemainingRequests(ip: string, maxRequests: number): number {
const entry = this.limits.get(ip);
if (!entry) return maxRequests;
return Math.max(0, maxRequests - entry.count);
}
getResetTime(ip: string): number {
const entry = this.limits.get(ip);
return entry ? entry.resetTime : Date.now();
}
}
const limiter = new RateLimiter();
export function rateLimitMiddleware(
maxRequests: number = 100,
windowMs: number = 60000
): Middleware {
return async (context: Context, next) => {
const ip = context.request.ip;
if (!limiter.check(ip, maxRequests, windowMs)) {
const resetTime = limiter.getResetTime(ip);
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
context.response.status = Status.TooManyRequests;
context.response.headers.set("Retry-After", retryAfter.toString());
context.response.headers.set("X-RateLimit-Limit", maxRequests.toString());
context.response.headers.set("X-RateLimit-Remaining", "0");
context.response.headers.set("X-RateLimit-Reset", resetTime.toString());
context.response.body = {
success: false,
message: "์์ฒญ ํ๋๋ฅผ ์ด๊ณผํ์ต๋๋ค",
retryAfter,
};
return;
}
const remaining = limiter.getRemainingRequests(ip, maxRequests);
context.response.headers.set("X-RateLimit-Limit", maxRequests.toString());
context.response.headers.set("X-RateLimit-Remaining", remaining.toString());
await next();
};
}
์ ์ญ ๋๋ ํน์ ๋ผ์ฐํธ์ ์ ์ฉ:
// ์ ์ญ ์ ์ฉ
app.use(rateLimitMiddleware(100, 60000)); // ๋ถ๋น 100 ์์ฒญ
// ํน์ ๋ผ์ฐํธ์๋ง ์ ์ฉ
authRouter.post(
"/auth/login",
rateLimitMiddleware(5, 60000), // ๋ก๊ทธ์ธ์ ๋ถ๋น 5ํ๋ก ์ ํ
login
);
๐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
// src/middleware/metrics.ts
import { Context, Middleware } from "../../deps.ts";
interface Metrics {
totalRequests: number;
successfulRequests: number;
failedRequests: number;
averageResponseTime: number;
requestsByEndpoint: Map<string, number>;
responseTimesByEndpoint: Map<string, number[]>;
}
class MetricsCollector {
private metrics: Metrics = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
requestsByEndpoint: new Map(),
responseTimesByEndpoint: new Map(),
};
recordRequest(
endpoint: string,
responseTime: number,
success: boolean
): void {
this.metrics.totalRequests++;
if (success) {
this.metrics.successfulRequests++;
} else {
this.metrics.failedRequests++;
}
// ์๋ํฌ์ธํธ๋ณ ์์ฒญ ์
const count = this.metrics.requestsByEndpoint.get(endpoint) || 0;
this.metrics.requestsByEndpoint.set(endpoint, count + 1);
// ์๋ํฌ์ธํธ๋ณ ์๋ต ์๊ฐ
const times = this.metrics.responseTimesByEndpoint.get(endpoint) || [];
times.push(responseTime);
this.metrics.responseTimesByEndpoint.set(endpoint, times);
// ์ ์ฒด ํ๊ท ์๋ต ์๊ฐ ๊ณ์ฐ
this.calculateAverageResponseTime();
}
private calculateAverageResponseTime(): void {
let totalTime = 0;
let totalCount = 0;
for (const times of this.metrics.responseTimesByEndpoint.values()) {
totalTime += times.reduce((a, b) => a + b, 0);
totalCount += times.length;
}
this.metrics.averageResponseTime = totalCount > 0
? totalTime / totalCount
: 0;
}
getMetrics(): Metrics {
return { ...this.metrics };
}
getEndpointStats(endpoint: string) {
const times = this.metrics.responseTimesByEndpoint.get(endpoint) || [];
const count = this.metrics.requestsByEndpoint.get(endpoint) || 0;
if (times.length === 0) {
return null;
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);
return {
endpoint,
count,
averageResponseTime: avg,
minResponseTime: min,
maxResponseTime: max,
};
}
}
export const metricsCollector = new MetricsCollector();
export const metricsMiddleware: Middleware = async (context: Context, next) => {
const start = Date.now();
const endpoint = context.request.url.pathname;
try {
await next();
const responseTime = Date.now() - start;
const success = context.response.status < 400;
metricsCollector.recordRequest(endpoint, responseTime, success);
} catch (error) {
const responseTime = Date.now() - start;
metricsCollector.recordRequest(endpoint, responseTime, false);
throw error;
}
};
// ๋ฉํธ๋ฆญ ์กฐํ ์๋ํฌ์ธํธ
export const getMetrics = (context: Context) => {
const metrics = metricsCollector.getMetrics();
const endpointStats = Array.from(metrics.requestsByEndpoint.keys())
.map(endpoint => metricsCollector.getEndpointStats(endpoint))
.filter(stat => stat !== null);
context.response.body = {
success: true,
data: {
summary: {
totalRequests: metrics.totalRequests,
successfulRequests: metrics.successfulRequests,
failedRequests: metrics.failedRequests,
successRate: metrics.totalRequests > 0
? (metrics.successfulRequests / metrics.totalRequests * 100).toFixed(2) + "%"
: "0%",
averageResponseTime: metrics.averageResponseTime.toFixed(2) + "ms",
},
endpoints: endpointStats,
},
};
};
๋ฉํธ๋ฆญ ๋ผ์ฐํธ ์ถ๊ฐ:
// src/routes/metricsRoutes.ts
import { Router } from "../../deps.ts";
import { getMetrics } from "../middleware/metrics.ts";
import { authMiddleware, requireRole } from "../middleware/auth.ts";
export const metricsRouter = new Router();
metricsRouter.get(
"/metrics",
authMiddleware,
requireRole("admin"),
getMetrics
);
๐ฏ ์ฑ๋ฅ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
โ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์ต์ ํ - ์ธ๋ฑ์ค ํ์ฉ, N+1 ๋ฌธ์ ํด๊ฒฐ
โ
์บ์ฑ ์ ๋ต - ์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ ์บ์ฑ
โ
ํ์ด์ง๋ค์ด์
- ๋๋ ๋ฐ์ดํฐ๋ ํ์ด์ง ๋จ์๋ก ์กฐํ
โ
์์ถ - gzip ์์ถ์ผ๋ก ์ ์ก ๋ฐ์ดํฐ ํฌ๊ธฐ ์ค์ด๊ธฐ
โ
์ฐ๊ฒฐ ํ๋ง - ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ฌ์ฌ์ฉ
โ
๋น๋๊ธฐ ์ฒ๋ฆฌ - ๋ฌด๊ฑฐ์ด ์์
์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฒ๋ฆฌ
โ
CDN ํ์ฉ - ์ ์ ํ์ผ์ CDN์ผ๋ก ์๋น
โ
๋ชจ๋ํฐ๋ง - ์ฑ๋ฅ ์งํ๋ฅผ ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋ง
๐งช ํ ์คํธ ์์ฑํ๊ธฐ
์ข์ ์ฝ๋๋ ํ ์คํธ๊ฐ ํ์์ผ! Deno๋ ํ ์คํธ ๋ฌ๋๊ฐ ๋ด์ฅ๋์ด ์์ด์ ๋ณ๋ ์ค์น๊ฐ ํ์ ์์ด. ๐
โ ๊ธฐ๋ณธ ํ ์คํธ ์์ฑ
// tests/post_test.ts
import { assertEquals, assertExists } from "https://deno.land/std@0.208.0/assert/mod.ts";
import { PostStore } from "../src/models/Post.ts";
Deno.test("PostStore - ํฌ์คํธ ์์ฑ", () => {
const store = new PostStore();
const post = store.create({
title: "ํ
์คํธ ํฌ์คํธ",
content: "ํ
์คํธ ๋ด์ฉ",
author: "ํ
์คํฐ",
tags: ["test"],
});
assertExists(post.id);
assertEquals(post.title, "ํ
์คํธ ํฌ์คํธ");
assertEquals(post.author, "ํ
์คํฐ");
assertExists(post.createdAt);
});
Deno.test("PostStore - ํฌ์คํธ ์กฐํ", () => {
const store = new PostStore();
const created = store.create({
title: "์กฐํ ํ
์คํธ",
content: "๋ด์ฉ",
author: "์์ฑ์",
tags: [],
});
const found = store.findById(created.id);
assertExists(found);
assertEquals(found?.id, created.id);
assertEquals(found?.title, "์กฐํ ํ
์คํธ");
});
Deno.test("PostStore - ํฌ์คํธ ์์ ", () => {
const store = new PostStore();
const created = store.create({
title: "์๋ณธ ์ ๋ชฉ",
content: "์๋ณธ ๋ด์ฉ",
author: "์์ฑ์",
tags: [],
});
const updated = store.update(created.id, {
title: "์์ ๋ ์ ๋ชฉ",
});
assertExists(updated);
assertEquals(updated?.title, "์์ ๋ ์ ๋ชฉ");
assertEquals(updated?.content, "์๋ณธ ๋ด์ฉ");
});
Deno.test("PostStore - ํฌ์คํธ ์ญ์ ", () => {
const store = new PostStore();
const created = store.create({
title: "์ญ์ ํ ํฌ์คํธ",
content: "๋ด์ฉ",
author: "์์ฑ์",
tags: [],
});
const deleted = store.delete(created.id);
assertEquals(deleted, true);
const found = store.findById(created.id);
assertEquals(found, undefined);
});
Deno.test("PostStore - ํ๊ทธ๋ก ๊ฒ์", () => {
const store = new PostStore();
store.create({
title: "Deno ํฌ์คํธ",
content: "๋ด์ฉ",
author: "์์ฑ์",
tags: ["deno", "typescript"],
});
store.create({
title: "Oak ํฌ์คํธ",
content: "๋ด์ฉ",
author: "์์ฑ์",
tags: ["oak", "framework"],
});
const denoPosts = store.findByTag("deno");
assertEquals(denoPosts.length, 1);
assertEquals(denoPosts[0].title, "Deno ํฌ์คํธ");
});
ํ ์คํธ ์คํ:
deno test
ํน์ ํ์ผ๋ง ํ ์คํธ:
deno test tests/post_test.ts
์ปค๋ฒ๋ฆฌ์ง ํ์ธ:
deno test --coverage=coverage
deno coverage coverage
๐ API ํตํฉ ํ ์คํธ
// tests/api_test.ts
import { assertEquals } from "https://deno.land/std@0.208.0/assert/mod.ts";
import { superoak } from "https://deno.land/x/superoak@4.7.0/mod.ts";
import app from "../src/app.ts";
Deno.test("GET /posts - ๋ชจ๋ ํฌ์คํธ ์กฐํ", async () => {
const request = await superoak(app);
await request
.get("/posts")
.expect(200)
.expect("Content-Type", /json/)
.expect((res) => {
assertEquals(res.body.success, true);
assertExists(res.body.data);
assertExists(res.body.count);
});
});
Deno.test("POST /posts - ์ ํฌ์คํธ ์์ฑ", async () => {
const request = await superoak(app);
const newPost = {
title: "API ํ
์คํธ ํฌ์คํธ",
content: "ํ
์คํธ ๋ด์ฉ์
๋๋ค",
author: "ํ
์คํฐ",
tags: ["test", "api"],
};
await request
.post("/posts")
.send(newPost)
.expect(201)
.expect("Content-Type", /json/)
.expect((res) => {
assertEquals(res.body.success, true);
assertEquals(res.body.data.title, newPost.title);
assertExists(res.body.data.id);
});
});
Deno.test("GET /posts/:id - ํน์ ํฌ์คํธ ์กฐํ", async () => {
const request = await superoak(app);
// ๋จผ์ ํฌ์คํธ ์์ฑ
const createRes = await request
.post("/posts")
.send({
title: "์กฐํ ํ
์คํธ",
content: "๋ด์ฉ",
author: "์์ฑ์",
tags: [],
});
const postId = createRes.body.data.id;
// ์์ฑ๋ ํฌ์คํธ ์กฐํ
await request
.get(`/posts/${postId}`)
.expect(200)
.expect((res) => {
assertEquals(res.body.success, true);
assertEquals(res.body.data.id, postId);
});
});
Deno.test("GET /posts/invalid-id - ์กด์ฌํ์ง ์๋ ํฌ์คํธ", async () => {
const request = await superoak(app);
await request
.get("/posts/invalid-id-12345")
.expect(404)
.expect((res) => {
assertEquals(res.body.success, false);
});
});
๐ก ํ ์คํธ ์์ฑ ํ
1. ํ
์คํธ๋ ๋
๋ฆฝ์ ์ผ๋ก
๊ฐ ํ
์คํธ๋ ๋ค๋ฅธ ํ
์คํธ์ ์์กดํ์ง ์์์ผ ํด. ์์์ ์๊ด์์ด ์คํ๋ ์ ์์ด์ผ ํด.
2. AAA ํจํด ์ฌ์ฉ
Arrange(์ค๋น), Act(์คํ), Assert(๊ฒ์ฆ) ํจํด์ผ๋ก ํ
์คํธ๋ฅผ ๊ตฌ์กฐํํด.
3. ์๋ฏธ ์๋ ํ
์คํธ ์ด๋ฆ
ํ
์คํธ ์ด๋ฆ๋ง ๋ด๋ ๋ฌด์์ ํ
์คํธํ๋์ง ์ ์ ์์ด์ผ ํด.
4. ์ฃ์ง ์ผ์ด์ค ํ
์คํธ
์ ์ ์ผ์ด์ค๋ฟ๋ง ์๋๋ผ ์๋ฌ ์ผ์ด์ค๋ ๊ผญ ํ
์คํธํด.
5. ๋ชจํน ํ์ฉ
์ธ๋ถ ์์กด์ฑ์ ๋ชจํนํด์ ํ
์คํธ๋ฅผ ๋น ๋ฅด๊ณ ์์ ์ ์ผ๋ก ๋ง๋ค์ด.
๐ ์ค์ ํ๋ก์ ํธ ์์
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ TODO API๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ. ๐
๐ฏ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ
๊ธฐ๋ฅ ์๊ตฌ์ฌํญ:
โข ์ฌ์ฉ์ ํ์๊ฐ์
/๋ก๊ทธ์ธ
โข TODO ์์ฑ/์กฐํ/์์ /์ญ์
โข TODO ์๋ฃ ์ํ ํ ๊ธ
โข ์ฌ์ฉ์๋ณ TODO ๋ชฉ๋ก ์กฐํ
โข ์ฐ์ ์์๋ณ ์ ๋ ฌ
โข ๊ฒ์ ๊ธฐ๋ฅ
๊ธฐ์ ์คํ:
โข Deno + Oak
โข Supabase (PostgreSQL)
โข JWT ์ธ์ฆ
โข TypeScript
๐ ๋ฐ์ดํฐ ๋ชจ๋ธ
// src/models/Todo.ts
export interface Todo {
id: string;
user_id: string;
title: string;
description: string;
completed: boolean;
priority: "low" | "medium" | "high";
due_date: Date | null;
created_at: Date;
updated_at: Date;
}
export interface CreateTodoDto {
title: string;
description?: string;
priority?: "low" | "medium" | "high";
due_date?: string;
}
export interface UpdateTodoDto {
title?: string;
description?: string;
completed?: boolean;
priority?: "low" | "medium" | "high";
due_date?: string;
}
๐ฎ TODO ์ปจํธ๋กค๋ฌ
// src/controllers/todoController.ts
import { RouterContext, Status } from "../../deps.ts";
import { supabase } from "../db/supabase.ts";
import type { CreateTodoDto, UpdateTodoDto } from "../models/Todo.ts";
export const getTodos = async (context: RouterContext<"/todos">) => {
const userId = context.state.user.userId;
const url = context.request.url;
// ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํ์ฑ
const completed = url.searchParams.get("completed");
const priority = url.searchParams.get("priority");
const search = url.searchParams.get("search");
const sortBy = url.searchParams.get("sortBy") || "created_at";
const order = url.searchParams.get("order") || "desc";
let query = supabase
.from("todos")
.select("*")
.eq("user_id", userId);
// ํํฐ ์ ์ฉ
if (completed !== null) {
query = query.eq("completed", completed === "true");
}
if (priority) {
query = query.eq("priority", priority);
}
if (search) {
query = query.or(`title.ilike.%${search}%,description.ilike.%${search}%`);
}
// ์ ๋ ฌ
query = query.order(sortBy, { ascending: order === "asc" });
const { data, error } = await query;
if (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: error.message,
};
return;
}
context.response.body = {
success: true,
data,
count: data.length,
};
};
export const getTodoById = async (context: RouterContext<"/todos/:id">) => {
const userId = context.state.user.userId;
const { id } = context.params;
const { data, error } = await supabase
.from("todos")
.select("*")
.eq("id", id)
.eq("user_id", userId)
.single();
if (error || !data) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.body = {
success: true,
data,
};
};
export const createTodo = async (context: RouterContext<"/todos">) => {
const userId = context.state.user.userId;
try {
const body = await context.request.body();
const todoData: CreateTodoDto = await body.value;
// ์ ํจ์ฑ ๊ฒ์ฌ
if (!todoData.title || todoData.title.trim() === "") {
context.response.status = Status.BadRequest;
context.response.body = {
success: false,
message: "์ ๋ชฉ์ ํ์ ํญ๋ชฉ์
๋๋ค",
};
return;
}
const { data, error } = await supabase
.from("todos")
.insert([
{
user_id: userId,
title: todoData.title,
description: todoData.description || "",
priority: todoData.priority || "medium",
due_date: todoData.due_date || null,
completed: false,
},
])
.select()
.single();
if (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: error.message,
};
return;
}
context.response.status = Status.Created;
context.response.body = {
success: true,
data,
message: "TODO๊ฐ ์์ฑ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const updateTodo = async (context: RouterContext<"/todos/:id">) => {
const userId = context.state.user.userId;
const { id } = context.params;
try {
const body = await context.request.body();
const updates: UpdateTodoDto = await body.value;
const { data, error } = await supabase
.from("todos")
.update({
...updates,
updated_at: new Date().toISOString(),
})
.eq("id", id)
.eq("user_id", userId)
.select()
.single();
if (error || !data) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.body = {
success: true,
data,
message: "TODO๊ฐ ์์ ๋์์ต๋๋ค",
};
} catch (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค",
error: error.message,
};
}
};
export const deleteTodo = async (context: RouterContext<"/todos/:id">) => {
const userId = context.state.user.userId;
const { id } = context.params;
const { error } = await supabase
.from("todos")
.delete()
.eq("id", id)
.eq("user_id", userId);
if (error) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
context.response.status = Status.NoContent;
};
export const toggleTodo = async (context: RouterContext<"/todos/:id/toggle">) => {
const userId = context.state.user.userId;
const { id } = context.params;
// ํ์ฌ ์ํ ์กฐํ
const { data: currentTodo, error: fetchError } = await supabase
.from("todos")
.select("completed")
.eq("id", id)
.eq("user_id", userId)
.single();
if (fetchError || !currentTodo) {
context.response.status = Status.NotFound;
context.response.body = {
success: false,
message: "TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
};
return;
}
// ์ํ ํ ๊ธ
const { data, error } = await supabase
.from("todos")
.update({
completed: !currentTodo.completed,
updated_at: new Date().toISOString(),
})
.eq("id", id)
.eq("user_id", userId)
.select()
.single();
if (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: error.message,
};
return;
}
context.response.body = {
success: true,
data,
message: `TODO๊ฐ ${data.completed ? "์๋ฃ" : "๋ฏธ์๋ฃ"} ์ํ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค`,
};
};
export const getStats = async (context: RouterContext<"/todos/stats">) => {
const userId = context.state.user.userId;
const { data: todos, error } = await supabase
.from("todos")
.select("*")
.eq("user_id", userId);
if (error) {
context.response.status = Status.InternalServerError;
context.response.body = {
success: false,
message: error.message,
};
return;
}
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
const pending = total - completed;
const byPriority = {
high: todos.filter(t => t.priority === "high").length,
medium: todos.filter(t => t.priority === "medium").length,
low: todos.filter(t => t.priority === "low").length,
};
context.response.body = {
success: true,
data: {
total,
completed,
pending,
completionRate: total > 0 ? ((completed / total) * 100).toFixed(2) + "%" : "0%",
byPriority,
},
};
};
๐ฃ๏ธ TODO ๋ผ์ฐํธ
// src/routes/todoRoutes.ts
import { Router } from "../../deps.ts";
import {
getTodos,
getTodoById,
createTodo,
updateTodo,
deleteTodo,
toggleTodo,
getStats,
} from "../controllers/todoController.ts";
import { authMiddleware } from "../middleware/auth.ts";
export const todoRouter = new Router();
// ๋ชจ๋ ๋ผ์ฐํธ์ ์ธ์ฆ ํ์
todoRouter
.get("/todos/stats", authMiddleware, getStats)
.get("/todos", authMiddleware, getTodos)
.get("/todos/:id", authMiddleware, getTodoById)
.post("/todos", authMiddleware, createTodo)
.put("/todos/:id", authMiddleware, updateTodo)
.patch("/todos/:id/toggle", authMiddleware, toggleTodo)
.delete("/todos/:id", authMiddleware, deleteTodo);
๐ ์์ฑ๋ ์ ํ๋ฆฌ์ผ์ด์
// src/app.ts
import { Application } from "../deps.ts";
import { authRouter } from "./routes/authRoutes.ts";
import { todoRouter } from "./routes/todoRoutes.ts";
import { metricsRouter } from "./routes/metricsRoutes.ts";
import {
logger,
errorHandler,
corsMiddleware,
} from "./middleware/logger.ts";
import { metricsMiddleware } from "./middleware/metrics.ts";
import { rateLimitMiddleware } from "./middleware/rateLimit.ts";
const app = new Application();
// ์ ์ญ ๋ฏธ๋ค์จ์ด
app.use(errorHandler);
app.use(logger);
app.use(corsMiddleware);
app.use(metricsMiddleware);
app.use(rateLimitMiddleware(100, 60000));
// ๋ผ์ฐํฐ ๋ฑ๋ก
app.use(authRouter.routes());
app.use(authRouter.allowedMethods());
app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
app.use(metricsRouter.routes());
app.use(metricsRouter.allowedMethods());
// ํฌ์ค ์ฒดํฌ
app.use((context) => {
if (context.request.url.pathname === "/health") {
context.response.body = {
status: "healthy",
timestamp: new Date().toISOString(),
};
return;
}
// 404
context.response.status = 404;
context.response.body = {
success: false,
message: "์์ฒญํ์ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค",
path: context.request.url.pathname,
};
});
export default app;
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
Deno๋ฅผ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด? ์ ์ฉํ ๋ฆฌ์์ค๋ค์ ์๊ฐํ ๊ฒ! ๐
๐ ๊ณต์ ๋ฌธ์ ๋ฐ ๊ฐ์ด๋
Deno ๊ณต์ ๋ฌธ์
deno.land/manual - ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด
Oak ๋ฌธ์
oakserver.github.io/oak - Oak ํ๋ ์์ํฌ ๊ณต์ ๋ฌธ์
Deno by Example
examples.deno.land - ์ค์ฉ์ ์ธ ์์ ๋ชจ์
Deno Deploy ๋ฌธ์
deno.com/deploy/docs - ๋ฐฐํฌ ๊ด๋ จ ์์ธ ๊ฐ์ด๋
๐ฌ ์ปค๋ฎค๋ํฐ
Discord
Deno ๊ณต์ Discord ์๋ฒ - ์ค์๊ฐ์ผ๋ก ์ง๋ฌธํ๊ณ ๋ต๋ณ๋ฐ์ ์ ์์ด
GitHub
github.com/denoland/deno - ์ด์ ํธ๋ํน, ๊ธฐ์ฌํ๊ธฐ
Reddit
r/Deno - ๋ค์ํ ํ ๋ก ๊ณผ ์ ๋ณด ๊ณต์
Stack Overflow
#deno ํ๊ทธ - ๊ธฐ์ ์ ์ธ ์ง๋ฌธ๊ณผ ๋ต๋ณ
๐ฅ ์ถ์ฒ ํ์ต ์๋ฃ
YouTube ์ฑ๋
โข Traversy Media - Deno ํฌ๋์ ์ฝ์ค
โข Fireship - Deno in 100 Seconds
โข The Net Ninja - Deno ํํ ๋ฆฌ์ผ ์๋ฆฌ์ฆ
์จ๋ผ์ธ ๊ฐ์
โข Udemy - Deno: The Complete Guide
โข Pluralsight - Getting Started with Deno
โข Frontend Masters - Deno Fundamentals
๋ธ๋ก๊ทธ ๋ฐ ์ํฐํด
โข Dev.to #deno ํ๊ทธ
โข Medium - Deno ๊ด๋ จ ๊ธ๋ค
โข Hashnode - Deno ์ปค๋ฎค๋ํฐ
๐ ๋ค์ ๋จ๊ณ: ๋ ๋์๊ฐ๊ธฐ
๊ธฐ๋ณธ์ ๋ง์คํฐํ๋ค๋ฉด ์ด์ ๋ ๊ณ ๊ธ ์ฃผ์ ๋ก ๋์๊ฐ ์๊ฐ์ด์ผ! ๐ฏ
๐ฅ ๊ณ ๊ธ ์ฃผ์ ๋ค
1. WebSocket ์ค์๊ฐ ํต์
์ฑํ
์ฑ์ด๋ ์ค์๊ฐ ์๋ฆผ ์์คํ
์ ๋ง๋ค์ด๋ด. Oak๋ WebSocket์ ์ ์ง์ํด!
2. GraphQL API
REST ๋์ GraphQL์ ์ฌ์ฉํด๋ณด๋ ๊ฒ๋ ์ข์. gql ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด๋ด.
3. ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ
์ฌ๋ฌ Deno ์๋น์ค๋ฅผ ์กฐํฉํด์ ๋ง์ดํฌ๋ก์๋น์ค๋ฅผ ๊ตฌ์ถํด๋ด.
4. ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง (SSR)
Fresh ํ๋ ์์ํฌ๋ก SSR์ ๊ฒฝํํด๋ด. Deno์ ๋ ๋ค๋ฅธ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ด์ผ!
5. CLI ๋๊ตฌ ๊ฐ๋ฐ
Deno๋ก ์ปค๋งจ๋๋ผ์ธ ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ด. ์ปดํ์ผํด์ ์คํ ํ์ผ๋ก ๋ฐฐํฌํ ์๋ ์์ด!
6. ๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ
ETL ์์
์ด๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ์ Deno๋ก ๊ตฌ์ถํด๋ด.
7. ํฌ๋ก ์์
์ค์ผ์ค๋ง
์ ๊ธฐ์ ์ธ ์์
์ ์๋ํํ๋ ์ค์ผ์ค๋ฌ๋ฅผ ๋ง๋ค์ด๋ด.
๐ฏ ํ๋ก์ ํธ ์์ด๋์ด
์ค๋ ฅ์ ํค์ฐ๋ ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ ์ง์ ๋ง๋ค์ด๋ณด๋ ๊ฑฐ์ผ! ์ฌ๊ธฐ ๋ช ๊ฐ์ง ํ๋ก์ ํธ ์์ด๋์ด๋ฅผ ์ ์ํ ๊ฒ:
์ด๊ธ
โข URL ๋จ์ถ ์๋น์ค
โข ๊ฐ๋จํ ๋ธ๋ก๊ทธ API
โข ๋ ์จ ์ ๋ณด API
โข ํ ์ผ ๊ด๋ฆฌ ์ฑ
์ค๊ธ
โข ์์
๋ฏธ๋์ด API
โข ์ ์์๊ฑฐ๋ ๋ฐฑ์๋
โข ํ์ผ ์
๋ก๋ ์๋น์ค
โข ์ค์๊ฐ ์ฑํ
์ฑ
๊ณ ๊ธ
โข ๋น๋์ค ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ
โข ํ์
๋๊ตฌ (Notion ํด๋ก )
โข ๊ฒ์ ์๋ฒ
โข IoT ๋ฐ์ดํฐ ์์ง ํ๋ซํผ
๐ก ์ค๋ฌด ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค์ ํ๋ก์ ํธ์์ ์ ์ฉํ ํ๋ค์ ๊ณต์ ํ ๊ฒ! ์ด๊ฑด ๊ฒฝํ์์ ์ฐ๋ฌ๋์จ ์กฐ์ธ๋ค์ด์ผ. ๐
โ ๏ธ ํํ ์ค์๋ค
1. ๊ถํ ํ๋๊ทธ ๋จ์ฉ
--allow-all์ ํธํ์ง๋ง ๋ณด์์ ์ํํด. ํ์ํ ๊ถํ๋ง ๋ช
์์ ์ผ๋ก ๋ถ์ฌํด.
2. ์๋ฌ ์ฒ๋ฆฌ ์ํ
try-catch๋ฅผ ๋นผ๋จน์ง ๋ง๊ณ , ์๋ฏธ ์๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ฐํํด.
3. ํ์
๋ฌด์
TypeScript๋ฅผ ์ฐ๋ฉด์ any๋ฅผ ๋จ๋ฐํ๋ฉด ์๋ฏธ๊ฐ ์์ด. ์ ๋๋ก ๋ ํ์
์ ์ ์ํด.
4. ํ๊ฒฝ ๋ณ์ ํ๋์ฝ๋ฉ
API ํค๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์ฝ๋์ ์ง์ ๋ฃ์ง ๋ง. ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํด.
5. ํ
์คํธ ์๋ต
"๋์ค์ ํ
์คํธ ์์ฑํ์ง ๋ญ"๋ ์ ๋ ์ค์ง ์์. ์ฒ์๋ถํฐ ํ
์คํธ๋ฅผ ์์ฑํด.
โ ๋ฒ ์คํธ ํ๋ํฐ์ค
1. ํ๋ก์ ํธ ๊ตฌ์กฐ
์ผ๊ด๋ ํด๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํด. controllers, models, routes, middleware ๋ฑ์ผ๋ก ๋ช
ํํ ๋ถ๋ฆฌ.
2. ์์กด์ฑ ๊ด๋ฆฌ
deps.ts ํ์ผ์ ๋ชจ๋ ์ธ๋ถ ์์กด์ฑ์ ๋ชจ์๋๊ณ , ๋ฒ์ ์ ๋ช
์ํด.
3. ํ๊ฒฝ ์ค์
.env ํ์ผ์ ์ฌ์ฉํ๊ณ , .env.example์ ์ ๊ณตํด์ ํ์ํ ํ๊ฒฝ ๋ณ์๋ฅผ ๋ฌธ์ํํด.
4. ๋ก๊น
์ ์ ํ ๋ก๊ทธ ๋ ๋ฒจ(debug, info, warn, error)์ ์ฌ์ฉํด์ ๋๋ฒ๊น
์ ์ฝ๊ฒ ๋ง๋ค์ด.
5. API ๋ฌธ์ํ
Swagger๋ OpenAPI๋ฅผ ์ฌ์ฉํด์ API๋ฅผ ๋ฌธ์ํํด. ํ ํ์
์ ํ์์ผ!
6. ๋ฒ์ ๊ด๋ฆฌ
API ๋ฒ์ ์ URL์ ํฌํจ์์ผ (/api/v1/...) ํ์ ํธํ์ฑ์ ์ ์งํด.
7. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
์๋ต ์๊ฐ, ์๋ฌ์จ ๋ฑ์ ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํด.
8. ๋ณด์
HTTPS ์ฌ์ฉ, CORS ์ค์ , Rate Limiting, ์
๋ ฅ ๊ฒ์ฆ์ ์ฒ ์ ํ ํด.
๐ ์ฝ๋ ์คํ์ผ ๊ฐ์ด๋
// ์ข์ ์์ โ
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
export async function createUser(
email: string,
password: string,
name: string
): Promise<User | null> {
try {
// ์ ํจ์ฑ ๊ฒ์ฌ
if (!isValidEmail(email)) {
throw new Error("Invalid email format");
}
// ๋น์ฆ๋์ค ๋ก์ง
const hashedPassword = await hashPassword(password);
const user = await db.users.create({
email,
password: hashedPassword,
name,
});
return user;
} catch (error) {
console.error("Failed to create user:", error);
return null;
}
}
// ๋์ ์์ โ
export async function createUser(e: any, p: any, n: any): Promise<any> {
const u = await db.users.create({ email: e, password: p, name: n });
return u;
}
์ฝ๋ ์คํ์ผ ํฌ์ธํธ:
โข ๋ช
ํํ ๋ณ์๋ช
์ฌ์ฉ
โข ํ์
์ ๋ช
์์ ์ผ๋ก ์ ์
โข ์๋ฌ ์ฒ๋ฆฌ ํฌํจ
โข ์ฃผ์์ผ๋ก ์๋ ์ค๋ช
โข ์ผ๊ด๋ ๋ค์ด๋ฐ ์ปจ๋ฒค์
(camelCase, PascalCase)
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ฐ๋ฆฌ๋ Deno์ ๊ธฐ์ด๋ถํฐ Oak ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ ์ค์ API ๊ฐ๋ฐ, ๊ทธ๋ฆฌ๊ณ ์๋ฒ๋ฆฌ์ค ๋ฐฐํฌ๊น์ง ๋ชจ๋ ๊ณผ์ ์ ํจ๊ป ์ดํด๋ดค์ด.
Deno๋ ์ ๋ง ๋งค๋ ฅ์ ์ธ ๋ฐํ์์ด์ผ. Node.js์ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ๋ฉด์๋, ํ๋์ ์ธ ๊ฐ๋ฐ ๊ฒฝํ์ ์ ๊ณตํ์ง. TypeScript ๋ค์ดํฐ๋ธ ์ง์, ๋ณด์ ์ฐ์ ์ค๊ณ, ๋ด์ฅ ๋๊ตฌ๋ค... ์ด ๋ชจ๋ ๊ฒ ๊ฐ๋ฐ์์ ์ถ์ ๋ ํธํ๊ฒ ๋ง๋ค์ด์ค. ๐
Oak ํ๋ ์์ํฌ๋ Deno ์ํ๊ณ์์ ๊ฐ์ฅ ์ฑ์ํ ์น ํ๋ ์์ํฌ์ผ. Express์ ํธ๋ฆฌํจ๊ณผ Koa์ ์ฐ์ํจ์ ๋ชจ๋ ๊ฐ์ง๊ณ ์์ด์, RESTful API๋ฅผ ๋ง๋ค๊ธฐ์ ์ ๋ง ์ข์. ํนํ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ๊ณผ์ ๊ถํฉ์ด ์๋ฒฝํ๋ค๋ ๊ฒ ํฐ ์ฅ์ ์ด์ง!
๐ฏ ํต์ฌ ์์ฝ
Deno์ ์ฅ์
โ
๋ณด์ ์ฐ์ ์ค๊ณ
โ
TypeScript ๋ค์ดํฐ๋ธ ์ง์
โ
ํ์ค ์น API ์ฌ์ฉ
โ
๋ด์ฅ ๋๊ตฌ๋ค (ํฌ๋งคํฐ, ๋ฆฐํฐ, ํ
์คํธ ๋ฌ๋)
โ
URL ๊ธฐ๋ฐ ๋ชจ๋ ์์คํ
Oak์ ์ฅ์
โ
์ง๊ด์ ์ธ API
โ
๊ฐ๋ ฅํ ๋ฏธ๋ค์จ์ด ์์คํ
โ
TypeScript ์นํ์
โ
ํ๋ฐํ ์ปค๋ฎค๋ํฐ
โ
์๋ฒ๋ฆฌ์ค ๋ฐฐํฌ ์ต์ ํ
์๋ฒ๋ฆฌ์ค์ ์ฅ์
โ
์๋ฒ ๊ด๋ฆฌ ๋ถํ์
โ
์๋ ์ค์ผ์ผ๋ง
โ
์ฌ์ฉํ ๋งํผ๋ง ๊ณผ๊ธ
โ
์ ์ญ CDN ํ์ฉ
โ
๋น ๋ฅธ ๋ฐฐํฌ
์ด์ ๋น์ ๋ Deno์ Oak๋ก ๋ฉ์ง API๋ฅผ ๋ง๋ค ์ ์์ด! ์ฒ์์๋ ๋ฏ์ค ์ ์์ง๋ง, ์กฐ๊ธ๋ง ์ต์ํด์ง๋ฉด Node.js๋ณด๋ค ํจ์ฌ ํธํ๋ค๋ ๊ฑธ ๋๋ ๊ฑฐ์ผ. ๐
ํน์ Deno๋ Oak ๊ด๋ จํด์ ๋ ๋ฐฐ์ฐ๊ณ ์ถ๊ฑฐ๋, ํ๋ก์ ํธ์ ๋์์ด ํ์ํ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ๋ค์ ๋์์ ๋ฐ์ ์๋ ์์ด. ์ค์ ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด ๋ง์ด ํ๋ํ๊ณ ์๊ฑฐ๋ !
๋ง์ง๋ง์ผ๋ก ํ ๊ฐ์ง ์กฐ์ธ์ ํ์๋ฉด, ์ด๋ก ๋ง ๊ณต๋ถํ์ง ๋ง๊ณ ์ง์ ๋ง๋ค์ด๋ด! ์์ ํ๋ก์ ํธ๋ผ๋ ์ข์ผ๋๊น ์ฒ์๋ถํฐ ๋๊น์ง ์์ฑํด๋ณด๋ ๊ฒฝํ์ด ์ ๋ง ์ค์ํด. ๊ทธ ๊ณผ์ ์์ ๋ง๋๋ ์๋ฌ๋ค, ํด๊ฒฐํ๋ ๊ณผ์ ๋ค์ด ๋ชจ๋ ๋น์ ์ ์ค๋ ฅ์ด ๋๋ ๊ฑฐ์ผ. ๐ช
๐ ๋ค์ ์คํ ์ ์
1์ฃผ์ฐจ - Deno ๊ธฐ์ด ๋ค์ง๊ธฐ, ๊ฐ๋จํ CLI ๋๊ตฌ ๋ง๋ค๊ธฐ
2์ฃผ์ฐจ - Oak๋ก ๊ธฐ๋ณธ CRUD API ๊ตฌํ
3์ฃผ์ฐจ - ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ, ์ธ์ฆ ๊ตฌํ
4์ฃผ์ฐจ - ํ
์คํธ ์์ฑ, ๋ฐฐํฌ
5์ฃผ์ฐจ ์ดํ - ์ค์ ํ๋ก์ ํธ ์งํ, ํฌํธํด๋ฆฌ์ค ๊ตฌ์ถ
์ด๋ ๊ฒ ๋จ๊ณ์ ์ผ๋ก ์ ๊ทผํ๋ฉด ํ ๋ฌ ์์ ์ค์ ํฌ์
๊ฐ๋ฅํ ์์ค์ด ๋ ์ ์์ด!
์, ์ด์ ๋น์ ์ ์ฐจ๋ก์ผ! ๐ ์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋ฉ์ง API๋ฅผ ๋ง๋ค์ด๋ด. ๊ทธ๋ฆฌ๊ณ ๋ง๋ ํ๋ก์ ํธ๋ฅผ GitHub์ ์ฌ๋ฆฌ๊ณ , ์ปค๋ฎค๋ํฐ์ ๊ณต์ ํด๋ด. ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ํผ๋๋ฐฑ์ ๋ฐ์ผ๋ฉด์ ์ฑ์ฅํ๋ ๊ฑฐ์ผ!
Deno์ Oak์ ์ธ๊ณ๋ ์์ง ํํํ ๊ฒ ๋ง์. ์ด ๊ธ์ด ๊ทธ ์ฌ์ ์ ์ข์ ์ถ๋ฐ์ ์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด. ํ์ดํ
! ๐ช๐ฆ
์ด ๊ธ์ด ๋์์ด ๋์๋์?
Deno์ Oak๋ก ๋ฉ์ง ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์! ๐
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ ์ธ์ ๋ ํ์์
๋๋ค! ๐ฌ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.