일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- TypeScript
- react-query&Next.js
- 사내 오류 해결
- javascript
- 결제페이지
- react-query
- React
- 일상 생각
- react-query v5
- 기술 낙서장
- router instance
- SSR
- 사내 오류 대응
- 사내 이슈
- ClientSide
- react-query 도입후기
- nextjs
- state 관리
- no router instance found
- state 사용하기
- Next.js
- React.JS
- 캡스톤디자인 후기
- SW캡스톤디자인
- node.js
- 기술낙서장
- 더블엔씨
- JS변수
- 리액트
- MSW
- Today
- Total
코딩을 잘하고 싶은 코린이 동토니
React Query와 Next.js 본문
React-Query와 Next.js를 연결해보자
최근 회사에서 Next의 SSR을 극대화 시키는 방법으로 React-query의 initial data기능을 사용하기 시작했다.
우선 react-query의 initial data에 대해 먼저 알아보면
특정 쿼리가 client에서 데이터를 요청하기 전에 처음부터 가지고 있을 데이터를 미리 설정해두고 뿌려주는 기능이다
먼저 React-query의 공식문서 예시를 한번 살펴보자
function Todos() {
const result = useQuery(['todos'], () => fetch('/todos'), {
initialData: initialTodos,
})
}
생김새는 기존의 useQuery와 똑같고 단지 마지막에 initialData라는 옵션에 initialTodos를 넣어주고 있다.
해당 코드를 보며 생각을 해볼수 있는건
'아 result라는 변수에 useQuery로 인해 넘어오는 데이터를 저장하고있고, useQuery에서는 ['todos']라는 queryKey에 fetch('/todos')를 통해 넘어오는 데이터가 관리되고 있구나, 그리고 initialData로는 initialTodos라는 무언가가 들어있구나!'
라고 생각을 해볼 수있다.
그러면 해당 쿼리는 어떻게 동작을 할까?
결론부터 말하자면 :
1. initialTodos라는 어떠한 데이터가 ['todos']라는 키값을 통해 관리되는 메모리에 데이터가 저장이 되고
2. 해당 키값으로 관리되는 데이터는 우선적으로 result에 저장이 되어 사용자가 최초로 화면에 진입했을때 initialTodos 데이터를 마주하게 된다.
해당 옵션을 사용하게 되면 유저는 처음에 페이지에 들어왔을때 initial data를 통한 초기 데이터를 더 빠르게 받아 볼 수 있다.
가장 기본적인 방법은 위와 같고 해당 방법을 조금 더 변형시키는 방법은? 좀 더 유저입장에서 좋은 경험을 누리도록 해줄 수 있는 방법은 뭐가 있을까?
우선 initialData의 특징에 대해서 다시 한번 살펴보면 initialData에 들어온 데이터는 사용자가 최초로 받아보게될 데이터이다.
그러면 이 옵션과 Next.js의 SSR, SSG, ISR 등 Next.js에서 제공하는 서버사이드 함수들과 사용하면 어떻게될까?
대충 상상해보면 이런 결과를 예측해볼 수 있다.
1. 각 페이지 마다 유저가 받아볼 데이터들을 serverSide로 미리 불러와둔다.
2. 유저가 페이지에 진입했을때 pre-render된 데이터들을 useQuery의 initialData로 집어넣어 별도의 로딩페이지 없이 데이터가 채워진 화면을 보게된다.
이런 형태라면 SSR의 장점인 유저가 나누어진 각각의 페이지에 접근할 때 큰 불편함 없이 접근 할 수 있게 된다.
그러면 한번 react-query 공식문서에서 제시하는 Next.js와 SSR을 react-query와의 결합에 대해서 살펴보자.
1. initial Data
이 형식은 위에서 말한 형식과 같다.
serverSide 함수를 통해 미리 데이터를 불러오고 해당 데이터 값을 pageProps로 받아 미리 useQuery initialData에 집어넣는다
그럼 한번 예시를 살펴보자
1. 초기세팅
//_app.tsx
import { QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>☕☕☕</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
</Head>
...
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
...
</ErrorBoundary>
</>
)
}
export default MyApp
_app.tsx 파일안에 App 함수 바깥에 QueryClient Instance를 생성해준다.
그리고 QueryClientProvider를 통해 queryClient를 넣어주고 컴포넌트를 감싸주면 초기세팅은 끝난다.
**주의 QueryClient를 App 컴포넌트 안에 선언하게 되면 각각의 페이지들은 다른 페이지들과 queryData를 공유할 수 없게된다 이 부분에 대해서는 밑에서 다뤄보자.
2. ServerSide 함수와 InitialData
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery(['posts'], getPosts, { initialData: props.posts })
// ...
}
getPosts라는 Post리스트를 fetch해주는 함수를 getStaticProps를 통해 먼저 불러와준다. 그리고 해당 post리스트들을 props로 return해준다.
/pages/Posts.tsx 해당 파일에서는 props로 setStaticProps를 통해 넘어온 post리스트 데이터를 받고 useQuery안에 initialData로 넣어둔다.
이렇게 하면 해당 페이지는
1. getStaticProps를 통해 빌드타임에 fetch해온 데이터를 가진 상태로 만들어진다.
2. props를 통해 넘어온 이 데이터는 useQuery의 ['posts']라는 키값과 매핑된 메모리에 initialData로(초기 데이터)저장된다.
3. 유저는 useQuery를 통해 넘어온 data를 통해 화면을 보게 된다.
4. 이미 data는 initialData로 빌드타임에 fetch된 데이터를 가지고 있어 유저는 아무런 영향 없이 화면을 바로 마주하게 된다.
initialData의 옵션과 Next.js에서 제공하는 SSR 함수를 적절하게 섞어서 사용하면 유저는 좋은 경험을 하며 페이지를 볼 수 있게 되는것이다.
이번에는 InitialData말고 react-query에서 제시하는 또다른 방법을 살펴보자
2. Hydration
Hydration이라는 용어는 Next.js를 사용해보신 분들이라면 이미 어느정도 알고있는 개념일테니 따로 설명하지는 않겠다. (나중에 Next.js에 대해서 한번 정리해서 포스팅 할 예정입니다)
react-query에서는 해당 방법을 'React Query supports prefetching multiple queries on the server in Next.js and then dehydrating those queries to the queryClient. This means the server can prerender markup that is immediately available on page load and as soon as JS is available, React Query can upgrade or hydrate those queries with the full functionality of the library. This includes refetching those queries on the client if they have become stale since the time they were rendered on the server.' 이라고 소개하고 있다. 해석해보자면
1. react-query는 다수의 query들을 Next.js에서 pre-fetching할 수 있다. 그리고 queryClient를 통해 dehydrating할 수 있다.
2. 서버는 페이지 로드시 즉시 사용할 수 있는 마크업을 미리 렌더링 할 수 있고, JS를 사용이 가능해지는 즉시 해당 query들을 update하거나 hydrate시킬 수 있다.
3. 해당 query들은 사용자가 stale한 데이터를 가지고 있다면 server에서 rendered됬던 데이터라도 refetch하게 된다.
굉장히 nice한 방법인 것 같다.
그러면 한번 초기 세팅을 살펴보자
1. 초기세팅
//_app.tsx
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
initialData와는 뭔가가 많이 다르다. 한번 천천히 살펴보자.
1. 우선 queryClient가 App컴포넌트 외부가 아닌 내부에 선언되었다. 왜일까?
이는 하나의 queryClient가 각각의 하나의 컴포넌트를 lifeCycle로 가지고 data가 외부의 요소로 인해 변경되는 것을 막기위해서이다.
2. QueryClientProvider 이외에 Hydrate라는 무언가가 또 wrapping하고 있다.
<Hydrate>안에 state로 dehydratedState를 넣어줌으로써 무언가를 dehydrate 시켜 내려준다는 것을 알 수있다.
2. ServerSide 함수와 page
// pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query';
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(['posts'], getPosts)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Posts() {
const { data } = useQuery(['posts'], getPosts)
const { data: otherData } = useQuery(['posts-2'], getPosts)
// ...
}
이또한 위의 initialData 형식과 많이 다른 형태인 것을 볼수 있다.
1. getStaticProps 함수에서 QueryClient 클래스를 통해 queryClient인스턴스를 생성한다.
2. prefetchQuery를 통해 prefetch할 데이터를 호출한다.
3. 해당 queryClient를 deHydrate시켜 props로 전달한다.
그러면 서버사이드로 만들어진 Posts 페이지는 어떻게 동작할까?
가장 먼저 dehydrate된 QueryClient를 log를 찍어보면 다음과 같이 나온다
{
mutations: [],
queries: [
{ state: [Object], queryKey: [Array], queryHash: '["post"]' },
]
}
queryClient가 가지고 있는 query와 mutation정보에 대해 알려주고 있다. 그 말은 해당 queryClient가 이미 데이터를 가지고 있는 상태인 것이다.
그러면 prefetch된 데이터에 접근하기 위해서 Posts페이지는 props로 전달되는 데이터를 받는것이 아닌 react-query Key를 통해 해당 데이터에 접근하면된다.
function Posts() {
const { data } = useQuery(['posts'], getPosts)
const { data: otherData } = useQuery(['posts-2'], getPosts)
// ...
}
위에 코드에서 ['posts']로 매핑된 데이터는 이미 prefetch된 데이터를 불러왔고
['posts-2']로 매핑된 데이터는 prefetch된 데이터가 아닌 완전히 새로운 데이터를 호출하게 된다.
그럼 여기서 궁금증이 하나가 생길 수 있다.
'그러면 ['posts']에 매핑된 데이터는 useQuery를 통해 server에서 한번 Client에서 한번 총 2번 호출되는게 아닌가?'
결론부터 말하자면 맞다. 실제로 해당 데이터를 서버에서 한번, client에서 refetch 총 2번을 호출한다.
하지만 다시 생각해보면 이미 prefetch된 query Key를 통해서 해당 useQuery는 전역 변수처럼 어느 depth에서나 사용이 가능하고 사용자가 이미 prefetch된 데이터를 단순 query Key만을 통해서 더 빠르게 화면을 볼 수 있다는 점에서 단점이 상쇄된다고 생각한다.
하지만 이 조차 불필요하다고 판단되면 useQuery에 refetchOnMount 옵션을 false로 설정해주거나 staleTime을 infinity로 설정해주고 사용하면 된다.
위에 예시에서는 SSG를 통해 Server Side를 알아봤지만 ISR방식이나 SSR이 좀 더 유용하게 react-query와 next.js를 잘 혼합해서 사용하는게 아닐까라는 생각이 든다.
'그러면 initialData가 좋나요 hydrate 방식이 좋나요?'
우선 react-query에서는 hydrate방식을 권장하고 있다. queryClient를 통해서 유저별 인스턴스를 따로 생성해 데이터를 관리 할 수있고, initialData의 경우 pages에서만 props를 통해 데이터를 전달받고 더 depth가 깊은곳에 해당 데이터를 전달하려면 props로만 계속해서 내려주어야 하기 때문이다.
하지만 hydrate방식을 쓰면 query Key를 통해서 해당 데이터를 depth가 어느곳이든 전역변수처럼 데이터를 불러다 사용할 수 있게 되므로 더 깔끔하게 사용할 수 있게된다.
'Web > Next.js' 카테고리의 다른 글
Next.js Router Instance에 대한 고찰 (1) | 2024.09.23 |
---|---|
Next.js app router에 MSW 적용하기 (1) | 2024.09.03 |
ServerSideRendering vs ClientSideRendering (0) | 2022.09.18 |