++ 리액트를 더 잘 쓰기 위한 정리 시리즈
컴포넌트가 많고 복잡한 앱일수록 최적화가 더욱 필요하다.
여러 하위 컴포넌트를 중첩한 경우 상위 컴포넌트에서 새로운 상태로 업데이트가 되면 하위 컴포넌트들도 렌더링이 된다. 불필요한 컴포넌트들이 렌더링이되고, 그 수가 많을수록 당연히 성능 저하로 이어진다.
최적화를 할 수 있는 상황과 방법을 예제를 통해 알아보자!
memo 는 컴포넌트를 감싸는 최적화 API로, 컴포넌트의 props 속성값이 바뀌지 않으면 업데이트가 필요하지 않는 컴포넌트에 사용할 수 있다. (마치 y = x 와 같은 선형 함수에서 입력값이 바뀌지 않으면 출력도 바뀌지 않는 것처럼)
+ 버튼을 누르면 counter가 증가하고 - 버튼을 누르면 counter가 감소하는 기능을 생각해보자.
아래 코드는 버튼의 내용과 스타일의 틀을 만들어주는 컴포넌트일 뿐이다. counter의 증가와 감소와는 사실 관련이 없다.
// memo 적용 전
export default function IconButton({ children, icon, ...props }) {
const Icon = icon;
return (
<button {...props} className="button">
<Icon className="button-icon" />
<span className="button-text">{children}</span>
</button>
);
}
IconButton 컴포넌트는 props 속성으로 { children, icon, ...props} 가 필요한데 아래 코드처럼 icon 도 고정, children 인 Decrement, Increment도 고정, 나머지 ...props 인 onClick에 사용하는 함수도 변경되는 로직이 아니다.
// InconButton 컴포넌트를 사용하는 Counter 컴포넌트
<IconButton icon={MinusIcon} onClick={handleDecrement}>
Decrement
</IconButton>
<CounterOutput value={counter} />
<IconButton icon={PlusIcon} onClick={handleIncrement}>
Increment
</IconButton>
그렇다면 우리는 IconButton 컴포넌트를 최적화 할 수 있다!
아래 코드처럼 memo 를 import 하고, 컴포넌트를 감싸주기만 하면 된다.
import { memo } from "react";
const InconButton = memo(function IconButton({ children, icon, ...props }) {
const Icon = icon;
return (
<button {...props} className="button">
<Icon className="button-icon" />
<span className="button-text">{children}</span>
</button>
);
});
export default InconButton;
memo를 적용한 후 컴포넌트 렌더링이 어떻게 진행되는지 콘솔로 확인해보면

아직도 IconButton 컴포넌트가 렌더링된다..
그렇다면 렌더링이 필요한 이유인 props 값의 변화가 있었다는 말이다.
위에서 설명했듯, icon, children 속성은 고정되어 있다.
그러면 의심해볼만한건 나머지 ...props 인 onclick에 정의한 함수 밖에 없다.
함수가 변경되는 것도 아닌데 왜?? 라는 생각이 들었지만 JS에서 함수는 곧 객체로 만들어졌고, Counter 컴포넌트가 렌더링되면서 함수는 매번 새롭게 생성되며 정의된다. 즉, 이전 함수와 생김새와 역할은 같은데 다른 함수라는 것이다.
이 때, useCallback 함수를 사용할 수 있다.
react 에서 useCallback 훅을 import 한 뒤 최적화하려는 함수를 감싸고 의존성 배열을 설정해주면 된다.
import { useCallback } from "react";
const handleDecrement = useCallback(function handleDecrement() {
setCounter((prevCounter) => prevCounter - 1);
}, []);
const handleIncrement = useCallback(function handleIncrement() {
setCounter((prevCounter) => prevCounter + 1);
}, []);
의존성 배열에 대한 더 자세한 설명은 참고문헌을 확인해주세요!
https://react-ko.dev/reference/react/useCallback
함수까지 최적화를 하고 난 뒤, Counter 컴포넌트 다음에 IconButton 컴포넌트가 렌더링되지 않은 것을 확인할 수 있다!

useMemo 훅은 useCallback 훅과 비슷하다. useCallback이 함수 자체를 새롭게 생성하는 것을 막는 것처럼 useMemo는 함수의 반환값을 그대로 사용할 수 있게 해준다.
만약 isPrime이라는 입력값이 소수이면 true를 소수가 아니면 false를 리턴해주는 함수를 생각해보자. 입력에 10000, 100000 ... 점점 큰 값을 넣을경우 값을 계산하는데 시간이 오래걸리면 최적화 성능에 영향을 주게 된다.
function isPrime(number) {
if (number <= 1) {
return false;
}
const limit = Math.sqrt(number);
for (let i = 2; i <= limit; i++) {
if (number % i === 0) {
return false;
}
}
return true;
}
그래서 useMemo를 사용해 입력값이 변함 없다면 함수의 반환값도 변함 없으니까 복잡한 계산을 다시 안하도록 해줄 수 있다. useCallback 처럼 의존성 배열에 initialCount 값을 넣어주었다. 입력값인 initialCount가 변하면 함수를 실행해서 결과를 가져와야하기 때문이다!
import { useMemo } from "react";
const initialCountIsPrime = useMemo(
() => isPrime(initialCount),
[initialCount]
);
memo는 props값이 변화하지 않은 이상 컴포넌트를 렌더링하지 않아 앱의 성능을 높여줄 수 있는 함수다.
useCallback은 불필요한 함수의 재생성을 방지하고 해당 함수가 props로 사용되는 컴포넌트의 최적화에 영향을 준다.
useMemo는 useCallback처럼 시간 복잡도가 높을 수 있는 함수의 실행을 필요한 경우만 사용할 수 있게 해준다. 반환값의 재사용
최적화를 적용할 때는 이 작업이 필요한지 생각해봐야 한다.🤔
memo 의 경우 props에 해당하는 속성들이 변화가 있는지 리액트가 체크하는 과정이 필요하다. props값이 자주 변경되고 업데이트가 필요한 컴포넌트라면 굳이 memo를 사용해 props가 변경되는지 체크하는 과정들을 더 넣는 것이 최적화에 유의미한지 고민해봐야 한다.
함수의 재생성은 이 자체로 앱에 큰 부담을 주지 않는다고 한다. 리소스를 크게 사용하지 않아서.. 즉, useCallback 자체로 사용하는 것은 큰 의미가 없을 수도 있다.. 하지만 memo와 함께 컴포넌트 렌더링의 최적화를 위해 사용한다면 중요한 훅이다.
이번에 최적화에 대해 더 깊이 이해할 수 있던 시간이라고 생각한다. 물론 전부 이해하진 못했지만,, 실제 프로젝트를 하면서 생각없이 최적화 훅을 사용하는 것보다 이유를 생각하면서 코드를 짜는데 큰 도움이 될 것이라고 생각한다 😀