[React] 모노레포 환경에서 React Compiler 도입기

2026-03-04 hit count image

6개의 앱과 공유 패키지로 구성된 모노레포에 React Compiler를 일괄 도입한 과정과 마주친 문제들, panicThreshold 선택, eslint-disable 전략, ref.current 오탐지 대응, 그리고 문제 발생 시 "use no memo" 디렉티브를 활용한 대응 방법을 공유합니다.

react

개요

이전 블로그 포스트에서 React의 렌더링 최적화와 useMemo, useCallback, React.memo를 활용한 메모이제이션에 대해 알아보았습니다. 아직 읽지 않으신 분들은 다음 링크를 참고해 주시기 바랍니다.

React 19로 업그레이드를 완료한 후, 자연스럽게 다음 단계로 React Compiler 도입을 검토하게 되었습니다. React 19 마이그레이션에 대해서는 다음 링크를 참고해 주시기 바랍니다.

React Compiler는 컴포넌트와 훅을 자동으로 메모이제이션하여, 수동으로 useMemo, useCallback, React.memo를 작성하지 않아도 불필요한 리렌더링을 방지해 줍니다. 이번 블로그 포스트에서는 6개의 앱과 공유 패키지로 구성된 모노레포에 React Compiler를 일괄 도입한 과정과, 그 과정에서 마주친 문제들을 공유합니다.

환경

도입 대상 프로젝트의 환경은 다음과 같습니다.

  • React 19, Vite 5, TypeScript
  • Yarn Workspaces 기반 모노레포 (6개 앱 + 공유 라이브러리)
  • ESLint 8 (레거시 설정 형식)
  • 전체 함수형 컴포넌트 (Class 컴포넌트 없음)

도입 과정

패키지 설치

먼저 필요한 패키지들을 설치합니다.

# babel-plugin-react-compiler 설치
yarn add -D [email protected]

# eslint-plugin-react-hooks 업그레이드 (v6에 컴파일러 검증 룰 통합)
# eslint-plugin-react-compiler 설치
yarn add -D eslint-plugin-react-hooks@^6.1.0 [email protected]

주의할 점은 eslint-plugin-react-hooks v6의 recommended-latest 프리셋은 ESLint 9의 flat config 형식에서만 동작한다는 것입니다. ESLint 8 레거시 형식을 사용하는 경우 기존의 recommended를 유지하고, eslint-plugin-react-compiler를 별도로 설치해야 합니다.

ESLint 설정

ESLint 공유 설정 파일에 react-compiler 플러그인과 룰을 다음과 같이 추가합니다.

// eslint 공유 설정
module.exports = {
  extends: ['./index.js', 'plugin:react-hooks/recommended'],
  plugins: ['react-refresh', 'react-compiler'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    'react-compiler/react-compiler': 'error',
  },
};

react-compiler/react-compiler 룰을 error로 설정하면, React의 규칙을 위반하는 코드를 빌드 전에 잡아낼 수 있습니다.

React 프로젝트에서 ESLint를 설정 하는 방법에 대해 더 자세히 알고 싶으신 분들은 다음 링크를 참고해 주시기 바랍니다.

Vite 설정

각 앱의 vite.config.ts에 다음과 같이 Babel 플러그인을 추가합니다.

import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', { panicThreshold: 'NONE' }]],
      },
    }),
  ],
});

모노레포에서 각각의 앱이 공유 라이브러리의 소스를 import하고 있다면, 각 앱 빌드 시 자동으로 컴파일되므로 공유 패키지에 별도 설정이 필요가 없습니다. 설정은 앱 레벨의 vite.config.ts에만 추가하면 됩니다.

Vite 기반 React 프로젝트를 시작하는 방법에 대해서는 다음 링크를 참고해 주시기 바랍니다.

마주친 문제들

panicThreshold 선택

처음에는 panicThreshold: 'CRITICAL_ERRORS'로 설정했습니다. 이 옵션은 최적화 불가능한 컴포넌트를 건너뛰되, 심각한 에러만 빌드를 실패시킵니다.

그런데 기존 코드에 있던 eslint-disable react-hooks/exhaustive-deps 주석이 컴파일러에 의해 critical error로 분류되면서 빌드가 실패했습니다. 이러한 패턴이 프로젝트 전반에 걸쳐 313곳이나 존재했기 때문에, panicThreshold: 'NONE'으로 변경하여 해당 컴포넌트들은 자동으로 최적화를 건너뛰도록 했습니다.

panicThreshold의 각 옵션은 다음과 같습니다.

옵션동작
NONE최적화 불가 시 조용히 건너뜀
CRITICAL_ERRORS심각한 위반만 빌드 실패
ALL_ERRORS모든 위반에서 빌드 실패

점진적 도입을 위해서는 NONE으로 시작하여, 안정화 이후에 단계적으로 올려가는 것을 권장합니다.

eslint-disable 주석 전략

react-compiler/react-compiler 룰을 error로 설정하면 기존에 hooks 룰을 위반하던 313곳에서 lint 에러가 발생합니다. 이를 처리하기 위해 두 가지 방법을 검토했습니다.

파일 상단에 일괄 추가 (채택하지 않음)

/* eslint-disable react-compiler/react-compiler */

파일 전체의 컴파일러 검증이 비활성화되어, 같은 파일 내 다른 컴포넌트까지 검증이 누락됩니다.

