[CRP]Critical Rendering Path에 이어 react와 Next.js로 CSR,SSR 이해하기

이주영·2023년 4월 24일
1

Javascript

목록 보기
6/11

들어가기 앞서

작년 7월부터 지금까지 기본적인 html, css, js에서부터 react 라이브러리를 활용해서 프로젝트를 진행하기도 했고 어느덧 next.js를 활용하여 사이드 프로젝트를 진행하게 됐다. 이 시점에서 다시 기본적인 원리를 정리해보고 더 견고한 지식 베이스를 쌓아 보려고 블로그로 정리하려고 한다.

브라우저 렌더링 🔄

대부분의 프로그래밍 언어는 OS 혹은 VirtualMuchine(CPU) 위에서 실행되지만, 웹 application은 client side JS는 브라우저에 HTML, CSS와 함께 실행된다. 따라서 브라우저 환경을 고려해야한다. 브라우저 환경을 고려한다는 것은 브라우저 렌더링 과정(critical rendering path)을 이해하는 것이다.

용어 정리

  • 파싱 : 텍스트 문서를 브라우저가 읽을 수 있도록 변환하는 과정
  • 렌더링 : HTML,CSS,JS로 작성된 문서를 파싱해서, 브라우저에 시각적으로 출력하는 것을 의미
  • DOM : 브라우저가 이해할 수 있는 자료구조
  • Render Tree : DOMCSSOM 중 브라우저에 표현될 노드만 포함한 자료구조

브라우저 렌더링의 큰 그림 🗺️

  1. HTML,CSS,JS,이미지,폰트 파일등 렌더링에 필요한 리소스를 서버로 부터 응답받는다.
  2. 브라우저의 렌더링 엔진이 응답된 HTML과 CSS를 파싱해서 DOM과 CSSOM을 만들고 결합해서 render tree를 만든다.
  3. 브라우저의 JS 엔진은 서버로 부터 받은 JS를 파싱해서 AST를 생성하고 바이트 코드로 변환하여 실행한다. 이때 자바스크립트 DOM API를 통해서 DOM이나 CSSOM을 변경할 수 있게 된다. 변경된 DOM ,CSSOM은 다시 렌더 트리로 결합된다. 이 과정이 반복될 때 이것을 리플로우/리페인트라고 하며 성능에 악영향을 미칠 수 있어서 참고로 react 라이브러리에서는 가상돔을 활용하여 변경된 사항을 UI에 한 번에 변경하는 방식을 통해서 효과적인 리렌더링을 방식을 취한다.
  4. Render Tree를 기반으로 HTML 요소의 레이아웃을 계산하고 브라우저 화면에 HTML 요소를 페인팅한다.

큰 그림 해부하기

1. HTML 파싱과 DOM 생성

택스트를 브라우저에 시각적인 픽셀로 렌더링을 위해서는 브라우저가 이해할 수 있는 자료구조로 변환해서 메모리에 저장해야한다. 그게 DOM이다. 다시 말해 DOM이란 HTML 택스트를 브라우저에 렌더링 하기 위해 브라우저가 이해할 수 있는 자료구조라고 정리해볼 수 있다.

2. DOM 생성 과정

  • 서버 입장
  1. 바이트 : 브라우저가 요청한 HTML을 메모리에 저장된 바이트를 인터넷을 경유하여 응답한다.
  • 클라이언트 입장
  1. 문자 : HTML 문서는 meta태크의 charset 속성에 의해 지정된 인코딩 방식을 기준으로 문자열로 변환
  2. 토큰 : 문자열로 변환된 HTML 문서를 읽어 들여 문법적 의미를 갖는 코드의 최소단위 토큰으로 분해
  3. 노드 : 각 토큰들을 객체로 변환하여 노드를 생성. 토큰의 내용에 따라 document node, element node, attribute node, text node가 형성된다.
  4. DOM : HTML의 부자 관계를 반영하여 모든 노드들을 트리 자료구조로 구성한다.

3. CSS 파싱과 CSSOM 생성

