ETC -

moonbucks-menu 스탭3 회고

  • -

블랙커피 스터디 소개

블랙커피 스터디를 소개합니다. 아래 링크를 클릭해서 확인할수 있습니다 :)

 

블랙커피를 소개합니다.

훌륭한 의사소통은 블랙커피처럼 자극적이며, 후에 잠들기가 어렵다 - A.M. 린드버그(미국의 작가, 수필가)

www.makerjun.com

moonbucks-menu 스탭3 회고

안녕하세요 TriplexLab 입니다. 
오늘은 moonbucks-menu 스탭2을 이어서 스탭3 회고를 작성해보겠습니다.

 

moonbucks-menu 스탭1 회고

moonbucks-menu 스탭 1 회고 안녕하세요 TriplexLab 입니다. blackcoffee study라는 프론트 앤드 개발자 스터디를 최근에 알게 되어서 그곳에서 들은 내용들을 적어 보겠습니다. 블랙커피를 소개합니다. 훌

triplexlab.tistory.com

 

moonbucks-menu 스탭2 회고

moonbucks-menu 스탭2 중간 회고 안녕하세요 TriplexLab 입니다. 오늘은 moonbucks-menu 스탭1을 이어서 스탭2 회고를 작성해보겠습니다. moonbucks-menu 스탭1 회고 moonbucks-menu 스탭 1 회고 안녕하세요 T..

triplexlab.tistory.com

moonbucks-menu 스탭1, 2 회고 참고해주세요. 👍👍 

👉 스탭 3 중간과정에서 얻은 인사이트

1. MenuApi 객체를 따로 분리해서 서버와의 통신 로직을 구현합니다.(서버와 통신 CRUD작업 구현)
2. fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.

// moonbucks-menu/src/js/index.js

// 웹 서버를 띄운다.
// [x]서버에 새로운 메뉴명이 추가될수 있도록 요청한다.
// [x]서버에 카레고리별 메뉴리스트를 불러와서 화면에 그려준다.
// [x]서버에 메뉴가 수정 될 수 있도록 요청한다.
// [x]서버에 메뉴의 품절상태가 토글될 수 있도록 한다.
// [x]서버에 메뉴가 삭제 될 수 있도록 요청한다.

// 리팩토링 부분
//  localStorage에 저장하는 로직은 지운다.
//  fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.

// 사용자 경험
//  API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
//  중복되는 메뉴는 추가할 수 없다.

import {$} from './utils/dom.js'

const BASE_URL = 'http://localhost:3000/api';

const MenuApi = {
    async getAllMenuByCategory(category) {
        const res = await fetch(`${BASE_URL}/category/${category}/menu`)
        return res.json();
    },

    async createMenu(category, name){
        const res = await fetch(`${BASE_URL}/category/${category}/menu`,{
            method: 'POST',
            headers: {
                'Content-Type':'application/json'
            },
            body: JSON.stringify({name})
        })
        if(!res.ok){
            console.log('에러가 발생했습니다.')
        }
    },

    async updatemenu(category, name, menuId) {
        const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}`, {
            method: 'PUT',
            headers : {
                'Content-Type':'application/json'
            },
            body: JSON.stringify({name})
        })
        if(!res.ok){
            console.log('에러가 발생했습니다.')
        }
        return res.json();
    },

    async toggleSoldOutMenu(category, menuId){
        const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}/soldout`,{
            method: 'PUT',
        });
        if(!res.ok){
            console.error('에러가 발생했습니다.')
        }
    },

    async deleteMenu(category, menuId){
        const res = await fetch(`${BASE_URL}/category/${category}/menu/${menuId}`,{
            method: 'DELETE',
        });
        if(!res.ok){
            console.error('에러가 발생했습니다.')
        }
    }
}

