# 199 [udemy React 완벽 가이드 노트] custom-hooks의 구체적인 예시
import React, { useEffect, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
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-55f5b-default-rtdb.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);
};
useEffect(() => {
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;
import { useState } from 'react';
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://react-http-55f5b-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);
};
return (
<Section>
<TaskForm onEnterTask={enterTaskHandler} loading={isLoading} />
{error && <p>{error}</p>}
</Section>
);
};
export default NewTask;
이 두 가지에는 http 요청전송과 오류 처리 작업 등이 일부 중복되어있다.
App.js에는 본문과 헤더 없이 요청을 전송하고 있고,
받아온 응답에 적용되는 변환 로직도 있다.
NewTask에는 POST요청을 보내고,
body를 데이터에 추가하고 있고
응답에 대한 변환로직은 조금 다르다.
비슷한 로직을 재사용할 수 있게 만들면 좋겠다.
로직이 다른 리액트 훅이나 state를 사용하고 있고
그런 경우에는 정규함수로 변환이 불가하다
정규함수에서는 리액트 훅을 사용할 수 없기 때문이다.
커스텀 훅을 사용하기 가장 좋은 시점이다.
먼저 App.js에 관련한 로직을 커스텀 훅으로 만들어보자.
이름은 use-http.js (이름은 마음대로 적을 수 있다)
그러나 hooks의 이름은 use로 시작해야한다.
App.js의 아래 모든 것들을 그대로 가져와서 useState를 import하고 함수 이름을 조금 변경한다.
import { useState } from "react";
const useHttp = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = async taskText => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(
"https://react-http-.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);
};
}
그런데 이건 App.js에만 국한되어 있는 부분이 조금 있다.
request를 body와 url, method, headers 등이 재사용성을 불가하게 하므로
객체를 매개변수로 넣어서 그 안에는 개별적인 부분들을 넣어보도록 하자.
setTasks도 App에만 국한되어 있는 state이므로 삭제하자.
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,
body: JSON.stringify(requestConfig.body),
headers: requestConfig.headers,
});
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,
};
};
export default useHttp;
requestConfig에는 커스텀 훅을 사용하는 컴포넌트에서
url, method, body, headers 등을 넣을 수 있다.
그리고 가져온 데이터를 넣는 작업을 하는 아랫부분은 조금 변경하도록 하자.
data를 json에만 한정하도록 하고, 나머지 데이터를 처리하는 함수는
커스텀 훅을 사용하는 컴포넌트에서 받아오는 것으로 하자.
그걸 매개변수에서 받아와야 하므로 매개변수 부분에도 applyData라는 이름으로
매개변수를 하나 더 입력한다.
그리고 커스텀 훅에서 관리하고 있는 state를 객체로 return하도록 하자.
키와 밸류가 같다면 그냥 하나만 써도 된다.
App.js에서 커스텀 훅 사용해보기
이제 App.js에서 커스텀 훅을 사용하고 국지적인 부분을 인자로 넣어 호출하도록 하자.
그런데 문제점이 있다. 우리가 request를 하는데 body라든지 headers, method는 필요가 없다.
지금으로선 url만 필요한 것이다.
문제없이 사용하려면 커스텀훅에 인자로 모든것을 다 보내지 않아도
기본작동할 수 있게 커스텀 훅을 약간 변경하자
try {
const response = await fetch(requestConfig.url, {
method: requestConfig.method ? requestConfig.method : "GET",
body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
headers: requestConfig.headers ? requestConfig.headers : {},
});
이제 App.js에서 커스텀 훅을 사용한다.
function App() {
const [tasks, setTasks] = useState([]);
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.firebaseio.com/tasks.json" },
transformTasks
);
useEffect(() => {
fetchTasks();
}, []);
const taskAddHandler = task => {
setTasks(prevTasks => prevTasks.concat(task));
};
App.js에서 isLoading, error,sendRequest를 사용할 수 있으려면 이렇게 구조분해를 하여
작성하도록 하자. 그러면 커스텀 훅에서 관리하는 state들을 사용할 수 있게 되었다.
fetchTasks 무한루프
그 밑의 fetchTasks이름으로 사용하고 있는 sendRequest 함수는
dependencies 없이 useEffect안에 들어가있다.
그 안에 fetchTasks를 넣으려고 봤더니 무한루프가 된다.
이전에는 state update 함수만 호출하고 있었다.
state update함수는 react에 의해 절대 변경되지 않도록 보장되어있으므로
dependencies가 비어있어도 괜찮았다.
지금은 조금 다르다
sendRequest를 실행하면 커스텀 훅의 state가 설정된다.
그런데 이 state들이 App컴포넌트에서 사용하고 있기 때문에
state의 변경은 App컴포넌트의 재실행을 야기시킨다.
그러면 또 sendRequest가 새로운 함수로 생성되어
useEffect가 재실행 되고 또다시 state가 변경되고
App 컴포넌트가 재실행된다.
방법 1.
무한루프를 방지하려면 useCallback으로 sendRequest를 감싸면 된다.
지난번에 배웠던 것과 같이 함수를 저장한다.
const sendRequest = useCallback(async () => {
useCallback에도 dependencies도 필요하다.
내부에서 작성하지 않은 외부에서 들여온
requestConfig와 applyData를 dependencies로 넣어준다.
그런데 이 두개도 객체다.
그러면 useCallback을 사용해도 계속 실행이 될 것이다.
App.js로 가서 다시 useHttp를 사용할 때 넘겨주는 객체들이
재생성되지 않게 해보자.
const transformTasks = useCallback(tasksObj => {
이 안에는 dependencies가 빈 객체여도 된다.
useState밖에 들어있지 않기 때문이다.
또한 url이 들어가있는 객체는 useMemo로 저장하면 된다.
방법 2.
useHttp 커스텀 훅을 살짝 바꿔도 된다.
const useHttp = applyData => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const sendRequest = useCallback(
async requestConfig => {
setIsLoading(true);
setError(null);
useHttp에는 applyData만 전달해 주고,
url은 useEffect안에서 전달해 주도록 한다.
const { isLoading, error, sendRequest: fetchTasks } = useHttp(transformTasks);
useEffect(() => {
fetchTasks({
url: "https://react-http.firebaseio.com/tasks.json",
});
}, [fetchTasks]);
그러면 transformTasks에도 똑같이 적용해 볼 수 있다.
useCallback을 삭제하고,
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();
커스텀 훅을 아무런 인자 없이 사용하는 것이다
그리고 useEffect안에 transformTasks를 위치시키고 fetchTasks에 인자로 transformTasks를 전달해 준다.
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-.firebaseio.com/tasks.json",
},
transformTasks
);
}, [fetchTasks]);
그럼 이제 useHttp에는 아무런 인자가 들어가지 않고
sendRequest에 인자가 들어가게 되는것이다.
const sendRequest = useCallback(
async (requestConfig, applyData) => {
setIsLoading(true);
setError(null);
try {
이제 무한루프도 없고 커스텀 훅을 사용할 수 있게 되었다.
NewTask에서 커스텀훅 사용하기
1) useHttp 를 import한다.
2) useHttp를 NewTask 컴포넌트에서 호출한다.
(아무런 인자도 전달하지 않아도 된다. 위에서 리팩토링 했으므로..)
3) 객체구조분해를 통해서 isLoading과 error, sendRequest를 사용할 수 있게 한다.
const NewTask = props => {
const { isLoading, error, sendRequest: sendTaskRequest } = useHttp();
enterTaskHandler에서 sendTaskRequest를 호출되어야한다.
const enterTaskHandler = async taskText => {
sendTaskRequest(
{
url: "https://react-http-55f5b-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
body: { text: taskText },
headers: {
"Content-Type": "application/json",
},
},
);
};
sendTaskRequest에 url과 method, body, headers를 전달해 주도록 한다.
body는 JSON으로 변경하지 않아도 된다.
useHttp에서 JSON.stringify하고 있기 때문에.
그리고 createTask를 만들어 두번째 인자로 보내준다.
const createTask = taskData => {
const generatedId = taskData.name; // firebase-specific => "name" contains generated id
const createdTask = { id: generatedId, text: taskText };
props.onAddTask(createdTask);
};
const enterTaskHandler = async taskText => {
sendTaskRequest(
{
url: "https://react-http-55f5b-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
body: { text: taskText },
headers: {
"Content-Type": "application/json",
},
},
createTask
);
};
createTask는 response 받은 데이터를 사용하는 함수다
여기서는 useCallback같은 훅스를 사용할 필요가 없는데,
enterTaskHandler에서는 sendTaskRequest만 호출하고 있기 때문이다.
useEffect를 사용하고 있지 않기 때문이다.
여기서의 요청은 컴포넌트가 재평가되어도 전송되지 않는다.
폼이 제출 될 때만 함수가 실행될 것이다.
문제는 createdTask에서 taskText이다.
이것은 원래 enterTaskHandler를 통해 전달받았던 매개변수였다.
이걸 받아올 수 있게 하기 위해서는
방법1.
enterTaskHandler안에 createTask를 넣으면 된다.
const enterTaskHandler = async taskText => {
const createTask = taskData => {
const generatedId = taskData.name; // firebase-specific => "name" contains generated id
const createdTask = { id: generatedId, text: taskText };
props.onAddTask(createdTask);
};
sendTaskRequest(
{
url: "https://react-http-55f5b-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
body: { text: taskText },
headers: {
"Content-Type": "application/json",
},
},
createTask
);
};
이건 조금 깊은 중첩이 있는 방법이다.
방법2.
const createTask = (taskText, taskData) => {
const generatedId = taskData.name; // firebase-specific => "name" contains generated id
const createdTask = { id: generatedId, text: taskText };
props.onAddTask(createdTask);
};
const enterTaskHandler = async taskText => {
sendTaskRequest(
{
url: "https://react-http-55f5b-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
body: { text: taskText },
headers: {
"Content-Type": "application/json",
},
},
createTask
);
};
또다른 방법으로는 taskText를 매개변수로 가져오는 방법이다.
그러려면 useHttp 커스텀 훅에서 한 개의 인자만 전달하고 있기 때문에
bind 메서드를 이용한다.
bind메서드는 함수를 사전에 구성할 수 있게 만든다.
첫번째 인자는 실행이 예정된 함수에서
this 예약어를 사용하게 하는 것인데
지금은 필요 없으므로 null로 두고
두 번째 인자는 호출예정인 함수가 받는 첫 번째 인자가 된다.
taskText를 전달하면 두번째 인자에 taskText를 전달해서
form제출이 일어나면 그 때 taskText를 찾게 하면 된다.
이제 createTask의 나머지 인자인 taskData는
여전히 사전 설정된 것이므로 createTask.bind(null, taskText)에서 받는다.
함수가 실제로 호출되는 useHttp에서 전달되는 다른 인자의 경우는
간단하게 이 매개변수의 목록 끝에 추가하여 처리하면 되는 것이다.
그러므로 useHttp에서 applyData(data)에서의 data는
bind를 호출했으므로 createTask의 두 번째 인자로 추가된다.
const createTask = (taskText, taskData) => {
const generatedId = taskData.name; // firebase-specific => "name" contains generated id
const createdTask = { id: generatedId, text: taskText };
props.onAddTask(createdTask);
};
const enterTaskHandler = async taskText => {
sendTaskRequest(
{
url: "https://react-http-55f5b-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
body: { text: taskText },
headers: {
"Content-Type": "application/json",
},
},
createTask.bind(null, taskText)
);
};