Query 상태에 따른 컴포넌트를 깔끔하게 정리해보자
📌 Query 상태 그리고 컴포넌트
나는 useQuery를 이용해 be와 통신해서 각 상황별로 컴포넌트를 렌더링하고 있다. 예를 들어, data를 불러오는 경우 <Loading/> 컴포넌트, 빈 경우에는 <Empty/>, 에러인 경우는 <Error/>를 표시한다.
그런데 최근에 작업한 내용에는 동일한 컴포넌트를 기반으로 한 페이지가 여럿 있었는데, 각 컴포넌트 별로 이것을 작성하는 건 상당히 비효율적이란 생각이 들었다.
문제는 동일한 컴포넌트지만, api도 다르고 데이터가 전부 다르다는 건데.. 🤔 타입스크립트를 사용하기 때문에 타입을 어떻게 맞추면 좋을지 고민이 되었고, 이를 해결하기 위해 제네릭을 사용하였다.
이번 글은 이 내용을 정리하는 글이다.
👀 제네릭을 사용해보자!
✅ 기본 정리
사실 나는 <Component<T>> 형태의 타입을 지정하는 컴포넌트가 생소했다. 이게 어떤 의미인지 부터 살펴보자.
1
2
3
function Component<T>(value: T): T {
return value
}
먼저 함수는 다음과 같이 제네릭을 쓸 수 있다. 이 식에는 T가 3번 사용되었는데, 순서대로 해석해보자면 다음과 같다.
<T>
- 이 함수가 제네릭 함수임을 ㄴ타낸다.
- 타입 매개변수 T를 선언해, 어떤 타입이든 받을 수 있음을 나타낸다.
(value:T)- 1번에서 선언한 타입 변수를 사용해 매개변수의 타입을 지정해준 것이다.
- 즉 이 함수는 T 타입의 값을 인자로 받게 된다.
:T- 함수의 반환 타입을 T로 지정하겠다는 뜻이다.
실제 사용은 다음과 같다.
1
const str = Component<string>("Hello TypeScript")
여기서 T는 string으로 지정되며, value:T에 따라 매개변수도 string이 되고, 반환값도 string이 된다.
✅ object 사용해보기
기존과 동일하게 아래와 같은 함수를 만든 다음, 객체 타입을 사용해보자.
1
2
3
function Component<T>(value: T): T {
return value
}
1
2
3
4
5
6
type User = {
name: string
age: number
}
const user = Component<User>({ name: "신짱구", age: 5 })
이제 제내릭 타입 매개변수 T가 직접 지정해준 User로 지정되며, 모든 타입 추론이 User 기준으로 작동하게 된다.
1
2
3
function Component(value: User): User {
return value
}
🫡 Query에서 사용하기
일단 내 목표는 다음과 같다.
- 데이터가 다른 다수의 테이블을 렌더링한다.
- 이 때 useQuery의 상태값을 활용해 필요한 컴포넌트가 각각 렌더링 한다.
- 유지보수를 위해
<QueryStatus/>공통 컴포넌트를 만들고 이를 구현하도록 한다.
먼저 <QueryStatus/>를 작성한다. 이때 data값이 달라질 수 있기 때문에 이 부분은 제네릭 매개변수를 활용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type QueryStatusProps<T> = {
isLoading: boolean
isError: boolean
data: T | undefined
children: React.ReactNode
}
function QueryStatus<T>({
isLoading,
isError,
data,
children,
}: QueryStatusProps<T>) {
if (isLoading) return <Loading />
if (isError) return <Error />
if (!data || (Array.isArray(data) && data.length === 0)) return <Empty />
return <>{children}</>
}
실제 사용은 아래와 같이 진행한다.
1
2
3
4
5
6
7
8
9
10
11
export default function MemberModal() {
const { data, isLoading, isError } = useCreateMember()
return (
<>
<QueryStatus<User[]> isLoading={isLoading} isError={isError} data={data}>
<UserTable data={data!} />
</QueryStatus>
</>
)
}
<QueryStatus<User[]> 이 부분에서 User[]를 이용해 T의 타입을 지정해주고 있다. 즉 <QueryStatus/> 컴포넌트에서 사용하는 data의 타입이 안전하게 지정된다.
⭐ data!의 의미
여기서 data! 라고 작성하는 부부은 null이 아님을 단언한다. 이미 내부 <QueryStatus/> 컴포넌트에서 data가 지정된 <User[]>외의 null, undefined가 될 수 있는 상황, isLoading이나 isError를 모두 처리해 두었기 때문이다. 이를 통해 <User[]> | undefined가 아닌 <User[]> 만 내려주어도 타입 에러가 발생하지 않는다.