스코프와 클로저

Yeji·2023년 9월 12일
0

1. 스코프(Scope)

function add(a, b){
  console.log(a, b); // 4 1
  return a + b;
}

add(4, 1);

console.log(a, b); // ReferenceError

위의 예시에서 마지막 콘솔은 ReferenceError가 뜬다.
변수 a와 b를 담고 있는 함수 밖에서 다른 코드는 a와 b의 값을 참조하지 못하기 때문이다.
이처럼 변수가 선언된 위치에 따라 그 유효 범위가 결정되는 것을 스코프라고 한다.

1-1. 정의

식별자가 본인이 선언된 위치에 따라 그 유효 범위가 결정되는 것

자바스크립트 코드는 전역과 지역으로 구분할 수 있다.
어떤 변수가 전역적으로 선언되었다면 전역 스코프를 가지는 전역 변수, 지역적으로 선언되었다면 지역 스코프를 가지는 지역 변수라고 한다.

[전역 변수]
전역에서 선언된 변수로, 어디서든 참조할 수 있다.

[지역 변수]
지역(함수) 내에서 선언된 변수로, 그 지역과 그 지역의 하부 지역에서만 참조할 수 있다.

만약 x라는 변수가 지역에도 선언되어 있고, 전역에도 선언되어 있다면 자바스크립트 엔진은 어떤 값을 참조하게 될까?

함수의 중첩과 스코프 체인을 통해 알아보자.

1-2. 스코프 체인(Scope Chain)

어떤 함수는 전역적으로 선언될 수 있지만 또 어떤 함수는 다른 함수의 내부에서 정의될 수 있다. 이를 함수의 중첩이라고 한다.

전역 > 외부 함수 > 중첩 함수

함수가 중첩된다는 것은 각 함수의 지역 스코프 또한 중첩될 수 있다는 것을 의미하며, 이를 스코프 체인이라 한다.

1-2-1. 정의

스코프 체인을 더 자세하게 정의하면 다음과 같다.

함수의 중첩에 의해 스코프가 계층적인 구조를 가지는 것

1-2-2. 스코프 체인의 단방향성

자바스크립트 엔진에서 스코프 체인을 따라 상위 스코프 방향으로 이동해 변수를 참조하는 것

자바스크립트 엔진은 변수를 참조할 때 이 스코프 체인을 따라 올라가며 참조한다.

어떤 변수를 사용하는 스코프에 해당 변수가 없다면 한 단계 위의 상위 스코프를 참조하고, 그곳에도 변수가 없다면 스코프 체인을 따라 전역 스코프까지 올라간다.

다음 예시에서 스코프 체인은 다음과 같다.

전역 스코프 > outer 지역 스코프 > inner 지역 스코프

var x = "global x"; // 전역변수 x

// 외부함수 outer
function outer(){
  var y = "outer local y";
  console.log(x); // global x
  console.log(y); // outer local y
  // 중첩함수 inner
  function inner(){
    var x = "inner local x";
    console.log(x); // inner local x
    console.log(y); // outer local y
  }
  inner();
}

outer();
console.log(x); // global x
console.log(y); // ReferenceError

전역 스코프까지 갔는데도 찾는 변수가 존재하지 않으면 나타나는 것이 바로 ReferenceError다.

1-3. 스코프 레벨 (Scope Level)

스코프는 어떤 레벨을 가지냐에 따라 두 가지로 구분할 수 있다.

블록 레벨 스코프

C, Python, Java 등 대부분의 프로그래밍 언어가 블록 레벨 스코프에 해당한다.

(함수, if문, for문, while문 등) 모든 코드 블록 안에서 선언된 변수는 코드 블록 내에서만 유효하고 블록 외부에서는 참조할 수 없다. 코드 블록 내부에 선언한 변수는 지역 변수다.

함수 레벨 스코프

JavaScript가 함수 레벨 스코프를 가지는 대표적인 언어인데, var키워드로 선언한 값은 함수를 제외하고 스코프를 가지지 않는다.

