이 글은 소프트웨어 회사 LogRocket 의 블로그에 기고된
David Omotayo
님의 글 'Exploring the React Compiler: A detailed introduction' 을 한국어로 옮긴 것입니다.
React는 등장 이래로 개발자들이 사용자 인터페이스를 구축하는 방식을 혁신적으로 변화시켜 왔으며, 매번 새로운 업데이트를 통해 컴포넌트 기반의 동적인 애플리케이션을 더욱 효과적으로 개발할 수 있는 강력한 기능을 제공하고 있습니다.
그러나 여러 강점에도 불구하고, React는 전통적으로 Vue나 Svelte 같은 프레임워크에 비해 전용 컴파일러가 부족하다는 한계가 있었습니다. 이로 인해 개발자들은 성능 최적화와 리렌더링 제어를 위해 useMemo
나 useCallback
같은 Hook을 사용해야만 했습니다.
하지만 최신의 React 19에서는, 새로운 React Compiler를 도입함으로써 이러한 성능 최적화 문제를 해결할 수 있게 되었습니다! 이러한 혁신은 수동으로 메모이제이션하고 최적화해야 하는 부담을 줄여, React 기반의 프론트엔드 개발을 더욱 효율적으로 만들어 줍니다.
이 글에서는, React Compiler가 무엇인지와 어떻게 동작하는지, 그리고 프론트엔드 개발에 어떠한 이점을 가져오는지를 살펴볼 것입니다.
일부 개발자들은 전체 프론트엔드 개발 경력에서 React만 사용해왔을 수도 있다는 점을 고려해본다면, "컴파일러"라는 개념이 다소 생소하게 느껴질 수 있습니다. 따라서 먼저 컴파일러란 무엇인지부터 간략히 소개하는 것이 도움이 될 것입니다.
물론 React 개발자들이 일반적으로 컴파일러에 대한 기본적인 지식이 부족하다는 뜻은 아닙니다. 하지만 전통적인 프로그래밍 언어에서의 컴파일러와 웹 프레임워크에서 사용되는 컴파일러의 차이점을 명확히 구분하고 이해하는 것은 중요합니다. 이제 이 두 가지가 어떻게 다르고, 각 유형에 따라 구체적으로 어떠한 기능을 수행하는지를 살펴보도록 하겠습니다.
전통적인 컴파일러는 C, Java, Rust와 같은 고수준 프로그래밍 언어를 컴퓨터 하드웨어가 직접 실행할 수 있는 저수준의 기계어로 변환하는 역할을 수행합니다.
컴파일러는 여러 단계에 걸쳐 코드를 처리하는데, 대표적으로 분석(analysis), 최적화(optimization) 및 코드 생성(code generation) 등의 단계를 거칩니다. 이후, 생성된 기계어 코드는 라이브러리 및 기타 모듈과 연결되어 특정 플랫폼에서 최종적으로 실행 가능한 바이너리를 생성합니다.
반면, 웹 프레임워크에서의 컴파일러는 선언형 컴포넌트 기반 코드를 웹 브라우저에서 실행할 수 있는 최적화된 JavaScript, HTML 및 CSS로 변환하도록 설계되었습니다. 각 프레임워크의 컴파일러가 컴파일 과정을 처리하는 방식은 모두 다르지만, 일반적으로 다음과 같이 동일한 단계를 거칩니다:
이쯤에서 이러한 의문이 들 수도 있습니다: React에는 컴파일러가 없는데, 앞서 언급한 웹 프레임워크에서의 필수적인 개념들을 어떻게 처리할 수 있을까요?
그에 대한 해답은 바로 React의 핵심 멘탈 모델에 있습니다. React Compiler의 세부 내용을 살펴보기 전에, 먼저 React가 어떻게 동작하는지 이해할 필요가 있습니다.
기본적으로 React는 선언형 프로그래밍 모델을 채택합니다. 이 모델에서는 현재 애플리케이션의 상태를 기반으로 UI가 어떻게 보여야 하는지를 선언적으로 기술합니다. 즉, 개발자가 직접 DOM을 조작하고 업데이트하는 방식(명령형 프로그래밍)이 아니라,
의도하는 결과를 정의하면 React가 이를 반영하여 DOM을 자동으로 업데이트해 주는 것입니다.
다음과 같은 컴포넌트의 예시를 살펴보겠습니다:
function ReactComponent() {
return (
<div>
<h1>Hello, World!</h1>
</div>
);
}
초기 렌더링에서, 이 스니펫은 ReactComponent
가 렌더링될 때, "Hello, World!"
라는 텍스트가 포함된 h1
요소가 포함된 div
를 생성해야 한다고 선언합니다.
이제 이 컴포넌트가 state를 가지고, 부모 컴포넌트로부터 props를 받는다고 가정해 보겠습니다:
function ReactComponent(props) {
const [state, setState] = useState(null);
return (
<div>
<h1>Hello, World!</h1>
</div>
);
}
위 컴포넌트의 state나 props가 변경되면, React는 일련의 반응형 프로세스를 거쳐 컴포넌트를 다시 렌더링(re-render)합니다. 이를 통해 선언된 내용이 항상 최신의 state와 일치하도록 보장됩니다.
이는 React의 재조정(reconciliation)을 통해 처리되며, 새로운 state와 일치하도록 UI를 업데이트하는 데 필요한 최소한의 변경 횟수를 결정하게 됩니다.
재조정 과정에서, React는 UI의 메모리 내(in-memory) 표현인 가상 DOM을 활용합니다. 변경이 필요한 컴포넌트를 표시하고, 변경 전후의 가상 DOM을 비교(diffing)하여 효율적으로 변경 사항을 식별한 후 실제 DOM을 업데이트합니다.
요컨대 React는 기본적으로 다시 렌더링하도록 설계되었다는 것입니다. 이름에서 알 수 있듯, 애플리케이션의 UI가 내부 state와 동기화된 상태를 유지할 수 있도록 state가 변경될 때마다 컴포넌트는 다시 렌더링됩니다.
이전 섹션에서 설명한 것처럼, React는 다시 렌더링되도록 설계되었습니다. 하지만 이것이 문제가 될 수도 있습니다. React는 단순히 state가 변경된 컴포넌트만 다시 렌더링하는 것만이 아니라, 해당 컴포넌트 내부의 모든 컴포넌트와 그 내부의 컴포넌트까지, 즉 컴포넌트 트리의 종단에 도달할 때까지 재귀적으로 다시 렌더링하기 때문입니다.
다음의 예제를 살펴보면, 버튼을 클릭하여 message
state가 "Hello, React!"
로 변경되었을 때 childComponent
와 AnotherChildComponent
두 컴포넌트가 모두 다시 렌더링됩니다. 이는 AnotherChildComponent
가 message
prop을 사용하지 않았음에도 발생합니다:
import React from 'react';
function AnotherChildComponent() {
return <p>Another child component</p>;
}
function ChildComponent({ message }) {
return <p>{message}</p>;
}
export function ParentComponent() {
const [message, setMessage] = useState('Hello, World!');
const updateMessage = () => {
setMessage('Hello, React!'); // 이는 ChildComponent와 AnotherChildComponent가 다시 렌더링되게끔 만들 것입니다.
};
return (
<div>
<AnotherChildComponent />
<ChildComponent message={message} />
<button onClick={updateMessage}>Update Message</button>
</div>
);
}
이것이 반드시 나쁜 것은 아니지만, 너무 자주 발생하거나 트리 하위의 컴포넌트 중 하나가 무거운 경우(즉, 복잡한 연산이 있는 경우)에는 애플리케이션의 성능에 심각한 영향을 미칠 수 있습니다.
다음의 예제를 살펴보겠습니다:
import React, { useState } from 'react';
// 비싼 연산을 모의하는 함수
function expensiveComputation(num) {
console.log('Running expensive computation...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num * Math.random();
}
return result;
}
function HeavyComponent({ number }) {
// 렌더링 과정에서 직접 비싼 연산 수행하기
const result = expensiveComputation(number);
return (
<div>
<p>Expensive Computation Result: {result}</p>
</div>
);
}
export function ParentComponent() {
const [number, setNumber] = useState(1);
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<HeavyComponent number={number} />
<button onClick={incrementCount}>Increment Count: {count}</button>
</div>
);
}
count
state가 변경될 때마다, HeavyComponent
가 다시 렌더링되고 이 과정에서 expensiveComputation
함수가 호출하여 성능에 상당한 영향을 미칠 것입니다.
이러한 연쇄적인 리렌더링을 방지하고 최적화하기 위해, 개발자들은 이러한 컴포넌트를 수동으로 메모해야 했습니다. 메모이제이션은 React 16에서 처음 도입된 최적화 테크닉으로, 높은 비용의 함수 호출 결과를 캐싱하고 동일한 입력이 다시 주어질 때 이를 재사용하여 불필요한 리렌더링을 방지합니다.
React는 메모이제이션을 위한 몇 가지 도구를 제공합니다: React.memo
, useMemo
및 useCallback
입니다. 이러한 Hook은 통상 컴포넌트나 props를 감싸서 부모 컴포넌트에 의존하지 않는다는 것을 React에 알리는 데 사용됩니다. 부모 컴포넌트가 다시 렌더링되더라도, (이러한 Hook에) 감싸진 컴포넌트는 다시 렌더링되지 않습니다.
이 개념을 활용하여, 앞선 예제를 최적화해볼 수 있습니다:
import React, { useState, useMemo } from 'react';
// 비싼 연산을 모의하는 함수
function expensiveComputation(num) {
console.log('Running expensive computation...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num * Math.random();
}
return result;
}
const HeavyComponent = React.memo(({ number }) => {
// 비싼 연산의 결과 메모하기
const result = useMemo(() => expensiveComputation(number), [number]);
return (
<div>
<p>Expensive Computation Result: {result}</p>
</div>
);
});
export function ParentComponent() {
const [number, setNumber] = useState(1);
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<div>
<HeavyComponent number={number} />
<button onClick={incrementCount}>Increment Count: {count}</button>
</div>
);
}
여기서는 useMemo
Hook을 사용하여 HeavyComponent
컴포넌트에서 expensiveComputation
함수의 결과를 메모하였으며, 이를 통해 number
prop이 변경될 때만 연산이 다시 수행됩니다. 또한 React.memo
로 HeavyComponent
를 감싸서 해당 컴포넌트의 프로퍼티가 변경되지 않는 한 불필요한 리렌더링을 방지합니다.
이러한 메모이제이션 Hook은 강력하고 효과적이지만, 적절하게 사용하는 것은 쉽지 않습니다. 언제 어떻게 사용해야 하는지 판단하기 어렵기 때문에, 많은 개발자들이 앱 속도를 향상시키고자 useCallback
과 useMemo
로 감싼 함수와 컴포넌트를 과도하게 사용하면서 코드가 난잡해지는 경우가 많습니다.
그래서 이러한 문제를 해결하기 위해 React Compiler가 등장했습니다.
React Compiler는 원래 React Forget이라는 이름으로 React Conf 2021에서 처음 소개되었습니다. 애플리케이션의 코드를 자동으로 분석하고, 컴포넌트와 그 프로퍼티 및 Hook의 의존성을 최적화된 코드로 변환하는 저수준 컴파일러입니다.
본질적으로 React Compiler는 memo
, useMemo
및 useCallback
등의 Hook이 수행하는 최적화 작업을 자동으로 적용함으로써 리렌더링 비용을 최소화합니다.
초기 발표 이래로 컴파일러는 지속적으로 발전해 왔으며, 단순히 컴포넌트를 메모이제이션하는 것 이상의 역할을 수행하게 되었습니다. 최근의 아키텍처에서는 지역 범위의 변경(local mutations)나 참조 의미론(reference semantics)과 같은 복잡한 코드 패턴까지 최적화할 수 있게 되었습니다. Meta의 Instagram과 같은 애플리케이션에서는 이미 이전부터 컴파일러를 적극적으로 활용하고 있습니다.
컴파일 과정에서 React Compiler는 코드를 리팩토링하며, 이전에는 useMemoCache
로 알려졌던 _c
라는 Hook을 사용하여 캐싱할 수 있는 요소들의 배열을 생성합니다.
이 과정에서 각 컴포넌트의 일부를 가져와 배열의 특정 슬롯에 저장한 다음, 메모이제이션 블록을 생성합니다. 이는 기본적으로 if
문을 사용하여 배열 내 요소가 변경되었는지를 검사하며, 다음 번 컴포넌트가 호출될 때 요소가 변경되지 않았다면 기존 캐시된(원본) 요소를 반환합니다.
예를 들어, 아래와 같은 간단한 컴포넌트가 있다고 가정해 보겠습니다:
function SimpleComponent({ message }) {
return (
<div>
<p>{message}</p>
</div>
);
}
컴파일된 출력은 다음과 같을 것입니다:
function SimpleComponent(t0) {
const $ = _c(2);
const { message } = t0;
let t1;
if ($[0] !== message) {
t1 = (
<div>
<p>{message}</p>
</div>
);
$[0] = message;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
이 코드에서 어떤 일이 일어나고 있는지 분석해 보겠습니다. 먼저, 컴파일러는 _c
Hook을 사용하여 두 개의 슬롯이 있는 배열을 초기화하고 컴포넌트 상태를 캐시한 다음, t0
prop 객체에서 message
prop을 분해하여 JSX 요소를 보유할 암시적 t1
변수를 생성합니다:
const $ = _c(2);
const { message } = t0; // props 객체에서 message prop 추출 (t0)
let t1; // JSX 요소를 보유할 변수
다음으로, React Compiler는 메모이제이션 블록을 생성하여 message
prop이 변경되었는지 확인합니다. 만약 변경되었다면, 새로운 JSX를 생성하여 t1
변수에 할당하고, 배열을 업데이트하여 변경된 message
와 JSX 요소를 저장합니다. 반대로, message
가 변경되지 않았다면 기존에 캐시된 JSX 요소를 그대로 사용합니다.
if ($[0] !== message) { // `message` prop이 변경되었는지 확인
t1 = ( // `message`가 변경되었다면 JSX 요소를 생성
<div>
<p>{message}</p>
</div>
);
$[0] = message; // 새로운 message로 캐시 갱신
$[1] = t1; // 새로운 JSX 요소로 캐시 갱신
} else {
t1 = $[1]; // message가 변경되지 않았다면 캐시에서 JSX 요소 재사용
}
React Compiler를 직접 사용해 보고 싶다면, React Compiler Playground를 활용하거나 여러분의 프로젝트에 통합해 볼 수 있습니다. 다음 섹션에서는 이를 설정하는 방법을 자세히 다룰 예정입니다. 또한, 컴파일러에 대해 더 심층적으로 상세히 살펴보고 싶으시다면, Sathya Gunasekaran의 "React Compiler Deep Dive" React Conf 강연을 시청해 보시는 것도 좋습니다.
코드를 효율적으로 컴파일하고 성능을 최적화하려면, 먼저 React Compiler가 해당 코드를 이해할 수 있어야 합니다. 여기서 컴파일러는 JavaScript와 React의 규칙을 활용합니다. 이러한 규칙들은 예측 가능하고 효율적인 React 애플리케이션을 작성하는 데 도움이 되며, 버그를 줄이는 역할도 합니다.
다음은 React의 주요 규칙들입니다:
useEffect
같은 Hook을 사용하여 관리해야 합니다.setState
또는 useState
와 같은 Hook에서 제공하는 유사한 함수)를 사용해야 합니다.컴파일 과정에서, 컴파일러는 이러한 규칙이 위반되었는지를 감지합니다. 만약 규칙을 위반한 컴포넌트나 Hook이 발견되면, 해당 부분을 자동으로 건너뛰고 안전하게 나머지 코드의 최적화를 진행합니다. 또한, 코드가 이미 충분히 최적화되어 있다면 컴파일러를 활성화하더라도 큰 성능 향상을 체감하지 못할 수도 있습니다.
추가적으로, 만약 컴파일러가 메모된 컴포넌트에서 최적화를 안전하게 유지할 수 없다고 판단하면, 해당 컴포넌트의 최적화를 건너뛰고 대신 개발자가 적용한 수동 메모이제이션을 수행합니다.
다음 코드를 예시로 들어보겠습니다:
import React, { useState, useMemo } from 'react';
function ItemList({ items }) {
const [filter, setFilter] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
const filteredAndSortedItems = useMemo(() => {
const filteredItems = items.filter(item => item.includes(filter));
const sortedItems = filteredItems.sort((a, b) => {
if (sortOrder === 'asc') return a.localeCompare(b);
return b.localeCompare(a);
});
return sortedItems;
}, [filter, sortOrder, items]);
return (
<div>
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items"
/>
<select value={sortOrder} onChange={e => setSortOrder(e.target.value)}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<ul>
{filteredAndSortedItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default ItemList;
React Compiler는 이러한 코드를 건너뛰는데, 그 이유는 메모된 값이 컴포넌트 내부나 애플리케이션의 다른 곳에서 변경될 가능성이 있기 때문입니다. 이러한 변경은 메모이제이션을 무효화하고 예기치 못한 버그와 성능 저하를 초래할 수 있습니다.
React Compiler Playground에서 위 코드를 실행하면, 다음과 같은 에러가 발생할 것입니다:
React Compiler는 아직 실험적인 단계이며, 프로덕션에서의 사용은 권장되지 않습니다. 하지만 작은 프로젝트에서 시험해 보거나 기존 프로젝트에 통합해보고 싶으시다면, 이 섹션에서 React Compiler를 시작하는 방법을 안내해드릴 수 있습니다.
컴파일러를 설치하기 전에, 몇 가지 사전 준비가 필요합니다. 여기에는 프로젝트가 컴파일러와 호환되는지 확인하는 작업과 컴파일러용 ESLint 플러그인을 설치하는 것이 포함됩니다.
프로젝트가 컴파일러와 호환되는지 확인하려면, 프로젝트 디렉토리에서 다음 명령어를 실행합니다:
npx react-compiler-healthcheck@latest
이 명령어는 최적화할 수 있는 컴포넌트의 수, strict mode 활성화 여부, 그리고 컴파일러와 호환되지 않을 수 있는 라이브러리의 설치 여부를 검사합니다. 코드가 모든 규칙을 준수하고 있다면, 다음과 유사한 응답을 확인할 수 있습니다:
React Compiler에는 코드가 React의 규칙을 준수하는지 확인하고 잠재적인 문제를 감지할 수 있도록 도와주는 ESLint 플러그인이 포함되어 있습니다. 이 플러그인은 컴파일러와 독립적으로 동작하므로, 컴파일러 없이도 사용할 수 있습니다. 따라서 이를 사용하려면 먼저 설치해야 합니다:
npm install eslint-plugin-react-compiler
그런 다음 ESLint 설정에 추가합니다:
module.exports = {
plugins: [
'eslint-plugin-react-compiler',
],
rules: {
'react-compiler/react-compiler': "error",
},
}
현재 이 글을 작성하는 시점에서, React Compiler는 React 19에서만 호환됩니다. 따라서 컴파일러를 사용하려면 프로젝트를 최신 테스트 버전의 React 및 React DOM으로 업그레이드해야 합니다. 이를 위해 프로젝트 디렉토리에서 다음 명령어를 실행하세요:
npm install --save-exact react@rc react-dom@rc
터미널 창에 다음과 같은 내용이 표시되어야 합니다:
다음으로, 컴파일러용 Babel 플러그인인 babel-plugin-react-compiler
를 설치합니다. 이 플러그인을 사용하면 빌드 프로세스에서 컴파일러를 실행할 수 있습니다:
npm install babel-plugin-react-compiler
설치 후에는, Babel 설정 파일의 plugins
배열에 다음을 추가합니다:
// babel.config.js
const ReactCompilerConfig = { /* ... */ };
module.exports = function () {
return {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig], // 먼저 실행되어야 합니다!
// ...
],
};
};
Vite를 사용한다면, 대신 vite.config
파일에 플러그인을 추가하세요:
// vite.config.js
const ReactCompilerConfig = { /* ... */ };
export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
// ...
};
});
컴파일러가 빌드 파이프라인에서 가장 먼저 실행되도록 설정해야 합니다. 즉, 다른 플러그인이 있다면 컴파일러보다 후순위로 배치하세요. 또한, 오류를 방지하기 위해 ReactCompilerConfig
객체를 설정 파일의 최상위 수준에 추가해야 합니다.
이제, 개발 서버를 실행한 후 브라우저에서 React Developer Tools를 열어보면, 컴파일러가 최적화한 컴포넌트 옆에 Memo✨
배지가 표시되는 것을 확인할 수 있을 것입니다:
이제 끝났습니다! React Compiler를 프로젝트에 성공적으로 통합했습니다.
새 프로젝트에서 React Compiler를 사용하려면, 모든 설정이 포함된 Next.js의 canary 버전을 설치하는 것이 가장 좋은 방법입니다(역주: 글 작성 시점 현재는 stable 버전이 릴리즈된 상태이므로, canary 버전을 설치하지 않아도 됩니다). 프로젝트를 시작하려면 다음 명령어를 실행하세요:
npm install next@canary babel-plugin-react-compiler
그 다음, next.config.js
파일에서 experimental
옵션을 사용하여 컴파일러를 활성화합니다:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;
experimental 옵션에서는 다음 환경에서 컴파일러가 지원되도록 합니다:
앞서 언급했듯, 컴파일러는 아직 실험적인 단계이며 프로덕션 환경에서 사용하는 것은 권장하지 않습니다. 이는 JavaScript의 유연한 특성으로 인해 컴파일러가 규정된 규칙을 위반할 수 있는 모든 가능성을 완벽하게 포착하는 것이 불가능하고, 오탐(거짓 양성, false negatives)과 함께 컴파일될 수 있기 때문입니다.
이러한 이유로, React Compiler를 전체 프로젝트에 적용하기보다는 애플리케이션의 특정 부분에만 제한적으로 사용하는 것이 좋습니다. 그러면 코드베이스 전체에 영향을 주지 않으면서 컴파일러의 이점을 점진적으로 도입하고 실험해 볼 수 있습니다.
이를 위한 방법으로는 크게 두 가지가 있습니다. 각각을 간략히 살펴보겠습니다.
특정 디렉토리 내의 파일에만 React Compiler를 사용하도록 빌드 설정을 구성할 수 있습니다. 이를 위해 이전에 설정했던 Babel 또는 Vite 구성 파일의 ReactCompilerConfig
객체에 다음 코드를 추가하세요:
const ReactCompilerConfig = {
sources: (filename) => {
return filename.indexOf('src/path/to/dir') !== -1;
},
};
그런 다음 src/path/to/dir
을 컴파일러가 동작해야 할 폴더의 경로로 바꿉니다.
또 다른 방법으로는, 파일 상단에 특별한 디렉티브 주석을 추가하여 파일별로 컴파일러를 활성화하도록 선택할 수 있습니다. 이 방식은 전체 설정을 변경하지 않고도 개별 파일에만 컴파일러를 적용할 수 있도록 해 줍니다. 이를 위해 ReactCompilerConfig
객체에 다음 설정을 추가합니다:
const ReactCompilerConfig = {
compilationMode: "annotation",
};
그런 다음, 컴파일러가 개별적으로 최적화하도록 하려는 컴포넌트에 "use memo"
디렉티브를 추가합니다:
// src/app.jsx
export default function App() {
"use memo";
// ...
}
React Compiler는 다른 프레임워크의 기능과 비교했을 때 현재로서는 특별한 이점을 제공하지 않을 수도 있습니다. 그리고 유감스럽게도 특히 많은 개발자들이 기대하는 개선 사항인 useEffect
와 같은 Hook에서 의존성 배열을 제거하는 것은 아직 구현되지 않았습니다.
그럼에도 불구하고, 컴파일러는 가까운 미래에 실현될 수 있는 가능성을 엿볼 수 있게 해줍니다. 예를 들면, Hook의 의존성 배열을 불필요한 것으로 만들 수 있는 가능성이 있습니다. 그렇게 되면 React 컴포넌트의 상태 관리와 부작용을 단순화하여 보일러플레이트 코드를 줄이고, 잘못된 의존성과 관련된 버그의 위험을 최소화할 수 있습니다.
그 동안에 React Compiler를 실험해 보면서 피드백을 제공한다면 향후 개발 방향을 결정하는 데 도움이 될 수 있을 것입니다. 그럼 즐겁게 개발하세요!