json-server로 fake server를 구축해 TO DO LIST를 구현하는 토이프로젝트를 실습해보았다


목표

  1. json server로 로컬에 REST API를 구축하고 연동해 데이터 CRUD 구현하기

  2. TO DO LIST 입력창에 텍스트를 입력하고 Enter를 누르거나 오른쪽의 버튼을 누르면 아래 목록에 추가되게 하기

  3. 체크박스를 눌렀을 때, 해당 상태가 서버의 데이터에 반영되게 하기

  4. 편집 버튼을 눌렀을 때, 입력을 받을 수 있도록 활성화되고 버튼들이 바뀌게 하기

  5. 편집 완료나 취소 버튼을 눌렀을 때, 해당사항이 반영되도록 하기

  6. 삭제 버튼을 눌렀을 때, 데이터가 삭제되도록 하기

  7. 하단부에 페이지로 데이터를 나눠 보여주도록 하기 (다음글에서)




1. json server 설치

VSCODE를 이용해 JSON SERVER를 설치했다.
VSCODE의 터미널에서 npm install -g json-server@0.17.4 를 입력해서 설치하면 된다.
-g는 전역에 설치하는 방법인데 지역에 설치하는 것을 권장하는 분들도 있으니 참고해 설치하기.
@0.17.4는 버전이다. (이 버전으로 설치하기를 권장)

그냥 npm install -g json-server라고 입력해 설치할 수도 있지만 이렇게 설치해서 페이지네이션 과정에서 제대로 동작하지 않아 원인을 찾다가 너무 고생했다ㅠㅠ 뭘 해도 안됐는데 결국 stackoverflow에서 똑같은 문제로 도움을 구한 분이 있길래 참고해 버전을 다르게 해서 설치했더니 해결됐다. 참고한 링크 : https://stackoverflow.com/questions/77927487/my-pagination-for-my-json-data-is-not-working

혹시 비슷한 문제로 어려움을 겪는 분들이 있다면 버전을 확인해보시길 추천합니다ㅠㅠ



2. db.json파일 만들기

db.json은 서버의 데이터베이스역할을 하는 파일이 될 것이다. json객체를 이용해 만들었다.

우선 아래와 같이 테스트용으로 데이터를 넣었다.

{
  "todos": [
    {
      "id": 1,
      "content": "테스트 1",
      "completed": false
    }
  ]
}


3. json server 작동시키기


터미널에 json-server --watch db.json 를 입력해주고 아래와같이 뜬다면 연결 성공이다.



4. 기본세팅

  'use strict'

  const get = (target) => {
    return document.querySelector(target)
  }

  const API_URL = 'http://localhost:3000/todos'

  const $todos = get('.todos')
  const $form = get('.todo_form')
  const $todoInput = get('.todo_input')

'use strict'를 이용해 엄격모드로 진행한다.

