본문으로 건너뛰기
SoulLog

자바스크립트 패턴 — 3.3 모듈 패턴

17분 읽기

모듈 패턴은 자바스크립트에서 가장 명망 높은 패턴 중 하나입니다. 임의로 함수를 호출하여 생성하는 임의 모듈 생성과 선언과 동시에 실행하는 즉시 실행 모듈 생성, 이 두 가지 유형이 있습니다.

모듈의 목적은 서로 다른 소스를 조합하여 거대한 프로그램을 만드는 것이고, 모듈 제작자가 예상하지 않은 코드가 있더라도 모든 코드가 제대로 작동하는 것입니다.

모듈 패턴이 명망 높은 이유를 좀 더 살펴보면 다음과 같습니다.

  • 느슨한 결합성: 여러 모듈을 함께 사용할 때 서로 영향을 주지 않도록 잘 구조화된 코드를 지원합니다.
  • 캡슐화: 자바스크립트에는 private, public 같은 키워드가 없지만, 모듈 패턴을 통해 구현이 가능합니다.
  • ES5 호환: ES5에서는 기본적으로 모듈 기능이 없어 이 기법이 널리 사용되었습니다.

위의 내용을 종합해보면, 객체지향 프로그래밍에 익숙한 많은 개발자들이 자바스크립트에서도 비슷한 방식으로 개발하기 위해 이 패턴을 널리 사용해왔고, 그 때문에 명망 높은 패턴으로 자리매김하게 된 것으로 보입니다.


기본 모듈

모듈을 만드는 가장 기본적인 방식은 앞서 배웠던 객체 리터럴을 활용하는 방식입니다.

var MyApp = {
    author : 'MostOneBrush',
    msg : function (words) {
        if (!words) {
            return `There is no words`
        } else {
            return `Welcome to ${this.author}'s house`
        }
    },
}

위 모듈에서 매개변수에 값을 지정하지 않으면 There is no words를, 어떠한 값이라도 존재하는 경우 내부의 author 속성에 저장된 'MostOneBrush'를 전달받아 Welcome to MostOneBrush's house를 반환합니다.

var testNothing, testWord1, testWord2;
var MyApp = {
    author : 'MostOneBrush',
    msg : function (words) {
        if (!words) {
            return `There is no words`
        } else {
            return `Welcome to ${this.author}'s house`
        }
    },
}
 
testNothing = MyApp.msg()
testWord1 = MyApp.msg(1)
testWord2 = MyApp.msg('Water hyacinth')
 
console.log(testNothing)  // There is no words
console.log(testWord1) // Welcome to MostOneBrush's house
console.log(testWord2) // Welcome to MostOneBrush's house

기대한 대로 값을 반환하는 것을 확인했습니다. 하지만 다음 예제를 보면 문제가 드러납니다.

var MyApp = MyApp || {}
var testNothing, testWord1, testWord2;
MyApp = {
    author : 'MostOneBrush',
    msg : function (words) {
        if (!words) {
            return `There is no words`
        } else {
            return `Welcome to ${this.author}'s house`
        }
    },
}
 
testNothing = MyApp.msg()
testWord1 = MyApp.msg(1)
testWord2 = MyApp.msg('Water hyacinth')
 
MyApp.author = 'Hacker'
 
console.log(testNothing)  // There is no words
console.log(testWord1) // Welcome to MostOneBrush's house
console.log(testWord2) // Welcome to MostOneBrush's house
 
testWord1 = MyApp.msg(3)
testWord2 = MyApp.msg()
 
console.warn(testNothing) // --- (1)
console.warn(testWord1) // --- (2)
console.warn(testWord2) // --- (3)
 
testWord2 = MyApp.msg('Water hyacinth')
console.warn(testWord2) // --- (4)

testWord1의 결과만 변경되길 희망했는데, testWord2에도 영향을 미칩니다.

이처럼 객체 리터럴을 활용한 기본 모듈은 하나의 개입이 다른 부분에까지 영향을 미칠 수 있는 위험성이 존재합니다. 외부의 개입으로 인해 변질되지 않는 방안을 찾아 새로운 모듈의 필요성이 생겼고, 그것이 클로저를 활용하는 임의 모듈 형태입니다.


클로저를 활용한 임의 모듈 생성

원래 구상했던 모듈의 기능을 다시 정리해보면 다음과 같습니다.

  1. 매개변수에 값이 있을 경우 → 모듈 내에 지정된 author 값을 받아 Welcome to MostOneBrush's house 반환
  2. 매개변수에 값이 없을 경우 → There is no words 반환

클로저를 활용하면 이 이슈를 해결할 수 있습니다. 클로저는 본래 가진 고유한 본질이나 기능을 기억하고, 변화한 환경에 맞춰 현재의 상태를 업데이트할 수 있는 함수를 말합니다.

var testNothing, testWord1, testWord2
var MyApp = MyApp || function (words) {
    var author = 'MostOneBrush' // property 대신 상단에 변수를 지정합니다.
    return {
        msg : function () {
            if (!words) {
                return `There is no words`
            } else {
                return `Welcome to ${author}'s house`
            }
        },
    }
}
 
