[Javascript] Sync / Async

Bik_Kyun·2022년 4월 24일
0
post-thumbnail

1. Javascript

Javascript라는 프로그래밍 언어의 동기 비동기 개념을 한번 알아보자.
(자바스크립트 문법 먼저 공부해야하는데 이게 갑자기 궁금해서 정리해본다)
자바스크립트는 Synchronous이고, Blocking이며, Single-threaded한 언어이다.
만? 이것은 오직 한 연산에서의 특성을 의미하며, 모든 것에서의 특성을 의미하지는 않는다.

✅ 시작 전 참고

Blocking vs Non-Blocking

  • Blocking : 프로세스가 시스템을 호출하고 나서 결과가 반환되기까지 다음 처리로 넘어가지 않음.
  • Non-blocking : 시스템을 호출한 직후에 프로그램으로 제어가 다시 돌아와서 시스템 호출의 종료를 기다리지 않고 다음 처리로 넘어갈 수 있음.

동기(Sync) vs 비동기(Async)

  • 동기(Synchronous) : 스레드에 작업을 맡긴 후 그 작업이 끝날 때까지 기다렸다가 다음 작업을 시작하는 것을 말한다.
  • 비동기(Asynchronous) : 스레드에 작업을 맡긴 후 기다리지 않고 다음 작업을 하는 것을 말한다.
  • 스레드(Thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다.
    Sync/Async/Blocking/Non-Blocking 정리

동기 비동기 개념은 컴퓨터가 작업을 처리하는 작업 방식과도 연관이 있다. 작업 방식으로는 직렬처리와 동시처리가 있다.

직렬(Serial) 처리 : 다른 하나의 스레드에 분산처리를 맡기는 경우.
동시(Cocurrent) 처리 : 다른 여러 개의 스레드에 분산처리를 맡기는 경우.

여기서 우리는 의문점이 생길 수 있다.
스레드가 여러개 있으면 무조건 분산처리해서 나눠서 작업하는게 효율적인데 왜 굳이 직렬처리 방식이 있는가? 에 대한 의문이다.
하지만 조금만 생각해보면 그 답은 쉽게 유추할 수 있다. 프로그래밍을 할 때 순서대로 처리해야하는 경우가 필수적으로 존재한다. 그렇기 때문에 직렬처리도 필요한 것이다. 그래서 결론적으로 직렬 처리는 순서가 중요한 작업을 처리할 때 유용하고 동시 처리는 각자 독립적이지만 유사한 여러개의 작업을 처리할 때 사용한다.

Javascript의 동작 원리

Javascript는 Single-Threaded 런타임을 가지고 있다.
Single-Thread란 하나의 프로그램은 하나의 코드만 실행할 수 있다는 것을 의미한다.
이것은 결국 싱글 콜 스택(call stack)을 의미하는 것이다.

call stack의 개념을 알기 위해서는 Javascript의 엔진 구성요소부터 살펴볼 필요가 있다.
Javascript의 엔진은 Memory HeapCall Stack으로 이루어져 있다.

Javascript 엔진 구성

Memory Heap : 변수와 객체의 메모리 할당을 담당하는 곳.
Call Stack : 함수가 호출되면 쌓이는 곳이다. 함수가 쌓이는 순서와는 반대로 실행된다.

즉, call stack은 먼저 들어온 것이 먼저 나간다의 개념으로 선입선출로 호출되어 나가게 된다.

2. 비동기의 개념은 왜 필요한가?

지금까지 javascript의 동작 원리를 알아보았다. javascript는 기본적으로 Synchronous Single-Threaded 프로그래밍 언어라고 알아보았다. 그럼 웹 프로그래밍에 javascript를 사용할 때 비동기 개념이 왜 필요한가 알아보자.

사용자들이 웹 페이지를 검색했을 때 페이지 소스를 기다리며 로딩이 걸리게 된다. 하지만 javascript로 이루어진 페이지가 있을 때 동기식 싱글 스레드 방식으로 페이지 소스를 기다리게 된다면 사용자는 응답이 없는 화면을 몇초간 심하면 수십 초간 기다려야 하는 상황이 발생한다. 이러한 단점들 때문에 비동기가 필요한 것이다.

어떻게 Javascript가 브라우저에서 비동기적인 처리가 가능한가?

Javascript Runtime 환경은 운 좋게도 브라우저에서의 자바스크립트 실행 환경(Runtime)에서는 자바스크립트 엔진 자체가 제공하지 않는 일부 기능인 DOM 조작이나 AJAX 같은 비동기 처리를 위한 web API를 제공한다. 또, 이를 제어하기 위해 이벤트 루프(Event Loop), 이벤트 큐(Callback Queue 혹은 task Queue)가 존재한다.

3. 근데 왜 Javascript를 비동기 언어라고 착각하나

사람들이 JavaScript가 비동기 언어라고 흔히 오해하는 이유는, 우리는Javascript가 비동기식으로 동작하도록 조작할 수 있기 때문이다. 그 방법에 대해 살펴보자.

1) 비동기적 Callback

동기식 세계에서 가장 간단한 해결책은 비동기 Callback을 사용하는 것이다.
데이터베이스 요청을 예로 들어, 비동기 Callback은 데이터베이스에 요청을 보내는 callback 함수(또는 다른 중첩된 Callback 함수를 발생시킬 수 있도록 한다. 그 함수가 데이터베이스로부터 응답을 기다릴 동안, 남은 코드를 자유롭게 실행할 수 있다.
일단 데이터베이스 요청이 끝나면, 그 결과(또는 다른 중첩된 코드)는 에 보내진 다음, 이벤트 루프를 통해 처리된다. Function C, E, F, G는 모두 브라우저, 큐, 이벤트 루프에 보내진다.
이는 좋은 해결책이긴 하지만, 아직 문제점이 존재한다. Function C가 언제 해결될지 정확히 예측할 수 없기 때문에, 모든 종속 함수를 C안에 중첩시켜야한다. 결국 많은 중첩 함수가 생겨 코드가 복잡해지는 것이 바로 Callback Hell이다.
ex)

