혼합 크기 텍스트 줄은 기준선 정렬이 위쪽과 아래쪽 범위를 결합하기 때문에 두 span의 line-height 값보다 더 높아질 수 있습니다. 이 가이드에서는 추가 높이를 진단하고 Mac 데스크톱 브라우저에서 두 레이블이 있는 행을 조정하는 방법을 보여 줍니다.

가장 높은 span이 줄 높이를 결정하지 않는 이유는 무엇인가요?

큰 Label A 옆에 작은 Label B가 있는 추상적인 페이지 헤더를 생각해 보세요. 두 span 모두 line-height: 40px이지만, 이들을 담은 행의 높이는 40 CSS 픽셀을 넘습니다. 이는 인라인 레이아웃이 바깥쪽 줄을 계산하기 전에 상자를 정렬하는 방식에 따른 결과입니다.

일반적인 인라인 텍스트에서 브라우저는 각 상자의 높이 기여분을 계산하고, 상자를 세로로 정렬한 다음, 결합된 범위를 둘러쌉니다. 기준선 정렬에서는 가장 높은 위쪽과 가장 낮은 아래쪽이 서로 다른 span에 속할 수 있습니다. 따라서 결과 높이가 각 span의 개별 기여분보다 커질 수 있습니다. 부모도 전통적으로 **스트럿(strut)**이라고 부르는, 눈에 보이지 않는 최소 크기 상자를 더합니다. W3C: 줄 높이 계산

기준선은 텍스트를 정렬할 때 사용하는 기준입니다. 줄 높이 기여분의 세로 중간 지점은 아닙니다. 크기가 다른 텍스트에 같은 줄 높이를 지정해도 기준선 위아래의 거리가 같아지지는 않습니다. 초기 vertical-align 값은 baseline입니다. MDN: vertical-align

다음 세 가지 측정값을 구분하세요. 글꼴의 어센트 및 디센트 메트릭, 해당 글자들의 실제 표시 범위, 그리고 줄 레이아웃에 기여하는 공간입니다. span의 배경색이나 선택 영역 사각형을 바깥쪽 줄 상자의 직접 측정값으로 간주하면 안 됩니다. CSS는 인라인 요소가 그려지는 영역과 줄 높이 계산을 구분합니다. W3C: 리딩과 하프 리딩

40px 기여분 두 개가 어떻게 48.4px 줄을 만들 수 있나요?

줄 높이를 명시한 일반적인 기준선 정렬 텍스트에서는 다음 계산이 유용합니다. 글꼴의 어센트와 디센트 합계를 줄 높이에서 빼고, 남은 공간을 위아래에 똑같이 나눕니다. 이 추가 공간을 리딩이라고 하며, 그 절반을 하프 리딩이라고 합니다. CSS Inline Layout Level 3 작업 초안은 이 계산을 설명하며 음수 리딩도 허용합니다. 초안이라는 상태만으로 문서에 포함된 모든 최신 속성의 브라우저 지원이 보장되지는 않습니다. W3C: 레이아웃 경계 계산

다음 입력값은 Arial이나 Mac의 어떤 글꼴을 측정한 값이 아니라 예시입니다. 다른 인라인 콘텐츠나 세로 오프셋이 없고, 부모 스트럿의 기여분이 작은 span과 일치한다고 가정합니다.

기여분 글꼴 어센트 글꼴 디센트 줄 높이 하프 리딩 기준선 위 기준선 아래
큰 span 32px 8px 40px 0px 32px 8px
작은 span 9.6px 2.4px 40px 14px 23.6px 16.4px
부모 스트럿 9.6px 2.4px 40px 14px 23.6px 16.4px

이 기준선 정렬 예시에서는 양쪽 범위에서 각각 가장 큰 값을 취합니다. max(32, 23.6, 23.6) + max(8, 16.4, 16.4) = 48.4px. 각 기여분은 총 40px이지만, 큰 span이 위쪽을 결정하고 작은 span과 부모가 아래쪽을 결정합니다. 이 계산 결과는 브라우저 측정값의 예측이 아니라 문서에 설명된 레이아웃 계산에서 도출한 것입니다. W3C: 인라인 레이아웃 모델

따라서 진단할 때는 **각 경계를 어떤 요소가 결정하는가?**를 물어야 합니다. 작은 span의 줄 높이를 줄여도 부모 스트럿이 같은 아래쪽 기여분을 제공하면 높이는 줄지 않을 수 있습니다.

Mac에서 어떻게 재현하고 검사할 수 있나요?

이 설명에서는 Mac의 Chrome을 사용합니다. 새 탭을 열어 about:blank로 이동한 다음 Command + Option + J를 눌러 Console을 여세요. Chrome은 이 단축키로 JavaScript를 실행할 수 있다고 설명합니다. Chrome DevTools: 개요

아래 전체 프로그램을 빈 탭에서 실행하세요. 같은 레이블을 사용하는 가상 행 세 개를 만들고 측정값을 출력하며, 그림자 루트(shadow root)를 사용해 데모 스타일을 한데 모읍니다. 패키지 설치나 외부 라이브러리는 필요하지 않습니다. 다시 실행하면 이전 데모가 교체됩니다.

(async () => {
  document.getElementById('line-box-demo')?.remove();
  const host = document.createElement('div');
  host.id = 'line-box-demo';
  document.body.append(host);
  const root = host.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = `
    :host { display: block; padding: 24px; color: #222; background: #fff; }
    .label { margin: 20px 0 6px; font: 14px Arial, sans-serif; }
    .row { font: 12px/40px Arial, sans-serif; white-space: nowrap;
      margin: 0; padding: 0; border: 0; background: #e8eef4; }
    .large { font-size: 40px; line-height: 40px; }
    .small { font-size: 12px; line-height: 40px; }
    .trim-span .small, .trim-both .small { line-height: 12px; }
    .trim-both { font-size: 40px; }
  `;
  root.append(style);
  const cases = [
    ['', 'Shared 40px line-height'],
    ['trim-span', 'Smaller span only'],
    ['trim-both', 'Parent and smaller span']
  ];
  const rows = cases.map(([className, label]) => {
    const caption = document.createElement('p');
    caption.className = 'label';
    caption.textContent = label;
    const row = document.createElement('div');
    row.className = `row ${className}`;
    for (const [size, text] of [['large', 'Label A'], ['small', ' Label B']]) {
      const span = document.createElement('span');
      span.className = size;
      span.textContent = text;
      row.append(span);
    }
    root.append(caption, row);
    return { label, row };
  });
  await document.fonts.ready;
  console.table(rows.map(({ label, row }) => ({
    case: label,
    parentLineHeight: getComputedStyle(row).lineHeight,
    largeLineHeight: getComputedStyle(row.children[0]).lineHeight,
    smallLineHeight: getComputedStyle(row.children[1]).lineHeight,
    rowHeight: row.getBoundingClientRect().height
  })));
})();

프로그램은 document.fonts.ready을 기다립니다. 이 값은 문서의 글꼴 로딩과 관련 레이아웃 작업이 완료된 뒤에 확인됩니다. 이후 다른 글꼴을 도입하거나 콘텐츠를 변경하면 해당 변경이 반영된 뒤 다시 측정하세요. MDN: FontFaceSet.ready

세 행을 통제된 비교로 살펴보세요. 첫 번째 행에서는 두 span과 부모 모두 줄 높이가 40px입니다. 두 번째 행에서는 작은 span의 줄 높이만 12px로 줄입니다. 세 번째 행에서는 부모의 글꼴 크기도 40px로 바꿔 스트럿이 큰 텍스트의 글꼴 크기 및 줄 높이와 일치하게 합니다. 두 번째 행은 여전히 높고 세 번째 행은 작아진다면, 비교 결과를 통해 부모의 기여분이 원인임을 확인할 수 있습니다.

예시의 48.4px 값을 기대하기보다 브라우저의 결과를 사용하세요. 프로그램은 getBoundingClientRect()으로 각 행을 측정합니다. 이 메서드는 패딩과 테두리를 포함한 요소의 경계 사각형을 반환합니다. 여기서 행에는 패딩이나 테두리가 없고, 줄바꿈되지 않은 한 줄만 있으며, 높이가 자동으로 정해지므로 그 높이를 줄 상자 높이의 유용한 근사값으로 쓸 수 있습니다. 같은 API로 span을 측정하면 span의 사각형이 측정될 뿐, 바깥쪽 줄 상자가 직접 드러나는 것은 아닙니다. MDN: getBoundingClientRect()

기존 레이아웃에서 무엇을 확인해야 하나요?

해당 텍스트를 Control-클릭하고 Inspect를 선택하세요. Elements에서 두 span과 이를 감싸는 블록을 검사합니다. Styles에서 선언을 추적하고 Computed에서 적용된 값을 확인하세요. 상속된 속성을 검사해야 할 때는 Show All을 켜세요. Chrome은 이러한 보기와 속성 필터를 설명합니다. Chrome DevTools: CSS 기능 참고 자료

각 요소에 대해 font-family, font-size, line-height, display, vertical-align를 기록하세요. 두 span만 살펴보지 말고 부모부터 확인합니다. 그런 다음 다음 순서로 원인을 좁히세요.

  1. 레이아웃 모델을 확인합니다. 자식 요소가 일반 인라인 흐름에 참여하는지, 아니면 flex 또는 grid 항목인지 확인합니다.
  2. 한 줄인지 확인합니다. 줄바꿈으로 측정이 혼란스러워지지 않도록 넓은 테스트 영역과 짧은 가상 레이블을 사용합니다.
  3. 정렬을 확인합니다. 위 계산을 적용하기 전에 기준선 이동이나 기본값이 아닌 세로 정렬을 일시적으로 제거합니다.
  4. 기여 요소를 하나씩 바꿉니다. 작은 span의 줄 높이를 줄여 측정한 다음, 부모의 글꼴 및 줄 높이 기여분을 따로 확인합니다.
  5. 실제 조건을 복원합니다. 원인을 찾은 뒤 의도한 글꼴과 사용 가능한 너비, 텍스트로 다시 확인합니다.

상속도 확인하세요. 단위 없는 line-height: 1.2는 배수를 전달하고, 각 자손 요소는 자신의 글꼴 크기에 그 배수를 적용합니다. 40px 같은 길이 값은 재정의되지 않는 한 고정 길이를 전달합니다. 백분율은 선언한 요소의 글꼴 크기를 기준으로 계산됩니다. 이런 값 설정으로 작은 텍스트에 예상보다 넉넉한 리딩이 생길 수 있습니다. normal는 글꼴 및 브라우저에 따라 달라지므로 고정된 수치로 가정하기에 적합하지 않습니다. MDN: line-height 값과 상속

의도한 결과에 맞는 조정 방법은 무엇인가요?

두 레이블이 무엇을 나타내는지에 따라 조정 방법을 선택하세요. 별도의 레이블로 구성된 간결한 헤더와 줄바꿈되는 문단 안에 포함된 단어는 요구 사항이 다릅니다.

의도한 결과 시도할 조정 방법 확인할 사항
텍스트의 기준선을 공유 작은 span에 적절한 줄 높이를 지정하고 부모의 글꼴 메트릭을 본문 텍스트에 맞춥니다 부모 스트럿이 원치 않는 아래쪽 범위를 더하지 않는지 확인합니다
별도 레이블의 위쪽을 맞춤 두 인라인 span에 vertical-align: top을 적용해 봅니다 바뀐 기준선 관계가 디자인에 맞는지 확인합니다
행 안에서 별도 레이블을 가운데 정렬 align-items: center를 사용하는 flex 컨테이너를 사용합니다 좁은 너비에서의 동작과 항목 줄바꿈이 레이아웃에 적합한지 확인합니다
문단의 연속적인 흐름을 유지 인라인 텍스트를 유지하고 상속되는 줄 높이 값을 의도적으로 지정합니다 결과로 생기는 모든 줄이 혼합 텍스트를 수용하는지 확인합니다

vertical-align: top은 요소와 그 자손을 줄의 위쪽에 맞춥니다. middle의 정의는 다릅니다. 부모 기준선에 부모 x 높이의 절반을 더한 위치에 요소의 중간점을 맞춥니다. 따라서 행 전체의 기하학적 중앙에 맞춘다는 뜻은 아니며, 정렬을 바꿔도 최종 높이가 특정 값으로 정해진다고 보장할 수 없습니다. MDN: vertical-align 값

서로 독립된 두 레이블에는 flexbox의 align-items를 사용해 교차축 정렬을 명시할 수 있습니다. 가운데 정렬과 기준선 정렬은 서로 다른 디자인 선택입니다. flexbox로 전환하면 레이아웃 컨텍스트가 바뀌므로, 레이블이 별도 항목일 때 사용하고 좁은 너비에서도 결과를 살펴보세요. MDN: align-items

줄 높이를 낮추면 텍스트가 레이아웃 기여 범위를 넘어 그려질 수도 있습니다. 간격을 좁힌 뒤에는 주변 줄을 확인하세요. 측정된 상자가 작아졌다는 사실만으로 텍스트가 겹치지 않고 들어간다고 단정할 수는 없습니다. W3C: 리딩과 하프 리딩

캔버스 측정값이 브라우저의 줄 레이아웃을 대체할 수 있나요?

캔버스 측정은 관련된 질문에 답합니다. WHATWG는 measureText()을 정의하면서 글꼴 어센트 및 디센트 메트릭과 해당 텍스트의 실제 경계 범위를 구분합니다. 이 측정값은 캔버스 그리기에 유용하지만, 잉크 경계 측정에는 주변 DOM 부모의 스트럿, 인라인 정렬, 줄 높이 기여분이 반영되지 않습니다. 이 행의 높이를 예측할 때 이것만 사용하지 마세요. WHATWG: TextMetrics

실험할 때 텍스트 셰이핑 경계를 유지하세요. CSS에서는 실질적인 서식 변경이 없다면 인라인 경계를 넘어 셰이핑을 이어가야 합니다. 반면 일부 경계(비초기 세로 정렬 등)에서는 셰이핑이 끊겨야 합니다. 측정하기 위해 글자를 하나씩 감싸지 마세요. 자소 클러스터나 합자를 분리하면 일관되지 않은 결과가 나올 수 있습니다. 완전한 레이블이나 의미 있는 텍스트 구간을 그대로 유지하세요. W3C: 요소 경계에 걸친 텍스트 처리 및 셰이핑

추가 높이를 해결하려면 부모를 포함해 위쪽 경계와 아래쪽 경계를 각각 결정하는 요소를 파악하세요. 해당 요소의 줄 높이 기여분을 조정하거나 레이블의 목적에 맞는 정렬 모델을 선택한 다음, 의도한 텍스트와 글꼴을 사용해 전체 행을 다시 측정합니다.

출처