문제 발생 위치에 인라인 추가 (채택)

// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps

정확한 위치에만 적용되므로, 같은 파일의 다른 코드는 정상적으로 검증됩니다. lint를 실행해서 에러가 발생하는 정확한 파일과 라인 번호를 추출한 뒤, 스크립트로 일괄 삽입했습니다.

ref.current 변이 오탐지

이벤트 핸들러 내에서 ref.current를 통해 DOM을 조작하는 것은 React의 규칙을 위반하지 않습니다. 하지만 컴파일러는 “함수의 반환값을 변이하면 안 된다”는 경고를 발생시키는 경우가 있습니다.

const handleLoadedMetadata = () => {
  const video = videoRef.current;
  if (!video || !timelineRef.current) return;

  video.currentTime = 0; // 컴파일러 경고 발생
  timelineRef.current.setCurrentTime(0); // 컴파일러 경고 발생
};

이런 false positive의 경우에도 eslint-disable-next-line으로 억제하는 것이 적절합니다.

리스크와 테스트 가이드

레벨별 리스크

높음 - 렌더링 동작 변경

컴파일러가 자동으로 메모이제이션을 삽입하면서, 기존에 매 렌더링마다 실행되던 코드가 스킵될 수 있습니다.

  • useEffect 밖에서 사이드이펙트를 실행하는 코드
  • 렌더링 중 매번 새로운 객체/배열을 생성하는 것에 의존하는 로직
  • 렌더링 중 ref.current를 읽고 쓰는 패턴

React의 렌더링 동작에 대해서는 다음 블로그를 참고해 주시기 바랍니다.

높음 - 커스텀 Reconciler 사용 라이브러리

react-konva처럼 React의 내부 reconciler를 커스텀화해서 사용하는 라이브러리는 컴파일러의 메모이제이션과 충돌할 가능성이 높습니다. Canvas 기반 렌더링(어노테이션, 바운딩 박스 등)을 중점적으로 테스트해야 합니다.

중간 - 차트/키보드 관련 라이브러리

recharts, react-hotkeys-hook 등은 내부적으로 독자적인 state 관리나 글로벌 이벤트 리스너를 사용합니다. 차트 렌더링과 키보드 단축키 동작을 확인해야 합니다.

낮음 - 순수 렌더링/분석 라이브러리

qrcode.react, react-ga4 등은 순수한 렌더링 컴포넌트이거나 사이드이펙트만 수행하므로 리스크가 낮지만, 정상 동작을 하는지 확인하는 것이 좋습니다.

중점 테스트 항목

항목확인 포인트
페이지 초기 로드useEffect 초기화 로직 정상 실행 (데이터 fetch, 상태 세팅)
폼 입력/유효성 검증입력값 변경 즉시 반영, 에러 메시지 표시 타이밍
다이얼로그 열기/닫기상태 초기화, 데이터 리로드
테이블 정렬/페이지네이션정렬 순서, 페이지 전환 후 데이터
무한 스크롤IntersectionObserver 기반 스크롤 로드
라우팅 전환상태 클린업 및 새 페이지 초기화

문제 발생 시 대응: “use no memo” 디렉티브

테스트나 QA 중 특정 컴포넌트에서 이상 동작이 발견되면, "use no memo" 디렉티브로 해당 컴포넌트만 컴파일러 최적화에서 제외할 수 있습니다.

const ProblematicComponent = () => {
  'use no memo';

  return <div>...</div>;
};

ESLint disable과의 차이

이 두 가지는 작동 레벨이 다릅니다.

"use no memo"eslint-disable react-compiler
영향 범위컴파일러 자체가 해당 함수를 스킵ESLint 경고만 숨김
빌드 시최적화 코드를 생성하지 않음panicThreshold에 의존
용도런타임에서 문제 발생 시lint 에러 억제

대응 플로우

테스트나 QA 중 문제가 발견되면 다음과 같이 대응할 수 있습니다.

  1. 테스트 또는 QA에서 이상 동작 발견
  2. 해당 컴포넌트에 "use no memo" 추가
  3. 문제 해결 확인 → 해결되면 유지
  4. 근본 원인 파악 후 코드 수정 → "use no memo" 제거

브라우저의 React DevTools에서 Memo ✨ 표시가 있는 컴포넌트가 컴파일러에 의해 최적화된 것입니다.

React DevTools - React Compiler에 의해 Memo 표시가 된 컴포넌트

정리

React Compiler 도입의 핵심은 점진적 접근입니다.

  • panicThreshold: 'NONE'으로 시작하여, 최적화 불가능한 컴포넌트는 자동으로 건너뛰게 합니다.
  • ESLint 룰은 error로 설정하되, 기존 위반 부분에는 인라인 주석으로 억제합니다.
  • 문제가 발생하면 "use no memo"로 개별 컴포넌트를 opt-out 합니다.
  • 안정화 이후에 수동 useMemo/useCallback/React.memo를 제거하고, panicThreshold를 단계적으로 올려갈 수 있습니다.

이전 블로그 포스트에서 다루었던 수동 메모이제이션의 복잡함과 실수 가능성을 생각하면, React Compiler의 자동 최적화는 큰 개선입니다. 다만, 기존 코드베이스의 규칙 위반이 많을수록 도입 시 마주치는 문제도 많아지므로, 점진적으로 접근하는 것이 중요합니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.



SHARE
Twitter Facebook RSS