ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React.js] Custom Hooks (Rules of Hooks)
    Front-End(Web)/React - 프레임워크(React, Next) 2021. 3. 11. 05:09
    반응형

    이전에, 미니 프로젝트나 다른 함수형 코드들을 참고하다보면, useOOOO 형태의 몰랐던 Hooks 들이 가끔 보일 때가 있다.

    이는, React에서 제공하는 Hooks API가 아닌, 개발자가 임의로 만든 Custom Hook 이라는 사실을 알게 되었다.

     

    이 Custom Hook 의 개념과 의의(주는 반복의 최소화), 각종 사례를 공부하고, 3차 프로젝트가 스프린트보다 빨리 끝나게 되어 일부 반복기능을 커스텀 훅으로 수정해보려고 한다!


    📒 Custom Hooks 소개

    - Custom Hooks 를 왜 사용하는가? 

    React의 Hooks 개념은 16.8버전(2019년 초) 에서 새로 추가되었다. 함수형 컴포넌트가 클래스형을 대체하도록 많은 기능을 지원하기 위해 등장했으며 아래와 같은 장점들을 뽐내며 소위 '훅의 시대' 가 현재진행형에 있다.

    • 클래스형 컴포넌트에 비해 적은 양의 코드로 동일한 로직을 구현 가능
    • 코드량은 적으나 시인성(명료함) 면에서 장점이 있다. (useState, useInputs ... )
    • 상태관리 로직의 재활용이 가능하다.

    커스텀 훅은 위의 세번째 장점과 연관성이 깊다. 컴포넌트를 만들다보면, 반복되는 로직이 자주 발생한다.

    (당장 떠오르는 것만 해도, input값 관리를 '계산된 변수명' 으로 통일하거나, checked 유무를 관리하는 등)

     

    이렇게 반복되는 로직들을, React Hooks API를 통해 하나의 함수로 분할 & 재사용하는 것이 'Custom Hooks' 이다.

    그렇기에, Custom Hook은 기능보다는 컨벤션(convention)에 가깝게 해석하는 경향이 많다.

     

    - Custom Hooks 제작방법

    커스텀 훅의 제작방법은 생각보다 심플하다.

    • src 폴더 > hooks 폴더 생성 후, 커스텀 훅 파일을 추가한다. 파일명(훅네임)은 통상 use +"키워드" 형태로 설정한다. 
    • 훅 내에선, React Hooks API(useState, useEffect, useCallback, useRef 등) 를 사용하여 원하는 기능을 구현하고 컴포넌트에서 사용하고자 하는 값을 반환하면 된다.
    • Rules of Hooks 규칙들을 준수해야 한다. (그렇지 않으면, 예측못한 동작이 발생할 수 있으며 디버깅이 난해해진다.)

     

     

    📒 Rules of Hooks

    위에서 언급했듯, React Hooks 의 사용 및 커스텀에 있어 몇 가지 규칙이 존재한다. 공식문서 소스를 참고해보자.

     

    - Hook 사용 규칙

    Hook은 그냥 JavaScript 함수이지만, 두 가지 규칙을 준수해야 한다.

     

    1) 최상위(at the Top Level)에서만 Hook을 호출해야 합니다

     

    반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 마세요. 대신 early return이 실행되기 전에 항상 React 함수의 최상위(at the top level)에서 Hook을 호출해야 합니다. 이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다. 이러한 점은 React가 useState  useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다. 

     

    2) 오직 React 함수 내에서 Hook을 호출해야 합니다

     

    Hook을 일반적인 JavaScript 함수에서 호출하지 마세요. 대신 아래와 같이 호출할 수 있습니다.

    • ✅ React 함수 컴포넌트에서 Hook을 호출하세요.

    • ✅ Custom Hook에서 Hook을 호출하세요.

    * 링크 : React 공식문서(한글), ko.reactjs.org/docs/hooks-rules.html


    📒 Custom Hooks 만들기

    - 예제 : useFriendStatus

    공식문서의 예제이다. 아래, FriendStatus()는 친구의 온라인 상태를 메세지로, FriendListItem()은 온라인 사용자 이름을 초록색으로 표시하는 각각의 컴포넌트들이다. 여기서 반복되는 useState, useEffect Hooks의 로직을 Custom Hook으로 제작하려고 한다.

    // FriendStatus.js
    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';
    }
    // FriendListItem.js
    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>
      );
    }

     

    1) Custom Hooks 추출하기

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

    먼저, 두 컴포넌트에서 반복되는 로직을 별도의 함수로 추출했다. 이 함수가 곧 Custom Hook 이 되는 것이다.

    일반함수와 동일하게 입력인자와 반환값을 설정하고, Hooks API 들로 제작해준다.

    단, 이름은 use + "키워드" 를 준수한다. 그래야, 이를 Hooks로 인식하며, Rules of Hooks 를 따르는지 확인해주기 때문이다.

     

    2) Custom Hooks 적용하기

    // FriendStatus.js
    function FriendStatus(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }
    
    // FriendListItem.js
    function FriendListItem(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      return (
        <li style={{ color: isOnline ? 'green' : 'black' }}>
          {props.friend.name}
        </li>
      );
    }

    이처럼 코드가 본질적으로 바뀌진 않았다. 다만, React Hooks 들로 반복되던 로직을 하나의 함수로 추출했을 뿐이다.

    중요한 부분은, 위 isOnline 이라는 상태가 공유되지 않는다는 점이다. (Redux, 디스패치 등과는 완전히 다른 개념인 것!)

    우리가 useState, useEffect 를 여러번 부를수 있듯, 커스텀 훅도 각각의 컴포넌트에서 독립적으로 작동하는 것이다.

     

    * Tip #1 : Hook에서 Hook으로 정보 전달하기

    공식문서 예제에서 보여주는 Hooks 간의 정보공유 예시이다. 

    요지는, recipientID 상태가 useFriendStatus 의 isOnline 상태관리와 연동되어, <Circle> 색깔변동이 실시간으로 일어난다는 것이다.

    const friendList = [
      { id: 1, name: 'Phoebe' },
      { id: 2, name: 'Rachel' },
      { id: 3, name: 'Ross' },
    ];
    
    function ChatRecipientPicker() {
      const [recipientID, setRecipientID] = useState(1);
      const isRecipientOnline = useFriendStatus(recipientID);
    
      return (
        <>
          <Circle color={isRecipientOnline ? 'green' : 'red'} />
          <select
            value={recipientID}
            onChange={e => setRecipientID(Number(e.target.value))}
          >
            {friendList.map(friend => (
              <option key={friend.id} value={friend.id}>
                {friend.name}
              </option>
            ))}
          </select>
        </>
      );
    }
    

     

    * Tip #2 : useYourImagination()

    커스텀 훅은 로직공유의 유연성을 제공한다. (<form> 태그제어, 애니메이션, 타이머 등)

    다만, 공식문서는 너무 성급한 로직추출을 권장하지 않는다. 즉, 용도에 따라 선택을 적절하게 해야하는 것이다.

    (목적이 로직공유라면 타당하나, 상태공유라면 커스텀 훅보다는 Redux Reducer 혹은 useReducer Hooks 가 더 나은 것처럼 말이다.)

     

     

     

    - useInput

    const useInput = (initialValue) => {
      const [value, setValue] = useState(initialValue);
      return { value };
    }

    먼저, useInput 은 initialValue(최초 입력값)을 인자로 받는다. 또, 이를 State Hooks 로 업데이트한 값을 반환해야 한다.

     

    const useInput = (initialValue) => {
      const [value, setValue] = useState(initialValue);
      const onChange = (event) => {
        const { target: { value } } = event;
      }
      setValue(value);
      return { value, onChange };
    }

    이제, setValue() 함수를 통해 변환된 값으로 최신화한 value와, 이를 처리하는 onChange 함수를 만들어서 반환했다.

     

    const App = () => {
      const name = useInput("Mr.");
      return (
        <div className="App">
          <input type="text" {...name} />
        </div>
      )
    };

    전개구문을 통해 input에 적용한 경우이다. useInput이 반환하는 value, onChange 가 각각 <input> 태그에 전달될 것이다.

     

    // useInput.js
    const useInput = (initialValue, validator) => {		// 1) useInput 인자에 validator 함수추가
      const [value, setValue] = useState(initialValue);
      const onChange = (event) => {
        const { target: { value } } = event;
      }
      // 2) useInput에 Validator 조건문 추가
      let willUpdate = true;
      if (typeof validator === "function") {
        willUpdate = validator(value);
      }
      if (willUpdate) {
        setValue(value);
      }
      return { value, onChange };
    }
    
    // App.js
    const App = () => {
      const maxLen = (value) => value.length < 10;
      const name = useInput("Mr.", maxLen);		// 3) useInput 사용시, validator 함수 입력
      return (
        <div className="App">
          <input type="text" {...name} />
        </div>
      )
    };

    Validation 로직을 추가로 반영한 결과이다. validator 함수를 두 번째 인자로 입력하고, 이것이 참일 경우에만 onChange가 진행된다.

    당연히, App.js 에서 사용할 때에도 validator 함수를 전달해줘야 한다.


    지금까지도 커스텀 훅 사용을 지양해오고 있었다. 개념을 정확히 모르기도 했었고, 그렇기에 거부감이 들었기 때문이다.

    하지만, 막상 개념을 짚고 나니 React Hooks API들의 반복로직을 함수화하는 단순한 과정이었을 뿐이었다.

     

    이번 3차 프로젝트에서도 문득 생각나는 부분이 있다. 바로 스크롤 이벤트이다!

    모든 페이지에서, 스크롤 상하방향에 따라 Navbar의 Fixed 제어 혹은 사라지거나 나타나는 조작을 해야했다.(translateY)

    또한, 리스트 페이지는 2개의 Navbar를 위처럼 제어해야 하므로, 이러한 반복에 대해 useScroll 커스텀 훅을 만들어봐야겠다!

     

    [출처]

    - React 공식문서(한글번역) : ko.reactjs.org/docs/hooks-custom.html  

    - React Velopert : react.vlpt.us/basic/21-custom-hook.html  

    - 신권철 님의 블로그 : medium.com/finda-tech/%ED%95%80%EB%8B%A4%EC%97%90%EC%84%9C-%EC%93%B0%EB%8A%94-react-custom-hooks-1a732ce949a5  

    - 김민석 님의 블로그 : velog.io/@choidy180/React-Hook-1-useInput

    반응형
Designed by Tistory.