React 18에서 가장 관심있는 기능인 Suspense에 대해 알아보려 작성한 포스트이며 2021년 6월에 작성된 dan abramov의 New Suspense SSR Architecture in React 18 를 정리한 글이다.
😇 이해를 돕기위해 변경 된 내용이 있을 수 있으며 이에 문제가 있는 경우는 수정 또는 삭제 하도록 하겠습니다
SSR을 사용하면 서버의 React 컴포넌트에서 HTML을 생성하고 해당 HTML을 사용자에게 전달 할 수 있다.
즉, SSR을 사용하게 된다면 JavaScript 번들이 로드 및 실행되기 전에 사용자가 페이지의 콘텐츠를 빠르게 볼 수 있도록 한다는 말로 이해 할 수 있다.
React의 SSR은 항상 여러 단계로 발생한다.
- 서버에서 전체 앱에 대한 데이터를 가져온다
- 서버에서 HTML을 렌더링하고 클라이언트로 응답한다
- 클라이언트에서 전체 앱에 대한 JavaScript 코드를 로드한다
- 클라이언트에서 JavaScript 로직을 서버에서 응답받은 HTML에 연결한다 → hydration
여기서 핵심적인 부분은 다음 단계가 시작되기 전에 각 단계가 전체 앱에 대해 한 번에 완료되어야 한다는 것이다.
React 18을 사용하면 Suspense 를 사용하여 앱을 독립적으로 나눌 수 있다. 이 단계에서 나머지 앱이 작동을 차단하지 않는 작은 독립 단위로 수행된다. 결과적으로 유저는 콘텐츠를 더 빨리 볼 수 있으며, 훨씬 빠르게 상호작용할 수 있다. 이러한 개선 사항은 특별한 조정 코드를 작성할 필요 없이 자동으로 작동한다.
이것은 React.lazy가 SSR에서 자동으로 작동한다는 것을 의미한다. 데모
유저는 앱을 로드할 때 빨리 상호작용이 가능한 페이지를 보고싶어 할 것이다.
그러나 페이지에 대한 JavaScript코드가 로드되기 이전까지는 페이지에 상호작용을 할 수는 없다 페이지의 로드 시간의 대부분이 애플리케이션의 코드를 다운로드하는데 소비된다.
SSR을 사용하지 않는 경우에는 JavaScript가 로드되기 이전까지 유저가 페이지에서 보게되는 것은 빈 페이지 이다.
이는 분명히 좋지 않은 방법이며 SSR을 사용하는 이유가 된다 SSR을 사용하면 서버의 React 컴포넌트를 HTML로 렌더링하여 유저에게 보낼 수 있다 이렇게 전달 된 HTML은 JavaScript가 로드되기 전까지는 상호작용 할 수 없지만 빠르게 페이지를 확인 할 수 있게 된다
JavaScript가 로드되기 이전에 버튼을 클릭해도 아무런 작업도 수행되지 않지만 콘텐츠가 많은 웹사이트의 경우 SSR은 JavaScript가 로드되는 동안 유저가 미리 콘텐츠를 읽을 수 있기 때문에 유용하다
React와 애플리케이션 코드가 모두 로드되면 상호작용을 할 수 있도록 해당 HTML에 이벤트 핸들러를 연결하는 과정을 거치고 이를 hydration이라고 한다. 이는 상호작용 및 이벤트 핸들러를 물로 건조한 HTML에 물을 주는 것과 같다고 한다.
hydration 이후에는 상태를 설정하고 버튼을 클릭하는 등의 동작이 정상적으로 수행된다.
이는 일종의 SSR의 트릭이라고 할 수 있다. 실제로 앱이 상호작용하게 되는 속도가 빨라지지는 않지만 JavaScript가 로드되는 동안 정적의 콘텐츠를 볼 수 있게 된다. 이 트릭은 네트워크 연결이 좋지 않은 유저에게 많은 차이를 만들어 내며 전반적으로 성능을 향상시킨다. 또한 검색엔진 최적화에 도움이 된다.
위에서 알아본 접근 방식은 효과가 있지만 최적의 방법은 아니다.
오늘날 SSR의 한 가지 문제점은 데이터 대기를 허용하지 않는다는 것이다. API를 사용하면 HTML로 렌더링할 때 서버의 컴포넌트에 대한 모든 데이터가 준비되어 있어야 한다. 즉, HTML을 클라이언트에 보내기 전에 서버의 모든 데이터를 수집해야 한다는 것을 의미한다.
예를 들어 게시글을 렌더링한다고 가정해보면 게시글의 내용을 렌더링 하기 위한 JS 로드가 길어질 경우 게시글 내용 외의 다른 데이터의 렌더링까지 지연이 될 수 있다. 이는 좋지않은 문제점이다.
hydration 이전에 데이터의 로드가 이루어 져야한다.
JavaScript코드가 로드 된 후에 HTML을 hydration 하도록 React에 지시한다. React는 컴포넌트를 렌더링하는 동안 서버에서 생성한 HTML을 이동하고 이벤트 핸들러를 첨부한다. hydration이 작동하려면 React에서 컴포넌트에 의해 생성된 트리가 서버에서 생성된 트리와 일치해야 한다.
hydration은 한번만 실행되며, 결과적으로 모든 컴포넌트와 상호작용하기 위해서는 모든 컴포넌트의 hydration이 완료 되어야 한다.
데이터 가져오기(서버) → HTML로 렌더링(서버) → 코드 로드(클라이언트) → hydration(클라이언트)
SSR은 이전 단계가 완료될 때까지 다음 단계를 시작할 수 없다. 위에서 정리한 문제를 해결하기 위해 화면의 일부에 대해 이러한 각 단계를 수행할 수 있도록 작업을 분리하는 것이다
이를 위해 2018년에 <Suspense> 컴포넌트를 도입했다. 도입할 때 클라이언트에서 lazy-loading 코드에 대해서만 지원했지만 목표는 그것을 SSR과 통합하고 이러한 문제를 해결하는 것이었다.
이러한 문제를 해결하기 위해 React 18에서 <Suspense>를 사용하는 방법을 살펴보도록 하자
React 18의 Suspense에는 두 가지 주요 SSR 기능이 있다.
<Suspense>로 래핑해야 한다.먼저 모든 HTML을 렌더링한다
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
클라이언트는 HTML을 응답받아 모든 코드를 로드한 후에 hydration 한다
그러나 React 18의 <Suspense>는 페이지의 일부를 래핑하고, 해당 래핑한 부분이 준비가 될 때 까지 <Spinner /> 컴포넌트를 표시해야 한다고 React에게 알린다
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Comments를 Suspense로 래핑하여 React에 페이지의 Comment를 기다리는 동안 HTML 스트리밍을 기다릴 필요가 없다고 알리고, 대신 React는 해당 컴포넌트 대신 Spinner 를 표시한다.

