본문 바로가기
Javascript

JavaScript - IIFE, Modules and Namespaces

by Su1993 2020. 9. 18.
반응형

IIFE

IIFE(Immediately Invoked Function Expression)는 정의와 동시에 즉시 실행되는 함수를 의미한다.

(function() {
  statements
})();

 

Self-Executing Anonymous Function으로 알려진 디자인 패턴이고 크게 두 부분으로 구성된다.

첫 번째는 괄호((), Grouping Operator)로 둘러싸인 익명함수(Anonymous Function)이다. 이는 전역 스코프에 불필요한 변수를 추가해서 오염시키는 것을 방지할 수 있을 뿐 아니라 IIFE 내부 안으로 다른 변수들이 접근하는 것을 막을 수 있다.

두 번째는 즉시 실행 함수를 생성하는 괄호() 이다. 이를 통해 자바스크립트 엔진은 함수를 즉시 해석해서 실행한다.

아래 함수는 즉시 실행되는 함수 표현이다. 표현 내부의 변수는 외부로부터의 접근이 불가능하다.

(function () {
  let aName = "Tim";
})();
// IIFE 내부에서 정의된 변수는 외부 범위에서 접근이 불가능하다.
aName // ReferenceError: aName is not defined

 

IIFE를 변수에 할당하면 IIFE 자체는 저장되지 않고, 함수가 실행된 결과만 저장된다.

let result = (function() {
  let name = "Tim";
  return name;
})();
// 즉시 결과를 생성한다.
result; // "Tim"

 

IIFE는 값을 리턴할 수 있을 뿐만 아니라 호출될 때, 인자를 받을 수도 있다.

(function IIFE(a, b) {
  for(let i=1; i<=b; i++){
    console.log(a);
  }
}("test", 3));

 

위 예 첫번째 줄에서 IIFE는 a, b 각각의 두 개의 파라미터를 갖는다. 다섯 번째 줄에서 IIFE를 실행할 때, 여태까지 사용했던 빈 괄호 () 대신에 인자(arguments)를 IIFE로 넘기면 두 번째와 세 번째 줄에서 이 파라미터를 IIFE 내부에서 사용한다. jQuery 등 다른 라이브러리에서 이러한 형식이 자주 사용된다.

(function($, global, document) {
  // jQuery를 위해 $사용, window를 위해 global 사용
}(jQuery, window, document));

 

위 예에서 jQuery, window, document를 IIFE에 인자로 넘겼고, IIFE 내부의 코드는 $, global, document를 각각 참조할 수 있다.

 

IIFE는 두 가지 문체로 쓸 수 있다.

(function() {
  alert("test");
}());

(function() {
  alert("test");
})();

 

1. 첫 번째는 함수 식을 호출하기 위한 () 괄호는 바깥 괄호 안에 포함된다. 바깥 괄호가 바깥 함수를 함수식으로 만들기 위해서 필요하다.

2. 두 번째는 함수 식을 호출하기 위한 () 괄호는 함수 표현식을 위한 감싸는 괄호 밖에 있다. 두 가지 모두 다 많이 사용된다.

 

괄호를 생략하는 경우도 있다. 함수 표현식에서 앞 뒤에 괄호를 해주는 이유는 기본적으로 함수가 statement가 아닌 expression이 되도록 만들어주기 위해서인데, 자바스크립트 엔진이 판단하기에 해당 코드가 명확히 함수 표현식이라면 기술적으로 감싸는 괄호를 하지 않아도 된다.

let result = function() {
  return "Hello world";
}();

 

하지만 괄호를 사용하는 것이 함수가 IIFE가 될 것이라는 것을 명확히 알려주는 것이기 때문에 위 예처럼 쓸 필요가 없다 하더래도 가독성을 위해 괄호를 써 주는 것이 좋다.

 


Modules

개발하는 애플리케이션의 크기가 커지면 파일을 여러 개로 분리해야 하는 시점이 온다. 이 때 분리된 파일 각각을 모듈(Module)이라 부른다. 모듈은 보통 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.

