아래 내용은 You Don't Know JS의 this 관련 내용을 정리한 것임.

 

this는 작성 시점이 아닌 런타임 시점, 함수가 호출되는 상황에 따라 콘텍스트가 결정된다.
this 바인딩이 일어날 때 함수 호출부를 확인하여 this가 무엇을 가리키는지 알아야 한다.

아래 코드는 호출부와 호출스택을 이해하기 위한 코드이다.

function baz() {
  // 호출 스택: baz
  // 호출부: 전역스코프 내부
  console.log("baz");
  bar();
}

function bar() {
  // 호출 스택: baz -> bar
  // 호출부: baz 내부
  console.log("bar");
  foo();
}

function foo() {
  //호출 스택: baz -> bar -> foo
  // 호출부: bar 내부
  console.log("foo")
}

baz(); // baz의 호출부

위 코드에서 최종적인 호출부는 전역 스코프가 된다.

참조 규칙

1. 기본 바인딩

어떤 규칙에도 해당하지 않을때 적용되는 기본 규칙이다.

function foo() {
  console.log(this.a);
}

var a = 2;
foo(); // 2

this는 전역 객체에 바인딩되고, foo에서는 a라는 프로퍼티를 참조한다.
여기서 다른 규칙 없이 foo의 호출부인 전역 스코프에 바운딩 되었음을 알 수 있다.strict mode에서는 전역 객체가 기본 바인딩 대상에서 제외되므로 thisundefined가 된다.

function foo() {
  "use strict"

  console.log(this.a);
}

var a = 2;
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined

다만 foo() 함수 본문를 none-strict mode에서 실행할 시, 호출부의 strict 여부와는 상관 없이 전역 객체만이 기본 바인딩의 대상이 된다.

function foo() {
  console.log(this.a);
}

var a = 2;
(function() {
  "use strict"

  foo(); // 2
})();

2. 암시적 바인딩

호출부에 컨텍스트 객체가 있으면, 즉 객체가 포함(Owning)/소유(Containing)하고있으면 this는 암시적으로 바인딩된다.

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2,
  foo: foo
};

obj.foo() // 2

위처럼 objfoo를 별도로 선언하든 객체 내부에 foo 함수를 직접 작성하든 obj가 실제로 foo 함수를 가지고 있는것은 아니다. 다만 호출부에서 obj 컨텍스트로 foo를 호출하므로 해당 시점에서 함수의 레퍼런스를 포함/소유하고 있다고 볼 수 있다.
함수 레퍼런스에 대한 콘텍스트 객체가 존재하면 this는 해당 콘텍스트 객체에 바인딩된다. foo를 호출하는 컨텍스트가 obj이므로 thisobj가 된다.

만일 프로퍼티 참조가 chaining 되어있다면 최상위/최하위 수준의 정보만 호출부와 연관된다.
아래 코드에서 중간 단계인 obj1.a는 무시된다.

function foo() {
  debugger;
  console.log(this.a);
}

var obj2 = {
  a: 2,
  foo: foo
}

var obj1 = {
  a: 1,
  obj2: obj2
};
obj1.obj2.foo() // 2

암시적 소실

암시적 바인딩이 되었던 this가 기본 바인딩으로 인해 전역 객체나 undefined로 바인딩 되는 경우가 있다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo
}
var bar = obj.foo;
var a = "엥, 전역이네!";
bar(); //엥, 전역이네!

여기서 barobj.foo를 참조하는 것 처럼 보이나 실제로는 foo를 직접 참조하는 변수가 된다. 때문에 bar()을 실행한 전역 객체로 this가 기본 바인딩 된다.

콜백 함수를 전달할 시에도 같은 문제가 일어난다.

function foo() {
  console.log(this.a);
}
function bar(callback) {
  callback(); //호출부
}
var obj = {
  a: 2,
  foo: foo
}
var a = "엥, 전역이네!";
bar(obj.foo); //엥, 전역이네!

인자로 전달하는 행위 자체가 암시적인 레퍼런스 할당이 되어 this가 전역 객체로 바인딩 되는 문제가 일어난다.

이런 문제를 해결하기 위해 this를 고정하는 명시적 바인딩을 사용할 수 있다.

3. 명시적 바인딩

암시적 바인딩에서는 함수 레퍼런스를 객체에 넣기 위해 객체 자신에 함수를 참조할 속성을 추가해야했다. 이를 따로 이용하지 않고 코드에 this를 명확히 밝히기 위해 명시적 바인딩을 사용한다

.모든 자바스크립트 함수는 prototype을 통해 call()apply() 매소드를 사용할 수 있다. 두 메소드는 인자로 객체를 받아 이를 this로 바인딩하며, 이를 명시적 바인딩이라고 한다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2
}

// foo.call()에서 명시적으로 obj를 바인딩한다.
foo.call(obj); // 2

객체 대신 primitive type을 인자로 전달하면 해당 type에 대응되는 객체로 wrapping 해주며, 이를 boxing이라고 한다.

하드 바인딩

그러나 명시적 바인딩을 사용해도 this의 바인딩 대상이 덮어씌워지는 문제는 해결할 수 없다. 이를 해결하기 위해 다음과 같은 패턴을 사용한다.

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2
};
var bar = function() {
  foo.call(obj);
}

bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2, 재정의된 this는 의미가 없다.

함수 bar()는 어떻게 호출해도 fooobj를 바인딩해서 사용하므로 결과는 변하지 않는다. 이런 바인딩은 명시적이고 강력해서 하드 바인딩이라고 한다.

다음과 같이 bind 헬퍼를 구현할 수 있다.

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}
// bind 헬퍼
function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments);
  }
}

var obj = {
  a: 2
}
var bar = bind(foo, obj);

var b = var(3); // 2 3
console.log(b) // 5

ES5에서는 Function.prototype.bind이라는 이름으로 구현되었으며, foo.bind(obj)와 같은 형식으로 사용할 수 있다.

4. new 바인딩

일반적으로 new연산자는 클래스 지향 언어에서 생성자를 호출하여 클래스 인스턴스를 만들어낸다.

something = new MyClass();
그러나 자바스크립트에서 생성자는 클래스와 상관없이 new 연산자로 실행하였을때 함께 실행되는 일반 함수이다. 생성자 자체는 클래스 내부에 있는 함수도 아니며 클래스 인스턴스화 기능도 없다.

ES5에서는 대부분의 함수를 new 연산자로 실행할 수 있었으나 ES6에서 함수 선언 방식이 달라지면서 constructor이 아닌 함수는 부를 수 없게 되었다.function 키워드를 붙여 생성한 함수는 자동으로 constructor 역할을 함께 하며, arrow function으로 생성한 함수는 constructor이 되지 않는다.

