앞 레슨에서 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();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. 무엇이 눌렸나 찾기:
event.target.closest('li')로 줄을 찾고Number(li.dataset.id)로 숫자id를 꺼냅니다. - 2. 배열 바꾸기: 삭제 단추(
.delete)가 눌렸다면todos.filter로t.id !== id인 항목만 남기고, 체크박스나 글자가 눌렸다면 그 항목의done을!item.done으로 뒤집습니다. - 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자주 틀리는 곳
- 화면에서
li.remove()만 부르고todos배열은 안 바꾼다: 화면에서만<li>를 지우면todos.length는 그대로여서 남은 개수(#count)가 틀려지고, 다음render()때 지웠던 줄이 다시 살아납니다. 언제나 **배열을 먼저 바꾸고render()**를 부릅니다. const id = li.dataset.id;처럼Number()를 빼먹는다:li.dataset.id는 문자열이므로t.id === id와t.id !== id비교가 모두 어긋나 완료 토글도 삭제도 되지 않습니다.let todos대신const todos로 선언해 두고todos.filter(...)결과를 다시 대입한다:filter는 새 배열을 만들어 돌려주므로todos변수에 다시 대입하려면let todos로 선언되어 있어야 합니다(const이면TypeError가 납니다).
확인 문제
눌린 것 바꾸고 다시 그리기패턴의 세 단계를 순서대로 쓰세요.
정답
1. 무엇이 눌렸나 찾기 → 2. 배열(todos) 바꾸기 → **3. 다시 그리기(render())**입니다.
item.done이false일 때item.done = !item.done;을 한 번 실행하면item.done은 무엇이 되며, 한 번 더 실행하면 무엇이 되나요?
정답
첫 번째 실행 뒤에는 true가 되고, 한 번 더 실행하면 다시 false로 뒤집힙니다.