ToDo List 만들기

bp.chys·2020년 5월 28일
0
post-thumbnail

Todo List 를 만들어보자.

기능 요구사항

✅ todo list에 todoItem을 키보드로 입력하여 추가하기
✅ todo list의 체크박스를 클릭하여 complete 상태로 변경. (li tag 에 completed class 추가)
✅ todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
✅ todo list를 더블클릭했을 때 input 모드로 변경. (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
✅ todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
✅ todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기

컴포넌트 구성

Redux 창시자 Dan Abramov-Todo list 예제를 따라서 실습을 진행했다.

최상위 컴포넌트(TodoApp)에 TodoItems 라는 상태를 정의하고 해당 상태를 하위 컴포넌트에서 내려 받아 사용하는 방식이다.

이렇게 한 화면에서 상태를 공유할 수 있는 관리방법으로 setStaterender 함수를 사용한 패턴이 많이 사용된다.

직접 만들어보자

각 요구사항에 따라 핵심 기능을 추가하면서 진행해보자

1. todo list에 todoItem을 키보드로 입력하여 추가하기

가장 먼저 TodoItems라는 이름의 배열 상태를 갖는 최상위 컴포넌트를 정의한다.

function TodoApp() {
    const $todoList = document.querySelector('#todo-list')

    this.todoItems = []  // todoItems 를 배열의 상태로 갖고 있다. 
    this.setState = updatedItems => {
      this.todoItems = updatedItems
    }
}

입력을 위한 TodoInput 함수 컴포넌트를 추가한다.

function TodoInput({onAdd}) {
  const $todoInput = document.querySelector('#new-todo-title')
  
  $todoInput.addEventListener('keyup', event => 
                              this.addTodoItem(event))
  
  this.addTodoItem = event => {
    $target = event.target
    if (this.isValid(event, $target.value)) {
      event.preventDefault()
      onAdd($target.value)
      $target.value = ""
    }
  }
  
  this.isValid(event, value) {
    return event.key === 'Enter' && value.trim() !== ""
  }
}
  • 컴포넌트가 가지고 있는 엘리먼트에 이벤트 리스너를 바인딩하고 콜백함수를 작성한다.
  • 인자로 받은 onAdd() 함수에 input값을 넣고 전달한다.

최상위 앱에서 onAdd() 함수에 TodoItems를 채워넣는 로직을 구현해보자.

function TodoApp() {
  const todoItems = []
  
  let id = 0
  
  new TodoInput({
    onAdd: contents => {
      const todoItem = new TodoItem(id++, contents, 'active')
      this.todoItems.push(todoItem)
      this.setState(this.todoItems)
    }
  })
}

새로운 todoItem을 받아서 추가했다면, 추가된 항목들을 화면에 띄우는 작업을 해보자.
TodoList라는 컴포넌트를 만들어서 render 함수를 구현하면 된다.

export function TodoList() {
  const $todoList = document.querySet('#todo-list')
  
  this.setState = updatedItems => {
    this.render(updatedItems)
  }
  
  this.render(todoItems) {
    const template = todoItems.map(TodoListTemplate)
    this.$todoList.innerHTML = template.join('')
  }
}

로직은 단순하다. setState함수의 인자로 들어온 upatedItems 데이터를 화면에 뿌려주는 역할을 한다. 이때 상위 앱에서 TodoList 인스턴스를 만들고 setState 함수를 호출해줘야 한다.

function TodoApp() {
  const $todoList = document.querySelector('#todo-list')
  
  const todoItems = []
  
  this.setState = updatedItems => {
    this.todoItems = updatedItems
    $todoList.setState(updatedItems)
  }
  
  new TodoInput({
    onAdd: contents => {
      const todoItem = new TodoItem(id++, contents, 'active')
      this.todoItems.push(todoItem)
      this.setState(this.todoItems)
    }
  })
}

이렇게 하면 input 창에 새로운 항목을 입력하고 enter를 입력하면 list에 항목이 추가된 것을 볼 수 있다.

2. todo list의 체크박스를 클릭하여 complete 상태로 변경

체크박스를 클릭하면 css를 변경함과 동시에 클릭된 TodoItem의 상태를 completed로 변경하도록 구현해보자. 여기서는 이벤트 위임을 사용해야 한다.

먼저 checkbox 컴포넌트를 만들고 엘리먼트에 이벤트 리스너를 등록하자.

export function TodoCheckBox({onCheck}) {
    const $todoList = document.querySelector('#todo-list')

    $todoList.addEventListener('click', event => 
                               this.clickCheckbox(event))

    this.clickCheckbox = event => {
        const $target = event.target
        if ($target.type !== 'checkbox') {
            return;
        }
        onCheck($target.closest('li').id)
    }
}
  • todoList엘리먼트에 콜백함수를 바인딩하고 TodoApp에 정의된 onCheck함수를 호출한다.
  • 'checkbox' type 엘리먼트를 선택했을 때만 반응한다.

TodoCheckBox를 인스턴스를 생성하고 onCheck메서드를 외부에 정의해보자.

function TodoApp() {
  const $todoList = document.querySelector('#todo-list')
  
  const todoItems = []
  
  this.setState = updatedItems => {
    this.todoItems = updatedItems
    $todoList.setState(updatedItems)
  }
  
  new TodoInput({
    onAdd: contents => {
      const todoItem = new TodoItem(id++, contents, 'active')
      this.todoItems.push(todoItem)
      this.setState(this.todoItems)
    }
  })
  
  new TodoCheckBox({
    onCheck: id => {
      const updatedItems = this.todoItems.map(
        item => item.swapCheckStatus(parseInt(id))
      )
      this.setState(updatedItems)
    }
  })
}

// TodoItem 에 swapCheckStatus() 함수 추가
export class TodoItem {
  constructor(id, content, status) {
    this.id = id
    this.content = content
    this.status = status
  }
  
  swapCheckStatus(id) {
    if (this.id === id && this.status === 'active') {
      this.status = 'completed'
    } else if (this.id === id && this.status === 'completed') {
      this.status = 'active'
    }
    return this
  }
}
  • TodoCheckBox 인스턴스를 만들어서 onCheck 함수를 정의했다.
  • TodoItem 클래스 안에서 스스로 상태를 바꿀 수 있도록 swapCheckStatus 함수를 추가했다.
  • updatedItems를 만든뒤 this.setState를 통해 상태 저장소를 업데이트해준다.

자 이렇게 하면 checkBox를 클릭하면 원하는대로 css가 토글이 되는 것을 확인할 수 있을 것이다.

3. todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제

이번에는 Todo항목을 삭제하는 기능을 추가해보자. checkbox를 클릭하면 특정 이벤트가 수행되는 것과 아주 비슷한 원리이므로 자세한 설명은 생략한다.

이번에는 x (destroy)버튼에 이벤트를 바인딩하기 위해서 해당 컴포넌트를 만들어보자.

export function TodoDestroy({onDestroy}) {
  const $todoList = document.querySelector('#todo-list')
  
  $todoList.addEventListener('click', event => 
                             this.onDestroy(event))
  
  this.onDestroy = event => {
    const $target = event.target
    if ($target.classList.contains('destroy') {
      event.preventDefault()
      if (confirm("삭제하시겠습니까?")) {
        onDestroy($target.closest('li').id)
      }
    }
  }
}

// TodoApp.js
// ... 추가
  new TodoDestroy({
    onDestroy: id => {
      const updatedItems = this.todoItems.filter(
        item => item.id !== parseInt(id))
      this.setState(updatedItems)
    }
  })
  • x버튼은 'destroy'라는 클래스 name을 가지고 있다.
  • 클래스 이름으로 엘리먼트를 찾고 onDestory 함수를 호출한다.
  • 파괴적인 행위는 confirm을 통해 사용자의 실수를 방지한다.
  • 현재 todoItem들 중 선택된 item을 제외하고 새로운 state로 등록한다.

4. todo list를 더블클릭했을 때 input 모드로 변경.

(단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀)

특정 리스트를 더블클릭했을 때, editing className을 토글할 수 있는 기능을 추가해보자.
li를 감싸고 있는 ul태그에 해당하는 #todo-list에서 조작이 일어나야하므로 TodoList안에 이벤트를 바인딩하자.

// TodoList.js
export function TodoList({}) {
  const $todoList = document.querySelector('#todo-list')
  
  this.setState(updatedItems) {
    return render(updatedItems)
  }
  
  this.render(items) {
    const template = items.map(TodoListTemplate)
    $todoList.innerHTML = template.join('')
  }  
  
  this.onChangeToEditMode = event => {
    event.preventDefault()
    $target = event.target
    $todoItem = $target.closest('li')
    $todoItem.classList.toggle('editing')
  }
  
  $todoList.addEventListener('dblclick', event => 
                             onChangeToEditMode(event))
}

이렇게 하면 'editing'이라는 클래스 네임이 토글되는 것까지 성공이다. 실제로 수정이 가능하게 만들고 esc를 누르면 다시 view 모드로 돌아가게 만들어보자.

이를 위해서 TodoList에 'keyup'에 해당하는 이벤트를 추가하고 함수를 추가한다. 전체 앱 state에도 반영해야하기 때문에 state를 변경하는 로직은 외부로 뺀다.

// TodoList.js
export function TodoList({onEditMode, onUpdate}) {
    const $todoList = document.querySelector("#todo-list");

    this.setState = updatedTodoItems => {
        this.render(updatedTodoItems);
    };

    this.render = items => {
        const template = items.map(TodoItemTemplate);
        $todoList.innerHTML = template.join("");
    }

    this.onChangeEditMode = event => {
        event.preventDefault()
        const $target = event.target

        const $todoItem = $target.closest('li')
        $todoItem.classList.toggle('editing')
        onEditMode($todoItem.id)
    }

    this.onFinishEditMode = event => {
        event.preventDefault()
        const $target = event.target

        if ($target && event.key === 'Escape') {
            document.getSelection().anchorNode.classList.remove('editing')
        } else if ($target && event.key === 'Enter') {
            const id = $target.closest('li').id
            onUpdate(id, $target.value)
        }
    }

    $todoList.addEventListener('dblclick', event => this.onChangeEditMode(event))
    $todoList.addEventListener('keyup', event => this.onFinishEditMode(event))
}

// TodoApp.js 에 TodoList 부분 변경
.....

    const todoList = new TodoList({
        onEditMode: id => {
            const updateItems = this.todoItems.map(item => {
                if (item.id === parseInt(id) && item.status === 'active') {
                    return new TodoItem(item.id, item.content, 'editing')
                }
                return item
            })
            this.setState(updateItems)
        },
        onUpdate: (id, value) => {
            const updateItems = this.todoItems.map(item => {
                if (item.id === parseInt(id) && item.status === 'editing') {
                    return new TodoItem(item.id, value, 'active')
                }
                return item
            })
            this.setState(updateItems)
        }
    })  // TodoList 함수 인스턴스 생성
....

5. todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기 + todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기

이제 item의 총 개수를 계산하는 함수를 만들어보자.
이는 TodoCount라는 컴포넌트를 만들어서 어플리케이션 실행 시 init에서 state를 전달받는 코드를 추가해보자.

// TodoApp.js
... 추가
  const todoCount = new TodoCount({
        selectedTodoItems: selectedTodoItems => {
            todoList.setState(selectedTodoItems)
        }
    })

    this.init = () => {
        todoCount.init()
        todoCount.setState(this.todoItems)
        todoList.setState(this.todoItems)
    }
...
}
// TodoCount.js
export function TodoCount({onSelectedGroup}) {
  const $count = document.querySelector('.todo-count')
  const $all = document.querySelector('.all')
  const $active = document.querySelector('.active')
  const $completed = document.querySelector('.completed-job')
  
  this.todoItems = []
  
  this.setState = items => {
    this.todoItems = [...items]
    this.render(this.todoItems)
  }
  
  this.removeSelected = () => {
    $all.classList.remove('selected')
    $active.classList.remove('selected')
    $completed.classList.remove('selected')
  }
  
  this.onShowSelectedItems = (event, element, status) => {
    event.preventDefault()
    this.removeSelected()
    element.classList.toggle('selected')
    let selectedItems = this.todoItems
    if (status === 'completed' || status === 'active') {
      selectecItems = this.todoItems.filter(
      item => item.status === status)
    }
    this.render(selectedItems)
    onSelectedGroup(selectedItems)
  }
  
  this.init() {
    $all.addEventListener('click', event => onShowAllItems(event, $all, ''))
    $active.addEventListener('click', event => onShowActiveItems(event, $active, 'active'))
    $completed.addEventListener('click', event => onShowCompletedItems(event, $completed, 'completed'))
  }
  
  this.render(items) {
    $count.innerHTML = TodoCountTemplate(items)
  } 
}

이렇게 하면 처음 작성한 요구사항을 모두 만족하는 TodoList를 만들 수 있다.
setState와 render를 통해서 상위 컴포넌트에 공통자원을 저장해놓고 필요할 때마다 가져다 쓰는 방식으로 구현할 수 있다.

이전 글에서는 store 객체를 따로 만들고 상태를 별도로 관리 했었는데 이번 글에서는 상위 컴포넌트에서 관리하는 방법을 사용해보았다.
다음 포스팅에서는 todo list에 Ajax 통신을 추가해보겠다.

profile
하루에 한걸음씩, 꾸준히

0개의 댓글