
코드잇 스프린트 : 프론트엔드 2기를 수강하며 작성하였습니다.
- 멘토님이 개별적으로 주신 구현 과제를 풀어가는 제작기입니다.
- 완성한 프로젝트 링크 : https://jolly-alfajores-454595.netlify.app/
팀 프로젝트가 시작 하기 전 멘토님이 새로운 구현 과제를 내주셨다.
바로 TODO list
꽤 재밌는 프로젝트가 될 것 같다.


할일을 하나 적어보니, li 태그에 input, label, button이 생성되었다.

현재 있는 것은 index.html과 css, 그리고 조금의 assets 이미지들이다.
요구사항과 비교해보며, 어떤 태그에, 어떻게 줘야될 지 한번 생각해보자.
input과 ul <main>
<input class="toggle-all" type="checkbox" />
<ul id="todo-list" class="todo-list"></ul>
main 태그의 아래에는 input태그와, 리스트를 저장할 ul 태그가 있다.
todo list에 todoItem을 키보드로 입력하여 추가하기
키보드로 Input에 입력한 할일을 ul 태그에 li로 넣어주면 될 것 같다.
클래스는 데모사이트에서 확인한 대로 지정해주면 될 것 같다.
일단 이대로 하드코딩 해보자.
<ul id="todo-list" class="todo-list">
<li class="todo-item">
<input type="checkbox" class="toggle" false />
<label class="label">할일1</label>
<button class="destroy"></button>
</li>
</ul>
예쁘게 html 문서에 하드코딩 해봤다.

할일을 했는지 체크하는 체크박스와, 할일 목록, 그리고 삭제 버튼이 생겼다.
<li class="todo-item completed">
<input type="checkbox" class="toggle" checked />
<label class="label">할일1</label>
<button class="destroy"></button>
</li>
요구사항 대로 li에 completed 클래스를 추가하고, input에 checked 속성을 추가해봤다.

체크 박스가 표시되고, 할일에 밑줄이 쳐진다.
src 디렉토리를 만들고, index.js 파일을 생성했다.

우선 DOM 요소를 많이 다룰 것 같으므로, 선택자 $ 함수를 만들어야 할 것 같다.
const $ = (element) => document.querySelector(element);
const dummyCountArray = []; // 더미 배열
const handleSubmit = (e) => {
e.preventDefault();
// 새 li 생성
const newItem = createNewItem($('#new-todo-title').value, dummyCountArray.length);
dummyCountArray.push(newItem);
$('#new-todo-title').value = '';
checkListCount(); // 개수 체크
};
일단 새로고침을 막고,
새로운 list 아이템을 생성한다. 인자로는 input의 값과 배열의 길이를 줬다.
배열의 길이를 주는 이유는 후술.
그리고 input의 값을 초기화 해주고, 추가된 할일 목록을 계산한다.
const createNewItem = (value, index) => {
const li = document.createElement('li');
li.setAttribute('data-index', String(index));
const div = document.createElement('div');
div.classList.add('view');
const input = document.createElement('input');
input.classList.add('toggle');
input.setAttribute('type', 'checkbox');
const label = document.createElement('label');
label.classList.add('label');
label.textContent = value;
const button = document.createElement('button');
button.classList.add('destroy');
div.append(input, label, button);
li.append(div);
$('.todo-list').append(li);
return li;
};
템플릿 대로 요소를 생성하고, 필요한 클래스명과 값을 적용해준다. 그리고 리스트에 추가해준다.
배열의 길이를 받아온 이유는 li 태그에 data-index 속성을 주어 구별하기 위함이다.
const checkListCount = () => {
$('.todo-count > strong').textContent = $('.todo-list').childElementCount;
};

총 할일 개수를 확인해서 반영해주는 함수다.
ul 태그의 자식 요소 개수를 textContent로 넣어줬다.

