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 };
}
이렇게 분리하고 나니 에러가 있을 때 확실히 어느 부분으로 가서 수정해야 하는지 빠르게 인지가 가능했고, 가독성도 좋아졌다는 동료 개발자의 리뷰가 있었다☺️
바로 깔끔하게 코드를 작성할 수 있으면 좋겠지만,,ㅎㅎ 아직 그정도의 실력이 아니니 리팩토링을 힘들어하지 않고 이번처럼 코드 분리를 하는 시간이 꼭 필요하다는 생각이 들었다. 추가 기능을 구현할 때도 깔끔하게 분리되어 있으면 어려움 없이 작성할 수 있겠다는 자신감도 생겼던 것 같다.