architecture2025-05-08·8·81/348

Express.js Router 중첩 라우팅 완벽 가이드

Express.js에서 라우터를 중첩하여 구조화된 API를 만드는 방법과 그 장단점을 다룹니다.

Introduction

Express.js 애플리케이션이 커지면서 라우터를 구조화하는 것이 중요해집니다. 중첩 라우팅은 API를 논리적으로 그룹화하고 관리하기 쉬운 구조를 만드는 데 유용합니다.

Environment

npm list express
# express@4.18.2

node --version
# v20.11.0

Problem

모든 라우트가 하나의 파일에 정의되어 있어 유지보수가 어려웠습니다:

// 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

  1. mergeParams 활용: 중첩 라우터에서 부모 파라미터에 접근하려면 mergeParams: true를 사용하세요
  2. 모듈화: 각 리소스별로 라우터 파일을 분리하여 유지보수성을 높이세요
  3. 라우터 팩토리: CRUD 패턴이 반복될 때 팩토리 패턴을 활용하세요
  4. 에러 핸들링: 각 라우터에서 에러를 적절히 처리하고 다음 미들웨어로 전달하세요
  5. 성능 고려: 불필요한 미들웨어 체이닝을 줄여 성능을 최적화하세요

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.