티스토리 뷰

https://www.notion.so/simsimjae/10-setTimeout-setInterval-b71a02545c8f44e6afcb6b685d675cd3

아래 글들은 검색엔진 노출을 위한 글입니다. 

노션에서 확인하시면 이미지와 함께 정리된 글을 보실수 있습니다. 궁금하신점은 블로그 댓글이나 노션 코멘트를 이용해주세요(@comment)


제가 직접 만든 프로젝트입니다.

http://pickvs.com : 닥전닥후


 

setTimeout

function sayHi(phrase, who) { alert( phrase + ', ' + who); } setTimeout(sayHi, 1000, "Hello", "John"); //setTimeout(sayHi(), 1000); //sayHi()가 실행되면 undefined가 리턴됨. 잘못됨!!

  • 최소 delay시간 후에 콜백 함수를 실행시킨다.

  • sayHi부분에 코드를 직접 블록으로 감싸서 넣을순 있지만 재활용이 불가하기 때문에 권장하지 않는다.

    let timerId = setTimeout(() => alert("never happens"), 1000); clearTimeout(timerId);

  • 위와 같이 timerid를 받아서 스케쥴링을 취소시킬수도있다.

  • 이 timerid는 브라우저에서는 숫자이지만 다른 환경(Node.js등)에서는 다른것일수도있다.

setInterval

// 2초마다 반복 let timerId = setInterval(() => alert('tick'), 2000); // 5초 후에 정지 setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

  • 실제로 실행시켜보면 콘솔에 timerId가 9가 찍힌다.
  • tick이라는 얼럿이 두번뜨고나서 stop얼럿이 한번 뜨며 종료된다.

재귀 setTimeout

  • setInterval와 마찬가지로 setTimeout을 재귀적으로 쓰면 함수를 주기적으로 호출할 수 있다.

    let timerId = setTimeout(function tick() { alert('tick'); timerId = setTimeout(tick, 2000); /* ...code */ }, 2000);

  • 만약 code부분에 10초가 걸리는 작업이 있다고 해보자.

  • 처음 setTimeout이 호출되면 브라우저에게 2초뒤에 tick()을 실행시켜달라고 요청한다.

  • 브라우저는 2초뒤에 큐에 tick()을 넣고 상황에 따라 스택에 tick()을 push한다.

  • 그리고 나서 tick()은 약 10초동안 실행되고 또 자기자신을 재귀호출한다.

  • 이번에도 브라우저는 setTimeout에 의해서 2초뒤에 tick()을 큐에 넣는다.

  • 하지만 아직, JS엔진에서는 그전에 실행중인 tick()이 계속 실행중이라서 큐에 있는 tick()을 스택에 push할수없다.

  • 나는 2초마다 tick()을 호출하고 싶어서 위와 같이 코드를 짰는데 내 생각과는 다르게 동작할 수 있는걸 유의해야한다!!

재귀적 setTimeout은 setInterval보다 유연하다. 예를들어, 내가 5초마다 서버에 요청을 보내는 로직을 작성하고 있는데 서버에 요청이 과하게 들어올때는 요청을 보내는 주기를 늘려주는게 바람직하다. 이럴때, 요청 주기를 늘리기 위해서 setInterval은 delay값이 고정되어 있기 때문에 clear해주고 다시 호출해야 하는 반면에 재귀적인 setTimeout은 내부 로직에서 delay를 늘릴 수 있을것이다.

