본문 바로가기

[연습 프로젝트] 문답 게시판 _ pagination & local storage 코드 분석

[연습 프로젝트] 문답 게시판 _ pagination & local storage 코드 분석

처음에는 뭐가 뭔지 이해가 하나도 안 갔던 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 는 같은 주소값 참조로 연동된 상태.

 

 

더미 데이터를 쌓기 위한 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
⬆︎