업무를 하다가보면 closure 영역을 나도모르게 많이 사용하고 있다는 걸 정리하면서 생각이 들었다. (위젯 define option, setTimeout, IIFE 에서도...) 그런 closure에 대해서 정리를 해보겠다.
1 클로저의 의미 및 원리 이해
2 클로저와 메모리 관리
3 클로저 활용사례
3-1 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
3-2 접근 권한 제어(정보 은닉)
3-3 부분 적용함수
3-4 커링함수
1 클로저의 의미 및 원리 이해
클로저란?
- 함수형 프로그래밍 언어에서 등장하는 특징
- 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수a가 사라지지 않는 현상
- 내부함수를 외부로 전달하는 방법: 함수를 return하는 경우, callback으로 전달
- mdn 설명 A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
외부 함수의 변수를 참조하는 내부 함수(1)
var outer = function() {
var c = 1
var c1
var inner = function() {
return ++c
}
return inner()
}
console.log(outer())
- inner함수 내부에서는 c를 선언하지 않았기 때문에 environmentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 다시 c를 찾는다.
-
outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironmentReference에 저장된 식별자들(c, inner)에 대한 참조를 지운다.
- 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집 대상이된다.
- inner function안에서 closure 영역에 있는 변수는 'c' 하나 뿐이다. c1처럼 변수 선언만 하고 할당하지 않으면 메모리에 올라오지 않는다.
외부 함수의 변수를 참조하는 내부 함수(2)
outer 함수의 실행컨텍스트가 종료된 후 inner 함수를 호출하면?
var outer = function() {
var c = 1
var c1
var inner = function() {
return ++c
}
return inner
}
var executeInnerFunc = outer()
console.log(executeInnerFunc()) // 2
console.log(executeInnerFunc()) // 3
- inner함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없습니다.
- outerEnvironmentReferenece에는 inner함수가 선언된 위치의 LexicalEnvironment가 참조복사
-
inner함수는 outer함수 내부에서 선언됐으므로, outer함수의 Lexicalenvironment가 담김
- 위 단계로 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 그 값인 2를 반환
- inner함수의 실행 컨텍스트가 종료
- executeInnerFunc를 두번째 호출하게 되면 위 단계를 걸쳐 2->3 으로 증가한다.
-
중요
inner함수의 실행시점에는 outer함수는 이미 실생 종료된 상태인데 outer함수의 Lexicalenvironment에 어떻게 접근할 수 있는걸까?
- 가비지 컬렉터의 동작 방식때문
- 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다.
- 언젠가 inner함수의 실행컨텍스트가 활성화 되면 outerEnvironmentReferenece가 outer함수의 Lexicalenvironment를 필요로 할 것이므로 수집 대상에서 제외
- 그래서 inner함수 c 변수에 접근 가능
return 없이도 클로저가 발생하는 다양한 경우
// setInterval/setTimeout
(function() {
var a = 0
var intervalId = null
var inner = function() {
//CallStack inner에서 clsoure scope: a, intervalId
if (++a > 9) {
clearInterval(intervalId)
}
console.log(a)
}
intervalId = setInterval(inner, 1000)
})();
// clouser & eventListener
(function() {
var count = 0
var button = document.createElement("button")
button.innerText = "click"
button.addEventListener("click", function() {
//callback function CallStack 에서 clsoure scope: count
console.log(++count, "times clicked")
})
document.body.appendChild(button)
})();
2 클로저와 메모리 관리
- 객체지향, 함수형 모두를 아우르는 중요한 개념
- 클로저는 메모리 소모가가 있지만 이런 특성을 정확히 이해하고 잘 활용하도록 노력해야한다.
-
closure를 GC가 수거하게 하는 방법
- 3장 예시에 아래 코드 POINT 주석 3개 참고
- 첫번째 예
//1번째 예시 var outer = function() { var c = 1 var c1 var inner = function() { return ++c } inner() } outer() outer = null // POINT: outer실별자의 inner 함수 참조를 끊음
- 두번째 예
//2번째 예시 (function() { var a = 0 var intervalId = null var inner = function() { //CallStack inner에서 clsoure scope: a, intervalId, inner(inner식별자의 함수 참조를 끊기위해 참조) if (++a > 9) { clearInterval(intervalId) inner = null // POINT: inner식별자의 함수 참조를 끊음 } console.log(a) } intervalId = setInterval(inner, 1000) })();
- 세번째 예
//3번째 예시 // clouser&eventListener (function() { var count = 0 var button = document.createElement("button") button.innerText = "click" var clickHandler = function() { //console.log(++count, 'times clicked'); //callback function CallStack 에서 clsoure scope: count, clickHandler(식별자 함수 참조를 끊기위해서 closure영역 clickHandler참조) if (++count > 9) { clickHandler = null //POINT: clickHandler 식별자 함수 참조를 끊음 } } button.addEventListener("click", clickHandler) document.body.appendChild(button) })();
3 클로저 활용사례
3-1 Event Listener(콜백 함수 내부에서 외부 데이터를 사용하고자 할 때)
-
event Listener callback function
var colorList = ["red", "blue", "white"] var $ul = document.createElement("ul") colorList.forEach(function(color) { //(A) var $li = document.createElement("li") $li.innerText = color $li.addEventListener("click", function() { //(B) console.log("your choice: ", color) }) $ul.appendChild($li) }) document.body.appendChild($ul)
- (B)는 color라는 클로저가 있다.
- (A)는 colorList만큼의 '실행 컨텍스트가 활성화 됨'
- (B) outerEnvironmentReferenece가 (A)의 LexicalEnvironment참조
-> (B)함수가 참조할 예정인 변수 color에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능
-
event Listener callback function에 bind 사용
var $ul = document.createElement("ul") var colorList = ["red", "blue", "white"] var consoleColor = function(color) { if (this === window) { //POINT3, POINT2 console.log( "### this is window -> " + this + "/// callback function param: " + color ) console.log("### this: ", this) //Window {} console.log("### color: ", color) //blue console.log("--------------------") } else { //POINT1 console.log( "*** this is HTMLElement -> this Value:" + this + "/// callback function param: " + color ) console.log("*** this: ", this) //<li>blue</li> console.log("*** color: ", color) //MouseEvent console.log("====================") } } colorList.forEach(function(fruit) { var $li = document.createElement("li") $li.innerText = fruit $li.addEventListener("click", consoleColor) //POINT 1: eventListener의 this, 첫번째 파라미터는 각각 "클릭한 dom", 'MouseEvent" 객체들이다. $li.addEventListener("click", consoleColor.bind(this, fruit)) //POINT 2: forEach의 callback function에서 this = "window"/ fruit는 "clousre" $ul.appendChild($li) }) document.body.appendChild($ul) consoleColor(colorList[2]) //POINT 3
- POINT1,2,3의 consoleColor function의 console 확인 필요
- POINT1,2의 this, 첫번째 param을 다시 상기 시켜보자.
-
POINT1
- eventListener의 callback function에서 this: target dom
- eventListener의 callback function에서 첫번째 param: event 객체
-
POINT2 제약사항
- 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점(2번째 param으로 event 객체가 넘어옴)
- 함수 내부에서의 this가 eventListener callback function에서 가르키는것(target dom)과 달라지는 점은 감안해야한다.
-
event Listener에 고차 함수 사용
- 함수형 프로그램밍에서 자주 쓰이는 방식
var $ul = document.createElement("ul") var colorList = ["red", "blue", "white"] var consoleColor = function(color) { return function() { console.log(color) } } colorList.forEach(function(fruit) { var $li = document.createElement("li") $li.innerText = fruit $li.addEventListener("click", consoleColor(fruit)) $ul.appendChild($li) }) document.body.appendChild($ul)
3-2 접근 권한 제어(정보 은닉)
var outer = function() {
var a = 1 //closure
var inner = function() {
return ++a
}
return inner
}
var outer2 = outer()
console.log(outer2())
console.log(outer2())
- outer함수는 외부(전역 스코프)로부터 철저하게 격리
- 외부에서는 외부 공간에 노출돼 있는 outer라는 변수를 통해 outer함수를 실행할 수 있지만
, outer함수 내부에는 어떠한 개입도 할 수 없다. - 외부에서는 오직 outer함수가 return한 정보에만 접근할 수 있다. (== return 값이 외부에 정보를 제공하는 유일한 수단)
3-3 부분 적용함수
var add = function() {
var result = 0
for (var i = 0; i < arguments.length; i++) {
result += arguments[i]
}
return result
}
var addPartial = add.bind(null, 1, 2, 3, 4, 5)
console.log(addPartical()) //15
console.log(addPartial(6, 7, 8, 9, 10)) //55
console.log(addPartical(10)) //25
3-4 커링함수
- 여러 인자를 받은 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성
-
당장 필요한 정보만 받아서 전달하고 또 필요한 ㅈ어보가 들어오면 전달하는 방법으로 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미룬다.
- 이를 함수형 프로그램이에서는 지연실행(lazy execution)이라고 한다.
- 커링은 원하는 시점까지 지연시켜싿가 실행을 하는 것이 요긴하다.
-
currying function eg
var curryingFuncTest = function(func) { debugger return function(a) { debugger return function(b) { // getMaxWith10, getMinWith10 funciton debugger return func(a, b) } } } var getMaxWith10 = curryingFuncTest(Math.max)(10) console.log(getMaxWith10(8)) console.log(getMaxWith10(25)) var getMinWith10 = curryingFuncTest(Math.min)(10) console.log(getMinWith10(8)) console.log(getMinWith10(25))
-
curring function with arrow function
var curryingFuncTestWithArrowFunc = func => a => b => func(a, b) var getMaxWith10WithArrowFunc = curryingFuncTestWithArrowFunc(Math.max)(10) console.log(getMaxWith10WithArrowFunc(8)) console.log(getMaxWith10WithArrowFunc(25))
-
curring function 실제 사용 예
- HTML5의 fetch 함수는 url을 받아 해당 url에 HTTP 요청을 합니다.
- 보통 REST API를 이용할 경우 baseUrl은 몇개로 고정되지만 나머지 path나 id 값은 다양하다.
- 이런 상황에서 버서에 정보를 요청할 필요가 있을 때마다 매번 baseUrl부터 전부 기입해주기 보다는 공통적이 요소는 기억시킨다.
- 그리고 특정한 값(path, id)만 서버 요청을 수행하는 함수를 만들어두는 편이 개발 효율성이나 가독성 측면에서 좋다.
var getInfomation = function(baseUrl) { // 서버에 요청할 주소의 기본 URL return function(path) { // path값 return function(id) { // id return fetch(baseUrl + path + "/" + id) // 실제 서버에 정보 요청 } } } var getInformation = baseUrl => path => id => fetch(baseUrl + path + "/" + id) var imageUrl = "http://imgAddr.com/" // 이미지 타입별 요청 함수 준비 var getImage = getInformation(imageUrl) //http://imgAddr.com/ var getEmoticon = getImage("emoicon") //http://imgAddr.com/emoticon var getIcon = getImage("icon") //http://imgAddr.com/icon //실제 요청 var emoticon1 = getEmoticon(100) //http://imgAddr.com/emoticon/100 var emoticon2 = getEmoticon(200) //http://imgAddr.com/emoticon/200 var icon1 = getIcon(10) //http://imgAddr.com/icon/10 var icon2 = getIcon(20) //http://imgAddr.com/icon/20
3-4 커링함수예 - Redux Middleware 'Logger', 'thunk'
- Flux 아키텍처의 구현체중 하나인 Redux의 미들워어도 커링함수를 적용해서 만들었다.
- 두 미들웨어는 공통적으로 store, next, action순서로 인자를 받는다.
- 이중 store, next(dispatch 의미)는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않는속성
-
action의 경우는 매번 달라진다.
- 그래서 store, enxt값이 결정되면 Redux 내부에서 logger, thunk에 store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게끔 한 것이다.
-
이유로 최근 여러 프레임워크, 라이브러리 등에서 커링을 상당히 광범위하게 사용하고 있다.
// Redux Middleware 'Logger' const logger = store => next => action => { console.log('dispatching', action); console.log('next stage', store.getState()); return next(action); } // Redux Middleware 'thunk'; const thunk = store => next => action => { return typeof action === 'function' ? action(dispatch, store.getState) : next(action); }
참고
- 코어 자바스크립트 - 위키북스
- 인사이드 자바스크립트
- 자바스크립트 완벽 가이드