프로그래머스 데브코스, 국비지원교육, 코딩부트캠프
TIL이 늦어진 이유... 주말 + 어제까지 코드를 전체적으로 수정하느라... 계속 다짐만 하던 문제의 비동기!!! 모든 코드를 드디어 비동기 처리로 바꿨다. 사실 코드 수정 자체는 막 그렇게 오래 걸리지는 않았는데 수정 중간중간 발생하는 에러라든가 비동기 공부를 하느라 그게 더 오래 걸렸다.
사유 : 콜백지옥
좋아요를 구현하는데 말만 들었던 그 콜백 지옥을 경험하고 말았다...
const addLike = (req, res) => {
const { id } = req.params;
const { user_id } = req.body;
const sqlInsert = `insert into likes (user_id, liked_book_id) values (?, ?)`;
const values = [user_id, id];
conn.query(checkExist, values, (err, result) => {
if (err) {
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '서버 에러',
});
}
if (result[0].user_exists === 0) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 유저입니다.',
});
}
if (result[0].book_exists === 0) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 도서입니다.',
});
}
conn.query(sqlSelect, values, (err, result) => {
if (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '서버 에러',
});
}
if (result.length > 0) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '이미 좋아요한 책입니다.',
});
}
conn.query(sqlInsert, values, (err, result) => {
if (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '서버 에러',
});
}
return res.status(StatusCodes.OK).json({
message: '좋아요 성공',
});
});
};
위 코드를 보면 계속해서 중첩해서 호출을 하고 있다. 보기에 정말 아주 불편한 코드다. 이렇게 들여쓰기가 반복되는 코드를 일명 콜백지옥(callback hell)
, 멸망의 피라미드(pyramid of doom)
라고 하는데 이를 해결하기 위해서는 async/await 구조를 활용하게 된다.
벨로그... 왜 접어 보기가 안되는 거냐고!!!! 코드가 깁니다... 스압주의
mariadb.js
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.HOST,
user: process.env.USER,
database: process.env.DATABASE,
password: process.env.PASSWORD,
});
const getConnection = async () => {
try {
const connection = await pool.getConnection(async (conn) => conn);
return connection;
} catch (err) {
return err;
}
};
module.exports = {
getConnection,
};
데이터베이스도 connection pool을 이용하게 수정하였다.
UserController
const getUserByEmail = async (connection, email) => {
const selectEmail = 'select * from users where email = ?';
const [rows] = await connection.query(selectEmail, email);
return rows.length > 0 ? rows[0] : null;
};
const hashPassword = (password, salt) => {
try {
return crypto.pbkdf2Sync(password, salt, 10000, 10, 'sha512').toString('base64');
} catch (err) {
console.log(err);
throw new Error('비밀번호 암호화 중 문제가 발생하였습니다.');
}
};
const signup = async (req, res) => {
const connection = await conn.getConnection();
const { email, name, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashedPwd = hashPassword(password, salt);
const sqlInsert = 'insert into users (email, name, password, salt) values (?, ?, ?, ?)';
const values = [email, name, hashedPwd, salt];
try {
const existedEmail = await getUserByEmail(connection, email);
if (existedEmail) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '이미 존재하는 이메일입니다.',
});
}
const [rows] = await connection.query(sqlInsert, values);
if (rows.affectedRows > 0) {
res.status(StatusCodes.CREATED).json({
message: '회원가입 성공',
});
} else {
res.status(StatusCodes.BAD_REQUEST).json({
message: '회원가입 실패',
});
}
} catch (err) {
console.log(err);
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '회원가입 중 문제가 발생하였습니다.',
});
} finally {
connection.release();
}
};
const signin = async (req, res) => {
const { email, password } = req.body;
const connection = await conn.getConnection();
try {
const existedEmail = await getUserByEmail(connection, email);
if (!existedEmail) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: '해당하는 이메일이 존재하지 않습니다.',
});
}
const hashedPwd = hashPassword(password, existedEmail.salt);
if (hashedPwd === existedEmail.password) {
const token = jwt.sign(
{
email: existedEmail.email,
},
process.env.TOKEN_PRIVATE_KEY,
{
expiresIn: '1h',
issuer: process.env.TOKEN_ISSUER,
}
);
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'none',
});
return res.status(StatusCodes.OK).json({
message: '로그인 성공',
token: token,
});
} else {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: '비밀번호가 일치하지 않습니다.',
});
}
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '로그인 중 문제가 발생하였습니다.',
});
} finally {
connection.release();
}
};
const pwdResetRequest = async (req, res) => {
const { email } = req.body;
const connection = await conn.getConnection();
try {
const existedEmail = await getUserByEmail(connection, email);
if (!existedEmail) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: '해당하는 이메일이 존재하지 않습니다.',
});
} else {
return res.status(StatusCodes.OK).json({
message: '이메일 발송 성공',
email: email,
});
}
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '이메일 발송 중 문제가 발생하였습니다.',
});
} finally {
connection.release();
}
};
const pwdReset = async (req, res) => {
const connection = await conn.getConnection();
const { email, password } = req.body;
const salt = crypto.randomBytes(10).toString('base64');
const hashedPwd = hashPassword(password, salt);
const sqlUpdate = 'update users set password = ?, salt = ? where email = ?';
const values = [hashedPwd, salt, email];
try {
const existedEmail = await getUserByEmail(connection, email);
if (!existedEmail) {
return res.status(StatusCodes.UNAUTHORIZED).json({
message: '해당하는 이메일이 존재하지 않습니다.',
});
}
const hashedNewPassword = hashPassword(password, existedEmail.salt);
if (hashedNewPassword === existedEmail.password) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '새 비밀번호는 기존 비밀번호와 달라야 합니다.',
});
}
const [rows] = await connection.query(sqlUpdate, values);
if (rows.affectedRows > 0) {
res.status(StatusCodes.OK).json({
message: '비밀번호 초기화 성공',
});
} else {
res.status(StatusCodes.BAD_REQUEST).json({
message: '비밀번호 초기화 실패',
});
}
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '비밀번호 초기화 중 문제가 발생하였습니다.',
});
} finally {
connection.release();
}
};
기존에 콜백마다 정의했던 서버 에러 처리도 try/catch 구문으로 좀 더 보기 좋게? 정리해보았다.
BookController
const allBooks = async (req, res) => {
const connection = await conn.getConnection();
const { category_id, news, limit, current_page } = req.query;
// limit : 페이지 당 도서 수
// currentPage : 현재 페이지
// offset : 페이지 당 도서 수 * (현재 페이지 - 1)
const parsedLimit = parseInt(limit);
const parsedCurrentPage = parseInt(current_page);
const offset = parsedLimit * (parsedCurrentPage - 1);
const values = [];
let sql = 'select *, (select count(*) from likes where books.id=liked_book_id) as likes from books';
if (category_id && news) {
sql +=
' left join category on books.category_id = category.category_id where books.category_id = ? and pub_date between date_sub(now(), interval 1 month) and now()';
values.push(category_id);
} else if (category_id) {
sql += ' left join category on books.category_id = category.category_id where books.category_id = ?';
values.push(category_id);
} else if (news) {
sql += ' where pub_date between date_sub(now(), interval 1 month) and now()';
}
sql += ' limit ?, ?';
values.push(offset, parsedLimit);
try {
const [rows] = await connection.query(sql, values);
if (rows.length === 0) {
const message = category_id ? '해당하는 도서가 없습니다.' : '도서가 없습니다.';
return res.status(StatusCodes.NOT_FOUND).json({
message: message,
});
}
return res.status(StatusCodes.OK).json(rows);
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '도서 조회 중 에러가 발생하였습니다.',
});
} finally {
connection.release();
}
};
const bookDetail = async (req, res) => {
const connection = await conn.getConnection();
const { user_id } = req.body;
const book_id = req.params.id;
const sql = `select *, (select count(*) from likes where books.id=liked_book_id) as likes,
(select exists(select * from likes where liked_book_id=? and user_id=?)) as liked from books
left join category on books.category_id = category.category_id where books.id=?`;
const values = [book_id, user_id, book_id];
try {
const [rows] = await connection.query(sql, values);
if (rows.length === 0) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '해당하는 도서가 없습니다.',
});
}
return res.status(StatusCodes.OK).json(rows[0]);
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '도서 조회 중 에러가 발생하였습니다.',
});
} finally {
connection.release();
}
};
오늘 수정한 부분도 포함되어 있어서 SQL문이 이전과 살짝 다르다. async/await을 이용하니 훨씬 보기 좋고 깔끔해졌다.
CategoryController
const allCategory = async (req, res) => {
const connection = await conn.getConnection();
const sql = 'select * from category';
try {
const [rows] = await connection.query(sql);
if (rows.length === 0) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '카테고리가 없습니다.',
});
}
return res.status(StatusCodes.OK).json(rows);
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: '카테고리 조회 중 에러가 발생하였습니다.',
});
} finally {
connection.release();
}
};
LikeController
const checkExistValues = async (connection, values) => {
const [rows] = await connection.query(checkExist, values);
return {
user_exists: rows[0].user_exists === 1,
book_exists: rows[0].book_exists === 1,
};
};
const addLike = async (req, res) => {
const { id } = req.params;
const { user_id } = req.body;
const sqlInsert = `insert into likes (user_id, liked_book_id) values (?, ?)`;
const values = [user_id, id];
const connection = await conn.getConnection();
try {
const { user_exists, book_exists } = await checkExistValues(connection, values);
if (!user_exists) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 유저입니다.',
});
}
if (!book_exists) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 도서입니다.',
});
}
const [result] = await connection.query(sqlSelect, values);
if (result.length > 0) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '이미 좋아요한 책입니다.',
});
}
const [insertResult] = await connection.query(sqlInsert, values);
if (insertResult.affectedRows > 0) {
return res.status(StatusCodes.OK).json({
message: '좋아요 성공',
});
} else {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '좋아요 실패',
});
}
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '서버 에러',
});
} finally {
connection.release();
}
};
const deleteLike = async (req, res) => {
const { id } = req.params;
const { user_id } = req.body;
const sqlDelete = `delete from likes where user_id = ? and liked_book_id = ?`;
const values = [user_id, id];
const connection = await conn.getConnection();
try {
const { user_exists, book_exists } = await checkExistValues(connection, values);
if (!user_exists) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 유저입니다.',
});
}
if (!book_exists) {
return res.status(StatusCodes.NOT_FOUND).json({
message: '존재하지 않는 도서입니다.',
});
}
const [result] = await connection.query(sqlSelect, values);
if (result.length === 0) {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '좋아요하지 않은 책입니다.',
});
}
const [deleteResult] = await connection.query(sqlDelete, values);
if (deleteResult.affectedRows > 0) {
return res.status(StatusCodes.OK).json({
message: '좋아요 취소 성공',
});
} else {
return res.status(StatusCodes.BAD_REQUEST).json({
message: '좋아요 취소 실패',
});
}
} catch (err) {
console.log(err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: '서버 에러',
});
} finally {
connection.release();
}
};
문제의 LikeController 부분도 async/await으로 수정을 하니 훨씬 깔끔해진 걸 볼 수 있다.
근데 아직 좀 아쉬운 게 먼저 유저/책이 존재하는지 확인하는 부분이 중복되고 있어서 이걸 따로 빼고 싶어서 이런저런 시도를 해봤는데... 딱히 마음에 들게 수정되지 않아서 일단 저대로 뒀다.
그리고 서버 에러 부분도 계속 중복되고 있으니 저것도 handler로 뺄 수 있을까 고민중이다. 근데 내가 원하는 방식으로는 자꾸 안 되고 있어서 조금 더 찾아봐야 할 것 같다.
일단 비동기에 대해서 잘 모르는 상태이기도 해서 코드가 좀 이상할 수도 있다...😂 이 부분은 계속 공부하면서 아마 수정해나가지 않을까 싶다!!! 일단 당장 내일부터 비동기에 대한 수업이라서 그 부분을 들으면서 수정 방향을 정해볼까 한다.