Hooks의 규칙과 Custom Hook 만들기 [처음 만난 리액트 #23]
7강. Hooks
Hook의 규칙
1. Hook은 무조건 최상위 레벨에서만 호출해야 한다.
최상위 레벨 : 리액트 컴포넌트의 최상위 레벨을 의미
반복문이나 조건문, 중첩된 함수들 안에서 Hook을 호출하면 안됨
Hook은 컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 한다.
-> 리액트가 다수의 useState 훅과 useEffect 훅에 호출해서 컴포넌트의 State를 올바르게 관리할 수 있게 됨
잘못된 Hook 사용법 예제코드
function MyComponent(props) {
const [name, setName] = useState('Inje');
if (name !== '') {
useEffect(() => {
...
});
}
...
}
if문에 들어간 조건문의 값이 참인 경우에만 useEffect 훅을 호출하도록 되어있음
이런 경우 중간에 name의 값이 긴 문자열이 되면 조건문의 값이 false가 되어 useEffect 훅이 호출되지 않음
-> 렌더링할 때 마다 훅이 같은 순서대로 호출되는 것이 아니라 조건문의 결과에 따라 호출되는 훅이 달라지므로 잘못된 코드! (Hook은 꼭 최상위 레벨에서만 호출해야 함)
2. 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.
일반적인 자바스크립트 함수에서 Hook을 호출하면 안됨
Hook은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 Custom Hook에서만 호출할 수 있음
이 규칙에 따라 리액트 컴포넌트에서 state와 관련된 모든 로직은 소스코드를 통해 명확하게 확인이 가능해야 함
eslint-plugin-react-hooks
(Hook의 규칙과 관련해서 개발에 도움이 되는 패키지)
: Hook의 규칙을 따르도록 강제해주는 플러그인
- 이 플러그인을 사용하면 리액트 컴포넌트가 Hook의 규칙을 따르는지 아닌지 분석할 수 있음
- 의존성 배열이 잘못되어있는 경우에 자동으로 경고 표시를 해주며 고칠 방법을 제안해주기도 함
eslint : 자바 스크립트 코드에서 발견되는 문제 패턴을 식별하기 위한 정적 코드분석 도구
const memoizedValue = useMemo(
() => {
// 연산량이 높은 작업을 수행하여 결과를 반환
return computeExpensiveValue(의존성 변수1, 의존성 변수2);
},
[의존성 변수1, 의존성 변수2]
);
useMemo에서 의존성 배열에 넣은 변수들은 create 함수의 파라미터로 전달되지 않음
useMemo Hook의 원래 의미가 의존성 배열의 변수 중 하나라도 변하면 create 함수를 다시 호출하는 것이기 때문에 create 함수에서 참조하는 모든 변수를 의존성 배열에 넣어주어야 함
-> eslint-plugin-react-hooks 패키지를 사용!
▼ 참고 링크
eslint-plugin-react-hooks
ESLint rules for React Hooks. Latest version: 4.6.0, last published: 8 months ago. Start using eslint-plugin-react-hooks in your project by running `npm i eslint-plugin-react-hooks`. There are 7151 other projects in the npm registry using eslint-plugin-rea
www.npmjs.com
Custom Hook 만들기
● 리액트에서 기본적으로 제공되는 Hook들 이외에, 추가적으로 필요한 기능이 있다면 직접 Hook을 만들어 사용할 수 있음
● 여러 컴포넌트에서 반복적으로 사용되는 로직을 Hook으로 만들어 재사용
Custom Hook을 만들어야 하는 상황
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);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
if(isOnline === null) {
return '대기중...';
}
return isOnline ? '온라인' : '오프라인';
}
UserStatus는 isOnline이라는 status에 따라서 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트
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);
return () => {
ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.user.name}
</li>
);
}
동일한 웹사이트에서 연락처 목록을 제공하는데, 이때 온라인인 사용자의 이름은 초록색으로 보여주고 싶을 떄,
컴포넌트의 이름을 userListItem이라고 하고 비슷한 로직을 넣음
useState, useEffect 훅을 사용하는 부분은 UserStatus와 동일 (중복되는 코드)
● 기존에 리액트에서는 state와 관련된 로직이 중복되는 경우에 render props 또는 HOC라고 불리는 higher order component를 사용함
● 여기에서는 중복되는 코드를 추출하여 Custom Hook을 만드는 새로운 방법 사용
Custon Hook 추출하기
두 개의 자바스크립트 함수에서 하나의 로직을 공유하도록 하고싶을 때에는 새로운 함수를 하나 만드는 방법을 사용
리액트 함수 컴포넌트와 훅은 모두 함수이기 때문에 동일한 방법을 사용할 수 있음
Custon Hook : 이름이 use로 시작하고 내부에서 다른 Hook을 호출하는 하나의 자바스크립트 함수
▼ 중복되는 로직을 useUserStatus라는 커스텀 훅으로 추출
import { useState, useEffect } from "react";
function useUserStatus(userId) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ServerAPI.subscribeUserStatus(userId, handleStatusChange);
return () => {
ServerAPI.unsubscribeUserStatus(userId, handleStatusChange);
};
});
return isOnline;
}
두 개의 컴포넌트에서 중복되는 로직을 추출해서 가져옴
● 다른 컴포넌트 내부에서와 마찬가지로 다른 훅을 호출하는 것은 무조건 커스텀 훅의 최상위 레벨에서만 해야 함
● 리액트 컴포넌트와 달리, 커스텀 훅은 특별한 규칙이 없음
ex) 파라미터로 무엇을 받을지, 어떤 것을 리턴할지 개발자가 직접 정할 수 있음
-> 커스텀 훅은 단순한 함수와도 같음, 이름은 use로 시작하도록 해서 단순한 함수가 아닌 리액트 훅이라는 것을 나타내는 것
● 훅의 두 가지 규칙이 적용
useUserStatus 훅의 목적은 사용자의 온라인, 오프라인 상태를 구독하는 것
-> useUserStatus 훅의 파라미터로 userId를 받도록 만들었고, 해당 사용자가 온라인인지 오프라인인지의 상태를 리턴하게 함
Custom Hook 사용하기
처음 커스텀 훅을 만들기로 했을 때의 목표는 userStatus와 userListItem 컴포넌트로부터 중복된 로직을 제거하는 것
두 컴포넌트 모두 사용자가 온라인 상태인지 알기를 원했음
-> 중복되는 로직을 useUserStatus 훅으로 추출했기 때문에 이 훅을 사용하여 다음과 같이 코드를 변경할 수 있음
function UserStatus(props) {
const [isOnline, setIsOnline] = useUserState(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>
);
}
Custom Hook의 이름은 꼭 use로 시작해야 한다!
만약 이름이 use로 시작하지 않는다면, 특정 함수의 내부에서 Hook을 호출하는지를 알 수 없기 때문에 Hook의 규칙위반 여부를 자동으로 확인할 수 없음
같은 Custom Hook을 사용하는 두 개의 컴포넌트는 state를 공유하는 것일까?
● 아님, Custom Hook은 단순히 state와 연관된 로직을 재사용이 가능하도록 만든 것
● 여러 개의 컴포넌트에서 하나의 Custom Hook을 사용할 때 컴포넌트 내부에 있는 모든 state와 effects는 전부 분리되어 있다.
Custom Hook은 어떻게 state를 분리하는 것일까?
리액트 컴포넌트는 각 Custom Hook 호출에 대해서 분리된 state를 얻게 되기 때문
앞 예제 코드에서 useUserStatus 훅을 직접 호출하는 것처럼 리액트의 관점에서는 컴포넌트에서 userStatus와 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={useId}
onChange={event => setUserId(Number(event.target.value))}
>
{userList.map(user => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</>
);
}
ChatUserSelector라는 컴포넌트 : select 태그를 통해 목록에서 사용자를 선택할 수 있게 해주고 있으며, 사용자를 선택할 경우 해당 사용자가 온라인인지 아닌지를 보여줌
const [userId, setUserId] = useState(1);
const isUserOnline = useUserStatus(userId);
▲ useState Hook을 사용해서 userId라는 state를 만듦
: 현재 선택된 사용자의 Id를 저장하기 위한 용도
userId는 useUserStatus Hook의 파라미터로 들어감
-> setUserId 함수를 통해 userId가 변경될 때 마다 useUserStatus Hook은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독함