자바스크립트는 기본적으로 단일 스레드에서 동작한다. 즉 한 번에 한 가지 일만 할 수 있다.
자바스크립트에서는 매우 일찍부터 비동기적 실행 매커니즘이 존재했지만, 필요한 장치가 추가되었다. 콜백, 프라미스, 제너레이터가 그것이다.
제너레이터는 비동기적 프로그래밍을 전혀 지원하지 않으므로 비동기적으로 쓰기 위해 프라미스나 특수한 콜백과 함께 사용해야한다.
사용자 입력 외에, 비동기적 테크닉을 사용해야 하는 경우는 크게 세 가지가 있다.
- Ajax 호출을 비롯한 네트워크 요청
- 파일을 읽고 쓰는 등의 파일시스템 작업
- 의도적으로 시간 지연을 사용하는 기능(알림 등)
14.1 비유
분주한 음식점에서 전화번호를 받아서 자리가 나면 알려줌 : 콜백
음식점에 자리가 났을 떄 진동하는 호출기를 넘겨줌 - 프라미스
14.2 콜백
콜백은 자바스크립트에서 가장 오래된 비동기적 매커니즘이다.
콜백이란 나중에 호출할 함수이다.
콜백 함수는 일반적으로 다른 함수에 넘기거나, 객체의 프로퍼티로 사용한다. 배열에 넣어서 쓰기도한다. 대게 익명 함수로 사용한다.
1 | // ## 14.2 콜백 |
작성하는 코드와 실제 실행되는 코드가 다르다. 비동기적 실행의 가장 중요한 점은 어떤 것도 차단하지 않는다는 것 자바스크립트는 싱글 스레드를 사용하므로, 컴퓨터에 60초 동안 대기한 후 코드를 실행하게되면 프로그램이 멈추고, 사용자 입력을 받아들이지 않는 등의 문제가 발생한다. 위 예제는 이름 붙은 함수 f를 setTimeout에 넘겼으나, 이름을 쓸 이유가 없다면 일반적으로 익명 함수를 사용한다.
1 | setTimeout(function(){ |
14.2.1 setInterval 과 clearInterval
setTimeout은 콜백 함수를 한 번만 실행하고 멈추지만,
setInterval은 콜백을 정해진 주기마다 호출하며 clearInterval을 사용할 때까지 멈ㅊ지 않는다.
1 | //분이 넘어거거나 10회째가 될 때까지 5초마다 콜백 실행 |
setInterval이 ID 값을 반환하므로 이 ID를 써서 실행을 멈출 수 있다. clearInterval이 반환하는 ID값을 받아 타임아웃을 종료
14.2.2 스코프와 비동기적 실행
비동기적 실행에서 혼란스럽고 에러가 자주 일어나는 부분은 스코프와 클로저가 비동기적 실행에 영향을 미치는 부분이다.
함수를 호출하면 항상 클로저가 만들어진다. 매개변수를 포함해 함수 안에서 만든 변수는 모두 무언가가 자신에 접근 할 수 있는한 계속 존재
1 | //5초 카운트 다운 |
var가 아닌 let 변수를 for 루프 밖에서 선언했으므로 같은 문제가 발생한다. 즉 for 루프가 실행을 마치고, i의 값이 -1이 된 다음에야 콜백이 실행된다. 여기서 i는 두 가지 방법으로 사용됬다. 1. (5-i)이는 첫번째 타임아웃 : 0 , 두번쨰 : 1000, 세번쨰 : 2000 이렇게 동기적으로 실행된다.
1 | //1. 즉시표현식 |
즉시 호출하는 함수 표현식(IIFE)를 쓰거나, 더 간단하게는 for 루프 선언부에서 let i 를 쓰는 방식으로 해결 가능 콜백은 자신을 선언한 스코프(클로저)에 있는 것에 접근 할 수 있다. 그러므로 i의 값은 콜백이 실행되는 순간마다 다를 수 있다. 이 원칙은 모든 비동기적 테크닉에 적용된다.
14.2.3 우선 오류 콜백
우선 오류 콜백 : 콜백을 사용하면 예외 처리가 어려워 지므로, 콜백과 관련된 에러를 처리할 표준이 필요
이에 따라 콜백의 첫 번쨰 매개변수에 에러 객체를 쓰자는 것이 등장
에러가 null이나 undefined 이면 에러가 없는 것
우선 오류 콜백에서 가장 먼저 생각할 것은
에러 매개변수를 체크하고 그에 맞게 반응한다는 것
1 | //노드에서 파일 콘텐츠를 읽을 떄 우선 오류 콜백을 사용할 경우 |
콜백에서 가장 먼저 하는 일은 err 가 참 같은 값인지 확인하는 것 err가 참 같은 값이면 파일을 읽는 데 문제가 있다는 뜻이므로 콘솔에 오류를 보고하고 즉시 빠져나옵니다. 우선 오류 콜백을 사용할때 많이 하는 실수는 빠져나와야 한다는 사실을 잊는 다는 것 프라미스를 사용하지 않으면 우선 오류 콜백은 노드 개발의 표준이나 다름없음
14.2.4 콜백 헬
콜백 헬 : 중괄호로 둘러쌓여 끝없이 중첩된 삼각형의 코드 블록
14.3 프라미스
프라미스 : 콜백의 단점을 해결하려는 시도에서 만들어짐
프라미스가 콜백을 대체하는 것은 아니다.
- 프라미스는 콜백을 예측 가능한 패턴으로 사용할 수 있게 한다.
- 프라미스 기반 비동기 함수를 호출하면 그 함수는 promise 인스턴스를 반환
- 성공(fulfilled)하거나, 실패(rejected) 하는 단 두가지 뿐
- 성공한 프라미스가 나중에 실패할 일 같은 경우는 없음
- 단 한번만 일어난다. -> 그 프라미스를 결졍됐다(settled)고 한다.
- 프라미스는 객체이므로 어디든 전달 할수 있다.
(음식점에서 받은 예약 호출기를 친구에게 맡기는 것과 비슷)
14.3.1 프라미스 만들기
성공(resolve)와 실패(reject) 콜백이 있는 함수로 새 promise 인스턴스를 만들기만 하면 된다.
1 | // 5초 카운트 다운에 매이지 않고, 카운트가 끝나면 프라미스를 반환 |
resolve와 reject는 함수이다. resolve를 여러번 호출해도 결과는 같다. 첫 번째로 호출한 것만 의미 있다. 프라미스는 성공 또는 실패를 나타낼 뿐이다.
14.3.2 프라미스 사용
1 | //13을 만나면 에러를 내는 함수 |
13에서 에러가 발생, 그러나 콘솔에는 12부터 다시 카운트를 기록한다. reject나 resolve가 함수를 멈추지는 않는다. 그저 프라미스의 상태를 관리할 뿐이다. 즉, 프라미스는 비동기적 작업이 성공 또는 실패하도록 확정하지만, 현재는 진행 상황을 전혀 알려주지 않는다. 즉, 프라미스는 완료되거나 파기될 뿐 50% 진행되었다.라는 개념자체가 없다.
14.3.3 이벤트
이벤트가 일어나면 이벤트 발생을 담당하는 개체(emitter)에서 이벤트가 일어났음을 알린다.
필요한 이벤트는 모두 주시(listen)할 수 있다. 콜백을 통해서 가능
노드에서 이벤트를 지원하는 모듈 EventEmitter가 내장됨
EventEmitter는 클래스와 함께 사용하도록 설계
1 | //countdown 함수를 countdown 클래스로 변경 |
EventEmitter를 상속하는 클래스는 이벤트를 발생시킬 수 있다. 실제 카운트다운을 시작하고 프라미스를 반환하는 부분은 go 메서드이다. go메서드에서 1) const countdown = this; 즉 countdown에 this를 할당 2) 카운트가 얼마나 남았는지 알려면 this 값을 알아야한다.
14.3.4 프라미스 체인
프라미스에는 체인으로 연결할 수 있다는 장점이 존재
프라미스가 완료되면 다른 프라미스를 반환하는 함수를 즉시 호출 가능
1 | function launch(){ |
프라미스 체인을 사용하면 모든 단계에서 에러를 캐치할 필요가 없다. 체인 어디서든 에러가 생기면 체인 전체가 멈추고 catch 핸들러가 동작한다.
14.3.5 결졍되지 않는 프라미스 방지하기
프라미스는 비동기적 코드를 단순화하고 콜백이 두 번 이상 실행되는 문제를 방지
하지만, reslove나 reject를 호출하는 걸 잊어서 프라미스가 결정되지 않는 문제까지 자동으로 해결하지는 못함
결정되지 않는 프라미스를 방지하는 방법은 프라미스에 타임아웃을 거는 것
14.4 제너레이터
제너레이터는 함수와 호출자 사이의 양방향 통신을 가능하게 함.
제너레이터는 동기적 성격을 지녔지만, 프라미스와 결합하면 비동기 코드를
효율적으로 관리 가능
1 | //파일 3개를 읽고 1분간 기다린 다음 그 내용을 합쳐서 네 번째 파일에 작성 |
14.4.2 제너레이터 실행기를 직접 만들지 마세요
co(link: https://github.com/tj/co) kos 미들웨어(link: https://koajs.com/)
14.4.3 제너레이터 실행기와 예외처리
제너레이터 실행기를 쓰면 try/catch를 써서 예외 처리가 가능
콜백이나 프라미스를 사용하면 예외 처리가 쉽지 않다.
제너레이터 실행기는 비동기적으로 실행하면서도 동기적인 동작 방식을 유지하므로 try/catch문과 함께 쓸 수 있다.
1 | function* theFutureIsNow(){ |
14.5 요약
요약내용
- 자바스크립트의 비동기적 실행은 콜백을 통해 이루어진다.
- 프라미스를 콜백 대신 사용할 수 있는 것은 아니다. 프라미스 역시 콜백을 사용
- 프라미스는 콜백이 여러 번 호출되는 문제를 해결
- 콜백을 여러 번 호출해야 한다면 이벤트와 결합하는 방법도 있다.
(프라미스도 함께 쓸 수 있다.)- 프라미시는 반드시 결졍된다는(성공 or 실패한다는) 보장은 없다. 다만 프라미스에 타임아웃을 걸면 이 문제가 해결
- 프라미스는 체인으로 연결 할 수 있다.
- 프라미스와 제너레이터 실행기를 결합하면 비동기적 실행의 장점을 유지하면서도 동기적인 사고 방식으로 문제 해결 가능
- 제너레이터를 써서 동기적인 사고방식으로 문제를 해결 할 땐, 프로그램의 어느 부분에서 동시에 실행할 수 있는지 잘 봐야함. 동시에 실행할 부분은 Promise.all을 써서 실행
- 제너레이터 실행기 직접 만들지 말기
- 노드 스타일 콜백을 프라미스로 바꿀 필요도 없다. Q를 써라
- 제너레이터 실행기를 쓰면 예외 처리도 익숙한 방식으로 할 수 있다.