2022-05-30 클로저에 대해

정종훈·2022년 5월 30일
0

T.I.L

목록 보기
17/20

클로져에 대해서 설명하세요.

면접시 대답 :

클로저란 외부함수의 변수에 접근할 수 있는 내부 함수입니다.

함수가 속한 렉시컬 스코프(렉시컬 환경)을 기억하여 함수가 렉시컬 스코프 밖에서 실행될때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻합니다.

이를 이용해 클로저가 소멸될때 까지 상태유지를 하거나,

정보를 은닉할 수 있는 이점이 있습니다.

클로저에 대한 나의 이야기

나는 클로저에 대해 진저리가 난다.

우선 mdn에 나오는 정의인 `클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다` 이게 한국어인지 아랍어인지 이해가 가지 않는다는 점이다. 그래서 그냥 공부하기도 싫었다.

그래도 뭐 어쩌겠어… 해야지..

내가 찾아본 와닿는 정의는

함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능 이다.

왜 와닿았냐면

렉시컬 스코프

코드를 작성할때 함수가 어디서 실행되느냐(동적 스코프)가 아닌 어디서 선언되었느냐에 따라

정의되는 스코프라는 것을 이해했기 떄문이다.

우선 렉시컬 스코프에 대해 잠깐 예제를 보자

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 이다. 아니 letvar 로 바뀌었을뿐인데 왜? 뭐 호이스팅 그런건가?

대충 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"
  1. CoolModule()은 그저 하나의 함수일 뿐이지만, 모듈 인스턴스를 생성하려면 반드시 한번은 호출해야한다. 최외곽 함수가 실행되지 않으면 내부 스코프와 클로저는 생성되지 않는다.

  2. CoolModule() 함수는 객체를 반환한다.

해당 객체는 내장 함수들에 대한 참조를 가지지만, 내장 데이터 변수(something, another)에 대한 참조는 가지지 않는다. 즉 은닉화 가 가능하다.

또한 CoolModule() 함수는 모듈 처럼 사용할 수 있다.

클로저를 응용해 커링을 사용 할수도 있다.

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개의 클로저 함수를 만들어

각각 인자를 받게 하는 방법이다.

참조:

  • 면접시 대답

https://velog.io/@rat8397/JS-개념정리-클로저

  • 클로저에 대한 나의 이야기

https://velog.io/@rat8397/JS-개념정리-클로저

https://meetup.toast.com/posts/90

카밀 심슨, you don't know js, 한빛미디어, 239-252

커링 : https://velog.io/@nittre/JavaScript-Closure와-Currying

profile
괴발개발자에서 개발자로 향해보자

0개의 댓글