포스트

[이제와서 시작하는 Next.js 마스터하기 #14] React Compiler로 자동 최적화

[이제와서 시작하는 Next.js 마스터하기 #14] React Compiler로 자동 최적화

“수동 메모이제이션을 줄일 수 있지만, 측정과 React 규칙은 여전히 중요합니다.” - React Compiler를 안전하게 적용해봅니다.

🎯 이 글에서 배울 내용

  • React Compiler가 무엇인지
  • 자동 메모이제이션의 원리
  • 성능 향상 측정 방법
  • 마이그레이션 가이드
  • annotation 모드와 opt-out 기준

예상 소요 시간: 30분 난이도: 중급


🎨 React Compiler란?

컴파일 타임에 React 컴포넌트를 분석해 불필요한 재계산과 리렌더링을 줄이는 도구입니다. Next.js에서는 babel-plugin-react-compiler를 설치한 뒤 next.config.ts에서 명시적으로 켤 수 있습니다.

다만 “이제 useMemouseCallback을 전혀 몰라도 된다”는 뜻은 아닙니다. Compiler가 안전하게 최적화할 수 있는 코드를 작성해야 하고, 외부 라이브러리나 복잡한 mutable 상태가 있는 곳은 기존 최적화 지식이 여전히 필요합니다.

Next.js는 모든 파일에 무작정 React Compiler를 적용하지 않고, JSX나 React Hook이 있는 관련 파일에만 적용되도록 최적화합니다. 그래도 Babel plugin을 거치기 때문에 빌드 시간이 약간 늘 수 있으며, 적용 전후를 측정하는 단계가 필요합니다.

Before (수동 최적화)

1
2
3
4
5
6
7
8
9
10
11
const ExpensiveComponent = ({ data }) => {
  const computed = useMemo(() => {
    return heavyCalculation(data);
  }, [data]);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <div onClick={handleClick}>{computed}</div>;
};

After (React Compiler)

1
2
3
4
5
6
7
8
9
const ExpensiveComponent = ({ data }) => {
  const computed = heavyCalculation(data);  // 자동 메모이제이션!

  const handleClick = () => {
    console.log('clicked');
  };  // 자동 메모이제이션!

  return <div onClick={handleClick}>{computed}</div>;
};

🚀 활성화 방법

1
npm install -D babel-plugin-react-compiler
1
2
3
4
5
6
7
8
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactCompiler: true,
};

export default nextConfig;

활성화 후에는 먼저 개발 환경에서 주요 화면을 클릭해보고, 콘솔 경고와 hydration 오류가 없는지 확인하세요. 컴파일러는 React 규칙을 잘 지키는 코드에서 가장 안전하게 동작합니다.

annotation 모드로 작게 시작하기

전체 앱에 바로 적용하기 부담스럽다면 annotation 모드로 시작할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  reactCompiler: {
    compilationMode: 'annotation',
  },
};

export default nextConfig;

