본문 바로가기

DOM 조작과 이벤트 처리 기초

DOM 요소를 선택해 내용·속성·스타일·구조를 바꾸고 이벤트 리스너로 클릭과 입력에 반응하는 UI를 만듭니다.

우리는 지금까지 자바스크립트 기본 문법, 데이터 저장/조작, 흐름 제어를 학습했습니다.

이제는 이 지식을 바탕으로 웹 페이지와 직접 상호작용하는 단계로 넘어갑니다.

이를 가능하게 하는 핵심이 DOM(Document Object Model) 조작과 이벤트 처리입니다.

이번 장에서는 두 개념을 통해 정적 페이지에 동작과 반응을 추가하는 기본 흐름을 정리합니다.

DOM 선택부터 상태 갱신까지 — 상태 추적

등록→대기→이벤트→DOM 갱신 반복과 소유자 종료 시 별도 해제 경로가 있다. 등록 자원/현재 DOM 값/소유자 상태를 추적하면 매 갱신 후 해제한다는 오해를 줄인다.

DOM 갱신: 반복 처리와 등록 자원의 정리

DOM UPDATE · RESOURCE LIFETIME

DOM 갱신: 반복 처리와 등록 자원의 정리

사용자 이벤트를 받을 때마다 현재 값을 읽고 필요한 DOM을 갱신한다. 정리는 매 갱신의 뒤가 아니라, 등록한 자원이 더 이상 필요 없거나 교체될 때 소유자가 수행한다.

DOM 이벤트 반복 처리와 별도 정리 경로선택과 등록 뒤 이벤트를 기다린다. 핸들러는 다음 상태를 계산하여 필요한 DOM을 갱신하고 다시 이벤트를 기다린다. 소유자가 종료되거나 등록 자원이 교체될 때 별도 경로로 해제하며 매 갱신 뒤 해제하는 것은 아니다.선택·리스너 등록대상 존재·등록 범위 확인이벤트 대기click / input핸들러 실행상태 읽기·다음 값 계산필요한 DOM 갱신textContent · class소유자 종료·교체리스너 제거 / signal abort다른 자원은 해당 API 정리등록 자원 수명 종료
DOM 이벤트 반복 처리와 별도 정리 경로선택과 등록 뒤 이벤트를 기다린다. 핸들러는 다음 상태를 계산하여 필요한 DOM을 갱신하고 다시 이벤트를 기다린다. 소유자가 종료되거나 등록 자원이 교체될 때 별도 경로로 해제하며 매 갱신 뒤 해제하는 것은 아니다.선택·등록대상 존재·범위 확인이벤트 대기click / input핸들러 실행상태 읽기다음 값 계산DOM 갱신textContent · class소유자 종료·교체리스너 제거 / abort다른 자원은 해당 API 정리등록 자원 수명 종료
  1. 선택·등록

    대상이 존재하는지 확인하고 필요한 범위에 리스너를 등록한다.

  2. 사용자 이벤트·핸들러

    click/input 이벤트에서 현재 값과 전달된 데이터를 읽고 다음 값을 계산한다.

  3. 필요한 DOM 갱신

    textContent·class·attribute와 필요한 노드만 바꾸고 다음 이벤트를 기다린다.

  4. 등록 자원 종료·교체

    소유자가 리스너를 제거하거나 연결한 signal을 abort한다. 타이머·observer는 각각의 정리 API를 사용한다.

DOM 연결 여부만으로 정리 책임이 끝나지는 않는다

노드를 제거해도 다른 참조나 등록 자원이 남을 수 있다. AbortSignal은 그 signal을 연결한 리스너를 해제하며, 일반 타이머나 observer까지 자동 정리하지 않는다. 텍스트는 textContent로 처리하고 신뢰하지 않는 입력을 innerHTML에 바로 넣지 않는다.

사용자 이벤트의 일반적인 처리 흐름을 그렸다. dispatchEvent로 만든 이벤트는 동기 호출일 수 있고, 한 번 처리하는 리스너는 once 옵션을 사용할 수 있다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석선택·등록: 대상이 존재하는지 확인하고 필요한 범위에 리스너를 등록한다.
원도표 조건·주석사용자 이벤트·핸들러: click/input 이벤트에서 현재 값과 전달된 데이터를 읽고 다음 값을 계산한다.
원도표 조건·주석필요한 DOM 갱신: textContent·class·attribute와 필요한 노드만 바꾸고 다음 이벤트를 기다린다.
원도표 조건·주석등록 자원 종료·교체: 소유자가 리스너를 제거하거나 연결한 signal을 abort한다. 타이머·observer는 각각의 정리 API를 사용한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
listener 등록listener: 연결됨 / DOM: 이전 표시callback을 등록하고 현재 함수가 반환한다등록 자체가 사용자 사건 실행은 아니다
사건 처리callback: 실행 중 / DOM: 새 결과후속 event callback이 현재 사건을 읽어 DOM을 바꾼다콜백이 실행될 때만 UI 동작이 발생한다
소유자 종료listener: 해제 / 후속 사건: 이 handler 없음더 이상 필요하지 않을 때 listener를 해제한다매 사건마다가 아니라 연결 소유권 종료 시 정리한다

