[번역] 자바스크립트 프레임워크에서 효율적인 하이드레이션(Hydration)이 어려운 이유

eunbinn·2022년 2월 16일
29

FE 번역

목록 보기
1/10
post-thumbnail

원문: https://dev.to/this-is-learning/why-efficient-hydration-in-javascript-frameworks-is-so-challenging-1ca3

하이드레이션은 자바스크립트 프레임워크에서 페이지가 서버에서 렌더링된 이후에 브라우저에서 초기화되는 프로세스를 말합니다. 서버가 초기 HTML을 생성하면, 그 결과에 이벤트 핸들러를 붙이고 브라우저에서 인터랙티브하게 동작하도록 애플리케이션 상태를 초기화해야 합니다.

대부분의 프레임워크에서 하이드레이션은 페이지를 처음 로드할 때 상당히 큰 비용을 수반합니다. 자바스크립트를 로드하고 하이드레이션이 완료되는 데 걸리는 시간이 오래 걸릴수록 우리는 인터랙티브해 보이지만 실제로는 그렇지 않은 페이지를 오랜 시간 마주하게 됩니다. 이는 사용자 경험에 치명적이며 특히 디바이스의 성능이 좋지 않을수록, 또 네트워크가 느릴수록 이를 경험할 확률이 높습니다.

이 문제를 해결할 수 있는 많은 방법이 있을 거라고 생각하실 겁니다. 실제로 해결법은 많지만, 어떤 것도 완벽하지 않습니다. 라이브러리 개발자들은 수 년 동안 이 기술을 점진적으로 개선해오고 있습니다. 그래서 오늘 저는, 우리가 다루고 있는 문제를 가장 잘 이해하기 위해 구체적으로 하이드레이션이라는 주제에 대해 알아보려고 합니다.

서버 렌더링이 만능 해결책은 아닙니다

제일 선호하는 클라이언트 렌더링 자바스크립트 프레임워크를 사용해서 서버 렌더링에 사용합니다. 더 나은 SEO, 더 좋은 성능 모두를 얻을 수 있죠.

... 아닙니다. 그만 멈추세요.

이것은 흔한 오해입니다. 단순히 서버가 SPA(Single Page App)를 렌더링한다고 모든 것이 해결되지는 않습니다. 오히려 자바스크립트 코드를 증가시키며, 애플리케이션이 인터렉티브 할 때까지 걸리는 시간이 단순 클라이언트 렌더링보다 더 길어질 수 있습니다.

네? 뭐라고요?!...

장난치는 게 아니에요. 대부분의 프레임워크에서 하이드레이션 준비 코드는 궁극적으로 두 가지 작업을 모두 수행해야 하기 때문에 일반적인 클라이언트 코드보다 큽니다. 처음에는 하이드레이션만 할지라도 당신의 프레임워크는 클라이언트 측 렌더링을 허용하기 때문에 그것을 위한 코드도 필요합니다.

또한, 데이터를 로드할 때 사용자에게 보여줄 수 있는 HTML 페이지를 보여주는 대신, 서버에서 전체 페이지가 로드되고 렌더링될 때까지 기다려야 합니다. 게다가 그 페이지는 모든 HTML과 애플리케이션이 부트스트랩에 필요한 데이터를 포함하고 있기 때문에 훨씬 큽니다.

나쁜 점만 있는 것은 아닙니다. 일반적으로는 브라우저가 자바스크립트를 로드하기 위한 추가 왕복 시간을 기다릴 필요가 없기 때문에 메인 콘텐츠가 더 빨리 보이게 됩니다. 하지만 애플리케이션을 하이드레이션 하기 위한 자바스크립트를 포함하고 있는 애셋의 로딩을 지연시킵니다.

주의: 이는 사용자 네트워크와 데이터 지연 시간에 크게 좌우됩니다. 또한 스트리밍과 같이 로드 성능 타이밍을 해결하기 위한 많은 기술이 있습니다. 그러나 이것이 명백한 해결책이 아니며 새로운 트레이드오프와 고려사항이 있다는 것을 설명하고자 합니다.

근본적인 문제

클라이언트 측 하이드레이션의 경우 두 가지 매우 아쉬운 점이 있습니다. 하나는 서버에서 렌더링을 한 후, 브라우저에서 하이드레이션 하기 위해 또 다시 렌더링을 해야한다는 것입니다. 두 번째는 우리가 모든 것을 HTML로 한 번, 자바스크립트로 한 번, 총 두 번 전송한다는 것입니다.

일반적으로 3가지 형태로 전송됩니다.

  1. 템플릿 - 컴포넌트 코드 / 정적 템플릿
  2. 데이터 - 템플릿을 채우기 위한 데이터
  3. 구현된 뷰 - 최종 HTML

템플릿 뷰는 번들된 자바스크립트와 렌더링된 HTML, 두 가지 모두에 포함되어 있으며, 데이터도 페이지에 렌더링된 스크립트 태그와 최종 HTML의 일부, 두 가지 모두에 표시됩니다.

클라이언트 렌더링의 경우 템플릿을 보내고 렌더링할 데이터를 요청합니다. 중복은 없습니다. 그러나 무언가를 표시하려면 자바스크립트 번들을 로드하는 네트워크를 기다릴 수 밖에 없습니다.

따라서 서버에서 구현된 HTML을 사용하면 서버 렌더링의 모든 이점을 얻을 수 있습니다. 이는 사이트를 표시함에 있어 자바스크립트 로딩 시간에 구애받지 않게 해줍니다. 하지만 서버 렌더링으로 인해 발생하는 추가적인 비용은 어떻게 해결해야 할까요?

정적 라우팅 (하이드레이션 없음)

예시: Remix, SvelteKit, SolidStart

여러 자바스크립트 SSR 프레임워크에 채택된 아이디어 중 하나는 일부 페이지에서 <script> 태그를 제거하는 기능입니다. 이 페이지들은 정적이고 자바스크립트가 필요하지 않습니다. 자바스크립트가 없다는 것은 추가적인 트래픽, 데이터 직렬화, 하이드레이션 등이 없다는 것을 의미합니다.

물론 자바스크립트가 필요하지 않다면요. 페이지에 바닐라 자바스크립트를 몰래 넣을 수도 있고 어떤 경우에는 괜찮을 수도 있겠지만, 바람직하진 않습니다. 이는 두 번째 애플리케이션 계층을 생성하는 것입니다.

터무니없지는 않습니다만, 현실적으로 한번 동적 요소를 추가하면 프레임워크를 활용해서 모든 것을 끌어 들이고자 합니다. 이 접근 방식은 SSR을 통해 항상 가능했지만, 유연하지 못합니다. 멋진 요령이지만 대부분의 경우 해결책이 되지 않습니다.

자바스크립트 레이지로딩 (점진적 하이드레이션)

예시: Astro(섬(Islands)과 결합해서)

이 방법은 "점진적" 또는 "레이지(Lazy)" 하이드레이션라고 말합니다. 자바스크립트를 바로 로드하지 않고 인터렉션에 따라 로드하는 것을 의미합니다. 마우스를 클릭하거나, 움직이거나, 스크롤 될 때 등 인터렉션에 따라 로드합니다. 심지어 만약 인터렉션이 없다면 자바스크립트를 아예 보내지 않을 수도 있습니다. 하지만 한 가지 문제가 있습니다.

대부분의 자바스크립트 프레임워크는 하향식으로 하이드레이션 합니다. 이것은 리액트에서도, 스벨트에서도 해당됩니다. 따라서 애플리케이션에 (단일 페이지 애플리케이션처럼) 공통 루트(root)가 포함되어 있는 경우 이를 로드해야 합니다. 그리고 렌더 트리가 너무 얕지 않다면, 화면의 중간 버튼을 클릭했을 때 엄청난 양의 코드를 로드하고 하이드레이션 해야 합니다. 사용자가 무언가를 할 때까지 오버헤드를 미루는 것은 좋지 않습니다. 사용자를 기다리게 할 것이라는 것이 확실하니 오히려 더 안좋다고도 말할 수 있습니다. 하지만 사이트의 라이트하우스 점수는 좋겠죠.

따라서 넓고 얕은 트리가 있는 애플리케이션에서는 도움이 될 수 있겠지만, 최신 SPA에서는 그렇지 않습니다. 클라이언트 측 라우팅, 컨텍스트 제공자(Context Provider) 및 경계(Boundary) 컴포넌트(서스펜스, 에러 또는 기타)등의 패턴으로 인해 깊은 트리가 만들어지기 때문입니다.

이 방법만으로는 사용할 수 있는 모든 데이터를 직렬화하는 것을 막을 수 없습니다. 결국 무엇이 로드될지 모르기 때문에 모든 것을 사용할 수 있어야 합니다.

HTML에서 데이터 추출

예시: Prism Compiler

보통 사람들이 바로 생각하는 다른 방법은 렌더링된 HTML에서 상태를 리버스 엔지니어링 하는 것입니다. 큰 JSON blob을 보내는 대신 HTML에 삽입된 값으로부터 상태를 초기화합니다. 겉으로 보기에는 나쁘지 않은 아이디어입니다. 문제는 모델과 뷰가 항상 1 대 1로 대응되지 않는다는 것이죠.

