[10분 테코톡] 🍧 엘라의 Scope & Closure(16분) 를 보고 정리한 글입니다 :)


1️⃣ 스코프

🤔 스코프란?

자바스크립트를 포함한 모든 프로그래밍 언어에서 가장 기본적이고 중요한 개념입니다.

function add(x,y) {
  console.log(x,y) //9
  return x+y;
}

add(3,6);

console.log(x,y) // ReferenceError 🚨

변수 이름, 함수 이름, 클래스 이름과 같은 '식별자'
본인이 선언 된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것


☑️ 코드 전체는 전역과 지역으로 구분할 수 있습니다.

  • 어떤 변수가 전역에서 선언된다면 해당 변수는 전역 스코프를 받게 됩니다.

  • 어떤 변수가 지역에서 선언된다면 해당 변수는 지역 스코프를 받게 됩니다.


그렇다면 참조하려는 변수가 전역에는 있는데 지역에는 없고 또 이렇게 보니까 지역에는 있는데 전역에도 있고 이런 경우에는 코드는 어떤 변수를 참조해야 할까요?


⛓ 스코프 체인

함수는 전역에서 정의가 될 수도 있지만 어떠한 함수 내부 몸체에서 정의될 수도 있습니다. 이를 '함수의 중첩' 이라고 합니다.

  • 함수 내부에서 정의된 함수를 중첩 함수라고 하고 중첩함수를 포함하는 함수를 '외부 함수' 라고 합니다.

  • 이처럼 함수가 중첩이 된다면 각각 함수의 지역 스코프도 중첩이 될 수 있습니다.

  • 이는 스코프가 함수의 중첩에 의해 계층적인 구조를 가질 수 있다는 것을 의미합니다.


var x = 'I am global x';

// 👓 outer 스코프 시작 
function outer() {
  var y = 'I am y of outer function'
  console.log(x); // ① 'I am global x';
  console.log(y); // ② 'I am y of outer function'
  
  // 🕶 inner 스코프 시작 
  function inner() {
    var x = 'I am x of inner function';
    console.log(x); // ③ 'I am x of inner function';
    console.log(y); // ④ 'I am y of outer function'
  }
  // 🕶 inner 스코프 종료 
  
  inner();
}
// 👓 outer 스코프 종료 

outer();
console.log(x); // ⑤ 'I am global x';
console.log(y); // ⑥ ReferenceError 🚨

🕶 inner 지역 스코프 → 👓 outer 지역 스코프 → 🌎 전역 스코프

  • inner 함수는 outer 함수의 내부에서 선언이 되었기 때문에 inner 함수의 상위 스코프는 outer 함수의 스코프 입니다.

  • 전역에서 정의된 outer 함수 스코프의 상위 스코프는 전역스코프 입니다.


이처럼 스코프가 계층적으로 연결이 되어 있는 것을 '스코프 체인' 이라고 합니다. 스코프 체인은 물리적으로 존재합니다. 변수를 참조할때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조합니다. 현재 함수가 어떤 변수를 참조하려고 하는데 만약 내 스코프 안에 원하는 변수가 없다면

  1. 한 스코프 위로 올라갑니다. 그런데 올라온 스코프에도 원하는 변수가 없다면?

  2. 또 한 스코프 위로 올라갑니다. 이런 방식으로 계속 타고 올라가면 최상위 스코프인 전역 스코프 까지 올라가게 됩니다.

  3. 그런데도 전역 스코프에도 내가 원하는 변수가 없으면 'ReferenceError 🚨'를 출력합니다.


👻 스코프 종류

☑️ 스코프 레벨은 '블록 레벨 스코프'와 '함수 레벨 스코프'로 구분

⚙️ 블록 레벨 스코프

  • 모든 코드 블록(함수, if문, for문, while문, try/catch문 등)내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없습니다. 즉, 코드 블록 내부에서 선언한 변수는 지역 변수입니다.

⚙️ 함수 레벨 스코프

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

☑️ 상위 스코프가 결정되는 시점을 기준으로 '동적 스코프'와 '정적 스코프'로 구분

