NextJS + next-redux-wrapper 로 서버사이드 환경에서 전역상태 꺼내먹기 (ft. 리덕스툴킷, 타입스크립트)

퍼렁꽁치·2022년 3월 24일
3

리덕스와 씨름한 고난의 행군길의 여정..

개인 프로젝트를 만들어 나가면서 리덕스로 전역상태를 관리할 필요성을 절실히 느꼈고 리덕스툴킷으로 전역상태를 관리하고 있었다.
각 숙소의 상세페이지를 만들면서 컴포넌트를 세분화해서 만들수록 props 를 자식 컴포넌트로 내려주고 내려주고 내려주고... 일명 '드릴링'이 심해져서 전역으로 상태를 관리해야겠다고 마음먹었다.

그런데 예상치 못한 난관을 만났으니..

숙소의 객실정보, 안내 등을 담은 상세페이지의 정보는 크게 변하지 않기 때문에 getStaticProps 로 정적 서버사이드 렌더링을 해주고 있었는데, 숙소 정보를 담은 데이터를 전역으로 관리해서 각 컴포넌트에서 useSelector 로 뿌려주면 서버사이드에서 나타나지 않는 문제점이 발생했다.
이에 대한 해결책을 여러방면 찾아 보았고 내가 찾은 해결책은 next-redux-wrapper 라는 라이브러리였다.

리덕스 너 너무 어렵다

// roomsSlice.ts
import { createSlice } from "@reduxjs/toolkit";

interface RoomState {
  value: any[]
  roomData: any | null
}
const initialState: RoomState = {
  value: [],
  roomData: null
}

export const roomsSlice = createSlice({
  name: 'rooms',
  initialState,
  reducers: {
    // 숙소 상세페이지 접속시 숙소 정보를 roomData 에 저장
    addRoomInfo: (state, action) => {
      state.roomData = action.payload
    },
  },
  extraReducers: {},
})

export const { addRoomInfo } = roomsSlice.actions

export default roomsSlice.reducer

기존의 내 리덕스툴킷 slice 는 대충 이렇게 생겼었다.

// configureStore.ts
import { configureStore } from '@reduxjs/toolkit'
import userReducer from 'store/usersSlice'
import feedReducer from 'store/feedsSlice'
import filterReducer from 'store/filterSlice'
import restaurantReducer from 'store/restaurantsSlice'
import roomsReducer from 'store/roomsSlice'

export const store = configureStore({
  reducer: {
    users: userReducer,
    feeds: feedReducer,
    filter: filterReducer,
    restaurants: restaurantReducer,
    rooms: roomsReducer,
  },
  middleware: (getDefaultMiddleware) => 
    getDefaultMiddleware({
      serializableCheck: false
    })
})

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

그리고 이런 슬라이스들을 configureStore.ts 파일에서 종합해서 store 를 만들었고 _app.tsx 에서 <Provider> 로 감싼 뒤 store 를 내려주고 있는 형태였다.
그래서 특별히 문제가 있었냐?
없었다. 서버사이드에서 전역상태를 꺼내먹을 수 없다는 걸 알기 전까진

=> 그래서 도입한 것이 바로 next-redux-wrapper 이녀석이다!! 이제부터 나의 삽질의 기록의 결과가 나타난다.

1. next-redux-wrapper 설치

yarn add next-redux-wrapper

먼저 next-redux-wrapper 를 설치해준다

2. rootReducer 생성하기

사실 내가 적용한 코드에 대해서 백퍼센트 이해하고서 작성한 것은 아니었다.
여러모로 삽질을 너무 많이했고 그중에 가장 상세하게 설명이 적힌 블로그의 코드를 참고해서 차근차근 따라해보았다.
삽질을 열심히 하면서 내가 빼먹은 부분이 바로 이 부분이었다.

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

import userReducer, { UserState } from "store/slices//usersSlice";
import feedReducer, { FeedState } from "store/slices/feedsSlice";
import filterReducer, { FilterState } from "store/slices//filterSlice";
import restaurantReducer, { RestaurantState } from "store/slices/restaurantsSlice";
import roomsReducer, { RoomState } from "store/slices/roomsSlice";

