HTMLSnippet JS 위젯에서 한 번에 접수된 버그 10건을 분석해 보니, 대부분이 Mendix의 페이지 이동 방식에서 나온 문제였다. 이벤트가 중복으로 등록됐고, 페이지가 바뀌는 순간 이전 화면과 새 화면이 잠깐 함께 존재했다.
문제
버튼이 두세 번 실행되고, 토글이 바로 닫히고, 팝업을 닫으면 버튼이 안 눌리고, 다른 페이지로 가면 목록이 무한 로딩
원인
Mendix는 페이지를 이동해도 문서를 새로 읽지 않음. 스니펫 JS가 다시 실행되면서 이벤트가 쌓이고, 잠깐 남아 있는 이전 화면 요소를 새 화면 JS가 잡음
해결
단일 IIFE, document 한 곳에서 이벤트 위임, 이전 실행분 정리, 내 위젯 루트 기준으로만 요소 조회
Mendix 페이지 이동에서 일어나는 일
페이지 A 진입
스니펫 JS 실행document에 click 핸들러 등록
window 객체전역 변수, 타이머 유지
페이지 이동
페이지 B 진입
스니펫 JS 다시 실행click 핸들러가 하나 더 쌓임
이전 화면 잠깐 남음같은 클래스, 같은 id 요소가 2개
증상과 원인
| 증상 | 원인 |
|---|---|
| 저장, 삭제가 두세 번 실행 | 같은 핸들러가 페이지 진입마다 추가 등록 |
| 상세 검색 토글이 열렸다 바로 닫힘 | 위임 핸들러와 직접 등록한 핸들러가 둘 다 실행 |
| 팝업을 닫은 뒤 버튼이 반응 없음 | 두 핸들러가 서로의 상태를 되돌림 |
| 팝업 안을 클릭해도 팝업이 닫힘 | 배경 클릭 판정을 클릭 대상 비교 없이 처리 |
| 체크박스를 누르면 1페이지로 이동 | change 이벤트를 필터 변경과 구분하지 않음 |
| 다른 페이지로 가면 목록 무한 로딩 | 새 화면 JS가 이전 화면 요소에 데이터를 그림 |
코드 파일 안에 같은 블록이 중복으로 들어가 있던 것도 원인이었다. 여러 차례 수정하면서 덧붙인 코드가 겹쳐 있었다. 증상별로 고치지 않고 이벤트 구조를 처음부터 다시 짰다.
재작성 패턴
- 이전 실행분 정리이전 실행에서 등록한 이벤트와 타이머를 지움. 정리 함수를 window에 남겨 둠
- 내 위젯 루트 잡기아직 연결되지 않은 루트 중 마지막 것을 선택하고 표시
- 이벤트는 document 한 곳에서click, change, input을 각각 하나씩만 등록하고 data 속성으로 분기
- 요소 조회는 루트 기준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편
- PI 담당자가 로우코드로 사내 시스템을 직접 만들고 운영한 기록
- Mendix Datagrid2 대신 HTMLSnippet과 JS로 목록 화면 만들기
- Mendix SPA에서 HTMLSnippet JS가 꼬이는 이유: 이벤트 중복과 DOM 공존 (현재 글)
- Mendix mx.data.commit이 조용히 실패할 때
- Mendix Java heap OOM 추적기: base64 이미지 컬럼
- Mendix 커밋 롤백, 용량 문제가 아니었다: FileDocument 전환기
- Mendix 첫 접속 지연 줄이기: 워밍업과 캐시
- Mendix SAML SSO 자동 로그인과 딥링크 유지
- Mendix Studio Pro에서 안 바뀌는 스타일 main.scss로 바꾸기
- Mendix MPK 파일 열어서 마이크로플로우 구조 보기
- SSO 로그 테이블로 MAU와 재방문율 구하는 SQL
- Mendix DateTime이 BI 도구에서 하루 빠르게 보이는 이유
- 대시보드 숫자가 틀렸을 때 확인하는 순서
- 보기 좋은 대시보드에서 할 일을 알려주는 대시보드로
- 여러 점검 시스템을 하나로 합치는 데이터 모델
- 데이터 비전공자에게 분석 결과 보고하기