※ 다음 글은 해당 포스트의 번역본입니다.
리액트는 별도로 성능 최적화를 시키지 않아도 자체적으로 성능을 어느 정도 최적화합니다. React.memo
는 렌더링 되는 리액트 컴포넌트들의 최적화가 더 잘 이뤄지도록 도와줍니다.
이 글에서는 React.memo
를 활용하여 리액트의 성능을 향상시키는 방법과, 당신이 맞닥뜨릴 수 있는 흔한 에러들, 그리고 React.memo
를 사용해선 안 되는 경우에 대해 알아볼 것입니다.
React.memo
는 순수 함수 컴포넌트와 훅의 렌더링 성능을 향상시키는 기능입니다.
Memo는 메모이제이션(memoization)에서 유래됐습니다. React.memo
로 감싸진 함수의 결과값은 메모리에 저장되는데, 같은 인풋값으로 해당 함수를 재호출할 시, 저장해 둔 결과값을 반환해 줍니다.
순수 함수에 사용되기 때문에, 만일 인수가 바뀌지 않는다면, 결과값 또한 바뀌지 않습니다. React.memo
는 함수로 하여금 이러한 경우에서의 실행을 방지합니다.
이 링크에서 시각화 된 예시를 볼 수 있습니다.
파란색 컴포넌트는 세 개의 하위 컴포넌트를 가지고 있습니다. 한 개의 인풋란과 2개의 검정색 타일이 있습니다. 노란색은 각 컴포넌트의 페인트 개수를 나타냅니다.
인풋란에 무언가를 입력하면, 키보드를 입력할 때마다 왼쪽의 검정색 컴포넌트와 파란색 컴포넌트의 내용은 변경되지만 오른쪽 컴포넌트는 변경되지 않는 걸 볼 수 있습니다.
오른쪽 타일은 프롭스가 바뀌지 않는 경우 렌더링 되는 것을 막기 위해 React.memo
로 감싸져 있습니다.
기본 사용 방법은 다음과 같습니다. 함수 컴포넌트를 React.memo
함수로 감싸주면 됩니다.
const Tile = React.memo(() => {
let eventUpdates = React.useRef(0);
return (
<div className="black-tile">
<Updates updates={eventUpdates.current++} />
</div>
);
});
이러한 작은 변경은 컴포넌트의 렌더링 성능을 향상시키는 데에 도움을 주겠지만, 복잡한 컴포넌트 처리에 있어선 몇 가지 이슈들을 발생시킬 수 있습니다.
React.memo
가 예상대로 작동하지 않는다구요? React.memo
를 사용하기 시작하면서 마주할 수 있는 몇 가지 흔한 에러들을 소개합니다.
const App = () => {
const updates = React.useRef(0);
const [text, setText] = React.useState('');
const data = { test: 'data' };
return (
<div className="app">
<div className="blue-wrapper">
<input
value={text}
placeholder="Write something"
onChange={(e) => setText(e.target.value)}
/>
<Updates updates={updates.current++} />
<Tile />
<TileMemo data={data} />
</div>
</div>
);
};
별안간 React.memo
가 작동을 멈추고, 키를 입력할 때마다 컴포넌트가 렌더링 되는 걸 볼 수 있습니다.
React.memo
가 더 이상 작동하지 않는 이유는 이것이 컴포넌트 속성을 얕은 수준으로 비교하기 때문입니다. data
변수는 App
이 업데이트 될 때마다 재 선언됩니다. 그러므로 이 객체는 이전과 다른 참조값을 갖게 되므로 이전과 같은 객체라고 볼 수 없게 됩니다.
areEqual
로 해결하기React.memo
는 두 번째 인자를 통해서 해당 문제를 위한 해결책을 제공합니다. 이 두 번째 인자는 areEqual
이라는 함수를 받는데, 이는 컴포넌트를 언제 업데이트 해야할 지를 컨트롤 하는 데에 사용됩니다.const TileMemo = React.memo(() => {
let updates = React.useRef(0);
return (
<div className="black-tile">
<Updates updates={updates.current++} />
</div>
);
}, (prevProps, nextProps) => {
if (prevProps.data.test === nextProps.data.test) {
return true; // 프롭스가 같다면
}
return false; // 프롭스가 다르다면 -> 컴포넌트를 업데이트
});
React.useMemo
위 문제에 대한 대안으로는 React.useMemo()
를 사용해서 객체를 감싸는 방법이 있습니다. 이렇게 했을 경우, 변수를 메모이제이션하여 새로운 객체를 생성하지 않도록 합니다.
const data = React.useMemo(() => ({
test: 'data',
}), []);
useMemo
의 두 번째 인자는 변수의 의존성 배열입니다. 이들 중 하나라도 값이 변경되면, 리액트는 값을 재연산합니다.
예제에서는 빈 배열이므로 그러한 일은 일어나지 않을 겁니다.
onClick
함수는 App
이 업데이트 될 때마다 선언됩니다. 그렇게 되면, TileMemo
는 참조가 변경되었으므로 onClick
도 변경됐다고 생각하게 되죠.const App = () => {
const updates = React.useRef(0);
const [text, setText] = React.useState('');
const onClick = () => {
console.log('click');
};
return (
<div className="app">
<div className="blue-wrapper">
<input
value={text}
placeholder="Write something"
onChange={(e) => setText(e.target.value)}
/>
<Updates updates={updates.current++} />
<Tile />
<TileMemo onClick={onClick} />
</div>
</div>
);
};
React.useCallback
const onClick = React.useCallback(() => {
console.log('click');
}, []);
여기서의 두 번째 인자도 의존성 배열입니다. 배열 내의 데이터가 변경되는 경우 계산을 유발합니다.
React.memo
를 사용하면 안 되는 이유?당신은 React.memo
에 아무 단점도 없기에 모든 함수 컴포넌트를 React.memo
로 감싸야겠다고 생각할 수도 있습니다. 문제는 React.memo
는 함수를 캐시에 저장한다는 겁니다. 이 말인 즉슨, 메모리에 결과값이 저장된다는 거죠.
이러한 다량 혹은 대량의 컴포넌트들을 저장하는 것은 더 많은 메모리 소비로 이어질 수 있습니다. 그러므로 크기가 큰 컴포넌트를 메모이제이션할 때는 주의해야 합니다.
컴포넌트의 프롭스가 자주 바뀌는 경우에도 Reacrt.memo
의 사용을 주의해야 합니다. React.memo
는 프롭스와 메모이제이션 된 프롭스를 비교하는 데에 추가적인 간접비을 소비합니다. 이는 성능에 크게 영향을 미칠 뿐만 아니라 React.memo
를 통해 얻어낼 수 있는 성능 최적화 또한 잃게 되는 겁니다.
리액트는 이미 매우 효율적으로 랜더링 성능을 최적화합니다. 불필요한 리렌더링을 최적화하기 위해 시간을 낭비해서는 안 됩니다. 첫 번째 단계는 언제나 성능의 병목화 현상을 측정하고 확인하는 단계가 되어야 합니다. 어떤 컴포넌트가 가장 많이 렌더링 되는지 확인하기 이전에 리액트 앱의 개요를 파악하는 것도 좋은 아이디어입니다. 이러한 컴포넌트에 React.memo
를 적용해야 큰 영향을 가질 수 있습니다.