export interface IState {
  users: UserState,
  feeds: FeedState,
  filter: FilterState
  restaurants: RestaurantState
  rooms: RoomState
}

const rootReducer = (state: IState, action: AnyAction): CombinedState<IState> => {
  switch (action.type) {
    case HYDRATE: 
      return action.payload
    default: {
      const combinedReducer = combineReducers({
        users: userReducer,
        feeds: feedReducer,
        filter: filterReducer,
        restaurants: restaurantReducer,
        rooms: roomsReducer,
      })
      return combinedReducer(state, action)
    }
  }
}

export default rootReducer

먼저 IState 인터페이스를 만들었다.
각 슬라이스에서 미리 정해둔 initialState 의 타입들을 모두 import 한뒤 한데 모아 IState 라는 인터페이스를 만들었다.
(타입스크립트는 타입 지정하는게 참 힘들다..)

그래서 rootReducer 를 만들어서 첫번째 인자는 state (타입은 IState), 두번째 인자는 action(타입은 AnyAction)이고 CombinedState<IState> 를 반환하는 루트리듀서를 만들었다.

여기서 중요한 부분이 바로 HYDRATE 였다.


( __NEXT_REDUX_WRAPPER_HYDRATE__ 란 이름으로 서버사이드에서 전역상태를 dispatch 한 것을 리덕스 데브툴에서 확인할 수 있었다 )

이 부분의 코드가 다른 블로그에서 참고할 때 기존의 리덕스 코드와 유사해보였다.
그런데 나는 리덕스툴킷을 쓰고 있었고 리덕스 툴킷은 기존의 리덕스와는 조금 다른, 그치만 내 기준 보다 편리한 코드 작성방식이었기 때문에 이 부분의 코드가 예전 리덕스의 코드처럼 보였고 이 부분을 빼먹었었다.
그런데 rootReducer 안의 switch 문 안의 case HYDRATE 가 있는데, 이 HYDRATE 가 next-redux-wrapper 라이브러리에서 꺼내온 코드라는 것이 다시 보았을 때 발견할 수 있었다. (즉, 이 부분을 빼먹었기 때문에 삽질을 한게 아닐까 싶었다.)

나 스스로 묻는다면 넥스트와 리덕스의 코드를 정확하게 이해하고서 사용하고 있는 것은 아니다. 오히려 사용하면서 배워가자는 마음으로 만들면서 공부하고 있다.
그런데 넥스트에서 중요한 개념이 hydration 이다.

hydration: 수분 공급

hydration 은 수분공급이라는 뜻을 가지고 있는데, 이게 Next 에서는 서버사이드에서 프리렌더링된 데이터를 가지고 먼저 HTML 을 그려내는 것을 상징한다. 이로 인해 Next 는 기존의 React 가 SPA 로서 가지고 있는 단점인 '초기 렌더링 속도'와 '검색 엔진 최적화(SEO)' 를 개선해냈다.

때문에 이부분, HYDRATE 라는 부분은, 정확히 이 부분의 코드를 이해하고 있는 것은 아니지만, 리덕스 전역 상태를 서버사이드에서 활용해 화면에 그려냄으로써 화면을 그려준다는 것을 추측해볼 수 있었다.
그렇게 여기선 각 슬라이스에서의 리듀서들을 rootReducer 로 종합해서 export defualt 로 기본 내보내기하고 있다.

3. wrapper 만들기

// configureStore.ts
import { AnyAction, configureStore, Reducer, Store } from '@reduxjs/toolkit'
import { createWrapper } from 'next-redux-wrapper'
import rootReducer, { IState } from 'store/reducer'

const createStore = () => {
  const store = configureStore({
    reducer: rootReducer as Reducer<IState, AnyAction>,
  })
  return store
}
const store = createStore()

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

const wrapper = createWrapper<Store<IState>>(createStore)
export default wrapper

configureStore.ts 로 와서 createStore 함수를 만들었다.
createStore 함수 안에서 configureStore() 로 store 를 만들어서 내보내고 있고, 밑에는 store 를 가지고 RootStateAppDispatch 등의 타입을 만들었다.

