일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 알고리즘
- typescript
- CSS방법론
- React-Router-Dom
- framer
- 시스템디자인
- 테오의 스프린트
- VS Code
- framer-motion
- 타입스크립트
- 글또 10기
- JUNCTION2023
- 캐나다취준
- 코드트리
- useState
- Semantic Versioning
- SemVer
- 회고
- 글또
- JSBridge
- 개발자 원칙
- 캐나다개발자
- Effective Typescript
- react
- TS
- 이펙티브타입스크립트
- CSS
- ASP.NET
- 개발자를 위한 글쓰기 가이드
- Framer motion
- Today
- Total
큰 꿈은 파편이 크다!!⚡️
리액트 성능 향상 첫걸음: useMemo, useCallback, React.memo 본문
들어가며: 나의 기술부채 💰
리액트를 배울 때 정말 기본으로 포함되어있는 useMemo
, useCallback
훅과 React.memo
는 재사용과 리렌더링 방지를 통한 성능 향상을 야기한다고 알려진 함수들이다. 이 주제에 대해 처음 학습할 때 성능 향상에 대한 효과까지는 이해를 했었으나 항상 성능을 향상시키는 것이 아니라는 글을 보고서는 이해가 안되어서 그냥 사용을 안해버렸는데.. 이번 기회를 통해 간단하게나마 전체적인 정리를 해보려 한다.
useMemo: 연산 값 재사용
리액트에서 제공하는 useMemo
훅은 메모이제이션Memoization 이라는 기법을 통해 기존에 계산했던 결과를 저장하여 필요 시 재사용하므로써 불필요한 연산을 줄여 성능을 향상시킨다.
반대로, 항상 바뀌는 값이라면 결과를 재사용할 일이 없으니 useMemo
를 사용할 필요가 없다.
1. 문제 상황 예시: 팩토리얼
· 재귀 팩토리얼 연산 factorialOf
을 수행할 때마다 함수가 호출되었음을 콘솔에 출력한다.
· Re-render
버튼을 누르면 실제로 사용되지 않는 inc
값을 증가시켜 <CalculateFactorial />
컴포넌트를 리렌더링하는데, 콘솔에 출력되는 결과를 통해 input 값을 변경하지 않았음에도 factorialOf
연산을 한다는 것을 알 수 있다.
· 즉, 이미 계산했기 때문에 계산할 필요가 없는 상황에서 계산을 하고 있다. 이전에 계산한 값을 재사용하고 싶지 않은가? 😎
2. 이때 useMemo를!
const cachedValue = useMemo(calculateValue, dependencies)
useMemo
는 두 개의 인자를 받는다.
· calculateValue
: 인자가 없는 연산 함수 (ex. () =>
)
· dependencies
: 연산 함수에 쓰이는 값들 (다시 연산해야 할 때의 기준이 되는 값)
· cachedValue
: useMemo
의 첫번째 인자인 함수를 호출했을 때의 결과
이 모양을 팩토리얼 예시에 적용해보면 아래와 같다.
const factorial = useMemo(() => factorialOf(number), [number]);
3. 문제 해결
숫자를 바꿔가며 팩토리얼 계산을 하다가 Re-render
버튼을 눌러도 더이상 로그가 찍히지 않는다. 계산을 다시 하지 않는다는 뜻이다!
useCallback: 함수 재사용
useCallback
은 함수를 재사용하는데 도움을 주는 훅이다.
함수도 결국 값이다. useMemo
로도 함수를 재사용할 수 있다. 따라서 useCallback
은 useMemo
에서도 사용할 수 있는 방법을 리팩토링한 정도로 이해할 수 있다.
//✅ useMemo
const handleSubmit = useMemo(() => {
return (orderDetails) => { //👈 함수 한번 더 감싸는 것 뿐..!
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
//✅ useCallback
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
이러한 특성 때문에 공식 문서에서는 useCallback
을 아래와 같이 표현한다.
The only benefit to useCallback is that it lets you avoid writing an extra nested function inside. It doesn’t do anything else.
따라서 useCallback
의 모양은 이렇다.
const cachedFn = useCallback(fn, dependencies)
· cachedFn
: 캐싱된 fn
함수
· fn
: 캐싱할 함수 (호출이 아님!)
· dependencies
: fn
함수 안에서 참조되는 값의 배열
React.memo
memo는 컴포넌트의 props를 비교하여, 변경된 경우에만 리렌더링을 하도록 한다.
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
· SomeComponent: 메모할 컴포넌트
· arePropsEqual?: 이전 props와 새로운 props를 인자로 받아, 같으면 true, 아니면 false를 반환하는 함수
메모할 컴포넌트를 감싸는 방식으로 코드를 작성한다.
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});
단, 항상 성능을 향상시키는 것은 아니다 👀
Every line of code which is executed comes with a cost.
성능을 향상시키기 위한 코드에도 비용cost이 있다. 개선 효과가 미미하다면 개선을 고민하고 적용하는 시간은 낭비가 된다.
따라서 이 세 가지 방법은 컴포넌트의 성능을 실제로 개선시킬 수 있는 경우에만 사용하자
두 가지 중 성능이 더 좋은 코드는 무엇일까? 그 이유는?
//✅ original
const dispense = (candy) => {
setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}
//✅ useCallback
const dispense = React.useCallback((candy) => {
setCandies((allCandies) => allCandies.filter((c) => c !== candy));
}, [])
위 코드를 약간 리팩토링해보자.
//✅ original
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
//✅ useCallback
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])
정답은 original 코드를 사용하는 것이다!
코드를 보면 useCallback
을 사용하는 경우가 React.useCallback
를 호출하고, []
를 정의하므로서 조금 더 많은 일을 하는 것을 알 수 있다. 두가지 경우 모두 해당 컴포넌트를 렌더링 할 때마다 메모리에 함수를 할당하지만, useCallback
이 구현된 방식에 따라 더 많은 메모리 할당이 일어날 수 있다.
덧붙여 위 경우에서는, 컴포넌트가 두 번째 렌더링 될 때 original 버전에서는 기존 dispense
함수가 가비지 콜렉트 되며 메모리 공간을 해제하고, 새로운 dispense
함수를 생성한다. useCallback
버전에서는 가비지 콜렉션 없이 새로운 dispense
함수를 생성하게 되어 (props가 바뀌기 전까지는 값을 캐싱해야 하는 useCallback
의 특성 때문이다) 비효율적이다.
useCallback, useMemo
· (앞서 말했듯이,) deps 배열에 항상 바뀌는 값(새로운 객체, 배열 등..)을 넣는 것은 값을 재사용할 일이 없다는 뜻이므로 메모이제이션을 하는 의미가 없다.
· 코드를 복잡하게 만들 수 있다
React.memo
· 항상 props를 비교하게 되는 비용이 있다는걸 생각하자
· 실제로 렌더링을 줄일 수 있는 상황에서만 사용하자
맺으며:
자료조사를 하며 리액트를 많이 사용하고 있지만 모르는 부분이 훨씬 더 많다는 것을 깨달았다. 한편으로는 성능 개선이라는 작업은 이해도가 꽤 있어야 할 수 있는 것이라는 생각이 들었다.
아주 오랜 시간동안의 숙원이었던 기술부채를 조금은 정리한 기분이 든다. 한번쯤은 실전에서 실제로 성능을 개선하기 위해 사용해보고 싶다.
Reference
'Web FE' 카테고리의 다른 글
리액트의 철학 (0) | 2023.05.07 |
---|---|
SSR 개발 중 만난 실전 디자인 패턴 PRG (ft. ASP.NET) (0) | 2023.04.09 |
리액트 앱 배포 후 Cache 문제 🧹 삽질기 (0) | 2023.02.26 |
이펙티브 타입스크립트 🦅 2장 (3) (0) | 2022.10.08 |
이펙티브 타입스크립트 🦅 2장 (2) (0) | 2022.09.28 |