바닐라 자바스크립트로 TodoList 구현하기(1)

최훈오·2023년 10월 8일
0

데브코스

목록 보기
1/29
post-thumbnail

벌써 데브코스를 시작한지 3주..! 학교수업이랑 병행하니 힘들어도 데브코스에 전념하다 보니 따라가기 벅찬 부분은 없지만 내용이 생각보다 쉽지 않았다. 그래도 내가 데브코스에 들어오기 전 가장 배우고 싶었던 컴포넌트 관점에서 vanilla 자바스크립트를 다룰 수 있어서 좋았다.

컴포넌트 방식으로 생각하기

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>KDT</title>
  </head>
  <body>
    <main class="app"></main>
    <script src="./src/storage.js"></script>
    <script src="./src/Header.js"></script>
    <script src="./src/TodoForm.js"></script>
    <script src="./src/TodoList.js"></script>
    <script src="./src/App.js"></script>
    <script src="./src/main.js"></script>
  </body>
</html>

모든 js파일을 여기서 관리한다.

주의할 점은 script 태그의 위치의 순서가 유의미 하다는 점이다. 만약 storage.js 파일의 변수나 함수를 Header.js 에서 참조한다면 먼저 선언되어 있어야 하므로 storage.js 의 위치가 Header.js 보다 위에 있어야 한다!

Header.js

function Header({ $target, text }) {
  const $header = document.createElement("h1");

  $target.appendChild($header);

  this.render = () => {
    $header.textContent = text;
  };

  this.render();
}

제목을 만드는 부분이다.

고정적으로 문자열이 들어가는 부분으로 textContent 를 통해 text를 삽입한다.

TodoList.js

function TodoList({ $target, initialState }) {
  const $todoList = document.createElement("div");
  $target.appendChild($todoList);

  this.state = initialState;

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  };

  this.render = () => {
    $todoList.innerHTML = `
          <ul>
              ${this.state.map(({ text }) => `<li>${text}</li>`).join("")}
          </ul>
      `;
  };

  this.render();
}

todo를 화면에 보여주는 부분이다.

어디에 태그를 넣을지 의미하는 target 와 기본 상태를 나타내는 initialState 를 통해 ul 태그와 li 태그에 statetext를 화면에 그린다.

그리고 setState를 통해 상태를 업데이트하고 render를 통해 화면에 다시 그리는 동작을 한다.

TodoForm.js

function TodoForm({ $target, onSubmit }) {
  const $form = document.createElement("form");

  $target.appendChild($form);

  let isInit = false;

  this.render = () => {
    $form.innerHTML = `
            <input type="text" name="todo" />
            <button>Add</button>
        `;
    if (!isInit) {
      // 핸들러 등록은 최초 1회만
      $form.addEventListener("submit", (e) => {
        e.preventDefault();
        const $todo = $form.querySelector("input[name=todo]");
        const text = $todo.value;
        if (text.length > 1) {
          $todo.value = "";
          onSubmit(text);
        }
      });
      isInit = true;
    }
  };
  this.render();
}

todo리스트 추가에 관련된 input 폼을 관리하는 부분이다.

**TodoForm 에서 입력 받은 값을 TodoList에 넣으려면 어떤 방식으로 구현해야 할까?**

가 포인트다.

  1. TodoForm 생성 파라미터에 TodoList 넣고 직접 참조?
    • TodoFormTodoList 의 의존성이 강하게 생김
  2. TodoForm 생성 파라미터에 이벤트 콜백을 넣고, text를 입력 받으면 해당 콜백을 통해 text 넘겨주기
    • onSubmit에 대한 내부 로직은 알 수가 없고 그냥 text만 넘긴다(처리하는 로직은 todoForm바깥에 있음)

2번을 선택하여 TodoForm 컴포넌트와 TodoList 컴포넌트의 의존성을 최대한 없애는 방향으로 구현한다. 이렇게 하면 나중에 유지보수 할 때 쉽다.

App.js

function App({ $target, initialState }) {
  new Header({
    $target,
    text: "Simple Todo List",
  });

  new TodoForm({
    $target,
    onSubmit: (text) => {
      const nextState = [
        ...todoList.state,
        {
          text,
        },
      ];

      todoList.setState(nextState);

      storage.setItem("todos", JSON.stringify(nextState));
    },
  });

  const todoList = new TodoList({
    $target,
    initialState,
  });
}

모든 컴포넌트를 관리하는 부분이다.

Header, TodoForm, TodoList 컴포넌트에 어디에 태그를 넣을지 의미하는 target 과 기본 상태를 나타내는 initialState 를 인자로 전달한다.

main.js

const initialState = storage.getItem("todos", []);

const $app = document.querySelector(".app");

new App({
  $target: $app,
  initialState,
});

최상위 컴포넌트로 App 을 호출하는 부분이다.

최종적으로 컴포넌트 간의 의존성을 이렇게 볼 수 있다. 형제 컴포넌트 끼리는 의존성을 최소화하고 부모 컴포넌트에서 이것을 모두 관리하는 모습이 이상적이다.

ClientSide에서 데이터 저장하기

간단하게 로컬 스토리지를 이용해서 클라이언트에서 데이터를 저장하여 관리해보자.

storage.js

const storage = (function (storage) {
  const setItem = (key, value) => {
    try {
      storage.setItem(key, value);
    } catch (e) {
      console.log(e);
    }
  };

  const getItem = (key, defaultValue) => {
    try {
      const storedValue = storage.getItem(key);

      if (storedValue) {
        return JSON.parse(storedValue);
      }
      return defaultValue;
    } catch (e) {
      console.log(e);
      return defaultValue;
    }
  };

  return {
    setItem,
    getItem,
  };
})(window.localStorage);

로컬스토리지로 데이터를 주고받을때는 JSON 함수를 매번 사용해야 하므로 불편함을 덜기위해, 에러 처리를 한번에 하기 위하여 따로 storage.js를 만들어 관리한다.

const initialState = storage.getItem("todos", []);

먼저 getItem 은 위와 같이 기본 상태를 받아올 때 로컬스토리지에 값이 없으면 빈 배열을 반환하도록 한다. 빈 배열을 넣지 않으면 list를 돌때 map 함수가 쓰이는데 이때 오류가 발생하기 때문이다. 또한, 문자열이 아닌 Object 형식의 데이터가 들어간 경우 이를 무시한다.

storage.setItem("todos", JSON.stringify(nextState));

그리고 setItem은 로컬스토리지에 저장할 데이터의 크기가 제한을 초과하면 에러가 발생하는데 이런 경우를 처리하기위해 두었다. 특히, 캐싱을 날리는 코드를 잡지 않으면 쌓여서 에러가 나는 경우가 있다고 한다. 이럴 때는 이것을 무시하고 원래대로 동작하게 해야 한다.

느낀점

아직 기초단계 이지만 많은 것을 배울 수 있었다. 이전에 자바스크립트로도 todolist를 구현해보고, 리액트로도 구현해봤었는데 지금 생각해보니 그때는 옳은 코드가 무엇인지 고민하지 않은 상태에서 막 구현했던 것 같다. 또한, 프레임워크나 라이브러리 없이도 상태관리를 통한 컴포넌트 관리가 가능하다는 것이 신기했고 나중에 리액트나 뷰를 통해 구현을 할때 도움이 굉장히 많이 될 것 같다.

0개의 댓글