즉, 모듈은 파일(스크립트) 하나이다. 모듈에 특수한 지시자 exportimport를 적용하면 다른 모듈을 불러와 불러온 모듈에 있는 함수를 호출하는 등 기능 공유가 가능하다.

  • export 지시자를 변수나 함수 앞에 붙이면 외부 모듈에서 해당 변수나 함수에 접근할 수 있다.(모듈 내보내기)
  • import 지시자를 사용하면 외부 모듈의 기능을 가져올 수 있다.(모듈 가져오기)

export 지시자를 사용해 파일 testOne.js 내부의 함수 testOne을 외부로 내보낸다면

// testOne.js
export function testOne(user) {
  alert(`testOne, ${user}`);
}

 

import 지시자를 사용해 testTwo.js에서 함수 testOne을 사용할 수 있게 해 본다면

// testTwo.js
import {testOne} from './testOne.js';

alert(testOne); // 함수
testOne('Tim'); // testOne, Tim

 

위 예시에서 import 지시자는 상대경로(./testOne.js)를 이용해 모듈을 가져오고 testOne.js에서 내보낸 함수 testOne을 상응하는 변수에 할당한다. 

모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type = "module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 한다.

[index.html]

<!doctype html>
<script type="module">
  import {testOne} from './testOne.js';
  
  document.body.innerHTML = testOne('Tim');
</script>

 

브라우저가 자동으로 모듈을 가져오고 평가한 다음 이를 실행한 것을 확인할 수 있다.

 

모듈의 핵심 기능

모든 호스트 환경에서 공통으로 적용되는 모듈의 핵심 기능이 있다.

 

'엄격 모드'로 실행

 

모듈은 항상 엄격 모드(use strict)로 실행된다. 선언되지 않은 변수에 값을 할당하는 등의 코드는 에러를 발생시킨다.

<script type="module">
  a = 5; // 에러
<script>

 

모듈 레벨 스코프

 

모듈은 자신만의 스코프가 있다. 따라서 모듈 내부에서 정의한 변수나 함수는 다른 스크립트에서 접근할 수 없다.

즉, testOne.js와 testTwo.js를 가져오고 testOne.js에서 선언한 변수 user를 testTwo.js에서 사용하면 에러가 난다.

외부에 공개하려는 모듈은 export 해야 하고, 내보내진 모듈을 가져와 사용하려면 import 해줘야 한다. 전역 변수를 대신해 testTwo.js에 testOne.js를 가져와 필요한 기능을 얻을 수 있다.

//index.html
<!doctype html>
<script type="module" src="testTwo.js"></script>

// testOne.js
export let user = "Tim";

// testTwo.js
import {user} from './testOne.js';

document.body.innerHTML = user; // Tim

 

브라우저 환경에서도 <script type="module">을 사용해 모듈을 만들면 독립적인 스코프가 만들어진다.

<script type="module">
// user는 해당 모듈 안에서만 접근 가능하다.
  let user = "Tim";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>

 

브라우저 환경에서 부득이하게 window 레벨 전역 변수를 만들어야 한다면 window 객체에 변수를 명시적으로 할당하고 window.user와 같이 접근하는 방식을 취하면 된다. (이 방법은 필요한 경우에만 사용하기)

 

한 번만 실행

 

동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 단 한 번만 실행된다. 실행 후 결과는 이 모듈을 가져가려는 모든 모듈에 내보내 진다. alert 함수가 있는 모듈(alert.js)을 여러 모듈에서 가져오는 것을 예로 든다면, alert창은 단 한 번만 나타난다.

// alert.js
alert("모듈 호출");

// 동일한 모듈을 여러 모듈에서 가져오기

// 1.js
import `./alert.js`; // alert창에 '모듈 호출' 출력

// 2.js
import `./alert.js`; // 아무런 반응 x

 

실무에선 최상위 레벨 모듈을 보통 초기화나 내부 데이터 구조를 만들 때 사용한다. 이것들을 내보내 재사용한다.

객체를 내보내는 모듈을 예로 든다면

// admin.js
export let admin = {
  name: "Tim";
};

 

