Custom Hook에 관하여

·2023년 11월 16일
1

🕛 커스텀 훅이란?

리액트는 제공해주지 않지만, 개발을 진행할 때 개인적으로 'hook'으로 만들면 더 편하겠다고 느낄 때, 자신만의 hook으로 만드는 것을 의미한다.
즉, 같은 로직이 반복될 때 두 로직을 나누어서 작성하는 게 가능할지라도, 굉장히 비효율적이다. 이러한 로직을 하나의 커스텀훅 으로 따로 정의하여 비효율성을 낮출 수 있다.
커스텀 훅은 컴포넌트 분할과는 달리, 컴포넌트 로직 자체를 분할 + 재사용 할 수 있게 하는 것이다.

🕐 custom hook 규칙

  1. use로 시작해야 한다.

    리액트에서 hook들은 모두 use로 시작하는 것이 원칙이고, custom hook 또한 예외가 될 수 없다. use뒤 대문자로 시작하는 것 또한 지켜주어야 한다.
    따라서 커스텀훅이 아닌 일반 함수를 정의할 때에는 use로 시작하는 것을 지양해주어야 한다.

  2. state 자체가 아닌 state 저장 논리를 공유하는 것

    hook에 대한 각 호출은 동일한 hook에 대해 모든 호출이 독립적이다.
    즉, 같은 hook이 연이어 호출되었다고 해도 서로의 기능에는 영향을 미치지 않는다.
    한마디로 잘 작동한다. 모두 독립적인 로직 자체만을 공유하는 것이기 때문!

  3. custom hook은 순수 함수로 기대된다.

    custom hook은 컴포넌트가 리렌더링되면 함께 리렌더링이 된다.
    커스텀 훅은 늘 최신의 props와 state를 받기 때문이다.

  4. 최상위에서만, React 함수 내에서만 호출해야 한다.

리액트에서는 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장되어야 한다라고 말한다. 즉, hook을 조건문이나 일반 함수(Js)에서 사용하게 되면, 컴포넌트가 렌더링 될 떄마다 항상 동일한 순서로 호출이 보장되는 것의 약속을 깨지는 결과가 일어난다.

  1. customHook에 함수 넘겨주기

    함수를 custom hook에게 넘겨주는 형태로 변환할 수 있다.
    예시 코드로 살펴보자

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

위의 코드에서 showNotification 부분을 컴포넌트의 로직으로 바꾸고 싶다면?

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });
  // ...

컴포넌트에 로직을 심고, 이후

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onReceiveMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}

위와 같이 커스텀 훅으로 넘겨줄 수 있다.

하지만, 위와 같은 코드는 onReceiveMessage 에 deps를 추가했기 때문에, 컴포넌트가 리렌더링 될 때마다, 계속해서 커스텀훅또한 리렌더링될 것이다.

그렇다면 어떻게 해결할까?
useEffectEvent 를 통해 해결 가능하다.

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  const onMessage = useEffectEvent(onReceiveMessage);

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ All dependencies declared
}

이렇게 코드를 수정하면, 컴포넌트가 리렌더링되어도, 커스텀 훅에는 영향을 끼치지 못한다.

🕑 언제 사용할까?

모든 로직이 겹친다고 하나하나 커스텀 훅으로 뺄 필요는 없다.
그렇지만, Effect에 관한 부분이 있다면, 커스텀훅으로 감싸주는 것이 더 낫다.
Effect를 남용해서는 안되기 때문,,,
커스텀 훅으로 감쌀 때, 더욱 명확하게 결과를 보여주는 경우가 있다.

🤍 예시로 살펴보자 🤍

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  // This Effect fetches cities for a country
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]);

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  // This Effect fetches areas for the selected city
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]);

-> 위 코드의 겹치는 부분을 커스텀 훅으로 만들 수 있다.

다음 예시를 살펴보자

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    if (url) {
      let ignore = false;
      fetch(url)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setData(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [url]);
  return data;
}

-> 겹치는 Effect를 custom hook으로 만든 예시이다. 이는 컴포넌트에서 다음과 같이 불려올 수 있다.

