이전 시간에는 프로젝트에 사용자 인증 미들웨어를 추가했다.
이번에는 여러 미들웨어를 추가해볼 예정이다.
로그(Log) 미들웨어
로그 미들웨어는 클라이언트의 모든 요청 사항을 기록하여 서버의 상태를 모니터링하기 위한 미들웨어다. 이렇게 하면 문제가 발생할 때 빠르게 진단할 수 있다는 장점이 있다. 또한 로그 데이터를 바탕으로 사용자의 행동을 분석하는 등 데이터 분석 작업에도 활용할 수 있다.
그러나 규모가 큰 프로젝트에선 모든 로그를 일일이 확인하는 건 불가능하다.
이런 경우를 대비해 로그 기능을 지원하는 morgan, winston과 같은 라이브러리를 사용하거나, AWS CloudWatch, Datadog와 같은 외부 모니터링 솔루션 서비스를 이용해 로그를 수집하거나 관리할 수 있다.
이번에는 winston 라이브러리를 이용해 미들웨어를 구현해 볼 것이다.
로그 미들웨어
# yarn을 이용해 winston을 설치합니다.
yarn add winston
// src/middlewares/log.middleware.js
import winston from "winston";
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(), // 실제 winston이 어떤 형태로 출력할 것인지(여기서는 json 형태로 출력)
transports: [
new winston.transports.Console() // 로그를 콘솔에 출력
]
});
// API가 실행됐을 때, 로그 미들웨어 호출
// 이후 API의 res가 정상적으로 반환이 되면
// res.on()이 실행
export default function(req, res, next) {
const start = new Date().getTime(); // 해당하는 로그가 언제 시작했는지 확인하기 위해 시간 정보 적용
// 해당하는 응답이 완료됐을 때 로그를 출력한다.
res.on('finish', () => {
const duration = new Date().getTime() - start;
// logger의 info 레벨을 출력
// 메서드와 URL, status, 비즈니스 로직이 수행된 시간 출력
logger.info(`Method: ${req.method}, URL: ${req.url}, Station: ${res.statusCode}, Duration: ${duration}ms`);
})
next();
}
// app.js
import LogMiddleware from './middlewares/log.middleware.js';
// 중략
app.use(LogMiddleware);
// 중략
이후 아무 API를 사용해보면 다음과 같은 결과를 볼 수 있다.
!!!! 알아둘 것 !!!!
로그 레벨은 로그의 중요도를 나타낸다. 지금은 단순히 클라이언트의 요청 사항을 기록하기 위해 "info"를 사용했지만, error, warn, debug 등 다양한 로그 레벨이 있으며, 특정 상황에 따라 출력하는 레벨을 다르게 구현할 수 있다.
또한 로그 미들웨어는 클라이언트의 요청이 발생했을 때, 가장 먼저 실행되어야 하는 미들웨어다. 따라서, app.use() 중에서 가장 최상단에 위치한다.
에러 처리 미들웨어
이전에도 에러 처리 미들웨어를 구현했다. 이와 같은 방식으로 이번 프로젝트에서도 에러 처리 미들웨어를 구현하고자 한다.
그런데 나는 이전에 사용자 인증 미들웨어를 작성하면서 전역 에러 처리 미들웨어를 작성한 적 있다(링크). 또한 회원가입 API 역시 asyncHandler로 관리하고 있어, 강의에서처럼 try/catch를 사용하긴 애매하다.
무엇보다 회원가입 API 자체적으로 오류가 발생했을 때, return으로 즉시 반환하기 때문에 에러 처리 미들웨어로 갈 일이 없다.
따라서 이번에 내가 해야할 건, 에러 처리 미들웨어를 한 곳에 정리하고, 에러가 발생했을 시, 즉시 return으로 반환하지 않고 에러 처리 미들웨어로 넘기는 회원가입 API로 리팩토링하는 것이다.
// 회원가입 API
router.post('/sing-up', asyncHandler(async (req, res, next) => {
const {email, password, name, age, gender, profileImage} = req.body;
const isExistUser = await prisma.users.findFirst({
where: {
email
}
});
// if(isExistUser)
// return res.status(409).json({message: '이미 존재하는 이메일이다.'});
if (isExistUser) {
const error = new Error('이미 존재하는 이메일이다.');
error.status = 409; // 충돌(Conflict) 상태 코드
throw error;
}
// 사용자 비밀번호를 암호화합니다.
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.users.create({
data: {
email,
password: hashedPassword
}
});
const userInfo = await prisma.usersInfos.create({
data: {
usersId: user.usersId, // 사용자의 usersId를 바탕으로 사용자 정보 usersId 생성
name,
age,
gender: gender.toUpperCase(),
profileImage
}
});
return res.status(201).json({message: '회원가입 완료!'});
}));
// src/middlewares/error-handling.middleware.js
export default function (err, req, res, next) {
console.error(err); // 에러 로그 출력
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: '토큰이 만료되었다.' });
}
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ message: '토큰이 조작되었다.' });
}
if (err.status === 409) {
return res.status(409).json({ message: err.message });
}
res.status(500).json({ message: err.message || '서버 내부 에러가 발생했습니다.' });
}
// src/app.js
// 중략
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
// 중략
app.use('/api', [UsersRouter]);
app.use(ErrorHandlingMiddleware);
// 전역 에러 처리 미들웨어
// app.use((err, req, res, next) => {
// switch(err.name) {
// case 'TokenExpiredError':
// return res.status(401).json({ message: '토큰이 만료되었다.' });
// case 'JsonWebTokenError':
// return res.status(401).json({ message: '토큰이 조작되었다.' });
// default:
// res.status(500).json({ message: err.message ?? '비정상적인 요청이다' });
// }
// });
app.listen(PORT, () => {
console.log(`서버가 ${PORT}번 포트에서 실행 중입니다.`);
});
이처럼 에러 처리 미들웨어는 클라이언트의 요청이 실패했을 때, 가장 마지막에 실행되어야 한다.
또한 한가지 주의해야 할 점은, 모든 에러를 관리하는 미들웨어, 서버 내부에서 발생한 에러를 상세하게 클라이언트에게 제공한다면 악의적인 사용자에게 표적이 될 수 있다. 그렇기 때문에 "서버 내부 에러가 발생했다" 처럼 추상적인 내용을 클라이언트에게 전달하는 게 좋다.
'프로젝트' 카테고리의 다른 글
24/09/17 - [실습] 나만의 게시판(6): 댓글 API (0) | 2024.09.17 |
---|---|
24/09/17 - [실습] 나만의 게시판(5): 게시글 API (0) | 2024.09.17 |
24/09/13 - [실습] 나만의 게시판(3): 사용자 정보 조회 API(사용자 인증 미들웨어) (0) | 2024.09.13 |
24/09/13 - [실습] 나만의 게시판(2): 회원가입&로그인 API 구현(인증/인가, asyncHandler, 암호화(bcrypt 모듈), JWT 토큰과 쿠키) (0) | 2024.09.13 |
24/09/12 - [실습] 나만의 게시판(1): DB 설계 (0) | 2024.09.12 |