React debounce test with Jest

2 분 소요


👉 이어서 읽기를 추천합니다.

1. 디바운스, Debounce

이벤트들을 그룹화하여 특정 시간이 지난 후 맨 마지막 이벤트만 발생하도록 처리하는 방법입니다. 순차적 호출을 하나의 그룹으로 만들고, 맨 마지막 함수(혹은 맨 처음)만 호출합니다. 잦은 이벤트로 인한 부하로 성능 문제를 일으킬 수 있는 경우 사용합니다.

예를 들어, 사용자가 검색창에 키워드를 입력할 때 변경 내용마다 서버로 API 요청을 하는 것은 서버와 브라우저 모두에게 부하를 일으킵니다. 이런 경우에 디바운스를 이용하는데, 사용자 입력이 멈춘 후 일정 시간이 지난 후에 사용자가 입력한 내용을 모아 한 번만 요청합니다.

대표적으로 디바운스를 이용하여 이벤트 발생을 제어하는 기능은 다음과 같습니다.

  • 화면 확대, 축소
  • 검색어 입력시 자동 완성 혹은 연관 검색어 노출
  • 스크롤링(scrolling)으로 발생하는 과도한 이벤트 처리
디바운스 처리 방법

https://codepen.io/jaehee/pen/XoKeRW

2. 디바운스 처리 구현

2.1. 테스트 코드

2.1.1. element rendering 테스트

  • 컴포넌트 렌더링 후 화면에 element들이 존재하는지 확인합니다.
    describe('test rendering elements', () => {

        it('exists input box for search and message when rendered', () => {

            // setup, act
            render(<App/>);

            // assert
            expect(screen.getByPlaceholderText('검색어')).toBeInTheDocument()
            expect(screen.getByText('현재 API 호출 횟수 = 0')).toBeInTheDocument();
        });
    });

2.1.2. 사용자 조작 테스트

  • 입력창에 검색 키워드 입력 후 적절한 파라미터와 함께 axios 호출이 1회 있었는지 확인합니다.
  • 화면에 보이는 문구가 변경되었는지 확인합니다.
    describe('test user interaction', () => {

        it('call axios get method one time when typed some keyword', () => {

            // setup
            jest.useFakeTimers();
            const spyAxios = jest.spyOn(axios, 'get').mockResolvedValue({data: {}});

            // act
            render(<App/>);
            userEvent.type(screen.getByPlaceholderText('검색어'), 'Junhyunny');
            act(() => {
                jest.advanceTimersByTime(500);
            });

            // assert
            expect(spyAxios).toHaveBeenNthCalledWith(1, 'http://localhost:8080/search', {
                params: {
                    keyword: 'Junhyunny'
                }
            });
            expect(screen.getByText('현재 API 호출 횟수 = 1')).toBeInTheDocument();
        });
    });

2.2. App.js

2.2.1. Debounce 처리

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

2.2.2. 전체 코드

import {useCallback, useState} from "react";
import axios from "axios";
import classes from './App.module.css';

function App() {

    const [apiCallCount, setApiCallCount] = useState(0);
    const [keyword, setKeyword] = useState('');

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

    const searchKeyword = (params) => {
        setApiCallCount(prevState => prevState + 1);
        axios.get('http://localhost:8080/search', {
            params
        });
    };

    const deboundHandler = useCallback(debounce(searchKeyword, 500), []);

    const keywordChangeHandler = ({target: {value}}) => {
        setKeyword(value);
        deboundHandler({keyword: value});
    };

    return (
        <div className={classes.App}>
            <input placeholder="검색어" value={keyword} onChange={keywordChangeHandler}/>
            <p>현재 API 호출 횟수 = {apiCallCount}</p>
        </div>
    );
}

export default App;

3. 테스트 결과

디바운스 처리를 하지 않았을 때와 했을 때 어떻게 다른지 비교해보았습니다. 또, useCallback 훅(hook)을 사용하지 않으면 어떤 현상이 발생하는지 확인해았습니다.

3.1. 디바운스 처리하지 않았을 때 현상

  • 키보드 입력이 발생할 때마다 API 요청 횟수가 증가합니다.
  • 이는 클라이언트와 서버에 모두 부하를 발생시킬 수 있습니다.

3.2. useCallback 훅을 사용하지 않았을 때 현상

  • useCallback 훅을 사용하지 않으면 예상대로 테스트 결과가 나오지 않습니다.
  • 컴포넌트가 다시 렌더링되면 함수가 새로 생성되기 때문에 이전 타이머가 클리어되지 않고 새로운 타이머가 계속 생겨나게 됩니다.
  • 디바운스 코드로 약간의 딜레이가 있지만, 디바운스 처리를 하지 않은 것과 동일한 결과를 얻게 됩니다.
  • useCallback 훅을 통해 해당 컴포넌트에서 최초 1번만 생성되도록 구현합니다.

    // const deboundHandler = useCallback(debounce(searchKeyword, 500), []);
    const deboundHandler = debounce(searchKeyword, 500);

    const keywordChangeHandler = ({target: {value}}) => {
        setKeyword(value);
        deboundHandler({keyword: value});
    };

3.3. 디바운스 처리된 결과

  • 사용자 이벤트가 일정 시간 없을 경우 API 요청을 수행합니다.

TEST CODE REPOSITORY

REFERENCE

댓글남기기