Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2단계 - 페이먼츠] - 에프이(박철민) 미션 제출합니다. #382

Merged
merged 53 commits into from
Apr 29, 2024

Conversation

chysis
Copy link
Member

@chysis chysis commented Apr 26, 2024

안녕하세요 카일! step2의 구현을 마치고 돌아왔습니다. step1에 비해 구현할 것도 많아지고 코드도 길어지다 보니 로직을 분리하기 위해 커스텀 훅을 작성하는 데 집중했습니다.

🌐 배포 주소

App: 바로 가기

Storybook: 바로 가기

📚구조

파일 구조 트리 확인하기

📦src
┣ 📂assets
┃ ┣ 📜cardAdded.png
┃ ┣ 📜ellipse.png
┃ ┣ 📜masterCard.png
┃ ┗ 📜visaCard.png
┣ 📂components
┃ ┣ 📂CardConfirmButton
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂CardCVCNumberInput
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂CardNumberInput
┃ ┃ ┣ 📜index.stories.tsx
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂CardOwnerNameInput
┃ ┃ ┣ 📜index.stories.tsx
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂CardPasswordInput
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂CardPreview
┃ ┃ ┣ 📜CardCVCNumberDisplay.tsx
┃ ┃ ┣ 📜CardNumberDisplay.tsx
┃ ┃ ┣ 📜CardPreview.stories.tsx
┃ ┃ ┣ 📜CardPreview.tsx
┃ ┃ ┣ 📜CardText.tsx
┃ ┃ ┗ 📜ExpirationDateDisplay.tsx
┃ ┣ 📂CardSubmitButton
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂common
┃ ┃ ┣ 📜Arrows.tsx
┃ ┃ ┣ 📜Caption.stories.tsx
┃ ┃ ┣ 📜Caption.tsx
┃ ┃ ┣ 📜Dropdown.stories.tsx
┃ ┃ ┣ 📜Dropdown.tsx
┃ ┃ ┣ 📜Input.stories.tsx
┃ ┃ ┣ 📜Input.tsx
┃ ┃ ┣ 📜Label.tsx
┃ ┃ ┗ 📜Title.tsx
┃ ┣ 📂ExpirationDateInput
┃ ┃ ┣ 📜index.stories.tsx
┃ ┃ ┗ 📜index.tsx
┃ ┗ 📂layout
┃ ┃ ┣ 📜AppLayout.tsx
┃ ┃ ┣ 📜ErrorPageLayout.tsx
┃ ┃ ┣ 📜InputPageLayout.tsx
┃ ┃ ┗ 📜SubmitPageLayout.tsx
┣ 📂constants
┃ ┗ 📜card-app.ts
┣ 📂hooks
┃ ┣ 📜useCardCVCNumberInput.ts
┃ ┣ 📜useCardNumberInput.ts
┃ ┣ 📜useCardOwnerNameInput.ts
┃ ┣ 📜useCardPasswordInput.ts
┃ ┣ 📜useCardType.ts
┃ ┗ 📜useExpirationDateInput.ts
┣ 📂pages
┃ ┣ 📜CardSubmitPage.tsx
┃ ┣ 📜ErrorPage.tsx
┃ ┗ 📜NewCardInputPage.tsx
┣ 📂styles
┃ ┗ 📜global.ts
┣ 📂types
┃ ┣ 📜card.ts
┃ ┗ 📜index.d.ts
┣ 📂utils
┃ ┗ 📜errorCaption.tsx
┣ 📂validators
┃ ┗ 📜cardInputValidator.ts
┣ 📜App.css
┣ 📜App.tsx
┣ 📜index.css
┣ 📜main.tsx
┗ 📜vite-env.d.ts

커스텀 훅으로 도메인 로직과 UI 분리

  • 드롭다운을 제외한 5개의 input form에 대해 커스텀 훅을 작성해서 도메인 로직과 UI를 분리했습니다.
    • useCardNumberInput, useExpirationDateInput, useCardOwnerNameInput, useCardCVCNumberInput, useCardPasswordInput
  • 필요한 state 정보는 커스텀 훅 내부에서 관리하고, 기존의 input form 컴포넌트들은 필요한 정보들을 props로 받아와 UI만 출력합니다.
    • 다음 폼을 렌더링해도 되는지 여부를 나타내는 isNextVisible 값을 커스텀 훅 내부에서 계산하고 반환합니다.

