[Js & JQuery] 새로운 팝업을 맨 앞으로 당기기

식빵·2022년 12월 7일
0
post-thumbnail

🍁작성계기

이번 프로젝트에서 프론트 단 코드를 짜는데, 유지보수 팀의 검토를 받아봤다.
유지보수 팀에서는 우리 프로젝트에서 띄워지는 팝업의 개수가 많아지면,
새롭게 화면상에 보이는 팝업이 기존에 이미 띄워지는 팝업에 가려지는 것에 문제를 제기했다.

그래서 이걸 듣던 차장님께서 "그러면 새로운 팝업은 보일 때마다 z-index 를 늘려서 앞으로 땡기면 되겠네?" 라고 조언을 해주셨다.

괜찮은 방법이라 생각하여 수용하기로 했다.




🍁기존 코드의 문제점

그런데 막상 코드를 짜려니 문제가 있었다.
우리가 팝업을 띄우는 방식은 각 팝업과 매칭되는 자바스크립트 코드(모듈)의 pop 메소드를
호출해서 화면에 보이도록 해왔다.

ex) my.project.utilityPopup.pop()

간단하게 생각하면 저 pop 이라는 메소드에 "모두" z-index 를 변환하는 코드를 넣어주면
되지 않냐고 생각할 수 있다. 하지만 팝업의 개수가 얼마나 더 늘어날지 모르는 상황에서
이런 방식의 접근은 좋지 않다고 생각했다.

그래서 마음속으로 아래와 같은 목표를 갖게 되었다.

  • 기존 코드를 건들지 않는다.
  • 팝업이 표출되는 순간 z-index 가 바뀐다.
  • 더불어서 팝업이 서로 겹쳐있는 상황에서 뒤의 팝업을 클릭하면 앞으로 당긴다.





🐱‍👤해결 코드


✔ 유틸 객체 생성

PopupZIndexManager = (function(window, $){
  
  const INITIAL_Z_INDEX = 1;
  const MAX_Z_INDEX = INITIAL_Z_INDEX + 499;
  let   zIndexOffset = 1;
  
  // 초기화 메소드
  function init() {
    
    // 기존 jQuery show 메소드를 override()
    const func = $.fn.show;
    $.fn.show = function(...arg) {
      return $(this).each(function() {
        let obj = $(this);
        if(obj.hasClass('popup')) { 
          console.debug('show method 에 의한 z-index 변환');
          changeZIndexToTop(obj);
        }
        func.apply(obj, [...arg]);	// 기존 show 메소드 호출
      });
    }

    /*
    $(document).on("click dragstart", ".popup", function(e) {
      if($(e.target).hasClass('btnPopClose')) {
        return false; 
      }
      changeZIndexToTop(e.currentTarget);
    });
    // ===> 이렇게 하면 버그 생김! 아래처럼 하도록!
    */
    
    // ps) 2022-12-21 버그 발생하여 수정.
    // 자세한 내용은 아래 "ps.2022-12-21 버그 고치기" 내용 참고
    Array.from(document.querySelectorAll(".popup"))
	.forEach(item => {

      	// 클릭 이벤트
        item.addEventListener('click', function(event) {
        	console.debug('click event 에 의한 z-index 변환');
      		changeZIndexToTop(item);
        }, true); // ==> 진짜 중요!!! bubbling X, capturing O
      
        // dragstart 이벤트
      	$(item).on("dragstart", function(event){
			console.debug('dragstart event 에 의한 z-index 변환');
			changeZIndexToTop(item);
    	});
	}); 
    
    	
  }
  
  
  /**
  * z-index 를 꼭대기로 보낸다.
  * @param {HTMLElement|jQuery} dom
  */
  function changeZIndexToTop(dom) {
    
    // 가장 큰 z-index 값을 부여받은 것은 z-index 값을 늘리지 않는다.
    const zIndexString = $(dom).get(0).style.zIndex;
    if((zIndexString !== '') 
       && (+zIndexString === zIndexOffset - 1)) {
       return
	}
    
    // 만약에 최대값 제한을 넘겨버리면 cycle 을 돌린다.
    if(MAX_Z_INDEX <= zIndexOffSet) {
		const sortedPopupList = $('.popup').toArray()
        	.filter(popup => Number(popup.style.zIndex) > 0)
        	.sort((a,b) => a.style.zIndex - b.style.zIndex);
      
      	zIndexOffset = INITIAL_Z_INDEX;
      
      	for (let i = 0; i < sortedPopupList.length; i++) {
          	sortedPopupList[i].style.zIndex = zIndexOffset++;
        }
    }
    
    $(dom).css("z-index", zIndex);
  }
  
  return {init}
  
})(window, jQuery);

위처럼 유틸을 하나 만들고, 메인 페이지 제일 하단에 init 을 호출했다.



✔ 메인 페이지

<html>
  <head>
  </head>
  <body>
    <script src="./PopupZIndexManager.js"></script>
    <script>
      // ... 중간 생략 ...
      window.addEventListener('DOMContentLoaded', (event) => {
		PopupZIndexManager.init();
      });
    </script>
  </body>
</html>



✔ ps.2022-12-21 버그 고치기

기존에는 아래처럼 click 이벤트에 대하여 매핑을 했다.

$(document).on("click dragstart", ".popup", function(e) {
  if($(e.target).hasClass('btnPopClose')) {
    return false; 
  }
  changeZIndexToTop(e.currentTarget);
});

그런데 이러면 문제가 있다.
아래 같은 상황을 생각해보자.

  1. 팝업 A 안의 버튼 A 를 클릭
  2. 버튼 A 와 매핑된 EventListener callback 실행
  3. 콜백 메소드 내에서 $("팝업_B").show() 메소드 실행 ==> z-index change!
  4. 팝업 B 가 화면에 보임
  5. 팝업 A버튼 A 에 의한 click bubbling 때문에 z-index change!
  6. 결과적으로 팝업 B 가 앞에 나왔다가 다시 팝업 A 가 맨 앞으로 당겨져서 팝업 B 가 안 보임
  7. 😭

우리가 버튼을 눌러서 어떤 화면이 앞에 나오길 바랐는데,
이렇게 되면 버튼이 있는 팝업이 가장 앞으로 오는 아이러니한 상황이 발생한다.

이건 버블링과 캡처링에 대한 이해가 부족해서 일어난 버그였다.
버블링과 캡처링에 대해 공부하고 나서, click 이벤트에 대해서는 capture phase 에서
콜백이 실행되도록 코드를 수정했다.

Array.from(document.querySelectorAll(".popup"))
  .forEach(item => {

  // 클릭 이벤트
  item.addEventListener('click', function(event) {
    console.debug('click event 에 의한 z-index 변환');
    changeZIndexToTop(item);
  }, true); // ==> 진짜 중요!!! bubbling X, capturing O

  // dragstart 이벤트
  $(item).on("dragstart", function(event){
    console.debug('dragstart event 에 의한 z-index 변환');
    changeZIndexToTop(item);
  });
}); 

이러면 button 에 의한 bubbling phase 보다 내가 위에 작성한
capturing phase 에 의한 이벤트 리스너 콜백이 항상 먼저 실행되므로,
위에서 말한 문제가 사라진다.

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글