Skip to content

this와 객체 프로토타입 비동기과 성능

Yongku cho edited this page Nov 2, 2019 · 17 revisions

this와 객체 프로토타입

2.2 단지 규칙일 뿐

2.2.1 기본 바인딩

첫 번째 규칙은 가장 평범한 함수 호출인 '단독 함수 실행'에 관한 규칙으로 나머지 규칙에 해당하지 않을 경우 적용되는 this의 기본 규칙이다.

function foo () {
  console.log(this.a)
}
var a = 2
foo() // 2

엄격 모드에서는 전역 객체가 기본 바인딩 대상에서 제외된다. 그래서 this는 undefined가 된다.

function foo () {
  "use strict"
  console.log(this.a)
}
var a = 2
foo() // TypeError

2.2.2 암시적 바인딩

두 번째 규칙은 호출부에 콘텍스트 객체가 있는지, 즉 객체의 소유/포함 여부를 확인하는 것이다.

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

다음 예제처럼 객체 프로퍼티 참조가 체이닝된 형태라면 최상위/최하위 수준의 정보만 호출부와 연관된다.

function foo () {
  console.log(this.a)
}
var obj2 = {
  a: 42,
  foo: foo
}
var obj1 = {
  a: 2,
  obj2: obj2
}
obj1.obj2.foo() // 42
암시적 소실

'암시적으로 바인딩 된' 함수에서 바인딩이 소실되는 경우가 있다.

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

2.2.3 명시적 바인딩

this로 지정할 객체를 직접 바인딩 하므로 이를 '명시적 바인딩'이라 한다. call(), apply(), bind()를 사용한다.

2.2.4 new 바인딩

함수 앞에 new를 붙여 생성자 호출을 하면 다음과 같은 일들이 저절로 일어난다.

  1. 새 객체가 만들어진다.
  2. 새로 생성된 객체의 [[Prototype]]이 연결된다.
  3. 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩 된다.
  4. 이 함수가 자신의 또 다른 객체를 반환하지 않는 한 new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다.
function foo(a) {
  this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

앞에 new를 붙여 foo()를 호출했고 새로 생성된 객체는 foo 호출 시 this에 바인딩 된다. 따라서 결국 new는 함수 호출 시 this를 새 객체와 바인딩 하는 방법이며 이것이 'new 바인딩'이다.

2.3 모든 건 순서가 있는 법

  1. 명시적 바인딩
  2. new 바인딩
  3. 암시적 바인딩
  4. 기본 바인딩
function foo (a) {
  this.a = a
}
var obj1 = {
  foo: foo
}
var obj2 = {}

obj1.foo(2)
console.log(obj1.a) // 2

obj1.foo.call(obj2, 3)
console.log(obj2.a) // 3

var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4

비동기와 성능

2 콜백

콜백은 큐에서 대기 중인 코드가 처리되자마자 본 프로그램으로 '되돌아올' 목적지기 때문에 콜백이다.

2.2.2 중첩/연쇄된 콜백

listen('click', function handler(event) {
  setTimeout(function request() {
    ajax('http://some.url.1', function response(text) {
      if (text === 'hello') {
        handler()
      } else if (text === 'world') {
        request()
      }
    })
  }, 500)
})

이른 바 콜백 지옥 또는 운명의 피라미드라고도 불리는 코드다.

하지만 콜백 지옥은 중접/들여쓰기와는 무관하고 그보다 훨씬 심각한 문제를 안고 있다.

중첩이 원인일까? 비동기 흐름을 따라가기 어렵게 만드는 주범이 중첩인가? 물론 중첩이 원인 제공을 한 공범자인 건 맞다. 그러나 중첩 없이 이벤트/타임아웃/ajax 예제를 다시 써보면,

listen('click', handler)

function handler(event) {
  setTimeout(request, 500)
}

function request() {
    ajax('http://some.url.1', response)
}

function response(text) {
  if (text === 'hello') {
    handler()
  } else if (text === 'world') {
    request()
  }
}

중첩/들여쓰기로 도배했던 이전 코드보다 알아보기는 훨씬 편하다. 하지만 콜백 지옥에 취약한 것 매한가지다.

순차적으로 이 코드를 추론하자면 한 함수에서 다름 함수로, 또 그다음 함수로, 시퀀스 흐름을 '따라가기' 위해 코드 베이스 전체를 널뛰기해야 한다.

일일이 모든 내용을 단계별로 하드 코딩하는 방법도 가능하지만 십중팔구 다른 단계나 비동기 흐름에서는 재사용할 수 없는, 매우 반복적인 코드가 낭비가 초래 될 것이다.

바로 이것이 콜백 지옥이다. 중첩/들여쓰기 같은 건 주의를 분산시키는 부수적인 요소일 뿐이다.

2.3 믿음성 문제

순차적인 두뇌 계획과 콜백식 비동기 자바스크립트 코드 사이의 부조화는 콜백 문제점의 일부에 불과하다. 더 심각한 문제가 있다.

콜백 함수의 개념을 프로그램의 연속이란 관점에서 다시 보자. 콜백은 다른 프로그램의 제어하에 나중에 실행된다. 제어권을 주고받는 행위 때문에 프로그램이 항상 탈이 나는 건 아니다.

하지만 문제가 불거지는 주기가 길다고 하여 제어권 교환이 별일 아니라고 지레짐작해선 안된다. 사실, 제어권 교환이야말로 콜백 중심적 설계 방식의 가장 큰 문제점이다. 내가 작성하는 프로그램인데도 실행 흐름은 서브 파티에 의존해야 하는 이런 상황을 제어의 역전이라고 한다.

2.4 콜백을 구하라

지금까지 살펴본 믿음성 문제를 일부라도 해결하기 위해 기존 디자인을 변형한 콜백 체계가 있다. 예를 들어, 더욱 우아하게 에러를 처리하려고 분할 콜백 기능을 제공하는 API가 있다. (분할 콜백: 한쪽은 성공 알림, 다른 쪽은 에러 알림)

function success (data) {
  console.log(data)
}
function failure (data) {
  console.log(data)
}
ajax('http://some.url', success, failure)

분할 콜백 디자인은 ES6 프라미스 API가 사용하는 패턴이다.

'에러 우선 스타일'이라는 콜백 패턴이다. 우선, 일견 믿음성 문제가 대체로 해결된 것처럼 보이지만 실상은 전혀 그렇지 않다. 원하지 않는 반복적인 호출을 방지하거나 걸러내는 콜백 기능이 전혀 없다. 더구나 이제는 성공/에러 신호를 동시에 받거나 아예 전혀 못 받을 수 있으므로 상황별로 코딩해야 하는 부담까지 가중됬다.

2.5 정리하기

콜백은 자바스크립트에서 비동기성을 표현하는 기본 단위다. 그러나 자바스크립트와 더불어 점점 진화하는 비동기 프로그래밍 환경에서 콜백만으로 충분지 않다.

첫째, 사람의 두되는 순차적, 중단적, 단일-스레드 방식으로 계획하는 데 익숙하지만 콜백은 비동기 흐름을 비선형적, 비순차적인 방향으로 나타내므로 구현된 코드를 제대로 이해하기가 매우 어렵다. 추론하기 곤란한 코드는 곧 악성 버그를 품은 나쁜 코드로 이어진다.

둘째, 이 부분이 더 중요한데, 콜백은 프로그램을 진행하기 위해 제어를 역전, 즉 제어권을 다른 파트에 암시적으로 넘겨줘야 하므로 골치가 아프다. 이렇게 제어권이 넘어가면서 예상보다 더 자주 콜백을 호출하는 등 여러 가지 믿음성 문제에 봉착하게 된다.

3.1 프라미스란

API만으로 그 이면의 추상화까지 파악하기 어렵다. 프라미스가 꼭 그렇다. API 사용법만 재빨리 익혀 사용하는 것과 API가 지향하는 바와 대상이 무엇인지 제대로 알고 쓰는 것이 얼마나 극적으로 달라질 수 있는 지 뼈저리게 느끼게 해주는 도구가 프라미스다.

3.1.1 미랫값

프라미스는 시간 의존적인 상태를 외부로부터 캡슐화하기 때문에 프라미스 자체는 시간 독립적이고 그래서 타이밍 또는 내부 결괏값에 상관없이 예측 가능한 방향으로 구성할 수 있다.

또한, 프라미스는 일단 귀결된 후에는 상태가 그대로 유지되며 몇 번이든 필요할 때마다 꺼내 쓸 수 있다.

불변성은 프라미스를 제대로 알려면 반드시 이해해야 할, 강력하고 중요한 개념이다. 엄청난 삽질을 하면서 같은 로직을 콜백을 조합하여 코딩하여 놓아도 콜백 조합을 끊임없이 되풀이해야 하기에 결국 임시변통일 뿐 효과적인 전략이라 할 수 없다.

프라미스는 미랫값을 캡슐화하고 조합할 수 있게 해주는 손쉬운 반복 장치다.

3.2.1 완료 이벤트

프라미스 각각은 미랫값으로서 작동하지만 프라미스의 귀결은 비동기 작업의 여러 단계 '흐름 제어'하기 위한 체계라 볼 수 있다.

프라미스는 똑같은 결과를 영원히 유지하므로 이후에 필요하다면 몇번이고 계속 꺼내 쓸 수 있음을 알 수 있다.

3.2 데너블 덕 타이핑

진짜 프라미스는 then() 메서드를 가진, '데너블(Thenable)'이라는 객체 또는 함수를 정의하여 판별하는 것으로 규정됐다. 데너블에 해당하는 값은 무조건 프라미스 규격에 맞다고 간주하는 것이다.

어떤 값이 타입을 그 형태를 보고 짐작하는 타입 체크를 일반적인 용어로는 덕 타이핑이라 한다. "오리처럼 보이는 동물이 오리 소리를 낸다면 오리가 분명하다"는 것이다. 이를테면 덕 타이핑 방식으로 데너블 체크를 한다면 다음과 같다.

3.3 프라미스 믿음

콜백만 사용한 코드의 믿음성 문제를 되짚어보자. 콜백을 넘긴 이후 일어날 수 있는 경우는 다음과 같다.

  • 너무 일찍 콜백을 호출
  • 너무 늦게 콜백을 호출
  • 너무 적게, 아니면 너무 많이 콜백을 호출
  • 필요한 환경/인자를 정상적으로 콜백에 전달 못함
  • 발생 가능한 에러/예외 무시함

프라미스 특성은 이와 같은 모든 일들에 대해 유용하고 되풀이하여 쓸 수 있는 해결책을 제시하게끔 설계됐다.

3.3.1 너무 빨리 호출

같은 작업인데도 어떨 때는 동기적으로, 어떨 때는 비동기적으로 끝나 결국 경합 조건에 이르게 되는, 자르고 현상(Zalgo-like Effects)을 일으킬 코드인지 확인하는 문제다.

프라미스는 바로 이루어져도 프라미스의 정의상 동기적으로 볼 수는 없으니 이 문제는 영향받을 일이 없다.

then()을 호출하면 프라미스가 이미 귀결된 이후라 해도 then()에 건넨 콜백은 항상 비동기적으로만 부른다. 굳이 setTimeout(..., 0) 같은 꼼수는 쓸 필요가 없다. 프라미스는 자르고를 알아서 예방한다.

3.3.7 미더운 프라미스

Promise.resolve()에 진짜 프라미스가 넘어가도 결과는 마찬가지다.

const p1 = Promise.resolve(10)
const p2 = Promise.resolve(11)

console.log(p1 === p2) // false
const p1 = Promise.resolve(10)
const p2 = Promise.resolve(p1)

console.log(p1 === p2) // true

3.4 연쇄 흐름

프라미스는 여러 개를 길게 늘어놓으면 일련의 비동기 단계를 나타낼 수 있다. 비결은 프라미스에 내재된 다음 두 가지 작동 방식이다.

  • 프라미스에 then()을 부를 때 마다 생성하며 반환하는 새 프라미스를 계속 연쇄할 수 있다.
  • then()의 이룸 콜백 함수가 반환한 값은 어떤 값이든 자동으로 연쇄된 프라미스의 이룸으로 세팅된다.
Clone this wiki locally