빌드된 tttable(임시 작명이라 가명임;;) 레포의 index.js를 required 하면 오류가 나는것
평소에 commonjs와 esm, amd 등이 뭔지 헷갈려서 이참에 정리겸 간단하게 요약해본다.
CommonJS(CJS)란?
// 모듈 정의
const circle = require('./circle.js');
const radius = 5;
console.log(`반지름 ${radius}인 원의 넓이: ${circle.area(radius)}`);
console.log(`반지름 ${radius}인 원의 둘레: ${circle.circumference(radius)}`);
JavaScript를 위한 모듈 로딩 시스템 표준 중 하나이다.
Node.js와 같은 서버 측 JavaScript 환경에서 사용되며, 모듈을 정의하고 로드하기 위한 표준화된 방법을 제공한다.
node.js나 프론트엔드의 config 파일에서 많이 본 require를 사용한 방식이 commonJS 방식이다.
초기 Javascript는 모듈 시스템이 없었기 때문에 모듈화를 위해 commonJS, AMD 등이 개발되어 사용되었다.
commonJS는 동기적으로 모듈을 로드하기 위해 사용되고,
비동기를 따로 다루기 위해 AMD(Asynchoronous Module Definition)이 비동기적으로 모듈을 다루는 것에 대한 표준안으로 제시되었다고 한다.
ECMAScript Module(ESM)이란?
// 모듈 불러오기
import circle from './circle.mjs';
// 모듈 사용
const radius = 5;
console.log(`반지름 ${radius}인 원의 넓이: ${circle.area(radius)}`);
console.log(`반지름 ${radius}인 원의 둘레: ${circle.circumference(radius)}`);
ES6부터 표준화 된 모듈 시스템으로, 클라이언트에서 흔히 사용하는 import, export를 사용하는 방식이다.
브라우저에서 사용하기 위해서는 type="module"을 script 혹은 package.json에 명시해주면 된다.
자바스크립트는 비동기 함수를 처리하기 위해서 이벤트루프라는 방식을 사용하는데 이벤트 루프는 지금 콜스텍이 비었는지(실행하고 있는 task가 없는지)와 테스크 큐(들)에 지금 실행 가능한 task가 있는지를 반복적으로 확인한다. 그래서 비동기 함수는 큐에서 대기하다가 실행이 가능해지면 꺼내져와서 실행하는데, promise같은 경우에는 실행 우선 순위가 매우 높아서(마이크로 테스크큐) 실행 가능해지면 먼저 실행되게 된다.
async/await 함수의 동작 순서
1. await 키워드가 붙은 대상을 실행한다.
2. 1번 대상이 함수가 아니거나, 함수의 실행이 끝나면 '1번 대상을 실행한' async 함수를 일시정지하고 콜스텍에서 마이크로 테스크 큐에 옮긴다. 이때 await를 실행한 위치를 기억한다.
3. 지금 실행하고 있던 함수가 콜스텍에서 빠져나왔으니 다른 콜스텍을 실행한다.
4. 콜스텍이 비었으면 2번에서 빠져 나왔던 async 함수를 다시 콜스택으로 옮겨 실행한다. 이때 await가 실행된 위치 다음부터 실행된다.
P.S) 바벨로 트렌스파일 말고 진짜 Generator로 변환하는가 찾아봤는데 V8 주석의 경우 변환한다고 적혀있긴 하다..
// ES#abstract-ops-async-function-await
// AsyncFunctionAwait ( value )
// Shared logic for the core of await. The parser desugars
// await value
// into
// yield AsyncFunctionAwait{Caught,Uncaught}(.generator_object, value)
// The 'value' parameter is the value; the .generator_object stands in
// for the asyncContext.
일반 함수와 같이 함수의 코드블록을 한번에 실행하지 않고 필요할때 재시작할 수 있는 특수한 함수이다.
value 값으로 yield 값이 리턴되고, done 값으로 언제 끝날지 알려주게 된다.
generator를 사용하면
다음과 같은 방식으로도 구현 가능하다.
function *ex(){
let a = yield;
let b = yield;
yield a+b;
}
const test = ex();
test.next();
test.next(1); // 인자전달
console.log(test.next(2)); // {value: 3, done: false}
선언 단계(Declaration phase) :변수를 실행 컨텍스트의 변수 객체에 등록하는 단계를 의미합니다. 이 변수 객체는 스코프가 참조하는 대상이 됩니다.
초기화 단계(Initialization phase) :실행 컨텍스트에 존재 하는 변수 객체에 선언 단계의 변수를 위한 메모리를 만드는 단계 입니다. 이 단계에서 할당된 메모리에는 undefined로 초기화 됩니다.
할당 단계(Assignment phase) :사용자가 undefined로 초기화된 메모리의 다른 값을 할당하는 단계 입니다.
모든 변수는 생성되면 위 3단계를 거쳐 생성됩니다.
그래서 자바스크립트가 실행되면 모든 변수들은 평가단계를 거치게 되는데, 호이스팅이란 모든 변수들을 끌어 모아서 각 유효범위(블록 {}안)에 최상단에 선언하는것을 말합니다.
var, let, const 모두 위 3단계를 거칩니다.
다만 var는 선언단계와 초기화 단계가 호이스팅 단계에서 동시에 실행됩니다.
그래서 아직 할당이 되지 않았는데 var로 생성된 변수를 부르게 되면 undefined를 되돌려 줍니다.
let,const는 선언은 되지만 초기화는 되지 않고, 할당이 되기 전까지 TDN(temporal dead zone)이라는 곳에 등록됩니다. 그래서 할당이 되지 않았는데 불러온다면 TDN 구간에 있는 변수들은 참조 에러(Reference Error)를 발생시키게 됩니다.
즉, 호이스팅은 모두 발생하지만 var와 let,const는 다루는 방법에 차이가 있게 되는것입니다.
다른 언어에서 흔히 쓰는 변수들은 구조적으로는 약간씩 다르겠지만 결론적으로 let, const와 동일하게 쓰인다고 이해하시면 편합니다.
참고로 함수 선언문은 위 3가지가 동시에 진행됩니다.
함수 선언식 예시
function func { ... }
그래서 어디서 선언하든 유효 범위 (스코프) 내에서는 사용할 수 있습니다.
함수 표현식 예시
함수 표현식은 변수처럼 동작하고 호이스팅 됩니다.
constfunc= ()=>{ ... }
따라서 var, const, let중 뭘 썼는지에 따라 호이스팅되고 변수처럼 처리됩니다.
호이스팅을 하는 이유는 현재의 실행 컨텍스트의 지금 스코프 내부에 어떤 식별자들이 있는지 탐색하고, 렉시컬 환경에 저장(기록) 해놓기 위해서 입니다.
즉.. 자바스크립트의 모든 핵심 개념이 서로 연동되있다고 볼 수 있을꺼 같은데요
자세히 알아보시려면 JavaScript deep dive란 책을 읽어보시길 추천 드립니다!
그리고 이거에 대해서 재밌는 문제가 하나 있는데요,
클로저 개념과 연동되는 문제이니 한번 생각해 보시면 좋습니다.
for(var i =0; i <100; i+=1){
setTimeout(function(){
console.log(i);
},i*1000);
}
해당 코드를 실행시키면 0, 1, 2, 3, 4가 출력되는게 아니라 100, 100, 100... 이 출력되게 되는데
자바스크립트에서 타이머 이벤트를 쓸 수 있는 방법으로는 setTimeout, setInterval 두가지가 있다.
이 함수들이 어떻게 비동기를 지원할까?
자바스크립트는 단일 쓰레드 기반의 언어이지만 이벤트 루프를 사용해서 동시성을 지원할 수 있다.
타이머를 실행시키면 Web API에서 타이머의 기한이 끝날때까지 대기하다가 테스크 큐로 들어오게 된다.
콜스택이 비었다면 테스크 큐에서 테스크를 가져와 실행한다.
이 말이 무엇인가를 다시 풀어써보기로 하자.
우선 Web API가 뭔지부터 알아야 할것 같다.
Web API 와 Javascript 런타임
런타임이란 해당 프로그램 언어가 작성된 코드가 구동되는 환경을 뜻하고, 웹 브라우저나 Node.js 가 대표적 JS 런타임이다. 그리고 JS 이외에 다양한 기능을 제공하기 위해서 브라우저에서는 웹 API가 여러 함수(setTimeout등)을 지원하고, Node에서는 화면의 예시처럼 fs 등 함수를 지원하게 된다.
브라우저 환경 구조
자바스크립트 엔진
-Heap: 객체들은 힙 메모리에 할당된다. 크기가 동적으로 변하는 값들의 참조 값을 갖고 있다. -Call Stack: 함수 호출 시, 실행 컨텍스트가 생성되며, 이러한 실행 컨텍스트들이 콜 스택을 구성한다.
Web API or Browser API
- 웹 브라우저에 구현된 API이다. - DOM event, AJAX, Timer 등이 있다.
이벤트 루프
- 콜 스택이 비었다면, 태스크 큐에 있는 콜백 함수를 처리한다. (이후 자세히 설명)
태스크 큐
- 비동기 함수(setTimeout, setInterval 등)는 처리 전 이곳에서 대기하게 된다. - 이벤트 루프는 하나 이상의 태스크 큐를 갖는다. - 태스크 큐는태스크의 Set이다. - 이벤트 루프가 큐의 첫 번째 태스크를 가져오는 것이 아니라, 태스크 큐에서 실행 가능한(runnable) 첫 번째 태스크를 가져오는 것이다. 태스크 중에서 가장 오래된 태스크를 가져온다.
그리고, 자바스크립트가 단일 쓰레드 기반이라는 말이 정확히는 '자바스크립트 엔진'은 단일 호출 스택을 사용한다 라는 것이다.
위 그림같은 경우는 Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 이벤트 루프를 제공한다. 브라우저에서는 Web API가 setTimeout 같은 함수를 지원하며, 타이머를 JS의 싱글 쓰레드가 아닌, 브라우저의 자체 쓰레드에서 세다가 자바스크립트의 테스크 큐에 넣어주게 된다.
즉, 실제 자바스크립트가 구동되는 환경(브라우저, Node.js)는 여러개의 쓰레드를 사용하고, 이 다중 쓰레드 환경과 자바스크립트 엔진이 상호 연동하기 위해 내부적으로 이벤트 루프를 이용해 처리하게 된다.
(또 다른 예시로는 Node.js에서 I/O를 처리할때 자바스크립트의 싱글 쓰레드로만 동작하는게 아니고, 커널 쓰레드에서 처리하는것?이라 볼 수 있을것 같다?)
그럼 자바스크립트가 어떻게 비동기 함수를 처리하는지, 이벤트 루프를 어떻게 사용하는지 예시를 보자
function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
bar();
console.log('foo!'); // (3)
}
function bar() {
delay();
console.log('bar!'); // (2)
}
function baz() {
console.log('baz!'); // (4)
}
setTimeout(baz, 10); // (1)
foo();
위 코드를 작동시키면 setTimeout으로 타이머를 걸어놓은 baz가 정확히 10ms 이후에 실행되지 않는다.
그 이유는 자바스크립트가 비동기 함수를 지원하는 방식은 '이벤트 루프'를 사용해서 구현하고,
이벤트 루프는 '콜스텍에 현재 실행중인 task가 없는지'와 'task 큐에 태스크가 있는지'를 반복적으로 확인한다.
그리고 현재 실행중인 테스크가 없다면 이제서야 테스크 큐에 담겨있는 테스크를 꺼내와서 실행하게 되는것이다.
즉, 콜스택이 지금 바쁘다면 타이머 함수는 제 시간에 실행되지 않을 수 있다.
이를 하나의 함수가 실행이 완료되기 전에는 다른 함수가 실행될 수 없는"Run to Completion" 라 부른다.
아래 코드 또한 좋은 예시이다.
let i = 0;
setTimeout(() => alert(i), 100); // ?
// 아래 반복문을 다 도는 데 100ms 이상의 시간이 걸린다고 가정합시다.
for(let j = 0; j < 100000000; j++) {
i++;
}
const interval = INTERVAL; // 1000ms
let time = Date.now() + interval;
let timerId = -1;
const exactlyTimeInterval = () => {
const dt = Date.now() - time;
time += interval;
if (dt > interval) {
// something really bad happened. Maybe the browser (tab) was inactive?
// possibly special handling to avoid futile "catch up" run
// 브라우저 말고 다른곳을 보고 있다면 브라우저의 타이머가 멈춰있고, 제대로 작동하지 않는다.
// 그때 이 if문 안에 들어오게 된다
}
...(생략)
timerId = setTimeout(exactlyTimeInterval, Math.max(0, interval - dt));
};
timerId = setTimeout(exactlyTimeInterval, interval);
setTimeout를 재귀적으로 실행하면 다음과 같이 function이 끝난 후 다음 계획을 실행한다.
setTimeout 예시
그래서 Date 객체의 now 값을 이용해서 실시간으로 다음 계획까지 걸리는 타이머를 보정받는다.
콜스택에 의해 지금 function의 실행이 늦었다면 다음 실행의 경우 늦은만큼 보정한다.
하지만 브라우저가 inactive(다른 화면을 보고있다던지)하게 되면 타이머는 정지되게 되고, 다시 브라우저가 활성화된다면 그때 황급히 카운트를 쭉 세게 된다.