리액트 훅은 리액트 컴포넌트 함수 또는 커스텀 훅 내에서만 사용 가능하고 했는데,
커스텀 훅
은 왜 만들어야 하고 어떻게 만들 수 있을까?커스텀 훅도 내장 훅이나 useState 처럼 정규 함수
인데, 함수 내부에 상태를 설정할 수 있는 로직을 포함한 함수(Outsource stateful login into re-usable functions)이다. 즉, 커스텀 훅을 만들면 재사용가능한 함수에 상태를 설정하는 로직을 아웃소싱할 수 있다.
정규 함수와는 다르게, 커스텀 훅은 다른 커스텀 훅을 포함한 다른 리액트 훅을 사용할 수 있다.(Unlike, "Regular functions", custom hooks can use other React hooks and React state)
따라서 useState/useReducer를 통해 관리하는 리액트의 상태를 활용할 수 있다.
useEffect에도 접근할 수 있다.
커스텀 훅을 통해 다른 컴포넌트에서 사용할 수 있는 로직을 커스텀 훅으로 아웃소싱할 수 있고, 이를 통해 다양한 컴포넌트에서 호출이 가능하다.
즉, 로직 재사용이 가능한 매커니즘인 셈이다.
커스텀 훅 함수에서는 리액트 훅과 다른 훅을 사용할 수 있다.
이제 훅을 직접 만들어 보자.
/hooks 폴더 만들기
훅 폴더에 use-counter.js 파일 생성
파일 안의 함수 이름이 따라야 하는 규칙 때문에 이렇게 이름 정했다.
use-
로 훅 함수 생성
use-
: 리액트에게 이 함수가 커스텀 훅임을 알려주는 이름 규칙으로, 이 커스텀 훅을 리액트 내장 훅과 같은 방식으로 쓰겠다는 약속이다.
훅의 궁극적인 목적은 이 함수를 다른 파일에서 사용하는 것이다.
훅 안에 재사용하려는 로직을 추가하고, 상태 값을 리턴한다.
import { useState, useEffect } from "react";
const useCounter = () => {
//재사용하려는 로직 추가
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter((prevCounter) => prevCounter + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
//상태를 리턴해 주면 useCounter 훅을 호출했을 때 상태를 각각 사용할 수 있다.
return counter;
};
export default useCounter;
컴포넌트 안에서 이렇게 커스텀 훅을 호출했을 때 이 컴포넌트가 어떤 상태나 효과를 등록하면, 그 상태나 효과는 커스텀 훅을 사용하고 있는 커스텀 훅에 묶이게 된다.
따라서 이 컴포넌트에서 useCounter 훅을 호출하면, useCounter에서 만들어진 상태가 이 컴포넌트에 묶인다.
다수의 컴포넌트에서 커스텀 훅을 사용하게 되면 모든 컴포넌트가, 컴포넌트 전반에 걸쳐 상태나 효과를 공유하는 것이 아니라 각자의 상태를 받게 된다. 모든 컴포넌트에서 커스텀 훅이 재실행되고 해당하는 모든 컴포넌트 인스턴스가 각자의 상태를 받게 된다.
따라서 로직만 공유하는 것이고 상태를 공유하는 것은 아니다! 그래서 커스텀이다.
리액트 내장훅 사용할 때랑 똑같다. 커스텀 훅은 함수이므로 어떤 것이든 반환할 수 있고, 그 반환하는 것을 아래처럼 상수에 할당하여 사용하면 된다.
import useCounter from "../hooks/use-counter";
import Card from "./Card";
const ForwardCounter = () => {
//상수에 커스텀 훅 할당하여 사용
const counter = useCounter();
return <Card>{counter}</Card>;
};
export default ForwardCounter;
같은 로직인데 +1 씩 되는 카운터와 -1 씩되는 카운터가 있다면 커스텀 훅을 재사용하기 위해 로직을 조금만 수정해 주면 된다.
커스텀 훅도 함수이기 때문에 함수처럼 매개변수를 받을 수 있다.
현재 우리가 원하는 것은 카운터가 어떻게 증가하는지 제어하는 지표이므로, 갱신 함수 전체를 받아들여 작동을 유연하게 할수도 있다.
//커스텀 훅
import { useState, useEffect } from "react";
// 커스텀훅에 매개변수로 counterUpdateFn를 준다.
const useCounter = (counterUpdateFn) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
//상태 업데이트 함수에 보내는 것도 바꿔준다.
setCounter(counterUpdateFn);
}, 1000);
return () => clearInterval(interval);
}, []);
return counter;
};
export default useCounter;
//ForwardCounter 컴포넌트
import useCounter from "../hooks/use-counter";
import Card from "./Card";
const ForwardCounter = () => {
//커스텀 훅에 매개변수로 +1씩 해주는 함수를 보낸다.
const counter = useCounter((prevCounter) => prevCounter + 1);
return <Card>{counter}</Card>;
};
export default ForwardCounter;
//BackwardCounter 컴포넌트
import Card from "./Card";
import useCounter from "../hooks/use-counter";
const BackwardCounter = () => {
//커스텀 훅에 매개변수로 -1씩 해주는 함수를 보낸다.
const counter = useCounter((prevCounter) => prevCounter - 1);
return <Card>{counter}</Card>;
};
export default BackwardCounter;
//커스텀 훅
import { useState, useEffect } from "react";
// 커스텀훅에 매개변수로 불리언 플래그를 사용하고 조건에 따라 +,- 할 수도 있다.
// 기본 값으로 true로 설정해 준다.
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]);
//forwards는 useEffect 함수에서 정의된 것도 아니고, 커스텀 훅 외부에서 설정된 것도 아니다.
//매개변수로 받게 되는 값이기 때문에 의존성에 추가해야 한다.
//forwards 의존성이 변경될 때 마다 이펙트 함수가 재실행되는 것을 보장할 수 있다.
return counter;
};
export default useCounter;
//ForwardCounter 컴포넌트
import useCounter from "../hooks/use-counter";
import Card from "./Card";
const ForwardCounter = () => {
//기본 값이 true로 설정되어 있기 때문에 아무 것도 안 넣어도 된다.
const counter = useCounter();
return <Card>{counter}</Card>;
};
export default ForwardCounter;
//BackwardCounter 컴포넌트
import Card from "./Card";
import useCounter from "../hooks/use-counter";
const BackwardCounter = () => {
//불리언 값
const counter = useCounter(false);
return <Card>{counter}</Card>;
};
export default BackwardCounter;
http GET 요청하는 부분과 POST 요청하는 부분의 로직이 상당히 비슷하다.
다른 부분은 딱 두 가지로, fetch()로 보내는 인자 부분과, 응답받은 data를 구체적으로 활용하는 부분이다.
따라서 이 부분을 커스텀 훅으로 생성하여 재사용 가능하게 해보자.
이 훅은 어떤 종류의 요청이든 받아서 모든 종류의 URL로 보낼 수 있어야 하고 어떤 데이터 변환도 할 수 있어야 한다.
동시에 로딩과 오류 상태를 관리하고 모든 과정을 동일한 순서대로 실행해야 한다.
requestConfig
는 url을 포함하여 어떤 종류의 설정 사항도 포함할수 있는 객체가 되어야 한다.
따라서 fetch() 부분은 아래 처럼 작성하면 된다.
const response = await fetch(requestConfig.url, {
method: requestConfig.method,
body: JSON.stringify(requestConfig.body),
headers: requestConfig.headers,
});
데이터를 최종적으로 처리하는 부분은 구체적인 부분이기 때문에 훅에 포함되어서는 안된다.
대신 여기에 데이터를 가져오면 이 훅을 사용하는 컴포넌트로부터 얻은 함수를 실행하여 그 함수에 데이터를 넘기는 방법을 사용하자.
함수 안에서 무엇이 발생하는지는 커스텀 훅을 사용하는 컴포넌트에게 제공한다.
applyData(data);
커스텀 훅은 무엇이든 반환할 수 있다.
다른 컴포넌트에서 접근할 수 있도록 사용할 isLoading, error 상태와 sendRequest 함수를 객체에 담아 반환하자.
return {
isLoading,
error,
sendRequest,
};
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, {
//메소드를 설정하지 않으면 GET
method: requestConfig.method ? requestConfig.method : "GET",
//헤더 설정하지 않으면 빈 객체
headers: requestConfig.headers ? requestConfig.headers : {},
//바디 설정하지 않으면 null
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);
};
//커스텀 훅은 무엇이든 반환할 수 있다.
//다른 컴포넌트에서 접근할 수 있도록 사용할 것을 객체에 담아 반환
return {
isLoading,
error,
sendRequest,
};
};
export default useHttp;
import React, { useEffect, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
import useHttp from "./components/hooks/use-http";
function App() {
const [tasks, setTasks] = useState([]);
//원래 응답 data 처리하던 부분의 로직 새로 작성
//매개변수인 taskObj에 useHttp에서 응답된 data json이 인자로 들어가게 된다.
const transformTasks = (taskObj) => {
const loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
};
//커스텀 훅 사용
//구조분해 할당으로 사용할 거 꺼내오고
//보내야할 데이터는 보내기: 헤더나 바디는 필요 없음
const {
isLoading,
error,
//이렇게 다른 이름 부여할 수 있음
sendRequest: fetchTask,
} = useHttp(
{ url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json" },
transformTasks
);
//함수호출하여 요청 활성화하기
fetchTask();
useEffect(() => {
fetchTask();
}, []);
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;
App 컴포넌트가 재구축 되었다.
isLoading과 error의 상태에 접근할 수 있고, 이는 Tasks 컴포넌트에 있는 Tasks에 전달된다.
fetchTasks에도 접근 가능하지만 요청을 보내거나 오류 처리 등은 커스텀 훅의 일부가 되었다.
아, 그런데 의존성 배열에 fetchTasks를 추가해 줘야 한다.
자바스크립트에서 함수는 객체이다. 내부에 같은 로직을 가지고 있더라도 함수가 재생성되면 메모리에서는 새로운 객체로 인식되기 때문에 useEffect는 이를 새로운 값으로 받아들이므로 재실행을 유발한다.
무한루프를 탈출하기 위해 use-http 커스텀 훅에서 재생성을 막을 함수 sendRequest를 useCallback으로 감싸주면 된다.
import { useState, useCallback } from "react";
const useHttp = (requestConfig, applyData) => {
const [isLoading, setIsLoading ] = useState(false);
const [error, setError ] = useState(null);
//재생성 막을 함수 useCallback으로 감싸기
const sendRequest = useCallback(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,
});
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);
//의존성에 넣어줘야 함
}, [requestConfig, applyData]);
return {
isLoading,
error,
sendRequest,
};
};
export default useHttp;
의존성 배열에는 이 함수 안에서 사용하는 모든 것을 나열해야 하는데, 이 경우에는 requestConfig 객체
와
applyData 함수
이다.
그런데 의존성 배열에 이 둘을 추가하면 또 다른 문제가 발생한다. 😵
requestConfig 객체
와 applyData 함수
둘 모두 JS는 객체 취급한다.
App 컴포넌트에 이 객체와 함수가 전달되어 아래처럼 GET 요청을 보낼 때 사용한다.
// @ App.js
//...
const { isLoading, error, sendRequest: fetchTask } = useHttp(
{ url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json" },
transformTasks
);
따라서 이 객체들이 App이 재실행될 때마다 재생성되지 않도록 해야 한다.
그러기위해 또 useCallback을 사용하여 transformTasks 함수를 감싸줘야한다 ^^~ 아아ㅏ~~증말 ^ㅇ^ㅇ^^ㅇ
import React, { useEffect, useState } from "react";
import Tasks from "./components/Tasks/Tasks";
import NewTask from "./components/NewTask/NewTask";
import useHttp from "./components/hooks/use-http";
function App() {
const [tasks, setTasks] = useState([]);
//useCallback으로 감싸기
const transformTasks = useCallback((taskObj) => {
const loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
//상태 업데이트 함수인 setTasks 외에 어떤 것도 외부에서 사용하지 않으므로 의존성 배열에 추가할 것 없음
}, []);
const { isLoading, error, sendRequest: fetchTask } = useHttp(
{ url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json" },
transformTasks
);
fetchTask();
useEffect(() => {
fetchTask();
}, []);
const taskAddHandler = (task) => {
setTasks((prevTasks) => prevTasks.concat(task));
};
return (//...
);
};
이렇게 하면 transformTasks 함수가 변하지 않고 재생성되지 않게 할 수있다.
그러면 이제 남은 requestConfig 객체의 재생성을 막으려면 어떻게 해야 할까?
useMemo 등을 활용하여 이 객체가 변하지 않게 해야 한다.
아니면 그냥 커스텀 훅을 바꾸자 ^^!
requestConfig 객체를 훅에서 받지 않고 sendRequest에서 매개변수로 받는 것이다.
sendRequest에서 어쨌든 fetch하면서 requestConfig객체 내용을 사용하고 있으니, useHttp()가 아닌 sendRequest()에서 매개변수를 받자.
import { useState, useCallback } from "react";
//훅에 있던 매개변수 없애고 sendRequest 함수에 매개변수로 바로 받아오자고
const useHttp = (applyData) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
//useCallback으로 감싸서 무한 루프 방지
//sendRequest 함수에서 매개변수 받아오기
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!");
}
const data = await response.json();
applyData(data);
} catch (err) {
setError(err.message || "Something went wrong!");
}
setIsLoading(false);
// requestConfig는 이제 외부 의존성이 아닌, 래핑된 함수의 매개변수이므로
// 의존성 배열에 추가하지 않아도 된다.
}, [applyData]);
return {
isLoading,
error,
sendRequest,
};
};
export default useHttp;
이렇게 하면 requestConfig는 외부 의존성이 아닌 래핑된 함수의 매개변수가 되므로 의존성 배열에서 빼도 된다.
function App() {
const [tasks, setTasks] = useState([]);
//useCallback으로 감싸기
const transformTasks = useCallback((taskObj) => {
const loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
//상태 업데이트 함수인 setTasks 외에 어떤 것도 외부에서 사용하지 않으므로 의존성 배열에 추가할 것 없음
}, []);
//requestConfig 매개변수에서 뺐으니까 없애고
const { isLoading, error, sendRequest: fetchTask } = useHttp(
transformTasks
);
fetchTask();
//sendRequest인 fetchTask()에 바로 requestConfig 매개변수로 전달하기
useEffect(() => {
fetchTask({ url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json" });
}, []);
//...
transformTasks 함수도 useCallback으로 감싸기 귀찮다면 그냥 useEffect 함수에 넣어주자.
그러면 이펙트 함수에 외부 의존성은 사라진다.
function App() {
const [tasks, setTasks] = useState([]);
//매개변수 다 뺌
const { isLoading, error, sendRequest: fetchTask } = useHttp();
fetchTask();
//sendRequest인 fetchTask()에 바로 requestConfig와 transformTasks 인자로 전달하기
useEffect(() => {
const transformTasks = (taskObj) => {
const loadedTasks = [];
for (const taskKey in taskObj) {
loadedTasks.push({ id: taskKey, text: taskObj[taskKey].text });
}
setTasks(loadedTasks);
};
fetchTask({ url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json" },
transformTasks
);
}, []);
//...
요청에 대한 설정 및 데이터 전송 후 적용해야할 데이터 변환을 이펙트 함수에서 직접 보내게 되었기 때문에, 이제 useHttp()는 의존성이나 어떤 매개변수 없이도 호출이 가능하다.
transformTasks도 매개변수에서 빼버렸으니 use-http의 매개변수도 빼주자.
import { useState, useCallback } from "react";
//훅에 있던 매개 변수 다 뺌!
const useHttp = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
//useCallback으로 감싸서 무한 루프 방지
//sendRequest 함수에서 매개변수 모두 받아오기
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);
// requestConfig, applyData 모두 이제 외부 의존성이 아닌, 래핑된 함수의 매개변수이므로
// 의존성 배열에 추가하지 않아도 된다.
}, []);
return {
isLoading,
error,
sendRequest,
};
};
export default useHttp;
커스텀 훅이 다루는 모든 데이터는 useCallback으로 래핑된 sendRequest 함수에서 받고 있으니 외부 의존성이 더 이상 존재하지 않기 때문에 useCallback 의존성 배열은 비워두면 된다.
오케이!
이제 무한 루프 해결!
이 방법은 의존성 개수를 최소화하여 관리를 편하게 할 수 있는 방법이다.
다른 방법을 사용해도 상관 없음 ㅇㅇ!
이제 POST 요청을 보내는 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();
//🔥3-1. 먼저 함수 만들어서 바인딩 하장
//createTask 함수는 id, text 만들어서 할일 목록에 저장하는 함수
//applyData(data) 부분에서 data인 taskData를 매개변수로 받는다.
//(참고)
//이펙트 함수가 아닌 enterTaskHandler 에서만 sendTaskRequest를 호출하고 있기 때문에 useCallback 같은 건 호출할 필요 없다.
//여기서 POST 요청은 컴포넌트가 재평가되어도 전송되지 않는다. 폼이 제출될 때맏 함수가 실행된다.
//무한루프 같은 문제는 발생하지 않을 것이므로 useCallback 같은거 안써도 된다.
//App 컴포넌트에서는 이펙트함수 안에서 요청을 보내기 때문에 의존성배열에 추가하고 뭐 하다 보면 요청 전송 무한 루프에 빠질 수도 있지만 이 컴포넌트에서는 그럴일 없음 ㅇㅇ
//🔥🔥3-2. taskText 는 사용자가 폼에서 http 요청 통해 전달하려는 텍스트 부분이다.
//taskText는 enterTaskHandler 안에 위치해 있다.
//enterTaskHandler 안의 sendTaskRequest로 전달하는 createTask 함수 안에서 이를 사용하려면
//1. 함수를 가져와서 컴포넌트 함수 안의 중첩 함수 안에 있는 enterTaskHandler 안에서 정의 (JS 스코프에 따라 같이 작동 => tastText에 접근 가능, 하지만 너무 깊은 중첩 구조 ㅠㅠ)
//2. 1 같은 중첩 구조 피하기 위해 그냥 그대로 두고, taskText를 createTask의 매개변수로 받아서 사용하기 -> 🔥bind() 메소드 사용하여 이 함수로 taskText 바인딩 해주면 된다.
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) => {
//🔥1. sendRequest: sendTaskRequest가 호출되는 위치는 여기!
//왜냐하면 폼이 제출될 때 마다 enterTaskHandler가 트리거되는데, 그럴때 마다 http 요청이 호출되어야 하기 때문이다.
sendTaskRequest(
//🔥2. 첫 번째 인자: POST 요청하는 requestConfig
{
url: "https://react-http-35c4a-default-rtdb.firebaseio.com/tasks.json",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: { text: taskText },
},
//🔥3. 두 번째 인자: 응답 데이터인 data를 받아서 뭔가 하는 함수인 applyData(data)
createTask.bind(null, taskText)
//❗️3-3. taskText 받아와서 외부 함수인 createTask로 taskText를 바인딩 해주자.
//createTask.bind() 호출!
//그러면 bind 메소드는 함수를 사전에 구성할 수 있게 해준다. 호출 즉시 함수가 실행되지는 않는다.
);
};
return (
<Section>
<TaskForm onEnterTask={enterTaskHandler} loading={isLoading} />
{error && <p>{error}</p>}
</Section>
);
};
export default NewTask;
bind()
bind()
는 JS 기본 메서드로 어떤 함수에 대해서도 이를 사전 구성하기 위해 사용할 수 있다.첫 번재 인자
두 번째 인자
taskText
를 전달하여 제출된 폼에서 taskTest를 찾게 하면 된다.createTask의 나머지 인자인taskData
(응답 data)는 위에 createTask에서 미리 설정했기 때문에 bind 받는 이 위치에서는 자동으로 받는다.
함수가 실제로 호출되는 useHttp에서 전달되는 다른 인자인 applyData의 경우, 간단하게 이 매개변수의 목록 끝에 추가하여 처리하면 된다.
이렇게 중복되는 로직, 특히 상태 설정과 같은 로직들을 커스텀 훅으로 아웃소싱하여 재사용할 수 있다!
다른 컴포넌트들에 적용하여 다양한 종류의 요청을 보내고 응답 데이터에 대해서도 다양한 작업을 하고, 로딩 및 오류 상태를 공유 로직을 통해 최적화 할 수 있다.