HTML에서 Markdown으로 변환하는 과정에서 줄 바꿈이 문단 구조를 깨뜨리는 문제
요약
HTML에서 Markdown으로 변환할 때 줄 바꿈 처리 방식 때문에 RAG 파이프라인의 검색 결과가 문장 조각들로 분리되는 문제가 발생했습니다. 이 문제는 특히 청킹(chunking) 과정에서 심각했으며, 공백 의미론을 인지하여 토큰-AST 레벨에서 수정하는 방식으로 해결할 수 있었습니다.
핵심 포인트
- HTML-Markdown 변환 시 줄 바꿈 처리의 어려움이 핵심 문제였습니다.
- 청킹기(Chunker)가 줄 바꿈 기준으로 분할하면서 문장이 잘려나갔습니다.
- 공백 의미론을 인라인 토큰 레벨에서 제어하는 것이 해결책이었습니다.
- 수정 후, 검색 결과 조각화 문제가 크게 개선되었으며, 새로운 회귀 테스트 케이스로 활용됩니다.
HTML에서 Markdown으로 변환하는 API를 구축하던 중, 겉보기에는 무해해 보이는 버그에 직면했습니다. 저는 40페이지 분량의 프론트엔드 튜토리얼을 자체 RAG 테스트 파이프라인에 넣었는데, 검색 결과가 계속 문장 조각들만 반환하는 문제가 발생했습니다. 처음 보기에는 괜찮아 보였지만, 브라우저와 비교(diff)해 보니 달랐습니다.
원본 HTML은 사람이 읽기 좋게 포맷되어 있었습니다:
A component is a reusable piece of UI.
제 변환기는 줄 바꿈을 그대로 유지하여, 어떤 HTML 전문가도 올바르다고 할 만한 Markdown을 생성했습니다. 하지만 Markdown은 HTML이 아닙니다. CommonMark는 단일 줄 바꿈을 소프트 브레이크(soft break)로 취급하지만, 많은 GFM(GitHub Flavored Markdown) 기반 렌더러, 채팅 클라이언트, 텍스트 추출기는 이를 하드 브레이크(hard break)로 처리합니다. 저에게 더 심각했던 점은, 제 청킹기(chunker)가 줄 바꿈을 기준으로 분할한다는 것이었습니다. 그래서 해당 문단이 두 개의 청크로 나뉘게 되었습니다. 'A component is a reusable'는 하나의 검색 버킷에 들어가고, 'piece of UI.'는 다른 버킷에 들어갔습니다. 문장이 생각 중간에 잘려나가 개별적인 완전한 사실처럼 순위가 매겨졌습니다.
HTML 사양은 블록 요소 사이의 모든 공백 연속이 단일 공백으로 축소된다고 명시합니다. 브라우저는 이 기능을 90년대부터 수행해 왔지만, 제 토큰-AST(Abstract Syntax Tree) 패스는 그렇지 못했습니다. 해결책은 끝에서 전역적인 리플로우(global reflow)를 하는 것이 아니었습니다. 그것은 <pre> 블록을 망가뜨리기 때문입니다. 대신, 블록 구조가 결정되기 전에 인라인 토큰 레벨에서 공백을 축소하는 방식이 필요했고, 오직 문단(paragraphs), 목록 항목(list items), 그리고 제목(headings) 내부에서만 적용되어야 했습니다. 코드 블록의 내용은 도착한 바이트를 그대로 유지합니다.
수정 후 측정 결과: 해당 튜토리얼 코퍼스 내 문단의 약 35%가 중간에 소스 줄 바꿈을 포함하고 있었으며, 제 평가 세트(eval set)에서 조각이 최상위 검색 결과로 나오는 경우는 0으로 떨어졌습니다. 다만 한 가지 예외 케이스—줄 단위의 서식이 중요한 정의 목록(definition lists)—는 약간 더 나빠지긴 했지만, 저는 이를 숨기기보다는 알려진 트레이드오프(tradeoff)로 문서화하고 있습니다.
강의: 두 형식을 연결할 때는 구문(syntax)뿐만 아니라 공백 의미론(whitespace semantics)을 매핑해야 합니다. 저는 수정된 파이프라인을 https://x402.freeq.one/tools/html_to_markdown.html으로 패키징했고, 이 튜토리얼 코퍼스는 이제 토크나이저 변경 후 제가 실행하는 첫 번째 회귀 테스트가 되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기