redux-toolkit 초기설정 (with NextJS hydrate)

NextJS 프로젝트에 redux-toolkit을 적용하게 되어 초기 설정과 함께 간단한 사용방법까지 정리하는 시간을 갖게 되었다. SSR로 동작하는 NextJS 특성상 Server redux store, Client redux store가 각각 생성되기 때문에 이를 합쳐주는 hydrate 작업까지 설정해보려고 한다.

redux
redux-logger // 선택!
react-redux
next-redux-wrapper
@types/react-redux
@types/redux-logger // 선택!
@reduxjs/toolkit

createSlice

createSlice 는 reducer를 생성하던 기존 방식에 액션타입을 자동으로 설정하고 상태관리를 위해 immer를 내장하는 등의 편의기능이 제공된다.

기존의 reducer를 분리하던 스타일 방식과는 다르게 공식문서에서는 slice를 features 내에 상태 도메인 단위로 나누어서 작성하는 것을 확인할 수 있다.

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

const counterSliceType = "slice/counter";

export interface ICounterState {
	value: number;
}

const initialState: ICounterState = {
	value: 0
}

const counterSlice = createSlice({
	initialState,
    name: counterSliceType,
    reducers: {
    	increase: (state) => {
        	state.value += 1;
        },
        increaseByAmount: (state, action: PayloadAction<number>) => {
        	state.value += action.payload;
        }
    }
})

export const { increase, increaseByAmount } = counterSlice.actions;
export default counterSlice;
  • initialState : 리듀서의 초기값

  • name : 슬라이스를 식별하기 위한 스트링 값
    앞으로 생성될 reducer 액션타입의 prefix가 된다
    ex: slice/counter/increase, slice/counter/increaseByAmount

  • reducers: redux-toolkit은 reducers 내부에 immer를 지원하기 때문에 별도의 설치없이 mutative한 코드로 불변 상태관리가 가능하다

FSA

대게 하나의 액션은 간단한 인자들을 바로 리턴해서 사용하기 보다는 어떠한 특정 로직을 처리한 후에 reducer에서 처리하는 방식을 많이 사용한다.
createSlice 에서는 FSA(flux의 표준 액션 생성방식)을 다음과 같이 prepare 라는 콜백을 통해 사용할 수 있다.

const counterSlice = createSlice({
	...
    reducers: {
    	...
        decrease: {
        	reducer: (state, action: PayloadAction<number>) => {
            	state.value -= action.payload;
            },
            prepare: () => {
            	return {
                	payload: Math.random()*100 // 사용자 정의 값을 payload로 반환함
                }
            }
        }
    }
})

RootReducer

리듀서는 목적에 따라 여러개를 생성할 수 있으니 여러 리듀서를 합치기 위해 기존에 사용했던 combineReducers 를 활용한다.

추가로 next-redux-wrapper가 제공하는 HYDRATE를 통해서 서버, 클라이언트 양측의 상태를 합쳐주는 작업까지 진행해준다.

interface RootStates {
	counter: ICounterState;
}

const rootReducer = (
	state: ReducerStates,
    action: AnyAction
): CombinedState<ReducerStates> => {
	
    switch(action.type) {
    	case HYDRATE:
        	return action.payload;
        default: {
        	const combinedReducer = combineReducers({
            	counter: counterSlice.reducer
            })
            return combinedReducer(state, action);
        }
    }

}

그리고 특정 컴포넌트에서 rootReducer에 접근해서 원하는 상태값을 활용할 수 있도록 타입을 정해준다
export type RootState = ReturnType<typeof rootReducer>;

store

next-redux-wrapper 는 SSR의 특성상 서버 측에서 생성되는 리덕스 스토어와 클라이언트 측에서 생성되는 리덕스 스토어를 합쳐주는 작업, 이를 통해 NextJS 에서 제공하는 getInitialProps 같은 메서드가 리덕스 스토어에 접근할 수 있도록 도와주는 꿀같은 라이브러리이다.

next-redux-wrapper 는 스토어를 생성하는 함수를 통해 루트 컴포넌트를 고차컴포넌트(HOC)로 감싸는 형태로 이루어진다.

redux-toolkit 은 store를 만들기 위해 기존 리덕스 코어 메서드인 createStore를 추상화한 configureStore 라는 새로운 메서드를 제공하며 다음과 같은 형태로 작성한다.

