본문으로 건너뛰기
fe.run

flex 1이 약속을 못 지킬 때

글 복사 완료!

flex 1로 반씩 나누는데 긴 탭바 하나에 양쪽이 다 밀려나는 순간이 있어요.

·18분·

화면을 1대 1로 가르는 듀얼 뷰를 짜고 있었어요. 양쪽에 flex: 1만 주면 끝날 줄 알았죠. 짧은 텍스트를 넣었을 때는 멀쩡했는데, 왼쪽에 긴 탭바를 넣는 순간 양쪽이 같이 화면 밖으로 밀려났어요. flex: 1은 남는 공간을 어떻게 나눌지만 정하고, 얼마나 작아질 수 있는지의 하한선은 min-width가 따로 쥐고 있거든요.

짧은 텍스트는 되고 긴 탭바는 터진다

flex: 1은 세 속성을 한 번에 쓴 단축 표기예요. flex-grow: 1, flex-shrink: 1, flex-basis: 0. 기준 크기를 0으로 두고 출발하니까 컨테이너 전체가 남는 공간이 되고, 그걸 둘이 반씩 나눠 가져요. 여기까지는 기대한 그대로예요.

함정은 flex-shrink: 1에 있어요. 줄어들 수 있다고 선언은 했는데, 어디까지 줄어들 수 있는지는 이 값이 정하지 않아요. MDN은 그 바닥을 이렇게 못 박아요.

"Small items will not shrink to less than their min-content size, which is the smallest size the element can be if it used all the available soft wrapping opportunities." - MDN

줄바꿈이 가능한 자리를 전부 써먹고도 더는 못 줄이는 크기, 그게 min-content 예요. flex-shrink 값을 아무리 키워도 이 바닥을 뚫고 내려가지는 않아요.

아래 데모에서 버튼을 눌러보세요. 짧은 텍스트일 때는 두 패널이 점선 안에 얌전히 들어가는데, 긴 탭바로 바꾸는 순간 점선을 뚫고 나가요.

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

여기서 대부분이 한 번 놀라요. min-width의 초기값은 0이 아니라 auto거든요.

평소에 이 사실이 안 보이는 이유는, 일반 블록 박스에서는 auto가 그냥 0으로 풀리기 때문이에요. CSS Sizing 명세도 auto는 자동 최소 크기를 뜻하지만 레이아웃 모듈이 따로 정의하지 않는 한 사용값은 0이라고 적어뒀어요. 그리고 flexbox가 바로 그 "따로 정의한" 모듈입니다.

"In general, the automatic minimum size of a flex item is the smaller of its specified size suggestion and its content size suggestion." - W3C CSS Flexbox Level 1

flex 아이템에서 min-width: auto는 0이 아니라 콘텐츠가 요구하는 최소 크기로 해석돼요. width 같은 명시 크기가 없으면 남는 건 콘텐츠 쪽 값, 즉 min-content 하나예요.

그래서 min-width: 0을 준다는 건 새로운 제약을 거는 게 아니에요. 원래 다른 곳에서는 0이던 기본값을 flex 아이템에서도 0으로 되돌리는 일에 가깝죠. 아래에서 체크박스를 켜보면 1대 1 비율이 곧바로 돌아와요.

체크박스를 켜도 탭은 여전히 패널 밖으로 삐져나와요. 박스 크기를 되돌린 것과 넘치는 콘텐츠를 처리한 것은 서로 다른 일이거든요. 후자는 마지막 절에서 이어서 다뤄요.

왜 한쪽이 아니라 둘 다 밀려났나

제가 가장 이해가 안 갔던 부분이 이거였어요. 긴 탭바는 왼쪽 패널에만 넣었는데, 왜 멀쩡한 오른쪽까지 같이 밀려났을까요.

flex의 공간 분배는 한 번에 끝나는 계산이 아니라 루프거든요. 나눠주고, 각 아이템의 최소·최대 제약을 넘겼는지 검사하고, 넘긴 아이템은 그 값으로 고정해서 분배 대상에서 빼내고, 남은 공간을 나머지끼리 다시 나눠요. 컨테이너가 1000px일 때 실제로 무슨 일이 벌어지는지 따라가 볼게요.

1

남는 공간을 반씩 나눈다

flex-basis 가 0 이라 1000px 전체가 나눠줄 공간이에요. 왼쪽 500px, 오른쪽 500px 로 출발합니다.

2

최소 크기 제약을 검사한다

왼쪽 탭바는 줄바꿈이 안 돼서 min-content 가 960px 이에요. 배정받은 500px 은 이 하한선을 아래로 뚫었죠.

3

위반한 아이템을 고정해서 빼낸다

왼쪽은 960px 에 고정되고 이후 분배에서 빠져요. 남는 공간은 1000 에서 960 을 뺀 40px 뿐입니다.

4

나머지끼리 다시 나눈다

오른쪽 몫은 500px 이 아니라 40px 이 돼요. 오른쪽도 자기 min-content 가 300px 이면 거기서 또 멈추고, 합계 1260px 이 컨테이너를 뚫습니다.

순서가 중요해요. 1대 1이 먼저 깨지고, 넘치는 건 그 다음이에요. 화면이 밀려나기 전에 이미 비율은 무너져 있었던 거죠. DevTools에서 "왜 500px이 아니라 40px이지" 싶은 숫자를 봤다면 그 아이템은 피해자 쪽입니다.

탭바가 유독 잘 터지는 이유

짧은 텍스트는 되고 탭바만 터진 것도 우연이 아니에요. min-content 크기는 콘텐츠가 줄바꿈을 얼마나 허용하느냐에 따라 완전히 달라지거든요.

