Using the experimental React Compiler

Alex O'Callaghan

실험적인 React Compiler 사용하기

원문은 Alex O'Callaghan님이 에 게재했습니다. 이 블로그 구독하기

실험 단계의 React Compiler가 오픈소스로 공개되었으며, React 19 베타 버전을 사용해야 한다. 컴파일러는 JavaScript 시맨틱과 Rules of React에 대한 이해를 바탕으로 메모이제이션을 자동으로 적용해 성능을 개선한다. 이를 통해 useMemo, useCallback, React.memo를 이용한 수동 메모이제이션을 대체할 수 있다.

이 컴파일러를 우리의 마이크로프론트엔드 셸 애플리케이션에 적용해 보기로 했다. 이 셸 애플리케이션은 여러 마이크로프론트엔드를 하나의 앱으로 조합하는 역할을 하므로, 성능을 개선하면 플랫폼 전반의 사용자 경험에 큰 영향을 줄 수 있다. 게다가 앱 곳곳에서 useMemouseCallback을 사용하고 있어, 컴파일러로 이를 없앨 수 있을지도 궁금했다.

Healthcheck

코드베이스가 컴파일러와 호환되는지 확인해 주는 react-compiler-healthcheck 패키지가 있다.

npx react-compiler-healthcheck로 실행해 봤는데, 결과는 고무적이었다:

Successfully compiled 43 out of 45 components.
StrictMode usage found.
Found no usage of incompatible libraries.

eslint 플러그인

컴파일러와는 별개로 사용할 수 있는 eslint 플러그인도 있다.

eslint 설정에 eslint-plugin-react-compiler를 추가하고 eslint를 실행했다.

그리 순조롭지는 않았다. eslint에서 오류가 발생했다:

TypeError: Error while loading rule 'react-compiler/react-compiler': Cannot read properties of undefined (reading 'endsWith')
Occurred while linting /home/aocallaghan/dev/frontend-app-shell/.eslintrc.js

다른 파일을 대상으로 지정해 보기도 했지만(예: React 컴포넌트만) 처음 마주한 파일부터 오류가 발생했다. 문제를 일으킨 코드는 바로 이 부분이었다:

if (context.filename.endsWith(".tsx") || context.filename.endsWith(".ts")) {

context.filename이 정의되어 있지 않았는데, 상위 스코프에는 이미 filename 변수가 정의되어 있었다. 로컬에서 filename 변수를 사용하도록 수정하니 정상 동작했고 오류 없이 통과했다.

알고 보니 이는 이후에 수정된 버그였다: github.com/facebook/react/pull/29104

Babel 연동

우리는 Webpack과 Babel로 애플리케이션을 번들링한다. babel-plugin-react-compiler를 추가하는 것이 가장 간단해 보였다. 문서에 안내된 대로 babel.config.js에 플러그인을 첫 번째로 추가했다.

여기서 react/compiler-runtime을 해석하는 과정에서 오류가 발생했는데, React 19 베타 버전을 사용해야 하므로 예상된 일이었다.

React 19로 업그레이드했다:

npm install react@beta react-dom@beta

Webpack 빌드는 성공했다. 하지만 앱은 빈 화면만 보였고 콘솔에는 오류가 출력됐다:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'ReactCurrentDispatcher')

구글에 검색해 보니 reactreact-dom 버전이 맞지 않을 때 흔히 발생하는 오류인 듯했다. yarn의 resolutions 옵션을 사용해 모든 의존성이 React 19를 사용하도록 강제해 봤지만, yarn.lock을 다시 생성하고 node_modules를 재설치한 뒤에도 여전히 런타임 오류가 발생했다.

오류를 추적해 보니 OAuth2 인증에 사용하는 react-oidc 패키지에서 발생하고 있었다. 해당 패키지가 라이브러리 내부에 react-jsx-runtime을 포함하고 있었고, 이것이 버전 불일치의 원인이었다. vite.config.ts 파일을 로컬에서 수정해 패키지를 직접 다시 빌드했다.

upstream에서 수정될 수 있도록 이슈를 등록했다(github.com/AxaFrance/oidc-client/issues/1370).

다음에는 다른 오류가 나타났다:

Uncaught (in promise) TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.createFactory) is not a function

createFactoryReact 19에서 제거되었다. 오류는 우리가 내부적으로 사용하던 일부 패키지에 포함된 recompose 패키지에서 발생하고 있었다. recompose는 React Hooks 이후로 더 이상 사용되지 않는데, 그동안 이 의존성에서 벗어나지 못하고 있었다.

아직 recompose를 사용하고 있던 두 패키지를 살펴봤다:

  • 하나는 의존성으로 포함되어 있을 뿐 실제로는 더 이상 사용하지 않고 있어, 불필요한 의존성을 제거했다
  • 다른 하나는 주로 defaultProps 설정에 사용하고 있었다. 앱을 테스트하는 데 해당 컴포넌트들이 실제로 동작할 필요는 없으므로 일단 이를 제거하고 의존성도 없앤 뒤, 나중에 제대로 수정하기로 메모해 두었다

다음은 코드베이스 자체에서 발생한 마지막 오류였다. ReactDOM.render를 호출하고 있었는데, 이제는 createRoot로 대체되었다. 간단히 수정했다.

작업 완료! 앱이 렌더링되고 정상적으로 동작한다!

성능 평가

React Developer Tools Profiler를 사용해 적용 전후를 간단히 비교해 봤다.

  • 기존에는 애플리케이션 하나를 초기 렌더링할 때 20번의 커밋이 발생했는데, React Compiler를 적용한 뒤에는 16번의 커밋으로 줄었다
  • 앱 내부의 모든 useCallbackuseMemo 사용을 제거한 뒤에도 초기 로드 시 커밋은 여전히 17번에 불과했다

개발자 도구에서는 컴파일러가 컴포넌트 렌더링을 자동으로 메모이제이션한 부분도 하이라이트해 준다:

React Developer Tools에서 React Compiler 최적화를 보여주는 화면

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글