Skip to content

[페이먼츠 2단계 - hooks & state] 콘티(조건형) 미션 제출합니다. #542

Open
iftype wants to merge 68 commits into
woowacourse:iftypefrom
iftype:step2
Open

[페이먼츠 2단계 - hooks & state] 콘티(조건형) 미션 제출합니다. #542
iftype wants to merge 68 commits into
woowacourse:iftypefrom
iftype:step2

Conversation

@iftype
Copy link
Copy Markdown

@iftype iftype commented May 12, 2026

🎯 페이먼츠

이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.

2단계

  • 다양한 Form 구성 요소들간의 상태를 효율적으로 관리한다.
  • hooks API를 이용하여 상태 관리 로직을 구현한다.
  • custom hooks를 생성하여 Form 관리 로직을 컴포넌트에서 분리하고 재사용한다.
  • Controlled & Uncontrolled Components에 입각하여 Form을 핸들링한다.

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

  • 기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
  • 기본적인 프로그래밍 요구 사항(코드 컨벤션, 에러 핸들링 등)을 준수했나요?
  • 배포한 데모 페이지에 정상적으로 접근할 수 있나요?

리뷰 요청 & 논의하고 싶은 내용

안녕하세요 루트! 많은 문제가 있었고 해결 과정들이 많아 작성에 시간이 걸리게 되어 죄송합니다!
우선 구조부터 설명드리겠습니다

image

페이먼츠에서 훅의 데이터 흐름을 제 나름대로 표현해봤습니다.

features/hooks은 전부 도메인에 관련된 훅들입니다. 여기서 도메인 관련 유효성 검증과 해당 필드들의 스텝을 관리하도록 했습니다.

inputFocus 는 각각 두 레이어로 구성됩니다.

  • 필드들의 포커스 이벤트
  • 필드 내의 포커스 이벤트(cardNumbers, expiryDate)

useInputstatetouched의 상태를 소유합니다. 도메인 훅에선 공용 포커스 훅과 공용 인풋 훅을 조합해 도메인 훅으로 만들게 됐습니다.

1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점

검증 로직이 두 개인 문제

이번 단계에서 가장 많이 고민한 문제는 useField입니다.

image

루트가 스텝1에 남겨준 피드백 중 가장 큰 고민을 하게 만든 문제입니다.

위와 같은 문제가 어떻게 생기게 되었는지, 이를 해결하며 어떤 변화가 있었는지 작성하겠습니다.

//  변경 전 useField ( 현재는 useInput으로 변경 )
interface UseFieldProps {
   validateFormat: (value: string) => string | undefined;   // 입력 포맷 검증 함수 에러 시 에러메세지 반환
   validateComplete: (value: string) => string | undefined;   // 실제 데이터 유효성 검증 함수  에러 시 에러메세지 반환
 }

