JS NORMAL | 동기 / 비동기, 블로킹 / 논블로킹

chaen·2024년 1월 25일
0

JS Grammar

목록 보기
21/28
post-thumbnail

💻 싱글 스레드 JS와 멀티 스레드 브라우저

모든 코드를 순차적으로 실행하면 코드 실행의 순서가 보장되지만, 중간에 오랜 시간이 소요되는 코드가 있다면 전체적인 실행이 지연될 것이다.
Cc# 등의 언어는 멀티 쓰레드로 동기적 방식의 문제를 해결하지만, 자바스크립트는 쓰레드가 하나밖에 없다.
따라서 자바스크립트는 '비동기' 라는 개념을 통해 이 문제를 해결한다.

자바스크립트의 쓰레드가 특별한 기능을 가지고 있는 게 아니기 때문에, 자바스크립트는 동기적으로 코드를 실행하다가 비동기 함수를 만나면 웹 브라우저의 별도영역인 웹 APIs에게 처리를 부탁한 후 기다리지 않고 다음 코드를 실행한다. 웹 APIs는 처리가 끝나면 자바스크립트에게 코드를 돌려주고, 그제서야 비동기 함수는 실행이 된다.

웹 어플리케이션은 작업을 멀티로 처리해야 하는 경우가 부지기수이다. 만약 브라우저가 한 번에 하나씩만 동작하게 된다면 인터넷 세상은 돌아가지 않을 것이다.

따라서 오래 걸리고 반복적인 작업은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레드인 Web APIs, Timer, Event, Render 등을 사용하여 비동기(Async) 방식논블로킹(non-blocking) 실행을 통해 효율적으로 처리된다.

이로 인해 JS는 싱글 스레드 구조를 유지하면서도 멀티 작업 처리가 가능한 것처럼 보인다.


❓ 동기와 비동기

여러 개의 요청 작업을 순차적으로 처리하느냐 아니냐에 따라서 동기와 비동기로 나뉜다.

1. 동기 (Synchronous)

동기 통신: 전체 페이지를 로딩하는 방식으로, 비동기 통신 이전에 사용되었다.

console.log("first");
console.log("second");
console.log("third");

장점

  • 요청과 응답 값의 순서, 처리 결과 값을 보장한다.

단점

  • 서버 부하가 커지고 시간이 오래 걸린다.
  • 로딩하는 시간동안 다른 데이터 처리가 불가능하다

2. 비동기 (Asynchronous)

비동기 통신: 반응을 기다리지 않고 Non Block 상태로 계속하여 일을 진행하는 방식
Ajax Web API 요청, 파일 읽기, 암호화, 작업 예약 등의 경우 비동기를 사용한다.
자바스크립트에는 비동기로 동작하는 비동기 전용 함수가 있으며 대표적으로 setTimeout, setInterval 이나 fetch API, XMLHttpRequest, addEventListener 가 있다.

console.log("first");
setTimeout(()=>{
  console.log("second");}, 0)
console.log("third");

장점

  • 자원 사용이 효율적, 성능 향상

단점

  • 순서를 보장하지 않음
  • Response에 대한 처리 결과를 보장받고 처리해야 하는 서비스에는 적합하지 않음

💻 비동기 함수 방식

자바스크립트에서 비동기 함수를 실행한 후, 이어지는 결과를 처리하기 위해서 콜백 함수, Promise, async await 이렇게 크게 3가지를 사용한다.

1. 콜백(Callback) 함수

  • 함수 안에서 어떤 특정한 시점에 호출되는 함수
  • 다른 함수에 파라미터로 넘겨지는 함수
  • 비동기 처리를 위해 콜백 함수를 여러 번 중첩해서 사용한다면 해당 코드는 가독성과 유지보수가 어려워지며, 이를 콜백 지옥이라고 부른다.
step1(function(value1) {
  step2(function(value2) {
    step3(function(value3) {
      step4(function(value4) {
        step5(function(value5) {
          step6(function(value6) {
            // Do something with value6
          })
        })
      })
    })
  })
})

