본문 바로가기

개념/javaScript

[javaScript] 클로저(closure)란? 어휘적 환경(lexcial environment)이란?

📦클로저란

클로저 = '함수와 함수가 선언된 어휘적 환경(Lexcial Environment)의 조합'  <출처: mdn>

이 말을 완벽하게 이해하기 위해서는 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.


👀어휘적 범위 지정 (Lexical Scoping)

 

사실 '어휘적 범위 지정'이란 말 자체가 굉장히 뭉뚱그려 말하는 추상적인 느낌을 주는데, 그게 맞다. 

이는 '어휘적 범위 지정'이라는게 'a의 경우 b가 된다.' 식의 정형화된 규칙이 아니라, 일종의 자바스크립트가 그때그때 변수를 읽는 방식을 퉁쳐서 부르는 표현이기 때문이다.

 

어휘적 범위 지정(lexical scoping)의 "lexical"이란, 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. <출처: mdn>

 

이 말을 좀 더 이해하기 위해 Lexical(어휘적)의 의미를 비유적으로 머리에 한번 박고 넘어가고자 한다.

<출처: 카카오 거시기콘>

'거시기' 라는 단어는 말이 쓰인 상황에 따라, 그 의미가 달라질 수 있다.  아래처럼 말이다.

"간밤에 거시기 혔는가?" ➡️ '간밤에 편안하게 잘 쉬었는가'

"말이 좀 거시기 허네!" ➡️ '말이 좀 듣기 거북하네'

"그건 쫌 거시기 헌디..." ➡️ '그건 쫌 곤란한데'

"내 맴이 좀 거시기 혀" ➡️ '내 마음이 좀 심란해'

"오늘 컨디션이 거시기 허네" ➡️ '오늘 컨디션이 별로네'

"이따 거시기 할껴?" ➡️ (함께 밥을 먹고 있는 상황에서)'이따 다먹고 커피한잔 할거야?'

 

이는 모두 '거시기'라는 단어를 들은 청자가 어휘적 환경을 고려해서, 그니까 문맥을 고려해서 거시기를 적절하게 다른 말로 대체해서 알아듣는 것이다. 

 

하지만 만약 오늘 처음 만났는데, 만나자마자, 아무런 사전정보없이 상대방이 '거시기는?' 하면?? 당연히 뭔말이냐고 되물을 것이다. 이렇게 우리는 '거시기'라는 단어를 외부 환경에서 참조해올 수도 있고, 혹은 상대방의 '거시기'의 의미의 범주를 읽어오는데 실패할 수도 있다.

 

여기서 어휘적 범위 지정(Lexical Scoping)의 '어휘적(lexical)'의미를 바꿔말해보자!

변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 
⬇️
'거시기'가 어디에서 사용 가능한지 알기 위해, 그 '거시기'가 대화 및 상황 내 어디에서 쓰였는지를 고려한다. 

이를 코드로 바꿔보면

function meetFriend() {
    var geosigee = "식사"; // 거시기는 greet함수의 지역변수이다.
    function greet() { // greet()는 내부함수이며 클로저(closure)이다.
        console.log(geosigee + ' 했는가?'); // greet()는 부모함수에서 변수 geosigee를 쓴다.
    }
    greet();
}

meetFriend();

 

자바스크립트가 코드를 읽을때 문맥적으로  콘솔에서 geosigee가 의미하는 것을 찾아보다가 지역 블록스코프에서 찾아보더니 없어서 상위의 meetFriend의 함수스코프로 타고 올라가서 geosigee값을 가져온다. (참고로 이렇게 타고 올라가는걸 '스코프 체인'이라고 한다.)

 

이렇듯 '클로저'란 단순한 구조가 아니라 '함수'와 '함수가 선언된 어휘적 상황'을 모두 고려해서 파악해야 하는 함수, 혹은 그 환경를 말하는 것이다.

 

이때 또 중요한게 "호출된" 위치가 아닌,  "선언된" 위치를 기준으로 변수를 참고 한다는 것!


 

클로저의 예시

다시 돌아와서 그럼 클로저의 의미를 곱씹으며 클로저 함수를 살펴보자

    function makeMultiple(x) {
      let multiplier = x;
      return function(y) {
        let multiplicand = y;
        return multiplier * multiplicand;
      };
    }

    var multi5 = makeMultiple(5); //multi5는 인자를 받아 5*인자의 값을 토하는 구구단 5단 함수
    var multi7 = makeMultiple(7); //multi7는 인자를 받아 7*인자의 값을 토하는 구구단 7단 함수
    //클로저에 x값이 구구단에서 곱해지는 앞의 숫자로 저장됨

    console.log(multi5(2));  // 구구단 5단에 2대입 > 5*2=20
    console.log(multi7(2)); // 구구단 7단에 2대입 > 7*2=14
    //함수 실행 시 클로저에 저장된 multiplier에 접근하여 답을 구함

1️⃣ 외부함수 실행:  makeMultiple(5) 인자의 값으로 5가 전달되면서 makeMultiple함수 내에 선언된 multiplier변수에 5가 할당되고, 5에 어떤수(아직 할당이 되지 않음)를 곱하는 함수가 리턴된다.

2️⃣ 외부함수로 리턴된 내부함수를 변수로 저장: 그렇게해서 리턴된 함수 자체를 multi5로 저장해놓는다.

3️⃣ 내부함수 실행: 내부함수 multi5에 한번더 인자2를 넣어서 실행시키면, 이제 외부함수가 실행되면서 할당된 5에 내부함수 실행으로 전달받은 2가 곱해지면서 값이 나온다.

 

