Post

Next.js의 Hydrate

Next.js 프레임워크의 동작원리에서 Hydrate는 중요한 개념이다.

Next.js는 서버 사이드 렌더링, 정적 웹 페이지 생성 등 리액트 기반 웹 애플리케이션 기능들을 가능케 하는 오픈 소스 웹 개발 프레임워크로 React(CSR)의 단점을 보안하기 위해 같이 사용되곤 한다.

Hydrate를 이해하기 위해 먼저 React(CSR)와 Next(SSR)의 동작 과정과 장단점에 대해 알아보겠다.

CSR과 SSR

1. CSR

Client Side Rendering의 약자

말 그대로 렌더링이 클라이언트 쪽에서 일어난다. 즉, 서버는 요청을 받으면 클라이언트에 HTML과 JS를 보내준다.

image

위의 그림처럼 서버 측에서 받은 HTML은 비어있는 상태이지만 랜더링된 화면을 보면 요소들이 차 있는것을 볼 수 있다.

단순 구조만 있는 HTML 문서와 JS 파일들을 모두 클라이언트에 보낸 뒤 Client-Side에서 JS 로드들을 통해 웹 페이지를 렌더링한다.


1
2
3
4
5
6
7
8
9
10
11
// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>


1
2
3
4
5
6
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./src/App";

ReactDOM.render(<App />, document.getElementById("root"));


기본 뼈대만 있는 index.html 파일 대신 src/index.js의 JS 코드에서 모든 화면을 렌더링한 뒤 ReactDOM의 render를 통해 HTML의 DOM 요소 중 root라는 ID를 가진 element를 찾아 하위에 내용을 주입한다.

동작 과정

전체적인 흐름을 시간순으로 본다면 아래 그림처럼 표현된다.

image

  1. 서버에서 내려준 HTML을 화면에 띄우지만 비어있기 때문에 아무것도 보이지 않는다.

  2. HTML에 링크되어 있는 Js파일을 요청하여 동적으로 HTML을 생성할 수 있는 웹어플리케이션 로직이 담긴 Js파일을 받는다.

  3. Js파일을 받으면 ReactDOM의 render를 통해 화면을 랜더링하며 사용자에게 화면이 보여지고 interaction도 가능하게 된다.

단점

  1. 사용자가 첫 화면을 보기까지 시간이 오래걸린다.

  2. 좋지 못한 SEO

  • SEO(Search Engine Optimization): 검색 결과에서 상위에 노출될 수 있도록 내 콘텐츠를 최적화하는 방식

검색 엔진들은 서버에 등록된 웹사이트를 하나씩 확인하며 HTML문서를 보게 되는데 CSR의 HTML문서는 비어 있는 상태이기 때문에

검색 엔진들이 CSR로 작성된 웹사이트를 분석하는데에 어려움을 겪어 SEO가 좋지 못함.



2. SSR

Server Side Rendering의 약자

말 그대로 서버쪽에서 렌더링 준비를 끝마친 상태로 클라이언트에 전달하는 방식이다.

서버 측 렌더링을 사용하면 각 요청 에 대해 페이지의 HTML이 서버에 생성된다.

image

위의 그림처럼 서버 측에서 받은 HTML은 내용이 꽉 찬 상태이기 때문에 클라이언트는 이를 랜더링만 하면 된다.

하지만, Initial UI에서 Like 버튼이 활성화 되지 않은 것을 볼 수 있는데 이는 웹 화면을 보여주기 위한 HTML일 뿐이고, 동작에 필요한 JS 요소는 존재하지 않기 때문이다.

특정 JS 모듈 뿐 아니라 단순 클릭과 같은 이벤트들이 각 웹 페이지의 DOM 요소에 적용되어 있지 않은 상태의 페이지가 전송되는 것이다.

그래서 Hydration이란 작업을 통해 버튼을 클릭할 수 있는 Interactive UI가 생성되는 것이다.

Hydration은 아래에서 자세히 설명하도록 하겠다.

동작 과정

전체적인 흐름을 시간순으로 본다면 아래 그림처럼 표현된다.

image

  1. 서버에서 이미 잘 만들어진 HTML을 내려주기 때문에 사용자는 내용이 꽉 찬 화면을 볼 수 있다.

    • 동적으로 제어할 수 있는 Js파일은 받아오지 않았으므로 interaction은 불가능
  2. 서버에서 Js파일을 받으면 Hydrate과정을 통해 interaction이 가능해진다.

단점

  1. 서버에 과부하가 걸리기 쉽다.

    • 서버에서 필요한 데이터를 가지고 와서 HTML을 생성해야 한다.
  2. 사용자가 화면볼 수 있는 시점과 interaction이 가능한 시점의 공백이 존재한다.

    • 사용자가 클릭을 했는데 아무일도 발생하지 않을 수 있다.




Hydrate

SSR시 Hydrate를 통해 interaction이 가능해진다고 설명하였다. 이 글의 목적인 Hydrate에 대해 알아보겠다.

Hydration 은 수분 공급이라는 의미를 갖는다.

SSR시 DOM에는 동적인 이벤트가 하나도 없는 메마를 상태일 것이다. 그래서 이 메마른 뼈대에 수분을 보충한다고 하여 붙여여진 이름이라고 한다.

