실제 사진 갤러리 만들기

HTMLBeginner
지금 연습하기

소개

Moonroom은 실제 Apollo 사진을 보여 주는 작은 전시입니다. 사진 모음은 준비되어 있지만 현재 전시에는 사진 카드가 없습니다. 의미가 잘 전달되는 카드 한 장을 만들고, 데이터에서 전체 모음으로 확장한 다음, 좁고 넓은 화면에서 모두 읽기 좋은 갤러리를 만듭니다. 별도의 Study collection에서 시작하기 전에 목표로 하는 관람 경험을 확인할 수 있습니다.

기본 HTML 구조, CSS 선택자와 레이아웃, JavaScript 객체와 배열 및 DOM 이벤트를 알아야 합니다. 이 실습에서는 figure, figcaption, 기본 제공 버튼, 안정적인 사진 ID, forEach, dataset, 여기서 사용하는 반응형 규칙을 소개합니다. 첫 실습의 상세 뷰어는 준비되어 있으며, 다음 실습에서 직접 동작을 만듭니다. 50분은 읽기와 일반적인 편집 실수 해결 시간을 포함한 작성자의 예상 시간입니다.

웹 페이지를 열거나 새로 고친 뒤에는 로딩이 끝난 후 사진 컨트롤을 작동하세요. /home/labex/project/moonroom을 편집할 때는 WebIDE, 웹 사이트를 사용할 때는 Web 8080을 이용합니다. 이 실습에서는 index.html, gallery.js, styles.css만 편집합니다. photos.js, viewer.js, images/, credits.html은 보존하세요. 제공된 이미지 파일과 메타데이터는 원래 ID와 계속 연결되어야 합니다. 웹 사이트의 모든 레이블은 영어로 유지됩니다. 이 실용적인 키보드 확인은 완전한 접근성 인증을 뜻하지 않습니다.

사진과 그 의미 연결하기

이 단계에서는 작동하는 전시를 키보드로 사용한 뒤 내 전시의 첫 카드를 만듭니다.

Web 8080으로 전환하고 페이지 링크 Study collection을 선택하세요. 따로 준비된 예시이며 이를 바꾸는 것은 내 작업 범위가 아닙니다. 첫 Earthrise 사진 버튼에 테두리 표시가 보일 때까지 Tab을 누른 뒤 Enter를 누르세요. 상세 보기에는 달의 지평선 위에 지구가 보입니다. Esc를 누르면 같은 카드로 초점이 돌아갑니다. 다른 Earthrise 카드는 사진과 설명이 다르다는 점에 주목하세요. 제목은 레이블일 뿐 고유한 식별자가 아닙니다.

페이지 링크 Current exhibition을 선택하여 내 페이지로 돌아갑니다. 이 링크는 웹 사이트 안에서 이동하는 것이며 LabEx 탭을 바꾸지 않습니다. 처음에는 내 페이지에 제목과 출처만 있고 카드가 없습니다. 이것이 바꿔야 할 미완성 상태입니다.

WebIDE로 전환하세요. Explorer에서 /home/labex/project 아래의 moonroom 폴더를 엽니다. Explorer가 숨겨져 있으면 View → Explorer를 선택하세요. photos.js를 엽니다. photos 배열에는 사진마다 객체 하나가 있으며, 각 객체는 다음 관련 값을 함께 보관합니다.

필드 의미
id earthrise-eight 같은 안정적인 식별자. 두 제목이 같아도 됩니다.
thumbnail 카드에서 사용하는 실제 로컬 소형 이미지 버전.
large 뷰어에서 사용하는 대응하는 큰 이미지 버전.
alt 이미지를 볼 수 없는 사람에게 내용을 설명하는 의미 있는 텍스트.
title 및 caption 화면에 보이는 이름과 맥락.

이 파일은 편집하지 마세요. 첫 레코드의 썸네일은 images/earthrise-eight-thumb.jpg, 큰 이미지는 images/earthrise-eight.jpg입니다. 상대 이미지 경로는 편집기의 현재 폴더가 아닌 HTML 페이지를 기준으로 해석됩니다. 대소문자와 .jpg 확장자가 중요합니다.

