리액트에서 여러 컴포넌츠에서 사용되는 값이 변하는 데이터
(즉, state = 상태)는 해당 컴포넌츠들의 공통된 상위 컴포넌츠에 정의한다.
그리고 해당 상태를 필요로 하는 컴포넌츠로 전달하고 → 전달해서 → 사용하게 된다.
그런데 모든 상태들을 이렇게 관리하는 것에는 문제가 있다.
App 컴포넌츠
, ItemListContainer
, ShoppingCart
도 상태 데이터를 가짐
이러한 단점을 보완하기 위한 방법으로는
① 컴포넌트와 관련있는 state는 될 수 있으면 가까이 유지하는 방법과
② 상태관리 라이브러리를 사용하는 방법이 있다.
상태관리 라이브러리를 사용하게 되면 전역으로 관리하는 저장소에서 직접 state를 꺼내쓸 수 있다.
🗂 React에서 사용할 수 있는 대표적인 상태관리 라이브러리로 Redux가 있다.
[주의] 💡 Redux는 React의 관련 라이브러리, 혹은 하위 라이브러리가 아니며, React 없이도 사용할 수 있는 상태 관리 라이브러리이다.
Dispatch(Action) → Reducer(State, Action) → Store에 있는 State를 변화시킴
Dispatch() 함수에 Action을 넣어서 Reducer 에게 전달 하면 컴포넌츠의 state를 지정한 Action 대로 업데이트하게 된다.
Redux에서는 Action → Dispatch → Reducer → Store
순서로 데이터가 단방향으로 흐른다.
리액트 프로젝트의 터미널에서 npm install react-redux
또는 yarn add react-redux
로 설치한다.
// store 폴더 안의 store.js
import { createStore } from 'redux';
import rootReducer from '../reducers/index';
const store = createStore(rootReducer);
export default store;
Store를 생성할 파일에서 createStore 메서드를 활용해 Reducer를 연결해서 Store를 생성할 수 있다.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
전역 상태 저장소 store를 사용하기 위해서는 최상위 컴포넌트인 App 컴포넌트를 Provider로 감싸준 후 props로 변수 store를 전달해주여야 한다.
state를 어떻게 변화시킬 지 정의해 놓은 객체로 dispatch( )의 파라미터로 전달해서 사용한다.
dispatch({ Action 객체 })
// type을 요청받았을 때 어떤 값을 반환할 지 payload가 필요 없는 경우
{type: "ADD_TO_CART" } // type 은 필수로 지정해야한다.
// payload가 필요한 경우
{ type: "ADD_TO_CART", payload: {quantity: 1, itemId} }
// ADD_TO_CART 라는 요청이 들어오면 {quantity: 1, itemId} 객체로 state를 바꾸기 위해 payload를 지정함
Action 객체가 어떤 동작을 하는지 명시하는 type 은 필수로 지정을 해 주어야 한다.
필요에 따라 payload 프로퍼티를 작성해 구체적인 값을 전달한다.
// 직접 객체를 만들기 보다는 Action Creator 함수로 Action 객체를 작성한다.
const addToCart = (itemId) => {
return {
type: "ADD_TO_CART",
payload: {
quantity: 1,
itemId,
},
};
};
보통 Action을 직접 작성하기보다는 Action 객체를 생성하는 함수를 만들어 사용하는 경우가 많다. 이러한 함수를 액션 생성자(Action Creator)라고도 한다.
Dispatch는 Reducer로 Action을 전달해주는 함수이다.
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
dispatch({type: "ADD_TO_CART" })
useDispatch() 는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드이다.
useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드이다.
import React from "react";
import { addToCart, notify } from "../actions/index";
// useDispatch와 useSelector 은 "react-redux"에서 import 한다.
import { useSelector, useDispatch } from "react-redux";
import Item from "../components/Item";
function ItemListContainer() {
const state = useSelector((state) => state.itemReducer); // useSelector로 현재 컴포넌트(ItemListContainer)와 Redux 저장소의 state를 연결해준다.
const { items, cartItems } = state;
const dispatch = useDispatch(); // 컴포넌츠 안에서 useDispatch로 정의해준다.
/* 클릭했을 때 장바구니에 없으면 '장바구니에 추가되었습니다'가 뜨면서,
Action 객체가 state를 바꾸고,
있으면 '이미 추가된 상품입니다' 가 뜨는 이벤트 함수 */
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
// addToCart는 Action 객체를 생성하는 함수이다.
dispatch(addToCart(item.id)); // 호출 된 addToCart()의 리턴 값인 Action 객체가 dispatch()의 파라미터로 담긴다.
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
} else {
dispatch(notify("이미 추가된 상품입니다."));
}
};
return <button onClick={()=>handleClick(item)}> 장바구니 추가 </button>
}
Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수이다.
import { initialState } from "./initialState";
// 위의 예제 코드에서 useSelector()로 연결한 state.itemReducer 이다.
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TO_CART":
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload],
});
default:
return state;
}
};
// action객체의 type 프로퍼티 값이 "ADD_TO_CART"인 경우,
// cartItem 프로퍼티 값인 배열에
// action객체의 payload 프로퍼티값이 추가된다.
// action 객체의 payload 값은 위 Action 객체 설명의 예제 코드에 있는 { quantity: 1, itemId } 객체 이다.
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다.
즉, Redux에는 데이터를 저장하는 Store라는 단 하나뿐인 공간이 있음과 연결이 되는 원칙이다.
상태는 읽기 전용이라는 뜻으로, Redux의 상태도 직접 변경할 수 없다. 즉, Action 객체가 있어야만 상태를 변경할 수 있음과 연결되는 원칙이다.
변경은 순수함수로만 가능하다는 뜻으로, 상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer와 연결되는 원칙이다.
화면에서 수량 변경은 되는데, 이 부분이 테스트를 통과가 안됐다.
👉🏻 test 를 위한 문제일 뿐, 실제 작동하는데에는 문제없다는 답변을 받았다.
코드를 만지면서 action 객체 생성자 함수에 이미 작성되어 있던 코드를 내가 임의로 바꿔서 문제가 됐던 거였다.
테스트 불통과된 내용상 문제 원인 : itemId와 quantity가 바뀌어 있다.
왜????
이유는 내가 순서를 거꾸로 거슬러 올라가면서 이미 작성되어 있던 코드를 바꿔서 테스트가 통과되지 않았던 거였다.
작성 되어 있던 코드를 건드리지 않고 그에 맞춰서 작성했으면 통과했을 것😂
원래 훑어봐야 하는 순서가
① action 객체
(생성자 함수) index.js 파일 →
② 이벤트 핸들러 함수 정의된 곳 & 이벤트 핸들러 함수 바디의 dispatch() 함수
shopingCart.js 파일→
③ 이벤트 핸들러 함수가 적용된 곳에서 전달받는 파라미터 CartItem.js 파일
순서대로 봐야 했는데,
나는 파일을 반대 순서대로 살펴보면서 하다가 ① action 객체(생성자 함수)에 전달되는 파라미터에 이미 setQuantity(quantity, itemId)
순서로 작성되어 있던 것을,
setQuantity(itemId, quantity)
로 순서를 바꿔서 해당 테스트 통과가 안됐다.
// ③번째 파일 CartItem.js
<input
type="number"
min={1}
className="cart-item-quantity"
value={quantity}
onChange={(e) => {
handleQuantityChange(Number(e.target.value), item.id);
}} // 여기서 ❶ Number(e.target.value), ❷ item.id 순서로,
// 즉, ❶ quantity, ❷ itemId 순서로 전달
></input>
수량 변경 이벤트가 일어나는 input의 onChange 에서
handleQuantityChange()
함수로
첫 번째 파라미터로 입력된 숫자를,
두 번째 파라미터로 상품의 ID를 넘겨주는 코드는 이미 작성되어 있었다.
// ②번째 파일 shopingCart.js
const handleQuantityChange = (quantity, itemId) // 여기도 ❶ 첫 번째 파라미터로 quantity, ❷ 두 번째 파라미터로 itemId를 작성해준다.
=> {
dispatch(setQuantity(itemId, quantity)); // 여기서 주의! setQuantity( ) action객체 생성자 함수에 넘겨줄 때에는
// setQuantity() 함수가 정의되어 있는 파일에 이미 파라미터 순서 코드가 작성되어 있었어서 그 순서에 맞춰서 넘겨줘야 한다.
};
그래서 handleQuantityChange( ) 가 정의된 상위 컴포넌츠인 shopingCart.js 에 가서
첫 번째 파라미터로 quantity, 두 번째 파라미터로 itemId 를 코드로 작성해야한다.
dispatch()
에도 Action 객체를 생성하는 함수(Action Creator)의 파라미터로 넘겨주는데,
이때 ! setQuantity() 함수가 정의되어 있는 곳에서도 이미 파라미터의 순서 코드가 작성되어 있었어서 그 코드에 맞춰서 파라미터의 순서를 바꿔서 넣어줘야한다.
즉, 첫 번째 파라미터로 itemId를,
두 번째 파라미터로 quantity를 넘겨주어야 한다.
// ①번째 파일 index.js
export const setQuantity = (itemId, quantity) => { // 여기서도 Id, quantity 순서로 작성한다.
return {
type: SET_QUANTITY,
payload: { itemId, quantity },
};
};
정리하자면,
Action 객체를 생성하는 함수의 파라미터
의 순서와
dispatch()에 전달하는 함수의 파라미터
의 위치가
동일하면 문제 없다.
(테스트 불통과 문제는 그냥 테스트를 위한 설정이었을 뿐..)
참고 : React Hooks에 취한다 - useReducer 확실히 정리해드려요 | 리액트 훅스 시리즈
코드스테이츠 유어클래스