본문 바로가기

JavaScript/[강의] 문벅스 -ing

step2. 상태관리로 메뉴 관리하기

* 이 자료는 제가 복습하기 위해서 작성한 내용입니다. 강의내용과 제 생각이 한 데 섞여있어서 본 내용이 정확하지 않을 수 있습니다. 제대로 공부하시려면 유데미 강의를 구매하시길 추천드립니다.

 

0.  step 2을 통해서 내가 배운 것

1) 기술적

- 상태 & 로컬스토리지 개념

- localStorage에 메뉴 상태를 저장하여 관리하기 (메뉴 추가, 수정, 삭제, 불러오기)

- 계층적인 데이터 상태를 저장하고 관리하기

- 모듈화 하여 export, import 하는 법

- 다양한 메서드들 또는 방법

▶ localStorage에 저장된 데이터 세팅하는: localStorage.setItem(Key, Value)

▶ localStorage에 저장된 데이터 가져오는: localStorage.getItem(Key)

▶ 객체를 문자열로 변환해주는: JSON.stringfy(객체)

▶ 문자열을 JSON 객체로 파싱후 리턴하는: JSON.parse(문자열)

▶ html template을 string으로 return 한 다음 html 모양의 string을 합치는, 즉 배열의 모든 요소를 연결해 하나의 문자열로 만드는: join("")

▶ html 코드 안에 데이터 속성넣는 방법과 JS에서 데이터 속성에 접근하는 방법

 

2) 태도

- 동작하는 앱에서 어떤 데이터가 변하는가? 질문하게 된 점
- 데이터(여기서는 menu)가 어떤 형태의 데이터로 들어오게 될런지 미리 생각하게 된 점

- 주로 데이터를 저장하는 곳은 "객체로 만들어서 관리한다"는 점

( 여기에선 최종적으로 다음과 같이 데이터 관리를 했다 { espresso: [], frappucino: [], desert: [] }  )

데이터가 변하기 때문에 관리해줘야 한다는 관점

- 상태관리가 중요한 이유: 상태관리란 변하는 데이터를 관리한다는 의미인데(데이터를 추가한다거나, 삭제한다거나, 업데이트할 때 잘 반영한다는 의미) 이는 사용자의 interaction을 잘 반영한다는 말과 같다. 상태값이 잘 저장되고 불러와져야 동적인 앱을 만들 수 있다.

- true/false 로 return 되는 것들은 변수화 시켜서 관리하는 점

- 최대한 한 파일에 객체는 하나만 있는게 파일명을 대표하는 이름으로 짓기 편하다는 점

 

3) 내게 아직 어려운 부분, 자꾸 까먹는 부분

- 자바스크립트 변수는 함수가 될 수 있다는 점
- 함수 실행을 통해서 얻은 값이 필요하다면 꼭 리턴하기

 

 

1. 요구사항 분석

1) 요구사항 분석 후 알게 된 점

- 변하는 데이터는 변하기 때문에 관리가 필요하다. 프로그래밍 관점에서 변하는 데이터 관리를 "상태 관리"라고도 한다.

- stpe2에서는 데이터를 localStorage에 저장하고, localStorage에 저장된 데이터를 불러오는 등의 작업을 한다.

데이터는 추가하고(생성하고), 불러오고(읽고), 수정하고(업데이트하고), 지우고(삭제하고) 이 방향으로 처리된다.

- 과정

① App 함수 바깥에 localStorage에 담을 데이터를 세팅하고, localStorage에 있는 데이터를 가져올 수 있는 객체를 만든다.

② App() 함수내에 변하는 데이터를 관리할, 담을 곳을 정한다. this.menu = []; 여기서는 이와 같이 초기화하였다. 이 때 데이터를 담을 곳을 배열로 정했다. 왜? 여러개의 데이터가 들어올 건데 배열로 관리하면 다양한 메서드를 쓸 수 있고 관리가 편하기 때문이다.

③ localStorage에 데이터가 있으면 읽어서 this.menu 변수에 업데이트하고 화면에 렌더링한다. 없으면 빈 배열 그대로 둔다.

