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
함수는 모두 같은 설정을 해야 합니다. 안하면 쿼리와 뮤테이션들이 외로워 합니다.
확인해봅시다
이제 제대로 데이터가 전해졌는지 확인해봅시다.
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
[createApi | Redux Toolkit # extractRehydrationInfo](https://redux-toolkit.js.org/rtk-query/api/createApi#extractrehydrationinfo)
[Server Side Rendering | Redux Toolkit](https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering)
[GitHub - kirill-konshin/next-redux-wrapper: Redux wrapper for Next.js](https://github.com/kirill-konshin/next-redux-wrapper)
댓글을 작성해보세요.