HTML에서 Comment의 모습은 볼 수 없고 Spinner가 표시되는 것을 확인 할 수 있다.
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
Comment가 서버에서 준비되면 React는 추가 HTML을 동일한 스트림에 보내고 해당 HTML을 올바른 위치에 배치하기 위한 최소한의 인라인 script 태그를 보낸다.
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
이는 우리의 첫 번째 문제를 해결한다. 이제 렌더링 하기 전에 모든 데이터를 가져올 필요가 없으며 화면의 일부가 초기 HTML을 지연시키는 경우 모든 HTML을 지연하거나 HTML에서 제외할지 선택할 필요가 없다. 나중에 HTML 스트림에서 해당 부분이 “pop in” 되도록 허용할 수 있다.
이제 서버에서 초기 HTML을 더 일찍 보낼 수 있지만 여전히 문제는 존재한다 JavaScript 코드가 로드될 때 까지클라이언트에서 hydration을 시작 할 수 없다. 코드 크기가 크면 시간이 걸릴 수도 있다.
큰 번들을 피하기 위해서 일반적으로 code splitting을 사용한다. React.lazy로 code splitting 사용하여 기본 번들에서 코드를 분할할 수 있다.
import { lazy } from 'react';
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>;
이전에는 SSR에서 작동하지 않았다.
그러나 React 18에서 <Suspense>를 사용하면 Comments가 로드되기 전에 앱을 hydration 할 수 있다.
이것은 선택적 hydration 의 예시다 Comments를 Suspense를 사용함으로써 페이지의 다른 부분이 스트리밍 되는 것을 차단하지 않도록 React에게 알린다. 더 이상 hydration 을 시작하기 위해 모든 코드가 로드될 때까지 기다릴 필요가 없으며 React는 부분적으로 로드되는 곳을 hydration시킬 수 있다.
선택적 hydration 덕분에 JS의 무거운 부분이 페이지에 나머지 부분의 상호 작용을 막지 않게 되었다.
React는 이 모든 것을 자동으로 처리하기 때문에 예상치 못한 순서로 일어나는 일에 대해 걱정할 필요가 없다. 예를 들어 HTML이 스트리밍되는 동안에도 로드하는 데 시간이 걸릴 수 있다. JavaScript 코드가 모든 HTML보다 먼저 로드되면 React는 기다릴 이유가 없다 페이지의 나머지 부분에 hydration을 진행한다.
Susponse로 댓글을 래핑했기 때문에 hydration으로 인해 브라우저가 다른 작업을 수행하는 것을 더 이상 차단하지 않는다.
예를 들어, 댓글이 hydration 되는 동안 사용자가 사이드바를 클릭한다고 가정해 보자.

