UI와 비즈니스 로직 분리하기

cho·2025년 1월 25일

💥 트러블 슈팅

목록 보기
4/11
post-thumbnail

UI와 비즈니스 로직을 분리해야겠다고 생각하게 됐던 계기는 한 파일 안에 너무 많은 코드가 있을 경우 내 코드지만 읽기 싫다는 생각이 강하게 들어서였다.

항상 책임분리와 깔끔한 코드 작성이 유지보수와 가독성에 큰 영향을 미친다는 것을 들었음에도 기능구현을 하고나면 리팩토링 하는 것이 숙제와 같이 느껴져 제대로 하지 못했던 것 같다.

그래서 이번에 온라인 주식투자 사이트를 개발하면서 좀 더 명확하게 코드분리를 하여 코드의 품질을 높여보고 싶다는 생각이 들었고, 이를 어떻게 진행했는지 써보고자 한다.

export default function EditCancel() {
  const { stockName } = useStockInfoContext();
  const { token, isAuthenticated } = useAuth();
  const { showToast } = useToast();
  const queryClient = useQueryClient();
  const { data: limitOrderData } = useQuery({
    queryKey: ["limitOrder"],
    queryFn: () => getTrade(token, stockName),
    enabled: !!isAuthenticated && !!token,
  });
  const [selectedOrders, setSelectedOrders] = useState<string[]>([]);
  const [isEditForm, setIsEditForm] = useState(false);
  const [isCancelTable, setIsCancelTable] = useState(false);

  const findOrder = (orderId: string) =>
    limitOrderData?.find((data) => data.OrderId.toString() === orderId);

  const toggleOrderSelection = (orderId: string) => {
    setSelectedOrders((prev) =>
      prev.includes(orderId) ? prev.filter((id) => id !== orderId) : [orderId],
    );
  };

  const handleEdit = () => {
    setIsEditForm(true);
    if (selectedOrders.length > 0) {
      setIsEditForm(true);
    } else {
      showToast("정정할 데이터를 선택해주세요.", "error");
    }
  };

  const handleCancel = () => {
    if (selectedOrders.length > 0) {
      setIsCancelTable(true);
    } else {
      showToast("취소할 데이터를 선택해주세요.", "error");
    }
  };
  
  const { mutate: cancelTradeMutate } = useMutation({
    mutationFn: cancelTrade,
    onSuccess: () => {
      showToast("주문이 취소되었습니다", "success");
      queryClient.invalidateQueries({ queryKey: ["limitOrder"] });
    },
    onSettled: () => {
      setIsCancelTable(false);
    },
  });

  const { mutate: modifyTradeMutate } = useMutation({
    mutationFn: modifyTrade,
    onSuccess: () => {
      showToast("주문을 수정했습니다.", "success");
      queryClient.invalidateQueries({ queryKey: ["limitOrder"] });
    },
    onSettled: () => {
      setIsEditForm(false);
      setSelectedOrders([]);
    },
  });

  const handleCancelConfirm = (orderId: string) => {
    cancelTradeMutate({ token, orderId });
  };

  if (isEditForm) {
    return <div>수정</div>;
    const order = findOrder(selectedOrders[0]);
    return (
      <Trade type="edit" defaultData={order} handleMutate={modifyTradeMutate} />
    );
  }

  if (isCancelTable) {
    return (
      <>
        {selectedOrders.map((orderId) => {
          const order = limitOrderData?.find(
            (data) => data.OrderId.toString() === orderId,
          );
          const order = findOrder(orderId);
          return order ? (
            <TransactionTable
              key={orderId}
              color="green"
              submittedData={{
                stockName: order.stockName,
                count: order.remainCount,
                bidding: order.buyPrice,
                totalAmount: order.remainCount * order.buyPrice,
              }}
              onClickGoBack={() => setIsCancelTable(false)}
              onClickConfirm={() => {
                handleCancelConfirm(orderId);
              }}
            />
          ) : (
            "취소 정보를 받아오지 못했습니다."
          );
        })}
      </>
    );
  }

이런식으로 대부분의 파일들이 ui와 데이터와 관련된 함수들이 모두 있었다. 내가 생각했을 때는 책임분리도 잘 되지 않고 가독성도 떨어진다고 생각되어 이를 해결하기 위해 커스텀 훅을 활용해 분리를 해보았다.

아래와 같이 tanstack query를 사용하여 데이터를 다루는 부분을 useLimitOrderData와 useOrderMutations로 분리하였다. 수정 / 취소 / 취소확인 핸들러는 ui 로직과 함께 있는게 맞다고 생각되었다.


export default function EditCancel() {
  const [selectedOrders, setSelectedOrders] = useState<string[]>([]);
  const [isEditForm, setIsEditForm] = useState(false);
  const [isCancelTable, setIsCancelTable] = useState(false);

  const { showToast } = useToast();
  const { token } = useAuth();
  const {
    data: limitOrderData,
    isLoading,
    isPending,
    findOrderById,
  } = useLimitOrderData();
  const { cancelTradeMutation, modifyTradeMutation } = useOrderMutations();

  const handleEdit = () => {
    if (selectedOrders.length > 0) {
      setIsEditForm(true);
    } else {
      showToast("정정할 데이터를 선택해주세요.", "error");
    }
  };

  const handleCancel = () => {
    if (selectedOrders.length > 0) {
      setIsCancelTable(true);
    } else {
      showToast("취소할 데이터를 선택해주세요.", "error");
    }
  };

  const handleCancelConfirm = (orderId: string) => {
    cancelTradeMutation.mutate({ token, orderId });
    setIsCancelTable(false);
  };

  if (isLoading || isPending) {
    return <LoadingSpinner className="mt-230" />;
  }

  if (isEditForm) {
    const order = findOrderById(selectedOrders[0]);
    return (
      <BuyAndSell
        type={TradeType.Edit}
        defaultData={order}
        handleMutate={modifyTradeMutation.mutate}
      />
    );
  }

  if (isCancelTable) {
    return (
      <>
        {selectedOrders.map((orderId) => {
          const order = findOrderById(orderId);
          return order ? (
            <TradeTable
              key={orderId}
              color="green"
              submittedData={{
                stockName: order.stockName,
                count: order.remainCount,
                bidding: order.buyPrice,
                totalAmount: order.remainCount * order.buyPrice,
                buyOrder: order.type,
              }}
              onClickGoBack={() => setIsCancelTable(false)}
              onClickConfirm={() => {
                handleCancelConfirm(orderId);
              }}
            />
          ) : (
            "취소 정보를 받아오지 못했습니다."
          );
        })}
      </>
    );
  }

  return (
    <>
      <EditTable
        limitPriceHistory={limitOrderData}
        selectedOrders={selectedOrders}
        setSelectedOrders={setSelectedOrders}
      />
      {limitOrderData && limitOrderData.length > 0 && (
        <div className="mt-20 text-center">
          <Button
            variant="custom"
            className="mr-10 w-120 bg-lime-500 text-black hover:bg-lime-500/80"
            onClick={handleEdit}
          >
            정정
          </Button>
          <Button
            variant="custom"
            className="w-120 bg-red-500 hover:bg-red-500/80"
            onClick={handleCancel}
          >
            취소
          </Button>
        </div>
      )}
    </>
  );
}
import { useQuery } from "@tanstack/react-query";

import { getTrade } from "@/api/transaction";
import { useStockInfoContext } from "@/context/stock-info-context";
import { useAuth } from "@/hooks/use-auth";

export default function useLimitOrderData() {
  const { stockName } = useStockInfoContext();
  const { token, isAuthenticated } = useAuth();

  const queryResult = useQuery({
    queryKey: ["limitOrder", stockName],
    queryFn: () => getTrade(token, stockName),
    enabled: !!isAuthenticated && !!token,
  });

  const findOrderById = (orderId: string) =>
    queryResult.data?.find((data) => data.OrderId.toString() === orderId);

  return {
    ...queryResult,
    findOrderById,
  };
}
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { cancelTrade, modifyTrade } from "@/api/transaction";
import { useToast } from "@/store/use-toast-store";

export default function useOrderMutations() {
  const { showToast } = useToast();
  const queryClient = useQueryClient();

  const cancelTradeMutation = useMutation({
    mutationFn: cancelTrade,
    onSuccess: () => {
      showToast("주문이 취소되었습니다", "success");
      queryClient.invalidateQueries({ queryKey: ["limitOrder"] });
    },
    onError: (error: Error) => {
      showToast(error.message, "error");
    },
  });

  const modifyTradeMutation = useMutation({
    mutationFn: modifyTrade,
    onSuccess: () => {
      showToast("주문을 수정했습니다.", "success");
      queryClient.invalidateQueries({ queryKey: ["limitOrder"] });
    },
    onError: (error: Error) => {
      showToast(error.message, "error");
    },
  });

  return { cancelTradeMutation, modifyTradeMutation };
}

이렇게 분리하고 나니 에러가 있을 때 확실히 어느 부분으로 가서 수정해야 하는지 빠르게 인지가 가능했고, 가독성도 좋아졌다는 동료 개발자의 리뷰가 있었다☺️

바로 깔끔하게 코드를 작성할 수 있으면 좋겠지만,,ㅎㅎ 아직 그정도의 실력이 아니니 리팩토링을 힘들어하지 않고 이번처럼 코드 분리를 하는 시간이 꼭 필요하다는 생각이 들었다. 추가 기능을 구현할 때도 깔끔하게 분리되어 있으면 어려움 없이 작성할 수 있겠다는 자신감도 생겼던 것 같다.

0개의 댓글