[JS] 비동기적 프로그래밍

Chanki Hong·2022년 12월 19일
0

JavaScript

목록 보기
16/30
post-thumbnail

비동기적 프로그래밍

  • 비동기적 프로그래밍이 필요한 이유는,
  • 사용자의 행동은 전적으로 비동기적이며, (사용자가 클릭, 터치, 타이핑 등을 할지 예상 할 수 없음)
  • JS의 본성 또한 비동기적이기 때문.
  • JS 애플리케이션은 단일 스레드에서 동작. (한 번에 한가지 일만 처리)
  • 컴퓨터의 성능이 좋기 때문에 단일 스레드는 A작업을 잠시 하고, B작업을 잠시 하고, C작업을 잠시 하는 식으로 멀티스레드를 흉내 낼 수 있음.
  • 싱글 스레드이기 때문에 일이 조금 제한적이지만, 멀티스레드 프로그래밍에서 겪는 문제를 신경 쓰지 않아도 됨.
  • 부드럽게 동작하는 소프트웨어를 만들기 위해서는 사용자의 입력뿐 아니라 여러 문제를 비동기적 관점에서 생각해야함.
  • JS의 비동기적 프로그래밍에는 세 가지의 패러다임이 존재하는데,
  • 콜백, 프라미스(promise), 제너레이터 순서.
  • 콜백은 비동기 처리 외에 이벤트 처리 등에서 유용함.
  • 비동기적 처리는 Ajax Web API 요청(서버쪽에서 데이터를 받을 때), 파일 읽기, 암호화/복호화, 작업 예약등에서 이용.

    바쁜 음식점예약하지 않고 방문한 경우를 생각하자.
    이런 상황에서 콜백내게서 전화번호를 받고 자리가 나면 알려주는 음식점과 비슷하다.
    프라미스내가 진동벨을 받고 자리가 나면 알려주는 음식점과 비슷하다.
    기다리는 동안 나와 음식점은 서로의 일을 할 수 있고, 어느 쪽도 서로 가만히 기다리지 않는다.

동기적(Synchronous) 프로그래밍

  • 우선순위를 정하고, 1번 부터 시작하여 작업이 끝날 때까지 나머지 코드는 실행 되지 않으며 준비상태로 기다림. (다른 작업 불가능)
function work() {
  const start = Date.now();
  for (let i = 0; i < 1000000000; i++) {}
  const end = Date.now();
  console.log(end - start + 'ms');
}
console.log('작업 시작!');
work();
console.log('다음 작업');
/*
작업 시작!
516ms
다음 작업
*/

비동기적(Asynchronous) 프로그래밍

  • 동기적 처리와 다르게 여러가지 코드를 실행.
  • 동시에 여러가지 작업을 처리 가능.
  • 기다리는 과정에서 다른 함수 호출 가능.
  • 코드를 실행할 때 흐름이 멈추지 않음.
function work() {
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();
    console.log(end - start + 'ms');
  }, 0);
  // 0ms 후에 실행되게 설정되지만,
  // 사실 4ms이후에 실행됨. (브라우저에서지정한 최소시간)
}
console.log('작업 시작!');
work();
console.log('다음 작업');
/*
작업 시작!
다음 작업
511ms
*/
  • 콜백을 이용해서 다시한번 더 살펴본다면,
function work(callback) {
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();
    console.log(end - start + 'ms');
    callback(end - start);
  }, 0);
}
console.log('작업 시작!');
work((ms) => {
  console.log('작업이 끝났어요!');
  console.log(ms + 'ms 걸렸다고 해요.');
});
console.log('다음 작업');
/*
작업 시작!
다음 작업
519ms
작업이 끝났어요!
519ms 걸렸다고 해요.
*/

콜백(callback)

  • CallBack Function(콜백 함수) 또 CallBack(콜백).
  • JS에서 가장 오래된 비동기적 메커니즘.
  • 간단히 말하면 나중에 호출할 함수. (특정 함수가 실행 된 후, 특정 시점에 도달했을 때 호출)
  • 제어권은 특정 함수에게 있음.
  • 일반적으로 다른 함수(파라미터)에 넘기거나 객체의 프로퍼티로 사용함. (드물게는 배열에 넣어서 사용)
  • 콜백은 보통 익명 함수로 사용.
  • setTimeout(), setInterval(), clearInterval() 은 모두 전역 객체(브라우저에서는 window, 노드에서는 global)에 정의되어 있음.

    콜백 함수는 프로그래밍에서 널리 사용되며, 비동기 프로그래밍 및 배열 내장 메서드에서 모두 사용될 수 있습니다. 그러나 이 두 맥락에서 콜백 함수의 역할과 동작 방식은 약간 다를 수 있습니다.

