출처
‣
‣
‣
Hydrate?
- Hydrate란 서버 사이드 렌더링된 정적 페이지와 번들링된 JS파일을 클라이언트에게 보낸 뒤, 클라이언트 단에서 html코드와 JS코드(리액트)를 서로 매칭 시키는 과정을 말합니다.
- 위 과정이 왜 필요한가?
- React는 JS 파일만을 이용하여 웹 화면을 구성하는 원리(CSR)를 가졌기 때문에, 실제 html 코드 안에는 내용이 하나도 없는 상태입니다. (이것이 CSR이 SEO에 적합하지 않은 이유이기도 합니다.)
- 단순 뼈대만 있는 HTML document와 JS 파일들 → 클라이언트 → 클라이언트 단에서 JS코드들을 통해 웹 화면을 렌더링 하며 페이지를 그리게 되는 것입니다.
- 웹 페이지 렌더링을 한 뒤에도 페이지 내에서 동작하는 모든 이벤트 또한 JS 코드들로 인해 일어나게 됩니다.
- 위 코드처럼 index.html에는 기본 뼈대만 있고 나머지는 index.js의 JS코드에서 모든 화면을 렌더링 한 뒤, HTML DOM 요소 중 root라는 id를 가진 엘리먼트를 찾아서 하위로 주입하게 되는 것입니다.
- Next.js는 클라이언트에게 웹 페이지를 보내기 전에 서버 사이드 단에서 미리 웹 페이지를 pre-rendering을 합니다(SSR). 그리고 pre-rendering으로 인해 생성된 HTML document를 클라이언트에게 전송합니다.
- 그러나 현재 클라이언트가 받은 웹 페이지는 단순히 웹 화면만 보여주는 html일 뿐이고, JS 요소들이 하나도 없는 상태입니다. 단순 클릭과 같은 이벤트 리스너들이 각 웹 페이지의 DOM 요소에 하나도 적용되어 있지 않은 빈 껍데기 같은 상태인 것 입니다.
- Next.js. 서버에서는 pre-rendering된 웹 페이지를 클라이언트에게 보내고 나서, 바로 리액트가 번들링 된 JS 코드들을 클라이언트에게 전송하는 것입니다.
- 네트워크 탭에서, 맨 처음 document type 파일을 응답받고, 그 이후에 react코드들이 렌더링 된 JS 파일들이 chunk 단위로 다운로드 되는 것을 확인할 수 있습니다.
- 이 JS 코드들이 이전에 보내진 HTML DOM 요소 위에서 한번 더 렌더링 하면서 각자 자기 자리를 찾아가며 매칭이 되는 것인데, 이 과정을 Hydrate라고 부릅니다.
- 마치 JS코드들이 DOM 요소 위에 물을 채우듯이 필요로 하던 요소들을 채운다고 해서 Hydate라는 용어를 쓴다고 합니다.
- 위의 이미지처럼 잠깐의 깜빡임이 Next.js에서 나타나는 일반적인 현상입니다.
- 새로고침할 때마다 약간 뒤늦게 스타일이 적용되는 듯한 과정이 , HTML DOM 요소에 뒤늦게 JS가 동작하고 Hydration이 되어서 나타나는 현상인것입니다.
- 정확히는 JS는 외부 서버에서 웹폰트를 요청해서 받아오는데, Hydrate 이전에는 웹 폰트를 아직 요청하지 못해 적용되지 않아서 이기 때문이긴 합니다.
- cf) 서버에서 한번, 클라이언트에서 한번 총 두번 렌더링 하면 비효율적이지 않나?
- 그렇지만, 서버 단에서 빠르게 pre-rendering하고 유저에게 빠른 웹 페이지로 응답할 수 있다는 것에 더욱 큰 이점을 가져갈 수 있습니다.
- 또한, pre-rendering한 document는 모든 JS요소들이 빠진 상태라 가벼워서 클라이언트에게 빠른 로딩이 가능합니다.
- 그리고 클라이언트 단에서 JS가 렌더링을 할 때, 단지 각 DOM 요소에 JS 속성을 매칭 시키는 것이지, 실제 웹 페이지를 다시 그리는 과정까지는 하지 않기 때문에 괜찮습니다.
React의 웹 페이지 구성 원리
HTML DOM : html문서를 브라우저가 이해할 수있도록 만든 트리구조
Next.js의 웹페이지 구성 원리 (feat.Hydration)
cf) Next.js hydration style 이슈 파악하기
- 원인을 파악해 봅시다
- 아래 사진은 서버 사이드에서 렌더링 되어서 내려온 html으로, 서버 사이드 렌더링 자체는 잘 되었습니다.
- 리액트가 두 개의 트리를 어떻게 비교할까?
- 이렇게 두 트리를 비교하는 것이 맞아보지만, 사실은 아닙니다. hydrate는 html파일을 렌더링 된 트리가 아닌, 버츄얼 돔과 비교하기 때문에 useEffect가 실행되기 전의 상태와 비교하게 됩니다.
- 그럼 위의 사진처럼 비교를 할까요? X
- 왜냐하면 리액트 hydration은 텍스트나 속성값은 비교하지 않기 때문입니다. 결국 아래처럼 렌더링된 엘리멘트 타입과 순서만 비교하는 것입니다.
- 이대로 리액트의 판단 대로 하나씩 적용해 보겠습니다. 서버 사이드 렌더링된 결과물에, 클라이언트 사이드 렌더링 된 결과물을 하나씩 대입해보는 것입니다.
- 사실 이 상태에서 리렌더링(상태가 변할 때)이 발생할 경우, 각 컴포넌트의 props를 다시 제대로 인식하게 되면서 애초에 원했던 결과가 나오게 됩니다.
- 이 문제는 정확히, 리렌더링이 일어나지 않는 페이지에서만 발생합니다. 또한, 대체로 컴포넌트 단위보다는 페이지의 레이아웃 단위에서 자주 보여집니다.
- 위에 언급했듯이, 리액트는 DOM element 단위에서만 비교하기 때문에 className이나 id 같은 프로퍼티가 엉뚱한 엘리먼트에 잘못 붙는 현상이 발생하게 됩니다. 잘못된 스타일이 먹히는 것이 가장 일반적인 문제입니다.
(cf)
dynamic import의 한계?
Dynamic Import는 pre-renderinge된 Document에서도 보이지 않게 된다.
- Dynamic Import는 해당 컴포넌트의 Chunk File을 걷어내 웹 성능은 향상시킬 수 있겠지만, 그 이전 서버단의 pre-rendering 과정에서도 해당 컴포넌트가 초기 로딩 때 바로 보여질 상태가 아니라면 렌더링 요소 대상에서 제외됩니다.
- 어짜피 해당 영역이 뷰 포트로 접근했을 때 유저에게 정상적으로 보여지면 상관 없지 않나?
- 유저 관점으로는 문제가 없겠지만, SEO관점으로 볼땐 상관이 있습니다. 크롤러가 웹 페이지에 방문 했을 때 접속한 document 페이지에 크롤링할만한 정보가 없기 때문입니다.
- 그러면, pre-rendering은 그대로 보존하고, 해당 컴포넌트 Chunk File이 필요한 순간에 Lazy하게 요청해서 가져올 수 있는 방안이 있을까?
- Next Lazy Hydration : document는 유지하고 script 코드는 lazy하게 가져와서 hydration 문제를 해결할 수 있는 방안
- 브라우저에 응답을 받자마자 HTML document는 필요한 script 코드를 서버로 요청하게 되는데, 이러한 script 요청을 제어할 수 있는 작은 Webpakc Plugin을 구현해야합니다.
- 빌드 시 스크립트 파일 단위로 해당 webpack이 컴파일을 시작할때, lazy하게 가져오려는 chunk file에 별도의 표시를 해놔야 합니다.
- Next.Script를 확장해서, 기존의 모든 Chunk Script를 Next Script 자체에서 관리했는데, Lazy Hudration 할 Chunk Script는 별도로 Webpack에서 관리될 것이기 때문에, Next Script Data에서 'lazy-'로 표시한 Dynamic Import Chunk File들을 제외시키도록 한다.
- 깜박임 방지를 위해 아래 모듈을 사용합니다.’
- 내부 interactive Obsercer와 whenVisible 속성을 통해 해당 컴포넌트가 뷰 포트에 들어와 유저 인터렉션이 필요해지는 순간에 hydration을 수행합니다.
- 따라서 html은 그대로 남아있고, hydration이 필요해지는 순간에 웹팩이 등장해서 동적으로 해당 chunk script를 요청한 후 hydration하여 정상적으로 해당 컴포넌트가 동작하도록 하는 것입니다.
Webpack Plugin 설정
Next.js 내부 설정 재정의
1. _document.tsx에서 <script> 태그를 제어할 수 있도록 next/head를 확장한다.