akjfal

background에서 setinterval이나 settimeout 돌리기 본문

하루의 이슈

background에서 setinterval이나 settimeout 돌리기

akjfal 2023. 5. 9. 12:44

페이지에서 서버에 파일을 업로드하며 API를 setInterval로 지속적으로 쏘고, 응답을 받아서 진행률을 업데이트 해야하는 작업이 있었습니다.

그런데 이때 화면을 보고 있을때는 정상적으로 페이지가 동작하지만, 파일 업로드를 눌러놓고 다른 탭의 작업을 진행했을 때 setInterval이 비정상적으로 동작하는 현상을 발견했습니다.


원인

setInterval이나 setTimeout를 통해 polling을 구현하고 있는 페이지에서 돌리고 있다고 가정해봅시다.

// app.js interval 발생시키기
setInterval(()=> {
  console.log('setinterval')
}, 100);

위 코드를 실행 시킬 시 setinterval이라는 콘솔이 계속 찍히게 됩니다. 이때 탭을 옆으로 넘어가거나 최소화시켰을 때브라우저 자체적으로 자원절약을 위해 타이머를 조절합니다.

백그라운드로 돌아간 시간이 길어질 경우 polling이 동작하는 주기가 더욱 길어지게 됩니다.

크롬의 경우에 크롬 88 버전부터 아래의 경우 분당 1회체크로 변하게 됩니다.

  • 페이지가 5분이상 백그라운드화
  • 체인수가 5 이상
  • 30초이상 반응이 없음
  • WebTRC를 미사용

해결 방법

이러한 문제를 해결하기 위해서는 timer를 제거해야 합니다. 그래서 해결 방법을 위해서 찾아보았습니다.

1. web socket 사용하기

  1. 서버와 클라이언트간에 WEB 프로토콜로 전환하여 양방향 통신을 통해 데이터를 지속적으로 주고 받을 수 있습니다.
  2. 프론트에서 진행 상황 API를 쏘는 작업 자체를 제거할 수 있습니다.
  3. 서버에서 지속적으로 진행 상황을 프론트로 쏴주면 작업이 성공합니다.

예시 코드를 실행시켜보면 탭을 이동하거나 최소화시켜도 데이터의 전송에 문제가 없음을 확인할 수 있습니다.

// server
const express = require('express');
const cors = require('cors');
const app = express();
const { WebSocketServer } = require("ws")

const wss = new WebSocketServer({ port: 8001 })

let corsOptions = {
    origin: '<http://127.0.0.1:3000>',
    credentials: true
}

// 웹소켓 서버 연결 이벤트 바인드
wss.on("connection", ws => {
    // 데이터 수신 이벤트 바인드
    console.log('connection');
    ws.on("message", data => {
        setInterval(()=>{
            ws.send(`Received ${data}`)
        }, 100)
    })
})

app.use(cors(corsOptions));

app.get("/", function (req, res) {
    return res.send("hello world")
})

app.listen(3001, function () {
    console.log("server listening on port 3001")
})
// client

export default function Home() {
  const webSocketUrl = "ws://127.0.0.1:8001";
  const ws = useRef(null);

  const handleClickOpen = async () => {
    if (!ws.current) {
      ws.current = new WebSocket(webSocketUrl);
      ws.current.onmessage = (event) => {
				// 메시지 송신 확인
        console.log(event.data)
      }
    }  
  }

  const handleClickSendMessage = async () => {
    ws.current.send('send Message')
  }

  const handleClickClose = async () => {
    ws.current.close()
  }

  return (
    <div>
      interval
      <div>
        <button onClick={handleClickOpen}>
          소켓 오픈
        </button>
        <button onClick={handleClickSendMessage}>
          메시지 전송
        </button>
        <button onClick={handleClickClose}>
          소켓 닫기
        </button>
      </div>
    </div>
  )
}

 