DOM 선택부터 상태 갱신까지 — 상태 추적

등록→대기→이벤트→DOM 갱신 반복과 소유자 종료 시 별도 해제 경로가 있다. 등록 자원/현재 DOM 값/소유자 상태를 추적하면 매 갱신 후 해제한다는 오해를 줄인다.

DOM 갱신: 반복 처리와 등록 자원의 정리

DOM UPDATE · RESOURCE LIFETIME

DOM 갱신: 반복 처리와 등록 자원의 정리

사용자 이벤트를 받을 때마다 현재 값을 읽고 필요한 DOM을 갱신한다. 정리는 매 갱신의 뒤가 아니라, 등록한 자원이 더 이상 필요 없거나 교체될 때 소유자가 수행한다.

DOM 이벤트 반복 처리와 별도 정리 경로선택과 등록 뒤 이벤트를 기다린다. 핸들러는 다음 상태를 계산하여 필요한 DOM을 갱신하고 다시 이벤트를 기다린다. 소유자가 종료되거나 등록 자원이 교체될 때 별도 경로로 해제하며 매 갱신 뒤 해제하는 것은 아니다.선택·리스너 등록대상 존재·등록 범위 확인이벤트 대기click / input핸들러 실행상태 읽기·다음 값 계산필요한 DOM 갱신textContent · class소유자 종료·교체리스너 제거 / signal abort다른 자원은 해당 API 정리등록 자원 수명 종료
DOM 이벤트 반복 처리와 별도 정리 경로선택과 등록 뒤 이벤트를 기다린다. 핸들러는 다음 상태를 계산하여 필요한 DOM을 갱신하고 다시 이벤트를 기다린다. 소유자가 종료되거나 등록 자원이 교체될 때 별도 경로로 해제하며 매 갱신 뒤 해제하는 것은 아니다.선택·등록대상 존재·범위 확인이벤트 대기click / input핸들러 실행상태 읽기다음 값 계산DOM 갱신textContent · class소유자 종료·교체리스너 제거 / abort다른 자원은 해당 API 정리등록 자원 수명 종료
  1. 선택·등록

    대상이 존재하는지 확인하고 필요한 범위에 리스너를 등록한다.

  2. 사용자 이벤트·핸들러

    click/input 이벤트에서 현재 값과 전달된 데이터를 읽고 다음 값을 계산한다.

  3. 필요한 DOM 갱신

    textContent·class·attribute와 필요한 노드만 바꾸고 다음 이벤트를 기다린다.

  4. 등록 자원 종료·교체

    소유자가 리스너를 제거하거나 연결한 signal을 abort한다. 타이머·observer는 각각의 정리 API를 사용한다.

DOM 연결 여부만으로 정리 책임이 끝나지는 않는다

노드를 제거해도 다른 참조나 등록 자원이 남을 수 있다. AbortSignal은 그 signal을 연결한 리스너를 해제하며, 일반 타이머나 observer까지 자동 정리하지 않는다. 텍스트는 textContent로 처리하고 신뢰하지 않는 입력을 innerHTML에 바로 넣지 않는다.

사용자 이벤트의 일반적인 처리 흐름을 그렸다. dispatchEvent로 만든 이벤트는 동기 호출일 수 있고, 한 번 처리하는 리스너는 once 옵션을 사용할 수 있다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석선택·등록: 대상이 존재하는지 확인하고 필요한 범위에 리스너를 등록한다.
원도표 조건·주석사용자 이벤트·핸들러: click/input 이벤트에서 현재 값과 전달된 데이터를 읽고 다음 값을 계산한다.
원도표 조건·주석필요한 DOM 갱신: textContent·class·attribute와 필요한 노드만 바꾸고 다음 이벤트를 기다린다.
원도표 조건·주석등록 자원 종료·교체: 소유자가 리스너를 제거하거나 연결한 signal을 abort한다. 타이머·observer는 각각의 정리 API를 사용한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
listener 등록listener: 연결됨 / DOM: 이전 표시callback을 등록하고 현재 함수가 반환한다등록 자체가 사용자 사건 실행은 아니다
사건 처리callback: 실행 중 / DOM: 새 결과후속 event callback이 현재 사건을 읽어 DOM을 바꾼다콜백이 실행될 때만 UI 동작이 발생한다
소유자 종료listener: 해제 / 후속 사건: 이 handler 없음더 이상 필요하지 않을 때 listener를 해제한다매 사건마다가 아니라 연결 소유권 종료 시 정리한다

DOM 조작: 웹 페이지의 요소 다루기

웹 브라우저가 HTML 문서를 로드하면, 그 문서는 객체 형태로 구조화되어 메모리에 저장됩니다.

이것이 바로 DOM(Document Object Model)입니다.

DOM은 HTML 문서의 각 요소를 객체(노드)로 표현하고, 이 객체들을 트리(tree) 구조로 연결합니다.

자바스크립트는 이 DOM을 이용하여 HTML 요소의 내용, 스타일, 속성 등을 읽고 변경하며, 새로운 요소를 추가하거나 기존 요소를 삭제할 수 있습니다.

