원문: The React Compiler at Eighteen Months: The Arc, the Debates, and What's Next

리액트 컴파일러는 2024년 말, 리액트 19와 함께 공개 베타로 출시되었으며 2025년 10월에 1.0 버전에 도달했습니다. 공개 베타가 시작된 지 18개월이 지난 지금, 생태계는 예상할 수 있었던 단계를 차례로 거쳐 왔습니다. 프레임워크 통합, 도구의 성숙, 그리고 커뮤니티의 논쟁이 그것입니다. 이 글에서는 지난 흐름을 되돌아보고, 리액트 팀이 앞으로 어떤 방향을 제시하고 있는지 살펴보겠습니다.
리액트 19는 2024년 말에 출시되었으며, 리액트 컴파일러는 이와 함께 공개 베타로 제공되었습니다. 이 컴파일러는 이미 Meta의 프로덕션 환경에서 사용되고 있었고, 리액트 팀 역시 명시적으로 도입을 권장했습니다. 이후 2025년 10월에는 마침내 1.0 버전이 릴리스되었습니다. 공개 베타가 시작된 지 18개월이 지난 지금, 생태계는 대규모 플랫폼 수준의 변화에서 흔히 볼 수 있는 단계를 차례로 거쳐 왔습니다. 처음의 발표, 프레임워크 통합, 도구의 성숙, 그리고 그 기술이 실제로 무엇을 의미하는지를 둘러싼 느리고 복잡한 논의가 이어졌습니다.
이 글은 이러한 흐름을 되돌아보고, 리액트 팀이 앞으로 어떤 방향을 제시하고 있는지 살펴보는 회고입니다. 이 글의 목적은 개념을 처음부터 설명하는 입문서가 아닙니다. 다만 참고를 위해 필요한 설명은 함께 포함되어 있습니다. 저는 아직 대규모 프로덕션 환경에서 직접 마이그레이션을 수행해본 경험은 없습니다. 따라서 이 글의 내용은 공개 문서, RFC, 리액트 팀의 발표 자료, 그리고 글 마지막에 링크한 초기 도입 사례들을 바탕으로 정리한 것입니다.
컴파일러가 남길 가장 큰 유산은 벤치마크 수치가 아닐 것입니다. 그것은 특정 종류의 버그를 더 이상 걱정하지 않아도 되게 만들었다는 점입니다. “useCallback의 의존성 배열에 무언가를 빠뜨렸습니다.” 이제 이런 대화는 더 이상 필요하지 않게 되었습니다.
18개월의 흐름
2024년 10월 공개 베타 발표 이후, 리액트 컴파일러의 여정은 비교적 예측 가능한 흐름을 따라왔습니다. 출시, 통합, 도구의 성숙, 그리고 논쟁입니다.
출시와 초기 통합
리액트 19가 출시된 이후 몇 달 동안, 컴파일러를 실제로 사용하는 데 있어 가장 중요한 부분은 설정(configuration)이었습니다. Next.js, Expo, TanStack Start, 그리고 Vite 기반 프레임워크들은 각각 자신의 빌드 파이프라인에 컴파일러를 통합했습니다. 새로운 애플리케이션에서는 “기본적으로 활성화되어 있으며, 필요하면 비활성화할 수 있다”는 것이 자연스러운 기본값이 되었습니다. 기존 애플리케이션에서는 ESLint 플러그인이 도입 준비 상태를 판단하는 주요 지표 역할을 하며 점진적인 마이그레이션 경로가 마련되었습니다.
조용했던 중간 단계
이 흐름의 중간 단계는 초기의 큰 기대와 비교하면 상당히 조용했습니다. 초기에 도입한 팀들도 화려한 홍보보다는 신중한 태도로 접근했습니다. 그리고 그들이 보고한 성과 역시 다소 평범한 종류의 것이었습니다. 코드 리뷰 과정에서 발견되는 불필요한 리렌더링 버그가 줄어들었고, “왜 이 부분이 느린가?”와 같은 성능 관련 버그가 감소했으며, 코드베이스에 새로운 메모이제이션 코드가 계속 쌓이는 현상이 멈췄습니다. 커뮤니티가 예상했던 극적인 벤치마크 글들이 크게 등장하지 않은 이유도 여기에 있습니다. 컴파일러의 가장 큰 가치는 인상적인 숫자를 보여주는 것이 아니라, 애초에 발생할 수 있는 버그를 예방하는 데 있기 때문입니다.
생태계의 현실 점검
2025년 말이 되자 논의의 초점은 “도입해야 할까?”에서 “컴파일러와 호환되지 않는 라이브러리를 어떻게 처리해야 할까?”로 옮겨갔습니다. 그리고 지금도 많은 팀들이 바로 이 지점에서 고민하고 있습니다. 리액트의 규칙(Rules of React)은 원래부터 존재하던 규칙이었지만, 이제는 빌드 시점에 강제로 검증할 수 있게 되었습니다. 그 결과, 생태계의 적지 않은 부분이 그 규칙을 조용히 위반하고 있었다는 사실이 드러났습니다. 오래된 상태 관리 라이브러리, 레거시 폼 라이브러리, 일부 드래그 앤 드롭 구현체, 그리고 널리 사랑받던 몇몇 유틸리티 훅들이 여기에 해당했습니다. 컴파일러가 이들을 망가뜨린 것이 아닙니다. 애초부터 문제가 있었고, 컴파일러는 그 사실을 드러냈을 뿐입니다.
현재 상황을 한 문장으로 요약하면 그린필드 프로젝트는 이미 해결되었습니다. 브라운필드 프로젝트는 여전히 하나의 프로젝트입니다.
컴파일러가 하는 일
간단히, 참고를 위해 정리해 보겠습니다. 컴파일러는 빌드 타임 변환입니다. 컴포넌트를 읽고, 해당 코드가 리액트의 규칙을 따른다고 가정한 뒤, 도움이 되는 위치에 메모이제이션을 자동으로 삽입합니다. 컴파일러 자체로 인한 런타임 비용은 없습니다. 최종 결과물은 일반적인 리액트 코드일 뿐입니다. 또한 그 세분화 수준은 사람이 직접 작성하는 것보다 더 뛰어납니다. 개발자가 직접 작성한 useMemo는 보통 전체 파생 객체를 한 번에 메모이제이션하지만, 컴파일러는 그 내부의 개별 표현식 단위로 메모이제이션할 수 있습니다. 따라서 객체의 한 필드가 변경되더라도 나머지 부분은 무효화되지 않습니다.
실제로 다음과 같은 코드는
const DashboardRow = memo(({ entity, onSelect }: Props) => {
const formatted = useMemo(
() => ({
label: formatLabel(entity),
total: entity.items.reduce((sum, i) => sum + i.value, 0),
status: entity.state === "active" ? "green" : "red",
}),
[entity],
);
const handleClick = useCallback(() => {
onSelect(entity.id);
}, [entity.id, onSelect]);
return (
<Row onClick={handleClick}>
<Label color={formatted.status}>{formatted.label}</Label>
<Total>{formatted.total}</Total>
</Row>
);
});
컴파일러가 활성화된 코드베이스에서는 다음과 같은 형태로 변환됩니다.
function DashboardRow({ entity, onSelect }: Props) {
const label = formatLabel(entity);
const total = entity.items.reduce((sum, i) => sum + i.value, 0);
const status = entity.state === "active" ? "green" : "red";
return (
<Row onClick={() => onSelect(entity.id)}>
<Label color={status}>{label}</Label>
<Total>{total}</Total>
</Row>
);
}
memo 래퍼, useMemo, useCallback은 사라집니다. 실제 사용자 인터랙션에서 가장 눈에 띄는 변화는 리렌더링 비용이 증가하는 방식 자체가 달라진다는 점입니다. 대표적인 실패 사례는 리스트 렌더링입니다. 부모 컴포넌트의 useCallback에 잘못된 의존성이 하나라도 있으면, 입력할 때마다 모든 행이 다시 렌더링됩니다. 이 경우 비용은 리스트 크기에 비례해 증가하고, 결국 입력 지연이 발생하게 됩니다. 컴파일러는 이러한 구조를 제거합니다. props가 변경되지 않은 행은 리스트의 크기와 관계없이 그대로 유지됩니다.
균형 있게 이야기하기 위해 비용도 함께 살펴보겠습니다. 공개 벤치마크 기준으로 빌드 시간은 수십 퍼센트 정도 증가할 수 있습니다. 다만 증분 빌드(incremental build)는 대부분 큰 영향을 받지 않습니다. 번들 크기 역시 인라인된 메모이제이션 헬퍼 코드 때문에 한 자릿수 초반 수준으로 소폭 증가할 수 있습니다. 물론 이 수치는 프로젝트마다 다르므로, 직접 측정하기 전까지는 어떤 구체적인 숫자도 그대로 신뢰해서는 안 됩니다.
컴파일러는 리렌더링 문제를 해결합니다. 하지만 네트워크 워터폴, 과도한 데이터 요청, 번들 크기, 혹은 느린 초기 자바스크립트 실행과 같은 문제까지 해결해주지는 않습니다. 애플리케이션이 느리다고 느껴진다면, 무엇보다 먼저 측정부터 해야 합니다. 컴파일러를 도입하고 나면 실제 병목이 전혀 다른 곳에 있었다는 사실이 드러나는 경우도 많습니다.
컴파일러가 적용되지 않는 경우
18개월에 걸친 프로덕션 사용을 통해, 컴파일러가 처리하지 않는 패턴들이 보다 명확해졌습니다. 아래 네 가지 패턴은 제가 읽어본 거의 모든 마이그레이션 후기에서 공통적으로 등장했습니다.
1. 렌더링 중 props나 클로저를 변경하는 경우
function Row({ entity }: Props) {
entity.lastSeen = Date.now(); // 렌더링중에 변경 됨
return <span>{entity.name}</span>;
}
컴파일러는 이런 코드를 변환하지 않습니다. 코드를 수정해야 합니다.
2. 렌더링 중 ref를 읽는 경우
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const width = ref.current?.offsetWidth; // 렌더링중에 ref 값을 읽음
return <div ref={ref}>{width}px</div>;
}
ref를 읽는 로직은 이펙트나 useLayoutEffect 안으로 옮겨야 합니다. 컴파일러는 이 문제를 표시하지만, 코드를 대신 다시 작성해주지는 않습니다.
3. 레거시 클래스 컴포넌트
클래스 컴포넌트는 전혀 컴파일되지 않습니다. 아직 React.Component를 상속한 하위 클래스가 있다면, 해당 컴포넌트는 지금까지와 동일하게 동작합니다. 새로운 프로젝트에서는 거의 문제가 되지 않지만, 오래된 코드베이스에서는 알아둘 필요가 있습니다.
4. 그 외에는 문제가 없어 보이는 컴포넌트 안의 미지원 문법
이것은 사람들이 예상하지 못해 놀라는 실패 모드입니다. 컴파일러는 추론할 수 없는 개별 문법 패턴을 만나면 해당 부분에서 빠져나옵니다. 그리고 적절한 린트 규칙을 활성화하지 않았다면 이 동작은 조용히 일어납니다. 가장 자주 등장하는 패턴은 다음과 같습니다.
// 구조 분해한 'props'를 다시 할당하는 경우
function Field({ value }: Props) {
value = value ?? defaultValue; // 컴파일러는 이 함수를 건너뜁니다.
return <input defaultValue={value} />;
}
// 'try/catch' 내부에서 조건문, 옵셔널 체이닝, 또는 null 병합 연산자를 사용하는 경우
async function load(url: string) {
try {
const res = await fetch(url);
const data = ((await res.json()) as Data | undefined) ?? {};
if (data.ok) setResult(data); // 이 부분 역시 컴파일 대상에서 제외됩니다.
} catch (e) {
logError(e);
}
}
두 코드 모두 일반적인 리액트 코드로 컴파일됩니다. 어느 쪽도 런타임 오류를 일으키지는 않습니다. 문제는 주변 컴포넌트가 조용히 메모이제이션 대상에서 제외된다는 점입니다. 그리고 대부분의 경우, 아무도 주목하지 않았던 리렌더링 성능 회귀가 발생한 뒤에야 이를 알아차리게 됩니다.
이 문제를 방지하기 위한 방법이 react-hooks/unsupported-syntax 규칙입니다. 이 규칙은 eslint-plugin-react-hooks v6 이상에 포함되어 있습니다. (eslint-plugin-react-compiler는 2025년 말에 이 패키지로 통합되었습니다.) v7부터는 recommended 설정에 기본 포함되지만, 기본 심각도는 warning입니다. 따라서 CI가 오류만 실패로 처리하도록 설정되어 있다면 이 경고는 그대로 지나갈 수 있습니다. 컴파일러가 조용히 해당 코드를 건너뛰는 상황을 실제로 빌드 실패로 만들고 싶다면, 이 규칙의 심각도를 error로 올리는 것이 좋습니다.
5. "use no memo" 이스케이프 해치
때로는 컴파일러의 판단이 적절하지 않을 수 있습니다. 또는 특정 컴포넌트를 컴파일러 친화적으로 수정하는 비용이 당장은 감당할 만한 수준이 아닐 수도 있습니다. 이런 경우 "use no memo" 지시어를 사용해 함수 단위로 컴파일러 적용을 비활성화할 수 있습니다.
function ComplicatedLegacyThing() {
"use no memo";
// 컴파일러는 이 함수를 건너뛰고 일반적인 리액트 컴포넌트로 처리합니다.
...
}
모든 "use no memo"는 코드베이스 안에 숨어 있는 성능 절벽과 같습니다. 이를 영구적인 표시가 아니라 앞으로 해결해야 할 TODO로 취급해야 합니다. 또한 왜 해당 코드가 아직 컴파일러와 호환되지 않는지 설명하는 주석을 함께 남겨두는 것이 좋습니다. 시간이 지나면서 이 지시어를 검색해 개수를 추적하는 것은 코드베이스의 전반적인 상태를 가늠할 수 있는 유용한 건강 지표가 됩니다.
대부분의 팀이 따르는 마이그레이션 경로
리액트 공식 문서와 Next.js, Expo, TanStack Start의 통합 가이드를 종합해 보면, 일반적으로 권장되는 순서는 비교적 간단합니다.
- 먼저 컴파일러를 지원하는 버전으로 리액트를 업그레이드합니다. 즉, 리액트 19 이상을 사용해야 합니다.
- ESLint 플러그인을 적용하고 규칙의 심각도를 조정합니다. 2025년 말부터 관련 규칙은
eslint-plugin-react-hooksv6 이상에 포함되어 있습니다. (eslint-plugin-react-compiler는 더 이상 별도로 사용되지 않으며 해당 패키지로 통합되었습니다.) 이미eslint-config-nextv16 이상과 같은 프레임워크 프리셋을 사용하고 있다면, v7 플러그인이 간접적으로 설치되고recommended규칙 세트도 기본적으로 활성화되어 있으므로 대부분의 경우 별도로 설치할 것은 없습니다. 실제로 중요한 작업은 경고(warn) 수준의 규칙을 오류(error)로 올리는 것입니다. 특히react-hooks/unsupported-syntax규칙을 error로 설정하면, 컴파일러가 조용히 건너뛴 컴포넌트가 성능 회귀로 이어지기 전에 린트 오류로 드러납니다. 컴파일러 설정을 건드리기 전에 먼저 이 규칙들이 발견한 문제를 해결해야 합니다. - annotation-driven 모드로 컴파일러를 활성화합니다. 우선 하나 또는 두 개의 리프 컴포넌트부터 적용하고 동작을 관찰합니다.
- inference 모드로 전환합니다. 이제 어떤 파일을 컴파일할지 컴파일러가 스스로 판단하도록 맡깁니다. 테스트를 실행하고 실제 사용자 인터랙션을 프로파일링합니다.
- 이제 컴파일러가 대신 처리하는 수동 메모이제이션 코드를 제거합니다.
useMemo,useCallback,React.memo호출은 대량으로 삭제할 수 있습니다. - 코드베이스가 충분히 정리되었다면 strict mode 도입을 고려합니다. 이 모드에서는 규칙 위반이 조용히 건너뛰는 대신 즉시 오류로 드러납니다.
바로 5단계로 건너뛰는 팀은 대개 몇 주 동안 열려 있는 거대한 PR을 만들게 됩니다. 반면 1단계와 2단계는 독립적으로 적용할 수 있으며, 그 자체만으로도 코드 품질을 개선하는 효과가 있습니다.
실제로 “규칙을 활성화한다”는 것은 무엇을 의미할까?
엄격한 규칙 세트를 기존 코드베이스에 실제로 적용해 보기 전까지는 잘 드러나지 않는 것들이 있습니다. 단순히 규칙 목록만 읽어서는 체감하기 어려운 부분들입니다.
다음은 eslint.config.mjs의 플랫 설정에 바로 추가할 수 있는, 실질적인 출발점이 되는 strict 설정 예시입니다.
{
rules: {
"react-hooks/unsupported-syntax": "error",
"react-hooks/exhaustive-deps": "error",
"react-hooks/incompatible-library": "error",
"react-hooks/todo": "error",
"react-hooks/syntax": "error",
"react-hooks/capitalized-calls": "error",
"react-hooks/rule-suppression": "error",
"react-hooks/no-deriving-state-in-effects": "error",
"react-hooks/void-use-memo": "error",
"react-hooks/automatic-effect-dependencies": "error",
"react-hooks/memoized-effect-dependencies": "error",
"react-hooks/hooks": "error",
},
}
처음 세 개는 이미 warn으로 제공되는 recommended 규칙의 심각도를 올린 것입니다. 나머지는 기본적으로 비활성화되어 있는 컴파일러 규칙입니다. eslint-config-next v16 이상을 사용하거나, eslint-plugin-react-hooks v7을 끌어오는 최신 프레임워크 프리셋을 사용하고 있다면 별도로 설치할 플러그인은 없습니다.
react-hooks/todo는 가장 많은 문제를 잡아냅니다. 이 규칙은 기본적으로 활성화되어 있지 않으며, 현재 react.dev에도 문서화되어 있지 않습니다. 하지만 컴파일러 내부의 lowering 실패, 즉 컴파일러가 아직 처리하는 방법을 학습하지 못한 패턴들을 가장 넓게 드러내는 규칙입니다. 실제 코드베이스에서 자주 등장하는 두 가지 예시는 다음과 같습니다.
// `.map()` 람다 안에서 캡처된 카운터를 변경하는 경우
let globalIndex = 0;
return (
<>
{groupA.map((row, i) => render(row, i, globalIndex++))}
{groupB.map((row, i) => render(row, i, globalIndex++))}
</>
);
// 이펙트 안에서 동적 import()를 사용하는 경우
useEffect(() => {
void import("heavy-lib").then(({ default: lib }) => {
/* ... */
});
}, []);
두 코드 모두 컴파일되고 실행됩니다. 그리고 둘 다 주변 컴포넌트를 조용히 컴파일러의 처리 대상에서 제외합니다. 해결 방법은 기계적으로 적용할 수 있습니다. 변경되는 카운터는 미리 계산된 오프셋으로 대체하면 됩니다. 예를 들어 groupB.map((row, i) => render(row, i, offsetB + i))처럼 작성할 수 있습니다. 또한 동적 import는 모듈 수준에서 캐시된 프로미스로 끌어올려, import 표현식이 더 이상 컴포넌트 본문 안에 존재하지 않도록 만들면 됩니다. 이 규칙은 가능한 한 엄격한 신호를 얻기 위해 켜둘 만합니다. 다만 unsupported-syntax보다 출력이 더 시끄러울 수 있다는 점은 감안해야 합니다.
조용한 스킵은 연쇄적으로 전파됩니다. 한 컴포넌트가 컴파일 대상에서 제외되면, 같은 컴포넌트에 대해 하위 규칙들이 자신의 진단 결과를 드러내지 못하는 경우가 있습니다. 분석이 첫 번째 실패 지점에서 멈출 수 있기 때문입니다. 따라서 상위의 스킵 원인을 수정하면 곧바로 두 번째 문제가 드러날 수 있습니다. 린트 정리는 한 번에 끝나는 작업이 아니라 반복적인 과정으로 다뤄야 합니다. 첫 실행은 개수를 알려주고, 이후의 실행들이 진짜 상태를 보여줍니다.
대부분의 프로덕션 코드는 통과합니다. 기본적으로 비활성화되어 있는 다른 컴파일러 규칙들(syntax, capitalized-calls, rule-suppression, no-deriving-state-in-effects, void-use-memo, effect deps 관련 규칙들)은 일반적인 리액트 관용구를 따르는 코드베이스에서는 대체로 조용합니다. 그래도 활성화해두는 것이 좋습니다. 오탐 비율은 낮고, 이 규칙들이 문제를 잡아내는 영역은 조용히 방치해서는 안 되는 영역이기 때문입니다.
여전히 논쟁 중인 것들
18개월이 지난 지금까지도 커뮤니티가 결론을 내리지 못한 주제는 크게 세 가지입니다.
리액트의 규칙을 강제 가능한 계약으로 볼 것인가
리액트의 규칙은 컴파일러 이전에도 존재했습니다. 하지만 그때까지는 어디까지나 권장 사항에 가까웠습니다. 컴파일러는 이 규칙들을 빌드 타임에 검증되는 경계로 바꾸었습니다. 규칙을 위반하면 해당 컴포넌트는 조용히 메모이제이션 대상에서 제외됩니다. 일부에서는 이것이 리액트가 원래 약속했던 것보다 더 엄격한 프로그래밍 모델을 사실상 강요한다고 주장합니다. 반대로, 리액트의 규칙은 애초부터 선택 사항이 아니었고 컴파일러는 그 결과를 눈에 보이게 만들었을 뿐이라는 반론도 있습니다. 두 입장 모두 일정 부분 타당합니다. 그리고 이러한 긴장은 리액트의 규칙이 명문화되기 이전에 만들어진 오래된 라이브러리들에서 가장 뚜렷하게 드러납니다.
"use no memo"는 영구적인 기술 부채인가
탈출구(escape hatch) 매우 쉽게 사용할 수 있습니다. 그래서 많은 사람들이 코드베이스에 비슷한 표시들이 수년간 쌓이는 모습을 떠올리며 우려합니다. 흔히 @ts-ignore나 // eslint-disable과 비교됩니다. 급한 상황에서 유용한 안전장치이지만, 다시 검토되지 않으면 시간이 지날수록 문제를 키우기 쉽습니다. 반면 다른 사람들은 이 지시어가 전체 리팩터링 없이도 배포할 수 있게 해주기 때문에 실질적인 가치가 있다고 봅니다. 이를 영구적인 표시로 사용하는 것은 기능 자체의 문제가 아니라 사용 방식의 문제라는 주장입니다. 한 가지 분명한 점은 이제 아무도 "use no memo"의 개수를 코드베이스 건강 상태를 나타내는 지표로 보는 것에 이견을 제기하지 않는다는 것입니다.
컴파일러와 런타임 최적화 도구의 관계
대부분의 애플리케이션에서는 컴파일러만으로 충분합니다. 하지만 리스트 중심의 특수한 UI, 예를 들어 트레이딩 대시보드, 로그 뷰어, 스프레드시트와 같은 애플리케이션에서는 Million.js와 같은 블록 기반 리컨실러가 여전히 눈에 띄는 성능 이점을 제공할 수 있습니다. 두 계층은 서로 다른 문제를 해결합니다. 컴파일러는 컴포넌트가 얼마나 자주 리렌더링되는지를 줄이고, 런타임 최적화 도구는 각 리렌더링을 얼마나 빠르게 처리할지를 개선합니다. 일부 팀은 두 방식을 함께 사용합니다. 실제로는 잘 동작하지만, 그 상호작용에 대해 충분히 정리된 문서는 아직 많지 않습니다. 언젠가는 누군가 이 주제를 완전히 정리한 결정적인 글을 쓰게 될 것이라고 생각합니다.
앞으로의 방향
리액트 팀의 공개 발언, Meta의 엔지니어링 글, 그리고 현재 진행 중인 RFC들을 종합해 보면 앞으로의 방향은 다섯 가지 정도로 정리할 수 있습니다. 다만 이것들은 확정된 약속이 아니라 방향성을 보여주는 신호로 받아들이는 것이 좋습니다.
더 세밀한 컴파일 제어
현재 컴파일러의 설정 방식은 비교적 단순합니다. 활성화, 비활성화, annotation-driven 모드 정도가 전부입니다. 하지만 실제 팀들이 원하는 것은 보다 세밀한 제어입니다. 특정 컴포넌트는 공격적으로 최적화하고, 다른 컴포넌트는 보수적으로 처리하며, 레거시 코드 영역은 아예 제외하고 싶어 합니다. 또한 컴파일러가 실제로 어떤 최적화를 적용했는지를 확인할 수 있는 도구도 필요합니다. 앞으로는 현재의 다소 이분법적인 설정 방식을 보완할 수 있는 새로운 지시어가 등장할 가능성이 높습니다.
서버 컴포넌트와의 더 긴밀한 통합
RSC는 이미 클라이언트 아일랜드에 대해 메모이제이션 친화적인 결과물을 생성합니다. 다음 단계는 직렬화 경계를 더욱 최적화하는 것입니다. 컴파일러가 특정 값이 브라우저로 전달될 필요가 없다고 증명할 수 있다면, 브라우저가 하이드레이션해야 하는 페이로드 자체를 줄일 수 있습니다. 실제 애플리케이션에서 가장 큰 성능 개선이 기대되는 지점도 바로 여기입니다. 리렌더링 횟수를 줄이는 것보다 하이드레이션 비용을 줄이는 것이 더 중요할 수 있습니다. 초기 로딩 시 사용자가 직접 체감하는 비용은 하이드레이션이기 때문입니다.
useEvent의 등장 가능성
최신 상태를 읽으면서도 안정적인 이벤트 콜백을 제공하는 useEvent는 오랫동안 논의되어 온 API입니다. 이 API의 동작을 검증 가능하게 만드는 핵심 요소가 바로 컴파일러의 순수성 분석입니다. 컴파일러가 등장하기 전까지 이 제안이 진전되지 못했던 이유도 여기에 있습니다. 어떤 형태로든 가까운 미래에 공식적으로 제공될 가능성이 높습니다.
리액트 네이티브
네이티브 환경에서의 뷰 비교(diffing)는 웹의 재조정보다 비용이 더 큽니다. 따라서 자동 메모이제이션의 효과는 리액트 네이티브에서 더 크게 나타날 수 있습니다. Expo는 이미 컴파일러 지원을 제공하고 있지만, 리액트 네이티브 자체의 지원은 상대적으로 더디게 진행되고 있습니다. 컴포넌트당 기대할 수 있는 성능 이점이 더 크기 때문에, 네이티브 환경에서도 지원에 대한 요구는 점점 커지고 있습니다.
개발자 도구
현재 가장 부족한 부분은 컴파일러가 각 컴포넌트에 대해 무엇을 했고, 왜 그렇게 했는지를 보여주는 DevTools 패널입니다. 지금은 ESLint 플러그인이 어떤 컴포넌트가 건너뛰어졌는지를 알려줄 뿐입니다. 여기에 더해 실제로 어떤 최적화가 적용되었는지를 시각적으로 보여주는 도구가 있다면, 컴파일러의 동작을 훨씬 직관적으로 이해할 수 있을 것입니다. 이는 RSC 트리 시각화 도구와 비슷한 역할을 할 수 있습니다. 제가 아는 한 공식 로드맵에 포함되어 있지는 않지만, 매우 자연스러운 다음 단계이며 리액트 팀보다 먼저 커뮤니티 플러그인 형태로 등장할 가능성도 충분히 있습니다.
앞으로의 방향에 대한 개인적인 생각
18개월이 지난 지금, 리액트 컴파일러가 남길 가장 큰 유산은 벤치마크 수치가 아닐 것입니다. 진짜 변화는 특정 종류의 버그가 더 이상 문제로 남지 않게 되었다는 점입니다. “useCallback의 의존성 배열을 빠뜨렸네요.”라는 말은 이제 더 이상 의미 있는 대화 주제가 아닙니다. “이 컴포넌트에 메모이제이션을 적용해야 할까?”라는 질문도 마찬가지입니다. 답은 항상 “그렇다”이며, 그 작업은 컴파일러가 대신 처리합니다.
더 흥미로운 질문은, 이제 컴파일러가 리액트의 규칙을 강제하게 된 상황에서 이 규칙들이 어떤 의미를 갖게 되는가입니다. 오랫동안 이 규칙들은 “좋은 리액트 코드라면 자연스럽게 따르게 되는 관례”에 가까웠습니다. 하지만 이제는 이를 준수하지 않는 코드를 받아들이지 않는 빌드 타임 계약이 되었습니다. 생태계에서 가장 오래된 라이브러리들 중 일부는 여전히 이 변화에 적응하는 과정에 있습니다. 앞으로 1년 동안의 핵심 변화는 컴파일러 자체보다도, 생태계 전체가 얼마나 컴파일러 친화적으로 재편되느냐에 달려 있을 가능성이 큽니다.
새로운 프로젝트라면 선택은 간단합니다. 그냥 활성화하면 됩니다. 기존 프로젝트에서는 가장 오래된 코드를 직접 수정할지, 아니면 "use no memo"를 일종의 유예 장치로 받아들일지를 결정해야 합니다. 두 선택 모두 충분히 합리적이며, 중요한 것은 의도적으로 결정하는 것입니다.
2026년에 useMemo나 useCallback을 보게 된다면, 현대 자바스크립트에서 직접 작성한 for 루프를 보는 것처럼 생각해도 좋습니다. 대부분의 경우 문제는 없습니다. 때로는 여전히 필요합니다. 하지만 많은 경우, 더 나은 도구가 등장하기 전에 작성된 코드라는 신호일 가능성이 큽니다.
아래 글도 읽어보세요.
컴파일러가 수동 메모이제이션을 사실상 없앴다면, 2026년에 컴포넌트로 데이터가 전달되는 방식을 크게 바꾼 또 하나의 변화는 use() 훅입니다. 이 새로운 기본 요소가 무엇인지, 그리고 이를 어떻게 활용할 수 있는지에 대해서는 별도의 글에서 자세히 다룹니다. 대부분의 코딩 에이전트는 리액트 컴파일러 이전의 방식으로 코드를 생성하는 경향이 있습니다. 이는 에이전트가 학습한 데이터의 대부분이 컴파일러가 등장하기 이전에 작성된 코드이기 때문입니다. /react-compiler 스킬은 이러한 편향을 보완하기 위한 역할을 합니다. 기본적으로 수동으로 useMemo, useCallback, React.memo를 작성하지 않도록 유도하며, 기존 코드베이스를 점검하기 위한 감사 워크플로와 엄격한 린트 설정도 함께 제공합니다. 다음 명령어로 설치할 수 있습니다. npx skills@latest add saschb2b/skills --skill react-compiler. 또는 react-compiler 스킬의 전체 문서를 직접 읽어볼 수도 있습니다.
'개발 > 번역' 카테고리의 다른 글
| [번역] 팩토리 모델: 코딩 에이전트가 소프트웨어 엔지니어링을 어떻게 바꾸고 있는가 (0) | 2026.06.21 |
|---|---|
| [번역] startTransition은 언제 정말로 필요할까요? (2) | 2026.04.26 |
| [번역] 코드 리뷰를 잘한다면, AI 에이전트를 잘 활용하게 될 것입니다 (1) | 2026.03.08 |
| [번역] 장애허용성 (1) | 2026.02.09 |
| [번역] 모든 것을 배열로 바꾸는 것을 그만두세요 (대신 일을 덜 하세요) (1) | 2026.01.24 |