본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 7장: React Compiler - 자동 최적화의 시대
2026년 2월 4일·웹 개발·

7장: React Compiler - 자동 최적화의 시대

React Compiler의 동작 원리, HIR 기반 분석, 자동 메모이제이션, 설치와 설정, ESLint 통합, 실전 적용 전략을 다룹니다.

15분590자8개 섹션
reactnextjsperformancefrontendtypescript
공유
react19-rsc7 / 11
1234567891011
이전6장: Suspense 고급 패턴과 스트리밍 SSR다음8장: ref 개선, 메타데이터, 리소스 로딩 API

지금까지 React 19의 서버 중심 기능들을 살펴보았습니다. 이번 장에서는 클라이언트 사이드 성능을 근본적으로 개선하는 React Compiler를 다룹니다. React Compiler는 빌드 타임에 컴포넌트를 분석하여 자동으로 메모이제이션을 적용하는 최적화 도구로, 2025년 10월에 안정 버전 1.0이 출시되었습니다.

왜 React Compiler가 필요한가

수동 메모이제이션의 문제

React는 상태가 변경되면 해당 컴포넌트와 모든 자식 컴포넌트를 다시 렌더링합니다. 이를 최적화하기 위해 개발자들은 수동으로 메모이제이션을 적용해왔습니다.

수동 메모이제이션의 부담
typescript
import { useMemo, useCallback, memo } from 'react';
 
const ExpensiveList = memo(function ExpensiveList({
  items,
  onSelect,
}: {
  items: Item[];
  onSelect: (id: string) => void;
}) {
  const sortedItems = useMemo(
    () => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
    [items]
  );
 
  const handleSelect = useCallback(
    (id: string) => {
      onSelect(id);
    },
    [onSelect]
  );
 
  return (
    <ul>
      {sortedItems.map(item => (
        <ListItem key={item.id} item={item} onSelect={handleSelect} />
      ))}
    </ul>
  );
});
 
const ListItem = memo(function ListItem({
  item,
  onSelect,
}: {
  item: Item;
  onSelect: (id: string) => void;
}) {
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
});

이 코드에서 useMemo, useCallback, memo는 모두 불필요한 재렌더링을 방지하기 위한 것입니다. 하지만 이 접근법에는 여러 문제가 있습니다.

  • 인지 부담: 어디에 메모이제이션을 적용해야 할지 판단하기 어렵습니다.
  • 과도한 적용: 불필요한 곳에 적용하면 메모리를 낭비하고 코드를 복잡하게 만듭니다.
  • 부족한 적용: 필요한 곳에 빠뜨리면 성능 저하가 발생합니다.
  • 의존성 배열 실수: 의존성을 잘못 지정하면 버그가 발생합니다.
  • 전파 문제: memo로 감싼 컴포넌트에 전달하는 모든 함수와 객체도 메모이제이션해야 효과가 있습니다.

React Compiler의 해법

React Compiler는 이 모든 문제를 빌드 타임 자동화로 해결합니다.

React Compiler 적용 후
typescript
// useMemo, useCallback, memo가 불필요
function ExpensiveList({
  items,
  onSelect,
}: {
  items: Item[];
  onSelect: (id: string) => void;
}) {
  const sortedItems = items.slice().sort((a, b) =>
    a.name.localeCompare(b.name)
  );
 
  return (
    <ul>
      {sortedItems.map(item => (
        <ListItem key={item.id} item={item} onSelect={onSelect} />
      ))}
    </ul>
  );
}
 