평범한 한국어 문단은 어절마다 줄바꿈 기회가 널려 있어서 최소 폭이 가장 긴 단어 하나 수준이에요. white-space: nowrap을 걸면 그 기회가 통째로 사라지니까 문장 전체 길이가 최소 폭이 되고요. 탭바는 여기서 한 단계 더 나가요. 자기도 display: flex인데 자식 탭마다 nowrap이 걸려 있으니, 최소 폭이 탭 너비의 총합이 돼버려요. 탭 8개에 각 120px이면 그 패널은 960px 아래로는 절대 안 줄어드는 박스인 거예요.

세 박스에 똑같이 width: min-content를 줬는데 폭이 전혀 다르죠. 줄바꿈 기회를 어떤 값으로 얼마나 열고 닫는지는 word-break 편에서 따로 다뤘어요.

고쳐야 할 자리는 한 군데가 아니다

가장 직접적인 처방은 min-width: 0이에요. 다만 넘침이 시작되는 아이템 하나에만 걸면 부족한 경우가 많아요. 듀얼 뷰 컨테이너 자체가 바깥쪽 flex의 아이템이라면 그 컨테이너에도 똑같은 자동 최소 크기가 걸려 있거든요. 넘치는 지점에서 위로 올라가면서 flex 아이템인 조상마다 풀어줘야 화면 밖으로 새어나가는 게 멈춰요.

overflow: hidden이나 auto를 걸었더니 해결됐던 경험이 있다면, 그것도 같은 규칙의 다른 출구예요. 아이템이 스크롤 컨테이너가 되면 자동 최소 크기가 다시 0으로 돌아가도록 명세에 예외가 박혀 있거든요. 의도를 드러내는 쪽은 min-width: 0이에요. overflow는 부수효과로 최소 크기를 푸는 것이라, position: sticky 자식이나 바깥으로 나가야 할 드롭다운까지 같이 잘려나가요. 크기 제약을 푸는 일은 min-width에, 넘치는 콘텐츠를 어떻게 보여줄지는 overflow에 맡기는 편이 나중에 덜 헷갈려요.

Grid로 짜면 이 함정이 1fr에 숨어 있어요. CSS Grid 명세minmax() 밖에 홀로 쓰인 <flex> 값이 자동 최소를 암시한다고, 즉 1fr이 사실은 minmax(auto, 1fr)이라고 정의해요.auto가 flex 아이템에서와 똑같이 콘텐츠 기반 최소 크기로 풀리고요. 그래서 Grid에서 진짜 1대 1을 원하면 minmax(0, 1fr)이라고 적어야 해요. Flex와 Grid 중 어느 쪽이 이 레이아웃에 맞는지가 먼저 궁금하다면 Flexbox만으로는 부족하다 쪽을 먼저 보셔도 좋아요.

세로 방향도 같아요. flex-direction: column이면 주축이 세로라서 같은 규칙이 min-height: auto로 나타나요. 세로로 넘치고 있다면 찾아야 할 건 min-height: 0이에요.

다음은 처음의 듀얼 뷰를 고친 결과예요. 패널에 min-width: 0을 주고, 탭바를 감싼 래퍼에 overflow-x: auto를 얹었어요. 비율은 1대 1로 돌아왔고 넘치는 탭은 패널 안에서 스크롤돼요.

레이아웃이 화면을 뚫고 나갈 때 확인할 것

☐ 넘치는 요소부터 위로 올라가며 Computed 패널의 min-widthauto 인 flex 아이템 찾기
☐ 그 아이템의 사용 너비가 콘텐츠 최소 폭과 같은지 대조하기
☐ 스크롤 컨테이너로 만들 자리와 min-width: 0 만 줄 자리를 나누기
☐ Grid 라면 1frminmax(0, 1fr) 로 바꿔보기
☐ 세로로 넘친다면 같은 검사를 min-height 로 반복하기

정리하면 축은 하나예요. flex: 1은 비율을 정하고, 그 비율이 실제로 지켜질 수 있는 하한선은 min-width가 정해요. 두 값이 충돌하면 언제나 하한선이 이기고, 그 결과가 화면을 뚫고 나오는 콘텐츠로 보이는 거죠. 그래서 넘침을 만났을 때 던질 첫 질문은 "얼마나 줄어들어야 하지"가 아니라 "지금 이 박스가 줄어들 수 있는 바닥이 어디지"예요.

자주 묻는 질문

자주 묻는 질문

답을 펼치기 전에 스스로 답해보세요

해결되지 않아요. flex-shrink 는 여러 아이템이 함께 줄어들 때의 비율을 정할 뿐, 얼마까지 줄어들 수 있는지의 하한선은 건드리지 않거든요. 하한선은 min-width 가 쥐고 있고, 기본값 auto 가 콘텐츠 최소 크기로 풀리는 한 아무리 큰 shrink 값도 그 아래로는 못 내려가요.
권하지 않아요. 명세가 auto 를 기본값으로 둔 이유가 콘텐츠를 보이지 않게 새어나가지 않도록 막는 것이거든요. 전역으로 풀어버리면 텍스트가 조용히 잘리거나 밖으로 빠져나가도 눈치채기 어려워져요. 넘침을 실제로 처리할 스크롤이나 말줄임을 함께 붙일 자리에만 거는 편이 안전해요.
flex-direction: column 이면 주축이 세로라서 같은 규칙이 min-height: auto 로 나타나요. 원인은 동일하고 속성 이름만 바뀌니, 세로 오버플로에서는 min-height: 0 을 찾으면 돼요.

참고 자료

관련 글