ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 바닐라JS 로 만드는 북마크 웹앱 📓 (2) 게시물 편
    으쌰으쌰 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 에서 충격을 많이 먹어서 (내 실력과 상태에 ...) 임시로 만들어두고 후에 수정할 계획이다.

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

     

     

     

     

     

    댓글

Designed by Tistory.