티스토리 뷰

프로젝트를 하면서 '상태 관리'를 편하게 하기 위해 사용하는 라이브러리가 'Redux'이다.

그러나, 'Redux'는 이전에 사용한 라이브러리이며 단점이 존재한다. 이를 개선해 나온 것이 'Redux Toolkit'이다.

이번 시간에 프로젝트에 활용할 'Redux Toolkit'을 미리 학습하려고 한다.

 

npm install @reduxjs/toolkit

우선, 사용할 라이브러리를 설치해야 한다.

 

// productSlice.js

let initialState = {
	productList: [],
    selectedItem: null,
}

function productReducer(state = initialState, action) {
	let {type, payload} = action;
    switch (type) {
    	case "GET_PRODUCT_SUCCESS":
        	return {...state, productList: payload.data};
        case "GET_SINGLE_PRODUCT_SUCCESS":
        	return {...state, selectedItem: payload.data};
        default: 
        	return {...state};
    }
}

위는 이전 문법을 사용한 것으로, 스위치에서 케이스별로 이름을 유니크하게 만들어주어야 한다.

이렇게 옛날 문법을 사용하면, 스위치 케이스를 계속 만들어야 하며, 이름을 작성해야 하는데, 디스패치할 때, 같은 이름을 사용해야 함으로 복잡할 수 있다.

 

아래의 코드는 위의 문제를 개선을 위해 "createSlice"를 활용한 것이다.

 

// productSlice.js

import {createSlice} from "@reduxjs/toolkit";

let initialState = {
	productList: [],
    selectedItem: null,
}

const productSlice = createSlice({
	name: "product",
    initialState,
    reducers: {
    	getAllProducts(state, action) {
        	state.productList: action.payload.data; // initialState의 productList, 내가 바꾸고 싶은 값, payload는 action에서 옴
        },
        
        getSingleProduct(state, action) {
        	state.selectedItem = action.payload.data;
        }
    }
})

console.log("pppp", productSlice); // actions, caseReducers, getInitialState, reducer 출력

export const productActions = productSlice.actions; // dispatch를 위해
export default productSlice.reducer; // 결국 하나의 큰 reducer이기 때문

"createSlice"는 한마디로 Reducer를 만드는 것을 도와준다. 그리고 반드시 3개의 객체가 있어야 한다.

  • name: createSlice에 이름을 주는 것 (예. 'counter')
  • initialState: 초기값
  • reducers: 객체 타입으로, 사용될 모든 함수들을 생성, 매개변수는 state와 action이 존재

 

위에 Reducer 코드를 작성했다. 그렇다면, 이를 사용할 수 있는 Store는 어떻게 작성할 수 있을까?

 

// store.js

import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

let store = createStore(
	rootReducer,
    composeWithDevTools(applyMiddleware(thunk))
);

export default store;

위는 이전에 일반적으로 사용했던 "Store" 작성 방법이다. 이렇게 작성하고 있으면, Redux가 버전이 업데이트가 되면서 더 이상 createStore를 지원하지 않으려고 한다. 아마, 최신 버전을 사용하고 있다면 위의 코드는 에러 상황을 마주하게 될 것이다.

 

// store.js

import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// combine 한 것을 createStore에 보내어 사용 => combineReducer, thunk, applyMiddleware, composeWithDevTools 사용 필요
let store = createStore(
	rootReducer, // reducer 보내기, combineReducers 사용
    composeWithDevTools(applyMiddleware(thunk))
);

export default store;

이전에는, createStore를 사용하기 위해서 combineReducers를 사용해야 했다. combineReducers를 사용하게 되면 또 다시 필요한 것들이 있다.

 

  • combineReducer
  • thunk
  • applyMiddleware
  • composeWithDevTools

항상 위의 네 가지를 사용해야 했기 때문에 귀찮아질 수 있다.

 

// index.js

import {combineReducers} from 'redux';
import authenticateReducer from './authenticateReducer';
import productReducer from './productReducer';

export default combineReducers({
	auth: authenticateReducer,
    product: productReducer
})

 

그렇기 때문에, 위의 단점을 개선하여 사용할 수 있는 것이 "configureStore"이다. 이것을 사용하면, combineReducer를 따로 쓸 필요가 없다.

// store.js

import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

import {configureStore} from '@reduxjs/toolkit';
import authenticateReducer from './authenticateReducer';
import productReducer from './productReducer';

const store = configureStore({
	reducer: {
    	auth: authenticateReducer,
    	product: productReducer
    }
})

export default store;

 

그렇다면, 이제 사용하기 위해 "Dispatch"는 어떻게 사용할 수 있을까?

function getProducts(searchQuery) {
	return async (dispatch, getState) => {
    	let url = `...q=${searchQuery}`;
        let response = await fetch(url);
        let data = await response.json();
        
        dispatch({ type: "GET_PRODUCT_SUCCESS", payload: {data} });
    }
}

function getProductDetail(id) {
	return async (dispatch) => {
    	let url = `.../${id}`;
        let response = await fetch(url);
        let data = await response.json();
        
        dispatch({ type: "GET_SINGLE_PRODUCT_SUCCESS", payload: {data} });
    }
}

export const productAction = { getProducts, getProductDetail };

위의 방법은 기존 Redux에서 Dispatch하는 방법이었다. Toolkit에서 사용하는 방법도 위와 크게 다르지 않다.

 

import {productActions} from '../reducers/productReducer'

function getProducts(searchQuery) {
	return async (dispatch, getState) => {
    	let url = `...q=${searchQuery}`;
        let response = await fetch(url);
        let data = await response.json();

        dispatch(productActions.getAllProducts({data}));
    }
}

function getProductDetail(id) {
	return async (dispatch) => {
    	let url = `.../${id}`;
        let response = await fetch(url);
        let data = await response.json();
        
        dispatch(productActions.getSingleProduct({data}));
    }
}

export const productAction = { getProducts, getProductDetail };

이제는, Reducer에 선언한 함수들을 가져와서 사용하면 된다. 매개변수로 전달하면 자동으로 payload로 값이 전달된다.

(Reducer에서 action.payload.data로 작성했을 때, 이 때의 payload)

 

그렇다면, 저장한 값을 가져와서 사용하려면 어떻게 하면 좋을까?

간단한 예시로 확인하고 나중에 꼭 참고해서 검색 기능 구현에 사용해보자.

 

// App.js

import React from 'react';
import {createStore} from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';

function reducer(state, action) {
	if (action.type === 'up') {
		return {...state, value:state.value + action.step}
	}

	return state;
}

const initialState = { value: 0 }
const store = createStore(reducer, initialState);

function Counter() {
	const dispatch = useDispatch();
    const count = useSelector(state => state.value);
    
    return <div>
    	<button onClick={() => {
        	dispatch({type:'up', step: 2});
        }}>+</button> {count}
    </div>
}

export default function App() {
	return (
    	<Provider store={store}>
        	<div>
            	<Counter></Counter>
            </div>
        </Provider>
    )
}

위에는 기존 'Redux'로 활용한 방법이다. 이제 위의 코드를 'Redux Toolkit'으로 변경해보자.

 

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
	name: 'counterSlice',
    initialState: { value: 0 },
    reducers: {
    	up: (state, action) => {
        	state.value = state.value + action.payload; // payload가 action 값의 기본명, 약속
        }
    }
});

export const {up} = counterSlice.actions;
export default counterSlice;

//store.js
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counterSlice';

const store = configureStore({
	reducer: {
		counter: counterSlice.reducer // reducers 모두 가져오기
	}
})

export default stroe;

// App.js
import React from 'react';
import { Provider, useSelector, useDispatch } from 'react-redux';
import store from './store';
import { up } from './counterSlice';

function Counter() {
	const dispatch = useDispatch();
    const count = useSelector(state => state.counter.value); // store의 counter
    
    return <div>
    	<button onClick={() => {
        	dispatch(up(2));
        }}>+</button> {count}
    </div>
}

export default function App() {
	return (
    	<Provider store={store}>
        	<div>
            	<Counter></Counter>
            </div>
        </Provider>
    )
}

위 처럼 'Redux Toolkit'으로 구현을 수정해 사용할 수 있다.