지금 쯤이면 함수와 함수의 기능에 대해 많이 알고 계실 거라고 생각합니다.
JavaScript의 함수 관련에서 가장 중요한 부분은 아마 클로저라고 생각합니다.
클로저는 함수(function)와 변수 유효 범위(variable scope)가 교차하는 교집합이라고 볼 수 있습니다.
이제는 클로저에 대해 말로만 할게 아니라 이제는 코드를 보면서 설명하겠습니다.
클로저가 무엇인지 설명하기 위해 여기서 더 얘기한들 더 혼란스러울 뿐입니다.
다음 섹션에서는 익숙한 영역에서 시작해서 클로저랑 관계있는 좀더 어려운 영역으로 진행하면서 설명해 보겠습니다.
함수 내 함수
첫 번째로 할 일은 함수 내에 함수가 있고 내부 함수가 반환될 때 어떤 일이 발생하는지 자세히 살펴 보는 것입니다.
일단 먼저 함수의 역할을 다시 한번 되새겨 봅시다.
다음 코드를 한번 보시죠.
function calculateRectangleArea(length, width) {
return length * width;
}
var roomArea = calculateRectangleArea(10, 10);
alert(roomArea);
calculateRectangleArea 함수는 두 개의 인수를 사용하여 해당 인수를 곱한 값을 반환합니다.
위 코드에서 calculateRectangleArea를 호출하는 역할은 변수 roomArea에 의해 제어됩니다.
이 코드가 실행 된 후 roomArea 변수에는 10과 10을 곱한 결과가 저장됩니다.
아시다시피 함수가 반환하는 것은 무엇이든 될 수 있습니다.
이 경우 숫자를 반환했습니다.
텍스트 (일명 문자열), undefined 값, 사용자 정의 객체 등 거의 모든 걸 쉽게 반환할 수 있습니다.
함수를 호출하는 코드가 호출되는 함수가 뭘 반환하는지 알고, 또 반환값으로 뭘 해야 하는지 안다고 할때, 프로그래머는 무엇이든지 할 수 있습니다.
심지어 반환할 때 다른 함수 자체를 반환 할 수도 있습니다. 좀 더 깊게 알아봅시다.
아래 코드는 방금 말한 얘기의 예제입니다.
function youSayGoodBye() {
alert("Good Bye!");
function andISayHello() {
alert("Hello!");
}
return andISayHello;
}
내부에 함수를 포함하는 함수가 있을 수 있고 만들수 있습니다.
이 예에는 alert 함수와 andISayHello라는 또 다른 함수가 포함 된 youSayGoodBye 함수가 있습니다.
흥미로운 부분은 youSayGoodBye 함수가 호출 될 때 반환하는 것이 andISayHello 함수입니다.
function youSayGoodBye() {
...
return andISayHello;
}
계속해서이 이 예제를 실행 해 보겠습니다.
이 함수를 호출하려면 youSayGoodBye를 가리키는 변수를 만들어야 합니다.
var something = youSayGoodBye();
위 코드가 실행되는 순간 youSayGoodBye 함수 내의 모든 코드도 실행됩니다.
즉, Good Bye!라는 대화 상자 (alert 덕분에)가 표시됩니다.
실행 완료의 일부로 andISayHello 함수가 생성 된 다음 반환됩니다.
이 시점에서 우리의 something 변수는 한 가지에만 눈을 돌립니다. 그 것은 andISayHello 함수입니다.
something 변수의 관점에서 보면 외부 함수인 youSayGoodBye 는 바로 사라집니다.
something 변수가 이제 함수를 가리 키기 때문에 일반적으로 함수를 호출하는 것처럼 열기와 닫기 괄호를 사용하여 이 함수를 호출 할 수 있습니다.
var something = youSayGoodBye();
something();
이렇게 하면 반환 된 내부 함수 (일명 andISayHello)가 실행됩니다.
이전과 마찬가지로 대화 상자가 표시되지만 이 대화 상자에는 Hello! 라고 내부 함수 내에서 지정한 alert 함수의 문구가 표시됩니다.
일단은 그 함수가 뭘 리턴한다는 것에 의미를 둘 필요가 없고 단지 뭘 반환했는지 즉, 반환 된 값만 중요하게 생각해야 합니다.
여기까지는 어렵지 않지만 좀더 어려운 클로저 쪽으로 가까이 가고 있습니다.
다음 섹션에서는 예제를 약간 비틀어 좀더 깊게 들어가 보겠습니다.
내부 함수가 독립적이 않을 때
이전 예제에서 andISayHello 내부 함수는 자체적으로 독립적이었으며, 외부 함수의 변수 또는 상태에 의존하지 않았습니다.
function youSayGoodBye() {
alert("Good Bye!");
function andISayHello() {
alert("Hello!");
}
return andISayHello;
}
많은 실제 시나리오에서는 이와 같은 경우는 거의 발생하지 않습니다.
외부 함수와 내부 함수간에 공유되는 변수와 데이터가 종종 있습니다.
그럼 다음 코드를 살펴보십시오.
function stopWatch() {
var startTime = Date.now();
function getDelay() {
var elapsedTime = Date.now() - startTime;
alert(elapsedTime);
}
return getDelay;
}
위 코드는 무언가를 수행할 때 시간을 재는 가장 기본적인 방식의 코드입니다.
stopWatch 함수의 내부에는 startTime 이라는 변수가 있고 그 값은 Date.now() 라는 현재 시간으로 초기화되었습니다.
function stopWatch() {
var startTime = Date.now();
...
}
또한 getDelay라는 내부 함수도 있습니다.
function stopWatch() {
...
function getDelay() {
var elapsedTime = Date.now() - startTime;
alert(elapsedTime);
}
...
}
getDelay 함수는 앞에 선언된 startTime과 getDelay 함수가 실행할 때의 시간과의 차이를 alert 함수로 보여주는 내부 함수입니다.
stopWatch 라는 외부 함수가 마지막으로 하는 일은 종료하기 전에 getDelay 함수를 반환하는 것입니다.
보시다시피 위 코드는 이전 예제와 매우 유사합니다.
외부 함수도 있고 내부 함수도 있습니다.
그리고 내부 함수를 반환하는 외부 함수가 있습니다.
이제 stopWatch 함수가 작동하는지 확인하려면 다음 코드를 실행해 봅시다.
var timer = stopWatch();
// do something that takes some time
for (var i = 0; i < 1000000; i++) {
var foo = Math.random() * 10000;
}
// invoke the returned function
timer();
최종 HTML 코드는 아래와 같습니다.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Closures</title>
<style>
</style>
</head>
<body>
<script>
function stopWatch() {
var startTime = Date.now();
function getDelay() {
var elapsedTime = Date.now() - startTime;
alert(elapsedTime);
}
return getDelay;
}
var timer = stopWatch();
// do something that takes some time
for (var i = 0; i < 1000000; i++) {
var foo = Math.random() * 10000;
}
// invoke the returned function
timer();
</script>
</body>
</html>
이 예제를 실행하면 타이머 변수가 초기화되고, for 루프가 실행되고, 타이머 변수가 함수로 호출되기까지 걸린 시간 (밀리 초)을 표시하는 대화 상자가 표시됩니다.
위 코드는 요약해서 쉽게 설명해 보면, 여기에 우리가 호출한 스톱워치가 있고, 그 스톱워치로 무언가 시간이 오래 걸리는 작업을 한 다음 다시 호출하여 그 작업이 걸린 시간을 확인한 겁니다.
우리의 스톱워치 예제가 잘 작동하는 것을 봤으므로, stopWatch 함수로 돌아가서 정확히 무슨 일이 일어나는지 살펴 보겠습니다.
일단은 우리가 위에서 살펴본 youSayGoodBye / andISayHello 예제와 유사합니다.
이 예제를 다르게 만드는 약간의 트윅이 있으며, 주목해야 할 중요한 부분은 getDelay 함수가 timer 변수로 반환 될 때 무슨 일이 발생하는지 입니다.
아래 그림은 약간 부족하지만 무엇이 일어나는지 보여줍니다.
stopWatch 외부 함수는 더 이상 작동하지 않으며 timer 변수는 getDelay 함수에 바인딩됩니다.
자, 여기에 트윅이 있습니다.
getDelay 함수는 외부 stopWatch 함수의 컨텍스트에 있는 startTime 변수에 의존합니다.
function stopWatch() {
var startTime = Date.now(); // stopWatch 컨텍스트에 있는 startTime 변수
function getDelay() {
var elapsedTime = Date.now() - startTime; // 외부 함수에 있는 startTime 변수 액세스
alert(elapsedTime);
}
return getDelay;
}
getDelay가 timer 변수로 반환 될 때 외부 stopWatch 함수가 사라지면 다음 줄에서 어떤 일이 발생할까요?
function getDelay() {
var elapsedTime = Date.now() - startTime; // 여기서만 볼때는 startTime은 undefined?
alert(elapsedTime);
}
getDelay 함수 내부에서만 볼때 지금까지 배운대로는 startTime 변수가 실제로 정의되지 않은 undefined 이어야 하지 않나요?
그런데 위의 전체 코드는 잘 작동하고 뭔가 다른것이 무언가가 일어나고 있는 거 같습니다.
자바스크립트에서는 그 다른 것을 샤이(shy)하고 미스테리한 클로저라고 합니다.
이제 startTime 변수가 undefined 값이 아니고 왜 실제적으로 값을 저장하는지 알아 볼 차례입니다.
자바스크립트의 런타임은 모든 변수, 메모리 사용, 참조 등 모든 것을 추적합니다.
그 정도로 JavaScript 런타임은 영리합니다. 우리가 보고 있는 이 예제에서는 자바스크립트 런타임은 내부 함수 (getDelay)가 외부 함수 (stopWatch)의 변수에 의존하고 있음을 감지합니다.
이 경우 런타임은 외부 함수가 사라지더라도 내부 함수에서 필요한 외부 함수의 모든 변수를 계속 사용할 수 있도록합니다.
위 내용을 올바르게 시각화하기 위해 다음은 timer 변수의 메모리 모형화 그림입니다.
여전히 getDelay 함수를 참조하고 있지만 getDelay 함수는 외부 stopWatch 함수에 있던 startTime 변수에도 액세스 할 수 있습니다.
이 내부 함수는 외부 함수의 관련 변수를 버블 (일명 범위)로 묶었기 때문에 클로저라고 합니다.
클로저를 보다 공식적으로 정의해 보면, 변수 컨텍스트(variable context)도 포함하고 있는 새로 생성 된 함수입니다.
클로저를 다시 한번 리뷰해 보면, startTime 변수는 stopWatch 함수가 실행되고 timer 변수가 초기화 될 때의 그 시간을 Date.now() 함수를 통해 저장하는 변수입니다.
그리고 stopWatch 함수가 내부 함수 getDelay 를 반환할 때, 외부 함수 stopWatch 함수는 사라집니다.
그런데 여기서 사라지지 않는 것은 stopWatch 내부에 있는 공유된 변수 즉 내부함수 getDelay가 의존하는 변수입니다.
이 공유된 변수는 파괴되지 않습니다.
대신, 그 공유 변수는 클러저라는 내부함수를 둘러싸고 있습니다.
클로저 요약해보기
먼저 예제를 통해 클로저를 살펴보면서 지루하고 따분한 이론, 어려운 의미 등 별로 마음에 들지 않을 겁니다.
그러나 진지하게 말하면 JavaScript에서 클로저는 매우 일반적이며 중요합니다.
우리는 여러 가지 미묘하고 미묘하지 않은 방식으로 클로저를 자주 만날 것입니다.
이 모든 것을 제거 할 수있는 것이 있다면 다음 사항을 기억하십시오.
클로저가 하는 가장 중요한 것은 환경이 급격히 변하거나 사라지더라도 함수가 계속 작동하도록 하는 것입니다.
함수가 생성 될 때 범위에 있던 모든 변수는 함수가 계속 작동하도록 하기 위해 포함되고 보호됩니다.
이 동작은 자주 즉석에서 생성, 수정 및 파괴하는 JavaScript와 같은 매우 동적인 언어에 필수적입니다.
지금까지 어려운 클로저를 살펴봤습니다.
다음편에서 뵐게요.
'코딩 > Javascript' 카테고리의 다른 글
[JS-기초편] 11.콘솔 로그 (0) | 2020.12.19 |
---|---|
[JS-기초편] 10.코드 위치 (0) | 2020.12.19 |
[JS-기초편] 8.변수 유효 범위(variable scope) (0) | 2020.12.16 |
[JS-기초편] 7.타이머(Timers) (0) | 2020.12.15 |
[JS-기초편] 6.주석 달기(comments) (0) | 2020.12.14 |