React 공부 (11) 훅

seon·2024년 2월 16일

Web

목록 보기
18/33
post-thumbnail

# Hook의 규칙

훅은 단순한 자바스크립트 함수이지만 두 가지 지켜야 할 규칙이 있습니다. 지금부터 훅의 규칙에 대해서 배워보도록 하겠습니다.
첫번째 규칙은

1. Hook 은 무조건 최상위 레벨에서만 호출해야 한다.

는 것입니다. 여기에서 말하는 최상위 레벨은 리액트 함수 컴포넌트에서의 최상위 레벨을 의미합니다. 따라서 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안된다는 뜻입니다.
이 규칙에 따라서

Hook은 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출되어야 합니다.

이렇게 해야 리액트가 다수의 useState() 훅과 useEffect() 훅을 호출해서 컴포넌트의 state를 올바르게 관리할 수 있게 됩니다.

이 코드를 한번 보겠습니다.

function MyComponent(props) {
  const [name, setName] = useState('Inje');
  
  if (name !== '') {
    useEffect(() => {
      ...
    });
    }

    ...
}
  • 이 코드에서는 if문에 들어간 조건문의 값이 참인 경우에만 useEffect() 훅을 호출하도록 되어 있습니다.
  • 이런 경우 중간에 name의 값이 빈 문자열이 되면 조건문의 값이 false 가 되어 useEffect() 훅이 호출되지 않습니다.
  • 결과적으로 렌더링할 때마다 훅이 같은 순서대로 호출되는 것이 아니라 조건문의 결과에 따라 호출되는 훅이 달라지므로 잘못된 코드입니다.

훅은 꼭 최상위 레벨에서만 노출해야 한다는 점을 기억하세요.

훅의 두 번째 규칙은

2. 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.

는 것입니다.
그렇기 때문에 일반적인 자바스크립트 함수에서 훅을 호출하면 안됩니다. 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있습니다.

이 규칙에 따라 리액트 컴포넌트에 있는 state와 관련된 모든 로직은 소스 코드를 통해 명확하게 확인이 가능해야 합니다.


# eslint-plugin-react-hooks

여기서 잠깐 훅의 규칙과 관련해서 개발에 도움이 되는 패키지를 하나 소개하도록 하겠습니다.
eslint의 플러그인인데, 이 플러그인은 훅의 규칙을 따르도록 강제해주는 플러그인입니다.
eslint는 자바스크립트 코드에서 발견되는 문제 패턴을 식별하기 위한 정적 코드 분석 도구입니다.
그리고 이 플러그인을 사용하면 리액트 컴포넌트가 훅의 규칙을 따르는지 아닌지 분석할 수 있습니다.
이 플러그인은 의존성 배열이 잘못되어 있는 경우에 자동으로 경고 표시를 해주며 고칠 방법을 제안해 주기도 합니다.
예를 들면

const memoizedValue = useMemo(
  () => {
    //연산량이 높은 작업을 수행하여 결과를 반환
    return computeExpensiveValue(의존성 변수1, 의존성 변수2);
  }
  [의존성 변수1, 의존성 변수2]
);
  • useMemo()에서, 의존성 배열에 넣은 변수들은 create 함수의 파라미터로 전달되지 않습니다.
  • 하지만 useMemo() 훅의 원래 의미가 의존성 배열에 있는 변수 중 하나라도 변하면 create 함수를 다시 호출하는 것이기 때문에 create함수에서 참조하는 모든 변수를 의존성 배열에 넣어주는 것이 맞습니다.

