먼저, 원문은 어떤 글인가
원문 (번역본)은 리액트 컴파일러의 문법이나 내부 구조를 처음부터 가르치는 입문서라기보다, 공개 베타 이후 약 18개월 동안 리액트 생태계가 컴파일러를 어떻게 받아들였는지 회고하는 글이다.
핵심 메시지는 다음과 같다.
리액트 컴파일러의 가장 큰 가치는 벤치마크 점수를 극적으로 높이는 데 있지 않다. 개발자가 직접 관리하던 메모이제이션과 그로 인해 발생하던 버그를 컴파일러가 대신 처리한다는 데 있다.
원문은 크게 다음 흐름으로 진행된다.
- 리액트 컴파일러가 공개된 뒤 프레임워크에 통합된 과정
- 컴파일러가 실제로 자동화하는 것
- 컴파일러가 처리하지 못하거나 건너뛰는 코드
- 기존 프로젝트에 점진적으로 도입하는 방법
- 리액트 규칙을 강제하는 것에 대한 논쟁
- 앞으로 컴파일러가 확장될 가능성
특히 컴파일러를 단순한 성능 도구가 아니라, 리액트 프로그래밍 모델을 더 엄격한 계약으로 바꾸는 장치로 해석한다. (참고)
글을 읽고 공부 차 TIL을 남긴다.
들어가며
리액트 성능 최적화 코드를 보다 보면 반복해서 등장하는 API가 있다.
React.memo
useMemo
useCallback컴포넌트를 만들고 나면 이런 고민을 하게 된다.
- 이 컴포넌트에
memo를 붙여야 할까? - 이 객체는
useMemo로 감싸야 할까? - 자식에게 넘기는 함수에는
useCallback이 필요할까? - 의존성 배열에는 무엇을 넣어야 할까?
- 메모이제이션 비용이 오히려 더 크지는 않을까?
React Compiler는 이런 판단의 상당 부분을 개발자 대신 수행하는 빌드 타임 최적화 도구다.
한 문장으로 정리하면 다음과 같다.
React Compiler는 컴포넌트와 훅의 코드를 분석하여, 값·함수·JSX가 다시 계산될 필요가 없는 구간을 자동으로 찾아 메모이제이션하는 컴파일러다.
2025년 10월 React Compiler 1.0이 정식 공개되었고, 현재 공식 문서에서는 신규 애플리케이션뿐 아니라 기존 애플리케이션을 위한 점진적 도입 방법도 제공한다. (React)
1. React에서 렌더링은 무엇을 의미할까
React에서 컴포넌트가 렌더링된다는 것은 기본적으로 컴포넌트 함수가 다시 호출되는 것을 의미한다.
function Profile({ user }) {
const fullName = `${user.firstName} ${user.lastName}`;
return <div>{fullName}</div>;
}부모 컴포넌트가 다시 렌더링되면 Profile 함수도 다시 실행될 수 있다.
그러면 다음 작업도 다시 수행된다.
const fullName = `${user.firstName} ${user.lastName}`;그리고 JSX도 다시 생성된다.
<div>{fullName}</div>여기서 중요한 점은 다음과 같다.
컴포넌트 함수가 다시 실행되는 것과 실제 DOM이 변경되는 것은 다르다.
React는 새 렌더링 결과와 이전 결과를 비교하고, 실제로 달라진 부분만 DOM에 반영한다. 따라서 컴포넌트 함수가 다시 호출됐다고 해서 DOM 전체가 다시 만들어지는 것은 아니다.
하지만 컴포넌트 함수 내부에서 무거운 계산을 수행하거나, 수천 개의 자식 컴포넌트를 다시 평가하거나, 새로운 객체와 함수를 계속 생성한다면 DOM 변경 여부와 별개로 비용이 생길 수 있다.
2. 기존에는 개발자가 직접 메모이제이션했다
예를 들어 다음 컴포넌트가 있다고 하자.
function ProductList({ products, onSelect }) {
const visibleProducts = products.filter(
product => product.isVisible,
);
const handleSelect = productId => {
onSelect(productId);
};
return (
<List
products={visibleProducts}
onSelect={handleSelect}
/>
);
}ProductList가 렌더링될 때마다 다음 값이 새로 생성된다.
visibleProducts
handleSelectfilter()는 새로운 배열을 반환한다.
products.filter(...)함수 표현식 역시 새로운 함수 객체를 만든다.
productId => {
onSelect(productId);
}따라서 List가 React.memo로 감싸져 있어도 새로운 배열과 함수가 전달되면 props가 변경되었다고 판단할 수 있다.
이를 막기 위해 기존에는 다음과 같이 작성했다.
const List = React.memo(function List({
products,
onSelect,
}) {
// ...
});
function ProductList({ products, onSelect }) {
const visibleProducts = useMemo(
() =>
products.filter(
product => product.isVisible,
),
[products],
);
const handleSelect = useCallback(
productId => {
onSelect(productId);
},
[onSelect],
);
return (
<List
products={visibleProducts}
onSelect={handleSelect}
/>
);
}이제 products가 바뀌지 않으면 visibleProducts의 이전 참조를 재사용한다.
onSelect가 바뀌지 않으면 handleSelect도 이전 함수 참조를 재사용한다.
그 결과 List의 props가 동일하게 유지되고, List 렌더링을 건너뛸 수 있다.
문제는 이 코드가 꽤 번거롭다는 것이다.
3. 수동 메모이제이션이 헷갈리는 이유
의존성 배열을 정확히 작성해야 한다
const handleSelect = useCallback(() => {
onSelect(selectedId);
}, [onSelect]);이 코드에는 selectedId가 의존성 배열에서 빠져 있다.
함수는 처음 생성되었을 때의 selectedId를 계속 참조할 수 있다. 이른바 stale closure 문제가 발생한다.
올바른 코드는 다음과 같다.
const handleSelect = useCallback(() => {
onSelect(selectedId);
}, [onSelect, selectedId]);최적화가 깨질 수 있다
const handleSelect = useCallback(
item => onSelect(item.id),
[onSelect],
);
return items.map(item => (
<Item
key={item.id}
onClick={() => handleSelect(item)}
/>
));handleSelect는 안정화했지만 Item에 전달하는 화살표 함수는 렌더링마다 새로 생성된다.
() => handleSelect(item)따라서 Item이 memo로 감싸져 있어도 onClick은 계속 다른 참조가 된다.
공식 React Compiler 문서에서도 이런 코드를 수동 메모이제이션이 쉽게 실패하는 사례로 제시한다. (React)
메모이제이션 자체에도 비용이 있다
useMemo를 사용하면 React는 이전 의존성을 저장하고, 새 의존성과 비교하고, 캐시된 값을 관리해야 한다.
const doubled = useMemo(() => count * 2, [count]);count * 2처럼 매우 저렴한 계산이라면 직접 다시 계산하는 것이 더 단순할 수 있다.
따라서 기존에는 개발자가 매번 판단해야 했다. (다시 계산하는 비용 vs 의존성을 비교하고 캐시를 관리하는 비용)
이 판단을 모든 컴포넌트에서 사람이 정확히 수행하는 것은 꽤나 번거롭다.
4. React Compiler가 대신하는 일
React Compiler를 활성화하면 개발자는 보통 다음처럼 평범한 코드를 작성한다.
function ProductList({ products, onSelect }) {
const visibleProducts = products.filter(
product => product.isVisible,
);
const handleSelect = productId => {
onSelect(productId);
};
return (
<List
products={visibleProducts}
onSelect={handleSelect}
/>
);
}컴파일러는 이 코드를 분석한다.
visibleProducts는products에 의존한다.handleSelect는onSelect에 의존한다.List에 전달되는 JSX는 두 값에 의존한다.- 입력이 같다면 이전 결과를 재사용할 수 있다.
그리고 빌드 결과에 캐시 로직을 삽입한다.
개념적으로 표현하면 다음과 비슷하다.
function ProductList({ products, onSelect }) {
const cache = getMemoCache();
let visibleProducts;
if (cache.previousProducts !== products) {
visibleProducts = products.filter(
product => product.isVisible,
);
cache.previousProducts = products;
cache.visibleProducts = visibleProducts;
} else {
visibleProducts = cache.visibleProducts;
}
// handleSelect와 JSX도 비슷한 방식으로 재사용한다.
}실제 생성 코드는 이보다 훨씬 기계적이고 최적화되어 있다. 공식 설치 문서의 출력 예시에서는 react/compiler-runtime에서 캐시를 가져오고, 캐시 슬롯에 이전 JSX나 계산 결과를 저장하는 형태를 확인할 수 있다. (React)
핵심은 다음과 같다.
개발자가
useMemo를 작성하는 것이 아니라, 컴파일러가 코드의 데이터 흐름을 분석하여 안전한 캐시 경계를 만든다.
5. Babel이나 TypeScript와 무엇이 다를까
React Compiler도 코드를 다른 코드로 변환한다는 점에서는 일반적인 컴파일러와 비슷하다.
하지만 목적이 다르다.
TypeScript 컴파일러는 주로 타입을 검사하고 타입 문법을 제거한다.
function add(a: number, b: number): number {
return a + b;
}결과는 대략 다음과 같다.
function add(a, b) {
return a + b;
}Babel은 최신 자바스크립트 문법이나 JSX를 브라우저가 실행할 수 있는 코드로 바꾼다.
<div>Hello</div>예전 JSX 변환 방식에서는 대략 다음과 같이 바뀌었다.
React.createElement("div", null, "Hello");React Compiler의 주목적은 문법 호환성이 아니다.
TypeScript
타입 검사와 타입 문법 제거
Babel
문법 변환과 JSX 변환
React Compiler
React 코드의 의미를 분석하고 메모이제이션 삽입React Compiler는 컴포넌트와 훅이 어떤 값을 읽고, 어떤 값에 의존하며, 어느 계산 결과를 재사용할 수 있는지 분석한다.
즉, 일반적인 문법 변환기보다 React의 프로그래밍 모델을 더 깊게 이해한다.
6. 컴파일러는 무엇을 메모이제이션할까
React Compiler를 단순히 “자동으로 useMemo를 넣어주는 도구”라고만 이해하면 조금 부족하다.
컴파일러는 여러 종류의 값을 재사용할 수 있다.
계산 결과
const total = items.reduce(
(sum, item) => sum + item.price,
0,
);items가 바뀌지 않았다면 이전 total을 재사용할 수 있다.
객체와 배열
const style = {
color,
fontSize,
};color와 fontSize가 같다면 이전 객체를 재사용할 수 있다.
함수
const handleClick = () => {
onSelect(id);
};onSelect와 id가 같다면 이전 함수 참조를 재사용할 수 있다.
JSX
const header = (
<Header
title={title}
onClose={onClose}
/>
);의존하는 값이 같다면 이전 JSX 객체를 재사용할 수 있다.
컴포넌트의 일부 영역
사람이 useMemo를 작성하면 보통 비교적 큰 단위로 묶는다.
const summary = useMemo(
() => ({
label: formatLabel(entity),
total: calculateTotal(entity.items),
status:
entity.state === "active"
? "green"
: "red",
}),
[entity],
);여기서는 entity 객체 참조가 바뀌면 label, total, status를 모두 다시 계산한다.
하지만 실제 의존성은 더 세밀할 수 있다.
label → entity.name에 의존
total → entity.items에 의존
status → entity.state에 의존컴파일러는 사람이 작성한 큰 useMemo보다 더 작은 반응형 영역으로 코드를 나눌 수 있다.
이것이 자동 메모이제이션의 중요한 장점이다.
7. React Compiler는 무엇을 기준으로 안전성을 판단할까
컴파일러가 자동으로 값을 재사용하려면 다음 질문에 답할 수 있어야 한다.
- 입력이 같으면 출력도 같은가?
예를 들어 다음 함수는 입력이 같으면 결과도 같다.
function double(value) {
return value * 2;
}이런 함수를 순수 함수라고 부른다.
반면 다음 함수는 호출 시점에 따라 결과가 달라진다.
function getCurrentTime() {
return Date.now();
}또 다음 함수는 외부 값을 변경한다.
function updateUser(user) {
user.lastSeen = Date.now();
return user;
}React 컴포넌트의 렌더링은 기본적으로 순수해야 한다.
function Greeting({ name }) {
return <p>Hello, {name}</p>;
}같은 name을 넣으면 같은 UI를 반환해야 한다.
React Compiler는 이 성질을 이용한다.
컴포넌트가 React의 규칙을 따른다면, 입력이 바뀌지 않은 계산은 재사용할 수 있다.
따라서 React Compiler는 단순히 코드를 빠르게 만드는 도구가 아니다. React의 순수성 규칙을 기반으로 동작하는 최적화 도구다.
8. 왜 렌더링 중 변경이 문제가 될까
다음 코드를 보자.
function UserRow({ user }) {
user.lastSeen = Date.now();
return <span>{user.name}</span>;
}컴포넌트가 렌더링되는 동안 props로 받은 객체를 변경하고 있다.
이 코드는 같은 user를 전달하더라도 호출할 때마다 외부 상태를 바꾼다.
첫 번째 렌더링
user.lastSeen = 10:00:00
두 번째 렌더링
user.lastSeen = 10:00:01컴파일러가 이 렌더링을 건너뛰면 lastSeen 변경도 발생하지 않는다.
즉, 렌더링을 생략하는 것이 프로그램의 동작을 바꿔버린다.
그래서 React에서는 렌더링 중 props나 state를 직접 변경해서는 안 된다.
function UserRow({ user }) {
const displayName = user.name;
return <span>{displayName}</span>;
}변경이 필요하다면 이벤트나 이펙트 같은 명시적인 경계에서 수행해야 한다.
9. 왜 렌더링 중 ref를 읽는 것이 문제가 될까
다음 코드를 보자.
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const width = ref.current?.offsetWidth;
return <div ref={ref}>{width}px</div>;
}ref.current는 React의 렌더링 데이터 흐름에 포함되지 않는다.
state나 props가 바뀌지 않아도 DOM 크기는 달라질 수 있다.
- 창 크기 변경
- 폰트 로딩
- 부모 레이아웃 변경
- CSS 변경
그러나 컴파일러는 ref.current.offsetWidth가 언제 바뀌는지 알 수 없다.
따라서 이런 DOM 측정은 보통 useLayoutEffect 등으로 옮겨야 한다.
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
if (!ref.current) {
return;
}
setWidth(ref.current.offsetWidth);
}, []);
return <div ref={ref}>{width}px</div>;
}핵심은 ref가 나쁘다는 것이 아니다.
렌더링 결과를 결정하는 반응형 데이터와, React 바깥의 가변 값을 구분해야 한다.
10. 컴파일러가 규칙 위반을 발견하면 어떻게 될까
React Compiler는 잘못된 코드를 무조건 억지로 변환하지 않는다.
공식 디버깅 문서가 설명하는 기본적인 방향은 다음과 같다.
- 안전하게 컴파일할 수 있는 함수는 최적화한다.
- 분석할 수 없거나 안전성을 증명할 수 없는 함수는 건너뛸 수 있다.
- ESLint 규칙으로 문제를 먼저 발견할 수 있다.
- 필요한 경우 특정 함수를 명시적으로 제외할 수 있다. (React)
이 특성은 점진적 도입에 중요하다.
코드베이스 전체가 완벽해야만 컴파일러를 켤 수 있는 것이 아니다.
컴파일 가능한 컴포넌트
→ 최적화
컴파일하기 어려운 컴포넌트
→ 기존 방식으로 실행다만 조용히 건너뛴 컴포넌트가 많아지면 “컴파일러를 활성화했지만 실제로는 별로 적용되지 않은” 상태가 될 수 있다.
그래서 ESLint 설정이 중요하다.
11. ESLint는 컴파일러의 사전 검사기 역할을 한다
React Compiler 1.0과 함께 컴파일러 관련 린트 규칙은 eslint-plugin-react-hooks에 통합되었다. 공식 발표에 따르면 컴파일러 기반 규칙은 플러그인의 recommended 및 recommended-latest 프리셋으로 제공된다. (React)
이 규칙들은 다음과 같은 코드를 잡아낸다.
- 렌더링 중 props나 state 변경
- 렌더링 중 ref 접근
- 훅 규칙 위반
- 잘못된 effect 의존성
- 컴파일러와 호환되지 않는 라이브러리 사용
- 컴파일러가 지원하지 못하는 문법
- 메모이제이션을 깨뜨리는 코드
여기서 중요한 변화가 있다.
예전에는 Rules of React가 주로 문서와 코드 리뷰를 통해 지켜야 하는 규칙처럼 느껴졌다.
React Compiler 환경에서는 이 규칙들이 다음 의미를 가진다.
규칙을 지킨다
→ 컴파일러가 코드의 의미를 신뢰하고 최적화할 수 있다.
규칙을 어긴다
→ 컴파일러가 안전성을 증명하지 못하고 최적화를 건너뛸 수 있다.React 규칙이 단순한 스타일 가이드에서 컴파일 가능한 코드의 계약으로 바뀌는 셈이다.
12. "use memo"와 "use no memo"
React Compiler에는 함수 단위로 동작을 제어하는 지시어가 있다.
"use memo"
function ExpensiveChart({ data }) {
"use memo";
return <Chart data={data} />;
}"use memo"는 이 함수를 컴파일하도록 명시한다.
대부분의 프로젝트가 사용하는 기본 infer 모드에서는 컴파일러가 컴포넌트와 훅을 스스로 찾기 때문에 일반적으로 필요하지 않다.
주로 annotation 모드에서 일부 함수만 점진적으로 컴파일할 때 사용한다. (React)
"use no memo"
function LegacyEditor() {
"use no memo";
// 레거시 구현
}이 지시어는 해당 함수를 컴파일 대상에서 제외한다.
공식 문서는 이것을 영구적인 설계 수단보다는 임시 디버깅 도구로 사용할 것을 권장한다. (React)
컴파일러를 켠 뒤 특정 컴포넌트에서 문제가 발생했을 때 다음과 같이 원인을 분리할 수 있다.
function ProblematicComponent() {
"use no memo";
// 이 상태에서 문제가 사라지는지 확인한다.
}문제가 사라진다면 다음 가능성을 조사한다.
- 컴포넌트가 React 규칙을 위반하고 있는가
- 컴파일러 버그인가
- 사용하는 라이브러리가 참조 동일성에 의존하는가
- 자동 메모이제이션과 맞지 않는 외부 상태가 있는가
하지만 코드베이스에 "use no memo"가 계속 쌓이면 해당 영역은 컴파일러의 이점을 얻지 못한다.
따라서 다음처럼 이유를 남기는 편이 좋다.
function LegacyEditor() {
"use no memo";
// TODO: mutable selection model을 제거한 뒤 다시 활성화
}13. 컴파일 모드
React Compiler는 여러 컴파일 모드를 제공한다.
infer
기본 모드다.
컴파일러가 이름과 코드 형태를 보고 컴포넌트와 훅을 추론한다.
{
compilationMode: "infer",
}일반적인 React 명명 규칙을 따라야 한다.
function Button() {
return <button>Click</button>;
}
function useCounter() {
// ...
}컴포넌트는 PascalCase, 훅은 use 접두사를 사용하는 규칙이 컴파일러의 추론에도 영향을 준다. (it.react.dev)
annotation
"use memo"가 붙은 함수만 컴파일한다.
{
compilationMode: "annotation",
}기존 프로젝트에서 아주 작은 범위부터 시작할 때 유용하다.
function NewProductCard() {
"use memo";
// 이 함수만 우선 적용
}all
가능한 모든 함수를 컴파일 대상으로 본다.
{
compilationMode: "all",
}컴포넌트가 아닌 일반 유틸리티 함수까지 컴파일될 수 있어, 공식 문서는 성능에 부정적인 영향을 줄 가능성을 주의하라고 설명한다. (it.react.dev)
일반적인 애플리케이션에서는 infer가 기본 선택이다.
14. React Compiler가 해결하는 성능 문제
React Compiler가 주로 해결하는 것은 업데이트 과정에서 발생하는 불필요한 재계산과 리렌더링이다.
예를 들어 검색 입력창과 긴 목록이 있다고 하자.
function SearchPage({ items }) {
const [query, setQuery] = useState("");
const handleSelect = item => {
// ...
};
return (
<>
<input
value={query}
onChange={event =>
setQuery(event.target.value)
}
/>
<ItemList
items={items}
onSelect={handleSelect}
/>
</>
);
}사용자가 글자를 입력할 때마다 SearchPage가 다시 렌더링된다.
컴파일러가 없다면 handleSelect도 매번 새로운 함수가 된다.
그 결과 ItemList와 그 아래 수많은 행이 다시 평가될 수 있다.
컴파일러는 handleSelect가 실제로 의존하는 값이 바뀌지 않았다면 이전 함수와 JSX를 재사용할 수 있다.
따라서 입력창 상태 변경이 목록 전체의 불필요한 작업으로 번지는 것을 막을 수 있다.
원문 역시 컴파일러의 현실적인 장점을 극적인 벤치마크보다 다음과 같은 변화에서 찾는다.
- 불필요한 리렌더링 버그 감소
- 메모이제이션 누락 감소
- 의존성 배열 실수 감소
- 성능 코드 리뷰 부담 감소
- 수동 최적화 코드 증가 억제 (참고)
15. React Compiler가 해결하지 않는 것
React Compiler를 켠다고 모든 React 애플리케이션이 자동으로 빨라지는 것은 아니다.
컴파일러는 주로 렌더링 계산과 참조 안정성을 최적화한다.
다음 문제는 별도로 해결해야 한다.
네트워크 워터폴
사용자 요청
→ 사용자 API 완료
→ 프로젝트 API 시작
→ 권한 API 시작요청이 직렬로 이어지는 문제는 자동 메모이제이션으로 해결되지 않는다.
지나치게 큰 번들
초기 JavaScript가 5MB라면 다운로드·파싱·실행 비용은 그대로 존재한다.
코드 스플리팅이나 의존성 정리가 필요하다.
과도한 데이터 요청
같은 API를 여러 번 호출하거나 필요 이상의 데이터를 가져오는 문제는 데이터 계층에서 해결해야 한다.
무거운 초기 하이드레이션
서버가 생성한 HTML에 이벤트와 상태를 연결하는 비용은 리렌더링 메모이제이션과 다른 문제다.
DOM 자체가 지나치게 많은 경우
10만 개 행을 실제 DOM에 전부 렌더링한다면, 재렌더링을 줄이는 것만으로는 충분하지 않을 수 있다.
가상화가 필요할 수 있다.
무거운 이벤트 핸들러
function handlePointerMove(event) {
performVeryExpensiveCalculation(event);
}이벤트가 발생할 때마다 실행되는 계산은 컴포넌트 렌더링을 캐시한다고 사라지지 않는다.
원문도 컴파일러가 네트워크 요청, 번들 크기, 초기 JavaScript 실행 같은 병목까지 해결하지는 않는다고 강조한다. (원글 번역본)
결국 성능 최적화의 출발점은 여전히 같다.
먼저 측정하고, 실제 병목을 찾는다.
16. 컴파일러를 쓰면 렌더링 자체가 사라지는가
정확히는 아니다.
React Compiler는 모든 렌더링을 제거하지 않는다.
입력이 바뀌면 필요한 계산은 다시 수행해야 한다.
function Price({ amount, currency }) {
const formatted = formatCurrency(
amount,
currency,
);
return <span>{formatted}</span>;
}amount가 바뀌면 formatted는 다시 계산해야 한다.
currency가 바뀌어도 다시 계산해야 한다.
컴파일러가 하는 일은 다음과 같다.
필요한 업데이트
→ 그대로 수행
입력이 같은 계산
→ 이전 결과 재사용
변경과 무관한 JSX
→ 이전 결과 재사용따라서 “렌더링을 없애는 도구”보다는 변경의 영향을 필요한 범위로 제한하는 도구에 가깝다.
17. 컴파일러와 Virtual DOM은 어떤 관계인가
React Compiler가 등장했다고 Virtual DOM이나 reconciliation이 사라지는 것은 아니다.
둘은 서로 다른 단계에서 동작한다.
React Compiler
컴포넌트 함수가 어떤 값을 다시 계산할지 줄인다.
React reconciliation
새 React 트리와 이전 트리를 비교한다.
React DOM
실제 DOM에 필요한 변경을 반영한다.컴파일러가 JSX 결과를 재사용하면 reconciliation이 처리해야 하는 작업 역시 줄어들 수 있다.
하지만 컴파일러가 React의 런타임 전체를 대체하는 것은 아니다.
빌드 타임 최적화
↓
더 적은 계산과 안정적인 참조
↓
React 런타임
↓
트리 비교
↓
필요한 DOM 변경18. 컴파일러는 컴포넌트를 React.memo로 감싸는 것과 같은가
비슷한 효과가 있지만 완전히 같지는 않다.
React.memo는 주로 컴포넌트 경계에서 props를 비교한다.
const UserCard = React.memo(
function UserCard({ user }) {
return <div>{user.name}</div>;
},
);부모가 UserCard를 렌더링하려 해도 props가 같으면 컴포넌트 실행을 건너뛸 수 있다.
반면 React Compiler는 컴포넌트 내부의 계산과 JSX까지 분석할 수 있다.
function Dashboard({ user, items, theme }) {
const total = calculateTotal(items);
const userName = formatUserName(user);
return (
<Panel theme={theme}>
<Header name={userName} />
<Total value={total} />
</Panel>
);
}컴파일러는 개념적으로 의존성을 나눌 수 있다.
userName → user
total → items
Panel → theme
Header → userName
Total → total즉, 단순히 컴포넌트 바깥에 memo 하나를 붙이는 것보다 더 세밀한 최적화가 가능하다.
19. 기존 useMemo와 useCallback은 바로 지워야 할까
컴파일러를 켜자마자 모든 수동 메모이제이션을 삭제하는 것은 좋지 않다.
안전한 순서는 대략 다음과 같다.
1. 컴파일러를 설치한다
프레임워크별 통합 설정을 적용한다.
React Compiler는 React 19에서 가장 자연스럽게 사용할 수 있지만, 공식 문서상 React 17과 18도 지원한다. (React)
2. ESLint 규칙을 먼저 활성화한다
컴파일러가 분석하기 어려운 코드를 찾는다.
3. 작은 범위부터 적용한다
기존 코드베이스라면 annotation 모드나 디렉터리 단위 설정으로 시작할 수 있다.
4. 테스트를 실행한다
- UI 테스트
- 상태 변경
- 폼 입력
- 드래그 앤 드롭
- 외부 상태 관리
- ref 기반 코드
- 서드파티 라이브러리 연동
5. DevTools로 적용 여부를 확인한다
공식 문서에 따르면 컴파일러가 최적화한 컴포넌트에는 React DevTools에서 Memo ✨ 표시가 나타난다. (React)
6. 프로파일링한다
컴파일러 적용 전후의 실제 상호작용을 측정한다.
7. 불필요한 수동 메모이제이션을 점진적으로 제거한다
useMemo
useCallback
React.memo한 번에 거대한 삭제 PR을 만드는 것보다, 기능 단위로 검증하며 제거하는 편이 안전하다.
20. 컴파일러와 라이브러리의 호환성
애플리케이션 코드가 React 규칙을 잘 지켜도 사용하는 라이브러리가 문제를 일으킬 수 있다.
특히 다음 유형은 확인이 필요하다.
- 렌더링 중 외부 객체를 변경하는 라이브러리
- 내부적으로 ref 값을 렌더링에 사용하는 라이브러리
- 프록시나 getter의 부수 효과에 의존하는 상태 관리
- 함수나 객체가 매번 새로 생성된다는 사실에 의존하는 코드
- 비표준 훅 구현
- 오래된 클래스 컴포넌트 중심 라이브러리
원문은 2025년 말부터 생태계의 관심이 “컴파일러를 도입할까?”에서 “호환되지 않는 레거시 라이브러리를 어떻게 다룰까?”로 이동했다고 설명한다. (원글 번역본)
이것을 컴파일러가 기존 코드를 망가뜨린다고만 보기는 어렵다.
컴파일러가 없을 때도 규칙 위반은 존재했다.
다만 이전에는 우연히 동작하거나, 성능 문제로만 나타나거나, 특정 렌더링 모드에서만 드러났을 수 있다.
컴파일러는 이런 암묵적인 가정을 더 명확하게 드러낸다.
21. 클래스 컴포넌트는 어떻게 될까
React Compiler는 함수 컴포넌트와 훅을 중심으로 설계되었다.
원문에서도 레거시 클래스 컴포넌트는 컴파일 대상이 아니며 기존 방식대로 동작한다고 설명한다. (원글 번역본)
class Counter extends React.Component {
render() {
return <div>{this.props.count}</div>;
}
}컴파일러를 활성화한다고 클래스 컴포넌트가 자동으로 함수 컴포넌트로 변환되는 것은 아니다.
따라서 오래된 코드베이스에서는 다음 상태가 공존할 수 있다.
함수 컴포넌트
→ 컴파일러 최적화 가능
클래스 컴포넌트
→ 기존 React 런타임 방식 유지React Compiler 도입과 클래스 컴포넌트 마이그레이션은 별도의 프로젝트로 보는 편이 현실적이다.
22. React Compiler가 바꾸는 코드 리뷰
컴파일러 이전에는 코드 리뷰에서 다음과 같은 대화가 자주 등장했다.
이 함수 useCallback으로 감싸야 하지 않을까요?
여기 dependency에 value가 빠졌습니다.
자식이 memo인데 객체를 매번 새로 만들고 있습니다.
이 useMemo는 계산이 너무 가벼워서 필요 없어 보입니다.컴파일러가 안정적으로 적용되는 코드베이스에서는 이런 판단의 많은 부분을 도구에 맡길 수 있다.
대신 리뷰의 초점이 바뀐다.
이 렌더링은 순수한가?
props나 외부 상태를 변경하고 있지 않은가?
이 값은 state여야 하는가, 렌더링 중 계산할 수 있는가?
effect가 정말 필요한가?
이 컴포넌트의 데이터 흐름이 명확한가?
서드파티 라이브러리가 React의 규칙을 지키는가?즉, 성능 트릭을 검토하는 시간이 줄고 코드의 의미와 데이터 흐름에 더 집중할 수 있다.
23. 컴파일러가 React 코딩 스타일에 미치는 영향
컴파일러 시대의 React 코드는 오히려 더 평범해질 가능성이 크다.
이전
const filteredItems = useMemo(
() =>
items.filter(item =>
item.name.includes(query),
),
[items, query],
);
const handleSelect = useCallback(
itemId => {
onSelect(itemId);
},
[onSelect],
);
const ListItem = memo(function ListItem({
item,
onSelect,
}) {
// ...
});컴파일러 적용 후
const filteredItems = items.filter(item =>
item.name.includes(query),
);
const handleSelect = itemId => {
onSelect(itemId);
};
function ListItem({ item, onSelect }) {
// ...
}개발자는 코드의 의미를 직접 표현한다.
컴파일러는 그 코드에서 안정적인 값을 찾아낸다.
이 방향은 최적화가 사라졌다는 의미가 아니다.
최적화를 소스 코드에서 사람이 직접 표현하던 방식에서, 컴파일러가 파생하는 방식으로 이동한 것이다.
24. 그래도 React의 동작 원리를 공부해야 할까
React Compiler가 자동으로 최적화해 주더라도 다음 개념은 여전히 중요하다.
- 렌더링과 커밋의 차이
- state 업데이트
- props와 참조 동일성
- closure
- effect의 실행 시점
- ref의 역할
- reconciliation
- key
- batching
- concurrent rendering
- 순수 함수
오히려 컴파일러를 정확히 이해하려면 이 개념들이 필요하다.
예를 들어 컴파일러가 왜 다음 코드를 싫어하는지 이해하려면 순수성을 알아야 한다.
props.user.name = "Changed";왜 다음 값을 추적하기 어려운지 이해하려면 ref를 알아야 한다.
ref.current.offsetWidth왜 함수 참조를 캐시하는지 이해하려면 참조 동일성과 memo를 알아야 한다.
컴파일러는 React 지식을 불필요하게 만드는 도구가 아니다.
반복적인 최적화 구현을 자동화하는 도구다.
25. React Compiler의 진짜 의미
React Compiler를 단순히 useMemo 자동 생성기로만 보면 변화의 절반만 보는 셈이다.
컴파일러가 동작하려면 React 코드가 분석 가능해야 한다.
분석 가능하려면 렌더링이 순수해야 한다.
같은 props와 state
→ 같은 렌더링 결과외부 값을 몰래 변경해서는 안 된다.
React가 추적할 수 없는 값을 렌더링의 근거로 사용해서도 안 된다.
훅은 일관된 순서로 호출되어야 한다.
이 규칙들은 원래부터 React에 존재했다.
하지만 컴파일러 이전에는 종종 “권장되는 좋은 습관”처럼 느껴졌다.
컴파일러가 등장하면서 규칙의 의미가 바뀐다.
이전
규칙을 지키면 버그가 적고 이해하기 좋은 코드가 된다.
이후
규칙을 지켜야 컴파일러가 코드의 의미를 증명하고 최적화할 수 있다.Rules of React가 스타일 가이드에서 정적 분석 가능한 프로그래밍 계약으로 가까워지는 것이다.
26. 정리
React Compiler는 빌드 시점에 React 코드를 분석한다.
입력값이 무엇인지
어떤 계산이 어떤 값에 의존하는지
어떤 함수와 객체를 재사용할 수 있는지
어떤 JSX를 다시 만들 필요가 없는지그리고 결과 코드에 자동 메모이제이션 로직을 삽입한다.
덕분에 개발자는 대부분의 경우 다음 코드를 직접 관리하지 않아도 된다.
useMemo
useCallback
React.memo하지만 컴파일러가 모든 성능 문제를 해결하는 것은 아니다.
네트워크 워터폴
큰 번들
과도한 데이터 요청
무거운 초기 실행
지나치게 많은 DOM
비효율적인 알고리즘이런 문제는 여전히 별도로 측정하고 해결해야 한다.
React Compiler가 가져오는 가장 큰 변화는 단순히 애플리케이션이 조금 더 빨라지는 데 있지 않다.
개발자가 직접 작성하던 메모이제이션 코드와 그 판단을 컴파일러로 옮긴다는 데 있다.
그리고 그 과정에서 React의 규칙은 더 이상 느슨한 관례에 머물지 않는다.
순수하고 분석 가능한 React 코드를 작성하면, 컴파일러가 최적화를 맡는다.
React Compiler는 React를 완전히 다른 프레임워크로 만드는 기술이 아니다.
우리가 원래 작성해야 했던 올바른 React 코드를 컴파일러가 이해하고, 그 위에 최적화를 자동으로 얹는 기술이다.