원문: https://denodell.com/blog/youre-looking-at-the-wrong-pretext-demo
Cheng Lou가 만든 새로운 자바스크립트 라이브러리 Pretext는 출시 3일 만에 GitHub 스타 7,000개를 넘겼습니다. 그 기간 동안 프런트엔드 엔지니어링 커뮤니티 근처에 있었다면 이런 데모를 보셨을 겁니다. 물처럼 텍스트를 가르는 용, 타이포그래피 ASCII로 렌더링된 유체 연기, 문자 격자로 그린 와이어프레임 토러스, 60fps로 텍스트를 밀어내는 애니메이션 오브가 있는 다단 편집 레이아웃. 시각적으로 놀라운 데모이고, 라이브러리가 바이럴된 이유이기도 합니다.
하지만 이 라이브러리가 중요한 이유는 따로 있습니다.
Pretext가 하는 진짜 중요한 일은 DOM을 읽지 않고도 텍스트 블록의 높이를 예측하는 것입니다. 레이아웃 재계산을 단 한 번도 트리거하지 않고 텍스트 노드를 배치할 수 있다는 뜻입니다. 텍스트는 DOM에 남아 있으므로 스크린 리더가 읽을 수 있고, 사용자가 선택하고, 복사하고, 번역할 수 있습니다. 접근성(accessibility) 트리는 온전하게 유지되고, 실제로 성능은 향상되며, 모든 사용자의 경험이 보존됩니다. 프로덕션 웹 애플리케이션이 텍스트를 다루는 방식을 바꿀 기능이며, 거의 아무도 시연하지 않는 기능이기도 합니다.
커뮤니티는 사흘 동안 용을 만드는 데 시간을 쏟았습니다. 사실은 채팅 인터페이스를 개발해야 했을 텐데요. 용은 화제가 된 반면 측정 엔진은 아무도 눈여겨보지 않았다는 사실은, 프런트엔드 커뮤니티가 도구를 평가하는 방식에 대해 중요한 시사점을 던져줍니다. 즉, 우리는 눈에 보이는 것에 최적화할 뿐, 우리가 만든 도구를 사용하는 사람들에게 가장 중요한 요소에는 최적화하지 않는다는 것입니다.
문제는 강제 레이아웃 재계산으로, 브라우저가 작업을 계속하기 전에 일시 정지하여 페이지 레이아웃을 다시 측정해야 한다는 점입니다. UI 컴포넌트가 텍스트 블록의 높이를 알아야 할 때, 표준적인 접근 방식은 DOM에서 측정하는 것입니다. getBoundingClientRect()를 호출하거나 offsetHeight를 읽으면 브라우저가 동기적으로 레이아웃을 계산해 답을 줍니다. 가상 리스트의 텍스트 블록 500개에 대해 이 작업을 수행하면 500번의 멈춤을 강제하게 됩니다. 레이아웃 스레싱(layout thrashing)이라 불리는 이 패턴은 복잡한 웹 애플리케이션에서 화면 끊김 현상의 주요 원인으로 남아 있습니다.
Pretext의 통찰은 canvas.measureText()가 DOM 렌더링과 동일한 폰트 엔진을 사용하면서도 브라우저의 레이아웃 프로세스 바깥에서 완전히 작동한다는 것입니다. 캔버스(canvas)로 단어를 측정하고 너비를 캐시하면, 그 시점부터 레이아웃은 순수한 산술 연산이 됩니다. 캐시된 너비를 순회하고, 누적 줄 너비를 추적하며, 컨테이너의 최대값을 초과하면 줄바꿈을 삽입합니다. 느린 측정 읽기도, 동기적 멈춤도 없습니다.
아키텍처는 이를 두 단계로 분리합니다. prepare()가 비용이 큰 작업을 한 번만 수행합니다. 공백을 정규화하고, Intl.Segmenter로 로케일에 맞는 단어 경계를 기준으로 텍스트를 분할하고, 양방향 텍스트(영어와 아랍어 혼합 등)를 처리하고, 캔버스로 세그먼트를 측정한 뒤 재사용 가능한 참조를 반환합니다. layout()은 캐시된 너비에 대한 순수 계산으로, 500개 텍스트 배치에 약 0.09ms가 소요되는 반면 prepare()는 약 19ms가 걸립니다. Cheng Lou 본인도 500배 비교가 "불공정하다"고 말합니다. 일회성 prepare() 비용을 제외하기 때문입니다. 하지만 그 비용은 한 번만 지불되고 이후 모든 호출에 분산됩니다. 텍스트가 처음 나타날 때 한 번 실행되며, 이후 모든 리사이즈는 빠른 경로를 통해 수행되므로 성능 향상은 실제적이고 상당합니다.
핵심 아이디어는 Meta에서 Sebastian Markbage가 진행한 연구로 거슬러 올라갑니다. Cheng Lou는 캔버스 폰트 메트릭이 DOM 측정을 대체할 수 있음을 증명한 초기 text-layout 프로토타입을 구현했습니다. Pretext는 그 기반 위에 프로덕션급 국제화, 양방향 텍스트 지원, 그리고 빠른 경로를 만드는 2단계 아키텍처를 구축했습니다. Lou는 이런 일에 이력이 있습니다. react-motion과 ReasonML 모두 모두가 당연시하는 제약 조건을 식별하고, 더 나은 추상화를 통해 이를 제거하는 동일한 패턴을 따랐습니다.
Pretext가 제공하는 첫 번째 사용 사례이자 제가 주장하고 싶은 것은, 브라우저에 높이를 묻지 않고도 정확한 위치에 DOM 텍스트 노드를 렌더링할 수 있도록 텍스트 높이를 측정하는 것입니다. 타협의 경로가 아니라 라이브러리가 할 수 있는 가장 강력한 일입니다.
500개의 채팅 메시지가 있는 가상 스크롤링(virtual scrolling) 리스트를 생각해 보세요. 보이는 것만 렌더링하려면 각 메시지가 뷰포트에 들어오기 전에 높이를 알아야 합니다. 전통적인 접근 방식은 텍스트를 DOM에 삽입하고 측정한 다음 배치하는 것으로, 메시지마다 레이아웃 비용을 지불합니다. Pretext를 사용하면 높이를 수학적으로 예측한 다음 올바른 위치에 텍스트 노드를 렌더링할 수 있습니다. 텍스트 자체는 여전히 DOM에 존재하므로, 접근성 모델, 선택 동작, 페이지 내 검색 모두 다른 텍스트 노드와 정확히 동일하게 작동합니다.
실제로는 다음과 같습니다.
const prepared = prepare(message.text, '16px Inter');
const { height } = layout(prepared, containerWidth, 24);
함수 호출 두 번. 첫 번째는 측정하고 캐시하며, 두 번째는 계산을 통해 높이를 예측합니다. 레이아웃 비용은 없지만, 이후 렌더링하는 텍스트는 완전한 접근성을 갖춘 표준 DOM 노드입니다.
shrinkwrap 데모는 이 경로가 왜 중요한지 가장 명확하게 보여줍니다. CSS width: fit-content는 가장 넓은 줄에 맞게 컨테이너 크기를 조정하는데, 마지막 줄이 짧을 때 공간을 낭비합니다. "정확히 N줄로 줄바꿈되는 가장 좁은 너비를 찾아라"라는 CSS 프로퍼티는 없습니다. Pretext의 walkLineRanges()는 최적의 너비를 수학적으로 계산하며, 결과물은 표준 DOM 텍스트 노드로 렌더링된 더 촘촘한 채팅 버블입니다. 성능 향상은 더 똑똑한 측정에서 비롯되지, DOM을 포기한 데서 비롯되지 않습니다. 최종 사용자에게 텍스트는 아무것도 달라지지 않습니다.
Pretext로 높이를 계산하는 아코디언 섹션과, DOM 읽기 대신 높이 예측을 사용하는 메이슨리(masonry) 레이아웃 모두 같은 모델을 따릅니다. 빠른 측정이 표준 DOM 렌더링으로 이어지는 것입니다.
알아둘 만한 엣지 케이스가 있습니다. 예측은 측정 시점에 사용 가능한 폰트 메트릭만큼만 정확하므로, prepare() 실행 전에 폰트가 로드되어야 합니다. 그렇지 않으면 결과가 틀어질 수 있습니다. 리거처(ligature, 두 문자가 하나의 글리프로 합쳐지는 것, "fi" 등), 고급 폰트 기능, 특정 CJK 합성 규칙은 캔버스 측정과 DOM 렌더링 사이에 미세한 차이를 만들 수 있습니다. 해결 가능한 문제이고 라이브러리가 이미 많은 부분을 처리하지만, 마법처럼 취급하지 않고 진지하게 접근하려면 이런 점을 인정해야 합니다.
Pretext는 캔버스, SVG, WebGL로 렌더링하기 위한 수동 라인 레이아웃도 지원합니다. 이 API들은 정확한 줄 좌표를 제공해서 DOM이 처리하게 두는 대신 직접 텍스트를 그릴 수 있게 합니다. 바이럴된 경로이자 모든 커뮤니티 쇼케이스를 지배하는 경로입니다.
캔버스 데모는 인상적이고 DOM이 60fps에서 할 수 없는 일을 해내고 있습니다. 하지만 픽셀을 그리는 것이기도 하며, 텍스트를 캔버스 픽셀로 그리면 브라우저는 그 픽셀이 언어를 나타내는지 전혀 알지 못합니다. VoiceOver, NVDA, JAWS 같은 스크린 리더는 접근성 트리로 페이지를 이해합니다. 접근성 트리 자체가 DOM에서 만들어지므로 캔버스 콘텐츠는 스크린 리더에 보이지 않습니다. 브라우저의 페이지 내 검색과 번역 도구도 캔버스 픽셀을 완전히 건너뜁니다. 네이티브 텍스트 선택은 DOM 텍스트 노드에 묶여 있고 캔버스에는 그에 상응하는 것이 없으므로, 사용자가 콘텐츠를 선택하거나 복사하거나 키보드로 탐색할 수 없습니다. <canvas> 요소는 하나의 탭 정지점이기도 해서, 수천 단어가 포함되어 있더라도 키보드 사용자가 개별 단어나 문단 사이를 이동할 수 없습니다. 요약하면, 텍스트를 텍스트의 이미지가 아닌 텍스트답게 만드는 모든 것이 사라집니다.
그렇다고 해서 캔버스 방식이 무조건 잘못된 것은 아닙니다. 캔버스를 이용한 텍스트 렌더링이 올바른 선택이 되는 정당한 맥락들도 존재합니다. 게임, 데이터 시각화, 크리에이티브 인스톨레이션, 그리고 캔버스 위에 자체 접근성 레이어를 구축하는 데 수년을 투자한 디자인 도구가 그렇습니다. SVG 렌더링은 트레이드오프가 또 다릅니다. SVG 텍스트 요소는 접근성 트리에 참여하므로 DOM과 캔버스의 중간 지점이 됩니다.
하지만 캔버스 방식이 획기적인 혁신은 아닙니다. 캔버스 텍스트 렌더링은 수십 개의 라이브러리에서 15년 이상 존재해 왔기 때문입니다. 그중 어떤 것도 레이아웃 비용을 지불하지 않고 DOM 텍스트 레이아웃을 예측하는 방법을 제공하지 못했습니다. Pretext의 prepare()와 layout()이 정확히 그것을 해내며, 이것은 진정으로 새로운 것입니다.
이 패턴은 프런트엔드 생태계에서 자주 반복되며, 왜 그런지 이해합니다.
물처럼 텍스트를 가르는 용은 GIF로 녹화해서 소셜 미디어에 올리고 수천 개의 노출을 모을 수 있는 것입니다. 텍스트 높이를 미리 계산하는 가상 스크롤링 리스트는 그렇지 않은 것과 똑같아 보입니다. 성능 차이는 상당하지만 눈에는 보이지 않습니다. "VoiceOver와 완벽하게 작동합니다"나 "강제 레이아웃 없이 10,000개 메시지를 스크롤합니다"라는 쇼케이스를 만드는 사람은 없습니다. 이런 것은 아무것도 아닌 것처럼 보이기 때문입니다. 웹 페이지가 원래 작동해야 하는 대로 작동하는 것처럼 보입니다.
이것은 웹 성능에 적용된 굿하트의 법칙(Goodhart's Law)입니다. 지표가 목표가 되는 순간, 더 이상 유효한 측정 기준이 될 수 없습니다. 프레임 속도와 레이아웃 비용은 "사용자에게 잘 작동하는가"의 대리 지표입니다. GitHub 스타는 "유용한가"의 대리 지표입니다. 대리 지표만 최적화하다 보면, 접근성 트레이드오프가 가장 큰 경로를 사용하는 시각적으로 인상적인 데모가 주목받고, 라이브러리가 왜 중요한지에 대한 실제 신호는 사라집니다. 처음 72시간 동안 가장 시각적으로 인상적인 기능이 라이브러리의 정체성을 결정하고, 프레임은 "나는 무언가를 그리고 있다"가 되어버립니다. "나는 누구보다 빠르게 측정하고 있다"가 아니라요. 한번 정해진 프레임은 바꾸기 어렵습니다.
웹에서 최고의 텍스트 편집 라이브러리인 CodeMirror, Monaco, ProseMirror는 모두 DOM을 벗어나는 것이 더 빠를 수 있는 상황에서도 의도적으로 DOM 내에 머무르기로 결정했습니다. 접근성 모델은 선택 사항이 아니기 때문입니다. Pretext의 DOM 측정 방식은 이러한 전통을 따르면서도 한 걸음 더 나아갑니다. 앞서 언급한 편집기들은 무언가의 높이를 알아야 할 때 여전히 DOM에서 읽습니다. Pretext는 그 단계를 완전히 제거하고, 노드가 렌더링되기 전에 산술로 높이를 예측합니다. 이는 동일한 철학 하에서 취할 수 있는 다음 단계의 논리적 선택입니다. 텍스트는 본래 있어야 할 곳에 두되, 이를 위해 측정 비용을 지불하는 것은 중단하는 것입니다.
저는 경력 대부분을 성능 엔지니어링이라는 분야에 대해 생각해 왔고, Pretext에서 가장 인상적인 점은 진정한 혁신이 바로 가장 눈에 띄지 않는 곳에 있다는 사실입니다. 텍스트가 페이지에 표시되기 전에 어떻게 배치될지 예측하면서, 텍스트를 DOM에 유지하고 접근성을 보장하는 것은 웹 플랫폼에서 진정으로 새로운 기능입니다. 이는 텍스트가 많은 모든 복잡한 애플리케이션이 즉시 도입할 수 있는 근본적인 개선 사항입니다.
이번 주에 Pretext를 사용하려 한다면, prepare()와 layout()을 먼저 사용하세요. 텍스트를 DOM에 유지하면서 브라우저에 묻지 않고 높이를 예측하는 무언가를 만드세요. 모든 사용자가 읽고, 선택하고, 검색하고, 탐색할 수 있는 인터페이스를 출시하세요. 아직 아무도 이것을 하지 않았고, 만들어 볼 가치가 있습니다.
성능 엔지니어링은 누구에게도 무언가를 포기하라고 요구하지 않으면서 모든 사람에게 도움이 될 때 가장 빛납니다. 누군가를 어지럽게 만들지 않는 더 빠른 프레임 속도. 운동 장애가 있는 사람이 필요할 때 페이지가 응답하게 만드는 더 적은 레이아웃 멈춤. 빠르고 읽을 수 있고 선택할 수 있고 번역할 수 있고 키보드로 탐색할 수 있고 스크린 리더가 이해할 수 있는 텍스트.
용은 재미있습니다. 측정 엔진은 중요합니다. 이 둘을 혼동하지 맙시다.