SSR의 렌더링 과정 (CSR과는 뭐가 다를까?)

hyerin·2024년 10월 13일
post-thumbnail

우리가 흔하게 쓰는 리액트는 CSR기반 라이브러리이다. 하지만 렌더링 방식에는 CSR외에도 SSR이 있다. 리액트에 이미 익숙해진 프론트엔드 개발자라면 SSR을 처음 접했을 때 이에 대해 생소한 느낌을 받을 수 밖에 없을 것이다. 나 또한 SSR을 이해하는데 어려움을 겪었기에, SSR를 어려워하는 개발자들에게 조금이가도 도움을 주고자 정리했다. 과연 SSR은 우리가 아는 CSR과 어떤 점이 다르고, 어떤 식으로 동작하는지 자세하게 살펴보자!😁

1. SSR이란?

SSR는 서버 사이드 렌더링이다. 직역하면, '서버 측에서 렌더링한다' 라고 할 수 있다. 우선, 여기서 말하는 서버에 대해 알 필요가 있다. 서버라니, 프론트엔드 개발자에게는 너무 관련이 없는 단어라고 생각할 수도 있다. 리액트는 단순히 클라이언트 측에서 UI를 렌더링하는 역할을 하고, 서버는 백엔드가 만들어 주는데..? 🤔

SSR에서의 서버는 이 백엔드 서버랑은 다르다. 프론트엔드에서 구현한 별도의 서버이다. (이제부터 이 SSR에서 말하는 서버를 SSR서버라고 말하겠다. ) 하지만 순수하게 리액트만 쓰는 우리는 이 서버의 존재를 모를 수 밖에 없다. 왜냐면 리액트만 썼던 개발자라면(나...) 이 서버를 아직 본 적이 없기 때문이다!
이 SSR서버를 만들려면 node.js 기반의Express를 쓰면 된다. 이 외에도 SSR서버를 리액트 기반으로 쉽게 만들어주는 프레임워크를 사용하면 훨씬 쉽게 만들 수 있는데, 그 프레임워크가 바로 Next.js이다.

☀️ SSR를 구현하고 싶다면?

  • 순수 react (x)
  • react + express (o)
  • next.js (o)
    ...

경우에 따라 다르겠지만, 하나의 프레임워크로 쉽게 프론트 + 백을 구현할 수 있는 Next.js를 많은 개발자들이 선호하는 편인 것 같다. 하지만 SSR의 원리를 이해하기 위해, 이 포스팅에서는 express + react 방식으로 예제코드를 구현할 것이다.

이제부터, CSR, SSR의 렌더링 과정을 설명할 텐데 SSR과 CSR의 가장 큰 차이점이 1. 초기 렌더링 + 2. 데이터 패칭 이라는 것을 생각하면서 읽으면 이해가 수월해질지도 모른다!

2. CSR의 렌더링 과정

우선 CSR의 초기 렌더링에 대해 먼저 살펴보자!

(1) 빈 html과 js 번들을 받는다.

우선 브라우저는 url이 바뀌면, 서버에게 해당 페이지에 대한 리소스를 요청한다. 처음에 서버는 html과 js 번들 파일을 제공한다. 이 때 html은 프로젝트의 index.html 로 내용이 비어있는 파일이다. 실제로, 프로젝트 방끗은 CSR 방식의 리액트로 이루어져 있어서, 불러온 리소스를 보면 html의 내용이 비워져 있는 것을 볼 수 있다.

(2) js를 실행하여 dom을 생성하고 렌더링한다.

하지만 실제로 보는 페이지의 html은 다음과 같다. 처음에 받아온 html 과 다르게 내용이 채워진 것을 볼 수 있다. 이는 위와 같이 브라우저 받은 js를 파싱하고 돔을 생성해 렌더링한 결과물이다.

즉, 우리가 위에 완성된 화면을 보려면 브라우저가 서버로부터 받은 js파일을 해석 + 파싱하고, 돔을 렌더링하는 과정을 거칠때까지 기다려야 한다.

(3) 데이터 패칭이 필요한 경우 클라이언트에서 직접 수행한다.

만약, 렌더링할 페이지가 서버에서 데이터를 불러와야 하는 페이지라면 어떨까? 이렇게 되면 사용자가 최종 화면을 보는 시간은 더 늦어지게 될 것이다. 보통 클라이언트에서 해당 컴포넌트를 1차적으로 렌더링 한후, 필요한 데이터를 패칭하는 작업을 수행하고, 다시 해당 데이터를 포함한 dom을 다시 렌더링 하기 때문이다.

즉, 모든 데이터가 다 로드된 페이지를 보기 위해서는 브라우저가 해당 과정을 모두 진행할 때까지 기다려야 한다. 이 과정이 조금 비효율적이게 보일 수도 있다. 사용자는 빨리 화면을 보고 싶어하니까, 그냥 처음부터 완성된 html 을 받고 렌더링 하는 것이 아무리봐도 빠르고 효율적이기 않은가? 이 내용이 이해가 간다면 SSR의 개념을 반은 이해한 것이라고 볼 수 있다.

