sik9252

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

Projects

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

sik9252 2024. 7. 20. 13:33
SMALL

지금까지 다수의 프로젝트를 진행했었지만, 실제 운영하는 서비스가 아닌 이상 지난 프로젝트 코드를 다시 열어본적은 거의 없었다. 하지만, 내가 그동안 얼마나 성장했는지 예전 코드를 보고 무언가 불편한점을 느낄 수 있는지 확인해보기 위해 1년전 프로젝트를 다시 열어보기로 했다.

 

타겟 프로젝트는 1년전에 대학교 재학 당시 코로나의 여파로 인해 처음 대면 활동을 시작하면서 학교 캠퍼스 지리를 잘 알지 못하는 재학생들과 신입생들을 대상으로 학교 내 건물과 강의실 위치를 제공해주기 위해 개발했던 서비스로 안타깝게도 지금은 운영하고 있지 않지만, 이를 내 나름대로 리팩토링을 진행해보기로 했다.

어떤 것을 리팩토링할까?

코드를 열어본 뒤 수정하면 좋을 것 같은 부분들을 생각해보았다.

1. 공통 타입 분리하기

api 요청시 요청, 응답에 필요한 데이터가 무엇인지 알 수 있도록 하기위해 interface를 이용해 해당 객체들에 대한 타입을 선언해두었었는데 이들이 한 파일에 함께 존재하고있었다.

// interface 선언과
export interface FloorImageType {
  buildingId?: number;
  description?: string;
  floorValue?: number;
  image?: Blob;
  floorId?: number;
}

export interface FloorImageResponseType extends FloorImageType {
  dir: string;
  uniqueNumber: string;
}

// api 요청에 대한 함수들이 한 파일에 있음
export function useGetBuildingRequest(params: BuildingQueryRequestType, isEnabled?: boolean) {
  return useQuery(
    [`/admin/building?page=${params.page}`, params],
    () =>
      httpClient<BuildingQueryResponseType>({
        method: 'GET',
        url: `/admin/building?page=${params.page}`,
      }),
    { enabled: isEnabled },
  );
}

export function useUpdateFloorImageRequest() {
  return useMutation((data: FloorImageType) => {
    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-type': 'multipart/form-data',
      },
    );
  });
}

...

 

위 코드처럼 되어있었는데 중간중간 생략을 했기때문에 짧아보이지만, 실제 코드를 보면 코드가 너무 긴 곳도 있었다. 그래서 한 곳에서만 사용하는 타입을 제외하고 여러곳에서 사용하는 타입들은 따로 분리하여 다른곳에서도 재사용 가능하도록 하고, 한곳에서 관리할 수 있게 하기로했다.

2. Custom Hook 다듬기

api를 요청하는 부분을 아래와 같이 use~를 사용한 Custom Hook 형태로 작성해두었는데 실제 요청을 수행하는 부분만 있고 해당 api의 응답에 대한 후처리는 요청을 호출한 부분에서 따로 수행하고 있었다.

// 기존 AccountApi.ts
export function useGetAdminAccountRequest(params: IAccountRequest, isEnabled?: boolean) {
  return useQuery(
    [`/admin/account/admin?page=${params.page}`, params],
    () =>
      httpClient<IAccountResponse>({
        method: 'GET',
        url: `/admin/account/admin?page=${params.page}`,
      }),
    { enabled: isEnabled },
  );
}
// 기존 AccountPage.tsx (후처리를 api 요청 함수를 호출한 곳에서 처리하고 있었음)
const [accountList, setAccountList] = useState<IAccountData[] | null>([]);
const [totalPosts, setTotalPosts] = useState(1);

// 1. api 요청 함수를 호출하고
const {
  data: accountResult,
  isError: accountError,
  refetch: accountRefetch,
} = useGetAdminAccountRequest({ page: currentPage - 1 }, false);

// 2. useEffect를 통해 api 응답에 대한 후처리를 진행
useEffect(() => {
 if (accountResult) {
   setAccountList(accountResult?.data.memberList);
   setTotalPosts(accountResult?.data.accountCount);
 } else if (accountError) {
   toast.error('회원 계정 목록을 불러오는데 실패했습니다.');
 }
}, [accountResult, accountError]);

 

두번째 "기존 AccountPage.tsx" 코드를 보면 useGetAdminAccountRequest()를 호출하고, 응답 데이터가 있다면 useEffect를 통해 accountList에 응답 데이터를 세팅해주거나 toast를 통해 에러를 표시하는 작업을 하고 있었다. 근데 생각해보니 어차피 요청하는 부분을 Custom Hook형태로 만들어두었는데 호출 외부에 useEffect를 작성해가지고 여러 api를 요청하는 페이지인 경우 코드를 복잡해보이게 할 필요가 있나 싶었다.(api를 5개 요청해야하면 useEffect가 5개 있는것임) 그래서 특정 요청에 대한 응답의 후처리는 해당 api를 요청하는 함수 안에서 함께 처리하고 결과만을 반환하도록 하여 해당 요청 함수를 호출하는 곳에서의 코드 길이를 줄이고 api를 요청하는 함수 자체를 Custom Hook으로 작성한 의미가 퇴색되지 않도록 수정하기로 했다. 물론 이렇게하면 api를 요청하는 함수의 내부 코드가 길어지겠지만, 어차피 같은 일을 하는 애들끼리 묶어져있기 때문에 이전에 한 파일에 다른 요청에 대한 후처리를 수행하는 useEffect가 여러개 남발되어있던것보다는 괜찮다고 판단했다.

