폴링(Polling), 롱 폴링(Long polling) 그리고 JavaScript 예제

3 분 소요


👉 해당 포스트를 읽는데 도움을 줍니다.

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

0. 들어가면서

폴링(Polling), 롱 폴링(Long polling) 그리고 스프링 예제 포스트에서 개념은 한번 정리하였으므로 이번 포스트에서는 간단한 개념 복습과 JavaScript를 사용한 예시 코드를 정리하였습니다. 자세한 개념을 확인하시고 싶은 분들은 이전 글을 읽어보시길 바랍니다.

1. JavaScript 폴링 구현하기

클라이언트가 일정 주기로 서버에게 데이터를 요청합니다. setTimeout 함수와 setInterval 함수를 사용할 때 미묘하게 기능이 다르기 때문에 관련된 내용도 함께 정리하였습니다.

이미지 출처, https://rubberduck-debug.tistory.com/123


1.1. setTimeout(callback, timeout) 사용

1.1.1. 테스트 코드

    it('given 6 seconds, timeout 1 second when call polling method then 6 times call', async () => {

        // setup
        jest.useFakeTimers();
        const spyFunc = jest.fn();

        // act
        timeoutPolling(spyFunc, 1000);
        for (let i = 0; i < 6; i++) {
            jest.advanceTimersByTime(1000);
            await Promise.resolve();
        }

        // assert
        expect(spyFunc).toHaveBeenCalledTimes(6)
    });

1.1.2. 구현 코드

export const timeoutPolling = (func, timeout, maxAttempts = -1) => {
    if (maxAttempts === 0) {
        return;
    }
    setTimeout(async () => {
        try {
            await func();
        } catch (error) {
            console.error(error);
        }
        timeoutPolling(func, timeout, maxAttempts - 1);
    }, timeout);
};

1.1.3. 함수 실행 간격

  • 콜백 함수 실행 시간과 상관없이 콜백 함수 실행 간격이 일정하게 보장됩니다.

이미지 출처, https://ko.javascript.info/settimeout-setinterval


1.2. setInterval(callback, timeout) 사용

1.2.1. 테스트 코드

    it('given 6 seconds, interval 1 second, maximum attempts 5 times when call polling method then 5 times call', async () => {

        // setup
        jest.useFakeTimers();
        const spyFunc = jest.fn();

        // act
        intervalPolling(spyFunc, 1000, 5);
        for (let i = 0; i < 6; i++) {
            jest.advanceTimersByTime(1000);
        }

        // assert
        expect(spyFunc).toHaveBeenCalledTimes(5)
    });

1.2.2. 구현 코드

export const intervalPolling = (func, interval, maxAttempts = -1) => {
    let attempts = 0;
    let intervalId = setInterval(() => {
        if (maxAttempts === attempts) {
            clearInterval(intervalId);
            return;
        }
        attempts++;
        func();
    }, interval);
};

1.1.3. 함수 실행 간격

  • 콜백 함수 실행 시간이 길어지면 콜백 함수 실행 간격이 짧아집니다.
  • 예를 들어, interval이 100ms 인 경우
    • 콜백 함수 실행 시간이 30ms라면 다음 콜백 함수 실행은 70ms 뒤 입니다.
    • 콜백 함수 실행 시간이 60ms라면 다음 콜백 함수 실행은 40ms 뒤 입니다.

이미지 출처, https://ko.javascript.info/settimeout-setinterval


1.3. sleep(timeout) 함수 사용 (feat. @jskim1991)

시간 간격을 만들 수 있는 sleep(timeout) 함수를 정의하여 폴링을 제어합니다. JestmockResolvedValue(혹은 mockResolvedValueOnce)함수와 useFakeTimers 함수를 사용하는 경우 테스트 코드가 지저분해집니다. 리얼 타이머를 사용하여 테스트 하되 timeout이 크면 테스트 시간이 길어지므로 짧게 지정하여 테스트합니다.

1.3.1. 테스트 코드

    it('sleep 100 ms, getting data what you want at 2nd trial when call polling method then 2 times call', async () => {

        const mockCallback = jest
            .fn()
            .mockResolvedValueOnce({
                data: 'Welcome',
            })
            .mockResolvedValueOnce({
                data: 'Junhyunny',
            })
            .mockResolvedValueOnce({
                data: 'Dev',
            })
            .mockResolvedValueOnce({
                data: 'Log',
            });

        const validateFn = (result) => 'Junhyunny' === result.data;

        const data = await sleepPolling(mockCallback, validateFn, 100);

        expect(mockCallback).toHaveBeenCalledTimes(2);
        expect(data).toEqual({
            data: 'Junhyunny',
        });
    });

1.3.2. 구현 코드

const sleep = (timeout = 100) => {
    return new Promise(resolve => {
        setTimeout(resolve, timeout);
    });
}

export const sleepPolling = async (func, validateFunc, timeout) => {
    let result = await func();
    while (!validateFunc(result)) {
        await sleep(timeout);
        try {
            result = await func();
        } catch (e) {
            console.log(e.message);
        }
    }
    return result;
}

2. JavaScript 롱 폴링 구현하기

서버는 클라이언트 요청에 대해 즉시 응답을 주지 않습니다. 다음과 같은 순서로 진행됩니다.

  • 클라이언트가 서버에게 요청을 보냅니다.
  • 서버는 즉시 응답을 주지 않습니다.
  • 특정 이벤트가 발생하거나 타임아웃(timeout)이 발생하면 응답을 전달합니다.
  • 클라이언트는 응답을 받은 후 다시 서버에게 데이터를 요청합니다.
롱 폴링 방식

이미지 출처, https://rubberduck-debug.tistory.com/123


2.1. 롱 폴링 구현하기

Long polling 포스트의 코드를 일부 변경하였습니다.

2.1.1. 테스트 코드

    it('sleep 100 ms, getting data what you want at 3rd trial when call polling method then 3 times call', async () => {

        const mockCallback = jest
            .fn()
            .mockResolvedValueOnce({
                status: 500
            })
            .mockResolvedValueOnce({
                status: 502
            })
            .mockResolvedValueOnce({
                status: 200,
                data: 'Junhyunny'
            })
            .mockResolvedValueOnce({
                status: 200,
                data: 'Log',
            });

        const validateFn = (response) => 'Junhyunny' === response.data;

        const data = await longPolling(mockCallback, validateFn, 100);

        expect(mockCallback).toHaveBeenCalledTimes(3);
        expect(data).toEqual({
            status: 200,
            data: 'Junhyunny'
        });
    });

2.1.2. 구현 코드

export const longPolling = async (func, validateFunc, timeout) => {
    try {
        let response = await func();
        if (response.status === 200 && validateFunc(response)) {
            return response;
        }
        // status 502 is a connection timeout
        if (response.status !== 502) {
            // when not connection timeout, sleep and try
            await sleep(timeout);
        }
    } catch (error) {
        await sleep(timeout);
    }
    return await longPolling(func, validateFunc, timeout);
}

CLOSING

클라이언트 측 폴링과 롱 폴링 기능을 구현하면서 아래와 같은 것들을 배웠습니다.

  • jest.useFakeTimers() 기능을 사용하면 테스트가 어려워집니다.
  • advanceTimersByTime(ms) 호출시 내부에서 setTimeout, setInterval의 콜백 함수를 동기적으로 처리합니다.
  • 클라이언트 측 폴링과 롱 폴링 코드의 다른 점은 서버의 타임아웃 처리 여부로 보입니다.

TEST CODE REPOSITORY

REFERENCE

댓글남기기