React의 공식문서를 보면 Hook에 대해 이런 문구가 있다.
Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해 줍니다.
Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메서드를 의미한다. Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다.
...
render(){
/* 클래스 컴포넌트는 render() 안에서 변수를 작성할 수 있다. */
const [counter, setCounter] = useState(0);
...
}
1. 리액트 함수의 최상위에서만 호출해야 한다.
반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 우려가 있다.
...
if(counter) {
const [sample, setSample] = useState(0);
}
...
컴포넌트 안에는 useState나 useEffect 같은 Hook들이 여러 번 사용될 수 있는데, React는 이 Hook을 호출되는 순서대로 저장을 해놓는다.
그런데 조건문, 반복문 안에서 Hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고, 결국 예기치 못한 버그를 초래하게 될 수 있다.
2. 오직 리액트 함수 내에서만 사용되어야 한다.
이는 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 의미다.
...
window.onload = function () {
useEffect(() => {
// do something...
}, [counter]);
}
...
애초에 Hook은 React의 함수 컴포넌트 내에서 사용되도록 만들어진 메서드이기 때문에 근본적으로 일반 JavaScript 함수 내에서는 정상적으로 돌아가지 않는다.
useMemo은 특정 값(value)을 재사용하고자 할 때 사용하는 Hook이다.
function Calculator({value}){
const result = calculate(value);
return <>
<div>
{result}
</div>
</>;
}
만약 여기서 calculate가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다고 가정해 보자. 그렇다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그때마다 시간이 몇 초 이상 소요가 될 것이다. 이 몇 초의 지연은 렌더링에도 영향을 미칠 것이고, 사용자는 “앱의 로딩 속도가 느리네?”라는 생각을 하게 될 것이다.
/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 한다. */
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <>
<div>
{result}
</div>
</>;
}
이런 식으로 useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 된다. 이는 메모이제이션(Memoization) 개념과 긴밀한 관계가 있다.
기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다. 이 메모이제이션을 적절히 사용한다면 굳이 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화할 수 있다.
useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook이다. useMemo는 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook이다.
function Calculator({x, y}){
const add = () => x + y;
return <>
<div>
{add()}
</div>
</>;
}
/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 한다. */
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>
{add()}
</div>
</>;
}
사실 useCallback만 사용해서는 useMemo에 비해 괄목할 만한 최적화를 느낄 수는 없다. 왜냐하면 useCallback은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 Hook이기 때문이다. 따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
자식 컴포넌트의 props로 함수를 전달해 줄 때 이 useCallback을 사용하기가 좋다.
개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용한다. 이를 이용하면
해당 컴포넌트는 실제 React 공식 문서에 있는 컴포넌트다.
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
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 : 친구가 online일 때 초록색으로 표시하는 컴포넌트
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.friend.name}
</li>
);
}
FriendStatus
컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem
컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트다. 이 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재하고 있다. 이 로직을 빼내서 두 컴포넌트에서 Custom Hook을 사용한다면 공유가 가능하다.
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만든다. 이렇게 Custom Hook을 정의할 때는 일종의 규칙이 필요하다.
이렇게 만들어진 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을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의되어 있다.