todolist

TaejoonPark·2021년 10월 9일
0

데이터 받아오고 렌더링

작성하게 된 계기
DOM과 이벤트를 다루는 방법을 손에 익게 하기 위해 코드를 여러번 써봤는데 다음에 복습하기 위해서 정리해두면 좋을 것 같아서 별도로 정리해둔다.

DOMContentLoaded

DOM이 완전히 생성됬을 때 todos데이터를 서버에서 받아왔다고 가정하자.

// State =======================================
let todos = [];

// State Function ==============================
const fetchTodos = () => {
  todos = [
    { id: 1, content: 'Javascript', completed: false },
    { id: 2, content: 'CSS', completed: true },
    { id: 3, content: 'HTML', completed: false }
  ];
};

window.addEventListener('DOMContentLoaded', fetchTodos);

렌더링

데이터를 받아왔을 때 ul안에 기본적인 list가 나오도록 하기 위해서 render함수를 만든다.

HTML

section .todo-app
	header .header
	section .main
		input type="checkbox" id="toggle-all" class="toggle-all"
		label for="toggle-all"
		ul .todo-list
			<!-- 이곳에 list -->
		footer .footer
	footer .info

Javascript

// state =======================================
let todos = [];

// DOM Nodes ===================================
const $todoList = document.querySelector('.todo-list');

// State Function ==============================

const render = () => {
  $todoList.innerHTML = todos
    .map(
      ({ id, content, completed }) => 
    `<li data-id="${id}">
      <div class="view">
        <input type="checkbox" class="toggle" ${completed ? 'checked' : ''}/>
        <label>${content}</label>
        <button class="destroy"></button>
      </div>
      <input class="edit" value="${content}" />
    </li>`).join('');
};

const fetchTodos = () => {
  todos = [
    { id: 1, content: 'Javascript', completed: false },
    { id: 2, content: 'CSS', completed: true },
    { id: 3, content: 'HTML', completed: false }
  ];
  render();
};

window.addEventListener('DOMContentLoaded', fetchTodos);

Main

전체토글

전체토글버튼을 눌렀을 때 todos객체의 completed를 전부 true나 false로 만든다.

label을 누르는 거지만 실제로는 input창에 체크되는거다. 이 input창에 ckecked 프로퍼티를 이용해서 true, false를 알아내고 true면 todos의 completed 키를 전부 true로 만드는 식으로 한다.

일단 구현해놓고 그 다음에 나눠주자.

// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
...

// State Function
...

// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$toggleAll.onchange = () => {
  todos = todos.map(todo => ({ ...todo, completed: $toggleAll.checked }));
  render();
};

State Function 부분과 Event Binding으로 나누어 준다.

// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
...

// State Function
...
const toggleAllTodosCompleted(completed => {
  // todos = todos.map(todo => ({...todo, completed: completed}));
  todos = todos.map(todo => ({...todo, completed}));
  render();
})

// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$toggleAll.onchange = () => {
  toggleAllTodosCompleted($toggleAll.checked);
};

그런데 계속 todos에 할당하는 부분과 render함수를 호출해주는 부분이 반복된다. 따로 함수로 빼주자. 그 다음에 정리하면 한결 깨끗해졌다.

// State =======================================
let todos = [];

// DOM Nodes ===================================
const $toggleAll = document.querySelector('.toggle-all');
const $todoList = document.querySelector('.todo-list');

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  todos = newTodo;
  render();
};

const fetchTodos = () => {
  setTodos([
    { id: 1, content: 'Javascript', completed: false },
    { id: 2, content: 'CSS', completed: true },
    { id: 3, content: 'HTML', completed: false }
  ]);
};

const toggleAllTodosCompleted = completed => {
  setTodos(todos.map(todo => ({ ...todo, completed })));
};

// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$toggleAll.onchange = () => {
  toggleAllTodosCompleted($toggleAll.checked);
};

todo 항목 추가

input에 추가한 값을 value 프로퍼티로 받아와서 todos에 추가한다.

// State =======================================
let todos = [];

// DOM Nodes ===================================
const $newTodo = document.querySelector('.new-todo');
...

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => Math.max(...todos.map(todo => todo.id)) + 1

