React Custom Hook 빌드

맛없는콩두유·2022년 8월 29일
0
post-thumbnail

지난시간에 이어, Task를 추가하는 App을 만들어보겠습니다.

마찬가지로, 지난 시간에는 Movie로 fetch를 하였고, 이번에는 Tasks라는 이름으로 만들어 보겠습니다.

Custom Hooks을 만들어서, 재사용 가능한 함수에 상태를 설정하는 로직을
아웃소싱할 수 있습니다.

정규 함수와는 다르게, 커스텀 훅은 다른 커스텀 훅을 포함한 다른 리액트 훅을 사용할 수 있습니다. useState, useEffect 등을 통해 관리하는 리액트 상태를 활용할 수 있습니다.

Custom React 컴포넌트 생성

  • BackwordsCounter.js
import { useState, useEffect } from 'react';

import Card from './Card';

const BackwardCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter - 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Card>{counter}</Card>;
};

export default BackwardCounter;
  • ForwordsCounter.js
import { useState, useEffect } from 'react';

import Card from './Card';

const ForwardCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

두 컴포넌트는 상당히 많은 중복되는 Hook을 사용하고 있다.

  • use-counter.js
import { useState, useEffect } from 'react';

const useCounter = () => {
    const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);
};

export default useCounter;

use-counter.js를 만들어 먼저 forwordcounter.js의 훅 내용을 useCounter 안에 넣었다! 이 떄, 규칙이 있는데 이름을 use로 시작해야 이 것은 커스텀 훅을 의미한다는 것을 뜻한다.

사용자 정의 훅 사용하기

import Card from "./Card";

import useCounter from "../hooks/use-counter";

const ForwardCounter = () => {
  const counter = useCounter();

  return <Card>{counter}</Card>;
};

export default ForwardCounter;

usecounter 훅을 import 하여서 사용하려면 useState와 똑같이 사용하면 된다!

import { useState, useEffect } from "react";

const useCounter = () => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCounter((prevCounter) => prevCounter + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  ~~return counter~~;
};

export default useCounter;

주의할 점은 useCouter Hook에서 return 값으로 다른 컴포넌트에서 사용할 것을 무조건 return counte로 보내줘야 import한 컴포넌트에서 사용할 수 있다!

사용자 정의 훅 구성하기

Custom 훅에서도 마찬가지로 매개변수를 이용할 수 있습니다.

  • use-counter.js
const useCounter = (forwards = true) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      if (forwards) {
        setCounter((prevCounter) => prevCounter + 1);
      } else {
        setCounter((prevCounter) => prevCounter - 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [forwards]);

  return counter;
};
  • BackwordCounter.js
import Card from "./Card";

import useCounter from "../hooks/use-counter";

const BackwardCounter = () => {
  const counter = useCounter(false);

  return <Card>{counter}</Card>;
};

export default BackwardCounter;

덧셈 뺼셈을 이용하여 forwards 매개변수가 true일 떄와 false일 떄 setCounter를 다르게 하여 각각 컴포넌트가 다르게 동작하는 것도 가능합니다.


다른 예시

  • App.js
function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [tasks, setTasks] = useState([]);

  const fetchTasks = async (taskText) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(
        'https://react-http-6b4a6.firebaseio.com/tasks.json'
      );

      if (!response.ok) {
        throw new Error('Request failed!');
      }

      const data = await response.json();

      const loadedTasks = [];

      for (const taskKey in data) {
        loadedTasks.push({ id: taskKey, text: data[taskKey].text });
      }

      setTasks(loadedTasks);
    } catch (err) {
      setError(err.message || 'Something went wrong!');
    }
    setIsLoading(false);
  };
  • NewTask.js
 const enterTaskHandler = async (taskText) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
        {
          method: "POST",
          body: JSON.stringify({ text: taskText }),
          headers: {
            "Content-Type": "application/json",
          },
        }
      );

      if (!response.ok) {
        throw new Error("Request failed!");
      }

      const data = await response.json();

      const generatedId = data.name; // firebase-specific => "name" contains generated id
      const createdTask = { id: generatedId, text: taskText };

      props.onAddTask(createdTask);
    } catch (err) {
      setError(err.message || "Something went wrong!");
    }
    setIsLoading(false);
  };

두 컴포넌트의 fetch하는 부분이 중복되는 상황이여서 이 부분을 커스텀 훅을 통해 관리할 수 있다.

