본문 바로가기
JAVASCRIPT

[JS][스코프 클로저] 모듈

by KBS 2022. 2. 19.
728x90

모듈

클로저의 능력을 활용하면서 표면적으로는 콜백과 상관없는 코드 패턴들이 있다. 그중 가장 강력한 패턴인 모듈을 살펴보자.

function foo() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log(something);
  }

  function doAnother() {
    console.log(another.join("!"));
  }
}

 

이 코드에는 클로저의 흔적이 보이지 않는다. 우리가 볼 수 있는 것은 몇 가지 비공개 데이터 변수인 something과 another 그리고 내부 함수 doSomething()과 doAnother()가 있다. 이들 모두 foo()의 내부 스코프를 렉시컬 스코프로 가진다.

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log(something);
  }

  function doAnother() {
    console.log(another.join("!"));
  }

  return {
    doSomething: doSomething,
    doAnother: doAnother,
  };
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

 

이 코드와 같은 자바스크립트 패턴을 모듈이라고 부른다. 가장 흔한 모듈 패턴 구현 방법은 모듈 노출이고, 앞의 코드는 이것의 변형이다.

먼저, 앞의 코드에서 몇가지를 살펴보자.

첫째. CoolModule()은 그저 하나의 함수일 뿐이지만, 모듈 인스턴스를 생성하려면 반드시 호출해야 한다. 최외곽 함수가 실해오디지 않으면 내부 스코프와 클로저는 생성되지 않는다.

둘째, CoolModule() 함수는 객체를 반환한다. 반환되는 객체는 객채 - 리터럴 문법 {key: value, ... }에 따라 표기된다. 해당 객체는 내장 함수들에 대한 참조를 가지지만, 내장 데이터 변수에 대한 참조는 가지지 않는다. 내장 데이터 변수는 비공객로 숨겨져 있다. 이 객체의 반환값은 본질적으로 모듈의 공개 API라고 생각할 수 있다.

객체의 반환 값은 최종적으로 외부 변수 foo에 대입되고, foo.doSomething()과 같은 방식으로 API의 속성 메서드에 접근할 수 있다.

모듈에서 꼭 실제 객체를 반환할 필요 없이 직접 내부 함수를 반환해도 된다. 제이쿼리가 이런 반환을 하는 좋은 사례다. 제이쿼리와 $확인자는 제이쿼리 '모듈'의 공개 API이고, 동시에 그 자신들은 단순한 함수이기도 하다.

 

함수 doSomething()doAnother()는 모듈 인스턴스의 내부 스코프에 포함되는 클로저를 가진다. 반환된 객체에 대한 속성 참조 방식으로 이 함수들을 해당 렉시컬 스코프 밖으로 옮길 대 클로저를 확인하고 이용할 수 있는 조건을 하나 세웠다.

쉽게 말해, 이 모듈 패턴을 사용하려면 두 가지 조건이 있다.

  1. 하나의 최외곽 함수가 존재하고, 이 함수가 최소 한 번은 호출되어야 한다.
  2. 최외곽 함수는 최소 한 번은 하나의 내부 함수를 반환해야 한다. 그래야 해당 내부 함수가 비공개 스코프에 대한 클로저를 가져 비공개 상태에 접근하고 수정할 수 있다.

하나의 함수 속성만을 가지는 객체는 진정한 모듈이 아니다. 함수 실행 결과로 반환된 객체에 데이터 속성들은 있지만 닫힌 함수가 없다면, 당연히 그 객체는 진정한 모듈이 아니다.

앞의 코드는 독립된 모듈 생성자 CoolModule()을 가지고, 생성자는 몇 번이든 호출할 수 있고 호출할 때마다 새로운 모듈 인스턴스를 생성한다. 이 패턴에서 약간 변경된 오직 하나의 인스턴스 '싱글톤'만 생성하는 모듈을 살펴보자.

var foo = (function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log(something);
  }

  function doAnother() {
    console.log(another.join("!"));
  }

  return {
    doSomething: doSomething,
    doAnother: doAnother,
  };
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

 

앞의 코드에서 모듈 함수를 IIFE로 바꾸고 즉시 실행시켜 반환 값을 직접 하나의 모듈 인스턴스 확인자 foo에 대입시켰다.

모듈은 함수이므로 다음 코드처럼 인자를 받을 수 있다.

function CoolModule(id) {
  function indentify() {
    console.log(id);
  }

  return {
    identify: identify,
  };
}

var foo1 = CoolModule("foo 1");
var foo2 = CoolModule("foo 2");

foo1.identify(); // foo 1
foo2.identify(); // foo 2

 

약간 변형한 효과적인 모듈 패턴 중 또 하나는 다음 콰드와 같이 공개 API로 반환하는 객체에 이름을 정하는 방식이다.

var foo = (function CoolModule(id) {
  function change() {
    publicAPI.identify = identify2;
  }

  function identify1() {
    console.log(id);
  }

  function identify2() {
    console.log(id.toUpperCase());
  }

  var publicAPI = {
    change: change,
    identify: identify1,
  };

  return publicAPI;
})("foo module");

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

 

공개 API 객체에 대한 내부 참조를 모듈 인스턴스 내부에 유지하면, 모듈 인스턴스를 내부에서 부터 메서드와 속성을 추가 또는 삭제하거나 값을 변경하는 식으로 수정할 수 있다.

현재의 모듈

만흥ㄴ 모듈 의존성 로더와 관리자는 본질적으로 이 패턴의 모듈 정의를 친숙한 API 형태로 감싸고 있다. 특정한 하나의 라이브러리를 살펴보기보다는 개념을 설명하기 위해 마우 단순한 증명을 제시하겠다.

