
mall에서 상품권 판매를 시작했다. 상품권 코드를 입력하여 등록하는 컴포넌트를 추가적으로 만들었다. 일단 생각 없이 화면을 그려보니 대충 이런 중복 파티 코드가 나왔었다.ㅋㅋ
const firstRef = useRef<HTMLInputElement>(null);
const secondRef = useRef<HTMLInputElement>(null);
const thirdRef = useRef<HTMLInputElement>(null);
...
<input
ref={firstRef}
onChange={changeFirst}
/>
<input
ref={secondRef}
onChange={changeSecond}
/>
<input
ref={thridRef}
onChange={changeThird}
/>
...
아무리 그래도 그렇지 이건 좀 아니지 않나! 저 똑같은 input들을 map 함수를 이용해야겠다고 생각했다. 그럼 ref는 어떻게 주지? 방법은 이랬다.(함수의 바디를 꼭 {} 으로 감싸주어야 함)
const inputs = useRef<(HTMLInputElement | null)[]>([]);
...
{CODE_INPUT_KEYS.map((codeInput, idx) => (
<div key={codeInput} className='d-flex align-itmes-center'>
<input
className='form_input center'
ref={(el) => {
inputs.current[idx] = el;
}}
onChange={...}
onFocus={...}
/>
{idx !== CODE_INPUT_CNT - 1 && (
<span className='m03rem'>-</span>
}}
</div>
))}
그리고 onChange 함수를 사용할 땐 idx값을 같이 넘겨줘서 해당 input dom에 접근할 수 있다.
코드의 형식은 11111-22222-33333와 같은 형식이다. 유저가 직접 입력할 땐 5자리가 되면 자동으로 다음으로 포커스가 이동해야하고, 하이픈이 포함된 코드를 복사 붙여넣기하면 split되어 각각 제 자리에 입력되어야 한다.
onChange={(evt) => {
const removedHyphen = evt.target.value.replace(
/(^-|-$)/g,
''
);
if (removedHyphen.length >= 5)
inputHandler(removedHyphen, idx);
}}
우선 유저가 ‘-22222-33333’ 만 복사할 수도 있고 ‘-22222-’ 만 복사할 수도 있다고,,, 가능성을 열어두었다. 그래서 양 끝 하이픈을 제거한 후 길이가 5 이상이 되면 inputHandler() 함수를 호출한다.
inputHandler()와 splitCode() 함수의 내용은 다음과 같다.
const splitCode = (code: string, idx: number) => {
const codes = code.split('-');
if (codes.length >= 2) {
const codeCnt = codes.length;
const startIndex = codeCnt === 3 ? 0 : idx;
codes.forEach((codePerInput, codeIdx) => {
const inputDom =
inputs.current[
(codeCnt === CODE_INPUT_CNT - 1 && idx === 2
? idx - 1
: startIndex) + codeIdx
];
if (inputDom) inputDom.value = codePerInput;
});
}
};
유저가 전체 코드를 복사 붙여넣기 한 경우 첫 번 째 인풋 박스부터 차례대로 붙여넣기를 하면 되기 때문에 ‘-’기준으로 split한 코드의 길이(codeCnt)가 3이면 0번 인덱스의 input 부터 채워넣으면 된다.
만약 ‘22222-33333’와 같이 두 칸 만큼의 코드를 복사를 한 경우 첫 번째 혹은 두 번째 인풋 박스에 붙여넣기를 했다면 해당 인풋 박스와 그 다음 인풋 박스에 채워 넣으면 되지만 마지막 인풋박스에 붙여넣기를 했다면 해당 인풋 박스와 그 이전 인풋 박스에 채워 넣어야 한다.
const inputHandler = (code: string, idx: number) => {
if (code.length === CODE_INPUT_LENGTH) {
const inputDom = inputs.current[idx];
if (inputDom) inputDom.value = code;
nextFocus(idx);
return;
}
splitCode(code, idx);
};
만약 유저가 복붙이 아니라 직접 입력한다면 5자가 되었을 때 다음으로 포커스를 이동해준다. 이 inputHandler() 함수는 value의 길이가 5이상일 때만 호출되므로 if문에 걸리지 않았다면 길이는 6이상인 셈이다. 이때 복사 붙여넣기를 했다고 가정한다. (사실 그럴 수 밖에 없음)
코드는 숫자 15자리로 이루어져있다. 하이픈까지 포함하면 17자리이다. 만약 17자리가 아니거나, 중간에 문자가 포함되어있으면 이는 서버로 요청을 보낼 필요가 없다. 서버의 부담을 줄이기 위해 유효하지 않은 코드는 클라이언트단에서 컷 하는게 좋겠다.
const validationCheckAndRegist = (code: string) => {
const codeLength =
CODE_INPUT_LENGTH * CODE_INPUT_CNT + (CODE_INPUT_CNT - 1);
const isNotOnlyNumber = code.match(/(?!-.)(?!\d)./gi);
if (code.length !== codeLength || isNotOnlyNumber) {
setValidState(2);
return;
}
// 조회
const validCoupon = await isValidCoupon(code);
if (!validCoupon) {
setValidState(2);
return;
}
if (validCoupon.customer) {
setValidState(1);
return;
}
// 등록
regist(validCoupon.id);
});
여기서 등록 버튼을 빠르게 연속적으로 누루면 어떻게 되지? 숫자로만 이루어진 15자리의 코드를 입력했다면 등록 버튼을 누르는 만큼 서버로 조회 요청을 보낼것이고, 등록 가능한 유효한 코드라면? 상품권이 중복으로 등록될 수도 있다. 이를 방지하고자 useOnce hook을 만들었다.
일단 once() 함수의 모양은 다음과 같다.
const once = (fn: Function) => {
let isDone = false;
return (...args: any) => {
if (!isDone) {
isDone = true;
fn(...args);
}
};
};
이를 useOnce hook으로 만들어보자.
export function useOnce(fn: Function) {
const [isDone, setIsDone] = useState(false);
const [values, setValues] = useState<any[]>([]);
return (...args: any[]) => {
const isSameArgs = values.every((value, idx) => args[idx] === value);
if (!isDone || !isSameArgs) {
setIsDone(true);
setValues(args);
fn(...args);
}
};
}
음.. 일단 등록 버튼이 단 한 번만 동작하는 것은 안 된다. 같은 코드에 대해서만 한 번만 실행 되어야 하고 입력한 코드가 달라지면 조회든 등록이든 한 번은 실행 되어야 한다.
따라서 함수가 호출 되었는지를 나타내는 isDone 상태와 함께 매개변수가 동일한지를 판단하기 위해 values 상태도 함께 갖는다. 함수가 호출된 적이 있고, 똑같은 값이 동일한 위치에 전달 되었다면 이 때만 다시 콜백 함수를 호출하지 않는다!