React의 Hydration에 대하여

huurray·2022년 2월 10일
42
post-thumbnail

들어가기

React v18에서는 새로운 Suspense SSR Architecture가 추가될 예정이고 이제 React의 Suspense를 융합해 선택적인 Hydratation을 웹사이트에 적용할 수 있게 된다. Hydratation은 이미 서버 사이드 렌더링을 제공하는 많은 프레임워크나 라이브러리(Gatsby, Next.js 등)에서 필수적으로 제공하는 솔루션이다. 왜 이 기술을 사람들이 사용하려 하는지 다시 생각해 보고 그 동작 원리를 한번 정리하는 시간을 가져 보려 한다.

Hydration

In web development, hydration or rehydration is a technique in which client-side JavaScript converts a static HTML web page, delivered either through static hosting or server-side rendering, into a dynamic web page by attaching event handlers to the HTML elements.

웹페이지를 랜더링하는 과정에서 React와 같은 Client Side Redering(CSR) 라이브러리는 번들된 js를 가져와 DOM을 랜더링한다. 이러한 방식은 많은 장점과 함께 초기 랜더링의 속도 저하, SEO의 한계 등과 같은 문제점 또한 가지고 있다. 그래서 상황에 따라 Server Side Rendering(SSR)을 선택하곤 하는데, SSR을 사용하면 언급한 CSR의 문제점을 해결할 수 있기 때문이다. SSR과 CSR 중 무엇이 더 좋은 방식이냐에 대한 오랜 논쟁이 있지만 어떤 프로젝트이고 무엇을 중점에 두느냐에 따라 누가 장단점이 있어 왔다.

그래서 개발자들은 Server Side 단에서 먼저 정적 페이지를 렌더링하고 JS파일들도 번들링한 후에 둘다 Client Side로 보내주는 생각을 해냈다. 하지만 그 DOM에는 동적인 이벤트가 하나도 없는 메마를 상태일 것이다. 그래서 이 메마른 뼈대에 수분을 보충해서, 즉 HTML 코드와 JS 코드를 서로 매칭시켜 동적인 웹사이트를 브라우저에 랜더링하는 기술이 등장했는데 이게 바로 Hydratation이다. 그래서 Hydration을 한글로 직역하면 수분 보충이라고 말 할 수 있다.

Next.js는 React에서 v16부터 제공하는 hydrate 기능을 사용해서 이러한 솔루션을 성공적으로 제공했다. hydrate는 ReactDOM의 함수인데 흔히 리액트 프로젝트 구축 시 초반에 꼭 작성해주는 render 함수와 잠깐 비교를 해보자.

ReactDOM.render(element, container, [callback]);

ReactDOM.render() 함수는 특정 컴포넌트를 두 번째 파라미터인 지정된 DOM 요소에 하위로 주입하여 렌더링을 처리해주는 함수이다. 그리고 렌더링이 완료되면 특정 이벤트를 처리할 콜백 함수를 세 번째 파라미터로 넣어줄 수 있다.

ReactDOM.hydrate(element, container, [callback]);

ReactDOM.hydrate() 함수는 특정 컴포넌트를 두 번째 파라미터인 지정된 DOM 요소에 하위로 hydrate 처리만 한다. 이는 렌더링을 통해 새로운 웹 페이지를 구성할 DOM을 생성하는 것이 아니라, 기존 DOM Tree에서 해당되는 DOM 요소를 찾아 정해진 자바스크립트 속성(이벤트 리스너 등)들만 부착시키겠다는 말이다.

HTML Streaming과 Selective Hydration

이제 서버 렌더링과 마찬가지로 빠른 First Contentful Paint를 구현한 다음 클라이언트에서 hydration을 사용하여 매칭할 수 있는 아키텍쳐를 새로운 솔루션으로 사용할 수 있게되었다. 하지만 잘못 설계하면 기존의 폭포수 방식의 렌더링에 비해 성능에 상당한 단점이 있을 수도 있다.

그 단점은 First Paint를 개선하더라도 Time To Interactive에는 부정적인 영향을 미칠 수 있다는 것이다. SSR의 페이지는 종종 엄청나게 빨리 로드되고 인터렉션 가능한 것처럼 보이지만 실제로 클라이언트 측 JS가 실행되고 이벤트 핸들러가 첨부 될 때까지 입력에 응답할 수 없을 수 있다. 또한 이 응답할 수 없는 시간은 모바일에서는 몇 초, 심지어 몇 분이 걸릴 수 있다. 그래서 페이지 로드는 되었지만 일정 시간동안 클릭하거나 터치해도 반응이 없는 경우 고객은 빠르게 실망으로 변한다.(이탈률도 굉장히 높아질 것이다.)

