DOM 텍스트를 안정적으로 측정하려면 관련 스타일시트가 로드될 때까지 기다리고, 텍스트를 의도한 레이아웃에 배치한 다음, 기하 정보를 읽기 전에 document.fonts.ready을 기다리세요. 이 가이드에서는 Mac 데스크톱 브라우저에서의 순서를 보여 주고, 언제 반복해야 하는지 설명합니다.

여기서 다루는 작업은 브라우저 페이지에서 합성 레이블이나 줄바꿈된 문단을 측정하는 것입니다. 예를 들어 카드에 필요한 공간이 얼마나 되는지 결정할 수 있습니다. 텍스트 레이아웃은 브라우저가 처리하고, 코드는 준비 상태를 확인한 다음 결과 상자를 읽어야 합니다.

어떤 준비 신호를 기다려야 하나요?

문서 구성, 스타일시트 로드, 글꼴 레이아웃에는 각각 별도의 완료 시점이 있습니다. 직접 제어하는 작업에 맞는 완료 시점을 선택한 다음, 콘텐츠와 스타일이 적용된 후 측정하세요.

신호 확인되는 사항 추가로 확인할 사항
DOMContentLoaded 파싱과 지연된 스크립트 실행이 완료됨 스타일시트 로드를 직접 기다리지는 않음
스타일시트의 load 이벤트 해당 스타일시트와 가져온 콘텐츠가 로드되고 파싱됨 글꼴 로드와 그에 따른 대상의 레이아웃
window의 load 이벤트 지연 로드를 제외한 스타일시트 등 초기 페이지 리소스가 로드됨 이후의 애플리케이션 변경과 글꼴 준비 상태
document.fonts.ready 문서에서 사용되는 글꼴과 관련된 로드 및 레이아웃이 완료됨 이후의 콘텐츠, 스타일 또는 크기 변경

MDN은 스타일시트와 관련된 세부 사항을 설명합니다. DOMContentLoaded 자체는 스타일시트가 로드될 때까지 기다리지 않지만, 지연된 스크립트는 스타일시트를 기다리므로 간접적으로 이 이벤트가 지연될 수 있습니다. 요소를 찾는 데는 유용하지만, 측정 기준으로는 불완전합니다. Document: DOMContentLoaded event.

직접 삽입하는 스타일시트의 경우 링크를 삽입하기 전에 load 및 error 핸들러를 연결하세요. MDN에 따르면 스타일시트의 load 이벤트는 로드와 파싱이 끝난 뒤, 스타일 적용이 시작되기 직전에 발생합니다. WHATWG의 스타일시트 처리 과정에서도 성공적인 로드와 error 이벤트를 구분합니다. Stylesheet load events, HTML Standard: Stylesheet links.

페이지의 초기 리소스 로드에는 window의 load 이벤트가 더 포괄적인 확인 지점입니다. 그 이후에 시작하는 스크립트는 구독하기 전에 document.readyState을 확인해야 합니다. 이미 발생한 이벤트는 새로 연결한 핸들러를 실행하지 않기 때문입니다. 나중에 스타일시트를 삽입하는 애플리케이션 코드는 여전히 해당 삽입을 명시적으로 추적해야 합니다. Window: load event, Checking document readiness.

글꼴 준비 상태가 실제로 보장하는 것은 무엇인가요?

W3C CSS Font Loading 명세에 따르면 ready은 관련 레이아웃 작업이 완료되고 해당 작업에 더는 글꼴 로드가 필요하지 않을 때 이행됩니다. 여기에는 글꼴 대체로 인해 추가 로드가 발생하는 경우도 포함됩니다. 이 프라미스는 한 번만 이행되며, 이후 변경으로 글꼴을 다시 로드할 수 있습니다. 시작할 때 한 번 받은 프라미스를 계속 사용하지 말고, 각 측정과 관련된 변경이 일어난 뒤 document.fonts.ready을 읽으세요. CSS Font Loading Module Level 3: The ready attribute.

