Mendix SPA에서 HTMLSnippet JS가 꼬이는 이유: 이벤트 중복과 DOM 공존

2026.10.04
  • 시리즈 3/16
  • 개발
  • Mendix 10
  • JavaScript

HTMLSnippet JS 위젯에서 한 번에 접수된 버그 10건을 분석해 보니, 대부분이 Mendix의 페이지 이동 방식에서 나온 문제였다. 이벤트가 중복으로 등록됐고, 페이지가 바뀌는 순간 이전 화면과 새 화면이 잠깐 함께 존재했다.

문제

버튼이 두세 번 실행되고, 토글이 바로 닫히고, 팝업을 닫으면 버튼이 안 눌리고, 다른 페이지로 가면 목록이 무한 로딩

원인

Mendix는 페이지를 이동해도 문서를 새로 읽지 않음. 스니펫 JS가 다시 실행되면서 이벤트가 쌓이고, 잠깐 남아 있는 이전 화면 요소를 새 화면 JS가 잡음

해결

단일 IIFE, document 한 곳에서 이벤트 위임, 이전 실행분 정리, 내 위젯 루트 기준으로만 요소 조회

Mendix 페이지 이동에서 일어나는 일

페이지 A 진입
스니펫 JS 실행document에 click 핸들러 등록
window 객체전역 변수, 타이머 유지
페이지 B 진입
스니펫 JS 다시 실행click 핸들러가 하나 더 쌓임
이전 화면 잠깐 남음같은 클래스, 같은 id 요소가 2개
문서는 그대로이고 화면 영역만 바뀜

증상과 원인

증상원인
저장, 삭제가 두세 번 실행같은 핸들러가 페이지 진입마다 추가 등록
상세 검색 토글이 열렸다 바로 닫힘위임 핸들러와 직접 등록한 핸들러가 둘 다 실행
팝업을 닫은 뒤 버튼이 반응 없음두 핸들러가 서로의 상태를 되돌림
팝업 안을 클릭해도 팝업이 닫힘배경 클릭 판정을 클릭 대상 비교 없이 처리
체크박스를 누르면 1페이지로 이동change 이벤트를 필터 변경과 구분하지 않음
다른 페이지로 가면 목록 무한 로딩새 화면 JS가 이전 화면 요소에 데이터를 그림

코드 파일 안에 같은 블록이 중복으로 들어가 있던 것도 원인이었다. 여러 차례 수정하면서 덧붙인 코드가 겹쳐 있었다. 증상별로 고치지 않고 이벤트 구조를 처음부터 다시 짰다.

재작성 패턴

  1. 이전 실행분 정리이전 실행에서 등록한 이벤트와 타이머를 지움. 정리 함수를 window에 남겨 둠
  2. 내 위젯 루트 잡기아직 연결되지 않은 루트 중 마지막 것을 선택하고 표시
  3. 이벤트는 document 한 곳에서click, change, input을 각각 하나씩만 등록하고 data 속성으로 분기
  4. 요소 조회는 루트 기준document.getElementById 대신 root.querySelector
JS 스니펫 뼈대
(function () {
  // 1. 이전 실행분 정리
  if (window._listWidgetCleanup) window._listWidgetCleanup();

  // 2. 아직 연결되지 않은 루트 중 마지막 것이 이번 페이지의 위젯
  var roots = document.querySelectorAll('.lw-root:not([data-bound])');
  var root = roots[roots.length - 1];
  if (!root) return;
  root.setAttribute('data-bound', '1');

  function $(sel) { return root.querySelector(sel); }

  // 3. 이벤트는 한 곳에서 분기
  function onClick(e) {
    var el = e.target.closest('[data-act]');
    if (!el || !root.contains(el)) return;
    switch (el.getAttribute('data-act')) {
      case 'toggle-filter': $('.lw-filter').classList.toggle('open'); break;
      case 'open': openPopup(el.getAttribute('data-guid')); break;
      case 'delete': removeIssue(el.getAttribute('data-guid')); break;
    }
  }
  function onChange(e) {
    if (!root.contains(e.target)) return;
    if (e.target.matches('.lw-row-check')) { updateSelection(); return; }  // 페이지 유지
    if (e.target.matches('[data-filter]')) { state.page = 1; render(); }
  }

  document.addEventListener('click', onClick);
  document.addEventListener('change', onChange);

  // 4. 다음 실행 때 지울 것들
  window._listWidgetCleanup = function () {
    document.removeEventListener('click', onClick);
    document.removeEventListener('change', onChange);
  };
})();

팝업 배경 클릭 판정

팝업은 배경 자체를 눌렀을 때만 닫기
function onBackdropClick(e) {
  if (e.target !== e.currentTarget) return;   // 팝업 안쪽 클릭은 무시
  closePopup();
}
실제 운영에서는 워치독으로 먼저 대응했다무한 로딩 문제는 처음에 렌더 감시 타이머로 막았다. 몇 초 동안 데이터는 있는데 로딩 표시가 남아 있으면 다시 그리는 방식이다. 처음부터 다시 짠다면 위 루트 기준 조회가 더 단순하다.

바꾸지 말아야 할 것

  • __INITIALIZED__ 같은 전역 플래그로 두 번째 실행을 막는 방식은 맞지 않음. 페이지를 다시 들어오면 화면은 새로 그려졌는데 JS가 실행되지 않음
  • 요소마다 addEventListener를 다는 방식과 위임 방식을 섞지 않음
  • window에 남는 것은 정리 함수와 캐시처럼 의도한 것만 둠

점검 목록

  • 스니펫 JS가 IIFE 하나로 감싸져 있는가
  • 시작 부분에서 이전 실행분을 정리하는가
  • document에 등록하는 이벤트가 종류별로 하나인가
  • 요소 조회가 위젯 루트 기준인가
  • setInterval, setTimeout을 정리 함수에서 해제하는가
  • 페이지 A → B → A 이동을 직접 해 보고 같은 동작이 한 번만 실행되는가
시리즈 목차 16편
  1. PI 담당자가 로우코드로 사내 시스템을 직접 만들고 운영한 기록
  2. Mendix Datagrid2 대신 HTMLSnippet과 JS로 목록 화면 만들기
  3. Mendix SPA에서 HTMLSnippet JS가 꼬이는 이유: 이벤트 중복과 DOM 공존 (현재 글)
  4. Mendix mx.data.commit이 조용히 실패할 때
  5. Mendix Java heap OOM 추적기: base64 이미지 컬럼
  6. Mendix 커밋 롤백, 용량 문제가 아니었다: FileDocument 전환기
  7. Mendix 첫 접속 지연 줄이기: 워밍업과 캐시
  8. Mendix SAML SSO 자동 로그인과 딥링크 유지
  9. Mendix Studio Pro에서 안 바뀌는 스타일 main.scss로 바꾸기
  10. Mendix MPK 파일 열어서 마이크로플로우 구조 보기
  11. SSO 로그 테이블로 MAU와 재방문율 구하는 SQL
  12. Mendix DateTime이 BI 도구에서 하루 빠르게 보이는 이유
  13. 대시보드 숫자가 틀렸을 때 확인하는 순서
  14. 보기 좋은 대시보드에서 할 일을 알려주는 대시보드로
  15. 여러 점검 시스템을 하나로 합치는 데이터 모델
  16. 데이터 비전공자에게 분석 결과 보고하기
  • #Mendix
  • #HTMLSnippet
  • #SPA
  • #이벤트위임
  • #JavaScript
  • #디버깅

댓글