transform: scale 로 줄인 iframe 이 flex 부모를 밀어낼 때 — min-width: auto

transform 은 레이아웃에 영향을 주지 않는다. 고정폭 iframe 의 원래 폭이 flex item 의 min-content 크기로 잡혀 flex-basis 를 무시하고 컬럼을 아래로 떨어뜨린다.

“미리보기” 컴포넌트는 대체로 이렇게 만든다. 실제 페이지를 고정 논리 해상도로 iframe 에 띄우고, 컨테이너 실측 폭에 맞춰 transform: scale() 로 줄여서 보여준다.

<div style={{ width: '100%', aspectRatio: '1280/720', overflow: 'hidden' }}>
  <iframe style={{ width: 1280, height: 720, transform: `scale(${scale})`, transformOrigin: 'top left' }} />
</div>

이걸 컨트롤 패널과 나란히 두려고 flexbox 로 감쌌다. 좌측 미리보기, 우측 컨트롤. 프레젠터 화면에서 흔한 배치다.

<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
  <div style={{ flex: '3 1 480px' }}><Preview /></div>
  <div style={{ flex: '1 1 280px' }}><Controls /></div>
</div>

결과는 의도와 달랐다. 미리보기가 부모 컨테이너보다 넓게 튀어나오고, 컨트롤 컬럼이 아래로 떨어져 세로로 쌓였다.

측정해보니 미리보기 컬럼의 실제 렌더 폭이 1288px 였다. flex-basis 로 준 480px 도 아니고 부모 폭도 아니다. iframe 의 1280px 에 테두리를 더한 값이다.

원인 두 개가 겹친다

transform 은 레이아웃을 안 바꾼다

transform: scale()시각적으로만 축소한다. iframe 이 레이아웃상 차지하는 박스는 여전히 width: 1280 이다. 리플로우 계산에서 1280px 그대로 잡힌다.

이건 의도된 동작이다. transform 이 레이아웃에 영향을 주면 애니메이션마다 리플로우가 발생한다.

flex item 의 min-width 기본값은 0 이 아니다

flex item 은 기본적으로 min-width: auto 를 갖는다. 여기서 auto 는 0 이 아니라 콘텐츠의 min-content 크기를 뜻한다.

iframe 의 레이아웃 폭 1280px 가 그대로 min-content 크기가 되므로, flex-basis: 480px 를 줘도 아이템이 1280px 밑으로 줄지 않는다. flex 알고리즘이 “더 줄이고 싶지만 최소치가 1280”이라고 판단하는 것이다.

부모에 overflow: hidden 을 걸어도 소용없다. 그건 그려진 걸 자를 뿐 min-content 계산에는 반영되지 않는다.

해결

flex item 에 min-width: 0 을 명시해 기본값을 덮는다.

<div style={{ flex: '3 1 480px', minWidth: 0 }}>
  <Preview />
</div>

이러면 flex item 이 콘텐츠 크기와 무관하게 flex-basis / shrink 계산대로 줄어든다. 이미 overflow: hidden 으로 시각적 잘림을 처리해뒀으니 안전하다.

컴포넌트 자기 루트에도 minWidth: 0 을 넣어두면 재사용할 때 안전하다. 이 컴포넌트를 쓰는 쪽이 매번 이 사정을 알아야 한다면 그건 잘 만든 컴포넌트가 아니다.

같은 증상을 만나면

의심 신호: flex 레이아웃에 flex-wrap: wrap 을 줬는데 “합쳐도 충분히 좁은데” 줄바꿈된다. 어느 한 아이템이 콘텐츠 때문에 강제로 넓어져 있다는 뜻이다.

범인 찾기: 각 아이템에서 getBoundingClientRect().width 를 찍는다. flex-basis 보다 훨씬 큰 값이 나오는 애가 범인이다.

[...container.children].forEach((el, i) =>
  console.log(i, el.getBoundingClientRect().width)
);

후보 목록: 고정폭 iframe, canvas, 명시적 width 가 붙은 이미지, 긴 단어가 든 white-space: nowrap 텍스트, 그리고 가로 스크롤을 가정한 테이블. 특히 고정폭 요소를 transform: scale 로 줄여 보여주는 미리보기 컴포넌트는 거의 항상 이 문제의 후보다.

정리

기억할 문장은 하나다. transform 은 min-content 계산에 안 잡힌다. 눈에 보이는 크기와 레이아웃이 쓰는 크기가 갈라지는 순간, flex 의 min-width: auto 가 레이아웃 쪽 숫자를 그대로 들고 온다.

같은 미리보기 컴포넌트를 만들다 스태킹 순서 쪽에서도 한 번 걸렸는데, 그건 z-index: -1 배경 레이어가 안 보이는 문제에 따로 적었다.