그래서 이것을 해결하기위해서 지난 몇 년 동안 여러 가지 발전이 있었다. 앞서 말했듯 React v18에서의 주요 변경점 중 하나가 HTML Streaming과 점진적인 Hydration이다. 이 방법을 사용하면 서버 렌더링 애플리케이션의 개별 부분이 전체 응용 프로그램을 한꺼번에 초기화하는 현재의 일반적인 방식보다는 시간이 지남에 따라 부팅되는 효과를 볼 수 있다. 이러한 효과는 페이지의 우선 순위가 낮은 부분의 클라이언트 측 업그레이드를 지연하여 필요한 JavaScript의 양을 줄이는 방법을 통해 수행된다.

구체적으로 말해보면,

서버 쪽에서는 기존 renderToString을 사용한 전통적인 방식으로 SSR을 구현하면 브라우저에서는 서버에서 보내주는 html 페이지를 하나의 파일로 통째로 받았다.(First Contentful Paint를 이야기함으로 페이지별 html 청크 분할에 대한 이야기는 제외한다.) 그러나 이제 v18에서부터는 pipeToNodeWritable를 이용해 html 코드를 스트리밍 형식을 통해 작은 청크 형태로 나누어 보내줄수 있다. 예시 서버 코드를 보면 다음과 같다.

const data = createServerData();
let assets = {
  'main.js': '/main.js',
  'main.css': '/main.css',
};

// 기존에 우리가 했던 방식
res.send(
 '<!DOCTYPE html>' +
  renderToString(
   <DataProvider data={data}>
     <App assets={assets} />
   </DataProvider>,
  )
);

// 리액트 v18의 pipeToNodeWritable
const { startWriting, abort } = pipeToNodeWritable(
  <DataProvider data={data}>
    <App assets={assets} />
  </DataProvider>,
  res,
  {
    onReadyToStream() {
      res.statusCode = someError ? 500 : 200;
      res.setHeader('Content-type', 'text/html');
      res.write('<!DOCTYPE html>');
      startWriting();
    },
    onError(e) {
      console.error(e);
    },
  },
);

function createServerData() {
  let done = false;
  let promise = null;
  return {
    read() {
      if (done) {
        return;
      }
      if (promise) {
        throw promise;
      }
      promise = new Promise((resolve) => {
        setTimeout(() => {
          done = true;
          promise = null;
          resolve();
        }, API_DELAY);
      });
      throw promise;
    },
  };
};

<Layout>
  <NavBar />
  <Sidebar />
  <Content>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </Content>
</Layout>

그리고 클라이언트 쪽에서는 렌더링하는데 비용이 큰 컴포넌트들을 Suspense로 감싸는 것을 통해 부분적이고 독립적으로 hydrate을 시작한다. 기존에는 번들 로드가 느린 어떤 컴포넌트 때문에 모든 페이지가 완전하게 인터렉션하게 될때까지 기다려야 했지만, 이제는 사이드바나 네비게이션바의 우선순위를 높게 하면 본인들의 역할을 보다 빨리 할 수 있게 된 것이다. 유저는 해당 페이지가 완전히 동작되기 이전에 다른 곳으로 이동하고 싶다면 이미 hydration이 완료된 네비게이션 바를 이용할 수 있다. 빠른 인터렉션을 제공해 더 나은 UX를 제공할 수 있게 되어 사용성을 높일 수 있는 것이다.

마무리

이제 직접 입맛대로 Hydration을 설계 할 수 있게 되어 좋은 개발 환경이 되었다고 생각한다. 그러나 한 편으로는 이러한 아키텍쳐는 서버 개발자와의 협업이나 처음 구조를 설계하는것에 있어서 더욱 복잡성을 가중시킬 수는 있다. 도입하려는 프로젝트에 이 기술이 꼭 필요한 것인지 신중히 검토하고 판단하는 것이 중요할 것 이다.

참고

helloinyong님 블로그
Google Web 포럼

profile
Frontend Developer.

6개의 댓글

comment-user-thumbnail
2022년 7월 13일

공식문서로는 이해가 잘 안가서, 검색하다가 글을 봤습니다
좋은 정보 정말 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 8월 2일

많은 도움이 되었습니다. 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 11월 16일

좋은 글 감사합니다!

1개의 답글