728x90
728x90

리덕스(Redux), 리덕스 툴킷(Redux Toolkit)

들어가며

  • 리액트(React.js)의 전역 상태 관리 라이브러리 중의 하나인 리덕스(Redux)리덕스 툴킷(Redux Toolkit)에 대해 정리해본다.

 

 

리덕스(Redux)

개념

  • 자바스크립트 애플리케이션에서 상태 관리를 위해 사용되는 상태 컨테이너
  • 애플리케이션의 상태를 중앙에서 관리함으로써 상태 변화를 쉽게 추적하고, 디버깅을 단순화하며, 다양한 컴포넌트 간에 상태를 공유하는 것을 쉽게 해준다.
  • 리덕스를 사용하기 위해서는 아래의 명령을 실행하여 관련 패키지를 설치해준다.
$ yarn add redux # npm install redux
$ yarn add react-redux # npm install react-redux

 

 

리덕스(Redux)는 리액트(React.js) 뿐만 아니라 다양한 자바스크립트 패키지에서도 사용할 수 있다.

 

 

 

액션(Action)

  • 애플리케이션에서 일어나는 사건을 설명하는 객체
  • type 속성을 필수로 가지며, 상태 변경을 트리거하는 역할을 한다.
  • 예) { type: 'INCREMENT' }, { type: 'ADD_TODO', text: 'Learn Redux' }

 

리듀서(Reducer)

  • 액션을 처리하여 새로운 상태를 반환하는 함수
  • 이전 상태 액션 객체를 인자로 받아 새로운 상태 객체를 반환한다.
  • 순수 함수여야 하며, 입력이 같으면 출력도 항상 같아야 한다.
    • 순수 함수(Pure Function)
      • 동일한 인자가 주어졌을 때 항상 동일한 결과를 변환
      • 함수의 실행이 외부 상태에 의존하지 않음
      • 외부 상태를 변경하지 않는 함수
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

 

스토어(Store)

  • 애플리케이션의 상태를 담고 있는 객체
  • createStore 함수를 사용하여 스토어를 생성한다.
  • 스토어는 3가지 메소드를 가진다:
    • getState(): 현재 상태 반환합
    • dispatch(action): 상태를 변경하는 액션 보내기
    • subscribe(listener): 상태가 변경될 때마다 호출되는 리스너를 등록하기

 

미들웨어(Middleware)

  • 액션이 리듀서에 도달하기 전에 처리할 수 있는 기능을 제공한다.
  • 로깅, 에러 보고, 비동기 작업 처리 등에 사용된다.

 

 

사용 방법

  • 예제 코드와 함께 Redux를 사용하는 방법을 간단하게 정리해본다.

 

./src/store/index.js
  • 프로젝트의 src 폴더 내부에 store 폴더를 생성한 후, 안에 index.js 파일을 생성한다.
import { createStore } from 'redux';
const counterReducer = (state = initialState, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
showCounter: state.showCounter,
};
}
if (action.type === "increase") {
return {
counter: state.counter + action.amount,
showCounter: state.showCounter,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
showCounter: state.showCounter,
};
}
if (action.type === "toggle") {
return {
showCounter: !state.showCounter,
counter: state.counter,
};
}
return state;
};
// 중앙 스토어 생성 및 리듀서 함수 연결
const store = createStore(counterReducer);
export default store;

 

./src/index.js
  • 생성한 store.js 파일에서 store를 불러온다.
  • react-redux 패키지에서 Provider 컴포넌트를 불러온다.
  • Provider 컴포넌트로 <App /> 컴포넌트를 감싸준다.
    • store prop의 값을 store.js에서 불러온 store로 설정해준다.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
// Provider, store 불러오기
import { Provider } from "react-redux";
import store from "./store/index";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}> {/* <App/><Provider>로 감싸준 후, store prop 설정 *}
<App />
</Provider>
);

 

./src/components/counter.js
  • 실제 컴포넌트에서 사용하는 예제이다.
  • useSelecter전역 상태 보관소(store.js)의 값에 접근한다.
  • useDispatch를 사용하여 컴포넌트에서 전역 상태 보관소(store.js)에 액션(Action)을 전달하여 상태를 변경한다.
import { useSelector, useDispatch } from "react-redux";
import classes from "./Counter.module.css";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const increaseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {
dispatch({ type: "toggle" });
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={increaseHandler}>Increase by 5</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;

 

사용 시 주의 사항

  • 리덕스를 사용할 때 절대로 다음과 같이 변수 자체를 변형(Mutation)시키지 않는다.
if (action.type === "increment") {
state.counter++;
return state;
}

 

  • 그 대신에 다음과 같이 객체로 오버라이딩(Overriding)한다.
if (action.type === "increment") {
return {
counter: state.counter + 1,
showCounter: state.showCounter,
};
}

 

상태 업데이트 시 일부 상태만 설정하면 나머지 상태는 undefined로 처리되어 예상치 못한 결과를 초래할 수 있다. 따라서 상태 업데이트를 할 때는 모든 기존 상태를 포함하도록 새로운 상태 객체를 반환해야 한다.

 

 

리덕스 툴킷(Redux Toolkit, RTK)

개념

  • 리덕스를 더 쉽고 효율적으로 사용하기 위해 리덕스 툴킷(Redux Toolkit)을 사용할 수 있다.
$ npm install @reduxjs/toolkit # yarn add @reduxjs/toolkit
$ npm install react-redux # yarn add react-redux

 

리덕스 툴킷은 아래와 같은 라이브러리로 구성되어 있다.

redux : 상태 관리 
immer : 상태 불변성 관리

redux-thunk : 비동기 액션 처리

reselect : 셀렉터 최적화

 

  • 리덕스 툴킷은 리덕스의 일반적인 보일러플레이트(반복되는) 코드를 줄이고, 직관적이며 간결한 코드 작성을 가능하게 한다.
    • 기본 리덕스를 사용할 때는 많은 설정과 보일러플레이트 코드가 필요하다.
      • 예를 들어, 액션 생성자, 리듀서, 스토어 설정 등을 별도로 작성해야 하는데 리덕스 툴킷은 이러한 설정을 단순화하여 개발자의 작업을 줄여준다.
  • 리덕스 툴킷은 configureStore 함수를 제공하여 스토어를 쉽게 설정할 수 있다.
    • 이 함수는 기본적인 미들웨어 설정과 디버깅 도구 설정을 자동으로 처리해준다.
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = configureStore({
reducer: rootReducer,
});

 

  • 리덕스 툴킷의 createSlice 함수는 액션 생성자리듀서를 한 번에 생성할 수 있게 해준다.
    • 리덕스의 핵심 개념을 더 직관적이고 간결하게 구현할 수 있다.
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

 

  • 리덕스 툴킷은 리덕스 개발자 도구(Redux DevTools)와 통합하여 상태 변화를 시각적으로 추적하고 디버깅하는 데 큰 도움을 준다.
  • 리덕스 툴킷은 타입 스크립트(TypeScript)와의 호환성을 강화하여, 타입 안전성을 유지하면서도 리덕스를 간편하게 사용할 수 있게 해준다.
  • 리덕스 툴킷은 immer를 사용하여 불변성 관리를 자동으로 처리해주며, 기본적으로 Redux Thunk 미들웨어를 포함한다.
    • 이러한 설정들은 개발자가 직접 최적화할 필요 없이 기본적으로 제공되어 개발 속도를 빠르게 해준다.
Redux Toolkit은 Redux를 더욱 편리하게 사용할 수 있도록 만들어진 도구로, Redux에서 자주 사용되는 기능들을 포함한 '배터리가 장착된' Redux라고 할 수 있다.

 

Immer와 상태 불변성(Immutability)

  • 리덕스를 사용할 때 절대로 변수 자체를 변형(Mutation)시키면 안된다.
  • 하지만, 리덕스 툴킷에서는 createSlice를 사용할 때 Immer라는 라이브러리가 자동으로 포함되어 있어, 불변성을 직접 관리할 필요 없이 더 직관적인 방식으로 상태를 변경할 수 있다.
  • 즉, 상태를 직접 변형하는 것처럼 보이는 코드를 작성해도 Immer내부적으로 상태를 불변성으로 유지해 준다.
const counterSlice = createSlice({
name: 'counter',
initialState: { counter: 0, showCounter: true },
reducers: {
increment(state) {
state.counter++;
}
}
});

 

state.counter++상태를 직접 변형하는 것처럼 보이지만, Immer가 이 코드를 감싸서 실제로는 불변성을 유지한 상태로 새로운 상태를 반환하게 해준다.

 

리덕스 툴킷에서는 상태를 명시적으로 오버라이딩(Overriding)하는 방식도 사용할 수 있지만, Immer 라이브러리 때문에 상태를 변경하는 코드를 작성할 수도 있다.

 

 

createAsyncThunk() 함수

  • 리덕스 툴킷은 비동기 작업을 간편하게 처리할 수 있는 createAsyncThunk 함수를 제공한다.
    • 비동기 작업의 상태 관리(로딩(pending), 성공(fulfilled), 실패(rejected) 등)를 쉽게 처리할 수 있다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
export const fetchUser = createAsyncThunk('users/fetchUser', async (userId) => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
});
const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export default userSlice.reducer;

 

  • 관련 내용은 아래의 글을 참고한다.
 

[React.js] Thunk API (Redux Toolkit)

Thunk API (Redux Toolkit)들어가며리덕스 툴킷(Redux Toolkit)의 Thunk API에 대해 정리해본다. Thunk API개념비동기 작업을 간편하게 처리할 수 있는 도구로, 여러가지 기능을 제공한다.주로 API 호출과 같은

dev-astra.tistory.com

 

사용 방법

  • 실제 제작했었던 예제 코드를 바탕으로 사용 방법을 정리해본다.

 

슬라이스 생성하기

./src/features/cart/cartSlice.jsx
  • 슬라이스 파일을 생성해준다.
  • ./src 경로에 features 폴더를 생성한 후, 전역 상태를 관리할 항목의 범주에 따라 폴더(cart)와 슬라이스 파일(cartSlice.jsx)을 생성해준다.
  • 슬라이스 파일(cartSlice.jsx)createSlice 함수를 이용하여 슬라이스를 생성한다.
    • 슬라이스 이름(name) : 'cart'
    • 상태 초기값(initialState) : initialState
    • 리듀서(reducers) : { action: (state, { payload }) => {}, ..., action: (state, { payload }) => {} }
      • 리듀서 안에 액션(Action)들을 정의해준다.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { openModal } from '../modal/modalSlice';
const url = 'https://www.example-api.com/cart-project';
// 상태 초기값 설정하기
const initialState = {
cartItems: [],
amount: 0,
total: 0,
isLoading: true,
};
// API 통신 후 데이터 가져오기
export const getCartItems = createAsyncThunk(
'cart/getCartItems',
async (name, thunkAPI) => {
try {
// console.log(name);
// thunkAPI를 이용하여 다양한 정보를 얻을 수 있다. (state 등)
// console.log(thunkAPI);
// console.log(thunkAPI.getState());
// thunkAPI.dispatch(openModal());
const resp = await axios(url);
return resp.data;
} catch (error) {
return thunkAPI.rejectWithValue('something went wrong');
}
}
);
// 슬라이스 생성하기
const cartSlice = createSlice({
// (1) 슬라이스 이름 설정
name: 'cart',
// (2) 상태 초기값 설정
initialState,
// (3) 리듀서 설정
reducers: {
// 액션1 : 카트 비우기
clearCart: (state) => {
state.cartItems = [];
},
// 액션2 : 아이템 삭제하기
removeItem: (state, action) => {
const itemId = action.payload;
state.cartItems = state.cartItems.filter((item) => item.id !== itemId);
},
// 액션3 : 아이템 개수 증가시키기
// payload에는 dispatch를 수행하는 외부 컴포넌트에서 보내는 값이 들어있다.
increase: (state, { payload }) => {
const cartItem = state.cartItems.find((item) => item.id === payload.id);
cartItem.amount = cartItem.amount + 1;
},
// 액션4 : 아이템 개수 감소시키기
decrease: (state, { payload }) => {
const cartItem = state.cartItems.find((item) => item.id === payload.id);
cartItem.amount = cartItem.amount - 1;
},
// 액션5 : 전체 아이템 개수 및 가격 계산하기
calculateTotals: (state) => {
let amount = 0;
let total = 0;
state.cartItems.forEach((item) => {
amount += item.amount;
total += item.amount * item.price;
});
state.amount = amount;
state.total = total;
},
},
// (4) 추가 리듀서 설정
// RTK 2.0부터 객체 형태가 아닌, '빌더 콜백' 형태로 사용한다.
extraReducers: (builder) => {
builder
// pending 상태일 경우 (pending)
.addCase(getCartItems.pending, (state) => {
state.isLoading = true;
})
// 모두 불러와졌을 경우 (fulfilled)
.addCase(getCartItems.fulfilled, (state, action) => {
// console.log(action);
state.isLoading = false;
state.cartItems = action.payload; // 불러와진 데이터로 cartItem 업데이트
})
// 거절되었을 경우 (rejected)
.addCase(getCartItems.rejected, (state, action) => {
console.log(action);
state.isLoading = false;
});
},
});
// <1> 리듀서 내보내기
// - 기본 내보내기를 했기 때문에, 외부에서 불러올 때는 아무 이름으로 불러올 수 있다.
// 예) import cartReducer from './features/cart/cartSlice';
export default cartSlice.reducer;
// <2> 액션 내보내기
// 외부에서 사용할 수 있는 액션을 내보낸다.
export const { clearCart, removeItem, increase, decrease, calculateTotals } =
cartSlice.actions;

 

./src/features/modal/modalSlice.jsx
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isOpen: false,
};
const modalSlice = createSlice({
name: 'modal',
initialState,
reducers: {
// 액션1 : 모달 열기
openModal: (state, action) => {
state.isOpen = true;
},
// 액션2 : 모달 닫기
closeModal: (state, action) => {
state.isOpen = false;
},
},
});
// 리듀서 내보내기
export default modalSlice.reducer;
// 액션 내보내기
export const { openModal, closeModal } = modalSlice.actions;

 

스토어(Store) 생성하기

./src/store/index.js
  • ./src 경로에 store 폴더를 만들고, 안에 index.js 파일을 생성한다.
  • 그리고 안에서 위에서 생성했던 슬라이스 파일을 불러온 후, configureStore()를 이용하여 리듀서를 설정해준다.
    • 이제 외부 컴포넌트에서에서 이 저장소를 통해 슬라이스의 값(cart, modal)에 접근할 수 있게 된다.
import { configureStore } from '@reduxjs/toolkit';
// (참고) export default로 내보내기 했기 때문에, import 할 때는 아무런 이름을 지정하여 가져올 수 있다.
import cartReducer from '../features/cart/cartSlice';
import modalReducer from '../features/modal/modalSlice';
export const store = configureStore({
reducer: {
// 외부에서 접근할 때 다음과 같이 사용한다.
// -> const { value } = useSelector((state) => state.cart);
cart: cartReducer,
modal: modalReducer,
},
});

 

 

최상위 경로의 파일(main.jsx)에 스토어 적용하기

./src/main.jsx
  • 최상위 경로의 파일(./src/main.jsx)에 스토어(Store, ./src/store/index.js)를 적용시켜준다.
    • react-redux 패키지에서 Provider 컴포넌트를 불러온다.
    • ./store/index.js 파일에서 store를 불러온다.
    • 최상위 컴포넌트인 <App /> 컴포넌트를 <Provider 컴포넌트로 감싸 준다.
      • store prop의 값을 ./store/index.js 파일에서 불러온 store로 설정해준다.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
// 리덕스 사용하기
import { Provider } from 'react-redux';
import { store } from './store/index.js';
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);

 

사용하기

  • 이제 전역 상태 관리를 위한 구성을 끝냈다.
  • 전역 상태 변수액션에 접근하는 방법을 정리해본다.

 

전역 상태 변수에 접근하기
  • react-redux 패키지의 useSelector 함수를 사용하여 전역 상태 변수에 접근할 수 있다.
import { CartIcon } from '../icons';
import { useSelector } from 'react-redux';
const Navbar = () => {
// 전역 상태 변수에 접근하기
const { amount } = useSelector((state) => state.cart);
return (
<nav>
<div className="nav-center">
<h3>Cart with Redux Toolkit</h3>
<div className="nav-container">
<CartIcon />
<div className="amount-container">
<p className="total-amount">{amount}</p>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;

 

액션에 디스패치 하기
  • react-redux 패키지의 useDispatch 함수를 이용하여 액션 디스패치할 수 있다.
  • 이때, 디스패치하기 위해 필요한 함수들을 슬라이스 파일에서 불러와준다.
    • import { removeItem, increase, decrease } from '../features/cart/cartSlice';
  • 이렇게 디스패치를 함으로써 전역적으로 상태를 변화시킬 수 있다.
import { useDispatch } from 'react-redux';
import { ChevronDown, ChevronUp } from '../icons';
import { removeItem, increase, decrease } from '../features/cart/cartSlice';
const CartItem = ({ id, img, title, price, amount }) => {
const dispatch = useDispatch();
return (
<article className="cart-item">
<img src={img} alt={title} />
<div>
<h4>{title}</h4>
<h4 className="item-price">${price}</h4>
{/* remove button */}
<button className="remove-btn" onClick={() => dispatch(removeItem(id))}>
remove
</button>
</div>
<div>
{/* increase amount */}
<button
className="amount-btn"
onClick={() => dispatch(increase({ id }))}
>
<ChevronUp />
</button>
{/* amount */}
<p className="amount">{amount}</p>
{/* decrease amount */}
<button
className="amount-btn"
onClick={() => {
if (amount === 1) {
dispatch(removeItem(id));
return;
}
dispatch(decrease({ id }));
}}
>
<ChevronDown />
</button>
</div>
</article>
);
};
export default CartItem;

 

프로젝트 구조 정리
/src
├─ /features ─ /cart/cartSlice.js, /modal/modalSlice → 슬라이스 파일 보관
├─ /store ─ index.js → 스토어 파일 보관
├─ /components → 전역 상태 변수 접근 및 액션 디스페치
├─ App.jsx
└─ index.jsx → 스토어 적용

 

사용 시 주의 사항

  • RTK 2.0부터는 createReducercreateSlice.extraReducers에서 객체(Object) 형태의 사용을 더 이상 지원하지 않는다.
// RTK 2.0 이전의 방식
const cartSlice = createSlice({
// ...
extraReducers: {
[getCartItems.pending]: (state) => {
state.isLoading = true;
},
[getCartItems.fulfilled]: (state, action) => {
console.log(action);
state.isLoading = false;
state.cartItems = action.payload;
},
[getCartItems.rejected]: (state) => {
state.isLoading = false;
},
},
}

 

  • 빌더(Builder) 콜백 형태간결하며, 타입스크립트(TypeScript)와의 통합에서도 더 효과적이기 때문이다.
// RTK 2.0 이후의 방식
// -> 빌더 콜백 형태로 사용한다.
const cartSlice = createSlice({
// ...
extraReducers: (builder) => {
builder
.addCase(getCartItems.pending, (state) => {
state.isLoading = true;
})
.addCase(getCartItems.fulfilled, (state, action) => {
// console.log(action);
state.isLoading = false;
state.cartItems = action.payload;
})
.addCase(getCartItems.rejected, (state, action) => {
console.log(action);
state.isLoading = false;
});
},
}

 

함께 사용하면 좋은 것들

Redux DevTools

  • 브라우저 확장 프로그램이다.
  • 애플리케이션의 상태(state)를 쉽게 디버깅하고 추적할 수 있게 도와준다.
  • 애플리케이션의 상태 변화, 액션(action)의 흐름, 상태 변화 전후의 차이를 실시간으로 확인할 수 있다.
 

Redux DevTools - Chrome 웹 스토어

Redux DevTools for debugging application's state changes.

chromewebstore.google.com

 

참고 사이트

 

Redux - A JS library for predictable and maintainable global state management | Redux

A JS library for predictable and maintainable global state management

redux.js.org

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

 

728x90
728x90

리덕스(Redux), 리덕스 툴킷(Redux Toolkit)들어가며리덕스(Redux)개념액션(Action)리듀서(Reducer)스토어(Store)미들웨어(Middleware)사용 방법사용 시 주의 사항리덕스 툴킷(Redux Toolkit, RTK)개념Immer와 상태 불변성(Immutability)createAsyncThunk() 함수사용 방법슬라이스 생성하기스토어(Store) 생성하기최상위 경로의 파일(main.jsx)에 스토어 적용하기사용하기사용 시 주의 사항함께 사용하면 좋은 것들Redux DevTools참고 사이트