v19.2Latest

커스텀 훅으로 로직 재사용하기

React에는 useState,useContext, useEffect와 같은 여러 내장 훅이 있습니다. 때로는 더 구체적인 목적을 위한 훅이 있었으면 하는 경우가 있습니다. 예를 들어, 데이터를 가져오거나, 사용자가 온라인 상태인지 추적하거나, 채팅방에 연결하는 등의 용도입니다. 이러한 훅을 React에서 찾지 못할 수도 있지만, 애플리케이션의 필요에 맞게 자신만의 훅을 만들 수 있습니다.

배울 내용
  • 커스텀 훅이 무엇인지, 그리고 자신만의 훅을 작성하는 방법
  • 컴포넌트 간에 로직을 재사용하는 방법
  • 커스텀 훅의 이름 지정 및 구조화 방법
  • 커스텀 훅을 추출하는 시기와 이유

커스텀 훅: 컴포넌트 간 로직 공유

대부분의 앱과 마찬가지로 네트워크에 크게 의존하는 앱을 개발 중이라고 상상해 보세요. 사용자가 앱을 사용하는 동안 네트워크 연결이 실수로 끊겼을 때 사용자에게 경고하고 싶습니다. 어떻게 해야 할까요? 컴포넌트에 두 가지가 필요할 것 같습니다:

  1. 네트워크가 온라인 상태인지 추적하는 상태 조각.
  2. 전역 onlineoffline이벤트를 구독하고 해당 상태를 업데이트하는 Effect.

이렇게 하면 컴포넌트가 네트워크 상태와동기화됩니다. 다음과 같이 시작할 수 있습니다:

네트워크를 켜고 끄면서 이 StatusBar가 사용자의 동작에 반응하여 업데이트되는 것을 확인해 보세요.

이제 다른 컴포넌트에서도 동일한 로직을 사용하고 싶다고또한상상해 보세요. 네트워크가 끊겨 있을 때 비활성화되고 "저장" 대신 "재연결 중..."을 표시하는 저장 버튼을 구현하려고 합니다.

시작하려면 isOnline상태와 Effect를SaveButton에 복사하여 붙여넣을 수 있습니다:

네트워크를 끄면 버튼의 모양이 변경되는지 확인하세요.

이 두 컴포넌트는 잘 작동하지만, 둘 사이의 로직 중복은 아쉽습니다. 두 컴포넌트가 서로 다른시각적 외관을 가지고 있더라도, 그들 사이의 로직을 재사용하고 싶은 것 같습니다.

컴포넌트에서 자신만의 커스텀 훅 추출하기

잠시 useStateuseEffect와 유사하게 내장된useOnlineStatus훅이 있다고 상상해 보세요. 그러면 이 두 컴포넌트를 단순화하고 그들 사이의 중복을 제거할 수 있습니다:

그러한 내장 훅은 없지만, 직접 작성할 수 있습니다.useOnlineStatus라는 함수를 선언하고 이전에 작성한 컴포넌트에서 중복된 모든 코드를 그 안으로 이동하세요:

함수 마지막에서 isOnline을 반환합니다. 이렇게 하면 컴포넌트가 해당 값을 읽을 수 있습니다:

네트워크를 켜고 끄는 것이 두 컴포넌트를 모두 업데이트하는지 확인하세요.

이제 컴포넌트에는 반복적인 로직이 많이 없습니다.더 중요한 것은, 그 안의 코드가어떻게 수행할지(브라우저 이벤트를 구독하는 방식)보다는무엇을 원하는지(온라인 상태를 사용하라!)를 설명한다는 점입니다.

로직을 커스텀 훅으로 추출하면, 외부 시스템이나 브라우저 API를 어떻게 다루는지에 대한 복잡한 세부 사항을 숨길 수 있습니다. 컴포넌트의 코드는 구현 방식이 아니라 의도를 표현합니다.

훅 이름은 항상 use로 시작합니다

React 애플리케이션은 컴포넌트로 구성됩니다. 컴포넌트는 내장 훅이든 커스텀 훅이든 훅으로 구성됩니다. 다른 사람이 만든 커스텀 훅을 자주 사용하게 될 가능성이 높지만, 가끔은 직접 작성할 수도 있습니다!

다음 명명 규칙을 따라야 합니다:

  1. React 컴포넌트 이름은 대문자로 시작해야 합니다,예를 들어StatusBarSaveButton처럼요. React 컴포넌트는 또한 JSX 조각처럼 React가 표시할 수 있는 무언가를 반환해야 합니다.
  2. 훅 이름은 use로 시작하고 그 뒤에 대문자가 와야 합니다,예를 들어useState(내장) 또는useOnlineStatus(커스텀, 페이지 앞부분에서처럼)처럼요. 훅은 임의의 값을 반환할 수 있습니다.

