[TIL] 스코프와 클로저

XCC629·2022년 6월 25일
0

frontend

목록 보기
13/16
post-thumbnail

📚 스코프

프로그래밍을 하기 전에 필수적으로 알아야 하는 개념은 무엇일까요? 여러가지 있겠지만, 변수를 선언했을때 이 변수가 어디까지 이용가능한지를 알아야 코드를 작성할 수 있을 것 같습니다.

그 변수가 이용가능한 범위를 바로 '스코프'라고 합니다.

정의: 변수이름, 함수이름 등의 식별자가 살아있는 유효범위

예시)

var x = 1;

function add(){
  var x = 3;
  var y = 2;
  console.log(x+y)
}

add() // 5

console.log(x) // 1
console.log(y) // 'error'
  1. 가장 상위에 선언된 x는 전역변수라고 합니다. 스코프가 전체라는 것입니다.
  2. 함수 add안에 선언된 x,y는 지역변수입니다. 스코프가 선언된 함수 안에서만 살아있습니다.

그렇기 때문에 마지막 x와 y를 참조하면 x는 전역변수에 선언된 1의 값을 보이고,
y는 찾을 수 없기 때문에 에러가 발생합니다.

그러면 이 함수들은 어떻게 자기보다 더 상위에 있는 스코프를 알고 있을까요?


스코프 체인

스코프 체인은 스코프가 계층적으로 연결된 것을 말합니다.

물리적으로 스코프체인이 있기 때문에, 자바스크립트가 변수를 참조할때 이 스코프체인을 따라서 참조하게 됩니다.

이 예시와 같이, 가장 내부에 있는 bar 함수에서 필요하지만 그 스코프 안에 선언된게 없는 변수를 참조할때는 스코프체인을 타고 올라가면서 찾는 것입니다.


📚 스코프 구분법 2가지

1. 함수레벨 스코프 vs. 블록레벨 스코프

함수 레벨 스코프는 함수만 코드 블록이 분리되는 것을 말하고, 블록레벨 스코프는 {}로 묶이는 것을 모두 한 블록으로 보는 것을 말합니다.

자바스크립트 같은 경우에는 함수레벨 스코프를 가지고 있습니다. 그래서 아래와 같은 상황이 생깁니다.

var x = 0;
{
  var x = 1;
  console.log(x); // 1
}
console.log(x);   // 1

var 키워드로 선언된 x의 값이 재할당 되고 맙니다.

이건 var를 사용하는데 있어서 단점으로도 볼 수 있습니다. 같은 이름의 변수가 선언되었는데도 오류없이 재할당을 하기도하고, 일반적인 언어들처럼 블록레벨 스코프가 아니니, 실수로 바꾸면 안되는 변수의 값을 바꾸는 오류도 발생합니다.

그래서 es6에서는 자바스크립트도 블록레벨 스코프를 사용하기 위해서 let, const 키워드를 추가하게 되었습니다.

let y = 0;
{
  let y = 1;
  console.log(y); // 1
}
console.log(y);   // 0

결론: let, const를 사용하면 블록레벨 스코프를 사용할 수 있다!

2. 동적 스코프 vs. 렉시컬 스코프

함수가 호출될 때 스코프가 결정되는 것을 동적 스코프, 함수 정의될 때 스코프 결정하는 것을 렉시컬 스코프라고합니다.

자바스크립트의 경우에는 렉시컬 스코프로 작동됩니다.
따라서 선언 할때에 본인의 내부 슬롯에 상위 스코프의 참조를 저장해둡니다.


자바스크립트 엔진

자바스크립트 엔진의 작동원리를 파악하기 위해서는 알아야 하는 개념이 2개 있습니다.

렉시컬 환경

렉시컬 환경은 실행 중인 함수, 코드 블록, 스크립트 실행 전에 생성되는 특별한 객체로, 실행할 스코프 범위 안에 있는 변수와 함수의 프로퍼티를 저장하는 객체를 말합니다.

(저도 무슨 소리인지 잘 모르겠지만...)

let string = 'hi';

