컴퓨터 공부/🕸️ Web

Javascript - Design Pattern 디자인패턴

letzgorats 2023. 12. 21. 05:07

< JavaScript Design Pattern (자바스크립트에 디자인 패턴) >

※ 디자인패턴이란 뭘까?

- 소프트웨어 설계의 주어진 콘텍스트 내에서 일반적으로 발생하는 문제에 대한 일반적이고 재사용 가능한 솔루션이다.

- 소스나 기계어직접 변환할 수 있는 완성된 디자인이 아니다.

- 오히려 다양한 상황에서 사용할 수 있는 문제를 해결하는 방법에 대한 설명 또는 템플릿이다.

- 디자인 패턴은 프로그래머가 응용 프로그램이나 시스템을 디자인할 때 일반적인 문제를 해결하는 데 사용할 수 있는 공식화된 모범 사례이다.

디자인 패턴

※ 디자인패턴의 장점

  • 최고의 솔루션 : 디자인 패턴은 여러번 수정 하면서 완성되었기 때문에, 디자인 패턴은 이미 잘 작동한다는 것을 알고 있다. 그래서 대부분의 개발자가 자주 사용한다.

 

  • 재사용성 : 디자인 패턴은 단일 문제에만 존재할 수 없으므로 여러 문제를 해결하기 위해 특정 상황에서 수정할 수 있는 재사용 가능한 솔루션을 나타낸다.

 

  • 풍부한 표현력 : 디자인 패턴은 큰 문제를 부분적으로 효율적으로 설명할 수 있기 때문에 더 이상의 설명이 필요하지 않다.

 

  • 향상된 의사 소통 : 디자인 패턴에 익숙한 개발자는 문제에 대한 공통 목표를 설정하여 잠재적인 문제와 이러한 문제에 대한 솔루션에 대해 서로 의사 소통하는 데 도움이 된다.

 

  • 필요없는 코드 리팩토링 : 디자인 패턴은 종종 다양한 문제에 대한 최적의 솔루션으로 불린다. 디자인 패턴을 염두에 두고 응용 프로그램을 작성하는 경우, 생성된 솔루션이 효율적인 솔루션이므로 코드 리팩토링이 필요하지 않다고 가정한다.

 

  • 코드베이스 크기 감소 : 유일한 최적의 솔루션인 디자인 패턴은 공간을 거의 차지하지 않고 몇 줄의 코드로 필요한 솔루션을 구현하여 소중한 공간을 보존한다.

많은 디자인 패턴들

위에 그림에서 보듯 엄청 많은 디자인 패턴이 있으며 이 모든 것을 다 알고 있어야 하는 것은 아니기 때문에 자주 쓰이는 디자인패턴만 같이 살펴보도록 하겠다.


< Singleton Pattern >

Singleton(싱글톤) 패턴은 클래스의 인스턴스화하나의 객체로 제한하는 디자인 패턴이다.

이는 시스템 전체에서 작업을 조절하는 데 정확히 하나의 객체가 필요한 경우에 유용하다.

고전적으로 Singleton Pattern 은 클래스가 존재하지 않는 경우, 클래스의 새 인스턴스를 생성하는 메서드로 클래스를 생성하여 구현할 수 있다.

인스턴스가 이미 존재하는 경우, 해당 개체에 대한 참조를 반환한다.

 

ES2015+ 를 사용하면, 싱글톤 패턴을 구현하여 한 번 인스턴스화되는 JavaScript 클래스의 전역 인스턴스를 만들 수 있다. 싱글톤 인스턴스는 모듈 내보내기를 통해 노출될 수 있다. 이렇게 하면, 더 명확하고 제어할 수 있으며 다른 전역 변수와 차별화된다. 클래스의 새 인스턴스를 만들 수는 없지만, 클래스에 정의된 공용 get 및 set 메서드를 사용하여 인스턴스를 읽고 수정할 수 있다.

 