⚙️ 정적 스코프

  • 함수가 정의되는 시점에 상위 스코프가 결정되는 것을 정적 스코프(렉시컬 스코프 Lexical scope)라고 합니다.

  • 자바스크립트는 렉시컬 스코프를 따르기 때문에 함수는 생성이 되자마자 상위 스코프가 결정이 되고,
    이후에 해당 함수에 의해 함수 객체가 생성되면 해당 함수 객체는 본인의 상위 스코프를 항상 알 수 있게 됩니다.

  • 이렇게 해당 함수가 상위 스코프를 항상 알 수 있게 되는 이유는 자바스크립트에서 함수는 생성되면서 자신의 내부 슬롯에 '상위 스코프의 참조'를 저장하기 때문입니다.

⚙️ 동적 스코프

  • 함수가 어디서 호출 되는지에 따라 동적으로 상위 스코프가 결정되는 것을 말합니다.

함수 호출 → 실행 컨텍스트 생성(stack push) → 렉시컬 환경 생성

함수가 호출이 되면 자바스크립트 엔진에 의해 다음과 같은 진행이 됩니다.

  1. 호출된 함수에 실행 컨텍스트를 생성하고 이를 실행 컨텍스트 스택에 push 합니다.

  2. 함수는 본인의 렉시컬 환경을 생성합니다.

    • 렉시컬 환경은 어떠한 코드가 어디서 실행이 되고 본인 주변에 어떤 코드들이 있는지 대체적인 정보를 담고 있는 환경이라고 할 수 있습니다.

    • 함수 본인 내부의 식별자 그리고 식별자에 바인딩 된 값 등을 기록하고 있는 하나의 자료구조 입니다.

  3. 코드의 실행이 끝나면 실행 컨텍스트 스택에서 해당 컨텍스트를 pop하여 제거합니다.


2️⃣ 클로저

"클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다."

사실 무슨 코드를 작성하든간에 거의다 클로저 관계로 이루어질 수밖에 없다.

const x = 1;

function outer() {
  const x = 10;
  
  // 클로저 😎
  const inner = function () {
    console.log(x);
  };
  
  return inner;
}

const ella = outer(); 
ella(); // 10
  1. outer 함수는 중첩함수 inner를 ella에게 반환하면서 생명주기를 마감합니다.

  2. outer함수가 종료되면 실행 컨텍스트는 실행 컨텍스트 스택에서 제거가 됩니다.

  3. 따라서 outer 함수의 지역 변수 x 또한 생명주기를 동시에 마감하게 됩니다.
    우리는 더이상 지역변수 x에 접근할 방법이 없어보입니다..

  4. 하지만! 코드를 실행해보면 10이라는 값이 나오게 됩니다.


바로 이유는 클로저 때문입니다 😮

중첩함수 inner가 이미 생명주기를 마감한 outer 함수 즉 내부 함수의 지역 변수 x를 참조할 수 있다면 이때 inner를 '클로저' 라고 합니다.

outer 함수 종료되는 모습을 살펴보도록 하겠습니다.

1. outer 함수의 생명주기가 끝났으니 실행 컨텍스트 스택에서 제거가 됩니다.


2. outer 함수는 엘라한테 inner 함수를 반환하면서 사라지기 때문에 지금부터 엘라는 inner 함수 객체를 참조합니다.

이 상황은 outer 함수가 실행 중에 있다가 생명주기를 마감하면서 최종적으로 종료되었을 때의 모습입니다. 실행 컨텍스트 스택을 보면 outer 함수는 완전하게 제거가 되었습니다. 하지만 outer 함수의 렉시컬 환경까지 손을 대지는 않습니다.

새로 생성된 화살표를 보면 ella는 inner 함수 객체를 참조하고 있고 inner 함수 객체는 본인의 내부 슬롯에 저장된 outer 함수의 렉시컬 환경을 참조하기 때문에 가비지 컬렉션의 대상이 되지 않습니다.

따라서 엘라에 의해 inner 함수를 다시 호출하면 outer 함수 안에 있는 10을 값으로 가지고 있는 변수 x를 다시 참조할 수 있게 됩니다.

  • inner 함수의 객체 기준 내부 함수는 생명주기를 마감하고 실행 컨텍스트 스택에서 전부 사라졌습니다.

  • 하지만 이 함수가 기억하고 있는 내부 슬롯에 저장된 상위 스코프에 의존하여 상위 스코프 내의 식별자를 참조할 수 있게 되는 것입니다. 그리고 이것이 바로 클로저라는 개념입니다.