카페를 주제로 한 예시이다.

const cafe = function (drink, callback) {
  const handleResult = (result) => {
    console.log(result);
    callback(null, result);
  };

  const handleError = (error) => {
    console.log(error);
    callback(error, null);
  };

  if (drink === 'coffee') {
    handleResult('Here you go');
  } else {
    handleError(`There isn't it here`);
  }
};

cafe('the drink', (error, result) => {
  if (error) {
    console.log('Error:', error);
  } else {
    console.log('Result:', result);
  }
});                                                               

callback(arg,arg2)에서 null이 있는 이유

콜백 함수는 보통 첫 번째 매개변수로 에러를 받고, 두 번째 매개변수로 성공 또는 처리 결과를 받기 때문이다.

따라서 첫 번째 코드에서는 (성공) 에러자리에 null, 성공자리에 결과를 반환하고, 두 번째 코드 (실패) 에서는 에러 자리에 결과, 성공 자리에 null을 반환한다.


2. Promise

  • 비동기 작업을 조금 더 효율적으로 처리할 있도록 도와주는 내장 객체
  • 비동기 작업들을 감싸는 객체로, 감싸고 있는 비동기 작업을 실행하거나, 상태를 관리하거나, 저장하는 등 비동기 작업에 필요한 거의 모든 기능을 제공해주는 객체
  • 콜백 함수의 error, success의 처리를 보다 간단하게 하기 위해 Promise가 도입되었다.
  • 비동기 작업이 끝날 때까지 결과를 기다리는 것이 아니라, 결과를 제공하겠다는 '약속'을 반환한다는 의미에서 Promise라 명명 지어졌다고 한다. 
  • Promise의 기본 형태
const promise = new Promise ((resolve, reject) => {
	//executor: 비동기 작업을 실행하는 함수
})

Promise의 세 가지 상태

Pending(대기) : 처리가 완료되지 않은 상태 (처리 진행중), 초기 상태
Fulfilled(이행) : 성공적으로 처리가 완료된 상태
Rejected(거부) : 처리가 실패로 끝난 상태

resolve vs reject

executor는 자동으로 실행되며, 작업에 성공 여부에 따라 resolvereject를 호출한다.

resolve : 만약 요청이 성공하여 데이터가 있다면, 그 결과를 나타내는 value와 함께 호출한다.
reject : 만약 요청이 실패한다면, 에러 객체를 나타내는 error와 함께 호출한다. 그 뒤의 내용은 실행하지 않고 코드를 종료한다.

프로미스 핸들러

이렇게 선언된 Promise 객체는 성공과 실패에 대한 후속 처리를 진행할 수 있다.

resolve를 호출하여 성공했다면, .then()메소드가 실행된다. 매개변수로는 resolve의 인수를 받는다.

reject를 호출하게 되면, .catch()메소드를 사용하여 에러 처리를 진행한다. <- reject와 달리 .catch 구문 외 그 뒷 내용을 계속하여 실행한다.

이행/거부와 상관없이 실행할 콜백함수는 .finally()이며, 이는 Promise 체인의 마지막, 딱 한번 실행해야 한다.

// 기본구조
// 프로미스 객체를 반환하는 함수 생성
function myPromise(arg) {
  return new Promise((resolve, reject) => {
    // 비동기적인 작업을 할 executor 함수
    setTimeout(() => {
     if (/* 성공 조건 */) {
       resolve(/* 결과 값 */);
     } else {
       reject(/* 에러 값 */);
     }
    }, 2000);
  });
}

// 프로미스 객체를 반환하는 함수 사용
myPromise(arg)
    .then((result) => {
      // 성공 시 실행할 콜백 함수
    });
myPromise(arg)
    .catch((error) => {
      // 실패 시 실행할 콜백 함수
    });

프로미스 체이닝