function ListItem({
  item,
  onSelect,
}: {
  item: Item;
  onSelect: (id: string) => void;
}) {
  return (
    <li onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
}

코드가 훨씬 간결해졌지만, Compiler가 자동으로 필요한 메모이제이션을 적용하므로 성능은 동일하거나 더 좋습니다.

React Compiler의 동작 원리

컴파일 파이프라인

React Compiler는 Babel 플러그인으로 구현되어 있으며, 다음 단계를 거칩니다.

  1. AST 파싱: JavaScript/TypeScript 소스 코드를 Abstract Syntax Tree로 파싱합니다.
  2. HIR 변환: AST를 High-Level Intermediate Representation으로 변환합니다. HIR은 Control Flow Graph(CFG) 기반 아키텍처로, 코드의 실행 흐름을 정밀하게 분석할 수 있습니다.
  3. 데이터 흐름 분석: 각 값의 생성, 사용, 변경(mutation)을 추적합니다.
  4. 메모이제이션 삽입: 분석 결과를 바탕으로 최적의 위치에 메모이제이션 코드를 삽입합니다.

분석 수준

React Compiler는 컴포넌트 단위가 아닌 값 단위로 메모이제이션을 적용합니다.

세분화된 메모이제이션
typescript
function UserCard({ user, theme }: { user: User; theme: Theme }) {
  // Compiler는 이 스타일 객체를 theme이 변경될 때만 재생성
  const cardStyle = {
    background: theme.cardBg,
    color: theme.text,
  };
 
  // 이 계산은 user.posts가 변경될 때만 재실행
  const recentPosts = user.posts
    .filter(p => p.published)
    .slice(0, 5);
 
  // 이 JSX는 관련 값이 변경될 때만 재생성
  return (
    <div style={cardStyle}>
      <h2>{user.name}</h2>
      <ul>
        {recentPosts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

수동으로 useMemo를 적용했다면 cardStyle과 recentPosts 각각에 별도의 useMemo가 필요했을 것입니다. Compiler는 이를 자동으로 처리하며, 조건부 반환 이후의 값도 정확하게 메모이제이션합니다.

변경 감지 (Mutation Tracking)

Compiler의 핵심 기능 중 하나는 값의 변경(mutation)을 정밀하게 추적하는 것입니다.

변경 추적 예시
typescript
function TodoList({ todos }: { todos: Todo[] }) {
  // Compiler는 이 배열이 새로 생성되었고,
  // sort가 원본을 변경하지 않음을 이해
  const sorted = [...todos].sort((a, b) =>
    a.priority - b.priority
  );
 
  // filteredTodos는 sorted에서 파생되므로
  // todos가 변경되지 않으면 재계산하지 않음
  const filteredTodos = sorted.filter(t => !t.completed);
 
  return (
    <ul>
      {filteredTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

설치와 설정

기본 설치

bash
pnpm add --save-dev --save-exact babel-plugin-react-compiler@latest
Tip

React Compiler는 정확한 버전을 고정(--save-exact)하는 것이 권장됩니다. 향후 버전에서 메모이제이션 전략이 변경될 수 있으며, 예기치 않은 동작 변화를 방지합니다.

Next.js 설정

next.config.ts
typescript
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
 
export default nextConfig;

Next.js 15.3.1 이상에서 기본 지원됩니다.

Vite 설정

vite.config.ts
typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
const ReactCompilerConfig = {
  // 컴파일러 옵션
};
 
export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', ReactCompilerConfig],
        ],
      },
    }),
  ],
});

호환성

React Compiler는 다음 환경을 지원합니다.

React 버전지원추가 패키지
React 19+완전 지원불필요
React 18지원react-compiler-runtime 필요
React 17지원react-compiler-runtime 필요
bash
# React 17/18에서 사용할 경우
pnpm add react-compiler-runtime

ESLint 통합

React Compiler의 ESLint 규칙은 eslint-plugin-react-hooks의 recommended 프리셋에 포함되어 있습니다. 별도의 플러그인이 필요 없습니다.

eslint.config.mjs
typescript
import reactHooks from 'eslint-plugin-react-hooks';
 
export default [
  {
    plugins: {
      'react-hooks': reactHooks,
    },
    rules: reactHooks.configs.recommended.rules,
  },
];

Compiler가 감지하는 문제들

Compiler 진단 예시
typescript
// 렌더링 중 setState: set-state-in-render
function Counter() {
  const [count, setCount] = useState(0);
  setCount(count + 1); // 렌더링 중 상태 변경 - 무한 루프
  return <p>{count}</p>;
}
 
// 렌더링 중 ref 접근: refs
function Input({ ref }: { ref: React.Ref<HTMLInputElement> }) {
  // ref.current는 렌더링 중에 읽으면 안 됨
  console.log(ref.current?.value);
  return <input ref={ref} />;
}

Compiler는 이러한 Rules of React 위반을 빌드 타임에 감지하고 진단 메시지를 제공합니다.

점진적 도입 전략

특정 디렉토리에만 적용

next.config.ts - 디렉토리 제한
typescript
const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: {
      compilationMode: 'annotation',
    },
  },
};

compilationMode: 'annotation'으로 설정하면, 'use memo' 디렉티브가 있는 파일만 컴파일합니다.

선택적 적용
typescript
'use memo';  // 이 파일만 Compiler 적용
 
function OptimizedComponent() {
  // Compiler가 이 컴포넌트를 최적화
}

기존 useMemo/useCallback과의 공존

React Compiler가 적용된 후에도 기존의 useMemo, useCallback은 그대로 동작합니다. Compiler는 이들을 추가 힌트로 활용합니다.

공존 패턴
typescript
function Component({ data }: { data: Data }) {
  // Compiler가 적용되면 이 useMemo는 사실상 불필요
  // 하지만 제거하지 않아도 됨 - Compiler가 적절히 처리
  const processed = useMemo(
    () => expensiveProcess(data),
    [data]
  );
 
  return <Display data={processed} />;
}

Compiler가 안정적으로 동작하는 것을 확인한 후, 기존 메모이제이션 코드를 점진적으로 제거할 수 있습니다. 하지만 급하게 제거할 필요는 없습니다.

성능 영향

Meta의 프로덕션 결과

