티스토리 뷰

반응형

javascript의 작동 방식에 대해 알아보자. 동기적 방식과 비동기적 방식에 대한 근본적인 원리에 대해 심층적으로 알아보자.

 

 

 

 

 

1. Javascript의 동기적 작동방식

우선 코드를 보자.

 

function one() {
  two()
  console.log('one')
}

function two() {
  console.log('two')
}

one()

 

위의 코드는 어떻게 작동할까?  결과는 아래와 같다.

 

two
one

 

  1. one 함수를 실행한다.
  2. one 내부의 two 함수가 실행된다.
  3. console.log('two') 가 실행된다.
  4. console.log('one')이 실행된다.

 

위의 예시로 Javascript는 위에서 아래로 실행되고, 왼쪽에서 오른쪽으로 실행되는 것을 확인할 수 있다. 이렇게 순서대로 실행되는 것을 동기적으로 실행된다고 한다. 이러한 방식을 조금 더 자세히 알아보자.

 

 

 

2. 호출 스택 ( = 콜 스택 = call stack )

메모리란 임시 저장장치로 컴퓨터가 잠깐 기억하고 있는 공간이다. Javascript는 위와 같이 one() 함수를 선언 시 one() 함수는 메모리에 올라간다. 그리고 함수 호출 시 메모리에서 함수가 선언 되었는지 찾는다. 이를 호출 스택이라 부르며, 호출된 함수는 STACK 형식으로 아래와 같이 쌓인다.

 

two()
one()
anonymous

 

아래에서 위로 쌓이고, 위에서 아래로 실행된다. 위와 같은 스택으로 머리속에서 그릴 수 있어야 한다. Anonymous는 가상의 전역 컨텍스트(scope)로 파일 실행 시에 먼저 아래부분에 쌓이게 된다. 그리고 함수가 실행 될 때 마다 anonymous 위로 하나씩 차곡차곡 쌓인다. 그리고 함수의 범위를 나타내는 {}에서 } 부분(함수 끝부분)을 만나면 쌓인 STACK에서 빠지게 된다. 이러한 방식으로 하나씩 실행이 되면, 마지막에는 anonymous만 남게 된다. 마지막으로 Anonymous가 실행되면 파일 실행은 끝이나고 javascript 실행이 완료 된다.

 

 

이러한 호출 스택은 Javascript가 어떠한 순서로 작동하는가를 확인할 때 필수적으로 사용된다. 하지만 비동기라면 조금 더 개념을 알아야 한다.

 

 

 

3. javascript 비동기 작동방식

setTimeout을 활용하여 비동기 방식의 간단한 예를 보자.

 

function run() {
    console.log('1초 후 실행');
}

console.log('시작')
setTimeout(run, 1000);
console.log('끝')

 

위의 결과 값은 아래와 같다.

 

시작
끝
1초 후 실행

 

우선은 위에서 알게 된 호출 스택 방식으로 동기적으로 분석을 하면 아래와 같다.

 

  1. 파일 실행 시 anonymous(전역 스코프) STACK아래에 생성된다.
  2. function run 선언 시 메모리에 run 함수 저장된다.
  3. console.log 호출하면 호출 스택에 쌓인다.
  4. console.log 실행 후에, 실행이 끝나면 호출 스택에서 바로 빠진다.
  5. setTimeout도 호출되서 스택 들어왔다 실행 후 빠진다.
  6. console.log('끝')도 스택에 들어왔다 나간다. 
  7. anonymous를 실행하면, javascript 실행이 종료 된다.

 

위의 방식은 잘못된 방식이다. 위 코드는 호출 스택만으로 비동기가 설명이 안된다. 호출스택과 이벤트 루프로 설명이 가능하다.

 

 

4. Javascript 이벤트 루프 ( Task Queue )

기본적으로 이벤트 루프를 이해하려면, '호출스택 / 백그라운드 / 메모리 / 태스크 큐 / 콘솔창' 에 대한 기본적인 이해가 있어야한다. 위의 예제를 다시 한번 호출스택에 이벤트 루프를 포함하여 설명해 보겠다.

 

1. 파일 실행 시 anonymous(전역 스코프) STACK아래에 생성된다.

2. function run 선언 시 메모리에 run 함수 저장된다.

3. console.log 호출하면 호출 스택에 쌓인다.

4. console.log 실행 후에, 실행이 끝나면 호출 스택에서 바로 빠진다.

5. setTimeout도 호출되서 스택 들어왔다 실행 후 빠진다. (여기까지는 동일하다.)

5-1. 백그라운드에 '타이며(run, 3초)'를 넣어준다. 백그라운드의 장점은 코드가 백그라운드로 가면, 호출스택과 백그라운드가 동시에 실행된다. 따라서 아래의 console.log 실행하면서도 백그라운드에서는 3초를 계속 세고 있다. 추가로 백그라운드가 먼저 끝난다고 하더라도 호출스택이 먼저 처리가 되어야 한다.

