중첩함수를 콜백으로 받는 이벤트리스너의 문제점

Sunflo·2023년 4월 16일
0

🐞Bug report

목록 보기
1/1
post-thumbnail

🐌 버그를 만나다...

땡세권 프로젝트를 리팩토링 하던 중 버그를 만났다.
원래는 잘 작동하던 이벤트 리스너가 중복되어 등록되는 것!
localOverlayClickHandler() ,subwayOverlayClickHandler() 이 두 함수가 선언된 위치가 원인이었다.

아래는 버그가 발생한 코드다.
main.js

kakao.maps.event.addListener(map, "zoom_changed", function (mouseEvent) {
  // 이외 코드 생략
  kakaoMap.setEventOnOverlay();
});

kakaoMap.js

function setEventOnOverlay() {

  const localOverlay = document.querySelectorAll(
    ".customOverlay.customOverlay--local"
  );
  
  // * 문제가 되는 이벤트 핸들러
  const localOverlayClickHandler = (event) => {
    let overlay = event.target;
    map.setLevel(DEFAULT_MAP_LEVEL - 1);
    map.setCenter(
      new kakao.maps.LatLng(overlay.dataset.lat, overlay.dataset.lng)
    );
  };

  // * 문제가 되는 이벤트 핸들러
  const subwayOverlayClickHandler = (event) => {
    let overlay = event.target;

    displayLocalOverlay(false);
    displaySubwayOverlay(false);

    // 방 클러스터가 있음을 알리는 상태
    setRoomClusterState(true);

    // 이미 방에 대한 마커가 있다면 삭제, 삭제하지 않으면 계속 중첩된다.

    if (roomCluster) {
      roomCluster.clear();
    }

    // 방 정보를 요청하여 방클러스터 생성
    createOneRoomCluster(overlay.dataset.name);

    // 새로운 지하철로 매물을 검색했으니 필터용 오리지널 방 정보를 초기화
    originalRoomAndMarker.length = 0;

    // 필터 버튼 활성화
    filter.ableFilterBtn();

    map.setLevel(5);
    map.setCenter(
      new kakao.maps.LatLng(overlay.dataset.lat, overlay.dataset.lng)
    );
  };
  
  localOverlay.forEach((overlay) => {
    // * 문제가 되는 이벤트 핸들러 등록
    overlay.addEventListener("click", localOverlayClickHandler);
  });

  // 지하철 오버레이는 많아서 모든 오버레이를 선택할수 있게 살짝 지연시켰다.
  setTimeout(() => {
    const subwayOverlay = document.querySelectorAll(
      ".customOverlay.customOverlay--subway"
    );

    subwayOverlay.forEach((overlay) => {
      // * 문제가 되는 이벤트 핸들러 등록
      overlay.addEventListener("click", subwayOverlayClickHandler);
    });
  }, 500);
}

🤔 왜 이런 문제가 발생했을까?

일단 알고있는걸 적어보자

  1. 리스너 인자에 익명함수를 사용하면 이벤트가 중복된다.
  2. 리스너 인자에 기명함수를 사용하면 이벤트가 중복되지 않는다.
  3. 중첩함수는 상위함수가 실행될 때마다 재선언된다.
  4. 익명함수는 매번 새롭게 메모리에 등록된다.

2번에 중첩함수라면 기명함수더라도 중복이 된다. -> 따라서 2번의 전제는 틀린것이다.
1차 추론 : "이벤트리스너는 리스너를 이름으로 구분하는 것이 아니다"

익명함수와 중첩함수는 매번 새로 생성된다. -> 이 둘을 중점으로 찾아보자...
찾아봤지만 리스너와 중첩함수에 대한 명확한 자료는 나오지 않았다.
하지만 익명함수와 중첩함수는 메모리에 새로운 인스턴스로 등록되며, 재사용할 목적이라면 함수 외부에서 선언하여 메모리를 낭비하지 말라는 공통점을 찾았다.
2차 추론 : 이벤트리스너는 리스너를 메모리로 구분한다.

따라서 아래의 과정으로 문제가 발생된다는 것을 알 수 있다.
1. setEventOnOverlay() 함수를 호출한다
2. 이후 함수 내부의 localOverlayClickHandler, subwayOverlayClickHandler 를 새 메모리에 저장
3. 매번 새 메모리에 저장되므로 메모리가 달라 기명함수임에도 이벤트 중첩이 발생

✅ 결론

중첩함수를 함수 밖에서 선언하는 방법으로 문제를 해결했다.
removeEventListener()를 사용하는 방법도 있지만 앱의 구조상 이벤트가 여러번 등록돼서 매번 생성하고 삭제하는건 비효율적이라고 판단했다. 코드의 가독성을 줄이고 메모리의 효율성을 택했다.

  1. 이벤트리스너는 이름이나 내용으로 콜백함수를 구분하는 것이 아니다.
  2. 이벤트리스너는 메모리로 콜백함수를 구분한다.

명확한 자료를 찾지는 못했지만 추론하여 원인을 찾아보았다.
이후 자료를 찾게되면 내용을 추가한다.

자료를 찾아 추가한다!

MDN의 addEventListener 문서에 이런 내용이 있다.

Actually, regarding memory consumption, the lack of keeping a function reference is not the real issue; rather it is the lack of keeping a static function reference.

해석하자면 결국 정적 함수를 참조해야한다는 뜻이다.
이 내용에 대해서 영어 문서에는 이렇게만 나와있고 한글 문서에 더 자세한 내용이 있었다.

상황 3에서는 반복할 때마다 익명 함수에 대한 참조를 재할당하고, 상황 4에서는 함수 전체 정의는 변하지 않지만 매번 마치 새로운 함수처럼 반복적으로 재정의되므로 두 상황 모두 정적이지 않습니다. 따라서 코드를 보기엔 다수의 동일한 이벤트 수신기처럼 보이지만, 사실 각 반복마다 새로운 처리기를 참조하는 새로운 이벤트 수신기를 생성하고 있는 것입니다.
또한 상황 3과 4에서는 함수 참조가 유지되긴 하지만 매번 addEventListener() 전에 재정의되므로, removeEventListener("click", processEvent, false)로 수신기를 제거할 수는 있으나 오직 마지막으로 정의된 수신기만 제거됩니다.

아래는 공식문서의 예시 코드이다.

const els = document.getElementsByTagName('*');

function processEvent(e){
  /* do something */
}

// 시연을 위해 [i] 대신 [j]를 사용하는 실수를 한 것에 주의하세요. 반복문 내에서 정의한 수신기를 모두 첫 요소에 등록하고 있습니다.

// 상황 3
for(let i = 0, j = 0 ; i < els.length ; i++){
  els[j].addEventListener("click", processEvent = function(e){/* do something */}, false);
}

// 상황 4
for(let i = 0, j = 0 ; i < els.length ; i++){
  function processEvent(e){/* do something */};
  els[j].addEventListener("click", processEvent, false);
}

위 코드를 적절히 수정해서 실행해보면 모두 반복된만큼 이벤트가 실행된다.
완벽하게 해결했다!

profile
뭐든지 할 수 있고, 뭐든지 될 수 있다.

0개의 댓글