❗기능 구현 사항

카드 정보 입력 폼 동적 생성

  • 카드 정보 입력 폼을 완성하고 유효성 검사를 통과하면 다음 폼이 위쪽에 렌더링됩니다.
  • 다음 폼이 렌더링된 이후, 이전 폼의 값을 변경해서 유효하지 않게 된 경우에도 다음 폼이 사라지지 않도록 구현했습니다.

오토 포커싱

  • 카드 번호의 경우 숫자 4자리를 모두 입력하면 다음 input으로 포커스가 이동합니다.
  • 유효 기간의 경우 숫자 2자리를 모두 입력하면 다음 input으로 포커스가 이동합니다.
  • 카드 소유자 이름의 경우 사용자가 Enter 키를 눌렀을 때 다음 폼으로 포커스가 이동합니다.

유효성 검사

  • 정규 표현식을 사용해서 폼에서 요구하는 입력 타입(숫자, 영문자 등) 이외의 타입을 입력할 수 없게 했습니다.
  • 카드 소유자 이름 입력 폼은 영문자만 입력 가능하며, 소문자를 입력하면 자동으로 대문자로 변환됩니다.

카드 제출

  • 각 폼이 모두 완성된 경우 화면 하단에 확인 버튼이 렌더링됩니다.
  • 확인 버튼을 클릭할 경우 제출 페이지로 이동하며, 카드 시작 번호카드사 정보가 표시됩니다.
    • 카드 시작 번호카드사 정보는 navigate의 state로 전달합니다.

에러 페이지

  1. 카드 정보가 입력되지 않은 상태(제출 페이지가 state로 null을 받는 경우)로 제출 페이지에 접근하는 경우
  2. 존재하지 않는 페이지에 접근하는 경우
  • 사용자가 url을 직접 조작했을 때 발생하는 위의 두 가지 경우에 대비하여 에러 페이지를 출력하도록 구현했습니다.
  • 하지만 github pages에서는 SPA의 클라이언트 사이드 라우팅을 지원하지 않기 때문에 직접 url을 조작해서 테스트할 수 없었습니다.
    • 마감일 전까지 해결 방법을 적용해보겠습니다!
에러 페이지 사진 보기

⁉️고민한 내용

  • 커스텀 훅으로 로직을 분리했지만 재사용성이 전혀 없다고 생각이 들었습니다.
    • 하지만 오늘 수업에서 ‘아직은 재사용성의 측면에서 유용함을 느끼기 어려운 작은 규모일 수 있기 때문에 관심사 분리 측면에서 접근해보면 좋을 것 같다’는 준의 의견을 참고했습니다. UI와 나머지 로직들을 분리하는 데 초점을 맞췄습니다😊

❓ 질문

  • 현재 카드 소유자 이름 입력 폼에서 Enter 키를 눌러야 다음 폼으로 넘어가는데, 모바일에서는 '다음' 버튼으로 보여 다음 폼으로 넘어가지지 않습니다.
모바일 구동 화면 보기

컴퓨터에서의 Enter키 이벤트를 모바일까지 고려하면 어떻게 처리해주는 것이 자연스러울지 카일의 의견이 궁금합니다! 일정 글자 수가 되면 다음으로 넘어가게 하는 방법도 생각해보았는데, 적절한 방법은 아닌 것 같습니다.

이번 리뷰도 잘 부탁드려요!

chysis added 30 commits April 23, 2024 20:32
@chysis chysis changed the title Step2 [2단계 - 페이먼츠] - 에프이(박철민) 미션 제출합니다. Apr 26, 2024
Copy link

@igy95 igy95 left a comment

Choose a reason for hiding this comment

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

안녕하세요 에프이~ 말씀 주신대로 커스텀 훅을 관심사 분리 측면에서 적용하려는 모습이 잘 보였습니다. 앱에서 테스트해보았을 때 UX도 깔끔하게 잘 만들어주셨네요 👍 세부 코멘트 확인 부탁드리고 질문에 대한 답변으로 마무리하도록 하겠습니다!