3. SSR의 렌더링 과정

SSR의 초기 렌더링은 위에서 설명했던 것처럼, 클라이언트(브라우저 + 리액트) 가 아닌, SSR서버에서 이루어진다. (이 서버는 리액트외에 다른 도구들로 만든 것이며, 여기서는 express를 예시로 들겠다.) SSR의 초기 렌더링 방식은 크게 2단계로 이루어진다.

(1) SSR서버에서 정적 페이지를 렌더링한다.

SSR의 초기 렌더링의 1단계는 다음과 같다. 브라우저에서 페이지를 요청하면 브라우저는 리소스를 서버에 요청한다. 서버는 html 템플릿을 완성시킨뒤 브라우저에 전달한다. 이 때 브라우저는, SSR 서버로부터 받은 정적인 html 을 렌더링한다. 이때 렌더링 된 페이지는, 클릭 이벤트 같은 상호작용이 불가능한 정적인 페이지이다. 제대로 된 페이지 렌더링은 아직 안됐다고 볼 수 있다.

상호작용이 아직 불가능한 이유는, 브라우저에서 서버가 전달한 정적인 html을 렌더링한 것이기 때문이다. 이를 이해하기 위해서 서버가 이 html을 만드는 과정을 더 자세히 설명해보겠다.

✅ 서버가 html 을 만드는 과정

(1) 특정 경로에 대한 함수가 실행됨. 해당 페이지에서 필요한 템플릿(컴포넌트) 를 결정함.

SSR서버가 브라우저로부터 필요한 페이지의 리소스를 요청받으면, 렌더링할 컴포넌트를 찾는다. 그리고 해당 컴포넌트를 renderToString 이라는 함수를 정적인 HTML 문자열로 변환한다. 이게 가능하려면 필요한 컴포넌트에 대한 코드가 이미 파일에 있어야 하고, 서버에서 이 파일을 접근할 수 있어야 한다.

//App.js 
//렌더링할 컴포넌트가 있는 파일이다.
<div movies={movies}>
  <div>이번주 핫 뮤비</div>
  <MovieList movies={movies}/>
