Vanilla ToDo

이번에는 toDo프로그램을 개발할 것인데 처음에 vanilla JS로 작성하고 다음은 reduc로 작성할 시간을 가질거다. 우선 HTML로부터 입력받은 text를 console로 출력하는 프로그램을 만들것이다.

import { createStore } from "redux";

const form = document.querySelector("form");
const input = document.querySelector("input");
const ul = document.querySelector("ul");

const reducer = (state = [], { type }) => {
  console.log(type);
};
const store = createStore(reducer);

const onSubmit = (e) => {
  e.preventDefault();
  const toDo = input.value;
  input.value = "";
  store.dispatch({ type: toDo });
};

form.addEventListener("submit", onSubmit);

위 코드를 해석하면 JS로 HTML 태그 정보를 가져와 form에 submit이 감지되면 onSubmit이 호출되어 input value를 dispatch에 전달하고 reducer는 전달 받은 action을 console을 출력하였다. 여기서 주로 다루어야 하는 데이터는 HTML에서 받은 input.value이기에 input.value를 target으로하는 store을 생성하여 개발을 진행한다.


AddToDos

HTML에서 받은 데이터를 저장하여 HTML 리스트에 추가하는 기능을 작성한다. dispatch로 input.value을 전달받고 reducer로 state를 변경하였다. 여기서 store.subscribe(() => paintToDos());으로 paintToDos를 호출하여 HTML에 리스트를 추가하는 기능을 구현하였다.

import { createStore } from "redux";

const form = document.querySelector("form");
const input = document.querySelector("input");
const ul = document.querySelector("ul");

const reducer = (state = [], { type }) => {
  return type;
};

const store = createStore(reducer);
store.subscribe(() => paintToDos());

const paintToDos = () => {
  const li = document.createElement("li");
  const toDos = store.getState();
  li.innerText = toDos;
  ul.appendChild(li);
};

const onSubmit = (e) => {
  e.preventDefault();
  const toDo = input.value;
  input.value = "";
  store.dispatch({ type: toDo });
};

form.addEventListener("submit", onSubmit);

HTML에 리스트를 추가하는것 까지는 좋았지만 여기서 문제점은 추가된 리스트 가지고 무엇도 할 수 없게된다. 왜냐하면 state를 추가, 수정, 삭제, 저장하는 함수가 reducer인데 reducer는 단순하게input.value로부터 받은 값으로 state를 저장하고 있기 때문이다. 즉, 우리는 이전 입력된 정보를 state에 저장할 필요가 있다. 그리고 그러한 기능을 reducer안에서도 구현할 것이다.

...
const ADD_TODO = "ADD_TODO";

const reducer = (state = [], { type, data }) => {
  switch (type) {
    case ADD_TODO:
      return [data, ...state];
    default:
      return state;
  }
};
...
const paintToDos = () => {
  ul.innerHTML = "";
  const toDos = store.getState();
  for (const toDo of toDos) {
    const li = document.createElement("li");
    li.innerText = toDo;
    ul.appendChild(li);
  }
};

const onSubmit = (e) => {
  e.preventDefault();
  const toDo = input.value;
  input.value = "";
  store.dispatch({ type: ADD_TODO, data: toDo });
};

입력받은 input.value를 저장하기위해서 reducer에 ADD_TODO기능을 추가하였다. onSubmit이 호출되면 reducer에 { type: ADD_TODO, data: toDo }형태로 전달하여 type으로 기능을동작을 정의하였고 정의된 기능에따라 전달받은 data를 state에 추가하여 state를 set하였다. 그리고 reducer 호출이 끝나면 paintToDos이 동작하여 state에 저장되었던 data를 li tag를 만들어 ui에 추가해 주었다.


DeleteToDos

toDo 추가기능을 구현한 다음은 삭제 기능도 구현해볼 것이다. 삭제는 추가된 li에 위치하며 버튼이 눌린 li가 삭제되도록 기능을 구현한다.

const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";

const reducer = (state = [], { type, data }) => {
  switch (type) {
    case ADD_TODO:
      return [data, ...state];
    case DELETE_TODO:
      return state.filter((text) => text !== data);
    default:
      return state;
  }
};
const onClick = (e) => {
  const text = e.target.parentNode.textContent.replace("Del", "");
  store.dispatch({ type: DELETE_TODO, data: text });
};
const paintToDos = () => {
  ul.innerHTML = "";
  const toDos = store.getState();
  for (const toDo of toDos) {
    const li = document.createElement("li");
    const btn = document.createElement("button");
    btn.innerText = "Del";
    btn.addEventListener("click", onClick);
    li.innerText = toDo;
    li.appendChild(btn);
    ul.appendChild(li);
  }
};

기존 reducer에도 삭제 기능을 추가하였고 paintToDos에 li에 버튼과 버튼의 click이벤트를 추가하였다. 또한 click이벤트는 reducer을 호출하는 dispatch를 사용하여 삭제 기능을 구현하였다. 하지만 여기서 문제가 발생하는것은 li의 삭제기준이 되는것이 toDo text라는 것이다. 만약 toDo list에 같은 text의 item이 여러개 있다면 동시에 삭제되는 문제가 있다. 그렇기에 toDo item이 유니크한 심볼을 추가하여 해당 item을 삭제할 수 있게 reducer의 action인자 데이터 구조를 다음과 같이 바꾼다.

const ADD_TODO = "ADD_TODO";
const DELETE_TODO = "DELETE_TODO";

const reducer = (state = [], { type, ...data }) => {
  switch (type) {
    case ADD_TODO:
      return [data, ...state];
    case DELETE_TODO:
      return state.filter((text) => text.id !== data.id);
    default:
      return state;
  }
};

const store = createStore(reducer);
store.subscribe(() => paintToDos());

const onClick = (e) => {
  const id = parseInt(e.target.parentNode.id);
  store.dispatch({ type: DELETE_TODO, id });
};

const paintToDos = () => {
  ul.innerHTML = "";
  const toDos = store.getState();
  for (const { id, data } of toDos) {
    const li = document.createElement("li");
    const btn = document.createElement("button");
    btn.innerText = "Del";
    btn.addEventListener("click", onClick);
    li.id = id;
    li.innerText = data;
    li.appendChild(btn);
    ul.appendChild(li);
  }
};

const onSubmit = (e) => {
  e.preventDefault();
  const toDo = input.value;
  input.value = "";
  store.dispatch({ type: ADD_TODO, id: Date.now(), data: toDo });
};

ToDo item을 추가하였을때 text뿐만아니 유니크한 날짜스탬프를 더해서 state데이터 구조를 변경하였고 유니크한 데이터인 id로 삭제 기능을 대체하였다. 그럼 text가 같은 toDo item이여도 하나만 지워지는걸 확인 할 수 있다. 이로서 redux로 간단한 toDo list를 만들어 보았다.