캔버스 안에 편집용 팝업을 띄워야 했다. 조건은 두 개였다. 선택한 객체 옆에 붙을 것, 그리고 화면 밖으로 나가지 말 것.
간단해 보였는데 두 번 막혔다.
1. max-height 436, 실제 높이 462
먼저 화면을 벗어나지 않게 높이를 제한했다.
maxHeight: 436 // 컨테이너 높이에서 여백을 뺀 값
그런데 재보니 카드가 462px였다. 26px가 더 크다.
카드의 스타일은 이랬다.
padding: 12, // 위아래 합쳐 24
border: '1px solid', // 위아래 합쳐 2
24 + 2 = 26. 정확히 넘친 만큼이다.
max-height 는 기본적으로 콘텐츠 상자에만 걸린다. 안쪽 여백과 테두리는 그 위에 얹힌다. width·height·max-width 도 같다.
box-sizing: content-box (기본값)
max-height 436 ─┐
├─ padding 24 ─┐
└──────────────├─ border 2 ─┐
└────────────┴─→ 실제 462
고치는 건 한 줄이다.
boxSizing: 'border-box' // padding·border 를 높이 안에 포함
왜 잘 안 걸리나 — 요즘 프로젝트는 대부분 * { box-sizing: border-box } 리셋을 깔아둔다. 그래서 평소엔 만날 일이 없다. 인라인 스타일로 컴포넌트를 만들거나, 리셋이 닿지 않는 곳(shadow DOM, 서드파티 위젯 안)에서 갑자기 튀어나온다.
2. 높이를 모르면 배치가 절반으로 잘린다
다음 문제는 세로 위치였다. 스크롤이 생기지 않게 하고 싶었다.
카드 높이를 모르니, 재지 않고 푸는 방법을 먼저 썼다. 객체가 위쪽 절반이면 아래로 펼치고, 아래쪽 절반이면 위로 펼친다.
const up = sy > H / 2
return up
? { bottom: H - sy - 20, maxHeight: sy + 20 }
: { top: sy - 20, maxHeight: H - sy + 20 }
동작은 한다. 그런데 가용 높이를 스스로 절반으로 잘라버린다.
| 컨테이너 높이 | 815px |
| 카드 내용 높이 | 695px |
| 이 방식이 허용한 높이 | 436px |
695가 815 안에 들어가는데도 스크롤이 생겼다. 객체가 한가운데 있으면 위로도 아래로도 절반씩밖에 못 쓰기 때문이다.
useLayoutEffect 로 재기
결국 높이를 알아야 한다. React에서 렌더된 크기를 재려면 ref로 읽는다.
const ref = useRef(null)
const [h, setH] = useState(0)
useLayoutEffect(() => {
const next = ref.current ? ref.current.scrollHeight : 0
setH((prev) => (Math.abs(prev - next) > 1 ? next : prev))
})
세 가지가 중요하다.
useEffect 가 아니라 useLayoutEffect. 전자는 브라우저가 그린 뒤에 돈다. 그러면 잘못된 자리에 한 프레임 그려졌다가 옮겨지고, 사용자 눈에는 깜빡임으로 보인다. 후자는 그리기 전에 동기로 돌아 보정이 화면에 나타나지 않는다.
clientHeight 가 아니라 scrollHeight. max-height 가 걸린 상태에서 clientHeight 는 잘린 높이를 준다. 내용이 실제로 얼마나 필요한지는 scrollHeight 여야 한다.
비교에 여유를 준다. 매 렌더마다 setState 를 부르므로, 값이 미세하게 흔들리면(서브픽셀·소수점) 렌더 → 측정 → 렌더가 끝나지 않는다. > 1 로 재랜더를 끊는다.
들어가면 max-height 를 걸지 않는다
높이를 알고 나면 배치는 단순하다.
const room = H - M * 2
if (!h || h <= room) {
// 객체에 가운데를 맞추되, 카드 전체가 들어가도록 안쪽으로 민다
const top = Math.max(M, Math.min(sy - h / 2, H - h - M))
return { top } // maxHeight 없음
}
return { top: M, maxHeight: room } // 컨테이너보다 클 때만
핵심은 들어갈 수 있으면 max-height 를 아예 걸지 않는 것이다. 습관적으로 걸어두면 필요 없는 스크롤이 생긴다. 제한은 “물리적으로 불가능할 때”만 있으면 된다.
가로도 같은 요령이다. 오른쪽에 자리가 있으면 오른쪽, 없으면 왼쪽으로 넘기고, 양쪽 다 좁으면 넓은 쪽에 붙인 뒤 범위 안으로 clamp 한다.
결과는 이렇게 갈렸다.
| 창 높이 | 컨테이너 | 카드 | 스크롤 |
|---|---|---|---|
| 950px | 815px | 695px 전체 | 없음 |
| 700px | 565px | 549px (내용 693) | 있음 |
작은 창에서는 스크롤을 피할 수 없다. 그때도 최대한 길게 펴고 남는 만큼만 스크롤에 맡긴다.
곁다리 — 공간을 넓힌 게 이득이긴 했나
이 팝업을 만든 이유는 옆의 사이드 패널(269px)을 접어서 보는 영역을 넓히려는 것이었다. 접으면 편집을 못 하니 팝업이 필요했던 것이다.
그런데 정작 넓어졌는지를 재보니 이랬다.
| 컨테이너 | 이미지 표시 크기 | 좌우 여백 | |
|---|---|---|---|
| 패널 있음 | 1316×815 | 1153×815 | 163px |
| 패널 접음 | 1585×815 | 1153×815 | 432px |
이미지는 한 픽셀도 커지지 않았다.
object-fit: contain 계열은 가로·세로 중 더 빡빡한 쪽이 표시 크기를 정한다. 여기서는 콘텐츠 비율(1.41)이 컨테이너 비율(1.61)보다 작아서 높이가 한계였다. 폭을 269px 늘렸지만 늘어난 건 여백뿐이다.
확인은 나눗셈 한 번이면 된다.
콘텐츠 비율 < 컨테이너 비율 → 높이가 한계. 폭을 늘려도 소용없다
폭을 늘려서 이득이 나는 건 확대해서 콘텐츠가 컨테이너를 가로로도 채운 뒤부터다.
정리
max-height·width는 기본적으로 콘텐츠 상자 기준이다.padding·border만큼 커진다 →box-sizing: border-box- 전역 리셋이 있으면 안 만나는 함정이다. 인라인 스타일로 만든 컴포넌트에서 튀어나온다
- 높이를 모르는 채로 배치하면 쓸 수 있는 공간을 스스로 절반으로 자르게 된다
- 렌더된 크기를 재서 배치할 때는
useLayoutEffect(그리기 전) +scrollHeight(잘리지 않은 값) + 비교에 여유(무한 렌더 방지) - 들어가면
max-height를 걸지 않는다. 제한은 불가능할 때만 contain으로 맞춘 콘텐츠는 한쪽 축만 한계다. 다른 축을 늘려도 여백만 늘어난다
두 번 다 “재지 않고 풀 수 있을 것 같아서” 우회하다 막혔다. 배치는 결국 크기를 알아야 정해지는 문제였다.
댓글