GODRICH

이미지 갤러리 CSS 9종 — 메이슨리부터 라이트박스까지

이미지 갤러리 CSS는 사진 여러 장을 그리드나 워터폴 모양으로 배치하고 호버·클릭에 반응하게 만드는 레이아웃 기법입니다. 핀터레스트에 저장해 둔 그 갤러리처럼 예쁘지만, 막상 코드로 옮기려면 그리드 수식과 크롭 처리에서 막히기 쉽습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순서가 아니라 갤러리 하나를 실제로 만들 때 부딪히는 순서로 놓았습니다. 먼저 칸을 똑같이 나누는 가장 기본적인 그리드부터 시작해(01), 사진 비율이 제각각이라는 현실적인 문제를 풀고(02), 화면 폭에 맞춰 칸 수가 스스로 바뀌는 반응형을 더하고(03), 높이가 다른 사진을 빈틈없이 쌓는 워터폴을 두 가지 방식으로 비교하고(04~05), 인스타그램처럼 정사각형만 고집하거나(06) 대표 사진 하나를 키우는 편집 레이아웃을 얹고(07), 마지막으로 캡션과 클릭 확대라는 상호작용을 붙입니다(08~09).

번호 이름 무엇이 움직이나 JS
01 2단 균등 그리드 타일 translateY·scale 없음
02 3단 그리드 · 비율 통일 사진 scale, 프레임 translateY 없음
03 자동 반응형 그리드 타일 opacity·scale 진입 없음
04 멀티 컬럼 워터폴 타일 translateY 없음
05 그리드 워터폴 타일 scale 없음
06 정사각 그리드 고정 사진 scale, 프레임 translateY 없음
07 첫 장 강조 배치 타일 scale 없음
08 호버 캡션 오버레이 캡션 translateY·opacity 없음
09 클릭 시 라이트박스 패널 scale·opacity 없음

012단 균등 그리드

사진 네 장을 2×2 칸에 똑같은 크기로 나눠 놓고, 커서가 올라간 칸만 살짝 떠오르며 그림자가 짙어져 지금 보고 있는 사진이 어디인지 알려줍니다. 포트폴리오 섬네일이나 블로그 글 속 사진 나열처럼 가장 단순한 갤러리에 씁니다.

grid-template-columns:repeat(2,1fr)gaptranslateY hover
.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: $sp-2;
}
.tile {
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-4px) scale(1.03);
  box-shadow: $shadow-raised;
}

023단 그리드 · 비율 통일

세로 사진·가로 사진·정사각 사진이 섞여 있어도 각 이미지에 aspect-ratio: 4/3object-fit: cover를 걸면 모든 칸이 같은 크기로 맞춰지고, 호버하면 프레임이 살짝 떠오르며 안쪽 사진도 함께 확대됩니다. 휴대폰으로 찍어 비율이 제각각인 사진을 올리는 갤러리에 맞습니다.

aspect-ratio:4/3object-fit:coveroverflow:hidden
.cell {
  aspect-ratio: 4 / 3;
  overflow: hidden;
}
.photo {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform $duration $easing;
}
.cell:hover .photo, .cell:focus-within .photo {
  transform: scale(1.12);
}

03자동 반응형 그리드

grid-template-columns: repeat(auto-fit, minmax(56px, 1fr)) 한 줄만 걸어 두면 미디어쿼리 없이도 화면 폭에 맞춰 한 줄에 들어가는 칸 수가 저절로 바뀌고, 새 타일은 옅게 나타나며 자리를 잡습니다. 카테고리마다 사진 수가 다른 상품 갤러리에 씁니다.

repeat(auto-fit,minmax())그리드 자동 배치opacity+scale 진입
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
  gap: $sp-2;
}
.tile {
  opacity: .35;
  transform: scale(.88);
  transition: transform $duration $easing, opacity $duration $easing;
}
.tile.is-visible {
  opacity: 1;
  transform: scale(1);
}

04멀티 컬럼 워터폴

