h e 1 1 o !
Redux / 주요 개념 (Cmarket Redux 과제) 본문
리덕스 데이터 흐름
Action → Dispatch → Reducer → Store
Cmarket redux 과제 설명
- 페이지는 아이템 리스트 페이지(ItemListContainer)와 장바구니 페이지(ShoppingCart) 총 두 페이지
- Store의 initial state에는 전체 아이템 목록(items), 장바구니 목록(cartItems)이 들어있습니다.
- 각 ItemListContainer, ShoppingCart 페이지 컴포넌트 및 components 폴더의 여러 컴포넌트들에서 Store(state)에 접근해 보세요. (Redux에서 제공하는 hooks, useDispatch, useSelector를 사용합니다.)
📂 짚어보기
과제와 관련된 코드를 보기 전에 redux가 실행될 수 있게 해주는 요소들을 먼저 보자.
✔️ store
스토어는 상태가 관리되는 오직 하나뿐인 저장소 역할을 한다. Redux 앱의 state가 저장되어 있는 공간으로,
createStore메서드로 Reducer를 연결하고 store를 생성한다.
import { createStore } from 'redux';
const store = createStore(rootReducer);
✔️ combineReducer
createStore에 reducer를 전달해야하는데, reducer가 여러개라면 combineReducer로 묶어서 전달한다.
우리 과제에서는 reducer가 두 개라서 rootReducer로 사용하고 있다.
//리듀서 인덱스 파일
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';
const rootReducer = combineReducers({
itemReducer,
notificationReducer
});
export default rootReducer;
✔️ Provider
바뀌는 state들을 어떤 컴포넌트에 제공할 것인가에 대한 가장 바깥쪽 울타리를 정의.
Provider에는 store 속성을 넣어준다.
//인덱스 파일
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store';
import { Provider } from 'react-redux';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
✔️ Reducer
리듀서는 변화를 일으키는 함수이다. 외부 요인의 영향을 받으면 안되기 때문에 순수함수여야 한다.
위에, store에서 creareStore로 store를 생성하고 reducer를 연결 했다.
reducer로 store에 접근해서 새로운 state를 반환할 수 있다.
// 리듀서 파일
import { initialState } from "./initialState";
const itemReducer = (state = initialState, action) => { ... }
reducer에 전달되는 인수는 초기 state, action이다.
우리 과제에서 초기의 state는 initialState에 담겨 있어서 저렇게 써줬다.
이렇게 전달 받은 초기 state와 action을 활용해서 새로운 state를 반환한다.
✔️ useSelector
useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드이다.
Redux Hooks 메서드는 'redux'가 아니라 'react-redux'에서 불러온다.
//화면에 그려질 컴포넌트 파일
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();
...
}
📂 Action
Action은 컴포넌트에서 발생할 변화를 분류하고 특정한 모양(payload)으로 담아 디스패치로 보내는 역할을 한다.
디스패치에 액션을 담아서 리듀서로 보내면
리듀서에서는 액션이 가져온 데이터의 type에 따라 payload를 활용하여 데이터를 가공하고,
store에 접근해서 새로운 state가 만들어진다.
✔️ Redux의 세 가지 원칙 / State is read-only
상태는 읽기 전용이라는 뜻으로, React에서 상태갱신함수로만 상태를 변경할 수 있었던 것처럼, Redux의 상태도 직접 변경할 수 없음을 의미한다. 즉, Action 객체가 있어야만 상태를 변경할 수 있음과 연결되는 원칙.
acaion은 action 폴더의 index.js 파일에서 작성한다.
addToCart, removeFromCart, setQuantity 세 가지 action을 만들었다.
✔️ 액션의 구성
액션의 객체 안에는 리듀서에서 각각의 액션을 구분하게 해주는 type, payload가 있다.
type은 필수이고 payload(객체)는 필요한 경우에 작성한다.
여기서 setQuantity 액션을 예로 들자면, type은 SET_QUANTITY, payload는 itemId, quantity를 키값으로 하는 객체가 담겨 있다. setQuantity 액션은 shoppingCart 페이지의 CartItem 컴포넌트에 전달된 handleQuantityChange 함수가 호출되면 함께 호출된다.
//<ShoppingCart> // CartItem 컴포넌트의 상위 컴포넌트
const handleAllCheck = (checked) => {
if (checked) {
setCheckedItems(cartItems.map((el) => el.itemId));
} else {
setCheckedItems([]);
}
};
...
return (
<CartItem
key={idx}
...
handleQuantityChange={handleQuantityChange}
/>
);
// <CartItem>
onChange={(e) => {
handleQuantityChange(Number(e.target.value), item.id)
}}>
handleQuantityChange 함수는 하위 컴포넌트 CartItem 컴포넌트에 전달되어 변경된 quantity값과 해당 item.id를 shoppingCart 페이지로 다시 가지고 온다. 그럼 이 handleQuantityChange 함수 안에서 액션이 실행되어 변경 된 값을 store까지 전달해야한다.
📂 Dispatch
이를 위해서는 먼저 dispatch를 호출해 액션을 전달해야한다. 이 과정은 쉽다.
//<ShoppingCart> 컴포넌트
const handleQuantityChange = (quantity, itemId) => {
dispatch(setQuantity(itemId, quantity));
};
위에서 본 것처럼 dispatch 안에 액션을 넣어주기만 하면 된다. 그럼 dispatch를 통해 액션이 reducer로 간다.
📂 Reducer
✔️ Redux의 세 가지 원칙 / Changes are made with pure functions
변경은 순수함수로만 가능하다는 뜻으로, 상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer 와 연결되는 원칙이다.
itemReducer 파일 전체. itemReducer는 파라미터로 state, action을 받고 있다.
두번째 파라미터 action으로 action이 들어온다.
그리고 switch문에 의해 type에 따라 액션으로 가져온 데이터 처리 방법이 나뉜다.
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 기존 cartItems에 추가
return {
//객체를 리턴 중
...state,
cartItems: [...state.cartItems, action.payload],
};
case REMOVE_FROM_CART:
//itemId 전달됨
return {
...state,
cartItems: state.cartItems.filter(
(el) => el.itemId !== action.payload.itemId
),
};
case SET_QUANTITY:
//itemId, quantity 전달됨
let idx = state.cartItems.findIndex(
(el) => el.itemId === action.payload.itemId
);
//TODO
//idx 전, idx, idx 후를 cartItems에 넣기
return {
...state,
cartItems: [
...state.cartItems.slice(0, idx),
action.payload,
...state.cartItems.slice(idx + 1),
],
};
default:
return state;
}
};
export default itemReducer;
setQuantuty 액션이 전달된 reducer 부분.
case SET_QUANTITY:
//itemId, quantity 전달됨
let idx = state.cartItems.findIndex(
(el) => el.itemId === action.payload.itemId
);
//TODO
//idx 전, idx, idx 후를 cartItems에 넣기
return {
...state,
cartItems: [
...state.cartItems.slice(0, idx),
action.payload,
...state.cartItems.slice(idx + 1),
],
};
선택된 아이템만 특정해서 quantity 값을 바꿔야하기 때문에
원래의 cartItems 배열과 액션으로 가져온 itemId를 비교해서 일치하는 idx를 찾는다. 그 인덱스 넘버가 변수 idx에 담겨 있다.
새로운 객체를 리턴하는데, 구조분해 할당을 통해서 객체를 만들어준다.
state의 형식부터 살펴보자. 아래처럼, 객체를 요소로 가지는 배열을 값으로 가지는 요소를 키로 가지는 객체이다.
복잡하지만 그렇다 ㅎ
{
"items": [{}, {}, {}...],
"cartItems": [{}, {}, {}, ...]
}
return {
...state, //기존의 state들을 담는다
cartItems: [ // state의 한 요소인 cartItem(배열이 value)도 담는데,
...state.cartItems.slice(0, idx), //원래 cartItem의 요소들 중에서 0~ idx(수량을 변경해야 하는 아이템) 이전까지 담고,
action.payload, // 원래의 idx 요소 대신, action으로 담아온 payload를 통째로 담는다.payload에는 itemId와 quantity가 함께 있다.
...state.cartItems.slice(idx + 1), //idx 이후 나머지 객체(요소)들을 담아준다.
],
};
요렇게 넣어준다. 기존의 state는 변형시키면 안되기 때문에 구조분해 할당을 이용한다.
Reducer의 Immutability(불변성)
Reducer 함수를 작성할 때 주의해야 할 점이 있습니다. 바로 Redux의 state 업데이트는 immutable한 방식으로 변경해야 한다는 것인데요. Redux의 장점 중 하나인 변경된 state를 로그로 남기기 위해서 꼭 필요한 작업입니다.
그래서 immutable한 방식으로 state를 변경하기 위해서는 새로운 객체를 리턴해야한다.
위의 구조분해 할당을 이용한 새로운 객체 리턴은 Object.assign을 이용한 코드와 같다.
📂 Store
✔️ Redux의 세 가지 원칙 / Single source of truth
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미입니다. 즉, Redux에는 데이터를 저장하는 Store라는 단 하나뿐인 공간이 있음과 연결이 되는 원칙입니다.
스토어는 이렇게 생겼다.
위의 과제 파일에서 createStore 부분에 줄이 그어져 있는 것은, 최신 Redux toolkit을 이용할 것을 권장하기 때문이다.
아무튼, store에서는 createStore를 하는데, 리듀서를 연결해준다. 스토어에 대한 다른 내용은 맨 처음에 다뤄서 생략한다.
요렇게 한 액션을 따라서 스토어까지 전해지고 state가 바뀌는 과정을 설명했다.
리덕스 툴킷을 이용해서 리팩토링도 해봤는데... 리덕스도 익숙하지 않아서 그런지 어려웠다.
그래도 툴킷을 사용하니 더 간편해진다는 것은 확실히 느꼈다.
하지만, 이론적으로 useState를 활용한 상태 관리 보다 리덕스가 간편하다는 것은 알겠는데...
useState가 불편하다는 것을 아직 몰라서 그런지 확 와닿지 않는다. 하하.
하루라도 빨리 작은 프로젝트를 하며 배운 것을 써먹어보면 알겠지.
참, 리덕스 툴킷을 사용하는 것이 더 보편적이라고 하니, 바로 리덕스 툴킷을 사용하는 게 좋겠다.
그럼이만 총총
'r e a c t' 카테고리의 다른 글
JSX wrapper component (0) | 2022.07.13 |
---|---|
styled components / 조건부 스타일 (0) | 2022.07.11 |
redux / Provider, useSelector, dispatch (0) | 2022.07.07 |
가계부 만들기 (0) | 2022.06.29 |
State (0) | 2022.06.15 |