[JS] 몰래보는 Scope & Closure

sjoleee·2022년 9월 29일
1
post-thumbnail

Scope란?

변수 이름, 함수 이름, 클래스 이름과 같은 식별자는 본인이 선언된 위치에 따라 다른 코드에서 자신을 참조할 수 있는 유효 범위가 결정된다. 이를 스코프라 한다.

스코프라는 개념이 없다면, 같은 이름을 갖는 변수는 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다.

스코프의 종류

🎯 스코프 레벨에 따라

🔹 블록 레벨 스코프

대부분의 프로그래밍 언어는 블록 레벨 스코프.
if문, for문, while문, try~catch문, 함수 등 모든 코드 블록이 지역 스코프를 만든다.

🔹 함수 레벨 스코프

자바스크립트는 함수만이 지역 스코프를 만들 수 있었다.
es6이후부터 let const를 사용하면 모든 블록 단위에 대한 스코프를 가질 수 있게 되었다.

🎯 상위 스코프 결정 시점에 따라

🔹 동적 스코프

어디서 호출되는지에 따라 상위 스코프가 결정되는 스코프.

프로그램 런타임 도중, 실행 컨텍스트나 호출 컨텍스트에 의해 스코프가 결정되는 것을 의미함.

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

어디서 정의되는지에 따라 상위 스코프가 결정되는 스코프.
자바스크립트는 정적 스코프를 따른다.

전역 스코프의 전역 변수는 어디서든지 참조할 수 있다.
지역 스코프의 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 참조할 수 있다.

스코프 체인?
함수 안에서 함수를 정의할 경우, 내부에 있는 함수를 중첩 함수, 외부에 있는 함수를 외부 함수라고 한다.
함수가 중첩된다는 것은 곧 지역 스코프 역시 중첩될 수 있으며, 그에 따라 스코프는 계층적 구조를 갖는다(스코프 체인)
이때, 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라 한다.

자바스크립트 엔진은 변수를 참조할 때 스코프 체인을 통해 출발점에서 상위 스코프로 이동하며 변수를 검색한다.
이를 통해 상위 스코프에서 선언한 변수를 하위 스코프에서도 참조할 수 있다.

let x = "global x";
let y = "global y";

function outer() {
  let z = "outer's local z";

  console.log(x); //global x
  console.log(y); //global y
  console.log(z); //outer's local z

  function inner() {
    let x = "inner's local x";

    console.log(x); //inner's local x
    console.log(y); //global y
    console.log(z); //outer's local z
  }

  inner();
}

outer();

console.log(x); //global x
console.log(y); //global y
console.log(z); //ReferenceError: z is not defined

전역 스코프
x 👉 "global x"
y 👉 "global y"
outer 👉 function object

outer 지역 스코프
z 👉 "outer's local z"
inner 👉 function object

inner 지역 스코프
x 👉 "inner's local x"

inner에서 console.log(y) console.log(z)를 실행하기 위해 상위 스코프를 탐색하여 inner 내부에서 정의되지 않은 y, z를 참조하게 되는 것이다.

이렇게 함수가 정의되는 시점에 상위 스코프가 결정되는 것을 정적 스코프(렉시컬 스코프)라고 한다.
또한, 자바스크립트에서 함수는 태어나면서 본인의 내부 슬롯에 상위 스코프에 대한 참조를 저장한다.

Closure란?

중첩 함수에서 상위 스코프의 식별자를 참조하고 있고, 외부 함수보다 더 오래 살아있는 하위 스코프클로져라 부른다.

함수 호출
👉 실행 컨텍스트 생성, 실행 컨텍스트 스택에 push
👉 렉시컬 환경(포함하는 식별자, 식별자에 바인딩 된 값, 상위 렉시컬 환경에 대한 참조) 생성
👉 코드 실행 종료시 실행 컨텍스트 스택에서 pop하여 제거

const x = 1;

function outer() {
  const x = 10;

  function inner() {
    console.log(x); //10
  }

  return inner;
}

const run = outer();
run();

위 예시에서, outerrun에게 inner를 반환하면서 생명주기를 마감한다.
동시에 outer의 지역변수인 x도 생명주기를 마감한다.
그런데 어떻게 실행결과는 10일까?

중첩함수 inner클로져로, 생명주기를 마감한 외부함수의 지역변수 x를 참조할 수 있기 때문이다.
(이때, 클로져에 의해 참조된 변수를 자유변수라 부른다.)

이러한 클로져는 하나의 state가 의도치 않게 변경되지 않도록 state를 안전하게 은닉하고,
특정 함수에게만 state 변경을 허용하기 위해 사용한다.

👉 useState Hook의 동작 원리

function useState(initialValue) {
  let _val = initialValue; // _val은 useState에 의해 만들어진 지역 변수입니다.

  function state() {
    // state는 내부 함수이자 클로저입니다.
    return _val; // state()는 부모 함수에 정의된 _val을 참조합니다.
  }

  function setState(newVal) {
    // setState 내부 함수이자 클로저입니다.
    _val = newVal; // _val를 노출하지 않고 _val를 변경합니다.
  }

  return [state, setState]; // 외부에서 사용하기 위해 함수들을 노출
}

var [foo, setFoo] = useState(0); // 배열 구조분해 사용

console.log(foo()); // 0 출력 - 위에서 넘긴 initialValue

setFoo(1); // useState의 스코프 내부에 있는 _val를 변경합니다.
console.log(foo()); // 1 출력 - 동일한 호출이지만 새로운 initialValue

참고한 글
https://hewonjeong.github.io/deep-dive-how-do-react-hooks-really-work-ko/
https://www.youtube.com/watch?v=PVYjfrgZhtU
모던 자바스크립트 Deep Dive

profile
상조의 개발일지

0개의 댓글