[React] Custom hook을 만들기 전에 고려해야 할 것들
Front-End/React

[React] Custom hook을 만들기 전에 고려해야 할 것들

 

 

 

 

들어가면서🚪

 

Building your own Hooks lets you extract component logic into reusable functions.

 

최근 내가 컴포넌트를 설계할 때 가장 중요하게 생각하는 것은 컴포넌트의 역할이 명확하게 나타나는가 이다. 예를 들어보자. app이라는 페이지에 input field가 있고 확인 버튼이 있다.

아마도 이런 화면..

 

이 화면의 코드는 대충 이런 식일 것이다.

function App() {
const [value, setValue] = useState('');

const onClick = () => {
    클릭 이벤트
};

    return (
        <div className='box'>
                <input placeholder='name' value={value} />
                <button onClick={onClick} />
        </div>
    );
}

 

app 컴포넌트의 역할은 'input field와 button을 적절한 위치에 그리는 것' 그리고 '확인 버튼을 누르면 input field 값을 넘기는 것'일 것이다. 하지만 실제로 컴포넌트를 구현하면 자잘한 로직이 app 컴포넌트에 추가된다.

 

 

만약 input field의 값을 10자리로 제한해야 한다면, onChange함수와 유효성 조건 등 자잘한 로직을 app컴포넌트 안에 구현해 주어야 한다.

 

function App() {
const [value, setValue] = useState('');

const onClick = () => {
    클릭 이벤트
};

const isValid = (value) => value.length <= 10 // 유효성 검사

const onChange = (e) => {
        const { value } = e.target;

    let willUpdate = true;
    willUpdate = isValid(value);

    if (willUpdate) {
      setValue(value);
    }
};


    return (
        <div className='box'>
                <input placeholder='Name' value={value} onChange={onChange}/>
                <button onClick={onClick} />
        </div>
    );
}

 

결과적으로 코드만 봤을때 app 컴포넌트는 훨씬 잡다한 역할을 하고 있게 된다. input field와 button을 적절한 위치에 그리는 것' 그리고 '확인 버튼을 누르면 input field 값을 넘기는 것' 그리고 'input값이 유효한지 확인하는 것'...

 

 

여기서 input field와 관련된 모든 로직을 custom hook으로 분리한다면, app컴포넌트는 다시 명확한 역할을 가지게 된다.


// custom hook으로 분리
const useInput = () => {
  const [value, setValue] = useState('');

  const isValid = (value) => value.length <= 10 // 유효성 검사

  const onChange = (e) => {
        const { value } = e.target;

    let willUpdate = true;
    willUpdate = isValid(value);

    if (willUpdate) {
      setValue(value);
    }
  };

  return { value, onChange };
};


function App() {
  const { value, onChange } = useInput();

  const onClick = () => {
    클릭 이벤트
   };

  return (
    <div className='box'>
      <input placeholder='Name' value={value} onChange={onChange} />
            <button onClick={onClick} />
    </div>
  );
}

export default App;

 

 

 

대개 input field를 사용하는 컴포넌트는 동일한 로직을 필요로 하기 때문에, 코드를 조금만 변경해줌으로써 custom hook을 재사용하도록 만들 수도 있다.

 

const useInput = (validator) => {
  const [value, setValue] = useState('');

  const onChange = (e) => {
    const { value } = e.target;

    let willUpdate = true;
    if (typeof validator === 'function') {
      willUpdate = validator(value);
    }
    if (willUpdate) {
      setValue(value);
    }
  };
  return { value, onChange };
};

function App() {
  const maxLen = (value) => value.length <= 10; // 상황에 따라 원하는 조건을 넣어준다
  const { value, onChange } = useInput(maxLen);

  return (
    <div className='box'>
      <input placeholder='Name' value={value} onChange={onChange} />
            <button onClick={onClick} />
    </div>
  );
}

export default App;

 

이렇게 custom hook은 컴포넌트 로직 자체를 분할하거나 재사용할 수 있게 해 준다.

 

 

 

 

 

custom hook의 장점

 

  1. 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있다.
  2. 코드 양이 적지만 명료함을 잃지 않는다.
  3. 상태 관리 로직의 재활용이 가능하다.

 

 

 

 

Hooks의 규칙

 

