[V8 GC] Concurrent marking

이 포스트의 원문은 여기 임을 밝힙니다.


이 포스트에서는 concurrent marking 이라고 불리는 garbage collection 테크닉에 대해 소개합니다. 이를 통해 garbage collector가 heap에서 활성 객체를 찾고 마킹하기 위해 작업하는 동안, 자바스크립트 어플리케이션이 멈추지 않고 계속 실행될 수 있습니다. 벤치마킹을 통해서 concurrent marking이 메인 쓰레드에서 마킹하는데 드는 시간을 60% ~ 70% 감소시킨다는 것을 확인하였습니다. Concurrent marking은 Orinoco project의 마지막 퍼즐 조각입니다. (과거의 garbage collector를 점진적으로 concurrent, parallel한 garbage collector로 대체하고자 하는 프로젝트 입니다.)

배경

마킹(marking)은 V8의 Mark-Compact garbage collector의 한 과정입니다. 이 과정 동안 collector는 모든 활성 객체를 탐지하고, 마킹합니다. 마킹은 전역 객체와 현재 활동 중인 함수들과 같은 (이른바 “roots”), 알려진 활성 객체들의 집합으로부터 시작합니다. collector는 그 루트들을 활성화 되었다고 마킹하고, 그들이 가지고 있는 포인터들을 추적해서 더 많은 활성 객체들을 탐지합니다. collector는 더이상 마킹할 객체가 없을 때 까지 계속해서 새로 발견하는 객체들을 마킹하고, 포인터들을 추적합니다. 마킹을 다 진행한 뒤, heap 내에 마킹되지 않은 객체들이 있다면 이들은 모두 어플리케이션에서 도달할 수 없는 녀석들이고, 안전하게 수집될 수 있습니다.

마킹을 그래프 순회(graph traversal)로 생각할 수 있습니다. heap에 있는 객체들은 그래프의 노드들입니다. 한 객체에서 다른 객체로 이어지는 포인터들은 그래프에서 엣지에 해당합니다. 그래프에서 노드가 주어지면, 해당 노드의 밖으로 나아가는(out-going) 모든 엣지들을 그 객체의 hidden class를 통해 찾을 수 있습니다.

Object graph Figure 1. 객체 그래프

V8은 두 개의 mark-bit와 마킹 worklist를 통해 마킹을 구현합니다. 두 mark-bits는 세 가지 색깔로 인코드 됩니다: 흰색(00), 회색(10), 검정(11). 처음에 모든 객체들은 흰색입니다. 이는 아직 그들을 collector가 발견하지 못했음을 의미합니다. collector가 그들을 발견하고 marking worklist에 그들을 추가하면, 흰색 객체는 회색이 됩니다. 마지막으로 collector가 marking worklist에서 꺼내어 그것의 필드(fields)를 모두 방문하면 검정색이 됩니다. 이 스키마를 tri-color marking이라고 부릅니다. 마킹은 더이상 회색 객체가 없을 때 종료됩니다. 모든 남아있는 흰색 객체들은 도달할 수 없는 것이고, 안전하게 수집될 수 있습니다.

marking starts from the roots Figure 2. 마킹은 루트에서 시작합니다

the collector turns a grey object into black by processing its pointers Figure 3. collector는 그것의 포인터들을 처리하면서 회색 객체를 검정으로 바꿉니다

the final state after marking is finished Figure 4. 마킹을 모두 마친 후의 모습

위에서 설명한 마킹 알고리즘이 진행되는 동안에는 어플리케이션 실행이 잠시 중단돼야 한다는 것에 주목해야 합니다. 만약 마킹 중에 어플리케이션이 계속 실행된다면, 그래프가 변경될 수 있을 것이고 결국 활성 객체를 collector가 수집해버릴 수도 있습니다.

마킹에 의한 중단 줄이기

대량의 heap의 경우 마킹이 한 번 실행되면 수백 밀리초를 소모하게 됩니다.

