React 18 주요 변경점
React v18 주요 변경점
1. Automatic Batching
- React-18v 부터 상태 업데이트(setState)를 하나로 통합해서 배치처리를 한 후 리렌더링을 진행합니다.
- 리렌더링 관련 성능 개선
- 과거 React-17v 에서는 이벤트 핸들러 내부에서 발생하는 상태 업데이트만 배치처리를 지원했습니다.
- 하지만 이벤트 핸들러 내부에
fetch()
등 과 같은 콜백을 받아 처리하는 메소드가 존재할 경우 내부의 콜백이 모두 완료된 후에는 Automatic Batching이 처리되지 않았습니다.
1.1 React-17v
이벤트 핸들러 내부에서 상테 업데이트가 여러번 발생
- 소스코드
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;
- 결과
- 하나의 핸들러에서 2가지의 상태 업데이트가 이루어졌음에도 불구하고 리렌더링은 1번만 일어나고 있습니다.
- 리렌더링 관련 성능 개선
- 하지만 이벤트 핸들러 내부에
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;
- 하나의 핸들러에서 2가지의 상태 업데이트가 이루어졌음에도 불구하고 리렌더링은 1번만 일어나고 있습니다.
이벤트 핸들러 내부에서 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;
- 결과
- 버튼 클릭 1번당 리렌더링이 2번 발생하고 있습니다.
- Automatic Batching이 작동하지 않고 있음
1.2 React-18v
이벤트 핸들러 내부에서 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;
- 결과
- 버튼 클릭 1번당 리렌더링이 1번 발생하고 있습니다.
- React-17v와 다르게 Automatic Batching이 작동하고 있습니다.
이벤트 핸들러 내부에서 2가지 상태를 활용할 경우
- 소스코드
- 일반적인 상태 업데이트 + 콜백함수 내부에서 상태 업데이트를 진행할 경우 Automatic Batching이 작동할까?
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;
- 결과
- 버튼 클릭 1번당 리렌더링이 2번 발생하고 있습니다.
- 동시 사용 불가
- 결론적으로 혼용하여 사용할 경우 React-18v에서도 Automatic Batching이 작동하지 않습니다.
- Automatic Batching은 지원 범위를 기존 콜스택에서 태스크 큐까지 확장한 것입니다.
- 콜스택: JS 엔진에서 일반적으로 Job을 처리하는 일반적인 자료구조
- 태스크 큐: 비동기 콜백 등을 처리하는 자료구조
- 버튼 클릭 1번당 리렌더링이 2번 발생하고 있습니다.
Tip) Automatic Batching 기능을 사용하고 싶지 않다면?
-
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;
2. Concurrent Feature
- 기존 React에서 추구하고 있는 Concurrent Mode( 동시성 )를 18v 부터 하나의 기능으로 지원하게 되었습니다.
- 자바스크립트는 싱글 스레드기반 언어이다보니 여러 작업을 동시에 처리할 수 없었습니다.
- React에서도 UI 렌더링 도중에 일어나는 모든 작업은 차단합니다.
- 하지만 React는 Concurrent Mode를 사용해 여러 작업을 동시에 처리할 수 있도록 기능들을 확대하고 있었습니다.
- 동시성이라는 개념을 활용하여 여러 작업을 동시에 처리하도록 React는 구현하고 있습니다.
- 여러 작업을 작은 단위로 나눈 후 작업들 간의 우선순위를 정합니다.
- 정해진 우선순위에 따라 작업을 수행하는 방법입니다.
- 즉 실제로는 동시에 작업이 수행되지는 않지만 작업 간의 전환이 매우 빠르기 때문에 동시에 수행되는 것처럼 보입니다.
- 왜 React는 Concurrent Mode를 개발하려고 하는가?
- 사용자 경험에서 아주 중요한 역할을 가집니다.
- 디바운스와 쓰로틀링
- 기본적으로 Input 관련 기능을 이용할 때 디바운스 / 쓰로틀링을 활용합니다.
- 이 문서에서는 디바운스 / 쓰로틀링이 무엇인지 설명하지 않습니다.
- 궁금하시면 꼭 찾아보시길 권장합니다.
- 사용자 경험을 개선하기 위해 자주 활용되지만 한계점이 존재
- 디바운스: 무조건 일정 시간을 기다려야 함
- 쓰로틀링: 성능이 좋은 기기에서는 사용자 경험을 높일 수 있지만 성능이 안좋은 기기에서는 버벅거리는 현상이 발생
- Concurrent Mode는 이와 같은 한계점을 해결할 수 있습니다.
- 작업간의 우선순위를 정하여 사용자 입력 / 다른 작업들을 동시에 처리되는 경험을 보여줄 수 있고 개발자가 설정한 Delay에 의존되는 것이 아닌 사용자 기기 성능에 따라 달라지게 됩니다.
- suspense 기능
- React는 suspense기능을 지원하여 해당 페이지를 불러오기 전 로딩 기능을 지원하고 있습니다.
- 하지만 기기 성능이 좋다보니 빠른 렌더링을 지원함에도 불구하고 의미없는 로딩을 보여주게 됩니다.
- Concurrent Mode는 일정 시간동안 현재 페이지를 유지하면서 다음 페이지의 렌더링에 대한 작업을 동시에 진행하게 됩니다.
2.1 createRoot
- 기존 React-17v의
render()
와는 다르게createRoot
API를 활용해야 합니다.- 동시성 API / Automatic Batching 지원
- 소스코드
- React-17v와 React-18v의 차이점
// 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 />);
2.2 startTransition
-
기존에 사용자 경험을 개선하기위 사용했던 디바운스 / 쓰로틀링 /
setTimeout
등의 기능을 지원합니다.- 일반적으로 자바스크립트에서 활용하는
setTimeout
동작방식과 다르게 동작합니다.- 태스트 큐를 활용하지 않으며 동기적으로 즉시 실행
- 일반적으로 자바스크립트에서 활용하는
-
useTransition
훅을 활용하여isPending
상태값을 가져와 렌더리 결과를 분기 처리 가능isPending
: state 변경 직후에도 UI를 리렌더링 하지 않고 UI를 잠시 유지하는 상태- 각 상태 업데이트에 대한 우선순위를 설정할 수 있는 Hook 입니다.
-
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을 설정할 수 있습니다.- 최대 얼마까지 기다릴 것인지 시간을 설정합니다.
- 아래의 코드는 5초 동안 렌더링을 기다리다가 5초가 지나도 pending값이 변하지 않으면 강제로 렌더링 됩니다.
const [isPending, startTransition] = useTransition({ timeoutMs: 5000 });
2.3 Suspense와 SSR
-
기존 React SSR 방식은 waterfall 방식을 사용하고 있었습니다.
- Server는 React 코드를 전달 받음 => HTML로 변환 => React는 다시 변환된 HTML 코드를 전달 받음 => hydrate
- hydrate: HTML 문서에 자바스크립트를 붙이는 작업
- 특정 부분에서 병목현상이 발생할 경우 성능 이슈가 발생하게 됩니다.
- Server는 React 코드를 전달 받음 => HTML로 변환 => React는 다시 변환된 HTML 코드를 전달 받음 => hydrate
-
React-18v 는 독립적으로 각각의 렌더링이 가능한 기능이 추가 되었습니다.
- 기존의
createRoot
대신hyrateRoot
사용 - 즉 유저에게 처음 보여주는 페이지 전체를 그려 내려주는 것이 아닌 빠르게 준비되는 부분부터 보여줍니다.
- HTML Streaming API + Suspense를 연계하여 SSR 설계 지원
- Selective hydrating 기능 지원
- 기존의
HTML Streaming
-
Server에서 HTML 문서를 내려주는 것을 의미합니다.
-
기존의 React는
renderToString()
을 활용하였습니다.- 완성된 HTML 문서를 전달 받음
-
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>
- 결론
- 기존에는 SSR을 구현할 때
Suspense
와renderToString()
과 같이 사용할 수 없었으며 다른 서드파티 라이브러리의 도움이 필요했습니다. - React-18v 부터는 응답이 오래걸리는 컴포넌트는
<Suspense>
를 적용하여 초기 렌더링 속도를 개선할 수 있습니다!
- 기존에는 SSR을 구현할 때
Selective hydrating
-
상단의 HTML Streaming 기능을 통해 하나의 컴포넌트가 영향을 미치는 빈도수는 감소하였지만 극단적으로 하나의 컴포넌트가 너무 복잡할 경우 계속
<Spinner />
만 보여지게 됩니다. -
즉 다른 컴포넌트들은 렌더링이 완료되고 내려받은 자바스크립트 코드를 hydration 해야하는데 아직 렌더링 되지 않는 컴포넌트 때문에 기다려야 하는 현상이 발생합니다.
-
React-18v 부터는
<Suspense>
를 활용해 구현할 경우 해당 컴포넌트가 아직 렌더링되지 않았어도 상관없이 다른 컴포넌트들은 hydration을 시작할 수 있게 되었습니다. -
아래의 그림처럼 네비게이션 영역에서는 먼저 hydration이 완료되어 유저는 다른 클릭 이벤트를 먼저 수행할 수 있습니다.
- 사용자 경험 향상!
- 그리고 어떤 것들을 먼저 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을 진행합니다.
- 결론적으로 사용자의 관심사에 따라 인터랙션이 가능한 컴포넌트 부터 기능을 제공할 수 있게 되었습니다.
3. React Server Component(RSC)
- React에서 node.js와 같은 Server 역할을 수행하는 Server Component 기능을 제공합니다.
- 브라우저가 받아오는 용량을 줄이기 위해 서버에서 실행되는 컴포넌트입니다.
.client.jsx
,.server.jsx
,.jsx
3개의 파일로 구성
- 기존 SSR 기능과는 다르게 RSC는 HTML 파일을 가져오지 않고 JSON 데이터를 가져오게 됩니다.
기존 SSR 방식
renderToString
함수를 통해 초기 렌더링 결과를 HTML String으로 반환합니다.- 위에서 내려받은 마크업이 포함된 HTML 문서를 먼저 사용자에게 보여줍니다.
- 그리고 나머지 청크파일을 다시 받은 후
ReactDOM.hydrate
함수를 통해 자바스크립트 / 변경된 곳을 업데이트 합니다.- 예시
input
태그에 검색어 입력onChange
동작 => 내부 검색 Fetch API 작동 => 검색 결과를 받아옴- 받은 검색 결과를 다시 React에 전달하여 컴포넌트 렌더링 진행
- 예시
import ReactDOMServer from 'react-dom/server';
const ReactDOMServer = require('react-dom/server');
ReactDOMServer.renderToString(element)
변경된 SSR+RSC 방식
-
예시
-
input
태그에 검색어 입력 -
onChange
동작 => 렌더링 서버에 Fetch 키워드 전달 -
렌더링 서버에서 Fetch API 요청 => 검색결과를 받아 비 HTML 형식으로 클라이언트에게 전달
-
클라이언트는 UI로 렌더링 진행
- React 컴포넌트가 아니기 때문에 컴포넌트 처리 비용이 절약
- 불필요한 청크 파일을 받아오는 것을 막을 수 있음
-
-
소스 코드
- 서버 컴포넌트는 API 호출 방식을 사용하지 않고 직접 DB에 접근하여 note 데이터를 받아오고 있습니다.
- 이것이 RSA의 장점!
- 받아온 데이터를 바탕으로 NodeEditor라는 컴포넌트를 구성하고 있습니다.
- NodeEditor 컴포넌트는 클라이언트에게 내려줄 컴포넌트 입니다.
- 서버 컴포넌트가 리렌더링 되도 클라이언트 컴포넌트가 기존에 가지고 있는 DOM / State들은 유지됩니다.
- 서버에서 내려주는
props
를 바탕으로 생성
- 서버에서 내려주는
- 또한 NodeEditor 컴포넌트를 RSC가 import할 때 필요할때만 dynamic하게 import할 수 있습니다.
- 즉 RSA는 서버에게 직접 JSON 데이터를 받거나, 클라이언트에서 필요한 전처리 과정, 파일 시스템 등을 수행하고 클라이언트 컴포넌트는 React의 순수 컴포넌트 기능을 제공하게 됩니다.
- 서버 컴포넌트는 API 호출 방식을 사용하지 않고 직접 DB에 접근하여 note 데이터를 받아오고 있습니다.
// 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
- API 호출을 통해 여러 데이터를 불러올 필요 없이 DB 접근 / 파일 시스템 등을 접근할 수 있습니다.
-
Automatic Code Splitting
- 기존 lazy loading 방식을 자동으로 지원합니다.
// 기존 방식 // 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
oreffect
가 포함된 커스텀 Hook 미지원
-
-
폴리필 하지 않는 한 브라우저 API 미지원
- 브라우저 API를 활용한 함수들도 미지원
-
다른 서버 컴포넌트 / 다른 클라이언트 컴포넌트 혹 / HTML Tag 렌더링 가능
-
4. New Hooks
-
새로 추가된 Hook들은 상세하게 다루지 않기 때문에 공식문서를 참조하시길 바랍니다.
-
useId
- 난수 ID를 생성하는 Hook 입니다.
- 클라이언트와 서버간의 hydration의 불일치를 피하면서 유니크 아이디를 생성 기능을 제공
- 특정 key값을 생성하는 것이 아닙니다.
- 참고: 공식문서
-
useSyncExternalStore
- 기존의
useMutableSource
hook에서 변경되었습니다. - 동시성 기능을 사용할 때 전역 상태 관리 라이브러리의 상태가 업데이트 되지 않을 경우 강제로 업데이트를 발생시키는 Hook입니다.
- 라이브러리 제작 활용
- 참고: 공식문서
- 기존의
-
useDeferredValue
- 트리에서 급하지 않은 부분의 재랜더링을 지연할 수 있는 기능을 지원하는 Hook 입니다.
- 디바운스와 비슷하지만 고정된 지연시간이 없고 렌더링이 반영되는 시점에 지연 렌더링을 시도합니다.
- 지연된 렌더링은 인터럽트가 가능 => 사용자 입력을 차단하지 않음
- 참고: 공식문서
-
useInsertionEffect
-
Css-in-JS 라이브러리를 활용할 때 스타일 삽입 성능 문제를 해결할 수 있는 Hook 입니다.
- 라이브러리 제작 활용
-
Dom이 한번 mutate된 이후 실행되지만 layout effect가 발생하기 전 새 레이아웃을 한번 읽을 수 있기 때문에 사전에 계산할 수 있는 기회가 주어집니다.
-
기존
useLayoutEffect
와 비슷하지만 다른점은 DOM 노드에 대한 참조에 접근할 수 있게 됩니다. -
참고: 공식문서
5. 참고 문헌
-
-
https://tecoble.techcourse.co.kr/post/2021-07-24-concurrent-mode/
Author And Source
이 문제에 관하여(React 18 주요 변경점), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@woodong/React-18-주요-변경점저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)