function App() {
    const menuForm = $('#menu-form');
    const idMenuName = $('#menu-name');
    const submitBtn = $('#menu-submit-button');
    const menuList = $('#menu-list');
    this.menu = {
        espresso: new Array(),
        frappuccino: new Array(),
        blended: new Array(),
        teavana: new Array(),
        desert: new Array()
    };
    this.currentCategory = 'espresso';

    this.init = async () => {
        this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
        render();
        initEventListeners();
    };
    
    const render = () => {
        const template = this.menu[this.currentCategory].map((item) => {
            return`<li data-menu-id='${item.id}' class="menu-list-item d-flex items-center py-2">
                    <span class="w-100 pl-2 menu-name ${item.isSoldOut ? 'sold-out' : ''}">${item.name}</span>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-sold-out-button"> 품절 </button>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-edit-button"> 수정 </button>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm menu-remove-button"> 삭제 </button>
                    </li>`;
            }).join('');
        menuList.innerHTML = template;
        commonUpdateMenuCount();
    };

    const commonUpdateMenuCount = () => {
        const menuCount = this.menu[this.currentCategory].length;
        $('.menu-count').innerText = `총 ${menuCount} 개`;
    };
    const commonAddMenuName = async () => {
        if(idMenuName.value === ''){
            alert('값을 입력해주세요.');
            return;
        }
        const menuName = idMenuName.value;
        await MenuApi.createMenu(this.currentCategory, menuName);
        this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
        render();
        idMenuName.value = "";
    };

    const updateMenuName = async (e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        const menuName = e.target.closest('li').querySelector('.menu-name');
        const updateMenuName = prompt('메뉴명을 수정하세요', menuName.innerText);
        await MenuApi.updatemenu(this.currentCategory, updateMenuName, menuId);
        this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
        render();
    };

    const removeMenuName = async(e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        if(confirm('정말 삭제하시겠습니까?')){
            await MenuApi.deleteMenu(this.currentCategory, menuId)
            this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
            render();
        }
    };

    const soldOutMenu = async (e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        await MenuApi.toggleSoldOutMenu(this.currentCategory, menuId)
        this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
        render();
    };

    const initEventListeners = () => {
        menuList.addEventListener('click', (e) => {
            if(e.target.classList.contains('menu-edit-button')){
                updateMenuName(e);
                return;
            }
    
            if(e.target.classList.contains('menu-remove-button')){
                removeMenuName(e);
                return;
            }
    
            if(e.target.classList.contains('menu-sold-out-button')){
                soldOutMenu(e);
                return;
            }
        });
    
        menuForm.addEventListener('submit', (e) => {
            e.preventDefault();
        });
    
        submitBtn.addEventListener('click', commonAddMenuName);
        
        idMenuName.addEventListener('keypress', (e) => {
            if(e.key !== 'Enter') return;
            commonAddMenuName();
        });
    
        $('nav').addEventListener('click', async (e) => {
            const isCategoryBtn = e.target.classList.contains('cafe-category-name');
            if(isCategoryBtn){
                const catagoryName = e.target.dataset.categoryName;
                this.currentCategory = catagoryName;
                $('#category-title').innerText = `${e.target.innerText} 메뉴 관리`;
                this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
                render();
            }
        });
    };
}
const app = new App();
app.init('menu');

 

중간 까지 작업한 파일을 공유합니다. 😃😃

moonbucks.zip
0.73MB


👉 스탭 3에서 얻은 인사이트

1. API 파일 따로 만들어서 진행하므로써 코드의 가독성이 좋아짐.
2. 비동기 처리할때 순서를 보장해주기위해서 async, await를 사용합니다.
3. 서버 요청 할 때 option 객체(POST, PUT, DELETE) 분리한다.하므로써 코드의 중복을 제거합니다.
4. request 함수따라(응답 데이터가 있을때랑, 없을때를 구분) 분리한다.

// src/js/api/index.js

const BASE_URL = "http://localhost:3000/api";

const HTTP_METHOD = {
    POST(data){
        return {
            method: 'POST',
            headers: {
                'Content-Type':'application/json'
            },
            body: JSON.stringify(data)
        }
    },
    PUT(data){
        return{
            method: 'PUT',
            headers: {
                'Content-Type':'application/json'
            },
            body: data ? JSON.stringify(data) : null
        }
    },

    DELETE(){
        return {
            method: 'DELETE'
        }
    }
}

const request = async (url, option) => {
  const res = await fetch(url, option);
  if(!res.ok){
    alert('에러가 발생했습니다.')
    console.log(e);
  }
  return res.json();
};

const requestWithoutJson = async(url, option) => {
    const res = await fetch(url, option);
    if(!res.ok){
      alert('에러가 발생했습니다.')
      console.log(e);
    }
    return res;
}

const MenuApi = {
    async getAllMenuByCategory(category) {
        return request(`${BASE_URL}/category/${category}/menu`);
    },
    async createMenu(category, name) {
        return request(`${BASE_URL}/category/${category}/menu`, HTTP_METHOD.POST({name}))
    },
    async updatemenu(category, name, menuId){
        return request(`${BASE_URL}/category/${category}/menu/${menuId}`, HTTP_METHOD.PUT({name}))
    },
    async toggleSoldOutMenu(category, menuId){
        return request(`${BASE_URL}/category/${category}/menu/${menuId}/soldout`, HTTP_METHOD.PUT())
    },
    async deleteMenu(category, menuId) {
        return requestWithoutJson(`${BASE_URL}/category/${category}/menu/${menuId}`, HTTP_METHOD.DELETE())
    }
}

export default MenuApi

 

// src/js/index.js

// TODO step3 서버 요청 부분
// [x]웹 서버를 띄운다.
// [x]서버에 새로운 메뉴명이 추가될 수 있도록 요청한다.
// [x]서버에 카테고리별 메뉴리스트를 불러온다.
// [x]서버에 메뉴가 수정 될 수 있도록 요청한다.
// [x]서버에 메뉴의 품절상태를 변경(토글)할수 있도록 요청한다.
// [x]서버에 메뉴가 삭제될수 있도록 요청한다.

// TODO 리팩토링 부분
// [x]localStorage에 저장하는 로직은 지운다.
// [x]fetch 비동기 api를 사용하는 부분을 async await을 사용하여 구현한다.