즉, 서버 사이드에서 먼저 정적인 페이지(HTML)를 렌더링하고, JS 코드가 모두 로드되면 이 HTML에 이벤트 핸들러 등을 붙여 동적인 페이지를 만드는 과정을 hydration이라 말한다.


img

출처: https://qanda.ai/ko


아마 위의 GIF 이미지처럼 잠깐의 스타일 깜빡임이 Next.js에서 나타나는 일반적으로 많이 보는 현상일 것이다.

새롭게 페이지를 로딩할 때마다 약간 뒤늦게 스타일이 적용되는 듯한 이 과정이 Hydrate 현상이다.

자바스크립트 코드들이 이전에 보내진 HTML DOM 요소 위에서 한번 더 렌더링을 하면서, 각자 자기 자리를 찾아가며 매칭이 되며 잠깐의 스타일 깜빡임이 나타나는 것이다.

(정확하게는, 자바스크립트로 외부 서버에 웹폰트를 요청해서 받아오는데, Hydrate 이전에는 웹 폰트를 아직 요청하지 못해 적용되지 않아서이다.)


❓ 두 번의 랜더링 비효율적이지 않을까 ❓

Server에서 한번 렌더링하고 Client에서도 한번 더 렌더링 하면 비효율적인 렌더링 방식 아닌가요?

그러나 서버 단에서 빠르게 Pre-Rendering하고 유저에게 빠른 웹 페이지로 응답할 수 있다는 것에 더욱 큰 이점을 가져갈 수 있다.

심지어 이 Pre-Rendering 한 Document는 모든 자바스크립트 요소들이 빠진 굉장히 가벼운 상태이므로 클라이언트에게 빠른 로딩이 가능하다.

이는 같은 화면에 대해 두 번 렌더링이 일어난다는 단점을 보완하고도 남는다.




React Query의 Hydrate

이전 게시글 React-Query 적용하기에서 React-Query를 SSR에 적용할 때 Hydrate 기능을 사용하였다.

Next.js와 React Query 모두 “hydrate”라는 용어를 사용하지만, 다른 컨텍스트에서 다른 의미를 가진다.

Next.js의 hydrate는 앞에서 설명한 것처럼 정적인 페이지(HTML)를 렌더링하고, JS 코드가 모두 로드되면 이 HTML에 이벤트 핸들러 등을 붙여 동적인 페이지를 만드는 기능이다.


React Query에서도 hydration과 관련된 기능을 제공하고 있는데, hydrate는 데이터를 서버에서 가져와 클라이언트 상태로 초기화하는 데 사용되며 dehydrate와 hydrate 메서드를 사용한다.

  • dehydrate: 나중에 hydrate로 공급할 수 있는 cache에 대한 고정된 표현을 생성

  • hydrate: 이전에 dehydrate된 state를 cache에 추가

React Query는 Next.js 서버에서 여러 개의 query를 prefetch하고 그 query들을 queryClient에 dehydrate하는 것을 지원한다.

즉, 서버는 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있으며, JS를 사용할 수 있게 되면 React Query는 라이브러리 자체의 기능으로 이러한 query들을 업그레이드하거나 hydrate할 수 있다.

이 기능 중에는 query들이 서버에서 렌더링된 이후로 클라이언트에서 stale한 상태가 되었을 때 refetch해오는 것도 포함된다.

서버에서의 query 캐싱 지원 및 hydration 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// _app.jsx
import {
  Hydrate,
  QueryClient,
  QueryClientProvider
} from "@tanstack/react-query";

export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  );
}
  • app, instance ref (또는 React 상태) 내에 새로운 QueryClient instance를 생성 (이렇게 하면 컴포넌트 라이프사이클 당 QueryClient를 오직 한 번만 생성하여 데이터가 서로 다른 사용자와 요청 간에 공유되지 않는다.)

  • app 컴포넌트를 로 감싸고 client instance에 넘겨줌

  • app 컴포넌트를 로 감싸고 pageProps의 dehydratedState prop을 넘겨줌



SSR - 데이터 prefetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from "@tanstack/react-query";

export async function getStaticProps() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery("posts", getPosts);

  return {
    props: {
      dehydratedState: dehydrate(queryClient)
    }
  };
}

function Posts() {
  const { data } = useQuery(["posts"], getPosts);
  // ...
}
  • 각 페이지의 request 별로 새로운 QueryClient instance를 생성하자. 이렇게 하면 서로 다른 사용자와 요청 간에 데이터가 공유되지 않는다.

  • client의 prefetchQuery 메서드를 사용해 데이터를 prefetch해오고 완료되기까지 기다리자.

  • query cache를 dehydrate하기 위해 dehydrate 메서드를 사용하고, dehydratedState prop을 통해 이를 페이지에 넘겨주자. 이는 _app.js에서 불러온 cache와 동일한 prop이다.




📑 참고 자료

[간단정리] CSR vs SSR 특징 및 차이

서버사이드 렌더링 (개발자라면 상식으로 알고 있어야 하는 개념 정리 ⭐️)

Next.js의 Hydrate란?

[한글 번역] React Query - SSR (Using Next.js)

This post is licensed under CC BY 4.0 by the author.