JavaScript란? (비동기 프로그래밍의 관점에서)

최윤석·2023년 3월 9일
1

JavaScript

목록 보기
2/2
post-thumbnail

⭐포스팅 목표

JavaScript는 브라우저의 표준 프로그래밍 언어이며, 현재 JavaScript는 브라우저 뿐만 아니라 서버 측에서도 사용된다.

대부분의 F.E. 개발자들은 JavaScript를 사용한다.

하지만 아마 나를 포함한 많은 사람들에게 "JavaScript가 어떤 언어이며, 그로 인해 발생한 JavaScript의 특징들에 대해 설명해보라."라는 질문을 던지면 쉽게 대답하지 못할 것이다.

나는 JavaScript의 사용법. 즉, 구현에만 집중하고 JavaScript가 무엇인가에 대해 생각해보지 않았다.

이번 포스팅에서는 JavaScript라는 언어에 대해 알아보고, 그로 인해 발생한 JavaScript의 특징들에 대해 알아볼 것이다.

본 포스팅은 JavaScript 문법에 대해 설명하지는 않을 것이며, 문법에 대한 내용은 MDN 등을 참고하자.




자바스크립트란?


정의

웹페이지에 생동감을 불어넣기 위해 만들어진 프로그래밍 언어

현재는 웹페이지 뿐 아니라 서버, 모바일 애플리케이션 등 다양한 분야에서 사용된다.



자바스크립트의 특징


1. 자바스크립트는 멀티패러다임 언어이다.

Javascript함수형, 명령형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티패러다임 언어이다.

본 포스팅에서는 각 프로그래밍의 특징에만 대해 간략하게 다룰 것이다.


Functional Programming

함수형 프로그래밍부수 효과(side effect)가 없는 순수 함수(pure function)를 조합하여 자료 처리를 수학적 함수의 계산으로 취급하는 프로그래밍 패러다임이다.

특징

  1. 순수함수
    부수효과가 없는 함수를 사용하여 프로그래밍

  2. 불변성
    데이터는 변경 불가능해야 하며, 변경이 필요한 경우 새로운 데이터를 생성한다.
    이를 통해 프로그램의 복잡도를 줄이고, 데이터를 다루는 동작이 예측 가능해진다.

  3. 함수의 일급 객체화
    함수를 인자로 전달하거나 반환값으로 사용할 수 있다.
    이를 통해 추상화 수준이 높은 코드를 작성할 수 있다.

  4. 고차 함수
    함수를 인자로 받거나 반환하는 함수를 의미한다.
    이를 통해 코드 재사용성이 높아지고 다양한 동작을 구성할 수 있다.

  5. 함수 합성
    여러 개의 함수를 함성하여 새로운 함수를 생성한다.
    이를 통해 함수를 재사용하고 코드의 가독성을 높일 수 있다.


Imperative Programing

명령형 프로그래밍컴퓨터가 수행해야 하는 일련의 명령문을 정의하고 실행하는 방식의 프로그래밍 패러다임으로 프로그래머가 직접 명령문을 작성하여 원하는 동작을 수행한다.

  1. 명령문
    프로그램의 동작을 수행하는 명령문은 변수에 값을 할당하거나, 조건문반복문을 사용하여 프로그램의 제어 흐름을 결정한다.

  2. 상태
    프로그램이 수행되는 동안 상태가 변경되며, 이러한 상태는 변수, 객체 등으로 나타내며, 이를 통해 프로그램의 동작을 결정한다.

  3. 부수 효과
    명령문의 수행으로 인해 상태가 변경되는 부수 효과가 발생하며 프로그램의 실행 결과를 예측하기 어렵게 만들 수 있다.


Prototype-based Object-Oriented Programming

프로토타입 기반 객체지향 프로그래밍은 객체지향 프로그래밍 패러다임의 하나로, 클래스가 아닌 프로토타입을 이용하여 객체를 생성하는 방식의 프로그래밍입니다.

  1. 클래스(class)가 없음
    프로토타입 기반 객체지향 프로그래밍에서는 객체를 생성하기 위해 클래스가 없으며, 이미 존재하는 객체(프로토타입)을 복사하여 새로운 객체를 생성합니다.

  2. 프로토타입(prototype)
    프로토타입은 객체의 원형을 의미한다.
    이미 존재하는 객체를 복사하여 새로운 객체를 생성할 때, 복사할 원본 객체프로토타입으로 지정하며 생성된 객체는 프로토타입의 속성과 메서드를 상속받는다.

  3. 객체 상속(object inheritance)
    객체는 다른 객체를 상속받을 수 있으며, 이를 통해 코드의 재사용성유지보수성을 높일 수 있습니다.

  4. 동적 객체 생성(dynamic object creation)
    객체를 생성할 때, 프로토타입을 지정하거나, 객체의 속성과 메서드를 동적으로 추가하거나 제거할 수 있습니다.

JavaScript에서 Prototype은 굉장히 중요한 개념이다.

본 포스팅에서 해당 내용을 다루기에는 내용이 많고 심도있는 내용이므로 추후 포스팅을 통해
추가 학습을 할 수 있는 링크를 남긴다.

프로토타입 기반 언어, 자바스크립트
https://ui.toast.com/weekly-pick/ko_20160603
자바스크립트는 왜 프로토타입을 선택했을까
https://medium.com/@limsungmook/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%99%9C-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85%EC%9D%84-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EA%B9%8C-997f985adb42


2. 자바스크립트는 싱글 스레드 언어이다.


자바스크립트는 왜 싱글스레드를 선택했을까?

JavaScript가 개발될 당시 가장 인기있었던 언어는 Java였다.

Java는 대표적인 멀티 스레드 언어로서 웹사이트를 구현하려는 개발자들에게는 다소 무겁고 어려운 언어였다.

만약 JavaScript멀티 스레드 방식을 선택하게 된다면, 동시성 문제, 데드락 문제, 컨텍스트 스위칭 오버헤드, 병렬성 문제 등을 해결해야 했다.

JavaScript는 브라우저에서 동작하며, 동적인 기능을 수행하는 보조적인 역할을 위해 개발된 언어이다.

따라서, JavaScript는 이러한 문제점을 방지하고 안전하게 해결할 수 있으며 멀티 스레드 방식보다 간단한 싱글 스레드 방식을 선택했다.

동시성 문제

멀티스레드 환경에서 여러 개의 스레드가 동시에 같은 자원에 접근하게 되면, 데이터의 일관성을 보장할 수 없는 문제가 발생한다.

데드락 문제

멀티스레드 환경에서, 두 개 이상의 스레드가 서로 상대방이 점유한 자원을 기다리는 상황이 발생하여 무한히 대기하는 문제가 발생한다.

컨텍스트 스위칭 오버헤드

멀티스레드 환경에서 스레드 간에 컨텍스트 스위칭이 발생하면, 오버헤드가 발생하여 처리 성능이 저하됩니다.

병렬성 문제

멀티스레드 환경에서 여러 개의 스레드가 병렬적으로 실행되면, 서로 간섭이 발생하여 성능이 저하되는 문제가 발생한다.

싱글스레드는 단점이 없나요?

싱글 스레드 방식의 가장 큰 문제점은 하나의 작업이 많은 시간을 소비할 경우, 다른 작업들은 대기해야 한다는 것이다.

이러한 현상을 Blocking(블로킹)이라고 한다.


자바스크립트가 블로킹 문제를 해결한 방법

자바스크립트는 블로킹 문제를 해결하기 위해 Event Loop, Task Queue를 도입하고 Callback Pattern, Promise, async/await 방식을 사용했다.

위 단어들은 JavaScript의 동작 방식을 이해하는 핵심 개념들이며 본 포스팅을 통해 위 개념들에 대해 다루도록 한다.




자바스크립트의 비동기 프로그래밍

JavaScript싱글 스레드방식으로 동작한다.

그렇다면 JavaScript싱글 스레드방식 속에서 어떻게 비동기 프로그래밍을 구현했는지 알아보자.