함수 안에서 선언된 변수는 함수 안에서만 유효하고 함수 외부에서는 참조할 수 없다. 함수 내부에 선언한 변수는 지역변수이고, 함수 외부에서 선언한 변수는 모두 전역 변수다.

그런데 ES6 이후로 이를 보완하기 위해 const, let과 같은 키워드가 등장했다. 이를 통해 자바스크립트에서도 블록 레벨 스코프를 보장한다.

1-4. 상위 스코프

상위 스코프가 결정되는 시점을 기준으로 스코프를 나눌 수 있다.

동적 스코프

함수가 호출되는 시점에 상위 스코프가 결정된다.

정적 스코프 (렉시컬 스코프)

함수가 정의되는 시점에 상위 스코프가 결정된다.

자바스크립트는 정적 스코프를 따르고 있다. 함수가 선언되고 함수 객체가 형성되면 함수는 자신의 내부 슬롯에 상위 스코프에 대한 정보를 저장한다. 렉시컬 환경을 저장한다고도 말할 수 있겠다.

[렉시컬 환경]
하나의 자료구조로, 어떤 코드가 어디서 실행되고 주위에 어떤 코드가 있는지에 대한 정보를 저장한다.

2. 클로저(Closure)

아래 코드를 보면 콘솔에는 3이 찍히게 된다.

const x = 1;

function outer(){
  const x = 3;
 
  function inner(){
    console.log(x); // 3
  }
  return inner();
}

const goExecute = outer();
goExecute();

outer 함수가 먼저 실행되고 실행 컨텍스트 스택에서 사라져 생명주기를 마감했는데, inner 함수는 x의 값을 어떻게 참조하고 있는걸까?

외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수으 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

외부 함수가 생명주기가 마감되었지만 내부 슬롯에 저장되어 있는 상위 스코프의 정보를 가지고 상위 스코프의 지역 변수를 참조할 수 있다.

중첩 함수 inner가 생명주기를 마감한 외부 함수 outer의 지역 변수를 참조할 수 있다면 inner 함수는 클로저라고 한다. 여기서 참조된 x의 값은 자유 변수라고 한다.

2-1. 정의

클로저를 정리하면 다음과 같다.

  1. 상위 스코프의 지역 변수를 참조하고 있고,
  2. 자신의 외부 함수보다 더 오래 살아있는 중첩 함수

2-2. 활용

2-2-1. 상태 기억

자신이 생성 되었을 때의 렉시컬 환경을 기억한다는 것이 어떤 의미인지 예시를 통해 이해해보자.

const toggle = (function () {
  let isShow = false;

  return function () {
    box.style.display = isShow ? 'block' : 'none';

    isShow = !isShow;
  };
})();

toggleBtn.onclick = toggle;

버튼을 누를 때마다 박스의 display 속성이 토글된다.

얼핏 보면 isShow의 값이 계속 false로 고정될 것 같지만 클로저는 생성되었을 때의 렉시컬 환경을 기억하기 때문에 값이 true, false로 잘 토글된다.

2-2-2. 정보 은닉

생성자 함수 내부에 자유 변수 counter가 정의되어 있다.
이 자유 변수는 this로 바인딩 되어 있지 않기 때문에 생성자 함수로 생성한 인스턴가 해당 변수에 접근하지 못한다.

function Counter() {
  let counter = 0;	// 자유 변수

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

그런데 생성자 함수 내부에 정의된 increase와 decrease는 정의될 때의 렉시컬 환경을 기억하기 때문에 counter에 접근해 값을 조작할 수 있다.

이러한 특성을 이용하면 클래스 기반 언어의 private 키워드를 구현할 수 있다.
만약 this로 바인딩 된 변수였다면 외부에서도 접근 가능한 public 속성을 가질 것이다.

참고

엘라의 Scope & Closure
poiemaweb-js closure
poiemaweb-block scope

profile
채워나가는 과정

0개의 댓글