Next + TypeScript 에 리덕스툴킷 적용하기 (+ React)

퍼렁꽁치·2022년 4월 1일
4

리액트를 하다보면 전역 상태 관리에 대한 갈증을 느낀다.
그때 필요한게 뭐다? 바로 전역 상태 관리 라이브러리들이다.

어떤 라이브러리를 써야할까?

전역 상태 관리를 위한 라이브러리는 많이 나와있다.
대표적으로 Redux, MobX, recoil 등이 있다.
npm trends 라는 사이트를 가면 라이브러리들의 사용 빈도를 비교해볼 수 있다.
그 중에 나는 가장 많이 쓰이는 리덕스, 그 중에 리덕스툴킷을 선택해서 쓰고 있다.

이 글에선 넥스트(또는 리액트)에서 리덕스툴킷을 타입스크립트로 적용하는 방법에 대해 작성해보고자 한다.

1. 필요한 패키지 다운 받기

  1. react-redux
  2. @types/react-redux
  3. @reduxjs/toolkit

기본적으로 이 세가지 패키지를 다운받아 준다.

2. slice 구성하기

기존의 리덕스는 작성해야될 코드의 양이 굉장히 많고 복잡했다.
그런데 리덕스툴킷으로 오면서 코드의 양이 이전에 비해 심플해졌고 적어졌다.
그 중에 리덕스툴킷에서 제공하는 createSlice 라는 함수로 쉽게 전역 상태와, 액션, 리듀서들을 구성하기 쉬워졌다.

import { createSlice } from '@reduxjs/toolkit'

export const userSlice = createSlice({
	name: 'user',
  	initialState: {},
  	reducers: {},
})

export cosnt {} = userSlice.actions

export default userSlice.reducer

나는 사용자 user 에 관한 전역 상태 관리를 위한 userSlice 를 만들었다.
userSlice 를 만들기 위한 기본 구성은 이렇다.
createSlice() 의 인자로 들어오는 객체에는 기본적으로 name, initialState, reducers 이 3가지가 들어온다.

  • name 에는 이 slice 의 이름이,
  • initialState 에는 전역으로 관리할 상태의 초기값을,
  • reducers 에는 이 상태를 변경시키는 리듀서 함수가 오게 된다.

리듀서 함수 정의하기

타입스크립트까지 적용해서 실제로 내용을 작성하면 이렇게 된다.

import { createSlice } from '@reduxjs/toolkit'

// initialState 의 기본 타입을 인터페이스로 지정해줌
interface UserState {
  myInfo: UserType | null
  loading: boolean
  error: object | null
}

const initialState: UserState = {
  myInfo: null,
  loading: false,
  error: null
}

export const userSlice = createSlice({
	name: 'user',
  	initialState,
  	reducers: {
    	loadMyInfo: (state, action) => {
        	state.myInfo = action.payload
        },
    },
})

export cosnt { loadMyInfo } = userSlice.actions

export default userSlice.reducer

먼저 interface 로 전역상태의 초기값인 initialState 의 각 상태들의 타입을 지정해주었고,
해당 타입으로 initialState 의 초기값을 할당해준 모습이다.

그리고 reducers 에는 loadMyInfo 라는 리듀서함수를 작성해서 인자로 들어온 action.payloadstate.myInfo 에 할당해서 전역상태를 변경할 수 있게 해주는 구조다.

비동기 리듀서함수 정의하기

만약에 myInfo 상태를 비동기로 받아온다면 slice 안에 extraReducers 안에다가 정의해준다.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

// `createAsyncThunk` 로 비동기액션함수를 만듦
export const loadMyInfo = createAsyncThunk(
  "GET/LOAD_MY_INFO_REQUEST",
  async () => {
    const response = await axios.get('/user/myInfo')
    return response.data
  }
)

// initialState 의 기본 타입을 인터페이스로 지정해줌
interface UserState {
  myInfo: UserType | null
  loading: boolean
  error: object | null
}

const initialState: UserState = {
  myInfo: null,
  loading: false,
  error: null
}

export const userSlice = createSlice({
	name: 'user',
  	initialState,
  	reducers: {},
  	extraReducers: {
        [loadMyInfo.pending.type]: (state, action) => {
          state.loading = true
          state.myInfo = null
        },
        [loadMyInfo.fulfilled.type]: (state, action) => {
          state.loading = false
          state.myInfo = action.payload
        },
        [loadMyInfo.rejected.type]: (state, action) => {
          state.loading = false
          state.error = action.error.message
        },
    },
})

export cosnt {} = userSlice.actions

export default userSlice.reducer

비동기로 myInfo 데이터를 받아오기 위해서 리덕스툴킷에서 createAsyncThunk 함수를 import 했다.
createAsyncThunk 비동기 액션함수의 첫번째 인자로는 '액션 타입'을 적어주고, 두번째 인자로 실제 받아오는 로직을 적어주면 된다.

