2024. 9. 4. 21:20ㆍ[Node.js_6기 본캠프 TIL]
▶미들웨어 (Middleware)
서버의 요청(Request) - 응답(Response) 과정 중간에서 특정 기능을 수행하는 함수라고 할 수 있다. 보통 웹 서버에서 요청을 받을때, 모든 요청에 대한 공통적인 처리를 하고 싶을 때 사용한다. 모든 요청에 대한 로그를 남기거나, 특정 사용자만 접근할 수 있게 하는 등의 처리가 필요한 상황 등을 예로 들 수 있다. 또한 사용자가 웹 페이지에서 Form을 통해 전송한 데이터를 서버에서 쉽게 파싱(Body Parser)하여 사용할 수 있게 해주는 미들웨어도 존재한다.
Node.js는 Express.js라는 웹 애플리케이션 프레임워크를 통해 미들웨어를 지원한다. 자주 쓰이는 미들웨어의 형태는 하기와 같다.
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
- urlencoded: form-urlencoded 라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어
- json: JSON 이라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어
위의 예시보다 더 다양한 미들웨어를 사용하고 싶을 경우, 공통적으로 사용하는 인터페이스는 하기와 같다.
app.use((req, res, next) => {
// 필요한 코드
});
- req: 요청(Request)에 대한 정보가 담겨있는 객체
- HTTP Headers, Query Parameters, URL 등 '브라우저가 서버로 보내는 정보'들이 담겨있다.
- res: 응답(Response)을 위한 기능이 제공된다
- 어떤 HTTP Status Code로 응답 할지, 어떤 데이터 형식으로 응답 할지, 헤더는 어떤 값을 넣어 응답 할지 등, 다양한 기능을 제공한다.
- next: 다음 스택으로 정의된 미들웨어를 호출한다.
다양한 기능을 활용할 수 있으며, 관리가 용이한 만큼 필요에 따라 적극적으로 활용할 필요가 있다. 다만 코드상 위에서 아래 방향으로 순차적으로 실행되기 때문에, 순서를 고려하지 않고 사용하다보면 예기치 못한 오류가 발생할 수 있다.
▽ 예시 코드
// app.js
import express from 'express';
const app = express();
const PORT = 3000;
app.use((req, res, next) => {
console.log('첫번째 미들웨어');
next();
});
app.use((req, res, next) => {
console.log('두번째 미들웨어');
next();
});
app.get('/', (req, res, next) => {
console.log('GET / 요청이 발생했습니다.');
next();
});
app.use((req, res, next) => {
console.log('세번째 미들웨어');
res.json({ message: 'Hi' });
});
app.use((req, res, next) => {
console.log('네번째 미들웨어');
res.json({ message: '마지막 미들웨어 입니다.' });
});
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
이 예시 코드를 실행하면 어떻게 될까? 터미널에는 '첫번째 미들웨어'부터 순차적으로 찍혀, '세번째 미들웨어'까지 찍히고 종료될 것이다. next() 메서드를 실행하지 않아서 더 이상 다음 미들웨어로 넘어가지 않기 때문이다. 그럼 세번째 미들웨어에서 next() 메서드를 실행하면 어떻게 될까? 이미 세번째 미들웨어에서 클라이언트에게 res.json으로 Hi라는 메세지를 보낸 상태이기 때문에 네번째 미들웨어의 res.json과 충돌하여 오류가 발생하게 된다. 때문에 미들웨어 자체에서 res.json으로 응답을 보내는 것은 권장되지 않는다.
★ 요청에 따라 Express.js의 미들웨어가 실행되도록 설정하기
- app.use(Middleware) : 모든 요청에서 미들웨어가 실행된다.
- app.use('/api', Middleware) : /api로 시작하는 모든 요청에서 미들웨어를 실행한다.
- app.post('/api', Middleware, (req,res,)=>{} ) : /api로 시작하는 `POST` 요청에서 미들웨어를 실행한다.
★ Router와 미들웨어의 차이
Router는 미들웨어 기반으로 구현된 객체이므로 미들웨어와 동일한 방식으로 작동한다. 즉, Router는 미들웨어 함수를 특정 경로에 바인딩하는 역할을 하며, 요청이 들어온 URL 경로에 따라 서로 다른 미들웨어를 실행시킬 수 있게 도와준다고 할 수 있다.
▶ 데이터 유효성 검증 라이브러리 Joi
Joi는 JavaScript 유효성 검증을 위한 라이브러리로, 여러 타입과 규칙을 이용해 간단하게 유효성을 검증할 수 있다. 유효성 검증에 실패하면 오류를 발생시킨다. 특정 사이트에 회원가입을 할 때, 아이디는 영문과 숫자 조합으로만 생성가능하고 비밀번호는 10글자 이상의 숫자+영문+특수문자의 조합으로만 생성 가능한 경우를 많이 보았을 것이다. 이때 사용되는 것이 바로 유효성 검사라고 할 수 있다.
이름을 필수값으로 받을 때, 최소 3자 / 최대 30자 / 문자열 이라는 조건을 걸어야한다면 Joi를 통해 어떻게 코드를 쓸 수 있을까?
import Joi from 'joi';
// Joi 스키마 정의
const schema = Joi.object({
name: Joi.string().min(3).max(30).required(),
});
// 검증할 데이터 정의
const user = { name: 'Foo Bar' };
// schema를 통한 user 데이터 검증
const validation = schema.validate(user);
// 검증 결과값 중 error가 존재한다면, 에러 메시지 출력
if (validation.error) {
console.log(validation.error.message);
} else {
// 검증 결과값 중 error가 존재하지 않는다면, 데이터가 유효하다는 메시지 출력
console.log('Valid Data!');
}
우선 Joi가 VScode에 설치되어 있다는 전제 하에, import 를 통해 라이브러리 Joi를 가져온다. 그리고 위와 같이 schema를 선언하고 Joi.object를 사용하여 name에 조건을 걸어준다. 걸어준 조건을 검증하기 위해 schema.validate을 사용하여 하나라도 조건에 맞지 않는 요청값이 들어왔을 경우, error 메세지가 출력되도록 세팅한다.
★ 라이브러리 사용 시 주의!
라이브러리를 import하여 사용하다 보면, 내가 생각한 것과 다른 형태로 자동완성되거나 대소문자를 혼용하는 경우가 있다. 오늘 이 글을 정리하게 된 이유 중 하나도 라이브러리 사용이 미숙한 바람에 한참을 에러 메세지를 읽어야만 했기 때문이다.
특정 라이브러리에서 특정 함수를 가져오는 경우, 함수명과 위치를 정확히 기재해야 한다. 저번주까지만 해도 이 방식으로만 라이브러리를 써봤기 때문에, 모든 라이브러리가 똑같이 작동하는 줄 알았다. 자동완성이 되면... 정해진 데이터를 불러오는 것이니까 오류가 날 리 없다고 생각했다. 그러나 Joi처럼 라이브러리를 통으로 가져올 땐, import 뒤에 오는 단어가 해당 파일에서 라이브러리를 사용하는 함수명 그 자체가 된다.
예시에 있던 코드 그대로 import Joi from 'joi'; 로 라이브러리를 불러왔는데, 파일을 여러개 쓰면서 강의 파일이 import joi from 'joi';로 진행되고 있던 걸 모르던 나는 밑에 있는 코드를(joi.object 같은) 똑같이 적었는데 대체 왜 오류가 나는지 알 수가 없어서 한참을 고민해야 했다. 라이브러리를 사용할 땐 꼭 import 뒤의 단어를 유심히 살펴보자.
★ 에러 처리를 위한 try/catch 설정하기
서버에서 에러가 발생하게 된다면 클라이언트가 요청(Request)한 처리는 중단되며, 이때 서버는 클라이언트에게 응답(Response)를 보내지 못하게 된다. 이를 방지하기 위해 발생 가능한 에러를 미리 대비하는 예외 처리를 항상 고려해야 한다.
Joi의 데이터 유효성 검증을 실패한 코드에서 에러가 발생하는 경우, try/catch문으로 예외 처리를 할 수 있다.
▽ 예시 코드
router.post('/todos', async (req, res) => {
try {
// 클라이언트에게 전달받은 데이터를 검증합니다.
const validateBody = await createTodoSchema.validateAsync(req.body);
// 클라이언트에게 전달받은 value 데이터를 변수에 저장합니다.
const { value } = validateBody;
// Todo모델을 사용해, MongoDB에서 'order' 값이 가장 높은 '해야할 일'을 찾습니다.
const todoMaxOrder = await Todo.findOne().sort('-order').exec();
// 'order' 값이 가장 높은 도큐멘트의 1을 추가하거나 없다면, 1을 할당합니다.
const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;
// Todo모델을 이용해, 새로운 '해야할 일'을 생성합니다.
const todo = new Todo({ value, order });
// 생성한 '해야할 일'을 MongoDB에 저장합니다.
await todo.save();
return res.status(201).json({ todo });
} catch (error) {
console.error(error);
// Joi 검증에서 에러가 발생하면, 클라이언트에게 에러 메시지를 전달합니다.
if (error.name === 'ValidationError') {
return res.status(400).json({ errorMessage: error.message });
}
// 그 외의 에러가 발생하면, 서버 에러로 처리합니다.
return res
.status(500)
.json({ errorMessage: '서버에서 에러가 발생하였습니다.' });
}
});
try 블록은 할 일을 생성하는 비즈니스 로직을 실행하고, catch 블록은 에러를 처리하도록 구현되어 있다. (그 외에 Joi가 아닌 서버에서 발생한 오류는 따로 오류 코드를 처리한다.)
'[Node.js_6기 본캠프 TIL]' 카테고리의 다른 글
AWS에서 사라진 EC2 찾기 (1) | 2024.09.06 |
---|---|
[프로그래머스] 푸드 파이트 대회(JavaScript) (1) | 2024.09.05 |
[알고리즘 코드카타] 두 개 뽑아서 더하기(JavaScript) (0) | 2024.09.03 |
[Node.js_6기] 본캠프 TIL_0902 (0) | 2024.09.02 |
[Node.js_6기] 본캠프 TIL_0830 (0) | 2024.08.30 |