코드스테이츠_S3U4_3W_화,수,목

윤뿔소·2022년 11월 1일
0

CodeStates

목록 보기
31/47

우린 저번에 리액트를 배우며 상태에 대해 배웠었다.

(프론트엔드로서) 상태는 UI에 동적으로 표현되는 데이터

예를 들어 장바구니가 있다.변하는 값을 가진 탭, 물품, 체크박스, 수량이 변하면 금액, 총량 등의 상태도 변한다.

그래서 '리액트로 사고하기'를 보면 '상태를 어디에 놓을 것 인지'가 리액트의 핵심이다. 이번엔 그 상태를 생성, 관리 등을 자세하게 배울 것이다.

전역 상태 관리

'리액트로 사고하기'를 봤을 때 단방향 데이터 흐름을 가진 컴포넌트들이 어떤 상태를 가지는지 정해야한다고 했다. 여기에 더해 '전역 상태', '로컬 상태'도 배워보자.

  • 우리는 JS에서 이미 전역, 로컬의 개념을 알았다. 여기에 더해 전역 변수는 다양한 함수, 변수들이 사용할 때 선언하지만 남용하지 말라는 것도 배웠다. 상태에도 똑같이 개념이 적용된다.
  • 전역 상태: 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태
    • 다크 모드(모든 페이지, 모든 컴포넌트 적용되기에), 국제화(Globalization) 설정(UI 언어를 바꾸는 등)
  • 로컬 상태: 컴포넌트 내에서만 영향을 끼지는 상태
    • input box, select box 등등
  • 장바구니
    • 예를 들어 위 사진을 볼 때, 수량 입력하는 Form 데이터 등이 컴포넌트 내에서 실행되는 로컬 상태이라한다.
    • 전역 상태는 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태이므로 상품 선택 여부에 따라 금액에 영향을 주는, 담긴 물품의 갯수를 표현할 때
  • 그래서! 상태 관리 툴을 사용해야한다.
    1. 전역 상태를 위한 저장소를 제공
    2. props drilling 방지(상태 끌올 같은 문제)
    • React Context, Redux, MobX 등

참고: 데이터 무결성을 위해!

우리는 왜 상태관리를 엄격하고 철저하게 해야할까? 답을 하자면 위를 보더라도 상태가 조금 꼬이면 단방향, 단일 데이터 흐름에 위배된다. 왜 지켜야하는데??

Single source of truth(신뢰할 수 있는 단일 출처) 원칙

  • 동일한 데이터는 항상 같은 곳에서 데이터를 가지고 오도록
  • 데이터가 존재하고, 그 데이터를 보여줘야 하는 프론트엔드에서는 철저하게 우리가 의도한 대로 예외 상황 없이 데이터를 잘 보여주어야 할 것
  • 프론트엔드 뿐만 아니라 데이터를 다루는 업들은 모두가 가져야할 덕목

즉 상태가 꼬이면 저런 원칙에 위배돼 별로 좋지않은 현상이 발생하니 주의하자. 그래서! 상태 관리가 좋아야한다.

Props Drilling

상태를 관리할 때 꼭 알아둬야할 것이다.

상위 컴포넌트의 state를 props를 통해 전달하고자 하는 컴포넌트로 전달하기 위해 그 사이는 props를 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 데이터를 전달하는 현상

위 그림처럼 A에서 쓰는 상태를 I 컴포넌트가 써야하는데 그 상태가 필요하지 않는 C, G를 거쳐 Props를 내려줘야한다. Props Drilling이 발생하는 것!
1. 해당 상태를 직접 사용하지 않는 최상위 컴포넌트, 컴포넌트1, 컴포넌트2도 상태 데이터를 가짐
2. 상태 끌어올리기, Props 내려주기를 여러 번 거쳐야 함
3. 애플리케이션이 복잡해질수록 데이터 흐름도 복잡해짐
4. 컴포넌트 구조가 바뀐다면, 지금의 데이터 흐름을 완전히 바꿔야 할 수도 있음

당연히 문제가 생긴다.

  • 코드의 가독성이 매우 떨어짐 => 유지•보수가 힘들어짐
  • state 변경시 불필요하게 관여된 컴포넌트들 또한 리렌더링이 발생 => 웹 성능에 악영향!!