3. View와 로직 분리하기

지금 코드는 AccountPage.ts라는 곳에 Account 데이터에 대한 api를 요청하는 부분, 받아온 데이터를 가공하는 부분, Modal의 상태를 조작하는 부분 등의 로직과 화면을 렌더링하는 View에 대한 코드가 아래처럼 한 파일에 존재하고있었다.

// AccountPage.ts
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<IAccountData[] | null>([]);
  const [adminCount, setAdminCount] = useState(0);

  const { setSelectedAccount } = useSelectedAccountAtom();

  const {
    data: accountResult,
    isError: accountError,
    refetch: accountRefetch,
  } = useGetAdminAccountRequest({ page: currentPage - 1 }, false);

  const {
    data: totalAccountCountResult,
    isError: totalAccountCountError,
    refetch: totalAccountCountRefetch,
  } = useGetTotalAccountCountRequest(false);

  useEffect(() => {
    void accountRefetch();
  }, [currentPage]);

  useEffect(() => {
    void totalAccountCountRefetch();
  }, []);

  useEffect(() => {
    if (accountResult) {
      setAccountList(accountResult?.data.memberList);
      setTotalPosts(accountResult?.data.accountCount);
    } else if (accountError) {
      toast.error('회원 계정 목록을 불러오는데 실패했습니다.');
    }
  }, [accountResult, accountError]);

  useEffect(() => {
    if (totalAccountCountResult) {
      setAdminCount(totalAccountCountResult?.data.adminCount);
    } else if (totalAccountCountError) {
      toast.error('총 계정 수를 불러오는데 실패했습니다.');
    }
  }, [totalAccountCountResult, totalAccountCountError]);

  const handleAccountManageModal = (account: IAccountData) => {
    setSelectedAccount(account);
    setIsModalOpen(true);
  };

  // 뷰가 함께있음
  return (
    <RightContainer title={'계정 관리'}>
      <BanAlertDialogModal
        isAlertOpen={isAlertOpen}
        setIsAlertOpen={setIsAlertOpen}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
      <AccountManageModal
        isModalOpen={isModalOpen}
        setIsModalOpen={setIsModalOpen}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
      <Box mb="10px" fontWeight="600" color="#25549c">
        관리자 계정 관리 (총 계정 수: {adminCount})
      </Box>
      <TableContainer>
        <Table variant="simple">
          <Thead>
            <Tr>
              <Th>계정 이름</Th>
              <Th>계정 이메일</Th>
              <Th>계정 권한</Th>
              <Th>계정 생성일</Th>
              <Th>가입 승인 상태</Th>
              <Th textAlign={'center'}>계정 관리</Th>
            </Tr>
          </Thead>
          <Tbody>
            {accountList &&
              accountList.map((account) => (
                <Tr key={account.id}>
                  <Td>{account.name}</Td>
                  <Td>{account.email}</Td>
                  <Td> {account.admin ? 'Admin' : account.manager ? 'Manager' : account.staff ? 'Staff' : ''}</Td>
                  <Td>{account.createAt?.slice(0, 10)}</Td>
                  <Td>{account.ban ? <BanState>미승인</BanState> : <div>승인</div>}</Td>
                  <Td textAlign={'center'}>
                    <SettingsIcon cursor={'pointer'} onClick={() => handleAccountManageModal(account)} />
                  </Td>
                </Tr>
              ))}
          </Tbody>
        </Table>
      </TableContainer>
      <Pagination
        totalPosts={totalPosts * 10}
        postPerPages={10}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
    </RightContainer>
  );
}
export default AdminAccountPage;

 

AccountPage.tsx와 같이 Page를 의미하는 곳에는 View에 대한 코드만 놔두고, api 요청, 데이터 가공, Modal 상태관리와 같은 로직들은 useAccount.tsx라는 파일을 생성한 후 hook으로 분리하여 사용할 수 있게 수정하기로했다.

4. interface라는 것을 명확하게 나타내기

타입 위에 마우스 커서를 올려보면 interface라고 나오기 때문에 굳이 접두사에 I를 붙이지 않아도 됐을것 같다는 생각이 들었다. 하지만, 커서를 올려보지 않고도 변수만 딱 봤을때 명시적으로 얘는 interface다를 알려주기위해 붙이기로했다.

 

2탄에서는 실제 리팩토링을 진행하고 어떻게 바뀌었는지를 보여주는것을 주제로 돌아오겠다.

LIST