querySelector를 이용해 css에서 사용하는 식으로 dom을 지정하는 함수를 만들고
(ex .=class / #=id 등)

url 주소는 데이터를 CRUD할 때마다 쓰일 것이므로 상수로 선언해준다

그리고 dom을 변수에 할당해준다.

아래와 같은 방법으로 HTML에 요소를 만들어 넣어줄 수 있음

  const createTodoElement = (item) => {
    const { id, content, completed } = item
    // 데이터를 받을 때 id, content, completed를 받을 것이다
    const $todoItem = document.createElement('div')
    const isChecked = completed ? 'checked' : ''
    // completed 상태가 true라면 'checked'를 input 속성으로 넣어줄 예정
    $todoItem.classList.add('item')
    $todoItem.dataset.id = id
    $todoItem.innerHTML = `
            <div class="content">
              ... 삽입하고 싶은 html 요소들 ...
            </div>
      `
    return $todoItem
  }


5. 서버에서 데이터를 받아 todos로 넣어 목록을 만들기

// 아래 목록을 만드는 역할. 서버에서 받은 데이터 배열을 todos로 넣을 예정
  const renderAllTodos = (todos) => {
    $todos.innerHTML = ''
    todos.forEach((item) => {
      const todoElement = createTodoElement(item)
      $todos.appendChild(todoElement)
    })
  }

// fetch를 이용해 json서버에서 데이터 불러옴
  const getTodos = () => {
    fetch(API_URL)
      .then((response) => response.json())
      .then((todos) => renderAllTodos(todos))
      .catch((error) => console.error(error))
  }

비동기 처리 기법인 ajax를 이용할 예정이라 fetch API를 이용해 작성해준다.
fetch API의 response는 실제 json이 아니라 json으로 변환해주어야 한다.
추가메서드를 이용해 작성한 코드 : .then((response) => response.json())



6. TO DO LIST 입력 기능 구현

// submit버튼을 누르는 event가 발생했을 때 처리되어야할 것들
  const addTodo = (e) => {
    e.preventDefault() // submit버튼을 눌렀을 때 새로고침되는 것을 막기 위해
    const todo = {
      content: $todoInput.value,
      completed: false,
    }
    // fetch로 서버에 데이터를 보내기
    fetch(API_URL, {
      method: 'POST',
      headers: {   // json server 리드미에 있는 header 그대로 복사해 사용
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(todo),
    })
      // 서버로 데이터를 보내고 나서 아래 목록을 다시 불러와 반영되도록 하는 코드
      .then(getTodos)
      .then(() => {
        $todoInput.value = '' // 입력란을 비우고 focusing되게 하는 코드
        $todoInput.focus()
      })
      .catch((error) => console.error(error))
  }

이번에는 ajax를 이용해 비동기 처리를 해줄 예정이므로 submit버튼을 눌렀을 때 새로고침되는 것을 막기 위해 e.preventDefault()코드를 추가해주었다.



7. 체크박스 상태 반영

// 체크박스 눌렀을 때 데이터 반영
  const toggleTodo = (e) => {
    // 체크박스가 아닌 다른 곳을 클릭했을 때 아무것도 하지 않게
    if (e.target.className !== 'todo_checkbox') return
    
    // 해당 체크박스에 가장 가까운 클래스가 item인 요소를 찾아서 변수에 할당(체크박스들끼리 구분 위해)
    const $item = e.target.closest('.item')
    const id = $item.dataset.id // 해당요소의 dataset에서 id만 불러옴
    const completed = e.target.checked

    // 데이터의 프로퍼티 중 일부만 변경하기위해 method: 'PATCH'사용 (전체 변경에는 PUT)
    // body: 데이터의 completed의 값을 변경할 것이다 (true, false형태)
    fetch(`${API_URL}/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ completed }),
    })
      .then(getTodos) // 새로 렌더링해주기
      .catch((error) => console.error(error))
  }


8. 편집 버튼 활성화 기능 구현

  // 편집 버튼 눌렀을 때 변화 구현
  const changeEditMode = (e) => {
    // 필요한 dom들
    const $item = e.target.closest('.item')
    const $label = $item.querySelector('label')
    const $editInput = $item.querySelector('input[type="text"]')
    const $contentButtons = $item.querySelector('.content_buttons')
    const $editButtons = $item.querySelector('.edit_buttons')
    const value = $editInput.value

    // 편집 버튼을 누를 경우 완료/취소 버튼 보여지게
    if (e.target.className === 'todo_edit_button') {
      $label.style.display = 'none'
      $editInput.style.display = 'block'
      $contentButtons.style.display = 'none'
      $editButtons.style.display = 'block'
      $editInput.focus()
      $editInput.value = ''
      $editInput.value = value
      // 편집 버튼 클릭 시 입력창 커서를 맨 끝으로 나타나게 하기 위해 아예 다시 불러옴
    }

    // 편집 취소 버튼을 눌렀을 때 편집/삭제 버튼 다시 보여지게
    if (e.target.className === 'todo_edit_cancel_button') {
      $label.style.display = 'block'
      $editInput.style.display = 'none'
      $contentButtons.style.display = 'block'
      $editButtons.style.display = 'none'
      $editInput.value = $label.innerText
      // 취소버튼 누른 후 다시 편집버튼 눌렀을 때 수정 전 내용 그대로 유지되게 하기 위해
    }
  }

const $item = e.target.closest('.item')
// 눌린 edit버튼과 가장 가까운 class가 item인 요소를 가져옴

const $label = $item.querySelector('label')
// querySelector 메서드로 item 내의 label요소를 가져옴

const $editInput = $item.querySelector('input[type="text"]')
// 편집창인 input요소도 가져옴

...



9. 편집 완료 버튼을 눌렀을 때 서버로 데이터 보내 업데이트 하기

  // 편집완료 버튼 눌렀을 때 반영되게 하는 기능 구현
  const editTodo = (e) => {
    if (e.target.className !== 'todo_edit_confirm_button') return
    const $item = e.target.closest('.item')
    const id = $item.dataset.id
    const $editInput = $item.querySelector('input[type="text"]')
    const content = $editInput.value

    fetch(`${API_URL}/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    })
      .then(getTodos) // 새로 렌더링
      .catch((error) => console.error(error))
  }

7번의 체크박스 상태 데이터를 서버로 보낸 것과 유사하게 하고, 변경할 내용이 들어갈 body부분에 content를 넣어주면 된다



10. 삭제 버튼 기능 구현

  const removeTodo = (e) => {
    if (e.target.className !== 'todo_remove_button') return
    const $item = e.target.closest('.item')
    const id = $item.dataset.id

    fetch(`${API_URL}/${id}`, {
      method: 'DELETE',
    })
      .then(getTodos) // 새로 렌더링
      .catch((error) => console.error(error))
  }


11. 각각의 이벤트에 만든 기능 함수들 등록해주기

  const init = () => {
    window.addEventListener('DOMContentLoaded', () => {
      getTodos()
    })
    $form.addEventListener('submit', addTodo)
    $todos.addEventListener('click', toggleTodo)
    $todos.addEventListener('click', changeEditMode)
    $todos.addEventListener('click', editTodo)
    $todos.addEventListener('click', removeTodo)
  }
  init()

이벤트들을 init함수에 하나로 모아 실행시켜주는 것으로 코드를 마무리한다



12. 위 코드들을 즉시실행 함수로 감싸주기

코드들을 즉시실행 함수로 감싸 변수충돌을 방지해준다.

profile
⛅🛩️ 먼 길을 돌아서 온 프론트엔드 개발자 ✈️⛅

0개의 댓글