[React] Deep Dive 모던 리액트(11) 사용자 정의 훅 & 고차 컴포넌트
FrontEnd/Deep Dive

[React] Deep Dive 모던 리액트(11) 사용자 정의 훅 & 고차 컴포넌트

728x90

 

중복 코드를 피해야 한다.

 

개발자라면 모두 공감하는 말이다. 중복코드는 당연히 존재만으로 비효율적일 뿐 아니라 유지보수도 어렵게 만든다. 고차 컴포넌트와 사용자 정의 훅을 사용하면 이런 문제를 해결할 수 있다.

 

 

사용자 정의 훅

 

HTTP요청을 하는 fetch를 기반으로한 사용자 정의 훅 예제를 보자.

 

import React, { useEffect, useState } from "react";

function useFetch<T>(
  url: string,
  { method, body }: { method: string; body?: XMLHttpRequestBodyInit }
) {
  const [result, setResult] = useState<T | undefined>();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [ok, setOk] = useState<boolean | undefined>();

  const [status, setStatus] = useState<number | undefined>();

  useEffect(() => {
    const abortCotroller = new AbortController();

    (async () => {
      setIsLoading(true);

      const response = await fetch(url, {
        method,
        body,
        signal: abortCotroller.signal,
      });

      setOk(response.ok);
      setStatus(response.status);

      if (response.ok) {
        const apiResult = await response.json();
        setResult(apiResult);
      }
      setIsLoading(false);
    })();
  }, [url, method, body]);

  return { ok, result, isLoading, status };
}

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const MyComponent = () => {
  const { isLoading, result, status, ok } = useFetch<Array<Todo>>(
    "https://jsonplaceholder.typicode.com/todos",
    { method: "GET" }
  );

  useEffect(() => {
    if (!isLoading) {
      console.log("fetchResult >>", status);
    }
  }, [status, isLoading]);

  return (
    <div>
      {ok
        ? (result || []).map(({ userId, title }, index) => (
            <div key={index}>
              <p>{userId}</p>
              <p>{title}</p>
            </div>
          ))
        : null}
    </div>
  );
};

/* STYLE */

export default MyComponent;

 

 

결과

 

 

이러한 사용자 정의 훅을 사용하지 않았다면 fetch로 API호출을 담당하는 경우마다 최소 4개의 state를 구현해야 한다. 만약 useReducer를 사용하도라도 useEffect를 활용해야 하기때문에 코드가 복잡해진다.

 

위와같이 복잡하고 반복된 훅을 사용자 정의 훅으로 만들면서 useFetch훅만 사용해도 api 사용이 쉽게 가능해졌다.

 

이름

훅에는 use라는 이름이 붙는다. 사용자 정의 훅은 내부에 리액트에서 제공하는 훅들을 조합하여 나만의 훅을 만드는 것이기 때문에 꼭 use를 앞에 붙여야 한다.

 

만약 이 훅의 규칙을 따맂 않는다면 에러를 발생한다.

 

 

 

고차 컴포넌트

 

 

고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.

 

고차 컴포넌트는 고차함수의 일종이기 때문에 사용자 정의 훅과는 달리 자바스크립트 환경에서 널리 사용할 수 있다. 리액트에서 가장 유명한 고차 컴포넌트는 React.memo이다.

 

 

아래 예시를 보면 ChildComponent의 값이 변하는게 없는데 부모 컴포넌트의 변화 때문에 불필요한 렌더링이 발생하고 있다.

 

import { useEffect, useState } from "react";

const ChildComponent = ({ value }: { value: string }) => {
  useEffect(() => {
    console.log("render !!!!");
  });

  return <>난 정민규~</>;
};

const MyComponent = () => {
  const [state, setState] = useState(1);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setState(+e.target.value);
  }

  return (
    <>
      <input type="number" value={state} onChange={handleChange} />
      <ChildComponent value="hello" />
    </>
  );
};

export default MyComponent;

 

 

불필요한 렌더링이 발생

 

 

아래처럼 memo를 사용하면 불필요한 렌더링이 발생하지 않게 된다.

import { memo, useEffect, useState } from "react";

const ChildComponent = memo(({ value }: { value: string }) => {
  useEffect(() => {
    console.log("render !!!!");
  });

  return <>난 정민규~</>;
});

const MyComponent = () => {
  const [state, setState] = useState(1);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setState(+e.target.value);
  }

  return (
    <>
      <input type="number" value={state} onChange={handleChange} />
      <ChildComponent value="hello" />
    </>
  );
};

export default MyComponent;

 

 

 

렌더링이 발생하지 않는다

 

 

useMemo를 사용해도 같은 작업을 할 수 있다.

다만 이 경우 값을 반환받기 때문에 JSX함수방식이 아닌 {} 를 사용한 할당식으로 받아야 하므로 목적과 용도가 뚜렷한 memo를 사용하는 것이 일반적인 경우엔 더 좋다.

 

 

 

고차 함수

고차함수란 함수를 인수로 받거나 결과로 반환하는 함수이다. 대표적인 고차함수는 Array.prototype.map 함수가 있다.

 

고차함수를 활용해서 고차 컴포넌트를 하나 만들어 보자. 사용자 인증 정보에 따라 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도 정의된 컴포넌트를 보여주는 예제이다.

 

 

import React, { ComponentType } from "react";

interface LoginProps {
  loginRequired?: boolean;
}

function withLoginComponent<T>(Component: ComponentType<T>) {
  return function (props: T & LoginProps) {
    const { loginRequired, ...restProps } = props;
    if (loginRequired) {
      return <div>로그인이 필요합니다.</div>;
    }
    return <Component {...(restProps as T)}></Component>;
  };
}

const Component = withLoginComponent((props: { value: string }) => {
  return <h3>{props.value}</h3>;
});

const MyComponent = () => {
  const isLogin = true;
  return <Component value="나는 민규" loginRequired={isLogin} />;
};

/* STYLE */

export default MyComponent;

 

 

 

이러한 고차 컴포넌트는 컴포넌트 전체를 반환하기 때문에 사용자 정의 훅보다 더 큰 영향력을 미치는 것이 가능하다. 고차 컴포넌트도 이름을 with로 시작하는 이름을 사용하는 것이 관습이다.

 

또한 고차 컴포넌트를 사용하는 경우 부수효과를 최소화 해야함을 생각해두자. 고차 컴포넌트는 컴포넌트를 인자로 받게 되는데 컴포넌트의 props를 임의로 수정,추가,삭제하는 일은 없어야 한다. 만약 고차컴포넌트에서 컴포넌트의 props를 임의로 변경하면 예측하지 못한 상황에서 props가 변경될 수 있고 이는 버그로 이어질 수 있다.

 

또한 고차컴포넌트를 과도하게 중첩하는 경우 오히려 컴포넌트의 복잡성이 증가할 수 있다.

 

 

사용자 정의 훅 vs 고차 컴포넌트

 

useEffect,useState와 같이 리액트에서 제공하는 훅만으로 공통 로직을 짤 수 있다면 사용자 정의 훅을 사용하는 것이 좋다. 훅 자체로는 렌더링에 영향을 미치지 않기 때문이다.

 

하지만 앞선 예제처럼 애플리케이션 관점에서 어떤 사용자가 컴포넌트에 접근할때 무언가 해야하는 경우에는 고차 컴포넌트가 조금 더 유리할 수 있다.

 

혹은 에러가 발생했을때 그에 대응하는 컴포넌트를 보여주는 경우 또한 고차 컴포넌트를 사용하는 것이 좋을 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90