ws2react 로고ws2react

화면 64개를 자동 변환하며 배운 것 — 검증 루프가 LLM을 믿을 만하게 만드는 방법

WebSquare → React 마이그레이션 2편 · 2026-08-25 · ws2react 팀

앞 글에서 규칙·LLM·검증 루프의 3계층 파이프라인을 설명했다. 이 글은 그 파이프라인에 실제 화면 64개를 넣고 나온 숫자와, 그 숫자를 만든 장치들이다. 숫자만 보면 "LLM이 잘한다"로 읽히기 쉬운데, 실제로는 LLM을 감싼 검증과 격리가 결과를 만들었다.

실측

항목
코퍼스해운 시스템 A 39화면 + 물류 FMS B 25화면 (조회/등록, 팝업, 탭/멀티그리드, 트리, 파일첨부, 대형 화면)
자동 변환64 / 64 — 검증 전 단계 통과, 수작업 코드 수정 0건
시도 횟수평균 1.8회 (1회 통과 약 30%, 2회 약 60%, 3회 이상 약 10%)
소요화면당 2~5분 (검증 루프 포함), 대형 화면 10~20분
최대 화면11,717줄 (XML + 스크립트)
LLM 비용화면당 $0.3~6 (API 정가 환산 — 소형 $0.9, 중형 $1.6, 대형·2회 시도 $6.1)

"1회 통과 30%"가 낮아 보일 수 있다. 우리도 처음엔 "전부 1회 무개입"이라고 기록했는데, 측정 오염이었다. 아래 첫 번째 장치가 그 이야기다.

실패를 막은 다섯 가지 장치

1. 모델에게 도구를 주지 않는다 — 측정 오염의 발견

초기 회귀에서 화면들이 전부 1회에 통과했다. 너무 좋았다. 원인은 LLM 백엔드(Claude Code CLI)가 도구 권한을 갖고 있어서 스스로 파일을 쓰고 typecheck를 돌리며 고치고 있었기 때문이다. 파이프라인의 검증 루프가 아니라 모델이 검증을 하고 있었다. 도구를 전부 끄자(--tools "") 평균 시도가 2회로 올라갔고, 그제서야 "에러 피드백 재시도"가 실제로 필수라는 것이 입증됐다.

이건 지표 문제만이 아니다. 고객 리포지토리에서 모델이 임의로 파일을 수정하는 것은 그 자체로 사고다. 그래서 도구 차단은 절대 규칙이 됐고, 같은 이유로 모델은 리포지토리 밖 임시 디렉터리에서 실행된다 — 그렇지 않으면 리포의 프로젝트 지침 파일을 읽고 변환을 거부하거나 출력 형태가 오염되는 것을 실측했다.

2. 실패한 산출물을 남기지 않는다

3회 한도에서 실패한 화면의 반쪽짜리 TSX가 screens/에 남으면, 이후 모든 화면의 typecheck가 그 파일 때문에 깨진다. 한 화면의 실패가 다른 두 화면을 수작업 큐로 오이관시킨 사고를 겪은 뒤, 실패 시 기존 통과본 복원(신규면 삭제)을 예외 경로까지 포함해 보장했다. 검증 루프의 전제는 "산출물 디렉터리에는 통과본만 있다"이다.

3. 검증은 세 겹이다

검증 실패의 상세(어느 submission이 빠졌는지)가 재시도 프롬프트에 그대로 들어가므로 수렴이 빠르다.

4. 재시도는 회귀와 함께

규칙·프롬프트·런타임을 바꾸면 이미 통과한 화면 전체를 재변환·재검증한다. 새 화면을 통과시키려고 기존 화면을 깨뜨리는 변경은 되돌린다. 코퍼스 자체가 테스트 스위트다. 64화면 회귀는 npm run regress 한 번이다.

5. 대형 화면은 출력 전송 방식이 문제였다

4,000줄이 넘는 화면이 "중간부터 시작하는 응답"으로 실패했다. 모델의 문제가 아니라 CLI의 텍스트 출력 포맷이 마지막 메시지만 돌려주는 것이 원인이었다. 스트리밍 JSON으로 전환해 모든 메시지를 이어 붙이고, 여러 코드펜스에 나뉜 파일을 조립하고, 잘린 꼬리는 이어쓰기로 마무리했다. 그 뒤로 11,717줄 화면까지 통과했고 크기 상한은 사실상 사라졌다.

두 번째 시스템이 가르쳐 준 것

첫 시스템에서 파이프라인을 만들고 두 번째 시스템(물류 FMS)을 넣었을 때 진짜 시험이 됐다. 두 시스템은 공통 프레임을 전혀 공유하지 않았다 — 통신 래퍼가 com.execute(url, ref…)로 인라인 호출이고, 버튼 태그가 다르고, 컬렉션 접두가 다르고, confirm 콜백 형태가 달랐다.

결과: 코어(파서·런타임) 변경은 파서 어휘 3줄과 범용 동적 submission 추출기, 통신 러너 21줄이 전부였다. 프로젝트 지식은 전부 어댑터(projects/<p>/)와 프롬프트 오버레이, 설정 파일로 분리됐고, 첫 시스템의 프롬프트는 바이트 하나 바뀌지 않았다. 두 번째 시스템 25화면도 자동 변환 100%, 평균 1.88회 — 첫 시스템(1.81회)과 같은 수준이었다.

이 분리가 중요한 이유는 제품이냐 일회성 스크립트냐를 가르기 때문이다. 특정 프로젝트 분기가 코어에 들어가는 순간 다음 프로젝트에서 깨진다.

남는 것

100%는 코퍼스 안의 이야기다. 미지원 항목은 목록으로 관리된다 — 엑셀 다운로드, 리포트 뷰어, MDI 창, 그리드 헤더 병합 같은 것들은 스텁이거나 TODO(manual)로 보존된다. 파이프라인은 이것들을 풀려고 확장하지 않는다. 검증을 통과한 것과 사람이 봐야 할 것이 리포트에서 분명히 갈리는 것, 그것이 이 도구가 약속하는 전부다.

다음 글: WebSquare 그리드·DataCollection·Submission을 React에서 호환 래퍼로 살리기