주녁, DevNote
article thumbnail

개요

모던 자바스크립트의 구조를 학습하고 컴포넌트 기반의 코드를 작성한다.

목표

순수 Javascript로 작성된 Todo List 어플리케이션을 작성한다.

(이 글은 Next Step에서 진행한 js-todo-list-step을 따라 작성되었습니다.)

 

전체 코드는 여기에서 보실 수 있습니다.

 


여정

TodoCount 기본 뼈대 작성

TodoCount 컴포넌트는 3가지 기능을 가지고 있어야 한다.

  • 필터링 : 3가지 필터를 설정할 수 있다(전체보기, 해야할 일, 완료한 일)
  • 할 일 카운트 : 현재 표시된 할 일의 갯수를 표시한다
  • 전체 삭제 : 할 일 전체를 삭제한다.

일단 컴포넌트 뼈대부터 잡으면 아래와 같다.

// TodoCount.js
import Component from "../core/Component.js";
import { store } from "../store/index.js";


export default class TodoCount extends Component {
    initState() { return {}; }
    mounted() {
        // 컴포넌트가 마운트된 후에 동작한다.
    }
    template() {
        return `
            <span class="todo-count">
                총 <strong>0</strong> 개
            </span>
                <ul class="filters">
                    <li>
                        <a href="#" class="all selected">전체보기</a>
                    </li>
                    <li>
                        <a href="#active" class="active">해야할 일</a>
                    </li>
                    <li>
                        <a href="#completed" class="completed">완료한 일</a>
                    </li>
                </ul>
            <button class="clear-completed">모두 삭제</button>
        `;
    }

    setEvent() {

    }
}

 

우선, 다른 기능에 영향을 미치는 필터링 기능을 만들어보자.


완료 체크박스 이벤트 구현

필터링 기능에 앞서,

우리는 완료 체크박스를 눌렀을 때 동작을 구현하지 않았다.

빠르게 작성한 결과만 보도록 하자.

// reducer.js

let initialState = {
    users: [],
    todos: [],
    filter: FILTER_TYPE.ALL, // 추가
}

