프론트엔드로 웹 페이지를 개발할 때 렌더링에 대해 고려해야 합니다. 요즘 시대 같은 경우 SSR(서버 사이드 렌더링)에 대해 많은 논의가 오가고 필요성이 부각되었습니다.
이번 포스팅에서는 어떤 식으로 사이드 렌더링이 발전했는지 알아보겠습니다.
이번 포스팅의 주된 내용은 다음과 같습니다. 포스팅의 내용이 매우 길 수 있습니다. 2개로 쪼개려고 했으나 흐름을 끊지 않기 위해 하나로 정리했습니다.
CSR과SSRSPA와MPASSR과SSGISR
사실 놀랍게도 SSR은 가장 먼저 등장한 개념입니다. 당시에는 웹 페이지에서 클라이언트와 많은 상호작용을 하던 시기가 아닙니다.
이는 자바스크립트의 등장배경을 보면 알 수 있습니다.
초창기 자바스크립트는 웹 페이지의 보조적인 기능을 수행하기 위해 한정적인 용도로 사용되었습니다. 이 시기에 대부분 로직은 주로 웹 서버에서 실행되었고 브라우저는 서버로부터 전달받은 HTML과 CSS 를 단순히 렌더링하는 수준이었습니다.
그 후로 클라이언트와 웹 페이지의 상호작용이 필요해지며 AJAX(Asynchronous JavaScript and XML)와 같은 기술이 도입되며 웹 페이지는 점차 복잡한 기능들을 담기 시작했습니다.
당시에는 웹 애플리케이션의 기능이 다양해지고 사용자가 늘어나면 동시에 서버도 확장해야했지만 클라우드의 개념이 부족했습니다. 그래서 서버를 확장하는 것은 매우 번거로웠고, 서버의 리소스를 사용하는 SSR방식보다는 그러한 부담을 클라이언트에게 넘기는 CSR방식이 인기를 끌게 됩니다.
클라이언트 측에서 JavaScript를 사용하여 동적으로 콘텐츠를 생성하는 방식

placeholder를 볼 수 있음placeholder자리에 넣어줌즉, CSR방식은 모든 HTML, CSS, JS를 다운받기까지 아무것도 웹 페이지에 렌더링을 할 수 없습니다.
서버에서 HTML을 생성하여 클라이언트로 전송하는 방식

SSR방식의 경우 HTML, CSS, JS가 모두 다운로드 받지 않아도 렌더링 자체는 먼저 일어나게 됩니다.
이는 SPA와 MPA의 특성과도 연결됩니다.
SPA는 말그대로 한개의 페이지로 구성된 애플리케이션입니다. 실제 웹 사이트는 여러개의 페이지가 있지만 한개의 페이지로 운영된다는 뜻입니다.

MPA는 말그대로 여러개의 페이지로 구성된 애플리케이션입니다.

일반적으로 SPA는 CSR과 많이쓰이고, MPA는 SSR과 많이쓰인다. 하지만 항상 그렇지는 않으니 같은 개념이라고 생각하면 절대 안된다. 특히 Next.js의 경우는 SPA와 SSR을 혼용해서 사용하고 있다.
이렇게 우리가 익숙하게 잘 사용하던 CSR, SPA의 시대가 흘러가다가 다른 문제점에 봉착합니다.
웹사이트가 너무 발전한 것입니다.
자바스크립트 파싱을 위해 CPU를 소비하는 시간이 눈에 띄게 증가합니다. 그래서 웹페이지 로딩도 평균 20초가 걸리고 모바일에서도 웹페이지에서 사용자가 상호작용을 하려면 15초 이상 대기를 해야했습니다.
SPA의 특성때문이기도 하지만 이는 과거에 비해 현재의 웹 애플리케이션이 다양한 작업을 처리하고 있기 때문입니다.
그래서 최초의 사용자에게 보여줄 페이지를 빠르게 서버에서 렌더링해 가져오는 것의 필요성이 생겼습니다.
그렇다면 재등장한 SSR은 어떠한 장단점을 갖고 있는지 확인해봅시다.
위에서 확인해본 이유와 같이 최초 페이지 진입이 CSR방식보다 훨씬 빠릅니다.
검색 엔진 로봇이 페이지에 진입을 하게 되면, 페이지가 HTML정보를 제공하여 로봇이 다운로드합니다. 하지만, 검색 엔진 로봇은 HTML을 다운로드만 하고 자바스크립트 코드는 실행하지 않습니다.
따라서, 최초 렌더링 작업이 서버에서 일어나는 서버 사이드 렌더링의 경우 HTML 코드를 서버에서 가공이 가능하기 때문에 검색 엔진 최적화에 대응하기가 용이합니다.
서버 사이드 렌더링을 사용한다고 검색 엔진 최적화가 좋다는 것이 아니라 대응이 용이한 것에 유의
CSR의 경우에는 페이지 콘텐츠가 API 요청에 의존하고, 각각 응답속도가 제각각이지만, SSR의 경우는 이러한 요청이 완전히 완료된 이후에 완성된 페이지를 제공하므로 CLS가 적음
→ 위의 경우에 SSR이 더 느릴 수 있음
물론, useEffect와 같이 클라이언트에서 컴포넌트가 마운트된 이후에 실행되는 코드의 경우는 발생할 수 있음
단순히 서버 사이드 렌더링을 고려해야지! 수준이 아니라 브라우저의 전역 객체인 window 와 같은 전역 객체를 건드리는 경우 오류가 발생함
이 뿐만이 아니라, 라이브러리를 사용하고 있다면, 라이브러리가 서버에 대한 고려가 반드시 되어있어야 하므로 이를 고려해야 함
CSR의 경우 최초 렌더링에서 지연이 나오는 경우 Suspense를 활용하여 로딩표시를 하여 사용자 경험을 해치지 않을 수 있지만, SSR의 경우 최초 렌더링에서 지연이 생기는 경우 사용자 경험이 안좋아짐
위에서 본 결과같이 서버사이드 렌더링의 경우 장단점이 명확하다. CSR과 SSR의 장점만을 합친 새로운 프레임워크 Next.js가 인기가 있는 이유이다.
Next.js는 SSR을 활용해 초기 페이지 로딩 속도를 개선하면서도, CSR처럼 클라이언트에서 동적 라우팅을 지원하여 SPA처럼 작동할 수 있습니다. 또한, SSG와 ISR을 통해 정적 페이지 생성을 최적화할 수도 있습니다.
앞선 SSR의 단점을 해결하고 CSR과 SSR의 장점을 합친 SSG방식의 Next.js가 등장하게 되었다.
그렇다면 SSG는 대체 무엇일까?
SSG방식은 HTML을 빌드 타임에 각 페이지별로 생성하고 해당 페이지로 요청이 올 경우 이미 생성된 HTML 문서를 반환한다.
서버사이드 렌더링과는 살짝 다르다.
서버 사이드 렌더링 : 요청이 올 때 마다 해당하는 HTML 문서를 그때 그때 생성하여 반환

