Next.js의 Hydrate란?
Next.js 프레임워크의 동작원리를 제대로 파악하고 있는 개발자라면 Hydrate에 대해선 이미 익숙한 용어일 것이다.
그러나 Next.js의 주요 동작 방식 중 하나임에도, 눈에 잘 띄지 않아 놓치기도 쉬운 개념인 만큼, 한번 제대로 정리를 하고 넘어가보려고 한다.
Hydrate는 Server Side 단에서 렌더링 된 정적 페이지와 번들링된 JS파일을 클라이언트에게 보낸 뒤, 클라이언트 단에서 HTML 코드와 React인 JS코드를 서로 매칭 시키는 과정을 말한다.
이 과정이 왜 필요한지 간략하게 설명하기 위해, 우선 React에 대해 잠깐 얘기해보자.
React의 웹 페이지 구성 원리
React는 JS파일만을 이용하여 웹 화면을 구성하는 원리를 가지고 있다. 그래서 실제 HTML 코드는 안에 내용이 하나도 없는 상태이다. (Client Side Rendering이 SEO에 적합하지 않은 이유이기도 하다.)
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
위 코드는 처음 리액트 프로젝트 세팅할 때 많이 본 익숙한 HTML 코드일 것이다.
단순 뼈대만 있는 HTML document와 JS 파일들을 클라이언트로 모두 보낸 뒤, 클라이언트 단에서 JS 코드들을 통해 웹 화면을 렌더링 하며 페이지를 그리게 된다.
웹 페이지 렌더링을 한 뒤에도 페이지 내에서 동작하는 모든 이벤트 또한 자바스크립트로 인해 일어나게 된다.
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from './src/App';
ReactDOM.render(<App />, document.getElementById("root"));
위 코드처럼 public/index.html에는 아무 내용 없는 기본 뼈대만 있고, 나머지는 src/index.js의 자바스크립트 코드에서 모든 화면을 렌더링 한 뒤 HTML DOM 요소 중 root라는 아이디를 가진 엘리먼트를 찾아서 하위로 주입을 하게 된다.
Next.js의 웹 페이지 구성 원리
Next.js는 클라이언트에게 웹 페이지를 보내기 전에 Server Side 단에서 미리 웹 페이지를 Pre-Rendering 한다. 그리고 Pre-Redering으로 인해 생성된 HTML document를 클라이언트에게 전송한다.
그런데 이 시점에서 중요한 것은 아래 내용이다.
현재 클라이언트가 받은 웹 페이지는 단순히 웹 화면만 보여주는 HTML일 뿐이고, 자바스크립트 요소들이 하나도 없는 상태이다. 이는 웹 화면을 보여주고 있지만, 특정 JS 모듈 뿐 아니라 단순 클릭과 같은 이벤트 리스너들이 각 웹 페이지의 DOM 요소에 하나도 적용되어 있지 않은 상태임을 말한다.
그러면 이렇게 페이지만 보여주고 동작조차 하지 못하는 마치 빈 껍데기 같은 웹 페이지가 나중에는 어떻게 정상적으로 동작하게 되는 것일까.
Next.js Server에서는 Pre-Rendering된 웹 페이지를 클라이언트에게 보내고 나서, 바로 리액트가 번들링 된 자바스크립트 코드들을 클라이언트에게 전송한다.
네트워크 탭을 보면, 맨 처음 응답받는 요소가 document Type의 파일이고, 이후에 React 코드들이 렌더링 된 JS 파일들이 Chunk 단위로 다운로드되는 것을 확인할 수 있다.
그리고 이 자바스크립트 코드들이 이전에 보내진 HTML DOM 요소 위에서 한번 더 렌더링을 하면서, 각자 자기 자리를 찾아가며 매칭이 된다.
이 과정을 Hydrate라고 부른다.
이것은 마치 자바스크립트 코드들이 DOM 요소 위에 물을 채우 듯 필요로 하던 요소들을 채운다 하여 Hydrate(수화)라는 용어를 쓴다고 한다.
아마 위의 GIF 이미지처럼 잠깐의 스타일 깜빡임이 Next.js에서 나타나는 일반적으로 많이 보는 현상일 것이다.
새롭게 페이지를 로딩할 때마다 약간 뒤늦게 스타일이 적용되는 듯한 이 과정이, HTML DOM 요소에 뒤늦게 자바스크립트가 동작하고 Hydration 돼서 나타나는 현상이다.
(정확하게는, 자바스크립트로 외부 서버에 웹폰트를 요청해서 받아오는데, Hydrate 이전에는 웹 폰트를 아직 요청하지 못해 적용되지 않아서이다.)
Server에서 한번 렌더링하고 Client에서도 한번 더 렌더링 하면 비효율적인 렌더링 방식 아닌가요?
어쩌면 두번 렌더링 하는 것은 비효율적으로 보일 수 있다.
그러나 서버 단에서 빠르게 Pre-Rendering하고 유저에게 빠른 웹 페이지로 응답할 수 있다는 것에 더욱 큰 이점을 가져갈 수 있다. 심지어 이 Pre-Rendering 한 Document는 모든 자바스크립트 요소들이 빠진 굉장히 가벼운 상태이므로 클라이언트에게 빠른 로딩이 가능하다.
이는 같은 화면에 대해 두 번 렌더링이 일어난다는 단점을 보완하고도 남는다.
더 나아가서 클라이언트 단에서 자바스크립트가 렌더링을 할 때, 단지 각 DOM 요소에 자바스크립트 속성을 매칭 시키기 위한 목적이므로 실제 웹 페이지를 다시 그리는 과정까지는 하지 않는다.(Paint 함수 호출 X)
Hydrate 과정은 Next.js에서만 일어나는 과정인가요?
사실 Hydrate는 Next.js에 종속된 동작이 아니라 ReactDOM 함수이다.
흔히 리액트 프로젝트 구축 시 초반에 꼭 작성해주는 ReactDOM.render() 함수와 잠깐 비교를 해보자.
ReactDOM.render(element, container, [callback]);
ReactDOM.render() 함수는 특정 컴포넌트를 두 번째 파라미터인 지정된 DOM 요소에 하위로 주입하여 렌더링을 처리해주는 함수이다.
그리고 렌더링이 완료되면 특정 이벤트를 처리할 콜백 함수를 세 번째 파라미터로 넣어줄 수 있다.
ReactDOM.hydrate(element, container, [callback]);
ReactDOM.hydrate() 함수는 특정 컴포넌트를 두 번째 파라미터인 지정된 DOM 요소에 하위로 hydrate 처리만 한다. 이는 렌더링을 통해 새로운 웹 페이지를 구성할 DOM을 생성하는 것이 아니라, 기존 DOM Tree에서 해당되는 DOM 요소를 찾아 정해진 자바스크립트 속성(이벤트 리스너 등)들만 부착시키겠다는 말이다.
Hydrate에 대해 우리가 신경 써야 할 것이 있을까요?
사실 그냥 웹 페이지 및 일반 Feature 개발만 한다면 이 과정을 몰라도 Next.js를 쓰는 것에 큰 문제는 없지만, 우선 원리는 알면 좋다.
그리고 나는 겪어보진 않았었는데, 어찌어찌 구글링 하다가 Next.js에서 Hydrate로 인해 발생하는 스타일 이슈 같은 게 있나 보다.
이와 관련된 자세한 내용은 내가 봤던 아래 블로그 링크로 남겨둔다.
그리고 혹시나 Next.js 내에 Redux를 사용하게 된다면, 아마 Next-Redux-Wrapper라는 라이브러리를 이용하게 될 텐데, Reducer root Store에서 Hydrate를 처리해 주는 부분이 있다. 이는 Server Side 단에서 dispatch 했던 Store들을 클라이언트 단에서 그대로 사용할 수 있도록 Redux Store도 클라이언트 단에서 같이 Hydration이 필요하기 때문이다.
그리고 사실 지금와서 Hydrate에 대해 블로깅을 한 이유는, 이번에 웹 성능 최적화 작업을 하다가 이 Next.js Hydration에 대해 흥미로운 점을 발견하게 돼서 이에 대한 블로깅(Partial Hydration에 대해)을 하기 위해 미리 Hydrate에 대해 우선 정리를 했다.