๐Ÿ—‚ WIL/๐Ÿ“ React

useTransition & useDeferredValue

nalong 2024. 10. 13. 21:58

๋ฆฌ์•กํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋ฉด ์ž์ฃผ ์“ฐ๋Š” ํ›…๋“ค๋งŒ ์ต์ˆ™ํ•ด์ง€๊ณ , ์ƒ๊ฐ๋ณด๋‹ค ๋‹ค์–‘ํ•œ ํ›…๋“ค์„ ๋†“์น˜๊ฒŒ ๋˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

useTransition๊ณผ useDeferredValue ๊ฐ™์€ ํ›…๋“ค์„ ๊ฐ„๊ณผํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ์ด๋ฒˆ์— ์•Œ๊ฒŒ ๋˜์–ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •๋ฆฌํ•ด ๋ณด์•˜๋‹ค. 

1๏ธโƒฃ useTransition

useTransition์€ ๋น„๋™๊ธฐ UI ์—…๋ฐ์ดํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ, UI๋ฅผ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ณ  ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋Š” ํ›…์ด๋‹ค.

์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„ "์ „ํ™˜(transition)"์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๊ณ , ์ด๋ฅผ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ค€๋‹ค. 

const [isPending, startTransition] = useTransition();

์ด ํ›…์€ ๋‘ ๊ฐ€์ง€ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ, ํ•˜๋‚˜๋Š” ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ธ์ง€ ๋‚˜ํƒ€๋‚ด๋Š” isPending ์ด๊ณ , ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” ์ „ํ™˜์„ ์‹œ์ž‘ํ•˜๋Š” ํ•จ์ˆ˜์ธ startTransition ์ด๋‹ค.

import { useState, useTransition } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {isPending && <p>Loading...</p>}
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}
  • useState์™€ useTransition ์‚ฌ์šฉ: tab ์ƒํƒœ๋Š” ํ˜„์žฌ ์„ ํƒ๋œ ํƒญ์„ ๊ด€๋ฆฌํ•˜๋ฉฐ, useTransition์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „ํ™˜ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. isPending ๊ฐ’์€ ์ „ํ™˜์ด ์ง„ํ–‰ ์ค‘์ธ์ง€ ๋‚˜ํƒ€๋‚ด๊ณ , startTransition ํ•จ์ˆ˜๋Š” ํƒญ ์ „ํ™˜์„ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋กœ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•œ๋‹ค.
  • selectTab ํ•จ์ˆ˜: ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ์ƒˆ๋กœ์šด ํƒญ์„ ์„ ํƒํ•  ๋•Œ ํ˜ธ์ถœ๋˜๋Š”๋ฐ, startTransition์œผ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์–ด ํƒญ ์ „ํ™˜ ์ž‘์—…์ด ๋น„๋™๊ธฐ์ ์œผ๋กœ, ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋‚ฎ์€ ์ž‘์—…์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค. ๋”ฐ๋ผ์„œ UI๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์‘ํ•  ์ˆ˜ ์žˆ๋‹ค. 
    ํ•ด๋‹น ์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ๋ฆฌ์•กํŠธ ๊ณต์‹๋ฌธ์„œ์—์„œ ๊ฐ€์ ธ์˜จ ๊ฑฐ๋ผ ์ง์ ‘ ๋™์ž‘์‹œ์ผœ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ, PostsTab์ฒ˜๋Ÿผ ๋ฌด๊ฑฐ์šด ์ž‘์—…์ด ํฌํ•จ๋œ ํƒญ์„ ์ „ํ™˜ํ•  ๋•Œ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋‚ฎ๊ธฐ ๋•Œ๋ฌธ์— Posts ํด๋ฆญ ํ›„ Contact ํƒญ์„ ํด๋ฆญํ•œ๋‹ค๋ฉด Posts ํƒญ์ด ๋™์ž‘๋  ๋•Œ๊นŒ์ง€ UI ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฉˆ์ถ”๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋น ๋ฅด๊ฒŒ Contact ํƒญ์œผ๋กœ ์ „ํ™˜๋จ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค! 
  • ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ: isPending ๊ฐ’์ด true์ผ ๋•Œ, ์ „ํ™˜์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ "Loading..." ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋Š” ์ „ํ™˜์ด ์ง„ํ–‰ ์ค‘์ž„์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

2๏ธโƒฃ useDeferredValue 

useDeferredValue๋Š” UI ์ผ๋ถ€ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง€์—ฐ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ํ›…์ด๋‹ค.