현재 카드 소유자 이름 입력 폼에서 Enter 키를 눌러야 다음 폼으로 넘어가는데, 모바일에서는 '다음' 버튼으로 보여 다음 폼으로 넘어가지지 않습니다.

제가 생각하는 바람직한 방법은, 실제 레퍼런스가 될 만한 다른 앱을 참고하여 그걸 바탕으로 구현 혹은 디자이너에 제안해볼 수 있을 듯해요. 인풋을 작성하는 UX는 자주 확인할 수 있으니 한번 찾아보시면 좋�겠습니다!

그리고 개인적으로 (가이드라인으로는 알고 있지만) 인풋 입력 후 엔터를 눌렀을 때 다음 인풋으로 넘어가는 것은 조금 우려되는 지점이 있을 것 같아요. 폼 내에 submit 버튼이 있을 경우 엔터 키를 눌렀을 때 폼 제출되는 동작이 native이기 때문에 이와 충돌되는 UX가 생길 수도 있을 듯해서요! 물론 이벤트가 상위로 전파되는 것을 막을 수는 있겠지만, 그보다는 다른 UX를 고려해봐도 괜찮지 않을까 싶습니다.

cardPassword: string;
}

export interface CardFormValidity {
Copy link

Choose a reason for hiding this comment

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

CardInfo와 강한 연관성을 띄는 파생 타입으로 보이는데, 필드를 일일이 직접 선언해주기보다 기존 타입에서 확장하는 방식으로 고민해보면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

CardFormValidity는 커스텀 훅으로 로직을 분리하기 이전에 NewCardInputPage에서 각 form별 상태를 state 하나로 관리하기 위해 사용했던 인터페이스였네요...! 지금은 사용하지 않는다는 것을 뒤늦게 확인한 점 죄송합니다😅

사용 여부와는 별개로, 저도 기존 타입을 이용하는 것이 더 적절하다고 생각합니다!
CardInfo를 순회하면서 key들을 CardFormValidity 타입에 동적 생성하면 될 것 같아요.

image

Comment on lines +33 to +34
onFocus={onFocusChange}
onBlur={onFocusChange}
Copy link

Choose a reason for hiding this comment

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

onFocusonBlur는 각기 다른 시점인데, 같은 핸들러로 처리하신 이유가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

각기 다른 시점이지만 두 이벤트 모두 카드 앞/뒷면 상태를 토글한다는 공통 action이 있기 때문에 하나의 핸들러로 처리했습니다.

{cardOwnerNameState.isNextVisible && (
  <>
    <Title content={CARD_META_INFO.cardCVCNumber.query} />
    <CardCVCNumberInput
      cardCVCNumber={cardCVCNumberState.value}
      cardCVCNumberError={cardCVCNumberState.error}
      onCardCVCNumberChange={handleCardCVCNumberChange}
      onFocusChange={() => setIsCardFront((prev) => !prev)} // 카드 face 토글
    />
  </>
)}

Copy link

@igy95 igy95 Apr 29, 2024

Choose a reason for hiding this comment

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

인터페이스를 제공하는 쪽이 사용하는 쪽의 상황에 맞추어 인터페이스를 수정하게 되면, 이는 컴포넌트 간 강결합을 불러일으키면서 재사용성을 점차 떨어트리게 됩니다. (이로 인해 제공하는 쪽은 어느정도 독립성을 가져야하는 이유이기도 합니다.) 예컨대 언젠가 focus, blur 시점에 각기 다른 이벤트를 처리해주고자하는 니즈가 생길 수 있는데, 현재와 같이 운용한다면 이를 대응하기가 아무래도 어려울 듯 해요. 때문에 인터페이스는 분리하고 사용하는 쪽에서 이 컨벤션을 맞추도록 하는 게 좋을 것 같습니다.

src/components/CardConfirmButton/index.tsx Show resolved Hide resolved
src/components/CardNumberInput/index.tsx Outdated Show resolved Hide resolved
<InputField>
<Label htmlFor='card-password'>{CARD_META_INFO.cardPassword.label}</Label>
<Input
id='card-password'
Copy link

Choose a reason for hiding this comment

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

현재는 크게 문제 없겠지만 어떠한 컴포넌트에서 id를 직접 선언할 때는, 해당 컴포넌트가 페이지 내에 여러 개 렌더링 될 가능성을 고려하여 유일값을 부여하는 것이 좋을 듯 합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

질문

{Array.from({ length: cardNumbers.length }, (_, index) => (
  <Input
    key={index}
    id={`card-number-${index}`}
    type='text'
    placeholder='1234'
    value={cardNumbers[index]}
    maxLength={INPUT_RULES.maxCardNumberPartLength}
    size='small'
    isError={cardNumberErrors[index]}
    onChange={(e) => onCardNumberChange(e, index)}
    autoFocus={index === 0}
  />
))}

4개의 input이 렌더링되는 CardNumberInput의 경우 length만큼 반복하면서 반환된 index 값을 사용해서 유일한 id를 설정했습니다.

하지만 map을 사용하지 않고 단순히 여러 개의 컴포넌트가 나열되는 경우를 생각해보았을 때, card-password-0과 같이 숫자를 하드코딩하는 방식은 좋지 않을 것 같습니다.

이런 이유로 uuid를 도입할 수 있을 것 같습니다.
혹시 카일이 의도하신 방법이 있는지, 그리고 uuid의 사용이 적절한지 궁금합니다!

Copy link

Choose a reason for hiding this comment

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

음 우선 id를 선언해야 하는 이유에 대해 먼저 생각해보면 좋을 것 같습니다.

