FoO의 개발 블로그

[JS 개념정리 3] scope, 스코프 체인, 호이스팅, TDZ, 클로저 본문

Programming/FE

[JS 개념정리 3] scope, 스코프 체인, 호이스팅, TDZ, 클로저

FoO__511 2024. 6. 30. 21:43

JavaScript의 scope(스코프)는 변수와 함수의 접근성과 생존 기간을 결정하는 규칙이다.

 

스코프의 종류

  • 전역 스코프(Global Scope)
  • 함수 스코프(Function Scope)
  • 블록 스코프(Block Scope)
  • 렉시컬 스코프(Lexical Scope)

스코프와 관련된 개념들

  • 스코프 체인(Scope Chain)
  • 호이스팅(Hoisting)
  • 클로저(Closure)

 

전역 스코프(Global Scope)

  • 코드의 가장 바깥쪽에 선언된 변수나 함수
  • 어디서든 접근 가능
  • 과도한 사용은 네임스페이스 오염을 일으킬 수 있음

 

함수 스코프(Function Scope)

  • 함수 내부에 선언된 변수
  • var 키워드로 선언된 변수에 적용
  • 해당 함수 내에서만 접근 가능
function exampleFunction() {
    var functionScopedVar = "I'm function-scoped";
    console.log(functionScopedVar); // 정상 작동
}

console.log(functionScopedVar); // ReferenceError

 

  • let과 const로 선언된 변수도 함수 스코프의 영향을 받지만 var와는 다른 특성을 가짐
    • 블록 스코프를 가짐
    • 함수 스코프도 적용(함수도 하나의 블록이기 때문)
    • var와 다르게 블록 레벨 스코프를 존중
  • 예를 들어, 함수안의 블럭(if문 등)에서 var로 변수가 선언된 경우 해당 함수 내의 어디서든 해당 변수에 접근이 가능하지만, let과 const로 선언된 변수는 블럭을 벗어나면 접근할 수 없게 됨.
function blockScopeExample() {
    if (true) {
        var varVariable = "I'm var";
        let letVariable = "I'm let";
        const constVariable = "I'm const";
    }
    
    console.log(varVariable);      // "I'm var" (함수 스코프)
    console.log(letVariable);      // ReferenceError (블록 스코프)
    console.log(constVariable);    // ReferenceError (블록 스코프)
}

blockScopeExample();

 

 

블록 스코프(Block Scope)

  • ES6에서 도입된 let과 const 키워드로 선언된 변수에 적용
  • 가장 가까운 중괄호 {} 내에서만 접근 가능
if (true) {
    let blockScopedVar = "I'm block-scoped";
    const alsoBlockScoped = "Me too";
    console.log(blockScopedVar); // 정상 작동
}

console.log(blockScopedVar); // ReferenceError

 

 

렉시컬 스코프(Lexical Scope)

  • 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정
  • 클로저(Closure)와 밀접한 관련이 있음
function outer() {
    var outerVar = "I'm from outer";
    
    function inner() {
        console.log(outerVar); // 'outer' 함수의 변수에 접근 가능
    }
    
    inner();
}

outer();

 

 

스코프 체인(Scope Chain)

  • 내부 스코프에서 외부 스코프로 변수를 찾아가는 과정
  • 가장 가까운 스코프부터 검색하여 전역 스코프까지 올라감
var globalVar = "global";

function outer() {
    var outerVar = "outer";
    
    function inner() {
        var innerVar = "inner";
        console.log(innerVar, outerVar, globalVar);
    }
    
    inner();
}

outer(); // "inner outer global"

 

 

호이스팅(Hoisting)

  • 변수와 함수 선언이 스코프의 최상단으로 끌어올려지는 것처럼 동작
  • var로 선언된 변수와 함수 선언문에 적용
  • let과 const는 블록 스코프를 가지며 호이스팅 되지 않음(TDZ(Temporal Dead Zone)으로 인해 초기화 전 접근 불가)
console.log(hoistedVar); // undefined (에러가 발생하지 않음)
var hoistedVar = "I'm hoisted";

hoistedFunction(); // 정상 작동
function hoistedFunction() {
    console.log("I'm a hoisted function");
}

 

 

TDZ(Temporal Dead Zone)

