[번역] 성능과 하이라이팅을 모두 잡는 CSS Highlights API

샛별·2026년 2월 5일

translations.zip

목록 보기
26/28
post-thumbnail

원저자의 허락을 받아 <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 살펴보기

CSS 커스텀 Highlight API는 DOM 구조를 조작하지 않고도 특정 텍스트 범위를 스타일링하는 방법을 제공합니다. 래퍼 요소를 생성하는 대신, 텍스트 노드 안의 특정 문자 위치를 가리키는 Range 객체를 만들고, 이를 스타일 타입별로 그룹화한 뒤 브라우저의 하이라이트 레지스트리에 등록하는 방식입니다.

왜 더 빠를까요?

  • DOM 조작이 없음: 텍스트는 하나의 텍스트 노드로 존재함
  • 적은 메모리 사용량: Range는 가벼운 객체
  • 브라우저 최적화: 브라우저가 직접 페인팅을 처리
  • 명확한 분리: 스타일링은 CSS에서만 처리

브라우저 지원

CSS 커스텀 Highlight API는 모든 모던 브라우저에서 지원됩니다.

  • Chrome/Edge 105+
  • Firefox 140+
  • Safari 17.2+
  • Opera 91+

최신 버전에서 잘 지원되지만 API가 존재하는지 확인하는 것이 좋습니다.
if (!CSS.highlights) { \/\* 없는 경우 폴백 대비 \*/ }

예제 구현 코드

CSS Highlight API를 사용하여 구문을 하이라이팅하는 코드를 구현해 보겠습니다.

1단계) CSS Highlight 스타일 정의하기

먼저 ::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;
}

2단계) 하이라이팅 로직 구현하기

코드 요소에 하이라이트를 적용하는 핵심 로직을 구현해보면 다음과 같습니다.

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);
        }
      }
    }
  };
}

3단계) 애플리케이션에 사용하기

간단하게 바닐라 자바스크립트로 코드를 작성하면 다음과 같습니다.

// 코드 뷰어 요소 생성
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 하이라이트 스타일용입니다. 여러분이 수정한 코드가 실시간으로 미리보기에 반영됩니다!

역자주: 블로그 플랫폼 특성상 데모를 복사해올 수 없어 링크로 대체합니다. 관심 있으신 분은 원문에서 확인해 보세요 :)

작동 방식

  1. 토큰화: 렉서가 소스 코드를 스캔해 토큰을 생성하고, 각 토큰에 타입 정보와 문자 위치를 포함시킵니다.
  2. Range 생성: 각 토큰이 텍스트 노드 내에서 정확히 어디에 있는지를 나타내는 Range 객체를 만듭니다.
  3. 그룹화: 토큰 타입(키워드, 문자열, 주석 등)별로 Range를 묶습니다.
  4. 하이라이트 등록: 각 그룹을 Highlight 객체로 감싼 뒤, 토큰 타입을 키로 하여 CSS.highlights에 등록합니다.
  5. CSS 스타일링: 브라우저는 ::highlight(token-type) 규칙에 정의된 스타일을 적용합니다.
  6. 클린업: 컴포넌트가 언마운트될 때 모든 Range를 레지스트리에서 제거합니다.

장점

  • 성능: DOM 변경이 없으므로 초기 렌더링과 리렌더링이 더 빠름
  • 💾 효율적인 메모리: 래퍼 요소에 비해 Range는 메모리를 거의 사용하지 않음
  • 🧹 깨끗한 HTML: DOM에는 단일 텍스트 노드만 남아 구조가 깔끔함
  • 🎨 순수한 CSS 스타일링: 모든 스타일을 CSS에서 선언적으로 정의
  • ♻️ 간단한 클린업: DOM을 건드리지 않고 Range의 추가/삭제가 가능함

한계

  • 텍스트 노드만 지원: 순수 텍스트 콘텐츠에만 동작함
  • 단일 텍스트 노드 필요: 하이라이팅 할 요소가 단일 텍스트 노드를 가져야 함
  • 정적 Range: 텍스트 내용이 변경되더라도 Range가 자동으로 갱신되지 않음
  • 구형 브라우저: Chrome 105, Firefox 140, Safari 17.2 이전 브라우저에서는 폴백 필요

기존 방식과의 비교

비교 사항기존 방식 (래퍼 코드)CSS Highlight API
DOM 노드수백 혹은 수천 개하나의 텍스트 노드
메모리 사용량높음낮음
초기 렌더링느림빠름
리렌더링느림빠름
HTML 구조복잡함단순함
브라우저 지원모든 브라우저최신 브라우저

마무리

CSS 커스텀 Highlight API는 문법 하이라이팅이나 텍스트 스타일링 기능을 구현하는 데 있어 의미 있는 변화입니다. 래퍼 DOM 요소가 필요 없으므로 성능이 크게 향상되며, 코드도 더욱 깔끔하고 유지보수가 쉽습니다.

또한 주요 브라우저에서 꽤 우수한 지원을 제공하므로, 대부분의 최신 웹 프로덕션 환경에서 바로 사용할 수 있습니다. 버전이 낮은 브라우저도 지원해야 한다면 기존의 DOM 기반 하이라이팅 방식으로 폴백할 수 있습니다.

텍스트 하이라이팅의 미래는 <span> 태그 없이 이미 우리에게 다가왔습니다! 🎨✨

profile
["NAVER", "FE", "PLACE", "UX"]

0개의 댓글