함수 앞에 new 키워드를 붙여 생성자 호출을 하면 자동으로 다음과 같은 일을 해준다.

  1. 새 객체가 만들어진다.
  2. 새로 생성된 객체의 [[prototype]]이 연결된다.
  3. 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩된다.
  4. 이 함수가 자신의 또 다른 객체를 반환하지 않으면 new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다.

this 확정 규칙

바인딩 예외

1. this 무시

call, apply, bind 메서드에 첫 번째 인자로 null 또는 undefined를 넘기면 this 바인딩이 무시되고 기본 바인딩 규칙이 적용된다.

function foo() {
  console.log("a:" + a + ", b:" + b);
}

var a = 2;
foo.apply(null, [2, 3]); // a:2, b:3

foo.bind(null, 2);
foo(3); // a:2, b:3

함수 호출 시 applybind는 종종 함수의 인자를 컨트롤 하기 위해 사용된다. 이 때 굳이 this를 지정해줄 필요가 없으면 함수의 첫번째 인자를 null로 주는것이다.
이 경우 주의할 점은, 자신이 다루지 않는 함수(서브파티 라이브러리 함수 등)가 내부적으로 this를 참조하고 있다면 window 객체를 참조하는 등의 영향을 끼칠 우려가 있다는 것이다.

이를 방지하기 위해 부작용과 무관한 객체를 바인딩하는 방법이 있다. 이러한 객체는 내용이 없고, 아무것도 위임되지 않는다.

var blank = Object.create(null); //{}와 달리 Object.prototype으로 위임하지 않음

2. 간접 레퍼런스

간접 레퍼런스가 일어나면 무조건 기본 바인딩이 적용된다. 간접 레퍼런스는 할당문에서 가장 빈번하게 발생한다.

function foo() {
  console.log(this.a);  
}
var a = 2;
var o = { a: 3, foo: foo};
var p = { a: 4 };

o.foo(); // 3
(p.foo = o.foo)(); // 2

p.foo = o.foo의 반환 값은 원 함수(Underlying Function) 객체의 레퍼런스이므로, 실제 호출부는 foo()가 된다. 때문에 기본 바인딩 규칙이 적용된다.

3. 소프트 바인딩

앞서 함수 바인딩이 예상과 달라지는 것을 막아주는 하드 바인딩에 대해 언급하였다. 그러나 이후에 this를 다시 암시적 혹은 명시적 바인딩하기가 어려워진다.

이 문제를 해결한 위해 임의로 this를 바인딩 할 수 있는 동시에 기본 바인딩 값을 세팅할 수 있는 Soft Binding 유틸리티가 있다.
softBind()의 로직은 bind()와 거의 동일하지만, this가 존재하지 않거나 전역 변수인 경우에는 초기에 바인딩한 this가 되도록 한다.

Build and scale multiple voice application by using Typescript

일반적으로 인공지능 비서와 연동하는 앱을 만드는 것이 그리 복잡하지는 않다. 사용자의 발화를 텍스트로 변환하거나 반대로 텍스트를 음성으로 변환하는 것은 서비스의 몫이며, 개발하는 측에서는 대화나 단어에 대한 반응을 넣는 것 만으로도 충분히 앱을 만들 수 있다.

Webhook을 사용하는 것과도 비슷한데, POST로 날아온 JSON 데이터를 처리한 후 다시 JSON으로 돌려보내는 작업이라는 공통점이 있다. 기본적으로는 하나의 대화에 하나의 웹훅 실행이기 때문에 stateless한 작업으로도 볼 수 있다. 그러나 좀 더 복잡하게 만들려면 과거 대화나 문맥 등이 포함되기 때문에 DB가 필요한 경우가 생기기도 한다.

아마존에서 개발하는 Alexa에서 실행할 수 있는 앱을 스킬이라고 부른다. 기본적으로는 스마트 홈 스킬, 플래시 뉴스(맞춤형 기사) 스킬, 비디오 스킬, 음악 스킬 등이 있으며 외부에서 만들어지는 게임, 음식 배달 등은 커스텀 스킬로 취급된다.

스킬 개발에는 보통 AWS를 사용하는데, AWS Lambda 또는 HTTPS API를 사용할 것인지 지정할 수 있으며, node.js로 개발할 때는 주로 AWS Lambda를 사용하는 경우가 많다고 한다.

javascript계열에서는 개발할 때 ask-sdk 라이브러리를 사용해야 한다. typescript 사용자를 위한 ask-sdk-model을 사용하면 IDE에서 request 타입 정보를 제공 받을 수 있다.

Handler 코드를 작성할 때는 canHandle 함수에서 사용하는 명령어가 맞는지 확인 한 후, handle 함수에서 실제로 요청에 대해 처리해야 한다.

// Skill에 대한 Handler의 구조
export const Handler = {
    canHandle: () => {
      // 처리해야할 리퀘스트인지 판단
        return isHandleType;
    },
    handle: () => {
     // 리퀘스트와 Alexa의 응답에 대한 처리

            return skillBuilders
                .speak('안녕하세요')
                .reprompt('안녕하세요')
                .getResponse();
    }
}

이런 알렉사 앱 개발자 사이에서는 하나의 스킬에 여러 기능을 넣을 것인가, 하나의 기능을 가진 여러 스킬을 배포할 것인가에 대해 논의가 이루어지고 있다고 하였다.

다기능 스킬은 최초에 어떤 기능을 사용할 것이냐는 대화가 필요하므로 짧은 대화로 사용자의 목적을 달성하기 힘들어진다. 반면 스킬명과 기능명을 동일시 한 단기능 스킬의 경우 이런 문제는 없지만 코드의 반복사용이 심해지고 사용자가 내 스킬을 찾기 힘들어진다는 단점이 있다.

발표자는 50가지 정도의 스킬을 공개하고 관리 중인 경험에 의거하여 이에 따른 고충과 자신이 해결한 방법을 소개하였다.

  1. 인프라가 스킬 수에 비례해서 함께 늘어난다 - 런타임 갱신 등 관리 코스트가 늘어간다
    • IaC(Infrastructure as Code)방식을 적용하여 인프라를 한 곳에서 한 번에 관리하도록 하여 수정이나 추가 시에 실수가 없도록 한다
  2. 비슷한 처리의 반복 - 한 스킬에서 수정한 것을 다른 스킬에서 수동으로 일일히 적용해주어야 한다
    • ask-sdk는 말 그대로 기본적인 공통 기능만 가지고 있으므로, 스스로 범용 함수를 만드는 것이 중요하다
    • 또한 ask-sdk 자체가 계속 발전 중이므로 필요한 함수가 있다면 PR을 해보는것도 좋다
  3. 비슷한 기능을 가진 단기능 스킬을 만들었을 때 반복이 너무 많다
    • 애플리케이션 그 자체를 재이용 한다
    • RequestHandler / SkillBuilders / Lambda를 애플리케이션끼리 공유할 수 있다

