ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/Pattern] 대표적인 자바스크립트, 디자인 패턴 정리 (Javascript, Design Pattern)
    Frontend 2019. 8. 1. 17:59

    디자인 패턴이란 무엇일까? 소프트웨어 개발을 하면서 발생하는 다양한 이슈들을 해결하는데 도움을 주는 일종의 증명된 기술들이다. 이미 많은 개발자들이 자바스크립트를 개발하면서 겪은 다양한 경험들을 바탕으로 만들어진 것들이다. 즉, 이런 상황에서는 이런 패턴을 사용하면 좋을거라는 일종의 방향성을 제시해준다. 패턴들은 정확한 해결책을 제공해주는 것이 아니다. 프레임워크나 라이브러리, 패턴 등은 그저 우리가 자바스크립트로 개발하는데 있어서 도움을 주는 도구일 뿐이고 이 패턴들을 어떻게 활용해서 어떤 식으로 개발한 것인지는 순전히 개발자의 역량에 달려있는 것이다.

     

     

    Modules

    Modules are an integral piece of any robust application's architecture and typically help in keeping the units of code for a project both cleanly separated and organized.

     

    모듈이란 쉽게 말해서 레고 블록의 블록 한 개와 같은 것이다. 모듈은 탄탄한 어플리케이션 구조를 만드는데 꼭 필요한 요소이다. 모듈은 프로젝트의 코드를 깨끗한 구조로 정돈하는데 도움을 준다.

     

    자바스크립트에서 모듈을 만드는 방법에는 여러 가지가 있다.

    • The Module pattern
    • Object literal notation
    • AMD modules
    • CommonJS modules
    • ECMAScript Harmony modules

     

    그 중에서도 제일 간단하고 기본적인 방법은 Object literal notation이다.

     

    - Object Literal

    var module = {
      someProp: 'hello',
      someMethod: function () {
        console.log(this.someProp);
      }
    };

     

    가장 일반적이고 간단한 형태이다. module이란 변수에 중괄호와 함께 프로퍼티와 메소드를 선언하는 형태이다.

     

     

    const phone = {
      battery: 0,
      rechargeBattery: function () {
        battery = 100;
      }
    };
    
    phone.rechargeBattery();
    
    phone.battery = 1000000;

     

    Object literal은 사용하기 매우 간편하고 매우 유용하지만 큰 문제가 있다. 보안에 취약하다는 점이다. 이것은 Object literal만의 문제는 아니다. 자바스크립트는 일반적으로 브라우저에서 사용자에게 직접 노출되기 때문에 보안에 취약할 수밖에 없다. 그래서 우리는 위와 같이 글로벌 공간에 변수를 선언하는 것을 되도록이면 피해야한다. 

     

     

    const phone = (function () {
      let battery = 0;
    
      return {
        rechargeBattery: function () {
          battery = 100;
        },
        showRemainBattery: function () {
          return battery;
        }
      }
    })();
    
    phone.showRemainBattery(); // 0
    phone.rechargeBattery();
    phone.showRemainBattery(); // 100

     

    우리는 클로져나 익명함수 등을 이용하여 최대한 변수가 충돌되지 않도록 보호해야한다. 글로벌 공간에 어쩔 수 없이 변수를 선언해야한다면 Namespace를 이용하는 방법도 있다.

     

    위 방법은 즉시실행함수를 사용하여 객체를 리턴하고 battery 변수를 함수 안에 선언하여 보호하는 방법이다. 그러나 위 방법을 사용하면 새로운 phone을 만들기 위해서 계속해서 똑같은 즉시실행함수를 몇 번이고 적어주어야 한다.

     

     

    function createPhone () {
      let battery = 0;
    
      return {
        rechargeBattery: function () {
          battery = 100;
        },
        showRemainBattery: function () {
          return battery;
        }
      }
    }
    
    const phone1 = createPhone();
    phone1.rechargeBattery()
    
    const phone2 = createPhone();
    
    console.log(phone1.showRemainBattery());  // 100
    console.log(phone2.showRemainBattery());  // 0

     

    우리는 위 코드를 재활용하여 사용하기 위해 아예 createPhone이라는 이름의 함수를 만들어 쓸 수 있다.

    그러나 이 방법도 여전히 createPhone이 글로벌에 선언되므로 완전하지 않다.

     

     

    (function phoneBattery () {
      function createPhone () {
        let battery = 0;
    
        return {
          rechargeBattery: function () {
            battery = 100;
          },
          showRemainBattery: function () {
            return battery;
          }
        }
      }
    
      const phone1 = createPhone();
      const phone2 = createPhone();
    })();

     

    그러므로 createPhone()을 다시 한 번 즉시실행함수로 감싸는 방법을 사용할 수 있다.

     

     

     

    Singletons

    Singletons 패턴은 한 클래스에서 인스턴스를 한 개만 생성하도록 제한한다. singletons 패턴은 인스턴스가 하나도 없는 경우에만 인스턴스를 만드는 메소드를 포함하여 클래스를 생성하는 방법으로 적용할 수 있다. 만약에 이미 인스턴스가 존재한다면 함수는 그 인스턴스의 reference를 리턴하도록 한다.

     

    -아래 예제는 바닐라코딩(Vanilla Coding) 수업 시간에 쓰인 예제 코드 중 일부입니다.-

    var userModule = (function () {
      var users = [];
      var userId = 0;
    
      return {
        create: (username, password) => {
          var user = { id: userId, username, password };
          users.push(user);
          userId++;
    
          return user;
        },
        get: (username) => {
          var targetUser;
    
          users.forEach((user) => {
            if (user.username === username) {
              targetUser = user;
            }
          });
    
          return targetUser;
        }
      };
    })();
    
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Paul', 'hello456'));

     

    singletons 패턴이 필요할 때는 바로 위와 같은 상황에서이다.

     

     

    username이 'Julia'인 데이터는 하나만 있어야하는데, 위 코드를 실행하면 Julia 데이터가 이미 있음에도 불구하고 계속 새로 생성한다.

     

     

    var userModule = (function () {
      var userAddress = {};
      var users = [];
      var userId = 0;
    
      return {
        create: (username, password) => {
          if (!userAddress.hasOwnProperty(username)) {
            var user = { id: userId, username, password };
            userAddress[username] = users.length + '';
            users.push(user);
            userId++;
    
            return user;
          } else {
            return users[userAddress[username]];
          }
        },
        get: (username) => {
          return (userAddress[username]) ? users[userAddress[username]] : null;
        }
      };
    })();
    
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Julia', 'hello123'));
    console.log(userModule.create('Paul', 'hello456'));
    
    console.log(userModule.get('Julia'));
    console.log(userModule.get('Paul'));
    console.log(userModule.get('Mike'));

     

    코드 내용을 살짝 바꾸면 원하는 결과가 나온다.

    아래 코드는 singleton을 적용해 본 다른 예제 코드이다.

     

     

    var mySingleton = (function () {
      var instance;
    
      function init () {
        function privateMethod () {
          console.log("I'm private");
        }
    
        var privateVariable = "I'm also private";
        var privateRandomNumber = Math.random();
    
        return {
          publicMethod: function () {
            console.log('The public can see me!');
          },
          publicProperty: "I'm also public",
          getRandomNumber: function () {
            return privateRandomNumber;
          }
        }
      }
    
      return {
        getInstance: function () {
          if (!instance) {
            instance = init();
          }
    
          return instance;
        }
      }
    })();
    
    var singleA = mySingleton.getInstance();
    var singleB = mySingleton.getInstance();

     

    위와 같이 mySingleton이 리턴하는 객체의 getInstance 메소드는 instance 변수에 아무것도 할당되지 않았을 때는 init 함수를 실행하여 리턴되는 객체를 instance 변수에 할당하고 만약에 이미 instance 변수에 객체가 할당되어 있다면 할당되어있는 인스턴스 변수의 값을 리턴한다.

     

     

    var HTTPModule = (function () {
      var instance = null;
    
      return {
        create: function () {
          if (!instance) {
            instance = {
              get: url => {
                return $.get(url);
              },
              post: (url, data) => {
                return $.post(url, data);
              }
            };
            return instance;
          } else {
            return instance;
          }
        }
      }
    })();
    
    var module1 = HTTPModule.create();
    var module2 = HTTPModule.create();
    
    console.log(module1 === module2);

     

    이 코드는 비동기 상황에서의 singleton 패턴이다.

     

     

     

    Factory Pattern

    Factory pattern은 객체를 생성할 때 고려되는 방법 중 하나이다. factory 패턴은 비슷한 객체를 공장에서 찍어내듯 반복적으로 생성할 수 있게 하는 패턴이다. 이 때 new 키워드를 사용한 constructor 함수가 아닌 그냥 일반 함수에서 객체를 반환하는 것을 우리는 factory function(팩토리 함수)이라고 부른다.

     

    factory 패턴은 다음과 같은 상황에서 매우 유용하다.

     

    • When our object or component setup involves a high level of complexity
    • When we need to easily generate different instances of objects depending on the environment we are in
    • When we're working with many small objects or components that share the same properties
    • When composing objects with instances of other objects that need only satisfy an API contract (aka, duck typing) to work. This is useful for decoupling.

     

    function createCandy () {
      return {
        type: 'candy',
        flavour: 'strawberry',
        sweetness: 9
      };
    }
    
    function createChocolate () {
      return {
        type: 'chocolate',
        flavour: 'almonds',
        sweetness: 7
      };
    }
    
    function createGiftBox () {
      return {
        type: 'gift box',
        contains: [
          createCandy(),
          createChocolate()
        ]
      };
    }
    
    const gift1 = createGiftBox();

     

    create로 시작하는 함수를 호출하면 매번 새로운 인스턴스를 반환할 것이다.

    이런 함수를 팩토리 함수라고 한다. 여러 개의 팩토리 함수를 조합하여 또 다른 팩토리를 만들 수도 있다.

     

     

    function startTeaTime () {
      let time = new Date();
      let hh = time.getHours();
      let mm = time.getMinutes();
      let ss = time.getSeconds();
    
      console.log(`${hh}:${mm}:${ss}, let's have tea!`);
    }
    
    function EarlgreyFactory (count) {
      const tea = {};
    
      tea.teabags = count;
      tea.name = 'Earl grey';
      tea.caffeine = 'strong';
      tea.milkAdded = true;
      tea.drink = function () {
        this.teabags -= 1;
        console.log(`${this.teabags} tea bags left`);
      }
      tea.start = startTeaTime;
    
      return tea;
    }
    
    function sconeFactory (count) {
      const scone = {};
    
      scone.countScones = count;
      scone.name = 'scone';
      scone.sweetness = 6;
      scone.eat = function () {
        this.countScones -= 1;
        console.log(`${this.countScones} scones left`);
      }
      scone.start = startTeaTime;
    
      return scone;
    }
    
    const blackTea = EarlgreyFactory(10);
    const chocolateScone = sconeFactory(6);
    
    chocolateScone.start();
    blackTea.drink();
    chocolateScone.eat();

     

    위 코드를 보면 두 개의 팩토리 함수가 startTeaTime이라는 똑같은 메소드를 공유하고 있다.

    위와 같이 쓰지 않고 좀 더 간단하게 코드를 공유하는 Mixin 패턴을 살펴보자.

     

     

    Mixin

    Mixin 패턴은 한 객체의 프로퍼티를 다른 객체에 복사해서 사용하는 패턴을 말한다. 이 패턴은 주로 기존에 있던 객체의 기능을 그대로 보존하면서 다른 객체에 추가할 때 사용한다. 

     

    function EarlgreyFactory (count) {
      const tea = {};
    
      tea.teabags = count;
      tea.name = 'Earl grey';
      tea.caffeine = 'strong';
      tea.milkAdded = true;
      tea.drink = function () {
        this.teabags -= 1;
        console.log(`${this.teabags} tea bags left`);
      }
    
      return tea;
    }
    
    function sconeFactory (count) {
      const scone = {};
    
      scone.countScones = count;
      scone.name = 'scone';
      scone.sweetness = 6;
      scone.eat = function () {
        this.countScones -= 1;
        console.log(`${this.countScones} scones left`);
      }
    
      return scone;
    }
    
    const blackTea = EarlgreyFactory(10);
    const chocolateScone = sconeFactory(6);
    
    function startTeaPartyMixin (food) {
      food.start = function () {
        let time = new Date();
        let hh = time.getHours();
        let mm = time.getMinutes();
        let ss = time.getSeconds();
    
        console.log(`${hh}:${mm}:${ss}, let's have tea!`);
      };
    }
    
    startTeaPartyMixin(blackTea);
    blackTea.start();

     

    위 코드를 보면 Factory 함수로부터 생성된 객체 blackTea를 startTeaPartyMixin() 함수에 넣어

    start라는 프로퍼티를 추가하여 사용하는 것을 확인할 수 있다.

     

     

    function Robot (usage) {
      this.usage = usage;
      this.battery = 100;
      this.owner = 'Julia';
    }
    
    batteryMixin(Robot.prototype);
    
    Robot.prototype.soldTo = function (newOwner) {
      this.owner = newOwner;
    }
    
    function SmartPhone (brand) {
      this.brand = brand;
      this.battery = 100;
    }
    
    batteryMixin(SmartPhone.prototype);
    
    function batteryMixin (target) {
      target.rechargeBattery = function () {
        let that = this;
    
        setTimeout(function () {
          that.battery = 100;
          console.log(`${that.battery}% charged`);
        }, 1000);
      }
    
      target.turnOn = function () {
        let that = this;
    
        setTimeout(function () {
          that.battery -= 80;
          console.log(`${that.battery}% of battery remaining`);
        }, 1000);
      }
    }

     

    이렇게 Mixin 패턴을 사용하여 여러 생성자 함수에 프로퍼티와 메소드들을 복사할 수 있다.

    Mixin 패턴을 사용하는 주된 목적은 코드를 재사용(re-use)하는 것에 있다.

    그렇다면 Mixin 패턴이 아닌 다른 재사용 방법에는 뭐가 있을까?

     

     

     

    Behavior Delegation / Inheritance

    Behavior Delegation이란 부모 프로토타입에 저장되어있는 변수나 메소드들을 위임받아 사용하는 것을 말한다.

     

    function Machine () {
      this.battery = 100;
    }
    
    Machine.prototype.rechargeBattery = function () {
      let that = this;
    
      setTimeout(function () {
        that.battery = 100;
        console.log(`${that.battery}% charged`);
      }, 1000);
    };
    
    Machine.prototype.turnOn = function () {
      let that = this;
    
      setTimeout(function () {
        that.battery -= 80;
        console.log(`${that.battery}% of battery remaining`);
      }, 1000);
    };
    
    function Robot (usage, owner) {
      Machine.call(this);
      this.usage = usage;
      this.owner = owner;
    }
    
    Robot.prototype = Object.create(Machine.prototype);
    Robot.prototype.constructor = Robot;
    
    Robot.prototype.soldTo = function (newOwner) {
      this.owner = newOwner;
    };
    
    function SmartPhone (brand) {
      Machine.call(this);
      this.brand = brand;
    }
    
    SmartPhone.prototype = Object.create(Machine.prototype);
    SmartPhone.prototype.constructor = SmartPhone;
    
    const myPhone = new SmartPhone('Apple');
    myPhone.turnOn();
    myPhone.rechargeBattery();

     

    위 예제 코드를 살펴보면 Machine이라는 상위 클래스가 있고,

    Machine의 프로토타입에는 rechargeBattery와 turnOn이란 메소드가 있다.

     

    그리고 Machine의 프로토타입을 프로토타입으로 하는 Robot과 SmartPhone이란 하위 클래스를 만들었다.

    여기서 Robot과 SmartPhone은 Machine의 프로토타입을 공유하고 있다.

    그렇기 때문에 Robot과 SmartPhone의 인스턴스들은 원할 때 언제든지 상위 프로토타입으로부터

    rechargeBattery나 turnOn과 같은 메소드들을 위임받아 사용할 수 있다.

     

     

     


    참고자료:

    https://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript

     

    Learning JavaScript Design Patterns

     

    addyosmani.com

     

    반응형

    COMMENT