프로젝트 구조
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
디렉토리의 파일에서 특정 컨트롤러의 실행 순서까지 작성해야 한다는 점이 문제였다. 이 때문에 코드가 길어질 수록 아래의 문제점이 발생했다.
- 각 컨트롤러 파일에 존재하는
uploader
,validator
,controller
등의 요소를 일일이import
해야 하는데 번거롭다.- 이후에 수정하면서 컨트롤러 파일에 존재하는 미들웨어의 실행 순서가 변화해야 한다면 이를 라우터 파일에서 설정해야 하다 보니 실수가 발생할 가능성이 있다.
문제 예시
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;