클로저(Closure)는 프로그래머가 창조적이고 인상적이며 간결한 프로그래밍을 할 수 있게 해줍니다. 클로저는 빈번하게 사용되며 자바스크립트 스킬과 관계없이 자주 마주치게 될 것입니다. 물론, 지금 당장 클로저는 복잡해 보일 수 있습니다만, 이 글을 읽고 클로저에 대해 이해하게 된다면 자바스크립트 코딩시에 매일 사용하게 될 것입니다.
이 글은 비교적 클로저에 대해 간략히 설명하고 있습니다. 이 글을 계속 읽기전에 먼저 자바 스크립트의 변수 범위에 대해 이해할 필요가 있습니다. 만약, 그렇지 못하다면 블로그의 자바스크립트 변수의 범위와 호이스팅에 관한 글을 먼저 읽어 주시기 바랍니다.
클로저란 무엇인가
클로저는 외부함수(포함하고 있는)의 변수에 접근할 수 있는 내부 함수를 일컫습니다. 스코프 체인(scope chain)으로 표현되기도 합니다. 클로저는 세가지 스코프 체인을 가집니다: 클로저 자신에 대한 접근(자신의 블럭내에 정의된 변수), 외부 함수의 변수에 대한 접근, 그리고 전역 변수에 대한 접근. 이렇게 3단계로 구분할 수 있습니다.
내부 함수는 외부 함수의 변수뿐만 아니라 파라미터에도 접근할 수 있습니다. 단, 내부 함수는 외부 함수의 arguments 객체를 호출할 수는 없습니다. (하지만, 외부 함수의 파라미터는 직접 호출할 수 있습니다.)
기본적인 클로저 예제:
1
function showName(firstName, lastName) { var nameIntro = "Your name is "; // 이 내부 함수는 외부함수의 변수뿐만 아니라 파라미터 까지 사용할 수 있습니다. function makeFullName() { return nameIntro + firstName + " " + lastName; } return makeFullName(); } showName("Michael", "Jackson"); // Your name is Michael Jackson
클로저는 Node.js의 비동기, 논-블록킹 아키텍처의 핵심기능으로 활용되고 있습니다. 클로저는 jQuery에서도 빈번히 사용되며, 거의 모든 자바스크립트 코드에서 볼 수 있습니다.
jQuery의 전형적인 클로저 사용예:
1
$(function() { var selections = []; $(".niners").click(function() { // 이 클로저는 selections 변수에 접근합니다. selections.push(this.prop("name")); // 외부 함수의 selections 변수를 갱신함 }); });
클로저 규칙과 부수 효과
클로저는 외부함수가 리턴된 이후에도 외부함수의 변수에 접근할수 있습니다.
클로저를 사용하면서 가장 헷갈리는것 중의 하나는 외부함수가 리턴된 이후에도 여전히 내부함수가 외부함수의 변수에 접근하고 있다는 것입니다. (네. 당신이 바로 읽은것 맞습니다.-.-) 자바스크립트의 함수가 실행되었을때, 함수는 자신이 생성되었을때와 동일한 스코프 체인을 사용합니다. 그러므로, 당신은 내부 함수를 나중에 호출할 수 있습니다.
1
function celebrityName(firstName) { var nameIntro = "This is celebrity is "; // 이 내부 함수는 외부함수의 변수와 파라미터에 접근할 수 있습니다. function lastName(theLastName) { return nameIntro + firstName + " " + theLastName; } return lastName; } var mjName = celebrityName("Michael"); // 여기서 celebrityName 외부함수가 리턴됩니다. // 외부함수가 위에서 리턴된 후에, 클로저(lastName)가 호출됩니다. // 아직, 클로저는 외부함수의 변수와 파라미터에 접근 가능합니다. mjName("Jackson"); // This celebrity is Michael Jackson
클로저는 외부 함수의 변수에 대한 참조를 저장합니다.
클로저는 실제 값을 저장하지 않습니다. 클로저가 호출되기 전에 외부함수의 변수가 변경되었을때, 클로저는 더 흥미로워 집니다. 그리고, 이 강력한 기능은 창의적인 방법으로 활용될 수 있습니다. 아래의 내부(private) 변수예제는 더글라스 크락포드(Douglas Crockford)에 의해 처음 시연되었습니다:
1
function celebrityID() { var celebrityID = 999; // 우리는 몇개의 내부 함수를 가진 객체를 리턴할것입니다. // 모든 내부함수는 외부변수에 접근할 수 있습니다. return { getID: function() { // 이 내부함수는 갱신된 celebrityID변수를 리턴합니다. // 이것은 changeThdID함수가 값을 변경한 이후에도 celebrityID의 현재값을 리턴합니다. return celebrityID; }, setID: function(theNewID) { // 이 내부함수는 외부함수의 값을 언제든지 변경할 것입니다. celebrityID = theNewID; } } } var mjID = celebrityID(); // 이 시점에, celebrityID외부 함수가 리턴됩니다. mjID.getID(); // 999 mjID.setID(567); // 외부함수의 변수를 변경합니다. mjID.getID(); // 567; 변경된 celebrityID변수를 리턴합니다.
클로저 비꼬기
클로저가 갱신된 외부함수의 변수에 접근함으로써, 외부 함수의 변수가 for문에 의해 변경될 경우 의도치 않은 버그가 발생할 수 있습니다.
1
function celebrityIDCreator(theCelebrities) { var i; var uniqueID = 100; for (i=0; i<theCelebrities.length; i++) { theCelebrities[i]["id"] = function() { return uniqueID + i; } } return theCelebrities; } var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}]; var createIdForActionCelebs = celebrityIDCreator(actionCelebs); var stalloneID = createIdForActionCelebs[0]; console.log(stalloneID.id); // 103
위의 예제에서, 익명의 내부함수가 실행될 시점에 i의 값은 3입니다(배열의 크기만큼 증가한 값). 숫자 3은 uniqueID에 더해져 모든 celebritiesID에 103을 할당합니다. 그래서, 기대(100,101,102)와 달리 모든 리턴된 배열의 id=103이 됩니다.
이런 결과가 나타난 이유는, 앞서 언급했듯이 클로저는(이 예제에서 내부의 익명함수) 외부 변수에 대해 값이 아닌 참조로 접근하기 때문입니다. 즉, 클로저는 최종 갱신된 변수(i)에 대해서만 접근할 수 있으므로, 외부 함수가 전체 for문을 실행하고 리턴한 최종 i의 값을 리턴하게 됩니다. 100+3=103.
이런 부작용을 고치기 위해서 “즉시 호출된 함수 표현식(Immediately Invoked Function Expression. IIFE)”를 사용할 수 있습니다.
1
function celebrityIDCreator(theCelebrities) { var i; var uniqueID = 100; for (i=0; i<theCelebrities.length; i++) { theCelebrities[i]["id"] = function(j) { // j 파라미터는 호출시 즉시 넘겨받은(IIFE) i의 값이 됩니다. return function() { // for문이 순환할때마다 현재 i의 값을 넘겨주고, 배열에 저장합니다. return uniqueID + j; } () // 함수의 마지막에 ()를 추가함으로써 함수를 리턴하는 대신 함수를 즉시 실행하고 그 결과값을 리턴합니다. } (i); // i 변수를 파라미터로 즉시 함수를 호출합니다. } return theCelebrities; } var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}]; var createIdForActionCelebs = celebrityIDCreator(actionCelebs); var stalloneID = createIdForActionCelebs[0]; console.log(stalloneID.id); // 100 var cruiseID = createIdForActionCelebs[1]; console.log(cruiseID.id); // 101
이 글은 javascriptissexy.com의 허락을 받아 번역 하였습니다. 상업적인 목적이 아니라면 얼마든지 퍼가셔도 좋습니다. 단, 원문과 번역의 차이가 다소 있을 수 있으므로 번역글과 함께 아래 원문 사이트 주소도 항상 첨부해 주시기 바랍니다. 그외 오역이나 개선될 문장이 있다면 트위터 멘션이나 댓글, 이메일등 어떤 경로로든 알려주시면 바로 잡도록 하겠습니다.
JavaScript는 접근 수정자(private, public, protected)를 제공하지 않기 때문에, 기본적으로 모든 객체의 속성(변수, 메서드)은 public이다. 그럼 어떻게 객체를 캡슐화 할 수 있을까?
우리 팀이 이용하고 있는 방식은 아래와 같다. 이렇게 하라고 강제한 것은 아닌데 대부분 이렇게 코드를 작성한다.
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
varUser=function(){
this._$init.apply(this,arguments);
};
User.prototype={
_$init:function(htOption){
this._sName=htOption.sName;
this._nAge=htOption.nAge;
},
_callAjax:function(){
console.log("This is Private");
console.log("Ajax Call!!");
},
resetData:function(){
console.log("This is Public");
this._callAjax();
},
printInfo:function(){
console.log(this._sName+"세 남자 "+this._nAge);
}
};
어디서 많이 본 듯한 느낌인데, 클래스 기반 언어의 클래스와 비슷한 하다. jindo.class의 영향을 받은 것 같다. (jQuery처럼 메모리에 로딩해놓고 사용하는 전역 라이브러리를 개발하는 프로젝트가 없었기 때문인 것 같기도 함)
여기에 적용된 코딩 규칙은 간단하다.
prototype에 객체의 멤버를 정의한다.
private 속성은 이름 앞에 _를 붙여서 구분한다.
객체 내에서 사용할 멤버는 _$init 함수 안에 선언한다.
이 방식은 가독성이 높고, 객체를 생성할 때마다 매번 객체의 멤버를 생성하지 않아도 되기 때문에, 객체 생성 비용을 절약할 수 있다는 것이 장점이다. 기존의 객체지향언어에 익숙한 개발자가 적응하기 쉽다는 것 또한 좋은 점이라고 할 수 있다.
그런데 개인적으로 이 방식에 뭔가 찝찝한 구석이 하나 있다. 바로 private이 private이 아니라는 것. 그냥 속성(메서드나 멤버변수)의 이름 앞에 _를 붙이면 private으로 취급하자고 암묵적으로 약속을 했을 뿐, 외부에서 얼마든지 접근이 가능하기 때문에 본질적으로 private이 아니다.
물론 “그게 뭐 그렇게 중요한 문제인가?” 라고 쿨하게 생각해버리면 그만이다. 이 주제를 가지고 누군가와 말 싸움이 붙으면 무기로 이용할 적당한 논리가 생각은 안 나지만, 어쨌든 난 이게 마음에 들지 않는다. 그래서 이걸 외부에서 접근할 수 없는 private으로 바꿔주고 싶다.
열심히 머리를 굴려서 Module Pattern과 Closure를 이용해서 아래와 같이 객체를 선언했다.
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
varUser=(function(){
var_User=function(){
this._$init.apply(this,arguments);
};
/* private method */
var_callAjax=function(){
console.log("This is Private");
console.log("Ajax Call!!");
};
/* public method */
User.prototype={
_$init:function(htOption){
this._sName=htOption.sName;
this._nAge=htOption.nAge;
},
resetData:function(){
console.log("This is Public");
/* 함수 실행 콘텍스트를 맞춰줘야 하므로. */
_callAjax.apply(this);
},
printInfo:function(){
console.log(this._sName+"세 남자 "+this._nAge);
}
};
return_User;
});
오…!! _callAjax 함수를 외부에서 접근할 수 없는 private으로 만들었다. 함수를 호출할 때 콘텍스트를 맞춰주기 위해 다소 귀찮은 작업을 해줘야 한다는 것은 단점이다. 하지만 앞에 this._를 붙여서 호출하는 거나 이거나 싶다.
JavaScript
1
2
3
4
5
_callAjax.apply(this);
this._callAjax;
아예 함수를 선언할 때 .bind(this)를 해줄 수도 있을 거 같긴 한데 이건 더 귀찮을 것 같다.
JavaScript
1
2
3
4
5
6
var_callAjax=function(){
console.log("This is Private");
console.log("Ajax Call!!");
}.bind(this);
어쨌든 private 함수를 만드는 데는 성공했다.
이제 멤버변수를 감춰야 한다. 후딱해치우………고자 했으나 이게 쉽지 않네…? 잉?
다시 내가 원하는 private 변수의 조건을 정리하고 차근차근 생각해보자.
private으로 외부에서 접근할 수 없어야 한다.
public 메서드는 prototype에 선언하며, prototype 메서드에서 private 변수를 사용할 수 있어야 한다.
생성자를 이용해서 생성한 객체가 고유의 값을 가질 수 있어야 한다.
뾰족한 방법이 안 떠오르는데, 별도의 변수 저장공간을 만들어서 캡슐화하면 될 것 같다.
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
varUser=(function(){
var_User=function(htOption){
varhtPrivateVars={
"_sName":htOption.sName,
"_nAge":htOption.nAge
};
this.getPrivateVar=function(sKey){
returnhtPrivateVars["_"+sKey];
};
this._$init.apply(this,arguments);
};
/* private method */
var_callAjax=function(){
console.log("This is Private");
console.log("Ajax Call!!");
}.bind(this);
/* public */
User.prototype={
_$init:function(htOption){
/* 뭔가 초기화 작업을 하겠지. */
},
resetData:function(){
console.log("This is Public");
/* 함수 실행 콘텍스트를 맞춰줘야 하므로. */
_callAjax.apply(this);
},
printInfo:function(){
console.log(this.getPrivateVar("nAge")+"세 남자 "+this.getPrivateVar("sName"));
}
};
return_User;
})();
htPrivateVar라는 별도의 멤버변수 저장공간을 만들어서 getPrivateVar라는 getter를 통해서만 접근할 수 있도록 했다.
이렇게 만들어서 팀 동료에게 보여줬더니, “가독성이 떨어지고 뭔가 더 귀찮아 보인다”라고… ㅠ-ㅠ;;
“낯설어서 그런거다. 익숙해지면 괜찮음!!!” 하고 말해주고 싶지만, 이렇게해서 얻는 이득이 잃는 것보다 큰 것 같지 않아서 더 좋은 방법을 찾아야 할 것 같다.
다른 방법을 찾아보자….
.
.
.
다음에 시간날 때 다시 생각해봐야겠다. 힝.
– 2013.11.4 21:58
집에 오는 지하철에서 잠깐 생각해봤는데, 외부에서 래핑하지 않고서는 내가 원하는 조건을 충족하는 private 변수를 선언할 수 없을 것 같다. 정말 그러한지를 증명하는 게 더 빠를 듯?
– 2013.11.6 23:15
팀 동료 우영이랑 술 한잔 하면서 이런저런 이야기를 하다가 생각이 정리가 되었다.
내가 아는 지식 내에서,
private으로 외부에서 접근할 수 없어야 한다.
public 메서드는 prototype에 선언하며, prototype 메서드에서 private 변수를 사용할 수 있어야 한다.
생성자를 이용해서 생성한 객체가 각기 다른 값을 참조해야 한다.
이 조건을 만족하는 private 변수는 만들 수 없다. 그 이유는 클로저가 생길 수 없기 때문이다.
JavaScript에서 private 변수는 클로저를 이용해서 존재하지 않는 접근 수정자를 우회적으로 만드는 일종의 테크닉이다. 클로저는 함수 선언 시점에 함수 블럭의 유효범위 안에서 외부에 있는 변수를 참조할 때 생긴다. 즉, prototype에 객체 리터럴로 함수를 선언할 때, private으로 만들고 싶은 멤버변수가 외부에 접근할 수 있는 곳에 있어야 한다는 이야기다.
이러한 성질을 이용해서 private 변수를 만들려면 prototype 객체를 정의할 당시의 유효 범위 안에 var로 멤버변수를 선언해야 하는데, 이럴 경우 new로 생성한 모든 객체가 같은 값을 공유해서 private으로 선언한 정적 변수처럼 취급되어 버린다.
글로 설명하기 참 힘든데, 예제를 놓고 이야기해보자.
위의 스크린 샷에서 빨간 색으로 박스 쳐 놓은 부분에 var 키워드로 선언한 변수는 prototype 함수에서 접근할 수 있다. var _sName; var _nAge;를 선언하는 콘텍스트에 prototype 객체에 정의하는 함수의 어휘적 유효범위가 속해있기 때문이다. 따라서 prototype 객체에 정의한 함수에서 접근할 수 있으므로 클로저가 생긴다. 그런데 사용자가 User 객체를 생성할 때 넘겨주는 초기화 값을 _User 생성자 함수에서 _sName과 _nAge를 참조하여 처리하고 있는 것이 문제다.
_User 함수에서 자신의 유효범위 밖에 있는 변수를 참조하기 때문에 여기에도 클로저가 생겨버린다. 따라서 _User 생성자 함수로 생성하는 모든 객체는 하나의 변수를 참조하게 된다. 이는 내가 세운 조건 중에 3번째 조건을 위반한다.
생성자를 이용해서 생성한 객체가 각기 다른 값을 참조해야 한다.
JavaScript의 클로저를 잘 모른다면 이해하기 어려운 이야기인데, 다음과 같이 요약할 수 있다.
“private으로 외부에서 접근할 수 없으나, prototype에서 사용 가능하면서 모든 객체가 각기 다른 값을 갖는 멤버 변수를 만드는 것은 그러한 클로저를 만들 수 없기 때문에 불가능하다.”
일단 지금까지 고민한 결과로 나는 이렇게 결론을 내렸는데 혹시 어떤 방법을 아는 분 계시다면 언제든지 연락주세요!.
– 2013.11.10 12:50
며칠 고민한 끝에 내가 내린 결론은, “억지로 만들지 말자” 이다. 계속 생각을 해봤는데, private 멤버를 만듦으로써 얻는 것보다 잃는 것이 더 많기 때문이다.가독성도 떨어져, 성능도 떨어져. 차라리 암묵적으로 약속한 _ 표기를 사람들이 잘 따라줄 것이라고 믿는 편이 나을 것 같다.
언어는 언어마다 자신의 철학을 가지고 있다. 그 철학을 무시하고, “프로그래밍 언어는 이래야 해”라는 관점에서 접근하는 것은 약간의 오버를 더해서 마치 인종차별과 유사하다. 존재하거나 존재하지 않는 모든 것에는 어떤 이유가 있다. 그래서 성급하게 결론 내리고 억지로 껴 맞추기 보다는, 왜 그러한가에 좀 더 집중하기로 했다.
=================================
=================================
=================================
반응형
728x90
출처: https://gamecodingschool.org/tag/javascript/
오늘은 JavaScript 함수의 두 가지 대표적인 특징인 스코핑 규칙과 클로저에 대해 설명합니다.
스코핑(scoping)
JavaScript는 정적 스코핑(static/lexical scoping)하는 언어입니다. 초기 Lisp을 제외하고 동적 스코핑(dynamic scoping)하는 언어는 거의 없기 때문에 “정적” 스코핑이란 말은 크게 의미가 없습니다. 다만, 다른 언어와 달리 블록 스코핑(blocking scoping)이 아닌 함수 스코핑(function scoping)이라는 점이 특이합니다.
예를 들어, 아래 foo() 함수 호출은 x의 값을 리턴하는데, x 값은 1이 아닌 2가 리턴됩니다. 블록 안에서 새로 변수 x를 선언하고 2를 할당했지만, 자바스크립트는 함수 스코핑이기 때문에 함수 foo()의 x 변수는 단 하나만 존재합니다. 따라서 var x=2는 새로운 변수를 선언이 아니라 단순히 x=2라는 할당문으로 취급됩니다.
1
2
3
4
5
6
7
8
9
10
11
functionfoo() {
varx = 1;
{
varx = 2;
}
returnx;
}
varx = foo();
console.log(x);
// 2
함수 스코핑이 블록 스코핑에 비해 가지는 장점은 없습니다. 의도한 설계라기 보다는 초기 프로그래밍 언어 설계 오류라고 보는 편이 맞습니다. 실제로 ES6에서는 이 문제를 바로 잡기 위해 let 키워드를 이용한 블록 스코핑을 새로 도입하였습니다. 아래 foo_() 함수는 var 키워드를 let으로 바꾸었을 뿐인데, 1이 리턴되는 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
functionfoo_() {
letx = 1;
{
letx = 2;
}
returnx;
}
varx_ = foo_();
console.log(x_);
// 1
ES6는 이전 버전 JavaScript와 호환성 때문에 var와 let 키워드를 모두 허용하지만, 새로 작성되는 코드는 모두 let으로 작성하는 것이 좋습니다.
클로저
JavaScript 함수는 1등 시민(first-class citizen)이기 때문에 string이나 number와 마찬가지로 함수의 인자로 넘기거나, 함수의 리턴값으로 돌려주거나, 변수에 저장하는 등의 일이 가능합니다. 따라서 함수 안에서 number나 string을 생성하고 사용할 수 있는 것처럼 함수 안에서 새로운 함수를 선언하고 사용하는 것도 가능합니다.
1
2
3
4
5
6
7
8
9
10
functionmakeAdder() {
functionadd(a, b) {
returna + b;
}
returnadd;
}
varadder = makeAdder();
console.log(adder(1, 2));
// 3
위의 makeAdder() 함수를 호출하면 새로운 함수 add()를 리턴하는데, 이 함수는 인자 a, b를 받아서 합을 리턴하는 함수입니다. 호출 결과를 변수 adder에 저장하면 adder(1,2)처럼 함수 호출도 가능함을 확인할 수 있습니다. 이 경우, 함수add()는 makeAdder() 안에 선언되어 있기 때문에 함수 밖에서 접근할 수 없다는 점을 제외하면 최상단에 정의한 함수와 다를 바가 없습니다.
JavaScript는 여기서 한발 더 나아갑니다. 아래 makeAdder() 함수는 인자 a를 받아서 a를 더해주는 함수 addA를 리턴합니다. 여기서 a는 addA()에 선언된 변수가 아니라 makeAdder()에 선언된 변수라는 점이 특별합니다. JavaScript 함수는 해당 함수에 참조하고자 하는 변수가 없으면 여기서 포기하지 않고 바깥 함수(enclosing function)들에 있는 변수를 찾는 것입니다.
1
2
3
4
5
6
7
8
9
10
functionmakeAdder(a) {
functionaddA(b) {
returna + b;
}
returnaddA;
}
varadd1 = makeAdder(1);
console.log(add1(2));
// 3
따라서 makeAdder(1)이 리턴하는 함수는 인자 b를 받아서 1을 더해주는 함수가 됩니다. addA()에서 참조한 변수 a는 makeAdder() 함수가 리턴된 후에도 addA()에서 사용할 수 있기 때문에 가비지 콜렉션되지 않도록 JavaScript 런타임에 처리를 해줍니다.
우리는 보통 함수를 코드로만 생각합니다. 데이터는 함수 호출 시 인자로 넘기는 것이고 함수 자체는 순수한 코드라고 생각하는 경향이 있습니다. 하지만 위 예에서 볼 수 있듯이 JavaScript 함수는 단순히 코드가 아니라 코드+데이터입니다.addA() 함수는 덧셈을 하는 코드도 가지고 있지만, a의 값인 데이터도 가지고 있기 때문입니다. 이렇게 코드와 데이터를 모두 가지고 있는 함수를 클로저(closure)라고 부릅니다.
클로저가 강력한 이유는 “코드+데이터 = 프로그램”이기 때문입니다. 즉, 클로저만 있으면 모든 프로그램을 표현할 수 있다는 뜻입니다. 오브젝트도 마찬가지입니다. “오브젝트 = 코드+데이터”이고, “코드+데이터 = 클로저”이므로 “오브젝트 = 클로저”라는 공식이 성립합니다.
실제로 JavaScript에서는 클로저를 이용해 오브젝트를 만드는 일이 흔합니다. 클로저를 이용해 counter 오브젝트를 만들면 다음과 같습니다. (이 코드는 MDN 클로저 페이지에서 차용했습니다.) changeBy(), increment(), decrement(),value() 함수 모두 privateCounter 변수에 접근하는 클로저임을 확인할 수 있습니다.
이들 고차 함수의 공통점은 함수의 인자로 함수를 받거나 함수를 리턴한다는 점입니다. filter를 예로 들면, 배열의 각 원소에 대해 callback 함수를 호출하여true가 리턴되는 경우에만 해당 인자를 리턴 값의 배열에 포함시킵니다. 아래 예제 코드를 보면, isBigEnough 함수를 통과하는 값(10보다 크거나 같은 값)만 리턴 값에 포함되는 것을 확인할 수 있습니다.
이런 고차함수는 아주 작은 수준의 코드 재활용으로 생각할 수 있습니다. 만약filter라는 함수가 없었다면, 우리는 필터링 조건이 달라질 때마다 새로운 함수를 작성했을 것입니다. 예를 들어, 아래 pos와 neg 두 함수는 배열을 인자로 받아 각각 0보다 큰 숫자, 0보다 작은 숫자만 리턴하는 함수입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
functionpos(arr)
{
varret = [];
for(vari = 0; i < arr.length; i++) {
if(arr[i] > 0) {
ret.push(arr[i]);
}
}
returnret;
}
functionneg(arr)
{
varret = [];
for(vari = 0; i < arr.length; i++) {
if(arr[i] < 0) {
ret.push(arr[i]);
}
}
returnret;
}
varxs = [0, -1, 2, 3, -4, 5];
console.log(pos(xs));
console.log(neg(xs));
// [ 2, 3, 5 ]
// [ -1, -4 ]
위 두 함수는 if 문 안의 조건을 제외하고는 사실상 모든 코드가 동일합니다. 따라서 이 부분만 인자로 빼내면 filter와 유사한 범용적인 함수가 나오게 됩니다. 그리고 이렇게 일단 filter 함수가 생기면 pos, neg 함수는 filter 함수를 재활용해서 쉽게 작성할 수 있게 됩니다.
재미있는 건 재활용이 여기서 그치지 않습니다. pos와 neg 함수가 서로 동일한 구조를 공유하고 있었기 때문에 filter라는 함수를 뽑아낼 수 있었던 것처럼filter를 포함한 다른 고차함수들도 비슷한 구조를 공유하고 있기 때문에 공통점을 다시 뽑아낼 수 있습니다.
여기서 등장하는 함수가 reduce 함수입니다. reduce는 맥가이버칼처럼 여러 고차함수를 만들어낼 수 있는 표현력을 가지고 있습니다. reduce 함수는 배열의 각 원소를 돌면서 콜백 함수를 반복적으로 적용하여 하나 값을 뽑아내는 함수입니다. 아래 예제는, 배열을 돌면서 각 원소 값의 총합을 리턴하는 코드입니다.
1
2
3
4
functionadd(acc, value) {
returnacc + value;
}
[1, 2, 3, 4, 5].reduce(add, 0);
이 코드의 실행 결과는 add(add(add(add(add(0, 1), 2), 3), 4), 5)와 같습니다. reduce의 두 번째 인자가 acc의 초기값이 되고, 이후 각 원소를 덧셈한 결과가 계속 acc에 누적되어 넘어가는 방식입니다. 이 과정을 그림으로 표현하면 다음과 같습니다. 참고로 JavaScript의 reduce 함수는 사실 함수 프로그래밍에서는 foldl이라는 함수명으로 더 잘 알려져 있습니다.
얼핏 보기에 reduce 함수와 filter 함수는 크게 공통점이 없어 보입니다. filter함수는 배열을 받아서 다른 배열을 리턴하는데, reduce 함수는 배열을 받아서 원소를 리턴하는 것처럼 보이기 때문입니다. 하지만 reduce 함수를 이용해 다음과 같이 filter 함수를 구현할 수 있습니다. acc에 빈 배열 []을 넘기고predicate(x)가 참이면 해당 원소를 acc에 추가하면 최종적으로 predicate(x)를 만족하는 원소들을 리턴하게 됩니다.
1
2
3
4
5
6
7
functionmyfilter(arr, predicate) {
returnarr.reduce(function(xs, x) {
if(predicate(x))
xs.push(x);
returnxs;
}, []);
}
비슷한 방식으로 map 함수도 구현할 수 있습니다.
1
2
3
4
5
6
functionmymap(arr, f) {
returnarr.reduce(function(xs, x) {
xs.push(f(x));
returnxs;
}, []);
}
filter와 map은 별개의 함수처럼 보이지만, reduce 함수를 통해 공통점을 뽑아낼 수 있는 수준의 유사점도 가지고 있음을 확인할 수 있습니다. 두 함수 모두 배열이라는 데이터 구조를 돌면서 각 원소에 대해 어떤 처리를 하고 결과값을 리턴합니다. filter는 각 원소가 predicate를 통과하는지 확인하고, 통과하는 원소들만 다시 배열로 리턴하는 반면 map은 각 원소에 함수 f를 적용하고 결과값을 조건 없이 배열로 리턴한다는 차이점이 있지만, 결과적으로 배열이라는 데이터 구조를 돌면서 결과값을 만들어내는 방식 자체는 동일한 셈입니다.
바꿔 말해, 배열로 뭔가를 해야 하는 거의 모든 함수는 reduce 함수 (혹은reduceRight 함수)로 만들어 낼 수 있다는 뜻입니다. filter와 map 외에도underscore.js가 제공하는 대부분의 고차 함수를 reduce로 만들어 낼 수 있습니다.
JavaScript 바로 알기에서 JavaScript가 많은 단점에도 불구하고 살아남은 이유는 JavaScript의 함수가 first-class이기 때문이라고 말씀드렸습니다. 비유를 하자면, first-class 함수는 무엇이든 만들어낼 수 있는 줄기 세포와 같아서 JavaScript의 부족한 부분을 채울 수 있었기 때문입니다.
블록 범위(block scoping)
아래 코드를 실행하면 콘솔에 1이 찍힐까요, 아니면 2가 찍힐까요?
1
2
3
4
5
vara = 1;
{
vara = 2;
}
console.log(a);
일반적인 기대와는 다르게 JavaScript는 콘솔에 2를 출력합니다. { } 블록이 Java나 C#의 블록 범위를 연상시키지만, JavaScript는 이런 일반적인 기대와 달리 함수 범위(function scoping)만 지원하기 때문에 같은 함수 내에서는 블록과 상관 없이 변수의 범위가 하나만 존재합니다.
하지만 실망할 필요는 없습니다. 다음과 같이 JavaScript의 익명 함수(anonymous function)를 정의한 후에 곧바로 호출하면 var a = 2가 이 익명 함수 범위에 정의되므로 사실상 블록 범위를 지정한 것과 같은 효과를 낼 수 있습니다.
JavaScript는 Python, Ruby 등과 마찬가지로 동적 타이핑하는 언어이고, 동적 타이핑하는 언어는 일반적으로 메소드나 필드에 대한 접근 제한자(access modifier)를 제공하지 않습니다. 바꿔 말해, 모든 메소드나 필드가 공개(public)됩니다.
하지만 JavaScript에서는 함수를 이용하여 비공개 필드(private field)를 만들어 낼 수 있습니다. 생성자(constructor) 함수에서 지역 변수를 선언하고, 클로저(closure)로 정의한 메소드가 참조하게 만드는 방법입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
functionCounter(init)
{
varcount = 0;
this.inc = function() {
count++;
console.log(count);
};
this.dec = function() {
count--;
console.log(count);
};
}
varc = newCounter();
c.inc();
c.dec();
위 예제에서 count 변수는 로컬 변수이기 때문에 Counter 함수가 리턴되고 나면 더 이상 접근이 불가능하지만, inc와 dec 메소드에서 참조하고 있기 때문에 두 메소드에서는 이후에도 계속 접근이 가능합니다. inc, dec 메소드 외에는 이 변수에 접근할 방법이 없으므로 비공개 필드와 똑같은 효과를 냅니다.
참고로 이 문제는 ES6 Symbol 도입으로 해결하려고 했으나 아직 엄밀한 의미의 비공개 필드는 제공하지 않고 있습니다.
클래스
JavaScript는 일반적인 객체지향 프로그래밍 언어와 달리 프로토타입이라는 특이한 상속 모델을 가지고 있는 프로그래밍 언어입니다. 클래스 상속과 프로토타입 상속의 장단점을 차치하고, 일단 대부분의 개발자들에게 익숙치 않은 방식이라는 측면에서 프로토타입 상속 방식은 사실상 실패했다고 볼 수 있습니다. 다행인 것은 함수를 이용해 클래스 방식의 상속을 만들어낼 수 있다는 점입니다.
ES6에서는 class 지원이 포함되었기 때문에 위 코드는 아래와 같이 일반적인 객체지향 프로그래밍 스타일로 표현할 수 있습니다. 하지만 ES6에 class 지원이 공식적으로 포함되기 전에도 JavaScript 개발자는 위와 같이 first-class 함수를 이용해 이미 클래스 기반의 상속 메커니즘을 사용하고 있었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
classAnimal {
constructor(public name: string) { }
move(meters: number) {
alert(this.name + " moved "+ meters + "m.");
}
}
classSnake extendsAnimal {
constructor(name: string) { super(name); }
move() {
alert("Slithering...");
super.move(5);
}
}
정리
JavaScript는 1999년에 제정된 ES3 이후로 거의 10년이 넘게 새로운 표준을 제정하지 못하고 표류했습니다. 일반적인 프로그래밍 언어라면 이미 사장되고도 남을 충분한 시간이었습니다. 하지만 웹브라우저에서 사용할 수 있는 마땅한 대안 언어가 없었고, 다행히 JavaScript가 first-class 함수를 지원한 덕분에 언어의 부족한 면을 이 글에서 소개한 다양한 기술들을 이용해 메꾸어 왔습니다. ES 5, 6, 7 표준화를 통해 이런 부분들이 채워지면 위에서 소개한 방법들도 추억 속으로 사라지겠지만, 우리가 여전히 기억해야할 것은 “(first-class) 함수는 많은 것을 할 수 있다”라는 사실입니다.
JavaScript는 세상에서 가장 많이 쓰이는 언어 중 하나입니다. “시작은 미약하였으나 끝은 창대하리라”는 말이 딱 들어맞는 언어가 JavaScript인 것 같습니다. 초기 웹브라우저의 간단한 스크립트 언어로 출발한 뭔가 어설픈 언어가 인터넷의 발전과 더불어 지금은 전세계에서 가장 많은 개발자를 확보하고 있는 메인스트림 언어가 되었으니 말입니다.
전 웹브라우저 개발을 하면서 2008-2010년에 JavaScript의 표준을 정하는 Ecma의TC39 미팅에 정기적으로 참석을 했었습니다. 당시 모질라, 구글, 야후, MS, IBM, 오페라 등이 참여하여 10년 이상 정체된 JavaScript의 다음 버전을 논의하고 있었는데, 그렇게 정리되서 나온 것이 EcmaScript 5(이하 ES5)입니다. 얼마 전에 ES6가 나왔고, ES 7을 이미 논의하기 시작했으니 벌써 시간이 많이 흘렀습니다. 당시 저는 30초반의 경험 부족 엔지니어라 세계적인 대가인 Brendan Eich, Douglas Crockford, Mark Miller, Waldemar Horwat, Allen Wirfs-Brock 등과 같은 자리에서 이야기 나누고, 같이 밥 먹는 것만으로 신기했던 기억이 납니다.
Douglas Crockford가 JavaScript: The Good Parts에서 이야기한 것처럼 JavaScript는 좋은 점도, 나쁜 점도 많은 언어입니다. 하지만 좋은 점이 나쁜 점을 모두 상쇄하고도 남을 만큼 강력하였기 때문에 다른 언어로 대체되지 않고, 지금도 계속 발전하고 있다고 생각합니다.
제가 생각하는 JavaScript의 좋은 점은 “객체지향 프로그래밍”이 아닌 “함수형 프로그래밍”에 있습니다. Java나 C#이 람다(lambda)를 도입하기 훨씬 전에, JavaScript는 이미 함수를 인자로 넘기고, 리턴값으로 돌려주고, 변수에 저장할 수 있었습니다. JavaScript는 이 first-class 함수를 이용하여 JavaScript의 많은 단점(block scoping의 부재, class의 부재 등)을 극복해 냅니다. 그렇게 때문에 JavaScript를 바로 아는 것은 JavaScript에 숨어 있는 함수형 프로그래밍을 이해하고 활용하는 방법을 아는 것이 됩니다.
JavaScript는 게임 개발자와도 분리할 수 없는 언어입니다. Unity가 지원하는 언어 중 하나(Unity JavaScript)이기도 하고, node.js 프레임워크가 많이 사용되면서 게임 서버 프로그래밍에서도 중요한 언어가 되었기 때문입니다. 그래서 앞으로 “C# 바로 알기” 시리즈와 더불어서 “JavaScript 바로 알기” 시리즈를 블로그에 연재하려고 합니다. 많은 관심 부탁드립니다.
앞서 게임 서버: node.js의 장점과 단점이라는 글에서 node.js의 문제점으로 불편한 비동기 프로그래밍 모델을 꼽았습니다. 글의 말미에 비동기 프로그래밍의 불편함을 극복하기 위한 노력으로 ES6의 Promise와 (아직 논의 중인) ES7의 async 함수에 대한 이야기를 잠깐 언급했었습니다. 이 시리즈에서는 ES6 Promise에 대해 좀 더 자세히 설명하겠습니다.
일단 한 가지 의문점으로 시작하겠습니다. 이미 웹브라우저나 node.js에서 모두 콜백 방식으로 API를 제공하고 있고, 자바스크립트 개발자라면 당연히 이런 방식에 익숙할 수밖에 없는데, 왜 ES6에서는 이와는 다른 Promise라는 스타일을 표준으로 만든 걸까요? 물론 PromiseJS, Q, RSVP, Bluebird, when.js 등의 자바스크립트 Promise 라이브러리가 많이 나와있지만, 일부 개발자만이 사용할뿐 대세와는 거리가 멀었던 게 사실입니다.
ES6 Promise를 소개하고 있는 글들은 대부분 ES6 Promise가 새로운 스타일의 비동기 프로그래밍 모델을 제공하고 있고, 기존의 콜백 방식에 비해서 더 좋다고 주장하고 있습니다. 하지만 ES6 Promise를 실제로 사용해 보신 분들은 이 지점에서 고개를 갸웃거릴 수밖에 없습니다. 기존 콜백 방식에서도 async 모듈 등을 사용해서 나름대로 불편함을 해결해왔기 때문에 말 그대로 “스타일의 차이” 외에 ES6 Promise 확실히 더 좋은 비동기 프로그래밍 모델이라고 주장할 근거가 약하기 때문입니다.
특히, HTML5 Rocks에 올라온 JavaScript Promises There and back again와 같은 글은 비교 방식에 문제가 있습니다. ES6 Promise에는 then(), map(), foreach() 등 sequencing과 parallelism을 표현하는 함수가 존재하고 콜백 방식에는 없는 것처럼 설명하고 있는데, 콜백 방식에서도 이미 async 모듈의 series, parallel, map 등을 사용해 같은 수준의 추상화를 하고 있기 때문입니다.
물론 ES6 Promise의 장점은 분명히 존재합니다. 특히, err 인자를 모든 함수에 넘기는 방식에 비해 ES6 Promise의 에러 처리 방식은 분명히 개선된 점이 있습니다. 하지만 ES6 Promise를 단순히 기존 콜백 방식의 문제점을 약간 개선한 새로운 비동기 프로그래밍 스타일 정도로 설명해서는 안 됩니다. ES6 Promise 꿈은 더 원대합니다. ES6 Promise는 불편한 “비동기 프로그래밍” 세상을 떠나 다시 우리가 살던 낙원인 “동기 프로그래밍”으로 돌아가기 위한 노력이기 때문입니다.