[React]useMemo와 useCallback의 차이(+ React.memo)
1. 개요
현재 내가 만들고 있는 앱에서 불필요한 렌더링이 무지막지하게 일어나고 있는걸 확인했다.
이유는 하나의 부모컴포넌트에서 모든 자식 컴포넌트의 상태값을 가지고 있기 때문에
렌더링 되지 않아도 되는 자식 컴포넌트 마저도 렌더링을 시키고 있는 것이다.
memoization을 통해 성능최적화를 해준다고 들었던 useMemo와 useCallback.
공문을 보아도 이 둘의 차이가 명확하게 느껴지지 않았다.
(심지어 useCallback안에 useMemo를 사용하기도 한다고..)
공문과 블로그 내용들을 보며 두 함수의 차이를 알아보고
언제 어느 상황에서 어떤 함수를 써야하는지 판단해 직접 써보기위해 정리를 해보려한다
2. Memoization
이 memoization의 개념을 먼저 잘 잡고 가야한다.
위키피디아에 따르면 컴퓨터가 동일한 계산을 반복해야 할 떄, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산을 하지 않게해서 속도를 높이는 기술이다.보통 앱의 최적화에 사용된다.
3. useMemo
useMemo는 리렌더링 사이에 계산된 결과 값을 캐싱해주는 리액트 훅이다.
const cachedValue = useMemo(calculateValue, dependencies)
calculateValue:
계산하려는 값을 반환하는 함수. 인자를 받지않고 () => fn 처럼 쓴다.
dependencies:
useEffect의 dependencies 배열과 같다.
useMemo는 의존하는 값, 즉 dependencies가 변할 때까지 리렌더링 사이에 결과 값을 캐시한다.
import { useMemo, useState } from "react";
export default function App() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
console.log("num1: ", num1);
return (
<>
<button onClick={() => setNum1((curr) => curr + 1)}>+Num1</button>
<button onClick={() => setNum2((curr2) => curr2 + 1)}>+Num2</button>
</>
);
}
코드를 보자.
num2의 값을 1 올려주는 버튼을 클릭하더라도 num1의 콘솔값이 출력되고 있다.
이것은 리액트의 자연스러운 동작이다.
state값이 변하기 때문에 해당 컴포넌트가 리렌더링이 일어났고
자연히 num1의 상태값에 관계없이 그 값이 콘솔로 찍히고 있는 상황이다.
이런 단순한 콘솔값은 문제가 없지만, 복잡한 연산 식이라면 어떨까? 한번 연산에 1초씩 걸린다고 가정해보자.
관련 없는 상태값의 변화로 인해 렌더링하는데 1초씩 딜레이가 생긴다면 분명 문제가 있다.
3-1. useMemo 사용 예제
(예제1)
import { useMemo, useState } from "react";
export default function App() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
useMemo(() => console.log(num1), [num1]);
return (
<>
<button onClick={() => setNum1((curr) => curr + 1)}>+Num1</button>
<button onClick={() => setNum2((curr2) => curr2 + 1)}>+Num2</button>
</>
);
}
3에서 본 코드에 useMemo 함수를 등록하여 fn에 콘솔을 넣어주었다.
이제 num1 상태값이 변경될 때만 콘솔값이 찍히는 것을 볼 수 있다.
즉 num1에 의존하여 연산 값을 반환할 수 있도록 useMemo를 사용했다.
4. useCallback
앞에 useMemo 는 메모이제이션된 값을 반환해준다고 했는데 useCallback은 메모이제이션된 함수를 반환한다.
const cachedFn = useCallback(fn, dependencies)
useCallback 역시 입력된 dependencies 값이 변하면 fn에 등록한 함수를 반환한다.
기본적으로 useCallback으로 함수를 감싸주게 되면 컴포넌트의 첫 렌더링 때만 실행하고 그 뒤부터는 함수를 기억하고 있어 재생성하지 않는다.
useCallback을 사용하는 곳은 다음과 같다.
1) 자식 컴포넌트에 props로 함수를 전달할 경우
2) 외부에서 값을 가져오는 api를 호출하는 경우
5. React.memo
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
컴포넌트를 감싸서 사용하며 부모 컴포넌트로 부터 들어온 props의 값이 바뀔 때만 리렌더링한다.
따라서 부모로부터 props만 받는 자식 컴포넌트에서만 사용하는 것이 권장된다.
보통 이런 식으로 사용이 된다.
// 방법 1
const Result = React.memo(() => {
return ()
})
// 방법 2
export default React.memo(Result)
직접 작성해본 코드로 보는 사용 예시
1시간 안에 끝내야 하는 todo list를 만드는 앱이다.
> /App.js
import { useState } from "react";
export default function App() {
const [todo, setTodo] = useState("");
const [todoList, setTodoList] = useState([]);
const handleChange = (value) => {
setTodo(value);
};
const onSubmit = (e) => {
e.preventDefault();
setTodoList([...todoList, todo]);
};
const getAvgPerHour = (list) => {
if (list.length === 0) return 0;
return 60 / list.length;
};
const avgPerHour = getAvgPerHour(todoList);
return (
<>
<Title title="1시간 안에 끝내는 Todo List"></Title>
<form onSubmit={onSubmit}>
<input type="text" onChange={(e) => handleChange(e.target.value)} />
</form>
<ul>
{todoList.map((todo, index) => {
return <li key={index}>{todo}</li>;
})}
</ul>
<div>할 일 하나당 걸릴 시간: {avgPerHour}</div>
</>
);
}
> Title.js
export default function Title({ title }) {
console.log(title);
return <h1>{Title}</h1>;
}
todo 를 하나 등록하면 해당 todo를 완료해야하는데 걸릴 시간을 getAvgPerHour(todoList)가 연산을 해준다.
todo가 1개 일 때는 60분, 2개 일 때는 30분, 3개 일때는 각 todo 당 20분이 걸려야한다.
여기서 문제가 있다.
todo를 입력하는 input에 텍스트를 입력해서 onChange 이벤트핸들러가 실행될 때마다
getAvgPerHour 안에 코드도 실행이 되는 불상사가 일어난다.
게다가 Title 컴포넌트 내부에 title도 계속해서 콘솔이 찍힌다.
코드를 보면,
"블로그"글자를 하나 입력했더니 getAvgPerHour함수도, Title 컴포넌트도 계속해서 불러지는 것이 확인된다.
getAvgPerHour이랑 Title은 무슨 죄인가.
먼저 todoList값을 이용해 todo 하나당 걸릴 시간을 리턴하는 getAvgPerHour 함수는
todoList에 변화가 생겼을 때만 해당 함수를 호출해 리턴 값을 받을 수 있다면 베스트일 것이다.
useMemo를 이용해보자.
const avgPerHour = useMemo(() => getAvgPerHour(todoList), [todoList]);
블로그를 입력하고 submit 해주면
콘솔이 한번만 찍힌 것을 확인할 수 있다.
또한 props로 들어오는 값이 달라지지 않는다면 Title컴포넌트를 리렌더링 시키지 않으면 좋을 것이다.
> /Title.js
import React from "react";
function Title({ title }) {
console.log(title);
return <h1>{title}</h1>;
}
export default React.memo(Title);
이렇게 하면 input에 아무리 값을 입력하고 submit을 하더라도 props가 변경되지 않은 TItle 컴포넌트는 리렌더링되지 않는다.
useCallback 예시를 새로 만들어보겠다.
useCallback은 함수의 재생성을 막아주는 것이기 때문에 콘솔로 확인하기가 힘들다.
> /App.js
import { useState } from "react";
import Submit from "./Submit";
export default function App() {
const [todo, setTodo] = useState("");
const [todoList, setTodoList] = useState([]);
const handleChange = (value) => {
setTodo(value);
};
const onSubmit = (e) => {
console.log("somethimg to submit");
};
return (
<>
<input type="text" onChange={(e) => handleChange(e.target.value)} />
<Submit onSave={onSubmit} />
</>
);
}
> /Submit.js
export default function Submit({ onSave }) {
return <button onClick={onSave}>Save!</button>;
}
input에 값이 입력되면 App컴포넌트가 리렌더링 발생하면 onSubmit함수가 재생성되고
Submit컴포넌트에 props로 넘긴 onSubmit함수가 새로 전달되게 된다
이때 Submit컴포넌트에 useMemo를 사용하더라도 이전 onSubmit과 이후 onSubmit은 같은 값을 반환하더라도 참조가 다른 함수가 되어버리기 때문에 onSubmit은 계속해서 새로 전달된다.
그렇기때문에 Submit컴포넌트는 부모컴포너넌트를 따라 리렌더링이 계속해서 일어난다.
여기서 중요한 것은 함수는 같은 값이 더라도 값이 아닌 참조로 비교를 한다는 것을 알고있어야한다.
const function1 = function() {
return 1;
};
const function2 = function() {
return 1;
};
function1 === function2 // false
이때 onSubmit함수를 useCallback으로 묶어주고 deps에 빈 값을 할당해주면,
> /App.js
const onSubmit = useCallback(() => console.log("somethimg to submit"), []);
onSubmit 함수는 더이상 input의 입력에 따라 새로 생성되지 않고
첫 렌더링 때만 생성된 후 메모이제이션이 된다.
끝으로 useMemo와 useCallback을 공부하며
많이 궁금해할 것 같은 질문에 대한 좋은 답변을 써놓은 블로그가 있어 가져와보았다.
끝
참고
[React] useMemo와 useCallback의 차이
1. 개요 useMemo와 useCallback이라고 하는 hook 함수가 있지만 둘의 개념을 살펴보면 두 함수다 비슷한 역할을 가지고 있는 것 같아, 언제 어느 상황에 useMemo 또는 useCallback을 써야할지 판단을 하기 위
narup.tistory.com
https://www.howdy-mj.me/react/memoization
React의 Memoization
위키피디아에 따르면, 메모이제이션(memoization)은 컴퓨터가 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로서 동일한 계산을 하지 않도록 하여, 속도를 높이는 기술이다
www.howdy-mj.me
https://www.copycat.dev/blog/react-usecallback/
Stop Unnecessary Renders with React useCallback - CopyCat Blog
Learn all you need to know about the React useCallback Hook with this complete guide that includes definitions, examples, and more.
www.copycat.dev