반디북
모던 리액트 Deep Dive
Ch4
준영
도은

4.1 서버 사이드 렌더링이란?

  • 서버 사이드 렌더링은 SPA를 만드는 것보다 신경쓸 점이 훨씬 많다.
  • 서버 사이드 렌더링이 왜 필요할까?

4.1.1 싱글 페이지 애플리케이션의 세상

서버 사이드 렌더링의 반대 개념 === 싱글 페이지 애플리케이션

싱글 페이지 애플리케이션이란?

  • 렌더링라우팅 에 필요한 대부분의 기능을 브라우저의 자바스크립트에 의존
  • 하나의 페이지에서 모든 작업을 처리
  • HTML 코드를 보면 <body /> 내부에 아무런 내용 X
    • ⭐️ 모두 JS로 삽입한 이후에 렌더링되기 때문
  • 페이지 전환 시 새로운 HTML 페이지 요청 X
    • 다음 페이지에서 필요한 정보만 요청을 해서
    • <body /> 내부에 DOM을 추가, 수정, 삭제하는 방법으로 페이지 전환하는 것
  • ⭐️ 최초에 서버에서 최소한의 데이터를 불러온 이후부터
    • 이미 가지고 있는 JS 리소스와 브라우저 API 기반으로 모든 작동이 이루어짐
  • ⭐️ 단) 최초에 로딩해야 할 JS 리소스가 커짐 (서버에서는 최소한의 일만 하니까)
  • ⭐️ 장) 한번 로딩된 이후에는 서버를 거쳐 리로스를 받아올 일이 적어 → 사용자 경험 👆🏼

전통적인 방식의 애플리케이션과 싱글 페이지 애플리케이션의 작동 비교

  • 전통적인 방식에서는..
    • 페이지 전환이 발생할 때마다 새롭게 페이지 요청 → HTML 페이지 다운로드 & 파싱
    • 이 과정은 페이지를 처음부터 새로 그려야 함 → 페이지 전환마다 깜빡거림을 경험
  • SPA에서는..
    • 이러한 페이지 전환 과정을
    • JS 리소스로 하며, 이를 맨 처음에 몽땅 가져오므로
    • 막상 애플리케이션 실행 후 페이지 전환할 때는 필요한 일부 영역만 다시 JS로 그리게 됨
    • 그래서 페이지 전환 시 깜빡거림이 없다

싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장

  • 과거 PHP, JSP === 서버 사이드 렌더링
  • JS는 어디까지나 인터랙티브한 요소를 위한 보조적인 수단으로
  • 🏃 JS가 점점 다양한 작업을 수행하면서..
    • CommonJSAMD(Asynchronous Module Definition)
    • 모듈화하는 방안 논의… → 프레임워크들 인기
  • SPA === 클라이언트 사이드 라우팅
    • 단순히 사용자 경험 향상 제공만의 이유는 아님!!
    • 개발자들에게도 간편한 개발 경험
  • 🏃 이러한 SPA 유행으로 JAM 스택이라는 용어 등장
    • 기존 웹 개발은 LAMP 스택

4.1.2 서버 사이드 렌더링이란?

보여줄 페이지를 서버에서 렌더링 → 사용자에게 바로 제공

  • 싱글 페이지 애플리케이션과 서버 사이드 렌더링의 차이점은
    • ⭐️ 웹 페이지의 렌더링의 책임을 어디에 두느냐
    • SPA : JS 번들에서
    • SSR : 서버에서

서버 사이드 렌더링의 장점

  1. 최초 페이지 진입이 비교적 빠르다
    • FCP(First Contentful Paint): 진입했을 때 페이지에 유의미한 정보가 그려지는 시간
    • 만약, 최초에 보여줘야 할 화면에 외부 API 호출에 많이 의존해야 한다면
      • SPA: 진입 후에 모든 요청에 대한 응답이 이루어져야 이 결과로 화면을 그린다
      • ^ 이러한 작업은 서버에서 수행하는 것이 더 빠르다
      • HTML을 그리는 작업도 서버에서 그려서 내려주면
        • 클라이언트에서 기존 HTML을 삽입하는 것보다 빠르다
    • ⭐️ 화면 렌더링이 외부 API에 의존적 or HTML 크기가 크다면
      • 상대적으로 SSR이 더 빠를 수 있다
  2. 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다
    • 검색 엔진이 최초에 방문했을 때
    • 메타 정보를 제공할 수 있어야 한다
    • SSR은 제공할 정보를 서버에서 가공해서 바로 전달해주므로 SEO 유리
  3. 누적 레이아웃 이동이 적다
    • CLS(Cumulative Layout Shift): 페이지를 보여준 이후 뒤늣게 HTML 정보 추가 삭제화면이 덜컥 거리는 것과 같은 부정적인 사용자 경험 의미
    • 사용자가 예상치 못한 시점에 페이지 변경
    • API 요청의 응답 속도에 부분을 처리해두지 않는다면, 이러한 레이아웃 이동 문제 발생 가능
    • 반면, 서버 사이드 렌더링의 경우에는 이러한 요청이 완료된 이후에 완성된 페이지를 제공
  4. 사용자의 디바이스 성능에 비교적 자유롭다
    • JS 리소스 실행은 사용자의 디바이스에서만 실행 → 절대적으로 사용자 디바이스 성능에 의존
    • SSR은 이러한 부담을 서버에 나눌 수 있으므로 사용자의 디바이스 성능으로부터 조금 더 자유로워질 수 있다.
    • 절대적인 것은 아니다.
    • 인터넷 속도가 느려진다면 어떠한 방법론을 쓰든 느려질 것
  5. 보안에 좀 안전하다
    • 브라우저 개발자 도구를 사용하면 웹사이트에서 일어나는 거의 대부분의 작업을 파악할 수 있다.
    • 이 작업에는 API 호출과 인증처럼 사용자에게 노출되면 안 되는 민감한 작업도 포함
    • SSR은 민감 혹은 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공
      • 이러한 보안 위협을 피할 수 있다.

