이전 글에서 React에서 Suspense 동작원리를 정리한 포스트를 작성했었다.
핵심은 Promise를 상위 컴포넌트로 던지는 것!!
ref) https://velog.io/@bnb8419/Suspense%EC%99%80-lazy-Loading
가장 대표적인 활용이 코드 스플릿팅을 통한 번들사이즈 감소이다.
React.lazy를 통해 자식 컴포넌트를 분할한다. 컴포넌트가 필요할 때 Promise의 상태(pending, fullfilled..) 에 따라 상위 컴포넌트로 Promise를 던진다.
pending 상태일땐 fallback을, fullfilled일때는 자식컴포넌트를 렌더링한다.
React 18이전에서 Hydration은 보통 다음과 같은 단계로 이루어진다. (Suspense 적용 이전)
JS파일 전체를 불러온 후 다시 리렌더링하는 작업이 실행됨. (원문기준으로 3가지 존재)
유저와 상호작용이 정말 많은 페이지라면? 유저가 실제로 상호작용 할 수 있는 TTI의 시점이 그만큼 늦어진다는 의미임
18V 이전에는 Suspense를 Server Side에서 사용 할 수 없었음.
서버에서 전체 애플리케이션에서 사용할 데이터를 가져온다.
그 후, 서버에서 애플리케이션을 HTML로 렌더링한 후 응답(response)로 보낸다.
그 후, 클라이언트에서 JavaScript를 불러온다.
그 후, 클라이언트에서 서버에서 생성된 HTML에 JavaScript 로직을 연결시킨다.
서버에서 html을 렌더링하기전에 app에 필요한 모든 데이터를 가져와야함.
오늘날의 SSR이 가진 문제 중 하나는 컴포넌트로 하여금 “데이터를 기다리도록” 하지 않는다. 현재 제공되는 API를 사용하면 HTML에 렌더할 때 서버상에서 컴포넌트에 필요한 데이터를 모두 다 준비해놔야 한다. 이 뜻은 클라이언트에 HTML을 보내기 전에 서버상에서 모든 데이터를 모아놔야 한다는 것이다. 이 방법은 꽤나 비효율적이다.
예를들어, 댓글이 있는 글을 렌더링하고 싶다고 가정해보자. 댓글은 이른 시기부터 보여주는 것이 중요하기 때문에, 서버사이드 HTML 출력에 추가하고 싶다. 하지만 DB나 API 레이어의 속도가 느린데 이건 건드릴 수 없는 상황이다. 이럴 경우 힘든 결정을 내려야 한다. 서버 출력물에서 제외하면 유저는 JS가 완벽히 불러와지기 전까지 볼 수 없을 것이다. 하지만 서버 출력에 포함시키면 댓글이 불러와지고 전체 트리를 렌더하기 전까지 나머지 HTML을 전송하는 것을 지연시켜야 한다 (네비게이션바, 사이드바, 그리고 심지어 포스팅 본문까지도 여기에 포함된다). 이건 좋지 않다.
한 가지 덧붙히자면, 몇몇 데이터를 가져오는 방법들은 데이터가 완전히 불러와지기 전까지 트리를 HTML에 렌더하고 결과물을 버리는 방식을 반복적으로 수행한다. 이는 React가 더 좋은 옵션을 제공하지 않기 때문이고, 우리는 이런 극단적인 타협책을 요구하지 않는 방법을 제시하고자 한다.
hydrate를 시작하기 전에 모든 JS코드가 로드되어야 한다.
JavaScript 코드가 불러와진 후, React에게 HTML을 “하이드레이트”하라 지시하고, 이를 통해 페이지는 상호작용이 가능한 상태가 된다. React는 컴포넌트를 렌더링하는 과정 중 서버사이드에서 생성된 HTML을 순회하며 이벤트 핸들러를 붙혀준다. 이게 동작하기 위해 브라우저에서 컴포넌트를 기반으로 생성된 트리(tree)가 서버에서 생성된 트리와 일치하여야 한다. 그렇지 않으면, 리액트는 말 그대로 “일치시킬 수”없다! 가장 안타까운 것은 어떠한 하이드레이션도 시작하기 전에 모든 컴포넌트를 대상으로한 JavaScript가 클라이언트상에 완전히 불러와져야 한다는 점이다.
예를들어, 댓글 위젯은 많은 양의 복잡한 상호작용 로직을 가지고 있고, JavaScript를 불러오기 위해 꽤나 오랜 시간이 걸린다고 가정하자. 다시 힘든 결정을 내려야 한다. 유저에게 이른 시기부터 보여주기 위해 댓글을 서버상에서 HTML로 렌더하는 것이 좋을 것이다. 하지만 오늘날의 하이드레이션은 단일 작업만 가능하기에, 네비게이션바, 사이드바, 그리고 포스트 본문들까지 댓글 위젯에 대한 코드가 불러와지기 전까지 하이드레이션을 할 수 없다. 물론 코드 스플리팅을 통해 따로따로 로드할 수도 잇지만, 이럴 경우 서버 HTML에 있는 댓글을 빼줘야 할 것이다. 그렇지 않으면 React는 HTML의 일부(chunk)만 가지고 무얼 해야할지 모르고 (이 코드는 어디로 가는 것일까?) 하이드레이션 단계에서 해당 코드를 삭제할 것이다.
상호작용을 시작하기 전 모든 html에 hydrate가 완료되어야 한다.
하이드레이션 자체에도 비슷한 문제가 있다. 오늘날 React는 트리를 한 번의 작업을 통해 하이드레이션을 진행한다. 이 뜻은, 하이드레이션을 한 번 시작하면 (말하자면 컴포넌트 함수를 호출하는 과정), React는 전체 트리에 대해 이 과정을 완료하기 전까지 멈추지 않는다. 결과적으로, 컴포넌트 중 어느 하나라도 상호작용 하기 위해서는 모든 컴포넌트가 하이드레이션 되어야 한다.
예를들어, 댓글 위젯쪽에 굉장히 시간이 오래 걸리는 렌더링 로직이 들어있다고 가정하자. 본인의 컴퓨터에서는 빠르게 동작할 수 있지만, 저사양 디바이스에서는 모든 로직을 실행하는 것이 빠르지 않고, 심지어 몇 초간 화면을 고정시킬 수도 있다. 물론, 이상적으로 클라이언트 사이드에 이런 로직은 없을 것이다 (그리고 이런 경우를 대비하기 위해 Server Component가 개발되고 있는 것이다). 하지만 몇몇 로직은 부착된 이벤트 핸들러의 작업을 결정하고 상호작용에 필수적이기 때문에 이런 상황이 불가피하다. 결과적으로 한 번 하이드레이션이 시작되면 전체 트리가 완전히 하이드레이션 되기 전까지 유저는 네비게이션 바, 사이드바, 포스팅 본문과 상호작용할 수 없다. 특히나 네비게이션의 경우 유저가 이 페이지 자체에서 떠나고 싶지만 현재 클라이언트에서 열심히 하이드레이션을 진행하고 있기 때문에 더 이상 보고 싶지 않은 페이지에 남아 있어야 하는 굉장히 안좋은 케이스다.
아래와 같은 기능이 존재(관련PR)
Full built-in support for (which integrates with data fetching)
Code splitting with lazy without flashes of "disappearing" content
Streaming of HTML with "delayed" content blocks "popping in" later
import { lazy } from "react";
const Comments = lazy(() => import("./Comments.js"));
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
Hydration은 Next.js만의 특별한 동작이 아니라 ReactDOM함수임.
ReactDOM.hydrate 함수를 사용해 하이드레이션 진행
ReactDOM.render(element, container, [callback]);
ReactDOM.hydrate(element, container, [callback]);
next.js에서 컴포넌트 동적로딩을 구현하는 방법은 두가지가 존재한다.
Default로 Server Component일 경우 자동적으로 코드스플릿팅이 적용된다. ( streaming & hydration 자동적용)
Server Component란?
서버 컴포넌트에 관한 자세한 내용은 아래 두 블로그를 참고하자
ref) https://haesoo9410.tistory.com/404
ref) https://velog.io/@sententia/React-Server-Component-RSC
클라이언트 컴포넌트에게 동적로딩을 적용가능함.
React.lazy + Suspense 조합과 Dynamic Import를 사용하는 방법이 있음.
next/dynamic is a composite of React.lazy() and Suspense.
It behaves the same way in the app and pages directories to allow for incremental migration.
fallback 설정 및 ssr : false로 설정하면 클라이언트 사이드에서 렌더링됨.
import dynamic from 'next/dynamic'
const DynamicHeader = dynamic(() => import('../components/header'), {
loading: () => <p>Loading...</p>,
ssr: false,
})
export default function Home() {
return <DynamicHeader />
}
ref) https://velog.io/@lky5697/suspense-in-different-architectures
ref) https://immigration9.github.io/react/2021/06/13/new-suspense-ssr-architecture.html