(예를 들어보자면, MP3 뮤직 앱 같은 것이 있을 때, 뮤직 앱에서 볼륨을 조정하는 것이 있다. 그게 만약에 하나의 객체 인스턴스로 되어 있으면 A라는 사람이 볼륨을 20으로 올리다가 B 라는 사람이 하나의 객체 인스턴스이기 때문에 볼륨을 30으로 올린다면, A도 20으로 듣고 있었던 것을 30으로 듣게 되는 셈이다.)

 

예시를 들어보자.

let instance;
// 1. 생성자에서 하나의 인스턴스만 생성될 수 있게 로직 작성
class Counter {
    constructor(){
        if (instance) {
            throw new Error('하나의 인스턴스만 생성할 수 있다.');
        }
        this.counter = 0;
        instance = this;
    }
    
    getCount(){
        return this.counter;
    }
    
    increment(){
        return this.counter++;
    }
    
    decrement(){
        return this.counter--;
    }    
}

// 2. 객체 인스턴스 생성, 두번째 생성한 객체는 에러가 난다.
const singletonCounterA = new Counter();

singletonCounterA.increment();
singletonCounterA.increment();

console.log(singletonCounterA.getCount());

 

인스턴스 1개만 생성

 

singletonCounterA 인스턴스 1개만 생성하면 정상적으로 동작한다. 

그럼, 인스턴스를 또 하나 더 만들면 어떻게 될까?

let instance;
// 1. 생성자에서 하나의 인스턴스만 생성될 수 있게 로직 작성
class Counter {
    constructor(){
        if (instance) {
            throw new Error('하나의 인스턴스만 생성할 수 있다.');
        }
        this.counter = 0;
        instance = this;
    }
    
    getCount(){
        return this.counter;
    }
    
    increment(){
        return this.counter++;
    }
    
    decrement(){
        return this.counter--;
    }    
}

// 2. 객체 인스턴스 생성, 두번째 생성한 객체는 에러가 난다.
const singletonCounterA = new Counter();

const CounterB = new Counter();    // 여기서 에러가 날 것이다.

singletonCounterA.increment();
singletonCounterA.increment();

console.log(singletonCounterA.getCount());

인스턴스 객체가 이미 있어서 에러 출력

 

singletonCounterA 라는 인스턴스가 이미 1개 존재해서 새로운 인스턴스를 추가로 하나 더 만들면, throw new Error를 통해 "하나의 인스턴스만 생성할 수 있다" 라는 에러를 던진다.


< Factory Pattern >

Factory(팩토리) 패턴을 사용하면 특수 함수인 팩토리 함수를 사용하여 비슷한 객체를 많이 만들 수 있다.

=> 비슷한 객체를 반복적으로 생성해야 하는 경우 사용한다.

팩토리 패턴

사용 예시를 살펴보자.

 

보통 객체를 생성하는 방법은 아래와 같다.

const pet1 = {
    name = 'Allu',
    owner = 'JY Kim',
    species = 'dachsund',
    color = 'black',
}

const pet2 = {
    name = 'Happy',
    owner = 'SH Lee',
    species = 'golden retriever',
    color = 'gold',
}

const pet3 = {
    name = 'Choco',
    owner = 'KH Park',
    species = 'pomeranian',
    color = 'white',
}

 

여기에서 단점은 코드가 반복된다는 점이고, 만약 pet 객체에 수정사항이 생기면, 모든 pet 객체에 다 수정해줘야 하는 단점이 있다.

 

위 문제를 해결하기 위한 팩토리 패턴을 이용한 객체 생성 방법은 아래와 같다.

// 팩토리 함수
const createPet = (name,owner,species,color) => ({
    name,
    owner,
    species,
    color,
});


const pet1 = createPet('Allu','JY Kim','dachsund','black');
const pet2 = createPet('Happy','SH Lee','golden retriever','gold');
const pet3 = createPet('Choco','KH Park','pomeranian','white');

 

팩토리 패턴은 동일한 코드를 계속해서 반복할 필요없이 동일한 속성을 공유하는 여러 객체를 만들어야 할 때 유용하다.

