Express.js Router 중첩 라우팅 완벽 가이드
Express.js에서 라우터를 중첩하여 구조화된 API를 만드는 방법과 그 장단점을 다룹니다.
Introduction
Express.js 애플리케이션이 커지면서 라우터를 구조화하는 것이 중요해집니다. 중첩 라우팅은 API를 논리적으로 그룹화하고 관리하기 쉬운 구조를 만드는 데 유용합니다.
Environment
npm list express
# express@4.18.2
node --version
# v20.11.0Problem
모든 라우트가 하나의 파일에 정의되어 있어 유지보수가 어려웠습니다:
// app.js - 모든 라우트가 하나의 파일에
const express = require('express');
const app = express();
// 수백 개의 라우트가 여기에...
app.get('/api/users', getUsers);
app.get('/api/users/:id', getUserById);
app.post('/api/users', createUser);
app.put('/api/users/:id', updateUser);
app.delete('/api/users/:id', deleteUser);
app.get('/api/products', getProducts);
app.get('/api/products/:id', getProductById);
// ... 계속 반복Analysis
라우터 구조 설계
routes/
├── index.js # 메인 라우터
├── users.js # /api/users
├── products.js # /api/products
├── orders.js # /api/orders
└── middleware/
├── auth.js # 인증 미들웨어
└── validate.js # 검증 미들웨어Solution
1. 기본 중첩 라우팅
// routes/users.js
const express = require('express');
const router = express.Router();
// 라우터 수준 미들웨어
router.use((req, res, next) => {
console.log('User route accessed');
next();
});
router.get('/', async (req, res) => {
const users = await User.find();
res.json(users);
});
router.get('/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
router.post('/', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
module.exports = router;
// routes/index.js
const express = require('express');
const router = express.Router();
const usersRouter = require('./users');
const productsRouter = require('./products');
router.use('/users', usersRouter);
router.use('/products', productsRouter);
module.exports = router;
// app.js
const express = require('express');
const apiRouter = require('./routes');
const app = express();
app.use('/api', apiRouter);2. 깊은 중첩 라우팅
// routes/users/posts.js
const express = require('express');
const router = express.Router({ mergeParams: true }); // 부모 파라미터 접근
// /api/users/:userId/posts
router.get('/', async (req, res) => {
const { userId } = req.params;
const posts = await Post.find({ author: userId });
res.json(posts);
});
router.post('/', async (req, res) => {
const { userId } = req.params;
const post = await Post.create({
...req.body,
author: userId,
});
res.status(201).json(post);
});
// /api/users/:userId/posts/:postId
router.get('/:postId', async (req, res) => {
const { userId, postId } = req.params;
const post = await Post.findOne({ _id: postId, author: userId });
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
module.exports = router;
// routes/users.js
const express = require('express');
const router = express.Router();
const postsRouter = require('./posts');
// 중첩 라우팅
router.use('/:userId/posts', postsRouter);
module.exports = router;3. 라우터 팩토리 패턴
// factories/routerFactory.js
function createResourceRouter(resource, controller) {
const router = express.Router();
router.get('/', async (req, res, next) => {
try {
const items = await controller.findAll(req.query);
res.json(items);
} catch (err) {
next(err);
}
});
router.get('/:id', async (req, res, next) => {
try {
const item = await controller.findById(req.params.id);
if (!item) {
return res.status(404).json({ error: `${resource} not found` });
}
res.json(item);
} catch (err) {
next(err);
}
});
router.post('/', async (req, res, next) => {
try {
const item = await controller.create(req.body);
res.status(201).json(item);
} catch (err) {
next(err);
}
});
router.put('/:id', async (req, res, next) => {
try {
const item = await controller.update(req.params.id, req.body);
res.json(item);
} catch (err) {
next(err);
}
});
router.delete('/:id', async (req, res, next) => {
try {
await controller.delete(req.params.id);
res.status(204).end();
} catch (err) {
next(err);
}
});
return router;
}
// 사용 예시
const usersRouter = createResourceRouter('User', userController);
const productsRouter = createResourceRouter('Product', productController);
router.use('/users', usersRouter);
router.use('/products', productsRouter);4. 조건부 라우팅
// middleware/featureToggle.js
function featureToggle(featureName) {
return (req, res, next) => {
if (isFeatureEnabled(featureName)) {
next();
} else {
res.status(404).json({ error: 'Feature not available' });
}
};
}
// 조건부 라우팅 적용
const router = express.Router();
// 기능 토글에 따라 라우트 활성화
router.use('/beta', featureToggle('beta-features'), betaRouter);
router.use('/v2', featureToggle('api-v2'), v2Router);5. 라우터 모듈화
// routes/index.js
const express = require('express');
const router = express.Router();
const loadRoutes = () => {
const routes = [
{ path: '/users', handler: './users' },
{ path: '/products', handler: './products' },
{ path: '/orders', handler: './orders' },
];
routes.forEach(({ path, handler }) => {
const routeModule = require(handler);
router.use(path, routeModule);
});
};
loadRoutes();
module.exports = router;Lessons Learned
- mergeParams 활용: 중첩 라우터에서 부모 파라미터에 접근하려면
mergeParams: true를 사용하세요 - 모듈화: 각 리소스별로 라우터 파일을 분리하여 유지보수성을 높이세요
- 라우터 팩토리: CRUD 패턴이 반복될 때 팩토리 패턴을 활용하세요
- 에러 핸들링: 각 라우터에서 에러를 적절히 처리하고 다음 미들웨어로 전달하세요
- 성능 고려: 불필요한 미들웨어 체이닝을 줄여 성능을 최적화하세요
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.