Custom Hooks

hyena_lee·2023년 3월 23일
0

React

목록 보기
7/10
post-thumbnail

Custom Hooks

  • 개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있습니다.

  • 여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용합니다.

  • 이를 이용하면상태관리 로직의 재활용이 가능하고 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있습니다. (e.g. useSomething)

  • 예를 들어 이런 컴포넌트가 있다고 봅시다. 해당 컴포넌트는 실제 React 공식 문서에 있는 컴포넌트입니다.

  • react 공식문서에 있는 컴포넌트로 친구가 온라인인지 오프라인인지를 나타내는 메시지를 표시하는 채팅 애플리케이션의 구성요소이다.

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

//FriendListItem : 친구가 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={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

FriendStatus 컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem 컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트입니다. 이 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재하고 있습니다. 이 로직을 빼내서 두 컴포넌트에서 공유할 수는 없을까요? Custom Hook을 사용한다면 가능합니다.

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

두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만듭니다. 이렇게 Custom Hook을 정의할 때는 일종의 규칙이 필요합니다.

	- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙입니다.
	- 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킵니다.
	- Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 합니다. 즉 return 하는 값은 조건부여서는 안 됩니다. 그렇기 때문에 위의 이 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있습니다.

이렇게 만들어진 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성할 수 있습니다. 일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook 에서는 가능하다는 것 또한 알아두면 좋을 점입니다.

이제 이 useFriendStatus Hook을 두 컴포넌트에 적용해보겠습니다.

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={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • 로직을 분리해 Custom Hook으로 만들었기 때문에 두 컴포넌트는 더 직관적으로 확인이 가능해집니다.

  • 그러나 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아닙니다. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있습니다.

Custom Hook 만드는 법

Custom Hook의 예시

<[코드] 여러 url을 fetch할 때 쓸 수 있는 useFetch Hook>

const useFetch = ( initialUrl:string ) => { // useFetch 함수를 정의하고 initialUrl을 매개변수로 받는다.
	const [url, setUrl] = useState(initialUrl); // url과 setUrl을 useState를 이용해 초기값으로 initialUrl을 가진다.
	const [value, setValue] = useState(''); // value와 setValue를 useState를 이용해 초기값으로 빈 문자열을 가진다.

	const fetchData = () => axios.get(url).then(({data}) => setValue(data)); // fetchData 함수를 정의하고 axios를 이용해 url에서 데이터를 가져와서 setValue로 값을 설정한다.

	useEffect(() => { // useEffect를 이용해 렌더링 후 fetchData 함수를 실행한다.
		fetchData();
	},[url]);

	return [value]; // value를 반환한다.
};

export default useFetch; // useFetch 함수를 내보낸다.

<[코드] 여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks>

import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  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;

-useInput 라는 커스텀 룩을 만드는 함수

  • 입력 폼을 관리하기 위한 상태와 입력값이 변경될때 호출될 함수 그리고 초기값으로 폼을 리셋할 수있는 함수를 반환
  • initialForm이라는 초기 입력값을 받아들이는 함수를 선언한다.
  • useState hook을 사용하여 form이라는 상태(state)와 그 값을 업데이트하는 함수 setForm을 생성합니다. form의 초기값은 initialForm으로 설정된다.
  • seCallback hook을 사용하여 onChange 함수를 생성합니다.
  • onChange 함수는 이벤트 객체 e를 인자로 받아들이며, name과 value를 추출하여 form 상태를 업데이트합니다.
  • 이때, 이전 상태 값을 사용하여 새로운 객체를 생성하는 방식으로 업데이트하도록 합니다.
  • 두 번째 인자인 빈 배열([])은 의존성 배열이며, onChange 함수가 만들어지는 시점에서는 어떤 변수에도 의존하지 않기 때문에 빈 배열로 설정합니다.
  • useCallback hook을 사용하여 reset 함수를 생성합니다.
  • reset 함수는 setForm 함수를 사용하여 form 상태를 initialForm으로 초기화합니다.
  • 이때, initialForm이 변경되었을 때에만 reset 함수가 새로 생성되도록 의존성 배열에 initialForm을 지정합니다.
  • form, onChange, reset을 배열로 반환합니다. form은 현재 입력값들을 담고 있는 객체, onChange는 입력값이 변경될 때 호출될 함수, reset은 초기값으로 폼을 리셋할 때 호출될 함수입니다.
    -이 커스텀 훅을 사용하면, 입력 폼을 관리하는데 필요한 상태와 함수를 간단하게 구현할 수 있습니다.

실습

custom hook을 이용하여 useEffect 로직 분리하기

fetch/util/useFetch.js

import { useState, useEffect } from 'react'

const useFetch = (initialUrl) => {
  const [url, setUrl] = useState(initialUrl);
  const [value, setValue] = useState('');

  const fetchData = () => //axios.get(url).then(({ data }) => setValue(data));
    fetch('data.json', {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json"
      }
    })
      .then((response) => {
        return response.json();
      })
      .then((myJson) => {
        setValue(myJson);
      })

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

  return [value];
};
  • 이 Hook은 initialUrl 매개 변수를 받아 초기 URL 값을 설정하고, 그 URL에서 데이터를 가져옵니다.

  • 이 때, useState Hook을 사용하여 URL 및 데이터 값을 유지하고, useEffect Hook을 사용하여 URL 값이 변경될 때마다 데이터를 가져옵니다.

  • fetchData 함수는 fetch API를 사용하여 데이터를 가져옵니다.

  • 이 코드에서는 로컬 JSON 파일을 가져오는 예시로 작성되어 있습니다.

  • 이를 수정하여 실제 API의 주소로 변경할 수 있습니다. 이때, setValue 함수를 호출하여 데이터 값을 설정합니다.