브라우저 렌더링 엔진은 HTML을 동기적으로 파싱하여 DOM을 생성하다가 CSS를 로드하는 link,style 태그를 만나면 DOM 생성을 중단한다. 그리고 HTML과 동일한 과정인 (바이트 -> 문자 -> 토큰 -> 노드-> CSSOM)을 파싱해서 쏨트리를 생성, 완료되면 다시 DOM 생성시 중단됐던 지점부터 파싱하여 DOM을 완성시킨다.

4. Render Tree 생성

  • Render Tree 단계
    렌더링을 위해서 DOM과 CSSOM을 결합하여 render tree로 만들어진다. 브라우저 렌더링을 위해서 지정된 자료
    구조는 RENDER tree

  • layout 단계 : 생성된 render tree를 사용해 요소들의 위치 크기 조절이 이루어진다.

  • painting 단계 : 브라우저 화면에 렌더링하는 페인팅 처리에 입력된다.

5. 자바스크립트 파싱과 실행

DOM은 HTML문서의 구조만이 아닌 프로그래밍 interface인 DOM API를 제공한다. 즉 자바스크립트 코드에서 DOM이 제공해준 API를 사용하면 이미 생성된 DOM을 조작할 수 있다.

HTML,CSS 파싱과 동일하게 DOM을 생성하다가 script 태그를 만나면 DOM 생성을 중단하고 script의 src의 서버에 요청을 하여 자바스크립트를 받아온다. 이때 자바스크립트 코드를 파싱하기 위해 제어권이 렌더링 엔진에서 V8 엔진으로 넘어간다.

자바스크립트 파싱과 실행이 종료되면 다시 렌더링 엔진으로 제어권이 넘어가고 HTML 파싱이 중단된 지점부터 다시 HTML 파싱을 시작하여 DOM 생성을 재개한다.

자바스크립트 파싱, 실행은 전적으로 자바스크립트 엔진이 처리한다. 자바스크립트 코드를 파싱하기 위해서 Low-level-language로 변환하고 실행하는 역할을 수행한다.

자바스크립트를 해석하여 AST(추상적 구문트리)를 생성한다. AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트 코드를 생성하여 실행한다.

  • 어휘 분석
    토크 나이징 : 단순한 문자열을 분석하여 문법적 의미를 갖는 최소단위인 토큰으로 분해
  • 구문 분석
    1. 토큰들의 집합을 AST를 생성. AST를 사용하면 TS,Babel,Prettier 같은 트랜스파일러를 구현할 수 있다.
    2. AST가 인터프리터가 실행할 수 있는 바이트 코드로 변환되고 인터프리터에 의해 실행

6. 리플로우와 리페인트

DOM API에 의해 이전에 만들어진 DOM과 CSSOM이 변경되어 렌더 트리를 기반으로 레이아웃과 페인트 과정을 거져 리렌더링한다. 이를 reflow, repaint라고 한다.

7. 자바스크립트 파싱에 의한 HTML 파싱 중단

브라우저는 동기적으로 HTML,CSS, JS를 파싱하는데 이로 인해 HTML의 돔트리가 무거운 연산이 들어있는 JS에 의해 다소 늦게 돔이 완성될 경우가 있다. 그렇게 된다는 것은 사용자가 서버에 html을 요청하고 오랜 기간이 지난 후에나 페이지를 볼 수 있다는 것인데... 이를 해결 할 수 있는 방법은 없을까?

script의 위치 설정

  1. body 최하단 : 지나온body 태그들을 이미 DOM에 생성했기에 JS에서 태그들을 인식할 수 있다.
  2. HTML5부터 생긴 async : HTML파싱과 외부 파일 로드를 비동기적으로 진행. 단 파싱과 실행할떄는 DOM을 잠시 중단하고 완료후 돔이 다시 생성된다. 여러개의 script에 async 속성을 넣는다고 가정한다면 async 속성은 로 드되는 순서에 의해 파싱되고 실행되기에 순서가 보장되지 않는다.
  3. defer 속성 : async와 마찬가지로 로드를 비동기적으로 진행한다. DOM이 생성된 직후 JS가 파싱하고 실행한다. 또한 순서도 보장된다.

SSR과 브라우저 렌더링