2. WebWorker를 사용한 방법이 있습니다.

  1. WebWorker는 브라우저 단에서 해당 동작을 위한 스레드를 따로 동작시켜, background에서 timer가 부정확해지는 문제가 발생하지 않습니다.
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#000000" />
        <meta
        name="description"
        content="Web site created using create-react-app"
        />
        <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
        <title>Worker</title>
    </head>
    <body>
        <div class="wrapper">
            <button id="start-worker" onclick="startWorker()">
                Start worker
            </button>
            <button id="stop-worker" onclick="stopWorker()">
                End worker
            </button>
        </div>
    </body>
    <script id="worker1" type="javascript/worker">
        let interval;

        self.addEventListener('message', function(e) {
            const data = e.data;
            switch (data) {
                case 'start':
                    interval = setInterval(async ()=> {
                        postMessage('interval');
                    }, 100)
                    break;
                case 'stop':
                    clearInterval(interval);
                    self.postMessage('WORKER STOPPED: ' + data.msg +
                                    '. (buttons will no longer work)');
                    self.close(); // Terminates the worker.
                    break;
                default:
                    self.postMessage('Unknown command: ' + data.msg);
            };
        });
    </script>
    <script>
        const blob = new Blob([document.querySelector('#worker1').textContent]);

        const worker = new window.Worker(window.URL.createObjectURL(blob));

        worker.onmessage = (e) => {
            console.log('main onmessage', e.data)
        }

        const startWorker = () => {
            console.log('start worker');
            worker.postMessage('start');
        }

        const stopWorker = () => {
            console.log('end worker');
            worker.postMessage('stop');
            worker.terminate();
        }
    </script>
</html>

3. server-sent event

  1. socket과 비슷하지만 socket은 단방향, server-sent는 단방향인 차이점이 있습니다.
  2. 주로 소셜미디어 업데이트, 뉴스 피드 등에 사용되는 기술입니다.
new EventSource('<http://127.0.0.1:3001/>')
...
sse.addEventListener('message', function(e){
    console.log(e.data);
})
sse.addEventListener('open', function(e){
    console.log(e.data);
})

4. push messages

  1. Push message의 경우엔 HTML5의 표준인 Service Worker 기술을 통해서 메시지가 사용자에게 노출 되는 방법입니다. 해당 사이트에 접속중이지 않더라도 브라우저가 실행중이라면 사용자에게 알림이 노출됩니다.

5. fetch stream

  1. fetch를 stream을 통해서 데이터를 chunk단위로 가져오는 방법입니다.

참고 자료

https://pks2974.medium.com/web-worker-간단-정리하기-4ec90055aa4d

https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

https://vroomfan.tistory.com/41

https://dev-l.tistory.com/15

https://web.dev/workers-basics/#toc-enviornment-loadingscripts

https://inpa.tistory.com/entry/NODE-📚-Server-Sent-Events-💯-정리-사용법

https://nsinc.tistory.com/218

https://web.dev/i18n/ko/fetch-upload-streaming/

그런데 이때 화면을 보고 있을때는 정상적으로 페이지가 동작하지만, 파일 업로드를 눌러놓고 다른 탭의 작업을 진행했을 때 setInterval이 비정상적으로 동작하는 현상을 발견했습니다.


원인

setInterval이나 setTimeout를 통해 polling을 구현하고 있는 페이지에서 돌리고 있다고 가정해봅시다.

// app.js interval 발생시키기
setInterval(()=> {
  console.log('setinterval')
}, 100);

위 코드를 실행 시킬 시 setinterval이라는 콘솔이 계속 찍히게 됩니다. 이때 탭을 옆으로 넘어가거나 최소화시켰을 때브라우저 자체적으로 자원절약을 위해 타이머를 조절합니다.

위처럼 백그라운드로 돌아간 시간이 길어질 경우 polling이 동작하는 주기가 더욱 길어지게 됩니다.

크롬의 경우에 크롬 88 버전부터 아래의 경우 분당 1회체크로 변하게 됩니다.

  • 페이지가 5분이상 백그라운드화
  • 체인수가 5 이상
  • 30초이상 반응이 없음
  • WebTRC를 미사용

해결 방법

이러한 문제를 해결하기 위해서는 timer를 제거해야 합니다. 그래서 해결 방법을 위해서 찾아보았습니다.

  1. web socket 사용하기
    1. 서버와 클라이언트간에 WEB 프로토콜로 전환하여 양방향 통신을 통해 데이터를 지속적으로 주고 받을 수 있습니다.
    2. 프론트에서 진행 상황 API를 쏘는 작업 자체를 제거할 수 있습니다.
    3. 서버에서 지속적으로 진행 상황을 프론트로 쏴주면 작업이 성공합니다.

예시 코드를 실행시켜보면 탭을 이동하거나 최소화시켜도 데이터의 전송에 문제가 없음을 확인할 수 있습니다.

// server
const express = require('express');
const cors = require('cors');
const app = express();
const { WebSocketServer } = require("ws")

const wss = new WebSocketServer({ port: 8001 })

