이전 포스팅에서 데이터베이스에 연결해서 로그인, 회원가입, 글 CRUD 기능 개발을 마쳤다.
그런데 나는 쿼리 결과 값을 true인지 null인지 판단해서 사용자에게 응답을 해주는 것이 조금은 비효율적이라고 생각이 들었다.
따라서 서버가 정상적인 처리를 하지 못했을 경우에는 오류를 던지고, 오류 처리 미들웨어가 그 오류를 받아서 사용자에게 응답해주도록 코드를 수정해보려고 한다.
나는 오류를 처리하기 위해 세 개의 오류 클래스를 만들었다.
BadRequest
: 사용자가 잘못된 데이터를 보냈을 경우 ExpiredToken
: 토큰이 유효하지 않을 경우MethodNotAllowed
: 사용할 수 없는 메소드에 접근했을 경우오류 클래스는 Error 클래스를 상속받아서 만들었다.
error 폴더에 각자 파일을 생성했다.
error/badRequest.js
class BadRequest extends Error {
status = 400;
constructor(message = '잘못된 요청입니다.') {
super(message);
this.name = 'Bad Request';
}
}
module.exports = BadRequest;
error/expiredToken.js
class ExpiredToken extends Error {
status = 419;
constructor(message = '유효하지 않은 토큰입니다.') {
super(message);
this.name = 'Expired Token';
}
}
module.exports = ExpiredToken;
error/methodNotAllowed.js
class MethodNotAllowed extends Error {
status = 405;
constructor(message = '사용할 수 없는 메소드입니다.') {
super(message);
this.name = 'Method Not Allowed';
}
}
module.exports = MethodNotAllowed;
먼저, 존재하지 않는 경로로 요청이 들어왔을 때 사용할 MethodNotAllowed 부터 적용했다.
app.js
const MethodNotAllowed = require('./error/methodNotAllowed');
app.all('*', (res, req) => {
throw new MethodNotAllowed();
});
토큰 유효성을 체크하는 미들웨어에도 적용했다.
middleware/tokenVerify.js
const tokenService = require('../jwt');
const BadRequest = require('../error/badrequest');
const ExpiredToken = require('../error/expiredToken');
const tokenVerify = (req, res, next) => {
// 토큰이 없을 때
if (!req.headers.authorization) {
throw new BadRequest('토큰이 존재하지 않습니다.');
}
try {
const payload = tokenService.getPayload(req.headers.authorization);
req.user_id = payload.user_id;
} catch (error) {
// 토큰 만료됐을 때
throw new ExpiredToken();
}
next();
};
그리고 로그인, 회원가입 기능에서도 id가 없을 경우마다 체크해주지 않고 오류를 발생시키도록 했다.
user/index.js
const BadRequest = require('../error/badrequest');
// 사용자 등록
async function create({ id, password, name}) {
try {
await database.query(`
insert into users (id, password, name)
values ('${id}', '${password}', '${name}')
`);
} catch (error) {
throw new Error('사용자 등록중 오류가 발생했습니다.');
}
}
// 회원가입
app.post('/user/register', async (req, res) => {
const { id, password, name } = req.body;
// id, password, name이 있는지 체크한다.
if (!id || !password || !name) {
throw new BadRequest('id, password, name은 필수입력 사항입니다.');
}
// id는 중복되지 않도록한다.
const user = await getById(id);
if (user) {
throw new BadRequest('이미 존재하는 아이디입니다.');
}
// 사용자를 추가한다.
const result = await create(req.body);
res.send({ message: '사용자를 등록했습니다.' });
});
// 로그인
app.post('/user/login', (req, res) => {
const { id, password } = req.body;
// id, password가 있는지 체크한다.
if (!id || !password) {
throw new BadRequest('id, password는 필수입력 사항입니다.');
}
// 입력받은 id의 사용자를 찾는다.
const user = await getById(id);
if (!user) {
throw new BadRequest('존재하지 않는 사용자입니다.');
}
// 입력받은 password와 찾은 사용자의 password가 일치하는지 체크한다.
if (user.password !== password) {
throw new BadRequest('비밀번호가 일치하지 않습니다.' );
}
// 토큰을 발급한다.
res.status(200).send({ token: 'token' });
});
post 라우터에도 적용해줬다.
post/index.js
const BadRequest = require('../error/badrequest');
// id로 글 조회 함수
async function getById(_id) {
try {
const { rows } = await database.query(`
select _posts.id, name, title, content, created_on
from _posts
inner join _users
on _posts.user_id = _users.id
where _posts.id = '${_id}'
`);
if (!rows[0]) {
throw new BadRequest('존재하지 않는 글입니다.');
}
return rows[0];
} catch (error) {
throw new Error('글 조회중 오류가 발생했습니다.');
}
}
// 글 생성 함수
async function create({ user_id, title, content }) {
try {
await database.query(`
insert into _posts (id, user_id, title, content, created_on)
values ('${uuid.v1()}', '${user_id}', '${title}', '${content}',
'${new Date().toISOString()}')`);
} catch (error) {
throw new Error('글 생성 중 오류가 발생했습니다.');
}
}
// 글 수정 함수
async function update({ user_id, id, title, content }) {
const post = await getById(id);
try {
await database.query(`
update _posts
set title='${title}', content='${content}'
where id='${id}'`);
} catch (error) {
throw new Error('글 수정 중 오류가 발생했습니다.');
}
}
// 글 삭제 함수
async function remove({ user_id, _id }) {
const post = await getById(_id);
try {
await database.query(`delete from _posts where id='${_id}'`);
} catch (error) {
throw new Error('글 삭제 중 오류가 발생했습니다.');
}
}
// 글 전체 조회
router.get('/', async (req, res) => {
try {
const { rows } = await database.query(`
select _posts.id, name, title, created_on
from _posts inner join _users on _posts.user_id = _users.id
`);
res.send(rows);
} catch (error) {
res.status(400).send({ message: '글 조회중 오류가 발생했습니다.' });
}
});
// 글 단일 조회
router.get('/:id', async (req, res) => {
const post = await getById(req.params.id);
res.send(post);
});
// 글 등록
router.post('/', async (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
throw new BadRequest('title, content는 필수 입력 사항입니다.');
}
const result = await create({ title, content });
res.send({ message: '글을 등록했습니다.' });
});
// 글 수정
router.put('/:id', async (req, res) => {
const { title, content } = req.body;
if (!req.params.id || !title || !content) {
throw new BadRequest('id, title, content는 필수 입력 사항입니다.');
}
const result = await update({ id: req.params.id, user_id: req.user_id, title, content });
res.send({ message: '글을 수정했습니다.' });
});
// 글 삭제
router.delete('/:id', async (req, res) => {
if (!req.params.id) {
throw new BadRequest('id는 필수 입력 사항입니다.');
}
const result = await remove({ id: req.params.id, user_id, req.user_id });
res.send({ message: '글을 삭제했습니다.' });
});
if 조건문으로 판단하는 로직이 사라져서 훨씬 깔끔해졌다.
마지막으로 오류 처리 미들웨어에서 오류를 받아서 사용자에게 상태 코드와 메시지를 반환해주도록 수정했다.
app.js
app.use((error, req, res, next) => {
res
.status(error.status || 500)
.send({
name: error.name || 'Internal Server Error',
message: error.message || '서버 내부에서 오류가 발생했습니다.'
});
});
만약 error의 status, 메시지 등이 없다면 서버 내부의 오류로 생각하고 대응하도록 했다.
API 호출을 하는 테스트를 했는데, 예상치 못한 오류가 발생했다. 검색을 해보니, express에서 비동기 오류를 지원하지 않아서 발생하는 오류였고 express-async-errors 라이브러리를 사용해서 해결했다.
$ npm i express-async-errors
서버가 실행되는 app.js 상단에서 라이브러리를 불러왔다.
app.js
require('express-async-errors');
이제는 비동기 에러 처리가 잘 된다!