준비 상태는 문서에서 사용되는 글꼴에 관한 것입니다. 선언된 모든 글꼴이 다운로드되었다는 뜻은 아닙니다. MDN은 font-display: optional로 인해 실제 사용되는 글꼴 집합이 선언된 집합과 달라질 수 있다고 설명합니다. 따라서 측정이 완료되더라도 대체 글꼴이 적용된 텍스트의 크기를 정확히 나타낼 수 있습니다. Document: fonts property.

대기 코드를 작성하기 전에 필요한 결과를 결정하세요.

  • 현재 렌더링된 결과를 측정하려는 경우: 글꼴 준비 상태를 기다린 다음 DOM 기하 정보를 읽고, 브라우저의 글꼴 대체 결정을 받아들입니다.
  • 선언된 다운로드 가능 글꼴이 필요한 경우: 일치하는 글꼴을 명시적으로 로드하고 실패를 처리한 다음, 측정 전에 문서의 글꼴 준비 상태를 기다립니다.
  • 변경되는 콘텐츠를 측정하려는 경우: 관련 업데이트가 발생할 때마다 순서를 반복하고, 다른 업데이트로 대체된 결과는 버립니다.

두 번째 결과가 필요하다면 document.fonts.load()에 CSS 글꼴 사양과 선택적 샘플 텍스트를 지정할 수 있습니다. SampleFace라는 합성 글꼴 패밀리를 선언한 경우, 사양은 400 18px \"SampleFace\"이고 텍스트는 Panel Alpha가 될 수 있습니다. 일치하는 글꼴 로드에 실패하면 반환된 프라미스가 거부됩니다. 텍스트 인수는 선언된 유니코드 범위를 통해 글꼴을 선택하며, 개별 글리프 지원 여부를 검증하지는 않습니다. FontFaceSet: load() method.

이 요구사항에 document.fonts.check()를 대신 사용하지 마세요. MDN 설명에 따르면 요청한 글꼴을 사용할 수 없어도 true을 반환할 수 있습니다. 마찬가지로 W3C는 ready이 절대 거부되지 않는다고 명시하므로, 성공적으로 완료되었다는 사실만으로 글꼴 로드 성공을 증명할 수는 없습니다. FontFaceSet: check() method, CSS Font Loading: The ready attribute.

스타일시트 로드부터 측정까지 전체 예제를 실행하세요

Mac에서 현재 데스크톱 브라우저로 about:blank를 열고 개발자 도구를 연 다음 Console을 선택하세요. 아래의 완전한 JavaScript 프로그램을 한 번 실행하세요. 프로그램 자체가 중립적인 샘플 텍스트와 스타일시트를 만들기 때문에 서버, 라이브러리 또는 글꼴 파일이 필요하지 않습니다.

이 예제는 의도적으로 system-ui을 사용합니다. 다운로드 가능한 리소스에 의존하지 않고 대기 순서를 보여 줍니다. 웹 글꼴을 사용하는 페이지에서는 이 순서를 유지하고 해당 페이지의 실제 글꼴 선언이 리소스를 제공하게 하세요. 브라우저 예제를 검토했지만 이 문서 작성 과정에서 실행하지는 않았습니다.