이 모듈을 가져오는 모듈이 여러 개여도 모듈은 최초 호출 시 단 한번만 평가된다. 이때 admin 객체가 만들어지고 이 모듈을 가져오는 모든 모듈에 admin 객체가 전달된다. 각 모듈에 동일한 admin 객체가 전달되는 것이다.

// 1.js
import {admin} from './admin.js';
admin.name = "Peter";

// 2.js
import {admin} from './admin.js';
alert(admin.name); // Peter

// 1.js와 2.js 모두 같은 객체를 가져오므로
// 1.js에서 객체에 가한 조작을 2.js에서도 확인할 수 있다.

 

이런 특징을 이용하면 모듈 설정을 쉽게 할 수 있다. 최초로 실행되는 모듈의 객체 프로퍼티를 원하는 대로 설정하면 다른 모듈에서 이 설정을 그대로 사용할 수 있다.

예시)

// admin.js
export let admin = { };

export function testOne() {
  alert(`${admin.name}, Hi!`);
}

// init.js
import {admin} from './admin.js';
admin.name = "Peter";

// other.js
import {admin, testOne} from './admin.js';

alert(admin.name); // Peter

testOne(); // Peter, Hi!

 

import.meta

 

import.meta 객체는 현재 모듈에 대한 정보를 제공해준다.

호스트 환경에 따라 제공하는 정보의 내용은 다른데, 브라우저 환경에선 스크립트의 url 정보를 얻을 수 있다. HTML 안에 있는 모듈이라면, 현재 실행 중인 웹페이지의 url 정보를 얻을 수 있다.

<script type="module">
  alert(import.meta.url); // script URL(인라인 스크립트가 위치해 있는 html 페이지의 URL)
</script>

 

this는 undefined

 

모듈 최상위 레벨의 this는 undefined이다. 모듈이 아닌 일반 스크립트의 this는 전역 객체인 것과 대조된다.

<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>

 

브라우저의 특정 기능

지연실행

 

모듈 스크립트는 외부 스크립트, 인라인 스크립트와 관계없이 항상 지연 실행된다.

  • 외부 모듈 스크립트 <script type="module" src="...">를 다운로드할 때 브라우저의 HTML 처리가 멈추지 않는다. 브라우저는 외부 모듈 스크립트와 기타 리소스를 병렬적으로 불러온다.
  • 모듈 스크립트는 HTML 문서가 완전히 준비될 때까지 대기 상태에 있다가 HTML 문서가 완전히 만들어진 이후에 실행된다.(모듈의 크기가 작아서 HTML보다 빨리 불러온 경우에도 해당)
  • 스크립트의 상대적 순서가 유지된다. 문서상 위쪽의 스크립트부터 차례로 실행된다.

이런 특징 때문에 모듈 스크립트는 항상 완전한 HTML 페이지를 볼 수 있고 문서 내 요소에도 접근할 수 있다.

모듈 스크립트는 지연 실행되기 때문에 문서 전체가 처리되기 전까지 실행되지 않고, 일반 스크립트는 바로 실행되므로 일반 스크립트는 첫 번째 모듈 스크립트보다 먼저 실행된다는 점에 주의해야 한다.

모듈을 사용할 땐 HTML 페이지가 완전히 나타난 이후에 모듈이 실행된다는 점에 항상 유의해야 한다. 페이지 내 특정 기능이 모듈 스크립트에 의존적인 경우, 모듈이 완전히 로딩되기 전에 페이지만 먼저 사용자에게 노출되면 사용자가 혼란을 느낄 수 있기 때문이다. 이런 경우 모듈 스크립트를 불러오는 동안 투명 오버레이나 로딩 인디케이터(loading indicator)를 보여주면서 혼란을 예방할 수 있다.

 

인라인 스크립트의 비동기 처리

 