요소 선택하기: 조작의 첫걸음

어떤 HTML 요소를 조작하려면, 먼저 해당 요소를 자바스크립트 코드 내에서 선택해야 합니다. 아래 HTML과 JavaScript는 짝으로 실행합니다. JavaScript를 실행할 때 대상 요소가 이미 파싱되어 있어야 하므로 인라인 스크립트는 대상 요소 뒤에 두거나, 외부 script.js는 <script src="script.js" defer></script>로 연결하세요. 존재하지 않는 id·선택자로 단일 요소를 찾으면 null이므로 그대로 textContent 등을 사용하면 오류가 납니다.

요소를 선택하는 다양한 방법이 있습니다.

document.getElementById('id명'): id 속성 값으로 요소를 선택합니다.

id는 문서 내에서 유일해야 합니다.

<h1 id="mainTitle">안녕하세요, DOM 조작!</h1>
// script.js (HTML 파일에 연결된 자바스크립트 파일)
const titleElement = document.getElementById('mainTitle');
console.log(titleElement); // <h1 id="mainTitle">...</h1> 요소 객체가 출력됩니다.

document.querySelector('CSS 선택자'): CSS 선택자와 동일한 방식으로 요소를 선택합니다.

조건에 맞는 첫 번째 요소를 반환합니다.

<p class="greeting">반갑습니다.</p>
<div id="container">
    <span class="text">첫 번째 텍스트</span>
    <span class="text">두 번째 텍스트</span>
</div>
const firstParagraph = document.querySelector('p');    // 첫 번째 <p> 태그
const greetingText = document.querySelector('.greeting'); // class가 greeting인 첫 번째 요소
const containerDiv = document.querySelector('#container'); // id가 container인 요소
const firstSpan = document.querySelector('#container .text'); // container 내부의 첫 번째 class="text" span

console.log(firstParagraph);
console.log(greetingText);
console.log(containerDiv);
console.log(firstSpan);

document.querySelectorAll('CSS 선택자'): CSS 선택자와 동일한 방식으로 요소를 선택합니다.

조건에 맞는 모든 요소를 정적인 NodeList 형태로 반환합니다. 목록은 조회 시점에 고정됩니다. 위 두 span을 조회한 뒤 세 번째 span을 추가해도 기존 목록의 길이는 2이며, 다시 조회해야 길이 3인 새 목록을 얻습니다. 다만 목록에 담긴 기존 요소 객체를 수정하면 실제 DOM도 바뀝니다.

배열과 유사하지만 완전히 같지는 않습니다.

forEach 등을 사용할 수 있습니다.

const allSpansInContainer = document.querySelectorAll('#container .text');
console.log(allSpansInContainer); // NodeList(2) [span.text, span.text]

allSpansInContainer.forEach(span => {
    console.log(span.textContent);
});
// 결과:
// 첫 번째 텍스트
// 두 번째 텍스트

요소 내용 조작하기

선택한 요소의 텍스트 내용이나 HTML 내용을 변경할 수 있습니다.

  • element.textContent: 요소 내부의 텍스트 콘텐츠를 가져오거나 설정합니다. HTML 태그는 순수한 텍스트로 처리됩니다.
  • element.innerHTML: 요소 내부의 HTML 콘텐츠를 가져오거나 설정합니다. HTML 태그가 포함된 문자열을 할당하면 브라우저가 이를 HTML로 파싱하여 렌더링합니다.
    <p id="myParagraph">원래 텍스트</p>
    <div id="myDiv"></div>
    const paragraph = document.getElementById('myParagraph');
    const myDiv = document.getElementById('myDiv');
    // textContent 변경
    paragraph.textContent = "새로운 텍스트입니다!";
    console.log(paragraph.textContent); // 결과: 새로운 텍스트입니다!
    // innerHTML 변경 (HTML 태그 포함)
    myDiv.innerHTML = "<h2>새로운 제목</h2><p>이것은 <b>HTML</b>입니다.</p>";
    innerHTML은 편리하지만, 사용자 입력을 그대로 innerHTML에 넣는 것은 보안상 취약점(XSS 공격)을 야기할 수 있으므로 주의해야 합니다.

순수한 텍스트만 변경할 때는 textContent를 사용합니다. 요소에 값을 대입하면 그 안의 기존 자식 요소도 텍스트로 교체됩니다. 입력 문자열에 태그처럼 보이는 내용이 있더라도 마크업으로 실행하지 않게 할 수 있습니다.

요소 속성(Attribute) 조작하기

HTML 요소의 속성(예: src, href, class, id 등)을 가져오거나 설정할 수 있습니다.

  • element.getAttribute('속성명'): 특정 속성의 값을 가져옵니다.
  • element.setAttribute('속성명', '값'): 특정 속성의 값을 설정합니다.
  • element.removeAttribute('속성명'): 특정 속성을 제거합니다.
    <img id="myImage" src="image1.jpg" alt="첫 번째 이미지">
    <a id="myLink" href="https://www.google.com">Google로 이동</a>
    const image = document.getElementById('myImage');
    const link = document.getElementById('myLink');
    // src 속성 값 가져오기
    console.log(image.getAttribute('src')); // 결과: image1.jpg
    // src 속성 값 변경
    image.setAttribute('src', 'image2.png');
    image.setAttribute('alt', '두 번째 이미지');
    // href 속성 값 변경
    link.setAttribute('href', 'https://www.naver.com');
    link.textContent = "Naver로 이동"; // 링크 텍스트도 변경
    // class 속성 추가/제거 (class는 특별히 classList 메서드를 주로 사용)
    // image.removeAttribute('alt'); // alt 속성 제거
