[react] useCallback

IKNOW·2024년 5월 4일

useCallback은 리렌더링간 함수 정의를 캐시해주는 Hook이다.

const cachedFn = useCallback(fn, dependencies)
  • fn: 캐싱할 함수. 어떤 인자나 반환값을 반환해도 된다. 첫 렌더링시에 이 함수를 반환한다. (함수를 호출하는 것이 아니다!) 이후 컴포넌트가 렌더링 되는 경우 dependencies가 동일하다면 React는 같은 함수를 반환한다. 만약 dependencies가 변경되었다면, 새롭게 함수를 생성하고 저장한다.
  • dependencies: fn에서 참조되는 반응형 값들의 배열이다. Object.is 알고리즘을 사용해 비교한다.
  • return: 최초 렌더링시에는 fn함수를 그대로 반환하고, 다음 렌더링 시에는 dependencies를 비교하고
    • 변경되지 않았다면 → 저장된 fn을 반환한다
    • 변경되었다면 → 새로운 fn 함수를 만들어서 반환한다.

사용법

컴포넌트의 재 렌더링 스킵

렌더링 성능을 최적화 할때는 자식 컴포넌트로 넘기는 함수를 캐싱해야 한다. 렌더링간 함수를 캐싱하기 위해서는 함수를 useCallback으로 감싸면 된다.

function ProductPage({productId, referrer, theme}) {
	const handleSubmit = useCallback((orderDetails) => {
		post('/product' + productId + '/buy', {
			referrer,
			orderDetails,
		});
	}, [productId, referrer]);

이 handleSubmit함수를 자식 컴포넌트로 전달해보자

function ProductPage({ productId, referrer, theme }) {
	return (
		<div className={theme}>
			<ShippingForm onSubmit={handleSubmit} />
		</div>
	}

자식 컴포넌트 ShippingForm은 props가 변하지 않았다면 렌더링을 건너뛸 수 있다. handleSubmit이 동일하다면 ShippingForm을 굳이 렌더링 할 필요가 없다.

리액트 컴포넌트는 순수하게 작성해야하기 때문에 인풋이 같다면 아웃풋도 같아야한다. 무조건.

useCallback은 성능 최적화를 위해서 사용해야한다. useCallback 없이 동작하지 않는다면 잘못 작성한것이다.

Memoized 콜백에서 상태 업데이트

function TodoList() {
	const [todos, setTodos] = useState([]);
	
	const handleAddTodo = useCallback((text) => {
		const newTodo = { id: nextId++, text }
		setTodos([...todos, newTodo])
	}, [todos])

위 handleAddTodo는 todos상태가 변할때마다 새로 생성된다. 하지만 업데이트함수를 사용해서 의존성을 제거 할 수 있다.

function TodoList() {
	const [todos, setTodos] = useState([]);
	
	const handleAddTodo = useCallback((text) => {
		const newTodo = { id: nextId++, text }
		setTodos([...todos, newTodo])
		}, [])

Effect가 너무 자주 실행되는 것을 방지

useEffect에서 함수를 호출해야 하는 경우

function ChatRoom({ roomId }) {
	const [message, setMessage] = useState("")
	
	function createOptions() {
		return {
			serverUrl: 'https://...',
			roomId: roomId
		}
	}
	
	useEffect(() => {
		const options = createOptions();
		const connection = createConnection();
		connection.connect();
	}, [createOptions])
}

이렇게 useEffect에서 함수를 호출하는 경우 모든 반응형 값들이 Effect의 의존성으로 선언되어야 한다. 이경우 createOptions가 의존성으로 선언되고 createOptions가 변경될때마다 useEffect가 실행되고 connect하게된다.

const createOptions = useCallback(() => {
	return {
		serverUrl: 'https://...',
		roomId: roomId
	}
}, [roomId])

이렇게 createOptions를 useCallback으로 감싸게 되면 roomId가 변경되지 않는 이상 createOptions는 새로 생성되지 않는다.

다만 여전히 useEffect에 함수에 대한 의존성이 필요한 것이 불편하다면 createOptions를 useEffect내부로 이동시킬 수 있다.

useEffect(()=> {
	function createOptions() {
		return {
			serverUrl: "https://...",
			roomId: roomId
		}
	}
	const options = createOptions()
	const connection = createConnection()
	connection.connect()
	return () => connection.disconnect()
	}, [roomId])

useCallback은 필요하지않고 코드가 더 간단해 진것을 확인 할 수 있다.

Custome Hook 최적화

커스텀 훅을 작성하는 경우 반환하는 함수는 모두 useCallback으로 감싸는 것이 좋다.

function useRouter() {
	const { dispatch } = useContext(RouterStateContext);
	
	const navigate = useCallback((url) => {
		dispatch({ type: 'navigate', url })
	}, [dispatch])
	
	const goBack = useCallback(() => {
		dispatch({ type: 'back' })
	}, [dispatch])
	
	return {
		navigate,
		goBack,
	}
}
profile
조금씩,하지만,자주

0개의 댓글