Skip to Content
WebJavaScriptWeb APIs05. 이벤트 위임으로 추가·삭제·편집

이번 편의 결과물: 04편에서 만든 정적 카드 목록에 추가·삭제·인라인 편집 기능을 붙입니다. 새로고침하면 사라지는 임시 상태입니다. · 다루는 개념: addEventListener 위임, event.target/closest, 폼 없는 인라인 추가, 상태 배열 갱신

이 편에서 만드는 파일

notes-app/ ├── index.html (~ 인라인 추가 영역 추가) └── src/ ├── notes.js (~ addNote/removeNote/updateNote 추가) ├── render.js (~ 편집 모드 렌더링 추가) └── main.js (~ 이벤트 위임 리스너 추가)

개념 정리

이벤트 위임

카드마다 버튼에 리스너를 따로 달면, 메모가 추가·삭제될 때마다 리스너를 다시 붙이고 떼야 합니다. 이벤트 위임은 목록 컨테이너 하나에만 리스너를 달고, 실제 클릭된 요소를 event.target으로 찾아 처리하는 방식입니다. 목록이 몇 번을 다시 그려져도 리스너는 그대로입니다.

방식리스너 수동적 추가 대응
카드마다 리스너 등록메모 개수만큼추가될 때마다 새로 등록 필요
위임(컨테이너 하나)1개추가만 해도 자동으로 동작

event.target과 closest

클릭 이벤트의 event.target은 실제로 클릭된 가장 안쪽 요소입니다. 버튼 안의 아이콘을 클릭하면 target은 아이콘일 수 있습니다. closest(선택자)는 자신부터 조상 방향으로 올라가며 선택자와 일치하는 첫 요소를 찾습니다. 버튼을 특정하려면 event.target.closest('[data-action]')을, 카드를 특정하려면 actionEl.closest('.note-card')을 씁니다.

상태와 화면의 분리

notes.js는 메모 배열(상태)만 관리하고, render.js는 그 배열을 받아 화면(DOM)만 그립니다. main.js는 이벤트가 오면 상태를 바꾸고, 바뀐 상태로 다시 render()를 호출합니다. 이 흐름을 지키면 상태와 화면이 어긋나지 않습니다.

실습

1. src/notes.js 열기

04편에서 만든 초기 배열에 addNote, removeNote, updateNote 함수를 추가합니다.

2. 코드 작성

// src/notes.js let notes = [ { id: 'n1', title: 'Vite 프로젝트 만들기', body: 'npm create vite@latest notes-app -- --template vanilla 로 시작한다.', tags: ['setup'], pinned: true, updatedAt: '2026-09-15T09:00:00+09:00', }, { id: 'n2', title: '이벤트 위임 메모', body: '목록 전체에 리스너 하나를 달고 event.target.closest 로 버튼을 찾는다.', tags: ['dom', 'events'], pinned: false, updatedAt: '2026-09-15T11:20:00+09:00', }, { id: 'n3', title: 'fetch 취소', body: 'AbortController 의 signal 을 fetch 옵션으로 넘기고 abort() 로 취소한다.', tags: ['fetch'], pinned: false, updatedAt: '2026-09-16T08:00:00+09:00', }, { id: 'n4', title: 'IndexedDB 는 트랜잭션 단위', body: 'objectStore 접근은 transaction 안에서만 가능하다.', tags: ['storage'], pinned: false, updatedAt: '2026-09-16T09:30:00+09:00', }, ]; function generateId() { return `n${Date.now()}`; } export function getNotes() { return notes; } export function addNote({ id, title, body, tags = [], pinned = false, updatedAt }) { const note = { id: id ?? generateId(), title, body, tags, pinned, updatedAt: updatedAt ?? new Date().toISOString(), }; notes = [note, ...notes]; return note; } export function removeNote(id) { notes = notes.filter((note) => note.id !== id); } export function updateNote(id, patch) { notes = notes.map((note) => note.id === id ? { ...note, ...patch, updatedAt: new Date().toISOString() } : note, ); }

addNoteidupdatedAt을 선택 인자로 받습니다. 지금은 항상 새로 만들지만, 08편에서 서버가 내려준 메모를 그대로 저장할 때 같은 함수를 재사용합니다.

src/render.js를 열어 카드 하나를 그리는 부분에 편집 모드를 추가합니다.

