본문 바로가기
개발/웹 개발

Day 26: REST API 서버 (라우팅과 컨트롤러 분리)

1. 오늘의 학습 목표

학습 목표: 애플리케이션의 규모가 커짐에 따라 API 코드를 체계적으로 구성하는 방법을 배웁니다. Express의 Router를 사용하여 경로 설정(Routing)을 별도의 파일로 분리하고, 실제 비즈니스 로직을 담당하는 컨트롤러(Controller) 함수를 만들어 코드의 가독성과 유지보수성을 높이는 방법을 익힙니다.

핵심 요약: '라우터-컨트롤러 패턴'은 API의 주소와실제 행동을 분리하는 것입니다. 이 패턴을 적용하면 코드가 훨씬 더 깔끔해지고, 기능 확장이 용이해집니다.


2. 핵심 개념 파헤치기

2.1. 문제점: 비대해지는 서버 파일

Day 25에서는 server.js 파일 하나에 모든 라우팅 코드를 작성했습니다. 하지만 사용자를 관리하는 API, 게시물을 관리하는 API 등 기능이 추가될수록 server.js 파일은 수백, 수천 줄로 길어지고 관리하기가 매우 어려워집니다.

 

2.2. 해결책: 라우터-컨트롤러 패턴

이 문제를 해결하기 위해 관심사 분리(Separation of Concerns) 원칙에 따라 코드의 역할을 나눕니다

  • 라우터 : 교통 경찰 같은 역할을 합니다. 특정 경로(URI)와 HTTP 메서드(GET, POST 등)의 요청이 들어왔을 때, 이 요청을 처리할 담당자(컨트롤러 함수)에게 연결해주는 역할만 담당합니다. express.Router()를 사용하여 만듭니다
  • 컨트롤러 : 실무 담당자 역할을 합니다. 라우터로부터 요청을 넘겨받아, 실제 비즈니스 로직(데이터베이스 조회, 데이터 가공 등)을 수행하고 클라이언트에게 최종 응답을 보냅니다

2.3. 새로운 프로젝트 구조

이 패턴을 적용하면 프로젝트 구조가 바뀝니다

/project
  ├── server.js         //서버 실행 및 기본 설정
  ├── /routes           //라우터 파일들을 모아두는 폴더
  │   └── userRoutes.js
  │   └──postRoutes.js
  └── /controllers      //컨트롤러 함수들을 모아두는 폴더
      └──userController.js
      └──postController.js

3. 코드로 배우기

3.1. 구현 목표

사용자(user) 관련 API를 server.js에서 라우터와 컨트롤러 파일로 분리하여 리팩토링합니다.

 

3.2. [1단계: 컨트롤러 로직 분리하기

기존 server.js에 있던 라우트 핸들러 콜백 함수들을 별도의 파일로 옮기고, exports를 이용해 외부에서 사용할 수 있도록 만듭니다.

//controllers/userController.js

//모든 사용자 조회
exports.getAllUsers = (req, res) => {
  res.send('Get all users');
};

	//특정 사용자 조회
exports.getUserById = (req, res) => {
  res.send(`Get user with ID: ${req.params.id}`);
};

  //새로운 사용자 생성
exports.createUser = (req, res) => {
  res.send('Create a new user');
};

 

3.3. [2단계: 라우터 설정하기 (routes/userRoutes.js)]

express.Router()를 사용하여 user와 관련된 경로들을 정의합니다. 각 경로에는 방금 만든 컨트롤러 함수를 연결합니다.

//routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);

module.exports = router;

 

3.4. [3단계: 메인 서버 파일 정리하기 (server.js)]

메인 서버 파일에서는 분리한 라우터를 가져와 특정 기본 경로(prefix)에 연결해주는 역할만 합니다. 코드가 매우 간결해집니다.

//server.js
const express = require('express');
const app = express();
const port = 3000;

// 유저 라우터를 불러옵니다.
const userRouter = require('./routes/userRoutes');

//'/api/users' 경로로 들어오는 모든 요청은 userRouter가 처리합니다.
app.use('/api/users', userRouter);

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});

4. 전체 코드

controllers/userController.js

//controllers/userController.js

//모든 사용자 조회 로직
exports.getAllUsers = (req, res) => {
  //실제로는 DB에서 모든 유저를 조회
  res.status(200).json({ status: 'success', data: 'All users here' });
};

//특정 사용자 조회 로직
exports.getUserById = (req, res) => {
  const { id } = req.params;
  //실제로는 DB에서 id로 유저를 조회
  res.status(200).json({ status: 'success', data: `User ${id} here` });
};

//사용자 생성 로직
exports.createUser = (req, res) => {
  //실제로는 req.body의 정보로 DB에 유저를 생성
  res.status(201).json({ status: 'success', message: 'User created' });
};

 

routes/userRoutes.js

//routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

//GET /api/users/ 와 POST /api/users/ 에 대한 라우트
router
  .route('/')
  .get(userController.getAllUsers)
  .post(userController.createUser);

//GET /api/users/:id 에 대한 라우트
router.route('/:id').get(userController.getUserById);

module.exports = router;

 

server.js

//server.js
const express = require('express');
const app = express();
const port = 3000;

const userRouter = require('./routes/userRoutes');
//const postRouter = require('./routes/postRoutes'); // 게시물 라우터도 추가 가능

app.get('/', (req, res) => {
  res.send('API is running...');
});

//'/api/users' 라는 경로의 요청들은 모두 userRouter에게 넘겨줍니다.
app.use('/api/users', userRouter);
//app.use('/api/posts', postRouter);

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});