본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Reusing Logic with Custom Hooks(커스텀 훅으로 로직 재사용하기)

Custom Hooks: Sharing logic between components(커스텀 훅: 컴포넌트간의 로직 공유)

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

- 위의 첫 번째 예제는 네트워크 연결 상태 state에 따라 Online 또는 Disconnected를 화면에 표시하는 예제다.

- 위의 두 번째 예제는 네트워크 연결 상태 state에 따라 button의 상태 변경해서 화면에 표시하는 예제다.

- 위의 두 로직은 잘 작동하지만, 두 컴포넌트 간의 로직이 중복을 피하고 로직을 재사용하는 방법에 대해서 알아보자.


Extracting your own custom Hook from a component(컴포넌트에서 커스텀 훅 추출하기)

- 위 두 개의 예제에서 중복되는 로직을 useOnliceStatus라는 훅을 만들어 분리해보자.

// useOnlineStatus.js
import { useState, useEffect } from 'react';

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

// App.js
import { useOnlineStatus } from './useOnlineStatus.js';

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

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Hook names always start with use(훅의 이름은 언제나 use로 시작됩니다)

- React 애플리케이션은 컴포넌트로 빌드되기 때문에, 빌트인이든 커스텀이든 상관없이 훅으로 빌드된다.

- 따라서, 커스텀 훅은 명명 규칙을 따라야 한다.

1. React 컴포넌트 이름은 StatusBar나 SaveButton과 같이 대문자로 시작해야 한다.

2. 훅의 이름은 useState(빌트인)이나 useOnlineStatus(커스텀) 처럼 use로 시작해야 하고, 다음의 첫글자는 대문자여야 한다.

 

- 컴포넌트 내부에 getColor() 함수 호출의 경우, use로 시작하지 않기 때문에, React state를 포함할 수 없다는 것을 확신할 수 있지만, useOnlieStatus()와 같은 함수 호출은 내부에 다른 훅에 대한 호출을 포함할 가능성이 높다!

 

* Should all functions called during rendering start with the use prefix?(렌더링 시에 호출되는 모든 함수에 use 접수사를 써야 하나요?)

- 함수가 훅을 호출하지 않는다면 use 접두사를 사용할 필요가 없다. 즉, 훅을 호출하는 함수에만 use 접두사를 붙이면 된다.


 

Custom Hooks let you share statefull logic, not state itself(커스텀 훅은 state 자체가 아닌 상태적인 로직(stateful logic)을 공유합니다)

- 앞선 예제에서 중복되는 로직을 분리하는 경우에, isOnline state 변수가 두 컴포넌트 간에 공유되는 것은 아니다.

function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

- 즉, 위의 코드처럼 중복을 제거하기 전처럼 두 컴포넌트가 갖고 있는 isOnline state는 독립적인 state 변수 및 Effect이다.

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

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

// App.js
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>
    </>
  );
}

- 위의 예제는 Form 컴포넌트에서 중복되는 로직을 useFormInput 훅으로 분리하는 로직이다.

- 위 예제에서 볼 수 있듯이, useFormInput 훅에는 value라는 state 변수를 하나만 선언하고 있다.

- 하지만, Form 컴포넌트는 useFormInput를 두 번 호출하는데, 이는 바로 두 개의 state 변수를 선언하는 것처럼 작동하는 이유다.

 

- 즉, 커스텀 훅을 사용하면 상태 로직(stateful logic)은 공유할 수 있지만 state 자체는 공유할 수 없다. 각 훅 호출은 동일한 훅에 대한 다른 모든 호출과 완전히 독립적이기 때문이다.


Passing reactive values between Hooks(훅 사이에 반응형 값 전달하기)

// ChatRoom.js
import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

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

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

// useChatRoom.js
import { useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

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


// App.js
import { useState } from 'react';
import ChatRoom from './ChatRoom.js';

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
      />
    </>
  );
}