React 공식 문서에서는 Hooks 사용 시 두 가지 규칙을 준수해야 한다고 명시한다.

 

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

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 않아야 한다.
이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다. 이러한 점은 React가 useState와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해준다.

 

2. 오직 React 함수 내에서 Hook을 호출해야 한다.

이 규칙을 지키면 컴포넌트의 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있다.

 

 

 

 

 

 

Custom Hooks 생성 시 고려해야 할 점

 

이름이 ”use“로 시작하고, 안에서 다른 Hook을 호출한다면 그 함수를 custom Hook이라고 부를 수 있습니다

 

custom Hook을 만들 때에는 위에서 언급한 Hooks의 규칙에서 추가로 몇 가지 규칙을 더 고려해 주어야 한다. custom hooks를 잘못 정의하여 사용한다면 예측하지 못한 동작들을 만들어 수 있으며, 디버깅까지 어렵게 만들 수 있다.

 

 

 

리액트 개발자인 Dan Abramov는 custom Hooks 생성 시 이 두 가지를 고려해야 한다고 말했다.

 

 

 

 

 

합성

 

custom Hook은 동시에 사용할 수 있어야 한다.

 

더 자세하게 말하면, 하나의 컴포넌트에서 여러 개의 custom Hooks 사용 시 state 업데이트는 고유성을 유지해야 하며 서로에게 영향을 주어선 안된다.

 

하나의 컴포넌트 내에서 두 개의 useState를 사용하는 상황이 생겼다 가정해보자. useState를 호출하는 여러 개의 커스텀 Hooks는 충돌하지 않는다.

 function useMyCustomHook1() {
  const [value, setValue] = useState(0);
  // What happens here, stays here.
}

function useMyCustomHook2() {
  const [value, setValue] = useState(0);
  // What happens here, stays here.
}

function MyComponent() {
  useMyCustomHook1();
  useMyCustomHook2();
  // ...
}

 

여러 개의 state 중 하나가 바뀐다고 하더라도 다른 state들은 영향을 받을 일이 전혀 없다. 따라서 새로운 state 선언 시 같은 컴포넌트 내에 어떤 Hooks가 사용되었는지 따져볼 필요가 없다.

 

 

 

그렇다면 이 조건을 만족하지 못하는 경우는 언제일까?

 

React.memo 는 마지막 렌더링 시의 props와 현재 props를 비교해서 그 결과가 같다면 리렌더링을 하지 않는다. 이 기능을 하는 hooks가 있다고 가정해보자. 렌더링을 건너뛰는지 여부를 결정하기 때문에 이름은 useSkipRender이다.

 

 

function Button({ color }) {
  useSkipRender(prevColor => prevColor !== color, color);

  return (
    <button className={'button-' + color}>  
      OK
    </button>
  )
}

 

만약 이 useSkipRender를 두 개의 custom hook에서 각각 사용하고, 그 두 개의 custom hook이 한 컴포넌트에서 사용되었을 경우를 보자.

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useSkipRender(prevIsOnline => prevIsOnline !== isOnline, isOnline); // <-여기✨

  useEffect(() => {
    const handleStatusChange = status => setIsOnline(status.isOnline);
    ChatAPI.subscribe(friendID, handleStatusChange);
    return () => ChatAPI.unsubscribe(friendID, handleStatusChange);
  });

  return isOnline;
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useSkipRender(prevWidth => prevWidth !== width, width); // <-여기✨

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  });

  return width;

 

useWindowWidthwidth 가 바뀌었을 때만 리렌더링을 발생시키고

useFriendStatus는 prop로 받은 ID의 isOnline 값이 바뀌었을때만 리렌더링을 발생시킨다.

 

 

 

function ChatThread({ friendID, isTyping }) {
  const width = useWindowWidth();
  const isOnline = useFriendStatus(friendID);
  return (
    <ChatLayout width={width}>
      <FriendStatus isOnline={isOnline} />
      {isTyping && 'Typing...'}
    </ChatLayout>
  );
}

하지만 이 두 custom hooks를 한 컴포넌트에 넣어준다면, 해당 컴포넌트는 언제 렌더링이 되어야 할까?

 

useWindowWidth()가 유발하는 리렌더링은 useFriendStatus()에 의해 차단될 것이다. 그 반대도 마찬가지이다.

