방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2202-12-04: 생활코딩 - var, let, const와 닫기/폐쇄(Closure)

목차

 1️⃣ var

 2️⃣ let과 const

 3️⃣ 닫기, 폐쇄 (Closure)

1️⃣ var

  • 대다수의 프로그래밍에 전통적으로 변수를 선언하기 위한 연산자
  • var를 통해 선언한 변수는 전역 스코프에 할당된다.
  • var 연산자는 로컬 혹은 함수와 전역 스코프에 선언될 수 있다.
    • 그러나 블록과 스크립트 스코프에는 선언될 수 없다.
  • var 연산자를 찾을 시, 로컬 혹은 함수 스코프에 선언되어있다면 해당 값을 먼저 찾는다.
    • 없다면, 실행 컨텍스트의 순서에 따라 전역 스코프에서 해당 값을 찾게 된다.
  • var 연산자를 이용해 변수에 값을 재할당할 수 있지만, 같은 이름을 가진 변수를 재선언할 수도 있다.

  • var를 이용해 변수를 선언하는 데에는 문제점이 있다.

    • 이미 전역 스코프 안에 있는 속성이나 메소드의 이름으로 변수명을 정하게 된다면 동일한 이름을 가진 속성 혹은 메소드들의 내용이 원치 않게 불러오게 될 수 있다.
    • 뿐만 아니라 우리가 선언한 이름을 전역 스코프 내에서 찾을 경우, 동일한 이름을 선언했다면 찾기가 어려울 수 있다.
  • 위의 문제들을 해결하기 위해 ES6에 새롭게 변수를 선언하는 연산자 두 개를 만들어 냈다.
    • 그것이 아래의 letconst이다.

2️⃣ let과 const

  • const
    • 변수의 값이 바뀌지 않는 값인 상수로 선언하기 위한 연산자
  • let
    • 변수의 값이 상황에 따라 바뀔 수 있는 값이라고 선언하기 위한 연산자
  • 두 연산자는 모두 지역 및 함수, 블록, 스크립트 스코프선언될 수 있다.
    • 그러나 전역 스코프에선 선언될 수 없다.
  • 두 연산자는 지역 및 함수, 블록 스코프에 선언되어 있다면 해당 값을 먼저 찾는다.
    • 없을 경우, 실행 컨텍스트의 순서에 따라 스크립트 스코프에서 해당 값을 찾게 된다.
  • let 연산자를 이용해 변수에 값을 재할당할 수 있지만, 같은 이름을 가진 변수를 재선언할 수도 있다.
    • 단, const 연산자는 상수를 가리키므로 재할당, 재선언 모두 다 불가능하다.
  • 블록 스코프(Block Scope)스크립트 스코프(Script Scope)

    • 블록 스코프 (Block Scope)
      • 조건문, 반복문 등에 쓰이는 괄호 안의 범위를 가리킴.
      • 함수도 일종의 블록 스코프라 말할 수 있지만, 함수를 선언하면 별도로 로컬 스코프로 따로 분리되어 가리키므로 함수는 함수 스코프로 따로 부르기도 한다.
    • 스크립트 스코프 (Script Scope)
      • ES6 이후, let과 const를 통해 변수를 선언할 시에 들어가게 되는 전역 스코프를 대체한 스코프.
      • JavaScript에서의 전역 스코프는 브라우저, 즉 window 객체를 가리킨다.
      • 이 경우, 많은 변수를 통해 속성이나 메소드 등이 정의된 상황이므로 이 브라우저 객체에 선언된 요소들을 건드리면 안 될 상황이었다.
      • 이런 window 객체를 보호하되, 전역 스코프와 동일하게 사용자가 정의내린 데이터들을 사용할 수 있게 해주는 스코프가 필요해졌는데 이렇게 생겨난 것이 스크립트 스코프(Script Scope)이다.
  • var와 let, const의 차이점을 표로 정리하면 아래와 같다.

| | var | let | const | | --- | --- | --- | --- | | 전역 스코프 (Global Scope) | YES | NO | NO | | 스크립트 스코프 (Script Scope) | NO | YES | YES | | 함수 스코프 (Function Local Scope) | NO | YES | YES | | 블록 스코프 (Block Scope) | NO | YES | YES | | 변수의 재선언 | YES | NO | NO | | 변수의 재할당 | YES | YES | NO |

