"useMemo와 useCallback이 왜 필요한가?"를 먼저 생각해보겠습니다.
리액트 내장 훅인 이 두 함수가 우리에게 편리함을 제공해준다면, 분명히 필요성이 제시되었기 때문에 누군가 개발했다는 것을 의미를 합니다. 가장 핵심적인 키워드는 '렌더링 최적화'입니다.
메모이제이션은 일종의 캐싱과 같습니다. 특정 연산을 반복해야 할 때 사용되는 기법으로, 반복되는 결과를 메모리에 저장하여 중복되는 연산 없이 빠른 실행을 가능하게 해 줍니다.
(캐시(cache) : 데이터나 값을 미리 복사해놓는 임시 장소를 의미합니다. 캐시는 접근 시간에 비해 원래 데이터에 접근하는 시간이 오래 걸리는 경우나 연산을 반복해야 하는 시간을 절약하고 싶을 때 사용됩니다. 데이터를 미리 복사해놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터 접근이 가능합니다.)
다만 메모이제이션 기법은, 결국 메모리에 특정한 값을 저장하는 것이기 때문에, 정말 필요한 경우가 아닌데 남용하는 경우에는 오히려 성능을 저하시킬 수 있으니, 정말 필요한 경우에만 사용해야합니다.
그러므로 어떠한 학습적인 용도가 있는게 아닌이상, 눈에띄는 성능저하가 발생하는 경우에만 사용하도록 합시다.
사용해야할 경우를 간단하게 정리하자면, 성능저하가 눈에띌정도의 값비싼 함수의 결과값을 저장해야하는 경우, 여러곳에서 재사용이 필요한 경우, 종속성배열의 값이 자주 변경되지 않는경우.
useMemo(() => { /* 로직 */ }, []);
const [sum, setSum] = useState(0);
const addNum = useCallback(() => {
setSum(num + 1);
}, [num]);
아래 로직 같은 경우 API 요청 -> 요청 후 특정 마켓에 있는 조건에 맞는 상장 코인만 필터해서 화면에 렌더해주는 함수이다.
//업비트 API를 요청하고 전체 상장된 코인중 KRW PAIR 상장된 코인을 VIEW로 뿌려준다.
const [number, setNumber] = useState(0);
const upbitListing = useSelector(state => state.upbitListingCoin);
const getUpbitAllPairs = () => {
let result = []
console.log("전체 상장된 원화 코인을 요청했습니다.")
for (const key in upbitListing.KRW_MARKET) {
result.push(<View key = {upbitListing.KRW_MARKET[key].korean_name.toString()}><Text>{upbitListing.KRW_MARKET[key].korean_name}</Text></View>)
}
return result;
}
return (
<Container>
<TouchableOpacity
onPress={() => {
setNumber(number+1);
}}
style = {{height:50,backgroundColor:'red',alignItems:'center',justifyContent:'center'}}>
<Text>
number = {number}
</Text><
/TouchableOpacity>
<ScrollView>
<View>
{getUpbitAllPairs()}
</View>
</ScrollView>
</Container>
같은 컴포넌트에서 number State가 변경될때마다 컴포넌트가 리렌더링 되면서 getUpbitAllPairs 함수가 항상 호출되고 콘솔 로그가 찍히는것을 확인 할 수 있었다. 같은 컴포넌트에 있는 State가 바뀔때마다 무조건적으로 해당 함수가 호출되어 코인 리스트를 불러오는것은 비효율적이라는 생각이 들었다.
위와 같이 컴포넌트 내에서 어떤 State가 변하든 또는 부모 컴포넌트에서 Props가 새로 내려오던, 무조건적으로 함수가 요청된다. 만약 해당 함수가 단순 API 요청 함수가 아니고 API요청 후 특정 무거운 비즈니스 로직까지 거친다면, 비효율적일것임.
로직 코드 개선 후
const [number, setNumber] = useState(0);
const upbitListing = useSelector(state => state.upbitListingCoin);
const [filterData, setFilterData] = useState([]);
const getUpbitAllPairsUseMemo = useMemo(() => {
let result = []
for (const key in upbitListing.KRW_MARKET) {
result.push(<View key={upbitListing.KRW_MARKET[key].korean_name.toString()}><Text>{upbitListing.KRW_MARKET[key].korean_name}</Text></View>)
}
return result;
}, [filterData])
return (
<Container>
<TouchableOpacity
onPress={() => {
setNumber(number+1);
}}
style = {{height:50,backgroundColor:'red',alignItems:'center',justifyContent:'center'}}>
<Text>
number = {number}
</Text><
/TouchableOpacity>
<ScrollView>
<View>
{getUpbitAllPairsUseMemo}
</View>
</ScrollView>
</Container>
)
number State가 변해도 getUpbitAllPairsUseMemo()는 useMemo에 캐싱되어있기 때문에 새로 렌더링 하지않고 캐시 메모리에서 꺼내서 사용하게 된다.