const reducer = (state = initialState, action = {}) => {
    switch (action.type) {
    	...
        case CHANGE_FILTER:
        return {
            ...state,
            filter: action.payload.filter,
        }

먼저, Reducer에 filter라는 상태를 추가하고 연산 정의해준다.

 

이어서 TodoList에 클릭 이벤트를 구현해주면 된다.

// TodoList.js
...
const TodoItem = (todo) => {
    const { id, content, completed, } = todo;
    return `
        <li data-id="${id}" class="${completed ? 'completed' : ''}"> // class 추가
			...
        </li>
    `;
}

export default class TodoList extends Component {
	...
    setEvent () {
        this.onClickCompleteButton();
    }
	...
    onClickCompleteButton() {
        this.addEvent('click', '.toggle', (event) => {
            const todoItem = this.getTargetTodoItem(event);
            this.toggleTodoComplete(todoItem);
            this.render();
        })
    }
    toggleTodoComplete(todoItem) { 
    	store.dispatch(toggleTodoComplete(todoItem.dataset.id)); 
    }
    ...

 

기존에 작성했던 추가, 삭제 기능과 크게 다르지 않다.

이제 완료 체크박스를 클릭하면

할일 label에 취소선이 그어진다.

 

계속 진행해보자.


필터링 기능 구현

어려워보이지만, 차근차근 분리해서 구현하면 할만하다.

 

템플릿 함수부터 분리해서 구현해보자.

// TodoCount.js
template() {
    const { todos, filter } = store.getState();
    return `
        ...
        <ul class="filters">
            ${renderFilterList(filter)}
        </ul>
        ...
    `;
}

앞서 filter를 구현했으니 store에서 불러올  수 있다.

이 필터값은 현재 선택된 필터를 나타낸다.

즉, 선택된 필터는 선택되었음을 표시하고 있어야 한다.

 

필터링 버튼을 그려주는 함수를 작성하여 한번 더 분리해보자.

// TodoCount.js
const renderFilterList = (filter) => {
    return `
        <li>
            <a href="#" class="${setFilter(FILTER_TYPE.ALL, filter)}">전체보기</a>
        </li>
        <li>
            <a href="#active" class="${setFilter(FILTER_TYPE.ACTIVE, filter)}">해야할 일</a>
        </li>
        <li>
            <a href="#completed" class="${setFilter(FILTER_TYPE.COMPLETED, filter)}">완료한 일</a>
        </li>
    `;
}
const setFilter = (filterType, filter) => {
    return filterType === filter ? filterType + ' selected' : filterType;
}

현재 filter 값에 따라서 필터 타입과 "selected" 클래스 속성을 부여해주고 있다.

 

이제 이벤트를 추가해보자.

// TodoCount.js
setEvent() {
    this.onClickFilter();
}
onClickFilter() {
    this.addEvent('click', '.filters a', (event) => {
        const filter = event.target.className;
        this.changeFilter(filter);
        this.render();
    })
}
changeFilter(filter) {
    store.dispatch(changeFilter(filter));
}

클래스 속성을 가져와서 단순하게 store에 저장하고 있다.

 

하지만 아직 화면은 바뀌지 않는다.

할일 목록을  그려주는 TodoList에서 필터에 따라 그려주지 않았기 때문이다.

// TodoList.js
const filterTodoList = (todos, filter) => {
    return todos && todos.filter((todo) => isFilteredTodo (filter, todo));
}

export default class TodoList extends Component {
	...
    template () {
        const { todos, filter } = store.getState();
        const filteredTodos = filterTodoList(todos, filter);
        
        return `
            <ul class="todo-list">
                ${renderTodoList(filteredTodos)}
            </ul>
        `;
    }
    
// utils/filter.js
const isFilteredTodo = (filter, todo) => {
    switch (filter) {
        case FILTER_TYPE.ACTIVE:
            return !todo.completed;
        case FILTER_TYPE.COMPLETED:
            return todo.completed;
        case FILTER_TYPE.ALL:
            return true;
    }
}

필터값에 따라서 TodoList를 렌더링 하도록 변경하였다.

 

이제 실행해보면, 필터값에 따라 할일이 보여지는 것을 확인할 수 있다.

체크박스를 눌러도 잘 동작하는지도 확인하자.

완료한 일 필터링 화면

 

 

자, 이제 카운트 기능과, 전체 삭제 기능이 남아있다.

희망적인 소식은 남은 두 기능은 아주 쉽다는 것이다.


카운트 기능 구현

 

카운트 기능 구현은 아래와 같이 구현할 수 있다.

// TodoCount.js

const renderTodoCount = (todos) => {
    const count = todos && todos.filter(todo => isFilteredTodo (filter, todo)).length;
    return '총 <strong>' + count + '</strong> 개';
}

export default class TodoCount extends Component {
    ...
    template() {
        const { todos, filter } = store.getState();
        return `
            <span class="todo-count">
                ${renderTodoCount(todos, filter)}
            </span>
            ...
        `;    
    }

TodoList에서 작성했던 것과 같이필터링된 목록의 갯수만 세어서 표시하면 된다.

 

전체삭제 기능 구현

전체 삭제 기능은

단순 클릭 이벤트만 구현하면 된다.

// TodoCount.js    
    setEvent() {
        ...
        this.onClickDeleteAllButton();
    }
    ...
    onClickDeleteAllButton() {
        this.addEvent('click', '.clear-completed', (event) => {
            if(!confirm('정말 삭제하시겠습니까?')) { return; }
            store.dispatch(deleteAllTodo());
        })
    }

 

Reducer도 간단하게 Todos만 초기화 시켜주면 된다.

const reducer = (state = initialState, action = {}) => {
    switch (action.type) {
        ...
        case DELETE_ALL_TODO:
            return {
                ...state,
                todos: [],
            }

 

실행시켜보면

처음 의도했던 3가지 기능이 정상적으로 동작하는 것을 볼 수 있다.

필터링과 카운트가 모두 동작하는 모습


마무리

여기까지 오느라 정말 고생많았다.

기본적인 컴포넌트, Reducer 작성부터

User, Todo 컴포넌트 작성까지 초기 기능은 모두 구현했다.

 

다음 시간에는 

User마다 Todo Item을 소유할 수 있도록 구조를 리팩토링해보자.


 

profile

주녁, DevNote

@junwork

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!