서버 사이드 렌더링의 단점

  1. 소스코드를 작성할 때 항상 서버를 고려해야 한다.
    • 전반에 걸쳐 서버 환경에 대한 고려 필요
    • 예) 브라우저 전역 객체인 window, localStorage와 같이 브라우저에만 있는 전역 객체 사용
    • 서버에서 사용되면 not defined 에러
    • 해당 라이브러리가 서버에 대한 고려 X → 의도치 않게 동작할수도
    • 이럴수록.. 클라이언트에서만 실행되는 코드가 많아진다면 서버 사이드 렌더링의 장점을 잃는 것
  2. 적절한 서버가 구축되어 있어야 한다.
    • SPA: HTML과 JS, CSS 리소스를 다운로드할 수 있는 준비만
      • 서버는 JS와 HTML을 제공하면 역할 끝
    • SSR: 사용자의 요청을 받아 렌더링을 수행할 서버 필요
      • 사용자의 요청에 따라 대응할 수 있는 물리적인 가용량 확보
      • 복구 전략도 필요
      • 요청 분산, 프로세스 매니저 도움도 필요
  3. 서버 지연에 따른 문제
    • SPA에서 느린 작업이 있다면
      • 최초에 어떤 화면이라도 보여준 상태에서 무언가 느린 작업 수행
      • ‘로딩중’과 같이 작업이 진행 중임을 적절히 안내 가능
    • SSR에서 느린 작업이 있다면
      • 특히 최초 렌더링에 발생한다면 그 어떤 정보도 제공 불가능

🤔 서버 지연에 따른 문제 부분이 잘 와닿지 않는데..

  • 우선 프론트엔드 서버를 이야기하는 것 같고
  • SPA에서 서버는: HTML과 JS 리소스를 주게 되는데
    • HTML에 로딩중 UI를 바로 삽입하여
    • JS 리소스가 모두 로드되기 전 화면을 표시하는 것이 가능
  • SSR에서 서버는: HTML을 모두 그린 후 전달
    • HTML을 다 그린 후에 전달해주기 때문에
    • UI 표시 없이 빈화면을 봐야할 수도 있다.

ㄹㅇ된다 🤔

ㄹㅇ된다 🤔

4.1.3 SPA와 SSR을 모두 알아야 하는 이유

서버 사이드 렌더링 역시 만능이 아니다

  • 웹페이지에서 사용자에게 제공하고 싶은 내용은 무엇인지
  • 어떤 우선순위에 따라 페이지의 내용을 보여줄지 잘 설계하는지 중요
  • 설계와 목적, 우선순위에 따라 SPA이 더 효율적일 수 있다.

🤔 어떨 때 SPA가 더 유리할까?

  • 실시간 상호작용, 복잡한 UI 변경, 실시간 업데이트가 필요한 서비스
    • SPA는 페이지 전환 시 전체 페이지를 다시 로드하지 않고 필요한 부분만 업데이트
    • 사용자는 더 빠르고 부드러운 페이지 전환을 경험

4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기

  • 이 API는 당연히 브라우저의 window 환경이 아닌 Node.js와 같은 서버 환경에서만 실행 가능
  • window 환경에서 실행 시 에러 발생
  • 서버 사이드 렌더링을 실행할 때 사용되는 API 확인 → react-dom/server.js

4.2.1 renderToString

