[JS] 동기 & 비동기

Minyoung's Conference·2022년 6월 15일
0

JavaScript

목록 보기
13/28
post-thumbnail

JavaScript의 동작원리를 이해하기 위해선 동기 & 비동기에 대한 개념을 정확히 이해할 필요가 있다. 한번 제대로 알아보자.

수행해야 할 작업이 3개가 있고, 작업 실행 순서가 다음과 같다고 가정해보자.

// 수행해야 할 작업(함수) 3개
function taskA() {
  console.log("TASK A");
}
function taskB() {
  console.log("TASK B");
}
function taskC() {
  console.log("TASK C");
}
// 작업 실행(함수 호출) 순서
taskA();
taskB();
taskC();

동기적 방식일 떄 위 코드들이 어떻게 실행되는 지 확인해보자.
<자바스크립트 싱글 스레드 작업 수행 방식>

위를 블로킹 방식(한 스레드에서 작업이 진행되면 다른 일을 동시에 할 수 없음)이라고도 부른다.

위와 같이 자바스크립트는 코드가 작성된 순서대로 작업을 처리하고, 이전 작업이 진행중이면 다음 작업을 수행하지 않고 기다리게 된다. 먼저 작성된 코드를 다 실행하고 나서 뒤에 작성된 코드를 실행한다. 이를 "동기 방식 처리" 라고 한다.

동기 방식 처리의 문제점은 다음과 같다.

동기적 처리의 단점은 하나의 작업이 너무 오래 걸리게 되면, 모든 작업이 오래 걸리는 그 작업이 끝날 때까지 올 스탑 되기 떄문에, 전반적인 흐름이 느려진다.

그러면 멀티스레드가 된다면 해결되지 않을까라는 의문이 생길 수 있다.
확인해보자.

이렇게 멀티 스레드 방식으로 작동시키면 위와 같이 작업 분할이 가능하다.
하지만!! 중요한 것은 자바스크립트는 '싱글 스레드 방식' 이라 불가능하다는 점이다.

그렇기 떄문에, '비동기 작업 방식' 을 통헤 이를 해결하려고 한다.

비동기적으로 여러 개의 작업을 동시에 실행시킨다. 그러면, 먼저 작성된 코드의 결과를 기다리지 않고 다음 코드를 바로 실행하게 된다.
이를 논 블로킹 방식이라고도 한다.

그러면 동시에 실행되는지 직접 확인하기 위해선 어떻게 해야할까?
콜백함수를 붙여주고 결과값의 순서를 보면 된다.

taskA((resultA) => { console.log(`A 끝났습니다. 작업 결과 : ${resultA}`)});  // A 콜백 
taskB((resultB) => { console.log(`B 끝났습니다. 작업 결과 : ${resultB}`)});  // B 콜백 
taskC((resultC) => { console.log(`C 끝났습니다. 작업 결과 : ${resultC}`)});  // C 콜백 

우리가 항상 사용해오던 동기적 방식은 아래와 같다.

function taskA() {
  console.log("A작업 끝");
}

taskA();
console.log("코드끝");

// A작업 끝
// 코드끝

taskA 함수가 실행되고 콘솔이 실행된다.

그럼 내장 비동기 함수인 setTimeout() 이라는 함수를 사용해 비동기적 방식을 실행해보자.

function taskA() {
  setTimeout(() => {
    console.log("task A End");
  }, 2000);
}

taskA();
console.log("코드끝");

// 코드끝
// task A End 

순서가 바뀐 것을 확인할 수 있다.
그러면 지역상수 res를 이용해서 3초 후의 매개변수의 합이 출력되도록 해보자.

//a, b 그리고 콜백함수 cb 를 받는 taskA에서 cb를 실행시켜 res를 반환한다. 
function taskA(a,b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}

taskA(3, 4, (res) => {
  console.log("A Task Result :", res);
});
console.log("코드끝");

// 코드끝 
// A Task Result : 7

비동기를 처리할 때 콜백함수를 많이 사용하게 된다.

여기서 함수를 추가해보자.

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);
}

taskA(3, 4, (res) => {
  console.log("A Task Result :", res);
});

taskB(3, (res) => {
  console.log("B Task Result :", res);
});

console.log("코드끝");

// 코드끝 
// B Task Result : 6
// A Task Result : 7

setTimeout이 불러내는 시간에 따라 함수가 비동기적으로 실행된다.

그렇다면, JS Engine은 어떻게 동기와 비동기를 구분해서 실행하는 지 궁금할 수 있다. 확인해보자. 우선 동기적으로 동작했을 때이다.

우선, 자바스크립트엔진은 웹 브라우저에 탑재되어 있다. 그리고 Heap과 Call Stack 이 두 가지의 방식으로 구성되어 있다. Heap은 변수나 상수의 메모리가 저장되는 영역이다. (우선 패스) 중요한 부분은 Call Stack이다.

Call Stack은 우리가 작성한 코드의 실행에 따라 호출 stack이 쌓이는 영역이다.

