지난시간에 이어, Task를 추가하는 App을 만들어보겠습니다.
마찬가지로, 지난 시간에는 Movie로 fetch를 하였고, 이번에는 Tasks라는 이름으로 만들어 보겠습니다.
Custom Hooks을 만들어서, 재사용 가능한 함수에 상태를 설정하는 로직을
아웃소싱할 수 있습니다.
정규 함수와는 다르게, 커스텀 훅은 다른 커스텀 훅을 포함한 다른 리액트 훅을 사용할 수 있습니다. useState, useEffect 등을 통해 관리하는 리액트 상태를 활용할 수 있습니다.
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;
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을 사용하고 있다.
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 훅에서도 마찬가지로 매개변수를 이용할 수 있습니다.
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;
};
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를 다르게 하여 각각 컴포넌트가 다르게 동작하는 것도 가능합니다.
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);
};
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하는 부분이 중복되는 상황이여서 이 부분을 커스텀 훅을 통해 관리할 수 있다.
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를 객체로 전달한다!
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를 정의하여 보내준다.
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에서 받는 객체의 모든 작업은 프론트엔드에서 필요한 구조의 유형을 갖는 객체로 변환될 것이다.
return {
isLoading,
error,
sendRequest, //자바스크립트에서는 key value 값이 일치하면 하나만 적으면 된다.
};
현재 App.js 에서는 isLoading/ error/ sendRequest 객첼르 반환하며
요청을 활설화 하려면 App.js에서 호출을 해야한다.
const {
isLoading,
error,
sendRequest: fetchTasks,
} = useHttp(
{
url: "https://react-http-ecb71-default-rtdb.firebaseio.com/tasks.json",
},
transformTasks
);
useEffect(() => {
fetchTasks();
}, [fetchTasks]);
의존성 배열로 fetchTasks를 추가해야한다. 하지만 여기서 문제가 생긴다.
재실행되고 재생성되는 무한루프로 빠지게 된다.
따라서 이를 피하려면 use-http.js에 useCallback으로 감싸야 한다.
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!");
}
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을 사용하는 것이 복잡하다면,
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는 어떠한 매개변수 없이도 호출이 가능합니다.
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입니다.
저장 후 실행하면 똑같이 되는 것을 알 수 있습니다.