이 규칙은 컴포넌트를 살펴볼 때 항상 그 안의 상태, Effect 및 기타 React 기능이 어디에 "숨겨져" 있을 수 있는지 알 수 있도록 보장합니다. 예를 들어, 컴포넌트 내부에서getColor() 함수 호출을 보면, 그 이름이 use로 시작하지 않기 때문에 그 안에 React 상태가 포함될 수 없다는 것을 확신할 수 있습니다. 그러나useOnlineStatus()와 같은 함수 호출은 그 안에 다른 훅 호출이 포함될 가능성이 매우 높습니다!

참고

린터가 React용으로 구성되어 있다면,이 명명 규칙을 적용할 것입니다. 위의 샌드박스로 스크롤하여useOnlineStatusgetOnlineStatus로 이름을 바꿔보세요. 린터가 더 이상 그 안에서useStateuseEffect를 호출하는 것을 허용하지 않을 것입니다. 오직 훅과 컴포넌트만이 다른 훅을 호출할 수 있습니다!

Deep Dive
렌더링 중 호출되는 모든 함수는 use 접두사로 시작해야 하나요?

커스텀 Hook은 상태 자체가 아닌 상태 관련 로직을 공유합니다

이전 예제에서 네트워크를 켜고 끌 때 두 컴포넌트가 함께 업데이트되었습니다. 그러나 단일isOnline상태 변수가 그들 사이에 공유된다고 생각하는 것은 잘못된 것입니다. 다음 코드를 살펴보세요:

이는 중복을 추출하기 전과 동일하게 작동합니다:

이는 완전히 독립적인 두 개의 상태 변수와 Effect입니다! 동일한 외부 값(네트워크가 켜져 있는지 여부)으로 동기화했기 때문에 동시에 동일한 값을 가지게 된 것입니다.

이를 더 잘 설명하기 위해 다른 예제가 필요합니다. 다음Form컴포넌트를 고려해 보세요:

각 폼 필드에 대해 반복적인 로직이 있습니다:

  1. 상태 조각이 있습니다 (firstNamelastName).
  2. 변경 핸들러가 있습니다 (handleFirstNameChangehandleLastNameChange).
  3. 해당 입력에 대한 valueonChange속성을 지정하는 JSX 조각이 있습니다.

반복적인 로직을 이 useFormInput커스텀 Hook으로 추출할 수 있습니다:

이것은 하나의 상태 변수인 value만 선언합니다.

그러나 Form 컴포넌트는 useFormInput을 두 번 호출합니다:

이것이 두 개의 별도 상태 변수를 선언하는 것처럼 작동하는 이유입니다!

커스텀 훅을 사용하면상태 저장 로직은 공유할 수 있지만상태 자체는 공유할 수 없습니다.훅에 대한 각 호출은 동일한 훅에 대한 다른 모든 호출과 완전히 독립적입니다.이것이 위의 두 샌드박스가 완전히 동등한 이유입니다. 원한다면 위로 스크롤하여 비교해 보세요. 커스텀 훅을 추출하기 전과 후의 동작은 동일합니다.

여러 컴포넌트 간에 상태 자체를 공유해야 할 때는,상태를 끌어올려 내려주는 방식을 사용하세요.

훅 간에 반응형 값 전달하기

커스텀 훅 내부의 코드는 컴포넌트가 다시 렌더링될 때마다 다시 실행됩니다. 이것이 컴포넌트와 마찬가지로 커스텀 훅도순수해야 하는 이유입니다.커스텀 훅의 코드를 컴포넌트 본문의 일부로 생각하세요!

커스텀 훅은 컴포넌트와 함께 다시 렌더링되므로 항상 최신의 props와 상태를 받습니다. 이것이 무엇을 의미하는지 이해하려면 이 채팅방 예제를 살펴보세요. 서버 URL이나 채팅방을 변경해 보세요:

이나 roomId를 변경하면 Effect가 변경 사항에"반응"하여다시 동기화합니다

이제 Effect의 코드를 커스텀 훅으로 옮겨보세요:

이렇게 하면ChatRoom컴포넌트가 내부 동작을 걱정하지 않고 커스텀 훅을 호출할 수 있습니다:

훨씬 간단해 보입니다! (하지만 동일한 작업을 수행합니다.)

로직이 여전히 prop과 상태 변화에반응한다는 점에 주목하세요. 서버 URL이나 선택된 방을 편집해 보세요:

한 Hook의 반환값을 어떻게 가져오는지 주목하세요:

그리고 그것을 다른 Hook의 입력으로 전달합니다:

컴포넌트가 다시 렌더링될 때마다 최신ChatRoomroomId마치serverUrl의 출력이 useState의 입력에 "공급"되는 것처럼 말입니다.)

커스텀 Hook에 이벤트 핸들러 전달하기

