미치 프로젝트 피그마에 올라온 디자인이다.
안내 문구를 보면 닉네임 중복 검사 기능이 있다는 걸 알 수 있다. 그런데 중복 검사하는 버튼은? 없다.
요즘엔 이렇게 중복 검사, 검색 등 입력하는 값에 따라 즉각 반응하는 UI가 많다(특히 모바일).
그런데 이렇게 하면 언제 사용자가 입력을 끝낼지 모른다!
그래서 input 값이 바뀔 때(사용자가 키보드를 누를 때)마다 이벤트 핸들러가 실행되도록 해야 하는데, 그러면 쓸모없는 API 요청이 너무 많이 생기고 서버엔 과부하가 걸릴 수도 있다. 성능, 비용적인 측면에서도 좋지 않은 방법이다.
디바운싱(Debouncing)
이걸 해결할 수 있는 방법은 "디바운싱(Debouncing)"을 사용하는 것이다.
✨ 디바운싱이란,
연이어 호출되는 함수들 중 마지막(또는 처음) 함수만 호출되도록 하는 프로그래밍 기법
사용법은 간단하다.
- 이벤트 호출 시 실행될 함수는 setTimeout을 통해 일정 시간 후 호출되도록 한다(타이머 생성). 이 때 지연 시간은 너무 길지 않은 게 사용자 경험에 좋다.
- 이벤트가 실행되면 이전에 생성한 타이머가 있는지 확인하고, 있다면 삭제 후 새 타이머를 생성한다.
- 사용자의 이벤트가 지정한 시간 내에 연속 호출되면 계속해서 이전 타이머는 삭제되고, 결국 사용자의 이벤트 호출이 끝난 후 마지막 타이머만 실행된다.
const handleChangeId = (text: string) => {
setId(text);
...
// 중복 검사(api) 디바운싱
if (timer) {
clearTimeout(timer);
}
const newTimer = setTimeout(async () => {
await idValidation(text);
}, 500);
setTimer(newTimer);
};
이런 식으로 사용하면 된다!
여기서 검사(함수 실행)하는 동안 사용자에게 검사하고 있음을 알려주면 좋을 것 같아서 중간에 setCheckMessage("...")를 넣어줬다.
- 전체 코드
// 영문자로 시작해야 합니다.
// 영문자, 숫자, 밑줄(_)로만 이루어져야 합니다.
// 길이는 4자 이상 20자 이하여야 합니다.
const regex = /^[a-zA-Z][a-zA-Z0-9_]{3,19}$/;
const defaultMessage = "* 영어, 숫자, 밑줄(_)만 사용해주세요.\n* 4자 이상 20자 이내로 입력해주세요.";
export function Id(): React.JSX.Element {
const { top } = useSafeAreaInsets();
const navigation = useNavigation<NativeStackNavigationProp<SignUpRootStackParam>>();
const [id, setId] = useRecoilState(idState);
const [checkMessage, setCheckMessage] = useState<string>(defaultMessage);
const [isAvailable, setIsAvailable] = useState<boolean>(false);
const [timer, setTimer] = useState<ReturnType<typeof setTimeout>>(); // 디바운싱 타이머
useEffect(() => {
if (id) idValidation(id);
}, []);
const idValidation = async (text: string) => {
setIsAvailable(false);
if (!regex.test(text)) {
if (/[ㄱ-ㅎㅏ-ㅣ가-힣]/g.test(text)) {
setCheckMessage("* 한글은 사용할 수 없습니다.");
} else if (/[^a-zA-Z0-9_]/.test(text)) {
setCheckMessage("* 밑줄(_)을 제외한 특수문자, 공백은 사용할 수 없습니다.");
} else if (!/^[a-zA-Z]/.test(text)) {
setCheckMessage("* 영문자로 시작해야 합니다.");
} else if (text.length < 4) {
setCheckMessage("* 4자 이상 20자 이내로 입력해주세요.");
} else {
setCheckMessage(defaultMessage);
}
return;
}
try {
const res = await fetch(`${userUrl}/id-check/${text}`);
if (res.ok) {
setCheckMessage("사용 가능한 아이디입니다.");
setIsAvailable(true);
} else {
setCheckMessage("* 이미 사용 중인 아이디입니다.");
}
} catch (e) {
console.error("id duplicate check error", e);
setCheckMessage("* 사용할 수 없는 아이디입니다.");
}
};
const handleChangeId = (text: string) => {
setId(text);
// 빈 값일 때 타이머 제거
if (!text) {
clearTimeout(timer);
setCheckMessage(defaultMessage);
setIsAvailable(false);
return;
}
setCheckMessage("...");
setIsAvailable(false);
// 중복 검사(api) 디바운싱
if (timer) {
clearTimeout(timer);
}
const newTimer = setTimeout(async () => {
await idValidation(text);
}, 500);
setTimer(newTimer);
};
const handlePressNextButton = () => {
navigation.push("password");
};
return (
<View style={[commonStyles.container, { paddingTop: top }]}>
<ScrollView contentContainerStyle={commonStyles.scrollBox} showsVerticalScrollIndicator={false}>
<Title text="아이디를 입력해주세요" />
<TextInputField
label="아이디 ID"
placeholder="아이디를 입력해주세요"
value={id}
setValue={handleChangeId}
message={checkMessage}
maxLength={20}
isAvailable={isAvailable}
/>
</ScrollView>
<NextButton onPress={handlePressNextButton} disabled={!isAvailable} />
</View>
);
}
- 결과물
참고 자료