ECMAScript 6(ES6)에서 let과 const 키워드와 함께 도입된 개념이다. 변수가 선언된 위치부터 초기화되기 전까지의 코드 영역을 가리킨다.

 

  • 변수 접근 제한
    • 변수가 선언되기 전에 접근하는 것을 방지
    • var의 호이스팅으로 인한 혼란을 줄이고 코드의 예측 가능성을 높이기 위함
  • 적용 대상
    • let과 const로 선언된 변수에 적용
    • 클래스 선언에도 적용
  • 동작 방식
    • 변수가 스코프에 들어가는 시점부터 선언되는 시점까지 TDZ에 있다고 봄
    • 이 기간 동안 변수에 접근하려고 하면 ReferenceError 발생
{
    // TDZ 시작
    console.log(x); // ReferenceError
    let x = 5;      // TDZ 종료, x 초기화
    console.log(x); // 5 (정상 작동)
}

{
    // TDZ 시작
    const func = () => console.log(x);
    // 여기서 func를 호출하면 에러 발생
    let x = 5;      // TDZ 종료, x 초기화
    func();         // 5 (정상 작동)
}

 

TDZ의 이점

  • 버그 예방: 변수를 선언하기 전에 사용하는 실수를 방지
  • 코드 명확성: 변수가 어디서 선언되었는지 명확히 알 수 있음
  • const의 의미 강화: const로 선언된 변수가 항상 초기값을 가지도록 보장

 

클로저(Closure)

클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다.

다르게 말하면, 함수가 자신이 선언된 스코프의 변수들에 접근할 수 있는 메커니즘이다.

 

기본 개념

  • 내부 함수가 외부 함수의 변수에 접근할 수 있다.
  • 외부 함수가 반환된 후에도 내부 함수는 외부 함수의 변수를 계속 참조할 수 있다.
function outerFunction(x) {
    let y = 10;
    function innerFunction() {
        console.log(x + y);
    }
    return innerFunction;
}

const closure = outerFunction(5);
closure(); // 출력: 15

 

데이터 프라이버시

  • 클로저를 사용해 private 변수를 흉내낼 수 있다.
function createCounter() {
    let count = 0;
    return {
        increment: function() { count++; },
        decrement: function() { count--; },
        getCount: function() { return count; }
    };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 출력: 2
console.log(count); // ReferenceError: count is not defined

 

위 예에서 createCounter 함수가 종료 됐음에도 내부 함수에서 여전히 createCounter 스코프에 있는 count 변수에 접근할 수 있다. 내부 함수를 제외하곤 count 변수에 접근할 수 있는 방법은 없다.

 

함수 팩토리

  • 클로저를 사용해 다양한 함수를 생성할 수 있다.
function multiplyBy(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 출력: 10
console.log(triple(5)); // 출력: 15

 

외부 함수가 반환된 후에도 내부 함수는 외부 함수의 변수를 계속 참조할 수 있기 때문에 선언시 사용된 factor 변수를 계속하여 사용할 수 있다.

모듈 패턴

  • 클로저를 사용해 private 멤버와 public 멤버를 가진 모듈을 만들 수 있다.
const myModule = (function() {
    let privateVar = 0;

    function privateFunction() {
        console.log('private function');
    }

    return {
        publicVar: 1,
        publicFunction: function() {
            privateVar++;
            privateFunction();
            console.log(privateVar);
        }
    };
})();

myModule.publicFunction(); // 출력: private function, 1
console.log(myModule.publicVar); // 출력: 1
console.log(myModule.privateVar); // undefined

 

이벤트 핸들러와 콜백

  • 비동기 작업에서 클로저를 활용할 수 있다.
function setupButton(label) {
    let count = 0;
    const button = document.createElement('button');
    button.textContent = label;
    button.addEventListener('click', function() {
        count++;
        console.log(`${label} clicked ${count} times`);
    });
    document.body.appendChild(button);
}

setupButton('Button A');
setupButton('Button B');

 

클릭시 동작하는 콜백 함수에서 외부 함수의 변수인 label과 const에 계속해서 접근이 가능하다.

 

클로저 사용시 주의사항

  • 메모리 사용: 클로저는 외부 변수를 참조하므로 메모리를 더 사용할 수 있다.
  • 가비지 컬렉션: 클로저로 인해 참조된 변수는 가비지 컬렉션이 되지 않는다.
  • 성능: 위 이유로 과도한 클로저 사용은 성능에 영향을 줄 수 있다.