Factory 기능은 현재 환경 또는 사용자별 구성에 따라 사용자 지정 객체를 쉽게 반환할 수 있다.


< Mediator Pattern >

Mediator(중재자) 패턴은 객체 그룹에 대한 중앙 권한을 제공한다. 이 패턴은 채팅방을 예시로 들어서 설명해보겠다.

중재자 패턴

채팅방(중재자)에 들어와서 채팅 세션에 참여하는 3명의 참여자가 있다.

각 참가자는 Participant 객체로 표시된다. 참가자는 서로에게 메시지를 보내고 채팅방에서 라우팅을 처리한다.

(먼저 Mediator가 채팅을 받은 후에 라우팅으로 채팅을 전달한다.)

메시지를 다이렉트로 보내는 것이 아니라 중재자를 거쳐서 메시지가 전달된다.

 

이렇게 함으로써 정크 메시지를 받지 않도록 참가자를 보호할 수 있고, 이를 위해 '정크 필터' 와 같은 다른 복잡한 규칙을 추가할 수 있다.

 

예시를 살펴보자.

class Participant  {
    constructor(name) {
        this.name = name;
        this.chatRoom = null;
        this.messages = [];
    }
    
    send(message, to){
        this.chatRoom.send(message, this, to);
    }
    
    receive(message, from){
        this.messages.push({message, from});
    }
    
    showMessage(){
        console.log(this.messages);
    }
}

class ChatRoom{
    constructor() {
        this.participants = {}
    }
    
    enter(participant) {
        this.participants[participant.name] = participant;
        participant.chatRoom = this;
    }
    
    send(message, participant, to){
        this.participants[to.name].receive(message,participant);
    }
}

const chatRoom1 = new ChatRoom();

const user1 = new Participant('user1');
const user2 = new Participant('user2');
const user3 = new Participant('user3');

chatRoom1.enter(user1);
chatRoom1.enter(user2);
chatRoom1.enter(user3);

console.log(chatRoom1);
console.log(user1);

 

현재 채팅방을 1개 만들었고, 그 방에 3명의 user가 들어가 있는 상황이다.

ChatRoom1 과 user1의 상태

여기서 메시지를 보내보겠습니다.

user1.send("Hello", user2);
user1.send("My name is Allu",user2);

console.log(chatRoom1);

user2가 중재자로부터 메시지를 받았다.

user1 이 직접 user2 에게 메시지를 보내는 것이 아니라 Participant 클래스 내부의 send()를 통해 ChatRoom.send()를 호출하는 것이므로, ChatRoom 클래스 내부의 send()로 메시지가 전달되는 것이다.

(중재자인 ChatRoom 을 통해 다른 유저에게 메시지를 보내게 된다 는 말이다.)

중재자 패턴 로직

 

1) 유저가 채팅방에 들어오게 되면(enter), 객체 인스턴스 chatRoom에다가 해당 chatRoom이 들어가게 된다. 이 때, participants에는 해당 유저 이름이 들어간다. 

2,3) user가 메시지를 보내면(send), chatRoom 객체의 send()를 호출해서 상대방 유저 객체의 receive 메소드를 타고 (메시지, 메시지를 보낸 유저) 가 push를 통해 들어간다.

4) 특정 객체 내의 메시지 객체를 보여준다. 

 

추가로 더 메시지를 보내보자.

user2.send("Hello",user1);
user2.send("Nice to meet you!",user1);
user2.send("My name is KJY",user1);

user3.send("Heyy,how have you been today~?",user1);
user3.send("Heyy,how have you been today~?",user2);

user1과 user2는 각각 메시지를 서로에게 보냈고, user3는 user1과 user2에게 동일한 메시지를 보냈다.


< Observer Pattern >

Observer(옵저버) 패턴은 event-driven 시스템을 이용하는 것을 Observer Design Pattern 이라고도 부른다.

옵저버 패턴

이 패턴에는 특정 Subject를 관찰하는 많은 Observer 가 있다.

관찰자는 기본적으로 관심이 있고 해당 주제 내부에 변경 사항이 있을 때 알림을 받기를 원한다.

그래서 그들은 그 주제에 스스로를 등록(Register)한다.

주제에 대한 관심을 잃으면, 단순히 해당 주제에서 등록을 취소한다. 때때로 이 모델은 게시자-구독자(Publisher-Subscriber) 모델이라고도 한다.

 

예를 들어, 트위터 팔로워가 많은 유명인을 생각할 수 있다. 이 팔로워들 각각은 자신이 좋아하는 유명인의 최신 업데이트를 모두 받고 싶어한다. 따라서 관심이 지속되는 한 유명인을 팔로우할 수 있다. 그가 흥미를 잃으면 그는 단순히 그 유명인을 따르는 것을 중단한다. 여기서 우리는 추종자를 관찰자(Observer)로, 유명인을 주체(Subject)로 생각할 수 있다.

 

소스코드로 예시를 살펴보자.

HTML 코드는 아래와 같다.

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Pattern Observer</title>
</head>

<body>
    <div id="app">
        <button id="pink-btn">I'm a pink button!</button>
        <button id="blue-btn">I'm a blue button!</button>
    </div>
    <script src="script.js"></script>
</body>

</html>

 

 

JS 코드는 아래와 같다.

const pinkBtn = document.getElementById('pink-btn');
const blueBtn = document.getElementById('blue-btn');

function sendToGoogleAnalytics(data) {
    console.log('Sent to Google analytics: ', data);
}

function sendToCustomAnalytics(data) {
    console.log('Sent to custom analytics: ', data);
}

function sendToEmail(data) {
    console.log('Sent to email: ', data);
}

const observers = [];
const Observer = Object.freeze({
    notify: (data) => observers.forEach((observer) => observer(data)),
    subscribe: (func) => observers.push(func),
    unsubscribe: (func) => {
        [...observers].forEach((observer, index) => {
            if (observer === func) {
                observers.splice(index, 1);
            }
        });
    },
});

Observer.subscribe(sendToGoogleAnalytics);
Observer.subscribe(sendToCustomAnalytics);
Observer.subscribe(sendToEmail);

pinkBtn.addEventListener('click', () => {
    const data = '🎀 Click on pink button! 🎀';
    Observer.notify(data);

});

blueBtn.addEventListener('click', () => {
    const data = '🦋 Click on blue button! 🦋';
    Observer.notify(data);
});

 

sendToGoogleAnalytics, sendToCustomAnalytics, sendToEmail 라는 3개의 Observer 들이 있고, Observer.subscribe()을 통해 구독을 함으로써 각 Subject를 관찰하는 것이다.

subscribe()는 observers 라는 배열에다가 해당 함수들을 다 Push를 해준다. observers 에는 3개의 함수들이 다 들어가 있게 된다.

 

pinkBtn.addEventListener 처럼 버튼을 클릭을 하게 되면 Observer.notify() 를 통해 observers 배열에 있는 각각의 함수를 다 호출한다. 이 때, 호출을 할 때, data 를 다 넣어서 호출을 하는데, 각 함수에 data가 그대로 전달되어 notify 가 되는 것이다.

pink 버튼 1회 클릭, blue 버튼 1회 클릭

 

위 사진과 같이 notify 가 각각의 observer 들한테 오게 되는 것이다.


< Module Pattern >

Module(모듈) 패턴은 코드를 더 작고 재사용 가능한 조각으로 분할하는 것을 도와준다.

ES2015는 내장 JavaScript 모듈을 도입했다. 모듈은 JavaScript 코드가 포함된 파일이며, 특정 값을 쉽게 노출하고 숨길 수 있도록 한다.

 

모듈 패턴은 더 큰 파일을 여러 개의 더 작고 재사용 가능한 조각으로 분할하는 좋은 방법이다.

또한, 모듈 내의 값은 기본적으로 모듈 내에서 비공개로 유지되고, 수정할 수 없기 때문에 코드 캡슐화를 촉진한다.

export 키워드를 사용하여 명시적으로 내보낸 값만 다른 파일에서 액세스할 수 있다.

 

예시를 살펴보자.

function validateInput(input){
    if (typeof input != 'number') {
        throw new Error('Invalid input');
    }
}

function sum(x,y) {
    return x + y;
}

function multiply(x,y) {
    return x * y;
}

function subtract(x,y) {
    return x - y;
}

function divide(x,y) {
    return x / y;
}

 

위 코드가 모듈 패턴을 사용하지 않고 작성한 코드라면,

모듈 패턴을 사용한다면 아래코드처럼, 하나의 파일이 아닌, 관련된 것끼리 묶어 모듈화를 해서 작성한다.

exprot function validateInput(input){
    if (typeof input != 'number') {
        throw new Error('Invalid input');
    }
}

input.js

export function sum(x,y) {
    return x + y;
}

export function multiply(x,y) {
    return x * y;
}

export function subtract(x,y) {
    return x - y;
}

export function divide(x,y) {
    return x / y;
}

math.js

import { validateInput } from "./input.js";
import { sum } from "./math.js";

console.log(validateInput, sum);

index.js

 

export 지시자를 변수나 함수 앞에 붙이면, 외부 모듈에서 해당 변수나 함수에 접근할 수 있다. (모듈 내보내기)

import 지시자를 사용하면, 외부 모듈의 기능을 가져올 수 있다.(모듈 가져오기)

 

이처럼 모듈 패턴을 사용할 때는 자바스크립트에서 원래 스크립트로 로드할 때, type으로 module을 주면 된다.

<body>
    <script src = "index.js" type="module"></script>
</body>

 

즉, 모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 한다. 브라우저가 자동으로 모듈을 가져오고 평가한 다음, 이를 실행하게 된다.

 

※ 그럼 '일반 스크립트'와 '모듈'의 차이가 뭘까?

 

모듈의 특징

  • 항상 엄격 모드(use strict) 로 실행된다.
  • 지연실행 된다.
  • 인라인 모듈 스크립트도 비동기 처리를 할 수 있다.
  • 외부 오리진에서 스크립트를 불러오려면 Cors 헤더가 있어야 한다.
  • 중복된 스크립트는 무시된다.

실제 앱에서 import export 를 사용할 때는 성능과 같은 이점 때문에 웹팩같은 번들러를 사용한다.


1. 엄격 모드 실행

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

선언되지 않은 변수에 값을 할당하는 코드 에러

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

 

2. 지연실행

 

보통 script에서 지연실행을 하려면 defer 속성을 붙여줘야 한다.

하지만, module 스크립트는 defer 속성 없이도 지연실행을 하게 된다.



html 은 우리가 바로 브라우저에서 보여줄 수 있는 것이 아니라 브라우저가 html 을 분석을 한 다음에 보여줄 수 있다.

 

연두색 부분이 html을 파싱하는 부분(분석)이고

회색 부분은 브라우저가 파싱을 멈추고

파란색 부분이 script를 다운로드 받는 것이다.

빨간색 부분은 다운받은 script 를 실행하는 부분이다.

 

일반&nbsp;script

일반 script 에서는 script 를 다운로드 하고 실행할 때, html 파싱을 멈춘다. 그리고 다시 html 파싱을 한다.

비동기&nbsp;script

async 에서는 script 를 다운로드 할 때, html 파싱을 멈추지 않는다. script를 실행할 때만 파싱을 멈춘다. 그리고 다시 html 파싱을 한다.

defer를 사용한 script & module script

 

외부 module script 를 다운로드할 때, 브라우저 HTML 처리가 멈추지 않는다! script는 HTML 문서가 다 준비가 완료된 후 실행된다.

 

그럼, 해당 예시에서 어떤 부분이 먼저 호출될까?

<body>
    <script type="module">
        alert(typeof button);
    </script>
    
    <script>
        alert(typeof button);
    </script>
    <button id="button">Button</button>
</body>

 

그냥 스크립트에서 먼저 호출되는데, 아직 HTML Button 부분 처리가 안되어 있기 때문에 (페이지가 완전히 구성되기 전) undefined 가 나오고, module script 에는 HTML 처리가 끝내고 실행되기에 object 라고 나온다.

