[React] createRoot, hydrateRoot
2024. 3. 19. 01:29ㆍReact
728x90
반응형
리액트를 쓸 수 있게 해주는 루트를 만드는 API들!
Next.js 같은 프레임워크를 쓴다면 별로 쓸 일이 없을 것 같긴 한...🫠
📄 Docs
🔗 createRoot
🔗 hydrateRoot
createRoot
- 브라우저의 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성하는 API
1. Reference
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
- 루트를 생성한 후, 그 안에 React 컴포넌트를 표시하기 위해
root.render(<App />;)
를 호출해야 한다. - 온전히 React만으로 구축된 앱은 보통 루트 컴포넌트에 대한
createRoot
호출이 하나만 있다. - 페이지의 일부에만 React를 사용하는 경우 필요한 만큼 여러 개의 독립적인 루트를 가질 수 있다.
1) Parameters
domNode
- 루트를 생성할 DOM 엘리먼트
options (optional)
- 루트에 대한 옵션을 가진 객체
- onRecoverableError: 오류로부터 자동으로 복구될 때 호출될 콜백
- identifierPrefix: useId에 의해 생성된 ID에 사용할 문자열 접두사로, 같은 페이지에서 여러 개의 루트를 사용할 때 충돌을 피하는 데 유용하다.
2) Returns
render
메서드와unmount
메서드가 있는 객체를 반환한다.
3) 주의사항
- 앱이 서버에서 렌더링되는 경우
createRoot
를 사용할 수 없고,hydrateRoot
를 사용해야 한다. - 앱에
createRoot
호출이 하나만 있을 가능성이 높다. 프레임워크를 사용하는 경우에는 프레임워크가 대신 호출 해줄걸 - 현재 컴포넌트의 자식이 아닌 DOM 트리의 다른 부분(ex: Modal, Tooltip 등)에 JSX 조각을 렌더링하려는 경우에는
createPortal
을 사용해야 한다.
이어서, 리턴되는 객체에 담긴
render
메서드와unmount
메서드에 대해 알아보자
1-1. root.render(reactNode)
root.render(<App />);
- 이걸 호출해서 JSX 조각을 React 루트의 브라우저 DOM 노드에 표시한다.
root.render(<App />);
를 호출하면, React는 루트 안에<App/>
을 표시하고 그 안의 DOM 관리를 인수한다. 😎
1) Parameters
reactNode
- 표시하려는 React 노드
- 일반적으로
<App />
과 같은 JSX 조각이 되지만,createElement()
로 구성된 React 엘리먼트, 문자열, 숫자, null, undefined도 ㄱㄴ
2) Returns
- undefined
3) 주의사항
- 처음 호출할 때, React는 React 컴포넌트를 렌더링하기 전에 React 루트 안에 있던 기존 HTML 컨텐츠를 전부 지우고 React 컴포넌트를 그 안에 렌더링한다.
- 서버에서 또는 빌드 중에 React에 의해 생성된 HTML이 루트의 DOM 노드에 포함된 경우,
createRoot
대신에 이벤트 핸들러를 기존 HTML에 첨부하는hydrateRoot()
를 사용해야 한다. - 같은 루트에 render를 여러 번 호출하면, React는 최신 JSX를 반영하도록 필요에 따라 DOM을 업데이트한다.
- 이전에 렌더링 된 트리와 비교해서 재사용할 수 있는 부분과 다시 만들어야 하는 부분을 결정한다.
- 같은 루트에 다시 render를 호출하는 것은 set 함수를 호출하는 것과 비슷하다.
- 즉, 불필요한 업데이트는 알아서 피한다. 👍
1-2. root.unmount()
root.unmount();
- React 루트 안에 렌더링된 트리를 파괴한다.
- 온전히 React만으로 작성된 앱에는 보통
root.unmount
을 호출하지 않는다. - 이 함수는 React 루트의 DOM 노드 (또는 그 조상 노드)가 다른 코드에 의해 DOM에서 제거될 수 있는 경우에 유용하다.
- ex: DOM에서 비활성 탭을 제거하는 jQuery 탭 패널은 탭이 제거되면 그 안에 있는 모든 것도 DOM에서 제거될 테니 이때
root.unmount
를 호출해서 제거된 루트의 컨텐츠 관리를 중지하도록 React에 알려줘야 한다. - 그러지 않으면 제거된 루트 내부의 컴포넌트가 구독과 같은 전역 리소스를 정리하고 확보할 수 없다.
- ex: DOM에서 비활성 탭을 제거하는 jQuery 탭 패널은 탭이 제거되면 그 안에 있는 모든 것도 DOM에서 제거될 테니 이때
root.unmount
를 호출하면 루트의 모든 컴포넌트가 unmount되고, 트리의 이벤트 핸들러나 state가 제거되며, 루트 DOM 노드에서 React가 분리된다.- Parameters와 Returns 없음
1) 주의사항
root.unmount
를 호출하고 나면 같은 루트에 대해root.render
를 다시 호출할 수 없다.- unmount된 루트에서
root.render
를 호출하려고 하면Cannot update an unmounted root
오류가 발생한다. - but, 해당 노드의 이전 루트가 unmount 된 후 동일한 DOM 노드에 새로운 루트를 만드는 건 ㄱㄴ (새로 createRoot 해야된다는 뜻)
- unmount된 루트에서
2. Usage
1) React뿐인 앱 렌더링하기
- 전체 앱에 대해 단일 루트를 생성한다.
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
- 이 코드는 시작할 때 한 번만 실행하면 된다.
- 앱이 온전히 React만으로 작성되었으면 추가적으로 루트를 더 만들거나
root.render
를 다시 호출할 필요 없다.- 이 시점부터 React는 전체 앱의 DOM을 관리하므로,
- 컴포넌트를 더 추가하려면 App 컴포넌트 안에 중첩 ㄱㄱ
- UI 업데이트는 각 컴포넌트의 state를 통해 ㄱㄱ
- 모달, 툴팁과 같은 추가 컨텐츠를 DOM 노드 외부에 표시해야 하는 경우 portal로 렌더링 ㄱㄱ
- 이 시점부터 React는 전체 앱의 DOM을 관리하므로,
- HTML이 비어있으면 앱의 JS 코드가 로드되고 실행될 때까지 사용자에게 빈 페이지가 표시되어서 느리게 느껴질 수 있다.
- 이 문제를 해결하기 위해 서버에서 or 빌드 중에 컴포넌트로부터 초기 HTML을 생성할 수 있다.
- 이런 최적화를 기본적으로 수행하는 프레임워크를 사용하는 것을 추천한다.
- 실행 시점에 따라 이를 SSR(server-side rendering) 또는 SSG(static site generation)라고 한다.
- SSR이나 SSG를 사용하는 앱은 createRoot 대신 hydrateRoot를 호출해야 한다.
- 그러면 React는 DOM 노드를 파괴하고 재생성하는 대신 재사용(hydrate)한다.
2) 부분적으로 React로 작성된 페이지 렌더링하기
- React가 관리하는 각 최상위 UI에 대한 루트를 생성하기 위해
createRoot
를 여러 번 호출하고, 루트마다root.render
를 호출해서 각각 다른 컨텐츠를 표시할 수 있다.
import './styles.css';
import { createRoot } from 'react-dom/client';
import { Comments, Navigation } from './Components.js';
const navDomNode = document.getElementById('navigation');
const navRoot = createRoot(navDomNode);
navRoot.render(<Navigation />);
const commentDomNode = document.getElementById('comments');
const commentRoot = createRoot(commentDomNode);
commentRoot.render(<Comments />);
document.createElement()
를 사용해서 새 DOM 노드를 생성하고 문서에 수동으로 추가할 수도 있다.
const domNode = document.createElement('div');
const root = createRoot(domNode);
root.render(<Comment />);
document.body.appendChild(domNode); // You can add it anywhere in the document
- 이 기능은 React 컴포넌트가 다른 프레임워크로 작성된 앱 내부에 있는 경우에 유용하다.
3) 루트 컴포넌트 업데이트하기
- 같은 루트에서
render
를 여러 번 호출할 수 있다. - 컴포넌트 트리 구조가 이전 렌더링과 일치하면 React는 기존 state를 유지한다.
를 볼 수 있는 괴상한 예시 👇
const root = createRoot(document.getElementById('root'));
let i = 0;
setInterval(() => {
root.render(<App counter={i} />);
i++;
}, 1000);
이딴식으로 계속 다시 render를 호출해도 컴포넌트 안의 state가 유지된다.
- 하지만 이렇게 render를 여러 번 호출하는 경우는 드문 일이다. 일반적으로는 컴포넌트가 state를 업데이트 한다.
3. Troubleshooting
1) Target container is not a DOM element
오류
createRoot
에 전달한 것이 DOM 노드가 아닐 때 나는 오류이다.- 잘 모르겠으면 콘솔에 로깅해보자
const domNode = document.getElementById('root');
console.log(domNode); // ???
const root = createRoot(domNode);
root.render(<App />);
예상되는 이유
- id를 잘못 적었거나
- HTML에서
<script>
태그는 그 뒤에 나타나는 DOM 노드를 볼 수 없음
2) Functions are not valid as a React child.
오류
root.render
에 전달한 것이 React 컴포넌트가 아닐 때 나는 오류
3) 서버에서 렌더링된 HTML이 처음부터 다시 생성됨
- 서버에서 렌더링된 앱은
createRoot
대신hydrateRoot
써야 함! - 앱이 서버에서 렌더링되어 React에 의해 생성된 초기 HTML을 포함하는 경우,
root.render
를 호출하는 과정에서, 모든 HTML이 삭제되고 모든 DOM 노드가 처음부터 다시 생성된다. - 이렇게 하면 속도가 느려지고, 포커스와 스크롤 위치가 재설정되며, 그 밖의 다른 사용자 입력들도 손실될 수 있다.
hydrateRoot
react-dom/server
를 통해 사전에 생성한 HTML 컨텐츠를 가진 브라우저 DOM 노드 안에 React 컴포넌트를 표시할 수 있게 해주는 API
1. Reference
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, reactNode);
- 이미 서버 환경에서 React에 의해 렌더링된 기존 HTML에 React를 붙이는(attach) 방식
- React는 DOM 노드 내부에 존재하는 HTML에 붙어 그 안의 DOM을 관리하게 된다.
- 전부 React로 구성된 앱은 일반적으로 루트 컴포넌트와 함께 hydrateRoot를 한 번만 호출한다.
1) Parameters
domNode
- 서버에서 루트 요소로서 렌더링된 DOM 요소
reactNode
- 기존 HTML을 렌더링하는 데 사용된 React 노드
- 보통 ReactDOM 서버 메서드인
renderToPipeableStream(<App />)
으로 렌더링된 JSX 조각인<App />
이 된다.
options (optional)
- 루트에 대한 옵션을 가진 객체
- onRecoverableError: 오류로부터 자동으로 복구될 때 호출될 콜백
- identifierPrefix: useId에 의해 생성된 ID에 사용할 문자열 접두사로, 같은 페이지에서 여러 개의 루트를 사용할 때 충돌을 피하는 데 유용하다. 서버에서 사용된 것과 동일한 접두어여야 한다!
2) Returns
render
메서드와unmount
메서드가 있는 객체를 반환한다.
3) 주의사항
hydrateRoot
는 렌더링된 컨텐츠가 서버에서 렌더링된 컨텐츠와 동일하다고 예상하므로, 불일치를 버그로 취급하고 고쳐야 한다.- dev mode에서 React는 hydration 중 일어나는 불일치에 대해 경고해준다.
- 여기서 불일치는 서버에서 렌더링된 HTML과 클라이언트 사이드에서의 React 컴포넌트 간에 일치하지 않는 부분이 있는 것을 의미한다.
- 속성이 불일치할 경우 해당 속성이 올바르게 적용되지 않을 수 있다.
- 대부분의 앱에서 불일치는 드물기 때문에 성능을 위해 모든 markup을 검증하지 않는다.
- App에서
hydrateRoot
는 단 한 번만 호출하게 될 것이고, 프레임워크를 사용한다면 프레임워크가 대신 해줄걸! - App을 사전에 렌더링된 HTML 없이 클라이언트에서 직접 렌더링한다면
hydrateRoot
말고createRoot
를 사용 ㄱㄱ
이어서, 리턴되는 객체에 담긴
render
메서드와unmount
메서드에 대해 알아보자
1-1. root.render(reactNode)
root.render(<App />);
- hydrate된 React 루트 내부의 컴포넌트를 업데이트하기 위해
root.render
를 호출한다. - 리액트는 hydrate된 루트 안에서
<App />
을 업데이트 한다.
1) Parameters
reactNode
- 업데이트하려는 React 노드
- 일반적으로
<App />
과 같은 JSX 조각이 되지만,createElement()
로 구성된 React 엘리먼트, 문자열, 숫자, null, undefined도 ㄱㄴ
2) Returns
- undefined
3) 주의사항
- root가 hydrating을 마치기 전에
root.render
를 호출하면, React는 기존의 서버에서 렌더링된 HTML 내용을 지우고 전체 루트를 클라이언트 렌더링으로 전환한다.
1-2. root.unmount()
createRoot와 완전 똑같다!
root.unmount();
- React 루트 안에 렌더링된 트리를 파괴한다.
- 온전히 React만으로 작성된 앱에는 보통
root.unmount
을 호출하지 않는다. - 이 함수는 React 루트의 DOM 노드 (또는 그 조상 노드)가 다른 코드에 의해 DOM에서 제거될 수 있는 경우에 유용하다.
- ex: DOM에서 비활성 탭을 제거하는 jQuery 탭 패널은 탭이 제거되면 그 안에 있는 모든 것도 DOM에서 제거될 테니 이때
root.unmount
를 호출해서 제거된 루트의 컨텐츠 관리를 중지하도록 React에 알려줘야 한다. - 그러지 않으면 제거된 루트 내부의 컴포넌트가 구독과 같은 전역 리소스를 정리하고 확보할 수 없다.
- ex: DOM에서 비활성 탭을 제거하는 jQuery 탭 패널은 탭이 제거되면 그 안에 있는 모든 것도 DOM에서 제거될 테니 이때
root.unmount
를 호출하면 루트의 모든 컴포넌트가 unmount되고, 트리의 이벤트 핸들러나 state가 제거되며, 루트 DOM 노드에서 React가 분리된다.- Parameters와 Returns 없음
1) 주의사항
root.unmount
를 호출하고 나면 같은 루트에 대해root.render
를 다시 호출할 수 없다.- unmount된 루트에서
root.render
를 호출하려고 하면Cannot update an unmounted root
오류가 발생한다. - but, 해당 노드의 이전 루트가 unmount 된 후 동일한 DOM 노드에 새로운 루트를 만드는 건 ㄱㄴ (새로 createRoot 해야된다는 뜻)
- unmount된 루트에서
2. Usage
1) 서버에서 렌더링된 HTML을 hydrate하기
react-dom/server
로 앱의 HTML을 만들었으면 클라이언트에서 hydrate 해줘야 한다.
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
- 이렇게 하면 서버 HTML을 브라우저 DOM node에서 React 컴포넌트를 이용해 hydrate 해준다.
- (위에서도 설명했지만) 보통 앱을 시작할 때 단 한 번만 실행한다. (프레임워크를 쓴다면 프레임워크가 알아서 해줄걸!)
- 앱을 hydrate 하기 위해서 React는 컴포넌트의 로직을 사전에 서버에서 만들어진 HTML에 붙인다(attach).
- hydration을 통해 서버에서 만들어진 초기의 HTML 스냅샷을 브라우저에서 실행되는 fully interactive한 앱으로 변환한다.
hydrateRoot
에 전달한 React 트리는 서버에서 만들었던 것과 동일한 출력을 생성해야 한다.- 서버에서 렌더링한 결과와 클라이언트에서 최초로 렌더링한 결과가 같아야 한다는 뜻!
- WHY? 사용자 경험을 위해서!
- 앱이 더 빨리 로드되는 환상을 보여주기 위해서 HTML 스냅샷을 JS 코드가 로드되기 전에 보여주는 것인데, 갑자기 다른 내용을 보여주면 그 환상이 와장창.. 깨짐 💥
- 주로 이런 이유들로 인해 hydration 에러가 발생한다.
- 새 줄(newlines) 같은 추가적인 공백이 React가 생성한 HTML 주변에 있는 경우
- 렌더링 로직에서
typeof window !== 'undefined'
와 같은 검사를 하는 경우 - 렌더링 로직에서
window.matchMedia
와 같은 브라우저 전용 API를 사용하는 경우 - 서버와 클라이언트에서 다른 데이터를 렌더링하는 경우
- React는 일부 hydration 오류에서 복구되지만, 다른 버그처럼 마찬가지로 수정해야 하는 문제이다.
- 알아서 잘 복구되면 성능 저하뿐이지만, 최악의 경우 이벤트 핸들러가 잘못된 요소에 연결될 수도 있다!
2) Document 전체를 hydrate하기
- 앱 전체가 React인 경우
<html>
태그를 포함해 JSX로 된 전체 document를 렌더링할 수 있다. - 글로벌 변수인 document를 hydrateRoot의 첫번째 인자로 주면 됨
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
3) 불가피한 hydration 불일치 에러 억제하기
- 쩔수 없이 element의 속성이나 텍스트가 서버와 클라이언트에서 서로 다를 수밖에 없다면(ex: 타임스탬프) hydration 불일치 경고를 안 보이게 할 수 있다.
- 해당 element에
suppressHydrationWarning={true}
를 전달하면 됨 - 적용한 요소에만 작동하며(자식은 ㄴㄴ), 예외적인 escape hatch일 뿐이므로 남용 금지!
- 이제 React는 이것을 수정하려고 시도하지 않을 것이며, 미래의 업데이트까지 일관성을 잃을 수 있다.
export default function App() {
return (
<h1 suppressHydrationWarning={true}>
Current Date: {new Date().toLocaleDateString()}
</h1>
);
}
4) 서로 다른 클라이언트와 서버 컨텐츠 다루기
- 의도적으로 다른 내용을 렌더링하고싶다면, 클라이언트와 서버에서 서로 다른 방법으로 렌더링하면 된다.
- 클라이언트에서는 Effect 안에서 true로 할당되는 state를 사용할 수 있다.
- 이렇게 하면 처음에는 서버와 동일한 결과물을 렌더링하므로 불일치 문제를 피할 수 있다.
- But! 이렇게 하면 두 번 렌더링되므로 hydration이 느려진다.
// index.html
<div id="root"><h1>Is Server</h1></div>
// App.js
export default function App() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
<h1>
{isClient ? 'Is Client' : 'Is Server'}
</h1>
);
}
5) hydrate된 root 컴포넌트 업데이트하기
- root의 hydrating이 끝난 이후에
root.render
를 호출해서 업데이트 할 수 있다. createRoot
와 다르게 HTML로 최초의 컨텐츠가 이미 렌더링 되어 있기 때문에 자주 사용할 필요는 없다.- hydration 후에
root.render
를 호출했을 때, 컴포넌트의 트리 구조가 이전에 렌더링한 구조와 일치한다면 React는 그 상태를 유지한다. - 이것도 createRoot에서와 마찬가지로 이미 hydrate된 root에
root.render
를 호출하는 것은 흔한 일이 아니다. 컴포넌트 안에서 state를 업데이트 하도록..^a^
728x90
반응형
'React' 카테고리의 다른 글
[React] createPortal (2) | 2024.03.05 |
---|---|
[React] useFormState, useFormStatus (1) | 2024.02.12 |
왜 Hooks는 컴포넌트의 최상위 레벨에서 호출해야 하나요? (1) | 2023.12.29 |