GITHUB
useMemo, useCallback, useEventCallback
BLOGProject
Seohyun
Develop
01 Dec 2024
useMemo, useCallback, useEventCallback

useMemo

  • 값을 메모이제이션합니다.
  • 의존성 배열이 변경되지 않으면 기존 계산 결과를 재사용합니다.
  • 주로 복잡한 계산이나 렌더링 비용이 높은 작업을 최적화할 때 사용합니다.
const Parent = ({ numbers }) => {
const sortedNumbers = useMemo(() => {
console.log("리스트 정렬 중...");
return numbers.sort((a, b) => a - b);
}, [numbers]); // numbers가 변경될 때만 다시 계산.
return <div>{sortedNumbers.join(", ")}</div>;
};
  • numbers가 변경되지 않으면, useMemo는 이전에 계산했던 정렬된 리스트를 그대로 사용합니다.
  • 복잡한 계산을 매번 다시 할 필요가 없어서 성능이 좋아집니다.

📌 상황 1: 복잡한 계산 결과를 캐싱하여 렌더링 성능 최적화

  • 사용자가 검색어를 입력할 때 필터링된 데이터를 보여주는 경우
const Search = ({ items, query }) => {
const filteredItems = useMemo(() => {
console.log("필터링 중...");
return items.filter((item) =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]);
return (
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};
  • 사용자가 검색어를 입력할 때마다 리스트를 필터링하면 비용이 커질 수 있습니다.
  • useMemo를 사용하여 itemsquery가 변경될 때만 필터링 로직을 다시 실행합니다.

📌 상황 2: React 컴포넌트에 비싼 연산이 있는 경우

  • 데이터 정렬 후 화면에 표시하는 경우
const SortedList = ({ numbers }) => {
const sortedNumbers = useMemo(() => {
console.log("리스트 정렬 중...");
return [...numbers].sort((a, b) => a - b);
}, [numbers]);
return <div>{sortedNumbers.join(", ")}</div>;
};
  • 정렬 연산은 데이터 크기에 따라 시간이 많이 걸릴 수 있습니다. useMemo로 불필요한 연산을 방지할 수 있습니다.

📌 상황 3: 의존성을 가진 값이 반복적으로 계산될 때

  • 스타일링 객체를 생성하는 경우
const Component = ({ isActive }) => {
const style = useMemo(() => {
return { color: isActive ? "blue" : "gray" };
}, [isActive]);
return <div style={style}>Hello World</div>;
};
  • 리액트는 객체의 참조 비교를 하기 때문에 매번 새로운 객체를 생성하면 불필요한 리렌더링이 발생할 수 있습니다. 이 때 useMemo로 동일한 참조를 재사용하면 성능 개선에 도움이 됩니다.

useCallback

  • 함수를 다시 계산하지 않고 이전 결과를 재사용하는 메모이제이션을 합니다.
  • React에서 함수는 컴포넌트가 다시 렌더링될 때마다 새로 만들어집니다. useCallback은 함수를 불필요하게 새로 만들지 않게 하거나, 자식 컴포넌트를 불필요하게 다시 렌더링 시키고 싶지 않는 상황에서 사용합니다.
  • 의존성 배열이 변경되지 않으면 기존 함수 참조를 재사용합니다.
  • 의존성이 변하지 않으면 기존 함수 참조를 재사용하지만, 함수 내부에서 사용하는 값은 의존성 배열에 따라 최신 상태를 참조하지 못할 수 있으므로, 최신상태를 참조하려면 의존성 배열에 그 값을 포함해야 합니다.
  • 주로 자식 컴포넌트에 함수를 전달할 때나 의존성이 필요한 함수에서 사용합니다.

📌 상황1 : 자식 컴포넌트의 불필요한 재렌더링 방지

  • 부모 컴포넌트에서 자식 컴포넌트로 이벤트 핸들러를 전달하는 상황
const Child = React.memo(({ onClick }) => {
console.log("Child 렌더링");
return <button onClick={onClick}>클릭</button>;
});
const Parent = () => {
const handleClick = useCallback(() => {
console.log("버튼 클릭!");
}, []);
return <Child onClick={handleClick} />;
};
  • 만약 useCallback을 쓰지 않으면, Parent가 다시 렌더링될 때마다 handleClick 함수가 새로 만들어져서 Child도 불필요하게 다시 렌더링됩니다.
  • 하지만 useCallback을 사용하면 Parent가 다시 렌더링되더라도 handleClick 함수가 새로 생성되지 않습니다. 따라서 Child 컴포넌트도 재렌더링되지 않아 불필요한 렌더링을 방지합니다.

📌 상황2: 성능 최적화를 위해 불필요한 이벤트 핸들러 재생성 방지

  • 리스트 항목에 이벤트 핸들러를 전달해야 하는 경우
const List = ({ items }) => {
const handleClick = useCallback((item) => {
console.log(item.name);
}, []);
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => handleClick(item)}>
{item.name}
</li>
))}
</ul>
);
};
  • useCallback으로 handleClick 함수를 고정하여 매번 새로 생성하지 않습니다.

