처음 만난 리액트 문서


7.8 나만의 Hooks 만들기

리액트에서 기본적으로 제공되는 훅들 이외에 추가적으로 필요한 기능이 있다면 직접 훅을 만들어서 사용할 수 있습니다. 이를 커스텀 훅(Custom Hook) 이라고 부르는데, 커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어서 재사용하기 위함입니다. 어떤식으로 커스텀 훅을 만드는지 예제 코드를 통해서 배워보도록 하겠습니다.

7.8.1 커스텀 훅을 만들어야 하는 상황

먼저 아래 예제 코드를 보도록 하겠습니다. 아래 코드에 나와 있는 UserStatus라는 컴포넌트는 isOnline이라는 state에 따라서 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트입니다.

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 ? '온라인' : '오프라인';
}

그리고 동일한 웹사이트에서 연락처 목록이 제공되는데, 이때 온라인인 사용자의 이름은 초록색으로 표시해주고 싶다고 해보겠습니다. 이 컴포넌트의 이름을 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>
    );
}

코드를 살펴보면 위에 나왔던 UserStatususeState(), useEffect()훅을 사용하는 부분이 동일한 것을 볼 수 있습니다. 여러 곳에서 중복되는 코드인 것이죠. 기존의 리액트에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우에 render props 또는 HOC(higher order components)를 사용합니다. 하지만 여기서는 중복되는 코드를 추출해서 커스텀 훅으로 만드는 새로운 방법을 사용해보도록 하겠습니다.

7.8.2 커스텀 훅 추출하기

이제 중복되는 로직을 커스텀 훅으로 추출해보도록 하겠습니다. 두 개의 자바스크립트 함수에서 하나의 로직을 공유하도록 하고 싶을 때, 새로운 함수를 하나 만드는 방법을 사용합니다. 리액트 컴포넌트와 훅은 모두 함수이기 때문에 동일한 방법을 사용할 수 있는 것입니다.

커스텀 훅이라고 해서 뭔가 특별한 것이 아니라, 이름이 use로 시작하고 내부에서 다른 훅들을 호출하는 하나의 자바스크립트 함수입니다. 아래 코드는 중복되는 로직을 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;
}

위 코드를 보면 특별할 것이 없고, 그냥 두 개의 컴포넌트에서 중복되는 로직을 추출해서 가져온 것입니다. 다만, 다른 컴포넌트 내부에서와 마찬가지로 다른 훅들을 호출하는 것은 무조건 커스텀 훅의 최상위 레벨에서만 해야 합니다.

리액트 컴포넌트와 달리, 커스텀 훅은 특별한 규칙이 없습니다. 예를 들면, 파라미터로 무엇을 받을지, 어떤 것을 리턴해야 할지를 개발자가 직접 정할 수 있습니다. 다시 말하면, 커스텀 훅은 그냥 단순한 함수와도 같습니다. 하지만 이름은 use로 시작하도록 하여 이것이 단순한 함수가 아니라 리액트 훅이라는 것을 나타내주는 것이죠. 그리고 훅이기 때문에 위에서 배운대로 훅의 두 가지 규칙이 적용 됩니다.

useUserStatus()훅의 목적은 사용자의 온라인/오프라인 상태를 구독하는 것입니다. 그렇기 때문에 아래 코드처럼 useUserStatus()훅의 파라미터로 userId를 받도록 만들었고, 해당 사용자가 온라인인지 오프라인인지 그 상태를 리턴하게 했습니다.

function useUserStatus(userId) {
    const [isOnline, setIsOnline] = useState(null);

    // ...

    return isOnline;
}

이제 다음 절에서 방금 만든 커스텀 훅을 사용하는 방법에 대해서 알아보겠습니다.

7.8.3 커스텀 훅 사용하기

처음 커스텀 훅을 만들기로 했을 때의 목표는 UserStatusUserListItem컴포넌트로부터 중복된 로직을 제거하는 것이었습니다. 그리고 두 개의 컴포넌트는 모두 사용자의 온라인 여부를 알기를 원했습니다.

이제 중복되는 로직을 useUserStatus()훅으로 추출했기 때문에, 그걸 사용하여 아래와 같이 코드를 변경할 수 있습니다.

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

이 코드는 커스텀 훅을 적용하기 전과 동일하게 작동합니다. 동작에 변경이 없고 중복되는 로직만을 추출하여 커스텀 훅으로 만든 것이기 때문이죠. 커스텀 훅은 리액트 기능이 아닌 훅의 디자인에서 자연스럽게 따르는 규칙입니다.

그렇다면 커스텀 훅의 이름은 꼭 use로 시작하게 해야 할까요? 네, 그렇습니다. 이것은 중요한 규칙이기 때문에 꼭 지켜야 합니다. 만약 이름이 use로 시작하지 않는다면, 특정 함수에 내부에서 훅을 호출하는지 알 수 없기 때문에 훅의 규칙을 위반했는지를 자동으로 확인할 수 없습니다.

또한 같은 커스텀 훅을 사용하는 두 개의 컴포넌트는 state를 공유하는 것일까요? 아닙니다. 커스텀 훅은 단순히 state와 연관된 로직을 재사용 가능하게 만든 것입니다. 그래서 여러 개의 컴포넌트에서 하나의 커스텀 훅을 사용할 때는, 컴포넌트 내부에 있는 모든 stateeffects는 전부 분리되어 있습니다.

그렇다면 커스텀 훅은 어떻게 state를 분리하는 것일까요? 각각의 커스텀 훅 호출에 대해서 분리된 state를 얻게 됩니다. 위 예제 코드에서 useUserStatus()훅을 직접 호출하는 것처럼, 리액트의 관점에서는 컴포넌트에서 useState()useEffect()훅을 호출하는 것과 동일한 것입니다. 또한 하나의 컴포넌트에서 useState()useEffect()훅을 여러번 호출 할 수 있는 것처럼, 각 커스텀 훅의 호출 또한 완전히 독립적이라고 볼 수 있습니다.

NOTE. 훅들 사이에서 데이터를 공유하는 방법
훅을 호출하는 것은 각 호출에 대해 완전히 독립적이라고 배웠습니다. 그렇다면 훅들 사이에서 데이터를 공유하고 싶다면 어떻게 해야 할까요? 아래 예제 코드를 한 번 보도록 하겠습니다.

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>태그를 통해서 목록에서 사용자를 선택할 수 있게 해주고 있으며, 사용자를 선택할 경우 해당 사용자가 온라인인지 아닌지 여부를 보여주게 됩니다. 여기서 눈여겨 봐야할 부분은 바로 아래 부분입니다.

const [userId, setUserId] = useState(1);
const isUserOnline = useUserStatus(userId);

이 코드를 자세히 보면 useState()훅을 사용해서 userId라는 state를 만들었습니다. 현재 선택된 사용자의 아이디를 저장하기 위한 용도죠. 그리고 이 userId는 바로 다음에 나오는 useUserStatus훅의 파라미터로 들어가게 됩니다. 이렇게 하면 setUserId함수를 통해 userId가 변경될 때마다, useUserStatus훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독하게 됩니다. 훅들 사이에서는 이러한 방법으로 데이터를 공유할 수 있습니다.


마지막 업데이트: 2025년 08월 21일 09시 05분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.

On this page