일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- react native
- javascript
- react
- react animation
- Throttle
- RN새로운아키텍쳐
- React-Native업데이트
- react-native-permissions
- Swift
- no-permission-handler-detected
- hydration mismatch
- RN아키텍쳐
- rn
- Promise
- axios
- motion.div
- await
- debounce
- react-native
- private-access-to-photos
- CS
- RN업데이트
- async
- 비동기
- promise.all
- named type
- ios
- react-native-image-picker
- animation
- Hash-table
- Today
- Total
하루살이 개발일지
[React] hydration과 hydration mismatch에 대해서 본문
CSR, SSR
먼저 Hydration 개념 이해에 필요한 CSR과 SSR 개념을 간단히 정리하면 다음과 같다.
클라이언트 사이드 렌더링 CSR에서 서버는 빈 HTML 페이지와 JavaScript 번들을 반환한다. 브라우저는 JavaScript 번들을 받아 클라이언트 단에서 페이지를 렌더링한다. 즉 모든 로직, 데이터 패칭, 라우팅 등은 서버가 아닌 클라이언트에서 처리된다.
이는 서버의 부하를 줄이고 첫 페이지 로드 후 다른 페이지를 로드하는 속도가 빨라지지만, 초기 빈 HTML을 전달해주기 때문에 검색 엔진 최적화(SEO)에 불리하며 초기 로드 시간이 길어지게 된다.
서버 사이드 렌더링 SSR은 서버에서 웹 페이지를 생성해 클라이언트로 전달한다. 이미 렌더 준비가 완료된 HTML을 전달해주기 때문에 HTML은 즉시 렌더링되고 초기 렌더링 시간이 줄어들게 된다. 또한 SEO에 유리하다. 그러나 SSR은 모든 요청이 서버에서 처리되므로 서버에 부하가 갈 수 있으며 보안과 캐싱에 더 많은 노력이 필요하다.
Hydration in React
Hydration은 웹 개발에서 서버 사이드 렌더링(SSR)과 관련된 중요한 개념이다. 이 과정을 통해 서버에서 미리 렌더링(SSR)된 HTML을 클라이언트 측에서 React를 사용하여 완전히 인터렉티브한 웹 애플리케이션으로 변환할 수 있다.
기존 React와 CSR
일반적으로 React 애플리케이션은 클라이언트 사이드 렌더링(CSR)을 사용한다. 이 방식에서 클라이언트는 서버로부터 빈 껍데기인 HTML을 받아오고, React와 같은 JavaScript 라이브러리를 통해 컴포넌트를 생성하고 DOM에 추가한다. 예를 들어, 다음과 같은 HTML을 브라우저가 받는다고 가정해보자 :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
여기서 `bundle.js`는 React 애플리케이션의 모든 로직을 포함한 JavaScript 파일이다. 브라우저는 이 JavaScript 파일을 다운로드하고 실행하여, ReactDOM을 사용하여 React 컴포넌트를 `#root` 요소에 렌더링한다.
서버 사이드 렌더링(SSR)과 Hydration
최신 버전의 React(v18)에서는 서버 측에서 컴포넌트를 렌더링할 수 있다. 이는 서버가 생성하여 클라이언트에 전송한 HTML이 React에서 기원한다는 것을 의미한다. 하지만 이 HTML은 대화형이 아니다. 여기서 Hydration이 필요하다.
즉 Hydration은 서버에서 렌더링된 HTML에 React가 이벤트 리스너를 추가하여, 클라이언트 측에서 대화형 애플리케이션으로 만드는 과정이다. 이를 통해 초기 로딩 시점에서는 정적 콘텐츠를 보여주고, 이후 JavaScript가 로드된 후 대화형 기능을 활성화할 수 있다.
Hydration에 사용되는 `hydrateRoot` 함수는 `react-dom/server`에 의해 생성된 HTML 콘텐츠를 포함하는 DOM 노드 내에 React 컴포넌트를 연결한다. 이는 해당 HTML에 React의 이벤트 리스너와 상태 관리를 추가하여, 정척 콘텐츠를 대화형 애플리케이션으로 변환하는 것을 의미한다.
*hydrateRoot() API는 react 18에 등장한 개념이며 hydrate() 메소드를 대체한다.
hydrateRoot 함수는 세 가지 인수를 받을 수 있다 :
hydrateRoot(domNode, reactNode, ?Options)
- domNode : 서버에서 미리 렌더링된 HTML을 포함하는 루트 DOM 요소.
- reactNode : 해당 HTML을 렌더링하는 데 사용되는 React 노드, 일반적으로 JSX 파일.
- Options : (선택적) Hydration 과정에서 추가 설정을 위해 사용됨.
다음은 서버에서 렌더링된 HTML을 클라이언트 측에서 Hydration하는 예시이다 :
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
hydrateRoot(container, <App />);
이 코드에서 `hydrateRoot`는 서버에서 렌더링된 HTML을 포함하는 DOM 노드를 받아, 해당 HTML에 React의 이벤트 리스너와 상태를 추가하여 대화형으로 만든다.
Hydration의 활용예시
다음과 같은 React 코드로 hydration을 구현해 보자 :
import { useState } from 'react';
export default function App() {
return (
<>
<h1>Hello, hydration!</h1>
<Counter />
</>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
You clicked me {count} times
</button>
);
}
연결할 html은 다음과 같다. 가독성을 위해 코드에 줄 바꿈이 있지만 주석처럼 whitespace(newline)가 존재하면 안 된다.
<!-- 원래는 안됨 -->
<div id="root">
<h1>Hello, hydration!</h1>
<button>You clicked me <!-- -->0<!-- --> times</button>
</div>
<!-- 공백이 없어야됨 -->
<div id="root"><h1>Hello, hydration!</h1><button>You clicked me <!-- -->0<!-- --> times</button></div>
index.js에서 hydrateRoot 함수를 활용해 hydration을 시켜준다.
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(
document.getElementById('root'),
<App />
);
hydration을 하기 위해 준비한 html에서 You clicked me <!-- -->0<!-- --> times 의 0을 주목해보자. React 코드의 useState(0)을 통해 처음 렌더링 될 텍스트가 0으로 렌더링될 것이다. 그래서 위와 같이 변경될 수 있는 html은 React의 초기 렌더링 결과물과 동일해야 한다. 실제 next.js로 빌드하면 react예제와 동일한 html이 생성된다. 이를 통해 hydration 과정이 진행된다.
Hydration mismatch
이 과정에서 유의할 점이 있다. React를 통해 렌더링 될 최초의 결과물이 hydration할 html과 반드시 일치해야 하는 것이다. 그렇지 않으면 hydration mismatch가 발생하게 되는데 이는 서버와 클라이언트의 렌더링 결과가 달라서 생기는 문제로, react는 이를 감지하고 경고를 표시한다.
다음은 hydration mismatch가 발생할 수 있는 예시이다.
서버 측에서 렌더링할 html은 다음과 같다고 가정해보자. 서버는 사용자 이름을 포함한 html을 렌더링한다 :
<div id="root">
<h1>Hello, Alice!</h1>
</div>
클라이언트 측에서는 React 컴포넌트를 통해 동일한 요소를 렌더링하지만, 사용자 이름이 변경되었다고 가정해보자. 클라이언트 측에서는 다음과 같은 React 코드가 실행된다 :
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
function App() {
const userName = 'Bob'; // 클라이언트에서 다른 사용자 이름을 사용
return <h1>Hello, {userName}!</h1>;
}
const container = document.getElementById('root');
hydrateRoot(container, <App />);
결과적으로 서버에서 렌더링된 HTML은 <h1>Hello, Alice<h1> 이지만, 클라이언트에서 렌더링된 React 컴포넌트는 <h1>Hello, Bob!<h1>이다. 이로 인해 hydration mismatch가 발생하고 React는 콘솔에 경고를 표시한다. 브라우저 콘솔에 표시되는 경고 메시지는 다음과 같다 :
Warning: Text content did not match. Server: "Hello, Alice!" Client: "Hello, Bob!"
위 예제에서는 서버와 클라이언트 렌더링 결과가 일치하지 않아 react가 이를 감지하고 클라이언트 측에서 html을 업데이트한다. 이는 잠깐의 깜빡임을 유발할 수 있으며 사용자 경험에 영향을 줄 수 있다. 서비스가 멈추는 등의 문제가 발생하지는 않지만 공식 문서에서는 이러한 오류를 반드시 고칠 것을 권장한다.
대표적인 mismatch 케이스와 해결 방법
1. html의 root 노드에 whitespace가 있는 경우
파싱할 html의 root 노드에 whitespace(newline)가 있으면 hydration mismatch가 발생한다.
<!-- hydration error -->
<div id="root">
<h1>Hello, hydration!</h1>
<button>You clicked me <!-- -->0<!-- --> times</button>
</div>
이는 공백이나 개행을 없애면 해결된다.
<!-- correct! -->
<div id="root"><h1>Hello, hydration!</h1><button>You clicked me <!-- -->0<!-- --> times</button></div>
next.js와 같은 프레임워크를 사용하면 이런 부분을 고려하여 html을 생성하기 때문에 경험 확률이 낮지만 CRA 등으로 직접 react 환경을 구축해 hydration 로직을 작성하면 발생하기 쉬운 에러이다.
2. 렌더링 로직에 typeof window !== 'undefined' 같은 clinet/server 상태 분기
렌더링 로직에 window나 localStorage 같은 브라우저 전용 API를 사용할 때 server와 client 환경의 조건을 다르게 하면 발생한다.
export default function App() {
const isClient = typeof window !== 'undefined';
return (
<h1>
{isClient ? 'Is Client' : 'Is Server'}
</h1>
);
}
위 코드로 next.js의 SSR을 활용하면 서버에서 만들어 준 html과 react로 hydration을 진행할 때 문제가 생긴다. next.js의 SSR은 사용자가 요청할 때 html을 만들기 때문에 처음 생성하는 html은 서버 환경에서 만들게 된다.
<!-- 서버에서 만든 html -->
<html>
<h1>Is Server</h1>
</html>
<!-- react 트리가 첫 렌더링 때 기대하는 html -->
<html>
<h1>Is Client</h1>
</html>
실제 렌더링해보면 is server 텍스트가 잠깐 보였다가 is client로 변경된다. 이러한 mismatch를 해결하기 위해 여러 방법이 있는데, 첫 번째로 useEffect를 활용해 첫 렌더링 때 의도적으로 원하는 상태를 업데이트하지 않는 방법이 있다. 아래 예제는 isClient의 첫 상태를 false로 동일하게 맞출 수 있다 :
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
그러나 이런 방법은 컴포넌트가 무조건 두 번은 렌더링하기 때문에 사용자 경험이 저하될 수 있다. 또한 next.js에서는 dynamically import의 {ssr : false} 옵션으로 특정 컴포넌트의 ssr을 비활성화할 수 있다 :
// no-ssr.jsx
const NoSSR = () => {
return <h1>Is Client</h1>
}
// index.js
const NoSSR = dynamic(() => import('../no-ssr'), { ssr: false });
export default function App() {
return (
<div>
<NoSSR />
</div>
);
}
3. 불가피하게 발생하는 mismatch
hydration mismatch의 첫 예제처럼 빌드 결과물이 매번 달라 hydration mismatch가 발생하는 경우 suppressHydrationWarning 옵션으로 mismatch 경고를 억제할 수 있다.
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
function App() {
const userName = 'Bob'; // 클라이언트에서 다른 사용자 이름을 사용
return <h1 suppressHydrationWarning>Hello, {userName}!</h1>;
}
const container = document.getElementById('root');
hydrateRoot(container, <App />);
이렇게 하면 서버와 클라이언트가 일치하지 않는 경우에도 경고가 발생하지 않으나, 근본적인 해결 방법은 아니므로 가능한 서버와 클라이언트의 렌더링 결과를 일치시키는 것이 좋다.
4. 그 외 케이스
1. Browser extension을 통해 html이 변경되는 경우
2. validateDOMNestion Hydration failed
html 태그에 잘못된 중첩이 있는 경우 발생한다. 이 경우 react는 hydration 과정에서 validateDOMNesting 오류를 발생시킨다. hydration 과정 중 이러한 오류가 발생하면 서버에서 렌더링된 html과 클라이언트에서 react가 렌더링하는 dom구조 사이에 불일치가 생기기 때문이다.
아래는 잘못된 html 코드 중 하나이다 :
<p> nesting <div> div </div> </p>
html 표준에 따르면, 특정 태그는 특정 태그를 포함할 수 없다. <p>태그는 '구조적' 요소로, 블록 레벨의 요소를 포함할 수 없다. <div> 태그는 블록 레벨 요소이기 때문에, <p> 태그 안에 포함될 수 없다. 즉 블록 레벨 요소는 블록 레벨 컨테이너 내부에 위치해야 한다. 따라서 위 예시를 다음과 같이 수정할 수 있다 :
<div>
<p>nesting</p>
<div>div</div>
</div>
3. iOS의 safari에서 발생하는 hydration mismatch
iOS에서는 전화번호 포맷을 자동으로 전화번호로 파싱하는 문제가 있다. 이는 meta 태그를 추가하면 해결된다.
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
Reference
https://react.dev/reference/react-dom/client/hydrateRoot
https://dev.to/jitendrachoudhary/what-is-react-hydration-2bc3
https://blog.hwahae.co.kr/all/tech/13604
'웹개발 > React' 카테고리의 다른 글
[React] react-error-boundary로 에러 핸들링하기 (0) | 2024.08.18 |
---|---|
socket으로 실시간 채팅 구현하기 (2) | 2024.07.15 |
Jotai 상태관리 라이브러리에 대해서 (0) | 2024.03.02 |
모듈 패턴(Module Pattern)에 대하여 (1) | 2023.09.13 |
Axios : instance와 interceptor (0) | 2023.07.04 |