1. 비동기
1) 동기와 비동기
(1) 동기(synchronous)
특정 코드의 실행이 완료될때까지 기다린 후에 다음 코드를 수행한다.
즉 여러가지를 한꺼번에 실행할 수 없고 한번에 하나씩만 실행한다.
(2) 비동기(asynchronous)
특정 코드의 실행이 완료될때까지 기다리지 않고 다음 코드들을 수행한다.
즉 여러가지 코드를 한꺼번에 실행할 수 있다. 동기적인 코드보다 효율적이다.
2) 타이머 관련 API
이 API들은 브라우저에서 제공하는 Web API이며 비동기로 작동하도록 구성되어 있다.
(1) setTimeout() / clearTimeout()
const timerId = setTimeout(()=>{
console.log('1초 뒤에 실행')
}, 1000)
clearTimeout(timerId); // setTimeout 종료
- setTimeout(callback, millisecond) : 일정 시간 뒤에 콜백함수를 실행한다. 시간은 밀리초로 작성한다.
- clearTimeout(timerId) : setTimeout 타이머를 종료시킨다.
(2) setInterval() / claerInterval()
const timerId = setInterval(()=>{
console.log('1초마다 실행');
}, 1000);
clearInterval(timerId); // setInterval 종료
- setInterval(callback, millisecond) : 일정 시간의 간격을 두고 함수를 반복 실행한다. 시간간격은 밀리초로 작성한다.
- clearInterval(timerId) : setInterval 타이머를 종료시킨다.
비동기 코드는 코드가 작성된 순서대로 작동하는 것이 아니라 동작이 완료되는 순으로 작동한다.
=> 코드의 순서를 예측할 수 없다.
=> callback 함수를 활용해 순서를 제어할 수 있다.
3) Callback 함수를 활용한 비동기 코드의 순서 제어
const printString = (string, callback) => {
setTimeout(function () {
console.log(string)
callback(); // 콜백함수, 즉 printString을 실행한다
}, Math.floor(Math.random() * 100) + 1) // -> 랜덤한 간격이지만 A, B, C 순으로 실행됨
}
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {})
})
})
}
printAll();
실행 순서는 아래와 같다.
1. printAll() 함수 호출
2. printString('A', () =>...) 실행
3. printString 함수 실행 => setTimeout이 실행됨.
4. 콘솔에 'A'출력
5. callback 함수 호출. 즉 { printString('B' ...) } 가 실행됨.
6. 3~5 반복. ('B' 출력 후 callback 함수 호출 -> 'C' 출력 후 callback 함수 호출)
이렇게 callback 함수를 이용해 코드의 순서를 제어할 수 있다. 하지만 여기에도 문제점이 있다.
작성해야 하는 내용이 많아질수록 코드가 길어지고, 복잡해지며 가독성이 떨어진다. => Callback Hell 발생!
Callback Hell의 예시
이러한 Callback Hell 현상을 방지하기 위해 Promise가 사용되기 시작했다.
4) Promise
Promise는 클래스이다. new 키워드를 사용해 Promise 객체를 생성할 수 있다.
Promise는 비동기 처리를 수행할 콜백함수(executor)를 인수로 전달받는데, 이것은 resolve, reject 함수를 인수로 받는다.
예시
let promise = new Promise((resolve, reject) => {
// 1. 정상적으로 처리되는 경우
// resolve 인자로 값 전달가능
resolve(value)
// 2. 에러가 발생하는 경우
// reject의 인자로 에러메세지를 전달가능
reject(error)
})
(1) Promise가 작동하는 순서
Promise 객체 생성 -> 콜백함수(executor) 실행 -> 코드 작동 => 정상작동 => resolve 함수 호출
=> 에러발생 => reject 함수 호출
코드 작동시에는 값을 전달하거나 에러 메세지를 전달할 수 있다.
(2) State / Result
Promise 객체의 내부 프로퍼티. 직접 접근은 불가능하다. then / catch / finally 메서드를 사용할때만 접근 가능하다.
State
- 기본상태 : pending(대기)
- 정상 작동 : fulfilled(이행) 으로 변경됨
- 에러 발생: rejected(거부) 로 변경됨
Result
- 초기값 : undefined
- 정상 작동 : resolve의 값으로 변경. 위 예시에서는 resolve의 value.
- 에러 발생 : reject의 값으로 변경. 위 예시에서는 reject의 error.
(3) then / catch / finally
let promise = new Promise(function(resolve, reject) {
resolve("then으로 접근");
});
promise
.then(value => {
console.log(value);
// then으로 접근
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log("finally로 접근");
// finally로 접근
})
then
executor가 정상 처리되고 resolve 함수 호출시 then 메서드로 접근 가능하다.
then이 리턴한 값이 Promise 객체일 경우, Promise의 내부 프로퍼티 result를 다음 then의 콜백함수의 인자로 받아오고 Promise가 아닐 경우 리턴한 값을 then의 콜백함수의 인자로 받아올 수 있다.
catch
에러 발생시 reject가 호출되고 .catch로 접근 가능하다.
finally
executor의 정상/에러 작동 여부와 관계 없이 접근 가능하다.
(4) Promise Chaining
비동기 작업을 순차적으로 실행할때 필요하다.
Promise Chaining은 then / catch / finally 메서드들이 Promise를 반환하기 때문에 가능하다.
then 메소드를 통해 연결하고, 에러 발생시에는 catch로 처리한다.
let promise = new Promise(function(resolve, reject) {
resolve("성공");
});
promise
.then((value) => {
console.log(value);
return '성공';
})
.catch((error) => {
console.log(error);
return '실패';
})
.finally(() => {
console.log('성공이든 실패든 작동!');
});
(5) Promise.all()
여러개의 비동기 작업을 동시에 처리하려고 할때 사용한다. 인자로는 배열을 받는다.
배열에 들어있는 모든 Promise에서 excutor 내 작성한 코드들이 정상적으로 처리될경우 결과를 배열에 저장하여 새로운 Promise를 반환한다.
단, 인자로 받는 배열의 Promise 중 하나라도 에러가 발생하면 나머지 Promise의 state와는 상관없이 즉시 종료되고 에러를 반환한다.
// 여러개의 프로미스들
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));
이렇게 Promise를 통해 비동기 코드의 순서를 제어할 수 있지만, Callback Hell처럼 Promise도 구현할 내용이 많을 수록 코드가 길어지고 복잡해지고 가독성이 낮아지며 Promise Hell과 같은 현상이 나타난다.
=> 코드를 더 간결하게 작성할 수 있도록 async / await 키워드를 사용할 수 있다!
5) async / await
사용하고자 하는 함수 앞에 async 키워드를 사용하고, async 함수 내에서만 await 키워드를 사용하면 된다.
await 키워드가 작성된 코드가 동작하고 나서 다음 순서의 코드가 동작함으로, 이를 통해 비동기 코드의 순서를 제어할 수 있다.
예시
// 함수 선언식
async function funcDeclarations() {
await 작성하고자 하는 코드
...
}
// 함수 표현식
const funcExpression = async function () {
await 작성하고자 하는 코드
...
}
// 화살표 함수
const ArrowFunc = async () => {
await 작성하고자 하는 코드
...
}
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
console.log(string);
}, Math.floor(Math.random() * 100) + 1);
});
};
const printAll = async () => {
await printString('A'); // 이 코드의 실행이 끝나야 다음 코드가 실행된다
await printString('B');
await printString('C');
};
* 기타
typeof window !== 'undefined'
window 객체는 브라우저가 렌더링 될때 undefined가 아니게 된다.
따라서 위의 코드는 브라우저가 렌더링 된 다음 코드를 실행해야할때의 조건으로 사용할 수 있다.
************************
일일회고 겸 요약정리
오늘은 동기/비동기에 대해서 공부했다.
callback 함수, Promise, async / await로 비동기 코드의 순서를 제어하는 방법도 배웠다.
확실히 앞의 두 방법을 먼저 배우고 나니 async / await가 훨씬 간단하게 느껴진다.
callback으로 비동기 코드의 순서를 제어해야 할때, 구현해야 하는 코드 내용이 많을수록 콜백이 중첩되어 callback Hell이 발생할 수 있다. 보기만 해도 멀미난다...🤢🤮
이와 같은 callback Hell을 방지하기 위해 Promise를 사용하고, Promise는 성공과 실패 결과를 분리해서 각각 then과 catch 메서드를 사용해 값을 전달할 수 있다. 또 실패 결과와 관계 없이 값에 접근 할 수 있는 finally 메서드도 사용할 수 있다. then과 catch, finally는 Promise 객체를 전달함으로 Promise Chaining이 가능하게 된다.
이를 통해 여러개의 비동기 코드를 제어해야 할 경우, 배열에 Promise를 담아 Promise.all()에 넘겨주어 사용할 수도 있다.
단 배열에 담긴 Promise 중 하나라도 에러가 날 경우 해당 배열에 들어있는 모든 Promise의 동작이 종료되기때문에 주의해야한다.
그리고 callback Hell과 같은 Promise Hell을 방지하기 위해 async / await 키워드를 사용할 수 있다.
사용하고자 하는 함수 앞에 async 키워드를 붙이고, 해당 함수 내에서 제어해야 할 코드들의 앞에 await를 붙여준다.
await가 붙은 코드가 실행되어야 다음 코드가 실행됨으로 이를 이용해 비동기 관련 코드들을 제어할 수 있다.
아마 프로젝트를 할때는 async / await를 더 많이 사용하게 되지 않을까?
틈틈히 레퍼런스를 찾아보고 스스로 연습해봐야겠다.
'study > TIL' 카테고리의 다른 글
23.01.19 - fetch, axios (0) | 2023.01.19 |
---|---|
23.01.18 - Node.js (0) | 2023.01.18 |
23.01.16 - 프로토타입 체인 (0) | 2023.01.16 |
23.01.13 - 클래스와 인스턴스, 객체 지향 프로그래밍, 프로토타입과 클래스 (0) | 2023.01.13 |
23.01.12 - 고차 함수, filter, map, reduce (0) | 2023.01.12 |