
작업 순서
api.js 만들기
api 호출을 담당하는 코드를 api.js에 작성하여, 다른 js파일에서 api 호출 코드를 반복하지 않도록 한다.
TodoList 컴포넌트 만들기
TodoComments 컴포넌트 만들기
App 컴포넌트 만들기
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Second Todo List</title>
</head>
<body>
<main class ="app"></main>
<script src ="./src/main.js" type="module"></script>
</body>
</html>
XMLHTTPRequest를 이용해 api를 불러온다. 참고로 XMLHTTPRequest는 요즘 잘 쓰이지 않고 `fetch가 많이 사용된다.
export function request (url, successCallback, failCallback) {
const xhr = new XMLHttpRequest()
xhr.addEventListener("load", (e) => {
if(xhr.readyState ===4) {
if(xhr.status === 200) {
successCallback(JSON.parse(xhr.responseText))
} else {
failCallback(xhr.statusText)
}
}
})
xhr.addEventListener('error', (e) => failCallback(xhr.statusText))
xhr.open('GET', url)
xhr.send()
}
onclick은 TodoList 안에 있는 항목을 클릭했을 때 실행할 콜백함수이다.
this.state.map( )에서 .join('')을 빠뜨리면 안된다. map 함수는 새로운 배열을 만들고, join은 배열을 한 줄로 만든다.
이벤트 바인딩을 render 함수 밖에서 하면 문제가 생긴다. innerHTML의 경우 이전에 쓰인걸 모두 날리고 다시 렌더링하는 것이기 때문에 이전에 걸었던 이벤트가 날라가 버린다. 따라서 렌더링하는 곳에서 매번 이벤트를 다시 걸어줘야 한다.
export default function TodoList({$target, initialState, onClick}) {
const $element = document.createElement('div')
$target.appendChild($element)
this.state = initialState
this.setState = nextState => {
this.state = nextState
this.render()
}
this.render = () => {
if(Array.isArray(this.state)) {
$element.innerHTML = `
<h1>Simple TodoList</h1>
<ul>
${this.state.map(({id, text}) =>
`<li data-id="${id}>${text}</li>`).join('')}
</ul>
`
// 이벤트 바인딩을 render 함수 밖에서 하면 문제가 생긴다.
$element.querySelectorAll('li').forEach($li => {
$li.addEventListener('click', (e) => {
const {id} = e.target.dataset
onClick(parseInt(id,10))
})
})
}
}
this.render()
}
export default function TodoComments({$target, initialState}) {
const $element = document.createElement('div')
$target.appendChild($element)
this.state = initialState
this.setState = nextState => {
this.state = nextState
this.render()
}
this.render = () => {
const {selectedTodo, comments} = this.state
if(!selectedTodo || !comments) {
$element.innerHTML = ''
return
}
$element.innerHTML = `
<h2>${selectedTodo.text}의 댓글들</h2>
${comments.length === 0? '댓글이 없습니다' : ''}
<ul>
${comments.map(({content})=> `<li>${content}</li>`).join('')}
</ul>
`
}
this.render()
}
import TodoList from "./TodoList.js";
import TodoComments from "./TodoComments.js";
import { request } from "./api.js";
export default function App({$app}) {
this.state = {
todos: [],
selectedTodo: null,
comments:[]
}
this.setState = nextState => {
this.state = nextState
todoList.setState(this.state.todos)
todoComments.setState({
selectedTodo: this.state.selectedTodo,
comments: this.state.comments
})
}
const todoList = new TodoList({
$target: $app,
initialState: this.todos,
onClick: id => {
const selectedTodo = this.state.todos.find(todo => todo.id ===id)
this.setState({
...this.state,
selectedTodo
})
request(`https://kdt.roto.codes/comments?todo.id=${id}`,
(comments) => {this.setState({...this.state,comments})
})
}
})
const todoComments = new TodoComments({
$target: $app,
initialState: {
selectedTodo: this.state.selectedTodo,
comments: this.state.comments
}
})
// todos를 불러오기
this.init = () => {
request(`https://kdt.roto.codes/todos`, (todos) => {
this.setState({
...this.state,
todos
})
})
}
this.init()
}
import App from'./App.js'
const $app = document.querySelector('.app')
new App({$app})
API를 붙이기 전에
main.js에 더미 데이터를 넣고 렌더링이 잘되는지 먼저 확인해보는 것이 좋다. 그래야 오류가 발생했을 때, API를 붙이는 과정에서의 문제인지 다른 코드에서의 문제인지 확인이 가능하다.new TodoList({ $target : $app, initialSrate : [ { text : "Learn React" }, { text : "Learn Javascript" }, { text : "Learn CSS" } ] })new TodoComments({ $target : $app, initialSrate : { selectedTodo : { text : "Learn Javascript" }, comments : [ { text : "안녕하세요" }, { text : "반가워요" }, ] } })