JS-5) todo-list 만들기

 * 아래 진행은 '노마드코더' 님의 무료 JS 강의를 참고했습니다.

이번에는 사용자가 원하는 todo-list를 입력할 경우 웹 브라우저 상에 표시해주고, 삭제를 가능하게 만들 예정이다.


[index.html handlin]

- 먼저 index.html에 새로운 form 태그와 input 태그를 추가해 준다.

form id="todo-form"

input type="text" placeholder="Write your to do and press enter." required

여기서 새로 추가된 required는 html에서 해당 input에 입력을 필수로 강제한다. 값을 입력하지 않으면 submit 할 수가 없다.

- 다음으로 input 된 내용을 담아서 표시해 줄 ul 태그를 추가한다.

ul id="todo-list"

ul 태그 내부는 todo.js 파일에서 추가해줄 예정이라 비워둔다. ul 요소는 내부의 목록을 숫자 순서가 없는 항목으로 그룹화 하는데 사용된다. 일반적으로 점, 원 등 여러 가지 형태의 글머리 기호로 표시된다.


[todo.js handling]

- todo-form과 input, todo-list를 불러온다.

const todoForm = document.querySelector("#todo-form");

const todoInput = todoForm.querySelector("input");

const todoList = document.querySelector("#todo-list");

여기서 todoInput 같은 경우는 todo-form 내부에 있기 때문에 위처럼 표기가 가능하다.

- 다음으로 todo-form에 submit을 적용하고, todoInput을 변수 선언할 함수를 함께 짜준다.

function todoSubmit(event) {

    event.preventDefault();

    const newTodo = todoInput.value;

    todoInput.value = "";

}

todoForm.addEventListener("submit", todoSubmit);

todoInput.value를 newTodo로 변수 선언한 후 밑에 줄에서 todoInput.value를 비워줬다. 이렇게 해야 웹 브라우저 상에서 submit을 했을 때, 해당 칸이 비워지게 표시된다.

- 이제 우리가 newTodo로 변수 선언한 내용을 웹 브라우저 상에 띄워주는 함수를 만들어 보자.

function paintTodo(newTodo) {

    const li = document.createElement("li");

    const span = document.createElement("span");

    span.innerText = newTodo;

    const button = document.createElement("button");

    button.innerText = "X";

    li.appendChild(span);

    li.appendChild(button);

    todoList.appendChild(li);

}

위처럼 작성한 후 todoSubmit 함수의 아래에 paintTodo(newTodo)를 추가해 준다. li, span, button 태그를 각각 생성하고, span 내부에 newTodo에서 입력된 내용을 추가해준다. 그리고 li 태그에 span과 button을 담은 후, html 의 todoList에 넣어준다. 이전 내용들에서 사용한 메서드들의 재활용이다.

- 위까지 작성한 상태에서 브라우저로 돌아오면, 작성을 할 때 마다 밑에 사용자가 작성한 글이 리스트로 등록되는 걸 볼 수 있다. 아직 X 버튼을 눌러도 아무런 동작이 일어나지 않는다. 이제 X 버튼을 눌렀을 때 해당 글이 삭제 되는 함수를 추가해보자.

function deleteTodo(event) {

    const li = event.target.parentElement;

    li.remove();

}

-> paintTodo 내부

button.addEventListener("click", deleteTodo);

위의 함수와 paintTodo 내부에 코드를 추가하면 우리가 작성한 글 옆의 x 버튼을 눌렀을 때, 해당 글이 삭제된다. deleteTodo 함수에서 event.target은 event가 갖는 많은 property 중 target을 호출해 그 안의 parentElement를 불러온 거다. 즉, 클릭 된 button의 부모 요소를 특정해서 호출해 해당 부모 요소를 remove 하는 코드다.

- 이제 새로고침을 해도 사용자가 input 한 값이 사라지지 않게 input 값을 localStorage 저장해보자.

let todos = [];

function saveTodo() {

    localStorage.setItem("todos", todos)

}

-> todoSubmit 함수 내부에 아래 코드를 추가한다.

todos.push(newTodo);

우선 input 값들을 받을 [] 변수를 하나 선언한다. 그 다음 사용자의 input 값을 해당 리스트에 push 해준다. 이제 push 받은 todos를 localStorage에 저장한다. todos는 밑에서 업데이트를 할 필요가 있어서 let으로 선언한다.

- 하지만 이렇게 저장된 값은 text의 나열로 저장될 뿐 array의 형태로 저장되지 않는다. 우리가 array의 형태를 필요로 하는 이유는 만약 같은 내용이 input 되었을 때, 저장된 값 마다 고유의 id가 없다면, 우리가 원하는 값만 localStorage에서 삭제하는 게 불가능하기 때문이다. 그렇기에 우선 보이는 형태를 array처럼 변환하기 위해 saveTodo 함수 내부의 코드를 아래 코드로 바꿔준다.

