
다음 시간에 카테고리별 도서 조회할 때 0,1,2 값이 아닌 category 테이블에 name (동화,소설,사회)처럼 나오도록 구현하기 위해서 books 테이블을 수정할 필요가 있습니다.
현재 books / category 테이블 모습입니다.


이 books 테이블의 category_id 컬럼을
category 테이블의 참조하여 사용할 수 있도록 외래키 설정을 해주어야합니다
ALTER TABLE books ADD INDEX category_id_idx (`category_id` ASC);
ALTER TABLE books ADD CONSTRAINT category_id
FOREIGN KEY (category_id) REFERENCES category (id);
우선 ADD INDEX 명령어를 사용하여 category_id 컬럼에 대한 색인을 생성함으로써 검색 성능을 최적화했습니다.
그리고 나서 category 테이블의 id를 참조하는 books 테이블의 category_id를 외래키(FK)로 설정했습니다.
위에 수정된 테이블을 활용해서 개별 도서 조회에 join을 활용하여 category_name도 추가해보겠습니다.
export const getBookById = (req, res) => {
const { id } = req.params;
const sql = `SELECT * FROM books LEFT JOIN category ON books.category_id = category.category_id WHERE books.book_id = ?`;
conn.query(sql, [id], (err, results) => {
if (err) {
console.error("도서 개별 조회 요청 DB 에러:", err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json(err);
}
if (results[0]) {
return res.status(StatusCodes.OK).json(results[0]);
} else {
return res.status(StatusCodes.NOT_FOUND).json({
message: "존재하지 않는 도서입니다.",
});
}
});
};
left Join을 통해서 books 테이블 뒤에 바로 category 테이블의 category 테이블의 name 컬럼이 pub_date 바로 뒤에 배치됩니다.


날짜 데이터를 다룰 때는 현재 시간을 기준으로 특정 기간을 더하거나 빼는 함수를 자주 사용합니다.
시간 더하기
DATE_ADD(기준 날짜, INTERVAL)
시간 빼기
DATE_SUB(기준 날짜, INTERVAL)
시간 범위를 설정해서 SELECT
SSELECT * FROM books WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW();
사용자의 요청(Query String)에 따라 SQL 문을 유연하게 조립하는 동적 쿼리 방식으로 구현하였습니다.
export const getAllbooks = (req, res) => {
let { category_id, news } = req.query;
let sql = "SELECT * FROM books";
let values = [];
if (category_id && news) {
sql +=
" WHERE category_id = ? AND pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()";
values = [category_id];
} else if (category_id) {
sql += " WHERE category_id = ?";
values = [category_id];
} else if (news) {
sql +=
" WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()";
}
conn.query(sql, (err, results) => {
if (err) {
console.error("도서 전체 조회 요청 DB 에러:", err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json(err);
}
return res.status(StatusCodes.OK).json(results);
});
};
쿼리문을 수정할 수 있도록 let sql로 선언하고, ?에 매핑될 값을 담을 values 배열을 준비합니다.
category_id와 news가 모두 있을 때: 두 조건을 AND로 연결합니다.
각 조건에 맞는 WHERE 절을 추가합니다.
conn.query 호출 시 values 배열을 인자로 전달하여 SQL Injection을 방지하고 정확한 데이터를 매핑합니다.



데이터베이스에 저장된 수만, 수백만 개의 데이터를 한 번에 클라이언트로 보내면 서버도 힘들고 브라우저도 느려집니다. 그래서 데이터를 일정한 크기(페이지)로 나누어 필요한 부분만 가져오는 것을 페이징이라고 합니다.
LIMIT: 한 페이지에 몇 개의 데이터를 보여줄 것인가? (페이지 당 데이터 수)
OFFSET: 어디서부터(몇 번째 데이터부터) 시작할 것인가? (건너뛸 데이터 수)
예시 (한 페이지에 4개씩)
1페이지
SELECT * FROM books LIMIT 4 OFFSET 0;(1~4번 데이터)
2페이지
SELECT * FROM books LIMIT 4 OFFSET 4;(5~8번 데이터)
3페이지
SELECT * FROM books LIMIT 4 OFFSET 8;(9~11번 데이터)
기존의 카테고리 필터링, 신간 조회 로직에 LIMIT과 OFFSET을 활용한 페이징 기능을 통합하였습니다.
xport const getAllbooks = (req, res) => {
let { category_id, news, limit, currentPage } = req.query;
let parsedLimit = parseInt(limit);
let offset = (parseInt(currentPage) - 1) * parsedLimit;
let sql = "SELECT * FROM books";
let values = [parseInt(limit), offset];
if (category_id && news) {
sql +=
" WHERE category_id = ? AND pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()";
values = [category_id, news];
} else if (category_id) {
sql += " WHERE category_id = ?";
values = [category_id];
} else if (news) {
sql +=
" WHERE pub_date BETWEEN DATE_SUB(NOW(), INTERVAL 1 MONTH) AND NOW()";
}
sql += " LIMIT ? OFFSET ?";
values.push(parsedLimit, offset);
conn.query(sql, values, (err, results) => {
if (err) {
console.error("도서 전체 조회 요청 DB 에러:", err);
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json(err);
}
return res.status(StatusCodes.OK).json(results);
});
};
currentPage와 limit을 활용해 DB에서 '앞에서 몇 개를 건너뛰고 조회를 시작할지' 결정하는 offset 값을 구합니다.LIMIT과 OFFSET은 반드시 쿼리의 가장 마지막에 위치해야 합니다.SELECT 문을 먼저 선언합니다.WHERE) 조건이 있을 경우 이를 먼저 sql 문자열에 덧붙입니다.LIMIT ? OFFSET ?를 추가하여 쿼리를 완성합니다.conn.query에 전달하는 values 배열은 SQL 문 내의 ? 순서와 정확히 일치해야 합니다.category_id 등을 먼저 배열에 담고, 페이징에 필요한 limit과 offset 값을 마지막에 push하여 쿼리가 안전하게 실행되도록 구현했습니다.