④ this.menu에 데이터를 추가하고(push), 배열의 인덱스를 활용해 원소 값을 바꾸고, 삭제한다(splice).

⑤ 변경된 데이터를 담고 있는 this.menu를 localStorage에 반영시킨다.

 

왼쪽의 요구 사항이 >> 오른쪽 요구 사항으로 구체화, 세분화, 명료화 됨

 

 

2. localStorage 개념

1) step2에서 localStorage를 쓰는 이유

- 원래는 서버에서 데이터를 읽어오고, 업데이트한 데이터를 서버에 다시 보내는 등의 작업을 해야하는데, 이 과정은 step3에서 진행된다. step2에서는 브라우저에 데이터를 저장하기 위해서 localStorage를 사용한다.

https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage

 

2) localStorage 기본 사용방법

- localStorage 데이터 세팅하기(키와 값은 항상 문자열~)

- localStorage 데이터 가져오기

localStorage.setItem("menu", "espresso");
localStorage.getItem("menu");

 

콘솔 application 탭 > Storage > Local Storage 에서 Key가 "menu"이고 Value가 "espresso" 를 확인 가능

 

 

3. localStorage 데이터(배열 또는 객체) 저장 및 불러오기

구현 목표: localStorage에 데이터를 저장하고 불러오는 함수를 간편하게 쓰기 위해서 객체로 만들어준다.

=> App() 함수 바깥에 객체를 만들어 데이터를 세팅하고, 가져오는 메서드를 만든다.

 

1) 객체나 배열을 문자열로 바꿔주는 메서드: JSON.stringify()

- localStorage.setItem(Key, Value)에서 Value 파라미터로 넘기는 데이터가 객체나 배열일 경우 문자열로 치환 후 파라미터로 전달해야 한다. 

 

2) 문자열을 객체로 다시 변환해주는 메서드: JSON.parse();

- localStorage.getItem(Key), 해당 Key에 해당하는 Value의 데이터 타입은 문자열이다. 

문자열로는 데이터를 추가, 수정할 수 없기 때문에 객체로 다시 파싱(변환)해줘야 한다.

const store = {
  setLocalStorage: (menu) => {
    localStorage.setItem("menu", JSON.stringify(menu));
  },
  getLocalStorage: () => {
    return JSON.parse(localStorage.getItem("menu"));
  }
}

function App () {
  ...
}

 

 

4. 상태관리 (1) (feat.배열)

구현 목표: 앱 내에서 변하는 데이터를 찾고 변하는 데이터를 관리하기 위해서, 즉 상태 관리를 위해서 상태를 저장할 곳을 선언한다.

=> App() 함수 내에 메뉴명을 관리할 변수를 선언한다. 여러개의 메뉴명을 다루기 때문에 배열로 선언한다.

 

1) 변할 수 있는 데이터 = 상태 = (이 앱에서는) 메뉴명

변하니깐 관리를 해줘야 함.

이 앱에서는 메뉴명이 변하기 때문에 메뉴명을 관리해보자.

// index.js
function App () {
  this.menu = []; // 앞으로 여기다가 상태관리
  ...
}
const app = new App(); // 생성자 함수로 선언해야만 App 함수내의 this가 App 자기 자신을 가리키게 된다.

 

2) 왜 this.menu 를 배열로 관리할까?

- 메뉴명이 여러개 추가될 수 있으니깐 배열로 관리(나중에는 객체로 바꿔줄 예정)

- 이렇게 배열형태로 초기화를 해줘야만 나중에 this.menu.push 와 같은 배열 메서드를 쓸 수 있다.

- 다른 사람들과 협업할 때, 이 곳에는 어떤 데이터형태로 관리가 되는지 한 눈에 파악할 수 있기 때문에 꼭 this.menu = []; 이런식으로 초기화를 해주자.

 

* 여기서 왜 this.menu 에서 this를 사용해서 관리하는 것일까?

new 연산자를 사용하여 생성자 함수를 호출하게 되면 이때의 this는 "만들어질 객체" 를 참조하게 되기 때문이다. 

왜 생성자 함수로 호출하느냐고? 그야 나중에 여러개의 인스턴스를 만들 수 있기 때문에.

