이전부터 useCallback
과 useMemo
를 알고 있었지만 최근에 실제로 써보면서 다시 관련 내용을 찾아보는 나를 발견하고 블로그에 간단히 정리해보기로 했다.
참조 : https://medium.com/@jan.hesters/usecallback-vs-usememo-c23ad1dc60
===
와 ==
) const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
메모이제이션된 콜백을 반환한다. 메모이제이션된 콜백은 콜백의 의존성(dependency)이 변경되었을 때만 변경된다.
const memoizedValue = useMemo(() => computeExpensiveValue(a,b), [a,b]);
메모이제이션된 값을 반환한다. 의존성(dependency)가 변경되었을 때만 메모이제이션된 값을 다시 계산한다. 이를 통해 불필요한 연산을 피한다.
useMemo
로 전달된 함수는 렌더링 중에 실행된다! side effect는useEffect
에서 하는 일이고useMemo
에서 하는 일이 아니다.
의존성 배열에 아무 것도 없으면 매 랜더링마다 새 값을 계산한다.
useCallback(fn, deps)
은 useMemo(() => fn, deps)
와 같다.
함수를 다른 변수와 같이 다룰 수 있는 함수를 말한다.
const number = 1;
const greeting = 'hello';
function foo() {
return 'bar';
}
// 변수에 할당한 함수
const otherFoo = function() {
return 'bar';
}
// 변수에 할당한 함수(Arrow Function)
const anotherFoo = () => 'bar';
number;
greeting;
foo();
otherFoo();
anotherFoo();
함수를 인자로 전달 받거나 함수를 결과로 반환하는 함수.
다음의 실행 값이 무엇이 나올지 생각해보자
const identity = x => x;
identity(1);
identity(foo);
identity(foo)();
결과
identity(1); // 1
identity(foo); // ƒ foo() {
return 'bar';
}
identity(foo)(); // 'bar'
identity(foo)
가 함수를 return
한 부분이 중요하다.
identity(foo)()
는 return
한 함수를 실행시킨 결과를 반환했다.
참조 평등은 두 객체에 대한 포인터가 동일하다는 것을 의미한다.
결과를 예상해보자
const anotherFoo = () => 'bar';
function sameFoo() {
return 'bar';
}
const fooReference = foo;
'hello' === 'hello';
greeting === otherGreeting;
foo === foo;
foo === otherFoo;
foo === anotherFoo;
foo === sameFoo;
foo === fooReference;
결과
'hello' === 'hello'; // true
greeting === otherGreeting; //true
foo === foo; // true
foo === otherFoo; // false
foo === anotherFoo; // false
foo === sameFoo; // false
foo === fooReference; //true
여기서 눈 여겨봐야 하는 부분은 greeting === otherGreeting; //true
와 foo === sameFoo; // false
의 결과가 전혀 다른 것이다.
foo
와 smaeFoo
는 실제로 같지 않다. 즉, 포인터의 위치가 같지 않다.
자, 위의 사전 지식을 열심히 습득했다면 이제 useCallback
과 useMemo
의 차이를 짐작할 수 있다!!!!
useCallback
과 useMemo
가 무엇을 반환하는지 생각해보자
useCallback(fn, deps)
useMemo(fn, deps)
결과
useCallback
은 호출되지 않은 함수를 반환
useMemo
호출한 함수의 결과를 반환
identity(foo);
와 identity(foo)();
의 결과 차이를 다시 생각해보자!!
useCallback
과 useMemo
의 사용
function foo() {
return 'bar';
}
CONST memoizedCallback = useCallback( foo, []);
CONST memoizedResult = useMemo( foo, []);
위를 바탕으로 다음의 실행 결과를 예상해보자
memoizedCallback;
memoizedResult;
memoizedCallback();
memoizedResult();
결과
memoizedCallback;
// f foo() {
return 'bar';
}
memoizedResult; // 'bar'
memoizedCallback(); // 'bar'
memoizedResult(); // TypeError
리액트에서 useCallback
과 useMemo
가 사용되는 모습
function MyComponent({foo, initial}) {
const memoizedCallback = useCallback(() => {
someFunc(foo, bar);
}, [foo, bar]);
const memoizedResult = useMemo(() => someOtherFunc(foo, bar), [foo, bar]);
// 등등
}
useCallback
은 deps를 사용하는 함수를 호출하는 inline callback을 사용한다. useMemo
은 일부 함수를 호출하고 그 결과를 반환하는 "create function"을 사용한다. 왜 다음은 잘 못 되었을까?
function sum(a, b) {
console.log('sum() ran');
return a + b;
}
function App() {
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const [name, setName] = useState('Jim');
const result = useCallback(sum(val1, val2), [val1, val2]);
return (
<div className="App">
<input
value={val1}
onChange={({ target }) =>
setVal1(parseInt(target.value || 0, 10))
}
/>
<input
value={val2}
onChange={({ target }) =>
setVal2(parseInt(target.value || 0, 10))
}
/>
<input
placeholder="Name"
value={name}
onChange={({ target }) => setName(target.value)}
/>
<p>{result}</p>
</div>
);
}
힌트 : useCallback
은 함수를 반환하지 값을 반환하지 않는다
답
: useCallback
는 값의 메모이제이션에 사용할 수 없다. 즉. useCallback(fn(), [deps])
로는 사용 불가
import React, { useEffect, useState } from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
function User({ userId }) {
const [user, setUser] = useState({ name: '', email: '' });
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser);
};
useEffect(() => {
fetchUser();
}, []);
return (
<ListItem dense divider>
<ListItemText primary={user.name} secondary={user.email} />
</ListItem>
);
}
export default User;
해당 컴포넌트를 실행시킨다면 Infinity Loop를 확인 할 수 있다.
클로저 때문...!
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser); // 🔴 setState triggers re-render
};
useEffect(() => {
fetchUser();
}, [fetchUser]); // fetchUser is a new function on every render
setState
가 계속 랜더링이 되도록 만든다.
그럼 해결책은 어떤 것이 있을까?
: useEffect
내로 fetchUser
함수를 이동시켜 클로저 문제를 해결
// 1. Way to solve the infinite loop
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser); // Triggers re-render, but ...
};
fetchUser();
}, [userId]); // ✅ ... userId stays the same.
: userId
가 바뀌면 fetchUser
가 호출된다.
// 2. Way to solve the infinite loop
const fetchUser = useCallback(async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // ✅ fetchUser stays the same between renders
기능에는 문제가 없지만 랜더링 될 때마다 실행되는 함수... 어떻게 할까?
// Some FP magic 🧙🏼♂️
const filter = (f, arr) => arr.filter(f);
const prop = key => obj => obj[key];
const getName = prop('name');
const strIncludes = query => str => str.includes(query);
const toLower = str => str.toLowerCase();
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const nameIncludes = query =>
pipe(
getName,
toLower,
strIncludes(toLower(query))
);
function UserList({ query, users }) {
// 🔴 Recalculated on every render
const filteredUsers = filter(nameIncludes(query), users);
// ...
}
: query
와 users
가 변경 되지 않으면 filteredUser
호출 없이 기존 값을 가져오도록!
function UserList({query, users}) {
// ✅ Recalculated when query or users change
const filteredUsesr = useMemo(
() => filter(nameIncludes(query), users),
[query, users]
);
};