useMemo
- 값을 메모이제이션합니다.
- 의존성 배열이 변경되지 않으면 기존 계산 결과를 재사용합니다.
- 주로 복잡한 계산이나 렌더링 비용이 높은 작업을 최적화할 때 사용합니다.
const Parent = ({ numbers }) => {
const sortedNumbers = useMemo(() => {
console.log("리스트 정렬 중...");
return numbers.sort((a, b) => a - b);
}, [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
를 사용하여 items
와 query
가 변경될 때만 필터링 로직을 다시 실행합니다.
📌 상황 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);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
return useCallback(
(...args: any[]) => {
return callbackRef.current(...args);
},
[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([]);
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
- 최신 상태나 값을 사용하는 이벤트 핸들러가 필요할 때 사용합니다.
- 비동기 함수나 상태가 자주 변경되는 경우 적합합니다.
- 상태가 변경되더라도 의존성을 따로 관리하지 않아도 최신 값을 사용합니다.