단순하게 서버에게서 데이터를 요청하고 요청 받는 것은 React Query로 간단하게 구현할 수 있다.
이제 변화가 필요하다.
단순하게 서버에서 데이터를 받는것 뿐 아니라 네트워크 성능, 렌더링 부분도 신경을 써야겠다는 생각이 들었다.
Request Waterfalls
요청 폭포수, 앞선 요청이 완료되어야지 다음 요청을 진행할 수 있는 것을 말한다.
| -> Markup
| -> JS
| -> CSS
// ...
다음과 같이 React 같은 코드에서는 Markup ( HTML 로드 )가 되고 JS 로드를 하고 CSS 같은 부분들이 로드가
이루어 진다.
해당 플로우 자체는 HTML 파일에서 JS 파일을 불러와야지 전체적인 코드가 실행되고 그 과정에서 root div에서 렌더링이
발생하면서 CSS 처리가 되니깐 자연스러운 일이다.
하지만 필요한 요청이 아닌 불필요한 요청들이 플로우에 있다면 부정적인 영향이 발생할 것이다.
Tanstack Query를 사용할 때는 Markup을 진행하고 자바스크립트가 로드가 되고 Query 요청을 보낸다.
이 과정에서 부정적인 Request Waterfall이 발생할 수 있다.
- Single Component Waterfalls
- useSuspense
- Nested Component Waterfalls
- Code Split
Single Component Waterfalls
하나의 Query가 끝나야지 다음 Query를 실행할 수 있던지, Query가 다른 Query에 의존되는 경우에 발생하는
Request Waterfall이다.
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
getUserByEmail 요청이 완료되야지 getProjectsByUser 요청을 보낼 수 있다.
해당 쿼리를 합쳐서 getProjectsByUserEmail 요청으로 만든다면 의존적이지 않고 Request Waterfall을 해소할 수 있다.
useSuspenseQuery
Suspense와 함께 사용하기 위해서 사용하는 것이 useSuspenseQuery다.
호출하면 데이터 요청을 하고 대기 시간동안 Suspense 컴포넌트의 fallback이 렌더링이 된다.
하지만 하나의 컴포넌트에서 2개 이상의 useSuspenseQuery를 호출하면 문제가 발생한다.
네트워크 탭에서 회색 선이 2개가 보이는데, Tanstack Query는 데이터를 병렬로 요청해서 받는다.
하지만 useSuspenseQuery를 사용하면 직렬로 데이터를 요청하기 때문에 Request Waterfall이 발생한다.
useSuspenseQueries를 사용하면 원하던 병렬로 요청이 가능해진다.
Nested Component Waterfalls
부모와 자식 컴포넌트 사이에서 발생하는 문제이다. 부모와 자식 컴포넌트 모두 query를 가지고 있고 부모 컴포넌트의
쿼리를 완료하기 전까지 자식 컴포넌트를 렌더링하지 않는 경우를 말한다.
자식 컴포넌트가 부모 컴포넌트의 데이터에 조건부로 렌더링이 되거나, 자식 컴포넌트가 부모 컴포넌트에게서부터
전달받은 일부 props에 따라 의존적으로 실행되는 경우 Reqest Waterfall이 발생한다.
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: getArticleCommentsById,
})
...
}
Comments 컴포넌트는 부모 컴포넌트로부터 전달받은 id를 사용하지만 Article 컴포넌트가 렌더링될 때 해당 id를
사용할 수 있으므로 기사와 댓글을 동시에 가지고 오지 못할 이유가 없다.
실제 개발은 데이터를 사용하는 자식 컴포넌트가 실제 부모보다 훨씬 아래 중첩될 수 있고 이런 종류의
Request Waterfall은 수정하기 어렵겠지만, 하나의 방법으로 요청을 부모로 올리는 것이 될 수 있다.
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} />
<ArticleBody articleData={articleData} />
{commentsPending ? (
'Loading comments...'
) : (
<Comments commentsData={commentsData} />
)}
</>
)
}
두개의 요청이 병렬로 실행이 된다. 만약 Suspense를 사용한다면 개별로 useSuspenseQuery를 사용할 수 없기 때문에
useSuspenseQueries를 대신 사용하면 해결이 될 수 있다.
또 다른 방법으로는 Aticle 컴포넌트에서 댓글을 Prefetching하거나 페이지 로드 또는 탐색 시 라우터에서 요청들을
모두 Prefetching하는 방법이 될 수 있다.
function Feed() {
const { data, isPending } = useQuery({
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery({
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
getGraphDataById는 두가지 경우로 부모 컴포넌트에 종속이 되어 있다.
첫 번째로는 feedItem의 type이 graph가 아니라면 렌더링이 되지 않는다. 두 번째로는 부모 컴포넌트의 id가 필요하다.
1. |> getFeed()
2. |> getGraphDataById()
이 경우, Query를 부모 컴포넌트에 올려주거나, Prefetching을 하는 방법으로는 Request Waterfall을 해결할 수 없다.
첫 번째 방법으로 getFeed 요청에 GraphData를 포함시키도록 리팩토링하는 방법이 있을 것이다.
다른 방법으로는 Server Components ( Next.js )를 활용해서 Request Waterfall을 지연 시간이 짧은 서버로
이동시킬 수 있지만 이 경우에는 큰 작업이 될 수 있다.
Code Splitting
애플리케이션의 JS 코드를 더 작은 덩어리로 분할하고 필요한 부분만 로드하는 것은 성능 향상을 위해서
중요한 단계이다. 하지만 Request Waterfall이 자주 발생할 수 있다는 것이 단점이다. 분할된 코드에 요청이 있다면
더더욱 악화될 수 있다.
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = React.lazy(() => import('./GraphFeedItem'))
function Feed() {
const { data, isPending } = useQuery({
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery({
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
다음과 같은 예제는 두개의 Request Waterfall이 발생한다 :
1. |> getFeed()
2. |> JS for <GraphFeedItem>
3. |> getGraphDataById()
하지만 이것은 예제 코드 한정으로 발생하는 부분이고, 첫 번째 페이지 로딩을 생각한다면 5번의 요청이 필요하다.
1. |> Markup
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
이 경우 getGraphDataById 요청을 Feed 컴포넌트로 올리고 조건부로 만들거나 조건부 Prefetching을 추가하면
병렬로 가져와서 도움이 될 수 있다.
'React > 실험실' 카테고리의 다른 글
디바운싱 검색 (1) | 2024.10.19 |
---|---|
CSS를 컴포넌트에 중복 호출하면 안되는 EU (1) | 2024.08.17 |
Table 컴포넌트 (0) | 2024.07.31 |
useFunnel 만들기 (2) | 2024.06.16 |
[React] Controlled and UnControlled Input (0) | 2024.03.14 |