나중에는 컴파일러가 개선되어 이러한 의존성 배열을 자동으로 생성할 수 있게 될 것입니다. 하지만 지금은 직접 의존성 배열을 잘 만들어 주는 것이 중요합니다. 하지만 지금은 직접 의존성 배열을 잘 만들어 주는 것이 중요합니다. 이를 위해 eslint-plugin-react-hooks 패키지를 사용하면 도움이 됩니다.
이 패키지를 살펴보고 싶다면 이 주소를 참고하시기 바랍니다.
( https://www.npmjs.com/package/eslint-plugin-react-hooks )


# 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)l
    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() 훅을 사용하는 부분이 동일한 것을 볼 수 있습니다. 여러 곳에서 중복되는 코드인 것이죠.

기존의 리액트에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우에 render props 또는 hoc라고 불리는 higher order 컴포넌트를 사용했습니다.
하지만 여기에서 중복되는 코드를 추출하여 커스텀 훅으로 만드는 새로운 방법을 사용해 보도록 하겠습니다.


# Custom Hook 추출하기

이제 중복되는 로직을 커스텀 훅으로 추출해 보겠습니다.
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;
}
  • 이 코드는 중복되는 로직을 useUserStatus라는 커스텀 훅으로 추출해낸 것입니다.
  • 이 코드를 보면 특별할 것이 없고 그냥 2개의 컴포넌트에서 중복되는 로직을 추출해서 가져온 것입니다.
  • 다만 다른 컴포넌트 내부에서와 마찬가지로 다른 훅을 추출하는 것은 무조건 커스텀 훅의 최상위 레벨에서만 해야 합니다. 리액트 컴포넌트와 달리 커스텀 훅은 특별한 규칙이 없습니다. 예를 들면 파라미터로 무엇을 받을지 어떤 것을 리턴해야 할지를 개발자가 직접 정할 수 있습니다. 다시 말하면 커스텀 훅은 단순한 함수와도 같습니다. 하지만 이름은 use로 시작하도록 해서 이것이 단순한 함수가 아닌 리액트 훅이라는 것을 나타내주는 것이죠. 또한 훅이기 때문에 위에서 배운대로 훅의 두 가지 규칙이 적용됩니다.
  • useUserStatus의 목적은 사용자의 온라인 오프라인 상태를 구독하는 것입니다. 그렇기 때문에 위 코드처럼 useUserStatus의 파라미터로 userId를 받도록 만들었고, 해당 사용자가 온라인인지 오프라인인지의 상태를 리턴하게 했습니다.

# Custom Hook 사용하기

이제 방금 만든 커스텀 훅을 사용하는 방법에 대해서 알아보겠습니다. 처음 커스텀 훅을 만들기로 했을 때의 목표는 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들 사이에서 데이터를 공유하는 방법

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>
	</>
  );
}
  • 이 코드에서는 ChatUserSelector 라는 컴포넌트가 나옵니다.
  • 이 컴포넌트는 select 태그를 통해 목록에서 사용자를 선택할 수 있게 해주고 있으며 사용자를 선택할 경우 해당 사용자가 온라인인지 아닌지를 보여주게 됩니다.
  • 코드에서 //****//에 해당하는 부분을 눈여겨 보자. 이 코드를 자세히 보면 useState 훅을 사용해서 userId 라는 state를 만들었습니다. 현재 선택된 사용자의 id를 저장하기 위한 용도죠.
    그리고 이 userId는 바로 다음에 나오는 useUserStatus 훅의 파라미터로 들어가게 됩니다.
    이렇게 하면 setUserId 함수를 통해 userId가 변경될 때마다 useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독하게 됩니다.
    Hook들 사이에서는 이러한 방법으로 데이터를 공유할 수 있습니다. 이것은 참고로 알아가시기 바랍니다.

📍정리

Hook의 규칙

  1. Hook은 무조건 최상위 레벨에서만 호출해야 한다.
  2. 리액트 함수 컴포넌트에서만 Hook을 호출해야 한다.
    eslint-plugin-react-hooks : 리액트 컴포넌트가 훅의 규칙을 따르는지 아닌지 분석

Custom Hook 추출

  • 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함
  • 이름이 use로 시작(해야 함수 내에서 훅을 호출하는지 알 수 있다)하고 내부에서 다른 Hook을 호출하는 하나의 자바스크립트 함수
  • 본문의 useUserStatus 훅이 바로 UserStatus와 UserListItem으로부터 중복된 로직을 추출한 커스텀 훅이다.
  • 여러 개의 컴포넌트에서 공통된 커스텀 훅을 사용하여도 그 state와 effect는 공유되지 않는다. 각각의 커스텀 훅의 호출에 의해 분리된 state를 얻게 되는 것.



profile
🌻

0개의 댓글