비동기적 프로그래밍
자바스크립트 어플리케이션은 단일 스레드에서 동작한다. 즉 자바스크립트는 한 번에 한 가지 일만 할 수 있다. 사용자 입력 외에, 비동기적 테크닉을 사용해야 하는 경우
- Ajax 호출을 비롯한 네트워크 요청
- 파일을 읽고 쓰는 등의 파일시스템 작업
- 의도적으로 시간 지연을 사용하는 기능(알람 등)
callback
callback은 나중에 호출할 함수. callback 함수는 일반적으로 다른 함수를 넘기거나 객체의 프로퍼티로 사용한다. 보통 익명 함수로 사용함.
console; // 1{console; // 1분 뒤 표시 4}; // 1분console; // 2console; // 3
setInterval과 clearInterval
setTimeout은 callback 함수를 한 번만 실행하고 멈추지만, setInterval은 callback을 정해진 주기마다 호출하며 clearInterval을 사용할 때까지 멈추지 않는다. 참고로 clearInterval의 syntax는 아래와 같다.
scope
setInterval호출 할 때 intervalID값을 변수에 담아 두었다가 clearInterval을 호출할 때 이용한다.
scope와 비동기적 실행
함수를 호출하면 항상 클로저가 만들어진다. 클로저란 무엇인가?
클로저란 함수와 함수가 선언된 어휘적 환경의 조합이다. 라고 MDN에서는 말한다.
{var name = "Mozilla";{;}return displayName;}var myFunc = ;;
함수 안의 지역 변수들은 그 함수가 수행되는 기간 동안에만 존재한다. makeFunc() 실행이 끝나면 name 변수에 더 이상 접근할 수 없게 될 것으로 예상하지만 코드가 여전히 예상대로 작동한다. 그 이유는 자바스크립트의 함수가 클로저를 형성하기 때문이다. 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 범위 내에 있는 모든 지역 변수로 구성된다. 위의 경우, myFunc은 makeFunc이 실행될 때 생성된 displayName 함수의 인스턴스에 대한 참조다. displayName의 인스턴스는 그 변수, name 이 있는 어휘적 환경에 대한 참조를 유지한다. 이런 이유로 myFunc가 호출될 때 변수 name은 사용할 수 있는 상태로 남게 되고 “Mozilla” 가 alert 에 전달된다.
{return {return x + y;};}var add5 = ; // 동일한 함수 참조var add10 = ; // 동일한 함수 참조console; // 7console; // 12
add5와 add10 둘은 같은 함수를 호출하여 변수에 할당받았으나, javascript의 전역 변수에 할당될 때에는 다음과 같이 될 것이다.
var {// 이 블록에서 x는 5이다.x = 5;return {return x + y;};}var {// 이 블록에서 x는 10이다.x = 10;return {return x + y;};}
add5와 add10은 클로저이다. 둘은 같은 함수를 공유하지만 서로 다른 어휘적 환경을 저장한다. { } 블록 안의 환경이 다르다는 말인 듯.
callback은 자신을 선언한 scope(클로저)에 있는 것에 접근할 수 있다.
오류 우선 callback
callback의 첫 번째 매개변수에 에러 객체를 쓰는 것. 에러를 먼저 확인 한 후 다음 로직을 처리하라는 것인데, 문제는 에러가 발생한 경우 callback에서 빠져나와야 한다는 사실을 잊는 경우가 많다는 것이다. callback이 실패하는 경우도 염두해야 한다. callback을 사용하는 인터페이스를 만들 때에는 오류 우선 callback을 사용해야 한다.
promise
promise는 callback을 예측 가능한 패턴으로 사용할 수 있게 해 준다. promise는 성공(fulfilled)과 실패(rejected) 단 두 가지 뿐이다.
// setInterval & clearInterval | |
// setInterval은 정해진 주기마다 호출 clearInterval을 사용할 때 까지 실행 | |
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); | |
// 스코프와 비동기적 실행 | |
// 함수를 호출하면 항상 클로저가 만들어짐 | |
function countdown() { | |
// let i; 이렇게 밖에다 선언하면 -1이 6번 실행됨. | |
console.log('CountDown:'); | |
for (let i = 5; i >= 0; i--) { | |
// i는 블록 스코프 변수 | |
// 2. loop안에서 만드는 콜백은 모두 i에 접근 가능하며 접근 하는 i는 모두 똑같은 i이다. | |
setTimeout(function() { | |
console.log(i === 0 ? 'GO!' : i); | |
}, (5 - i) * 1000); | |
} | |
} | |
countdown(); // 1. 호출 시 변수 i가 들어있는 클로저 생성 | |
// 콜백은 자신이 선언한 스코프(클로저)에 있는 것에 접근 할 수 있다. | |
// 따라서 i의 값은 콜백이 실제 실행되는 순간마다 다를 수 있다. | |
// Promise (성공:resolve, 실패:reject 콜백 함수가 있는 Promise 인스턴스 반환). 현재 진행 상황은 알 수 없음. | |
function countdown2(seconds) { | |
return new Promise(function(resolve, reject) { | |
for (let i = seconds; i >= 0; i--) { | |
setTimeout(function() { | |
if (i === 13) return reject(new Error('Oh my god')); | |
if (i > 0) console.log(i + '...'); | |
else resolve(console.log('GO2!')); | |
}, (seconds - i) * 1000); | |
} | |
}); | |
} | |
const cf = countdown2(15); | |
cf.then(function() { | |
console.log('countdown completed successfully'); | |
}).catch(function(err) { | |
console.log('countdown experienced an error:' + err.message); | |
}); | |
// event | |
const EventEmitter = require('events').EventEmitter; | |
class CountDown extends EventEmitter { | |
constructor(seconds, superstitious) { | |
super(); | |
this.seconds = seconds; | |
this.superstitious = !!superstitious; | |
} | |
go() { | |
const countdown = this; | |
const timeoutIds = []; | |
return new Promise(function(resolve, reject) { | |
for (let i = countdown.seconds; i >= 0; i--) { | |
timeoutIds.push( | |
setTimeout(function() { | |
if (countdown.superstitious && i === 13) { | |
// 대기중인 타임아웃을 모두 취소한다. | |
timeoutIds.forEach(clearTimeout); | |
return reject(new Error('Oh my God!!!')); | |
} | |
countdown.emit('tick', i); | |
if (i === 0) resolve(); | |
}, (countdown.seconds - i) * 1000) | |
); | |
} | |
}); | |
} | |
} | |
const c = new CountDown(5); | |
c.on('tick', function(i) { | |
if (i > 0) console.log(i + '~~~'); | |
}); | |
c.go() | |
.then(function() { | |
console.log('GO~~!!'); | |
}) | |
.catch(function(err) { | |
console.error(err.message); | |
}); | |
// 제너레이터 사용으로 await 처럼 구현해보자(feat. Q 프라미스 라이브러리) | |
function nfcall(f, ...args) { | |
return new Promise(function(resolve, reject) { | |
f.call(null, ...args, function(err, ...args) { | |
if (err) return reject(err); | |
resolve(args.length < 2 ? args[0] : args); | |
}); | |
}); | |
} | |
function ptimeout(delay) { | |
return new Promise(function(resolve, reject) { | |
setTimeout(resolve, delay); | |
}); | |
} | |
function grun(g) { | |
const it = g(); | |
(function iterate(val) { | |
const x = it.next(val); | |
if (!x.done) { | |
if (x.value instanceof Promise) { | |
x.value.then(iterate).catch(err => it.throw(err)); | |
} else { | |
setTimeout(iterate, 0, x.value); | |
} | |
} | |
})(); | |
} | |
// function* theFutureIsNow() { | |
// const dataA = yield nfcall(fs.readFile, 'a.txt'); | |
// const dataB = yield nfcall(fs.readFile, 'b.txt'); | |
// const dataC = yield nfcall(fs.readFile, 'c.txt'); | |
// yield ptimeout(60 * 1000); | |
// yield nfcall(fs.writeFile, 'd.txt', dataA + dataB + dataC); | |
// } | |
grun(theFutureIsNow); | |
// **제너레이터 실행기를 직접 만들지 마세요. | |
// promise.all을 사용해 구현 가능. 배열에 들어있던 순서대로 반환한다. | |
function* theFutureIsNow() { | |
const data = yield Promise.all([ | |
nfcall(fs.readFile, 'a.txt'), | |
nfcall(fs.readFile, 'b.txt'), | |
nfcall(fs.readFile, 'c.txt') | |
]); | |
yield ptimeout(60 * 1000); | |
yield nfcall(fs.writeFile, 'd.txt', data[0] + data[1] + data[2]); | |
} |
이벤트
노드에는 이벤트 지원 모듈 EventEmitter가 내장되어 있다.
요약
- 자바스크립트의 비동기적 실행은 callback을 통해 이루어진다.
- Promise 역시 callback을 사용한다.
- Promise는 callback이 여러 번 호출되는 문제를 해결했다.
- Promise는 반드시 결정된다는 보장은 없지만, timeout을 걸면 해결된다.
- Promise는 체인으로 연결할 수 있다.
- 제너레이터를 써서 동기적으로 처리할 때에는 동시에 실행할 수 있는 부분은 Promise.all을 사용해라.