일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 이메일 인증
- kakao blind recruitment
- Github Actions
- React.memo
- next.js
- TextFormField
- 사탕게임
- visual test
- context api
- Flutter
- javascript
- ZOD
- 프로그래머스
- Python
- useEffect
- useMemo
- Vanilla JavaScript
- interaction test
- Props Drilling
- suffixicon
- custom hook
- typescript
- storybook
- 리팩토링
- next-auth
- 피보나치 함수
- 백준
- React-hook-form
- locale data
- react
- Today
- Total
Dev Diary
[리팩토링] 1년 전에 만든 프로젝트 코드 다시 열어보기 - 2탄 본문
1. 공통 타입 분리하기
// 분리, 인터페이스명 수정 @types/Building.ts
interface BuildingData {
id?: number;
name?: string;
floorsUp?: number;
floorsDown?: number;
description?: string;
latitude?: number;
longitude?: number;
uniqueNumber?: string;
}
interface EditBuildingResponse extends BuildingData {
success: boolean;
}
interface FloorImage {
buildingId?: number;
description?: string;
floorValue?: number;
image?: Blob;
floorId?: number;
}
...
export type {
BuildingData,
EditBuildingResponse,
FloorImage,
...
};
기존 api 요청 함수가 존재하는 파일에 함께 있었던 interface들을 @types라는 폴더를 새로 생성하여 하위에 여러곳에서 공통적으로 사용할 interface들을 같은 도메인끼리 묶어 정의하고 export 해주는 방식으로 수정했다.
// refator된 api 요청
import { httpClient } from '.';
import { useMutation, useQuery } from '@tanstack/react-query';
import {
BuildingData,
EditBuildingResponse,
FloorImage,
FloorQueryRequest,
FloorQueryResponse,
} from '../@types/Building';
export function useGetBuildingRequest(params: BuildingListRequest, isEnabled?: boolean) {
return useQuery(
[`/admin/building?page=${params.page}`, params],
() =>
httpClient<BuildingListResponse>({
method: 'GET',
url: `/admin/building?page=${params.page}`,
}),
{ enabled: isEnabled },
);
}
export function useGetFloorRequest(params: FloorQueryRequest, isEnabled?: boolean) {
return useQuery(
[`/admin/floor?buildingId=${params.buildingId}`, params.buildingId],
() =>
httpClient<FloorQueryResponse>({
method: 'GET',
url: `/admin/floor?buildingId=${params.buildingId}`,
}),
{ enabled: isEnabled },
);
}
export function useCreateBuildingRequest() {
return useMutation((data: BuildingData) =>
httpClient<EditBuildingResponse>({
method: 'POST',
url: '/admin/building',
data,
}),
);
}
export function useCreateFloorImageRequest() {
return useMutation((data: FloorImage) => {
const formData = new FormData();
formData.append('image', data.image ? data.image : '');
return httpClient<string>(
{
method: 'POST',
url: `/admin/floor/${data.buildingId ? data.buildingId : ''}?floorValue=${
data.floorValue ? data.floorValue : ''
}`,
data: formData,
},
{
'content-': 'multipart/form-data',
},
);
});
}
export function useUpdateBuildingRequest() {
return useMutation((data: BuildingData) =>
httpClient<EditBuildingResponse>({
method: 'PATCH',
url: `/admin/building/${data.id ? data.id : ''}`,
data,
}),
);
}
export function useUpdateFloorImageRequest() {
return useMutation((data: FloorImage) => {
const formData = new FormData();
formData.append('image', data.image ? data.image : '');
return httpClient<string>(
{
method: 'PATCH',
url: `/admin/floor/${data.floorId ? data.floorId : ''}`,
data: formData,
},
{
'content-': 'multipart/form-data',
},
);
});
}
export function useDeleteBuildingRequest() {
return useMutation((data: BuildingData) =>
httpClient<EditBuildingResponse>({
method: 'DELETE',
url: `/admin/building/${data.id ? data.id : ''}`,
}),
);
}
2. Custom Hook 다듬기
// /api-hooks/Account.ts
export function useGetAdminAccountRequest(params: AccountRequest, isEnabled?: boolean) {
const [accountList, setAccountList] = useState<AccountData[] | null>([]);
const [totalPosts, setTotalPosts] = useState(1);
const {
data,
error,
refetch: adminAccountRefetch,
} = useQuery(
[`/admin/account/admin?page=${params.page}`, params],
() =>
httpClient<IAccountResponse>({
method: 'GET',
url: `/admin/account/admin?page=${params.page}`,
}),
{ enabled: isEnabled },
);
// api 호출 외부에 있던 후처리 로직을 요청 안으로 가지고옴
useEffect(() => {
if (data) {
setAccountList(data?.data.memberList);
setTotalPosts(data?.data.accountCount);
} else if (error) {
toast.error('회원 계정 목록을 불러오는데 실패했습니다.');
}
}, [data, error]);
// api 요청 후 응답을 통해 완성된 데이터를 반환하도록 함
return { accountList, totalPosts, adminAccountRefetch };
}
기존에는 api 요청을 호출하는 곳(Page)에서 후처리를 수행했었는데 이 로직을 api 요청을 수행하는 함수 내부로 가져와 완성된 데이터를 반환하도록 하여 api 요청 호출이 존재했던 부분의 가독성을 증가시키고 코드량을 감소시켰다.
// 기존 api 요청 함수의 호출 방식
const {
data: accountResult,
isError: accountError,
refetch: accountRefetch,
} = useGetAdminAccountRequest({ page: currentPage - 1 }, false);
useEffect(() => {
if (accountResult) {
setAccountList(accountResult?.data.memberList);
setTotalPosts(accountResult?.data.accountCount);
} else if (accountError) {
toast.error('회원 계정 목록을 불러오는데 실패했습니다.');
}
}, [accountResult, accountError]);
// 리팩토링 후 api 요청 함수의 호출 방식
const { accountList, totalPosts, adminAccountRefetch } = useGetAdminAccountRequest({ page: currentPage - 1 }, false);
3.View와 로직 분리
function AdminAccountPage() {
// 여기서부터
const [isAlertOpen, setIsAlertOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalPosts, setTotalPosts] = useState(1);
const [accountList, setAccountList] = useState<AccountData[] | null>([]);
const [adminCount, setAdminCount] = useState(0);
const { setSelectedAccount } = useSelectedAccountAtom();
const {
data: accountResult,
isError: accountError,
refetch: accountRefetch,
} = useGetAdminAccountRequest({ page: currentPage - 1 }, false);
const { accountList, totalPosts, adminAccountRefetch } = useGetAdminAccountRequest({ page: currentPage - 1 }, false);
const { adminCount, totalAccountCountRefetch } = useGetTotalAccountCountRequest(false);
useEffect(() => {
void accountRefetch();
}, [currentPage]);
useEffect(() => {
void totalAccountCountRefetch();
}, []);
const handleAccountManageModal = (account: AccountData) => {
setSelectedAccount(account);
setIsModalOpen(true);
};
// 여기까지 댕강 잘라서 useAdminAccount.tsx에 분리
return (
<RightContainer title={'계정 관리'}>
...
</RightContainer>
);
}
export default AdminAccountPage;
기존 코드에서 화면을 그리는 부분을 제외한 모든 로직을 잘라서 다른 파일에 분리하여 필요한 곳에서 가져와 사용할 수 있도록 수정했다.
// 리팩토링 후
function AdminAccountPage() {
const {
isAlertOpen,
setIsAlertOpen,
isModalOpen,
setIsModalOpen,
currentPage,
setCurrentPage,
accountList,
totalPosts,
adminCount,
handleAccountManageModal,
} = useAdminAccount();
return (
<RightContainer title={'계정 관리'}>
...
</RightContainer>
);
}
export default AdminAccountPage;
4. interface라는 것을 명확하게 나타내기 + Type이란 접미사 제거하기
export interface FloorImageType {
buildingId?: number;
description?: string;
floorValue?: number;
image?: Blob;
floorId?: number;
}
기존에는 FloorImageType라고 적혀있던 것에 이것은 interface임을 명확하게 나타내기위해 'I' 접두사를 추가하고, Type이란 접미사가 붙어있었는데 이제 접두사에 I를 추가함으로써 이것은 타입임을 알 수 있으므로 Type이란 접미사를 제거하고 깔끔하게 바꾸었다.
export interface IFloorImage {
buildingId?: number;
description?: string;
floorValue?: number;
image?: Blob;
floorId?: number;
}
이렇게 일단 4가지 주요 주제를 중심으로 리팩토링을 수행해보았는데, 정답은 없다지만 내가 생각한대로 고친것이라 이게 정말 맞는 리팩토링 방법인지 의문이 들었다. 나름 시간을 들여서 수정하였는데 "나는 리팩토링이라 생각해서 한건데 혹시 더 악화된 것은 아닐까?"라는 생각이 자꾸 들었다. 아무래도 주위에 피드백을 받을 곳이 마땅치 않아서 그랬던것 같은데 이를 계기로 마틴 파울러의 리팩터링
을 읽어보려고 한다. 이 책이 나의 작은 사수가 되어줬으면 좋겠다.
'Projects' 카테고리의 다른 글
[Question Cloud] 회원가입 만들기 (1) (2) | 2024.10.06 |
---|---|
[Question Cloud] Chromatic을 활용한 Storybook 배포 (0) | 2024.09.03 |
[Question Cloud] Storybook 과 Testing 설정하기 (0) | 2024.08.23 |
[리팩토링] 1년 전에 만든 프로젝트 코드 다시 열어보기 - 1탄 (0) | 2024.07.20 |
사이드 바 리뉴얼하기 (23.11.09) (1) | 2023.11.10 |