Meta에서 React Compiler를 프로덕션에 적용한 결과는 인상적입니다.

지표개선
초기 로드최대 12% 빠름
페이지 전환최대 12% 빠름
특정 인터랙션최대 2.5배 빠름
메모리 사용량중립 (증가 없음)

최적화가 효과적인 경우

  • 빈번한 상태 업데이트: 부모의 상태 변경으로 자식이 불필요하게 재렌더링되는 경우
  • 복잡한 계산: 매 렌더링마다 실행되는 정렬, 필터링, 변환 로직
  • 콜백 안정성: 이벤트 핸들러가 매 렌더링마다 새로 생성되어 자식에게 전달되는 경우

주의할 점

Compiler는 순수한 React 코드에 최적화되어 있습니다. React의 규칙을 위반하는 코드(렌더링 중 외부 상태 변경, ref 잘못 사용 등)에는 예상치 못한 동작이 발생할 수 있습니다.

Compiler와 호환되지 않는 패턴
typescript
// 렌더링 중 외부 변수 변경 - 순수하지 않음
let globalCounter = 0;
 
function BadComponent() {
  globalCounter++; // 외부 상태 변경
  return <p>{globalCounter}</p>;
}

이런 코드는 Compiler 없이도 버그를 유발할 수 있으므로, Compiler 도입을 계기로 코드 품질을 개선하는 것이 좋습니다.

React.memo와의 관계

React Compiler가 도입되면 대부분의 경우 React.memo가 불필요해집니다.

React.memo 전환 가이드
typescript
// Before: React.memo로 감싸야 했던 컴포넌트
const MemoizedCard = memo(function Card({ title, content }: CardProps) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});
 
// After: Compiler가 자동으로 최적화
function Card({ title, content }: CardProps) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}

다만, React.memo에 커스텀 비교 함수를 전달하는 경우는 Compiler가 자동으로 대체하지 않습니다.

커스텀 비교 함수는 유지
typescript
// 이 패턴은 Compiler가 대체하지 않음 - 유지 필요
const DeepCompareList = memo(
  function List({ items }: { items: Item[] }) {
    return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
  },
  (prev, next) => isDeepEqual(prev.items, next.items)
);

핵심 요약

  • React Compiler는 빌드 타임에 컴포넌트를 분석하여 자동으로 메모이제이션을 적용하는 Babel 플러그인입니다.
  • HIR(High-Level Intermediate Representation) 기반으로 데이터 흐름과 변경(mutation)을 정밀하게 추적합니다.
  • 값 단위의 세분화된 메모이제이션을 수행하여, useMemo, useCallback, React.memo를 수동으로 작성할 필요가 없어집니다.
  • React 17 이상에서 사용할 수 있으며, Next.js 15.3.1+에서 실험적 옵션으로 활성화합니다.
  • Meta 프로덕션 환경에서 최대 12% 초기 로드 개선, 특정 인터랙션 2.5배 개선이 확인되었습니다.
  • 기존 메모이제이션 코드와 공존 가능하며, 점진적으로 도입할 수 있습니다.

다음 장에서는 ref 개선, 메타데이터 지원, 리소스 로딩 API 등 React 19의 DX 개선사항들을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#react#nextjs#performance#frontend#typescript

관련 글

웹 개발

8장: ref 개선, 메타데이터, 리소스 로딩 API

React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.

2026년 2월 6일·16분
웹 개발

6장: Suspense 고급 패턴과 스트리밍 SSR

React 19에서 강화된 Suspense의 고급 패턴, 스트리밍 SSR, 중첩 Suspense 전략, 배칭 동작, Partial Pre-rendering을 다룹니다.

2026년 2월 2일·16분
웹 개발

9장: 성능 최적화 전략과 베스트 프랙티스

React 19 애플리케이션의 성능을 극대화하는 전략을 다룹니다. 번들 최적화, 렌더링 성능, Core Web Vitals 개선, 측정 도구 활용법을 배웁���다.

2026년 2월 8일·15분
이전 글6장: Suspense 고급 패턴과 스트리밍 SSR
다음 글8장: ref 개선, 메타데이터, 리소스 로딩 API

댓글

목차

약 15분 남음
  • 왜 React Compiler가 필요한가
    • 수동 메모이제이션의 문제
    • React Compiler의 해법
  • React Compiler의 동작 원리
    • 컴파일 파이프라인
    • 분석 수준
    • 변경 감지 (Mutation Tracking)
  • 설치와 설정
    • 기본 설치
    • Next.js 설정
    • Vite 설정
    • 호환성
  • ESLint 통합
    • Compiler가 감지하는 문제들
  • 점진적 도입 전략
    • 특정 디렉토리에만 적용
    • 기존 useMemo/useCallback과의 공존
  • 성능 영향
    • Meta의 프로덕션 결과
    • 최적화가 효과적인 경우
    • 주의할 점
  • React.memo와의 관계
  • 핵심 요약