출력값이 정규 표현식(regex)과 일치하자 AI 빌더가 "완료"라고 말했다
요약
AI 빌더 서비스 Zugo가 생성된 코드의 성공 여부를 판단할 때 겪은 시행착오와 해결 방법을 다룹니다. 단순 정규 표현식 검증의 한계를 극복하기 위해 샌드박스 환경 내 스크립트 주입과 에러 리스너를 활용한 검증 시스템 구축 과정을 설명합니다.
핵심 포인트
- 단순한 태그 존재 여부(Regex)만으로는 코드의 실제 작동 여부를 보장할 수 없음
- 샌드박스 환경의 보안 제약으로 인해 호스트가 내부 에러를 직접 감지하기 어려움
- 에러 보고 스크립트를 <head> 최상단에 주입하여 초기 로드 에러를 포착해야 함
- React 경고와 실제 런타임 에러를 구분하여 검증의 정확도를 높여야 함
우리는 단 한 줄의 문장을 실행 가능한 게임, 사이트 또는 앱으로 변환하는 Zugo를 만들고 있습니다. 한동안 빌드가 성공했는지 여부를 결정하는 기준은 다음과 같았습니다:
function htmlComplete(text) {
return /<\/html>/i.test(text);
}
닫는 태그(closing tag)였습니다. 그것이 관문이었습니다. 만약 모델이 </html>을 생성하면, 해당 턴은 성공한 것으로 간주되어 초록색 체크 표시가 나타나고 크레딧이 차감되었습니다. 페이지가 실제로 열리는지는 결정 과정의 일부가 아니었는데, 왜냐하면 우리가 비용을 청구하는 시점에는 문서가 어디에도 마운트(mount)되지 않았기 때문입니다.
코드를 생성하는 무언가를 만들고 있다면, 이것이 바로 함정입니다. 파일을 쓰는 것은 관찰하기 쉽습니다. 하지만 파일이 제대로 작동하는지는 관찰하기 어렵기 때문에, "완료(done)\
미리보기 프레임은 allow-same-origin이 없는 sandbox="allow-scripts" 상태입니다. 이는 불투명한 출처(opaque origin)를 부여하며, 이것이 바로 핵심입니다. 생성된 문서는 호스트 앱, 해당 앱의 저장소 또는 세션에 접근할 수 없어야 합니다. 하지만 동일한 장벽 때문에 호스트 역시 내부를 들여다볼 수 없습니다. 미리보기 주변에 설정된 React 에러 경계(error boundary)는 문서 내부의 그 어떤 것도 잡아내지 못합니다. 만약 사용자가 우리의 에러 경계를 보게 된다면, 그것은 사용자의 빌드가 아니라 **에디터(editor)**가 충돌한 것입니다.
따라서 리스너(listener)는 프레임 내부에 존재해야 하며, 이는 우리가 이를 주입(inject)해야 함을 의미합니다.
하네스(harness), 그리고 이를 작동하게 만드는 두 가지 세부 사항
우리는 생성된 문서의 맨 앞에 작은 스크립트를 추가하여 error, unhandledrejection, 그리고 console.error를 postMessage를 통해 호스트로 보고합니다. 두 가지 세부 사항이 이 방식의 가치를 결정합니다.
스크립트는 반드시 빌드가 작성한 모든 것보다 앞선 <head> 안에 있어야 합니다. 우리의 첫 번째 버전은 훅(hook)을 </body> 앞에 추가했습니다. 로드 중에 예외를 던지는 스크립트는 리스너가 존재하기 전에 실행되었고, 따라서 가장 심각한 실패들이 정작 아무것도 보고하지 못하는 상황이 발생했습니다. 이제 이 순서는 훅의 인덱스가 </head> 및 빌드가 바디(body)에 배치한 모든 스크립트보다 낮음을 확인하는 테스트에 의해 고정되었습니다.
console.error는 실패가 아닙니다. React는 일반적인 개발 경고(development warnings)를 console.error를 통해 기록하며, "리스트의 각 자식은 고유한 키를 가져야 합니다(each child in a list should have a unique key)"라는 메시지는 앱이 고장 난 것이 아닙니다. 따라서 하네스는 이를 별도의 필드로 보고합니다:
var ce = console.error;
console.error = function () {
rep({ logErr: [].map.call(arguments, s).join(" ").slice(0, 400) });
...
통과(pass) 또는 실패(fail)를 결정하는 모든 하위 프로세스는 무언가를 카운트하기 전에 if (d.logErr) return;을 확인합니다. 경고는 여전히 기록되지만, 정상적인 빌드를 실패하게 만들지는 않습니다. 이 두 가지를 하나의 채널로 합쳐버리는 것이 바로 사람들이 읽기를 포기할 때까지 양치기 소년처럼 울부짖는 검증기(verifier)를 만드는 방식입니다.
판정(verdict)은 나빠질 수는 있어도, 좋아질 수는 없다
마지막 규칙은 만약 다른 모든 것을 버려야 한다면 제가 끝까지 유지할 규칙입니다. 판정은 오직 한 방향으로만 전달됩니다:
if (tn.files !== vFiles || (tn.rendered && (!tn.rendered.ok || v.ok))) return tn;
한 번 빌드(build)가 '나쁨(bad)'으로 표시되면, 이후의 어떤 신호도 이를 '좋음(good)'으로 표시할 수 없습니다. 두 번째 렌더링(render), 재시도(retry), 재마운트(re-mount), 혹은 반복되지 않는 일시적인 오류 등 그 무엇도 실패를 다시 성공으로 승격시킬 수 없습니다. 마운트(mount) 후 오류가 여전히 유효한 9초의 시간 창(window)과 결합하여, 이 규칙은 불안정한 빌드(flaky build)가 결국 깨끗한 상태를 보고하고 사용자에게 모든 것이 괜찮다고 알려지는 명백한 탈출구(escape hatch)를 차단합니다.
이제 초록색 체크 표시가 기다립니다. "스트림이 종료됨(the stream finished)"과 "프레임이 응답함(the frame answered)" 사이에서 UI는 빌드가 열리는지 확인 중이라고 표시합니다. 이는 확언이 아닌 중립적인 상태입니다. 우리는 잘못된 것으로 판명될 확신을 보여주느니, 차라리 3초 동안 불확실성을 보여주는 쪽을 택하겠습니다.
일반적인 형태
버그는 정규 표현식(regex)이 아니었습니다. 정규 표현식은 제 역할을 잘 수행했습니다. 버그는 값싼 관찰 가능 대상(cheap observable)이 값비싼 관찰 가능 대상(expensive one)을 대신하게 둔 것이었습니다. 값비싼 대상은 늦게 도착하고, 값싼 대상은 지금 바로 사용할 수 있기 때문입니다.
여러분의 파이프라인(pipeline)에 대해 던져볼 만한 세 가지 질문입니다:
- 여러분의 성공 신호(success signal)는 실제로 무엇을 관찰합니까? 이름이 무엇인지가 아니라 말입니다.
- 실제 답변이 도착하기 전에 결제(billing)와 같이 되돌릴 수 없는 일이 발생합니까?
- 나중에 발생하는 더 조용한 신호에 의해 실패가 다시 성공으로 승격될 수 있습니까?
하네스(harness)가 포함된 버전을 확인하고 싶다면, Zugo를 무료로 체험해 볼 수 있으며, 템플릿(templates)은 모두 직접 열고 편집할 수 있는 실제 빌드들입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기