우아한테크코스의 미션 중 신용카드 정보를 입력하는 커스텀 훅을 만드는 미션을 진행했다.
위의 사진과 같이 카드사 식별 번호에 따른 포매팅을 적용하였다.
카드 브랜드 구분 로직 (Diners / AMEX / UnionPay / etc)
Diners: 36으로 시작하는 14자리 숫자
- 예시: 3612 345678 9012
AMEX: 34, 37로 시작하는 15자리 숫자
- 예시 (34로 시작): 3412 345678 90123
- 예시 (37로 시작): 3712 345678 90123
유니온페이: 카드의 앞 번호가 아래 3가지 조건을 만족하는 16자리 숫자
- 622126~622925로 시작하는 경우: 6221 2612 3456 7890
- 624~626로 시작하는 경우: 6240 1234 5678 9012
- 6282~6288로 시작하는 경우: 6282 1234 5678 9012
etc: 그 외 나머지
- 예시: 1234 5678 2345 5678
React.ChangeEvent
함수에서 유저가 입력한 카드번호(e.target.value) 값을 가져온다.그래서 클라이언트가 123456789
을 입력하면 1234 5678 9
로 자동 포멧팅이 된 값을 확인할 수 있게 된다.
잘 보면 포매팅이 적용되는 순간부터 마우스 커서가 가장 뒤로 이동하는 것을 확인할 수 있다.
포매팅이 적용되면서 원래 state 값이 모두 변경되었기에 기존 커서 위치를 유지하지 못하고 가장 뒤로가게 되는 문제로 사용자 경험을 저하시키는 문제가 있었다.
그렇다면 커서 위치를 수동으로 계산하여 이동시켜줘야한다. 그래서 카드 번호 입력하는 커스텀 훅 내에 커서 위치를 계산하여 커서를 위치시키는 로직을 추가하였다.
const handleCardNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value, selectionStart } = e.target;
...
setTimeout(() => { // 커서 위치를 계산하여 위치시키는 로직
if (selectionStart === null) return;
if (formattedValue[selectionStart - 1] === " ") {
const newSelectionStart =
formattedValue.length > value.length ? selectionStart + 1 : selectionStart - 1;
return e.target.setSelectionRange(newSelectionStart, newSelectionStart);
}
e.target.setSelectionRange(selectionStart, selectionStart);
});
}
// selectionStart : 현재 유저의 커서 위치값
// value : 사용자의 입력값
// formattedValue : 입력값을 카드 브랜드에 맞게 새로 포매팅 적용된 값
그래서 handleCardNumberChange 훅에서 의도한 로직은
1. 유저가 값을 입력하면 커스텀 훅이 동작한다.
2. 유효성 검사 및 새로 포매팅을 적용한다.
3. 새로운 포매팅된 값으로 state 를 변경한다.
4. setTimeout 으로 커서 위치를 계산하여 커서 위치를 수동으로 위치시킨다.
내가 의도한 바는 useState 로 인해 input 값이 새로 리렌더링 후 setTimeout 의 콜백함수가 처리가 되어 커서 위치가 변경되는건데, 그럼 이때 반드시 setState 의 리렌더링 후 setTimeout 의 콜백함수가 동작하게 될까?
앞에 서론이 길었는데 직접 실험을 통해 해당 결과를 도출해보았다.
언뜻보면 문제가 없어보이는데 잘 보면 1234 5
가 있을 때 공백 앞 4에서 값을 입력했을 때 커서 위치가 어떤 경우에는 공백 앞에 위치하고 어떤 경우에는 공백 뒤에 위치하는 것을 확인할 수 있었다.
이러한 결과가 나오는 이유는 어떤 경우에는 setState 로 인한 렌더링이, 어떤 경우에는 setTimeout 의 콜백이 처리되기에 어떤게 먼저 처리되는지에 대한 보장이 없기 때문이다.
우선 setState 로 값이 변경이 된 후 race condition 이 작동한다.
해당 두 동작중에 먼저 처리되는 순서대로 동작이 진행된다.
(무엇이 먼저 실행될지에 대한 순서 보장 ❌)
그러니 setState와 setTimeout 을 같이 이용하여 처리하는게 아닌 안전하게 useEffect를 이용하여 구현하는게 좋다!