1. hook의 목적2. 기본 기능2-1. 입력 값 설명2-1-1. enabled가 필요한 이유2-1-2. deps가 필요한 이유2-2. 출력 값 설명2-2-1. isLoading3. 구현 코드3-1. as const의 기능과 필요한 이유(Type Widening)
1. hook의 목적
- 간단한 fetch를 할 일이 필요할 때 외부 의존성 없이 custom hook으로 간결하게 사용하고자 할 때
- 굳이 외부 의존성을 설치하지 않고도 타입스크립트의 기본적인 도움을 받을 수 있는 훅을 간단히 구현하는 방법을 설명합니다.
- fetch 조회 요청에 대한 (요청 실행 여부, 로딩 상태, 오류 전달) 기능만 제공하며, react-query 등에서 제공하는 여러 가지 옵션들은 추후 구현할 예정입니다.
2. 기본 기능
- 입력 값
- 필수
- enabled 옵션
- fetchFn 옵션
- deps 옵션
- 편의
- initialData 옵션
- 출력 값
- data
- isLoading
- 오류
- 현재 구현은 오류 발생 시 ErrorBoundary에서 처리할 수 있도록 훅에서 그대로 오류를 던집니다.
2-1. 입력 값 설명
2-1-1. enabled가 필요한 이유
- useFetch는 마운트 됐을 때 자동으로 요청이 실행되기 때문에, 요청에 필요한 데이터가 없을 때는 요청을 하지 않는 조건이 필요합니다.
2-1-2. deps가 필요한 이유
- fetchFn은 주로 인라인으로 정의되는데, 이 경우 매 렌더링마다 재생성됩니다.
- 구현하는 입장에서 fetchFn이 바뀔 때마다 리-렌더하면 쉽겠지만 사용성이 떨어집니다.
- 이를 위해 fetchFn이 의존하는 값들을 deps에 직접 전달함으로써 fetchFn을 useCallback으로 무조건 감싸지 않아도 되도록 할 수 있습니다.
2-2. 출력 값 설명
2-2-1. isLoading
- data의 값이 fetchFn에서 reolsve한 값인지 아닌지를 isLoading 여부에 따라 결정되도록 합니다.
- 즉,
if (isLoading) return;
라인 다음에는data: ResultType
이어야 합니다.
3. 구현 코드
- 제네릭 인자
ResultType
를 사용했지만,fetchFn
의 타입으로 자동으로 추론되므로 필수 입력 값이 아닙니다.
as const
로 TypeScript에서 타입이 뭉개지는 현상을 회피했습니다.
import { useEffect, useState } from "react"; interface UseFetchProps<ResultType> { enabled?: boolean; initialData?: ResultType; fetchFn: () => Promise<ResultType>; deps?: unknown[]; } export const useFetch = <ResultType>({ enabled = true, initialData = undefined, fetchFn, deps = [], }: UseFetchProps<ResultType>) => { const [fetchResult, setFetchResult] = useState<ResultType | undefined>( initialData, ); const [error, setError] = useState<unknown | undefined>(undefined); useEffect(() => { if (!enabled) { return; } (async function () { try { const resultData = await fetchFn(); setFetchResult(resultData); } catch (error) { setError(error); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, ...deps]); // NOTE: callback에서 React tree로 오류를 전파 if (error) { throw error; } return fetchResult === undefined ? ({ data: undefined, isLoading: true, } as const) : ({ data: fetchResult, isLoading: false, } as const); };
3-1. as const
의 기능과 필요한 이유(Type Widening)
TypeScript는 객체의 필드와 같은 재할당 가능한 값은 Type Widening을 수행합니다.
const obj = { data: undefined, isLoading: true }; // TypeScript: { data: undefined; isLoading: boolean }
const message = "message"; // TypeScript: message 변수의 타입 = "message" (값 변경 X) let message = "message"; // TypeScript: message 변수의 타입 = string. (widening 발생, let이므로)
이 때문에
as const
가 없다면 isLoading
필드의 타입은 boolean
이 됩니다.이미 useFetch의 반환 값의 타입이
isLoading
과 무관하게 boolean
으로 추론됐기 때문에, isLoading
으로 Type Narrowing을 수행할 수가 없습니다.{ data: undefined; isLoading: boolean } | { data: ResultType; isLoading: boolean }
as const
는 리터럴 타입으로 추론합니다. 리터럴 타입이란, 특정 값(상수)만을 가질 수 있는 타입입니다. 위의 예시에서 봤듯, 일종의 특정 상수를 타입으로 사용합니다.이 경우
isLoading
값에 따라 true
, false
로 분기하기 때문에, 사용처에서 Type Narrowing이 가능하게 됩니다.const obj = { data: undefined, isLoading: true }; // { readonly data: undefined; readonly isLoading: true }