์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Deno์™€ Oak ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์„œ๋ฒ„๋ฆฌ์Šค API ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ Deno์™€ Oak ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์„œ๋ฒ„๋ฆฌ์Šค API ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ์ฐจ์„ธ๋Œ€ JavaScript ๋Ÿฐํƒ€์ž„ ํ™œ์šฉ๋ฒ•

์•ˆ๋…•! ๐Ÿ‘‹ ์˜ค๋Š˜์€ ์š”์ฆ˜ ๊ฐœ๋ฐœ์ž๋“ค ์‚ฌ์ด์—์„œ ํ•ซํ•œ ํ™”์ œ์ธ Deno์™€ Oak ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ™œ์šฉํ•œ ์„œ๋ฒ„๋ฆฌ์Šค API ๊ฐœ๋ฐœ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. Node.js์˜ ์ฐฝ์‹œ์ž๊ฐ€ ๋งŒ๋“  ์ฐจ์„ธ๋Œ€ ๋Ÿฐํƒ€์ž„์ธ Deno, ๋“ค์–ด๋Š” ๋ดค๋Š”๋ฐ ๋ง‰์ƒ ์–ด๋–ป๊ฒŒ ์‹œ์ž‘ํ•ด์•ผ ํ• ์ง€ ๋ง‰๋ง‰ํ–ˆ๋‹ค๋ฉด? ์ด ๊ธ€์ด ๋”ฑ ๋„ˆ๋ฅผ ์œ„ํ•œ ๊ฑฐ์•ผ! ๐Ÿ˜Š

์„œ๋ฒ„๋ฆฌ์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ์ด์ œ ์„ ํƒ์ด ์•„๋‹Œ ํ•„์ˆ˜๊ฐ€ ๋˜์–ด๊ฐ€๊ณ  ์žˆ์–ด. ํŠนํžˆ ์Šคํƒ€ํŠธ์—…์ด๋‚˜ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ๋น ๋ฅด๊ฒŒ MVP๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•  ๋•Œ, Deno์™€ Oak์˜ ์กฐํ•ฉ์€ ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋ฌด๊ธฐ๊ฐ€ ๋˜์–ด์ค„ ๊ฑฐ์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ์ตœ์‹  ๊ธฐ์ˆ  ์Šคํƒ์„ ํ™œ์šฉํ•œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค์ด ์ ์  ๋Š˜์–ด๋‚˜๊ณ  ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค! ๐Ÿ”ฅ

Deno + Oak Framework Serverless API Development ๐Ÿฆ• API โ˜๏ธ

๐Ÿค” 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์˜ ์„ธ๊ณ„๋Š” ์•„์ง ํƒํ—˜ํ•  ๊ฒŒ ๋งŽ์•„. ์ด ๊ธ€์ด ๊ทธ ์—ฌ์ •์˜ ์ข‹์€ ์ถœ๋ฐœ์ ์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ํ™”์ดํŒ…! ๐Ÿ’ช๐Ÿฆ•

Happy Coding! Deno + Oak = ๐Ÿš€ ๐Ÿฆ• ๐ŸŒฒ โ˜๏ธ

์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”? Deno์™€ Oak๋กœ ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐ŸŽ‰
์งˆ๋ฌธ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์€ ์–ธ์ œ๋“  ํ™˜์˜์ž…๋‹ˆ๋‹ค! ๐Ÿ’ฌ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.