3️⃣ 닫기, 폐쇄 (Closure)

아래의 코드를 보자.

let l0 = 'l0';

function fn2() {
    let l2 = 'l2';
    console.log(l0, l1, l2);
}

function fn1() {
    let l1 = 'l1';
    console.log(l0, l1);
    fn2();
}

fn1();

위의 코드의 마지막줄처럼 함수 fn1을 실행시키면, 함수가 작동을 하다가 함수 fn2가 실행되는 도중에 에러가 출력되는 것을 볼 수 있다.

스코프를 확인해보면 위의 코드는 아래처럼 나타난다.

  • 함수 fn1의 스코프에서는 fn2가 포함되어 있지 않다.

    • 따라서, 함수 fn2 안의 변수 l2을 찾을 수 없다.

  • 함수 fn2는 함수 fn1 안에서 실행되도록 설정되어 있으나 함수 fn1과는 독립적인 스코프를 가진다.

    • 따라서, 함수 fn1 안의 변수 l1을 함수 fn2에서는 찾을 수 없다.

  • 즉, 함수 fn2가 함수 fn1 안에서 호출됐다 하더라도 fn1fn2 안에서 정의된 변수에 접근할 수 없다.

만약, 위의 코드를 바라는 대로 함수 fn2에 선언된 변수 l2를 함수 fn1 안에 접근하려면 어떻게 해야할까?

아래의 코드를 보자.

let l0 = 'l0';

function fn1() {
    function fn2() {
        let l2 = 'l2';
        console.log(l0, l1, l2);
    }

    let l1 = 'l1';
    console.log(l0, l1);
    fn2();
}

fn1();

아래의 코드를 통해 함수 fn1을 실행하면 처음 썼던 것과 다르게 console.log가 문제 없이 작동한 것을 볼 수 있을 것이다.

위의 이미지대로 코드를 실행하는 순서를 보면 함수 fn1 안에 fn2을 선언한 뒤, 첫 console.log를 부르고 난 뒤에 함수 fn2를 실행시켜지면서 실행 컨텍스트 순서에 따라 내부의 함수 fn2에 접근하는 걳을 볼 수 있다.

이때, fn1의 범위는 닫기/폐쇄/Closure로 지정되고 현재의 로컬 스코프가 fn2로 지정된 것이 보일 것이다.

이렇게 하면 fn2에서는 부모가 되는 외부 함수 fn1에 있는 변수에도 접근할 수 있으므로 우리가 원하는 대로 두 번째의 console.log도 작동하는 것을 볼 수 있다.

이처럼, 함수의 유효 범위는 함수가 어디서 호출됐냐가 아닌 함수가 어디서 정의됐냐에 따라 달라지게 되는데 이것이 이전에 배웠던 내용인 렉시컬 스코프(Lexical Scope)를 말한다.

그리고, 이런 식으로 외부 함수 안에 내부 함수를 정의하면 내부 함수는 외부 함수의 요소에 접근할 수 있는데 이것을 우리는 클로저(Closure), 폐쇄, 닫기라고 말한다.

이렇게 클로저의 개념을 안다면 아래와 같이 이용할 수 있다.

function plusFactory(init) {
    function plus(int){
        return init + int;
    }
    return plus;
}

let doPlus1 = plusFactory(1); // init = 1
console.log(doPlus1(1)); // 2
console.log(doPlus1(2)); // 3

let doPlus2 = plusFactory(2); // init = 2
console.log(doPlus2(1)); // 3
console.log(doPlus2(2)); // 4

이처럼 변수에 외부 함수를 선언해줌으로서 이를 통해 내부 함수 값에 접근해 내부 함수의 최종 값을 반환해줄 수 있다.

그리고 doPlus1doPlus2와 같이 함수가 만들어지는 시점에서 유효 범위가 정해지므로, 내부 함수에 접근하더라도 외부 함수였던 plusFactoryinit에도 접근할 수 있다.

📁 참고 자료

JavaScript - var VS let VS const

JavaScript - closure