-마지막으로, useEffect Hook에서 fetchData 함수를 호출하여 URL 값이 변경될 때마다 데이터를 가져오도록 설정합니다. return 문에서는 데이터 값을 반환합니다.

CustomFetchExcercise.js

import { useEffect, useState } from "react";
import './fetch.css';
import useFetch from "./util/useFetch";

const CustomFetchExcercise = () => {
  const [data] = useFetch('data.json')

  return (
    <div className="todo-wrap">
      <h1 className="todo-title">To do List</h1>
      <div className="todo-list">
        {data &&
          data.todo.map((el) => {
            return <li key={el.id}>{el.todo}</li>;
          })}
      </div>
    </div>
  );
}

export default CustomFetchExcercise;
  • 이 코드는 To-Do List를 화면에 렌더링하기 위해 데이터를 가져오는 컴포넌트입니다.

  • useFetch라는 커스텀 훅을 사용하여 data.json에서 데이터를 가져온다.

  • useFetch 훅은 초기 URL을 인자로 받고, 이 URL을 사용하여 데이터를 가져온다.

  • 그리고 useState를 사용하여 데이터를 저장한다.

  • useEffect를 사용하여 컴포넌트가 마운트될 때 fetchData() 함수를 실행한다.

  • 이 때, fetchData() 함수는 fetch() 메소드를 사용하여 data.json에서 데이터를 가져와 setValue() 함수를 통해 데이터를 저장합니다. 마지막으로, useFetch 훅은 데이터를 반환한다.

  • CustomFetchExcercise 컴포넌트는 useFetch 훅으로부터 반환된 data를 사용하여 To-Do List를 화면에 렌더링한다.

  • data가 존재하면, data.todo 배열을 순회하며 각각의 객체를 키로 사용하여 리스트 아이템을 생성한다.

  • 이 리스트 아이템은 el.todo 값으로 구성되어 있다.

input.jsx


function Input({ name, value, onChange }) {
  // TODO : input의 로직을 유의하며 컴포넌트로 분리합니다.

  return <input
    name={name}
    value={value}
    onChange={onChange}
    type='text'
  />
}

export default Input;
  • Input 컴포넌트는 name, value, onChange 속성을 받아 input 요소를 반환합니다.

  • name 속성은 input 요소의 name 속성으로 사용되고, value 속성은 input 요소의 현재 값을 설정합니다.

  • onChange 속성은 input 요소의 값이 변경될 때 실행되는 함수로, 상위 컴포넌트에서 onChange 함수를 전달하여 Input 컴포넌트를 제어할 수 있습니다.

  • 이 컴포넌트는 Input 요소의 로직을 분리하고, 재사용성을 높이기 위해 만들어졌습니다.

  • 이 컴포넌트를 사용하면 여러 곳에서 input 요소를 쉽게 생성하고 재사용할 수 있습니다.

CustomInputExcercise.js

import { useState } from "react";
import useInputs from "./util/useInput";
import Input from "./components/Input";
import shortid from "shortid";
import "./input.css";

const CustomInputExcercise = () => {
  //TODO : input에 들어가는 상태값 및 로직을 custom hook으로 구현합니다.
  //until 폴더에 useInput.js 파일이 만들어져 있습니다.
  const [form, onChange, reset] = useInputs({ first: '', last: '' })
  const [nameArr, setNameArr] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    let userName = {
      id: shortid.generate(),
      first: form.first,
      last: form.last
    }
    setNameArr([...nameArr, userName]);
    reset()
  };

  return (
    <div className="Input-wrap">
      <h1>Name List</h1>
      <div className="name-form">
        <form onSubmit={handleSubmit}>
          <div className="name-input">
            <label></label>
            <input
              name='first'
              value={form.first}
              onChange={(e) => onChange(e)}
              type="text"
            />
          </div>
          <div className="name-input">
            <label>이름</label>
            <input
              name='last'
              value={form.last}
              onChange={(e) => onChange(e)}
              type="text"
            />
          </div>
          <button>제출</button>
        </form>
      </div>
      <div className="name-list-wrap">
        <div className="name-list">
          {nameArr.map((el) => {
            return <p key={el.id}>{el.first} {el.last}</p>;
          })}
        </div>
      </div>
    </div>
  );
}

export default CustomInputExcercise;
  • 이 코드는 React로 작성된 이름을 입력받아 리스트에 추가하는 기능을 구현한 컴포넌트입니다.

  • useState Hook을 사용하여 nameArr 상태값을 생성하고, setNameArr 함수를 사용하여 상태값을 업데이트 합니다. - --form 상태값은 useInputs 커스텀 훅을 사용하여 생성하며, 이 커스텀 훅은 first와 last라는 두 개의 상태값을 갖고 있으며, 이 상태값들은 onChange 함수를 통해 업데이트 됩니다. reset 함수는 입력값을 초기화하는 함수입니다.

  • handleSubmit 함수는 폼을 제출할 때 실행되며, shortid 패키지를 사용하여 유일한 id 값을 생성한 후 nameArr 배열에 userName 객체를 추가하고, reset 함수를 호출하여 입력값을 초기화합니다.

  • 컴포넌트는 이름을 입력받는 폼과 입력한 이름을 출력하는 리스트로 구성됩니다.

  • 폼은 onSubmit 핸들러를 통해 handleSubmit 함수를 실행하며, 이름을 입력하는 input 요소는 form.first와 form.last 값을 사용하여 업데이트합니다.

  • 이름 리스트는 nameArr 배열을 .map() 메소드를 사용하여 출력합니다.

  • 각 요소에는 id, first, last 값을 사용하여 키를 부여합니다.

profile
실수를 두려워 말고 계속 도전 하는 개발자의 여정!

0개의 댓글