본문 바로가기

JavaScript

JavaScript 동작 원리(동기와 비동기)

*테스크큐와 콜백큐는 같은 개념입니다.

 

 

 

 

 

 

동기와 비동기를 예시 코드와 함께 살펴보도록 하겠습니다.

 

 

function taskA() { //taskA가 완료되는 시간 : 0.5초
  console.log("A");
}

function taskB() { //taskB가 완료되는 시간 : 5초
  console.log("B");
}

function taskC() { //taskC가 완료되는 시간 : 10초
  console.log("C");
}

taskA();
taskB();
taskC();

위의 예시 코드처럼 3가지의 작업을 해야 하는 상황이라고 가정해보도록 하겠습니다.

 

taskA의 작업이 완료되는 시간은 0.5초, taskB가 완료되는 시간은 5초, taskC가 완료되는 시간을 10초라고 가정해보겠습니다.

 

taskA, taskB, taskC의 작업 방식을 대략적으로 나타내면 위의 그림과 같습니다.

 

각 task 마다 있는 코드를 한 줄, 한 줄 실행하게 되는데 이때 코드를 실행하는 것을 Thread라고 부를 수 있습니다.

 

JavaScript에서는 하나의 Thread로 지시한 순서대로 taskA 실행, taskB 실행, taskC 실행을 하게 됩니다.

 

이때 Thread는 taskA를 실행하고 있을 때 다음 순서인 taskB를 실행하지 않고 있습니다.

 

즉 taskA의 실행이 끝나지 않으면 다음 순서로 넘어가지 않는 것입니다.

 

이렇게 앞에 실행이 끝날 때까지 기다렸다가 끝이 나면 실행되는 것을 동기적 방식이라고 합니다.

 

그리고 Thread에서 작업 하나가 실행되고 있을 때 다른 작업을 동시에 할 수 없는 것을 블로킹 방식이라고 합니다.

 

작업 시간이 짧다면 상관없겠지만 위의 이미지처럼 하나의 작업 시간이 5초, 10초라고 가정한다면 3가지의 작업이 끝이 나려면

15초가 넘는 시간이 소요됩니다.

 

JavaScript의 사용 목적이 브라우저에 활동성을 부여하기 위함인데 사용자가 버튼 하나를 클릭했을 때 이렇게 긴 시간이 소요된다면

사용자에게는 좋지 못한 경험을 주게 되는 것입니다.

 

이렇게 동기적으로만 모든 작업을 처리하게 되면 작업 시간이 길어지면서 성능에 문제가 발생하게 됩니다.

 

이러한 문제는 하나의 Thread에서 모든 작업을 수행하기 때문에 시간이 길어진다고 생각할 수 있고 여러 개의 Thread를 추가하여

 

작업을 분리시키면 빠르게 작업을 수행할 수 있지 않을까 라는 생각을 하실 수 있지만

 

JavaScript는 여러 개의 Thread를 사용하는 다른 언어와 달리 싱글 Thread, 즉 하나의 Thread만 사용합니다.

 

 

 

싱글 Thread만 사용하는 상황에서 어떻게 빠르게 작업을 수행할 수 있을지 알아보겠습니다.

방법은 taskA, taskB, taskC를 동시에 실행시키는 것입니다.

 

즉 각각의 task가 작업 종료 여부와 상관없이 동시에 실행시켜버리는 것입니다.

 

이렇게 동기적이지 않게 여러 개의 작업을 동시에 실행시키는 것을 비동기 방식이라고 합니다.

 

그리고 이렇게 하나의 작업이 수행될 때 블로킹을 하지 않는 방식을 논 블로킹 방식이라고 합니다.

 

 

taskA((result) => {
  `작업 A가 종료되었습니다. : ${result}`; // 콜백함수
});
taskB((result) => {
  `작업 B가 종료되었습니다. : ${result}`; // 콜백함수
});
taskC((result) => {
  `작업 C가 종료되었습니다. : ${result}`; // 콜백함수
});

이렇게 비동기적 방식으로 작업을 실행했을 때 작업들이 정상적으로 종료되었는지의 여부를 콜백 함수를 통해 전달해주시면 됩니다.

 

즉 함수 taskA는 자신의 작업을 완료하게 되면 전달받은 콜백 함수를 호출하여 결괏값을 이용할 수 있도록 만들어주는 것입니다.

 

 

 

 

예시 코드와 함께 조금 더 자세히 살펴보도록 하겠습니다.

 

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

