Express.js 라우터 기능 개선

@bbearcookie · March 24, 2023 · 8 min read

프로젝트 구조

src
├── config
│   ├── express.ts
│   ├── router.ts
├── router
│   ├── photo.ts
│   ├── voucher.ts
│   ├── ...
├── controller
│   ├── photo
│   │   ├── getPhotoDetail.ts
│   │   ├── ...
│   ├── voucher
│   │   ├── getVoucherDetail.ts
│   │   ├── ...
├── app.js

라우터와 컨트롤러의 분리

요청의 URI에 따라 실행할 미들웨어를 매칭해주는 부분을 @router 가 담당하게 하고, 실제 미들웨어의 로직은 @controller 에 작성해서 이후에 프로그램에 존재하는 API가 무엇이 있는지 확인해야 하거나, 수정해야 할 때 용이하도록 @router 디렉토리를 확인하여 복잡한 로직 없이 경로에 대한 매칭 정보만 확인할 수 있게 했다.

문제점

로직을 매칭하는 부분과 실제 로직 코드를 분리한 것은 좋았으나, @router 디렉토리의 파일에서 특정 컨트롤러의 실행 순서까지 작성해야 한다는 점이 문제였다. 이 때문에 코드가 길어질 수록 아래의 문제점이 발생했다.

  1. 각 컨트롤러 파일에 존재하는 uploader, validator, controller 등의 요소를 일일이 import 해야 하는데 번거롭다.
  2. 이후에 수정하면서 컨트롤러 파일에 존재하는 미들웨어의 실행 순서가 변화해야 한다면 이를 라우터 파일에서 설정해야 하다 보니 실수가 발생할 가능성이 있다.

문제 예시

import express from 'express';
import * as getPhotos from '@controller/photo/getPhotos';
import * as getPhotoDetail from '@controller/photo/getPhotoDetail';
import * as postPhotos from '@controller/photo/postPhotos';
import * as putPhoto from '@controller/photo/putPhoto';
import * as deletePhoto from '@controller/photo/deletePhoto';

const router = express.Router();

router.route('/')
  .get(getPhotos.validator, getPhotos.controller)
  .post(
    postPhotos.uploader.array,
    postPhotos.uploader.errorHandler,
    postPhotos.validator,
    postPhotos.controller);

router.route('/:photocardId')
  .get(getPhotoDetail.validator, getPhotoDetail.controller)
  .put(
    putPhoto.uploader.single,
    putPhoto.uploader.errorHandler,
    putPhoto.validator,
    putPhoto.controller)
  .delete(deletePhoto.validator, deletePhoto.controller);

export default router;

해결 방법

컨트롤러의 실행 순서는 각 컨트롤러에서 배열 형태로 작성하도록 하고, 라우터에서는 그 배열만 가져와서 사용하도록 변경했다.

해결 예시

@router/photo.ts
import express from 'express';
import getPhotos from '@controller/photo/getPhotos';
import getPhotoDetail from '@controller/photo/getPhotoDetail';
import postPhotos from '@controller/photo/postPhotos';
import putPhoto from '@controller/photo/putPhoto';
import deletePhoto from '@controller/photo/deletePhoto';

const router = express.Router();

router.route('/')
  .get(getPhotos)
  .post(postPhotos);

router.route('/:photocardId')
  .get(getPhotoDetail)
  .put(putPhoto)
  .delete(deletePhoto);

export default router;
@controller/postPhotos.ts
interface Body {
  groupId: number;
  memberId: number;
  names: string[];
}

const validator = [
  isAdmin,
  body('groupId')
    .customSanitizer(v => Number(v))
    .isNumeric().withMessage("그룹 ID는 숫자여야 해요.").bail()
    .custom(v => v > 0).withMessage("그룹을 선택해주세요.").bail(),
  body('memberId')
    .customSanitizer(v => Number(v))
    .isNumeric().withMessage("멤버 ID는 숫자여야 해요.").bail()
    .custom(v => v > 0).withMessage("멤버를 선택해주세요.").bail(),
  body('names')
    .isArray({ min: 1 }).withMessage('포토카드를 등록해주세요.'),
  body('names.*')
    .trim()
    .notEmpty().withMessage('포토카드 이름이 비어있어요.').bail()
    .isString().withMessage('포토카드 이름은 문자열이어야 해요.').bail()
    .isLength({ min: 1, max: 100 }).withMessage('포토카드 이름은 최대 100글자까지 입력할 수 있어요.').bail(),
  validate
]

const controller = async (req: Request, res: Response, next: NextFunction) => {
  const { groupId, memberId, names } = req.body as Body;
  const files = req.files as Express.Multer.File[];

  // 포토카드 추가 관련 로직 ..
  return res.status(200).json({ message: '새로운 포토카드를 등록했어요.' });
  next();
}

const uploader = imageUploader('images[]', PHOTO_IMAGE_DIR);

const postPhotos = [
  uploader.array,
  uploader.errorHandler,
  ...validator,
  controller
];

export default postPhotos;
@bbearcookie
Frontend Developer