HTML 및 상호작용의 클라이언트 측 렌더링

JavaScript로 HTML을 렌더링하는 것은 서버에서 전송한 HTML을 렌더링하는 것과 다르며 성능에 영향을 줄 수 있습니다. 이 가이드의 차이점과 특히 상호작용이 중요한 웹사이트의 렌더링 성능을 유지하기 위해 취할 수 있는 조치에 대해 알아보세요.

HTML의 파싱 및 렌더링은 브라우저의 내장 탐색 로직('기존 페이지 로드' 또는 '하드 탐색'이라고도 함)을 사용하는 웹사이트에서 기본적으로 매우 잘 작동하는 기능입니다. 이러한 웹사이트를 다중 페이지 애플리케이션 (MPA)이라고도 합니다.

하지만 개발자는 애플리케이션 요구에 맞게 브라우저 기본값을 해결할 수 있습니다. 자바스크립트를 사용하여 클라이언트에서 HTML/DOM의 많은 부분을 동적으로 만드는 단일 페이지 애플리케이션 (SPA) 패턴을 사용하는 웹사이트에서는 확실히 그러한지 확인합니다. 클라이언트 측 렌더링은 이 디자인 패턴의 이름으로, 작업이 과도하게 많은 경우 웹사이트의 다음 페인트에 대한 상호작용 (INP)에 영향을 미칠 수 있습니다.

이 가이드에서는 서버에서 브라우저로 보낸 HTML을 사용하는 것과 JavaScript를 사용하여 클라이언트에서 만드는 것의 차이를 따져볼 수 있으며, HTML을 사용하면 결정적인 순간에 긴 상호작용 지연 시간이 발생할 수 있습니다.

브라우저가 서버에서 제공한 HTML을 렌더링하는 방법

기존 페이지 로드에 사용되는 탐색 패턴은 탐색할 때마다 서버에서 HTML을 수신하는 것입니다. 브라우저의 주소 표시줄에 URL을 입력하거나 MPA의 링크를 클릭하면 다음과 같은 일련의 이벤트가 발생합니다.

  1. 브라우저가 제공된 URL에 대한 탐색 요청을 보냅니다.
  2. 서버가 청크 단위의 HTML로 응답합니다.

마지막 단계가 핵심입니다. 또한 서버/브라우저 교환에서 가장 기본적인 성능 최적화 방법 중 하나이며 스트리밍이라고도 합니다. 서버가 가능한 한 빨리 HTML 전송을 시작할 수 있고 브라우저가 전체 응답이 도착할 때까지 기다리지 않는 경우, 브라우저는 도착하는 대로 HTML을 청크로 처리할 수 있습니다.

서버에서 전송한 HTML의 파싱을 보여주는 스크린샷이 Chrome DevTools의 성능 패널에 시각화되어 있습니다. HTML이 스트리밍되면 이 조각의 청크가 여러 개의 짧은 작업에서 처리되며 렌더링은 증분됩니다.
Chrome DevTools의 성능 패널에 시각화된 대로 서버에서 제공하는 HTML의 파싱 및 렌더링입니다. HTML을 파싱하고 렌더링하는 작업은 청크로 분할됩니다.

브라우저에서 발생하는 대부분의 작업과 마찬가지로 HTML 파싱도 작업 내에서 이루어집니다. HTML이 서버에서 브라우저로 스트리밍될 때 브라우저는 해당 스트림의 비트가 청크로 도착할 때 조금씩 이동하여 HTML 파싱을 최적화합니다. 결과적으로 브라우저가 각 청크를 처리한 후 주기적으로 기본 스레드에 양보하므로 장기 작업이 방지됩니다. 즉, HTML을 파싱하는 동안 사용자에게 페이지를 표시하는 데 필요한 증분 렌더링 작업, 페이지의 중요한 시작 기간에 발생할 수 있는 사용자 상호작용 처리 등 다른 작업이 발생할 수 있습니다. 이렇게 하면 페이지의 Interaction to Next Paint (다음 페인트에 대한 상호작용) 점수가 개선됩니다.

이 사실은 무엇을 시사할까요? 서버에서 HTML을 스트리밍하면 HTML의 증분 파싱 및 렌더링이 이루어지며 무료로 기본 스레드에 자동으로 반영됩니다. 클라이언트 측 렌더링에서는 이 같은 현상이 발생하지 않습니다.

브라우저가 자바스크립트에서 제공하는 HTML을 렌더링하는 방법

페이지로 이동하는 모든 탐색 요청에는 서버에서 HTML을 제공해야 하지만 일부 웹사이트에서는 SPA 패턴을 사용합니다. 이 접근 방식에는 보통 서버에서 제공하는 HTML의 최소 초기 페이로드가 수반되지만, 클라이언트는 서버에서 가져온 데이터로 조합된 HTML을 페이지의 기본 콘텐츠 영역에 채웁니다. 이후의 탐색(이 경우 '소프트 탐색'이라고도 함)은 완전히 JavaScript에 의해 처리되어 페이지를 새 HTML로 채웁니다.

