작성하게 된 계기
DOM과 이벤트를 다루는 방법을 손에 익게 하기 위해 코드를 여러번 써봤는데 다음에 복습하기 위해서 정리해두면 좋을 것 같아서 별도로 정리해둔다.
DOM이 완전히 생성됬을 때 todos데이터를 서버에서 받아왔다고 가정하자.
// State =======================================
let todos = [];
// State Function ==============================
const fetchTodos = () => {
todos = [
{ id: 1, content: 'Javascript', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'HTML', completed: false }
];
};
window.addEventListener('DOMContentLoaded', fetchTodos);
데이터를 받아왔을 때 ul안에 기본적인 list가 나오도록 하기 위해서 render함수를 만든다.
HTML
section .todo-app
header .header
section .main
input type="checkbox" id="toggle-all" class="toggle-all"
label for="toggle-all"
ul .todo-list
<!-- 이곳에 list -->
footer .footer
footer .info
Javascript
// state =======================================
let todos = [];
// DOM Nodes ===================================
const $todoList = document.querySelector('.todo-list');
// State Function ==============================
const render = () => {
$todoList.innerHTML = todos
.map(
({ id, content, completed }) =>
`<li data-id="${id}">
<div class="view">
<input type="checkbox" class="toggle" ${completed ? 'checked' : ''}/>
<label>${content}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${content}" />
</li>`).join('');
};
const fetchTodos = () => {
todos = [
{ id: 1, content: 'Javascript', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'HTML', completed: false }
];
render();
};
window.addEventListener('DOMContentLoaded', fetchTodos);
전체토글버튼을 눌렀을 때 todos객체의 completed를 전부 true나 false로 만든다.
label을 누르는 거지만 실제로는 input창에 체크되는거다. 이 input창에 ckecked 프로퍼티를 이용해서 true, false를 알아내고 true면 todos의 completed 키를 전부 true로 만드는 식으로 한다.
일단 구현해놓고 그 다음에 나눠주자.
// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
...
// State Function
...
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$toggleAll.onchange = () => {
todos = todos.map(todo => ({ ...todo, completed: $toggleAll.checked }));
render();
};
State Function 부분과 Event Binding으로 나누어 준다.
// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
...
// State Function
...
const toggleAllTodosCompleted(completed => {
// todos = todos.map(todo => ({...todo, completed: completed}));
todos = todos.map(todo => ({...todo, completed}));
render();
})
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$toggleAll.onchange = () => {
toggleAllTodosCompleted($toggleAll.checked);
};
그런데 계속 todos에 할당하는 부분과 render함수를 호출해주는 부분이 반복된다. 따로 함수로 빼주자. 그 다음에 정리하면 한결 깨끗해졌다.
// State =======================================
let todos = [];
// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
const $todoList = document.querySelector('.todo-list');
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
todos = newTodo;
render();
};
const fetchTodos = () => {
setTodos([
{ id: 1, content: 'Javascript', completed: false },
{ id: 2, content: 'CSS', completed: true },
{ id: 3, content: 'HTML', completed: false }
]);
};
const toggleAllTodosCompleted = completed => {
setTodos(todos.map(todo => ({ ...todo, completed })));
};
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$toggleAll.onchange = () => {
toggleAllTodosCompleted($toggleAll.checked);
};
input에 추가한 값을 value 프로퍼티로 받아와서 todos에 추가한다.
// State =======================================
let todos = [];
// DOM Nodes ===================================
const $newTodo = document.querySelector('.new-todo');
...
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => Math.max(...todos.map(todo => todo.id)) + 1
const addTodos = content => {
setTodos([{id: generateTodoId(), content, completed: false}, ...todos])
}
const toggleAllTodosCompleted = completed => {
...
};
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
if (e.key !== 'Enter') return;
addTodos(e.target.value)
}
$toggleAll.onchange = () => {
...
};
이렇게 하면 content가 비어있을 때 빈 공백으로 todo가 추가되는 문제가 있고 추가하고 나서 그대로 input창에 value가 그대로 남아있는 문제가 있다. 수정해준다.
$newTodo.onkeyup = e => {
if (e.key !== 'Enter') return;
const content = e.target.value;
if(content) addTodos(content)
e.target.value = '';
}
hidden 클래스를 추가해주면 display: none
처리가 되있다. todo 리스트 개수가 0개일 때 hidden 클래스를 추가해주고 0개가 아닐 때는 hidden클래스를 제거하는 방법으로 toggle을 사용한다.
위에서 우선적으로 해주는게 좋은데 지나치는 바람에 이 타이밍에 작업했다.
// State =======================================
let todos = [];
// DOM Nodes ===================================
...
const $main = document.querySelector('.main');
const $footer = document.querySelector('.footer');
// State Function ==============================
const render = () => {
...
[$main, $footer].forEach($el => $el.classList.toggle('hidden', todos.length === 0));
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
// State =======================================
let todos = [];
// DOM Nodes ===================================
...
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
setTodos(todos.map(todo => todo.id === +id ? {...todo, completed: !todo.completed} : todo));
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
if(!e.target.classList.contains('.toggle')) return;
toggleTodosCompleted(e.target.closest('li').dataset.id);
}
matches가 아직 익숙하지 않아서 주의깊게 봐야한다.
두 가지 기능을 추가해야 한다. 더블클릭으로 편집으로 진입했을 때와 Enter를 눌렀을 때 입력했었던 값으로 리렌더링되도록 해야 한다.
// State =======================================
let todos = [];
// DOM Nodes ===================================
...
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
...
}
const updateTodoContent = (id, content) => {
setTodos(todo.map(todo => todo.id === +id ? {...todo, content} : todo));
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
...
}
$todoList.dblclick = e => {
if(!e.target.matches('.view > label')) return;
e.target.closest('li').classList.add('.editing');
}
$todoList.onkeyup = e => {
if(e.key !== 'Enter') return;
updateTodoContent(e.target.closest('li').dataset.id, e.target.value)
}
e.target.value에서 live객체와 live객체가 아닌 것의 차이를 볼 수 있었다.
만약 더블클릭해서 데이터를 Javascript
에서 123123
으로. 수정하고 엔터를 눌렀을 때 아래 콘솔을 보자.
$todoList.onkeyup = e => {
if(e.key !== 'Enter') return;
updateTodoContent(e.target.closest('li').dataset.id, e.target.value)
console.log(e.target.value); // 123123
console.log(e.target.getAttribute('value')); // Javascript
}
// State =======================================
let todos = [];
// DOM Nodes ===================================
...
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
...
}
const updateTodoContent = (id, content) => {
...
}
const removeTodo = id => {
setTodos(todos.filter(todo => todo.id !== +id))
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
...
}
$todoList.dblclick = e => {
...
}
$todoList.onkeyup = e => {
...
}
$todoList.onclick = e => {
if(!e.target.classList.contains('destroy')) return;
removeTodo(e.target.closest('li').dataset.id);
}
좌측 하단 아직 미완료한 todo 개수 카운팅
// State =======================================
let todos = [];
// DOM Nodes ===================================
...
const $todoCount = document.querySelctor('.todo-count')
// State Function ==============================
const render = () => {
...
const activeTodos = todos.filter(todo => !todo.completed).length;
$todoCount.textContent = `${activeTodos} ${activeTodos > 1? 'items' : 'item'} left`
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
...
}
const updateTodoContent = (id, content) => {
...
}
const removeTodo = id => {
...
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
...
}
$todoList.dblclick = e => {
...
}
$todoList.onkeyup = e => {
...
}
$todoList.onclick = e => {
...
}
// State =======================================
let todos = [];
let currentFilter = 'all';
// DOM Nodes ===================================
...
const $filters = document.querySelector('.filters');
// State Function ==============================
const render = () => {
const _todos = todos.filter(todo =>
currentFilter === 'completed'
? todo.completed
: currentFilter === 'active'
? !todo.completed
: true
);
todoList.innerHTML = _todos
.map(
({ id, content, completed }) =>
`<li data-id="${id}">
<div class="view">
<input type="checkbox" class="toggle" ${completed ? 'checked' : ''}/>
<label>${content}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${content}" />
</li>`).join('');
[$main, $footer].forEach($el => $el.classList.toggle('hidden', todos.length === 0));
const activeTodos = _todos.filter(todo => !todo.completed).length;
$todoCount.textContent = `${activeTodos} ${activeTodos > 1? 'items' : 'item'} left`
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const setFilter = newFilter => {
currentFilter = newFilter;
render();
}
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
...
}
const updateTodoContent = (id, content) => {
...
}
const removeTodo = id => {
...
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
...
}
$todoList.dblclick = e => {
...
}
$todoList.onkeyup = e => {
...
}
$todoList.onclick = e => {
...
}
$filters.onclick = e => {
if(!e.target.matches('filters')) return;
[...filters.querySelectorAll('a')].forEach($a => $a.classList.toggle('selected', $a.id === e.target.id))
setFilter(e.target.id);
}
// State =======================================
let todos = [];
let currentFilter = 'all';
// DOM Nodes ===================================
...
const $clearCompleted = document.querySelector('.clear-completed');
// State Function ==============================
const render = () => {
...
};
const setTodos = newTodo => {
...
};
const fetchTodos = () => {
...
};
const generateTodoId = () => ...
const setFilter = newFilter => {
...
}
const addTodos = content => {
...
}
const toggleAllTodosCompleted = completed => {
...
};
const toggleTodosCompleted = id => {
...
}
const updateTodoContent = (id, content) => {
...
}
const removeTodo = id => {
...
}
const removeAllCompletedTodos = () => {
setTodos(todos.filter(todo => !todo.completed))
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);
$newTodo.onkeyup = e => {
...
}
$toggleAll.onchange = () => {
...
};
$todoList.onchange = e => {
...
}
$todoList.dblclick = e => {
...
}
$todoList.onkeyup = e => {
...
}
$todoList.onclick = e => {
...
}
$filters.onclick = e => {
...
}
$clearCompleted.onclick = removeAllCompletedTodos
정리 하면서 깨달은 점
html상에서 순서대로 작업하면 JS코드를 순서신경쓰면서 다시 위치를 바꾸는 등 번거로워지는 작업이 줄어 들 수 있다.
한 개의 파일이 아니라 모듈을 이용해서 나누는 중요성을 알게 된 것 같다.
작업을 하면서 스크롤을 끝과 끝으로 이동을 하는데 가독성도 좋지 않고 불편했다.
그래도 여러번 쳐보면서 얻은 지식이 많았다. 단순히 볼 때 이해했다고 생각한 부분들이 실제로 쳐보면 전혀 그렇지 않다는 걸 알았다. 손으로 여러번 치다보니까 손이 기억하는 부분들도 있었는데 1~2주 시간을 두고 다시한번 쳐서 완전히 내 것으로 만드는 것이 중요할 것 같다.