[express] 라우터, 컨트롤러, 미들웨어로 나누어 관리하기

Jerry Baba·2020년 3월 28일
8
post-thumbnail
post-custom-banner

공식 문서 예제

express 공식 문서의 hello world 예제를 보면 라우팅을 다음과 같이 처리하는 것을 볼 수 있다.

app.get('/', (req, res) => res.send('Hello World!'))

코드를 분석해보자면, 라우터를 등록하기 위해 app.get을 이용했고, app.get의 인자로 엔드포인트 '/'와 컨트롤러 함수를 사용했다.

이러한 방법은 어떤 엔드포인트로 접근했을 때 어떤 동작을 하게 되는지 한 눈에 볼 수 있어 유용할 수 있다. 그런데 좀만 앱이 복잡해지면 위와 같은 방법으로 라우팅을 관리하는 것은 좋지 않다.

끔찍한 상황

예를 들어 GET /users/<:uid>/letters/라는 엔드포인트가 있다고 하자. 해당 엔드포인트는 사용자(uid)가 작성한 편지 리스트를 얻는 것이 주요 목표다. 그런데 데이터를 얻기 전 사용자 인증을 거치고 사용자 권한까지 확인해야 한다면 어떻게 할 것인가? 다음과 같이 모든 로직을 라우팅 함수에서 처리할 것인가?

app.get('/users/:uid/letters/', async (req, res) => {
  // 사용자 인증
  const { authorization: token }  = req.headers;
  const secret = process.env.JWT_SECRET;
  
  try {
    const { id: requesterId } = jwt.verify(token, secret);
  } catch(e) {
    res.status(401).json({ status: 401, name: 'Unahorized', msg: '사용자 인증 실패', data: null });
  }
  
  // 사용자 권한 확인
  const { uid } = req.params;
  if (requesterId !== Number(uid)) {
    res.status(403).json({ status: 403, name: 'Forbidden', msg: '사용자 권한 확인 실패', data: null });
  }
  
  // 메인 로직 시작
  const user = await User.findByPk(uid);
  const letters = await user.getLetters();
                   
  res.json({ status: 200, name: 'OK', msg: `사용자 ${uid}의 편지 리스트를 가져왔습니다.`, data: letters });
})

이는 좋지 않은 방법이다. 간단한 로직이고 간단히 쓰려고 한건데도 벌써 코드가 비대해지기 시작했다. 코드를 좀 더 깔끔히 관리할 수 있는 좋은 해법은 라우터, 컨트롤러, 미들웨어로 나누는 것이다.

라우터, 컨트롤러, 미들웨어로 나누기

  • 라우터: 엔드포인트와 해당 엔드포인트에서 실행돼야 할 로직을 연결해주는 역할
  • 컨트롤러: 미들웨어의 일종이지만 메인 로직을 담당하므로 분리해서 관리
  • 미들웨어: 메인 로직의 컨트롤러 앞뒤로 추가적인 일을 담당

위의 예시를 라우터, 컨트롤러, 미들웨어로 분리하면 다음과 같이 나누어서 쓸 수 있다.

/**
* 컨트롤러
*/
const getUserLetters = async (req, res, next) => {
  const { uid } = req.params;
  
  const user = await User.findByPk(uid);
  const letters = await user.getLetters();
  
  res.json({ status: 200, name: 'OK', msg: `사용자 ${uid}의 편지 리스트를 가져왔습니다.`, data: letters });
}
/**
* 미들웨어
*/
const authentication = (req, res, next) => {
  const { authorization: token } = req.headers;
  const secret = process.env.JWT_SECRET;
  
  try {
    const { id: requesterId } = jwt.verify(token, secret);
  } catch(e) {
    return res.status(401).json({ status: 401, name: 'Unahorized', msg: '사용자 인증 실패', data: null });
  }
  
  next();
}

const authorization = (req, res, next) => {
  const { uid } = req.params;
  const { authorization: token } = req.headers;
  const { id: requesterId } = jwt.decode(token);
  
  if (requesterId !== Number(uid)) {
    return res.status(403).json({ status: 403, name: 'Forbidden', msg: '사용자 권한 확인 실패', data: null });
  }
  
  next();
}
/**
* 라우터
*/
app.get('/users/:uid/letters/', authentication, authorization, getUserLetters);

역할과 책임이 나누어져서 더 관리하기 쉬워졌고, 보기도 좋아졌다.

만약에 /users/:uid/contacts/라는 새로운 엔드포인트와 api가 필요하다면, 연락처를 가져오는 메인 로직만 새로 만들면 되고 사용자 인증과 사용자 권한 확인은 라우터에서 구현돼있는 것을 붙여주기만 하면 된다.

만약 클라이언트에서 보낸 요청 데이터의 유효성을 검증하는 validation을 구현하기로 했다면 어떨까? 미들웨어로서 이를 추가로 구현하고 라우터에 붙여주기만 하면 된다.

위 코드에서는 에러 처리를 각 미들웨어나 컨트롤러에서 처리했지만 에러핸들러 미들웨어를 따로 만들어 에러 처리를 모두 거기에서 처리할 수도 있다. 에러핸들러는 특별하게 생겼기 때문에 공식 문서에서도 따로 설명해 놓았다. 필요하다면 참고해보기 바란다.

profile
행복한 인생의 성장을 추구합니다.
post-custom-banner

3개의 댓글

comment-user-thumbnail
2020년 8월 13일

개념이 깔끔하게 정리되어 있어서 도움이 많이 되었습니다 감사합니다.
질문이 하나 있는데 미들웨어라고 정의하신건 사용자가 사용하는 여러 로직들을 통칭하신건가요?

1개의 답글
comment-user-thumbnail
2021년 3월 4일

express를 이제 공부하기 시작해서, 미들웨어랑 라우터의 관계,차이점이 궁금했는데 덕분에 이해가 잘 되었습니다. 감사합니다!

답글 달기