비동기

유슬기·2023년 1월 24일
0

프론트엔드

목록 보기
28/64
post-thumbnail

동기와 비동기

동기

특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것을 의미한다.

예를 들어, 카페에서 첫 번째 손님이 주문을 하면 주문한 커피를 다 만들고, 그 커피를 받아간 뒤에야 두 번째 주문을 할 수 있는 경우를 생각하면 된다.

첫 주문이 마무리 될 때까지 다음 주문을 할 수 없게 작업을 막는 것을 blocking 이라고 한다.

첫 주문 완료 시점과 두번째 주문 시작 시점이 같은 위와같은 상황을 “동기적(synchronous)이다” 라고 한다.

비동기

특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것을 의미한다.

예를 들어, 카페에서 주문을 받을 때 여러 손님의 주문을 받으면서 커피를 만들고, 완성되는 대로 손님에게 서빙을 하는 경우를 생각해보자.

커피 주문이 blocking 되지 않고, 언제든지 주문을 받을 수 있다. non-blocking

첫 주문의 완료 시점과 다음 주문의 시작 시점이 같을 필요가 없다. 비동기적(asynchronous)으로 작동


JavaScript의 비동기적 실행(Asynchronous execution)이라는 개념은 웹 개발에서 특히 유용하다.

비동기적으로 작동되어야 효율적인 대표적인 작업

  • 백그라운드 실행, 로딩 창 등의 작업
  • 인터넷에서 서버로 요청을 보내고, 응답을 기다리는 작업
  • 큰 용량의 파일을 로딩하는 작업

JavaScript의 작동원리

JavaScript는 싱글 스레드 기반으로 동작하는 언어로, 동기적으로 작동하게 된다.
그러나 JavaScript에서도 비동기 처리가 가능하다.

비동기 처리가 가능한 이유
JavaScript가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있다.

타이머 관련 API

setTimeout(callback, ms)

일정 시간 후에 함수를 실행한다.

  • 매개변수: 실행할 콜백함수, 콜백 함수 실행 전 기다려야 할 시간(밀리초)
  • return 값: 임의의 타이머 ID
setTimeout(function () {
	console.log('1초 후 실행');
	}, 1000);
// return 값: 1398 (임의의 타이머 ID)
// 1초(1000ms) 후 콘솔에 출력: 1초 후 실행

clearTimeout(timerId)

setTimeout 타이머를 종료한다.

  • 매개변수: 타이머 ID
  • return 값: 없음
const timer = setTimeout(function () {
	console.log('10초 후 실행');
	}, 10000);
clearTimeout(timer);
// setTimeout이 종료되어 아무 함수도 실행되지 않음

setInterval(callback, ms)

일정 시간의 간격을 가지고 함수를 반복적으로 실행한다.

  • 매개변수: 실행할 콜백 함수, 반복적으로 함수를 실행시키기 위한 시간 간격(밀리초)
  • return 값: 임의의 타이머 ID
setInterval(function () {
	console.log('1초마다 실행');
	}, 1000);
// return 값: 1467 (임의의 타이머 ID)
// 1초 뒤 부터 콘솔에 1초마다 반복하여 콘솔에 출력: 1초마다 실행

clearInterval(timerId)

setInterval 타이머를 종료한다.

  • 매개변수: 타이머 ID
  • return 값: 없음
const timer = setInterval(function () {
	console.log('1초마다 실행');
	}, 1000);
clearInterval(timer);
// setInterval이 즉시 종료되어 아무 함수도 실행되지 않음

Callback

Callback 함수를 활용하는 것은 비동기로 작동하는 코드를 제어할 수 있는 방법 중 하나로, 비동기 코드의 순서를 제어할 수 있다. (비동기를 동기화)

const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {});
    });
  });
};

printAll();

console.log(`아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다!`);

/* 실행 결과 */
// 아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다!
// A
// B
// C

하지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.

Callback Hell을 방지하기 위해 Promise가 사용되기 시작했다.

Promise

new Promise

Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성한다.

Promise는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받는데, 이 콜백 함수는 resolve, reject 함수를 인수로 전달받는다.

Promise 객체가 생성되면 executor는 자동으로 실행되고, 작성했던 코드들이 작동된다.

코드가 정상적으로 처리 되었다면 resolve 함수를 호출하고, 에러가 발생했을 경우에는 reject 함수를 호출하면 된다.