then() 메서드의 경우 Promise 객체 그 자체를 다시 반환한다. 따라서 아래와 같이 thencatch를 따로 쓸 필요가 없이, then 뒤에 catch를 바로 붙여쓸 수 있다.

promise
  .then((value)=>{});
promise // 이 promise는 위의 promise.then 구절과 그냥 동일하다.
  .catch((value)=>{});
// 따라서 위 대신 아래처럼 쓸 수 있다.
promise
  .then((value)=>{})
  .catch((value)=>{});

이 기능을 프로미스 체이닝이라고 한다.

프로미스 체이닝이란, 프로미스 핸들러를 연달아 연결하는 것을 말한다. 이렇게 하면 여러 개의 비동기 작업을 순차적으로 수행할 수 있다는 특징이 있다.

function doSomething() {
  return new Promise((resolve, reject) => {
      resolve(100);
  });
}

doSomething()
    .then((value1) => {
        const data1 = value1 + 50;
        return data1;
    })
    .then((value2) => {
        const data2 = value2 + 50;
        return data2;
    })
    .then((value3) => {
        const data3 = value3 + 50;
        return data3;
    })
    .then((value4) => {
        console.log(value4); // 250 출력
    })

위의 콜백 함수 코드를 promise 를 사용하여 바꾼 예시이다.

const cafe = function (drink){
  const promise = new Promise((resolve,reject) => {
    if (drink == 'coffee') {
      resolve('Here you go');
    } else {
      reject(`There isn't it here`);
    }
  });  
  return promise;
  };
                     
cafe('the drink')
  .then((res) => console.log(res))
  .catch((rej) => console.log(rej))
  .finally(() => console.log('Thank you'));

또한 카페 코드를 더 발전시킨 예시이다.

const orderDrink = function (drink) {
  return new Promise((resolve, reject) => {
    if (drink === 'coffee' || drink === 'latte') {
      resolve(`Preparing ${drink}...`);
    } else {
      reject(`We don't serve ${drink} here`);
    }
  });
};

const addSugar = function (drink) {
  return new Promise((resolve) => {
    resolve(`${drink} with sugar added.`);
  });
};

const serveDrink = function (message) {
  return new Promise((resolve) => {
    resolve(`${message} Here's your drink!`);
  });
};

// 프로미스 체이닝
orderDrink('latte')
  .then((message) => addSugar(message))
  .then((messageWithSugar) => serveDrink(messageWithSugar))
  .then((finalMessage) => console.log(finalMessage))
  .catch((error) => console.log(`Error: ${error}`))
  .finally(() => console.log('Thank you!'));

순서 생각해보기

let myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

myPromise.then((successMessage) => {
  return "Yay! " + successMessage;
}).then((msg) => {
  console.log(msg);
}).then(() => {
  console.log('finished');
});
  1. Promise 생성자가 실행됨
    • executor(첫 번째 인자로 준 함수)가 즉시 실행되고, 내부에서 setTimeout을 webAPI에 타이머(매크로태스크)로 등록.
  2. 생성자가 myPromise 변수에 저장됨
  3. .then 체인 등록(동기)
    • myPromise.then(...), .then(...), .then(...) 세 개의 핸들러가 순서대로 등록됨
  4. 매크로태스크 - 1초 뒤 타이머 콜백 실행
    • setTimeout 콜백에서 resolve("Success!") 호출 → 프로미스가 fulfilled로 전이
    • 이 순간, 첫 번째 .then 핸들러가 마이크로태스크 큐에 enqueue 됨 (준비가 완료되었을 때 큐에 등록)
  5. 마이크로태스크 – 첫 번째 .then
    • 첫 번째 .then 실행: "Yay! " + successMessage 문자열을 반환.
    • 반환값은 자동으로 Promise.resolve(반환값)으로 래핑되어 다음 .then으로 전달됨.
    • 이어서 두 번째 .then 핸들러가 또 마이크로태스크 큐에 enqueue 됨
  6. 두 번째 .then 실행 -> 세 번째 .then 실행
  • 오류가 생길 경우 .then 을 실행하지 않고 catch 실행

