이전의 React.memo에서 분명 함수는 변하지 않고 코드상으로는 동일한 함수를 호출하는 것처럼 보이는데, re-rendering 되었을 때 함수가 재생성되는 것을 보았다.
이는 코드 상으로는 동일해보이지만, 내부적으로는 다르다는 것을 얘기했었다.
또한,React.memo
는 배열, 객체, 함수에서는 사용할 수 없고 특정 결과값(컴포넌트 등)을 재사용 할 때 사용한다고 했다.이번에 알아볼
useCallback
은,React.memo
와 비슷한Hook
이지만 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용한다.
useCallback
이란, 컴포넌트 실행 전반에 걸쳐 함수를 cache
할 수 있게 해주는 훅(Hook
)이다. 여기서 cache
란 저장을 의미한다.
그렇다면 useCallback
은 어떻게 사용할까?
const cachedFn = useCallback(fn, dependencies)
인자는 총 2개로 구성된다.
fn
: cache
하고 싶은 함수.
리렌더링 됐을 때, 마지막 렌더링 이후로 dependencies
가 변경되지 았았다면 리액트는 같은 함수를 제공한다. 즉, 함수를 재사용할 수 있다는 것이다.
(✅ 중요한 것은 리액트가 함수를 호출(call
)하는게 아닌 제공(give
)한다는 것)
dependencies
: useCallback
호출에 대한 의존성 배열이며, fn
에서 사용되는 모든 값의 리스트(배열)를 의미한다. 여기서 값이라는 것은 props
, state
, context
등 fn
에서 사용되는 모든 것들을 의미한다. (의존성은 useEffect
에서 사용되는 것과 의미가 동일하다.)
const toggleParagrahHandler = useCallback(() => {
setShowParagraph((prevShowParagraph) => !prevShowParagraph);
}, []);
re-rendering
)을 막고 싶을 때state
를 기반으로 현재 state
를 업데이트할 때custom Hook
)을 최적화하고 싶을 때2, 3번의 경우를 살펴보자.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
위는 todos
의 다음 상태를 업데이트하는 코드이다. 의존성 배열에 todos
를 넘겼다.
여기서 의존성 배열을 가능하면 최대한 적게 넘기고 싶을 수 있다. 만약, 다음 state
를 계산하기 위해서 현재 state
를 읽기만 한다면, updater function을 넘겨서 의존성을 삭제할 수 있다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...
위는 의존성인 todos
를 삭제하고 state
를 업데이트 하는 (todos => [...todos, newTodo]
)를 사용했다.
가끔 아래 코드와 같이, Effect 내부에 함수를 호출 해야하는 경우가 있을 수 있다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...
위 코드에서 createOptions
를 의존성으로 넘긴다면, 매 렌더링마다 chat room에 계속해서 재연결하게 될 것이다.
이를 방지하기 위해서는 다음과 같이 코드를 수정해야 한다.
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ...
이는, roomId
가 동일하다면 createOptions
함수는 완전히 동일함을 보장한다.
그리고 roomId
가 변경되지 않는 한, createOptions
는 재생성되지 않는다.(즉, chat room에 계속해서 재연결하지 않는다.)
그런데 여기서 굳이 useCallback
을 사용하지 않고 useEffect
내부로 함수를 옮기고, 의존성 배열에 roomId
를 넘기면, 위의 코드와 완전히 동일한 기능을 할 수 있다.
useEffect(() => {
function createOptions() { // ✅ No need for useCallback or function dependencies!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
결론적으로 useCallback
은 다음의 역할을 수행한다.
fn
)를 리액트의 내부 저장 공간에 저장해서 함수 객체가 실행될 때 마다 이를 재사용할 수 있게 해준다.useCallback
을 사용해 함수를 저장하면 된다.