  • 스타일 목적이라면 -> styled-component 사용
  • DOM 요소 조작 목적이라면 -> ref 사용

때문에 id가 따로 사용되어야 하는 이유가 있는지 먼저 자문해보면 어떨까 싶어요. (React 내에서 id를 사용하는 경우는 잘 없었던 것 같아서요!) 그럼에도 필요하다면, uuid 사용을 고려해볼 듯 합니다.

src/components/ExpirationDateInput/index.tsx Outdated Show resolved Hide resolved
Comment on lines 42 to 51
export const CARD_COMPANIES = [
'BC카드',
'신한카드',
'카카오뱅크',
'현대카드',
'우리카드',
'롯데카드',
'하나카드',
'국민카드',
];
Copy link

Choose a reason for hiding this comment

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

카드 회사 정보는 리터럴 자체에 의미를 가지고 있기 때문에 enum 혹은 union type으로 관리하는 것이 더 적절할 듯 합니다

type CardCompany = 'BC카드' | '신한카드' | ...

Copy link
Member Author

Choose a reason for hiding this comment

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

CARD_COMPANIES는 드롭다운의 option prop으로 전달하는 배열입니다!

카드사 이름을 union type으로 관리한다고 해도 위와 같이 타입을 모두 담고 있는 배열은 따로 정의해줘야 할 것 같습니다!

질문

카드 회사 정보와 그 배열을 관리하는 두 가지 방법을 시도해보았습니다.

  1. union type을 먼저 정의하고 CardCompany[] 타입의 옵션 배열을 만드는 방법입니다.
export type CardCompany =
  | 'BC카드'
  | '신한카드'
  | '카카오뱅크'
  | '현대카드'
  | '우리카드'
  | '롯데카드'
  | '하나카드'
  | '국민카드';

export const CARD_COMPANY_OPTIONS: CardCompany[] = [
  'BC카드',
  '신한카드',
  '카카오뱅크',
  '현대카드',
  '우리카드',
  '롯데카드',
  '하나카드',
  '국민카드',
];
  1. 카드 회사 배열을 바탕으로 union type을 만드는 방법입니다.
export const CARD_COMPANIES = [
  'BC카드',
  '신한카드',
  '카카오뱅크',
  '현대카드',
  '우리카드',
  '롯데카드',
  '하나카드',
  '국민카드',
] as const;

export type CardCompany = (typeof CARD_COMPANIES)[number];

image

두 번째 방법은 코드가 더 간결하지만 배열에서 타입을 뽑아내는, 선후 관계가 뒤집힌 느낌이 듭니다.
이런 경우 어떤 방법이 더 적절할지 궁금합니다!

Copy link

Choose a reason for hiding this comment

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

위 CARD_COMPANIES는 드롭다운의 option prop으로 전달하는 배열입니다!

넵 이 부분은 인지하고 코멘트 드린 것입니다! 컴포넌트 내에서 데이터와 연관된 타입을 사용해야하는 경우가 있다면, 대부분 데이터에 기반한 원형 타입 -> 특정 UI를 위한 타입으로 파생되는 데요, 기존과 같이 유지하거나 질문 주신 두번째 케이스인 경우에는 파생 타입에서 원형 타입을 추출하는 형태이기에 선후 관계가 뒤집힌 게 맞습니다.

따라서 첫번째 방식이 좀 더 적절하다고 볼 수 있겠습니다.

src/constants/card-app.ts Outdated Show resolved Hide resolved
src/hooks/useCardNumberInput.ts Outdated Show resolved Hide resolved
src/pages/NewCardInputPage.tsx Outdated Show resolved Hide resolved
@chysis
Copy link
Member Author

chysis commented Apr 28, 2024

안녕하세요 카일! 1차 피드백 반영을 완료했습니다.

🌐 배포 주소

App: [바로 가기]

Storybook: [바로 가기]

🔧수정 사항

Github Pages 라우팅 문제 해결

