프로젝트를 진행할 때 맛플레이스, 맛픽커 관련 마커 및 맛포스트 등의 데이터를 fetch 해오는 코드가 많을 것이라고 생각했고, 커스텀훅을 만들어 관리하기 용이하도록 분리해야한다고 논의가 이루어졌다.
서버에서 받아온 데이터를 프론트에서 관리하면서 결과적으로,
이러한 상태를 만들기 위해 이번 글에서 그 여정을 써본다.
import { useState, useEffect, useCallback } from "react";
import axios from "axios";
interface MemberData {
nickname: string;
email: string;
birthday: string;
profileImg: string;
gender: string;
memo: string;
createdAt: string;
modifiedAt: string;
followers: string;
followings: string;
postlist: Array<Post>;
picklist: Array<Pick>;
}
interface Post {
postId: number;
likes: number;
commentcount: number;
thumbnail_url: string;
}
interface Pick {
groupId: number;
name: string;
color: string;
}
interface UseAxiosReturn {
memberData: MemberData | null;
loading: boolean;
error: Error | null;
}
const useAxios = (url: string): UseAxiosReturn => {
const [memberData, setMemberData] = useState<MemberData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const axiosData = useCallback(async () => {
setLoading(true);
try {
const response = await axios.get<MemberData>(url);
setMemberData(response.data);
} catch (error) {
setError(Object.assign(new Error(), error));
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
axiosData();
}, [axiosData]);
return { memberData, loading, error };
};
export default useAxios;
전에 사용했던 커스텀훅을 가져와 이렇게 썼는데 문제가 생겼다.
Axios
) 분리 및 코드 따로 보관 Axios
코드가 컴포넌트 이곳 저곳에 흩어져있음 => 줄이기 위해 커스텀훅과 비슷하게 한 폴더에 관리useAxios
하나로 비동기 코드를 관리할 수 있어야 하는데 위 사진처럼 난잡하게 네트워크 통신 코드 및 커스텀훅 코드를 써버렸음.우리 커스텀훅을 보시고 이러한 fetch 훅을 많이 만들어 놨으니 참고하여 프로젝트에 맞게 만들어보라고 말씀하셨다. 되게 도움이 많이 됐다.
useAsync와 같은 추상화 방식이 사용되는 이유는 코드를 보면 아시겠지만 data fetching 시, 중복되는 반복 로직(status, fetching, etc...)을 한 데 모아 추상화 할 수 있다는 장점 때문입니다.
다만, hooks 내부에 data 상태를 api 호출을 통해서만 변경할 수 있기 때문에 멘토링때 다뤘던 MatListView와 같은 컴포넌트에서는 별도의 POST/PUT/DELETE api 호출 후에 다시 GET를 한번 호출해줘야 하는 상황이 생깁니다. 당연히 효율성만 봤을때는 최대한 api 호출을 적게 하고, 프론트에서 처리해주는 게 맞지만, 이 부분은 상황에 따라 POST/PUT/DELETE 후, 다시 GET 해도 큰 문제는 없을 것 같습니다. 보통 그렇게 많이 하기도 합니다.
그렇게 하지 않으려면 하나의 hooks에 GET/POST/PUT/DELETE/ 관련 함수를 전부 넣어 관리해야합니다. useUser라고 예를 들면 return {users, deleteUser, updateUser} 이런 식일텐데, 이 방식도 나쁘진 않지만 useAsync 내부 코드와 같은 중복 로직이 많이 생길 수 있습니다. (노가다가 많이 생깁니다.)
react 진영에 hooks가 생겨나면서 api(비동기)를 통해 가져온 상태 관리(server side state)에 대한 다양한 의견들이 많이 생겨났는데 그중 react-query, swr과 같은 data-fetching 전용 hooks 라이브러리들이 이런 것들을 쉽게 구현할 수 있게 여러 기능들을 제공해주고 있습니다. 인기도 많습니다. 편하기도 합니다. 이건 나중에 회사 가셔서 한번 다룰 기회가 있으면 좋을 것 같네요~
엔지니어링에 은탄환은 없으니 장/단점을 잘 살피셔서 개발에 가장 효율적인 방법을 가져가시면 될 것 같습니다. 기능/구조는 여러분들이 더 잘 아시니까요~
제가 useAsync를 말씀드린 이유는 react-hooks도 추상화를 시켜 사용한다는 점을 말씀드리고 싶었고,
아래 링크 보시면 여러 기본적인 hooks들에 대한 예제가 있는데, 이런 식으로 감싸서 많이 사용하는구나 정도로만 보시고, 추후에 필요하실 때 찾아서 클론 코딩해도 큰 도움이 될 것 같네요~~
https://usehooks.com/
https://swr.vercel.app/
https://react-query-v3.tanstack.com/
커스텀훅으로 추상화을 활용한 useAsync
류는 데이터 가져오기와 같은 중복되는 반복 로직을 모아 추상화할 수 있어 코드의 가독성을 높일 수 있다. 그러나 useAsync 내부 코드는 GET/POST/PUT/DELETE 함수를 모두 포함하지 않으므로 이러한 함수를 사용하려면 별도의 hooks를 만들어야 한다. 이러한 상황에서 react-query, swr과 같은 data-fetching 전용 hooks 라이브러리를 사용하면 많은 기능을 제공해주기 때문에 효율적으로 상태를 관리할 수 있다. 최적의 방법은 상황에 따라 다르기 때문에 장단점을 고려하여 개발을 해야한다. 이와 관련하여 usehooks.com에는 여러 가지 기본적인 hooks들에 대한 예제가 있으므로 필요할 때 참고할 수 있다.
위의 얘기를 듣고 우리는 회의에 들어갔다. 일단 펫칭 커스텀 훅을 참고하여 우리가 관련 커스텀훅을 만들기로 했다. react-query등을 사용해 시간을 절약할 수 있었지만 이걸 직접 만듦으로써 비동기 관련 코드에 대해 더욱 잘 이해할 수 있기 때문에 실패를 거듭하더라도 만들기로 했다.
hooks와 api로 나눠 url 관련된 네트워크 통신 코드는 api에, 추상화가 잘 된 커스텀훅은 hooks로 나눠 관리했다.
덕분에 개발서버의 url 바뀔 때 마다 url하나만 바꿔줘도 되고, api 관련 수정사항은 저 폴더 안에서만 해결할 수 있으니 관리/보수가 편해졌다. 좋아!
useAxios
의 전면적인 개편import { useState, useEffect, useCallback } from "react";
type Status = "Idle" | "Loading" | "Success" | "Error";
interface UseAxiosReturn<T> {
axiosData: () => void;
responseData: T | null;
status: Status;
}
const useAxios = <T>(
callback: () => Promise<T>,
deps: any[] = [],
skip = false
): UseAxiosReturn<T> => {
const [responseData, setResponseData] = useState<T | null>(null);
const [status, setStatus] = useState<Status>("Idle");
const axiosData = useCallback(async () => {
setStatus("Loading");
try {
const data = await callback();
setResponseData(data);
setStatus("Success");
return data;
} catch (error) {
setStatus("Error");
throw error;
}
}, deps);
useEffect(() => {
if (skip) return;
axiosData();
}, deps);
return { axiosData, responseData, status };
};
export default useAxios;
deps
(의존성 배열 관련 변수): deps
는 호출 관련 변수가 변하면 실시간 반영을 위해 받아주는 파라미터skip
(페이지 렌더링 후 실행 유무 변수):skip
은 true
면 페이지 렌더링 됐을 때 말고 나중에 데이터 호출을 실행하기 위해 넣어줄 수 있는 옵션.axiosData
를 리턴해 함수 호출을 원할 때 호출할 수 있게 했다. skip
이 true
면 무조건 있어야하는 값.responseData
: 응답 데이터!status
: 불러와졌는지 확인할 수 있는 상태 변수, 조건문에 조건으로 쓸 수 있게 리턴했다!초기 상태로 이미 남용하여 거의 모든 컴포넌트 및 페이지에 초기 코드를 쓴 상태에서 멘토님에게 조언을 들어 바꿨었다.
이미 쓴 코드를 수정하려고 하니까 진짜 힘들었다. 레퍼런스를 봐도 어떻게 우리가 이걸 최적화할건지와 다 쓴 코드들을 수정하고 수정한 코드에 맞춰서 다시 데이터를 가공하는 것, 이러한 점들이 되게 힘들었다. 설날 때 시골에서 맥북 키고 했었다..
알게된 것은 '역시 바퀴를 다시 발명하는 건 힘들다.'였고 비동기는 생각보다 더 까다롭다. 특히 SPA 특성상 그냥 axios 호출하여 GET하고 PATCH, POST 했을 때 데이터가 오면 실시간 반영이 안됐다. 지금보니 초기 상태의 코드로는 어림도 없었지.. 어떤 변수가 변했고, 그 변수를 다시 get해오는 것을 1부터 10까지 다 해줬어야 했는데 관련 코드가 없었으니까! 으이구!!
또, 아는 만큼 보인다고 useQuery
존재라든지 펫칭 관련 커스텀훅을 모르니 뭐가 문제인지도 몰랐다. 되게 부끄러웠다. 좀더 공부해서 프론트 개발 기획을 잡을 때부터 올바른 길을 걷기 위한 시야를 넓히고 싶었다.
이러한 일들을 겪고나니 다음 번엔 useQuery
를 써보고 싶어졌다. 지금 보니 진짜 만능같다. 나중에 후술할 실시간 반영 관련 문제, 무한스크롤 등에 다 쓰이는 것 같았다. 개인프로젝트에 꼭 배운 점들을 반영하여 성장하자.
useQuery 포스팅이 너무 기다려지네요..! 언제 업로드 예정이신가요. 현기증나요.