위의 그림과 같이 SSG는 미리 정적 HTML파일을 갖고 있고, 이를 CDN 등에 배포하여 캐시로 저장한다. 여기서 가져온 HTML파일은 hydration과정을 통해 HTML파일에 JavaScript의 이벤트나 상태를 붙여 사용자와 상호작용을 할 수 있다. 즉, 단순한 정적 페이지가 아니란 것이다.
Hydration서버에서 렌더링된 HTML을 React가 takeover(인수)하여, 이벤트 핸들러 및 상태 관리를 추가하는 과정
→ SSR된 정적인 HTML에 JavaScript코드를 추가하여 React가 HTML코드를 다시 활성화하는 과정
그렇다면 무조건 SSG가 SSR보다 좋은가? 꼭 그렇지만은 않다. 모든 것은 장단점이 있듯이 사용목적에 맞게 사용하면 된다.
SSR의 특징인 사용자의 요청마다 HTML을 반환하는 것을 생각하면 어떤 경우에 사용하면 좋을지 알 수 있다.
SSG는 변경이 적은 컨텐츠에 유리하다. 변경이 잦게 되면 캐싱을 하는 의미가 없기 때문이다. 이러한 단점을 보완하기 위해 ISR(Incremental Static Regeneration)방식이 도입되었다.
ISR은 증분 정적 재생성이라고도 합니다. 또한, SSG와 SSR의 장점을 조합한 방식입니다.
SSG처럼 정적 페이지를 미리 생성하지만, 특정 주기마다 새로운 데이터를 반영할 수 있습니다. 즉, 빌드를 다시 하지 않고도 정적 페이지를 일정 시간마다 자동으로 갱신할 수 있습니다.
정리하면 정적 페이지 + 특정 주기마다 자동 업데이트하는 하이브리드 방식

ISR이 작동할 수 있는 이유는 revalidate속성 때문입니다.
공식문서의 코드를 참고해보겠습니다. 여기서 다 볼 필요는 없고 중요한 것은 아래의 revalidate입니다.
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
const res = await fetch('https://.../posts')
const posts = await res.json()
// Get the paths we want to pre-render based on posts
const paths = posts.map((post) => ({
params: { id: post.id },
}))
// We'll pre-render only these paths at build time.
// { fallback: blocking } will server-render pages
// on-demand if the path doesn't exist.
return { paths, fallback: 'blocking' }
}
// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 10 seconds
revalidate: 10, // In seconds
}
}
export default Blog
revalidate로 설정한 시간이 지나기 전까지는 캐시된 기존 페이지가 반복적으로 제공됩니다.위의 예시에는 revalidate 가 10초로 설정이 되어있습니다. 그렇다면 사용자가 접속하고 20초 후에 데이터를 요청했다면?
일단은 캐시된 페이지인 똑같은 페이지를 보여주고 백그라운드에서 새로운 페이지를 생성합니다.
다시 데이터의 요청이 있다면 새로운 페이지를 반환합니다.
주의해야 할 것은 오해사항으로 revalidate 시간마다 새로 업데이트를 진행한다고 알 수도 있는데 그렇지 않다는 점에 유의합시다.
이렇게, 초기 SSR부터시작된 웹 페이지가 SPA를 기반한 CSR로 넘어가고, 다시 SSR로 돌아와 SSG와 ISR까지 발전하는 과정을 담았습니다.
그 과정에서 각 방식에 대한 비교를 정리해보았습니다. 궁금한 점이나 수정사항이 있다면 댓글로 알려주시기 바랍니다.