useEventCallback

import {useCallback, useEffect, useRef} from 'react';
export function useEventCallback<Callback extends (...args: any[]) => any>(
callback: Callback,
) {
const callbackRef = useRef<Callback>(callback);
// callback이 바뀔 때마다 최신 값을 callbackRef에 저장
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 항상 같은 함수 참조를 반환, 하지만 내부적으로 최신 callback을 호출
return useCallback(
(...args: any[]) => {
return callbackRef.current(...args); // 최신 callback 사용
},
[callbackRef],
) as Callback;
}
  • 최신 상태를 기억하는 커스텀 훅으로 작성된 useEventCallback입니다.
  • useRef를 사용해 callback을 저장하고, useEffect로 callback이 변경될 때마다 useRef를 최신 값으로 업데이트한다. useCallback으로 이벤트가 발생할 때 저장된 최신 callback을 실행합니다.
import { useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useEventCallback(() => {
console.log("현재 카운트:", count);
});
return (
<>
<button onClick={() => setCount((prev) => prev + 1)}>증가</button>
<button onClick={handleClick}>현재 카운트 확인</button>
</>
);
}
  • useEventCallback은 최신 상태를 보장한다. 내부적으로 useRef를 사용해 콜백 참조를 고정하고, useRef.curren를 통해 항상 최신 callback을 호출합니다.

📌 상황 1: 최신 상태를 참조해야 하는 비동기 작업

  • 사용자가 입력한 키워드를 기반으로 서버에서 검색 결과를 가져오는 컴포넌트
import { useState } from "react";
import { useEventCallback } from "./useEventCallback";
const SearchComponent = () => {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// useEventCallback을 사용해 항상 최신 query 값을 참조하는 API 호출 핸들러
const fetchSearchResults = useEventCallback(async () => {
if (!query) return; // 빈 검색어일 경우 실행하지 않음
try {
const response = await fetch(`{{BASE_URL}}/search?q=${query}`);
const data = await response.json();
setResults(data.results);
} catch (error) {
console.error("검색 결과를 가져오는 중 오류 발생:", error);
}
});
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="검색어를 입력하세요"
/>
<button onClick={fetchSearchResults}>검색</button>
<ul>
{results.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
</div>
);
};
export default SearchComponent;
  • 사용자가 검색어를 입력한 뒤 검색 버튼을 누르면 최신 상태의 query 값을 기반으로 API를 호출해야 합니다.
  • 일반적으로 useCallback을 사용할 경우, query를 의존성 배열에 포함해야 합니다. 하지만 의존성이 변할 때마다 새로운 함수 참조가 생성됩니다.
  • useEventCallback은 의존성 배열 없이도 항상 최신 query 값을 안전하게 참조할 수 있습니다.
  • 버튼 클릭 시, 최신 query 값을 기반으로 API를 호출하므로 상태 관리가 간단해집니다.

📌 상황 2: 이벤트 핸들러에서 최신 상태를 안전하게 참조

const Component = () => {
const [messages, setMessages] = useState([]);
const handleMessage = useEventCallback((newMessage) => {
setMessages((prevMessages) => [...prevMessages, newMessage]);
});
useEffect(() => {
const socket = new WebSocket("ws://example.com");
socket.onmessage = (event) => handleMessage(event.data);
return () => socket.close();
}, [handleMessage]);
return <div>{messages.join(", ")}</div>;
};
  • handleMessage는 항상 최신 상태(messages)를 안전하게 참조할 수 있어 상태 관리가 간단해집니다.
  • 일반 이벤트 핸들러로 구현하면 의존성 배열 관리가 복잡해질 수 있습니다.

💡 useMemo vs. useCallback vs. useEventCallback

useMemo

  • 연산 비용이 큰 작업(필터링, 정렬 등) 또는 반복적으로 계산이 발생할 때 useMemo를 통해 연산 결과(값)을 메모이제이션합니다.

useCallback

  • 함수가 자주 새로 만들어지는 것을 막고, 자식 컴포넌트의 불필요한 재렌더링을 방지하고 싶을 때 사용합니다.
  • 부모 컴포넌트에서 자식 컴포넌트로 이벤트 핸들러를 전달할 때 사용합니다. 자식 컴포넌트의 재렌더링을 막아 불필요한 렌더링을 방지하기 위함입니다. 또는 이벤트 핸들러가 자주 재생성되는 경우 사용합니다.

useEventCallback

  • 최신 상태나 값을 사용하는 이벤트 핸들러가 필요할 때 사용합니다.
  • 비동기 함수나 상태가 자주 변경되는 경우 적합합니다.
  • 상태가 변경되더라도 의존성을 따로 관리하지 않아도 최신 값을 사용합니다.

© 2024 Park Seohyun