[번역] 리액트 컴파일러

eunbinn·2024년 6월 3일
30

FrontEnd 번역

목록 보기
32/38
post-thumbnail

출처: https://react.dev/learn/react-compiler

이 글에서는 새로운 실험 단계인 리액트 컴파일러에 대한 소개와 어떻게 사용해 볼 수 있는지 알려드립니다.

※ 진행 중
이 문서의 내용은 아직 진행 중인 작업입니다. 더 많은 문서는 리액트 컴파일러 워킹 그룹 저장소에서 확인할 수 있으며, 안정화되면 이 문서에 업데이트될 예정입니다.

학습 내용

  • 컴파일러 시작하기
  • 컴파일러 및 eslint 플러그인 설치하기
  • 문제 해결

※ 주의 사항
리액트 컴파일러는 커뮤니티의 초기 피드백을 얻기 위해 오픈소스로 공개한 새로운 실험적인 컴파일러입니다. 아직 다듬어지지 않은 부분이 있으며 프로덕션용으로 완전히 준비되지 않았습니다.

리액트 컴파일러를 사용하려면 리액트 19 RC가 필요합니다. 리액트 19로 업그레이드할 수 없는 경우 워킹 그룹에 설명된 대로 캐시 기능을 사용자 공간에서 구현해 시도해 볼 수 있습니다. 그러나 이는 권장되지 않으며 가능하면 리액트 19로 업그레이드하는 것이 좋습니다.

리액트 컴파일러는 커뮤니티의 초기 피드백을 얻기 위해 오픈소스로 공개한 새로운 실험적인 컴파일러입니다. 빌드 시간에만 사용되는 도구로, 리액트 앱을 자동으로 최적화합니다. 순수 자바스크립트로 작성되었으며 리액트의 규칙을 이해하므로 컴파일러를 사용하기 위해 코드를 다시 작성할 필요가 없습니다.

컴파일러에는 컴파일러의 분석 결과를 에디터에서 바로 확인할 수 있는 eslint 플러그인도 포함되어 있습니다. 이 플러그인은 컴파일러와 독립적으로 실행되며 앱에서 컴파일러를 사용하지 않더라도 사용할 수 있습니다. 코드베이스의 품질을 개선하기 위해 이 eslint 플러그인을 사용할 것을 모든 리액트 개발자에게 권장합니다.

컴파일러는 무엇을 하나요?

애플리케이션을 최적화하기 위해 리액트 컴파일러는 자동으로 코드를 메모화합니다. useMemo, useCallback, React.memo와 같은 API를 통한 메모화에 익숙하실 겁니다. 이러한 API를 사용하면 입력이 변경되지 않은 경우 애플리케이션의 특정 부분을 다시 계산할 필요가 없음을 리액트에 알려주어 업데이트 작업을 줄일 수 있습니다. 강력하지만 메모를 적용하는 것을 잊어버리거나 잘못 적용하기 쉽습니다. 이는 리액트가 의미 있는 변경이 없는 UI 부분까지 확인해야 하기 때문에 비효율적인 업데이트로 이어질 수 있습니다.

컴파일러는 자바스크립트와 리액트의 규칙에 대한 지식을 사용해 컴포넌트와 훅 내의 값 또는 값 그룹을 자동으로 메모화합니다. 규칙 위반이 감지되면 해당 컴포넌트나 훅만 자동으로 건너뛰고 다른 코드를 계속해서 컴파일 합니다.

코드베이스가 이미 잘 메모화되어 있다면 컴파일러를 사용해도 큰 성능 향상을 기대하기 어려울 수 있습니다. 그러나 실제로 성능 문제를 일으키는 정확한 종속성을 직접 메모하는 것은 어려운 작업입니다.

※ 파헤치기

리액트 컴파일러는 어떤 종류의 메모화를 추가하나요?

리액트 컴파일러의 초기 릴리즈는 주로 업데이트 성능 개선(기존 컴포넌트 리렌더링)에 초점을 맞추고 있으므로 아래의 두 가지 사용 사례에 중점을 두고 있습니다.

  1. 컴포넌트의 폭포식 리렌더링 건너뛰기
  • <Parent />를 다시 렌더링하면 <Parent /> 만 변경되었음에도 불구하고 컴포넌트 트리에 있는 많은 컴포넌트가 다시 렌더링됩니다.
  1. 리액트 외부의 복잡한 계산 건너뛰기
  • 예를 들면, 컴포넌트 또는 해당 데이터가 필요한 훅 내부에서 복잡한 expensivelyProcessAReallyLargeArrayOfObjects() 를 호출하는 경우에 해당합니다.

