현제 SSR의 문제점?
API를 사용하면 HTML 로 렌더링할 때 서버의 컴포넌트에 대해 모든 데이터가 준비 되어있어야 한다.
이 말은, HTML을 클라이언트에게 보내기 전에 서버의 모든 데이터를 수집해야 한다는 것이다.
그렇다면, 게시물을 렌더링한다고 했을 때 게시물의 데이터가 큰 경우 서버에서 생성하는 HTML의 생성이
게시물의 데이터를 받아와 렌더링하여 클라이언트에 전송하기까지 지연될 수 있다.
이로 인하여 게시물 외 페이지의 다른 영역의 렌더링 또한 지연되게 된다.
또한, Javascript 코드가 로드된 후 HTML을 hydration하고 상호작용할 수 있도록 React에 전달하는데, React는 컴포넌트를 렌더링하는 동안 서버에서 생성한 HTML에 이벤트 핸들러를 첨부한다.
이때 React에서 생성된 트리가 서버에서 생성된 서버에서 생성된 트리와 일치해야 정상적인 hydration이
이루어지지만, 게시물 Javascript 코드가 큰 경우 게시물 코드가 로드되지 전까지 다른 영역의
hydration 또한 지연될 것이다.
문제 해결은 React.lazy와 Suspense 로 해결하자
( 다음의 내용은 다음 링크를 참고하였다. 다른 링크도 있다. )
기존의 React는 SSR에서 4단계를 거쳐 동작했다.
- 서버에서 전체 앱의 데이터를 받는다. ( 데이터 fetching )
- 서버에서 전체 앱을 HTML로 렌더링 후 Response로 전송
- 클라이언트에서 전체 앱의 JS코드를 로드
- 클라이언트에서 서버에서 생성된 전체 앱의 HTML과 JS 로직을 연결 ( Hydration )
이때 앱 전체 대상으로 각 단계가 완료되어야만 다음 단계로 넘어갈 수 있다.
만약 전체 컴포넌트 트리 중 일부가 느리다면, SSR의 전체 성능은 낮아지게 되며
그 느린 부분에서 병목 현상이 발생한다.
1. 서버에서 전체 앱의 데이터를 받는다. ( Data Fetching )
싱글 앱 애플리케이션(SPA)에서는 JS가 로딩되는 동안에는 빈 화면만 나오고 로딩이 끝나야 화면이 나온다.
서버에서 애플리케이션을 미리 HTML로 렌더링하여 Response로 내려준다면,
이용자가 더 빠르게 애플리케이션 화면을 볼 수 있다.
자바스크립트 코드가 로딩되기 전이기 때문에 곧바로 상호 작용을 할 수는 없지만......
2. 서버에서 전체 앱을 HTML로 렌더링한 후 Response로 전송한다.
현제 회색 영역은 상호 작용이 불가능한 HTML 영역이다.
HTML은 받아왔지만 JS는 로딩이 안된 상태 이기에 React 애플리케이션으로 구현된
상호작용은 이용할 수 없다.
Input과 같은 기본적으로 웹에 내장된 상호작용만 이용할 수 있다.
3. 클라이언트에서 전체 앱의 JS 코드를 로드한다.
4. 클라이언트에서 서버에서 생성된 전체 앱의 HTML과 JS 로직을 연결 ( Hydration )
녹색 영역은 Hydration 까지 끝나서 상호작용이 가능한 영역이다.
이 Hydration이 끝나면 리액트 애플리케이션의 초기화가 완료된다.
지금 이 방식에는 문제점이 있다.
- HTML을 Response로 내려주는데 지연이 발생하면 Data Fetching이 오래 걸리고
이용자는 빈 화면에 노출되는 시간은 길어진다. - 특정 컴포넌트의 코드량이 크면 로딩이 길어지고 Hydration 단계로 넘어갈 수 없다.
- 특정 컴포넌트의 로직이 복잡하여 렌더링 하는 시간이 길어지면 앞의 두가지 문제점이
같이 일어날 것이다.
React 18
React 18 부터는 <Suspense>와 연계하여 사용할 수 있는 두가지 메이저 SSR 기능이 추가된다.
- HTML 스트리밍 : 서버 단에서, renderToString 대신 새로운 pipeToNodeWritable API를 사용하여
HTML을 스트리밍 할 수 있게 되었다.
기존에도 스트리밍을 통해 서버 사이드 렌더링을 하는 renderToNodeStream API가 있었으나,
Data Fetching을 기다릴 수 없는 반쪽짜리였습니다. 이제 이 API는 Deprecated 된다
- 선택적 (Selective) Hydration : 앱에서 렌더링 비용이 많이 드는 서브 컴포넌트 트리를
<Suspense>로 감싸서, 전체 앱의 Hydration을 방해하지 않고 별도의 Hydration을 진행할 수 있다.
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
위의 코드를 예시로 삼아보자
<Suspense>의 영역을 제외하고는 즉시 스트리밍되며 레더링 된다.
기존의 SSR의 장점중 하나인 사용자에게 빈 화면을 최대한 적게 보여주는 목적을 극대화할 수 있다.
이 화면은 <Comments> 컴포넌트의 Data Fetching이 끝난 후
서버에서 렌더링이 완료되면 대체된다.
이제 Data Fetching이 오래 걸리면 생기는 문제점을 해결했다.
나머지 문제를 해결하려면 React.lazy를 보자
React.lazy는 동적 import 를 사용하여 컴포넌트를 렌더링할 수 있게 해주는 함수이다.
// lazy 컴포넌트
const Component = React.lazy(() => import('./Component'));
이러한 컴포넌트를 lazy 컴포넌트라 하는데, 반드시 <Suspense> 컴포넌트 하위에 렌더링 되어야만 한다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
React 18부터는 새로운 렌더링 API인 pipeToNodeWritable 덕분에,
<Suspense>와 함께 lazy 컴포넌트를 사용할 수 있게 되었다.
현재 리액트 생태계의 주류 환경인 웹팩 기반의 애플리케이션에서,
lazy 컴포넌트를 사용하면 코드 스플리팅(Code Splitting)이 적용되어
별도의 자바스크립트 Chunk 파일로 분리됩니다.
그리고 이 <Suspense> 컴포넌트 하위 트리의 렌더링 외부 트리의 렌더링 과정을
막지 않고 별도의 과정이 진행됩니다.
또한, React 18에서 Suspense 하위의 Hydration은 브라우저가 이벤트를
처리할 수 있도록 짧은 갭과 함께 진행됩니다.
덕분에 클릭은 즉시 처리되고 성능이 낮은 기기에서도 브라우저가 멈춘 것처럼 보이지 않게 된다.
거기다 2개 이상의 lazy 컴포넌트를 SSR 할 수 있다.
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
<Sidebar>컴포넌트와 <Comments> 컴포넌트는 나머지 일반 영역이
먼저 HTML로 렌더링되며 클라이언트로 스트리밍된 이후, 별도의 스트리밍이 시작됩니다.
<Sidebar>컴포넌트와 <Comments> 컴포넌트의 Chunk 코드가 로딩된다.
코드 로딩이 완료되면 React는 두 컴포넌트 모두 Hydration을 시도하고,
컴포넌트 트리에서 더 먼저 발견된 Suspense 영역부터 Hydration을 시작한다.
즉, 여기서는 <Sidebar> 컴포넌트가 먼저 Hydration이 진행된다.
사용자가 코멘트 영역을 클릭한다고 가정해 보면
리액트는 이 클릭을 기록한다.
그리고 <Comments> 컴포넌트에게 Hydration 우선순위를 높인다.
왜냐하면, 사용자가 이 컴포넌트와 상호작용을 하고자 했으므로 더 긴급해졌기 때문이다.
현재 Hydration이 진행 중이었던 <SideBar> 대신 <Comments> 컴포넌트를 먼저 Hydration 하게 된다.
<Comments> 영역의 Hydration이 완료되면, 리액트는 기록했던 클릭 이벤트를 다시 실행하여
이 컴포넌트에게 상호 작용에 반응하게끔 합니다.
예를 들어, 이용자가 Hydration 이전에 “코멘트 자세히 보기” 버튼을 클릭했었다면
Hydration이 끝난 후 “코멘트 자세히 보기" 버튼에 연결된 이벤트 핸들러를 실행하는 식입니다.
이제 리액트가 긴급하게 Hydration 해야 할 <Suspense> 영역이 없다면, 리액트는 <Sidebar>를 마저 Hydration 할 것입니다.
'React' 카테고리의 다른 글
React) 21장 백엔드 프로그래밍: nodemon 사용하기 (0) | 2022.07.07 |
---|---|
React) 21장 백엔드 프로그래밍: Koa 1 (0) | 2022.07.06 |
React) 20장 서버 사이드 렌더링 (3) SSR과 코드 스플리팅 (0) | 2022.06.02 |
React) 20장 서버 사이드 렌더링 (2) 데이터 로딩 (0) | 2022.06.02 |
React) 20장 서버 사이드 렌더링 (1) SSR 구현하기 (0) | 2022.05.31 |