setTimeout()

  • setTimeout() 은 콜백의 실행을 지정된 ms(미리세컨드)만큼 지연하는 내장 함수.
  • 콜백을 한 번만 실행하고 멈춤.
console.log('timeout 실행 전: ' + new Date());

function f() {
  console.log('timeout 실행 후: ' + new Date());
}
setTimeout(f, 60 * 1000); // 1분

console.log('timeout 뒤에 실행');
console.log('timeout 뒤에 실행2');
/* 비동기적 실행의 결과.
timeout 실행 전: Mon Dec 26 2022 21:27:10 GMT+0900 (대한민국 표준시)
timeout 뒤에 실행
timeout 뒤에 실행2
timeout 실행 후: Mon Dec 26 2022 21:28:10 GMT+0900 (대한민국 표준시)
*/
/*
JS는 싱글 스레드를 사용하므로 만약 위의 예시에서 동기적으로 실행 하였다면,
60초 동안 컴퓨터가 대기 한 후 코드를 실행함.
그 과정에서 60초 동안 프로그램이 멈추고,
사용자의 입력을 받아들이지 않고,
화면도 업데이트하지 않음.
비동기적 테크닉은 이러한 일을 방지함.
*/
  • 비동기적 실행의 가장 큰 목적은 어떤 것도 차단하지 않는다는 것.
  • 일반적으로 콜백은 익명함수를 사용함.
console.log('timeout 실행 전: ' + new Date());

setTimeout(function () {
  console.log('timeout 실행 후: ' + new Date());
}, 60 * 1000); // 1분

console.log('timeout 뒤에 실행');
console.log('timeout 뒤에 실행2');

setInterval()clearInterval()

  • setInterval() 은 콜백을 정해진 주기마다 호출하며 clearInterval() 을 사용할 때까지 멈추지 않음.
// 분이 넘어가거나 10회째가 될 때까지 5초마다 콜백
const start = new Date();
let i = 0;

const intervalId = setInterval(() => {
  let now = new Date();
  if (now.getMinutes() !== start.getMinutes() || ++i > 10) {
    return clearInterval(intervalId);
  }
  console.log(`${i}: ${now}`);
}, 5 * 1000);

/*
1: Mon Dec 26 2022 21:50:12 GMT+0900 (대한민국 표준시)
2: Mon Dec 26 2022 21:50:17 GMT+0900 (대한민국 표준시)
3: Mon Dec 26 2022 21:50:22 GMT+0900 (대한민국 표준시)
4: Mon Dec 26 2022 21:50:27 GMT+0900 (대한민국 표준시)
5: Mon Dec 26 2022 21:50:32 GMT+0900 (대한민국 표준시)
6: Mon Dec 26 2022 21:50:37 GMT+0900 (대한민국 표준시)
7: Mon Dec 26 2022 21:50:42 GMT+0900 (대한민국 표준시)
8: Mon Dec 26 2022 21:50:47 GMT+0900 (대한민국 표준시)
9: Mon Dec 26 2022 21:50:52 GMT+0900 (대한민국 표준시)
10: Mon Dec 26 2022 21:50:57 GMT+0900 (대한민국 표준시)
*/

스코프와 비동기적 실행의 혼란

  • 스코프(클로저)는 비동기적 실행에 영향을 미치고 이 과정은 혼란을 줌.
function countdown() {
  let i;
  console.log('Countdown');
  for (i = 5; i >= 0; i--) {
    setTimeout(() => {
      console.log(i === 0 ? 'GO!' : i);
    }, (5 - i) * 1000);
  }
}
countdown();
/*
Countdown
-1
-1
-1
-1
-1
-1
*/
  • 동기적으로 실행됐기 때문에 (5 - i) * 1000 의 부분은 정상작동 하지만,
  • setTimeout 에 전달된 함수는 비동기적으로 실행 되기 때문에 정상 작동하지 않음. (setTimeout 은 동기적으로 작동함)
  • 이 문제는 ifor 루프에서 선언하는 방식으로 해결 가능.
