avatar
Ganymedian

#6-1. Suspense and Next

Aug 10
·
31 min read

2024년 7월 15일 오후 1:57

  1. Workthrough for prev. project

  2. Code Splitting

  3. Suspense

  4. React.lazy

  5. Suspense 와 함께 사용되는 hooks

  6. Async React Router

  7. React Conf. 2024 와 근황 이것저것

  8. Final Project

  9. 마치면서…

  10. 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 를 덕지덕지 추가해줄 필요가 없으니까요. 그냥 스플리팅 된 비동기 컴포넌트 는 무조건 lazySuspense 로 불러와야 한다고 기억해 두어도 괜찮겠습니다.

// 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 주의사항

  1. Suspense는 부모부터 자식 순으로 렌더링: Suspense 컴포넌트가 중첩된 경우, 부모 Suspensefallback이 먼저 렌더링되고, 자식 Suspensefallback은 부모 Suspense가 완료된 후에 렌더링됩니다. 따라서 부모 Suspense가 로딩 중일 때는 자식 Suspense의 로딩 상태가 표시되지 않습니다.

  2. 에러 처리: Suspense는 로딩 상태를 처리하지만, 에러 상태는 처리하지 않습니다. 따라서 중첩된 Suspense 구조에서는 ErrorBoundary를 함께 사용하여 에러를 처리해야 합니다. ErrorBoundarySuspense 보다 상위에 위치하면서, 하위의 Suspense 컴포넌트에서 발생하는 에러가 전파되면 이를 처리합니다.

  3. 복잡도 증가: Suspense 컴포넌트가 중첩될수록 컴포넌트 트리의 구조가 복잡해질 수 있습니다. 이는 유지보수를 어렵게 만들 수 있으므로, 필요 이상으로 중첩하는 것은 피하는 것이 좋습니다.

  4. 성능 고려: 중첩된 Suspense 구조는 각 Suspense 컴포넌트가 독립적으로 로딩 상태를 관리하기 때문에, 필요에 따라 로딩 상태를 개별적으로 제어할 수 있습니다. 하지만, 너무 많은 Suspense 컴포넌트를 중첩하면 성능에 영향을 미칠 수 있습니다.

Nested Suspense 두 줄 요약

  • Suspense 가 복수의 parallel 컴포넌트를 품으면, Promise.all 처럼 작동한다.

  • Suspense 중첩 구조도 가능하지만 머리에 쥐날 수 있으니, 기억해뒀다가 필요할 때 써먹자.

.

.

.

React.lazy

lazy 로 비동기 컴포넌트를 만들고, 코드 스플리팅을 통해 런타임에 동적으로 컴포넌트를 다운로드 할 수 있도록 코딩할 수 있다고 배웠습니다.

그렇다면, 어떤 컴포넌트를 스플리팅 해줘야 할까요? 모든 컴포넌트를 모두 스플리팅 해줄 필요는 없지만, 다음의 경우에는 스플리팅을 고려해야 할 것입니다.

.

.

Lazy 를 사용해야 할 상황

  1. 큰 컴포넌트:

    • 크기가 큰 컴포넌트는 스플리팅 대상이 됩니다. 예를 들어, 대규모의 서드파티 라이브러리나 대량의 UI 요소를 포함하는 컴포넌트는 초기 로딩 타임을 줄이기 위해 스플리팅하면 좋습니다.

  2. 드물게 사용되는 컴포넌트:

    • 사용자가 특정 상황에서만 접근하는 컴포넌트. 예를 들어, 설정 페이지, 대시보드, 모달 창 등은 필요할 때만 로드되도록 스플리팅할 수 있습니다.

  3. 라우트 기반 컴포넌트:

    • SPA에서 페이지 단위로 컴포넌트를 나눌 때, 각 페이지 컴포넌트를 코드 스플리팅하면 유용합니다. React Router 에서 각 라우트가 처음 접근될 때만 로드되도록 할 수 있습니다.

  4. 조건부로 렌더링되는 컴포넌트:

    • 특정 조건이 충족될 때만 렌더링되는 컴포넌트. 예를 들어, 사용자 권한에 따라 보이는 사용자 페이지나, 특정 기능 토글에 따라 보여지는 컴포넌트 등.

  5. 성능 최적화가 필요한 경우:

    • 초기 로딩 성능이 중요한 경우, 코드 스플리팅을 통해 초기 번들을 작게 유지하면 사용자 경험을 향상시킬 수 있습니다.

분리해야 하는 컴포넌트 :

너무 큰거 - 너무 큰 컴포넌트?? 누가 그런 걸 만들지??

자주 안쓰는거

로그인 후에 보여지는거

.

.

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;

이 예제에서는, 부모 컴포넌트에서 빠른 키입력 이벤트가 발생한다고 가정해 보겠습니다.

  1. 부모 컴포넌트에서 search value 가 변화하고, 이를 자식 컴포넌트에게 프롭으로 전달하고 있습니다.

  2. 자식 컴포넌트에서 useDeferredValue(value) 이렇게, 프롭으로 전달 받은 valueuseDeferredValue 훅에게 전달되었습니다.

  3. useDeferredValue 훅은, 자체 알고리즘으로, 연속되는 value 변화가 발생중일 때, 그 변화가 중단되는 시점까지 아무런 밸류를 리턴하지 않고 있다가, 밸류 변화가 중단되었을 때 그 최종 밸류를 리턴합니다.

  4. useDeferredValue(value) 가 리턴하는 밸류가 useEffect 의 dependency 로 지정되어 있으므로, 해당 fetch 액션이 트리거 됩니다.

useDeferredValue 훅의 지연된 값은 다음과 같은 방식으로 동작합니다:

  1. 연속적인 변화 중 지연:

    • 값이 연속적으로 변화할 때, React는 이 변화가 잠시 멈출 때까지 대기합니다. 즉, 사용자가 빠르게 입력을 계속하고 있을 때는 최신 값을 곧바로 반영하지 않고 기다립니다.

  2. 변화가 중단된 시점의 값:

    • 사용자의 입력이 일정 시간 동안 멈추면 그 시점의 값을 지연된 값으로 반영합니다. useDeferredValue는 내부적으로 이 "멈춘 시점"을 감지하여 최신 값을 업데이트합니다.

  3. React의 스케줄링:

    • useDeferredValue는 React의 스케줄링 기능을 활용합니다. 값의 업데이트가 우선순위가 낮은 작업으로 처리되기 때문에, UI가 부드럽게 반응할 수 있도록 도와줍니다. 지연된 값은 기계적으로 몇 초 후의 값이라기보다는, React가 성능을 최적화하기 위해 적절한 시점에 값을 반영하도록 스케줄링된 값입니다.

.

.

.

분량이 커서 한 번에 안 올라가네요.

분할해서 올립니다.







....