이렇게 오랜 시간 어플리케이션이 중단되는 것은 어플리케이션을 둔하게 만들고 유저 경험(UX)을 안 좋게 합니다. 2011년 V8은 stop-the-world 마킹에서 점진적인(incremental) 마킹으로 전환했습니다. 점진적 마킹을 수행하는 동안 garbage collector는 마킹 작업을 더 작은 덩어리들(chunks)로 쪼개고 그들 사이사이에 어플리케이션이 실행될 수 있도록 합니다:

garbage collector는 어플리케이션에 의한 할당 비율과 일치시키기 위해 각 덩어리에서 얼마나 마킹을 수행할 것인가를 선택합니다. 일반적으로 어플리케이션의 반응성이 상당히 향상됩니다. 메모리 부족으로 인해 대량의 heap들에 대해서는 collector가 할당을 유지하려고 시도할 때 여전히 오랜 시간 중단이 있을 수 있습니다.

점진적 마킹은 알아서 제공되지 않습니다. 어플리케이션은 garbage collector에게 객체 그래프를 변화시키는 모든 연산에 대해 알려야합니다. V8은 다익스트라 스타일의 write-barrier를 이용해서 그 알림을 구현합니다. 자바스크립트에서 각각의 object.field = value 형식의 쓰기 연산 뒤에, V8은 이 write-barrier 코드를 삽입합니다:

// `object.field = value` 호출 후
write_barrier(object, field_offset, value) {
  if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}

write-barrier는 검정 객체가 흰색 객체를 가리키지 못하도록 강제합니다. 이는 strong tri-color invariant라고도 불리는데, 어플리케이션이 garbage collector로부터 활성 객체를 숨길 수 없도록 보장해줍니다. 따라서 마킹 후에 모든 흰색 객체는 확실하게 도달될 수 없고, 안전하게 수집될 수 있는 것입니다.

점진적 마킹은 이전의 블로그 포스트에서 설명한 idle time garbage collection 스케쥴링과 잘 통합됩니다. 크롬의 Blink 테스크 스케쥴러는 jank 없이 메인 쓰레드가 노는 시간(idle time) 동안에 약간의 점진적인 마킹 단계들을 스케쥴할 수 있습니다. 이 최적화는 노는 시간이 사용될 수 있는 경우에 효과적입니다.

write-barrier 비용 때문에, 점진적인 마킹은 어플리케이션의 처리량을 감소시킬 수도 있습니다. 추가적인 워커 쓰레드를 만듦으로써, 처리량과 중단 시간을 모두 개선시키는 것이 가능합니다. 워커 쓰레드에서 마킹을 진행하는 두 가지 방법이 있습니다: parallel 마킹과 concurrent 마킹.

Parallel 마킹은 메인 쓰레드와 워커 쓰레드에서 일어납니다. parallel 마킹 동안에 어플리케이션은 중단됩니다. 이는 stop-the-world 마킹의 멀티 쓰레드 버전입니다:

Concurrent 마킹은 주로 워커 쓰레드에서 일어납니다. concurrent 마킹이 진행되는 동안 어플리케이션은 계속해서 실행될 수 있습니다.

다음에 나오는 두 섹션에서 우리가 어떻게 V8에 parallel과 concurrent 마킹에 대한 지원을 추가했는지 설명합니다.

Parallel 마킹

parallel 마킹 동안에 어플리케이션이 함께 실행되지 않을 수 있다고 가정할 수 있습니다. 이렇게 되면 객체 그래프는 정적이고 바뀌지 않을 것이기 때문에, 구현이 상당히 간단해집니다. parallel로 객체 그래프를 마킹하기 위해서는, garbage collector 자료 구조를 thread-safe하게 만들 필요가 있고, 쓰레드 간에 마킹 결과도 효율적으로 공유할 방법도 찾아야 합니다. 아래의 다이어그램이 parallel 마킹에 관련된 자료구조들을 보여줍니다. 화살표는 데이터 흐름을 나타냅니다. 간단하게 하기 위해, heap defragmentation(조각 모으기)을 위해 필요한 자료구조들은 생략했습니다.

