사전 준비
create-next-app으로 프로젝트 생성
npx create-next-app
redux 세팅에 필요한 패키지 설치
npm i @reduxjs/toolkit
npm i react-redux
npm i next-redux-wrapper
npm i redux-logger --save-dev # 필요한 경우에 설치
cna로 앱을 생성했다면 다음과 같은 구조를 가진다.
기본 프로젝트 구조
.
├── README.md
├── next.config.js
├── node_modules
├── package-lock.json
├── package.json
├── pages
├── public
└── styles
store디렉토리를 하나 만들고 다음과 같이 구조를 잡아주자.
store디렉토리 추가 후 프로젝트 구조
.
├── README.md
├── next.config.js
├── node_modules
├── package-lock.json
├── package.json
├── pages
├── public
├── store
│ ├── modules
│ │ ├── counter.js (리듀서 모듈)
│ │ └── index.js (리듀서 모듈 통합)
│ └── index.js (store 생성 && wrapper 생성)
└── styles
store/modules/conter.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 }; // 초기 상태 정의
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => { state.value += 1 },
decrement: state => { state.value -= 1 },
},
});
export const { increment, decrement } = counterSlice.actions; // 액션 생성함수
export default counterSlice.reducer; // 리듀서
다른 컴포넌트/모듈 내에서 사용하기 위해 정의한 리듀서와 액션 생성함수를 export한다.
- createSlice: action과 reducer를 한 번에 정의한다.
- createAction + createReducer = createSlice
- 비동기적인 리듀서 함수를 정의하고자 할 땐 객체의 프로퍼티로 extraReducers 객체를 추가한다.
store/modules/index.js
import { combineReducers } from "@reduxjs/toolkit";
import { HYDRATE } from "next-redux-wrapper";
import counter from './counter';
const reducer = (state, action) => {
if (action.type === HYDRATE) {
return {
...state,
...action.payload
};
}
return combineReducers({
counter,
// 여기에 추가
})(state, action);
}
export default reducer;
modules 내에서 정의한 모듈들을 합쳐주는 역할을 한다.
- if (action.type === HYDRATE): SSR작업 수행 시 HYDRATE라는 액션을 통해서 서버의 스토어와 클라이언트의 스토어를 합쳐주는 작업을 수행한다.
- combineReducers: 함수명 그대로 정의한 리듀서 모듈들을 결합하는 역할을 한다.
- 리듀서 모듈(slice)을 추가할 때마다 combineReducers함수의 인자로 전달되는 객체 내부에 추가해준다.
이제 여기까지 완료됐다면 store디렉토리 바로 아래에 index.js라는 파일을 만들고 다음과 같이 작성한다.
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from "next-redux-wrapper";
import logger from 'redux-logger';
import reducer from './modules';
const makeStore = (context) => configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
});
export const wrapper = createWrapper(makeStore, {
debug: process.env.NODE_ENV !== 'production',
});
스토어를 생성하는 함수를 정의하고, 이를 인자로 wrapper HOC를 만든다.
- configureStore: store를 생성한다.
- redux-toolkit은 devTools 등의 미들웨어들을 기본적으로 제공한다. 여기에 사용하고 싶은 미들웨어가 있다면 추가로 정의해준다.
- 위의 코드에선 logger를 추가해줬다.
- createWrapper: wrapper를 생성한다.
- 지금까지 리듀서 모듈 만들기 -> 리듀서 모듈 합치기 -> 스토어 생성의 과정을 거쳤다.
- 이제 최종적으로 wrapper에 스토어를 바인딩해주면 된다.
pages/_app.js
import '../styles/globals.css'
import { wrapper } from "../store";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default wrapper.withRedux(MyApp)
wrapper의 withRedux HOC로 App컴포넌트를 감싸준다.
이제 각 페이지에서 getStaticProps, getServerSideProps 등의 함수 내에서 스토어 접근이 가능해진다.
작동을 확인하기 위해 다음과 같이 pages 디렉토리 내에 counter.js(counter.jsx)를 작성해보자.
pages/counter.js
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import * as counterActions from '../store/modules/counter';
export default function Test() {
const dispatch = useDispatch();
const value = useSelector(({ counter }) => counter.value);
const plus = useCallback(({ value }) => {
dispatch(counterActions.increment());
}, [dispatch]);
const minus = useCallback(({ value }) => {
dispatch(counterActions.decrement());
}, [dispatch]);
return (
<div>
<h1>Counter</h1>
<button onClick={() => minus()}>-</button>
<span>{value}</span>
<button onClick={() => plus()}>+</button>
</div>
);
}
실제 작동화면
참고