할 일 관리 앱
DOM 과 배열, 저장소를 모아 하나의 앱을 만듭니다.
무엇을 만드나
할 일을 적어 넣고, 마친 것을 표시하고, 지우는 화면을 만듭니다. 새로 고쳐도 남습니다.
화면에 있을 것
- 입력 칸과 추가 단추
- 할 일 목록 — 항목마다 글자와 지우기 단추
- 남은 개수 표시
지켜야 할 규칙
- 빈칸만 넣으면 더하지 않습니다.
- 글자를 누르면 마친 표시가 켜지고 꺼집니다.
- 지우기를 누르면 그 항목만 사라집니다.
- 바뀔 때마다 저장하고, 열 때 불러옵니다.
4부(DOM · 이벤트 · 위임) · 3부(배열) · 7.4(저장소)를 함께 사용합니다.
1단계 — 데이터를 먼저 정합니다
화면부터 만들면 나중에 헝클어집니다. 무엇을 담을지부터 정하고, 화면은 그것을 그리는 일로 둡니다.
// 할 일 하나는 이렇게 생겼습니다 let todos = [ { id: 1, text: "연필 사기", done: false }, { id: 2, text: "책 읽기", done: true } ]; console.log(todos.length); console.log(todos.filter(todo => !todo.done).length); console.log(todos[0].text);
id 를 두는 까닭이 있습니다. 글자가 같은 할 일이 둘일
수 있어, 지울 때 무엇을 지울지 가릴 것이 필요합니다.
2단계 — 데이터를 화면으로
그리는 일을 함수 하나에 모읍니다. 데이터가 바뀔 때마다 이 함수만 다시 부르면 됩니다(4.6).
const list = document.querySelector("#list"); const count = document.querySelector("#count"); let todos = [ { id: 1, text: "연필 사기", done: false }, { id: 2, text: "책 읽기", done: true } ]; function render() { list.replaceChildren(); todos.forEach(todo => { const li = document.createElement("li"); li.dataset.id = todo.id; if (todo.done) li.classList.add("done"); const text = document.createElement("span"); text.className = "text"; text.textContent = todo.text; const del = document.createElement("button"); del.textContent = "지우기"; li.append(text, del); list.append(li); }); count.textContent = `남은 일 ${todos.filter(t => !t.done).length}개`; } render(); console.log(list.children.length); console.log(count.textContent);
li.dataset.id 에 번호를 붙여 두었습니다. 나중에
누른 항목이 어느 데이터인지 찾을 때 씁니다(4.3).
3단계 — 더하기
const input = document.querySelector("#todo"); const add = document.querySelector("#add"); const list = document.querySelector("#list"); let todos = []; let nextId = 1; function render() { list.replaceChildren(); todos.forEach(todo => { const li = document.createElement("li"); li.textContent = todo.text; list.append(li); }); } add.addEventListener("click", () => { const text = input.value.trim(); if (text === "") return; todos.push({ id: nextId++, text: text, done: false }); input.value = ""; render(); }); // 코드로 두 번 눌러 봅니다 input.value = "연필 사기"; add.click(); input.value = " "; add.click(); console.log(todos.length); console.log(list.children.length);
빈칸만 넣은 두 번째는 더해지지 않았습니다. trim 뒤에
빈 글자면 먼저 끝냅니다(7.3 의 조기 반환).
4단계 — 토글과 지우기
항목은 계속 늘어나므로 목록 하나에만 이벤트를 붙입니다(4.7 의 위임).
const list = document.querySelector("#list"); let todos = [ { id: 1, text: "연필 사기", done: false }, { id: 2, text: "책 읽기", done: false } ]; function render() { list.replaceChildren(); todos.forEach(todo => { const li = document.createElement("li"); li.dataset.id = todo.id; if (todo.done) li.classList.add("done"); const text = document.createElement("span"); text.className = "text"; text.textContent = todo.text; const del = document.createElement("button"); del.textContent = "지우기"; li.append(text, del); list.append(li); }); } list.addEventListener("click", event => { const li = event.target.closest("li"); if (!li) return; const id = Number(li.dataset.id); if (event.target.closest("button")) { todos = todos.filter(todo => todo.id !== id); } else if (event.target.closest(".text")) { const found = todos.find(todo => todo.id === id); found.done = !found.done; } render(); }); render(); // 첫 항목의 글자를 누르고, 둘째 항목을 지웁니다 list.querySelector(".text").click(); list.querySelectorAll("button")[1].click(); console.log(todos.length); console.log(todos[0].done);
데이터를 고치고 다시 그립니다. 화면을 직접 고치지 않습니다 — 데이터와 화면이 어긋날 일이 없어집니다.
dataset.id 는 글자이므로
Number 로 바꿔 비교합니다(4.3).
5단계 — 저장하고 불러오기
const KEY = "todos"; function save(todos) { localStorage.setItem(KEY, JSON.stringify(todos)); } function load() { const raw = localStorage.getItem(KEY); if (raw === null) return []; try { return JSON.parse(raw); } catch { return []; } } console.log(load()); save([{ id: 1, text: "연필 사기", done: false }]); console.log(load().length); console.log(load()[0].text);
처음에는 아무것도 없으므로 빈 배열입니다. 망가진 값이 들어 있어도
try...catch 로 빈 배열이 됩니다(7.4).
이 화면에서는 다시 실행하면 비어 있습니다. 실제 페이지에서는 새로 고쳐도 남습니다.
모두 합치기
실행한 뒤 미리보기 탭에서 직접 더하고, 누르고, 지워 보세요.
const KEY = "todos"; const input = document.querySelector("#todo"); const add = document.querySelector("#add"); const list = document.querySelector("#list"); const count = document.querySelector("#count"); function load() { const raw = localStorage.getItem(KEY); if (raw === null) return []; try { return JSON.parse(raw); } catch { return []; } } let todos = load(); let nextId = todos.reduce((max, t) => Math.max(max, t.id), 0) + 1; function save() { localStorage.setItem(KEY, JSON.stringify(todos)); } function render() { list.replaceChildren(); todos.forEach(todo => { const li = document.createElement("li"); li.dataset.id = todo.id; if (todo.done) li.classList.add("done"); const text = document.createElement("span"); text.className = "text"; text.textContent = todo.text; const del = document.createElement("button"); del.textContent = "지우기"; li.append(text, del); list.append(li); }); count.textContent = `남은 일 ${todos.filter(t => !t.done).length}개`; } function update() { save(); render(); } add.addEventListener("click", () => { const text = input.value.trim(); if (text === "") return; todos.push({ id: nextId++, text: text, done: false }); input.value = ""; update(); }); list.addEventListener("click", event => { const li = event.target.closest("li"); if (!li) return; const id = Number(li.dataset.id); if (event.target.closest("button")) { todos = todos.filter(todo => todo.id !== id); } else if (event.target.closest(".text")) { const found = todos.find(todo => todo.id === id); found.done = !found.done; } update(); }); render(); // 확인용으로 하나 넣어 둡니다 input.value = "연필 사기"; add.click(); console.log(todos.length); console.log(count.textContent);
update 하나가 저장과 그리기를 함께 합니다.
두 곳에서 따로 부르면 한쪽을 빠뜨립니다(7.3).
nextId 는 저장된 것 가운데 가장 큰 번호의 다음
값입니다. 1 부터 다시 세면 이미 있는 항목과 번호가
부딪힙니다.
더 해 볼 것
입력 칸에서 엔터를 눌러도 더해지게 하세요.
keydown 을 걸고 event.key === "Enter" 일 때 추가 단추와 같은 일을 합니다(4.5). 더하는 코드를 함수로 빼 두면 두 곳에서 부를 수 있습니다.전체 · 남은 것 · 마친 것을 고르는 단추 셋을 두고, 고른 것만 보이게 하세요.
render 안에서 그리기 전에 filter 를 겁니다(3.3). 데이터는 그대로 두고 그리는 것만 거릅니다.할 일에 마감일을 붙이고, 오늘이 지난 것은 눈에 띄게 하세요.
"2026-09-07" 같은 글자로 담고, 견줄 때만 new Date() 로 바꿉니다. JSON 에는 날짜가 그대로 담기지 않습니다(3.8).이 프로젝트에서 배운 것
- 화면보다 데이터를 먼저 정합니다.
- 그리는 일을 함수 하나에 모으고, 바뀔 때마다 그것만 다시 부릅니다.
- 화면을 직접 고치지 않고 데이터를 고친 뒤 다시 그립니다.
- 늘어나는 항목은 부모에 이벤트를 한 번만 붙입니다.
- 저장과 그리기를 한 함수로 묶으면 한쪽을 빠뜨리지 않습니다.