여기까지는 잘 만들어졌다.
const handleCheckBoxClick = (e) => {
// 부모 li에 담긴 data-index 값 가져오기
const indexNum = e.target.parentElement.parentElement.dataset['index'];
if (!indexNum) return;
const clickedTarget = e.target;
switch (clickedTarget.className) {
case 'toggle':
// 해당 li 완료하기
$(`[data-index="${indexNum}"]`).classList.toggle('completed');
$(`[data-index="${indexNum}"] > div > input`).toggleAttribute('checked');
break;
case 'destroy':
// 해당 li 삭제하기
$(`[data-index="${indexNum}"]`).remove();
checkListCount();
break;
default:
break;
}
};
우선 클릭한 대상의 부모의 부모요소, 즉 li 태그의 data-index 값을 가져온다.
얼리 리턴으로 불필요한 동작을 막았다.
클릭한 요소를 클래스명으로 구분해 동작 하도록 했다.
체크박스를 클릭하면, 부모 li 태그에 'completed' 클래스를 추가한다.
또 input에 checked 속성을 추가해준다.
해당 li 태그를 할일 목록에서 지운다.
그리고 checkListCount로 개수를 갱신해준다.
const handleFilterClick = (e) => {
const clickedTarget = e.target;
if (!clickedTarget.tagName === 'LI') return;
const lists = $('.todo-list').children;
// 버튼에 따른 리스트 보이고 가리기
if (clickedTarget.classList.contains('all')) {
for (let i = 0; i < lists.length; i++) {
lists[i].style.display = 'block';
}
}
if (clickedTarget.classList.contains('active')) {
for (let i = 0; i < lists.length; i++) {
lists[i].classList.contains('completed')
? (lists[i].style.display = 'none')
: (lists[i].style.display = 'block');
}
}
if (clickedTarget.classList.contains('completed')) {
for (let i = 0; i < lists.length; i++) {
lists[i].classList.contains('completed')
? (lists[i].style.display = 'block')
: (lists[i].style.display = 'none');
}
}
};
마찬가지로, 얼리 리턴을 활용해 다른 버튼을 누르면 동작을 끝낸다.
그리고 필터 리스트의 클래스 명에 따라 동작을 분기한다.
lists 에는 할일 목록 li 태그를 담았다. 참고로 HTML Collection이라 유사배열이다.
lists를 순회하며 전부 display: block을 적용해 보이도록 했다.
lists 를 순회하며 '완료했다'는 의미의 completed 클래스가 있는지 확인하고, 있으면 display:none으로 가려준다. 완료하지 않은 목록들은 보여주도록 했다.
lists를 순회하며 '완료했다'는 의미의 completed 클래스가 있는지 확인하고, 있으면 display:block으로 표시해준다. 없으면 안보이게 가려주도록 했다.
const handleDoubleClick = (e) => {
const clickedTarget = e.target;
if (!clickedTarget.tagName === 'LABEL') return;
const indexNum = clickedTarget.parentElement.parentElement.dataset['index'];
$(`[data-index="${indexNum}"]`).classList.toggle('editing');
createInput(indexNum);
$(`[data-index="${indexNum}"] .edit`).addEventListener('keydown', handleKeyDown);
};
마찬가지로 얼리 리턴을 활용해 불필요한 동작을 막았다.
더블클릭한 위치의 부모 li 태그의 data-index 값을 가져왔고, 이를 활용해 input요소를 생성해줬다.
생성된 input 요소에는 키보드 이벤트리스너를 달아줬다.
const createInput = (indexNum) => {
const input = document.createElement('input');
input.classList.add('edit');
input.value = $(`[data-index="${indexNum}"] > div > label`).textContent;
$(`[data-index="${indexNum}"]`).append(input);
};
인풋 태그를 생성하도록 했다.
지정된 클래스명을 적용해줬고, 기존의 값을 기억하도록 input의 value에 현재 아이템 label의 값을 넣어줬다.
const handleKeyDown = (e) => {
const indexNum = e.target.parentElement.dataset['index'];
if (e.key === 'Enter') {
$(`[data-index="${indexNum}"] > div > label`).textContent = e.target.value;
$(`[data-index="${indexNum}"]`).classList.remove('editing');
$(`[data-index="${indexNum}"] .edit`).remove();
}
if (e.key === 'Escape') {
$(`[data-index="${indexNum}"]`).classList.remove('editing');
$(`[data-index="${indexNum}"] .edit`).remove();
}
};
인풋 창에서 엔터 클릭시 현재 값을 기존 li 태그에 적용시켜주고, 방금 작성한 input을 삭제시켜줬다.
인풋 창에서 esc 클릭시 그냥 input을 삭제시키고, 기존의 li 태그가 보이도록 해줬다.




한줄로 요약하자면...
도대체 리액트 없을 때 개발 어떻게 한거임?
생각보다 어려웠다.
리액트를 배워서 그런지 완료한 상태나, 리스트들의 반복되는 것을 보면서
'useState나 useEffect가 있다면 더 편할텐데...',
'jsx로 li를 반복 시키면 좋을 텐데...',
'props를 활용하면 쉬울 것 같은데...'
와 같은 생각이 아른아른 거렸다.
뭔가 함수로 빼서 재사용을 하고 싶어도, 이것저것 다른 부분이 있어 만들기 애매했고,
요소의 데이터 값을 가져오려고 해도 부모요소에 왔다갔다 해서 가져와야 하는 등, 불편한 점도 많았다.
또 어떻게 어찌저찌 만들었지만, 저번 숫자야구 때 보다는 코드도 만족스럽게 짰다는 느낌은 들지 않았던 것 같다.
이번에도 스스로 정말 자바스크립트를 잘 하고 있는게 맞는지, 되돌아 볼 수 있는 시간이였다...