testNothing = MyApp()
testWord1 = MyApp(1)
testWord2 = MyApp(2)
 
console.log(testNothing.msg()) // There is no words
console.log(testWord1.msg()) // Welcome to MostOneBrush's house
console.log(testWord2.msg()) // Welcome to MostOneBrush's house

기존에는 객체 내부의 property로 설정했던 author에 외부 개입이 가능했습니다. 하지만 내부 함수의 변수로 변경하게 되면서 외부에서 이 값을 변경할 수 없게 되었습니다.

사실 이와 같은 변수를 프라이빗 변수라고 부르는데, console.log로 찍으면 값이 보이기 때문에 왜 private라고 부르는지 의문이 들었습니다.

console.log로도 해당 변수와 값을 찾을 수 있는데 왜 private 변수라고 할까요?

현재로서는, 여기서 말하는 private의 의미는 **외부 다른 코드에 영향을 주지 않고 해당 함수 내에서만 쓰는 변수(함수 전용 변수)**로 이해하면 됩니다. 이 변수는 내부에서 직접 변경하지 않는 한 외부에서 아무리 값을 변경하려 해도 변하지 않는 고유한 속성을 지닙니다.

var testNothing, testWord1, testWord2
var MyApp = MyApp || function (words) {
    var author = 'MostOneBrush'
    return {
        msg : function () {
            if (!words) {
                return `There is no words`
            } else {
                return `Welcome to ${author}'s house`
            }
        },
    }
}
 
testNothing = MyApp()
testWord1 = MyApp(1)
testWord2 = MyApp(2)
console.log(testNothing.msg()) // There is no words
console.log(testWord1.msg()) // Welcome to MostOneBrush's house
console.log(testWord2.msg()) // Welcome to MostOneBrush's house
 
var author = 'Hacker'
console.log(testWord1.msg()) // Welcome to MostOneBrush's house

원하던 결과를 얻었습니다. 이번에는 author에 변화를 주고 싶을 때만 변화를 줄 수 있는지도 확인해보았습니다.

var testNothing, testWord1, testWord2
var MyApp = MyApp || function (name) {
    if (name) name = 'MostOneBrush'
    return {
        author : function (authorName) {
            name = authorName
        },
        msg : function () {
            if (!name) {
                return `There is no words`
            } else {
                return `Welcome to ${name}'s house`
            }
        }
    }
}
 
testNothing = MyApp()
testWord1 = MyApp(1)
testWord2 = MyApp('Water hyacinth')
 
console.log(testNothing.msg()) // There is no words
console.log(testWord1.msg()) // Welcome to MostOneBrush's house
console.log(testWord2.msg()) // Welcome to MostOneBrush's house
 
testWord1.author('Hacker')
 
console.log(testNothing.msg()) // --- (1)
console.log(testWord1.msg()) // --- (2)
console.log(testWord2.msg()) // --- (3)
 
testWord1 = MyApp('Hi')
testWord2 = MyApp('Oppa')
 
console.warn(testNothing.msg()) // --- (4)
console.warn(testWord1.msg()) // --- (5)
console.warn(testWord2.msg()) // --- (6)

각 변수에 할당된 함수들은 서로에게 영향을 주지 않고 독립적으로 동작합니다.

여기서 3가지를 확인할 수 있습니다.

  1. testWord1에서 author 메서드를 사용하여 반환값을 변경해도 다른 변수에는 영향을 미치지 않습니다. 각 변수에 할당된 함수들은 독립적으로 행동하며, 내부 변수나 메서드도 고유한 스코프를 가집니다.
  2. 메서드로 값을 변경한 뒤 testWord1에 동일한 방식으로 재할당하면 원래의 Welcome to MostOneBrush's house가 반환됩니다. 클로저가 자신이 태어났을 때의 환경을 기억하고 있기 때문입니다.
  3. return 안에 들어있지 않은 변수 및 함수에는 접근이 불가능하지만, return 안에 들어가 반환되는 메서드에는 외부에서 접근이 가능합니다. 이것을 public(공개된) 요소라고 일컫습니다.

즉시실행모듈 생성

즉시실행함수(IIFE)의 기본 형태는 다음과 같습니다.

/* 즉시 실행 함수 (기본) */
(function() {
    console.log('즉시 실행 함수')
}());
 
/* ES6 arrow function 형태로 작성할 경우 */
(() => {
    console.log('즉시 실행 함수 (화살표)')
})();

즉시실행함수는 다양한 이유로 사용되지만, 주로 다음의 이유로 쓰입니다.

  • 즉시 실행되어야 하지만 전역 스코프를 오염시키고 싶지 않을 때
  • 한 번의 실행만 필요로 하는 초기화 코드에 사용할 때
  • 라이브러리 전역 변수의 충돌을 방지하기 위해

앞서 사용했던 내용을 즉시실행함수로 변환하면 다음과 같습니다.

var MyApp = MyApp || (function (){
    var author = 'MostOneBrush'
    var authorFunc =  function (authorName) {
        author = authorName
    }
    var msgFunc = function (name) {
        if (!name) {
            return `There is no words`
        } else {
            return `Welcome to ${author}'s house`
        }
    }
    return {
        author : authorFunc,
        msg : msgFunc
    }
})();

