본문 바로가기
Javascript

[requireJS] 전역 변수 오염을 어떻게 방지할까?

by kellis 2020. 10. 21.

prerequisite

 

일반적으로 전역 변수가 오염되는 것을 막기 위해 네임스페이스 패턴을 많이 이용합니다.

$.namespace("listgrid1.eventhandler");
listgrid1.eventhandler = {
    bindButtonEvent : function(){
        ...생략
    }
}

위 코드에서 보이는 namespace plugin의 구현부는 다음과 같습니다.

$.namespace = function() {
    var a = arguments, o = null, i, j, d;
    for (i = 0; i < a.length; i = i + 1) {
        d = a[i].split(".");
        o = window;
        for (j = 0; j < d.length; j = j + 1) {
            o[d[j]] = o[d[j]] || {};
            o = o[d[j]];
        }
    }
 
    return o;
};
  • 4라인 : namespace plugin 매개변수로 입력받은 값에 대해 첫 번째 점(.)을 기준으로 앞의 값은 전역 변수명으로 선언합니다. 위에서 보여드렸던 코드에 적용하면, window.listgrid1이 선언됩니다.
  • 6~8라인 : 그리고 점(.)의 이후의 값들은 listgrid1에 할당되는 객체의 필드명이 됩니다.  window.listgrid1 = {eventhandler : {..}}  

 

그러나 namespace 패턴 역시도 전역 스코프를 사용하고 있습니다. 다만 depth를 줌으로써 오염을 어느 정도 방지하는 것뿐입니다. 따라서 전역 변수의 오염을 막기 위해서는 namespace 패턴보다 requirejs를 선호합니다. (물론 front-end framework의 인기가 높아지면서 requirejs의 사용도 줄어드는 추세입니다.) 

 

requirejs는 Javascript 모듈화를 위해 AMD 표준을 따르는 라이브러리로, 모듈화가 제공하는 주요한 기능 중 하나가 바로 전역 변수의 오염을 방지하는 것입니다. (물론 이것만이 모듈화의 목적은 아닙니다.)

 

그렇다면 모듈화라는 것은 무엇일까요?

 

모듈화 패턴은 javascript 파일 단위로 스코프를 분리하는 것이 핵심입니다. 모듈화 패턴을 이용하면 전역 스코프를 이용하지 않고 새로운 스코프를 생성합니다. 따라서 하나의 파일 내에서 정의한 변수와 함수를 외부의 스코프에서 참조할 수 없도록  숨길 수 있습니다. 이는 java의 접근 제어자 private과 같은 효과를 냅니다. 외부의 스코프에서 접근하고자 한다면 모듈 내에서 return 키워드를 이용하면 되는데, 이것은 java의 접근 제어자 public과 같습니다.

모듈화 패턴은 ES5와 ES6에서 다르게 구현할 수 있는데, ES5에서는 모듈화 패턴을 구현하기 위해 클로저와 즉시실행함수를 이용합니다.

var calculator = (function(){
    var ip = "222.222.222.223"
    function getUrl(){
        return "http://"+ip;
    }
    return {
        url: getUrl()
    }
})()
  • 6라인에서 return 키워드를 통해 url의 접근제어를 public으로 설정했습니다.

만약 2라인에서 선언한 ip도 public으로 설정하길 원한다면, return의 object literal에 추가시켜주어야 합니다.

var calculator = (function(){
    var ip = "222.222.222.223"
    function getUrl(){
        return "http://"+ip;
    }
    return {
        ip: ip
        url: getUrl()
    }
})()

ES6에서는 class, export, import 키워드가 생기면서 더 직관적으로 모듈화할 수 있게 되었습니다.

