
원저자의 허락을 받아 <High-Performance Syntax Highlighting with CSS Highlights API> 아티클을 한국어로 번역한 글입니다.
대부분의 구문 하이라이팅 도구는 각 토큰(키워드, 문자열, 연산자 등)을 <span> 요소로 감싸고 CSS 클래스를 적용하는 방식으로 작동합니다. 그래서 수백 또는 수천 개의 DOM 노드를 생성해야 하는 경우도 있습니다.
<span class="keyword">const</span>
<span class="identifier">greeting</span>
<span class="operator">=</span>
<span class="string">"Hello World"</span>
이러한 각 노드는 브라우저의 렌더링 파이프라인에 부담을 더합니다. 파싱해야 할 노드가 늘어나고, 레이아웃 계산과 페인트 작업이 더 많이 수행되고, 메모리 사용량도 커지기 때문입니다. 문서 위주의 사이트나 코드 양이 많은 앱이라면, 성능에 직접적으로 영향을 미칠 수 있습니다.
CSS 커스텀 Highlight API는 DOM 구조를 조작하지 않고도 특정 텍스트 범위를 스타일링하는 방법을 제공합니다. 래퍼 요소를 생성하는 대신, 텍스트 노드 안의 특정 문자 위치를 가리키는 Range 객체를 만들고, 이를 스타일 타입별로 그룹화한 뒤 브라우저의 하이라이트 레지스트리에 등록하는 방식입니다.
왜 더 빠를까요?
CSS 커스텀 Highlight API는 모든 모던 브라우저에서 지원됩니다.
최신 버전에서 잘 지원되지만 API가 존재하는지 확인하는 것이 좋습니다.
if (!CSS.highlights) { \/\* 없는 경우 폴백 대비 \*/ }
CSS Highlight API를 사용하여 구문을 하이라이팅하는 코드를 구현해 보겠습니다.
먼저 ::highlight() 의사 요소(pseudo-element)를 사용하여 각 토큰 타입에 해당하는 스타일을 정의합니다.
::highlight(keyword) {
color: #0000ff;
font-weight: bold;
}
::highlight(string) {
color: #a31515;
}
::highlight(comment) {
color: #008000;
font-style: italic;
}
::highlight(number) {
color: #098658;
}
::highlight(operator) {
color: #000000;
}
::highlight(function) {
color: #795e26;
}
코드 요소에 하이라이트를 적용하는 핵심 로직을 구현해보면 다음과 같습니다.
function applyHighlighting(element: HTMLElement, code: string): () => void {
// 브라우저 지원 확인
if (!CSS.highlights) {
console.warn("CSS Custom Highlight API not supported");
return () => {};
}
// 텍스트 노드 가져오기 (반드시 하나의 텍스트 노드여야 함)
const textNode = element.firstChild;
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
return () => {};
}
// 코드 토큰화 (필요에 맞는 어휘 분석기 사용)
const tokens = lexTypeScript(code);
// 토큰별로 Range 객체 생성
const tokenRanges = tokens.map((token) => {
const range = new Range();
range.setStart(textNode, token.start);
range.setEnd(textNode, token.end);
return { type: token.type, range };
});
// 토큰 타입별로 Range 그룹화
const highlightsByType = Map.groupBy(
tokenRanges,
(item: { type: string; range: Range }) => item.type
);
// Highlight를 생성하고 등록하기
const createdHighlights = new Map<string, Highlight>();
for (const [type, items] of highlightsByType) {
const ranges = items.map(
(item: { type: string; range: Range }) => item.range
);
const highlight = new Highlight(...ranges);
createdHighlights.set(type, highlight);
// 전역 CSS 하이라이트 레지스트리에 등록
const existing = CSS.highlights.get(type);
if (existing) {
ranges.forEach((range) => existing.add(range));
} else {
CSS.highlights.set(type, highlight);
}
}
// 클린업 함수
return () => {
for (const [type, highlight] of createdHighlights) {
const globalHighlight = CSS.highlights.get(type);
if (globalHighlight) {
highlight.forEach((range) => globalHighlight.delete(range));
if (globalHighlight.size === 0) {
CSS.highlights.delete(type);
}
}
}
};
}
간단하게 바닐라 자바스크립트로 코드를 작성하면 다음과 같습니다.
// 코드 뷰어 요소 생성
function createCodeViewer(code, language = "javascript") {
const container = document.createElement("div");
container.style.cssText = `
position: relative;
background: #f5f5f5;
padding: 15px;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
overflow-x: auto;
border: 1px solid #e0e0e0;
white-space: pre;
line-height: 1.5;
`;
const codeElement = document.createElement("div");
codeElement.textContent = code;
container.appendChild(codeElement);
// 하이라이팅 적용
const cleanup = applyHighlighting(codeElement, code);
// 나중에 사용할 클린업 함수 저장
container._cleanup = cleanup;
return container;
}
// 적용
const viewer = createCodeViewer(`
const greeting = "Hello, World!"
function sayHello() {
console.log(greeting)
}
`);
document.body.appendChild(viewer);
// 제거될 때 클린업 처리
// viewer._cleanup()
// viewer.remove()
혹은 리액트로 작성할 수도 있습니다.
import { useEffect, useRef } from "react";
function CodeViewer({ code, language = "javascript" }) {
const codeRef = useRef < HTMLDivElement > null;
useEffect(() => {
if (!codeRef.current) return;
const cleanup = applyHighlighting(codeRef.current, code);
return cleanup;
}, [code]);
return (
<div
style={{
position: "relative",
background: "#f5f5f5",
padding: "15px",
borderRadius: "4px",
fontFamily: "monospace",
fontSize: "14px",
overflowX: "auto",
border: "1px solid #e0e0e0",
whiteSpace: "pre",
lineHeight: "1.5",
}}
>
<div ref={codeRef}>{code}</div>
</div>
);
}
여러분이 직접 해볼 수 있습니다! 코드를 작성하고 구문 하이라이팅 스타일을 직접 정의해 보세요. 아래 데모에는 두 개의 편집기가 나란히 배치되어 있습니다. 각각 자바스크립트 코드용과 CSS 하이라이트 스타일용입니다. 여러분이 수정한 코드가 실시간으로 미리보기에 반영됩니다!
역자주: 블로그 플랫폼 특성상 데모를 복사해올 수 없어 링크로 대체합니다. 관심 있으신 분은 원문에서 확인해 보세요 :)
Range 객체를 만듭니다.Highlight 객체로 감싼 뒤, 토큰 타입을 키로 하여 CSS.highlights에 등록합니다.::highlight(token-type) 규칙에 정의된 스타일을 적용합니다.| 비교 사항 | 기존 방식 (래퍼 코드) | CSS Highlight API |
|---|---|---|
| DOM 노드 | 수백 혹은 수천 개 | 하나의 텍스트 노드 |
| 메모리 사용량 | 높음 | 낮음 |
| 초기 렌더링 | 느림 | 빠름 |
| 리렌더링 | 느림 | 빠름 |
| HTML 구조 | 복잡함 | 단순함 |
| 브라우저 지원 | 모든 브라우저 | 최신 브라우저 |
CSS 커스텀 Highlight API는 문법 하이라이팅이나 텍스트 스타일링 기능을 구현하는 데 있어 의미 있는 변화입니다. 래퍼 DOM 요소가 필요 없으므로 성능이 크게 향상되며, 코드도 더욱 깔끔하고 유지보수가 쉽습니다.
또한 주요 브라우저에서 꽤 우수한 지원을 제공하므로, 대부분의 최신 웹 프로덕션 환경에서 바로 사용할 수 있습니다. 버전이 낮은 브라우저도 지원해야 한다면 기존의 DOM 기반 하이라이팅 방식으로 폴백할 수 있습니다.
텍스트 하이라이팅의 미래는 <span> 태그 없이 이미 우리에게 다가왔습니다! 🎨✨