더 많은 컴포넌트에서useChatRoom을 사용하기 시작하면, 컴포넌트가 그 동작을 커스터마이즈할 수 있도록 하고 싶을 수 있습니다. 예를 들어, 현재 메시지가 도착했을 때 수행할 로직은 Hook 내부에 하드코딩되어 있습니다:

이 로직을 컴포넌트로 다시 옮기고 싶다고 가정해 보세요:

이것이 작동하도록 하려면, 커스텀 Hook이 명명된 옵션 중 하나로onReceiveMessage를 받도록 변경하세요:

이것은 작동하지만, 커스텀 Hook이 이벤트 핸들러를 받을 때 할 수 있는 한 가지 개선점이 더 있습니다.

에 대한 의존성을 추가하는 것은 이상적이지 않습니다이 이벤트 핸들러를 Effect Event로 감싸서 의존성에서 제거하세요:

이제 컴포넌트가 다시 렌더링될 때마다 채팅이 다시 연결되지 않습니다. 다음은 커스텀 Hook에 이벤트 핸들러를 전달하는 완전히 작동하는 데모입니다. 직접 실험해 볼 수 있습니다:

이제 어떻게useChatRoom이 작동하는지 알 필요가 없어졌다는 점에 주목하세요. 다른 컴포넌트에 추가하거나 다른 옵션을 전달해도 동일하게 작동할 것입니다. 이것이 커스텀 훅의 힘입니다.

커스텀 훅을 사용할 시기

모든 작은 중복 코드에 대해 커스텀 훅을 추출할 필요는 없습니다. 약간의 중복은 괜찮습니다. 예를 들어, 앞서처럼 단일useState호출을 감싸기 위해useFormInput훅을 추출하는 것은 불필요할 수 있습니다.

하지만 Effect를 작성할 때마다, 커스텀 훅으로 감싸는 것이 더 명확할지 고려하세요.Effect는 자주 필요하지 않으므로,Effect를 작성한다는 것은 외부 시스템과 동기화하거나 React에 내장된 API가 없는 작업을 수행하기 위해 "React 외부로 나가야" 한다는 의미입니다. 커스텀 훅으로 감싸면 의도를 명확히 전달하고 데이터가 어떻게 흐르는지 알 수 있습니다.

예를 들어, 두 개의 드롭다운을 표시하는ShippingForm컴포넌트를 생각해 보세요: 하나는 도시 목록을, 다른 하나는 선택된 도시의 지역 목록을 표시합니다. 다음과 같은 코드로 시작할 수 있습니다:

이 코드는 상당히 반복적이지만,이러한 Effect들을 서로 분리된 상태로 유지하는 것이 올바릅니다.그들은 서로 다른 두 가지를 동기화하므로 하나의 Effect로 병합해서는 안 됩니다. 대신, 위의ShippingForm컴포넌트를 간소화하기 위해 그들 사이의 공통 로직을 자신만의useData훅으로 추출할 수 있습니다:

이제 ShippingForm컴포넌트의 두 Effect를 모두useData호출로 대체할 수 있습니다:

커스텀 훅을 추출하면 데이터 흐름이 명확해집니다.url을 입력하면data를 얻습니다. Effect를useData 안에 "숨김"으로써, ShippingForm컴포넌트에서 작업하는 사람이 여기에불필요한 의존성을 추가하는 것을 방지합니다.시간이 지나면 앱의 대부분의 Effect는 커스텀 훅 안에 있게 될 것입니다.

Deep Dive
커스텀 훅을 구체적인 고수준 사용 사례에 집중하세요

커스텀 훅은 더 나은 패턴으로 마이그레이션하는 데 도움을 줍니다

Effect는"탈출구"입니다: "React 외부로 나가야 할 때"와 사용 사례에 대해 더 나은 내장 솔루션이 없을 때 사용합니다. 시간이 지남에 따라 React 팀의 목표는 더 구체적인 문제에 대한 더 구체적인 솔루션을 제공하여 앱의 Effect 수를 최소한으로 줄이는 것입니다. Effect를 커스텀 훅으로 감싸면 이러한 솔루션이 제공될 때 코드를 업그레이드하기가 더 쉬워집니다.

이 예시로 돌아가 보겠습니다:

위 예시에서 useOnlineStatususeStateuseEffect쌍으로 구현되었습니다. 그러나 이것은 가능한 최선의 솔루션이 아닙니다. 고려하지 않은 여러 가지 경계 사례가 있습니다. 예를 들어, 컴포넌트가 마운트될 때isOnline이 이미true라고 가정하지만, 네트워크가 이미 오프라인 상태라면 이는 틀릴 수 있습니다. 브라우저의navigator.onLineAPI를 사용하여 이를 확인할 수 있지만, 이를 직접 사용하면 초기 HTML을 생성하는 서버에서는 작동하지 않습니다. 요컨대, 이 코드는 개선될 수 있습니다.

