react query 의 성능과 waterfall
react query는 캐싱 측면에서 불필요한 API 요청을 줄여주기 때문에 많이 사용할수록 좋을 것 같습니다. 그러나 waterfall에 대해서 인지하고 있지 않으면 성능적으로 더 불리해질 수 있습니다.
waterfall은 무엇일까요?
네트워크에서 데이터 요청이 순차적으로 일어나면서 발생하는 성능 이슈를 의미합니다. 불필요한 순차적 요청으로 인해서 네트워크 요청이 지연되면 그만큼 느린 속도로 인해 성능이 저하될수도밖에 없습니다.
그래서 보통 이러한 waterfall 이슈를 없애기 위해서 병렬처리, prefetch, caching 등의 방법으로 해결합니다.
react query를 클라이언트에서 사용할 때 발생할 수 있는 waterfall 케이스에 대해서 살펴보면
1. Single component / useSuspenseQuery
아래는 아주 흔히 접할 수 있는 코드인데요. 2개의 API가 서로 의존성을 가지고 있는 상태에서는 결국 2개의 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,
})
그리고 Suspense를 활용하기 위해 useSuspenseQuery를 사용하게 되면 순차적으로 실행되면서 3개의 waterfall이 형성됩니다. 아래 코드는 useSuspenseQueries로 간단하게 병렬처리가 가능합니다.
function App () {
// The following queries will execute in serial, causing separate roundtrips to the server:
const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects })
// Note that since the queries above suspend rendering, no data
// gets rendered until all of the queries finished
...
}
// 병렬 처리
const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['teams'], queryFn: fetchTeams },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
})
2. 중첩 컴포넌트 waterfall
부모 컴포넌트인 Article에서 query 실행이 끝난 후 자식 컴포넌트인 Comments가 실행되기 때문에 2개의 waterfall이 생성됩니다. 두 개의 query는 의존성이 없기 때문에 하나의 컴포넌트 레벨에서 호출하여 병렬적으로 처리할 수 있습니다.
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,
})
...
}
// 병렬 처리
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} />
)}
</>
)
}
3. code splitting으로 인한 waterfall
초기 번들 사이즈를 줄이기 위해 code splitting을 하게 되면 결국 해당 컴포넌트 번들을 다시 요청하고, API 요청도 일어나면서 5번개의 waterfall이 형성됩니다. 초기 번들에 모든 코드를 넣을 것인지? 아니면 초기 번들 사이즈를 줄이고 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,
})
...
}
1. |> Markup
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
평소 react query를 사용할 때 성능적인 이슈를 전혀 고려하지 않았느데요,, 해당 document를 읽으면서 좀 더 의식적으로 waterfall을 고려해야겠다는 생각이 듭니다. 2개의 API를 조합해서 사용해야하고, 의존성을 가지고 있다면 백엔드와 의논하여 1개의 API로 합치는 방법을 논의하는것도 좋아보입니다.
Reference
'Frontend > React' 카테고리의 다른 글
Next.js SSR accesstoken 개선(Localstorage에서 cookie로) (1) | 2024.11.09 |
---|---|
[React] 함수형 컴포넌트에서 API 불러오기 (0) | 2021.08.16 |
[React] axios(내용 추가하기) (0) | 2021.08.16 |
[React] map 응용2(portfolio) (0) | 2021.08.16 |
[React] map 응용하기 (0) | 2021.08.16 |