리렌더링 최적화하기

리액트를 사용하면 현재 상태(더 구체적으로는 프로퍼티, 상태, 컨텍스트)의 함수로 UI를 표현할 수 있습니다. 현재 구현에서는 컴포넌트의 상태가 변경되면 리액트가 해당 컴포넌트와 그 모든 자식들을 다시 렌더링합니다(useMemo(), useCallback() 또는 React.memo()로 수동 메모화를 적용하지 않았다면). 예를 들어, 다음 예제에서 <FriendList>의 상태가 변경될 때마다 <MessageButton>이 다시 렌더링됩니다.

function FriendList({ friends }) {
  const onlineCount = useFriendOnlineCount();
  if (friends.length === 0) {
    return <NoFriends />;
  }
  return (
    <div>
      <span>{onlineCount} online</span>
      {friends.map((friend) => (
        <FriendListCard key={friend.id} friend={friend} />
      ))}
      <MessageButton />
    </div>
  );
}

리액트 컴파일러 플레이그라운드에서 확인해보기

리액트 컴파일러는 수동 메모화에 해당하는 기능을 자동으로 적용하여 상태가 변경될 때 앱의 관련 부분만 다시 렌더링하도록 하며, 이를 "세분화된 반응성(fine-grained reactivity)"이라고도 합니다. 위의 예시에서 리액트 컴파일러는 friends가 변경되더라도 <FriendListCard />의 반환값을 재사용할 수 있다고 판단하여 이 JSX를 다시 생성하지 않고 friends 수가 변경될 때 <MessageButton>을 다시 렌더링하지 않을 수 있습니다.

복잡한 계산도 메모화합니다

컴파일러는 렌더링 중에 사용되는 복잡한 계산을 자동으로 메모화할 수 있습니다.

// 컴포넌트나 훅이 아니기 때문에 리액트 컴파일러에 의해 메모화되지 **않습니다**
function expensivelyProcessAReallyLargeArrayOfObjects() {
  /* ... */
}

// 컴포넌트이기 때문에 리액트 컴파일러에 의해 메모화됩니다
function TableContainer({ items }) {
  // 아래 함수 호출이 메모화 됩니다
  const data = expensivelyProcessAReallyLargeArrayOfObjects(items);
  // ...
}

리액트 컴파일러 플레이그라운드에서 확인해보기

그러나 expensiveProcessAReallyLargeArrayOfObjects가 정말 복잡한 함수라면, 리액트 외부에서 자체 메모화를 구현하는 것도 고려해 볼 수 있습니다.

  • 리액트 컴파일러는 모든 함수가 아닌 리액트 컴포넌트와 훅만 메모화합니다.
  • 리액트 컴파일러의 메모화는 여러 컴포넌트나 훅에서 공유되지 않습니다.

따라서 여러 컴포넌트에서 복잡한 expensivelyProcessAReallyLargeArrayOfObjects가 사용되었다면, 똑같은 항목이 전달되더라도 복잡한 계산이 반복적으로 실행될 것입니다. 코드를 더 복잡하게 만들기 전에 먼저 프로파일링을 통해 실제로 그렇게 비용이 많이 드는지 확인하는 것이 좋습니다.

컴파일러는 무엇을 가정하나요?

리액트 컴파일러는 코드가 다음과 같다고 가정합니다.

  1. 유효한 시맨틱 자바스크립트입니다.
  2. 접근하기 전에 nullable/optional 값과 프로퍼티가 정의되어 있는지 확인합니다. (예를 들어, 타입스크립트를 사용하는 경우 strictNullChecks를 활성화해 확인합니다), 즉 if (object.nullableProperty) { object.nullableProperty.foo } 또는 옵셔널 체이닝으로 object.nullableProperty?.foo와 같이 정의되어 있는지 테스트합니다.
  3. 리액트의 규칙을 따릅니다

리액트 컴파일러는 많은 리액트의 규칙을 정적으로 확인할 수 있으며, 오류를 감지하면 컴파일을 안전하게 건너뜁니다. 오류를 확인하려면 eslint-plugin-react-compiler를 설치하는 것이 좋습니다.