리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수

  • SSR을 구현하는 데 가장 기초적인 API
  • 최초의 페이지를 HTML로 렌더링 → 그 역할을 하는 함수 renderToString
  • ⭐️ 이벤트 핸들러는 결과물에 포함 X
  • 브라우저가 렌더링할 수 있는 HTML을 제공하는 데 목적이 있는 함수
  • ⭐️ 클라이언트에서 실행되는 JS 코드 포함 X
    • 필요한 JS 코드는 생성된 HTML과는 별도로 제공
  • renderToString 을 사용하면 먼저 HTML을 서버에서 제공 가능 → 초기 렌더링에서 뛰어난 성능
  • ⭐️ SSR === 최초 HTML을 빠르게 그려주는 데 목적
    • 사용자는 완성된 HTML을 빠르게 볼 수 있지만
    • 이벤트 핸들러와 같이 사용자와 인터랙션할 준비를 하기 위해서는
    • ⭐️ 별도의 JS 코드를 모두 다운로드, 파싱, 실행

4.2.2 renderToStaticMarkup

  • renderToString 과 유사한데
  • 루트 요소에 추가한 data-reactroot 와 같은 리액트에서만 사용하는 추가적인 DOM 속성 추가 X
  • 리액트에서만 사용하는 속성을 제거 → HTML의 크기를 아주 약간이라도 다이어트
  • 이 함수를 사용하면 완전히 순수한 HTML 문자열이 반환
  • renderToStaticMarkup 의 결과물 기반 → hydrate 수행 → 서버와 클라이언트의 내용이 일치 X 에러
  • ⭐️ renderToStaticMarkuphydrate를 수행하지 않는다는 가정하에 순수한 HTML을 반환
  • ⭐️ 리액트의 이벤트 리스너가 필요없는 완전히 순수한 HTML을 만들 때만 사용
  • 아무런 브라우저 액션이 없는 정적인 내용만 필요한 경우에 유용

4.2.3 renderToNodeStream

  • renderToString 과 결과물이 완전히 동일, but 2가지 차이점 존재
  • 첫 번째 차이점: 브라우저에서 사용하는 것이 완전 불가능
    • renderToString 이랑 renderToStaticMarkup 은 브라우저에서도 실행 가능
    • 완전 node.js 환경에 의존
  • 두 번째 차이점: 결과물의 타입
    • Node.js의 ReadableStream
    • ReadableStreamutf-8 로 인코딩된 바이트 스트림
    • Node.js나 Deno, Bun 같은 서버 환경에서만 사용 가능
    • 궁긍적으로 브라우저가 원하는 결과물, 즉 string을 얻기 위해서는 추가적인 처리 필요
  • 🤔 왜 필요할까?
    • 스트림: 큰 데이터를 다룰 때 데이터를 청크(chunk, 작은 단위)로 분활해 조금씩 가져오는 방식
    • renderToString이 생성해야 하는 HTML의 크기가 매우 크다면
      • 스트림을 활용해 청크 단위로 분리해 순차적 처리 가능
export default function App({ todos }) {
  return (
    <>
      <h1>나의 할일!</h1>
      <ul>
        {todos.map((todo, index) => {
          <Todo key={index} todo={todo} />;
        })}
      </ul>
    </>
  );
}

위 App은 todos를 순회하면서 렌더링 → todos가 엄청 많다면 → renderToString은 이를 모두 한 번에 렌더링하려고 하기 때문에 시간이 많이 소요될 것

⭐️ 그러나 이를 renderToNodeStream으로 렌더링하면?

  • HTML이 여러 청크로 분리되어 내려온다
  • 스트림 대신 renderToString 을 사용했다면 거대한 HTML 파일이 완성될 때까지 기다려야 했을 것
  • 스트림을 활용하면
    • 렌더링하는 Node.js 서버의 부담 축소
    • 대부분 리액트 SSR 프레임워크는 renderToString 대신 renderToNodeStream 을 채택

4.2.4 renderToStaticNodeStream

  • renderToStringrenderToStaticMarkup 의 차이와 동일하게
  • 리액트 속성 제공 X
  • hydrate를 할 필요가 없는 순수 HTML 결과물이 필요할 때 사용

4.2.5 hydrate

생성된 HTML에 JS 핸들러나 이벤트를 붙이는 역할

  • 렌더링한 HTML은 인터렉션이 불가능한 상태
  • hydrate는 정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 완전한 웹페이지 결과물 생성

render

import * from ReactDOM from 'react-dom'
import App from './App'
 
const rootElement = document.getElementById('root')
 
ReactDOM.render(<App />, rootElement)
  • render컴포넌트HTML의 요소를 인수로 받는다
    • HTML 요소에 컴포넌트를 렌더링
    • 여기에 이벤트 핸들러를 붙이는 작업까지 모두 한 번에 수행

hydrate

import * from ReactDOM from 'react-dom'
import App from './App'
 
// containerId를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미한다.
const element = document.getElementById(containerId)
 
// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다.
ReactDOM.hydrate(<App />, element)
  • 기본적으로 이미 렌더링된 HTML이 있다는 가정하에 작업 수행
  • 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행하는 것