일반 스크립트에서 async 속성은 외부 스크립트를 불러올 때만 유효하다. async 속성이 붙은 스크립트는 로딩이 끝나면 다른 스크립트나 HTML 문서가 처리되길 기다리지 않고 바로 실행된다. 반면, 모듈 스크립트에선 async 속성을 인라인 스크립트에도 적용할 수 있다. 아래 인라인 스크립트엔 async 속성이 붙었기 때문에 다른 스크립트나 HTML이 처리되길 기다리지 않고 바로 실행된다. 가져오기(./analytics.js) 작업이 끝나면 HTML 파싱이 끝나지 않았거나 다른 스크립트가 대기 상태에 있더라도 모듈이 바로 실행된다. 이런 특징은 광고나 문서 레벨 이벤트 리스너, 카운터 같이 어디에도 종속되지 않는 기능을 구현할 때 유용하게 사용할 수 있다.

// 필요한 모듈(analytics.js)의 로드가 끝나면
// 문서나 다른 <script>가 로드되길 기다리지 않고 바로 실행된다.
<script async type="module">
  import {counter} from './analytics.js';
  
  counter.count();
</script>

 

외부스크립트

 

type="module"가 붙은 외부 모듈 스크립트엔 두 가지 특징이 있다.

 

1. src 속성 값이 동일한 외부 스크립트는 한 번만 실행된다.

// my.js는 한번만 로드 및 실행된다.
<script type="module" src="my.js"></script>
<script type="module" src="my.js"></script>

 

2. 외부 사이트 같이 다른 오리진에서 모듈 스크립트를 불러오려면 CORS 헤더가 필요하다. 모듈이 저장되어있는 원격 서버가 Access-Control-Allow-Origin: * 헤더를 제공해야만 외부 모듈을 불러올 수 있다. 참고로 * 대신 fetch를 허용할 도메인을 명시할 수도 있다.

// another-site.com이 Access-Control-Allow-Origin을 지원해야만 외부 모듈을 불러올 수 있다.
// 그렇지 않으면 스크립트는 실행되지 않는다.
<script type="module" src="http://another-site.com/their.js"></script>

 

이 특징은 보안을 강화해준다.

 

경로가 없는 모듈은 금지

 

브라우저 환경에서 import는 반드시 상대 혹은 절대 URL 앞에 와야 한다. 경로가 없는 모듈은 허용하지 않는다.

import {testOne} from 'testOne'; // Error
// './testOne.js'와 같이 경로 정보를 지정해줘야 한다.

 

Node.js나 번들링 툴은 경로가 없어도 해당 모듈을 찾을 수 있는 방법을 알기 때문에 경로가 없는 모듈을 사용할 수 있지만 브라우저는 경로 없는 모듈을 지원하지 않는다.

 

호환을 위한 nomodule

 

구식 브라우저는 type="module"을 해석하지 못하기 때문에 모듈 타입의 스크립트를 만나면 이를 무시하고 넘어간다. nomodule 속성을 사용하면 이런 상황을 대비할 수 있다.

<script type="module">
  alert("모던 브라우저");
</script>

<script nomodule>
  alert("type=module을 해석할 수 있는 브라우저는 nomodule 타입의 스크립트는 넘어간다. 따라서 이 alert문은 실행되지 않는다.")
  alert("오래된 브라우저를 사용하고 있다면, type=module이 붙은 스크립트는 무시한다. 대신 이 alert문이 실행된다.")
</script>

 

빌드 툴

브라우저 환경에서 모듈을 단독으로 사용하지 않고 보통 웹팩(Webpack)과 같은 특별한 툴을 사용해 모듈을 한 데 묶어(번들링) 프로덕션 서버에 올리는 방식을 사용한다.

번들러를 사용하면 모듈 분해를 통제할 수 있다. 여기에 더하여 경로가 없는 모듈이나 CSS, HTML 포맷의 모듈을 사용할 수 있게 해 준다는 장점이 있다.

 

빌드 툴의 역할

  • HTML의 <script type="module">에 넣을 주요 모듈(진입점 역할을 하는 모듈)을 선택한다.
  • 주요 모듈에 의존하고 있는 모듈 분석을 시작으로 모듈 간의 의존 관계를 파악한다.
  • 모듈 전체를 한데 모아 하나의 큰 파일을 만든다.(설정에 따라 여러 개의 파일을 만드는 것도 가능) 이 과정에서 import문이 번들러 내 함수로 대체되므로 기존 기능은 그대로 유지된다.
  • 이런 과정 중 변형이나 최적화도 함께 수행된다.

    - 도달 가능하지 않은 코드는 삭제된다.

    - 내보내진 모듈 중 쓰임처가 없는 모듈을 삭제한다.(가지치기(tree-shaking))

    - console, debugger 같은 개발 관련 코드가 삭제된다.

    - 최신 자바스크립트 문법이 사용된 경우 바벨(Babel)을 사용하여 같은 기능을 하는 낮은 버전의 스크립트로 변환한다.

    - 공백 제거, 변수 이름 줄이기 등으로 산출물의 크기를 줄인다.