이 모드에서는 "use memo" 지시어를 붙인 컴포넌트나 Hook만 Compiler 대상으로 삼습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
export function ProductList({ products }) {
  'use memo';

  const visibleProducts = products.filter((product) => product.visible);

  return (
    <ul>
      {visibleProducts.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

반대로 특정 컴포넌트를 제외하고 싶다면 "use no memo"를 검토할 수 있습니다. 단, opt-out은 임시 우회로 두고 원인을 추적하는 편이 좋습니다.


📊 성능 측정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use client';

import { useState } from 'react';

export default function PerformanceTest() {
  const [count, setCount] = useState(0);

  // 무거운 계산
  const expensiveValue = (() => {
    console.log('Calculating...');
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i;
    }
    return result;
  })();

  return (
    <div>
      <p>Count: {count}</p>
      <p>Expensive: {expensiveValue}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

React Compiler 없이: “Calculating…” 매번 출력 React Compiler 사용: “Calculating…” 한 번만 출력

이 예제는 개념을 보여주기 위한 단순화된 코드입니다. 실제 프로젝트에서는 콘솔 출력만으로 판단하지 말고 다음 지표를 함께 봐야 합니다.

  • React DevTools Profiler의 commit 횟수와 렌더링 시간
  • 사용자 입력 후 화면 반응 시간
  • next build 시간 증가 여부
  • Babel 설정이 Turbopack 빌드에 미치는 영향
  • Compiler 적용 후 사라진 useMemo, useCallback이 실제 가독성을 높였는지

측정할 때는 같은 시나리오를 반복해야 합니다.

  1. Compiler 적용 전 브랜치에서 Profiler 기록을 저장합니다.
  2. 검색, 필터, 대시보드처럼 리렌더링이 많은 화면을 클릭합니다.
  3. Compiler를 켠 뒤 같은 화면, 같은 데이터, 같은 브라우저에서 다시 측정합니다.
  4. 렌더링 시간이 줄었더라도 build 시간이 크게 늘었다면 적용 범위를 조정합니다.
  5. 숫자가 비슷하다면 코드를 단순하게 만든 효과가 있는지 별도로 판단합니다.

✅ 잘 맞는 코드 패턴

React Compiler는 순수한 렌더링 로직을 좋아합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Product = {
  id: string;
  name: string;
  price: number;
};

export function ProductSummary({ products }: { products: Product[] }) {
  const total = products.reduce((sum, product) => sum + product.price, 0);

  return (
    <section>
      <p>상품 수: {products.length}</p>
      <p>총액: {total.toLocaleString()}</p>
    </section>
  );
}

좋은 신호:

  • props와 state를 직접 변경하지 않습니다.
  • 렌더링 중 네트워크 요청이나 DOM 조작을 하지 않습니다.
  • 파생 값은 입력값만으로 계산됩니다.
  • 컴포넌트 바깥 mutable 변수를 렌더링 결과에 섞지 않습니다.
  • useEffect로 파생 state를 다시 계산하지 않고 렌더링 중 계산 가능한 값은 그대로 계산합니다.

이런 코드는 Compiler가 최적화하기 좋고, Compiler가 없더라도 React에서 예측 가능하게 동작합니다.


⚠️ 주의할 패턴

아래처럼 렌더링 중 외부 값을 바꾸는 코드는 Compiler가 최적화하기 어렵고, React의 기본 규칙과도 맞지 않습니다.

1
2
3
4
5
6
let renderCount = 0;

export function BadExample() {
  renderCount += 1;
  return <p>렌더링 횟수: {renderCount}</p>;
}

이런 코드는 useEffect, 서버 로직, 상태 관리 로직으로 분리하세요. Compiler를 켠 뒤 문제가 생긴다면 먼저 React의 순수 렌더링 규칙을 어긴 부분이 없는지 확인하는 것이 빠릅니다.

다음 패턴도 주의하세요.

패턴 왜 위험한가 대안
props 배열을 직접 push/sort 부모 데이터까지 바뀔 수 있음 toSorted, spread, map처럼 새 배열 생성
렌더링 중 Date.now()/Math.random() 사용 같은 입력에도 출력이 달라짐 서버에서 값 생성 또는 effect/event로 이동
DOM을 직접 읽고 쓰기 React 렌더링 모델 밖의 변경 ref + effect로 분리
외부 mutable singleton 참조 캐시와 렌더링 결과가 엇갈릴 수 있음 명시적 props/state/store 구독으로 연결
library Hook 규칙 위반 Compiler 이전에 React 규칙 위반 eslint-plugin-react-hooks로 먼저 정리

Compiler는 문제가 있는 코드를 마법처럼 고치는 도구가 아니라, React 규칙을 잘 지킨 코드를 더 효율적으로 실행하도록 돕는 도구입니다.


🧪 실습: Compiler 적용 전후 비교

  1. reactCompiler: true를 켜기 전 브랜치에서 React Profiler 기록을 남깁니다.
  2. Compiler를 켜고 babel-plugin-react-compiler를 설치합니다.
  3. 상품 목록, 대시보드, 검색 UI처럼 렌더링이 많은 화면을 다시 측정합니다.
  4. 불필요해진 useMemo, useCallback은 한 번에 지우지 말고 작은 PR로 제거합니다.
  5. 테스트와 주요 사용자 흐름을 확인한 뒤 적용 범위를 넓힙니다.

팀 프로젝트라면 rollout 단계를 이렇게 나눌 수 있습니다.

단계 적용 범위 목표
1단계 annotation 모드 + 한 화면 빌드/런타임 이상 여부 확인
2단계 대시보드, 검색, 리스트 화면 리렌더링 많은 영역의 체감 개선 확인
3단계 전체 앱 reactCompiler: true 수동 memo 정리와 회귀 테스트
4단계 CI 기준 고정 build 시간, Profiler 결과, E2E 통과 기준 문서화

적용 후 바로 모든 useMemouseCallback을 지우지 마세요. 일부는 참조 안정성이 필요한 외부 라이브러리 props나 context value에 여전히 의미가 있을 수 있습니다.


🎯 오늘 배운 내용 정리

  1. React Compiler
    • 컴파일 타임 자동 최적화
    • 순수한 React 코드에서 효과가 큼
    • 수동 메모이제이션을 줄일 수 있지만 검증은 필요
  2. 활성화
    • babel-plugin-react-compiler 설치
    • reactCompiler: true 설정
    • 필요한 경우 annotation 모드로 제한 적용
  3. 검증
    • Profiler와 build 시간을 함께 확인
    • React 순수 렌더링 규칙 위반을 먼저 정리
    • 수동 memo 제거는 작은 PR로 나누기

📚 시리즈 네비게이션


“좋은 React 코드 위에서 Compiler는 더 빛납니다.” 🎨

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.