[Mendix] Datagrid2 버리고 HTML+JS 위젯으로 갈아탄 삽질기
Mendix 기본 위젯의 한계를 넘기 위해 Java Action + HTML Snippet 조합으로 커스텀 목록 위젯을 만들면서 만난 7가지 에러와 해결 과정을 정리합니다.
1. 목표 — 왜 Datagrid2를 버리려 했나
점검 결과를 관리하는 페이지에서 Mendix 기본 제공 Datagrid2를 사용하고 있었다.
그런데 요구사항이 쌓일수록 기본 위젯만으로 구현하기 어려운 것들이 늘어났다.
| 요구사항 | Datagrid2로 가능? | 비고 |
|---|---|---|
| 행 클릭 → 커스텀 팝업 상세보기 | △ 제한적 | 기본 팝업 레이아웃 자유도 부족 |
| 역할별 편집 권한 분기 (3단계) | ✗ | Admin / 담당자 / 일반 구분 필요 |
| 행 확장 — 요약 + 상세 컬럼 토글 | ✗ | 행 내 ▶ 클릭으로 펼치기 |
| 상태 탭 필터 (조치예정 / 지연 / 완료) | △ | MF 호출 매번 필요 |
| 조치기한 지난 행 색상 강조 | ✗ | 조건부 스타일 지원 없음 |
| 사진 필드 Ctrl+V 붙여넣기 | ✗ | CKEditor HTML을 그대로 표시 필요 |
결국 Datagrid2를 걷어내고 HTML/JavaScript Snippet 위젯 2개로 전체 목록+팝업을 직접 구현하기로 했다.
2. 설계한 아키텍처
전체 데이터 흐름
└→ Mendix DB XPath 조회 → StringBuilder로 JSON 직렬화 → String 반환
[Microflow Wrapper] MF_DS_GetIssueList_JSON
└→ Call JA → Return $ReturnValue
[Browser] mx.data.action("Inspection.MF_DS_GetIssueList_JSON")
└→ JSON.parse → DOM 렌더링
[HTML Snippet #1] <style> + HTML 마크업
[HTML Snippet #2] JavaScript 로직 (태그 없이 순수 JS)
getElementById로 DOM을 참조하기 때문에 마크업이 먼저 렌더링되어야 한다.
권한 분기 구조
| 역할 | 점검정보 탭 | 불합리정보 탭 | 불합리조치 탭 | 저장 버튼 |
|---|---|---|---|---|
| 관리자 | ✏ 편집 | ✏ 편집 | ✏ 편집 | 표시 |
| 조치담당자 | 👁 읽기전용 | 👁 읽기전용 | ✏ 편집 | 표시 |
| 일반 | 👁 읽기전용 | 👁 읽기전용 | 👁 읽기전용 | 숨김 |
3. 마주친 문제들 한눈에 보기
| # | 에러 / 증상 | 원인 | 해결 |
|---|---|---|---|
| 1 | package org.json does not exist |
Mendix 런타임에 org.json 라이브러리 없음 | StringBuilder 직접 JSON 구성 |
| 2 | retrieveXPathQuery 시그니처 불일치 |
Mendix 버전별 오버로드 차이 | 5-arg → 4-arg 시그니처로 교체 |
| 3 | SyntaxError: Unexpected token '<' |
HTML Snippet에 <script> 태그 포함 |
태그 제거, 순수 JS만 붙여넣기 |
| 4 | does not exist — mx.data.action이 JA 직접 호출 불가 |
mx.data.action은 Microflow만 호출 가능 | JA 감싸는 Microflow Wrapper 추가 |
| 5 | Association 필드 값이 전부 undefined | 연결 엔티티 속성명 'Name' 불일치 ('column1'이 실제명) |
속성명을 Studio Pro에서 직접 확인 후 교체 |
| 6 | 정렬 시 Can't find attribute 에러 |
오타: InspectionDetectionDate → 실제명은 IssueDetectionDate |
실제 Attribute명으로 수정 |
| 7 | 팝업 상단이 Mendix 내비바에 가려짐 | 모달 position:fixed; top:0이 내비바 뒤로 들어감 |
JS로 내비바 높이 동적 측정 → padding-top 적용 |
4. 문제별 상세 해결법
① org.json 라이브러리 없음
Mendix 런타임에 org.json이 없어서 JSONArray, JSONObject import가 전부 실패했다.
해결: StringBuilder로 JSON을 직접 조립하고, 특수문자 처리용 헬퍼를 추가했다.
// org.json 대체: StringBuilder 직접 JSON 구성
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
private void appendStr(StringBuilder sb, String key, String val, boolean last) {
sb.append("\"").append(key).append("\":\"")
.append(escapeJson(val)).append("\"");
if (!last) sb.append(",");
}
② retrieveXPathQuery 시그니처 불일치
작성한 코드가 7-arg 오버로드를 사용했는데, Mendix 10.x에는 해당 시그니처가 없다.
// ❌ 잘못된 시그니처 (Mendix 10에 없음)
Core.retrieveXPathQuery(ctx, xpath, -1, 0, new HashMap<>(), "attr", false);
// ✅ 올바른 시그니처
Map<String, String> sortMap = new HashMap<>();
sortMap.put("IssueDetectionDate", "desc");
List<IMendixObject> issues = Core.retrieveXPathQuery(ctx, xpath, -1, 0, sortMap);
③ HTML Snippet에서 <script> 태그 오류
Mendix HTML/JavaScript Snippet 위젯은 Content Type을 HTML로 설정하면
<script> 태그를 그대로 문자열로 파싱하려 해서 Syntax Error가 발생한다.
| 위젯 | Content Type | <style> | <script> |
|---|---|---|---|
| Widget #1 (HTML) | HTML | ✅ 포함 | ❌ 없어야 함 |
| Widget #2 (JS) | JavaScript | ❌ | ❌ 태그 없이 JS 코드만 |
④ mx.data.action이 Java Action 직접 호출 불가
mx.data.action("Inspection.DS_GetIssueList_JSON", ...)을 호출했더니
"does not exist" 에러. mx.data.action은 Microflow만 호출한다.
해결: JA를 감싸는 얇은 Microflow Wrapper 하나를 만들면 끝.
Return type: String
└→ [Call Java Action] DS_GetIssueList_JSON → $ReturnValue
└→ [End Event] Return: $ReturnValue
⑤ Association 속성명 불일치
JA에서 연결 엔티티의 표시값을 읽을 때 "Name"으로 썼는데,
실제 엔티티 Attribute명이 "column1"이었다.
// ❌ 엔티티에 'Name' 속성이 없어서 에러
getAssocValue(ctx, obj, "...R_InspectionRegulation", "Inspection.R_InspectionRegulation", "Name");
// ✅ Studio Pro에서 실제 속성명 확인 후 적용
getAssocValue(ctx, obj, "...R_InspectionRegulation", "Inspection.R_InspectionRegulation", "column1");
column1을 쓰는 경우가 많다.
⑥ Attribute 오타 (정렬 키)
정렬 키로 "InspectionDetectionDate"를 썼는데 실제 엔티티 속성명은
"IssueDetectionDate"였다. 런타임에서 정렬 시점에 터짐.
// ❌
sortMap.put("InspectionDetectionDate", "desc");
// ✅ 실제 Attribute 물리명 확인 후
sortMap.put("IssueDetectionDate", "desc");
⑦ 팝업 모달이 Mendix 내비바에 가려짐
모달 backdrop을 position: fixed; top: 0; inset: 0으로 설정했더니
Mendix 상단 내비바(높이 약 60px)가 모달 위에 올라와 헤더가 가려졌다.
// JS에서 내비바 실제 높이를 동적으로 읽어 padding-top 적용
function getNavbarHeight() {
const selectors = ['.mx-navbar', 'nav.navbar', 'header'];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return Math.ceil(el.getBoundingClientRect().bottom);
}
return 60; // fallback
}
// 모달 열 때
document.getElementById('eil-backdrop').style.paddingTop
= getNavbarHeight() + 'px';
9999 이상으로 설정해야 Mendix 내부 요소들 위에 표시된다.
5. 핵심 코드 구조
JA — Association 값 읽기 헬퍼
private String getAssocValue(IContext ctx, IMendixObject obj,
String assocName, String targetEntity, String attrName) {
try {
IMendixIdentifier id = (IMendixIdentifier) obj.getValue(ctx, assocName);
if (id == null) return "";
IMendixObject ref = Core.retrieveId(ctx, id);
if (ref == null) return "";
Object val = ref.getValue(ctx, attrName);
return val == null ? "" : val.toString();
} catch (Exception e) {
return "";
}
}
JS — Microflow 호출 (mock fallback 포함)
function loadIssues() {
if (typeof mx !== 'undefined') {
mx.data.action({
params: { actionname: 'Inspection.MF_DS_GetIssueList_JSON' },
callback: function(result) {
try {
allData = JSON.parse(result);
renderTable();
} catch(e) {
console.error('JSON 파싱 실패', e);
loadMock(); // fallback
}
},
error: function(e) {
console.error('MF 호출 실패', e);
loadMock();
}
});
} else {
loadMock(); // Mendix 런타임 외부 (개발 미리보기)
}
}
JS — 권한 분기 로직
function getCurrentUserRole() {
if (typeof mx === 'undefined') return 'admin'; // 개발 모드 → 관리자
const user = mx.session.getUserName ? mx.session.getUserName() : '';
if (user === 'MxAdmin') return 'admin';
const roles = mx.session.getUserRoles ? mx.session.getUserRoles() : [];
if (roles.some(r => r.includes('Administrator') || r.includes('Admin'))) return 'admin';
return 'user'; // 일반 유저; 조치담당자 판단은 IssueClearPersonId 비교로
}
function openModal(row) {
const role = getCurrentUserRole();
const isMyTask = (row.IssueClearPersonId === currentUser);
const canEditAll = (role === 'admin');
const canEditClear = (role === 'admin' || isMyTask);
// ...탭별 readonly 속성 토글
}
JS — 사진 필드 (contenteditable — CKEditor HTML 렌더링)
// 저장된 HTML을 innerHTML로 바로 렌더링 (이미지 포함)
function setPhotoArea(fieldId, htmlContent, readonly) {
const el = document.getElementById(fieldId);
if (!el) return;
el.innerHTML = htmlContent || '';
el.contentEditable = readonly ? 'false' : 'true';
}
// 저장 시 HTML 추출
function getPhotoHtml(fieldId) {
const el = document.getElementById(fieldId);
return el ? el.innerHTML : '';
}
6. 교훈 — 삽질에서 배운 것
mx.data.action은 Microflow만 호출한다
Java Action은 직접 호출 불가. 반드시 Microflow Wrapper로 감싸야 한다. 문서에 명시가 약하다.
HTML Snippet에 <script> 태그 넣지 말 것
Content Type이 JavaScript인 위젯에는 태그 없이 JS 코드만 붙여야 한다. 태그가 들어가면 Syntax Error.
라이브러리 의존 없이 JDK만 써라
Mendix 런타임에 외부 JSON 라이브러리가 없다. StringBuilder로 직접 구성하면 의존성 문제 없이 깔끔하다.
Association 속성명은 반드시 Studio Pro에서 확인
관행적으로 'Name'을 쓰면 십중팔구 에러. Domain Model 열고 Attributes 탭에서 실제 String 속성명을 확인해야 한다.
모달과 내비바는 반드시 높이 동적 측정
고정 px로 내비바 높이를 때려 넣으면 환경마다 다르다. JS로 getBoundingClientRect().bottom을 읽어서 적용해야 정확하다.
mock fallback 먼저 붙이고 개발하라
Mendix 없는 환경에서도 typeof mx === 'undefined' 분기로 목업 데이터로 fallback되게 해두면 프론트 개발 속도가 훨씬 빠르다.
'Stack > Lowcode' 카테고리의 다른 글
| [Mendix] SAML SSO 환경에서 딥링크(DeepLink) 구현 삽질기 (1) | 2026.03.23 |
|---|---|
| [Mendix] DeepLink + SAML SSO 구현 — 메일 버튼 클릭 한 번으로 팝업 이동 (0) | 2026.03.18 |
| [Mendix] 자동 메일 발송 기능 개발기: Logic 에러부터 HTML 테이블까지 (feat. 회사 API) (0) | 2026.02.14 |
| [Mendix] 배포 실패! ConnectionBusException과 DB 동기화의 악몽 (feat. Unique Constraint) (0) | 2026.02.14 |
| [Mendix] Spotfire 연동 2탄: 배치 스케줄링 & 데이터 중복 방지 (feat. Full Refresh) (0) | 2026.02.14 |