이 과정에서 외부함수의 변수 multiplier은 내부함수에 존재하지 않아도 어휘적 적용 범위의 파워로 내부함수의 계산식에 관여할 수 있게되는것이다. 

 

여기서 내부함수를 '클로저'라고 보통 부르며, 이런 문맥자체를 '클로저 함수'등으로 뭉뚱그려 부르기도 한다.


클로저 더 쉽게 파악하기

이제 클로저가 무엇을 의미하는지는 알았을 것이다. 그럼 좀 더 명확하게 정리해보고자 한다.

 

클로저는...

📌 외부함수가 내부함수를 리턴하는 환경에서 만들어진다.
📌 내부함수가 외부함수의 변수의 접근이 가능해야 한다. (어휘적 적용범위의 영향으로) 
📌 이때 내부함수를 '클로저'라고 부른다.

특히, 두번째 사항은 클로저의 필수 조건이라고 볼 수 있다. 아래의 코드는 첫번째 사항은 만족하지만, 클로저라고 치지 않는다. 내부함수가 외부함수에서 건들이는 변수가 없기 때문이다.

let multiplyByFive = function() {
  return function(y) {
    return 5 * y;
  }
}

let multiplyBy5 
multiplyBy5 = multiplyByFive();
multiplyBy5(4);

반면 아래의 함수는 클로저 사용의 예로 볼 수 있다. 내부함수가 외부함수를 건들이도록 설계되었기 때문이다.

let multiplyByX = function(x) {
  return function(y) {
    return x * y;
  }
}

let multiplyBy5;
multiplyBy5 = multiplyByX(5);

multiplyBy5(4);

클로저 문제 풀어보기

 

Question. A, B, C, D 각각에 출력 될 내용으로 틀린 것을 고르세요.

var a = 0;
function foo() {
    var b = 0;
    return function() {
        console.log(++a, ++b);
    };
}

var f1 = foo();
var f2 = foo();

f1(); // --> A
f1(); // --> B
f2(); // --> C
f2(); // --> D

답)

//A 1 1
//B 2 2
//C 3 1
//D 4 1

 

해설)

  • foo함수 안에 익명함수가 리턴되고 있고, 그 내부 함수는 외부의 b값을 참조하므로 '클로저'라고 볼 수 있다.
  • 클로저 내부함수는 b뿐만 아니라 a 변수도 외부에서 값을 읽어온다. 어휘적 적용 범위의 원칙을 따라, 스코프체인으로 지역변수에 없는 a를 찾기 위해 타고 올라가, 전역변수로 선언된 a의 값을 참조한다
  • f1과 f2는 똑같은 기능을 하지만 다른 함수이다. 그렇기 때문에, f1이 수행되었을때에 외부함수의 지역변수 b에 축적되는 값은 f2에 공유될 수 없다.
  • 하지만 전역변수 a는 f1과 f2의 울타리에 갇혀져있지 않은 가장 큰 범위에 속하므로, 어디서든 변화를 시키면 그 축적사항이 f1과 f2 모두에게 영향을 준다.
  • 다시말해 f1의 b변수, f2의 b변수는 각각 다른 함수스코프에 있는 다른 변수이지만, a변수는 하나다.

 

++++

사실 나는 이문제에서 이해가 가지 않던 부분이 클로저보다도

console.log(++a,++b)를 하면 어째서 a와 b의 값을 축적시켜서 변화시키는지 였다.

내가 생각하기엔 한번 값이 더해져서 1,1이 찍히긴 했지만,

이는 콘솔로그상에서 연산되서 출력된거지 a,와b 변수의 값 자체를 변화시킨 것은 아니므로,

다음번에 똑같 console.log(++a,++b)를 하면 또 1,1이 찍혀야 되는것 아닌가 싶었던 것이다. 

 

이에 대해 구글링 해보니, 콘솔로그의 함수를 실행하기 전에 ++a와 ++b이 먼저 연산이 되기 때문이었다. 

  1. 먼저, a와 b에 1을 더하고 a와 b를 업데이트한다. (a=1,b=1)
  2. 그리고 이제 콘솔로그라는 함수에 인자로 넣어주어 콘솔창에 띄워준다.

때문에 아래와 같은 코드도 말이 된다.

let foo = 0;
console.log(foo=foo+2) //2 출력
console.log(foo=foo+2) //4 출력

이건 또 처음 알았다.


클로저가 쓰이는 실무 예시 보기

<!DOCTYPE html>
<html>
<body>
  <button class="toggle">toggle</button>
  <div class="box" style="width: 100px; height: 100px; background: red;"></div>

  <script>
    var box = document.querySelector('.box');
    var toggleBtn = document.querySelector('.toggle');

    var toggle = (function () {
      var isShow = false;
	
      //① 클로저를 반환하는 함수
      return function () {
        box.style.display = isShow ? 'block' : 'none';
        // ③ 변수 상태 변경
        isShow = !isShow;
      };
      
    })();

    // ② 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;
  </script>
</body>
</html>

물론 꼭 클로저가 아니더라도 가능은 하다.

하지만, 전역변수를 쓰는 방법대신, 클로저에 변수를 가두고 내부함수에서 참조해서 쓰도록 하는 이 방식은 안전하고 깔끔해 보이기는 하다.

다음에 혹시 또 쓰거나 마주치게 되면 내용을 추가해야겠다.


클로저 요약

* 정의: 함수와 함수가 선언된 어휘적 환경(lexical environment)의 조합
* 특징: 내부 함수가 외부 함수 안에 선언된 변수에 접근할 수 있다. 내부 함수를 클로저 함수라고 부르기도 한다.
* 응용: namespacing, privacy, function factory, partially applied functions, ...