#6-1. Suspense and Next
2024년 7월 15일 오후 1:57
Workthrough for prev. project
Code Splitting
Suspense
React.lazy
Suspense 와 함께 사용되는 hooks
Async React Router
React Conf. 2024 와 근황 이것저것
Final Project
마치면서…
BaaS : BackEnd as a Service Providers
.
.
.
Workthrough for prev. project
이번 웤쓰루도 역시, 지난 장에서 공개했던 코드로 갈음합니다.
지난 장의 과제에서, 지금까지 배워온 범위를 벗어난 태스크는 없었던 것 같습니다. 제시된 소스코드와 앱의 작동 방식으로도 충분하지 않을까 생각합니다.
..
이제 이번이 마지막 장이 되는군요. 지금까지 꽤 많은 토픽들을 다룬 것 같지만, 충분히 깊이 다루지 못했다는 아쉬움이 남습니다.
깊이 경험하는 것은 온전히 개발자 개개인의 몫이 되겠죠. 자신만의 프로젝트를 수행하면서, 부닥치는 문제들과 씨름하고 한 단계씩 노하우를 쌓아가는 여정이야말로, 우리 모두가 가고 있는 길이 아닐까 생각합니다.
이번 장은 시리즈의 마지막 장이면서, 뭔가 의미있는 프로젝트를 해보고 싶은 욕심도 생기는데요...
자, 갈 길이 좀 멉니다. 어서 들어가 봅시다.
.
.
.
#6. Suspense and Next
우리는 지금까지, React 의 basic hook 에서부터 시작해서, 복합 상태를 다루는 useReducer, useContext 를 배우고, 이어서 커스텀 훅을 작성하고 사용하는 방법을 배웠습니다.
그리고 React 에서 Form 을 효율적으로 다루는 방식을, RHF 을 통해 구현해 보았고, 지난 장에서는 메뉴바와 레이아웃을 구성해서 디렉토리 구조에서의 라우팅을 구성했죠. React 이전 시대의 기술로는 상당한 분량의 작업이었을 것이 분명한 작업들을 짧은 시간 안에 뚝딱 해치울 수 있는, React 의 높은 생산성을 경험해 보았습니다. 이로써 사실상 React 로 할 수 있는 일들은 대체적으로 골고루 다뤄본 셈입니다.
우리가 만들어본 React 앱 자체는 SPA 이기 때문에, 첫 로딩 이후에는 데이터 요청을 제외하면 서버와의 통신이 발생할 일이 전혀 없었죠. React Web App 은, 빌드 시점에 한개의 거대한 스크립트 파일과 한개의 Html 파일로 빌드됩니다. 그리고 이 스크립트 파일과 Html 이 클라이언트에 다운로드 되어서 hydration, 즉, 스크립트가 텅 빈 Html Dom 에 생기를 불어넣어 줌으로써 앱이 작동하게 됩니다.
SPA React Web App 의 모든 페이지와 스크립트는, 첫 로딩에서 한꺼번에 다운로드 되고,
HTML Dom 위에 React 로 작성한 모든 스크립트가 입혀져서 생기를 넣어준다.
그러나 이 구조의 문제는, 초기 다운로드와 로딩이 불필요하게 무거워질 수도 있다는 것입니다. 지난 장에서 RRD 로 작업한 /users, /posts, /todos 등의 하위 페이지들은, 어쩌면 모든 사용자들에게 모두 필요하지 않을 수도 있습니다. 어떤 사용자에게는 /todos 의 데이터만 필요할 수 있고, 또 어떤 사용자에게는 /home 의 자료만 필요할 뿐, 하위 페이지들은 아예 필요하지 않을지도 모르죠.
지난 장 까지 작업해온 우리의 SPA App 에서의 문제는, 모든 사용자들이 싫든 좋든, 모든 페이지와 모든 페이지에 실릴 스크립트들을 모두 다 다운로드 받아야 한다는 점입니다.
이 문제를 해결하기 위해서는, 빌드 시점에 코드를 분할하는 빌드가 진행되어야 할 것입니다.
.
.
.
Code Splitting
코드 스플리팅은 Webpack 등의 번들러가 수행하는 작업이고, 이 과정은 App 의 빌드 시점에 이루어집니다. Webpack 등의 번들러가 이를 이해하고 chunk 로 파일을 분리하는 작업을 수행할 수 있도록 React 의 코드에서 지정해줘야 할 필요가 있는데, 이 때 사용되는 리액트 아키텍쳐가 React.lazy
입니다.
React.lazy
는, 하위 컴포넌트들을 chunk 로 분할해서 필요한 상황에 추가로 다운로드 받을 수 있도록 빌드 시점에 관여하는 리액트 아키텍쳐입니다.
그리고, 스플리팅 된 코드들은 App의 런타임 시점에 동적으로 불러와야 합니다. 이 때, 즉 스플리팅 되어서 빌드된, 비동기 컴포넌트
를 불러올 때 사용되는 리액트 아키텍쳐가 React.Suspense
입니다.
// LazyComponent 를 동적으로 로딩하는 App.jsx
import React, { Suspense, lazy } from 'react';
// LazyComponent 는 빌드 시점에 chunk 로 splitting 되어서 빌드됩니다.
// LazyComponent 는 요청이 발생했을 때, 동적으로 추가 다운로드 됩니다.
// lazy 펑션은, 동적으로 컴포넌트를 가져오는 '펑션' 을 인자로 받아야 하기 때문에 애로우 펑션을 인자로 줍니다.
// import 는 모듈을 가져오는 Promise 를 반환하고, lazy 는 이 Promise 를 내부적으로 처리합니다.
const LazyComponent = lazy(() => import('./LazyComponent'));
// 스플리팅 된 컴포넌트는 Suspense 로 불러와야 합니다.
// Suspense 의 fallback 은 loading 상태만 감당이 가능합니다.
// 에러는 ErrorBoundary 를 통해 캐치하고 처리해야 합니다.
const App = () => (
const [showLazyComponent, setShowLazyComponent] = useState(false);
const handleButtonClick = () => {
setShowLazyComponent(true);
};
return (
<div>
<button onClick={handleButtonClick}>Load Lazy Component</button>
{showLazyComponent && (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
)}
</div>
);
);
export default App;
lazy(() => import('./LazyComponent'))
는 LazyComponent를 필요할 때 로드하도록 설정합니다. Webpack은 이를 감지하고 별도의 청크 파일로 분리합니다.Suspense
는 LazyComponent가 로드되는 동안 fallback - 로딩 중임을 나타내는 UI (<div>Loading...</div>
)를 표시합니다.코드 스플리팅이 제대로 작동하고 있는지 확인하는 방법은, 개발자 툴에서
네트워크
탭을 열어놓고App
컴포넌트를 호출해보면 됩니다. 위의 예제 코드에서 코드 스플리팅이 정상 작동중이라면, 버튼을 클릭했을 때, 네트워크 탭에서 새로운 스크립트 파일이 다운로드 될 것입니다.
Code Splitting 요약
React.lazy
로, 빌드 시점에 코드 스플리팅 될 컴포넌트를 지정해줍니다.스플리팅 된 코드는, React 앱이 빌드될 때, 앱의 본체 스크립트에서 분리되어서 별도의 개별 스크립트로 빌드됩니다.
그리고 React 앱이 사용자의 클라이언트에 다운로드 될 때, 함께 다운로드 되지 않고 있다가,
Suspense
가 트리거 되면 그 때 다운로드가 시작됩니다.코드 스플리팅 된 컴포넌트는
비동기 컴포넌트
이고,Suspense
를 사용할 수 있습니다.
Suspense
Suspense
는 비동기 컴포넌트
를 불러오고 렌더링하면서 그 loading
state 를 선언적으로 관리합니다.
React 의 원칙상, 컴포넌트 자체는 비동기 컴포넌트
일 수 없습니다. 컴포넌트는 동기적으로 렌더링되어야 하며, 비동기 로직은 컴포넌트의 외부에서 관리해야 합니다. React 에서 컴포넌트를 생성할 때, async
컴포넌트는 허용되지 않습니다. 오직, 코드 스플리팅 된 컴포넌트만 비동기 컴포넌트
일 수 있습니다.
// 이런 컴포넌트는 React 에서 만들어지지 않습니다.
const Posts = async() => {
...
return(...)
}
export default Posts
코드 스플리팅 된 비동기 컴포넌트
를 Suspense 없이, 동적으로 import 해서 일반 컴포넌트처럼 사용할 수도 있습니다. 하지만, 그렇게 사용하는 사람은 아무도 없죠. Suspense
의 혜택을 놔두고 굳이 useState
를 덕지덕지 추가해줄 필요가 없으니까요. 그냥 스플리팅 된 비동기 컴포넌트
는 무조건 lazy
와 Suspense
로 불러와야 한다고 기억해 두어도 괜찮겠습니다.
// lazy & Suspense 로 구성된 컴포넌트 중첩
// Suspense 를 제거한다면 useState 와 조건부 렌더링이 지저분하게 추가되어야만 할 것입니다.
import React, { Suspense } from 'react';
import { fetchData } from './api';
const LazyComponent = lazy(() => import('./LazyComponent'));
// ErrorBoundary 내부에서 발생하는 Error 는 모두 ErrorBoundary 로 전파됩니다.
const App = () => (
<ErrorBoundary fallback={...}>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
export default App;
Suspense 의 사용으로, 코드도 간결해졌을 뿐만 아니라, loading 의 처리도 단순화 되었습니다.
.
.
Suspense & use hook
Suspense
가 이렇게 편리한 녀석이라면, 다른 비동기 출력을 담당하는 컴포넌트에서도 사용하고 싶어질 것입니다.
Suspense
는 스플릿으로 빌드된 비동기 컴포넌트
뿐만 아니라, 제약적이지만 일반 비동기 출력 컴포넌트에도 사용이 가능합니다. 조건이 충족될 경우, 비동기 출력을 담당하는 컴포넌트가 있을 때, Suspense 로 감싸주면 로딩 상태에 따라 조건부 렌더링을 컨트롤 할 수 있으며, ErrorBoundary 와 함께 감싸서 에러 핸들링을 추가해줄 수 있습니다.
하지만 일반 컴포넌트의 Suspense 사용에는 까다로운 조건이 붙는데, Suspense
로 감싸줄 수 있는 일반 컴포넌트는
프라미스를 반환하는 펑션을
use
훅으로 불러와야 한다는 것입니다.
use
훅은 React 18 의 Canary 버전 훅으로, 아직 시험버전에 속하는 훅입니다. React 19 에서 정식으로 포함될 예정이라고 하죠.
// use 훅을 사용한, 컴포넌트의 비동기적 처리와 Suspense 사용
import React, { Suspense } from 'react';
import { fetchData } from './api';
const LazyComponent = lazy(() => import('./LazyComponent'));
// 데이터 fetch 로직을 컴포넌트 외부로 분리함
const dataPromise = fetchData();
const DataComponent = () => {
const data = use(dataPromise); // React 18 Canary 의 `use` 훅 사용
return <div>{data}</div>;
};
const App = () => (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
export default App;
비동기 작업을 수행하는 컴포넌트 들에 Suspense
를 사용할 수 있다면, 로딩 상태관리가 한결 간결해질 수 있을 것 같긴 한데, 그러려면 시험 버전의 React 를 사용해야 하고, 아직 생소한 use
훅을 써야 한다니 어딘가 꺼림직 합니다. 게다가 코드의 모양새도 그다지 맘에 들지 않습니다.
이제 이 문제를 좀 더 쉽게 만들어줄, 또 하나의 잘 알려진 라이브러리인 Tanstack-Query 를 알아보죠.
.
.
Tanstack Query : aka. React-Query
본래는 React-Query 로 잘 알려졌는데, 널리 사용되고 성장하면서 다른 프레임웍 (Vue, Angular, Svelte …) 까지 그 지원 범위를 확장하면서 이름을 Tanstack Query 로 바꾸었죠. 아직은 React-Query 라는 이름에 많은 사람들이 더 익숙합니다.
이번 Suspense 예제 코드에서는 react-query 를 사용해서 Suspense 환경을 만들어 보겠습니다. react-query 에서 자체적으로 suspense 를 지원하고 있기 때문이죠.
먼저 프로젝트에 react-query 를 설치하고,
# npm i @tanstack/react-query
react-query 의 useSuspenseQuery
훅을 사용해서 비동기 컴포넌트처럼 작동하는 child 컴포넌트를 생성해 보겠습니다.
// /src/versions/children/Posts.tsx
// react-query ^5 에서 useSuspenseQuery 가 추가되었습니다.
// React-Query 의 useQuery | useSuspenseQuery 에서 각 항목의 용법과 목적입니다.
// queryKey:
// 각 query 를 식별하기 위해 사용합니다.
// queryKey 가 같으면 캐시가 활용되고, 캐시가 없으면 새로운 query 가 생성됩니다.
// queryKey 가 일치한다면 리턴되는 data 도 일치되도록 설계해야 합니다.
// queryFn:
// queryKey 에 대한 API 데이터를 가져오는 함수를 정의합니다.
// 비동기 함수이어야 하며, 이 펑션의 결과가 useQuery 의 리턴 data 가 됩니다.
// retry:
// react-query 는 디폴트로 3번의 fetch retry 를 시도합니다.
// false 로 설정하면 retry 하지 않음.
import { axiosRequest } from "@/util/axiosInstance";
import { useSuspenseQuery } from "@tanstack/react-query";
type PostsPropsType = {
targetDB: string;
};
export type PostType = {
userId: number;
id: number;
title: string;
body: string;
};
export type UserType = {
id: number;
name: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};
// const ITEMS_PER_PAGE = 10;
const Posts = ({ targetDB }: PostsPropsType) => {
const config = { method: "GET" };
const queryResult = useSuspenseQuery<PostType[] | UserType[]>({
queryKey: [targetDB],
queryFn: async () => {
const response = await axiosRequest({ endPoint: `/${targetDB}`, config });
return response.data;
},
retry: false,
});
// queryResult에서 필요한 값들을 추출
const { data } = queryResult;
return (
<div className="flex flex-col justify-start items-start">
<h2>{targetDB}</h2>
<ul className="flex flex-col items-start">
{data?.map((d: PostType | UserType, i: number) => (
<li key={i}>{isPostType(d) ? d.title : d.name}</li>
))}
</ul>
</div>
);
};
function isPostType(item: PostType | UserType): item is PostType {
return (item as PostType).title !== undefined;
}
export default Posts;
이제 이 컴포넌트는, Suspense 를 지원하는 컴포넌트가 되었습니다. Posts 를 호출하는 부모 컴포넌트에서,
// /src/versions/Version1.tsx
import { Suspense, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@/components/ErrorFallback";
import LoadingMain from "@/components/LoadingMain";
const Posts = lazy(() => import("./children/Posts"));
const LazyComponent = lazy(() => import('./LazyComponent'));
const Version1 = () => {
const [targetDB, setTargetDB] = useState<string | null>(null);
return (
<div>
<h1>Version1</h1>
<div className="flex flex-col justify-between items-start gap-6">
<p>JsonPlaceholder 의 users 와 posts 를 선택적으로 가져와보자.</p>
<div className="flex flex-row justify-center items-center gap-6">
<button
className="border-2 border-foreground bg-accent rounded-none px-4 py-2"
onClick={() => setTargetDB("users")}
>
Users
</button>
<button
className="border-2 border-foreground bg-accent rounded-none px-4 py-2"
onClick={() => setTargetDB("posts")}
>
Posts
</button>
</div>
<ErrorBoundary
fallbackRender={(fallbackProps) => (
<ErrorFallback {...fallbackProps} />
)}
>
<Suspense fallback={<LoadingMain />}>
{targetDB && <Posts targetDB={targetDB} />}
<LazyComponent />
</Suspense>
</ErrorBoundary>
</div>
</div>
);
};
export default Version1;
이렇게 Suspense 로 호출할 수 있습니다.
Suspense 가 잘 작동하는군요.
React-Query 는 이제 State Management 에도 활용되고 있죠. React-Query 의 다양한 활용사례는 다음 기회로 미루고, 오늘은 Suspense 와 관련해서 살펴본 것으로 마무리 짓겠습니다.
.
.
Nested Suspense
앞선 예제에서의 Suspense 는 두개의 컴포넌트를 감싸주었죠.
<ErrorBoundary
fallbackRender={(fallbackProps) => (
<ErrorFallback {...fallbackProps} />
)}
>
<Suspense fallback={<LoadingMain />}>
{targetDB && <Posts targetDB={targetDB} />}
<LazyComponent />
</Suspense>
</ErrorBoundary>
Suspense 는 자식 컴포넌트가 모두
완료될 때 까지 fallback 상태를 유지합니다. Posts 컴포넌트의 로딩이 완료되었다 하더라도, LazyComponent 의 로딩이 완료되기 전 까지는 렌더링을 하지 않고, fallback 상태를 유지합니다. 만약 하나라도 로딩에 실패하면 <ErrorBoundary/>
가 렌더링 되죠.
이 처리 방식은 사용자 경험 면에서는 장점이 되기도 하지만, 상황에 따라 불친절하게 느껴질 수도 있습니다. 어쩌면 사용자는 <LazyComponent />
는 확인하고 싶었을 수도 있겠죠.
이런 상황에는 필요에 따라 Suspense 를 중첩구조로 만들어줄 수 있습니다.
// Suspense 의 nested 구조 예제
const App = () => (
<ErrorBoundary
fallbackRender={(fallbackProps) => (
<ErrorFallback {...fallbackProps} />
)}>
<Suspense fallback={<LoadingMain />}>
<UserComponent />
<ErrorBoundary fallbackComponent={ChildErrorFallback}>
<Suspense fallback={<LoadingChild />}>
<PostsComponent />
</Suspense>
</ErrorBoundary>
</Suspense>
</ErrorBoundary>
);
export default App;
Nested Suspense 주의사항
Suspense
는 부모부터 자식 순으로 렌더링:Suspense
컴포넌트가 중첩된 경우, 부모Suspense
의fallback
이 먼저 렌더링되고, 자식Suspense
의fallback
은 부모Suspense
가 완료된 후에 렌더링됩니다. 따라서 부모Suspense
가 로딩 중일 때는 자식Suspense
의 로딩 상태가 표시되지 않습니다.에러 처리:
Suspense
는 로딩 상태를 처리하지만, 에러 상태는 처리하지 않습니다. 따라서 중첩된Suspense
구조에서는ErrorBoundary
를 함께 사용하여 에러를 처리해야 합니다.ErrorBoundary
는Suspense
보다 상위에 위치하면서, 하위의Suspense
컴포넌트에서 발생하는 에러가 전파되면 이를 처리합니다.복잡도 증가:
Suspense
컴포넌트가 중첩될수록 컴포넌트 트리의 구조가 복잡해질 수 있습니다. 이는 유지보수를 어렵게 만들 수 있으므로, 필요 이상으로 중첩하는 것은 피하는 것이 좋습니다.성능 고려: 중첩된
Suspense
구조는 각Suspense
컴포넌트가 독립적으로 로딩 상태를 관리하기 때문에, 필요에 따라 로딩 상태를 개별적으로 제어할 수 있습니다. 하지만, 너무 많은Suspense
컴포넌트를 중첩하면 성능에 영향을 미칠 수 있습니다.
Nested Suspense 두 줄 요약
Suspense 가 복수의 parallel 컴포넌트를 품으면, Promise.all 처럼 작동한다.
Suspense 중첩 구조도 가능하지만 머리에 쥐날 수 있으니, 기억해뒀다가 필요할 때 써먹자.
.
.
.
React.lazy
lazy
로 비동기 컴포넌트를 만들고, 코드 스플리팅을 통해 런타임에 동적으로 컴포넌트를 다운로드 할 수 있도록 코딩할 수 있다고 배웠습니다.
그렇다면, 어떤 컴포넌트를 스플리팅 해줘야 할까요? 모든 컴포넌트를 모두 스플리팅 해줄 필요는 없지만, 다음의 경우에는 스플리팅을 고려해야 할 것입니다.
.
.
Lazy 를 사용해야 할 상황
큰 컴포넌트:
크기가 큰 컴포넌트는 스플리팅 대상이 됩니다. 예를 들어, 대규모의 서드파티 라이브러리나 대량의 UI 요소를 포함하는 컴포넌트는 초기 로딩 타임을 줄이기 위해 스플리팅하면 좋습니다.
드물게 사용되는 컴포넌트:
사용자가 특정 상황에서만 접근하는 컴포넌트. 예를 들어, 설정 페이지, 대시보드, 모달 창 등은 필요할 때만 로드되도록 스플리팅할 수 있습니다.
라우트 기반 컴포넌트:
SPA에서 페이지 단위로 컴포넌트를 나눌 때, 각 페이지 컴포넌트를 코드 스플리팅하면 유용합니다. React Router 에서 각 라우트가 처음 접근될 때만 로드되도록 할 수 있습니다.
조건부로 렌더링되는 컴포넌트:
특정 조건이 충족될 때만 렌더링되는 컴포넌트. 예를 들어, 사용자 권한에 따라 보이는 사용자 페이지나, 특정 기능 토글에 따라 보여지는 컴포넌트 등.
성능 최적화가 필요한 경우:
초기 로딩 성능이 중요한 경우, 코드 스플리팅을 통해 초기 번들을 작게 유지하면 사용자 경험을 향상시킬 수 있습니다.
분리해야 하는 컴포넌트 :
너무 큰거 - 너무 큰 컴포넌트?? 누가 그런 걸 만들지??
자주 안쓰는거
로그인 후에 보여지는거
.
.
Lazy for named export
lazy
의 구문을 보면,
// default export 에 대해서만 코드가 작동되고 있습니다.
const Posts = lazy(() => import("./children/Posts"));
이처럼 export default Component
에 대해서만 lazy
코드를 작성할 수 있습니다.
하지만 named export 된 컴포넌트도 있을 수 있죠. 이럴 땐 어떻게 해줘야 할까요. import 는 프라미스를 반환하기 때문에, .then 으로 리턴 객체에 default
라는 속성명만 추가해줄 수 있습니다.
// import 가 반환하는 프라미스에 .then 으로 살짝 만져주기
const Posts = lazy(() => import("./children/Posts")
.then((module) => {
return { default: module.Posts }
}
);
.
.
.
Suspense 와 함께 사용되는 hooks
useDeferredValue
앞서, 우리가 useEffect
를 배우던 시점 즈음에, 사용자의 키 입력에 반응하는 state
를 다루고, 이 입력이 곧바로 UI 에 반영되는 코드를 다뤘던 기억이 있으실 겁니다.
그런데, 사용자의 키 입력이, 일반적으로 초당 수회 이상 발생하기 때문에, 모든 키 입력에 대해 앱의 액션이 트리거되지 않도록, useEffect
의 클린업 펑션으로 직전 키입력 대응 액션을 클린업 해줬던 것 기억 하시나요? 앱의 ‘검색어 제안’ 과 같은 기능은, 사용자의 키입력에 즉각 반응해야 하지만, ‘모든’ 키입력에 대해 일일이 비용이 발생하는 액션을 트리거해줄 수는 없습니다. 때로는 lodash.debounce 등을 사용하거나, 때로는 cleanUp 펑션을 트리거 해주거나 하는 방법으로, 사용자의 ‘최종 입력’ 에 대해 서버 액션을 트리거 해주는 것이 일반적입니다.
React 의 useDeferredValue
훅은, 입력 지연 상태를 관리하는 데 사용됩니다. 이 훅은 사용자가 입력한 값이 즉시 업데이트되지 않도록 지연시켜서, 성능이 중요한 상황에 UI가 더욱 부드럽게 반응하도록 도와줍니다.
//
import React, { useState, useDeferredValue, useEffect } from 'react';
const SearchComponent = ({ value }) => {
// Mock API call
const fetchData = (query) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Results for "${query}"`);
}, 1000);
});
};
const [results, setResults] = useState('');
const deferredValue= useDeferredValue(value);
// deferredValue 를 effect 의 디펜던시로 지정해주었습니다.
// deferredValue 값이 안정화 되었을 때 effect 코드가 트리거됩니다.
useEffect(() => {
if (deferredValue) {
fetchData(deferredValue).then((data) => setResults(data));
}
}, [deferredValue]);
return (
<div>
<p>Search Results: {results}</p>
</div>
);
};
const App = () => {
const [search, setSearch] = useState('');
return (
<div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<SearchComponent value={search} />
</div>
);
};
export default App;
이 예제에서는, 부모 컴포넌트에서 빠른 키입력 이벤트가 발생한다고 가정해 보겠습니다.
부모 컴포넌트에서 search
value
가 변화하고, 이를 자식 컴포넌트에게 프롭으로 전달하고 있습니다.자식 컴포넌트에서
useDeferredValue(value)
이렇게, 프롭으로 전달 받은value
가useDeferredValue
훅에게 전달되었습니다.useDeferredValue
훅은, 자체 알고리즘으로, 연속되는value
변화가 발생중일 때, 그 변화가 중단되는 시점까지 아무런 밸류를 리턴하지 않고 있다가, 밸류 변화가 중단되었을 때 그 최종 밸류를 리턴합니다.useDeferredValue(value)
가 리턴하는 밸류가useEffect
의 dependency 로 지정되어 있으므로, 해당 fetch 액션이 트리거 됩니다.
useDeferredValue
훅의 지연된 값은 다음과 같은 방식으로 동작합니다:
연속적인 변화 중 지연:
값이 연속적으로 변화할 때, React는 이 변화가 잠시 멈출 때까지 대기합니다. 즉, 사용자가 빠르게 입력을 계속하고 있을 때는 최신 값을 곧바로 반영하지 않고 기다립니다.
변화가 중단된 시점의 값:
사용자의 입력이 일정 시간 동안 멈추면 그 시점의 값을 지연된 값으로 반영합니다.
useDeferredValue
는 내부적으로 이 "멈춘 시점"을 감지하여 최신 값을 업데이트합니다.
React의 스케줄링:
useDeferredValue
는 React의 스케줄링 기능을 활용합니다. 값의 업데이트가 우선순위가 낮은 작업으로 처리되기 때문에, UI가 부드럽게 반응할 수 있도록 도와줍니다. 지연된 값은 기계적으로 몇 초 후의 값이라기보다는, React가 성능을 최적화하기 위해 적절한 시점에 값을 반영하도록 스케줄링된 값입니다.
.
.
.
분량이 커서 한 번에 안 올라가네요.
분할해서 올립니다.