
이전 포스팅 [프론트엔드 비즈니스로직, 뷰로직 테스트에 대하여] 에 이은 내용입니다. 굳이 선행되어야 이해할 수 있는 글은 아니지만 보고온다면 비즈니스 로직을 왜 분리해야하는지 정도는 감을 잡으실 것이라고 생각합니다.
습관처럼 작성하는 코드?
코드를 짤때 가장 많이 고려되는 것이 무엇일까요? 테크 회사라면 다를 수 있지만 주니어만 있거나 개발자가 많지 않은 곳에서는 보통 “빨리 짤 수 있는” 에 초점이 맞춰져 있는 경우가 많습니다. 기능정의, 아키텍처 설계/분리 가 이루어지지 않은채 “빨리 짤 수 있는”코드는 과연 유지보수에 유리할까요?
추가로, “빨리 짤 수 있는” 코드를 계속 작성하다보면 우리는 이 방법에 익숙해질 것이고, 결국 빠른 결과물을 낼 순 있겠지만 완성도 있는 결과물을 주긴 어려울 것입니다. 습관이 된다 라고 한다면 더 좋은 습관을 기르는게 좋지 않을까요? 우리는 아키텍처 관점에서 코드를 짜고, 습관화 할 수 있도록 공부를 진행할 것입니다.
비즈니스 로직 + 뷰 로직
import { useEffect, useState } from "react"; import { mockProductList } from "../mockProduct"; import ListForm from "./ListForm"; import CartItem from "./CartItem"; interface CartType extends ProductType { quantity: number; } const OldCode = () => { const [productList, setProductList] = useState<ProductType[]>([]); const [cartList, setCartList] = useState<CartType[]>([]); useEffect(() => { (() => { setProductList(mockProductList); })(); }, []); const addToCart = (product: ProductType) => { const findIndex = cartList.findIndex((i) => i.id === product.id); if (findIndex !== -1) { const addProduct = { ...cartList[findIndex], quantity: cartList[findIndex].quantity + 1, }; setCartList((i) => i.map((j) => (j.id === product.id ? addProduct : j))); return; } setCartList((i) => [...i, { ...product, quantity: 1 }]); }; const removeProduct = (product: ProductType) => { setCartList((i) => i.filter((j) => j.id !== product.id)); }; return ( <div className="flex flex-col items-center mt-12"> <h2 className="text-3xl font-bold text-center">Product List</h2> <p className="text-sm text-center mb-4">상품 목록</p> <ListForm onClick={addToCart} listItem={productList} /> <h2 className="text-xl text-center font-bold mb-2">장바구니</h2> {cartList.map((cartProduct) => ( <CartItem key={cartProduct.id} onClick={removeProduct} cartProduct={cartProduct} /> ))} </div> ); }; export default OldCode;
이런 코드가 있다고 가정해봅시다. 우리는 상품을 불러와서 카트에 담는 비즈니스 로직과 뷰 로직을 작성했습니다. 지금은 단순히 카트 추가, 제거 로직만 있어서 읽기 편할 수 있지만 여기에 다른 기능들이 붙는다고 하면 코드의 길이는 방대해질 것 입니다. 과연 이러한 로직이 관리에 용이할까요?
또 테스트 코드를 짜기에도 명확한 기준이 없습니다. 어디까지가 비즈니스 로직 테스트인지, 어디까지가 뷰 로직 테스트인지.. 명확한 테스트는 중복된 테스트를 할 필요가 사라질 것입니다.
예를들어 위 코드처럼 작성한다면 다른 페이지에서 장바구니 추가페이지가 생긴다면 똑같은 로직을 다시 그 페이지에 추가하게 될 것입니다. 우리는 테스트 커버리지를 올려야하므로 해당 페이지에서도 테스트 코드를 작성하게되겠죠?
불필요한 중복 코드와 중복 테스트가 생기게 되는 것입니다. 그렇다면 위 코드를 어떻게 변경할 수 있을까요?
비즈니스 로직 분리?
// product.ts export default class Product { readonly productId: string; readonly name: string; readonly price: number; readonly description: string; readonly thumbnail: string; constructor({ productId, name, price, description, thumbnail, }: { productId: string; name: string; price: number; description: string; thumbnail: string; }) { this.productId = productId; this.name = name; this.price = price; this.description = description; this.thumbnail = thumbnail; } priceFormat(count: number = 1) { return Intl.NumberFormat("ko-KR").format(this.price * count); } } // cart.ts import Product from "../product/Product"; export type CartType = { product: Product; quantity: number; }; export default class Cart { readonly items: CartType[] = []; constructor({ items = [] }: { items?: CartType[] } = {}) { this.items = items; } addProduct(product: Product) { const findIndex = this.items.findIndex((cart) => cart.product.productId === product.productId); if (findIndex !== -1) { return this.editQuantity(product.productId); } return new Cart({ items: [ ...this.items, { product, quantity: 1, }, ], }); } editQuantity(id: string, isUpQuantity: Boolean = true) { const findIndex = this.items.findIndex((cart) => cart.product.productId === id); if (findIndex !== -1) { if (isUpQuantity) { this.items[findIndex].quantity += 1; } else { this.items[findIndex].quantity -= 1; } } return new Cart({ items: this.items }); } totalCount() { return this.items.reduce((total, item) => total + item.quantity, 0); } removeProduct(productId: string): Cart { const newItems = this.items.filter((item) => item.product.productId !== productId); return new Cart({ items: newItems }); } getTotalPrice() { return this.items.reduce((total, item) => total + item.product.price * item.quantity, 0); } getItems(): CartType[] { return this.items; } }
위에서 필요로 하는 비즈니스 로직을 담은 class를 생성해줍니다. cart class에는 product class 를 주입해주고, cart class에서 method를 호출해서 사용하는 로직일 것입니다.
// cart.test.ts import { productMockData } from "../product/product.test"; import Cart from "./Cart"; describe("Cart Domain Test", () => { const duplication = "231"; const productIds = [duplication, "12", "134", "534", duplication, "50"]; const cart = productIds.reduce((prevCart, productId) => { return prevCart.addProduct(productMockData({ productId })); }, new Cart()); it("카트 수량 확인하기", () => { const duplicationCount = cart.getItems().find((i) => i.product.productId === duplication)?.quantity; expect(cart.totalCount()).toBe(6); expect(duplicationCount).toBe(2); }); it("카트 수량 줄이기", () => { const newCart = cart.editQuantity(duplication, false); const findProduct = cart.getItems().find((i) => i.product.productId === duplication); expect(newCart.totalCount()).toBe(5); expect(findProduct?.quantity).toBe(1); }); it("카트 삭제하기", () => { const newCart = cart.removeProduct(duplication); expect(newCart.totalCount()).toBe(4); expect(newCart.getItems().find((i) => i.product.productId === duplication)).toBeUndefined(); }); it("금액확인", () => { const mockPrices = [1000, 2000, 3000, 4000, 1000, 5000]; const newCart = productIds.reduce((prevCart, productId, index) => { return prevCart.addProduct(productMockData({ productId, price: mockPrices[index] })); }, new Cart()); const totalPrice = mockPrices.reduce((prev, cur) => prev + cur, 0); expect(newCart.getTotalPrice()).toBe(totalPrice); }); });
이렇게 작성된 비즈니스 로직은 따로 테스트코드 한번으로 견고하게 만들 수 있습니다. 이러면 장바구니 로직이 필요한 곳에서 받아 사용하고, 뷰 로직에만 신경쓰면 되게되죠.
그럼 중복된 코드도 줄이고, 중복된 테스트도 줄어들 것입니다.
어떤가요? 아키텍처 설계는 사실 개발하기전에 정말 중요한 파트입니다. 시간도 많이 들지만 그만큼 처음에 견고한 코드는 유지보수와 완성도 면에서 최고의 효율을 발휘할 것입니다.
습관처럼 작성하는 코드가 테스트 기반 비즈니스로직 분리라면 우리는 앞으로 적은 시간으로 많은 효율을 낼 수 있을 것입니다. 처음이 어렵지 하다보면 괜찮아 질 것입니다.!
위 코드를 비즈니스 로직 + 뷰 로직을 합친 코드와 / 분리한 코드 / 테스트 코드까지 Github에 올려두었습니다.

별건 아니지만 예제 코드와 예제 테스트를 본다면 좀 더 이해가 쉽지 않을까 생각이 됩니다.
다음 포스팅엔 테스트 코드 심화버전으로 돌아오려고 합니다. context나 mock을 활용한 테스트 코드 작성에 대해 설명하고, 유용한 라이브러리 (ex. 토스의 useOverlay) 등의 테스트 코드 작성법 등에 대해서도 알아보죠!
두서없는 글 읽어주셔서 감사합니다.