</div>
//express
app.get('*', (req, res) => {
  // 특정 경로에 맞는 HTML을 생성하기 위해 렌더링할 컴포넌트를 결정
  const html = ReactDOMServer.renderToString(<App />);

앞서 말했듯이, 해당 문자열은 html로 이벤트같은 상호작용은 불가능한 상태로 만들어진다. 어쨌든 보이는 UI상은 문제가 없는 상태이므로, 이 상태로 그대로 브라우저에게 전달이 된다.

//renderToString로 SSR서버에서 렌더링한 html
`<div movies={movies}>
  <div>이번주 핫 뮤비</div>
  <MovieList movies={movies}/>
</div>`

(2) SSR서버가 데이터 패칭을 직접 수행하고 html 스트링에 데이터 삽입

만약 이 과정에서 데이터 패칭이 필요한 경우는 어떻게 될까? 데이터 패칭도 SSR서버의 담당이다. 필요한 데이터 또한 서버에서 직접 json으로 받아 html 에 넣어준 뒤, html을 완성한다.

이런 데이터 패칭 과정 또한 CSR과 가장 차이가 나는 부분이다. CSR의 경우 브라우저가 필요한 경우 직접 데이터 패칭을 수행한다. 하지만 SSR은 서버가 직접 데이터 패칭을 하기 때문에, 브라우저가 데이터 패칭하는 시간을 아낄 수 있다는 장점이 있다.

데이터를 받아온 뒤에는, 위에서 만든 html string에다가 데이터를 삽입한다. 이로서 html은 데이터가 들어간 완전한 뷰를 가진 html이 된다. 이 html을 전체 템플릿에다가 잘 넣고, 이것을 send 메서드로 보낼 준비를 하면 된다. 이렇게 되면 / 라우트로 요청이 올 때 SSR 서버는 해당 html 을 보낼 것이다.

router.use("/", async (_, res) => {
  ...
	//외부 서버에서 받아온 json데이터
   const movies = await getMovieList();
    //html에 해당 데이터를 넣어줍니다.
   const html = ReactDOMServer.renderToString(<App movies={movies}/>);
  
  
//만든 html 결과물을 보냅니다.
res.send(`
  <html>
    <body>
      <div id="root">${html}</div>
      <script src="/bundle.js"></script>
    </body>
  </html>
`);
	res.send(html);

SSR 은 초기 렌더링이 빠르다.

SSR 은 초기 렌더링이 빠른데, 왜냐하면 처음부터 서버가 데이터가 들어간 완성된 html 브라우저에게 보내주기 때문에, 브라우저는 데이터 패칭, dom 생성  에 대한 부담 없이 렌더링만 실행하면 되기 때문이다. 이것이 SSR이 초기 렌더링에서 속도적 이점을 가지고 있는 이유이다. 다시 한번 CSR을 보면서 비교해보자!

🌟 CSR

CSR은 브라우저가 js 파일을 읽고 해석하고, dom을 그리고, 데이터 패칭이 필요한 경우 패칭을 다 끝내고 다시 돔을 업데이트 해서 렌더링을 해야 사용자는 최종 화면을 볼 수 있게 된다.

🌟 SSR

하지만 SSR은 위 복잡한 과정을 SSR서버에서 처리해주기 때문에, 브라우저는 완성된 html을 렌더링만 하면 되서 빠르다.

또한 SSR서버가 만든 정적인 html 은 크롤러가 쉽게 크롤링이 가능하기 때문에, SEO(검색 엔진 최적화)에서도 SSR이 유리하다.

(2) 리액트에서 하이드레이션을 수행한다.

1단계에서 완성한 페이지는 상호작용이 안되는 50% 완성도의 페이지이다. 100%로 상호작용이 가능한 페이지로 만들려면, 꼭 거쳐야 하는 과정이 바로 하이드레이션 이다.

리액트는 1단계의 정적 html이 완성된 이후, 이미 렌더링 된 html리액트 컴포넌트를 비교하여 이벤트 처리, 상태 관리와 같은 작업을 진행한다. 즉, 하이드레이션은 정적인 html에 이벤트, 상태 관리와 같은 상호작용을 가능하게 하는 작업을 말한다. 이 것이 마치 건조한 곳에 물을 뿌리는 작업 같다고 하여 hydration(수분공급) 이라는 이름이 붙은 것이다.

사실 상호작용에 관한 기능 (상태 관리, 이벤트 주입 등..) 은 이미 SSR서버가 할 수 없는 영역이다. 애초에 express 는 해당 메서드나 상태 관리에 대한 개념이 존재하지 않기 때문이다. 그래서 SSR 서버 렌더링을 마친 후에야 react-dom 이 하이드레이션 작업을 진행한다.

위에서 SSR 서버에서 보내준 html 에 js 번들이 포함된 이후도, 정적 페이지가 렌더링 된 이후에 하이드레이션을 클라이언트 측에서 실행시키기 위함이다. 하이드레이션이 작동하는 코드는 클라이언트(리액트 /bundle.js) 에 존재하기 때문이다.

<script src="/bundle.js"></script>

위 코드가 정적 html 에 포함되어 있지 않다면, 하이드레이션 과정이 생략되어 상호작용이 불가능한 페이지가 렌더링될 것이다.

✅ 하이드레이션 과정

그렇다면 하이드레이션은 어떻게 수행될까? 우선 하이드레이션은 hydrateRoot 라는 리액트 함수를 사용한다. hydrateRoot 는 위에서 말한 렌더링된 html 과 클라이언트의 컴포넌트를 비교하는 작업을 수행한다.

hydrateRoot(
  document.getElementById("root"),
  <App popularMovies={initialData.movies} />
);

클라이언트의 컴포넌트를 <App popularMovies={initialData.movies} /> 를 말한다.
initialData.movies는 서버에서 직접 전달받은 데이터로, SSR서버에서 html에 해당 데이터를 삽입하여 전달한다. 클라이언트는 해당 데이터를 가져와, 자체적으로 비교하기 위한 dom을 그린다.

여기서 이미 만들어진 html와 클라이언트가 자체적으로 만드는 가상dom을 비교하면서, 기존 HTML에 이벤트 리스너가 연결한다.

이 때, 두개의 렌더링된 구조는 완전히 일치해야 하며, 일치하지 않으면 react-dom 에서는 두개의 구조가 일치하지 않아, 하이드레이션 과정을 수행할 수 없다는 에러가 뜬다.

이는 어찌보면 당연한것이, react-dom 은 단순히 이벤트를 주입하고, 하이드레이션을 하기 위해 비교하는 것인데, 애초에 구조가 틀리면 작업 진행이 불가능하기 때문이다. 문제가 없이 두 dom이 일치한다면, 렌더링 된 dom에 필요한 곳에 이벤트를 주입하고, 상태를 넣어 실제 상호작용할 수 있는 생생한 dom으로 변화시킨다.

4.마무리

여기까지 SSR의 렌더링 과정을 알아보았다. 무턱대로 SSR이 가능한 next.js 를 쓰는 것보다는 SSR를 직접 구현하고 동작원리를 이해하니, SSR의 장단점이 명확하게 보이는 순간이 있었다.  이제 CSR , SSR 이 필요할 때 쓸 수 있을 것 같다.

profile
글쓰기의 시작은 나를 위해, 끝은 읽는 당신을 위해

3개의 댓글

comment-user-thumbnail
2024년 10월 14일

정말 구체적인 내용이 적혀있네요. 덕분에 이해가 수월했습니다. 감사합니다

1개의 답글