[페이먼츠 2단계 - hooks & state] 콘티(조건형) 미션 제출합니다. #542
Conversation
- 포맷 검사와 데이터 유효성 검사를 분리
- {도메인}.프로퍼티 모양으로
- cardNumbers -> values
- 포커스 떄문에 핸들러에서 boolean값을 반환하게 되어 의미가 불명확해짐
- ExpirationDate -> ExpiryDate
… step2 ; Conflicts: ; src/entities/card/cardNumbers.ts ; src/features/CardFormGroup/hooks/useCardNumbers.ts ; src/features/CardFormGroup/ui/BankSelectFormGroup.tsx ; src/pages/payments/Payments.tsx
- FormGroup -> Field
| month: expiryDate.month.value, | ||
| year: expiryDate.year.value, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
P1;
해당 질문과 연관된 리뷰라 여기에 남겨요.
네! 정답은 없지만 usePassword, useCvc 또한 제 생각엔 조금 과하단 생각이 들어요.
Cvc, Password의 코드 흐름을 방해하지 않기도 하고, 나눠서 얻을 잇점이 크지 않다고 생각하기 때문이에요.
다만 왜 이 hooks들을 만들었을까? 생각을 해보면 Page에서 선언하기 위해서라고 생각되는데요.
이미 Cvc, Password 등 컴포넌트를 만들어 두었는데 해당 로직들을 내부에서 관리하는게 좋지 않을까? 하는 생각이 들었어요.
내부라면 굳이 별도의 useCvc같은 훅은 불필요 하기도 하구요.
- page > cardInfo 값 전체를 관리, useFocus 관련 값 관리.
- 각 FormGroup > 각 요소에 맞는 로직 대응 (hook에서 관리하는 로직들은 여기로 옮기기)
같은 방식으로 풀어 볼 수 있을까요?
컴포넌트에서 사용할 로직인데 외부에서 주입해주는 부분이 어색하게 느껴지기도 하고, 외부에서 주입해줘야 할 이유가 크게 없는 것 같아서 제안드려봐요!
혹시 이렇게 해야하는 이유가 있다면 제게 이야기 해주셔도 좋아요!
추가로 만약 제가 제안해주신 방향을 적용한다면 이렇게 하면 무엇이 좋을지도 고민해보시면 좋겠어요!
There was a problem hiding this comment.
지금 제가 훅으로 분리한 이유는 다음과 같습니다!
(솔직히 잘못된 방향으로 가고있지않나 고민했습니다)
1. 버튼의 렌더링 조건은 모든 값이 유효한 지 확인하고 렌더링해야됐습니다. 그러기 위해서 모든 상태를 가져와 모든 요소가valid 한지 구해야했던 상황이라 페이먼츠로 상태를 옮겼어야 됐습니다.(이 당시 페이먼츠안에 폼 컨테이너가 혼합된 상태였습니다)
2. 부모에서 상태만 저장했을 시, 입력마다 valid를 검사하려면 useEffect로 값을 감시하거나handleChange에서 유효성 검증을 통해 함수를 실행해야됐습니다.
useEffect는 공식문서에서 확인한 내용으로 이벤트 처리에서 사용하지 말라고 이해하여
버튼컴포넌트의 조건을 가지고있는 Payments(최상단)에 handleChange를 만들 필요성을 느껴 로직이 최상단으로 올라가 훅으로 분리하게 됐습니다
해결완료했습니다.
잘못된 방향으로 가고있었으나, 루트의 조언으로 길을 찾았습니다 정말감사합니다!!!!
해결한 내용은 아래 코멘트로 남기겠습니다!!
- cardNumbers -> numbers
- 스텝을 관리하기 위함
- errorMessgae->errorMessage
- feature/registerCard/submit -> feature/submit
|
감사합니다 루트 덕분에 숨막히게 하던 문제가 해결됐습니다!! 컴포넌트를 세 개로 나누고, 커스텀 훅들을 제거해보며 해결방법이 보이기 시작했습니다. Form 을 한 컴포넌트로 <div>
<CardPreview />
<CardForm />
<SubmitButton />}
</div>
위 리뷰를 듣고 두 가지 구조 변경을 시도했습니다.
프리뷰와 폼이 같은 계층으로 묶여있던 // 전의 구현방식
const numbersField = useNumbers({onComplate: () => step(node,1) });하지만 제 이전 구현방식은 이밴트 핸들러에서 로직 공유를 하기위해, 훅의 정의부분에
위 그림과 같이 책임을 분산시킬 수 있었고 적용 부분은 아래에 작성하였습니다. // Payments.tsx
const numbersField = useNumbers();
const expiryField = useExpiryDate();
const [cvc, setCvc] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [bank, setBank] = useState<Bank>();
const fields = {
numbersField,
expiryField,
bankField: {
value: bank,
handleChange: (v: Bank | undefined) => setBank(v),
},
cvcField: {
value: cvc,
handleChange: (v: string) => setCvc(v),
},
passwordField: {
value: password,
handleChange: (v: string) => setPassword(v),
},
//...SubmitButton의 렌더 조건에 따라 // features/registerCard/ui/CardForm/CardForm.tsx
const { step, toStep, setStepRef } = usePaymentStep();
return (
<form id={formId} onSubmit={handleSubmit}>
//...
{step >= 3 && (
<CvcField
cvcField={cvcField}
setStepRef={(node) => setStepRef(node, STEP.CVC)} // 미리 만들어 내려줌
onComplate={() => toStep(STEP.PASSWORD)} // 완성시 콜백만 내려줌
/>
)스텝 간 포커스 이동을 위해, ref등록과 // features/registerCard/ui/fields/CvcField.tsx
export const CvcField = ({ cvcField, setStepRef, onComplate }: CvcFieldProps) => {
const { handleChange, value } = cvcField;
const [touched, setTouched] = useState<boolean>(false);
const handleChangeCvc = (inputValue: string): void => { // 내부에서 조율할 핸들러
if (inputValue !== '' && !isNumericString(inputValue)) return;
handleChange(inputValue); // 부모에서 내려받은 핸들러 체인지를 통해 값 변경
setTouched(false);
if (validateCvc(inputValue)) onComplate(); // 미리 정의된 onComplate 함수 실행
};
// ...CvcField 컴포넌트 내에서 핸들러를 새로 정의하고, setState의 지연 변경 문제 떄문에 핸들러 내에서 지금 입력된 값으로 유효성 검증을 사용하여 완료 이벤트를 추적 할 수 있었습니다. Cvc를 분리하며
해당 부분을 제대로 이해하지 못한 것은 로직을 내부에서 관리하라는 것이 상태를 내부에 관리하라는 뜻으로 이해해버려서 문제가 됐었습니다!! 정말 감사드립니다!! 가벼운 추가 질문루트 기준에서 지금의 제 코드를 볼 때 흐름을 따라가기 힘들었다 하는 부분이 있었나요? 있다면 솔직하게 말씀주세요!! FSD를 가볍게 적용하다, 폴더 구조가 더 어려워 진 것 같아 step3에 변경해보도록 하겠습니다 리뷰 요청을 하고나서 늦게 깨달은 문제들리뷰요청 이후에는 추가푸시가 금지 되어있어 늦게 알았지만 수정하지 못한 문제들입니다.. 라우팅은 페이지에서 submit 이벤트는 폼에서
레스의 리뷰를 늦게 확인했습니다. 페이지에서 엔티티에 섞여있는 책임들// entities/card/mode/numbers.ts
export const getTotalErrorMessage = (
isTouched: boolean,
fieldErrors: boolean[],
brand: string,
): string | undefined => {
const hasError = fieldErrors.some((e) => e === true);
if (hasError) {
return CARD_NUMBER_ERRORS.LENGTH;
}
if (isTouched && brand === BRAND.UNKNOWN) { // ⚠️ 문제가 되는 부분!
return CARD_NUMBER_ERRORS.UNKNOWN;
}
};엔티티의 계층에는 순수한 도메인지식을 넣고, 터치드같은 UI관련 책임이 섞여있는 경우 features/model 로 분리해야한다고 느꼈습니다. |

🎯 페이먼츠
이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.
2단계
🕵️ 셀프 리뷰(Self-Review)
제출 전 체크 리스트
리뷰 요청 & 논의하고 싶은 내용
안녕하세요 루트! 많은 문제가 있었고 해결 과정들이 많아 작성에 시간이 걸리게 되어 죄송합니다!
우선 구조부터 설명드리겠습니다
페이먼츠에서 훅의 데이터 흐름을 제 나름대로 표현해봤습니다.
features/hooks은 전부 도메인에 관련된 훅들입니다. 여기서 도메인 관련 유효성 검증과 해당 필드들의 스텝을 관리하도록 했습니다.inputFocus는 각각 두 레이어로 구성됩니다.useInput은state와touched의 상태를 소유합니다. 도메인 훅에선 공용 포커스 훅과 공용 인풋 훅을 조합해 도메인 훅으로 만들게 됐습니다.1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점
검증 로직이 두 개인 문제
이번 단계에서 가장 많이 고민한 문제는
useField입니다.루트가 스텝1에 남겨준 피드백 중 가장 큰 고민을 하게 만든 문제입니다.
위와 같은 문제가 어떻게 생기게 되었는지, 이를 해결하며 어떤 변화가 있었는지 작성하겠습니다.
지금은
useInput으로 바뀐useField는이러한 어색한 입력 포맷 부분은 피그마의 시안 때문에 고민을 시작하게 됐는데요
이걸 보고 잘못된 입력이 들어왔을 때, 사용자에게 피드백은 주고 실제 상태는
valid하게 유지해야한다고 생각했습니다.이를 구현한 결과 입력 가능한 형식을 제한하는 로직과 완성된 값의 유효성을 검사하는 로직이 섞여있었습니다.
실제 상태와 에러메세지가 불일치하게 저장됐고 코드를 너무 복잡하게 만들었습니다.
(handleChange 내에서 상태를 setState 를 변경하기 전에, 에러메세지를 세팅해야 됐습니다)
하지만 상태가 항상 유효한 상태를 유지한다면 에러메세지가 항상 같은 값으로 파생되어야 한다고 생각하여
잘못된 입력들은 입력 자체를 막고 피드백을 주지 않게 변경했습니다
handle change 의 반환
그리고 위 코드에서 어색한 부분이 하나 더 있는데요,
handleChange가string과undefined를 반환하고 있습니다.이 문제는 포커스 이벤트를 만들면서 생기게 된 문제인데요.
포커스 이벤트는 입력 마다 체크하여 모든 길이를 달성했을 시 발생합니다.
이는 변경의 위치(index)와 유무를 파악할 수 있는 handleChange 에서 일어나야한다고 생각하여 handleChange에 위치시켰습니다.
하지만 변경이 일어나는
handleChange안의setState는handleChange가 끝나야 변경되기 때문에 반환타입을 통해 사용하는 쪽에서 모든 길이를 달성했는지 확인했습니다.해당 부분은
onComplate라는 정의된 콜백함수를props로 받아 해결했습니다.useField 와 useFocusInput의 조합
이렇게
handlerChange로직이 복잡해짐에 따라도메인 훅의 필요성을 느꼈습니다.도메인 훅 안에 상태를 전부 집어넣고, 포커스 이동을 같이 처리했습니다.
도메인 훅을 만들고 보니, 검증같은 도메인 책임은 여기 두고,
입력 차단이나touched의 인풋창을 담당하는 공용 훅을 만들면 좋겠다고 느껴 위에서 사용한useField에서 비즈니스 개념을 제외한useInput을 만들게 됐습니다.그 결과 엄청나게 복잡도가 감소했고 각 훅의 책임이 더 명확하게 드러났습니다.
2) 이번 리뷰를 통해 논의하고 싶은 부분
페이먼츠에서 상태를 묶는 훅의 필요성 (usePaymentsForm)
현재 도메인훅들을
Payments컴포넌트에서 사용하고 있습니다. 여기서 상태를 관리하게 된 목적은 다음과 같습니다.위와 같은 이유들로 추상화에 대한 목적을 찾지 못하였기 때문에 컴포넌트에 두게되었지만,
usePayments로 묶어 한단계 더 추상화를 해야하는지 고민했습니다.루트의 의견은 어떠신가요?
폼 컨테이너 컴포넌트의 필요성
위에서 파생된 고민입니다.
<form>과 아래의 컴포넌트를 묶어CardFormContainer처럼 묶는 크루들이 많아 고민했습니다.만약 폼을 컨테이너 컴포넌트로 묶는다면 페이먼츠가 하게 될 일이 적어져(프리뷰와 폼의 연동만 남음) 컴포넌트를 만들지 않았습니다.
하지만 저번 화상미팅 때 루트가 본인이라면 묶었다고 말하여 이 부분에 대해서도 많은 고민을 했습니다.
루트라면
CardFormContainer를 컴포넌트로 만들어 관리했나요? 만약 컴포넌트를 만들 것 같다면, 만들게 된 가장 큰 이유는 무엇인가요?UX로직이 컴포넌트에 드러나는 것
저는 각 컴포넌트에서 스텝과 포커스 로직을 위와 같이 드러냈습니다.
하지만 UX관련된 로직들도 UI 훅으로 분리한 크루의 코드를 봤는데,
백 스페이스 시 전 필드로 이동하는 기능처럼 책임이 늘어나게 된다면 컴포넌트에 등록하는 과정조차 복잡해 질 것 같았습니다.UI 관련 로직을 어디까지 드러내고, 어디까지 추상화 할 지에 대한 고민이 들었습니다.
카드넘버의 단일 진실
카드넘버를 string으로 관리하며 동적으로 나누어 보여줄지, 배열 자체를 단일 진실로 가질지 고민했습니다.
UI에서는 각 input의 입력 상태와 포커스를 독립적으로 관리해야 했기 때문에,
도메인 모델과 다른 형태의 상태를 가지게 되었습니다.
루트라면 카드번호의 상태를 문자열로 가지고있을지, 아니면 배열로 가지고있을지 궁금합니다!
✅ 리뷰어 체크 포인트
1. Form 상태 관리 & Custom Hook 분리
2. 입력 UI 흐름과 UX
3. 컴포넌트 구조 및 재사용성
4. 상태 기반 유효성 검사 및 확인 버튼 활성화
5. 비동기 상태 · 네트워크 경계 · 통합 테스트
idle | loading | success | error네 가지로 명시적으로 관리하고,isLoading/error를 별도 boolean으로 쪼개지 않았는가?POST/GET/DELETE /cards와 400 시나리오까지 포함하여 네트워크 경계에서 동작하는가?fetch·axios를 모킹하지 않고, MSW + RTL로 사용자 관점에서 작성되었는가?getByRole → getByText → getByLabelText → getByTestId우선순위를 따르고, 비동기 요소에findBy*를 사용했는가?