useTransition ์™€ ๋น„์Šทํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ๋Š”๋ฐ, useTransition ๋Š” ํ•จ์ˆ˜ ์‹คํ–‰์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•˜๋Š” ํ›…์ด๋ผ๋ฉด, useDefferdValue๋Š” ๊ฐ’์˜ ์—…๋ฐ์ดํŠธ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•œ๋‹ค.

const deferredValue = useDeferredValue(value)

 

value๋Š” ์ฆ‰์‹œ ๋ฐ˜์˜๋˜๋Š” ๊ฐ’์ด์ง€๋งŒ, deferredValue๋Š” ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋‚ฎ์€ ์ž‘์—…์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด, React๊ฐ€ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์— ๋ถ€๋‹ด์„ ์ฃผ์ง€ ์•Š๋Š” ์‹œ์ ์— ์—…๋ฐ์ดํŠธ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ์„ ๋น ๋ฅด๊ฒŒ ํ•˜๋”๋ผ๋„ UI๋Š” ์ฆ‰๊ฐ ๋ฐ˜์‘ํ•˜๊ณ , ๊ฒฐ๊ณผ๋Š” ์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„ ํ‘œ์‹œ๋œ๋‹ค.

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

 

๋จผ์ € useState ํ™œ์šฉํ•ด์„œ ๋งŒ๋“  ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค query๊ฐ€ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋”ฉํ•˜๋А๋ผ UI๊ฐ€ ์ž ์‹œ ๋™์•ˆ ๋ฉˆ์ถ”๊ฑฐ๋‚˜ Suspense ํด๋ฐฑ ๋ฉ”์‹œ์ง€("Loading...")๊ฐ€ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ๋‹ค. 

 

๊ทธ๋Ÿผ ์—ฌ๊ธฐ์— useDeferredValue ๋ฅผ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ?

https://codesandbox.io/p/sandbox/lsmcyq

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}
  1. query๋Š” ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ: ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•  ๋•Œ query๋Š” ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์ž…๋ ฅ ํ•„๋“œ์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์ด ๋ฐ”๋กœ ๋ฐ˜์˜๋œ๋‹ค.
  2. deferredQuery๋Š” ์ง€์—ฐ๋œ ๊ฐ’: SearchResults ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋˜๋Š” ๊ฐ’์€ ์ง€์—ฐ๋œ deferredQuery์ด๋‹ค.
    ์ฆ‰, React๋Š” ์—ฌ์œ ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ deferredQuery๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋”ฉํ•˜๋„๋ก ํ•œ๋‹ค. useDeferredValue ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์ „ UI์—์„œ๋Š” Loading UI ๊ฐ€ ์ž ์‹œ ๋ณด์˜€๋‹ค๋ฉด,  useDeferredValue ๋ฅผ ํ™œ์šฉํ•œ UI์—์„œ๋Š” ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์œ ์ง€๋œ๋‹ค.  

๐Ÿคจ ๋กœ๋”ฉ ํ‘œ์‹œ๊ฐ€ ์•ˆ๋˜๋ฉด ๋ถˆ์นœ์ ˆํ•œ UI ์•„๋‹Œ๊ฐ€...

useDeferredValue๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋น ๋ฅด๊ฒŒ ์ž…๋ ฅ๋  ๋•Œ ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์œ ์ง€๋˜๋ฏ€๋กœ, ๋กœ๋”ฉ UI๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ์ ์ด ์žฅ์ ์ผ ์ˆ˜ ์žˆ๋‹ค.     ๋‹ค๋งŒ, ๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ ์‹œ๊ฐ์ ์œผ๋กœ ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š์œผ๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ถˆ์นœ์ ˆํ•œ UI๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋Š”๋ฐ..

์ด๋Ÿด ๋•Œ, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ตœ์‹  ์ฟผ๋ฆฌ์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ(์ฆ‰, ์˜ค๋ž˜๋œ ๊ฒฐ๊ณผ์ผ ๋•Œ) CSS๋ฅผ ํ™œ์šฉํ•ด ํ๋ฆฌ๊ฒŒ ํ‘œ์‹œํ•จ์œผ๋กœ์จ, ๋กœ๋”ฉ ์ค‘์ž„์„ ์‹œ๊ฐ์ ์œผ๋กœ ์•Œ๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค! ๐Ÿ‘

const isStale = query !== deferredQuery;

<div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
</div>