AAoM-02: W3C 호환 XML 파서
Source: Dev.to
스킬
나는 아직도 MoonBit 시스템 프롬프트와 IDE 스킬을 사용하여 Claude Code (Opus 4.5)를 사용하고 있습니다.
게다가 MoonBit 언어에 대한 모범 사례와 일반적인 함정을 AI에게 알려주기 위해 moonbit-lang이라는 새로운 스킬을 만들었습니다. 헤더는 다음과 같습니다:
---
name: moonbit-lang
description: "MoonBit language reference and coding conventions. Use when writing MoonBit code, asking about syntax, or encountering MoonBit-specific errors. Covers error handling, FFI, async, and common pitfalls."
---
# MoonBit Language Reference
@reference/fundamentals.md
@reference/error-handling.md
@reference/ffi.md
@reference/async-experimental.md
@reference/package.md
@reference/toml-parser-parser.mbt
이 스킬 문서에서는 AI가 익숙하지 않은 공식 파일‑I/O 패키지 moonbitlang/x/fs도 언급하고 있습니다.
전체 스킬 문서와 참고 자료는 GitHub에서 확인할 수 있으며, 나는 사용 중인 스킬들을 지속적으로 업데이트하고 있습니다.
AI(Codex와 Claude 모두)는 시작 시 설명만 읽고 나머지는 필요할 때 불러옵니다. 나는 스킬 문서를 간단하게 유지하는데, 이는 지나치게 긴 문서가 AI가 세부 사항을 이해하는 데 방해가 된다는 내 경험에 기반합니다.
Problem
XML은 구성 파일, 데이터 교환, 레거시 시스템에서 여전히 널리 사용됩니다. 규격에 맞는 XML 파서는 다음을 처리해야 합니다:
- 요소 태그, 속성, 네임스페이스
- 엔티티 참조
아래는 최소 문서를 파싱하고 결과 이벤트 스트림을 검사하는 간단한 테스트입니다:
let xml = "\n\n\n"
let reader = Reader::from_string(xml)
let events : Array[Event] = []
for {
match reader.read_event() {
Eof => {
events.push(Eof)
break
}
event => events.push(event)
}
}
inspect(
to_libxml_format(events),
content="[DocType(\"doc\"), Empty({name: \"doc\", attributes: []}), Eof]",
)
잘못된 형식의 예시:
test "w3c/not-wf/not_wf_sa_001" {
// Attribute values must start with attribute names, not "?".
let xml = "\n\n\n"
let reader = Reader::from_string(xml)
let has_error = for {
try reader.read_event() catch {
_ => break true
} noraise {
Eof => break false
_ => continue
}
}
inspect(has_error, content="true")
}
총 735개의 테스트가 생성되었으며, 약 14 k줄의 코드로 구성됩니다. 몇 개의 수동 테스트를 추가한 후, 현재 스위트에는 800개의 테스트가 포함되어 있습니다.
파서 구현
quick‑xml이 초기 레퍼런스였기 때문에 Claude는 이를 기반으로 한 풀‑파서 아키텍처를 따랐으며, 이는 우리의 목표에 적합하다고 생각했습니다. API는 다음과 같습니다:
let reader = @xml.Reader::from_string(xml)
for {
match reader.read_event() {
Eof => break
Start(elem) => println("Start: \{elem.name}")
End(name) => println("End: \{name}")
Text(content) => println("Text: \{content}")
_ => continue
}
}
lxml이 트리를 반환하는 반면 우리 파서는 이벤트를 발생시키므로, Claude에게 이벤트 스트림을 lxml이 생성하는 정확한 형식으로 변환하는 to_libxml_format 함수를 구현하도록 요청했습니다. 이를 통해 테스트 비교가 간단해졌습니다.
기본 구현은 AI‑전용 작업만으로 약 4시간 정도 걸렸으며(가끔 “계속 진행해 주세요” 프롬프트 제외), 가장 복잡한 기능은 DTD 파싱 및 검증이었습니다. 구현을 구조화하기 위해 Claude의 플랜 모드를 사용했습니다. 아래는 그 플랜의 요약입니다:

