아카이브 · 당시 기준

2026-09-13 원본에서 평가 계약·구현·코퍼스가 달라진 것을 확인해 당시 기록으로 보관했습니다. 본문과 자동 동기화 표는 보관 당시 내용입니다.

현재 기준과 원본 보기 → · 보관 목록

← 미리디 아카이브

내용시각화 이슈 정의 · 담당자 지도 · 작업 항목 2026-08-30 코드 실측

내용 대치 후 요소 재배치

내용 대치를 '병합'으로 하면 정렬이 깨진다. 원인이 두 층이고 해결 레이어가 두 갈래라 담당자가 갈린다 — 어디서 깨지는지를 먼저 판정하면 물어볼 사람이 자동으로 정해진다. 그리고 이 문제의 정면 해결은 이미 MOR-2018에서 진행 중이다.

이 판(2026-08-30)은 위키의 「내용 대치 후 텍스트 요소 재배치 이슈 정의」를 합쳐 범위(§1) · 개입 지점(§3) · 렌더 왕복 계약(§4) · 용어(부록)을 더했다. 위키 답변 중 코드가 이미 달라진 세 곳은 코드를 적고 어긋난 사실을 표시했다.

miricanvas-iui comp-sign MOR-2018 post-replace-relayout wiki 이슈 정의 ailabs SmartBlock
§1

문제 정의와 범위

정의

내용 대치 이후, 텍스트 가독성이 있는(넘침 · 부적절한 줄바꿈 · 과도한 축소가 없는) 디자인 결과물을 생성한다.

여기서 내용 대치는 컴포넌트의 기존 박스는 그대로 두고 그 안의 글자만 바꾸는 동작이다. 원본 문구(예: 8자)보다 긴 문구(예: 11자)가 들어가면 컨테이너가 수직 방향으로만 늘어나고, 렌더러가 폰트를 축소 하한(기본 원래 크기의 70%)까지 줄이거나, 그래도 안 되면 줄이 늘어나 박스·배경 도형을 넘어간다.

컨테이너는 텍스트 요소가 그려지는 배경 도형을 가리킨다 — 텍스트 요소의 bounding box 를 품고 있어야 한다. 용어가 서로 겹쳐 헷갈리므로 부록 「용어」에 따로 정리했다.

문제의 본질

전체 공간이 부족한 게 아니라 그 공간의 분배가 안 된다는 것이다. 그래서 해법도 "더 줄이기"가 아니라 "남는 여백을 누구에게 줄지 정하기"가 된다.

범위

구분대상 요소무엇을 건드리나
Scope 1 이번 ElementType.Text 텍스트의 컨테이너 크기, 그리고 요소·컨테이너의 위치
Scope 2 후속 ElementType.Image · Chart 등 대치 이후 심미적 완결성이 있는 thumbnail 까지
제외 격자(grid) 노드 텍스트 용량 수요 계산에서 빠지고 대치·레이아웃 규칙이 달라 Scope 1 에서 뺀다

Scope 1 에서 이미지·차트 등 다른 요소의 크기는 변형하지 않는다 — 텍스트 컨테이너만 조정한다.

범위 밖 — 남이 이미 하고 있거나, 이번엔 안 하는 것

1

컨테이너 크기를 유지한 채 안에서 조절하는 것

컨테이너 크기는 그대로 두고 텍스트 크기·줄바꿈·행간을 조절하고 컨테이너 위치만 옮기는 접근. 이석님이 이미 작업한 영역이다 (UNICORN-78825).

그 작업은 렌더 전에 텍스트 배치 예측값으로 배치한다 — 예측이 어려워 결과가 정확하지 않다. 그리고 컨테이너 위치는 옮겼지만 크기는 유지했기 때문에 ㉠ 텍스트의 과도 축소에 대응하기 어렵고 ㉡ 가용 여백을 쓰는 데 제약이 있다. 이번 작업이 크기까지 손대는 이유가 여기다.

2

엔진 단에서 하고 있는 배치 최적화 루트

렌더러(텍스트 흘리기 · 폰트 오토핏 · 재레이아웃)는 우리 통제 밖이다. 그 안을 고치는 경로는 이 이슈가 다루지 않는다 — 다만 성능 개선을 위해 나중에 연결점을 찾는다(§8-D).

3

요소 배치의 재창조 — 순서·위상을 바꾸는 것

배치 순서(alignment-direction)를 바꾸거나, 요소 사이의 위상(topology)을 바꾸는 것(리플로우)은 범위 밖이다. 추후 고려할 가능성은 열어 둔다.

출처 — 위키 「내용 대치 후 텍스트 요소 재배치 이슈 정의」 · MOR-2018

§2

원인 두 층

'병합'은 렌더링 장치가 아니다. 검색용 쿼리 트리를 한 단계 접는 변형이고, 접은 만큼 점수를 깎는다. 그림을 고쳐 주는 코드가 아니라 후보를 더 찾아 주는 코드다.

① 병합 대치 시 정렬 깨짐
창석님(강창석) 요청 · "스마트블록으로 가능?"
② 병합 = 검색 재현율 트릭
자식을 한 leaf 로 접어 재검색 · 감점 mergeStep
③ 근본원인: 구조 재배치가 없다
대치 = 텍스트 주입 + 박스 보정만 → 없는 계층을 만들어 내지 못한다

② 병합이 실제로 하는 일 — 코드

병합은 시그니처 트리를 접는 연산이고, 접힌 단계 수만큼 점수에서 빠진다.

한 단계 자식 병합 comp-match/matching.ts:459
/**
 * 한 단계 자식 병합. 현재 최대 leaf depth 의 형제 그룹 / 외동 leaf 를
 * 모두 한 번에 병합해 하나의 새 forest 를 만든다.
 * ...
 * 형제 병합할 그룹이 없는 depth ≤1 트리는 null.
 */
병합 감점 — root 에 가까울수록 크게 comp-match/scoring.ts:456
// 병합 감점은 root 에 가까운 단계일수록 크게: 1+2+...+step = step(step+1)/2.
const mergeWeight = (args.mergeStep * (args.mergeStep + 1)) / 2;

그래서 "스마트블록으로 가능?"의 답은

병합은 검색 단계의 연산이라 정렬을 고칠 수단이 애초에 없다. 정렬은 그 뒤 단계(박스 보정 · 재배치)의 일이고, 그 단계에 지금 무엇이 있는지가 §5다.

③ 대치가 실제로 넘기는 것 — 코드