그럼 이제 slice 안에서 extreaReducers 에 각 상태에 따른 상태처리를 해주면 된다.

  1. 첫번째로 loadMyInfo 가 pending 상태 즉 현재 불러오고 있는 상태에서는 state.loading 을 true 로 해준 상태다. loading 전역 상태를 둔 이유는 이 loading 이 true 인 경우 스켈레톤이나 스피너 등으로 사용자 화면에서 로딩상태를 표시해줄 수 있기 때문이다.

  2. 두번째로 loadMyInfo 가 fulfilled 상태 즉 로딩이 성공적으로 완료되었을 때는 state.loading 을 false 로 바꿔 로딩 상태를 해제해주고, state.myInfo 에 action.payload 로 받아온 유저정보를 넣어주면 된다.

  3. 세번째로 loadMyInfo 가 rejected 상태 즉 어떤 이유로 비동기로직이 실패했을 때는 에러핸들링을 해주면 된다. 우선 state.loading 을 false 로 바꿔 로딩 상태를 해제해준 뒤, state.error 에 에러메시지를 넣어주었다.

대강 이런 식으로 리덕스툴킷의 slice 안에서 전역상태를 관리해줄 수 있다.
그러나 이게 끝이 아니다. 실제로 프로젝트에 적용되도록 해야 한다.

3. configureStore 구성하기

// store/configureStore.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer from 'store/userSlice'

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

configureStore.ts 파일을 생성한 뒤 위처럼 작성해준다.
configureStore 함수는 이렇게 작성한 slice 들을 하나의 reducer 로 종합해 구성해주고,
이렇게 완성된 store 를 내보내기 해준다.

또 밑에서는 store.getStatestore.dispatch 의 타입을 정의해준다.

4. hooks 정의하기

// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./configureStore";

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

react-redux 를 받으면 useDispatchuseSelector 로 액션을 dispatch 할 수도 있고, 전역 상태를 꺼내올 수도 있다.
타입스크립트로 리덕스를 쓸 때는 이들을 타입을 붙여서 만든 useAppDispatchuseAppSelector 로 사용한다. 기능은 같다.

configureStore.ts 에서 정의한 타입들이 여기 hooks.ts 에서 쓰인다.

5. Provider 로 감싸기

이제 전역상태를 관리하는 store 도 구성됐고, 이들을 dispatch 하고 꺼내오는 useAppDispatchuseAppSelector 훅도 정의해놨기 때문에 프로젝트에서 쓸 수 있도록 <Provider> 로 감싸서 store 를 쓸 수 있도록 흘려보내주면 된다.

Next

// _app.tsx
import { Provider } from "react-redux";
import { store } from "store/configureStore";
...

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <ThemeProvider theme={themeOptions}>
        <Provider store={store}>
          <AppLayout>
            <Component {...pageProps} />
          </AppLayout>
        </Provider>
      </ThemeProvider>
    </>
  );
}

React

// app.tsx
import React from 'react';
import { Provider } from "react-redux";
import { store } from "store/configureStore";

const App: FC = () => {
	return (
    	<Provider store={store}>
      		<Home />
      	</Provider>
    )
}

넥스트는 _app.tsx 에서, 리액트는 index.ts 또는 app.tsx 에서 전체 프로젝트를 <Provider> 로 감싼뒤 store 를 내려주면 이제 store 에서 전역으로 상태를 관리할 수 있고 어디서든 dispatch 하고 select 할 수 있게 된다.

6. 리덕스 훅 사용하기

실제로 이제 전역 상태를 꺼내고 변경할 때는 useAppDispatchuseAppSelector 훅을 사용하면 된다.
기존의 react-reduxuseDispatch, useSelector 훅과 똑같이 사용하면 된다.

import { useAppDispatch, useAppSelector } from 'store/hooks'
import { loadMyInfo } from 'store/userSlice'

const Home: NextPage = () => {
	const dispatch = useAppDispatch()
	const myInfo = useAppSelector(state => state.user.value)
    
    useEffect(() => {
    	dispatch(loadMyInfo(data))
    }, [])

	return (
		// ...
	)
}



이상 넥스트와 리액트에서 리덕스툴킷을 사용하고 타입스크립트를 적용하는 방법에 대해 알아봤다.
이 또한 굉장히 반복적으로 보일러 플레이트처럼 쓰이는 부분이기도 하지만, 또 리덕스툴킷은 공식 홈페이지에서 다양한 함수들을 제공하고 있다. 꼭 이 방식이 아니어도 전역상태를 관리할 수 있다는 말이다.

리덕스 툴킷 공식문서

또한 리덕스툴킷에 타입스크립트를 적용하는 방법도 공식문서에 잘 나와있기 때문에 잘 참고하면 좋을 것 같다.

profile
무엇이든 될 수 있는 멋쟁이 토마토🍅 프론트 꿈나무💙

0개의 댓글