사진 다섯 장에 columns: 2만 걸면 자바스크립트 없이도 높이가 제각각인 사진이 컬럼을 따라 빈틈없이 쌓이는 워터폴 레이아웃이 되고, break-inside: avoid로 사진이 컬럼 경계에서 반으로 잘리는 걸 막습니다. 핀터레스트 스타일 무드보드에 어울립니다.

columns:2break-inside:avoidcolumn-gap
.gallery {
  columns: 2;
  column-gap: $sp-2;
}
.tile {
  break-inside: avoid;
  margin: 0 0 $sp-2;
  transition: transform $duration $easing;
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-3px);
}

05그리드 워터폴 (읽는 순서 유지)

grid-auto-rows를 4px 같은 아주 작은 단위로 쪼개고 사진마다 자기 높이에 맞는 grid-row-end: span N을 미리 계산해 주면, 컬럼 방식과 달리 사진이 위→아래가 아니라 왼쪽→오른쪽 읽는 순서 그대로 배치되는 워터폴이 됩니다. 발행 순서가 중요한 뉴스·포트폴리오 갤러리에 맞습니다.

grid-auto-rows:4pxgrid-row-end:span N읽는 순서 유지
.gallery {
  display: grid;
  grid-auto-flow: row dense;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 4px;
  column-gap: $sp-2;
}
.tile--1 { grid-row-end: span 10; }
.tile--2 { grid-row-end: span 13; }
.tile--3 { grid-row-end: span 9; }

06정사각 그리드 고정

모든 타일에 aspect-ratio: 1 / 1object-fit: cover를 걸어 원본 사진 비율과 상관없이 인스타그램 피드처럼 완벽한 정사각형만 나열되게 하고, 호버하면 타일이 살짝 떠오르면서 안쪽 사진도 함께 확대됩니다.

aspect-ratio:1/1object-fit:coverscale hover
.cell {
  aspect-ratio: 1 / 1;
  overflow: hidden;
}
.photo {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform $duration $easing;
}
.cell:hover .photo {
  transform: scale(1.1);
}

07첫 장 강조 배치

첫 번째 사진에만 grid-column: span 2grid-row: span 2를 줘서 나머지보다 네 배 크게 띄우고, 그 옆으로 작은 사진들이 채워지는 잡지 레이아웃을 순수 CSS 선택자 하나로 만듭니다. 대표 사진 하나를 강조하는 블로그 헤더 갤러리에 씁니다.

:nth-of-typegrid-column: 1 / 3grid-row: 1 / 3
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 56px);
  grid-template-rows: repeat(2, 56px);
  gap: $sp-2;
}
.tile:first-child {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

08호버 캡션 오버레이

평소엔 사진 아래쪽에 살짝 가려져 있던 반투명 띠가 호버하면 translateY로 완전히 올라오면서 제목과 위치 같은 캡션 글자가 함께 드러납니다. 여행 사진마다 장소 이름을 붙이는 갤러리에 맞습니다.

translateY hover반투명 그러데이션opacity
.caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  transform: translateY(64%);
  opacity: 0;
  transition: transform $duration $easing, opacity $duration $easing;
}
.tile:hover .caption, .tile:focus-within .caption {
  transform: translateY(0);
  opacity: 1;
}

09클릭 시 라이트박스 (JS 없이)

타일을 클릭하면 popovertarget 속성 하나로 원본 크기 사진이 담긴 팝오버가 뜨고, ::backdrop이 배경을 어둡게 덮는 동안 @starting-style이 사진을 작게·투명하게 시작해 커지며 나타나게 합니다. <button popovertarget="lb-01"><div id="lb-01" popover>만 짝지으면 열고 닫는 로직 전부를 브라우저가 대신합니다.

popovertarget::backdrop@starting-style
.lightbox {
  opacity: 0;
  transform: scale(.8);
  transition: opacity $duration $easing, transform $duration $easing,
    overlay $duration $easing allow-discrete, display $duration $easing allow-discrete;
  @starting-style { opacity: 0; transform: scale(.8); }
}
.lightbox:popover-open { opacity: 1; transform: scale(1); }
.lightbox::backdrop { background: rgba($stage-ink, .7); opacity: 0; }
.lightbox:popover-open::backdrop { opacity: 1; }