클래스(Class) 조작: classList

요소의 class 속성은 스타일링에 매우 중요하며, 자바스크립트에서는 classList 속성을 통해 편리하게 조작할 수 있습니다.

  • element.classList.add('클래스명')
  • element.classList.remove('클래스명')
  • element.classList.toggle('클래스명') (있으면 제거, 없으면 추가)
  • element.classList.contains('클래스명') (해당 클래스가 있는지 확인, 불리언 반환)
    <button id="myButton" class="btn">클릭하세요</button>
    /* style.css */
    .btn {
        padding: 10px 20px;
        background-color: blue;
        color: white;
    }
    .active {
        background-color: green;
        font-weight: bold;
    }
    const button = document.getElementById('myButton');
    button.classList.add('active');    // 'active' 클래스 추가
    console.log(button.className);     // 결과: "btn active"
    button.classList.remove('btn');    // 'btn' 클래스 제거
    console.log(button.className);     // 결과: "active"
    button.classList.toggle('active'); // 'active' 클래스 제거
    console.log(button.className);     // 결과: ""
    button.classList.toggle('active'); // 'active' 클래스 다시 추가
    console.log(button.classList.contains('active')); // 결과: true

요소 스타일(CSS) 조작하기

자바스크립트로 직접 요소의 CSS 스타일을 변경할 수 있습니다.

element.style.속성명 형식으로 접근합니다.

CSS 속성 이름이 하이픈(-)으로 연결되어 있다면, 자바스크립트에서는 카멜 케이스(camelCase)로 변환하여 사용해야 합니다.

<p id="styledParagraph">이 문단은 스타일이 바뀔 것입니다.</p>
const styledParagraph = document.getElementById('styledParagraph');

styledParagraph.style.color = 'red';        // 글자색 변경
styledParagraph.style.fontSize = '20px';    // 글자 크기 변경
styledParagraph.style.backgroundColor = 'lightgray'; // 배경색 변경 (CSS: background-color)

element.style은 이 예제처럼 직접 지정한 인라인 스타일을 다룹니다. CSS 파일의 모든 계산 결과를 읽어 오는 속성은 아닙니다. 여러 속성으로 이루어진 상태를 바꾸려면 CSS 규칙을 작성하고 클래스를 토글하면 관리하기 쉽습니다. 클래스 이름만 추가해도 새 CSS 규칙이 생기는 것은 아닙니다.

새로운 요소 생성 및 추가/제거하기

DOM API를 사용하여 완전히 새로운 HTML 요소를 생성하고, 문서에 추가하거나 제거할 수 있습니다.

  • document.createElement('태그명'): 새로운 HTML 요소를 생성합니다.
  • 부모요소.appendChild(자식요소): 부모 요소의 자식으로 요소를 맨 뒤에 추가합니다.
  • 부모요소.removeChild(자식요소): 부모 요소에서 특정 자식 요소를 제거합니다.
    <ul id="myList">
        <li>기존 아이템 1</li>
        <li>기존 아이템 2</li>
    </ul>
    const myList = document.getElementById('myList');
    // 새로운 li 요소 생성
    const newItem = document.createElement('li');
    newItem.textContent = "새로운 아이템 3";
    newItem.style.color = "purple"; // 생성된 요소에 스타일 적용
    // ul에 li 추가
    myList.appendChild(newItem);
    // 결과: <ul>...<li>새로운 아이템 3</li></ul>
    // 마지막 아이템 제거
    const lastChild = myList.lastElementChild; // 마지막 자식 요소 선택
    if (lastChild) {
        myList.removeChild(lastChild);
    }
    // 결과: <ul><li>기존 아이템 1</li><li>기존 아이템 2</li></ul> (새로운 아이템 3은 사라짐)

이벤트 처리: 사용자 상호작용에 반응하기

앞의 요소 추가·제거 코드를 한 번에 실행하면 마지막에 새 항목까지 제거되어 기존 두 항목만 남습니다. 추가된 모습을 따로 보려면 appendChild까지 실행해 확인한 뒤 제거 부분을 실행하세요. 아래 이벤트 예제는 같은 버튼에 리스너가 누적될 수 있으므로 서로 다른 예제는 새 페이지에서 실행합니다.

이벤트 전파와 위임의 인과 경로 — 상태 추적

같은 click이 Window→ul→span을 거쳐 캡처/타깃 단계에 도달한 뒤 ul로 버블하며 target/currentTarget과 위임 처리 지점이 다르다. 현재 전파 단계와 처리 여부가 실제로 바뀐다.

이벤트 위임: 시작점과 리스너 등록점은 다르다

EVENT PROPAGATION · DELEGATION