임의 모듈 방식과 비교했을 때 기능상의 큰 차이는 없으나, 초기화의 목적전역 변수의 충돌 방지라는 이점에서 이 방식을 선호하는 것으로 보입니다.


모듈 생성의 원칙

모듈 패턴을 사용할 때 항상 고려해야 하는 사항입니다.

  • 한 모듈에 한 가지 일만 시킵니다.
  • 모듈 자신이 쓸 객체가 필요하다면 의존성 주입 형태로 객체를 제공하는 방안을 고려합니다.
  • 다른 객체 로직을 확장하는 모듈은 해당 로직의 의도가 바뀌지 않도록 분명히 밝힙니다.

용어 정리

네임스페이스

모듈 패턴을 구성하는 요소 중 하나로, 코드 내의 이름 충돌뿐만 아니라 같은 페이지 내에 존재하는 자바스크립트 라이브러리·위젯 등과의 이름 충돌을 방지하기 위해 사용합니다.

var MyApp = MyApp || {}

일반적으로는 var MyApp = {}로 선언하겠지만, 논리연산자를 사용하는 이유가 있습니다.

// example.js
var MyApp = 'Hello World'
<script src="./example.js"></script>
<script>
    var MyApp = {}
    console.log(MyApp) // {}  ← 기존의 'Hello World'가 덮어씌워집니다.
</script>

외부 파일에 선언된 MyApp이 덮어씌워지는 것을 확인할 수 있습니다.

하지만 MyApp = MyApp || {} 방식으로 선언하면 기존에 선언된 내용이 변질되는 것을 방지할 수 있습니다.

example.js의 MyApp이 유지되는 것을 확인할 수 있습니다.

물론 ES6의 let이나 const를 사용하면 이미 선언된 변수에 대한 에러가 발생하여 방지할 수 있습니다. 하지만 네임스페이스 패턴을 활용하면 기존에 무엇으로 사용되었는지를 알 수 있다는 점에서 이점이 있습니다.

자바스크립트의 캡슐화

객체의 외부에서는 접근할 수 없게 만드는, 외부에 감춰진 속성이나 메서드를 의미합니다(은닉성). 이 캡슐화를 통해 값이 변질되지 않고 통제가 가능하기 때문에 모듈 패턴을 선호하는 것으로 추측됩니다.

스코프

자바스크립트에서 스코프(Scope)는 유효범위를 뜻합니다. 특히 다음과 같은 경우에 자주 참조됩니다.

  • 서로 연관 있는 변수 및 함수가 서로 어느 범위까지 영향을 주는가
  • 선언된 변수가 어디까지 영향을 주는가 (지역변수, 전역변수 등)

참고: Scope | PoiemaWeb

클로저

클로저는 기본적으로 중첩된 함수에서 나오는 개념입니다.

function outerFunc () {
    var x = 10
    var innerFunc = function () { console.log(x); };
    return innerFunc;
}
 
var a = outerFunc()
a()

실행 컨텍스트의 개념에 따르면 위의 내용은 다음과 같은 과정을 거칩니다.

  1. 자바스크립트 엔진이 실행에 필요한 정보들을 담을 객체들을 생성합니다. (전역 함수 outerFunc, 지역 변수 x, 내부 함수 innerFunc)
  2. 스코프 체인(각각의 함수 및 변수의 유효 범위 및 관계도)을 만들어 저장합니다. 이 과정을 통해 실행 순서를 스택에 쌓습니다.
  3. 지정한 순서대로 실행되고 종료되면서 outerFunc의 라이프사이클이 끝납니다.

이 때 outerFunc 함수 객체는 생성되는 과정에서 [[Scopes]] 프로퍼티를 갖게 됩니다.

실험삼아 outerVar라는 전역변수도 선언해봤지만, [[Scopes]] 프로퍼티는 outerFunc 함수에만 생성됩니다.

[[Scopes]] 프로퍼티는 outerFunc 함수 객체가 실행되는 환경의 정보를 담고 있습니다. 일반적인 스코프 개념대로라면 중괄호가 닫히는 시점에 함수의 역할이 마무리되어야 하겠지만, 위와 같이 전역에서 참조가 가능해지면서 함수 선언이 끝난 이후에도 접근이 가능해집니다.

innerFuncouterFunc의 내부에서 존재했지만 return을 통해 외부로 반환되었음에도 불구하고, 처음 생성되었던 환경(Lexical environment)을 기억하고 그 환경에 다시 접근할 수 있습니다. 이것이 바로 클로저입니다.

요약하자면, 자신의 고유한 환경을 기억해서 본질적인 성질이 변하지 않으면서도, 환경 변화에 맞춰 자신의 상태를 업데이트할 수 있는 함수를 클로저라고 부릅니다.

참고: 클로저 - 생활코딩

실행 컨텍스트

클로저의 개념을 이해하기 위해서는 자바스크립트의 실행 컨텍스트에 대한 이해가 필요합니다.

참고: Execution Context | PoiemaWeb