-
Notifications
You must be signed in to change notification settings - Fork 0
[Week05] 엘릭 5주차 미션 #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| error: string | null; | ||
| }; | ||
|
|
||
| export function useCustomFetch<TData>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전반적으로 코드 잘 보았습니다. 몇 가지 부분만 개선되면 더 좋은 코드가 될 것 같습니다!
*현재 문제
훅이 “의존성 변화”로만 다시 호출됩니다. 버튼으로 새로 고침, 무한 스크롤, 필터 적용 직후 재조회 같은 수동 재요청이 불가합니다. 네트워크가 느릴 때 “먼저 보낸 요청”이 “나중에 보낸 요청”보다 늦게 도착하면, 낡은 응답이 최신 화면을 덮어쓰는 경쟁 상태가 발생할 수 있습니다.
*개선안
훅 내부의 요청 함수를 refetch()로 노출해 사용자가 원하는 시점에 재요청할 수 있게 합니다. 요청마다 고유 id를 부여해 가장 최신 요청만 상태에 반영하도록 합니다.
axios.isCancel/ERR_CANCELED는 에러로 취급하지 않고 조용히 무시합니다.
*기대 효과
검색/필터/페이지네이션/재시도 버튼 등 다양한 UI에서 동일 훅 재사용성이 높아지고, 느린 이전 응답이 최신 화면을 덮어쓰는 버그를 예방합니다.
*테스트 포인트
필터 값을 빠르게 여러 번 바꿔도 최종 선택 값의 결과만 렌더링되는지 확인 “새로고침” 버튼을 눌렀을 때 loading 플래그가 정상 동작하고 에러/성공 후 상태가 일관적인지 확인합니다.
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
import axios from "axios";
import { tmdb } from "../api/tmdb";
type UseCustomFetchOptions = {
params?: Record<string, unknown>;
enabled?: boolean;
/** 외부 의존성(검색어, 필터 등) 변경 시 자동 재요청 /
dependencies?: unknown[];
/* 초기 표시용 데이터가 있으면 첫 로딩 스피너를 숨깁니다. */
initialData?: TData | null;
};
type UseCustomFetchReturn = {
data: TData | null;
loading: boolean;
error: string | null;
/** 수동 재요청 (새로고침/재시도 버튼 등에서 사용) */
refetch: () => Promise;
};
export function useCustomFetch<TData = unknown>(
endpoint: string | null,
options: UseCustomFetchOptions = {}
): UseCustomFetchReturn {
const {
params,
enabled = true,
dependencies = [],
initialData = null,
} = options;
const [data, setData] = useState<TData | null>(initialData);
const [loading, setLoading] = useState(
() => Boolean(enabled && endpoint && !initialData)
);
const [error, setError] = useState<string | null>(null);
// 최신 요청만 상태에 반영하기 위한 요청 id 및 취소 컨트롤러 보관
const requestIdRef = useRef(0);
const controllerRef = useRef<AbortController | null>(null);
// params를 안정적으로 비교하기 위한 키 (key 순서 정렬 후 JSON 직렬화)
const paramsKey = useMemo(() => {
if (!params) return "";
try {
const entries = Object.entries(params).sort(([a], [b]) =>
a > b ? 1 : a < b ? -1 : 0
);
return JSON.stringify(entries);
} catch {
// 실패시 예측 가능하게 고정값 반환(불필요한 재요청 방지)
return "";
}
}, [params]);
const refetch = useCallback(async () => {
if (!enabled || !endpoint) return;
// 이전 요청 중단
if (controllerRef.current) controllerRef.current.abort();
const controller = new AbortController();
controllerRef.current = controller;
// 이 요청의 고유 id
const myId = ++requestIdRef.current;
setLoading(true);
setError(null);
try {
const res = await tmdb.get<TData>(endpoint, {
params,
signal: controller.signal,
});
// 최신 요청만 반영
if (myId === requestIdRef.current) {
setData(res.data);
}
} catch (err: any) {
// 취소된 요청은 무시
const isCanceled =
axios.isCancel?.(err) ||
err?.code === "ERR_CANCELED" ||
err?.name === "CanceledError";
if (isCanceled) return;
if (myId === requestIdRef.current) {
const message =
err instanceof Error
? err.message
: "데이터를 불러오는 중 문제가 발생했습니다.";
setError(message);
}
} finally {
if (myId === requestIdRef.current) {
setLoading(false);
}
}
}, [enabled, endpoint, paramsKey]);
// 초기 로딩 및 의존성 변경 시 자동 재요청
useEffect(() => {
if (!enabled || !endpoint) {
setLoading(false);
return;
}
void refetch();
// 언마운트/다음 요청 전 현재 요청 취소
return () => {
if (controllerRef.current) controllerRef.current.abort();
};
// dependencies는 외부에서 명시적으로 전달받은 값만 추가
}, [refetch, ...dependencies]);
return { data, loading, error, refetch };
}
No description provided.