useMemo & useCallback
메모이제이션(Memoization)
값비싼 함수 호출의 결과를 캐싱하고 동일한 입력이 다시 발생할 때 캐싱된 결과를 반환하는 프로그래밍 기술
즉 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법
적절하게 활용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있음
[ useMemo ]
메모이제이션된 값을 반환하는 함수
리렌더링이 발생할 경우 의존성 배열에 지정된 deps가 변할때에만 useMemo에 등록한 함수가 실행되도록 처리하면 불필요한 연산을 하지 않게됨
useMemo(() => fn, [deps])
의존성 배열에 지정된 deps로 지정된 값이 변하게 되면 () => fn함수를 실행하고 그 함수의 반환 값을 반환해줌
deps는 dependency의 약어로 의존성을 뜻하며 useMemo가 deps에 의존하고 있다는 것을 말함
[ useCallback ]
메모이제이션된 함수를 반환
useCallback이 함수를 반환하기 때문에 그 함수를 가지는 const 변수에 초기화하는 것이 일반적인 모양
useCallback(fn, [deps])
useCallback 또한 deps 즉 의존성이 있는 값이 변하면 fn에 등록한 함수를 반환
▶ useCallback 사용처
자식 컴포넌트에 props로 함수를 전달하는 경우
함수는 값이 아닌 참조로 비교된다는 점을 알고 있어야 함
const functionOne = function() {
return 5;
};
const functionTwo = function() {
return 5;
};
// 서로의 참조가 다르기 때문에 false
console.log(functionOne === functionTwo);
동일한 값을 반환하지만 참조가 다르기 때문에 false가 나옴
컴포넌트에서 특정 함수를 정의할 경우 각각의 함수들은 모두 고유한 함수가 됨
이런 고유한 함수가 생성될 경우 부모를 통해 props에 함수를 전달받는 자식 컴포넌트에서는 props가 변경되었다고 판단해 리렌더링이 발생하게 됨
//useCallback X
function App() {
const [name, setName] = useState('');
const onSave = () => {};
return (
<div className="App">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Profile onSave={onSave} />
</div>
);
}
//useCallback O
function App() {
const [name, setName] = useState('');
const onSave = useCallback(() => {
console.log(name);
}, [name]);
return (
<div className="App">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Profile onSave={onSave} />
</div>
);
}
useCallback을 사용하지 않을 경우 name이 변경되어 리렌더링이 발생하면 onSave함수가 새로 만들어지고 Profile 컴포넌트의 props로 onSave함수가 새로 전달되게 됨
이때 Profile 컴포넌트에서 useMemo를 사용해도 이전 onSave와 이후 onSave가 같은 값을 반환하지만 참조가 다른 함수가 되어버리기 때문에 리렌더링이 일어나게 됨
하지만 useCallback을 사용해서 onSave라는 함수를 재사용하는 것으로 자식 컴포넌트의 리렌더링을 방지할 수 있음
외부에서 값을 가져오는 api를 호출하는 경우
//useCallback X
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = () =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
//useCallback O
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = useCallback(
() =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user),
[userId]
);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
fetchUser 함수가 변경될 때만 외부에서 api를 가져와 useEffect가 실행됨
하지만 이렇게 되면 무한루프에 빠지게 됨
Profile 컴포넌트가 리렌더링이 발생할 경우 fetchUser함수에는 새로운 함수가 할당되게 됨
그러면 useEffect가 호출되어 user값이 바뀌에 되고 state값이 바뀌었기 때문에 다시 리렌더링이 발생됨
이때 useCallback을 사용할 경우 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있음
api의 옵션으로 사용되는 userId가 변동될 때만 fetchUser에 새로운 함수가 할당되도록 설정하고 그것이 아니면 동일한 함수가 실행되게 되서 무한 루프에 빠지지 않도록 할 수 있음
[ useMemo와 useCallback의 사용 ]
사용하지 말아야 할 경우
- 연산이 복잡하지 않은 함수에 useCallback을 사용하는 것은 메모리 낭비이므로 간단한 일반 함수들에는 useCallback을 사용하지 않는게 좋음
- 단순히 함수 내부에서 setState나 dispatch 함수등을 호출하는 경우에는 useCallback을 사용하지 않는게 좋음
이미 리액트 자체에서 useState 와 useDispatch에 대한 성능 최적화가 보장되기 때문에 렌더링이 새로 되어도 해당 함수는 재생성되지 않음 - useCallback, useMemo의 의존성 배열에 완전히 새로운 객체나 배열을 전달해서는 안됨
useCallback 내부 함수나 useMemo 내부 값에서 사용하지 않는 props를 전달한다면 메모이제이션을 하는데 소용이 없음 - 의도적으로 매번 새로운 함수나 값을 계산해야 한다면 굳이 useCallback이나 useMemo를 사용할 필요가 없음
- DOM에서 다른 컴포넌트를 렌더링하지 않는 컴포넌트 (html 태그만 렌더링하는 컴포넌트)에서는 useMemo를 사용할 필요가 없음
- div, span, a, img와 같이 호스트 환경 (브라우저 / 모바일)에 속하는 플랫폰 컴포넌트에 전달하는 항목에는 useMemo와 useCallback을 사용할 필요가 없음
리액트는 해당 컴포넌트들에 함수 참조가 변경되었는지 신경쓰지 않기 때문(ref는 제외)
사용해야 할 경우
- 연산 혹은 처리량이 매우 많아서 렌더링의 문제가 되는 경우 리렌더시 비용 절감을 위해서 useMemo를 사용
- 자식 컴포넌트에서 useEffect가 반복적으로 트리거 되거나 무한 루프에 빠질 위험이 있을 때 useMemo, useCallback을 사용
- 자식 컴포넌트에 함수를 props로 넘길 때 불필요한 렌더링이 일어난다고 판단된다면 useCallback으로 함수 동등성을 유지
- 함수 자체가 매우 복잡하거나 다시 계산하는데 비용이 많이 드는 경우에 useCallback을 사용
- 사용자의 입력값이 map 혹은 filter 등을 사용하여 이후 렌더링에서도 동일한 참조를 사용할 가능성이 높을 경우 useMemo를 사용해서 메모이제이션을 적용
- 리액트 상위 트리에서 부모가 리렌더링 될 때 자식 컴포넌트까지의 렌더링 전파를 막고 싶을 때 useMemo를 사용
자식 컴포넌트가 useMemo로 메모이제이션 컴포넌트일 경우 메모이제이션된 props를 사용해 필요한 부분만 리렌더링 할 수 있음 - ref 함수를 부수작용(side effect)와 함께 전달하거나 ref로 wrapper 함수를 만들 때 useMemo를 사용
리액트는 ref 함수가 변경될 때 마다 과거 값을 null로 호출하고 새로운 함수를 호출하기 때문인데 이 경우 ref 함수의 이벤트 리스터가 변경되는 등의 불필요한 작업이 일어날 수 있음
[ 결론 ]
새로운 리액트 문서에서 제시하는 권장사항에서 "useMemo, useCallback 혹은 React.memo를 사용하여 캐싱할지 여부를 고려한다면 성능 문제가 발견될 때까지 캐싱을 미루세요"가 있음
항상 메모하는 것은 큰 단점이 아니지만 코드의 가독성이 떨어지고 모두 메모하는것이 최적화의 도움이 되지 않기 때문
큰 문제가 아니라면 사용하지 않고 코드를 작성하고 성능문제가 발견되면 useCallback과 useMemo가 꼭 필요한지 현재 적용하는 곳에 적합한지를 고민하고 장단점을 명확하게 파악한후에 프로젝트에 적용시키는것이 중요함