React v18의 정식 출시가 코앞에 있고, 가장 큰 변경점 중 하나가 서버사이드 렌더링에 관한 내용이라는 소식을 듣고 마침 SSR 에 대해 흥미가 있던 터라 한번 React v18의 주요 변경점이 뭔지 공부해보기로 했다.
먼저 기존의 React v17을 v18로 마이그레이션 하는 것은 문제 없다고 한다. 리액트 팀에서 이 부분을 특히 신경써서 만들었다고 하니 믿어도 될 것같다.
Automatic batching
먼저 리액트의 배치에 대해 알아야한다.
Batching
리액트에서 더 나은 성능과 예상치않는 버그의 방지를 위해 여러 개의 상태 업데이트를 한번의 리렌더링으로 묶는 작업
아래 예시를 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 아직 리렌더링 되지 않습니다.
setFlag(f => !f); // 아직 리렌더링 되지 않습니다.
// 리액트는 오직 마지막에만 리렌더링을 한 번 수행합니다. (배치 적용)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
handleClick 에서는 총 두번의 상태 업데이트를 했다. 하지만, 리액트에서는 언제나 배치를 수행하여 이 두번의 상태업데이트를 한번의 리렌더링으로 처리한다.
하지만 배치가 수행되지 않는 예외가 존재한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// 리액트 17 및 그 이전 버전에서는 배치가 수행되지 않습니다. 왜냐하면
// 이 코드들은 이벤트 이후의 콜백에서 실행되기 때문입니다.
setCount(c => c + 1); // 리렌더링
setFlag(f => !f); // 리렌더링
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
위처럼 리액트 v17까지는 이벤트 핸들러 안에서의 상태업데이트에서는 배치가 적용되지 않았다.
하지만 v18부터는 자동배치가 추가되어, 이벤트 핸들러 함수 안에서 상태 업데이트에 대해서도 자동으로 배치를 적용시켜준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 리액트는 오직 마지막에만 리렌더링을 한 번 수행합니다. (배치 적용)
}, 1000);
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 리액트는 오직 마지막에만 리렌더링을 한 번 수행합니다. (배치 적용)
})
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
// 리액트는 오직 마지막에만 리렌더링을 한 번 수행합니다. (배치 적용)
});
이 자동 배치를 사용하기위해서는 리액트앱의 최상단에서 ReactDOM.render 함수 대신에 새로운 ReactDOM.createRoot
함수를 사용해야한다.
하지만 자동배치를 하고 싶지 않은 경우도 매우 드물게 있을 것이다. 이때는 다음과 같이 ReactDOM.flushSync라는 함수를 사용하면 해결된다.
1
2
3
4
5
6
7
8
9
10
11
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
}); // 리액트는 즉시 DOM을 업데이트합니다.
flushSync(() => {
setFlag(f => !f);
}); // 리액트는 즉시 DOM을 업데이트합니다.
}
startTransition
startTransition은 동시성(concurrent) API 안에 분류되어있는데, 아직 자세한 내용은 공개되지 않았다고 한다. 추후 조사가 필요함
리액트에서 발생하는 상태업데이트에는 두가지 분류가 있다.
- 긴급업데이트 : 직접적인 상호 작용 반영. (타이핑, 오버, 스크롤링 등) 느려질 경우 UX에 안좋은 영향을 줌
- 전환업데이트 : 하나의 뷰에서 다른 뷰로의 UI 전환. 느려도 어느정도 괜찮음
중요한 건 전환업데이트때문에 긴급업데이트가 방해되면 안된다.
React v17까지는 상태 업데이트를 긴급 혹은 전환으로 명시하는 방법은 없었다. 하지만 React v18부터는 startTansition API를 제공함으로써 전환업데이트를 명시적으로 구분하여 상태업데이트를 할 수 있게 되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useTransition } from 'react';
function SearchBar() {
const [isPending, startTransition] = useTransition();
// ...
function handleChange(e) {
const input = e.target.value;
// 긴급 업데이트: 타이핑 결과를 보여준다.
setInputValue(input);
// 이 안의 모든 상태 업데이트는 전환 업데이트가 된다.
startTransition(() => {
setSearchQuery(input);
});
}
// ...
}
좀 더 이해를 돕기위해 다음 예시를 보자.
React v17 까지의 방식대로 아래와 같이 코드짰다고 해보자
1
2
3
4
5
6
function handleChange(e) {
const input = e.target.value;
setInputValue(input);
setSearchQuery(input);
}
이때, 리액트에서는 배치가 이루어져, setInputValue를 거치고, setSearchQuery를 기다렸다가 한번에 상태업데이트를 했다.
이는 긴급업데이트를 하는데 전환업데이트까지 기다려야하는 UX의 관점에서 안좋은 영향을 끼친다.
하지만, startTansition으로 아래와 같이 코드를 짰다고 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
function handleChange(e) {
const input = e.target.value;
// 긴급 업데이트: 타이핑 결과를 보여준다.
setInputValue(input);
// 이 안의 모든 상태 업데이트는 전환 업데이트가 된다.
startTransition(() => {
// 전환 업데이트: 결과를 보여준다.
setSearchQuery(input);
});
}
setInputValue을 거치고, startTransition가 수행된 후 바로, startTansition 외부에 있는 상태들이 업데이트되고, 내부에 있는 상태업데이트는 pending 상태가 된다. 따라서 긴급업데이트는 바로 되고, 전환업데이트는 기다렸다가 되도록 하여, UX의 관점에서 더 향상된다.
이때, 소수의 케이스에 대응하기 위해 Hook을 거치지 않고, React.startTransition을 사용할 수도 있다.
startTransition을 사용하는 2가지 케이스
- 느린 렌더링 : 작업량이 많아 결과를 보여주기 위한 UI전환까지 시간이 걸리는 상태업데이트
- 느린 네트워크 : 네트워크로부터 데이터를 기다리기 위한 시간이 걸리는 업데이트 -> Suspense와 연계가능
Suspense
Suspense는 SSR에 관한 컴포넌트이기 때문에 먼저 SSR에 대해서 알아야한다.
SSR을 사용하는 이유
React는 자바스크립트로 이루어져있고, 따라서 사용자 브라우저로 자바스크립트가 다운이 되고, 이 자바스크립트를 실행하고, 로드가 완료가 되어야 화면에 컴포넌트들이 보여진다. 즉, 이 자바스크립트를 로딩하는 동안 사용자가 볼수 있는 페이지는 빈페이지 뿐이고, 어플리케이션의 크기가 크다면 이 시간은 더 길어지게 된다.
하지만 SSR을 사용하면, 서버에서 리액트 컴포넌트를 HTML로 렌터링 하여 사용자에게 전송할 수 있다. 이때 자바스크립트는 로딩되지 않았으므로 인터랙션은 불가능하지만, 최소한 사용자가 컨텐츠는 볼 수 있도록 해준다.
자바스크립트가 모두 로딩되면, 이 HTML을 인터랙션이 가능한 상태로 만든다. 이때 우리는 리액트에게 서버에서 생성된 페이지가 있다. 여기에 이벤트 핸들러를 붙여
라고 명령하고, 리액트는 이미 생성되어있는 HTML에 이밴트핸들러를 연결한다. 이 작업을 hydration이라고 한다.
기존 React에서의 SSR 문제점
위에서 설명한 SSR 과정을 정리하면 다음과 같다.
- 서버에서 앱 전체를 위한 데이터를 불러옴.
- 서버에서 앱 전체를 HTML로 렌터링 한 다음에 이를 응답으로 돌려보내준다.
- 클라이언트에서 앱 전체 자바스크립트 코드를 실행한다.
- 클라이언트에서 서버에서 만들어진 HTML과 자바스크립트 로직을 결합한다.(이를 hydration이라고 함.)
문제점 1 : 1번에서의 문제점
HTML을 렌더할때 필요한 데이터를 모두 가지고 있어야한다.
예를 들어, 댓글을 서버 HTML 결과물에 포함시킨다고 결정하면 댓글에 필요한 데이터를 API나 데이터베이스를 통해 불러오고 결과 HTML에 포함시켜야한다. 이때 이 댓글 데이터를 불러오는 것 때문에, 이미 작업이 끝난 다른 HTML들도 내려보내지 못한다.
만약 포함시키지 않으면 자바스크립트를 통해 렌더해야하므로 사용자는 자바스크립트 로딩이 끝날때까지 댓글을 볼 수 없다.
문제점 2 : hydration에서의 문제점 1
hydrating 과정에서 리액트는 컴포넌트들을 렌더링하는 동안, 서버에서 생성한 HTML을 순회하면서 이벤트핸들러를 HTML에 연결한다. 이 작업을 수행하려면 브라우저의 컴포넌트에서 생성된 트리가 서버에서 생성된 트리와 일치되어야한다. 그렇지 않으면 React는 이를 맞추지 못하게 된다. 결과적으로 클라이언트에 있는 모든 컴포넌트의 자바스크립트를 로딩해야 hydrate가 가능해진다.
예를 들어 댓글에 복잡한 기능이 포함되어있어 이를 위한 자바스크립트 로딩이 오래걸린다고 해보자.
서버에서 댓글을 HTML에 포함 시킬 경우, hydration은 한번에 일어나야하기떄문에 댓글 위젯에 있는 코드를 로딩하기 전까지는 다른 컴포넌트(ex : 사이드바, 게시글)에 대해서는 hydration을 할 수가 없다.
코드 분할을 통해서 해결할 수 있지만, 이때는 서버 HTML에서 댓글을 제거해야한다. 그렇지 않으면 리액트는 이 HTML을 어떻게 해야할지 모르고 hydration 중에 삭제해 버린다.
문제점 3 : hydration에서의 문제점 2
현재 리액트는 한번에 모든 트리에 hydration을 진행한다. 즉 일단 hydration을 시작하면, 트리 전체에서 이 작업이 완료할 때까지 멈출 수 없다. 따라서 모든 컴포넌트가 hydration을 할 때까지 기다려야만 사용자는 컴포넌트와 상호작용할 수 있다.
만약 사용자가 로딩이 오래 걸려 페이지에서 벗어나고 싶어해도, 브라우저는 hydration 작업으로 바쁘기 때문에 아직 로딩중이고, 다른 페이지로 이동할 수도 없다.
Suspense
기존 SSR에서의 문제점들의 공통점은 모든 작업들이 전체 앱을 기준으로 일어나고, 한 단계가 끝나야 다음 단계로 넘어가는 방식으로 되어있어 발생한다는 점이다. 따라서 React 18에서는 Suspense를 이용한 두가지 주요 SSR 기능을 제공한다.
- 서버에서 HTML을 스트리밍한다. 이를 위해 renderToString 대신 pipeToNodeWritable을 사용해야한다.
- 클라이언트에서 선택적 hydration : 이를 위해 createRoot를 사용하고, <Suspense> 로 감싼다.
모든 데이터를 불러오기전에, HTML을 스트리밍한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section>
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</section>
</main>
현재 SSR 체계에서 위 HTML을 밭으면 클라이언트를 아래를 그릴 것이다.
그리고 코드를 로딩하고 hydration이 끝나면 아래와 같은 완전한 앱이 완성된다.
하지만 React 18에서는 페이지의 일부를 Suspense로 감쌀 수 있다.
예를들어, 댓글을 Suspense로 감싸고, 로딩되기전까지는 Spinner를 보여지게 할 수 있다.
1
2
3
4
5
6
7
8
9
10
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
<Comments>를 <Suspense>로 감싼 덕분에, 리액트는 댓글 컴포넌트를 기다리지 않고, HTML을 스트리밍할 수 있다.
클라이언트가 받은 최초의 HTML은 다음과 같을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<main>
<nav>
<!--NavBar -->
<a href="/">Home</a>
</nav>
<aside>
<!-- Sidebar -->
<a href="/profile">Profile</a>
</aside>
<article>
<!-- Post -->
<p>Hello world</p>
</article>
<section id="comments-spinner">
<!-- Spinner -->
<img width="400" src="spinner.gif" alt="Loading..." />
</section>
</main>
그리고 댓글 데이터가 준비 된다면, 리액트는 같은 스트림으로 추가적인 HTML을 보낼 텐데, 여기에는 HTML을 올바른 위치에 삽입하기 위한 최소한의 인라인 script 태그가 포함되어있다.
1
2
3
4
5
6
7
8
9
10
11
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document
.getElementById('sections-spinner')
.replaceChildren(document.getElementById('comments'))
</script>
그 결과 리액트 자체가 클라이언트에 로드되기 이전에, 뒤늦게 댓글용 HTML 코드가 삽입될 것이다.
이로써 첫번째 문제를 해결할 수 있다. 더이상 모든 데이터를 fetch할 때까지 기다릴 필요가 없다. 화면 일부분이 초기 HTML을 지연시킨다면, 모든 HTML을 지연시키거나 HTML에서 일부를 제외시킬 필요가 없다. HTML 스트림에서 나중에 해당 부분을 별도로 삽입할 수 있다.
모든 코드가 로드되기 전에 페이지를 hydration 하기
초기 HTML은 일찍 보낼 수 있지만, 아직 전체 hydration이 끝나야 사용자가 인터랙션할 수 있다는 것에는 변함이 없다.
따라서 큰 번들을 피하기 위해 “코드 스플릿”을 통해 코드의 일부를 동기적으로 로딩하지 않게하거나, 혹은 번들러가 별도의 script 태그로 분할하는 방법도 있다.
이때 React.lazy 로 코드를 분할하여 메인번들에서 댓글 코드를 아래처럼 분리할 수 있다.
1
2
3
4
5
6
7
8
9
import { lazy } from 'react'
const Comments = lazy(() => import('./Comments.js'));
// ...
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
과거에는 이 방법은 동작하지 않았다. 왜냐하면, 기존의 SSR에서는 코드스플릿 컴포넌트를 제외하거나 코드를 모둔 로딩한 후 hydration을 하거나 둘 중 하나일뿐이었다. 따라서 코드스플릿의 목적이 다소 손상되었다. 하지만 React v18에서는 Suspense을 통해 댓글 위젯이 로드되기 전에 애플리케이션을 hydration 할 수 있다.
즉, <Comments>를 <Suspense>로 감쌈으로써, 리액트에 페이지의 나머지 부분을 스트리밍하는 것을 차단하지 않게하고, hydration 과정 또한 차단하지 않게 할 수 있다. 이로써 첫번째 두번째 문제점은 해결되었다.
모든 컴포넌트가 hydrate 되기 전에 페이지와의 상호작용
그럼 세번째 문제점인, 전체 앱이 hydration 되기 전까지는 다른 컴포넌트와 인터랙션 할 수 없다는 문제는 어떻게 될까?
React 18에서는 hydration 작업이 브라우저의 다른 작업을 하는 것을 막지 않음으로써 해결된다.
React 18에서는, Suspense 바운더리 내에 있는 hydration 콘텐츠는 브라우저가 이벤트를 처리할 수 있는 한에서만 수행된다. 따라서 사용자가 다른 부분을 클릭하면 그것이 먼저 처리되고, 페이지에서 벗어나고 싶을때도 바로 처리된다.
여러개의 Suspense가 있을 때
만약 한 페이지에 여러개의 Suspense로 감싸진 컴포넌트가 있으면 어떻게 동작할까?
예를들어, sidebar와 comments가 Suspense로 감싸져 있다고 해보자. 그러면 sidebar와 comments를 제외한 부분의 작업이 hydration까지 모두 마쳐 다음과 같은 모습을 보이게 될 것이다.
그 다음, sidebar와 comments를 모두 포함하는 번들이 로딩되고, 두 컴포넌트에 대해 hydration을 시도 할 것이다.
이때 만약, 사용자가 댓글위젯에 인터랙션을 시도했다고 해보자. 그럼 아래와 같이 hydration의 우선순위가 바뀌어서 리액트는 comments 부분을 먼저 hydration 하도록 하게된다.
아래와 같이 Suspense를 사용하게 되면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Layout>
<NavBar />
<Suspense fallback={<BigSpinner />}>
<Suspense fallback={<SidebarGlimmer />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</RightPane>
</Suspense>
</Layout>
초기 HTML은 <NavBar>만을 포함하지만, 나머지는 사용자가 상호작용한 부분을 우선시하여 관련 코드가 로딩되는 즉시 스트리밍하여 컴포넌트에서 hydration을 할 것이다.
“어떻게 애플리케이션이 전체적으로 hydration 되지 않았는데 동작할 수 있는걸까? 리액트는 개별 컴포넌트에 개별적으로 hydration 하는 것이 아닌
<Suspense>
바운더리에 대해 hydration을 발생시킨다<Suspense>
는 당장 나타나지 않는 컨텐츠에 사용되므로, 코드는 이 자식 컨텐츠가 즉시 이용할 수 없는 상태에 대해 탄력적으로 대처할 수 있다. 리액트는 항상 부모 컴포넌트를 우선순위로 hydration 하므로, 컴포넌트는 항상 props set을 가지고 있을 수 있다. 리액트는 이벤트가 발생될 때 이벤트 지점에서 전체 상위 트리에 hydration이 진행될 때 까지 이를 보류 시켜 둔다. 마지막으로 상위 항목이 그럼에도 hydration이 되지 않는다면, 리액트는 이를 숨기고 코드가 로드 될 때 까지fallback
으로 화면을 바꿔 둔다. 이렇게 하면 트리가 일관되게 유지된다.”
데모
https://codesandbox.io/s/festive-star-9hfqt?file=/src/App.js
위 데모코드에서 sever/delays.js을 조작함으로써 React 18의 SSR을 확인할 수 있다.
- APP_DELAY : 댓글을 가져오는 시간을 오래걸리게 하여 HTML의 나머지 부분을 초기에 전송하는 것을 보여줌
- JS_BUNDLE_DELAY : script 태그가 로딩되는 것을 지연하여 댓글 HTML이 나중에 삽입되는 것을 볼 수 있음.
- ABORT_DELAY :서버에서 가져오는 시간이 너무 길어질 경우, 서버가 렌더링을 포기하고 클라이언트에서 렌더링하는 것을 볼수 있음.
요약
리액트 18은 SSR을 위한 두가지 주요 기능을 제공한다.
- HTML 스트리밍: 개발자가 원하는 만큼 HTML을 조기에 스트리밍 할 수 있게 해주며, 나중에 로딩된 HTML을 올바른 위치에 놓아주는
<script>
태그와 함께 추가적으로 스트리밍할 수 있다. - 선택적 hydration: HTML과 자바스크립트 코드의 나머지 부분이 완전히 다운로드 되기전에 가능한 빨리 애플리케이션이 hydration 할 수 있도록 한다. 또한 사용자가 상호작용하는 컴포넌트에 hydration 하는 것을 우선시하여, 즉각적으로 hydration 되는 것과 같은 착각을 불러일으킨다.
이 두가지 기능은 SSR과 관련된 아래 세가지 문제를 해결해준다.
- HTML을 내보내기전에 서버에서 모든 데이터가 로딩될 때 까지 기다릴 필요가 없다. HTML을 보낼 수 있는 상황이라면 바로 HTML을 보내고, 나머지부분은 준비되는 대로 스트리밍 할 수 있다.
- hydration을 하기 위해 모든 자바스크립트 코드가 로드 될 때 까지 기다릴 필요가 없다. SSR과 함께 코드 스플릿을 사용할 수 있다. 이렇게 하면 서버 HTML은 그대로 보존할 수 있고, 리액트는 관련 코드가 로드될 때 추가로 hydration 한다.
- 페이지와 상호작용하기 위해 모든 컴포넌트가 hydration 되는 것을 기다릴 필요가 없다. 대신 선택석 hydration을 사용하여 사용자가 상호작용하는 컴포넌트에 우선순위를 지정하고 조기에 hydration을 수행할 수 있다.
<Suspense>
는 이러한 모든 기능에 대한 옵트인 역할을 한다. 이 개선사항은 리액트 내부에서 자동으로 수행되며, 기존 리액트 코드의 대부분과 함께 작동 될 것으로 보인다. 이는 로딩중 상태를 선언적으로 표현하는 역할을 한다. if (isLoading)
과 크게 달라보이지 않을 수 있지만, <Suspense>
는 이러한 모든 개선사항을 실현해낸다.
New Hooks
useId
클라이언트와 서버 사이의 hydraton missmatch를 피하면서 unique ID를 생성해주는 Hook. SSR을 할때 사용함. 물론 그냥 Id 를 사용하고 싶을때 사용해도 됨.
useTransition
startTransition을 만들기 위한 Hook
useDeferredValue
1
const deferredValue = useDeferredValue(value);
value를 받고, 그 복사본을 반환함. 이 복사본은 좀 더 긴급한 업데이트가 끝날때까지 연기하기위해 사용됨.
user input 같은 긴급한 업데이트가 있다면, deferredValue는 이전의 값을 가리키고, 긴급 업데이트가 끝난 후에 업데이트 됨.
긴급한 업데이트가 진행중일때 자식 컴포넌트가 리렌더링되는 것을 막으려면 useMemo 등과 함께 사용되어야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing tells React to only re-render when deferredQuery changes,
// not when query changes.
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);
return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}
- suggestions는 query가 아니라 deferredQuery가 업데이트 되었을때 업데이트 된다.
useSyncExternalStore
redux store, 글로벌 변수, dom 상태 등 external store를 구독하여 concurrent read를 지원함.
React 18부터는 렌더링이 렌더링을 잠시 중단할 수 있게된다. 그러면서 각 컴포넌트별로 업데이트 속도가 달라서 발생하는 Tearing(시각적 불일치) 문제가 발생했는데 이를 해결하기 위한 Hook이다.
external data를 구독할때 useEffect 대신에 사용할 수 있다.
1
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
- subscribe : 변화가 생겼을때 호출되는 callback
- getSnapshot : 구독할 store의 값
useInsertionEffect
css-in-js 라이브러리가 렌더링 도중에 스타일을 삽입할때 성능문제를 해결할 수 있는 새로운 Hook
모든 DOM mutation이 발생하기 전에 동기적으로 호출됨.
DOM에 스타일을 삽입할때 사용함.
한정된 영역에서 사용되므로, refs나 schedule update가 불가능하다.
1
useInsertionEffect(didUpdate)
useLayoutEffect
로 dom-layout를 읽기 전에 사용한다.
그외 변경점
일관된 useEffect 타이밍
클릭, 키다운과 같은 이벤트 중에 업데이트가 발생할때, useEffect가 일관된 타이밍에 호출되도록 함.
이전에는 일관되지 않았음.
New js environment requirements
이제 React는 Promise
, Symbol
, Object.assign
과 같은 최신JS에 의존한다. 따라서 IE와 같은 오래된 브라우저에 대응하려면 bundled application에 polyfill를 사용해야한다.
컴포넌트가 undefined를 렌더할 수 있게 됨.
이전에는 undefined를 렌더할 경우 throw가 발생했으나, 이제는 undefined도 된다.(null은 원래 됐음)
React v18로의 업데이트 방법
클라이언트
React 18부터는 react-dom의 render함수는 deprecated 되고, createRoot 가 사용됨.
1
2
3
4
5
6
// ~React 17
ReactDOM.render(<App />, container);
// React 18
const root = ReactDOM.createRoot(container);
root.render(<App />);
기존의 redner 함수를 사용하게 되면, 컴포넌트 트리를 React 17과 그 이전의 동작과 동일한 레거시 모드로 동작하도록 함.
새로운 createRoot 함수는 컴포넌트 트리를 동시성 기능들과 자동배치 등 React 18의 기능들이 동작하도록 함.
서버
React 18에서는 렌더링을 위한 3가지 API가 존재함
- renderToString : 유지 (제한된 Suspense 지원)
- renderToNodeStream : Deprecated (Full suspence를 지원하나, 스트리밍 되지 않음)
- pipeToNodeWritable : 신규 API, 사용추천. (스트리밍으로 Full Suspense 지원)
출처
https://medium.com/naver-place-dev/react-18%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EC%84%B8%EC%9A%94-8603c36ddb25
https://yceffort.kr/2021/09/react-18-ssr-architecture#%EC%98%A4%EB%8A%98%EB%82%A0-ssr%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
https://velog.io/@moonelysian/React-18-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0