이번 포스트에서는 프론트엔드의 대표적인 렌더링 방식인 CSR(Client Side Renering)
과 SSR(Server Side Rendering)
에 특징과 어떤 시기에 사용해야 가장 좋은지에 알아보겠다.
본격적인 렌더링 방식을 알아보기 전에 웹어플리케이션을 페이지 구성 방식에 따라 크게 두가지로 나누어볼 수 있다.
페이지 변경 없이 하나의 페이지 안에서 어플리케이션이 구동되는 방식 - ex) React
, Vue
, Angular
등
사용자에 상호작용에 따라 여러개의 페이지가 제공되는 웹어플리케이션 - ex) php
, JSP
등
이러한 웹어플리케이션은 각자의 특징때문에 과거에는 각자에게 필요한 렌더링 방식을 사용해야했다. 나중에 제대로 설명할 것이지만 렌더링 방식은 크게 3가지로 아래와 같은데,
CSR - Client Side Rendering
: 클라이언트 측에서 렌더링을 하는 방식SSR - Server Side Rendering
: 서버 측에서 렌더링을 하는 방식SSG - Static Site Generation
: 빌드 타임에 미리 리소스들을 정적으로 생성하는 방식이러한 렌더링 방식과 결합시켜 아래와 같은 구성으로 웹어플리케이션을 구성하였다.
App 구성 #1 | App 구성 #2 | |
---|---|---|
페이지 구성 방식 | SPA | MPA |
렌더링 방식 | CSR | SSR |
하지만, 최근들어 다양한 기술들과 프레임워크가 등장하면서부터는 SPA
상에서도 SSR
을 구성할 수 있게되었다. 지금까지 웹어플리케이션의 페이지 구성방식에 대해 알아봤다면 이제부터는 정말로 렌더링 방식에 대해 알아볼 차례이다.
CSR
은 클라이언트 측에서 리소스를 다운로드 받아 직접 렌더링에 관여하는 방식인데 다음의 특징들을 가진다.
대표적으로 React
라이브러리를 이용한다면 아래와 같이 useEffect 훅을 이용해서 렌더링에 필요한 데이터를 백엔드로부터 불러오는 것이 대표적인 예이다.
useEffect(() => {
axios
.get('https://worldtimeapi.org/api/ip')
.then((res) => {
setDateTime(res.data.datetime);
})
.catch((error) => console.error(error));
}, []);
SSR
은 CSR
과 달리 서버가 HTML 뼈대와 JS 링크를 주는 것에 그치지 않고 직접 HTML페이지와 관련된 JS파일을 실행시켜 완성된 HTML과 브라우저 측에서 실행할 JS 파일링크를 넘기는 방식으로 렌더링을 진행한다. 이러한 SSR
방식은 다음의 특징들을 가진다.
초기 렌더링시 HTML 뼈대만이 아니라 렌더링이 완료된 HTML과 이후 실행될 자바스크립트 코드가 제공되어 웹 크롤러 입장에서는 더 상세하게 인덱싱이 가능하여 SEO(검색엔진 최적화)
에 매우 용이하다.
이미 렌더링이 완료된 상태로 페이지가 제공되기 때문에 초기 구동속도가 빠르다. 다만, HTML과 관련된 JS 로직들이 모두 연결되기 전까지는 사용자의 상호작용이 불가능하다. 이러한 경우 TTV(Time To View) ≠ Time To Interact
인 상황이라고도 한다.
Hydration
이라고 한다.또한 서버가 매 요청시 필요한 리소스를 직접 만들어야하기 때문에 서버에 과부하가 생길 수 있다. 이러한 경우, 사용자가 페이지 최초 진입시 페이지가 보여지는데 오랜시간이 걸릴수도 있다.
CSR과 SSR의 특징들을 정리하여 장점과 단점으로 분리하면 다음과 같다.
CSR | SSR | |
---|---|---|
장점 | 화면 깜빡임(≠ 화면전환)이 없음 초기 로딩 이후 구동 속도 빠름 TTV와 TTI 사이 간극이 없음 TTV와 TTI 사이 간극이 없음 서버 부하 분산 | 초기 구동 속도가 빠름 SEO에 유리함 |
단점 | 초기 로딩 속도 느림 SEO에 불리함 | 화면 깜빡임이 존재 TTV와 TTI 사이 간극 존재 서버 부하가 존재 |
위에서 살펴보았던 CSR 단점들은 다음의 해결책들을 통해 어느정도 해소될 수 있다.
Code-splitting
- SPA는 초기 실행시에 필요한 웹 리소스를 다운받는 특징이 있다. 이를 해소하기 위해 일부 리소스들에 대해서 lazy loading
을 적용하여 한번에 다운로드 받지 않도록 하는 방법이 있다. 리액트에서는 React.lazy
와 Suspense
컴포넌트를 활용해 일부 리소스를 필요할 때 import 시킴으로써 최적화할 수 있다.const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
Code splitting
에 대해서 자세하게 알아보고싶다면 아래를 참고하길 바란다. Code-Splitting(코드 스플릿팅)Tree-shaking
- 웹어플리케이션을 개발하다보면 다양한 모듈들을 설치해 사용하게 될텐데 이중에서 사용되지 않은 모듈들을 최대한 배제하는 기법이다. 나무를 흔들어서 죽은 나뭇잎들을 떨어뜨리는 데에서 유래 되었다고 한다.import * as util from '../utilFile';
대표적으로 특정 파일에서 모듈을 위와 같은 방식으로 import
하게 될 경우, 사용하지 않는 모듈들도 같이 import
하게될텐데 이런식으로 불필요한 리소스가 누적되면 어플리케이션을 build
하면서 만들어지는 번들의 크기가 매우 커지게된다. 이로 인해 리소스를 로딩하는 시간을 지연시킬 수 있다. 이에 대한 자세한 설명은 아래를 참고하면 좋을것 같다. 웹 성능 최적화를 위한 Tree Shaking 소개SEO 개선
SEO
가 잘 안되던 기존의 문제점을 해결할 수 있다. 이처럼 페이지가 미리 렌더링되도록 해놓는 작업을 Pre-rendering
이라고 한다.SSR/SSG를 부분 도입
React
- NextJS
, Gatsby
Angular
- Universal
Vue
- Nuxt
대표적으로 NextJS
를 이용해 SSR를 할 경우 다음과 같은 방식으로 특정 페이지에 대하여 SSR을 진행할 수 있다. getServerSideProps
라는 함수가 프론트엔드 서버측에서 미리 실행되어 data를 패치한뒤 이를 페이지 컴포넌트의 props로 넘기는 방식이다.
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// Fetch data from external API
const res = await fetch(`https://.../data`)
const data = await res.json()
// Pass data to the page via props
return { props: { data } }
}
export default Page
다만, 프레임워크를 사용하게되면 당여히 프레임워크에 대한 이해가 필요하여 러닝커브가 존재하고 코드 복잡도도 상승하게 된다는 단점이 있다.
지금까지 렌더링하는 방식들의 특징들과 도입방법들에 대해서 알아보았다. 각각의 장단점들이 있으니 상황에 맞게 사용하는 것이 중요하다. 각 렌더링 방식에 대하여 사용해야될 경우를 특정해보면 다음과 같다.
CSR
이 선호되는 경우
SSG/SSR
이 선호되는 경우
SSR
SSG
Universal(CSR + SSR)
이 선호되는 경우
SSR
CSR
SEO
최적화 필요 ⇒ SSR
위의 내용을 참고하되 정해진 정답은 없으니 프로젝트의 상황에 맞게 렌더링 방식을 선택하는 것이 좋겠다.
Code-splitting
Tree-shaking
SEO(Search Engine Optimization)
Hydration
Client-Side v/s Server-Side Rendering: What to Choose When? - DZone Web Dev