[번역] 웹에서 렌더링하기

Saetbyeol·2023년 4월 13일
4

translations.zip

목록 보기
3/13
post-thumbnail

원문: https://web.dev/rendering-on-the-web/

애플리케이션에서 로직과 렌더링을 어디에 구현해야 할까요? SSR을 사용하는 것이 좋을까요? Rehydration은 어떤가요? 답을 찾아봅시다!

개발자인 우리는 종종 애플리케이션의 전체 아키텍처에 영향을 주는 결정을 내려야 하는 상황에 직면합니다. 웹 개발자가 내려야 하는 중요한 결정 중 하나는 애플리케이션 어디에 로직과 렌더링을 구현할 것인가에 대한 부분입니다. 웹사이트를 구축하는 방법은 여러 가지가 있으므로 어려운 결정일 수 있습니다.

이러한 영역에 대한 이해는 지난 몇 년 동안 대규모 사이트와 함께 한 Chrome의 작업을 바탕으로 합니다. 우리는 대체로 개발자들에게 전체 rehydration 방식보다는 서버 렌더링 또는 정적 렌더링을 고려해볼 것을 권장합니다.

결정을 내릴 때 선택한 아키텍처를 잘 이해하려면, 우리는 각 접근 방식에 대한 깊은 이해와 이에 대해 이야기할 때 사용할 수 있는 일관된 용어가 필요합니다. 접근 방식 간의 차이점은 성능 관점에서 웹을 렌더링하는 데 발생하는 트레이드 오프를 설명하는 데 도움이 됩니다.

용어

렌더링

  • SSR: 서버 사이드 렌더링, 서버에서 클라이언트 사이드 또는 유니버셜 앱을 서버에서 HTML로 렌더링하는 방식
  • CSR: 클라이언트 사이드 렌더링, 일반적으로 DOM을 사용하여 브라우저에서 애플리케이션을 렌더링하는 방식
  • Rehydration: 서버에서 렌더링된 HTML의 DOM 트리와 데이터를 재사용하여 클라이언트에서 자바스크립트 뷰를 "부팅"하는 것
  • 프리렌더링(Prerendering): 빌드 시점에 클라이언트 사이드 애플리케이션을 실행하여 초기 상태를 정적 HTML로 캡쳐하는 것

성능

  • TTFB(Time to First Byte): 링크를 클릭하고 내부 콘텐츠가 처음으로 보일 때까지 걸리는 시간
  • FP(First Paint): 사용자에게 처음으로 픽셀이 보이는 시간
  • FCP(First Contentful Paint): 요청받은 콘텐츠(아티클 본문 등)가 보이는 데 걸리는 시간
  • TTI(Time To Interactive): 페이지가 사용자와 상호작용 가능하게 되는 데(이벤트 연결 등) 걸리는 시간

서버 렌더링

서버 렌더링은 탐색에 대한 응답으로 서버에서 페이지에 대한 전체 HTML을 생성합니다. 이는 브라우저가 응답을 받기 전에 처리되기 때문에 클라이언트에서 데이터를 가져오거나 템플릿을 작성하는 데 걸리는 추가적인 왕복이 발생하지 않습니다.

서버 렌더링은 일반적으로 빠른 First Paint(FP)와 First Contentful Paint(FCP)를 가집니다. 서버에서 페이지 로직을 실행하고 렌더링하면 클라이언트에 많은 자바스크립트 파일을 보내지 않아도 됩니다. 이에 따라 빠른 Time to Interactive(TTI)를 달성할 수 있게 합니다. 서버 렌더링을 사용하면 단순히 텍스트와 링크를 사용자의 브라우저로 전송하기만 하면 되기 때문에 어쩌면 당연한 결과입니다. 이러한 방식은 다양한 디바이스와 네트워크 조건에서 잘 동작하며, 스트리밍 문서 파싱과 같이 흥미로운 브라우저 최적화를 가능하게 만듭니다.

서버 렌더링을 사용하면 사용자가 사이트를 사용하기 전에 CPU에 바인딩된 자바스크립트 코드가 처리될 때까지 기다릴 필요가 없습니다. 심지어 서드파티 JS를 사용하더라도, 자체 퍼스트파티 JS 비용을 줄여주는 서버 렌더링을 통해 나머지 부분에서 더 큰 "예산"을 확보하게 되는 셈입니다. 그러나, 이 접근 방식에는 한 가지 큰 단점이 존재합니다. 서버에서 페이지를 생성하는 데 어느 정도 시간이 걸리기 때문에 느린 Time to First Byte(TTFB)을 갖게 될 수 있습니다.

