[๐Ÿ”ฅ์†ก์ด์™€ ๐Ÿฅ•ํ™] Redux Toolkit + Nextjs

XCC629ยท2022๋…„ 7์›” 21์ผ
1
post-thumbnail

๋ฆฌ๋•์Šค ํˆดํ‚ท์€ ๋ฆฌ๋•์Šค์˜ ๋ณต์žกํ•œ ํ™˜๊ฒฝ์„ค์ •์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๊ฑด ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์Šต๋‹ˆ๋‹ค.

๋ช‡ ๊ฐ€์ง€ ๊ฐœ๋…๋งŒ ์•Œ์•„๋‘๋ฉด ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


1. slice

action, ruducers ๋“ฑ๋“ฑ๋“ฑ ๊ด€๋ จ๋œ ๊ฑธ ํŽธํ•˜๊ฒŒ ํ•œ๋ฒˆ์— ์ •์˜ํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฆ„์€ ๊ทธ๋ƒฅ slice๋ผ๊ณ  ๊ณต์‹๋ฌธ์„œ์—์„œ ๋ถ€๋ฆ…๋‹ˆ๋‹ค. ์ด์œ ๋Š” ๋ชจ๋ฆ„

  • createSlice

์˜ˆ์‹œ(๊ตฌ์„ฑ)

import { createSlice } from '@reduxjs/toolkit';

// ํƒ€์ž…์„ ์–ธ 


// ์ดˆ๊ธฐ ์ƒํƒœ ์ •์˜
const initialState = { value: 0 }; 


// createSlice ์ž๋ฆฌ
// action & reducers ๋™์‹œ์— ํŽธํ•˜๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒํ•ฉ๋‹ˆ๋‹ค.
// name, ์ดˆ๊ธฐ๊ฐ’, reducers๋งŒ ๋„ฃ์–ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
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; // ๋ฆฌ๋“€์„œ


2. store

์ด๋ ‡๊ฒŒ ์ •์˜ํ•œ ์•ก์…˜๋“ค๊ณผ ๋ฏธ๋“ค์›จ์–ด ๋“ฑ๋“ฑ์„ ์ •์˜ํ•˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ๋งŒ์•ฝ context api์™€ redux ์ค‘ ๊ณ ๋ฏผํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ๋‹จ์ง€ reducer์™€ action, dispatch๋งŒ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ „์ž๋ฅผ, ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ฆฌ๋•์Šค๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒŒ ๋งž๋‹ค๋Š” ์ด์•ผ๊ธฐ๋ฅผ ์–ด๋””์„œ ๋ณด์•˜์Šต๋‹ˆ๋‹ค... ๐Ÿ’โ€โ™€๏ธ

  • ConfigureStore: ์ด๊ฑฐ์˜ ๋ ˆ๊ฑฐ์‹œ ๋ฒ„์ „์ด ์žˆ๋Š”๋ฐ ์•„๋งˆ ์ธํ„ฐ๋„ท์ž๋ฃŒ์—์„œ๋Š” ๊ทธ๊ฑธ ์“ฐ๋Š” ์˜ˆ์‹œ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. vsCode์—์„œ ์ž๋™์œผ๋กœ ConfigureStore๋ฅผ ์“ฐ๋ผ๊ณ  ๊ถŒ์žฅํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.(eslint์—์„œ ๋ด์ฃผ์งˆ ์•Š์Šต๋‹ˆ๋‹ค.)

๋ณดํ†ต ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

const store = configureStore({
  reducer: ์‚ฌ์šฉํ•˜๋Š” ๋ฆฌ๋“€์„œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.,
  middleware: ๋ฏธ๋“ค์›จ์–ด ์ •์˜,
  devTools: process.env.NODE_ENV !== 'production',
})

๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉ๊นŒ์ง€ ํฌํ•จ๋œ ์˜ˆ์‹œ

import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'

export type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware()
      .prepend(
        // correctly typed middlewares can just be used
        additionalMiddleware,
        // you can also type middlewares manually
        untypedMiddleware as Middleware<
          (action: Action<'specialAction'>) => number,
          RootState
        >
      )
      // prepend and concat calls can be chained
      .concat(logger),
  devTools: process.env.NODE_ENV !== 'production',
})

export type AppDispatch = typeof store.dispatch

export default store

๋Œ€ํ‘œ์ ์ธ ๋ฏธ๋“ค์›จ์–ด

logger
action๊ณผ Payload๋ฅผ ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์— ์ž๋™์œผ๋กœ ์ฐ์–ด์ค๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ๋ชจ๋“œ์—์„œ๋งŒ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •์„ ํ•ด๋‘ฌ์•ผํ•˜๋Š”๋ฐ ๊ทธ๊ฑฐ ๋ฐ”๋กœ devTools: process.env.NODE_ENV !== 'production' ๋ถ€๋ถ„ ์ž…๋‹ˆ๋‹ค.

์ด ์™ธ์—๋„ ๋งŽ์€๋ฐ ์ข‹์€ ๊ฑธ ์ฐพ์•„์„œ ์“ฐ๋ฉด ๋ฉ๋‹ˆ๋‹ค!



5. ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ

import { useDispatch } from 'react-redux';
import { ์Šคํ† ์–ด์—์„œ ๋‚ด๋ณด๋‚ธ ๋””์ŠคํŒจ์น˜ ํƒ€์ž… } from '../../redux/store';

export default function ์–ด์ฉŒ๊ณ (){
	const dispatch = useDispatch<์Šคํ† ์–ด์—์„œ ๋‚ด๋ณด๋‚ธ ๋””์ŠคํŒจ์น˜ ํƒ€์ž…>
    
    dispatch(์•ก์…˜?)


}


4. NextJs์—์„œ์˜ ์‚ฌ์šฉ

๋„ฅ์ŠคํŠธ์—์„œ๋Š” ssr์—์„œ๋„ ์Šคํ† ์–ด๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋„๋ก ์ „์ฒด์ ์ธ ์Šคํ† ์–ด๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ถ€๋ถ„์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์นœ๊ตฌ๋“ค์„ ๋‹ค ๋ฌถ์–ด์ฃผ๋Š” ๋„ฅ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค์น˜ํ•œ ํŒจํ‚ค์ง€

  • "next-redux-wrapper"
    ์ด๊ฒŒ ๋‹ค ๋ฌถ์–ด์ฃผ๋Š” ๊ฑฐ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค? ์ž์„ธํ•œ ๊ฑด ๊ฒ€์ƒ‰ํ•ด๋ณด์•„์•ผ ํ•  ๊ฒƒ ๊ฐ™์—์š”!

1. ๊ตฌ์กฐ ์„ค๊ณ„

๋ฆฌ๋•์Šค ํ™˜๊ฒฝ์€ ํ•˜๋Š” ์‚ฌ๋žŒ ๋‚˜๋ฆ„(?)์ด๊ธฐ์— ์ž์‹ ์ด ํŽธํ•œ๋Œ€๋กœ ๊ตฌ์กฐ๋ฅผ ์ž˜ ์ž‘์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํ•„์ˆ˜์ ์ธ ๋ถ€๋ถ„๋งŒ ์žˆ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. ์•ก์…˜๊ณผ ๋ฆฌ๋“€์„œ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ถ€๋ถ„
  2. ์Šคํ† ์–ด ๋ถ€๋ถ„
  3. ssr์—์„œ๋„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ „๋ถ€ ๋ฌถ์–ด์ฃผ๋Š” ๋ถ€๋ถ„

ํ˜„์žฌ ํ™˜๊ฒฝ์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. (์•„์ง ์ž‘๋™๋˜๋Š” ๊ฑด์ง€ ํ…Œ์ŠคํŠธํ•˜์ง„ ๋ชปํ•ด์„œ ๋‚˜์ค‘์— ๋ฐ”๋€” ๊ฐ€๋Šฅ์„ฑ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.)

stores
ใ„ด modules
 ใ„ด slices
 	ใ„ด ๊ฐ slices.ts
 ใ„ดindex.ts
ใ„ดindex.ts

stores/index.ts

์ „์ฒด์ ์ธ ์Šคํ† ์–ด์ž…๋‹ˆ๋‹ค. ๋ฏธ๋“ค์›จ์–ด๋กœ๋Š” logger ๋„ฃ์–ด๋’€์Šต๋‹ˆ๋‹ค.

  • createWrapper๋Š” next-redux-wrapper์—์„œ ๊ฐ€์ ธ์˜จ ๋ชจ๋“ˆ(?)๋กœ ์Šคํ† ์–ด๋ฅผ ํ•ฉ์ณ์ค๋‹ˆ๋‹ค...?
import { configureStore } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import logger from 'redux-logger';
import counterSlice from './modules/slices/counter';

const makeStore = () => {
  const store = configureStore({
    reducer: {
      //๋ฆฌ๋“€์„œ ์ด๋ฆ„: slice์ด๋ฆ„
    },
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
    devTools: process.env.NODE_ENV !== 'production',
  });
  return store;
};

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

stores/modules/index.ts

๋„ฅ์ŠคํŠธ์—์„œ ์Šคํ† ์–ด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๊ฑด ์ˆ˜์ •๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค!

import { AnyAction, CombinedState, combineReducers } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';

import test from './slices/counter';

const reducer = (
  state: CombinedState<{ test: { value: number } }>,
  action: AnyAction,
) => {
  switch (action.type) {
    case HYDRATE:
      return action.payload;
    default:
      return combineReducers({
        test,
      })(state, action);
  }
};

export default reducer;

stores/modules/slices/[์–ด์ฉŒ๊ณ ].ts

