์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Node.js์™€ TypeScript๋กœ ๋งŒ๋“œ๋Š” Express ๋ฐฑ์—”๋“œ API ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ Node.js์™€ TypeScript๋กœ ๋งŒ๋“œ๋Š” Express ๋ฐฑ์—”๋“œ API ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ๊ฐ–์ถ˜ ํ”„๋กœํŽ˜์…”๋„ํ•œ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ

Node.js Type Script Express API ํƒ€์ž… ์•ˆ์ •์„ฑ + ๊ณ ์„ฑ๋Šฅ ๋ฐฑ์—”๋“œ ํ”„๋กœํŽ˜์…”๋„ํ•œ API ์„œ๋ฒ„ ๊ตฌ์ถ•์˜ ์‹œ์ž‘

๐ŸŽฏ ์™œ TypeScript๋กœ ๋ฐฑ์—”๋“œ๋ฅผ ๊ฐœ๋ฐœํ•ด์•ผ ํ• ๊นŒ?

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ JavaScript๋กœ๋„ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ์ž–์•„? ๊ทผ๋ฐ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด์„œ ๋А๋ผ๋Š” ๊ฑฐ ์žˆ์ง€ ์•Š์•„? ๐Ÿ˜… "์ด ํ•จ์ˆ˜๊ฐ€ ๋ญ˜ ๋ฆฌํ„ดํ•˜๋”๋ผ?", "์ด ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž…์ด ๋ญ์˜€์ง€?", "๋Ÿฐํƒ€์ž„์—์„œ ์™œ ๊ฐ‘์ž๊ธฐ ์—๋Ÿฌ๊ฐ€...?" ์ด๋Ÿฐ ๊ฒฝํ—˜๋“ค ๋ง์ด์•ผ.

๋ฐ”๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค ๋•Œ๋ฌธ์— ์š”์ฆ˜ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋“ค์ด TypeScript๋กœ ๋„˜์–ด์˜ค๊ณ  ์žˆ์–ด. ํŠนํžˆ Node.js์™€ Express ์กฐํ•ฉ์— TypeScript๋ฅผ ๋”ํ•˜๋ฉด, ์ •๋ง ๊ฐ•๋ ฅํ•œ ์‹œ๋„ˆ์ง€๊ฐ€ ๋‚˜๊ฑฐ๋“ . ๐Ÿš€

๐Ÿ’ก TypeScript ๋ฐฑ์—”๋“œ์˜ ํ•ต์‹ฌ ์žฅ์  ํƒ€์ž… ์•ˆ์ •์„ฑ: ์ปดํŒŒ์ผ ํƒ€์ž„์— ์—๋Ÿฌ๋ฅผ ์žก์•„๋‚ด์„œ ๋Ÿฐํƒ€์ž„ ๋ฒ„๊ทธ๋ฅผ ๋Œ€ํญ ์ค„์—ฌ์ค˜
์ž๋™์™„์„ฑ: IDE๊ฐ€ ๋˜‘๋˜‘ํ•ด์ ธ์„œ ๊ฐœ๋ฐœ ์†๋„๊ฐ€ 2๋ฐฐ๋Š” ๋นจ๋ผ์ ธ
๋ฆฌํŒฉํ† ๋ง: ์ฝ”๋“œ ๊ตฌ์กฐ ๋ณ€๊ฒฝํ•  ๋•Œ ์–ด๋””์„œ ๋ฌธ์ œ ์ƒ๊ธธ์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์–ด
ํ˜‘์—…: ํƒ€์ž… ์ •์˜๊ฐ€ ๊ณง ๋ฌธ์„œํ™”์•ผ. ํŒ€์›๋“ค์ด ์ฝ”๋“œ ์ดํ•ดํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ
์œ ์ง€๋ณด์ˆ˜: 6๊ฐœ์›” ํ›„์— ๋‚ด๊ฐ€ ์ง  ์ฝ”๋“œ ๋ด๋„ ๋ฌด์Šจ ์˜๋ฏธ์ธ์ง€ ๋ฐ”๋กœ ํŒŒ์•… ๊ฐ€๋Šฅ

์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ TypeScript ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ์ฐพ๋Š” ์ˆ˜์š”๊ฐ€ ์—„์ฒญ ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์–ด. ์™œ๋ƒ๋ฉด ๊ธฐ์—…๋“ค์ด ์ด์ œ ํƒ€์ž… ์•ˆ์ •์„ฑ์˜ ์ค‘์š”์„ฑ์„ ๊นจ๋‹ฌ์•˜๊ฑฐ๋“ . ๐Ÿ’ผ

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ธํŒ…๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์ œ๋Œ€๋กœ ๊ฐ–์ถฐ์•ผ ํ•ด. ์ด ๋ถ€๋ถ„์ด ํŠผํŠผํ•ด์•ผ ๋‚˜์ค‘์— ์‚ฝ์งˆ ์•ˆ ํ•˜๊ฑฐ๋“ ! ๐Ÿ˜Ž

๐Ÿ“ฆ ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜ํ•˜๊ธฐ

Node.js๋Š” ์ด๋ฏธ ์„ค์น˜๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ• ๊ฒŒ. ์—†์œผ๋ฉด nodejs.org์—์„œ LTS ๋ฒ„์ „ ๋ฐ›์•„์„œ ์„ค์น˜ํ•˜๋ฉด ๋ผ. ๊ทธ๋Ÿผ ์ด์ œ ํ”„๋กœ์ ํŠธ ํด๋” ๋งŒ๋“ค๊ณ  ์‹œ์ž‘ํ•ด๋ณด์ž!

# ํ”„๋กœ์ ํŠธ ํด๋” ์ƒ์„ฑ ๋ฐ ์ด๋™
mkdir my-typescript-api
cd my-typescript-api

# package.json ์ดˆ๊ธฐํ™”
npm init -y

# TypeScript ๋ฐ ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜
npm install express
npm install -D typescript @types/node @types/express

# TypeScript ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ
npx tsc --init

์—ฌ๊ธฐ์„œ -D ํ”Œ๋ž˜๊ทธ๋Š” ๊ฐœ๋ฐœ ์˜์กด์„ฑ(devDependencies)์œผ๋กœ ์„ค์น˜ํ•œ๋‹ค๋Š” ๋œป์ด์•ผ. TypeScript๋Š” ๊ฐœ๋ฐœํ•  ๋•Œ๋งŒ ํ•„์š”ํ•˜๊ณ , ์‹ค์ œ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ปดํŒŒ์ผ๋œ JavaScript๊ฐ€ ์‹คํ–‰๋˜๋‹ˆ๊นŒ ๋ง์ด์ง€. ๐ŸŽฏ