📝 클로저 정리

  • 한 중첩 함수가 상위 스코프의 식별자를 참조하고 있고, 거기서 중첩 함수의 외부 함수보다 더 오래 살아 있다면 이 중첩 함수는 클로저 입니다.

  • 이러한 클로저는 본인의 상위 스코프에서 현재 참조하는 식별자만을 기억 하는데, 이렇게 클로저에 의해 참조된 변수를 '자유변수' 라고 합니다.

  • 따라서, 뭔가 닫혀 있다는 느낌이 드는 '클로저'라는 이름은 함수 본인이 기억하고 있는 자유 변수에 의해 '닫혀 있다, 같혀 있다, 즉 closed 되어 있다'라고도 생각할 수 있습니다.

  • 추가로 클로저는 하나의 state가 의도치 않게 변경되지 않도록 state를 안전하게 은닉하고 또는 특정 함수에게만 state 변경을 허용하기 위해 사용한다고 합니다.


📝 아래의 추가 예제는 필자가 클로저의 추가적인 이해를 돕기위해 작성해 둔 추가적인 예제입니다.

위의 본문을 읽으시고 예제들을 보시며 심화 학습을 하시면 좋을 것 같습니다.

💫 클로저의 이해를 돕는 예제

☑️ 은닉화를 가능하게 하는 코드

function closure() {
  let cnt = 0;
  
  function cntPlus() { // 1을 더해주는 함수 
    cnt += 1;
  }
  
  function setCnt(value) {
  	cnt = value;
  }
  
  function printCnt() { // closure 내부의 cnt를 호출하는 함수
    console.log(cnt);
  }
  
  return { cntPlus, setCnt, printCnt }
}

const cntClosure = closure();
cntClosure.printCnt(); // 0

cntClosure.cntPlus();
cntClosure.printCnt(); // 1

cntClosure.setCnt(100);
cntClosure.printCnt(); // 100

☑️ 왜 클로저는 이해하기 힘들까

// pure 키워드는 설명을 하기 위한 임의의 예시입니다. (자바스크립트에 없는 문법입니다 🚨)
pure function add1(a,b) { // 순수 함수
  return a+b;
}

let poison = 0;

function add2(a,b) { // 클로저 함수
  return a + b + poison;
}

다른 프로그래밍 언어는 클로저 함수와 그렇지 않은 함수를 이런식으로 명확하게 구분을 해서 제공을 하는 경우가 많습니다.

위의 예시에서는 가상의 언어를 예시로 들어서 pure라는 키워드를 사용해서 설명해보겠습니다. pure가 붙으면 클로저 함수가 아닌거고, pure가 없으면 클로저 함수라고 생각합니다.

pure 같은 키워드가 없는 자바스크립트는 사실상 다 클로저라고 합니다. 그래서 자바스크립트를 가지고 클로저를 이해하려고 하면 개념 구분을 하기가 사실 쉽지가 않아서 어렵게 느껴질 수 있습니다.

기본적으로 모든 함수가 클로저 기능을 가지고 있다는 건 코드를 처음 작성할 때는 상당히 편하지만, 작성한 코드를 나중에 다시 읽는 입장에서는 그렇지 않을 수 있습니다. 함수가 클로저 인지 아닌지 정확한 정보를 바로 제공 해주지 않기 때문에 이해할때 번거롭습니다.


☑️ 반복문과 비동기 함수

for (let i=0; i<100; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}
  1. setTimeout 안의 익명 함수 function에서 i를 찾는다.

  2. 찾을 수 없기 때문에 function 밖의 전역에서 탐색한다.
    찾은 i는 for문의 i가 된다.

  3. 1~99까지의 수가 순서대로 출력이 될 것 같지만, 100만 100번 출력된다 😮

이유: 컴퓨터의 연산속도는 생각보다 아주 빠릅니다. 연산이 모두 이루어진 이후에 값을 참조하기 때문에 그렇습니다. 함수 안의 변수는 '실행'될 때 값이 결정됩니다.


⚙️ 클로저를 사용해서 원하는 의도로 수정

for (let i=0; i<100; i++) {
  (function() {
  	setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })();
}
// 반복문은 100회 반복되므로 100개의 환경이 생길 것이고, 
// 100개의 서로 다른 환경에 100개의 서로 다른 j가 생긴다.


🌐 참조 링크

profile
느리지만 꾸준하게 💪🏻

0개의 댓글