let promise = new Promise((resolve, reject) => {
	// 1. 정상적으로 처리되는 경우
	// resolve의 인자에 값을 전달할 수도 있다.
	resolve(value);

	// 2. 에러가 발생하는 경우
	// reject의 인자에 에러메세지를 전달할 수도 있다.
	reject(error);
});

Promise 객체의 내부 프로퍼티

new Promise가 반환하는 Promise 객체는 stateresult 내부 프로퍼티를 갖는다.

하지만 직접 접근할 수 없고 .then, .catch, .finally 의 메서드를 사용해야 접근이 가능하다.

State

기본 상태는 pending(대기) 이다.

비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동했다면 fulfilled (이행)로 변경이 되고, 에러가 발생했다면 rejected (거부)가 된다.

Result

처음은 undefined 이다.

비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동하여resolve(value)가 호출되면 value로, 에러가 발생하여 reject(error)가 호출되면 error로 변한다.

then, catch, finally

아래의 예시로 Promise의 동작 방식을 확인해보자.

Then

executor에 작성했던 코드들이 정상적으로 처리가 되었다면 resolve 함수를 호출하고 .then 메서드로 접근할 수 있다.

또한 .then 안에서 리턴한 값이 PromisePromise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아오고, Promise가 아니라면 리턴한 값을 .then 의 콜백 함수의 인자로 받아올 수 있다.

let promise = new Promise((resolve, reject) => {
	resolve("성공");
});

promise.then(value => {
	console.log(value);
	// "성공"
})

Catch

executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 .catch 메서드로 접근할 수 있다.

let promise = new Promise(function(resolve, reject) {
	reject(new Error("에러"))
});

promise.catch(error => {
	console.log(error);
	// Error: 에러
})

Finally

executor에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally 메서드로 접근할 수 있다.

let promise = new Promise(function(resolve, reject) {
	resolve("성공");
});

promise
.then(value => {
	console.log(value);
	// "성공"
})
.catch(error => {
	console.log(error);
})
.finally(() => {
	console.log("성공이든 실패든 작동!");
	// "성공이든 실패든 작동!"
})

Promise chaining

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 '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
  .finally(() => {
    console.log('성공이든 실패든 작동!');
  });

Promise.all()

Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을때 사용한다.
인자로는 배열을 받으며, 해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 결과를 배열에 저장해 새로운 Promise를 반환 해준다.

아래 예시에서 Promise chaining을 사용했을 경우는 코드들이 순차적으로 동작되기 때문에 총 6초의 시간이 걸리게 되며, 같은 코드가 중복되는 현상도 발생하게 된다.

하지만 Promise.all()을 사용하면 비동기 작업들을 동시에 처리하기 때문에 3초 안에 모든 작업이 종료되며, 코드도 간결해진다.

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));

/* Promise chaining */
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);  
	 // (6초 뒤) ['1초', '2초', '3초']
  })

/* promise.all */
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // (3초 뒤) ['1초', '2초', '3초']
  .catch((err) => console.log(err));

추가적으로 Promise.all은 인자로 받는 배열에 있는 Promise중 하나라도 에러가 발생하게 되면 나머지 Promise의 state와 상관없이 즉시 종료된다.

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));
	// (1초 후 콘솔 출력) Error: 에러1

하지만 Promise 또한 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};

const printAll = () => {
  printString('A').then((value) => {
    console.log(value);

    printString('B').then((value) => {
      console.log(value);

      printString('C').then((value) => {
        console.log(value);

        printString('D').then((value) => {
          console.log(value);

          printString('E').then((value) => {
            console.log(value);

            printString('F').then((value) => {
              console.log(value);

              printString('G').then((value) => {
                console.log(value);

                ...

              });
            });
          });
        });
      });
    });
  });
};

printAll();
// A
// B
// C
// D
// E
// ...

Async/Await 키워드를 사용하면 Promise를 보다 간결하게 작성 가능하다.

Async/Await

JavaScript는 ES8에서 async/await키워드를 제공하여 이를 통해 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었다.

사용법: 함수 앞에 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');
  await printString('D');
  await printString('E');
  ...
};

printAll();
// A
// B
// C
// D
// E
// ...
profile
아무것도 모르는 코린이

0개의 댓글