본문 바로가기
2024/TIL

[React] 쉽지 않은 유효성 검사(Debouncing, Throttling)

by ye-jji 2024. 6. 23.

1. 유효성 검사

유효성 검사(Validation)는 입력된 데이터나 정보가 특정 조건이나 규칙에 부합하는지를 확인하는 과정입니다. 이는 데이터의 정확성과 일관성을 보장하기 위해 중요하며, 다양한 분야에서 사용됩니다. 유효성 검사는 주로 입력된 데이터가 예상된 형식과 범위 내에 있는지 확인하는 것을 목표로 합니다.

주요 유효성 검사 종류

  1. 형식 유효성 검사(Format Validation)
    • 데이터의 형식이 올바른지 확인합니다. 예를 들어, 이메일 주소 형식이 올바른지, 전화번호가 정해진 형식을 따르는지 등을 검사합니다.
  2. 범위 유효성 검사(Range Validation)
    • 데이터가 특정 범위 내에 있는지 확인합니다. 예를 들어, 나이가 0 이상 120 이하인지, 점수가 0점에서 100점 사이인지 등을 검사합니다.
  3. 존재 유효성 검사(Presence Check)
    • 필수 입력 항목이 비어 있지 않은지 확인합니다. 예를 들어, 이름이나 이메일 주소와 같은 필수 입력 필드가 채워졌는지 검사합니다.
  4. 일관성 유효성 검사(Consistency Check)
    • 서로 관련된 여러 데이터 항목이 일관성을 유지하는지 확인합니다. 예를 들어, 비밀번호와 비밀번호 확인이 일치하는지, 날짜 순서가 올바른지 등을 검사합니다.
  5. 길이 유효성 검사(Length Check)
    • 입력된 데이터의 길이가 정해진 기준에 맞는지 확인합니다. 예를 들어, 비밀번호가 최소 8자 이상인지, 사용자의 이름이 50자를 초과하지 않는지 등을 검사합니다.
  6. 논리적 유효성 검사(Logical Check)
    • 논리적으로 맞는지 확인합니다. 예를 들어, 시작 날짜가 종료 날짜보다 이전인지, 나이가 음수가 아닌지 등을 검사합니다.

유효성 검사의 중요성

  • 데이터 무결성: 유효성 검사를 통해 데이터의 무결성을 유지할 수 있습니다. 이는 시스템이 정확하고 신뢰할 수 있는 데이터를 처리하도록 보장합니다.
  • 보안: 유효하지 않은 데이터를 걸러내어 보안 문제를 예방할 수 있습니다. 예를 들어, SQL 인젝션과 같은 보안 공격을 방지할 수 있습니다.
  • 사용자 경험 개선: 사용자에게 적절한 피드백을 제공함으로써 잘못된 데이터 입력을 방지하고, 사용자 경험을 향상시킬 수 있습니다.
  • 비즈니스 논리 유지: 비즈니스 로직에 맞지 않는 데이터 입력을 방지하여 시스템의 논리적 일관성을 유지할 수 있습니다.

유효성 검사는 클라이언트 측(예: 웹 브라우저)과 서버 측(예: 웹 서버)에서 모두 수행될 수 있으며, 두 단계 모두에서 검사를 수행하는 것이 일반적입니다. 클라이언트 측 검사는 사용자 경험을 향상시키고, 서버 측 검사는 보안 및 데이터 무결성을 보장합니다.

 

2. 디바운싱

디바운싱(Debouncing)은 연속적으로 발생하는 이벤트를 특정 시간 동안 하나로 묶어 처리하는 기술입니다. 주로 자바스크립트에서 사용되며, 사용자가 입력한 값이나 이벤트를 효율적으로 처리하기 위해 사용됩니다. 예를 들어, 사용자가 빠르게 키보드를 입력하거나, 윈도우 리사이즈 이벤트가 발생하는 경우, 이러한 이벤트가 너무 자주 발생하면 성능에 부정적인 영향을 미칠 수 있습니다. 디바운싱을 통해 이러한 문제를 해결할 수 있습니다.

디바운싱의 원리

디바운싱은 일정 시간 동안 이벤트가 더 이상 발생하지 않을 때까지 대기한 후, 마지막 이벤트만을 처리합니다. 예를 들어, 사용자가 입력 필드에 타이핑을 할 때, 입력이 멈춘 후 일정 시간 이후에만 이벤트를 처리하도록 할 수 있습니다.

디바운싱의 예시

다음은 자바스크립트에서 디바운싱을 구현하는 예시입니다.

