CSS Grid Lanes로 메이슨리 레이아웃 만들기 — 구현보다 어려운 건 카드의 순서다

11분 읽기조회 2
공유

프로젝트 카드 열두 개를 세 열로 나열한다고 해보자. 첫 번째 프로젝트는 가로로 긴 화면 캡처를 쓰고, 두 번째는 모바일 화면을 세로로 보여준다. 세 번째 프로젝트에는 설명이 네 줄 붙는다.

이 상태에서 일반적인 CSS Grid를 적용하면 어딘가 허전해진다. 가장 긴 카드에 행 높이가 맞춰지고, 짧은 카드 아래에는 넓은 빈 공간이 남는다. 간격을 줄여도 그 공간은 없어지지 않는다. gap 때문에 생긴 여백이 아니라, 같은 행을 공유하면서 생긴 공간이기 때문이다.

흔히 선택하는 해결책은 콘텐츠를 자르는 것이다. 사진은 같은 비율로 맞추고, 제목은 한 줄, 설명은 두 줄에서 끊는다. 상품 목록처럼 일관된 형태가 필요한 화면에서는 합리적인 선택이다. 하지만 프로젝트 소개나 사진 갤러리에서는 조금 다른 문제가 된다. 레이아웃을 맞추기 위해 보여줘야 할 내용을 먼저 줄이게 된다.

CSS Grid Lanes는 카드의 높이를 통일하지 않고 이 목록을 배치하기 위한 기능이다. 메이슨리, 워터폴, 핀터레스트형 레이아웃이라는 이름으로 보던 형태를 CSS에서 다룬다.

다만 display: grid-lanes 한 줄로 JavaScript 라이브러리를 대체한다는 설명만으로는 부족하다. 카드가 놓이는 방식이 바뀌면 사람이 목록을 훑는 순서도 달라진다. 이미지가 늦게 도착하거나 필터로 항목이 사라졌을 때도 살펴봐야 한다. 코드보다 먼저 정할 것은 이 목록에서 무엇을 유지할 것인가다.

행을 없애는 것이 아니라, 한 방향의 배치 규칙을 바꾼다

일반 Grid를 이해할 때는 행과 열을 함께 생각한다. 세 열에 카드 세 장을 놓으면 각 카드가 하나의 행에 속하고, 다음 카드들은 그 아래 행으로 내려간다.

Grid Lanes에서는 한 방향에 그리드 트랙을 만들고, 다른 방향에는 항목을 쌓는 배치 방식을 사용한다. grid-template-columns로 열을 정의하면, 카드들이 그 열을 따라 세로로 쌓이는 형태가 된다. 반대로 행을 정의해 가로로 쌓이는 형태도 만들 수 있다. 이 글에서는 갤러리에 익숙한 세로형 배치만 다룬다. MDN의 Grid Lanes 설명

단순화한 예를 들어보자. 같은 너비의 열 세 개에 카드가 한 장씩 들어갔고, 각 열의 끝이 다음과 같다고 가정한다.

1 2 3 첫 번째 열: 260px 두 번째 열: 180px 세 번째 열: 340px

다음 카드를 공통된 새 행에 놓는 대신, 여유가 있는 열에 이어 놓는다. 이 예에서는 두 번째 열 쪽이 먼저 후보가 된다. 그래서 첫 번째 열과 세 번째 열의 긴 카드를 기다리지 않고 빈 공간을 채울 수 있다.

그렇다고 이미 놓인 카드를 모두 재조합해 전체 높이가 가장 낮아지는 답을 찾는 퍼즐 풀이로 이해하면 곤란하다. 배치에는 항목의 순서와 트랙, 여러 열을 차지하는 항목 같은 조건이 관여한다. CSS Grid Level 3의 현재 편집 초안도 트랙 크기 계산과 항목 배치 알고리즘을 별도로 정의한다. 아직 진행 중인 명세이므로 구현과 문법의 변화 가능성도 남아 있다. CSS Grid Level 3 편집 초안