컴파일러를 사용해봐도 될까요?

컴파일러는 아직 실험 단계이며 다듬어지지 않은 부분이 많다는 점에 유의하세요. Meta와 같은 회사에서 프로덕션 환경에서 컴파일러를 사용했지만, 앱의 프로덕션 환경에 컴파일러를 배포하는 것은 코드베이스의 상태와 리액트의 규칙을 얼마나 잘 준수했는지에 따라 달라질 수 있습니다.

지금 컴파일러를 서둘러 사용할 필요는 없습니다. 안정적인 릴리스가 나올 때까지 기다렸다가 도입해도 괜찮습니다. 그러나, 여러분의 앱에서 작은 실험을 통해 시도해 보고 피드백을 제공해 주시면 컴파일러를 개선하는 데 도움이 될 것입니다.

시작하기

이 문서 외에도 컴파일러에 대한 추가 정보와 토론은 리액트 컴파일러 워킹 그룹에서 확인하실 것을 권장합니다.

호환성 검사

컴파일러를 설치하기 전에 먼저 코드베이스가 호환되는지 확인할 수 있습니다.

npx react-compiler-healthcheck@latest

이 스크립트는 아래와 같은 역할을 합니다.

  • 성공적으로 최적화할 수 있는 컴포넌트 수 확인: 최적화 할 수 있는 컴포넌트는 많을수록 좋습니다.
  • <StrictMode> 사용 여부 확인: 이 옵션을 활성화하고 준수하면 리액트의 규칙이 준수될 가능성이 높아집니다.
  • 호환되지 않는 라이브러리 사용 확인: 컴파일러와 호환되지 않는것으로 알려진 라이브러리가 있는지 확인합니다.

예를 들어보겠습니다.

Successfully compiled 8 out of 9 components.
StrictMode usage not found.
Found no usage of incompatible libraries.

eslint-plugin-react-compiler 설치하기

리액트 컴파일러는 eslint 플러그인도 지원합니다. eslint 플러그인은 컴파일러와 독립적으로 사용할 수 있으므로 컴파일러를 사용하지 않더라도 eslint 플러그인을 사용할 수 있습니다.

npm install eslint-plugin-react-compiler

그런 다음 eslint 설정에 다음과 같이 추가합니다.

module.exports = {
  plugins: ["eslint-plugin-react-compiler"],
  rules: {
    "react-compiler/react-compiler": "error",
  },
};

eslint 플러그인은 에디터에서 리액트의 규칙을 위반하는 모든 것을 표시합니다. 이렇게 표시되면 컴파일러가 해당 컴포넌트나 훅 최적화를 건너뛴다는 것을 의미합니다. 이를 건너뛰어도 무방하며 컴파일러는 다시 복구하고 코드베이스의 다른 컴포넌트를 계속 최적화할 수 있습니다.

모든 eslint 위반을 바로 수정할 필요는 없습니다. 각자가 원하는 속도로 최적화되는 컴포넌트와 훅의 양을 늘려나갈 순 있지만, 컴파일러를 사용하기 전에 모든 것을 수정할 필요는 없습니다.

컴파일러를 코드베이스에 배포하기

기존 프로젝트

컴파일러는 리액트의 규칙을 따르는 함수형 컴포넌트와 훅을 컴파일하도록 설계되었습니다. 또한 컴파일러는 해당 컴포넌트나 훅을 건너뛰는 방식으로 규칙을 위반하는 코드를 처리할 수 있습니다. 그러나 자바스크립트의 유연한 특성으로 인해 컴파일러는 가능한 모든 위반을 포착할 수는 없으며 잘못 판단하여 컴파일 할 수도 있습니다. 컴파일러는 실수로 리액트 규칙을 위반하는 컴포넌트 또는 훅을 컴파일하여 정의되지 않은 동작을 유발할 수 있습니다.

따라서 기존 프로젝트에 컴파일러를 성공적으로 적용하려면 먼저 작은 디렉토리에서부터 컴파일러를 실행하는 것이 좋습니다. 컴파일러가 특정 디렉토리 집합에서만 실행되도록 설정하면 됩니다.

const ReactCompilerConfig = {
  sources: (filename) => {
    return filename.indexOf("src/path/to/dir") !== -1;
  },
};