๐Ÿ”ฅ Pro Tip: @types/๋กœ ์‹œ์ž‘ํ•˜๋Š” ํŒจํ‚ค์ง€๋“ค์€ TypeScript ํƒ€์ž… ์ •์˜ ํŒŒ์ผ์ด์•ผ. JavaScript๋กœ ์ž‘์„ฑ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์— ํƒ€์ž… ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ด. Express๋„ ์›๋ž˜๋Š” JavaScript ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ง€๋งŒ, @types/express๋ฅผ ์„ค์น˜ํ•˜๋ฉด TypeScript์—์„œ ํƒ€์ž… ์ฒดํฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ ธ!

โš™๏ธ tsconfig.json ์„ค์ •ํ•˜๊ธฐ

npx tsc --init ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด tsconfig.json ํŒŒ์ผ์ด ์ƒ์„ฑ๋ผ. ์ด ํŒŒ์ผ์€ TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์„ค์ •์„ ๋‹ด๊ณ  ์žˆ์–ด. ๊ธฐ๋ณธ ์„ค์ •๋„ ๊ดœ์ฐฎ์ง€๋งŒ, ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์— ์ตœ์ ํ™”๋œ ์„ค์ •์œผ๋กœ ๋ฐ”๊ฟ”๋ณด์ž! ๐Ÿ’ช

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

๐ŸŽ“ ์ฃผ์š” ์„ค์ • ํ•ญ๋ชฉ ์„ค๋ช…

target: ์ปดํŒŒ์ผ๋  JavaScript ๋ฒ„์ „. ES2020์ด๋ฉด ์ตœ์‹  Node.js์—์„œ ์ž˜ ๋Œ์•„๊ฐ€
module: Node.js๋Š” CommonJS ๋ชจ๋“ˆ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•ด
outDir: ์ปดํŒŒ์ผ๋œ ํŒŒ์ผ์ด ์ €์žฅ๋  ํด๋”
rootDir: ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” ํด๋”
strict: ์—„๊ฒฉํ•œ ํƒ€์ž… ์ฒดํฌ ํ™œ์„ฑํ™”. ์ด๊ฒŒ TypeScript์˜ ์ง„์งœ ํŒŒ์›Œ์•ผ! ๐Ÿ’ช
esModuleInterop: CommonJS์™€ ES Module ๊ฐ„ ํ˜ธํ™˜์„ฑ ๊ฐœ์„ 
sourceMap: ๋””๋ฒ„๊น…ํ•  ๋•Œ ์›๋ณธ TypeScript ์ฝ”๋“œ ์œ„์น˜๋ฅผ ์•Œ๋ ค์ค˜

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์žก๊ธฐ

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ž˜ ์žก์•„๋†“์œผ๋ฉด ๋‚˜์ค‘์— ์ •๋ง ํŽธํ•ด. ๋‚ด๊ฐ€ ์ถ”์ฒœํ•˜๋Š” ๊ตฌ์กฐ๋Š” ์ด๊ฑฐ์•ผ:

my-typescript-api/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ controllers/     # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
โ”‚   โ”œโ”€โ”€ routes/          # API ๋ผ์šฐํŠธ ์ •์˜
โ”‚   โ”œโ”€โ”€ models/          # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๋ฐ ํƒ€์ž…
โ”‚   โ”œโ”€โ”€ middlewares/     # ๋ฏธ๋“ค์›จ์–ด ํ•จ์ˆ˜๋“ค
โ”‚   โ”œโ”€โ”€ services/        # ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™
โ”‚   โ”œโ”€โ”€ utils/           # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”‚   โ”œโ”€โ”€ types/           # ๊ณตํ†ต ํƒ€์ž… ์ •์˜
โ”‚   โ””โ”€โ”€ app.ts           # Express ์•ฑ ์„ค์ •
โ”‚   โ””โ”€โ”€ server.ts        # ์„œ๋ฒ„ ์‹œ์ž‘์ 
โ”œโ”€โ”€ dist/                # ์ปดํŒŒ์ผ๋œ ํŒŒ์ผ
โ”œโ”€โ”€ node_modules/
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ tsconfig.json

์ด๋ ‡๊ฒŒ ํด๋”๋ณ„๋กœ ์—ญํ• ์„ ๋ช…ํ™•ํžˆ ๋‚˜๋ˆ„๋ฉด ์ฝ”๋“œ ์ฐพ๊ธฐ๋„ ์‰ฝ๊ณ , ๋‚˜์ค‘์— ํŒ€์›์ด ํ•ฉ๋ฅ˜ํ•ด๋„ ๊ตฌ์กฐ ํŒŒ์•…์ด ๋นจ๋ผ์ ธ. ๐ŸŽฏ

๐Ÿš€ ์ฒซ ๋ฒˆ์งธ TypeScript Express ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

์ž, ์ด์ œ ์ง„์งœ ์ฝ”๋”ฉ ์‹œ์ž‘์ด์•ผ! ๋จผ์ € ๊ฐ„๋‹จํ•œ ์„œ๋ฒ„๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณด์ž. src/app.ts ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ค˜.

๐Ÿ“ app.ts - Express ์•ฑ ์„ค์ •

import express, { Application, Request, Response, NextFunction } from 'express';

// Express ์•ฑ ์ƒ์„ฑ
const app: Application = express();

// JSON ํŒŒ์‹ฑ ๋ฏธ๋“ค์›จ์–ด
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// ๊ธฐ๋ณธ ๋ผ์šฐํŠธ
app.get('/', (req: Request, res: Response) => {
  res.json({
    message: '๐ŸŽ‰ TypeScript Express API ์„œ๋ฒ„๊ฐ€ ์ •์ƒ ์ž‘๋™ ์ค‘์ž…๋‹ˆ๋‹ค!',
    timestamp: new Date().toISOString()
  });
});

// 404 ์—๋Ÿฌ ํ•ธ๋“ค๋ง
app.use((req: Request, res: Response) => {
  res.status(404).json({
    error: 'Not Found',
    message: '์š”์ฒญํ•˜์‹  ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'
  });
});

// ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฏธ๋“ค์›จ์–ด
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal Server Error',
    message: err.message
  });
});

export default app;

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ์€ ํƒ€์ž… ์–ด๋…ธํ…Œ์ด์…˜์ด์•ผ. Application, Request, Response ๊ฐ™์€ ํƒ€์ž…๋“ค์„ ๋ช…์‹œํ•ด์คŒ์œผ๋กœ์จ, IDE๊ฐ€ ์ž๋™์™„์„ฑ์„ ์ œ๊ณตํ•˜๊ณ  ํƒ€์ž… ์—๋Ÿฌ๋ฅผ ๋ฏธ๋ฆฌ ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์–ด. ๐Ÿ˜Š

๐ŸŽฌ server.ts - ์„œ๋ฒ„ ์‹œ์ž‘์ 

import app from './app';

const PORT: number = parseInt(process.env.PORT || '3000', 10);

app.listen(PORT, () => {
  console.log(`๐Ÿš€ ์„œ๋ฒ„๊ฐ€ ํฌํŠธ ${PORT}์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.`);
  console.log(`๐Ÿ“ http://localhost:${PORT}`);
});