figure는 이미지와 해당 figcaption을 묶습니다. 버튼을 쓰면 사진 열기가 기본 제공 키보드 동작이 됩니다. Tab으로 초점을 받을 수 있고 Enter로 작동합니다. 마우스 클릭 핸들러만 있는 div는 이런 동작을 자동으로 제공하지 않습니다. type="button"은 의도한 버튼 동작을 명시합니다. 보이는 제목과 설명은 마우스를 올리지 않아도 읽을 수 있습니다. aria-label은 버튼에 Open Earthrise라는 짧은 접근 가능한 이름을 주고, alt는 이미지의 내용을 설명합니다. 두 사진이 같은 이름을 쓰므로 aria-describedby로도 설명의 고유한 id를 통해 버튼과 설명을 연결합니다. 설명은 Apollo 8 또는 Apollo 11처럼 구별할 맥락을 제공합니다. 접근 가능한 이름은 컨트롤을 식별하고, 연결된 설명은 맥락을 더합니다. 따라서 사진을 보지 않고 탐색하는 사람도 같은 이름의 두 컨트롤을 구별할 수 있습니다. 자리 표시자나 배경 이미지로는 이런 이미지 설명을 제공할 수 없습니다.

data-photo-card 속성은 전체 카드 컨테이너를 식별하고, data-photo-id는 열기 버튼을 식별합니다. 둘 다 레코드의 안정적인 ID를 사용합니다. 이 작은 연결 속성을 유지해야 데이터, 카드, 뷰어가 같은 사진을 가리킬 수 있습니다. 화면에 표시하는 텍스트가 아닙니다. 카드에 추가 래퍼가 있어도 되지만, 버튼과 설명은 일치하는 data-photo-card가 있는 가장 가까운 컨테이너에 속해야 합니다.

index.html을 열고 Ctrl+A 또는 Cmd+A로 전체 텍스트를 선택한 뒤 아래의 완전한 HTML로 바꿉니다. script 줄을 유지하세요. defer는 HTML 분석 후 나열된 순서대로 스크립트를 실행합니다. photos.js는 데이터를 제공하고, gallery.js는 다음 단계에서 모음을 만들며, 준비된 viewer.js는 이 실습의 상세 보기를 담당합니다. dialog 마크업은 방금 사용한 패널이며, 그 동작은 다음 실습에서 설명하고 구현합니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Moonroom — A little farther from home</title>
  <link rel="stylesheet" href="styles.css">
  <script src="photos.js" defer></script>
  <script src="gallery.js" defer></script>
  <script src="viewer.js" defer></script>
</head>
<body>
  <main>
    <header>
      <nav aria-label="Exhibitions"><a href="/">Current exhibition</a> · <a href="/example/">Study collection</a></nav>
      <p class="eyebrow">Moonroom / Photographic studies</p>
      <h1>A little farther from home.</h1>
      <p class="intro">Three moments from the Apollo archive. Take a closer look.</p>
    </header>
    <section id="gallery" class="photo-grid" aria-label="Photographs">
      <figure data-photo-card="earthrise-eight">
        <button type="button" class="photo-button" data-photo-id="earthrise-eight" aria-label="Open Earthrise" aria-describedby="caption-earthrise-eight">
          <img src="images/earthrise-eight-thumb.jpg" alt="Blue and white Earth above the grey lunar horizon">
          <span class="photo-name">Earthrise</span>
        </button>
        <figcaption id="caption-earthrise-eight">Apollo 8 · Earth above the Moon, December 1968.</figcaption>
      </figure>
    </section>
    <footer>Photographs: NASA. An independent educational exhibition. <a href="credits.html">Image credits</a></footer>
  </main>
  <dialog id="viewer" aria-labelledby="photo-title" aria-describedby="photo-caption">
    <button id="close" type="button" autofocus>Close</button>
    <h2 id="photo-title"></h2>
    <img id="large-photo" alt="">
    <p id="photo-error" role="status"></p>
    <p id="photo-caption"></p>
    <nav aria-label="Photo navigation">
      <button id="previous" type="button">Previous</button>
      <p id="photo-position"></p>
      <button id="next" type="button">Next</button>
    </nav>
  </dialog>
</body>
</html>

Ctrl+S 또는 Cmd+S로 저장합니다. Web 8080으로 전환하고 필요하면 Current exhibition을 선택한 뒤 그 웹 페이지를 새로 고칩니다. Earthrise 카드 한 장과 실제 이미지, Apollo 8 설명이 보여야 합니다. 레이아웃은 아직 단순하며 뒤에서 스타일을 추가합니다. Tab으로 카드에 도달하고 Enter로 열고 Esc로 돌아오세요. 큰 이미지는 같은 장면을 더 자세히 보여야 합니다.