그리고 두둥.. next-redux-wrapper 에서 꺼내온 createWrapper() 함수의 인자로 createStore 를 만들어서 wrapper 를 만들고 이를 기본 내보내기 해주었다.

4. _app.tsx 로 가서 wrapper 로 감싸주기

// _app.tsx
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <ThemeProvider theme={themeOptions}>
        <GlobalStyles />
        <AppLayout>
          <Component {...pageProps} />
        </AppLayout>
      </ThemeProvider>
    </>
  )
}

export default wrapper.withRedux(MyApp)

_app.tsx 로 와서 기존에 <Provider> 로 감싸서 store 를 넘겨주던 코드는 지워주고,
App 을 export default 해주는 부분에서 configureStore.ts 에서 가져온 wrapper 를 가지고,
wrapper.withRedux(MyApp) 처럼 감싸서 _app.tsx 를 내보내도록 했다.

5. 서버사이드에서 disptach 하기!!

// RoomInfoHeader.tsx
const RoomInfoHeader: FC = () => {
  const roomData: AccommodationWithId = useAppSelector(state => state.rooms.roomData)
  ...
  
return (
  ...

현재 내 숙소 상세페이지는 한개 숙소 데이터를 전역으로 상태를 보관해서 세분화해서 쪼개놓은 컴포넌트에 뿌리는 것이다.
그러나 기존의 리덕스는 useDispatch, useSelector 훅을 통해서 상태를 보관하고 꺼내 쓰기 때문에 함수형 컴포넌트 내부에서 쓸 수 있는 코드고, 그 말은 서버사이드에서 미리 dispatch 하고 selec 해서 그릴 수 없는 구조였다.

이 부분을 이제 getStaticProps 안에서 dispatch 할 수 있도록 바꾸었다.

// accommodation/[id].tsx
export const getStaticPaths = async () => {
  const paths = await getAllAccommodationsId()
  return {
    paths,
    fallback: false
  }
}

export const getStaticProps = wrapper.getStaticProps((store) => async ({ params }) => {
  const data = await getAccommodationById(params!.id as string)
  store.dispatch(addRoomInfo(data))
  return { props: { data }}
})

각 숙소 상세페이지를 동적으로 라우팅하는 [id].tsx 에서 getStaticPathsgetStaticProps 를 사용하고 있는 부분이다.
getStaticProps 부분에서 wrapper.getStaticProps 를 쓰고 그 안에 콜백을 넣어주었고, 이렇게 params 로 입력받은 각 숙소의 id 를 가지고 찾은 한개 숙소의 data 를 전역상태로 서버사이드에 dispatch 했다.

때문에 dispatch 한 숙소데이터가 전역상태로 서버사이드에서도 보관되게 됐고 이를 화면에서는 꺼내서 그려내면서 서버사이드에서도 전역상태로 관리하는 데이터를 가지고 프리렌더할 수 있게 되었다.

다른 코드들을 참고하면, 이렇게 서버사이드에서 dispatch 하면서 전역상태로 보관했기 때문에 굳이 props 로 data 를 내려줄 필요가 없어 그냥 props: {} 처럼 빈객체를 리턴하는 것을 볼 수 있었다.
그런데 나는 역으로 props 로 빈객체를 그냥 내려보내자 서버사이드에선 전역상태에서 상태를 꺼내 화면을 그리는데 정작 클라이언트사이드에서 화면이 비어있는 사태가 발생했다..?!

때문에 props 로 data 를 내려주고, 클라이언트단에서도 dispatch 해서 전역으로 data 를 보관해서 똑같이 화면을 그려냈다.




이상 내가 next-redux-wrapper 와 씨름하면서 결국 서버사이드에서 전역으로 상태를 보관한 것을 꺼내서 화면에 프리렌더를 성공한 과정이었다.
코드에 대해서 백퍼센트 이해한 것이라고는 볼 수 없지만, 보일러 플레이트처럼 쓸 수 있는 부분이 있어 기록하는 것이 좋겠다고 여겼다

참고한 블로그

https://kir93.tistory.com/entry/NextJS-Redux-Toolkit-next-redux-wrapper-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

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

0개의 댓글