혼자 적어보는 노트

프로그래머스 자바스크립트 스터디 - mission4 / 마지막 후기 본문

스터디

프로그래머스 자바스크립트 스터디 - mission4 / 마지막 후기

jinist 2022. 2. 22. 04:05

 

미션 4 - TodoList App 업그레이드하기 2

mission2에서 만든 TodoList를 업그레이드해서 api호출로 데이터를 CRUD하는 과정이였다.


 

1. 컴포넌트별 state 관리

이번과제를 진행하면서 크게 고민했던 것은 각 컴포넌트별 state관리였다.

어찌보면 단순한 문제로 보일 수 있겠지만, 상위컴포넌트에서 데이터를 담고

그 담은 데이터를 부모컴포넌트의 setState로 관리하고 한번 더 자식들의 setState라는 함수를 통해서

전달하는 방식이 익숙치 않았던 것 같다.

 

  // initialData 선언
  
  this.$target = $target
  this.todoData = {
    currentUser: 'jinist',
    todos: [],
  }
  this.isLoading = false
  this.usersData = []
  
  // setState 선언
  this.setState = (nextData) => {
    
	// .. 생략
    
    todoInput.setState(this.todoData.currentUser)
    todoList.setState({ currentUser, todos })
    completedTodoList.setState({ currentUser, todos: completed })
    loading.setState(false) // *문제 부분
}
  //.. 생략
  const loading = new Loading({ $target, isLoading: this.isLoading })

 

App 컴포넌트의 상단에 데이터를 tihs로 데이터를 선언해놓고

Loading 컴포넌트에 this.isLoading의 데이터를 전달해준 상태였다.

하지만 setState부분에서 App 컴포넌트에서 this.isLoading 로 선언한 데이터를 사용하지 않으니

혼돈이 올 수 있다는 것을 알았다.

이런 경우에서는 isLoading을 굳이 선언할 필요가 없을 것 같다.

 

const loading = new Loading({ $target, isLoading: false })

 

 

컴포넌트 구조에 관해 관심을 가지다가 지난기수의 리뷰에 참고 링크를 발견했는데

작성된지 좀 된 글이지만 단일책임 원칙과 관심사 분리에 대한 필요성에 대해 설명하는 글이다.

https://rinae.dev/posts/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-kr

 

한번 읽어보았는데 잘 읽히기도 하고 다른 패턴에도 관심이 생기게 되었다.

 


 

2. fetch반복 줄이기 / 분리, 추상화

 

데이터를 받아오기만 하는 GET요청 외에도 POST, PUT, DELETE를 사용하였는데

fetch부분과 res.ok를 확인하는 로직을 일일히 반복되게 작성을 했었는데, 리뷰를 받고 수정했다.

 

 [ 이전코드 ]

export const getUsersFetch = async () => {
  try {
    const res = await fetch(`${API_URL}/users`)
    if (!res.ok) {
      throw new Error('API Error: Get Users 요청 실패')
    }
    return await res.json()
  } catch (error) {
    console.log(error)
  }
}

export const getTodoFetch = async (username) => {
  if (!username) return []
  try {
    const res = await fetch(`${API_URL}/${username}`)
    if (!res.ok) {
      throw new Error('API Error: Get Todo 요청 실패')
    }
    return await res.json()
  } catch (error) {
    console.log(error)
  }
}

