면접시 대답 :
클로저란 외부함수의 변수에 접근할 수 있는 내부 함수입니다.
함수가 속한 렉시컬 스코프(렉시컬 환경)을 기억하여 함수가 렉시컬 스코프 밖에서 실행될때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻합니다.
이를 이용해 클로저가 소멸될때 까지 상태유지를 하거나,
정보를 은닉할 수 있는 이점이 있습니다.
클로저에 대한 나의 이야기
나는 클로저에 대해 진저리가 난다.
우선 mdn에 나오는 정의인 **클로저**는 함수와 함수가 선언된 어휘적 환경의 조합이다 이게 한국어인지 아랍어인지 이해가 가지 않는다는 점이다. 그래서 그냥 공부하기도 싫었다.
그래도 뭐 어쩌겠어… 해야지..
내가 찾아본 와닿는 정의는
함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능 이다.
왜 와닿았냐면
렉시컬 스코프 란
코드를 작성할때 함수가 어디서 실행되느냐(동적 스코프)가 아닌 어디서 선언되었느냐에 따라
정의되는 스코프라는 것을 이해했기 떄문이다.
우선 렉시컬 스코프에 대해 잠깐 예제를 보자
스코프 : 렉시컬 스코프(정적 스코프) vs 동적 스코프
정적 스코프 : 함수가 어디서 선언되었느냐
function foo() {
let a = 2;
*function bar() {
console.log(a); // 2
}
bar();*
}
foo();
함수 bar()는 렉시컬 스코프 검색 규칙을 통해 바깥 스코프의 변수 a에 접근할 수 있다.
이것을 학술적인 말로 bar()는 foo() 스코프에 대한 클로저를 가진다 라고 말한다.
달리 말하면 bar() 는 foo() 스코프에서 닫힌다. bar()는 중첩되어 foo()안에 존재하기 때문이다.
허나 이는 렉시컬 스코프는 분명하게 볼 수 있으나 클로저란 무시무시한 녀석은 뒤에 숨어있다.
이 클로저의 정체를 여실히 드러낼 코드를 보자.
function foo() {
var a = 2;
function bar() {
console.log(a)
}
return bar;
}
var baz = foo(); --- (1)
baz() // 2 --- (2)
foo()를 실행하여 반환한 값(bar () 함수자체 )를 baz라는 변수에 대입하고
실제로는 baz() 함수를 호출했다.
그런데 bar는 함수가 선언된 렉시컬 스코프(foo 함수 안) 밖에서 실행되었다.
foo()가 실행된 이후 ( —- (1) ) 에는 가비지 컬렉션에 의해 foo()의 내부 스코프가 사라진게 아닌가?
아니다! foo의 내부 스코프는 여전히 사용중이다.
누가? bar() 자신이!
bar()는 foo() 스코프에 대한 렉시컬 스코프 클로저를 가지고, foo()는 bar()가 나중에 참조할 수 있도록 스코프를 살려둔다.
즉, bar()는 여전히 해당 스코프에 대한 참조를 가지는데, 그 참조를 바로 클로저라고 부른다!
클로저가 뭔지 설명할수 없지만 그래도 대충 이해는 됐다고?
자 이번엔 더 이상한 예제를 보자.
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000)
}
위의 식은 결과가 어떻게 나올까? 1, 2, 3, 4, 5 너무 쉽다.
그러면
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000)
}
얘는?
답은 6, 6, 6, 6, 6 이다. 아니 let 이 var 로 바뀌었을뿐인데 왜? 뭐 호이스팅 그런건가?
대충 6이 왜 나왔냐보면 반복문이 끝날 조건이 i = 6이니 6이 나온것 같긴 하지만..
여기서 2가지 질문이 있다.
- var 키워드를 사용한 반복문을 의도한대로 1, 2, 3, 4, 5로 작동시키려면 어떻게 해야하는가
- 왜 let 키워드를 사용한 반복문은 의도한대로 1, 2, 3, 4, 5로 잘 작동하는가
첫번째 질문에 대답해보자.
반복마다 새로운 닫힌 스코프를 만들면 된다. 무슨말이냐고?
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log(j);
}, i * 1000)
})();
}
function() 으로 한번 더 감싸서 닫힌 스코프를 만들어 주고 자체 변수로 처리하면 된다.
두번쨰 질문에 대답해보자.
let 키워드는 본질적으로 하나의 블록을 닫을 수 있는 스코프로 바꾸기 때문이다.
또한 반복문에서 let으로 선언된 변수는 한 번만 선언되는 것이 아니라 반복할때마다 선언이 된다.(왜?)
그러면 이 클로저를 언제 사용할까? 또 예시를 보자.
function CoolModule() {
let something = "cool";
let another = [1, 2, 3, 4];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join("!"));
}
return {
doSomething,
doAnother
}
}
let foo = CoolModule();
foo.doSomething() // "cool"
- CoolModule()은 그저 하나의 함수일 뿐이지만, 모듈 인스턴스를 생성하려면 반드시 한번은 호출해야한다. 최외곽 함수가 실행되지 않으면 내부 스코프와 클로저는 생성되지 않는다.
- CoolModule() 함수는 객체를 반환한다.
해당 객체는 내장 함수들에 대한 참조를 가지지만, 내장 데이터 변수(something, another)에 대한 참조는 가지지 않는다. 즉 은닉화 가 가능하다.
또한 CoolModule() 함수는 모듈 처럼 사용할 수 있다.
function useFoo() {
let foo = 0;
function getFoo() {
return foo;
}
function setFoo(value) {
foo = value;
}
return [getFoo, setFoo];
}
const [getFoo, setFoo] = useFoo();
console.log(getFoo()); // print 0
setFoo(1);
console.log(getFoo()); // print 1
위의 코드는 어디서 많이 본 것 같지 않은가?
리액트의 useState 와 비슷하다.
즉. 리액트의 리액트 훅과 클로저도 관계가 있다.
또한 클로저를 응용해 커링을 사용 할수도 있다.
function ( a, b, c) {
a= 2;
b- 3;
c =4;
return a+b+
function multiply(x) {
return function(y) {
return function(z){
return x*y*z;
}
}
}
let multiply3 = multiply(3); // 3이 고정됨
let multiply3And5 = multiply3(5); // 3과 5가 고정됨
let multiply3And2 = multiply3(2); // 3과 2가 고정됨
console.log(multiply3And5(7));
console.log(multiply3And5(8));
console.log(multiply3And2(1));
함수에 n개의 인자를 일일히 받는 대신, 이렇게 n개의 클로저 함수를 만들어
각각 인자를 받게 하는 방법이다.
'스터디 > JS,Node.js' 카테고리의 다른 글
브라우저 저장소 (쿠키, 로컬 스토리지, 세션 스토리지) (0) | 2023.08.17 |
---|---|
callback, promise, async/await 의 특징과 차이점에 대해서 설명하세요. (0) | 2022.06.19 |
호이스팅과 Temporal Dead Zone이 어떻게 연관되어있는지 설명하세요. (0) | 2022.06.19 |