Dev Diary

[리팩토링] 1년 전에 만든 프로젝트 코드 다시 열어보기 - 2탄 본문

Projects

[리팩토링] 1년 전에 만든 프로젝트 코드 다시 열어보기 - 2탄

sik9252 2024. 7. 21. 12:29
SMALL

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가지 주요 주제를 중심으로 리팩토링을 수행해보았는데, 정답은 없다지만 내가 생각한대로 고친것이라 이게 정말 맞는 리팩토링 방법인지 의문이 들었다. 나름 시간을 들여서 수정하였는데 "나는 리팩토링이라 생각해서 한건데 혹시 더 악화된 것은 아닐까?"라는 생각이 자꾸 들었다. 아무래도 주위에 피드백을 받을 곳이 마땅치 않아서 그랬던것 같은데 이를 계기로 마틴 파울러의 리팩터링을 읽어보려고 한다. 이 책이 나의 작은 사수가 되어줬으면 좋겠다.

LIST