우선 우리가 한가지 짚고 넘어가야 할 것은 re-evaluating과 re-rendering은 완전히 동일한 개념이 아니다 라는 사실이다.
즉, 함수 컴포넌트가 재실행(re-execute)되고 재평가(re-evaluate)된다고 해서 무조건 리렌더링(re-render)이 일어나는 것이 아니다. 재실행 되는 것은 컴포넌트이고, 리렌더링은 실제 DOM에서 일어나는 변화이다
☑️ 리액트의 기본 동작 방식
- Re-evaluate (재평가):
- 상태나 속성이 변경되면 컴포넌트를 다시 계산해서 새로운 가상 DOM을 만든다.
- Diffing (비교):
- 새로 생성된 가상 DOM과 이전 가상 DOM을 비교해서 어떤 부분이 변했는지 찾는다.
- Re-render (재렌더링):
- 변경된 부분만 실제 DOM에 반영하고, 화면을 다시 그린다.
React는 state, props, context 등이 변화하면 함수 컴포넌트를 재실행(re-execute) 하여, 위에서부터 차례로 재평가(re-evaluate)한다. 이렇게 재평가한 결과는 ReactDOM에게 전달되고, ReactDOM은 virtual DOM을 이용해서 전후 비교를 한 이후에, 바뀐 부분만 real DOM 에 반영하는 리렌더링을 일으킨다. 즉, 실행과 평가의 주체는 React이고 그 대상은 컴포넌트이며, 리렌더링의 주체는 ReactDOM이고 그 대상은 DOM이다. 서로 연관되어 있기는 하지만, 같은 의미도 아닐 뿐더러 꼭 함께 일어난다고 말할 수 없다.
(하지만 많은 사람들이 re-rendering을 re-evaluate(재평가) 과정까지 포함하는 더 포괄적인 용어로 사용한다. 그리고 real DOM을 업데이트 하여 re-rendering 하는 것을 repaint 등으로 부를 수 있다. 그래서 이는 문맥과 상황에 맞게 받아들여야 한다)
☑️ React.momo
리액트의 공식 문서를 보면, React.memo에 대한 설명은 다음과 같다:
(memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 re-rendering을 건너뛸 수 있다)
여기서 말하는 Re-rendering을 건너뛴다는 말은 사실 정확히 말하면 Re-evaluate를 건너뛴다는 뜻이야. React.memo는 안 바뀐 것이 확실한 부분에 대해서는 리액트가 다시 계산하거나 비교하지 않도록 해주는 역할을 한다.
처음에 React.momo 공부할 때 헷갈렸던 부분이 리액트는 애초에 비교 후 변화된 부분이 없으면 랜더링을 안해 최적화를 해주는데 왜 굳이 React.memo를 추가적으로 사용해야 할까? 라고 생각했다
근데 알고보니 React.memo는 불필요한 Re-evaluate 자체를 막아주는 역할을 한다.
부모 컴포넌트가 다시 렌더링될 때, 자식 컴포넌트의 속성(props)이 변하지 않았다면, 자식 컴포넌트는 당연히 바뀐게 없기 떄문에 자식 컴포넌트를 다시 평가(Re-evaluate)할 필요가 없다.
따라서 React.memo는 props가 변화하였을 때만 해당 컴포넌트를 재실행 및 재평가 하도록 컴포넌트를 메모이제이션(memoization) 한다. 즉, React.memo는 컴포넌트를 메모이제이션(memoization)하여 이전 props와 새 props가 동일하면 컴포넌트를 다시 평가하지 않도록 최적화해준다.
[주의]
React.memo를 사용할 때 주의해야 할 한 가지는 함수를 props로 전달하는 경우다. 함수는 참조 타입(reference value)이기 때문에, 매번 새로운 함수 인스턴스가 생성되고 메모리 주소가 달라지기 때문에 React.memo가 이를 다른 값으로 인식하게 된다.따라서 props가 그대로인 것처럼 보여도 함수가 포함되어 있다면 매번 다른 함수로 인식하게 되어, React.memo가 적용되지 않을 수 있다.
이 문제를 해결하기 위해서는 useCallback을 함께 사용해 함수를 메모이제이션하는 것이 필요하다.
☑️ useCallback
useCallback은 함수를 메모이제이션하여, 같은 함수가 불필요하게 새로 생성되지 않도록 최적화해준다. 이렇게 함으로써 함수가 props로 전달될 때 매번 새로운 함수가 생성되는 것을 방지하고, 불필요한 Re-evaluate를 줄일 수 있다.
예시 1: React.memo만 사용한 경우
import React, { useState } from 'react';
// 자식 컴포넌트, React.memo로 감싸져 있음
const Child = React.memo(({ onClick }) => {
console.log("Child component rendered");
return <button onClick={onClick}>Click me!</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Button clicked!");
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<Child onClick={handleClick} /> {/* Child에 함수 전달 */}
</div>
);
}
export default Parent;
부모 컴포넌트가 렌더링될 때마다, handleClick 함수가 새로운 함수로 인식되기 때문에, Child 컴포넌트는 다시 렌더링된다
예시 2: React.memo와 useCallback을 함께 사용한 경우
import React, { useState, useCallback } from 'react';
// 자식 컴포넌트, React.memo로 감싸져 있음
const Child = React.memo(({ onClick }) => {
console.log("Child component rendered");
return <button onClick={onClick}>Click me!</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// useCallback으로 handleClick 함수 메모이제이션
const handleClick = useCallback(() => {
console.log("Button clicked!");
}, []); // 빈 배열이므로 처음에 한 번만 생성됨
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<Child onClick={handleClick} /> {/* Child에 메모이제이션된 함수 전달 */}
</div>
);
}
export default Parent;
☑️ 최적화를 남발하면 안 되는 이유
최적화는 분명히 유용하지만, 모든 컴포넌트에 적용할 필요는 없다. 최적화에도 비용이 따르기 때문이다.
React.memo로 최적화 한 컴포넌트의 경우, 기존 props 와 새로운 props 의 값을 비교해야한다. 그러기 위해서 기존의 props 값을 저장할 공간 또한 필요하다. 즉, 최적화란, 컴포넌트를 재평가하는 데 필요한 성능 비용과 props 를 비교하는 성능의 비용을 서로 맞바꾸는 것이다.
자식 컴포넌트가 매우 많고, 많이 겹쳐있는 상황이라면 최적화를 통해서 비용을 아낄 수 있겠지만, 매우 작은 앱이나 매우 작은 앱이나 간단한 컴포넌트 트리에서는 React.memo의 효과가 미미할 수 있다. 따라서 꼭 필요한 컴포넌트에만 선택적으로 적용하는 것이 좋다.
'React' 카테고리의 다른 글
[React] JSX Key 속성 이해하기 (1) | 2024.09.12 |
---|---|
[React] Hooks란? (3) | 2024.09.09 |
[React] 리액트에서 불변성(Immutability) 관리의 중요성과 방법 (0) | 2024.08.21 |
[React] State란? (0) | 2024.08.20 |
[React] Props란? (0) | 2024.08.16 |