taskA();
console.log("작업 끝!")

이렇게 함수 taskA를 호출한 후 console.log("작업 끝!")를 입력하게 되면 

 

함수 taskA의 실행이 종료된 다음 console.log("작업 끝!")의 작업이 실행되어 결과는 'A'가 출력되고 '작업 끝!'이 출력되게 됩니다.

 

즉 함수 taskA의 실행이 끝나지 않으면 console.log("작업 끝!")의 실행은 시작되지 못하는 것입니다.

 

이렇게 작업이 실행되는 것이  동기적 방식이라고 합니다.

 

 

이번에는 taskA를 비동기 방식으로 변환해보겠습니다.

function taskA() {
  setTimeout(() => {
    console.log("A");
  }, 3000); // 딜레이 타임은 밀리세컨즈 단위로 전달합니다. (3000 은 3초를 의미합니다.)
}

taskA();
console.log("작업 끝!");

JavaScript에는 setTimeout라는 내장 비동기 함수가 존재합니다.

 

setTimeout에는 2개의 인자가 들어가는데 첫 번째 인자는 콜백 함수, 두 번째 인자는 딜레이 타임을 받습니다.

 

setTimeout은 인자로 전달받은 딜레이 타임이 지나고 난 뒤 콜백 함수를 실행시키게 됩니다.

 

이렇게 setTimeout을 이용하여 비동기 방식으로 실행시키게 되면 '작업 끝!'이 먼저 출력되고 3초 뒤에 'A'가 출력되게 됩니다.

 

작업 실행 지시는 taskA가 먼저 실행되게 했지만 taskA의 작업이 종료되기를 기다리지 않고 console.log("작업 끝!")의 작업이 실행되었기 때문입니다.

 

정리하면 먼저 실행된 작업이 종료되기를 기다리지 않고 실행되는 것이 비동기 방식입니다.

 

 

 

함수 taskA에 두 개의 인자를 전달받아 setTimeout 함수를 이용하여 전달받은 인자를 더하는 예제를 살펴보겠습니다.

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

taskA(1, 2, (result) => {
  console.log(`A TASK RESULT : ${result}`);
});
console.log("작업 끝!");

인자로 받은 a와 b를 더하여 상수 res에 할당하였습니다.

 

이때 함수 taskA 호출 시 res를 사용해주어야 했기 때문에 콜백 함수를 사용해주었습니다.

 

즉 함수 taskA를 호출하는 과정에서 인자로 콜백 함수를 전달하였고, 

 

const res = a + b;의 코드 실행이 완료되게 되면 코드의 흐름이 콜백 함수로 넘어와 상수 res에 할단된 값이 콜백 함수의 매개변수 result에 인자로 전달되게 됩니다.

 

결과는 '작업 끝!'이 먼저 출력이 되고 'A TASK RESULT : 3'가 출력되게 됩니다.

 

이렇게 비동기 방식의 결괏값을 이용할 때는 위의 방식처럼 콜백 함수를 전달하여 이용할 수 있습니다.

 

 

 


 

JavaScript는 어떻게 동기적 방식과 비동기 방식을 구분하는지 살펴보도록 하겠습니다.

JavaScript Engine은 Heap과 Call Stack으로 이루어져 있습니다.

 

Heap은 변수나 상수에 사용되는 메모리를 저장하는 영역이고, Call Stack은 작성한 코드의 실행에 순서에 따라 호출 스택을 쌓는 영역입니다.

 

 

어떻게 동기적 방식과 비동기 방식을 구분하는지 알기 위해 Call Stack에 집중하여 예시 코드와 함께 살펴보도록 하겠습니다.

 

동기적 방식
function one() {
  return 1;
}

function two() {
  return one() + 1;
}

function three() {
  return two() + 1;
}

console.log(three()); //3 출력

JavaScript의 코드가 실행이 되면 위의 이미지처럼 Call Stack에 JavaScript의 최상위 문맥인 Main Context 가장 먼저 들어가게 됩니다.

 

그렇기 때문에 Main Context가  Call Stack에 들어가는 순간이 프로그램 실행 순간인 것이고, Main Context가 Call Stack에서 나가는 순간이 프로그램이 종료되는 순간입니다.

이렇게 Main Context가 들어오게 되면 다음으로 첫 번째 코드인 console.log(three());가 실행이 됩니다.

 

이때 함수 three가 실행되게 됨으로 Call Stack에 추가됩니다.

