React의 Rehydration에 대한 놀라운 깨달음
TL; DR
서버사이드 렌더링은 두 가지 관점에서 생각해야 한다.
- 보편적으로 먼저 만들 수 있는 것들 (상자의 포장)
- 클라이언트 별로 달라질 수 있는 것들 (상자의 내용물과 유통기한)
재수화시 리액트는 서버 사이드 렌더링 결과와 클라이언트에서 렌더링한 결과가 동일할 것으로 예상한다.
- 렌더링 로직에 결과가 클라이언트 측, 서버 측에서 다를 수 있는 부분이 들어가 있다면 오류가 발생한다.
- useHasMount와 같이 클라이언트에서만 동작 가능한 상태 로직을 포함한 훅을 사용하면 된다.
- next13의 리액트 서버 컴포넌트, 리믹스를 사용하는 것도 하나의 방법이다.
- next13을 이용하면 코드의 변경을 최소화 / 제거하며 CSR, SSR, ISR 등을 쉽게 지원할 수 있다.
The Perils of Rehydration
A surprisingly-common misconception can lead to big rendering issues that are difficult to debug. This deep-dive tutorial examines how React and Gatsby can be used to pre-render content, and how we can work around the constraints to build dynamic, personal
www.joshwcomeau.com
<!-- In development, things are correct -->
<main>
<div class="ContentFooter">
Last updated: <strong>Sometime</strong>
</div>
<div class="NewsletterSignup">
<form>
<!-- Newsletter signup form stuff -->
</form>
</div>
</main>
운영 환경에서는, 상단 컴포넌트의 자식으로 컴포넌트가 렌더링 되고 있었습니다.
<!-- In production, things had teleported! -->
<main>
<div class="ContentFooter">
Last updated: <strong>Sometime</strong>
<div class="NewsletterSignup">
<form>
<!-- Newsletter signup form stuff -->
</form>
</div>
</div>
</main>
문제 될 수 있는 코드들
function Navigation() {
if (typeof window === 'undefined') {
return null;
}
// Pretend that this function exists,
// and returns either a user object or `null`.
const user = getUser();
if (user) {
return (
<AuthenticatedNav
user={user}
/>
);
}
return (
<nav>
<a href="/login">Login</a>
</nav>
);
};
서버 사이드 렌더링 101
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Maybe some stuff here -->
</head>
<body>
<div id="root"></div>
<script
src="/static/bundle.js"
></script>
<script
src="/static/0.chunk.js"
></script>
<script
src="/static/main.chunk.js"
></script>
</body>
</html>
그런데 이 모든게 서버 사이드 렌더링인가요?
어떤 사람들은 SSG라고 부르기 시작했습니다.
사람에 따라 "Static Site Generation" 또는 "Server-Side Generated"라고 합니다.
클라이언트의 코드
동적인 Section
코드로 다시 돌아갑시다.
const Navigation = () => {
if (typeof window === 'undefined') {
return null;
}
// Pretend that this function exists,
// and returns either a user object or `null`.
const user = getUser();
if (user) {
return (
<AuthenticatedNav
user={user}
/>
);
}
return (
<nav>
<a href="/login">Login</a>
</nav>
);
};
- 사용자가 로그인한 경우 <AuthenticatedNav/>를 렌더링합니다.
- 사용자가 로그인하지 않은 경우 <UnauthenticatedNav/>를 렌더링합니다.
- 사용자가 로그인했는지 여부를 알 수 없으면 아무 것도 렌더링하지 않습니다.
슈뢰딩거의 사용자
우리 앱도 비슷한 곤경에 직면해 있습니다.
사용자가 사이트에 있는 처음 몇 분 동안은 로그인 여부를 알 수 없습니다.
예시 : Guardian 뉴스 웹사이트는 당신 계정 정보를 표시하기 전에 '로그인' 링크를 표시합니다.
예시 : Airbnb 웹사이트도 로그아웃된 사용자의 네비게이션 바를 디폴트로 보여주는 실수를 했네요
나는 이 문제를 재현하는 미니 Gatsby 앱을 만들었습니다.
3G 속도에서는 잘못된 상태가 꽤 오랫동안 표시됩니다!
멋지지만 완벽하지 않은 해결 방법
const Navigation = () => {
if (typeof window === 'undefined') {
return null;
}
재수화 != 렌더
React 앱이 재수화되면 DOM 구조가 일치한다고 가정합니다.
- 모든 컴포넌트를 마운트하여 DOM이 어떻게 보여야 하는지에 대한 멘탈 모델을 구축합니다.
- 그런 다음 이미 페이지에 있는 DOM 노드와 해당 DOM을 비교합니다.
컴파일 타임에 만들어진 HTML
<!-- The initial HTML
generated at compile-time -->
<header>
<h1>Your Site</h1>
</header>
리액트가 렌더링 후 기대하는 것
<!-- What React expects
after rehydration -->
<header>
<h1>Your Site</h1>
<nav>
<a href="/login">Login</a>
</nav>
</header>
Gatsby의 경우
React 팀은 rehydration 불일치가 펑키한 문제로 이어질 수 있다는 것을 알고 있으며,
콘솔 메시지로 불일치를 강조 표시했습니다.

- 불행히도 Gatsby는 프로덕션용 빌드 시 서버 사이드 렌더링 API만 사용합니다.
- 일반적으로 React 경고는 개발 중에만 실행되기 때문에 이러한 경고는 Gatsby로 빌드할 때 표시되지 않습니다 😱
해결방법
function Navigation() {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
const user = getUser();
if (user) {
return (
<AuthenticatedNav
user={user}
/>
);
}
return (
<nav>
<a href="/login">Login</a>
</nav>
);
};
- useEffect는 구성 요소가 마운트된 후에만 실행됩니다.
- 리하이드레이션 중에 React 앱이 document의 DOM을 채택할 때, useEffect가 아직 호출되지 않은 상태입니다.
- React가 기대한 것과 동일합니다.
- 이전에는 플래그 때문에 달랐습니다
컴파일 타임에 생성된 HTML
<!-- The initial HTML
generated at compile-time -->
<header>
<h1>Your Site</h1>
</header>
리액트가 재수화 시 예상하는 HTML
<!-- What React expects
after rehydration -->
<header>
<h1>Your Site</h1>
</header>
- 처음에는 아무것도 보여주지 않습니다.
- 로그인 되었으면 유저 정보를 보여줍니다.
- 로그인 된 상태가 아니면 로그인 버튼을 보여줍니다.
Two-pass rendering

먼저, 모든 "보편적인" 항목이 인쇄됩니다.
- 로고, 요정 그림, 시리얼의 질감을 보여주기 위해 확대된 사진, 스마트 워치의 무작위 사진.
- 이러한 것들은 정적이기 때문에 한 번에 수백만 장을 미리 대량 생산하고 인쇄할 수 있습니다.
- 박스 제조업체는 유통기한을 모릅니다.
- 그 상자를 채울 시리얼은 아마도 아직 존재하지도 않을 것입니다!
- 그래서 대신 빈 파란색 사각형을 인쇄합니다.
- 훨씬 후 시리얼이 생산되어 상자에 주입된 후에 흰색 유통 기한을 찍고, 배송을 위해 포장할 수 있습니다.
2-패스 렌더링도 똑같습니다.
- 첫 번째 패스는 컴파일 타임에 정적이 아닌 모든 정적 콘텐츠를 생성하고 동적 콘텐츠의 구멍을 남겨두는 작업입니다.
- 두 번째 패스는 React 앱이 클라이언트 상태에 따라 달라지는 모든 동적 콘텐츠를 해당 구멍에 채우는 작업입니다.
성능 영향
추상화
function ClientOnly({ children, ...delegated }) {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
return (
<div {...delegated}>
{children}
</div>
);
}
<ClientOnly>
<Navigation />
</ClientOnly>
function useHasMounted() {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
function Navigation() {
const hasMounted = useHasMounted();
if (!hasMounted) {
return null;
}
const user = getUser();
if (user) {
return (
<AuthenticatedNav
user={user}
/>
);
}
return (
<nav>
<a href="/login">Login</a>
</nav>
);
};
주 : Remix나 next13을 이용해 동적 컨텐츠를 Suspense로 감싸는 방법도 있음!
중요한 부분은 2 패스 렌더링의 멘탈 모델입니다.
렌더링의 첫 번째 패스는 컴파일 시간에 훨씬 앞서 발생하며 페이지의 기초를 만들어, 모든 사용자에게 보편적인 모든 것을 채웁니다.
렌더링의 두 번째 패스는 그 다음 훨씬 나중에 사람마다 다른 상태를 이용해 빈 구멍을 채웁니다.
'FrontEnd' 카테고리의 다른 글
[번역] 자바스크립트 디버깅 완벽가이드 (0) | 2022.12.04 |
---|---|
[타입스크립트] Object vs object vs {} (2) | 2022.11.29 |
[Vue3] 일반적인(intrinsic한) 컴포넌트 만들기 with typescript (0) | 2022.11.27 |
[번역]모듈 번들러는 무엇이며 어떻게 동작하는가? (0) | 2022.11.27 |
Javascript ES module과 순환 참조 해결하기 (0) | 2022.11.27 |