처음에는 뭐가 뭔지 이해가 하나도 안 갔던 pagination과 local storage를 통한 게시판 기능 구현.
하루에 한 번씩 들여다 보니 어느 순간 이해가 되어 정리된 것들을 블로그에 옮겨본다.
pagenation과 local storage 원리를 코드와 함께 하나하나 뜯어서 정리해보겠다. (우걱우걱)
초기 세팅 (변수 data)
// console.log(agoraStatesDiscussions);
let data; // 앞으로 사용할 data 변수 선언
const dataFromLocalStorage = localStorage.getItem("agoraStatesDiscussions"); // 아고라더미데이터 가져오기
if(dataFromLocalStorage) { // 만약 localStorageData가 있으면 (= submit을 한 번 한 이후)
data = JSON.parse(dataFromLocalStorage) // 로컬 스토리지에서 가져온 데이터로 할당
} else { // localStorageData가 없으면 //( = 최초 렌더링일 경우)
data = agoraStatesDiscussions.slice();
} // data는 더미 데이터를 복사한 그대로. [{글1}. {글2}] -> 객체는 data, agora~ 연동됨.
- 우선 앞으로 사용할 data 변수를 선언해준다.
- 그 뒤, dataFromLocalStorage 라는 변수를 선언하여 "agora~Discussions"라는 키 값에 해당되는 로컬 데이터를 불러온다.
- (즉, 초기 단계에서는 저장되어 있는 로컬 데이터는 없는 상태, 로컬 데이터가 저장 되기도 전에 불러오는 코드를 만듬)
- if 문을 통해 dataFromLocalStorage 값이 있다면 -> data 값에는 로컬 데이터를 불러온다.
- dataFromLocalStorage 값이 없다면(즉, 최초 렌더링일 경우) -> data 값에는 더미 데이터를 그대로 복사한다.
- 더미 데이터는 [{게시글1}, {게시글2}...] 의 형식이므로 얕은 복사가 진행된다.
즉, data 와 agoraStatesDiscussions 의 2-depth 는 같은 주소값 참조로 연동된 상태.
- 더미 데이터는 [{게시글1}, {게시글2}...] 의 형식이므로 얕은 복사가 진행된다.
더미 데이터를 쌓기 위한 DOM을 세팅하는 작업 (게시판 속 글 1개의 형식)
// convertToDiscussion은 아고라 스테이츠 데이터를 DOM으로 바꿔주는 함수.
const convertToDiscussion = (obj) => {
const li = document.createElement("li"); // li 요소 생성 (게시글 하나의 박스)
li.className = "discussion__container"; // 클래스 이름 지정
const avatarWrapper = document.createElement("div");
avatarWrapper.className = "discussion__avatar--wrapper"; // avatar--wrapper 클래스 이름 지정
const discussionContent = document.createElement("div");
discussionContent.className = "discussion__content"; // discussion__content 클래스 이름 지정
const discussionAnswered = document.createElement("div");
discussionAnswered.className = "discussion__answered"; // discussion__answered 클래스 이름 지정
// 객체 하나에 담긴 정보를 DOM에 적절히 넣어준다.
// 프로필 사진(이미지)
const avatarImg = document.createElement("img");
avatarImg.className = "discussion__avatar--image";
avatarImg.src = obj.avatarUrl;
avatarImg.alt = `avatar of ${obj.author}`;
avatarWrapper.append(avatarImg);
// 내용
const titleBox = document.createElement("h2"); // 제목 가져옴 (링크))
const titleLink = document.createElement("a");
titleLink.href = obj.url;
titleLink.textContent = obj.title;
titleBox.className = "discussion__title";
titleBox.append(titleLink);
const information = document.createElement("div"); // 올린 글 정보
information.className = "discussion__information";
information.textContent = `${obj.author} / ${new Date(obj.createdAt).toLocaleString().slice(0, -3)}`
discussionContent.append(titleBox, information);
const checkBoxLabel = document.createElement("label"); // 체크박스
checkBoxLabel.className = "container";
const inputBox = document.createElement("input");
inputBox.type = "checkbox";
inputBox.checked = obj.answer ? "checked" : ""; // 답변 유무에 따른 체크박스 설정
const checkMarkBox = document.createElement("div");
checkMarkBox.className = "checkmark";
checkBoxLabel.append(inputBox, checkMarkBox)
discussionAnswered.append(checkBoxLabel);
li.append(avatarWrapper, discussionContent, discussionAnswered);
return li; // 게시물 하나를 리턴하는 함수
};
li 안에는 게시글 1개에 속하는 요소들이 들어간다. (아바타 사진, 게시글 제목, 작성아이디, 올린 날짜 등)
이것을 반복문을 돌려서 최종적으로 ul 박스에 넣어줄 것이다.
렌더링하는 함수도 만들자.
// data 배열의 모든 데이터를 화면에 렌더링하는 함수이다. - element에는 ul(게시판 바깥박스)이 들어갈 예정
const render = (element, from, to) => {
if (!from && !to) { // from과 to 값이 0 or undefined면 => 분량이 한페이지밖에 없으면
from = 0;
to = data.length - 1; //? to는 데이터 마지막 인덱스값??? i < to인데?
}
while (element.firstChild) { // 페이지가 넘어가면, 일단 ul 안의 내용 다 지운다.
element.removeChild(element.firstChild);
}
for (let i = from; i < to; i += 1) { // 데이터 from째부터 to-1 인덱스까지 다시 ul에 넣는다.
element.append(convertToDiscussion(data[i]));
}
return
};
// render(ul, 0, 5) or render(ul, 5, 10) ... render(ul, 20, 25) 이런 형식이 될 예정
// 즉 페이지 당 5개의 게시글을 보여줄 계획
render 함수는 내가 현재 보고 있는 페이지에 속하는 데이터들만 화면에 구현해준다.
만약 내가 구현한 게시판이 10페이지가 넘어가고 한 페이지당 데이터를 5개씩 보여준다면, 해당 페이지에 속하는 5개의 데이터만 렌더링한다는 뜻이다.
if는 이해가 안 감. to는 데이터 마지막 인덱스값??? i < to인데?
처음 접속할 때 보여지는 게시글들 세팅 (가장 최근에 쓰여진 5개의 글)
// 페이지네이션을 위한 변수
let limit = 5, // 한 페이지 당 5개 게시물만 보이도록
page = 1; // page는 하나의 페이지 (1쪽!)
// ul 요소에 agoraStatesDiscussions 배열의 모든 데이터를 화면에 렌더링한다.
const ul = document.querySelector("ul.discussions__container");
render(ul, 0, limit);
// 처음 렌더링할 때 디폴트 값은 최신 Index 0~4의 게시글 5개를 보여주는 것. (아래와 같다.)
// const render = (ul, 0, 5) => {
// while (ul.firstChild) {
// ul.removeChild(ul.firstChild);
// }
// for (let i = 0; i < 5; i += 1) {
// ul.append(convertToDiscussion(data[i]));
// }
// return
// };
게시글을 5개씩 자르기 위한 함수
const getPageStartEnd = (limit, page) => { // 예) limit = 5, page = 1 이라고 하자
const len = data.length - 1; // 길이는 데이터의 마지막 인덱스 값 (데이터가 10개 있다고 치면 len = 9)
let pageStart = Number(page - 1) * Number(limit); // pageStart = Number(1-1) * Number(5) = 5
let pageEnd = Number(pageStart) + Number(limit); // pageEnd = Number(5) + Number(5) = 10
if (page <= 0) { // 만약 page가 음수라면
pageStart = 0; // pageStart는 0이다. (디폴트값)
}
if (pageEnd >= len) { // pageEnd가 data index 값보다 더 크면
pageEnd = len; // pageEnd는 data index 마지막 값이 된다.
}
return { pageStart, pageEnd }; // 이 값을 리턴!
};
게시글을 1쪽 : 0~5 (0<=게시글 index<5) , 2쪽 : 5~10, 3쪽 : 10~15... 로 자르기 위한 작업이다.
- 게시글의 마지막 index는 당연히 data.length-1 이다.
- 페이지 값 (예: 3)에 따라 pageStart (예:10) 값이 결정되고 이어서 pageEnd(예:15) 값이 결정된다.
예외처리
- page 값이 0 또는 음수라면 -> pageStart 값은 디폴트값 0 이다. (0이나 음수일 경우가 있을까?)
- pageEnd 값이 data 자료의 수보다 더 많다면 pageEnd는 data.length-1 값이 된다.
- 즉, pageEnd는 20인데 data가 17개밖에(마지막 index 값이 16) 없다면 마지막 index 값으로 할당
페이지 앞 뒤로 넘기는 버튼 구현
// page 넘기는 버튼
const buttons = document.querySelector(".buttons");
buttons.children[0].addEventListener("click", () => { // <- 버튼을 누르면
if (page > 1) { // 페이지가 1보다 클 때 (2,3,...) 예) 만약 3이었다면
page = page - 1; // 기존 페이지에 -1 해라. 예) 2가 됨.
} // page 값이 바뀌었다!
// **** 그렇기 때문에 쪽수에 맞게 데이터 index(5개) 추출
const { pageStart, pageEnd } = getPageStartEnd(limit, page);
render(ul, pageStart, pageEnd); // 예) (ul, 5, 10)이 됨.
});
buttons.children[1].addEventListener("click", () => { // -> 버튼을 누르면 만약 현재 page가 5 라면
if (limit * page < data.length - 1) { //
page = page + 1; // page는 6이 됨.
}
const { pageStart, pageEnd } = getPageStartEnd(limit, page); // pageStart 값은 6*5 = 30.. pageEnd 값은 35가 됨.
render(ul, pageStart, pageEnd); // 6쪽에 해당되는 데이터들을 렌더링해준다.
});
로컬 데이터 삭제하는 버튼 구현
const localStorageBtn = document.querySelector(".localStorageBtn");
localStorageBtn.addEventListener("click", () => { // 로컬데이터 삭제 버튼을 누르면
localStorage.removeItem("agoraStatesDiscussions"); // 로컬에 저장된 key인 agoraStatesDiscussions의 로컬데이터를 삭제함.
data = agoraStatesDiscussions.slice(); // 원상복구 시작 -> 기존 더미데이터를 복사함.
limit = 5;
page = 1;
render(ul, 0, limit);
});
'submit' 버튼을 누르면 -> 게시글이 하나 추가되며 + 로컬 데이터에 쌓이며 + 페이지도 변화가 생긴다.
// submit 버튼을 누르면 - 이름, 제목, 질문을 가져온다 -
const submitBtn = document.querySelector("button")
submitBtn.addEventListener('click', submitContentResult)
function submitContentResult(event) {
event.preventDefault() // 'submit'의 디폴트 이벤트 실행시키지 않기 위함
let userName = document.querySelector('#name').value
let userTitle = document.querySelector('#title').value
let userStory = document.querySelector('#story').value
// 객체를 생성해 폼에서 입력받은 값을 넣어 준다.
let newOne = {
id: Math.random(),
createdAt: new Date().toISOString(),
title: userTitle ,
url: "https://github.com/codestates-seb/agora-states-fe/discussions",
author: userName,
bodyHTML: userStory,
answer: null,
avatarUrl: "https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkDKJe%2Fbtr2ZCJESvs%2FCmmMIpR2tKLJLoqbHuVNn0%2Fimg.png"
}
// 데이터에 새 정보(객체)를 앞에! 넣어준다.
data.unshift(newOne);
// 드디어 나왔다. "agoraStatesDiscussions"라는 키 값으로
// 해당 value(data)를 로컬스토리지에 '문자 형태로' 저장
localStorage.setItem("agoraStatesDiscussions", JSON.stringify(data));
// 다시 0~5 Index 게시물 렌더링해줘야 함.
//(0~4 index 값 게시글이 뜨기 때문에 unshift 해야 최신 게시물이 뜸!)
render(ul, 0, limit);
const form = document.querySelector("form")
form.reset();
}
728x90
'📌 TOY-PROJECT > 2303 문답게시판' 카테고리의 다른 글
[연습 프로젝트] 문답 게시판_서버 개발 (0) | 2023.04.09 |
---|---|
[연습 프로젝트] 문답 게시판_클라이언트 : 페이지네이션 등 (0) | 2023.03.16 |
[연습 프로젝트] 컴포넌트(component) 만들기 (0) | 2023.03.12 |
[연습 프로젝트] 나만의 아고라 스테이츠 만들기 (0) | 2023.03.12 |