notes
CSS,Style - *(Masonry,Waterfall )Layout
CSS Masonry·Waterfall Layout의 배치 방식과 구현 선택지 메모.
- 카드 높이가 제각각인데 빈틈 없이 2열로 쌓는 배치를 Masonry 라고 부른다.
- JS 라이브러리 없이 CSS columns 만으로 만들었고, 인피니티 스크롤도 DOM 추가만으로 된다.
- 대신 순서가 열 우선이라, 좌우 번갈아 순서가 필요하면 JS 로 배열을 나눠야 한다.
이런 배치를 뭐라고 부르나
카드 높이가 제각각인데 2열로 쌓이고, 좌우 열이 가로로 맞지 않는 배치는 보통 이렇게 부른다.
- Masonry layout (메이슨리 레이아웃): 벽돌 쌓기처럼 빈틈 없이 위에서 아래로 채운다는 뜻이다. Pinterest 에서 많이 써서 Pinterest layout 이라고도 한다.
- Waterfall layout (폭포수 레이아웃): 아시아권 UI, 기획에서 자주 쓰는 말이다.
- Staggered grid: 높이가 어긋난 그리드. Android 쪽에서 많이 쓰는 이름이다.
- Uneven grid, asymmetric grid: 덜 기술적인 표현이다.
구현 방법은 CSS columns, JS 라이브러리(Masonry.js, react-masonry-css), 그리고 아직 준비 중인 네이티브 CSS 정도가 있다.
1. 전체 구조
파일 네 개가 역할을 나눈다.
2. 핵심은 CSS Multi-column Layout
Masonry.js 대신 브라우저에 들어 있는 CSS 다단(column) 레이아웃을 썼다.
const masonryClassName = cn("columns-2 gap-atomic-unit-spacing-8");
const itemClassName = cn("mb-atomic-unit-spacing-8 w-full break-inside-avoid");
기존에 쓰던 grid grid-cols-2 는 행 기준으로 정렬된다. 행 높이가 그 행에서 제일 긴 카드에 맞춰지니까 짧은 카드 아래에 빈틈이 생긴다.
columns-2 는 열 기준으로 채운다. 카드가 위에서 아래로 바로 이어 붙어서 빈틈이 없다.
3. 카드 배치 순서
여기가 제일 중요하다. CSS columns 는 DOM 순서대로 1열을 먼저 위에서 아래로 채우고 나서 2열로 넘어간다.
Figma 시안처럼 1·2, 3·4 순서(좌우 번갈아)가 필요하면 CSS columns 로는 안 된다. JS 로 배열을 두 열로 나눠서 만들어야 한다. 지금은 Pinterest 처럼 열 우선으로 채우는 방식이다.
4. break-inside-avoid 역할
이 속성이 없으면 긴 카드가 열 경계에서 잘려서, 위쪽은 1열 끝에 아래쪽은 2열 처음에 보일 수 있다.
5. 더미 사진 생성 로직
API 에 사진 필드가 없어서 reviewSeq 로 항상 같은 더미를 만든다. 높이 5가지 중 하나를 골라서 카드마다 높이가 달라지게 했다.
const REVIEW_DUMMY_IMAGE_VARIANTS = [
{ width: 400, height: 520 },
{ width: 400, height: 400 },
{ width: 400, height: 560 },
{ width: 400, height: 480 },
{ width: 400, height: 440 },
] as const;
export function getCombineReviewDummyImage(reviewSeq: number) {
const variant =
REVIEW_DUMMY_IMAGE_VARIANTS[
Math.abs(reviewSeq) % REVIEW_DUMMY_IMAGE_VARIANTS.length
];
return {
url: `https://picsum.photos/seed/combine-review-${reviewSeq}/${variant.width}/${variant.height}`,
aspectRatio: `${variant.width} / ${variant.height}`,
photoCount: (Math.abs(reviewSeq) % 5) + 1,
};
}카드에서는 aspectRatio 를 inline style 로 넣는다.
<div className={imageWrapClassName} style={{ aspectRatio: dummyImage.aspectRatio }}>
고정 aspect-[3/4] 대신 카드마다 비율을 다르게 주니까, columns 레이아웃에서 높이 차이가 커져 워터폴처럼 보인다.
6. 카드 내부 구조
7. 인피니티 스크롤과의 관계
스크롤 컨테이너(#scroll-root) 안의 sentinel 이 보이면 훅이 fetchNextPage() 를 부르고, 리스트가 늘어나면 Masonry 가 새 카드를 그린다.
- Masonry 쪽에 따로 JS 레이아웃 코드가 없다.
- 새 카드가 DOM 에 붙으면 브라우저가 columns 규칙으로 알아서 배치한다.
- Masonry.js 처럼
reloadItems(),layout()을 부를 필요가 없다.
하나 주의할 건, columns 는 전체 높이를 다시 나눠서 배치하기 때문에 카드가 추가될 때 기존 카드가 다른 열로 옮겨 갈 수 있다는 점이다. 무한 스크롤에서 이게 거슬리면 JS 로 두 열을 나눠 담는 방식이 더 안정적이다.
8. Masonry.js vs CSS columns
9. 네이티브 CSS Masonry 현황
처음에는 grid-template-rows: masonry 로 제안됐는데, 이름과 문법을 두고 논의가 길어지다가 display: grid-lanes 로 정리됐다.
2026년 10월 기준으로 Safari 26.4 부터 기본 지원되고, Chrome, Edge, Firefox 는 아직 플래그를 켜야 된다. 지금 쓰려면 @supports 로 감싸고 columns 를 fallback 으로 두는 게 맞다.
.masonry {
columns: 2;
column-gap: 8px;
}
.masonry > * {
break-inside: avoid;
margin-bottom: 8px;
}
@supports (display: grid-lanes) {
.masonry {
display: grid-lanes;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.masonry > * {
margin-bottom: 0;
}
}grid-lanes 는 카드를 가장 짧은 열에 넣어서 순서가 좌우로 흐른다. columns 의 열 우선 문제도 같이 해결된다.
10. 나중에 API 사진을 붙일 때
combine.utils.ts 만 고치면 된다.
// API 필드가 추가된 뒤
export function getCombineReviewImage(item: ISearchReviewReviewListItems) {
if (item.reviewPicUrl) {
return {
url: item.reviewPicUrl,
aspectRatio: "3 / 4",
photoCount: item.photoCount ?? 1,
};
}
return getCombineReviewDummyImage(item.reviewSeq);
}combine.review.masonry.tsx 와 columns 레이아웃은 그대로 두면 된다. 단 실제 사진은 비율을 미리 모르니까, API 가 가로세로 크기를 같이 주면 로딩 중 레이아웃이 흔들리지 않는다.