끄적이는 개발노트
Next.js 기본 사용법(10) - next-redux-wrapper 본문
기존의 React.js에도 상태를 관리하는 Redux 가 있고, store가 존재한다.
일반적으로 React.js에는 하나의 Redux store만 존재하기 때문에 Redux를 만드는데 별다른 어려움이 없다.
하지만, Next.js에서 Redux를 사용하게 되면 사용자가 request를 보낼 때마다 Redux store가 생성되기 때문에 여러 개의 Redux store가 생성된다.
또한, Next.js에서 제공하는 getInitialProps, getServerSideProps 등에서 Redux store에 접근할 수 있어야 하기 때문에 복잡하다.
이를 간편하게 해결해주는 라이브러리가 바로 next-redux-wrapper 이다.
이번 포스트에서는 카운터 state를 통해 사용법을 알아본다.
1. 프로젝트 생성
$> npx create-next-app next_redux_example --typescript
2. npm 설치
$> npm i react-redux redux next-redux-wrapper
$> npm i redux-devtools-extension redux-logger --dev-save
3. store 폴더 생성 및 wrapper 설정
우선, 루트에 store 폴더를 생성하고, storeConfigure.ts 파일을 생성한다.
/store/configureStore.ts
// /store/configureStore.ts
import { createStore, applyMiddleware, compose } from "redux";
import { createWrapper } from "next-redux-wrapper";
import { composeWithDevTools } from "redux-devtools-extension";
import rootReducer from "./reducer";
const configureStore = () => {
const store = createStore(rootReducer);
return store;
};
const wrapper = createWrapper(configureStore, { debug: true });
export default wrapper;
_app.tsx
// app.tsx
import "../styles/globals.css";
import { AppProps } from "next/app";
import { NextPage } from "next";
import wrapper from "../store/configureStore";
const MyApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
return (
<>
<Component {...pageProps} />
</>
);
};
export default wrapper.withRedux(MyApp);
위와 같이 생성한 configureStore 함수를 정의해서 _app.tsx에 넘기고 wrapper의 withRedux 로 App 컴포넌트를 감싸준다.
그럼 이제 각 페이지의 getInitialProps, getServerSideProps, action의 dispatch 등을 통해 store에 있는 상태값에 접근이 가능하다.
4. interfaces 폴더 생성
현재 타입스크립트를 사용하므로 interface를 설정해줘야 한다.
이를 위해 interfaces 폴더를 만들고 그 안에 필요한 파일들을 다음과 같이 생성한다.
/store/interfaces/index.ts
// /store/interfaces/index.ts
export * from "./counter/counter.interfaces";
export * from "./counter/counterAct.interfaces";
/store/interfaces/RootState.ts
// /store/interfaces/RootState.ts
import { CounterState } from "./index";
export interface RootStateInterface {
counter: CounterState;
}
interfaces 폴더 아래에 counter 폴더를 만들고 아래 두 파일을 생성한다.
/store/interfaces/counter/counter.interface.ts
counter state의 타입을 지정한다.
// /store/interfaces/counter/counter.interface.ts
export interface CounterState {
count: number;
}
/store/interfaces/counter/counterAct.interface.ts
counter state의 액션에 대해 타입을 지정한다.
// /store/interfaces/counter/counterAct.interface.ts
export enum actionTypesCounter {
COUNTER_INCREMENT = "COUNTER_INCREMENT", // 숫자 + 1
COUNTER_DECREMENT = "COUNTER_DECREMENT", // 숫자 - 1
COUNTER_RESET = "COUNTER_RESET", // 초기화
}
export type ActionsCounter = CounterIncrement | CounterDecrement | CounterReset;
export interface CounterIncrement {
type: actionTypesCounter.COUNTER_INCREMENT;
}
export interface CounterDecrement {
type: actionTypesCounter.COUNTER_DECREMENT;
}
export interface CounterReset {
type: actionTypesCounter.COUNTER_RESET;
}
5. reducer 생성
실질적인 state 작업을 하는 곳으로 아래 두 파일을 생성한다.
/store/reducer/index.ts
// /store/reducer/index.ts
import { combineReducers, Reducer, AnyAction } from "redux";
import { RootStateInterface } from "../interfaces/RootState";
import counter from "./counter";
const rootReducer: Reducer<
RootStateInterface,
AnyAction
> = combineReducers<RootStateInterface>({
counter,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
분리된 reducer(현재는 counter 하나)를 combineReducers 를 사용해서 합쳐준다.
/store/reducer/counter.ts
// /store/reducer/counter.ts
import { HYDRATE } from "next-redux-wrapper";
import {
CounterState,
actionTypesCounter,
ActionsCounter,
} from "../interfaces";
export const initialState: CounterState = {
count: 0,
};
interface HydratePayload {
counter: CounterState;
}
const counter = (
state = initialState,
action: ActionsCounter | { type: typeof HYDRATE; payload: HydratePayload },
): CounterState => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload.counter };
case actionTypesCounter.COUNTER_INCREMENT:
return {
...state,
...{ count: state.count + 1 },
};
case actionTypesCounter.COUNTER_DECREMENT:
return {
...state,
...{ count: state.count - 1 },
};
case actionTypesCounter.COUNTER_RESET:
return {
...state,
...{ count: initialState.count },
};
default:
return state;
}
};
export default counter;
만들어 준 reducer 에서 action.type 으로 사용할 Hydrate를 next-redux-wrapper 로부터 import 한다.
Hydrate
서버 단에서 렌더링 된 정적 페이지와 번들링된 JS파일을 클라이언트에게 보낸 뒤, 클라이언트 단에서 HTML 코드와 React인 JS코드를 서로 매칭시키는 과정을 말한다.
여기서 사용된 Hydrate는 SSR을 위한 것으로, getInitialProps, getServerSideProps에서도 Redux store에 접근이 가능하게 하기 위한 처리다.
6. index.tsx 수정
pages/counter.tsx
// pages/counter.tsx
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootStateInterface } from "../store/interfaces/RootState";
import { CounterState } from "../store/interfaces";
import { actionTypesCounter } from "../store/interfaces/counter/counterAct.interface";
const Counter = () => {
const { count } = useSelector(
(state: RootStateInterface): CounterState => state.counter
);
const dispatch = useDispatch();
const handleClick = (num: number) => {
if (num === 1) {
dispatch({ type: actionTypesCounter.COUNTER_INCREMENT });
} else if (num === 0) {
dispatch({ type: actionTypesCounter.COUNTER_DECREMENT });
} else if (num === 2) {
dispatch({ type: actionTypesCounter.COUNTER_RESET });
}
};
return (
<div>
<div>현재값:{count}</div>
<div>
<button onClick={() => handleClick(1)}>+</button>
<button onClick={() => handleClick(0)}>-</button>
<button onClick={() => handleClick(2)}>reset</button>
</div>
</div>
);
};
export default Counter;
useSelector를 통해 Redux store에서 원하는 데이터를 가져올 수 있다.
또한, useDispatch를 통해 Redux store에서 함수에 대한 참조를 반환한다.
7. 실행
실행을 해보면 다음과 같이 화면이 뜨는 것을 확인할 수 있다.
버튼을 각각 클릭해보면 +, -, reset 기능이 원활하게 작동하며 state 값이 변경되는 것을 확인할 수 있다.
여기에 redux-thunk 라는 개념도 있지만, 이는 next-nest 프로젝트에서 사용을 하였기 때문에 거기서 알아보도록 한다.
이상으로, Next.js에서 Redux를 통해 상태 관리하는 방법에 대해 알아보았다.
'JavaScript > Next.js' 카테고리의 다른 글
react-query (2) - useQuery (0) | 2023.02.14 |
---|---|
react-query (1) - 설치 및 설정 (0) | 2023.02.14 |
Next.js 기본 사용법(9) - 환경변수 (0) | 2021.11.03 |
Next.js 기본 사용법(8) - Built-In CSS (0) | 2021.11.01 |
Next.js 기본 사용법(7) - Data Fetching | getServerSideProps (0) | 2021.11.01 |