만들어진 데이터를 또 다른 데이터로 만들기 위해 원본으로 되돌리려는 것은 대부분의 경우 불가능합니다. 예를 들어 포맷팅된 타임스탬프를 표시한다고 했을 때, 해당 HTML에서는 초단위를 인코딩하지 않았지만 초단위를 허용하는 다른 UI로 변경하려는 경우 어떻게 해야 할까요?

아쉽게도 이것은 초기화한 상태뿐만 아니라 데이터베이스와 API로부터 오는 데이터에도 적용됩니다. 또 단순히 모든 것을 직렬화하지 않을 수도 없습니다. 대부분의 하이드레이션은 브라우저가 탑다운 방식으로 초기화하는 시점에 애플리케이션을 다시 실행한다는 것을 유념해야 합니다. 동형(Isomorphic) 데이터 페칭(fetching) 기능은, 데이터를 전송하지 않고 일종의 클라이언트 사이드 캐시를 설정하지 않으면, 이 때 브라우저에서 페칭을 다시 시도합니다.

섬 (부분 하이드레이션)

예시: Marko, Astro

웹 페이지를 브라우저에서 다시 렌더링하거나 하이드레이션할 필요가 없는 거의 정적인 HTML이라고 상상해보세요. 그 안에는 사용자가 인터렉션할 수 있는 몇 개의 장소가 있는데, 이를 "섬"이라고 부릅니다. 이러한 접근 방식을 부분 하이드레이션이라고 하는데, 그 이유는 이러한 섬에만 하이드레이션 하면 되고 페이지의 다른 부분에 대한 자바스크립트 전송을 건너뛸 수 있기 때문입니다.

이러한 방식으로 설계된 애플리케이션에서는 인풋 또는 프로퍼티를 최상위 컴포넌트에 직렬화하기만 하면 됩니다. 더 상위에 상태를 저장하고 있는 것이 없다는 것을 알고 있기 때문입니다. 상위 레벨에서의 리렌더링은 절대 일어나지 않습니다. 섬 밖에 있는 것들은 바뀌지 않습니다. 따라서 단순히 사용하지 않는 데이터를 보내지 않는 것만으로도 이중 데이터 문제를 많이 해결할 수 있습니다. 최상위 인풋이 아니라면 브라우저에서 필요할 리 없습니다.

그렇다면 경계(boundaries)는 어디에 둬야 할까요? 컴포넌트 수준에 경계를 두는 것은 이해할 수 있기에 합리적입니다. 하지만 섬들은 더 세분화 될수록 더 효과적입니다. 섬 아래에 있는 어떤 것이든 다시 렌더링 될 수 있다면 그 코드는 브라우저로 전송해야 합니다.

하나의 해결책은 하위 컴포넌트 레벨에서 상태를 확인할 수 있을 정도로 스마트한 컴파일러를 개발하는 것입니다. 그렇다면 우리 트리에서 정적인 가지뿐만 아니라 상태를 저장하고 있는 컴포넌트 아래에 중첩된 가지도 제거할 수 있습니다. 그러나 이러한 컴파일러는 크로스 모듈 방식으로 분석될 수 있도록 특화된 DSL(Domain Specific Language, 도메인 특화 언어)이 필요합니다.

더 중요한 것은, 네비게이션 시 서버에서 각 페이지를 렌더링한다는 것입니다. 이 다중 페이지(MPA) 접근 방식은 웹이 고전적으로 동작하는 방식입니다. 그러나 클라이언트 측 트랜지션으로 네비게이션 하지 않기 때문에 클라이언트의 상태가 손실됩니다. 사실상 부분 하이드레이션은 위에서 언급한 정적 라우팅에서 사용하는 기능에 대해서만 비용이 발생하도록 개선된 버전입니다.

순서가 뒤바뀐 하이드레이션

예시: Qwik

부분 하이드레이션이 정적 라우팅의 개선된 버전이라면, 순서가 뒤바뀐 하이드레이션은 레이지 로딩의 개선된 버전입니다. 하이드레이션이 일반적인 하향식 렌더링 프레임워크에 의해 제한되지 않는다면 어떨까요? 그렇다면 화면 중간 버튼의 하이드레이션이 컴포넌트 계층의 상위에 있는 클라이언트 라우팅과 상태 관리 로직을 로딩하는 것과 상관없이 가능합니다.

