TIL 75 | JS Closure(1) 클로저는 왜 클로저일까?

Gom·2021년 5월 22일
2

JavaScript

목록 보기
19/22
post-thumbnail

'설명하긴 어렵지만 알 것 같아!'
클로저를 대충 이해하고 코드를 작성하다보면 위와 같은 생각이 들곤 한다.
안다고 착각하지만 모르는 상태는 가장 위험하다.
때문에 🤣 글을 통해 개념을 정리하고자 한다.

먼저 클로저를 공부하고 싶게 만드는 문제를 소개한다.

🤗 클로저 공부하고 싶게 만드는 문제 ~

function count() {
    var i;
    for (i = 0; i < 5; i++) {
      console.log(i)
        setTimeout(function() {
            console.log(i)
        }, i*1000)
    }
}
count();

클로저를 모른다면 💭 :
i가 1씩 증가하며 1초에 한 번씩 5번 출력되니까 0,1,2,3,4가 출력되겠군 !

실제 출력결과 :
5,5,5,5,5
5가 1초에 한 번씩 5번 출력된다.

✅🕵️‍♀️ Why? 이 문제를 이해하기 위해 알아야할 것

  1. 비동기 함수가 실행되는 방식과 순서 (이벤트 루프)
  2. 변수 유효범위와 사용 가능한 변수가 선언된 위치를 탐색하는 과정 (Lexical scoping)

1+2를 조합해 간단히 해석하면 아래와 같다.
function() { console.log(i) } 함수 안의 변수 i는 setTimeout이 실행될 때 값이 결정된다. i*1000을 대기한 후 setTimeout 매개변수로 주어진 함수가 선언과 실행되는 시점에는 i의 반복문이 모두 돌아 i가 5가 되어있는 상태를 참조하게 되므로 5가 5번 출력된다.

이 글에서는 (1)이벤트 루프에 대한 설명을 생략하고
클로저와 밀접한 (2)Lexical scoping 개념부터 시작하여 클로저에 대해 자세히 알아보겠다.


Lexical scoping (어휘 또는 정적 범위 지정)

JS의 스코프는 lexical scoping, 즉 변수의 범위는 소스코드가 작성된 그 문맥에서 바로 결정된다.
실행 지점과 상관없이 생성 시점의 환경을 기억해 사용한다.
외부 함수 수행이 끝난 이후에도 렉시컬 환경 참조는 계속된다.

Lexical scoping은 자바스크립트가 변수의 유효범위를 지정하는 메커니즘이다.
이 메커니즘은 함수가 선언되는 시점에 함수가 사용하는 변수를 어디에서 참조해올 것인가를 결정한다. 선언 시에 scope와 scope chain이 정해지는 것이라 할 수 있다. 사용 가능한 변수를 찾아 참조하고자 할 때 함수 호출 지점을 기준으로 탐색하는 것이 아니라 '선언된 시점'의 환경을 고려하여 참조할 변수를 정한다. 함수 실행 시에도 선언 시점에 미리 정해진 변수 위치를 참조한다.

요약)
함수안에 쓰이는 name이라는 변수의 값을 참조하기 위하여 참조할 수 있는 값이 나올 때까지
자기 스코프에서부터 범위를 확장하며 변수를 탐색한다.
보라색 영역 -> 노란색 영역 -> 전역

예시 상세 설명)
MDN 예시를 보면 외부 함수 init()안에 내부 함수 displayName()가 존재한다.
lexical scoping에 따라 displayName() 함수가 선언되는 순간에 이용할 변수의 참조 위치를 탐색하게 된다. displayName() 함수에는 name이 사용되지만 해당 함수 내에는 지역 변수 name이 존재하지 않는다. 이런 경우 자기 스코프에서 가까운 곳에서부터 범위를 확장시켜가며 변수를 탐색한다(실행컨텍스트와 scope chain 원리). 예시에서는 자기 스코프(보라색 영역)에 참조 가능한 name 변수가 존재하는 지를 가장 먼저 탐색한다. 없다면 상위 범위(노란색 영역)에서 name 변수를 탐색한다. 예시에는 var name = "Mozilla"로 선언되어 있으므로 displayName()은 부모 함수 init()에서 선언된 변수 name을 참조한다. 만약 이 코드가 존재하지 않았다면 노란색 영역 바깥의 전역변수까지 탐색하게 되며 전역변수에 존재했다면 참조를, 전역 스코프에도 선언된 name 변수가 없다면 변수를 찾지 못하였다는 에러가 발생한다.


Closure (클로저)는 왜 클로저인가?

자유변수를 기억하는 함수? 스코프를 기억하는 함수? 환경을 기억하는 함수?
클로저를 설명하는 말이 아주 많지만 클로저란 이름으로 불리는 이유를 생각하며 접근해보자.