// TODO 사용자 경험
// [x]API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
// [x]중복되는 메뉴는 추가할 수 없다.

import {$} from './utils/dom.js'
import MenuApi from './api/index.js';

function App() {
    const menuForm = $('#menu-form');
    const idMenuName = $('#menu-name');
    const submitBtn = $('#menu-submit-button');
    const menuList = $('#menu-list');
    this.menu = {
        espresso: new Array(),
        frappuccino: new Array(),
        blended: new Array(),
        teavana: new Array(),
        desert: new Array()
    };
    this.currentCategory = 'espresso';

    this.init = async() => {
        render();
        initEventListeners();
    };
    
    const render = async() => {
        this.menu[this.currentCategory] = await MenuApi.getAllMenuByCategory(this.currentCategory);
        const template = this.menu[this.currentCategory].map((item) => {
            return`<li data-menu-id='${item.id}' class="menu-list-item d-flex items-center py-2">
                    <span class="w-100 pl-2 menu-name ${item.isSoldOut ? 'sold-out' : ''}">${item.name}</span>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-sold-out-button"> 품절 </button>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm mr-1 menu-edit-button"> 수정 </button>
                    <button type="button" class="bg-gray-50 text-gray-500 text-sm menu-remove-button"> 삭제 </button>
                    </li>`;
            }).join('');
        menuList.innerHTML = template;
        commonUpdateMenuCount();
    };

    const commonUpdateMenuCount = () => {
        const menuCount = this.menu[this.currentCategory].length;
        $('.menu-count').innerText = `총 ${menuCount} 개`;
    };
    
    const commonAddMenuName = async() => {
        if(idMenuName.value === ''){
            alert('값을 입력해주세요.');
            return;
        }

        const duplicaterItem = this.menu[this.currentCategory].find(menuItem => menuItem.name === idMenuName.value)
        if(duplicaterItem){
            alert('이미 등록된 메뉴입니다. 다시 입력해주세요.');
            idMenuName.value = "";
            return;
        }

        const menuName = idMenuName.value;
        await MenuApi.createMenu(this.currentCategory, menuName);
        render();
        idMenuName.value = "";
    };

    const updateMenuName = async(e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        const menuName = e.target.closest('li').querySelector('.menu-name');
        const updateMenuName = prompt('메뉴명을 수정하세요', menuName.innerText);
        await MenuApi.updatemenu(this.currentCategory, updateMenuName, menuId);
        render();
    };

    const removeMenuName = async(e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        if(confirm('정말 삭제하시겠습니까?')){
            await MenuApi.deleteMenu(this.currentCategory, menuId);
            render();
        }
    };

    const soldOutMenu = async(e) => {
        const menuId = e.target.closest('li').dataset.menuId;
        await MenuApi.toggleSoldOutMenu(this.currentCategory, menuId);
        render();
    };

    const changeCategory = (e) => {
        const isCategoryBtn = e.target.classList.contains('cafe-category-name');
        if(isCategoryBtn){
            const catagoryName = e.target.dataset.categoryName;
            this.currentCategory = catagoryName;
            $('#category-title').innerText = `${e.target.innerText} 메뉴 관리`;
            render();
        }
    };

    const initEventListeners = () => {
        menuList.addEventListener('click', (e) => {
            if(e.target.classList.contains('menu-edit-button')){
                updateMenuName(e);
                return;
            }
    
            if(e.target.classList.contains('menu-remove-button')){
                removeMenuName(e);
                return;
            }
    
            if(e.target.classList.contains('menu-sold-out-button')){
                soldOutMenu(e);
                return;
            }
        });
    
        menuForm.addEventListener('submit', (e) => {
            e.preventDefault();
        });
    
        submitBtn.addEventListener('click', commonAddMenuName);
        
        idMenuName.addEventListener('keypress', (e) => {
            if(e.key !== 'Enter') return;
            commonAddMenuName();
        });
    
        $('nav').addEventListener('click', changeCategory);
    };
}
const app = new App();
app.init('menu');

 

👉 작업 순서

1. BASE_URL 웹 서버 변수명 선언
2. 비동기 처리하는데 해당하는 부분이 어디인지 확인하고, 웹서버에 요청하게끔 코드 짜기
3. 서버에 요청한 후 데이터를 받아서 화면에 렌더링 하기
4.리팩터링
- LocalStorage 부분 지우기
- API 파일 따로 만들어서 진행
- 페이지 렌더링과 관련해서 중복되는 부분들 제거
- 서버 요청 할 때 option 객체(POST, PUT, DELETE) 분리한다.
- request 함수따라(응답 데이터가 있을때랑, 없을때를 구분) 분리한다.
- 카테고리 버튼 클릭 시 콜백함수 분리
5.사용자 경험 부분
- API 통신이 실패하는 경우에 대해 사용자가 알 수 있게 alert으로 예외처리를 진행한다.
- 중복되는 메뉴는 추가할 수 없다.

여기 까지 작업한 파일을 공유합니다. 😃😃

moonbucks.zip
0.73MB

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.