비동기

FAST FOX·2023년 4월 19일
0

비동기란??

우선 비동기의 반대인 동기(synchronous)가 무엇인지 알아보자.
동기적인 것은 선행작업이 끝나는 시점과 새로운 작업이 시작하는 시점이 같은 것을 말한다. 즉, 새로운 작업은 선행작업이 끝나기 전에는 시작될 수 없다는 말이고 이런 현상을 블로킹(blocking)이라고 부른다.

그리고 이와 반대되게 블로킹이 없이 작업이 시작되는 것이 비동기라고 한다.

비동기적인 작동은 다음과 같은 작업에서 특히 유용하다.

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

싱글 스레드라고 무조건 동기적으로 작동하는 것도 아니고 멀티 쓰레드라고 꼭 비동기적으로 작동하는 것도 아니다.
JavaScript는 싱글 스레드 기반이기 때문에 동기적으로 작동하지만 브라우저에서의 실행 환경(Runtime)에서는 자바스크립트 엔진 자체가 제공하지 않는 일부 기능인 DOM 조작이나 AJAX 같은 비동기 처리를 위한 web API를 제공한다.
동기?비동기?쓰레드?멀티 쓰레드?

타이머 API

JavaScript의 비동기를 공부하다보면 만나게 되는 API이다.
타이머 API는 브라우저에서 제공하는 Web API이며 비동기로 작동하도록 구성되어 있다.

1.setTimeout(callback,millisecond) : 일정 시간 후에 함수를 실행

setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);
// 123(임의의 타이머 ID)

2.clearTimeout(timerId) : setTimeout을 종료

