함수 메모이제이션
첫번째 인자로 넘어온 함수를 두번째 인자로 넘어온 의존성 배열 내의 값이 변경될 때까지
저장해놓고 재사용할 수 있게 해준다.
useCallback(fn, deps)
컴포넌트가 리렌더링되어도 함수를 캐싱하여 남아있도록 해줌
특정 함수를 리렌더링 시 새로 만들지 않고 재사용하고 싶을 때 사용한다.
한 번 함수가 만들어지고 props 변경이 거의 없을 경우 virtual dom에 새로 렌더링 하지 않도록 최적화할 수 있다.
컴포넌트를 렌더링할 때 마다 함수를 새로 선언하는 것은 사실 성능상 문제가 크게 되진 않음
단순히 재생성을 방지하고자 하는 목적으로 사용하는 것은
오히려 코드 가독성을 떨어뜨리고 의미가 없을 수 있으며 오히려 성능 저하를 유발할 수 있다.
의존성 비교라는 비용을 상쇄할 수 있을 정도로 성능을 향상시킬 수 있을 때 사용할 것!
렌더링 할 때 선언된 함수를 초기화한다.
함수는 객체이고 참조 주소값을 가지며 리렌더링 시에 함수를 다시 그린다.
따라서 useEffect
같은 hook의 의존성에 사용하게 될 경우 선언된 값이 계속 변경되는 것으로
동작하기 때문에 문제가 발생할 수 있다.
영어로 이름을 입력하면 어떤 국적일 확률인지 보여주는 api를 사용하는 간단한 예제로 확인해보자
const UserProfile = ({ name }) => {
const [countries, setCountries] = useState([]);
const getCountries = () => {
return fetch(`https://api.nationalize.io/?name=${name}`)
.then((response) => response.json())
.then((result) => {
return result.country;
});
};
useEffect(() => {
getCountries().then((res) => setCountries(res));
}, [getCountries]);
return (
<div>
{name}의 국적은:
{countries?.map((c, i) => (
<div key={i}>
<p>{c.country_id}일 확률이</p>
<p>{(c.probability * 100).toFixed(2)}%</p>
</div>
))}
</div>
);
};
getCountries
함수가 변경될 때만 api를 호출하도록 의도되었다.
하지만 위에서 언급했듯 렌더링 마다 다른 참조 주소 값을 가져
이전 렌더링 주기와 현재의 동등함을 만족하지 못하여 useEffect
가 동작하게 되고
그로인해 무한 렌더링이 발생하게 된다.
무한 렌더링 때문에 계속 api 요청을 하다가 요청횟수 제한에 걸려버린 모습이다.
const UserProfile = ({ name }) => {
const [countries, setCountries] = useState([]);
console.log("RENDER!!");
const getCountries = useCallback(() => {
console.log("나는 getCountries");
return fetch(`https://api.nationalize.io/?name=${name}`)
.then((response) => response.json())
.then((result) => {
console.log(result);
return result.country;
});
}, [name]);
useEffect(() => {
console.log("getCountries 호출");
getCountries().then((res) => setCountries(res));
}, [getCountries]);
return (
<div>
{name}의 국적은:
{countries?.map((c, i) => (
<div key={i}>
<p>{c.country_id}일 확률이</p>
<p>{(c.probability * 100).toFixed(2)}%</p>
</div>
))}
</div>
);
};
이와 같은 상황에서 useCallback hook
을 사용하면
컴포넌트가 다시 랜더링되더라도 함수의 참조 주소값을 동일하게 유지시킬 수 있다.
의도했던 대로 useEffect
에 넘어온 함수는 prop으로 넘어온 name
값이 변경되지 않는다면 재호출하지 않게 된다!
React.memo??
react.memo로 감싼 컴포넌트는 props 가 변경되지 않으면 다시 호출되지 않음
React.memo를 언제쓸까?
react는 컴포넌트를 렌더링하고 이전렌더와 현재를 비교해 다르면 dom을 업데이트 하는데 이 속도를 높일 수 있음 React.memo 로 래핑된 컴포넌트는 컴포넌트 렌더링 후 결과를 메모이징 해두고 다음 렌더링 시 props 변경이 없다면 그대로 재사용한다.
같은 props로 렌더링이 자주 일어나는 컴포넌트라고 예상될 때 사용하면 좋다
예를 들어 서버에서 일정 주기로 조회수를 패치해와 렌더링 해주는 컴포넌트가 있고 내부에 변경되지 않는 부분(타이틀, 내용)을 다루는 컴포넌트가 있을 때 조회수 업데이트로도 변경이 필요없는 컴포넌트가 같이 계속 렌더링 될텐데 이런 부분이 메모이제이션 적용에 적절한 케이스다.
언제쓰면안될까
props 가 자주 변하는 컴포넌트에 사용한다면 동등 비교를 계속 시도하고 결과는 false 이기 때문에 비효율적일 수 있다.
성능적인 이점이 있을거라 확신하지 못한다면 사용하지 않는 것이 좋으며
반드시 사용하지 않음을 먼저 고려하고 성능 테스트를 하며 적용하고 이득이 생기는지 확인할 것
주의점
react.memo
의 prop
동등 비교 시 얕은비교(주소)를 수행하기 때문에
객체의 값(동일성) 비교를 원한다면 equal
함수를 정의해 사용해줘야 한다.
useCallback with memo 예시
메모이제이션이 없다면 App컴포넌트의 count또는 text의 변경에 대해
두 컴포넌트 모두를 리렌더링 해버리는데 메모이제이션을 활용해 이를 방지함.
App 컴포넌트의 함수 a()
또한 리렌더링 시 다시 선언되고
이는 count의 props 로 넘겨지기에 text를 변경해도 count가 같이 업데이트 되는데
useCallback
을 통해 a함수를 캐싱해 이를 방지했다.
최적화 해준다. 캐싱해준다. 리렌더링 방지해서 속도를 높여준다.
달콤하지만 세상이 그렇듯 일방적으로 우리에게 이득을 주기 위해 노력해주는 것 따위는 없다.
나중에 프로젝트를 할 때 성능적으로 최적화 할 수 있다고 판단되는 컴포넌트에 대해 꼭 적용해보고
성능 테스트도 공부해서 직접 비교해보아야 겠다.