이벤트 위임: 시작점과 리스너 등록점은 다르다

button 내부 span을 클릭해도 조상 ul의 리스너에서 처리할 수 있다. target은 시작점, currentTarget은 현재 실행 중인 리스너가 등록된 요소다.

span에서 시작한 click과 ul의 위임 리스너중간 DOM 조상은 생략했다. Window에서 ul과 span으로 캡처하고 span의 타깃 단계 뒤 ul로 버블링한다. ul의 위임 리스너는 closest로 버튼을 찾고 포함 범위를 확인한다. 중단하지 않으면 Window 방향으로 전파가 계속된다. span은 클릭 시작점이며 ul은 현재 리스너 등록점이다.Window조상 ulspan캡처캡처타깃버블버블 계속closest · 범위 확인 · 처리
span에서 시작한 click과 ul의 위임 리스너중간 DOM 조상은 생략했다. Window에서 ul과 span으로 캡처하고 span의 타깃 단계 뒤 ul로 버블링한다. ul의 위임 리스너는 closest로 버튼을 찾고 포함 범위를 확인한다. 중단하지 않으면 Window 방향으로 전파가 계속된다. span은 클릭 시작점이며 ul은 현재 리스너 등록점이다.Window조상 ulspan캡처캡처타깃버블버블 계속closest · 범위 확인 · 처리
  1. Capture · 바깥에서 target으로

    Window와 조상을 거쳐 클릭된 span으로 내려간다. 중간 조상은 그림에서 생략했다.

  2. Target 단계

    target의 capture 리스너와 비캡처 리스너가 target 단계에서 실행된다.

  3. Bubble · 조상의 리스너

    버블링 click이 ul에 도달하면 등록한 비캡처 위임 리스너가 실행된다.

  4. 처리 뒤에도 전파 가능

    대상 판별과 처리만으로 전파가 끝나지 않는다. 중단하지 않으면 바깥 조상으로 계속 간다.

target · 클릭 시작점

이 예시에서는 button 내부 span이다. Element인 target에서 closest("[data-action]")로 실제 처리 버튼을 찾는다.

currentTarget · 현재 리스너의 등록점

위임 리스너가 실행되는 동안 ul이다. 찾은 버튼이 없거나 currentTarget.contains(버튼)이 false이면 처리하지 않는다.

기본 동작 취소와 전파 중단은 다른 제어다

preventDefault는 취소 가능한 이벤트의 기본 동작을 막고 전파를 멈추지는 않는다. passive 리스너에서는 기본 동작을 취소할 수 없다. stopPropagation은 다른 노드로의 전파를, stopImmediatePropagation은 현재 노드의 뒤쪽 리스너까지 멈춘다.

그림은 전파를 중단하지 않은 일반 DOM의 bubbling click 예시다. Shadow DOM의 retargeting과 이벤트별 기본 동작 시점은 별도 규칙이다. 리스너 정리에는 동일한 함수 참조와 capture 값 또는 연결한 AbortSignal을 사용한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석Capture · 바깥에서 target으로: Window와 조상을 거쳐 클릭된 span으로 내려간다. 중간 조상은 그림에서 생략했다.
원도표 조건·주석Target 단계: target의 capture 리스너와 비캡처 리스너가 target 단계에서 실행된다.
원도표 조건·주석Bubble · 조상의 리스너: 버블링 click이 ul에 도달하면 등록한 비캡처 위임 리스너가 실행된다.
원도표 조건·주석처리 뒤에도 전파 가능: 대상 판별과 처리만으로 전파가 끝나지 않는다. 중단하지 않으면 바깥 조상으로 계속 간다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
capturetarget: span 유지 / currentTarget: 각 listener 위치window부터 ul을 지나 span target으로 내려간다target과 현재 handler 위치는 다르다
target 및 bubbletarget: span 유지 / currentTarget: ul 등 현재 listenerspan target 뒤 ul로 올라가는 handler를 실행한다위임 listener의 currentTarget은 현재 등록 위치이고 target은 최초 span으로 유지된다
위임 대상 검사검사: closest + contains / 동작: 허용된 대상만closest 결과가 지정 container 안인지 확인한다바깥 조상 매칭을 같은 목록 항목으로 처리하지 않는다

이벤트 전파와 위임의 인과 경로 — 상태 추적

같은 click이 Window→ul→span을 거쳐 캡처/타깃 단계에 도달한 뒤 ul로 버블하며 target/currentTarget과 위임 처리 지점이 다르다. 현재 전파 단계와 처리 여부가 실제로 바뀐다.

이벤트 위임: 시작점과 리스너 등록점은 다르다

EVENT PROPAGATION · DELEGATION

이벤트 위임: 시작점과 리스너 등록점은 다르다

button 내부 span을 클릭해도 조상 ul의 리스너에서 처리할 수 있다. target은 시작점, currentTarget은 현재 실행 중인 리스너가 등록된 요소다.

