
이번 작업물의 배포 깃허브
https://github.com/mysunshinedying/jQueryTodoList
0. 들어가기에 앞서
이번 주차는 jQuery에 대해서 배웠다. 본래는 jQuery의 연습 문제 리뷰를 해보려고 했는데, 그것으로는 컨텐츠가 되지 않는다. 마침 메인을 만드는 과정을 진행하고 있으니, 따로 jQuery 메서드를 사용하여 무언가를 만드는 것도 괜찮아보였다. 코드 자체의 암기도 중요하지만 실습은 더 중요한 거니까.
이번 주차에서는 따라서 localStorage를 활용하여 브라우저에 내용을 저장하는 Todo List를 만들 것이다. 이 Todo List는 작업 시간 또한 시간 형태로 기록할 수 있어 유용하다고 할 수 있다.
1. 무엇을 만들까?- 기획
만들 것을 정하기
우선 무엇을 만들까에 대한 고민이 컸다. 서버 사이드 언어가 아닌 클라이언트 사이드 언어만으로 무언가의 기능을 직접 기획해서 만드는 것은 처음이다. 정확히는 해본 적은 있지만, 대부분 정적인 웹사이트 정도였기 때문이다. 그래서 클로드 소넷을 통하여 jQuery로 만들만한 것에 대해서 물어보았다. 그가 제시해준 것은 여러 가지가 있었다. 미니 게시판이나 지금부터 이야기할 todo list 같은 것이었다. 그 중에서 todo list를 골라서 만들어보기로 했다.
처음에 클로드 소넷이 제시한 것은 단순한 todo list에 가까웠기 때문에, 이것을 좀 더 확장시켜보기로 했다. 작업시간 스톱워치나, todo list를 필터하는 기능, todo의 완료 상태에 대한 기능을 조금 더 넣기로 했다.
기능 정하기
무엇을 만들까를 정했기 때문에, 이제 세부 기능을 정해야 할 차례다. 스톱워치와 todo list 그리고 필터링에 대한 각 세부적인 기능을 정한다. 만들면서도 기능에 대한 아이디어가 떠올라 기능을 추가했다. 세부 기능을 미리 계획하는 것은 중요하다. 만드는 도중에 추가하더라도, 대부분 계획하는 과정에서 기능이 정해지기 때문이다. 기획 단계에서 내가 이 기능이 가능할지 아닐지 판단하는 쪽이 개발 과정에서 헤매지 않을 수 있다.
작업시간 스톱워치
작업시간 스톱워치에는 스톱워치의 기본 기능이 들어가야한다. 스톱워치를 시작하여 시간을 세는 시작 기능. 그리고 스톱워치를 정지하고 시간을 초기화하는 정지 기능이다.
(1) 시작
스톱워치를 시작해 시간을 센다.
(2) 정지
스톱워치를 정지하고, 시간을 초기화한다.
추가 기능을 3가지 더 넣기로 했다. 특히 리셋의 경우 스톱워치 기능에서 자주 사용된다. 일시정지의 경우 그냥 정지를 제외하고 기능에 들어가는 경우도 있다(이 경우 리셋 버튼이 정지의 기능도 담당한다). 각 기능은 다음과 같이 세부 결정하였다.
(3) 일시정지
스톱워치를 정지하지만, 시간을 초기화하지 않는다.
(4) 리셋
스톱워치가 정지하든 하지 않든 시간을 초기화한다. 이 버튼의 경우 개발 중간에 있는 것이 좋다고 판단하여 추가하게 되었다.
(5) 총 작업시간
누적된 스톱워치 시간을 센다.
Todo List
todo list, 즉 할 일에 대한 리스트는 스톱워치보다 기능을 다양하게 구현할 수 있다. 그날 하루만의 할 일을 기록할 수도, 한 달의 할 일을 기록할 수도, 아니면 영구적인 기간의 할 일을 기록할 수도 있다. 이번에 만드는 것은 영구적인 할 일 리스트이다.
(1) Todo 등록
할 일을 등록한다.
(2) Todo 삭제
등록된 할 일을 삭제한다.
(3) 체크표시를 통해 할 일 완료 표기 하기
체크 표시로 해당 할 일을 완료했다고 표기한다.
여기까지가 기본 기능이라고 할 수 있다. 여기에서 개인적으로 선호하는 기능 두 가지를 넣도록 하겠다.
(4) Todo에 기한 날짜를 넣기
할 일에 대한 기한 날짜를 넣는다. 기한 날짜가 지나면 지워지거나 하는 기능도 고려해보았지만, 기한이 지나더라도 그대로 볼 수 있도록 그대로 두기로 했다.
(5) 완료 표기가 되면 맨 아래로 내려가기 ↔ 완료 표기가 해제되면 본래의 위치로 돌아가기
완료 표기가 될 경우 해당 할 일은 맨 아래로 내려간다. 완료된 할 일과 완료되지 않은 할 일을 구분하는 것이다. 이렇게 하면 그 때 남은 할 일을 바로 주시할 수 있다. 그리고 완료 표기가 해제 되면, 본래 완료되기 직전 그 할 일이 있던 위치로 돌아간다.
필터링 기능
실시간으로 할 일(todo)를 필터링하는 기능이다. 검색 기능이라고 볼 수도 있는데, 일반적으로 검색 버튼을 누르거나 해야하는 경우와 달리 실시간으로 필터링한다.
(1) 필터링 항목에 따라서 필터링 할 input의 타입을 변화하기
할 일의 제목과 기한 두 가지로 분류하여 각각 다른 형태로 필터링하기로 했다. 이 과정에서 기한은 날짜date이기 때문에 input이 다른 쪽이 더 검색하기 용이하다.
(2) 입력할 때마다 실시간으로 Todo를 필터링하기
위에서 설명했듯, 필터링 박스에서 입력할 때 실시간으로 필터링하는 형태가 된다. 이로서 검색 버튼을 누르지 않아도 된다. 일반적으로는 버튼을 사용하는 쪽이 더 부담은 적지만, 실시간으로 필터링 되는 걸 보여주는 쪽이 더 미관적으로 좋다고 판단했다.
요소/메서드 세분화
요소 및 메서드를 세분화한다. 구현을 위한 메서드 자체는 만들면서 정해도 괜찮지만, 이 단계에서 미리 어떤 함수-메서드를 사용할 것인가 정해두는 것이 좀 더 낫다.
여기서는 일단
(1) 구조에 대한 파악 (2) 요소를 세분화 (3) 메서드를 정한다
의 과정을 거칠 예정이다.
작업시간 스톱워치
스톱워치의 구조를 일단 확인하자.

이렇게 "시간"과 각 기능을 실행할 "버튼"이 있다. 시간의 경우는 일반적인 텍스트에 시간을 display 하면 된다. 기능에 대한 버튼의 경우, 위에서 정했던 기능에 따라서 (1) 시작 (2) 정지 (3) 일시정지 (4) 리셋 버튼을 넣으면 된다. 총 작업 시간의 경우는 텍스트 요소로 시간과 비슷한 형태로 들어가면 된다.
그렇다면 구조의 UI는 다음과 같이 정해진다.

이제 이것들의 메서드를 정한다. 시작의 경우 setInterval을 사용하여 매 초마다 시간을 증가시키면 된다. 정지는 clearInterval을 사용하여 setInterval을 중단 시키고, 시간을 0으로 만든다. 일시정지는 clearInterval을 사용하여 setInterval을 중단만 시킨다. 리셋은 setInterval을 중단시키지 않고, 시간을 0으로 만든다. 총 시간의 경우 별도의 변수로 분리하여 마찬가지로 setInterval을 사용해 매 초마다 시간을 증가시키면 된다.
스톱워치는 간단하다. 그러면 다음이다.
todo list
구현이 제일 복잡한 todo list다. todo list의 구조를 일단 확인하자.

할 일을 적는 곳과 체크하는 곳이 있다. 할 일은 텍스트로 나오게 하고, 체크하는 곳은 체크박스로 결정한다. 그리고 기한을 추가하기로 했으니 이 또한 텍스트로 나타나게 한다.
다만 전자 todo list의 경우 등록 및 삭제가 필요하기 때문에, 이에 대한 부분도 필요하다. 그렇다면 삭제 버튼을 할 일의 목록에 넣도록 하고, 등록에 대한 세부도 정해야한다. 지금까지 넣어야할 것은 (1) 할 일(제목)에 대한 입력 (2) 기한에 대한 입력 (3) 등록 버튼 일 것이다.
이에 따른 UI는 다음과 같이 그려진다.

이제 이것들의 메서드를 정한다. 꽤 많은 양이니 따로따로 정하는 것이 맞다.
(1) 할 일 등록
input의 값을 가져와 버튼을 누르면 ul에 추가시킨다. $.append를 사용한다.
(2) 리스트 관리
삭제 버튼을 누르면 삭제한다. $.remove를 사용한다. 체크를 사용하여 완료를 했다는 표기를 할 class를 추가한다. toggleClass를 사용한다. 그리고 체크를 하면 맨 아래로 내려가는 기능의 구현도 필요한데, 이건 다소 복잡한 로직이 필요하다. attribute 사용, 배열의 사용이 필요하다. 이 부분은 아래에서 구현 단계에서 상세 설명을 하겠다.
그리고 전체 삭제 기능을 추가했다. 이건 개발 중에 집어넣은 것이다. 할 일 전체를 삭제하고 다시 등록할 필요를 느껴 추가하였다. 이것은 ul 내부의 것을 전부 비우면 되기 때문에, $.empty를 사용한다.
이걸로 todo list는 끝난다.
필터링
필터링할 타입을 정할 select box와 필터링할 단어(검색어)를 정할 input을 넣는다. UI는 간단하므로 다음과 같다.

그리고 filter 메서드를 사용한다. 필터링은 일치나 시작, 끝이 아니라 포함으로 결정할 예정이므로 include를 사용한다. 이제 필터링도 종료되었다.
2. HTML 제작
이제 HTML을 제작해야한다. 그 전에 DOM Tree를 그려 구조를 본다. 보통은 DOM Tree를 따로 이미지로 그리지는 않지만, 머릿속에서 큰 요소를 먼저 그리고 아래 세부 요소를 만든다.
DOM Tree 그리기