let delay = 5000; let timerId = **setTimeout**(function request() { // http요청.. if(//서버과부하에 의한 요청 실패?) { delay *= 2;// 다음 실행까지 인터벌을 좀 늘리자.. } timerId = **setTimeout**(request, delay); }, **delay**);

setInterval은 정확한 간격마다 실행되지 않을 수 있다

일정시간마다 함수를 호출하는 두가지 방법(setInterval, 재귀적 setTimeout)중 어떤것이 더 좋은방법인지에 대해서 알아보자.

1. setInterval 사용 100ms마다 반복 호출

let i = 1; setInterval(function () { func(i); }, 100);

  • 위 코드는 다음과 같이 실행될것이다.

 

  • 여기서 만약에 func함수가 100ms보다 오래걸린다면??? 딱봐도 문제가 생길것같지 않은가?
  • 그리고 또하나의 문제는 나는 그전 함수 호출의 종료 - 현재 함수 호출의 시작 사이의 간격을 100ms로 하고 싶었던건데 이 100ms안에 함수 실행시간이 포함되어서 실제 함수 실행간 간격이 100ms보다 훨씬 적다.
  • func가 200ms가 걸린다면 func함수는 딜레이없이 계속 실행될것이다.

2. setTimeout 사용 100ms마다 반복 호출

let i = 1; setTimeout(function run() { func(i); setTimeout(run, 100); }, 100);

  • 반면에 재귀적인 setTimeout은 거의 정확한 함수 호출간 딜레이 시간을 보장한다.

 

  • 함수 호출이 종료되고 그 다음 함수가 실행되기 직전 까지 최소 100ms이 걸린다.
  • 이 방법을 사용하면 거의 정확히 고정된 delay를 보장해 줄 수 있다.

가비지 컬렉션 setTimeout과 setInterval에 넘겨진 콜백함수는 가비지 컬렉션에 대상이 되지 않는다.

어떻게 보면 당연한 말이다. delay를 만약에 2h로 주게되면 그동안 가비지 컬렉션이 충분히 일어날 수 있는 시간이다. 갑자기 콜백함수가 가비지 컬렉션 된다는건 말이 안된다.

브라우저가 이 콜백함수에 대한 레퍼런스를 들고 있어서 가비지 컬렉션 되지 않는것이다.

이 콜백함수는 lexical환경 바깥을 참조하고있기 때문에 그 바깥에 선언된 변수들도 마찬가지로 가비지 컬렉션의 대상이 되지 않는다. 그렇기 때문에 많은 변수와 함수들이 메모리를 계속 차지하고 있을 수 있다.

스케쥴러를 사용하지 않으면 clear로 초기화 하라는게 다 이런 이유떄문이다.

이제 왜 setTimeout의 delay를 0으로 설정하는지 이해할 수 있다.

setTimeout(() => alert("World")); alert("Hello");

  • 위 코드 실행 결과 HelloWorld가 출력된다.
  • World가 포함된 콜백함수는 브라우저API를 거쳐 큐에 삽입되었다가 스택에 있는 alert("Hello")가 실행을 마치자마자 스택에 삽입되어 실행된다.
  • 즉, setTimeout( func, 0) 쓰는 이유는 함수 호출의 실행 순서를 바꿔주기 위함이다.

setTimeout 0는 무거운 작업을 split 할때도 사용된다.

let i = 0; let start = Date.now(); function count() { // 약간의 무거운 작업을 해봅시다. (*) do { i++; } while (i % 1e6 != 0); if (i == 1e9) { alert("Done in " + (Date.now() - start) + 'ms'); } else { setTimeout(count); // 호출을 스케쥴링합니다. (**) } } count();

  • count()함수는 내부 로직에서 i를 0에서 ie9까지 증가시키는 굉장히 무거운 작업이다.

  • 자바스크립트는 싱글스레드이기 떄문에 한번에 하나의 일만 처리할 수 있다.

  • 만약 이 함수가 실행된다면 엔진은 이 함수 실행 외에는 아무일도 할 수 없기 때문에 버튼같은걸 클릭해도 자바스크립트가 동작하지 않고 먹통이 되는 현상이 발생할것이다.

  • 그렇기 때문에 이렇게 무거운 작업은 나눠서 처리하는게 좋은데 그럴떄 사용하는것이 setTimeout 0이다.

  • 0에서 1e9까지 숫자를 증가시키는 작업을 1e6씩 증가시키는 작업 여러개로 쪼개는것이다.

  • 쪼개진 작업은 쪼개지지 않았던 큰 덩어리 하나의 작업보다 훨씬 적은 시간안에 실행된다.

  • 그리고 여러번 setTimeout 함수로 실행시켰기 때문에 중간에 다른 함수들이 치고들어와서 실행될수 있다.

  • 그래서 위 함수가 실행되는 동시에 유저는 화면과 상호작용할 수 있다.

  • 이렇게 무거운 작업을 나눠서 실행하던 나누지 않고 한번에 순차적으로 처리하든 총 처리 시간은 거의 비슷하다. 그럼? 무조건 나눠실행해서 유저가 답답함을 느끼지 않도록 하는게 훨씬 좋은 코드일것이다.

    let i = 0; let start = Date.now();

    function count() { // 스케쥴링을 앞으로 이동. if ( i < 1e9 - 1e6) { //조건문이 필요한 이유는 아래 그림에. setTimeout(count);
    }

    do { i++; } while (i % 1e6 != 0); if ( i == 1e9) { alert("Done in " + (Date.now() - start) + 'ms'); }

    }

    count();

  • 위 코드는 무거운 작업을 먼저하고 그다음 스케쥴링을 했다.

  • 근데 이 코드는 스케쥴링을 먼저하고 무거운 작업을 진행한다. 앞 코드에 비해 순서만 바뀌었지만 이 코드가 앞전코드보다 빠르게 동작한다.

  • 왜냐면, 무거운 작업을 하고 스케쥴링을 하게 되면 그 스케쥴링 된 다음 함수가 호출되기 까지 다른 함수들이 중간에 스택에 치고들어올 수 있다. 작업간 딜레이가 어쩔수없이 생기는것이다.

  • 하지만, 무거운 작업을 하기 전에 스케쥴링을 하게 되면 무거운 작업이 끝날때쯤에는 큐에서 그 다음 스케쥴링된 함수가 대기를 하고 있기 때문에 바로바로 실행이 된다.

  • 그래서 if문으로 스케쥴링 해야하는 구간을 설정해준것이다.

 

브라우저는 중첩된 타이머가 있을때최소 간격을 강제로 넣는다.

let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); //콜백함수가 실행된 시점을 times배열에 저장함. if (start + 100 < Date.now()) alert(times); // 100ms이후에는 기록하지않음. else setTimeout(run); // 100ms이전에는 계속 스케쥴링함. }); // times : 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100

  • 위 코드를 보면, 100ms이전에는 계속 재귀적인 스케쥴링이 일어난다.
  • 브라우저 → 큐 → 스택 순서대로 콜백이 계속 순환하면서 실행되는데, 스택에서 실행되는 시점을 times배열에 기록하는 코드이다.
  • 이렇게 빠르게 타이머를 계속 사용하는 경우에 브라우저는 각 타이머에 최소한의 간격을 강제로 집어 넣는다.
  • 이렇게 하는 이유는 재귀적으로 타이머가 계속 실행되면 그 콜백만 계속 실행되고 다른 콜백들이나 함수들이 실행하는데 방해가 될 수 있기 때문인것같다.(확실하진않으나 추측)
  • Node.js등과 같이 브라우저가 아닌 JS환경에서는 최소 간격 강제 현상이 없다고 한다.

브라우저가 렌더링하도록 허락하기

브라우저 내부에는 자바스크립트 엔진이 있다. 브라우저가 자바스크립트 코드를 만나게 되면 그 자바스크립트의 실행이 완료되고 나서야 화면을 다시 그리는 repainting 이 일어난다.

그래서 만약 무거운 JS 로직이 실행되면 그 로직이 끝날때까지 화면은 변화하지 않는다. 그래서 이 무거운 작업을 위에서 배운 setTimeout으로 splite하는 기법을 사용해서 잘라 실행해야 한다.

그렇게 되면, 잘게 쪼개진 자바스크립트 실행이 끝날때마다 화면에 업데이트 내용이 반영되어서 뭔가가 진행되고 있다는걸 사용자도 알 수 있게 된다.

<div id="progress"></div> <script> let i = 0; function count() { for (let j = 0; j < 1e6; j++) { i++; // 현재의 i 값을 progress div에 넣습니다. // innerHTML에 대해 더 알아봅시다. progress.innerHTML = i; } } count(); </script>

  • 해당 코드가 실행되면 for문이 모두 실행되고 나서야 마크업에 그 값이 반영 된다.

  • 즉, 화면에서는 아무것도 안보이다가 갑자기 1e6이라는 숫자가 보일것이다.

  • 변화되는 내용을 중간중간 화면에 보여주기 위해서는 setTimeout으로 작업을 split 해야한다.

    <div id="progress"></div>

    <script> let i = 0;

    function count() { do { // do a piece of the heavy job (*) i++; progress.innerHTML = i; } while (i % 1e3 != 0); if (i < 1e9) { setTimeout(count); //이게 실행되는 순간 JS코드 실행이 종료되고 브라우저APi가 실행된다. 따라서, 이 시점에 위에 do while문에서 열심히 증가시킨 i값이 화면에 반영될것이다. } } count();

    </script>

결론

일정시간 단위로 어떤 함수를 호출하고 싶으면 setInterval을 사용하지말고재귀적 setTimeout을 사용하자.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함