CSS — 나중에 쓴 규칙이 이긴다 · 화면 폭이 아니라 상자 폭이다 · 이론
카드는 자기가 어디 놓였는지 모른다
한 줄 요약
flex 는 한 축을 나누고 grid 는 두 축을 동시에 잡는다. 그리고 반응형의 진짜 질문은 "화면이 몇 px 인가" 가 아니라 "이 상자가 몇 px 인가" 이고, 그 질문에 답하는 것이 컨테이너 질의다.
왜 이게 필요했나
카드 컴포넌트를 하나 만든다. 좁으면 세로로, 넓으면 가로로 놓이게 하려고 미디어 쿼리를 쓴다.
.card { display: block; }@media (min-width: 700px) { .card { display: flex; } }이 카드를 본문에 놓으면 잘 동작한다. 그런데 같은 카드를 좁은 사이드바에 놓으면 망가진다. 화면은 1400px 이라 미디어 쿼리는 참인데, 카드가 들어앉은 상자는 280px 이기 때문이다.
컴포넌트는 자기가 어디 놓였는지 모른다. 미디어 쿼리는 화면 폭만 묻는데, 컴포넌트가 알아야 하는 것은 자기 상자의 폭이다. 이 어긋남이 디자인 시스템에서 가장 자주 나는 구조적 문제였다.
어떻게 동작하나 — flex 와 grid
둘 중 무엇을 쓸지는 취향이 아니라 차원으로 갈린다. [MDN 의 비교 문서](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Relationship_of_grid_layout_with_other_layout_methods)가 이 관계를 정리한다.
| 상황 | 맞는 것 |
|---|---|
| 한 줄(또는 한 열) 안에서 나눈다 | flex |
| 내용 크기에 따라 알아서 배분 | flex |
| 행과 열을 둘 다 정한다 | grid |
| 화면 골격 — 머리·옆·본문·꼬리 | grid |
| 개수가 달라져도 자리를 지켜야 한다 | grid |
flex 는 내용에서 출발한다. 아이템이 자기 크기를 정하고 남거나 모자란 만큼을 나눈다. grid 는 틀에서 출발한다. 먼저 칸을 정하고 아이템을 그 칸에 넣는다.
그래서 툴바나 버튼 줄처럼 "안에 무엇이 들어올지 모르는 한 줄" 은 flex 가 맞고, 페이지 골격처럼 "칸이 먼저 정해진 것" 은 grid 가 맞다.
flex 아이템이 안 줄어드는 문제
flex 에서 가장 자주 만나는 함정이 있다. 긴 글이나 넓은 표가 들어간 아이템이 줄어들지 않고 컨테이너를 밀어낸다.
원인은 flex 아이템의 min-width 기본값이 auto 라는 것이다. 이 값은 "내용의 최소 크기보다 작아지지 않는다" 는 뜻이라, 줄바꿈할 수 없는 긴 문자열이 있으면 그만큼은 반드시 차지한다. [MDN 의 flex 비율 문서](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Controlling_ratios_of_flex_items_along_the_main_axis)가 이 동작을 설명한다.
.item { min-width: 0; } /* 또는 overflow: hidden */이 한 줄이 "왜 가로 스크롤이 생기지" 의 답인 경우가 아주 많다.
flex: 1 은 flex-grow: 1; flex-shrink: 1; flex-basis: 0% 의 축약이다. flex-basis: 0% 라서 내용 크기를 무시하고 남은 공간을 똑같이 나눈다. flex: auto 는 flex-basis: auto 라서 내용 크기에서 출발한다. 이 둘을 섞어 쓰면 예상과 다른 결과가 나온다.
grid 로 줄 수를 세지 않고 배치하기
grid 에는 미디어 쿼리 없이 반응하는 관용구가 있다.
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px;}"최소 240px 인 칸을 들어가는 만큼 만들고, 남는 자리는 똑같이 나눠라" 는 뜻이다. 화면 폭을 세지 않고도 칸 수가 알아서 바뀐다. [CSS Grid 2](https://www.w3.org/TR/css-grid-2/)가 auto-fit 과 auto-fill 의 차이를 정의한다 — auto-fill 은 빈 칸을 남기고, auto-fit 은 빈 칸을 접어 남은 아이템이 늘어난다.
컨테이너 질의
그래도 남는 문제가 있다. 위 관용구는 칸 수를 바꾸지만 카드 안의 배치는 못 바꾼다. 카드가 240px 일 때와 600px 일 때 안쪽 구조가 달라야 한다면, 카드가 자기 폭을 물어야 한다.
.card-wrap { container-type: inline-size; }@container (min-width: 420px) { .card { display: grid; grid-template-columns: 120px 1fr; }}container-type: inline-size 는 "이 요소의 인라인 방향 크기를 질의 대상으로 삼겠다" 는 선언이다. 그러면 그 자손이 @container 로 조상의 폭을 물을 수 있다. [CSS Containment 3](https://www.w3.org/TR/css-contain-3/)이 정의하고 [MDN 의 컨테이너 질의 문서](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries)가 사용법을 정리한다.
두 가지를 기억한다.
- 자기 자신은 못 묻는다. 컨테이너로 선언한 요소가 아니라 그 안쪽 요소에 규칙을 건다. 그래서 래퍼가 한 겹 필요하다
container-type: inline-size는 크기 담기(size containment) 를 켠다. 그 방향으로 자식이 부모 크기에 영향을 주지 않게 되므로, 높이를 내용에 맞추던 곳에서 예상 밖 결과가 날 수 있다
현장에서 만나는 모습
실제로 겪은 것 셋이다.
첫째, 같은 컴포넌트가 두 자리에서 다르게 망가진다. 본문에서는 멀쩡하고 사이드바에서만 깨진다. 이건 거의 언제나 미디어 쿼리를 써야 할 자리에 컨테이너 질의가 필요했던 경우다.
둘째, 가로 스크롤이 생겼는데 범인을 못 찾는다. flex 아이템의 min-width: auto 때문인 경우가 가장 많고, 다음이 width: 100vw(스크롤바 폭이 포함된다)다.
셋째, 미디어 쿼리 중단점이 열 개를 넘는다. 이건 대개 컴포넌트가 화면 폭을 물을 이유가 없는데 묻고 있다는 신호다. repeat(auto-fit, minmax(...)) 와 컨테이너 질의로 옮기면 중단점 대부분이 사라진다.
다음 퀴즈에서 확인할 것
flex 와 grid 의 갈림, flex: 1 과 flex: auto 의 차이, min-width: auto 가 만드는 넘침, auto-fit 과 auto-fill 의 차이, 그리고 컨테이너 질의가 미디어 쿼리와 무엇이 다른지를 확인합니다.