BackEnd/Express

[Express] middleware

Grace 2023. 4. 19. 10:06

미들웨어 함수는 요청 오브젝트(req), 응답 오브젝트(res), 그리고 애플리케이션의 요청-응답 주기 중 그 다음의 미들웨어 함수에 대한 액세스 권한을 갖는 함수입니다. 그 다음의 미들웨어 함수에 액세스하기 위해서 next()를 사용합니다.

라우터, 컨트롤러, 미들웨어
라우터: 엔드포인트와 해당 엔드포인트에서 실행돼야 할 로직(함수)를 연결해주는 역할
컨트롤러: 미들웨어의 일종이지만 메인 로직을 담당
미들웨어 : 메인 로직(미들웨어)의 컨트롤러 앞 뒤로 추가적인 로직 담당
이런 식으로 역할과 책임을 나누어서 관리해주면 코드 가독성이 쉬워지고 유지보수에 좋습니다.
각 코드 예시는 아래와 같습니다.
// 라우터
app.get('/users/:uid/letters/', authentication, authorization, getUserLetters);
// 컨트롤러
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();
}
라우터 또한 모듈화 할 수 있습니다. 자세한 사항은 https://metadataplanning.atlassian.net/wiki/spaces/data/pages/101515383 문서를 참조하세요!

미들웨어 함수는 다음과 같은 태스크를 수행합니다.

  • 모든 코드 실행
  • 요청/응답 오브텍트에 대한 변경
  • 요청-응답 주기 종료
  • 스택 내의 다음 미들웨어 호출

현재의 미들웨어가 요청-응답을 종료하지 않는 경우 다음 미들웨어 함수에 제어를 전달해야 합니다. 그렇지 않으면 해당 요청이 정지된 채로 방치되기 때문입니다.

미들웨어 함수를 로드하려면 미들웨어 함수를 지정하여 app.use()를 호출해야 합니다.

const express = require('express');
const router = express.Router(); // 라우터 모듈화
const isAuth = require('../../middleware/auth.js');
...

// 인가를 확인하는 미들웨어 함수 로드
router.use(isAuth);

// 미들웨어 함수를 통해 인가가 확인되면 루트 경로에서 createBoard API 실행
router.post('/', boardController.createBoard);

다음과 같은 유형의 미들웨어를 사용할 수 있습니다.

  • 애플리케이션 레벨 미들웨어
  • 라우터 레벨 미들웨어
  • 오류 처리 미들웨어
  • 기본 제공 미들웨어
  • 써드파티 미들웨어

애플리케이션 레벨 및 라우터 레벨 미들웨어는 선택적인 마운트 경로를 통해 로드할 수 있습니다. 일련의 미들웨어 함수를 함께 로드할 수도 있으며, 이를 통해 하나의 마운트 위치에 미들웨어 시스템의 하위 스택을 작성할 수 있습니다.

애플리케이션 레벨 미들웨어

app.use() 혹은 app.METHOD() 함수를 통해 미들웨어를 앱에 바인딩 할 수 있습니다.
다음과 같이 마운트 경로가 없는 경우 앱이 요청을 수신할 때마다 실행됩니다.

const express = require('express');
const pageAuth = require('../middleware/pageAuth');
...

const app = express();
...
app.use(pageAuth);

만약 마운트 경로가 표시된다면, 표시된 경로에 대한 요청이 있을 때 실행됩니다.
next()를 사용하면 다음 미들웨어로 넘어가고, next('route')를 사용하면 다음 라우터로 넘어갑니다.

...

app.get('/', (req, res, next) => {
    if (false) {
    	next(); // 다음 미들웨어로 넘어간다.
    } else {
    	next('route'); // next()에 'route'인수를 주게되면, 다음 미들웨어가 아닌, 다음 라우터로 넘어가게 된다. 
    }
    
}, (req, res) => { 
	// ... next()면 실행되지만,
	// ... next('route')면 실행되지 않는다.
});


app.get('/', (req, res, next) => { 
	// ... next('route')면, 그 다음 라우터인 이쪽이 실행된다.
})

라우터 레벨 미들웨어

앞서 미들웨어를 설명드리면서 라우터 레벨 미들웨어는 이미 보여드렸습니다.
express.Router() 인스턴스에 바인딩되는 점만 빼면 애플리케이션 레벨 미들웨어와 동일한 방식으로 동작합니다.

const express = require('express');
const router = express.Router(); // 라우터 모듈화
const isAuth = require('../../middleware/auth.js');
...

// 인가를 확인하는 미들웨어 함수 로드
router.use(isAuth);

// 미들웨어 함수를 통해 인가가 확인되면 루트 경로에서 createBoard API 실행
router.post('/', boardController.createBoard);

오류 처리 미들웨어

오류(에러) 처리 미들웨어는 매개변수가 err, req, res, next로 네 개입니다. 모든 매개변수를 사용하지 않더라도 매개벼수가 반드시 네 개여야 합니다.

app.use((err, req, res, next) => {
...
})

기본 제공 미들웨어

