으쌰으쌰

바닐라JS 로 만드는 북마크 웹앱 📓 (2) 게시물 편

HYEMBURGER 2023. 2. 4. 19:40

 

 

 

2편 !

 

 

바닐라 JS로 만드는 북마크 웹앱 📓 2편

~ 게시물 편 ~

 

게시물이라고 하기에는 애매하지만 설명의 편의를 위해 게시물 편이라고 이름 지었다.

정확히는 인상 깊은 구절, 발췌 내용 등을 기록한다는 느낌이 강하다는 걸 먼저 밝힌다.

 

참 고민도 많고 시간도 많이 걸린 게시물 편을 기록한다. 🤸🏻

구현한 기능은 게시물 저장 / 확인 / 수정 / 삭제로 총 네 가지 기능이다.

 

 

 


 

 

천 리 길도 한 걸음부터, (0) 계획 편 (1) 회원가입/로그인 편 보러 가기 👏

📌 계획편

📌 회원가입/로그인 편

 

 

 


 

 

✔︎ 게시물 저장

0. 로그인을 성공하면 게시물 작성을 위한 form이 보인다.

1-1. 발췌내용을 저장하는 textarea.

1-2. 출처명을 적는 (책 제목, 화자 이름 ... 등) input(type=text).

1-3. submit을 위한 button (value = 기록) 으로 이루어져 있음.

1-4. textarea와 input은 required 속성을 부여. 입력 필수. 

2. 작성 후 기록 button을 누르면 function 동작 

3. 필요

  - localStorage에 value 값으로 저장할 빈 array

    👏 게시물이 한 개만이 아니라 여러 개이므로 이터러블인 배열로 설정했다.

  - textarea value

  - input value

  + 작성 date -> new Date() 이용 (format 형식 수정해야 함)

  + 고유 id 값 만들어 부여 -> Date.now() 를 이용.

  - 받아온 정보(값)를 저장할 object.

4. 준비해 둔 object에 값을 모두 저장한 후 value 값으로 들어갈 빈 array에 push.

5. localStorage.setItem("record", JSON.stringify(push 한 array이름));

6. 게시물 확인 function을 실행한다. (추가할 때마다 확인 위함)

 

 

🍔 전역 변수, 이벤트 리스너

// 전역변수
const recordForm = document.querySelector("#recordForm");
const recordArea = document.querySelector("#recordArea");
const recordSource = document.querySelector("#recordSource");
const hiddenId = document.querySelector("#recordId");

let recordArr = [];

// 이벤트리스너
recordForm.addEventListener("submit", saveRecord); //form submit > save function

 

🍔 수정 전 코드

// 수정 전
function saveRecord(e) {
  e.preventDefault();

  const date = new Date();
  const id = Date.now();

  if (recordArea.value !== "") {
    const recordObj = {
      text: recordArea.value,
      source: recordSource.value,
      date: date,
      id: id,
    };

    recordArr.push(recordObj);
    setRecord();

    recordArea.value = "";
    recordSource.value = "";
    seeRecord(recordObj);
  }
}

// 같은 게 두 번 쓰여서 function으로 뺐다.
function setRecord() {
  localStorage.setItem("record", JSON.stringify(recordArr));
}

👏 후에 수정 기능 구현 때 사용되는 setItem ... 은 따로 function으로 만들어 추가했다.

👏 로컬 스토리지에 저장이 완료되면 작성창 모두가 초기화되고, 저장하기 위해 만들었던 object를 게시물 확인 function의 인수로 할당하여 실행시킨다. 

 

❗️다시 코드를 읽다 보니 if문은 필요 없다는 걸 깨달았다. textarea도 input도 모두 required 속성을 갖고 있기 때문에!! js 코드가 한 번 더 확인할 필요는 없었다. 

❓고유 id값으로 사용하기 위해 가져온 Date.now()를 따로 변수를 만들어 할당했는데 굳이 그러지 않아도 되려나? 변수에 할당하지 않고 바로 메서드 상태로 집어넣어도 될 거 같기도 .. date는 포맷한다고 쳐도 id는 특별히 포맷을 하는 것도 아니니까... 

 