function countdown() {
  console.log('Countdown');
  for (let i = 5; i >= 0; i--) {
    setTimeout(() => {
      console.log(i === 0 ? 'GO!' : i);
    }, (5 - i) * 1000);
  }
}
countdown();
/*
Countdown
5
4
3
2
1
GO!
*/
  • 주의할 부분은 콜백은 자신이 선언된 스코프(클로저)에 접근 가능하다는 점. (모든 비동기적 테크닉에 적용)

콜백 지옥(콜백 헬)

  • 콜백으로 비동기적 실행을 하지만, 현실적인 단점이 존재.
  • 콜백이 많이 중첩될 때,
function increaseAndPrint(n, callback) {
  setTimeout(() => {
    const increased = n + 1;
    console.log(increased);
    if (callback) {
      callback(increased);
    }
  }, 1000); // 1초에 한번씩 작동.
}

increaseAndPrint(0, (n) => {
  increaseAndPrint(n, (n) => {
    increaseAndPrint(n, (n) => {
      increaseAndPrint(n, (n) => {
        increaseAndPrint(n, (n) => {
          console.log('작업 끝!');
        });
      });
    });
  });
});
  • 이러한 콜백 헬은 버그 등에서 관리가 어려움.
  • 그래서 프라미스가 등장.

Promise

  • 프로미스는 JavaScript의 비동기 작업을 처리하는 객체임.
  • 콜백의 단점을 보완하고, 비동기 작업을 편하게 처리하기 위해 ES6에 도입.
  • 콜백을 대체하는 것은 아니며, 프라미스에서도 콜백을 사용함.
function countdown(seconds) {
  return new Promise(function (resolve, reject) {
    for (let i = seconds; i >= 0; i--) {
      setTimeout(function () {
        if (i > 0) console.log(i + '...');
        else resolve(console.log('GO!'));
      }, (seconds - i) * 1000);
    }
  });
}
  • 프라미스 기반의 비동기적 함수를 호출하면, 그 함수는 Promise 인스턴스를 반환.
  • 프라미스는 성공(fullfilled), 실패(rejected) 두가지 상태를 가짐.
  • 프라미스가 성공 또는 실패하면 그 프라미스는 결정됨(settled)을 의미. (단 한번만 일어남)
  • Promise 생성자를 사용해 프로미스 생성.
const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행하고
  // 성공하면 resolve 호출, 실패하면 reject 호출
});

then 메서드

  • 성공 콜백과 에러 콜백 두가지를 인수로 받을 수 있음.
  • 일반적으로 첫 번째 인수로 성공 콜백을 두 번째 인수로 실패 콜백을 받음.
  • 그러나 실패 콜백을 생략하고, catch 메서드로 오류를 처리.
// 초를 매개변수로 받아 카운트다운히고,
// 끝나면 프라미스를 반환.
function countdown(seconds) {
  return new Promise(function (resolve, reject) {
    for (let i = seconds; i >= 0; i--) {
      setTimeout(() => {
        if (i === 13) return reject(new Error('Oh my god'));
        if (i > 0) console.log(i + '...');
        else resolve(console.log('GO!'));
      }, (seconds - i) * 1000);
    }
  });
}

// then 핸들러는 성공 콜백과 에러 콜백을 받음.
countdown(5).then(
  function () {
    console.log('카운트다운 성공');
  },
  function (err) {
    console.log('카운트다운 실패: ' + err.message);
  }
);

catch 메서드

  • 실패(에러) 콜백을 받을 수 있음.
// 초를 매개변수로 받아 카운트다운히고,
// 끝나면 프라미스를 반환.
function countdown(seconds) {
  return new Promise(function (resolve, reject) {
    for (let i = seconds; i >= 0; i--) {
      setTimeout(() => {
        if (i === 13) return reject(new Error('Oh my god'));
        if (i > 0) console.log(i + '...');
        else resolve(console.log('GO!'));
      }, (seconds - i) * 1000);
    }
  });
}
const p = countdown(15);
// then 핸들러는 성공 콜백을 받고,
p.then(function () {
  console.log('카운트다운 성공');
});
// catch 핸들러는 실패를 받음.
p.catch(function (err) {
  console.log('카운트다운 실패: ' + err.massage);
});
myPromise
  .then((result) => {
    // 성공한 경우
  })
  .catch((error) => {
    // 실패한 경우
  });

0개의 댓글