function ShippingForm({ country }) {
  const cities = useData(`/api/cities?country=${country}`);
  const [city, setCity] = useState(null);
  const areas = useData(city ? `/api/areas?city=${city}` : null);
  // ...

이에 따라, useEffect를 그냥 쓰는 것보다 훨씬 간단하고 효과적으로 data fetching을 완료 할 수 있다. (그치만, useEffect 써도 막 크게 문제되는 건 없긴 함,,,)

🕒 Custom Hook 장점

  1. Effect와 데이터 흐름을 명확하게 만들 수 있다.
  2. 컴포넌트가 자신의 역할에 집중할 수 있다.
  3. 리액트가 새로운 기능을 추가했을 시, 컴포넌트 요소를 바꾸지 않고도 해당 effect를 제거할 수 있다.

    🕓 Custom Hook 만들어 보자


    -> 몇 가지 체크 박스가 있고 모두 체크 되었을 경우, 다음으로 넘어가는 기능을 첨가해 만들 것이다. 현재는 두 개만 체크되어 있기 때문에 비활성화가 된 상태이며, 체크 박스를 모두 눌러야만 활성화가 된다. 그렇다면 코드로 이를 구현해보자.
const labels = ['check 1', 'check 2', 'check 3']
 
const App: React.FunctionComponent = () => {
  const [checkList, setCheckList] = useState([false, false, false])
 
  // index 번째 체크 상태를 반전시킨다
  const handleCheckClick = (index: number) => {
    setCheckList((checks) => checks.map((c, i) => (i === index ? !c : c)))
  }
 
  const isAllChecked = checkList.every((x) => x)
 
  return (
    <div>
      <ul>
        {labels.map((label, idx) => (
          <li key={idx}>
            <label>
              <input
                type='checkbox'
                checked={checkList[idx]}
                onClick={() => handleCheckClick(idx)}
              />
              {label}
            </label>
          </li>
        ))}
      </ul>
      <p>
        <button disabled={!isAllChecked}>다음</button>
      </p>
    </div>
  )
} 

-> useState를 이용해서 checkList라는 상태 변수를 선언(boolean 배열)하고, 요소들을 체크 박스에 하나씩 대응한다. 이때, 체크박스가 나열된 부분을 컴포넌트로 분할하면 다음과 같다.

type Props = {
  checkList: readonly boolean[]
  labels: readonly string[]
  onCheck: (index: number) => void
}
 
export const Checks: React.FunctionComponent<Props> = ({
  checkList,
  labels,
  onCheck
}) => {
  return (
    <ul>
      {labels.map((label, idx) => (
        <li key={idx}>
          <label>
            <input
              type='checkbox'
              checked={checkList[idx]}
              onClick={() => onCheck(idx)}
            />
            {label}
          </label>
        </li>
      ))}
    </ul>
  )
} 

이 컴포넌트를 사용해보자.

const labels = ['check 1', 'check 2', 'check 3']
 
const App: React.FunctionComponent = () => {
  const [checkList, setCheckList] = useState([false, false, false])
 
  // index 번째 체크 상태를 반전시킨다
  const handleCheckClick = (index: number) => {
    setCheckList((checks) => checks.map((c, i) => (i === index ? !c : c)))
  }
 
  const isAllChecked = checkList.every((x) => x)
 
  return (
    <div>
      <Checks
        checkList={checkList}
        labels={labels}
        onCheck={handleCheckClick}
      />
      <p>
        <button disabled={!isAllChecked}>다음</button>
      </p>
    </div>
  )
} 

이와 같은 부분에서, App 컴포넌트는 현재도 여전히,
1. 상태변수인 checkList
2. 이벤트 핸들러인 handleCheckClick
3. isAllCheck 계산하는 로직
4. UI 보여주기
네 개의 기능이 남아 있다.
이때 App 컴포넌트는 모두 체크되었는지? 에만 관심이 있고,
무엇이 체크되었는지는 App이 소관하는 영역이 아니다.

이런 부분에서 커스텀 훅을 등장시켜 보자

type UseChecksResult = [boolean, () => JSX.Element]
 
export const useChecks = (labels: readonly string[]): UseChecksResult => {
  const [checkList, setCheckList] = useState(() => labels.map(() => false))
 
  const handleCheckClick = (index: number) => {
    setCheckList((checks) => checks.map((c, i) => (i === index ? !c : c)))
  }
 
  const isAllChecked = checkList.every((x) => x)
 
  const renderChecks = () => (
    <Checks checkList={checkList} labels={labels} onCheck={handleCheckClick} />
  )
 
  return [isAllChecked, renderChecks]
} 

useChecks라는 훅을 정의했는데, 이는 체크 박스(string[])를 받아와서 체크 박스를 관리하는 로직을 내포하고 있다.
반환값으로는 모두 체크 되었는지?체크박스를 렌더링하는 함수가 있다.

이를 적용하면 다음과 같다.

const App: React.FunctionComponent = () => {
  const [isAllChecked, renderChecks] = useChecks(labels)
 
  return (
    <div>
      {renderChecks()}
      <p>
        <button disabled={!isAllChecked}>다음</button>
      </p>
    </div>
  )
} 

출처

라인
리액트공식문서

profile
new blog: https://hae0-02ni.tistory.com/

6개의 댓글

comment-user-thumbnail
2023년 11월 16일

평소 관심 있던 분야여서 흥미롭게 글을 읽은 거 같습니다.

저는 보통 custom hooks를 사용하게 되는 경우는
View를 담당하는 컴포넌트에서 로직을 분리하기 / 반복되는 로직을 추상화해서 선언적으로 사용하기

위해서 인 것 같습니다.

custom hooks를 사용해서 코드의 관심사의 분리나 추상화가 더 집중적으로 될 수 있다고 생각하는데,
custom hooks 자체의 코드도 관리 할 필요가 있다는 생각을 하게 됐습니다. 간단한 기능을 가진 custom hooks를 만들게 되면 상관 없지만, 복잡한 기능을 넣어주게 되면 그 마저도 클린코드가 필요하니까요 !
계속 해서 재활용 되어 지는 custom hooks는 클린코드를 신경쓰고, 가독성을 높이며 리펙토링이 쉽게 하는 것이 중요한 부분 중 하나가 아닐까 생각합니다.

예전 프로젝트 당시 다양한 custom hooks를 만들어 프로젝트를 진행했었는데, 관련 내용을 정리한 github wiki 링크를 함께 남겨둡니다 !!
https://github.com/yulpumta-clone-team/Co-nect/wiki/%ED%95%B5%EC%8B%AC-hooks-%EC%84%A4%EB%AA%85

custom hooks에 대해서는 더 많이 공부하고 싶네요 !!! 좋은 글 감사합니다 🔥

답글 달기
comment-user-thumbnail
2023년 11월 19일

이번 과제에서 로그인과 회원가입 하는 부분의 로직이 비슷해서 커스텀훅으로 구현할까 하다가 뭔가 로직이 중복되는 듯 안 되는 듯 하여 .. 커스텀훅을 사용하지 않았습니다 ... 그래서 어느정도로 동일한 로직일 경우에 사용하는게 맞는 것인지에 대해서 명확한 기준이 잘 안잡혔던 터라 이번 아티클이랑 세션에서 많이 배워가려구 합니다 !!
아티클을 읽으면서 useEffectEvent라는 것을 처음알게 되었는데 기존에 있던 useEvent를 업데이트 시킨 것이라고 하더라구요 !
useEvent는 reference가 동일한 함수를 반환(useCallback 처럼)하고 함수 안에서 접근하고 있는 state는 항상 컴포넌트의 현재 state와 동일하도록 하여(deps 없이 항상 현재 상태를 사용 가능) 중복 렌더링을 최적화 할 수 있는 Hook이었는데 useEffectEvent에서는 함수 reference를 반환하지 않는 대신 dependencies array에 지정하지 않고 useEffect에서 참조할 수 있도록 한다구 해요 !
https://velog.io/@eunseo9808/%EB%B2%88%EC%97%AD-React%EC%97%90-useEffectEvent%EA%B0%80-%EB%82%98%EC%98%A8%EB%8B%A4#1-useevent%EB%8A%94-%EC%A2%85%EB%A3%8C%EB%90%90%EC%A7%80%EB%A7%8C-useeffectevent%EA%B0%80-%EC%98%A4%EB%9E%98-%EC%A7%80%EC%86%8D%EB%90%9C%EB%8B%A4
관련 아티클 첨부하고 갑니당 !

답글 달기
comment-user-thumbnail
2023년 11월 19일

커스텀 훅에 대한 아티클 잘 읽었습니다!
커스텀 훅에 대해 제대로 알지 못한 채, 사용을 하려고 했던 적이 있었어요. 함수 내에서 커스텀 훅을 부르려고 했을 때나 useEffect 내에서 호출 시에도 에러가 발생하곤 하더라구요. 아티클을 읽으며 주먹구구 식으로 사용하기도 했던 커스텀 훅에 대해 깔끔하게 정리할 수 있었습니다!

특히 예시를 들어주신 부분 중 useEffectEvent라는 건 처음 접하게 되어서 따로 자료를 더 찾아봤는데요, useEffectEvent는 리액트의 effect 안에서 특정 코드 블록을 실행할 때, 해당 코드 블록이 특정 종속성에 반응하지 않도록 하는 메커니즘이라고 해요. props 또는 state의 최신 값을 기반으로 작업을 수행하려는 경우에 유용하며, 이렇게 하면 effect가 특정 값이 변경될 때만 다시 실행되고 다른 값들의 변경에는 반응하지 않는다고 합니다.
다만, useEffectEvent는 아직 정식 출시된 훅이 아니기 때문에 커스텀 훅으로 직접 작성해야 하며, 사용 방법이 제한적이라고 하네용 !

커스텀 훅에 대한 실습도 기대하겠습니당 수고하셨어요 !

답글 달기
comment-user-thumbnail
2023년 11월 19일

custom hook은 뭐랄까 진입장벽은 높은데 막상 익숙해지면 훨씬 깔끔한 코드를 짤 수 있게 해주는 것 같습니다.. 저는 아직 그 진입장벽을 제대로 넘지 못했는데 이렇게 또 정리된 글과 코드를 읽어보니 너무 어렵게 생각할 필요도 없고~ 명확한 기능 분리를 위해 꼭 여러번 연습해봐야겠다는 생각이 많이 듭니다!
그러나 역시 고민되는 것은 언제 어떻게 사용해야 하냐는 것입니다. 관련해서 좀 더 알아보다가 좀 더 확실해진 것은, 컴포넌트의 역할을 명확히 하는 것이 언제나 중요하다는 점입니다. 이를 위해 custom hook은 특히 Presentation Component와 Container Component를 분리하기 위해 좋은 기술이라는 생각이 듭니다. custom hook을 통한 로직 분리를 통해 버튼이 보여지는 것과, 버튼이 동작하는 것을 명확히 나눔으로써 재사용도 유지보수도 훨씬 좋아질것입니다. 더불어 Dan Abramov이 직접 custom hook에 대해서 두가지 고려해야할 점을 이야기한 적이 있는데요, 1. 합성과 2. 디버깅입니다. 합성은 여러 개의 custom hook이 서로에게 영향을 주지 않고 독립적으로 함께 동작할 수 있어야 함을 의미하고, 디버깅은 custom hook이 코드의 인과관계를 파악하는데 영향을 주지 않아야 한다는 것입니다.

답글 달기
comment-user-thumbnail
2023년 11월 20일

I have a lot of clients who come back again and again, which shows that I provide the most efficient service of escorts available in Udaipur. Many people want to spend the day in Udaipur with the woman who is their soulmate. Independent escorts in Udaipur fulfill their desires and I offer the woman they always desire. I want to see because of my beauty, I have never had anyone assert that I am not a model since I maintain my figure and figure.
https://www.simranroy.co.in
https://www.simranroy.co.in/nainital-escorts-services.html
https://www.simranroy.co.in/haldwani-escorts-services.html
https://www.simranroy.co.in/ramnagar-escorts-services.html
https://www.simranroy.co.in/rudrapur-escorts-services.html
https://www.simranroy.co.in/bhimtal-escorts-services.html
https://www.simranroy.co.in/ajmer-escorts-services.html
https://www.simranroy.co.in/jodhpur-escorts-services.html
https://www.simranroy.co.in/jaipur-escorts-services.html
https://www.simranroy.co.in/nasirabad-escorts-services.html
https://www.simranroy.co.in/beawar-escorts-services.html
https://www.simranroy.co.in/pushkar-escorts-services.html
https://www.simranroy.co.in/kishangarh-escorts-services.html
https://www.simranroy.co.in/dehradun-escorts-services.html
https://www.simranroy.co.in/haridwar-escorts-services.html
https://www.simranroy.co.in/mussoorie-escorts-services.html
https://www.simranroy.co.in/rishikesh-escorts-services.html
https://www.simranroy.co.in/lucknow-escorts-services.html
https://www.simranroy.co.in/agra-escorts-services.html
https://www.simranroy.co.in/raipur-escorts-services.html
https://www.simranroy.co.in/bhopal-escorts-services.html
https://www.simranroy.co.in/ahmedabad-escorts-services.html
https://www.simranroy.co.in/kanpur-escorts-services.html
https://www.simranroy.co.in/roorkee-escorts-services.html
https://www.simranroy.co.in/chennai-escorts-services.html
https://www.simranroy.co.in/surat-escorts-services.html
https://www.simranroy.co.in/patna-escorts-services.html
https://www.simranroy.co.in/gwalior-escorts-services.html
https://www.simranroy.co.in/kota-escorts-services.html
https://www.simranroy.co.in/mount-abu-escorts-services.html
https://www.simranroy.co.in/dungarpur-escorts-services.html
https://www.simranroy.co.in/hapur-escorts-services.html
https://www.simranroy.co.in/noida-escorts-services.html
https://www.simranroy.co.in/faridabad-escorts-services.html
https://www.simranroy.co.in/daman-escorts-services.html
https://www.simranroy.co.in/gurgaon-escorts-services.html
https://www.simranroy.co.in/sikar-escorts-services.html

답글 달기
comment-user-thumbnail
2023년 11월 21일

custom hook의 규칙에 대해서 다시 한 번 정리할 수 있어서 좋았습니다! 평소 복잡한 로직은 최대한 hook이나 util로 분리를 하려고 하는 편이라 이번 아티클을 흥미롭게 읽었습니다!! 정리 너무나도 깔끔하게 잘하셔서 수월하게 읽을 수 있었습니다

useEffectEvent에 대해서는 저도 이번 글을 통해 처음 접하게 되었는데요
다른 분들도 이미 많이 댓글을 달아주셨고, 제가 조사한 내용도 비슷한 내용들이라 생략하도록 하겠습니다

useEffect의 사용 지양에 대해 요즘들어 가장 관심을 갖고 있는데 마침 또 이와 관련된 공식자료를 첨부해주셔서 더 읽어봤습니다! 전에 아티클 준비하면서 읽었었는데 양이 많아 다 읽지 못했는데 이번 기회에 다른 부분들까지 정리할 수 있었습니다

이번에 알게된 내용을 조금 적자면(이번 아티클 주제와 살짝 동떨어진 감이 없진 않지만, 다른 분들이 너무나도 잘 작성해 주셔서 이에 대해 더 적는 건 불필요하다고 생각했습니다)

race condition을 피하기입니다

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 피해야 할 부분: 정리 로직 없이 가져오기
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

위와 같은 코드가 있을 때, 만약 요청이 빠르게 많이 온다면, 각각의 요청이 들어오는 대로 처리가 되겠지만 응답이 도착하는 순서에는 보장을 할 수가 없습니다
그로 인해 이 전 호출 보다 그 후 호출이 먼저 도착할 수도 있습니다
이것이 race condition이고 이를 해결하기 위해선 clean up 함수를 추가해야 합니다

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

위의 코드가 이를 추가한 예제입니다
이렇게 함으로써, 요청들이 빠르게 왔을 때 마지막으로 요청된 응답 외의 모든 응답은 무시할 수 있게 됩니다

이 부분은 알고 있으면 좋을 거 같아 가지고 와봤습니다!!

또한 마지막으로 컴포넌트에서 useEffect 호출이 적을수록 프로그램을 유지 관리하기 쉬워진다고 하니, 말씀해주신거 처럼 custom Hook으로 기능을 추출할 수 있으면 그렇게 하는 것이 좋다고 하네요! 위의 예제도 hook으로 분리하는 것이 좋다고 합니다!

좋은 아티클 잘 읽었습니다!!

답글 달기