span에서 시작한 click과 ul의 위임 리스너중간 DOM 조상은 생략했다. Window에서 ul과 span으로 캡처하고 span의 타깃 단계 뒤 ul로 버블링한다. ul의 위임 리스너는 closest로 버튼을 찾고 포함 범위를 확인한다. 중단하지 않으면 Window 방향으로 전파가 계속된다. span은 클릭 시작점이며 ul은 현재 리스너 등록점이다.Window조상 ulspan캡처캡처타깃버블버블 계속closest · 범위 확인 · 처리
span에서 시작한 click과 ul의 위임 리스너중간 DOM 조상은 생략했다. Window에서 ul과 span으로 캡처하고 span의 타깃 단계 뒤 ul로 버블링한다. ul의 위임 리스너는 closest로 버튼을 찾고 포함 범위를 확인한다. 중단하지 않으면 Window 방향으로 전파가 계속된다. span은 클릭 시작점이며 ul은 현재 리스너 등록점이다.Window조상 ulspan캡처캡처타깃버블버블 계속closest · 범위 확인 · 처리
  1. Capture · 바깥에서 target으로

    Window와 조상을 거쳐 클릭된 span으로 내려간다. 중간 조상은 그림에서 생략했다.

  2. Target 단계

    target의 capture 리스너와 비캡처 리스너가 target 단계에서 실행된다.

  3. Bubble · 조상의 리스너

    버블링 click이 ul에 도달하면 등록한 비캡처 위임 리스너가 실행된다.

  4. 처리 뒤에도 전파 가능

    대상 판별과 처리만으로 전파가 끝나지 않는다. 중단하지 않으면 바깥 조상으로 계속 간다.

target · 클릭 시작점

이 예시에서는 button 내부 span이다. Element인 target에서 closest("[data-action]")로 실제 처리 버튼을 찾는다.

currentTarget · 현재 리스너의 등록점

위임 리스너가 실행되는 동안 ul이다. 찾은 버튼이 없거나 currentTarget.contains(버튼)이 false이면 처리하지 않는다.

기본 동작 취소와 전파 중단은 다른 제어다

preventDefault는 취소 가능한 이벤트의 기본 동작을 막고 전파를 멈추지는 않는다. passive 리스너에서는 기본 동작을 취소할 수 없다. stopPropagation은 다른 노드로의 전파를, stopImmediatePropagation은 현재 노드의 뒤쪽 리스너까지 멈춘다.

그림은 전파를 중단하지 않은 일반 DOM의 bubbling click 예시다. Shadow DOM의 retargeting과 이벤트별 기본 동작 시점은 별도 규칙이다. 리스너 정리에는 동일한 함수 참조와 capture 값 또는 연결한 AbortSignal을 사용한다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석Capture · 바깥에서 target으로: Window와 조상을 거쳐 클릭된 span으로 내려간다. 중간 조상은 그림에서 생략했다.
원도표 조건·주석Target 단계: target의 capture 리스너와 비캡처 리스너가 target 단계에서 실행된다.
원도표 조건·주석Bubble · 조상의 리스너: 버블링 click이 ul에 도달하면 등록한 비캡처 위임 리스너가 실행된다.
원도표 조건·주석처리 뒤에도 전파 가능: 대상 판별과 처리만으로 전파가 끝나지 않는다. 중단하지 않으면 바깥 조상으로 계속 간다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
capturetarget: span 유지 / currentTarget: 각 listener 위치window부터 ul을 지나 span target으로 내려간다target과 현재 handler 위치는 다르다
target 및 bubbletarget: span 유지 / currentTarget: ul 등 현재 listenerspan target 뒤 ul로 올라가는 handler를 실행한다위임 listener의 currentTarget은 현재 등록 위치이고 target은 최초 span으로 유지된다
위임 대상 검사검사: closest + contains / 동작: 허용된 대상만closest 결과가 지정 container 안인지 확인한다바깥 조상 매칭을 같은 목록 항목으로 처리하지 않는다

이벤트(Event)는 웹 페이지에서 발생하는 모든 종류의 사건을 의미합니다.

예를 들어, 사용자가 버튼을 클릭하는 것, 키보드를 누르는 것, 마우스를 움직이는 것, 페이지 로딩이 완료되는 것 등 모든 것이 이벤트입니다.

이벤트 처리(Event Handling)는 이러한 이벤트가 발생했을 때 특정 자바스크립트 코드를 실행하도록 하는 메커니즘입니다.

이벤트 리스너 등록하기

요소에 이벤트를 감지하고 처리하는 함수(이벤트 핸들러)를 연결하는 가장 일반적이고 권장되는 방법은 addEventListener() 메서드를 사용하는 것입니다.