사용자 정의 Http 훅 빌드하기

  • use-http.js
import { useState } from "react";

const useHttp = (requestConfig, applyData) => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const sendRequest = async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(requestConfig.url, {
        method: requestConfig.method,
        headers: requestConfig.headers,
        body: JSON.stringify(requestConfig.body),
      });

      if (!response.ok) {
        throw new Error("Request failed!");
      }

      const data = await response.json();
      applyData(data);
    } catch (err) {
      setError(err.message || "Something went wrong!");
    }
    setIsLoading(false);
  };

  return {
    isLoading,
    error,
    sendRequest, //자바스크립트에서는 key value 값이 일치하면 하나만 적으면 된다.
  };
};

export default useHttp;

App.js에서 공통으로 처리할 부분을 추출하여 넣고, fetch 부분은 NewTask.js 부분과 다르게 동작을 하여야 하기 떄문에, 매개변수로 받아서 처리하고 applyData 매개변수를 활용하여 import 한 컴포넌트에 데이터를 전달하는 방식이다. 그리고 return 값으로 isLoading, error, sendRequest를 객체로 전달한다!

사용자 정의 Http 훅 사용하기

  • App.js는 GET요청만 하기 떄문에 fetch로 url만 전달하면 된다.
    따라서 use-http.js을 수정해야한다.
const sendRequest = async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(requestConfig.url, {
        method: requestConfig.method ? requestConfig.method : "GET",
        headers: requestConfig.headers ? requestConfig.headers : {},
        body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
      });

method: requestConfig.method ? requestConfig.method : "GET",
headers: requestConfig.headers ? requestConfig.headers : {},
body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,

확대한 부분을 바꿔주면 GET일 때와 POST방식일 떄 둘 다 쓸 수 있게된다.
transformTasks를 정의하여 보내준다.

  • App.js

use-http.js의 두 번 쨰 매개변수로 데이터를 받는 함수를 App.js에서 정의해야한다.

import useHttp from "./hooks/use-http";
  const transformTasks = (tasksObj) => {
    const loadedTasks = [];
    for (const taskKey in tasksObj) {
      loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text });
    }

    setTasks(loadedTasks);
  };
    const {
    isLoading,
    error,
    sendRequest: fetchTasks,
  } = useHttp(
    {
      url: "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
    },
    transformTasks
  );

use-http.js의 두 번 쨰 매개변수로 데이터를 받는 함수를 App.js에서 정의해야한다. 이렇게 정의하면 Firebase에서 받는 객체의 모든 작업은 프론트엔드에서 필요한 구조의 유형을 갖는 객체로 변환될 것이다.

  • use-http.js
 return {
    isLoading,
    error,
    sendRequest, //자바스크립트에서는 key value 값이 일치하면 하나만 적으면 된다.
  };

현재 App.js 에서는 isLoading/ error/ sendRequest 객첼르 반환하며
요청을 활설화 하려면 App.js에서 호출을 해야한다.

  • App.js
const {
    isLoading,
    error,
    sendRequest: fetchTasks,
  } = useHttp(
    {
      url: "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
    },
    transformTasks
  );

사용자 정의 훅 로직 조정하기

  • App.js
useEffect(() => {
    fetchTasks();
  }, [fetchTasks]);

의존성 배열로 fetchTasks를 추가해야한다. 하지만 여기서 문제가 생긴다.
재실행되고 재생성되는 무한루프로 빠지게 된다.

따라서 이를 피하려면 use-http.js에 useCallback으로 감싸야 한다.

  • use-http.js
