javascript는 기본적으로 싱글 스레드 엔진을 기반으로 실행되기 때문에 기본적으로 동기 처리되어 작성한 순서대로 수행된다. 하지만 소요가 큰 작업이 수행되면 어떨까? 선행 코드가 수행되기까지 기다려야한다. 선행 여부와 관계없이 수행할 수 있는 기능이 바로 비동기처리이다.
javascript에서 코드를 실행시킬때 방식은 2가지로 분류할 수있다. 동기처리 방식과 비동기처리 방식이다. 다음 그림이 동기와 비동기의 차이를 명확하게 해준다.
쉽게 말해 실행 시점의 차이가 동기/비동기의 차이다.
동기 처리는 선행 작업이 완료되어야 뒤에 작업이 가능한 실행방식이고
비동기 처리는 선행 작업 완료 여부와 상관 없이 실행되는 방식인 것이다.
// 동기 처리 방식
console.log('1');
console.log('2');
console.log('3');
// 1
// 2
// 3
// 비동기 처리 방식
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// 1
// 3
// 2
위의 예제를 보면 동기 처리방식은 순차적으로 잘 수행했다.
비동기 처리의 경우 console.log('2');
를 출력하지 않았는데 console.log('3');
이 수행되었다. 이때 비동기로 수행했다고 말할 수 있는 것이다. 이는 setTimeout
의 경우 비동기 함수이기 때문이다. setTimeout
은 WebAPI 요소기 때문에 stack에 들어가면 바로 수행되지 않고 Callback Queue에 쌓이게 된다. 그렇기때문에 순서를 무시하고 console.log('3');
이 출력된 것이다.
다른 함수의 인자로써 넘겨진 후 특정 이벤트에 의해 호출되는 함수. 이 때 함수는 포인터나 람다식 등으로 전달된다.
쉽게 말해 어떤 작업 '기능1' 이 끝난 이후에 수행되야되는 '기능2' 가 있다 치자.
앞서 말했듯 javascript는 동기 처리하게 되어있다. 하지만 중간에 비동기 함수를 사용하는 경우나 안에 내부 속성만 바꾸는 경우 순서가 꼬일 수 있다. 그럴때 callback함수를 사용하면 순서를 제어할 수 있다.
사용 방법은 간단하다. 우리가 함수를 호출할 때 파라미터를 넘겨주듯 함수를 파라미터로 집어넣으면 된다.
const fn1 = (callback) => {
let a = 0;
a++;
callback(a);
};
fn1((n) => {
console.log(n);
});
// 1
const fn2 = (n) => {
console.log(n + 1);
}
fn1(fn2);
// 2
다음 예시와 같이 함수를 호출할때 람다식으로 선언해서 집어넣는 방법과 객체로 할당해서 집어넣는 방법이 있다. 코드를 보면 간단하다. 선행되야할 코드를 수행하고 해당 callback 함수를 호출하면 된다.
순차적으로 실행해야할 프로세스가 늘어날 수록 코드 가독성이 떨어지는 현상
이렇게 callback함수를 사용했을 때 callback 지옥에 빠질 수 있다. 무슨 말이냐면
절차적으로 수행되어야할 작업이 있어서 callback을 사용했는데 그 callback에 또 callback 그 안에 또 callback.. 이런 구조를 callback 지옥에 빠졌다고 표현한다.
다음 코드를 보면 이해가 쉬울 것이다.
const fn1 = (n, callback) => {
n++;
console.log(n);
callback(n);
};
fn1(0, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
console.log('끝!');
});
});
})
})
});
// 1
// 2
// 3
// 4
// 5
// 끝!
이런식으로 callback의 callback의 callback.. 구조를 띄게되면 다음과 같이 가독성이 떨어지는 현상을 말한다. 이를 개선하고 기능상의 편의를 위해 ES6부터 추가된 개념이 바로 Promise다.
생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리기를 연결할 수 있다.
하여튼 공식 문서는 항상 어렵게 말을 한다. 그냥 callback하고 똑같은 놈인데 기능상 편의, 가독성을 보완해서 나온 모델이라고 생각하면 된다. 으으..
프로미스의 처리 과정은 다음과 같다.
new Promise()
로 프로미스를 생성한 상태다.
프로미스 생성시 파라미터는 resolve
와 reject
다.
resolve
는 이행 함수가 들어가고 reject
는 실패했을때 수행할 함수다.
const fn1 = () => {
return new Promise((resolve, reject) => {
...
});
}
resolve
에 해당하는 영역으로 정상 수행됐을 때 호출한다.
const fn1 = () => {
return new Promise((resolve, reject) => {
resolve(2);
});
}
// resolve() 결과 값 then에 매핑
fn1().then((number) => {
console.log(number); // 2
}).catch();
reject
에 해당하는 영역으로 오류 상황인 경우 호출한다.
const fn1 = () => {
return new Promise((resolve, reject) => {
reject(new Error("error"));
});
}
// resolve() 결과 값 then에 매핑
fn1().then().catch((err) => {
console.log(err); // Error: error
});
한마디로 프로미스를 반환하는 함수를 호출했을때 resolve
는 .then()
에 매핑되고
reject
는 .catch
에 매핑된다. 유가릿?
// callback
const fn1 = (n, callback) => {
n++;
console.log(n);
callback(n);
};
fn1(0, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
fn1(n, (n) => {
console.log('끝!');
});
});
})
})
});
// 1
// 2
// 3
// 4
// 5
// 끝!
// ES6 Promise
new Promise((resolve, reject) => {
let n = 0;
n++;
console.log(n);
resolve(n++);
}).then((n) => {
console.log(n);
return n++;
}).then((n) => {
console.log(n);
return n++;
}).then((n) => {
console.log(n);
return n++;
}).then((n) => {
console.log(n);
return n++;
}).then((n) => {
console.log('끝!');
}).catch();
// 1
// 2
// 3
// 4
// 5
// 끝!
적어도 callback 사용할때 처럼 옆으로 늘어나는 일은 없다. 개념상 callback이나 Promise나 같은 놈이라고 보면 된다. 쉽게 가자 그냥
ES8(ES2017) 에서 추가된 기능으로 기존에 사용하던 callback과 Promise의 단점을 보완하고 간편하게 작성할 수 있게 해주는 키워드다.
한마디로 'callback이나 Promise의 문구가 너무 길고 장황하니 간단하게 만들게 해줄게' 이얘기다. 무슨말인고.. 그냥 이전 callback과 Promise의 사용 코드를 보자
HTTP 통신으로 서버에서 User 정보를 받아와 출력해야되는 함수가 있다 가정하자.
여기서 getUser
함수가 서버에서 User정보를 받아오는 함수다. 당연히 HTTP 송신이 완료된 다음 수행되어야 되기 때문에 다음과 같이 구현해야됐을 것이다.
// callback
const getUser = (callback) => {
let user = null;
... // HTTP 통신 어쩌구 저쩌구
callback(user);
}
const fn1 = () => {
getUser((user) {
console.log(user.name);
});
}
// ES6 Promise
const getUser = () => {
return new Promise((resolve, reject) => {
let user = null;
... // HTTP 통신 어쩌구 저쩌구
resolve(user);
});
}
const fn1 = () => {
getUser().then((user) => {
console.log(user.name);
});
}
자 다음 async-await을 보자
const getUser = () => {
return new Promise(function(resolve, reject) {
let user = null;
... // HTTP 통신 어쩌구 저쩌구
resolve(user)
});
}
const fn1 = async () => {
const user = await getUser();
console.log(user.name);
}
???? 그냥 그래보이는데?..
일단 간지나니깐 쓰자.
함수의 앞에 async
를 붙인다. 그리고 그 함수 내부에서 callback처럼 수행되어야 하는 코드 앞에 await
를 붙인다. await
이 붙는 함수는 반드시 프로미스 객체를 반환해야 정상적으로 작동한다.
// 일반 함수 선언식
async function fn1() {
// async, await는 try catch로 오류처리한다.
try {
await fn2(); // fn2는 반드시 Promise를 return 해야한다.
} catch (e) {
// error 처리
}
}
// ES6 화살표 함수 선언식
const fn1 = async () => {
// async, await는 try catch로 오류처리한다.
try {
await fn2(); // fn2는 반드시 Promise를 return 해야한다.
} catch (e) {
// error 처리
}
}
오늘은 javascript의 동기/비동기 처리 방식의 개념, callback / Promise / async & await 기능에 대해 알아봤다. 비동기 처리되는 코드에 있어서 반드시 사용해야할 매커니즘이니 알아두도록 하자.
오늘 저녁은 순대국이다. 🥕
참조 : https://joshua1988.github.io/web-development/javascript/promise-for-beginners/
https://jacobko.info/javascript/ES6_11/
https://lunuloopp.tistory.com/entry/%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%9D%98-%EC%9D%B4%ED%95%B4-callback%ED%95%A8%EC%88%98-promise-async-await