localStorage.setItem("todos", JSON.stringify(todos));

JSON.stringify() 를 사용하면 javascript object나 array 등 어떤 JS코드든지 string으로 만들어준다. * 위의 코드를 쓰면 array처럼 보일 뿐, 아직 array가 아닌 string이다.

- array의 모습으로 저장된 string을 array의 모습으로 되돌려서 활용하려면 JSON.parse 메서드를 활용하면 된다.

const savedTodo = localStorage.getItem("todos");

if (savedTodo) {

    const parsedTodo = JSON.parse(savedTodo);

}

위 코드의 parsedTodo를 사용하면 이제 우리가 원하는 array 형태의 값을 불러올 수 있다.

- 이제 새로고침을 해도 값이 사라지지 않고, input을 추가해도 기존 데이터가 남아있도록 코드를 수정하자.

if (savedTodo) {

    const parsedTodo = JSON.parse(savedTodo);

    todos = parsedTodo;

    parsedTodo.forEach(paintTodo);

}

forEach() 메서드는 앞에 받은 요소를 () 안의 함수에 하나씩 차례대로 넣어주는 역할을 한다. 그렇기에 새로고침을 해도 브라우저에서 값이 사라지지 않고 계속 나타난다.
 하지만 forEach() 메서드만 사용할 경우, localStorage에 저장된 값은 브라우저가 새로고침 된 후 새로운 값이 input되면 초기화가 된다.

이를 해결하기 위해 todos = parsedTodo; 를 사용한다. 위에서 todos 리스트를 변수 선언할 때 let을 사용했기 때문에 아래 코드에서 기존 데이터를 갖고 있는 상태(parsedTodo)로 todos를 업데이트 할 수 있다. 그렇게 하지 않으면 todos는 브라우저에서 새로고침 될 때 마다 초기값인 빈칸으로 돌아가게 된다.

- 이제 내가 브라우저에서 X 버튼을 눌렀을 때, 브라우저 상에서만 사라지는 게 아니라 localStorage에서도 삭제되도록 코드를 추가한다.
 위에서 언급했듯이 우리가 만약 완전히 일치하는 input 값을 데이터로 갖고 있을 경우, 우리가 원하는 값 하나만을 삭제하는 건 불가능하다. 그렇기에 각 input값 마다 난수로 만들어진 id를 추가해야 한다.

-> todoSubmit() 함수 내부

todoInput.value = "";

const newTodoObj = {

    id : Date.now(),

    text : newTodo

};

todos.push(newTodoObj);

paintTodo(newTodoObj);

-> paintTodo() 함수 내부

const li = document.createElement("li");

li.id = newTodo.id;

const span = document.createElement("span");

span.innerText = newTodo.text;

-> deleteTodo() 함수 내부

todos = todos.filter(todo => todo.id !== parseInt(li.id));

saveTodo();

 3개의 함수 내부에 코드가 추가돼서 내용이 조금 길다. 순차적으로 todoSubmit 함수에서는 text만 있던 newTodo에 id를 추가해서 object 형태로 변환시켰다. 그리고 바뀐 형태를 todos.pust와 paintTodo에 넣어준다.

 paintTodo 함수에서는 object 형태로 받은 newTodoObj를 분리 시켜서 id는 li에 넣고, text는 span에 내용으로 추가했다. 이렇게 나눌 경우 삭제를 원할 경우 span의 parentElement인 li의 id를 찾아서 삭제하기가 용이하다.

 deleteTodo 함수는 filter 메서드를 활용해서 기존 데이터에서 false가 난 값을 제외하고, 새로운 todos를 덮어 씌운 후 localStorage.setItem을 한 거다. filter 메서드의 경우 forEach 메서드 처럼 받은 요소를 각각 함수 내부에 대입한 후, true가 나온 값만을 출력해준다. 따라서 위처럼 todos에서 받은 todo.id 와 deleteTodo 내부에서 remove되는 li.id의 값이 일치할 경우 false가 반환 되고, 해당 값이 제외된 형태로 새로운 todos가 업데이트 되는 거다.

 parseInt() 를 해준 이유는 todo.id는 number의 형태인데, li.id는 html의 요소이기 때문에 string 형태를 띄기 때문에 type을 일치시키기 위함이다.

이상으로 todo-list 만들기가 끝났다. 최종 코드는 아래와 같다.



이렇게 여러 함수가 동시에 사용되는 건 처음 작성해 봐서 조금 어려운 감이 있었다. 하지만 다른 코드들과 마찬가지로 글로 정리하다 보니 가닥이 잡힌 것 같다. 작성한 글을 읽으면서 한 번 더 곱씹어 보고 내 것으로 만들어야겠다.

"기록은 기억을 이기지 못한다" 계속 힘내자!



댓글