요리 레시피를 공유하는 커뮤니티를 개발하고 있다.
유저 페이지(user page)는 유저가 작성한 모든 레시피 게시물을 확인할 수 있다.
현재 1%의 사용을 차지는 로그인한 사용자의 유저 페이지를 최신으로 유지하기 위해
해당 페이지를 클라이언트에서 렌더링(CSR)하고 있다.
사진이 많이 있고 많은 데이터가 포함될 수 있는 화면인 만큼
1%의 사용성을 위해 CSR을 하기에는
너무 느리다.
모든 사용자가 같은 화면을 확인할 수 있는 만큼
99%의 경우에 해당하는, 로그인한 유저의 페이지가 아닌 페이지를 미리 그릴 수 없을까?
ISR/SSR을 사용해 미리 마크업을 가져올 수 없을까?
async function List({ username }: { username: string }) {
const { ok, data: recipes } = await getRecipes(username, 'username');
if (!ok) return notFound();
return (
<section>
<div className={style.tabs}>
<span className={style.tab}>
<Icon icon='grid' /> RECIPES
</span>
</div>
<RecipeList recipes={recipes} />
</section>
);
}
'use client';
function RecipeList({ recipes }: Props) {
const userInfo = getUserInfo();
const params = useParams<UserPageParams>();
const [isClient, setIsClient] = useState(false);
const isLoggedInUserPage = params.username === userInfo.username;
useEffect(() => {
setIsClient(true);
}, []);
const { data, error } = useQuery(
recipeListOptions({
query: params.username || '',
type: 'username',
enabled: isLoggedInUserPage,
initialData: recipes,
}),
);
if (!isClient) return null;
if (error) return notFound();
return <Cards recipes={data} />;
}
export default RecipeList;
invalidateQueries
사용)useParams
-> props로 params 받기서버
에서 렌더링 한 것과 클라이언트
에서 렌더링 한 마크업이 다르면 에러가 발생함'use client'
를 사용한 클라이언트 컴포넌트
경우에도 Next.js
는 최대한 서버
에서 렌더링 하기 위해 렌더링을 시도함useEffect
안의 코드는 클라리언트
에서만 실행됨typeof window !== 'undefined'
와 같이 클라리언트에서만 실행될 수 있는 코드는 useEffect
안에 넣어야함'use client';
function RecipeList({ recipes }: Props) {
const params = useParams<UserPageParams>();
const { data, error } = useQuery(
recipeListOptions({
query: params.username || '',
type: 'username',
initialData: recipes,
staleTime: 180000, // 3 minutes
}),
);
if (error) return notFound();
return <Cards recipes={data} />;
}
export default RecipeList;
이 부분은 react-query와 관련 있다.
서버에서 data를 받아와 initialData에 사용하는 방식은
prefetch의 한 방법이다.
하지만, 이 방법은 사용에 따라 문제가 생길 수 있다.
query의 initalData는 한 번 추가되어 캐시되면 다시는 컴포넌트가 리렌더링되어 initial에 새로 추가되어도 변경되지 않는다. 새로 받은 데이터가 더 새로운 데이터여도 말이다. 그래서 페이지를 앞뒤로 움직이거나, 새로 페이지를 받아와도 오래된 데이터가 쓰일 수 있다.
If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point
If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to all queries that needs it can also be cumbersome.
There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead
If there is already data in the cache for a query, initialData will never overwrite this data, even if the new data is fresher than the old one.To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated.
그렇기 때문에
intial에 데이터를 넣는 방식이 아닌 prefetchQuery를 사용해서 dehydrate하는 아래의 방식이 더 추천된다.
queryClient를 새로 만들고 HydrationBoundary를 계속 사용해도 문제가 없다.
tanstack query 공식문서 - advanced ssr
async function List({ username }: { username: string }) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery(
recipeListOptions({
query: username || '',
type: 'username',
}),
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<section>
<div className={style.tabs}>
<span className={style.tab}>
<Icon icon='grid' /> RECIPES
</span>
</div>
<RecipeList />
</section>
</HydrationBoundary>
);
}
'use client';
function RecipeList() {
const params = useParams<UserPageParams>();
const { data, error } = useQuery(
recipeListOptions({
query: params.username || '',
type: 'username',
staleTime: 180000, // 3 minutes
}),
);
if (error) return notFound();
return <Cards recipes={data ?? []} />;
}
export default RecipeList;
위와 같은 유저 페이지의 경우
헤더에 1개, 리스트에 2개의 사진이 있는 것을 확인할 수 있다.
오늘도 이렇게
조금 빠른 사진 요청으로
조금 더 나은
UX를 만들 수 있게 되었다. 🥹
tanstack query 공식문서 - request waterfalls
tanstack query 공식문서 - ssr
tanstack query 공식문서 - advanced ssr
위 3개 문서는
Next.js + React Query 조합을 생각 중이라면 꼭 읽어보면 좋을 문서이다.