Javascript

Scope와 Closure

Jay-JI 2024. 7. 30. 12:18

이번 포스트를 보기 앞서 이전 포스트를 보고 오시기 바랍니다.

https://jaystorage.tistory.com/46

 

Execution Context(실행 컨텍스트)

Lexical Environment(어휘적 환경) 란코드가 어디서 실행되며 주변에 어떤 코드가 있는지 대체적인 정보를 담고 있는 환경함수 본인 내부의 식별자, 식별자에 바인딩 된 값 등을 기록하고 있는 하나의

jaystorage.tistory.com

 


 

Scope 란

 

스코프란 변수 이름, 함수 이름, 클래스 이름과 같은 식별자가 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것을 의미한다. 보다 간단하게 설명하면 스코프는 식별자가 유효한 범위라고 할 수 있다.

 

스코프는 유효범위에 따라 다음과 같이 2가지로 분류된다.

  1. 전역 스코프
    - 어디서든지 참조 가능
  2. 지역 스코프
    - 자신의 지역과 하위 지역에서만 참조 가능

 

지역 스코프 생성 방법

  1. 블록 레벨 스코프
    - 대부분의 프로그래밍 언어가 지역 스코프를 생성할 때 사용하는 방식
    - 함수 몸체와 모든 코드 블록이 지역 스코프를 생성한다.
    - 모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없다.
    - 자바스크립트에서는 let, const 키워드가 블록 레벨 스코프로 작동한다. (var 키워드의 단점을 보완하고자 ES6때 도입됨)

  2. 함수 레벨 스코프
    - 함수의 코드 블록만을 스코프로 인정하는 방식
    - 즉, 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.
    - 자바스크립트의 var 키워드는 함수 레벨 스코프로 작동한다.

 


 

Scope Chain 이란

 

함수가 중첩되면 지역 스코프 또한 중첩이 일어나고 이러한 스코프들은 함수의 중첩에 의해 계층적 구조를 가지게 된다. 결국 스코프 체인이란 스코프가 계층적으로 연결된 것을 의미한다.
이러한 스코프 체인은 execution context의 lexical environment에 있는 outer environment reference(외부 환경 참조)에 의해 구현된다.

 

스코프 체인 예시

 

 

스코프 체인에서 변수를 참조할 때는 무조건 상위 스코프 방향으로만 올라간다. 이로 인해 상위 스코프에서 선언한 변수를 하위 스코프에서 참조하는 것이 가능하다. 하지만 스코프 체인의 단방향성으로 인해 상위 스코프에서 하위 스코프 변수를 참조하는 것은 불가능하다.

 

 

동적 스코프와 정적 스코프

상위 스코프가 결정되는 시점을 기준으로 스코프를 다음과 같이 분류할 수 있다.

  1. 동적 스코프
    - 상위 스코프가 함수가 호출되는 시점에 결정
    - Lisp와 같은 언어가 동적 스코프를 사용한다.

  2. 정적 스코프(렉시컬 스코프)
    - 상위 스코프가 함수가 정의되는 시점에 결정
    - C언어, Java, Javascript와 같은 대부분의 프로그래밍 언어는 정적 스코프를 사용한다.

 


 

Closure 란

 

클로저는 여러 함수형 프로그래밍에서 등장하는 보편적 특성이라 자바스크립트만이 가지고 있는 고유의 개념은 아니다. 따라서 문헌별로 클로저에 대한 설명이 각각 다르다.

 

자바스크립트 핵심가이드에서는 클로저를 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수라고 설명했다. 반면 MDN에서는 클로저를 함수와 함수가 선언된 어휘적(Lexical) 환경의 조합이라고 설명하고 있다.

 

사실 이러한 설명만 보명 클로저가 왜 필요한 것이고 정확히 어떤 것인지 감이 오지 않는다. 하지만 클로저가 다음과 같은 문제를 해결할 수 있다는 걸 인지한다면 클로저가 왜 필요한지 좀 더 이해하기 쉬울 것이다.

 

  • 자바스크립트는 java의 public, protected, private 처럼 변수 자체에 접근 권한을 직접 부여하도록 설계되어 있지 않다. (자바스크립트는 정보 은닉을 완전하게 지원하지 않는다.)

 


 

Closure의 작동 원리

 