Figure 5. parallel 마킹을 위한 자료구조

쓰레드들은 객체 그래프를 읽기만 할 뿐 바꾸지는 않는다는 것을 주목해야 합니다. 해당 객체의 mark-bit들과 마킹 worklist는 읽기/쓰기 접근이 가능해야만 합니다.

마킹 worklist와 일감 뺏기

마킹 worklist의 구현은 성능 면에서 중요하고, 또 빠른 쓰레드 로컬 성능과 일감이 부족한 경우 얼마 만큼의 일이 다른 쓰레드들에게 분배될 수 있는가를 균형 맞춥니다. trade-off space에서의 극단적인 측면들은 (a)모든 객체가 공유될 가능성이 있으므로, 최적의 공유를 위한 완전한 concurrent 자료구조를 사용하는 것과 (b)객체가 공유될 수 없는 곳에서 쓰레드 로컬 산출물을 위한 최적화를 하며 완전한 쓰레드 로컬 자료구조를 사용하는 것입니다. Figure 6.은 쓰레드 로컬 삽입과 삭제 세그먼트(segments)에 기반한 마킹 worklist를 이용하여 V8이 어떻게 그들의 요구를 균등하게 처리하는지 보여줍니다. 한 세그먼트가 꽉 차게 되면, 공유하는 전역 pull(뺏기가 가능한)에 공표(publish) 됩니다. 이런 방식으로 V8은 마킹 쓰레드들이 지역적으로, 가능한 오랫동안 동기화 없이 동작할 수 있도록 해주고, 완전히 자신의 로컬 세그먼트를 drained한 다른 쓰레드가 굶주리는 동안 단일 쓰레드가 객체들의 새로운 서브 그래프에 도달한 경우들도 처리할 수 있도록 해줍니다.

Figure 6. 마킹 worklist

Concurrent 마킹

concurrent 마킹은 워커 쓰레드들이 heap에 있는 객체들을 방문하는 동안, 자바스크립트가 메인 쓰레드에서 실행될 수 있도록 해줍니다. 이로 인해 많은 잠재적 데이터 레이스가 열립니다. 예를 들어, 자바스크립트는 워커 쓰레드가 어떤 객체의 필드를 읽는 동안 동시에 그 객체의 필드에 값을 쓰고 있을 수도 있습니다. 이런 데이터 레이스는 garbage collector가 활성 객체를 수집할지말지 혼란스럽게 하거나, 원시 값과 포인터를 혼동하도록 혼란스럽게 합니다.

객체 그래프를 변경시키는 메인 쓰레드에서의 각 연산은 데이터 레이스의 잠재적 원인입니다. V8은 여러 객체 레이아웃 최적화를 통한 높은 성능의 엔진이기 때문에, 잠재적 데이터 레이스의 소스들의 리스트는 약간 깁니다. 이들은 높은 수준의 breakdown 입니다:

  • 객체 할당
  • 객체 필드에 쓰기
  • 객체 레이아웃 변경
  • 스냅샷 디-시리얼라이즈하기
  • 함수 비최적화 동안 실체화하기
  • 젊은 세대의 garbage collection 동안 evacuation
  • 코드 패치

이러한 연산들에 있어 메인 쓰레드는 워커 쓰레드들과 동기화를 해야합니다. 동기화의 비용과 복잡도는 연산에 의존합니다. 대부분의 연산들은 원자 단위 메모리 접근의 가벼운 동기화를 허용하지만, 몇몇 연산들은 객체에 대해 독점적인 접근을 요구합니다. 다음 서브 섹션들에서 몇몇 흥미로운 경우들을 강조하겠습니다.

쓰기 barrier

