새소식

반응형
언어/자바스크립트(JavaScript)

[JavaScript] 함수 스코프(Function Scope)와 클로저(Closure)

2024.02.07
  • -
반응형

자바스크립트는 웹 개발에서 광범위하게 사용되는 다재다능하고 강력한 프로그래밍 언어입니다. 자바스크립트의 고유한 기능 중 하나인 클로저는 매우 강력한 기능을 제공하지만 이를 처음 접했을 때는 다소 이해하기 어려울 수 있는 개념입니다.

 

이 글에서는 자바스크립트 클로저의 작동 방식과 사용하는 예제를 살펴보면서 자바스크립트 클로저의 세계에 대해 자세히 알아보는 시간을 가져볼 것입니다.

 

1. 함수 스코프(Function scope)

자바스크립트에서 변수함수에는 프로그램 내에서 액세스할 수 있는 위치를 결정하는 특정 '범위(scope)'가 있습니다. 이는 코드를 작성하고 구성하는 방법뿐만 아니라 문제를 디버깅하고 성능을 최적화하는 방법에도 영향을 줄 수 있습니다.

 

함수 스코프는 JavaScript의 가장 기본적인 개념 중 하나로, '함수 내에서 선언된 변수는 명시적으로 반환되거나 다른 함수로 전달되지 않는 한 해당 함수 내에서만 액세스할 수 있다'는 개념을 말합니다.

 

예를 들어, 아래를 코드를 볼까요?

function myFunction() {
  var x = 1;
  console.log(x); // Output: 1
}

console.log(x); // Output: ReferenceError: x is not defined

 

이 예제에서 변수 xmyFunction 함수 내에서 선언되었기 때문에 해당 함수 내에서만 액세스할 수 있습니다. 함수 외부에서 x를 로깅하려고 하면 x가 전역 범위에 정의되지 않았기 때문에 ReferenceError가 발생하는 것을 볼 수 있을 겁니다.

이러한 동작은 의도적인 것으로, 코드에서 이름 충돌 및 기타 문제를 방지하는 데 도움이 될 수 있습니다. 또한 서로 간섭하지 않고 다른 함수에 같은 이름의 변수를 만드는 경우에도 이러한 개념을 사용할 수 있습니다.

그리고 변수가 더 높은 스코프에서 정의되었다면, 함수 바깥에서 선언된 변수에도 액세스할 수 있다는 점에 주목해야 합니다. 예를 들어:

var y = 2;

function myFunction() {
  console.log(y); // Output: 2
}

myFunction();

 

이 경우 y는 전역 범위에서 정의되었으며, myFunction 함수에서 해당 변수에 액세스할 수 있습니다. 이는 외부 변수나 구성에 따라 달라지는 재사용 가능한(reusable) 코드를 만드는 데 유용할 수 있습니다.

 


함수 스코프를 이해하는 것은 자바스크립트 학습의 중요한 첫 단계이며, 보다 효율적이고 유지 관리하기 쉬운 코드를 작성하는 데 도움이 될 수 있습니다.

 

다음 섹션에서는 로컬전역 스코프에서 변수를 사용하는 방법과 블록 스코프와의 이름 충돌을 피하는 방법을 살펴보겠습니다.

 

 

1-1. 로컬 및 전역 스코프의 변수를 사용하는 법

변수는 정의 위치에 따라 다양한 스코프로 선언할 수 있습니다. 로컬 변수와 전역 변수를 사용하는 방법을 이해하면 보다 모듈적이고 유연한 코드를 작성하는 데 도움이 될 수 있습니다.

 

지역 변수(Local variables)

로컬 변수는 함수 내부에서 선언된 변수로, 해당 함수 내에서만 액세스할 수 있습니다. 로컬 변수는 특정 컨텍스트 내에서만 필요한 임시 값이나 중간 결과를 저장하는 데 자주 사용됩니다. 예를 들어:

function myFunction() {
  var x = 1; // local variable
  console.log(x); // Output: 1
}

myFunction();

console.log(x); // Output: ReferenceError: x is not defined

 

이 예제에서 x는 myFunction 함수 내에서 로컬 변수로 선언되어 있으며 해당 함수 내에서만 액세스할 수 있습니다. 함수 외부에서 x를 로깅하려고 하면 x가 전역 범위에 정의되지 않았기 때문에 ReferenceError가 발생합니다.

로컬 변수함수 내에서 로직과 데이터를 캡슐화하고 다른 함수나 전역 범위에서 정의된 변수와의 이름 충돌을 방지하는 데 유용할 수 있습니다.

 

전역 변수(Global variables)

전역 변수는 함수 외부에 선언된 변수로, 코드의 어느 부분에서나 액세스할 수 있습니다. 전역 변수는 프로그램의 여러 부분에서 공유해야 하는 데이터와 구성을 저장하는 데 유용할 수 있습니다. 그러나 전역 변수를 과도하게 사용하면 명명(naming) 충돌이 발생하고 코드의 동작을 추론하기가 어려워질 수 있습니다. 예를 들어:

 

var y = 2; // global variable

function myFunction() {
  console.log(y); // Output: 2
}

myFunction();

console.log(y); // Output: 2

 

이 예제에서 y는 전역 변수로 선언되어 myFunction 함수와 전역 범위 모두에서 액세스할 수 있습니다. 그러나 실수로 덮어쓰거나 변수 값이 예기치 않게 변경되는 등 전역 변수를 사용할 때 발생할 수 있는 잠재적인 함정에 유의해야 합니다.

 

1-3. 다른 스코프의 변수 사용하기

경우에 따라 다른 범위에서 선언된 변수에 액세스하고 싶은 경우도 있을 수 있습니다. 이를 위한 방법은 변수를 함수에 매개변수로 전달하거나 함수에서 변수를 반환하는 것입니다. 예를 들어:

 

function myFunction() {
  var x = 1;
  return x;
}

var y = myFunction();

console.log(y); // Output: 1

 

이 예제에서 x는 myFunction에서 지역 변수로 선언되었지만 함수에서 값을 반환하고 다른 변수에 저장하여 해당 값을 사용할 수 있습니다.

 

1-4. 블록 스코프에서 변수 이름 충돌을 막는법

블록 스코프는 변수의 표시 범위를 전체 함수나 프로그램이 아닌 특정 코드 블록으로 제한하는 방법입니다. 이렇게 하면 이름 충돌을 피하고 보다 모듈화되고 유지 관리하기 쉬운 코드를 만들 수 있습니다.

 

let과 const의 블록 스코프

ES6 이상 버전의 자바스크립트에서는 let 및 const 키워드로 블록 범위를 생성할 수 있습니다. let 및 const로 선언된 변수는 해당 변수가 정의된 블록과 중첩된 블록 내에서만 액세스할 수 있습니다. 예를 들어:

function myFunction() {
  var x = 1; // function-scoped variable

  if (true) {
    var x = 2; // overrides the outer x variable
    let y = 3; // block-scoped variable
    const z = 4; // block-scoped variable

    console.log(x); // Output: 2
    console.log(y); // Output: 3
    console.log(z); // Output: 4
  }

  console.log(x); // Output: 2
  console.log(y); // Output: ReferenceError: y is not defined
  console.log(z); // Output: ReferenceError: z is not defined
}

myFunction();

 

이 예제에서 x함수 스코프 변수로 선언되어 전체 함수에서 액세스할 수 있습니다. 그러나 var 키워드를 사용하여 if 블록 내에 새로운 변수 x를 선언하면 외부의 x 변수가 재정의(override)됩니다. 이를 방지하기 위해 let 또는 const를 사용하여 if 블록 내에서만 액세스할 수 있는 블록 범위 변수로 yz를 선언할 수 있습니다.

 

1-5. 즉시 호출 함수 표현식(Immediately-Invoked Function Expressions)의 블록 스코프

블록 범위를 만드는 또 다른 방법은 즉시 호출 함수 표현식(IIFE)을 사용하는 것입니다. IIFE는 정의되는 그 즉시 호출되는 함수로, 변수에 대한 새 범위를 만드는 데 사용할 수 있습니다. 예를 들어:

(function() {
  var x = 1; // block-scoped variable

  console.log(x); // Output: 1
})();

console.log(x); // Output: ReferenceError: x is not defined

 

이 예제에서는 블록 범위 변수 x를 선언하는 IIFE를 정의합니다. IIFE는 즉시 호출되므로 x는 해당 함수 내에서만 액세스할 수 있고 전역 범위에서는 액세스할 수 없습니다.

let, const 또는 IIFE와 함께 블록 범위를 사용하면 명명 충돌을 피할 수 있습니다. 이렇게 하면 보다 모듈화되고 유지 관리가 쉬운 코드를 생성할 수 있게됩니다. 

 

2. 클로저 (Closure)

2-1. Closure란?

함수가 자신이 생성될 때의 환경을 "기억"하는 특성

 

 

클로저private 변수함수를 만드는 강력한 기술입니다. 클로저를 사용하면 함수 내에서 상태(state)와 동작(behavior)을 캡슐화하여 외부 코드가 해당 상태에 직접 액세스하거나 수정하지 못하도록 할 수 있습니다.

 

클로저는 함수가 다른 함수 안에 정의되어 있고 내부 함수가 외부 함수의 변수와 매개변수에 액세스할 수 있는 경우 생성됩니다. 즉, 외부 함수가 실행을 완료한 후에도 어떤 함수가 그 외부 함수의 변수를 기억하고 액세스할 수 있도록 하는 자바스크립트의 기본 개념 중 하나인 것입니다.

 

간단히 말해서, 클로저는 외부 함수의 변수와 범위를 유지하면서 해당 범위를 "close"하는 역할을 합니다.

 

흔히 클로저에 대한 정의를 보게되면, '함수그 함수가 선언될 때의 렉시컬 환경(Lexical Environment)의 조합'이라고 설명되어 있습니다. 여기서 '렉시컬 환경'이란 실행 컨텍스트(execution context)에 대한 환경을 의미하는 것으로, 함수가 생성될 때 그 주변의 변수와 스코프에 대한 정보를 담고 있는 환경을 말합니다. 자바스크립트에서 함수는 자신이 생성될 때의 스코프에 있는 변수들에 접근할 수 있는데, 함수 종료 이후에도 이러한 변수들에 접근할 수 있는 이유가 바로 클로저 때문입니다.

  • 외부 함수가 호출되면 그 함수의 자체 변수 및 매개변수 집합을 기반으로 새로운 실행 컨텍스트가 생성됩니다.

 

이러한 것이 가능한 이유는 클로저는 함수가 자신이 선언됐을 때의 렉시컬 환경에 있는 변수를 참조할 수 있게 하며, 이 렉시컬 환경은 함수가 실행을 마친 후에도 생존할 수 있게 하는데, 이는 JavaScript 엔진의 가비지 컬렉션 메커니즘이 이렇게 도달 가능한 데이터에 대해서는 메모리에 계속해서 유지시키기 때문에 해당 변수에 접근할 수 있었던 것입니다.

 

2-2. 클로저는 어떻게 작동하는가

클로저에 대한 이해를 위해 아래 예시를 살펴봅시다:

function outer() {
    const message = "Hello, ";

    function inner(name) {
    console.log(message + name);
    }

    return inner;
}
const greet = outer();
greet("John");

 

이 예제에서 outer() 함수는 인삿말을 기록하는 `message` 변수와 내부 함수 `inner()`를 정의합니다. outer() 함수에서는 `inner()` 함수를 반환하고 있습니다.

 

outer()를 호출하고 이를 `greet` 변수에 할당하면 클로저가 생성된 것입니다. 이제 `outer()` 함수가 실행을 종료하더라도 `greet` 변수는 여전히 `message` 변수에 계속 액세스할 수 있습니다.

  • 원래라면 outer() 함수가 종료했기 때문에 호출 스택에서 outer() 함수가 pop되어 그 안의 변수는 접근 못하는 것이 자바스크립트의 방식이었습니다.

 