6. console.log('끝')도 스택에 들어왔다 나간다. 

7. anonymous를 만나면, 호출 스택이 모두 비워진다.

8. 3초가 끝나면 run을 태스크 큐로 보내고 백그라운드는 지워진다.

9. 이벤트 루프의 역할은 호출 스택이 비어있을 떄, 태스크 큐에 있는 함수들 하나하나를 호출스택에  끌어와서 실행해 준다.

10. 호출스택에 run이 담기고 태스크 큐의 run은 지워진다.

11. run 실행되면서 안에 있는 console.log가 run위에 쌓인다.

12. console.log 끝나면, 나가면서 콘솔창에 글 남기고 호출스택 빠짐

12. run 함수도 호출스택에서 빠짐.

+ 호출스택, 백그라운드, 태스크 큐가 다 지워져 있으면 JS 실행이 완료 된 것이다.

 

위의 방식으로 JS 코드 순서 분석이 가능하다. 사실 자세히 공부해보면, 호출스택에 this나 scope 들도 고려를 해야하기 때문에 더 복잡하다. 추가로 백그라운드는 다른 쓰레드를 사용한다. 쉽게 말하면, 작동 방식은 멀티 스레드처럼 동작한다고 보면된다. 그러나 백그라운드에 들어갈 수 있는 함수(setTimeout 등)는 한정적으로 정해져있다. JS는 싱글 스래든인데 백그라운드에서 어떻게 동시 실행될까? 백그라운드는 JS가 아니라 C++이다. 즉, 호출 스택만 JS이고 백그라운드와 태스크 큐는 다른언어라고 생각하면 된다.

 

 

5. promise 와 비동기

먼저 코드를 보자. setTimeout, 비동기, promise가 모두 담겨진 예시이다.

 

function endTest(){
    console.log('order :> four')
}
function run() {
    console.log('order :> one');
    setTimeout(()=> {
        console.log('order :> two')
    }, 0)
    new Promise((resolve)=>{
        resolve('order :> three');
    })
    .then(console.log);
    endTest();
}
console.log('start!')
setTimeout(run, 1000)
console.log('end!')

 

결과값은 아래와 같다.

 

start!
end!
order :> one
order :> four
order :> three
order :> two

 

변경된 부분만 말하면, setTimeout(익명함수, 0)은 실행 후에 호출 스택에서 바로 사라지고 백그라운드로 이동시킨다. new Promise 부분은 처음엔 동기로 작동한다. 호출 스택의 run 함수 위에 new Promise가 쌓인다. 그리고 그 위에 resolve('order :> three')도 쌓인다. 실행 이후에 then을 만나는 순간 백그라운드로 이동시킨다. then 이후에 endTest()가 호출 스택 쌓인 후에 쌓은 호출 스택을 모두 실행한다. 호출 스택을 비운 후에, 백그라운드에 있는 setTimeout의 익명함수와 Promise에서 받은 then 을 처리해야한다. 기본적으로는 먼저 끝나는 부분이 태스크 큐로 이동을 하게 되지만, 여기서는 Promise가 우선 순위로 들어간다. 정리하면, 태스크 큐는 Promise가 우선 순위로 실행된다. 

 

promise.then / promise.catch / process.nextTick 등은 태스크 큐에서 타이머 보다 먼저 들어가서 실행이 된다. 사실 설명의 편의상 promise를 태스크 큐에 들어간다고 했지만, 타미어 보다 먼저 실행이 되는 것들은 정확히는 마이크로 태스크 큐에 들어가게 된다.

 

 

5. Micro Task Queue

바로 예제부터 확인해 보자.

 

console.log('start :> call stack');
setTimeout(() => console.log('Task Queue'), 0);
Promise.resolve().then(() => console.log('Micro Task Queue'));
console.log('end :> call stack')

 

결과는 아래와 같다.

 

start :> call stack
end :> call stack
Micro Task Queue
Task Queue

 

결과를 정리하면 아래와 같은 순서로 실행되는 것을 확인 할 수 있다.

  1. 호출 스택
  2. 마이크로 태스크 큐
  3. 태스크 큐

정리하면, 호출 스택을 모두 실행하면서 비동기 되는 함수들을 큐에 넣는다. 호출 스택이 모두 실행된 이후 이벤트 루트는 큐에서 함수를 가져와 호출 스택에 넣고 실행한다. 이 부분이 콜백 함수의 원리라고 할 수 있다. 콜백함수가 태스크 큐/마이크로 태스크 큐 중 어디에 들어가는지는 아래를 참고하면 된다.

 

5.1 Task Queue 콜백함수

setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI 렌더링

 

 

5.2 Micro Task Queue 콜백함수

process.nextTick, Promise, Object.observe, MutationObserver

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함