app.ts์™€ server.ts๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ์ด์œ ๋Š” ํ…Œ์ŠคํŠธ ๋•Œ๋ฌธ์ด์•ผ. ๋‚˜์ค‘์— ์œ ๋‹› ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•  ๋•Œ ์„œ๋ฒ„๋ฅผ ์‹ค์ œ๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ ๋„ ์•ฑ ๋กœ์ง์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿงช

๐Ÿƒ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ ์„ค์ •

package.json์— ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์ž. ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘๋˜๋ฉด ํŽธํ•˜๋‹ˆ๊นŒ nodemon๊ณผ ts-node๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ.

# ๊ฐœ๋ฐœ ๋„๊ตฌ ์„ค์น˜
npm install -D ts-node nodemon @types/nodemon

๊ทธ๋ฆฌ๊ณ  package.json์˜ scripts ์„น์…˜์„ ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•ด:

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "watch": "tsc --watch"
  }
}

์ด์ œ npm run dev ๋ช…๋ น์–ด๋กœ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘๋˜๋‹ˆ๊นŒ ์—„์ฒญ ํŽธํ•ด! ๐ŸŽ‰

# ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
npm run dev

# ๋ธŒ๋ผ์šฐ์ €๋‚˜ curl๋กœ ํ…Œ์ŠคํŠธ
curl http://localhost:3000

๐ŸŽจ ํƒ€์ž… ์ •์˜๋กœ API ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ

TypeScript์˜ ์ง„์งœ ํž˜์€ ํƒ€์ž… ์ •์˜์—์„œ ๋‚˜์™€. API๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ๋จผ์ € ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ํƒ€์ž…์œผ๋กœ ์ •์˜ํ•ด๋‘๋ฉด, ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ์‹ค์ˆ˜ํ•  ์ผ์ด ํ™• ์ค„์–ด๋“ค์–ด. ๐Ÿ›ก๏ธ

๐Ÿ“‹ ์ธํ„ฐํŽ˜์ด์Šค์™€ ํƒ€์ž… ์ •์˜

src/types/user.types.ts ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉ์ž ๊ด€๋ จ ํƒ€์ž…์„ ์ •์˜ํ•ด๋ณด์ž:

// ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค
export interface User {
  id: string;
  email: string;
  username: string;
  createdAt: Date;
  updatedAt: Date;
}

// ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์š”์ฒญ DTO
export interface CreateUserDto {
  email: string;
  username: string;
  password: string;
}

// ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ ์š”์ฒญ DTO
export interface UpdateUserDto {
  email?: string;
  username?: string;
}

// ์‚ฌ์šฉ์ž ์‘๋‹ต DTO (๋น„๋ฐ€๋ฒˆํ˜ธ ์ œ์™ธ)
export interface UserResponseDto {
  id: string;
  email: string;
  username: string;
  createdAt: string;
}

// API ์‘๋‹ต ๋ž˜ํผ
export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  message?: string;
}
๐Ÿ’ก DTO๊ฐ€ ๋ญ์•ผ? DTO(Data Transfer Object)๋Š” ๋ฐ์ดํ„ฐ ์ „์†ก ๊ฐ์ฒด๋ผ๋Š” ๋œป์ด์•ผ. ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์— ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ์˜ ํ˜•ํƒœ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฑฐ์ง€. ์˜ˆ๋ฅผ ๋“ค์–ด CreateUserDto๋Š” ์‚ฌ์šฉ์ž ์ƒ์„ฑํ•  ๋•Œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋‹ด๊ณ  ์žˆ์–ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „์†ก๋˜๋Š” ๊ฑธ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

๐Ÿ”ง ์ œ๋„ค๋ฆญ ํƒ€์ž… ํ™œ์šฉํ•˜๊ธฐ

์œ„์—์„œ ApiResponse<T>๋ฅผ ๋ดค์ง€? ์ด๊ฒŒ ๋ฐ”๋กœ ์ œ๋„ค๋ฆญ์ด์•ผ. ์–ด๋–ค ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋“  ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ์œ ์—ฐํ•œ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด.

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const userResponse: ApiResponse<UserResponseDto> = {
  success: true,
  data: {
    id: '123',
    email: 'user@example.com',
    username: 'cooluser',
    createdAt: new Date().toISOString()
  }
};

const usersListResponse: ApiResponse<UserResponseDto[]> = {
  success: true,
  data: [
    // ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ...
  ]
};

์ œ๋„ค๋ฆญ์„ ์“ฐ๋ฉด ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ์ด ์—„์ฒญ ๋†’์•„์ ธ. ๊ฐ™์€ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿš€

๐Ÿ›ฃ๏ธ ๋ผ์šฐํ„ฐ์™€ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌ์กฐํ™”ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์ฐจ๋ก€์•ผ. Express์—์„œ๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.

๐ŸŽฎ ์ปจํŠธ๋กค๋Ÿฌ ์ž‘์„ฑํ•˜๊ธฐ

๋จผ์ € src/controllers/user.controller.ts๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import { Request, Response, NextFunction } from 'express';
import { 
  User, 
  CreateUserDto, 
  UpdateUserDto, 
  UserResponseDto, 
  ApiResponse 
} from '../types/user.types';

// ์ž„์‹œ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ (์‹ค์ œ๋กœ๋Š” DB ์‚ฌ์šฉ)
let users: User[] = [];

export class UserController {
  // ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
  static async getAllUsers(
    req: Request, 
    res: Response, 
    next: NextFunction
  ): Promise<void> {
    try {
      const response: ApiResponse<UserResponseDto[]> = {
        success: true,
        data: users.map(user => ({
          id: user.id,
          email: user.email,
          username: user.username,
          createdAt: user.createdAt.toISOString()
        }))
      };
      
      res.json(response);
    } catch (error) {
      next(error);
    }
  }

  // ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
  static async getUserById(
    req: Request<{ id: string }>, 
    res: Response, 
    next: NextFunction
  ): Promise<void> {
    try {
      const { id } = req.params;
      const user = users.find(u => u.id === id);

      if (!user) {
        res.status(404).json({
          success: false,
          error: 'User not found'
        });
        return;
      }

      const response: ApiResponse<UserResponseDto> = {
        success: true,
        data: {
          id: user.id,
          email: user.email,
          username: user.username,
          createdAt: user.createdAt.toISOString()
        }
      };

      res.json(response);
    } catch (error) {
      next(error);
    }
  }

  // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  static async createUser(
    req: Request<{}, {}, CreateUserDto>, 
    res: Response, 
    next: NextFunction
  ): Promise<void> {
    try {
      const { email, username, password } = req.body;

      // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ
      const existingUser = users.find(u => u.email === email);
      if (existingUser) {
        res.status(400).json({
          success: false,
          error: 'Email already exists'
        });
        return;
      }

      const newUser: User = {
        id: Date.now().toString(),
        email,
        username,
        createdAt: new Date(),
        updatedAt: new Date()
      };

      users.push(newUser);

      const response: ApiResponse<UserResponseDto> = {
        success: true,
        message: 'User created successfully',
        data: {
          id: newUser.id,
          email: newUser.email,
          username: newUser.username,
          createdAt: newUser.createdAt.toISOString()
        }
      };

      res.status(201).json(response);
    } catch (error) {
      next(error);
    }
  }

  // ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ
  static async updateUser(
    req: Request<{ id: string }, {}, UpdateUserDto>, 
    res: Response, 
    next: NextFunction
  ): Promise<void> {
    try {
      const { id } = req.params;
      const updates = req.body;

      const userIndex = users.findIndex(u => u.id === id);
      if (userIndex === -1) {
        res.status(404).json({
          success: false,
          error: 'User not found'
        });
        return;
      }

      users[userIndex] = {
        ...users[userIndex],
        ...updates,
        updatedAt: new Date()
      };

      const response: ApiResponse<UserResponseDto> = {
        success: true,
        message: 'User updated successfully',
        data: {
          id: users[userIndex].id,
          email: users[userIndex].email,
          username: users[userIndex].username,
          createdAt: users[userIndex].createdAt.toISOString()
        }
      };

      res.json(response);
    } catch (error) {
      next(error);
    }
  }

  // ์‚ฌ์šฉ์ž ์‚ญ์ œ
  static async deleteUser(
    req: Request<{ id: string }>, 
    res: Response, 
    next: NextFunction
  ): Promise<void> {
    try {
      const { id } = req.params;
      const userIndex = users.findIndex(u => u.id === id);

      if (userIndex === -1) {
        res.status(404).json({
          success: false,
          error: 'User not found'
        });
        return;
      }

      users.splice(userIndex, 1);

      res.json({
        success: true,
        message: 'User deleted successfully'
      });
    } catch (error) {
      next(error);
    }
  }
}

์—ฌ๊ธฐ์„œ ์ฃผ๋ชฉํ•  ์ ์ด ๋ช‡ ๊ฐ€์ง€ ์žˆ์–ด! ๐Ÿ˜Š

๐ŸŽฏ Request ํƒ€์ž… ์ œ๋„ค๋ฆญ Request<{ id: string }, {}, CreateUserDto> ์ด๋Ÿฐ ์‹์œผ๋กœ Request ํƒ€์ž…์— ์ œ๋„ค๋ฆญ์„ ์‚ฌ์šฉํ–ˆ์–ด. ์ˆœ์„œ๋Œ€๋กœ:
1๏ธโƒฃ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… (req.params)
2๏ธโƒฃ ์‘๋‹ต ๋ฐ”๋”” ํƒ€์ž… (๋ณดํ†ต ์‚ฌ์šฉ ์•ˆ ํ•จ)
3๏ธโƒฃ ์š”์ฒญ ๋ฐ”๋”” ํƒ€์ž… (req.body)
4๏ธโƒฃ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž… (req.query)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด req.params.id๋‚˜ req.body.email ๊ฐ™์€ ๊ฑธ ์“ธ ๋•Œ ์ž๋™์™„์„ฑ์ด ๋˜๊ณ , ํƒ€์ž… ์ฒดํฌ๋„ ๋ผ!

๐Ÿ›ค๏ธ ๋ผ์šฐํ„ฐ ์„ค์ •ํ•˜๊ธฐ

์ด์ œ src/routes/user.routes.ts๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ผ์šฐํŠธ๋ฅผ ์ •์˜ํ•ด๋ณด์ž:

import { Router } from 'express';
import { UserController } from '../controllers/user.controller';

const router: Router = Router();

// GET /api/users - ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
router.get('/', UserController.getAllUsers);

// GET /api/users/:id - ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
router.get('/:id', UserController.getUserById);

// POST /api/users - ์‚ฌ์šฉ์ž ์ƒ์„ฑ
router.post('/', UserController.createUser);

// PUT /api/users/:id - ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ
router.put('/:id', UserController.updateUser);

// DELETE /api/users/:id - ์‚ฌ์šฉ์ž ์‚ญ์ œ
router.delete('/:id', UserController.deleteUser);

export default router;

๊ทธ๋ฆฌ๊ณ  src/app.ts์— ์ด ๋ผ์šฐํ„ฐ๋ฅผ ๋“ฑ๋กํ•ด์ค˜์•ผ ํ•ด:

import express, { Application } from 'express';
import userRoutes from './routes/user.routes';

const app: Application = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// ๋ผ์šฐํ„ฐ ๋“ฑ๋ก
app.use('/api/users', userRoutes);

// ... ๋‚˜๋จธ์ง€ ์ฝ”๋“œ

export default app;

์ด์ œ API๊ฐ€ ์™„์„ฑ๋์–ด! ํ…Œ์ŠคํŠธํ•ด๋ณด์ž. ๐ŸŽ‰

# ์‚ฌ์šฉ์ž ์ƒ์„ฑ
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","username":"testuser","password":"secret123"}'

# ๋ชจ๋“  ์‚ฌ์šฉ์ž ์กฐํšŒ
curl http://localhost:3000/api/users

# ํŠน์ • ์‚ฌ์šฉ์ž ์กฐํšŒ
curl http://localhost:3000/api/users/1234567890

๐Ÿ›ก๏ธ ๋ฏธ๋“ค์›จ์–ด๋กœ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ

API์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ ๊ฒ€์ฆํ•˜๋Š” ๊ฑด ์ •๋ง ์ค‘์š”ํ•ด. ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ์„œ๋ฒ„๊ฐ€ ํ„ฐ์งˆ ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜ฑ TypeScript์™€ ํ•จ๊ป˜ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์•ˆ์ „ํ•ด์ ธ.

โœ… ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด

src/middlewares/validation.middleware.ts๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import { Request, Response, NextFunction } from 'express';
import { CreateUserDto, UpdateUserDto } from '../types/user.types';

// ์ด๋ฉ”์ผ ํ˜•์‹ ๊ฒ€์ฆ
const isValidEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

// ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๊ฒ€์ฆ
export const validateCreateUser = (
  req: Request<{}, {}, CreateUserDto>,
  res: Response,
  next: NextFunction
): void => {
  const { email, username, password } = req.body;

  // ํ•„์ˆ˜ ํ•„๋“œ ์ฒดํฌ
  if (!email || !username || !password) {
    res.status(400).json({
      success: false,
      error: 'Missing required fields',
      message: 'email, username, password are required'
    });
    return;
  }

  // ์ด๋ฉ”์ผ ํ˜•์‹ ๊ฒ€์ฆ
  if (!isValidEmail(email)) {
    res.status(400).json({
      success: false,
      error: 'Invalid email format'
    });
    return;
  }

  // ์‚ฌ์šฉ์ž๋ช… ๊ธธ์ด ๊ฒ€์ฆ
  if (username.length < 3 || username.length > 20) {
    res.status(400).json({
      success: false,
      error: 'Username must be between 3 and 20 characters'
    });
    return;
  }

  // ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด ๊ฒ€์ฆ
  if (password.length < 6) {
    res.status(400).json({
      success: false,
      error: 'Password must be at least 6 characters'
    });
    return;
  }

  next();
};

// ์‚ฌ์šฉ์ž ์—…๋ฐ์ดํŠธ ๊ฒ€์ฆ
export const validateUpdateUser = (
  req: Request<{ id: string }, {}, UpdateUserDto>,
  res: Response,
  next: NextFunction
): void => {
  const { email, username } = req.body;

  // ์ตœ์†Œ ํ•˜๋‚˜์˜ ํ•„๋“œ๋Š” ์žˆ์–ด์•ผ ํ•จ
  if (!email && !username) {
    res.status(400).json({
      success: false,
      error: 'At least one field must be provided'
    });
    return;
  }

  // ์ด๋ฉ”์ผ์ด ์žˆ์œผ๋ฉด ํ˜•์‹ ๊ฒ€์ฆ
  if (email && !isValidEmail(email)) {
    res.status(400).json({
      success: false,
      error: 'Invalid email format'
    });
    return;
  }

  // ์‚ฌ์šฉ์ž๋ช…์ด ์žˆ์œผ๋ฉด ๊ธธ์ด ๊ฒ€์ฆ
  if (username && (username.length < 3 || username.length > 20)) {
    res.status(400).json({
      success: false,
      error: 'Username must be between 3 and 20 characters'
    });
    return;
  }

  next();
};

์ด์ œ ๋ผ์šฐํ„ฐ์— ์ด ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•ด๋ณด์ž:

import { Router } from 'express';
import { UserController } from '../controllers/user.controller';
import { validateCreateUser, validateUpdateUser } from '../middlewares/validation.middleware';

const router: Router = Router();

router.get('/', UserController.getAllUsers);
router.get('/:id', UserController.getUserById);

// ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€
router.post('/', validateCreateUser, UserController.createUser);
router.put('/:id', validateUpdateUser, UserController.updateUser);

router.delete('/:id', UserController.deleteUser);

export default router;
๐Ÿ”ฅ ๋” ๊ฐ•๋ ฅํ•œ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด? ์‹ค๋ฌด์—์„œ๋Š” joi, yup, class-validator ๊ฐ™์€ ๊ฒ€์ฆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋งŽ์ด ์จ. ํŠนํžˆ class-validator๋Š” TypeScript ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•ด์„œ ์ •๋ง ๊น”๋”ํ•˜๊ฒŒ ๊ฒ€์ฆ ๋กœ์ง์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด! ๋‚˜์ค‘์— ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด ์ด๋Ÿฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž…์„ ๊ณ ๋ คํ•ด๋ด. ๐Ÿ˜Š

๐ŸŽญ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ์ œ๋Œ€๋กœ ํ•˜๊ธฐ

์—๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ์ฃฝ๊ฑฐ๋‚˜, ์‚ฌ์šฉ์ž์—๊ฒŒ ์ด์ƒํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฐ€๊ฑฐ๋‚˜, ๋ณด์•ˆ ์ •๋ณด๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜ฐ

๐Ÿšจ ์ปค์Šคํ…€ ์—๋Ÿฌ ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

src/utils/errors.ts๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ปค์Šคํ…€ ์—๋Ÿฌ ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•ด๋ณด์ž:

// ๊ธฐ๋ณธ API ์—๋Ÿฌ ํด๋ž˜์Šค
export class ApiError extends Error {
  statusCode: number;
  isOperational: boolean;

  constructor(statusCode: number, message: string, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;

    // ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ์œ ์ง€
    Error.captureStackTrace(this, this.constructor);
  }
}

// 404 Not Found ์—๋Ÿฌ
export class NotFoundError extends ApiError {
  constructor(message = 'Resource not found') {
    super(404, message);
  }
}

// 400 Bad Request ์—๋Ÿฌ
export class BadRequestError extends ApiError {
  constructor(message = 'Bad request') {
    super(400, message);
  }
}

// 401 Unauthorized ์—๋Ÿฌ
export class UnauthorizedError extends ApiError {
  constructor(message = 'Unauthorized') {
    super(401, message);
  }
}

// 403 Forbidden ์—๋Ÿฌ
export class ForbiddenError extends ApiError {
  constructor(message = 'Forbidden') {
    super(403, message);
  }
}

// 409 Conflict ์—๋Ÿฌ
export class ConflictError extends ApiError {
  constructor(message = 'Conflict') {
    super(409, message);
  }
}

// 500 Internal Server Error
export class InternalServerError extends ApiError {
  constructor(message = 'Internal server error') {
    super(500, message, false);
  }
}

๐Ÿ› ๏ธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฏธ๋“ค์›จ์–ด

src/middlewares/error.middleware.ts๋ฅผ ๋งŒ๋“ค์–ด์„œ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž:

import { Request, Response, NextFunction } from 'express';
import { ApiError } from '../utils/errors';