이미지가 깨지거나 페이지가 비어 있으면 WebIDE로 돌아갑니다. 파일 이름이 index.html이고 moonroom 안에 저장되어 있으며 src, 따옴표, 닫는 태그가 예시와 맞는지 확인하세요. 저장 후 Current exhibition을 다시 열고 새로 고칩니다. 환경을 초기화하거나 다른 이미지로 잘못된 경로를 숨기지 마세요. 필요하면 전체 HTML을 다시 복사하여 파일 전체를 복구할 수 있습니다.

실제 카드가 보인 뒤 이 단계의 검사를 실행합니다. 검사는 별도 예시가 아닌 내 현재 전시를 확인합니다.

식별자를 유지하며 모든 레코드 렌더링하기

이 단계에서는 직접 작성한 카드 한 장을 전체 배열에서 생성한 카드들로 바꿉니다.

WebIDE에 머물면서 비교를 위해 photos.js를 다시 여세요. 첫 번째와 세 번째 레코드의 제목은 모두 Earthrise지만 ID, 이미지 경로, 설명이 다릅니다. 제목으로 사진을 찾으면 이 차이를 잃습니다. 완성된 갤러리에는 제공된 세 ID 각각에 대해 정확히 하나의 보이는 열기 버튼이 있어야 하고, 썸네일, 제목, 대체 텍스트, 설명이 일치해야 합니다. 제공된 순서와 데이터를 유지하세요.

이제 gallery.js를 엽니다. document.querySelector('#gallery')는 id가 gallery인 요소를 선택합니다. replaceChildren()은 그 요소를 비우고 임시로 손수 작성한 카드를 제거하여 두 번 표시하지 않도록 합니다. photos.forEach(function (photo) { ... })는 배열 레코드마다 순서대로 내부 함수를 한 번 호출합니다. photo는 현재 객체입니다. 반복문과 같은 반복 작업의 개념이며, 각 레코드를 함수에 전달합니다.

document.createElement()는 메모리에 실제 HTML 요소를 만듭니다. textContent에 대입하면 텍스트를 HTML로 해석하지 않고 문자로 넣습니다. append()는 준비한 요소를 나열한 순서대로 삽입합니다. 브라우저는 첫 번째 figure, 버튼, 이미지, 설명과 같은 관계를 봅니다.

dataset은 data- 속성을 읽고 씁니다. JavaScript는 카멜 표기법을 사용합니다. figure.dataset.photoCard는 data-photo-card에 쓰고, button.dataset.photoId는 data-photo-id에 씁니다. 둘 다 photo.id를 받고, 표시할 제목은 photo.title을 받습니다. 각 설명에도 caption-과 photo.id를 연결한 고유 id를 설정하고, 버튼의 aria-describedby가 이 설명 ID를 참조합니다. 보이는 설명은 버튼 밖에 그대로 두되 버튼과 연결합니다. setAttribute()는 aria-label 같은 속성을 설정하며, + 연산자는 텍스트 Open과 제목을 이어 붙입니다. 문자열 템플릿 문법은 필요 없습니다.

gallery.js의 전체 내용을 다음 완전한 코드로 바꾸고 저장하세요.

const gallery = document.querySelector('#gallery');
gallery.replaceChildren();
photos.forEach(function (photo) {
  const figure = document.createElement('figure');
  figure.dataset.photoCard = photo.id;
  const button = document.createElement('button');
  button.type = 'button';
  button.className = 'photo-button';
  button.dataset.photoId = photo.id;
  button.setAttribute('aria-label', 'Open ' + photo.title);
  const image = document.createElement('img');
  image.src = photo.thumbnail;
  image.alt = photo.alt;
  const title = document.createElement('span');
  title.className = 'photo-name';
  title.textContent = photo.title;
  const caption = document.createElement('figcaption');
  caption.id = 'caption-' + photo.id;
  button.setAttribute('aria-describedby', caption.id);
  caption.textContent = photo.caption;
  button.append(image, title);
  figure.append(button, caption);
  gallery.append(figure);
});

