작업 순서
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 : "반가워요" }, ] } })