클로저의 작동 원리를 이해하려면 반드시 execution context가 어떤 식으로 작동하는지 알고 있어야 한다.

 

아래와 같은 코드가 있다고 가정하자

const x = 1;

function outer() {
    const x = 10;
    const inner = function () {
        console.log(x);
    };

    return inner;
}

const ella = outer();
ella();

 

위 코드의 execution context는 다음과 같은 순서로 생성된다.

 

1. 전역 실행 컨텍스트 (Global Execution Context)

  • 생성 단계 : 전역 실행 컨텍스트가 생성되고 전역 변수와 함수 선언이 초기화
  • 실행 단계 : outer 함수가 전역 스코프에 정의

2. outer 함수 호출

  • 생성 단계 : outer 함수의 실행 컨텍스트가 생성됨
  • 실행 단계 : x 변수에 10이 할당되고 inner 함수가 정의된 후 반환됨
  • 소멸 단계 : outer 함수의 실행 컨텍스트가 소멸됨. 그러나 inner 함수에서 outer 함수의 x를 참조하고 있기 때문에(클로저) x 값은 유지됨

3. ella 함수 호출

  • 생성 단계 : ella(inner) 함수의 실행 컨텍스트가 생성됨
  • 실행 단계 : console.log(x); 가 실행되어 10이 출력됨
  • 소멸 단계 : inner 함수의 실행 컨텍스트가 소멸됨

outer 함수가 종료되기 전의 execution context 상태

 

 

outer 함수가 종료된 후의 execution context 상태 – outer 함수의 execution context는 생명주기가 끝나 call stack에서 pop 되어 사라졌으나, outer 함수의 lexical environment는 inner 함수의 참조로 인해 가비지 컬렉션의 대상이 되지 않아 남아 있는다.

  • 위 코드에서는 inner가 상위 스코프의 식별자를 참조하고 있고 본인의 외부 함수보다 더 오래 살아있기 때문에 클로저가 된다.
  • 즉 클로저는 자신이 선언된 환경의 변수를 기억하고 접근할 수 있는 함수를 의미한다.

 


Closure 의 장점

 

앞서 설명했듯 클로저를 잘 활용하면 자바스크립트에서 데이터를 은닉할 수 있다.

