JavaScript | 동기와 비동기

이진웅·2021년 12월 19일
0

JavaScript

목록 보기
8/8
post-thumbnail

동기 (Synchronous)

JavaScript는 기본적으로 동기적으로 작동한다.
동기적으로 작동한다는 것을 풀어서 설명한다면, 문서에서 var로 선언한 변수나 함수가 호이스팅 되서 최상단으로 끌어올려진뒤 위에서 아래로 순차적으로 작동된다는 것이다.

동기 방식은 하나의 작업이 완료된 후에 다음 작업으로 넘어가기 때문에 작업 처리 시간이 오래 걸려서 비효율적이다. 이렇게 되면 하나의 웹페이지가 모든 데이터를 다운 받아야 열리는 참사가 일어날 것이다.

비동기 (Asynchronous)

동기 방식의 단점을 극복할 수 있는 방법이 바로 비동기이다.
비동기 방식은 여러 작업을 동시에 시작할 수 있게끔 해준다.

동기방식과 같이 위에서 아래로 내려오면서 작업이 진행되지만, 완료되지 않아도 다음 작업으로 내려가서 진행한다.
이렇게 되면 웹페이지를 우선 구현해놓은 후 하나씩 다운받은 요소들이 활성화 될 것이다.
마치 내려오면서 스위치를 하나씩 키는 느낌이라고 생각하면 이해가 빠르게 될 것 같다.

비동기 방식을 구현하는 방법은 3가지가 있는데
바로 callback Promise async 이다

callback()

콜백은 함수 속에 인자로 함수를 받았을 때를 말하는데, 인자로 받은 함수가 완료되어야 함수가 완료되기 때문에 인자로 받은 함수로 리턴시점을 컨트롤 할 수 있다. 즉 동기 비동기 방식을 선택 할 수 있다는 뜻이다.

// Synchronous callback
function printImmediately(print) {
  print();
}
printImmediately(() => console.log('hello')); // 이 함수는 바로 결과값이 출력 되지만

// Asynchronous callback
function printWithDelay(print, timeout) {
  setTimeout(print, timeout);
}
printWithDelay(() => console.log('async callback'), 2000); // 이 함수는 2초뒤에 출력된다

Callback hell

이런 콜백함수를 남용하게 되면 콜백지옥에 빠지게 되는데, 가독성이 떨어지기 때문에 함수가 올바르게 작동한다고해도 유지보수가 어렵기 때문에 이 방법은 지양하는 것이 좋다.

Promise()

위의 콜백지옥에서 벗어날 수 있는 방법 중 하나는 바로 프로미스를 사용하는 것이다
프로미스는 비동기적인 메서드를 동기적으로 사용할 수 있게 도와주며 이름 그대로 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)을 반환한다.

프로미스는 항상 pending , fulfilled , rejected 3가지 상태 중 하나를 가지게된다.

pending: 이행하지도, 거부하지도 않은 초기 상태
fulfilled: 연산이 성공적으로 완료됨
reject: 연산이 실패함

그리고 데이터를 생성하는 Producer와 생성된 데이터를 사용하는 Consumer로 분류 할 수 있다.

Producer

프로듀서는 비동기적인 함수를 생산하고, 완료 됐을 때 결과 값을 반환하는 데이터를 만들어 낸다

const promise = new Promise((resolve, reject) => {
	// doing some heavy work (network, read files)
	console.log('doing something...'); // (1)
	setTimeout(() => {
		resolve('ellie');
	}, 2000); // (2)
});

프로미스는 클래스로 구분되기 때문에 클래스처럼 선언해서 사용하면 된다.
예시를 기준으로 본다면 먼저 (1)의 내용이 먼저 출력된 뒤, 2초(2000ms) 후에 (2)의 내용이 출력 될 것이다.

Consumer

컨슈머는 단어 뜻대로 프로듀서가 생산한 것을 소비한다.
then , catch , finally 3가지의 방법으로 받아온 데이터를 이용 할 수 있다.

promise
  .then((value) => {
    console.log(value);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log('finally');
  });

then 은 제대로 데이터를 받아왔을때 어떻게 할 것인가?
catch 는 데이터를 받아오지 못했을때 어떻게 할 것인가?
finally 는 이후 최종적으로 어떤 것을 할 것인가?

위 3가지 방법을 각 상황에 맞춰 사용하면 된다.

async & await

프로미스도 많이 사용하면 가독성이 떨어질 수 있어 더 깔끔한 문법인 에이싱크를 사용한다고 한다.
사용방법은 함수 선언을 할 때 앞에 async 를 붙이고, 비동기적으로 관리할 함수에 await 를 붙여주면 된다고한다.

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getApple() {
  await delay(2000);
  return '🍎'; 
}

async function getBanana() {
  await delay(1000);
  return '🍌'; 
}



function pickFruits() { // promise만 사용
  return getApple().then((apple) => {
    return getBanana().then((banana) => `${apple} + ${banana}`);
  });
}

async function pickFruits() { // async와 await 사용
  const applePromise = getApple();
  const bananaPromise = getBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

아래의 두 함수는 같은 기능을 하지만 가독성 문제로 아래의 async , await 조합을 사용하는 것이 좋아보인다.

Promise.all()

async function main() { // await를 사용한 예
  const startTime = new Date();
  const id = 0; 
  let name, age, gender; 
  
  name = await getName(id); // 4.2초 걸리는 작업
  age = await getAge(id); // 2.7초 걸리는 작업
  gender = await getGender(id); // 5.6초 걸리는 작업
    
  const endTime = new Date();
  
  console.log(`${(endTime - startTime) / 1000}s`); // 12.507s
  
  return {
    name,
    age,
    gender,
  };
  
}
async function main() {
  const startTime = new Date();
  const id = 0; 
  let name, age, gender; 
      
  const result = await Promise.all([getName(id), getAge(id), getGender(id)]); 
  // Promise.all 메서드를 활용한 예
  
  const endTime = new Date();
  
  console.log(`${(endTime - startTime) / 1000}s`); // 5.602s
  
  return {
    name: result[0],
    age: result[1],
    gender: result[2],
  };
}

하지만 async 가 제일 좋다는 것은 아니다 .

첫번째 예를 보면 모든 작업에 async 를 주었더니 작업이 끝나야 다음 작업으로 넘어가기 때문에 약 12.5초가 소요된다. 비동기 속 동기적인 작업을 수행하는 것이다.

하지만 두번째 예인 Promise.all() 메서드를 활용하면 병렬적으로 모든 작업을 동시에 시작하기 때문에 작업시간이 가장 긴 함수 getGender의 작업시간 5.6초만 소요된다.

이처럼 하나의 방식만 사용하기 보다는 상황에 맞게 다양한 방법을 활용하는 것이 좋을것 같다.

0개의 댓글