Why?
지난 글 "컴포넌트 Re-rendering을 피하는 5가지 방법"에서 언급한 글에서 부족한 면들이 보여, 보완을 하기 위해 같은 주제를 가진 다른 글들을 살펴 보았습니다. 물론 공식문서를 이미 정독을 한 상황 이었고, 조금은 Advanced 한 내용들에 초점을 맞추어 검색을 해 보았지만, 입맛에 맞는 내용들을 찾지 못하였습니다. 그러던 도중 "Understanding re-rerendering and memoiztion in React"라는 제목에 글에 이끌리게 되었고, 짧은 글이었지만 핵심만을 전달하는 좋은 글이라고 생각되어 공유하게 되었습니다. 이 글은 약간의 팁과 더불어 왜 중요한지에 대한 당위성도 포함되어 있는 유익한 글이라 판단됩니다.
만약 지난 글 "컴포넌트 Re-rendering을 피하는 5가지 방법"이 궁금하시다면 아래의 링크를 클릭해주세요. 그럼 시작해 보겠습니다.
* 이 글은 Understanding re-rendering and memoization in React 번역하였습니다.
Motivation
대체적으로 리렌더링은 컴포넌트의 state 또는 props가 변경되었을 대 일어납니다. 메모이제이션 되지 않은 하나의 컴포넌트가 리렌더링 되면, 모든 자식 컴포넌트 또한 리렌더링됩니다. 물론 예외 상황도 있지만, 대부분은 해당 사항에 포함됩니다.
이 글을 확실하게 이해하기 위해서는 React가 언제 컴포넌트를 리렌더 하는지에 대해 알아야 합니다. 아래의 글 "When React re-renders components"은 보다 나은 해당 정보를 제공합니다. 관심있으신 분들은 확인해주세요.
useRef
React 개발의 공통적인 실수는 렌더링 될 때 유지 할 언제든지 변할 수 있는 값에 useState를 사용하는 것입니다. useState는 렌더링 된 출력이 값에 따라 달라지는 경우 좋은 방법이지만, 그렇지 않은 경우에선 useRef가 더 최적인 방법이 될 것입니다. 아래의 예시를 살펴 봅시다.
const [firstName, setFirstName] = useState();
return (
<form onSubmit={() => alert(firstName)}>
<input onChange={(e) => { setFirstName(e.target.value) }} />
<button>Submit</button>
</form>
);
위에 예시를 살펴보면, 사용자가 입력을 할 때, firstName state는 변경됩니다. state가 변경 될 때마다 리렌더링이 발생합니다. 즉 사용자가 입력을 할 때마다 리렌더링이 발생한다고 할 수 있습니다.
firstName은 렌더링된 출력에서 사용되지 않기 때문에 useRef를 사용하여 리렌더링이 일어나는 것을 방지할 수 있습니다.
const firstName = useRef();
return (
<form onSubmit={() => alert(firstName.current)}>
<input onChange={(e) => { firstName.current = e.target.value}}/>
<button>Submit</button>
</form>
);
Memo
React를 최적화하기 위한 중요한 부분으로는 메모이제이션을 꼽을 수 있습니다. 메모이제이션은 함수의 결과 값을 캐싱을 하고 후속 요청을 위해 캐싱된 값을 반환하는 일련의 절차입니다.
컴포넌트를 리렌더링하는 것은 단순하게 이야기하면 컴포넌트 함수를 다시 호출하는 것과 같습니다. 만약 컴포넌트가 자식 컴포넌트를 가지고 있다면, 다수의 컴포넌트 함수들을 호출한 것과 같습니다. 트리의 밑바닥에 있는 컴포넌트까지 호출하는 것이죠. 그런 다음 결과를 DOM과 비교하여 UI를 업데이트 해야 할지에 대한 여부를 결정합니다. 이런 차이를 계산하고 조정하는 것을 reconcilation이라 합니다.
컴포넌트들은 단지 함수이므로, React.memo()를 사용하여 기억할 수 있습니다. 이는 의존되는 것들(props)이 변경되지 않는 이상 리렌더링되는 컴포넌트들을 방지합니다. 만약에 거대한 컴포넌트를 가지고 있다면, 메모이제이션하는 것은 굉장히 좋은 방법이라고 할 수 있습니다. 그러나 모든 컴포넌트에 사용하는 것은 바람직하지 않습니다. 메모이제이션은 메모리를 사용하며 특정 경우에는 성능이 떨어질 수 있습니다.
컴포넌트를 메모이제이션 할 때, 리렌더링 대신 React는 컴포넌트의 새로운 props와 이전 props를 비교합니다. 여기서 고려해야 할 trade off는 props끼리를 비교하는지, 함수를 비교하는 것인지에 대한 부분입니다. 만약 props 안에 거대한 객체가 있고, 이를 비교해야 한다면 컴포넌트를 메모이제이션 하는 것은 낮은 성능을 보일 수 있습니다.
const HeavyComponent: FC = () => { return <div/>}
export const Heavy = React.memo(HeavyComponent);
UseCallback
컴포넌트에 대해 불 필요한 리렌더링을 방지할 수 있는 중요한 기능 중 하나인 useCallback에 대해 살펴보겠습니다. 이미 메모제이징된 컴포넌트에 useCallback을 사용하지 않은 채로 일반 함수를 전달하게 되면 메모이징된 효과를 제거할 수 있습니다. 그 이유는 referential equality 때문입니다. 이전에 언급했던 것 처럼, 리렌더링이 될때 컴포넌트들의 함수를 호출합니다. 이 말은 즉슨, 컴퍼넌트 안에 함수를 선언 하였다면, 리렌더링이 될 때마다 새로운 함수가 만들어진다는 것입니다. 만약 이미 메모이제이션 된 컴포넌트에 함수를 props로 전달했을 시 함수의 내용이 실제로 변경되지 않더라도, 참조가 변경되어 자식 컴포넌트들은 리렌더링 됩니다.
export const ParentComponent = () => {
const handleSomething = () => {};
return <HeavyComponent onSomething={handleSomething} />
};
위 예시를 보면 해당 컴포넌트들이 메모이제이션이 되었다고 하더라도, handleSomething 함수를 prop으로 내려주는 한 ParentComponent은 리렌더링 될 것이고 이에 HeavyComponent도 마찬가지로 리렌더링 됩니다. 이런 상황에서 해결할 수 있는 방법이 usecallback을 사용하는 것입니다. 이는 참조가 변경되는 것을 방지합니다.
export const ParentComponent = () => {
const handleSomething = useCallback(() => {}, []);
return <HeavyComponent onSomething={handleSomething} />
};
자세한 사항은 아래의 링크를 클릭해주세요.
UseMemo
리렌더링은 컴포넌트의 함수를 다시 호출한다는 의미와 같다라는 것을 알게 되었습니다. 이 말은 즉슨, 컴포넌트 안에 heavy한 함수(API 호출 등)가 포함되어 있다면 리렌더링이 될 때마다 그 함수 또한 계속 호출된다는 의미와 일맥상통합니다. 이런 상황을 타개하기 위해 리렌더링이 되는 heavy한 함수를 메모이제이션 해주어야 합니다. 첫 번째 렌더에서는 그 함수를 호출하고 다음 리렌더에서는 함수의 캐싱된 값을 반환을 받는 것이 그 어떠한 것보다 나은 방법이라고 생각됩니다.
useMemo는 이러한 문제를 해결할 수 있는 좋은 hook입니다. 사용하는 방법은 매우 단순합니다.
const value = useMemo(() => expensiveFunction(aDep), [aDep]);
위에 예시와 같이 값을 캐시하고, aDep이 변경될때에만 업데이트 됩니다.
자세한 사항은 아래의 링크를 클릭해주세요.
UseState lazy initialization
useState의 조금은 덜 알려진 기능 중 초기 state을 느리게 설정할 수 기능이 있습니다. 함수를 useState로 전달하게 되면, 그 함수는 컴포넌트가 처음으로 렌더될때에만 호출이 됩니다. 이는 리렌더링할 때마다 초기 값이 설정되는 것을 방지할 수 있습니다. 이는 초기 상태가 계산적으로 heavy한 경우에 유용하게 사용될 수 있습니다. 하지만 heavy하지 않은 경우에는 불 필요한 방법이라 생각됩니다.
const initialState = () => calculateSomethingExpensive(props);
const [count, setCount] = useState(initialState);
Conclusion
해당 주제를 가진 글들이 무수히 많다는 걸 알고 있지만, 그럼에도 불구하고 이 글이 조금 더 정확하면서 더 나은 도움을 주었으면 좋겠습니다. 만약 더 깊이 알아보고 싶다면 아래의 글을 참고해 보세요. 감사합니다.
< 참고자료 >
[사이트] #medium
Understanding re-rendering and memoization in React end
'Language & Framework & Library > React' 카테고리의 다른 글
useEffect 완벽 가이드 1편 (1) | 2023.10.29 |
---|---|
Design Patterns - Compound component pattern (0) | 2023.10.21 |
컴포넌트 Re-rendering을 피하는 5가지 방법 (0) | 2022.03.13 |
React vs Vue (2) | 2021.07.13 |
React의 컴포넌트 수명주기 (0) | 2021.02.12 |