const addTodos = content => {
  setTodos([{id: generateTodoId(), content, completed: false}, ...todos])
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
  if (e.key !== 'Enter') return;
  addTodos(e.target.value)
}  

$toggleAll.onchange = () => {
  ...
};

이렇게 하면 content가 비어있을 때 빈 공백으로 todo가 추가되는 문제가 있고 추가하고 나서 그대로 input창에 value가 그대로 남아있는 문제가 있다. 수정해준다.

$newTodo.onkeyup = e => {
  if (e.key !== 'Enter') return;
  const content = e.target.value;
  if(content) addTodos(content)

  e.target.value = '';
}  

목록에 아무것도 없을 때

hidden 클래스를 추가해주면 display: none처리가 되있다. todo 리스트 개수가 0개일 때 hidden 클래스를 추가해주고 0개가 아닐 때는 hidden클래스를 제거하는 방법으로 toggle을 사용한다.

위에서 우선적으로 해주는게 좋은데 지나치는 바람에 이 타이밍에 작업했다.

// State =======================================
let todos = [];

// DOM Nodes ===================================
...
const $main = document.querySelector('.main');
const $footer = document.querySelector('.footer');

// State Function ==============================
const render = () => {
  ...
  [$main, $footer].forEach($el => $el.classList.toggle('hidden', todos.length === 0));
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};

항목별 토글

// State =======================================
let todos = [];

// DOM Nodes ===================================
...

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  setTodos(todos.map(todo => todo.id === +id ? {...todo, completed: !todo.completed} : todo));
}
  
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  if(!e.target.classList.contains('.toggle')) return;
  toggleTodosCompleted(e.target.closest('li').dataset.id);
}

항목 수정하기

matches가 아직 익숙하지 않아서 주의깊게 봐야한다.

두 가지 기능을 추가해야 한다. 더블클릭으로 편집으로 진입했을 때와 Enter를 눌렀을 때 입력했었던 값으로 리렌더링되도록 해야 한다.

// State =======================================
let todos = [];

// DOM Nodes ===================================
...

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  ...
}

const updateTodoContent = (id, content) => {
  setTodos(todo.map(todo => todo.id === +id ? {...todo, content} : todo));
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  ...
}
  
$todoList.dblclick = e => {
  if(!e.target.matches('.view > label')) return;
  e.target.closest('li').classList.add('.editing');
}

$todoList.onkeyup = e => {
  if(e.key !== 'Enter') return;
  updateTodoContent(e.target.closest('li').dataset.id, e.target.value)
}

e.target.value에서 live객체와 live객체가 아닌 것의 차이를 볼 수 있었다.

만약 더블클릭해서 데이터를 Javascript 에서 123123 으로. 수정하고 엔터를 눌렀을 때 아래 콘솔을 보자.

$todoList.onkeyup = e => {
  if(e.key !== 'Enter') return;
  updateTodoContent(e.target.closest('li').dataset.id, e.target.value)
  console.log(e.target.value); // 123123
  console.log(e.target.getAttribute('value')); // Javascript
}

항목 삭제하기

// State =======================================
let todos = [];

// DOM Nodes ===================================
...

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  ...
}

const updateTodoContent = (id, content) => {
  ...
}
  
const removeTodo = id => {
  setTodos(todos.filter(todo => todo.id !== +id))
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  ...
}
  
$todoList.dblclick = e => {
  ...
}

$todoList.onkeyup = e => {
  ...
}
  
$todoList.onclick = e => {
  if(!e.target.classList.contains('destroy')) return;
  removeTodo(e.target.closest('li').dataset.id);
}

좌측 하단 아직 미완료한 todo 개수 카운팅

// State =======================================
let todos = [];

// DOM Nodes ===================================
...
const $todoCount = document.querySelctor('.todo-count')

// State Function ==============================
const render = () => {
  ...
  
  const activeTodos = todos.filter(todo => !todo.completed).length;
  $todoCount.textContent = `${activeTodos} ${activeTodos > 1? 'items' : 'item'} left`
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  ...
}

const updateTodoContent = (id, content) => {
  ...
}
  
const removeTodo = id => {
  ...
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  ...
}
  
