1. Masonry 레이아웃이란?

Masonry layout is a layout method where one axis uses a typical strict grid layout, most often columns, and the other a masonry layout.
On the masonry axis, rather than sticking to a strict grid with gaps being left after shorter items, the items in the following row rise up to completely fill the gaps.
[MDN] Masonry layout

Masonry의 사전적 의미는 벽돌을 쌓아올린다는 뜻으로, 웹에서의 Masonry 레이아웃은 하나의 축(대부분 열)이 일반적인 그리드 레이아웃을 사용하고 다른 한 축이 Masonry 레이아웃을 사용하는 레이아웃 방법입니다. 다시 말해 Masonry 레이아웃을 사용하는 축의 방향으로는 요소를 빈틈 없이 차곡차곡 쌓는 형태의 UI를 가리킵니다. Pinterest 또는 네이버 이미지 검색 등 이미지 콘텐츠를 다루는 웹 페이지에서 Masonry 레이아웃이 구현된 페이지를 흔하게 확인할 수 있습니다.

벽돌은 지면과 가까운 곳에서부터 위 방향으로 쌓아올리지만, 웹에서는 보통 스크롤 동작이 이루어지는 방향인 위에서부터 아래로 요소를 나열하는 것을 가리켜 Masonry 레이아웃이라고 합니다. 이 문서에서는 열을 그리드 축으로, 행을 Masonry 축으로 하는 웹상의 일반적인 Masonry 레이아웃을 추가적인 JS 작업 없이 구현하는 방법에 대해 UI 개발자의 관점에서 설명하고자 합니다.

그렇다면 Masonry 레이아웃을 구현하는 방법엔 어떤 것들이 있을까요?
먼저 CSS 만으로 구현이 가능할지 알아봅시다.

2. CSS만으로 구현하기

(1) column 속성 사용하기

.list {
    column-count: 4; // 1
}

.item {
    display: inline-block; // 2
    width: 100%; // 3
    break-inside: avoid; // 4
}
  1. column-count로 단의 개수를 선언하거나 column-width로 단의 너비를 정하여 다단 구조로 노출합니다.
  2. display: inline-block을 추가하면 각 아이템이 여러 개의 단에 걸쳐 노출되는 것을 방지할 수 있습니다.
  3. 인라인 블록 요소가 되었으니 width 값도 지정해 줘야 합니다.
  4. break-inside: avoid 역시 각 아이템이 여러 개의 단에 걸쳐 노출되는 것을 방지하기 위해 추가한 속성입니다.
코드 예시
결론

언뜻 보면 Masonry 레이아웃을 구현한 것처럼 보이지만 이 방법에는 한계가 있습니다.

  • 다단 구조를 기본으로 하기 때문에 위와 같이 아이템이 추가될 때마다 각 아이템의 위치가 변경될 수 있습니다.
  • 아이템이 가장 왼쪽 상단에서 하단 순으로, 다음 우측 열의 상단에서 하단 순으로 역 N 방향으로 반복하여 나열됩니다. (↓↗↓↗…)

결론적으로 콘텐츠의 순서가 중요한 페이지를 만드는 경우에는 옳지 않은 방법입니다.

(2) flex 속성 사용하기

.list {
    display: flex; // 1
    flex-direction: column; // 2
    flex-wrap: wrap; // 3
    align-content: start; // 4
    height: 1000px; // 5
}

.item {
    width: 25%; // 6
}
  1. 컨테이너 요소에 display: flex를 적용합니다.
  2. flex 아이템이 추가될 때 열 방향(↓)으로 나열되도록 방향을 설정합니다.
  3. 아이템이 정해진 영역 안에서 줄(열) 바꿈 되어 나열되도록 합니다. 아이템이 열 방향(↓)으로 나열되다가 영역을 벗어나려고 하는 시점에 줄(열) 바꿈 되어 다음 열의 최상단부터 노출됩니다.
  4. 아이템의 개수가 적은 경우에도 4단에 걸쳐 나열될 수 있도록(한 방향으로 치우치지 않도록) 정렬 속성을 추가합니다.
  5. 높이 값을 지정하여 영역의 크기를 고정합니다.
  6. flex 아이템의 너비를 지정합니다. N 개의 단에 나누어 그려지길 원한다면 1 / N * 100% 값으로 계산하여 설정합니다.
  1. 위의 단계 5에서 고정 높이 값 대신 max-height 속성으로 최대 높이 값을 지정하면 리스트 다음 요소를 자연스럽게 배치할 수 있습니다.
코드 예시
결론

Masonry 레이아웃을 구현한 것처럼 보이는 이 방법 또한 다음과 같은 한계를 가집니다.

  • 방법 (1)과 동일하게 역 N 방향으로 반복하여 나열됩니다. (↓↗↓↗…)
  • 개발자가 임의로 높이 값을 height 또는 max-height 으로 고정하여 적용해야 한다는 부담도 있습니다.

(3) grid 속성 사용하기

grid 구조에서는 아주 잘게 쪼갠 셀들을 병합하는 방식으로 Masonry 레이아웃 흉내를 낼 수는 있습니다.
하지만 위 화면과 같이 개발자가 각 아이템의 위치와 크기를 하나하나 지정해 줘야만 하기 때문에 이 방법으로는 개별적으로 랜덤한 높이 값을 가지는 아이템들에 대응할 수는 없습니다.

이러한 문제를 해결하기 위해 grid-template-rows 속성의 값을 masonry로 지정하면 CSS만으로도 손쉽게 Masonry 레이아웃을 만들 수 있습니다.

