React 공부/기초

Hook의 규칙과 custom Hook 만들기

책다니엘 2024. 1. 17. 23:53
1. Hook은 무조건 최상위 레벨에서만 호출해야 한다!

 

여기서 최상위 레벨이란 React 함수 컴포넌트의 최상위 레벨을 의미한다.

따라서, 반복문이나 조건문, 중첩된 함수들 내에서 hook을 호출하면 안 된다는 것이다.

이러한 규칙에 따라서, hook은 컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 한다. 

 

2. Hook은 리액트 함수 컴포넌트에서만 호출되어야 한다. 

 

Hook 기능은 리액트라는 javascript 라이브러리 내에서만 지원하는 고유한 기능이기에, 다른 javascript 환경에서 hook을 호출하면 인식하지 않는다. Hook은 React 함수 컴포넌트나 직접 만든 커스텀 hook에서만 호출할 수 있다.

 

여기서 소플의 처음 만난 리액트 강의에서는 개발 도중 hook을 효과적으로 사용할 수 있게 해 주는 패키지를 하나 소개해주었다. eslint-plugin-react-hooks 패키지인데, hook의 규칙을 따르도록 강제해준다. eslint는 javaScript 코드에서 발견되는 문제 패턴을 식별하기 위한 정적 코드 분석 도구로, 이 플러그인을 사용하면 React 컴포넌트가 hook의 규칙을 따르는지 아닌지 확인할 수 있다. 이 플러그인은 의존성 배열이 잘못되어 있는 경우에 자동으로 경고 표시를 해 주고, 고칠 방법을 제안해주기도 한다. 

 

https://www.npmjs.com/package/eslint-plugin-react-hooks

 

 

Custom hook이란?

 

React에서 기본적으로 제공해주는 기능들 외에, 자주 쓰는 기능이 있다면 직접 custom hook을 만들어 사용할 수 있다. 개발 과정 중 반복되는 로직을 개발자가 직접 hook으로 만들어 재사용하기 위함이다.(node의 모듈과 비슷한 개념!)

 

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이라는 state에 따라 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트이다. 동일한 웹사이트에서 연락처 목록을 제공하는데, 이 때 온라인인 사용자의 이름은 초록색으로 표시해 주고 싶다고 해 보자. 이 컴포넌트의 이름을 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>
  );
}

이 코드는 위 코드처럼 동일하게 UserStatus와 useState, useEffect를 사용하고 있다. 기존의 리액트에서는 이렇게 스테이트와 관련된 로직이 중복되는 경우에 renderProps 또는 HOC(Higher Order Component)를 사용했다고 한다. 최근에는 커스텀 훅을 이용해 중복되는 코드를 추출하여 재사용하도록 할 수 있다.

 

일반적으로 두 개의 자바스크립트 함수에서 하나의 로직을 공유하기 위해서는 새로 함수를 하나 만드는 법을 사용한다. React 함수 컴포넌트와 hook은 모두 함수이기 때문에, 동일한 방법을 사용할 수 있는 것이다. 

 

커스텀훅을 만드는 공식을 간단히 정리하자면 다음과 같다:

Custom Hook: 이름이 use로 시작하고, 내부에서 다른 Hook을 호출하는 하나의 javaScript 함수!

 

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;
}

 

위 코드는 앞서 사용한 UserStatus, UserListItem의 중복되는 부분을 추출해 useUserStatus란 function으로 추출해 낸 것이다. 이것을 사용하기 위해서는 다른 컴포넌트 내부에서와 마찬가지로 다른 훅으로 추출하기 위해서 무조건 커스텀 훅의 최상위 레벨에서만 해야 한다.

 

React컴포넌트와 달리, 커스텀 훅은 특별한 규칙이 없다. 파라미터로 무엇을 받아야 할지, 어떤 것을 리턴해야 할지 개발자가 직접 정할 수 있다. 커스텀 훅은 단순 함수와 같다. 다만 use를 꼭 함수 이름 앞에 붙여서 이 함수가 리액트의 hook이라는 것을 알 수 있도록 해야 한다. 

 

또한 훅이기 때문에 훅의 두 가지 규칙이 적용된다. 위 함수의 목적은 사용자의 온라인, 오프라인 상태를 구독하는 것이다. 그렇기 때문에 이 코드처럼 useUserStatus hook의 파라미터로 userId를 받도록 만들었고, 해당 사용자의 상태가 isOnline인지 아닌지를 리턴하게 되어 있다. 

 

추출한 커스텀 훅을 토대로 앞서 사용했던 UserStatus함수를 단순화해보면 다음과 같다.

 

function UserStatus(props) {
  const isOnline = useUserStatus(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>
  );
}

위 코드는 커스텀 훅을 사용하여, 앞서 작성한 코드와 작동 방식에서 변동이 없고 동일하게 작동하는 코드이다. 커스텀 hook은 리액트 기능이 아닌 Hook의 디자인에서 자연스럽게 따르는 규칙이다. 

 

커스텀 훅의 사용 규칙을 정리하면 다음과 같다.

 

1. Custom Hook의 이름은 꼭 use로 시작해야 한다.
2. 여러 개의 컴포넌트에서 하나의 Custom hook을 사용할 때, 컴포넌트 내부에 있는 모든 state와 effect는 전부 분리되어 있다. 
3. 각 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(Numeber(event.target.value))}
      >
        {userList.map((user) => (
          <option key={user.id} value={user.id}>
            {user.name}
          </option>
        ))}
      </select>
    </>
  );
}

 

이 코드에서는 chatUserSelector라는 컴포넌트가 나온다. <select>태그를 통해 목록에서 사용자를 선택할 수 있도록 하고 있으며, 사용자를 선택할 경우 해당 사용자가 온라인인지 아닌지를 보여주게 된다. 이 코드에서는 useState hook을 이용해 userId라는 state를 만들었다. 현재 선택된 사용자의 아이디를 저장하기 위한 용도이다. 이 유저아이디는 바로 다음에 나오는 useUserStatus Hook의 파라미터로 들어간다. 이렇게 하면 setUserId 함수를 통해 userId가 변경될 때마다 useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고, 새로 선택된 사용자의 온라인 여부를 구독하게 된다. 이러한 방식으로 hook들 사이에서 데이터를 공유할 수 있다.

 

출처: 소플의 처음 만난 리액트

'React 공부 > 기초' 카테고리의 다른 글

React, Next.js 주요 개념  (6) 2024.10.03
useMemo(), useCallback(), useRef()에 대하여  (1) 2024.01.16
Hooks의 개념과 useState(), useEffect()  (2) 2024.01.14