디바운스(Debounce)
연속으로 호출되는 함수들 중에서 마지막 함수만 실행 되도록하는 기법.
짧은 시간 동안 여러 번 호출되는 함수가 있을 떄, 마지막 호출로부터 일정 시간이 지난 후에만 함수를 실행.
import { useEffect, useState } from 'react';
export function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Input Validation 예시
1. 이메일 유효성 검사 컴포넌트
import { useState, useEffect } from 'react';
import { useDebounce } from './useDebounce';
interface ValidationState {
isValid: boolean;
errorMessage: string;
isChecking: boolean;
}
export function EmailInput() {
const [email, setEmail] = useState('');
const [validation, setValidation] = useState<ValidationState>({
isValid: true,
errorMessage: '',
isChecking: false
});
// 500ms 후에 validation 체크
const debouncedEmail = useDebounce(email, 500);
useEffect(() => {
if (!debouncedEmail) {
setValidation({
isValid: true,
errorMessage: '',
isChecking: false
});
return;
}
// 유효성 검사 시작
setValidation(prev => ({ ...prev, isChecking: true }));
// 이메일 형식 검증
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValidFormat = emailRegex.test(debouncedEmail);
// 비동기 중복 검사 시뮬레이션
setTimeout(() => {
if (!isValidFormat) {
setValidation({
isValid: false,
errorMessage: '올바른 이메일 형식이 아닙니다.',
isChecking: false
});
} else if (debouncedEmail === 'test@test.com') {
setValidation({
isValid: false,
errorMessage: '이미 사용 중인 이메일입니다.',
isChecking: false
});
} else {
setValidation({
isValid: true,
errorMessage: '',
isChecking: false
});
}
}, 300);
}, [debouncedEmail]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
// 타이핑 중에는 검사 상태로 설정
if (e.target.value && !validation.isChecking) {
setValidation(prev => ({ ...prev, isChecking: true }));
}
};
return (
<div className="input-container">
<label htmlFor="email">이메일</label>
<input
id="email"
type="email"
value={email}
onChange={handleChange}
placeholder="이메일을 입력해주세요"
className={`input ${!validation.isValid ? 'error' : ''} ${validation.isValid && email ? 'success' : ''}`}
/>
{validation.isChecking && (
<span className="checking">✓ 확인 중...</span>
)}
{validation.errorMessage && (
<span className="error-message">❌ {validation.errorMessage}</span>
)}
{validation.isValid && email && !validation.isChecking && (
<span className="success-message">✅ 사용 가능한 이메일입니다.</span>
)}
</div>
);
}
2. 비밀번호 강도 검사 컴포넌트
import { useState, useEffect } from 'react';
import { useDebounce } from './useDebounce';
interface PasswordStrength {
score: number;
message: string;
color: string;
}
export function PasswordInput() {
const [password, setPassword] = useState('');
const [strength, setStrength] = useState<PasswordStrength>({
score: 0,
message: '',
color: 'gray'
});
// 300ms 후에 비밀번호 강도 체크
const debouncedPassword = useDebounce(password, 300);
useEffect(() => {
if (!debouncedPassword) {
setStrength({ score: 0, message: '', color: 'gray' });
return;
}
const checkPasswordStrength = (pwd: string): PasswordStrength => {
let score = 0;
let message = '';
let color = 'red';
// 길이 체크
if (pwd.length >= 8) score += 1;
if (pwd.length >= 12) score += 1;
// 대소문자 체크
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score += 1;
// 숫자 체크
if (/\d/.test(pwd)) score += 1;
// 특수문자 체크
if (/[!@#$%^&*(),.?":{}|<>]/.test(pwd)) score += 1;
switch (score) {
case 0:
case 1:
message = '매우 약함';
color = 'red';
break;
case 2:
message = '약함';
color = 'orange';
break;
case 3:
message = '보통';
color = 'yellow';
break;
case 4:
message = '강함';
color = 'lightgreen';
break;
case 5:
message = '매우 강함';
color = 'green';
break;
}
return { score, message, color };
};
setStrength(checkPasswordStrength(debouncedPassword));
}, [debouncedPassword]);
return (
<div className="input-container">
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="비밀번호를 입력해주세요"
className="input"
/>
{password && (
<div className="password-strength">
<div className="strength-bar">
{[1, 2, 3, 4, 5].map((level) => (
<div
key={level}
className={`strength-segment ${strength.score >= level ? 'active' : ''}`}
style={{
backgroundColor: strength.score >= level ? strength.color : '#e0e0e0'
}}
/>
))}
</div>
<span className="strength-text" style={{ color: strength.color }}>
{strength.message}
</span>
</div>
)}
{password && (
<div className="password-requirements">
<p>비밀번호 요구사항:</p>
<ul>
<li className={password.length >= 8 ? 'valid' : ''}>
8자 이상 {password.length >= 8 && '✓'}
</li>
<li className={/[a-z]/.test(password) && /[A-Z]/.test(password) ? 'valid' : ''}>
대소문자 포함 {/[a-z]/.test(password) && /[A-Z]/.test(password) && '✓'}
</li>
<li className={/\d/.test(password) ? 'valid' : ''}>
숫자 포함 {/\d/.test(password) && '✓'}
</li>
<li className={/[!@#$%^&*(),.?":{}|<>]/.test(password) ? 'valid' : ''}>
특수문자 포함 {/[!@#$%^&*(),.?":{}|<>]/.test(password) && '✓'}
</li>
</ul>
</div>
)}
</div>
);
}
3. 사용자명 중복 검사 컴포넌트
import { useState, useEffect } from 'react';
import { useDebounce } from './useDebounce';
export function UsernameInput() {
const [username, setUsername] = useState('');
const [status, setStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'invalid'>('idle');
const [message, setMessage] = useState('');
// 800ms 후에 중복 검사 실행
const debouncedUsername = useDebounce(username, 800);
useEffect(() => {
if (!debouncedUsername) {
setStatus('idle');
setMessage('');
return;
}
// 사용자명 형식 검증
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
if (!usernameRegex.test(debouncedUsername)) {
setStatus('invalid');
setMessage('3-20자의 영문, 숫자, 언더스코어만 사용 가능합니다.');
return;
}
// 중복 검사 시작
setStatus('checking');
setMessage('확인 중...');
// API 호출 시뮬레이션
const checkUsername = async (name: string) => {
// 금지된 사용자명 목록
const forbiddenNames = ['admin', 'root', 'user', 'test', 'guest'];
return new Promise<boolean>((resolve) => {
setTimeout(() => {
resolve(!forbiddenNames.includes(name.toLowerCase()));
}, 500);
});
};
checkUsername(debouncedUsername)
.then((isAvailable) => {
if (isAvailable) {
setStatus('available');
setMessage('사용 가능한 사용자명입니다.');
} else {
setStatus('taken');
setMessage('이미 사용 중인 사용자명입니다.');
}
})
.catch(() => {
setStatus('invalid');
setMessage('확인 중 오류가 발생했습니다.');
});
}, [debouncedUsername]);
const getInputClassName = () => {
switch (status) {
case 'available':
return 'input success';
case 'taken':
case 'invalid':
return 'input error';
case 'checking':
return 'input checking';
default:
return 'input';
}
};
const getStatusIcon = () => {
switch (status) {
case 'checking':
return '🔄';
case 'available':
return '✅';
case 'taken':
case 'invalid':
return '❌';
default:
return '';
}
};
return (
<div className="input-container">
<label htmlFor="username">사용자명</label>
<div className="input-wrapper">
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="사용자명을 입력해주세요"
className={getInputClassName()}
/>
<span className="status-icon">{getStatusIcon()}</span>
</div>
{message && (
<span className={`message ${status === 'available' ? 'success' : status === 'checking' ? 'info' : 'error'}`}>
{message}
</span>
)}
</div>
);
}
4. 통합 폼 예시
import { useState } from 'react';
import { EmailInput } from './EmailInput';
import { PasswordInput } from './PasswordInput';
import { UsernameInput } from './UsernameInput';
export function RegistrationForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// 폼 제출 로직
try {
// API 호출
await new Promise(resolve => setTimeout(resolve, 2000));
alert('회원가입이 완료되었습니다!');
} catch (error) {
alert('회원가입 중 오류가 발생했습니다.');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="registration-form">
<h2>회원가입</h2>
<UsernameInput />
<EmailInput />
<PasswordInput />
<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? '가입 중...' : '회원가입'}
</button>
</form>
);
}
이렇게 UI적으로 일부러 감지를 늦춰 실시간 피드백을 제공하고 불필요한 검증을 방지