ADR-0034: 반응형 레이아웃 — 컨테이너 쿼리 + 한국어 keep-all + AI 패널 오버레이
배경
"창 크기를 줄이거나 AI 에이전트 패널을 열면 문구가 세로 한 줄로 깨진다"는 신고에서 출발해, 전 라우트를 실측했다.
계측 방법 — Playwright 로 63 라우트 × 창 폭 7종(1536/1366/1280/1152/1024/900/800)
× AI 패널 개폐 2종 = 882 샘플. 각 샘플에서 텍스트 노드가 만든 줄 박스를
Range.getClientRects() 로 세어 줄수 >= 3 && 글자/줄 <= 4.5 && 줄폭 <= 160px 을
붕괴로 판정했다. (요소 높이 / line-height 로 줄수를 추정하면 형제 아이콘·버튼의
높이가 섞여 오탐이 난다 — 실제로 초기 계측의 26/68 건이 이 오탐이었다.)
결과: 42건 붕괴. 원인은 세 층으로 갈렸다.
- 한국어 개행 규칙 — 기본값(
word-break: normal)에서 CJK 는 글자 단위로 끊긴다. 따라서 축소 가능한 텍스트의min-content폭이 1글자로 수렴한다./get-data카드 설명은 폭 15px · 25줄 · 1.4자/줄까지 붕괴했다. - 열 수를 창 폭으로 정하는 그리드 —
sm:/md:/lg:는 뷰포트 미디어쿼리다. 사이드바(180px) + AI 패널(420px) 이 먹은 폭은 여기 보이지 않아서 "창은 1152px 인데 카드는 130px" 상태가 만들어졌다. 접두사 없는grid-cols-3은 더 심하다. - AI 패널이 본문을 밀어냄 — 항상 나란히 배치하면 컨텐츠 폭이 1024px 창에서 424px, 800px 창에서 200px 이 된다. 42건 중 35건이 이 구간에서만 발생했다.
결정
D-1. 한국어 개행은 word-break: keep-all 을 전역 기본값으로 한다
ui/src/index.css 의 body 에 word-break: keep-all 을 둔다.
- 어절(공백) 단위로만 끊기므로
min-content가 "가장 긴 어절"이 되고, 1글자/줄 붕괴가 구조적으로 불가능해진다. - 라틴 문자에서
keep-all은normal과 동일하게 동작하므로 영문 UI 렌더는 바뀌지 않는다. - 긴 토큰(URL·FQN)을 쪼개야 하는 자리는 이미
break-words(overflow-wrap: break-word) 를 쓰고 있고,overflow-wrap은keep-all보다 우선하므로 그 동작도 유지된다.
컴포넌트별 break-keep 을 흩뿌리는 대안은 기각했다 — 원인이 전역 개행 규칙이므로
차단점도 한 곳이어야 하고, 신규 화면이 자동으로 보호된다.
D-2. 열 수는 뷰포트가 아니라 컨테이너 폭으로 정한다
카드/KPI 그리드는 sm:/md:/lg: 대신 @container + @3xl: 계열 컨테이너 쿼리를 쓴다.
// ❌ 창 폭 기준 — 사이드바·AI 패널이 먹은 폭이 안 보인다
<div className="grid gap-4 sm:grid-cols-2">
// ✅ 컨테이너 폭 기준
<div className="@container">
<div className="grid gap-4 @3xl:grid-cols-2">
주의: 컨테이너 쿼리는 자기 자신을 조회할 수 없다. @container 는 래퍼에 두고
변형(@3xl:)은 자손에 둔다.
D-3. AI 에이전트 패널은 좁은 창(xl 미만)에서 본문 위에 겹친다
xl(1280px) 이상에서는 기존처럼 본문 옆에 나란히(xl:static), 그 미만에서는
absolute inset-y-0 right-0 오버레이 + 스크림(바깥 클릭 시 닫힘)으로 전환한다.
컨텐츠 폭이 200~400px 로 짜부라지는 구간 자체를 없애는 것이 개별 화면을 패치하는
것보다 근본적이다.
D-4. 회귀 가드는 클래스가 아니라 렌더 결과를 단언한다
ui/tests/responsive-text-squeeze-e2e.spec.ts — 위 계측기를 그대로 스펙으로 만들었다.
className 문자열을 단언하면 구현을 바꿀 때마다 깨지고 결함은 놓친다. 줄 박스 계측은
구현과 무관하게 결함만 잡는다.
결과
동일 하네스 882 샘플 재측정: 붕괴 42건 → 0건, 신규 회귀 0건, 페이지 수평 오버플로 0건, 라우트 로드 실패 0건. UI 단위 테스트 1,589건 전건 통과.
넓은 화면(1536px, 패널 닫힘) 스크린샷은 수정 전과 사실상 동일하다 — keep-all ·
flex-wrap · 컨테이너 쿼리는 모두 "자리가 부족할 때만" 동작하기 때문이다.
후속
정적 감사(전 컴포넌트 스캔 6그룹 + 그룹별 적대 검증)에서 적대 검증을 통과한 59건 중
데이터가 채워진 상태에서만 재현되는 후보(다열 테이블의 min-w-* 부재, Pipeline
Studio 툴바 9버튼, 온톨로지 인스턴스 탭 등)는 #2762
로 분리해 같은 계측기로 검증하며 처리한다.
주의: min-w-0 은 이 결함의 해법이 아니다. flex item 의 min-width: auto(= min-content)
를 0 으로 풀면 박스가 어절보다 좁아져 글자가 박스를 넘쳐 형제 위에 겹친다. truncate
와 짝지을 때만 쓴다.