WebSquare 그리드·DataCollection·Submission을 React에서 호환 래퍼로 살리기
변환된 React 화면이 "돌아갈 곳"이 필요하다. WebSquare 화면은 프레임워크가 주는 공통 기능 — dataMap/dataList, submission, $w·com 유틸, WFrame 셸 — 에 기대어 동작한다. 이 대응물이 React 쪽에 없으면 아무리 잘 변환해도 실행이 안 된다. 이 글은 그 대응물, 호환 런타임의 설계 결정들이다.
5계층, 그리고 "범위는 화면이 정한다"
| # | 계층 | WebSquare | React 쪽 |
|---|---|---|---|
| 1 | 데이터 | dataMap / dataList | 같은 메서드 시그니처의 스토어(WsDataMap, WsDataList) |
| 2 | 통신 | submission | 기존 서버 규약을 감싸는 러너(SubmissionRunner + 어댑터) |
| 3 | 컴포넌트 | 그리드·트리·캘린더·탭·팝업·파일첨부 | 호환 래퍼(외부 API는 WebSquare 스타일, 내부는 React) |
| 4 | 유틸 | $w, com.* 공통 스크립트 | 화면이 실제 호출하는 함수만 이식 |
| 5 | 셸 | WFrame·메뉴·레이아웃 | Vite SPA 앱 셸 |
가장 중요한 결정은 이 표의 완성을 목표로 삼지 않은 것이다. WebSquare API 전체를 덮으려 하면 끝이 없고, 그것이 옛 프로젝트가 멈춘 원인 중 하나였다. 대신 워킹 스켈레톤으로 갔다 — 대표 화면 하나가 실제로 쓰는 API만 만들고, 화면을 추가할 때마다 파서가 뽑은 의존성 목록에서 없는 것만 채운다. "미리 만들어두면 좋을 것 같은" 기능은 넣지 않는다.
수렴은 생각보다 빨랐다. 유형 사다리(조회/등록 → 팝업 → 탭/멀티그리드 → 트리 → 복잡 이벤트)를 한 번 오르고 나니, 같은 유형의 두 번째 화면 세트에서 신설된 런타임은 createAliasDataMap 하나였고, 그 다음 갭 0 화면 4개 연속에서는 신설이 0이었다. 남은 것은 새 계층이 아니라 기존 API 표면의 확장과 계약 완화(null·숫자 허용 등)뿐이다.
데이터 계층: 시그니처를 지키는 이유
WsDataList는 getRowCount, getCellData, setCellData, insertRow, deleteRow, getMatchedIndex, getMatchedJSON, rowStatus(C/U/R), getAllXML 같은 메서드를 WebSquare 이름 그대로 갖는다. React답게 다시 설계하고 싶은 유혹이 크지만, 시그니처를 지키면 두 가지가 좋아진다.
- LLM이 스크립트 로직을 옮길 때 "번역"이 아니라 "타입 붙이기"가 된다.
dlTrvMenu.getMatchedJSON("value", value, true)는 그대로dlTrvMenu.getMatchedJSON('value', value, true)다. 재시도 횟수가 줄고 편차가 준다. - 사람이 검토할 때 원본과 나란히 읽힌다.
다만 엔진의 내부 동작 중 검증 없이 흉내내면 안 되는 것이 있다. 예를 들어 setJSON이 onmodelchange를 통지하는지는 문서만으로 확정할 수 없어서 운영 캡처로 실측한 뒤에야("통지하지 않는다") 구현을 확정했다. 이런 추정 항목은 코드와 문서에 [추정]으로 표시하고, 캡처로 확정될 때마다 지운다.
통신 계층: 백엔드는 한 줄도 안 바꾼다
이 프로젝트의 가장 강한 영업 문장은 "백엔드는 손대지 않습니다"다. 기술적으로 그것을 보장하는 것이 통신 계층이다. SubmissionRunner는 화면이 등록한 submission의 ref(요청 컬렉션) → action(URL) → target(응답 컬렉션) 흐름을 그대로 수행하되, 요청 래핑과 응답 봉투는 어댑터가 프로젝트별로 맞춘다.
두 시스템의 규약은 실제로 전혀 달랐다.
- 시스템 A: 요청
{ dma_inputMap: {...} }래핑, 응답{ id, action }규약, 저장 응답은 빈 객체. - 시스템 B:
com.execute(sbmId, url, [{id,key}], target)인라인 호출, 요청{ <key>: {...}, CommonData: { appCd, mnuNo, … } }+?locale=ko, 응답{ dl_x: [...], rsMsg: { statusCode: 'S', … } }.
두 규약 모두 코어가 아니라 각 프로젝트의 어댑터에 산다. 그리고 두 규약 모두 운영 서버를 무인 캡처(Playwright로 로그인 → 메뉴 → 화면 → 조회, 조회 전용)해서 확정했다. 캡처된 요청/응답은 픽스처가 되어 검증 루프의 재생 어댑터가 화면의 요청을 규약과 대조한다 — 규약을 어기면 400이고, 모델은 그 에러를 보고 고친다.
컴포넌트 계층: 1:1 매핑 포기
그리드는 호환 래퍼 WsGrid다. 다행 헤더, 체크박스 열, displayFormat, 더블클릭, 커스텀 컨텍스트 메뉴, 편집 셀, 수정 상태 트래킹, 핸들 API 8종을 WebSquare 쪽 이름으로 제공하고, 내부는 순수 React다(내부 그리드 라이브러리는 교체 가능한 구조). 트리(WsTreeView), 캘린더(WsInputCalendar), 탭(WsTabControl + 화면 스코프 레지스트리 — getFrame/$p.parent와 부모 컬렉션 공유 aliasDataMap 대응), 팝업(popupManager — 콜백형과 Promise형 둘 다), 파일첨부(원본 UDC를 이식한 ComMultiAttach)도 같은 원칙이다.
미지원은 숨기지 않는다. 헤더 병합, 헤더 체크박스, 엑셀 다운로드, 리포트 뷰어, MDI 창 같은 항목은 스텁이거나 TODO(manual)로 원본을 주석 보존한다. 원본의 데드코드·jQuery 직접 조작·엔진 내부 전역은 억지로 구현하지 않는다 — 억지 구현은 검증을 통과하는 것처럼 보이면서 동작을 바꾼다.
새 프로젝트를 붙일 때
두 번째 시스템을 손으로 붙이면서 가장 큰 비용이 엔진이 아니라 "공통 스크립트 9,000줄을 읽고 어댑터를 쓰는 사람의 시간"이라는 것을 알았다. 그래서 그 과정을 도구화했다.
bootstrap— 화면 XML과 공통 스크립트에서 화면이 실제 호출하는 표면(경로·호출 수·원본 정의 위치·시그니처), 규약(접두·버튼 태그·동적 통신 래퍼의 인자 위치), 갭(미지원 태그·UDC·정의 없는 네임스페이스)을 결정론적으로 뽑아 리포트와 골격을 만든다.port— 그 표면을 LLM에 넣어 TypeScript 어댑터 모듈을 생성하고 타입검사로 검증한다. 서버 규약은[추정]으로 표시된다.- 캡처 —
[추정]을 실측으로 바꾼다.
두 번째 시스템에 이 도구를 되돌려 적용했을 때 손으로 만든 어댑터와 같은 표면이 나왔다. 세 번째 시스템은 이 순서로 붙이게 된다.
정리
호환 런타임의 원칙은 세 줄이다. 시그니처를 지킨다, 범위는 화면이 정한다, 프로젝트 편차는 코어에 넣지 않는다. 이 세 줄이 LLM의 재시도를 줄이고, 회귀를 가능하게 하고, 다음 프로젝트에서 깨지지 않게 한다.
시리즈: 1편 — 규칙 기반 컨버터는 왜 80%에서 멈추는가 · 2편 — 화면 64개를 자동 변환하며 배운 것
ws