본문 바로가기

Javascript/응용프로젝트

[1] 자바스크립트로 투두리스트(todo-list) 만들기

 

자바스크립트로 투두리스트 (todo-list) 만들기 1탄입니다.

강의를 보고 난 후 혼자 정리하는 느낌이라.. 조금 조잡하고 설명이 정확하지 않을 수 있습니다
HTML,CSS는 강의자료로 받았고, 기능만 구현했습니다.

어떻게 기능을 구현했는지를 보시는게 좋을 것 같습니다. (개발환경이 다를 것 같기에)


 

완성본


기능 설명 (1탄)

1. 할일 추가

2. 할일 수정

3. 할일 지우기

4. 할일 카테고리별로 필터링


HTML 과 CSS

강의 자료이기때문에, 참고용으로 보시라고 캡쳐본으로 올립니다
* 모듈 번들러는 rollup 을 사용했습니다. 커스텀 설정은 제 github 에 있습니다.

index.html
style.scss


기초 작업

스크립트는 ES6 클래스 문법을 사용했습니다. ( 저도 아직 ES6 문법에 익숙치 않아서 기능 구현에 중점을 맞춰서 설명합니다 )

 

사용할 엘리먼트 변수 지정

  inputContainerEl; // 할일 추가와 카테고리들을 묶고 있는 요소
  inputAreaEl; // 할일을 작성하고 추가하는 요소들을 묶고 있는 요소
  todoInputEl; // 할일을 작성하는 input
  addBtnEl; // 할일을 추가하는 button
  todoContainerEl; // 투두리스트 container
  todoListEl; // 할일 들을 추가 할 공간의 부모 요소
  radioAreaEl; // 카테고리들을 묶고 있는 부모 요소
  filterRadioBtnEls; // 라디오 버튼으로 형성되어있는 카테고리 input 들

 

Element 가져오기

  assignElement() {
    this.inputContainerEl = document.getElementById("input-container");
    this.inputAreaEl = this.inputContainerEl.querySelector("#input-area");
    this.todoInputEl = this.inputAreaEl.querySelector("#todo-input");
    this.addBtnEl = this.inputAreaEl.querySelector("#add-btn");
    this.todoContainerEl = document.getElementById("todo-container");
    this.todoListEl = this.todoContainerEl.querySelector("#todo-list");
    this.radioAreaEl = this.inputContainerEl.querySelector("#radio-area");
    this.filterRadioBtnEls = this.radioAreaEl.querySelectorAll(
      'input[name="filter"]'
    );
  }

 

addEventListener 함수를 사용하는 코드 묶기

addEvent(){ 추후에 들어갈 addEventListener 함수 )

 

constructor 에 클래스 필드를 선언하고 초기화 해주기

  constructor(storage) {
    this.assignElement();
    this.addEvent();
	// 추후 더 추가될 예정임
  }

 


할 일 추가 기능

 

이벤트리스너 함수

  addEvent() {
    this.addBtnEl.addEventListener("click", this.onClickAddBtn.bind(this));
  }

 

onClickAddBtn () - 추가 버튼을 클릭 시 발생하는 이벤트

  onClickAddBtn() {
    if (this.todoInputEl.value.length === 0) {
      alert("내용을 입력해주세요");
      return;
    }
    const id = Date.now();
    // this.storage.saveTodo(id, this.todoInputEl.value); 3탄에서 함
    this.createTodoElement(id, this.todoInputEl.value);
  }
1. input.value (할일을 작성하는 Input) 의 아무런 값이 없다면 alert 메소드를 사용해, 사용자에게 내용을 입력하라고 함.
2. 할일이 추가 될 때마다 새로운 id 가 생성됨.
3. createTodoElement() 라는 할일이 추가 되었을 때 할일 리스트에 추가 할 엘리먼트들을 생성하는 함수를 호출해준다.
4. 파라미터로 id 와, input에 작성한 값을 보내준다.
Date.now()
- 1970년 1월 1일 0시 0분 0초부터 현재까지 경과된 밀리 초를 Number 형으로 반환해줌
- now()는 Date의 정적 메소드이기 때문에, 항상 Date.now() 처럼 사용해야 함
Date.now()
1672924948319

 

