2025년 05월 11일
React는 클라이언트 사이드 라이브러리로 시작했지만, 점차 SSR 즉, 서버 사이드 렌더링에 대한 요구가 증가하게 됩니다. React는 이러한 요구에 대응하기 위해 서버 사이드 렌더링을 지원하는 기능을 제공합니다.
이번 포스트에서는 서버 사이드 렌더링의 수요가 늘어난 이유와 함께 이를 어떻게 활용할 수 있을지를 살펴보도록 하겠습니다.
CSR이라고 부르는 클라이언트 사이드 렌더링은 가장 보편적으로 사용하는 웹 접근 방식이지만, 이 방식은 웹이 계속 발전함에 따라 몇 가지 한계점을 겪게 됩니다.
클라이언트 사이드 렌더링의 주요 한계 중 하나는 일부 검색 엔진의 크롤러가 자바스크립트를 실행하지 않으며, 자바스크립트를 실행하더라도 예상대로 실행되지 않을 수 있기에 콘텐츠를 올바르게 색인하지 못할 수 있다는 것입니다.
이로 인해 메타 태그나 콘텐츠 정보가 노출되지 않아 SEO에 불리합니다. 특히, 블로그, 뉴스, 쇼핑몰처럼 검색 유입이 중요한 서비스라면 CSR은 큰 단점이 될 수 있습니다.
클라이언트에서 렌더링되는 애플리케이션은 느린 네트워크나 낮은 성능 기기에서 성능 문제를 겪을 수 있습니다. 콘텐츠를 렌더링하기 전에 자바스크립트를 다운로드하고, 구문 분석과 실행까지 해야하기 때문에 콘텐츠 렌더링이 상당히 지연될 수 있습니다.
인터렉티브 가능 시간은 사용자 참여율과 이탈률에 직접적으로 영향을 미치기 때문에 매우 중요한 측정 지표입니다. 즉, 애플리케이션을 읽어 들이는 시간이 매우 오래 걸리면 사용자가 페이지를 떠날 수 있고, 이는 검색 엔진의 페이지 순위에 부정적 영향을 미칠 수 있습니다.
위와 같은 문제들 때문에 서버 사이드 렌더링 또는 정적 사이트 생성이 더 신뢰할 수 있는 대안이 될 수 있습니다.
SSR은 말 그대로 React 앱을 서버에서 먼저 렌더링하여 완성된 HTML을 브라우저에 전달하는 방식입니다. 이렇게 되면 사용자는 페이지에 처음 접근했을 때 렌더링된 콘텐츠를 바로 볼 수 있고, 검색 엔진에서도 콘텐츠를 올바르게 색인할 수 있습니다.
또한, 초기 로딩 시간이 줄어들어 사용자 경험이 향상됩니다.
CSR과 SSR의 차이점을 살펴보도록 하겠습니다.
HTML 로딩(전체 UI, 서버에서 가져온 데이터 포함)
CSR과 다르게 SSR은 이미 필요한 데이터를 가져와서 서버에서 렌더링 했기 때문에 처음 페이지를 로딩하는 순간부터 사용자가 원하는 내용을 볼 수 있습니다.
페이지가 최초로 렌더링된 후 사용자가 인터페이스에 익숙해지는 동안 관련 자바스크립트를 읽어 들이는데, 이 과정에서 하이드레이션(hydration) 이라는 프로세스가 동작합니다.
이러한 프로세스 아래 사용자는 추가 로딩을 기다릴 필요 없이 즉시 애플리케이션을 사용할 수 있게 만드는 장점이 있습니다.
또한 클라이언트의 브라우저로 다운로드되었을 때 발생할 수 있는 CSRF 공격 또한 방지할 수 있습니다.
보안적인 측면에서도 큰 장점을 가질 수 있는 것입니다.
정리하자면 아래와 같은 장점을 가질 수 있습니다.
다만 단점도 있습니다.
서버에서 렌더링된 HTML은 정적이며 자바스크립트를 읽어들이지 않은 상태이기 때문에 상호 작용 지원이 부족합니다.
이벤트 리스너와 같은 동적 기능은 포함되지 않습니다. 이를 포함하기 위해선 필요한 자바스크립트를 정적 HTML에 추가해줘야 합니다.
이를 하이드레이션 이라고 부릅니다.
하이드레이선은 서버에서 생성되어 클라이언트로 전송되는 정적 HTML에 이벤트 리스너와 여러 자바스크립트 기능을 추가하는 프로세스를 의미합니다.
하이드레이션의 목적은 브라우저가 서버 렌더링 애플리케이션을 읽어 들인 후 여기에 상호 작용을 추가해서 사용자에게 빠르고 원활한 경험을 제공하는 것입니다.
즉, 서버에서 렌더링된 정적 HTML에 자바스크립트를 추가하여 상호 작용을 활성화하는 과정이라 볼 수 있습니다.
간단하게 SSR 예시를 살펴보겠습니다.
React에서는 renderToString 메서드를 사용하여 서버에서 렌더링된 HTML을 문자열로 반환할 수 있습니다.
스트리밍 렌더링을 사용하기 위해서는 renderToPipeableStream 메서드도 사용할 수 있지만 renderToString 메서드로 예시를 구현해보겠습니다.
먼저 서버쪽 코드를 작성해보면 아래와 같습니다.
import express from 'express'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from './App'
const server = express()
server.use(express.static('public'))
server.get('*', (req, res) => {
const html = ReactDOMServer.renderToString(<App />)
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR Example</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`)
})
server.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000')
})
그리고 클라이언트 쪽에서는 다음과 같이 하이드레이션을 수행하여 상호 작용을 활성화 합니다.
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import App from './App'
hydrateRoot(document.getElementById('root'), <App />)
;``
결론적으로는 위와 같이 직접 SSR을 구현하는 것보다는 Next.js 또는 Remix 같은 프레임워크를 사용하는 것을 권장합니다. 직접 SSR을 구현하는 것은 굉장히 어렵고 시간도 많이 듭니다.
서버 렌더링을 구현하려면 비동기 데이터 페칭, 코드 스플리팅 외 리액트 사이클 관리 등 매우 많은 부분을 고려해야 합니다. 위와 같이 직접 구현하는 과정보다 앞서 언급된 프레임워크를 사용하는 것이 훨씬 효율적일 수 있습니다. 마침 Next.js를 언급했으니 SSG에 대해서도 살펴보겠습니다. 사실 Next.js는 기본적으로 정적 사이트 생성 즉, SSG를 기반으로 동작합니다.
간략하게 렌더링 방식을 정리하자면 아래와 같습니다. Next.js는 빌드 시 HTML을 미리 생성하여 전달해줍니다. 그래서 자주 바뀌지 않는 페이지에 대해서는 SSG가 적합할 수 있습니다.
SSR은 요청 시 서버에서 렌더링 된다면 SSG는 빌드 시점에 미리 렌더링을 하여 전달해주는 것이 가장 큰 차이라고 볼 수 있습니다. 그래서 초기 로딩 속도도 다소 빠르지만 동적 데이터 반영이 어려워 유연성 측면에서는 조금 떨어질 수 있습니다.
하지만 세 개 다 뭐가 정답이다. 어떤 걸 사용해야 한다는 건 없습니다. 개발자가 프로덕트의 특성에 따라 적합한 방식을 선택하는 것이 중요합니다. 그래서 이 차이를 알고 적절한 방식을 선택하는 것이 좋은 개발자가 되기 위한 중요한 요소라 생각했습니다.
간단히 차이점을 정리해보면 아래와 같습니다.
[CSR]
요청 → index.html + JS → 브라우저에서 렌더링
[SSR]
요청 → 서버에서 HTML 생성 → 브라우저에 전달 → Hydration
[SSG]
빌드 시 HTML 미리 생성 → 요청 시 정적 파일 전달 → Hydration
SSR과 CSR을 구체적으로 비교해보며 학습해본건 이번이 처음인데 이렇게 정리하니 조금 머릿속에서 어떻게 구현해야할 지 감이 오는 것 같습니다. 특히 사내 프로젝트로 Next.js를 쓰고 있기 때문에 더더욱 이런 개념을 잘 알고 있어야 할 것 같습니다.
거기에 요즘 V0 , ChatGPT , Claude 등 프론트엔드 코드를 짜줄 때 Next.js가 보편적으로 사용되는 것 같습니다.
단순히 AI가 짜주는 코드를 적용하기 보단 어떤 방식이 내가 만드는 프로덕트에 더 적합한지 기술적으로 고민하는 것이 개발자의 덕목이라 생각됩니다.
늘 끊임없이 고민하고 배우고 성장하는 마음가짐을 유지하며 다음 포스트도 좋은 주제로 준비해보겠습니다.