치춘짱베리굿나이스
비동기 처리와 Promise 본문
비동기 처리와 Promise
동기 (Synchronous) 와 비동기 (Asynchronous)
const [testValue, setTestValue] = useState(0);
const handleOnClick = () => {
setTestValue(testValue + 1);
setTestValue(testValue + 1);
setTestValue(testValue + 1);
setTestValue(testValue + 1);
setTestValue(testValue + 1);
};
...
<div>{`${testValue}`}</div>
<div onClick={handleOnClick}>add 1</div>
앞에서 prevState
에 관해 짧게 정리할 때 슬쩍 본 코드이다
testValue
가 한번에 5씩 증가하지 않는 이유가 ‘비동기’ 적인 작동방식 때문이라고 했었는데, 상태값이야 prevState
로 이전 값을 끌어다 씀으로써 보정할 수 있다지만, 다른 값들은 비슷한 이슈를 어떻게 처리할까?
짭짤감튀가 먹고 싶어서 맥도날드에 갔다고 생각해 보자 (내가 먹고싶은 건 아니고)
동기식 처리
void Mcdonald(void) {
firstPerson();
secondPerson();
thirdPerson();
fourthPerson();
me();
}
...
first person got his hamburger set.
second person got her mcnugget.
third person got his bigmac set.
fourth person got her americano.
I finally got french fries.
동기식 처리의 경우에는 카운터가 하나밖에 없고, 나는 짭짤감튀를 먹기 위해서 앞의 모든 사람들의 결제 및 햄버거 수령이 끝날 때까지 기다려야 한다
내가 언제 짭짤감튀를 받아갈 수 있는지 그 순서는 명확하지만, 앞의 사람들의 처리가 느려지면 느려질 수록 나는 오늘 내로 짭짤감튀를 먹을 수 없을 지도 모른다
이처럼 동기식 처리는
- 동시에 여러 작업을 (병렬로) 수행할 수 없다
- 어느 함수 (코드) 가 언제 실행되는지, 그 순서가 알기 쉽다
비동기식 처리
const McDonald(void) {
firstPerson();
setTimeout(() => { // 대표적인 비동기 함수
secondPerson();
thirdPerson();
}, 0);
fourthPerson();
me();
}
...
first person got his hamburger set.
fourth person got her americano.
I finally got french fries.
second person got her mcnugget.
third person got his bigmac set.
반면, 비동기식 처리는 맥도날드에 키오스크가 여러 대 있어서, 앞의 사람들이 결제를 끝냈는지 여부와 상관 없이 중간에 짭짤감튀를 먹을 수 있다
내가 언제 짭짤감튀를 받아갈 수 있는지 예측할 수 없다는 단점이 있지만, 키오스크 덕분에 여러 작업이 한번에 수행되어 처리 속도가 빨라진다
따라서 비동기식 처리는
- 동시에 여러 작업을 병렬로 수행할 수 있다
- 다만 어떤 함수 (코드) 가 언제 실행되는지 명확히 예측하기 어렵다
위의 예제에서 setTimeout()
은 대표적인 비동기 API이며, 동기적 코드인 firstPerson()
, fourthPerson()
, me()
가 먼저 실행된 후 비동기 API 내의 내용들이 실행된다
const McDonald(void) {
setTimeout(firstPerson, 2000); // 2000ms
setTimeout(secondPerson, 1000); // 1000ms
setTimeout(thirdPerson, 1500); // 1500ms
setTimeout(fourthPerson, 500); // 500ms
setTimeout(me, 1000); // 1000ms
console.log('Everyone arrived at the Kiosk.');
}
...
Everyone arrived at the Kiosk.
fourth person got her americano.
second person got her mcnugget.
I finally got french fries.
third person got his bigmac set.
first person got his hamburger set.
모든 코드를 비동기 함수 setTimeout()
에 넣고, 각자 계산 후 음식을 받기까지 걸리는 시간을 설정해 주자
- 비동기 함수
setTimeout()
이 순서대로 실행되면서, 동작을 예약하고 종료된다 - 모든
setTimeout()
이 동작 예약을 완료하면,console.log
가 실행된다 - 예약했던 동작들이 병렬적으로 실행된다. 이때 대기 시간을 다르게 조정했으므로 각
setTimeout()
은 다른 동작에 영향을 미치거나 영향을 받지 않고 독립적으로 해당 시간만큼 대기한다 ⇒ 대기 시간이 짧은 동작 먼저 수행된다
동작 예약?
이때 ‘동작을 예약한다' 라고 표현했는데, 각 함수가 실행될 때마다 코드가 Call Stack
에 push되고, 스택에 푸시되었던 함수가 setTimeout()
을 지원하는 API에 요청을 보내면서 pop된다
console.log
는 Call Stack
에 등록되지마자 콘솔에 내용을 출력하고 동작이 끝나기 때문에 바로 pop되지만, setTimeout()
은 별도의 API에 동작을 예약한 후 해당 API가 병렬로 동작을 수행한다
그렇다는 것은 자바스크립트 / 타입스크립트의 모든 함수가 비동기식인 것이 아니라, 몇몇 함수들이 실행 환경 (node.js
, 브라우저) 의 도움을 받아 비동기식으로 동작하는 것 뿐이다
주로 사용되는 대표적인 비동기 함수는 서버에서 데이터를 받아오는 fetching 함수 (fetch
, axios
) 와 파일을 읽어들이는 함수가 있다
콜백 함수
const foo = () => {
console.log('hello?');
}
const bar = (str, callback) => {
console.log(str);
callback();
}
bar('byebye', foo);
const handleButtonClick = () => {
console.log('clicked!');
}
...
<button onClick={handleButtonClick}>클릭</button>
다른 함수의 인자로 받아지는 함수와, 어떠한 이벤트에 의해 호출되어지는 함수를 콜백 함수라 한다
예시에서 bar
은 callback
함수를 인자로 받아와 내부에서 실행하므로, foo
는 콜백 함수라고 할 수 있다
또한 handleButtonClick
은 ‘클릭 이벤트' 가 발생했을 때 호출되므로, handleButtonClick
도 콜백 함수라고 할 수 있다
쉽게 말하면 코드에서 바로 호출되는 함수가 아니라, 어딘가에 함수를 등록해 놓고 특정 이벤트나 시점에 실행되는 함수가 콜백 함수인 것이다
말이 어려워서 그렇지 콜백 함수 자체는 어렵지 않다
비동기에서의 콜백 지옥
foo(() => {
bar(() => {
aaa(() => {
bbb(() => {
ccc(() => {
ddd(() => {
eee(() => {
fff(() => {
run('hello');
}).bind(this);
}).bind(this);
}).bind(this);
}).bind(this);
}).bind(this);
}).bind(this);
}).bind(this);
}).bind(this);
콜백 함수에 대해 알아본 이유는 콜백 지옥 때문이었다
비동기 함수들을 순차적으로 실행시키고 싶을 때, 보통은 어떠한 작업이 끝나고 나야 함수가 동작하도록 콜백 함수로 등록하여 사용하는데, 이 방식을 사용하면 작업량이 많아질 수록 콜백을 잔뜩 중첩해야 하기 때문에 (특정 작업이 끝났을 때 다른 작업을 수행하기 위해서) 콜백 지옥에 빠져버린다
이 콜백 지옥을 타파하기 위해 나온 것이 Promise
이다
Promise
비동기 작업을 간단하게 처리하도록 도와주는 객체이다
쉽게 말하면, 비동기 작업을 수행한 후 작업 성공 시와 실패 시에 전달 값을 다르게 하여 서로 다른 작업을 가능케 한다
순차적으로 수행되지 않는 비동기 함수의 실행 순서를 사용자가 제어할 수 있도록 도와주고, 콜백 지옥에 비해 가독성 또한 뛰어나다
Promise 객체를 반환하는 대표적인 라이브러리로 Axios, Fetch가 있다
Promise로 비동기 작업 관리하기
const promise = new Promise((resolve, reject) => {
// 수행할 비동기 작업
});
/*
(resolve, reject) => {} 함수가 executor이다
*/
Promise
객체는 위와 같이 생성할 수 있다
Promise()
클래스를 통해 객체를 만들며, 인자로 함수를 전달받는다- 이 함수는
executor
라고 불리며, 인자 2개를 받는다 - 첫 번째 인자는
resolve
로,executor
내에서 호출할 수 있는 또 하나의 함수이다.resolve
는 비동기 작업이 성공했을 때 호출한다 - 두 번째 인자는
reject
로,executor
내에서 호출할 수 있는 또 하나의 함수이다.reject
는 비동기 작업이 실패했을 때 호출한다
Promise
객체는 new Promise()
코드에 도달하자마자 내부에 선언된 비동기 작업을 시작한다
이 비동기 작업이 언제 끝날지는 경찰도 모르기 때문에 일단 실행부터 시켜놓고, 성공했을 때 (resolve
가 호출되었을 때)와 실패했을 때 (reject
가 호출되었을 때) then
과 catch
메소드를 통해 각각 다른 처리를 수행할 수 있도록 미리 지정해놓는 것이 일반적인 사용밥이다
then-catch
를 어디선가 많이 봤다 했는데 Axios
에서도 데이터를 받아올 때 then-catch
메소드으로 에러 처리를 한다
그 이유야 당연히 Axios
도 Promise
객체를 반환하기 때문이지
Promise로 후속 동작 정의
const promise = new Promise((resolve, reject) => {
// 수행할 비동기 작업
});
promise
.then(() => {
console.log('성공했대요');
})
.catch((e) => {
console.log(e);
});
이렇게 promise
객체의 then
, catch
메소드를 이용하면 비동기 작업의 결과에 따른 후속 작업을 정의할 수 있다
성공하면 ‘성공했대요' 라는 문구가 콘솔에 출력될 것이고, 실패하면... 에러 메시지 (e
) 가 출력될 것이다
const promise = new Promise((resolve, reject) => {
resolve();
});
Promise
선언 시에 내부에서 아예 resolve()
함수를 실행하도록 선언해 버리면, 실패 없이 무조건 성공으로 간주한다
따라서 then
으로 지정한 동작이 실행된다
const promise = new Promise((resolve, reject) => {
reject();
});
반대로 Promise
선언 시에 내부에서 reject()
함수를 실행하도록 선언해 버리면, 무조건 실패로 간주한다
따라서 catch
로 지정한 동작이 실행된다
결론은 성공 케이스와 실패 케이스를 분리하고, 성공할 경우 resolve()
, 실패할 경우 reject()
를 실행할 수 있도록 executor
를 구성하면 된다
Promise 재사용으로 같은 작업 반복하기
const promiseFunc = (boolVar) => {
return new Promise((resolve, reject) => {
if (boolVar) resolve();
else reject();
});
}
new Promise()
부분을 아예 별개의 함수로 빼 보자
언제 resolve
가 호출되고 언제 reject
가 호출되는지도 정의하였다
const promise1 = promiseFunc(true);
promise1
.then(() => {
console.log('성공!');
})
.catch(() => {
console.log('실패!');
});
// 성공!
이렇게 만들면 promiseFunc
를 호출할 때 비동기 작업이 시작되므로, 함수의 동작 시점을 조절할 수 있다
Axios
나 Fetch
의 반환값을 변수에 넣어 사용하는 것도 이러한 맥락이라 할 수 있겠다
작업 결과 전달하기
const promiseFunc = (boolVar) => {
return new Promise((resolve, reject) => {
if (boolVar) resolve('great');
else reject('error');
});
}
const promise1 = promiseFunc(true);
promise1
.then(() => {
console.log('성공!');
})
.catch(() => {
console.log('실패!');
});
// great!
// 성공!
성공했을 때와 실패했을 때 문구를 출력하기 위해 값을 인자로 전달해줄 수 있다
그 외
const throwError = (boolVar) => {
return new Promise((resolve, reject) => {
throw Error('error!');
});
}
const promise1 = promiseFunc(true);
promise1
.then(() => {
console.log('성공!');
})
.catch(() => {
console.log('실패!');
});
// 실패!
executor
내에서 에러 발생 시 (에러가 throw
될 시) 자동으로 reject()
가 수행된다
const promiseFunc = (boolVar) => {
return new Promise((resolve, reject) => {
resolve();
reject();
});
}
const promise1 = promiseFunc(true);
promise1
.then(() => {
console.log('성공!');
})
.catch(() => {
console.log('실패!');
});
// 성공!
executor 내에서 resolve와 reject가 순서대로 적혀 있다면, 첫 번째 함수만 유효하다
위의 예시에선 resolve()
가 reject()
보다 먼저 호출되므로, 동작 성공으로 판단하여 항상 then으로 넘어간다
const promiseFunc = (boolVar) => {
return new Promise((resolve, reject) => {
resolve();
return (true);
});
}
const promise1 = promiseFunc(true);
promise1
.then(() => {
console.log('성공!');
})
.catch(() => {
console.log('실패!');
});
// 성공!
executor
의 반환값은 무시된다
결론
Promise
는 약속한 동작이 성공했을 때 fullfilled로 판단하여 then
에 등록한 동작을 수행하고, 실패했을 때 rejected로 판단하여 catch
에 등록한 동작을 수행한다
그리고 executor에 등록한 비동기 동작이 아직 수행 대기 중일 땐 pending 상태로 판단한다
이처럼 비동기 작업을 수행하고 후속 작업을 등록할 때 비교적 간결하게 설정할 수 있어 콜백 지옥에서도 벗어나고 유연한 설계가 가능하다
이 Promise
와 관련된 문법으로 async
/ await
가 있는데, 정리글이 너무 길어져 다른 포스팅에서 다뤄보도록 할 것
후.... C만 주구장창 파다가 자바스크립트 본격적으로 파보려 하니 비동기가 의외로 발목을 잡는다
예상치 못한 동작이 심심찮게 일어나서 항상 내 머리를 아프게 만든다
참고자료
JavaScript 비동기 핵심 Event Loop 정리
콜백 함수(Callback)의 정확한 의미는 무엇일까?
[Javascript] 비동기, Promise, async, await 확실하게 이해하기
[javascript]비동기처리와 Promise/axios/fetch
'Javascript + Typescript > 이론과 문법' 카테고리의 다른 글
require, import, export (0) | 2022.07.25 |
---|---|
Throttle & Debounce (0) | 2022.05.18 |
[Typescript] Type vs Interface (0) | 2022.05.09 |
spread, rest (0) | 2022.04.12 |
비구조화 할당 (구조분해 할당) (0) | 2022.04.12 |