WEB의 자체무게

Web의 무게에 대해 논하기 위해서는 Web의 정의에 대해 알아보아야 한다. Web은 어떤 것을 할 수 있고, 무엇을 하기 위한 것일까? 사실 WHATWG나 RFC 등에서도 Web 스펙에 대한 정의나 제한이 분명하게 다루어진 적은 없다고 한다.

따라서 Web에 기능을 추가할 때 중요한 것은 "Web에 필요한가/필요하지 않은가"(애시당초 불분명한 부분)이 아니라, "현재 Web 상에서 올바르게 설계할 수 있는가"이다. 기능이 Web 호환성에 어긋나지 않는지, 보안 이슈는 없는지 등을 살펴봐야 하며, 모질라에서는 mozilla specification positions에서 기능에 대하여 평가하고 있다.

결론적으로 Web은 어떤 기술이나 스펙으로 정의할 수 있는것이 아닌, 사용자의 수많은 UseCase의 집합이라고 할 수 있다.

이와 관련하여 최근 브라우저의 다양성이 줄어가는 것에 대한 우려 또한 생기고 있다. UseCase라고 하는 것은 시간이 지날수록 많아질 수 밖에 없으며 사용자의 추가 스펙 요구도 함께 늘어나게 된다. 그렇게 계속 스펙이 추가되다가 웹 자체가 너무 무거워지면 어떤 브라우저는 나가 떨어지게 되고, 결국 부담은 고스란히 남은 브라우저에게 넘어가게 된다. 그렇다면 신규 브라우저의 진입장벽은 늘어날 수 밖에 없다.

그러면 Web 무게를 줄이기 위해서는 어떻게 해야할까? 1차원적으로는 Web은 UseCase의 집합이므로 이걸 줄여야한다고 생각할 수 있다. 그러나 이 것을 정하는 기준은 사람마다 너무 다르고 막연하므로 현재 상황에서는 고르기 어려운 선택지이다. 현실적으로 가능한 방법은 브라우저의 스펙 자체를 낮은 레이어 단계로 줄여 브라우저의 부담을 앱으로 옮기는 것이다. 예를 들면 Style에 대한 스펙을 이것저것 규정하지 말고 Canvas를 사용하게 해서 개발자에게 권한을 넘기면 웹의 부담은 많이 줄어들 것이다.

물론 이런 식으로 모든 스펙을 줄여버리고 개발자에게 일임해버리면 개발자의 책임이 너무 커지게 되므로 어느정도의 기본적인 기능과 API는 브라우저에서 가지고 있어야 한다.

애초에 현재 Web에 존재하는 스택으로 모든 UseCase를 만족시킬 수 있냐는 의문 또한 있다. 발표자가 말하길, 답은 동작은 하지만 최적은 아니라는 것이었습니다. 극단적인 예로는 게임에 대한 UseCase가 있는데, 현재도 그래픽을 DOM으로 나타내기는 부적절해서 Canvas로 만드는 것이 일반적이다. 더 깊게 파고 들면 디바이스나 OS까지 내려가서 연결한 컨트롤러로부터 입력을 받는 부분도 필요한데, 이런 부분에 대해서 Web에서는 레이어에 안전하게 접근하는 방법을 마련할 필요가 있다.

이렇게 안정성과 호환성을 고려하면서 계속 낮은 레이어의 스택을 유지해야 사용자들이 원하는 UseCase를 만족시키면서도 Web이 자신의 무게로 인해 무너지지 않을 수 있을 것이다.

최신 웹 기술로 IOT 개발하기

IoT를 시작하는 법

IoT가 실생활에 보급되고는 있지만 직접 만들기에는 시작할 엄두는 잘 나지 않는다. 준비해야 할 것도 많아보이고 알아야 할 것도 많아보인다. 그러므로 일단 단순하게 IoT가 무엇인지 부터 생각해보자. 세션에서는 IoT를 그저 "인터넷"과 "사물"의 연결이라고 정의하였다.

여기서 인터넷에 해당하는 것은 친숙하며 우리가 언제든지 준비할 수 있는 것들이 많다. 반대로 사물에 해당하는 센서, 모터 등은 완제품이 아니면 보기조차 힘든 물건들이고, 어렵게 느껴지기 마련이다. 그래서 하드웨어가 익숙하지 않은 대부분의 개발자는 여기에서 막히고 만다.

그런데 사실 아주 밀접한 곳에 "사물"로 사용할 수 있는 물건이 있다. 바로 스마트폰이다. 인터넷으로 연결이 되고 각종 센서가 있다는 점에서 충분히 IoT 사물의 조건을 만족한다. 그러므로 간단하게 IoT를 시도해보고 싶다면 먼저 스마트폰 앱으로 시작해보는 것이 좋다.

다만 스마트폰으로 본격적인 IoT를 만들기는 어렵다. 첫째로 스마트폰을 오로지 사물로 쓰기에는 너무 고가라는 점, 둘째로 지나치게 고사양인 디스플레이 등 필요없는 기능이 너무 많다는 점, 셋째로는 반대로 물리적인 움직임이나 온도 계측 센서 등 필요한 센서가 다 있지 않다는 점도 걸림돌이다.

결국 제대로 IoT를 만들고 싶다면 커스텀 하드웨어를 만들어야 한다. 하드웨어는 몇가지 부품을 조립하면 만들 수 있는 간단한 조립 컴퓨터라고 생각하면 쉽다. 부품은 크게 4가지로 구분할 수 있다.

가장 신중하게 선택해야 하는 것은 CPU이다. CPU에 따라서 프로그램이 돌아가는 환경과 사용할 수 있는 언어가 달라지기 때문에 만들어지는 결과물이 달라지게 된다. CPU를 결정하면 나머지 부품은 필요에 따라 추가하거나 변경해주면 된다.

Javascript로 IoT를 만들 떄의 선택

IoT에서 사용하는 CPU는 보통 C언어를 지원하는 것이 많다. Javascript를 지원하는 CPU는 적은데다가, 파워가 많이 필요하기 때문에 가격또한 비싸진다.

세션에서는 그 중에서도 javascript를 지원하는 적당한 두 가지 CPU를 소개하였다.

  • ESP32
    • CPU 위에서 Javascript가 돌아감
    • javascript를 바이너리로 컴파일하고 usb를 통해 기기에 설치
    • 내부에서 코드가 돌아가므로 동작이 빠름
    • 업데이트가 불편하고 CPU 사양이 부족한 케이스가 생김
  • obniz
    • 서버에서 Javascript가 돌아감
    • 인터넷을 통해 결과를 매번 통신(API통신과 비슷)
    • javascript가 서버 환경에 의존하기 때문에 CPU에 부담이 가지 않음
    • 통신이 필요하므로 빠른 응답이 나타나지 않음

이 중에서 obniz는 발표자가 제작에 참여하고 있는 CPU로, 코드 예시 또한 obniz에서 사용하는 것으로 볼 수 있었다.

obniz로 Web을 활용하여 IoT 만들기

위 슬라이드는 HTML에서 버튼을 누르면 obniz에 연결된 LED가 켜지는 코드를 보여주고 있다. obniz가 연결되면 obniz 객체에서 기기에 연결된 led 객체를 가져오고, 버튼을 누르면 led.on을 실행하는 간단한 코드이다.

이 코드는 버튼을 누르면 모터를 작동시킨다. 위 코드랑 비교했을 때 작동하는 객체만 달라진 것을 알 수 있다.

obniz로 만든 재미있는 IoT 기기 영상이 있다.

발표자는 위와 같이 javascript로 코드를 짤 수 있다면 센서와 모터를 구해 IoT를 만들 수 있으니, javascript 개발자들은 겁내지 않고 IoT 개발에 흥미를 가져주기를 바랐다.

후기

해외에서 컨퍼런스를 들은 건 처음이지만 한국에서 진행하는 컨퍼런스와는 분위기가 많이 다르다고 느꼈다. 먼저 비일본인 사람들의 비율이 굉장히 많은 것이 놀라웠고, 스탭과 발표자, 참가자가 다들 친밀한 사이처럼 보였다. 심화된 논의 보다도 가볍게 담소를 나누는 느낌이었는데, 이런 분위기 덕에 발표도 가볍게 듣기 좋았던 것 같다.

한국에서는 주로 프로젝트를 진행하면서 어려웠던 점이나 어떤 기술을 깊게 파보는 세션이 많았던 반면, 앞서 말했듯이 일본에서는 자바스크립트로 할 수 있는 재미있고 가벼운 기능을 소개하고 새로운 것을 시도해보자는 취지의 세션이 많았던 것 같다.

Web의 자체무게 세션은 생각해본 적도 없는 Web의 정의나 스펙에 대해 고민하는 시간을 가질 수 있었고, 실무에는 적용하기 어렵지만 재미있는 프로젝트로 할 수 있는 IoT나 PROC-GEN 세션, 작지만 중요한 사실을 알려주는 리액트 라이선스 관련 세션 등 알찬 발표를 많이 들을 수 있는 시간이었다.

2019년 11월 30일~12월 1일, 2일간 일본 도쿄에서 열리는 JSCONF JP에 참석하였다. 강의를 들은 후부터 2년 정도가 지나서, 현재와는 다른 부분이 있을 수도 있으나 전체적으로 자바스크립트 개발자에게 도움이 되는 내용이기에 당시에 정리한 글을 올려본다.

THE STATE OF JAVASCRIPT

The State of Javascript가 만들어진 이유는 현대 javascript 환경에서 수많은 도구가 쏟아지며 개발자들이 고통받고 있고((javascript fatigue, 자바스크립트 피로), 이를 해소하기 위함이다. 무엇을 선택하고 사용할지 알기 쉬운 지표를 얻기 위해 개발자들을 대상으로 설문조사도 시행하고 있으며, 당시에는 2019년에 대한 설문조사도 막 끝났던 참이었다.

그리고 설문 중인 2019년의 결과를 살짝 보여주겠다며 javascript 개발 시에 사용되는 유명한 도구들의 사용 비율 변화를 보여주었다. 비율 변화 그래프에서 흥미 있었던 부분은 역시 직접 사용할 일이 많은 프레임워크 부분이었다. 현재 프로젝트에서도 사용 중인 React는 전체 사용자 비율이 천천히 올라가고 있지만, '사용하였으나 앞으로 사용하지 않을 예정'의 비율이 점점 많아지고 있었다.

Vue의 경우는 '들어본 적 없음'이 0%에 가까워지며 존재감을 드러내고 있다는 것을 나타내었으며, SVELTE는 프레임워크는 Vue 초기와 비슷한 양상을 보이고 있어 가까운 미래에 인기를 얻지 않을까 하는 견해를 보였다. 이 외에도 Next.js 사용률은 React와 함께 증가하며, typescript 사용자가 급격히 상승하는 경향을 보이는 등 재미있는 결과를 볼 수 있었다.

또한 The state of javascript의 성공을 디딤돌 삼아 The State Of CSS(https://stateofcss.com/)가 만들어졌으며, 동일한 개발자가 참여한 Best Of Javascript(https://bestofjs.org/) 또한 비슷한 취지에서 만들어진 페이지이다.

JAVASCRIPT AST 프로그래밍: 입문과 한걸음 더

자신이 Javascript로 개발을 했다면, 특히 eslint나 babel을 적용해본 사람이라면 AST를 자기도 모르게 접해본 적이 있다는 뜻이다. 발표자는 AST를 사람들에게 좀 더 친숙하게 만들고, 이걸로 무언가 해보자는 생각이 들게 하는 것이 이번 발표의 목적이라고 발언하였다.

AST란 Abstract Syntax Tree(추상적 구문 트리)의 줄임말이다. 풀어 쓰자면, javascript(그리고 대부분의 프로그래밍 언어)는 코드 텍스트를 한 번 파싱 한 후, 이를 해석하여 실행시킴으로써 우리가 원하는 결과를 노출시킨다. 이때 구문을 Tree 형태로 파싱 한 결과를 AST라고 부른다. javascript에서는 우리에게 친숙한 JSON 형태의 object tree로 표현된다.

AST를 다루기 위해서는 기본적으로 다음과 같은 도구를 사용한다.

  • parser → 코드를 쪼개서 AST로 만드는 도구
  • walker, traversal, visitor... → AST를 탐색해서 코드 조각을 찾기 위한 도구
  • unparser → 편집한 코드를 다시 붙이는 도구

이 도구들을 사용하면 다음과 같은 과정을 거쳐서 코드를 프로그래밍적으로 수정할 수 있다.

  • Javascript → (parser) → ASTJS → (탐색을 통한 코드 수정) → ASTJS → (unparser) → Javascript

다음 코드에서 무슨 일이 일어나는지 알아보자.

("b" + "a" + + "a" + "a").toLowerCase() // banana

코드를 실행시키면 바나나라는 결과가 나온다. 이 코드를 AST로 분석하면 왜 바나나가 되었는지 알 수 있다.

아래 코드를 참조해서 간단한 AST 파서를 만들어보았다.

//parser.js

const acorn = require('acorn'); // parser를 포함한 라이브러리
const code = process.argv[2];
const ast = acorn.parse(code);
console.log(JSON.stringify(ast));

이 코드를 아래처럼 실행시키면 AST로 만들어진 코드 구조를 볼 수 있다.

$node parser.js "("b" + "a" + + "a" + "a").toLowerCase()" | jq

{
  "type": "Program",
  "start": 0,
  "end": 31,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 31,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 31,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 29,
          "object": {
            "type": "BinaryExpression",
            "start": 1,
            "end": 16,
            "left": {
              "type": "BinaryExpression",
              "start": 1,
              "end": 12,
              "left": {
                "type": "BinaryExpression",
                "start": 1,
                "end": 6,
                "left": {
                  "type": "Identifier",
                  "start": 1,
                  "end": 2,
                  "name": "b"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "start": 5,
                  "end": 6,
                  "name": "a"
                }
              },
              "operator": "+",
              "right": {
                "type": "UnaryExpression",
                "start": 9,
                "end": 12,
                "operator": "+",
                "prefix": true,
                "argument": {
                  "type": "Identifier",
                  "start": 11,
                  "end": 12,
                  "name": "a"
                }
              }
            },
            "operator": "+",
            "right": {
              "type": "Identifier",
              "start": 15,
              "end": 16,
              "name": "a"
            }
          },
          "property": {
            "type": "Identifier",
            "start": 18,
            "end": 29,
            "name": "toLowerCase"
          },
          "computed": false
        },
        "arguments": []
      }
    }
  ],
  "sourceType": "script"
}

모든 결과를 보지 않아도 AST에 대한 구조는 감이 올 것이다. 표현식의 좌변은 left, 우변은 right 속성으로 들어가며, type이 Identifiername 속성이, Expression이 들어가는 type에는 operator 속성이 있는 등 몇가지 특징을 찾을 수 있다.

이 결과에서 banana의 비밀을 찾을 수 있는데, 바로 UnaryExpression 부분이다. 여기에서 3번째 "a"에 대한 단항 연산을 하기 때문에, 'baNaNa'에 toLowercase를 적용하여 'banana'가 된다. parser 라이브러리에 따라 각 Object의 구성요소는 조금씩 다른데 expure이라는 라이브러리가 가장 표준에 가까운 라이브러리라고 한다.

이와 같이 AST를 잘 사용하면 코드 자체에 대한 디버깅이 가능하다. 이렇게 파싱 한 정보를 pipe형식 함수에 통과시킴으로써 코드 조각 하나하나에 대한 탐색도 할 수 있는데, 예를 들면 production 환경에서 console.log 함수를 제거하는 라이브러리를 만드는 것도 가능하다.

FOUR YEARS OF JS PROC-GEN

연사: Andy Hall

PROC-GEN(Procedural Generator)란 간단히 말하자면 어떤 컨텐츠를 알고리즘을 통해 만들어 내는 방법이다. 일반적으로는 게임에 주로 사용하는데, 가장 유명한 예로는 세계적으로 유명한 게임 "마인크래프트"가 있다. 이 게임의 맵은 어떤 알고리즘에 의거하여 지형을 적절하게 만들고 동식물이 알맞은 지형에 배치되도록 하며 나무나 건물 등이 올바른 형태를 가지지만 이런 구조물이 모두 동일하지는 않게 만들어진다.

PROC-GEN을 설명하기 위해서 발표는 네 단계를 거쳐 진행되었다.

첫번째, 알고리즘은 똑똑하지 않아도 된다.

발표자는 알고리즘을 사용해서 무언가를 만들 때 반드시 빠르고 좋은 알고리즘을 쓸 필요는 없다고 발언하였다. 극단적으로는 완전 랜덤으로 수백만 번을 반복하더라도 올바른 결과가 나오면 그것도 올바른 알고리즘이라고 생각한다고 하였다.

머신러닝이나 인공지능이라면 훌륭한 알고리즘을 만들어 내는 것이 중요하겠지만, proc-gen에서는 이미 있는 알고리즘을 창의적이고 반복적으로 사용하는 것이 중요하므로, 알고리즘 자체에 대해서 큰 고민을 하지 말라는 요지였다.

두 번째, Functional Space로 사고해라.

함수로 input에 어떠한 변화를 주었을 때 일정한 결과가 나온다면 그것만으로도 proc-gen을 잘 사용하는 것이라고 한다. 발표자는 이번엔 가상의 게임을 예시로 들었는데, 어떤 함수를 통해 proc-gen으로 몬스터의 종류를 만드는 과정을 보자.

위 예시에서는 makeSlime에 어떤 color 값을 input으로 준다면 input에 따라서 결과인 슬라임의 색이 변화할 수 있음을 보여준다. 여기서 슬라임에 색을 입히는 알고리즘이 사용되고 있는데, 만약 색에 더해 크기에 대한 input을 받고 이를 반영한다면 다음과 같이 2차원 공간에 매핑할 수 있다.

이처럼 proc-gen은 콘텐츠를 만들 때, input에 따라서 n차원 공간에 매핑하는 함수로 생각하면 된다고 한다. 만약 게임 안의 3차원 맵을 만든다고 하면 어렵게 들리지만, 내부에 어떠한 블랙박스 알고리즘이 있는 function(x, y, z)의 결과를 3차원 공간에 맵핑한 결과물이라고 생각하면 좀 더 간단할 것이다.

세 번째, 숨겨진 레이어를 사용하라

만약 proc-gen으로 음악을 만든다고 생각한다면 시간이라는 x축에 음의 높낮이를 매핑하는 작업이라고 할 수 있다. 그러나 input이 하나라면 반복적이고 단조로운 음악이 되어버리므로 듣는 사람 입장에서는 별로 즐겁지 않을 것이다.

음악적으로는 코드 진행이나 화음을 통해 이런 점을 극복해야 한다. 이걸 적용하면 input은 두 개, 세 개로 늘어나게 되지만 결과물은 여전히 시간에 대한 음의 높낮이로 그려진다. 결과 자체는 1차원이지만 매핑하는 알고리즘은 2차원 이상으로 늘어나게 되며, 여기서 화음이나 코드 진행 input을 발표자는 숨겨진 레이어라고 불렀다.

여기서 다시 앞서 말한 게임 "마인크래프트"를 예시로 들어주었는데, 3차원으로 이루어진 공간이지만 숲이나 들판 등 어떤 영역의 생태계라는 숨겨진 레이어가 적용되고 있다고 생각할 수 있다는 것이다.

넷째, 플레이그라운드를 만들어라

어쨌거나 proc-gen으로 무언가를 만드는 데에는 반복과 시행착오가 중요하기 때문에, 케이스에 따라 결과를 바로 볼 수 있는 플레이그라운드는 만들어 두는 것이 좋다. Proc-gen 뿐만 아니라 대부분에 해당하는 이야기이며, 이는 만들어지는 컨텐츠에 대한 유닛 테스트적인 의미도 겸하게 된다. 특히 javascript는 눈으로 보이는 테스트 환경을 만드는 데에 최적화되어있으므로 proc-gen을 사용해보기 좋은 언어라고 평가하였다.

