React Native 에서의 메모이제이션 (2. React Compiler 의 자동 메모이제이션)

eeennsu·2026년 8월 2일

React Native

목록 보기
77/96

1. React Compiler란?

React Compiler(이하 RC)는 빌드 타임에 코드를 분석해 메모이제이션을 자동으로 삽입하는 컴파일러다. 지금까지 개발자가 손으로 깔던 React.memo / useMemo / useCallback을, 컴파일러가 데이터 흐름과 가변성(mutability)을 분석해 적절한 위치에 자동으로 넣어준다. 런타임 라이브러리가 아니라 빌드 단계의 변환 도구이고, Rules of React를 따르는 코드라면 코드를 다시 쓸 필요 없이 적용된다.

2025년 10월 7일 1.0 정식 버전이 출시되었고, React와 React Native 양쪽을 지원한다.


1) RC가 실제로 절약하는 것 — JS 스레드의 재실행·재조정

가장 먼저 바로잡아야 할 오해는 "RC가 네이티브 렌더링이나 화면 갱신을 줄여준다"는 그림이다. 아니다. RC가 메모이제이션으로 절약하는 대상은 컴포넌트 함수의 재실행과 그 서브트리의 재조정(reconciliation) 이며, 둘 다 순수하게 JS 스레드에서 일어나는 작업이다.

React의 동작을 단계로 나누면 분명해진다. 상태·props 변화로 컴포넌트 함수가 재실행되고(렌더 단계), 새 트리와 이전 트리를 diff해 변경분을 추리고(재조정), 변경분만 호스트에 반영한다(커밋 단계). 출력이 이전과 같으면 재조정 단계에서 "변경 없음"으로 걸러져 커밋(네이티브 갱신)은 어차피 일어나지 않는다 — 메모이제이션 여부와 무관하게.

따라서 RC가 막아주는 것은 "출력이 같은데도 굳이 컴포넌트를 재실행하고 그 자식들을 재조정하는" JS 스레드 낭비다. 이 구분이 RC를 이해하는 핵심이다.


2) 어떻게 동작하는가 — 표현식 단위 캐시

RC는 컴포넌트를 컴파일하면서, 렌더 중 생성되는 각 값·함수가 어떤 입력에 의존하는지를 표현식 단위로 추적한다. 그리고 값마다 독립된 캐시 슬롯과 독립된 의존성 비교를 배치한다. (산출물에 등장하는 _c 헬퍼는 내부 구현 디테일이며 공식 API가 아님)

// 원본
function Component({ x, y }) {
  const a = expensiveA(x); // x에만 의존
  const b = expensiveB(y); // y에만 의존
  return <Child a={a} b={b} />;
}
// RC가 생성하는 코드 (개념도)
function Component({ x, y }) {
  const $ = _c(5); // 캐시 슬롯 배열

  let a;
  if ($[0] !== x) {
    // a는 x가 바뀔 때만 재계산
    a = expensiveA(x);
    $[0] = x;
    $[1] = a;
  } else {
    a = $[1];
  }

  let b;
  if ($[2] !== y) {
    // b는 y가 바뀔 때만 재계산 — a와 독립
    b = expensiveB(y);
    $[2] = y;
    $[3] = b;
  } else {
    b = $[3];
  }

  // <Child a={a} b={b} /> JSX도 a·b가 그대로면 이전 엘리먼트를 재사용
  // ...
}

x만 바뀌면 a만 다시 계산하고 b는 캐시 값을 그대로 쓴다. JSX 엘리먼트 자체도 같은 방식으로 캐시되어, props가 그대로면 이전에 만든 엘리먼트를 재사용한다 — 이것이 React.memo가 하던 "props 안 바뀌면 자식 리렌더 스킵"과 동일한 효과다.


2. 수동 메모이제이션보다 정밀한 이유

수동 useMemo는 블록 단위다. 콜백이 반환하는 값을 통째로 캐시하고, deps 중 하나만 바뀌어도 콜백 전체가 다시 돈다. 위 예시를 useMemo 하나로 묶으면 x만 바뀌어도 expensiveB(y)까지 재실행된다.

RC는 이를 표현식 단위로 쪼개므로, 같은 정밀도를 손으로 내려면 값마다 useMemo를 따로 걸어야 하며 값이 늘수록 비현실적이다. "표현식 단위로 캐시한다"는 말의 뜻은 각 값이 자기 의존성에만 반응하는 것이며 값마다 독립된 캐시 슬롯과 독립된 의존성 비교를 둔다는 것이다.

게다가 RC는 조건부(분기 안에서의) 메모이제이션처럼 수동으로는 표현하기 어려운 최적화도 한다. 공식 문서가 "RC는 useMemo/useCallback/React.memo보다 더 정밀하고 granular하게 메모이제이션한다"고 말하는 근거가 이것이다.


