React 18 베타 Concurrent Features 적용 후기

배경

10월에 공개된 React 18 베타를 사이드 프로젝트에 적용해봤다. 특히 대용량 데이터 테이블 렌더링에서 성능 이슈가 있었는데, Concurrent Features가 도움이 될 것 같았다.

Automatic Batching

기존 React 17에서는 이벤트 핸들러 내부에서만 자동 배칭이 동작했다. React 18에서는 Promise, setTimeout 내부에서도 자동으로 배칭된다.

// React 17: 2번 렌더링
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 1000);

// React 18: 1번 렌더링

실제로 개발자 도구로 확인해보니 불필요한 렌더링이 줄어든 것을 확인했다.

startTransition으로 우선순위 분리

검색어 입력 시 필터링된 5000개 아이템을 렌더링하는 상황에서 인풋이 버벅거렸다.

const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);

const handleChange = (e) => {
  const value = e.target.value;
  setQuery(value); // 긴급 업데이트
  
  startTransition(() => {
    // 지연 가능한 업데이트
    setFilteredItems(items.filter(item => 
      item.name.includes(value)
    ));
  });
};

입력은 즉시 반영되고 리스트 렌더링은 지연시켜 UX가 개선됐다. isPending 상태로 로딩 표시도 가능했다.

useDeferredValue

startTransition 대신 useDeferredValue로도 구현 가능했다.

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

const filteredItems = useMemo(() => 
  items.filter(item => item.name.includes(deferredQuery)),
  [deferredQuery]
);

개인적으로는 startTransition이 의도가 더 명확해서 선호한다.

마이그레이션 이슈

ReactDOM.render 대신 createRoot를 사용해야 Concurrent Features가 활성화된다. 기존 코드를 수정하지 않으면 React 17과 동일하게 동작한다.

// React 18
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

useEffect 클린업 타이밍이 변경돼 일부 테스트 코드를 수정해야 했다.

결론

아직 베타라 프로덕션 적용은 이르지만, 복잡한 UI에서 확실히 체감되는 성능 개선이 있었다. 내년 정식 릴리즈 후 실무 프로젝트에 도입할 계획이다.

React 18 베타 Concurrent Features 적용 후기