이 차이를 알고 나면 사용처도 조금 선명해진다. 높이가 다른 콘텐츠를 자연스럽게 보여주는 목록에는 잘 맞는다. 반면 같은 행에 있는 가격·기능·상태를 가로로 비교해야 하는 화면에서는 행 정렬을 포기하는 것이 오히려 손해다.

겉모습이 비슷해도 columns와는 다르다

높이가 다른 카드를 쌓기 위해 다음과 같은 코드를 사용하기도 한다.

1 2 3 4 5 6 7 8 9 .gallery { columns: 3; column-gap: 1rem; } .gallery > li { break-inside: avoid; margin-bottom: 1rem; }

다단 레이아웃으로 콘텐츠를 나누고, 카드 중간에서 열이 갈라지지 않도록 하는 방식이다. 구현이 짧고 지원 범위도 넓다. 사진 몇 장을 배치하는 용도라면 충분히 검토할 수 있다.

하지만 다단 레이아웃은 본래 긴 콘텐츠를 여러 열로 흘려보내는 모델이다. 신문 기사가 첫 번째 열의 아래까지 이어진 뒤 다음 열로 넘어가는 모습을 떠올리면 된다. break-inside는 카드가 나뉘는 지점을 제어하지, 그 흐름을 가로 방향의 목록으로 바꿔주지는 않는다. MDN의 다단 레이아웃 안내

이 차이는 최신순 목록에서 드러난다. 사용자는 화면 위쪽을 왼쪽부터 훑으면서 최근 항목을 보고 있다고 생각할 수 있다. 그런데 실제 문서의 다음 항목은 첫 번째 열 아래쪽에 있을 수 있다. 카드를 계속 추가하는 목록이라면, 추가 전후에 열이 어떻게 채워지는지도 확인해야 한다.

따라서 columns를 Grid Lanes의 대체 레이아웃으로 사용하는 것이 틀렸다는 뜻은 아니다. 비슷하게 생겼다는 이유만으로 같은 탐색 방식을 제공한다고 가정하면 안 된다는 뜻이다.

단순 사진 모음인지, 최신 게시물 목록인지, 순위가 있는 검색 결과인지에 따라 허용할 수 있는 차이가 달라진다.

실제 적용은 일반 Grid에서 시작하는 편이 낫다

2026년 11월 기준 Grid Lanes는 모든 주요 브라우저에서 사용할 수 있는 Baseline 기능이 아니다. MDN 역시 제한적 지원과 실험적 기술이라는 점을 명시한다. 지원 브라우저에서 잘 나온다는 이유만으로 전체 사용자에게 같은 화면이 보일 것이라고 가정해서는 안 된다. 현재 지원 상태

