과제의 핵심취지
- 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는 잘 제거했나요?
-
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
알게 된 점
- 리액트 프로젝트를 항상 새로 만들거나 읽기 힘들정도의 레거시를 맛보지 못햇다는 것을 깨닫게 되었습니다.
- 프롭드릴링의 끔찍함에 대해서 잘 몰랐다는 걸 깨달았습니다. 제가 훅을 적절하게 분산시켜 만들지 못한 점도 있겠지만, 프롭드릴링을 경험하니 숨이 턱턱 막혀서 사이다 오백만병을 찾고싶었습니다.
좋았던 점
- 쏙쏙 함수형 코딩을 가볍게 읽어 본적이 있는데 적용해볼 생각은 못했었습니다. 책에서 강조하는 순수함수의 '계산'이라는 포인트가 계속 머릿속에 맴돌아서 학습에 정말 좋았습니다. 물론, 이 과제가 그것을 기반으로 잘 작성되었는가?라고 하면 아니라고 1초만에 대답할 것 같습니다.
- 그래도 발제에서 강조하셨던
데이터 > 계산 > 액션을 지키려 노력을 많이 했습니다.
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
Basic : Model > Service > Hook > View 4계층
선택 이유
사실, 힌트 폴더를 보면서 Model > Hook > View 로 3계층을 만들 생각이었습니다.
각 도메인의 Model 내 함수에 존재하는 복잡도와 역할의 범위가 너무 차이가 많이 나기 시작했습니다.
하나의 모델 안에 단순히 매핑하거나 순회를 도는 함수가 있는 반면, 모델함수를 조합하여 비즈니스 로직 함수가 존재하기도 했습니다.
해당 내용을 퍼블렉시티에게 설명하면서 model을 어떻게 관리해야할지 조언을 얻었고, 비즈니스로직중 단순 계산이나 포맷팅은 model과 비즈니스로직 및 조합의 경우는 service로 분리하는 방식을 선택하게 되었습니다.
구조
저처럼 힌트를 확인하고 시작한 스터디원이라면 거의 똑같은 계층을 생각하고 만들지 않았을까 합니다.
데이터의 호출은 model > service > hook 순으로만 부르게 작성했습니다.
hook 에서는 model을 직접 부르지 않고 service를 부르게끔 노력했으며, 다른 서비스 간 함수는 호출하되 순환참조는 일어나지 않게 했습니다.
후반부에 급하게 작성한 것들이 너무 많아 이 룰이 지켜졌을지는 잘 모르겠습니다. (제발..)
sequenceDiagram
Component->>Hook: 이벤트 발생
Hook->>Service: 비즈니스 로직 실행
Service->>Model: 데이터 조작
Model-->>Service: 결과 반환
Service-->>Hook: 처리 결과
Hook-->>Component: 상태 업데이트
상태
model > service 순으로 도메인 중심적으로만 생각하여 reducer(useReducer)를 채택하여 cart, product, coupon 을 관리하고 있습니다.
결국에는 볼륨이 다소 큰 useCart ,useActionCart 와 같이 상태와 액션 훅을 각각 만드는 상황이 되었습니다.
contextAPI에서 상태 프로바이더와 액션 프로바이더를 각각 만드는 것이 생각이 났으며, 그런 관점으로 구축해나갔습니다.
코치님들이 훅을 최대한 분리 하라는 말씀을 정말 많이 하셨는데 저는 그러지 못해 조금 아쉽다는 생각이 들었습니다. 각각의 도메인을 중심으로 관리하려 했기에, 다른 스터디원 분들의 코드와 솔루션 코드를 참고해서 좀 더 인사이트를 얻어야 할것 같습니다.
Advanced : 3계층 + Atoms
(사실, 3계층 + atoms 이름은 클로드가 알려줬습니다. 하하) cart,coupon, product와 같이 도메인마다 contextAPI로 묶으려고 했던 것을 jotai로 교체 했더니 생각한것이랑 비슷하면서 달랐습니다. 원래도 취향으로 인해 jotai나 recoil을 잘 쓰는 편이 아니라 쪼금 마음에 안들기도 했습니다.
흐름을 따라갈 수 있다는 관점에서는 프롭드릴링 좋다는 생각을 했었고, 컴포넌트 관리나 복잡성을 줄이기 위함에서는 jotai가 월등히 좋은 것을 체감했습니다.
sequenceDiagram
participant C as Component
participant H as Hook
participant A as Atom
participant S as Service
participant M as Model
C->>H: 이벤트 발생
H->>A: 전역 상태 읽기
H->>S: 비즈니스 로직 실행
S->>M: 데이터 조작
M-->>S: 결과 반환
S-->>H: 처리 결과
H->>A: 전역 상태 업데이트
A-->>C: 자동 리렌더링 (Props drilling 없음)
순수함수
model 과 service 계층의 함수는 모두 순수함수로 만들었습니다.
아쉬운 점
피알을 작성하면서 코드를 다시 확인 하니 타입 일관성을 지키지 않은 부분도 있더라구요?! service 계층에서는 밸리데이트, 포맷팅, 에러핸들링이 함께 존재하기 때문에 결과 값을 타입으로 두어 일관성을 주려고 노력했습니다. 해당 비즈니스 로직이 성공인가 실패인가에 대한 내용도 값으로 지정하여 반환된 곳에서 사용할 수있겠끔 했습니다.
초반에는 Result 타입으로 success, value, message 로 포맷팅하여 사용하고 있었습니다.
하지만 notification 함수에 인자를 넘겨주기 위해서는 어떤 상태(status)인가에 대한 정보가 필요했습니다. success를 제거하고 status('success','error')를 전달함으로써, 의미상으로 성공여부를 줄 뿐 아니라 notification 인자에 그대로 넘겨 주었습니다.
- 변경의도 :
notification.add(result.message, result.status); - 현재 :
onSuccess: (message) => notification.add(message, "success"),(흑, 어쩌다 바꿔버렸다..) 현재 코드에서는 다르게 notification을 띄우지만 해당 타입 선언은 위와 같은 패턴을 위하여 사용했습니다.
그런데 위에서 말했듯이 타입을 완전히 정리하지 못했었더라구요. 타입을 확인하다 놀래서 눈물쭈륵 하면서 피알에 쓰게 되었습니다.
export type CartOperationResult =
| {
value: CartItem[];
message: string;
status: "success";
}
| {
message: string;
status: "error";
};
export type CouponOperationResult =
| {
success: true; // 이걸 정리했어야 했는데!
value: Coupon[];
message: string;
status: "success"; // 이미 stauts 가 'success'임을 보여주는것이데 중복 코드 ㅠ_ㅠ
}
| {
success: false;
message: string;
status: "error";
};
export type OrderOperationResult =
| {
orderNumber: string;
message: string;
status: "success";
}
| {
message: string;
status: "error";
};
export type ProductOperationResult =
| {
value: Product[];
message: string;
status: "success";
}
| {
message: string;
status: "error";
};
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
이번 주차의 주제 중 하나인, 함수형 프로그래밍의 중심인 순수함수를 많이 만들어보려는 활동은 많이 했습니다. 하지만, 디자인 패턴에 대해서는 고려해보지않고 과제를 진행했습니다.
물론 이전까지 생각해보지 못했던 계층을 고민하며 작성한 경험은 좋았지만, 앞으로는 특정 문제를 해결하기위해 패턴을 사용해보고싶습니다.
사실 notification, localstorage 에서 obserber 패턴을 차용하려 했는데, 복잡도가 올라갈 수 밖에 없는게 아쉽더라구요. localstorage는 제대로 만들지 못했던 건지 테스트 코드가 통과되지 못하는 경우도 왕왕있어서 결국에는 패턴을 후순위로 두고 작업했습니다. 1순위는 계층나누기, 2순위는 데이터, 계산 , 액션 고려하며 순수함수 만들기로 잡았습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
models, service
사실 아키텍쳐에 대해서 잘 모릅니다. 그냥 불편해서 고민하다 지금의 구조를 가지게 되었습니다. 과제를 마치고나서 클로드에게 basic구조가 어느정도 괜찮은 구조인지, 모델과 서비스는 순수함수로 잘 만들어졌는지 검토를 하기도 했습니다.
이런 방식의 구조라면, model과 service에는 각각 어떤 역할의 함수가 존재하게 되는 것일까요? 기본적으로 models를 데이터 중심으로, service를 비즈니스로직 중심으로 작성했습니다.
처음 과제를 내실때 힌트에 models 폴더를 넣어주신 의도에서는, 어떤 함수가 들어있길 바라셨을까요? 도메인 종속인 비즈니스 로직의 순수함수인것을 알지만 어떤 범위까지의 함수인지 너무 궁금합니다.
커스텀 훅
도메인 단위로 장바구니 목록, 상품 목록 등을 기준잡아서 하나의 상태, 여러개의 액션으로 생각했습니다.
그래서 제 커스텀훅은 reducer를 썼다고는 하나, 무척 사이즈가 큽니다. 어쩌면 무척 클린하지 않을지도 모른다고 생각합니다. 하나의 목적을 위해서만 수정해야한다는 것이 단일책임원칙이라고 알고있습니다. 저의 훅은 그 목적이 너무 넓지 않았나 생각이 듭니다. ㅠ_ㅠ
qna나 다른 질문에서 답변을 많이 해주셨을수도 있겠지만, 이렇게 하나의 상태에 여러 액션이라면 커스텀 훅을 한두개로 모아서 가져가 되는 것일까요? 아니면, 이 상황은 리듀서를 써서 그런것일까요? 짧은 식견으로 고민이 많습니다.
+) 정말 궁금해서 제 코드를 넣어서 클로드에게 물어봤더니, DDD 였기에 도메인 로직이 응집되어 훅이 커졌다고 알려주더라구요. 혹시 이렇게 이해해도 될까요?
과제 피드백
안녕하세요 지현, 수고했습니다! 이번 과제는 컴포넌트를 도메인 관점으로 분리하고 props drilling 문제를 해결하면서 함수형 프로그래밍의 순수함수 개념을 체득하는 것이 목표였어요. 지현이 작성한 회고를 보니 정말 많은 고민과 시행착오를 거치면서 과제의 핵심을 제대로 파악하고 경험했다는 게 느껴지네요.
특히 "프롭드릴링의 끔찍함"을 직접 경험하고 사이다 오백만병을 찾고 싶었다는 표현이 정말 와닿았어요. 저도 과제 솔루션 다시 만들면서... 내가 왜 이걸 느껴보라고 했을까 하면서 답답함을 느꼈답니다.
그리고 흐름을 따라갈 수 있다는 관점에서는 프롭드릴링 좋다는 생각을 했었고, 컴포넌트 관리나 복잡성을 줄이기 위함에서는 jotai가 월등히 좋은 것을 체감했다는 측면에서 과제의 취지는 다 해줬다고 생각했는데...
정작 코드를 보니 아직 불필요한 props사용등이 많아 보였어요. 생각해보면 props도 interface를 가지게 되는데 interface는 많거나 복잡한 것이 곧 프로젝트의 복잡도를 나타내거든요. 가급적이면 이러한 props interface를 줄이거나 통일하거나 일관성있게 만들 수 있는 방향으로 고민을 해보면 좋겠어요.
프롭드릴링이 좋은 것과 전역상태관리의 장점을 둘 다 취해서 mix하는 방향은 어떤 방향일까를 6주차 과제에 잘 녹여보길 바래요.
Q) models과 service의 역할 구분에 대해서 => 지현이 고민한 대로 models는 데이터 중심의 순수한 계산(매핑, 포맷팅, 단순 변환)을 담당하고, service는 비즈니스 로직과 모델 함수들의 조합을 담당하는 게 맞습니다. 쉽게 말해 models는 "무엇"을 다루고 services는 "어떻게"를 다룬다고 생각하면 돼요. 예를 들어 cart model에서는 아이템 찾기, 수량 가져오기 같은 단순 작업을, cart service에서는 재고 확인하고 할인 적용하고 에러 처리하는 등의 비즈니스 규칙을 담당하는 거죠.
Q) 커스텀 훅이 커진 것에 대해서 => DDD 방식으로 도메인에 응집시키다 보니 훅이 커진 건 맞습니다. 하지만 "한 곳에서 많은 일을 한다"와 "도메인 응집도가 높다"는 다른 개념이에요. 일부는 컴포넌트에서만 동자갛는 로직이라 훅에 포함되지 않을수도 있고 어떤 로직은 다른 기능들을 복합적으로 사용해야하기에 외부에서 조립하도록 만들어야 했을 수도 있어요.
=> 커스텀 훅이 모든 기능을 다 아우러야 한다기 보다는 추상화 이면으로 숨겨도 괜찮을 것들 - 너무나 당연히 예측이 가능 동작 - 들을 숨긴다라고 생각해보면서 그 접점을 잘 찾아가 보기를 바래요.
=> 과제는 그렇지 못하지만 실전에서는 코딩하면서 이 안의 내용들이 궁금해 한다거나 디버깅을 하는 과정에서 왔다 갔다를 많이 하는 걸 경험하게 되면 좋은 구조가 아니구나 이렇게 판단하는 징조로 활용해보세요.
6주차 과제도 화이팅입니다!