대치는 렌더러에 ThumbnailLoad 한 덩어리를 넘긴다. 그 안에 구조를 바꾸는 필드가 없다 — 글자(nodeTextMap)와 박스(nodeBboxMap)뿐이다.

buildContentReplacement 반환 build-replacement.ts:109-168
return {
  type: 'COMPONENT_THUMBNAIL',
  sheetJson,           // 후보 원본 — 구조 그대로
  nodeTextMap,         // 글자 주입
  nodeBboxMap,         // 박스 보정 (여기가 전부다)
  siblingGroups: buildSiblingGroups(textMap, sheetJson),
  ...(rlsc ? { rlsc } : {}),
};

박스 보정 토글 네 개 — 기본값이 중요하다

토글하는 일기본값
expandText텍스트 노드를 형제·부모와 겹치지 않는 선에서 가로 확장false
relieveTopBias위로 쏠림 완화 — 글씨 절반만큼 아래로 이동true
ensureBgMargin텍스트 박스를 배경 도형 경계 안쪽으로 clamptrue
tidyLayout 공간 정돈force-directed 시뮬레이션으로 box 일괄 조정 — 켜지면 위 셋을 대체false

근거: build-replacement.ts:54-70 · 즉 정렬 개선 수단(tidyLayout)은 기본으로 꺼져 있다. "왜 정렬이 안 맞나"의 첫 번째 답이 여기일 수 있다.

파이프라인 상 원인 — 여백을 못 나누는 세 층

위 둘이 "왜 병합으로는 못 고치나"의 답이라면, 아래 셋은 대치 파이프라인 자체가 왜 여백을 못 나누나의 답이다. 세 층이 각각 다른 곳에 있어서 한 곳만 고쳐서는 안 풀린다.

원인어디서설명
박스가 원본 텍스트 기준으로 고정 design_objects.design_object_sheet_json 컴포넌트 제작 시 원본 문구 길이에 맞춰 디자이너가 잡아 둔 박스다
대치 전 보정이 노드 단위 국소 보정 helpers.ts:819 buildNodeBboxMap 각 노드를 개별적으로 조금 밀거나 clamp 할 뿐, "이 페이지의 남는 여백을 누구에게 줄지"를 못 정한다
축소가 마지막 방어선 types.ts:222 fontScaleRange 기본 { min: 0.7, max: 1 } — 최대 30%까지 줄여 흡수하고, 그래도 안 되면 넘친다

⚠ 문서와 코드가 어긋난 곳 — 위키·핸드오프 자료는 buildNodeBboxMap 을 helpers.ts:685 로 적지만, 현재 코드에서는 :819 다. fontScaleRange 기본값 {min:0.7, max:1} 은 코드와 일치한다.

유사 선례 — "정렬 최적화"가 실제로 어떤 버그였나

전부 다른 티켓이지만, 대치 이후 정렬이 깨질 때 어떤 모습으로 나타나는지의 표본이다. 새 케이스를 분류할 때(§8 B-3 #11) 대조군으로 쓸 수 있다.

티켓증상
UNICORN-70945불렛 적용 시 원본 정렬(중앙/좌/우) 유지 규칙
UNICORN-70809특정 표에서 내용 대치 시 내용이 전부 상단에 붙어버림
UNICORN-70728컴포넌트 추가 시 텍스트 top 위치가 틀어짐
UNICORN-78435 · 74379줄바꿈이 생긴 열의 너비 계산 문제
UNICORN-72202내용 대치 시 불렛이 작아지는 현상
§3

개입 지점 세 곳 — 왜 C인가

대치 파이프라인에서 우리가 손댈 수 있는 자리는 세 곳뿐이다. 기존 보정도, 실패한 tidyLayout 도 전부 A 에 있다. 이 이슈가 다루는 것은 C — 1차 렌더 후, 최종 렌더 직전이다.

【내용 대치】 buildContentReplacement()
out: { sheetJson, nodeTextMap, nodeBboxMap, siblingGroups }
◀ 개입 지점 A — "이 노드는 이 박스로 그려라"
【렌더】 preview / 내장 렌더 호스트
실제 텍스트 흘리기 + 폰트 오토핏 + 재레이아웃 (브라우저 전용)
◀ 개입 지점 B — fontScaleRange
렌더된 sheetJson (= 대치된 문서)
COMPONENT_THUMBNAIL_LOADED.pageJson
◀ 개입 지점 C — 렌더 후 후처리 · 이 이슈
composite → page.webp
최종 산출물
핸드오프 자료 docs/post-replace-relayout-handoff.md §2 의 파이프라인 지도를 옮긴 것.
지점방법장점단점
A렌더 전 nodeBboxMap 대치 전에 박스를 다시 계산해 넘긴다 렌더러가 알아서 흘려 준다. 기존 보정·tidyLayout 이 모두 여기 실제 텍스트가 어떻게 흐를지는 예측(predictReplacedTextBbox)에 의존
BfontScaleRange 축소 하한·상한 조정한 줄 근본 해결이 아니다. 하한을 낮추면 더 작아질 뿐
C 이 이슈렌더 후 sheetJson 렌더 결과를 보고 재배치한 뒤 다시 렌더 실제 흐른 결과(줄 수·넘침·축소)를 관측하고 판단한다 왕복 2회. 반복 수렴 설계가 필요

A 는 "예측 기반 1-pass", C 는 "관측 기반 iterative". 선행 실험(tidyLayout)이 실패한 이유가 렌더 결과가 아니라 예측치로 배치했기 때문이라는 것이 C를 고른 근거다.

"재배치"의 정의 — 무엇을 바꾸는가

텍스트 요소를 포함한 컨테이너의 두 가지를 보정한다

  • (1) 절대 좌표값 — x, y
  • (2) 크기 — width, height

실제 필드는 node.data.props.boundingBox = { left, top, width, height } 다. node.data.boundingBox 가 아니다 — 자주 틀리는 지점.

진행 순서 — 지표가 먼저다

선행 실험이 실패한 원인이 "예측치로 배치"였다면, 실패를 실패라고 부를 자가 없었다는 것이 두 번째 원인이다. 그래서 지표 정의가 1순위다.

1

정량 평가 지표부터 정의 선행 필수

넘침 노드 수 · 최소 폰트 배율 · 정렬선(가이드) 보존율 등. 렌더된 sheetJson 에서 계산해야 한다 — 예측치로 재면 같은 실패를 반복한다. 구체 기준은 TC 채점 로직 과 가독성 지표 문서에 있다.

2

평가용 데이터셋 구축 — 네 벌

전부 컴포넌트 매칭 이후, 내용 대치가 완료된 것이어야 한다.

original (대치 전 원본) · before (대치 직후) · after (before + 재배치 알고리즘) · expected (사람이 보기에 optimal 한 정답)

⚠ expected 는 담당자 주관이 들어간 결과라 제3자 리뷰가 필요하다 — 이것이 §8 B-0(채점 방식 결정) 이 최우선인 이유다. thumbnail 은 화면 확인용일 뿐이라 없어도 될 수 있다.

3

기준선(baseline) 측정

현재 기본 on 인 국소 보정(ensureBgMargin · expandText)의 지표를 먼저 재서 개선의 기준으로 삼는다.

4

재배치 알고리즘 프로토타입

렌더링 파이프라인을 두 번 거치는 비효율이 있지만, 일단 결과를 얻기 위해 그 방법으로 구현한다. 성능 개선·다른 방법 도출은 그 뒤다.

5

재사용 자산 — 실패한 것은 "전역 수렴 접근"이지 부품이 아니다

아래 넷은 그대로 쓴다.

부품무엇어디
buildVisualTreeRebased문서를 절대 AABB 트리로 정규화 — 형제·조상 판정의 단일 출처helpers.ts:103
textPredictInputsFromNode
predictReplacedTextBbox
대치 텍스트 박스 예측textPredict.ts:71 · :84
배경 도형 식별 · 가이드 선 그룹핑"원래 정렬돼 있던 변들" 탐지 — 구조·균형 유지 요구에 직접 대응tidyLayout.ts
TidyLayoutDebugModal재배치가 무엇을 하는지 눈으로 보는 디버그 도구 — 새 알고리즘도 프레임만 흘려주면 재사용TidyLayoutDebugModal.tsx

위험

알고 시작하는 네 가지

  • 완성 기간을 못 재고 있다 — 온보딩 이후 첫 업무라 계획보다 지연될 가능성이 있다.
  • 난이도가 이미 검증된 영역 — 선행 실험이 실패로 판정됐다. 정렬(가이드)과 확장(텍스트·배경)이 서로 반대 방향으로 당기는 구조라 단순 가중치 튜닝으로는 수렴하지 않을 수 있다.
  • 렌더 왕복 2회의 비용 — 처리 비용과 수렴 반복 설계 부담.
  • 지표를 잘못 잡으면 다시 수렴 실패 — 그래서 지표 정의가 1순위다.
§4

렌더 왕복으로 무엇을 관측하나

C(렌더 후 후처리)가 성립하려면 1차 렌더 결과를 코드로 읽어올 수 있어야 한다. 결론부터 — 가능하고, 이미 도는 코드가 있다. 다만 무엇을 실측이라 부를 것인가에서 위키 답변과 현재 코드가 갈린다.

메시지 계약

preview iframe 과 주고받는 네 개 comp-match/types.ts:210-249
parent → iframe   COMPONENT_THUMBNAIL_PING
iframe → parent   COMPONENT_THUMBNAIL_READY
parent → iframe   COMPONENT_THUMBNAIL        { sheetJson, nodeTextMap, nodeBboxMap?, siblingGroups?, fontScaleRange?, rlsc? }
iframe → parent   COMPONENT_THUMBNAIL_LOADED { pageJson?: Doc }   ← 이게 렌더 결과

'COMPONENT_THUMBNAIL*' 의 wire 문자열은 iframe 쪽 코드와 공유한다 — 바꾸면 안 된다. 노드 id 는 원본과 같아서 원본 doc 과 노드 단위로 대조할 수 있다.

이미 쓰고 있는 경로

용도코드메모
헤드리스(Playwright) 추출comp-match-eval-render.ts:360renderViaPreview() — payload 를 던지고 pageJson 을 받는다
앱(브라우저) 안에서Thumbnail.tsx:314setRenderedDoc(e.data.pageJson) — hover 강조 박스 좌표로 쓴다
재배치 뷰어의 라운드트립viewer/DocIframe.tsx:218추출한 sheetJson 을 nodeTextMap 을 비운 채 다시 넣어 렌더(텍스트 재대치 방지)
렌더 재검증 프로브 현행viewer/useRenderProbe.tsx화면 밖 iframe 하나에 문서를 여러 번 밀어 넣고 Promise 로 답을 받는다
내장 렌더 호스트 현행doc-render/DocRenderHost.tsx:171onViewLoaded — pageJson 과 함께 tightBoxes 를 실어 보낸다

자세한 것은 엔진 내장 렌더 호스트 문서에 있다.

⚠ 문서와 코드가 갈리는 지점 — pageJson 만으로는 재검증이 안 된다

위키 답변은 pageJson 이 "대치·폰트 오토핏·재레이아웃까지 전부 끝난 sheetJson" 이고 data.props.boundingBox 에 재레이아웃 결과가 반영된다고 적는다. 현재 코드는 그렇지 않다고 말한다.

내장 렌더 경로에서 텍스트 노드의 boundingBox 는 문서에 적힌 값 그대로 돌아온다 — 폭을 60%로 줄여 넣어도 높이가 한 자리도 안 변하는 것이 실측됐고, layout: autoHeight 인 텍스트도 마찬가지였다. 엔진이 재는 것은 뷰 안쪽이고, 잰 값을 노드 props 로 되쓰는 단계가 이 경로에 없다.

그래서 "폭을 열었더니 세 줄이 두 줄이 됐나"를 pageJson 으로 물으면 언제나 「예측대로」라고 답한다 — 조용히 틀리는 종류의 검증이다.

대신 글자 영역(tight bounding box)을 직접 잰다 — COMPONENT_THUMBNAIL_LOADED.tightBoxes. 엔진 계산기 (src/text-tight-box/compute.ts)로 잰 값이고, CLI 포트 (batch/tightBoxProbe.ts)·구워 둔 값(eval-out/cases/<id>/tight/)과 같은 자·같은 좌표 규약(프레임 원점 기준)이라 뷰어와 CLI 가 같은 답을 낸다.

근거: DocRenderHost.tsx:150-171 (onViewLoaded 주석) · useRenderProbe.tsx:21-38. 이 서술이 §8 B-3 #5「렌더러가 실제로 무엇을 바꾸나」 의 답이기도 하다.

주의할 점

1

좌표계가 다시 잡힌다

렌더러가 컴포넌트를 페이지의 80%에 fit 시키고 절대좌표로 내보낸다. 실측 케이스에서 uniform scale ×1.910, 그룹이 (378.8, 416.1, 631.6×427.9) → (356.7, 108, 1206.5×864) 로 갔다. 원본 doc 과 직접 좌표 비교는 안 되고, 정규화하거나 렌더된 doc 끼리만 비교해야 한다. 다행히 fit 은 멱등이라 라운드트립은 안정적이다 — 재입력해도 scale 1.0.

2

rotatedBoundingBox 가 stale 하고 angle/angleType 이 빠진다

Thumbnail.tsx:579 가 preferRotated: renderedDoc == null 로 우회한다. 회전 노드는 rotated 값을 믿으면 안 된다.

3

preview 서버 의존은 뷰어에서 이미 걷어냈다 위키와 다름

위키의 주의 3·4(origin allowlist · 매일 20:00 자동 sleep)는 design-2 의 preview 배포 unicorn-73380 을 띄우던 시절 이야기다. 그 배포는 삭제됐고, 뷰어는 엔진 렌더러를 이 저장소에 내장해 (doc-render/) same-origin iframe 으로 바꿨다. 남의 서버가 켜져 있는지 신경 쓸 일이 없어졌다.

단, CLI 경로는 아직 옮겨지지 않았다 — comp-match-eval-render.ts:39-49 는 여전히 unicorn-73380.preview.miricanvas.com 을 가리키고 COMP_MATCH_EMBEDDER_ORIGIN(기본 https://iui.miricanvas.com)으로 부모 페이지를 위장해 띄운다. 이 경로는 지금 실패한다.

4

재는 iframe 에는 오토핏이 없다

내장 렌더 호스트에는 글자를 줄이는 동작 자체가 없다. 그래서 재는 값은 언제나 「문서에 박힌 글자 크기로 몇 줄」이고, 재검증이 묻는 것도 그 값이다. 폰트 오토핏이 실제로 얼마나 줄이는지는 preview 렌더러 경로에서만 관측된다 — 위키에 적힌 25.732 → 18.012 같은 값이 그쪽 산출물이다. 두 경로를 섞어서 비교하면 안 된다.

§5

해결 레이어 두 갈래

박스가 넘치거나 변이 어긋나는 기하 문제인지, 대상이 담을 수 없는 계층이 필요한 구조 문제인지에 따라 손댈 곳이 다르다.

④a 기하적 정렬
박스 넘침 · 변 어긋남 · 여백 불균형
comp-sign tidyLayout
force-directed 공간 정돈 (이미 있음 · 기본 꺼짐)
홍지혜 · 담당
이 갈래를 맡는다 — miricanvas-iui
이석님 Pod 의 기존 구현은 참고만 한다
④b 구조적 재배치
대상이 못 담는 계층 필요
SmartBlock 생성엔진
DimensionPolicy: resize · 정렬 · gap 재배치
장주영 · AI연구팀
조건부 에스컬레이션
ailabs SmartBlock 생성기 소관일 때
내 라인에서 안 풀릴 때만 장주영님 라인으로 넘어간다. 이석님 Pod 이 이미 만들어 둔 것은 「기존 접근 방법」 절로 따로 뺐다 — 참고만 한다.

⚠ 세 번째 갈래가 있다 — MOR-2018

기존 지도에는 ④a·④b 둘뿐이었지만, 실제로는 이 문제를 정면으로 푸는 세 번째 작업이 이미 돌고 있다 — miricanvas-iui/src/post-replace-relayout/ 의 재배치 후보 다섯 개다. 그리고 그 안에서 tidyLayout 은 후보가 아니라 대조군으로 쓰인다. 자세한 것은 §8-B.

§6

먼저 확인할 것 — 정의 충돌

⚠ "스마트블록"이 두 갈래다

같은 단어가 서로 다른 것을 가리켜서, 어느 쪽을 말하는지에 따라 담당이 갈린다.

A. 이석 Pod · 스마트블록 검색·대치
내용시각화 엔진 내부 (RLSC 기반) · miricanvas-iui + miricanvas-web-2
B. ailabs · SmartBlock 생성기
AI연구팀 독립 파이프라인 (장주영) · 이 머신에 체크아웃 없음
창석님 질문이 A/B 어느 쪽인지 확인하면 담당이 자동 결정된다.
§7

코드 지도 — 어디를 열면 되나

아래 줄 번호는 고정 커밋 기준이라 링크가 그 시점을 가리킨다. 코드가 움직이면 어긋날 수 있으니, 엉뚱한 곳이 뜨면 커밋을 갱신할 것.

miricanvas-iui — 대치·정돈이 실제로 도는 곳

무엇파일 · 줄메모
대치 페이로드 조립build-replacement.ts:109buildContentReplacement() — 여기가 대치의 출구
박스 보정 토글build-replacement.ts:61-70tidyLayout 기본 false
공간 정돈 본체comp-match/tidyLayout.ts815줄 · buildTidyLayoutBboxMap
병합 트리 생성comp-match/matching.ts:459한 단계 자식 병합
병합 감점comp-match/scoring.ts:456step(step+1)/2
재배치 후보 · 지표src/post-replace-relayout/MOR-2018의 구현 대상 · 14,836줄
컨테이너 판정relayout-core/metrics.tsfindTextContainer — 넘침·겹침 전부의 기반
렌더 메시지 계약comp-match/types.ts:210-249PING / READY / THUMBNAIL / LOADED — wire 문자열 고정
내장 렌더 호스트doc-render/DocRenderHost.tsx:171onViewLoaded — pageJson + tightBoxes
렌더 재검증 프로브viewer/useRenderProbe.tsx화면 밖 iframe · Promise 로 실측 회수
글자 영역 실측기src/text-tight-box/compute.tscomputeTightBoxesForDoc — 뷰어·CLI 공용

miricanvas-web-2 — 렌더러 쪽 (블랙박스)

무엇파일메모
대치 4단계buildMappedComponentSheetJson.ts:108내용→폰트→스타일→이미지
SmartBlock 자동 레이아웃AbstractSmartBlockLayoutWithChildrenStrategy.tsFlex · Grid · V1 세 구현
캔버스 반영ComponentItemReplaceCommand.ts대치의 종착점

⚠ 렌더러(텍스트 흘리기 · 폰트 오토핏 · 재레이아웃)는 우리 통제 밖이다. 끄는 레버는 shrinkConfigs 하나뿐이고, 줄바꿈·재레이아웃은 그래도 일어난다.

aippt-prisonbreak

이 문제에 직접 닿는 코드는 없다 — RLSC(structure_json)를 생산하는 쪽이라 대치·정렬 런타임에는 관여하지 않는다. 다만 대치의 판단 근거인 Component RLSC가 여기서 나오므로, "구조가 애초에 잘못 잡혔나"를 의심할 때 거슬러 올라가는 지점이다.

§8

작업 항목 — 지금 열려 있는 것

담당을 가르는 질문(A)과, 그와 별개로 이미 돌고 있는 근본 해결 작업(B)을 나눠 적는다. B는 MOR-2018 의 조사·구현 문서에서 가져왔고, 아래 ↻ 표시가 붙은 표는 원본이 바뀌면 sync-sources.py 로 다시 채워진다.

A. 담당과 에스컬레이션

담당 — 홍지혜

병합 · 대치 · 정돈 갈래(④a)는 홍지혜가 맡는다. 이석님 Pod 이 이미 만들어 둔 접근(tidyLayout · comp-sign 스마트블록 대치)은 「기존 접근 방법」 절로 뺐고 참고만 한다 — 전제로 삼지 않는다. 실제 해결 경로는 아래 B 다.

A-1

장주영 — 생성엔진 영역이면 에스컬레이션

조건부 장주영 · AI연구팀

확인 결과 생성엔진 영역이면 AI연구팀으로 넘긴다. ailabs SmartBlock 생성기 소관일 때만.

A-2

제품 판단 — 우선순위·PRD

상시 김수현 · 정원석

우선순위·PRD는 내용시각화 스쿼드가 갖는다 — 김수현(스쿼드 리더) + 정원석(PM-E), 창석님은 요청자.

한 줄 결론

문제가 나는 지점(병합 · 대치 · 정돈)은 홍지혜가 맡는다. 이석님 Pod 의 기존 구현은 참고 자료이고, 구조 재배치가 필요할 때만 주영님.

B. 근본 해결 — post-replace-relayout (MOR-2018)

재배치 알고리즘의 현재 구조

재배치 알고리즘 파이프라인 v0.7 — method 1~8의 역할과 대표 조합 3→1→2→5→4→7→8→6, 실제 글자 영역을 이용한 이동, 축소 전 검증과 최종 외곽선 보정을 정리했다. 아래의 기존 측정 표는 각 표에 표시된 시점을 기준으로 읽는다.

"내용 대치가 끝난 컴포넌트를 ① 넘침 ② 과도 축소 ③ 여백 낭비가 없도록 재정렬한다." 이 문제를 다루는 현재 구현은 연산자 8개와 이를 조합한 실행 경로로 구성돼 있다.

하위 문서 — 폰트 축소 하한

후보 다섯을 다 돌리고도 남는 FAIL 을 글자를 줄여 풀 수 있나, 줄인다면 어디서 멈춰야 하나. 커밋된 119건 실측과 하한 기준의 결정 (「원안 대비 배율」 vs 「페이지 대비 크기」)을 그 페이지에 따로 뒀다.

B-0

채점 방식 결정 — 다른 모든 작업의 전제

최우선 방법론 결정

정답(expected_result.json)이 스크립트 산출물이라 알고리즘이 같은 절차를 쓰면 채점이 순환한다. 커밋 a0cb02aa 에서 손으로 만든 정답이 스크립트 산출물로 덮였다.

  • (a) 정답을 사람이 다시 만든다 — 비용 크지만 채점이 뜻을 갖는다
  • (b) 절대 지표로만 채점 — 넘침·유휴 여백·겹침만. "얼마나 예쁜가"는 못 잰다
  • (c) 정답을 참고 답안으로 격하 — 정량 게이트가 사라진다

현재 코드는 (b)에 가깝게 이동했다 — README가 채점 기준을 original.json(대치 이전 원안)으로 바꿨다. 문서(w3 §5)와 코드의 결론이 갈리는 지점이라 확인이 필요하다.

B-1

후보 다섯 + 합본 넷 — 구현 완료, 실측 진행 중

구현됨 miricanvas-iui
  • method1 — 세로 공간 재분배. 폭 고정이라 예측 0
  • method2 — 가로 확장. 줄 수를 줄인다 · 줄바꿈 예측이 들어간다
  • method3 — 가로 넘침 해소. 도형을 좌우로 키우고, 막히면 텍스트를 안쪽 폭으로 좁힌다
  • method4 — 대치가 만든 겹침 해소 (유닛 사이) · 크기 고정 · 예측 0
  • method5 — 유닛 안의 겹침 해소. 한 배경 도형 안에서 글자끼리 포개진 것

읽는 법 — 넘침을 줄이는 일은 합본 3+1+2 에서 끝난다 (4,107,470 → 534,454px², 87%). 게이트(신규 겹침)는 +4·+5+4 가 따로 푼다. tidy 열이 대조군이다 — 게이트 34/119로 before(35)보다도 낮다.

열 넘침 노드(H/V) 넘침 면적 게이트 PASS 폰트 배율(평균) 정렬선(평균)
original(기준)11 (8/9)129,625px²기준1.0001.000
before97 (63/93)3,079,999px²22/851.0000.924
method148 (43/44)1,839,292px²26/851.0000.897
method268 (55/65)2,387,056px²30/851.0000.879
method389 (54/88)2,410,296px²23/851.0000.909
합본 1+240 (37/38)1,599,446px²51/851.0000.846
합본 3+1+232 (28/31)911,461px²51/851.0000.833
합본 3+1+2+432 (28/31)911,461px²59/851.0000.820
합본 3+1+2+5+432 (28/31)911,461px²63/851.0000.817
합본 3+1+2+5+4+630 (26/30)744,726px²69/851.0000.813
합본 …+5+4+7+630 (26/30)744,726px²76/851.0000.809
합본 …+5+4+8+630 (26/30)744,726px²70/851.0000.810
합본 …+5+4+7+8+630 (26/30)744,726px²77/851.0000.806
tidy33 (18/29)1,213,757px²56/851.0000.887
expected(목표선)3 (3/3)52,676px²37/410.9850.843

↻ post-replace-relayout 후보별 실측 — 원본에서 자동 동기화

⚠ 이 표는 갱신이 멈춰 있다 — 원본(src/post-replace-relayout/README.md)의 채점표가 후보별 119건 비교에서 100건 SUCCESS·ACCEPTABLE·FAIL 요약으로 바뀌면서 sync-sources.py 가 절을 못 찾는다(2026-08-30 확인). 최신 성적은 재배치 알고리즘 파이프라인 v0.7 에 있다 — 채점 대상 88건 중 SUCCESS 70(79.5%) · ACCEPTABLE 16(18.2%) · FAIL 2(2.3%). 위 표는 연산자별 기여도를 보는 용도로만 읽는다.

B-2

착수 순서 — 공용 기반 먼저, method2 는 조건부

진행 MOR-2018 w3 §4.4

예비 실측에서 커밋된 7건 전부가 세로 여유만으로 부족분을 덮었다 — 가로 넘침 0건. 그래서 method2 는 method1 이 실패하는 케이스를 찾은 뒤에 착수한다는 것이 원래 계획이었다. (실제로는 method3 이 필요해지면서 계획이 한 번 갈렸다 — B-1 참고)

# 항목 어디에 규모
0채점 방식 결정 (§5)—먼저. 이게 없으면 비교 자체가 무의미
1tight box를 판정 기준으로 붙이기공용2일
2위상 동결 — 관계 그래프 추출공용3일
3Abacus PlaceRow + 스택 추출공용4일
4여백 대수(1D, 닫힌 형식)method12일
5최대 빈 사각형 열거method1 (+ 합본의 분기 기준)3일
6method1 완성 → 5건 채점—
7DocIframe이 pageJson 반환하도록 수정공용1일
8최소구성 + 자기 캘리브레이션method2 — 조건부4일
9제약 그래프 여유 + 실현가능성method2 — 조건부1주
10method2 완성 → 채점 → §4.3 판정—

↻ 착수 순서 — 원본에서 자동 동기화

B-3

남은 확인 필요 항목 11건

미해결 MOR-2018 w3 §8

이 중 #1 채점 방식은 B-0이고, #6 폰트 복원·#7 행간 변경은 제품 결정이라 엔지니어링으로 못 넘긴다 — B-4.

# 항목 무엇을 막나 상태
1채점 방식 결정 (§5의 a/b/c)정답이 스크립트 산출물이라 지금은 채점이 성립하지 않는다. 다른 모든 작업의 전제최우선, 방법론 결정
2wrapBlock 오차 2건(78/80)의 정체 — CJK 금칙? 긴 라틴 토큰?4위 전체의 전제미측정
3tight box 굽기의 프로덕션 비용 (headless Chrome, 문서당)5위 [전] 적용 가능 여부미측정
4여백 균등 분배 vs 비율 분배2위의 유일한 설계 미결케이스로 확인 가능
5렌더러가 실제로 무엇을 바꾸나 — 특히 컨테이너를 키우면 폰트를 되돌리나5위 [후]의 "뜻밖의 이득" 가설pageJson을 버리고 있어 아무도 안 봄
6폰트 복원 권한 (original.json 사용 허용)§6.1 — 문제유형 ②를 다룰 수 있느냐제품 결정
7행간(lineSpacing) 변경 허용§6.2 — 가장 싼 세로 회수 수단제품 결정
8장식 요소 필터링의 정확도3위의 거짓 양수 사이클DECORATION_TYPES 재사용 검증
9기요틴 분해율 (샘플 ≥500)§6.4 계측기 착수w2에서 이월, 미측정
10케이스를 7건보다 늘려 method1이 실패하는 사례 찾기§4.3.1 실측이 7건 전부 "세로로 해결"이라 method2를 만들 근거가 아직 없다. 반례가 없으면 만들지 않는다미착수
11case.json의 problems 라벨 채우기 (7건 중 6건이 비어 있음)케이스가 어떤 증상인지 사람이 분류한 기록이 없어 교차 검증이 불가능사람이 이미지를 봐야 함

↻ 남은 확인 필요 항목 — 원본에서 자동 동기화

⚠ 테스트가 0개다 — wrapBlock·boxHeightOf·rendererLineCount 는 method 4·5위의 기반이고 findContainer·overflowOf 는 전부의 기반이라, 여기가 조용히 깨지면 전부 깨진다. (문서 기준. 현재 코드에는 relayout-core/ 아래 테스트 파일이 다수 있어 재확인 필요)

B-4

제품 결정 대기 2건 — 규칙 개정이 필요하다

대기 제품 판단 (A-2)
  • 폰트 복원 권한 — original.json(대치 전 원안)의 글자 크기로 되돌려도 되나. 허용하지 않으면 문제유형 ②(과도 축소)를 아예 다룰 수 없다.
  • 행간(lineSpacing) 변경 허용 — 현행 규칙(before-after-rules.md §2)이 금지한다. 하지만 행간 −20%는 세로 20%를 회수하면서 글자 크기 손실이 0이라 가장 싼 세로 회수 수단이다 (폰트 0.894와 동일한 면적 효과).

이 둘은 A-2(김수현·정원석) 이 답할 항목이다 — 엔지니어링 선택이 아니라 품질 정책이다.

B-5

확정된 결정 — 되돌리려면 근거가 필요하다

확정 MOR-2018 w2 §10
  • 결정론이 요구사항이다 — LLM·VLM 호출, 학습된 생성 모델, SA·force-directed 전부 채점 대상에서 제외. 같은 입력이 같은 출력을 내야 회귀 테스트가 성립한다. 이 결정이 tidyLayout(force-directed)을 후보가 아니라 대조군으로 만든 이유다.
  • 비겹침은 하드 제약 — tidyLayout 실측에서 5건 중 2건이 신규 겹침 게이트 FAIL, 1건은 중앙 오차 26.1 → 87.4. 소프트로 다루면 안 된다는 우리 데이터.
  • 연산 순서 — "크기 먼저 vs 위치 먼저"를 채택하지 않는다. 위상 고정 → 크기·위치 동시 → 여백 흡수 마지막.
  • 목적함수는 이차(QP) 필수, LP 금지 — 선형 목적함수는 slack을 한 변수에 몰아 "요소가 한쪽으로 치우침" 증상을 수학적으로 재현한다.
날짜 구분 항목 내용 반영 위치
2026-08-20확정Method 1 착수 승인w2 §3 "크기·위치 동시" 원칙과 모순 없음. 폭 동결 시 높이는 자유변수가 아니라 실측값 — 최소 섭동 목적 하에서 h_i* = h_실측,i가 조인트 최적임이 증명됨(h를 더 키우면 자기 페널티↑ + y 실현가능 영역↓). 잔여 (y, θ) 조인트 QP는 PlaceRow/여백대수가 정확해를 냄. Method 1 = 예측 0 부분공간으로 제한한 조인트 솔브이지 grow-then-move 휴리스틱이 아님. w2의 경고는 폭이 자유변수여서 피드백 루프가 있을 때의 얘기(→ Method 2 영역)w3 §4
2026-08-20확정갭 분배는 델타 균등(닫힌 형식)method1 노트 §5 장난감 예제의 완전 균등(55×4)과 w3 2위 닫힌 형식 θ_j = θ^orig + Δ/m(→ 25/45/25/125)이 불일치. 후자가 디자이너 여백 위계를 보존하므로 구현 기준. 완전 균등화는 idle-margin용 별도 플래그로 분리 채점w3 2위
2026-08-20권고착수 순서 #7을 #6 앞으로Method 1의 "예측 0"에 숨은 피드백 채널 = 렌더러 오토핏(박스 확대 → 폰트 복원 → 텍스트 높이 변화). h_실측은 현재 fontSizeScale에서 잰 값이라 최종 렌더와 어긋날 수 있음(w3 §8-5 "아무도 안 봄"). pageJson 회수(1일)를 m1 채점 전에 넣어 5건 채점을 render-verified로. "뜻밖의 이득"(증상 ② 간접 해결) 가설도 같은 실험에서 확인됨 — m1 vs m2 판정에 영향w3 §4.4, §8-5
2026-08-20권고m1 1차 채점은 보수 모드확대 + 법제화 + 델타 분배만. 미학적 채움·전면 균등화 제외 — §4.3 축 판정의 귀속(attribution) 보전w3 §4.3
2026-08-20권고배경 도형은 이동 결합이 아니라 크기 결합텍스트 박스가 자라면 배경 도형도 padding 보존으로 커져야 Rule.md §0 overlap 요구 유지. 재줄바꿈 없으므로 예측 0w3 1위 필요조건

↻ Method 1 착수 전 재검토 결정 — 원본에서 자동 동기화

C. 참고 — MOR-2018 문서 지도

이 아카이브 표시가 붙은 둘은 이 사이트로 옮겨 왔다 — 사이드바에서 이 문서의 하위로 들어가 있고, 슬라이더·시뮬레이터도 원본 그대로 돈다. 나머지는 MOR-2018/ 안의 원본이다.

문서무엇
Rule.mdBefore/After 정의 규칙 · 불변 조건 · 문제 유형 ①②③
research/relayout-research_w2.md문헌 조사 정본 · 결정 로그(§10)
research/relayout-research_w3.md순수 알고리즘 Top 5 · 조립 계획(§4) · 남은 항목(§8)
post-replace-relayout_method1-method2-notes.mdMethod 1/2 직관 설명 · 장난감 예제
logics/find-text-container.mdfindTextContainer 판정 3순위 해부
logics/overlap-area-map.mdoverlapAreaMap · 신규 겹침 게이트
overlapAreaMap 겹침 게이트 이 아카이브지표 ⑥ 의 2단계 알고리즘 · 제외 규칙 다섯 · 판정 시뮬레이터. 원본 overlap-gate-notes.html
TC/ · 예시/테스트 케이스 sheetJson · Before/After 예시 이미지

logics/ 의 로직 문서는 사내에 배포되어 있다 — post-replace-relayout-key-func.miridih.app (VPN).

D. 이후 계획 — 지금은 안 하지만 적어 두는 것

D-1

렌더 왕복 2회를 한 번으로 — 엔진 합성 경로와 연결

탐색 엔진팀 의존

재배치 알고리즘을 적용한 뒤 엔진팀 구현과 이어 붙일 최적화 여지가 있는지 탐색이 필요하다. 아이디에이션 수준의 후보: miricanvas-iui/src/style-mapper/render-page.ts 의 requestComposite 만 호출하는 방향.

⚠ 용어 주의 — 여기서 합성(Composite) 은 헤드리스 Chromium 을 블랙박스로 써서 디자인을 이미지·PDF·PPTX·영상으로 만들어 내는 서버 사이드 잡 파이프라인 전체를 말한다. 브라우저 엔진이 말하는 "Composite" 과 다른 말이다 (결국 그것을 포함하기는 한다).

D-2

텍스트 바운딩박스 측정을 pretext 방식으로 전환

검토 성능

재배치 후 렌더링 파이프라인 진행 과정의 성능 최적화 수단으로 검토한다. (관련: Canvas 위키 2697266052)

D-3

Scope 확장 · 스마트블록 응용

후속 범위

§1 의 Scope 2(Image·Chart)로 넓히는 것, 그리고 스마트블록에 응용하는 것.

D-4

표·차트 프리셋을 후속 범위로 언제 넣나

미결 범위 결정

1차 범위에서는 제외돼 있다. 언제 포함할지가 아직 안 정해졌다.

참고

기존 접근 방법 — 이석님 Pod 이 이미 만들어 둔 것

이 절은 참고용이다

아래는 이석님 Pod 이 이미 구현해 둔 접근이다. 담당이 홍지혜로 넘어왔으므로 이번 작업의 전제로 삼지 않는다 — 무엇이 이미 있고 어디까지 커버되는지 알아 두는 용도로만 읽는다.

실제 해결 경로는 §8-B(post-replace-relayout) 다. 그 안에서 tidyLayout 은 후보가 아니라 대조군으로 쓰인다.

tidyLayout 은 무엇인가 — 힘 일곱 개의 물리 시뮬레이션

각 노드의 bbox 네 변을 1D Verlet 입자로 두고, 아래 힘을 합산해 240회 적분한 뒤 최종 박스만 낸다. 결정론적이지 않고, 비겹침을 하드 제약으로 다루지 않는다 — 이 성질이 §8-B에서 문제가 된다.

힘내용
텍스트 확장 단방향예측 높이가 현재 박스보다 크면 verticalAlign 방향으로 확장. 작으면 그대로
텍스트 가로 확장 단방향세로로 겹치는 형제·부모 경계 안에서 horizontalAlign 방향으로 펼침 (WIDE_PADDING=24)
배경 확장 단방향자식이 커져 여백이 좁아지면 목표 여백(BG_MAX_MARGIN=24)까지 배경 변을 바깥으로
가이드 선 유지시작 시 같은 직선(GUIDE_TOL=4)에 있던 변들을 묶어 평균으로 당김. 부모-자식은 서로 영향 없음
겹침 방지형제끼리 겹치면 최소 침투축으로 밀어냄. 시작 시 이미 겹쳐 있던 쌍은 제외
여백 균형배경 컨테이너의 자식 텍스트에 한해 상하 여백이 같아지도록 세로 평행이동 (크기 보존)
페이지 제한 hard변을 페이지 안으로 clamp. 최초부터 밖이던 입자는 예외

ITERATIONS=240 · DAMPING=0.6 · comp-match/tidyLayout.ts:13-46

판정이 실제로 도는 방식 — 네 개의 규칙

모든 판정은 회전 반영 AABB(vn.absBox) 좌표로 한다. 진입점은 buildTidyLayoutBboxMap (:161) 이다. 새 알고리즘에서 부품으로 떼어 쓸 만한 곳이라 줄 번호를 남긴다.

①

겹침 쌍 선별 — 준비 단계에서 1회 :391~406

같은 visual 부모를 가진 입자쌍만 후보로 만들고, 시작 시 이미 겹쳐 있던 쌍은 영구 제외한다 — 원래 겹침은 의도된 디자인일 수 있다.

②

매 step 침투 깊이 + 최소 침투축 분리 :648~686

가로·세로 침투 깊이 ix·iy 중 작은 쪽이 collision normal 이고, 그 축으로 K_OVERLAP × 침투 × 0.5 만큼 민다 (K_OVERLAP = 0.3). 부호는 두 박스의 중심합으로 정한다.

성질 — 마주보는 두 변을 같은 d 로 옮기니 크기 보존(고정인 텍스트 좌우도 함께 옮긴다) · 형제만 판정 · 배경도 참여 · 한 노드가 여러 쌍에 걸리면 변위가 누적된다. 배경이 밀리면 그 안 자손도 같은 변위로 따라간다 — 쟁반-컵, shiftSubtree (:417) 의 childrenOf 재귀.

③

텍스트 가로 확장은 "세로로 겹치는 형제"만 막는다 :615

세로로 안 겹치는 형제는 확장을 막지 않는다. 막는 형제 중 중심이 내 중심보다 오른쪽이냐 왼쪽이냐로 extR·extL 을 정한다 (WIDE_PADDING = 24 안쪽까지).

④

여백 균형은 "가로로 겹치는 형제"로 상·하 한계를 잡는다 :715

가로로 겹치는 형제로 위·아래 한계를 잡아 상하 여백이 같아지도록 평행이동한다 (K_BALANCE = 0.2). 배경의 직속 자식 텍스트만 대상이다.

상수값뜻
ITERATIONS240시뮬레이션 iteration 수(고정)
DAMPING0.6Verlet 속도 감쇠 — 클수록 관성이 크다
K_MARGIN0.25배경 확장(초기 여백 회복) 강성
K_GUIDE0.2가이드 선 유지 강성
K_OVERLAP0.3겹침 방지 강성
K_BALANCE0.2여백 균형 강성
BG_MAX_MARGIN · WIDE_PADDING24px배경 여백 회복 상한 · 가로 확장 시 남길 여백
GUIDE_TOL4px같은 직선(가이드) 판정 허용 오차
EXPAND_EPS0.05px단방향 확장 판정의 부동소수점 허용

이 절의 줄 번호·상수는 현재 브랜치 HEAD(7b4354f6) 실측이다. 강성 합이 1을 넘지 않게 보수적으로 잡혀 있다 — tidyLayout.ts:45-67

확인

tidyLayout 이 어디까지 커버하나 — 이석님께 확인할 것

참고 이석 · IUI 엔진 (구현자)

④a(기하적 정렬)가 tidyLayout 커버 범위인지, comp-sign '스마트블록 대치'로 ④b(구조적 재배치)까지 가능한지 확인.

  • 같이 물을 것 — 대치 시 tidyLayout 을 켜고 있나? 기본값이 false다(§2).
  • 같이 물을 것 — 켰을 때 신규 겹침 게이트에 걸리는 사례를 알고 있나? (MOR-2018 실측: 5건 중 2건 FAIL)
부록

용어 — 너무 비슷해서 섞이는 것들

이 이슈의 두 단어

재배치 알고리즘
sheetJson 이 한 번 렌더된 이후의 내용을 근거로 재배치하는 데 쓰는 알고리즘. "렌더 전 예측으로 배치"와 대비되는 말이다.
위상 (topology)
좌표를 전부 지워도 남는, 요소들 사이의 상대적 관계. 이것을 바꾸는 것(리플로우)은 §1 의 범위 밖이다.
합성 (Composite)
헤드리스 Chromium 을 블랙박스로 써서 디자인을 이미지·PDF·PPTX·영상으로 만드는 서버 사이드 잡 파이프라인 전체. 브라우저 엔진의 "Composite" 과 다른 말이다.

텍스트 영역 — 네 가지가 서로 다른 것을 가리킨다

용어정의어디서 나오나
Bounding box 텍스트를 감싸는 가장 작은 사각형 영역 sheetJson 에 적혀 있는 값 · data.props.boundingBox
Tight bounding box
= ink rect · actual bounding box
엔진이 렌더해서 실측한 결과로 나온, 텍스트를 감싸는 가장 작은 영역 tightBoxes — §4 가 말하는 "실측"이 이것이다
Rendered bounding box 렌더링된 결과로 나온 bounding box → Tight bounding box 와 같은 말이라 폐기했다. 쓰지 말 것
Visual bounding box 코드상의 텍스트 데이터 영역(Text Metrics)과 대비해, 사용자 눈에 보이는 모든 시각 요소를 포함한 영역 불렛·글자 연관 데코레이션까지 텍스트와 함께 묶어 재배치·겹침 계산에 쓰기 위한 개념

예: "불렛을 포함한 컴포넌트의 실제 점유 공간은 Visual Bounding Box 를 기준으로 계산한다."

표준 용어와의 대응

표준 용어어디정의
ink overflow CSS Overflow Level 3 · MDN 용어집 글자가 실제로 칠해진 영역 — ascender·descender 가 em box 밖으로 튀어나오는 것을 포함
actual bounding box
actualBoundingBoxAscent 등
Canvas 2D TextMetrics 텍스트를 감싸는 가장 작은 사각형. 글자의 실제 픽셀 외곽선 또는 폰트 메트릭 기준. measureText() 로 width·actualBoundingBoxLeft· actualBoundingBoxAscent 를 얻는다
ClientRect / DOMRect DOM HTML 요소에서 텍스트가 화면에 실제로 그려져 차지하는 사각형. getBoundingClientRect() 로 얻고, 뷰포트 기준 x, y, width, height 를 돌려준다
line box CSS Inline Layout Level 3 줄 하나가 차지하는 상자