const useHttp = (applyData) => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const sendRequest = useCallback(
    async (requestConfig) => {
      setIsLoading(true);
      setError(null);
      try {
        const response = await fetch(requestConfig.url, {
          method: requestConfig.method ? requestConfig.method : "GET",
          headers: requestConfig.headers ? requestConfig.headers : {},
          body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
        });

        if (!response.ok) {
          throw new Error("Request failed!");
        }
  • App.js
function App() {
  const [tasks, setTasks] = useState([]);

  const transformTasks = useCallback((tasksObj) => {
    const loadedTasks = [];
    for (const taskKey in tasksObj) {
      loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text });
    }

    setTasks(loadedTasks);
  }, []);

const { isLoading, error, sendRequest: fetchTasks } = useHttp(transformTasks);

  useEffect(() => {
    fetchTasks({
      url: "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
    });
  }, [fetchTasks]);
> 기존 매개변수로 requestConfig를 받았지만, requestConfig는 App.js에서 상태가 변경될 때 마다 재실행되고 재생성된다. 기서을 막기 위해 sendRequest의 useCallback 부분으로 매개변수를 옮겨서 App.js에 useEffect 부분에 fetchTasks에 매개변수로 url을 보내면 된다.

만약 App.js에서 useCallback을 사용하는 것이 복잡하다면,

  • App.js
function App() {
  const [tasks, setTasks] = useState([]);

  const { isLoading, error, sendRequest: fetchTasks } = useHttp();

  useEffect(() => {
    const transformTasks = (tasksObj) => {
      const loadedTasks = [];

      for (const taskKey in tasksObj) {
        loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text });
      }

      setTasks(loadedTasks);
    };

    fetchTasks(
      {
        url: "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
      },
      transformTasks
    );
  }, [fetchTasks]);

  const taskAddHandler = (task) => {
    setTasks((prevTasks) => prevTasks.concat(task));
  };

만약 App.js에서 useCallback을 사용하는 것이 복잡하다면, usecallback을 지우고 useEffect 구문 안에 넣고, useHttp의 매개변수 였던 transformTasks를 fetchTasks안에 넣으면 useHttp는 어떠한 매개변수 없이도 호출이 가능합니다.

  • use-http.js
const useHttp = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const sendRequest = useCallback(async (requestConfig, applyData) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(requestConfig.url, {
        method: requestConfig.method ? requestConfig.method : "GET",
        headers: requestConfig.headers ? requestConfig.headers : {},
        body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
      });

      if (!response.ok) {
        throw new Error("Request failed!");
      }

      const data = await response.json();
      applyData(data);
    } catch (err) {
      setError(err.message || "Something went wrong!");
    }
    setIsLoading(false);
  }, []);

기존 useHttp로 매개변수를 받던 것을 sendRequest의 useCallback으로 옮기고 더 이상 외부에서 의존성 배열이 없기 때문에 의존성 배열에서도 삭제하여 [] 빈배열로 초기화한다.

이것으로 App.js에서 커스텀 훅을 사용해보았고, NewTask.js에서 커스텀 훅이 남아있습니다.

더 많은 컴포넌트에서 사용자 정의 훅 사용하기

NewTask.js에서 커스텀 훅이 남아있습니다.

  const { isLoading, error, sendRequest: sendTaskRequest } = useHttp();

먼저 저번시간에 처럼 useHttp()에 usecallback 없이 객체를 생성해줍니다.

const createTask = (taskText, taskData) => {
    const generatedId = taskData.name; 
    // firebase-specific => "name" contains generated id
    const createdTask = { id: generatedId, text: taskText };

    props.onAddTask(createdTask);
  };

createTask는 use-http.js에서 두 번쨰 인자로 전해줄 data입니다.
하지만, const createdTask = { id: generatedId, text: taskText };에 taskText가 선언이 안되어있어서 taskText도 같이 createTask에 매개변수로 넣어줍니다.

const enterTaskHandler = async (taskText) => {
    sendTaskRequest(
      {
        url: "https://react-http-6b4a6.firebaseio.com/tasks.json",
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: { text: taskText },
      },
      createTask.bind(null, taskText)
    );
  };

그리고 sendTaskRequest는 enterTaskHandler가 실행될 때 호출되어져야 하므로 enterTaskHandler 문 안에 sendTaskRequest는의 url/method/heders/body 등을 적습니다. 그리고 두 번쨰 인자로 createTask를 보내줍니다. 하지만, use-http.js에서는 applyData(data)에 하나의 매개변수만 받습니다. 이를 해결하기 위해 bind() 메서드를 이용합니다. bind 메서드는 함수를 사전에 구성할 수 있게 해줍니다. 호출 즉시 함수가 실행되진 않습니다. 첫 번쨰 인자는 실행이 에정된 함수에서 this 예약어를 사용하게 하는 것인데 없으므로 null로 두고, 두 번쨰 인자는 호출 예정인 함수가 받는 첫 번쨰 인자가 됩니다.
즉 const createTask = (taskText, taskData)에서 taskText입니다.

저장 후 실행하면 똑같이 되는 것을 알 수 있습니다.

profile
하루하루 기록하기!

0개의 댓글