애플리케이션에서 서버 렌더링으로 충분한 것인지에 대한 여부는 여러분이 구축하고자 하는 것의 유형에 따라 크게 달라집니다. 서버 렌더링과 클라이언트 사이드 렌더링에서 어떤게 옳은지에 대한 오랜 논쟁이 있습니다. 하지만 어떤 페이지에서는 서버 렌더링을 사용하고 그 외의 페이지에선 사용하지 않도록 선택할 수 있음을 알아야 합니다. 일부 사이트에서는 하이브리드 렌더링 기술을 성공적으로 도입했습니다. 넷플릭스는 비교적 정적인 랜딩 페이지를 서버 렌더링하면서 동시에 인터랙션이 많은 페이지의 자바스크립트 파일을 미리 가져오기 때문에 클라이언트에서 렌더링된 무거운 페이지를 더 빠르게 로딩할 수 있게 합니다.

많은 모던 프레임워크, 라이브러리 및 아키텍처를 사용하면 클라이언트와 서버 모두에서 동일한 애플리케이션을 렌더링할 수 있습니다. 이러한 기술은 서버 렌더링에서 사용될 수 있지만, 서버와 클라이언트 모두에서 렌더링이 이루어지는 아키텍처는 아주 다양한 성능적인 특성과 트레이드 오프를 가지는 해결책을 가진다는 것을 기억하세요. 리액트 사용자는 renderToString() 함수 또는 서버 렌더링을 위해 Next.js 위에 구축된 솔루션을 사용할 수 있습니다. 뷰(Vue) 사용자라면 뷰에서 제공하는 서버 렌더링 가이드 또는 Nuxt를 참고하면 되겠네요. 앵귤러 사용자에겐 Universal이 있습니다. 대부분의 인기 있는 솔루션은 어떤 형태로든지 hydration을 제공하기 때문에, 툴을 선택하기 전에 어떤 접근 방식을 사용하는지 확인해보세요.

정적 렌더링(Static Rendering)

정적 렌더링은 빌드 시점에 발생하며 클라이언트 측의 자바스크립트 파일 양이 제한되어 있다는 전제 하에 빠른 First Paint, First Contentful Paint, 그리고 Time To Interactive를 제공합니다. 서버 렌더링과 다르게, 일관되게 빠른 Time To First Byte를 가집니다. 페이지의 HTML을 즉석에서 생성할 필요가 없기 때문입니다. 일반적으로 정적 렌더링은 각 URL에 대한 HTML 파일을 개별적으로 미리 생성합니다. 생성된 HTML 파일을 여러 CDN에 배포하여 엣지 캐싱의 이점을 누릴 수도 있습니다.

정적 렌더링을 위한 솔루션은 다양한 형태와 크기로 제공됩니다. Gatsby와 같은 툴은 애플리케이션이 빌드 단계에서 생성되는 것이 아니라 동적으로 렌더링되는 것처럼 개발자가 느낄 수 있도록 설계되었습니다. JekyllMetalsmith 같은 다른 툴들은 정적인 특성을 반영하여 보다 더 템플릿 중심적인 접근 방식을 제공합니다.

정적 렌더링의 단점 중 하나는 모든 가능한 URL에 대해 개별적인 HTML 파일을 생성해야 한다는 것입니다. 미리 모든 URL을 전부 예측할 수는 없거나, 고유한 페이지의 수가 너무 많은 경우 정적 렌더링을 사용하는 것이 어렵거나 불가능할 수도 있습니다.

리액트 사용자라면 아마 컴포넌트를 사용하여 작성하기 편한 Gatsby, Next.js static export 또는 Navi에 친숙하게 느낄 수도 있는데요. 그렇지만 정적 렌더링과 프리렌더링의 차이점을 이해하는 건 아주 중요합니다. 정적으로 렌더링 페이지는 클라이언트 사이드 자바스크립트를 많이 실행할 필요없이 인터랙티브한 반면에, 프리렌더링은 페이지가 인터랙티브하도록 클라이언트에서 부팅해야 하는 SPA의 First Paint 또는 First Contentful Paint를 개선합니다.

정적 렌더링과 프리렌더링 중 어떤 것이 더 적합한 솔루션인지 확신이 없다면, 테스트를 해보세요. 자바스크립트를 비활성화하고 생성된 웹 페이지를 로드하는 방식으로 말이죠. 정적으로 렌더링된 페이지는 자바스크립트가 비활성화 되더라도 여전히 꽤 많은 기능이 계속 존재할 겁니다. 프리렌더링된 페이지의 경우 링크와 같은 일부 기본적인 기능들은 계속 사용할 수 있지만, 대부분의 페이지가 비활성 상태가 됩니다.

