[JS] 전역 변수를 지양하고 모듈 패턴을 이용하자

merci·2023년 8월 3일
0

JavaScript

목록 보기
9/15


전역 변수에 대해 알아보기 전에 먼저 스코프에 대해 알아봅시다.

스코프

스코프는 변수가 정의된 공간으로 변수의 생명 주기가 됩니다.


렉시컬(정적) 스코프

자바스크립트는 렉시컬 스코프를 따릅니다.
렉시컬 스코프(정적 스코프)는 식별자가 정의(선언)될 때 결정됩니다.
선언할 때의 위치에 따라 전역, 함수, 블록 스코프가 됩니다.
동적 스코프와 반대되는 개념입니다.

var x = 'static'; 

function foo(){ 
  var x = 'local'; // 블록 스코프
  bar();              
}

function bar(){ 
  console.log(x);
}

foo(); // "static"

bar() 함수는 전역 스코프로 선언 되었으므로 static 이 출력됩니다.

자바스크립트(ES6)는 함수 레벨과 블록 레벨의 렉시컬 스코프규칙을 따릅니다.


전역 스코프

var globalVar = "I'm global!";

블록 안에 선언하지 않으면 스크립트 실행 시 메모리에 저장되고 어디서든 접근 가능하며 스크립트 종료시 제거됩니다.


함수 스코프 - var, 함수

foo() 함수 호출시 생성된 실행 컨텍스트가 스택 메모리에 저장되어 if블록이 종료되더라도 함수 스코프를 가진 var 타입은 제거되지 않습니다.

function foo() {
    if (true) {
        var color = 'blue';
    }
    console.log(color); // blue
}
foo();

var 는 함수 블록안에 있지 않다면 전역 변수가 됩니다.

if (Math.random() > 0.5) {
  var x = 1;
} else {
  var x = 2;
}
console.log(x); // 1 or 2


블록 스코프 - let, const

let, const는 블록 스코프를 가지므로 if블록이 종료되면 제거됩니다.

function foo() {
    if(true) {
        let color = 'blue';
    }
    console.log(color); // ReferenceError: color is not defined
}
foo();

ES6 부터 let, const를 사용할때만 블록이 스코프가 됩니다.

letvar 차이는 스코프가 함수냐 블록이냐로 나뉘게 됩니다.

function foo() {
    if (true) {
        let blockScopeVariable = '블록 스코프';
        var functionScopeVariable = '함수 스코프';
    }
    console.log(functionScopeVariable); // "함수 스코프"
    // console.log(blockScopeVariable); // ReferenceError!
    // blockScopeVariable은 이 스코프에서 접근할 수 없습니다.
}
foo();


전역 스코프 변수를 사용하면 안되는 이유

전역 스코프를 가진 변수 사용을 지양해야 하는 이유는 다음과 같습니다.

  • 충돌 가능성
    전역 변수는 어디서나 접근할 수 있으므로 다른 라이브러리나 코드와 충돌할 수 있습니다.
    특히 프로젝트가 커질수록 충돌 가능성이 높아집니다.
    변수명의 중복 등으로 인해 의도치 않은 동작을 유발할 수 있습니다.
  • 스코프 오염
    전역 변수를 사용하면 다른 함수나 블록 내에서도 동일한 변수 이름을 사용하고 변경할 수 있습니다.
    이로 인해 변수의 예기치 않은 변경이 발생할 수 있습니다.
  • 유지보수 어려움
    전역 변수를 사용하면 코드를 이해하기 어려워질 수 있습니다.
    전역 변수는 코드 어디서든 영향을 미치기 때문에 변수가 어디에서 변경되는지 파악하기 어렵고, 코드의 의도를 파악하기 어려울 수 있습니다.
  • 성능 저하
    전역 변수를 사용하면 함수가 해당 변수에 접근하기 위해 스코프 체인을 거쳐야 합니다.
    이로 인해 성능 저하가 발생할 수 있습니다.
  • 메모리 낭비
    자바스크립트 엔진은 실행 컨텍스트를 콜스택에 저장하므로 전역 변수의 생명 주기는 스크립트 종료 시까지 유지되어 불필요한 메모리를 낭비하게 됩니다.

따라서 전역 스코프 변수의 사용을 최소화하고, 변수의 범위를 가능한 한 제한하여 코드의 가독성과 유지보수성을 향상시키는 것이 좋습니다.


이제 전역 스코프 변수를 사용하지 않는 다른 방법을 알아보겠습니다.

모듈 패턴 (ES6 모듈)

JavaScript에서 모듈 패턴은 코드의 재사용성, 유지 보수성 및 네임스페이스 관리를 위해 널리 사용되는 디자인 패턴입니다.

모듈 스코프는 ES6 (ECMAScript 2015)에서 도입된 JavaScript 모듈 시스템에서 사용되는 스코프입니다.
모듈 내부에서 선언된 변수나 함수는 내부에서만 접근가능하고 해당 모듈 바깥에서는 접근할 수 없습니다.

