동일한 로직은 항상 함수단위로 변경하고, 최대한 함수의 인자로 그 각각의 로직마다 필요한 사항들을 넘겨줘서 상황에 따라 다른 로직으로 재사용 할 수 있도록 하자!
기존 리액트 쿼리 사용 로직은 아래와 같다
문제 관련 페이지들의 레이아웃인 CommonLayoutWithMenus
문제 정보를 보여주는 ProblemInfo
문제 제출 페이지의 Submit
// API 로직
export const getProblemDetail = async <T extends ProblemDetailType>(id: number) => {
const res = await axios.get<T>(`/api/problem/detail/${id}`);
return res.data;
};
// useQuery (각 컴포넌트 마다 아래와 같이 선언 후 사용)
const { data } = useQuery({
queryKey: ['problemInfo', { problemId: Number(problemId) }],
queryFn: async () => {
const res = await getProblemDetail<ProblemDetailType>(Number(problemId));
return res.data;
},
staleTime: Infinity,
});
이걸 리팩토링 해 볼 것이다.
API분리
axios.create
를 따로 생성하고 (여기서 baseURL은 프록시로
사용하고 있으므로 여기에 작성할 필요 x)
// instance.ts
import axios from 'axios';
export const API = axios.create({});
API를 하나의 객체 메소드로 정의해서 호출한다
// problem.ts (문제 정보 관련 API 파일)
import axios from 'axios';
import { API } from './instance';
import { ProblemDetailType } from '@/type/problem';
// API
export const problemApi = {
... ,
getProblemDetail: async <T extends ProblemDetailType>(id: number) => {
return await API.get<T>(`/api/problem/detail/${id}`);
},
};
useQurey 분리
기존에 사용하던 useQurey를 각 컴포넌트에서 그대로
사용하지 않고 useQurey 자체를 하나의 함수로 분리를 한다
export const useGetProblemDetail = <T>(problemId?: number | null, options?: T) => {
return useQuery({
queryKey: ['problemDetail', { problemId }],
queryFn: async () => {
const res = await problemApi.getProblemDetail(problemId!);
return res.data;
},
staleTime: Infinity,
retry: 0,
enabled: problemId !== null,
});
};
API를 호출할때 사용할 인자
enabled 옵션으로 사용할 상태
그 외 option들을 useGetProblemDetail함수의 인자로 받는다
useGetProblemDetail(currentProblemId, {
enabled: currentProblemId !== null,
})
usage
const { data: { data: problemDetailData = null } = {} } = useGetProblemDetail(currentProblemId);
현재 호출하는 API의 명세를 보면 다음과 같다
Axios의 반환 객체의 data프로퍼티에 접근한 실데이터인 JSON객체
에서 , data프로퍼티로 한번 더 접근을 해야 내가 원하는 데이터가 들어온다
우선 useQuery의 queryFn에서 .data를 반환해줘서
실데이터인 JSON객체만 반환이 될 것이다
// 위의 useGetProblemDetail 일부
queryFn: async () => {
const res = await problemApi.getProblemDetail(problemId!);
return res.data;
},
그리고 이걸 해당 컴포넌트에서 사용해야 하는데
아래와 같이 data에는 useQuery가 반환하는 데이터로 접근하는
기본 내장 프로퍼티인 data 변수에 JSON객체가 들어간다
// data는 useQuery가 반환하는 데이터로 접근하는 프로퍼티
// JSON객체의 data가 아님
const { data } = useGetProblemDetail(currentProblemId);
원래 같으면 아래와 같이 접근을 해야 하지만
const { data } = useGetProblemDetail(currentProblemId);
console.log(data?.data)
그 대신 객체 구조분해 할당을 이용해서 바로 변수에 원하는 데이터를 넣어 줄 수 있다
{ data: { data: problemDetailData = null } = {} }
객체 구조분해 할당으로 useQuery의 data 안에 있는 data프로퍼티에 접근하고 ,
이 값을 problemDetailData 로 받는다
다만, useGetProblemDetail에서 반환하는 값이 undefined일 수도 있다 (비동기 호출 오류 시)
그러므로 useQuery에서 반환하는 값이 undefined일 수 있다는 것을 대비해서
undefined 반환 시 = {}
로 초기화 하도록 한다
{ data: { data: problemDetailData = null } = {} }
추가로, 데이터는 불러와 졌는데 이 JSON 데이터의 data프로퍼티 값이 없거나,
data프로퍼티 값이 undefined인 경우를 대비해서 = null
로 초기화를 하도록 한다
{ data: { data: problemDetailData = null } = {} }
위의 2가지 경우를 대비하지 않으면 TS는 자체적으로 에러를 뱉어낸다.
problemDetailData
의 기본 초깃값을 null로 설정 했으므로 앞으로problemDetailData
를 기반으로 진행하는 조건부 로직에선undefined가 아닌 null로 비교해야 한다
const { data: { data: problemDetailData = null } = {} } = useGetProblemDetail(currentProblemId, {
enabled: currentProblemId !== null,
});
return (
<div className={CommonLayoutStyle}>
{ problemDetailData !== null && <ProblemMenus problemDetail={problemDetailData} /> }
<Outlet />
</div>
);
useEffect(() => {
const getFirstValue = () => {
if (isStart && **problemDetailData !== null**) {
const templateStr = problemDetailData.template;
setIsStart(false);
setCode(templateStr);
}
};
getFirstValue();
}, [problemDetailData]);
최종적으로 3개 data 체이닝을 구조분해 할당으로 해결한 것이다
// AxiosResponse의 data , useQuery의 data , JSON내부의 data
data . data . data
사실 .data 체이닝을 해결한 것은
useQuery의 data
,JSON내부의 data
2개이다
첫번째 AxiosResponse의 data
는 애초에 api를 호출 후, res.data
로 반환을 해줬기 때문이다.