과제의 핵심취지
- React의 hook 이해하기
- 함수형 프로그래밍에 대한 이해
- 액션과 순수함수의 분리
과제에서 꼭 알아가길 바라는 점
- 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
- 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
- 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
- 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)
기본과제
-
Component에서 비즈니스 로직을 분리하기
-
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
-
뷰데이터와 엔티티데이터의 분리에 대한 이해
-
entities -> features -> UI 계층에 대한 이해
-
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
-
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
-
계산함수는 순수함수로 작성이 되었나요?
-
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
-
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
-
계산함수는 순수함수로 작성이 되었나요?
-
특정 Entitiy만 다루는 함수는 분리되어 있나요?
-
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
-
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
-
이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.
-
어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.
-
Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.
-
Context나 Jotai를 사용해서 전역상태관리를 구축했나요?
-
전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?
-
도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?
-
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
과제를 하면서 알게 된 점과 좋았던 점
책임의 분리'에서 시작된 도메인 주도 설계 학습 저번 과제와 이번 과제를 하면서 '책임의 분리'를 고민하다 자연스럽게 도메인 주도 설계에 대해 얕게나마 공부하게 됐다. 평소 "이걸 왜 이렇게까지 해야 하지?" 했던 부분들을 직접 경험하며 깨달을 수 있어 좋았다.
이번 경험을 통해 방법론과 디자인 패턴을 공부하는 이유가 문제를 체계적으로 분석하고, 유지보수성과 확장성을 고려한 설계를 하기 위함이라는 걸 알게 됐어. 다양한 패턴을 알면 여러 관점에서 문제를 고민해 더 좋은 설계를 할 수 있다는 인사이트도 얻었다.
역할 중심 vs. 도메인 중심 구조 기존 회사 코드는 역할 중심 구조로 되어 있었다. 각 역할별 관심사를 분리하는 데는 효과적이었지만, 도메인 코드가 흩어져 있어 기능 단위로 전체 흐름을 파악하기 어려운 단점이 있었다.
반면, 이번 과제에 적용하려고 시도한 도메인 중심 구조는 관련 기능, 타입, API 호출, UI 컴포넌트 등이 한곳에 모여 응집도가 높다. 덕분에 특정 도메인에 집중하기 쉽고, 유지보수나 확장 시에도 훨씬 효율적이어 보인다.
물론 프로젝트 규모나 팀 상황에 따라 적합한 구조는 달라질 수 있다. 도메인 중심 설계가 무조건 정답은 아니지만, 대부분의 프로덕트에서는 이 구조가 더 적절할 거라 생각한다.
성장 포인트 평소엔 프로젝트 구조를 직접 설계할 기회가 적어 늘 정해진 구조 안에서 코드를 작성하다 보니 "왜 코드 파악이 어렵지?"라는 고민만 계속했었다.
이번 과제를 통해 그 고민을 직접 해결해볼 수 있어 좋았고, 직접 문제를 해결하는 과정 속에서 배움의 속도가 빨라지는 걸 느꼈다.
다양한 방법론과 디자인 패턴의 중요성을 깨닫고, 여러 관점에서 문제를 바라보는 시야를 얻게 된 이번 과제는 나에게 매우 의미 있는 경험이었다.
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
책임의 분리 책임 분리에 가장 신경을 썼다.
처음에는 단순하게 가고 싶었다. components, features, layout 정도로 1depth로 시작해서 점점 쌓아가자는 식이었다.
근데 막상 "상품 관리" 같은 탭이 생기니까 애매해졌다.
Form은 어디에 둬야 하지? 그냥 UI 컴포넌트인가? 아니면 이 자체가 비즈니스 단위의 feature인가?
Form + Table + Submit 로직이 하나의 덩어리로 움직일 땐 이걸 ProductManagePage 같은 단위로 묶는 게 나은가?
이 과정에서 "이 파일은 어떤 책임을 가지는가?"라는 질문을 계속 던지게 됐다. 단순히 뷰만 그리는 컴포넌트인지, 아니면 도메인 지식이 들어가는지.
결국 지금은 책임의 구분이 더 중요하다는 걸 느꼈고, 어떤 파일이 어떤 레이어의 역할인지 명확히 하려고 노력 중이다.
formatPrice의 위치
const formatPrice = (price: number, productId?: string): string => { ... };
처음엔 단순한 가격 포맷팅 함수라고 생각했는데, 알고 보니 아니다.
이 함수는 productId가 있으면 재고 확인도 하고, 관리자인지에 따라 포맷도 다르게 뿌려준다. 즉, 도메인 정보를 엄청 물고 있다.
그래서 이걸 어디 둬야 할까를 두고 엄청 고민했다.
models/price? → 가격 느낌은 맞지만 product, cart 다 들고 오면 애매함productUtils? → 네이밍은 괜찮은데, formatPrice가 utility스럽지 않음domain/product? → 거기 두면 비즈니스 로직처럼 보여서 부담됨
결국 이건 비즈니스와 프레젠테이션의 어중간한 사이에 있는 함수였고, 그래서 더더욱 "위치"에 대한 고민이 깊어졌다.
findById도 위치 고민 대상이었다
const product = products.find((p) => p.id === productId);
이건 그냥 배열에서 찾는 건데도, 이걸 어디에 둘지 애매했다.
models에 두기엔 너무 단순하고, utils로 뺄까 싶다가도 products라는 데이터 자체에 강하게 의존한다는 생각이 들었음.
"그럼 결국 이걸 분리할 의미가 있나?"라는 의문에 도달했었고, 지금은 그냥 책임이 명확한 class나 service 구조가 있으면 이런 고민 줄어들겠다는 결론에 가까워졌다.
상태 업데이트와 유효성 검사, 그리고 throw Error
addToCart 로직을 구현하면서 가장 처음 든 생각은 단순했다.
"그냥 setCart 안에서 재고 체크하고 부족하면 throw 하면 되는 거 아냐?"
setCart((prev) => {
const remaining = getRemainingStock(product, prev);
if (remaining <= 0) throw Error("재고 없음");
return [...prev, product];
});
근데 곧 문제가 생긴다.
setState의 updater 함수는 기본적으로 순수해야 한다고 생각한다.
여기서 throw를 던지면 리액트 내부에서 무슨 일이 일어날지 장담 못한다.
게다가, 여기서 예외를 던지면 외부에서 잡기도 애매하다.
→ 결론: ❌ setCart 내부에서는 throw하지 말자.
그럼 외부에서 먼저 검사하고 넘기면 될까?
if (getRemainingStock(product) <= 0) {
alert("재고 없음");
return;
}
setCart(...);
이건 또 완전히 안전하지 않다.
state batching 이나 여러 탭에서 동시 접근이 있는 상황에서는, 외부에서 본 재고와 실제 상태가 달라질 수 있다.
즉, 검사는 외부에서도 하고, 내부에서도 한 번 더 해야 한다.
이중 체크가 필요한 구조가 됐다.
useTask로 정리한 이유
이렇게 유효성 검사 → 상태 업데이트 → 알림까지 흐름을 구성하고 나면, 부수효과 처리도 애매해진다.
setCart 안에서 알림 띄우면 안 되고, 바깥에서 try-catch로 잡자니 에러 핸들링 구조가 지저분하다.
그래서 useTask라는 훅을 만들었다.
const addToCart = (product) => {
const remaining = getRemainingStock(product);//getRemainingStock은 순수함수
if(remaining <= 0) throw Error("재고 부족");
addToCart(product);
}
const addToCartTask = useTask(
addToCart,
{
onSuccess: () => addNotification("장바구니에 담겼습니다", "success"),
onError: (e) => addNotification(e.message, "error"),
}
);
이렇게 작성하니 비즈니스 로직과 사이드 이펙트가 분리되는 느낌이라 개인적으로 나쁘진 않아보인다.
성공했을 때, 실패했을 때 UI에 어떤 반응을 줄지도 명확히 설정할 수 있고, 재사용성도 높다고 생각한다.
정리
- 작은 로직이라도 도메인 객체에 의존하면 위치 고민이 생긴다
setState내부는 순수해야 하고, side effect는 외부에서 처리하는 게 맞다- 상태를 업데이트하기 전에 검사해야 할 로직은, 내부 + 외부 둘 다에서 처리하는 게 안전하다
- 비즈니스 로직 흐름에 사이드 이펙트가 껴들면 테스트도 어렵고 유지보수도 불편해진다
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
- 평소 고민하던 내용을 축소화된 미니 프로젝트로 만들어 해결해보는 연습
- DDD(도메인 주도 설계), 클린 아키텍처, FSD 등 다양한 설계 방법론, FP,OOP같은 프로그래밍 패러다임과 디자인 패턴 학습
- 실제 팀 협업 환경에서 도메인 중심 설계를 적용해보기
- 상태 관리 라이브러리와 도메인 모델을 자연스럽게 연결하는 구조적 접근 연습
- 단순히 동작하는 코드를 넘어서, 기능이 커져도 유연하게 대응할 수 있는 구조 설계 연습
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
과제 피드백
진희님 체크박스를 체크를 하지 않으셨어요 ㅜㅜ 이번 과제는 스스로 체크하면서 의도한 대로 과제를 수행했는지가 중요합니다~ 진행하신 부분이 있지만, 어쩔수 없이 불합드립니다! 수고많으셨습니다. 진희님