이렇게 DOM Tree를 그린다. 이것이 구현하면서 반드시 동일하지 않을 수도 있다. 오히려 그런 경우가 많다. 그러므로 DOM Tree에 반드시 집착할 필요는 없다.
요소를 만들기
요소를 만들기 전에 일단 js 파일과 css 파일을 외부로 만들어 불러온다.
<link rel="stylesheet" href="./src/style.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script type="text/javascript" src="./src/script.js"></script>
jQuery를 사용하므로 jQuery cdn을 불러온다.
그리고 요소를 만들기 시작한다. 이 요소들은 완성본과는 차이가 있다. 아직 기능을 추가하지 않았고, 스타일이 지정되지 않았기 때문이다. 사람에 따라서 태그만 넣고 class나 id는 나중에 기능과 스타일을 정하면서 추가하는 경우도 있다.
<div id="wrapper">
<h1>Todo List</h1>
<span class="help">jQuery를 사용한 웹 Todo List입니다.</span>
<div id="timer">작업 시간
<span id="timerCount">00:00:00</span>
<button type="button">시작</button>
<button type="button">일시정지</button>
<button type="button">중지</button>
<button type="button">리셋</button>
</div>
<div>
<input type="text" name="title" id="todoTitle">
<input type="date" name="due" id="todoDue">
<button type="button">등록</button>
<button type="button">전체 삭제</button>
</div>
<ul id="todoList"> <!-- todo 리스트 목록 -->
</ul>
<div>
<select name="filterOption" id="filterOption">
<option value="title">제목</option>
<option value="due">기한</option>
</select>
<input type="text" name="filterTitle" id="filterTitle">
<input type="date" name="filterDue" id="filterDue">
</div>
</div>
이렇게 하면 끝이다. 기능을 추가하면서 추가 및 삭제된다.
3. jQuery로 기능 구현하기
스타일 시트로 꾸미기 전에 먼저 기능부터 구현화한다. 스타일 작업의 경우 효율을 위해 나중에 하는 것이 좋다. 요소가 만들어지고 나서야(동적으로 요소 생성을 포함하여) 라벨링이 쉽고, 라벨이 정해져야 스타일을 정할 수 있기 때문이다.
Todo 등록 및 삭제
먼저 가장 복잡하고, 다른 기능에도 연관이 깊은(필터링) 기능인 todo list부터 만드는 것이 좋겠다 싶다. 사실 스톱워치나 todo list의 순서는 크게 관련이 없다(둘은 아예 별개의 기능이다). 그럼 이제 등록부터 살펴보자.
등록
function todoSubmit(){
const titleValue = $('#todoTitle').val();
const dueValue = $('#todoDue').val();
let dueObject = ''; //오류 방지
if(!titleValue) { //title 체크 먼저, 없을 경우 등록 방지
alert('제목을 입력해주세요.');
return false;
}
if(dueValue) { //기한이 있다면 기한을 추가한다
dueObject = `<span class="dateLabel">기한</span><span class="date">${dueValue}</span>`;
}
$('#todoList').append(`<li"><input type="checkbox" value="1"> <span class="title">${titleValue}</span> ${dueObject}<button type="button" onclick="deleteList()">삭제</button></li>`);
$('#todoTitle').val('');
$('#todoDue').val('');
$('#todoTitle').focus();
}
등록을 위한 function todoSubmit이다.
할 일의 value와 기한의 value를 가져온다. 그리고 만약 할 일이 빈칸일 경우, 등록을 방지한다. 기한은 옵션이므로 기한이 있을 경우 추가할 요소를 설정해주었다. 만약 기한이 없다면 추가 요소로 인한 오류가 있을 수 있으므로 dueObject를 먼저 선언한다.
중복 등록 막기&엔터로 등록하기
같은 내용에 대한 등록을 방지한다. 이에 대한 function checkDuplication을 추가한다.
function checkDuplication(titleValue,dueValue) {
let countTodo = 0;
$('ul#todoList').find('li').each(function(){ //기존에 있는지 확인
if($(this).find('.title').text() == titleValue && $(this).find('.date').text().indexOf(dueValue) != -1) {
countTodo++;
}
});
return countTodo;
}
이 로직은 기존 ul#todoList 안에서 li(여기서는 할 일 목록 하나를 의미한다)를 탐색한다. 그것에서 할 일의 value와 기한의 value를 비교한다. .title 안에 있는 것이 목록 하나의 할 일의 값이고 date 안에 있는 것이 기한의 값이니 둘을 비교하면 된다.
여기에서 할 일은 완전히 일치로 처리한 것은, indexOf의 경우 '목욕'의 할 일 제목과 '강아지 목욕'의 할 일 제목이 동일한 결과가 나오기 때문이다. 그렇게 되면 todo 등록에서 불편한 일이 생긴다. 이 두 조건을 만족하면, countTodo를 증가한다.
그러면 이제 todoSubmit에 이런 걸 넣으면 된다.
if(checkDuplication(titleValue,dueValue) > 0) { //중복 등록을 방지한다
alert('같은 내용을 연속으로 등록할 수 없습니다.');
return false;
}
return 값이 0 이상일 경우 alert 처리하고 등록되지 않게 했다.
그리고 엔터로 등록하는 코드를 추가했다.
$('#todoTitle').on('keydown',function(e){
if(e.keyCode == 13){ //제목에 엔터 시에도 todoSubmit되도록 처리
todoSubmit();
}
});
Todo 삭제
함수 명은 위에서 요소를 생성하면서 지정해두었다. function deleteList다. 정확한 함수명은 아니지만(아마 deleteTodo가 더 맞을 것이다) 알아볼 수 있으므로 문제는 없다.
function deleteList() {
if (!confirm('정말 삭제하시겠습니까?')) {
return false;
}
else{
const $this = $(`li`);
$this.remove();
}
}
여기서 의아할 것이다. li들을 개별 식별하지 않으면 어쩌지? 보통 간단한 remove의 경우, 삭제 버튼의 부모를 삭제하면 된다. 하지만 여기에서는 개별 식별을 추가했는데(이 개별 식별의 사용은 체크 로직에서 후술하겠다) 본래는 index였지만 현재는 uniqueId라는 것으로 개별 식별을 하기로 했다. 따라서 본 코드는 다음처럼 완성되었다.
function deleteList(uniqueId) {
if (!confirm('정말 삭제하시겠습니까?')) {
return false;
}
else{
const $this = $(`li[data-id="${uniqueId}"]`);
$this.remove();
}
}
uniqueId라는 것을 매개변수로 받는다. 위 등록에서 추가하는 요소에도 해당 unique Id를 추가해준다. 그럼 이렇게 된다.
function todoSubmit(){
const titleValue = $('#todoTitle').val();
const dueValue = $('#todoDue').val();
let dueObject = ''; //undefined 방지
if(!titleValue) { //subject 체크 먼저, 없을 경우 등록 방지
alert('제목을 입력해주세요.');
return false;
}
if(checkDuplication(titleValue,dueValue) > 0) { //중복 등록을 방지한다
alert('같은 내용을 연속으로 등록할 수 없습니다.');
return false;
}
if(dueValue) { //기한이 있다면 기한을 추가한다
dueObject = `<span class="dateLabel">기한</span><span class="date">${dueValue}</span>`;
}
$('#todoList').append(`<li data-id="${uniqueId}"><input type="checkbox" value="1"> <span class="title">${titleValue}</span> ${dueObject}<button type="button" onclick="deleteList(${uniqueId})" class="caution"><i class="fa-solid fa-trash"></i></button></li>`);
$('#todoTitle').val('');
$('#todoDue').val('');
$('#todoTitle').focus();
}
그러나 이것이 이 함수의 완성은 아니다. 아래 index와 uniqueId의 추가로 좀 더 추가되는 것이 생긴다.
Todo 체크 기능
"Todo를 완료(체크)하면 아래로, 미완료하면 다시 원래 있던 위치로 가는" 기능이다. 참고로 이 기능은 매우 복잡하다. 보기만 해서는 쉬워보이는 과정일 수 있지만, 다음과 같은 과정을 거쳐야한다.
(1) 우선 각 할 일은 자신의 고유의 위치를 기억해야 한다.
(2) 체크 처리가 되면 이 고유의 위치와 관계 없이 맨 아래로 이동한다.
(3) 체크 처리를 해제하면 기억하고 있는 자신의 고유의 위치로 다시 이동한다.
실제로 요소가 이동을 한다는 건 굉장히 까다롭다. 단순히 이동만 하는 것이 아니다. 보통의 경우 요소를 복사 → 원본을 삭제 → 복사한 요소를 원하는 위치로 이동 이라는 과정을 거친다. 즉, 이 과정은 이렇게 된다.
(1) 우선 각 할 일은 자신의 고유의 위치를 기억해야 한다.
(2) 체크 처리가 되면 이 고유의 위치를 무시한다.
(3) 자신을 복제한 요소를 만들고, 해당 요소를 기억한다.
(4) 자신을 삭제하고, 복제한 요소는 맨 아래에 위치한다.
(5) 체크했다는 표시를 위한 클래스를 해당 요소에 추가한다.
(6) 체크 처리를 해제한다.
(7) 자신을 복제한 요소를 만들고, 해당 요소를 기억한다. 이 때 복제 요소도 자신의 고유 위치를 기억해야 한다.
(8) 자신을 삭제하고, 복제한 요소는 고유 위치로 이동한다.
(9) 체크했다는 표시를 위한 클래스를 해당 요소에서 제거한다.
그렇다면 이 요소에게 '고유의 위치'를 어떻게 기억시킬까? 이동은 어떻게 할까? 일단 맨 아래에 이동하는 함수를 짜보자. checkbox는 동적 요소이므로 $(document)를 포함한 동적 요소 컨트롤 메서드로 해야한다.
$(document).on('click',':checkbox',function(){
const $li = $(this).parents('li');
if($(this).is(':checked')) {
$li.remove();
$('#todoList').append($li);
$li.addClass('complete');
} else {
$li.removeClass('complete');
}
});
보면 일단 checkbox 제어 메서드이다. $li로 해당 요소의 부모인 li를 저장해두었다. 이걸로 클론이 만들어졌다. 그리고 그것이 체크되었다는 표시면 부모 요소를 제거하고, append로 맨 아래에 추가한다. 그리고 체크했다는 표시로 class를 추가한다. 반대로 체크에서 풀려나면, class를 제거한다.
하지만 보다시피 이 상황에서는 원래의 위치로 돌아갈 수 없다. 여기서 두 가지 속성을 더 추가한다. 'index'와 'uniqueId'이다.
index 만들기
이 index는 li의 현재 위치를 나타내는 것이다. 즉, 고유의 위치는 아니지만 이것은 이후 설명할 localStorage를 위해서, 그리고 현 위치를 확인하기 위해서도 필요하다. li에 data-index의 요소를 생성한다. 그러나 이 index를 어떻게 할당할까?
먼저 listIndex라는 변수를 전역으로 선언했다.
let listIndex = 0;
그리고 todoSubmit() 내에 다음을 추가한다.
listIndex++;
이제 등록을 할 때마다 listIndex가 증가한다. 참고로 이것은 안정성을 위해서 맨 아래로 위치를 두는 것이 좋다. 그리고 위에서 했던 li 생성을 수정한다.
$('#todoList').append(`<li data-index="${listIndex}" ><input type="checkbox" value="1"> <span class="title">${titleValue}</span> ${dueObject}<button type="button" onclick="deleteList(${uniqueId})" class="caution">추가</button></li>`);
data-index에 listIndex가 들어갔다. 이제 data-index는 0, 1, 2, 3, 4… 같은 순차적인 값을 가질 것이다.
삭제 로직에서 index를 맞추기
다만 이 경우 삭제처리를 하면 data-index가 꼬이는 문제가 있다. 삭제 후 listIndex--;의 방법도 있지만, 이 경우 복잡한 경우가 되면 감소가 제대로 안 이루어지고, 0,1,2,3의 data-index가 있다면 2번을 지운 후 추가를 할 경우 3이 두 개 나타나는 등의 문제가 있다.
그러므로 재할당한다. 다음과 같은 function을 만들었다.
function organizeIndex(){
$(`li[data-index]`).each(function(index){
$(this).attr('data-index',index); //재정렬
});
}
이렇게 하여 이렇게 만들어진다.
function deleteList(uniqueId) {
if (!confirm('정말 삭제하시겠습니까?')) {
return false;
}
else{
const $this = $(`li[data-id="${uniqueId}"]`);
$this.remove();
organizeIndex();
}
}
그러나 보다시피 이렇게 하면 listIndex는 초기화되지 않는다. 그러므로 하나의 과정을 더 추가했다.
function deleteList(uniqueId) {
if (!confirm('정말 삭제하시겠습니까?')) {
return false;
}
else{
const $this = $(`li[data-id="${uniqueId}"]`);
$this.remove();
listIndex = Number($('ul#todoList').find('li').length); //개수만큼으로 재할당한다.
organizeIndex();
}
}
이렇게 data-index와 총 개수는 맞춰진다.
uniqueId 만들기
이 단계 쯤에서 다음과 같은 고민을 했다.
여기서 index-data를 재할당한다면 문제는
'체크'를 처리할 경우, 본래의 origin data가 존재하고 있다는 점이야.
그 경우 재정리를 하면 순서대로 재할당이 된단 말이야? 고민이네.
고유 아이디를 가지고 이를 배열화해서 순서를 재정의할 수 있을까?
이것이 uniqueId를 만들게 된 이유다. 요소에는 data-id라는 속성으로 넣었다. 이것도 순서를 결정해야하므로 다음과 같은 전역 변수를 일단 선언했다.
let uniqueId = 0;
그리고 생성할 때마다 증가하도록 listIndex++ 아래에 uniqueId++을 선언해주면 된다. 그러나 이것만으로 끝이 아니다, 이것들은 배열화 해줘야한다. 그렇게 해주어야 uniqueId의 변동에도 문제가 생기지 않고 안정성이 늘어난다.
let listArray = [];
이렇게 전역변수로 배열도 추가한다. 이제 등록할 때마다 배열에 uniqueId를 넣는다.
function todoSubmit(){
const titleValue = $('#todoTitle').val();
const dueValue = $('#todoDue').val();
let dueObject = ''; //undefined 방지
if(!titleValue) { //subject 체크 먼저, 없을 경우 등록 방지
alert('제목을 입력해주세요.');
return false;
}
if(checkDuplication(titleValue,dueValue) > 0) { //중복 등록을 방지한다
alert('같은 내용을 연속으로 등록할 수 없습니다.');
return false;
}
if(dueValue) { //기한이 있다면 기한을 추가한다
dueObject = `<span class="dateLabel">기한</span><span class="date">${dueValue}</span>`;
}
$('#todoList').append(`<li data-index="${listIndex}" data-id="${uniqueId}"><input type="checkbox" value="1"> <span class="title">${titleValue}</span> ${dueObject}<button type="button" onclick="deleteList(${uniqueId})" class="caution"><i class="fa-solid fa-trash"></i></button></li>`);
listArray.push(uniqueId); //listArray에 uniqueId를 넣는다.
$('#todoTitle').val('');
$('#todoDue').val('');
uniqueId++;
listIndex++;
$('#todoTitle').focus();
}
그리고 제거에도 필요하므로 추가한다.
이 제거 과정은 splice를 사용했는데, 보다시피 array를 indexOf로 살펴본 뒤 해당 array의 index를 반환한다. 이것을 splice에서 인지하여 그 위치에 있는 것을 잘라 제거한다.
function deleteList(uniqueId) {
if (!confirm('정말 삭제하시겠습니까?')) {
return false;
}
else{
const $this = $(`li[data-id="${uniqueId}"]`);
const arrayIndex = listArray.indexOf(Number($this.attr('data-id')));
$this.remove();
listArray.splice(arrayIndex, 1);
resetListCount();
}
}
위에서 말했다시피 uniqueId는 고유 값이므로, 삭제에도 용이하다. 이렇게 uniqueId를 설정했으니 이제 재정렬을 할 차례다.
왜 uniqueId를 제목 등으로 고르지 않았는가?
사실 단순한 숫자는 id로서 그다지 좋은 방식은 아니다. 식별을 위해서라면 할 일의 제목쪽이 더 나을지도 모른다. 하지만 제목의 경우 같은 할 일을 중복 등록할 경우 문제가 생긴다. 그렇다면 별개의 식별을 위한 방법이 여러 가지 있는데, 거기서 간단한 uniqueId를 사용했다. 만약 다른 방식을 사용한다면 등록시간을 기준으로 하거나 해도 좋을 것이다. 이 경우 uniqueId++는 필요하지 않다.
재정렬하기
이제 uniqueId와 이를 배열화한 listArray를 통한 재정렬을 시작한다.
일단 function reorderList를 만든다. 그 후에 재정렬 로직을 생각해보자. 일단 우리는 listArray에 uniqueId를 순서대로 등록했다. 그렇기 때문에 listArray에는 본래 등록 '순서'가 저장되어 있다. 즉, listArray의 index순에 따라서 재할당하면 된다. 여기서 중요한 건, 그저 재할당하면 안된다는 사실이다. 여기서 uniqueId의 존재가 중요하다. 각 uniqueId의 값이 배열에 '저장 되어 있다.
따라서 이렇게 할당한다.
function reorderList(){
listArray.forEach(function(element,index) { //li index data를 재할당한다
$(`li[data-id="${element}"]`).attr('data-index',index);
});
그러나 이렇게 되면 data-index의 값을 listArray의 index로 재할당하기만 할뿐, 실질적인 요소의 재정렬은 되지 않는다. 여기에서 깊은 고민에 빠졌다. 그 주석을 공개한다.
이것만으로는 재정렬이 아예 되지 않는다.
todoList의 내용물을 비우거나 해서 다시 넣는 작업이 필요하다.
하지만 이 상태(listArray.forEach)에서 empty 처리는 불가능하다.
이 상태면 그냥 다 비워지고 마지막만 남아.
배열로 하나하나 li를 저장하고 불러오는 건 굉장히 비효율적일 거 같아.
하지만 empty처리 전에 담아둘 공간이 필요한데…
방법으로는 const $elementList = $(`li[data-id]`)를 배열화하하는 방법이었다. .toArray() 하여 배열화 한 뒤, sort를 사용했다.
const $elementList = $(`li[data-id]`).toArray();
$elementList.sort(function(a, b) {
return $(a).data('index') - $(b).data('index');
});
$('#todoList').empty().append($elementList);
sort는 기본적으로 비교함수를 전달해야한다. 이것이 콜백함수가 된다. 두 개의 배열 element를 파라미터로 입력받으며, 여기서 a,b는 비교를 위한 임의의 매개변수(즉, 파라미터를 변수로 받는 것이다)이다. a의 data-index의 값과 b의 data-index의 값을 비교한다. 여기서 음수가 나오면 b가 더 크단 뜻으로 a를 앞에 두고, 양수가 나오면 a가 더 크단 뜻이므로 b를 앞에 둔다.
여기까지 해두고 다음처럼 체크 메서드를 수정한다.
$(document).on('click',':checkbox',function(){
const $li = $(this).parents('li');
if($(this).is(':checked')) {
$li.remove();
$('#todoList').append($li);
$li.attr('data-index',listIndex);
$li.addClass('complete');
organizeIndex(); //index 재할당
} else {
$li.removeClass('complete');
reorderList(); //list 초기화
}
});
재정렬은 끝인줄 알았으나 버그가 터졌다.
디버깅- 완료되었던 Todo가 본래의 위치에 돌아가는 현상
재정렬까지 완료되었으나, 삭제 등의 행동을 할 때 본래의 위치로 돌아갔다. 이 reorderList 함수를 삭제 등에서도 호출하도록 했기 때문이다. 그뿐만이 아니라, 새로이 리스트를 추가할 때도 체크한 목록 아래에 새 목록이 추가되었다.
이 문제를 해결하기 위해서 다음 로직을 추가했다.
1. 체크한 것과 체크하지 않은 것을 li에서 분리
2. 체크하지 않은 것을 선행하여 출력, 체크한 것을 후에 출력.
3. 이후 data-index를 재할당한다.
4. 그리고 새로이 만들 때도, 삭제할 때도 재정렬을 추가한다.
그렇게 완성된 재정렬 함수는 다음과 같다.
function reorderList(){
listArray.forEach(function(element,index) { //li index data를 재할당한다
$(`li[data-id="${element}"]`).attr('data-index',index);
});
const $unchecked = $('li[data-id] input:not(:checked)').parent().toArray();
$unchecked.sort(function(a, b) { //체크되지 않은 것들끼리 비교한다.
return $(a).attr('data-index') - $(b).attr('data-index');
});
const $checked = $('li[data-id] input:checked').parent();
$('#todoList').empty().append($unchecked).append($checked);
organizeIndex(); //실제 위치가 바뀌었으므로 위에서 아래로 data-index를 재할당해야한다.
}
function organizeIndex(){
$(`li[data-index]`).each(function(index){
$(this).attr('data-index',index); //재할당
});
}
이후 만들 때도, 삭제할 때도, 체크할 때도 이렇게 재정렬 함수를 호출했다. 특히 체크 메서드는 다음처럼 수정되었다.
$(document).on('click',':checkbox',function(){
const $li = $(this).parents('li');
if($(this).is(':checked')) {
$li.addClass('complete');
} else {
$li.removeClass('complete');
}
reorderList();
});
이렇게 하면 이제 문제가 사라진다.
작업 스톱워치의 추가
이제 작업 시간을 재기 위한 스톱워치를 만들 예정이다.
스톱워치 시작하기/정지하기/일시정지하기
일단 이들에 대한 function을 나누고, 다음처럼 코드를 작성한다.
- 시작하기
let totalSeconds = 0;
let isTimerStop = true;
let intervalId;
function startTimer() {
isTimerStop = false; //리셋 기능을 위해서 추가하는 스톱워치가 작동하고 있는가에 대한 변수이다.
intervalId = setInterval(function() {
totalSeconds++;
},1000);
}
- 정지하기
function stopTimer(){
isTimerStop = true;
clearInterval(intervalId);
totalSeconds = 0;
}
- 일시정지하기
function pauseTimer(){
isTimerStop = true;
clearInterval(intervalId);
}
이렇게 만든 시각을 이제 표현해야한다(display)
표현하기
디스플레이에 표현하기 위해서 다음과 같은 함수를 추가했다.
function clockWork() {
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds % 3600) / 60);
let seconds = time % 60;
let display = String(hours).padStart(2, '0') + ':' +
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0');
$('#timerCount').text(display);
}
padStart는 문자열이 빌 경우 특정 문자열로 채워주는 함수이다. String.padStart(number,str) 형태가 된다. 보통 스톱워치는 00:00:00 형태이므로 01, 02와 같은 형태도 지원해야 한다. 따라서 padStart를 사용하여 채워주었다.
처음에 clockWork에서 고민한 것은 시간을 재는 방법이었다.
let seconds = 0;
let minutes = 0;
let hours = 0;
const splitTime = $('#timerCount').text().split(':');
hours = Number(splitTime[0]);
minutes = Number(splitTime[1]);
seconds = Number(splitTime[2]);
seconds++;
if(seconds === 60) {
minutes++;
seconds = 0;
}
if(minutes === 60) {
hours++;
minutes = 0;
}
hours = String(hours).padStart(2, "0");
minutes = String(minutes).padStart(2, "0");
seconds = String(seconds).padStart(2, "0");
$('#timerCount').text(`${hours}:${minutes}:${seconds}`);
이 방법은 너무 번거롭고 중복되기 때문에 삭제하고 위와 같은 방식으로 수정했다. 이제 이렇게 만든 clockWork를 위에 있는 시작하기에 넣으면 된다.
function startTimer() {
isTimerStop = false;
intervalId = setInterval(function() {
totalSeconds++;
clockWork();
},1000);
}
정지의 경우도 totalSeconds가 0이 되기 때문에 clockWork()를 불러 디스플레이에 표시하는 것이 좋다.
function stopTimer(){
isTimerStop = true;
clearInterval(intervalId);
totalSeconds = 0;
clockWork();
}
리셋 기능
리셋 버튼을 부르면 호출되는 function을 만들면 된다. 위에서 설정한 isTimerStop에 따라서 작동을 달리한다.
function resetTimer(){
totalSeconds = 0;
if(isTimerStop) {
clockWork();
} else {
clearInterval(intervalId);
startTimer();
}
}
startTimer가 중복 실행되는 걸 방지하기 위해 clear 과정을 거친다. 이러면 문제 없이 시간은 리셋된다.
디버깅- Start를 두 번 누르면 setInterval()이 중복 실행되는 현상
다만 Start 버튼을 두 번 이상 누르면 setInterval()이 중복 실행되는 현상이 발생했다. 이걸 방지하기 위해서, startTimer() 함수에 return을 넣었다. isTimerStop이 false일 경우, return 처리를 하여 중복 실행을 막는다.
function startTimer() {
if (!isTimerStop) { //중복 실행을 방지한다. @중복 실행 문제가 발생했다.
return;
}
isTimerStop = false;
intervalId = setInterval(function() {
totalSeconds++;
clockWork();
},1000);
}
그리고 이렇게 된 상황에서 reset 버튼에서의 로직이 불필요해져 생략했다.
function resetTimer(){
totalSeconds = 0;
if(isTimerStop) {
clockWork();
}
}
버그가 사라졌다. 스톱워치는 이것으로 종료되었다.
필터링 기능
이제 todo list를 필터링할 필터링 기능을 구현해야한다. 두 가지가 정해진다. 어떤 형식으로 필터링하는가(할 일 제목, 기한). 그리고 어떻게 필터링할 것인가. 이는 $.toggle() 메서드를 사용하기로 했다.
select에 따라서 필터링 구현
먼저 select에 on 메서드를 추가한다. 왜냐하면 select가 바뀔 때마다 input이 달리 나타나야 하기 때문이다.
$('#filterOption').on('change',function(){
let filterOption = $(this).val();
if(filterOption === 'title') {
$('#filterDue').hide().val(''); //val값 초기화.
$('#filterTitle').show();
} else {
$('#filterTitle').hide().val(''); //val값 초기화.
$('#filterDue').show();
}
});
이렇게 on chage하면 이제 input이 바뀐다.
function filterTodos(){
let filterOption = $('#filterOption').val();
if(filterOption === 'title') {
} else {
}
}
이제 Todo를 filter하는 함수를 만들자.
$.toggle()의 활용
먼저 필터링은 다음과 같은 과정을 거친다.
(1) 빈칸일 경우 모든 리스트가 보이도록 한다.(이 순서는 상관없다)
(2) 각 타입에 맞는 input을 입력한다.
(3) 타입값에 맞춘 input 값을 불러온다.
(4) 그 input값이 리스트(li)에 값이 존재하는지 체크한다.
(5) 존재한다면 드러나도록, 존재하지 않다면 숨도록 한다.
이 5번에서 $.toggle을 사용할 수 있다. $.toggle() 메서드는 괄호 안이 true면 보이도록, false면 보이지 않도록 한다. 하나하나 if를 쓰면서 $.show와 $.hide를 쓸 필요가 없는 것이다. 이것이 맞는지를 확인하기 위해 변수 isMatch를 만든다. isMatch가 true라면 보이도록, false라면 보이지 않도록 한다.
$('ul#todoList').find('li').each(function(){
const $li = $(this);
let isMatch = false;
if(filterOption === 'title') {
const title = $li.find('.title').text().toLowerCase();
isMatch = title.includes(keyword);
} else {
const due = $li.find('.date').text();
isMatch = due.includes(keyword);
}
$li.toggle(isMatch);
});
$li 내에서 .title내의 text값은 할 일의 제목이다. toLowrCase의 경우 소문자로 한다는 것인데, 대소문자 구별 없이 찾기 위해서다. date값은 할 일의 기한이다. String.includes(keyword)는 keyword를 String이 포함하는지 확인한다. 포함하면 true를, 포함하지 않으면 false를 반환한다. isMatch를 이렇게 재할당하여, $.toggle()에 사용한다.
필터링은 이렇게 종료되었다.
통계 기능
통계 기능은 중간에 추가된 기능이다. todo의 개수를 세거나, 작업 시간 전체를 알아보기 위해 추가했다. HTML도 이렇게 추가하였다.
<button type="button" onclick="calculateStats(); $('#TodoStats').slideToggle();">통계보기</button>
<div id="TodoStats">
<p><b>현재 todo 개수(완료/전체)</b> <span id="completeCount">0 / 0</span></p>
<p><b>총 작업 시간</b> <span id="totalWork">00:00:00</span>
<button type="button" onclick="calculateStats()" title="통계 새로고침"><i class="fa-solid fa-rotate"></i></button>
<button type="button" onclick="resetTotalTime();" class="caution">작업 시간 리셋</button>
</div>
이제 기능을 추가한다. 여기에서는 자동 반영보다는 통계보기 버튼을 눌렀을 때, 그리고 새로고침할 때만 적용하기로 했다. 자주 사용되는 기능이 아니기 때문이다.
todo 개수 세기
todo 개수는 완료(체크된 것)과 전체를 구해야 한다.
function calculateStats() {
let completeTodos = $('ul#todoList').find(':checkbox:checked').length;
let totalTodos = $('#todoList li').length;
let totalTodoRatio = `${completeTodos} / ${totalTodos}`;
$('#completeCount').text(totalTodoRatio);
}
이런 식으로 checkbox가 checked된 것의 개수, 전체 todo의 개수를 정하여 text로 표기했다.
총 작업시간 만들기
총 작업시간은 totalTime이라는 전역 변수를 만들어 totalSeconds와 다르게 관리하여 부르면 된다. 여기서 중요한 것은, 정지나 리셋 시에도 totalTime은 기록되어야 한다. 따라서 초기화는 하지 않고, 시작할 때 증가만 시킨다. 다만, 여기에서 display 문제가 있는데…
function clockWork() {
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds % 3600) / 60);
let seconds = time % 60;
let display = String(hours).padStart(2, '0') + ':' +
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0');
$('#timerCount').text(display);
}
위에서 적었던 clockWork가 timerCount와 totalSeconds에만 사용되는 것. 비슷한 로직을 또 구성하는 것은 코드를 복잡하게 할 뿐이다. 여기서 clockWork에 매개변수를 넣어 재활용하기로 했다.
function clockWork(time,element) {
let hours = Math.floor(time / 3600);
let minutes = Math.floor((time % 3600) / 60);
let seconds = time % 60;
let display = String(hours).padStart(2, '0') + ':' +
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0');
$(element).text(display);
}
그리고 당연히 clockWork가 적용된 함수들도 수정하였다.
function startTimer() {
if (!isTimerStop) { //중복 실행을 방지한다. @중복 실행 문제가 발생했다.
return;
}
isTimerStop = false;
intervalId = setInterval(function() {
totalSeconds++;
totalTime++;
clockWork(totalSeconds,'#timerCount');
},1000);
}
총 작업시간 리셋
스톱워치 리셋과 비슷한 형식으로 이루어졌다. 다만, 총 작업시간은 꽤 중요도가 높기 때문에, confirm 처리를 했다.
function resetTotalTime() { //총 작업시간에 대한 초기화 또한 원할 수 있으므로 추가.
if (confirm('총 작업시간을 초기화하시겠습니까?')) {
totalTime = 0;
localStorage.setItem('totalTime',0);
clockWork(totalTime, '#totalWork');
} else {
return false;
}
}
스톱워치에 대한 작업은 이제 종료된다.
localStorage를 사용하여 내용 저장하기
이것은 중간에 추가된 기능이다. 할 일 목록이라는 것은 기본적으로 기록용이기 때문에, 사라져버리면 아쉽다. 그러므로 저장을 할 수 있는 방법을 생각해보았는데… 바로 localStorage다. 서버의 DataBase를 사용할 수 없다면 데이터를 저장하는 방법은 여러 가지인데(BaaS의 사용 등) 여기서는 간단하게 저장할 생각으로 localStorage를 선택했다.
localStorage는 브라우저의 웹 스토리지 API 중 하나이다. 이를 통해 특정 도메인을 위한 세션 저장소 또는 로컬 저장소의 접근 경로로서 데이터를 추가하고 수정하거나 삭제할 수 있다. 브라우저 내에 데이터를 거의 영구적으로 저장하므로, 소실될 위험이 다른 방식보다는 적다. 로컬환경에서도 작동한다.
중간에 추가하지 않더라도 localStorage는 다른 기능을 완성한 뒤에 하는 쪽이 효율적이다. 변수도 정해져 있고, 함수도 대체로 구성 되었으니 저장 로직만 추가하면 되기 때문이다. 처음부터 이를 고려하며 짜기 시작하면 피곤해진다.
저장하기로 한 건 현재 시간(스톱워치에 기록된), 전체 작업 시간, 각 todo 데이터이다.
key와 value값으로 json화 해서 storage에 저장하기
저장 자체는 간단하다. totalTime의 경우 localStorage에 localStorage.setItem('totalTime', totalTime); 와 같이 저장하면 된다. 그러나 todo 같은 것은 배열화하지 않으면 안 된다. todo 하나에 개별적인 데이터를 가지고 있기 때문이다. 할 일 제목, 할 일의 기한, 그리고 체크가 되어있는가, 마지막으로 이 todo의 순서까지 저장해야 한다. 그러므로 여기서는 객체를 사용하여 배열화하고 json으로 바꾸어 storage에 저장해야한다.
1) 객체로 만든다
좁은 의미의 객체에서는 key와 value값으로 이루어진 한 쌍을 객체라고 한다. 하나의 todo를 파헤쳐 객체화하면 다음과 같다.
let todoObj = {
id: (아이디),
title: (할일 제목),
due: (기한),
done: (체크 여부),
index: (순서)
};
이것들의 정보를 각 할일 목록(li)에서 불러와야 한다.
if(!$(`li[data-id]`).length) { //목록이 없다면 돌려보낸다.
return;
}
$(`li[data-id]`).each(function(){
const listId = $(this).attr('data-id');
const todoIndex = $(this).attr('data-index');
const titleValue = $(this).find('.title').text();
const dueValue = $(this).find('.date').text();
let todoDone = 0;
//체크되었는가를 저장한다.
if($(this).find('input:checked').length > 0) {
todoDone = 1;
} else {
todoDone = 0;
}
let todoObj = {
id: listId,
title: titleValue,
due: dueValue,
done: todoDone,
index: todoIndex
};
});
이렇게 각 li에서 가져온다. checkbox의 경우 체크되었는가 여부를 직접 저장하기 어렵기 때문에, 저장되었다면 1, 저장되지 않았다면 0으로 저장하기로 한다. true와 false로 저장하는 것도 문제는 없다.
그렇지만 이 상태면 하나의 todoObj를 정의했을 뿐이다. 저장할 공간이 필요하다. 그게 바로 todoData라는 배열이다. 각 데이터를 todoData에 저장한다.
function todoSubmitStorage(){
todoData = [];
if(!$(`li[data-id]`).length) {
localStorage.setItem('todos', JSON.stringify(todoData));
return;
}
$(`li[data-id]`).each(function(){
const listId = $(this).attr('data-id');
const todoIndex = $(this).attr('data-index');
const titleValue = $(this).find('.title').text();
const dueValue = $(this).find('.date').text();
let todoDone = 0;
if($(this).find('input:checked').length > 0) {
todoDone = 1;
} else {
todoDone = 0;
}
let todoObj = {
id: listId,
title: titleValue,
due: dueValue,
done: todoDone,
index: todoIndex
};
todoData.push(todoObj);
});
}
그러나 이 상태에서 저장할 수는 없다. 배열은 문자열이 아니기 때문이다. localStorage에 저장하는 것은 기본적으로 문자열String이다. 그렇다면 배열을 문자열로 만들어야 하는데… 이때 자주사용되는 것이 json화이다. JSON.stringify(배열); 형태로 사용한다. 이렇게 배열을 변환한 뒤, 저장하면 된다. 따라서 최종 형태는 다음과 같다.
function todoSubmitStorage(){
todoData = [];
if(!$(`li[data-id]`).length) {
localStorage.setItem('todos', JSON.stringify(todoData));
return;
}
$(`li[data-id]`).each(function(){
const listId = $(this).attr('data-id');
const todoIndex = $(this).attr('data-index');
const titleValue = $(this).find('.title').text();
const dueValue = $(this).find('.date').text();
let todoDone = 0;
if($(this).find('input:checked').length > 0) {
todoDone = 1;
} else {
todoDone = 0;
}
let todoObj = {
id: listId,
title: titleValue,
due: dueValue,
done: todoDone,
index: todoIndex
};
todoData.push(todoObj);
});
localStorage.setItem('todos', JSON.stringify(todoData));
}
이 저장과정 함수는 이제 적절한 곳에 호출하면 되는데, reorderList() 함수의 호출이 제일 잦기도 하고, 순서 변경 등 중요한 부분이 있기 때문에, 맨 아래에 todoSubmitStorage()를 호출하기로 했다.
storage에서 불러오기
loadFromStorage() 함수를 로드할 때 불러온다. 참고로 localStorage는 문자열로 저장된다. 그러므로 localStorage의 데이터를 불러올 때는 문자열이 된다. 따라서 totalSeconds나 totalTime 같은 것은 숫자 변환이 필요하다.
function loadFromStorage(){
totalSeconds = Number(localStorage.getItem('timerCurrent')) || 0;
totalTime = Number(localStorage.getItem('totalTime')) || 0;
clockWork(totalSeconds,'#timerCount');
clockWork(totalTime,'#totalWork');
}
이렇게 숫자변환 후(값이 없다면 0으로 처리하도록 한다) display하는 함수를 사용한다.
그리고 todoData를 불러올 차례이다. 현재는 배열의 문자열화가 되어 있기 때문에, 다시 배열로 해체하는 과정이 필요하다. JSON.parse(json이 된 배열);으로 하여 배열로 돌릴 수 있다. 그리고 이 배열들을 forEach하여 해체 작업하면 된다. 다만, 앞서 우리는 객체를 배열화 해서 저장했다. 이 객체의 value값을 가져오는 방법은 바로 객체.(해당 키);를 사용한다. 이 과정을 다 거치면 다음과 같은 코드가 된다.
todoData = JSON.parse(localStorage.getItem('todos'));
todoData.forEach(function(element,index) {
const loadId = element.id;
const loadTitle = element.title;
const loadDue = element.due;
const loadDone = element.done;
const loadIndex = element.index;
}
이것들을 이제 요소로 만들어 ul#todoList에 삽입하면 된다. 다만 체크 관련하여 추가 설정이 필요하다. 1. done값이 1일 경우 체크박스에 체크 설정을 해야한다. 2. 체크된 li의 클래스를 complete로 설정한다. 다음을 코드를 추가했다.
let dueObject = '';
let isChecked = '';
let checkedClass = '';
if(Number(loadDone) == 1){
isChecked = 'checked';
checkedClass = 'class="complete"';
} else {
isChecked = '';
checkedClass = '';
}
if(loadDue) { //기한이 있다면 기한을 추가한다
dueObject = `<span class="dateLabel">기한</span><span class="date">${loadDue}</span>`;
}
이제 요소를 만들어 넣으면 된다.
const $li = `<li data-index="${loadIndex}" data-id="${loadId}" ${checkedClass}><input type="checkbox" value="1" ${isChecked}> <span class="title">${loadTitle}</span> ${dueObject}<button type="button" onclick="deleteList(${loadId})" class="caution"><i class="fa-solid fa-trash"></i></button></li>`
$('#todoList').append($li);
그리고 총 통계에 오류가 없도록 checkingChecked 변수를 추가하고, $('#completeCount').text(`${checkingChecked} / ${listIndex}`); 형태로 넣었다. 그리고 listIndex와 uniqueId를 재할당하지 않으면 새로이 생성할 때 부딪히는 오류가 생긴다.
uniqueId = Math.max(...listArray) + 1 || 0; //스프레드 연산자로 max를 판단함
listIndex = todoData.length || 0;
이렇게 하면 로드는 완료되는 거 같지만… 오류가 생겼다.
디버깅- 재정렬에서 체크 로직이 안 먹히는 문제
갑자기 reorderList() 에서 체크 로직이 안 먹히기 시작했다. 체크한 뒤까지는 문제가 없는데, 체크를 해제해도 그 위치에 그대로 남아있는 것. 무엇 때문에 이 문제가 발생했는가 체크해본 결과, 바로 이 객체가 문제였다.
let todoObj = {
id: listId,
title: titleValue,
due: dueValue,
done: todoDone,
index: todoIndex
};
상세 상황을 확인해보니 $.data('index')가, 객체와 부딪히고 있었던 것이다. 현재 코드는 $.attr('data-index')를 사용하지만, 디버깅 전에는 $.data('index') 값을 불러오고 있었다. 이것이 상호 부딪혀 index가 재할당 되지 않았던 것. 모든 코드를 $.attr('data-index')로 수정하고 해결되었다.
디버깅- localStorage에서 불러온 todo가 재정렬이 안 되는 문제
이렇게 불러온 데이터를 체크하거나 체크를 풀 때, 재정렬이 안 되는 문제가 있었다. 위에서는 또 다른 문제이다. 이 문제는, 내가 불러온 값을 listArray에 저장하지 않았기 때문에 발생했다. 따라서 localStorage에서 listArray를 재할당해준다.
listArray.push(Number(element.id));
listArray.sort(function(a, b) {//listArray의 배열도 정렬해준다.
return a - b;
});
이제 되었다.
디버깅- 전체 삭제 후 새 등록 시 data-id에 -infinity가 찍히는 현상
그러나 또 추가적인 문제가 발생했다. 전체 삭제 버튼을 추가하였는데, localStorage에서 불러온 todo list를 전체 삭제 버튼으로 삭제한 뒤, 새로고침 후 새로운 등록을 하면 data-id가 -infinity 가 찍히는 문제였다. 이는 uniqueId에서 listArray의 숫자를 세던 중 일으키는 문제로, listArray의 값이 없으면 나타나는 문제였다. 따라서 uniqueId를 다음처럼 수정했다.
uniqueId = listArray.length > 0 ? Math.max(...listArray) + 1 : 0;
이제 문제 없이 돌아간다.
이것으로 기능 구현은 마무리된다.
4. CSS로 스타일 마무리 하기
CSS로 스타일을 마무리하는 것은 중요하다. 미감도 미감이지만 어떤 기능인지, 무엇이 중요한지를 판단할 수 있게 해주는 것이 스타일, 즉 UI의 목적이기 때문이다.
지금까지 마무리한 것의 HTML만 가져오면 다음처럼 생겼다.

각각 요소의 명시성이 매우 낮고, 가독성도 낮다. 따라서 스타일 지정으로 다듬어주지 않으면 안 된다.
테마의 컬러 팔레트 정하기
기본적인 테마의 컬러 팔레트를 정해야 한다. 마음이 가는 대로 색상을 정하여도 좋지만, 코드를 하나하나 다시 찾아보거나 해야한다면 굉장히 비효율적이다. 따라서 미리 색상을 정한 다음, 팔레트를 지정한다.
:root {
--primary-color: #ff9aa2;
--sub-color: #ffe4e1;
--accent-color: #feb6c1;
--background-color: #fff8f5;
}
이걸로 팔레트는 정해졌다.
라벨링 하기
HTML에서 동일한 것의 class 명 등을 지정했다. 중요한 버튼의 색상은 caution으로 class 라벨을 지정해주었다. 그리고 이와 같이 설정을 해준다.
#wrapper button:hover,
#wrapper button.caution {
background: var(--accent-color);
color: var(--background-color);
}
이렇게 라벨링을 한 뒤, 각각에 대해서 색상과 스타일을 지정해주었다.
HTML>jQuery>CSS 세 개의 큰 과정을 거쳐 툴이 완성되었다. 완성된 툴은 다음과 같다.


