훅은 단순한 자바스크립트 함수이지만 두 가지 지켜야 할 규칙이 있습니다. 지금부터 훅의 규칙에 대해서 배워보도록 하겠습니다.
첫번째 규칙은
1. Hook 은 무조건 최상위 레벨에서만 호출해야 한다.
는 것입니다. 여기에서 말하는 최상위 레벨은 리액트 함수 컴포넌트에서의 최상위 레벨을 의미합니다. 따라서 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안된다는 뜻입니다.
이 규칙에 따라서
Hook은 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출되어야 합니다.
이렇게 해야 리액트가 다수의 useState() 훅과 useEffect() 훅을 호출해서 컴포넌트의 state를 올바르게 관리할 수 있게 됩니다.
이 코드를 한번 보겠습니다.
function MyComponent(props) {
const [name, setName] = useState('Inje');
if (name !== '') {
useEffect(() => {
...
});
}
...
}
훅은 꼭 최상위 레벨에서만 노출해야 한다는 점을 기억하세요.
훅의 두 번째 규칙은
2. 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.
는 것입니다.
그렇기 때문에 일반적인 자바스크립트 함수에서 훅을 호출하면 안됩니다. 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있습니다.
이 규칙에 따라 리액트 컴포넌트에 있는 state와 관련된 모든 로직은 소스 코드를 통해 명확하게 확인이 가능해야 합니다.
여기서 잠깐 훅의 규칙과 관련해서 개발에 도움이 되는 패키지를 하나 소개하도록 하겠습니다.
eslint의 플러그인인데, 이 플러그인은 훅의 규칙을 따르도록 강제해주는 플러그인입니다.
eslint는 자바스크립트 코드에서 발견되는 문제 패턴을 식별하기 위한 정적 코드 분석 도구입니다.
그리고 이 플러그인을 사용하면 리액트 컴포넌트가 훅의 규칙을 따르는지 아닌지 분석할 수 있습니다.
이 플러그인은 의존성 배열이 잘못되어 있는 경우에 자동으로 경고 표시를 해주며 고칠 방법을 제안해 주기도 합니다.
예를 들면
const memoizedValue = useMemo(
() => {
//연산량이 높은 작업을 수행하여 결과를 반환
return computeExpensiveValue(의존성 변수1, 의존성 변수2);
}
[의존성 변수1, 의존성 변수2]
);
나중에는 컴파일러가 개선되어 이러한 의존성 배열을 자동으로 생성할 수 있게 될 것입니다. 하지만 지금은 직접 의존성 배열을 잘 만들어 주는 것이 중요합니다. 하지만 지금은 직접 의존성 배열을 잘 만들어 주는 것이 중요합니다. 이를 위해 eslint-plugin-react-hooks 패키지를 사용하면 도움이 됩니다.
이 패키지를 살펴보고 싶다면 이 주소를 참고하시기 바랍니다.
( https://www.npmjs.com/package/eslint-plugin-react-hooks )
리액트에서 기본적으로 제공되는 훅들 이외에 추가적으로 필요한 기능이 있다면 직접 훅을 만들어서 사용할 수 있습니다. 이것을 커스텀 훅이라고 부르는데 커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함입니다.
지금부터는 어떤 식으로 커스텀 훅을 만드는지 예제 코드를 통해서 함께 배워보도록 하겠습니다. 먼저 어떤 경우 커스텀 훅을 만들어야 하는지 그 상황을 예제 코드를 통해 살펴보도록 하겠습니다.
먼저 예제 코드를 보겠습니다.
import React, { useState, useEffect } from "react";
function UserStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect((() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange)l
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
if (isOnline == null) {
return '대기 중...';
}
return isOnline ? '온라인' : '오프라인';
}
그리고 동일한 웹사이트에서 연락처 목록을 제공하는데 이때 온라인 사용자 이름은 초록색으로 표시해주고 싶다고 해봅시다. 이 컴포넌트 이름을 UserListItem이라고 하고 여기에 비슷한 로직을 넣어야 합니다. 다음 코드와 같이 말이죠.
import React, { useState, useEffect } from "react";
function UserListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect((() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.user.name}
</li>
};
}
기존의 리액트에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우에 render props 또는 hoc라고 불리는 higher order 컴포넌트를 사용했습니다.
하지만 여기에서 중복되는 코드를 추출하여 커스텀 훅으로 만드는 새로운 방법을 사용해 보도록 하겠습니다.
이제 중복되는 로직을 커스텀 훅으로 추출해 보겠습니다.
2개의 자바스크립트 함수에서 하나의 로직을 공유하도록 하고 싶을 때에는 새로운 함수를 하나 만드는 방법을 사용합니다. 리액트 함수 컴포넌트와 훅은 모두 함수이기 때문에 동일한 방법을 사용할 수 있는 것입니다.
custom 훅은 무언가 특별한 것이 아니라,
이름이 use로 시작하고 내부에서 다른 Hook을 호출하는 하나의 자바스크립트 함수
입니다.
import { useState, useEffect } from "react";
function useUserStatus(userId) {
const [isOnline setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscibeUserStatus(userId, handleStatusChange);
return () => {
ServerAPI.unsubscibeUserStatus(userId, handleStatusChange);
};
});
return isOnline;
}
이제 방금 만든 커스텀 훅을 사용하는 방법에 대해서 알아보겠습니다. 처음 커스텀 훅을 만들기로 했을 때의 목표는 UserStatus와 UserListItem으로부터 중복된 로직을 제거하는 것이었습니다. 그리고 2개의 컴포넌트는 모두 사용자가 온라인 상태인지를 알기 원했습니다.
이제 중복되는 로직을 useUserStatus 훅으로 추출했기 때문에 이 훅을 사용하여 다음과 같이 코드를 변경할 수 있습니다.
function UserStatus(props) {
const isOnline = useUserStaus(props.user.id);
if(isOnline === null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
function UserListItem(props) {
const isOnline = useUserStatus(props.user.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.user.name}
</li>
);
}
커스텀 훅은 리액트 기능이 아닌 훅의 디자인에서 자연스럽게 따르는 규칙입니다.
그렇다면 커스텀 훅의 이름은 꼭 use로 시작해야 할까요?
Custom Hook의 이름은 꼭 use로 시작해야 한다!
네 그렇습니다. 이것은 중요한 규칙이기 때문에 꼭 지켜야 합니다.
만약 이름이 use로 시작하지 않는다면 특정 함수의 내부에서 훅을 호출하는지를 알 수 없기 때문에 훅의 규칙 위반 여부를 자동으로 확인할 수 없습니다.
또한 같은 커스텀 훅을 사용하는 두 개의 컴포넌트는 state를 공유하는 것일까요? 아닙니다. 커스텀 훅은 단순히 state와 연관된 로직을 재사용이 가능하게 만든 것입니다. 따라서
여러 개의 컴포넌트에서 하나의 Custom Hook을 사용할 때 컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있습니다.
그렇다면 커스텀 훅은 어떻게 state를 분리하는 것일까요? 그것은 특별한 방법이 있는 것이 아니라, 리액트 컴포넌트는
각각의 Custom Hook 호출에 대해서 분리된 state를 얻게 됨!
되기 때문입니다. 앞에 예제 코드에서 useUserStatus 훅을 직접 호출하는 것처럼, 리액트 관점에서는 컴포넌트에서 useState와 useEffect 훅을 호출하는 것과 동일한 것입니다.
또한 하나의 컴포넌트에서 useState와 useEffect 훅을 여러 번 호출 할 수 있는 것처럼
각 Custom Hook의 호출 또한 완전히 독립적이다.
라고 볼 수 있습니다.
Hook을 호출하는 것은 각 호출에 대해 완전히 독립적이라고 했습니다. 그렇다면 훅들 사이에서 데이터를 공유하고 싶다면 어떻게 해야 할까요?
예제 코드를 보겠습니다.
const userList = [
{ id: 1, name: 'Inje' },
{ id: 2, name: 'Mike' },
{ id: 3, name: 'Steve' },
];
function ChatUserSelector(props) {
const [userId, setUserId] = useState(1); //****//
const isUserOnline = useUserStatus(userId); //****//
return (
<>
<Circle color={isUserOnline ? 'green' : 'red'} />
<select
value={userId}
onChange={event => setUserId(Number(event.target.value))}
>
{userList.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</>
);
}
//****//에 해당하는 부분을 눈여겨 보자. 이 코드를 자세히 보면 useState 훅을 사용해서 userId 라는 state를 만들었습니다. 현재 선택된 사용자의 id를 저장하기 위한 용도죠.Hook의 규칙
- Hook은 무조건 최상위 레벨에서만 호출해야 한다.
- 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.
eslint-plugin-react-hooks: 리액트 컴포넌트가 훅의 규칙을 따르는지 아닌지 분석Custom Hook 추출
- 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함
- 이름이 use로 시작(해야 함수 내에서 훅을 호출하는지 알 수 있다)하고 내부에서 다른 Hook을 호출하는 하나의 자바스크립트 함수
- 본문의 useUserStatus 훅이 바로 UserStatus와 UserListItem으로부터 중복된 로직을 추출한 커스텀 훅이다.
- 여러 개의 컴포넌트에서 공통된 커스텀 훅을 사용하여도 그 state와 effect는 공유되지 않는다. 각각의 커스텀 훅의 호출에 의해 분리된 state를 얻게 되는 것.