그리고 그 인스턴스마다 상태가 다 다를텐데 관리되어야 할 menu가 각각의 인스턴스를 가리키기 위해서는 this.를 써서 관리해야만 한다.

 

 

데이터가 변하는 부분 => 메뉴명 추가, 메뉴명 삭제, 메뉴명 수정 하는 부분 

즉 이 부분은 관리가 필요하다.!!

5. 데이터 추가 시 상태 관리

구현 목표: 데이터 변경 시, 변경된 데이터를 저장한 뒤 재렌더링한다.

=> 메뉴명 추가시 this.menu = [] 에 반영해준다. 데이터를 localStorage에 반영해준다. 데이터가 반영된 화면을 다시 그려준다.

 

1) 메뉴명을 추가하면 push 메서드를 통해 { name: "추가한 메뉴명 이름" } 이와 같은 형태로 추가해주자.

this.menu.push({name: "espresso"})

 

2) map 함수를 사용하여 각 메뉴명을 화면에 그려주자.

this.menu = [ { name: "espresso" }, { name: "americano" }, { name: "double shot" } ] 는 이와 같은 형태로 데이터가 들어감

this.menu에 있는 원소들을 다 돌면서 각 메뉴를 그려줘야 하기 때문에 map 함수를 이용하여 렌더링 해준다.

const addMenuName = () => {
  //...
  //...
  
  const template = this.menu.map((menuItem) => {
    return '<li>${menuItem.name}</li>'
  }).join("");
  
  $("#menu-list").innerHTML = template;
  
  //...
  //...
}

 

❗❗ 잠깐!! 그런데 왜 map 함수로 마크업을 다시 그려주는 것을 하는 것일까?

메뉴명이 추가할 때마다 그 추가된 메뉴명만 마크업해서 기존에 붙여두면 되잖아?

map을 쓰게 되면  메뉴명이 추가할 때마다 this.menu에 들어 있는 메뉴들을 다 돌면서 마크업을 그리게 되면 더 비효율적일거라고 생각이 되는데...

 

만약 메뉴명이 10개라고 한다면

1번 방식(insertAdjacentHTML)으로 마크업을 그려준다면 10번만 그리면되지만

2번 방식(map)으로 하게 되면 메뉴명을 10개까지 하나 하나씩 추가한다고 할때, 메뉴명 마크업은 처음엔 1개, 다음엔 2개, .. 이런식으로 마크업을 진행하게 되면서 총 55번 을 그려야 해서 비효율적일 것 같은데...

 

------

(작업 진행 하다보니)

메뉴를 수정할 때 내가 수정하고자 하는 메뉴를 알수 있는 고유한 정보가 필요하다(어떤 메뉴가 선택되었는지 컴퓨터가 알수 있도록 해줘야 한다).

메뉴 정보가 들어가 있는 배열(this.menu)의 각 원소들의 인덱스 값을 li 마크업의 데이터 속성 data-menu-id에 값으로 부여하면,

그 인덱스 값을 통하여 데이터를 수정이 가능하다. 
map 메서드를 쓰면 화면이 다시 렌더링 될 때마다 index 값이 재부여되기 때문에 데이터 관리에 훨씬 용이하다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map

 

3) 상태가 변경됐을 때 바로 storage 에 저장해주자.

store.setLocalStorage(this.menu);

 

 

6. 데이터 수정 시 상태 관리

구현 목표: 데이터 변경 시, 변경된 데이터를 저장한 뒤 재렌더링한다.

=> 메뉴명 수정시 this.menu 에서 수정할 메뉴를 찾아서 메뉴 이름을 수정한다. this.menu 데이터를 localStorage에 반영해준다. 데이터가 반영된 화면을 다시 그려준다.

 

1) 수정버튼을 클릭한 메뉴를 어떻게 식별해서 this.menu 배열안의 데이터를 바꿔줄 수 있을까?

>> 템플릿을 그리는 코드 안에서(지금은, 메뉴명을 추가하는 함수 안에 템플릿을 그리는 코드가 있다) 사용자가 선택하고 이벤트 핸들링할 요소의 유일한 값을 확인하기 위해서, 유일한 id인 값을 넣어서 템플릿이 그려지도록 만들자.

 

1-1) data-attribute 속성을 이용해서 생성될 li마다 유일한 id(this.menu의 index값) 를 저장해준다.

1-2) 템플릿을 그릴 때 map 메서드에서 두번째 인자로 this.menu의 index 값을 li 의 data-attribute 속성에다가 넣어준다.

 

3) 메뉴명 수정하는 함수 안에

수정할 메뉴 li 의 data-menu-id에 들어가는 값을 변수로 미리 저장하고(아래 코드 3번 line)

수정한 메뉴명을 화면과(10번 line) this.menu에 반영한다(11번 line).

// 메뉴명 수정
  const updateMenuName = (e) => {
    const menuId = e.target.closest("li").dataset.menuId;// <<<<<<< 이렇게 
    const $menuName = e.target.closest("li").querySelector(".menu-name");
    const updatedMenuName = prompt(
      "메뉴명을 수정해주세요.",
      $menuName.innerText
    );

    $menuName.innerText = updatedMenuName; // <<<<<<< 이렇게 
    this.menu[menuId].name = updatedMenuName; // <<<<<<< 이렇게 

    console.log(menuId);
    console.log(this.menu);
  };

 

* 배열에서 인덱스 값으로 원소를 찾을 때 배열[인덱스 값] 형식으로 찾으면된다.

여기에서는 인덱스 값을 menuId라는 변수에 저장했기 때문에 this.menu[menuId] 이와 같은 표현으로 배열내에 인덱스값에 해당하는 원소를 찾을 수 있다.

https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/Use_data_attributes

 

4) 수정한 메뉴명을 로컬스트로지에도 반영하기

store.setLocalStorage(this.menu);

 

 

7. 데이터 삭제 시 상태 관리

구현 목표: 데이터 변경 시, 변경된 데이터를 저장한 뒤 재렌더링한다.

=> this.menu 에서 삭제될 메뉴를 찾아서 메뉴를 삭제한다. this.menu 데이터를 localStorage에 반영해준다. 데이터가 반영된 화면을 다시 그려준다.

 

삭제버튼을 클릭한 메뉴를 어떻게 식별해서 this.menu 배열 안의 데이터를 바꿔줄 수 있을까?

 

1) html <li> template 안에 data-menu-id 속성에 부여했던 값을 통해서 삭제한다.

삭제 버튼 클릭시 가장 가까운 li 태그 안에 data-menu-id에 부여된 값(index 값)을 저장하고 this.menu[index 값].splice 메서드를 통해서 삭제한다.

 

2) this.menu에서도 메뉴를 삭제해주고, 변경된 this.menu의 데이터를 localStorage에 반영한다.

const removeMenuName = (e) => {
  if (confirm("정말 삭제하시겠습니까?")) {
    const menuId = e.target.closet("li").dataset.menuId;
  	this.menu.splice(menuId, 1);
  	store.setLocalStorage(this.menu);
    
    //...
  }
}

 

* splice는 배열 내에 있는 특정 인덱스의 값을 삭제할 수 있을 뿐만 아니라 추가도 가능하다!

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/splice

 

3) this.menu에 데이터가 변경되었으니 다시 재렌더링 해준다.

왜? 다시 렌더링 해주지 않으면 li태그 data-menu-id 속성에 들어간 값이 재세팅되지 않는데 

메뉴명을 삭제 후 다른 메뉴 수정시 this.menu의 인덱스값과 data-menu-id의 값이 일치하지 않아 에러가 발생한다.

 

따라서 this.menu를 돌면서 다시 마크업을 해준다.

메뉴명 추가하는 함수에도 다시 마크업을 해주는 코드가 있기 때문에 렌더링하는 함수를 만들어 재사용 가능한 함수로 만든다.

const renderMenuItem = (menuItem) => {
  const template = this.menu.map((menuItem, index) => {
    return `<li data-menu-id="${index}">${menuItem.name}</li>`;
  }).join("");
  
  $("#menu-list").innerHTML = template;
}
const removeMenuName = (e) => {
  if (confirm("정말 삭제하시겠습니까?")) {
    const menuId = e.target.closet("li").dataset.menuId;
  	this.menu.splice(menuId, 1);
  	store.setLocalStorage(this.menu);
    
    renderMenuItem();
    //...
  }
}

 

 

8. 새로고침할 때 localStorage에 있는 데이터를 가져와서 렌더링 하기: init 함수 만들기

구현 목표: 이 앱이 브라우저에서 최초로 실행될 때 실행할 init 함수 만들기

=> localStorage에 데이터가 있으면 this.menu 에 데이터를 할당하고, 메뉴 리스트별로 화면에 렌더링 시켜준다.

 

localStorage에 데이터가 있으면(데이터의 길이가 1이상 이면) 그 데이터를 this.menu에 할당하고, 메뉴 리스트별로 화면에 렌더링 시켜준다. 

function App() {
  this.menu = [];
  this.init = () => {
    if (store.getLocalStorage().length > 0) {
      this.menu = store.getLocalStorage();
    }
  }
}

 

 

9. 상태관리(2) (객체로 관리)

구현 목표: 여러개의 데이터를 따로 관리하기

=> 각 카테고리에 따른 데이터 관리하기

 

0) 에스프레소, 프라푸치노, 블렌디드 등 각 카테고리마다 관리되는 데이터가 다름

- 기존에 메뉴명을 저장하던 데이터는 아래와 같이 하나의 배열안에 여래 개의 객체 형태로 관리되었음

this.menu = [{name: "espresso"}, {name: "americano"}, {name: "caffe latte"}]

- 메뉴명을 저장하던 this.menu = [] 초기 형태를 카테고리로 분류해야하기 때문에 업데이트 필요한데? 

어떻게?

this.menu = { espresso: [], frappucinno: [] .. } 이렇게 하나의 객체로 만들어서 다뤄줘야 함...!!

데이터의 계층을 구축해야 함

 

하나의 객체 안에 속성들을 나누고 그 속성마다마다 각각의 데이터들을 관리해야만 한다.

this.menu = {
  espresso: [],
  frappuccino: [],
  blended: [],
  teavana: [],
  desert: []
}

// 앞으로 메뉴명이 추가되면 이런형태고 데이터가 관리되겠지
this.menu = {
  espresso: [{name: "espresso"}],
  frappuccino: [],
  blended: [],
  teavana: [],
  desert: [{name:"orange juice"}, {name: "blah blah"}]
}

 

1) 카테고리 버튼을 클릭할 때 이벤트 발생시키기(+버튼이 아닌 빈 공간을 눌렀을 때 이벤트 X)

* 이 때, e.target.classList.contains("클래스명") 이거는 ture or false 로 나오게 되는데

이걸 변수화 시켜서 관리를 함 const isCategoryButton = e.target.classList.contains("클래스명")

$("nav").addEventListener("click", (e) => {
  const isCategoryButton = e.target.classList.contains("cafe-category-name");
  if (isCategoryButton) {
    //...
    
  }
})

 

2) 현재 카테고리를 설정하는 변수를 만들고 / 각 카테고리 버튼을 누를 때마다 / 현재 카테고리를 클릭한 카테고리로 재할당하기

- 왜 App 함수 바로 하위에 현재 카테고리를 변수를 만들고 버튼을 클릭할 때마다 현재 카테고리 변수를 재할당해야만 할까???

>>> 현재 카테고리를 알아야만 this.menu에서 해당 카테고리의 속성에 접근할 수 있기 때문이다. 

 

- 각 카테고리 button에 있는 data attribute 속성을 활용해 categoryname 으로 저장

function App() {
	
    this.menu = { ... };
    
    this.currentCategory = "espresso";
    
    this.init = () => { ... };
    
	// ...
    
    $("nav").addEventListener("click", (e) => {
      const isCategoryButton = e.target.classList.contains("cafe-category-name");

      if (isCategoryButton) {
        const categoryName = e.target.dataset.categoryName;
        this.currentCategory = categoryName;
      }

    })
}

 

3) 상태관리 하는 데이터의 형태가 바뀌었으니 데이터 추가, 데이터 수정, 데이터 삭제, 카테고리 버튼 클릭 시 상태 관리하는 코드 전체 수정

* 객체의 속성에 접근 방법 => 객체이름[속성이름], this.menu[this.currentCategory] 

const renderMenuItem = () => {
  const template = this.menu[this.currentCategory].map((menuItem, index) => {
      return `<li data-menu-id="${index}">${menuItem.name}</li>`
    }).join("");
  
  $("#menu-list").innerHTML = template;
  
  //...
}

const addMenuName = () => {
  const menuName = $("#menu-name").value;
  this.menu[this.currentCategory].push({ name: menuName });
  store.setLocalStorage(this.menu);
  renderMenuItem();
};

const updateMenuName = (e) => {
  const menuId = e.target.closest("li").dataset.menuId;
  const $menuName = e.target.closest("li").querySelector(".menu-name");
  const updatedMenuName = propmpt("메뉴명을 수정해주세요", $menuName.innerText);
  $menuName.innerText = updatedMenuName;
  this.menu[this.currentCategory][menuId].name = updatedMenuName;
  
  store.setLocalStorage(this.menu);
}

const removeMenuName = (e) => {
  if (confirm("정말 삭제하시겠습니까?")) {
    const menuId = e.target.closest("li").dataset.menuId;
    this.menu[this.currentCategory].splice(menuId, 1);
    store.setLocalStorage(this.menu);
  
    renderMenuItem();
  }
}

 

4) this.menu 안에 있는 값들이 바뀌었다면 store.setLocalStorage(this.menu) 코드를 통해 바뀐 데이터를 localStorage에 반영시키기

 

 

10. 상태관리(3) (객체의 속성 내 배열의 원소에 속성 추가하기)......

구현 목표: 객체의 속성 내 배열의 원소에 속성 추가하기

=> 카테고리 내 각 메뉴에 품절 속성 추가

 

1) 품절 버튼 클릭 시 클래스 추가 & 삭제 반복 

"품절" 버튼 클릭 시 메뉴명 li에 "sold-out" 클래스 추가 및 삭제

2) 품절 버튼 클릭 시 메뉴 데이터에 품절 속성 추가

3) 변경된 데이터 localStorage에 반영

(불필요한 연산이 일어나지 않도록 적절하게 if 문에서 실행하고 뒷 부분을 실행할 필요가 없으면 return 하기)

const soldOutMenu = (e) => {
  const isSoldOut = e.target.closest("li").classList.contains("sold-out");
  const menuId = e.target.closest("li").dataset.menuId;

  if (!isSoldOut) {
    e.target.closest("li").classList.add("sold-out");
    this.menu[this.currentCategory][menuId].isSoldOut = true;
  } else {
    e.target.closest("li").classList.remove("sold-out");
    this.menu[this.currentCategory][menuId].isSoldOut = false;
  }

  store.setLocalStorage(this.menu);
}

$("#menu-list").addEventListener("click", (e) => {
  if (e.target.classList.contains("menu-sold-out-button")) {
    soldOutMenu(e);
    return;
  }
})

 

4) 렌더링하는 과정에서 품절 데이터를 활용하여 돔렌더링 하기 위해 처리해준다.

 

*클릭할 때마다 토글처럼 soldOut 속성이 true & false로 반복해서 바뀌게 하려면 5~6번 line처럼 작성하면 간편하다.

  // 메뉴 품절
  const soldOutMenu = (e) => {
    const menuId = e.target.closest("li").dataset.menuId;

    this.menu[this.currentCategory][menuId].soldOut =
      !this.menu[this.currentCategory][menuId].soldOut;
    store.setLocalStorage(this.menu);
    renderMenuItem();
  };

 

 

11. 리팩토링

1) 중복되는 코드는 함수로 만들어서 사용하기

2) 이벤트 관련 코드를 initEventListners() 로 만들어서 하나로 관리하기

3) 한 파일 안에는 최대한 하나의 객체만 넣기

 

 

* 회고

고통스러운만큼 그리고 그걸 승화한만큼 나의 자산이 된다.

단순히 코드를 따라 치는 것이 아니라 흐름, 생각을 훔치자!