내 맘대로 공부
article thumbnail

📌  비동기란 ?

비동기는 한 마디로 '동시에 여러가지 작업을 처리하는 것' 이라 설명할 수 있다. 

꽃, 떡볶이, 빵을 사려고 할 때, 꽃집에서 꽃다발이 완성될 때까지 기다리다가 꽃을 받고, 떡볶이 집을 가서 주문하고 받고, 마지막으로 빵집을 가서 빵을 사오는 순차적으로 진행하는 것보단 꽃집에 가서 주문하고, 떡볶이집에서 주문을 하고 다음에 빵을 사고 완성되는대로 꽃과 떡볶이를 받아오는 것이 시간 측면에서 더욱 효율적일 것이다. 

 

여기서 순차적으로 세 가지를 산 방법이 '동기' 각각을 주문하고 완성되는 대로 받은 것이 '비동기'이다.

즉 비동기라는 아이는 "효율성을 극대로 따지는 파워 J" 라고 설명할 수 있다.

 

 

🧸  비동기적으로 처리하는 경우

  • Ajax Web API 요청 : 서버쪽에서 데이터를 받아와야 하는 경우
  • 파일 읽기 : 서버에서 파일을 읽어야 하는 경우
  • 암호화/복호화 : 바로 처리 되지 않고, 시간이 어느정도 걸리는 경우
  • 작업 예약 : setTimeout을 사용하여 비동기 처리하는 경우

 

🧸  Javascript 에서의 비동기 처리 

JavaScript는 싱글 스레드 기반으로 동작하는 언어이다. 그래서 근본적으로 비동기 처리가 불가능한 언어이다. 하지만 JavaScript가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있다. (런타임은 나중 포스트에)

 

 그 중 첫 번째로 비동기를 경험할 수 있는 것은  브라우저에서 제공하는 Web API로, 타이머 관련 API가 있다. 

 

 

- setTimeOut(callBack, millisecond)

일정시간 후에 callBack 함수를 실행하도록 한다.

 

간단한 사용 예시를 보면 아래와 같다.

아래에 코드를 실행해보면 1초 후에 console 창에 '1초 후 실행'이라는 문구가 찍힐 것이다.

setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);

 

그래서 setTimeOut으로 비동기 처리를 할 수 있는 방법은 아래 코드를 보면 이해할 수 있다.

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

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

printAll();

비동기 처리라 함은 다음 실행될 코드를 막지 않는 'non-blocking'의 특징이 있는데 위의 코드에서 지연시간이 A,B,C 각각 3,2,1 초라고 했을 때, A는 3초 뒤, B는 2초 뒤, C는 1초 뒤에 실행되어 C-B-A 순으로 출력될 것이다. 하지만 'non-blocking'이기 때문에 모두 출력되기까지 걸린 시간은 총 3초이다. 

 

위의 코드는 실행할 때마다 출력되는 순서가 다르게 나타날 것이다. 이것이 바로 setTimeOut으로 비동기 처리 함수를 작성할 때의 단점이다. 왜냐하면 개발자 입장에서 출력 결과 예측이 아예 불가능하기 때문이다. 따라서 비동기로 작동하는 코드를 예측 가능하게 제어할 수 있도록 코드를 작성해야 한다.

 

 

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

setTimeOut()을 사용했을 때와 비슷한 코드이지만 callBack 함수를 활용한다는 점에서 출력 결과는 A-B-C 가 보장될 것이다.

setTimeOut()은 비동기적 코드로 여러 작업을 동시에 실행하도록 하지만 callBack 함수를 사용해줌으로써 실행 순서를 보장해주는 것으로 예측 가능한 결과를 나타내도록 한다.

 

하지만 이러한 callBack 함수를 여럿 쓰게 되면 가독성은 현저히 떨어지고 복잡해지기 때문에 흔히 'CallBack Hell'이라는 것을 경험할 수 있다. 예시 코드는 안가져오겠다... 상상만 해도 머리가 아파요 🫠 

 

 

 

- Promise

콜백 지옥을 해결할 수 있는 첫 번째 방법이자 비동기로 작동하는 코드를 조금 더 쉽게 제어할 수 있는 방법이다.

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

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

Promise는 class이기 때문에 new 연산자를 통해 객체를 생성하고, 총 두 개의 인자를 받는다. 

  • resolve : 작업이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출.
  • reject : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출.

 

Promise의 3가지 상태(states)

