Progressive Hydration
소개
서버에서 현재 탐색한 페이지에 대한 HTML이 만들어진다. 서버가 정적 UI표현에 꼭 필요한 CSS나 JSON데이터 를 포함한 HTML컨텐츠를 만들고 나면. 즉시 클라이언트에 전송된다. 서버에서 마크업을 이미 만들었기 때문에 클라이언트는 내용을 파싱하고 화면에 빠르게 출력한다. 즉시 꽤 빠른 FCP(First Contentful Paint) 시간이 측정되었다.
하지만 FCP가 빠르다고 하여 TTI가 항상 빠른것은 아니다. 웹 사이트의 요소들과 인터렉션을 위한 자바스크립트가 아직 로드되지 않았다. 버튼들은 인터렉션이 가능해 보이긴 하지만 아직 동작하지 않는다. 클릭 이벤트 핸들러는 자바스크립트 번들이 로드되고 처리된 이후에 바인딩된다. 이 과정을 hydration이라 한다. React는 현재의 DOM노드들을 확인하고 각 노드들을 대응하는 자바스크립트에 hydration한다.
사용자가 화면에 보이는 UI들과 인터렉션할 수 없을때까지의 시간을 “불쾌한 골짜기”(컴포넌트는 완전히 보여지는데 핸들러가 아직 바인딩되지 않아 인터렉션을 할 수 없는 순간) 이라 한다. 사용자는 웹사이트와 인터렉션이 가능할 것이라고 생각하지만. 컴포넌트 핸들러는 아직 바인딩되지 않았다. 이는 사용자로 하여금 UI가 굳은 것 처럼 느껴지게 할 수 있다.
서버로부터 받은 DOM 컴포넌트들이 완전히 hydration될 때까지는 시간이 걸린다. 컴포넌트가 hydrate가능해지기 전 까지. 자바스크립트 파일들은 로드되고 처리되며 실행된다. 이 때 모든 앱이 한번에 hydrate되도록 하는 대신, 이전에 했던것과 비슷하게 이 과정도 DOM노드마다 점진적으로 진행할 수 있다. 점진적 hydration은 위에서 언급했던 시간 동안에 개별 node별로 hydrate를 할 수 있도록 하여 최소 필요한 자바스크립트만 요청하여 처리할 수 있도록 해 준다.
앱을 점진적으로 hydration하여 페이지 내에서 덜 중요한 부분의 hydration을 지연시킬 수 있다. 이런 방식으로 페이지를 인터렉션 가능하게 만들기 위해 받아야 하는 자바스크립트의 크기를 줄일 수 있고 또 사용자에게 필요로 하는 노드만 hydrate할 수 있다. 점진적 hydration은 SSR의 rehydration과정에서 서버가 렌더링 한 DOM트리가 제거되고 다시 만들어질 때 발생할 수 있는 문제를 겪지 않게 해 줄 수 있다.
점진적 hydration은 특정 조건에 만족하는 컴포넌트에 대해서만 hydrate할 수 있도록 해 준다. 예를들어 뷰포트에 보여지는 컴포넌트들이 있다. 아래 예제에서는 사용자의 목록을 렌더링하고 있는데 뷰포트에 들어온 컴포넌트에 대해서만 hydrate하고 있다. 컴포넌트가 보라색으로 반짝이면 hydrate 되었다는 것이다.
이 과정이 빠르게 일어나는것과 별개로 UI는 UI대로 초기 진입때부터 렌더링 되는것을 확인할 수 있다. 초기에 받는 HTML이 동일한 정보들과 스타일을 포함하고 있기 때문에 어떠한 깜빡임과 움직임 없이 컴포넌트를 끊김없이 인터렉티브하게 만들 수 있다. 점진적 hydration은 조건부로 특정 컴포넌트를 인터렉트 가능하도록 만들 수 있고 이 과정이 진행중인것을 사용자는 알아차리기 어려울 것이다.
점진적 hydration 구현하기
React를 사용하여 SSR을 구현하는 섹션에서 서버에서 렌더링 된 앱에 대한 클라이언트 측 hydration에 대해서 이야기 했었다. Hydration은 클라이언트 측 React가 서버에서 렌더링 된 ReactDOM 컴포넌트를 인지하고 이벤트 핸들러를 바인딩할 수 있게 해 준다. 따라서 SSR앱에 클라이언트에서 활성화된 CSR앱과 같이 연속성과 끊김없이 부드러운 사용성을 제공한다.
페이지 내의 모든 컴포넌트들이 hydration을 통해 인터렉트가 가능하도록 하기 위해 이런 동작을 하는 React의 코드들이 번들에 포함되에 클라이언트에 다운로드 된다. 앱의 모든 부분이 자바스크립트에 의해 제어되어야 하여 인터렉트가 매우 중요한 SPA는 전체 번들이 한번에 필요하겠지만, 대부분의 인터렉트 가능한 요소가 적은 정적 웹 사이트들은 모든 컴포넌트가 즉시 인터렉트 가능해질 필요가 없다. 이러한 사이트들에 각 컴포넌트를 위해 크기가 큰 React번들을 전송하는것은 오버헤드가 될 수 있다.
점진적 hydration은 페이지가 로드 될 때 앱의 일정 부분에 대해서만 hydrate를 가능하게 하여 이런 문제를 해결한다. 다른 부분들은 필요에 따라 점진적으로 hydration된다.
위 그림에서 “You may also like” 와 “Other content”는 나중에 필요에 따라 hydrate될 수 있다.
전체 앱이 한번에 초기화되는 대신, hydration과정은 DOM트리의 루트부터 시작되지만 서버 렌더링 앱의 개별적인 부분들은 일정 기간동안 활성되된다. Hydration 과정은 다양한 지점에서 정지될 수 있고 각 부분들이 뷰포트에 들어오는 등의 조건을 만족할 때 재시작 될 수 있다. 주목할 점은 각 hydration을 위한 리소스의 로드 역시 코드 스플리팅에 의해 지연될 수 있다는 점이다. 따라서 페이지를 인터렉트 가능하게 만드는 데 필요한 자바크스립트의 양도 줄어든다.
점진적 hydration의 기본 개념은 개발한 앱의 코드를 분할하고 개별적으로 활성화하여 좋은 성능을 이끌어내는 것이다. 모든 점진적 hydration 솔루션은 그것을 적용했을 때 사용자 경험에 어떤 영향을 줄 수 있는지를 고려해야 한다. 화면의 각 부분에 대한 코드들이 화면에 보여지는 순서를 조절할 순 없지만, 이미 로드된 코드 부분이 활성화 되는 것이나, 사용자의 입력을 막게 될 수는 있다. 따라서 전체적인 점진적 hydration에 대한 요구사항은 아래와 같다.
- 모든 컴포넌트에 대해 SSR을 사용한다.
- 코드 스플리팅을 지원해 개별 컴포넌트 혹은 부분별로 코드를 나눌 수 있어야 한다.
- 분할된 코드들에 대한 hydration순서를 개발자가 정의할 수 있어야 한다.
- 이미 hydrate된 코드들에 대하여 사용자의 입력을 막으면 안된다.
- Hydration이 지연된 분할코드에 대하여 로딩 스피너등을 사용할 수 있어야 한다.
React의 동시성 모드를 사용하면 위 요구사항을 모두 충족시킬 수 있다. 동시성 모드는 React가 여러 작업을 동시에 진행할수 있게 해 주고 주어진 우선순위에 따라 해당 작업들을 전환할 수 있게 해 준다. 교체할 때 부분적으로 렌더링 된 트리는 커밋(VDOM을 실제 DOM에 반영하는 동작)할 필요가 없기 때문에 React가 동일한 작업으로 다시 전환되었을 때 렌더링 작업을 이어 진행할 수 있다.
동시성 모드는 점진적 hydration을 구현하는데 사용할 수 있다. 이 경우 페이지의 분할된 코드들을 hydration하는 것은 React동시성 모드에서 나눠진 각 작업에 대응한다. 사용자의 입력에 반응해야 하는 우선순위가 높은 작업이 들어올 경우 React는 hydration 작업을 일시적으로 멈추고 사용자 입력을 받아들이는 작업으로 전한다. lazy()와 Suspense()는 로딩 상태를 변수로 제공한다. 해당 변수를 사용해 분리된 코드들이 로드중일 때 스피너를 화면에 보여줄 수 있다. SuspenseList()
는 컴포넌트들의 지연로딩 순서를 정의할 때 사용할 수 있다. Dan Abramov가 공유한 데모에서는 동시성 모드의 동작과 점진적 hydration을 구현하고 있다.
React의 동시성 모드는 React의 다른 기능과도 함께 사용할 수 있다.
- 서버 컴포넌트는 전체 다운로드가 끝나는 것을 기다리지 않고, 상태 변경에 따라 리-렌더링 된 컴포넌트를 서버로부터 다시 받을 수 있으며 이 것을 클라이언트에 스트리밍하는것처럼 렌더링할 수 있다. 따라서 네트워크 요청이 진행중일 때에도 클라이언트의 CPU를 활용할 수 있다.
React의 동시성 모드를 기반으로 한 점진적 hydration은 아직 개발 단계에 있지만. 다른 오픈소스들을 통해서 사용할 수 있다. 점진적 hydration은 Google I/O ‘19 에서 Hydrator컴포넌트를 통해 페이지 내에서 각 부분을 부분적으로 hydration처리하는 [점진적 hydration데모](https://github.com/GoogleChromeLabs/progressive-rendering-frameworks-samples/tree/master/react-progressive-hydration)와 함께 공개되었다. 각기 다른 클라이언트 사이드 프레임웍에서 이를 지원하기 시작했다. 이 기능은 Vue, Angular, Next.js에서도 사용할 수 있다.
Preact와 Next.js를 사용하여 이를 구현한 예제를 살펴보자.
이 예제는 아래 라이브러리를 사용하여 부분 hydration을 구현하고 있다.
pool-attendant-preact
: Preact x 를 활용하려 부분 hydration을 구현한 라이브러리next-super-performance
: 클라이언트 측 성능을 위해 사용하는 Next.js 플러그인
pool-attendant-preact
라이브러리가 제공하는 withHydration
을 이용하여 먼저 hydration되길 원하는 특정 컴포넌트들을 따로 처리할 수 있다. 아래 코드처럼 사용할 수 있다.
import Teaser from './teaser'
import { withHydration } from 'next-super-performance'
const HydratedTeaser = withHydration(Teaser)
export default function Body() {
return (
<main>
<Teaser column={1} />
<HydratedTeaser column={2} />
<HydratedTeaser column={3} />
<Teaser column={1} />
<Teaser column={2} />
<Teaser column={3} />
<Teaser column={1} />
<Teaser column={2} />
<Teaser column={3} />
</main>
)
}
2, 3번째로 렌더링 되는 HydratedTeaser
가 제일 먼저 hydrate된다. 나머지 컴포넌트들은 클라이언트 측에서 동일한 라이브러리에서 제공되 hydrate()
를 통해 hydrate할 수 있다.
import { hydrate } from 'next-super-performance'
import Teaser from './components/teaser'
hydrate([Teaser])
아래 HydrationData
컴포넌트는 클라이언트를 에서 사용할 직렬화 된 props를 작성하기 위해 사용된다. hydrate될 컴포넌트들에게 필요한 데이터를 전달해 준다.
import Header from '../components/header'
import Main from '../components/main'
import { HydrationData } from 'next-super-performance'
export default function Home() {
return (
<section>
<Header />
<Main />
<HydrationData />
</section>
)
}
점진적 hydration - 장단점
점진적 hydration은 hydration자체의 비용을 최소화하며 서버 사이드 렌더링과 클라이언트 사이드 hydration을 제공한다. 이로 인해 아래 정리된 이점을 얻는다.
- 코드 스플리팅됨: 개별 컴포넌트들에 대하여 코드가 분리되어 지연 로딩되야 하기 때문에 코드 스플리팅은 점진적 hydration에 필수적인 기능이다.
- 자주 사용되지 않는 부분을 지연로딩 시킬 수 있다: 정적이거나 뷰포트를 벗어나 있거나 자주 사용되지 않는 컴포넌트들이 있을 수 있다. 이런 코드들은 지연로딩의 대상이 된다. 이런 컴포넌트를 hydration하는 코드 역시 페이지 로드 시점에 받을 필요가 없다. 대신 어떤 조건이 충족될 때 hydrate되도록 한다.
- 번들 사이즈를 감소시킨다: 코드 스플리팅으로 인해 번들 사이즈가 감소한다. 페이지를 로드할 때 실행되어야 하는 코드량이 줄어들어 FCP와 TTI간의 시간차가 줄어든다.
단점으로는 점진적 hydration은 페이지 로드와 함께 페이지 내의 모든 요소들이 인터렉션 가능해야 하는 동적인 앱에는 적합하지 않다. 사용자가 어떤 부분에 인터렉션을 먼저 할 지 알 수 없어 hydration순서를 따로 정의할 수 없기 때문이다.