서버 사이드 렌더링
서버 사이드 렌더링은 웹 컨텐츠의 렌더링에 꽤 오랫동안 사용된 방법이다. 서버사이드 렌더링에서는 사용자의 페이지 요청에 대해 전체 HTML을 만들어서 응답한다. 렌더링 된 컨텐츠에는 데이터 저장소나 외부 API로부터 받은 데이터가 포함되어 있다.
데이터를 조회하고 받아오는 것, 컨텐츠를 표현하기 위한 HTML을 만들어내는 것 모두 서버에서 처리한다. 따라서 서버 사이드 렌더링을 사용하면 데이터를 조회하고 템플릿 렌더링하기 위하여 서버와 별도로 통신하지 않아도 된다. 결과적으로 클라이언트 측에 렌더링 코드는 필요하지 않기 때문에 따로 코드를 전송하지 않아도 된다.
서버 사이드 렌더링에서 서버는 각 요청들이 연관되어 있고 크게 다르지 않더라도 독립적인 새로운 요청으로 간주하여 각각 처음부터 데이터를 생성한다. 서버는 여러 사용자들이 함께 사용하므로 특정 시간 동안은 이런 처리 기능을 함께 사용하게 된다.
클래식 서버사이드 렌더링 구현
아래 예제는 클래식 서버사이드 렌더링과 자바스크립트를 이용하여 현재 시간을 보여주는 페이지를 구현하고 있다.
<!DOCTYPE html>
<html>
<head>
<title>Time</title>
</head>
<body>
<div>
<h1>Hello, world!</h1>
<b>It is <div id=currentTime></div></b>
</div>
</body>
</html>
function tick() {
var d = new Date()
var n = d.toLocaleTimeString()
document.getElementById('currentTime').innerHTML = n
}
setInterval(tick, 1000)
이전에 구현했던 클라이언트 사이드 예제와 구별되는 점을 살펴보자. HTML이 서버에서 렌더링 되어 내려온 뒤. 시간은 클라이언트 측 자바스크립트 함수 tick()
에 의해 노출되는 것을 알 수 있다. 이런 데이터를 서버 시간과 같은 서버 데이터를 사용하려면 HTML이 렌더링 되기 전에 이런 코드들을 서버측에서 실행해야 한다. 따라서 페이지를 완전히 새로고침 하지 않으면 최신 데이터를 볼 수 없게 된다.
서버사이드 렌더링의 장점과 단점
렌더링 코드를 서버 측에서 실행하여 자바스크립트의 크기를 줄이면 다음의 이점이 생긴다.
자바스크립트의 크기가 작을수록 빠른 FCP와 TTI를 갖게 된다.
페이지 내에 다양한 UI요소들과 비즈니스 로직들이 포함되어 있을 때. 서버사이드 렌더링은 클라이언트 사이드 렌더링에 비해 상당히 작은 자바스크립트 번들을 사용한다. 로드하고 처리해야 하는 자바스크립트의 양이 줄어든다. FP, FCP, TTI가 짧아지고 FCP와 TTI가 거의 같아진다. 서버사이드 렌더링을 사용하면 화면에 요소가 보여지고 인터렉트 가능해지기까지 더 짧은 시간만 기다려도 된다.
클라이언트 사이드 자바스크립트 크기에 여유가 생긴다
개발팀은 원하는 성능을 이끌어내기 위해 페이지 내의 자바스크립트 번들의 크기를 관리해야 한다. 서버사이드 렌더링을 사용하면 페이지 렌더에 쓰이는 자바스크립트 코드들을 직접적으로 제거할 수 있어, 서드 파티 라이브러리 등 앱에 필요한 다른 자바스크립트 코드를 위한 공간이 확보된다.
검색 엔진 최적화
검색엔진 크롤러는 서버사이드 렌더링 앱의 컨텐츠에 대해서는 쉽게 크롤링 할 수 있고 따라서 페이지의 검색 엔진 최적화 수준이 향상된다.
서버사이드 렌더링의 정적 컨텐츠들은 위와 같은 장점이 있지만 크롤링의 모든 상황에 완벽히 처리하긴 어려우므로 최적화 작업을 따로 해주긴 해야 한다.
느린 TTFB
모든 프로세스가 서버에 의해 처리되므로 아래와 같은 상황 등으로 인해 서버의 응답이 지연될 수 있다.
- 동시에 아주 많은 사용자들이 데이터 로딩을 시작한다
- 네트워크가 느리다
- 서버 코드가 최적화되어 있지 않다
특정 인터렉션은 전체 페이지 새로고침이 필요할 수 있다
모든 코드를 클라이언트에서 사용할 수 없어 주요 동작들에 대해 서버 데이터가 필요한 경우 전체 페이지를 새로고침해야 할 수 있다. 이렇게 되면 사용자가 인터렉션 중간에 더 오래 기다려야 하므로 결과적으로 인터렉션 시간이 더 늘어날 수 있다. 따라서 서버사이드 렌더링에서는 SPA가 매우 어렵다.
이런 단점들을 극복하기 위해 요즘 프레임웍과 라이브러리들은 같은 앱을 서버, 클라이언트 양쪽에서 렌더링할 수 있게 되어있다. 다음 섹션에서 해당 내용에 대해 다룰 것이다. 먼저 Next.js를 사용한 간단한 서버사이드 렌더링 예제를 살펴보자.
Next.js의 서버사이드 렌더링
Next.js 프레임웍은 서버사이드 렌더링도 지원한다. 각 요청에 대해 서버에서 pre-render를 수행한다. 각 페이지에서 getServerSideProps()
를 구현하는것으로 서버사이드 렌더링을 수행할 수 있다.
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
인자에 있는 context
객체에는 라우팅 파라미터, 쿼리, 로케일 등 HTTP요청에 대한 데이터가 포함되어 있다.
getServerSideProps()
를 이용하여 포메팅 된 페이지를 React로 렌더링하는 예제는 여기서 볼 수 있다.
서버를 위한 React
React는 동형적으로 렌더될 수 있다. 이 뜻은 함수가 브라우저 뿐만 아니라 서버와 같은 기타 플랫폼에서도 실행될 수 있다는 것을 의미한다. 따라서 UI 요소들은 React를 활용하여 서버에서도 렌더링이 가능하다.
React는 유니버셜 코드로 같은 코드를 다양한 환경에서 실행할 수 있다. 이로 인해 Node.js로 만든 서버에서도 실행될 수 있다. 따라서 유니버셜 자바스크립트는 서버로부터 데이터를 조회하고 React를 이용해 화면을 렌더하는것이 가능하다.
아래는 이것이 가능하게 하는 함수를 사용하는 예시이다.
ReactDOMServer.renderToString(element)
이 함수는 React 엘리먼트와 대응되는 HTML문자열을 반환한다. 이 HTML은 빠른 페이지 로드를 위해 클라이언트 측 렌더링에 사용된다.
renderToString()
함수는 ReactDOM.hydrate()
함수와 함께 사용된다. 이 함수는 서버에서 렌더링 된 HTML을 그대로 클라이언트에서 쓸 수 있게 해 주므로 클라이언트에서는 이벤트 핸들러들만 등록하면 된다.
이를 구현하려면 클라이언트, 서버 각각의 페이지에서 .js
파일을 사용해야 한다. 서버 측 .js
파일은 HTML컨텐츠를 생성하고, 클라이언트 측 .js
파일은 이를 hydrate한다.
유니버셜 app.js
파일에 포함될 HTML컨텐츠를 포함한 App
컴포넌트가 있다고 가정해 보자. 서버, 클라이언트 측 React 모두 이 App
엘리먼트를 알고 있다.
서버 ipage.js
파일의 코드는 아래와 같다.
app.get('/', (req, res) => {
const app = ReactDOMServer.renderToString(<App />)
})
이제 App
은 렌더링 해야 하는 HTML컨텐츠를 생성하는 데 사용된다. 클라이언트 측 ipage.js
에서는 아래 코드를 통해 App
엘리먼트를 hydrate 해야 한다.
ReactDOM.hydrate(<App />, document.getElementById('root'))
완성된 예제는 여기에서 확인할 수 있다.