2023.01.31 자신만의 Hook 만들기(Custom Hook)
자신만의 Hook을 만들면 컴포넌트 로직을 함수로 뽑아내어 재사용할 수 있습니다.
예제 코드를 통해 어떤 식으로 커스텀 훅을 만드는지 배워보도록 합니다.
Effect Hook 사용하기를 배울 때,
채팅 애플리케이션에서 친구가 온라인 상태인지 아닌지에 대한 메시지를 표시하는 컴포넌트를 보았을 것입니다.
import React, { useState, useEffect } from 'react';
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';
}
FriendStatus라는 컴포넌트는 isOnline이라는 state에 따라서 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트입니다.
그리고 동일한 웹사이트에서 연락처 목록이 있으며
그중에서 온라인 상태인 사용자들의 이름을 초록색으로 표시하는 상황을 가정해 보겠습니다.
이 컴포넌트 이름을 FriendListItem라고 합시다.
위의 코드와 비슷한 로직을 복사하여 FriendListItem 컴포넌트 안에 붙여넣을 수도 있지만,
가장 좋은 방법이라고 할 수는 없습니다.
import React, { useState, useEffect } from 'react';
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와 useState( ), useEffect( )훅을 사용하는 부분이 동일한 것을 볼 수 있습니다.
여러 곳에서 중복되는 코드인 것이죠.
그 대신 이 로직을 FriendStatus와 FriendListItem에서 공유하도록 하고 싶습니다.
기존의 리액에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우에
React에는 상태 관련 로직을 컴포넌트에서 공유하는 두 가지 전통적인 방법이 있는데
하지만 여기서는 중복되는 코드를 추출하여 커스텀 훅으로 만드는 새로운 방법을 사용해 보겠습니다.
Hook을 사용하여 트리에 컴포넌트를 더하지 않고 위의 문제를 해결하는 방법을 보도록 하겠습니다.
두 개의 자바스크립트 함수에서 같은 로직을 공유하고자 할 때는 또 다른 함수로 분리합니다.
컴포넌트와 Hook 또한 함수이기 때문에 같은 방법을 사용할 수 있습니다!
사용자 정의 Hook은 이름이 use로 시작하는 자바스크립트 함수입니다.
사용자 Hook은 다른 Hook을 호출할 수 있습니다.
예를 들자면, 아래의 useFriendStatus가 우리의 첫 번째 사용자 정의 Hook입니다.
import { useState, useEffect } from 'react';
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;
}
딱히 새로운 것은 없습니다.
로직은 위의 컴포넌트로부터 복사해왔습니다.
다만 컴포넌트에서처럼 다른 Hook들은 사용자 Hook의 위로 놓여야 하며
사용자 정의 Hook은 조건부 함수가 아니어야 합니다.
React 컴포넌트와는 다르게 사용자 정의 Hook은 특정한 시그니처가 필요 없습니다.
무엇을 인수로 받아야 하며 필요하다면 무엇을 반환해야 하는 지를 사용자가 결정할 수 있습니다.
다시 말하지만, 보통의 함수와 마찬가지입니다.
이름은 반드시 use로 시작해야 하는데 그래야만 한눈에 보아도 Hook 규칙이 적용되는지를 파악할 수 있기 때문입니다.
useFriendStatus Hook의 목표는 친구의 온라인/오프라인 상태를 구독하기 위함입니다.
이를 위하여 파라미터로 friendID를 인수로 받고 해당 사용자의 온라인 상태의 여부를 반환합니다.
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
return isOnline;
}
이제 사용자 정의 Hook을 어떻게 이용하는지 보겠습니다.
처음 우리의 목표는 FriendStatus와 FriendListItem 컴포넌트에 중복되어있는 로직을 제거하는 것이었습니다.
그리고 두 컴포넌트 모두 친구의 온라인 상태 여부를 알아야 하죠.
이제 이 로직을 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>
);
}
본래의 예시와 동일한 코드인가요?
네 정확히 같은 방식으로 작동합니다.
자세히 보면 작동방식에 어떤 변화도 없다는 것을 알 수 있습니다.
바뀐 것은 오로지 공통의 코드를 뽑아내 새로운 함수로 만든 것뿐입니다.
사용자 정의 Hook은 React의 특별한 기능이라기보다 기본적으로 Hook의 디자인을 따르는 관습입니다.
사용자 정의 Hook의 이름은 “use”로 시작되어야 하나요?
네 그렇습니다. 이 관습은 아주 중요합니다.
이를 따르지 않으면 특정한 함수가 그 안에서 Hook을 호출하는지를 알 수 없기 때문에 Hook 규칙의 위반 여부를 자동으로 체크할 수 없습니다.
같은 Hook을 사용하는 두 개의 컴포넌트는 state를 공유하나요?
아니요. 사용자 정의 Hook은 상태 관련 로직(구독을 설정하고 현재 변숫값을 기억하는 것)을 재사용하는 메커니즘이지만
사용자 Hook을 사용할 때마다 그 안의 state와 effect는 완전히 독립적입니다.
사용자 정의 Hook은 어떻게 독립된 state를 얻는 건가요?
각각의 Hook에 대한 호출은 서로 독립된 state를 받습니다.
useFriendStatus를 직접적으로 호출하기 때문에
React의 관점에서 이 컴포넌트는 useState와 useEffect를 호출한 것과 다름없습니다.
또한 우리가 이전에 배웠듯이,
하나의 컴포넌트 안에서 useState와 useEffect를 여러 번 부를 수 있고 이들은 모두 완전히 독립적입니다.
Hook은 함수이기 때문에 Hook 사이에서도 정보를 전달할 수 있습니다.
상황설명을 위해 채팅 예시에 있는 다른 컴포넌트를 사용하겠습니다. 현재 선택된 친구가 온라인 상태인지를 표시하는 채팅 수신자 선택기입니다.
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
현재 선택된 친구의 ID를 recipientID state 변수에 저장하고 사용자가 <select> 선택기에 있는 다른 친구를 선택하면 이를 업데이트합니다.
useState Hook 호출은 recipientID state 변수의 최신값을 돌려주기 때문에 이를 useFriendStatus Hook에 인수로 보낼 수 있습니다.
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
이를 통해 지금 선택되어있는 친구의 온라인 상태 여부를 알 수 있습니다. 다른 친구를 선택하고 recipientID state 변수를 업데이트하면 useFriendStatus Hook은 이미 선택되어있는 친구의 구독을 해지하고 새로이 선택된 친구의 상태를 구독할 것입니다.
훅들 사이에서는 이러한 방법으로 데이터를 공유할 수 있습니다.
이번 실습에서는 useState( ), useEffect( ) 훅을 직접 사용해 보고
커스텀 훅을 직접 만들어 보도록 하겠습니다.
useCounter( ) 훅은 초기 카운트 값을 파라미터로 받아서
count라는 이름의 state를 생성하여 값을 제공하고 카운트 증가 및 감소를 편리하게
할 수 있도록 함수를 제공하는 훅입니다.
따라서 useCounter( )훅으로 어떤 함수 컴포넌트에서든지 카운트 기능을 쉽게 사용할 수 있습니다.
import React, { useState } from "react";
function useCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increaseCount = () => setCount((count) => count + 1);
const decreaseCount = () => setCount((count) => Math.max(count -1, 0));
return [count, increaseCount, decreaseCount];
}
export default useCounter;
다음은 useCounter( ) 훅을 사용하는 함수 컴포넌트를 만들어 보겠습니다.
이 컴포넌트를 사람을 수용하는 시설에서 사용한다고 가정해 보곘습니다.
Accommodate 컴포넌트는 앞에서 만든 useCounter( ) 훅을 사용하여 카운트를 관리합니다.
최대 카운트 개수는 MAX_CAPICITY라는 이름의 상수로 정의되어 있고,
카운트 개수가 최대 용량을 초과하면 경고 문구가 표시되며 더 이상 입장이 불가능해집니다.
여기에서 useEffect( ) 훅의 작동 방식을 확인하기 위해 일부러 두 개의 useEffect( )훅을 사용했습니다.
하나는 의존성 배열이 없는 형태이고
다른 하나는 있는 형태입니다.
의존성 배열이 없는 usEffect( )훅은 컴포넌트가 마운트된 직후에 호출되며 이후 컴포넌트가 업데이트될 때마다 호출됩니다.
그리고 의존성 배열이 있는 usEffect( )훅은컴포넌트가 마운트된 직후에 호출되며,
이후 count 값이 바뀔 때마다 호출되는데 이때 용량이 가득 찼는지 아닌지의 상태를 isFull이라는 state에 저장합니다.
import React, {useState, useEffect} from "react";
import useCounter from "./useCounter";
const MAX_CAPCITY = 10;
function Accommodate(props) {
const [isFull, setIsFull] = useState(false);
const [count, increaseCount, decreaseCount] = useCounter(0);
useEffect(() => {
console.log("----------------------");
console.log("useEffect() is called.");
console.log(`isFull: ${isFull}`);
});
useEffect(() => {
setIsFull(count >= MAX_CAPCITY);
console.log(`Current count value: ${count}`);
}, [count]);
return (
<div style={{ padding: 16}}>
<p>{`총 ${count}명 수용했습니다.`}</p>
<button onClick={increaseCount} disabled={isFull}>
입장
</button>
<buuton onClick={decreaseCount}>퇴장</buuton>
{isFull && <p style={{color: "red"}}>정원이 가득찼습니다.</p>}
</div>
)
}
export default Accommodate;
수정
import React, { Profiler } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Accommodate from './chapter_07/Accommodate';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Accommodate />
</React.StrictMode>
);
reportWebVitals();
처음에 출력된 로그를 보면 우리가 사용한 두 개의 useEffect()훅이 호출된 것을 알 수 있습니다.
입장 버튼을 누르면.
역시 두 개의 useEffect()훅이 호출되는데 카운트 값을 1 증가한 것을 볼 수 있습니다.
의존성 배열이 없는 useEffect() 훅이 호출된 이유는 컴포넌트가 업데이트 됐기 때문이고,
의존성 배열이 있는 useEffect() 훅이 호출된 이유는 count값이 변경되었기 때문입니다.
이 예제를 통해서 useEffect() 훅의 흐름을 확실히 이해하기를 바랍니다.
다음은 정원이 가득 찰 때까지 계속 입장 버튼을 눌러보겠습니다.
정원이 가득 차면 isFull의 값이 true가 되기 때문에 입장 버튼이 비활성화되어
더 이상 누룰 수 없게 되고 빨간 글씨로 경고 문구가 출력됩니다.
로그를 보면 카운트 값이 10이된 이후에는 더 이상 변하지 않기 때문에
count를 의존성 배열로 갖고 있는 useEffect() 훅은 호출되지 않는 것을 볼 수 있습니다.
이제 다시 퇴장 버튼을 눌러 수용 인원을 줄여보겠습니다.
역시 두 개의 useEffect() 훅이 호출되고 카운트 값이 줄어드는 것을 볼 수 있습니다.
마지막으로 수용 인원이 0이 될 때까지 계속 퇴장버튼을 눌러보면
useCounter() 훅에서 Math.max() 함수를 사용하여 카운트 값이 0 아래로
내려갈 수 없게 만들어 놨기 때문에 0이 되면 더 이상 useEffect() 훅도 호출되지 않습니다.