function debounce(func, delay) {
    let timer;
    return function(...args) {
        const context = this;
        clearTimeout(timer);
        timer = setTimeout(() => func.apply(context, args), delay);
    };
}

// 사용 예시
const handleResize = () => {
    console.log('Window resized');
};

window.addEventListener('resize', debounce(handleResize, 300));

 

위 코드에서 debounce 함수는 다른 함수(func)와 지연 시간(delay)을 인자로 받습니다. 이 함수는 타이머(timer)를 설정하고, 주어진 지연 시간 동안 추가 이벤트가 발생하면 타이머를 리셋하여 마지막 이벤트만 처리합니다.

디바운싱의 주요 활용 사례

  1. 검색 입력: 사용자가 검색어를 입력할 때마다 실시간으로 검색 결과를 가져오는 대신, 입력이 멈춘 후 일정 시간 이후에만 검색을 수행하여 불필요한 API 호출을 줄입니다.
  2. 윈도우 리사이즈: 창 크기 조정 이벤트가 발생할 때마다 처리하지 않고, 조정이 끝난 후 일정 시간 이후에만 이벤트를 처리하여 성능을 개선합니다.
  3. 폼 입력 검증: 사용자가 입력 필드에 값을 입력할 때마다 실시간으로 검증하지 않고, 입력이 멈춘 후 일정 시간 이후에만 검증을 수행합니다.

디바운싱의 장점

  • 성능 개선: 빈번하게 발생하는 이벤트로 인해 발생할 수 있는 성능 저하를 방지합니다.
  • 리소스 절약: 불필요한 함수 호출을 줄여 시스템 리소스를 절약합니다.
  • 사용자 경험 향상: 불필요한 처리를 줄여 더 나은 사용자 경험을 제공합니다.

 

3. 쓰로틀링

쓰로틀링(Throttling)은 디바운싱과 유사하지만, 특정 시간 간격 내에 이벤트가 일정 횟수 이상 발생하지 않도록 제한하는 기법입니다. 즉, 연속적으로 발생하는 이벤트를 일정 시간 간격으로 발생하도록 제어합니다. 쓰로틀링은 고빈도 이벤트를 제어하여 시스템 성능을 유지하면서도 이벤트가 일정 주기마다 실행되도록 보장합니다.

쓰로틀링의 원리

쓰로틀링은 주어진 시간 동안 한 번만 이벤트를 처리하도록 하는 것입니다. 예를 들어, 사용자가 계속해서 스크롤을 할 때, 매번 스크롤 이벤트를 처리하지 않고 일정한 간격으로 이벤트를 처리합니다.

쓰로틀링의 예시

다음은 자바스크립트에서 쓰로틀링을 구현하는 예시입니다.

function throttle(func, limit) {
    let lastFunc;
    let lastRan;
    return function(...args) {
        const context = this;
        if (!lastRan) {
            func.apply(context, args);
            lastRan = Date.now();
        } else {
            clearTimeout(lastFunc);
            lastFunc = setTimeout(() => {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    };
}

// 사용 예시
const handleScroll = () => {
    console.log('Scroll event');
};

window.addEventListener('scroll', throttle(handleScroll, 200));

 

위 코드에서 throttle 함수는 다른 함수(func)와 제한 시간(limit)을 인자로 받습니다. 이 함수는 마지막으로 함수가 실행된 시간을 기록하고, 지정된 시간(limit) 이후에만 함수를 실행합니다. 만약 지정된 시간 내에 다시 이벤트가 발생하면, 마지막 호출을 지연시켜 특정 시간 간격으로 이벤트를 처리합니다.

쓰로틀링의 주요 활용 사례

  1. 스크롤 이벤트: 사용자가 스크롤할 때마다 이벤트를 처리하는 대신, 일정한 간격으로 이벤트를 처리하여 성능을 최적화합니다.
  2. 윈도우 리사이즈: 창 크기 조정 이벤트가 발생할 때마다 처리하지 않고, 일정한 간격으로 이벤트를 처리하여 성능을 개선합니다.
  3. 마우스 이동: 마우스 이동 이벤트를 일정한 간격으로 처리하여 불필요한 연산을 줄입니다.

쓰로틀링의 장점

  • 성능 최적화: 빈번하게 발생하는 이벤트를 제어하여 성능 저하를 방지합니다.
  • 자원 절약: 불필요한 함수 호출을 줄여 시스템 자원을 절약합니다.
  • 일정 간격 이벤트 처리: 이벤트를 일정한 간격으로 처리하여 더 예측 가능한 동작을 제공합니다.

디바운싱과 쓰로틀링의 차이점

  • 디바운싱: 이벤트가 더 이상 발생하지 않을 때까지 대기한 후, 마지막 이벤트를 처리합니다. 이는 이벤트 발생 빈도를 줄여 불필요한 작업을 방지합니다.
  • 쓰로틀링: 주어진 시간 간격 내에 이벤트가 일정 횟수 이상 발생하지 않도록 제한합니다. 이는 이벤트를 일정한 주기로 처리하여 성능을 최적화합니다.

두 기법 모두 성능 최적화와 자원 절약을 목표로 하지만, 특정 시나리오에 맞게 선택적으로 사용해야 합니다. 예를 들어, 검색 필드 입력처럼 사용자 입력이 멈춘 후에 작업을 수행해야 하는 경우에는 디바운싱을 사용하고, 스크롤이나 리사이즈 이벤트처럼 지속적인 입력을 일정 간격으로 처리해야 하는 경우에는 쓰로틀링을 사용하는 것이 적합합니다.

 

4. 유효성 검사에 두 가지 모두 적용했을 때 문제 발생

목적

닉네임 입력 필드에 2~8자 제한이 걸려 있으며, 이 조건을 만족할 때만 '다음' 버튼이 활성화되어야 합니다.

문제상황

디바운싱과 쓰로틀링을 적용했을 때, 마지막 글자를 빠르게 타이핑하면 '다음' 버튼이 활성화되지 않는 오류 발생.

원인 분석

디바운싱과 쓰로틀링이 최종 입력을 즉시 반영하지 못해 유효성 검사가 지연되면서 버튼 활성화 상태가 갱신되지 않음.

해결 방안

  • 디바운싱과 쓰로틀링 적용 시, 마지막 입력 후 일정 시간 이후에도 조건을 다시 평가하도록 로직 수정.
  • 입력 필드의 onChange 이벤트에서 즉시 유효성 검사를 수행하고, 일정 시간 이후에도 다시 한 번 유효성 검사를 확인하는 로직 추가.

 

5. 수정한 코드(예시)

import React, { useState, useEffect, useCallback } from 'react';

function debounce(func: Function, delay: number) {
    let timer: NodeJS.Timeout;
    return (...args: any[]) => {
        clearTimeout(timer);
        timer = setTimeout(() => func(...args), delay);
    };
}

function throttle(func: Function, limit: number) {
    let lastFunc: NodeJS.Timeout;
    let lastRan: number;
    return (...args: any[]) => {
        const context = this;
        if (!lastRan) {
            func.apply(context, args);
            lastRan = Date.now();
        } else {
            clearTimeout(lastFunc);
            lastFunc = setTimeout(() => {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    };
}

const App: React.FC = () => {
    const [nickname, setNickname] = useState<string>('');
    const [isValid, setIsValid] = useState<boolean>(false);

    const validateNickname = useCallback(() => {
        setIsValid(nickname.length >= 2 && nickname.length <= 8);
    }, [nickname]);

    const debouncedValidate = useCallback(debounce(validateNickname, 300), [validateNickname]);
    const throttledValidate = useCallback(throttle(validateNickname, 300), [validateNickname]);

    useEffect(() => {
        debouncedValidate();
        throttledValidate();
    }, [nickname, debouncedValidate, throttledValidate]);

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setNickname(e.target.value);
    };

    return (
        <div>
            <label htmlFor="nickname">닉네임:</label>
            <input type="text" id="nickname" value={nickname} onChange={handleChange} />
            <button type="button" disabled={!isValid}>다음</button>
        </div>
    );
};

export default App;

 

 

  • 디바운싱 함수: debounce 함수는 주어진 지연 시간(밀리초) 동안 이벤트가 발생하지 않을 때까지 대기한 후 함수를 호출합니다.
  • 쓰로틀링 함수: throttle 함수는 주어진 시간 간격(밀리초) 내에서 한 번만 함수를 호출합니다.
  • 상태 관리: nickname과 isValid 상태를 사용하여 닉네임 입력 값과 버튼 활성화 상태를 관리합니다.
  • 유효성 검사 함수: validateNickname 함수는 닉네임 입력 필드의 값이 2자 이상 8자 이하인지 확인하여 isValid 상태를 업데이트합니다.
  • 디바운싱 및 쓰로틀링 적용: useCallback과 useEffect를 사용하여 디바운싱과 쓰로틀링을 적용합니다.
  • 이벤트 핸들러: handleChange 함수는 입력 필드의 값이 변경될 때마다 nickname 상태를 업데이트합니다.