export const errorHandler = (
  err: Error | ApiError,
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  // ApiError์ธ ๊ฒฝ์šฐ
  if (err instanceof ApiError) {
    res.status(err.statusCode).json({
      success: false,
      error: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
    return;
  }

  // ์ผ๋ฐ˜ ์—๋Ÿฌ์ธ ๊ฒฝ์šฐ
  console.error('Unexpected error:', err);
  res.status(500).json({
    success: false,
    error: 'Internal server error',
    ...(process.env.NODE_ENV === 'development' && { 
      message: err.message,
      stack: err.stack 
    })
  });
};

// 404 ํ•ธ๋“ค๋Ÿฌ
export const notFoundHandler = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  res.status(404).json({
    success: false,
    error: 'Not Found',
    message: `Cannot ${req.method} ${req.path}`
  });
};

์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด ์—๋Ÿฌ ํด๋ž˜์Šค๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import { NotFoundError, ConflictError } from '../utils/errors';

// ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด๋ถ€์—์„œ
static async getUserById(
  req: Request<{ id: string }>, 
  res: Response, 
  next: NextFunction
): Promise<void> {
  try {
    const { id } = req.params;
    const user = users.find(u => u.id === id);

    if (!user) {
      throw new NotFoundError('User not found');
    }

    // ... ์‘๋‹ต ์ฒ˜๋ฆฌ
  } catch (error) {
    next(error);
  }
}

static async createUser(
  req: Request<{}, {}, CreateUserDto>, 
  res: Response, 
  next: NextFunction
): Promise<void> {
  try {
    const { email } = req.body;
    
    const existingUser = users.find(u => u.email === email);
    if (existingUser) {
      throw new ConflictError('Email already exists');
    }

    // ... ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋กœ์ง
  } catch (error) {
    next(error);
  }
}

๊ทธ๋ฆฌ๊ณ  app.ts์— ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•ด์ค˜:

import { errorHandler, notFoundHandler } from './middlewares/error.middleware';

// ... ๋ผ์šฐํ„ฐ ๋“ฑ๋ก ํ›„

// 404 ํ•ธ๋“ค๋Ÿฌ (๋ชจ๋“  ๋ผ์šฐํŠธ ๋’ค์— ์œ„์น˜)
app.use(notFoundHandler);

// ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ (๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์œ„์น˜)
app.use(errorHandler);

export default app;

๐Ÿ” ํ™˜๊ฒฝ ๋ณ€์ˆ˜์™€ ์„ค์ • ๊ด€๋ฆฌ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํฌํŠธ ๋ฒˆํ˜ธ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด, API ํ‚ค ๊ฐ™์€ ์„ค์ •๊ฐ’๋“ค์„ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์œผ๋ฉด ์•ˆ ๋ผ. ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•ด! ๐Ÿ”’

๐Ÿ“ฆ dotenv ์„ค์ •ํ•˜๊ธฐ

# dotenv ์„ค์น˜
npm install dotenv
npm install -D @types/dotenv

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

# ์„œ๋ฒ„ ์„ค์ •
NODE_ENV=development
PORT=3000

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (๋‚˜์ค‘์— ์‚ฌ์šฉ)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=secret

# JWT ์‹œํฌ๋ฆฟ (๋‚˜์ค‘์— ์‚ฌ์šฉ)
JWT_SECRET=your-super-secret-key-change-this

# API ํ‚ค ๋“ฑ
API_KEY=your-api-key
โš ๏ธ ์ค‘์š”! .env ํŒŒ์ผ์€ ์ ˆ๋Œ€๋กœ Git์— ์ปค๋ฐ‹ํ•˜๋ฉด ์•ˆ ๋ผ! .gitignore์— ์ถ”๊ฐ€ํ•ด์ค˜:

node_modules/
dist/
.env
.env.local

โš™๏ธ ์„ค์ • ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

src/config/index.ts๋ฅผ ๋งŒ๋“ค์–ด์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜์ž:

import dotenv from 'dotenv';

// .env ํŒŒ์ผ ๋กœ๋“œ
dotenv.config();

interface Config {
  env: string;
  port: number;
  db: {
    host: string;
    port: number;
    name: string;
    user: string;
    password: string;
  };
  jwt: {
    secret: string;
    expiresIn: string;
  };
}

const config: Config = {
  env: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT || '3000', 10),
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT || '5432', 10),
    name: process.env.DB_NAME || 'myapp',
    user: process.env.DB_USER || 'postgres',
    password: process.env.DB_PASSWORD || ''
  },
  jwt: {
    secret: process.env.JWT_SECRET || 'default-secret-change-this',
    expiresIn: '7d'
  }
};

// ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ฒดํฌ
const requiredEnvVars = ['JWT_SECRET'];
const missingEnvVars = requiredEnvVars.filter(
  envVar => !process.env[envVar]
);

if (missingEnvVars.length > 0 && config.env === 'production') {
  throw new Error(
    `Missing required environment variables: ${missingEnvVars.join(', ')}`
  );
}

export default config;

์ด์ œ server.ts์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import app from './app';
import config from './config';

app.listen(config.port, () => {
  console.log(`๐Ÿš€ ์„œ๋ฒ„๊ฐ€ ${config.env} ๋ชจ๋“œ๋กœ ํฌํŠธ ${config.port}์—์„œ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค.`);
  console.log(`๐Ÿ“ http://localhost:${config.port}`);
});

๐Ÿ“Š ๋กœ๊น… ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” console.log๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด. ์ œ๋Œ€๋กœ ๋œ ๋กœ๊น… ์‹œ์Šคํ…œ์ด ํ•„์š”ํ•˜๊ฑฐ๋“ ! ๐Ÿ˜Š

๐Ÿ“ Winston ๋กœ๊ฑฐ ์„ค์ •

# Winston ์„ค์น˜
npm install winston
npm install -D @types/winston

src/utils/logger.ts๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import winston from 'winston';
import config from '../config';

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
};

const level = () => {
  const env = config.env;
  const isDevelopment = env === 'development';
  return isDevelopment ? 'debug' : 'warn';
};

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
};

winston.addColors(colors);

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
);

const transports = [
  new winston.transports.Console(),
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  new winston.transports.File({ filename: 'logs/all.log' }),
];

const logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
});

export default logger;

์ด์ œ ์ฝ”๋“œ ์ „์ฒด์—์„œ console.log ๋Œ€์‹  logger๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด:

import logger from './utils/logger';

// ๋‹ค์–‘ํ•œ ๋ ˆ๋ฒจ์˜ ๋กœ๊ทธ
logger.info('์„œ๋ฒ„๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
logger.debug('๋””๋ฒ„๊ทธ ์ •๋ณด');
logger.warn('๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€');
logger.error('์—๋Ÿฌ ๋ฐœ์ƒ!', { error: err });

// ์ปจํŠธ๋กค๋Ÿฌ์—์„œ
static async createUser(req: Request, res: Response, next: NextFunction) {
  try {
    logger.info(`์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์š”์ฒญ: ${req.body.email}`);
    // ... ๋กœ์ง
    logger.info(`์‚ฌ์šฉ์ž ์ƒ์„ฑ ์™„๋ฃŒ: ${newUser.id}`);
  } catch (error) {
    logger.error('์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹คํŒจ', { error });
    next(error);
  }
}

๐Ÿงช ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ๋Š” ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜์•ผ! ํŠนํžˆ TypeScript๋ฅผ ์“ฐ๋ฉด ํƒ€์ž… ์ฒดํฌ๋กœ ๋งŽ์€ ๋ฒ„๊ทธ๋ฅผ ์žก์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•ด์•ผ ํ•ด. ๐ŸŽฏ

๐Ÿ”ง Jest ์„ค์ •ํ•˜๊ธฐ

# Jest ๋ฐ ๊ด€๋ จ ํŒจํ‚ค์ง€ ์„ค์น˜
npm install -D jest ts-jest @types/jest supertest @types/supertest

jest.config.js ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  moduleFileExtensions: ['ts', 'js', 'json'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts',
    '!src/**/*.spec.ts',
  ],
};

โœ… API ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

src/__tests__/user.test.ts๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import request from 'supertest';
import app from '../app';

describe('User API', () => {
  describe('POST /api/users', () => {
    it('should create a new user', async () => {
      const userData = {
        email: 'test@example.com',
        username: 'testuser',
        password: 'password123'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);

      expect(response.body.success).toBe(true);
      expect(response.body.data).toHaveProperty('id');
      expect(response.body.data.email).toBe(userData.email);
      expect(response.body.data.username).toBe(userData.username);
    });

    it('should return 400 for invalid email', async () => {
      const userData = {
        email: 'invalid-email',
        username: 'testuser',
        password: 'password123'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(400);

      expect(response.body.success).toBe(false);
      expect(response.body.error).toContain('email');
    });

    it('should return 400 for missing fields', async () => {
      const response = await request(app)
        .post('/api/users')
        .send({ email: 'test@example.com' })
        .expect(400);

      expect(response.body.success).toBe(false);
    });
  });

  describe('GET /api/users', () => {
    it('should return all users', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200);

      expect(response.body.success).toBe(true);
      expect(Array.isArray(response.body.data)).toBe(true);
    });
  });

  describe('GET /api/users/:id', () => {
    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/999999')
        .expect(404);

      expect(response.body.success).toBe(false);
    });
  });
});

