회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
첫 개발을 클라이언트 사이드 렌더링(CSR)으로 시작해서 CSR에 익숙해지면, 브라우저 너머에 웹서버가 있다는 사실을 잊어버릴 수 있다. 널리 알려진 상식일 수도 있지만, 정확히 짚고 넘어가고 싶은 것이 있다. 우리가 보는 웹 브라우저 화면은 DOM이라는 트리 형태의 그래프라는 사실이다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
첫 개발을 클라이언트 사이드 렌더링(CSR)으로 시작해서 CSR에 익숙해지면, 브라우저 너머에 웹서버가 있다는 사실을 잊어버릴 수 있다. 널리 알려진 상식일 수도 있지만, 정확히 짚고 넘어가고 싶은 것이 있다. 우리가 보는 웹 브라우저 화면은 DOM이라는 트리 형태의 그래프라는 사실이다.
브라우저 엔진은 HTML 구문을 해석해 DOM 트리를 구성하고, HTML에서 CSS 소스를 추출하여 CSSOM 이라는 스타일링 객체를 구성한다. 두 객체가 잘 결합되어야 보기 좋은 화면이 만들어진다. 그렇다면 HTML 구문은 누가 만드는 걸까? 웹서버가 전부 다 만들 수도 있고, 또는 웹서버에서 초기 HTML을 받은 후에 대부분의 내용을 브라우저가 동적으로 생성해 붙일 수도 있다. 전자를 SSR(서버 사이드 렌더링), 후자를 CSR(클라이언트 사이드 렌더링)이라고 부른다. 즉, CSR에서는 브라우저가 많은 일을 한다.
CSR 방식의 웹을 구현한다는 건, 코드가 동작하는 시점에 이미 브라우저의 모든 기능이 활성화 되어있다는 의미이다. 서버로부터 받아온 최초의 HTML에 포함된 JS 파일은 브라우저 런타임 위에서 실행되기에, 브라우저의 저장소나 브라우저 API를 자유롭게 사용할 수 있다. 그렇게 CSR 방식의 개발을 하다 보면, (나만 그러는지는 모르겠지만) 브라우저가 제공해 주는 리소스들이 항상 존재할 거라고 가정한 채 코드를 작성하게 된다.
최근에는 Nuxt 프레임워크를 사용하는 프로젝트를 진행했는데, 그동안 Vue 개발에 익숙해진 나머지 브라우저 리소스가 당연히 존재할 거라 가정하는 실수를 범했다. 그리고 이 일을 계기로, SSR에서 DOM 생성 직전에 이루어지는 Hydration의 중요성을 깨닫게 되었다. 이번 글에서는 ‘SSR에서 브라우저 API를 사용할 때 Hydration을 고려해야 한다’는 기본적이면서도 중요한 사실을 이야기해 보고자 한다.
SSR과 CSR은 대표적인 웹 렌더링 방식이다. 용어를 보고 오해할 수 있지만 100% 서버만 또는 100% 클라이언트(브라우저)만 일을 하는 것은 아니다. 웹에 렌더링하기 위해서는 브라우저가 일을 해야 하며, 적어도 최초 한번은 서버로부터 페이지의 기본 정보를 받아와야 한다.
CSR 방식에서는 사용자가 메인 페이지에 접속하면, 브라우저가 해당 웹 애플리케이션에 필요한 HTML과 정적자원(CSS, js, 이미지 리소스 등)을 서버에 요청한다. 서버에서 기초적인 HTML과 자원들을 전송해 주면 브라우저는 HTML을 해석하여 DOM을 생성하고 자원들을 다운로드한다. 이벤트 리스너도 바로 이때, DOM이 생성될 때 부착된다.
이후 자바스크립트 엔진에 의해 js 파일들이 실행되고, 메인 페이지에서 보여줄 컴포넌트가 동적으로 생성되어 렌더링 된다. 이를 ‘Hydration’이라고 한다. React나 Vue와 같은 프레임워크에서는 이때 가상 DOM이 최초로 만들어진다. 이렇게 페이지에서 필요한 주요 내용을 동적으로 생성하는 방식이 바로 CSR이다.
그러면 메인 페이지가 아닌 다른 페이지에 접속하면 어떻게 될까? 라우팅 방식에 따라 차이가 나는데, 보통 CSR을 사용하는 서비스들은 클라이언트 사이드 라우팅을 채택한다. 미리 모든 페이지에서 필요한 js 파일들을 받아 두고, 특정 페이지로 이동 시 해당 페이지에 필요한 js 파일을 실행해 새로운 컴포넌트를 렌더링하고 화면을 업데이트한다.
이런 웹 애플리케이션을 부르는 명칭이 SPA(Single Page Application)다. 이는 사용자가 여러 페이지를 이동하더라도 최초에 서버에서 전송받은 하나의 HTML을 이용해, 컴포넌트만 교체해서 렌더링하기 때문에 단일 페이지로 볼 수 있다는 맥락에서 채택된 이름이다.
SSR 방식에서는 각 페이지 URL마다 보여줄 내용이 미리 결정되어 있다. 사용자가 웹 페이지에 접속하면 브라우저가 해당 URL을 서버에 요청하고, 서버는 이 URL을 기준으로 어떤 페이지를 렌더링할지 결정한다. 서버는 렌더링 엔진을 이용해 페이지를 렌더하고, 완성된 HTML을 브라우저에 응답한다. HTML뿐 아니라, 필요에 따라 js 파일 등의 추가 리소스도 포함될 수 있다. 이후 CSR와 마찬가지로 HTML을 해석하여 DOM을 생성하고 Hydration을 수행한다.
사실 웹 페이지를 어떻게 효율적으로 렌더할 것인가는 웹 개발자들의 오랜 숙제였다. 웹 페이지를 정적으로 생성할 것인지, 동적으로 생성할 것인지에 관한 고민에서부터 출발해, 화면 전체를 여러 구간으로 나누어 특정 구간만 먼저 렌더링하거나, 캐싱을 도입하거나 하는 등의 고민으로 이어지면서 다양한 렌더링 방식이 고안되었다.
요즘에는 서비스의 특징과 요구에 따라 다양한 렌더링 방식이 선택된다. 이글에서 자세히 다루지는 않지만 간략히 소개하면, 정적인 웹사이트에 주로 사용되는 SSG, 특정 페이지만 미리 렌더하는 prerendering, 꼭 필요한 순간까지 렌더링 순서를 미루는 lazy loading 등이 있다. 렌더링 퍼포먼스에 관심이 있다면 다양한 렌더링 방식을 알아 두면 좋을 것이다.
웹 개발자는 많은 브라우저 API를 사용한다. 대표적으로 널리 쓰이는 것들을 몇 개만 나열해 보면 다음과 같다. 아마 웹 개발자라면 대부분 친숙할 것이다.
- DOM API (Document Object Model)
브라우저 API는 브라우저가 지원하는 기능이다. 즉, 클라이언트 사이드에서만 동작하는 기능이다. 웹 개발자에게 브라우저 API는 공기처럼 당연하게 느껴진다. 다만 CSR 방식과 달리, SSR 방식의 웹을 개발할 때는 나의 js 코드가 실행되는 시점과 브라우저 API를 사용할 수 있는 시점이 다를 수 있음을 인지해야 한다. 이는 Hydration 타이밍 때문이다.
SSR의 Hydration은 CSR에서와는 다른 점이 있다. CSR에서는 사용자 인터랙션이 가능한 동적인 컴포넌트를 생성하는 과정이 곧 Hydration이다. 하지만 SSR에서는 브라우저에서 컴포넌트를 처음부터 새로 생성할 필요가 없다. 이미 HTML이 만들어져 있기 때문이다. 그래서 서버로부터 전송받은 초기 HTML 요소 중에서 재사용할 엘리먼트들을 찾아내고, 정적인 상태의 컴포넌트를 동적인 컴포넌트로 변환하는 일련의 과정을 거친다. 바로 이것이 SSR의 Hydration에 해당한다. 요컨대 Hydration이란 마치 건조한 나무에 수분을 공급하는 것처럼 정적인 웹을 동적으로 변화시키는 단계다.
CSR에서는 최초 렌더링 시점에 이미 DOM 객체가 다 만들어져서 브라우저 API 호출이 자유롭다. 그러나 SSR에서는 최초 렌더링이 서버 사이드에서 이루어지므로 아직 DOM 객체가 없고, 따라서 브라우저 API도 호출할 수 없다. 이를 해결하기 위해 가장 먼저 할 수 있는 일은 클라이언트 사이드 코드와 서버사이드 코드를 분리하고, 우리 웹 애플리케이션이 반드시 DOM이 생성된 이후에 브라우저 API를 호출하도록 보장하는 것이다. 그런데 만약 그럴 수 없다면? 브라우저 API 호출 시점을 우리 마음대로 변경할 수 없는 경우도 있지 않을까?
그 대표적인 사례로 우리가 사용하는 라이브러리에서 브라우저 API를 호출하는 경우가 있다. 라이브러리 코드는 초기에 로드되는 js 파일에 함께 번들링된다. 만약 의존하고 있는 라이브러리 소스 코드를 직접 수정하고 싶다면 그렇게 할 수 있지만, 이후 버전 업데이트 시에 번거로워진다. 일반적으로는 라이브러리 커뮤니티에 이슈 제기를 해서 정식 업데이트에 포함되도록 요청한다.
이 요청은 받아들여지지 않을 수도 있고, 받아들여지더라도 업데이트까지 얼마나 오랜 시간이 소요될지 알 수 없다. 그렇기 때문에 SSR 개발을 하는 경우, 사용하는 라이브러리가 SSR 대응을 해 두었는지 확인하는 편이 좋다.
이번에 Nuxt 개발을 하면서 문제를 겪은 라이브러리에서는 이미지 최적화를 위해 브라우저 뷰포트의 변동을 주시하는 Intersection Observer API를 사용하고 있었다. 물론 사용하는 것 자체는 문제가 되지 않지만, DOM 객체의 존재 혹은 Intersection Observer 객체의 존재 여부에 대한 확인 없이, 라이브러리가 최초로 등록될 때 즉각적으로 호출하고 있는 것이 문제였다. 그래서 우리 앱은 js 파일 로드 단계에서 멈춰버렸다.
처음에는 낯선 SSR 환경에서 이러한 크래시 현상을 이해할 수 없었고, Intersection Observer 객체가 존재하지 않는다는 사실을 받아들이는 데까지 시간이 좀 걸렸다. 아직 Hydration 되기 전이라서 그렇다는 것을 알아내기까지도 오래 걸렸다. 이때 선택지는 두 가지였다. 라이브러리를 교체하거나, SSR을 포기하거나. 결론적으로 Nuxt는 CSR 모드로 간단하게 변경이 가능해서, 일단은 SSR 모드를 포기했다. 만약 Hydration과 SSR 프로세스에 대해서 익숙해진 다음에 프로젝트를 시작했다면, SSR의 장점을 놓치지 않았을 거라 매우 아쉬웠다.
렌더링에서 브라우저와 서버의 역할을 분명하게 이해하는 것은 웹 개발에서 매우 중요한 부분이다. CSR과 SSR, 그리고 앞서 언급한 다양한 렌더링 방식은 결국 브라우저와 서버가 각각 어떤 역할을 맡는지에 따라 차이가 발생하며, 서비스의 목적에 따라 적합한 렌더링 방식을 선택할 수 있어야 한다.
SSR은 초기 렌더링을 서버 측에서 수행하므로 검색 엔진 최적화에 유리하다. 검색 엔진들은 사용자의 쿼리에 따라 주기적으로 웹을 탐색하고, 수많은 페이지를 URL 기준으로 수집해서 DB에 저장해 둔다. 그리고 크롤러는 웹 사이트의 지도와 링크를 따라다니며 웹 페이지의 내용들을 수집하고, 검색엔진 DB에 저장해 둔다.
URL 기반으로 완전히 렌더링된 콘텐츠를 전송하는 SSR 방식은 검색엔진과 크롤러에 내용이 더 잘 인식될 수 있다. 또한 SSR은 CSR에 비해, 최초에 js 파일 로드 없이도 HTML이 이미 렌더되어 보여지기 때문에 페이지 로딩이 매우 빠르다. 이것도 SEO를 좋게 만드는 요인이라고 한다.
반면 CSR은 최초 로딩은 느리더라도 클라이언트 측에서 자바스크립트를 사용해, 동적으로 컴포넌트를 렌더링하므로 지속적으로 볼 때 서비스 이용 경험이 훨씬 좋을 수 있다. 나날이 좋아지고 있는 웹 브라우저 성능을 고려하면 CSR은 좋은 선택지이며, SPA라는 개념이 등장한 것도 매우 자연스러운 현상으로 보인다. 결국 렌더링 방식은 주된 타깃 사용자와 서비스 특성에 맞게 선택해야 하는 부분이다.
개발자는 이러한 두 방식을 다룰 때 코드 컴파일 및 번들링, 렌더링 단계, 데이터 흐름 등을 고려해야 한다. 또한 개발할 때 코드가 최종 렌더링 될 때까지 어떻게 작동하는지 상상하면서 개발하는 것이 중요하다. 이는 단순히 기계적으로 코딩하는 것이 아니라, 웹 애플리케이션의 동작 원리를 이해하고 최적화하는 것까지를 의미한다. 평소에 SSR과 CSR의 차이를 잘 이해하고 필요한 상황에서 적절한 방법을 선택한다면, 웹 애플리케이션을 더 효율적으로 개발하고 운영할 수 있을 것이다.
<참고>
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.