자바스크립트의 함수
자바스크립트는 유연함, 모호함, 이상한, 흥미로움 등이 극적으로 뒤섞인 언어다. 가장 많이 쓰이면서도 가장 많은 불평을 듣는 언어이기도 하고 가장 빠르게 변화하고 있는 언어이기도 하다. 이 글은 그러한 자바스크립트의 많은 요소 중 특히 흥미로운 “함수”에 대한 개인적인 정리다.
# 함수 정의
함수는 다음과 같이 작성한다. 보통의 프로그래밍 언어와 마찮가지로 함수명과 인수 그리고 함수가 실행할 코드 블럭으로 구성된다. return 문으로 값을 반환할 수 있다. 명시적 return 문이 없을 경우 undefined를 반환한다. 따라서 모든 함수는 값을 반환한다.
function foo (x, y) {
var z = x + y;
return z; // 생략시 undefined 반환
}
함수 호출시 자바스크립트는 함수의 인수 전달을 느슨하게 처리하며 호출 자체는 언제나 성공한다. 이는 함수 호출 순간 런타임 오류를 발생시키지 않음을 의미한다.
function foo(x, y) { … }
foo(10, 20); // 성공
foo(10); // 성공
foo(); // 성공
foo(10, 20, 30, 40); // 성공
사양에 명시된 인수가 호출시 전달되지 않을 경우 undefined 값이 할당된다. 이런 느슨한 인수 전달 메커니즘을 이해하고 전달된 인수의 값 검증을 습관화하면 런타임 오류 가능성을 줄일 수 있다.
function(x, y) {
if(x === undefined || y === undefined) {
throw new Error(“인수 x, y 필요”);
}
return x + y;
}
# 가변 인수
함수의 사양보다 많이 전달된 인수는 함수 호출과 함께 유사 배열 arguments 에 담겨 전달된다. arguments 에는 언제나 전달된 모든 인수가 왼쪽부터 오른쪽 순서대로 담겨진다.
function foo(x, y) {
(x === arguments[0]); // true
(y === arguments[1]); // true
return arguments.length;
}
foo(10, 20); // 2
foo(); // 0
foo(10, 20, 30, 40); // 4
함수 호출시 언제나 제공되는 arguments로 매우 유연하게 가변 인수를 처리 할 수 있다.
// 전달된 인수의 합을 반환하는 함수
function add() {
var sum= 0;
for(var i=0; i<arguments.length; i++) {
sum += (typeof arguments[i] === ‘number’) ?
arguments[i] : 0;
}
return sum;
}
add(); // 0
add(10, 20, 30); // 60
add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 55
add(1, 2, ‘3'); // 3
# 객체로서의 함수
자바스크립트에서 원시 데이타 타입을 제외한 거의 모든 것은 객체다. 함수 또한 객체이며 객체와 같이 속성과 메소드를 가질 수 있다.
// 아무것도 하지 않는 함수
function empty(x, y) { }
console.log(empty.length); // 2
아무것도 하지 않는 함수 empty 는 인수 x, y를 가지며 아무일도 하지 않는다. 호출되면 undefined 를 반환하는 쓸모없는 함수다. 그런데 함수 호출이 아닌 empty.length 는 무엇일까? empty 함수의 속성 length 에 접근한다. 함수의 length 속성은 함수 인수 사양의 인수 갯수를 가진다. 함수는 length 속성 외에도 apply, call, bind, name, prototype, toString 등 다양한 속성과 메소드를 가지고있는 완벽한 객체다. 자바스크립트의 함수는 호출되거나 인스턴스를 만들거나 하는 특별한 기능을 가진 객체임을 알 수 있다.
# 함수의 속성 및 메소드
원한다면 언제든 함수에 속성을 추가할 수 있다.
function foo() { }
foo.location = ‘seoul’;
console.log(foo.location); // seoul;
속성을 추가할 수 있는 것을 이용하여 단 한번만 실행되는 신기하지만 쓸데없는 함수를 만들어 보자. 아래 double 함수는 단 한번만 자신이 할 일을 수행하고 그 이후에 모든 호출시 할 일을 하지 않는다.
// 단 한번만 할 일을 하는 게으른 함수 double
function double(x) {
if(!double.isCall) {
double.isCall = true;
} else {
return undefined;
}
return x * x;
}
double(10); // 100
double(20); // undefined
double(30); // undefined
좀더 파괴적인(?) 방법으로 단 한번만 할 일을 하는 게으른 double 함수를 바꿔보자.
function double(x) {
double = function() {};
return x * x;
}
double(10); // 100
double(20); // undefined
double(30); // undefined
호출되는 순간 double 함수를 재정의한 후 반환한다. 이 쓸모없는 예제는 함수가 스스로를 재정의 할 수 있는 유연성을 제공한다는 것 하나 만큼은 확실히 알려준다.
이제 조금 쓸모있을 것 같은 예제를 하나 보자. 몇 번 호출됐는지 스스로 기억하는 함수다.
function double(x) {
if(!double.callCount) {
double.callCount = 1;
} else {
double.callCount++;
}
return x * x;
}
for(var i=1; i<=100; i++) {
double(i);
}
console.log(double.callCount); // 100
# 함수에 포함된 함수
자바스크립트의 함수는 함수 또한 포함할 수 있다. 아래 코드와 같이 foo 함수 안쪽의 get 함수는 foo 함수 내에서 사용될 수 있다.
// 함수 get을 포함하고 있는 foo 함수
function foo() {
function get() {
return 100;
}
return get();
}
foo(); // 100
# 인스턴스 생성
그렇다면 foo 함수 내의 get 함수를 외부에서 호출할 수 있을까?
foo.get(); // undefined is not a function
접근할 수 없으며 런타임 오류가 발생한다. 보통의 OOP 언어와 달리 자바스크립트는 객체의 청사진인 클래스를 제공하지 않는다. 그러나 함수가 클래스와 유사한 역할을 제공하며 함수로부터 인스턴스를 생성할 수 있다. 이를 위하 자바스크립트는 new 구문을 제공한다.
function Foo(x) {
this.x = x;
this.getX = function () {
return this.x;
};
}
var foo = new Foo(10);
foo.getX(); // 10
new 와 함께 호출된 함수는 보통의 함수 호출과는 약간 다르게 작동된다. 호출된 이후 함수는 완료되며 암묵적으로 만들어진 함수의 인스턴스를 반환한다. 클래스와 유사한 목적으로 설계된 함수는 보통의 함수와 구분하기 위해 이름을 대문자로 시작하는 관례를 따른다.
# this
함수 안쪽에서 만들어진 지역 변수들은 함수가 종료되면 함께 삭제된다. 다음의 예제에서 두 함수 foo1 과 foo2 의 차이점을 눈여겨보자.
function Foo1() {
var x = 10;
}
function Foo2() {
this.x = 10;
}
var foo1 = new Foo1();
var foo2 = new Foo2();
foo1.x; // undefined
foo2.x; // 10
Foo1 안쪽의 지역 변수 x 는 인스턴스 생성 시간 동안 존재하며 반환과 함께 사라진다. 이는 함수 호출 동작 방식과 동일하다. 당연히 생성된 인스턴스 foo1 에선 참조할 수 없다. 이미 제거되었기 때문이다. 반면 Foo2의 인스턴스 foo2 에선 x 를 참조할 수 있다. 자바스크립트가 제공하는 인스턴스의 실행 컨텍스트를 가르키는 this 에 생성되었기 때문이다.
컨텍스트에 작성된 값들은 컨텍스트의 라이프 사이클 동안 유지된다. 따라서 Foo2 의 인스턴스 foo2가 메모리에서 제거되기 전까지 x 값은 유지되며 이것은 보통의 마치 public 멤버 변수와 유사하게 작동된다.
# 메소드
함수의 멤버 함수 즉, 메소드도 같은 원리로 this 에 추가되어야 인스턴스에서 사용할 수 있다.
function Foo() {
this.getX = function() {
return 10;
};
function getY() {
return 20;
};
}
var foo = new Foo();
foo.getX(); // 10
foo.getY(); // Error!
this 에 추가된 멤버 함수 getX 를 인스턴스에서 호출하고 참조할 수 있다는 것을 알 수 있다. 하지만 this에 추가되지 않은 Foo 함수의 안쪽 함수 getY 는 인스턴스에서 참조시 런타임 오류가 발생한다. 지역 변수와 마찮가지로 생성 시점에 함수 안쪽에서만 존재하며 반환과 함께 사라지기 때문이다.
# prototype
함수에 포함된 속성 중 prototype 이라는 특수한 객체가 있다. 함수의 prototype 에 동적으로 속성이나 메소드를 추가할 수 있는데 추가의 영향력은 함수의 모든 인스턴스에 즉시 반영된다. 다음 코드를 보자.
function Foo(x) {
this.x = x;
}
var foo1 = new Foo(10);
var foo2 = new Foo(20);
foo1.x; // 10
foo2.x; // 20
foo1.y = 100;
foo1.y; // 100
foo2.y; // undefined
Foo.prototype.z = 1000;
foo1.z; // 1000
foo2.z; // 1000
객체의 인스턴스에 동적으로 추가된 속성은 만들어진 인스턴스에서만 존재하며 prototype 으로 추가된 속성은 이미 만들어진 인스턴스 모두에 실시간으로 반영되는 것을 알 수 있다.
함수에 직접 추가된 속성은 함수의 생성자로 만들어진 인스턴스에서는 사용할 수 없음을 주의해야한다. 생성자가 만들어낸 인스턴스에서 재새용될 메소드나 속성은 반드시 prototype에 추가한다.
# 다양한 함수 호출 방식
지금까지 보아왔던 것 처럼 함수를 만들고 호출하는 방법은 간단한다.
function foo() { };
foo(); // 호출!
자바스크립트는 몇 가지의 다른 함수 호출 방법을 제공하며 그것은 실행 컨텍스트인 this와 밀접한 관련이 있다.
# call
자바스크립트의 모든 함수는 call, apply 라는 메소드를 가지고 있다. 이 두개의 메소드는 함수를 조금 다른 방식으로 호출할 수 있도록 한다.
function foo(x) { };
foo(10); // 호출
foo.call(this, 10); // 호출
foo.apply(this, [10]); // 호출
함수 foo 를 호출하는 세 가지 방식 모두 결과는 동일한다. 다른 점은 call과 apply의 첫 번째 인수 this 가 함수에게 전달되는 방식이다. 보통의 OOP 언어와 달리 자바스크립트의 this 는 객체 자체를 가르키는 지시어가 아니다. 지금까지 보아왔던 this 는 마치 그렇게 보였을 뿐이다. 함수가 호출될 때 함수에게 전달되는 모든 인수를 담은 arguments 유사 배열과 함께 this 도 암시적으로 전달된다. this 는 함수를 호출한 컨텍스트를 가르키며 전역 공간에서 호출될 땐 전역 객체를 가르킨다. 브라우저 환경이라면 window 객체와 같다. 객체의 메소드로서 함수가 호출되면 this 는 객체를 가르킨다. 다음 예제를 보자.
var person = {
age: 10,
addAge: function(n) {
this.age += n;
}
};
person.addAge(1);
console.log(person.age); // 11
객체 person의 메소드인 addAge가 호출되었다. addAge의 this는 암묵적으로 함수 호출시 전달된 객체이며 객체의 메소드인 경우 객체 그 자체가 된다. 이렇게 복잡하게 설명하는 이유는 뭘까? this 는 person이다 라고 설명해도 전혀 문제 없지 않은가? 맞다. 하지만 자바스크립트는 this가 지시하고 있는 대상을 변경할 수 있는 유연성을 제공한다. 다음 코드를 보자.
var person = {
age: 10,
addAge: function(n) {
this.age += n;
}
};
var monkey = {
age: 1
};
person.addAge.call(monkey, 1);
console.log(person.age); // 10;
console.log(monkey.age); // 2
자바스크립트의 함수라면 모두 가지고 있는 call 메소드로 addAge 함수를 호출했다. 첫 번째 인수는 this 가 아닌 monkey 객체를 넘겨줬다. 무슨일이 일어날까? addAge 함수의 코드는 변경된 것이 없다. 단지 호출 방식을 달리했을 뿐이다. 결과적으로 person 객체의 age 는 변경되지 않고 monkey 객체의 age 가 1 증가하여 2가 되었다. 객체 내부에서 사용하는 this 가 호출시점의 실행 컨텍스트를 가르키며 이는 함수가 어떻게 호출되는가에 따라 this 가 달라질 수 있다는 점을 항상 고려해야한다는 것을 의미한다. this 의 유연함이 가져다 주는 여러가지 장점과 혼란스러운 상황은 함수의 다른 면을 설명하면서 좀 더 다루어 보도록 하자.
# apply
call 과 똑같이 동작하는 apply는 두번째 인수. 즉, 호출할 함수에게 전달할 인수 목록을 배열로 받는다. 호출할 함수가 가변 인수를 받는 함수라면 배열을 전달하는 apply를 사용하면 손쉽게 처리할 수 있다.
# bind
ECMAScript 5 스펙에 추가된 bind 메소드는 함수와 객체를 연결시킨다.
# 1급 함수
자바스크립트는 함수도 변수에 할당할 수 있다. 변수에 담긴 함수는 당연하게도 함수의 인수로 전달될 수 있으며 함수의 반환값이 될 수도 있다. 변수에 할당된 함수의 호출은 일반적인 함수의 호출과 같은 방식으로 작동한다. 대입문으로 생성된 함수는 대입문의 오른쪽에 위치하며 이는 곧 수식으로 평가됨을 의미한다. 따라서 함수의 마지막은 구문 종료 문자 세미콜론이 되어야한다.
// 변수에 할당한 함수
var foo = function foo() { … };
// 변수에 할당된 foo 함수 호출.
foo();
# 이름 없는 익명 함수
자바스크립트에서 함수의 이름의 유무는 선택이다. 함수의 이름이 필요 없다면 기술하지 않아도 된다. 아래 코드는 이름 없는 함수다.
// 익명 함수 생성 후 foo 변수에 할당
var foo = function(x) {
return x * x;
};
foo(10); // 100
이름 없는 익명 함수를 참조하고 있는 foo 를 이용하여 마치 foo 라는 함수를 호출하는 것과 같은 방식으로 익명 함수를 호출할 수 있다. 함수는 length 속성 이외에 name 속성도 제공한다. name 속성은 함수의 이름을 담고 있다.
var foo1 = function orgFoo() { };
var foo2 = function() { };
console.log(foo1.name); // orgFoo
console.log(foo2.name); // 빈문자열
위 코드를 보면 익명 함수는 정말로 이름이 없다는 것을 알 수 있다. 익명 함수는 할당된 변수를 통해서만 호출될 수 있다. 익명 함수는 객체의 메소드를 만들 때 많이 사용된다.
var foo = {
age: 10,
addAge: function() {
this.age++;
}
};
위 코드는 객체 foo를 생성하며 addAge 메소드를 작성하며 익명 함수를 사용한다. 이 경우 addAge 속성이 존재함으로 함수의 이름이 불필요하기 때문에 이름이 없는 익명 함수를 사용한다.
# 즉시 실행 함수
간단하게 익명 함수를 생성과 동시에 즉시 실행시킬 수 있다.
(function() {
…
})();
이런 형태의 즉시 실행 익명 함수는 단 한번만 수행되며 다시 사용할 수 있는 참조가 없기 때문에 다시는 실행되지 않는다. 단 한번만 수행되는 초기화 작업 등, 전역 공간의 오염 없이 수행하기 위한 방법으로 많이 사용된다.
# 지역 유효 범위 생성자로서의 함수
자바스크립트의 함수는 지역 유효 범위를 만들어낸다. 함수가 생성한 지역 스코프에서 생성된 변수들은 모두 지역 변수가 되어 함수의 라이프 사이클과 함께 유지된다. 함수 내에서 var 구문 없이 생성된 변수는 전역 변수로 생성되며 지역 스코프에 존재하지 않고 함수의 수행이 종료된 후에도 전역 스코프에 잔류한다. 이것이 의도된 설계가 아니라면 메모리는 낭비된다.
function foo(x) {
sum = x * x;
}
foo(10);
console.log(sum); // 100
함수 안쪽의 함수 또한 지역 스코프를 만들어 내며 이는 지역 스코프가 중첩될 수 있음을 의미한다. 식별자(변수, 함수 등)를 탐색하기 위한 스코프 탐색 메커니즘에 따라 포함된 스코프는 자신을 포함한 상위 스코프의 변수에 접근할 수 있다.
function foo() {
var x = 10;
function getx() {
return x; // 상위 지역 스코프 변수 x를 참조. 10을 반환한다.
}
return getx();
}
foo(); // 10
# 클로저
자바스크립트는 클로저라는 특별한 메커니즘을 제공한다. 클로저는 함수와 함수가 접근 할 수 있는 주변 환경으로 구성된 특별한 개념이다. 클로저를 이해하기 위해 함수 자체를 반환하는 함수를 만들어보자.
function multiple() {
var x = 10;
return function(y) {
return y * x;
};
}
var mul = multiple();
mul(10); // 100
mul(5); // 50
함수 multiple 는 익명 함수를 반환한다. 반환되는 익명 함수의 바깥 함수 multiple 를 걷어내면 보통의 익명 함수를 할당하는 대입문과 다르지 않다는 것을 알 수 있다.
var mul = function(y) {
return y * x;
};
이 경우 변수 x는 전역 변수다. multiple 함수로 감싼 익명 함수 내의 변수 x는 스코프 체인 메커니즘에 의해 multiple 의 지역 변수 x가 된다. 그런데 multiple 를 호출하고 반환하며 지역 변수 x 는 당연히 소멸되어야한다. 그렇다면 반환된 익명 함수를 호출하여 실행될 때 익명 함수의 안쪽에서 변수 x는 존재할까? 변수 x는 존재하며 그 값은 익명 함수가 반환되는 시점의 x 값인 10이다. 이것을 클로저라한다.
클로저 패턴을 사용하면 새로운 함수를 동적으로 만들어 낼 수 있다.
function multiple(x) {
return function(y) {
return y * x;
};
}
var mul10 = multiple(10);
var mul5 = multiple(5);
var mul100 = multiple(100);
mul5(10); // 50
mul10(10); // 100
mul100(10); // 1000
클로저를 이용하여 입력한 값의 5배, 10배, 100배 값을 반환하는 함수 mul5, mul10, mul100 이 만들어진 것을 알 수 있다. 바깥 함수의 지역 변수에 접근할 수 있는 클로저의 특징을 이용하면 외부에 노출되지 않는 보호된 변수, private 변수를 흉내 낼 수 있다.
function Person(name, age) {
this.getName = function() {
return name;
};
this.getAge = function() {
return age;
};
this.addAge = function() {
age++;
};
}
var person = new Person(‘홍길동’, 5);
person.getName(); // 홍길동
person.getAge(); // 5
person.name; // undefined
person.addAge();
person.getAge(); // 6
# 콜백 함수
자바스크립트의 함수는 1급 함수로서 변수에 할당될 수 있다. 이는 함수를 함수의 인수로 전달할 수 있다는 것을 의미한다. 함수의 호출이 비동기 방식으로 작동되어야 할 때 이 콜백 함수를 이용할 수 있다. 타이머에 의한 함수 호출이 대표적인 경우다.
var count = 1;
function sayHello() {
console.log(“Hello?”, count);
count++;
}
// 첫번째 인수로 함수를 전달한다.
// setTimeout은 호출과 함께 즉시 제어권을 반환하고
// 10000ms 후에 sayHello를 호출한다.
setTimeout(sayHello, 10000);
sayHello(); // “Hello? 1" 출력. 10초 후 “Hello? 2" 출력.
인수로 받은 함수를 콜백 함수라 한다. 특정 시간 이후에 호출되어야 하거나, 네트워크 자원을 호출하여 반환값을 받는 것, 특정 이벤트가 발생했을 때 호출되어야 하는 함수(이벤트 핸들러)와 같이 함수 호출의 시기를 호출하는 시점에 특정할 수 없을 때 콜백 함수 패턴이 유용하게 쓰일 수 있다.
var saveButton = document.getElementById(“save”);
// click 이벤트가 발생했을 때 saveDocument 함수가 호출된다.
saveButton.addEventListener(“click”, saveDocument);
# 콜백 지옥
간단한 비동기 작업을 콜백 패턴으로 작성하는 것은 매우 깔끔하고 괜찮은 아이디어지만 비동기 작업이 중첩되는 상황에선 그리 훌륭한 방식이 아니다. 백앤드 자바스크립트인 node 어플리케이션에서 특히 콜백과 콜백이 중첩되는 상황은 자주 발생한다. 비동기가 중첩된 흐름 제어는 코드의 가독성을 떨어뜨리고 테스트를 어렵게 만든다.
// 3개의 비동기 콜백이 중첩된 코드
ayncJob(function(err, step1Data) {
ayncJobInner(function(err, step2Data) {
ayncJobDeepInner(function(err, step3Data) {
// create step4Data
});
});
});
만약 데이타베이스에 3개의 쿼리를 요청한 후 응답 데이타를 가공하여 4번째 데이타를 생성하고 이를 응답으로 보내야한다면 어떤 일이 발생할까? 3개의 쿼리 모두 비동기로 응답이 돌아오며 이는 콜백 함수로 처리해야 한다는 의미다. 실제 수행할 작업은 순서가 필요한 작업이다. 따라서 3개의 비동기 응답을 모두 받기 전까지 대기해야만 4번째 데이타를 받을 수 있고 이는 비동기 방식의 장점을 희석시킨다. 이 간단한 적업을 위해 상당한 노력의 코드를 작성해야한다. 물론 가독성 높은 코드를 작성하기는 더욱 힘들다.
# 흐름 제어 라이브러리
비동기 콜백과 흐름 제어(병렬 실행, 순차 실행)를 구현하기 위해 중첩된 콜백을 사용하지 않아도 되는 여러가지 대안 라이브러리가 존재한다.
- async https://github.com/caolan/async
- step https://github.com/creationix/step
- q https://github.com/kriskowal/q
- when https://github.com/cujojs/when
- RSVP https://github.com/tildeio/rsvp.js
# Promise & Generator
promise 와 generator는 비동기 흐름 제어 코드를 직관적이고 아름답게(?) 작성할 수 있도록 지원하는 자바스크립트의 네이티브 구현이다. ES 6 스펙으로 구현되었으며 극히 일부의 최신 브라우저와 node 에서 동작된다. (ex: chrome 36+, node —harmony) 자세한 내용은 아래 링크를 참조하자.
JavaScript Promises