React.js Custom Hooks in Http (boiler)

강정우·2023년 1월 10일
0

react.js

목록 보기
27/45
post-thumbnail
post-custom-banner

http 요청을 위한 커스텀 훅 만들기

  • 우리가 http request와 관련된 훅을 만들 때 신경써야할 부분이 몇가지 있다.
  1. 오류 상태를 설정하고 오류를 다루는 부분 역시 커스텀 훅의 일부가 되어야 한다
    그렇게 하면 이름도 좀 더 일반적이 되고 내부에 있는 로직을 좀 더 일반화할 수 있다.

  2. 이 훅은 어떤 종류의 요청이든 받아서 모든 종류의 URL로 보낼 수 있어야 하며
    어떤 데이터 변환도 할 수 있어야한다.
    동시에, 로딩과 오류라는 state를 관리하고 모든 과정을 동일한 순서대로 실행해야 한다.
    그것이 우리가 원하는 재사용이다. 이런 유연한 훅을 만들기 위해서는 몇 개의 매개변수가 필요하다.

  3. 위 모두가 아웃소싱되어야 합니다

custom hook http boiler code

use-http.js

import { useState } from "react";

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

  const sendRequest = async (taskText) => {
    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();
      applyDataFn(data);

    } catch (err) {
      setError(err.message || 'Something went wrong!');
    }
    setIsLoading(false);
  };
  return{
    isLoading,
    error,
    sendRequest
  }
}

export default useHttp;
  1. URL과 메서드, header, body 등 모두 사용할 수 있는 유연성을 갖추어야함.
    따라서, 여기에 requestConfig 매개변수가 필요하고 이는 URL을 포함해서 어떤 종류의 설정 사항도 포함할 수 있는 객체가 되어야 함.

  2. 현재 위의 코드는 const data 부분에서 오직 JSON만을 data로 받고 처리한다는 가정하에 만들어졌음 따로 parsing하진 않겠음

  3. 또한 모든 컴포넌트에서 자유롭게 쓰이도록 Fn을 따로 만들의 fetch의 결과값인 data를 유동적으로 처리할 수 있게 하였음

  4. return값은 문자열, 배열, 객체등 대부분의 type으로 반환이 되며 key와 value값의 이름이 같다면 위와같이 key,value를 하나로 합칠 수 있음

App.js

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

import Tasks from './components/Tasks/Tasks';
import NewTask from './components/NewTask/NewTask';
import useHttp from './hooks/use-http';

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:요청url"}, transformTasks);
  }, [fetchTasks]);

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

  return (
    <React.Fragment>
      <NewTask onAddTask={taskAddHandler} />
      <Tasks
        items={tasks}
        loading={isLoading}
        error={error}
        onFetch={fetchTasks}
      />
    </React.Fragment>
  );
};

export default App;

여기서 fetchTasks를 호출하는데 그렇게 되면 커스텀 훅 안에 있는 sendRequest 함수를 실행하게 되고 몇몇 state가 설정이 되게 된다. state 설정이 발생하면 커스텀 훅을 사용하는 컴포넌트들이 재렌더링을 하게 되는데, 그 이유는 state를 설정하는 커스텀 훅을 만들고 컴포넌트의 훅을 사용하게 되면 컴포넌트는 묵시적으로 커스텀 훅이 설정한 state를 사용하게 되기 때문이다.
즉 커스텀 훅에서 구성된 state가 그 훅을 사용하는 컴포넌트에 설정되게 되는 것이다.

여기 sendRequet 함수에서 setIsLoading과 setError를 호출하면 이는 App 컴포넌트의 재평가를 유발하게 된다. 컴포넌트 안에 있는 커스텀 훅을 사용하는 것이기 때문이다. 그렇게 되면, 재평가가 되는 순간 커스텀 훅이 다시 호출되고 훅이 다시 호출되면 이 sendRequest 함수가 재생성되면서 새로운 함수 객체를 반환하고 useEffect가 재실행된다.

여기서 문제가 발생한다. sendRequest함수가 무한정으로 재생성된다는 것이다. 그래서 이를 막고자
useCallback을 사용한다. 하지만 useCallback은 의존성 주입이 귀찮으므로 최대한 의존성 없이 깔끔하게 짜는 법을 보자

  1. useHttps 훅에서 넘어올 data들을 선언해준다.

  2. fetchTasks를 먼저 만들어준다. 그게 바로 사용자 지정 hook에 구현되어있다.

  3. fetchTasks의 transformTasks는 유동성을 위하여 사용자 지정 hook에서 구현하도록 만들었고
    이를 의존성 때문에 무한루프에 빠지는 것을 방지하고자 로직 전체를 useEffect 감싸준다.

use-http.js

import { useState, useCallback } from "react";

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

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

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

      const data = await response.json();
      applyDataFn(data);

    } catch (err) {
      setError(err.message || 'Something went wrong!');
    }
    setIsLoading(false);
  }, []);

  return{
    isLoading,
    error,
    sendRequest
  }
}

export default useHttp;
  • 기존에 훅에게 전달해주어야할 2개의 변수를 내부에 블럭함수에 useCallback을 사용하여 전달해주었다.
    이렇게 해야 함수의 재생성이 안 되고 코드를 깔끔하게 유지할 수 있기 때문이다.
    의존성 까지 주입을 하지 않았으니 위 코드는 렌더링된 이후에는 절대 바뀌지 않는 코드인 것이다.

예시

import { useState } from 'react';
import useHttp from '../../hooks/use-http';

import Section from '../UI/Section';
import TaskForm from './TaskForm';

const NewTask = (props) => {

  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const enterTaskHandler = async (taskText) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(
        'https://reat-test-c0f23-default-rtdb.asia-southeast1.firebasedatabase.app/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);
  };

  return (
    <Section>
      <TaskForm onEnterTask={enterTaskHandler} loading={isLoading} />
      {error && <p>{error}</p>}
    </Section>
  );
};

export default NewTask;
import useHttp from '../../hooks/use-http';

import Section from '../UI/Section';
import TaskForm from './TaskForm';

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

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

    props.onAddTask(createdTask);
  }

  const enterTaskHandler = async (taskText) => {
    sendTaskRequest({
      url:'https://reat-test-c0f23-default-rtdb.asia-southeast1.firebasedatabase.app/tasks.json', 
      method:"POST",
      headers:{
      'Content-Type': 'application/json'},
      body:{text: taskText}
    }, createTask.bind(null, taskText));

  };

  return (
    <Section>
      <TaskForm onEnterTask={enterTaskHandler} loading={isLoading} />
      {error && <p>{error}</p>}
    </Section>
  );
};

export default NewTask;
  1. state는 여기서는 안 쓰니 날려준다.
  2. 사용자 지정 hook으로 초기화구문은 전부 날려준다.
  3. enterTaskHandler에 TaskForm 태그 로직을 걸쳐 나온 결과값을 인수로 받아 sendTaskRequest메서드를 실행해준다.
  4. 이는 useHttp에 의존하고있다. 그래서 여기코드에서는 2개의 매개변수를 넣어준다. 1개는 객체 1개는 함수
  5. 그래도 useHttp는 범용성을 늘리기 위해 함수는 선언을 하지 않았는데 이를 위의 createTask코드에서 살펴볼 수 있다.
  6. 이때 useHttp에는 변수가 1개인데 여기서는 bind함수를 사용하여 this값은 없니까 null을 주고 1번째 인수를 받아온 인수로 고정하여 처리하였다.
profile
智(지)! 德(덕)! 體(체)!
post-custom-banner

0개의 댓글