🍔 수정 후 코드

// 수정 후 
function saveRecord(e) {
  e.preventDefault();

  const date = new Date();
  const recordObj = {
    text: recordArea.value,
    source: recordSource.value,
    date: `${date.getFullYear()}년${date.getMonth() + 1}월${date.getDate()}일 ${date.getHours()}:${date.getMinutes()}`,
    id: Date.now(),
  };

  recordArr.push(recordObj);
  setRecord();

  recordArea.value = "";
  recordSource.value = "";
  seeRecord(recordObj);
}

👏 위에서 말했던 필요 없는 if문 제거, 날짜 작성 형식 변경, 필요 없다고 느껴진 변수 id 제거를 수정한 코드.

❓date 부분을 더 깔끔하게 만들 수는 없을까? 생각해 보자.

 

 

 


 

 

✔︎ 게시물 확인

0. 웹페이지가 load 되었을 때 로컬스토리지 key="record"의 value가 비어있지 않다면, 확인 function을 실행한다. 

1. 빈 ul list를 만들어둔다. (HTML)

2. 각각의 요소마다 node 뭉치를 만들어 추가한다. 

 

🍔 HTML 

<section>
  <ul id="recordList"></ul>
</section>

빈 ul list를 만들어두고 js를 이용해 추가하기로 했다.

 

 

🍔 record value 출력

// 로컬스토리지가 비어있지 않다면! 바로 출력한다.
const getRecord = JSON.parse(localStorage.getItem("record"));
if (getRecord !== null) {
  recordArr = getRecord;
  recordArr.forEach(seeRecord);
}

👏 먼저 로컬스토리지가 비어있지 않다면 value를 받아온다.

JSON.stringify로 저장했기 때문에 문자열 상태인 value를 JSON.parse를 이용해 객체로 바꾼다.

이 객체를 recordArr (기록을 저장하는 빈배열) 에 할당한다.

recordArr 배열에 forEach 메서드를 사용해 배열 안 요소요소마다 seeRecord 함수를 실행시킨다. 

 

🍔 수정 후 코드

// 확인-보기
function seeRecord(recordValue) {
  const li = document.createElement("li");
  const recordList = document.querySelector("#recordList");
  const newLi = recordList.appendChild(li);
  newLi.id = recordValue.id;

  const p = document.createElement("p");
  const text = newLi.appendChild(p);
  text.innerText = recordValue.text;

  [recordValue.source, recordValue.date].forEach((text) => {
    const span = document.createElement("span");
    const textNode = document.createTextNode(text);
    span.appendChild(textNode);
    newLi.appendChild(span);
  });

  ["mod", "del"].forEach((text) => {
    const button = document.createElement("button");
    const textNode = document.createTextNode(text);
    button.appendChild(textNode);
    newLi.appendChild(button);
    button.classList.add(text);

    // button.addEventListener("click", whatBtn);
    
    button.addEventListener("click", (e) => {
      button.className === "mod" ? modRecord(e) : delRecord(e);
    });
  });
}

💢 노드를 여러 개 추가할 때 ... 이렇게 하면 안 될 거 같은데?

노드를 한 두 개 추가하는 거라면 하나하나 쓰면 되겠지만 ... 내가 원하는 노드를 모두 추가하기에는 선언해야 할 변수의 수도, 코드의 수도 너~무나 장황했다. 정말 장황하다고 느껴질 정도였다. 그리고 웬걸 열심히 어떻게든 적었는데 ... ‼️원하는 대로 동작하지 않는다‼️

❓문제 발생 이유
💭 DOM, Node 가 뭐지? 
DOM, Node에 대해 솔직히 말해 모르는 상태였다. 개념도 모르고 이해도 충분치 않은 상태, 내가 지금 node를 다루고 있다는 사실조차도 인지하지 못할 정도로. 부끄러웠다. 이때 공부한 내용은 추후에 정리해서 포스팅하도록 하자. (꼭!)