드문 경우지만 compilationMode: "annotation" 옵션을 사용하면 컴파일러가 "옵트인" 모드에서 실행되도록 구성할 수도 있습니다. 이렇게 하면 컴파일러가 "use memo" 지시어로 주석이 달린 컴포넌트와 훅만 컴파일하도록 설정할 수 있습니다. 어노테이션 모드는 얼리 어답터를 돕기 위한 임시 모드이며 "use memo" 지시문을 장기적으로 사용할 의도는 없다는 점에 유의하세요.

const ReactCompilerConfig = {
  compilationMode: "annotation",
};

// src/app.jsx
export default function App() {
  "use memo";
  // ...
}

컴파일러 배포에 자신감이 생기면 다른 디렉토리로 적용 범위를 확장하고 전체 애플리케이션에 천천히 확장할 수 있습니다.

새 프로젝트

새 프로젝트를 시작하는 경우, 기본 동작으로 컴파일러를 전체 코드베이스에 활성화할 수 있습니다.

사용법

Babel

npm install babel-plugin-react-compiler

컴파일러에는 빌드 파이프라인에서 컴파일러를 실행하는 데 사용할 수 있는 Babel 플러그인이 포함되어 있습니다.

설치 후 Babel 설정에 추가하세요. 파이프라인에서 컴파일러가 먼저 실행되어야 한다는 점에 유의하세요.

// babel.config.js
const ReactCompilerConfig = {
  /* ... */
};

module.exports = function () {
  return {
    plugins: [
      ["babel-plugin-react-compiler", ReactCompilerConfig], // 먼저 실행되어야 합니다
      // ...
    ],
  };
};

컴파일러는 안전한 분석을 위해 입력 소스 정보를 필요로 하므로 다른 Babel 플러그인보다 먼저 babel-plugin-react-compiler를 실행해야 합니다.

Vite

Vite를 사용하는 경우 vite-plugin-react에 플러그인을 추가할 수 있습니다.

// vite.config.js
const ReactCompilerConfig = {
  /* ... */
};

export default defineConfig(() => {
  return {
    plugins: [
      react({
        babel: {
          plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
        },
      }),
    ],
    // ...
  };
});

Next.js

Next.js에는 리액트 컴파일러를 활성화하기 위한 실험적인 설정이 있습니다. 이 설정은 자동으로 Babel이 babel-plugin-react-compiler로 설정되도록 합니다.

  • 리액트 19 릴리스 후보를 사용하는 Next.js canary 버전을 설치합니다.
  • babel-plugin-react-compiler를 설치합니다.
npm install next@canary babel-plugin-react-compiler

그런 다음 next.config.js에서 실험적 옵션을 추가합니다.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

module.exports = nextConfig;

실험적 옵션을 사용하면 아래에 대해 리액트 컴파일러에 대한 지원이 보장됩니다.

  • 앱 라우터
  • 페이지 라우터
  • 웹팩(기본값)
  • 터보팩(--turbo를 통해 옵트인)

Remix

vite-plugin-babel을 설치하고 컴파일러의 Babel 플러그인을 추가합니다.

npm install vite-plugin-babel
// vite.config.js
import babel from "vite-plugin-babel";

const ReactCompilerConfig = {
  /* ... */
};

export default defineConfig({
  plugins: [
    remix({
      /* ... */
    }),
    babel({
      filter: /\.[jt]sx?$/,
      babelConfig: {
        presets: ["@babel/preset-typescript"], // TypeScript를 사용하는 경우
        plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
      },
    }),
  ],
});

Webpack

다음과 같이 리액트 컴파일러용 로더를 직접 만들 수도 있습니다.

const ReactCompilerConfig = { /* ... */ };
const BabelPluginReactCompiler = require('babel-plugin-react-compiler');

function reactCompilerLoader(sourceCode, sourceMap) {
  // ...
  const result = transformSync(sourceCode, {
    // ...
    plugins: [
      [BabelPluginReactCompiler, ReactCompilerConfig],
    ],
  // ...
  });

  if (result === null) {
    this.callback(
      Error(
        `Failed to transform "${options.filename}"`
      )
    );
    return;
  }

  this.callback(
    null,
    result.code
    result.map === null ? undefined : result.map
  );
}

module.exports = reactCompilerLoader;

Expo

