TodoList에서 Todo가 [object Object] 문자열로 추가된다.

ZenTechie·2023년 10월 9일
0

Troubleshooting

목록 보기
9/9
post-thumbnail

입력 폼에 입력받은 문자열(Todo)이 TodoList에 추가되야 한다.
그러나, 아래와 같이 오류가 발생했다.

입력 폼은 문제가 없는 것 같으니, 내가 생각하기에 문제가 발생한 곳은 아래와 같다.

  • TodoList.js : <ul>TodoItem을 추가하는 컴포넌트
  • TodoItem.js : 입력 폼에 입력받은 문자열을 <li> 형태로 만드는 컴포넌트

TodoList.js의 코드는...

// params.$target - 해당 컴포넌트가 추가가 될 DOM 엘리먼트
// params.$initialState - 해당 컴포넌트의 초기 상태값
// $ : DOM 객체를 담고있는 변수를 의미
function TodoList(params) {
  // new 키워드를 사용하지 않았을 경우 예외 처리
  if (!(this instanceof TodoList)) {
    throw new Error('TodoList Component must be called with "new"');
  }

  const $todoList = document.createElement('div');
  const $target = params.$target;

  this.state = params.initialState;

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

  this.render = () => {
    $todoList.innerHTML = `
    <ul>
      ${this.state.map(
        (todo) => new TodoItem
      ({ $target: $todoList, initialState: todo })
      )}
    </ul>
    `;
    $target.appendChild($todoList);
  };
  this.render();
}

TodoItem의 코드는...

function TodoItem({ $target, initialState }) {
  // new 키워드를 사용하지 않았을 경우 예외 처리
  if (!(this instanceof TodoItem)) {
    throw new Error('TodoItem Component must be called with "new"');
  }

  const $todoItem = document.createElement('li');
  let isInit = false;

  this.state = initialState;

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

  this.onToggle = () => {
    this.setState({
      ...this.state,
      isCompleted: !this.state.isCompleted,
    });
  };

  this.render = () => {
    const { text, isCompleted } = this.state;
    $todoItem.innerHTML = `
      <span class="text">${text}</span>
      <button class="remove-btn">삭제</button>
    `;
    $todoItem.querySelector('span').style.textDecoration = isCompleted ? 'line-through' : 'none';
  };
  this.render();
}

팀원에게 도움을 요청했고, 팀원 분이 콘솔로 확인해보니, TodoItem이 가지고 있는 html이 아닌, TodoItem 인스턴스 자체가 넘어가서 [object Object]라고 뜨는 것이라고 하셨다.

여기서 헷갈리는 점이 있는데, 다른 코드를 먼저 살펴보자.

// Header.js
function Header({ $target, text }) {
  // new 키워드를 사용하지 않았을 경우 예외 처리
  if (!(this instanceof Header)) {
    throw new Error('Header Component must be called with "new"');
  }

  const $header = document.createElement('h1');
  this.render = () => {
    $header.textContent = text;

    $target.appendChild($header);
  }

  this.render();
}

위 코드는, 단순히 Header를 만드는 컴포넌트이고, 이러한 컴포넌트는 App.js에서 모두 관리한다.

// App.js
// 어떤 컴포넌트들이 추가되야 하는지 관리하는 곳
function App({ $target, initialState }) {
  // new 키워드를 사용하지 않았을 경우 예외 처리
  if (!(this instanceof App)) {
    throw new Error('App Component must be called with "new"');
  }

  new Header({ $target, text: 'Todo App' });
  new TodoForm({
    $target,
    onSubmit: (text) => {
      const nextState = [...todoList.state, { text: text, isCompleted: false }];
      todoList.setState(nextState);
      storage.setItem('todos', JSON.stringify(nextState));
    },
  });
  const todoList = new TodoList({
    $target,
    initialState,
  });

}

헷갈리는 게 바로 여기다.
위 코드에서는 모두 new로 호출해서 화면에 렌더링을 해주는데, 왜 TodoItem은 new로 호출할 시 Object가 넘어가는 것일까?


코드를 자세히 살펴보고 내린 결론은, 다음과 같다.
Header의 경우, $target에 header 컴포넌트를 붙인 후($target.appendChild($header) new로 호출한다. 하지만, TodoItem의 경우 TodoItem 컴포넌트를 innerHTML 안에서 호출하여 붙인다. 또한, map을 사용해서 새로운 배열을 리턴하기 때문에 여기서 TodoItem 컴포넌트가 [obejct Object]로 나타나는 듯 했다.
그렇기 때문에, 컴포넌트를 innerHTML에 바로 넣는 코드를 수정해야 한다.
나는 TodoItem에 $target인자로 $ul을 넘겨서,

<ul>
	<li></li>
	<li></li>
</ul>

의 형태로 TodoList의 형태를 만든 후에, $ul을 $todoList에 붙이는 것으로 수정했다.

수정된 코드

TodoList.js

const $todoList = document.createElement('div');
$todoList.className = 'todo-list';
 
const $ul = document.createElement('ul');
const $target = params.$target;

this.render = () => {
    $ul.innerHTML = '';
    this.state.forEach((todo) => {
      new TodoItem({
        $target: $ul,
        initialState: todo, 
        onClick: (clickedTodoIndex) => {
          // 삭제버튼 클릭
          params.onClick(clickedTodoIndex);
          this.setState(storage.getItem('todos', []));
        },
        onToggle: (clickedTodoIndex) => {
          // todo text 클릭
          params.onToggle(clickedTodoIndex);
          this.setState(storage.getItem('todos', []));
        },
      });
    });
    $todoList.appendChild($ul);
  };

최종적으로 DOM 트리에서의 TodoList는 아래와 같이 표현된다.


forEach, map

이전 코드에는 map을 사용했지만, 수정된 코드에서는 forEach를 사용했다.
두 함수는 배열을 순회한다는 점은 동일한데 무엇이 다른걸까?

  • forEach: 배열의 요소를 순회하면서, 각 요소에 대해 주어진 callback을 수행한다.
  • map: 배열의 요소를 순회하면서, 각 요소에 대해 callback을 실행하고 실행결과를 모은 t새로운 배열을 리턴한다.
profile
데브코스 진행 중.. ~ 2024.03

0개의 댓글