Skip to content

[1단계 - 페이먼츠 미션] 크리스(이송원) 미션 제출합니다#29

Merged
HyeonaKwon merged 60 commits into
woowacourse:swon3210from
swon3210:chris-step1
May 15, 2021
Merged

[1단계 - 페이먼츠 미션] 크리스(이송원) 미션 제출합니다#29
HyeonaKwon merged 60 commits into
woowacourse:swon3210from
swon3210:chris-step1

Conversation

@swon3210
Copy link
Copy Markdown

@swon3210 swon3210 commented May 3, 2021

데모 페이지

URL : https://keen-wright-2fa546.netlify.app



미리보기

페이먼츠1



기타 링크

피그마 : https://www.figma.com/file/6jB9o86LCjv7skopVOPfTH/payments-Chris-ZZZoomo?node-id=0%3A1
스토리북 : https://upbeat-carson-a9553c.netlify.app/?path=/story/payments-navigationbutton--primary



컴포넌트 구분 시각화

카드목록
카드입력(입력전) - 카드이미지O
카드입력(입력후) - 카드이미지O
카드입력(카드사선택) - 카드이미지O
카드추가완료

  • 주황 : 최하단 컴포넌트(스타일링)
  • 빨강 : 컨테이너 컴포넌트(컴포넌트 그룹화 & 로직 전달)
  • 초록 : 페이지 컴포넌트 (페이지 구성 & 로직 전달)



프로젝트 구조도

페이먼츠 미션 컴포넌트 구조도



📝 Requirements

  • 컴포넌트 구조 확립 및 퍼블리싱

  • Storybook 단위 테스트

  • 카드 입력 페이지

    • 카드번호 형식이 올바른지 확인한다.
    • 이용자가 카드번호를 입력하면, 자동으로 카드 종류가 선택된다.
    • 이용자의 카드번호를 통해 카드 종류를 구분할 수 없을 경우, 수동으로 카드를 선택하도록 한다.
      • 카드를 선택할 때 Bottom Slider 가 토글된다.
      • backdrop 을 클릭시 입력 완료가 된다. 카드사를 선택하지 않았다면 입력을 완료할 수 없다.
    • 카드 만료일 입력이 형식에 올바른지 확인한다.
    • 카드 소유자 이름의 형식이 올바른지 확인한다
      • 1글자 이상 30자 이하
    • 보안 코드 입력이 형식에 올바른지 확인한다.
      • 3자리 숫자
      • ? 아이콘에 마우스를 올리거나 클릭하면 보안 코드에 대한 가이드가 출력된다(이미지)
    • 카드 비밀 번호는 한글자씩만 입력 가능하다.
      • 두번째 숫자가 입력되면 다른 모든 입력 형식이 유효할 때, '다음' 버튼이 화면상에 나타난다.
    • 다음 버튼을 누르면 카드 추가 완료 페이지로 이동한다.
  • 카드 추가 완료 페이지

    • 카드의 이름(별칭)을 입력할 수 있다.
      • 페이지에 진입했을 때부터 카드 이름 입력 Input 에 focus가 되어 있다.
    • 확인 버튼을 눌러 카드 정보를 저장한다.
      • 카드 이름(별칭) 입력이 비어있다면 별칭을 지정하지 않은 채 진행할 것인지를 물어본다.
  • 카드 목록 페이지

    • 저장된 카드 목록을 조회할 수 있다.
    • 카드 추가 버튼을 클릭하여 카드 입력 페이지로 이동한다.



디렉터리 구조도

