동기적인 처리 방법
시작하기 전 먼저 다음의 단어들을 이해하고 넘어가자.
Client (클라이언트) | Server (서버) |
---|---|
번역하면 '서비스를 의뢰하는 사람' 이라는 기의를 가지고 있다. 이는 프로그래밍에서 "서버에게 데이터를 요구하는 것" 라는 뜻으로 쓰여진다. | 클라이언트에게 네트워크를 통해 서비스하는 컴퓨터를 의미한다. |
EX) 내 컴퓨터 | EX) 웹 서버, 게임 서버 |
클라이언트가 서버에서 데이터를 받아올 때 받아올 것이 4개로 가정한다면 동기적인 처리 방법은 다음과 같이 데이터를 처리할 것이다.
DATA | 일 순서 | 소요시간 (sec) |
---|---|---|
A | A -> B | 6 |
B | B-> C | 11 |
C | C -> D | 21 |
D | A->B->C->D | 29 |
🖥 위와 같이 A -> B -> C -> D 순으로 일을 처리한다.
따라서 총 소요시간 : 29초
비동기적인 처리 방법
위 예시와 조건 동일
DATA | 일 순서 | 소요시간 (sec) |
---|---|---|
A | A | 6 |
B | B | 5 |
C | C | 10 |
D | D | 8 |
모든 것을 동시에 처리하므로 총 소요시간은 가장 긴 시간인 C 가 총 소요시간이 된다.
따라서 총 소요시간 : 10초
유튜브 영상 로딩될 때 동기적이면 해당 영상이 로딩될 때까지 댓글창도 못쓰고 추천 영상도 못 보게 된다.
하지만 비동기적이면 유튜브 영상 하나가 로딩될 때 먼저 비동기적으로 처리 된 댓글창도 쓱 볼 수 있고 옆에 떠 있는 추천 영상도 확인 할 수도 있게 된다.
예를 들어 내가 A,B,C 함수를 순서대로 넣었고 이 순서대로 일 처리를 했으면 좋겠다고 생각했다. 근데 우리가 이전에서 배웠던 해당 함수의 시간복잡도나 서버에 데이터를 보낼 때 걸리는 시간 등 다양한 요소 때문에 우리가 원하는 순서대로 일 처리가 안되고 B,C,A 순으로 처리될 수도 있다.
CALLBACK 은 영문권에서 "야 너 그거 끝나면 나한테 다시 전화해" 라는 뜻으로, 실생활에서 많이 쓰는 단어
ex) Can you give me a call back?
각 함수 A,B,C 를
A() => {
B() => {
C() => {
}
}
}
이런식으로 콜백으로 넣게 되면
A를 실행해서 B 를 실행하고 B 를 실행한다음에 C를 실행하게끔 우리가 명시적으로 짜줬기 때문에 우리가 원하던 A->B->C 순으로 일을 처리할 수 있게 된다!
만약 함수가 A,B,C 뿐만 아니라 한 50개 정도를 순서대로 처리해야 한다고 가정하면
그 코드의 가독성은 매우 떨어질 것이다.
(영문권에서는 이것을 callback Hell 이라고 표현하나보다)
따라서 우리는 아래의 개념을 배워야 한다.
프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서 미래의 어떤 시점에 결과를 제공합니다.
Promise 는 다음 중 하나의 상태를 가지게 된다.
대기(pending): 이행하거나 거부되지 않은 초기 상태.
이행(fulfilled): 연산이 성공적으로 완료됨.
거부(rejected): 연산이 실패함.
Promise 라는 인스턴스는 아래의 형태로 이루어져있다.
new Promise((resolve, reject) => {})
주어진 값으로 이행하는 Promise 객체를 반환한다.
그렇지 않은 경우
프로미스인지 알 수 없는 경우
Promise.resolve(value)
후 반환값을 프로미스로 처리할 수 있다.주어진 이유로 거부하는 Promise 객체를 반환한다.
then() 메서드는 Promise를 리턴하고 두 개의 콜백 함수를 인수로 받습니다. 하나는 Promise가 이행했을 때, 다른 하나는 거부했을 때를 위한 콜백 함수입니다.
// resolve 실행 됐을때!
const promise1 = new Promise((resolve, reject) => {
resolve('Success!');
});
promise1.then((value) => {
console.log(value); // expected output: "Success!"
});
// reject 실행 됐을 때!
const promise1 = new Promise((resolve, reject) => {
reject('Failed!');
});
promise1.then((value) => {
console.log(value);
}, (reason) => {
console.log(reason) // Failed!
});
catch() 메서드는 Promise 를 리턴하고 rejected 된 값 만을 출력합니다.
프로미스 에러 처리는 .then()
와 .catch()
메서드가 있다.
function getData() {
return new Promise(function(resolve, reject) {
reject('failed');
});
}
// 1. then()의 두 번째 인자로 에러를 처리하는 코드
getData().then(function() {
// ...
}, function(err) {
console.log(err);
});
// 2. catch()로 에러를 처리하는 코드
getData().then().catch(function(err) {
console.log(err);
});
하지만 프로미스 에러 처리는 가급적 .catch()
를 사용하는 것을 권장하는데, 그 이유는 다음 코드를 보면 알 수 있다.
function getData() {
return new Promise(function(resolve, reject) {
resolve('hi');
});
}
getData().then(function(result) {
console.log(result);
throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
console.log('then error : ', err);
});
위와 같이 첫번째 콜백 함수 내부에서 에러가 나는 경우 제대로 처리하지 못한다.
다음은 catch()
를 사용했을 때이다.
// catch()로 오류를 감지하는 코드
function getData() {
return new Promise(function(resolve, reject) {
resolve('hi');
});
}
getData().then(function(result) {
console.log(result); // hi
throw new Error("Error in then()");
}).catch(function(err) {
console.log('then error : ', err); // then error : Error: Error in then()
});
위와 다르게 발생한 에러를 잘 출력한 모습이다.
Promise.all() 메서드는 순회 가능한 객체에 주어진 모든 프로미스가 이행한 후, 혹은 프로미스가 주어지지 않았을 때 이행하는 Promise를 반환합니다. 주어진 프로미스 중 하나가 거부하는 경우, 첫 번째로 거절한 프로미스의 이유를 사용해 자신도 거부합니다.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
출처 : MDN
// nested Promise
// Callback Hell 이 싫어서 Promise 를 도입했더니 이런 결과가...!!! 😱
A()
.then(data => {
console.log(data)
B()
.then(data => {
console.log(data)
C()
.then(data => {
console.log(data)
D()
.then(data => {
console.log(data)
})
})
})
})
위의 해결방법은 다음과 같다.
일의 순서를 확실하게 하기 위해 스코프를 확실하게 정한다.
다음 함수를 return 하게 한다.
// Promise Chain
A()
.then(data => {
console.log(data)
return B()
})
.then(data => {
console.log(data)
return C()
})
.then(data => {
console.log(data)
return D()
})
.then(data => {
console.log(data)
})
현재 실행되고 있는 문맥 다음에 .then()
메서드가 존재한다면 return
을 통해 현재 값을 다음 .then()
에 넘겨줄 수 있다.
이것을 Promise Chain 이라고 한다.
하지만 이러한 것만 보고서 무조건 "Promise Chain 이 옳다!!" 라고도 할 수 없다.
가장 적절한 방법은 상황에 따라 nested Promise 와 Promise Chain 을 적절히 섞어가며 사용하는 것이라고 할 수 있다.
async & await 라는 특별한 문법을 사용하면 Promise 를 좀 더 편하게 사용할 수 있다. async & await 은 굉장히 이해하기 쉽고, 사용법도 어렵지 않다.
async 를 사용하려면 무조건 function 앞에 위치해야 한다.
async function A() {
return 'hi';
}
해당 함수는 언제나 Promise 를 반환한다. (Promise 가 아니더라도)
함수 안에서 await 사용이 가능하다.
promise.then, promise.catch 가 거의 필요 없게 된다. (무조건 안써도 된다는건 아님)
자바스크립트는 await 키워드를 만나면 해당 Promise 가 처리될 때까지 기다린다.
async function A () {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("2초 뒤 뿅!"), 2000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다린다. 즉, 2초간 기다림
alert(result); // "2초 뒤 뿅!"
}
A();