코드노트

자바스크립트 디자인 패턴 정리 본문

Code note/자바스크립트

자바스크립트 디자인 패턴 정리

코드노트 2024. 6. 24. 18:27

 

 
디자인 패턴은 면접을 볼때 많이 물어보는 질문중 하나였다.
어떻게 보면 여러 디자인 패턴이 있지만 처음 디자인 패턴이라는 이야기를 들었을때에는 디자인이라는 단어 때문에
UI를 다루는 패턴인가? 라는 생각을 했던것 같다. 비전공자인.. 특히 나는 디자이너로 시작하여 개발자로 이직을 했다보니
더 그렇게 생각했었다. 그래서 첫 면접에서 당당히 나는 아토믹 패턴을 구구절절 이야기했었던 기억이 있다...ㅋ
 
그렇게 프론트엔드 개발을 공부하면서 디자인 패턴에 대해서 많이 알게 되었고, 현재 회사에서도 여러 패턴들을 적용하며 코드를 보다보니 내가 몰랐던 패턴들도 같이 정리를 해보려고 한다!


생성 패턴 (Creational)

- 객체의 생성 방식에 중점을 둔다.
- 객체 생성 과정에서 복잡성을 줄이고, 코드의 유연성과 재사용성을 높이기 위해 객체 생성 로직을 추상화 한다.
- 객체를 생성하거나 초기화하는 방법을 정의하여 객체 생성의 불필요한 복잡성을 제거하고, 객체 생성을 캡슐화 한다.

▶ 생성자 패턴 (Constructor)

  • 객체를 생성하는 가장 기본적인 방법중에 하나이다. new 키워드를 사용하여 클래스의 인스턴스를 생성한다. 자바스크립트에서는 함수가 생성자의 역할을 할 수 있다. 처음 클래스를 사용할때 알고있는 new 연산자를 통하여 생성하는 패턴이니 간단하다.
// 생성자 함수 정의
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 인스턴스 생성
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);

console.log(person1.name); // Alice
console.log(person2.age); // 30

 팩토리 패턴 (Factory)

  • 객체 생성 로직을 캡슐화하여 특정 조건에 따라 다른 클래스의 인스턴스를 반환하는 패턴이다. 복잡한 객체 생성 로직을 숨기고, 코드의 유연성과 재사용성을 높인다.
  • 예제 코드만 보게되면 상속을 사용하는건가 라고 생각할 수 있지만 상속을 하지 않더라도 내부에서 조건으로 객체 생성 로직을 캡슐화 하여 객체를 생성하게도 할 수 있다.( 객체 생성 로직을 캡슐화하여 객체 생성방식을 분리하는것 )
// Animal 클래스 정의
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

// Dog 클래스 정의
class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

// Cat 클래스 정의
class Cat extends Animal {
  speak() {
    console.log(`${this.name} meows.`);
  }
}

// 팩토리 함수 정의
function AnimalFactory(type, name) {
  switch (type) {
    case 'dog':
      return new Dog(name);
    case 'cat':
      return new Cat(name);
    default:
      return new Animal(name);
  }
}

// 인스턴스 생성
const dog = AnimalFactory('dog', 'Rex');
const cat = AnimalFactory('cat', 'Whiskers');

dog.speak(); // Rex barks.
cat.speak(); // Whiskers meows.

 


 싱글톤 패턴 (Singletom)

  • 특정 클래스의 인스턴스가 하나만 존재하도록 보장하는 패턴이다.
  • 애플리케이션 전체에서 동일한 인스턴스를 공유할 때 유용하다. 모든 곳에서 이 인스턴스에 접근할 수 있도록 하며, 전역상태를 관리하는데 유용할 수 있다.
const Singleton = (function () {
  let instance;

  function createInstance() {
    const object = new Object("I am the instance");
    return object;
  }

  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

구조 패턴 (Structural)

- 클래스나 객체들을 더 큰 구조로 조직화하는 패턴이다. 주로 객체 간의 관계를 명확히 하거나 코드의 유연성을 높이는데 사용된다.
- 코드의 유연성, 효율성을 높이고 객체 간의 관계를 관리하기 위해 클래스와 객체를 구조적으로 조합한다.
 

 모듈 패턴 (Module)

  • 내부 상태를 비공개로 유지하고, 공개 인터페이스만을 제공하여 코드의 구조를 개선하는 패턴이다. 주로 싱글톤과 함께 사용되어 하나의 모듈이 단일 인스턴스만을 가지도록 설계할 수 있다.
const CounterModule = (function () {
  let count = 0;

  function increment() { count++; }
  function decrement() { count--; }
  function getCount() { return count; }
  return { increment, decrement, getCount, }; })();

// 사용 예
CounterModule.increment();
CounterModule.increment();
console.log(CounterModule.getCount()); // 2

 


 프록시 패턴 (Proxy)

  • 다른 객체에 대한 접근을 제어하는 대리자 객체를 제공하여 객체의 접근을 간접적으로 제어하고 보호하는 패턴이다.
  • 접근 제어 및 추가적인 기능 제공, 복잡한 객체 접근 관리 등에 유용하게 사용된다. 
// 원본 객체
class RealSubject {
  request() {
    console.log('RealSubject handles request.');
  }
}

// 프록시 객체
class Proxy {
  constructor(realSubject) {
    this.realSubject = realSubject;
  }

  request() {
    if (this.checkAccess()) {
      this.realSubject.request();
    } else {
      console.log('Access denied.');
    }
  }

  checkAccess() {
    // 복잡한 접근 제어 로직을 여기에 구현
    return true;
  }
}

// 사용 예
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);

proxy.request(); // RealSubject handles request.

 데코레이터 패턴 (Decorator)

  • 객체에 동적으로 기능을 추가할 수 있도록 해주는 구조적 패턴이다.
  • 상속을 사용하지 않고도 객체에 새로운 기능을 추가할 수 있어서 유연성이 높다.
  • 객체 자체를 넘겨 동일한 인스턴스를 참조하여 사용하는게 일반적이다.
// 기본 컴포넌트
class Coffee {
  cost() {
    return 5;
  }
}

// 데코레이터 클래스
class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 2;
  }
}

// 데코레이터 클래스
class WhipDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost() + 1;
  }
}

// 사용 예
let coffee = new Coffee();
console.log(coffee.cost()); // 5

coffee = new MilkDecorator(coffee);
console.log(coffee.cost()); // 7

coffee = new WhipDecorator(coffee);
console.log(coffee.cost()); // 8

행위 패턴 (Behavioral)

- 객체 간의 커뮤니케이션과 책임을 분산시켜 시스템의 동작을 제어하고 조정한다.
 

 옵저버 패턴

  • 객체의 상태 변화를 관찰하는 옵저버 목록을 유지하고, 상태가 변화할 때마다 해당 목록에 있는 모든 옵저버들에게 알림을 보내는 패턴
  • 이 패턴은 일 대 다 의존성을 정의하며 객채 간의 느슨한 결합을 가능하게 한다.
  • EX) 주식 시장에서 주식 가격이 변동할 때 여러 개의 화면을 업데이트해야 할 때 옵저버 패턴을 사용할 수 있다. 각 화면(옵저버)은 주식 가격 변화를 구독하고 있고, 주식 가격이 변할 때 마다 모든 구독자에게 새로운 가격 정보를 전달한다. 어떠한 값을 관찰하고 있는것을 생각하면 이해하기 쉬울 것 같다.
// 옵저버 클래스 정의
class Observer {
  constructor(name) {
    this.name = name;
  }

  update(message) {
    console.log(`${this.name} received message: ${message}`);
  }
}

// 주제(Subject) 클래스 정의
class Subject {
  constructor() {
    this.observers = [];
  }

  // 옵저버 추가
  addObserver(observer) {
    this.observers.push(observer);
  }

  // 상태 변경 시 모든 옵저버에게 알림
  notify(message) {
    this.observers.forEach(observer => {
      observer.update(message);
    });
  }
}

// 사용 예
const subject = new Subject();

const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify('Hello World!');

 커맨드 패턴 (Commond)

  • 요청을 객체로 캡슐화하여 요청을 매개변수화하고, 메서드 호출, 큐잉, 로깅 등을 지원하여 실행 취소 기능을 제공하는 패턴
  • 이는 요청을 발신자(클라이언트)와 수신사(실행자) 사이에 분리시키고, 유연성을 높이며 확장성을 제공한다.
  • EX) 텍스트 에디터에서 '실행', '취소', '수정' 기능을 구현할 때 커맨드 패턴을 많이 사용한다. 각각의 작업은 커맨드 객체로 캡슐화 되어 에디터에서 발행한 모든 작업을 큐에 넣고, 사용자의 요청에 따라 순서대로 실행하거나 실행할 수 있다. 해당 실행 값을 캡슐화하여 관리하고 복잡한 상태관리를 간편하게 사용할 수 있다.
// 커맨드 인터페이스 정의
class Command {
  execute() {}
}

// 커맨드 구현 클래스
class ConcreteCommand extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }

  execute() {
    this.receiver.action();
  }
}

// 리시버 클래스
class Receiver {
  action() {
    console.log('Receiver is executing action.');
  }
}

// 인보커 클래스
class Invoker {
  constructor(command) {
    this.command = command;
  }

  setCommand(command) {
    this.command = command;
  }

  executeCommand() {
    this.command.execute();
  }
}

// 사용 예
const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker(command);

invoker.executeCommand();