fetch()
등 과 같은 콜백을 받아 처리하는 메소드가 존재할 경우 내부의 콜백이 모두 완료된 후에는 Automatic Batching이 처리되지 않았습니다.import React, { useState } from "react";
import "./App.css";
function App() {
// 2가지의 상태 존재
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(false);
// 하나의 핸들러에 2가지 상태를 업데이트
const onClickCreateNumber = () => {
setNumber((prev) => prev + 1);
setBoolean((prev) => !prev);
};
console.log("리렌더링");
return (
<>
<div>{number}</div>
<button onClick={onClickCreateNumber}>button</button>
</>
);
}
export default App;
fetch()
함수를 활용하여 상태 업데이트 여러번 발생import React, { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(false);
const onClickCreateNumber = () => {
// fetch()를 활용해서 콜백함수 내부에서 여러개의 상태 업데이트
fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) => {
setNumber((prev) => prev + 1);
setBoolean((prev) => !prev);
});
};
console.log("리렌더링");
return (
<>
<div>{number}</div>
<button onClick={onClickCreateNumber}>button</button>
</>
);
}
export default App;
fetch()
함수를 활용하여 상태 업데이트 여러번 발생import React, { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(false);
const onClickCreateNumber = () => {
// fetch()를 활용해서 콜백함수 내부에서 여러개의 상태 업데이트
fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) => {
setNumber((prev) => prev + 1);
setBoolean((prev) => !prev);
});
};
console.log("리렌더링");
return (
<>
<div>{number}</div>
<button onClick={onClickCreateNumber}>button</button>
</>
);
}
export default App;
import React, { useState } from "react";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(false);
const onClickCreateNumber = () => {
// 핸들러 내부에서 상태 업데이트 (콜스택)
setNumber((prev) => prev + 1);
// fetch() 콜백함수 내부에서 상태 업데이트 (태스트 큐)
fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) => {
setBoolean((prev) => !prev);
});
};
console.log("리렌더링");
return (
<>
<div>{number}</div>
<button onClick={onClickCreateNumber}>button</button>
</>
);
}
export default App;
react-dom
의 flushSync()
를 활용하여 Automatic Batching 기능을 off 할 수 있습니다.
소스코드
import React, { useState } from "react";
// flushSync() import
import { flushSync } from "react-dom";
import "./App.css";
function App() {
const [number, setNumber] = useState(0);
const [boolean, setBoolean] = useState(false);
const onClickCreateNumber = () => {
// flushSync() 활용
flushSync(() => {
setNumber((prev) => prev + 1);
});
flushSync(() => {
setBoolean((prev) => !prev);
});
};
console.log("리렌더링");
return (
<>
<div>{number}</div>
<button onClick={onClickCreateNumber}>button</button>
</>
);
}
export default App;
- 자바스크립트는 싱글 스레드기반 언어이다보니 여러 작업을 동시에 처리할 수 없었습니다.
- React에서도 UI 렌더링 도중에 일어나는 모든 작업은 차단합니다.
- 하지만 React는 Concurrent Mode를 사용해 여러 작업을 동시에 처리할 수 있도록 기능들을 확대하고 있었습니다.
- 동시성이라는 개념을 활용하여 여러 작업을 동시에 처리하도록 React는 구현하고 있습니다.
- 여러 작업을 작은 단위로 나눈 후 작업들 간의 우선순위를 정합니다.
- 정해진 우선순위에 따라 작업을 수행하는 방법입니다.
- 즉 실제로는 동시에 작업이 수행되지는 않지만 작업 간의 전환이 매우 빠르기 때문에 동시에 수행되는 것처럼 보입니다.
- 왜 React는 Concurrent Mode를 개발하려고 하는가?
- 사용자 경험에서 아주 중요한 역할을 가집니다.
- 디바운스와 쓰로틀링
- 기본적으로 Input 관련 기능을 이용할 때 디바운스 / 쓰로틀링을 활용합니다.
- 이 문서에서는 디바운스 / 쓰로틀링이 무엇인지 설명하지 않습니다.
- 궁금하시면 꼭 찾아보시길 권장합니다.
- 사용자 경험을 개선하기 위해 자주 활용되지만 한계점이 존재
- 디바운스: 무조건 일정 시간을 기다려야 함
- 쓰로틀링: 성능이 좋은 기기에서는 사용자 경험을 높일 수 있지만 성능이 안좋은 기기에서는 버벅거리는 현상이 발생
- Concurrent Mode는 이와 같은 한계점을 해결할 수 있습니다.
- 작업간의 우선순위를 정하여 사용자 입력 / 다른 작업들을 동시에 처리되는 경험을 보여줄 수 있고 개발자가 설정한 Delay에 의존되는 것이 아닌 사용자 기기 성능에 따라 달라지게 됩니다.
- suspense 기능
- React는 suspense기능을 지원하여 해당 페이지를 불러오기 전 로딩 기능을 지원하고 있습니다.
- 하지만 기기 성능이 좋다보니 빠른 렌더링을 지원함에도 불구하고 의미없는 로딩을 보여주게 됩니다.
- Concurrent Mode는 일정 시간동안 현재 페이지를 유지하면서 다음 페이지의 렌더링에 대한 작업을 동시에 진행하게 됩니다.
render()
와는 다르게 createRoot
API를 활용해야 합니다.// React-17v
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
// React-18v
import React from "react";
import App from "./App";
import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
기존에 사용자 경험을 개선하기위 사용했던 디바운스 / 쓰로틀링 / setTimeout
등의 기능을 지원합니다.
setTimeout
동작방식과 다르게 동작합니다.useTransition
훅을 활용하여 isPending
상태값을 가져와 렌더리 결과를 분기 처리 가능
isPending
: state 변경 직후에도 UI를 리렌더링 하지 않고 UI를 잠시 유지하는 상태startTransition
만 사용할 수 있습니다.
startTransition
: 클릭이나 키 입력에 의해 우선순위가 높은 상태 업데이트가 발생할 경우 내부에 선언한 상태 업데이트는 중단되고 클릭이나 키 입력이 끝난 후 이후에 해당 상태 업데이트가 발생합니다.소스코드
searchQuery
상태 업데이트 진행 중 inputValue
의 상태 업데이트가 발생하게 되면 잠시 중단하고 inputValue
상태 업데이트가 완료되면 searchQuery
상태 업데이트가 완료됩니다.setTimeout
을 활용해 특정 시간을 무저건 기다려야 했음import React, { useTransition, useState } from "react";
import "./App.css";
function App() {
const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState();
const [searchQuery, setSearchQuery] = useState();
const onClickCreateNumber = (e) => {
const input = e.target.value;
setInputValue(input);
// React에게 searchQuery의 상태 업데이트는 inputValue 상태보다 지연시켜! 라고 알리기
startTransition(() => {
setSearchQuery(input);
});
};
console.log("리렌더링", isPending, inputValue, searchQuery);
return (
<>
<input onChange={onClickCreateNumber} />
// isPending값이 true일 경우 searchQuery 상태가 우선순위에 밀려 pending 상태임으로 버튼 클릭 불가
// searchQuery 상태 업데이트가 완료되면 버튼 클릭 가능 => 이것을 활용해 로딩 기능 구현 가능
<button disabled={isPending}>button</button>
</>
);
}
export default App;
useTransition()
에서 timeout을 설정할 수 있습니다.const [isPending, startTransition] = useTransition({ timeoutMs: 5000 });
기존 React SSR 방식은 waterfall 방식을 사용하고 있었습니다.
React-18v 는 독립적으로 각각의 렌더링이 가능한 기능이 추가 되었습니다.
createRoot
대신 hyrateRoot
사용Server에서 HTML 문서를 내려주는 것을 의미합니다.
기존의 React는 renderToString()
을 활용하였습니다.
React-18v 부터는 pipeToNodeWritable()
를 활용해 HTML코드를 작은 청크로 나눈 후 보내줄 수 있습니다.
<Comments />
컴포넌트는 오래걸리는 컴포넌트 임으로 suspense
를 적용한 상태이고 해당 컴포넌트가 렌더링 되기 전에는 <Spinner />
컴포넌트를 보여주는 상황입니다.<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
<Comments />
컴포넌트는 보이지 않고 <Spinner />
컴포넌트만 보여주고 있습니다.<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>
<Comments />
컴포넌트 렌더링 준비가 완료되면 React는 추가적으로 HTML 코드를 스트리밍합니다.<Spinner />
컴포넌트 대신에 <Comments />
컴포넌트를 보여주고 있습니다.<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>
Suspense
와 renderToString()
과 같이 사용할 수 없었으며 다른 서드파티 라이브러리의 도움이 필요했습니다.<Suspense>
를 적용하여 초기 렌더링 속도를 개선할 수 있습니다!상단의 HTML Streaming 기능을 통해 하나의 컴포넌트가 영향을 미치는 빈도수는 감소하였지만 극단적으로 하나의 컴포넌트가 너무 복잡할 경우 계속 <Spinner />
만 보여지게 됩니다.
즉 다른 컴포넌트들은 렌더링이 완료되고 내려받은 자바스크립트 코드를 hydration 해야하는데 아직 렌더링 되지 않는 컴포넌트 때문에 기다려야 하는 현상이 발생합니다.
React-18v 부터는 <Suspense>
를 활용해 구현할 경우 해당 컴포넌트가 아직 렌더링되지 않았어도 상관없이 다른 컴포넌트들은 hydration을 시작할 수 있게 되었습니다.
아래의 그림처럼 네비게이션 영역에서는 먼저 hydration이 완료되어 유저는 다른 클릭 이벤트를 먼저 수행할 수 있습니다.
<Sidebar />
컴포넌트와 <Comments />
에 Suspense
를 적용한 상태에서 기본적으로 DOM Tree에 배치된 순서에 따라서 순차적으로 진행됩니다.<Sidebar />
컴포넌트가 <Comments />
보다 먼저 hydraion 진행<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
<Sidebar />
컴포넌트 hydration이 완료되기 전 <Comments />
컴포넌트의 클릭 이벤트를 발생시킨다면 React는 <Comments />
의 우선순위를 높여 먼저 hydration을 진행합니다.<Comments />
의 hydration이 완료되면 <Sidebar />
의 hydration을 진행합니다..client.jsx
, .server.jsx
, .jsx
3개의 파일로 구성renderToString
함수를 통해 초기 렌더링 결과를 HTML String으로 반환합니다.ReactDOM.hydrate
함수를 통해 자바스크립트 / 변경된 곳을 업데이트 합니다.input
태그에 검색어 입력onChange
동작 => 내부 검색 Fetch API 작동 => 검색 결과를 받아옴import ReactDOMServer from 'react-dom/server';
const ReactDOMServer = require('react-dom/server');
ReactDOMServer.renderToString(element)
예시
input
태그에 검색어 입력
onChange
동작 => 렌더링 서버에 Fetch 키워드 전달
렌더링 서버에서 Fetch API 요청 => 검색결과를 받아 비 HTML 형식으로 클라이언트에게 전달
클라이언트는 UI로 렌더링 진행
소스 코드
props
를 바탕으로 생성// Note.server.js - Server Component
import db from 'db.server';
// (A1) We import from NoteEditor.client.js - a Client Component.
import NoteEditor from 'NoteEditor.client';
function Note(props) {
const {id, isEditing} = props;
// (B) Can directly access server data sources during render, e.g. databases
const note = db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{/* (A2) Dynamically render the editor only if necessary */}
{isEditing ? <NoteEditor note={note} /> : null }
</div>
);
}
RSC의 장점 정리
Zero-Bundle-Size Components
Full Access to the Backend
Automatic Code Splitting
// 기존 방식
// NOTE: *before* Server Components import React from 'react';
// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
function Photo(props) {
// Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
};
// 개선된 방식 => 자동으로 코드 스플리팅이 적용되어 렌더링이 필요한 시점에 import
import React from 'react';
// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';
function Photo(props) { // Switch on feature flags, logged in/out, type of content, etc:
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <PhotoRenderer {...props} />;
}
}
번외 - RSA의 Rule
RSA는 요청 당 한번만 수행되기 때문에 상태 변화 Hook 미지원
useState
, useReducer
, useEffect
, useLayoutEffect
state
or effect
가 포함된 커스텀 Hook 미지원
폴리필 하지 않는 한 브라우저 API 미지원
다른 서버 컴포넌트 / 다른 클라이언트 컴포넌트 혹 / HTML Tag 렌더링 가능
새로 추가된 Hook들은 상세하게 다루지 않기 때문에 공식문서를 참조하시길 바랍니다.
useId
useSyncExternalStore
useMutableSource
hook에서 변경되었습니다.useDeferredValue
useInsertionEffect
Css-in-JS 라이브러리를 활용할 때 스타일 삽입 성능 문제를 해결할 수 있는 Hook 입니다.
Dom이 한번 mutate된 이후 실행되지만 layout effect가 발생하기 전 새 레이아웃을 한번 읽을 수 있기 때문에 사전에 계산할 수 있는 기회가 주어집니다.
기존 useLayoutEffect
와 비슷하지만 다른점은 DOM 노드에 대한 참조에 접근할 수 있게 됩니다.
참고: 공식문서
https://tecoble.techcourse.co.kr/post/2021-07-24-concurrent-mode/
좋은 글 감사합니다!