turndown은 메타데이터 스냅샷 시점에 테스트한 네 가지 라이브러리 중 가장 많은 GitHub 별점을 보유한 도구였으며, 11,386개의 별을 기록했다. 하지만 네 개의 공통 HTML 샘플에서는 기본 설정만으로 Markdown 테이블을 하나도 생성하지 못했고, 같은 입력 기준으로 markdownify보다 토큰을 24.6% 더 많이 사용했다.
네 도구 모두 네 개의 콘텐츠 검증 문자열은 전부 복원했다. 차이는 전적으로 구조를 어떻게 다루는지, 그리고 그 구조가 이후 처리 과정에서 어떤 비용을 만드는지에 있다.
무엇을, 어떤 샘플로 측정했나
이 연구에는 이미 markitdown을 위한 테스트 묶음이 있었고, HTML 샘플 5개와 사전 등록된 검증 문자열이 포함돼 있었다. 정확히 살아남아야 하는 문자열은 물론, 페이지의 불필요한 장식 요소를 잡아내기 위한 보일러플레이트 문자열도 함께 준비돼 있었다. 이번 통합 비교는 네 도구가 모두 공통으로 가진 네 개의 샘플만 사용했다. 즉, 서점 카탈로그, 인용문 사이트, 하키 통계 표, 그리고 위키피디아의 웹 스크래핑 문서다. 다섯 번째 샘플인 Nothing but tables는 표 중심 진단용으로만 쓰였고, 24.6% 전체 집계에서는 제외했다.
비교 대상 네 도구는 다음과 같다: turndown 7.2.4(Node), markdownify 1.2.3, html2text 2025.4.15, 그리고 공개된 결과를 그대로 사용한 markitdown. 대상 페이지는 서점 카탈로그, 인용문 사이트, 하키 통계 표, 위키피디아의 웹 스크래핑 문서다.
아래의 모든 지표명은 markitdown의 산출물 필드명을 그대로 따른다. 그래서 같은 의미를 서로 다른 정의로 억지로 맞출 필요 없이, 각 행을 바로 나란히 놓을 수 있다.
표

| Converter | Body probes | Output chars | Tokens (o200k) | Bytes/token | Markdown table rows | Links |
|---|---|---|---|---|---|---|
| turndown | 16/16 | 95,188 | 26,236 | 3.63 | 0 | 611 |
| markdownify | 16/16 | 76,868 | 21,062 | 3.65 | 36 | 599 |
| html2text | 16/16 | 76,452 | 21,176 | 3.61 | 32 | 545 |
| markitdown | 16/16 | 76,995 | 21,336 | 3.61 | 36 | 598 |
네 개의 샘플 기준이며, markitdown도 동일하게 이 네 개만 실행했다. 샘플별 전체 수치는 fiveway-scores.json에서 확인할 수 있다. 토큰 수는 o200k_base 기준으로 셌고, markitdown의 표 행 수는 다른 도구와 같은 카운터로 저장된 Markdown을 다시 계산했다.
콘텐츠 보존은 무승부다. 네 개의 샘플에 걸친 16개의 본문 검증 문자열은 모든 변환기에서 살아남았다. 즉, “텍스트가 통과하느냐”만 묻는다면 이 네 도구 모두 예라고 답한다.
구조는 무승부가 아니다. 네 개의 공통 샘플에서 markdownify와 markitdown은 각각 36개의 Markdown 표 행을 만들었고, html2text는 32개, turndown은 0개였다. 별도로 실행한 표 전용 진단 샘플에서는 markdownify가 62개, html2text가 59개를 생성했다. 다만 그 수치는 위의 전체 집계에는 포함되지 않았다.
turndown의 토큰 비용은 24.6% 더 높다. 그리고 그 이유는 표가 아니다. 처음에는 표가 원인이라고 생각했지만, 샘플별 수치를 보면 그렇지 않다. 아래에서 확인할 수 있다.
turndown이 표를 어떻게 바꾸는가
하키 통계 샘플을 같은 행 구성으로 세 가지 방식으로 보면 다음과 같다.
turndown:

Team Name
Year
Wins
Losses
Boston Bruins
1990
44
24
markdownify:
| Team Name | Year | Wins | Losses | ... |
| --- | --- | --- | --- | --- |
| Boston Bruins | 1990 | 44 | 24 | ... |
html2text:
Team Name | Year | Wins | Losses | ...
---|---|---|---|---
Boston Bruins | 1990 | 44 | 24 | ...
모든 값은 turndown 변환 결과에서도 사라지지 않는다. 그래서 이 도구가 검증 문자열 16개 중 16개를 통과한다. 하지만 사라지는 것은 각 값이 어느 열에 속하는지라는 관계다. turndown 출력만 보면 44는 그냥 한 줄의 숫자일 뿐이며, 빈 셀이 하나도 없다고 가정해 위치를 세어도 Boston의 승수를 복원할 수 없다. 그런데 이 샘플에는 빈 셀도 있어서, 위치를 세는 방식조차 통하지 않는다.
모델이 이 출력을 읽는다면, 그 차이는 질문에 답할 수 있는 표와 그저 숫자만 나열된 목록 사이의 차이다.
이건 결함이라기보다 문서화된 한계에 가깝다. turndown의 코어는 표를 처리하지 않으며, 이를 보완하기 위한 turndown-plugin-gfm이 따로 존재한다. 하지만 기본 설치에는 이 플러그인이 포함되지 않고, 11,386개의 별점이 말해주듯 많은 사용자가 기본 상태로 쓰고 있을 가능성이 높다.
토큰 차이가 실제로 생기는 이유
위 문단을 쓸 때는 24.6% 토큰 차이가 평면화된 표가 문자 수를 늘려서 생긴 것이라고 생각했다. 그런데 샘플별로 뜯어보니 그게 아니었다.
| Fixture | turndown tokens ÷ markdownify tokens |
|---|---|
| Quotes site (no tables) | 0.99× |
| Bookshop catalogue | 1.06× |
| Hockey statistics (one big table) | 1.37× |
| Wikipedia (mostly prose, 9 table rows) | 1.29× |
| Nothing but tables | 0.78× |
표만 있는 샘플에서는 turndown이 22% 더 저렴했다. 파이프(|) 기반의 표 틀도 토큰을 차지하는데, turndown은 그 틀을 아예 만들지 않기 때문이다. 즉, 표를 평면화하는 것 자체만으로 토큰 페널티가 생기는 것은 아니다.
전체의 74%를 차지하는 위키피디아 샘플에는 표 행이 9개뿐이다. 그런데 문자 수 차이가 15,378자나 된다면, 원인은 표가 아니다. 진짜 원인은 이 부분이다:
(function(){var className="client-js vector-feature-language-in-header-enabled…
.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}…
(RLQ=window.RLQ||[]).push(function(){mw.config.set({"wgHostname":"mw-web…
turndown은 <script>와 <style> 안의 내용을 제거하지 않는다. markdownify와 html2text는 둘 다 제거한다. 이 요소 안에만 존재하는 표시 문자열을 기준으로 세어보면, turndown 출력에는 4개 샘플 전체에 걸쳐 script 마커 10개와 style 마커 84개가 들어 있다. 반면 다른 두 도구는 둘 다 0개다. 위키피디아 페이지에서는 MediaWiki의 인라인 JavaScript 설정과 CSS가 8줄에 걸쳐 14,644자, 즉 전체 차이의 **95%**를 차지한다(script-style-stripping.json).
이게 실제로 주목해야 할 결과다. 평평해진 표는 눈에 보이는 구조 문제다. 하지만 Markdown 안에 JavaScript 설정 덩어리가 그대로 들어가는 건 정보는 없고 비용만 있는 순수 손실이다. 그리고 실제 웹페이지에서는 이 비용이 이번 비교의 다른 어떤 요소보다도 훨씬 크다.
html2text는 알아보지 못할 만큼 다른 방식으로 표를 쓴다
html2text는 markdownify와 markitdown이 36행을 기록한 샘플에서 32행을 기록했고, 내 첫 번째 카운터 버전에서는 1행으로 계산됐다.
문제는 라이브러리가 아니라 내 계산 규칙이었다. html2text는 Team Name | Year | Wins처럼 앞뒤 파이프가 없는 형태로 표를 출력하는데, 이것은 흔한 Markdown 표 스타일이지만 ^\|.*\|$ 정규식에는 잡히지 않는다. 나는 그 정규식을 직접 써서 돌린 뒤, html2text는 표를 지원하지 않는다고 보고할 뻔했다.
하지만 지원한다. 수정한 카운터는 파이프가 들어간 줄이 연속으로 이어지고 그 안에 구분 행이 있는지를 보는 휴리스틱을 사용한다. 그 규칙을 적용하면 html2text는 1행에서 32행으로 바뀐다. 물론 이것은 완전한 Markdown 파서가 아니므로, 이 표의 행 수는 보편적 렌더링 결과가 아니라 문서화된 카운터를 기준으로 측정한 값으로 읽어야 한다.
Markdown을 이후에 정규식으로 다시 처리하는 사람이라면 기억할 점이 있다. 이 네 도구 중 두 개는 바깥 파이프를 쓰고, 하나는 쓰지 않는다.
아무도 잘 말하지 않는 라이선스
| Converter | Licence | Install | Cold import | Stars | Last release |
|---|---|---|---|---|---|
| turndown | MIT | 3 npm packages, 8.8 MiB | 0.056 s | 11,386 | 2026-04-03 |
| markdownify | MIT | 5 packages, 1.8 MiB | 0.046 s | 2,235 | 2026-06-30 |
| html2text | GPL-3.0-or-later | 1 package, 0.2 MiB | 0.077 s | 2,168 | 2025-04-15 |
| markitdown | See its package metadata | Not measured in this install run | Not measured | — | — |
install-and-import.json. 각 라이브러리는 서로 다른 빈 환경에 설치했다. 라이선스는 레지스트리 메타데이터, GitHub 저장소, 그리고 설치된 패키지의 METADATA 파일까지 세 곳에서 확인했다. 해당 파일에는 License-Expression: GPL-3.0-or-later라고 적혀 있다.
이번 설치 비교에서 가장 가벼운 라이브러리인 1개 패키지, 0.2 MiB짜리는 GPL-3.0-or-later 라이선스다. 이게 프로젝트에 영향을 주는지는 소프트웨어를 어떻게 결합하고 배포하느냐에 따라 달라진다. 배포 책임이 있는 사람이 라이선스를 검토해야 할 선택 지점으로 생각하면 된다. 이 글은 법률 자문이 아니다. markitdown은 이번 측정에서 설치와 라이선스가 수집되지 않았기 때문에 여기서는 미측정으로 표시했다.
벤치마크 표에는 라이선스가 드러나지 않기 때문에, 이런 차이는 놓치기 쉽다.
그리고 셋 모두 같은 범주의 문서 추출 라이브러리들보다 훨씬 가볍다. 그쪽은 21~70 MiB 수준이다. 변환기(converter)는 추출기(extractor)보다 훨씬 작은 도구이므로, 의존성 예산에서 따로 분리해 보는 편이 좋다.
이 표가 맞기 전에 고쳐야 했던 두 가지 혼동 요소
위 숫자들은 세 번째 버전이다. 앞의 두 버전은 틀렸는데, 그 이유가 둘 다 재현하기 쉬운 실수였기 때문에 여기서 짚어둘 만하다.
다섯 개 샘플과 네 개 샘플을 섞어 비교했다. markitdown은 이 5개 파일 중 4개만 실행했고, 나머지 3개 도구는 모두 5개를 실행했다. 각 도구를 자기 샘플 수에 맞춰 합산하니 markdownify는 98개 표 행, markitdown은 36개가 되어 기능 차이처럼 보였다. 하지만 동일한 네 개만 비교하면 36 대 36으로 정확한 동률이다. 다섯 번째 샘플이 바로 표가 가장 많은 샘플이었기 때문에, 이 혼동은 새 도구를 기존 도구보다 거의 세 배나 더 불리하게 만드는 최악의 방향으로 작동했다.

카운터는 두 개인데, 열은 하나였다. markitdown이 공개한 md_table_rows 값은 내가 읽지 않은 자기 코드에서 나온 것이었다. 그 숫자와 내 카운터를 직접 비교하면, 변환기 두 개를 비교한 게 아니라 카운터 두 개를 비교한 셈이 될 수도 있었다. 다행히 Markdown 출력은 디스크에 저장돼 있었고, 해결 방법은 네 개 샘플 전부에 하나의 카운터를 돌리는 것이었다. 그렇게 해보니 markitdown의 재계산 값은 샘플별로 공개 수치와 정확히 일치했다(0, 0, 27, 9). 정의는 같았다. 다만 확인하기 전에는 알 수 없었을 뿐이다.
이 두 오류는 출력만 봐서는 드러나지 않는다. 둘 다 자신 있게 틀린 표를 만들어냈을 것이다.
누구에게 무엇이 맞나
모델에 넣거나, 이런 페이지의 구조화된 내용을 저장하려는 경우? markdownify부터 시작하라. 이 카운터 기준으로 markitdown과 동일한 표 행 수를 내고, 토큰 사용량은 가장 효율적인 그룹 안에 있으며, MIT 라이선스이고, 설치 용량도 1.8 MiB다. 다만 실제로 표준화하기 전에는 본인의 페이지 구조에서 먼저 검증하는 것이 좋다.
의존성 예산을 킬로바이트 단위로 보고 있고, 배포는 하지 않는다면? html2text가 적합하다. 패키지 1개, 0.2 MiB, 표도 유지된다. 다만 먼저 GPL 문제를 확인해야 하고, 마지막 릴리스가 2025년 4월이라는 점도 참고해야 한다.
이미 Node 스택을 쓰고 있다면? turndown에 turndown-plugin-gfm을 함께 설치하고, HTML을 넘기기 전에 <script>와 <style>를 먼저 제거하라. turndown은 그 둘을 스스로 지우지 않기 때문이다. 이 두 가지를 놓치면 토큰은 4분의 1 더 들고, 표 구조는 사라진다. 그리고 출력 결과를 보기 전까지는 그 사실을 알 수 없다.
이미 다른 문서 형식을 변환하고 있다면? markitdown은 PDF, Office 문서 등도 처리하며, HTML 출력도 전용 변환기들과 충분히 경쟁력 있다. 의존성을 하나로 줄일 수 있다는 점은 분명 가치가 있다.
관리형 API는 어디에 들어맞나
이 네 도구는 모두 이미 확보한 HTML을 변환한다. 페이지를 가져오지도 않고, JavaScript를 렌더링하지도 않으며, 안티봇 계층도 다루지 않는다. 그런데 실제 운영 환경에서는 그 절반이 오히려 더 어렵다.
Thunderbit의 개발자 스택은 바로 그 부분을 맡는다. POST /distill은 URL을 받아 렌더링과 가져오기를 처리한 뒤, LLM에 바로 넣을 수 있는 깔끔한 Markdown을 반환한다. POST /extract는 사용자가 제공한 JSON Schema를 기준으로 AI가 맞춘 구조화된 JSON을 돌려주는데, 이건 또 다른 출력 형태다. 즉, 나중에 Markdown 표로 다시 파싱해야 하는 대신 바로 행 단위 데이터를 받는 방식이다. 둘 다 MCP 서버와 CLI(npx @thunderbit/thunderbit-cli)에서 사용할 수 있다. 요금은 Thunderbit 요금 페이지에서 확인할 수 있다.
솔직하게 비교하면 이렇다. HTML을 이미 가지고 있고 Markdown만 필요하다면, markdownify는 무료이고 충분히 잘 작동한다. 그리고 이 표는 그 경쟁자들 중 누가 같은 일을 잘하는지도 보여준다. 하지만 페이지를 직접 가져와야 하거나, 산문이 아니라 구조화된 행이 필요하다면 그건 완전히 다른 선택이다.
더 넓은 범위에서는, 웹 스크래핑 API 총정리에서 호스티드 옵션을, 오픈소스 스크레이퍼 피라미드에서 자체 호스팅 옵션을 다룬다. Python에서 HTML을 Markdown으로 변환하는 방법은 실전용 가이드이고, llms.txt가 표준화하려는 것은 이 카테고리가 어디로 가는지 설명한다.
웹 데이터 추출을 위해 Thunderbit 사용해 보기
결론
이번 네 샘플 워크로드에서는 markdownify가 가장 무난한 기본값이다. 공통 카운터 기준으로 markitdown과 동일한 표 행 수를 만들고, 토큰 수도 다른 효율적인 변환기들과 1.3% 이내 차이이며, MIT 라이선스에 설치 용량은 1.8 MiB다.
인기와 실제 동작 사이의 차이가 바로 이번 발견이다. turndown은 훌륭한 라이브러리지만, 표 처리를 가능하게 하는 플러그인 없이 설치해 쓰는 사람이 매우 많다. 그 결과는 토큰이 4분의 1 더 들고, 표 구조가 아예 사라지는 형태로 드러난다. 숫자로 세기 전까지는 어느 쪽도 눈에 보이지 않는다.
하나만 기억한다면: 모델이 믿을 출력으로 쓰기 전에, 변환기가 표를 어떻게 바꾸는지 먼저 확인하라. 이 네 도구 중 세 개는 나름 합리적인 처리를 한다. 가장 인기 있는 하나는, 따로 요청하지 않으면 그렇지 않다.
웹 데이터 추출을 위해 Thunderbit 사용해 보기 Get Started Free
자주 묻는 질문
turndown은 정말 테이블을 지원하지 않나요?
코어에는 포함되지 않는다. 테이블 기능은 별도 패키지인 turndown-plugin-gfm에서 제공되며, 일반적인 npm install turndown에는 들어가지 않는다. 플러그인이 없으면 각 셀은 각각의 문단처럼 남을 뿐이다. 값은 모두 살아 있지만, 행과 열의 관계는 사라진다. 네 개 샘플 전체에서 Markdown 표 행 수는 0개였고, 같은 내용 기준으로 markdownify보다 토큰을 24.6% 더 사용했다.
왜 html2text는 처음에 표가 없는 것처럼 보였나요?
내 카운터가 앞뒤 파이프를 요구했는데, html2text는 그 형식으로 출력하지 않기 때문이다. Team Name | Year | Wins도 올바른 Markdown이며 정상적으로 렌더링된다. 다만 | Team Name | Year |와는 다른 스타일일 뿐이다. 수정된 카운터는 파서처럼 파이프가 들어간 줄이 연속으로 이어지고 그 안에 구분 행이 있는지를 찾으며, 그 결과 html2text는 1행에서 32행으로 바뀐다. Markdown을 정규식으로 후처리한다면 이 차이에서 문제가 생긴다.
html2text의 GPL 라이선스는 실제로 문제가 되나요?
소프트웨어를 어떻게 결합하고 배포하느냐에 따라 다르다. GPL-3.0-or-later는 MIT와 달리 의무를 만들 수 있으므로, 배포형 제품에 넣기 전에는 라이선스를 책임지는 사람과 먼저 상의해야 한다. 이건 법률 자문이 아니라 준수 확인용 체크포인트다. 라이선스 정보는 레지스트리 메타데이터, 저장소, 설치된 패키지의 METADATA에서 모두 확인했다.
이 토큰 수치는 다른 페이지에도 의미가 있나요?
24.6% 차이의 대부분은 turndown이 <script>와 <style> 내용을 그대로 남기기 때문에 생긴다. 따라서 현대적인 CMS 페이지처럼 해당 내용이 많은 페이지에서는 차이가 커지고, 정적인 페이지에서는 거의 0에 가깝다. 표는 반대 방향으로 작용한다. 표뿐인 샘플에서는 turndown이 오히려 22% 더 저렴했는데, 파이프 틀을 생성하지 않기 때문이다. 네 도구 모두 bytes/token은 3.61~3.65 사이로 비슷했으므로 출력 밀도는 거의 같고, 차이는 양에 있다. 예산에 중요한 수치라면 본인 코퍼스로 직접 측정하는 것이 맞다.
여기서 테스트하지 않은 것은 무엇인가요?
현실 세계의 다양성이다. 네 샘플은 어디까지나 네 샘플일 뿐이다. 중첩 목록, 정의 목록, 각주, 수식은 포함되지 않았다. 잘못된 HTML은 변환기 차이가 가장 크게 벌어지는 영역인데, 그것도 제외됐다. Markdown을 다시 HTML로 되돌리는 라운드트립도 하지 않았다. docling은 같은 샘플을 디스크에 갖고 있지만, 공개된 실행 결과가 이 필드를 보고하지 않아 측정값 대신 제외했다. 또한 설정 차이도 있다. html2text는 기본값 78이 모든 줄을 강제로 줄바꿈하기 때문에 body_width=0으로 실행했다. 그렇지 않았다면 이 표의 문자 수와 토큰 수가 모두 달라졌을 것이다.