WRAP-UP: High Performance Javascript

어떤 프로그래머든 최적화에 대한 고민이 있을 것이다. 웹의 속도가 빨라지면서 최적화의 중요성은 점점 늘고있는데, 이 세션에서는 Javascript의 구조를 알고, 각 구조에서 어떻게 최적화를 하고 javascript의 퍼포먼스를 높일 수 있나에 대하여 알아보았다.

먼저 Javascript에는 세가지 레이어가 있다.

  • Product: javascript로 만들어진 결과물 (application, library...)
  • Runtime: js를 실행하는 플랫폼(node.js, browser)
  • Engine: 언어를 파싱하고 컴파일한 후 실행하는 엔진(V8, SpiderMonkey...)

가장 하위 레이어인 Engine에서는 IC(인라인캐싱), SMI/HeapNumber 등을 사용해서 메모리를 줄이고 있고, Runtime 레이어에서는 비동기 실행, 코드 fetching 시간을 줄이려는 노력을 하고 있다. 실질적으로 개발하게 되는 Product단에서는 memorization이나 캐싱, 또는 알고리즘 개선으로 퍼포먼스를 높일 수 있다.

JS는 이미 Engine과 Runtime 단에서는 높은 수준의 최적화를 보여주고 있어, 이 부분에서 효과가 미미한 최적화(micro optimization)를 하기보다는 Product 최적화에 더 힘을 쏟는 것이 권장된다.

실질적으로 개발자가 할 수 있는 최적화에 대해서는 서버 사이드와 클라이언트 사이드로 나누어 설명되었다.

서버 사이드 자바스크립트라고 하면 일반적으로 node.js를 사용하므로, 설명도 node.js 기준으로 진행하였다. 웹 애플리케이션에서는 네트워크나 파일 시스템의 I/O가 일어나면서 병목현상이 일어나기 쉬운데, node.js는 single-threaded 기반이지만 EventLoop를 사용하여 비동기 논블로킹 방식을 가능하도록 하였다. 그러나 끝에 -Sync가 붙는 I/O함수를 실행하면 함수가 완료될 때까지 다른 작업이 불가하므로 이러한 함수의 사용은 자제해야 한다. (예: readFileSync 대신 readFile을 사용)

그러나 이런 I/O 함수가 아니더라도 긴 작업으로 병목현상이 일어날 수 있다. 이런 경우에는 timeout등을 사용하여 적절하게 스케줄링을 해줄 필요가 있다.(참조)

또한 streamAPI를 사용하면 I/O 도중에도 실시간으로 데이터를 받으므로 데이터가 커도 멈춤 없이 받아올 수 있고, 메모리를 효율적으로 사용 가능하며 브라우저에도 실시간으로 렌더링이 가능하다.

클라이언트 사이드에서는 일반적인 웹 퍼포먼스는 제외하고, 크롬 브라우저의 기능에 맞추어 설명이 진행되었다. 브라우저에서 자바스크립트를 실행할 때 가장 오래 걸리는 부분은 코드를 파싱 하고 컴파일하는 부분인데, 크롬은 이 부분을 최소한으로 줄이기 위해서 code caching을 사용해서 72시간 이내에 2번 이상 사용된 스크립트, 서비스 워커, 컴파일 결과를 모두 캐싱한다. 이후에 동일한 스크립트를 실행할 때는 파싱과 컴파일 과정을 모두 스킵할 수 있도록 되어 있다.

모던 브라우저에서는 모듈을 import/export를 가능하게 하는 ESModules를 기본적으로 지원한다. 그 중에서도 최신 크롬은 <link rel="preload" rel-"modulepreload"> rel속성에 preload라는 값을 지원하는데, 이는 브라우저에게 해당 리소스를 최우선으로 로드하도록 해준다. 스크립트 파일을 가져오는 modulepreload를 사용하면 실제로 코드를 실행하지 않아도 파싱을 해두고, 실행할 때에는 fetch와 compile만을 수행하도록 한다.

그 외에도 CPU의 여유가 있을 때 실행하도록 하는 idling(requestIdleCallback, cancelIdleCallback함수로 사용)이 있으며, 아예 코드를 늦게 파싱 하는 Lazy parsing 전략 등을 사용할 수 있다.

How To Boost Your Code With WebAssembly

웹 어셈블리는 브라우저 위에서 돌아가는 바이트로 이루어진 코드이다. 어셈블리 이름을 달고 있지만 실제 어셈블리처럼 기계어와 1:1로 대응되는 개념은 아니며, 실행을 위해서는 VM을 사용하거나 기계어로 한번 더 컴파일해야 하므로 JS보다는 빠르지만 기계어보다는 느리다. 웹 어셈블리가 등장하기 이전에 2012년부터 Emscripten을 사용하여 브라우저에서 C 혹은 C++을 컴파일하는 것 자체는 가능하였으나, JS로 CPU를 에뮬레이션 할 필요가 있었기 때문에 너무 느려서 실제로 쓰기는 힘들었다고 한다.

2015년부터 웹 어셈블리에 대한 논의가 시작되었고 2017년에는 모던 브라우저에서 버전 1.0(MVP)를 실제로 지원하기 시작하였으며, 2019년 현재에 이르기까지 WASI를 발표하는 등 활발하게 피처 추가 및 스펙 결정이 이루어지고 있다.

기능 자체는 JS와 크게 다를 것이 없으나, 속도 면에서 웹 어셈블리의 가치는 빛을 발한다. JS에서는 너무나도 느렸던 동작들을 훨씬 빠르게 끝낼 수 있다. 다만 C보다는 상당히 느린데, 그 이유는 안정성을 위해 정의되지 않은 동작(UB, undefined behavior)을 없앴기 때문이다. 예를 들면 C에서는 지정한 배열 크기보다 바깥을 참조하는 것은 정의되지 않은 동작이지만 웹 어셈블리에서는 확보한 메모리 내부라면 범위 바깥을 참조하는 동작까지 정의되어 있다. 그러나 이 안정성이 웹 어셈블리의 큰 장점으로 버그가 있어도 안심하고 실행할 수 있다는 특징을 만들어낸다. 이런 점을 종합해서 속도에 대해서는 Java에는 밀리지만 Javascript 보단 확실히 빠르다는 결론을 도출된다.

현재는 IE11을 제외한 대부분의 모던 브라우저에서 지원하고 있으며, eBay에서는 실제로 바코드 스캐너의 개발에 웹 어셈블리를 사용하고 있다. 또한 Bytecode Allience라는 단체에서는 웹 어셈블리를 Universal Binary로 성장시키려는 움직임을 보이고 있다.

