JS Function(자바스크립트 함수)
자바스크립트는 함수를 다양한 방법으로 사용합니다. 따라서 함수를 완벽히 익히는 것은 자바스크립트 개발자에게 필수 기술일 것입니다.
다른 언어에서는 별도의 문법으로 처리하는 다양한 작업들을 자바스크립트에서는 함수가 수행합니다.
이 글에서는 자바스크립트에서 함수를 정의하는 다양한 방법들과 함수 표현식 그리고 함수 선언문에 대해서 알아보고 지역 유효범위와 변수 호이스팅(hoisting)이 어떻게 동작하는지 살펴보도록 합니다.
그리고 나서 함수에 더 나은 인터페이스를 제공하는 API, 전역 변수를 덜 사용하는 초기화, 불필요한 작업을 회피해 성능에 도움을 주는 여러가지 패턴들에 대해 알아봅니다.
배경 지식
자바스크립트의 함수를 특별하게 만드는 두 가지 중요한 특징이 있습니다.
첫째, 함수는 일급(first-class)
객체입니다.
둘째, 함수는 유효범위(scope)
를 제공합니다.
함수는 다음과 같은 특징을 가지는 객체입니다.
- 런타임(run time), 즉 프로그램 실행 중에 동적으로 생성할 수 있다.
- 변수에 할당할 수 있고, 다른 변수에 참조를 복사할 수 있으며, 확장이 가능하고, 몇몇 특별한 경우를 제외하면 삭제할 수 있다.
- 다른 함수의 인자로 전달할 수 있고, 다른 함수의 반환 값이 될 수 있다.
- 자기 자신의 프로퍼티와 메서드를 가질 수 있다.
따라서, 다음과 같은 상황도 가능합니다.
함수 A가 객체로서 프로퍼티와 메서드를 가지고, 이 중 하나가 또 다른 함수인 B입니다. 이 함수 B는 C라는 함수를 인자로 받아들이고, 실행 결과로 또 다른 함수 D를 반환한다.
이런 형태를 처음 보았을 때 관리할 함수가 많다고 느낄 수 있을 것입니다. 하지만 다양한 함수 응용 방법에 익숙해지면 함수가 제공하는 능력과 유연성 그리고 표현력의 진가를 인정하게 될 것입니다.
일반적으로 말하자면 자바스크립트에서 함수는 하나의 객체라고 생각하면 될 것입니다.
다만 이 객체는 호출하여 실행할 수 있는 특별한 기능을 가지고 있습니다.
함수가 객체라는 사실은 다음과 같은 new Function()
생성자의 동작을 보면 명백해집니다.
// 안티 패턴
// 데모의 목적으로만 사용합니다.
var add = new Function('a, b', 'return a + b');
add(1,2); // 3을 반환한다.
이 코드에서 add()는 생성자를 통해 만들었기 때문에 객체라는 사실이 자명합니다.
그러나 Function()
생성자의 사용은 eval()
만큼이나 좋지 않은 방법입니다. 코드가 문자열로 전달되어 평가되기 때문입니다.
또한 따옴표를 이스케이프해야하고 가독성을 높이기 위해 함수 본문을 들여쓰기하려면 별도로 신경써야하기 때문에 읽고 쓰기에 불편합니다.
두 번째로 중요한 기능은 함수가 유효범위를 제공한다는 것입니다.
자바스크립트에서는 중괄호({})
지역의 유효범위가 없습니다. 달리 말해서 블럭이 유효범위를 만들지 않습니다.
단지 함수 유효범위가 있을 뿐입니다.
어떤 변수이건 함수 내에서 var로 정의되면 지역 변수이고 함수 밖에서는 참조할 수 없습니다. 중괄호가 지역 유효범위를 제공하지 않는다는 말은 변수를 if 조건문이나 for문, while문 내에서 var로 정의해도, 이 변수가 if나 for문의 지역 변수가 되지 않는다는 뜻입니다.
이 변수는 해당 블럭을 감싸는 함수가 있을 때만 지역 변수가 되고 감싸는 변수가 없으면 전역 변수가 된다는 것입니다.
용어 정리
패턴에 대해 이야기할 때 합의된 정확한 이름을 사용하는 것은 코드자체만큼이나 중요합니다.
그러므로 함수를 정의하는 코드와 관련된 용어에 대해 간단히 짚고 넘어가 봅니다.
다음의 코드를 살펴봅니다.
// 기명 함수 표현식
var add = function add(a, b) {
return a + b;
};
이 코드는 기명 함수 표현식(named function expression)을 사용한 함수입니다.
이름을 생략한 함수 표현식을 무명 함수 표현식(unnamed function expressioin)이라고 합니다. 그냥 간단하게 함수 표현식(function expression)이라고도 하며, 익명함수(anonymous function)라는 말로도 널리 쓰이고 있습니다.
예제는 다음과 같습니다.
// 함수 표현식 ( 또는 익명 함수)
var add = function (a, b) {
return a + b;
};
따라서 '함수 표현식'이 더 넓은 의미의 용어이며, '기명 함수 표현식'은 함수 표현식 중 이름을 정의한 함수를 가리키는 구체적인 용어입니다.
처음의 기명 함수 표현식 코드에서 두번째 add를 생략하고 무명 함수 표현식으로 끝내도, 함수의 정의나 뒤이른 함수의 호출에는 영향을 미치지 않습니다.
유일한 차이점은 함수 객체의 name 프로퍼티가 빈 문자열이 된다는 것입니다. name 프로퍼티는 ECMA 표준이 아니라 언어의 확장기능이지만 많은 실행 환경에서 폭넓게 사용되고 있습니다.
그리고 두번째 add를 그대로 유지한다면 add.name 프로퍼티는 문자열 'add'라는 값을 가지게 됩니다. name 프로퍼티는 파이어버그와 같은 디버거를 사용할 때, 그리고 함수 안에서 자기 자신을 재귀적으로 호출할 때 유용합니다.
이러한 용도로 사용할 게 아니라면 그냥 생략해도 무방합니다.
마지막으로, 함수 선언문(function declaration)
이 있습니다.
함수 선언문은 다른 언어들의 함수 사용과 비슷합니다.
function foo() {
// 함수 본문
}
문법적인 면에서, 함수 표현식의 결과를 변수에 할당하지 않을 경우(이러한 사용버을 콜백 패턴이라고 한다) 기명 함수 표현식과 함수 선언문은 비슷해 보일 것입니다.
때로는 함수를 생성하는 문맥을 보지 않고서는 함수 선언문과 이름이 지정된 함수를 구분할 수 있는 방법이 없습니다.(이는 뒤에서 다룹니다)
세미콜론이 붙는지 여부에 따라 그 둘의 문법적인 차이점이 있습니다.
함수 선언문에는 세미콜론이 필요하지 않지만 함수 표현식에는 필요합니다. 세미콜론 삽입 장치가 자동으로 세미콜론을 붙여줄 수 있더라도, 항상 세미콜론을 직접 추가해야 합니다.
함수 리터럴이라는 용어도 자주 사용된다. 이 용어는 함수 표현식을 뜻할 수도 있고 기명 함수 표현식을 뜻할 수도 있다.
따라서 이러한 애매한 표현의 사용은 자제하는 편이 낫다.
선언문 vs. 표현식 : 이름과 호이스팅
그렇다면 함수 선언문과 함수 표현식 중 어떤 것을 사용해야 하는 것일까?
이 물음의 문제는 구문상 함수 선언문을 사용할 수 없는 경우를 생각하면 의외로 쉽게 풀릴 것입니다.
함수 객체를 매개 변수로 전달하거나, 객체 리터럴로 메서드를 정의하는 다음의 예제를 생각해 봅니다.
// 함수 표현식을 callMe 함수의 인자로 전달한다.
callMe(function(){
// 이 함수는 무명함수(익명 함수) 표현식이다.
});
// 기명 함수 표현식을 callMe 함수의 인자로 전달한다.
callMe(function me() {
// 이 함수는 'me'라는 기명 함수 표현식이다.
});
// 함수 표현식을 객체의 프로퍼티로 저장한다.
var myObject = {
say : function () {
// 이 함수는 함수 표현식이다.
}
};
함수 선언문은 전역 유효범위나 다른 함수의 본문 내부, 즉 '프로그램 코드'에서만 쓸 수 있습니다.
변수나 프로퍼티에 할당할 수 없고, 함수 호출시 인자로 함수를 넘길 때도 사용할 수 없습니다.
그렇다면 언제 함수 선언문을 사용할 수 있을까?
다음 예제에서 함수 foo(), bar(), local()은 모두 함수 선언문 패턴으로 정의되었습니다.
// 전역 유효범위
function foo() {
function local() {
// 지역 유효범위
function bar() { }
return bar;
}
}
함수의 name 프로퍼티
함수를 정의하는 패턴을 선택할 때는 읽기 전용인 name 프로퍼티를 쓸 일이 있는지도 고려하는 편이 낫습니다.
앞서 언급했듯이 name 프로퍼티는 표준이 아니지만 많은 실행 환경에서 사용가능합니다.
함수 선언문과 기명 함수 표현식을 사용하면 name 프로퍼티가 정의됩니다. 반면 무명 함수 표현식의 name 프로퍼티 값은 경우에 따라 다릅니다.
IE에서는 undefined가 되고, 파이어폭스와 웹킷에서는 빈 문자열로 정의됩니다.
function foo() { } // 함수 선언문
var bar = function() { }; // 함수 표현식
var baz = function baz() { }; // 기명 함수 표현식
foo.name; // "foo"
bar.name; // ""
baz.name; // "baz"
name 프로퍼티는 파이어버그나 다른 디버거에서 코드를 디버깅할 때 유용할 수 있습니다.
함수 내에서 발생한 에러를 보여주어야 할 때, 디버거가 name 프로퍼티 값을 확인하여 이름표로 쓸 수 있기 때문입니다.
name 프로퍼티는 함수 내부에서 자신을 재귀적으로 호출할 때 사용하기도 합니다.
이 두 가지 경우에 해당하지 않는다면 무명 함수 표현식이 더 쓰기 쉽고 간결합니다.
함수 선언문보다 함수 표현식을 선호하는 이유는, 함수 표현식을 사용하면 함수가 다른 객체들과 마찬가지로 객체의 일종이며 어떤 특별한 언어 구성요소가 아니라는 사실이 좀더 명확하게 드러나기 때문입니다.
엄밀히 말하면, 기명 함수 표현식을 그와 다른 이름의 변수에 할당할 수 있다. 예를 들면 다음과 같다.
var foo = function bar() { };
하지만 어떤 브라우저(IE)에서는 이 사용법이 제대로 구현되어 있지 않기 때문에 권장하지는 않는다.
함수 호이스팅
앞선 내용을 보고 함수 선언문과 기명 함수 표현식의 동작 방식이 거의 동일하다고 판단지었을지도 모르겠습니다.
그러나 완전히 같지는 않습니다. 이는 호이스팅(hoisting)
동작에 차이점이 있습니다.
호이스팅이라는 용어는 ECMAScript에 정의되어 있지는 않지만 흔하게 사용되며 그 특성을 설명하게 적절하다.
이미 아시겠지만 모든 변수는 함수 본문 어느 부분에서 선언(declaration)되더라도 내부적으로 함수의 맨 윗부분으로 끌어올려(hoist)집니다.
함수 또한 결국 변수에 할당되는 객체이기 때문에 동일한 방식이 적용됩니다.
함수 선언문을 사용하면 변수 선언뿐 아니라 함수 정의(definition) 자체도 호이스팅되기 때문에 자칫 오류를 만들어내기 쉽습니다.
다음의 코드를 살펴봅니다.
// 안티 패턴(설명을 위해 사용함)
// 전역 함수
function foo() {
console.log('global foo');
}
function bar() {
console.log('global bar');
}
function hoistMe() {
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
foo(); // "local foo"
bar(); // Uncaught TypeError : bar is not a function
// 함수 선언문 :
// 변수 'foo'와 정의된 함수 모두 호이스팅된다.
function foo() {
console.log('local foo');
}
// 함수 표현식 :
// 변수 'bar'는 호이스팅되지만 정의된 함수는 호이스팅되지 않는다.
var bar = function () {
console.log('local bar');
};
}
hoistMe();
보다시피, hoistMe() 함수 본문 내에서 foo와 bar를 정의하면, 실제 변수를 정의한 위치와 상관없이 끌어올려져 전역 변수인 foo와 bar를 덮어쓰게 됩니다.
그런데 지역 변수 foo()는 나중에 정의되어도 상단으로 호이스팅되어 정상 동작하는 반면, bar()의 정의는 호이스팅되지 않고 선언문과 호이스팅됩니다.
때문에 bar()의 정의가 나오기 전까지는 undefined 상태이고, 따라서 함수로 사용할 수도 없습니다. 또한 선언문 자체는 호이스팅되었기 때문에 유효범위 체인 내애서 전역 bar()도 보이지 않습니다.
이제 함수에 대해 필요한 배경지식과 용어는 마무리 짓고, 콜백 패턴을 시작으로 자바스크립트에서 활용할 수 있는 함수와 관련된 패턴들을 살펴보도록 하겠습니다.
콜백(callback) 패턴
함수는 객체입니다. 즉 함수를 다른 함수에 인자로 전달할 수 있습니다.
introduceBugs() 함수를 writeCode() 함수의 인자로 전달하면 아마도 writeCode()는 어느 시점에 introduceBugs()를 실행(또는 호출)할 것입니다.
이 때 introduceBugs()를 콜백 함수 또는 간단하게 콜백이라고 부릅니다.
function writeCode(callback) {
// 어떤 작업을 수행하는 코드
callback();
// ...
}
function introduceBugs() {
// 버그를 만드는 코드
}
writeCode(introduceBugs);
introduceBugs()가 writeCode()의 인자로 괄호없이 전달된 사실을 눈여겨 보도록 합니다.
괄호를 붙이면 함수가 실행되는데 이 경우에는 함수의 참조만 전달하고 실행은 추후 적절한 시점에 writeCode()가 호출(실행)해줄 것이기 때문에 괄호를 덧붙이지 않은 것입니다.
콜백 예제
예제를 통해 살펴봅니다. 처음에는 콜백 없이 시작하여 나중에 리팩터링할 것입니다.
복잡한 작업을 수행한 후 그 결과로 대용량 데이터셋을 반환하는 범용 함수가 있다고 합시다.
이 함수는 findNodes()와 같은 형식으로 호출되며, DOM 트리를 탐색해 필요한 엘리먼트의 배열을 반환합니다.
var findNodes = function () {
var i = 10000, // 긴 루프
nodes = [], // 결과를 저장할 배열
found; // 노드 탐색 결과
while (i) {
i -= 1;
// 이 부분이 노드를 찾는 코드가 들어감
nodes.push(found);
}
return nodes;
};
이 함수는 범용적으로 쓸 수 있도록 실제 일리먼트에는 어떤 작업도 하지 않고 단지 DOM 노드의 배열을 반환하기만 하도록 유지하는게 좋습니다.
노드를 수정하는 로직은 다른 함수에 두도록 합니다.
예를 들어 hide()라는 함수를 만들어 봅니다. 이 함수는 이름에서 짐작할 수 있듯이 페이지에서 노드를 숨깁니다.
var hide = function (nodes) {
var i = 0, max = nodes.length;
for (; i < max; i += 1) {
nodes[i].style.display = 'none';
}
};
// 함수를 실행한다.
hide(findNodes());
이 구현은 findNodes()에서 반환된 노드의 배열에 대해 hide()가 다시 루프를 돌아야하기 때문에 비효율적입니다.
findNodes()에서 노드를 선택하고 바로 숨긴다면 재차 루프를 돌지 않아아 더 효율적일 것입니다. 그렇지만 findNodes()안에서 노드를 숨기는 로직을 구현하면 탐색과 수정 로직의 결합으로 인해 범용 함수의 의미가 퇴색될 것입니다.
바로 이럴 대 콜백 패턴을 사용할 수 있습니다.
노드를 숨기는 로직의 실행을 콜백 함수에 위임하고 이 함수를 findNodes()에 전달하도록 합니다.
// findNodes() 가 콜백을 받도록 리팩터링한다.
var findNodes = function (callback) {
var i = 10000, // 긴 루프
nodes = [], // 결과를 저장할 배열
found; // 노드 탐색 결과
// 콜백 함수를 호출할 수 있는지를 확인한다.
if (typeof callback !== 'function') {
callback = false;
}
while (i) {
i -= 1;
// 이 부분이 노드를 찾는 코드가 들어감
// 여기서 콜백을 실행한다.
if(callback) {
callback(found);
}
nodes.push(found);
}
return nodes;
};
이 구현 방법은 직관적입니다. findNodes()에는 콜백 함수가 추가되었는지 확인하고, 있으면 실행하는 작업 하나만 추가되었습니다.
콜백은 생략할 수 있기 때문에 리팩터링된 findNodes()는 여전히 이전과 동일하게 사용할 수 있고, 기존 API에 의존하는 코드를 망가뜨리지 않습니다.
다음과 같이 콜백으로 인해 hide()의 구현은 노드들을 순회할 필요없이 더 간단히 작성됩니다.
// 콜백 함수
var hide = function (node) {
node.style.display = 'none';
};
// 노드를 찾아서 바로 숨긴다.
findNodes(hide);
이 예제에서는 보다시피 이미 존재하는 함수를 콜백 함수로 사용할 수도 있지만, findNodes() 함수를 호출할 때 익명 함수를 생성해서 쓸 수도 있습니다.
예를 들어, 동일한 범용의 fundNodes() 함수를 사용해 노드를 보여주는 방법은 아래와 같습니다.
// 익명함수를 콜백으로 전달한다
findNodes(function (node) {
node.style.display = 'none';
});
콜백과 유효범위
이전의 예제에서 콜백은 다음과 같은 패턴으로 실행되었습니다.
callback(parameters);
이 코드는 간단하고 대부분의 경우 충분히 휼륭하게 동작합니다. 그러나 콜백이 일회성의 익명 함수나 전역 함수가 아니고 객체의 메서드인 경우도 많습니다.
만약 콜백 메서드가 자신이 속해있는 객체를 참조하기 위해 this를 사용하면 예상치 않게 동작할 수도 있습니다.
myapp이라는 객체의 메서드인 paint() 함수를 콜백으로 사용한다고 가정해 봅니다.
var myapp = {};
myapp.color = 'green';
myapp.paint = function (node) {
node.style.color= this.color
};
// findNodes() 함수 동작 코드
var findNodes = function (callback) {
// ...
if( typeof callback == 'function') {
callback(found);
}
// ...
}
findNodes(myapp.paint)를 호출하면 this.color가 정의되지 않아 예상대로 동작하지 않을 것입니다.
findNodes()가 전역 함수이기 때문에 객체 this는 전역 객체를 참조합니다.
findNodes()가 dom.findNodes()처럼 dom이라는 객체의 메서드라면, 콜백 내부의 this는 예상과는 달리 myapp이 아닌 dom을 참조하게 된다.
이러한 문제를 해결하기 위해서는 콜백 함수와 함께 콜백이 속해 있는 객체를 전달하면 되는 것이다.
findNodes(myapp.paint, myapp);
// 전달받은 객체를 바인딩할 수 있도록 findNodes() 또한 수정한다.
var findNodes = function (callback, callback_obj) {
// ...
if(typeof callback === 'function') {
callback.call(callback_obj, found)
}
// ...
};
call 메서드 사용방법에 대해서는 이곳에서 참고바랍니다.
비동기 이벤트 리스너
콜백 패턴은 일상적으로 다양하게 사용되고 있습니다.
예를 들어 페이지의 엘리먼트에 이벤트 리스너를 붙이는 것도, 실제로는 이벤트가 발생했을 때 호출될 콜백 함수의 포인터를 전달하는 것입니다.
다음은 document의 click 이벤트 리스너로 console.log() 콜백 함수를 전달하는 예제입니다.
document.addEventListener('click', console.log, false);
대부분의 클라이언트 측 브라우저 프로그래밍은 이벤트 구동(event-driven)방식입니다.
사용자는 페이지에 click, keypress, mouseenter, mouseleave와 같은 다양한 이벤트를 발생시킵니다. 자바스크립트가 이벤트 구동형 프로그래밍에 적합한 이유는 프로그램이 비동기적으로, 달리 말하면 무작위로 동작할 수 있게 하는 콜백 패턴 덕분입니다.
타임아웃
또 다른 콜백 패턴의 실전 예제는 브라우저의 window 객체에 의해 제공되는 타임아웃 메서드들인 setTimeout()
과 setInterval()
입니다.
이 메서드들도 콜백 함수를 받아서 실행시킵니다.
var thePlotThickens = function () {
console.log('500ms later...');
};
setTimeout(thePlotThickens, 500);
thePlotThickens가 괄호없이 변수로 전달된 점에 주의하기 바랍니다.
여기서는 이 함수를 곧바로 실행하지 않고 setTimeout()이 나중에 호출될 수 있도록 함수를 가리키는 포인터만을 전달하고 있습니다.
함수 참조(포인터) 대신 문자열 "thePlotThickens()"를 전달하는 것은 eval()과 비슷한 안티 패턴입니다.
라이브러리에서의 콜백
콜백은 라이브러리를 설계할 때 유용한 간단하고 강력한 패턴입니다.
소프트웨어 라이브러리에 들어갈 코드는 가능한 범용적이고 재사용할 수 있어야 합니다. 콜백은 이런 일반화에 도움이 될 수 있습니다.
생각할 수 있는 모든 기능을 예측하고 구현할 필요는 없습니다. 이는 라이브러리를 쓸데없이 부풀릴 뿐이고 대부분의 사용자는 그런 커다란 기능들의 덩어리를 절대 필요로 하지 않기 때문입니다.
대신에 핵심 기능에 집중하고 콜백의 형태로 '연결고리(hook)'를 제공할 수 있는 것입니다.
콜백 함수를 활용하면 조금 더 쉽게 라이브러리 메서드를 만들고 확장하고 가다듬을 수 있습니다.
함수 반환하기
함수는 객체이기 때문에 반환 값으로 사용될 수 있습니다.
즉 함수의 실행 결과로 꼭 어떤 데이터 값이나 배열을 반환할 필요는 없다는 뜻입니다. 보다 특화된 함수를 반환할 수도 있고, 입력한 값에 따라 필요한 함수를 새로 만들어낼 수도 있습니다.
간단한 예제를 살펴봅니다.
아래 함수는 일회적인 초기화 작업을 수행한 후 반환 값을 만듭니다. 반환 값은 실행 가능한 함수입니다.
var setup = function () {
console.log(1);
return function () {
console.log(2);
};
};
// setup 함수를 사용
var my = setup(); // console.log(1)의 1이 출력된다.
my(); // console.log(2)의 2가 출력된다.
setup()은 반환된 함수를 감싸고 있기 때문에 클로저를 생성합니다.
클로저는 반환되는 함수에서는 접근할 수 있지만 코드 외부에서는 접근할 수 없기 때문에, 비공개 데이터 저장을 위해 사용될 수 있습니다.
매번 호출될 때마다 값을 증가시키는 카운터(counter)를 예제로 들 수 있습니다.
var setup = function () {
var count = 0;
return function () {
return (count += 1);
};
};
// 사용 방법
var nextCnt = setup();
nextCnt(); // 1을 반환
nextCnt(); // 2을 반환
nextCnt(); // 3을 반환
자기 자신을 정의하는 함수
함수는 동적으로 정의할 수 있고 변수에 할당할 수 있습니다.
새로운 함수를 만들어서 이미 다른 함수를 가지고 있는 변수에 할당한다면, 새로운 함수가 이전 함수를 덮어쓰게 될 것입니다.
어떤 면에서는 이전의 함수 참조가 새로운 함수를 가리키도록 재사용하는 것입니다.
이러한 일을 이전 함수의 본문 내에서 할 수도 있습니다. 이경우 함수는 자기 자신을 새로운 구현으로 덮어쓰고 재정의하게 됩니다.
간단한 예제를 통해 살펴보도록 합니다.
var scareMe = function () {
console.log('Boo!');
scareMe = function () {
console.log('Double Boo!!');
};
};
// 자기 자신을 정의하는 함수를 사용
scareMe(); // Boo!
scareMe(); // Double Boo!!
이 패턴은 함수가 어떤 초기화 준비 작업을 단 한번만 수행할 경우에 유용할 수 있습니다.
불필요한 작업을 반복할 이유가 없기 때문에 함수의 일부는 더 이상 쓸모가 없습니다. 이런 경우 자기 자신을 재정의하여 구현 내용을 갱신할 수 있습니다.
간단히 말해서 재정의된 함수의 작업량이 적기 때문에 이 패턴은 애플리케이션 성능에 확실히 도움이 될 수 있습니다.
이 패턴은 '게으른 함수 선언'(lazy function definition)이라고도 불리는데 그 이유는 최초 사용 시점 전까지 함수를 완전히 정의하지 않고 있다가 호출된 이후에는 더 게을러져서 더 적게 일하기 때문이다.
이 패턴의 단점은 자기 자신을 재정의한 이후에는 이전에 원본 함수에 추가했던 프로퍼티들을 모두 찾을 수 없게 된다는 점입니다.
또한 함수가 다른 이름으로 사용된다면, 예를 들어 다른 변수에 할당되거나, 객체의 메서드로써 사용되면 재정의된 부분이 아니라 원본 함수의 본문이 실행됩니다.
scareMe() 함수를 다음과 같이 일급 객체로 사용하는 예를 살펴봅니다.
- 새로운 프로퍼티가 추가된다.
- 함수 객체가 새로운 변수에 할당된다.
- 함수는 메서드로써 사용된다.
var scareMe = function () {
console.log('Boo!');
scareMe = function () {
console.log('Double Boo!!');
};
};
// 1. 새로운 프로퍼티를 추가한다.
scareMe.property = 'property';
// 2. 다른 이름으로 할당한다.
var prank = scareMe;
// 3. 메서드로 사용한다.
var spooky = {
boo : scareMe
};
// 새로운 이름으로 호출한다.
prank(); // Boo!
prank(); // Boo!
console.log(prank.property); // property
// 메서드로 호출한다.
spooky.boo(); // Boo!
spooky.boo(); // Boo!
console.log(spooky.boo.property);
// 자기 자신을 재정의한 함수를 사용한다.
scareMe(); // Double Boo!!
scareMe(); // Double Boo!!
console.log(scareMe.property); // undefined
예제에서 보는 것처럼, 함수가 새로운 변수에 할당되면 예상과 달리 자기 자신을 정의하지 않습니다.
prank()가 호출될 때마다 콘솔창에는 'Boo!'가 출력됩니다. 또한 전역 scareMe() 함수를 덮어썼는데도 prank() 자신은 여전히 property 프로퍼티를 포함한 이전의 정의를 참조하고 있습니다.
spooky 객체의 boo() 메서드로 함수가 사용될 때에도 똑같은 일이 일어납니다. 이 모든 호출들은 계속해서 전역 scareMe() 포인터를 덮어씁니다.
따라서 마지막에 전역 scareMe()가 호출되었을 때 비로소 'Double Boo!!'를 출력하도록 갱신된 본문이 처음으로 제대로 실행됩니다.
또한 scareMe.property 도 더이상 참조할 수 없게 됩니다.
즉시 실행 함수
즉시 실행 함수 패턴은 함수가 선언되자마자 실행되도록 하는 문법입니다.
다음 예제를 봅니다.
(function(){
console.log('watch out!');
}());
이 패턴은 사실상 (기명이든 무명이든) 함수 표현식을 생성한 직후 실행시키는 것입니다.
즉시 실행 함수라는 용어는 ECMAScript 표준에서 정의된 용어가 아니지만 짧고 간단하며 이 패턴을 설명하고 논의하는 데 유용합니다.
즉시 실행 함수 패턴은 다음의 부분들로 구성된다.
- 함수를 함수 표현식으로 선언한다. (함수 선언문으로는 동작하지 않는다)
- 함수가 즉시 실행될 수 있도록 마지막에 괄호쌍을 추가한다.
- 전체 함수를 괄호로 감싼다. (함수를 변수에 할당하지 않을 경우에만 필요하다)
다음의 대체 문법 또한 일반적으로 사용되지만 JSLint는 처음의 패턴을 선호합니다.
닫는 괄호의 위치를 주의하기 바랍니다.
(function(){
console.log('watch out!');
})();
이 패턴은 초기화 코드에 유효범위 샌드박스(sandbox)를 제공한다는 점에서 유용합니다.
다음의 일반적인 시나리오를 생각해 보도록 합니다. 페이지 로드가 완료된 후, 이벤트 핸들러를 등록하거나 객체를 생성하는 등의 초기 설정 작업을 해야 합니다.
이 모든 작업은 단 한번만 실행되기 때문에 재사용하기 위해 이름이 지정된 함수를 생성할 필요가 없습니다.
하지만 한편으로는 초기화 단계가 완료될 때까지만 사용할 임시 변수들이 필요합니다. 이 모든 변수를 전역으로 생성하는 것은 좋지 않은 생각입니다.
이럴 경우에 즉시 실행 함수가 필요할 것입니다.
즉시 실행 함수는 모든 코드를 지역 유효범위로 감싸고 어떤 변수도 전역 유효범위로 새어나가지 않게 합니다.
(function(){
var days = ['Sun', 'Mon', 'tue', 'Wed', 'Thu', 'Fri', 'Sat'],
today = new Date(),
msg = 'Today is ' + days[today.getDay()]
+ ', ' + today.getDate();
console.log(msg);
}()); // Today is Fri, 22
만약 이 코드가 즉시 실행 함수로 감싸져 있지 않았다면 days, today, msg 변수는 전역 변수가 되어 초기화 코드 이후에도 남아 있게 될 것입니다.
즉시 실행 함수의 매개변수
즉시 실행 함수에 인자를 전달할 수도 있습니다.
다음의 예제를 봅니다.
// 출력 결과 :
// I met jaehee on Fri Jan 22 2016 15:07:12 GMT+0900 (대한민국 표준시)
(function(who, when){
console.log('I met ' + who + ' on ' + when);
}('jaehee', new Date()));
일반적으로 전역 객체가 즉시 실행 함수의 인자로 전달됩니다.
따라서 즉시 실행 함수 내에서 window를 사용하지 않고도 전역 객체에 접근할 수 있습니다.
이러한 방법을 통해 브라우저 외의 실행 환경에서도 코드를 공통으로 사용할 수 있습니다.
(function(global){
// 전역 객체를 'global' 로 참조
}(this));
일반적으로 즉시 실행 함수에 대한 인자를 너무 많이 전달하지 않는 것이 좋습니다. 코드의 동작을 이해하려고 계속해서 코드의 맨 윗부분과 아랫부분 사이를 오가며 스크롤하기기 부담스럽기 때문입니다.
즉시 실행 함수의 반환 값
다른 함수와 비슷하게, 즉시 실행 함수도 값을 반환할 수 있고 반환된 값은 변수에 할당될 수 있습니다.
var result = (function(){
return 2 + 2;
}());
감싸고 있는 괄호를 생략해서 같은 동작을 구현할 수 있습니다. 즉시 실행 함수의 반환 값을 변수에 할당할 때는 괄호가 필요없기 때문입니다.
첫 번째 괄효쌍을 생략하면 아래와 같은 형태가 됩니다.
var result = function(){
return 2 + 2;
}();
이 문법이 더 간단하지만 약간 오해의 소지가 있습니다. 누군가 코드를 읽을 때 마지막의 ()를 눈여겨 보지 못한다면 이 구문의 결과가 함수를 참조한다고 생각할 수 있을 것입니다.
사실 결과값은 즉시 실행 함수의 반환 값, 즉 이 경우에는 숫자 4를 참조합니다.
동일한 결과를 갖는 또 다른 문법은 다음과 같습니다.
var result = (function(){
return 2 + 2;
})();
이 예제는 즉시 실행 함수의 실행 결과로 원시 데이터 타입인 정수 값을 반환합니다. 원시 데이터 값 이외에도 모든 타입의 값이 가능하고, 새로운 함수를 반환할 수도 있습니다.
이 경우 즉시 실행 함수의 유효범위를 사용해 특정 데이터를 비공개 상태로 저장하고, 반환되는 내부 함수에서만 접근하도록 할 수도 있습니다.
다음 예제를 보면, 즉시 실행 함수가 함수를 반환하고 이 반환 값이 getResult라는 변수에 할당됩니다. 이 함수는 즉시 실행 함수에서 미리 계선하여 클로저에 저장해둔 res라는 값을 반환합니다.
var getResult = (function () {
var res = 2 + 2;
return function () {
return res;
};
}());
즉시 실행 함수는 객체 프로퍼티를 정의할 때에도 사용할 수 있습니다. 어떤 객체의 프로퍼티가 객체의 생명 주기 동안에는 값이 변하지 않고, 처음에 값을 정의할 때는 적절한 계산을 위한 작업이 필요하다고 가정해 봅니다.
그렇다면 이 작업을 즉시 실행함수로 감싼 후, 즉시 실행 함수의 반환 값을 프로퍼티 값으로 할당하면 됩니다.
다음의 예제를 살펴봅니다.
var o = {
message : (function () {
var who = 'me',
what = 'call';
return what + ' ' + who;
}()),
getMsg : function () {
return this.message;
}
};
// 사용 방법
console.log(o.getMsg()); // call me
console.log(o.message); // call me
이 예제에서, o.message는 함수가 아닌 문자열 프로퍼티이지만 값을 정의하려면 함수가 필요합니다. 이 함수는 스크립트가 로딩될 때 실행되어 프로퍼티를 정의합니다.
장점과 사용 방법
즉시 실행 함수 패턴은 폭넓게 사용됩니다. 전역 변수를 남기지 않고 상당량의 작업을 할 수 있게 해줍니다.
선언된 모든 변수를 스스로를 호출(self-invoking)하는 함수의 지역 변수가 되기 때문에 임시 변수가 전역 공간을 어지럽힐까봐 걱정하지 않아도 됩니다.
즉시 실행 함수 패턴은 다른 말로 자기 호출(self-invoking) 또는 자가 실행(self-excuting) 함수라고도 부른다.
그 이유는 함수 자신이 선언됨과 동시에 실행되기 때문이다.
즉시 실행 함수 패턴을 사용해 개별 기능을 독자적인 모듈로 감쌀 수도 있습니다.
페이지가 정적이고 자바스크립트 없이도 잘 동작한다고 가정해 봅니다. 점진적인 개선의 측면에서 약간의 코드를 추가해 페이지에 어느 정도 기능을 추가하려고 합니다.
이 코드(또는 모듈이나 기능)를 즉시 실행 함수로 감싸고 페이지에 추가된 코드가 있을 때와 없을 때 잘 동작하는지 확인합니다.
그러고 나서 더 많은 개선 사항을 추가하거나 제거할 수도 있고 개별로 테스트할 수도 있으며, 사용자가 비활성화할 수 있게 하는 등의 작업을 할 수 있습니다.
다음 템플릿을 활용하면 기능을 단위별로 정의할 수 있습니다. 이것을 module이라고 부른다고 한다면,
// moudule1.js 에서 정의한 module1
(function(){
// 모든 module1 코드...
}());
이 템플릿을 따라 또 다른 모듈도 코딩할 수 있습니다. 그리고 실제 사이트에 코드를 올릴 때, 어떤 기능이 사용될 준비가 되었는지 결정하고 빌드 스크립트를 사용해 해당하는 파일들을 병합하면 될 것입니다.
즉시 객체 초기화
전역 유효범위가 난잡해지지 않도록 보호하는 또 다른 방법을 앞서 언급한 즉시 실행 함수 패턴과 비슷한 즉시 객체 초기화 패턴이 있습니다.
이 패턴은 객체가 생성된 즉시 init() 메서드를 실행해 객체를 사용하도록 합니다.
init() 함수는 모든 초기화 작업을 처리하는 기능으로 구현합니다.
즉시 객체 초기화 패턴의 예제를 살펴봅니다.
({
// 여기에 설정 값(설정 상수)들을 정의할 수 있다.
maxwidth : 600,
maxheight : 400,
// 유틸리티 메서드 또한 정의할 수 있다.
gimmeMax : function () {
return this.maxwidth + ' * ' + this.maxheight;
},
// 초기화
init : function () {
console.log(this.gimmeMax());
// 더 많은 초기화 작업들을 작성할 수 있다.
// ...
}
}).init();
문법적인 면에서 이 패턴은 객체 리터럴을 사용한 일반적인 객체 생성과 똑같이 생각하면 될 것입니다.
객체 리터럴도 괄호(그룹 연산자)로 감싸는데, 이는 자바스크립트 엔진이 중괄호를 코드 블럭이 아니라 객체 리터럴로 인식하도록 지시하는 역할을 합니다.
그런 다음 닫는 괄호에 이어 init() 메서드를 즉시 호출합니다.
객체만 괄호로 감싸는 게 아니라 객체와 init() 호출 전체를 괄호 안에 넣을 수도 있습니다.
다시 말해서 다음과 같이 두 가지로 표현할 수 있습니다.
({...}).init();
({...}.init());
이 패턴의 장점은 즉시 실행 함수 패턴의 장점과 동일합니다. 단 한 번의 초기화 작업을 실행하는 동안 전역 네임스페이스를 보호할 수 있습니다.
코드를 익명 함수로 감싸는 것과 비교하면 이 패턴은 문법적을 신경써야 할 부분이 좀더 많은 것처럼 보일 수도 있습니다.
그러나 초기화 작업이 더 복잡하다면(실제로 자주 그렇다) 전체 초기화 절차를 구조화하는 데 도움이 될 것입니다.
예를 들어 비공개 도우미 함수들을 임시 객체의 프로퍼티로 정의하면, 즉시 실행 함수를 여기저기 흩어 놓고 쓰는 것보다 훨씬 구분하기 쉽습니다.
이 패턴의 단점은 대부분의 자바스크립트 압축 도구가 즉시 실행 함수 패턴에 비해 효과적으로 압축하지 못할 수 있다는 것입니다.
압축 도구의 관점에서는 비공개 프로퍼티와 메서드의 이름은 더 짧게 변경되지 않는 방식이 안전하기 때문입니다.
구글의 클로저 컴파일러의 고급 모드는 즉시 초기화되는 객체의 프로퍼티명을 단축시켜주고 있습니다. 다른 압축 도구를 사용할 시에는 위 패턴의 압축을 지원하는지 확인해 볼 필요가 있을 것입니다.
이 패턴은 주로 일회성 작업에 적합하다. init()이 완료되고 나면 객체에 접근할 수 없다.
init()이 완료된 이후에도 객체의 참조를 유지하고 싶다면 init()의 마지막에 return this;를 추가하면 된다.
함수는 1급 클래스다(문법은 물론 값으로도)
자바스크립트에서 함수는 객체입니다.
다시 말해 함수는 변수, 배열, 객체에 저장될 수 있다는 의미입니다.
또한 함수에 전달될 수도 있고 함수에서 반환될 수도 있습니다.
그리고 함수는 객체이기 때문에 속성도 가지고 있습니다.
이 같은 것들이 바로 자바스크립트의 1급 클래스로서 함수가 가지는 특성입니다.
'JavaScript > Core & 개념ㆍ용어' 카테고리의 다른 글
Function 생성자 및 인스턴스(instance) (3) | 2016.09.29 |
---|---|
JS : 함수 호출 패턴 기초 (0) | 2016.09.29 |
참조타입의 특성 (0) | 2016.09.29 |
참조란 무엇인가? (0) | 2016.09.29 |
값 타입의 데이터와 참조 타입의 데이터 & 프로그램 실행단계 (0) | 2016.09.29 |