객체 필드 값 쓰기에 의한 데이터 레이스는 쓰기 연산을 완화된 원자 단위(relaxed atomic write) 쓰기로 변경함으로써, 쓰기 barrier를 약간 수정함으로써 해결될 수 있습니다:

// atomic_relaxed_write(&object.field, value); 호출 후
write_barrier(object, field_offset, value) {
  if (color(value) == white && atomic_color_transition(value, white, grey)) {
    marking_worklist.push(value);
  }
}

이전에 사용한 쓰기 barrier와 비교해보세요:

// `object.field = value`; 후
write_barrier(object, field_offset, value) {
  if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}

두 가지 변화가 있습니다:

  1. 소스 객체의 색깔 체크(color(object) === black)가 없어졌습니다.
  2. 흰색에서 회색으로 value의 색 변화가 원자 단위로 실행됩니다.

소스 객체의 색깔 확인 없이는 쓰기 barrier가 더욱 보수적이게 됩니다. 즉, 실제로 접근할 수 없는 객체들에 대해서도 활성 객체로 마킹할 수도 있습니다. 쓰기 연산과 쓰기 barrier 사이에 필요하게 되는 값 비싼 메모리 fence를 피하기 위해 확인을 제거하였습니다:

atomic_relaxed_write(&object.field, value);
memory_fence();
write_barrier(object, field_offset, value);

메모리 fence 없이, 객체 색깔을 로드하는 연산은 쓰기 연산보다 앞으로 재배치 될 수 있습니다. 만약 재배치를 막지 않으면, 쓰기 barrier는 회색 객체를 관측하게 되고, bail out 할 수 있습니다. 워커 쓰레드가 새 값을 보지 않고 해당 객체를 마킹합니다. 다익스트라 등에 의해 제시된 오리지널 쓰기 barrier는 객체 색깔을 확인하지 않습니다. 그들은 단순하게 하기 위해 그랬지만, 우리는 정확성이 요구됩니다.

구제 worklist

코드 패칭과 같은 몇몇 연산들은 객체에 대한 독점적인 접근은 필요로 합니다. 초기에 객체 별 락을 피하기로 결정했습니다. 왜냐하면 우선순위가 도치되는 문제를 야기할 수도 있기 때문입니다. 메인 쓰레드가 객체 락을 유지하면서 예정되지 않은 워커 쓰레드를 기다려야만 합니다. 객체를 락하는 것 대신에, 워커 쓰레드가 그 객체를 방문하는 것으로부터 구제되도록 하였습니다. 워커 쓰레드는 메인쓰레드만이 처리할 수 있는 구제 worklist에 해당 객체를 밀어넣습니다:

Figure 7. 구제 worklist

워커 쓰레드는 최적화된 코드 객체들, hidden class 그리고 weak collection들을 구제합니다. 왜냐하면 그들을 방문하는 것은 락킹이나 비싼 동기화 프로토콜을 요구하기 때문입니다. 돌이켜보면, 구제 worklist는 점진적 개발에서 좋다는 것이 드러났습니다. 우리는 모든 객체 타입들을 구제하면서 워커 쓰레드들을 구현하였습니다. 그리고 하나씩 concurrency를 추가했습니다.

객체 레이아웃 변화

객체 필드는 세 종류의 값들을 저장할 수 있습니다: 태그된 포인터, 태그된 작은 정수(Smi), unbox된 플로팅 포인트 숫자와 같은 태그되지 않은 값. 포인터 태깅(Pointer tagging)은 잘 알려진 기술입니다. 그것은 unboxed된 정수를 효율적으로 표현할 수 있게 합니다. V8에서 태그된 값의 the least significant bit는 그것이 포인터인지 정수인지를 가리킵니다. 이것은 포인터들은 word-aligned라는 사실에 기인합니다. 한 필드의 태그 여부 정보는 그 객체의 hidden class에 저장됩니다.