JS 개발자가 웹 어셈블리를 좀 더 친숙하게 적용하는 방법 중 하나로는 AssemblyScript(이하 AS)를 사용하는 것이다. AS는 타입 스크립트를 문법을 기반으로 한 새로운 프로그래밍 언어로, 웹 어셈블리로 변환이 가능하며 npm을 통해 쉽게 세팅이 가능하다. 문법 등은 타입 스크립트에 가까우나 내용물은 C에 가깝다고 한다.

발표자는 자신이 느낀 AS로 코딩하면서 막히기 쉬운 점을 몇 가지 말해주었다.

  • C에서 메모리를 가리키는 포인터가 없는 대신 offset을 나타내는 usize 형만이 존재
  • load<u16>(), store <u16>()등이 little endian으로 규정되어있는 반면 JS의 Unit16 Array는 호스트의 endian을 따르므로 endian 호환이 안될 수 있음
    • 그러나 최근 환경에서는 대부분 리틀 엔디안을 사용하므로 크게 문제되지는 않을 것
  • 겉보기에는 typescript와 상당히 유사하므로 이에 따른 실수가 잦아질 수 있음

또한 웹 어셈블리만으로 Object나 DOM에 관련된 처리를 하기는 어렵고, Byte의 처리나 연산 처리를 했을 때 효과적으로 사용할 수 있을 것이라고 한다.

STREAMS API 제대로 이해하기

I/O에서 스트림이란 데이터를 '흐르는 것'으로 보고, 이 데이터의 처리 또는 처리를 위한 데이터 형식이다. node.js에서는 Stream API를 통해 데이터 흐름을 제어 가능하므로, Stream을 제어하는 자가 node.js를 지배한다고 해도 과언이 아니라고 한다.

기존에 Stream 없이 큰 데이터를 통신하면 데이터가 모두 전송될 때 까지 기다려야 했지만, Stream을 사용하면 개발자가 정의한 단위로 데이터를 분할하여 전송하는 것이 가능하다.

node.js에서 지원하는 Stream API는 크게 세가지로 나뉜다.

  • Readable Stream: ReadableStream 클래스의 constructor로 데이터 전체를 어떻게 분할하여 전송할지를 정하면(Underlying Source), 인스턴스로부터 Reader 객체를 받아 Promise를 반환하는 read() 함수를 통해 Stream 내부의 데이터를 읽어 들이는 것이 가능하다.
    • Readable은 (Node에서) 분할된 스트림 단위로 읽어 들일 수 있다는 뜻이다.
  • Writable Stream: WritableStream 클래스에 분할 데이터를 어떻게 가져올지(Underlying Sync)를 정하고, 인스턴스로부터 Writer 객체를 받아 Promise를 반환하는 write() 함수를 통해 Stream을 통해 흘러온 데이터를 쓰는 것이 가능하다.
    • Readable Stream과는 반대로 Node에서 스트림에 데이터 쓰기가 가능한 것을 Writable Stream이라고 부르고 있다.
  • Transform Stream: Transform Stream 클래스에서는 클래스 내부의 Writable Stream(클래스 외부의 ReadableStream → 클래스 내부의 WritableStream)으로부터 가져온 분할 데이터를 어떻게 변환할지 정하고(Transformer), 인스턴스로부터 ReadableStream객체와 WritableStream객체의 인스턴스를 가져오는 것이 가능하다.
    • Transform Stream은 Readable Stream과 Writable Stream 사이에서 데이터를 변환하는 Stream이다.

이 세 가지 스트림을 연결하기 위해서는 pipeTo 함수와 pipeThrough를 사용할 수 있다.

readableStream.pipeTo(writableStream)
transformStream.pipeTo(writableStream)

readableStream.pipeThrough(transformStream)

만약 ReadableStream과 WritableStream에서 불러온 활성화된 ReaderWriter가 이미 있는 경우, 해당 stream의 locked 속성은 true값이 되고 해당 stream이 잠겨 사용 불가 상태가 된다.

const readable = new ReadeableStream();
const reader = readable.getReader();
const writable = new WritableStream();
const writer = writable.getWriter();

readable.locked(); // true
writable.locked(); // true

readable.getReader() // error

같은 stream을 사용하고 싶은 경우 tee()함수를 사용할 수 있다.

const readable = usedStream;
const readable.tee() // 새로은 stream 인스턴스 두개를 반환한다. [instance1, instance2]

마지막으로 Stream과 관련된 기타 개념에 대한 설명으로 발표가 마무리되었다.

  • BackPressure: 데이터 전송이 밀려 chunk로 가득 차게 되는 경우 readable Stream에게 알려주는 시스템
  • Readable Stream에서 사용하는 underlying source에는 두 가지 타입이 있다.
    • pushSource: 준비가 된다면 언제든지 큐에 데이터를 날리는 타입
    • pullSource: 요청이 있을 때에만 데이터를 날리는 타입
  • Queuing Strategy: 각 스트림의 큐가 가지는 size와 size에 대한 최댓값을 설정 가능
    • highWaterMark: 스트림이 최대로 갖고 있을 수 있는 값(number)
    • size(chunk): 무엇을 size 기준으로 할지 정하는 값(number)
    • size 함수에서 return chunk.length구문이 있고 highWaterMark가 100이라면 chunk.length > 100인 문자열은 등록할 수 없음

React 애플리케이션의 라이선스 위반에 대해서

2017년 말 즈음 리액트 라이선스 정책이 바뀔뻔한 일이 있었다. 수많은 리액트 개발자의 반발 덕인지 다행히 MIT 라이선스로 정착되었지만, 정작 우리는 그 라이선스를 잘 지키고 있는 것일까?

발표자는 우리가 우리도 모르는 사이에 라이선스 위반을 하고 있을 수 있다고 하며, 특히 React 초기 세팅할 때 많이 쓰이는 create-react-apps 모듈은 production으로 실행하면 그것만으로도 라이선스 위반이 된다고 한다.

MIT 라이선스는 코드를 제한 없이 사용하는 대신 소프트웨어의 중요한 부분에 반드시 저작권자를 기재해야 하는 라이선스이다. 일반적으로는 install 하는 시점에서 코드 주석으로 처리되어 있기 때문에 별도의 처리를 하지 않아도 되지만, create-react-apps를 사용해서 production build를 하면 minify 되면서 주석이 사라지므로 별다른 처리를 하지 않으면 자신도 모르게 라이선스를 위반하게 된다.

해결책으로는 terser-webpack-plugin웹팩 플러그인을 사용할 것이 제시되었다. 다음과 같이 extractComments속성을 설정하면 라이선스 관련 주석을 모은 파일을 filename.LICENSE 이름으로 생성된다.

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        //...
                extractComments: true 
      }),
    ],
  },
};

