클라이언트 사이드 렌더링
Client-Side Rendering
클라이언트 사이드 렌더링에서 서버는 뼈대가 되는 최소한의 HTML만 렌더링한다. 로직, 데이터, 템플릿, 라우팅을 통한 페이지 전환은 모두 브라우저에서 실행되는 JavaScript에 의해 처리된다. CSR은 SPA개발에 거의 필수적으로 쓰이며 설치해 사용하는 앱과 웹 사이트의 경계를 흐릿하게 해 주었다.
다른 패턴들이 제공하는 장점을 더 잘 이해하기 위해 먼저 클라이언트 사이드 렌더링의 장점과 단점에 대해 자세히 알아보자.
클라이언트 사이드 렌더링 - 기본 구조
아래 예제는 React를 사용하여 현재 시간을 화면에 업데이트하여 보여주는 예제이다.
HTML:
<div id="root"></div>
JS:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
)
ReactDOM.render(element, document.getElementById('root'))
}
setInterval(tick, 1000)
HTML은 div 태그 하나만 포함하고 있고. 내용을 표현하는것과 업데이트하는 것은 모두 JavaScript에서 처리한다. 서버를 통하여 처리하는것은 따로 없으며, 렌더링된 HTML은 그대로 그 자리에서 업데이트 된다. 위의 예시에서 표현되는 시간은 API를 통해 환율이나 주가와 같은 실시간 데이터로 변경될 수 있고. 페이지 새로고침이나 서버를 통하지 않고 표현될 수 있다.
JavaScript번들과 성능
이미지를 보여준다던가 데이터 스토어로부터 데이터를 표현한다던가, 이벤트를 처리하는 등으로 페이지의 복잡도가 증가하는 만큼 페이지를 렌더링하기 위해 필요한 JavaScript의 크기 또한 증가한다. 클라이언트 사이드 렌더링에서는 JavaScript 번들의 크기가 클 수록 FCP와 TTI도 함께 증가하게 된다.
위의 그림에서 표현하는 것 처럼 bundle.js가 증가함에 따라 그 만큼 FCP와 TTI가 밀려나고 있다. 이 때문에 사용자는 FP와 FCP의 모든 구간에서 빈 화면을 보게 된다.
클라이언트 사이드 React의 장점과 단점
React의 모든 앱의 로직들은 클라이언트 사이드에서 실행되고 서버와는 API호출을 통해 데이터를 조회하거나 저장하게 된다. 거의 모든 UI 또한 클라이언트에서 만들어진다. 전체 웹 앱은 첫 번째 요청에서 다운로드 된다. 사용자가 링크를 클릭할 때 페이지 렌더를 위해서 서버에 별도의 처리를 하지 않는다. 화면 또는 데이터를 변화시키기 위한 코드들은 모두 클라이언트에서 실행되는 것이다.
클라이언트 사이드 렌더링은 SPA를 만들어 사용자가 페이지를 새로고침 하지 않아도 페이지를 탐색할 수 있어 좋은 사용자 경험을 줄 수 있다. 뷰를 변경하기 위한 데이터의 처리량이 제한되어 있기 때문에 페이지간 라우팅 속도가 빨라져 페이지가 좀 더 빨리 반응하는 것 처럼 보인다. 클라이언트 사이드 렌더링은 클라이언트와 서버 코드를 조금 더 명확하게 분리하여 관리할 수 있게 한다.
이런 장점들을 가지고 있지만. 클라이언트 사이드 렌더링은 몇가지 주의점이 있다.
- SEO 고려사항: 대부분의 웹 크롤러들은 웹사이트의 서버 렌더링을 직접적으로 읽어들인다. 클라이언트 사이드 렌더링의 과정이 복잡해져 API요청 등으로 네트워크 요청 단계가 늘어날 경우. 의미있는 내용의 렌더링이 늦어져 크롤러가 인덱싱할 수 없게 될 수 있다. 크롤러는 JavaScript의 동작을 어느정도 감지하지만 한계가 있다. 따라서 이 경우 SEO최적화를 위한 추가적인 작업을 해 주어야 한다.
- 성능: 클라이언트 사이드 렌더링을 사용하면 서버를 여러번 거치는 게 아니기 때문에 인터렉션 중에 응답 시간이 향상될 수 있다. 하지만 브라우저가 처음 컨텐츠를 렌더링 할 때 JavaScript를 로드하고 처리하기까지 기다려야 한다. 따라서 사용자는 최초 페이지 로드 시 까지 약간의 랙을 경험할 수 있다. 이는 JavaScript번들 크기가 증가할수록, 클라이언트의 성능이 부족할수록 사용자 경험에 악영향을 줄 수 있다.
- 유지보수성: 어떤 코드들은 클라이언트 측과 서버 측 양쪽에서 다른언어로 각각 구현해야 할 수 있다. 다시 말해 비즈니스 로직을 깔끔히 분리하는게 어렵다. 이런 상황은 유효성 검사, 통화 표기, 날짜 표현등의 로직에서 일어날 수 있다.
- 데이터 패칭: 클라이언트 사이드 렌더링에서 데이터 패칭은 주로 이벤트에서 시작된다. 페이지가 처음엔 아무 데이터 없이 로드될 수 있지만 데이터는 페이지 로드나 버튼 클릭에서 이뤄지는 API호출를 통해 받게 된다. 이런 데이터의 크기가 커지면 앱의 로드 시간이나 인터렉션 가능해지기까지의 시간을 증가시킨다.
이런 고려사항들의 중요성은 앱 마다 다를 수 있다. 개발자들은 상호 작용 시간을 손해보지 않고 사이트를 SEO최적화할 수 있는 방법을 찾곤 한다. 성능 기준에 대한 우선순위는 앱의 요구사항에 따라 다를 수 있다. 때로는 완전 다른 패턴을 고려하기 보다 클라이언트 사이드 렌더링에 약간의 개량을 하는 것으로도 충분할 수 있다.
클라이언트 사이드 렌더링의 성능
클라이언트 사이드 렌더링의 성능은 JavaScript번들 사이즈와 반비례하기 때문에 JavaScript코드를 성능을 고려하여 구조화해야 한다. 아래 항목들을 고려하면 도움이 될 수 있다.
- 번들 크기를 관리하자: 초기 페이지 로드 속도를 위해 빠듯하면서도 합리적인 JavaScript번들의 크기를 결정하자. minified되고 gzipped되었을 때 최소 100~170KB 정도로 잡는 것이 좋은 시작점이다. 코드가 이보다 더 늘어나면 필요에 따라 추가로 받도록 구성해야 한다.
- 프리로딩: 페이지의 로드에 필수적인 리소스들에 이 기법을 적용할 수 있다. 이런 필수 리소스들 중에는 JavaScript가 포함될 수 있고
<head>
태그 안에 아래의 지시자를 사용하여 처리할 수 있다.
이렇게 되면 브라우저가 페이지 렌더링 메커니즘을 시작하기 전에<link rel="preload" as="script" href="critical.js">
critical.js
를 로드하도록 할 수 있다. 따라서 이 스크립트는 조금 더 일찍 시작될 수 있고 페이지 렌더링 메커니즘을 방해하지 않으므로 성능 향상에 도움을 줄 수 있다. - 지연로딩: 지연로딩을 사용하여 리소스들이 꼭 필요하지 않고 나중에 필요해질 때 다운로드받도록 할 수 있다. 이 방법을 사용하면 초기에 받아야 하는 코드량이 줄어들게 되어 최초 페이지 로드 타임이 감소한다. 예를 들어 채팅 위젯 컴포넌트 같은 것은 최초 페이지 로드 타임에 필요하지 않을 수 있어. 사용하려 할 때 받게 하는것이다.
- 코드 스플리팅: 번들 크기가 증가하는것을 예방하기 위해 코드를 나눌 수 있다. 코드 스플리팅은 Webpack과 같은 모듈 번들러에 의해 지원되며 동적으로 로드 가능한 여러 번들을 만들어 낼 수 있다. 코드 스플리팅은 JavaScript코드를 나누는 것 또한 가능하다.
- 서비스워커를 이용한 앱 쉘 캐싱: 이 기법은 앱내 최소한의 UI를 구성하는 HTML, CSS, JavaScript를 캐싱하는 것이다. 서비스 워커를 이용하면 오프라인에서도 이 캐시를 이용할 수 있다. 껍데기 UI는 미리 보이고. 나머지 필요 데이터는 점진적으로 불러올 수 있어. 네이티브 앱을 이용하는것과 비슷한 사용자 경험을 제공할 수 있다.
클라이언트 사이드 렌더링은 이런 기술들을 통해 적절한 FCP와 TTI을 가진 SPA앱을 제공할 수 있다. 다음으로 서버 사이드 렌더링 중 다른 패턴들에 대해서 알아보자.