📦src
 ┣ 📂assets
 ┃ ┣ 📂images
 ┃ ┃ ┗ 📜card-cvc.png
 ┃ ┣ 📂scss
 ┃ ┃ ┣ 📂mixins
 ┃ ┃ ┃ ┣ 📜_breakpoint.scss
 ┃ ┃ ┃ ┣ 📜_normalize.scss
 ┃ ┃ ┃ ┗ 📜_shadow.scss
 ┃ ┃ ┣ 📂variables
 ┃ ┃ ┃ ┣ 📜_color.scss
 ┃ ┃ ┃ ┗ 📜_size.scss
 ┃ ┃ ┗ 📜index.scss
 ┃ ┗ 📂svgs
 ┃ ┃ ┣ 📜left-arrow.svg
 ┃ ┃ ┗ 📜question-mark.svg
 ┣ 📂components
 ┃ ┣ 📂AddBoxButton
 ┃ ┃ ┣ 📜AddBoxButton.jsx
 ┃ ┃ ┣ 📜AddBoxButton.module.scss
 ┃ ┃ ┗ 📜AddBoxButton.stories.jsx
 ┃ ┣ 📂BackDrop
 ┃ ┃ ┣ 📜BackDrop.jsx
 ┃ ┃ ┣ 📜BackDrop.module.scss
 ┃ ┃ ┗ 📜BackDrop.stories.jsx
 ┃ ┣ 📂BorderInput
 ┃ ┃ ┣ 📜BorderInput.jsx
 ┃ ┃ ┣ 📜BorderInput.module.scss
 ┃ ┃ ┗ 📜BorderInput.stories.jsx
 ┃ ┣ 📂BottomSlider
 ┃ ┃ ┣ 📜BottomSlider.jsx
 ┃ ┃ ┣ 📜BottomSlider.module.scss
 ┃ ┃ ┗ 📜BottomSlider.stories.jsx
 ┃ ┣ 📂Button
 ┃ ┃ ┣ 📜Button.jsx
 ┃ ┃ ┣ 📜Button.module.scss
 ┃ ┃ ┗ 📜Button.stories.jsx
 ┃ ┣ 📂Card
 ┃ ┃ ┣ 📜Card.jsx
 ┃ ┃ ┣ 📜Card.module.scss
 ┃ ┃ ┗ 📜Card.stories.jsx
 ┃ ┣ 📂CardExpirationInput
 ┃ ┃ ┣ 📜CardExpirationInput.jsx
 ┃ ┃ ┣ 📜CardExpirationInput.module.scss
 ┃ ┃ ┗ 📜CardExpirationInput.stories.jsx
 ┃ ┣ 📂CircleButton
 ┃ ┃ ┣ 📜CircleButton.jsx
 ┃ ┃ ┣ 📜CircleButton.module.scss
 ┃ ┃ ┗ 📜CircleButton.stories.jsx
 ┃ ┣ 📂Input
 ┃ ┃ ┣ 📂GuideInput
 ┃ ┃ ┃ ┣ 📜GuideInput.jsx
 ┃ ┃ ┃ ┣ 📜GuideInput.module.scss
 ┃ ┃ ┃ ┗ 📜GuideInput.stories.jsx
 ┃ ┃ ┣ 📂TextLimitInput
 ┃ ┃ ┃ ┣ 📜TextLimitInput.jsx
 ┃ ┃ ┃ ┣ 📜TextLimitInput.module.scss
 ┃ ┃ ┃ ┗ 📜TextLimitInput.stories.jsx
 ┃ ┃ ┣ 📜Input.jsx
 ┃ ┃ ┣ 📜Input.module.scss
 ┃ ┃ ┗ 📜Input.stories.jsx
 ┃ ┣ 📂InputBoxList
 ┃ ┃ ┣ 📜InputBoxList.jsx
 ┃ ┃ ┣ 📜InputBoxList.module.scss
 ┃ ┃ ┗ 📜InputBoxList.stories.jsx
 ┃ ┣ 📂Label
 ┃ ┃ ┣ 📜Label.jsx
 ┃ ┃ ┣ 📜Label.module.scss
 ┃ ┃ ┗ 📜Label.stories.jsx
 ┃ ┣ 📂NavigationButton
 ┃ ┃ ┣ 📜NavigationButton.jsx
 ┃ ┃ ┣ 📜NavigationButton.module.scss
 ┃ ┃ ┗ 📜NavigationButton.stories.jsx
 ┃ ┗ 📂SeperatedInputList
 ┃ ┃ ┣ 📜SeperatedInputList.jsx
 ┃ ┃ ┣ 📜SeperatedInputList.module.scss
 ┃ ┃ ┗ 📜SeperatedInputList.stories.jsx
 ┣ 📂containers
 ┃ ┣ 📂CardCompanySelectContainer
 ┃ ┃ ┣ 📜CardCompanySelectContainer.jsx
 ┃ ┃ ┗ 📜CardCompanySelectContainer.module.scss
 ┃ ┣ 📂CardInputContainer
 ┃ ┃ ┣ 📜CardInputContainer.jsx
 ┃ ┃ ┗ 📜CardInputContainer.module.scss
 ┃ ┗ 📂CardListContainer
 ┃ ┃ ┣ 📜CardListContainer.jsx
 ┃ ┃ ┗ 📜CardListContainer.module.scss
 ┣ 📂data
 ┃ ┗ 📜banks.json
 ┣ 📂hooks
 ┃ ┣ 📜cardCompanyHook.js
 ┃ ┣ 📜cardCVCHook.js
 ┃ ┣ 📜cardExpirationHook.js
 ┃ ┣ 📜cardListHook.js
 ┃ ┣ 📜cardNickNameHook.js
 ┃ ┣ 📜cardNumberHook.js
 ┃ ┣ 📜cardOwnerHook.js
 ┃ ┣ 📜cardPasswordHook.js
 ┃ ┗ 📜toggleHook.js
 ┣ 📂pages
 ┃ ┣ 📂AddCardCompletePage
 ┃ ┃ ┣ 📜AddCardCompletePage.jsx
 ┃ ┃ ┗ 📜AddCardCompletePage.module.scss
 ┃ ┣ 📂AddCardPage
 ┃ ┃ ┣ 📜AddCardPage.jsx
 ┃ ┃ ┗ 📜AddCardPage.module.scss
 ┃ ┗ 📂CardListPage
 ┃ ┃ ┣ 📜CardListPage.jsx
 ┃ ┃ ┗ 📜CardListPage.module.scss
 ┣ 📂utils
 ┃ ┣ 📜appConfirm.js
 ┃ ┣ 📜cardCompany.js
 ┃ ┗ 📜cardInputValidation.js
 ┣ 📜animation.scss
 ┣ 📜App.js
 ┣ 📜app.scss
 ┣ 📜constants.js
 ┣ 📜index.js
 ┣ 📜mediaQueries.scss
 ┣ 📜normalize.scss