const timer = setTimeout(function () {
  console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨.

3.setInterval(callback, millisecond) : 일정 시간의 간격을 가지고 함수를 반복실행

setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
// 345

4.clearInterval(timerId) : setInterval을 종료

const timer = setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨.

비동기 코드를 제어하는 방법

1. callback함수를 사용하여 코드 제어

// 랜덤한 시간이 지난 후에 string인자를 콘솔에 나타내고 callback함수를 실행하는 함수
const printString = (string, callback) => {
  setTimeout(function () {
    console.log(string);
    callback();
  }, Math.floor(Math.random() * 100) + 1);
};

const printAll = () => {
  printString('A', () => {
    //string인자인 "A"가 콘솔에 나타난 뒤에 
    //callbkack함수인 printString('B',() => {...})가 실행된다.
    printString('B', () => {
      //위의 'A'와 같은 방식으로 진행.
      printString('C', () => {});
    });
  });
};

2. Promise를 사용하여 코드 제어

콜백 함수를 사용하여 비동기 코드를 제어할 수 있지만 코드가 길어지게 되면 Callback Hell에 빠지게 된다.
콜백헬을 방지하고 비동기 처리 중 발생한 에러의 처리를 위해 ES6에서 Promise를 도입했다.

Promise 생성자 함수를 사용하여 인스턴스를 생성하고, 인스턴스는 비동기 처리를 수행할 Executor라는 콜백 함수를 인수로 전달 받고, Executor는 두 개의 인자로 resolvereject를 받는다.

  • resolve : 비동기 처리에 성공하면 호출되는 메소드, 비동기 처리 결과를 해당 메소드의 인자로 전달한다.
  • reject : 비동기 처리에 실패하면 호출되는 메소드, 실패 이유인 error
let promise = new Promise((resolve, reject) => {
	// 1. 정상적으로 처리되는 경우 resolve의 인자에 값을 전달할 수도 있습니다.
	resolve(value);
	// 2. 에러가 발생하는 경우 reject의 인자에 에러메세지를 전달할 수도 있습니다.
	reject(error);
});

2-1 Promise 내부의 프로퍼티

Promise객체는 state,result의 내부 프로퍼티를 갖지만 .then,.catch,finally의 메서드를 통해서만 접근이 가능하다.
여기서 '접근이 가능하다'라는 말은 밑에서 나올 예제를 보면 좀 더 쉽게 이해할 수 있다.

  • state : 기본상태는 pending이고 콜백 함수가 성공한다면 fulfilled로 변경, 실패한다면 reject로 변경된다.

  • result : 기본상태는 undefined이고 콜백 함수가 작동하여resolve(value)가 호출되면 value로, 에러가 발생하여 reject(error)가 호출되면 error로 변합니다.

2-2 then,catch,finally

  • .then() : Promise를 리턴하고 두 개의 콜백 함수를 인수로 받는다. 하나는 Promise가 이행했을 때, 다른 하나는 거부했을 때를 위한 콜백 함수이다.
promise.then(successCallback, failureCallback)

promise.then(function (value) {
  //성공했을 때 실행
}, function (reason) {
  //실패했을 때 실행
});
  • .catch() : 에러가 발생했을 경우에는 reject함수를 호출하고 .catch()메서드로 접근할 수 있다.

  • finally() : 성공 여부와 상관없이 작동시킬 때.

2-3 promise chaning

.then()메소드는 Promise를 리턴하고 인수로 받은 콜백 함수들의 리턴값을 이어 받는다. 그렇기 때문에 여러개의 then 호출들을 손쉽게 연결할 수 있다.
❗️then에 핸들러로 전달된 콜백 함수가 Promise를 반환할 경우 반환된 Promise의 result가 그 다음 then의 인자로 노출된다. 그렇지 않은 경우에는 then의 반환 값을 다음 then의 인자로 넘겨준다.

예시로 다음과 같은 Promise객체를 만들었다고 가정하자.

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

then의 반환값이 Promise가 아닌 경우

promise
  .then((value) => {
    console.log(value);
    return '성공1'; // 다음 then메소드의 인자로 "성공1"을 넘겨줌
  })
  .then((value) => {
    console.log(value);
    return '성공2'; // 다음 then메소드의 인자로 "성공2"을 넘겨줌
  })
  .then((value) => {
    console.log(value);
    return '성공3'; // 다음 then메소드의 인자로 "성공3"을 넘겨줌
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
//마지막 반환값
//Promise {<fulfilled>: '성공3'}
// [[Prototype]] : Promise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: "성공3"

then이 Promise를 반환하는 경우

promise
  .then((value) => {//
    return new Promise(function(resolve, reject) {
    	resolve("성공1"); // 다음 then메소드의 인자로 넘겨줄 result.
        console.log(value)
    })
  })
  .then((value) => {
    return new Promise(function(resolve, reject) {
    	resolve("성공2"); // 다음 then메소드의 인자로 넘겨줄 result.
        console.log(value)
    })
  })
  .then((value) => {
    return new Promise(function(resolve, reject) {
    	resolve("성공3"); // 다음 then메소드의 인자로 넘겨줄 result.
        console.log(value)
    })
  })
  .catch((error) => {
    console.log(error);
  })

//마지막 반환값
//Promise {<fulfilled>: '성공3'}
// [[Prototype]] : Promise
// [[PromiseState]]: "fulfilled"
// [[PromiseResult]]: "성공3"

2-4 Promise.all()

여러 개의 비동기 작업을 동시에 처리하려고 promise chaining을 사용하면 코드가 굉장히 길어진다. 그리고 이들은 동기적으로 작동하기 때문에 시간도 오래 걸리게 된다.
이때 Promise.all()을 사용한다.

인자로는 배열을 받고 해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 결과를 배열에 저장해 새로운 Promise를 반환 해줍니다.

<기존>
  
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))
  .catch((err) => console.log(err));

promise.all에서 에러가 발생한다면 즉시 종료된다.

3. Async/Await

JS ES에서 제공된 키워드이다. 이를 통해 복잡한 Promise를 간결하게 정리할 수 있게 됐다.

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');
};
profile
준비하는 개발자

0개의 댓글