JavaScript
는 브라우저의 표준 프로그래밍 언어이며, 현재 JavaScript
는 브라우저 뿐만 아니라 서버 측에서도 사용된다.
대부분의 F.E.
개발자들은 JavaScript
를 사용한다.
하지만 아마 나를 포함한 많은 사람들에게 "JavaScript가 어떤 언어이며, 그로 인해 발생한 JavaScript의 특징들에 대해 설명해보라."
라는 질문을 던지면 쉽게 대답하지 못할 것이다.
나는 JavaScript
의 사용법. 즉, 구현에만 집중하고 JavaScript
가 무엇인가에 대해 생각해보지 않았다.
이번 포스팅에서는 JavaScript
라는 언어에 대해 알아보고, 그로 인해 발생한 JavaScript
의 특징들에 대해 알아볼 것이다.
본 포스팅은 JavaScript
문법에 대해 설명하지는 않을 것이며, 문법에 대한 내용은 MDN
등을 참고하자.
웹페이지에 생동감을 불어넣기 위해
만들어진 프로그래밍 언어
현재는 웹페이지 뿐 아니라 서버, 모바일 애플리케이션 등 다양한 분야에서 사용된다.
Javascript
는 함수형
, 명령형
, 프로토타입 기반 객체지향
프로그래밍을 지원하는 멀티패러다임 언어이다.
본 포스팅에서는 각 프로그래밍의 특징에만 대해 간략하게 다룰 것이다.
함수형 프로그래밍
은부수 효과(side effect)
가 없는순수 함수(pure function)
를 조합하여 자료 처리를수학적 함수의 계산
으로 취급하는 프로그래밍 패러다임이다.
순수함수
부수효과가 없는 함수
를 사용하여 프로그래밍
불변성
데이터는 변경 불가능
해야 하며, 변경이 필요한 경우 새로운 데이터를 생성
한다.
이를 통해 프로그램의 복잡도를 줄이고, 데이터를 다루는 동작이 예측 가능해진다.
함수의 일급 객체화
함수를 인자
로 전달하거나 반환값
으로 사용할 수 있다.
이를 통해 추상화
수준이 높은 코드를 작성할 수 있다.
고차 함수
함수를 인자로 받거나 반환하는 함수를 의미한다.
이를 통해 코드 재사용성이 높아지고 다양한 동작을 구성할 수 있다.
함수 합성
여러 개의 함수를 함성하여 새로운 함수를 생성한다.
이를 통해 함수를 재사용하고 코드의 가독성을 높일 수 있다.
명령형 프로그래밍
은컴퓨터가 수행
해야 하는 일련의명령문을 정의
하고 실행하는 방식의 프로그래밍 패러다임으로 프로그래머가직접 명령문을 작성
하여 원하는 동작을 수행한다.
명령문
프로그램의 동작을 수행하는 명령문은 변수에 값을 할당하거나, 조건문
과 반복문
을 사용하여 프로그램의 제어 흐름을 결정
한다.
상태
프로그램이 수행되는 동안 상태가 변경
되며, 이러한 상태는 변수, 객체 등으로 나타내며, 이를 통해 프로그램의 동작을 결정한다.
부수 효과
명령문의 수행으로 인해 상태가 변경되는 부수 효과가 발생하며 프로그램의 실행 결과를 예측하기 어렵게
만들 수 있다.
프로토타입 기반 객체지향 프로그래밍
은 객체지향 프로그래밍 패러다임의 하나로, 클래스가 아닌프로토타입
을 이용하여객체를 생성
하는 방식의 프로그래밍입니다.
클래스(class)가 없음
프로토타입 기반 객체지향 프로그래밍에서는 객체를 생성하기 위해 클래스가 없으며, 이미 존재하는 객체(프로토타입)
을 복사하여 새로운 객체를 생성
합니다.
프로토타입(prototype)
프로토타입은 객체의 원형
을 의미한다.
이미 존재하는 객체를 복사하여 새로운 객체를 생성할 때, 복사할 원본 객체
를 프로토타입으로 지정
하며 생성된 객체는 프로토타입의 속성과 메서드를 상속
받는다.
객체 상속(object inheritance)
객체는 다른 객체를 상속받을 수 있으며, 이를 통해 코드의 재사용성
과 유지보수성
을 높일 수 있습니다.
동적 객체 생성(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
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 Stack
과 Heap
을 가지며, 나머지는 런타임에서 제공한다.
Call Stack (콜 스택)
Excution Context Stack(실행 컨텍스트 스택)
이라고도 불린다.
JavaScript
가 싱글 스레드
라는 말은 Call Stack
이 하나라는 말과 같다.
즉, JavaScript
는 하나의 Call Stack
을 가지며 이로 인해 Blocking
이 발생한다.
Heap (힙)
Memory Heap(메모리 힙)
이라고도 불린다.
객체
가 저장되는 메모리 공간
이다.
실행 컨텍스트는 힙에 저장된 객체를 참조한다.
객체
는 원시 값과 달리 크기가 정해져 있지 않으므로 할당해야 할 메모리 공간의 크기
를 런타임
에 동적 할당
해야 한다.
따라서 힙
은 구조화 되어 있지 않다
는 특징이 있다.
사실 원시 값도 실행 컨텍스트에 저장되므로 자바스크립트의 모든 값은 객체로 힙에 저장된다고 할 수 있다.
참고자료
모던 자바스크립트 Deep Dive
Event Loop (이벤트 루프)
이벤트 루프의 역할을 Call Stack
과 Task Queue
를 반복적으로 확인하고, Call Stack
이 비어있다면 우선 순위에 의해 Task Queue
의 첫 번째 Task
를 꺼내와 Call Stack
으로 옮긴다.
ECMAScript 사양
에는 Agent
라고 정의 되어있다.
Task Queue (태스크 큐)
setTimeout
, setInterval
, event
등의 작업이 저장되는 Queue
이다.
아래에 등장하는 Microtask Queue
와 구분하기 위하여 Macrotask Queue
라고도 부른다.
ECMAScript 사양
에는 Job Queue
라고 정의되어있다.
Task Queues는 Queue가 아니라 Set이다.
HTML Spec
을 참고하면Task Queues
는Queue
가 아니라Set
이라고 정의되어 있다.
하지만 이 말은 "Microtask Queue
와Macrotask Queue
가Set
이다" 라는 말이 아니다.
여기서 말하는Task Queues
는Microtask Queue
와Macrotask
를 포함하는 상위 집합으로 이외에 추가Task Queue
가 포함될 수 있다.
참고자료
[HTML Spec] task queues are sets, not queue
https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
MicroTask Queue (마이크로태스크 큐)
Promise
, async/await
, queueMicrotask
, process.nextTick
, MutationObserve
등의 작업이 저장되는 Queue
이다.
MacroTask
보다 우선순위가 높다.
Call Stack
이 비는 순간 Microtask
가 실행된다.
대기중인 Microtask
가 있다면, 모든 Microtask
를 처리 한 후 나머지 Task Queue
를 실행한다.
ECMAScript 사양
에는 PromiseJob queue
라고 정의되어있다.
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
Promise
, MutationObserve
, queueMicrotask
등의 작업이 저장되는 Microtask
는 Macrotask
보다 우선순위가 높다.
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
위 예시를 보면 setTimeout
이 queueMicrotask
와 Promise
보다 먼저 실행됐음에도 불구하고 나중에 출력되는 것을 볼 수 있다.
즉, Microtask Queue
에 대기중인 Task
가 존재하는 경우, Event Loop
는 Microtask
를 우선적으로 처리한다.
만약 Microtask
실행에 의해 추가적인 Microtask
가 생긴다면, 재귀적으로 Microtask
를 실행한다.
반면, Macrotask
의 경우 한 번에 하나 씩만 실행한다.
몇몇 기술블로그를 참고하면 requestAnimationFrame(rAF)
이 Macrotask Queue
에 저장되거나 Animation Frame
에 저장된다고 한다.
하지만 이는 사실이 아니다.
rAF
은 브라우저의 렌더링 엔진에서 처리하며, 단순히 비동기적으로 실행되는 콜백함수이다.
만약, rAF
이 Macrotask 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");
만약 rAF
가 Macrotask Queue
에 저장된다면, start
-> end
-> queueMicro
-> timeout (10,000번 출력)
-> animation (infinity 번)
이 출력될 것 이다.
만약 rAF
가 Animation Frame
에 저장되며 Macrotask Queue
와 Microtask Queue
사이의 우선순위를 갖는다면, start
-> end
-> queueMicro
-> animate (infinity 번)
-> timeout (출력 X)
이 출력될 것이다.
하지만, 실제로 브라우저 콘솔창에 해당 코드를 실행시켜 보면 다음과 같은 결과를 볼 수 있다.
위의 결과는 rAF
가 Task Queue
가 아니라 어딘가에서 별도로 관리된다는 것을 의미한다.
rAF
는 event 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
문 내에 있는 비동기 API
가 Task Queue
로 넘어가고 Promise
(혹은 setTimeout
)의 콜백함수는 try-catch
문과는 별개의 실행컨텍스트에서 실행되기 때문이다.
즉, Call Stack
에서 try-catch
문 실행컨텍스트가 삭제된 후, Promise
가 실행된다는 것이다.
따라서 Promise
에서 실행된 error
는 try-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,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>
위 코드를 실행하면 i
가 100
증가할때마다 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>
위 코드를 실행하면, 첫 번째 코드와 마찬가지로 렌더링이 한번만 발생한다.
그 이유는 Microtask
는 Microtask Queue
에 있는 모든 task
를 실행해야 다음 스케줄링을 발생시키기 때문이다.
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