  • hash router를 사용하지 않고 사용자 지정 404.html을 추가해서 문제를 해결했습니다.
  • 직접 url을 조작해서 submit 페이지로 넘어가는 경우와 존재하지 않는 페이지로 이동하는 경우에 에러 페이지를 보여줍니다.

카드 소유자 이름 입력 폼

  • 모바일에서 Enter키 인식이 제대로 되지 않는 문제를 해결하기 위해 onBlur 이벤트를 추가했습니다. 포커스가 해제될 때 입력이 유효하다면 다음 폼으로 넘어갑니다.
  • Enter키로 넘어가는 방식 이외에 뾰족한 수가 떠오르지 않아 이 부분은 그대로 남겨두었습니다. Tab 키를 눌렀을 때 이동하는 방법도 시도해봤는데, 다음 input으로 전환되는 본래 기능과 충돌해서 사용성이 좋지 않았습니다.

공용 Button 컴포넌트 구현

  • 공용 Button 컴포넌트를 두고 여기에 필요한 속성과 스타일을 덧붙여 재사용성을 높이는 방향으로 리팩토링했습니다.

기타 수정 사항

  • 정규 표현식 상수화
  • 타입 관련 리팩토링

HTMLAttribute 속성 관련 문제 해결

HTMLAttributes 타입을 사용하는 것이 좀 더 정확한 타입 선언이지 않을까 싶네요~

  • step1 마지막에 남겨주셨던 내용입니다!
// 공용 Label 컴포넌트
interface LabelProps extends React.HTMLAttributes<HTMLLabelElement> {
  children: React.ReactNode;
}

const Label = ({ children, ...props }: LabelProps) => {
  return <StyledLabel {...props}>{children}</StyledLabel>;
};

Label 컴포넌트의 Props interface가 HTMLAttributes<HTMLLabelElement>를 확장하고 있습니다.

image
그리고 외부에서 Label 컴포넌트를 사용하는 곳에서 발생하는 오류입니다.

LabelHTMLAttributes<HTMLLabelAttribute> 인터페이스를 확장해서 문제를 해결했습니다. HTMLAttributes에는 htmlFor 속성이 포함되어 있지 않아 발생한 문제였습니다.

Label 뿐만 아니라 Input과 새로 추가한 Button 컴포넌트도 함께 수정했습니다.

❓질문

  • 이전에 남겨주신 코멘트에 추가 질문을 남겨두었습니다! 감사합니다.

Copy link

@igy95 igy95 left a comment

Choose a reason for hiding this comment

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

안녕하세요 에프이~! 코멘트가 꽤 많았음에도 잘 반영해주셨네요 ㅎㅎ 추가적으로 주신 질문들도 구체적이어서 좋았습니다. 해당 미션은 여기서 마무리하면 될 듯 하여 이만 머지할게요! 고생 많으셨습니다~~

@igy95 igy95 merged commit 7a345f7 into woowacourse:chysis Apr 29, 2024
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