프로미스는 3가지의 상태(states)를 가지며, 여기서 상태란 프로미스의 처리 과정을 의미한다. new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 가진다.

  • Pending : 비동기 처리 로직이 아직 완료되지 않은 상태 (대기)
  • Fulfilled : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태 (성공)
  • Rejected : 비동기 처리가 실패하거나 오류가 발생한 상태(실패)

Pending

대기 상태로 Promise 객체가 생성만된 시기에 해당한다

new Promise();

 

 

FulFilled

비동기 처리가 성공된 경우로, resolve가 실행되면 이행(Fulfilled) 또는 완료 상태가 된다.

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

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

그리고 이행 상태가 되면 .then 메서드를 이용하여 콜백 함수의 인자로 결과값을 받아올 수 있다.

위의 코드를 보면 resolve 호출 시 "성공" 이라는 값이 저장되고, 이 값은 then 메서드의 인자를 통해서 가져올 수 있는 것이다.

 

 

Rejected

비동기 처리가 실패하여 reject가 호출된 경우 실패(rejected) 상태가 된다.

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

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

then()과 반대로 rejected 상태가 되면 .catch 메서드를 이용하여 then()과 동일하게 결과값을 받아올 수 있다.

성공은 then, 실패는 catch로 짝지어 결과 값을 구분해서 받아오는 것이다.

 

* then, catch 외 finally

더보기

* 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 객체가 줄줄이 연결된 것으로, 비동기 작업을 순차적으로 진행해야 하는 경우 사용된다.

이러한 체이닝은 .then, .catch, .finally 의 메서드들이 Promise를 반환하기에 .then을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 연결하여 처리할 수 있다.

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

const printAll = () => {
  printString('A')
    .then(() => {
      return printString('B');
    })
    .then(() => {
      return printString('C');
    });
};

printAll();

위의 코드는 순서대로 A-B-C가 출력되도록 비동기로 동작하는 코드를 제어한 코드이다.

처음 printString('A')가 호출되었을 때, resolve가 호출되었기 떄문에 그 다음 printString('B')를 호출할 수 있는 것이고, 그 뒤로도 동일하다. 

 

 

Promise All

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

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 chaining을 사용하는 경우는 코드들이 순차적(동기적)으로 실행되기 때문에 총 6초의 시간이 걸리게되며, 같은 코드가 중복되는 현상도 발생한다.

 

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

위에서 언급한 문제들을 해결할 수 있는 방법이 바로 Promise.all()이다.

Promise.all()은 비동기 작업들을 동시에 처리하여 3초 안에 모든 작업이 종료된다. 또한 가장 장점으로는 Promise chaining로 작성한 코드보다 훨씬 간결하게 코드를 작성할 수 있다는 것이다.

 

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

 

 

 

- Async / Await

Promise로 콜백 지옥을 경험하지 않도록 할 수 있지만, 결국 Promise도 코드가 길어지고 복잡해질 수록 가독성이 현저히 떨어지는 'Promise Hell'을 피해갈 수 없다. 하지만 언제나 해결방안은 있는 법 ! 그것이 바로 async / await 다.

 

async / await 문법은 사용방법도 간단하다. 사용하려는 함수 앞에 async 키워드를 사용하고, 그 함수 내에서만 await를 사용하면 된다. 그렇게 작성된 코드는 await 키워드가 작성된 코드가 완료된 후에야 다음 순서의 코드가 동작하게 되는 것으로 앞선 방법들보다 더욱 쉽게 비동기적 코드를 제어할 수 있게 된다.

// 함수 선언식
async function funcDeclarations() {
	await 작성하고자 하는 코드
	...
}

// 함수 표현식
const funcExpression = async function () {
	await 작성하고자 하는 코드
	...
}

// 화살표 함수
const ArrowFunc = async () => {
	await 작성하고자 하는 코드
	...
}

 

async / await를 사용하면 promise chaining으로 길어진 코드를 훨씬 간결하고, 가독성 좋게 작성할 수 있는 것을 확인할 수 있다.

// 터미널에 `node index.js`입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
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');
};

// Promise chaining으로 작성한 코드
// const printAll = () => {
//   printString('A')
//     .then(() => {
//       return printString('B');
//     })
//     .then(() => {
//       return printString('C');
//     });
// };

printAll();

 

profile

내 맘대로 공부

@곰도리도리잼

잘못된 정보가 있다면 알려주세요 🧸