function createCounter() {
    let count = 0; // 은닉된 변수

    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();

counter.increment(); // 출력: 1
counter.increment(); // 출력: 2
counter.decrement(); // 출력: 1
console.log(counter.getCount()); // 출력: 1

// 외부에서는 count에 직접 접근할 수 없음
console.log(counter.count); // undefined

 

 


Closure 추가 예시

 

아래는 클로저 관련 예시 코드이다.

 

 

1.  외부 함수의 변수를 참조하는 내부 함수

  • inner 함수에 a가 선언되어 있지 않으나 상위 스코프에 접근해서 a를 찾아냄
  • 결과적으로 2가 출력됨
  • 위 코드에서 9번째 line이 실행되고 outer 함수의 실행 컨텍스트가 종료되었을 시점에 Lexical Environment 에 있는 저장된 식별자들(inner함수와 변수 a)의 참조를 지움
  • 참조 카운트가 0이 되면(참조하고 있는 식별자가 없으면) 가비지 컬렉터의 수집 대상이 되어서 나중에 없어짐

 

 

2. 외부 함수의 변수를 참조하는 내부 함수 – 함수 실행결과가 아닌 함수 자체를 반환하는 경우

  • 위 코드에서 9번째 line이 실행되고 outer 함수의 실행 컨텍스트가 종료되었을 때 outer2 변수는 inner 함수를 참조하게 됨
  • 즉 outer2 함수를 호출하게 된다면 반환된 함수인 inner 함수가 실행됨
  • 따라서 콘솔에는 각각 2와 3이 출력됨

 

Q. 그런데 분명 outer 함수의 실행은 종료되었는데 어떻게 inner 함수 실행 시 outer 함수의 lexical environment에 접근할 수 있는 것인가?

 

A. 가비지 컬렉터가 동작할 때 어떤 값을 참조하는 변수가 하나라도 있다면(변수의 참조 카운트가 0이 아니라면) 수집대상에 포함시키지 않기 때문이다.

 

다시 코드를 분석하자

  • 위 코드에서 9번째 line이 실행되고 outer 함수의 실행 컨텍스트가 종료되었을 때 inner 함수를 반환한다.
  • 이 inner 함수는 outer2 변수가 가리키고 있기 때문에 inner 함수는 언젠가 호출될 가능성을 갖게 된다.
  • inner 함수의 실행 컨텍스트가 활성화되면 outer 함수의 변수 a에 접근해야 한다. 따라서 inner 함수는 가비지 컬렉터의 수집 대상에 포함되지 않는다.

 

 

3. 

 

Q. inner 함수가 outer 함수의 lexical environment 에 접근할 가능성이 있으면 가비지 컬렉터에 의해 수집되지 않는다. 그렇다면 위 코드에서 변수 b는 outer 함수 실행 종료 시점에 남아 있는가?

 

A. 변수 b는 어디에서도 참조되고 있지 않기 때문에 가비지 컬렉터의 수집 대상이 되어서 없어진다.

 

이러한 현상으로 볼 때 클로저를 보다 쉽게 설명하자면 다음과 같이 설명할 수 있다.

[외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있는 중첩 함수]

 

 

참고로 자바스크립트의 모든 함수는 상위 스코프를 참조하고 있다. 그렇다면 이론적으로 모든 함수를 클로저로 볼 수 있을 것 같으나 우리는 모든 함수를 클로저라 하지 않는다.

 

다음은 클로저로 착각하기 쉬운 예시들이다.

 

4. 클로저가 아닌 예 - 1

  • outer 함수의 결과로 inner 함수 자체를 반환하고 있기 때문에 inner 함수는 outer 함수보다 더 오래 유지된다.
  • 하지만 inner함수는 상위 스코프의 어떤 식별자도 참조하고 있지 않는다.
  • 이 경우 outer 실행 컨텍스트가 종료되었을 때 자바스크립트 엔진이 최적화를 통해 inner 함수의 상위 스코프를 기억하지 않게 한다. (메모리 낭비 방지)

 

5. 클로저가 아닌 예 - 2

  • inner 함수는 outer 함수의 변수 x를 참조하고 있지만 outer 함수의 외부로 inner 함수를 반환하지 않음
  • 즉 inner의 생명주기가 outer의 생명주기보다 짧다.
  • 클로저는 생명주기가 종료된 외부 함수의 식별자를 참조해야 하는데 위 코드는 이러한 클로저에 본질에 부합하지 않는다.

예시 코드를 통해서 클로저를 다시 설명한다면 다음과 같이 설명할 수 있다.

[어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상]

 

 


 

Closure 활용방법

 

클로저는 다양한 방식에 활용될 수 있으나 우리는 주로 상태를 안전하게 활용하고 유지하기 위해 사용한다. 다시 말해 클로저를 사용하면 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉할 수 있다.  (특정 함수한테만 상태변경을 허용하면 상태 은닉이 가능해진다.)

 

앞서 언급했듯이 자바스크립트는 java의 public, protected, private 처럼 변수 자체에 접근 권한을 직접 부여하도록 설계되어 있지 않다. (단 ES2019 부터는 선두에 #을 붙여서 private을 정의하는 방법이 등장했다.)

 

클로저는 함수 차원에서 public과 private을 구분한다. 외부 스코프에서 선택적으로 일부 변수에 대한 접근 권한을 부여하는 것이 가능하기 때문이다.

 

위 2번 예시 코드의 outer 함수는 외부(전역) 스코프로부터 철저하게 격리된 닫힌 공간으로서 작동한다. 외부에서 outer 함수에 있는 것들은 실행은 가능하지만 그 내부에는 직접적으로 개입이 불가능하게 설계되어 있다. outer 함수에서 반환해주는 그 값이 유일하게 외부에 정보를 제공하는 수단이다. 따라서 클로저를 사용해 접근제어를 구현하려면 다음과 같이 코드를 작성하면 된다.

 

  1. 함수에서 지역변수 및 내부함수 등을 생성
  2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return

 


출처 : 
https://www.youtube.com/watch?v=PVYjfrgZhtU
https://www.youtube.com/watch?v=xJtVVLPxgco&t=427s
https://en.wikipedia.org/wiki/Scope_(computer_science)
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures