Grid로 Masonry Layout 만들기

다연·2025년 3월 20일

Masonry화면

Masonry Layout ?

위 이미지 처럼 콘텐츠 블록들을 벽돌처럼 빈 공간 없이 배치하는 레이아웃 방식으로, 대표적으로 핀터레스트를 생각하면 된다.

이 레이아웃을 만드려면 아래와 같은 방법이 있다.

이왕 하는거 제대로 해보자는 생각이 들어 CSS Grid + 자바스크립트를 이용해 구현해보기로 했다.


🧐 어떻게 만들까 ?

이미지 가져오기

이미지는 따로 저장하지 않고 랜덤으로 가져오는 방식을 택했다. picsum의 무료 이미지를 이용했는데, 아래와 같은 형식으로 사용하면 된다.

<img src="https://picsum.photos/${가로사이즈}/${세로사이즈}?random=${고유번호}" alt="" />

이미지 고정하기

렌더링마다 이미지가 바뀌는 문제를 방지하기 위해, 초기 렌더링에서만 이미지 리스트가 고정되도록 useState를 사용했다.

const generateImageList = () => {
  return Array.from({ length: 50 }, (_, index) => ({
    num: index + 1, // 고유번호
    height: Math.floor(Math.random() * 7 + 2), // 2~8사이의 숫자(높이값 백의자리)
  }));
};

const Gallery = () => {
  const [imgList] = useState(generateImageList()); //이미지 고정
  
  return (
    <ul className="gallery">
      {imgList.map((item, index) => (
        <li key={`gallery-img-list-${index}`} className="gallery__item">
          <div>
            <img src={`https://picsum.photos/500/${item.height}00?random=${item.num}`} 
              width="500" height={`${item.height}00`} alt="" />
          </div>
        </li>
      ))}
    </ul>
  );
};

CSS Grid로 레이아웃 설정

.gallery {
  display: grid;
  grid-gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(250px,1fr));
  
  &__item {
    img {
      display: block;
      width: 100%;
      transition: var(--transition);
      content-visibility: auto;
    }
  }
}

그리드 아이템 높이조정 전
css만을 사용하면 위와 같은 레이아웃이 나온다. 이제 자바스크립트를 통해 각 아이템의 높이를 조정해보자.

그리드 아이템 높이 조정하기

grid-gap, grid-auto-rows 범위 이미지이미지의 높이에 따라 Grid 아이템이 여러 개의 Grid row를 차지하도록 설정했다.

1. grid-auto-rows 속성 추가

  • row 한개의 높이 값을 정해준다.
    예) grid-auto-rows: 30px;

Grid 참고 블로그 grid line에 대한 이해와 grid-row 속성 이용하는 방법을 알아보기 !

2. Grid item의 높이 구하기

offsetHeight를 통해 각 아이템의 실제 높이를 구한다.

3. grid-row-end: span ${rowSpan} 값 설정

이미지의 실제 높이(offsetHeight),행의 높이와 행 간격을 고려해서 span의 값을 설정한다.

  • span의 값 = Grid 내부 item 높이 / Grid row 높이
Grid 내부 각 item 높이 = 이미지태그.offsetHeight + 오차범위고려(rowGap)
Grid의 각 행 = 높이(rowHeight) + 행 간격(rowGap)
  • Math.floor()로 소수점 버림

4. 성능 최적화

  • Lazy loading: img태그 내 사용, content-visibility: auto;
    : 화면에 보이는 영역만 렌더링되게하는 CSS속성이다
  • contain-intrinsic-size: ${가로 세로} 설정
    : 렌더링 되지 않은 부분도 영역만 고정시켜 전체 레이아웃이 틀어지지 않도록 한다.
  • requestAnimationFrame()
    : 이미지가 로드 된 후 높이값을 계산하기 위해 추가
useEffect(() => {
  if (!gridRef.current) return;  // gridRef가 없으면 함수 종료

  const resizeGridItems = () => {
    const grid = gridRef.current; // 그리드 요소 가져오기
    if (!grid) return;  // grid가 없으면 함수 종료

    const items = grid.querySelectorAll(".gallery__item"); // 모든 그리드 아이템 선택

    // CSS로 고정된 그리드의 각 행의 높이와 행 간격을 가져옴
    const rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue("grid-auto-rows")) || 0;
    const rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue("grid-row-gap"));

    items.forEach((item) => {
      // 각 아이템의 이미지 요소 찾기
      const content = item.querySelector("img") as HTMLImageElement;
      if (!content) return;

      // 이미지 높이에 맞춰 몇 행(span)을 차지할지 계산
      const rowSpan = Math.floor((content.offsetHeight + rowGap) / (rowHeight + rowGap));

      // grid-row-end 속성으로 몇 행을 차지할지 설정
      (item as HTMLElement).style.gridRowEnd = `span ${rowSpan}`;
    });
  };

  requestAnimationFrame(resizeGridItems);  // 브라우저가 렌더링을 완료한 후 함수 실행
}, [imgList]); // imgList가 변경될 때마다 실행

완성!

item 순서

아이템 순서 이미지
이미지 순서가 어떻게 배치되는지 확인하기 위해 오름순으로 숫자를 부여했다. 위와 같이 시작점이 높은것부터 순차적으로 배치된다.

전체코드 / 스타일

마치며

예상했던 것처럼 스크립트 부분에서 시간을 많이 쏟았다. 특히 이미지 높이를 동적으로 측정하고, requestAnimationFrame을 활용해 불필요한 리렌더링을 최소화하려는 과정이 어려웠다. 그래도 성능 최적화를 위해 코드를 여러 번 수정하고 테스트하면서 최적화 기법에 대한 이해가 조금 생긴 것 같다.

이번 작업을 통해 CSS Grid를 다시 공부하게 되었는데, 실무에서는 flex만 주로 사용하다보니 매번 공부하는 느낌이지만 도움이 많이 됐다.😊 라이브러리에 의존하지 않고 직접 구현해보니 성취감이 큰 작업이었다.


출처
Masonry style layout with CSS Grid

0개의 댓글