export const useField = ({ isValidFormat, validator }: UseFieldProps): UseFieldResult => {
  const [value, setValue] = useState<string>('');     // 상태 저장
  const [error, setError] = useState<Error>();

  const handleChange = (nextValue: string) : string | undefined => {
    const errorMessage = validateFormat(nextValue);  
    setError(errorMessage);  // 입력 포멧 문제시 에러메세지 세팅 후 종료
    if (errorMessage) return errorMessage; 

    setValue(nextValue);    // 값 변경
    return
  }; 

  const handleBlur = () => {
    setError(validateComplete(value));  // onBlur 시 유효성 검사를 진행
  };

// handleBlur = ()= >{  ...

지금은 useInput 으로 바뀐 useField

  1. 인풋 하나의 상태를 저장하며,
  2. validateFormat를 통해 입력 포맷을 검증하여 기준에 따라 입력을 막고
  3. validateComplete를 통해 제출할 수 있는 상태인지 확인했습니다

이러한 어색한 입력 포맷 부분은 피그마의 시안 때문에 고민을 시작하게 됐는데요

  • 피그마 시안
image
  • 기능 요구 사항
image

잘못된 입력은 막고, 에러메세지를 뱉어준다.

이걸 보고 잘못된 입력이 들어왔을 때, 사용자에게 피드백은 주고 실제 상태는 valid하게 유지해야한다고 생각했습니다.

이를 구현한 결과 입력 가능한 형식을 제한하는 로직완성된 값의 유효성을 검사하는 로직이 섞여있었습니다.

실제 상태와 에러메세지가 불일치하게 저장됐고 코드를 너무 복잡하게 만들었습니다.
(handleChange 내에서 상태를 setState 를 변경하기 전에, 에러메세지를 세팅해야 됐습니다)

하지만 상태가 항상 유효한 상태를 유지한다면 에러메세지가 항상 같은 값으로 파생되어야 한다고 생각하여
잘못된 입력들은 입력 자체를 막고 피드백을 주지 않게 변경했습니다

handle change 의 반환

그리고 위 코드에서 어색한 부분이 하나 더 있는데요,

  const handleChange = (nextValue: string) : string | undefined => {

handleChangestringundefined를 반환하고 있습니다.

이 문제는 포커스 이벤트를 만들면서 생기게 된 문제인데요.

// 문제가 있었던 코드( 컴포넌트 내의 핸들러 가로채기)
export const ExpirationDateFormGroup = ({ month,  year,  setStepRef,  onComplete,
}: ExpirationDateFormGroupProps) => {
  const { setInputRef, focusNext } = useInputFocus();
  const handleChangeExpirationDate = (inputValue: string): void => {
    const error = expirationDate.handleChange(inputValue);
    if (!error) focusNext()  // 포커스 이동
  };

컴포넌트 내부의 핸들러의 반환값을 통해 현재 값을 검사했습니다. 이 당시 유효성 검증은 상태가 있는 쪽에서 이루어 져야한다고 생각했었습니다. 지금은 상태보다는 해당 도메인 책임이 있는 곳에서 해야한다고 생각 중입니다.

포커스 이벤트는 입력 마다 체크하여 모든 길이를 달성했을 시 발생합니다.
이는 변경의 위치(index)와 유무를 파악할 수 있는 handleChange 에서 일어나야한다고 생각하여 handleChange에 위치시켰습니다.

하지만 변경이 일어나는 handleChange안의 setStatehandleChange 가 끝나야 변경되기 때문에 반환타입을 통해 사용하는 쪽에서 모든 길이를 달성했는지 확인했습니다.

export const useField = ({ onComplete }: { onComplete: () => void }) => {

해당 부분은 onComplate 라는 정의된 콜백함수를 props로 받아 해결했습니다.

useField 와 useFocusInput의 조합

image
// 처음 구현한 유효기간 훅 
export const useExpiryDate = ({ onComplete }: { onComplete: () => void }): UseExpiryDateResult => {
  const [monthValue, setMonthValue] = useState('');  //상태를 저장하고있음
  const [monthTouched, setMonthTouched] = useState(false);
  const [yearValue, setYearValue] = useState('');
  const [yearTouched, setYearTouched] = useState(false);
  const { setInputRef, focusNext } = useInputFocus();

  const handleMonthChange = (value: string) => {
    if (!isValidFormatMonth(value)) return;
    setMonthValue(value);
    setMonthTouched(false);
    if (!validateMonth(value)) focusNext(1);   // 포커스 이동 (년으로)
  };

이렇게 handlerChange 로직이 복잡해짐에 따라 도메인 훅의 필요성을 느꼈습니다.
도메인 훅 안에 상태를 전부 집어넣고, 포커스 이동을 같이 처리했습니다.

도메인 훅을 만들고 보니, 검증같은 도메인 책임은 여기 두고, 입력 차단 이나 touched의 인풋창을 담당하는 공용 훅을 만들면 좋겠다고 느껴 위에서 사용한 useField 에서 비즈니스 개념을 제외한 useInput을 만들게 됐습니다.

export const useExpiryDate = ({ onComplete }: { onComplete: () => void }): UseExpiryDateResult => {
  const month = useInput({ validator: monthValidate });  // 공용 훅으로 input 처리
  const year = useInput({ validator: isNumericString });
  const { setInputRef, focusNext } = useInputFocus();  // 공용 포커스 훅

  const monthError = validateMonth(month.value);  // 파생값으로 처리 
  const yearError = validateYear(year.value);
  const isValid = !monthError && !yearError;

  const handleMonthChange = (value: string) => {
    month.handleChange(value);
    if (!validateMonth(value)) focusNext(1);
  };
 // ...

그 결과 엄청나게 복잡도가 감소했고 각 훅의 책임이 더 명확하게 드러났습니다.

2) 이번 리뷰를 통해 논의하고 싶은 부분

페이먼츠에서 상태를 묶는 훅의 필요성 (usePaymentsForm)

// pages/payments/Payments.tsx
  const cardNumbers = useCardNumbers({ onComplete: () => toStep(STEP.BANK) });
  const bank = useBank({ onComplete: () => toStep(STEP.EXPIRY) });
  const expiryDate = useExpiryDate({ onComplete: () => toStep(STEP.CVC) });
  const cvc = useCvc({ onComplete: () => toStep(STEP.PASSWORD) });
  const password = usePassword({ onComplete: () => STEP.BUTTON });
  //...
  return (
    <div className={styles.payments}>
      <CardPreview info={cardInfo} />
      <form className={styles.form} id="payment-form" onSubmit={handleSubmit}>
        {step >= 4 && <PasswordFormGroup password={password} setStepRef={SET_REFS.PASSWORD} />}
        {step >= 3 && <CvcFormGroup cvc={cvc} setStepRef={SET_REFS.CVC} />}
        {step >= 2 && <ExpiryDateFormGroup expiryDate={expiryDate} setStepRef={SET_REFS.EXPIRY} />}
        {step >= 1 && <BankSelectFormGroup bank={bank} setStepRef={SET_REFS.BANK} />}
        <CardNumberFormGroup cardNumbers={cardNumbers} setStepRef={SET_REFS.CARD_NUMBERS} />
        {isFormValid && <SubmitButton form="payment-form" />}
      </form>
    </div>
  );

현재 도메인훅들을 Payments 컴포넌트에서 사용하고 있습니다. 여기서 상태를 관리하게 된 목적은 다음과 같습니다.

  1. 프리뷰와의 연동
  2. 제출할 수 있는 상태 검증
  3. 필드 입력 완료 시 스텝 포커스 바인딩

위와 같은 이유들로 추상화에 대한 목적을 찾지 못하였기 때문에 컴포넌트에 두게되었지만,
usePayments로 묶어 한단계 더 추상화를 해야하는지 고민했습니다.
루트의 의견은 어떠신가요?

폼 컨테이너 컴포넌트의 필요성

  return (
    <div className={styles.payments}>
      <CardPreview info={cardInfo} />
      <form className={styles.form} id="payment-form" onSubmit={handleSubmit}>  // 해당하는 부분
          //...
      </form>
    </div>

위에서 파생된 고민입니다. <form> 과 아래의 컴포넌트를 묶어

  return (
    <div className={styles.payments}>
      <CardPreview info={cardInfo} />
      <CardFormContainer / >  // 🔥해당 부분!
    </div>

CardFormContainer 처럼 묶는 크루들이 많아 고민했습니다.
만약 폼을 컨테이너 컴포넌트로 묶는다면 페이먼츠가 하게 될 일이 적어져(프리뷰와 폼의 연동만 남음) 컴포넌트를 만들지 않았습니다.

하지만 저번 화상미팅 때 루트가 본인이라면 묶었다고 말하여 이 부분에 대해서도 많은 고민을 했습니다.
루트라면 CardFormContainer 를 컴포넌트로 만들어 관리했나요? 만약 컴포넌트를 만들 것 같다면, 만들게 된 가장 큰 이유는 무엇인가요?

UX로직이 컴포넌트에 드러나는 것

// features/cardFormGroup/ui/ExpiryDateFormGroup.tsx
      <Input
        ref={(node) => {
          setStepRef(node);   // 스텝 등록
          setInputRef(node, 0);  // 필드내에서 인풋 이동 등록
        }}
      />
      <Input
        ref={(node) => setInputRef(node, 1)}  // 이동 될 필드 등록
      />

저는 각 컴포넌트에서 스텝과 포커스 로직을 위와 같이 드러냈습니다.

하지만 UX관련된 로직들도 UI 훅으로 분리한 크루의 코드를 봤는데, 백 스페이스 시 전 필드로 이동하는 기능 처럼 책임이 늘어나게 된다면 컴포넌트에 등록하는 과정조차 복잡해 질 것 같았습니다.

UI 관련 로직을 어디까지 드러내고, 어디까지 추상화 할 지에 대한 고민이 들었습니다.

카드넘버의 단일 진실

카드넘버를 string으로 관리하며 동적으로 나누어 보여줄지, 배열 자체를 단일 진실로 가질지 고민했습니다.

UI에서는 각 input의 입력 상태와 포커스를 독립적으로 관리해야 했기 때문에,
도메인 모델과 다른 형태의 상태를 가지게 되었습니다.

루트라면 카드번호의 상태를 문자열로 가지고있을지, 아니면 배열로 가지고있을지 궁금합니다!

  • input ui가 입력 중 변하는 것이 부적절하다고 판별하여
    AMEX의 format도 [4,6,5] 에서 [4,4,4,3] 으로 변경하여 가지게 되었는데 유니온 번호에 대해서도 어떻게 저장하실 지 궁금합니다.

✅ 리뷰어 체크 포인트

1. Form 상태 관리 & Custom Hook 분리

  • 반복되는 로직을 custom hook으로 분리했는가?
  • hook 내부와 UI 컴포넌트의 역할이 명확하게 분리되어 있는가?
  • 상태 흐름이 직관적이며, Form 전체를 일관되게 관리할 수 있는 구조인가?

2. 입력 UI 흐름과 UX

  • 카드 번호 입력 시 필드 간 자동 포커싱 이동이 자연스럽게 동작하는가?
  • 숫자만 입력 가능한 필드에서 제한, 에러 메시지, 유효성 피드백 등 사용자 경험이 충분히 고려되었는가?

3. 컴포넌트 구조 및 재사용성

  • 컴포넌트가 명확한 역할과 책임을 가지며 과도하게 분리되거나 중첩되어 있지 않은가?
  • Input, Button 등 재사용 가능한 컴포넌트가 잘 정의되어 있는가?

4. 상태 기반 유효성 검사 및 확인 버튼 활성화

  • 모든 필드가 유효할 때만 확인 버튼이 정확히 활성화/비활성화되는가?
  • 유효성 검사의 기준이 명확하고, 상태 변경에 따른 UI 반응이 잘 연결되어 있는가?

5. 비동기 상태 · 네트워크 경계 · 통합 테스트

  • 비동기 상태를 idle | loading | success | error 네 가지로 명시적으로 관리하고, isLoading/error를 별도 boolean으로 쪼개지 않았는가?
  • MSW handler가 POST/GET/DELETE /cards와 400 시나리오까지 포함하여 네트워크 경계에서 동작하는가?
  • 통합 테스트가 fetch·axios를 모킹하지 않고, MSW + RTL로 사용자 관점에서 작성되었는가?
  • RTL 요소 탐색이 getByRole → getByText → getByLabelText → getByTestId 우선순위를 따르고, 비동기 요소에 findBy*를 사용했는가?

iftype added 30 commits May 7, 2026 20:21
- 포맷 검사와 데이터 유효성 검사를 분리
- {도메인}.프로퍼티 모양으로
- 포커스 떄문에 핸들러에서 boolean값을 반환하게 되어 의미가 불명확해짐
Copy link
Copy Markdown

@eastroots92 eastroots92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

화이팅입니다!!

month: expiryDate.month.value,
year: expiryDate.year.value,
},
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1;

해당 질문과 연관된 리뷰라 여기에 남겨요.

네! 정답은 없지만 usePassword, useCvc 또한 제 생각엔 조금 과하단 생각이 들어요.
Cvc, Password의 코드 흐름을 방해하지 않기도 하고, 나눠서 얻을 잇점이 크지 않다고 생각하기 때문이에요.

다만 왜 이 hooks들을 만들었을까? 생각을 해보면 Page에서 선언하기 위해서라고 생각되는데요.

이미 Cvc, Password 등 컴포넌트를 만들어 두었는데 해당 로직들을 내부에서 관리하는게 좋지 않을까? 하는 생각이 들었어요.
내부라면 굳이 별도의 useCvc같은 훅은 불필요 하기도 하구요.

  • page > cardInfo 값 전체를 관리, useFocus 관련 값 관리.
  • 각 FormGroup > 각 요소에 맞는 로직 대응 (hook에서 관리하는 로직들은 여기로 옮기기)

같은 방식으로 풀어 볼 수 있을까요?
컴포넌트에서 사용할 로직인데 외부에서 주입해주는 부분이 어색하게 느껴지기도 하고, 외부에서 주입해줘야 할 이유가 크게 없는 것 같아서 제안드려봐요!

혹시 이렇게 해야하는 이유가 있다면 제게 이야기 해주셔도 좋아요!
추가로 만약 제가 제안해주신 방향을 적용한다면 이렇게 하면 무엇이 좋을지도 고민해보시면 좋겠어요!

Copy link
Copy Markdown
Author

@iftype iftype May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 제가 훅으로 분리한 이유는 다음과 같습니다!
(솔직히 잘못된 방향으로 가고있지않나 고민했습니다)

image

1. 버튼의 렌더링 조건은 모든 값이 유효한 지 확인하고 렌더링해야됐습니다. 그러기 위해서 모든 상태를 가져와 모든 요소가valid 한지 구해야했던 상황이라 페이먼츠로 상태를 옮겼어야 됐습니다.(이 당시 페이먼츠안에 폼 컨테이너가 혼합된 상태였습니다)
2. 부모에서 상태만 저장했을 시, 입력마다 valid를 검사하려면 useEffect로 값을 감시하거나handleChange에서 유효성 검증을 통해 함수를 실행해야됐습니다.
useEffect는 공식문서에서 확인한 내용으로 이벤트 처리에서 사용하지 말라고 이해하여
버튼컴포넌트의 조건을 가지고있는 Payments(최상단)에 handleChange를 만들 필요성을 느껴 로직이 최상단으로 올라가 훅으로 분리하게 됐습니다


해결완료했습니다.

잘못된 방향으로 가고있었으나, 루트의 조언으로 길을 찾았습니다 정말감사합니다!!!!
해결한 내용은 아래 코멘트로 남기겠습니다!!

@iftype
Copy link
Copy Markdown
Author

iftype commented May 15, 2026

감사합니다 루트 덕분에 숨막히게 하던 문제가 해결됐습니다!!
코멘트 가 없었다면 정말 갈피를 못 잡았을 것 같은데... 무한한 감사를 전합니다

컴포넌트를 세 개로 나누고, 커스텀 훅들을 제거해보며 해결방법이 보이기 시작했습니다.

Form 을 한 컴포넌트로

    <div>
      <CardPreview />
      <CardForm />
      <SubmitButton />}
    </div>

그럼에도 저는 나눴을 것 같은데요. 나누어 별도로 컴포넌트를 만드는 큰 이유는 Form에는 현재 입력값 뿐 아니라 렌더링 관련 맥락들이 모두 섞여있는데 이게 Preview와 전혀 무관한 로직들이라 생각되기 때문이에요.

반대로 Submit의 경우 Form 과 또 분리해서 만들었을 것 같기도 해요.
즉, 화면 단위에서 알아야 할 맥락을 기준으로 나누는 것 같아요.

위 리뷰를 듣고 두 가지 구조 변경을 시도했습니다.

  1. <form> 자체를 컴포넌트로 만들고, Submit버튼 나누기
  2. 렌더링 관련 맥락을 분리하기(컴포넌트 내부로)

프리뷰와 폼이 같은 계층으로 묶여있던 <Payments> 를 분리하며 Preview와 관련이 없는 step 관련 로직들을 <CardForm>안에 두기로 결정했습니다.

// 전의 구현방식
  const numbersField = useNumbers({onComplate: () => step(node,1) });

하지만 제 이전 구현방식은 이밴트 핸들러에서 로직 공유를 하기위해, 훅의 정의부분에 onComplate를 넣는 해결방식을 선택하였습니다.
이러한 방법은 상태가 끌어올려지면 랜더링 로직도 같이 끌어올려지는 문제가 있었습니다.

image

위 그림과 같이 책임을 분산시킬 수 있었고 적용 부분은 아래에 작성하였습니다.

// 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의 렌더 조건에 따라 Payments에서 상태 저장의 책임을 뒀습니다.

// 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등록과 onComplate 시 행동을 만들어서 내려줬습니다.

// 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를 분리하며

이미 Cvc, Password 등 컴포넌트를 만들어 두었는데 해당 로직들을 내부에서 관리하는게 좋지 않을까? 하는 생각이 들었어요.
내부라면 굳이 별도의 useCvc같은 훅은 불필요 하기도 하구요.

해당 부분을 제대로 이해하지 못한 것은 로직을 내부에서 관리하라는 것이 상태를 내부에 관리하라는 뜻으로 이해해버려서 문제가 됐었습니다!!
이제 제가 위 리뷰에 대해서 이해한 바로는 데이터 상태의 저장 책임과 UI 관심사를 분리해도 된다는 것이었습니다.

정말 감사드립니다!!


가벼운 추가 질문

루트 기준에서 지금의 제 코드를 볼 때 흐름을 따라가기 힘들었다 하는 부분이 있었나요? 있다면 솔직하게 말씀주세요!!

FSD를 가볍게 적용하다, 폴더 구조가 더 어려워 진 것 같아 step3에 변경해보도록 하겠습니다


리뷰 요청을 하고나서 늦게 깨달은 문제들

리뷰요청 이후에는 추가푸시가 금지 되어있어 늦게 알았지만 수정하지 못한 문제들입니다..

라우팅은 페이지에서 submit 이벤트는 폼에서

지금은 Page === Form으로 1:1 매칭이지만 이후 Page에 여러 Form이 존재하거나, 여러 기능들이 섞여있다고 생각하면 조금 더 쉽게 이해되지 않을까 생각해요.

레스의 리뷰를 늦게 확인했습니다. 페이지에서handleSubmit을 제거하고 아래 스텝과 똑같이 onSubmit이벤트에 네비게이터만 연결하여 이벤트를 만들어 내리고, CardForm안의 핸들링에서 사용하도록 변경 필요성을 느꼈습니다

엔티티에 섞여있는 책임들

// 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 로 분리해야한다고 느꼈습니다.
해당 부분도 수정하겠습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants