JavaScript

[JS] 클로저

_doit 2024. 9. 22. 17:39
728x90
반응형

클로저를 이해하려면 먼저 자바스크립트가 변수의 유효범위를 어떻게 지정하는지, 즉 렉시컬 스코핑(Lexical Scoping) 개념을 이해해야 해요.

실행 컨텍스트(Execution Context)

실행 컨텍스트는 자바스크립트 코드가 평가되고 실행되는 환경을 추상적으로 설명한 개념이에요. 모든 자바스크립트 코드는 실행 컨텍스트 내에서 실행돼요.

 

☑️ 실행 컨텍스트 생성 과정

자바스크립트 코드가 실행되면, 그 코드가 어떤 스코프에서 실행되는지를 확인하고 그에 맞는 실행 컨텍스트가 생성돼요.

 

  • 전역 코드가 실행되면 전역 실행 컨텍스트가 생성되고,
  • 함수가 실행되면 그 함수만의 함수 실행 컨텍스트가 생성돼요.

각 실행 컨텍스트에는 해당 코드 블록에서 접근할 수 있는 변수, 함수, 상위 스코프에 대한 정보가 기록돼요.

let globalVar = 10;

function outer() {
  let outerVar = 20;

  function inner() {
    let innerVar = 30;
    console.log(globalVar); // 전역 스코프에 접근 (전역 실행 컨텍스트)
    console.log(outerVar);  // 외부 함수 스코프에 접근 (outer의 실행 컨텍스트)
    console.log(innerVar);  // 현재 함수 스코프에 접근 (inner의 실행 컨텍스트)
  }

  inner();
}

outer();

 

 

 

☑️ 실행 컨텍스트 주요 구성 요소

  1. 렉시컬 환경(Lexical Environment): 변수나 함수가 어디에 정의되었는지에 따라 변수나 함수에 접근할 수 있는 범위를 결정하며, 코드 실행 중 변수가 실시간으로 변경되는지를 관리해요.
  2. 변수 환경(Variable Environment): 함수나 블록이 실행될 때, 변수들이 처음 선언된 상태를 저장해요.
  3. This 바인딩: this 값을 설정해
더보기

VE(변수 환경)와 LE(렉시컬 환경)는 실행 컨텍스트가 처음 생성될 때는 동일한 내용을 담지만, 이후에는 VE는 스냅샷을 유지하고 LE는 실시간으로 변경사항을 반영해요.

결국, 실행 컨텍스트를 생성할 때, VE에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고 이후에는 주로 LE를 활용한답니다.

 

렉시컬 환경은 크게 두 가지 정보를 담고 있어요:

  • record (environmentRecord): 현재 스코프에서 선언된 변수나 함수 같은 식별자들을 저장하고 관리해요. 호이스팅(hoisting)이 일어나는 것도 이 기록 과정에서 발생
  • outer (outerEnvironmentReference): 현재 스코프보다 상위 스코프를 참조해요. 이 속성을 통해 현재 스코프에서 변수를 찾지 못하면 상위 스코프에서 변수를 검색할 수 있어요.

 

클로저

클로저는 함수가 선언될 때의 렉시컬 환경을 기억하는 기능이에요. 어떤 함수가 실행된 후에도 그 함수가 선언된 당시의 변수를 기억하고, 나중에 그 변수를 사용할 수 있는 걸 클로저라고 해요.

 

☑️   클로저의 주요 특징:

  • 데이터 은닉캡슐화 구현 가능
  • 함수 팩토리 생성
  • 비동기 처리에서의 활용

1) 클로저의 활용: 데이터 은닉과 상태 관리

let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
    // 카운트 상태를 1만큼 증가시킨다.
    return ++num;
};

console.log(increase());
// num = 100; // 치명적인 단점이 있어요.
console.log(increase());
console.log(increase());

 

위 코드에서는 num 변수가 외부에서 접근 가능해, 직접 수정할 수 있는 위험이 있어요.

따라서 상태를 안전하게 변경하고 유지하기 위해  외부에서는 직접 접근할 수 없게 해야한다.

// 카운트 상태 변경 함수 #2
const increase = function () {
  // 카운트 상태 변수
  let num = 0;

  // 카운트 상태를 1만큼 증가시킨다.
  return ++num;
};

// 이전 상태값을 유지 못함
console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1

이 코드에서는 num 변수가 함수 내부에 선언되어 있지만, 매번 함수가 호출될 때마다 새로운 num이 생성되기 때문에 이전 상태를 유지할 수 없어요.

이 문제를 해결하기 위해 클로저를 사용하면, 함수가 종료된 이후에도 상태를 유지할 수 있어요:

// 카운트 상태 변경 함수 #3
const increase = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저
  return function () {
    return ++num;
  };
})();

// 이전 상태값을 유지
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3

여기서 num 변수는 외부에서 직접 접근할 수 없고, 오직 클로저인 increase 함수에서만 값이 변경돼요. 이를 통해 상태를 안전하게 관리할 수 있어요

 

따라서 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding) 하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용돼요. 

 

2) 함수 팩토리 생성

아래 코드처럼 세금을 계산하는 로직이 존재한다고 가정해봅시다

const calculateTax = (amount, tax) => {
  return amount * tax;
}
const someTax1 = calculateTax(100, 0.19);
const someTex2 = calculateTax(200, 0.19);
const someTex2 = calculateTax(150, 0.25);

이 코드에서는 세금 종류금액을 매번 함수에 전달해야 해요. 이를 함수 팩토리로 개선할 수 있어요.

 

const createTaxCalculator = (tax) => {
  const calculateTax = (amount) => {
    return amount * tax;
  }
  return calculateTax;
}
const someTax3 = createTaxCalculator(0.19);
console.log(someTax3(100));
console.log(someTax3(200));

이제는 세금 비율을 한 번만 설정하고, 다른 금액들을 계속 계산할 수 있게 됐어요. 이렇게 하면 코드를 더 효율적으로 관리할 수 있어요.

 

3) 비동기 처리에서의 클로저 활용

function fetchData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`Data from ${url}`);
        }, 1000);
    });
}

function createDataFetcher(baseUrl) {
    const cache = new Map();

    return function(endpoint) {
        const url = `${baseUrl}/${endpoint}`;
        if (cache.has(url)) {
            console.log('Serving from cache');
            return Promise.resolve(cache.get(url));
        }

        return fetchData(url).then(data => {
            cache.set(url, data);
            return data;
        });
    };
}

const fetchFromAPI = createDataFetcher('<https://api.example.com>');

fetchFromAPI('users')
    .then(data => console.log(data))
    .then(() => fetchFromAPI('users'))  // 두 번째 호출은 캐시에서 제공됨
    .then(data => console.log(data));

위 코드는 클로저를 이용해 API 데이터를 캐시하고, 같은 데이터를 다시 요청할 때는 캐시된 데이터를 제공하는 방식이에요.

 

 

결론

실행 컨텍스트, 렉시컬 환경, 그리고 클로저는 자바스크립트에서 변수를 어떻게 관리하고, 함수가 어떤 스코프에서 실행되는지를 이해하는 데 필수적인 개념이에요. 클로저는 특히 상태 관리비동기 처리에서 강력한 도구로 사용될 수 있고, 코드의 안정성을 높이는 데 중요한 역할을 해요.

 

728x90
반응형