
회사에서 운영하고 있는 서비스에 대해 들어온 VOC였다. 회사 서비스 특성상 초기 렌더링 속도가 상당히 중요하기 때문에 계획하고 있던 작업을 잠시 홀드한 후 이 요구사항을 먼저 처리하기로 하였다.
React에서 로딩속도에 영향을 미치는 것은 무엇이 있을까?
실제로 영향을 끼치는 요소가 더 많이 있겠지만 현 이슈에서는 위 두 가지로 파악하고 실제 프로젝트에서의 이슈는 무엇일까 확인해 보니 한 가지, 내부 컴포넌트에 대한 비동기 처리를 놓친 것도 영향을 끼친 것도 하나였다.
아래 구현 방법은 서버 측 개선 방향은 차치하고 클라이언트 쪽에서 구현한 방법만 설명할까 한다.
해당 페이지에서 응답받는 API가 3~5개 정도 되는 중 유독 하나의 API가 응답 속도가 느린 것을 확인할 수 있었다.
보통 10~30ms, 늦어도 50ms를 넘지 않던 응답속도가, 해당 API에서만 최소 120ms, 늦게는 7~800ms까지 늦어지고 있었다.
따라서 해당 API를 두 개로 쪼개어, 렌더링에 필요한 최소한의 데이터만 loader에서 응답받고, 나머지 데이터는 미리 응답받은 데이터를 통해 추가로 조회하는 방식을 선택했다.
react-router-dom에서 제공하는 함수 중에 defer 함수가 있는데, 이는 간단히 말하면 응답이 늦어지는 데이터는 기다리지 않고 나중에 받을 수 있는 기능이다.
loader: async () => {
const foo = await getAtLeastData().then(r => r)
const bar = getMoreData().then(r => r)
return defer({
minialData: foo,
expandData: bar,
})
}
한가지 특이한 점이라면 나중에 받아도 되는 데이터는 await 처리를 하지 않는다는 점이다. await 비동기 처리를 하게 되면 저 데이터 또한 전부 응답받기 전까지 return 데이터를 넘기지 않기 때문에 await 처리를 꼭 제외해 주어야 한다.
2번에서 defer 함수를 활용하였는데, 이를 위해서는 react-router-dom의 Await 컴포넌트, React 18버전의 Suspense 컴포넌트와 함께 사용하여야 한다. (위 defer 공식문서에 함께 포함되어 있는 내용이다.)
import { Suspense } from "react";
import { Await } from "react-router-dom";
<Suspense fallback={<>Loading...</>}>
<Await resolve={data} errorElement={<>데이터를 불러오지 못했습니다.</>}>
{(data) => (
// 나의 경우 받아온 data값이 Array 형식이기 때문에 map 함수를 사용하여 렌더링을 진행함.
data.map(item => <렌더링 진행 할 컴포넌트 data={item} />)
)}
</Await>
</Suspense>
Suspense 컴포넌트의 fallback property는 로딩 중에 표시 할 Element를 설정할 수 있다.
Await 컴포넌트의 resolve property는 렌더링을 하기 전 있어야 하는 데이터를 설정할 수 있고, errorElement property는 데이터를 불러오는 데 실패할 경우 표시할 Element를 설정할 수 있다.
이 부분에서는 다른 분이 잘 설명해주신 글이 있어 공유하고자 한다.
(나의 경우에는 Router에서는 오히려 Router에서 lazy import를 하지 않았고, 위 3번에서 data.map 함수로 렌더링 시킬 컴포넌트들을 lazy import로 불러왔다.)
1번 방법에서 API를 둘로 쪼개어 진행했다고 했는데, 그 두번째 API를 loader 함수가 아닌 컴포넌트가 렌더링 될 때 useEffect로 호출하여 데이터를 받아오는 방식을 선택했다.
useEffect(() => {
(async() => {
try {
//추가 데이터 받아오기...
} catch(error) {
}
})()
}, [])
위 방법처럼 useEffect 안에서 비동기 즉시 실행 함수로 API호출을 통해 추가 데이터를 가져올 수 있다.
** 물론 추가 데이터를 가져오기 전까지의 로딩 컴포넌트는 따로 분기처리 혹은 다른 방식을 통해 구현해주어야 한다.
이번에 공부하면서 알게 된 내용인데,
react-router-dom에서 제공하는 Provider인 RouterProvider 컴포넌트에는 fallbackElement 값을 설정할 수가 있다.
이 값을 설정하게 되면, 보통 React 프로젝트에서의 router.tsx 파일에서는 수많은 페이지 컴포넌트들을 import하고 있는데, 이 과정에서 흰 화면을 보여주지 않고, fallbackElement 값에 설정한 컴포넌트를 보여줌으로서 적어도 서비스가 준비되고 있으니 조금만 기다려 달라는 조금의 눈속임 정도는 가능하다.
<RouterProvider router={router} fallbackElement={<>로딩 중입니다...</>} />
아직 경과는 더 지켜봐야 하겠지만 평균 7~8초 걸리던 초기 렌더링 속도가 평균 3~4초 정도로 개선되었음을 확인할 수 있었다.
해당 서비스 대부분의 트래픽이 인스타그램 웹뷰에서 발생하고 있는데, 인스타그램 측에서 웹뷰를 띄울 때 Linkshim이라는 기술을 사용해 페이지를 1차 검증을 진행한 후 redirect를 시키고 있기에 거기서 발생하는 복불복은 어쩔 수 없이 감안하고 이후 렌더링 속도에 중점을 두어 작업을 진행했다.