입력 폼에 입력받은 문자열(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에 붙이는 것으로 수정했다.
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는 아래와 같이 표현된다.
이전 코드에는 map
을 사용했지만, 수정된 코드에서는 forEach
를 사용했다.
두 함수는 배열을 순회한다는 점은 동일한데 무엇이 다른걸까?