(SEB_FE) Section2 Unit3 [JS/Node] 비동기

PYM·2023년 3월 17일
0

(SEB_FE) SECTION2

목록 보기
4/19
post-thumbnail
  • 어떤 경우에 중첩된 콜백(callback)이 발생하는지 이해할 수 있다.
  • 중첩된 콜백(callback)의 단점, Promise의 장점을 이해할 수 있다.
  • async/await 키워드에 대해 이해하고, 작동 원리를 이해할 수 있다.

🏀동기와 비동기

카페에 가서 주문을 하는데, 앞 사람이 주문한 커피가 나올때까지 나는 주문조차 할 수 없다면 얼마나 불편하고 답답할까?

이렇게 Task A의 종료시점과 Task B의 시작시점이 같은, 즉 앞선 Task가 완전히 끝나기 전에는 다음 Task는 블로킹(blocking)당해 시작조차 할 수 없는, 다르게 말하면 앞선 Task가 완전히 끝나야만 다음 Task가 실행을 시작하는 상황을 '동기적(synchronous)이다.' 라고 한다.

컴퓨터에게도 마찬가지로 이는 답답하고 효율 떨어지는 방법이다. 그래서 Node.js를 만든 개발자가 논 블로킹(non-blocking)하고 비동기적(asynchronous)으로 작동하는 런타임을 개발하게 된다.

이러한 비동기적 실행은 웹개발에서 특히나 유용하게 쓰이는데, 우리가 사용하는 웹에서 다음과 같은 기능들은 비동기적으로 발생해야 한다.

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

백그라운드에서 무언가가 실행 중이면 다른 건 전혀 못한다던가, A가 로딩중일 경우 B는 전혀 이용할 수 없다던가, 큰 용량의 파일을 로딩하는 사이에는 아무것도 할 수 없다면, 그 웹은 매우 느리고 답답할 것이다.

🏅 동기(synchronous)

  • JavaScript의 동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것’을 의미

🏅 비동기(asynchronous)

  • JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미

  • 동기적 처리보다 훨씬 효율적이다. (당연함)

JavaScript는 싱글 스레드 기반으로 동작하는 언어이기 때문에 동기적으로 작동하지만, JS가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있는 것!

🏀타이머 관련 API

🏅 1. setTimeout(callback, millisecond)

  • 일정 시간 후에 함수를 실행한다.
  • 매개변수
    • callback: 실행할 콜백 함수
    • millisecond: 콜백 함수 실행 전 기다려야 할 시간 (밀리초)
  • return 값: 임의의 타이머 ID
setTimeout(function () {
  console.log('1초 후 실행');
}, 1000);
// 123

🏅 2. clearTimeout(timerId)

  • setTimeout 타이머를 종료시킨다.
  • 매개변수: setTimeout() 에서 리턴값으로 받은 타이머 ID
  • return값은 없다.
const timer = setTimeout(function () {
  console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨.

🏅 3. setInterval(callback, millisecond)

  • 일정 시간의 간격을 가지고 함수를 반복적으로 실행한다.
  • 매개변수(parameter)
    • callback: 실행할 콜백 함수
    • millisecond: 반복적으로 함수를 실행시키기 위한 시간 간격 (밀리초)
  • return 값: 임의의 타이머 ID
setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
// 345

🏅 4. clearInterval(timerId)

  • setInterval 타이머를 종료시킨다.
  • 매개변수: setInterval() 에서 리턴값으로 받은 타이머 ID
  • return값은 없다.
const timer = setInterval(function () {
  console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨.

비동기 코드는 코드가 작성된 순서대로 작동되는 것이 아니라 동작이 완료되는 순서대로 작동하게 됩니다. 즉, 코드의 순서를 예측할 수 없다.
➡ 개발자는 언제나 예측가능한 코드를 작성하도록 노력해야 함. 따라서, 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 한다.

🏀 Callback 함수로 비동기 코드 순서 제어하기

비동기로 작동하는 코드를 제어할 수 있는 여러 방법 중 하나는 바로 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();
// A B C가 순차적으로 출력된다. 

🏅 Callback Hell

Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.

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

const printAll = () => {
  printString('A', () => {
    printString('B', () => {
      printString('C', () => {
        printString('D', () => {
          printString('E', () => {
            printString('F', () => {
              printString('G', () => {
                printString('H', () => {
                  printString('I', () => {
                    printString('J', () => {
                      printString('K', () => {
                        printString('L', () => {
                          printString('M', () => {
                            printString('N', () => {
                              printString('O', () => {
                                printString('P', () => {});
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    });
  });
};

printAll();
  • 위와 같은, Callback Hell의 현상을 방지하기 위해 Promise 가 사용되기 시작!

🏀 Promise로 비동기 코드 순서 제어하기

  • Promiseclass이다. 즉, 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 객체는 state, result 내부 프로퍼티를 갖는다. 하지만 직접 접근할 수 없고 .then, .catch, .finally의 메서드를 사용해야 접근이 가능하다.

  • State

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

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

🏅 then, catch, finally

🏆 Then

executor에 작성했던 코드들이 정상적으로 처리가 되어 resolve 함수가 호출된 경우에, .then 메서드로 접근 가능

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

🏆 Catch

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

🏆 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이 가능한 이유는 .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를 반환
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));

// promiseAll을 사용하지 않은 코드 
// 같은 코드가 중복될 뿐만 아니라 총 6초가 걸린다. 
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 사용한 코드
// 이 경우 모든 프로미스가 동시에 실행되기 때문에 총 3초가 걸린다. 
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // ['1초', '2초', '3초']
  .catch((err) => console.log(err));
  • Promise.all()은 인자로 받는 배열에 있는 Promise
    하나라도 에러가 발생하게 되면 나머지 Promisestate와 상관없이 즉시 종료
// 아래의 코드는 에러1이 발생하고 난 뒤로는 더이상 작동하지 않고 종료된다.
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));
	// Error: 에러1

🏅 Promise Hell

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

🏀 Async/Await

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

  • 함수 앞에 async 키워드를 사용하고 async 함수 내에서만 await 키워드를 사용하면 된다.
    • 이렇게 작성된 코드는 await 키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 된다.
// 함수 선언식
async function funcDeclarations() {
	await 작성하고자 하는 코드
	...
}

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

// 화살표 함수
const ArrowFunc = async () => {
	await 작성하고자 하는 코드
	...
}
profile
목표는 "함께 일하고 싶은, 함께 일해서 좋은" Front-end 개발자

0개의 댓글