일반 함수와 같이 함수의 코드블록을 한번에 실행하지 않고 필요할때 재시작할 수 있는 특수한 함수이다.
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}
브라우저 관점에서 입력이란 모든 사용자의 제스처(이벤트)를 의미한다. 스크롤도 이벤트이고, 화면 터치, 마우스 포인터를 올리는거도 입력 이벤트이다.
화면 터치와 같은 사용자 제스처가 발생했을 때 가장 먼저 제스처를 수신하는 것은 브라우저 프로세스이다. 브라우저 프로세스는 제스처가 어디에서 발생했는지만 알고 있다. 탭 내부의 콘텐츠는 렌더러 프로세스가 처리해야 한다.
그래서 브라우저 프로세스는 이벤트 종류와 좌표값(x,y 등)만 렌더러 프로세스에 넘기게 된다.
렌더러 프로세스는 이벤트 대상과 이벤트 리스너를 실행해 이벤트를 적절히 처리한다.
스크롤의 예시로 렌더러 프로세스 내부동작을 알아보자.
스크롤할때 레스터화된 레이어를 컴포지터가 합성해 스크롤을 부드럽게 처리한다.
이벤트리스너가 없다면 컴포지터 쓰레드는 메인 쓰레드와 상관없이 스스로 합성해서 프레임을 만들어낼 수 있다.
그런데 이벤트가 달려있다면 다르게 동작하게 된다.
합성하는 쓰레드는 컴포지트 쓰레드이고,
이벤트를 실제로 처리하는 쓰레드는 JS를 동작시키는 메인 쓰레드이다.
어떻게 둘이 상호작용을 하게 될까?
고속 스크롤 불가 영역(non-fast scrollable region)
JS가 실행되는 작업은 메인 스레드의 영역이므로 이벤트 핸들러가 연결된 영역을 고속 스크롤 불가 영역(non-fast scrollable region)이라고 표시한다. 이 영역에서 이벤트가 발생하면 렌더러 프로세스는 우선 컴포지터 스레드에게 작업을 보내고, 컴포지트 스레드는 입력 이벤트를 메인 스레드로 보내야하는지 정보를 확인 후 넘겨준다.
만약 고속 스크롤 불가 영역이 아니라면 컴포지터 스레드는 메인 스레드를 기다리지 않고 새 프레임을 합성한다.
(컴포지터 스레드는 복사된 자신만의 레이어 트리를 가지고 있는다. - pending tree -> active tree?)
passive : true 옵션을 주면 부드럽게 스크롤이 된다는거까지는 알았다. 그런데 스크롤을 JS의 event.preventDefault() 함수로 제한한다면 쓰레드가 다르므로 event.preventDefault() 가 실행되기 전 이미 수직 스크롤이 실행될 수도 있다. 그래서 event.cancelable()를 사용하면 스크롤 시작 여부를 확인 가능하다.
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
======
역주 :모질라에 따르면 passive : true는 event.preventDefault()가 내부에서 실행이 안될꺼라는 약속이라는데
By marking a touch or wheel listener as passive,
the developer is promising the handler won't call preventDefault to disable scrolling.
This frees the browser up to respond to scrolling immediately without waiting for JavaScript,
thus ensuring a reliably smooth scrolling experience for the user.
라는데 원글의 예시 코드는 이해를 돕기 위한 '예시'인지 좀 혼란스럽긴 하다..
일단 스택오버플로우와 원글의 공통점을 추론해보자면
passive : true를 설정하면 메인 쓰레드(JS)가 작동하지 않고 스크롤 이벤트때 컴포지트 쓰레드가 바로 동작하는 개념은 맞는듯 싶다.
======
(아무튼 계속 시작)
또는 아에 touch-action에서 아예 수직 스크롤 이벤트를 지워버릴수도 있다.
#area {
touch-action: pan-x;
}
이벤트 대상 찾기
컴포지터 쓰레드가 메인 스레드로 보낼때 이벤트 대상을 찾는 hit test를 한다.
이벤트가 발생한 좌표에 무엇이 있는지 확인을 위해 paint recoards data를 이용한다.
(브라우저의 렌더링 단계에서 파싱->스타일->layout->paint->composite 에서 paint)
(아마 opcacity, transform등의 정보를 따로 담고 있는 property tree의 정보도 참고하기 위해서
paint tree를 참고하는거로 추론?)
메인 스레드 이벤트 전송 최소화
일반적으로 모니터는 1초당 60번 (60프레임) 화면을 갱신한다. 그리고 애니메이션도 화면 갱신 주기에 맞춰야
부드럽게 움직인다.
일반적으로 touchEvent는 초당 60~120회 전달하고, 마우스는 초당 100회정도 전달한다.
즉, 화면 갱신보다 이벤트 갱신 주기가 더 짧아서 이벤트가 뚝뚝 끊겨보이는 상황을 연출할 수도 있다.
그래서 메인 호출이 과다해지는것을 막기 위해서 Chrome은 자체적으로 연속적(coalesces continuous)인 이벤트(wheel,mousewheel,mousemove,pointermove,touchmove)는 다음번 requestAnimationFrame() 메서드 실행 직전까지 전송하지 않고 기다리게 된다.
keydown,keyup,mouseup,mousedown,touchstart,touchend와 같은 비연속적인(discrete) 이벤트는 즉시 전달된다.
한 프레임 안에서 합쳐진 이벤트 정보를 얻기 위해 getCoalescedEvent() 사용
드로잉 앱 같은 경우 프레임 사이 사이 경로가 누락되 선을 매끄럽게 그리지 못할 수도 있다. 이때 getCoalesecedEvents를 사용하면 합쳐진 이벤트의 정보를 얻을 수 있다.
그리고 미디어 쿼리를 이용해 자신이 사용하는 미디어 타입에 걸맞는 css만 받아오고, 다른 기기의 css는 받아오지 않는 식으로 (용량을 줄여) 최적화 할 수 있다.
2) 스타일
dom tree와 CSSOM tree가 생성되었다면 결합해서 Render Tree를 만든다.
렌더링 트리에는페이지를 렌더링하는 데 필요한 노드만 포함된다.
그리고 어떤 렌더 객체는 DOM 노드에 대응하지만 트리의 동일한 위치에 있지 않다. float 처리된 요소 또는 position 속성 값이 absolute로 처리된 요소는 흐름에서 벗어나 트리의 다른 곳에 배치된 상태로 형상이 그려진다. 대신 자리 표시자가 원래 있어야 할 곳에 배치된다.
3) 레이아웃(재시작시 Reflow)
레이아웃은 요소들의 기하학적인 구조를 찾는 과정이다.
현재 viewport 기준으로 정확히 DOM이 어디있는지 위치, 크기를 계산한다.
만약 CSS나 돔의 layout과 관련된 요소(width, font-size 등)가 변경된다면 reflow 과정이 생길수도 있다.
추가적으로
display: none을 사용하면 렌더링 트리에서 요소가 완전히 제거된다.
visibility : hidden을 사용하면 레이아웃에서 공간을차지하지만 요소가 보이지 않는다.
그리고 레이아웃 트리를 순회하면서 속성 트리(property tree)를 만드는 작업이 페인트와 레이아웃 사이에 끼여있다.
속성 트리는 clip, transform, opacity등의 속성 정보만 가진 트리이다.
최신 Chrome에서는 이런 속성만 별도로 관리하고 각 노드에서는 속성 트리의 노드를 참조하는 방식으로 변경되고 있다.
4) 페인트(재시작시 repaint)
레이아웃 트리를 따라가 페인트 기록을 생성한다.
배경 이미지, 텍스트 색상, 그림자등 실제 레이아웃의 수치를 변형시키지 않는 스타일의 변경이 일어나면
repaint만 발생한다.
일반적으로 reflow가 발생되면 repaint도 같이 발생한다.
(레이아웃 수치를 다시 계산해서 배치를 해야하기 때문에)
5. 합성(composite)
페이지를 그리는 방법
컴포지팅은 한 페이지의 부분들을 여러 레이어로 나누고 그 것들을 각각 레스터화하며 컴포지터 스레드에서 페이지를 합성하는 기술이다. 그리고 만약 스크롤이 발생하면, 레이어들이 이미 레스터되었기 때문에, 해야 할 것은 새로운 프레임을 합성하는 것이다.
에니메이션은 레이어들을 움직이는 동일한 방식으로 이뤄지고 새로운 프레임을 합성한다.
어떻게 웹 사이트가 여러 레이어로 나뉘는 지 개발자 도구의Layers panel에서 볼 수 있다.
참고) 레스터화와 벡터화
레스터화는 각 요소 이미지를 컴퓨터가 이해할 수 있는 비트맵으로 변환하고, 하나의 비트맵으로 합성하는 과정이다.
그리고 OS는 프로세스에게 메모리 한 조각을 주고 모든 상태 정보를 고유 메모리 공간에 저장할 수 있게 한다. 어플리케이션이 끝나면 프로세스도 사라지고 OS가 메모리를 해제한다.
(개인적 생각으로 여기서 말하는 메모리는 RAM 메모리를 뜻하는것으로 추론된다.)
프로세스간 통신 - Inter Process Communication (IPC)
그리고 프로세스는 다른 프로세스에게 별도의 작업을 수행하도록 요청 혹은 통신이 가능하다. 많은 어플리케이션이 이 방식을 채택하고 있어 워커 프로세스가 무응답 상태에 빠지더라도 어플리케이션의 다른 부분을 수행하고 있는 프로세스들을 종료할 필요 없이 해당 프로세스만 재시작할 수 있다.
선언 단계(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이 끝난 후 다음 계획을 실행한다.
그래서 Date 객체의 now 값을 이용해서 실시간으로 다음 계획까지 걸리는 타이머를 보정받는다.
콜스택에 의해 지금 function의 실행이 늦었다면 다음 실행의 경우 늦은만큼 보정한다.
하지만 브라우저가 inactive(다른 화면을 보고있다던지)하게 되면 타이머는 정지되게 되고, 다시 브라우저가 활성화된다면 그때 황급히 카운트를 쭉 세게 된다.