
더 이상 느린 리액트는 없다: 리액트 컴파일러 완벽 가이드
React의 매력은 마치 레고 블록처럼 함수(컴포넌트)를 조립하여 UI를 구축하는 직관성에 있습니다.
개발자는 그저 함수를 만들고, React는 그 함수를 적절한 시점에 불러내 화면을 구성하죠.
하지만 이 간편함 뒤에는 성능이라는 잠재적 문제가 숨어 있습니다.
복잡한 연산을 수행하는 함수는 앱을 느리게 만드는 주범이 될 수 있기 때문입니다.
그래서 과거에는 개발자가 직접 성능 튜닝이라는 험난한 여정을 떠나야 했는데요.
"이 함수는 언제 다시 실행해야 최적일까?"라는 질문에 답을 찾기 위해 고군분투해야 했습니다.
이제, 리액트 팀이 이 고된 여정을 자동화해 줄 구원투수를 등판시켜줬는데요.
바로 리액트 컴파일러(React Compiler)입니다!
코드를 마법처럼 재구성하여 성능을 끌어올려 주는 녀석입니다.
그렇다면 리액트 컴파일러는 어떻게 코드를 바꾸고, 어떤 원리로 작동하는 걸까요?
그리고 우리는 이 녀석을 꼭 사용해야 할까요?
지금부터 그 비밀을 낱낱이 파헤쳐 보겠습니다!
컴파일러, 트랜스파일러, 최적화 도구: 용어 정리부터 확실하게!
최신 javascript 세계에서는 '컴파일러', '트랜스파일러', '최적화 도구'라는 용어가 자주 등장하는데요, 헷갈릴 수 있는 이 녀석들을 먼저 정리해 보죠.
트랜스파일링: JSX를 자바스크립트로 변신시키는 마법
트랜스파일러는 마치 번역가와 같습니다.
특정 언어로 작성된 코드를 다른 언어나, 같은 언어의 다른 버전으로 바꿔주는 역할을 하죠.
리액트 개발자라면 JSX라는 편리한 문법을 이미 알고 계실 겁니다.
HTML과 유사한 이 문법은 중첩된 함수 호출(결국 중첩된 객체 트리를 만드는)을 간편하게 표현하는 방식입니다.
하지만 자바스크립트 엔진은 JSX를 직접 이해하지 못합니다.
그래서 트랜스파일러가 필요합니다.
JSX를 분석하여 자바스크립트 엔진이 이해할 수 있는 함수 호출로 변환해 주기 때문이죠.
예를 들어, 아래와 같은 JSX 코드를 작성했다고 가정해 보겠습니다.
function App() {
return <Item item={item} />;
}
function Item({ item }) {
return (
<ul>
<li>{ item.desc }</li>
</ul>
)
}
트랜스파일러(Babel과 같은)를 거치면 다음과 같이 변환됩니다.
function App() {
return _jsx(Item, {
item: item
});
}
function Item({ item }) {
return _jsx("ul", {
children: _jsx("li", {
children: item.desc
})
});
}
이것이 실제로 브라우저에 전달되는 코드입니다.
HTML처럼 보이지만, 실은 'props'라고 불리는 자바스크립트 객체를 전달하는 중첩 함수 호출이죠.
이 변환 과정을 보면 JSX 안에서 왜 if 문을 자유롭게 쓸 수 없는지 이해가 가실 겁니다.
함수 호출 안에서는 if 문을 쓸 수 없기 때문이죠.
Babel을 활용하면 JSX가 어떻게 트랜스파일링되는지 직접 확인해 볼 수 있습니다.
컴파일과 최적화
그렇다면 트랜스파일러와 컴파일러는 어떻게 다를까요?
사실, 이 질문은 누구에게 묻느냐에 따라 답이 달라질 수 있습니다.
컴퓨터 과학 전공자라면 컴파일러를 주로 개발자가 작성한 코드를 기계어(프로세서가 이해하는 이진 코드)로 변환하는 프로그램으로 정의할 것입니다.
하지만 "트랜스파일러"는 "소스-투-소스 컴파일러"라고도 불리고, 최적화 도구는 "최적화 컴파일러"라고도 불립니다.
즉, 트랜스파일러와 최적화 도구는 모두 컴파일러의 일종이라는 것이죠!
이름 짓기는 어렵기 때문에 트랜스파일러, 컴파일러 또는 최적화 도구를 구성하는 요소에 대한 의견 차이가 있을 수 있습니다.
중요한 것은 트랜스파일러, 컴파일러, 최적화 도구가 코드가 포함된 텍스트 파일을 가져와 분석하고, 기능적으로 동일하지만 다른 코드의 새 텍스트 파일을 생성하는 프로그램이라는 점을 이해하는 것입니다.
이러한 도구는 코드를 개선하거나 코드 일부를 다른 사람의 코드 호출로 래핑하여 이전에는 없었던 기능을 추가할 수도 있습니다.
컴파일러, 트랜스파일러, 최적화 도구는 코드를 분석하여, 기능적으로 동일하지만 다른 코드를 생성하는 프로그램입니다.
리액트 컴파일러는 바로 이 마지막 부분을 수행합니다.
작성한 코드와 기능적으로 동일한 코드를 생성하지만, 그 일부를 리액트 개발자가 작성한 코드 호출로 감쌉니다.
이러한 방식으로 코드는 의도한 대로 작동하는 것 외에 더 많은 기능을 수행하는 코드로 다시 작성됩니다.
"더 많은 기능"이 정확히 무엇인지는 곧 살펴보겠습니다.
추상 구문 트리 (AST): 코드의 해부도
코드가 "분석된다"는 것은 컴퓨터가 코드를 문자 단위로 쪼개고, 알고리즘을 통해 코드를 이해하고, 조작하고, 재구성하고, 기능을 추가하는 방법을 알아낸다는 것을 의미합니다.
이 과정에서 주로 추상 구문 트리(AST)가 생성됩니다.
어려운 용어처럼 들리지만, AST는 단순히 코드를 나타내는 데이터 트리입니다.
작성된 코드보다 트리를 분석하는 것이 훨씬 쉽기 때문에 AST를 사용합니다.
예를 들어, 다음과 같은 코드가 있다고 가정해 보겠습니다.
const item = { id: 0, desc: 'hello' };
이 코드에 대한 AST는 다음과 같습니다.
{
type: VariableDeclarator,
id: {
type: Identifier,
name: Item
},
init: {
type: ObjectExpression,
properties: [
{
type: ObjectProperty,
key: id,
value: 0
},
{
type: ObjectProperty,
key: desc,
value: 'hello'
}
]
}
}
생성된 데이터 구조는 작성된 코드를 작은 조각으로 나누어 설명합니다.
각 조각은 타입과 관련 값을 포함합니다.
예를 들어 desc: 'hello'은 키가 'desc'이고 값이 'hello'인 ObjectProperty입니다.
트랜스파일러/컴파일러 등이 코드를 어떻게 처리하는지 이해하려면 이런 멘탈 모델을 갖는 것이 중요합니다.
결국 생성되는 코드는 이 AST와 다른 중간 언어에서 나옵니다.
이 데이터 구조를 반복하고 텍스트를 출력(동일한 언어 또는 다른 언어로 새 코드를 출력하거나 어떤 방식으로든 조정)하는 과정을 상상해 볼 수 있습니다.
리액트 컴파일러는 AST와 중간 언어를 모두 활용하여 새로운 리액트 코드를 생성합니다.
리액트와 마찬가지로 리액트 컴파일러도 누군가 작성한 코드라는 점을 기억하는 것이 중요합니다.
컴파일러, 트랜스파일러, 최적화 도구를 다룰 때는 이러한 도구를 신비로운 블랙박스로 생각하지 마세요.
시간적 여유가 있다면 직접 만들어 볼 수 있는 것으로 생각하세요.
리액트의 핵심 아키텍처
리액트 컴파일러로 넘어가기 전에 몇 가지 더 명확히 해야 할 개념이 있는데요.
리액트의 핵심 아키텍처는 인기의 원천이면서 동시에 잠재적인 성능 문제의 원인이라고 말씀드렸던 것을 기억하시나요?
JSX를 작성할 때 실제로는 중첩 함수 호출을 작성하고 있다는 것을 확인했습니다.
하지만 함수를 리액트에 제공하고, 리액트는 언제 호출할지 결정합니다.
대량의 아이템을 처리하는 리액트 앱의 시작 부분을 살펴보겠습니다.
App 함수는 일부 아이템을 가져오고, List 함수는 아이템을 처리하고 표시한다고 가정해 보겠습니다.
function App() {
// TODO: 여기서 아이템 가져오기
return <List items={items} />;
}
function List({ items }) {
const pItems = processItems(items);
const listItems = pItems.map((item) => <li>{ item }</li>);
return (
<ul>{ listItems }</ul>
)
}
함수는 자식(여기서는 여러 개의 li 객체가 됨)을 포함하는 ul 객체와 같은 일반 자바스크립트 객체를 반환합니다.
ul 및 li와 같은 일부 객체는 리액트에 내장되어 있습니다.
다른 객체는 List와 같이 직접 만드는 객체입니다.
궁극적으로 리액트는 이러한 모든 객체에서 Fiber 트리라는 트리를 구축합니다.
트리의 각 노드를 Fiber 또는 Fiber 노드라고 합니다.
UI를 설명하기 위해 노드의 자바스크립트 객체 트리를 직접 만드는 아이디어를 "가상 DOM" 생성이라고 합니다.
리액트는 실제로 트리의 각 노드에서 분기될 수 있는 두 개의 브랜치를 유지합니다.
하나의 브랜치는 트리의 해당 브랜치의 "현재" 상태(DOM과 일치)이고, 다른 하나는 함수가 재실행될 때 반환된 내용과 일치하는 트리의 해당 브랜치의 "진행 중인 작업" 상태입니다.
리액트는 실제 DOM 트리에 적용할 업데이트를 계산하기 위해 트리의 현재 브랜치와 진행 중인 작업 브랜치를 비교합니다.
이 프로세스를 "조정(reconciliation)"이라고 합니다.
따라서 앱에 추가하는 다른 기능에 따라 리액트는 UI를 업데이트해야 한다고 생각될 때마다 List 함수를 반복적으로 호출할 수 있습니다.
이는 멘탈 모델을 상당히 간단하게 만듭니다.
UI를 업데이트해야 할 때마다(예: 버튼 클릭과 같은 사용자 작업에 대한 응답으로) UI를 정의하는 함수가 다시 호출되고, 리액트는 브라우저의 실제 DOM을 함수가 UI가 표시되어야 한다고 말하는 방식과 일치하도록 업데이트하는 방법을 파악합니다.
하지만 processItems 함수가 느리면 List에 대한 모든 호출이 느려지고, 앱 전체가 느려집니다!
메모이제이션 (Memoization) - 비용이 많이 드는 함수의 호출을 줄이는 마법
비용이 많이 드는 함수에 대한 반복 호출을 처리하기 위한 프로그래밍 솔루션은 함수 결과를 캐싱하는 것입니다.
이 프로세스를 메모이제이션이라고 합니다.
메모이제이션이 작동하려면 함수가 "순수"해야 합니다.
즉, 함수에 동일한 입력을 전달하면 항상 동일한 출력을 얻어야 합니다.
그렇다면 출력을 가져와 입력 세트와 관련된 방식으로 저장할 수 있습니다.
다음에 비용이 많이 드는 함수를 호출할 때 입력을 확인하고, 해당 입력으로 함수를 이미 실행했는지 캐시를 확인하고, 이미 실행했다면 함수를 다시 호출하는 대신 캐시에서 저장된 출력을 가져오는 코드를 작성할 수 있습니다.
해당 입력이 마지막으로 사용되었을 때와 출력이 같을 것이라는 것을 알기 때문에 함수를 다시 호출할 필요가 없습니다.
이전에 사용된 processItems 함수에 메모이제이션이 구현되었다면 다음과 같을 것입니다.
function processItems(items) {
const memOutput = getItemsOutput(items);
if (memOutput) {
return memOutput;
} else {
// ...비용이 많이 드는 처리 실행
saveItemsOutput(items, output);
return output;
}
}
saveItemsOutput 함수는 items와 함수의 관련 출력을 모두 저장하는 객체를 저장한다고 생각할 수 있습니다.
getItemsOutput는 items가 이미 저장되어 있는지 확인하고, 저장되어 있다면 더 이상 작업을 수행하지 않고 관련 캐시된 출력을 반환합니다.
리액트의 함수 반복 호출 아키텍처에서 메모이제이션은 앱이 느려지는 것을 방지하는 데 중요한 기술이 됩니다.
Hook(훅) 저장소
리액트 컴파일러를 이해하기 위해 리액트 아키텍처의 한 가지 더 이해해야 할 부분이 있습니다.
리액트는 앱의 "상태"가 변경되면 함수를 다시 호출하는 것을 고려합니다.
즉, UI 생성이 의존하는 데이터가 변경되는 경우입니다.
예를 들어, 데이터의 한 부분은 true 또는 false인 showButton일 수 있으며, UI는 해당 데이터 값에 따라 버튼을 표시하거나 숨겨야 합니다.
리액트는 클라이언트 기기에 상태를 저장합니다.
어떻게 저장할까요?
아이템 목록을 렌더링하고 상호 작용하는 리액트 앱을 예로 들어 보겠습니다.
나중에 선택한 아이템을 저장하고, 렌더링을 위해 클라이언트 측에서 아이템을 처리하고, 이벤트를 처리하고, 목록을 정렬한다고 가정해 보겠습니다.
앱은 아래와 같이 시작될 수 있습니다.
function App() {
// TODO: 여기에서 일부 아이템 가져오기
return <List items={items} />;
}
function List({ items }) {
const [selItem, setSelItem] = useState(null);
const [itemEvent, dispatcher] = useReducer(reducer, {});
const [sort, setSort] = useState(0);
const pItems = processItems(items);
const listItems = pItems.map((item) => <li>{ item }</li>);
return (
<ul>{ listItems }</ul>
)
}
자바스크립트 엔진이 useState 및 useReducer 줄을 실행할 때 실제로 무슨 일이 일어날까요?
List 컴포넌트에서 생성된 Fiber 트리의 노드에는 데이터를 저장하기 위해 몇 가지 자바스크립트 객체가 더 연결되어 있습니다.
이러한 각 객체는 연결 목록이라는 데이터 구조로 서로 연결되어 있습니다.
그런데 많은 개발자들이 useState가 리액트에서 상태 관리의 핵심 단위라고 생각합니다.
하지만 그렇지 않습니다!
실제로는 useReducer에 대한 간단한 호출을 위한 래퍼입니다.
따라서 useState 및 useReducer를 호출하면 리액트는 앱이 실행되는 동안 유지되는 Fiber 트리에 상태를 연결합니다.
따라서 함수가 계속 재실행될 때 상태를 사용할 수 있습니다.
훅이 저장되는 방식은 루프 또는 if 문 내부에서 훅을 호출할 수 없는 "훅의 규칙"을 설명하기도 합니다.
훅을 호출할 때마다 리액트는 연결 목록의 다음 아이템으로 이동합니다.
따라서 훅을 호출하는 횟수는 일관성이 있어야 합니다.
그렇지 않으면 리액트가 연결 목록의 잘못된 아이템을 가리키는 경우가 발생할 수 있습니다.
궁극적으로 훅은 사용자 기기 메모리에 데이터(및 함수)를 저장하도록 설계된 객체일 뿐입니다.
이는 리액트 컴파일러가 실제로 무슨 일을 하는지 이해하는 데 중요합니다.
하지만 더 자세한 내용이 있습니다.
Hook(훅) 저장소와 결합한 리액트의 메모이제이션
리액트는 메모이제이션의 개념과 훅 저장소의 개념을 결합합니다.
Fiber 트리의 일부인 리액트에 제공하는 전체 함수(List와 같은) 또는 그 안에서 호출하는 개별 함수(processItems와 같은)의 결과를 메모이제이션할 수 있습니다.
캐시는 어디에 저장될까요?
상태와 마찬가지로 Fiber 트리에 저장됩니다!
예를 들어 useMemo 훅은 useMemo를 호출하는 노드에 입력과 출력을 저장합니다.
따라서 리액트에는 이미 Fiber 트리의 일부인 자바스크립트 객체의 연결 목록에 비용이 많이 드는 함수의 결과를 저장하는 아이디어가 있습니다.
한 가지를 제외하고는 훌륭합니다. 바로 유지 관리입니다.
리액트에서 메모이제이션은 메모이제이션이 의존하는 입력을 리액트에 명시적으로 알려야 하기 때문에 번거로울 수 있습니다.
processItems에 대한 호출은 다음과 같이 됩니다.
const pItems = useMemo(processItems(items), [items]);
마지막에 있는 배열은 '종속성' 목록입니다.
즉, 변경될 경우 리액트에 함수를 다시 호출해야 한다고 알리는 입력입니다.
이러한 입력을 올바르게 가져와야 합니다.
그렇지 않으면 메모이제이션이 제대로 작동하지 않습니다.
따라서 유지 관리가 사무적인 잡일이 됩니다.
구원 투수의 등장 리액트 컴파일러
드디어 리액트 컴파일러를 소개할 시간입니다.
리액트 코드를 분석하고 JSX 트랜스파일링을 위한 새로운 코드를 생성하는 프로그램이죠.
하지만 이 새로운 코드에는 몇 가지 특별한 기능이 추가됩니다.
이제 리액트 컴파일러가 우리 앱에 어떤 마법을 부리는지 살펴보겠습니다.
컴파일 전 코드는 다음과 같았습니다.
리액트 코드의 텍스트를 분석하고 JSX 트랜스파일링을 위한 새 코드를 생성하는 프로그램입니다.
하지만 새로운 코드에는 몇 가지 추가 사항이 있습니다.
이 경우 리액트 컴파일러가 앱에 어떤 영향을 미치는지 살펴보겠습니다.
컴파일 전에는 다음과 같았습니다.
function App() {
// TODO: 여기에서 일부 아이템 가져오기
return <List items={items} />;
}
function List({ items }) {
const [selItem, setSelItem] = useState(null);
const [itemEvent, dispatcher] = useReducer(reducer, {});
const [sort, setSort] = useState(0);
const pItems = processItems(items);
const listItems = pItems.map((item) => <li>{ item }</li>);
return (
<ul>{ listItems }</ul>
)
}
컴파일 후에는 다음과 같이 됩니다.
function App() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <List items={items} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function List(t0) {
const $ = _c(6);
const { items } = t0;
useState(null);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {};
$[0] = t1;
} else {
t1 = $[0];
}
useReducer(reducer, t1);
useState(0);
let t2;
if ($[1] !== items) {
const pItems = processItems(items);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (item) => <li>{item}</li>;
$[3] = t3;
} else {
t3 = $[3];
}
t2 = pItems.map(t3);
$[1] = items;
$[2] = t2;
} else {
t2 = $[2];
}
const listItems = t2;
let t3;
if ($[4] !== listItems) {
t3 = <ul>{listItems}</ul>;
$[4] = listItems;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
많이 복잡해졌죠?
다시 작성된 List 함수의 일부를 분석하여 이해해 보겠습니다.
다음과 같이 시작합니다.
const $ = _c(6);
_c 함수("c"는 "cache"를 의미)는 훅을 사용하여 저장되는 배열을 생성합니다.
리액트 컴파일러는 Link 함수를 분석하고 성능을 최대화하기 위해 6가지 항목을 저장해야 한다고 결정했습니다.
함수가 처음 호출될 때 이러한 6가지 항목 각각의 결과를 해당 배열에 저장합니다.
캐시가 작동하는 것은 함수가 후속 호출될 때입니다.
예를 들어, processItems를 호출하는 영역만 살펴보겠습니다.
if ($[1] !== items) {
const pItems = processItems(items);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (item) => <li>{item}</li>;
$[3] = t3;
} else {
t3 = $[3];
}
t2 = pItems.map(t3);
$[1] = items;
$[2] = t2;
} else {
t2 = $[2];
}
processItems 함수 호출과 li 생성을 포함한 전체 작업은 배열의 두 번째 위치에 있는 캐시($[1])가 함수가 마지막으로 호출되었을 때의 입력( List에 전달되는 items의 값)과 동일한지 확인하는 검사로 래핑됩니다.
같으면 캐시 배열의 세 번째 위치($[2])가 사용됩니다.
items를 매핑할 때 생성된 모든 li 목록이 저장됩니다.
리액트 컴파일러 코드는 "마지막으로 이 함수를 호출했을 때와 동일한 아이템 목록을 제공하면 마지막에 캐시에 저장한 li 목록을 제공합니다."라고 말합니다.
전달된 items가 다르면 processItems를 호출합니다.
그런 경우에도 캐시를 사용하여 하나의 목록 항목이 어떻게 표시되는지 저장합니다.
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (item) => <li>{item}</li>;
$[3] = t3;
} else {
t3 = $[3];
}
t3 = 줄이 보이시나요? li를 반환하는 화살표 함수를 다시 만드는 대신 캐시 배열의 네 번째 위치($[3])에 함수 자체를 저장합니
다.
이렇게 하면 다음에 List가 호출될 때 자바스크립트 엔진이 작은 함수를 생성하는 작업을 줄일 수 있습니다.
이 함수는 변경되지 않으므로 초기 if 문은 기본적으로 "캐시 배열의 이 지점이 비어 있으면 캐시하고, 그렇지 않으면 캐시에서 가져옵니다."라고 말합니다.
이러한 방식으로 리액트는 값을 캐시하고 함수 호출 결과를 자동으로 메모이제이션합니다.
출력되는 코드는 작성한 코드와 기능적으로 동일하지만 이러한 값을 캐시하는 코드가 포함되어 있어 리액트에서 함수가 반복적으로 호출될 때 성능 저하를 방지합니다.
리액트 컴파일러는 개발자가 일반적으로 메모이제이션을 사용하는 것보다 더 세분화된 수준에서 캐싱하고 있으며, 자동으로 수행합니다.
즉, 개발자는 종속성이나 메모이제이션을 수동으로 관리할 필요가 없습니다.
코드를 작성하기만 하면 리액트 컴파일러가 캐싱을 활용하여 코드를 더 빠르게 만드는 새 코드를 생성합니다.
리액트 컴파일러가 여전히 JSX를 생성한다는 점은 주목할 만합니다.
실제로 실행되는 코드는 JSX 트랜스파일링 후 리액트 컴파일러의 결과입니다.
자바스크립트 엔진에서 실제로 실행되는(브라우저 또는 서버로 전송되는) List 함수는 다음과 같습니다.
function List(t0) {
const $ = _c(6);
const {
items
} = t0;
useState(null);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {};
$[0] = t1;
} else {
t1 = $[0];
}
useReducer(reducer, t1);
useState(0);
let t2;
if ($[1] !== items) {
const pItems = processItems(items);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = item => _jsx("li", {
children: item
});
$[3] = t3;
} else {
t3 = $[3];
}
t2 = pItems.map(t3);
$[1] = items;
$[2] = t2;
} else {
t2 = $[2];
}
const listItems = t2;
let t3;
if ($[4] !== listItems) {
t3 = _jsx("ul", {
children: listItems
});
$[4] = listItems;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
리액트 컴파일러는 캐싱 값에 대한 배열과 필요한 모든 if 문을 추가했습니다.
JSX 트랜스파일러는 JSX를 중첩 함수 호출로 변환했습니다.
작성한 코드와 자바스크립트 엔진이 실행하는 코드 사이에는 상당한 차이가 있습니다.
원래 의도와 일치하는 것을 생성하기 위해 다른 사람의 코드를 신뢰하고 있습니다.
프로세서 사이클과 기기 메모리의 트레이드 오프: 신중한 선택
일반적으로 메모이제이션과 캐싱은 처리를 메모리와 교환하는 것을 의미합니다.
프로세서가 비용이 많이 드는 작업을 실행하지 않아도 되지만, 메모리에 항목을 저장하기 위한 공간을 사용하여 이를 방지합니다.
리액트 컴파일러를 사용한다는 것은 기기 메모리에 "가능한 한 많이 저장"하라는 것을 의미합니다.
브라우저에서 사용자 기기에서 코드가 실행되는 경우 고려해야 할 아키텍처적 고려 사항입니다.
많은 리액트 앱에서는 실질적인 문제가 되지 않을 것입니다.
하지만 앱에서 많은 양의 데이터를 처리하는 경우 리액트 컴파일러가 실험 단계를 벗어나면 기기 메모리 사용량을 최소한 인지하고 주시해야 합니다.
추상화와 디버깅: 멘탈 모델의 중요성
모든 형태의 컴파일은 작성하는 코드와 실제로 실행되는 코드 사이에 추상화 계층을 추가합니다.
리액트 컴파일러의 경우에서 볼 수 있듯이 브라우저에 실제로 전송되는 내용을 이해하려면 코드를 가져와 리액트 컴파일러를 통해 실행한 다음 해당 코드를 가져와 JSX 트랜스파일러를 통해 실행해야 합니다.
코드에 추상화 계층을 추가하면 단점이 있습니다. 코드를 디버깅하기 어렵게 만들 수 있습니다.
그렇다고 해서 추상화 계층을 사용하지 말아야 한다는 것은 아닙니다.
하지만 디버깅해야 하는 코드가 단순히 자신의 코드가 아니라 도구가 생성하는 코드라는 점을 명확하게 염두에 두어야 합니다.
추상화 계층에서 생성된 코드를 디버깅하는 능력에 진정한 차이를 만드는 것은 추상화에 대한 정확한 멘탈 모델을 갖는 것입니다.
리액트 컴파일러의 작동 방식을 완전히 이해하면 컴파일러가 작성하는 코드를 디버깅할 수 있으므로 개발 경험이 향상되고 개발 생활의 스트레스가 줄어듭니다.
'Javascript' 카테고리의 다른 글
| URL 쿼리 스트링과 React 리액트 상태 관리의 만남: `nuqs` 사용법 가이드 (1) | 2025.02.09 |
|---|---|
| Vite React vs Next.js: 더 나은 선택은? Hono와 함께하는 최신 웹 개발 트렌드 (0) | 2025.02.09 |
| React useState 업데이트 함수의 비밀: 최신 상태를 참조하는 마법 (디버깅으로 더 쉽게!) (1) | 2025.01.22 |
| TypeScript 성능 향상의 비밀: tsconfig.json 마스터하기 (1) | 2025.01.21 |
| 타입스크립트 Enum 정복: 실전 가이드 & 완벽 대안 (1) | 2025.01.21 |