자바스크립트의 비동기

지금까지는 사용자의 행동에 반응하면서 비동기적 프로그래밍을 접해봤다.
사용자의 행동은 전적으로 비동기적이다. 사용자가 언제 클릭할지, 터치할지, 또는 타이핑할지 전혀 알 수 없다. 하지만 비동기적 실행이 사용자 입력 하나 때문에 필요한 건 아니다.
자바스크립트의 본적 때문에 비동기적 프로그래밍이 필요하다.

자바스크립트 애플리케이션은 단일 스레드에서 동작한다.

스레드

스레드는 CPU의 이용의 기본 단위다.
프로세스 내에서 프로그램 명령을 실행하는 기본 단위이자 흐름, 개체라고 할 수 있다.
스레드는 스레드 ID, 프로그램 카운터, 레지스터 집합, 스택 으로 구성된다. 스레드는 같은 프로세스에 속한 다른 스레드와 코드, 데이터 섹션, 열린 파일이나 신호와 같은 운영체제 자원들을 공유한다.
프로세스에 하나의 제어 모델이 있으면 단일 스레드이며,
프로세스가 다수의 제어 스레드를 가진다면 다중 스레드 모델이다.

즉, 자바스크립트는 한 번에 한 가지 일만 할 수 있다.
싱글 스레드는 일이 제한된다고 느낄 수 있지만 멀티 스레드 프로그래밍에서 겪는 골치 아픈 문제들을 신경 쓰지 않아도 된다는 장점이 있다.
하지만 부드럽게 동작하는 소프트웨어를 만들기 위해 사용자의 입력뿐만 아니라 여러 문제를 비동기적 관점에서 생각해야 한다.

자바스크립트와 비동기적 프로그래밍에는 뚜렷이 구분되는 세 가지 패러다임이 있다.

  • 콜백
  • 프로미스
  • 제너레이터

사용자 입력 외에, 비동기적 테크닉을 사용해야 하는 경우

  • Ajax 호출을 비롯한 네트워크 요청
  • 파일을 읽고 쓰는 등의 파일시스템 작업
  • 의도적으로 시간 지연을 사용하는 기등 (알람 등)

콜백

콜백은 자바스크립트에서 가장 오래된 비동기적 메커니즘이다.
간단히 말해 나중에 호출할 함수이고 함수 자체에는 특별한 것이 전혀 없다. 콜백 함수는 일반적으로 다른 함수에 넘기거나 객체의 프로퍼티로 사용한다.

console.log('Before timeout:'+ new Date());
function f() {
  console.log('After timeout:' + new Date());
}
setTimeout(f, 5000);
console.log("I happen after setTimeout")
console.log('me too')

이 코드의 결과는

"Before timeout:Sun Oct 13 2019 19:59:32 GMT+0900 (한국 표준시)"
"I happen after "
"me too"
"After timeout:Sun Oct 13 2019 19:58:51 GMT+0900 (한국 표준시)"

우리는 컴퓨터가 우리가 작성한 코드를 정확히 그 순서대로 실행할 거라고 기대한다. 하지만 기대대로 된다면 그것은 비동기적이지 않다.
비동기적 실행의 가장 큰 목적, 가장 중요한 요점은 어떤 것도 차단하지 않는다는 것이다.

setInterval과 clearInterval

setTimeout은 콜백 함수를 한 번만 실행하고 멈추지만, setInterval은 콜백을 정해진 주기마다 호출하며 clearInterval을 사용할 때까지 멈추지 않는다.

이 예제는 분이 넘어가거나 10회째가 될 때까지 5초마다 콜백을 실행한다.

const start = new Date();
let i = 0;
const intervalId = setInterval(function() {
  let now = new Date();
  if(now.getMinutes() !== start.getMinutes() || ++i > 10)
    return clearInterval(intervalId);
  console.log(`${i}: ${now}`);
},5*1000);

이 예제를 보면 setInterval 이 ID를 반환한다는 사실을 알 수 있다.
setInterval과 clearInterval은 반환하는 ID를 받아 타임아웃을 멈춘다.

스코프와 비동기적 실행

비동기적 실행에서 혼란스럽고 에러도 자주 일어나는 부분은 스코프클로저가 비동기적 실행에 영향을 미치는 부분입니다. 함수를 호출하면 항상 클로저가 만들어진다. 매개변수를 포함해 함수 안에서 만든 변수는 모두 무언가가 자신에 접근할 수 있는 한 계속 존재한다.

이 countdown 함수의 목적은 5초 카운트다운을 만드는 것이다.

function countdown(){
  let i;
  console.log("Countdown:");
  for(i = 5; i >= 0; i--){
    setTimeout(function(){
      console.log(i === 0 ? "go" : i);
    }, (5-i)*1000);
  }
}

countdown();

결과는 -1을 여섯 번 반복한다.

let i; 를 for문 밖에서 선언했기 때문에 for 루프가 실행을 마치고 i의 값이 -1이 된 다음에서야 콜백이 실행되기 시작된다.

스코프와 비동기적 실행이 어떻게 연관되는지 이해하는 것이 중요하다. countdown을 호출하면 변수 i가 들어있는 클로저가 만들어지는데 for 루프 안에서 만드는 콜백을 모두 i에 접근할 수 있고, 이것이 접근하는 i는 모두 똑같은 i이다.

function countdown(){
  console.log("Countdown:");
  for(let i = 5; i >= 0; i--){
    setTimeout(function(){
      console.log(i === 0 ? "go" : i);
    }, (5-i)*1000);
  }
}

countdown();

i를 for 루프 선언부에서 선언하는 방식으로 선언할 수 있다.

여기서 주의할 부분은 콜백이 어느 스코프에서 선언됐느냐 이다. 콜백은 자신을 선언한 스코프 (클로저)에 있는 것에 접근할 수 있다. 따라서 i의 값은 콜백이 실제 실행되는 순간마다 다를 수 있다. 이 원칙은 콜백뿐만 아니라 모든 비동기적 테크닉에 적용된다.

프라미스

콜백을 사용해 비동기적으로 실행할 수 있지만, 한 번에 여러 가지를 기다려야 한다면 콜백을 관리하기가 상당히 어려워진다. 중괄호로 둘러싸여 끝없이 중첩된 코드 블록들을 콜백 헬이라고 부른다. 비동기적 코드가 늘어날수록 버그가 없고 관리하기 쉬운 코드를 작성하기는 매우 어려워진다. 그래서 프라미스가 등장했다.

프라미스의 기본 개념

프라미스 기반 비동기적 함수를 호출하면 그 함수는 Promise 인스턴스를 반환한다. 프라미스는 성공하거나, 실패하거나 단 두 가지뿐이다.

프라미스는 객체이므로 어디든 전달할 수 있다는 점도 콜백에 비해 간편한 장점이다.

프라미스를 만들어보자.

function countdown(seconds) {
    return new Promise(function(resolve, reject){
        for(let i = seconds; i >= 0; i--){
            setTimeout(function(){
                if(i > 0) console.log(i + "...");
                else resolve(console.log("Go"));
            }, (seconds-i) * 1000);
        }
    }
}

별로 좋은 함수는 아니지만 프라미스를 어떻게 만드는지 잘 드러나 있다.

프라미스를 사용해보자

countdown(5).then(
    function(){
        console.log("countdown success");
    },
    function(err){
        console.log("countdown error" + err.message);
    }
)

then 핸들러를 바로 호출했다. then 핸들러는 성공 콜백과 에러 콜백을 받는다.

const p = countdown(5);
p.then(function(){
    console.log("countdown success");
});
p.then(function(err){
    consoel.log("countdown error" + err.message);
})

프라미스 체인

프라미스에는 체인으로 연결할 수 있다는 장점이 있다. 프라미스가 완료되면 다른 프라미스를 반환하는 함수를 즉시 호출할 수 있다.

launch 함수를 만들어서 카운트다운이 끝나면 실행되게 만들어보자.

function launch(){
    return new Promise(function(resolve, reject){
        console.log("lift off");
        setTimeout(function(){
            resolve("In orbit!");
        }, 2 * 1000)
    })
}

이 함수를 카운트다운에 쉽게 묶을 수 있다.

const c = new Countdown(5)
    .on('tick', i => console.log(i + "..."));

c.go()
    .then(launch);
    .then(function(msg){
        consoel.log(msg);
    })
    .catch(function(err){
        console.log(err)
    })

프라미스 체인을 사용하면 모든 단계에서 에러를 캐치할 필요는 없다. 체인 어디에서든 에러가 발생하면 catch 핸들러가 작동하기 때문이다.

요약

  • 자바스크립트의 비동기적 실행은 콜백을 통해 이루어진다
  • 프라비스를 콜백 대신 사용할 수 있는 건 아니다. 프라미스 역시 콜백을 사용한다.
  • 프라미스는 콜백이 여러번 호출되는 문제를 해결했다.
  • 프라미스는 체인으로 연결될 수 있다.

참조


https://velog.io/@rohkorea86/Promiseis-%EB%B9%84%EB%8F%99%EA%B8%B0%EB%8F%99%EA%B8%B0%EC%97%90%EC%84%9C-Promise%EA%B9%8C%EC%A7%80#promise-%EB%8F%84%EC%9E%85%EB%AA%A9%EC%A0%81