thumnail.png

우리가 자바스크립트 런타임 환경으로 사용하는 NodeJS는 싱글 스레드 기반의 비동기, Non-Blocking으로 동작한다. 여기서 Non-Blocking이란 무었일까?

Blocking은 NodeJS에서 추가적인 자바스크립트 코드 실행을 위해 자바스크립트가 아닌 작업이 완료될 때 까지 기다려야 하는 상황, Blocking작업 동안은 이벤트루프가 자바스크립트 실행을 계속할 수 없다. 그렇다면 Non-Blocking은 Blocking과 반대로 자바스크립트 코드가 실행되기 위해 자바스크립트가 아닌 작업을 기다리지 않아도 되는 상황이다.

자바스크립트에서 Blocking 메서드는 동기적으로, Non-Blocking 메서드는 비동기적으로 실행된다.

비동기 코드 작성해보기

그러면 실제로 동기적으로 동작하는 코드와 비동기적으로 동작하는 코드가 뭐가 다른지 살펴보자!

const fs = require('fs');

fs.readFile('./chanyeong.txt', (error, data) => {
  if (error) {
    console.error(error);
  }

  // '오늘도 여전히 자바스크립트 공부...'
  console.log(data.toString());
});

fs모듈은 NodeJS에서 제공하는 파일을 읽을 수 있는 모듈이다. fs모듈을 통해 로컬에 있는 chanyeong.txt파일을 읽어보았다. 내가 chanyeong.txt파일에 작성해 놓은 오늘도 여전히 자바스크립트 공부...라는 텍스트가 출력되는 것을 볼 수 있다.

그러면 chanyeong.txt파일의 데이터를 변수안에 저장해보자!

let chanyeongInnerText;

fs.readFile('./chanyeong.txt', (error, data) => {
  if (error) {
    console.error(error);
  }

  chanyeongInnerText = data.toString();
});

// undefined
console.log(chanyeongInnerText);

분명히 chanyeongInnerText변수 안에 데이터를 넣어줬는데 출력해 보니 undefined가 나왔다. 왜 이런 결과가 나오냐면 fs모듈에서 파일 데이터를 읽기도 전에 console.log(chanyeongInnerText)로 출력을 시도했기 때문이다.

분명 코드상에서는 변수를 입력 받고 출력했지만 fs.readFile메서드는 non-blocking 하게 작동되기 때문에 파일을 다 읽지도 못 했는데 다음 코드가 실행된 것이다.

let chanyeongInnerText;

fs.readFile('./chanyeong.txt', (error, data) => {
  if (error) {
    console.error(error);
  }

  chanyeongInnerText = data.toString();
  // '오늘도 여전히 자바스크립트 공부...'
  console.log(chanyeongInnerText);
});

그렇다면 변수가 입력받기를 기다렸다가 출력시키면 정상적으로 출력되는 것을 알 수 있다. 물론 fs모듈 자체에서 파일을 동기적으로 불러오는 readFileSync라는 메서드가 있지만 비동기에 대한 예시를 알아보기 위해 사용했다.

자세히 보면 알겠지만 readFile안에는 두 개의 인자가 들어간다. 첫 번째로 읽어올 파일의 경로를 나타내는 인자와 파일을 읽은 후에 호출할 콜백 함수를 인자로 넣는다. 이런 방식을 콜백 기반 비동기 프로그래밍이라고 한다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 제공해야 한다.

callback queue

그렇다면 비동기 함수는 왜 저런 방식으로 실행될까?

보통 함수가 실행되면 call stack으로 들어가 실행된다. 하지만 비동기적으로 동작하는 fs.readFile같은 경우 call stack이 아닌 callback queue로 전달되어 대기한다. 이름과 같이 callback queue는 FIFO(First In First Out)방식으로 처리된다.

자바스크립트의 이벤트 루프가 시작되면 가장 먼저 call stack에 처리해야 할 함수가 있는지 확인하고 스택 가장 위에 있는 함수를 실행한다. 그러나 call stack이 비어있는 것을 발견하면 코드를 계속해서 실행시킨다. 코드의 끝에 도달하고 이벤트 루프가 시작되면 평소와 같이 call stack을 확인해 함수를 순차적으로 실행시키는데 call stack이 비어있는 것을 발견하면 그때 이벤트루프가 callback queue를 확인 해 큐의 함수들들 순차적으로 실행한다.

init.png

call stackcallback queue를 간단하게 그려보면 다음과 같다. 그럼 우리가 처음 작성한 코드대로 call stackcallback queue에 함수 실행 과정을 넣어보자!

let chanyeongInnerText;

fs.readFile('./chanyeong.txt', (error, data) => {
  if (error) {
    console.error(error);
  }

  chanyeongInnerText = data.toString();
});

// undefined
console.log(chanyeongInnerText);

first.png

우리는 처음에 fs.readFile 메서드를 사용해 파일을 불러왔다. 그러나 메서드는 비동기적으로 동작하기 때문에 callback queue에 들어가게 된다.

second.png

그리고는 console.log로 인해 변수를 출력하게 되는데 이때 아직 fs.readFile이 실행되지 않았으므로 변수의 값은 undefined가 출력된다.

first.png

그리고 console.log가 종료되어 call stack에서 사라지고 코드에 끝에 도달했으니 이제 fs.readFile메서드를 실행하게 되며 이때 chanyeongInnerText변수에 파일에서 읽어온 데이터가 입력된다.

마무리

그렇다면 비동기 함수를 좀 더 간편하고 개발자가 원하는 순서대로 작동하게 하려면 어떤식으로 코드를 작성해야 할까? 비동기 함수 내부마다 다음 실행시킬 비동기 함수를 넣어줘도 되지만 그렇게 되면 코드가 너무 지저분해 진다... 그렇기 때문에 다음 모던 자바스크립트 스터디 포스트에서 쉽게 비동기 함수를 작성할 수 있는 프라미스에 대해 작성하려고 한다.

본 포스트는 다음 문서를 참고해 작성했습니다.

https://ko.javascript.info/promise-basics

https://levelup.gitconnected.com/asynchronous-javascript-part-3-85390632dd1a

https://nodejs.org/en/docs/guides/blocking-vs-non-blocking/