React <Suspense>

권지현·2023년 5월 28일
0

요즘 브라우저 렌더링 방식에 대해서 개념을 다시 잡고있다. 오늘은 react에서 어떻게 SSR을 구현하는 지 찾아보면서 알게된 <Suspense>에 대해서 먼저 정리하려고한다.

SSR에서 사용되는 Suspense는 적용해보면서 다시 언급하겠지만 간단히 말하면, 서버사이드렌더링은 TTV의 시간을 줄일 수 있게 되지만 대신 JS를 받아오기까지(TTI) 시간이 걸리기 때문에 TTI 시간 소요에 대한 보완으로 Suspense를 사용해 사용자 경험을 개선할 수 있다고 한다.

결국에 이 suspense라는 컴포넌트는 어떤 비동기 처리를 할 때 그 응답을 기다리는 동안 fallback UI를 보여주고 자식 컨포넌트는 비동기 처리가 끝난 후 실제로 보여줄 부분만 작성하면 되는 것이다. suspense를 사용하면 비동기 처리 상태를 조금 더 “선언적(Declarative)”으로 표현할 수 있다.

Suspense를 정리해보면서 생각해보니 내가 작성한 코드는 데이터를 받아오고 처리하는 부분이 하나의 컴포넌트안에서(명령적) 다 실행되고 있었다. 그래서 이 부분에 바로 Suspence를 적용해보았다.

1. 적용

현재 포인트를 조회하는 페이지에서 작성하는 방식이 이렇다면,

export default function IndividualPointHistory(){
  const { data, isLoading } = 
        useQuery(['포인트 조회 query key'],
                 포인트 조회 함수
                );
  
  if(isLoading) return <데이터 로딩 컨포넌트 />
    
	return (
    	<>{data}</>
    )
}

<Suspense/> 사용했을 때는,

function IndividualPointHistory(){
  const { data } = 
        useQuery(['포인트 조회 query key'],
                 포인트 조회 함수,
                 { suspense: true } 
				//react-query suspense 옵션을 true로,
				// 전역에서 설정도 가능하고 이렇게 쿼리마다 직접 설정하는 방법도 있다.
       	);
    
	return (
    	<>{data}</>
    )
}

export default function SuspenceHistory() {

  return (
    <Suspense fallback={<데이터 로딩 컨포넌트 />}>
      <IndividualPointHistory />
    </Suspense>
  );
}

이런 식으로 자식 컨포넌트에서는 비동기 처리 후에 실제로 보여줄 부분에 대한 코드만 작성하고 비동기 처리동안 보여줄 UI는 Suspense fallback 함수에 작성하면 된다.

2. 오류와 해결

무한 요청 문제
그런데 suspense를 적용했더니 쿼리가 무한으로 요청되었다.
조금 황당한 이유라 허무했지만 처음에는 자식 컨포넌트 안에 전역 context가 있어서 그 부분이 영향을 주었나 싶어 지워보거나 다른 컨포넌트로 옮겨보고했다.

7a4f4c24c/image.png)하지만 여전히 무한대로 요청되어 devtools로 확인해보니 역시나 내가 key값 중 하나로 설정한 date가 계속 변하면서 새로 값을 요청하는 것이 문제였다.

waterfall 문제
date를 고정 값으로 지정해주니 무한대로 요청되던 문제는 해결되었다. 하지만 무한대로 요청되던 쿼리를 보면서 쿼리가 동기적으로 작동하고 있었다. 여기서는 두 가지 요청을 보내야하는데 그럼 사용자는 모든 요청이 끝날때까지 로딩페이지를 봐야한다. 데이터가 빠르게 응답이 오면 체감할 수 없겠지만 어떤 경우에는 몇 초이상 걸릴수도 있기 때문에 이 부분도 해결해보기로 했다.
1) useQueries

 const [{ data, refetch }, { data: history }] = useQueries({
    queries: [
      {
        queryKey:['포인트 조회 query key'],
        queryFn: 포인트 조회 함수,
		{ suspense: true } 
      },
      {
        queryKey: ['포인트 내역 조회 query key'],
        queryFn: 포인트 내역 조회 함수,
        suspense: true,
      },
    ],
  });

이 방법은 waterfall 현상은 해결되었지만 suspense가 useQueries를 인식하지 못했다.
useQueries v4.5에서는 useQueries도 suspense를 인식할 수 있게 되었다고 한다. 하지만 지금 프로젝트는 그보다 낮은 버전이기 때문에 다른 대체 방식을 찾아야했다.
2) 컨포넌트 하나 당 하나의 API
이 방식으로 진행하니 waterfall현상도 해결되고 모든 API의 요청이 끝난 후 화면이 보여졌다.

3. 결론

아마 suspense는 컨포넌트 1개에 하나의 비동기 요청만 처리하는 방식이었던 것 같다. 그래서 병렬로 되어있는 useQueries를 인식하지 못했던 것 같고, 4.5 이후 버전은 useQueries를 하나의 요청으로 인식할 수 있도록 수정된 것으로 보인다.
컨포넌트안에서 요청을 하나씩 처리하다보니 한 요청이 끝나야 다음 요청이 가도록 처리되어있다보니 waterfall 현상이 일어나게 된 것이다.

하지만 A,B 자식컨포넌트에 API를 작성하는 것이 아니라 B컨포넌트에 API 요청, A의 자식 컨포넌트에서 API요청을 보낸다면 결국 suspense특성상 가까운 컨포넌트의 요청을 먼저 인식하고 처리되거나 추후에 유지보수를 하거나 에러를 처리하는 상황에 혼란이 있을 수 있다고 본다. 이 부분은 적용해보고 확실히 해봐야하겠지만 4.5이후 useQueries가 적용된 버전을 사용하는 게 좋지않을까 생각해본다.

2023.09.12

react-query github을 확인하고 suspence를 뱉어주는 과정에서 promise를 반환하는 걸 확인. 추후 오픈 소스를 더 까보고 다시 정리할 필요가 있다고 판단.

[참고]
https://react.dev/reference/react/Suspense
https://blog.mathpresso.com/conceptual-model-of-react-suspense-a7454273f82e
https://happysisyphe.tistory.com/54

profile
FE 개발자 성장 기록 👩🏻‍💻

0개의 댓글