그러므로 외부에서 접근하도록 하려면 export 로 내보내고 import로 접근합니다.
ES6 모듈을 이용하면 각각의 모듈을 별도의 파일로 관리할 수 있어 필요한 모듈만 import로 사용할 수 있습니다.

스크립트를 모듈 스코프로 만들려면 type="module"을 추가합니다.
모듈 스크립트를 사용하면 자동적으로 defer가 됩니다. ( 비동기 다운로드 -> Dom트리 구축 후 실행 )

<script type="module" src="cal.js"></script>

모듈 스코프 변수

해당 모듈에서만 접근 가능한 변수를 말합니다.
모듈 내에서만 접근 가능하므로 전역 네임스페이스를 오염시키지 않습니다.

// state.js
let count = 0;

function increment() {
  count += 1;
}

function getCount() {
  return count;
}

export { increment, getCount };
// main.js
import { increment, getCount } from './state.js';

console.log(getCount()); // 0
increment();
console.log(getCount()); // 1

또한 node 서버에서 import 를 사용하려면 package.json에 다음줄을 추가합니다.

"type": "module"

이러한 ES6의 모듈 시스템을 이용한 모듈 패턴으로 전역 변수를 대체할 수 있습니다.
하지만 이러한 기능을 지원하는 브라우저가 필요하고 구형 브라우저의 경우 웹팩 같은 번들러를 사용해야 합니다.


즉시 실행 함수

함수를 정의하자마자 즉시 실행하는 자바스크립트의 패턴입니다.
코드를 즉시 실행하거나 함수의 스코프를 제한하여 전역 네임스페이스를 오염시키지 않고 변수를 캡슐화 하고자 할 때 사용합니다.

(function() {
    var privateVar = "I am private";
})();

위 함수의 실행 컨텍스트가 종료되면 가비지 컬렉터가 변수를 메모리에서 제거하게 됩니다.
만약 내부의 캡슐화된 변수를 메모리에 살려두고 싶다면 즉시 실행 함수에서 return으로 반환하면 됩니다.

내부에서 반환된 함수에 의해 참조가 되고 있으므로 내부 변수는 제거되지 않습니다.

var outerFunction = (function() {
    var privateVar = "I am private";
    return function() {
        return privateVar;
    }
})();

위와 같은 현상을 클로저라고 합니다.


클로저

클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말합니다.

간단히 말해 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수입니다.

클로저의 핵심은 다음과 같습니다.

함수 내부에서 또 다른 함수가 선언되고 반환됩니다.
이 내부 함수는 외부 함수의 지역 변수에 접근할 수 있습니다.

외부 함수가 실행 완료된 후에도, 내부 함수가 외부 함수의 지역 변수에 접근할 수 있는 능력을 유지합니다.

let counter = (function() {
  let count = 0;

  return {
    increment: function() {
      count += 1;
    },
    decrement: function() {
      count -= 1;
    },
    getCurrentCount: function() {
      return count;
    }
  };
})();

counter.increment();
counter.increment();
console.log(counter.getCurrentCount()); // 2
counter.decrement();
console.log(counter.getCurrentCount()); // 1

즉시실행함수 내부에 클로저를 생성하여 변수의 캡슐화된 상태를 유지합니다.
클로저가 즉시실행함수의 로컬 변수에 접근하고, 이를 통해 상태를 유지하는 역할을 합니다. ( 가비지 컬렉터 x )

클로저를 이용하면 클래스를 사용하지 않고도 private 메소드의 기능을 구사 할 수 있습니다.
이를 통해 전역 스코프 변수를 사용하지 않고도 전역 상태를 관리할 수 있습니다.

이러한 클로저를 이용해 모듈 패턴을 구현할 수 있습니다.


모듈 패턴 (클로저)

클로저를 이용한 모듈 패턴으로 전역 변수를 대체할 수 있습니다.

클로저를 이용한 모듈 패턴의 핵심은 다음과 같습니다.

특정 부분을 비공개(private)로 유지하고(캡슐화), 외부에서 접근할 수 있는 API를 제공합니다.

클로저는 이 패턴의 핵심적인 기능을 제공합니다.

    var counter = (function() {
      // private 변수
      var privateCounter = 0;
      
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        // public 메서드 및 속성
    	publicVar: 'public',
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      };
    })();

    console.log(counter.value()); // logs 0
    counter.increment();
    counter.increment();
    console.log(counter.value()); // logs 2
    counter.decrement();
    console.log(counter.value()); // logs 1
	console.log(counter.privateCounter);  // undefined
	console.log(counter.publicVar);  // public

increment, decrement, value 함수는 같은 환경을 공유하는 클로저입니다.
위 구조를 이용하면 private, public 기능과 유사하게 구현됩니다.

이러한 모듈 패턴을 이용하면 전역 변수를 사용함으로서 발생하는 여러 단점을 방지할 수 있습니다.


profile
작은것부터

0개의 댓글