* Closure의 정의

클로저 = 함수 + 함수를 둘러싼 환경(Lexical environment)

Closure는 닫힘, 폐쇄를 의미한다.

클로저란 아래 이미지의 보라색 영역에 해당하는 함수와 그를 둘러싼 환경을 칭하는 말이다.
lexical scoping에 의해 함수 선언 시점에 참조할 변수가 결정되고 나면 name이 다른 값을 가리키게 할 수 없다.

하단의 이미지 예시로 보면 보라색 영역의 name은 노란색 영역이 Lexical environment이고 그 안에 선언된 name 값을 참조하게 된다. name은 클로저 함수인 displayName()의 매개변수도 아니고 내부에서 생성한 변수도 아니다. makeFunc() 내 값을 직접 변경하지 않는 이상 값을 변경할 방법은 없다. 또한 스코프 체인 규칙에 의하면 Lexical environment의 외부에서 지역변수를 접근할 수도 없다.

생성시점에 결정된 Lexical environment에 의해 오직 클로저에서만 접근할 수 있다.

변수가 조작될 여지가 없도록 폐쇄된 모습이다.

(+) 예시의 name처럼 closure 함수의 매개변수도 아니고, closure 함수 내부에서 생성한 변수도 아닌 경우를 비공개 변수라고 한다. 수학적으로는 자유변수라고도 한다. 클로저가 자유변수를 기억하는 함수라고 불리는 이유이다.

예시 상세 설명)
1. displayName()name을 찾아 출력하는 함수로 정의되었다.
2. displayName()은 outer environment 참조로 makeFunc()의 environment를 저장하였다.
3. global의 myFuncdisplayName() 함수를 할당하였다.
4. global에서 myFunc(=displayName())을 호출했다.
5. displayName()은 자신의 스코프에서 name을 찾는다.
6. 함수 내부에 지역변수가 존재하지 않으므로 자신의 outer environment 참조를 찾아간다.
7. outer environment인 makeFunc()의 스코프를 탐색한다. name을 찾았다.
8. name의 값인 Mozilla를 출력한다.

✅🕵️‍ 의문점 > 클로저라 가능한 것들

  • (4, 6~7번 과정) global에서 호출myFunc(=displayName())makeFunc()지역변수를 참조한다.
    👀❓❓
    스코프 체인(scope chain)에 의하면 내부 함수에서 외부 함수의 변수에 접근 가능하지만 외부 함수에서는 내부 함수의 변수에 접근할 수 없어야한다.

    🔎
    closure는 Lexical scoping에 따라 선언 시점에 변수 참조 범위가 결정되고 이를 기억하고 있었던 것이므로 호출 지점과 무관하게 기억된 환경에 접근하여 참조 값을 가져올 수 있다.

  • (3~4번 과정) myFunc 변수에 할당되는 과정에서 실행이 완료된 makeFunc()의 지역변수가 회수되지 않고 이후에도 살아남아 myFunc() 실행 시점에 활용된다.
    👀❓❓
    함수 실행이 끝나면 컨텍스트가 소멸되고 GC에 의해 회수되어 해당 함수의 지역변수를 이용할 수 없을 것으로 예상 된다.

    🔎
    myFunc 변수에 할당되는 과정에서 makeFunc()이 실행되면 실행컨텍스트가 소멸되는 것은 맞다.
    하지만 (2번 과정)에서 displayName()이 이 Lexical environment를 참조하게 되는데 이런 경우 Lexical environment는 소멸되지 않고 남게 된다. 그 결과 myFunc(=displayName()) 함수가 실행되면, 참조된 Lexical environment를 통해 식별자 name이 가리키는 값인 'Mozilla'을 의도대로 참조할 수 있게 된다.

* Closure를 사용하는 이유

자바스크립트는 태생적으로는 private, public 등의 별도 문법이 없는 언어이다. (cf. JAVA 등)
private 기능이 없다는 것은 예상치 못한 누군가 접근과 조작이 가능하다는 것이다.
그렇게 되면 경우의 수를 통제하기 어렵고 공용 인터페이스가 혼란스러워질 수 있다.

클로저는 자바스크립트에서 private 기능을 보완한다. 클로저를 이용하면 제한적인 접근만을 허용하여 private한 변수 또는 메소드의 효과를 낼 수 있기 때문이다.
즉, 객체지향 프로그래밍의 정보 은닉과 캡슐화의 이점을 얻을 수 있다.


활용 예시와 사용 시 주의점은 2탄에 계속...


참고자료
MDN
Toast Meetup

profile
안 되는 이유보다 가능한 방법을 찾을래요

0개의 댓글