icon안동민 개발노트

Node.js와 Express


 Node.js 환경에서 TypeScript를 사용하는 것은 서버 사이드 애플리케이션 개발에 정적 타입의 이점을 제공합니다.

 이 절에서는 Node.js와 Express를 TypeScript와 함께 사용하는 방법을 상세히 다룹니다.

Node.js 환경에서 TypeScript 설정

  1. 프로젝트 초기화
mkdir ts-express-project
cd ts-express-project
npm init -y
  1. TypeScript 및 필요한 패키지 설치
npm install typescript @types/node @types/express express
npm install --save-dev ts-node nodemon
  1. TypeScript 설정 파일 (tsconfig.json) 생성
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
  1. package.json에 스크립트 추가
"scripts": {
  "start": "node dist/index.js",
  "dev": "nodemon src/index.ts",
  "build": "tsc"
}

Express와 TypeScript 통합

  1. 기본 Express 애플리케이션 설정
src/index.ts
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}`);
});
  1. 미들웨어 구현
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);
  1. 라우터 구현
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);
  1. 컨트롤러 구현
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 객체 타입 정의

  1. 커스텀 요청 타입 정의
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' });
  }
});
  1. 응답 타입 정의
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');
});

비동기 핸들러 함수와 에러 처리

  1. 비동기 핸들러 함수
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);
}));
  1. 전역 에러 핸들러
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Something went wrong!' });
});

유효성 검사 라이브러리 통합

  1. 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);

애플리케이션 구조 모듈화

  1. 디렉토리 구조
src/
├── controllers/
├── routes/
├── middlewares/
├── models/
├── services/
├── utils/
├── config/
└── index.ts
  1. 모듈화 예시
src/routes/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;

테스팅

  1. 단위 테스트 (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' });
  });
});
  1. 통합 테스트
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와 주의사항

  1. 일관된 코드 스타일을 위해 ESLint와 Prettier를 사용하세요.
  2. 환경 변수를 타입 안전하게 관리하기 위해 dotenv와 함께 사용자 정의 설정 모듈을 만드세요.
  3. 비즈니스 로직을 서비스 계층으로 분리하여 컨트롤러를 가볍게 유지하세요.
  4. 데이터베이스 작업에는 ORM(예 : TypeORM, Sequelize)을 사용하여 타입 안전성을 높이세요.
  5. API 문서화를 위해 Swagger나 TypeDoc을 사용하세요.
  6. 성능 모니터링을 위해 NewRelic이나 PM2와 같은 도구를 활용하세요.
  7. 로깅을 위해 Winston이나 Bunyan과 같은 구조화된 로깅 라이브러리를 사용하세요.
  8. 보안을 위해 helmet 미들웨어를 사용하고, CORS 설정을 적절히 관리하세요.
  9. 대규모 애플리케이션의 경우, 마이크로서비스 아키텍처를 고려하세요.
  10. 지속적인 통합 및 배포(CI/CD) 파이프라인을 구축하여 개발 프로세스를 자동화하세요.