-
💿 LocalStorage & Event Delegation 📤Web/JS30 2020. 11. 17. 17:33반응형
어느 순간부터 약간 의무감으로 하고 있었는데
주제들을 차근차근 다시 살펴보니, 자바스크립트를 다루기 위해서 꼭 필요한 것들을 잘 모아놓은 것 같다.
복습하는 셈 치고(물론 그 와중에 새로 배우는 내용들도 많다) 열심히 해야겠다.
이번에는 Local Storage와 Event Delegation에 관한 내용이다.
전혀 관련 없어보이는 두 내용이 함께 등장한 이유는 아마도
투두 리스트를 만드는 데에 있어서 함께 사용되는 기능이기 때문일 것.
미리 생성되어있는 변수들은 다음과 같다.
const addItems = document.querySelector('.add-items'); const itemsList = document.querySelector('.plates'); const items = [];
addItems는 item을 생성하는 form을 가리키고, itemsList는 아이템 li태그들을 담고 있는 ul 태그를 가리킨다.
items는 말 그대로, 리스트에 나열되는 아이템들을 포함하고 있는 배열이다.
addItem 함수
우선, input에 아이템을 입력하고 submit 버튼을 누르면 실행되는 addItem 함수를 구현해보자.
function addItem(e) { e.preventDefault(); const text = (this.querySelector('[name=item]')).value; const item = { text: text, done: false }; items.push(item); this.reset(); } addItems.addEventListener('submit', addItem);
1. submit 이벤트가 일어나는 곳은 'form 전체'이다. 그래서 addItem 내부의 this는 해당 form을 가리키게 된다.
2. this.querySelector문을 괄호로 감싸준 이유는 정확하진 않지만,
querySelector로 해당 element를 찾는 작업이 먼저 일어나고,
그 다음에 거기서 value 속성을 잡아내기 위함이라고 한다.
나는 원래 당연히 그 순서로 동작하는 거라고 생각했는데, 그게 아닌가보다.
3. this.reset() 은 해당 form의 input들을 초기화시키는 메서드.
populateList 함수
다음은, items 배열을 화면에 render하는 역할을 하는 populateList 함수를 살펴본다.
function populateList(plates = [], platesList) { platesList.innerHTML = plates.map((plate, i) => { return ` <li> <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} /> <label for="item${i}">${plate.text}</label> </li> `; }).join(''); } populateList(items, itemsList);
1. 매개변수의 기본값을 설정해 준 이유는, 만약 인자로 아무것도 들어오지 않더라도
빈 배열이 들어오도록 하여 에러가 발생하지 않게 하기 위함이다.
함수 내부에서 map 메서드를 사용할 것이기 때문에, 에러를 방지하기 위해서는 어떻게든 배열이 존재해야 한다.
2. render하는 HTML 코드를 보면, input의 id와 label의 for가 동일한 것을 알 수 있다.
이렇게 해야 해당 label과 input이 연결된다.
그러면, input과 checkbox 둘 중 하나만 클릭해도
둘이 연결되어있기 때문에 둘 모두에서 click 이벤트가 발생한다.
3. input의 checked 속성은 어떤 값이던지, 특정한 값이 존재하기만 하면 무조건 checked로 처리된다.
'checked=false'라고 해도 무조건 checked로 처리된다.
checked가 가질 수 있는 속성은 오로지 빈 문자열 혹은 checked, 둘 중 하나임.
태그에서 빈 문자열은 존재하지 않는다는 의미이므로.
4. checkbox에 data-index를 심어놓은 이유는 나중에 이벤트가 발생한 체크박스를 특정하기 위함.
5. 참고로, 이 함수에서 리스트를 render 하는 방식은 굉장히 비효율적이다.
새로운 값이 추가될 때마다, 그냥 무조건 전체 리스트를 다시 렌더링하기 때문.
이러면 리스트에 이벤트가 붙고, 뭐가 붙고, 내용이 많아지고 할 수록
성능적으로 굉장히 안좋아진다.
이를 해결하는 데에 도움을 주는 것이 바로 React나 Angular의 Virtual DOM.
근데, 이렇게만 해주면 새로 항목이 추가되어도 새로고침하면 다 날라가게 된다.
이럴 때 데이터베이스 없이 기존의 항목들을 유지하고싶다면, LocalStorage를 사용하면 된다.
LocalStorage
LocalStorage를 사용하는 방법은 간단하다.
데이터를 넣기 위해서는 localStorage.setItem()을,
데이터를 가져오기 위해서는 localStorage.getItem()을,
데이터를 삭제하기 위해서는 localStorage.removeItem()을 사용해주면 된다.
한 가지 명심해야 할 점은, localStorage는 반드시 문자열 데이터만 저장된다.
그래서, 객체를 데이터로 저장하기 위해서는 JSON 형식의 문자열로 변환해야한다.
객체를 JSON 형식의 문자열로 변환하기 위해서는 JSON.stringify()를,
JSON 형식의 문자열에서 객체로 다시 변환하기 위해서는 JSON.parse()를 사용해주면 된다.
객체를 문자화하지 않고 집어넣으면, 그냥 그 객체에 달린 toString 메소드를 임의로 호출한다.
그래서 localStorage에는 '[Object object]'라는 내용의 문자열이 그냥 그대로 저장되어버린다.
이제, addItem 함수를 다음과 같이 바꿔준다.
function addItem(e) { e.preventDefault(); const text = (this.querySelector('[name=item]')).value; const item = { text: text, done: false }; items.push(item); populateList(items, itemsList); localStorage.setItem('items', JSON.stringify(items)); this.reset(); }
아이템을 새로 추가할 때마다 화면에도 새로 render 해줘야 하므로 populateList 함수를 호출해줬고
그 때마다 localStorage에 저장해줘야 하므로 localStorage.setItem()을 호출해줬다.
toggleDone
이제, 체크박스에 관한 함수인 toggleDone 함수를 살펴보자.
function toggleDone(e) { if(!e.target.matches('input')) return; // skip this unless it's an input const el = e.target; const index = el.dataset.index; items[index].done = !items[index].done; localStorage.setItem('items', JSON.stringify(items)); populateList(items, itemsList); } itemsList.addEventListener('click', toggleDone);
1. 일단 맨 위의 if문은 대상을 input으로 한정시켜주는 역할을 한다.
아까 label과 checkbox를 연결시켜놨기 때문에, 둘 중 하나만 클릭해도 둘 모두에서 클릭 이벤트가 발생한다.
그래서 console.log로 event target을 찍어봐도 둘 모두가 출력된다.
이 때, 우리는 체크박스만을 다루고 싶기 때문에 label일 경우 return시켜줌으로써 대상을 input으로 한정한 것.
2. 그러면 이제 e.target인 el은 input만 가리키게 된다.
그리고 아까전에 심어놨던 data-index를 사용해주면, 이벤트가 어디에서 발생했는지를 특정할 수 있게 된다.
여러 체크박스 중 이벤트가 발생한 체크박스의 인덱스를 index 변수에 담아줬다.
3. items 배열의 순서와 화면에 render된 체크박스의 순서는 동일하므로,
그대로 items[index]의 done 속성을 반대로 바꿔주면 된다.
4. 체크를 했는지/안했는지 여부도 저장해야하는 데이터이므로,
당연히 변화가 생길 때마다 localStorage에 저장하고 populateList 함수를 호출하면 된다.
여기서, 모르는 사람은 눈치채지도 못할 만큼 아주 자연스럽게 도입된 개념이 Event Delegation이다.
Event Delegation
위의 toggleDone 함수를 click 이벤트 리스너로 붙여줄 때 보면
체크박스 하나하나마다 붙여준 게 아니라, 체크박스 리스트들을 포함하는 부모(ul)인
itemsList에다가 붙여준 것을 볼 수 있다.
우리가 만약에 체크박스(input) 하나하나마다 이벤트 리스너를 붙여준다고 생각해보자.
1. 우선, 아이템들이 브라우저에 등장하기 전에는 그 대상을 특정해서 addEventListener를 하는 게 불가능하다.
보통 대상을 변수에 담아서 addEventListener를 해줘야 하는데, 그 대상이 존재하지 않기 때문.
2. 만약 우리가 아이템들이 등장한 이후(populateList 이후)에,
그 녀석들을 대상으로 addEventListener를 한다고 치자.
그러면, 아이템을 새로 추가할 때는 그 새로 추가되는 녀석들에게는 event listener가 붙지 않게 된다.
심지어, 이 예제에서는 아이템을 새로 추가할 때마다 전체 리스트를 다시 render하기 때문에
기존의 아이템들에 존재하던 event listener들도 사라진다.
그렇다면 다시 1번으로 돌아가서, 페이지에 존재하지 않는 element에다가
event listener를 붙여주기 위해서는 어떤 방법을 사용해야 할까?
이럴 때 사용되는 것이 바로 Event Delegation이다.
아이템 각각에다가 event listener를 붙여주는 대신에,
이 아이템들(li)을 모두 포함하는 부모인 ul 태그에다가 event listener를 붙여준다.
ul 태그는 아이템들이 추가되기 이전에도 이미 브라우저에 존재하는 태그이기 때문에
거기다가 event listener를 붙여주는 것.
참고로, 하위 element에서 발생한 이벤트는
그 하위 element를 포함하는 상위 element에서도 발생하게 된다.
그래서, 부모가 이벤트를 listening하고 있다가, 이벤트가 발생하면 그 이벤트가 발생한 child를 찾아서
그 녀석에게 무엇을 해야 할지를 말해주게끔 하는 것이 Event Delegation이다.
여기서 이벤트가 발생한 child를 특정하기 위해 아까 data-index를 이용했던 것.
전체 자바스크립트 코드
<script> const addItems = document.querySelector('.add-items'); const itemsList = document.querySelector('.plates'); const items = JSON.parse(localStorage.getItem('items')) || []; function addItem(e) { e.preventDefault(); const text = (this.querySelector('[name=item]')).value; const item = { text: text, done: false }; items.push(item); populateList(items, itemsList); localStorage.setItem('items', JSON.stringify(items)); this.reset(); } function populateList(plates = [], platesList) { platesList.innerHTML = plates.map((plate, i) => { return ` <li> <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} /> <label for="item${i}">${plate.text}</label> </li> `; }).join(''); } function toggleDone(e) { if(!e.target.matches('input')) return; const el = e.target; const index = el.dataset.index; items[index].done = !items[index].done; localStorage.setItem('items', JSON.stringify(items)); populateList(items, itemsList); } addItems.addEventListener('submit', addItem); itemsList.addEventListener('click', toggleDone); populateList(items, itemsList); </script>
체크박스에 이모지 표시하는 방법
깨알같은 팁.
CSS를 이용해서 체크박스 모양을 바꿔줄 수 있다.
.plates input { display: none; } .plates input + label:before { content: "⬜️"; margin-right: 10px; } .plates input:checked + label:before { content: "🌮"; }
정확하게 말하면, input을 보이지 않게 없애버리고
그 앞에다가 체크박스 역할을 하는 이모지를 대신 보여주는 것.
반응형'Web > JS30' 카테고리의 다른 글
📊 Sort Without Articles 🎢 (0) 2020.11.19 🤩 CSS Text Shadow Mouse Move Effect 😎 (0) 2020.11.18 🍴 References VS Copying 🎎 (0) 2020.11.13 Slide In on Scroll 💻 (0) 2020.11.12 Key Sequence Detection(KONAMI CODE) 🎮🎲 (0) 2020.11.11