이벤트위임 (SHORT)

KHW·2021년 1월 31일
0

Javascript 지식쌓기

목록 보기
21/95
post-custom-banner

1. 이벤트위임

각각의 기능 클릭 시 특정 동작을 하게 하려면 보통은 다음과 같이 이벤트를 등록한다.

document.getElementById("file").addEventListener("click", function(e) {
  // 파일 메뉴 동작
});
document.getElementById("edit").addEventListener("click", function(e) {
  // 편집 메뉴 동작
});
document.getElementById("view").addEventListener("click", function(e) {
  // 보기 메뉴 동작
});

하지만 이벤트 위임을 사용하면 상위 엘리먼트인 <div id='menu'>에만 이벤트 리스너를 추가하면 된다.

document.getElementById("menu").addEventListener("click", function(e) {
  var target = e.target;
  if (target.id === "file") {
    // 파일 메뉴 동작
  } else if (target.id === "edit") {
    // 편집 메뉴 동작
  } else if (target.id === "view") {
    // 보기 메뉴 동작
  }
});

이벤트 위임의 장점

  1. 동적인 엘리먼트에 대한 이벤트 처리가 수월하다.
  2. 상위 엘리먼트에서만 이벤트 리스너를 관리하기 때문에 하위 엘리먼트는 자유롭게 추가 삭제할 수 있다.
  3. 이벤트 핸들러 관리가 쉽다.
  4. 동일한 이벤트에 대해 한 곳에서 관리하기 때문에 각각의 엘리먼트를 여러 곳에 등록하여 관리하는 것보다 관리가 수월하다.
  5. 메모리 사용량이 줄어든다.
    동적으로 추가되는 이벤트가 없어지기 때문에 당연한 결과이다. 1000건의 각주를 등록한다고 생각해보면 고민할 필요로 없는 일이다.
    메모리 누수 가능성도 줄어든다.
    등록 핸들러 자체가 줄어들기 때문에 메모리 누수 가능성도 줄어든다.

2. 이벤트 동작 예시

이벤트 동작 예시 ( 이벤트 위임 X )

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style>
[data-menu]{
    background-color : red;
    width:100px;
    height:300px;
}
[data-file],[data-edit],[data-view]{
    background-color:green;
    width:80px;
    height:80px;
    margin:10px;
}
</style>
<body>
<div data-menu>
    <button data-file id="file">A</button>
    <button data-edit id="edit">B</button>
    <button data-view id="view">C</button>
    </div>
</body>
<script>
    document.getElementById("file").addEventListener("click", function(e) {
        console.log('file')
  // 파일 메뉴 동작
});
document.getElementById("edit").addEventListener("click", function(e) {
        console.log('edit')
  // 편집 메뉴 동작
});
document.getElementById("view").addEventListener("click", function(e) {
        console.log('view')
  // 보기 메뉴 동작
});
</script>
</html>

각각의 버튼마다 핸들러를 설정해서 버튼이 누르면 작동한다.


이벤트 동작 예시 ( 이벤트 위임 O )

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style>
[data-menu]{
    background-color : red;
    width:100px;
    height:300px;
}
[data-file],[data-edit],[data-view]{
    background-color:green;
    width:80px;
    height:80px;
    margin:10px;
}
</style>
<body>
<div data-menu id="menu">
    <button data-file id="file">A</button>
    <button data-edit id="edit">B</button>
    <button data-view id="view">C</button>
    </div>
</body>
<script>
const handler = (e)=>{
var target = e.target;
  if (target.id === "file") {
    console.log('file1')
    // 파일 메뉴 동작
  } else if (target.id === "edit") {
    // 편집 메뉴 동작
    console.log('edit1')
  } else if (target.id === "view") {
    // 보기 메뉴 동작
    console.log('view1')
  }
}


document.getElementById("menu").addEventListener("click",handler);
</script>
</html>

각각의 태그를 담은 전체태그인 menu에 대해서 해당 target들에 대해 각각의 처리를 진행한다. (이벤트를 위임)


이벤트 동작 예시 + 매개변수 처리( 이벤트 위임 O )

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style>
[data-menu]{
    background-color : red;
    width:100px;
    height:300px;
}
[data-file],[data-edit],[data-view]{
    background-color:green;
    width:80px;
    height:80px;
    margin:10px;
}
</style>
<body>
<div data-menu id="menu">
    <button data-file id="file">A</button>
    <button data-edit id="edit">B</button>
    <button data-view id="view">C</button>
    </div>
