Ver3.0 ๐ Node.js์ TypeScript๋ก ๋ง๋๋ Express ๋ฐฑ์๋ API ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋

๐ Node.js์ TypeScript๋ก ๋ง๋๋ Express ๋ฐฑ์๋ API ๊ฐ๋ฐ ์๋ฒฝ ๊ฐ์ด๋
ํ์ ์์ ์ฑ์ ๊ฐ์ถ ํ๋กํ์ ๋ํ ๋ฐฑ์๋ ์๋ฒ ๊ตฌ์ถํ๊ธฐ
๐ฏ ์ TypeScript๋ก ๋ฐฑ์๋๋ฅผ ๊ฐ๋ฐํด์ผ ํ ๊น?
์ผ, ์์งํ ๋งํด์ JavaScript๋ก๋ ๋ฐฑ์๋ ๊ฐ๋ฐ ์ถฉ๋ถํ ํ ์ ์์์? ๊ทผ๋ฐ ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด์ ๋๋ผ๋ ๊ฑฐ ์์ง ์์? ๐ "์ด ํจ์๊ฐ ๋ญ ๋ฆฌํดํ๋๋ผ?", "์ด ํ๋ผ๋ฏธํฐ ํ์ ์ด ๋ญ์์ง?", "๋ฐํ์์์ ์ ๊ฐ์๊ธฐ ์๋ฌ๊ฐ...?" ์ด๋ฐ ๊ฒฝํ๋ค ๋ง์ด์ผ.
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ค ๋๋ฌธ์ ์์ฆ ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ค์ด TypeScript๋ก ๋์ด์ค๊ณ ์์ด. ํนํ Node.js์ Express ์กฐํฉ์ 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๊ฐ ์คํ๋๋๊น ๋ง์ด์ง. ๐ฏ
@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;
}
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<{ 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. ์์กด์ฑ ์ฃผ์ ํจํด ํ์ฉ
์ปจํธ๋กค๋ฌ, ์๋น์ค, ๋ฆฌํฌ์งํ ๋ฆฌ ๊ณ์ธต์ ๋ถ๋ฆฌํ๊ณ ์์กด์ฑ ์ฃผ์ ์ ์ฌ์ฉํ๋ฉด ํ ์คํธํ๊ธฐ ํจ์ฌ ์ฌ์์ ธ.
์บ์ฑ ํ์ฉ: 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๋ฅผ ๋ง๋ค์ด๋ด. ๋์ ์์ด๋์ด๋ฅผ ํ์ค๋ก ๋ง๋ค ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ฅผ ์์ ๋ฃ์ ๊ฑฐ์ผ. ํ์ดํ ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.