모르는 메서드들을 공부하기에 앞서, 투두리스트를 만들 때 어떤 식으로 구조를 구성했는지 확인해보자.
main에서 storage의 아이템들을 관리하고, index.html에서 화면을 출력하도록 한다. 그리고, App에서는 main으로부터 받아온 화면에 그려질 요소와 storage의 초기 state를 가져온다. 그리고 App이 Header, TodoForm, TodoList, TodoCount를 관리한다.
이때 컴포넌트들은 state, 즉 상태를 관리하게 되는데, 브라우저에서 모든 렌더링을 처리하고 있으므로 상태를 관리하는 것이 중요하다!
컴포넌트에 대한 공부는 개발자 황준일 님의(현재 데브코스의 멘토 님이시기도 하다!!😲) <Vanilla Javascript로 웹 컴포넌트 만들기>글을 참고하였다.
//src/App.js
const todoList = new TodoList({
$target,
initialState,
toggleTodo: (target) => {
const state = [...todoList.state];
const targetIdx = state.findIndex( (element) => element.id === Number(target));
try {
state[targetIdx].isCompleted = !state[targetIdx]?.isCompleted;
todoList.setState(state);
storage.setItem("todos", JSON.stringify(state));
todoCount.render();
} catch (e) {
alert("App의 TodoList에서 toggleTodo할 때가 문제입니다");
}
},
deleteTodo: (target) => {
const state = [...todoList.state];
const nextState = state.filter((element) => element.id !== Number(target)
);
try {
todoList.setState(nextState);
storage.setItem("todos", JSON.stringify(todoList.state));
todoCount.render();
} catch (e) {
alert("App에서 TodoList deleteTodo할 때가 문제입니다");
}
},
});
//src/TodoList.js
function TodoList({ $target, initialState, toggleTodo, deleteTodo }) {
if (!new.target) {
alert("Todolist컴포넌트를 new로 생성해주세요!");
return;
}
const $todoList = document.createElement("div");
$target.appendChild($todoList);
try {
this.state = initialState;
this.setState = (nextState) => {
this.state = nextState;
this.render();
};
} catch (e) {
alert("TodoList에서 state설정할 때가 문제입니다");
}
this.render = () => {
$todoList.innerHTML = `
<ul>
${this.state
.map(
(list) => `
<li style="text-decoration: ${
list.isCompleted ? "line-through" : "none"
}";><span id= ${list.id}>
${list.text}
</span>
<button id=${list.id}>Delete</button>
</li>
`
)
.join("")}
</ul>
`;
$todoList.querySelectorAll("span").forEach((span) => {
span.addEventListener("click", () => {
toggleTodo(span.id);
});
});
$todoList.querySelectorAll("button").forEach((btn) => {
btn.addEventListener("click", () => {
deleteTodo(btn.id);
});
});
};
this.render();
}
코드 설명
App에서 TodoList컴포넌트의 상태를 관리하게 된다. $target, initialState를 넘겨주고 Todo를 클릭하면 발생하는 이벤트 함수인 toggleTodo와 삭제 버튼을 누르면 발생하는 이벤트 함수인 deleteTodo도 관리한다. 각 이벤트 함수들은 실행되면 상태를 불러오고, 새로운 상태를 storage에 업데이트해준다. 그리고 이때 todoCount를 render해주는데...
지금 보니까 왜 render로 상태를 업데이트 해줬지? 싶다. state는 setState로 해주는 게 국룰이라고 했는데... 추후 수정할 예정이다.
TodoList.js는 개인적으로 코드 짜기가 가장 난해했던 부분이다.
이벤트 버블링
이벤트 버블링은 특정 화면 요소에서 이벤트가 발생했을 때 해당 이벤트가 더 상위의 화면 요소들로 전달되어 가는 특성을 의미한다.
아래 코드는 모든 span태그 각각에 click이벤트를 등록하고, 클릭했을 때 toggleTodo함수를 실행시킨다.const $todoList = document.createElement("div"); //... $todoList.querySelectorAll("span").forEach((span) => { span.addEventListener("click", () => { toggleTodo(span.id); }); }); $todoList.querySelectorAll("button").forEach((btn) => { btn.addEventListener("click", () => { deleteTodo(btn.id); }); });
이벤트 위임 Event Delegation
이벤트 위임을 한 문장으로 요약해보면 ‘하위 요소에 각각 이벤트를 붙이지 않고 상위 요소에서 하위 요소의 이벤트들을 제어하는 방식’이다. 위 코드는 todoList의 모든 아이템에 이벤트 리스너를 일일이 달아주고 있다! 만약 이벤트 위임을 사용한다면 편해짐. 새로운 요소가 추가될 때마다 일일이 이벤트를 생성해주지 않아도 span의 상위 요소인 div태그에 이벤트 리스너를 달아 하위에서 발생한 클릭 이벤트를 li 단에서 검사하고 감지한다. 이벤트 버블링으로 관리하기 굿!$todoList.addEventListener("click", (e) => { const $li = e.target.closest(".todo-item"); if ($li) { const { id } = $li.dataset; //실제 이벤트를 발생시킨 곳은 어딘지 찾는 법 const { className } = e.target; if (className === "remove") { onRemove(id); } else { onToggle(id); } } });
document.querySelectorAll()
: querySelectorAll() 는 지정된 셀렉터 그룹에 일치하는 다큐먼트의 엘리먼트 리스트를 나타내는 정적(살아 있지 않은) NodeList를 반환한다.
// src/App.js
const todoCount = new TodoCount({
$target,
completeTodoCount: () => {
const completeTodo = storage
.getItem("todos", [])
.filter((element) => element.isCompleted);
return completeTodo.length;
},
todoListCount: () => {
return storage.getItem("todos", []).length;
},
});
}
// src/TodoCount.js
function TodoCount({ $target, completeTodoCount, todoListCount }) {
if (!new.target) {
alert("TodoCount컴포넌트를 new로 생성해주세요!");
return;
}
const $todoCount = document.createElement("span");
$target.appendChild($todoCount);
this.setState = () => {
this.render();
};
this.render = () => {
$todoCount.textContent=`${completeTodoCount()}/${todoListCount()}`;
};
this.render();
}
코드 설명
TodoCount는 완료한 할 일 목록과 할 일 목록의 개수를 반환하는 컴포넌트로, TodoList컴포넌트와 독립적으로 작동해야 했기 때문에 App에서 직접 storage에 접근하고자 하였다. 그런데 그냥 App에서 넘겨주는 initialState를 사용하는 게 코드의 일관성 측면에서 더 좋을 것 같다는 생각이 든다...
여하튼, completedTodoCount함수는 완료된 할 일 목록 개수를 storage에서 filter로 갖고 오고 있고, todoListCount함수는 storage의 todos 아이템의 길이를 반환한다. 저 코드를 작성할 당시에는 filter함수가 사용된 completedTodoCount가 return으로 바로 들어가게 되면 넘 길어져서 가독성 나쁠까봐 쟤만 변수로 반환했는데, 팀원분이 일관성을 위해 차라리 한꺼번에 리턴하라고 하셨다. 그게 맞는 말 같음...
흠, setState를 render함수를 호출하는 용도로 쓴 게 신경쓰인다. "돌아가기만 해라..." 생각했던 내 염원이 그대로 느껴지는군...