(async () => {
  if (!document.fonts || !document.body) {
    throw new Error("A document body and CSS Font Loading support are required.");
  }
  if (document.getElementById("text-measure-demo")) {
    throw new Error("Reload the blank tab before running this example again.");
  }

  async function withDeadline(promise, label, milliseconds = 10000) {
    let timer;
    try {
      return await Promise.race([
        promise,
        new Promise((_, reject) => {
          timer = setTimeout(() => {
            reject(new Error(`${label} did not finish within the deadline.`));
          }, milliseconds);
        })
      ]);
    } finally {
      clearTimeout(timer);
    }
  }

  const paragraph = document.createElement("p");
  paragraph.id = "text-measure-demo";
  paragraph.textContent =
    "Panel Alpha contains a compact label. Panel Beta contains another label.";
  document.body.append(paragraph);

  const css = `
    #text-measure-demo {
      box-sizing: border-box;
      width: 280px;
      margin: 24px;
      padding: 12px;
      border: 1px solid #777;
      font: 400 18px/1.5 system-ui, sans-serif;
      letter-spacing: normal;
      white-space: normal;
      overflow-wrap: normal;
    }
  `;
  const stylesheetURL = URL.createObjectURL(
    new Blob([css], { type: "text/css" })
  );
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = stylesheetURL;

  const stylesheetLoaded = new Promise((resolve, reject) => {
    link.onload = resolve;
    link.onerror = () => reject(new Error("The sample stylesheet failed."));
  });

  try {
    document.head.append(link);
    await withDeadline(stylesheetLoaded, "Stylesheet loading");

    // Establish the target's styled layout before reading font readiness.
    paragraph.getBoundingClientRect();
    await withDeadline(document.fonts.ready, "Font readiness");

    const style = getComputedStyle(paragraph);
    const range = document.createRange();
    range.selectNodeContents(paragraph);
    const copyRect = rect => ({
      x: rect.x, y: rect.y, width: rect.width, height: rect.height
    });

    console.log({
      text: paragraph.textContent,
      fontFamily: style.fontFamily,
      fontSize: style.fontSize,
      lineHeight: style.lineHeight,
      elementBox: copyRect(paragraph.getBoundingClientRect()),
      textFragments: Array.from(range.getClientRects(), copyRect)
    });
  } catch (error) {
    link.remove();
    paragraph.remove();
    throw error;
  } finally {
    link.onload = null;
    link.onerror = null;
    URL.revokeObjectURL(stylesheetURL);
  }
})().catch(error => console.error(error));

중요한 순서는 스타일시트 로드 완료, 대상 레이아웃 설정, 글꼴 준비 완료, 마지막 기하 정보 읽기입니다. 첫 번째 기하 정보 읽기는 현재 레이아웃을 설정하기 위한 것이며, 그 값은 의도적으로 버립니다. 마지막 읽기는 글꼴 준비가 끝난 후의 결과를 나타냅니다. 10초 제한은 작동 방식을 설명하기 위한 예시입니다. 시간이 만료되면 시도가 완료되지 않았다고 알리고 측정값은 반환하지 않습니다.

예제를 수정할 때는 상자와 함께 fontSize 및 lineHeight을 확인하세요. getComputedStyle()은 활성 스타일시트가 적용된 후의 계산된 속성 값을 반환하므로 선택자나 캐스케이드가 일치하지 않는 경우를 찾는 데 유용합니다. 그 fontFamily 값은 CSS 글꼴 패밀리 목록을 나타냅니다. 어떤 글리프가 어떤 글꼴에서 제공되었는지 증명하는 것으로 간주해서는 안 됩니다. Window: getComputedStyle() method.

어떤 DOM 측정이 궁금한 점에 답해 주나요?

카드나 문단이 차지하는 바깥쪽 공간이 필요하면 element.getBoundingClientRect()을 사용하세요. 여기에는 패딩과 테두리가 포함되며, 위치는 뷰포트를 기준으로 합니다. 예제에서 선언된 테두리 상자 너비는 280px이고, 텍스트에 사용할 수 있는 콘텐츠 너비는 254px이며, 280 − 24 − 2로 계산됩니다. 이 계산은 지정된 CSS를 설명하는 것이지, 관찰된 브라우저 결과가 아닙니다. Element: getBoundingClientRect() method.

텍스트의 레이아웃 조각이 필요하면 텍스트를 대상으로 Range를 사용하세요. Range.getClientRects()은 둘러싼 문단 상자만 반환하는 것이 아니라 범위에 해당하는 사각형들을 반환합니다. 컬렉션을 유지하세요. 줄바꿈된 텍스트를 하나의 너비로 평탄화하면 텍스트가 차지하는 영역에 관한 유용한 정보가 사라집니다. 일반적인 구현에서 배열 길이를 lineCount라고 부르지 마세요. 복잡한 인라인 콘텐츠는 여러 개의 사각형을 만들 수 있습니다. Range: getClientRects() method.

측정 중에는 원래 텍스트 구간을 그대로 유지하세요. 모든 문자마다 래퍼를 추가하면 글꼴 셰이핑에 영향을 주는 서식 경계가 생길 수 있습니다. W3C CSS Text 초안은 인라인 경계가 셰이핑을 유지하거나 끊는 조건을 명시합니다. 기존 DOM을 범위로 측정하면 이러한 래퍼를 추가하지 않아도 됩니다. CSS Text Module Level 3: Shaping Across Element Boundaries.

Canvas는 용도가 다릅니다. measureText()은 지정된 문자열에 대해 캔버스 텍스트 측정값을 반환합니다. 이 작업처럼 DOM 카드 안에서 텍스트가 실제로 어떻게 줄바꿈되는지 알아보려면 준비가 완료된 후 DOM을 읽으세요. 캔버스 측정은 별도의 텍스트 설정을 사용하는 독립적인 계산으로 다루세요. CanvasRenderingContext2D: measureText() method.

측정값이 계속 바뀌면 어떻게 해야 하나요?

대기 시간을 추가하기 전에 다음 진단 순서를 따르세요.

  1. 기다린 스타일시트 완료 신호가 대상에 스타일을 적용하는 스타일시트에서 발생했는지 확인하세요. 동적으로 삽입된 스타일시트는 로드 프라미스를 삽입 코드와 함께 보관하세요.
  2. 대상의 계산된 타이포그래피와 너비를 살펴보세요. 스타일시트가 로드되어도 캐스케이드에서 우선순위를 잃을 수 있습니다.
  3. 글꼴 준비 상태를 기다리기 전에 대상 텍스트와 의도한 컨테이너가 존재하는지 확인하세요.
  4. 대체 글꼴을 허용할지 결정하세요. 특정 선언 글꼴이 필요하면 명시적으로 로드하고 실패를 알리세요.
  5. 기다린 후 다른 애플리케이션 업데이트가 텍스트, 클래스 또는 컨테이너를 변경했는지 확인하세요. 새로운 상태에 맞춰 측정을 다시 시작하세요.

프레임 콜백은 리소스 로드 완료를 보장하지 않습니다. requestAnimationFrame()은 다시 그리기 전에 콜백을 예약하며, 브라우저는 백그라운드 탭에서 이를 일시 중지하는 경우가 많습니다. 프레임 예약이 애플리케이션에 필요한 경우 사용하되, 스타일시트나 글꼴 프라미스를 한두 번의 프레임 콜백으로 대체하지 마세요. Window: requestAnimationFrame() method.

레이아웃 크기가 바뀔 수 있다면 관련 컨테이너를 ResizeObserver로 관찰하고 크기가 변경될 때마다 새 측정을 예약하세요. 옵저버는 콘텐츠 상자 또는 테두리 상자의 크기 변화를 보고합니다. 모든 텍스트 변경을 알리는 범용 알림 기능은 아닙니다. 텍스트를 교체하거나 타이포그래피를 변경하는 애플리케이션 작업에서도 측정을 실행하세요. ResizeObserver.

이미 Playwright를 사용한다면 비동기 page.evaluate() 콜백 안에서 브라우저 측 준비 대기와 기하 정보 읽기를 수행하세요. Playwright는 반환된 프라미스가 처리될 때까지 기다린 다음 결과를 호출 환경으로 가져옵니다. 예제처럼 일반 숫자형 사각형 필드를 반환하고, 호출 측에서 사용할 값은 모두 콜백에 명시적으로 전달하세요. Evaluating JavaScript.

유용한 측정값은 특정 텍스트, 스타일시트 상태, 글꼴 결과, 컨테이너 크기에 해당합니다. 이러한 입력을 설정하고 관련 로드 작업을 기다린 다음, 변경하지 않은 DOM을 측정하세요. 입력이 바뀌면 측정을 반복해 기록된 기하 정보가 실제 레이아웃 질문에 계속 답하도록 하세요.

출처