// src/render.js function renderDisplayCard(note) { return ` <h2>${note.title}</h2> <p>${note.body}</p> <div class="note-actions"> <button type="button" data-action="edit">편집</button> <button type="button" data-action="delete">삭제</button> </div> `; } function renderEditCard(note) { return ` <input class="edit-title" name="title" type="text" value="${note.title}" /> <input class="edit-body" name="body" type="text" value="${note.body}" /> <div class="note-actions"> <button type="button" data-action="save">저장</button> <button type="button" data-action="cancel">취소</button> </div> `; } export function renderList(notes, container, { editingId = null } = {}) { container.innerHTML = ''; for (const note of notes) { const li = document.createElement('li'); li.className = 'note-card'; li.dataset.id = note.id; li.innerHTML = note.id === editingId ? renderEditCard(note) : renderDisplayCard(note); container.appendChild(li); } }

src/main.js에 인라인 추가 입력과 위임 리스너를 작성합니다.

// src/main.js import { getNotes, addNote, removeNote, updateNote } from './notes.js'; import { renderList } from './render.js'; const listEl = document.querySelector('#note-list'); const titleInput = document.querySelector('#new-title'); const bodyInput = document.querySelector('#new-body'); const addBtn = document.querySelector('#add-btn'); let editingId = null; function render() { renderList(getNotes(), listEl, { editingId }); } addBtn.addEventListener('click', () => { const title = titleInput.value.trim(); if (!title) return; addNote({ title, body: bodyInput.value.trim() }); titleInput.value = ''; bodyInput.value = ''; render(); }); listEl.addEventListener('click', (event) => { const actionEl = event.target.closest('[data-action]'); if (!actionEl) return; const card = actionEl.closest('.note-card'); const id = card.dataset.id; const action = actionEl.dataset.action; if (action === 'delete') { removeNote(id); render(); } else if (action === 'edit') { editingId = id; render(); } else if (action === 'cancel') { editingId = null; render(); } else if (action === 'save') { const title = card.querySelector('.edit-title').value.trim(); const body = card.querySelector('.edit-body').value.trim(); updateNote(id, { title, body }); editingId = null; render(); } }); render();

index.html<main> 안, 목록 위에 인라인 추가 영역을 추가합니다.

<!-- index.html --> <div class="note-composer"> <input id="new-title" type="text" placeholder="제목" /> <input id="new-body" type="text" placeholder="내용" /> <button id="add-btn" type="button">추가</button> </div>

3. 실행

npm run dev

확인

  • 제목과 내용을 입력하고 추가 버튼을 누르면 목록 맨 위에 새 카드가 나타납니다.
  • 카드의 삭제를 누르면 그 카드만 목록에서 사라집니다.
  • 카드의 편집을 누르면 제목·내용이 입력 칸으로 바뀌고, 저장을 누르면 바뀐 값이 반영됩니다. 취소를 누르면 원래 표시로 돌아갑니다.
  • 브라우저를 새로고침하면 04편의 초기 데이터로 되돌아갑니다(아직 저장 기능이 없기 때문입니다. 07편에서 다룹니다).

직접 해보기

  1. 카드에 고정 버튼(data-action="pin")을 추가해 누르면 updateNote(id, { pinned: !note.pinned })를 호출하도록 만들어 봅니다.
  2. addNote 호출 전에 제목이 이미 존재하는 메모와 같으면 추가하지 않도록 막아 봅니다.

정답 보기

// listEl 리스너의 else if 체인에 추가 } else if (action === 'pin') { const note = getNotes().find((n) => n.id === id); updateNote(id, { pinned: !note.pinned }); render(); }
// addBtn 리스너 안, addNote 호출 전 const exists = getNotes().some((note) => note.title === title); if (exists) { window.alert('같은 제목의 메모가 이미 있습니다.'); return; }

자주 하는 실수

증상원인고치는 법
버튼을 눌러도 아무 반응이 없음리스너를 #note-list가 아니라 document나 개별 버튼에 달아 위임 대상이 잘못됨항상 카드를 감싸는 고정 컨테이너(listEl)에 리스너를 단다
card.dataset.idundefinedclosest('.note-card')를 호출하기 전에 event.target을 바로 사용actionEl.closest('.note-card')로 카드 요소를 먼저 찾는다
편집 중 다른 카드를 눌러도 편집 모드가 풀리지 않음editingId를 갱신하지 않고 클래스만 토글editingId 상태를 바꾸고 render()로 전체를 다시 그린다

확인 문제

문제 14지선다
이벤트 위임을 쓰는 가장 큰 이유는?
문제 24지선다
closest 메서드의 동작으로 옳은 것은?
문제 34지선다
notes.js와 render.js를 나누어 설계한 이유로 가장 적절한 것은?
문제 44지선다
편집 중인 카드의 id를 저장하는 editingId 변수가 main.js에 있는 이유는?

참고 자료

Last updated on