이제는 브라우저 화면에서 버튼과 입력이 어떻게 반응하는지 직접 만들어 볼 차례입니다. DOM(Document Object Model)은 브라우저가 만든 화면 트리이며, 이벤트는 사용자 행동을 뜻합니다. 글 전체는 개념 → 코드 → 이유 순서로 흐릅니다.
이번 글에서 새로 나오는 용어
- addEventListener: 클릭·입력 같은 사건이 발생했을 때 실행할 함수를 등록하는 메서드입니다.
- 이벤트 위임: 리스트 부모에 이벤트를 한 번만 달고 실제 클릭된 자식 요소를 판별하는 성능 좋은 패턴입니다.
- IntersectionObserver: 요소가 화면에 들어왔는지 자동으로 알려주는 감시자 API라서 스크롤 애니메이션에 유리합니다.
핵심 개념
- DOM 선택자:
querySelector,querySelectorAll처럼 요소를 찾는 함수입니다. 선택 결과가 없을 수 있으니 optional chaining(?.)을 붙여 안전하게 다룹니다. - 이벤트 핸들러: 클릭, 입력, 스크롤 같은 사건이 일어났을 때 실행되는 함수입니다.
addEventListener로 등록합니다. - classList API:
classList.add/remove/toggle로 CSS 클래스를 바꿔 상태를 시각적으로 표현합니다. - 이벤트 위임: 리스트 전체에 한 번만 이벤트를 달고, 실제 클릭된 요소를
event.target으로 판별하는 패턴입니다. 새로 추가된 항목도 자동으로 대응합니다. - IntersectionObserver: 화면에 요소가 들어왔는지 감시하는 브라우저 API입니다. 스크롤에 따라 애니메이션을 적용할 때 쓰며, 직접 스크롤 이벤트를 돌리는 것보다 성능 부담이 작습니다.
코드로 확인하기
const toggleBtn = document.querySelector(".nav-toggle");
const menu = document.querySelector(".nav-menu");
toggleBtn?.addEventListener("click", () => {
menu?.classList.toggle("is-open");
});
네비게이션 토글 버튼은 모바일 웹 UI에서 가장 자주 만나는 패턴입니다. classList.toggle 한 줄로 열림/닫힘을 표현할 수 있습니다.
const form = document.querySelector(".todo-form");
const list = document.querySelector(".todo-list");
form?.addEventListener("submit", (event) => {
event.preventDefault();
const input = form.querySelector("input[name=title]");
const value = input?.value.trim();
if (!value) return;
const item = document.createElement("li");
item.className = "todo-item";
item.innerHTML = `${value} <button class="todo-remove">삭제</button>`;
list?.appendChild(item);
input.value = "";
});
preventDefault는 폼이 페이지를 새로고침하지 않게 막아 줍니다. 이렇게 해야 단일 페이지 실습에서도 입력 즉시 리스트가 갱신됩니다.
list?.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (!target.matches(".todo-remove")) return;
target.closest(".todo-item")?.remove();
});
이벤트 위임을 사용하면 새 항목이 추가돼도 삭제 버튼이 그대로 작동합니다.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
}
});
});
document.querySelectorAll(".feature").forEach((section) => {
observer.observe(section);
});
IntersectionObserver는 특정 요소가 뷰포트에 들어왔는지 알려주는 감시자입니다. 스크롤 이벤트를 직접 돌리는 것보다 배터리 사용량을 줄일 수 있습니다.
BrowserMock로 상태 흐름 시각화
DOM 트리를 직접 그리지 않고도 UI 상태가 어떻게 변하는지 확인하고 싶다면 BrowserMock을 이용해 HTML 조각을 가짜 브라우저에 넣어 볼 수 있습니다.
const { document } = mock(`
<button class="nav-toggle"></button>
<nav class="nav-menu"></nav>
`);
const toggleBtn = document.querySelector(".nav-toggle");
const menu = document.querySelector(".nav-menu");
toggleBtn?.addEventListener("click", () => {
menu?.classList.toggle("is-open");
});
toggleBtn?.dispatchEvent(new document.defaultView.Event("click"));
console.log(menu?.className); // "nav-menu is-open"
BrowserMock은 DOMSelection → 이벤트 → 상태 변화가 어떤 순서로 이어지는지 콘솔에 바로 찍어 줍니다. UI 스케치 단계에서도 classList 변화를 시각화할 수 있어 실서비스 반영 전에 구조를 점검하기 좋습니다.
왜 중요한가
- 학생회 웹사이트처럼 빠른 반응이 필요한 페이지에서는 DOM 선택과 이벤트 패턴이 곧 사용자 경험 품질로 이어집니다.
- 입력값을 읽고 즉시 리스트를 갱신하면 서버 왕복 없이도 “내가 방금 추가했다”는 피드백을 줄 수 있습니다.
- 이벤트 위임과 IntersectionObserver는 요소 수가 늘어날 때 성능 저하를 막아 줍니다. 모바일 네트워크나 저사양 기기에서도 부드럽게 동작하도록 돕습니다.
실습
- 따라 하기:
todo-form,todo-list,nav-toggle요소를 준비하고, 추가·삭제·토글 이벤트를 모두 연결합니다. - 확장하기: 각 todo 항목에
data-priority를 저장한 뒤 버튼을 눌렀을 때 우선순위에 따라 클래스를 바꾸고, IntersectionObserver로 스크롤 애니메이션을 추가합니다. - 디버깅: 선택자를 일부러 틀려
null이 반환되도록 만든 뒤 optional chaining을 적용해 에러를 막고, 콘솔에서event.target과event.currentTarget차이를 출력합니다. - 완료 기준: 단일 HTML 페이지에서 추가/삭제/토글/스크롤 반응이 모두 정상으로 작동하고 새 항목에도 동일하게 이벤트가 적용되면 실습 완료입니다.
마무리
DOM 선택과 이벤트 흐름만 정확히 잡아도 대부분의 UI 인터랙션을 직접 만들 수 있습니다. 다음 글에서는 fetch와 async/await로 서버 데이터를 불러와 지금 만든 화면 패턴에 연결해 보겠습니다.
💬 댓글
이 글에 대한 의견을 남겨주세요