package.json์— ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

์ด์ œ npm test๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰

๐Ÿš€ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ค€๋น„ํ•˜๊ธฐ

๊ฐœ๋ฐœ์€ ๋๋‚ฌ์–ด. ์ด์ œ ์‹ค์ œ ์„œ๋ฒ„์— ๋ฐฐํฌํ•  ์ค€๋น„๋ฅผ ํ•ด๋ณด์ž! ๐Ÿ’ช

๐Ÿ“ฆ ๋นŒ๋“œ ์ตœ์ ํ™”

package.json์— ๋นŒ๋“œ ๊ด€๋ จ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด:

{
  "scripts": {
    "dev": "nodemon --exec ts-node src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "clean": "rm -rf dist",
    "prebuild": "npm run clean",
    "postbuild": "echo 'Build completed successfully!'",
    "prod": "npm run build && npm start"
  }
}

๐Ÿณ Docker ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)

Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋””์„œ๋“  ๋™์ผํ•œ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด. Dockerfile์„ ๋งŒ๋“ค์–ด๋ณด์ž:

# Node.js ์ด๋ฏธ์ง€ ์‚ฌ์šฉ
FROM node:18-alpine

# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
WORKDIR /app

# package.json๊ณผ package-lock.json ๋ณต์‚ฌ
COPY package*.json ./

# ์˜์กด์„ฑ ์„ค์น˜
RUN npm ci --only=production

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# TypeScript ๋นŒ๋“œ
RUN npm run build

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 3000

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
ENV NODE_ENV=production

# ์•ฑ ์‹คํ–‰
CMD ["node", "dist/server.js"]

.dockerignore ํŒŒ์ผ๋„ ๋งŒ๋“ค์–ด์ค˜:

node_modules
dist
.env
.git
.gitignore
README.md
npm-debug.log

๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณด์•ˆ์ด ์ •๋ง ์ค‘์š”ํ•ด. ๋ช‡ ๊ฐ€์ง€ ํ•„์ˆ˜ ๋ณด์•ˆ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜์ž:

# ๋ณด์•ˆ ๊ด€๋ จ ํŒจํ‚ค์ง€ ์„ค์น˜
npm install helmet cors express-rate-limit

app.ts์— ๋ณด์•ˆ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด:

import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

const app: Application = express();

// ๋ณด์•ˆ ํ—ค๋” ์„ค์ •
app.use(helmet());

// CORS ์„ค์ •
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
  credentials: true
}));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 100, // ์ตœ๋Œ€ 100๊ฐœ ์š”์ฒญ
  message: 'Too many requests from this IP, please try again later.'
});

app.use('/api/', limiter);

// JSON ํŒŒ์‹ฑ (ํฌ๊ธฐ ์ œํ•œ)
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// ... ๋‚˜๋จธ์ง€ ์ฝ”๋“œ

export default app;

๐Ÿ“ˆ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์ตœ์ ํ™”

์„œ๋ฒ„๊ฐ€ ์ž˜ ๋Œ์•„๊ฐ€๋Š”์ง€ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด. ์‹ค์‹œ๊ฐ„์œผ๋กœ ์„ฑ๋Šฅ์„ ์ฒดํฌํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ๋นจ๋ฆฌ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ“Š

โฑ๏ธ ์š”์ฒญ ์‹œ๊ฐ„ ๋กœ๊น…

src/middlewares/logger.middleware.ts๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

import { Request, Response, NextFunction } from 'express';
import logger from '../utils/logger';

export const requestLogger = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  const start = Date.now();

  // ์‘๋‹ต์ด ์™„๋ฃŒ๋˜๋ฉด ๋กœ๊ทธ ์ถœ๋ ฅ
  res.on('finish', () => {
    const duration = Date.now() - start;
    const message = `${req.method} ${req.path} ${res.statusCode} - ${duration}ms`;

    if (res.statusCode >= 500) {
      logger.error(message);
    } else if (res.statusCode >= 400) {
      logger.warn(message);
    } else {
      logger.http(message);
    }
  });

  next();
};

app.ts์— ์ถ”๊ฐ€ํ•ด:

import { requestLogger } from './middlewares/logger.middleware';

app.use(requestLogger);

๐ŸŽ“ ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์™”์œผ๋ฉด ๊ธฐ๋ณธ์€ ๋‹ค ๋ฐฐ์šด ๊ฑฐ์•ผ! ์ด์ œ ์‹ค์ „์—์„œ ์œ ์šฉํ•œ ํŒ๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ. ๐Ÿ˜Š

๐Ÿ’ก TypeScript ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

1. ํƒ€์ž…์„ ์ตœ๋Œ€ํ•œ ๊ตฌ์ฒด์ ์œผ๋กœ ์ •์˜ํ•˜๊ธฐ

any ํƒ€์ž…์€ ์ตœ๋Œ€ํ•œ ํ”ผํ•ด. TypeScript๋ฅผ ์“ฐ๋Š” ์ด์œ ๊ฐ€ ์—†์–ด์ง€๊ฑฐ๋“ ! ๋Œ€์‹  unknown์„ ์“ฐ๊ฑฐ๋‚˜ ์ œ๋„ค๋ฆญ์„ ํ™œ์šฉํ•ด๋ด.

2. ์ธํ„ฐํŽ˜์ด์Šค vs ํƒ€์ž… ๋ณ„์นญ

๊ฐ์ฒด ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•  ๋•Œ๋Š” interface๋ฅผ, ์œ ๋‹ˆ์˜จ์ด๋‚˜ ํŠœํ”Œ ๊ฐ™์€ ๋ณต์žกํ•œ ํƒ€์ž…์€ type์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ผ๋ฐ˜์ ์ด์•ผ.

3. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋Š” async/await ์‚ฌ์šฉ

Promise ์ฒด์ด๋‹๋ณด๋‹ค async/await๊ฐ€ ๊ฐ€๋…์„ฑ์ด ํ›จ์”ฌ ์ข‹์•„. ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋„ try-catch๋กœ ๊น”๋”ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๊ณ !

4. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ถ„๋ฆฌ

๊ฐœ๋ฐœ, ์Šคํ…Œ์ด์ง•, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ๋ณ„๋กœ ์„ค์ •์„ ๋ถ„๋ฆฌํ•ด์„œ ๊ด€๋ฆฌํ•ด. .env.development, .env.production ์ด๋Ÿฐ ์‹์œผ๋กœ!

5. ์˜์กด์„ฑ ์ฃผ์ž… ํŒจํ„ด ํ™œ์šฉ