V8에서의 몇몇 연산은 객체 필드를 태그된 것에서 태그되지 않은 것으로, 혹은 그 반대로 해당 객체를 다른 hidden class로 전이함으로써 변경합니다. 그러한 객체 레이아웃 변경은 concurrent 마킹에 있어 안전하지 않습니다. 만약 워커 쓰레드가 이전의 hidden class를 통해 객체를 방문하고 있는 도중에 변화가 발생한다면, 두 가지의 버그가 발생할 수 있습니다. 첫 째로, 워커는 그것을 태그되지 않은 값이라 생각하며 포인터를 잃어버릴 수도 있습니다. 쓰기 barrier는 이러한 종류의 버그를 방지합니다. 둘 째로, 워커는 태그되지 않은 값을 포인터라고 다루고, 그것을 역참조할 수도 있습니다. 일반적으로 이것은 타당하지 않은 메모리 접근을 야기하여 프로그램 충돌을 발생시킵니다. 이 경우를 처리하기 위해서, 해당 객체의 mark-bit의 동기화를 맞춰주는 스냅샷 프로토콜을 사용합니다. 이 프로토콜은 두 녀석들과 관련이 있습니다: 객체 필드를 태그된 것에서 태그 되지 않은 것으로 변경하는 메인 쓰레드와 그 객체를 방문하는 워커 쓰레드. 필드를 변경하기 전에, 메인 쓰레드는 객체는 검정으로 마킹되었음을 보장하고, 추후에 방문하기 위해 구제 worklist에 push합니다:

atomic_color_transition(object, white, grey);
if (atomic_color_transition(object, grey, black)) {
  // The object will be revisited on the main thread during draining
  // of the bailout worklist.
  bailout_worklist.push(object);
}
unsafe_object_layout_change(object);

아래 코드 조각에서 보여지는 것처럼, 워커 쓰레드는 먼저 객체의 hidden class를 불러오고 atomic relaxed load operations을 이용하여 hidden class에 명시된 객체의 모든 포인터들을 스냅샷 찍습니다. 그리고 원자 단위의 비교/교체 연산을 이용하여 객체를 검정색으로 마킹하기 시작합니다. 마킹이 성공적으로 끝났다면, 이는 hidden class와 스냅샷이 일치함을 의미합니다. 메인 쓰레드는 객체의 레이아웃이 변경되기 전에 검정으로 마킹했기 때문입니다.

snapshot = [];
hidden_class = atomic_relaxed_load(&object.hidden_class);
for (field_offset in pointer_field_offsets(hidden_class)) {
  pointer = atomic_relaxed_load(object + field_offset);
  snapshot.add(field_offset, pointer);
}
if (atomic_color_transition(object, grey, black)) {
  visit_pointers(snapshot);
}

안전하지 않은 레이아웃 변경을 겪고 있는 흰색 객체는 메인 쓰레드에서 마킹 되어야만 한다는 것에 주목하세요. 안전하지 않은 레이아웃 변경은 상대적으로 드뭅니다. 따라서 실제 어플리케이션에서는 큰 영향을 미치지 않습니다.

모두 모아서

concurrent 마킹을 존재하는 점진적인 마킹 infrastructure로 통합했습니다. 메인 쓰레드는 루트를 스캔함으로써 마킹을 시작하고, 마킹 worklist를 채웁니다. 그 후, concurrent 마킹 업무를 워커 쓰레드들에게 게시합니다. 워커 쓰레드들은 메인 쓰레드를 도와 마킹 worklist를 소모시킴으로써 마킹 처리를 더욱 빠르게 합니다. 가끔 메인 쓰레드는 구제 worklist와 마킹 worklist를 처리함으로써 마킹에 참여합니다. 마킹 worklist가 비어 있게 되면, 메인 쓰레드는 garbage collection을 완료합니다. 완료하는 동안에 메인 쓰레드는 루트를 다시 스캔하고 추가적인 흰색 객체들을 발견할 수도 있습니다. 이 객체들은 워커 쓰레드들의 도움으로 parallel하게 마킹됩니다.

결과

실세계 벤치마킹 프레임워크는 모바일과 데스크탑 각각에서 하나의 garbage collection 주기 동안 약 65%, 70%의 메인 쓰레드의 마킹 시간의 감소를 보여줍니다.

Concurrent 마킹은 Node.js에서 garbage collection jank 역시 감소시킵니다. Node.js는 유휴 시간 garbage collection 스케쥴링을 절대 구현하지 않았고, 그래서 non-jank-critical 단계에서 마킹 시간을 감출 수 없기 때문에 이는 특히 중요합니다. Concurrent marking은 Node.js v10에 실려있습니다.

Lazy Function Definition Pattern

패턴에 대해 공부하다가 멋진 패턴, 그리고 좋은 글을 발견하여서 번역해보고자 합니다.

번역을 허락해주신 Peter 님께 감사드립니다. 원문의 출처는 https://getsready.com/wp-content/uploads/2016/10/best-scenery-at-paris.jpg 임을 밝힙니다.


Lazy Function Definition Pattern

이번 포스트에서 제가 Lazy Function Definition라고 부르는, 함수형 프로그래밍 디자인 패턴을 소개하고자 합니다. 저는 이 패턴이 자바스크립트에서 유용하다는 것을 자주 느낍니다. 특히, runtime에서 결정하는 cross-browser 라이브러리 코드를 짤 때에는 특히 그렇습니다.

몸풀기 문제

Date object를 리턴하는 foo라는 함수를 짜봅시다. 단, 리턴하는 Date object는 foo가 처음 불렸을 때의 시간을 가져야 합니다.

해법 #1: 무모한(구시대적인) 방법

이 해법은 지나치게 단순합니다. 전역 변수 t를 사용하여 Date object 값을 저장합니다. 처음 foo가 불릴 때 t에 그 시간을 저장합니다. 그 후에 또 실행이 되면, foo는 단순히 t에 저장된 값을 리턴합니다.

var t;
function foo() {
  if (t) {
    return t;
  }
  t = new Date();
  return t;
}

위의 코드는 2가지 문제점이 있습니다. 첫 째로, t는 불필요한 전역 변수인데다가, 그 값이 변할지도 모릅니다. 둘 째로, foo가 불릴 때 마다 if 조건을 매번 체크해야하므로 효율적이지도 못합니다. 이 예시에서 위의 if 조건을 체크하는 것은 가벼운 연산이지만, 실제 개발하며 마주치는 문제에서는 if-else-else... 문들로 이루어져 무거운 연산일수도 있습니다.

해법 #2: 모듈 패턴(Module Pattern)

첫 번째 해법의 문제 중 하나를 모듈 패턴을 이용하여 해결할 수 있습니다. 바로, 클로저를 이용함으로써 전역 변수인 tfoo안에서만 접근가능하도록 숨길 수 있습니다.

var foo = (function() {
  var t;
  return function() {
    if (t) {
      return t;
    }
    t = new Date();
    return t;
  }
})();

이 해법은 아직 if 조건을 foo가 불릴 때마다 체크하므로, runtime 때에 최적화된 해법은 아닙니다.

모듈 패턴은 매우 강력한 도구이지만, 이 문제에 대해서는 아니라고 생각합니다.

해법 #3: 함수는 객체다.

자바스크립트 함수들이 property를 가질 수 있는 객체라는 것을 안다면, 모듈 패턴 해법과 비슷한 수준의 해법을 만들어 낼 수 있습니다.

function foo() {
  if (foo.t) {
    return foo.t;
  }
  foo.t = new Date();
  return foo.t;
}

함수 객체가 property들을 가질 수 있다는 사실은 때에 따라 상당히 깨끗한 해법을 제공합니다. 개인적으로, 이 해법이 이번 상황에서는 모듈 패턴보다 더 개념적으로 단순하다고 생각합니다.