또 다른 유용한 테스트는 Chrome DevTools를 사용하여 네트워크 상태를 느리게 만든 후, 페이지가 인터랙티브한 상태가 되기 전에 얼마나 많은 자바스크립트 파일이 다운로드되는지 관찰해보세요. 프리렌더링은 일반적으로 인터랙티브한 상태가 되기까지 많은 자바스크립트 파일이 필요합니다. 또한 프리렌더링의 자바스크립트 파일은 정적 렌더링에서 사용되는 점진적 향상 방식보다 더 복잡한 경향이 있습니다.

서버 렌더링 vs 정적 렌더링

서버 렌더링은 동적인 특성으로 인해 꽤 상당한 컴퓨팅 오버헤드 비용이 발생하기 때문에 완벽한 해결책은 아닙니다. 많은 서버 렌더링 솔루션은 일찍이 데이터를 비우지 않고, TTFB를 지연시키거나 전송되는 데이터(예: 클라이언트에서 자바스크립트가 사용하는 인라인 상태)의 양을 두 배로 늘릴 수도 있습니다. 리액트에서 renderToString() 함수는 동기식이며 싱글 스레드로 동작하므로 속도가 느릴 수 있습니다. 서버 렌더링을 "올바르게" 구현하기 위해서는 컴포넌트 캐싱, 메모리 소비 관리, 메모화 기법 적용 등 여러 문제를 해결하기 위한 솔루션을 찾거나 구축해야 할 수도 있습니다. 일반적으로 동일한 애플리케이션은 클라이언트에서 한번, 서버에서 한번 등 여러 번 처리/재빌드하게 됩니다. 서버 렌더링을 통해 무언가를 더 빨리 보여줄 수 있다고 해서 갑자기 할 일이 줄어드는 건 아닙니다.

서버 렌더링은 각 URL에 대해 온디맨드로 HTML을 생성하지만 정적으로 렌더링된 콘텐츠를 제공하는 것보다 느릴 수 있습니다. 추가 작업이 가능하다면, HTML 캐싱을 더해 서버 렌더링 시간을 크게 줄일 수 있습니다. 서버 렌더링의 장점은 정적 렌더링보다 "실시간" 데이터를 더 많이 가져오고, 더 완전한 요청에 대해 대응할 수 있다는 점입니다. 개인화가 필요한 페이지는 정적 렌더링으로는 제대로 동작하기 어려운 유형 중 하나입니다.

또한 서버 렌더링은 PWA를 구축할 때도 흥미로운 결정사항을 제시합니다. 전체 페이지 서비스 워커 캐싱을 사용하는 것이 나을까요, 아니면 각 콘텐츠만 서버 렌더링을 하는 것이 나을까요?

클라이언트 사이드 렌더링(CSR)

클라이언트 사이드 렌더링(CSR)은 자바스크립트를 사용하여 브라우저에서 직접 렌더링하는 것을 의미합니다. 모든 로직, 데이터 가져오기, 템플릿 생성하기, 라우팅은 서버 대신 클라이언트에서 처리됩니다.

클라이언트 사이드 렌더링은 모바일 환경에서 빠른 속도를 확보하고 유지하기 어려울 수 있습니다. 최소한의 작업으로 빠듯하게 자바스크립트 용량을 유지하며 가능한 적은 RTT로 값을 전달하는 경우라면 순수 서버 렌더링 성능에 근접할 순 있습니다. 파서가 더 빨리 동작하게 하는 <link rel=preload>를 사용하여 아주 중요한 스크립트와 데이터는 더 빨리 전달될 수 있습니다. 초기 탐색 및 차후 탐색이 즉각적으로 느껴지게 하기 위해 PRPL과 같은 패턴을 평가해 볼 가치가 있습니다.

클라이언트 사이드 렌더링의 가장 큰 단점은 애플리케이션이 커짐과 동시에 필요한 자바스크립트의 양이 증가한다는 점입니다. 처리 성능에 있어 경쟁을 해야 하거나 페이지 콘텐츠가 렌더링되기 전에 처리해야 하는 새로운 자바스크립트 라이브러리, 폴리필과 서드파티 코드가 더해지면 특히 어려워집니다. 대규모 자바스크립트 번들에 의존하는 CSR로 구축하고자 한다면 적극적인 코드 분할을 고려해야 하며, 자바스크립트를 지연 로드함으로써 "필요할 때만, 필요한 것만 제공"할 수 있어야 합니다. 상호 작용이 거의 또는 전혀 없다면, 서버 렌더링이 더욱 확장 가능한 해결책이 될 수 있습니다.