어디서 깨지는가 — 함정 하나

05를 처음 렌더링했을 때 두 번째 컬럼 위쪽에 예상치 못한 빈 칸이 생겼습니다. grid-auto-flow: row(dense 없이)로 두면 브라우저는 span이 큰 타일 뒤에 빈 칸을 그냥 남겨 두고 다음 줄로 넘어가 버리기 때문입니다. dense를 붙이자 그 구멍은 메워졌지만, dense는 DOM 순서보다 빈 칸 채우기를 우선하기 때문에 스팬 조합에 따라 타일이 실제 DOM 순서와 다른 위치에 그려질 수 있습니다. 04번(멀티 컬럼)이라면 애초에 순서가 위→아래·왼쪽 컬럼부터라 이런 문제가 없지만, 05번(그리드 행 스팬)은 dense를 켜는 순간 이 위험을 떠안게 된다는 뜻입니다. 발행 날짜처럼 순서 자체가 의미 있는 갤러리라면 dense를 빼고 빈 칸을 감수하거나, 압축 파일 비밀번호 24p2nyh2과 함께 들어 있는 vanilla 폴더의 span 값을 직접 조정해 빈 칸이 생기지 않게 미리 계산해야 합니다.

접근성

prefers-reduced-motion: reduce를 켠 화면에서는 아홉 개 모두 전환·루프만 꺼지고 결과 상태는 그대로 남도록 맞췄습니다. 01·02·06은 확대·이동이 순간적으로 적용되고, 03은 타일이 전환 없이 바로 제자리(불투명·원래 크기)로 나타나고, 04·05는 떠오름 없이 바로 제자리에 멈추고, 07은 강조 크기가 즉시 적용되고, 08은 캡션이 늘 펼쳐진 채로 고정되고, 09는 라이트박스가 전환 없이 바로 열리고 닫힙니다. 09의 클릭 트리거는 <button> 요소라 마우스뿐 아니라 키보드 Tab·Enter로도 열리고, popovertargetaction="hide" 닫기 버튼도 같은 방식으로 접근합니다. 나머지 여덟 개도 호버 전용 동작을 :focus-visible이나 :focus-within에도 걸어 두어 키보드 사용자가 마우스 없이도 같은 결과를 봅니다. CSS 그리드 메이슨리 표준화 논의는 MDN 문서에서, Popover API 지원 범위는 MDN Popover API에서 확인할 수 있습니다. 카드형 호버 효과가 더 궁금하면 이전에 올린 카드 호버 모음글래스모피즘 모음도 함께 보면 좋습니다.

FAQ

09 라이트박스는 Popover API를 지원하지 않는 브라우저에서 어떻게 됩니까?

popovertarget 속성을 브라우저가 모르면 그 속성은 그냥 무시되어 버튼이 평범한 버튼으로 남고 클릭해도 아무 일이 일어나지 않습니다. 레이아웃이 깨지거나 에러 없이 기능만 조용히 빠지는 안전한 방식이고, 최신 Chrome·Edge·Safari·Firefox는 이미 지원합니다.

04와 05 중 어떤 워터폴을 써야 합니까?

사진이 위에서 아래로 읽혀도 상관없다면(무드보드·갤러리) columns 방식(04)이 코드가 짧고 관리하기 쉽습니다. 최신순 정렬처럼 왼쪽 위부터 순서대로 읽혀야 한다면 그리드 방식(05)을 쓰고, 함정 문단에서 다룬 dense의 순서 어긋남 위험을 감안해야 합니다.

React 판에서 03 자동 반응형 그리드는 칸 수를 어떻게 계산합니까?

AutoFitGallery 컴포넌트는 칸 수를 계산하지 않습니다. grid-template-columns: repeat(auto-fit, minmax())가 컨테이너 폭만 보고 브라우저 안에서 스스로 계산하므로, 리사이즈 이벤트를 듣거나 상태를 두지 않아도 창 크기가 바뀌면 자동으로 다시 배치됩니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.