티스토리 뷰
동기와 비동기
커피숍에서 커피를 시킬 때, 한명이 주문하고 그 주문이 완료되면, 다음 사람이 주문하고 그 주문을 완료하고, 그리고 또 다음 사람이 주문하고 완료시킨다면 대기 줄이 굉장히 길 것이다. 전체 주문을 완수하기에도 굉장히 오래 걸릴 것이다. 이것이 동기(synchronous)이다. 이 때 이전 주문이 진행되고 있기 때문에 다음 주문을 막는 것은 블로킹(blocking)이라고 한다. 하나의 작업이 완료될 때까지 다음 작업을 막는 것이다.
현실의 커피숍에서는 이렇게 진행되지 않는다. 주문을 받고, 주문이 들어가고, 그 음료를 제조하는 동안 또 다른 주문을 받고, 그 주문을 처리하는 방식이다. 블로킹되지 않고 비동기적으로 진행된다.
웹 개발에서 자바스크립트의 비동기적 실행은 유용하다. 다음과 같은 작업이 동기적으로 실행된다면 웹페이지를 보기까지 로딩이 오랜 시간이 걸릴 것이다. 다음의 작업은 비동기적으로 실행된다.
- 백그라운드 실행, 로딩 창 등의 작업
- 인터넷에서 서버로 요청을 보내고, 응답을 기다리는 작업
- 큰 용량의 파일을 로딩하는 작업
Node.js를 만든 개발자도 이러한 합리적인 방식을 채택했다. Node.js는 비동기적으로 작동하고 논블로킹한 런타임이다.
자바스크립트는 동기? 비동기?
자바스크립트는 싱글스레드 기반으로 동작하는 언어이다. 처리할 수 있는 길이 실 한가닥밖에 없기 때문에 한 번에 한개의 작업만 할 수 있다. 따라서 동기적으로 작동한다. 그러나 자바스크립트에서 비동기적인 작업이 가능했다. 왜일까?
그 이유는 자바스크립트가 작동하는 환경(런타임)이 비동기 처리를 도와주기 때문이다. 런타임의 도움덕분에 우리가 자바스크립트에 특별한 작업을 해주지 않고도 비동기 처리를 할 수 있다.
자바스크립트 내장 비동기 함수
타이머 API
타이머 API는 브라우저에서 제공하는 Web API이다. 그리고 비동기로 작동하도록 구현되어 있다. 자바스크립트 내장 비동기 함수이다.
setTimeout(callback, millisecond)=>timerId
const tomato = setTimeout(function () {
console.log('2초가 지났습니다.')
}, 2000) // 2초 후에 console.log가 실행된다.
clearTimeout(timerId)
const tomato = setTimeout(function () {
console.log('2초가 지났습니다.')
}, 2000)
clearTimeout(tomato); // setTimeout함수가 바로 종료되어 console.log가 실행되지 않는다
setInterval(callback, millisecond)=>timerId, clearInterval(timerId) 은 millisecond간격으로 콜백을 반복적으로 실행하는 것이다. 사용방법은 setTimeout과 같다.
코드의 실행순서는 내가 정한다. 비동기를 제어하기
우리가 작성한 코드가 언제 어떻게 실행될 지 알고 있어야 한다. 그리고 원하는대로 실행되도록 제어할 수 있어야 한다. 함수가 완료가 되었든 안되었든 신경쓰지 않고 다음 함수를 실행해버리는 비동기 코드를 어떻게 제어할 수 있을까? 비동기로 작동하는 코드를 동기화하여 함수들의 실행순서를 제어할 수 있다. 비동기를 동기화하는 방법은 Callback, Promise, async/await 세가지가 있다.
Callback
콜백함수는 다른 함수의 전달인자로 넘겨주는 함수이다. 콜백함수를 넘겨받은 고차함수는 적절한 시점에 콜백함수를 실행할 것이다.
(혼자 공부할 때에는 동기, 비동기를 구분하기가 정말 힘들었다. 코어자바스크립트에 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeout), 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListener), 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등(XMLHttpRequest), 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드라고 나와있다. 하지만 이러한 비동기적이라고 하는 코드들이 내가 설정한대로 시간이 지나거나 이벤트가 발생하면 그 후에 내가 명령한 수행을 해서 이걸 왜 비동기라고 하는지 이해가 안됐다. 사실은 내가 콜백함수를 통해 이미 비동기적 코드를 동기화 한 것이었다.)
let count = 0;
const callbackFunc = function () {
console.log(count);
if (++count > 4) clearInterval(timer);
};
const timer = setInterval(callbackFunc, 300);
// 실행결과
// 0 (0.3초)
// 1 (0.6초)
// 2 (0.9초)
// 3 (1.2초)
// 4 (1.5초)
Callback Hell
const printString = (string, callback) => {
setTimeout(function () {
console.log(string);
callback();
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {
printString('D', () => {
printString('E', () => {
printString('F', () => {
printString('G', () => {
printString('H', () => {
printString('I', () => {
printString('J', () => {
printString('K', () => {
printString('L', () => {
printString('M', () => {
printString('N', () => {
printString('O', () => {
printString('P', () => {});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
};
printAll();
wow. 0.1초 간격으로 A부터 P까지의 알파벳들이 출력된다. 코드가 굉장히 길고 인덴트가 많아 복잡하고 가독성이 안좋아진다. 이렇게 콜백함수에 콜백함수가 꼬리에 꼬리를 무는 것을 콜백지옥이라고 한다. 콜백지옥을 보완하기 위해 Promise가 등장했다.
Promise
Promise, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'이다.
const promise = new Promise(executor)
Promise는 클래스이다. 프로미스는 클래스이기 때문에 new연산자로 프로미스 객체를 생성한다. 이 프로미스 생성자는 비동기 처리를 수행할 콜백함수 executor를 인자로 받는다. 그리고 콜백함수 executor는 resolve함수와 reject함수를 인자로 전달받는다. resolve함수, reject함수는 다음과 같이 사용한다.
const executor = (resolve, reject) => {
if(/*코드가 정상적으로 실행이 되면*/) {
resolve(value); // value: 반환하고 싶은 값
}
else { // 비동기 처리 실패하면
reject(errorMessage); // errorMessage: 전달하고 싶은 에러메세지
}
}
프로미스 객체가 생성되면 자동으로 executor가 실행된다. excutor는 비동기 처리가 정상적으로 완료되면 resolve 함수를 호출하고, 도중에 에러가 발생하면 reject 함수를 호출한다.
이 프로미스 객체는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태(state)와 처리결과(result) 정보를 갖는다. 프로미스의 상태는 resolve 함수 또는 reject 함수를 호출하는 것으로 결정된다.
프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled | 비동기 처리가 수행된 상태(성공) | resolve 함수 호출 |
rejected | 비동기 처리가 수행된 상태(실패) | reject 함수 호출 |
다음과 같이 개발자 도구에서 프로미스를 만들어 보았다.
즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체이다.
then, catch, finally
then 메서드는 두 개의 콜백함수를 인수로 전달받는다. 첫번째 콜백함수는 비동기 처리가 성공했을 때 호출되는 성공 처리 콜백함수이고, 두 번째 콜백 함수는 비동기 처리가 실패했을 때 호출되는 실패 처리 콜백함수이다. 이 콜백함수들은 각각 프로미스 객체 안의 result 값 하나, 하나의 fulfillment value 또는 rejection errorMessage를 인수로 갖는다.
new Promise(resolve => resolve('성공'))
.then(v => console.log(v)); // 성공
new Promise((_, reject) => reject(new Error('실패')))
.then(_, e => console.error(e)); // Error: 실패
catch메서드는 한 개의 콜백함수를 인수로 전달받는다. 이 콜백함수는 프로미스가 rejected상태인 경우만 호출된다. 또한 catch 메서드는 then의 두 번째 인자에 콜백함수를 넣었을 때와 동일하게 동작한다. 그러나 한 가지 문제점은, then 메서드를 이용한 에러 처리는 then의 첫 번째 콜백함수의 에러는 캐치하지 못하고 가독성이 안좋다. 때문에 catch 메서드를 이용해서 에러 처리를 하는 것이 권장된다.
new Promise((_, reject) => reject(new Error('실패')))
.catch(e => console.error(e)); // Error: 실패
finally 메서드는 한 개의 콜백함수를 인수로 전달받는다. finally 메서드의 콜백함수는 프로미스의 성공(fulfilled) 또는 실패(rejected)와 상관없이 무조건 한 번 호출된다. finally 메서드는 프로미스의 상태와 상관없이 공통적으로 수행해야 하는 작업이 있을 때 유용하다.
let promise = new Promise(function (resolve, reject) {
resolve('성공');
// reject("실패");
});
promise
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.catch((error) => {
console.log(error);
return '실패';
})
.finally(() => {
console.log('성공이든 실패든 작동!');
});
then, catch, finally 메서드 모두 프로미스를 반환하기 때문에 프로미스 체이닝이 가능하다. 프로미스 체이닝을 통해 비동기 작업들을 순차적으로 실행할 수 있다. 하지만 프로미스 체이닝 또한 콜백 패턴이고 코드가 길어지고 가독성이 좋지 않다.
ES8에서 도입된 async/await을 통해 프로미스의 후속 처리 메서드 없이 프로미스가 처리 결과를 반환하도록 구현할 수 있다.
Promise.all()
Primise.all()은 여러 개의 비동기 코드를 병렬로 동시에 실행시킬 수 있다.
const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));
// 기존
const result = [];
promiseOne()
.then(value => {
result.push(value);
return promiseTwo();
})
.then(value => {
result.push(value);
return promiseThree();
})
.then(value => {
result.push(value);
console.log(result); // ['1초', '2초', '3초']
})
// promise.all
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
.then((value) => console.log(value)) // ['1초', '2초', '3초']
.catch((err) => console.log(err));
프로미스 체이닝을 이용했을 경우 6초가 걸리는 반면 promise.all을 사용했을 경우 3초가 걸린다. 반환 순서는 완료 순서와 상관없이 매개변수에 주어진 순서이다. promise.all은 프로미스 체이닝보다 코드가 간결하고 걸리는 시간도 짧다. 또한 실패하는 시간도 짧다. 하나라도 실패하면 그 시점에 에러를 발생시키며 종료된다.
Promise.all([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
]).then((value) => console.log(value))
.catch((err) => console.log(err)); // Error: 에러1
promise.all의 전달인자 배열의 요소 중 하나라도 실패를 하면 즉시 에러가 발생한다. 위 코드는 1초만에 종료된다. fail-fast 방식이다.
다음 포스팅을 통해 프로미스 체이닝과 promise.all()의 동작에 대해 더 자세히 알 수 있다.
async / await
ES8에서 새로 생겼다. 콜백지옥과 프로미스지옥으로 인해 코드가 길어지고 가독성이 나빠지는 문제점을 해결하였다.
await 은 프로미스를 기다린다. await은 async function 에서 사용할 수 있다.
await 문은 프로미스가 settled(fulfilled 또는 rejected) 일때까지 async함수의 실행을 일시정지하고 fulfilled 상태가 되면 일시정지된 부분부터 이어서 실행한다. 반환값은 fulfill 된 값이다. rejected 상태가 되면 reject 된 값을 throw한다.
const printString = (string, callback) => {
setTimeout(function () {
console.log(string);
callback();
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = async () => {
await printString('A');
await printString('B');
await printString('C');
}
callback의 예시에서 사용했던 printString코드를 async/await을 통해 간결하고 깔끔하게 사용할 수 있다.
'개발 > JS, TS, React' 카테고리의 다른 글
React JSX / CRA / React Router (0) | 2023.03.22 |
---|---|
JS 프로미스 체이닝과 Promise.all(), Fetch API (0) | 2023.03.21 |
JS 프로토타입 (0) | 2023.03.16 |
JS 고차함수 (0) | 2023.03.15 |
JS var, let, const (0) | 2023.03.07 |
- Total
- Today
- Yesterday
- 태릉맛집
- 회고
- 태릉 이자카야
- 홍천 삼겹살
- 공릉 카페
- 춘천맛집
- 공릉 밀크티
- 태릉삼겹살
- 신불당 술집
- 깃허브 데스크탑 로그아웃
- 이수 맛집
- 공릉맛집
- 맥 깃허브 데스크탑
- 태릉 꼬치
- 롯데월드 키오스크
- Til
- sitemap
- 공릉 이자카야
- 태릉 술집
- 티스토리
- 춘천닭갈비
- 롯데월드 보조배터리
- solo project
- 구글서치콘솔
- 공릉 맛집
- 공릉 꼬치
- 공릉 술집
- 티스토리검색
- 을지로맛집
- 롯데월드 매직패스 프리미엄
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |