iconicon
BBBlllooogggPPPooorrrtttfffooollliiiooo
    Uncategorized

    디바운스(Debounce)

    2026년 04월 12일

    On this page

    • Input Validation 예시
    • 1. 이메일 유효성 검사 컴포넌트
    • 2. 비밀번호 강도 검사 컴포넌트
    • 3. 사용자명 중복 검사 컴포넌트
    • 4. 통합 폼 예시

    On this page
    • Input Validation 예시
    • 1. 이메일 유효성 검사 컴포넌트
    • 2. 비밀번호 강도 검사 컴포넌트
    • 3. 사용자명 중복 검사 컴포넌트
    • 4. 통합 폼 예시


    디바운스(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적으로 일부러 감지를 늦춰 실시간 피드백을 제공하고 불필요한 검증을 방지