이 해법은 첫 번째 해법의 t가 전역변수가 되는 것을 막습니다. 하지만, 여전히 foo가 불릴 때 마다 if 조건은 체크되고 있습니다.

해법 #4: Lazy Function Definition

자 이제, 여러분 모두가 여기까지 온 이유입니다..

var foo = function() {
  var t = new Date();
  foo = function() {
    return t;
  };
  return foo();
};

처음 foo가 불릴 때, 새 Date를 초기화하고 그 Date를 클로져로 가지는 새로운 함수에 foo를 재할당합니다. 처음 foo가 불리우고 끝마치기 전에, 새로운 함수 foo가 불리우고, 리턴 값까지 던져줍니다.

추후에 불리는 foo는 단순히 그것의 클로져에 있는 t를 리턴합니다. 빠르고 효율적입니다. 특히 이전 해법들에 사용된 조건문들이 복잡하고 많다면 더욱 그렇습니다.

이 패턴에 대해 또 다르게 바라보는 방법은, 처음에 foo에 할당되었던 바깥 함수가 promise라고 생각하는 것입니다. 즉, 그 함수가 처음 불릴 때, foo를 더 유용한 함수로 재정의 함을 약속(promise)해줍니다. “promise”라는 용어는 막연히 Scheme의 lazy evaluation 매커니즘에서 왔습니다. 자바스크립트 프로그래머라면 누구나 Scheme 책을 정말 공부해보길 바랍니다. Scheme에는 자바스크립트 함수형 프로그래밍에 대한 더 많은 내용이 쓰여있습니다.

페이지 스크롤 결정하기

cross-browser 자바스크립트를 작성할 때, 브라우저 마다 각기 다른 알고리즘들은 하나의 자바스크립트 함수 이름으로 감싸서 사용됩니다. 이 방법은 브라우저 마다 다른 점을 감춤으로써 브라우저 API들을 일반화하고, 페이지 마다 복잡한 자바스크립트를 작성하고 그것을 유지보수하는 것을 더욱 단순하게 해줍니다. 감싼 함수가 불리울 때에는, 반드시 해당 브라우저에 적합한 알고리즘이 실행되어야 합니다.

드래그&드랍 라이브러리에서, 마우스 이벤트로부터 전달받는 커서 위치 정보를 사용하는 것은 대부분의 경우에 필요합니다. 마우스 이벤트는 페이지가 아닌, 브라우저 창에 대해서 상대적인 커서 좌표를 제공해줍니다. 마우스 윈도우 좌표로 그 페이지가 스크롤 된 양을 더하면 마우스의 페이지 좌표를 알 수 있습니다. 따라서, 페이지 스크롤을 알려주는(reporting) 함수가 필요합니다. 설명을 위해 이 예제에서는 getScrollY 함수를 정의했습니다. 드래그 하는 동안에는 드래그&드랍 라이브러리가 계속 작동하므로, getScrollY는 최대한 효율적이어야합니다.

불행히도, 페이지 스크롤을 알려주는 알고리즘이 브라우저 마다 달라서, 3가지나 존재합니다. Richard Cornford는 이 4개의 알고리즘에 관해서 그의 특징 탐지에 관한 글(feature detection article)에 작성하였습니다. 그런데 한 가지 큰 문제는 네 개의 알고리즘 중 하나가 document.body를 사용한다는 점입니다. 자바스크립트 라이브러리들은 보통 HTML의 <head> 안에서 로드됩니다. 그리고, 그 시점에는 document.body property는 존재하지 않습니다. 따라서 라이브러리가 로드될 때, 특징 탐지 방법을 통해 어떤 알고리즘을 사용할지 결정할 수는 없습니다.

