리액트 성능 최적화와 관련된 내용을 살펴볼때마다 useMemo()와 달리 제대로 이해되지 않는 부분이 많았습니다. 이와 관련하여 알게된 내용을 정리합니다.
일반적으로 useMemo()와 useCallback()은 다음과 같은 방식으로 이해됩니다.
컴포넌트 내에 다음과 같은 함수가 선언되어 있으면 컴포넌트가 랜더링 될 때 마다 새로운 함수가 생성됩니다.
const add = () => x+y;
하지만 useCallback()을 사용하면 해당 컴포넌트가 랜더링 되더라도 x, y값이 변하지 않으면 동일한 함수를 재사용하도록 할 수 있습니다.
const add = useCallback(() => x+y, [x,y]);
위와 같은 방식으로 useCallback은 두 번째 인자의 값을 기준으로 함수를 새로 생성하지 않도록 도와줍니다.
그런데 여기서 의문점이 생깁니다.
직관력이 뛰어난 개발자들은 위의 의문이 들지 않을 수 있지만, 저는 위의 의문점들이 계속해서 머리에 맴돌며 점점 더 이해가 되지 않았습니다.
결론부터 말해보겠습니다.
위와 같은 의문점은 useCallback()이 "함수의 재사용"으로 최적화를 한다는 착각에서 나옵니다.
useCallback()의 최적화 핵심은 "리랜더링"이 일어나지 않도록 하는 것입니다.
코드를 통해 살펴봅시다.
function User({ userId, login, toggle }) {
console.log({ userId, login });
return (
<button onClick={toggle}>
{userId} {login ? "login" : "logout"}
</button>
)
}
이제 React.memo를 이용해 props가 동일하다면 다시 호출되지 않도록 하여 성능 최적화를 진행합니다.
User = React.memo(User);
다음으로 친구들의 상태를 관리하는 FriendsState컴포넌트를 작성합니다.
function FriendsState() {
const [friend1, setFriend1] = useState(false);
const [friend2, setFriend2] = useState(false);
const [friend3, setFriend3] = useState(false);
const toggleFriend1 = () => setFriend1(!friend1);
const toggleFriend2 = () => setFriend1(!friend2);
const toggleFriend3 = () => setFriend1(!friend3);
return (
<>
<User userId="friend1" login={friend1} toggle={toggleFriend1} />
<User userId="friend2" login={friend2} toggle={toggleFriend2} />
<User userId="friend3" login={friend3} toggle={toggleFriend3} />
</>
);
}
이제 친구 목록의 아무나 로그인 상태를 바꿔봅시다. 상태가 변한 컴포넌트만 호출하고 싶지만 모든 컴포넌트가 전부 호출되는 것을 확인할 수 있습니다.
{userId: "friend1", login: true}
{userId: "friend2", login: false}
{userId: "friend3", login: false}
이런 일이 벌어지는 이유는 FriendsStatus컴포넌트가 랜더링 될 때 마다 toggleFriend{n}에 해당하는 함수의 참조값이 모두 바뀌어 버리기 때문입니다.
즉, User컴포넌트 입장에선 props의 toggle이 변화하니 계속 새로 호출이 되는 것이죠.
문제 해결을 위해 useCallback()을 사용해봅시다.
function FriendsState() {
const [friend1, setFriend1] = useState(false);
const [friend2, setFriend2] = useState(false);
const [friend3, setFriend3] = useState(false);
const toggleFriend1 = useCallback(() => setFriend1(!friend1), [friend1]);
const toggleFriend2 = useCallback(() => setFriend2(!friend2), [friend2]);
const toggleFriend3 = useCallback(() => setFriend3(!friend3), [friend3]);
return (
<>
<User userId="friend1" login={friend1} toggle={toggleFriend1} />
<User userId="friend2" login={friend2} toggle={toggleFriend2} />
<User userId="friend3" login={friend3} toggle={toggleFriend3} />
</>
);
}
이제 친구의 상태를 변경하면 해당하는 User 컴포넌트만 호출되는 것을 확인할 수 있습니다.
{userId: "friend1", login: true}
이처럼 useCallback()함수는 컴포넌트의 랜더링을 최적화 할 때 사용됩니다. 그러나 위에서 살펴본 것 처럼 useCallback()을 잘못 사용하면 오히려 최적화에 방해가 될 수 있습니다.
최적화의 초점이 "함수의 재사용"이 아닌 "그로 인한 랜더링 최적화"에 있다는 사실을 기억해야 합니다.
출처
혹시 async / await에 대한 글은 없을까요?