여기엔 꽤 어려운 제약이 있습니다. 이 기능이 작동하려면 컴포넌트가 부모에 의존하지 않고 초기에 작동하는 데 필요한 모든 것을 갖추고 있어야 합니다. 그러나 컴포넌트는 인풋 또는 프로퍼티를 통해 부모와 직접적인 관계를 가집니다.

한 가지 해결책은 각각의 컴포넌트에 의존성을 주입하여 모든 인풋값을 주는 것입니다. 이로써 부모 자식들 사이에 직접적인 관계를 갖지 않게됩니다. 또, 서버 렌더 시 모든 컴포넌트의 인풋을 직렬화할 수 있습니다(물론 중복이 발생하겠죠).

하지만 이것은 컴포넌트에 전달되는 하위 요소들(children)에게도 적용됩니다. 모든 하위 요소들은 사전에 완전히 렌더링될 필요가 있습니다. 현존하는 프레임워크들은 매우 타당한 이유로 이러한 방식으로 작동하지 않습니다. 지연 평가(Lazy Evaluation)는 하위 요소가 어떻게 그리고 언제 삽입되는지 제어할 수 있는 능력을 줍니다. 한때 하위 요소들이 한번에 평가되도록 동작했던 거의 모든 프레임워크들이 이제는 지연 평가를 사용하고 있습니다.

우리에게 익숙한 부모 자식 상호 작용의 규칙들이 조정되고 제한될 필요가 있기 때문에 익숙하지 않은 방식으로 개발되게 됩니다. 또한 이 방식은 레이지 로딩과 마찬가지로 데이터 중복을 줄일 수 없습니다. 어떤 컴포넌트를 실제로 브라우저로 전송해야 하는지 알 수 없기 때문입니다.

서버 컴포넌트

예시: 리액트 서버 컴포넌트

부분 하이드레이션을 적용하지만, 그 후에 서버에서 정적인 부분을 다시 렌더링한다면 어떨까요? 그렇게 한다면 서버 컴포넌트가 있어야 합니다. 컴포넌트 코드 크기가 줄어들고 중복 데이터가 제거되므로 부분 하이드레이션의 많은 이점을 가져가면서 네비게이션 시 클라이언트 측 상태를 유지하는 것도 포기하지 않을 수 있습니다.

문제는 정적인 부분을 서버에서 다시 렌더링하려면 기존 HTML과 차이를 비교할 수 있는 특수한 데이터 형식이 필요하다는 것입니다. 또한 초기 렌더 시 일반 서버 HTML 렌더링을 유지해야 합니다. 이는 훨씬 더 복잡한 빌드 단계와 서버 컴포넌트와 클라이언트 컴포넌트 간에 다른 종류의 컴파일 및 번들이 필요함을 의미합니다.

더 나아가서, 증가하는 오버헤드를 제거했더라도 이 작업을 수행하려면 브라우저에서 더 큰 런타임이 필요합니다. 따라서 이 시스템의 복잡성은 더 큰 웹사이트와 어플리케이션이 될 때까지 비용을 상쇄하지 못할 것입니다. 하지만 그 한계점에 다다른다면, 이 방법은 무엇이든 가능하다고 느껴집니다. 초기 페이지 로드를 최대화하는 최선의 방법은 아니지만 자바스크립트 코드를 증가시키지 않고도 SPA의 이점을 유지할 수 있는 고유한 방법입니다.

결론

이것은 계속해서 연구되고 있는 분야이기 때문에 새로운 기술들이 끊임없이 등장하고 있습니다. 그리고 이 문제의 핵심은 다양한 기술의 조합이 최선의 해결책일 수 있다는 것입니다.

하위 컴포넌트 섬을 자동으로 생성하고, 순서가 뒤바뀐 하이드레이션이 가능하고, 서버 컴포넌트를 지원하는 컴파일러를 사용한다면 어떨까요? 우린 세상에서 가장 좋은걸 가질 수 있을 거에요, 그렇죠?

혹은 트레이드오프가 너무 커서 사람들이 사람들이 이해하는 방식과 맞지 않을 수도 있습니다. 해결책이 너무 복잡할 수 있죠.

이 문제가 해결될 수 있는 방법은 다양합니다. 이 글로 인해서 현대 자바스크립트의 가장 어려운 문제들 중 하나를 해결하기 위해 지난 몇 년간 진행되어온 작업에 대한 더 많은 인사이트를 얻었기를 바랍니다.

4개의 댓글

comment-user-thumbnail
2022년 2월 21일

좋은 인사이트입니다 🙇‍♂️

답글 달기
comment-user-thumbnail
2022년 4월 24일

좋은 글 번역 너무 너무 감사합니다!!

답글 달기
comment-user-thumbnail
2022년 6월 16일

잘읽었습니다

답글 달기