또한 minify를 여러 번 실행하는 경우에도 라이선스 표기가 소실되는 케이스가 있으므로 주의해야 한다. unpkg를 통해 제공되는 react.production.min.js 파일조차 object-assign에 대한 라이선스 표기가 되어있지 않기 때문에 사용하면 라이선스 위반이 된다.

마지막으로 라이선스 위반을 하지 않았는지 체크하는 방법으로 license-checker모듈을 사용하는 방법이 있다. 다음과 같이 사용하면 각 모듈의 라이선스와 의존성을 출력해준다.

npm license-check --production --unknown

혼자서 프로젝트를 진행하다 보면 라이선스를 미처 생각하지 못할 때가 많다. 모든 라이선스를 일일이 살펴보지는 못해도 언급된 도구들을 사용해서 가능한 라이선스를 위반하지 않고 오픈소스 생태계를 지켜내는 것이 중요하다.

https://leap-in.com/ja/lets-learn-how-to-browser-works/

위 글을 번역 및 간단하게 재구성 하였음.

  • 브라우저 렌더링은 일반적으로 60fps로 이루어진다.
    • 1프레임 = 약 16.6ms
  • 자바스크립트나 렌더링 과정에서 시간이 16ms 이상 걸리는 경우 렌더링이 1프레임 안에 완료되지 않으므로 프레임레이트가 저하된다. → 화면이 버벅거리는 것으로 보인다

렌더링의 흐름

  • 화면이 최초로 렌더링 될 경우 모든 과정을 실행한다.
  • 화면에 갱신이 생기면 필요한 과정만 거치고, 나머지 과정은 생략한다.

Parse

  • HTML과 CSS를 해석하여 구조체로 만드는 작업
  • HTML → DOM Tree

  • CSS → Style Rules(CSSOM Tree)

  • 브라우저의 렌더링을 담당하는 기능을 렌더링 엔진(브라우저 엔진)이라고 한다.
    • Safari: Webkit
    • Chrome: blink
    • Firefox: Gecko
    • edge: (구)EdgeHTML

Style

  • DOM Tree와 Style Rules를 묶어서 처리하는 과정
  • 어떤 스타일이 어떤 요소에 적용되는지 판단하고, 한 요소에 적용되는 스타일이 2개 이상이면 우선순위를 계산한다.
  • DOM 구조가 변경되거나 CSS 프로퍼티가 변경되면 호출된다.
  • 요소와 스타일의 매칭 정보를 Render Tree라고 하고, Render Tree의 각 노드를 Renderer이라고 부른다.

Layout

  • 각 요소의 위치와 크기를 계산하는 과정
  • Layout 혹은 Reflow라고 부른다.
  • Render Tree의 상위 Rendere부터 하위로 내려가면서 재귀적으로 계산한다.
    • 화면을 갱신하는 경우는 해당 요소와 하위 , 형제 요소만 다시 계산한다.
  • Layout Tree가 생성된다.
    • 화면에 표시되는 요소 대상으로만 생성된다.
      • display:none 이 적용된 요소는 미포함
      • visibility: hidden 요소는 포함됨
      • ::before(content: 'Text')등의 가상요소는 포함됨

  • Layout 재계산의 종류
    • Global Layout: 페이지 전체에 관련된 레이아웃 변경이 일어났을 경우
      • 브라우저 크기, 전체 폰트사이즈 등
    • Incremental Layout: 변경이 한정적인 경우(요소 추가 등)

Paint

  • 크게 두가지 작업이 이루어진다.
    • 레이어 위에 픽셀을 채우는 과정(= 래스터화)
    • 이후 PaintRecords를 생성한다.
    • PaintRecords
      • 겹쳐지는 요소를 올바르게 렌더링하도록 순서가 정해진 명령의 집합
      • Stacking Context에 명령 순서가 정해져있다 .모던 브라우저에서는 Layer Tree를 동시에 생성한다.
      • Layer Tree
        • Layout 전체를 분리하여 Layer단위로 변환한 구조
        • 어떤 레이어에서 변화가 일어났을 경우, 해당 Layer만 변경하여 연산량을 줄인다.
        • transform 혹은 will-change 속성이 있는 경우 레이어가 분리된다.
        • 크롬 개발자도구의 Layer 탭에서 확인할 수 있다.

Composite

  • Paint 과정에서 그려진 레이어를 Paint Records에 맞춰 합성하는 과정
    • 어떤 요소가 다른 요소 위에 나타날지가 결정되기 때문에 정확한 순서로 렌더링되어야 한다.
  • 합성된 레이어는 Viewport 내의 화면을 우선하고, 이후 스크롤 등으로 요청받은 순으로 GPU에서 처리한다.
  • Composite 과정은 main thread를 사용하지 않기 때문에 javascript가 실행되는 동안에도 css 애니메이션을 실행할 수 있다.
    • transform, opacity등은 이 과정에서 적용되므로 성능 저하가 크지 않다.

렌더링과 스레드

Main Thread

  • Parse에서 Paint까지의 과정을 담당하는 스레드
  • 렌더링 처리와 javascript 실행 처리를 함께 담당하고 있으므로, 부담을 적게 하는 것이 중요하다.

Compositor Thread

  • 레이어 합성(composite)를 담당하는 스레드
  • Raster Thread에게 래스터화(비트맵화)를 의뢰하고, 래스터화가 완료된 레이어를 합성한다.

Raster Thread

  • 총 4개가 존재한다.
  • Compositor Thread는 4개 중 비어있는 스레드에 요청을 보낸다.

GPU

  • 합성된 레이아웃을 화면에 출력한다.
  • transform과 opacity를 사용하는 애니메이션은 레이어가 분리되어있고 래스터화도 완료되어 있으므로 Compositor Thread와 GPU만 처리한다.

Unicode Property Escapes

// Non-binary values
\p{UnicodePropertyValue}
\p{UnicodePropertyName=UnicodePropertyValue}

// Binary and non-binary values  
\p{UnicodeBinaryPropertyName}

// Negation: \P is negated \p  
\P{UnicodePropertyValue}  
\P{UnicodeBinaryPropertyName}
  • 이모지 판별 예시
    (/\p{Emoji_Presetation}/).test(string)

참고 링크: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes

'개발' 카테고리의 다른 글

JSConf JP 2019 참석기 DAY2  (0) 2021.09.06
JSConf JP 2019 참석기 DAY1  (0) 2021.09.05
브라우저 렌더링 과정  (0) 2021.08.26
리액트 디자인 패턴  (0) 2021.06.06
부동소수점의 오차  (0) 2018.04.01

+ Recent posts