function say(name){
	alert('${string}, ${name}
}

say('John')

이런 경우에
name:'Jons', say: function, phrase: 'hello' 처럼 프로퍼티를 저장하는 객체가 있나봅니다.........

실행 컨텍스트

실행 가능한 코드가 실행되기 위해 필요한 환경이라고 할 수 있습니다. 실행 가능한 코드들이 실행되기 위해서는 정보가 필요한데, 이 정보라 함은 변수, 함수선언, 스코프, this등이 해당됩니다.

자바스크립트 엔진은 실행컨텍스트를 물리적 객체로 관리합니다.

둘의 관계

실행 컨텍스트가 렉시컬 환경을 관리하고 있습니다.
= 실행 컨텍스트가 렉시컬 환경이라는 객체를 저장해두고 변경이 있을 때마다 업데이트 하고 가져다쓰고!


실행 컨텍스트 스택

실행 컨텍스트는 스택은 각 함수가 호출될때마다 쌓이고, 그 함수가 다 실행되었을 때 삭제합니다. (스택이니까.)

예시 코드

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

/**
 *  함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
 *  그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
 */

var inner = outerFunc();
inner(); // 10

그림처럼 outerFunc 함수는 호출된 시점에서 스택에 쌓였다가, 호출이 끝나게 되면 삭제됩니다. 그렇게 되면 그 내부에 있는 x값도 사라지게 됩니다. 하지만 inner()의 결과는 10으로 나타납니다.

outerFunc 함수가 죽었는데 어떻게 10이라는 값을 참조하고 있을까요??
자기보다 상위 스코프에 있는 x의 값을 콘솔에 찍는 역할은 innerFunc이 하고 있습니다. 이런 함수를 바로 클로저라고 합니다.

결론: 이 예시의 innerFunc 함수처럼, 자기를 포함하고 있는 외부함수보다 내부함수가 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있는 함수를 '클로저'라고 부릅니다.


클로저

클로저의 사전적 의미를 찾으면 아래와 같습니다.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합

뭔소리일까요? 차분히 생각해봅시다.

함수 : 반환된 내부 함수 = innerFunc
그 함수가 선언될 때의 렉시컬 환경 : 내부 함수 선언 시의 스코프 = outerFunc 환경

풀어서 이야기 해보자면.
클로저는 반환된 내부 함수가 자신이 선언될 때의 환경인 스코프를 기억하여 자신이 선언됐을 때의 스코프 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수 라고 부를 수 있겠습니다!

자유변수

x와 같이 클로저에 의해 참조되는 외부함수의 변수를 '자유변수'라고 부릅니다.

클로저라는 이름의 의미

자유 함수에 엮여있는(닫혀있는) 의미를 가져서 '클로저'라고 이름 붙였습니다.

어떻게 가능한건데?

실행컨텍스트의 관점에서 보아야합니다.

예시 코드

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

/**
 *  함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
 *  그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
 */

var inner = outerFunc();
inner(); // 10

과정:

  1. outerFunc 함수가 종료되어 스택에서 제거

    • outerFunc 함수 inner에 innerFunc 함수를 반환하면서 사라짐
    • inner에 innerFunc 함수 객체를 참조
  2. outerFunc이 스택에서 사라져도 렉시컬 환경까지는 손대지 않음 (삭제하지 않음)

    • inner는 innerFunc 함수 객체 참조
      => innerFunc 함수 객체는 outerFunc 함수의 렉시컬 환경을 참조
  1. inner를 호출하면 =>
    outerFunc 함수의 렉시컬 환경 참조 =>
    변수 x를 다시 console에 찍을 수 있다!

클로저, 어디에 쓰는가?

클로저는 자신이 생성될때의 환경을 기억하므로 메모리 손해가 있긴 합니다만! 강력한 기능이기 때문에 필요한 상황에서는 적극적으로 사용해야 합니다.

1. 상태유지

  • 클로저가 가장 유용하게 사용되는 상황입니다.
  • 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것
<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>

2. 전역 변수의 사용 억제

이 사례를 잘 설명하는 것은 '버튼 클릭 시 누적되어 표시되는 카운터'를 만들 때이다. 이때에, 클릭된 횟수를 전역변수, 지역변수, 클로저로 관리하는 것 중에서 어느 것이 안전할까를 이야기해볼 수 있다.

  1. 전역변수로 관리
  • 누구나 접근하고 변경할 수 있음.
  1. 지역변수로 관리
    function increase() {
      // 카운트 상태를 유지하기 위한 지역 변수
      var counter = 0;
      return ++counter;
    }
  • 실행될때마다 counter가 0으로 초기화되어서 1 값만 나옴. 이전 상태 기억 x.
  1. 클로저로 관리
    var increase = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      var counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }()); 
    
  • 자기가 생성되었을 때의 렉시컬 환경을 기억.
  • 지역변수 counter를 기억하기 때문에 counter에 접근할 수 있고 유지된다.
  • 아무도 counter 값을 변경할 수 없다!

3. 정보 은닉

  • 아무도 counter 값을 변경할 수 없다!
  • class의 public을 흉내낼 수 있다!

참고자료

클로저
[10분 테코톡] 🍧 엘라의 Scope & Closure

profile
프론트엔드 개발자

0개의 댓글