5. 마무리하며
추가 하고 싶은 기능
기능을 마무리하며 포스트를 쓰는 동안, 이 기능은 어떨까? 하고 추가로 생각한 기능들이 있다.
- 뽀모도로 타이머
30분의 작업 타이머다. 이 또한 총 작업시간에 넣을 수 있다. 알람 기능은 가능하면 좋겠지만 고려 중이다.
- 랩타임 기능
스톱워치에 있는 기능으로, 중간 시간을 기록해주는 기능이다. 총 시간이 아니라 개별 시간을 필요로 할 수도 있다 생각되어 만들었다.
- 색상 테마 기능
색상 테마는 지금 기본적으로 핑크다. 블루나 옐로 같은 색상 테마도 가능하도록 수정하고 싶다.
이 부분은 별도의 포스트로 다루진 않겠지만, 아마 깃허브에 커밋하면서 새 버전으로 추가할 예정이다.
이번 포스트를 위한 작업을 하며, jQuery에 대한 복습을 하기 좋았다. 다음번 React의 경우도 비슷하게 실습 형태로 돌아올 예정이다.
'부트캠프 일지 > 멀티캠퍼스 TIL' 카테고리의 다른 글
| [Java 풀스택 개발자] Java의 용어와 개념: 낯선 Java를 친숙하게 만들어보자! (0) | 2026.02.02 |
|---|---|
| [Java 풀스택 개발자] React의 언어체계 (0) | 2026.01.26 |
| [Java 풀스택 개발자] #특별편(보충학습) - Javascript의 언어체계 (0) | 2026.01.18 |
| [Java 풀스택 개발자] JavaScript 완전 정복! (0) | 2026.01.13 |
| [Java 풀스택 개발자] #특별편(보충학습) - 콜백 함수에 대해 (0) | 2026.01.12 |