번들링 툴을 사용하면 스크립트들은 하나 혹은 여러 개의 파일로 번들링 된다. 이때 번들링 전 스크립트에 있던 import/export문은 특별한 번들러 함수로 대체된다. 번들링 과정이 끝나면 기존 스크립트에서 import/export가 사라지기 때문에 type="module"이 필요 없어진다. 따라서 아래와 같이 번들링 과정을 거친 스크립트는 일반 스크립트처럼 취급할 수 있다.

// 웹팩과 같은 툴로 번들링 과정을 거친 스크립트인 bundle.js
<script src="bundle.js"></script>

 

 

참고: https://ko.javascript.info/modules-intro#ref-904

 


Namespaces

전역 변수를 기초로 하는 JavaScript 단점 때문에 여러 스크립트가 한 페이지 안에 함께 있는 소스코드에서는 변수가 많아질수록 이름이 겹칠 우려가 있다. 이러한 단점들을 보완하기 위한 패턴이 네임스페이스 패턴이다.(Namespace pattern)

네임 스페이스란 구분이 가능하도록 정해놓은 범위나 영역을 의미한다. 즉, 이름 공간을 선언해 다른 공간과 구분하도록 하는 것이다.

 

객체 리터럴 네임 스페이싱(Object Literal NameSpacing)

 

하나의 전역 객체를 생성한 다음 모든 함수, 객체, 변수를 이 전역 객체에 추가하여 구현하는 방법이다. 이 방법은 JS  라이브러리나 서드 파티 코드와의 이름 충돌을 방지할 수 있다. 하지만 모든 변수, 함수에 상위 객체명을 붙여야 하기 때문에 코드 양이 많아지고 매번 객체에 접근할 때마다 체인이 길어지고 이름이 중첩된다는 단점이 있다.

// 전역 객체 생성
let test = {};

// 객체의 프로퍼티 추가
test.number = 1;

// 함수 추가
test.numFunc = function() {
  console.log("증가");
}

test.numFuncTwo = function() {
  console.log("감소");
}

 

범용 네임스페이스 함수

 

이미 있는 것을 재정의하는 일을 방지하기 위해 사용하는 방법이다. 이를 확인하지 못한 채 재정의 한다면 내용을 덮어쓰는 문제가 생기기 때문이다.

let MYAPP = {};
// 1. 기본
if(typeof MYAPP === "undefined") {
  let MYAPP = {};
}
// 2. 추천
let MYAPP = MYAPP || {};

 

위의 코드를 객체 생성 시마다 추가하는 것보다는 아래처럼 함수로 별도 생성하는 것이 효과적이다.

let MYAPP = MYAPP || {};

MYAPP.namespace = function(ns_string) {
 let parts = ns_string.split('.')
 parent = MYAPP,
 i;
 
 if(parts[0] === "MYAPP") {
   parts = parts.slice(1);
 }
 
 for(i=0; i < parts.length; i+=1) {
   if(typeof parent[parts[i]] === "undefined") {
     parent[parts[i]] = {};
   }
   
   parent = parent[parts[i]];
 }
 
 return parent;
}

 

위 코드를 아래처럼 사용할 수 있다.

MYAPP.namespace("MYAPP.modules.module2");
// 위 코드는 아래와 같다.
let MYAPP = {
  module : {
    module2: {}
  }
};

let module2 = MYAPP.namespace("MYAPP.modules.module2");
module2 === MYAPP.modules.module2; // true

 

참고: https://joshua1988.github.io/web-development/javascript/javascript-pattern-object/

반응형

댓글