살펴보기
React에서 Custom Hook을 사용하는 것은 코드 재사용성, 관심사의 분리, 그리고 테스트 용이성을 개선하는 효과적인 방법입니다. 이러한 특성은 특히 대규모 애플리케이션의 유지보수성과 가독성에 큰 영향을 미칩니다. 아래의 예시를 통해 이를 더 자세히 살펴보겠습니다.
예시 코드
useUsersQuery.ts
import { useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
//
//
//
interface UseUsersQueryParams {
/**
* if you want to get specific page, pass page number
*/
usersPage?: number;
/**
* if you want to get specific count, pass count number
*/
usersPerPage?: number;
/**
* user filter
*/
filterUser?: string;
/**
* user sort by
*/
sortBy?: string;
}
interface UseUsersQueryReturn {
users: Users[];
usersCount: number;
isUsersError: boolean;
isUsersLoading: boolean;
refetchUsers: () => void;
}
type UseUsersQuery = (params?: UseUsersQueryParams) => UseUsersQueryReturn;
//
//
//
export const USERS_ROOT_QUERY_KEY = ['usersGet'];
const USERS_PER_PAGE = 10;
//
//
//
export const useUsersQuery: UseUsersQuery = ({
usersPage = 1,
usersPerPage = USERS_PER_PAGE,
filterUser = '',
sortBy,
} = {}) => {
const _return = useRef({} as UseUsersQueryReturn);
//
// fetch users
//
const {
data: [users = [], usersCount = 0] = [],
isLoading: isUsersLoading,
isError: isUsersError,
refetch: refetchUsers,
} = useQuery({
queryKey: [
...USERS_ROOT_QUERY_KEY,
usersPage,
usersPerPage,
filterUser,
sortBy,
],
queryFn: async ({ signal }) => {
const countParams = {
filterUser: `%${filterUser}%`,
};
return Promise.all([
// user list
fetch('/users', {
method: 'GET',
body: {
...countParams,
skip: (usersPage - 1) * usersPerPage,
count: usersPerPage,
sortBy,
}
signal,
}),
fetch('/users/count', {
method: 'GET',
body:{
...countParams,
},
signal
},
),
]);
},
keepPreviousData: true,
refetchOnMount: true,
refetchOnWindowFocus: false,
});
//
//
//
_return.current.users = users;
_return.current.usersCount = Number(usersCount);
_return.current.isUsersLoading = isUsersLoading;
_return.current.isUsersError = isUsersError;
_return.current.refetchUsers = refetchUsers;
return _return.current;
};
- 목적: 사용자 데이터를 페이징, 필터링, 정렬 기능과 함께 가져오는 React Query 훅입니다.
- 파라미터: UseUsersQueryParams를 통해 데이터를 필터링하고 정렬하는 데 필요한 옵션을 제공합니다.
- 리턴 타입: 사용자 데이터, 로딩 및 에러 상태, 데이터 재요청 함수를 포함합니다.
- 주요 기능:
- useQuery를 사용하여 데이터를 비동기적으로 가져옵니다.
- modifyOnly 옵션을 통해 데이터 패치 활성화/비활성화를 제어합니다.
- 페이징을 위해 usersPage와 usersPerPage를 활용합니다.
- filterUser와 sortBy로 필터링 및 정렬 기능을 제공합니다.
useUserQuery.ts
import { useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
//
//
//
interface UseUserQueryRequest {
userId: string;
}
interface UseUserQueryReturn {
user: null | User;
isUserError: boolean;
isUserLoading: boolean;
refetchUser: () => void;
}
type UseUserQuery = (params: UseUserQueryRequest) => UseUserQueryReturn;
//
//
//
export const USER_ROOT_QUERY_KEY = ['userGet'];
//
//
//
export const useUserQuery: UseUserQuery = ({ userId }) => {
const _return = useRef({} as UseUserQueryReturn);
//
//
//
const {
data: user = null,
isLoading: isUserLoading,
isError: isUserError,
refetch: refetchUser,
} = useQuery({
queryKey: [...User_ROOT_QUERY_KEY, userId],
queryFn: async ({ signal }) =>
fetch('/user', {
method: 'GET',
body: { userId },
signal,
}),
keepPreviousData: true,
refetchOnMount: true,
refetchOnWindowFocus: false,
});
//
//
//
_return.current.user = user;
_return.current.isUserError = isUserError;
_return.current.isUserLoading = isUserLoading;
_return.current.refetchUser = refetchUser;
return _return.current;
};
- 목적: 특정 사용자의 데이터를 가져오는 React Query 훅입니다.
- 파라미터: userId 를 통해 특정 사용자를 식별합니다.
- 리턴 타입: 사용자 데이터, 로딩 및 에러 상태, 데이터 재요청 함수를 포함합니다.
- 주요 기능:
- useQuery를 사용하여 특정 사용자의 데이터를 가져옵니다.
- 데이터의 실시간성을 유지하기 위해 마운트 시 및 윈도우 포커스 변경 시 데이터를 재요청합니다.
useUserMutation.ts
import { useRef } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { USER_ROOT_QUERY_KEY } from './useUserQuery';
import { USERS_ROOT_QUERY_KEY } from './useUsersQuery';
//
//
//
interface UseUserMutationReturn {
isPostUserLoading: boolean;
isPatchUserLoading: boolean;
isRemoveUserLoading: boolean;
postUser: (params: UserPostRequest) => Promise<{ id: string }>;
patchUser: (params: UserPatchRequest) => Promise<unknown>;
removeUser: (params: UserDeleteRequest) => Promise<unknown>;
}
type UseUserMutation = () => UseUserMutationReturn;
//
//
//
export const useUserMutation: UseUserMutation = () => {
const queryClient = useQueryClient();
const _return = useRef({} as UseUserMutationReturn);
/**
*
*/
const handleOnSuccess = () => {
void queryClient.invalidateQueries(USERS_ROOT_QUERY_KEY);
void queryClient.invalidateQueries(USER_ROOT_QUERY_KEY);
};
//
// post user
//
const { mutateAsync: postUser, isLoading: isPostUserLoading } =
useMutation({
mutationFn: async ({userId, ...params}: UserPostRequest) => {
return fetch('/user', {
method: 'POST',
body: { ...params, userId },
});
},
onSuccess: () => {
handleOnSuccess();
},
onError: err => {
console.error(err);
},
});
//
// patch User
//
const { mutateAsync: patchUser, isLoading: isPatchUserLoading } =
useMutation({
mutationFn: async ({ userId, ...params }: UserPatchRequest) => {
return fetch('/user'{
method: 'PATCH',
body: { ...params, userId },
});
},
onSuccess: () => {
handleOnSuccess();
},
onError: err => {
console.error(err);
},
});
//
// delete user
//
const { mutateAsync: removeUser, isLoading: isRemoveUserLoading } =
useMutation({
mutationFn: async ({ userId }: UserDeleteRequest) => {
return fetch('/user'{
method: 'DELETE',
body: { userId },
});
},
onSuccess: () => {
handleOnSuccess();
},
onError: err => {
console.error(err);
},
});
//
//
//
_return.current.isPostUserLoading = isPostUserLoading;
_return.current.isPatchUserLoading = isPatchUserLoading;
_return.current.isRemoveUserLoading = isRemoveUserLoading;
_return.current.postUser = postUser;
_return.current.patchUser = patchUser;
_return.current.removeUser = removeUser;
return _return.current;
};
- 목적: 사용자 데이터를 생성, 수정, 삭제하는 작업을 관리하는 React Query 훅입니다.
- 리턴 타입: 각 CRUD 작업의 로딩 상태와 함수들을 포함합니다.
- 주요 기능:
- useMutation을 사용하여 서버에 데이터 생성, 수정, 삭제 요청을 합니다.
- queryClient를 사용하여 관련 쿼리를 무효화하고, 최신 데이터로 업데이트합니다.
- Sentry를 통해 에러를 모니터링하고 로깅합니다.
사용 예시
useUsersQuery 사용 예시
import React from 'react';
import { useUsersQuery } from './useUsersQuery';
const UsersList = () => {
const { users, isUsersLoading, isUsersError } = useUsersQuery();
if (isUsersLoading) return <div>Loading...</div>;
if (isUsersError) return <div>Error loading users.</div>;
return (
<div>
<h2>Users List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
export default UsersList;
useUserMutation 사용 예시
import React, { useState } from 'react';
import { useUserMutation } from './useUserMutation';
const UserForm = ({ userId }) => {
const [name, setName] = useState('');
const { patchUser } = useUserMutation();
const handleSubmit = async (event) => {
event.preventDefault();
await patchUser({ userId, name });
// 추가적인 처리 (예: 상태 업데이트, 알림 등)
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter new name"
/>
<button type="submit">Update Name</button>
</form>
);
};
export default UserForm;
추가 설명
useRef 사용 이유
useRef는 컴포넌트의 렌더링 사이클과 독립적으로 데이터를 유지하는데 사용됩니다. 주요 이점은 다음과 같습니다:
1. 참조 일관성 유지: `useRef`는 값이 저장된 공간입니다. 이 값은 컴포넌트가 여러 번 새로 그려져도 바뀌지 않아요. 이 기능 덕분에 반환된 객체의 주소가 계속 같아서, 컴포넌트가 필요 이상으로 다시 그려지지 않게 도와줍니다.
2. 함수 참조 안정성: `useRef`로 함수를 저장하면, 그 함수의 주소가 계속 같아요. 이렇게 되면 함수를 사용하는 컴포넌트가 다시 그려질 때마다 함수가 새로 만들어지지 않으니, 컴포넌트가 효율적으로 동작할 수 있어요.
Query와 Mutation 훅의 분리
각 훅은 특정 기능과 책임을 가지며, 이는 여러 면에서 이점을 제공합니다:
- 단일 책임 원칙: 각 훅은 고유한 기능을 수행합니다. 예를 들어, useUsersQuery는 다수 사용자 조회, useUserQuery는 단일 사용자 조회, useUserMutation은 사용자 데이터의 변경을 담당합니다.
- 재사용성과 유지보수: 특정 기능에 초점을 맞춘 훅의 분리로 필요한 기능만을 선택하여 사용할 수 있으며, 이는 유지보수를 간소화합니다.
- 가독성 향상: 분리된 훅은 각각의 역할이 명확해져 코드의 이해와 관리가 용이해집니다.
useUserMutation에서 직접적으로 userId 파라미터를 받지 않는 이유
- 별도의 상태 관리 필요성 감소: 만약 useUserMutation이 userId를 직접적인 파라미터로 받는다면, 이는 userId를 선택하는 별도의 상태 관리가 필요함을 의미합니다. 즉, 사용자의 선택이나 상호작용에 따라 변경되는 userId 값을 상태로 유지하고, 이를 훅에 전달하는 추가적인 로직이 필요해집니다. 이는 애플리케이션의 상태 관리를 더 복잡하게 만들며, 특히 여러 사용자의 데이터를 다룰 때 이러한 복잡성은 더욱 증가합니다.
- 재사용성 및 유연성 제한: userId를 훅의 외부 상태로 관리해야 한다면, 이 훅은 특정 컨텍스트나 상위 컴포넌트의 상태에 의존적이게 됩니다. 이는 훅의 재사용성을 제한하며, 다양한 시나리오나 컴포넌트에서의 유연한 사용을 어렵게 만듭니다.
- 실용성 감소: 각각의 CRUD 작업에서 userId를 독립적으로 받음으로써, useUserMutation은 더 실용적이고 직관적으로 사용될 수 있습니다. 특정 사용자에 대한 작업이 필요할 때마다 userId를 전달함으로써, 상태 관리의 복잡성을 줄이고, 코드의 명확성과 가독성을 향상시킬 수 있습니다.
결론적으로, useUserMutation에서 userId를 직접적인 파라미터로 받지 않는 방식은 상태 관리의 복잡성을 줄이고, 훅의 재사용성 및 유연성을 향상시키는 효과적인 접근법입니다. 이는 React에서 상태 관리를 간결하고 효율적으로 유지하는 데 중요한 요소입니다.
마무리
Custom Hook과 useRef의 사용은 React 개발에서 중요한 패턴으로 자리 잡았습니다. 이는 애플리케이션의 성능 최적화, 코드의 가독성 향상, 그리고 유지보수의 용이성에 크게 기여합니다. 특히, 데이터 관리의 복잡성이 높은 대규모 애플리케이션에서 이러한 패턴의 중요성은 더욱 부각됩니다.
React의 Custom Hook과 useRef를 활용하는 것은 단순히 코드를 재사용하는 것 이상의 가치를 제공합니다. 이는 애플리케이션의 전반적인 구조와 성능을 향상시키며, 개발자로 하여금 더 효율적이고 직관적인 방식으로 프로젝트를 관리할 수 있게 합니다. 이러한 패턴들을 적극적으로 활용하여, 더욱 강력하고 유지보수가 용이한 React 애플리케이션을 구축해 보시길 권장합니다.
'Frontend > Thoughts on Development' 카테고리의 다른 글
React Query에서 Non-null Assertion Operator 사용하기 좋은 방법일까? (0) | 2024.02.18 |
---|---|
똑똑한 컴포넌트, 어디까지가 좋을까? (0) | 2024.01.26 |
협업하기 위한 CSS 컨벤션 (0) | 2023.09.18 |
웹 접근성 적용하기 (0) | 2020.11.18 |
유용한 Sass(SCSS) (0) | 2020.11.03 |