일반 script 에서의 typeof button

 

module script 에서의 typeof button

 

 

3. 모듈 레벨의 스코프

<!doctype html>
<script type="module" src="a.js"></script>
<script type="module" src="b.js"></script>

index.html

alert(user);  // error!!!

a.js

let user = "Allu";

b.js

 

두 스크립트 모두 type="module" 로 설정되어 있다. b.js 파일에 있는 user 변수를 a.js에서 접근하지 못한다.

type="module"로 하면 그 파일안에 그 모듈안에 자신만의 스코프가 만들어지게 된다. 그래서, 모듈 내부에서 정의한 변수나 함수를 다른 스크립트에서 접근할 수 없다.

 

4. 한 번만 실행

<!doctype html>
<script type="module" src="a.js"></script>
<script type="module" src="b.js"></script>

index.html

export default alert("모듈이 실행되었습니다.");

c.js

import './c.js';

a.js

import './c.js';

b.js

 

 

a.js와 b.js 두 곳에서 import 해가도 한 번만 alert 가 실행된다.

즉, 동일한 모듈여러 다른 곳에서 사용하더라도 모듈은 최초 호출될 때 한번만 실행되게 된다.

 

→ 만약, 아래와 같다면 어떨까?

export let c = {
    c: "c"
};

c.js

import {c} from './c.js';
c.c = "a";

a.js

import {c} from './c.js';
alert(c.c);    // a

b.js

 

c.js 에서의 객체 c를 a.js 에서 불러와서 해당 객체 내의 변수 c를 수정을 'a'로 수정했다. b.js 에서 객체 c를 불러와서 해당 객체 내의 변수 c를 alert 해보면, 'a' 로 출력된다.

즉, 모듈은 단 한번 실행되고 실행된 모듈은 필요한 곳에 공유되므로, 어느 한 모듈에서 c 객체를 수정하면 다른 모듈에서도 변경사항을 확인할 수 있다.

 

5. import.meta

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

 

스크립트 모듈 안에서 어떤 정보들을 가져오려고 하면, import.meta 를 해서 정보들을 가져올 수 있는데,

만약 이 인라인 스크립트가 위치해 있는 html 페이지의 url 을 가져오려면 import.meta.url 을 이용하면 된다.

 

6. this

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

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

 

strict 모드에서 봤던 것처럼, 스크립트 scope 에서 this는 원래 윈도우 객체를 참조를 하지만, 엄격 모드에서 this를 사용하면 undefined 로 나온다.

 

 

7. 인라인 스크립트 비동기 처리

- 일반 script 에서 async

: 외부 스크립트를 불러올 때만 유효, 스크립트 로딩이 끝나면 다른 스크립트나 html 문서 처리를 기다리지 않고 바로 실행된다. 

 

- 모듈 script 에서 async

: 외부 스크립트 불러올 때뿐만 아니라 인라인 스크립트에도 적용가능하다. 인라인 스크립트에 async 가 붙었기에 다른 스크립트나 html 문서 처리 기다리지 않고 바로 실행된다.

<script async type="module">
    import { play } from './play.js';
    
    play();
</script>

위 코드는 인라인 스크립트에 모듈을 적용한 async다.

play.js 모듈의 로드가 끝나면, html 문서나 다른 <script> 가 로드되길 기다리지 않고 바로 실행한다

=> 독립적인 기능을 구현할 때 유용하다.

 

8. no module

구형 브라우저에서 type="module"을 해석하지 못한다. 그래서, 모듈 타입 스크립트를 만나면 무시하고 넘어가게 된다.

이럴 때, nomodule 속성을 이용해서 구형 브라우저에서도 대비할 수 있다.

<script type="nomodule">
    alert("type=nomodule script");
</script>

<script nomodule>
    alert("현재 구형브라우저를 사용하고 있습니다!");
    alert("그래서 모듈 스크립트를 사용할 수 없습니다.");
</script>

참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형