WebKit 대신 TextKit으로 Markdown을 렌더링하는 Quick Look
Rendering Markdown in Quick Look with TextKit instead of WebKit
핵심 요약
- MD QuickLook은 Rust에서 Markdown을 바이너리 레이아웃으로 변환하고 Swift와 TextKit으로 그려, 일반적인 README 미리보기를 약 30ms에 표시한다.
- 구문 강조 문법을 빌드 시 전처리하고 언어별 강조 작업을 병렬화해 초기 컴파일 시간을 줄이며, 긴 문서는 첫 화면을 먼저 그린 뒤 나머지를 백그라운드에서 처리한다.
- GFM, 220개 언어의 구문 강조, 수식과 Mermaid 다이어그램을 지원하며, macOS 12 이상에서 무료로 사용할 수 있는 MIT 라이선스 오픈 소스 앱이다.
도입
macOS Finder에서 Markdown 파일을 훑어볼 때 기본 Quick Look은 원문 텍스트를 보여준다. 기존 대안은 Markdown을 HTML로 바꿔 WebKit에서 표시하는 경우가 많아 웹 뷰 시작과 페이지 로드, JavaScript 실행 시간이 더해진다. MD QuickLook은 Rust에서 Markdown을 레이아웃으로 만들고 Swift와 TextKit으로 그리는 방식을 택했다. 글에 따르면 일반적인 README는 약 30ms 만에 준비되며, 같은 문서를 WebKit으로 표시할 때는 약 200ms가 걸린다.
상세 요약
렌더링 구조
MD QuickLook은 Markdown 파싱과 레이아웃을 Rust에서 수행하고, 결과를 Swift Quick Look 확장으로 전달해 TextKit으로 그린다. Rust는 comrak으로 CommonMark와 GFM을 파싱하고 syntect으로 코드를 강조한다. Mermaid 다이어그램의 레이아웃도 Rust에서 처리한다. 결과는 HTML이 아니라 간결한 바이너리 레이아웃이다.
레이아웃은 일반 텍스트와 세 테이블로 구성된다. 블록 테이블은 인용문, 목록, 코드 블록, 표 등을 부모 관계가 있는 트리로 표현한다. 문단 테이블은 문단 스타일과 가장 안쪽 블록을 가리킨다. 실행 테이블은 굵게, 코드, 링크, 색상 토큰 같은 인라인 스타일이나 이미지·수식·배지·체크박스 같은 첨부 항목을 가리킨다. 범위의 오프셋은 NSRange와 같은 UTF-16 단위다. 첨부 항목은 U+FFFC 문자 하나를 차지한다.
Swift는 Markdown을 파싱하지 않고, Rust는 AppKit을 다루지 않는다. Swift 측은 블록을 NSTextBlock, 문단을 NSParagraphStyle로 변환하고 실행에 해당하는 범위에 속성을 적용한다. 표는 NSTextTable로 표시한다. 이 구조에서 일반적인 README 미리보기는 약 30ms 만에 준비되며, 글에 제시된 WebKit 처리 시간은 같은 문서 기준 약 200ms다.
구문 강조와 긴 문서 처리
WebKit을 제거한 뒤 가장 느린 단계는 구문 강조 문법의 정규식 컴파일이었다. syntect은 Sublime Text 문법을 사용하며, TypeScript 문법의 최초 사용에는 약 45ms가 걸렸다. 글은 Oniguruma가 [:alpha:]와 \w 같은 유니코드 문자 클래스를 정규식마다 수천 개의 코드 포인트 범위로 확장하는 점을 원인으로 설명한다.
이를 줄이기 위해 빌드 시 문법을 전처리한다. 문자 클래스를 동일한 ASCII 텍스트에 일치하고 비ASCII 문자는 문자로 취급하는 범위로 다시 쓴다. 전처리한 문법은 3~5배 빠르게 컴파일된다. 문서 안의 각 언어는 별도 스레드에서 강조하므로 Rust, Swift, TypeScript 코드가 있는 문서는 세 작업을 순차적으로 합산하는 대신 가장 느린 언어의 처리 시간에 가까워진다. 자주 쓰는 언어 문법은 확장 실행 중 모든 코어를 사용해 미리 컴파일한다.
900KB 변경 기록처럼 긴 문서는 화면을 채우는 첫 부분과 나머지로 나눈다. 첫 화면은 약 60ms에 그리며, 나머지는 백그라운드에서 만들어 이어 붙인다. 분할은 최상위 블록 사이에서만 이뤄져 표나 코드 블록이 중간에 잘리지 않는다.
지원 기능과 제약
- GFM 표, 작업 목록, 각주,
> [!NOTE]알림, 제목 앵커, 이모지 단축 코드, 하이라이트, front matter, 접을 수 있는<details>를 지원한다. - 220개 언어의 구문 강조를 제공한다. 라이트 모드는 GitHub 스타일 색상, 다크 모드는 One Half Dark 색상을 사용하며 시스템 테마 변경을 따른다.
- 인라인·디스플레이 수식과
math코드 블록은 SwiftMath로 조판한다. SwiftMath에는 KaTeX의 기호, 별칭, 환경이 추가됐으며 행렬, 정렬 방정식, braket 표기, 늘어나는 구분 기호를 지원한다. - Mermaid 다이어그램은 mermaid-rs-renderer로 레이아웃한 뒤 resvg로 비트맵을 그린다. 플로차트, 시퀀스, 클래스, 상태, ER 다이어그램, 파이 차트, 마인드맵, Git 그래프를 지원한다. 렌더러는 아직 초기 단계이며 일부 레이아웃은 mermaid.js와 다를 수 있다.
Quick Look 미리보기 패널, Finder 미리보기 창과 열 보기, Finder 썸네일에서 사용할 수 있다. 같은 디렉터리의 로컬 이미지를 표시하고 링크도 처리한다. 제목 링크는 해당 위치로 스크롤하며 다른 Markdown 파일 링크는 파일을 연다. 확장은 샌드박스에서 실행되고 기본 설정에서는 네트워크 요청을 하지 않는다. 원격 이미지는 앱에서 허용했을 때만 불러온다. 정적 shields.io 배지는 오프라인에서도 실제 색상으로 로컬 렌더링한다.
TextKit 기반 Markdown 렌더링 구조의 또 다른 사례는 MarkdownEngine: TextKit 2 기반의 macOS용 네이티브 마크다운 에디터 엔진에서 확인할 수 있다.
MD QuickLook은 MIT 라이선스의 무료 오픈 소스이며 macOS 12 이상이 필요하다. Homebrew로 brew install --cask armamut/tap/md-quicklook을 실행하거나 릴리스에서 내려받을 수 있다. 앱은 아직 공증되지 않아 처음 열 때 시스템 설정의 개인정보 보호 및 보안에서 실행을 허용해야 한다.
결론
MD QuickLook의 핵심은 Markdown 파싱과 레이아웃을 Rust에, 화면 표시를 Swift와 TextKit에 분리하고 중간 결과를 HTML이 아닌 바이너리 레이아웃으로 전달하는 데 있다. 구문 강조 문법은 빌드 시 전처리하며 언어별 작업을 병렬화한다. 긴 문서는 화면에 보이는 부분부터 표시한다. Mermaid 렌더러는 아직 초기 단계라 mermaid.js와 레이아웃이 다를 수 있다. 앱은 기본적으로 네트워크 요청을 하지 않으며, macOS 12 이상을 지원하지만 아직 공증되지 않았다.