TanStackQuery 공식 홈페이지 따라하기

TanStackQuery (15) - waterfall

vitamin3000 2025. 2. 2. 15:49

 

 

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를 도입하는 경우를 생각해보고 이를 방지해보자

  • 부모가 이미 쿼리를 가지고 있는 사실을 모르고 자식에서 동일한 쿼리를 추가
  • 부모에게 쿼리를 추가하는데 자식에게 이미 쿼리가 있다는 사실을 모름
  • 쿼리가 있는 하위 항목이 있는 구성 요소를 쿼리가 잇는 상위 항목이 있는 새 부모로 이동

*권장하는 방법은 네트워크 탭을 확인하는 것이다.