웹 풀사이클 데브코스 TIL 9주차 DAY 1

갱갱·2024년 1월 9일
0

데브코스 TIL

목록 보기
20/24
post-thumbnail

프로그래머스 데브코스, 국비지원교육, 코딩부트캠프

🚩 프로그래머스 데브코스 웹 풀사이클 과정 9주차 DAY 1


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 구조를 활용하게 된다.

💧 대대적인 코드 수정


벨로그... 왜 접어 보기가 안되는 거냐고!!!! 코드가 깁니다... 스압주의

  1. 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을 이용하게 수정하였다.

  1. 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 구문으로 좀 더 보기 좋게? 정리해보았다.

  1. 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을 이용하니 훨씬 보기 좋고 깔끔해졌다.

  1. 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();
    }
};
  1. 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로 뺄 수 있을까 고민중이다. 근데 내가 원하는 방식으로는 자꾸 안 되고 있어서 조금 더 찾아봐야 할 것 같다.
일단 비동기에 대해서 잘 모르는 상태이기도 해서 코드가 좀 이상할 수도 있다...😂 이 부분은 계속 공부하면서 아마 수정해나가지 않을까 싶다!!! 일단 당장 내일부터 비동기에 대한 수업이라서 그 부분을 들으면서 수정 방향을 정해볼까 한다.

profile
괜찮은 개발자가 되어 보자

0개의 댓글