var MyModules = (function Manager() {
  var modules = {};

  function define(name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
    }

    modules[name] = impl.apply(impl, deps);
  }

  function get(name) {
    return modules[name];
  }

  return {
    define,
    get,
  };
})();

 

이 코드의 핵심부는 modules[name] = impl.apply(impl, deps)다. 이부분은 모듈에 대한 정의 래퍼 함수를 호출하여 반환 값인 모듈 API를 이름으로 정리된 내부 모듈 리스트에 저장한다.

해당 부분을 이용해 모듈을 정의하는 다음 코드를 보자.

MyModules.define("bar", [], function () {
  function hello(who) {
    return "Let me introduce: " + who;
  }

  return {
    hello,
  };
});

MyModules.define("foo", ["bar"], function (bar) {
  var hungry = "hippo";
  function awesome() {
    console.log(bar.hello(hungry).toUpperCase());
  }

  return {
    awesome,
  };
});

var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

 

foobar 모듈은 모두 공개 API를 반환하는 함수로 정의 됐다. foo는 심지어 bar의 인스턴스를 의존성 인자로 받아 사용할 수도 있다.

찬찬히 코드를 살펴보면 목적에 따라 사용된 클로저의 힘을 완전히 이애할 수 있다. 모듈 관리자를 만드는 특별한 마법이란 존재하지 않는다는 것을 기억해야 한다. 모든 모듈 관리자는 앞에서 언급한 모듈패턴의 특성을 모두 가진다. 즉, 이들은 함수 정의 래퍼를 호출하여 해당 모듈의 API인 반환 값을 저장한다. 좀 더 쓰기 편하게 포장한다고 해도 모듈은 그저 모듈일 뿐이다.

미래의 모듈

ES6는 모듈 개념을 지원하는 최신 문법을 추가했다. 모듈 시스템을 불러올 때 ES6는 파일을 개별 모듈로 처리한다. 각 모듈은 다른 모듈 또는 특정 API 멤버를 불러오거나 자신의 공개 API 멤버를 내보낼 수 도 있다.

함수 기반 모듈은 정적으로 알려진 패턴이 아니다. 따라서 이들 API의 의미는 런타임 전가지 해석되지 않는다. 즉, 실제로 모듈의 API를 런타임에 수정할 수 있다는 말이다. 반면, ES6 모듈 API는 정적이다. 따라서 컴파일러는 이 사실을 이미 알고있어서 컴파일레이션 중에 불러온 모듈의 API 멤버참조가 실제로 존재하는지 확인할 수 있다. API참조가 존재하지 않으면 컴파일러는 컴파일 시 초기 오류를 발생시킨다. 전통적인 방식처럼 변수 참조를 위해 동적 런타임까지 기다리지 않는다.

 

ES6 모듈은 inline 형식을 지원하지 않고, 반드시 개별 파일에 정의되어야 한다. 브라우저와 엔진은 기본 모듈 로더를 가진다. 모듈을 불러올 때 모듈 로더는 동기적으로 모듈 파일을 불러온다.

// bar.js
function hello(who){
  reutrn "Let me introduce: " + who;
}

export hello;

 

// foo.js : import only 'hello()' from the "bar" module
import hello from "bar";

var hungry = "hippo";

function awesome(){
  console.log(
    hello(hungry).toUpperCase();
  )
}

export awesome;

 

// baz.js : import the entire "foo" and "bar" modules
module foo from "foo";
module bar from "bar";

console.log(bar.hello("rhino")); // Let me introduce: rhino

foo.awesome(); // LET ME INTRODUCE: HIPPO

 

코드의 처음 두부분을 가지고 해당 내용이 포함된 'foo.js'와 'bar.js' 파일이 각각 생성된다. 그러면 세 번째 부분 'baz.js' 프로그램이 이들 모듈을 불러와 사용한다.

 

키워드 import 는 모듈 API에서 하나 이상의 멤버를 불러와 특정 변수에 묶어 현재 스코프에 저장한다. 키워드 module은 모듈 API 전체를 불러와 특정 변수에 묶는다. 키워드 export는 확인자를 현재 모듈의 공개 API로 내보낸다. 이 연산자들은 모듈의 정의에 따라 필요하면 얼마든지 사용할 수 있다.

앞서 살펴본 함수 - 클로저 모듈처럼 모듈 파일의 내용은 스코프 클로저에 감싸진 것으로 처리된다.

정리하기

편견에 찬 이들은 클로저를 자바스크립트 세계에서 홀로 떨어진, 가장 용감한 소수만이 닿을 수 있는 신비의 세계로 생각하는 것 같다. 그러나 클로저는 사실 표준이고, 함수를 값으로 마음대로 넘길 수 있는 렉시컬 스코프 환경에서 코드를 작성하는 방법이다.

클로저는 함수를 렉시컬 스코프 밖에서 호출해도 함수는 자신의 렉시컬 스코프를 기억하고 접근할 수 있는 특성을 말한다.

반복문을 예로 들면, 클로저를 통해 설사 우리가 기억하지 못했을 지라도 반복문이 어떻게 작동하는지 추적해 갈 수 있따. 또한, 클로저는 다양한 형태의 모듈 패턴을 가증하게 하는 매우 효과적인 도구이기도 하다.

모듈은 두 가지 특징을 가져야 한다.

  1. 최외곽 래퍼 함수를 호출하여 외곽 스코프를 생성한다.
  2. 래핑 함수의 반환 값은 반드시 하나 이상의 내부 함수 참조를 가져야 하고, 그 내부 함수는 래퍼의 비공개 내부 스코프에 대한 클로저를 가져야 한다.

이제 우리에게는 모든 코드에서 클로버를 볼 수 있고, 파악하고 활용할 수 있는 능력이 생겼다.

 


참고

  • You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )

 

728x90

댓글