React에는 useSyncExternalStore라는 전용 API가 포함되어 있어 이러한 모든 문제를 처리해 줍니다. 다음은 이 새로운 API의 장점을 활용하도록 다시 작성된 여러분의useOnlineStatus 훅입니다:

이 마이그레이션을 위해컴포넌트를 전혀 변경할 필요가 없었던점을 확인하세요:

이것이 Effect를 커스텀 Hook으로 감싸는 것이 종종 유익한 또 다른 이유입니다:

  1. Effect에서 오고 가는 데이터 흐름을 매우 명시적으로 만들 수 있습니다.
  2. 컴포넌트가 Effect의 정확한 구현보다는 의도에 집중하도록 할 수 있습니다.
  3. React가 새로운 기능을 추가할 때, 컴포넌트를 변경하지 않고도 해당 Effect들을 제거할 수 있습니다.

디자인 시스템과 유사하게, 앱의 컴포넌트에서 일반적인 관용구를 추출하여 커스텀 Hook으로 만드는 것이 도움이 될 수 있습니다. 이렇게 하면 컴포넌트 코드가 의도에 집중하게 되고, 원시 Effect를 자주 작성하지 않아도 됩니다. 많은 훌륭한 커스텀 Hook들이 React 커뮤니티에서 관리되고 있습니다.

Deep Dive
React가 데이터 페칭을 위한 내장 솔루션을 제공할까요?

방법은 하나 이상입니다

브라우저의 처음부터애니메이션의 각 프레임 동안,requestAnimationFrame API를 사용하여 ref로 보유한DOM 노드의 불투명도를1에 도달할 때까지 변경할 수 있습니다. 코드는 다음과 같이 시작될 수 있습니다:

컴포넌트의 가독성을 높이기 위해, 로직을useFadeIn커스텀 Hook으로 추출할 수 있습니다:

useFadeIn코드를 그대로 유지할 수도 있지만, 더 리팩터링할 수도 있습니다. 예를 들어, 애니메이션 루프 설정 로직을useFadeIn에서 추출하여 커스텀useAnimationLoopHook으로 만들 수 있습니다:

하지만 그렇게 할필요는 없습니다. 일반 함수와 마찬가지로, 궁극적으로 코드의 여러 부분 사이의 경계를 어디에 그릴지는 여러분이 결정합니다. 매우 다른 접근 방식을 취할 수도 있습니다. Effect 안에 로직을 유지하는 대신, 대부분의 명령형 로직을 JavaScript클래스 안으로 옮길 수 있습니다:

Effect는 React를 외부 시스템에 연결할 수 있게 해줍니다. Effect 간에 필요한 조정이 많을수록(예: 여러 애니메이션을 연결하는 경우), 그 로직을 Effect와 Hook에서완전히추출하는 것이 더 합리적입니다(위 샌드박스처럼). 그러면 추출한 코드는 “외부 시스템”이됩니다. 이렇게 하면 Effect는 React 외부로 옮긴 시스템에 메시지를 보내기만 하면 되므로 간단하게 유지될 수 있습니다.

위 예제들은 페이드인 로직을 JavaScript로 작성해야 한다고 가정합니다. 하지만 이 특정 페이드인 애니메이션은 일반CSS 애니메이션으로 구현하는 것이 더 간단하고 훨씬 효율적입니다:

때로는 Hook조차 필요하지 않습니다!

요약

  • 커스텀 Hook을 사용하면 컴포넌트 간에 로직을 공유할 수 있습니다.
  • 커스텀 Hook의 이름은use로 시작하고 대문자로 이어져야 합니다.
  • 커스텀 Hook은 상태 자체가 아닌 상태가 포함된 로직만 공유합니다.
  • 반응형 값을 한 Hook에서 다른 Hook으로 전달할 수 있으며, 그 값은 최신 상태로 유지됩니다.
  • 모든 Hook은 컴포넌트가 다시 렌더링될 때마다 다시 실행됩니다.
  • 커스텀 Hook의 코드는 컴포넌트의 코드처럼 순수해야 합니다.
  • 커스텀 Hook이 수신한 이벤트 핸들러는 Effect Event로 감싸세요.
  • 커스텀 Hook을 useMount처럼 만들지 마세요. 그 목적을 구체적으로 유지하세요.
  • 코드의 경계를 어디에 그리고 어떻게 정할지는 여러분에게 달려 있습니다.

Try out some challenges

Challenge 1 of 5:Extract a useCounter Hook #

This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:

export default function Counter() {
  const count = useCounter();
  return <h1>Seconds passed: {count}</h1>;
}

You’ll need to write your custom Hook in useCounter.js and import it into the App.js file.


커스텀 Hook으로 로직 재사용하기 | React Learn - Reflow Hub