SSR은 말 그대로 서버에서 미리 HTML을 파일을 만들어 클라이언트로 응답해주는 방식

전통적인 브라우저의 렌더링 방식과 동일!!!

  1. HTML 파싱하여 DOM 트리를 만든다
  2. CSS 파싱해서 CSSOM 만든다
  3. 두개를 결합하여 render tree를 만든다
  4. render tree를 기반으로 layout(위치 계산) 및 paint(픽셀로 그려주는 작업)이 이루어진다.
  5. 자바스크립트 엔진으로 제어권이 넘어간다.
  6. 중요한 것은 이 당시에 UI는 완성되어 있다는 것이 핵심

Next.js 둘러보기

Pre-Rendering

  1. 서버 사이드에서 ReactDomServer.renderToString()이라는 함수를 사용해 HTML을 문자열로 가지고 온다.
  2. 클라이언트로 Pre-rendering한 html을 전달해준다. 사전에 클라이언트 단에서 렌더링 방식을 정할 수 있습니다.
  3. 브라우저는 매 request에 응답으로 prerendering한 html을 받습니다. 이때 화면에 렌더링을 시작하고 html 문서에 있는 js 파일을 로드합니다. 이후 클라이언트에서 ReactDom.render 함수를 통해서 React Element를 렌더링하고 이후 hydration을 통해서 interactive한 페이지로 완성시킵니다.

next.js 공식문서를 보고 정리해봤어요. 가볍게 보시는데 추천드립니다.
next.js 공식 문서

AJAX의 등장이후 CSR 브라우저 렌더링과 가상돔

라이브러리 : react

CSR은 서버로부터 css,js link만 있는 빈 HTML파일을 전송받아 클라이언트 사이드에서 불러오는 방식

전통적인(MPA) 브라우저 렌더링과 SSR의 브라우저 렌더링 방식과 거의 동일
1. HTML 파싱하여 DOM 트리를 만든다
2. CSS 파싱해서 CSSOM 만든다
3. 두개를 결합하여 render tree를 만든다
4. render tree를 기반으로 layout(위치 계산) 및 paint(픽셀로 그려주는 작업)이 이루어진다.
5. 자바스크립트 엔진으로 제어권이 넘어간다.
6. 이 당시 UI가 미완성되어있고 JS에서 UI 요소를 하나하나 동적으로 생성하며 그려준다.

react

리액트는 HTML이 하나인 대중적인 SPA 라이브러리로서 전통적인 MPA 웹페이지과 다르게 페이지 이동시 깜빡임이 없다는 특징이 있다.

맨 처음 유저가 웹사이트에 도착하면 먼저는 하나의 HTML을 보내주고 비교적 큰 사이즈(리액트 라이브러리 소스 코드 포함) JS를 보내준다. 마지막으로 정적인 파일들을 전달해준다.

creat-react-app을 활용해서 react 프로젝트를 만들고 npm run eject 명령어를 통해 모듈 번들러가 어떻게 생성되어 있는지 확인해보니 output이 bundle.js로 설정돼있다. 아하 그래서 수 많은 JS 파일이 따로 로드되는게 아니라 한번에 로드되는구나!

Q. styled-components 동작 원리가 어떻게 되지?

styled-components를 사용하면 JS 코드로 작성되고 이말인 즉슨 런타임 중에 실행된다는 것. 그래서 cssom 트리에 반영되지 않고 페이지가 로드되는 동안 동적으로 생성된다. styled-components는 SSR을 통해 스타일을 사전에 렌더링한다고 한다. 서버에서 JS 코드에서 정의된 스타일에 따라 css 규칙을 생성한다.

JS로 기존의 돔이 수정될 경우, 리페인트 레플로우가 발생한다. 이떄 불필요한 리렌더링이 발생하면 성능에 악영향을 미치는데 이러한 문제를 극복하기 위해 react에서는 가상돔을 사용하여 변경된 부분만 반영해주는 방식으로 렌더링을 최적화한다.

참고 자료

  1. Deep Dive E-book
  2. Next.js 공식 문서
  3. React.dev 공식 문서
profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

0개의 댓글