정적으로 빌드된 마케팅 사이트에도 아코디언, 공개 패널, 필터 서랍이 있고, 높이는 0에서 자연 높이로 매끄럽게 가야 합니다. 오랫동안 정직한 답은 “JavaScript로 scrollHeight를 재고 픽셀을 써라”였습니다. height: auto가 계산값 단계에서 보간 불가 키워드로 취급됐기 때문입니다. 2026년에는 interpolate-size: allow-keywords와 calc-size()가 고유 크기와 트랜지션 수학 사이를 선언적으로 잇습니다. 다만 캐스케이드, flex/grid의 기본 최소 크기, WebKit의 리플로 스케줄을 이해해야 합니다. 이 글은 고통 지점, 기본기, @supports 점진 강화, 성능 규율, 접근성, 판단용 매트릭스를 다룹니다. 문서 간 전환과 페이지 안 높이 트윈을 비교하려면 정적 MPA용 View Transitions를, 패널 안에 자동으로 커지는 폼이 있다면 field-sizing과 폼을 함께 읽어 textarea 고유 폭과 높이 애니메이션이 싸우지 않게 하세요.
검증 비용도 현실입니다. 고객과 같은 GPU에서 서브픽셀을 재현하는 것은, 깨진 애니메이션을 프로덕션에 올리는 것보다 Mac mini를 빌리는 편이 저렴합니다. MacHTML 클라우드 Apple Silicon은 대략 $16.9/일 수준이며 아래에서는 그 숫자를 리허설 예산 앵커로 씁니다.
height:auto 애니메이션이 어려운 이유
일반 transition은 길이·퍼센트·일부 transform을 보간하지만 height: auto는 특별합니다. used value는 첫 레이아웃 이후 콘텐츠 측정에 달려 있습니다. transition: height 240ms ease로 0과 auto를 오가면 많은 엔진이 숫자 쌍이 없어 구간 끝에서 튑니다. max-height: 9999px 해킹은 매끄러워 보이지만 번역으로 본문이 길어지면 지속 시간이 체감 거리와 어긋나고, 레이아웃 트리가 거대한 상자를 가정해 GPU 메모리가 불어날 수 있습니다. scrollHeight를 읽는 JS는 메인 스레드 결합을 되돌리고 정적 호스트의 CSP를 복잡하게 하며 웹폰트가 중간에 도착하면 깜빡임을 유발합니다.
새 모델은 측정을 스타일 시스템 안에 둡니다. 여전히 고유 크기로 생각하지만 엔진이 보간 가능한 숫자 통로를 얻습니다. 번들러가 React 상태기계를 금지한 정적 출력이나 main.js 바이트에 민감한 프로젝트에 구조적 이득입니다. Safari 우선 팀은 flex 아이템 안에서 퍼센트 높이와 고유 키워드가 섞일 때 WebKit이 순환 해석을 엄격히 다뤘음을 기억하면 “브라우저 버그”로 잘못 보고하지 않습니다.
둥근 모서리용 overflow: hidden과의 공존도 미묘합니다. max-height에만 의존하면 그림자·아웃라인 페인트가 내용 노출과 어긋나 Retina에서 “이중 문”처럼 보일 수 있습니다. 고유 보간은 측정된 콘텐츠 박스에 used height를 더 가깝게 맞추지만, 관련 없는 속성을 동시에 움직이지 않아야 합니다.
interpolate-size: allow-keywords 실무
interpolate-size는 서브트리에 “키워드도 보간”을 허용합니다. 공개 루트에 interpolate-size: allow-keywords를 두면 고유 키워드와 길이 사이 전이를 측정 가능한 끝점으로 다룰 수 있습니다. 보통 .disclosure나 details 래퍼에만 두고 전역 body에는 두지 않습니다. 보간 표면이 넓어지면 무관한 규칙까지 비용이 커집니다.
.disclosure {
interpolate-size: allow-keywords;
overflow: clip;
transition: height 260ms cubic-bezier(.2,.8,.2,1);
}
명시적 height 상태와 짝을 이룹니다. 접힘은 0, 펼침은 여전히 auto여도 엔진이 수치 궤적을 계산합니다. 클래스로 height:0과 height:auto를 바꿀 때 마진 상쇄를 조심하고, “신비한 간격”이 나오면 애니메이션 요소의 margin 대신 안쪽 padding을 씁니다.interpolate-size는 상속되므로 중첩 아코디언에는 편하지만 컴포넌트 라이브러리에서 자식 카드가 실수로 상속하고 flex-basis까지 움직이면 Safari에서 다패스 레이아웃이 날 수 있습니다. 유틸리티로 범위를 좁히세요.
calc-size(fit-content) 측정 다리
calc-size()는 고유 키워드를 길이로 만들어 transition이 샘플링하게 합니다. 펼침을 height: calc-size(fit-content, size)로 두면 height:auto처럼 콘텐츠 박스를 재고 그 값이 보간에 들어가고, 접힘은 0이면 양끝이 모두 길이입니다.
.panel[data-open="true"] .panel-body {
height: calc-size(fit-content, size);
}
.panel[data-open="false"] .panel-body {
height: 0;
}
max-height에서 이전할 때 체감 속도를 비교하세요. calc-size()는 실제 깊이를 따라 짧은 답변과 긴 정책 텍스트 모두에서 이징이 덜 거짓말합니다. 단점은 동적 콘텐츠 민감도입니다. 전환 중 라이브 리전이 글자를 붙이면 끝점이 움직입니다. 정적 HTML에서는 transitionend 전까지 DOM 업데이트를 멈추는 편이 안전합니다.
box-sizing: border-box를 쓰는 템플릿에서는 측정 높이가 의도한 border box를 포함하는지 확인하세요. 그렇지 않으면 Safari가 콘텐츠 높이만 움직이고 테두리가 한 프레임에 튀어 보일 수 있습니다.
@supports 점진 강화
calc-size()가 없으면 즉시 토글 또는 max-height 클립으로 기능을 유지합니다. 기능 쿼리는 함수 토큰 자체를 테스트해야 부분 구현을 피합니다.
@supports (height: calc-size(fit-content, size)) {
.panel-body { transition: height 240ms ease; }
}
@supports not (height: calc-size(fit-content, size)) {
.panel-body { transition: max-height 320ms ease; max-height: 0; }
.panel[data-open="true"] .panel-body { max-height: 80vh; }
}
빌드 시 @supports 결과를 HTML 주석에 남기는 것은 지원팀에 도움이 되지만 모던 CSS만으로 콘텐츠를 막지 마세요. CSS 없는 사용자에게도 제목과 앵커가 필요합니다. 모션은 details/summary나 aria-expanded 버튼 위의 선택 옵션입니다.
Grid·flex·min-height:auto
flex 자식은 기본 min-height:auto로 콘텐츠 최소 기여보다 줄지 않습니다. 0에서 고유 높이로 갈 때 min-height:0이 필요할 수 있습니다. grid도 min-size:auto와 트랙 크기 때문에 행이 거부될 수 있습니다.
align-self:stretch 그리드 영역에서는 블록 크기가 확정되어 calc-size() 해석이 달라집니다. aspect-ratio와 충돌할 수 있으니 본문은 단일 열 그리드를 선호하고 움직이는 패널은 별도 서식 맥락으로 격리한 뒤 contain:layout은 포커스 링이 잘리지 않을 때만 씁니다.
gap과 높이를 동시에 애니메이션하면 Safari가 프레임당 두 번 의존 레이아웃을 돌릴 수 있습니다. 높이 트윈 동안 형제 margin은 고정하고 필요하면 opacity로 부드럽게 합니다.
Safari/WebKit 메모
메인 스레드가 바쁘면 WebKit이 스타일 업데이트를 합쳐 높이 전환이 뛸 수 있습니다. 정적 HTML이라도 분석 스니펫은 지연하세요. macOS 업그레이드 기간에는 안정판 Safari와 STP를 함께 보고 픽셀 차이를 저장하세요.
하드웨어 가속이 height를 합성 레이어로 보내지는 않습니다. transform과 겹치면 레이아웃+페인트가 두 배가 될 수 있어 제스처마다 하나만 선택하세요.
서브픽셀 텍스트는 -webkit-font-smoothing 차이로 반짝임으로 보일 수 있으니 접힘·펼침에서 동일하게 맞춥니다.
성능: 레이아웃 스래싱
높이 샘플마다 지오메트리가 바뀌어 레이아웃이 강제됩니다. 패널 다섯 개를 동시에 열면 비용이 곱합니다. 동시 실행을 제한하고 뷰포트 밖은 content-visibility:auto(스크린 리더 영향 확인 후)로 줄입니다. WebKit 타임라인에서 보라색 레이아웃 막대가 한 프레임을 넘는지 봅니다.
JS 폴백에 ResizeObserver가 있다면 CSS transition과 동시에 동기 읽기를 하면 피드백 루프가 됩니다. requestAnimationFrame으로 스로틀하고 document.hidden이면 쓰기를 건너뜁니다.
접근성과 prefers-reduced-motion
큰 영역의 움직임은 전정기관에 부담입니다. prefers-reduced-motion: reduce에서는 지속 시간을 거의 0으로 하거나 opacity 힌트만 씁니다. DOM 순서는 유지합니다.
@media (prefers-reduced-motion: reduce) {
.panel-body { transition: none !important; }
}
포커스 링이 클립되면 overflow:clip과 안쪽 패딩 또는 안쪽 래퍼로 아웃라인을 옮깁니다. aria-expanded는 속성 변경과 동시에 스크린 리더에 전달되게 합니다.
정적 HTML 아코디언 패턴
컨테이너에 단일 클래스를 두고 최소 JS로 data-open을 바꾸거나 네이티브 details를 CSS로 미러링합니다. JS 없이도 details는 열고 닫을 수 있지만 애니메이션은 오래 해킹에 의존했습니다. interpolate-size와 calc-size()로 의미와 모션이 맞습니다. 높이 소유권은 하나만 두세요.
CSS는 타이포→레이아웃→모션 순으로 쌓아 출시 직전 마케팅 !important가 전환을 죽이는 일을 막습니다. S3/CloudFront 같은 정적 호스트는 motion.css?v=로 캐시 무효화를 관리합니다.
매트릭스: 고유 높이를 움직일까
| 시나리오 | 고유 높이 보간? | 메모 |
|---|---|---|
| 긴 법률 아코디언 | 예 | calc-size는 실제 깊이를 따름. |
| 무한 피드 카드 | 드묾 | 가상화가 우선. |
| 모달 | 경우에 따라 | 출입은 transform 우선. |
| 스티키 내비 공개 | 주의 | sticky 합성과 상호작용 실측. |
모션 QA 체크리스트
- 320/390/834px에서 가장 긴 로케일 문자열로 접힘·펼침 높이를 검증합니다.
- macOS 접근성에서 prefers-reduced-motion을 바꿔 상태가 전달되는지 확인합니다.
- 패널 다섯 개를 빠르게 열어 기준 M2에서 CPU와 팬을 봅니다.
- 프로덕션과 같이 서드파티를 켠 채 WebKit 타임라인을 녹화합니다.
- 전환 중간에 클립된 컨테이너 안 포커스 순서와 가시성을 검증합니다.
- macOS 릴리스 창이면 안정판 Safari와 STP를 비교합니다.
- 웹폰트 preload 변경 후 고유 측정을 다시 합니다.
- 다크 모드와 고대비에서 픽셀 차이를 캡처합니다.
자주 묻는 질문
max-height는 여전히 괜찮나요?
폴백으로는 예. calc-size를 지원하면 긴 콘텐츠에서 타이밍 어긋남을 줄입니다.
스크롤 구동 애니메이션을 대체하나요?
아니요. 다른 문제입니다. 함께 쓰면 상호작용을 실측하세요.
width:auto도 비슷한가요?
개념은 비슷하지만 가로축 텍스트 리플로가 더 비쌉니다. 프로파일이 필요합니다.
믿을 만한 모션은 문법 암기보다 프로덕션에 가까운 폰트·확장·스케일에서 실제 Safari 하드웨어를 도는 일입니다. MacHTML의 Mac mini(대략 $16.9/일)는 고객 기기를 비추는 상시 타깃이며 SSH로 픽셀 스냅, VNC로 디자인 리뷰가 가능합니다.
클라우드 Mac mini에서 고유 높이 애니메이션 리허설
Apple Silicon Safari에서 정적 번들을 열고 아코디언 폭풍 동안 레이아웃을 프로파일한 뒤 프로덕션 CSS에 모션 토큰을 머지하세요.