콜백함수를 이해하기 위해서는 먼저 '호이스팅'에 대해 이해 해야 한다.
함수 안에 있는 선언들을 모두 끌어올려서 해당 함수 유효범위의 최상단에 선언하는 것.
함수가 실행되기 전에 함수에 필요한 모든 값을 최상단에 선언한다.
브라우저 API
callback 함수를 time(ms단위) 이후 출력한다.
자바스크립트는 Synchronous 이다.
Synchronous callback
console.log('1');
setTimeout(() => console.log('2'), 1000);
console.log('3');
function printImmediately(print){
print();
}
printImmediately(() => console.log('hello'));
호이스팅 결과.
function printImmediately(print){ // 1. 호이스팅 함수선언문
print();
}
console.log('1'); // 2. '1' 출력
setTimeout(() => console.log('2'), 1000); // 3. 브라우저API에 1초후에 '2' 출력 요청
console.log('3'); // 4. '3' 출력
printImmediately(() => console.log('hello')); // 5. 함수 즉시출력 실행 'hello' 출력
// 6. 1초 후, 브라우저API에 의해 '2' 출력
Asynchronous callback
console.log('1');
setTimeout(() => console.log('2'), 1000);
console.log('3');
function printImmediately(print){
print();
}
printImmediately(() => console.log('hello'));
function printWithDelay(print, timeout){
setTimeout(print, timeout);
}
printWithDelay(()=> console.log('async callback'), 2000);
호이스팅 결과
function printImmediately(print){
print();
}
function printWithDelay(print, timeout){
setTimeout(print, timeout);
}
console.log('1'); // 동기
setTimeout(() => console.log('2'), 1000); // 비동기------------>
console.log('3'); // 동기
printImmediately(() => console.log('hello')); // 동기
printWithDelay(()=> console.log('async callback'), 2000); // 비동기 ------------->
Promise는 "비동기 상태를 값으로 다룰 수 있는 객체"
Promise가 왜 필요한가요?
프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용합니다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 API를 사용합니다.
콜백 패턴의 문제
function requestData1(callback){
callback(data); // 2
}
function requestData2(callback){
callback(data); // 4
}
function onSuccess1(data){
console.log(data);
requestData2(onSuccess2); // 3
}
function onSuccess2(data){ // 5
console.log(data);
}
requestData1(onSuccess1); // 1
// 콜백 패턴의 코드의 흐름이 순차적이지 않기 때문에, 코드를 읽기가 힘들다.
requestData1()
.then(data => {
console.log(data);
return requestData2();
})
.then(data => {
console.log(data);
})
const p1 = new Promise((resolve, reject) => {
resolve(data)
// or reject('error message')
})
const p2 = Promise.reject('error message');
const p3 = Promise.resolve(param);
const p1 = Promise.resolve(123)
console.log(p1 !== 123); // true 이행됨 상태인 프로미스가 반환된다.
const p2 = new Promise(resolve => setTimeout(() => resolve(10), 1));
console.log(Promise.resolve(p2) === p2) // true
// Promise.resolve 함수에 프로미스가 입력되면 그 자신이 반환된다.
//requestData().then(onResolve, onReject);
// 프로미스 처리됨 상태가 되면, onResolve 함수가 호출되고, 거부됨 상태가 되면 onReject 함수가 호출 된다.
Promise.resolve(123).then(data => console.log(data)); // 123
Promise.reject('err').then(null, error => console.log(error)); // err
let requestData1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hun')
}, 1000)
})
let requestData2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('jenny')
}, 2000)
})
requestData1
.then(data => {
console.log(data);
return requestData2; // 1
})
.then(data => {
return data + 1; // 2
})
.then(data => {
throw new Error('some Error'); // 3
})
.then(null, error => {
console.log(error)
});
1.함수에서 프로미스를 반환하면 then메서드는 그 값을 그대로 반환한다.
2.프로미스가 아닌 값을 반환하면, then 메서드는 이행됨 상태인 프로미스를 반환한다.
3. 함수 내부에서 예외가 발생하면 then 메서드는 거부됨 상태인 프로미스를 반환한다.
Promise.reject('err')
.then(() => console.log('then 1')) // 1번
.then(() => console.log('then 2')) // 1번
.then(() => console.log('then 3'), ()=> console.log('then 4')) // 2번
.then(() => console.log('then 5'), ()=> console.log('then 6')); // 3번
거부 됨 상태인 프로미스는 처음으로 만나는 onReject 함수를 호출 하므로 1번 이 생략되고, 2번 코드의 then 4가 출력 된다. then 4를 출력하는 함수는 undefined 를 결과로 가지면서 이행됨 상태인 프로미스를 생성한다.
3번 따라서 이어지는 then 메서드에서는 then 5가 출력된다. then 메서드의 가장 중요한 특징은 항상 연결된 순서대로 호출된다는 점이다.
Promise.reject(1).then(null, error => {
console.log(error);
})
Promise.reject(1).catch(error => {
console.log(error);
})
예외 처리는 then메서드의 onReject를 이용하기보다는, catch메서드를 이용하는게 가독성 면에서 좋다.
Promise.resolve().then(
() => { // 1번
throw new Error('some error');
},
error => { // 2번
console.log(error);
},
);
1번 then 메서드의 resolve 함수에서 발생한 예외는 같은 then 메서드의 2번 reject함수에서 처리되지 않는다.
실행하면 Unhandled promise rejection 에러가 발생한다. 거부됨 상태인 프로미스를 처리하지 않았기 때문이다.
Promise.resolve()
.then(() => {
throw new Error('some error');
})
.catch(error => {
console.log(error);
})
Promise.reject(10)
.then(data => {
console.log('then1:', data);
return 20;
})
.catch(error => {
console.log('catch:', error);
return 30;
})
.then(data => {
console.log('then2:', data);
});
// catch: 10
// then2: 30
프로미스 이용하기 3 : finally
requestData()
.then(data => {
...
})
.catch(error => {
...
})
.finally(() => {
...
});
function requestData(){
return fetch()
.catch(error => {
...
})
.finally(() => {
senLogToServer('requestData finished')
})
}
requestData().then(data => console.log(data)); // 1번
1번, requestData 함수의 반환값은 finally 메서드 호출 이전의 프로미스다. 따라서, requestData함수를 사용하는 입장에서는 finally 메서드 존재 여부를 신경 쓰지 않아도 된다.
Promise.all은 여러 개의 프로미스를 병렬로 처리할 때 사용되는 함수이다. then 메서드를 체인으로 연결하면 각각 비동기 처리가 병렬로 처리되지 않는 단점을 극복한다.
requestData1()
.then(data => {
console.log(data);
return requestData2();
})
.then(data =>{
console.log(data);
});
비동기 함수 간에 서로 의존성이 없다면, 병렬로 처리하는게 더 빠르다. then 메서드를 체인으로 연결하지 않고, 비동기함수를 각각 호출하면, 병렬로 처리 된다.
requestData1().then(data => {
console.log(data);
})
requestData2().then(data => {
console.log(data);
})
requestData1과 requestData2는 동시에 실행 된다. 여러 프로미스를 병렬로 처리하고 싶을 경우 Promise.all을 사용하자.
Promise.all([requestData1(), requestData2()]).then(([data1, data2]) => {
console.log(data1, data2);
})
Promise.all 함수는 프로미스를 반환한다. Promise.all 함수가 반환하는 프로미스는 입력된 모든 프로미스가 처리됨 상태가 되어야 마찬가지로 처리됨 상태가 된다. 만약 하나가 거부됨 상태가 된다면, Promise.all 함수가 반환하는 프로미스도 거부됨 상태가 된다.
Promise.race 함수에 입력된 여러 프로미스 중에서 하나라도 처리됨 상태가 되면, Promise.race 함수가 반환하는 프로미스도 처리됨 상태가 된다.
Promise.race([requestData(), new Promise((_, reject) => setTimeout(reject, 3000)),
])
.then(data => console.log(data))
.catch(error => console.log(error));
requestData 함수가 3초안에 데이터를 받으면 then메서드가 호출되고 그렇지 않으면 catch 메서드가 호출 된다.
처리됨 상태가 되면, 그 상태를 유지하는 프로미스의 성질을 이용해서 데이터를 캐싱할 수 있다.
let cachedPromise;
function getData() {
cachedPromise = cachedPromise || requestData(); // 1번
return cachedPromise;
}
getData().then(v => console.log(v));
getData().then(v => console.log(v));
1번 getData 함수를 처음 호출 할 때만, requestData가 호출된다. 데이터를 가져오는 작업이 끝나면, 그 결과는 cachedPromise 프로미스에 저장된다.
then 메서드 내부 함수에서 return 키워드를 입력하는 것을 깜빡하기 쉽다.
then 메서드가 반환하는 프로미스 객체의 데이터는 내부함수가 반환한 값이다.
return 키워드를 사용하지 않으면 프로미스 객체의 데이터는 undefined가 된다.
Promise.resolve(10)
.then(data => {
console.log(data);
Promise.resolve(20); // 2번
})
.then(data => {
console.log(data); // 1번
})
1번은 의도와는 다르게 undefined가 출력 된다.
2번 코드에서 return 키워드를 입력하면 의도한 대로 20이 출력 된다.
프로미스는 불변 객체이다.
function requestData(){
const p = Promise.resolve(10);
p.then(() => { // 1번
return 20;
});
return p;
}
requestData().then(v => {
console.log(v); // 10 // 2번
})
1번 then 메서드는 기존 객체를 수정하지 않고, 새로운 프로미스를 반환한다. 2번, 코드에서 20이 출력되길 원한다면 requestData 함수를 다음과 같이 수정해야 한다.
function requestData(){
return Promise.resolve(10).then(v => {
return 20;
});
};
requestData1().then(result1 => {
requestData2(result2 => {
....
});
});
가독성이 좋지 않기 때문에, 아래 24번과 같이 바꾸자.
requestData1()
.then(result1 => {
return requestData2(result1)
})
.then(result2 => {
return ... // 1번
})
만약 1번, 에서 result1 변수를 참조 해야 한다면 어떻게 해야 할까?
Promise.all 함수를 사용하면 프로미스를 중첩하지 않고도 해결할수 있다.
requestData1()
.then(result1 => {
return Promise.all([result1, requestData2(result1)]) // 1번
.then(([result1, result2]) => {
...........
});
1번, Promise.all 함수로 입력하는 배열에 프로미스가 아닌 값을 넣으면, 그 값을 그대로 이행됨 상태인 프로미스처럼 처리된다.
프로미스를 동기(sync)코드와 같이 사용할 때는 예외 처리에 신경 써야 한다.
function requestData(){
doSync(); // 1번
return fetch()
.then(data => console.log(data))
.catch(error => console.log(error));
}
1번, doSync 함수가 반드시 fetch 전에 호출되어야 하는게 아니라면 다음과 같이 then 메서드 안에 넣어주는게 좋다.
function requestData() {
return fetch()
.then(data => {
doSync();
console.log(data);
})
.catch(error => console.log(Error));
}
doSync에서 발생하는 예외는 catch 메서드에서 처리가 된다.
프로미스는 객체로 존재하지만, async await는 함수에 적용되는 개념이다.
async function getData(){
return 123; //Promise {<fulfilled>: 123}
}
getData().then(data => console.log(data)); // 123 Promise {<fulfilled>: undefined}
async function getData(){
return Promise.resolve(123);
}
getData().then(data => console.log(data));
프로미스의 then 메서드와 마찬가지로 async await 함수 내부에서 반환하는 값이 프로미스라면 그 객체를 그대로 반환한다.
async function getData(){
throw new Error('123');
}
getData().catch(error => console.log(error));
// Error : 123
function requestData(value){
return new Promise(resolve =>
setTimeout( () => {
console.log('requestData:', value);
resolve(value);
}, 100),
);
}
async function getData() {
const data1 = await requestData(10); //1번
const data2 = await requestData(20); //1번
console.log(data1, data2); // 2번
return [data1, data2];
}
getData();
// requestData: 10 // 3번
// requestData: 20 // 3번
// 10, 20 // 3번
1번, requestData 함수가 반환하는 프로미스가 처리됨 상태가 될 때 까지 2번,의 코드는 실행되지 않는다. 따라서 getData 함수를 호출 한 결과는 3번,이다.
async 키워드는 오직 async await 함수 내에서만 사용할 수 있다. 일반함수에서 사용하면 에러가 발생한다.
function getData(){
const data = await requestData(10); // 에러 발생
console.log(data);
}
function getDataPromise(){
asyncFunc1()
.then(data => {
console.log(data);
return asyncFunc2();
})
.then(data => {
console.log(data);
})
} // 1번 프로미스로 작성한 코드
async function getDataAsync() {
const data1 = await asyncFunc1();
console.log(data1);
const data2 = await asyncFunc2();
console.log(data2);
} // 2번 async await로 작성한 코드
가독성이 async await로 작성 한 코드가 좋다.
간결한 이유는 async await 함수는 then 메서드를 호출할 필요가 없기 때문이다.
function getDataPromise() {
return asyncFunc1()
.then(data1 => Promise.all([data1, asyncFunc2(data1)])) // 1번
.then([data1, data2]) => {
return asyncFunc3(data1,data2);
});
}
async function getDataAsync() { // 2번
const1 data1 = await asyncFunc1();
const2 data2 = await asyncFunc2(data1);
return asyncFunc3(data1, data2);
}
1번, 두 반환값을 asyncFunc3 함수에 전달하기 위해 Promise.all을 사용했다.
2번, async await함수는 복잡한 의존성이 존재함에도 코드가 직관적이다.
async function getData(){
const data1 = await asyncFunc1();
const data2 = await asyncFunc2();
// ...
}
두 함수사이에 의존성이 없다면, 동시에 실행하는게 더 좋다.
프로미스는 생성과 동시에 비동기 코드가 실행된다.
따라서, 두 개의 프로미스를 먼저 생성하고 await 키워드를 나중에 사용하면 병렬로 실행되는 코드가 된다.
async function getData(){
const p1 = asyncFunc1();
const p2 = asyncFunc2();
const data1 = await p1;
const data2 = await p2;
}
async function getData(){
const [data1, data2] = await Promise.all([asyncFunc1(), asyncFunc2()]);
// ....
}
async await 함수 내부에서 발생하는 예외는 try catch 문으로 처리하는게 좋다.
async function getData(){
try {
await doAsync();
return doSync();
} catch(error){
console.log(error);
}
}
비동기함수와 동기함수에서 발생하는 모든 예외가 catch문에서 처리된다.
Thenable은 프로미스 처럼 동작하는 객체다.
class ThenableExample {
then(resolve, reject){ // 1번
setTimeout(() => resolve(123), 1000);
}
}
async function asyncFunc() {
const result = await new ThenableExaple(); // 2번
console.log(result); // 123
}
1번 ThenableExample 클래스는 then 메서드를 가지고 있으므로, ThenableExample 클래스로 생성된 객체는 Thenable이다. 2번 async await 함수는 Thenable도 프로미스 처럼 처리한다.