모던 웹에서 CSS Flex는 많이 사용되고 개념도 그리 어렵지는 않지만 내가 생각한 결과와 실제 브라우저에서 보여지는 레이아웃이 다르다면 당황스러울 수 있다. 플렉스 너비 결정 알고리즘을 이해한다면 플렉스 아이템의 크기가 왜 그렇게 결정되었는지, 왜 내가 생각한 대로 렌더링되지 않았는지 고민하느라 시간을 낭비하지 않을 수 있을 것이다.
문제를 간단하게 하기 위해 플렉스 아이템에 특별히 `position`속성을 설정하지 않은 경우, 그리고 플렉스메인 축(main axis)이 x축 방향일 때만을 고려해보자.(`flex-direction: column`이라면 동일한 내용을 y축에 대해 적용해주면 된다.)
definite size 컨테이너의 메인 축이 `width: 100px`, `width: 100%`와 같이 명시적으로 정해졌다면 그 값을 플렉스 아이템들이 차지할 수 있는 공간(available main space)으로 결정한다.
min-content, max-content 컨테이너에 min-content, max-content가 설정되어 있다면 각각 모든 아이템들이 최소한의 공간을 차지할 때, 최대한의 공간을 차지할 때의 크기로 플렉스 아이템들이 차지할 수 있는 공간(available main space)으로 결정한다.
그 외의 경우 명확한 크기도 min/max-content 제약 조건도 없다면 컨테이너의 margin, padding, border의 크기를 컨테이너가 사용 가능한 공간에서 뺀 값을 아이템들이 차지할 수 있는 공간으로 결정한다.
B. Determine Flex Base Size and Hypothetical Main Size
알고리즘에 대해 본격적으로 살펴보기 전에 알고리즘을 이해하기 위해 필수적으로 알아야 할 용어는 다음과 같다.
flex-basis 1. auto(기본값): 메인 축 방향의 `width` 속성값을 사용. `width` 또한 auto라면 `flex-basis: content`가 사용됨 2. content: 메인 축 방향의 `max-content` 크기.
inner size # 해당 요소의 content-box size로, padding과 border를 제외한 순수 컨텐츠 영역의 크기이다.
outer size # 해당 요소의 margin-box size로, inner size에 padding, border, margin이 더해진 영역의 크기이다.
1. Flex Base Size
1. `flex-basis`: 그 값을 아이템의 flex base size로 결정한다. 2. `flex-basis: content`: 플렉스 아이템의 메인 축에 대한 max-content 크기로 flex base size를 결정한다. 3. `aspect-ratio`: `flex-basis: content`이고 수직 축(cross size)가 결정된 값이라면 수직 축의 사이즈와 aspect-ratio로 계산된 값을 flex base size로 결정한다.
이렇게 지정된 `flex-basis`를 기반으로 아이템의 flex base size를 결정하는데,
위 두 가지는 아래 언급할 Hypothetical Main Size를 결정할 때 고려된다. 한편, 두번째 조건인 content-box size가 0보다 작아질 때 올림 처리를 무시한다는 건 무슨 의미일까? 애초에 content-box size가 0보다 작아질 수 있을까?
우선, 일반 블록 요소에서는 위와 같이 스타일을 설정하면 화면에 보여지는 요소의 크기는 양쪽 padding을 합해 20px로 보여진다. `box-sizing: border-box`이고 `width: 0`인데 왜 전체 너비가 0px이 아닐까? content-box size와 padding은 별개의 개념이고 content-box size는 0보다 작아질 수 없기 때문이다. 따라서 `.box` 요소의 content-box width는 -20px에서 0으로 올림 처리되고, 그 결과 화면에는 20px로 보여진다. #
The content width and height are calculated by subtracting the border and padding widths of the respective sides from the specified width and height properties. As the content width and height cannot be negative, this computation is floored at 0.
플렉스 레이아웃에서도 이런 상황이 생길 수 있다. 블록 레이아웃에서 content-box width에 대응되는 값은 플렉스 레이아웃에서 Inner Flex Base Size인데 이 값에 그러한 올림 처리를 적용하지 않겠다는 의미이다. `.flex-item` 요소를 예로 들어보자. 이 요소에 `flex-basis`가 별도로 지정되어 있지 않기 때문에 기본값인 auto(= width)이고, `box-sizing: border-box`, `margin: 0`이기 때문에Outer Flex Base Size는 0이 된다. 따라서Inner Flex Base Size는 outer basis에서 margin, padding, border를 제외한 -20px이 된다. 플렉스 레이아웃에서도 이렇게 content-box size가 0보다 작아질 수 있지만 일단 지금 단계에서는Inner Flex Base Size를 0으로 올림하지 않고 -20px 그대로 가져간다는 의미이다.
`box-sizing: border-box`조건이 붙었는데 그렇다면 `box-sizing`이 `border-box`인지 `content-box`인지에 따라Flex Base Size가 달라질 수 있다는 의미일까? 결론부터 말하면 그렇다.
`.one`과 `.two` 요소는 box-sizing 속성 외에 모든 값이 동일하다. 하지만 두 요소가 화면에 렌더링되는 결과를 보면 둘의 크기가 다른 걸 확인할 수 있는데 왜 다른지 한번 살펴보자.
`.one` - content-box width: `flex-basis`인 30px은 border-box width와 같기 때문에 양쪽 padding을 제외한 10px. - inner flex base size: content-box width와 같은 10px. - outer flex base size: content-box width에 padding, border, margin을 더한 30px.
`.two` - content-box width: `flex-basis`인 30px은 content-box width와 같기 때문에 30px. - inner flex base size: content-box width와 같은 30px. - outer flex base size: content-box width에 padding, border, margin을 더한 50px.
이렇게 `flex-basis` 값이 동일하더라도 플렉스 아이템의 `box-sizing`에 따라서 렌더링되는 크기는 다를 수 있으니 주의해야 한다.
2. Hypothetical Main Size
Flex Basis Size에서 해당 아이템의 used min/max size를 제약 조건으로 걸어 아이템의 가상 메인 사이즈(Hypothetical Main Size)를 결정한다. 여기서 말하는 계산된 min/max 크기는 플렉스 아이템에 지정된 `min-width`또는`max-width`속성값 그 자체를 의미하는 게 아니라 실제로 계산된 min/max 크기를 말한다. 예를 들어, `max-width: 100%`이라면 max Hypothetical Main Size는 100%가 아니라 실제로 계산된 크기(ex. 부모 요소의 크기가 400px이라면 400px)를 말한다. `min-width`, `max-width` 두 값을 명시적으로 설정하지 않았다면 3번 단계에서 결정될 Automatic Minimum Size m에 대해 Hypothetical Main Size는 결국 `max(m, FLEX_BASE_SIZE)`로 결정된다고 볼 수 있다. (참고) used min/max size의 예시
Flex Base Size `flex-basis`가 지정되었다면 해당 값. 지정되지 않았다면(기본값은 `auto`) 해당 아이템의 main size(`width` 또는 `height`)에 따라 결정되고, main size또한 지정되지 않았다면 `max-content`가 사용된다. 플렉스 아이템의 크기를 결정할 때 Initial Main Size로 사용되는 값. Hypothetical Main Size Flex Base Size에 used min/max size 제약 조건이 고려되어 결정되는 값.
CSS에서 `min-width`를 설정하지 않았을 때 그 기본값은 0이다. 하지만 플렉스 레이아웃에서는 min size를 조금 더 합리적으로 결정하기 위해 0 대신 별도로 계산된 값을 used min size로 사용한다. 플렉스 아이템이 Scroll Container인지 여부에 따라 사용되는 minimum size가 다른데 Scroll Container는 `overflow: auto, scroll`이 설정된 요소라고 생각하면 된다.
플렉스 아이템에 `aspect-ratio` 속성이 있다면 Specified Size Suggestion와 Content Size Suggestion의 최솟값이 아니지만 일단 그런 경우까지는 생각하지 않도록 하자.
Specified Size Suggestion: main size 속성값 Content Size Suggestion: min/max main size에 의해 clamp된 min-content 크기
Specified Size Suggestion와 Content Size Suggestion의 정의는 위와 같은데 `flex-direction: row`일 때 Specified Size Suggestion는 `width` 값.(definite `width`가 아니라면 `undefined`) Content Size Suggestion은 `clamp(min-width, min-content, max-width)` 값으로 생각할 수 있다.
플렉스 레이아웃이기 때문에 min size를 기본값인 0로 사용하든 이렇게 별도로 계산하든 큰 차이가 없을 것처럼 보이지만 automatic minimum size로 인해 결과가 달라지는 경우가 있다. 자세한 내용은 플렉스 아이템 너비 결정 알고리즘을 끝까지 살펴봐야 이해할 수 있기 때문에 CSS Flex 아이템 크기 결정 알고리즘: 예시 편을 참고하도록 하자.
2. Main Size Determination
각 아이템들이 컨테이너에서 어느 정도의 크기를 차지할지 결정하기 전에 플렉스 아이템들을 플렉스 컨테이너에 차례대로 쌓는 과정이다. 이 단계에서의 동작은 플렉스 컨테이너의 `flex-wrap` 속성값에 따라 달라진다.
플렉스 컨테이너는 multi-line으로, 플렉스 아이템을 하나하나 한 줄에 채워 나가는데 이때 사용되는 아이템의 크기는 outer hypothetical main size이다. 현재 아이템의 "다음" 아이템이 플렉스 컨테이너의 inner main size에 들어가지 못하고 넘치게 된다면 "다음" 아이템을 다음 줄에 채운다. 만약, 다음 줄로 넘어가게 된 아이템의 크기가 inner main size보다 크더라도 그 줄에 그대로 채운다.
플렉스 컨테이너에서 각각의 플렉스 아이템이 실제로 차지하는 크기Target Main Size를 결정하는 단계이다. 앞서 구한 Flex Base Size, Hypothetical Main Size는 이 Target Main Size를 구하기 위한 초기값인 셈이다.
Main Size: 메인 축(main axis) 방향에 해당하는 크기 속성을 나타낸다. `flex-direction: row`이라면 `width` 속성을 의미한다.
A. Outer Hypothetical Main Size 합산
각 플렉스 아이템의 Outer Hypothetical Main Size를 합한다. 아무리 플렉스 레이아웃이라고 하더라도 padding까지 같이 유연하게 변하는 건 말이 안되기 때문에 어떻게 보면 당연하다고 할 수 있다. 나중에 예시로도 살펴보겠지만 특정 플렉스 아이템에 padding이 존재한다면 모든 아이템들의 `flex-grow`와 `flex-shrink`값을 동일하게 설정하더라도 아이템들의 크기가 동일하지 않을 수 있다. 그 이유가 바로 Outer Hypothetical Main Size를 고려하기 때문이다.
B. Flex Factor 결정
1에서 합한 크기가 플렉스 컨테이너의Inner Main Size를 초과하면 flex shrink factor를 사용하고, 그렇지 않으면 flex grow factor를 사용한다.
C. Inflexible Item 결정
inflexible한 플렉스 아이템들의 크기를 결정한다. inflexible한 아이템은 다음과 같은 경우를 말하는데 이러한 아이템들의Target Main Size는 Hypothetical Main Size로 결정되고 이후 과정에서 더이상 크기를 변경하지 않는다.(Freeze)
1. flex factor가 0인 경우: 예를 들어 `flex: 0 0 5rem`속성값이 지정된 아이템 2. B에서 flex grow factor를 사용할 때 Flex Base Size > Hypothetical Main Size인 경우 3. B에서 flex shrink factor를 사용할 때 Flex Base Size < Hypothetical Main Size인 경우
1번은 당연한 것 같은데 2번, 3번 조건은 언뜻 봤을 때 무슨 의미인지 잘 와닿지 않는다. 우리는 앞서 Hypothetical Main Size가 Flex Base Size에 used min/max size 제약이 적용되어 결정된 값임을 알았다. flex grow factor를 사용한다면 플렉스 아이템의 크기를 늘릴 수 있어야 한다는 것인데 Hypothetical Main Size가Flex Base Size보다 작다는 말은 `hypothtical main size = used max size < flex base size`라는 의미이다. 즉, 해당 아이템의 크기를 더 이상 늘릴 수 없기 때문에 inflexible 아이템으로 취급하는 것이다. 3번 조건도 동일한 논리로 `flex base size < used min size = hypothtical main size`로 더 이상 줄일 수 없기 때문이다.
이렇게 되면 이후 단계에서 아이템의 크기를 Flex Base Size가 아니라 Hypothetical Main Size로 사용하는 이유를 짐작할 수 있을 것이다. Flex Base Size는 used min/max size 조건이 고려되지 않은 값이기 때문이다.
D. Initial Free Space 계산
플렉스 컨테이너의 innermain size에서 모든 플렉스 아이템들의 outersize를 뺀 값을 계산한다. 즉, 플렉스 컨테이너의 content-box size에서 각 플렉스 아이템들의 margin, padding, border, content-box size를 뺀 값이다. 이때, frozen 아이템은 C 단계에서 크기가 결정된 inflexible 아이템을 가리킨다.
Frozen Item: outer size = Outer Hypothetical Main Size → Outer Target Main Size UnFrozen Item: outer size = Outer Flex Base Size
E. 플렉스 아이템들의 Target Main Size 계산
모든 플렉스 아이템들이 frozen될 때까지 아래 과정을 반복해서 얻은 값을 Target Main Size로 설정한다.
1. Remaining Free Space(R) 계산
unfrozen 상태인 플렉스 아이템들에 대해서 flex factor의 합을∑F,D 단계에서 구한 initial free space를 R0라고 하자. ∑F가 1보다 크다면R = R0로 두고, ∑F 가 1보다 작다면 R0 * ∑F값을 구하고 이 값을 R로 둔다. ( < R0 )
flex factor의 합이 1보다 작을 때는 왜 R을 다르게 계산할까? 예를 들어, 플렉스 컨테이너에 `flex-grow: 1`인 아이템 하나만 존재한다고 해보자. 플렉스 아이템이 하나뿐이므로 이 아이템은 컨테이너의 100% 크기를 차지하게 된다. 이제 이 `flex-grow`값을 0.1, 0.01, 0.001, ...로 0에 가깝게 줄여나가보자. 이 아이템은 계속 컨테이너의 100% 크기를 차지하다가 `flex-grow`가 충분히 0에 가깝게 되는 시점에 크기가 갑자기 0으로 줄어들게 된다. 이건 굉장히 부자연스러운 동작이라고 할 수 있을 것이다. 이렇게 `flex-grow`가 1에서 0으로 작아질 때 플렉스 아이템이 가져가는 크기가 free space의 100%에서 0%로 자연스럽게 작아질 수 있도록 의도한 결과라고 한다. #
... This pattern is required for continuous behavior as flex-grow approaches zero (which means the item wants none of the free space). Without this, a flex-grow: 1 item would take all of the free space; but so would a flex-grow: 0.1 item, and a flex-grow: 0.01 item, etc., until finally the value is small enough to underflow to zero and the item suddenly takes up none of the free space. With this behavior, the item instead gradually takes less of the free space as flex-grow shrinks below 1, smoothly transitioning to taking none of the free space at zero.
2. Flex Factor에 비례해 Remaining Free Space 분배
①R = 0이라면 이 단계에서 아무것도 하지 않는다.
② B 단계에서 flex grow factor를 사용하는 것으로 결정되었다면, flex factor의 합을∑F 와 각 unfrozen 플렉스 아이템에 설정된 flex grow factor Fg의 비율인 Fn / F을 구하고 R에서 이 비율에 해당하는 값을 flex base size에 더한다.
UnFrozen Item: outer size = Outer Flex Base Size + R * ( Fg / ∑F)→ Outer Target Main Size(잠정)
③B 단계에서 flex shrink factor를 사용하는 것으로 결정되었다면, 각 unfrozen 플렉스 아이템에 대해 inner flex base size에 아이템에 설정된 flex shrink factor Fs를 곱한 값 scaled flex shrink factor( = s )을 구하고, 이 값들을 모두 더한 값을 s1 + s2 + s3 + ... = ∑S라고 하자. 이 합과 각 플렉스 아이템의 scaled flex shrink factor의 비율인 s / ∑S을 구하고 R에서 이 비율에 해당하는 값의 절대값을 flex base size에서 뺀다. 그 결과의 inner main size는 0보다 작아질 수 있다.
UnFrozen Item: outer size = Outer Flex Base Size - | R * ( s / ∑S) | → Outer Target Main Size(잠정)
3. min/max 위반 수정
unfrozen 아이템들에 대해, 2번 단계에서 결정된 target main size가 used min size보다 작다면 used min size로 조정하고, used max size보다 크다면 used max size로 조정한다. 만약, 아이템의 content-box size가 0보다 작다면 0으로 올림 처리하고 targe main size도 그에 맞게 조정한다. 이 과정에서 각 아이템들에 어떤 위반이 발생했는지 체크하고 다음 단계로 넘어간다.
max violation: 조정 후 target main size가 작아진 경우 min violation: 조정 후 target main size가 커진 경우
조정 후 크기가 작아졌을 때 max violation이고, 커졌을 때 min violation이라는 게 조금 혼란스러울 수 있지만 used min/max size가 그 기준이기 때문이다. 조정 후에 target main size가 "작아졌다"는 건 used max size를 위반해 크기를 줄였기 때문에 max violation이고, "커졌다"라는 건 used minsize를 위반해 크기를 늘렸기 때문이다.
4. 과잉 조정 아이템 고정
3번 단계를 적용하기 전의 크기를 unclamped size, 적용 후의 크기를 clamped size라고 했을 때, 각각의 아이템에 대해 둘의 차이를 모두 더한다. 즉, Total Violation = ∑(clamped size - unclamped size). Total Violation은 3번 단계에서 교정 후 Free Space에 - 부호를 붙인 값과 같다. 예를 들어, 2번 단계에서 Free Space를 모두 분해 후 아이템의 크기가 각각 60px, 60px이었다고 해보자. 이때 Free Space는 0이다. 3번 단계에서 used min size가 둘다 70px이라면 각각의 clamped size가 70px이 되고, Total Violation은 20px이 된다. 그런데 Free Space가 0일 때 두 아이템의 크기는 각각 60px이었는데 70px이 되어 이전보다 20px을 더 차지하게 되었으므로 Free Space는 -20px이 된다.
Total Violation = 0 : 모든 아이템을 freeze하고 루프를 종료 Total Violation > 0 : min violation 아이템들만 freeze 후, 나머지에 대해서 1번 단계부터 다시 진행 Total Violation < 0 :max violation 아이템들만 freeze 후, 나머지에 대해서 1번 단계부터 다시 진행
Total Violation이 0이라는 의미는 3번 단계에서 각각의 아이템들의 크기를 교정하면서 Free Space가 0에서 달라지지 않았다는 의미이기 때문에 더이상 할 작업이 없다. Total Violation이 0보다 크다는 것은 교정 후 Free Space가 부족해졌다는 의미이기 때문에 아이템의 크기를 다시 줄여야 한다. 하지만 used min size에 맞게 조정된 아이템들은 크기를 더이상 줄일 수 없기 때문에 이것들의 크기는 고정해야 한다. 대신 used max size에 맞춰 줄어든 아이템의 크기를 더 줄이거나, used min/max size 사이의 크기를 가지는 아이템의 크기를 줄이는 선택지가 있다. 그래서 min violation 아이템을 제외한 unfrozen 아이템에 대해 루프를 다시 진행하는 것이다. 동일한 논리로 Total Violation이 0보다 작은 경우에 대해서도 설명할 수 있다.
이쯤되면 왜 3번 단계에서 크기를 조정한 후 4번 단계에서 전체 조정 크기 합계를 구해 조정하는 과정을 두 번이나 거치는지 이해가 됐을지도 모르겠다. 3번 단계에서 크기를 조정하면 2번 단계에서 Free Space를 모두 배분한 결과에서 변동이 생기므로 4번 단계에서 다시 Free Space가 0이 될 수 있도록 각각의 아이템의 크기를 다시 조정하는 것이다.
여기까지가 Flex 레이아웃에서 개별 플렉스 아이템의 너비를 결정하는 알고리즘이다. 지금까지의 알고리즘을 완전히 이해하지 못했더라도 실제로 플렉스 아이템의 크기가 어떻게 결정되는지 계산하는 것은 어렵지 않다.CSS Flex 아이템 크기 결정 알고리즘: 예시 편 글에서 실제 예시를 통해 이 알고리즘이 어떻게 적용되는지 살펴보자.
CSS Flex 아이템 크기 결정 알고리즘: 이론 편
모던 웹에서 CSS Flex는 많이 사용되고 개념도 그리 어렵지는 않지만 내가 생각한 결과와 실제 브라우저에서 보여지는 레이아웃이 다르다면 당황스러울 수 있다. 플렉스 너비 결정 알고리즘을 이해한다면 플렉스 아이템의 크기가 왜 그렇게 결정되었는지, 왜 내가 생각한 대로 렌더링되지 않았는지 고민하느라 시간을 낭비하지 않을 수 있을 것이다.
문제를 간단하게 하기 위해 플렉스 아이템에 특별히 `position`속성을 설정하지 않은 경우, 그리고 플렉스 메인 축(main axis)이 x축 방향일 때만을 고려해보자.(`flex-direction: column`이라면 동일한 내용을 y축에 대해 적용해주면 된다.)
1. Line Length Determination
A. Flex 아이템들이 차지할 수 있는 공간 계산
컨테이너의 메인 축이 `width: 100px`, `width: 100%`와 같이 명시적으로 정해졌다면 그 값을 플렉스 아이템들이 차지할 수 있는 공간(available main space)으로 결정한다.
컨테이너에 min-content, max-content가 설정되어 있다면 각각 모든 아이템들이 최소한의 공간을 차지할 때, 최대한의 공간을 차지할 때의 크기로 플렉스 아이템들이 차지할 수 있는 공간(available main space)으로 결정한다.
명확한 크기도 min/max-content 제약 조건도 없다면 컨테이너의 margin, padding, border의 크기를 컨테이너가 사용 가능한 공간에서 뺀 값을 아이템들이 차지할 수 있는 공간으로 결정한다.
* 컨테이너의 크기가 설정되지 않았다면 그 크기가 무한대인가?
* max-content로 설정되어 있다면 그 크기가 무한대가 될 수 있나?
* width의 기본값은 auto인데 그 너비는 어떻게 결정되는가?
* definite size가 정확히 뭘 의미하는가?
https://www.w3.org/TR/css-flexbox-1/#definite
B. Determine Flex Base Size and Hypothetical Main Size
알고리즘에 대해 본격적으로 살펴보기 전에 알고리즘을 이해하기 위해 필수적으로 알아야 할 용어는 다음과 같다.
1. Flex Base Size
이렇게 지정된 `flex-basis`를 기반으로 아이템의 flex base size를 결정하는데,
위 두 가지는 아래 언급할 Hypothetical Main Size를 결정할 때 고려된다. 한편, 두번째 조건인 content-box size가 0보다 작아질 때 올림 처리를 무시한다는 건 무슨 의미일까? 애초에 content-box size가 0보다 작아질 수 있을까?
우선, 일반 블록 요소에서는 위와 같이 스타일을 설정하면 화면에 보여지는 요소의 크기는 양쪽 padding을 합해 20px로 보여진다. `box-sizing: border-box`이고 `width: 0`인데 왜 전체 너비가 0px이 아닐까? content-box size와 padding은 별개의 개념이고 content-box size는 0보다 작아질 수 없기 때문이다. 따라서 `.box` 요소의 content-box width는 -20px에서 0으로 올림 처리되고, 그 결과 화면에는 20px로 보여진다. #
플렉스 레이아웃에서도 이런 상황이 생길 수 있다. 블록 레이아웃에서 content-box width에 대응되는 값은 플렉스 레이아웃에서 Inner Flex Base Size인데 이 값에 그러한 올림 처리를 적용하지 않겠다는 의미이다. `.flex-item` 요소를 예로 들어보자. 이 요소에 `flex-basis`가 별도로 지정되어 있지 않기 때문에 기본값인 auto(= width)이고, `box-sizing: border-box`, `margin: 0`이기 때문에 Outer Flex Base Size는 0이 된다. 따라서 Inner Flex Base Size는 outer basis에서 margin, padding, border를 제외한 -20px이 된다. 플렉스 레이아웃에서도 이렇게 content-box size가 0보다 작아질 수 있지만 일단 지금 단계에서는 Inner Flex Base Size를 0으로 올림하지 않고 -20px 그대로 가져간다는 의미이다.
`box-sizing: border-box`조건이 붙었는데 그렇다면 `box-sizing`이 `border-box`인지 `content-box`인지에 따라 Flex Base Size가 달라질 수 있다는 의미일까? 결론부터 말하면 그렇다.
`.one`과 `.two` 요소는 box-sizing 속성 외에 모든 값이 동일하다. 하지만 두 요소가 화면에 렌더링되는 결과를 보면 둘의 크기가 다른 걸 확인할 수 있는데 왜 다른지 한번 살펴보자.
- content-box width: `flex-basis`인 30px은 border-box width와 같기 때문에 양쪽 padding을 제외한 10px.
- inner flex base size: content-box width와 같은 10px.
- outer flex base size: content-box width에 padding, border, margin을 더한 30px.
- content-box width: `flex-basis`인 30px은 content-box width와 같기 때문에 30px.
- inner flex base size: content-box width와 같은 30px.
- outer flex base size: content-box width에 padding, border, margin을 더한 50px.
이렇게 `flex-basis` 값이 동일하더라도 플렉스 아이템의 `box-sizing`에 따라서 렌더링되는 크기는 다를 수 있으니 주의해야 한다.
2. Hypothetical Main Size
Flex Basis Size에서 해당 아이템의 used min/max size를 제약 조건으로 걸어 아이템의 가상 메인 사이즈(Hypothetical Main Size)를 결정한다. 여기서 말하는 계산된 min/max 크기는 플렉스 아이템에 지정된 `min-width`또는`max-width`속성값 그 자체를 의미하는 게 아니라 실제로 계산된 min/max 크기를 말한다. 예를 들어, `max-width: 100%`이라면 max Hypothetical Main Size는 100%가 아니라 실제로 계산된 크기(ex. 부모 요소의 크기가 400px이라면 400px)를 말한다. `min-width`, `max-width` 두 값을 명시적으로 설정하지 않았다면 3번 단계에서 결정될 Automatic Minimum Size m에 대해 Hypothetical Main Size는 결국 `max(m, FLEX_BASE_SIZE)`로 결정된다고 볼 수 있다. (참고) used min/max size의 예시
예를 들어, `.flex-item`요소에는 `min-width`또는`max-width`속성이 지정되지 않았지만 padding과 border를 합쳐 최소한 22px의 공간을 차지해야 한다. 이 22px이 바로 Hypothetical Main Size이다.
* used min/max size가 정확히 무엇을 의미하는가
https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
3. 플렉스 아이템의 Automatic Minimum size #
CSS에서 `min-width`를 설정하지 않았을 때 그 기본값은 0이다. 하지만 플렉스 레이아웃에서는 min size를 조금 더 합리적으로 결정하기 위해 0 대신 별도로 계산된 값을 used min size로 사용한다. 플렉스 아이템이 Scroll Container인지 여부에 따라 사용되는 minimum size가 다른데 Scroll Container는 `overflow: auto, scroll`이 설정된 요소라고 생각하면 된다.
플렉스 아이템에 `aspect-ratio` 속성이 있다면 Specified Size Suggestion와 Content Size Suggestion의 최솟값이 아니지만 일단 그런 경우까지는 생각하지 않도록 하자.
Specified Size Suggestion와 Content Size Suggestion의 정의는 위와 같은데 `flex-direction: row`일 때 Specified Size Suggestion는 `width` 값.(definite `width`가 아니라면 `undefined`) Content Size Suggestion은 `clamp(min-width, min-content, max-width)` 값으로 생각할 수 있다.
플렉스 레이아웃이기 때문에 min size를 기본값인 0로 사용하든 이렇게 별도로 계산하든 큰 차이가 없을 것처럼 보이지만 automatic minimum size로 인해 결과가 달라지는 경우가 있다. 자세한 내용은 플렉스 아이템 너비 결정 알고리즘을 끝까지 살펴봐야 이해할 수 있기 때문에 CSS Flex 아이템 크기 결정 알고리즘: 예시 편을 참고하도록 하자.
2. Main Size Determination
각 아이템들이 컨테이너에서 어느 정도의 크기를 차지할지 결정하기 전에 플렉스 아이템들을 플렉스 컨테이너에 차례대로 쌓는 과정이다. 이 단계에서의 동작은 플렉스 컨테이너의 `flex-wrap` 속성값에 따라 달라진다.
A. nowrap
플렉스 컨테이너는 single-line으로 모든 플렉스 아이템들을 한 줄에 모두 담는다.
B. wrap 또는 wrap-reverse
플렉스 컨테이너는 multi-line으로, 플렉스 아이템을 하나하나 한 줄에 채워 나가는데 이때 사용되는 아이템의 크기는 outer hypothetical main size이다. 현재 아이템의 "다음" 아이템이 플렉스 컨테이너의 inner main size에 들어가지 못하고 넘치게 된다면 "다음" 아이템을 다음 줄에 채운다. 만약, 다음 줄로 넘어가게 된 아이템의 크기가 inner main size보다 크더라도 그 줄에 그대로 채운다.
3. Resolve Flexible Lengths #
플렉스 컨테이너에서 각각의 플렉스 아이템이 실제로 차지하는 크기 Target Main Size를 결정하는 단계이다. 앞서 구한 Flex Base Size, Hypothetical Main Size는 이 Target Main Size를 구하기 위한 초기값인 셈이다.
A. Outer Hypothetical Main Size 합산
각 플렉스 아이템의 Outer Hypothetical Main Size를 합한다. 아무리 플렉스 레이아웃이라고 하더라도 padding까지 같이 유연하게 변하는 건 말이 안되기 때문에 어떻게 보면 당연하다고 할 수 있다. 나중에 예시로도 살펴보겠지만 특정 플렉스 아이템에 padding이 존재한다면 모든 아이템들의 `flex-grow`와 `flex-shrink`값을 동일하게 설정하더라도 아이템들의 크기가 동일하지 않을 수 있다. 그 이유가 바로 Outer Hypothetical Main Size를 고려하기 때문이다.
B. Flex Factor 결정
1에서 합한 크기가 플렉스 컨테이너의 Inner Main Size를 초과하면 flex shrink factor를 사용하고, 그렇지 않으면 flex grow factor를 사용한다.
C. Inflexible Item 결정
inflexible한 플렉스 아이템들의 크기를 결정한다. inflexible한 아이템은 다음과 같은 경우를 말하는데 이러한 아이템들의Target Main Size는 Hypothetical Main Size로 결정되고 이후 과정에서 더이상 크기를 변경하지 않는다.(Freeze)
1번은 당연한 것 같은데 2번, 3번 조건은 언뜻 봤을 때 무슨 의미인지 잘 와닿지 않는다. 우리는 앞서 Hypothetical Main Size가 Flex Base Size에 used min/max size 제약이 적용되어 결정된 값임을 알았다. flex grow factor를 사용한다면 플렉스 아이템의 크기를 늘릴 수 있어야 한다는 것인데 Hypothetical Main Size가 Flex Base Size보다 작다는 말은 `hypothtical main size = used max size < flex base size`라는 의미이다. 즉, 해당 아이템의 크기를 더 이상 늘릴 수 없기 때문에 inflexible 아이템으로 취급하는 것이다. 3번 조건도 동일한 논리로 `flex base size < used min size = hypothtical main size`로 더 이상 줄일 수 없기 때문이다.
이렇게 되면 이후 단계에서 아이템의 크기를 Flex Base Size가 아니라 Hypothetical Main Size로 사용하는 이유를 짐작할 수 있을 것이다. Flex Base Size는 used min/max size 조건이 고려되지 않은 값이기 때문이다.
D. Initial Free Space 계산
플렉스 컨테이너의 inner main size에서 모든 플렉스 아이템들의 outer size를 뺀 값을 계산한다. 즉, 플렉스 컨테이너의 content-box size에서 각 플렉스 아이템들의 margin, padding, border, content-box size를 뺀 값이다. 이때, frozen 아이템은 C 단계에서 크기가 결정된 inflexible 아이템을 가리킨다.
E. 플렉스 아이템들의 Target Main Size 계산
모든 플렉스 아이템들이 frozen될 때까지 아래 과정을 반복해서 얻은 값을 Target Main Size로 설정한다.
1. Remaining Free Space(R) 계산
unfrozen 상태인 플렉스 아이템들에 대해서 flex factor의 합을 ∑F, D 단계에서 구한 initial free space를 R0라고 하자. ∑F 가 1보다 크다면 R = R0로 두고, ∑F 가 1보다 작다면 R0 * ∑F 값을 구하고 이 값을 R로 둔다. ( < R0 )
flex factor의 합이 1보다 작을 때는 왜 R을 다르게 계산할까? 예를 들어, 플렉스 컨테이너에 `flex-grow: 1`인 아이템 하나만 존재한다고 해보자. 플렉스 아이템이 하나뿐이므로 이 아이템은 컨테이너의 100% 크기를 차지하게 된다. 이제 이 `flex-grow`값을 0.1, 0.01, 0.001, ...로 0에 가깝게 줄여나가보자. 이 아이템은 계속 컨테이너의 100% 크기를 차지하다가 `flex-grow`가 충분히 0에 가깝게 되는 시점에 크기가 갑자기 0으로 줄어들게 된다. 이건 굉장히 부자연스러운 동작이라고 할 수 있을 것이다. 이렇게 `flex-grow`가 1에서 0으로 작아질 때 플렉스 아이템이 가져가는 크기가 free space의 100%에서 0%로 자연스럽게 작아질 수 있도록 의도한 결과라고 한다. #
2. Flex Factor에 비례해 Remaining Free Space 분배
① R = 0이라면 이 단계에서 아무것도 하지 않는다.
② B 단계에서 flex grow factor를 사용하는 것으로 결정되었다면, flex factor의 합을 ∑F 와 각 unfrozen 플렉스 아이템에 설정된 flex grow factor Fg의 비율인 Fn / F을 구하고 R에서 이 비율에 해당하는 값을 flex base size에 더한다.
③ B 단계에서 flex shrink factor를 사용하는 것으로 결정되었다면, 각 unfrozen 플렉스 아이템에 대해 inner flex base size에 아이템에 설정된 flex shrink factor Fs를 곱한 값 scaled flex shrink factor( = s )을 구하고, 이 값들을 모두 더한 값을 s1 + s2 + s3 + ... = ∑S 라고 하자. 이 합과 각 플렉스 아이템의 scaled flex shrink factor의 비율인 s / ∑S을 구하고 R에서 이 비율에 해당하는 값의 절대값을 flex base size에서 뺀다. 그 결과의 inner main size는 0보다 작아질 수 있다.
3. min/max 위반 수정
unfrozen 아이템들에 대해, 2번 단계에서 결정된 target main size가 used min size보다 작다면 used min size로 조정하고, used max size보다 크다면 used max size로 조정한다. 만약, 아이템의 content-box size가 0보다 작다면 0으로 올림 처리하고 targe main size도 그에 맞게 조정한다. 이 과정에서 각 아이템들에 어떤 위반이 발생했는지 체크하고 다음 단계로 넘어간다.
조정 후 크기가 작아졌을 때 max violation이고, 커졌을 때 min violation이라는 게 조금 혼란스러울 수 있지만 used min/max size가 그 기준이기 때문이다. 조정 후에 target main size가 "작아졌다"는 건 used max size를 위반해 크기를 줄였기 때문에 max violation이고, "커졌다"라는 건 used min size를 위반해 크기를 늘렸기 때문이다.
4. 과잉 조정 아이템 고정
3번 단계를 적용하기 전의 크기를 unclamped size, 적용 후의 크기를 clamped size라고 했을 때, 각각의 아이템에 대해 둘의 차이를 모두 더한다. 즉, Total Violation = ∑(clamped size - unclamped size). Total Violation은 3번 단계에서 교정 후 Free Space에 - 부호를 붙인 값과 같다. 예를 들어, 2번 단계에서 Free Space를 모두 분해 후 아이템의 크기가 각각 60px, 60px이었다고 해보자. 이때 Free Space는 0이다. 3번 단계에서 used min size가 둘다 70px이라면 각각의 clamped size가 70px이 되고, Total Violation은 20px이 된다. 그런데 Free Space가 0일 때 두 아이템의 크기는 각각 60px이었는데 70px이 되어 이전보다 20px을 더 차지하게 되었으므로 Free Space는 -20px이 된다.
Total Violation이 0이라는 의미는 3번 단계에서 각각의 아이템들의 크기를 교정하면서 Free Space가 0에서 달라지지 않았다는 의미이기 때문에 더이상 할 작업이 없다. Total Violation이 0보다 크다는 것은 교정 후 Free Space가 부족해졌다는 의미이기 때문에 아이템의 크기를 다시 줄여야 한다. 하지만 used min size에 맞게 조정된 아이템들은 크기를 더이상 줄일 수 없기 때문에 이것들의 크기는 고정해야 한다. 대신 used max size에 맞춰 줄어든 아이템의 크기를 더 줄이거나, used min/max size 사이의 크기를 가지는 아이템의 크기를 줄이는 선택지가 있다. 그래서 min violation 아이템을 제외한 unfrozen 아이템에 대해 루프를 다시 진행하는 것이다. 동일한 논리로 Total Violation이 0보다 작은 경우에 대해서도 설명할 수 있다.
이쯤되면 왜 3번 단계에서 크기를 조정한 후 4번 단계에서 전체 조정 크기 합계를 구해 조정하는 과정을 두 번이나 거치는지 이해가 됐을지도 모르겠다. 3번 단계에서 크기를 조정하면 2번 단계에서 Free Space를 모두 배분한 결과에서 변동이 생기므로 4번 단계에서 다시 Free Space가 0이 될 수 있도록 각각의 아이템의 크기를 다시 조정하는 것이다.
여기까지가 Flex 레이아웃에서 개별 플렉스 아이템의 너비를 결정하는 알고리즘이다. 지금까지의 알고리즘을 완전히 이해하지 못했더라도 실제로 플렉스 아이템의 크기가 어떻게 결정되는지 계산하는 것은 어렵지 않다. CSS Flex 아이템 크기 결정 알고리즘: 예시 편 글에서 실제 예시를 통해 이 알고리즘이 어떻게 적용되는지 살펴보자.
[참고]
How exactly is calculated hypothetical main size of flex item - CSS Spec question
'CSS' 카테고리의 다른 글