사실 업무를 하면서 리팩토링 하고 싶은 부분이 보이고 조금 더 시간을 두고 구조를 설계하고 싶지만
대부분의 업무가 개발자에게 그렇게 많은 시간을 주지는 않습니다.
그래서 사실 더 좋은 구조가 생각난다 하더라도 조금은 뒤로 미루며 기능개발을 할때가 많습니다. 그러다보니 더 좋은 구조에서 더 생각해보고 발전할 수 있는 방향을 모색하고 싶은 욕구가 있지만 포기하기도 하고 개발에 몰두하다보니 얼렁뚱땅 넘어가기도 하는데요.
이번 과제에서 그런 부분들이 어느 정도 해결된 것 같고 더 나아가서 제가 원했던 효율적인(제가 생각하기에) 구조 (OCP 와 전략패턴)에 대해서 공부해 볼수 있었습니다.
이번 발제 팀과제에서도 순수함수와 액션함수의 정의, 그리고 그것을 나누는 작업들을 했었는데요. 하면서 이걸 나누면 어떤 이득이 있는지 곰곰이 생각해봤습니다.
단위 테스트 용이성, 함수단일책임, 가독성, 확장성, 유지보수성 등의 이득을 갖고올수 있을거란 확신이 들었습니다.
사실 저는 업무를 하면서 함수단일책임을 한번도 크게 고려해보며 코드를 짠 적이 없습니다.
그렇다보니 테스트할때는 여러 변수들을 고려해야하며, 재사용성은 꿈도 꾸지 못했습니다. (컴포넌트에 여러 책임과 함수가 존재하는데 그게 재사용이 될리가요 ㅎ)
하지만 이번 과제를 하면서 배운것과 경험한 것을 토대로 실무에 점진적으로 적용할 수 있겠단 큰 자신감을 얻었습니다 ^^
제가 업무할때 코드 형식들이 정말 이번 5주차 과제의 /components/CartPage.tsx랑 비슷합니다.
계산함수와 액션함수의 혼용, 분리되지 UI 컴포넌트. 이렇게 코드를 짜다보면 에러가 생겼을때 디버깅이 까다롭습니다.
1번 변수가 문제인지 2번 변수가 문제인지 둘다 확인해야 합니다 -> 유지보수성, 테스트 용이성
함수가 단일책임이 아니기 때문에 해당 함수에서 어떤 책임을 가지고 있는지 불분명하고, 확장성 또한 매우 낮습니다.
현재 수정한 CartPage.tsx는 대략 모습이 이렇습니다.
const CartPage = () => {
return (
<ItemList addToCart={addToCart} cart={cart} products={products} />
<CartList removeFromCart={removeFromCart} updateQuantity={updateQuantity} cart={cart} />
<UsingCoupon coupons={coupons} selectedCoupon={selectedCoupon} applyCoupon={applyCoupon} />
<OrderSummary totalBeforeDiscount={totalBeforeDiscount} totalAfterDiscount={totalAfterDiscount} totalDiscount={totalDiscount} />
)
}
어떻게 이렇게 만들었는지 한번 복기해보겠습니다.
CartPage를 기준으로 보면 cart 폴더와 coupon 폴더로만 나눴습니다. cart를 주로 데이터로 쓰는 UI와 coupon을 주로 UI로 사용하는 기준으로 나눴습니다
순수함수와 액션함수를 나눠야한다는 생각이 있었는데
루트 폴더에서 hooks 폴더안에 hook 관련한 함수를 전부 넣었습니다.
(데이터를 변경하는 액션 함수) hooks/useCart.ts에서 선언한 훅은 CartPage.tsx에서 한번만 호출하면 데이터 일관성을 유지하고
CartPage의 다른곳에서 쓰는 데이터는 props로 넘겨주었습니다.
utils 폴더와 entities에는 순수함수만 넣어서 외부변수의 영향을 받지 않는 함수들로만 정리했습니다.(특정 도메인에 속해있냐 전역적으로 쓸수있냐에 따라 나눔. 도메인에 속한 객체)
features 폴더는 검색해보니 다른 함수들끼리 합쳐진? 예를들어,,, cart 와 coupon을 같이 사용하는 함수라던가,, 융합된 녀석들이 쓰이는 느낌인것 같았습니다. 그래서 필요없다고 생각해서 안만들었습니다.
각 책임별로 나눠보니 다시한번 cart 폴더는 네가지 컴포넌트로 나눌수 있다는게 눈에 보이기 시작했습니다.
cart-list, item-list, order-summary, using-coupon 네 가지로 나눌 수 있고 네 가지를 들어가보면 전부 하나의 UI만 책임지고 있다는 것이 보입니다.
또 만약 item-list나 cart-list를 다른 곳에서 써야한다고 가정했을 떄 import CartList 후에 CartList에서 필요한 데이터만 불러와주면 됩니다. (질문1) AdminPage.tsx line17
hooks 폴더 관련해서는 루트폴더에서의 hooks 폴더만으로는 각각 컴포넌트에서의 훅을 전부 다 표현하기가 어렵다고 생각했습니다.
그 이유는 refactoring/admin/coupon/add-coupon/hook.ts 를 보면 알수 있는데요.
루트 폴더에서의 hook은 많은 컴포넌트에서 사용하는 훅으로써 전역적으로 관리할 수 있는 훅이고, 컴포넌트 단위에서 선언된 훅은 해당 도메인에서만 사용된다는 걸 암시해줄 수 있는 부분이 있습니다. (앞으로도 이런 생각을 가지고 폴더구조를 짜면 스스로도 잘 이해가 될것 같습니다)
useLocalStorage 라이브러리 훅을 만들어봤고 수정은 복잡해서 저장/삭제 기능만 구현했습니다.
OCP 활용을 위해 FieldValidator 개념 추가해봤습니다. OCP란 Open & Closed Principal 이라는 건데
확장엔 오픈, 수정엔 닫혀있다는 뜻으로 수정할 땐 다 고치는게 아니라 딱 그 개념만 수정하고, 확장은 여러곳에서 가능한.
const fieldValidator: Record<keyof Product, FieldValidator> = {
price: (value) => typeof value === 'number' && value % 100 === 0,
stock: (_) => true,
name: (_) => true,
discounts: (_) => true,
id: (_) => true,
}
OCP를 충족하기 위해 전략패턴을 사용하고자 했습니다. 전략패턴이란 행동을 추상화하고 여러 구현을 교체/수정 가능하게끔 만드는 건데요.
stock에 대한 전략을 현재는 true만 나오도록 되어있지만 언제든 '전략' 추상화와 교체가 가능합니다.
제가 직접 만든 fieldValidator를 예시로 보면 수정할때는 fieldValidator의 속성 하나만 추가/수정 해준다면
handleAllEdit 함수의 수정없이 적용이 가능합니다.
const handleAllEdit = (field: keyof Product, value: string | number) => {
const validator = fieldValidator[field];
if(!validator(value)) {
alert(fieldErrorMessages[field]);
return;
}
이렇게 전략패턴을 사용하면 수정 / 추가가 매우 간단하다는 생각이 들었고 무엇보다도 테스트 용이성이 매우매우매우 올라갑니다.
이유는 수정이 필요한 부분이 딱 한 곳이기 때문에 다른 코드의 영향을 고려하지 않아도 되기 때문입니다. (다시 한번 크게 깨닫고 갑니다)
하지만 단점으로는 처음 OCP와 전략패턴의 맛을 본 저로썬 처음 설계할때 시간이 꽤 들것 같습니다 ㅎㅎ 그럼에도 불구하고 계쏙 시도해볼 생각입니다.
마지막으로 tc에 대해서 알수 있어서 좋았습니다. 과제안내에서 쉬운 코드는 테스트코드도 짜기 쉽다고 하셨는데 그 이유를 짜면서 바로 알았습니다. 만약 컴포넌트가 단일책임이 아니었고, 순수함수와 액션함수의 뒤죽박죽 섞인 코드였다면 테스트 코드를 적는 저로써도 테스트가 원하는 방향대로 진행되지 않을 때, 어디서부터 디버깅을 해야하는지 정말 막막했을 것 같습니다. 기존 테스트 코드들을 참고하며 짜보니 크게 어렵지 않았고 여러 참고자료를 보면서 했더니 테스트 코드는 금방 짰던 것 같습니다.
테스트코드를 만약 실무에서도 쓴다면 어떤 이점이 있을까 생각해봤는데 아닐수도 있지만 QA 하는 부분 ex) 로그인, 회원가입 과정을 직접 해가며 바뀐 부분들에 대해 체크)도 크게 시간을 절약할 수 있을 것 같고 (제 회사는 인력이 몇명 없기에 저도 개발하며 QA를 조금? 하고 PM도 하십니다) 테스트 코드가 잘 짜여질 수 있다는건 클린코드라는 반증일수도 있겠단 생각이 들었습니다.
이번 과제 너무 재밌었습니다.
질문
AdminPage.tsx line17
음 재활용 컴포넌트에 대한 질문인데요.
CartList는 cart, removeFromCart, updateQuantity 3개의 props가 필요한 컴포넌트인데 이걸 예를들어 admin에서 재사용한다고 했을 때 props들은 AdminPage에서 재정의하는게 좋을까요 아니면 App.tsx에서 props로 던져주는게 좋을까요?
회사별로 정하는 방법이 있는지 아니면 확실하게 좋은방법이 있는건지 궁금합니다.