본문으로 건너뛰기

더하기·끝내기·지우기

자바스크립트 목차할 일 앱 만들기레슨 74 / 76

목차

앞 레슨에서 todos 배열을 화면으로 그리는 render()를 만들었습니다. 이번 레슨에서는 새 할 일 더하기, 체크박스로 끝내기(완료 토글), × 단추로 지우기 세 가지 동작을 붙입니다. 세 동작 모두 눌린 것 바꾸고 다시 그리기 패턴(1. 무엇이 눌렸나 찾기 → 2. 배열 바꾸기 → 3. 다시 그리기) 하나로 통일됩니다.

앞 레슨에서 만든 app.js 펼치기
// app.js (13-1까지)
'use strict';

let todos = [
  {
    id: 1,
    text: '우유 사기',
    done: false,
  },
  {
    id: 2,
    text: '복습하기',
    done: true,
  },
];
todos.push({
  id: 3,
  text: '산책하기',
  done: false,
});
const listEl = document
  .querySelector(
    '#todo-list',
  );
const emptyEl = document
  .querySelector('#empty');
const countEl = document
  .querySelector('#count');
function makeRow(t) {
  const li = document
    .createElement('li');
  li.dataset.id =
    String(t.id);
  li.classList.toggle(
    'done',
    t.done,
  );
  const box = document
    .createElement('input');
  box.type = 'checkbox';
  box.checked = t.done;
  const span = document
    .createElement('span');
  span.className = 'text';
  span.textContent = t.text;
  const btn = document
    .createElement('button');
  btn.type = 'button';
  btn.className = 'delete';
  btn.textContent = '×';
  li.append(box, span, btn);
  return li;
}
function render() {
  listEl.textContent = '';
  for (const t of todos) {
    const r = makeRow(t);
    listEl.append(r);
  }
  emptyEl.hidden =
    todos.length > 0;
  const left = todos.filter(
    (t) => !t.done,
  ).length;
  countEl.textContent =
    `전체 ${todos.length}개` +
    ` 중 ${left}개 남음`;
}
render();

5단계 중 1단계. 첫 줄('우유 사기')을 누르면 closest('li')와 Number(li.dataset.id)로 눌린 줄의 번호 id(1)를 찾습니다.

1새 할 일 더하기: #todo-form의 submit

새로 만들 할 일에 붙일 번호 nextId를 4로 준비하고, 폼(#todo-form)이 제출될 때 todos 배열 끝에 새 객체를 넣은 뒤 render()를 부릅니다.

// app.js에 이어서 추가
let nextId = 4;
const formEl = document
  .querySelector('#todo-form');
const inputEl = document
  .querySelector(
    '#todo-input',
  );

이어서 submit 리스너를 등록합니다.

formEl.addEventListener(
  'submit',
  (event) => {
    event.preventDefault();
    const text =
      inputEl.value.trim();
    if (text === '') return;
    const item = {
      id: nextId++,
      text,
      done: false,
    };
    todos.push(item);
    inputEl.value = '';
    render();
  },
);
실행 결과
'장보기' 입력 후 Enter:
'장보기' 추가 · 전체 4개 중 3개 남음

2끝내기와 지우기: #todo-list 이벤트 위임

목록(#todo-list)에서 일어나는 클릭은 이벤트 위임으로 한 곳에서 받습니다.

  1. 1. 무엇이 눌렸나 찾기: event.target.closest('li')⁠로 줄을 찾고 Number(li.dataset.id)⁠로 숫자 id를 꺼냅니다.
  2. 2. 배열 바꾸기: 삭제 단추(.delete)가 눌렸다면 todos.filter로 t.id !== id인 항목만 남기고, 체크박스나 글자가 눌렸다면 그 항목의 done을 !item.done으로 뒤집습니다.
  3. 3. 다시 그리기: 마지막에 render()를 한 번 부릅니다.
listEl.addEventListener(
  'click',
  (event) => {
    // 1. 무엇이 눌렸나 찾기
    const li = event.target
      .closest('li');
    if (!li) return;
    const id = Number(
      li.dataset.id,
    );

이어서 삭제 단추인지 완료 토글인지에 따라 배열을 바꾸고 render()를 부릅니다.

    // 2. 배열 바꾸기
    const del = event.target
      .closest('.delete');
    if (del) {
      todos = todos.filter(
        (t) => t.id !== id,
      );
    } else {
      const item = todos.find(
        (t) => t.id === id,
      );
      if (item) {
        item.done = !item.done;
      }
    }
    // 3. 다시 그리기
    render();
  },
);
실행 결과
- 첫째 줄 클릭 → 완료 + 1개 남음
- 둘째 줄 × 클릭 → 삭제됨

더하기, 끝내기, 지우기 세 코드 모두 화면의 <li>를 직접 고치거나 지우지 않습니다. 오직 todos 배열만 바꾼 뒤 render()를 부르면, 목록과 빈 안내와 남은 개수가 한 치의 어긋남도 없이 함께 맞춰집니다(규칙 2의 화면 판).

세 동작이 배열 todos를 어떻게 바꾸는지 Node에서 그대로 실행해 확인해 봅니다.

let todos = [
  { id: 1, done: false },
  { id: 2, done: true },
];
todos.push({
  id: 3,
  done: false,
});
const item = todos.find(
  (t) => t.id === 1,
);
item.done = !item.done;
todos = todos.filter(
  (t) => t.id !== 2,
);
console.log(
  todos.length,
  todos[0].done,
);
실행 결과
2 true

자주 틀리는 곳

  1. 화면에서 li.remove()만 부르고 todos 배열은 안 바꾼다: 화면에서만 <li>를 지우면 todos.length는 그대로여서 남은 개수(#count)가 틀려지고, 다음 render() 때 지웠던 줄이 다시 살아납니다. 언제나 **배열을 먼저 바꾸고 render()**를 부릅니다.
  2. const id = li.dataset.id;⁠처럼 Number()를 빼먹는다: li.dataset.id는 문자열이므로 t.id === id와 t.id !== id 비교가 모두 어긋나 완료 토글도 삭제도 되지 않습니다.
  3. let todos 대신 const todos로 선언해 두고 todos.filter(...) 결과를 다시 대입한다: filter는 새 배열을 만들어 돌려주므로 todos 변수에 다시 대입하려면 let todos로 선언되어 있어야 합니다(const이면 TypeError가 납니다).

확인 문제

  1. 눌린 것 바꾸고 다시 그리기 패턴의 세 단계를 순서대로 쓰세요.
정답

1. 무엇이 눌렸나 찾기 → 2. 배열(todos) 바꾸기 → **3. 다시 그리기(render())**입니다.

  1. item.done이 false일 때 item.done = !item.done;⁠을 한 번 실행하면 item.done은 무엇이 되며, 한 번 더 실행하면 무엇이 되나요?
정답

첫 번째 실행 뒤에는 true가 되고, 한 번 더 실행하면 다시 false로 뒤집힙니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

자바스크립트 목차로