이제 greet("John")`을 호출하면 정상적으로 "Hello, John"이 콘솔에 출력되는 것을 확인할 수 있습니다..

 

 

2-3. 클로저 사용법

클로저는 자바스크립트에서 매우 중요하며, 다양한 시나리오에서 활용될 수 있습니다.

 

Encapsulation (캡슐화)

클로저를 사용하면 클로저 내에서만 액세스할 수 있는 private 변수와 함수를 생성하여 코드 상에 캡슐화를 이뤄낼 수 있습니다. 이는 의도치 않은 데이터에 대한 접근을 막을 수 있고 그로 인한 수정을 방지하는 데 굉장히 유용합니다. 위의 예제에서 message에 접근을 할 순 없지만 그것을 출력할 수는 있는 것에 해당합니다. 아래는 그러한 예시를 또 보여줍니다:

 

function createCounter() {
    let count = 0;

    return function() {
    count++;
    console.log(count);
    };
}
const counter = createCounter();
counter(); // 1
counter(); // 2

 

creatCounter 함수의 count 변수는 해당 함수가 종료된 이후에도 접근할 수 있습니다.

 

 

참고로 다음과 같이 두 가지 메서드를 가진 객체를 반환하는 것도 가능합니다.

function counter() {
  var count = 0;

  function increment() {
    count++;
    console.log(count);
  }

  function decrement() {
    count--;
    console.log(count);
  }

  return {
    increment: increment,
    decrement: decrement
  };
}

var myCounter = counter();
myCounter.increment(); // Output: 1
myCounter.increment(); // Output: 2
myCounter.decrement(); // Output: 1

 

이러한 메서드들(increment(), decrement())은 외부 함수에 선언된 count 변수에 액세스할 수 있지만 클로저 외부에서는 count 변수에 직접 액세스할 수 없습니다. 즉, 클로저에서 제공하는 메서드를 통해서만 increment 및 decrement할 수 있는 private counter를 만들 수 있습니다.

 

Data Persistance (데이터 영속성)

클로저는 함수 호출을 여러 번 할 때에도 원래의 data와 state 값을 유지할 수 있습니다. 이는 일반적으로 이벤트 핸들러와 비동기 연산에 사용됩니다.

function createTimer() {
    let startTime = Date.now();

    return function() {
    console.log(`Time elapsed: ${Date.now() - startTime}ms`);
    };
}
const timer = createTimer();
setInterval(timer, 1000);

 

createTimer 함수가 호출되어 반환하는 익명함수는 startTime 변수에 접근할 수 있는 클로저를 형성합니다. 이 익명 함수는 createTimer의 렉시컬 환경에 있는 startTime 변수를 기억하며, 이 변수에 접근하여 그 값을 사용할 수도 있습니다. 이 클로저 덕분에, 'createTimer' 함수 실행이 완료된 이후에도 startTimer 변수의 값이 소멸되지 않고 익명 함수에 의해 계속 참조될 수 있는 것입니다.

  • setInterval은 첫 번째 인자로 전달된 함수를 두 번째 인자로 전달된 시간 간격(밀리초 단위)마다 반복해서 호출하는 JavaScript 내장 함수입니다. 이 경우, timer 함수가 1000밀리초(1초)마다 호출됩니다. 따라서 이 코드는 타이머가 시작된 이후로 1초마다 경과한 시간을 콘솔에 출력합니다.

 

중요한 것은 setInterval에서 timer 콜백 함수가 실행될 때마다 startTime 변수는 초기화되지 않고 최초 createTimer 함수가 호출되어 설정된 값으로 유지된다는 것입니다. 

 

Function Factories (함수 팩토리)

클로저를 사용하면 함수 팩토리를 생성하여 매개변수를 기반으로 특정 동작을 하는 함수를 생성할 수 있습니다.

function greetGenerator(greeting) {
    return function(name) {
    console.log(`${greeting}, ${name}!`);
    };
}
const sayHello = greetGenerator("Hello");
const sayHi = greetGenerator("Hi");
sayHello("John"); // Hello, John!
sayHi("Alice"); // Hi, Alice!

 

이 코드는 클로저를 활용하여 각기 다른 인사말을 생성하는 함수를 동적으로 생성하는 방법을 보여줍니다.

 

고차 함수 greetGenerator를 사용하여 인사말의 "템플릿"을 만들고, 이를 통해 다양한 인사말을 출력할 수 있는 특화된 클로저들(sayHello, sayHi)을 생성합니다. 클로저 덕분에, 각 함수는 자신이 생성될 때의 greeting 값을 "기억"하고, 이를 통해 해당 값을 출력에 활용할 수 있습니다.

 

2-4. 정리

정리하면 클로저는 다음과 같은 조건을 만족할 때 생성됩니다.

  • 내부 함수가 외부 함수의 변수에 접근할 때
  • 내부 함수가 외부 함수의 실행이 종료된 후에도 어딘가에서 호출될 때

 

클로저는 자바스크립트의 핵심 개념 중 하나로, 함수와 그 함수가 선언된 렉시컬 환경과의 조합을 의미하는 개념이었습니다. 클로저는 내부 함수가 외부 함수의 변수에 접근할 수 있게 해주는 특성을 가지고 있으며, 이를 통해 여러 유용한 프로그래밍 패턴을 구현할 수도 있죠.

 

클로저가 생성되는 주된 상황은 어떤 함수 내부에 다른 함수가 선언되고, 내부 함수가 외부 함수의 스코프에 있는 변수들에 접근할 때 발생합니다. 중요한 점은 외부 함수가 실행을 마치고 반환된 후에도, 내부 함수는 외부 함수의 변수들에 대한 참조를 유지한다는 것입니다. 이는 내부 함수가 외부 함수의 변수들을 "기억"하게 하며, 이러한 변수들은 내부 함수가 존재하는 한 계속해서 접근 가능합니다.

 

이러한 특성 덕분에, 클로저는 데이터 캡슐화, 정보 은닉, 함수 팩토리, 모듈 패턴 등 다양한 고급 프로그래밍 기법의 구현에 있어 굉장히 중요한 역할을 합니다. 예를 들어, 클로저를 사용하면 객체지향 프로그래밍에서의 private 변수와 같은 효과를 낼 수 있어, 특정 함수를 통해서만 변수에 접근하고 수정할 수 있게 만들 수 있습니다.

 

클로저의 이러한 성질은 자바스크립트의 함수가 렉시컬 스코핑(lexical scoping)을 따른다는 사실과 밀접하게 연결되어 있습니다. 렉시컬 스코핑 함수의 스코프가 함수가 선언된 시점의 스코프에 의해 결정된다는 의미입니다. 따라서, 클로저는 함수가 선언될 당시의 환경을 "캡처"하여, 이후에도 그 환경에 접근할 수 있게 해주는 매커니즘입니다.

 

요약하자면, 클로저는 자바스크립트에서 매우 강력한 프로그래밍 기법을 가능하게 해주는 중요한 개념입니다. 이를 이해하고 활용하는 것은 복잡한 자바스크립트 애플리케이션을 개발하는 데 있어 필수적인 기술 중 하나입니다.

 

2-5. Callbacks과 Event Listeners

자바스크립트에서 클로저의 일반적인 사용 사례 중 하나는 콜백이벤트 리스너를 생성하는 것입니다. 클로저는 콜백이나 이벤트 리스너가 정의될 때 객체나 모듈의 상태캡처하고, '나중에 콜백이나 이벤트 리스너가 호출될 때 이 상태에 대한 액세스를 제공하는 데' 사용할 수 있습니다. 예를 들어

 

function myModule() {
  var state = 'initial';

  function onButtonClick() {
    console.log(state);
  }

  document.getElementById('myButton').addEventListener('click', onButtonClick);

  return {
    setState: function(newState) {
      state = newState;
    }
  };
}

var module = myModule();
module.setState('updated');

 

이 예제에서 myModule은 onButtonClick 함수가 정의될 때 상태 변수의 값을 캡처하는 클로저를 정의합니다. myButton 요소가 클릭되면 onButtonClick 함수가 호출되어 현재 상태 값을 콘솔에 출력합니다. 그 다음 해당 myModule 모듈에서 제공하는 setState 메서드를 사용하면 클로저 외부에서 상태 값을 업데이트할 수 있습니다.

 

2-6. Iterators와 Generator

자바스크립트에서 클로저의 또 다른 사용 사례는 IteratorGenerator를 만드는 것입니다. 이터레이터값의 시퀀스를 제공하는 객체이고, 제너레이터필요에 따라 값의 시퀀스를 생성할 수 있는 함수입니다. 이터레이터와 제너레이터는 모두 내부 상태와 제어 흐름을 유지하기 위해 클로저에 의존하는 경우가 많습니다. 예를 들어

 

function createIterator(items) {
  var i = 0;

  return {
    next: function() {
      var isDone = (i >= items.length);
      var value = !isDone ? items[i++] : undefined;

      return {
        isDone: isDone,
        value: value
      };
    }
  };
}

var myIterator = createIterator([1, 2, 3]);
console.log(myIterator.next()); // Output: { isDone: false, value: 1 }
console.log(myIterator.next()); // Output: { isDone: false, value: 2 }
console.log(myIterator.next()); // Output: { isDone: false, value: 3 }
console.log(myIterator.next()); // Output: { isDone: true, value: undefined }

 

이 예제에서 createIterator는 클로저를 사용하여 현재 인덱스 iitems 배열을 유지하는 next() 메서드가 있는 객체를 반환합니다. myIterator.next()를 호출하면 클로저는 items 시퀀스의 다음 값과 함께 시퀀스가 종료되었는지 여부를 나타내는 isDone 플래그를 반환합니다.

 

2-7. 함수 스코프와 클로저에서 발생한 에러 디버깅 하는 법

함수 스코프와 클로저 개념은 예기치 않은 방식으로 변수와 함수의 가시성과 동작에 영향을 미칠 수 있기 때문에 함수 스코프 및 클로저 관련 문제를 디버깅하는 것은 다소 어려운 일입니다. 이 섹션에서는 함수 스코프 및 클로저와 관련된 몇 가지 일반적인 문제와 이를 디버깅하는 방법을 살펴보겠습니다.

 

스코프 체인 문제 (Scope chain issue)

함수 스코프와 클로저의 일반적인 문제 중 하나는 예상 스코프 체인과 실제 스코프 체인 간의 불일치입니다. 이는 변수나 함수가 사용되는 범위와 다른 범위에서 선언되거나 클로저가 부모 범위에서 잘못된 값을 캡처할 때 발생할 수 있습니다. 예를 들어:

var x = 1;

function outer() {
  var y = 2;

  function inner() {
    console.log(x, y);
  }

  return inner;
}

var closure = outer();
closure(); // Output: 1, 2

var x = 3;
closure(); // Output: 3, 2

 

이 예제에서 inner() 함수는 전역 범위의 변수 xouter() 함수의 범위의 변수 y를 캡처합니다. 이때, 클로저 외부에서 x 의 값을 업데이트하면 변수 x가 outer() 함수의 범위 내에 정의되어 있지 않음에도 클로저의 출력에 영향을 미치게 됩니다.

스코프 체인 문제를 디버깅하려면 코드를 단계별로 살펴보고 각 단계의 변수 값을 검사하는 디버거를 사용하는 것이 도움이 될 수 있습니다. 또한 각 변수와 함수의 범위를 다시 한 번 확인하고 의도한 방식으로 사용되고 있는지 확인하는 것도 도움이 될 수 있습니다.

 

Memory Leaks (메모리 누수)

클로저의 또 다른 문제는 메모리 누수 가능성인데, 특히 클로저가 대량의 state들을 캡처하거나 순환 참조(circular reference)를 생성하는 데에 사용되는 경우 더욱 그렇습니다. 클로저가 필요하지 않거나 더 이상 사용되지 않는 변수를 캡처하면 메모리가 불필요하게 소모됩니다. 예를 들어:

 

function createLoop() {
  var elements = [];

  for (var i = 0; i < 1000000; i++) {
    var element = document.createElement('div');
    elements.push(element);

    element.addEventListener('click', function() {
      console.log(i);
    });
  }

  return elements;
}

var loop = createLoop();

 

이 예제에서 createLoop 함수는 백만 개의 <div> 요소를 생성하고 각 요소에 클릭 이벤트 리스너를 추가하여 i의 현재 값을 출력합니다. 이벤트 리스너 함수는 각각의 함수가 정의될 때의 i 변수의 값을 "기억" 합니다. 그러나 클로저는 부모 스코프의 변수 i를 캡처하기 때문에 루프 실행이 완료된 후에도 변수 i와 그 부모 범위가 garbage-collected 되지 않도록 하는 순환 참조를 생성합니다. (모든 이벤트 리스너가 같은 i 변수를 참조)

  • 즉 정리하면, 사용자가 <div> 중 하나를 클릭하면, 해당 <div>의 순서에 해당하는 번호가 콘솔에 출력되도록 의도된 것이지만 예상대로 동작하지 않게 됩니다. 왜냐하면 이벤트 리스너 내의 함수가 변수 i에 대한 클로저를 형성하기 때문입니다. 클로저는 함수가 정의된 당시의 변수들을 기억하는 기능인데, 이 경우 모든 이벤트 리스너가 루프의 마지막 값인 1,000,000을 기억하게 됩니다. 즉, 사용자가 어떤 <div>를 클릭하던지 간에 항상 1,000,000이 출력되게 될 것입니다.
  • 또한 각 <div>와 클로저가 변수 i를 참조하고 있기 때문에 이 변수와 이 변수를 참조하는 클로저는 garbage collection의 대상이 되지 않게됩니다. 그렇기 때문에 해당 요소들은 메모리에 계속 남아 성능을 저해시키는 요소로 남아있게 될 것입니다.


이로 인한 메모리 누수를 방지하려면 클로저가 캡처하는 변수와 함수를 염두에 두고 순환 참조나 대량의 state에 대한 불필요한 참조를 생성하지 않는 것이 중요합니다.

 

이러한 문제를 해결하기 위한 방법으로, 각 이벤트 리스너가 자신만의 고유한 i값을 기억하도록 하는 방법이 있습니다. 이를 위해 let 키워드를 사용하여 루프의 각 반복에 대해 i의 새로운 복사본을 생성하거나 이벤트 리스너 함수를 별도의 함수로 분리하여 해당 함수에 i값을 인자로 전달할 수 있습니다.

  • 메모리 누수를 방지하기 위해서는 페이지에서 요소를 제거할 때 해당 요소에 연결된 이벤트 리스너를 제거하는 식으로 해야 합니다.


이렇듯 함수 스코프와 클로저 관련 문제를 디버깅하는 것은 어려울 수 있지만, 약간의 연습과 주의를 기울인다면 이러한 개념이 JavaScript 코드에서 어떻게 작동하는지 더 잘 이해할 수 있습니다.

 

2-9. 함수 스코프와 클로저의 모범 사례

함수 스코프와 클로저는 모듈화되고 유지 관리가 쉬운 자바스크립트 코드를 만드는 데 강력한 도구입니다. 하지만 신중하게 사용하지 않으면 복잡성과 예기치 않은 동작이 발생할 수도 있습니다. 이 섹션에서 JavaScript 코드에서 함수 범위와 클로저를 효과적으로 사용하기 위한 몇 가지 모범 사례를 요약해 보겠습니다.

 

전역 스코프를 최소화하라

함수 범위와 클로저를 사용할 때 중요한 모범 사례 중 하나는 전역 변수와 함수 사용을 최소화하는 것입니다. 전역 변수와 함수는 특히 코드베이스가 커질수록 명명 충돌을 일으키고 코드의 동작을 추론하기 어렵게 만들 수 있습니다. 대신 함수나 객체 내에서 상태와 동작을 캡슐화하고 클로저를 사용하여 이러한 함수나 객체의 내부 상태를 유지하도록 합니다.

 

블록 스코프와 strict mode를 사용하라

함수 범위와 클로저를 사용하는 또 다른 모범 사례는 블록 범위와 strict mode(엄격 모드)를 사용하여 변수의 스코프를 최소화하고 의도하지 않은 동작을 방지하는 것입니다. 블록 스코프는 ES6 이상 버전의 JavaScript에서 let 및 const 키워드로 생성할 수 있으며, 이름 충돌을 방지하고 보다 모듈화되고 유지 관리가 쉬운 코드를 작성하는 데 도움이 될 수 있습니다. strict mode는 파일이나 함수의 시작 부분에 'use strict'; 지시문을 추가하여 활성화할 수 있는 모드로, 일반적인 오류를 포착하고 코드에 모범 사례를 적용하는 데 도움이 될 수 있습니다.

 

private 변수와 함수를 생성하는 클로저를 사용하라

클로저는 함수나 객체 내에서만 액세스할 수 있는 private(비공개) 변수와 함수를 만드는 강력한 방법이 될 수 있습니다. 이를 통해 구현 세부 사항을 캡슐화하고 외부 코드가 객체나 모듈의 내부 상태에 액세스하거나 수정하지 못하도록 방지할 수 있습니다. 하지만 대량의 state를 캡처하기 위해 클로저를 사용할 때는 메모리 사용량과 메모리 누수 가능성에 유의합니다.

 

Scope Chian(스코프 체인) 및 가비지 컬렉션(Garbage Collection)에 유의하라

클로저를 사용할 때는 스코프 체인과 가비지 컬렉션의 잠재적 문제를 염두에 두는 것이 중요합니다. 스코프 체인은 클로저 내의 변수와 함수의 가시성과 동작에 영향을 줄 수 있으며, 신중하게 사용하지 않으면 디버깅하기 어려울 수 있습니다. 가비지 컬렉션은 대량의 state를 캡처하거나 순환 참조를 생성하는 클로저의 영향을 받을 수도 있습니다. 이러한 문제를 방지하려면 클로저가 캡처하는 변수와 함수를 염두에 두고 대량의 state에 대한 circular reference(순환 참조)나 불필요한 참조를 만들지 않도록 주의합니다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.