---
title: "툴팁으로 감쌌더니 옆 요소가 밀려났다 — 래퍼가 가져가는 flex 성질"
description: "툴팁 같은 래퍼 컴포넌트가 부모의 flex 계산에 개입해 레이아웃을 깨뜨리는 문제를 해결하기 위해 래퍼의 flex 속성을 옵션으로 분리하고 레이아웃 설계 시 호출부의 영향력을 고려하는 방법을 제안합니다"
date: 2026-08-21
updated: 2026-08-27T02:54:36.672Z
tags: [css, flexbox, react, 컴포넌트설계]
canonical: https://blog.wooncloud.com/posts/tooltip-wrapper-flex-shrink
---

![툴팁 래퍼가 행의 정렬을 밀어내는 모습](/images/posts/tooltip-wrapper-flex-shrink/7a61ae75-f5cb-4358-b2f5-8c8d4af1db7d.webp)

목록 행의 한 칸에 "아이콘 + 텍스트" 배지가 있었다. 텍스트가 길면 `text-overflow: ellipsis` 로 잘리고, 그 옆에는 작은 상태 아이콘이 붙는다.

여기에 호버 툴팁을 달았다. 공용 `Tooltip` 컴포넌트는 자식을 span 으로 감싸 마우스 이벤트를 받는, 흔한 구조다.

```tsx
<Tooltip label={fullPath}>
  <Badge />
</Tooltip>
```

기능은 잘 됐다. 그런데 **다른 화면에서** 배지 옆에 있던 상태 아이콘이 칸 밖으로 밀려났다.

## 래퍼가 자식의 flex 성질을 대신 갖는다

툴팁 컴포넌트는 자기 래퍼에 이렇게 써 두었다.

```tsx
<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%` 로 셀 폭에 맞춰 말줄임한다.

```tsx
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 계산에 참여하는 **새 참여자**다.

체크할 것 셋이다.

1. 래퍼가 부모의 flex/grid 아이템 자리를 대신 차지하는가. `flex-shrink`, `min-width`, `align-self`, `grid-area` 가 자식에서 래퍼로 옮겨가야 하는지 본다.
2. 감싼 화면만 보고 판단하지 않는다. 그 컴포넌트를 쓰는 **모든 호출부**가 영향권이다.
3. 판정은 실측으로 한다. jsdom 기반 테스트에는 레이아웃 엔진이 없어 폭·말줄임·겹침을 볼 수 없다. 실제 브라우저에서 **before / 버그 / 수정 세 가지를 같은 조건에 놓고** 재면 몇 분이면 끝난다. 대조군 없이 "수정 후 잘 보인다"는 근거가 되지 못한다.

테스트로 박제할 수 있는 것은 스타일 축뿐이다. 어떤 모드에서 `flex-shrink` 가 무엇이어야 하는지 — 그것만으로도 회귀는 잡힌다.

```tsx
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");
});
```

## 참고

- [MDN — flex-shrink](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-shrink)
- [MDN — flex item 의 min-size gotcha](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Mastering_wrapping_of_flex_items#the_min-size_gotcha)