여기서는 프로젝트 소개 목록을 만든다고 가정하겠다. 같은 열 너비를 사용하되 사진의 비율과 설명 길이는 유지한다. 미지원 환경에서는 일반 Grid로 표시하고, 카드 아래에 남는 공간은 허용한다.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <ul class="projects" aria-label="프로젝트 목록"> <li class="project-card"> <a class="project-link" href="/projects/dashboard"> <img src="/images/dashboard.webp" width="1200" height="800" alt="매출과 주문 현황을 보여주는 대시보드 화면" /> <div class="project-content"> <h2>운영 대시보드</h2> <p> 주문 처리 상태와 매출 흐름을 한 화면에서 확인하는 내부 운영 도구. </p> </div> </a> </li> <!-- 같은 구조의 카드를 필요한 만큼 추가한다. --> </ul>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 .projects { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1.25rem; margin: 0; padding: 0; list-style: none; } .project-card { min-width: 0; } .project-link { display: block; overflow: hidden; color: inherit; text-decoration: none; border: 1px solid #d8dee6; border-radius: 0.875rem; background: #fff; } .project-link:focus-visible { outline: 3px solid #1769d2; outline-offset: 4px; } .project-link img { display: block; width: 100%; height: auto; } .project-content { padding: 1rem; } .project-content h2 { margin: 0 0 0.5rem; overflow-wrap: anywhere; } .project-content p { margin: 0; line-height: 1.7; overflow-wrap: anywhere; } @supports (display: grid-lanes) { .projects { display: grid-lanes; } }

사진과 경로는 예시이며, width와 height는 각 파일의 실제 비율에 맞춰야 한다.

열 정의가 길어 보이지만 역할은 나뉜다. 16rem은 카드 한 장에 확보하려는 최소 너비이고, 1fr은 남은 너비를 나눠 갖게 한다. 안쪽의 min(100%, 16rem)은 부모가 아주 좁을 때 최소 너비 때문에 가로로 넘치는 상황을 피하기 위한 값이다.

여기서는 항목이 적을 때 빈 열을 접는 auto-fit을 사용했다. 카드가 한두 장뿐인 상태에서도 카드 너비를 일정하게 유지해야 한다면 이 선택은 다시 검토해야 한다. 목록이 꽉 차 있을 때만 보고 반응형 배치가 끝났다고 판단하기 쉬운 지점이다.

또한 카드 전체를 링크 하나로 구성했다. 이 예제에 즐겨찾기 버튼을 추가한다면 링크 안에 버튼을 그대로 중첩해서는 안 된다. 카드 레이아웃과 별개로 상호작용 구조를 나눠야 한다. 실제 카드 컴포넌트를 만들 때는 이런 작은 요구사항이 초기의 단순한 예제를 빠르게 바꾼다.

미지원 환경에서 남는 빈 공간은 버그가 아니다

위 코드에서 Grid Lanes가 적용되지 않으면 일반 Grid가 남는다. 두 환경의 화면이 완전히 같지는 않다.

이를 해결하려고 미지원 브라우저에만 별도 JavaScript 배치기를 추가할 수도 있다. 다만 그때는 새 CSS를 도입하면서 기존의 복잡성까지 함께 유지하게 된다. 초기 배치 시점, 이미지 로딩 후 재계산, 화면 크기 변경, 컴포넌트 해제 시 정리 같은 일을 다시 관리해야 한다.

모든 환경에서 메이슨리 배치가 반드시 같아야 한다면 그 비용을 감수할 수 있다. 반대로 목적이 “각 프로젝트의 사진과 설명을 자르지 않고 보여주기”라면 일반 Grid도 그 요구사항을 만족한다. 달라지는 것은 카드 사이의 밀도다.

이 경우에는 허용할 차이를 미리 적어두는 편이 좋다.

지원 브라우저에서는 카드를 촘촘하게 배치한다. 미지원 브라우저에서는 행 정렬을 사용하되, 콘텐츠와 문서 순서는 동일하게 제공한다.

이 정도 기준이 있어야 브라우저별 화면을 검수할 때 차이와 오류를 구분할 수 있다. @supports는 문법 지원 여부를 분기할 뿐, 두 화면이 제품 요구사항을 만족하는지까지 판단해주지는 않는다.

fit-tolerance를 만져야 하는 이유

카드가 가장 짧은 열만 계속 찾아 들어가면 공간은 잘 채울 수 있다. 그러나 열 높이 차이가 아주 작을 때까지 민감하게 반응할 필요는 없을 수 있다.

가령 두 열의 끝 높이가 몇 픽셀 차이 나지 않는다고 해보자. 그 차이를 없애기 위해 다음 카드를 다른 열로 보내는 것이 시각적으로 더 자연스러운 배치일까. 빈 공간은 줄겠지만, 연속된 카드가 이 열 저 열로 움직이는 정도는 커질 수 있다.

fit-tolerance는 이때 얼마나 작은 차이까지 구분할지 조정하는 속성이다. 지정한 범위 안의 높이 차이는 비슷한 조건으로 취급한다. 기본값은 1em이다. Safari 27.2에서는 기존 flow-tolerance가 이 이름으로 바뀌었고, 옛 이름은 당분간 사용 중단 예정 별칭으로 남는다. Safari 27.2 변경 사항

1 2 3 4 5 6 @supports (display: grid-lanes) { .projects { display: grid-lanes; fit-tolerance: 3rem; } }

위의 3rem에 특별한 정답의 의미는 없다. 프로젝트 제목과 설명의 길이, 사진 비율에 맞춰 조정할 출발점이다. Grid Lanes는 지원하지만 새 속성은 지원하지 않는 브라우저에서는 해당 선언이 무시되므로, 배치 기능과 세부 속성의 지원도 구분해서 확인해야 한다.

값을 크게 설정했다고 읽기 순서 문제가 모두 해결되는 것도 아니다. 카드 높이 차이가 크거나 여러 열을 가로지르는 항목이 있으면 결과가 달라진다. 반대로 밀도를 지나치게 양보하면 Grid Lanes를 선택한 이점이 약해질 수 있다.

여기서는 화면을 줄이고 늘려보는 것과 함께, 키보드로 링크를 하나씩 이동해봐야 한다. 숫자만 보고 정할 수 있는 디자인 토큰과는 성격이 다르다.

문서 순서를 지킨다는 말과 읽기 편하다는 말은 다르다

Grid Lanes를 적용해도 HTML의 순서는 유지된다. 키보드 탐색과 화면 낭독은 그 문서 순서를 따른다. 하지만 화면에 놓인 카드의 위치가 달라지면 사용자가 눈으로 예상한 다음 항목과 실제 포커스가 이동하는 항목은 다를 수 있다. WebKit의 안내도 문서 순서와 시각적 배치가 벌어질 수 있음을 설명한다. Grid Lanes의 순서 관련 안내

이 문제를 “접근성을 지원한다” 혹은 “지원하지 않는다”로만 나누기는 어렵다. 문서 순서를 임의로 바꾸지 않는 것은 중요한 조건이다. 그렇더라도 키보드를 쓰는 사람이 현재 위치를 놓칠 만큼 포커스가 멀리 이동한다면 사용성 문제는 남는다.

프로젝트 갤러리라면 어느 카드를 먼저 보더라도 큰 문제가 없을 수 있다. 그러나 최신순 목록은 다르다. 위쪽에 있는 카드가 더 최신이라고 읽는 것이 자연스럽다. 인기순 검색 결과라면 위치가 순위라는 정보까지 전달한다. 이런 화면에서는 같은 배치 차이도 더 중요해진다.

따라서 CSS를 붙이기 전에 목록의 순서에 어떤 의미가 있는지 정해야 한다. 내용이 긴 카드가 빈 공간을 잘 채운다는 이유로 더 위쪽에 보이는 것이 허용되는가. 시각적인 우선순위를 가져야 할 항목이 있는가. 있다면 메이슨리 자체가 맞는 선택인지부터 다시 물어볼 필요가 있다.

이미지가 늦게 오면 카드도 다시 움직인다

정적인 색상 상자로 만든 데모에서는 배치가 안정적이다. 하지만 실제 카드에는 이미지, 웹 폰트, 비동기로 받아오는 태그나 설명이 들어간다.

사진의 높이를 미리 알 수 없다면 처음에는 작은 카드로 배치됐다가, 사진이 도착한 뒤 카드가 커질 수 있다. 그 변화는 주변 항목의 위치에도 영향을 줄 수 있다. Grid Lanes가 이미지 파일의 비율까지 알아서 예측하는 것은 아니다.

앞의 HTML에서 이미지에 width와 height를 넣은 이유도 여기에 있다. CSS의 width: 100%; height: auto와 함께 사용하면 반응형 크기로 표시하면서 이미지 비율에 맞는 공간을 미리 확보할 수 있다. web.dev 역시 크기를 알 수 없는 이미지와 동적으로 삽입되는 콘텐츠를 주요 레이아웃 이동 원인으로 다룬다. 레이아웃 이동을 줄이는 방법

카드 하단의 태그를 별도 요청으로 가져오는 구조라면 이미지 크기만 지정해도 충분하지 않다. 태그가 추가될 때 카드 높이가 바뀐다. 그 정보를 처음부터 응답에 포함할지, 일정한 영역을 확보할지, 늦게 나타나도 무방한 정보인지 판단해야 한다.

또 사진 로딩으로 생기는 불필요한 이동과 사용자가 필터를 눌러 생기는 목록 재배치는 구분해야 한다. 전자는 가능한 한 줄여야 할 불안정성이고, 후자는 사용자가 요청한 변화다. 둘을 같은 애니메이션으로 덮는다고 같은 문제가 되지는 않는다.

‘더 보기’와 필터를 붙인 뒤가 실제 검증이다

도입을 검토한다면 완성된 화면을 한 번 보는 것으로 끝내지 않는 편이 좋다. 다음 상황을 같은 카드 데이터로 차례대로 확인하면 판단할 근거가 생긴다.

처음에는 네트워크 속도를 낮춰 이미지가 늦게 도착하게 한다. 크기를 예약한 이미지와 그렇지 않은 이미지에서 화면이 얼마나 움직이는지 본다. 이 단계에서 목록이 크게 흔들린다면 배치 문법보다 카드의 높이가 언제 결정되는지를 먼저 고쳐야 한다.

다음으로 제목과 설명을 길게 바꿔본다. 짧은 한국어 제목으로는 보이지 않던 문제가 긴 영문 이름이나 공백 없는 식별자에서 나타날 수 있다. 설명을 무조건 자르지 않기로 했다면 긴 콘텐츠도 레이아웃이 받아줘야 한다.

그다음 카드를 일부 제거하고, 다시 추가한다. 검색 필터가 바뀌는 순간과 ‘더 보기’로 다음 페이지가 붙는 순간을 확인하는 것이다. 새 데이터가 추가될 때 이미 보고 있던 항목의 위치를 사용자가 따라갈 수 있는지, 포커스를 두고 있던 요소가 제거되면 어디로 이동하는지 살펴본다. 포커스 관리 문제는 CSS가 아니라 애플리케이션 코드에서 해결할 부분이다.

마지막으로 마우스를 치우고 목록 전체를 Tab 키로 이동한다. 포커스 표시가 잘 보이는지만 확인하지 말고 다음 위치를 예상할 수 있는지 본다. 열이 세 개일 때 괜찮아도 두 개로 줄면 다른 결과가 나올 수 있으므로 중간 너비도 필요하다.

이 과정은 벤치마크가 아니다. “CSS니까 빠르다”거나 “라이브러리를 없앴으니 성능이 좋아졌다”는 결론을 내릴 근거도 아니다. 성능 개선을 주장하려면 같은 데이터와 환경에서 별도로 측정해야 한다. 여기서는 기능을 적용해도 사용자가 목록을 안정적으로 읽고 조작할 수 있는지 확인하는 것이다.

이 화면에 메이슨리가 필요한가

프로젝트 갤러리나 사진 모음에서는 카드마다 다른 길이를 유지하는 것이 콘텐츠를 이해하는 데 도움이 될 수 있다. 그 차이를 없애려고 사진을 자르고 설명을 숨겨왔다면 Grid Lanes를 적용할 이유가 있다.

그러나 업무 목록이나 검색 결과에서는 빈 공간보다 항목을 다시 찾기 쉬운 것이 중요할 때가 많다. 가격과 상태를 비교해야 하는 상품 목록도 마찬가지다. 정렬된 행이 제공하는 기준선을 버리면 보기에는 자유로워져도 비교는 어려워질 수 있다.

현재로서는 일반 Grid를 기본으로 유지하고, 콘텐츠 특성에 맞는 화면에 Grid Lanes를 점진적으로 적용하는 쪽을 권한다. 미지원 브라우저에서도 정보가 빠지지 않고, 지원 브라우저에서는 불필요한 공백을 줄이는 방식이다.

만약 적용을 위해 사진 비율을 전부 통일하고 설명을 모두 같은 줄 수로 잘라야 한다면 잠깐 멈춰볼 필요가 있다. 높이가 다른 콘텐츠를 살리려고 선택한 레이아웃인데, 결과적으로 높이 차이를 없애고 있다면 처음의 문제 정의부터 다시 보는 편이 낫다.

지원 범위와 문법은 2026년 11월 2일 기준이다. 예제는 동작 원리를 설명하기 위한 코드이며, 특정 서비스에서 수행한 성능 측정이나 사용성 테스트 결과는 포함하지 않았다.

댓글을 작성하려면로그인이 필요합니다.