우선 이를 위해서는 JavaScript 엔진의 구조를 알아야 한다.

본 포스팅은 프론트 엔드 개발자의 관점에서 작성되는 포스팅으로 브라우저 환경을 기준으로 한다.

Node.js의 구조는 브라우저의 구조와 유사하지만 다르다.

따라서 Node.js를 다룬다면 본 포스팅 이후 Node.js 환경에 대한 추가적인 공부가 필요하다.


브라우저 환경에서의 자바스크립트 엔진

참고자료
https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax

위 그림은 브라우저 환경에서 JavaScript 런타임의 구성요소이다.

본격적으로 설명하기 전에 각각의 구성요소에 대해 알아보자.

JavaScript엔진은 Call StackHeap을 가지며, 나머지는 런타임에서 제공한다.

  1. Call Stack (콜 스택)
    Excution Context Stack(실행 컨텍스트 스택)이라고도 불린다.
    JavaScript싱글 스레드라는 말은 Call Stack이 하나라는 말과 같다.
    즉, JavaScript는 하나의 Call Stack을 가지며 이로 인해 Blocking이 발생한다.

  2. Heap (힙)
    Memory Heap(메모리 힙)이라고도 불린다.
    객체가 저장되는 메모리 공간이다.
    실행 컨텍스트는 힙에 저장된 객체를 참조한다.
    객체는 원시 값과 달리 크기가 정해져 있지 않으므로 할당해야 할 메모리 공간의 크기런타임동적 할당 해야 한다.
    따라서 구조화 되어 있지 않다는 특징이 있다.
    사실 원시 값도 실행 컨텍스트에 저장되므로 자바스크립트의 모든 값은 객체로 힙에 저장된다고 할 수 있다.

    참고자료
    모던 자바스크립트 Deep Dive

  3. Event Loop (이벤트 루프)
    이벤트 루프의 역할을 Call StackTask Queue를 반복적으로 확인하고, Call Stack이 비어있다면 우선 순위에 의해 Task Queue의 첫 번째 Task를 꺼내와 Call Stack으로 옮긴다.
    ECMAScript 사양에는 Agent라고 정의 되어있다.

  4. Task Queue (태스크 큐)
    setTimeout, setInterval, event 등의 작업이 저장되는 Queue이다.
    아래에 등장하는 Microtask Queue와 구분하기 위하여 Macrotask Queue라고도 부른다.
    ECMAScript 사양에는 Job Queue라고 정의되어있다.

    Task Queues는 Queue가 아니라 Set이다.

    HTML Spec을 참고하면 Task QueuesQueue가 아니라 Set이라고 정의되어 있다.
    하지만 이 말은 "Microtask QueueMacrotask QueueSet 이다" 라는 말이 아니다.
    여기서 말하는 Task QueuesMicrotask QueueMacrotask를 포함하는 상위 집합으로 이외에 추가 Task Queue가 포함될 수 있다.

    참고자료

    [HTML Spec] task queues are sets, not queue
    https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

  1. MicroTask Queue (마이크로태스크 큐)
    Promise, async/await, queueMicrotask, process.nextTick, MutationObserve 등의 작업이 저장되는 Queue이다.
    MacroTask보다 우선순위가 높다.
    Call Stack이 비는 순간 Microtask가 실행된다.
    대기중인 Microtask가 있다면, 모든 Microtask를 처리 한 후 나머지 Task Queue를 실행한다.
    ECMAScript 사양에는 PromiseJob queue라고 정의되어있다.

  2. Wep API
    DOM, XMLHttpRequest, fetch, requestAimationFrame 등 브라우저에서 지원하는 웹용 애플리케이션 프로그래밍 인터페이스이다.


브라우저에서의 비동기 프로그래밍

JavaScript엔진은 단순히 Task가 요청되면 Call Stack을 통해 요청된 작업을 실행한다.

비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 엔진을 구동하는 환경인 브라우저가 담당한다.

