준영
도은
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가 점점 다양한 작업을 수행하면서..
CommonJS
와AMD(Asynchronous Module Definition)
- 모듈화하는 방안 논의… → 프레임워크들 인기
SPA
===클라이언트 사이드 라우팅
- 단순히 사용자 경험 향상 제공만의 이유는 아님!!
- 개발자들에게도 간편한 개발 경험
- 🏃 이러한 SPA 유행으로
JAM 스택
이라는 용어 등장- 기존 웹 개발은
LAMP 스택
- 기존 웹 개발은
4.1.2 서버 사이드 렌더링이란?
보여줄 페이지를 서버에서 렌더링 → 사용자에게 바로 제공
- 싱글 페이지 애플리케이션과 서버 사이드 렌더링의 차이점은
- ⭐️
웹 페이지의 렌더링의 책임
을 어디에 두느냐 - SPA : JS 번들에서
- SSR : 서버에서
- ⭐️
서버 사이드 렌더링의 장점
- 최초 페이지 진입이 비교적 빠르다
- FCP(First Contentful Paint): 진입했을 때 페이지에 유의미한 정보가 그려지는 시간
- 만약, 최초에 보여줘야 할 화면에 외부 API 호출에 많이 의존해야 한다면
- SPA: 진입 후에 모든 요청에 대한 응답이 이루어져야 이 결과로 화면을 그린다
- ^ 이러한 작업은 서버에서 수행하는 것이 더 빠르다
- HTML을 그리는 작업도 서버에서 그려서 내려주면
- 클라이언트에서 기존 HTML을 삽입하는 것보다 빠르다
- ⭐️ 화면 렌더링이 외부 API에 의존적 or HTML 크기가 크다면
- 상대적으로 SSR이 더 빠를 수 있다
- 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다
- 검색 엔진이 최초에 방문했을 때
- 메타 정보를 제공할 수 있어야 한다
- SSR은 제공할 정보를 서버에서 가공해서 바로 전달해주므로 SEO 유리
- 누적 레이아웃 이동이 적다
- CLS(Cumulative Layout Shift): 페이지를 보여준 이후 뒤늣게 HTML 정보 추가 삭제 → 화면이 덜컥 거리는 것과 같은
부정적인 사용자 경험
의미 - 사용자가 예상치 못한 시점에 페이지 변경
- API 요청의 응답 속도에 부분을 처리해두지 않는다면, 이러한 레이아웃 이동 문제 발생 가능
- 반면, 서버 사이드 렌더링의 경우에는
이러한 요청이 완료된 이후
에 완성된 페이지를 제공
- CLS(Cumulative Layout Shift): 페이지를 보여준 이후 뒤늣게 HTML 정보 추가 삭제 → 화면이 덜컥 거리는 것과 같은
- 사용자의 디바이스 성능에 비교적 자유롭다
- JS 리소스 실행은 사용자의 디바이스에서만 실행 → 절대적으로 사용자 디바이스 성능에 의존
- SSR은 이러한 부담을 서버에 나눌 수 있으므로 사용자의 디바이스 성능으로부터 조금 더 자유로워질 수 있다.
- 절대적인 것은 아니다.
- 인터넷 속도가 느려진다면 어떠한 방법론을 쓰든 느려질 것
- 보안에 좀 안전하다
- 브라우저 개발자 도구를 사용하면 웹사이트에서 일어나는 거의 대부분의 작업을 파악할 수 있다.
- 이 작업에는 API 호출과 인증처럼 사용자에게 노출되면 안 되는 민감한 작업도 포함
- SSR은 민감 혹은 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공
- 이러한 보안 위협을 피할 수 있다.
서버 사이드 렌더링의 단점
- 소스코드를 작성할 때 항상 서버를 고려해야 한다.
- 전반에 걸쳐 서버 환경에 대한 고려 필요
- 예) 브라우저 전역 객체인 window, localStorage와 같이 브라우저에만 있는 전역 객체 사용
- 서버에서 사용되면
not defined
에러 - 해당 라이브러리가 서버에 대한 고려 X → 의도치 않게 동작할수도
- 이럴수록.. 클라이언트에서만 실행되는 코드가 많아진다면 서버 사이드 렌더링의 장점을 잃는 것
- 적절한 서버가 구축되어 있어야 한다.
- SPA: HTML과 JS, CSS 리소스를 다운로드할 수 있는 준비만
- 서버는 JS와 HTML을 제공하면 역할 끝
- SSR: 사용자의 요청을 받아 렌더링을 수행할 서버 필요
- 사용자의 요청에 따라 대응할 수 있는 물리적인 가용량 확보
- 복구 전략도 필요
- 요청 분산, 프로세스 매니저 도움도 필요
- SPA: HTML과 JS, CSS 리소스를 다운로드할 수 있는 준비만
- 서버 지연에 따른 문제
- SPA에서 느린 작업이 있다면
- 최초에 어떤 화면이라도 보여준 상태에서 무언가 느린 작업 수행
- ‘로딩중’과 같이 작업이 진행 중임을 적절히 안내 가능
- SSR에서 느린 작업이 있다면
- 특히 최초 렌더링에 발생한다면 그 어떤 정보도 제공 불가능
- SPA에서 느린 작업이 있다면
🤔 서버 지연에 따른 문제 부분이 잘 와닿지 않는데..
- 우선 프론트엔드 서버를 이야기하는 것 같고
- 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 에러- ⭐️
renderToStaticMarkup
은 hydrate를 수행하지 않는다는 가정하에 순수한 HTML을 반환 - ⭐️ 리액트의 이벤트 리스너가 필요없는 완전히 순수한 HTML을 만들 때만 사용
- 아무런 브라우저 액션이 없는 정적인 내용만 필요한 경우에 유용
4.2.3 renderToNodeStream
renderToString
과 결과물이 완전히 동일, but 2가지 차이점 존재- 첫 번째 차이점: 브라우저에서 사용하는 것이 완전 불가능
renderToString
이랑renderToStaticMarkup
은 브라우저에서도 실행 가능- 완전 node.js 환경에 의존
- 두 번째 차이점: 결과물의 타입
- Node.js의
ReadableStream
ReadableStream
은utf-8
로 인코딩된 바이트 스트림- Node.js나 Deno, Bun 같은 서버 환경에서만 사용 가능
- 궁긍적으로 브라우저가 원하는 결과물, 즉 string을 얻기 위해서는 추가적인 처리 필요
- Node.js의
- 🤔 왜 필요할까?
- 스트림: 큰 데이터를 다룰 때 데이터를 청크(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
renderToString
과renderToStaticMarkup
의 차이와 동일하게- 리액트 속성 제공 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을 기준으로 이벤트를 붙이는 작업만 실행하는 것