// element.addEventListener('이벤트_타입', 함수);
  • 이벤트_타입: 발생할 이벤트의 종류를 나타내는 문자열입니다. (예: 'click', 'mouseover', 'keydown', 'submit', 'load')
  • 함수: 이벤트가 발생했을 때 실행될 코드 블록을 포함하는 함수(이벤트 핸들러)입니다.
    <button id="clickMeBtn">여기를 클릭하세요</button>
    <input type="text" id="myInput" placeholder="여기에 입력하세요">
    const clickButton = document.getElementById('clickMeBtn');
    const myInput = document.getElementById('myInput');
    // 1. 클릭 이벤트 (click)
    clickButton.addEventListener('click', function() {
        alert("버튼이 클릭되었습니다!");
    });
    // 2. 입력 이벤트 (input)
    myInput.addEventListener('input', function() {
        console.log("현재 입력 값:", myInput.value);
    });
    // 3. 마우스 오버 이벤트 (mouseover)
    clickButton.addEventListener('mouseover', function() {
        this.style.backgroundColor = 'lightgreen'; // 일반 함수 리스너의 this는 보통 currentTarget과 같습니다.
    });
    // 4. 마우스 아웃 이벤트 (mouseout)
    clickButton.addEventListener('mouseout', function() {
        this.style.backgroundColor = ''; // 원래대로 (또는 특정 색상)
    });
이벤트 객체 (Event Object)

이벤트 핸들러 함수는 종종 event 객체(관례적으로 e나 event로 명명)를 인자로 받습니다.

이 event 객체는 발생한 이벤트에 대한 자세한 정보를 담고 있습니다.

myInput.addEventListener('keydown', function(e) {
    console.log("키가 눌렸습니다. 눌린 키:", e.key); // 눌린 키 문자열
    console.log("키 코드:", e.keyCode); // 눌린 키의 숫자 코드 (구식)
    if (e.key === 'Enter') {
        alert("Enter 키를 누르셨군요!");
    }
});

// 링크 기본 동작 방지 예시
const preventLink = document.getElementById('myLink');
preventLink.addEventListener('click', function(e) {
    e.preventDefault(); // 링크의 기본 동작(페이지 이동)을 막습니다.
    console.log("링크 클릭! 하지만 페이지 이동은 막았습니다.");
});
  • e.target: 이벤트가 실제로 발생한 요소를 가리킵니다.
  • e.currentTarget: 현재 실행 중인 리스너가 등록된 요소를 가리킵니다. 이벤트 위임에서는 target과 currentTarget이 서로 다를 수 있습니다.
  • e.eventPhase: 현재 리스너가 캡처, 타깃, 버블 단계 중 어디에서 실행되는지 나타냅니다.
  • e.preventDefault(): 특정 이벤트의 기본 동작(예: 링크 클릭 시 페이지 이동, 폼 제출 시 새로고침)을 막습니다. 단, 취소 가능한 이벤트에서만 의미가 있고 passive: true 리스너에서는 사용할 수 없습니다.
  • e.stopPropagation(): 이벤트가 상위 요소로 전파되는 것을 중단합니다. 같은 요소의 뒤쪽 리스너까지 막아야 한다면 stopImmediatePropagation()을 사용합니다.

일반 DOM에서 버블링하는 click은 캡처 → 타깃 → 버블의 경로로 전달됩니다. 모든 이벤트가 버블링하는 것은 아닙니다. 기본 동작은 이 전파의 고정된 네 번째 단계가 아니며 이벤트·요소별 규칙을 따릅니다. preventDefault()는 취소 가능한 기본 동작을 막는 제어이고, 이벤트 전파를 멈추는 제어와 구분해야 합니다.

대부분의 UI 코드는 버블 단계 리스너를 쓰지만, 바깥 클릭 감지나 문서 전체 감시처럼 먼저 확인해야 하는 작업은 { capture: true }를 선택할 수 있습니다.

이벤트 위임은 부모 요소의 리스너에서 실제 처리할 자식 대상을 찾는 방식입니다. e.target이 Element인지 확인한 뒤 closest('[data-action]')로 버튼 등을 찾을 수 있습니다. 일치하는 조상이 없으면 null입니다.

찾은 요소가 있고 e.currentTarget.contains(찾은요소)도 참일 때 처리하면 위임 영역 바깥의 조상을 잘못 선택하지 않게 됩니다. 그림에서 span은 클릭 시작점, ul은 리스너 등록점입니다. 버튼 안의 글자를 클릭해도 같은 버튼 동작으로 연결되는 이유를 화살표로 확인하세요.

이벤트 리스너 제거하기

removeEventListener() 메서드를 사용하여 등록했던 이벤트 리스너를 제거할 수 있습니다.

이는 특히 메모리 누수를 방지하거나, 특정 조건에서만 이벤트를 처리하고 싶을 때 유용합니다.

addEventListener()에 전달했던 것과 동일한 함수 참조를 사용해야 하며, 캡처 단계 리스너라면 capture 값도 맞춰야 합니다.

const onceButton = document.getElementById('clickMeBtn');

function clickHandler() {
    alert("한 번만 실행됩니다!");
    onceButton.removeEventListener('click', clickHandler); // 함수 실행 후 리스너 제거
}

onceButton.addEventListener('click', clickHandler);

한 번만 실행하면 되는 리스너는 직접 제거하는 대신 { once: true } 옵션을 사용할 수 있습니다.

onceButton.addEventListener('click', () => {
    alert("한 번만 실행됩니다!");
}, { once: true });

여러 리스너를 한꺼번에 해제해야 한다면 AbortController의 signal을 공유하는 방식도 사용할 수 있습니다. 아래 handleResize는 먼저 정의해야 하는 함수 이름입니다. 예를 들어 앞에 다음 정의를 둡니다.