클라이언트 측 렌더링은 HTML이 자바스크립트를 통해 DOM에 동적으로 추가되는 좀 더 제한적인 경우에 비 SPA에서도 발생할 수 있습니다.

자바스크립트를 통해 HTML을 만들거나 DOM에 추가하는 몇 가지 일반적인 방법이 있습니다.

  1. innerHTML 속성을 사용하면 브라우저에서 DOM으로 파싱하는 문자열을 통해 기존 요소의 콘텐츠를 설정할 수 있습니다.
  2. document.createElement 메서드를 사용하면 브라우저 HTML 파싱을 사용하지 않고 DOM에 추가할 새 요소를 만들 수 있습니다.
  3. document.write 메서드를 사용하면 문서에 HTML을 쓸 수 있으며 브라우저는 접근 방식 #1과 마찬가지로 문서를 파싱합니다. 그러나 여러 가지 이유로 인해 document.write는 사용하지 않는 것이 좋습니다.
Chrome DevTools의 성능 패널에 시각화된 JavaScript를 통해 렌더링된 HTML의 파싱 스크린샷. 작업은 기본 스레드를 차단하는 긴 단일 작업에서 발생합니다.
Chrome DevTools의 성능 패널에 시각화된 것과 같이 클라이언트에서 자바스크립트를 통해 HTML을 파싱하고 렌더링합니다. 파싱 및 렌더링과 관련된 작업은 분할되지 않으므로 기본 스레드를 차단하는 긴 작업이 생성됩니다.

클라이언트 측 자바스크립트를 통해 HTML/DOM을 만들면 다음과 같은 영향이 있을 수 있습니다.

  • 탐색 요청에 대한 응답으로 서버에서 스트리밍하는 HTML과 달리 클라이언트의 JavaScript 작업은 자동으로 분할되지 않으므로 기본 스레드를 차단하는 장기 작업이 발생할 수 있습니다. 즉, 클라이언트에서 한 번에 너무 많은 HTML/DOM을 만들면 페이지의 INP에 부정적인 영향을 미칠 수 있습니다.
  • 시작 시 클라이언트에서 HTML이 생성되면 HTML 내에서 참조되는 리소스는 브라우저 미리 로드 스캐너에서 검색되지 않습니다. 이 경우 페이지의 최대 콘텐츠 렌더링 시간 (LCP)에 부정적인 영향을 미칩니다. 런타임 성능 문제는 아니지만 (대신 중요한 리소스를 가져올 때 네트워크 지연으로 인한 문제임) 이 기본적인 브라우저 성능 최적화를 회피함으로써 웹사이트의 LCP가 영향을 받지 않도록 해야 합니다.

클라이언트 측 렌더링이 성능에 미치는 영향과 관련하여 취할 수 있는 조치

웹사이트가 클라이언트 측 렌더링에 크게 의존하는 경우 필드 데이터의 INP 값이 좋지 않음이 발견되는 경우 클라이언트 측 렌더링이 문제와 관련이 있는지 궁금할 수 있습니다. 예를 들어 웹사이트가 SPA인 경우 필드 데이터에 상당한 렌더링 작업을 유발하는 상호작용이 나타날 수 있습니다.

원인이 무엇이든, 다음과 같은 잠재적인 원인을 찾아 정상으로 돌려놓을 수 있습니다.

서버에서 HTML을 최대한 많이 제공

앞서 언급했듯이 브라우저는 기본적으로 서버에서 HTML을 매우 효율적인 방식으로 처리합니다. 긴 작업을 피하는 방식으로 HTML의 파싱과 렌더링을 분해하고 총 기본 스레드 시간을 최적화합니다. 이로 인해 총 차단 시간 (TBT)이 낮아지고 TBT는 INP와 밀접한 상관관계가 있습니다.

