앞서 만든 todo 앱에 fetch와 history를 적용하여, 각 유저의 todo List를 구현해보자!!
new!
- api 요청할 때, body 타입은 백엔드와 협의한다.(json, xml 등)
⇒ 여기서는 json으로!
⇒ 요청 header에 content-type을 application/json으로 지정해줘야 한다.
- post 등 요청을 했을 때 확인
⇒ 개발자도구 > 네트워크 > fetch/xhr > 하단에 request payload
4.
form
아래에 있는 input
요소를 접근하려 하니 생긴 Cannot set properties of null (setting 'value')
에러: this.render()로 input태그를 생성하기 전에 input요소를 접근하려니 생긴 문제였다;
- string.subString(start[, end])
- start~end-1까지 문자열을 반환
- end를 지정하지 않으면 start부터 끝까지
요구 사항
- 왼쪽에 유저 목록이 존재, 각 유저를 누르면 그들의 todo list가 렌더링된다. - todo를 추가, 리스트업은 API 호출을 통해 서버에 추가하고 불러온다 - but. 리스트업은 낙관적 업데이트를 사용한다 ㄴ 서버에 todo 등록/가져오기 전에 클라의 state로 리스트업을 해서 시간 단축 - 서버와 통신 중일 때, 상황을 알리는 UI를 보여준다. - form에서 제출하기 전에는 텍스트를 유지한다(새로고침에도)
API 주소
공통 endpoint : https://kdt-frontend.todo-api.programmers.co.kr 유저 목록 : /users ㄴ 해당 유저의 할일이 하나라도 있어야 서버에 유저 등록이 된다 할 일 목록 : /{username} 할 일 추가하기 : [POST] /{username} ㄴ 응답은 추가한 todo가 내려온다 할 일 삭제 : [DELETE] /{username}/{todo_id} 할 일 완료여부 토글 : [PUT] /{username}/{todo_id}/toggle ㄴ 호출만으로도 토글 상태를 바꾸는 듯.. body 필요 x
데이터 형태
- Todo { "_id": 할 일의 고유값. 숫자와 문자가 섞여있는 문자로 되어있음, "content": 할 일 text, "isCompleted": 할 일의 완료여부 } - App의 상태 {users, selectedUser, selectedTodos, isLoading}
할 일 추가하기 API 호출 방법
fetch('https://kdt-frontend.todo-api.programmers.co.kr/{username}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: '자바스크립트 공부하기' }) }).then( .... })
/** * (O) html로 좌우 레이아웃으로 *- form에서 제출하기 전에는 텍스트를 유지한다(새로고침에도) ⇒ localstorage + input의 keydown 이벤트 사용! * * 유저 => 컴포넌트 만들긔 * (O) 헤더 * (O) 유저 리스트 * (O) 유저 등록 form * * (O) 유저 클릭 -> 해당 유저 리스트 보여주기 * 유저 추가 * (O) 초기 투두리스트 보여주기 * * (O) 유저 클릭하기 전엔 투두리스트 보여주지 않기 * * select 했을 때 history api * ㄴ 새로고침해도 마지막 select된 유저 리스트를 보여줘야 함 */
코드
- index.html
<!DOCTYPE html> <html> <head> <title>유저들의 투두리스트닷~!</title> </head> <body> <main class="app" style="display:flex; margin-right: 15;"></main> <script src="/src/main.js" type="module"></script> </body> </html>
- App.js
import request from "./api/api.js" import UserList from "./components/userList.js" import TodoHeader from "./components/todoHeader.js" import TodoForm from "./components/todoForm.js" import TodoList from "./components/todoList.js" export default function App({$target}) { const $userContainer = document.createElement('div') const $todoContainer = document.createElement('div') $target.appendChild($userContainer) $target.appendChild($todoContainer) this.state = { selectedUser : '', users : [], selectedTodos : [], isLoading : false, } this.setState = (newState) => { this.state = newState this.render() userList.setState({ ...userList.state, users: this.state.users }) header.setState({ ...header.state, name : this.state.selectedUser, isLoading: this.state.isLoading }) list.setState(this.state.selectedTodos) } this.render = () => { const { selectedUser } = this.state $todoContainer.style.display = selectedUser ? 'block' : 'none' } const header = new TodoHeader({ $target: $todoContainer, initialState : { name : this.state.selectedUser, isLoading: this.state.isLoading } }) new TodoForm({ $target: $todoContainer, onSubmit : async (content) => { const { selectedUser, selectedTodos } = this.state const todo = { content, isCompleted : false } const isFirstTodo = selectedTodos.length === 0 this.setState({ ...this.state, selectedTodos : [ ...selectedTodos, todo ] }) await request(`/${selectedUser}`,{ method : 'POST', body : JSON.stringify(todo) }) if (isFirstTodo) { await fetchUsers() } await fetchTodos() } }) const list = new TodoList({ $target: $todoContainer, initialState : this.state.selectedTodos, onToggle : async (id) => { const newTodos = [...this.state.selectedTodos] const idx = newTodos.findIndex(todo => todo._id === id) newTodos[idx].isCompleted = !newTodos[idx].isCompleted this.setState({ ...this.state, selectedTodos : newTodos }) await request(`/${this.state.selectedUser}/${id}/toggle`,{ method: "PUT", }) await fetchTodos() }, onRemove : async (id) => { const newTodos = [...this.state.selectedTodos] const idx = newTodos.findIndex(todo => todo._id === id) newTodos.splice(idx,1) this.setState({ ...this.state, selectedTodos: newTodos }) await request(`/${this.state.selectedUser}/${id}`,{ method: "DELETE" }) await fetchTodos() } }) const userList = new UserList({ $target: $userContainer, initialState : { users: this.state.users }, onSelect : (userName) => { this.setState({ ...this.state, selectedUser: userName, }) history.pushState(null,'',`/?selectedUser=${userName}`) fetchTodos() } }) const fetchUsers = async () => { const users = await request('/users') this.setState({ ...this.state, users: users.reverse() }) } const fetchTodos = async () => { const { selectedUser } = this.state this.setState({ ...this.state, isLoading: true }) if (selectedUser) { const todos = await request(`/${selectedUser}`) this.setState({ ...this.state, selectedTodos: todos }) } this.setState({ ...this.state, isLoading: false }) } const init = async () => { await fetchUsers() const queryString = decodeURIComponent(location.search.substring(1)) const lastUser = new URLSearchParams(queryString).get('selectedUser') this.setState({ ...this.state, selectedUser: lastUser }) await fetchTodos() } init() window.addEventListener('popstate', init) }
- storage.js
const storage = window.localStorage export function setItem(key,value) { try { storage.setItem(key, JSON.stringify(value)) } catch(e) { console.log(e.message) } } export function getItem(key, defaultValue) { const value = storage.getItem(key) try { if (value) { return JSON.parse(value) } } catch(e) { console.log(e.message) } return defaultValue } export function removeItem(key) { storage.removeItem(key) }
- api.js
const API_END_POINT = 'https://kdt-frontend.todo-api.programmers.co.kr' export default async function request(url, options={}) { try { const res = await fetch(API_END_POINT+url, { ...options, headers : { 'Content-Type' : 'application/json' }, }) if (res.ok) { const json = await res.json() return json } throw new Error('API 호출 오류') } catch (e) { throw new Error(`Error : ${e.message}`) } }
- todoheader
export default function TodoHeader({$target, initialState}) { const $h1 = document.createElement('h1') $target.appendChild($h1) this.state = initialState this.setState = (newState) => { this.state = newState this.render() } this.render = () => { $h1.innerHTML = `${this.state.name}의 Todo List ${this.state.isLoading ? `로딩 중..` : ''}` } this.render() }
- todoform
import { getItem, removeItem, setItem } from "../storage/storage.js" const TODO_TEMP_SAVE_KEY = 'TODO_TEMP_SAVE_KEY' export default function TodoForm ({$target, onSubmit}) { const $form = document.createElement('form') $target.appendChild($form) this.render = () => { $form.innerHTML = ` <input type="text" placeholder="할 일을 입력하세요"></input> <button>추가하기</button> ` } $form.addEventListener('submit', (e)=> { e.preventDefault() const $input = $form.querySelector('input') const { value } = $input if (value) { onSubmit(value) $input.value = "" removeItem(TODO_TEMP_SAVE_KEY) } }) this.render() const $input = $form.querySelector('input') $input.value = getItem(TODO_TEMP_SAVE_KEY, "") $input.addEventListener('keyup', (event)=>{ const { value } = event.target setItem(TODO_TEMP_SAVE_KEY, value) }) }
- todolist
export default function TodoList ({ $target, initialState, onToggle, onRemove }) { const $todos = document.createElement('div') $target.appendChild($todos) this.state = initialState this.setState = (newState) => { this.state = newState this.render() } this.render = () => { if (!this.state.length) { $todos.innerHTML = `<br>todo를 생성하지 않았습니다` return } $todos.innerHTML = ` <ul> ${this.state.map(({_id, isCompleted, content}) => ` <li data-id="${_id}"> ${isCompleted ? `<s>${content}</s>` : content} <button class="remove">X</button> </li>`).join('')} </ul> ` } $todos.addEventListener('click', (e) => { const $li = e.target.closest('li') if ($li) { const { id } = $li.dataset if (e.target.className === "remove") { onRemove(id) } else { onToggle(id) } } }) this.render() }
- userlist
export default function UserList({ $target, initialState, onSelect }) { const $list = document.createElement('div') $target.appendChild($list) this.state = initialState this.setState = (newState) => { this.state = newState this.render() } let isInit = false this.render = () => { $list.innerHTML = ` <h1>User List</h1> <form> <input type="text" placeholder="유저 이름 추가"></input> <button>추가</button> </form> <ul> ${this.state.users.map(user => `<li data-username=${user}>${user}</li>` ).join('')} </ul> ` const $liList = $list.querySelectorAll('li[data-username]') $liList.forEach($li => { $li.onclick = () => onSelect($li.dataset.username) }) if (!isInit) { $list.addEventListener('submit', (e) => { e.preventDefault() const $input = e.target.querySelector('input') const { value } = $input if (value) { onSelect(value) $input.value = "" } }) isInit = true } } this.render() }