function handleResize() {
    console.log(window.innerWidth);
}
const controller = new AbortController();

window.addEventListener('resize', handleResize, {
    signal: controller.signal,
});

controller.abort(); // signal을 공유한 리스너 해제

위 예제는 등록 직후 controller.abort()를 실행하므로 그 뒤 창 크기를 바꾸어도 이 리스너는 실행되지 않습니다. 동작을 확인하려면 등록까지 먼저 실행하고 창 크기를 바꾼 뒤 마지막 해제 줄을 실행합니다. once도 그 리스너 하나의 수명만 정하며 같은 버튼의 다른 리스너까지 지우지는 않습니다.

자주 사용되는 이벤트 타입

몇 가지 일반적인 이벤트 타입을 정리해드립니다.

  • 마우스 이벤트: click, dblclick, mousedown, mouseup, mouseover, mouseout, mousemove
  • 키보드 이벤트: 새 코드에서는 keydown, keyup 중심으로 처리합니다.
  • 폼 이벤트: submit, focus, blur, change, input
  • 한글/IME 입력 이벤트: compositionstart, compositionupdate, compositionend
  • 문서/창 이벤트: load (페이지 로드 완료), DOMContentLoaded (DOM 트리 구성 완료), resize, scroll

DOM 조작과 이벤트 처리는 선택, 변경, 감지, 정리의 순서로 생각하면 안정적입니다.

사용자 입력이 화면 변경으로 이어질 때는 선택, 이벤트, 상태 읽기, 최소 DOM 갱신 순서를 함께 확인합니다.

작은 화면 동작을 완성해 보기

다음 전체 파일을 counter.html로 저장해 브라우저에서 여세요. 외부 이미지·라이브러리가 필요하지 않습니다. 버튼을 두 번 클릭하면 “2회”, 초기화하면 “0회”가 보입니다. 키보드로 버튼에 초점을 옮긴 뒤 Enter 또는 Space로도 확인해 보세요. 페이지를 다시 열면 이 예제의 상태는 0부터 시작합니다.

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>클릭 횟수</title>
</head>
<body>
  <p id="countResult" aria-live="polite">0회</p>
  <button id="increaseCount" type="button">횟수 늘리기</button>
  <button id="resetCount" type="button">초기화</button>
  <script>
    const result = document.getElementById("countResult");
    const increase = document.getElementById("increaseCount");
    const reset = document.getElementById("resetCount");
    let count = 0;
    function render() {
      result.textContent = `${count}회`;
    }
    increase.addEventListener("click", () => {
      count += 1;
      render();
    });
    reset.addEventListener("click", () => {
      count = 0;
      render();
    });
  </script>
</body>
</html>

스크립트가 두 버튼 뒤에 있으므로 조회 시 이미 존재합니다. 이벤트를 한 번 받을 때마다 변수 count를 바꾸고 render()로 해당 텍스트만 갱신합니다. render만 호출하면 횟수가 증가하지 않고, 변수만 바꾸면 화면 텍스트가 자동으로 바뀌지는 않습니다.

직접 풀어보기

위 파일을 바꾸어 최대 3회까지만 증가하게 하세요. 3회에서 증가 버튼을 비활성화하고 초기화 후에는 다시 사용할 수 있어야 합니다. 어떤 함수에 버튼 상태 갱신을 두면 두 리스너가 같은 규칙을 사용할 수 있을까요?

상태와 화면을 함께 갱신하는 풀이

render() 안에 increase.disabled = count >= 3;를 추가합니다. 초기값을 화면과 맞추려면 리스너 등록 뒤 render()도 한 번 호출합니다. 3회가 되면 버튼이 비활성화되어 사용자 클릭으로 증가하지 않으며, 초기화 리스너는 0으로 바꾼 뒤 같은 함수를 호출하여 다시 활성화합니다. 이 연습은 실제 사용자 버튼 조작을 대상으로 하며 임의의 dispatchEvent() 호출까지 막는 일반 입력 검증을 구현한 것은 아닙니다.

목록 규칙 확인: querySelectorAll의 정적 결과.


반복 실전 점검

이번 장에서는 자바스크립트로 웹 페이지를 동적으로 변경하는 DOM 조작과 이벤트 처리를 다뤘습니다.

DOM 조작에서는 요소 선택, 내용 변경(textContent, innerHTML), 속성 변경(setAttribute, classList), 스타일 변경(style)을 확인했습니다.

새 요소를 생성하고 추가하거나 제거하는 흐름도 함께 다뤘습니다.

이벤트 처리를 통해 사용자의 상호작용(클릭, 입력 등)에 반응하여 특정 자바스크립트 코드를 실행하는 방법을 배웠습니다.

addEventListener()를 사용하여 이벤트 리스너를 등록하고, event 객체를 통해 이벤트에 대한 상세 정보를 얻는 방법을 이해했습니다.

DOM 조작과 이벤트 처리는 정적인 HTML을 사용자 입력에 반응하는 화면으로 바꾸는 기본 도구입니다.

예제를 직접 바꿔 보며 어떤 이벤트가 어떤 요소와 상태를 바꾸는지 확인해 보세요.