-  위 예제를 보면, ChatRoom 컴포넌트가 다시 렌더링할 때마다 최신 roomId와 serverUrl을 Hook(useChatRoom)에 전달하고 있다.


Passing event handlers to custom Hooks(커스텀훅에게 이벤트 핸들러 전달하기)

- 앞선 예제에서는 useChatRoom 훅에서 Effect 내부에 로직이 하드코딩 되어있는데, 만약, useChatRoom 컴포넌트를 호출하는 곳에서 로직을 결정하고 싶은 경우에는 useChatRoom 훅에서 함수를 매개변수로 받을 수 있다.

- 하지만, 이때 전달받은 함수를 Effect의 의존성에 추가하게 되면, 컴포넌트가 렌더링 될 때마다 채팅이 다시 연결되는 문제가 발생한다.

- 따라서, 전달받은 이벤트 핸들러를 Effect Event로 감싸 의존성에서 제거하는 방법을 사용해야 한다.

// 이벤트 핸들러 함수를 의존성에 추가하는 경우
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
                                             // ✅ 모든 의존성이 선언됨
}



// 이벤트 핸들러 함수를 의존성에서 제외하는 경우, uesEffectEvent 사용
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
                           // ✅ 모든 의존성이 선언됨
}

When to use custom Hooks(언제 커스텀 훅을 사용할 것인가)

- 중복되는 모든 코드에 대해 커스텀 훅을 추출할 필요는 없지만, Effect를 작성한다면 외부 시스템과 동기화하거나 React에 빌트인 API가 없는 작업을 수행하기 위해 "React 외부로 나가야 한다는" 뜻이기 때문에 Effect를 커스텀 훅으로 감싸면 의도와 데이터 흐름 방식을 정확하게 전달할 수 있다.

 

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

- 커스텀 훅의 이름은 코드를 자주 작성하지 않는 사람이라도 커스텀 훅이 무엇을 하고, 무엇을 취하고, 무엇을 반환하는 지 짐작할 수 있을 정도로 명확해야 한다.

ex) useData(url), useImpressionLog(eventName, extraData), useChatRoom(options)

- 외부 시스템과 동기화할 때 커스텀 훅의 이름은 좀 더 기술적이고 해당 시스템과 관련된 전문 용어를 사용할 수 있다.

ex) useMediaQuery(query), useSocket(url), useIntersectionObserver(ref, options)

- 커스텀 훅을 useEffect API 자체에 대한 대안 및 편의 래퍼 역할을 하는 커스텀 생명주기 훅을 생성하거나 사용하지 말아야 한다.

ex) useMount(fn), useEffectOnce(fn), useUpdataEffect(fn)

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

  // 🔴 Avoid: using custom "lifecycle" Hooks
  // 🔴 이러지 마세요: 커스텀 "생명주기" 훅 사용
  useMount(() => {
    const connection = createConnection({ roomId, serverUrl });
    connection.connect();

    post('/analytics/event', { eventName: 'visit_chat' });
  });
  // ...
}

// 🔴 Avoid: creating custom "lifecycle" Hooks
// 🔴 이러지 마세요: 커스텀 "라이브사이클" 훅 생성
function useMount(fn) {
  useEffect(() => {
    fn();
  }, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
          // 🔴 React 훅 useEffect에 의존성 누락: 'fn'
}

- 위 예제에서처럼 일부 코드가 "마운트 할 때"에만 실행되는 커스텀 훅을 사용하는 것을 지양해야 한다.

 

- 좋은 커스텀 훅은 호출 코드가 수행하는 작업을 제한하여 보다 선언적으로 만든다.


Custom Hooks help you migrate to better patterns(커스텀 훅은 더 나은 패턴으로 마이그레이션하는데 도움을 줍니다.)


There is more than one way to do it(여러가지 방법이 있습니다)

- 세컨핸드 수정 부분 같이 넣기

 


* 참고 : React 공식문서(https://react-ko.dev/learn)