RTK 쿼리는 SSR을 하고 싶어!

서론

요즘 RTK 쿼리와 함께 Nextjs에서 SSR을 적용하는데 꽤 시간을 들이고 있습니다. 물론 리액트 쿼리라는 걸출한 패키지가 존재합니다. 그런데 어쩌겠습니까, 저의 마음 속 한 켠에는 이미 RTK 쿼리에 대한 열망이 불타올랐던 겁니다!

... 사실은 리덕스로 노는 와중에 찾아볼 게 있어서 문서에 들어갔다가 영업당한 것에 가깝습니다. 이 글을 작성할 동기를 제공한 리덕스 공식 문서에게 크나큰 박수를 보내주시면 감사하겠습니다.

그래서 사용하는 김에 SSR까지 적용해보면 어떨까? 라는 생각을 했습니다. 그렇습니다, 여러분. 오늘 글의 주제입니다.

RTK 쿼리는 외로워요

SSR의 원리는 간단합니다. 클라이언트사이드에서 요청을 보내면 서버사이드에서 원래 클라이언트사이드에서 했어야 할 작업들을 대신 수행해 주는 겁니다. 그리고 그 결과가 적용된 말끔한 HTML을 응답해 줍니다.

Nextjs는 이런 작업을 아주 멋지게 처리해 줄 getServerSideProps라는 함수를 제공합니다. 해야 하는 작업을 수행한 후 얻은 데이터를 페이지 컴포넌트의 props로 넣어 주는거죠.

이 쯤에서 문제가 생깁니다. 우리는 props가 아닌 RTK 쿼리를 사용해야 하기 때문입니다. getServerSideProps 함수 안에서 RTK 쿼리를 디스패치하는 부분까지는 가능합니다. 그러나 클라이언트사이드의 RTK 쿼리는 서버사이드에서 무슨 일이 생겼는지 알지 못합니다. 서버사이드의 RTK 쿼리 혼자 외롭게 남아있게 되는 겁니다.

그럼 어떻게 해야 할까요? 어떤 방식으로 서버사이드의 RTK 쿼리 스토어를 클라이언트사이드로 전달해 줄 수 있을까요?

next-redux-wrapper

next-redux-wrapper 패키지가 나설 차례입니다. 이 패키지는 서버사이드의 스토어를 클라이언트사이드 스토어에 덮어 씌워 줍니다. 서버사이드에서 실행된 RTK 쿼리를 클라이언트사이드로 가져오는 게 가능해진다는 의미이기도 합니다.

store.ts

next-redux-wrapper를 적용하기 위해 약간의 수정이 필요합니다.

// store.ts

/** 
 * configureStore를 감싸는 makeStore 함수를 작성
 * @param context `next-redux-wrapper`에서 제공하는 context
*/
export const makeStore = (context: Context) => {     
  return configureStore({
    reducer: {
      something: somethingReducer,
      [rtkQueryApi.reducerPath]: rtkQueryApi.reducer,
    },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(
        rtkQueryApi.middleware,
      ),
  });
};

// makeStore 함수의 ReturnType를 가져와 타입 작성
type AppStore = ReturnType<typeof makeStore>;
export type AppState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

/** 
 * `next-redux-wrapper`에서 제공하는 `createWrapper` 함수로 만들어진 wrapper를 export
*/
export const wrapper = createWrapper<AppStore>(makeStore, {debug: true});
//                                              ^
//                                  작성된 makeStore 함수를 넘김

다음과 같이 리덕스 스토어를 설정합니다. configureStore 함수로 만들어진 store를 바로 쓰는 대신 next-redux-wrapper 패키지에서 제공하는 createWrapper 함수로 wrapper를 만들어 사용합니다.

_app.tsx

// _app.tsx

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Component {...pageProps} />
  );
}

// `store.ts`에서 생성된 wrapper로 MyApp 컴포넌트를 감쌈
export default wrapper.withRedux(MyApp)

react-redux 패키지의 Provider 컴포넌트로 감싸던 기존의 방식 대신 wrapper.withRedux 함수로 MyApp 컴포넌트를 감쌉니다.

좋습니다. next-redux-wrapper를 사용할 준비가 끝났습니다.

SSR

이제 RTK 쿼리를 서버사이드에서 사용해봅시다. 생각보다 정말 간단합니다.

// store를 쓰기 위해 작성한 `wrapper`의 `getServerSideProps` 함수를 사용함
export const getServerSideProps = wrapper.getServerSideProps(
  (store) => async (context) => {
    // RTK 쿼리 디스패치
    store.dispatch(rtkQueryApi.endpoints.getSomething.initiate({}));
    // 실행된 것이 끝날 때까지 대기
    await Promise.all(cropApi.util.getRunningOperationPromises());

    return {
      props: {},
    };
  }
);

다음과 같이 RTK 쿼리를 서버사이드에서 실행할 수 있습니다.

하지만 문제가 생기게 됩니다. 클라이언트사이드의 스토어는 아무 것도 바뀌지 않았습니다. next-reudx-wrapper 패키지를 설정하는 과정에서 문제라도 생겼던 걸까요?

RTK Query 설정

이런, RTK Query의 api를 확인해 보니 한 가지 설정을 빠뜨렸습니다.

export const rtkQueryApi = createApi({
  ...
  // 서버사이드에서 실행된 쿼리를 클라이언트에 hydrate
  extractRehydrationInfo(action, { reducerPath }) {
    if (action.type === HYDRATE) {
      //                ^
      //   next-redux-wrapper에서 제공
   
      // 실행된 쿼리(reducerPath)를 리턴
      return action.payload[reducerPath];
    }
  }
  ...
});

HYDRATE 액션 때 서버사이드에서 실행된 쿼리를 전해주는 extractRehydrationInfo 함수를 설정해줘야 합니다.

혹시 다 설정해줘야 하냐구요? 맞습니다. 서버사이드에서 실행될 예정인 쿼리와 뮤테이션을 다루는 createApi 함수는 모두 같은 설정을 해야 합니다. 안하면 쿼리와 뮤테이션들이 외로워 합니다.

확인해봅시다

이제 제대로 데이터가 전해졌는지 확인해봅시다.

image

redux devtools에 들어가 액션을 살펴보세요. __NEXT_REDUX_WRAPPER_HYDRATE__ 안에 원하는 데이터가 들어 있다면 성공입니다.

사용

const ExampleComponent = () => {
  const { data } = useGetMyCropsQuery({});
  
  return (
    { data ? <div>{data.something}</div> : <div>...</div> }
  )
}

기존과 같이 훅으로 사용합니다.

TMI

왜 Provider 컴포넌트를 사용하지 않나요?

next-redux-wrapper가 Provider 컴포넌트의 역할을 합니다.

도움이 됐나요?

날씨가 추우니 손도 어는 기분이네요. 여러분들은 따뜻한 손으로 개발하고 있으시죠?

그렇다고요? 좋습니다, 오늘도 즐거운 개발 되세요!

References

 

 

댓글을 작성해보세요.

채널톡 아이콘