이번 페이먼츠 미션을 통해 사용자의 카드 정보를 저장하고 보여주는 애플리케이션을 제작했다. 페이먼츠 애플리케이션에는 카드 저장, 카드 별칭 등 여러 요구 사항이 있었다. 미션을 진행하며 가장 고민을 많이 하고 중점을 둔 요구 사항은 새로운 카드 등록 기능이었다. 이번 포스팅에서는 카드 등록 기능을 구현하며 겪었던 어려움과 이를 해결하는 과정들을 담았다.
카드 등록 페이지의 컴포넌트 구조는 위 사진과 같다. 사진을 보면 CardItem이라는 카드 모양 컴포넌트에서 사용자가 입력한 값을 실시간으로 반영해서 보여준다.
각 입력 값 상태를 폼 내부에서만 사용하는 것이 아니라 폼 외부의 다른 컴포넌트에서도 입력 상태를 알고 있어야한다. 다른 컴포넌트에 상태를 공유하기 위해 해당 입력값 상태를 끌어올려야 한다.
// 카드 등록 페이지
const CardRegistrationPage = () => {
// 카드 번호 입력 컴포넌트의 커스텀 훅
const { cardNumber, cardNumberErrorMessage, onChangeCardNumber } = useCardNumber();
// 만료일 입력 컴포넌트의 커스텀 훅
const { expirationDate, expirationDateErrorMessage, onChangeExpirationDate, onBlurExpirationDate } =
useExpirationDate();
// 소유자 이름 입력 컴포넌트의 커스텀 훅
const { name, nameErrorMessage, onChangeName } = useName();
// 보안 코드 입력 컴포넌트의 커스텀 훅
const { securityCode, securityCodeErrorMessage, onChangeSecurityCode } = useSecurityCode();
// 비밀번호 입력 컴포넌트의 커스텀 훅
const { password, passwordErrorMessage, onChangePassword } = usePassword();
...
}
Input의 갯수가 한 두개가 아니다 보니 끌어올려야 할 상태가 상당히 많아진다. 이로 인해 해당 상태들을 하나 하나 프롭스로 넘겨줘야 하는데, 이는 상당히 귀찮은 작업이고 또한 각 컴포넌트의 코드 가독성이 매우 떨어진다는 문제가 발생한다. 이 지저분한 상태와 함수들을 Context로 분리하여 컴포넌트의 가독성을 훨씬 깔끔하게 만들어주려고 한다.
먼저, 해당 데이터들을 전달 해줄 Provider를 다음과 같이 만들었다.
// CardItemProvider
// 각 Context의 타입 선언은 생략
const CardItemValueContext = createContext<CardItemValue | null>(null);
const CardItemActionContext = createContext<CardItemAction | null>(null);
const ErrorMessageValueContext = createContext<ErrorMessageValue | null>(null);
const CardItemProvider = ({ children }: CardItemProviderProps) => {
const { cardNumber, cardNumberErrorMessage, onChangeCardNumber } = useCardNumber();
const { expirationDate, expirationDateErrorMessage, onChangeExpirationDate, onBlurExpirationDate } =
useExpirationDate();
const { name, nameErrorMessage, onChangeName } = useName();
const { securityCode, securityCodeErrorMessage, onChangeSecurityCode } = useSecurityCode();
const { password, passwordErrorMessage, onChangePassword } = usePassword();
const cardItemValue = {
cardNumber,
expirationDate,
name,
securityCode,
password,
};
const cardItemAction = {
onChangeCardNumber,
onChangeExpirationDate,
onBlurExpirationDate,
onChangeName,
onChangeSecurityCode,
onChangePassword,
};
const errorMessage = {
cardNumberErrorMessage,
expirationDateErrorMessage,
nameErrorMessage,
securityCodeErrorMessage,
passwordErrorMessage,
};
return (
<CardItemValueContext.Provider value={cardItemValue}>
<CardItemActionContext.Provider value={cardItemAction}>
<ErrorMessageValueContext.Provider value={errorMessage}>{children}</ErrorMessageValueContext.Provider>
</CardItemActionContext.Provider>
</CardItemValueContext.Provider>
);
};
총 세가지의 컨텍스트 Provider를 하나로 묶어 CardItemProvider를 만들어줬다.
<CardItemProvider>
<CardRegistrationPage />
</CardItemProvider>
또한, 컨텍스트 사용처에서 쉽게 사용할 수 있도록 컨텍스트 별 커스텀 훅을 만들어줬다.
export const useCardItemValue = () => {
const value = useContext(CardItemValueContext);
if (value === null) {
throw new Error("CardItemValue 에러");
}
return value;
};
export const useCardItemAction = () => {
const value = useContext(CardItemActionContext);
if (value === null) {
throw new Error("CardItemAction 에러");
}
return value;
};
export const useErrorMessageValue = () => {
const value = useContext(ErrorMessageValueContext);
if (value === null) {
throw new Error("ErrorMessageValue 에러");
}
return value;
};
카드 등록 페이지에서 다음과 같이 컨텍스트 값들을 사용한다.
// 카드 번호 입력 컴포넌트
const CardNumberInput = () => {
const { cardNumber } = useCardItemValue();
const { onChangeCardNumber } = useCardItemAction();
const { cardNumberErrorMessage } = useErrorMessageValue();
...
}
// 카드 등록 페이지 컴포넌트
const CardRegistrationPage = () => {
const { cardNumber, expirationDate, name, company } = useCardItemValue();
...
return (
...
<CardItem card={cardNumber, expirationDate, name, company} />
...
);
기존에는 수많은 상태들과 이벤트 함수들을 컴포넌트 내부에서 선언하거나 프롭스로 넘겨줘야 하기 때문에 코드가 상당히 지저분 했었다. 컨텍스트를 도입하고 해당 데이터들을 컨텍스트로 옮기니 컴포넌트 코드의 가독성이 크게 증가했다. 또한 비즈니스 로직과 UI 로직의 관심사 분리도 이전보다 증가했다.