$todoList.dblclick = e => {
  ...
}

$todoList.onkeyup = e => {
  ...
}
  
$todoList.onclick = e => {
  ...
}

ALL, Active, Completed 나눠서 보기

// State =======================================
let todos = [];
let currentFilter = 'all';

// DOM Nodes ===================================
...
const $filters = document.querySelector('.filters');

// State Function ==============================
const render = () => {
  
  const _todos = todos.filter(todo => 
    currentFilter === 'completed'
    ? todo.completed
    : currentFilter === 'active'
    ? !todo.completed
    : true
    );
  
  todoList.innerHTML = _todos
    .map(
      ({ id, content, completed }) =>
        `<li data-id="${id}">
            <div class="view">
              <input type="checkbox" class="toggle" ${completed ? 'checked' : ''}/>
              <label>${content}</label>
              <button class="destroy"></button>
            </div>
            <input class="edit" value="${content}" />
          </li>`).join('');
  
  [$main, $footer].forEach($el => $el.classList.toggle('hidden', todos.length === 0));
  
  const activeTodos = _todos.filter(todo => !todo.completed).length;
  $todoCount.textContent = `${activeTodos} ${activeTodos > 1? 'items' : 'item'} left`
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const setFilter = newFilter => {
  currentFilter = newFilter;
  render();
}

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  ...
}

const updateTodoContent = (id, content) => {
  ...
}
  
const removeTodo = id => {
  ...
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  ...
}
  
$todoList.dblclick = e => {
  ...
}

$todoList.onkeyup = e => {
  ...
}
  
$todoList.onclick = e => {
  ...
}
  
$filters.onclick = e => {
  if(!e.target.matches('filters')) return;
  [...filters.querySelectorAll('a')].forEach($a => $a.classList.toggle('selected', $a.id === e.target.id))
  setFilter(e.target.id);  
}

이미 완료한 todo 삭제

// State =======================================
let todos = [];
let currentFilter = 'all';

// DOM Nodes ===================================
...
const $clearCompleted = document.querySelector('.clear-completed');

// State Function ==============================
const render = () => {
  ...
};

const setTodos = newTodo => {
  ...
};

const fetchTodos = () => {
  ...
};

const generateTodoId = () => ...

const setFilter = newFilter => {
  ...
}

const addTodos = content => {
  ...
}
  
const toggleAllTodosCompleted = completed => {
  ...
};

const toggleTodosCompleted = id => {
  ...
}

const updateTodoContent = (id, content) => {
  ...
}
  
const removeTodo = id => {
  ...
}
  
const removeAllCompletedTodos = () => {
  setTodos(todos.filter(todo => !todo.completed))
}
// Event Binding ==============================
window.addEventListener('DOMContentLoaded', fetchTodos);

$newTodo.onkeyup = e => {
	...
}  

$toggleAll.onchange = () => {
  ...
};
  
$todoList.onchange = e => {
  ...
}
  
$todoList.dblclick = e => {
  ...
}

$todoList.onkeyup = e => {
  ...
}
  
$todoList.onclick = e => {
  ...
}
  
$filters.onclick = e => {
  ...
}
  
$clearCompleted.onclick = removeAllCompletedTodos

정리 하면서 깨달은 점
html상에서 순서대로 작업하면 JS코드를 순서신경쓰면서 다시 위치를 바꾸는 등 번거로워지는 작업이 줄어 들 수 있다.
한 개의 파일이 아니라 모듈을 이용해서 나누는 중요성을 알게 된 것 같다.
작업을 하면서 스크롤을 끝과 끝으로 이동을 하는데 가독성도 좋지 않고 불편했다.
그래도 여러번 쳐보면서 얻은 지식이 많았다. 단순히 볼 때 이해했다고 생각한 부분들이 실제로 쳐보면 전혀 그렇지 않다는 걸 알았다. 손으로 여러번 치다보니까 손이 기억하는 부분들도 있었는데 1~2주 시간을 두고 다시한번 쳐서 완전히 내 것으로 만드는 것이 중요할 것 같다.

profile
공유하는 것을 좋아하는 프론트엔드 개발자

0개의 댓글

관련 채용 정보