모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

24강 / 전체 30강

비동기 상태 관리 패턴

5분 읽기 조회 13

리액트 18의 핵심 기능인 동시성 렌더링(Concurrent Rendering), useTransition, useDeferredValue의 동작 원리와 실전 사용법을 배웁니다.

⚡ 리액트 18 — 동시성의 시대

리액트 18은 동시성 렌더링(Concurrent Rendering)을 도입했습니다. 이것은 리액트의 렌더링 방식을 근본적으로 바꾼 업그레이드입니다. 이전에는 렌더링이 시작되면 완료될 때까지 중단할 수 없었습니다. 무거운 렌더링이 발생하면 UI가 멈추는(jank) 현상이 생겼습니다.

동시성 렌더링은 렌더링을 일시 중지하고 재개하며 우선순위를 부여할 수 있습니다. 긴급한 업데이트(사용자 입력)와 덜 긴급한 업데이트(검색 결과)를 구분해서 처리합니다.

🔄 useTransition — 긴급하지 않은 업데이트 표시

검색창에서 타이핑할 때마다 수천 개의 결과를 필터링하면 타이핑이 버벅거립니다. useTransition은 필터링 업데이트를 "긴급하지 않음"으로 표시해, 타이핑(긴급)이 먼저 처리되도록 우선순위를 부여합니다:

import { useState, useTransition, memo } from 'react'

// 수천 개 아이템 리스트 (일부러 무겁게)
const BigList = memo(function BigList({ filter }: { filter: string }) {
  const items = Array.from({ length: 10000 }, (_, i) => `아이템 ${i + 1}`)
    .filter((item) => item.includes(filter))

  return (
    <ul className="h-64 overflow-y-auto space-y-0.5">
      {items.map((item, i) => (
        <li key={i} className="px-3 py-1 text-sm text-gray-700">{item}</li>
      ))}
    </ul>
  )
})

function SearchWithTransition() {
  const [query, setQuery] = useState('')
  const [filter, setFilter] = useState('')  // BigList에 내려줄 값
  const [isPending, startTransition] = useTransition()

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value
    setQuery(value)  // 입력창 업데이트 — 즉시 (긴급)

    // 필터링 업데이트 — 느려도 되는 것 (비긴급)
    startTransition(() => {
      setFilter(value)
    })
  }

  return (
    <div>
      <div className="relative">
        <input
          value={query}
          onChange={handleChange}
          placeholder="검색..."
          className="border rounded-lg px-4 py-2 w-full"
        />
        {isPending && (
          <span className="absolute right-3 top-2.5 text-xs text-gray-400">필터링 중...</span>
        )}
      </div>
      <BigList filter={filter} />
    </div>
  )
}

startTransition 안의 상태 업데이트는 우선순위가 낮아집니다. 타이핑 중에는 입력창이 즉시 반응하고, 목록 필터링은 타이핑이 멈출 때 처리됩니다.

⏳ useDeferredValue — 값 기반 지연

useDeferredValue는 useTransition과 비슷하지만, 상태 setter 대신 값을 지연합니다. 외부에서 받은 값(Props)을 지연할 때 유용합니다:

import { useDeferredValue, useMemo } from 'react'

function FilterableList({ query }: { query: string }) {
  // query가 빠르게 바뀌어도 deferredQuery는 여유롭게 업데이트
  const deferredQuery = useDeferredValue(query)

  const filteredItems = useMemo(() => {
    return Array.from({ length: 10000 }, (_, i) => `아이템 ${i + 1}`)
      .filter((item) => item.includes(deferredQuery))
  }, [deferredQuery])  // deferredQuery가 바뀔 때만 재계산

  const isStale = query !== deferredQuery  // 오래된 값이면 true

  return (
    <ul style={{ opacity: isStale ? 0.7 : 1, transition: 'opacity 0.2s' }}>
      {filteredItems.map((item, i) => (
        <li key={i}>{item}</li>
      ))}
    </ul>
  )
}

목록이 오래된 값을 보여줄 때 살짝 흐리게 만들어 사용자에게 업데이트 중임을 알릴 수 있습니다.

🆚 useTransition vs useDeferredValue

구분useTransitionuseDeferredValue
제어 위치상태 업데이트 하는 곳값을 소비하는 곳
언제 사용자신이 상태를 바꿀 수 있을 때Props로 받은 값을 지연할 때
isPending 접근가능직접 비교 필요 (query !== deferredQuery)

✅ 정리

  • 리액트 18의 동시성 렌더링: 우선순위를 부여해 긴급한 업데이트 먼저 처리.
  • useTransition: 느려도 되는 상태 업데이트를 startTransition으로 감쌉니다.
  • useDeferredValue: Props 값을 지연해서 받아 무거운 렌더링을 늦춥니다.
  • 두 API 모두 측정 후 필요할 때만 사용 — 대부분의 앱에서는 필요하지 않습니다.

관련 주제

  • 데이터 페칭 로딩 에러 성공 상태
  • 재시도 로직
  • 스피너 토스트 피드백
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...