</body>
<script>
const handler = (e,val)=>{
var target = e.target;
  if (target.id === "file") {
    console.log('file1' + val)
    // 파일 메뉴 동작
  } else if (target.id === "edit") {
    // 편집 메뉴 동작
    console.log('edit1' + val)
  } else if (target.id === "view") {
    // 보기 메뉴 동작
    console.log('view1' + val)
  }
}

const value = 'alphabet'

document.getElementById("menu").addEventListener("click",(e)=>handler(e,value));
// document.getElementById("menu").addEventListener("click",handler);
</script>
</html>

매개변수를 넣으므로 addEventListener의 2번째 부분이 (e)=>handler(e,value) 형태로 e로 event를 받고 value를 받아낸다. (정확한 value인 매개변수 값을 제대로 정해줘야한다.)


3. 이벤트 위임을 이용한 동작부분 찾기

<!DOCTYPE html>
<meta charset="utf-8">
<title>헤더 요소</title>
<style>
[data-header]{
    background:green;
}
[data-header-nav]{
    background:skyblue;
}

[data-section-header]{
    background:purple;
}

[data-section-header-hgroup]{
    background:orange;
}
</style>
<body data-body>
  <header data-header>
    <h1 data-header-h1>페이지 헤더 h1</h1>
    <nav data-header-nav>
      <ul data-header-nav-ul>
        <li>메뉴1</li>
        <li>메뉴2</li>
        <li>메뉴3</li>
      </ul>
    </nav>
    <p>section 밖에 있으면 페이지(body)의 헤더.</p>
  </header>
  <section data-section>
    <header data-section-header>
      <hgroup data-section-header-hgroup>
        <h1>섹션 헤더 h1</h1>
        <h2>섹션 헤더 h2</h2>
      </hgroup>
      <p>섹션 안에 있으면 섹션의 헤더.</p>
    </header>
  </section>
  <footer data-footer>
    <ul data-footer-ul>
      <li>회사소개</li>
      <li>이용약관</li>
      <li>개인정보취급방침</li>
      <li>청소년보호정책</li>
      <li>고객센터</li>
    </ul>
    <address data-address>&copy; <strong><a href="http://corp.jmnote.com/" target="_blank">jmnote.com</a></strong></address>
  </footer>
</body>

<script>
    document.querySelector('body').addEventListener('click',(e)=>{
        console.log(e.target);
    })
</script>

이벤트를 전체를 담는 body를 기준으로 click핸들러를 모두 위임해주고있다.

해당 원하는 태그를 클릭하면 아래와 같이 나타난다.
원하는 태그를 다룰 수 있다.

// 추가로 id나 data-id같은거로 핸들링 처리 부분 확인


특정만 다루기

<!DOCTYPE html>
<meta charset="utf-8">
<title>헤더 요소</title>
<style>
[data-header]{
    background:green;
}
[data-header-nav]{
    background:skyblue;
}

[data-section-header]{
    background:purple;
}

[data-section-header-hgroup]{
    background:orange;
}
</style>
<body data-body>
  <header data-header>
    <h1 data-header-h1>페이지 헤더 h1</h1>
    <nav data-header-nav>
      <ul data-header-nav-ul>
        <li>메뉴1</li>
        <li>메뉴2</li>
        <li>메뉴3</li>
      </ul>
    </nav>
    <p>section 밖에 있으면 페이지(body)의 헤더.</p>
  </header>
  <section data-section>
    <header data-section-header>
      <hgroup data-section-header-hgroup>
        <h1>섹션 헤더 h1</h1>
        <h2>섹션 헤더 h2</h2>
      </hgroup>
      <p>섹션 안에 있으면 섹션의 헤더.</p>
    </header>
  </section>
  <footer data-footer>
    <ul data-footer-ul>
      <li>회사소개</li>
      <li>이용약관</li>
      <li>개인정보취급방침</li>
      <li>청소년보호정책</li>
      <li>고객센터</li>
    </ul>
    <address data-address>&copy; <strong><a href="http://corp.jmnote.com/" target="_blank">jmnote.com</a></strong></address>
  </footer>
</body>