React 18에서는 Suspense 내부의 요소가 hydration 하는 동안 클릭등의 상호작용이 발생하여 다른 페이지로 이동할 수도 있다.
위의 예에서는 하나의 Suspense로 래핑되므로 페이지의 나머지 부분에 대한 hydration은 한 번에 이루어진다. 그러나 더 많은 곳에서 Suspense를 사용하여 이 문제를 해결할 수 있다.
여러개의 컴포넌트도 래핑 해보도록 해보자
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
둘 다 HTML이 로드되었지만 코드가 아직 로드되지 않았다고 가정해 보자

그러면 SideBar와 Comments에 대한 코드가 포함된 번들이 로드된다. 여러 요소가 Suspense로 래핑되어 있는 경우 React는 트리의 앞부분에 있는 Suspense 경계부터 시작하여 hydration을 시도한다 (이 예에서는 Sidebar)
그러나 이때 유저가 Comment와 상호 작용하기 시작한다면 어떻게 될까?

React는 클릭 이벤트를 기록하고 Comment를 hydration한다
결과적으로 Comment의 클릭을 처리하고 상호 작용에 응답하기 위해 바로 hydration한다. 그 이후에 React가 급히 할 일이 없으므로 React는 sideBar를 hydration한다.
선택적 hydration 덕분에 상호 작용하기 위해 모든 것을 hydration 할 필요가 없다. React는 가능한 한 빨리 모든 것을 hydration하기 시작하고 유저의 상호 작용을 기반으로 화면에서 가장 긴급한 부분의 우선 순위 를 지정하게된다.
새로운 Suspense SSR 아키텍처가 어떻게 작동하는지 확인할 수 있는 데모
React 18은 SSR을 위한 두 가지 주요 기능을 제공한다.
스트리밍 HTML을 사용하면 원하는 만큼 빨리 HTML을 내보낼 수 있으며 추가 콘텐츠에 대한 HTML을 올바른 위치에 배치하는 <script> 태그와 함께 스트리밍할 수 있다.
선택적 hydration을 사용하면 나머지 HTML 및 JavaScript 코드가 완전히 다운 되기 전에 최대한 빨리 앱 hydration을 시작할 수 있다. 또한 유저가 상호 작용하는 부분에 hydration 을 우선적으로 제공 한다.
이러한 기능은 React의 SSR과 관련된 세 가지 오래된 문제를 해결한다.
더 이상 HTML을 보내기 전에 모든 데이터가 서버에 로드될 때까지 기다릴 필요가 없다.
더 이상 hydration을 시작하기 위해 모든 JavaScript가 로드될 때까지 기다릴 필요가 없고 대신 SSR과 함께 code splitting을 사용할 수 있다.
더 이상 모든 컴포넌트가 페이지와 상호 작용하기 시작할 때까지 기다릴 필요가 없고 대신 선택적 hydration에 의존하여 사용자가 상호 작용하는 컴포넌트의 우선 순위를 지정하고 미리 hydration 할 수 있다