패턴에 대해 공부하다가 멋진 패턴, 그리고 좋은 글을 발견하여서 번역해보고자 합니다.
번역을 허락해주신 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)
첫 번째 해법의 문제 중 하나를 모듈 패턴을 이용하여 해결할 수 있습니다. 바로, 클로저를 이용함으로써 전역 변수인 t
를 foo
안에서만 접근가능하도록 숨길 수 있습니다.
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.scrollTop
은 undefined
가 되고, 저 함수는 0
이 아니라 undefined
를 리턴할 것 입니다. 즉, 이것은 getScrollY
함수가 스크롤을 reporting할 능력이 있는지 확인하는 데에는 역부족입니다.
요약
Lazy Function Definition 패턴은 밀집되고(dense) 훌륭하며(robust) 효율적인 코드를 짤 수 있도록 해줍니다. 이 패턴을 볼 때마다 저는 자바스크립트의 함수형 프로그래밍 능력에 감탄합니다.
자바스크립트는 함수형, 객체지향형 프로그래밍을 모두 지원합니다. 자바스크립트에 적용할 수 있는 객체지향 디자인 패턴 책들은 여러 권 있습니다. 불행히도 함수형 프로그래밍 디자인 패턴에 관해 예를 드는 책은 적습니다. 자바스크립트 커뮤니티가 좋은 함수형 패턴들의 컬렉션을 종합하는 데에는 시간이 걸릴 것입니다.