웹사이트를 구축하기 위해 프런트엔드 프레임워크를 사용할 수 있습니다. 이 경우 서버에서 구성요소 HTML을 렌더링하고 있는지 확인해야 합니다. 이렇게 하면 웹사이트에 필요한 초기 클라이언트 측 렌더링의 양이 제한되므로 더 나은 환경을 제공할 수 있습니다.

  • React의 경우 Server DOM API를 사용하여 서버에서 HTML을 렌더링하는 것이 좋습니다. 하지만 서버 측 렌더링의 전통적인 방법은 동기식 접근 방식을 사용하므로 첫 바이트 소요 시간 (TTFB)은 물론 콘텐츠가 포함된 첫 페인트 (FCP), LCP와 같은 후속 측정항목도 길어질 수 있습니다. 가능한 경우 Node.js 또는 기타 JavaScript 런타임에 스트리밍 API를 사용하여 서버가 가능한 한 빨리 브라우저로 HTML 스트리밍을 시작할 수 있도록 합니다. React 기반 프레임워크인 Next.js는 다양한 권장사항을 기본적으로 제공합니다. 서버에서 HTML을 자동으로 렌더링할 뿐만 아니라 인증과 같은 사용자 컨텍스트에 따라 변경되지 않는 페이지에 대해 HTML을 정적으로 생성할 수도 있습니다.
  • 또한 Vue는 기본적으로 클라이언트 측 렌더링을 실행합니다. 하지만 React와 마찬가지로 Vue는 서버에서 구성요소 HTML을 렌더링할 수도 있습니다. 가능한 경우 이러한 서버 측 API를 활용하거나 권장사항을 쉽게 구현할 수 있도록 Vue 프로젝트에 상위 수준 추상화를 적용합니다.
  • Svelte는 기본적으로 서버에서 HTML을 렌더링합니다. 구성요소 코드가 브라우저 전용 네임스페이스 (예: window)에 액세스해야 하는 경우 서버에서 구성요소의 HTML을 렌더링하지 못할 수도 있습니다. 불필요한 클라이언트 측 렌더링을 일으키지 않도록 가능한 한 다른 접근 방식을 살펴보세요. Next.js가 React인 Svelte의 SvelteKit은 Svelte 프로젝트에 가능한 한 많은 권장사항을 포함하므로 Svelte만 사용하는 프로젝트의 잠재적 문제를 피할 수 있습니다.

클라이언트에 생성되는 DOM 노드의 양 제한

DOM이 클 경우 이를 렌더링하는 데 필요한 처리 작업이 증가하는 경향이 있습니다. 웹사이트가 정식 SPA든 MPA 상호작용의 결과로 기존 DOM에 새 노드를 삽입하든 상관없이 이러한 DOM을 가능한 한 작게 유지하는 것이 좋습니다. 이렇게 하면 클라이언트 측에서 HTML을 표시하는 데 필요한 작업을 줄여 웹사이트의 INP를 낮게 유지하는 데 도움이 될 수 있습니다.

스트리밍 서비스 워커 아키텍처 고려

이 기술은 고급 기법으로 모든 사용 사례에서 쉽게 작동하지 않을 수 있습니다. 하지만 사용자가 한 페이지에서 다음 페이지로 이동하면 즉시 로드되는 것처럼 느껴지는 웹 사이트로 MPA를 변환할 수 있습니다. 서비스 워커를 사용하여 CacheStorage에서 웹사이트의 정적 부분을 사전 캐시하는 동시에 ReadableStream API를 사용하여 서버에서 페이지의 나머지 HTML을 가져올 수 있습니다.

이 기법을 성공적으로 사용하면 클라이언트에서 HTML이 생성되지 않지만 캐시에서 콘텐츠 일부를 즉시 로드하면 사이트가 빠르게 로드되는 것처럼 보입니다. 이 접근 방식을 사용하는 웹사이트는 클라이언트 측 렌더링의 저하 없이 SPA와 거의 비슷하게 느껴질 수 있습니다. 또한 서버에 요청하는 HTML의 양도 줄어듭니다.

간단히 말해 스트리밍 서비스 워커 아키텍처는 브라우저의 기본 제공 탐색 로직을 대체하지 않고 여기에 추가합니다. Workbox를 사용하여 이 작업을 실행하는 방법에 관한 자세한 내용은 스트림을 사용한 빠른 멀티페이지 애플리케이션을 참고하세요.

결론

웹사이트에서 HTML을 수신하고 렌더링하는 방식은 성능에 영향을 미칩니다. 웹사이트가 작동하는 데 필요한 HTML 전체 (또는 대부분)를 서버에 전송하는 데 서버에 의존하면 증분 파싱 및 렌더링과 긴 작업을 피하기 위해 기본 스레드에 자동 양도하는 등 무료로 많은 것을 얻을 수 있습니다.

클라이언트 측 HTML 렌더링을 사용하면 여러 가지 잠재적인 성능 문제가 발생하지만 대부분의 경우 방지할 수 있습니다. 그러나 개별 웹사이트의 요구사항으로 인해 항상 완전히 방지하는 것은 아닙니다. 과도한 클라이언트 사이트 렌더링으로 인해 발생할 수 있는 잠재적 긴 작업을 줄이려면 가능한 한 많은 웹사이트의 HTML을 서버에서 전송하고, 클라이언트에서 렌더링해야 하는 HTML에 대해 DOM 크기를 가능한 한 작게 유지하고, 브라우저에서 HTML을 더 빠르게 전달하는 동시에 브라우저에서 로드한 HTML에 대해 브라우저가 제공하는 증분 파싱 및 렌더링을 활용하는 다른 아키텍처를 고려해 보세요.

웹사이트의 클라이언트 측 렌더링을 가능한 한 최소화하면 웹사이트의 INP뿐만 아니라 LCP, TBT, TTFB와 같은 기타 측정항목(경우에 따라)도 개선할 수 있습니다.

Maik JonietzUnsplash의 히어로 이미지입니다.