3. Async / awit

async/await는 프로미스를 더 직관적으로 사용할 수 있게 만든 문법상의 설탕(Syntactic Sugar)이다. 내부적으로는 Promise를 사용하여 처리하지만, 코드 작성을 더 용이하게 만들어준다. (유지보수성 향상)
간결한 코드로 비동기 작업을 마치 동기작업을 실행하듯 작성할 수 있다.

이는 prototypeclass 문법의 차이라고도 볼 수 있다.
자바스크립트는 클래스 형식의 문법을 지원하지만, 클래스의 내부는 여전히 프로토타입 형식으로 처리된다.

async

  • 비동기 로직을 포함하는 함수 앞에 붙임
  • 붙이게 되면 함수가 단순히 결과값을 반환하는 게 아닌 이 결과값을 갖는 새로운 Promise를 반환하는 함수가 됨 (함수를 console.log하면 Promise가 출력됨)
  • 함수 자체가 원래부터 return new Promise() 로 프로미스를 반환하는 함수였다면 별다른 기능을 하지 않고 Promise 기능 자체를 반환하도록 내버려둔다.
  • 함수 내부에서 await가 사용 가능하게 함

await

  • async 키워드를 붙인 함수 내부에서만 사용 가능
  • 비동기 함수가 다 처리되기를 기다리는 역할
  • Promise 객체를 리턴하는 함수 호출 코드 앞쪽에만 붙일 수 있음
  • Promise의 then() 부분에서 전달받던 성공 시 결과값을 곧바로 얻을 수 있음

try~catch 에러 발생 시 핸들링

function fnTest(){
  return "test~";
}

//async await
async function testPromise(){ // 1. 비동기 처리
  try {
    const test = await fnTest(); // 2. 처리 기다림
    console.log(test); // 3. 처리 후 실행코드
  } catch(e) {
    console.log(e);
  }
}

위의 카페 코드를 async await으로 바꿔 작성한 예시이다.

const cafe = async function (drink) {
  try {
    if (drink === 'coffee') {
      const result = await giveCoffee();
    } else {
      throw new Error(`There isn't it here`);
    }
  } catch (error) {
    console.log(error.message);
  } finally {
    console.log('Thank you');
  }
};

// 비동기 함수 정의
const giveCoffee = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Here you go');
    }, 1000);
  });
};

// 함수 호출
cafe('the drink');

❓ Blocking / Non-Blocking

다른 요청의 작업을 처리하기 위해 현재 작업을 block(차단, 대기)하냐 안하냐에 따라 블로킹과 논블로킹으로 나눌 수 있다.

동기/ 비동기와 헷갈릴 수 있는 개념이지만, 동기/ 비동기는 작업을 순차적으로 실행하느냐에 관한 개념이고, 블로킹/ 논블로킹은 병렬적으로 여러 작업이 실행 가능한지에 관한 개념이다.

setTimeout은 다른 작업을 멈추거나, 멈춰지지 않고 인수로 받은 시간만을 따라서 실행되므로 논블로킹이고, 앞뒤 코드와의 실행 순서를 신경쓰지 않고 마찬가지로 시간만을 따라서 신행되므로 비동기이다.


요약

구분Sync / AsyncBlocking / Non-blocking
설명A가 B를 호출했을 때, A가 B의 결과를 기다리는가A가 B를 호출했을 때, A가 막히는가
예시fetch().then(...), awaitsetTimeout, I/O, fetch


출처: 🔄 자바스크립트 이벤트 루프 동작 구조 & 원리
[Javascript] 🌟비동기 처리🌟
자바스크립트 Promise 개념 & 문법 정복하기
자바스크립트 Async/Await 개념 & 문법 정복
https://reactjs.winterlood.com/45ee7daf-0c32-4f40-9b20-b7bba338d39f

0개의 댓글