// store 만들기
// store.ts
const store = configureStore({
	reducer: rootReducer as Reducer<CombinedState<ReducerStates> AnyAction>,
    devTools: process.env.NODE_ENV === 'development',
});
  • reducer: 사용할 reducer, 슬라이스 리듀서들로 구성된 combinedReducers로 생성한 루트 리듀서를 사용했다.
  • devTools: 기존의 redux에서는 미들웨어를 통해 리덕스 개발자 도구를 넣는 방식이었다면, rtk는 불리언값으로 선택할 수 있게 되었다.
  • 미들웨어는 기본적으로 redux-thunk를 내장하고 있으며, 별개로 직접 원하는 미들웨어를 넣기 위해서는 다음과 같이 getDefaultMiddleware 를 활용해서 배열에 미들웨어를 추가하는 형식으로 사용한다.
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(customMiddleware)

스토어를 만들었으니 이제 이 스토어를 반환하는 함수를 만들어서 next-redux-wrapper 에서 제공하는 createWrapper 를 통해 고차함수를 제공할 wrapper 를 만들어준다.

const makeStore = () => {
	const store = configureStore({
    	...
    });
    return store;
}

const wrapper = createWrapper(makeStore);
export default wrapper;

_app.tsx 루트컴포넌트를 wrapper가 제공하는 withRedux 고차함수를 이용해 감싸준다

// _app.tsx

function MyApp(...) {
	return ..
}

export default wrapper.withRedux(MyApp)

이렇게 해주면 전체적인 redux 셋팅이 끝나게 된다.

사용해보기

// pages/index.tsx

import type { NextPage } from "next";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "src/redux/reducers";
import {
  increase,
  increaseByAmount,
  decrease,
} from "src/features/counter/counterSlice";

const Home: NextPage = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state: RootState) => state.counter.value);

  const increaseCount = () => {
    dispatch(increase());
  };

  const increaseCountByAmount = () => {
    dispatch(increaseByAmount(Math.random() * 100));
  };

  const decreaseCount = () => {
    dispatch(decrease());
  };

  return (
    <Wrapper>
      <div>{counter}</div>
      <button onClick={increaseCount}>+1</button>
      <button onClick={increaseCountByAmount}>+increase</button>
      <button onClick={decreaseCount}>-decrease</button>
    </Wrapper>
  );
};

export default Home;

const Wrapper = styled.div``;

createAsyncThunk

createAsyncThunk를 통해서 비동기 액션을 만들고 사용해보자
createAsyncThunk로 생성한 액션은 createSlice 의 reducer 내부에 사용하는게 아닌, 외부 리듀서 extraReducer 에 선언해서 사용할 수 있다.
extraReducercreateReducer 를 작성하듯이 사용하면 되는데, RTK 에서는 2가지 표기법(Map Object 표기법, builder 표기법) 중에서 선택해서 작성하면 되고, 그 중에서 builder 표기법을 이용해서 작성해보았다.

createAsyncThunk 는 첫번째 인자로 액션을 식별하기 위한 스트링값을 받는다.
두번째 인자는 실행할 액션 로직을 작성하면 된다.

export const asyncCounter = createAsyncThunk(
	'asyncCounter',
    async() => {
    	const result = await new Promise((resolve, _) => {
        	setTimeout(() => {
            	resolve(Math.random()*100);
            }, 1000);
        })
        return result;
    }
)

const counterSlice = createSlice({
	...
    extraReducers: (builder) => {
    	builder.addCase(asyncCounter.fulfilled, (state, action: PayloadAction<number>) => {
        	state.value += action.payload;
        }
    }
});

위 export한 asyncCounter 를 컴포넌트에서 가져와 사용해보면 1초 후에 값이 갱신됨을 확인할 수 있다.
예시에 있는 asyncCounter 의 fulfilled 시점 뿐만 아니라 pending, reject 와 같은 다양한 비동기 시점에서 사용가능하다.

references

https://redux-toolkit.js.org/tutorials/overview
https://redux-toolkit.js.org/tutorials/quick-start
https://redux-toolkit.js.org/tutorials/typescript
https://redux-toolkit.js.org/usage/usage-with-typescript
https://simsimjae.medium.com/next-redux-wrapper%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0-5d0176209d14

잘못된 부분이 있다면 댓글 부탁드리겠습니다. 빠른 시일내로 수정하겠습니다
1차 기록 4.19

좋은 웹페이지 즐겨찾기