sharingStorage

React custom hooks : what, how, when, why 본문

Front-End/React

React custom hooks : what, how, when, why

Anstrengung 2023. 11. 16. 21:12

React 커스텀훅 

 

what ?

컴포넌트간의 hook 로직을 공유하기 위해 사용하는 사용자가 커스텀한 hook입니다.

 

다음은 공식문서에서 발췌한 예시 코드입니다.

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

 

위의 코드는 네트워크가 연결된 경우를 추적하는 상태이고 전역 online, offline 이벤트를 구독하고 해당 상태를 업데이트하는 Effect hook입니다. 

 

이 중복되는 로직을 하나로 합쳐 로직을 공유하여 재사용하고 싶다는 마음이 생기지 않나요??

custom hook을 사용하면 가능합니다!

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}
function useOnlineStatus() {

// 이곳부터 
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
// 여기까지 중복되던 코드를 캡슐화시켰습니다.
  return isOnline;
}

위와 같이 state와 effect를 분리해 새로운 사용자만의 훅을 만들어서 사용할 수 있습니다.

 

이것이 커스텀훅을 사용하는 첫번째 이유입니다. 

반복되는 로직을 캡슐화하여 재사용가능하게 하고 다른 컴포넌트에서 또 다시 이 로직을 사용할 수 있게 합니다. (물론 이외에도 많은 이유가 있습니다 why파트에서 설명을 더하겠습니다.)

 

What : 커스텀훅은 state 자체가 아닌 state logic을 공유합니다. 

import { useFormInput } from './useFormInput.js';

export default function Form() {
  const firstNameProps = useFormInput('Mary');
  const lastNameProps = useFormInput('Poppins');

  return (
    <>
      <label>
        First name:
        <input {...firstNameProps} />
      </label>
      <label>
        Last name:
        <input {...lastNameProps} />
      </label>
      <p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
    </>
  );
}

위와 같이 같은 로직을 공유하지만 state는 각각 다르게 사용할 수 있습니다. 즉 hooks에 대한 각 호출은 완전히 독립적입니다.

 

 

How ?  (어떻게 사용하는가)

How : hooks의 이름은 use로 시작

커스텀훅은 use시작하고 그 뒤는 대문자로 시작하는 네이밍 컨벤션을 갖습니다. 마치 useState처럼요.

이 컨벤션은 커스텀훅 내부에 useState나 useEffect같은 기능들이 숨겨져 있더라도 우리에게 내부의 로직과 상태를 알 수 있게합니다.

 

예를 들면 getColor() 라는 함수는 내부에 react state를 포함하지 않는다는 것을 알 수 있고, useEffectOnce() 함수는 내부에 effect로직이 있다는 것을 알 수 있죠.

 

How : Linter가 React용으로 구성되어 있으면 use로 시작하지 않는 다른 함수에서 hooks를 호출할 수 없음.

get함수 내부에서 react hooks를 불러오면 lint에서 위와 같은 에러를 냅니다.

getColor는 리액트 함수 컴포넌트도 아니고 커스텀훅도 아닌데 내부에 hooks를 사용하면 에러가 발생하는 것을 확인하실 수 있습니다.

 

그럼 반대로 다른 일반 유틸함수들은 접미사로 use를 사용하지 않아야 custom hook과 일반 util 함수를 혼동하지 않겠죠??

 

How : 커스텀 훅은 조건부로 호출할 수 없음.

커스텀 훅은 결국은 리액트 훅의 사용 원칙을 따르기 때문에 hook의 순서를 보장해주어야합니다.

 

How : 훅사이에 반응형 값 전달하기

컴포넌트가 렌더링할 때마다 커스텀 훅 내부의 코드도 다시 실행되므로 커스텀 훅도 pure해야합니다. 커스텀 훅의 코드를 컴포넌트 본문의 일부로 생각하시면 됩니다.

또한 컴포넌트를 다시 렌더링할 때마다 커스텀 훅도 같이 리렌더링 되기 때문에 항상 최신 props와 state를 보장받습니다.

 

How : usehooks-ts 라이브러리 사용

  • 타스로 만들었지만 자스도 사용가능합니다
  • hook에 대해 어떻게 만들었는지 나와있어서 초기에 hook을 만드는데 영감받을 수 있습니다.

알아두면 좋을만한 custom hooks 예시

 

How : usehooks 라이브러리 사용

여러가지 훅을 import해서 사용 가능합니다.

https://usehooks.com/     

 

exam

import { useHover } from "@uidotdev/usehooks";

function getRandomColor() {
  const colors = ["green", "blue", "purple", "red", "pink"];
  return colors[Math.floor(Math.random() * colors.length)];
}

export default function App() {
  const [ref, hovering] = useHover();

  const backgroundColor = hovering
    ? `var(--${getRandomColor()})`
    : "var(--charcoal)";

  return (
    <section>
      <h1>useHover</h1>
      <article ref={ref} style={{ backgroundColor }}>
        Hovering? {hovering ? "Yes" : "No"}
      </article>
    </section>
  );
}

다음은 hover여부를 판단하는 custom hook을 사용해본 예제입니다.

useHover는 ref: ref값과 hovering: boolean 값을 배열로 리턴합니다.

※라이브러리 사용은 필수가 아닙니다! 참고할 수 있고 편의를 위해서 사용 가능성이 있다는 것만 알아두세요~

why ?