함수 three를 실행시켜 return 되는 값이 무엇인지 확인해보려고 하니 함수 two를 실행하고 있습니다.

 

이때 함수 two가 실행되게 됨으로 Call Stack에 추가됩니다.

 

함수 two를 실행시켜 return 되는 값이 무엇인지 확인해보려고 하니 함수 one를 실행하고 있습니다.

 

이때 함수 one가 실행되게 됨으로 Call Stack에 추가됩니다.

 

다음으로 종료된 함수가 Call Stack에서 빠지게 되는 것을 살펴보겠습니다.

 

Call Stack에서 빠져나갈 때는 가장 마지막에 들어온 것부터 먼저 빠지게 됩니다.

가장 마지막에 호출된 함수 one이 1을 return 하고 실행이 종료되게 되면 Call Stack에서 빠지게 됩니다.

다음으로 함수 two가 2를 return 하고 실행이 종료되게 되면 Call Stack에서 빠지게 됩니다.

다음으로 함수 three가 3을 return 하고 실행이 종료되게 되면 Call Stack에서 빠지게 됩니다.

마지막으로 3을 출력하고 프로그램을 종료하기 위해 Call Stack에서 Main Context가 빠지게 됩니다.

 

이렇게 JavaScript가 프로그램을 실행하고 종료하게 됩니다.

 

상단에서 설명한 ThreadCall Stack 하나만을 담당하고 Call Stack의 작동방식대로 명령어를 처리합니다.

 

JavaScript는 Call Stack이 하나만 존재하기 때문에 싱글 Thread로 작동한다고 생각하시면 됩니다.

 

 

 

비동기 방식
const taskA = (a, b, cb) => {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
};

taskA(1, 2, (result) => {
  console.log(`A TASK RESULT : ${result}`);
});
console.log("작업 끝!");

위의 예시 코드는 비동기 함수를 실행하는 함수를 호출하고 있습니다.

 

비동기 방식의 경우 동기적 방식과 다르게 Web APIs와 Callback Queue, Event Loop의 구성요소가 추가되었습니다.

 

세 가지 구성요소는 JavaScript Engine과 브라우저 간 상호작용 등을 위해 존재하는데 그중 가장 대표적인 상호작용이 비동기 방식입니다.

 

프로그램이 시작되면 Main Context가 Call Stack에 추가가 되고, 함수 taskA를 호출하게 되면 Call Stack에 taskA가 추가됩니다.

 

함수 taskA 실행시켜보니 함수 taskA 안에서 비동기 함수 setTimeout을 호출하며, 비동기 함수 setTimeout 안에는 콜백 함수 cb를 호출하고 있습니다.

Call Stack에 추가된 함수 setTimeout을 그대로 실행되게 놔두면 딜레이 타임을 기다리고 다음 코드를 실행하기 때문에 Web APIs로 넘기게 됩니다. 

 

Web APIs로 넘겨진 함수 setTimeout는 딜레이 타임을 기다리게 되고 실행을 마친 함수 taskA는 Call Stack에서 빠져나갈 수 있게 됩니다.

taskA는 Call Stack에서 빠져나가고 setTimeout의 딜레이 타임이 끝나게 되면 Web APIs에서 함수 setTimeout는 제거가 되고

 

Callback Queue에 콜백 함수 cb가 담기게 됩니다.

이렇게 Callback Queue에 옮겨진 콜백 함수 cb는 Event Loop를 통해 다시 Call Stack으로 옮겨지게 됩니다.

 

Event Loop를 통해 다시 Call Stack으로 다시 들어간다는 의미는 콜백 함수가 실제로 수행이 이루어지는 것입니다.

 

Event Loop Call Stack에서 Main Context를 제외한 다른 함수가 남아있지 않는지를 계속 확인하게 되고,

 

다른 함수가 남아있지 않다면 콜백 함수를 Call Stack으로 넘겨주게 됩니다.

 

넘기진 콜백 함수 cb가 실행되고 결과가 출력되면 마지막으로 남은 Main Context가 Call Stack에서 빠져나가게 되고 프로그램은 종료가 됩니다.

 

 

 


 

 

 

 

 

 

'JavaScript' 카테고리의 다른 글

클래스  (0) 2022.12.24
정규표현식  (0) 2022.12.24
Memory Leak  (0) 2022.12.23
Garbage Collection  (0) 2022.12.23
lodash를 활용한 깊은 복사  (0) 2022.12.23