확장 프로그램 React devtools 설치
개발자 도구에서 React라는 탭을 통해 현재 리액트 컴포넌트의 구성을 확인할 수 있다.
설정(톱니바퀴) - Highlight Updates를 체크하면 렌더링이 발생하는 곳을 확인할 수 있다.
useCallback은 useMemo를 기반으로 만들어졌다.
useMemo는 특정 결과값을 재사용할 때 사용하는 반면, useCallback은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.
useCallback 사용법도 동일하게 첫번째 파라미터로 넘어온 함수를, 두번째 파라미터로 넘어온 배열 내의 값이 변경될 때 까지 저장해놓고 재사용할 수 있게 해준다.
const memoizedCallback = useCallback(함수, 배열);
예를 들어 어떤 React 컴포넌트 함수 안에 함수가 선언이 되었다면 이 함수는 해당 컴포넌트가 렌더링 될 때 마다 새로운 함수가 생성된다.
const add = () => x + y;
하지만 useCallback을 사용하면, 해당 컴포넌트가 렌더링되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
즉,x
또는 y
값이 바뀌면 새로운 함수가 생성되어 add
변수에 할당되고, x
와 y
값이 동일하다면 다음 렌더링 때 이 함수를 재사용한다.
const add = useCallback(() => x + y, [x, y]);
사실 컴포넌트가 렌더링될 때 마다 함수를 새로 선언하는 것은 자바스크립트가 브라우저에서 얼마나 빨리 실행되는지를 생각해보면 성능상 큰 문제가 되지 않는다.
따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 useMemo와 마찬가지로 큰 의미가 없거나 오히려 손해인 경우도 있다.
useCallback Hook 함수는 어떻게 쓰일 때 의미있는 성능 향상을 기대할 수 있을까?
자바스크립트에서 함수간의 동등함은 함수도 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어난다.
이러한 자바스크립트 특성은 React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 props로 넘길 때 예상치 못한 성능 문제로 이어질 수 있다.
> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = () =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
많은 React Hook 함수들이 불필요한 작업을 줄이기 위해서 두번째 인자로, 첫번째 함수가 의존해야하는 배열을 받는다.
예를 들어, useEffect 함수는 두번째 인자로 넘어온 의존 배열이 변경될 때만 첫번째 인자로 넘어온 함수를 호출한다.
위의 예시에서 컴포넌트에서 API를 호출하는 코드는 fetchUser
함수가 변경될 때만 호출된다.
🚨 여기서 자바스크립트가 함수의 동등성을 판단하는 방식 때문에 예상치 못한 무한 루프에 빠지게 된다.
fetchUser
는 함수이기 때문에, userId
값이 바뀌든 말든 컴포넌트가 렌더링 될 때 마다 새로운 참조값으로 변경이 된다.
그러면 useEffect 함수가 호출되어 user
상태값이 바뀌고 그러면 다시 렌더링이 되고, 그럼 또 다시 useEffect 함수가호출되는 악순환이 반복된다.
import React, { useState, useEffect } from "react";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = useCallback(
() =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user),
[userId]
);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
이와 같은 상황에서 useCallback Hook 함수를 이용하면 컴포넌트가 다시 렌더링되더라도 fetchUser
함수의 참조값을 동일하게 유지시킬 수 있다. 따라서 의도했던 대로 useEffect에 넘어온 함수는 userId
값이 변경되지 않는 한 재호출 되지 않게 된다.
useCallback Hook 함수는 자식 컴포넌트의 불필요한 렌더링을 줄이기 위해서 React.memo 함수와도 사용할 수 있다.
예를 들어, 방이름(room), 조명 켜짐 여부(on), 조명 제어 함수(toggle)를 props로
Light
컴포넌트를 작성해보자.
function Light({ room, on, toggle }) {
console.log({ room, on });
return (
<button onClick={toggle}>
{room} {on ? "💡" : "⬛"}
</button>
);
}
그리고 React.memo 함수로 이 컴포넌트를 감싸준다.
Light = React.memo(Light);
이렇게 React 컴포넌트 함수를 React.memo 함수로 감싸주면 해당 컴포넌트 함수는 props 값이 변경되지 않는 한 다시 호출되지 않는다.
다음으로 3개의 방의 스위치를 중앙 제어해주는
SmartHome
컴포넌트를 작성한다.
function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = () => setMasterOn(!masterOn);
const toggleKitchen = () => setKitchenOn(!kitchenOn);
const toggleBath = () => setBathOn(!bathOn);
return (
<>
<Light room="침실" on={masterOn} toggle={toggleMaster} />
<Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room="욕실" on={bathOn} toggle={toggleBath} />
</>
);
}
이 컴포넌트를 이용해서 침실의 조명을 켜보면 침실 뿐만 아니라 다른 모든 방에 대한 Light 컴포넌트 함수가 호출이 되는 것이 콘솔 로그로 확인된다.
{room: "침실", on: true}
{room: "주방", on: false}
{room: "욕실", on: false}
조명을 키거나 끄는 방에 대한 Light
컴포넌트 함수만 호출되게 하고 싶어서, React.memo를 사용한 것인데 무엇이 문제일까? 바로 조명을 제어할 때 쓰이는 toggleMaster
, toggleKitchen
, toggleBath
함수의 참조값이 SmartHome
컴포넌트가 렌더링될 때마다 모두 바뀌어버리기 때문이다.
이 문제를 해결하려면 모든 조명 제어 함수를 useCallback Hook 함수로 감싸고 두번째 인자로 각 함수가 의존하고 있는 상태를 배열로 넘겨야 한다.
export default function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
const toggleKitchen = useCallback(
() => setKitchenOn(!kitchenOn),
[kitchenOn]
);
const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);
return (
<>
<Light room="침실" on={masterOn} toggle={toggleMaster} />
<Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room="욕실" on={bathOn} toggle={toggleBath} />
</>
);
}
이제는 침실에 조명을 켜보면 침실에 대한 Light 컴포넌트 함수만 호출되는 것을 확인할 수 있다.
{room: "침실", on: true}
따라서, useCallback을 사용하시기 전에 실질적으로 얻을 수 있는 성능 이점이 어느 정도인지 반드시 직접 측정 해보고 사용하는 것이 좋다.
출처