function delay(callback) {
  setTimeout(callback, 1000);
}

let arr = [];

delay(() => {
  console.log(1)
  arr.push(1);
  delay(() => {
    console.log(2)
    arr.push(2);
    delay(() => {
      console.log(3)
      arr.push(3);
      delay(() => {
        console.log(4)
        arr.push(4);
        delay(() => {
          console.log(5)
          arr.push(5);
          delay(() => {
            console.log(arr);
          })
        })
      })
    })
  })
})
// 1
// 1초 뒤 2
// 1초 뒤 3
// 1초 뒤 4
// 1초 뒤 5
// 1초 뒤 마지막으로 [1,2,3,4,5] 로그가 찍힘

2) Promise 객체

내용은 실행되었으나 결과를 아직 return하지 않은 객체

Promise는 어떤 값이 생성 되었을 때 그 값을 대신하는 대리자이다.
비동기 연산이 종료된 이후에 그 결과 값이나 에러를 처리할 수 있도록 처리기를 연결하는 역할을 하는 객체이다. 결과 값이나 에러 처리는 바로 알 수 없고, Promise라는 객체에 남겨 두었다가 그 다음 어떤 시점에서 결과를 사용할 수 있도록 한다.

  • callback 함수와 달리 Promise 객체는 비동기적으로 처리할 수 있지만 코드를 볼 때는 상 → 하의 흐름으로 읽을 수 있어 가독성이 더 좋다. 또 비동기에 대한 에러 처리 또한 할 수 있다.
  • Promise instance에 어떠한 값(성공했을 때는 성공 값-resolve, 그렇지 않았을 때는 에러 값-reject)을 담아 두었다가 .then이라는 method를 사용해서 비동기 처리 후 진행되어야 하는 순서를 컨트롤해준다. 프로미스 인스턴스에 담겨 있던 어떠한 값은 .then 뒷 부분의 전달 인자로 주어진다.
  • Resolve(성공값) : .then을 사용하면 결과를 return 한다.
  • Reject(실패값) : .catch로 연결된다.
  • Finally : 해당 부분은 무조건 실행된다.
  • 이같은 방법은 더 module 지향적이며, 읽기 쉽다는 비동기적 프로그래밍의 장점을 포함한다.

ex)

        function deplayP(n) {
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve(n);
            }, 1000);
          });
        }
        
        delayP(1).then(result => {
          console.log(result);
          return delayP(2);
        }).then(result => {
          console.log(result);
          return delayP(3);
        }).then(result => {
          console.log(result);
          return delayP(4);
        }).then(result => {
          console.log(result)
          return delayP(5);
        }).then(result => {
          console.log(result)
        })
        //1부터 1초씩 지나면서 1, 2, 3, 4, 5가 로그에 찍힌다.

3) Async & Await

Async & Await은 비동기 코드를 동기식으로 표현하는 더 나은 방법으로 ES2017에 등장하였다. 그 사이에도 Promise의 장황한 구조를 대체하기 위해 ES2016 generator 등의 노력이 있었다고 한다. 그 결과 더 명료한 함수 표현이 가능해졌다.

  • Async는 await과 항상 함께 한다. Await 모드는 Promise 객체를 받아 처리하고, 만약 비동기 함수가 아닌 동기적 함수라면 리턴 값을 그대로 받는다.
    Async 함수는 Promise가 없으면 의미가 없다. Promise 객체를 통해 비동기적으로 처리된 내용을 동기적인 코드 진행 순서로 보여주는 역할을 하기 때문이다.

ex) Async 함수를 이용해 비동기적으로 1,2,3,4,5 로그 찍기
이번에는 for loop로 돌려봤다. 별 다른 차이는 없고 for loop 안에 await 부분이 중요하다. 프로미스 객체를 리턴하는 delayP 함수의 값을 await 모드로 받은 것이 result 이다. result에 값이 담기기 전에는 그 다음 console.log 부분은 진행되지 않는다.(비동기 제어)

        function delayP(n) {
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve(n);
            }, 1000);
          });
        }
        async function myAsync() {
          for(let n = 1; n < 6; n++) {
            let result = await delayP(n);
            console.log(result);
          }
        };
        myAsync();
        //1부터 1초씩 지나면서 1, 2, 3, 4, 5가 로그에 찍힌다.

4) 그럼 무조건 await가 답인가?

이렇게 순차적으로 보면 async-await이 callback이나 Promise 방식보다 훨씬 좋아 보이지만, 그럼에도 이 3가지 방식은 용도에 맞춰서 적절히 사용해야 한다. callback은 별 다른 키워드 없이도 구현할 수 있는 문법이기 때문에 콜백 지옥을 맞이할 정도의 복잡한 상황이 아닐 때 사용하면 좋다. callback 외 Promise나 Async-await 방식은 Promise 인스턴스로 전환되었다가 다시 돌아오는 작업을 거쳐야 하기 때문에 이 과정을 굳이 하지 않아도 되는 상황까지 고려했을 때 callback도 때에 따라선 좋은 답이 될 수 있다.

profile
비진

0개의 댓글