👏 해결!

MDN과 모던 자바스크립트 DeepDive의 도움을 받아서 해결할 수 있었다. (언제나 MDN과 저자분들께는 감사한 마음만 가지게 된다...)

1. 일단 너무 장황해진 코드와 변수들을 보면서 결국 겹치는 부분이 있고, 반복문을 사용하면 될 것 같다는 생각까지는 할 수 있었다.

2. 거기까지는 좋았지만 어떻게 써야 할지 잘 모르겠고, 쓴다 해도 좀 더 효율적인 방법으로!! 쓸 수 있는 방법이 궁금했다.

3. MDN, 서적을 독파한 결과 내가 생각했던 반복문을 사용하는 방법을 알아냈다. 

3-2. 이를 활용했고 원하는 대로 출력되는 걸 확인! 

4. 한 가지 알아낸 중요한 사실은 DOM 변경은 높은 비용이 드는 처리이기 때문에 가급적 횟수를 줄이는 편이 성능에 유리하다는 점이다. 

4-2. 그래서 지금 사용한 방법보다 더 효율적인 방법이 있다는 사실! DocumentFragment 노드를 사용하는 방법이다. (이와 관련된 내용도 위에서 말한 것처럼 추후에 포스팅하겠다.) 

4-3. 효율적인 방법이 있다는 걸 알지만, 내가 하고 싶었던 방법(어떻게든 반복문을 사용하자!!)을 좀 더 직접적으로 다음에도 확인할 수 있도록 이렇게 남겨두기로 했다. (버전 업할 때 수정하자!!)

 

💢 addEventListner가 제대로 동작하지 않는 문제

if문 조건을 주고, 만약 button의 className이 mod일 경우 수정 기능 / del일 경우 삭제 기능을 담은 이벤트리스너를 만들었으나 제대로 동작하지 않았다. 

 

👏 해결!

1. 처음에는 whatBtn이라는 함수를 하나 더 만들고 어떤 버튼이든 클릭하면 그 함수를 실행하도록 했다.

이 함수는 동작한 버튼의 className을 받아와서 삼항 연산자를 이용해 기능을 나누는 방법이다.

function whatBtn(e) {
  const btnClass = e.target.className;
  btnClass === "mod" ? modRecord(e) : delRecord(e);
}

 

2. 하지만 eventListner 에는 함수명을 넣어서 사용하는 방법도 있지만, 

함수 자체를 넣는 방법도 존재한다는 걸 깨닫고 코드를 수정했다. 

  button.addEventListener("click", (e) => {
    button.className === "mod" ? modRecord(e) : delRecord(e);
  });

함수를 굳이 만들기에는 짧다고 느껴지고, 한 번만 사용하니까 이 편이 더 깔끔하다고 생각했다! 

 

 

 


 

 

 

✔︎ 게시물 수정

1. 게시물마다 있는 수정버튼을 누르면 수정 기능(1)이 실행.

2. 게시물의 내용, 출처가 기록하는 곳이었던 textarea, input 창에 출력.

2-2. 이때 기록버튼은 사라지고 숨겨져 있던 수정버튼 나타남.

2-3. id값도 함께 받아오는데 숨겨져 있는 span에 기록.

3. 내용을 수정하고 수정 버튼을 누르면 수정 기능(2)이 실행.

4. 로컬스토리지에 접근해 값을 변경.

5. 수정한 게시물을 확인할 수 있도록 index.html 다시 요청. 

 

 

🍔 수정(1)

// 수정(1)
function modRecord(e) {
  // 기록 버튼 사라지고 수정 버튼 보이기 (input type button)
  const id = e.target.parentElement.id;
  const btn = document.querySelector("#recordBtn");
  btn.classList.add("hidden");
  
  const modBtn = document.querySelector("#modBtn");
  modBtn.classList.remove("hidden");

  // 수정할 내용 textarea, input창에 보이기
  const answer = recordArr.filter((item) => item.id === Number(id));
  recordArea.value = answer[0].text;
  recordSource.value = answer[0].source;
  hiddenId.innerText = answer[0].id;

  modBtn.addEventListener("click", modText);
}

 

👏 수정기능은 총 두 개로 나누었다.

첫 번째인 이 코드는 수정버튼을 누른 게시물의 내용, 출처, id 값을 받아와 출력하는 코드.

수정 후 수정 버튼을 누르면 두 번째 수정 기능이 실행된다. 

👏 form 안에 button은 어떤 button을 누르든 form이 submit 되어버린다.

이를 방지하기 위해 새로 추가한 수정 button은 input type = "button"으로 만들어 이를 방지했다.

 

 

🍔 수정(2)

// 수정(2)
// 텍스트 수정 > 로컬스토리지 수정
function modText() {
  // 원본 불러오기
  // 텍스트 수정
  const id = hiddenId.innerText;
  recordArr.forEach((item) => {
    if (item.id === Number(id)) {
      item.text = recordArea.value;
      item.source = recordSource.value;
      item.date = item.date;
      item.id = item.id;
    }
  });
  setRecord();
}

💢 새로고침을 해야만 수정이 됐는지 확인 가능!?

새로고침을 해야만 수정이 됐는지 확인할 수 있는데 .... 새로고침을 하면 아이디확인부터 다시 해야 한다. (index.html에 같이 만들어서)

만약 이런 사이트가 있다 ...? 난 절대 안쓸 거 같다 ... 그러므로 당장 코드를 수정 + 추가했다. 

 

원래는 새로고침을 해도 로그인을 다시 하지 않아도 되는 방법을 찾았으나 뜻대로 안 돼서 좌절하고 있었다.

잘 생각해 보니 새로고침을 "안 해도" 수정한 내용을 확인할 수 있게 하면 되는 문제였다. 

 

👏 해결

1. 그래서 로컬스토리지에 덮어쓰기로 저장하면

2. id 값을 이용해 원하는 li 노드를 가져온 다음

3. li의 자식 노드인 p, span의 innerText를 변경해 준다.

의 방법을 사용했다. 

 

추가한 코드는 👇

  const li = document.getElementById(`${id}`);
  const p = li.firstElementChild;
  p.innerText = recordArea.value;
  const span = p.nextSibling;
  span.innerText = recordSource.value;

  recordArea.value = "";
  recordSource.value = "";

  recordBtn.classList.remove("hidden");
  modBtn.classList.add("hidden");

 

 

 


 

 

 

✔︎ 게시물 삭제

1. 게시물마다 있는 삭제 버튼을 누르면 삭제 기능 실행!

2. 게시물 id를 받아와서 로컬스토리지 value 검색

3. id가 같은 value 요소를 제외한 나머지를 recordArr에 재할당

4. localStorage.setItem 을 사용해 덮어쓰기

 

// 삭제
function delRecord(e) {
  const li = e.target.parentElement;
  li.remove();
  recordArr = recordArr.filter((item) => item.id !== Number(li.id));
  setRecord();
}

👏 다른 function들에 비해 길이도 짧고, 그래서 그런지 네 개의 기능 중 가장 간단하게 구현할 수 있었던 것 같다. 

➕누르자마자 삭제되는 게 아니라 '삭제하시겠습니까?' 라는 창을 보여주는 것도 좋을 것 같다. 실수로 누를 수도 있을 테니.

 

 

 


 

 

 

 

말도 많고 탈도 많은 게시물 편이 끝났다.

추가하고 싶은 혹은 수정하고 싶은 세세한 코드들은 마지막 정리 편 때 같이 기록하겠다!!

만들다가 의외로 HTML, CSS 에서 충격을 많이 먹어서 (내 실력과 상태에 ...) 임시로 만들어두고 후에 수정할 계획이다.

따로 게시물은 만들지 않고 다음은 마지막 정리 편에서 조금 더 자세히 담도록 하겠다. 아듀~🤸🏻