Node.js와 Express
Node.js 환경에서 TypeScript를 사용하는 것은 서버 사이드 애플리케이션 개발에 정적 타입의 이점을 제공합니다.
이 절에서는 Node.js와 Express를 TypeScript와 함께 사용하는 방법을 상세히 다룹니다.
Node.js 환경에서 TypeScript 설정
- 프로젝트 초기화
mkdir ts-express-project
cd ts-express-project
npm init -y
- TypeScript 및 필요한 패키지 설치
npm install typescript @types/node @types/express express
npm install --save-dev ts-node nodemon
- TypeScript 설정 파일 (tsconfig.json) 생성
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
- package.json에 스크립트 추가
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "tsc"
}
Express와 TypeScript 통합
- 기본 Express 애플리케이션 설정
import express, { Express, Request, Response } from 'express';
const app: Express = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Hello, TypeScript Express!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
- 미들웨어 구현
import { Request, Response, NextFunction } from 'express';
const loggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
console.log(`${new Date().toISOString()}: ${req.method} ${req.url}`);
next();
};
app.use(loggerMiddleware);
- 라우터 구현
import { Router } from 'express';
const userRouter: Router = Router();
userRouter.get('/', (req: Request, res: Response) => {
res.json({ message: 'User list' });
});
userRouter.post('/', (req: Request, res: Response) => {
res.json({ message: 'User created' });
});
app.use('/users', userRouter);
- 컨트롤러 구현
class UserController {
public static getUsers(req: Request, res: Response): void {
res.json({ message: 'User list from controller' });
}
public static createUser(req: Request, res: Response): void {
res.json({ message: 'User created from controller' });
}
}
userRouter.get('/', UserController.getUsers);
userRouter.post('/', UserController.createUser);
Request와 Response 객체 타입 정의
- 커스텀 요청 타입 정의
interface CustomRequest extends Request {
user?: {
id: number;
username: string;
};
}
app.get('/profile', (req: CustomRequest, res: Response) => {
if (req.user) {
res.json({ user: req.user });
} else {
res.status(401).json({ message: 'Unauthorized' });
}
});
- 응답 타입 정의
interface ApiResponse<T> {
data: T;
message: string;
}
function sendResponse<T>(res: Response, data: T, message: string): void {
const response: ApiResponse<T> = { data, message };
res.json(response);
}
app.get('/data', (req: Request, res: Response) => {
sendResponse(res, { item: 'example' }, 'Data retrieved successfully');
});
비동기 핸들러 함수와 에러 처리
- 비동기 핸들러 함수
import { RequestHandler } from 'express';
const asyncHandler = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/async-data', asyncHandler(async (req: Request, res: Response) => {
const data = await fetchDataFromDatabase();
res.json(data);
}));
- 전역 에러 핸들러
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong!' });
});
유효성 검사 라이브러리 통합
- joi 라이브러리를 사용한 유효성 검사
import Joi from 'joi';
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required()
});
const validateUser = (req: Request, res: Response, next: NextFunction) => {
const { error } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ message: error.details[0].message });
}
next();
};
userRouter.post('/', validateUser, UserController.createUser);
애플리케이션 구조 모듈화
- 디렉토리 구조
src/
├── controllers/
├── routes/
├── middlewares/
├── models/
├── services/
├── utils/
├── config/
└── index.ts
- 모듈화 예시
import { Router } from 'express';
import userRoutes from './userRoutes';
import productRoutes from './productRoutes';
const router = Router();
router.use('/users', userRoutes);
router.use('/products', productRoutes);
export default router;
테스팅
- 단위 테스트 (Jest 사용)
import { UserController } from '../controllers/userController';
describe('UserController', () => {
it('should return user list', () => {
const req = {} as Request;
const res = {
json: jest.fn(),
} as unknown as Response;
UserController.getUsers(req, res);
expect(res.json).toHaveBeenCalledWith({ message: 'User list from controller' });
});
});
- 통합 테스트
import request from 'supertest';
import app from '../app';
describe('User API', () => {
it('should get user list', async () => {
const res = await request(app).get('/users');
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('message', 'User list from controller');
});
});
Best Practices와 주의사항
- 일관된 코드 스타일을 위해 ESLint와 Prettier를 사용하세요.
- 환경 변수를 타입 안전하게 관리하기 위해 dotenv와 함께 사용자 정의 설정 모듈을 만드세요.
- 비즈니스 로직을 서비스 계층으로 분리하여 컨트롤러를 가볍게 유지하세요.
- 데이터베이스 작업에는 ORM(예 : TypeORM, Sequelize)을 사용하여 타입 안전성을 높이세요.
- API 문서화를 위해 Swagger나 TypeDoc을 사용하세요.
- 성능 모니터링을 위해 NewRelic이나 PM2와 같은 도구를 활용하세요.
- 로깅을 위해 Winston이나 Bunyan과 같은 구조화된 로깅 라이브러리를 사용하세요.
- 보안을 위해 helmet 미들웨어를 사용하고, CORS 설정을 적절히 관리하세요.
- 대규모 애플리케이션의 경우, 마이크로서비스 아키텍처를 고려하세요.
- 지속적인 통합 및 배포(CI/CD) 파이프라인을 구축하여 개발 프로세스를 자동화하세요.