이번 편의 결과물: projects.html 프로젝트 카드의 “자세히” 버튼을 누르면 dialog#project-dialog가 열려 프로젝트 상세 정보를 보여줍니다 · 다루는 개념: show()와 showModal()의 차이, ::backdrop, top layer, ESC로 닫히는 기본 동작
이 편에서 만드는 파일
web-practice/portfolio/
├── projects.html ~ (dialog#project-dialog 마크업 추가, 카드마다 "자세히" 버튼 추가)
└── js/
└── main.js ~ (프로젝트 데이터, dialog 여는 로직 작성)개념 정리
show()와 showModal()
<dialog>는 두 가지 방식으로 열립니다. 차이는 배경 상호작용 차단 여부입니다.
| 메서드 | 배경 클릭·포커스 | 시각 효과 | 용도 |
|---|---|---|---|
show() | 막지 않음(non-modal) | ::backdrop 없음 | 알림 패널처럼 배경과 공존하는 UI |
showModal() | 막음(modal) | ::backdrop 생김 | 사용자가 반드시 처리해야 하는 모달 |
프로젝트 상세 정보는 사용자가 “닫기”를 눌러야 원래 화면으로 돌아가는 것이 자연스러우므로 이 편에서는 showModal()만 씁니다. close()는 두 방식 모두에서 닫을 때 씁니다.
::backdrop과 top layer
showModal()로 연 dialog는 브라우저가 별도로 관리하는 top layer(문서 렌더링 트리 바깥의 최상단 레이어)에 올라갑니다. z-index 값을 조작하지 않아도 항상 다른 모든 요소 위에 그려지는 이유입니다. 이때 dialog와 나머지 화면 사이에 ::backdrop 의사 요소가 자동으로 생깁니다. 이 편에서는 최소한의 어둡기만 넣습니다.
/* portfolio/css/styles.css (css_fundamentals 과목에서 정식 배치, 지금은 개념 확인용) */
dialog#project-dialog::backdrop {
background: rgb(0 0 0 / 0.5);
}ESC로 닫히는 기본 동작
showModal()로 연 dialog는 별도 코드 없이 Esc 키로 닫힙니다. 브라우저가 자동으로 cancel 이벤트를 발생시키고 dialog를 닫습니다. 이 동작을 막으려면 cancel 이벤트에서 preventDefault()를 호출해야 하는데, 이 편에서는 기본 동작을 그대로 둡니다. 04편에서 “닫기” 버튼과 포커스 복귀를 다룰 때 이 기본 동작과 함께 씁니다.
실습
1. dialog 마크업 추가하기
projects.html의 project-grid 아래, </main> 앞에 재사용할 dialog 하나를 둡니다. 카드마다 dialog를 따로 만들지 않고 하나의 dialog 내용을 그때그때 채우는 방식을 씁니다.
<!-- portfolio/projects.html -->
<dialog id="project-dialog" class="project-dialog">
<h2 class="dialog-title"></h2>
<p class="dialog-background"></p>
<p class="dialog-role"></p>
<p class="dialog-learned"></p>
</dialog>2. 카드마다 “자세히” 버튼 추가하기
6개 프로젝트 카드 각각에 data-project-id를 붙인 버튼을 넣습니다. 아래는 첫 번째 카드입니다. 나머지 5개도 같은 구조로 id만 bookshelf·notes·a11y·tokens·todo-cli로 바꿔 반복합니다.
<!-- portfolio/projects.html의 project-grid 안 -->
<div class="project-grid">
<article class="project-card">
<img src="images/project-01.svg" alt="오늘의 날씨 프로젝트 화면" width="1200" height="800" />
<h3>오늘의 날씨</h3>
<p>위치 기반 날씨를 카드로 보여주는 웹앱입니다. 오프라인에서도 마지막 데이터를 보여줍니다.</p>
<ul class="tags">
<li>JavaScript</li>
<li>PWA</li>
</ul>
<button type="button" class="btn-detail" data-project-id="weather">자세히</button>
</article>
<!-- bookshelf, notes, a11y, tokens, todo-cli 카드도 같은 구조로 반복 -->
</div>main.js가 없어도 페이지는 깨지지 않지만, 지금은 버튼을 눌러도 아무 일도 일어나지 않는 상태입니다. 3단계에서 채웁니다.
3. main.js에 프로젝트 데이터와 여는 로직 작성하기
copy.md의 6개 프로젝트 설명을 3~4문장으로 늘려 데이터 객체를 만듭니다. 버튼을 누르면 id로 데이터를 찾아 dialog 내용을 채우고 showModal()을 호출합니다.
// portfolio/js/main.js
const projectDialog = document.querySelector('#project-dialog');
const projectDetails = {
weather: {
title: '오늘의 날씨',
background: '위치 기반 날씨 정보를 빠르게 확인할 수 있는 화면이 필요해서 시작한 프로젝트입니다.',
role: 'fetch로 외부 날씨 API를 호출하고, Service Worker로 마지막 응답을 캐시했습니다.',
learned: '네트워크가 끊겨도 마지막 데이터를 보여주는 오프라인 우선 설계를 배웠습니다.',
},
bookshelf: {
title: '독서 기록 bookshelf',
background: '읽은 책과 별점을 기록할 곳이 마땅치 않아 직접 만든 React 앱입니다.',
role: '컴포넌트를 상태별로 나누고, Context로 테마를 전역에서 관리했습니다.',
learned: 'props와 state를 분리하는 기준, 리스트 렌더링 최적화를 배웠습니다.',
},
notes: {
title: '메모 앱 notes',
background: '서버 없이 브라우저 저장소만으로 동작하는 메모 앱을 만들어 보고 싶었습니다.',
role: 'IndexedDB로 메모를 저장하고, 검색은 Worker 스레드로 분리했습니다.',
learned: '메인 스레드를 막지 않고 무거운 검색을 처리하는 방법을 배웠습니다.',
},
a11y: {
title: '접근성 체크리스트',
background: 'WCAG 항목을 하나씩 점검할 수 있는 정적 사이트가 필요했습니다.',
role: '시맨틱 요소와 ARIA 속성만으로 체크리스트 UI를 구성했습니다.',
learned: '시맨틱 마크업이 접근성의 기본 토대라는 것을 실감했습니다.',
},
tokens: {
title: '디자인 토큰 미리보기',
background: 'CSS 변수로 만든 색·간격 토큰을 한눈에 비교하고 싶어 만들었습니다.',
role: 'CSS 커스텀 속성을 값별로 나열하고, 배경색을 그대로 미리보기로 썼습니다.',
learned: '디자인 토큰을 코드로 관리하면 일관성을 지키기 쉽다는 것을 배웠습니다.',
},
'todo-cli': {
title: 'todo-cli',
background: '터미널에서 바로 쓸 수 있는 할 일 관리 도구가 필요해 만들었습니다.',
role: 'Node.js로 파일 저장소를 구현하고, 명령어별로 모듈을 나눴습니다.',
learned: '파일 I/O를 비동기로 다루는 법과 CLI 인자 파싱을 배웠습니다.',
},
};
document.querySelectorAll('[data-project-id]').forEach((button) => {
button.addEventListener('click', () => {
const detail = projectDetails[button.dataset.projectId];
projectDialog.querySelector('.dialog-title').textContent = detail.title;
projectDialog.querySelector('.dialog-background').textContent = detail.background;
projectDialog.querySelector('.dialog-role').textContent = detail.role;
projectDialog.querySelector('.dialog-learned').textContent = detail.learned;
projectDialog.showModal();
});
});4. 실행하고 확인하기
VS Code의 Live Server 등으로 projects.html을 엽니다. main.js를 아직 script 태그로 연결하지 않았다면 </body> 앞에 다음 한 줄을 추가합니다.
<script type="module" src="js/main.js"></script>$ (VS Code Live Server 확장으로 projects.html 열기)확인
- 첫 번째 카드의 “자세히”를 누르면 화면 가운데 dialog가 뜨고, 배경이 반투명하게 어두워진다(
::backdrop). - dialog 안 제목·본문 3문단이 클릭한 카드에 맞는 내용으로 채워진다.
Esc키를 누르면 별도 코드 없이 dialog가 닫힌다.- 다른 카드를 눌러도 같은 dialog가 내용만 바뀌어 다시 열린다.
직접 해보기
show()로 바꿔서 열어 보고 무엇이 달라지는지 관찰해 보세요. showModal()을 show()로 바꾼 뒤 배경 카드를 클릭하거나 Tab 키로 포커스를 옮겨 보면 어떤 차이가 있나요?
답 확인
show()로 열면 ::backdrop이 생기지 않아 배경이 어두워지지 않고, 배경의 다른 버튼이나 링크를 그대로 클릭·포커스할 수 있습니다. showModal()은 top layer에 올리면서 배경 상호작용을 차단하지만, show()는 dialog를 문서 흐름 위에 겹쳐 보여줄 뿐 배경을 막지 않습니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 버튼을 눌러도 dialog가 안 열린다 | main.js가 script 태그로 연결되지 않았다 | </body> 앞 script type="module" src="js/main.js" 확인 |
| dialog는 열리는데 배경이 어두워지지 않는다 | show()를 썼거나 ::backdrop CSS가 없다 | showModal()인지 확인하고, ::backdrop 스타일이 css_fundamentals 편에서 아직 없다면 정상이다 |
| 두 번째 카드를 누르면 내용이 안 바뀐다 | data-project-id 값과 projectDetails 키가 다르다 | 버튼의 data-project-id와 객체 키 철자를 문자 그대로 비교한다 |