export const addTodoFetch = async (username, todoText) => {
  if (typeof todoText !== 'string') {
    throw new Error('API Error: 추가할 todo의 type을 확인해주세요')
  }
  try {
    const res = await fetch(`${API_URL}/${username}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        content: todoText,
      }),
    })
    if (!res.ok) {
      throw new Error('API Error : Add Todo 요청 실패')
    }
  } catch (error) {
    console.log(error)
  }
}

// .. 생략하였지만 아래에 3개의 api요청이 더 있었다..

 

✅ 수정한 코드

const API_URL = 'https://todo-api.roto.codes'

const request = async (url, option) => {
  try {
    const res = await fetch(`${API_URL}/${url}`, option)
    if (!res.ok) {
      throw new Error(`API Error: ${option.method} 요청 실패`)
    }
    return await res.json()
  } catch (error) {
    console.log(error)
  }
}
export const getUsersFetch = async () => {
  return request('users', { method: 'GET' })
}

export const getTodoFetch = async (username) => {
  if (!username) return []
  return request(username, { method: 'GET' })
}

export const addTodoFetch = async (username, todoText) => {
  if (typeof todoText !== 'string') {
    throw new Error('API Error: 추가할 todo의 type을 확인해주세요')
  }
  return request(username, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      content: todoText,
    }),
  })
}

export const removeTodoFetch = async (username, id) => {
  return request(`${username}/${id}`, { method: 'DELETE' })
}

export const resetTodoFetch = async (username) => {
  return request(`${username}/all`, { method: 'DELETE' })
}

export const toggleFetch = async (username, id) => {
  return request(`${username}/${id}/toggle`, { method: 'PUT' })
}

fetch와 res.ok를 체크하던 부분을 따로 함수로 분리하고

url과 option을 매개변수로 전달해주어 중복되는 코드를 줄였다.

기존 코드는 아래부분을 생략했지만 80줄이 넘었던 코드를 거의 반절로 줄였다..!짜릿 

 

 


 

3. API가 느린 경우 인터렉션 처리

 

API에 호출을 보냈을 때 응답이 느릴 경우

사용자에게 응답을 기다리고 있는 중임을 알려야 한다는 보너스 미션을 받았다.

 

응답을 기다리는 데이터(API요청을 보낸 데이터)가 todoList뿐만 아니라 user의 데이터도 있기 때문에

로딩컴포넌트를 분리하여 speener가 돌아가게 만들었다.

 

import { validateBoolean } from './validator.js'

export default function Loading({ $target, isLoading }) {
  this.isLoading = validateBoolean(isLoading)

  this.$loading = document.querySelector('#loading')

  this.setState = (nextState) => {
    validateBoolean(nextState)
    this.isLoading = nextState
    this.render()
  }

  this.render = () => {
    this.$loading.className = this.isLoading ? 'active' : ''
  }
  this.render()
}

초기값을 false로 받고 api요청 시 active class가 추가되고 정상적으로 API의 응답을 받았을 경우

class를 지우는 방식을 사용했다.

 


 

4. Object data check

 

if (typeof data !== 'object') {
  throw new Error('데이터 타입이 Object가 아닙니다.')
}

위 처럼 typeof data !== 'object' 를 사용하여 해당 데이터가 객체인지 검증을 했었는데

data에 배열이 들어올 경우 배열 또한 object로 표시되기 때문에 적절한 검증 방법이 아니다.

 

lodash/isPlainObject

위의 참고 링크를 받았다. 🙇‍♀️

공개된 라이브러리의 코드를 확인하여 무언가 참고할 수 있다는 것을 알게 되었다.

 

✅ 수정한 코드

if (toString.call(data) !== "[object Object]") {
    throw new Error("데이터 타입이 Object가 아닙니다.");
}

 toString에 call을 사용하여 type을 확인 할 수 있었다.

 

 

추가적으로 아래와 같이 반환된 프로토타입과 비교하는 것으로도 object 확인이 가능했다!

let compare = data;
while (Object.getPrototypeOf(compare) !== null) {
    compare = Object.getPrototypeOf(compare);
}

if (Object.getPrototypeOf(data) !== compare) {
    throw new Error("데이터 타입이 Object가 아닙니다.");
}

 


 

5. Drag and Drop

현재 할 일과 완료된 할 일의 컴포넌트에서 할 일을 드래그하여  드롭 시

드롭한 곳으로 할 일이 이동되는 것을 구현하였다.

 

이전에 Drag and Drop기능을 사용해본 적 있어서 동작 방식에 대한 어려움은 없었는데

현재 할 일 컴포넌트 안에서 드래그/드롭 시에는 데이터가 변경되지 않고

다른 컴포넌트에 드롭을 했을 경우에만 데이터가 변경되게 처리 하는 부분이 복잡하다고 느꼈다.

 

[ 이전코드 ]

Drag Event 발생 시 드래그한 li의 id를 전달하여 드롭 시 해당 li의 id와 드래그한 li의 id를 비교하여

api요청을 보낼 지 여부를 결정하는 코드를 작성했다.

  const onDragStart = (e) => {
    e.dataTransfer.dropEffect = 'move'
    e.dataTransfer.setData('dataId', e.target.dataset.id)
    e.dataTransfer.setData('dataToggle', e.target.className)
  }
  
  const onDrop = (e) => {
    e.preventDefault()
    const dropElement = e.target.closest('.todo-item')
    if (!dropElement) {
      return
    }

    const dragId = e.dataTransfer.getData('dataId')
    const dragToggle = e.dataTransfer.getData('dataToggle')
    const dropId = dropElement.dataset.id

    if (dragId === dropId || dragToggle === dropElement.className) {
      return
    }
    
    onToggleTodo(dragId)
  }

  const onDragover = (e) => {
    e.preventDefault()
    e.dataTransfer.dropEffect = 'move'
  }

 

하지만한 컴포넌트에 데이터가 없어서 li가 보여지지 않을 경우 드롭할 대상의 li가 없다는 문제사항이 발생했다.

 

  수정한 코드

  const onDragStart = (e) => {
    e.dataTransfer.dropEffect = 'move'
    e.dataTransfer.setData('dragId', e.target.dataset.id)
    e.dataTransfer.setData(
      'dragClass',
      e.target.closest('.todo-list').className
    )
  }
  const onDrop = (e) => {
    e.preventDefault()
    const dropElement = e.target.closest('.todo-list')
    if (!dropElement) {
      return
    }
    const dragId = e.dataTransfer.getData('dragId')
    const dragClass = e.dataTransfer.getData('dragClass')
    const dropClass = dropElement.className

    if (dragClass === dropClass) {
      return
    }
    onToggleTodo(dragId)
  }

todo와 completed 컴포넌트의 파라미터로 클래스의 이름을 전달해 주어 각 컴포넌트에 class를 붙인 후에

드롭하는 target의 기준을 li가 아닌 해당 컴포넌트의 div영역으로 변경하여 문제사항을 해결해보았다.

 

 


 

✍️ mission 4를 진행하면서 느낀 점

미션중에 이번 미션4에서 리뷰를 가장 많이 받은것 같은데 혼나는 듯한 기분.. 도 들면서

자극이 되었다. 뭔가 혼나고 싶은 마음!!

이전 미션의 API를 이용해서 데이터를 불러오고 처리하는 것을 진행했어서 그런지

이번 미션은 그래도 수월하게 진행했었던 것 같다.

개인적으로 보너스 미션들이 좀 더 있었으면 좋겠다.

 


 

✍️  4개의 미션, 그리고 세션을 마치며

곧 typescript를 배울 것이라는 이유로 데이터를 검증하는 함수들을 만들생각을 하질 않았었는데

순수 javascript로도 만들어 보고 이후에 typescript를 활용하는게 순서에 맞는것 같다.

해야 할 일이라고 생각은 했지만 실제로 해본 적 없었던 에러처리에 관련해서 한번 더 생각을 하게 되었다.

아직 좀 더 큰 구조를 만드는 것은 미흡하지만 나중에 누군가가 javascript로 컴포넌트를 만들어 보라고 하면

그래도 얼추 만들 수 있을 것 같다.

그리고 git에 대한 학습의 필요성!! 을 느끼게 해 준 것에 대해 감사하다.🙇‍♀️

개인적으로 단순 리뷰만을 통한 코드의 개선과 학습보다는 미션을 클리어 해가는 과정에서

알아보게 되는 부분들과 미션 제출 이후 다른 사람의 코드들을 보며 습득한 것들,

그리고 세션을 통한 다른 사람들의 질문들로 인한 습득이 더 큰 것 같았다.

혼자 공부 하는 것에 대한 갈증을 전부 해결하지는 못했지만

나의 부족했던 부분을 다시 한 번 되돌아 보는 시간도 가졌기 때문에 충분히 가치있는 시간을 보낸 것 같다.

이 후 이와 같은 스터디나 프로젝트형 미션이 있다면 또 참여하고 싶다!!

 

Comments