궁금한 점

  • 대부분의 비즈니스 로직을 저는 커스텀 훅을 통해 관리하고 있습니다. 이렇게 하면 로직 자체를 재사용할 수 있는 기회가 늘어나리라 생각했기 때문입니다. 그러나 이렇게 하면 어떤 컴포넌트든 import 를 통해 해당 도메인의 로직을 수행해버릴 수 있는 위험도가 늘어날 수 있을까요? 제가 실제로 커스텀 훅을 적절히 사용하고 있는지가 궁금합니다.

  • 지금은 시간 상의 문제로 인해 아주 기본적인 모바일 - 타블렛 - 데스크탑의 반응형 디자인이 구현되어 있습니다. 하지만 이를 위해 미디어 쿼리를 중복해서 실행하고 있는 부분이 간간히 있는데요, 지금 당장은 사용되고 있는 미디어 쿼리의 수 자체가 적어서 눈에 띄는 성능상으 이슈는 없지만, 이러한 미디어 쿼리의 중복 실행이 성능에 어느 정도 유의미한 영향을 줄 수 있는지가 궁금합니다.


코드예시1
코드예시2



알려드릴 점

  • components 폴더에는 'CardExpirationInput' 라는, 다른 최하단 컴포넌트와는 달리 특정 역할에 종속적인 것처럼 보이는 컴포넌트가 하나 있습니다. 이 컴포넌트를 'SeperatedInputList' 컴포넌트를 좀 더 추상화하여 대체시키고 싶은데, 그러면 'SeperatedInputList' 최하단 컴포넌트가 처리할 수 있는 유즈 케이스가 너무 많은 것은 아닌지에 대한 고민을 하고 있습니다. 컴포넌트의 추상화를 어디까지 진행해야 하는지에 대한 질문을 좀 더 생각을 정리해서 다음 PR 때 드리도록 하겠습니다.