React에서 사용자 정의 hooks를 생성하면 개발자는 기능 요소 내에서 논리를 캡슐화하고 재사용할 수 있습니다. 

또 복잡한 논리를 재사용 가능한 단위로 추상화하여 코드 재사용성, 가독성 및 유지보수성을 향상시킵니다.

 

Why : 커스텀 훅을 사용하는 이유?

  • 데이터의 흐름을 명시적으로 파악하기 위해
  • 반복되는 hooks를 재사용하기 위해 (재사용성)
  • 비지니스 로직과 뷰를 분리하기 위해 (presentation and container design pattern)
  • 컴포넌트 설계할 때 단일 책임 원칙을 따를 수 있게 하기 위해
  • 더 쉬운 마이그레이션을 위해
  • 훌륭한 커뮤니티 (여러분이 필요한 훅이 이미 누군가 만들었을 가능성이 있습니다.) (라이브러리 사용 가능성 열려있음)

 

when? (언제 쓰는가 ?)

중복되는 모든 코드에 대해 커스텀 훅을 추출할 필요는 없습니다. 약간의 중복이 큰 문제가 되지 않습니다.

단 Effect를 사용할 땐 항상 커스텀훅으로 감싸는것이 더 명확할지 고려해야합니다.

그 이유는 Effect는 자주 필요하지 않고 Effect를 작성한다면 외부 시스템과 동기화하거나 React에 빌트인 API가 없는 작업을 수행하기 위해 React 외부로 나가야한다는 뜻입니다. 

  • 컴포넌트가 Effect의 정확한 구현보다 의도에 집중가능
  • React가 새로운 기능을 추가할 때 컴포넌트를 변경하지 않고도 해당 Effect 제거 가능

Effect를 커스텀훅으로 감싸면 의도와 데이터 흐름 방식을 정확하게 전달가능합니다.

결국은 컴포넌트의 가독성을 높이기 위해서, 유지보수성을 위해서 effect는 custom hook으로 사용하는 것을 공식문서에서 권장하고 있습니다.

 

아래 커스텀 훅으로 사용할 수 있는 몇가지 예시가 있습니다.

  • data fetching : 여러 구성요소에서 재사용해야하는 데이터 가져오기 로직이 있으면
  • form handling : 다양한 양식간 공유되는 유효성 검사나 제출로직 - 캡슐화 해하여 재사용 가능하게 만든다.
  • event handling : 커스텀 훅은 일반적인 이벤트 처리 로직을 추상화하여 구성요소 전체에서 사용할 수 있다.

https://medium.com/@navneetsingh_95791/reach-custom-hooks-when-to-use-reusability-and-portability-8c1486b9ea27

 

custom hook deep dive

naming convention

커스텀 훅의 명확한 이름을 정하는데 어려움이 있다면 Effect가 너무 많은 일을 하고 있는지 확인해봐야합니다. 이는 커스텀훅으로 추출될 준비가 되지 않았다는 것입니다. 이는 함수명을 지을 때 마주하는 어려움과 같습니다.

커스텀훅의 이름은 코드를 처음보는 사람도 훅이 무엇을 하고 무엇을 취하고 무엇을 반환하는지 짐작할 수 있을 정도로 명확해야합니다.

useSocket과 같은 기술적이거나 해당 시스템과 관련있는 전문용어 사용은 가능합니다.

 

custom hook good use

좋은 커스텀 훅이 수행하는 작업을 제한하여 보다 선언적으로 만듭니다.(이 부분에도 단일 책임 원칙이 적용되는 것 같습니다. )

무조건 hooks를 묶는다는 접근보다는 코드 여러 사이의 경계중에서 어디에 어떻게 그릴지를 개발자 자신이 근거를 명확히하고 결정하면 됩니다. 

항상 느끼는 거지만 개발자 나름의 근거를 가지고 코딩을 하는 것이 항상 중요한 덕목이라고 생각합니다.

예를들면 react hook을 커스텀훅으로 묶는 방법과 react hooks 의 로직 내부에 코드를 js 함수로 만들어 이것을 불러오는 방법 두가지를 적절한 근거를 가지고 선택할 수 있을 것 같습니다.

 

 

글을 마치며

처음엔 커스텀 훅이 그저 자주 사용하는 로직을 재사용하기 위한 목적으로 사용한다고 생각했지만 뷰와 로직의 분리하고 각각의 훅을 커스텀훅으로 분리하면서 컴포넌트를 가장 작은 단위로 만들면서 해당 컴포넌트에 대해 단일 책임 원칙을 지킬 수 있는 하나의 클린코드 접근 방법이라고도 생각이 들었습니다. 

마지막으로 우리가 자주 마주치던 fetch 함수를 custom hook으로 refactoring 하는 예제를 보여드리고 글을 마치겠습니다.

Exam

import React, { useState } from "react";
import useFetch from "./useFetch";

function App() {
  const [activeUserIndex, setActiveUserIndex] = useState(0);

  const [data, isLoading] = useFetch(
    `https://api.github.com/users/${users[activeUserIndex]}`
  );

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!data) {
    return <div>No data</div>;
  }
 return (
    <div className="App">
      <div>{data.data}</div>
    </div>
  );
}

export default App;


import { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    fetch(url)
      .then((res) => res.json())
      .then((info) => {
        setData(info);
        setIsLoading(false);
      });
  }, [url]);

  return [data, isLoading];
}

export default useFetch;

 

 

 

Reference

Comments