이러한 문제에 대해, 자바스크립트 라이브러리들은 다음 둘 중에 하나의 방법을 사용합니다. 첫 번째 방법은 navigator.userAgent를 확인하고, 해당 브라우저에 특정된 효율적이고 미니멀한 getScrollY를 만드는 것입니다. 하지만, 브라우저 스니핑(browser sniffing)은 불안정하고 에러가 발생하기 쉽기 때문에 매우 좋지 않은 방식입니다. 두 번째 훨씬 나은 방법은 getScrollY가 실행될 때마다 특징 탐지 방법을 사용해서 적합한 알고리즘을 결정하는 것입니다. 하지만, 이 두 번째 방법은 효율적이지 않습니다.

좋은 소식은 드래그&드랍의 getScrollY는 유저가 페이지 내의 엘리먼트들과 상호작용 하기 전까지는 사용되지 않을 거라는 점입니다. 만약 페이지 내의 엘리먼트가 존재한다면, document.body 또한 존재할 것입니다. 즉, getScrollY가 처음 불리울 때, 효율적인 getScrollY를 만들어내기 위해 Lazy Function Definition pattern을 특징 탐지 방법과 혼합하여 사용할 수 있습니다.

var getScrollY = function() {
  if (typeof window.pageYOffset == 'number') {
    getScrollY = function() {
      return window.pageYOffset;
    };

  } else if ((typeof document.compatMode == 'string') &&
            (document.compatMode.indexOf('CSS') >= 0) &&
            (document.documentElement) &&
            (typeof document.documentElement.scrollTop == 'number')) {
    getScrollY = function() {
        return document.documentElement.scrollTop;
    };
  } else if ((document.body) &&
            (typeof document.body.scrollTop == 'number')) {

    getScrollY = function() {
      return document.body.scrollTop;
    }
  } else {
    getScrollY = function() {
      return NaN;
    };
  }

  return getScrollY();
}

참고로, 위의 코드는 페이지 스크롤을 결정하기에 매우 큰 노력이 든다고 보일 수도 있습니다. 많은 사람들, 그리고 라이브러리들은 페이지 스크롤을 결정하기 위해 다음 방법을 사용하고, 만족해 합니다. 이 테크닉은 심지어 지금 이 글의 댓글에도 언급되었습니다.

var getScrollY = function() {
  return window.pageYOffset ||
    (document.documentElement && document.documentElement.scrollTop) ||
    (document.body && document.body.scrollTop);
}

하지만, 이 코드에서 만약 페이지가 스크롤 되지 않았고, 처음 두 조건 중 하나가 참이라면 document.body.scrollTopundefined가 되고, 저 함수는 0이 아니라 undefined를 리턴할 것 입니다. 즉, 이것은 getScrollY 함수가 스크롤을 reporting할 능력이 있는지 확인하는 데에는 역부족입니다.

요약

Lazy Function Definition 패턴은 밀집되고(dense) 훌륭하며(robust) 효율적인 코드를 짤 수 있도록 해줍니다. 이 패턴을 볼 때마다 저는 자바스크립트의 함수형 프로그래밍 능력에 감탄합니다.

자바스크립트는 함수형, 객체지향형 프로그래밍을 모두 지원합니다. 자바스크립트에 적용할 수 있는 객체지향 디자인 패턴 책들은 여러 권 있습니다. 불행히도 함수형 프로그래밍 디자인 패턴에 관해 예를 드는 책은 적습니다. 자바스크립트 커뮤니티가 좋은 함수형 패턴들의 컬렉션을 종합하는 데에는 시간이 걸릴 것입니다.

Javascript patterns

자바스크립트 패턴들에 대해 알아보겠습니다.

1. Module pattern

모듈 패턴(module pattern)은 가장 기초적인 패턴입니다.

var t;
function foo() {
    if (t) {
        return t;
    }
    t = new Date();
    return t;
}
var foo = (function() {
    var t;
    return function() {
        if (t) {
            return t;
        }
        t = new Date();
        return t;
    }
})();

2. Lazy function definition pattern

var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};