하지만 이 값은 위와 같이 브라우저 지원 범위가 매우 제한적이기 때문에 아직 실무에서 사용하기에는 어렵습니다.

Firefox 브라우저의 고급 환경 설정 편집창(about:config)에 진입하여 layout.css.grid-template-masonry-value.enabled 설정값을 true로 변경한 후 다음의 코드 예시 페이지에 접근하면 Masonry 레이아웃이 적용된 모습을 확인할 수 있습니다.

코드 예시

이 문서에서 다루고 있는 형태는 아니지만 다음과 같이 grid-template-columns: masonry로 설정하면 열을 축으로 한 Masonry 레이아웃을 만들 수도 있습니다.

결론

아직은 이르지만 grid-template: masonry 값의 브라우저 지원범위가 확대된다면 CSS만으로도 충분히 Masonry 레이아웃을 구현할 수 있을 것으로 기대합니다.

지금까지 나열한 것처럼 CSS만 사용하여 Masonry 레이아웃을 구현하는 데에는 한계가 있습니다.
리액트 기반의 프로젝트에서는 다음과 같은 라이브러리를 사용하여 구현할 수 있습니다.

3. 리액트 라이브러리 사용하기

(1) react-masonry-css

react-masonry-css는 다음과 같은 방식으로 구현됩니다.

  1. breakpointCols 옵션에 적용한 N값을 받아 div로 된 N개의 열을 만듭니다.
  2. 각 열의 너비는 1 / N * 100% 값으로 적용됩니다.
  3. 아이템을 하나씩 각각의 열에 순서대로 삽입합니다.
    첫번째 아이템은 첫번째 열에, 두번째 아이템은 두번째 열에, …, N+1번째 아이템은 다시 첫번째 열에, … 넣어 레이아웃을 그립니다.
코드 예시 
결론

이 라이브러리를 사용해도 Masonry 레이아웃을 정확히 구현해낼 수는 없습니다.
순서를 잘 보면 열이 아닌 행 방향(→)으로 아이템이 나열되기는 하지만, 열의 개수만큼 하나의 행을 모두 채우면 다음 행으로 넘어가는 방식으로 구현되어 있습니다.
이전 요소들의 높이 값에 맞춰 차곡차곡 쌓이는 것이 아니죠.

그렇기 때문에 각 아이템의 높이 값 차이가 많이 나는 경우, 위 화면과 같이 어색한 UI를 만들게 됩니다.

(2) react-masonry-component

react-masonry-component는 다음과 같은 방식으로 구현됩니다.

  1. 각 아이템은 컬럼으로 묶이지 않고 각각의 div 요소로 배치됩니다.
  2. 각 아이템에는 position: absolute이 인라인 스타일로 적용됩니다.
  3. 아이템마다 적용된 높이 값을 기반으로 하여 계산된 topleft 값이 인라인 스타일로 추가됩니다.
  4. 영역의 너비가 리사이징 되는 경우 topleft 값을 다시 계산하여 변경합니다.
코드 예시
결론

이 라이브러리를 사용하면 우리가 원하는 완벽한 Masonry 레이아웃을 구현할 수 있습니다.
하지만 모든 아이템이 position: absolute 속성을 가지기 때문에 화면이 리사이징 될 때마다 좌표 값이 재계산 된다는 부담이 있습니다.

4. 정리

각 방법마다 장단점이 있기 때문에 다음의 표를 참고하여 프로젝트별 스펙에 맞게 취사선택하여 적용하면 됩니다.

CSS만으로 구현하기

columnflexgrid
• 역 N 방향(↓↗↓↗…) 배치
• 아이템이 추가될 때마다 전체 아이템의 위치가 변경될 수 있음
• 역 N 방향(↓↗↓↗…) 배치
• 개발자가 임의로 고정 높이값을 적용해야 함
• 개발자가 아이템의 위치를 일일이 지정해 줘야 함
• grid-template-rows: masonry를 사용하면 손쉽게 구현이 가능하지만 브라우저 지원 범위가 제한적임

리액트 라이브러리 사용하기

react-masonry-cssreact-masonry-component
• Z 방향(→↙→↙…) 배치
• 각 아이템의 높이값과 상관없이 한 행의 열을 다 채우고 다음 행을 채우는 방식이기 때문에 완전한 Masonry 레이아웃이라고 볼 수 없음
• Z 방향(→↙→↙…) 배치를 기본으로 하고 Masonry 레이아웃을 정확하게 구현
• 각 아이템이 position: absolute 속성을 가지고, 매번 top, left 값을 계산해서 적용한다는 단점이 있음

egjs 컴포넌트 사용하기

추가적으로 이 문서에는 다루고 있지 않지만 JS 라이브러리 중 네이버에서 만든 Masonry 레이아웃을 구현할 수 있는 egjs 컴포넌트가 있습니다.
@egjs/grid 컴포넌트를 활용하면 우리가 원하는 Masonry 레이아웃을 구현할 수 있고, @egjs/infinitegrid 컴포넌트를 활용하면 Masonry 레이아웃에 무한 스크롤 기능까지 추가할 수 있습니다.
네이티브 Javascript 와 리액트뿐만 아니라 다양한 UI 환경을 지원하고 있으니 필요한 경우 사용해 보시면 좋을 것 같습니다.

5. 참고 자료

긴 글 읽어주셔서 감사합니다.


2개의 댓글

poll · 2023년 4월 8일 9:50 오후

grear post

simba · 2024년 6월 20일 11:22 오전

react-masonry-component는 active하게 maintain되고 있는 것 같지 않네요. react-virtualized의 Masonry 추천합니다.

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다