// 변수의 공개
export const ip = “218.38.0.1";
// 함수의 공개
export function getUrl(ip) { return “http://“+ip;}

매번 export를 하기 귀찮다면, 마지막에 일괄적으로 export할 수도 있습니다.

import { ip, getUrl } from './sample-module.js';

ES6에서의 모듈화는 쉽지만,  closure를 이용해서 모듈화하는 방식은 의존성을 관리/파악하는데 어려움이 있었습니다.

그래서 모듈화를 아름답게 하고자, AMD(Asynchronous Module Definition) 명세가 제안되었습니다. AMD는 동적 로딩, 의존성 관리, 모듈화에 대한 표준을 제시했고 이 AMD를 구현한 API 중 하나가 requireJS입니다.

 

ES5까지는 모듈화를 지원하지 않았기 때문에 클로저를 이용해 구현했으나, 클로저를 이용한 모듈화 패턴은 의존성 관리/파악이 어렵다는 단점이 있습니다. 이에 따라 AMD(Asynchronous Module Definition)가 제안됩니다.

AMD는 브라우저 환경을 중심으로 설계된 규약으로, 필요한 모듈을 네트워크를 이용해 내려받을 수 있도록 하는 비동기 모듈 표준안입니다. 그리고 이를 구현한 라이브러리 중 하나가 requireJS입니다. 

 

참고. Javascript 모듈화를 위한 또 다른 표준으로 CommonJS가 있습니다. 이는 Javascript를 브라우저에서뿐만 아니라, 서버 사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하기 위해 만들어졌습니다. 그러나 모듈을 비동기적으로 로딩하지 않는다는 점 때문에, 브라우저에서는 일반적으로 AMD를 이용합니다. 

 

requireJS를 사용하기 위해서 크게 3가지 작업을 합니다.

(1) requireJS 진입점 및 requireJS가 관리하는 모듈에 대한 설정

브라우저 로딩 시 다음과 같은 스크립트 태그를 통해, requireJS에 대한 진입점을 만들어줍니다. 

<script data-main="http://sample.com/js/sample/sample-base.js" src="http://sample.com/js/lib/require.js"></script>
  • data-main에는 requireJS가 관리하는 모듈에 대한 설정이 있는 파일을 입력해줍니다. 그리고 이 파일에는 설정뿐 아니라, requireJS가 가장 먼저 실행시키는 function이 포함되어 있습니다. 
  • src에는 https://requirejs.org/docs/download.html에서 다운로드 받은 require.js 파일의 경로를 입력해줍니다.

[requirejs 설정 파일 - sample-base.js]

requirejs.config({
    baseUrl: 'http://sample.com/js',
    paths: {
        jquery: 'lib/jquery',
        ...생략...
        sampleMain : 'sample/sample-main',
        sampleState : 'sample/sample-state',
    },
    shim : {
        notify : {
            deps: ['jquery'],
            exports : 'lib/notify'
        }
    }
});
requirejs(['sampleMain'], function(sampleMain){
    samplekMain.execute();
});
  • requireJS config에서는 baseUrl, paths, shim 이 3가지를 세팅해줍니다. 
  • baseUrl: requireJS가 읽을 파일의 기본 경로입니다.
  • paths : requireJS가 로딩할 모듈입니다.
    • 이는 key-value 형식으로 구성되는데, key는 앞으로 사용할 모듈명이고 value는 baseUrl을 기준으로 한 파일의 경로입니다.
  • shim : AMD를 지원하지 않는 다른 라이브러리나 객체를 모듈로 정의하고자 할 때 사용함.
    • 9라인의 notify는 해당 모듈의 이름이고, 그 value는 object literal로 구성되어 있습니다.
    • 10라인의 deps는 해당 모듈이 의존하고 있는 library로서, 이 모듈을 로딩하기 전에 jquery 모듈을 먼저 로드해야 한다는 의미입니다.
    • 11라인의 exports는 baseUrl을 기준으로 한 파일의 경로입니다.
  • 15- 18라인은 requireJS가 관리하는 모듈이 로딩된 후, 가장 먼저 실행하는 function에 대한 정의입니다. Java에서 main 함수 같은 역할을 하기에 이를 main function이라고 칭하겠습니다. 
    • 15라인의 첫 번째 매개변수에 'sampleMain'이 담겨있는 배열을 볼 수 있는데, 이는 config에서 정의한 모듈 이름입니다. 이 main function이 필요로 하는 모듈을 배열에 정의합니다. 이를 통해 의존성을 확인할 수 있습니다.
    • 15라인의 두 번째 매개변수는 main function입니다. main function의 매개변수에도 sampleMain이 있는데, 첫 번째 매개변수에서 정의한 의존 모듈을 주입받기 위한 것입니다.

 

requirejs(['sampleMain'], function(my){
    my.execute();
});

여기서 의존성 모듈의 이름과 main function에서 주입받는 이름은 달라도 됩니다. 무조건 순서에 의해 주입됩니다.

requirejs(['jquery','sampleMain'], function(my, $){
    my.execute(); //undefined function 에러 발생
});

순서에 맞춰 주입되기 때문에 위의 코드에서는 에러가 발생합니다. jquery 모듈이 my이기 때문입니다. 

 

(2) requireJS가 관리하는 모듈로 등록하는 방법

define(['jquery'], function($){
 
    var SampleState = function(){
        this.state = "WARN"
        ...생략...
    }
    SampleState.prototype.init = function(){
        ...생략...
    }
    ....생략...
     
    return SampleState
});
  • 1라인에서 첫 번째 매개변수에 의존하는 모듈을 배열로 정의하고, 두 번째 매개변수에는 실행시킬 function을 정의합니다. main function과 마찬가지로 매개변수에 주입받을 모듈을 순서에 맞춰 입력해야 합니다. 
  • 11라인에서 return 키워드를 사용해 외부에 공개할 Object를 선언합니다. 이 return 키워드를 통해 모듈을 등록하는 것입니다. sampleState는 생성자 방식으로 모듈을 등록한 예제이며, 뒤에서 자세히 다루겠습니다.

 

 

(3) requireJS로 등록한 모듈을 사용하는 방법

이번에는 main function에서 호출했던 sampleMain의 execute function을 살펴보겠습니다. 여기서는 앞서 모듈로 등록한 sampleState를 의존하고 있습니다. 

define(['sampleState'], function(SampleState){
    return {
        execute : function(){
            SampleState gs = new SampleState();
            console.log(gs.state) //로그에 warn이 찍힘.
            ...생략...   
        }
    }
});
  • 4라인에서 new 키워드를 통해 SampleState를 인스턴스로 만들어 사용합니다. 

 

그런데 SampleMain 역시 requireJS에 직접 등록한 모듈입니다. 그런데 return을 통해 반환하는 형태가 SampleState와 다르죠? 

앞서 잠시 언급했지만, requireJS로 모듈을 만들 때는 리터럴 방식과 생성자 방식 두 가지가 있습니다. 

SampleState의 경우는 생성자를 반환했고, 따라서 사용하는 쪽에서 new 키워드로 인스턴스를 만들어 사용해야 합니다. 

SampleMain의 경우는 객체 리터럴을 반환했고, 사용하는 쪽에서 function에 바로 접근할 수 있습니다.

define(['sampleMain'], function(sampleMain){
    sampleMain.execute();
});

리터럴 방식과 생성자 방식 중 어떤 것을 선택하느냐는 개발자의 몫이지만, 모듈화 된 객체가 싱글턴이면 리터럴 방식을 주로 이용합니다.

 

 

 

 

 

[references]

Javascript Scope and Closures

 

JavaScript Scope and Closures | CSS-Tricks

Scopes and closures are important in JavaScript. But, they were confusing for me when I first started. Here's an explanation of scopes and closures to

css-tricks.com

The Visual Guide To JavaScript Variable Definitions & Scope

 

An introduction to scope in JavaScript

Scope defines the lifetime and visibility of a variable. Variables are not visible outside the scope in which they are declared. JavaScript has module scope, function scope, block scope, lexical scope and global scope. Global Scope Variables defined outsid

www.freecodecamp.org

Declaring Variables in ES6+ JavaScript

 

Declaring Variables in ES6+ JavaScript

The var, the let and the const.

codeburst.io

 

'Javascript' 카테고리의 다른 글

[Javascript] Closure  (0) 2020.10.21
[Javascript] 실행 컨텍스트  (0) 2020.10.21
[Javascript] Scope  (0) 2020.10.21
[ES6+] var vs let vs const  (0) 2020.10.21
[ES6+] Map vs Object  (2) 2020.10.21

댓글