오류 상태를 설정하고 오류를 다루는 부분 역시 커스텀 훅의 일부가 되어야 한다
그렇게 하면 이름도 좀 더 일반적이 되고 내부에 있는 로직을 좀 더 일반화할 수 있다.
이 훅은 어떤 종류의 요청이든 받아서 모든 종류의 URL로 보낼 수 있어야 하며
어떤 데이터 변환도 할 수 있어야한다.
동시에, 로딩과 오류라는 state를 관리하고 모든 과정을 동일한 순서대로 실행해야 한다.
그것이 우리가 원하는 재사용이다. 이런 유연한 훅을 만들기 위해서는 몇 개의 매개변수가 필요하다.
위 모두가 아웃소싱되어야 합니다
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;
URL과 메서드, header, body 등 모두 사용할 수 있는 유연성을 갖추어야함.
따라서, 여기에 requestConfig 매개변수가 필요하고 이는 URL을 포함해서 어떤 종류의 설정 사항도 포함할 수 있는 객체가 되어야 함.
현재 위의 코드는 const data 부분에서 오직 JSON만을 data로 받고 처리한다는 가정하에 만들어졌음 따로 parsing하진 않겠음
또한 모든 컴포넌트에서 자유롭게 쓰이도록 Fn을 따로 만들의 fetch의 결과값인 data를 유동적으로 처리할 수 있게 하였음
return값은 문자열, 배열, 객체등 대부분의 type으로 반환이 되며 key와 value값의 이름이 같다면 위와같이 key,value를 하나로 합칠 수 있음
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은 의존성 주입이 귀찮으므로 최대한 의존성 없이 깔끔하게 짜는 법을 보자
useHttps 훅에서 넘어올 data들을 선언해준다.
fetchTasks를 먼저 만들어준다. 그게 바로 사용자 지정 hook에 구현되어있다.
fetchTasks의 transformTasks는 유동성을 위하여 사용자 지정 hook에서 구현하도록 만들었고
이를 의존성 때문에 무한루프에 빠지는 것을 방지하고자 로직 전체를 useEffect 감싸준다.
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;
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;