비동기 처리란?
특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 것을 비동기 처리라고 한다.
왜 비동기 처리일까?
- 서버로 데이터를 요청 시, 서버가 언제 그 요청에 대한 응답을 줄지도 모르는데 마냥 다른 코드를 실행 안 하고 기다릴 순 없음!
- 비동기 처리가 아니고 동기 처리라면 코드 실행하고 기다리고, 실행하고 기다리고...
비동기 코드를 처리하기 위한 3가지 방법
- callback 함수
- promise
- async / await
callback(콜백) 함수
- JavaScript는 함수를 인자로 받고 다른 함수를 통해 반환될 수 있는데, 인자(매개변수)로 대입되는 함수를 콜백함수라고 한다.
- 즉, 다른 함수가 실행을 끝낸 뒤 실행되는 함수
- 함수를 선언할 때는 parameter(인자, 매개변수)로 함수를 받아서 쓸 수 있다.
콜백 함수를 왜 사용할까?
예시 코드
const button = document.querySelector('button');
function sayHello() {
console.log('hello~~~~');
}
button.addEventListener('click', sayHello);
독립적으로 수행되는 작업도 있는 반면 응답을 받은 이후 처리되어야 하는 종속적인 작업도 있을 수 있으므로 그에 대한 대응 방법이 필요
callback 함수 사용 방법
보통 함수를 선언한 뒤에 함수 타입 파라미터를 맨 마지막에 하나 더 선언해 주는 방식으로 정의
예시 코드
function sum(x, y, callback) {
const add = x + y;
callback(add); // 콜백함수 호출
}
function print(result) {
console.log(result); // sum 함수에서 넘어온 매개변수 출력
}
sum(1, 2, print); // 3
Promise
- 비동기 방식으로 작성된 함수를 동기 방식처럼 순서대로 실행할 수 있도록 만들 수 있다. (하지만, 코드는 여전히 비동기 적으로 동작)
- 성공과 실패를 분리하여 반환
- 비동기 작업이 완료된 이후에 다음 작업을 연결시켜 진행할 수 있는 기능을 가짐
Promise 사용 방법
- new Promise 로 만들어서 사용!
- new Promise가 만들어질 때, executor(실행 함수)가 자동으로 실행
- executor의 인수인 reject 와 resolve
- Promise가 생성되면 작업을 실행하고, 작업의 완료 여부를 executor의 매개변수를 통해서 전달
- executor 매개변수 (resolve와 reject 모두 callback)
- resolve : 비동기 작업이 성공했을 때
- reject : 비동기 작업이 실패했을 때
Promise의 상태
- Pending(대기) : Promise를 수행 중인 상태
- Fulfilled(이행) : Promise가 Resolve 된 상태 (성공)
- Rejected(거부) : Promise가 지켜지지 못한 상태. Reject 된 상태 (실패)
- Settled : fulfilled 혹은 rejected로 결론이 난 상태
Promise 메서드
- then : Promise가 성공적으로 해결될 때 실행되는 콜백 함수를 등록하는 데 사용
- 이 메서드는 Promise가 해결될 때 그 결과값을 콜백 함수의 인자로 받아 처리할 수 있음
- 여러 개의 then 메서드를 체이닝하여 순차적으로 작업을 수행할 수 있음
- 즉, 앞에서 return 한 값을 다음 then에서 매개변수로 이어 받을 수 있음
- catch : Promise에서 발생한 오류를 처리하기 위한 콜백 함수를 등록하는 데 사용
- Promise 체인에서 어떤 위치에서든 오류가 발생하면 해당 오류를 처리하고 계속 체인을 이어나갈 수 있음
- finally : Promise가 성공 또는 실패와 관계없이 마지막으로 실행되는 콜백 함수를 등록하는 데 사용
- 네트워크 요청을 보낸 후에 무조건 수행되어야 하는 정리 작업이나 리소스 해제 작업을 처리할 때 유용
- Ex) loading... 여부 등등
예시 코드
function promise1(flag) {
return new Promise(function (resolve, reject) {
if (flag) {
resolve('promise 상태는 fulfilled!! then으로 연결됩니다. \n 이때의 flag가 true입니다.');
} else {
reject('promise 상태는 rejected!! catch로 연결됩니다. \n 이때의 flag는 false입니다.');
}
});
}
promise1(true)
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log('끝');
});
// 결과
/*
promise 상태는 fulfilled!! then으로 연결됩니다.
이때의 flag가 true입니다.
끝
*/
promise1(false)
.then((result) => {
console.log(result);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log('끝');
});
// 결과
/*
promise 상태는 rejected!! catch로 연결됩니다.
이때의 flag가 false입니다.
끝
*/
/** 여러 개의 then 메서드를 체이닝 */
function promise2() {
return new Promise(function (resolve, reject) {
resolve(1);
});
}
promise2()
.then((result) => {
console.log(result); // 1
return result * 2;
})
.then((result) => {
console.log(result); // 2
return result * 2;
})
.then((result) => {
console.log(result); // 4
});
Async / Await
- 프로미스 기반 코드를 좀 더 쓰기 쉽고 읽기 쉽게 하기 위해 등장
- 기능이 추가된 것이 아닌, Promise를 다르게 사용하는 것
- 에러는 try...catch로 핸들링
async
- 함수 앞에 붙여 Promise를 반환
- 프로미스가 아닌 값을 반환해도 프로미스로 감싸서 반환
await
- '기다리다' 라는 뜻을 가진 영단어
- 프로미스 앞에 붙여 프로미스가 다 처리될 때까지 기다리는 역할을 하며 결과는 그 후에 반환
예시 코드
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(ms);
}, ms);
});
}
async function print() {
console.log('1'); // 1
try {
const result = await delay(1000); // 1초를 기다리고 반환값을 result라는 변수에 저장
console.log(result); // 1000
} catch (err) {
console.log('Error!', err);
}
console.log('2'); // 2
}
print();
// 출력
/*
1
1000
2
*/
언제 어디서 어떻게 써야 할까?
영화 API를 가지고 박스 오피스 영화 순위, 순위에 맞는 영화 정보와 영화 예고편을 보여주는 사이트를 만든다고 가정해보자.
하지만 1개의 사이트에서 모든 정보를 제공하지 않는다. 찾아보니 3개의 API를 합치면 만들 수 있을 거 같다.
A 사이트에서 박스 오피스 영화 순위를 제공, B 사이트는 영화 정보를 제공, C 사이트는 영화 예고편을 제공해 주는 API가 있다고 해보자.
A 사이트에서 박스 오피스 순위 정보를 가지고 와서 B와 C 사이트에 해당 영화 순위에 맞는 정보와 예고편을 API 요청을 해야 한다.
위 내용을 순서대로 표현하면,
- A 사이트에서 현재 박스 오피스 순위 정보를 가져온다.
- 가져온 순위 정보에 따라 B 사이트와 C 사이트에 영화 정보와 예고편을 요청한다.
- B 사이트에서 해당 순위의 영화 정보를 받아오고, C 사이트에서 해당 순위의 영화 예고편을 받아온다.
- 받아온 영화 정보와 예고편을 화면에 그린다.
그림으로 표현하자면 아래와 같다.
예시 코드
async function getFromSiteA() {
const url = '해당 사이트 url';
const params = '가져올 정보';
const response = await fetch(url + params);
const data = await response.json();
return data;
}
async function getFromSiteB(movieRankings) {
const url = '해당 사이트 url';
const dataList = [];
for (const params of movieRankings) {
const response = await fetch(url + params);
const data = await response.json();
dataList.push(data);
}
return dataList;
}
async function getFromSiteC(movieRankings) {
const url = '해당 사이트 url';
const dataList = [];
for (const params of movieRankings) {
const response = await fetch(url + params);
const data = await response.json();
dataList.push(data);
}
return dataList;
}
function displayMovieInfo(movieInfoList) {
// 영화 정보를 그려주는 코드
}
function displayMovieTrailer(movieTrailerList) {
// 영화 예고편을 그려주는 코드
}
async function main() {
try {
const movieRankings = await getFromSiteA(); // A 사이트에서 가져온 오피스 박스 순위를 movieRankings에 저장
const movieInfoList = await getFromSiteB(movieRankings); // movieRankings 정보를 가지고 B 사이트에 정보 요청
const movieTrailerList = await getFromSiteC(movieRankings); // movieRankings 정보를 가지고 C 사이트애 정보 요청
displayMovieInfo(movieInfoList); // B 사이트에서 가져온 결과 값을 화면에 그려줌
displayMovieTrailer(movieTrailerList); // C 사이트에서 가져온 결과 값을 화면에 그려줌
} catch (err) {
console.log('Error!', err);
}
}
main();
위 코드를 순서대로 설명하면 아래와 같다.
- A 사이트에 정보를 요청하고 끝날 때까지 기다린다.
- A 사이트 응답이 끝나면 A 정보를 가지고 B 사이트에 정보를 요청하고 끝날 때까지 기다린다.
- B 사이트 응답이 끝나면 A 정보를 가지고 C 사이트에 정보를 요청하고 끝날 때까지 기다린다.
- C 사이트 응답이 끝나면 화면에 그려준다.
이 코드는 정상 동작은 하는데 약간 아쉬움이 있다.
B와 C는 서로 무관한데 getFromSiteC 함수를 쓰기 위해서는 getFromSiteB 함수가 끝날 때까지 기다려야 한다.
즉, getFromSiteB 함수 요청 시간이 2초, getFromSiteC 함수 요청 시간이 5초 이면 총 7초를 기다려야 한다.
위 코드를 개선하면 아래 코드로 바꿀 수 있다.
async function getFromSiteA() {
const url = '해당 사이트 url';
const params = '가져올 정보';
const response = await fetch(url + params);
const data = await response.json();
return data;
}
async function getFromSiteB(movieRankings) {
const url = '해당 사이트 url';
const dataList = [];
for (const params of movieRankings) {
const response = await fetch(url + params);
const data = await response.json();
dataList.push(data);
}
return dataList;
}
async function getFromSiteC(movieRankings) {
const url = '해당 사이트 url';
const dataList = [];
for (const params of movieRankings) {
const response = await fetch(url + params);
const data = await response.json();
dataList.push(data);
}
return dataList;
}
function displayMovieInfo(movieInfoList) {
// 영화 정보를 그려주는 코드
}
function displayMovieTrailer(movieTrailerList) {
// 영화 예고편을 그려주는 코드
}
async function main() {
try {
const movieRankings = await getFromSiteA(); // A 사이트에서 가져온 오피스 박스 순위를 movieRankings에 저장
const [movieInfoList, movieTrailerList] = await Promise.all([
getFromSiteB(movieRankings),
getFromSiteC(movieRankings),
]);
// movieRankings 정보를 가지고 B 사이트와 C 사이트에 동시에 요청을 한다. 두개 함수 중 늦게 끝나는 함수 기준으로 결과값을 반환
// await Promise.all에서 리턴값으로 배열이 반환되는데 구조 분해를 통해 movieInfoList와 movieTrailerList 변수에 결과값을 대입한다.
displayMovieInfo(movieInfoList); // B 사이트에서 가져온 결과 값을 화면에 그려줌
displayMovieTrailer(movieTrailerList); // C 사이트에서 가져온 결과 값을 화면에 그려줌
} catch (err) {
console.log('Error!', err);
}
}
main();
Promise.all 메서드를 사용하면 getFromSiteB 함수와 getFromSiteC 함수를 동시에 실행하고 늦게 끝나는 함수 기준으로 결과값을 반환한다.
위 코드를 순서대로 설명하면 아래와 같다.
- A 사이트에 정보를 요청하고 끝날 때까지 기다린다.
- A 사이트 응답이 끝나면 A 정보를 가지고 B 사이트와 C 사이트에 동시에 정보를 요청한다.
- B, C 사이트 중 응답이 늦게 끝나는 함수 기준으로 결과값을 반환한다.
- B, C 사이트 응답이 끝나면 화면에 그려준다.
즉, getFromSiteB 함수 요청시간이 2초, getFromSiteC 함수 요청 시간이 5초 이면 총 5초만 기다리면 된다.
개선 이전 코드보다 2초를 더 줄일 수 있다.
Promise.all 메서드 예시 코드
function delay(ms, message) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(message);
}, ms);
});
}
async function sequentialExecution() {
console.time('순차적 실행');
const result1 = await delay(2000, '첫번째');
const result2 = await delay(5000, '두번째');
console.log(result1, result2, '<<=== 순차적 실행');
console.timeEnd('순차적 실행');
}
async function parallelExecutionWithPromiseAll() {
console.time('Promise.all');
const [result1, result2] = await Promise.all([delay(2000, '첫번째'), delay(5000, '두번째')]);
console.log(result1, result2, '<<=== Promise.all');
console.timeEnd('Promise.all');
}
sequentialExecution(); // 걸리는 시간 7초
parallelExecutionWithPromiseAll(); // 걸리는 시간 5초
Promise.all 메서드를 사용 할 때에는, 등록한 프로미스 중 하나라도 실패하면, 모든게 실패 한 것으로 간주한다.
이를 해결 하려면 Promise.allSettled 메서드를 참고하면 되겠다.
'JavaScript' 카테고리의 다른 글
JavaScript Class (0) | 2024.04.02 |
---|---|
JavaScript fetch와 axios 사용법 (0) | 2023.08.22 |
JavaScript 배열 메서드 정리 (0) | 2023.08.15 |
마우스 이벤트(client, page, offset, screen)의 차이점 (0) | 2023.04.07 |
반복문 (0) | 2020.07.20 |