useCallback
- 메모이제이션 기법을 이용한 Hook
- useMemo가 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook이다.
- 컴포넌트가 리렌더링 되더라도 함수가 의존하고 있는 값들이 바뀌지 않았다면, 함수를 메모리 어딘가에 저장해뒀다가 다시 꺼내 쓸 수 있다.
(재렌더링을 막는 것이 아니라 재렌더링 되더라도 함수가 새로 만들어지는 것을 막는 것!)
useCallback을 사용하지 않았을 때
function Calculator({x, y}){
const add = () => x + y;
return <>
<div>{add()}</div>
</>;
}
Calculator
컴포넌트 내에 add
라는 함수가 선언돼 있음
add
함수는 props로 넘어온 x
와 y
의 값을 더해 <div>
태그에 값을 출력하고 있다.
- 이
add
함수는 해당 컴포넌트가 렌더링이 될 때마다 새롭게 만들어진다.
useCallback을 사용했을 때
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>{add()}</div>
</>;
}
useCallback
Hook을 사용하면 그 함수가 의존하는 값들(x
와 y
)이 바뀌지 않는 한 기존 함수를 계속해서 반환.
x
와 y
의 값이 이전과 동일하다면 다음 렌더링 때 이 함수를 다시 사용
- useCallback은 useMemo에 비해 괄목할 만한 최적화를 느낄 수 없다. 함수를 호출하지 않는 Hook이 아니라 그저 메모리 어딘가에서 함수를 꺼내서 호출하는 Hook이기 때문.
- 따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해
useCallback
을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
- 이보다는 🌟
자식 컴포넌트의 props로 함수를 전달해줄 때🌟 useCallback
을 사용하는 것이 좋다.
useCallback과 참조 동등성
- useCallback은 참조 동등성에 의존한다.
- 리액트는 JavaScript 언어로 만들어진 오픈소스 라이브러리기 때문에 기본적으로 JavaScript 문법을 따라간다.
- JavaScript에서 함수는 객체이다. 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장하기 때문에 반환하는 값이 같을지라도 일차연산자로 비교했을 때
false
가 출력된다.
function doubleFactory(){
return (a) => 2 * a;
}
const double1 = doubleFactory();
const double2 = doubleFactory();
double1(8);
double2(8);
double1 === double2;
double1 === double1;
double1
과 double2
는 같은 함수를 할당했음에도 메모리 주소 값이 다르기 때문에 같다고 보지 않는다.
- useCallback을 사용하지 않으면 리렌더링 시 함수를 새로 만들어 호출한다. (이전의 함수와는 다른 주소값을 가진 함수)
- 반면 useCallback을 이용해 함수를 저장했다가 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것과 같다.
- 따라서 리액트 컴포넌트 함수 내에서 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.
useCallback 예제
useCallback 사용 안 한 경우
import { useState } from "react";
import "./styles.css";
import List from "./List";
export default function App() {
const [input, setInput] = useState(1);
const [light, setLight] = useState(true);
const theme = {
backgroundColor: light ? "White" : "grey",
color: light ? "grey" : "white"
};
const getItems = () => {
return [input + 10, input + 100];
};
const handleChange = (event) => {
if (Number(event.target.value)) {
setInput(Number(event.target.value));
}
};
return (
<>
<div style={theme} className="wall-paper">
<input
type="number"
className="input"
value={input}
onChange={handleChange}
/>
<button
className={(light ? "light" : "dark") + " button"}
onClick={() => setLight((prevLight) => !prevLight)}
{light ? "dark mode" : "light mode"}
</button>
<List getItems={getItems} />
</div>
</>
);
}
- List.js (input에 따라 변하는 숫자 리스트..)
import { useState, useEffect } from "react";
function List({ getItems }) {
const [items, setItems] = useState([]);
useEffect(() => {
console.log("아이템을 가져옵니다.");
setItems(getItems());
}, [getItems]);
return (
<div>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
}
export default List;
- 자식 컴포넌트
<List>
에 prop으로 함수 getItems
를 전달해서 useEffect의 의존성 배열의 요소로 쓰고 있음.
- 현재는 light/dark mode 버튼 누르면 재렌더링 되어
getItems
함수가 새로 만들어지므로 (= 새로운 주소값으로 바뀌므로) 그때마다 이 useEffect 구문이 실행된다.
useCallback 사용한 경우
import { useState, useCallback } from "react";
import "./styles.css";
import List from "./List";
export default function App() {
const [input, setInput] = useState(1);
const [light, setLight] = useState(true);
const theme = {
backgroundColor: light ? "White" : "grey",
color: light ? "grey" : "white"
};
const getItems = useCallback(() => {
return [input + 10, input + 100];
}, [input]);
const handleChange = (event) => {
if (Number(event.target.value)) {
setInput(Number(event.target.value));
}
};
return (
<>
<div style={theme} className="wall-paper">
<input
type="number"
className="input"
value={input}
onChange={handleChange}
/>
<button
className={(light ? "light" : "dark") + " button"}
onClick={() => setLight((prevLight) => !prevLight)}
{light ? "dark mode" : "light mode"}
</button>
<List getItems={getItems} />
</div>
</>
);
}
- List.js (input에 따라 변하는 숫자 리스트..)
import { useState, useEffect } from "react";
function List({ getItems }) {
const [items, setItems] = useState([]);
useEffect(() => {
console.log("아이템을 가져옵니다.");
setItems(getItems());
}, [getItems]);
return (
<div>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
}
export default List;