React 36장 - custom hooks
포스트
취소

React 36장 - custom hooks

React

Custom Hooks

  • 개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
  • 여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용한다.

    • 상태관리 로직의 재활용이 가능하고
    • 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며
    • 함수형으로 작성하기 때문에 보다 명료하다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    // FriendStatus: 친구가 online인지 offline인지 return하는 컴포넌트
    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";
    }
    
    // FruendListItem: 친구가 online일 때 초록색으로 표시하는 컴포넌트
    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=>
            {props.friend.name}
          </li>
        </>
      );
    }
    
  • FriendStatus 컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem 컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트이다.
  • 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재하고 있으며, custom hook을 이용하여 로직을 빼내서 두 컴포넌트에서 공유가 가능하다.
  • Custom hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
  • 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom hook을 위치 시킨다.
  • Custom hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉 return하는 값은 조건부여서는 안 된다. 그렇기 때문에 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    function useFriendStatus(friendID) {
      const [isOnline, setIsOnline] = useState(null);
    
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline);
        }
    
        ChatAPI.subscribeStatus(friendID, handleStatusChange);
        return () => {
          ChatAPI.subscribeStatus(friendID, handleStatusChange);
        };
      });
    
      return isOnline;
    }
    
  • 이렇게 만들어진 Custom Hook은 Hook 내부에 useState와 같은 리액트 내장 Hook을 사용하여 작성할 수 있다.
  • 일반 함수 내부에서는 리액트 내장 Hook을 불러 사용할 수 없지만 Custom Hook에서는 가능하다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    function FriendStatus(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      if (isOnline === null) {
        return "Loading...";
      }
      return isOnline ? "Online" : "Offline";
    }
    
    function FriendListItem(props) {
      const isOnline = useFriendStatus(props.friend.id);
    
      return (
        <>
          <li style=>
            {props.friend.name}
          </li>
        </>
      );
    }
    

예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const useFetch = (initialUrl) => {
  const [url, setUrl] = useState(initialUrl);
  const [value, setValue] = useState("");

  const fetchData = () => axios.get(url).then(({ data }) => setValue(data));

  useEffect(() => {
    fetchData();
  }, [url]);

  return [value];
};

export default useFetch;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState, useCallback } from "react";

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);

  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    setForm((form) => ({ ...form, [name]: value }));
  }, []);

  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;

실습

custom hook 실습해보기

  • 임의의 데이터로 작성한 public/data.json 파일에서 fetch를 받아오는 App.js가 있다.
  • useEffect를 custom hook으로 만들어 분리한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    // App.js
    import { useEffect, useState } from "react";
    
    export default function App() {
      const [data, setData] = useState();
    
      useEffect(() => {
        fetch("data.json", {
          header: {
            "Content-Type": "application/json",
            Accept: "application/json",
          },
        })
          .then((res) => {
            return response.json();
          })
          .then((myJson) => {
            setData(myJson);
          })
          .catch((err) => {
            console.log(err);
          });
      }, []);
    
      return (
        <>
          <div className="App">
            <h1>To do List</h1>
            <div className="todo-list">
              {data &&
                data.todo.map((el) => {
                  return <li key={el.id}>{el.todo}</li>;
                })}
            </div>
          </div>
        </>
      );
    }
    
  • fetch로 App.js에서 전달될 data.json을 받아온다.
  • 받아온 데이터를 data에 넣어준 후 받아온 data를 리턴하는 custom hook을 만든다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    // src/util/useEffectHook.js
    import { useEffect, useState } from "react";
    
    const useFetch = (fetchUrl) => {
      const [data, setData] = useState();
    
      useEffect(() => {
        fetch(fetchUrl, {
          headers: {
            "Content-Type": "application/json",
            Accept: "application/json",
          },
        })
          .then((response) => {
            return response.json();
          })
          .then((myJson) => {
            setData(myJson);
          })
          .catch((err) => {
            console.log(err);
          });
      }, [fetchUrl]);
    
      return data;
    };
    
    export default useFetch;
    
  • App.js에서 data를 받아온다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    import useFetch from "./util/useFetch";
    
    export default function App() {
      const data = useFetch("data.json");
    
      return (
        <>
          <div className="App">
            <h1>To do List</h1>
            <div className="todo-list">
              {data &&
                data.todo.map((el) => {
                  return <li key={el.id}>{el.todo}</li>;
                })}
            </div>
          </div>
        </>
      );
    }
    

실습 2

custom hook 실습해보기

  • 작성되어 있는 App.js에서 input 요소를 ./component/Input 컴포넌트와 custom hooks인 ./util/useInput으로 분리한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    import { useState } from "react";
    import useInput from "./util/useInput";
    import Input from "./component/Input";
    import "./styles.css";
    
    export default function App() {
      const [firstName, setFirstName] = useInput("");
      const [lastName, setLastName] = useInput("");
      const [nameArr, setNameArr] = useState([]);
    
      const handleSubmit = (e) => {
        e.preventDefault();
        setNameArr([...nameArr, `${firstNameValue} ${lastNameValue}`]);
        setFirstName("");
        setLastName("");
      };
    
      return (
        <>
          <div className="App">
            <h1>Name List</h1>
            <div className="name-form">
              <form onSubmit={handleSubmit}>
                <Input label={""} value={firstName} onChange={setFirstName} />
                <Input label={"이름"} value={lasttName} onChange={setLasttName} />
                <button>제출</button>
              </form>
            </div>
            <div className="name-list-wrap">
              <div className="name-list">
                {nameArr.map((el, idx) => {
                  return <p key={idx}>{el}</p>;
                })}
              </div>
            </div>
          </div>
        </>
      );
    }
    
  • Input 컴포넌트로 input 요소들을 분리한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    // Input.js
    const Input = ({ label, value, onChange }) => {
      return (
        <>
          <div className="name-input">
            <label>{label}</label>
            <input
              type="text"
              value={value}
              onChange={(e) => onChange(e.target.value)}
            />
          </div>
        </>
      );
    };
    
  • useInput으로 cunstom hooks를 작성해준다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    import {useState} from "react";
    
    function useInput(value){
      const [value, setValue] = useState(value);
    
      const handleCange = (newValue) = {
          setValue(newValue);
      }
    
      return [value, handleChange];
    }
    
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.