์•ก์…˜๊ณผ ๋ฆฌ๋“€์„œ๋ฅผ ์ •์˜ํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์Šฌ๋ผ์ด์Šค๋ฅผ ํ•œ ๊ณณ์— ๋ชจ์•„๋„ ๋˜๊ณ , ๋”ฐ๋กœ๋”ฐ๋กœ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ๋„ ๋ฉ๋‹ˆ๋‹ค.


2. ์‚ฌ์šฉ

_app.tsx

ํ…Œ๋งˆ์ฒ˜๋Ÿผ _app.tsx์— ์ ์šฉํ•ด์ค˜์•ผ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
stores/index.ts ์—์„œ ๋งŒ๋“ค๊ณ  ๋‚ด๋ณด๋‚ธ wrapper๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

export default wrapper.withRedux(MyApp);

exportํ•˜๋Š” ๋ถ€๋ถ„์— ์ด๋ ‡๊ฒŒ ์“ฐ๊ธฐ๋งŒ ํ•˜๋ฉด๋ฉ๋‹ˆ๋‹ค.?

์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ

์ด ๋ถ€๋ถ„์ด Next๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ์™€ ์ฐจ์ด๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค. csr์ผ ๋•Œ๋Š” ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๋Š” ๊ฒƒ ๊ฐ™์ง€๋งŒ, Ssr๋กœ ์“ฐ๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.


export const getServerSideProps = wrapper.getServerSideProps((store) => async({req, res, ...ets)} => {
	await store.dispatch(//์—ฌ๊ธฐ์— ๋„ฃ๊ธฐ);
    return { props: {} } ;
});

์ถ”๊ฐ€๋กœ ์„ค์น˜๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

redux-thunk

context api๋ž‘ ๋น„๊ตํ•˜์—ฌ ๋ฆฌ๋•์Šค๊ฐ€ ์ข‹์€ ์ . ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ๋ฅผ ์•„์ฃผ์•„์ฃผ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. redux-toolkit์ด๋ž‘๋„ ์ข‹์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  extraReducers ์ •์˜ํ•ด์ค˜์•ผํ•ฉ๋‹ˆ๋‹ค.

  • createAsyncThunck :์–˜๊ฐ€ ๋น„๋™๊ธฐ์ฒ˜๋ฆฌ ํ•ด์ฃผ๋Š” ์ข‹์€ ์• ์ž…๋‹ˆ๋‹ค.

์ด ์˜ˆ์‹œ๋Š” ๋กœ๊ทธ์ธ๊นŒ์ง€ ๋งŽ์€ ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ–ˆ๋˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.( aํ† ํฐ ๋ฐ›๊ณ , ๊ทธ aํ† ํฐ์œผ๋กœ ์—‘์„ธ์Šค ํ† ํฐ ๋ฐ›๊ณ ...๋ญ์ด๋Ÿฐ ๊ฒฝ์šฐ!)

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';

import {
  postAccessToken,
  postRequestToken,
  postSessionId,
} from '../../apis/auth';
import { RootState } from '../store';

export const postLogin = createAsyncThunk(
  'user/login',
  async (email: string, thunkAPI) => {
    const data = {
      email: '',
      requestToken: '',
      accessToken: '',
      sessionId: '',
    };
    try {
      if (localStorage.getItem('requestToken')) {
        const accessTokenResult = await postAccessToken();
        const sessionIdResult = await postSessionId(
          accessTokenResult.access_token,
        );
        return {
          ...data,
          email,
          requestToken: localStorage.getItem('requestToken'),
          accessToken: accessTokenResult.access_token,
          sessionId: sessionIdResult.session_id,
        };
      }
      const result = await postRequestToken();
      return { result };
    } catch (err) {
      return thunkAPI.rejectWithValue(await err);
    }
  },
);

export interface User {
  loading: boolean;
  email: string;
  requestToken: string;
  accessToken: string;
  sessionId: string;
}

const initialState = {
  loading: false,
  email: '',
  requestToken: '',
  accessToken: '',
  sessionId: '',
};

export const loginSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {},
  ///์—ฌ๊ธฐ ์•„๋ž˜๋กœ extraReducers๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.
  extraReducers: {
    [postLogin.pending.type]: (state) => {
      return { ...state, loading: true };
    },
    [postLogin.fulfilled.type]: (state, action: PayloadAction<User>) => {
      state.loading = false;
      state.email = action.payload.email;
      state.requestToken = action.payload.requestToken;
      state.accessToken = action.payload.accessToken;
      state.sessionId = action.payload.sessionId;
    },
    [postLogin.rejected.type]: (state) => {
      return { ...state };
    },
  },
});

export const userSelector = (state: RootState) => {
  return state.userlogin;
};
export default loginSlice.reducer;


์ฐธ๊ณ ์ž๋ฃŒ

React Redux Toolkit ์‚ฌ์šฉํ•˜๊ธฐ
๊ณต์‹๋ฌธ์„œ
[Next.js] Next.js + redux toolkit ๊ธฐ๋ณธ ์„ธํŒ…

profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž

0๊ฐœ์˜ ๋Œ“๊ธ€