자바스크립트의 특별한 성질인, 클로저(Closure)가 드디어 나왔습니다. 클로저는 자바스크립트에서 많은 역할을 수행하며, 함수가 객체라는 성질과 객체의 유효 범위(스코프) 성질 덕분에 사용 가능합니다.
그렇다면, 유효 범위(Scope)가 무엇인지부터 알아봅시다.
객체의 유효 범위(Scope) ?
객체의 유효 범위란, 선언된 객체가 참조될 수 있는 범위를 의미합니다.
컴퓨터는 유한한 자원(저장 공간)을 지닌 물리적 매체이기 때문에, 모든 객체를 영원히 저장하는 것은 낭비입니다.
따라서 모든 프로그래밍 언어는 메모리를 효율적으로 사용하기 위하여 유효 범위를 가지고 있습니다.
일반적으로 C 프로그래밍으로부터 나온 언어들은 블록 스코프를 갖고 있습니다.
따라서, C의 경우에는 다음과 같은 상황이 있을 수 있습니다.
1 2 3 4 5 6 7 8 | int a = 5; { int a = 10; printf("%d\n", a); // 10을 출력합니다. } printf("%d\n", a); // 5를 출력합니다. | cs |
C 기반 언어에서의 변수는 블록(중괄호 범위 내부 { } ) 유효범위를 갖기 때문에, 위의 3~6 내부 블럭에서의 a는 1번 코드의 a가 아니라 4번 코드의 a를 의미합니다.
반면에, 자바스크립트는 블럭 스코프를 갖고 있지 않습니다.
위와 같은 코드를 자바스크립트로 실행해봅시다.
1 2 3 4 5 6 7 8 | var a = 5; { var a = 10; console.log(a); } console.log(a); | cs |
Repl.it에서 실행한 결과, 둘 다 10을 출력하는 것을 볼 수 있습니다.
이는 유효 범위가 블럭이 아니기 때문에 일어나는 일이며, C 기반 언어들에 익숙한 프로그래머가 자바스크립트 코딩을 할 때에 흔히 혼란스러워 하고 버그를 발생시키는 원인이 됩니다.
자바스크립트에서 유효 범위는 함수 유효범위를 가집니다. (중요!)
1 2 3 4 5 6 7 8 | var a = 5; var foo = function(){ var a = 10; console.log(a); } foo(); console.log(a); | cs |
foo라는 함수 내부에 선언된 a는, 함수를 벗어나면 유효 범위에서 벗어나기 때문에 효력을 상실합니다.
따라서 5번 코드는 10을 출력하고, 8번 코드는 5를 출력하게 됩니다.
(블럭 유효범위를 위하여 ECMAScript 6에서는 let 개념을 도입하였으나, 이는 추후 포스팅에서 참고하도록 하겠습니다.)
이러한 점 때문에, 클로저가 생겼고 유용하게 사용되고 있습니다.
클로저(Closure)란 무엇인가?
프로그래밍을 할 때에, 전역변수를 많이 사용하는 것은 좋지 않은 습관입니다.
컴파일 언어일 경우에는, 전역변수는 프로그램의 크기를 키우는데 역할을 하며, 효율적인 변수 활용에 방해가 될 수 있습니다.
자바스크립트 또한 전역변수의 사용을 지양하여야 하는데, 일반적인 함수 언어와 다르게 유효 범위가 함수 스코프이기 때문에 다른 방식으로 지양하고 있습니다.
클로저란, 변수를 지닌 함수를 의미합니다.
1 2 3 4 5 6 7 8 9 10 | function closure() { var name = "yangd"; function introduce() { console.log("Hi, I'm "+ name); } return introduce; } var myFunc = closure(); myFunc(); | cs |
위의 closure() 함수를 보면, 내부에 name이라는 문자열 객체를 지니고 있고, introduce라는 함수 객체를 지니고 있습니다.
이 함수 객체를 리턴해주는 것이 closure 객체의 역할입니다.
9번 코드의 myFunc = closure(); 코드를 통하여, myFunc는 introduce를 실행시킬 수 있는 함수 객체가 되는데, 이 introduce는 closure 함수 객체의 일부이기 때문에, 함수 내부의 name을 유지시키게 됩니다. (복잡합니다....)
따라서 위의 함수를 실행하면, Hi, I'm yangd 가 출력되게 됩니다.
이를 사용하여, 객체 지향 언어의 3대 속성 중에 하나인 캡슐화(Encapsulation)을 구현할 수 있습니다.
다음과 같은 코드를 활용해 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function blogger() { var name = "yangd"; return { getName: function(){ return name; }, setName: function(newName){ name = newName; }, introduce: function(){ console.log("Hi, I'm "+ name); } } } var a = blogger(); console.log("a is "+ a.getName()); a.setName('newYangd'); a.introduce(); | cs |
blogger는 클로저로 활용됩니다.
내부 객체로 name이라는 문자열 객체가 있고, 반환하는 것은 객체인데, getName, setName, introduce의 3개의 함수 객체를 속성으로 지닌 객체를 반환하게 됩니다.
위의 코드를 실행하면 우측과 같은 실행 결과가 나오게 됩니다.
a는 클로저를 활용한 객체가 되고, a 내부의 name에 접근하기 위해서는 getName, setName, introduce의 함수를 통해서만 접근할 수 있게 됩니다.
따라서, 다른 언어의 private 기능을 클로저를 통해 사용할 수 있게 됩니다.
그러나 이 방식에는 단점이 존재하는데, blogger라는 객체를 생성할 때 마다, getName, setName, introduce라는 함수 객체를 함께 생성하기 때문입니다.
따라서, 여러 blogger를 만들 경우에 각각의 blogger 객체들은 서로 다른 함수 객체들을 가지고 있습니다.
이는 메모리를 효율적으로 사용하지 못하는 방식이기 때문에, prototype을 사용하게 됩니다. (이는 다음 포스트에!)
한가지 더 해보자면, 다음과 같은 코드를 실행해봅시다.
1 2 3 | for(var i = 0;i< 10;i++){ setTimeout(function(){console.log(i)}, 0); } | cs |
자바스크립트에는 setTimeout이라는 타이머 함수가 존재하는데, 위의 코드의 경우에는, 함수가 실행되면 0초 후에(즉시) i를 출력하라는 함수입니다.
i를 증가시키면서 console.log로 출력하기 때문에, 0,1,2,3,4,5,6,7,8,9가 출력될 것 같지만 결과는 10을 10번 출력하게 됩니다.
이는 Timeout이 for문이 다 돌아간 이후에 실행되기 때문인데, 이 때의 i는 마지막 for 반복이 끝난 10을 저장하고 있게 됩니다.
따라서 10을 10번 반복하는 함수가 될 뿐입니다.
이를 해결하기 위해서는, setTimeout이 부르는 함수를 클로저화 시켜서 실행시켜줘야 합니다. 다음 코드를 봅시다.
1 2 3 4 5 | for(var i = 0;i< 10;i++){ setTimeout(function(i){ console.log(i); }(i), 0); } | cs |
이는 익명함수와 클로저를 합쳐서 사용한 코드인데, 이번에 setTimeout이 부르는 함수는 단순히 console.log(i)를 출력하는 함수가 아니라, i를 입력받아서 내부 객체에 저장하고 , 이를 출력하는 함수입니다. 따라서 각각의 함수의 i는 외부에서 부른 i가 아니라 함수 내부에 저장된 i가 되고, 이는 4번째 라인에서 (i)를 통하여 입력받게 됩니다.
따라서 이 함수는 0,1,2,3,4,5,6,7,8,9 의 원하는 출력값을 보여주게 됩니다.
함수 스코프를 잘 이해한다면, 클로저도 어려운 개념이 아닙니다 !