let corsOptions = {
    origin: '<http://127.0.0.1:3000>',
    credentials: true
}

// 웹소켓 서버 연결 이벤트 바인드
wss.on("connection", ws => {
    // 데이터 수신 이벤트 바인드
    console.log('connection');
    ws.on("message", data => {
        setInterval(()=>{
            ws.send(`Received ${data}`)
        }, 100)
    })
})

app.use(cors(corsOptions));

app.get("/", function (req, res) {
    return res.send("hello world")
})

app.listen(3001, function () {
    console.log("server listening on port 3001")
})
// client

export default function Home() {
  const webSocketUrl = "ws://127.0.0.1:8001";
  const ws = useRef(null);

  const handleClickOpen = async () => {
    if (!ws.current) {
      ws.current = new WebSocket(webSocketUrl);
      ws.current.onmessage = (event) => {
				// 메시지 송신 확인
        console.log(event.data)
      }
    }  
  }

  const handleClickSendMessage = async () => {
    ws.current.send('send Message')
  }

  const handleClickClose = async () => {
    ws.current.close()
  }

  return (
    <div>
      interval
      <div>
        <button onClick={handleClickOpen}>
          소켓 오픈
        </button>
        <button onClick={handleClickSendMessage}>
          메시지 전송
        </button>
        <button onClick={handleClickClose}>
          소켓 닫기
        </button>
      </div>
    </div>
  )
}
  1. WebWorker를 사용한 방법이 있습니다.
    1. WebWorker는 브라우저 단에서 해당 동작을 위한 스레드를 따로 동작시켜, background에서 timer가 부정확해지는 문제가 발생하지 않습니다.
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#000000" />
        <meta
        name="description"
        content="Web site created using create-react-app"
        />
        <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
        <title>Worker</title>
    </head>
    <body>
        <div class="wrapper">
            <button id="start-worker" onclick="startWorker()">
                Start worker
            </button>
            <button id="stop-worker" onclick="stopWorker()">
                End worker
            </button>
        </div>
    </body>
    <script id="worker1" type="javascript/worker">
        let interval;

        self.addEventListener('message', function(e) {
            const data = e.data;
            switch (data) {
                case 'start':
                    interval = setInterval(async ()=> {
                        postMessage('interval');
                    }, 100)
                    break;
                case 'stop':
                    clearInterval(interval);
                    self.postMessage('WORKER STOPPED: ' + data.msg +
                                    '. (buttons will no longer work)');
                    self.close(); // Terminates the worker.
                    break;
                default:
                    self.postMessage('Unknown command: ' + data.msg);
            };
        });
    </script>
    <script>
        const blob = new Blob([document.querySelector('#worker1').textContent]);

        const worker = new window.Worker(window.URL.createObjectURL(blob));

        worker.onmessage = (e) => {
            console.log('main onmessage', e.data)
        }

        const startWorker = () => {
            console.log('start worker');
            worker.postMessage('start');
        }

        const stopWorker = () => {
            console.log('end worker');
            worker.postMessage('stop');
            worker.terminate();
        }
    </script>
</html>
  1. server-sent event
    1. socket과 비슷하지만 socket은 단방향, server-sent는 단방향인 차이점이 있습니다.
    2. 주로 소셜미디어 업데이트, 뉴스 피드 등에 사용되는 기술입니다.
new EventSource('<http://127.0.0.1:3001/>')
...
sse.addEventListener('message', function(e){
    console.log(e.data);
})
sse.addEventListener('open', function(e){
    console.log(e.data);
})
  1. push messages
    1. Push message의 경우엔 HTML5의 표준인 Service Worker 기술을 통해서 메시지가 사용자에게 노출 되는 방법입니다. 해당 사이트에 접속중이지 않더라도 브라우저가 실행중이라면 사용자에게 알림이 노출됩니다.
  2. fetch stream
    1. fetch를 stream을 통해서 데이터를 chunk단위로 가져오는 방법입니다.

참고 자료

https://pks2974.medium.com/web-worker-간단-정리하기-4ec90055aa4d

https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

https://vroomfan.tistory.com/41

https://dev-l.tistory.com/15

https://web.dev/workers-basics/#toc-enviornment-loadingscripts

https://inpa.tistory.com/entry/NODE-📚-Server-Sent-Events-💯-정리-사용법

https://nsinc.tistory.com/218

https://web.dev/i18n/ko/fetch-upload-streaming/

Comments