예를 들어, 비동기로 동작하는 setTimeout(callback, delay) 함수가 호출되었다고 생각해보자.

JavaScript엔진은 setTimeout 실행컨텍스트를 생성하고 실행한다.

setTimeout함수가 실행되면, callback 함수를 호출 스케쥴링하고 종료한다.

이때, 호출 스케줄링을 담당하는 것은 브라우저의 역할이며 delay이후 Task Queue에 푸시한다.

이후 Call Stack이 비어있게 되면 Event Loop에 의해 callback 함수가 Call Stack에 푸시되고 callback 함수가 실행된다.

아래 예시를 참고하자.

console.log("start")
setTimeout(()=>console.log("timeout"), 0)
console.log("end")

// start
// end
// timeout

Macrotask vs Microtask

Promise, MutationObserve, queueMicrotask 등의 작업이 저장되는 MicrotaskMacrotask보다 우선순위가 높다.

console.log("start")
setTimeout(()=>console.log("timeout"), 0)
queueMicrotask(()=>console.log("queueMicro"))
Promise.resolve().then(()=>console.log("promise"))
console.log("end")

// start
// end
// queueMicro
// promise
// timeout

위 예시를 보면 setTimeoutqueueMicrotaskPromise보다 먼저 실행됐음에도 불구하고 나중에 출력되는 것을 볼 수 있다.

즉, Microtask Queue에 대기중인 Task가 존재하는 경우, Event LoopMicrotask를 우선적으로 처리한다.

만약 Microtask 실행에 의해 추가적인 Microtask가 생긴다면, 재귀적으로 Microtask를 실행한다.

반면, Macrotask의 경우 한 번에 하나 씩만 실행한다.


requestAnimationFrame

몇몇 기술블로그를 참고하면 requestAnimationFrame(rAF)Macrotask Queue에 저장되거나 Animation Frame에 저장된다고 한다.

하지만 이는 사실이 아니다.

rAF은 브라우저의 렌더링 엔진에서 처리하며, 단순히 비동기적으로 실행되는 콜백함수이다.

만약, rAFMacrotask Queue 혹은 Animation Frame에 저장된다고 가정하고 아래 예시 코드를 보자.

function animate() {
  console.log("animate");
  requestAnimationFrame(animate);
}

console.log("start");
for (let i = 0; i < 1e4; i++) {
  setTimeout(() => console.log("timeout"), 0);
}
requestAnimationFrame(animate)
// requestAnimationFrame(() => console.log("animation"));
queueMicrotask(() => console.log("queueMicro"));
console.log("end");

만약 rAFMacrotask Queue에 저장된다면, start -> end -> queueMicro -> timeout (10,000번 출력) -> animation (infinity 번) 이 출력될 것 이다.

만약 rAFAnimation Frame에 저장되며 Macrotask QueueMicrotask Queue 사이의 우선순위를 갖는다면, start -> end -> queueMicro -> animate (infinity 번) -> timeout (출력 X) 이 출력될 것이다.

하지만, 실제로 브라우저 콘솔창에 해당 코드를 실행시켜 보면 다음과 같은 결과를 볼 수 있다.

위의 결과는 rAFTask Queue가 아니라 어딘가에서 별도로 관리된다는 것을 의미한다.

rAFevent loop에 의해 별도의 렌더링 규칙을 따르며, 이는 브라우저가 관리한다.

아래 참고 자료는 반드시 읽어보자

⭐⭐참고 자료
[HTML Spec] event loop processing model
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
[HTML Spec] spin the event loop
https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop
Jake Archibald: 루프 속 - JSConf.Asia
https://www.youtube.com/watch?v=cCOL7MC4Pl0


비동기 프로그래밍에서의 예외처리

아래 코드의 결과를 생각해보자.

try {
  Promise.resolve().then(function () {
    throw "에러가 발생했다!";
  });
  // setTimeout(()=> { throw "에러가 발생했다!" }, 1000)
} catch (e) {
  console.log(e);
  console.log("에러에서 복구됐다.");
}

언뜻 보기에는 에러가 발생했다! -> 에러에서 복구됐다.가 출력될 것 같아보인다.

하지만, 실제로 해당 코드를 실행시켜보면 에러가 발생하여 코드가 중단된다.

왜 이런 일이 발생할까?

이는 try문이 실행되면 try-catch문 내에 있는 비동기 APITask Queue로 넘어가고 Promise (혹은 setTimeout)의 콜백함수는 try-catch문과는 별개의 실행컨텍스트에서 실행되기 때문이다.

즉, Call Stack에서 try-catch문 실행컨텍스트가 삭제된 후, Promise가 실행된다는 것이다.

따라서 Promise에서 실행된 errortry-catch문의 catch에 영향을 미치지 못한다.

이를 해결하기 위해 비동기 프로그래밍을 할 때에는 콜백함수 내에 try-catch를 포함하거나 Promise를 사용할 경우 .catch()를 사용해야 한다.

위의 코드를 개선해보자.

Promise.resolve()
  .then(function () {
    throw "에러가 발생했다!";
  })
  .catch((e) => {
    console.log(e);
    console.log("에러에서 복구됐다.");
  });

setTimeout(() => {
  try {
    throw "에러가 발생했다!";
  } catch (e) {
    console.log(e);
    console.log("에러에서 복구됐다.");
  }
}, 1000);

우리가 기대한 것처럼 에러가 발생했다! -> 에러에서 복구됐다.가 출력되는 것을 볼 수 있다.


비동기 프로그래밍의 활용

간단한 활용 예제들을 마지막으로 포스팅을 마치겠다.

1. 프로그레스 바

1 부터 1,000,000 까지 증가하는 숫자를 보여주는 progress 태그를 만든다고 생각해보자.

<div id="progress"></div>

<script>

  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }

  count();
</script>

위 예시는 count 함수가 종료될때까지 Blocking이 발생하며 count 함수가 종료된 뒤 1,000,000이 한번만 출력된다.

이를 해결하기 위해 setTimeout으로 task를 나눠 렌더링을 발생시킬 수 있다.

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // 무거운 작업을 쪼갠 후 이를 수행
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e6) {
      setTimeout(count);
    }

  }

  count();
</script>

위 코드를 실행하면 i100증가할때마다 Task Queue에 푸시한다.

Task Queue에 있는 task는 한번에 하나씩만 실행되므로 100 단위로 렌더링이 발생한다.

이번에는 Task Queue가 아닌 Microtask Queue를 사용해보자.

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // 무거운 작업을 쪼갠 후 이를 수행
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e6) {
      queueMicrotask(count);
    }

  }

  count();
</script>

위 코드를 실행하면, 첫 번째 코드와 마찬가지로 렌더링이 한번만 발생한다.

그 이유는 MicrotaskMicrotask Queue에 있는 모든 task를 실행해야 다음 스케줄링을 발생시키기 때문이다.

참고자료
https://ko.javascript.info/event-loop#ref-250


2. 배칭 (Batching)

Microtask Queue를 활용하면 배칭을 적용할 수 있다.

아래 코드는 messageQueue에 모든 message를 담아 한번에 출력하는 예시이다.

const messageQueue = [];

let sendMessage = (message) => {
  messageQueue.push(message);

  if (messageQueue.length === 1) {
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;
      console.log(json);
    });
  }
};

sendMessage("1")
sendMessage("2")
sendMessage("3")

만약 추가된 메시지가 첫 메시지인 경우, 메시지를 전송하는 Microtask를 예약하고 Call Stack이 비게 된다면 messageQueue에 있는 메시지들을 한번에 출력한다.

즉, Microtask를 활용하여 반복되는 요청을 줄일 수 있으며, Call Stack이 비었을때 가장 먼저 발생할 task를 지정할 수 있다.

참고자료
https://developer.mozilla.org/ko/docs/Web/API/HTML_DOM_API/Microtask_guide#%EC%98%88%EC%A0%9C

profile
프론트엔드를 공부하고 있는 주니어 개발자입니다.

0개의 댓글