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

JavaScript로 HTML을 렌더링하는 것은 서버에서 전송된 HTML을 렌더링하는 것과 다르며 성능에 영향을 줄 수 있습니다. 이 가이드에서 차이점을 알아보고 웹사이트의 렌더링 성능을 유지하기 위해 할 수 있는 작업(특히 상호작용과 관련된 경우)을 알아보세요.

HTML 파싱 및 렌더링은 브라우저의 기본 제공 탐색 로직을 사용하는 웹사이트의 경우 브라우저가 기본적으로 매우 잘 처리합니다. 이를 '기존 페이지 로드' 또는 '하드 탐색'이라고도 합니다. 이러한 웹사이트를 멀티 페이지 애플리케이션 (MPA)이라고도 합니다.

하지만 개발자는 애플리케이션 요구사항에 맞게 브라우저 기본값을 해결할 수 있습니다. 단일 페이지 애플리케이션 (SPA) 패턴을 사용하는 웹사이트의 경우 JavaScript를 사용하여 클라이언트에서 HTML/DOM의 많은 부분을 동적으로 생성하므로 이러한 문제가 발생할 수 있습니다. 이 디자인 패턴의 이름은 클라이언트 측 렌더링이며, 관련 작업이 과도한 경우 웹사이트의 다음 페인트까지의 상호작용 (INP)에 영향을 미칠 수 있습니다.

이 가이드에서는 서버에서 브라우저로 전송된 HTML을 사용하는 것과 JavaScript로 클라이언트에서 HTML을 만드는 것의 차이점과 후자가 중요한 순간에 높은 상호작용 지연 시간으로 이어질 수 있는 방법을 설명합니다.

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

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

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

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

Chrome DevTools의 성능 패널에 시각화된 서버에서 전송된 HTML 파싱의 스크린샷 HTML이 스트리밍되면 여러 개의 짧은 태스크에서 청크가 처리되고 렌더링이 점진적으로 이루어집니다.
Chrome DevTools의 성능 패널에 시각화된 서버에서 제공한 HTML의 파싱 및 렌더링 HTML을 파싱하고 렌더링하는 데 관련된 작업은 청크로 분할됩니다.

브라우저에서 발생하는 대부분의 작업과 마찬가지로 HTML 파싱은 작업 내에서 발생합니다. HTML이 서버에서 브라우저로 스트리밍되면 브라우저는 스트림의 비트가 청크로 도착할 때마다 조금씩 HTML 파싱을 실행하여 HTML 파싱을 최적화합니다. 그 결과 브라우저는 각 청크를 처리한 후 주기적으로 기본 스레드에 양보하여 긴 작업을 방지합니다. 즉, HTML이 파싱되는 동안 사용자가 페이지를 볼 수 있도록 하는 증분 렌더링 작업과 페이지의 중요한 시작 기간에 발생할 수 있는 사용자 상호작용 처리 등 다른 작업이 발생할 수 있습니다. 이 접근 방식을 사용하면 페이지의 다음 페인트에 대한 상호작용 (INP) 점수가 향상됩니다.

그 결과가 궁금하신가요? 서버에서 HTML을 스트리밍하면 HTML의 증분 파싱 및 렌더링이 이루어지고 기본 스레드에 자동으로 양보됩니다. 클라이언트 측 렌더링에서는 이러한 이점을 얻을 수 없습니다.

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

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

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

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

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

클라이언트 측 JavaScript를 통해 HTML/DOM을 생성하면 다음과 같은 심각한 결과가 발생할 수 있습니다.

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

클라이언트 측 렌더링의 성능 영향에 대해 취할 수 있는 조치

웹사이트가 클라이언트 측 렌더링에 크게 의존하고 필드 데이터에서 INP 값이 좋지 않은 것으로 확인된 경우 클라이언트 측 렌더링이 문제와 관련이 있는지 궁금할 수 있습니다. 예를 들어 웹사이트가 SPA인 경우 필드 데이터에 상당한 렌더링 작업을 담당하는 상호작용이 표시될 수 있습니다.

원인이 무엇이든 문제를 해결하기 위해 살펴볼 수 있는 몇 가지 잠재적 원인이 있습니다.

서버에서 최대한 많은 HTML 제공

앞에서 언급한 것처럼 브라우저는 기본적으로 매우 성능이 좋은 방식으로 서버의 HTML을 처리합니다. 긴 작업을 방지하고 총 기본 스레드 시간을 최적화하는 방식으로 HTML 파싱 및 렌더링을 분할합니다. 이렇게 하면 총 차단 시간 (TBT)이 줄어들고 TBT는 INP와 강력한 상관관계가 있습니다.

프런트엔드 프레임워크를 사용하여 웹사이트를 빌드할 수 있습니다. 이 경우 서버에서 구성요소 HTML을 렌더링해야 합니다. 이렇게 하면 웹사이트에 필요한 초기 클라이언트 측 렌더링의 양이 제한되어 환경이 개선됩니다.

  • React의 경우 서버 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을 렌더링하지 못할 수 있습니다. 불필요한 클라이언트 측 렌더링을 유발하지 않도록 가능한 경우 대체 접근 방식을 살펴보세요. SvelteKit은 Svelte에 Next.js가 React에 해당하는 것으로, 가능한 한 많은 권장사항을 Svelte 프로젝트에 삽입하므로 Svelte만 사용하는 프로젝트에서 발생할 수 있는 잠재적인 문제를 방지할 수 있습니다.

클라이언트에서 생성되는 DOM 노드 수 제한

DOM이 클수록 렌더링에 필요한 처리량이 증가하는 경향이 있습니다. 웹사이트가 완전한 SPA이든 MPA의 상호작용 결과로 기존 DOM에 새 노드를 삽입하든 이러한 DOM을 최대한 작게 유지하는 것이 좋습니다. 이렇게 하면 클라이언트 측 렌더링 중에 해당 HTML을 표시하는 데 필요한 작업이 줄어들어 웹사이트의 INP를 낮게 유지하는 데 도움이 됩니다.

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

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

이 기법을 성공적으로 사용하면 클라이언트에서 HTML을 만들지 않지만 캐시에서 콘텐츠 부분의 즉각적인 로드로 인해 사이트가 빠르게 로드된다는 인상을 줄 수 있습니다. 이 접근 방식을 사용하는 웹사이트는 클라이언트 측 렌더링의 단점 없이 거의 SPA와 같은 느낌을 줄 수 있습니다. 또한 서버에서 요청하는 HTML의 양을 줄여줍니다.

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

결론

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

클라이언트 측 HTML 렌더링은 대부분의 경우 피할 수 있는 여러 잠재적 성능 문제를 야기합니다. 하지만 각 웹사이트의 요구사항으로 인해 100% 완전히 피할 수는 없습니다. 과도한 클라이언트 측 렌더링으로 인해 발생할 수 있는 잠재적인 긴 작업을 완화하려면 가능한 한 서버에서 웹사이트의 HTML을 최대한 많이 전송하고, 클라이언트에서 렌더링해야 하는 HTML의 DOM 크기를 최대한 작게 유지하고, 서버에서 로드된 HTML에 대해 브라우저가 제공하는 증분 파싱 및 렌더링을 활용하면서 클라이언트에 HTML 전송 속도를 높이는 대체 아키텍처를 고려하세요.

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

Unsplash의 히어로 이미지(마이크 조니에츠 제공)