바닐라JS 로 만드는 북마크 웹앱 📓 (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 에서 충격을 많이 먹어서 (내 실력과 상태에 ...) 임시로 만들어두고 후에 수정할 계획이다.
따로 게시물은 만들지 않고 다음은 마지막 정리 편에서 조금 더 자세히 담도록 하겠다. 아듀~🤸🏻