자바스크립트의 코드실행이 시작되면, Main Context가 Call Stack에 먼저 쌓이게 된다. Call Stack을 사용하려면 Main Context가 최상위 문맥이기에 들어오는 순간이 프로그램이 시작되는 순간이라고 봐도 무방하다. 마찬가지로 Main Context가 나가는 순간이 프로그램이 종료되는 순간이다.

위 그림과 아래 함수를 비교해서 이해해보자.

function one() {
  return 1;
}
function two() {
  return one() + 1;
}
function three() {
  return two() + 1;
}
console.log(three());

콘솔로 three()를 호출했다. (함수 one, two, three 는 함수 생성만 한다.)

three()라는 함수를 실행하기 위해선 two()라는 함수를 불러와야 한다.
two()라는 함수를 실행하기 위해선 one()이라는 함수를 호출해야 한다.

하지만 one()을 불러오면서 1이라는 값을 return 하기 때문에 one 함수는 다시 빠져나가게 된다. 종료된 함수는 바로바로 Stack에서 빠져나가기 떄문이다.

Stack은 프링글스 통처럼 가장 마지막에 들어온 것이 가장 먼저 제거되는 구조이다.

one이 제거 되면서 two에서 2를 반환하고 two()가 2가 되면서 three() 함수에 3이라는 결과가 나온다.

그리고 나서 console.log(three()) 가 실행되어 3이라는 결과값을 반환하고 프로그램이 종료된다. 그 동시에 MainContext가 사라지게 된다.

그렇다면, 비동기 동작을 한번 그림으로 확인해보자.

function asyncAdd(a, b, cb) {
	setTimeout(() => {
    	const res = a + b;
      	cb(res);
    }, 3000)
}

asyncAdd(1, 3, (res) => {
	console.log("결과 : ", res);
})

그림과 코드는 위와 같다. 그림 상 동기와 다르게 Web APIs, Callback Queue, Event Loop가 눈에 띈다. 현재 asyncAdd라는 함수는 비동기작업을 수행하고 있다.
동기와 비동기의 처리 방식의 차이를 학습해보자.

일단, 동기와 마찬가지로 프로그램이 실행되면 Main Context가 열리고,
호출함수인 asyncAdd()가 실행되게 된다.

asyncAdd 함수를 실행하기 위해 들어가봤더니 setTimeout이라는 비동기 함수를 호출하고 있다. 그리고 cb라고 하는 콜백함수 또한 포함하고 있다.

setTimeout 함수는 비동기함수라 빨간색으로 표현했다. 동기함수 방식이라면 setTimeout 함수가 현재 3초 후에 결과가 나타나기 때문에 3초를 기다렸다가 다른 코드를 실행할 것이다. 그래서 자바스크립트 엔진은 비동기로서 실행될 수 있도록 Web APIs로 보내버리게 된다. 그리고 Web APIs에서 3초를 기다리게 된다.

그러면 Call Stack에서는 asyncAdd 함수를 끝낼 수 있게 된다.(Call Stack에서 제거)
setTimeout 함수는 3초가 지나고 나면 WebAPIs에서 제거가 된다. 그리고
콜백함수인 cb()는 Callback Queue로 옮겨지게 된다.

그리고나서, Event Loop에 의해 Call Stack으로 옮겨지게 된다.

Event Loop는 Main Context 이외의 다른 함수가 남아있는지를 계속해서 확인하게 된다. 아무것도 남아있지 않다면, 콜백 함수를 수행할 수 있게 한다. cb()를 수행하고 결과값을 출력한다 (결과 : 4)
그리고 Main Context를 제거하고 자바스크립트 프로그램이 완료된다.

동기와 비동기의 차이를 직접 확인해보았다. 앞 전의 예시를 사용해 조금 더 이해해보자.
이번엔 taskC 함수도 추가해보겠다.

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);
}

function taskC(a, cb){
  setTimeout(()=>{
    const res = a * -1;
    cb(res);
  },2000)
}
taskA(4, 5, (a_res) => {
  console.log("A Task Result :", a_res);
  taskB(a_res, (b_res)=>{
    console.log("B Task Result :", b_res);
    taskC(b_res, (c_res)=>{
      console.log("C Task Result :", c_res); 
    })
  })
});


console.log("코드끝");

// 코드끝 
// A Task Result : 9
// B Task Result : 18
// C Task Result : -18

비동기 결과를 또 다른 비동기 결과로 받아냈다. 실무에서 이런 경우가 종종있기에 이런 상황에 익숙해져야 한다. 하지만 가독성면에서 좋지 않고, task가 3개가 아니라 100개 라면? 콜백 지옥에 빠질 것이다. 이 지옥을 빠져나갈 수 있도록 자바스크립트에는 비동기 객체가 존재한다. 바로 Promise다. Promise에 대해 따로 한 번 다루어 보겠다.

profile
안녕하세요, FE 개발자 김민영입니다.

0개의 댓글