Expo는 Metro를 통해 바벨을 사용하므로 설치 방법은 바벨과 함께 사용하기 섹션을 참조하세요.

Metro (React Native)

React Native는 Metro를 통해 바벨을 사용하므로 설치 방법은 바벨과 함께 사용하기 섹션을 참조하세요.

문제 해결

이슈를 보고하려면 먼저 리액트 컴파일러 플레이그라운드에서 최소한의 재현을 생성하고 버그 리포트에 포함하세요. facebook/react 리포지토리에서 이슈를 열 수 있습니다.

또한 리액트 컴파일러 워킹 그룹에 멤버로 신청하여 피드백을 제공할 수도 있습니다. 가입에 대한 자세한 내용은 README를 참조하세요.

(0 , _c) is not a function 에러

이 문제는 리액트 19 RC 이상을 사용하지 않는 경우 발생합니다. 이 문제를 해결하려면 먼저 앱을 리액트 19 RC로 업그레이드하세요.

리액트 19로 업그레이드할 수 없는 경우 워킹 그룹에 설명된 대로 캐시 기능을 사용자 공간에 구현하도록 시도해 볼 수 있습니다. 그러나 이 방법은 권장되지 않으며 가능하면 리액트 19로 업그레이드하는 것이 좋습니다.

내 컴포넌트가 최적화되었는지 어떻게 알 수 있나요?

리액트 개발자 도구(v5.0+)는 React 컴파일러를 기본적으로 지원하며 컴파일러에 의해 최적화된 컴포넌트 옆에 "메모 ✨" 배지를 표시합니다.

컴파일 후 무언가 작동하지 않습니다.

eslint-plugin-react-compiler를 설치한 경우 컴파일러는 에디터에서 리액트의 규칙을 위반하는 모든 것을 표시합니다. 이렇게 표시되면 컴파일러가 해당 컴포넌트나 훅 최적화를 건너뛴다는 것을 의미합니다. 이는 완전히 괜찮으며 컴파일러는 다시 복구하고 코드베이스의 다른 컴포넌트를 계속 최적화할 수 있습니다. 모든 eslint 위반을 바로 수정할 필요는 없습니다. 원하는 속도에 맞춰 문제를 해결하여 최적화되는 컴포넌트와 훅의 양을 늘릴 수 있습니다.

그러나 유연하고 동적인 자바스크립트의 특성으로 인해 모든 경우를 포괄적으로 탐지할 수는 없습니다. 이러한 경우 무한 루프와 같은 버그 및 정의되지 않은 동작이 발생할 수 있습니다.

컴파일 후 앱이 제대로 작동하지 않지만 아무런 eslint 오류가 표시되지 않는다면 컴파일러가 코드를 잘못 컴파일하고 있는 것일 수 있습니다. 이를 확인하려면 "use no memo" 지시문을 통해 관련성이 있다고 생각되는 컴포넌트나 훅을 적극적으로 제외하여 문제를 해결해 보세요.

function SuspiciousComponent() {
  "use no memo"; // 리액트 컴파일러의 컴파일에서 선택적으로 해당 컴포넌트를 제외합니다
  // ...
}

※ Note

"use no memo"

"use no memo"는 컴포넌트와 훅이 리액트 컴파일러에서 컴파일되지 않도록 할 수 있는 임시 탈출구입니다. 이 지시어는 "use client"처럼 계속 사용할 수 있는 것은 아닙니다.

꼭 필요한 경우가 아니라면 이 지시어를 사용하지 않는 것이 좋습니다. 컴포넌트나 훅을 옵트아웃하면 해당 지시어가 제거될 때까지 영원히 옵트아웃됩니다. 즉, 코드를 수정하더라도 지시어를 제거하지 않으면 컴파일러가 컴파일을 건너뛰게 됩니다.

오류가 사라지면 옵트아웃 지시문을 제거해도 문제가 다시 발생하는지 확인하세요. 그런 다음 리액트 컴파일러 플레이그라운드에서 버그 리포트를 공유하여(작은 범위의 재현으로 축소하거나 오픈 소스 코드인 경우 전체 소스를 붙여넣어도 됩니다) 문제를 파악하고 해결할 수 있도록 도와주세요.

그 외 이슈

여기를 참조해 주세요. https://github.com/reactwg/react-compiler/discussions/7.

0개의 댓글