원하는 메뉴를 추가하여 선택할 수 있고 로컬스토리지를 이용하여 선택 사항을 저장하는 메뉴판을 만들어 보자.
이번 과제는 두 가지 방식으로 구현해 봤다. 먼저 강의를 보지 않고 내가 생각하는 방식대로 한 번 만들어 보고, 그 다음 강의에서 요구하는 이벤트 위임(Event Delegation) 방식으로 한 번 더 만들어 봤다. 둘의 차이에 대해서는 코드를 소개한 뒤에 정리해 보겠다.
메뉴판의 제목, 메뉴 목록, 메뉴를 추가할 수 있는 폼을 배치했고, 각 메뉴에는 선택용 체크박스와 삭제 버튼을 달았다.
로컬스토리지를 이용해 현재 메뉴 상황을 저장하거나 불러올 수 있도록 만들었다. 로컬스토리지에 존재하지 않는 키를 요청할 경우 null이 반환되는데, 배열과 null을 동시에 다루려면 예외 처리를 많이 두어야 하기 때문에 처음 접속시 스토리지를 초기화 해주었다.
메뉴 이름을 제출하면 메뉴를 추가할 수 있다. 이미 메뉴판에 있는 메뉴를 다시 추가하려고 하면 경고창을 2초간 표시한다. 메뉴는 이름과 선택 여부를 저장해야 하기 때문에 객체 형식으로 관리한다.
존재하는 메뉴를 각각 리스트 아이템으로 만들어서 보여준다. 각 아이템에는 체크박스, 메뉴 이름, 삭제 버튼이 들어가며, 메뉴가 하나도 없을 때는 메뉴를 추가해 달라는 메시지가 대신 표시된다.
paintMenus에서 아이템을 생성할 때 각 아이템에 이벤트 리스너를 동적으로 추가하는 방식이다.
체크박스를 클릭하면 해당 메뉴의 선택 여부를 토글하고, 삭제 버튼을 클릭하면 메뉴판에서 해당 메뉴를 삭제한다.
이벤트 위임이란, 비슷한 기능을 수행하는 요소가 여러 개 있을 때 각각에서 이벤트를 처리하지 않고, 이들을 감싸는 부모 요소에서 이벤트를 처리하는 방식을 말한다. 이번 과제에서는 li에서 체크박스 및 버튼의 클릭 이벤트가 발생했을 때 ul에서 한 번에 처리하도록 활용할 수 있다.
ul에서 클릭 이벤트가 발생했을 때 체크박스를 클릭했다면 체크박스 이벤트를 처리해주고, 버튼을 클릭했다면 삭제 이벤트를 처리해준다. 처리하는 함수는 위의 함수와 동일하게 작성하였으며 매개변수만 맞춰서 수정해줬다.
자바스크립트 튜토리얼 문서에서는 이벤트 위임의 장단점을 다음과 같이 설명하고 있다.
모든 자식 요소에 핸들러를 추가하면 동적으로 핸들러를 추가하고 제거하는 코드를 작성해야 하며, 요소의 수만큼 핸들러가 필요하다. 하지만 이벤트 위임으로 부모 요소에만 핸들러를 등록한다면 단 하나의 핸들러만 있으면 된다. 예를 들어 20개의 글이 표시되는 목록에 글마다 수정, 삭제, 댓글, 신고, 좋아요 기능을 추가하려고 한다면 100개의 핸들러가 필요하다. 이벤트 위임은 하나의 핸들러로 100개의 핸들러와 동일한 기능을 구현할 수 있도록 해준다. 심지어 꼭 동일한 기능이 아니더라도 data-action 등의 커스텀 태그로 호출하고 싶은 메소드의 이름을 할당하여 비슷한 형식으로 작동하는 여러 메소드를 등록할 수도 있다.
핸들러의 제거에 관해서는 자료를 더 찾아보았는데, 원칙적으로는 요소가 제거되기 전에 핸들러를 해제하고 요소를 제거하는 것이 맞다. 그러나 (늘 논란의 중심인) 구형 IE를 제외한 브라우저에서는 가비지 콜렉션이 잘 이루어지기 때문에 요소를 삭제할 때 연결된 핸들러도 알아서 해제해 준다. 자세히 읽어보기
위임 방식으로 이벤트를 처리할 수 있는 이유는 이벤트 버블링 덕분이다. 웹에서 이벤트가 발생하면 총 3단계의 흐름에 의해 처리된다. 먼저 부모 요소로부터 타깃으로 이벤트가 전파되고(캡처링), 타깃에서 이벤트가 처리된 다음(타깃), 다시 부모 요소로 이벤트가 전파된다(버블링). 우리가 평소 업무를 할 때도 일단 내가 먼저 해결한 뒤에 상사에게 보고하듯이(...) 버블링이 주로 사용되고 캡처링과 타깃 단계는 잘 사용되지 않으므로 버블링에 대해서만 알아보자.
HTML의 요소는 중첩형으로 구성될 수 있다.
<div>
<ul>
<li>
<p>
<span>
...
위의 구조에서 span을 클릭하는 경우를 생각해 보자. 이 클릭 이벤트의 타깃은 span이므로 우선 span의 onClick 핸들러가 호출된다. 그다음 이벤트가 종료되는 것이 아니라, span의 부모를 차례대로 올라가면서 onClick 핸들러를 모두 호출한다. 위 경우에는 span, p, li, ul, div...의 핸들러가 차례대로 호출될 것이다. 이를 이벤트 버블링이라고 부르며 document를 만날 때까지 계속된다. 그렇기 때문에 ul에만 이벤트 핸들러를 등록하더라도 span의 클릭 이벤트를 감지하여 처리할 수 있다. 이때 어떤 요소에서 일어난 이벤트인지 event.target을 통해 구별할 수 있고 버블링 단계에서 현재 이벤트를 처리하고 있는 요소는 event.currentTarget 또는 this로 전달된다. 단, 화살표 함수에서는 this의 바인딩에 유의해야 한다.
이벤트 위임이라는 강력한 이벤트 처리 패턴을 새로 배웠다. JS를 리액트로 시작해서 요소를 동적으로 관리하는 것에 익숙해져서인지 이벤트 핸들러도 동적으로 추가하는 것만 생각했는데, 버블링을 이용해 상위 요소에 핸들러를 등록해 확장성이 좋은 이벤트 처리 방식을 구현할 수 있다는 것을 알게 되었다. 그런데 여전히 개인적으로는 자식 요소에 핸들러를 각각 등록하는 게 더 마음에 든다. 이벤트 위임도 핸들러를 부모 요소에 등록한다는 차이만 있지 실제로 이벤트의 타깃은 자식 요소인데, 이는 실제 기능이 자식 요소에서 수행되어야 한다는 의미이다. 그렇기 때문에 자식 요소에 핸들러를 등록하는 게 캡슐화 측면에서 더 타당해 보인다. 하지만 이건 의미론적인 접근일 뿐이고 성능면에서 이벤트 위임이 더 우수하기 때문에 결국 이 방식을 써야 하긴 할 것이다. 실제로 어느 정도로 성능 차이가 있는지 비교하는 자료는 찾기 힘들어서 메모리나 처리 시간에 차이가 큰지, 앱 규모에 따른 차이는 어느 정도인지 궁금하긴 하다. 프레임워크를 사용할 경우 이벤트 위임에 크게 신경쓰지 않아도 된다. 이벤트 위임을 대신 처리해주는 모듈이 있는 경우도 있고, 리액트는 자체적으로 이벤트 위임 방식으로 처리하기 때문에 굳이 개발자가 이벤트 위임 형식으로 개발하지 않아도 된다.