express.static을 제외하면 모든 미들웨어 함수는 이제 별도의 모듈로 분리됩니다.
기본의 정적파일 제공 부분에서 설명 드린 적이 있습니다.https://metadataplanning.atlassian.net/wiki/spaces/data/pages/101744675
여기에 추가로 옵션을 붙일 수도 있습니다.

const options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now());
  }
}

app.use(express.static('public', options));

써드파티 미들웨어

morgan

const morgan = require('morgan')
...
app.use(morgan('dev')) // combined, common, short, tiny, ...

morgan에 연결 후 localhost:3000에 다시 잡속해보면 기존 로그 외에 추가적인 로그도 볼 수 있습니다.

...
GET / 500 7.409 ms - 50 

morgan 미들웨어는 요청과 응답에 대한 정보를 콘솔에 기록합니다.
안에 들어가는 인수들이 변경되는 로그도 변경됩니다. 개발 환경에서는 dev, 배포 환경에서는 combined를 사용하면 좋습니다.

body-parser

request 본문을 해석해 req.body 객체로 만들어주는 미들웨어입니다. 보통 폼 데이터나 ajax 요청의 데이터를 처리합니다. 단, 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못합니다. 이 경우에는 multer 모듈을 사용하면 됩니다.

app.use(express.json())
app.use(express.urlencoded({ extended: false })); /* false이면 노드의 쿼리스트링 모듈 사용
                                                      true이면 npm의 qs 모듈 사용 */

익스프레스 4 버전부터 body-parser 미들웨어의 일부 기능이 익스프레스에 내장되었으므로 따로 설치할 필요가 없습니다.

단, raw와 text 형식의 데이터를 해석해야 한다면 추가적으로 body-parser를 설치해야 합니다.
raw는 요청의 본문이 버퍼 데이터일 때, text는 텍스트 데이터일 떄 해석하는 미들웨어입니다.

const bodyParser = require('body-parse')
app.use('bodyParser.raw()')
app.use('bodyParser.text()')

cookie-parser

cookie-parser는 요청과 함께 전달된 쿠키를 해석해 req.cookie 객체로 만듭니다.

const cookieParser = require('cookie-parser')

app.use(cookieParser(SECRET_KEY))

해석된 쿠키들은 req.cookies 객체에 들어갑니다. 유효 기간이 지난 쿠키는 알아서 걸러냅니다.

첫 번째 인수로 비밀 키를 넣어주면 서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내 서버에서 만든 쿠키임을 검증하고 req.signedCookies 명의 객체에 key = value.sign 과 같은 모양으로 들어있습니다.

쿠키를 생성/제거하려면 res.cookie, res.clearCookie 메서드를 사용해야 합니다.

res.cookie('key', 'value', {
  expires: new Date(Date.now() + 900000),
  httpOnly: true,
  secure: true,
})
res.clearCookie('key', 'value', { httpOnly: true, secure: true });
/* 옵션에는 domain, expires, httpOnly, maxAge, path, 
    secure 등이 있습니다 */

쿠키를 지우려면 키와 값 외의 옵션도 정확히 일치해야 합니다(expires, maxAge 예외).
signed 옵션은 내가 작성한 쿠키임을 검증할 수 있으므로 보통은 true로 설정을 해주는게 좋으며, 비밀 키는 env에 COOKIE_SECRET으로 작성하시고 사용하실 때는 process.env.COOKIE_SECRET 형식으로 작성해주세요.

express-session

세션 관리용 미들웨어입니다. 로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 유용합니다. 세션은 사용자별로 req.session 객체 안에 유지됩니다.

// cookie-parser 뒤에 작성
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
  name: 'session-cookie'
}))

express-session은 인수로 세션에 대한 설정을 받습니다. resave는 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정하는 것이고, saveUninitialized는 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것입니다.
세션 관리 시 클라이언트에 쿠키를 보내는데, 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는 데 마찬가지로 비밀 키가 필요합니다.

store라는 옵션도 있는데, 현재 메모리를 세션에 저장하도록 하는 것입니다. 서버를 재시작하면 메모리가 초기화되어 세션이 모두 사라지기 때문에 배포 시에 store에 데이터베이서를 연결해 세션을 유지하는 것이 좋습니다(redis).

데이터를 요청이 끝날 때까지만 유지하고 싶어요!
미들웨어 간에 데이터를 전달할 경우, 데이터를 계속해서 유지하고 싶다면 세션에 저장하면 됩니다.
하지만, 데이터를 요청이 끝날때까지만 유지하고 싶다면 res.locals 객체를 사용하세요!
dotenv
dotenv는 process.env를 관리하기 위한 패키지입니다.
.env 파일을 읽어서 process.env로 만들어줍니다. 이렇게 process.env를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문입니다. 비밀 키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 키도 함께 유출될 수 있기 때문입니다.

'BackEnd > Express' 카테고리의 다른 글

[Express] req, res 객체  (0) 2023.04.19
[Express] Error Handling  (0) 2023.04.19
[Express] 라우팅  (0) 2023.04.19