useMemo는 렌더링 최적화를 위한 Hook 중 하나로, 특정 value값을 재사용하고자 할 때 사용하는 Hook이다.
function Calculator({value}) {
const result = calculate(value);
return (
<>
<div>{result}</div>
</>
)
}
Calculator 컴포넌트는 props로 넘어온 value 값을 calculate 함수에 인자로 넘겨 result 값을 구한 후, div 엘리먼트로 출력하고 있다.
만일, calculate 함수가 매우 복잡한 연산을 하는 함수여서 계산된 값을 반환하는 데 몇초 이상의 시간이 걸린다고 가정해보면,
- 해당 컴포넌트는 렌더링할 때마다 이 함수를 계속해서 호출할 것이고
- 그 때마다 시간이 몇 초 이상 소요가 될 것이다.
- 이 시간 지연은 렌더링에 영향을 미쳐 사용자로 하여금 앱의 로딩 속도가 느리다는 생각을 하게 만들 수 있다.
이러한 상황에서 사용하는 것이 useMemo이다.
// useMemo를 사용하기 전에는 꼭! import를 해서 불러와야 한다.
import React, { useMemo } from 'react';
function Calculator({value}) {
const result = useMemo(() => calculate(value), [value]);
return (
<>
<div>{result}</div>
</>
)
}
만일, 렌더링할 때마다 value 값이 계속 바뀌는 것이 아니라면, 렌더링 할 때마다 굳이 calculate 함수를 호출할 필요 없이 이 값을 어딘가에 저장했다가 다시 꺼내어 쓰면 된다.
이 때, 사용하는 것이 useMemo 이다.
위의 코드와 같이 기존에 호출하던 calculate 함수를 useMemo의 첫 번째 인자로 넘기고, 두 번째 인자로 props인 value를 넘긴다. 이렇게 해주면, calculate 함수는 value props가 바뀔 때만 호출이 되고, value props가 동일할 때는 최초 호출 결과가 계속해서 재사용된다.
useCallback 또한, useMemo와 마찬가지로 렌더링 최적화를 위한 Hook 중 하나이다.
값의 재사용을 위해 사용한 useMemo와 달리, useCallback은 함수의 재사용을 위해 사용한다.
function Calculate({x, y}) {
const add = () => x + y;
return (
<>
<div>{add()}</div>
</>;
)
}
위의 코드를 보면, 현재 Calculate 컴포넌트 내에는 props로 넘어온 x, y값을 더해 div 엘리먼트로 그 값을 출력하고 있는 add 함수가 선언되어 있는 상태이다.
이 함수는 해당 컴포넌트가 렌더링될 때마다 새롭게 만들어지게 된다.
useMemo의 경우와 마찬가지로
- 해당 컴포넌트가 리렌더링 되더라도
- add 함수가 의존하고 있는 값인 x,y 값이 바뀌지 않는다고 가정한다면,
- 함수 또한 메모리 어딘가에 저장해뒀다가 다시 꺼내어 쓸 수 있을 것이다.
이 상황에서 useCallback Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 이상, 기존의 함수를 계속해서 반환하게 된다. 위의 예시의 경우에는 x, y 값이 바뀌지 않는다면 다음 렌더링 때 이 함수를 다시 사용하게 된다.
// useCallbaock의 경우에도 사용하기 전에 꼭! import를 해서 불러와야 한다.
import React, { useCallback } from 'react';
function Calculator({x, y}) {
const add = useCallback(() => x + y, [x,y]);
return (
<>
<div>{add()}</div>
</>;
)
}
위의 코드와 같이 기존에 호출하던 add 함수를 useCallback의 첫 번째 인자로 넘기고, 두 번째 인자로 add 함수가 의존하는 값들인 x, y를 넘긴다. 이렇게 해주면, x 또는 y 값이 바뀔 때만 새로운 함수가 생성되어 add 변수에 할당이 되고, x와 y값이 동일할 경우 다음 렌더링 때 이 함수를 재사용한다.
사실, useMemo에 비하면 useCallback를 사용할 때는 큰 최적화를 느낄 수 없다.
그래서 단순히 컴포넌트 내에서 함수를 반복해 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해일 수도 있다.
그럼 도대체 useCallback은 어떻게 쓸 때 의미있는 성능 향상을 기대할 수 있을까?
이를 알기 위해서는 우선 JS 내에서 함수 간의 동등함이 어떻게 결정되는 지 알 필요가 있다.
useCallback과 참조 동등성
React는 JS 언어로 만들어진 오픈소스 라이브러리이기 때문에 기본적으로 JS의 문법을 따라간다.
JS 내에서 함수는 객체이다. 객체의 경우, 참조자료형으로서 메모리에 저장할 때 값을 저장하는 것이 아니라 값의 주소를 저장하기 때문에 반환하는 값이 같을 지라도, === 연산자로 비교하면 'false'가 나온다.
function doubleFactory() {
return (a) => 2 * a;
}
const double1 = doubleFactory();
const double2 = doubleFactory();
double1(8); // 2 * 8 = 16
double2(8); // 2 * 8 = 16
double1 === double2 // false
double1 === double1 // true
위의 코드로 예시를 들어보면, double1과 double2는 같은 함수를 할당했음에도 메모리의 주소 값이 다르기 때문에 다른 함수로 본다.
이러한 JS의 특성은 React에도 적용되는데, React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 props로 넘길 때 새롭게 호출되는 함수는 기존의 함수와 같은 함수가 아니다.
이 때, 이로 인한 예상치 못한 성능 문제가 발생할 수 있다.
이럴 때, useCallback을 사용한다.
useCallback은 함수 자체를 저장하여 다시 사용하는 것이기 때문에 즉, 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것이기 때문에 위의 예상치 못한 성능 문제를 막을 수 있다.
Custom Hooks는 간단히 말해 개발자가 스스로 커스텀한 훅을 의미하며, 이를 이용하여 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
Custom Hooks는 여러 url을 fetch할 때, 여러 input에 의한 상태 변경을 할 때등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용합니다.
Custom Hooks의 대표적인 장점에는 3가지가 있다.
React 공식 문서에 나와있는 예제 코드를 사용해 생각해보자.
아래의 코드는 친구가 online인지 offline인지 여부를 return 하는 컴포넌트인 FriendStatus, 친구가 online일 때 초록색으로 변하는 컴포넌트인 FriendListItem을 구현한 코드이다.
//FriendStatus 컴포넌트
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
//FriendListItem 컴포넌트
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{color: isOnline ? 'green' : 'black'}}>{props.frined.name}</li>
);
}
위의 코드를 보면 두 컴포넌트 모두 똑같이 쓰이는 로직이 존재하고 있다는 것을 확인할 수 있다. 이 로직을 빼내어 Custom Hook으로 만들면 두 컴포넌트에서 모두 공유할 수 있다.
두 컴포넌트에서 사용하기 위해서 동일하게 사용되고 있는 로직을 빼내어 함수 useFriendStatus 로 만들어보자.
본격적으로 만들기 전, Custom Hook을 정의할 때의 일종의 규칙에 대해 알아볼 필요가 있다.
- Custom Hook 을 정의할 때는 함수의 이름 앞에 'use'를 붙여야 한다.
- 대개의 경우에는 프로젝트 내의 hooks 디렉토리 내에 Custom Hook을 위치시킨다.
- Custom Hook으로 만들 때 함수는 조건부 함수가 아니여야 한다. 즉, return 하는 값이 조건부여서는 안된다.
이제 위의 로직을 Custom Hook으로 만들어보자.
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return isOnline;
}
이렇게 만들어진 Custom Hook은 Hook 내부에 useState와 같은 React Hook을 사용하여 작성할 수 있다. 일반 함수 내부에서는 React Hook을 불러서 사용할 수 없지만 Custom Hook에서는 가능하다.
이제 만들어진 useFriendStatus Hook을 두 컴포넌트에 적용해보자.
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{color: isOnline ? 'green' : 'black'}}>{props.friend.name}</li>
);
}
이와 같이 로직을 분리하여 Custom Hook으로 만들면 아까 전의 코드에 비해 더 직관적으로 확인이 가능해진다.
그러나 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하고 있다고는 할 수 없다. 두 컴포넌트는 그저 로직만 공유할 뿐, state는 각각의 컴포넌트 내에서 독립적으로 정의되어 있다.