본문 바로가기

React.js

[React 19] useTransition으로 더 부드러운 UI 구현하기

들어가며

React 애플리케이션을 개발하다 보면 무거운 작업으로 인해 UI가 버벅이는 경험을 해보셨을 거예요.
오늘은 이런 문제를 해결할 수 있는 useTransition 훅에 대해 자세히 알아보겠습니다! 😊

useTransition이란? 🤔

useTransition은 우선순위가 낮은 상태 업데이트를 표시하는 React 훅입니다.
이 훅은 두 가지 값을 반환합니다:

  • isPending: 전환이 진행 중인지 여부
  • startTransition: 우선순위가 낮은 업데이트를 시작하는 함수

기본 사용법 📝

import { useTransition } from 'react';

function SearchComponent() {
  const [isPending, startTransition] = useTransition();
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults, setSearchResults] = useState([]);

  const handleSearch = (e) => {
    const value = e.target.value;

    // 즉시 입력값 업데이트 (높은 우선순위)
    setSearchQuery(value);

    // 검색 결과 업데이트는 낮은 우선순위로 처리
    startTransition(() => {
      setSearchResults(searchItems.filter(item => 
        item.toLowerCase().includes(value.toLowerCase())
      ));
    });
  };

  return (
    <div>
      <input 
        value={searchQuery} 
        onChange={handleSearch} 
        placeholder="검색어를 입력하세요"
      />

      {isPending ? (
        <div>검색 결과 업데이트 중...</div>
      ) : (
        <SearchResults results={searchResults} />
      )}
    </div>
  );
}

실전 활용 예시 💡

1. 대량 데이터 필터링

function DataGrid({ items }) {
  const [isPending, startTransition] = useTransition();
  const [filters, setFilters] = useState({});
  const [filteredItems, setFilteredItems] = useState(items);

  const updateFilters = (newFilters) => {
    // 필터 UI 즉시 업데이트
    setFilters(newFilters);

    // 데이터 필터링은 낮은 우선순위로
    startTransition(() => {
      setFilteredItems(
        items.filter(item => {
          return Object.entries(newFilters).every(([key, value]) => 
            item[key].toString().includes(value)
          );
        })
      );
    });
  };

  return (
    <div>
      <FilterControls 
        filters={filters} 
        onUpdateFilters={updateFilters} 
      />

      {isPending ? (
        <DataGridSkeleton />
      ) : (
        <DataGridContent items={filteredItems} />
      )}
    </div>
  );
}

2. 탭 전환 애니메이션

function TabPanel() {
  const [isPending, startTransition] = useTransition();
  const [activeTab, setActiveTab] = useState('info');
  const [tabContent, setTabContent] = useState(null);

  const handleTabChange = (tab) => {
    // 탭 하이라이트 즉시 변경
    setActiveTab(tab);

    // 컨텐츠 로딩은 transition으로 처리
    startTransition(async () => {
      const content = await loadTabContent(tab);
      setTabContent(content);
    });
  };

  return (
    <div className="tab-container">
      <TabButtons 
        activeTab={activeTab} 
        onTabChange={handleTabChange} 
      />

      <div className={`tab-content ${isPending ? 'loading' : ''}`}>
        {isPending ? (
          <FadeTransition>
            <LoadingSpinner />
          </FadeTransition>
        ) : (
          <FadeTransition>
            {tabContent}
          </FadeTransition>
        )}
      </div>
    </div>
  );
}

3. 무한 스크롤 구현

function InfiniteList() {
  const [isPending, startTransition] = useTransition();
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);

  const loadMoreItems = async () => {
    // 로딩 상태 표시
    setPage(prev => prev + 1);

    startTransition(async () => {
      const newItems = await fetchItems(page);
      setItems(prev => [...prev, ...newItems]);
    });
  };

  return (
    <div>
      <ItemList items={items} />

      {isPending ? (
        <LoadingRow />
      ) : (
        <LoadMoreButton onClick={loadMoreItems} />
      )}
    </div>
  );
}

고급 활용 기법 🔥

1. 커스텀 훅으로 재사용

function useTransitionQuery(queryFn) {
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback((params) => {
    startTransition(async () => {
      try {
        const result = await queryFn(params);
        setData(result);
        setError(null);
      } catch (err) {
        setError(err);
        setData(null);
      }
    });
  }, [queryFn]);

  return {
    isPending,
    data,
    error,
    execute
  };
}

// 사용 예시
function SearchComponent() {
  const search = useTransitionQuery(searchAPI);

  return (
    <div>
      <input onChange={e => search.execute(e.target.value)} />
      {search.isPending ? "검색 중..." : search.data}
      {search.error && <ErrorMessage error={search.error} />}
    </div>
  );
}

2. 중첩된 트랜지션 처리

function NestedTransitions() {
  const [isPending1, startTransition1] = useTransition();
  const [isPending2, startTransition2] = useTransition();

  const handleComplexUpdate = () => {
    // 첫 번째 무거운 업데이트
    startTransition1(() => {
      setFirstData(computeExpensiveData());

      // 두 번째 무거운 업데이트
      startTransition2(() => {
        setSecondData(computeMoreExpensiveData());
      });
    });
  };

  return (
    <div>
      {isPending1 && <LoadingIndicator level={1} />}
      {isPending2 && <LoadingIndicator level={2} />}
      <button onClick={handleComplexUpdate}>업데이트</button>
    </div>
  );
}

성능 최적화 팁 ⚡️

  1. 트랜지션 범위 최소화
    // 좋은 예
    startTransition(() => {
    setSearchResults(newResults);
    });
    

// 피해야 할 예
startTransition(() => {
setSearchResults(newResults);
setOtherState(newValue); // 불필요한 포함
expensiveOperation(); // 여기에 있으면 안 됨
});


2. **적절한 로딩 상태 표시**
```jsx
function LoadingState({ isPending, children }) {
  return (
    <div className={isPending ? 'opacity-50' : ''}>
      {children}
      {isPending && (
        <div className="loading-overlay">
          <Spinner />
        </div>
      )}
    </div>
  );
}

주의사항 ⚠️

  1. 모든 상태 업데이트에 사용하지 않기
    • 간단한 업데이트는 일반적인 방식으로
    • 실제로 무거운 작업에만 사용
  2. 비동기 작업 처리
    • startTransition 내부의 비동기 작업은 적절히 에러 처리
    • 필요한 경우 cleanup 함수 구현
  3. 컴포넌트 언마운트 고려
  4. useEffect(() => { let mounted = true; startTransition(() => { heavyOperation().then(result => { if (mounted) { setState(result); } }); }); return () => { mounted = false; }; }, []);

마치며

useTransition은 React 애플리케이션의 사용자 경험을 크게 개선할 수 있는 강력한 도구입니다.
하지만 모든 상황에서 필요한 것은 아니니, 적절한 상황에 현명하게 사용하시기 바랍니다!

다음 시간에는 React 19의 다른 새로운 훅들에 대해 알아보도록 하겠습니다.
즐거운 코딩 되세요! 🚀