swon3210 and others added 30 commits April 20, 2021 23:50
Co-authored-by: JUNMO HAN <wnsah052@naver.com>
Co-authored-by: JUNMO HAN <wnsah052@naver.com>
Co-authored-by: JUNMO HAN <wnsah052@naver.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: JUNMO HAN <wnsah052@naver.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
swon3210 and others added 17 commits May 1, 2021 01:40
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Co-authored-by: SONG WON LEE <swon3210@users.noreply.github.com>
Copy link
Copy Markdown

@HyeonaKwon HyeonaKwon left a comment

Choose a reason for hiding this comment

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

안녕하세요 크리스님! 리뷰 많이 늦어 정말 죄송합니다😭

세부 피드백은 코멘트로 확인해주시구 아래는 전체적인 피드백 말씀드릴게요~

  1. 공통 컴포넌트를 전체적으로 사용하고 있지는 않은 것 같아요. 만들었으면 사용을 해야겠죠~? label 과 input 을 공통 컴포넌트로 바꿔보면 좋을 것 같습니다!
  2. setCardStateByKey 를 모든 hook 에서 한번더 래핑할 정도로 useCallback 을 사용할 필요가 있었나 싶긴합니다. 그래도 각 hook 이 정형화되어 있어서 코드 읽기는 편하네요! 그리고 질문주신 내용은 어차피 import 해서 사용할 때마다 새로운 값으로 저장될 것이기 때문에 문제는 없어보입니다! 그러려고 hook 을 쓰는거니까요.
  3. 이 정도의 미디어쿼리 양은 전혀 영향을 주지는 않겠지만, 모을 수 있다면 모아두는게 좋을 것 같네요! 큰 문제는 없을 것 같습니다.

Comment thread src/App.js
...state,
[cardStateKey]: cardStateValue,
}));
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

감사합니다!

Comment thread src/App.js
<Route>
<CardListPage cardListState={cardListState} />
</Route>
</Switch>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

오 Route 를 적용하셨네요 👍

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

<Route path="/photo" component={컴포넌트이름}/>

요런 형식도 고민해봐주세요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

이런 형식으로 작성하면 prop 넘겨주는게 힘들다고 판단해서 지금의 형식을 택했습니다. 저 형식으로도 prop 을 넘겨주는 방법이 있을까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

아하 넵! 넘기려면 함수의 return 값 형태로 넘길 수 있겠죠?!

maxLength="4"
className={cx("card__number-input", "card__number-input--visible")}
defaultValue={firstCardNumber}
disabled
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

input 네개가 disabled 인 이유가 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

이 부분은 이용자가 입력해서 바꿀 수 없게 만들고 싶었기 때문입니다. 카드 번호를 보여줄 때, 뒤 8 글자는 점으로 숨겨서 보여줘야 했기 때문에 따로 점을 보여주는 컴포넌트를 만들기보다는 input 의 타입을 password 로 해서 그냥 그렇게 해서 나타나는 점들을 이용하고 싶었었고, 뒤 8글자를 input 태그로 썼기 때문에 일관성을 지켜주고 싶어서 앞의 8글자도 input 태그로 만들고 diabled 속성을 준 것입니다.

그러나 지금은 이게 좋지 않은 방법이라는 것을 알게 되었습니다. 다른 브라우저나 OS 에서 접속했을 때 해당 부분이 많이 깨지는게 보이는군요. 고쳐서 반영하도록 하겠습니다!

Comment thread src/components/Card/Card.jsx Outdated
</div>
<div className={cx("card__bottom")}>
<span className={cx("card__owner")}>{cardOwner}</span>
<span className={cx("card__expiration")}>{Object.values(cardExpiration).every(value => value !== "") && Object.values(cardExpiration).join(" / ")}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요런거는 위에서 변수로 선언하는게 좋을 것 같아요! 어떤 의민지 코드를 다 읽어봐야하니까요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