Web 8080으로 전환하고 Current exhibition을 새로 고칩니다. Earthrise, A trace of a visitor, 두 번째 Earthrise가 이 순서로 보여야 합니다. 각 설명은 버튼 아래에서 계속 보입니다. 발자국은 세로 사진이고 Earthrise 이미지들은 정사각형입니다. 곧 갤러리의 썸네일을 일정한 틀에 맞추되 상세 보기에서는 서로 다른 비율을 보존합니다.

Tab과 Enter로 각 카드를 따로 여세요. 첫 Earthrise에는 Apollo 8 설명, 마지막에는 Apollo 11 설명이 있습니다. 매번 Esc를 누르고 사용한 카드로 초점이 돌아오는지 확인하세요. 마지막 카드를 사용해도 항상 첫 카드가 아닌 마지막 카드로 돌아와야 합니다.

카드가 하나뿐이면 스크립트가 저장되지 않았거나 문법 오류가 있거나 다른 폴더에 있을 수 있습니다. WebIDE로 돌아가 gallery.js를 전체 코드와 비교하고 forEach 끝의 닫는 });를 확인하세요. 레코드가 비슷해 보이도록 photos.js를 바꾸지 마세요. 저장하고 웹 사이트를 새로 고친 뒤 카드를 다시 비교하고 검사를 실행합니다.

화면에 맞춰 배치하고 키보드 초점 표시하기

이 단계에서는 전체 전시를 좁은 화면에서도 읽기 쉽게 만들고 초점이 있는 카드를 쉽게 찾도록 합니다.

WebIDE로 돌아가 styles.css를 엽니다. 반응형 레이아웃은 고정 페이지 너비를 강제하지 않고 사용 가능한 공간에 맞춥니다. 이 스타일시트는 CSS 기초에서 배운 flexbox를 사용합니다. display: flex는 카드를 한 줄에 놓고, flex-wrap: wrap은 새 줄로 넘길 수 있게 하며, gap은 카드 사이에 공간을 둡니다. flex: 1 1 260px는 figure가 커지거나 작아질 수 있게 하고 선호 너비를 260픽셀로 정합니다. min-width: 0은 긴 내용이 페이지를 넓히는 대신 카드 안에서 줄어들 수 있게 합니다.

카드 이미지는 width: 100%와 aspect-ratio: 4 / 3으로 일관된 프레임을 사용합니다. object-fit: cover는 표시된 가장자리를 잘라 썸네일 프레임을 채우며 파일 자체를 바꾸지 않습니다. 반면 상세 이미지는 object-fit: contain, max-width, max-height를 사용하여 전체 사진을 원래 비율로 보이게 합니다. vw와 vh는 뷰포트 너비와 높이의 100분의 1입니다. min()은 크기에 상한을 두고, clamp()는 제목 크기가 최솟값과 최댓값 사이에서 변하게 합니다. overflow-wrap: anywhere는 매우 긴 제목이 페이지를 넓히지 않고 줄바꿈되게 합니다.

:focus-visible은 키보드 초점을 표시해야 하는 컨트롤을 선택합니다. 대비가 높은 윤곽선과 outline-offset은 레이아웃을 움직이지 않고 현재 버튼을 드러냅니다. 다른 뚜렷한 표시 없이 브라우저의 윤곽선을 없애지 마세요. 이 프로젝트에서는 2픽셀 이상의 뚜렷한 초점 윤곽선, 2픽셀 이상의 뚜렷하게 보이는 테두리, 또는 키보드 초점을 받을 때 나타나는 보이는 그림자를 허용합니다. 이러한 관찰 가능한 동작을 유지하면 색상이나 레이아웃을 바꿔도 됩니다.

아래쪽 규칙은 준비된 뷰어를 보존합니다. ::backdrop은 모달 대화상자 뒤의 배경을 꾸미고, :disabled는 경계 버튼을 시각적으로 구분하며, 일반 뷰어 이미지가 display: block을 사용해도 [hidden]은 실패한 이미지를 실제로 숨겨야 합니다. 동작 줄이기 미디어 쿼리는 브라우저의 동작 설정을 존중합니다. 이 전시에는 애니메이션 전환이 없습니다.

styles.css 전체를 다음 완전한 스타일시트로 바꾸고 저장하세요.

* { box-sizing: border-box; }
body { margin: 0; background: #f5f1e8; color: #252c28; font: 17px/1.6 system-ui, sans-serif; }
main { max-width: 1160px; margin: auto; padding: 40px 24px; }
header { max-width: 760px; margin-bottom: 40px; }
.eyebrow { color: #6e593f; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; }
h1 { margin: 12px 0; font: clamp(2.5rem, 6vw, 5rem)/1.08 Georgia, serif; }
.intro { max-width: 40rem; color: #5c655e; }
.photo-grid { display: flex; flex-wrap: wrap; gap: 28px; }
figure { flex: 1 1 260px; min-width: 0; margin: 0; }
button { color: inherit; font: inherit; cursor: pointer; }
.photo-button { width: 100%; padding: 0; text-align: left; background: transparent; border: 0; }
.photo-button img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 4px; }
.photo-name { display: block; margin-top: 12px; font: 1.5rem/1.3 Georgia, serif; overflow-wrap: anywhere; }
figcaption { margin-top: 6px; color: #5c655e; font-size: 14px; }
a { color: #814820; text-underline-offset: 4px; }
button:focus-visible, a:focus-visible { outline: 3px solid #af421f; outline-offset: 5px; }
footer { margin-top: 48px; border-top: 1px solid #d1c8b8; padding-top: 18px; color: #5c655e; font-size: 13px; }
dialog { width: min(880px, 94vw); max-height: 94vh; overflow: auto; padding: 24px; border: 0; border-radius: 8px; background: #fffcf6; color: #252c28; }
dialog::backdrop { background: #19251fe6; }
dialog h2 { font: 1.8rem/1.3 Georgia, serif; overflow-wrap: anywhere; }
dialog img { display: block; max-width: 100%; max-height: 52vh; margin: auto; object-fit: contain; }
dialog img[hidden] { display: none; }
dialog button { padding: 8px 14px; border: 1px solid #6c746b; border-radius: 4px; background: #fffcf6; }
dialog nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
dialog button:disabled { opacity: .45; cursor: default; }
#photo-position { white-space: nowrap; }
#photo-error { color: #9b301c; }
#photo-error:empty { display: none; }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }

Web 8080으로 전환하고 Current exhibition을 새로 고칩니다. 넓은 뷰포트에서 카드들은 한 줄을 공유하고 좁은 뷰포트에서는 읽기 쉬운 한 열로 쌓입니다. 브라우저나 웹 사이트 패널을 약 390픽셀 너비로 줄였다가 다시 넓혀 보세요. 페이지의 가로 스크롤, 잘린 제목, 사라진 카드가 없어야 합니다. 실제 큰 사진은 열었을 때 세로 또는 정사각형 모양을 유지해야 합니다.

페이지에서 Tab을 반복해서 누릅니다. 웹 사이트 링크와 세 사진 버튼 모두 보이는 초점 윤곽선이 있어야 합니다. Enter는 초점이 있는 사진을 열고 Esc는 해당 카드로 돌아옵니다. 어떤 설명도 마우스 올리기를 요구해서는 안 됩니다. Image credits를 열어 실제 페이지에 NASA 이미지 출처가 나열되는지 확인한 뒤 Current exhibition으로 돌아가세요. 이들은 웹 사이트 링크이며 WebIDE와 Web 8080을 전환하는 것과 다릅니다.

레이아웃이 여전히 단순하면 저장된 파일 이름이 styles.css이고 index.html이 여전히 연결하는지 확인합니다. 카드가 페이지보다 넓으면 min-width: 0, 이미지의 width: 100%, flex 줄바꿈 규칙을 확인하세요. 필요하면 전체 스타일시트를 복구하되 어려운 사진을 숨기지 말고 모든 사진을 유지합니다. 두 화면 크기와 키보드 경로를 검토한 뒤 최종 검사를 실행하세요.

독립적으로 식별되는 세 사진이 있는 완성된 Moonroom 갤러리

이 최종 넓은 화면에서 첫 번째와 마지막 Earthrise의 설명을 비교하세요. 사진은 서로 다른 임무에서 왔습니다. 발자국은 독립된 중간 카드로 유지됩니다. 좁은 화면에서는 같은 세 카드가 세로로 쌓입니다.

요약

실제 이미지를 설명과 연결하고, 의미 있는 카드 하나를 데이터 기반의 전체 모음으로 확장했으며, 안정적인 ID로 같은 제목의 두 사진을 구분했습니다. 반응형 갤러리는 모든 카드, 실제 이미지 내용, 보이는 키보드 초점을 유지합니다. 다음 실습에서는 탐색 경계와 복구 가능한 이미지 오류를 포함한 상세 뷰어를 직접 만듭니다.