<script>
    document.querySelector('body').addEventListener('click',(e)=>{
        if(e.target.parentNode.dataset.sectionHeaderHgroup != undefined)
            console.log(e.target)
        else if(e.target.parentNode.dataset.footerUl != undefined)
            console.log(e.target)
    })
</script>

특정 sectionHeaderHgroup이 부모인 태그와
footerUl이 부모인 태그만 동작한다.

이벤트 위임 추가 정리 (210906)

li 태그가 계속 추가되는 상황에서 위임을 적용한다면?

this.render의 역할은 랜더링 기능이다.
즉, 매번 이벤트를 등록할 필요가 없다.

만약 랜더링이 ul부터 li까지 전체를 한다면
ul을 대상으로 핸들러를 계속 걸면 랜더링마다 this.render에서 핸들러를 다시 걸어야한다.

이를 해결하기 위해 ul의 상위 태그를 대상으로 핸들러를 걸면
랜더링시 바뀌는 ul li를 제외한 상위에서 이벤트 위임을 통해
this.render에 매번 이벤트를 걸어주지 않아도 정상적으로 동작한다.

문제코드

import {$,$create} from '../utils/utils.js'
import {checkGenerator,checkTodoListState} from "../utils/error.js";
import {setItem} from "../utils/storage.js";

export default function TodoList({$target, initialState = [], onToggle , onRemove}){
  checkGenerator(new.target)
  checkTodoListState(initialState)

  const $todoList = $create('div')
  $target.appendChild($todoList)

  this.state = initialState

  this.setState = (nextState ) => {
    this.state = nextState
    this.render()
  }

  //ul li btn 태그 렌더링
  this.render = () => {
    $todoList.innerHTML = `
      <ul id="ul-todo-item">
      ${this.state.map(({content , isCompleted},idx) =>
      `<li data-idx=${idx} id="li-todo-item" style="list-style:none">
                <input type="checkbox" ${isCompleted ? "checked" : ''} />
                ${isCompleted ? `<s>${content}</s>` : `${content}` }
                <button id="li-btn"> 🗑️ </button>
      </li>`).join('')}
      </ul>
    `

    //이벤트 위임을 통해 $li가 존재하고 li-btn을 id로 가지면 onRemove 그외에는 onToggle 실행
    $('#ul-todo-item').addEventListener('click',(e)=>{
      const {target} = e
      const $li = target.closest('li')
      if($li)
        target.id === 'li-btn'? onRemove($li.dataset.idx) : onToggle($li.dataset.idx)
    })
  }

  this.render()
}

this.render 안에 핸들러가 매번 동작하며 ul이 innerHTML로 새롭게 매번만들어질 때마다 핸들러가 사라져서 다시 등록시키는 동작을 시킨다.

해결방안

import {$,$create} from '../utils/utils.js'
import {checkGenerator,checkTodoListState} from "../utils/error.js";
import {setItem} from "../utils/storage.js";

export default function TodoList({$target, initialState = [], onToggle , onRemove}){
  checkGenerator(new.target)
  checkTodoListState(initialState)

  const $todoList = $create('div')
  $target.appendChild($todoList)

  this.state = initialState

  this.setState = (nextState ) => {
    this.state = nextState
    this.render()
  }

  //ul li btn 태그 렌더링
  this.render = () => {
    $todoList.innerHTML = `
      <ul id="ul-todo-item">
      ${this.state.map(({content , isCompleted},idx) =>
      `<li data-idx=${idx} id="li-todo-item" style="list-style:none">
                <input type="checkbox" ${isCompleted ? "checked" : ''} />
                ${isCompleted ? `<s>${content}</s>` : `${content}` }
                <button id="li-btn"> 🗑️ </button>
      </li>`).join('')}
      </ul>
    `
  }

  //이벤트 위임을 통해 $li가 존재하고 li-btn을 id로 가지면 onRemove 그외에는 onToggle 실행
  $todoList.addEventListener('click',(e)=>{
    const {target} = e
    const $li = target.closest('li')
    if($li)
      target.id === 'li-btn'? onRemove($li.dataset.idx) : onToggle($li.dataset.idx)
  })

  this.render()
}

핸들러를 ul태그가 아닌 상위 태그인 $todoList에 걸어서 해당 부분은 랜더링 되지않아 이벤트위임이 적용된다.

출처

이벤트 위임
캡틴판교

profile
나의 하루를 가능한 기억하고 즐기고 후회하지말자
post-custom-banner

0개의 댓글