저번 시간에 기본적인 테이블과 페이지네이션을 테스트코드까지 작성하여 개발했었습니다.
TDD의 마지막은 언제나 리팩토링이어야 합니다!
오늘은 그동안 미뤄왔던 리팩토링을 진행하겠습니다.
우선 APP의 기능을 3가지로 나눈다면
1. 테이블
2. 페이지네이션
3. 게시글 로드
app.js에서 테이블과 페이지네이션의 상세 구현까지는 필요가 없으므로 이 두가지를 컴포넌트로 분리하겠습니다.
간단하게 잘라내기 + 붙여넣기로 pagination.js와 post-table.js를 만들어줍니다.
그러면서 필요한 부분에 함수 분리도 해줍니다.
사실, 이전까지 몰랐던 클래스 관련 ES2022의 새로운 기능들에 대해서도 알게 되었습니다!
1. 이제 js에서 클래스 필드 초기화 가능
2. 접근제어자 추가
관련 상세한 내용은 아래 게시글로 확인해보세요!
https://gdsc-university-of-seoul.github.io/es-2022/
당신이 JS 개발자라면 반드시 알아야 할 ES2022 신규 기능 7가지 | GDSC UOS
시작 안녕하세요, GDSC UOS FE팀에서 코어 멤버로 활동하고 있는 이명재입니다. 지난 6월 22일, ECMAScript 2022가 정식 스펙으로 채택되었습니다. FE 개발자로서 떼려야 뗄 수 없는 Javascript의 새 소식이
gdsc-university-of-seoul.github.io
pagination.js
class Pagination {
#paginationInfo;
#parentElement;
#onChangePage;
constructor() {
this.#paginationInfo = {
page: 1,
};
this.#parentElement = null;
this.#onChangePage = () => {};
}
render(parentElement, onChangePage) {
this.#parentElement = parentElement;
this.#onChangePage = onChangePage;
this.#createPagination();
}
#createPagination() {
const paginationContainer = document.createElement("div");
paginationContainer.id = "pagination-container";
const goFirstPageButton = this.#createButton(
"go-first-page-button",
"|<",
() => this.changePage(1)
);
const goPrevPageButton = this.#createButton(
"go-prev-page-button",
"<",
() => this.#onChangePage(this.#paginationInfo.page - 1)
);
const pageNumbersWrapper = document.createElement("div");
pageNumbersWrapper.id = "page-numbers-wrapper";
const goNextPageButton = this.#createButton(
"go-next-page-button",
">",
() => this.#onChangePage(this.#paginationInfo.page + 1)
);
const goLastPageButton = this.#createButton(
"go-last-page-button",
">|",
() => this.#onChangePage(this.#paginationInfo.totalPages)
);
paginationContainer.appendChild(goFirstPageButton);
paginationContainer.appendChild(goPrevPageButton);
paginationContainer.appendChild(pageNumbersWrapper);
paginationContainer.appendChild(goNextPageButton);
paginationContainer.appendChild(goLastPageButton);
this.#parentElement.appendChild(paginationContainer);
}
#createButton(id, text, onClickHandler) {
const button = document.createElement("button");
button.id = id;
button.innerHTML = text;
button.addEventListener("click", onClickHandler);
return button;
}
#renderPagination() {
if (!document.getElementById("pagination-container")) {
this.createPagination();
}
const nextPageButton = document.getElementById("go-next-page-button");
const lastPageButton = document.getElementById("go-last-page-button");
const prevPageButton = document.getElementById("go-prev-page-button");
const firstPageButton = document.getElementById("go-first-page-button");
const { page, totalPages } = this.#paginationInfo;
const pageNumbersWrapper = document.getElementById("page-numbers-wrapper");
pageNumbersWrapper.innerHTML = "";
let startPage = Math.max(page - 3, 1);
let endPage = Math.min(startPage + 4, totalPages);
if (endPage === totalPages) {
startPage = Math.max(totalPages - 4, 1);
}
for (let currentPage = startPage; currentPage <= endPage; currentPage++) {
const pageButton = this.#createButton(
`page-button-${currentPage}`,
currentPage,
() => this.#onChangePage(currentPage)
);
pageButton.classList.toggle("current-page", currentPage === page);
pageNumbersWrapper.appendChild(pageButton);
}
firstPageButton.disabled = prevPageButton.disabled = page === 1;
lastPageButton.disabled = nextPageButton.disabled = page === totalPages;
}
setPaginationInfo(paginationInfo) {
this.#paginationInfo = paginationInfo;
this.#renderPagination();
}
}
export default Pagination;
post-table.js
class PostTable {
#posts = [];
#tableElement = null;
#parentElement = null;
constructor() {
this.#tableElement = null;
this.#posts = [];
}
render(parentElement) {
this.#parentElement = parentElement;
this.#createTable();
this.#renderRows();
}
#createTable() {
this.#tableElement = document.createElement("table");
const tableHead = document.createElement("thead");
const tableRow = document.createElement("tr"); // 테이블 헤더를 위한 행을 추가합니다.
// id 칼럼
const idCell = document.createElement("td");
idCell.innerText = "id";
tableRow.appendChild(idCell);
// title 칼럼
const titleCell = document.createElement("td");
titleCell.innerText = "title";
tableRow.appendChild(titleCell);
// liked 칼럼
const likedCell = document.createElement("td");
likedCell.innerText = "liked";
tableRow.appendChild(likedCell);
// 헤더 행을 thead 요소에 추가
tableHead.appendChild(tableRow);
// thead를 테이블에 추가
this.#tableElement.appendChild(tableHead);
const tableBody = document.createElement("tbody");
tableBody.id = "tbody";
this.#tableElement.appendChild(tableBody);
this.#parentElement.appendChild(this.#tableElement);
}
#renderRows() {
const tableBody = document.getElementById("tbody");
tableBody.innerHTML = "";
if (this.#posts) {
for (const post of this.#posts) {
const rowElement = document.createElement("tr");
const idElement = document.createElement("td");
idElement.innerHTML = post.id;
const titleElement = document.createElement("td");
titleElement.innerHTML = post.title;
const likedElement = document.createElement("td");
likedElement.innerHTML = post.liked;
rowElement.appendChild(idElement);
rowElement.appendChild(titleElement);
rowElement.appendChild(likedElement);
tableBody.appendChild(rowElement);
}
}
}
setPosts(newPosts) {
this.#posts = newPosts;
this.#renderRows();
}
}
export default PostTable;
이제 app.js에 중복되는 메서드를 모두 삭제하고 마찬가지로 ES2022 내용을 적용합니다.
import "./styles/main.css";
import PostTable from "./components/post-table";
import Pagination from "./components/pagination";
class App {
#API_URL = "http://127.0.0.1:3000/posts";
constructor() {
this.page = 1;
this.limit = 10;
this.init();
}
init() {
this.createMainElement();
this.initializeComponents();
this.renderComponents();
this.loadPosts(this.page, this.limit);
}
createMainElement() {
this.mainElement = document.createElement("main");
document.body.appendChild(this.mainElement);
}
initializeComponents() {
this.postTable = new PostTable();
this.pagination = new Pagination();
}
renderComponents() {
this.postTable.render(this.mainElement);
this.pagination.render(this.mainElement, this.changePage.bind(this));
}
async changePage(targetPage) {
this.page = targetPage;
await this.loadPosts(this.page);
}
async loadPosts(page) {
try {
await this.fetchPosts(page);
this.updateUI();
} catch (error) {
console.error("Error loading posts:", error);
}
}
async fetchPosts(page) {
const response = await fetch(
`${this.#API_URL}?page=${page}&limit=${this.limit}`
);
if (!response.ok) {
throw new Error(`Failed to fetch posts: ${response.statusText}`);
}
this.postsData = await response.json();
}
updateUI() {
if (this.postsData.currentPage === this.page) {
this.postTable.setPosts(this.postsData.posts);
this.pagination.setPaginationInfo({
page: this.postsData.currentPage,
totalPages: this.postsData.totalPages,
});
}
}
}
export default App;
이전과 훨씬 깔끔해졌습니다!
하지만 코드가 깔끔해졌다고, 이게 기능까지 그대로 유지되었을지 우리가 확신할 수 있을까요?
네!
바로 우리는 테스트를 꼼꼼히 작성했기 때문입니다.
리팩토링을 거친후에도, 기능이 정상적으로 작동함을 확인할 수 있습니다!
git을 통해 확인해보니 180라인이 71라인으로 줄었음을 확인할 수 있습니다.
약 60.56% 줄었네요! :)
'Study > Frontend' 카테고리의 다른 글
HTTP POST 요청에서 Body에 Null을 보내면 안되는 거에요? (0) | 2025.05.15 |
---|---|
완벽한 페이지네이션 바닥부터 개발하기 EP4 : 클로저를 활용한 캐싱 기능 추가 (0) | 2024.06.27 |
완벽한 페이지네이션 바닥부터 개발하기 EP2 : 테스트 통과시키기 (1) | 2024.06.19 |
완벽한 페이지네이션 바닥부터 개발하기 EP1 : 개발환경 구축하기 (1) | 2024.06.19 |
2024년에는 라이브러리를 위해 어떤 모듈 번들러를 선택해야 할까? (0) | 2024.04.12 |