아아...! 그렇네요 반영했습니다!

className={cx("card-expiration-input__input")}
maxLength={2}
onChange={onInputChange}
placeholder={yearPlaceholder}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

공통 컴포넌트가 아닌 구체적인 컴포넌트인데도 플레이스 홀더를 props로 받는 이유가 있을까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

제가 이전에는 공통 컴포넌트와 구체적인 컴포넌트의 차이를 완전히 견지하지 못하고 있었기 때문에 이 부분에 대한 충분한 고민이 부족했습니다. CardExpirationInput 이라는 컴포넌트 자체를 지우고 기존의 컴포넌트 중 SeperatedInputList 컴포넌트가 역할을 대신하도록 만들었습니다.

이렇게 하면 placeholder를 받아도 무리가 없을까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

넵 좋습니당!

Comment thread src/hooks/toggleHook.js Outdated
moveAnimation: "move-down",
}));
}, 350);
}, [setState]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

setToggled, setUntoggled 둘 다 두번째 인자에 setState 를 넣는 이유가 있나요??

Copy link
Copy Markdown
Author

@swon3210 swon3210 May 14, 2021

Choose a reason for hiding this comment

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

CRA 의 린터가 내부의 로직이 setState 함수에 의존하고 있기 때문에 dependency 에 setState 를 추가해야 한다고 생각했기 때문입니다. 그런데 setState 는 결국 리액트에서 컴포넌트가 unmount 되지만 않는다면 매번 새로 함수를 생성하지 않기 때문에 굳이 이러한 처리를 할 필요가 없는 것이라는 말씀이시군요!

디펜던시 비우고 린터가 이 부분 지적하지 않도록 주석 넣어주도록 하겠습니다!

Comment thread src/hooks/toggleHook.js
};
};

export default useToggle;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hook 깔끔하네요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

감사합니다!

Comment thread src/utils/cardCompany.js
import cardInfo from "../data/banks.json";

const preprocessing = (cardNumber) => {
return cardNumber.replace(/[^0-9]/g, "").slice(0, 6);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

정규식 👍

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

감사합니다!

Comment thread src/utils/cardCompany.js
(card) => card.name === cardCompany
);

return card?.color;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기서 card 가 없다면 undefined 가 반환되는 것 같은데, 25라인처럼 default 컬러를 반환해줄 필요는 없을까요??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

예외처리에 대한 꼼꼼함이 아직 부족하다는 것을 많이 느끼게 되네요...말씀하신대로 반영하도록 하겠습니다!

}
if (numberText instanceof Array) {
return numberText.every((value) => value.length === length);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

파라미터 명도 numberText 로 string 이겠지? 생각이 들게 하는 변수명인데, 실제 내부에서는 객체 또는 배열로 받고 있는게 어색해보입니다.
그리고 근본적으로 numberText 가 여러 타입인 이유는 무엇일까요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

해당 코드가 쓰인 isAllNumberListTextLengthCorrect 라는 함수가 object 도, list 도 받아서 처리할 수 있는 '편리한' 함수로 만들고 싶었기 때문입니다. 하지만 지금은 이게 성급한 추상화라는 생각이 드네요. 나름 이름을 일반화 해보려고 numberText 라는 이름을 쓴 것이지만 오히려 혼란만 키운 것 같습니다. 당시 급하게 내느라 신경을 많이 쓰지 못한 부분이 보이는 것 같습니다.

해당 함수를

isAllTextFilledInList
isAllTextFilledInObject

로 나누어 리팩토링 했습니다.

Copy link
Copy Markdown

@HyeonaKwon HyeonaKwon left a comment

Choose a reason for hiding this comment

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

isAllNumberTextLengthCorrect 를 checkAllValueLength() 이런식으로 범용적으로 바꿨어도 좋았을 것 같네요! :)
수정사항 잘 봤습니다. 반영해주셔서 감사해요! 👍 고생하셨습니다.

@HyeonaKwon HyeonaKwon merged commit c1c39ef into woowacourse:swon3210 May 15, 2021
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.

3 participants