CSS 플렉스박스(flex)로 반응형 레이아웃 만들기
플렉스박스는 이름 그대로 레이아웃을 만들 때 아주 유연하고 동적인 구조를 유지할 수 있는 박스 모델입니다.
유연한 레이아웃을 유지하는 플렉스박스의 속성을 잘 활용하면 미디어쿼리 없이도 웹브라우저 너비와 다양한 디바이스에 대응하는 반응형 레이아웃을 만들 수 있습니다.
미디어쿼리를 이용한 CSS 속성 재정의보다 적은 CSS 작성으로 반응형 레이아웃을 만들 수 있기 때문에, 코드 유지보수 또한 더 단순하고 효율적으로 할 수 있습니다.
플렉스박스로 레이아웃을 만들기 위해서는 몇 가지 중요한 플렉스박스 속성에 대해 알아야 합니다.
레이아웃을 만드는 플렉스박스 속성들
"display: flex;"를 선언한 요소를 플렉스박스, 또는 플렉스박스 컨테이너 라고 하며, 플렉스박스 요소들을 담는 그릇의 역할을 합니다. 플렉스박스 내부의 바로 하위에 있는 자식 요소들은 플렉스박스 아이템이 됩니다.
플렉스박스는 다음 6개의 속성을 사용해서 만듭니다.
속성 | 대상 | 기본값 | 설명 |
flex-wrap | 플렉스박스 컨테이너 | nowrap | "wrap", "nowrap", "wrap-reverse" 사용 가능. 플렉스박스 아이템을 배치하는 방법을 선택합니다. wrap : 인라인으로 아이템을 가로로 배치하며, 행 안에 아이템이 다 차면 다음 행으로 아이템이 이동합니다. |
flex-direction | 플렉스박스 컨테이너 | row | "row"는 가로 방향(왼쪽 위 에서 오른쪽으로), "column"은 세로 방향(왼쪽 위에서 아래로) 으로 아이템들을 배치합니다. 가로 방향은 속성 설정에 따라 여러 줄로 표현이 가능하며, 세로 방향은 세로로 1줄 표현만 가능합니다. 반대 방향으로 아이템을 배치할 수 있는 반대 방향 속성 값도 제공됩니다. "row-reverse"는 오른쪽 아래에서 왼쪽으로(그리고 위로 행이 추가됨), "column-reverse"는 왼쪽 아래에서 위로 아이템이 배치됩니다. |
gap | 플렉스박스 컨테이너 | 0 | 플렉스박스 아이템 사이의 여백을 지정합니다. "gap: 열여백 행여백;" 으로 표현하며, 값이 같으면 "gap: 여백;" 으로 사용 가능합니다. 속성을 나누어서 "gap-row", "gap-column" 2개로 행과 열의 사이 여백을 각각 정의할 수 있습니다. |
flex-basis | 플렉스박스 아이템 | auto | auto, 또는 숫자 단위 너비 값 사용 가능. 플렉스박스 아이템의 기본 크기를 설정합니다. flex-grow, flex-shrink 속성 값이 미지정인 경우, 기본 크기로 고정됩니다. |
flex-grow | 플렉스박스 아이템 | 0 | 기본 크기(flex-basis)를 기준으로 한 행에 플렉스박스 아이템 들을 배치한 후, 남는 공간을 아이템들에 분배하는 비율. 비율만큼 아이템이 늘어남. 소수점 사용 가능. |
flex-shrink | 플렉스박스 아이템 | 1 | 한 행에 배치된 플렉스박스 아이템 들이 플렉스박스 영역을 넘을 경우, 모자라는 공간만큼 아이템들을 줄이는 비율입니다. 비율에 비례해서 아이템이 너비가 줄어들며 소수점도 사용 가능합니다. 플렉스박스 속성으ㅗㄹ "flex-wrap: wrap;" 이 적용되면 이 속성은 무시됩니다. |
다음처럼 4개의 아이템을 가진 플렉스박스를 기본으로 해서 레이아웃을 만들어 보겠습니다.
<section class="content">
<div class="flexbox">
<div class="item">content1</div>
<div class="item">content2</div>
<div class="item">content3</div>
<div class="item">content4</div>
</div>
</section>
플렉스박스 기본 구조
플렉스박스 CSS는 다음처럼 기본 구조를 가지는 것이 일반적입니다.
다음의 기본 CSS 구조를 기초로 속성 값을 적절히 조절해서 레이아웃을 변경하게 됩니다.
.content{
max-width: 800px;
margin: 0 auto;
}
.flexbox{
display: flex;
flex-wrap: wrap;
gap: 1em;
}
.item{
min-height: 200px;
flex-basis: 150px;
flex-grow: 1;
}
완성된 플렉스박스는 다음과 같이 보입니다.
"flex-basis" 속성에서 정한 기본 아이템 너비를 기준으로 남는 여백을 자동으로 채우면서 아이템들을 다음 행에 채워나갑니다. 레이아웃 너비인 "800px" 안에 "flex-basis" 속성에서 정한 아이템 너비의 합이 레이아웃 너비보다 더 커지면 한 행에 3개, 또는 2개의 아이템이 자동으로 배치됩니다.
앞서의 레이아웃이 자동으로 크기가 늘어나면서 남는 여백을 메꾸는 것은 "flex-grow" 속성이 0보다 큰 값으로 지정되어 있기 때문입니다. "flex-basis" 속성 값으로 정한 너비 이상으로 아이템이 늘어나지 않도록 하려면 다음처럼 플렉스박스 아이템들에 "flex-grow: 0;" 으로 속성을 설정해야 합니다.
.flexbox > *{
flex-grow: 0;
}
아이템들이 자동으로 여백을 채우지 않도록 설정하면 다음처럼 아이템들이 배치됩니다.
한줄로 표현하는 레이아웃
아이템들의 너비 합이 레이아웃 너비보다 넓으면 여러 행에 나누어져 아이템이 배치되도록 해주는 속성은 "flex-wrap: wrap;" 속성입니다. 이 속성이 없으면 플렉스박스 안의 아이템들은 다음처럼 한 행에 모두 표시됩니다. 그리고 아이템들의 너비 합이 플렉스박스 너비보다 넓으면 다음처럼 레이아웃 영역을 넘지 않도록 아이템들의 너비가 같은 비율로 줄어들면서 한 행으로 레이아웃 안에 배치됩니다.
이것은 "flex-shrink" 속성의 기본 값이 "1"이어서 "flex-shrink" 속성을 정의하지 않아도 기본 값 "1"이 적용되기 때문입니다.
레이아웃 너비보다 아이템 너비 합이 더 넓을 경우, 남는 너비는 각각의 아이템들 너비에서 동일한 비율 만큼씩 나누어서 줄어듭니다. 아이템 너비가 자동으로 축소되지 않도록 최소 크기를 "flex-basis" 에서 정한 크기로 최소 너비로 제한하려면 "flex-shrink: 0;" 을 별도로 지정해야 합니다.
"flex-shrink: 0;" 속성은 "flex-basis" 에서 정한 너비보다 줄어들지 않기 때문에 다음처럼 레이아웃 오른쪽 바깥으로 아이템이 삐져나오게 될 수도 있습니다.
고정 너비 아이템이 있는 레이아웃
플렉스박스로 레이아웃을 만들다 보면 특정 아이템은 너비를 고정 크기로 지정해서 늘어나거나 줄어들지 않도록 하고 싶을 때가 있습니다.
다음처럼 고정된 너비의 사이드바(<aside>)와 컨텐츠 영역(<article>)으로 나누어진 플렉스박스가 있고, 컨텐츠 영역 안에는 다시 플렉스박스로 배치한 아이템들이 있는 레이아웃을 만들 때, 레이아웃 너비가 변해도 사이드바의 너비는 고정 크기를 가지도록 해보겠습니다.
<div class="layout">
<aside>
사이드바
</aside>
<article>
<h1>타이틀</h1>
<div class="flexbox">
<div class="item">content1</div>
<div class="item">content2</div>
<div class="item">content3</div>
<div class="item">content4</div>
</div>
</article>
</div>
레이아웃 너비가 "800px"로 제한되어 있고, 사이드바(<aside>)는 "160px" 로 고정합니다. "flex-grow", "flex-shrink" 속성 값을 모두 0으로 지정하면 플렉스박스의 아이템 너비가 변경되지 않고 "flex-basis" 속성으로 지정한 크기로 고정됩니다.
.layout{
max-width: 800px;
margin: 0 auto;
padding: 20px;
display: flex;
flex-wrap: nowrap;
gap: 1em;
}
aside{
flex-basis: 160px;
flex-grow: 0;
flex-shrink: 0;
background-color: #f0f0ff;
}
article{
flex-basis: 680px;
flex-grow: 1;
flex-shrink: 1;
background-color: #ffe0e0;
}
.flexbox{
display: flex;
flex-wrap: wrap;
gap: 1em;
padding: 10px;
background-color: #f0f0f0;
}
.item{
min-height: 150px;
flex-basis: 150px;
flex-shrink: 1;
flex-grow: 1;
}
플렉스박스 CSS가 적용된 레이아웃은 다음처럼 사이드바가 고정된 채로 레이아웃 너비가 변하면서 컨텐츠 영역 안의 플렉스박스 아이템들 배치가 변경됩니다.
컨텐츠 내용에 맞춰 자동 크기 조절하기
플렉스박스 아이템의 기본 크기를 결정하는 "flex-basis" 속성 값 중에는 "auto" 속성 값이 있습니다.
자동으로 아이템 너비를 맞춰준다는 뜻이고, 너비를 맞추는 기준은 아이템 안에 들어있는 컨텐츠 내용에 따라 결정됩니다. 다음처럼 아이템에 "auto" 속성 값으로 자동으로 너비가 결정되도록 변경합니다.
.flexbox .item{
flex-basis: auto;
background-color: #ffe;
}
아이템 안에 들어있는 컨텐츠가 동일하면 기존에 너비 값을 정한 것과 동일하게 다음처럼 아이템들이 배치됩니다.
두 번째 아이템에 긴 텍스트 내용을 입력하면 처음과는 전혀 다르게 컨텐츠 길이에 맞춰 아이템들이 재 배치됩니다.
미디어쿼리와 함께 반응형 지원하기
미디어쿼리와 함께 플렉스박스를 사용하면 반응형 레이아웃을 빠르고 간편하게 만들 수 있습니다.
특히, 아이템 사이의 여백 처리와 한 행당 표시하는 아이템 갯수를 빠르게 정의할 수 있습니다.
자동으로 아이템이 배치되도록 할 때, 특정 아이템 한개만 한 행에 위치하면서 너비가 플렉스박스 너비로 커지는 것은 같은 종류의 아이템(예를 들면 사진이나, 쇼핑몰 상품 목록 처럼)을 목록으로 나열하는 경우 문제가 됩니다.
데스크탑 화면에서는 한 행에 4개, 타블렛에서는 한 행에 2개, 모바일에서는 한 행에 1개의 아이템이 표시되도록 미디어쿼리와 플렉스박스 속성을 사용해 CSS를 만들어 보겠습니다.
먼저 반응형 레이아웃을 만들때 아이템 크기를 "%" 단위로 정의할 것이므로(한 행에 위치시킬 갯수를 이미 알고 있으므로) 아이템 사이의 여백(갭)도 다음처럼 "%" 단위로 변경해야 합니다.
.flexbox{
display: flex;
flex-wrap: wrap;
gap: 2%;
}
이제 미디어쿼리를 이용해 가로 너비를 기준으로 아이템 갯수가 고정되도록 플렉스박스 속성을 정의합니다.
.item{
min-height: 150px;
flex-basis: 23.5%;
flex-shrink: 1;
flex-grow: 1;
}
@media screen and (max-width:1023px){
.item{
flex-basis: 49%;
}
}
@media screen and (max-width:767px){
.item{
flex-basis: 100%;
}
}
간단하지만, 결과는 다음처럼 너비에 따라 표시되는 아이템 갯수가 정확하게 나누어집니다.
미디어쿼리와 조합해서 플렉스박스를 정의하면 중간의 불필요하게 아이템이 나누어지면서 행별로 아이템 갯수가 다른 경우가 발생하지 않도록 처리를 할 수 있습니다.
미디어쿼리 없이 모바일 대응하기
CSS 숫자 연산 결과를 얻는 calc() 메서드를 활용하면 모바일 기기에 대한 대응을 미디어쿼리 없이 할 수 있습니다.
미디어쿼리를 완전히 대체하는 것은 아니며, 레이아웃 구조가 단순해서 여러가지 너비에 대한 대응을 할 필요가 없거나, 조금이라도 더 빠르고 간결한 CSS 유지를 위한 트릭의 한가지 입니다.
모바일 기기 지원을 위한 레이아웃 너비를 "768px"로 하기로 하고, 그 보다 너비가 작은 레이아웃 화면(웹브라우저 너비) 에서는 플렉스박스 아이템 배치를 모바일 대응으로 하도록 하려면 다음과 같이 아이템의 너비를 정해줍니다.
.flexbox .item{
flex-basis: calc(calc(768px - 100%) * 1000000);
flex-grow: 1;
flex-shrink: 1;
}
중요한 것은 아이템 기본 너비를 결정하는 속성인 "flex-basis" 에 적용한 속성 값인 "calc(calc(768px - 100%) * 1000000)" 계산식입니다.
계산식의 "100%"는 플렉스박스의 현재 너비 값이 적용됩니다. 중요합니다.
"1000000" 는 아주 큰 숫자를 곱하기 위해 정한 임의의 숫자이며, 특별한 의미가 있는 것은 아닙니다.
참고로, 구글 크롬의 경우, 소수점 픽셀 계산 값이 그대로 화면 렌더링시 적용되며, 소수점 둘 째 자리까지는 일반적으로 표기됩니다. 이런 소수점 픽셀 계산 결과 값이 작은 계산 결과 값으로 인해 계산 착오가 생기는 것을 막기 위해 큰 숫자를 곱하는 것입니다.
경험적으로 "1000000" 이상이면 고해상도 모니터 너비에도 문제없이 대응이 되기 때문에 "1000000"으로 정한 것입니다.
계산식의 결과는 2가지로 나누어집니다.
- 플렉스박스 너비가 "768px" 보다 크거나 같으면 calc() 메서드 계산 결과는 0보다 작거나 같은 숫자가 되고, "flex-basis" 속성 값은 무시되며, 기본 값인 "auto" 가 됩니다. "flex-basis" 속성 값에서 음수 값은 무시됩니다.
- 플렉스박스 너비가 "768px" 보다 작으면 무한히 큰 숫자가 됩니다. 실제로 무한히 큰 것은 아니고 내부 calc() 메서드 결과 값에 1000000 을 곱했으므로 일반적으로 사용하는 웹 레이아웃 너비인 1000px ~ 1600px 너비보다는 훨씬 큰 값이 되기 때문에 레이아웃 용으로는 충분히 큰 크기가 됩니다.
따라서 플렉스박스의 "768px" 너비를 기준으로 다음처럼 레이아웃이 반응형으로 동작하게 됩니다.