Next.js 의 SSR 에서 redux 사용하기 (next-redux-wrapper)

배경

Next.js 에서 SSR 서버사이드렌더링로 특정 페이지를 제대로 렌더링해서 클라이언트로 보내주고 싶었다.
따라서 아래 2가지를 구현해야 했다

  • 서버사이드에서 액션을 dispatch 디스패치해서 데이터를 fetch하는 작업을 실행하기
  • 새로워진 (데이터를 품은) state (store) 를 이용해서 유의미한 콘텐츠를 서버사이드에서 렌더링하기

해결 과정

next-redux-wrapper 로 getServerSideProps 를 감쌀 wrapper 만들기

getServerSideProps 에서 store 이용하기

데이터 fetch 가 완료될 때까지 기다리는 함수를 개인적으로 추가했다

type Options = {
  actionType: DataSagaActionType
  key: string
}

export const waitDuringLoading = async (store: Store<RootState, AnyAction>, {actionType, key}: Options) => {
  while (true){
    await (async () => new Promise(resolve => setTimeout(resolve, 100)))()

    const isLoading = store.getState().data[actionType][key].status === DataSagaStatus.LOADING
    if (!isLoading) break;
  }
} 
import {waitDuringLoading} from // ...

// ...
export const getServerSideProps = wrapper.getServerSideProps(store => async ({req, res, ...etc}) => {
  const GET_PUBLIC_TASKS_KEY = ""
  const GET_GOALS_BY_IDS_KEY = ""

  // 액션 디스패치 하기
  store.dispatch(dataActionCreators[DataActionType.GET_PUBLIC_TASKS]({
    author: undefined,
    key: GET_PUBLIC_TASKS_KEY,
    startTime: new Date("1999-11-11"),
    endTime: new Date("2222-11-11"),
  }))

  // 데이터 fetch 완료될때까지 기다리기
  await waitDuringLoading(store, {actionType: DataActionType.GET_PUBLIC_TASKS, key: GET_PUBLIC_TASKS_KEY})

  // state 에서 값 읽기
  const tasksGoal = store.getState().data[DataActionType.GET_PUBLIC_TASKS][GET_PUBLIC_TASKS_KEY].data?.map(item => item.goal)

// ...
  return ({
    props: {}
  })
});

클라이언트 state 와 서버 state 알맞게 합치기

next-redux-wrapper 에서 작업해주는 HYDRATE 라는 액션을 이용해서

  • getStaticPropsgetServerSideProps 가 있는 페이지로 처음 접속 또는 이동할 때,
  • 이전 페이지에서의 클라이언트 측 state 와 서버 측에서 새로 만든 state 를 이용해서 최종 state 를 만든다
    • 이때 merge 를 하는 방식이 생각보다 중요하다
    • 나는 여러 state 를 묶어서 combinedReducer로 root state 를 만들어서 이용하는데, 여기서 일부 state (아래에서 navigation)는 client 측에서만 다루고 (즉 server 측에서는 이용하지 않고), 일부 state (아래에서 data)는 매번 deepmerge 로 서로 합치는 방식을 택했다
    • deepmerge에서 배열을 합치는 방식도 정해야 한다. 나는 객체의 id 값을 이용해서 같은 객체로 취급할 것인지 정했다
import merge, {Options as MergeOptions} from "deepmerge"
import {HYDRATE} from "next-redux-wrapper";
import {combineReducers} from "redux";
import {dataReducer, State as DataState, initialState as dataInitialState} from "./data";
import {navigationReducer, State as NavigationState, initialState as navigationInitialState} from "./navigation";

const combinedReducer = combineReducers({
  data: dataReducer,
  navigation: navigationReducer,
});

export type RootState = {
  data: DataState;
  navigation: NavigationState;
}

const initialRootState: RootState = {
  data: dataInitialState,
  navigation: navigationInitialState
}

const arrayMerge: MergeOptions["arrayMerge"] = (previousArray, incomingArray, options) => {
  const resultArray: typeof previousArray = [...previousArray]

  incomingArray.forEach((incomingItem) => {
    const prevItemIndex = previousArray.findIndex(previousItem => previousItem.id === incomingItem.id)
    if (prevItemIndex !== -1){
      resultArray[prevItemIndex] = merge(resultArray[prevItemIndex], incomingItem, options)
    }
    else {
      resultArray.push(incomingItem)
    }
  })
  return resultArray
}

const rootReducer = (previousClientState = initialRootState, action: any) => {
  if (action.type === HYDRATE) {
    const incomingServerState = action.payload as RootState

    const nextState: RootState = {
      navigation: previousClientState.navigation,
      data: merge(previousClientState.data, incomingServerState.data, {arrayMerge})
    }
    return nextState;
  } else {
    return combinedReducer(previousClientState, action);
  }
};

export default rootReducer;

렌더링이 잘 되서 클라이언트로 전달되는지 확인하기

  • chrome network 탭에서 해당 요청 (페이지 path 이름) response 복사하기

좋은 웹페이지 즐겨찾기