3. Rules of React에 의존

RC가 안전하게 동작하는 전제는 코드가 Rules of React를 지키는 것이다(렌더 순수성, 렌더 중 props/state/ref/전역 변이 금지, 훅 규칙 등). 컴파일러는 이 규칙 위반을 정적으로 감지하면 그 컴포넌트만 최적화에서 skip 하고 나머지는 계속 컴파일한다.

여기서 중요한 성질이 있는데, 바로 이 skip은 에러가 아니라 조용한 안전 후퇴다. 기본 동작은 치명적이지 않은 위반을 빌드를 깨지 않고 skip하는 것이며, 모든 위반을 무중단 skip 처리하려면 panicThreshold: 'none'을 명시(공식 docs의 프로덕션 권장 설정)한다. 어쨌든 어떤 컴포넌트가 최적화에서 빠졌는지 명시적 신호 없이 지나갈 수 있으므로, 두 가지 도구가 책임을 나눠 갖는다.

  • eslint-plugin-react-hooks recommended/recommended-latest 프리셋 — RC 1.0부터 별도 eslint-plugin-react-compiler는 deprecated이고, 검증 규칙이 이쪽으로 통합되었다. 역할은 컴파일러가 못 잡은 Rules of React 위반을 빌드 전에 표면화하는 것이다(RC를 켜지 않아도 켤 수 있다).
  • ✨ 배지 — React(Native) DevTools의 Component Inspector에서 실제로 컴파일된 컴포넌트에 ✨ 배지가 붙는다. 적용 여부 확인은 이쪽 책임이다 — 배지가 없으면 skip된 것.

즉 "RC를 켰으니 무조건 다 최적화된다"가 아니라, 린트로는 사전 위반 검출, DevTools 배지로는 적용 여부 확인이라는 절차가 따라붙는다. RC가 안전하게 적용하기 위해 지켜야 하는 rules는 다다음 글에서 살펴볼 예정이다.


4. React 19 / React Native와의 관계

RC는 React 19를 네이티브로 지원하며, RN은 0.78부터 React 19 런타임을 채택했다(RN 자체 버전은 여전히 0.x다). React 17/18에서도 동작시킬 수 있는데, 그땐 두 가지가 추가로 필요하다.

  • react-compiler-runtime 패키지 설치
  • compiler config에 target: '17' | '18' 명시 (기본 타깃은 '19')

RN에서는 Metro가 Babel을 쓰므로 babel-plugin-react-compiler로 적용한다. 이 플러그인은 다른 Babel 플러그인들보다 먼저 실행되어야 한다 — babel.config.js의 plugins 배열에서 반드시 첫 번째 위치에 둬야 변환이 깨지지 않는다(공식 docs에서 강조하는 제약).

New Architecture로 네이티브 커밋 비용이 낮아졌어도 컴포넌트 함수 실행과 재조정은 여전히 JS 스레드에 남으므로, RC의 역할은 아키텍처 전환과 무관하게 유지된다.


5. RC가 바꾸지 않는 것

마지막으로, RC가 무엇을 안 바꾸는지가 그 정체를 가장 잘 드러낸다. RC는 메모이제이션의 비용·이득 모델 자체를 바꾸지 않는다. 절약되는 대상은 여전히 JS 스레드의 재실행·재조정이고, 그 가치가 RN의 단일 JS 스레드 구조에서 더 크다는 점도 그대로다. RC가 바꾼 것은 오직 적용 방식이다 — 사람이 어디에 memo를 걸지 판단하던 일을, 컴파일러가 더 정밀하게 자동으로 한다.




정리

React Compiler는 빌드 타임에 데이터 흐름을 분석해 표현식 단위로 메모이제이션을 자동 삽입하는 컴파일러다. 절약하는 대상은 네이티브 렌더링이 아니라 JS 스레드의 컴포넌트 재실행·재조정이고, 수동 useMemo보다 잘게·조건부로 최적화한다.

동작 전제는 Rules of React 준수이며, 위반 컴포넌트는 빌드를 깨지 않고 조용히 skip한다. 그래서 eslint-plugin-react-hooks로 사전 위반을 검출하고 DevTools의 ✨ 배지로 실제 적용 여부를 확인하는 절차가 따라온다. RN에 도입할 땐 babel-plugin-react-compiler를 Babel plugins 배열의 첫 번째로 두고, React 17/18에서는 react-compiler-runtime + target 옵션을 명시한다. "이론은 동일하고 적용만 자동화된다"가 핵심이다.

profile
이력서 https://resume.eunsu.pro

0개의 댓글