싱글 페이지 애플리케이션을 구축하는 경우, 대부분의 페이지에서 공유하는 UI의 핵심 부분을 식별하면 애플리케이션 쉘 캐싱 기법을 적용할 수 있습니다. 서비스 워커와 결합하면, 여러 번 방문했을 때 체감되는 성능을 크게 향상시킬 수 있습니다.

Rehydration을 통한 서버 렌더링과 CSR의 결합

종종 유니버셜 렌더링 또는 간단히 "SSR"이라고 하는 이 방식은 클라이언트 사이드 렌더링과 서버 렌더링을 모두 수행하여 이 둘 간의 트레이드 오프를 절충하고자 합니다. 전체 페이지 로드나 새로고침과 같은 탐색 요청은 애플리케이션을 HTML로 렌더링하는 서버에서 처리한 후 렌더링에서 사용된 자바스크립트나 데이터를 결과 문서에 임베드합니다. 잘 구현한다면 서버 렌더링과 마찬가지로 빠른 First Contentful Paint을 얻고, (re)hydration이라고 불리는 기법을 활용하여 클라이언트에서 다시 렌더링합니다. 이는 아주 혁신적인 해결책이지만 상당한 성능 문제를 가질 수도 있습니다.

rehydration으로 SSR 처리하는 경우의 가장 큰 문제는 Time To Interactive에 아주 큰 부정적인 영향을 줄 수 있다는 것입니다. 심지어 First Paint를 개선했음에도 말이죠. SSR된 페이지는 겉보기에는 정상적으로 로드되었고 인터랙티브하게 보일지라도 클라이언트 측의 자바스크립트 파일이 실행되고 이벤트 핸들러가 붙기 전까지는 실제 사용자 입력에 응답하지 못하는 경우가 많습니다. 모바일 환경에서는 이러한 시간이 몇 초 또는 몇 분까지 늘어나기도 합니다.

페이지가 로드된 것처럼 보이지만 일정 시간 동안 아무리 클릭하거나 눌러도 아무 일도 발생하지 않는 경험이 있었을 겁니다. 이러한 상황은 사용자에게 곧 좌절감을 안겨줍니다. "왜 아무 일도 일어나지 않는 걸까? 왜 스크롤이 안 되는 거지?"

Rehydration 문제: 비용을 두 배로 지불하는 앱

Rehydration 문제는 자바스크립트로 인한 인터랙티브 지연보다 더 심각해질 수도 있습니다. 서버가 HTML을 렌더링하는 데 사용한 데이터를 다시 요청하지 않고도, 클라이언트 사이드 자바스크립트에서 서버가 중단한 부분을 정확하게 "선택"할 수 있도록 하기 위해, 현재 SSR 솔루션은 일반적으로 UI의 데이터 종속성으로부터의 응답을 스크립트 태그로 문서에 직렬화합니다. 결과적으로 HTML 문서는 아주 많은 중복을 갖게 됩니다.

보시다시피, 서버는 탐색 요청에 대한 응답으로 애플리케이션의 UI에 대한 표현을 반환합니다. 그러나 해당 UI를 구성하는 데 사용된 소스 데이터와 클라이언트에서 부팅되는 UI 구현의 전체 사본도 같이 반환합니다. bundle.js의 로딩과 실행이 완료되어야 UI가 인터랙티브되는 것입니다.

SSR rehydration을 사용한 실제 웹사이트에서 수집된 성능 지표들은 이 방식을 사용하지 않는 것이 좋다는 것을 보여줍니다. 궁극적으로 그 이유는 사용자 경험으로 귀결됩니다. 사용자들이 "불쾌한 골짜기"에 빠지는 것은 아주 쉽습니다.

스트리밍 서버 렌더링과 점진적 rehydration

서버 렌더링은 지난 몇 년간 많은 발전을 거듭해 왔습니다.

스트리밍 서버 렌더링은 HTML을 청크로 전송함으로써 브라우저가 각 청크를 받을 때마다 점진적으로 렌더링을 할 수 있게 합니다. 이는 마크업이 사용자에게 더 빨리 도착하여 빠른 First Paint와 First Contentful Paint를 제공할 수 있습니다. 리액트의 renderToPipeableStream()의 비동기적인 스트림은 동기적인 renderToString와 비교하여 백프레셔 상황을 잘 처리합니다.

*역자주, 참고: 백프레셔(backpressure)는 데이터가 처리되는 속도가 데이터의 수신 속도를 초과할 때 발생하는 상황을 의미합니다.