์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค, ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ณ„์ธต์„ ๋ถ„๋ฆฌํ•˜๊ณ  ์˜์กด์„ฑ ์ฃผ์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ.

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™”: N+1 ๋ฌธ์ œ๋ฅผ ์กฐ์‹ฌํ•˜๊ณ , ์ธ๋ฑ์Šค๋ฅผ ์ ์ ˆํžˆ ํ™œ์šฉํ•ด
์บ์‹ฑ ํ™œ์šฉ: Redis ๊ฐ™์€ ์ธ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‘๋‹ต ์†๋„๊ฐ€ ์—„์ฒญ ๋นจ๋ผ์ ธ
์••์ถ• ์‚ฌ์šฉ: compression ๋ฏธ๋“ค์›จ์–ด๋กœ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•ด
๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ ํ(Bull, BullMQ)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•ด
ํด๋Ÿฌ์Šคํ„ฐ๋ง: PM2๋กœ ๋ฉ€ํ‹ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ํ™œ์šฉํ•˜๋ฉด CPU๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด

๐Ÿ”ฎ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ

๊ธฐ๋ณธ์ ์ธ TypeScript Express API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋์–ด! ๐ŸŽ‰ ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ๋์ด ์•„๋‹ˆ์•ผ. ๋” ๋ฐœ์ „ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ๊ณต๋ถ€ํ•ด๋ด:

๐Ÿ“š ์ถ”๊ฐ€๋กœ ๊ณต๋ถ€ํ•˜๋ฉด ์ข‹์€ ๊ฒƒ๋“ค

๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™

PostgreSQL, MongoDB ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๋™ํ•˜๊ณ , TypeORM, Prisma, Mongoose ๊ฐ™์€ ORM์„ ์‚ฌ์šฉํ•ด๋ด. ํƒ€์ž… ์•ˆ์ •์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด!

๐Ÿ” ์ธ์ฆ/์ธ๊ฐ€ ์‹œ์Šคํ…œ

JWT, OAuth2.0, Passport.js๋ฅผ ํ™œ์šฉํ•œ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ด. ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•˜๊ฑฐ๋“ !

๐Ÿ“ก WebSocket ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

Socket.io๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…์ด๋‚˜ ์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ด. TypeScript์™€ ํ•จ๊ป˜ ์“ฐ๋ฉด ์ด๋ฒคํŠธ ํƒ€์ž…๋„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด!

๐Ÿงช ๊ณ ๊ธ‰ ํ…Œ์ŠคํŒ…

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, E2E ํ…Œ์ŠคํŠธ, ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ํ–ฅ์ƒ ๋“ฑ ๋” ๊นŠ์ด ์žˆ๋Š” ํ…Œ์ŠคํŒ… ์ „๋žต์„ ๊ณต๋ถ€ํ•ด๋ด.

๐Ÿณ ์ปจํ…Œ์ด๋„ˆํ™”์™€ CI/CD

Docker, Kubernetes, GitHub Actions, Jenkins ๊ฐ™์€ ๋„๊ตฌ๋“ค๋กœ ์ž๋™ํ™”๋œ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•ด๋ด.

๐Ÿ“Š ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜

์„œ๋น„์Šค๋ฅผ ์ž‘์€ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„๊ณ , gRPC๋‚˜ ๋ฉ”์‹œ์ง€ ํ๋กœ ํ†ต์‹ ํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํŒจํ„ด์„ ๊ณต๋ถ€ํ•ด๋ด.

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐Ÿ˜… Node.js์™€ TypeScript๋กœ Express ๋ฐฑ์—”๋“œ API๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋‹ค๋ค„๋ดค์–ด. ํƒ€์ž… ์ •์˜๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ๋ผ์šฐํŒ…, ๋ฏธ๋“ค์›จ์–ด, ์—๋Ÿฌ ํ•ธ๋“ค๋ง, ํ…Œ์ŠคํŒ…, ๋ฐฐํฌ๊นŒ์ง€ ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋ฐฐ์› ์ง€?

TypeScript๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฒ˜์Œ์—๋Š” ํƒ€์ž… ์ •์˜ํ•˜๋А๋ผ ์‹œ๊ฐ„์ด ์ข€ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ๊ทธ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•ด. ๋ฒ„๊ทธ๊ฐ€ ์ค„์–ด๋“ค๊ณ , ๋ฆฌํŒฉํ† ๋ง์ด ์‰ฌ์›Œ์ง€๊ณ , ํ˜‘์—…์ด ํŽธํ•ด์ง€๊ฑฐ๋“ ! ๐Ÿ’ช

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ TypeScript ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž์— ๋Œ€ํ•œ ์ˆ˜์š”๊ฐ€ ๊ณ„์† ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์–ด. ์ด ๊ธฐ์ˆ ์„ ์ž˜ ์ตํ˜€๋‘๋ฉด ์ •๋ง ๋งŽ์€ ๊ธฐํšŒ๊ฐ€ ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿš€

๐ŸŒŸ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด:
โ€ข ๋ธ”๋กœ๊ทธ API ์„œ๋ฒ„
โ€ข ํ•  ์ผ ๊ด€๋ฆฌ ์•ฑ ๋ฐฑ์—”๋“œ
โ€ข ๊ฐ„๋‹จํ•œ ์ „์ž์ƒ๊ฑฐ๋ž˜ API
โ€ข ์†Œ์…œ ๋ฏธ๋””์–ด ๋ฐฑ์—”๋“œ
โ€ข ํŒŒ์ผ ์—…๋กœ๋“œ ์„œ๋น„์Šค

์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋ถ€๋”ชํžˆ๋Š” ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๋‹ค ๋ณด๋ฉด ์‹ค๋ ฅ์ด ์‘ฅ์‘ฅ ๋Š˜์–ด๋‚  ๊ฑฐ์•ผ! ๐Ÿ’ฏ

๊ธฐ์–ตํ•ด, ์™„๋ฒฝํ•œ ์ฝ”๋“œ๋Š” ์—†์–ด. ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฑธ ์™„๋ฒฝํ•˜๊ฒŒ ํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋ง๊ณ , ์ผ๋‹จ ๋งŒ๋“ค์–ด๋ณด๊ณ  ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด? ๊ทธ๊ฒŒ ๋ฐ”๋กœ ๋ฐฐ์šฐ๋Š” ๊ธฐํšŒ์•ผ! ๐Ÿ˜Š

TypeScript์™€ Express๋กœ ๋ฉ‹์ง„ ๋ฐฑ์—”๋“œ API๋ฅผ ๋งŒ๋“ค์–ด๋ด. ๋„ˆ์˜ ์•„์ด๋””์–ด๋ฅผ ํ˜„์‹ค๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๋ฅผ ์†์— ๋„ฃ์€ ๊ฑฐ์•ผ. ํ™”์ดํŒ…! ๐ŸŽ‰

๐ŸŽ‰ ์™„์„ฑ! TypeScript Express API ๋งˆ์Šคํ„ฐ ์ด์ œ ๋‹น์‹ ๋„ ํ”„๋กœํŽ˜์…”๋„ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž!
๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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