툴팁으로 감쌌더니 옆 요소가 밀려났다 — 래퍼가 가져가는 flex 성질
툴팁 같은 래퍼 컴포넌트가 부모의 flex 계산에 개입해 레이아웃을 깨뜨리는 문제를 해결하기 위해 래퍼의 flex 속성을 옵션으로 분리하고 레이아웃 설계 시 호출부의 영향력을 고려하는 방법을 제안합니다

목록 행의 한 칸에 "아이콘 + 텍스트" 배지가 있었다. 텍스트가 길면 text-overflow: ellipsis 로 잘리고, 그 옆에는 작은 상태 아이콘이 붙는다.
여기에 호버 툴팁을 달았다. 공용 Tooltip 컴포넌트는 자식을 span 으로 감싸 마우스 이벤트를 받는, 흔한 구조다.
<Tooltip label={fullPath}>
<Badge />
</Tooltip>기능은 잘 됐다. 그런데 다른 화면에서 배지 옆에 있던 상태 아이콘이 칸 밖으로 밀려났다.
래퍼가 자식의 flex 성질을 대신 갖는다
툴팁 컴포넌트는 자기 래퍼에 이렇게 써 두었다.
<span style={{ display: "inline-flex", flexShrink: 0 }}>{children}</span>flex-shrink: 0 자체는 합리적인 선택이다. 툴팁 트리거는 보통 아이콘이나 버튼이고, 좁은 부모에서 찌그러지면 안 된다.
문제는 그 래퍼가 원래 자식이 가지고 있던 flex 성질을 대신 갖게 된다는 점이다. 감싸기 전에는 배지 자신이 flex 아이템이었고 min-width: 0 으로 줄어들 수 있었다. 그래서 폭이 모자라면 내부 텍스트가 말줄임됐다.
감싼 뒤에는 부모의 flex 계산에 참여하는 게 배지가 아니라 래퍼다. 줄어들지 않는 래퍼는 콘텐츠 폭을 그대로 요구하고, 배지는 셀보다 커진다. overflow: hidden 은 부모에 있으니 잘리기만 할 뿐 — 말줄임은 사라지고 이웃은 밖으로 밀려난다.
같은 폭(200px) 셀에 놓고 실측한 값이다.
| 배지 폭 | 말줄임 | 이웃 위치 | |
|---|---|---|---|
| 감싸기 전 | 179px | O | x=192 (셀 안) |
| 툴팁으로 감싼 뒤 | 222px | X | x=235 (셀 밖) |
고약한 건 증상이 나타난 위치다. 새로 툴팁을 붙인 화면은 배지가 자기 칸을 통째로 쓰는 자리라 멀쩡했고, 같은 컴포넌트를 쓰던 다른 화면이 깨졌다. 한 컴포넌트를 감싼 것뿐인데 손대지 않은 화면이 무너진 셈이다.
래퍼의 레이아웃 성질을 호출부가 정하게 한다
해결은 래퍼의 flex 성질을 옵션으로 여는 것이다. 기본값은 그대로 두고 — 기존 아이콘 트리거들이 찌그러지면 안 되니까 — 말줄임이 필요한 트리거만 opt-in 한다.
두 모드가 필요했다.
shrink— 좁은 줄에서 줄어들어야 하는 인라인 배지.flex-shrink: 1과min-width: 0을 함께 주면 감싸기 전과 동일한 폭 동작이 된다.fill— 폭이 정해진 셀을 채우는 자리.display: flex와width: 100%로 셀 폭에 맞춰 말줄임한다.
interface TooltipProps {
label: string;
/** 좁은 부모에서 줄어들 수 있게 — 말줄임하는 텍스트 트리거용 */
shrink?: boolean;
/** 고정폭 셀을 채운다 — 자기 칸이 정해진 자리용 */
fill?: boolean;
children: React.ReactNode;
}
function Tooltip({ label, shrink, fill, children }: TooltipProps) {
return (
<span
style={{
display: fill ? "flex" : "inline-flex",
width: fill ? "100%" : undefined,
// 기본은 찌그러지지 않음 — 아이콘 트리거가 다수라 이게 옳다
flexShrink: shrink ? 1 : 0,
minWidth: shrink ? 0 : undefined,
}}
onMouseEnter={/* ... */ undefined}
>
{children}
</span>
);
}min-width 를 함께 주는 게 핵심이다. flex 아이템의 min-width 초기값은 auto 이고, 이는 "콘텐츠보다 작아지지 않는다"는 뜻이다. flex-shrink: 1 만으로는 텍스트가 든 요소가 콘텐츠 폭 아래로 내려가지 않는다.
그리고 기본값은 뒤집지 않는 편이 낫다. flex-shrink: 0 을 없애면 이번엔 반대로 기존 아이콘 트리거들이 좁은 줄에서 찌그러진다. 다수가 의존하는 동작이 기본값이어야 한다.
감싸는 컴포넌트는 레이아웃 API 도 설계해야 한다
Tooltip, Popover, Draggable, ErrorBoundary 처럼 "기능만 추가한다"고 생각하는 래퍼들이 실제로는 DOM 한 겹을 끼워 넣는다. 그 한 겹은 부모의 flex/grid 계산에 참여하는 새 참여자다.
체크할 것 셋이다.
- 래퍼가 부모의 flex/grid 아이템 자리를 대신 차지하는가.
flex-shrink,min-width,align-self,grid-area가 자식에서 래퍼로 옮겨가야 하는지 본다. - 감싼 화면만 보고 판단하지 않는다. 그 컴포넌트를 쓰는 모든 호출부가 영향권이다.
- 판정은 실측으로 한다. jsdom 기반 테스트에는 레이아웃 엔진이 없어 폭·말줄임·겹침을 볼 수 없다. 실제 브라우저에서 before / 버그 / 수정 세 가지를 같은 조건에 놓고 재면 몇 분이면 끝난다. 대조군 없이 "수정 후 잘 보인다"는 근거가 되지 못한다.
테스트로 박제할 수 있는 것은 스타일 축뿐이다. 어떤 모드에서 flex-shrink 가 무엇이어야 하는지 — 그것만으로도 회귀는 잡힌다.
it("인라인 모드 — 줄어들 수 있어야 한다", () => {
const { container } = render(<Tooltip label="x" shrink><Badge /></Tooltip>);
const wrapper = container.querySelector("span");
expect(wrapper.style.flexShrink).toBe("1");
expect(wrapper.style.minWidth).toBe("0px");
});