React Compiler는 빌드 타임에 여러분의 React 애플리케이션을 자동으로 최적화해줘요. React는 최적화 없이도 보통 충분히 빠르지만, 때때로 앱의 반응성을 유지하기 위해 컴포넌트와 값들을 수동으로 메모이제이션해야 할 때가 있어요. 이런 수동 메모이제이션은 지루하고, 실수하기 쉽고, 유지보수해야 할 추가 코드를 만들어내죠. React Compiler는 이 최적화를 자동으로 해주기 때문에, 여러분은 이런 정신적 부담에서 벗어나서 기능 구현에 집중할 수 있어요.
컴파일러 없이는 리렌더링을 최적화하기 위해 컴포넌트와 값들을 수동으로 메모이제이션해야 해요:
// React Compiler 이전 예시
import { useMemo, useCallback, memo } from 'react';
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
const processedData = useMemo(() => {
return expensiveProcessing(data);
}, [data]);
const handleClick = useCallback((item) => {
onClick(item.id);
}, [onClick]);
return (
<div>
{processedData.map(item => (
<Item key={item.id} onClick={() => handleClick(item)} />
))}
</div>
);
});
이 수동 메모이제이션에는 메모이제이션을 깨뜨리는 미묘한 버그가 있어요:
<Item key={item.id} onClick={() => handleClick(item)} />
handleClick이 useCallback으로 감싸져 있더라도, 화살표 함수 () => handleClick(item)은 컴포넌트가 렌더링될 때마다 새로운 함수를 생성해요. 이건 Item이 항상 새로운 onClick prop을 받게 되어서 메모이제이션이 깨진다는 뜻이에요.
(쉽게 말해서, handleClick 자체는 메모이제이션했지만, 그걸 감싸는 화살표 함수는 매번 새로 만들어지기 때문에 결국 Item 입장에서는 매번 다른 함수를 받는 셈이 되는 거예요. 흔히 하는 실수 중 하나죠!)
React Compiler는 화살표 함수가 있든 없든 이걸 올바르게 최적화해서, Item이 props.onClick이 변경될 때만 리렌더링되도록 보장해요.
React Compiler를 사용하면, 수동 메모이제이션 없이 똑같은 코드를 작성할 수 있어요:
// React Compiler 이후 예시
function ExpensiveComponent({ data, onClick }) {
const processedData = expensiveProcessing(data);
const handleClick = (item) => {
onClick(item.id);
};
return (
<div>
{processedData.map(item => (
<Item key={item.id} onClick={() => handleClick(item)} />
))}
</div>
);
}
React Compiler Playground에서 이 예시 보기
React Compiler는 자동으로 최적의 메모이제이션을 적용해서, 여러분의 앱이 필요할 때만 리렌더링되도록 보장해요.
#### React Compiler는 어떤 종류의 메모이제이션을 추가하나요? {/*what-kind-of-memoization-does-react-compiler-add*/}React Compiler의 자동 메모이제이션은 주로 업데이트 성능 향상(기존 컴포넌트 리렌더링)에 초점을 맞추고 있어서, 다음 두 가지 사용 사례에 집중해요:
<Parent />를 리렌더링하면 <Parent />만 변경되었는데도 컴포넌트 트리의 많은 컴포넌트들이 리렌더링돼요expensivelyProcessAReallyLargeArrayOfObjects()를 호출하는 경우React는 현재 상태(더 구체적으로는: props, state, context)의 함수로 UI를 표현할 수 있게 해줘요. 현재 구현에서, 컴포넌트의 state가 변경되면 React는 해당 컴포넌트 그리고 모든 자식 컴포넌트들을 리렌더링해요 — useMemo(), useCallback(), 또는 React.memo()로 어떤 형태의 수동 메모이제이션을 적용하지 않는 한요. 예를 들어, 다음 예시에서 <MessageButton>은 <FriendList>의 state가 변경될 때마다 리렌더링돼요:
// FriendList.js
function FriendList({ friends }) {
const onlineCount = useFriendOnlineCount();
if (friends.length === 0) {
return <NoFriends />;
}
return (
<div>
<span>{onlineCount} online</span>
{friends.map((friend) => (
<FriendListCard key={friend.id} friend={friend} />
))}
<MessageButton />
</div>
);
}
React Compiler Playground에서 이 예시 보기
(여기서 문제는 onlineCount가 변경될 때마다 FriendList 전체가 리렌더링되고, 그러면 MessageButton도 같이 리렌더링된다는 거예요. MessageButton은 사실 onlineCount와 아무 관련이 없는데도요!)
React Compiler는 자동으로 수동 메모이제이션과 동등한 것을 적용해서, state가 변경될 때 앱의 관련된 부분만 리렌더링되도록 보장해요. 이건 때때로 "세밀한 반응성(fine-grained reactivity)"이라고 불려요. 위의 예시에서, React Compiler는 friends가 변경되더라도 <FriendListCard />의 반환값이 재사용될 수 있다고 판단하고, 이 JSX를 다시 생성하는 것을 피하며 count가 변경될 때 <MessageButton>을 리렌더링하는 것도 피할 수 있어요.
React Compiler는 렌더링 중에 사용되는 비용이 큰 계산도 자동으로 메모이제이션할 수 있어요:
// TableContainer.js
// 컴포넌트나 훅이 아니기 때문에 React Compiler에 의해 메모이제이션되지 **않아요**
function expensivelyProcessAReallyLargeArrayOfObjects() { /* ... */ }
// 컴포넌트이기 때문에 React Compiler에 의해 메모이제이션돼요
function TableContainer({ items }) {
// 이 함수 호출은 메모이제이션될 거예요:
const data = expensivelyProcessAReallyLargeArrayOfObjects(items);
// ...
}
React Compiler Playground에서 이 예시 보기
그런데 expensivelyProcessAReallyLargeArrayOfObjects가 정말로 비용이 큰 함수라면, React 외부에서 자체적인 메모이제이션을 구현하는 것을 고려해볼 수 있어요. 왜냐하면:
그래서 expensivelyProcessAReallyLargeArrayOfObjects가 여러 다른 컴포넌트에서 사용된다면, 완전히 동일한 items가 전달되더라도 그 비용이 큰 계산이 반복적으로 실행될 거예요. 코드를 더 복잡하게 만들기 전에 먼저 프로파일링을 해서 정말로 그렇게 비용이 큰지 확인하는 걸 추천해요.
모든 분들이 React Compiler를 사용하기 시작하길 권장해요. 컴파일러는 현재 React에서 여전히 선택적 추가 기능이지만, 미래에는 일부 기능이 완전히 작동하기 위해 컴파일러가 필요할 수 있어요.
React Compiler는 이제 안정적이고 프로덕션에서 광범위하게 테스트되었어요. Meta 같은 회사에서 프로덕션에 사용되고 있지만, 여러분의 앱에 컴파일러를 프로덕션에 적용하는 건 여러분 코드베이스의 상태와 React의 규칙들을 얼마나 잘 따랐는지에 따라 달라질 거예요.
React Compiler는 Babel, Vite, Metro, Rsbuild 같은 여러 빌드 도구에 설치할 수 있어요.
React Compiler는 주로 핵심 컴파일러를 감싸는 가벼운 Babel 플러그인이에요. 핵심 컴파일러는 Babel 자체와 분리되도록 설계되었어요. 컴파일러의 초기 안정 버전은 주로 Babel 플러그인으로 유지될 거지만, 우리는 swc 팀과 oxc 팀과 협력해서 React Compiler에 대한 일급 지원을 구축하고 있어요. 그래서 미래에는 빌드 파이프라인에 Babel을 다시 추가할 필요가 없을 거예요.
Next.js 사용자는 v15.3.1 이상을 사용해서 swc로 호출되는 React Compiler를 활성화할 수 있어요.
기본적으로, React Compiler는 자체 분석과 휴리스틱을 기반으로 여러분의 코드를 메모이제이션할 거예요. 대부분의 경우, 이 메모이제이션은 여러분이 직접 작성한 것만큼 정확하거나 더 정확할 거예요.
하지만 일부 경우에 개발자가 메모이제이션에 대해 더 많은 제어가 필요할 수 있어요. useMemo와 useCallback 훅은 React Compiler와 함께 어떤 값이 메모이제이션될지 제어하기 위한 탈출구로 계속 사용할 수 있어요. 이것의 일반적인 사용 사례는 메모이제이션된 값이 effect 의존성으로 사용될 때예요. 의존성이 의미 있게 변경되지 않았는데도 effect가 반복적으로 실행되지 않도록 보장하기 위해서요.
새로운 코드에서는 메모이제이션을 위해 컴파일러에 의존하고, 정밀한 제어가 필요한 곳에서 useMemo/useCallback을 사용하는 걸 추천해요.
기존 코드에서는 기존 메모이제이션을 그대로 두거나(제거하면 컴파일 출력이 변경될 수 있어요) 메모이제이션을 제거하기 전에 신중하게 테스트하는 걸 추천해요.
이 섹션은 React Compiler를 시작하고 프로젝트에서 효과적으로 사용하는 방법을 이해하는 데 도움이 될 거예요.
이 문서들 외에도, 컴파일러에 대한 추가 정보와 토론을 위해 React Compiler Working Group을 확인하는 걸 추천해요.