점진적인 rehydration은 주목할 가치가 있으며, 리액트 내에서도 계속 연구하고 있는 부분입니다. 이 접근 방식을 사용하면, 전체 애플리케이션을 한 번에 초기화하는 현재 방식과 다르게 서버 렌더링된 애플리케이션의 개별적인 부분이 시간에 따라 "부팅"됩니다. 우선순위가 낮은 부분에선 클라이언트 측의 업데이트를 지연시킴으로써 메인 스레드의 차단을 방지할 수 있어 페이지를 인터랙티브하게 만들기 위해 필요한 자바스크립트의 양을 줄이는 데 도움이 됩니다. 또한 가장 일반적인 SSR rehydration의 함정 중 하나는 서버 렌더링된 DOM 트리가 파괴되었다가 바로 다시 생성되는 것입니다. 초기 동기적인 클라이언트 사이드 렌더를 하는 데 아직 준비가 되지 않은 데이터가 필요하여 프로미스가 리졸브되기를 기다릴 때 자주 발생합니다. 하지만 점진적인 rehydration은 이러한 함정에서도 도움이 될 수 있습니다.

부분 rehydration

부분 rehydration은 구현하기 어려운 것으로 입증되었습니다. 이 접근 방식은 점진적 rehydration의 확장 개념으로, 점진적으로 rehydration할 개별 조각(컴포넌트/뷰/트리)을 분석하여 어떤 조각이 인터랙티브한지를 식별하는 것입니다. 대부분 정적인 부분들에 대해 해당 자바스크립트 코드는 비활성 참조와 장식적인 기능으로 변환되어, 클라이언트 측의 범위를 거의 0에 가깝게 줄입니다. 부분 rehydration 방식은 자체적인 문제와 타협점을 가집니다. 캐싱에 몇 가지 흥미로운 문제가 있으며, 클라이언트 사이드 탐색은 애플리케이션의 비활성된 부분에 대해 서버 렌더링된 HTML은 전체 페이지 로드 없이 사용 가능함을 가정할 수 없다는 것을 의미합니다.

삼체형(trisomorphic) 렌더링

서비스 워커를 사용 가능한 경우, 삼체형 렌더링에도 관심을 가져볼 수 있습니다. 이 기법은 초기/비JS 탐색에는 스트리밍 서버 렌더링을 사용하고, 서비스 워커와 설치된 후엔 서비스 워커가 탐색을 위한 HTML 렌더링을 담당하도록 할 수 있습니다. 이를 통해 캐시된 컴포넌트와 템플릿을 최신 상태로 유지하며, 동일한 세션에서 새로운 뷰를 렌더링할 때 SPA 스타일로 탐색을 사용할 수 있습니다. 이 방식은 서버와 클라이언트 페이지, 그리고 서비스 워커 간에 동일한 템플릿과 라우팅 코드를 공유할 수 있을 때 가장 좋습니다.

SEO 고려사항

팀이 웹 렌더링 전략을 선택할 때, SEO의 영향을 고려하는 경우가 많습니다. 크롤러가 쉽게 해석할 수 있는 '완전한' 경험을 제공하고자 서버 렌더링을 선택하곤 합니다. 크롤러는 자바스크립트를 이해할 수는 있지만, 렌더링되는 방식에 대해 종종 알고 있어야 하는 한계점이 있습니다. 클라이언트 사이드 렌더링은 추가적인 테스트와 귀찮은 작업들을 필요로 합니다. 최근에는 아키텍처가 클라이언트 측의 자바스크립트에 크게 구동되는 경우 동적 렌더링도 꽤 괜찮은 선택지가 되었습니다.

확실하지 않다면, Mobile Friendly Test 도구는 선택한 방식이 원하는 대로 잘 동작하는지 확인하는 데 매우 유용합니다. 구글 크롤러에게 페이지가 보이는 방식, 찾아진 직렬화된 HTML 콘텐츠(자바스크립트가 실행된 이후), 렌더링 중 발생한 오류를 시각적으로 미리보기를 제공합니다.

마무리...

렌더링의 방식을 결정할 때는 병목지점을 측정하고 이해하는 것이 필요합니다. 정적 렌더링 또는 서버 렌더링 중 어느 쪽이 90%를 도달할 수 있는지 고려해보세요. 인터랙티브한 경험을 제공하고자 최소한의 자바스크립트가 담긴 HTML을 주로 사용하는 것도 아주 좋은 방법입니다. 서버-클라이언트 스펙트럼을 잘 보여주는 간단한 인포그래픽입니다.

참여자

이 글을 리뷰해주시고 도움을 주신 모든 분들께 감사드립니다.
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson, 그리고 Sebastian Markbåge

0개의 댓글