TanStackQuery (15) - waterfall
React Query를 사용하거나 실제로 컴포넌트 내부에서 데이터를 패치할 수 있는 모든 데이터 패칭 라이브러리를 사용할 때
가장 큰 성능 저해 요인은 요청 워터풀이다.
What is 요청 워터풀
요청 폭포 현상은 리소스(코드, CSS, 이미지, 데이터)에 대한 요청이 다른 리소스에 대한 요청이 완료된 후에야 시작되는 경우이다.
웹 페이지를 생각해보았을 때, CSS,. JS를 로드하기 전에 브라우저는 먼저 마크업을 진행해야 한다.
1. |-> Markup
2. |-> CSS
2. |-> JS
2. |-> Image
JS 파일 내부에서 CSS를 가져오면 두배의 워터풀이 생긴다
1. |-> Markup
2. |-> JS
3. |-> CSS
해당 CSS가 배경 이미지를 사용하는 경우 트리플 워터풀이 발생
1. |-> Markup
2. |-> JS
3. |-> CSS
4. |-> Image
각 워터풀은 리소스가 로컬에 캐시되지 않는 한 서버에 대한 최소 한 번의 왕복을 나타낸다
우리는 최적의 방법인 왕복이 2번뿐인 첫번째 예로 평탄화할 수 있다면 최악보다 절반의 시간대로 로드할 수 있다
요청 워터풀 및 React 쿼리에서 발생할 수 있는 패턴과 피하는 방법
1. 단일 구성 요소 워터풀/직렬 쿼리
단일 구성요소가 먼저 한 쿼리를 패치한 다음 다른 쿼리를 패치하는 경우의 waterfall이다.
이는 두번째 쿼리가 종속 쿼리(이전 쿼리가 완료되어야만 사용가능)인 경우 발생할 수 있다.
필연적이게도 패치할 때 첫번째 쿼리의 데이터에 따라 달라진다
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
const {
status,
fetchStatus,
data: projects,
} = useQUery({
queryKey: ['projects', userId],
queryFn: getProjectsByuser,
enabled: !!userId, // !!연산이 어떤 역할인지 기억하시죠?
})
최적의 성능을 위해서는 API를 재구성하여 두 가지를 모두 단일 쿼리로 패치할 수 있도록 하는 것이 좋다
예를 들어서 getProjectsByuser를 사용할 수 있도록 getUserByEmail을 먼저 패치하는 대신 새로운 getProjectsByUserEmail 쿼리를 도입하는 것이다.
// 예를 들면 이렇게 작성 ..
async function getProjectsByUserEmail(email) {
const userResponse = await fetch(`/api/users?email=${email}`);
const user = await userResponse.json();
const projectsResponse = await fetch(`/api/projects?userId=${user.id}`);
const projects = await projectsResponse.json();
return { user, projects };
}
직렬 쿼리의 또다른 에로 useSuspenseQuery hook 사용
function App () {
const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchusers })
const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects })
일반적인 useQuery를 사용하면 병렬로 작업이 수행되는데,
컴포넌트에 여러 개의 대기형 쿼리가 있는 경우 항상 useSuspenseQuery를 사용하면 직렬로 사용할 수 있다.
2. 중첩된 구성 요소 폭포
중첩된 컴포넌트 waterfall은 부모와 자식 컴포넌트에 모두 쿼리가 포함되어 있고 부모가 쿼리가 완료될 때까지 자식을 렌더링하지 않는 경우이다.
이는 useQuery와 useSuspenseQuery 모두에서 발생할 수 있다.
종속 중첩 구성 요소 waterfall : 자식이 부모의 데이터에 따라 조건부로 렌더링하거나 자식이 쿼리를 수행하기 위해 부모로부터 prop으로 전달된 결과의 일부를 사용하는 경우
자녀가 부모에게 의존하지 않는 경우
function Article({ id }){
const { data: articleData, isPending } = useQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending){
return "Loading article.."
}
return(
<>
<ArticleHeader articleData = {articleData} />
<ArticleBody articleData = {articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentById,
})
...
}
<Comments>가 부모 요소로부터 prop으로 id 값을 가져오지만, 해당 id는 <Article>이 랜더링될 떄 이미 사용 가능하므로 Comments에서 반드시 사용할 수 있다.
Waterfall을 평평하게 만드는 방법은 Comments 쿼리를 부모로 호이스트 하는 것이다.
function Article({ id }) {
const { data: articleData, isPending: articlePending } = useQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
const { data: commentsData, isPending: commentsPending } = useQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsByid,
})
if (articlePending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData = {articleData} />
// ..
{commentsPending ? (
'Loaidng comments'
) : (
<Comments commentsData = {commnetsData} />
)}
</>
)
}
이때 2개의 data에 대한 isPending에 대한 처리를 왜 나눠서할까를 생각해보았는데,
화면에 로드되는 페이지는 맨위에 기사, 맨 아래에 댓글이기 때문이라고 결론지었다.
따라서 동시에 보여줄 필요가 없는 것
이제 두 쿼리가 병렬로 패치된다, 만약 suspense를 사용하는 경우(직렬),
이 두 쿼리를 단일 useSuspenseQueries로 결합해야 한다.
3. 주의사항 !
실수로 애플리케이션에 waterfall를 도입하는 경우를 생각해보고 이를 방지해보자
- 부모가 이미 쿼리를 가지고 있는 사실을 모르고 자식에서 동일한 쿼리를 추가
- 부모에게 쿼리를 추가하는데 자식에게 이미 쿼리가 있다는 사실을 모름
- 쿼리가 있는 하위 항목이 있는 구성 요소를 쿼리가 잇는 상위 항목이 있는 새 부모로 이동
*권장하는 방법은 네트워크 탭을 확인하는 것이다.