자바스크립트 셀프 QnA(6): JS는 닫힌 문, 클로저

9rganizedChaos·2021년 10월 26일
0
post-thumbnail

(<자바스크립트 셀프 QnA> 시리즈에 작성된 포스팅들은 각 주제에 해당하는 <모던 자바스크립트 딥다이브> 챕터를 읽으며 요약한 내용입니다. 더 자세한 내용은 <모던 자바스크립트 딥다이브>를 참고해주세요.)

모던 자바스크립트 딥다이브

Q1 렉시컬 스코프란 무엇인가요?

  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.
  • 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다.

  • 함수는 자신의 내부 슬롯 [[Environment]]에 자신의 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
  • 함수 객체의 내부 슬롯 [[Environment]]에는 함수 정의가 평가된 시점, 즉 전역 코드 평가 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 저장된다.
  • 함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다. 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다. 함수 객체는 내부 슬롯 [[Environment]]에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다. (자바스크립트의 모든 함수는 자신의 상위 스코프를 기억한다.)

Q2 함수 코드 평가 순서에 대해 설명하세요.

  • 함수 실행 컨텍스트 생성 > 함수 렉시컬 환경 생성 > 함수 환경 레코드 생성 > this 바인딩 > 외부 렉시컬 환경에 대한 참조 결정

Q3 클로저에 대해 설명하세요.

  • 콜 스택에서 이미 팝되어 제거된 실행 콘텍스트의 지역변수가 리턴된 내부 함수에 의해 부활이라도 한 듯 동장되는 현상.
  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있고, 이러한 중첩 함수를 클로저라고 부른다.
const x = 1;

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

const innerFunc = outer();
innerFunc(); // 10
  • outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다. outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 있고 inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다. 가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.

  • 이론적으로 모든 함수는 클로저다.
  • 하지만,상위 스코프의 어떤 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다. 참조하지 않는 식별자를 기억하는 것은 메모리 낭비이기 때문!
  • 더 나아가, 상위 스코프의 식별자를 참조하지만 외부 함수보다 일찍 소멸돼 생명주기가 종료되는 내부함수 역시 클로저의 본질에 부합하지 않는다.
  • 결국 클로저는 중첩함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.

  • 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다. 클로저란 "함수가 자유변수에 대해 닫혀있다"라는 의미이다. 클로저는 결국 "자유변수에 묶여있는 함수"라고 할 수도 있겠다.

Q4 클로저는 어디에 활용되나요?

  • 클로저는 상태를 안전하게 변경하고 유지하기 위해 사용한다. 클로저를 사용하면 상태를 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용할 수 있다.
// 카운터 함수 만들기
function counter() {
  let num = 0;
  
  return {
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    }
  }
}
  • num은 외부에서 접근이 불가하다.
  • 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.

  • 클로저 함수를 포함하고 있는 외부함수를 호출해 클로저를 반환할 떄 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.
// 위 카운터 함수를 두 번 호출해 할당하면...!
const counter1 = counter();
const counter2 = counter();

// 두 함수의 환경 슬롯 내부에 저장된 num 값은 각각 서로에게 영향을 끼지치 않는다.
  • 즉, 전역 변수 counter1과 counter2에 할당된 함수는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 자유변수 num을 공유하지 않아 카운터의 증감이 연동되지 않는다.

  • 클로저는 캡슐화와 정보은닉을 위해서도 활용된다.
  • 캡슐화란 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다.

그 외 활용사례

 const funcs = [];

for(let i = 0; i < 3; i++){
  funcs[i] = function() { return i };
}

for(let i  = 0; i < funcs.length; i++){
  console.log(funcs[i]()); // 0 1 2
}
  • 이 때 var를 사용해주면 안 된다. var와 같은 경우 함수 스코프를 따르기 때문에 for문의 코드블록이 무시 된다.
profile
부정확한 정보나 잘못된 정보는 댓글로 알려주시면 빠르게 수정토록 하겠습니다, 감사합니다!

0개의 댓글