IT/언어

[React] 커스텀 훅

개발자 두더지 2023. 9. 10. 18:55
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

커스텀 훅이란?


 커스텀 훅은 여러 개의 컴포넌트 내에서 존재하는 공통 처리를 빼내어 만든 함수이다. 범용적인 커스텀 훅을 만들면 1개의 어플리케이션 내에서 재이용할 수 있을 뿐만 아니라 React, Next.js, Remix를 제약없이 다른 어플리케이션에서도 재이용할 수 있다는 장점이 있다.

 커스텀 훅에는 범용적인 커스텀 훅을 모아둔 Rooks이라는 사이트가 있는데, 자체 제작하지 않고 다른 사람이 만들어 놓은 커스텀 훅을 이용할 수 있다. 

 또한 View(컴포넌트)와 로직을 분리하게 되므로 하나의 파일에 작성된 코드 양이 많아지지 않도록 조절할 수 있다는 것도 이점이다.

 

 

커스텀 훅의 공식 룰


1. 커스텀 훅의 파일 이름은 "use"로 시작하도록 작성하기

 React hooks는 모두 use로 시작한다. 따라서 커스텀 훅도 반드시 user로 시작하는 파일명으로 해야한다.

 

2. 훅을 호출하는 것은 함수

 훅은 함수 컴포넌트 혹은 커스텀 훅의 안에서만 사용할 수 있다. 즉 훅은 보통 함수에 비교해 쓸 수 있는 범위가 좁다.

 

 

커스텀 훅 설계 혹은 구현할 때 주의할 점


 커스텀 훅은 제약이 거의 없다. 따라서 자유롭게 정의할 수 있다. 그럼에도 설계에 고민이 되거나 작성자에 따라 너무 형식이 달라지는 문제가 있으므로 조심해야할 부분에 대해서 가볍게 소개하도록 하겠다.

 

1. 반환값에 룰을 마련하기

  제일 만드는 사람에 따라 달라지는 부분이 반환값이다. 따라서 반환값에 관해서는 프로젝트에서 룰을 마련하는 것을 추천한다.

 프로젝트에서 공통인식을 갖고 있으면 독자적인 룰을 만드는 것은 문제 없지만, 일반적으로 사용되고 있는 두 가지 패턴에 대해서 설명하도록 하겠다.

패턴1 : React hooks의 스타일 맞추기

 useState나 useEffect, useRef등의 기본적인 React hooks에는 반호나값에 룰이 있어, 커스텀 훅도 그 룰에 맞추는 방법이 있다. 

 일반적인 매너라고 불릴 정도로 제일 많이 사용되는 방법이다.

React hooks 반환값
useEffect 반환값없음
useRef 1개
useContext 1개
useState 배열([state, state갱신함수])
useReducer 배열([state, state갱신함수])

 React hooks에 스타일을 맞추게 되면 아래의 두 가지 장점이 있다.

  • 반환값만으로 어떤 훅이인지 어느 정도 예측할 수 있다.
  • 커스텀 훅의 처리 자체에도 통제할 수 있다.
    • 반환값이 거대화됐을 때, 처리를 분할해야하는가에 대해 살펴볼 수 있기 때문에

패턴2 : 오브젝트를 모두 반환

 반환하고 싶은 것을 모두 오브젝트로 묶어서 반환하는 패턴도 있다. 매우 심플하고, 누구든 관계없이 공통 인식을 맞추기 쉬운 방법이다. 

 오브젝트를 반환하는 방법에 대한 메리트는 아래의 세 가지이다.

  • 반환값을 늘리고 싶을 때 변경이 필요하지 않다.
  • 컴포넌트에서 호출할 때, 반환값의 명명이 변경되지 않는다.
  • 테스트할 때에 값을 취하기 쉽다(result.current.XX과 같은)

 

2. 메모화 의식하기

 커스텀 훅을 만들 때에 랜더링 최적화를 위해 메모화는 의식하는 것을 추천한다. React에서는 컴포넌트의 재랜더링과 함게 갑시아 함수도 새롭게 만들어진다.

 그러므로 값이나 함수를 캐시시켜, 재랜더링할 때에 캐시된 값이나 함수를 반환하도록 하는 것으로 랜더링을 최적화할 수 있다.

함수의 메모화(useCallback)

 useCallback은 함수를 메모화하기 위한 훅이다. 함수를 반환하는 커스텀 훅에 대해서는 모두 useCallback으로 감싸주는 것이 좋다.

import { useState } from "react";

export const useCounter = () => {
  const [count, setCount] = useState(0);

  // 메모화
  const incrementCount = useCallback(() => setCount(() => count + 1), []);

  // 메모화
  const decrementCount = useCallback(() => setCount(() => count - 1), []);

  return [count, { incrementCount, decrementCount }];
};

값의 메모화(useMemo)

 useMemo는 실행 결과나 값을 메모화할 수 있는 훅이다. 커스텀 훅내에서 배열 조작등의 무거운 처리를 실행한 결과를 반환할 경우 사용할 수 있다.

import { useContext } from "react";
import { usersContext } from "./Users";

/* usersContext으로 배되되는 유저 데이터는 아래와 같다.
  const users = [
    {
      id: 1,
      name: "홍길동"
    },
    {
      id: 2,
      name: "홍길서"
    },
    ・・・
  ]
*/

export const useUserNames = () => {
  const users = useContext(usersContext)

  // 배열 조작의 실행 결과를 메모화
  const userNames = useMemo(() => users.map(({name}) => name), [users])

  return { userNames }  
};

 

 

예시


 샘플 코드는 React 공식 사이트에 있는 것을 가져왔다. 채팅 어플리케이션을 만드는 것을 상정한 코드로, 친구가 온라인인지 오프라인인지를 표시하는 메시지를 반환하는 컴포넌트이다.

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

 useState로 state와 갱신 함수를 생성, useEffect내에서 API를 구독하고 state를 갱신한다. 그렇게 갱신된 isOnline에 따라 문자열을 반환한다. 코드 내용 자체는 이번에 메인 내용이 아니므로 가볍게 이해해두면 된다.

 문제는 동일한 로직을 다른 컴포넌트에서도 사용할 때이다. 예를 들어, 친구 리스트 화면에서 올라인 상태의 유저명은 녹색으로 하는 요건이 있다고 하자.

 위의 FriendStatus를 사용한 isOnline로직을 복사/붙여넣기하면 구현된다.

import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

 그러나 동일한 코드가 여러 컴포넌트내에서 있는 것은 별로 좋지 않다. 공통 로직을 다른 함수로 만들어 컴포넌트에서는 그 함수를 호출하는 방식으로 하면 깔끔해질 것이다. 

 그 공통 로직을 다른 함수로 만드는 것을 커스텀훅이라고 한다. 그럼 지금 바로 커스텀 훅을 만들어보자.

 FriendSatus도 FriendListItem도 특정 유저의 접속 상태를 판단한 isOnlen가 필요하다. 그러므로 유저 ID를 인수로 받아 isOnline을 반환하는 훅을 만들면 완벽하다.

 hooks 디렉토리를 만들어, 그 아래에 useFriendStatus.jsx이라는 파일을 만든다.

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

 위에서 봤던 코드 그대로이므로 특별히 새로운 것은 없다. 그럼 커스텀 훅이 완성 됐다.


참고자료

https://zenn.dev/luvmini511/articles/df410f137d1e21

https://qiita.com/cheez921/items/af5878b0c6db376dbaf0

https://reffect.co.jp/react/react-custom-hook/

728x90