createTodoElement () - 추가한 할일들의 Element를 동적으로 생성해주는 함수

  createTodoElement(id, value, status = null) {
    const todoDiv = document.createElement("div");
    todoDiv.classList.add("todo");
    if (status === "DONE") {
      todoDiv.classList.add("done");
    }
    todoDiv.dataset.id = id;
    const todoContent = document.createElement("input");
    todoContent.value = value;
    todoContent.readOnly = true;
    todoContent.classList.add("todo-item");
    const fragment = new DocumentFragment();
    fragment.appendChild(todoContent);
    fragment.appendChild(
      this.createButton("complete-btn", "complete-btn", ["fas", "fa-check"])
    );
    fragment.appendChild(
      this.createButton("edit-btn", "edit-btn", ["fas", "fa-edit"])
    );
    fragment.appendChild(
      this.createButton("delete-btn", "delete-btn", ["fas", "fa-trash"])
    );
    fragment.appendChild(
      this.createButton("save-btn", "save-btn", ["fas", "fa-save"])
    );
    todoDiv.appendChild(fragment);
    this.todoListEl.appendChild(todoDiv);
    this.todoInputEl.value = "";
  }
1. 하나의 할 일 요소들을 묶는 element 를 "todoDiv" 라고 만들어준다.
2. "todoDiv" 에게 'todo' 라는 클래스를 추가해준다. (css 적용을 위해)
3. 할 일의 상태가 완료('DONE')되었다면 todoDiv에게 "done" 이라는 클래스를 추가해준다.
4. todoDiv의 data-id에 id 값을 넣어준다 (data-id 도 동적으로 추가해주는 것)
5. 작성한 할 일을 읽거나 수정할 수 있도록 input element 를 만들어준다.
6. 할 일 input 에 value 값은 onClickAddBtn() 에서 가져온 todoInputEl.value 값을 넣어준다.
7. 할 일 input 은 처음엔 수정하지 않을테니 readOnly 라는 속성을 추가해준다 (true 로 해야지 읽기만 가능)
8. 할 일 input 에 "todo-item" 이라는 클래스를 추가해준다.
9. fragment 를 만들어서, DOM에 생성하기 전에 먼저 fragmentDOM에 생성 될 엘리먼트들을 추가해준다.
10. fragment 에 먼저 할 일 Input (todoContent) 를 넣어준다.
11. 체크버튼, 수정버튼, 삭제버튼, 저장버튼 을 생성해주도록 한다.
12. todoDiv 에 만들어놓은 fragment 들을 모두 추가한다.
13. todoListEl (할 일들이 추가 될 공간)에 todoDiv 를 추가한다.
14. 추가가 완료된다면, todoInputEl 의 value 값을 빈 값으로 만들어서, 새로 작성하기 쉽게 만들어준다.
appendChild()
- 선택한 요소 안에 자식요소를 추가하는 메서드
- prepend() 메서드와 달리 맨 뒤에 위치하도록 추가됨

 

createButton() - 속성을 갖고 있는 버튼을 생성하는 함수

  createButton(btnId, btnClassName, iconClassName) {
    const btn = document.createElement("button");
    const icon = document.createElement("i");
    icon.classList.add(...iconClassName);
    btn.appendChild(icon);
    btn.id = btnId;
    btn.classList.add(btnClassName);
    return btn;
  }
1. button 이라는 엘리먼트를 btn 이라는 변수에 저장해서 생성한다.
2. icon 을 추가하기 위해서 i 라는 태그도 icon 이라는 변수에 저장해서 생성한다.
3. icon에 createTodoElement() 에서 파라미터로 보내준 class name 을 추가해준다. (2가지이기때문에 스프레드 신택스를 사용)
4. btn 의 자식 요소에 icon 을 추가해준다.
5. 버튼의 아이디는 createTodoElement()에서 파라미터로 보내준 이름을 id로 주도록 함.
6. 버튼의 클래스 또한 위와 같이 추가함.
7. 만든 btn 을 반환해서 createTodoElement () 로 보내준다.

버튼들마다 이벤트 걸어주기

 

이벤트리스너 함수

  addEvent() {
	...
    this.todoListEl.addEventListener("click", this.onClickTodoList.bind(this));
  }

 

onClickTodoList () - 생성된 할 일에서 버튼을 클릭하면 발생하는 함수

  onClickTodoList(event) {
    const { target } = event;
    const btn = target.closest("button");
    if (!btn) return;
    if (btn.matches("#delete-btn")) {
      this.deleteTodo(target);
    } else if (btn.matches("#edit-btn")) {
      this.editTodo(target);
    } else if (btn.matches("#save-btn")) {
      this.saveTodo(target);
    } else if (btn.matches("#complete-btn")) {
      this.completeTodo(target);
    }
  }
1. 클릭한 target 을 먼저 event 에서 찾아 저장해준다.
2. button 안에는 svg 로 아이콘이 있기때문에, button을 클릭해도 아이콘을 클릭한 것처럼 이벤트 버블링이 생기기 때문에 svg가 target 으로 됨
=> closest() 메서드를 사용해 button 을 찾게 해준다.
3. 할 일 요소를 모두 묶고 있기 때문에 Input 을 클릭해도 이벤트가 발생한다
=> 조건문으로 클릭한 target 이 button 이 아니라면, 이벤트가 실행되지 않도록 한다.
4. 버튼들마다 실행 될 이벤트가 다르기 때문에 조건문을 사용해서 위와 같이 맞는 버튼을 찾아 함수를 호출해줍니다.
=> target 을 파라미터로 꼭 보내줍니다.
closest(selectors)
- 주어진 요소와 일치하는 요소를 찾을 때까지, 자기 자신을 포함해 위쪽 (부모 방향, 문서 루트까지)으로 문서 트리를 순회함
- 가장 가까운 element 또는 자기 자신, 일치하는 요소가 없으면 null 을 반환함.
matches(selectors)
- 기준 element가 css 선택자 ( ex. #id, .class, [href = https] 등의 선택자 ) 선택이 되는지 여부를 확인하는 메서드
- selectors : CSS 선택자와 동일한 표현식의 문자열
- 반환값은 boolean 값으로 선택되면 true, 선택되지않으면 false

삭제하는 이벤트

 

deleteTodo() - 할 일을 삭제해주는 이벤트

  deleteTodo(target) {
    const todoDiv = target.closest(".todo");
    todoDiv.addEventListener("transitionend", () => {
      todoDiv.remove();
    });
    todoDiv.classList.add("delete");
    // this.storage.deleteTodo(todoDiv.dataset.id); 3탄에서 함
  }
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다.
2. target 의 todoDiv에게 이벤트 리스너를 실행해준다
=> "transitionend" 라는 이벤트를 주어 , 애니메이션이 완료되면 target의 todoDiv (즉 할 일) 을 삭제해주도록 함
3. target의 todoDiv에게 delete 라는 클래스를 추가해, 이벤트를 주도록 하며 서서히 사라지게 해줌
addEventListener("transitionend")
- transition이 완료된 이후에 발생하는 이벤트, transition 완료를 감지한다.
- transition과 함께 사용하는 함수
- addEventListener를 사용하여 이벤트 모니터링 가능
remove()
- 선택한 요소를 DOM 트리에서 제거하는 메서드
- 삭제된 요소와 연관된 데이터나 이벤트도 같이 삭제됨

수정하는 이벤트

 

editTodo() - 수정 버튼을 클릭하면 발생하는 이벤트

  editTodo(target) {
    const todoDiv = target.closest(".todo");
    const todoInputEl = todoDiv.querySelector("input");
    todoInputEl.readOnly = false;
    todoInputEl.focus();
    todoDiv.classList.add("edit");
  }
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. 읽기 전용으로 되어있던 할 일이 적어져있는 input 을 가져와준다. (todoInputEl)
3. input의 읽기모드를 false 해주어, 수정 가능한 input 으로 바꾸게 해준다.
4. 읽기모드가 false 된다면, input 에 focus 될 수 있도록 focus() 메서드를 사용해준다.
5. todoDiv 에게 edit 라는 클래스를 추가해주어, edit 모드의 css 가 적용될 수 있도록 해준다.

 

saveTodo() - 수정 완료 버튼을 클릭하면 발생하는 이벤트

  saveTodo(target) {
    const todoDiv = target.closest(".todo");
    todoDiv.classList.remove("edit");
    const todoInputEl = todoDiv.querySelector("input");
    todoInputEl.readOnly = true;
    // const { id } = todoDiv.dataset; 3탄에서 함
    // this.storage.editTodo(id, todoInputEl.value); 3탄에서 함
  }
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. edit 클래스를 삭제해주어, edit 모드의 css 를 적용하지 않게 해준다.
3. 수정이 가능한 input 을 가져와준다. (todoInputEl)
4. 수정모드에서 읽기모드로 다시 변경해주도록 함

할 일 체크 이벤트

 

completeTodo() - 체크 버튼을 클릭하면 발생하는 이벤트

  completeTodo(target) {
    const todoDiv = target.closest(".todo");
    todoDiv.classList.toggle("done");
    // const { id } = todoDiv.dataset;
    // this.storage.editTodo(
    //   id,
    //   "",
    //   todoDiv.classList.contains("done") ? "DONE" : "TODO"
    // ); 3탄
  }
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. todoDiv에게 done 이라는 클래스를 추가해, 다른 할 일과 차별화를 줌 (css적으로)
=> 체크를 풀었다가 체크를 했다가 할 수 있도록 toggle 을 사용함

할 일 상태 카테고리별로 필터링

 

이벤트리스너 함수

  addEvent() {
	...
    this.addRadioBtnEvent();
  }

 

addRadioBtnEvent() - 카테고리 버튼들에게 클릭 이벤트를 주는 함수

  addRadioBtnEvent() {
    for (const filterRadioBtnEl of this.filterRadioBtnEls) {
      filterRadioBtnEl.addEventListener(
        "click",
        this.onClickRadioBtn.bind(this)
      );
    }
  }
1. for 문을 통해, 3개의 카테고리 버튼을 가져와 하나씩 클릭 이벤트를 주도록한다

 

onClickRadioBtn() - 카테고리 클릭하면 필터링 되는 이벤트

  onClickRadioBtn(event) {
    const { value } = event.target;
    this.filterTodo(value);
    // window.location.href = `#/${value.toLowerCase()}`; 2탄
  }
1. event.target.value 를 value 라는 이름으로 저장해준다.
=> 아래와 같이 클릭한 카테고리 버튼의 value 값이 나옴
ALL
TODO
DONE​

2. filterTodo() 라는 함수를 만들어서 호출해줌

 

filterTodo() - 필터링 해주는 함수

  filterTodo(status) {
    const todoDivEls = this.todoListEl.querySelectorAll("div.todo");
    for (const todoDivEl of todoDivEls) {
      switch (status) {
        case "ALL":
          todoDivEl.style.display = "flex";
          break;
        case "DONE":
          todoDivEl.style.display = todoDivEl.classList.contains("done")
            ? "flex"
            : "none";
          break;
        case "TODO":
          todoDivEl.style.display = todoDivEl.classList.contains("done")
            ? "none"
            : "flex";
          break;
      }
    }
  }
1. 추가 된 할 일들을 todoDivEls 라는 변수로 담아준다. (배열로 담아짐)
2. for 문을 사용해서 할 일들 하나하나에게 switch 문을 통해 상태에 따라 카테고리에 보여지는 모습을 정해준다.
- case "ALL" 일 때
: display 가 원래 none 으로 되어있는데 모두 "flex" 로 해서, 보이도록 한다.
- case "DONE" 일 때
: 할 일에게 "done" 이라는 클래스가 포함되어있다면 "flex"를 주어 "DONE" 카테고리에서 보이도록 한다.
- case "TODO" 일 때
: 할 일에게 "done" 이라는 클래스가 포함되어있다면 "none" 으로 주어 현재 진행중인 할 일만 보여주도록 함.
for ...of 반복문
- ES6에 추가된 새로운 컬렉션 전용 반복 구문
- 반복이 가능한 객체의 모든 원소를 하나씩 추출하여 변수에 담아 반복문을 수행하는 문법

전체 기능 구현 코드

addEventListener("DOMContentLoaded")
- 브라우저가 HTML을 전부 읽고 DOM Tree 를 완성하는 즉시 발생
- DOM Tree 가 다 만들어진 후에 DOM에 접근이 가능하기때문에, DOM이 생성되기전 DOM을 조작하는 자바스크립트 코드가 실행되어 원하지 않는 결과를 내는것을 막을 수 있다.
- 로딩 측면에서 DOMContentLoaded가 우위에 있음