프로젝트 요약

약 1 시간 정도 후에 DTD 지원이 구현되었고 726개의 테스트가 통과했습니다.
그 후 3 시간이 더 걸려 다음과 같은 엣지 케이스들을 처리했습니다:
- 엔티티 값 확장
- 텍스트‑분할 세부 사항
- UTF‑8 BOM 처리
결과
노력의 끝에서 800개의 W3C 호환성 테스트가 통과되었습니다.
- 59개의 테스트가
tests‑gen스크립트에 의해 건너뛰어졌습니다 because:- 일부는 유효했지만 lxml에 의해 거부되었습니다.
- 다른 일부는 형식이 올바르지 않았지만 lxml에 의해 통과되었습니다.
이것들은 “lxml 구현 특이점”으로 표시되었습니다.
가장자리 경우가 지나치게 복잡했기 때문에 각각을 자세히 검증하지는 않았지만, 남은 800개의 테스트는 충분한 신뢰를 제공했습니다.
지원되는 기능
- XML 1.0 + Namespaces 1.0
- 메모리 효율적인 스트리밍을 위한 Pull‑parser API
- XML 생성을 위한 Writer API
- 엔터티 확장을 포함한 DTD 지원
회고
잘된 점?
- 공식 테스트 스위트 사용 – W3C 호환성 테스트를 통해 문자 참조, DTD 특이점, 네임스페이스 처리 등과 같은 드물게 발생하는 엣지 케이스를 발견했으며, 이는 수동으로 테스트할 생각조차 못했을 것이다.
- 레퍼런스 구현 전환 –
quick‑xml은 의도적으로 관대하게 동작해 호환성 테스트가 어려웠다. libxml2로 전환하면서 엄격한 레퍼런스를 얻을 수 있었다. - 복잡한 기능을 위한 계획 모드 – DTD 파싱을 계획으로 나누어 작업을 체계화했으며, 그렇지 않으면 관련 없는 버그 사이를 오가며 작업했을 것이다.
겪은 어려움
Claude가 파서를 고치는 대신 테스트를 수정하려고 자주 시도했다:
- 잘못된 출력에 맞추기 위해 테스트 기대값을 변경함.
- 실패하는 테스트를 건너뛰도록 테스트 생성기를 업데이트함.
- 테스트를 “관대함”으로 표시하고 건너뛰게 함.
나는 Claude에게 계속해서 “테스트가 아니라 MoonBit 구현을 업데이트해라.” 라고 상기시켜야 했다.
다른 반복적인 문제들:
- 프로젝트 규칙을 잊어버림 (예: 탐색에
moon‑ide스킬을 사용하지 않거나,try/catch/noraise대신match (try? expr)사용). - 이러한 규칙을
CLAUDE.md에 추가했지만 문제를 완전히 해결하지는 못했다.
레딧에서 관련 논의를 찾았다 (link). 여기서는 Opus 4.5와 Sonnet 4.5에 버그가 있다고 제시하고 있다. 곧 수정되길 바란다.
향후 작업
앞으로 더 많은 파서를 구현하거나 포팅해야 할 것으로 예상한다. 파서를 작성하고 표준 기반 테스트 스크립트를 생성한 경험을 재사용 가능한 스킬이나 명령으로 전환하여 다음 프로젝트가 이 기반 위에서 혜택을 받을 수 있도록 할 계획이다.
시간 투자 (≈ 10 시간)
| 활동 | 시간 |
|---|---|
| 테스트 생성 스크립트의 협업 탐색 | 2 |
| 기본 기능의 자율 구현 | 4 |
| DTD, 네임스페이스, 엔터티 계획 및 구현 | 1 |
| 에지 케이스 처리 (테스트 실패 17건 수정) | 3 |
코드는 GitHub에 있습니다: