[Next.js] Next.js + TypeScript 환경에서 Redux Toolkit 사용하기
📌 Redux Toolkit이란?
Redux Toolkit은 효율적인 Redux 개발을 위해, Redux의 설정을 간소화하고 편리한 API를 제공하는 전역 상태 관리 도구입니다.
yarn add @reduxjs/toolkit
✅ Redux의 단점
- 보일러플레이트 코드
- Redux의 일반적인 설정과 사용 방법에 따라 보일러플레이트 코드가 많이 필요합니다.
- 특히 액션 타입, 액션 생성자 함수 등을 수동으로 작성해야 할 때가 많습니다.
🤔 보일러플레이트 코드 (boilerplate code)
프로그래밍에서 반복적이고 일반적인 작업을 수행하기 위해 필요한 기본적인 코드 템플릿
Redux Toolkit 은 이러한 리덕스의 단점을 보완하고자 등장하였습니다.
리덕스의 설정을 간소화하고, 많은 리덕스 보일러플레이트 코드를 줄일 수 있습니다.
슬라이스(slice) 기반의 리듀서, 액션 생성 함수의 자동 생성 등을 통해 코드 작성의 생산성을 높일 수 있습니다.
📌 Next.js + TypeScript 프로젝트에 적용
1. 패키지 설치
yarn add react-redux @reduxjs/toolkit
2. 리덕스 스토어(store) 생성
@reduxjs/toolkit 의 configureStore 를 사용하여 리덕스 스토어를 생성합니다.
(rootReducer 는 추후에 설명하겠습니다)
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";
const store = configureStore({
reducer: rootReducer,
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
- RootState: Redux 스토어의 전체 상태를 나타내는 타입입니다.
- ReturType<typeof store.getState> 를 통해 store.getState 함수의 반환 타입을 추론합니다.
- useSelector 훅을 사용할 때, RootState 타입을 참조함으로써 상태의 타입을 정확하게 체크할 수 있습니다.
- AppDispatch: Redux 스토어의 디스패치 함수 타입을 정의합니다.
- store.dispatch 는 Redux 액션을 디스패치하는 함수입니다.
- useDispatch 훅을 사용할 때, AppDispatch 타입을 참조함으로써 디스패치 함수의 타입을 정확하게 체크할 수 있습니다.
3. 리덕스 슬라이스(Slice) 생성
스토어를 생성한 파일 위치에 각각의 리덕스 슬라이스를 관리할 slices 폴더를 생성합니다.
각 슬라이스는 @reduxjs/toolkit의 createSlice 메소드를 사용하여 생성합니다.
이때 createSlice 메소드는 객체를 인수로 받아 슬라이스를 생성하며, 이 객체는 다음과 같은 속성들이 포함됩니다.
- name: 슬라이스의 이름으로 쓸 문자열입니다.
- initialState: 슬라이스의 초기 상태입니다.
- reducers: 상태를 업데이트하는 리듀서 함수들의 객체입니다.
- 각 함수는 상태(state)와 액션(action)을 인수로 받아 새로운 상태를 반환합니다.
- state: 현재 슬라이스의 상태입니다.
- action: 디스패치된 액션 객체로, type과 payload 속성을 가집니다
- type: 어떤 액션이 실행되었는지를 나타내는 문자열입니다. (createSlice를 사용하면 자동으로 생성됩니다)
- payload: 액션과 함께 전달된 데이터입니다.
🤔 슬라이스 Slice
- 상태(state)와 해당 상태를 관리하는 리듀서(reducer), 액션(action)을 묶어주는 단위
- createSlice 를 통해 리덕스에서 상태와 관련된 모든 것을 하나의 슬라이스로 묶어서 관리 가능
🤔 리듀서 Reducer
- 리덕스에서 상태 변화를 처리하는 순수 함수
- 현재 상태(state)와 액션(action)을 받아 새로운 상태를 반환
- 즉, action.type 과 action.payload에 따라 새로운 상태를 변환하는 함수
// src/store/slices/counterSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state: CounterState) {
state.value++;
},
decrement(state: CounterState) {
state.value--;
},
incrementByAmount(state: CounterState, action: PayloadAction<number>) {
state.value += action.payload;
},
},
});
// 이 액션들은 useDispatch 의 dispatch 함수의 인수로 지정될 액션입니다.
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
4. RootReducer 생성
@reduxjs/toolkit의 combineReducers 메소드를 통해 루트 리듀서를 생성합니다.
이를 통해 리덕스에서 여러 개의 리듀서를 하나의 최상위 리듀서 함수로 결합하여 관리할 수 있습니다.
- 코드 분할: 애플리케이션이 커질수록 복잡해지는 상태 관리 로직을 combineReducers를 통해 여러 개의 작은 리듀서로 분리하여 관리할 수 있습니다.
- 유지보수성 향상: 각 리듀서가 특정 상태 슬라이스를 관리하므로, 코드의 가독성과 유지보수성이 높아집니다.
- 모듈화: 기능별로 상태와 리듀서를 모듈화할 수 있습니다.
// src/store/rootReducer.ts
import { combineReducers } from "@reduxjs/toolkit";
import counterReducer from "./slices/counterSlice";
const rootReducer = combineReducers({
counter: counterReducer, // 이 부분에서 지정한 이름(counter)이 useSelector에서 사용됩니다.
});
export default rootReducer;
5. Next.js 와 Redux 연결
_app.tsx 파일에서 react-redux의 Provider 를 추가하여 리덕스 스토어를 애플리케이션 전체에 제공합니다.
// pages/_app.tsx
import type { AppProps } from "next/app";
import { GlobalStyle } from "../src/styles";
import { Provider } from "react-redux";
import store from "../src/store";
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<GlobalStyle />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
6. 컴포넌트에서 Redux 사용
이제 컴포넌트에서 useSelector와 useDispatch 훅을 사용하여 리덕스 상태와 액션을 사용하겠습니다.
// pages/index.tsx
import type { NextPage } from "next";
import styled from "styled-components";
import { RootState, AppDispatch } from "../src/store";
import { increment, decrement } from "../src/store/slices/counterSlice";
import { useDispatch, useSelector } from "react-redux";
const Home: NextPage = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch<AppDispatch>();
const handleIncrement = () => {
dispatch(increment());
};
const handleDecrement = () => {
dispatch(decrement());
};
return (
<StyledDiv>
<button onClick={handleDecrement}>-</button>
<span>{count}</span>
<button onClick={handleIncrement}>+</button>
</StyledDiv>
);
};
export default Home;
const StyledDiv = styled.div`
padding: 20px 30px;
width: max-content;
border-radius: 5px;
font-weight: bold;
background-color: orange;
`;
✅ useSelector 훅
useSelector 훅은 Redux 스토어의 상태를 조회하는 데 사용됩니다.
특정 컴포넌트 내에서 useSelector 훅을 통해 상태를 조회하고, 해당 상태가 변경되면 컴포넌트도 자동으로 리렌더링됩니다.
import { useSelector } from 'react-redux';
const selectedState = useSelector((state: RootState) => state.[선택할리듀서이름].[선택할상태이름]);
(이때 '선택할리듀서이름' 은 rootReducer에서 combineReducers 메소드에서 지정한 리듀서 이름입니다)
✅ useDispatch 훅
useDispatch 훅은 Redux 스토어의 dispatch 함수를 반환합니다.
dispatch 함수는 컴포넌트 내에서 액션을 디스패치할 수 있으며, 이를 통해 Redux 스토어의 상태를 변경할 수 있습니다.
import { useDispatch } from 'react-redux';
const dispatch = useDispatch<AppDispatch>();
dispatch(디스패치할액션함수());
(이때 '디스패치할액션함수()' 는 createSlice에서 reducers 에 지정된 액션들입니다.)