결국 useSkipRender 는 서로에게 영향을 주게 된다. 따라서 useWindowWidth()는 custom hooks로 적합하지 않다.

 

 

 

 

 

 

디버깅

 

custom hook이 코드의 인과관계를 파악하는 데 영향을 끼치지 않아야 한다.

 

 

useState를 사용한 예시를 보자.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  // ...
  return width;
}

function useTheme(isMobile) {
  // ...
}

function Comment() {
  const width = useWindowWidth();
  const isMobile = width < MOBILE_VIEWPORT;
  const theme = useTheme(isMobile);
  return (
    <section className={theme.comment}>
      {/* ... */}
    </section>
  );
}

 

만약 theme.comment 의 값이 잘못됐다고 가정한다면, Comment 컴포넌트 내부에 console.log() 를 사용하여 theme 의 값이 문제라는 것을 쉽게 파악할 수 있다. 그리고 useTheme 내부의 문제라는 것을 곧바로 추론할 수 있다. width 값이 이상하면 useWindowWidth 를 들여다보면 된다.

 

 

이렇게 useState 를 사용한 custom hook은 내부의 값이 어떤 상태인지 보는 것만으로 어떤 Hooks에서 문제가 발생했는지를 알아낼 수 있다.

 

 

 

 

useSkipRender 는 디버깅 조건을 만족하지 못한다.

 

function ChatThread({ friendID, isTyping }) {
  const width = useWindowWidth();
  const isOnline = useFriendStatus(friendID);
  return (
    <ChatLayout width={width}>
      <FriendStatus isOnline={isOnline} />
      {isTyping && 'Typing...'}
    </ChatLayout>
  );
}

 

만약 예상한 대로 컴포넌트가 렌더링이 되지 않는다면 우리는 그 원인을 찾기 위해 ChatThread 안의 모든 custom hook을 일일이 들여다보아야 한다. useSkipRender로 인하여 코드의 인과관계가 혼란이 오고 디버깅이 어려워지게 된다.

 

 

 

 

결국 이 두 조건이 말하고자 하는 바는 동일하다.

custom hook은 자신을 포함하여 다른 hook이나 component에 영향을 끼치지 않아야 한다는 점이다.

 


이 조건을 다른 말로 표현하면 다음과 같다.

 

custom hooks에서 공통된 값을 다루지 말아야 한다.

 

 

 

 

 

공통된 값을 다루는가?

 

useSkipRender는 리렌더링의 여부를 결정한다. 리렌더링의 여부는 여러 컴포넌트 간 공유하여 사용 가능하다. 공유할 수 있는 값을 사용하는 순간 다른 hook이나 component에 영향을 주게 되는 것이다.

 

Window.scroll() 을 사용하는 custom hook을 만들었다고 가정해보자.

 

 

<A>
  <B>
    <C/>
  </B>
</A>

function A(x) {
  useScrollTo(x);
  // ..
}

function C(y) {
  useScrollTo(y);
  // ..
}

이 custom hooks는 합성과 디버깅 조건을 모두 만족하지 못한다. 동일한 컴포넌트에 중복해서 사용 시 서로에게 영향을 주며, scroll 이슈가 발생하면 어디에서 문제가 생겼는지 원인을 바로 파악할 수 없다. 공통된 값을 사용했기 때문이다.

 

 

 

 

 

결론✨

 

custom hooks는 재사용이 가능하도록 로직을 분리하여 코드를 추상화시켜주는 동시에 컴포넌트의 간결함을 유지할 수 있게 해 준다. 하지만 무분별한 custom hooks 제작은 오히려 개발에 혼란을 가져오기 때문에 어떤 상황에서, 어떤 것을 custom hooks로 제작해야 할지를 잘 판단해서 만들어야 한다. 공통된 값을 다루는 로직을 custom hooks로 만드는 것은 예기치 못한 문제를 발생 시키고 이슈 발생시 그 원인을 파악하는 것도 힘들기 때문에 반드시 피해야 한다.

 

 

 

 

 

 

 

 

📌

https://overreacted.io/why-isnt-x-a-hook/

https://ko.reactjs.org/docs/hooks-custom.html

https://overreacted.io/ko/why-isnt-x-a-hook/