방지법

  • 최대한 상태와 관련있는 컴포넌트는 붙이기
  • 상태 관리 라이브러리(Redux, Context api, Mobx, Recoil 등) 사용하기
    • 전역으로 관리하는 저장소에서 직접 상태를 꺼내쓸 수 있기 때문에 Props Drilling을 방지

페어과제 1

이번엔 State hook을 사용하는 방법을 다시 복습할 것이다.
이미 어느정도 완성되어있는 쇼핑몰 홈페이지가 있었고 여기서 장바구니 담기, 삭제, 상품갯수 업데이트기능을 작동하도록 구현

구조

├── /Cmarket Hooks
│   ├── /public  
│   └── /src
│        ├── /assets
│        ├─── /state.js // 데이터
│        ├── /conponents
│        ├─── /CartItem.js // 장바구니 렌더링
│        ├─── /Item.js // 배치된 쇼핑아이템들 렌더링
│        ├─── /Nav.js // 네비게이션바
│        ├─── /OrderSummary.js
│        ├── /pages
│        ├─── /ItemListContainer.js // 장바구니 페이지
│        ├─── /ShoppingCart.js // 인덱스 페이지
│        ├── App.css
│        ├── App.js 
│        ├── index.js
중략

App.js

import React, { useState } from "react";
import Nav from "./components/Nav";
import ItemListContainer from "./pages/ItemListContainer";
import "./App.css";
import "./variables.css";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ShoppingCart from "./pages/ShoppingCart";
import { initialState } from "./assets/state";

function App() {
  const [items, setItems] = useState(initialState.items);
  const [cartItems, setCartItems] = useState(initialState.cartItems);

  return (
    <Router>
      <Nav cartItems={cartItems} />
      <Routes>
        <Route path="/" element={<ItemListContainer items={items} cartItems={cartItems} setCartItems={setCartItems} />} />
        <Route path="/shoppingcart" element={<ShoppingCart cartItems={cartItems} setCartItems={setCartItems} items={items} />} />
      </Routes>
      <img id="logo_foot" src={`${process.env.PUBLIC_URL}/codestates-logo.png`} alt="logo_foot" />
    </Router>
  );
}

export default App;

보면 useState를 전역 상태로 선언해서 각 컴포넌트를 사용할 수 있게 해놨고 react-router-dom 을 이용해 Client Side Routing을 구현해놨음, Nav에 Link도 구현

Todo1. 장바구니에 추가 및 상품 개수 업데이트

  • 메인 화면에서 [장바구니 담기] 버튼을 누른 후, 장바구니 페이지로 이동하면 상품이 담겨있어야 함
  • 내비게이션 바에 상품 개수가 즉시 표시되어야 함

ItemListContainer.js

  1. onClick={(e) => handleClick(e, item.id)}handleClick의 인자가 무얼 받는지 먼저 봐야한다.
  2. 요소 컴포넌트인 Item.js의 버튼과 관련된 props를 연결시켜줘야한다.(handleClick)
    또 만약 해당 품목이 있다면 수량만 올라가게 해야한다.
  3. 장바구니를 조작해야하기에 cartItems의 state와 조작 함수를 가져와 iditemId를 매칭해줘 추가하지만 조건도 붙인다.
function ItemListContainer({ items, cartItems, setCartItems }) {
  const handleClick = (event, id) => {
    // findIndex를 사용해 인덱스가 반환한다는 걸 이용하여 조건 및 추가
    let isInCart = cartItems.findIndex((el) => el.itemId === id);
    (isInCart === -1)
      ? setCartItems([
          ...cartItems,
          {
            itemId: id,
            quantity: 1,
          },
        ])
      : (cartItems[isInCart].quantity += 1);
  };
  return (
    <div id="item-list-container">
      <div id="item-list-body">
        <div id="item-list-title">쓸모없는 선물 모음</div>
        {items.map((item, idx) => (
          <Item item={item} key={idx} handleClick={handleClick} />
        ))}
      </div>
    </div>
  );
}

장바구니 개수를 보여줘야하므로 cartItems를 Props로 가져와 즉각적으로 반영하기

function Nav({ cartItems }) {
  return (
    <div id="nav-body">
      <span id="title">
        <img id="logo" src="../logo.png" alt="logo" />
        <span id="name">CMarket</span>
      </span>
      <div id="menu">
        <Link to="/">상품리스트</Link>
        <Link to="/shoppingcart">
    	  // length!
          장바구니<span id="nav-item-counter">{cartItems.length}</span>
        </Link>
      </div>
    </div>
  );
}

Todo2. 장바구니로부터 제거

  • 장바구니 페이지에서 [삭제] 버튼을 누른 후, 해당 상품이 목록에서 삭제

ShoppingCart.js

삭제 버튼의 속성인 onClick={() => { handleDelete(item.id) }의 인자를 보면 알겠지만 이벤트 객체의 id를 반환한다.
그걸 ShoppingCart.jshandleDelete함수에 연결시켜 filter해주면 끝!

const handleDelete = (itemId) => {
  setCheckedItems(checkedItems.filter((el) => el !== itemId));
  setCartItems(cartItems.filter((el) => el.itemId !== itemId));
};

Todo3. 수량 수정

handleQuantityChange 구현해봄

ShoppingCart.js

const handleQuantityChange = (quantity, itemId) => {
  let revQuantity = [...cartItems];
  let revIdx = cartItems.findIndex((el) => el.itemId === itemId);
  revQuantity[revIdx].quantity = quantity;
  setCartItems(revQuantity);
};

⭐️포인트: [...cartItems]를 참조형 얕은 복사 시키듯이 복사하고 quantity 재할당 해준 다음 setCartItems를 적용시키는 것! 그냥 하면 안돼!

Redux

위에서 배웠던 것처럼 만약 1~6의 컴포넌트들이 있고 1 => 6으로 가야하는 상태가 있다면 Props Drilling이 일어난다.
하지만 Redux는 전역 상태를 만들 수 있는 Store를 만들어 편하게 상태를 공유할 수 있다!

+ 참고: 역할 축약하기

  • Action(임원 : 일 안하고 프로세스만 만들어 놓음.)
  • Dispatch(입으로 일하는 녀석)
  • Reducer(실제로 갈리는 실무자)
  • Store(일 담아두는 창고)

Redux 사용법: 코드 구성

  1. 상태가 변경되어야 하는 이벤트가 발생하면, 변경될 상태에 대한 정보가 담긴 Action 객체가 생성
  2. 이 Action 객체는 Dispatch 함수의 인자로 전달
  3. Dispatch 함수는 Action 객체를 Reducer 함수로 전달해줌
  4. Reducer 함수는 Action 객체의 값을 확인하고, 그 값에 따라 전역 상태 저장소 Store의 상태를 변경
  5. 상태가 변경되면, React는 화면을 다시 렌더링

즉! Action → Dispatch → Reducer → Store로 단방향을 가진 데이터 흐름!

Store

상태가 관리되는 오직 하나뿐인 저장소의 역할, state가 저장돼있는 저장소

import { createStore } from 'redux';
const store = createStore(rootReducer);

실습

// .md, index.js 로 기본적인 걸 알려줌
Redux를 사용하기 위해서는 redux와 react-redux를 설치해야합니다.

- DEPENDENCIES
  redux, react-redux가 설치되어 있는 것을 확인하실 수 있습니다.

1. import { Provider } from 'react-redux';
   react-redux에서 Provider를 불러와야 합니다.
   - Provider는 store를 손쉽게 사용할 수 있게 하는 컴포넌트입니다.
     해당 컴포넌트를 불러온다음에, Store를 사용할 컴포넌트를 감싸준 후
     Provider 컴포넌트의 props로 store를 설정해주면 됩니다.
2. import { legacy_createStore as createStore } from 'redux';
   redux에서 createStore를 불러와야 합니다.
3. 전역 상태 저장소 store를 사용하기 위해서는 App 컴포넌트를
   Provider로 감싸준 후 props로 변수 store를 전달해주여야 합니다.
   주석을 해제해주세요.
4. 변수 store에 createStore 메서드를 통해 store를 만들어 줍니다.
   그리고, createStore에 인자로 Reducer 함수를 전달해주어야 합니다.
   (지금 단계에서는 임시의 함수 reducer를 전달해주겠습니다.)
5. 여기까지가 전역 변수 저장소를 설정하는 방법이였습니다.
   브라우저 창에 오류메세지가 나타나지 않는다면 잘 적용된겁니다.👏
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// 1
import { Provider } from 'react-redux';
// 2
import { legacy_createStore as createStore } from 'redux';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

// reducer는 다음 목차에!
const reducer = () => {};

// 4
const store = createStore(reducer);

root.render(
  // 3
  <Provider store={store}>
    <App />
  </Provider>
);

Reducer

Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수
⭐️이때 Reducer는 순수함수여야 한다. 외부 요인으로 인해 기대한 값이 아닌 엉뚱한 값으로 상태가 변경되는 일이 없어야하기 때문
또한 useState()setUseState()처럼 Immutability(불변성)
를 가지고 있어야한다.

실습

Reducer함수 첫번째 인자에는 기존 state가 들어오게 됩니다.
첫번째 인자에는 default value를 꼭 설정해주셔야 합니다!
그렇지 않을 경우 undefined가 할당되기 때문에 그로 인한 오류가 발생할 수 있습니다.
(https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers#creating-the-root-reducer)
두번째 인자에는 action 객체가 들어오게 됩니다.

action 객체에서 정의한 type에 따라 새로운 state를 리턴합니다.
새로운 state는 전역 변수 저장소 Store에 저장되게 됩니다.

1. 임의 함수 reducer를 conterReducer로 대체해주세요.
2. 가져온 conterReducer를 createStore에 다시 넣어주세요.
3. 주석을 해제해주세요.
4. 예제를 잘 불러오셨다면 정상적으로 화면이 나오는 것을 확인할 수 있습니다!
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { legacy_createStore as createStore } from 'redux';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

// 1
const count = 1;

// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {
  // Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
  switch (action.type) {
    //action === 'INCREASE'일 경우
    case 'INCREASE':
      return state + 1;

    // action === 'DECREASE'일 경우
    case 'DECREASE':
      return state - 1;

    // action === 'SET_NUMBER'일 경우
    case 'SET_NUMBER':
      return action.payload;

    // 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
    default:
      return state;
  }
};
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.

// 2
const store = createStore(counterReducer);

root.render(
  // 3
  <Provider store={store}>
    <App />
  </Provider>
);

참고: 여러개의 reducer를 사용할 때

combineReducers 메서드를 사용하기!

import { combineReducers } from 'redux';
const rootReducer = combineReducers({
  counterReducer,
  anyReducer,
  ...
});

Action 객체

어떤 액션을 취할 것인지 정의해 놓은 객체

// payload가 필요 없는 경우
{ type: 'INCREASE' }
// payload가 필요한 경우
{ type: 'SET_NUMBER', payload: 5 }

이런 식으로 type이 필수로 지정해 어떤 역할하는지 명시하기

보통은 위처럼 Action 객체를 직접 작성하는 것이 아닌 함수로 작성함,이를 액션 생성자(Action Creator)라고도 한다!

// payload가 필요 없는 경우
const increase = () => {
  return {
    type: 'INCREASE'
  }
}
// payload가 필요한 경우
const setNumber = (num) => {
  return {
    type: 'SET_NUMBER',
    payload: num
  }
}

실습

Action은 어떻게 state를 변경할지 정의해놓은 객체입니다.
Action 객체는 Dispatch 함수를 통해 Reducer 함수 두번째 인자로 전달됩니다.

Action 객체 안의 type은 필수로 지정을 해주어야 합니다.
여기서 지정한 type에 따라 Reducer 함수에서 새로운 state를 리턴하게 됩니다.

1. 유어클래스에 있는 Action 예제 중 Action Creator 함수 increase를
   복사해오세요.
2. Action Creator 함수 decrease를 만들어 주세요. type은 'DECREASE'로
   설정해주세요.
3. 앞서 만든 Action Creator 함수를 다른 파일에도 사용하기 위해 export를
   붙혀주세요.
// 모듈 및 DOM 생략

// 1 - 3
export const increase = () => {
  return {
    type: 'INCREASE',
  };
};
// 2 - 3
export const decrease = () => {
  return {
    type: 'DECREASE',
  };
};

// reduce 함수 생략

const store = createStore(counterReducer);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Dispatch

Reducer로 Action을 전달해주는 함수, Dispatch의 전달인자로 Action 객체가 전달

// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );
// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(5) );

Redux 사용법: Hooks로 코드 연결

  • 참고로 Redux는 React가 없어도 사용가능하다. 독립적임. 근데 여기서 나오는 Hooks가 없어 좀 제한적으로 구동하기에 리액트와 써야지 좋다!
  • Hooks는 위에서 했던 4형제를 연결시켜주는 역할을 하는데 딱 두가지만 기억하자 useSelector(), useDispatch()
  • Hooks는 Dispatch에 더 가깝긴 하다

useDispatch()

Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메소드
위에서 Dispatch를 설명할 때 사용한 dispatch 함수도 useDispatch()를 사용해서 만든 것이다!

import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
dispatch( increase() )
console.log(counter) // 2
dispatch( setNumber(5) )
console.log(counter) // 5

실습

// index.js로 4형제 완성 후 App.js에서!
dispatch 함수는 이벤트 핸들러 안에서 사용됩니다.
그리고 dispatch 함수는 action 객체를 Reducer 함수로 전달해줍니다.
1. import { useDispatch } from 'react-redux';를 통해
   react-redux에서 useDispatch를 불러와주세요.
2. import { increase,decrease } from './index.js';를 통해
   Action Creater 함수 increase, decrease를 불러와주세요.
3. useDispatch의 실행 값를 변수에 저장해서 dispatch 함수를
   사용합니다.(주석을 해제한 후 콘솔결과를 확인해보세요!)
4. 유어클래스 dispatch 예제를 참고해서 이벤트 핸들러 안에서 dispatch를
   통해 action 객체를 Reducer 함수로 전달해주세요.
5. 유어클래스 dispatch 예제를 참고해서 이벤트 핸들러 안에서 dispatch를
   통해 action 객체를 Reducer 함수로 전달해주세요.
// App.js
import React from 'react';
import './style.css';
// 1
import { useDispatch } from 'react-redux';
// 2
import { increase, decrease } from './index.js';

export default function App() {
  // 3
  const dispatch = useDispatch();
  console.log(dispatch);

  const plusNum = () => {
    // 4
    dispatch(increase());
  };

  const minusNum = () => {
    // 5
    dispatch(decrease());
  };

  return (
    <div className="container">
      {/* Count 숫자는 안바뀌어짐, 접근하려면 useSelector() 필요 */}
      <h1>{`Count: ${1}`}</h1>
      <div>
        <button className="plusBtn" onClick={plusNum}>
          +
        </button>
        <button className="minusBtn" onClick={minusNum}>
          -
        </button>
      </div>
    </div>
  );
}

useDispatch()index.js에서 가져온 모듈 in/de crese를 가져와 실행 함수에 연결시켜둔 형태.

useSelector()

이제 지금까지 useState()set~함수를 짠거나 마찬가지다.useDispatch()로 나눠주고 누르면 실행되게끔 로직을 짰으면 이제useState()의 값을 가져와 출력시킨다. 그게 useSelector()

useSeletor를 통해 state가 필요한 컴포넌트에서
전역 변수 저장소 store에 저장된 state를 쉽게 불러올 수 있습니다.
1. import { useDispatch, useSeletor } from 'react-redux';를 통해
   react-redux에서 useSeletor 불러와주세요.
2. useSelector의 콜백 함수의 인자에 Store에 저장된 모든 state가
   담깁니다. 그대로 return을 하게 되면 Store에 저장된 모든 state를
   사용할 수 있습니다.
3. 변수 state를 콘솔에서 확인해보세요. Store에 저장된 기존 state 값인
   1이 찍히는 것을 확인할 수 있습니다.
4. Store에서 꺼내온 state를 화면에 나타내기 위해 변수 state를 활용해보세요.
5. +, - 버튼을 누를 때마다 state가 변경되는 것을 확인할 수 있습니다!

3번까지 했을 때 상태가 변하는 걸 확인할 수 있다!@

import React from 'react';
import './style.css';
// 1
import { useDispatch, useSelector } from 'react-redux';
import { increase, decrease } from './index.js';

export default function App() {
  const dispatch = useDispatch();
  // 2
  const state = useSelector((state) => state);
  // 3
  console.log(state);

// plusNum 등 생략

  return (
    <div className="container">
      {/* 4 */}
      <h1>{`Count: ${state}`}</h1>
      <div>
        <button className="plusBtn" onClick={plusNum}>
          +
        </button>
        <button className="minusBtn" onClick={minusNum}>
          -
        </button>
      </div>
    </div>
  );
}

이로써 완성이다!

리팩토링

사실 리덕스 코드는 한곳에 쓰면 가독성도 안좋아지고 디버깅하기 어려워진다. 그래서 파일마다 지정해줘 써야한다. 이런 식으로꼭 한번 보자

+ Action들의 type은 따로 변수 선언 및 할당해줘 재사용 가능하게끔 만드는 것이 포인트임!

리덕스의 원칙

지금 하면서 알게된 원칙들이 세가지 있다. 정리해보자

Single source of truth
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미. 즉, Redux에는 데이터를 저장하는 Store라는 단 하나뿐인 공간이 있음과 연결이 되는 원칙.

State is read-only
상태는 읽기 전용이라는 뜻으로, React에서 상태갱신함수로만 상태를 변경할 수 있었던 것처럼, Redux의 상태도 직접 변경할 수 없음을 의미. 즉, Action 객체가 있어야만 상태를 변경할 수 있음과 연결되는 원칙.

Changes are made with pure functions
변경은 순수함수로만 가능하다는 뜻으로, 상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer와 연결되는 원칙.

꼭 알아두자

페어과제2

저번에 useState hook으로 상태 관리했던 페어과제1을 리덕스로 상태 관리 해보자!

리덕스의 데이터 흐름대로 구현하겠다! 강사님은 추가, 삭제, 수량변경 기능별로 액션-디스패치-리듀서-스토어 순으로 구현했다.

Action

리팩토링에 봤듯이 따로 js 파일을 만들어 구현했다.

  • 중요 포인트: Action Types을 변수로 선언하기!!
    • 오타 방지(자동완성 가능)
    • 유지•보수
    • 재사용성(리듀서에서 쓸 수 있게)이 더 좋게 만드는 것
// actions/index.js
// action types
export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const SET_QUANTITY = "SET_QUANTITY";
export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";

// actions creator functions
export const addToCart = (itemId) => {
  return {
    type: ADD_TO_CART,
    payload: {
      quantity: 1,
      itemId,
    },
  };
};

export const removeFromCart = (itemId) => {
  return {
    //TODO
    type: REMOVE_FROM_CART,
    payload: {
      itemId,
    },
  };
};

export const setQuantity = (itemId, quantity) => {
  return {
    //TODO
    type: SET_QUANTITY,
    payload: {
      itemId,
      quantity,
    },
  };
};

// notify 생략

액션은 실행됐을 때 받아올 데이터, 즉 payload를 전달해 reducer가 쓸 재료를 전달하는 역할이다.
그래서 해당 타입을 적어줬다. 예를 들어 추가할 땐 quantity가 1이며 itemId를 던져줘 cartItem에 넣어줄 객체 데이터 하나를 만들어줘서 누르면 추가되게 했고, 삭제 함수는 삭제할 id를, 수량 변경 함수는 변경할 id와 수량 자체를 전달한다.

dispatch

// ItemListContainer.js
import React from "react";
import { addToCart, notify } from "../actions/index";
import { useSelector, useDispatch } from "react-redux";
import Item from "../components/Item";

function ItemListContainer() {
  const state = useSelector((state) => state.itemReducer);
  const { items, cartItems } = state;
  const dispatch = useDispatch();

  const handleClick = (item) => {
    if (!cartItems.map((el) => el.itemId).includes(item.id)) {
      //TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달하세요.
      dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
      dispatch(addToCart(item.id));
    } else {
      dispatch(notify("이미 추가된 상품입니다."));
    }
  };
  
 return (
    <div id="item-list-container">
      <div id="item-list-body">
        <div id="item-list-title">쓸모없는 선물 모음</div>
        {items.map((item, idx) => (
          <Item
            item={item}
            key={idx}
            handleClick={() => {
              handleClick(item);
            }}
          />
        ))}
      </div>
    </div>
  );
}

// ShoppingCart.js
export default function ShoppingCart() {
  const state = useSelector((state) => state.itemReducer);
  const { cartItems, items } = state;
  const dispatch = useDispatch();
  const [checkedItems, setCheckedItems] = useState(cartItems.map((el) => el.itemId));

// check 기능 코드 중략

  const handleQuantityChange = (quantity, itemId) => {
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
    dispatch(setQuantity(itemId, quantity));
  };

  const handleDelete = (itemId) => {
    setCheckedItems(checkedItems.filter((el) => el !== itemId));
    //TODO: dispatch 함수를 호출하여 액션을 전달하세요.
    dispatch(removeFromCart(itemId));
  };
  
  // total 기능 중략
  return
  // return 초반 중략
  		  {!cartItems.length ? (
            <div id="item-list-text">장바구니에 아이템이 없습니다.</div>
          ) : (
            <div id="cart-item-list">
              {renderItems.map((item, idx) => {
                const quantity = cartItems.filter((el) => el.itemId === item.id)[0].quantity;
                return (
                  <CartItem
                    key={idx}
                    handleCheckChange={handleCheckChange}
                    handleQuantityChange={handleQuantityChange}
                    handleDelete={handleDelete}
                    item={item}
                    checkedItems={checkedItems}
                    quantity={quantity}
                  />
                );
              })}
            </div>
          )}
  // 생략
  • 각 페이지의 버튼에 state를 할당해준 것! dispatch 중요하다!
  • const state = useSelector((state) => state.itemReducer);코드를 보면 .itemReducer를 사용한다. 왜냐면 store에 넣을 때 rootReducer로 통일했기에 reducer를 골라야해서!

reducer

기능 구현! state의 데이터가 어떻게 돼있는지 알고 활용해야햔다.

  • 중요 포인트: Reducer는 immutable해야함!(setUseState 같이)
    • Object.assign()이나 ...obj같이 얕은 복사하여 원본 수정이 안되게!
// itemReducer.js
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";

const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TO_CART:
      //TODO
      // 얕은 복사를 해야 리듀서 기능 기본은 원본 수정이 방지된다. 그래서 assign을 넣어줬다. 스프레드도 가능!
      // state를 넣어주고, cartItems만 수정할거니 state.cartItems에 payload를 집어넣는다!
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload],
      });
    case REMOVE_FROM_CART:
      //TODO
      // let filteredList = state.cartItems.filter((el) => el.itemId !== action.payload.itemId);
      // return Object.assign({}, state, {
      //   cartItems: filteredList,
      // });
      return {
        ...state,
        cartItems: state.cartItems.filter((el) => el.itemId !== action.payload.itemId),
      };
    case SET_QUANTITY:
      let idx = state.cartItems.findIndex((el) => el.itemId === action.payload.itemId);
      //TODO
      // 이것도 가능
      // return {
      //   ...state,
      //   cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)]
      // }
      const reviseCart = state.cartItems.map((el, elIdx) => (elIdx === idx ? action.payload : el));
      return Object.assign({}, state, {
        cartItems: reviseCart,
      });
    default:
      return state;
  }
};

export default itemReducer;
  • Spread 연산자로 덮어쓰기가 가능하다! 뒤에 쓰면 assign처럼 덮어 쓰여질 수 있다.
  • 또 notification 기능이 있어서 store에 넣을 rootReducer를 쓰려면 combineReducers도 써야한다.
    import { combineReducers } from 'redux';
    import itemReducer from './itemReducer';
    import notificationReducer from './notificationReducer';
    const rootReducer = combineReducers({
    itemReducer,
    notificationReducer
    });
    export default rootReducer;
    + Redux Thunk로 notification 기능을 구현해줬다. setTimeOut으로 일정 시간 지나면 바로 없어지게 버튼이 실행되면 액션에서 notify안에 enqueueNotification뜨고 dequeueNotification뜨게 만들어(dispatch)실행되면 닫는 거 까지 구현해놨다. 신기해!
    요즘은 리덕스 툴킷에 있어서 예전 기능이다. 툴킷도 배워보자 나중에

store

import { compose, createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/index";
import thunk from "redux-thunk";
// thunk는 미들웨어로 notification을 얻을 때 사용

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

export default store;

뒤에 composeEnhancers는 notification 기능 구현에 쓰는 거고 rootReducer로 store에 저장해주는 모습이다.
그래서! combineReducers를 해야한다.

+ 솔직히 많이 어려운 편이어서 리덕스 툴킷이나 'zustand', 'Recoil'이라는 라이브러리들이 있다. 알아보자.

🦏

profile
코뿔소처럼 저돌적으로

0개의 댓글