배포 링크: https://jihoon-0330.github.io/front_6th_chapter2-2/
과제의 핵심취지
- 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는 분리되어 있나요?
-
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
-
재사용 가능한 Custom UI 컴포넌트를 만들어 보기
-
재사용 가능한 Custom 라이브러리 Hook을 만들어 보기
-
재사용 가능한 Custom 유틸 함수를 만들어 보기
-
그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기
-
UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
-
엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
-
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
이번 과제는 생각보다 쉬워 보였다. 지난주와 달리 읽기 쉬운 형태로 작성되어 있었고, 리액트를 사용하고 있었다. 많은 사람들이 AI 를 사용하지 않고 과제를 진행하는 것 처럼 보였고 나도 그렇게 시작을 했다. 하지만 생각보다 진행속도가 느렸고, 목요일엔 커서를 최대한 활용해서 작업을 진행했다. 어쩌다 이렇게 되었을까? 과제를 진행하면서 겪은 문제와 고민을 적어본다.
순수 함수는 어떤 값을 인자로 받아야 하는가?
과제 초반에 시간을 많이 잡아먹은 부분이다. 평소 순수 함수를 작성하려는 노력을 해본적이 없었기 때문에 이번 과제에서 순수 함수에 대한 고민이 많았던 것 같다. 순수 함수는 동일한 입력에 대해 동일한 출력을 반환하는 함수인데, 그렇다면 어떤 값을 인자로 받아야 할까? 단순 계산을 위해선 계산에 필요한 값들을 받으면 되긴 한다.
하지만 계산 과정에 조건문이 섞여 있다면 어떻게 될까?
addToCart 함수에서는 상품이 매진되었는지 확인하는 과정이 필요했는데, 매진을 확인하는 isSoldOut 이라는 함수를 만들어 둔 상태였다. 이 함수를 addToCart 내부에서 호출하는 것이 알맞을까? 아님 인자로 받는 것이 알맞을까? 아님 매진 결과만 인자로 받는 것이 알맞을까? 이런 종류의 고민이 함수를 작성하는 과정에서 지속적으로 발생했다.
과제를 진행하면서 addToCart 내부에서 매진을 확인하는 함수를 인자로 받아 호출하는 방식을 선택했는데, PR 을 작성하는 시점에선 매진이 되었다면 addToCart 함수가 애초에 호출이 되지 않도록 하는 것이 좋지 않을까 라는 생각도 든다.
순수 함수라 착각한 경우
setState(()=> { ...순수함수... }); 형태로 코드를 작성중이었다. 이때 장바구니 추가 버튼을 눌렀을 때 토스트 알림이 2번 실행되는 현상이 있었다. set 함수 내부에서 토스트를 띄우는 함수를 호출한 것이 원인이었다.
토스트를 띄우기 위해 계산 함수에 onFailure, onSuccess 를 인자로 받도록 구성했는데, '같은 입력에 대해 같은 출력을 반환한다' 라는 점에 집중한 나머지 함수 외부를 변화시키는 함수를 순수 함수라 착각을 했다. 리액트 스트릭모드를 사용중이지 않았다면 순수 함수를 잘못 구현한 사실을 인지하지 못했을 것 같다.
리액트 스트릭모드에선 순수성을 검증하기 위해 useEffect, state set 함수, useMemo, useReducer 등 일부 함수를 2번 실행하도록 한다. [리액트 공식 문서] 개발 중 이중 렌더링으로 발견한 버그 수정
컴포넌트 분리의 기준
그동안 UI 를 재사용 하기 위한 컴포넌트 분리를 주로 해왔다. 로직과 UI 를 분리해야 하는 이유가 크게 와닿지 않았다. 데이터 컴포넌트를 재사용하는 경우도 똑같은 로직이 필요하기 때문에 로직이 함께 있어도 재사용이 가능하지 않은가? 라는 생각이 있었다. 테스트 작성이난 스토리북 같은 UI 만 별도로 존재하는 것이 유리한 상황에선 나누는 것이 좋다는 생각이 들었지만, 그런 상황이 아니라면 굳이 나눠야 하는가 라는 생각도 공존했다. 멘토링 시간에 컴포넌트의 분리에 대한 질문을 남겼는데, '컴포넌트도 결국 함수' 라는 것에서 분리를 해야 하는 목적이 와닿는 느낌이었다. 관점의 차이일까? 컴포넌트를 볼 때 한 컴포넌트에서도 로직은 로직이고 UI 는 UI 지 라는 생각이 있었던 것 같은데, 컴포넌트 자체를 하나의 함수로 바라봤을 때 각 부분을 추상화 해야 하는 이유가 납득이 가는 것 같다.
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
순수 함수 설계
이번 과제에선 순수 함수를 어떻게 구성할지 가장 많이 고민했다. 특히 어떤 값을 인자로 받을지, 도메인 간 의존성을 어떻게 처리할지가 주요 포인트였다. 계산 로직에선 외부 상태를 건드리지 않도록 의식적으로 작성했지만, setState 내부에서 토스트를 띄우는 바람에 예상치 못한 부작용도 겪었다. 이걸 통해 순수 함수의 기준을 조금 더 명확히 정리할 수 있었다.
확장성 고려
할인이 여러 개 적용될 수 있는 상황을 가정해 구조를 유연하게 설계했다. 실제 요구사항엔 없었지만, 정책이 바뀌더라도 최소 수정으로 대응할 수 있도록 의도적으로 확장성을 신경 썼다.
일관된 코드 스타일
- 함수 인자를 객체로 받도록 통일했다. 객체를 인자로 사용하는 경우 타입을 지정할 때 읽기 불편하다 느껴지는 것 같다. 또한 대부분 1~2개의 인자만 필요해서 오히려 불편한 경우도 있었다. 다음엔 다른 방식을 고려해 볼 것 같다.
- 컴포넌트의 역할에 따라 이름을 통일했다. 컴포넌트 이름 뒤에 Container, UI 를 붙였다.
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
- 함수의 인자를 객체 형태로 통일해 과제를 진행해 보았다. 대부분의 함수들이 1~2 개의 인자를 받는 상황이라 오히려 불편함이 증가했다. 과제와 어울리지 않는 컨벤션을 정한 느낌이라 아쉬웠다.
- 순수 함수의 형태에 대해 고려해 볼 것 같다. 인자로 값만 받을지, 인자로 함수도 받을지, 조건문은 어떻게 처리할지 와 같은 부분을 좀 더 명확하게 정하면 더 일관된 코드를 작성할 수 있을 것 같다.
- UI 컴포넌트와 로직을 분리하는 방식에 대해 고민해 볼 것 같다. 이번 주차에서는 시간 분배를 잘 하지 못해 컴포넌트 분리 쪽은 신경을 많이 쓰지 못했다. 어떤 패턴을 사용할지, 이름은 어떻게 구분할지 생각해보면 일관된 구조를 만드는데 도움이 될 것 같다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
-
순수 함수에서 다른 순수 함수(a)의 계산 결과가 필요할 때, a의 계산 결과를 인자로 받는 것이 좋을까요? 아님 함수 내부에서 a 함수를 호출하는 것이 좋을까요? 함수 내부에서 a 를 호출하는 것이 더 간단한 방법처럼 보이는데, a 가 아닌 다른 함수를 호출해야 하는 상황이 발생했을 때 함수 내부의 코드를 수정해야 하는 부분이 좋지 않은 패턴처럼 느껴지기도 하는 것 같습니다.
-
컴포넌트 이름 짖는 것이 어렵게 느껴집니다. 컴포넌트를 쪼개다 보면 비슷한 이름을 가진 컴포넌트들을 만드는 경우가 있는 것 같은데, 의미를 잘 전달하는 방법이 있을까요?
과제 피드백
안녕하세요 지훈님! 역시 믿고 보는 지훈님의 과제네요 ㅎㅎ
순수 함수에서 다른 순수 함수(a)의 계산 결과가 필요할 때, a의 계산 결과를 인자로 받는 것이 좋을까요? 아님 함수 내부에서 a 함수를 호출하는 것이 좋을까요? 함수 내부에서 a 를 호출하는 것이 더 간단한 방법처럼 보이는데, a 가 아닌 다른 함수를 호출해야 하는 상황이 발생했을 때 함수 내부의 코드를 수정해야 하는 부분이 좋지 않은 패턴처럼 느껴지기도 하는 것 같습니다.
특별한 경우가 아니라면 a의 계산 결과를 인자로 받는게 좋지 않을까!? 라는 생각이 들어요 ㅎㅎ 혹은, 두 가지를 모두 처리하는 모습이어도 무방할 수 있답니다.
가령 Number 함수의 경우
Number("1") Number(1) Number(new Number(1))
이런식으로 여러가지 형태를 받아서 처리할 수 있어요. a를 전달할 때 a를 만들어내기 위한 인자를 전달하거나 a 자체를 전달하는 등 두 가지 일을 모두 수행할 수 있는 함수를 만드는거죠 ㅎㅎ 어떻게보면 단일책임 원칙에 위배된다고 느껴지기도 하는데,
책임을
- 변환하는 함수
- 계산하는 함수
- 결합하는 함수
이런식으로 분산해서 사용할 수 있을 것 같아요!
컴포넌트 이름 짖는 것이 어렵게 느껴집니다. 컴포넌트를 쪼개다 보면 비슷한 이름을 가진 컴포넌트들을 만드는 경우가 있는 것 같은데, 의미를 잘 전달하는 방법이 있을까요?
흠.. 말씀해주신 내용만 봤을 때는 어떤 사례인지 가늠이 잘 안 되네요 ㅎㅎ; 보통 컴파운트 패턴으로 많이 쓰는 것 같아요!
https://www.radix-ui.com/primitives/docs/components/alert-dialog
import { AlertDialog } from "radix-ui";
export default () => (
<AlertDialog.Root>
<AlertDialog.Trigger />
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialog.Content>
<AlertDialog.Title />
<AlertDialog.Description />
<AlertDialog.Cancel />
<AlertDialog.Action />
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
요로코롬!?
마지막 질문의 경우 구체적인 예시를 문의 채널에 남겨주시면 답변하도록 하겠씁니다!