이번 페이먼츠 미션을 통해 사용자의 카드 정보를 저장하고 보여주는 애플리케이션을 제작했다. 페이먼츠 애플리케이션에는 카드 저장, 카드 별칭 등 여러 요구 사항이 있었다. 미션을 진행하며 가장 고민을 많이 하고 중점을 둔 요구 사항은 새로운 카드 등록 기능이었다. 이번 포스팅에서는 카드 등록 기능을 구현하며 겪었던 어려움과 이를 해결하는 과정들을 담았다.
새로운 카드를 등록할 때, 위와 같은 UI에서 카드 정보에 대한 폼을 받아야 한다. Input 형식에 대한 정확한 요구 사항은 명시 되어있지 않기 때문에 피그마를 보고 Input의 요구 사항을 다음과 같이 유추했다.
- 카드번호 input
- 숫자만 받아야 한다.
- 4글자마다 '-'를 붙여준다.
- 마지막 8글자는 카드 번호를 숨긴다.
- 만료일 input
- 숫자만 받아야 한다.
- 월과 연도로 따로 받는다
- 월과 연도는 두자리 숫자로 받는다
- 월과 연도 사이에는 '/' 붙여준다.
- 월은 1~12 까지만 입력할 수 있다.
- 소유자 이름 input
- 소유자 이름은 필수로 받지 않아도 된다.
- 최대 길이는 30자
- 소유자 이름은 영문과 띄어쓰기로만 받는다.
- 보안코드 input
- 숫자만 받는다.
- 3자리를 받는다.
- 값을 모두 숨긴다.
- 카드 비밀번호 input
- 숫자만 받는다.
- 한 칸에 한 숫자만 받는다.
- 값을 모두 숨긴다.
모두 같은 Text 타입의 Input이지만 입력값에 대한 형식이 전부 달랐다. 입력 검증 방식도 다르고, UI에 어떤 형태로 나타나야 하는지도 달랐다. 어떤 입력은 숫자만 입력 받기도 하고, 어떤 입력은 입력 값의 길이 제한도 있다. 또 어떤 입력들은 입력 값을 숨겨야 한다. 이렇게 가지각색의 Input이 필요하다.
물론 각 Input의 컴포넌트를 개별적으로 만들면 쉽게 해결할 문제이다. 하지만 우리는 재사용 되는 요소들을 보면 참을 수 없는 개발자가 아닌가. 이 Input들의 공통적인 부분은 어떻게 재사용성 있게 컴포넌트 구조를 구현할 지에 대해 많은 고민을 했다.
일단 각 Input 컴포넌트의 View(UI) 측면에서 재사용되는 요소들을 추려내면 다음과 같았다. 사진의 빨간 상자 부분이 각 요소들이다.
일단 가장 기본적인 Input이다. 모든 입력은 Input 컴포넌트를 사용한다. 입력 형식에 따라 여러 개의 Input을 사용하기도 한다. 카드 번호 입력의 경우 4자리 숫자마다 ‘-’으로 구분해야 하기 때문에 4개의 Input이 있고, 비밀번호 입력은 각각 한 자리씩 입력 받으므로 2개의 Input이 있다.
카드 번호, 만료일, 보안코드 등등 입력 값에 대한 정보를 표시해준다.
사용자가 값을 입력할 때 잘못 된 값을 입력하면 Input 아래에 검증에 대한 피드백을 준다.
Input을 감싸 사용자에게 색상으로 입력에 대한 구분감을 준다.
위에서 나열한 재사용 UI 요소들로부터 styled-components 라이브러리
를 사용해 다음과 같은 컴포넌트들을 도출해냈다.
const Input = styled.input<{ textAlign?: string }>`
width: 100%;
height: 100%;
border: none;
background: none;
font-weight: 600;
font-size: 18px;
outline: none;
text-align: ${({ textAlign }) => textAlign || 'center'};
`;
기본 input 태그를 스타일링 했다. Input에 따라 텍스트를 중앙 정렬하는 Input도 있고, 좌측 정렬 하는 Input도 있기 때문에 이 부분은 textAlign을 프롭스로 받아 확장성을 챙겼다.
const InputBox = styled.div<{ width?: string; isError?: boolean }>`
display: flex;
height: 45px;
width: ${({ width }) => width || "100%"};
padding: 12px;
box-sizing: border-box;
background: #ecebf1;
border: 1px solid ${({ isError }) => (isError ? "#ec2f1b" : "#ecebf1")};
border-radius: 7px;
`;
폼의 테두리를 감싸는 InputBox 컴포넌트이다. 마찬가지로 확장성이 필요한 부분은 프롭스로 해당 값을 받을 수 있도록 했다.
const InputGroup = ({ children, labelValue, errorMessage }: InputGroupProps) => {
return (
<InputGroupContainer>
<Label>{labelValue}</Label>
<div>{children}</div>
<ErrorMessage>{errorMessage}</ErrorMessage>
</InputGroupContainer>
);
};
Input에서 계속해서 재사용되는 요소는 label과 error message가 있었다. 이 두 요소들을 한 번에 묶어서 간단하게 사용할 수 있도록 InputGroup이라는 컴포넌트를 만들어줬다. props로 labelValue와 errorMessage를 받고, Input에 관한 내부 요소는 children으로 받게된다.
실제 각 입력 컴포넌트들은 위와 같은 컴포넌트들을 조합해서 사용한다. 5개의 입력 컴포넌트 중 카드 번호 입력 컴포넌트
와 비밀번호 입력 컴포넌트
에 대한 예시는 다음과 같다. 나머지 3개의 입력 컴포넌트에서도 유사하게 사용된다.
<InputGroup labelValue="카드 번호" errorMessage={cardNumberErrorMessage}>
<InputBox isError={!!cardNumberErrorMessage}>
<Input />
<InputSeparator>-</InputSeparator>
<Input />
<InputSeparator>-</InputSeparator>
<Input />
<InputSeparator>-</InputSeparator>
<Input />
</InputBox>
</InputGroup>
위 JSX 코드를 UI에서 시각화하면 다음과 같다.
<InputGroup labelValue="카드 비밀번호" errorMessage={passwordErrorMessage}>
<InputBox width="43px" isError={!!passwordErrorMessage}>
<Input />
</InputBox>
<InputBox width="43px" isError={!!passwordErrorMessage}>
<Input />
</InputBox>
... // DotIcon
</InputGroup>
위 JSX 코드를 UI에서 시각화하면 다음과 같다.
UI의 재사용 요소를 도출하고 이를 컴포넌트화하면서 재사용성을 챙기려고 노력했다. 그러나 아직 확장성에서 부족한 부분이 보인다. 이번 미션에 한에서는 요구 사항을 잘 지켰지만 앞으로 새로운 요구 사항이 많이 추가된다면 이에 잘 대응 할 수 있을까?
이 문제들을 해결하기 위해 컴포넌트의 재사용 키워드를 학습하다보니 합성 컴포넌트 패턴
이라는 개념을 알게되었다. 합성 컴포넌트 패턴은 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 컴포넌트 패턴을 의미한다.
내 코드에서 InputGroup과 InputBox 등을 나눈 뒤 조합해서 사용한 것과 상당히 유사해 보여서 조금 놀랐다. 하지만 나의 코드에는 메인 컴포넌트, 서브 컴포넌트가 명확하게 분리되어 있지 않고 UI에 대한 자유도도 합성 컴포넌트 패턴의 코드보다 떨어진다. 이 합성 컴포넌트에 대해 더 깊게 학습 후 나중에 적용해 봐야겠다.