1. 디자인 패턴이란? 🎯

디자인 패턴(Design Pattern)은 소프트웨어 개발 과정에서 반복적으로 발생하는 문제들을 해결하기 위한 검증된 솔루션입니다.

🔍 디자인 패턴의 의미

💡 "이런 상황에서는 이런 패턴을 사용하면 좋다"는 일종의 방향성을 제시하는 것입니다.

디자인 패턴은 다음과 같은 이점을 제공합니다:

  • 경험 기반의 증명된 해결책 제공
  • 코드의 재사용성과 유지보수성 향상
  • 개발자 간 의사소통 효율화
  • 코드 구조의 표준화

🌟 주요 디자인 패턴 종류

JavaScript/프론트엔드에서 자주 사용되는 패턴들:

  • 모듈 패턴 - 코드를 모듈화하여 캡슐화
  • 싱글톤 패턴 - 단 하나의 인스턴스만 생성
  • 팩토리 패턴 - 객체 생성을 추상화
  • 믹스인 패턴 - 객체 간 기능 공유
  • 위임/상속 패턴 - 프로토타입 기반 코드 재사용

2. 모듈 패턴 📦

모듈 패턴(Module Pattern)은 코드를 논리적 단위로 분리하고, 캡슐화와 은닉화를 통해 보안성을 높이는 패턴입니다.

💡 방법 1: Object Literal (기본)

가장 간단한 모듈 생성 방법으로, 객체 리터럴을 사용합니다.

// Object Literal 방식
const calculator = {
    result: 0,
    
    add(num) {
        this.result += num;
        return this;
    },
    
    subtract(num) {
        this.result -= num;
        return this;
    },
    
    getResult() {
        return this.result;
    }
};

// 사용 예시
calculator.add(10).subtract(3);
console.log(calculator.getResult()); // 7

// ⚠️ 문제점: 내부 데이터에 직접 접근 가능
calculator.result = 999; // 보안 취약! 🚨
console.log(calculator.getResult()); // 999

장점: 간편하고 직관적
단점: 모든 프로퍼티가 public이라 보안에 취약

💡 방법 2: IIFE + 클로저 (개선된 모듈 패턴)

즉시실행함수(IIFE)클로저를 활용하여 private 공간을 만듭니다.

// IIFE + 클로저를 활용한 모듈 패턴
const secureCalculator = (function() {
    // Private 변수 (외부에서 접근 불가) 🔒
    let result = 0;
    
    // Private 함수
    function validate(num) {
        if (typeof num !== 'number') {
            throw new Error('숫자만 입력 가능합니다!');
        }
    }
    
    // Public API 반환
    return {
        add(num) {
            validate(num);
            result += num;
            return this;
        },
        
        subtract(num) {
            validate(num);
            result -= num;
            return this;
        },
        
        multiply(num) {
            validate(num);
            result *= num;
            return this;
        },
        
        divide(num) {
            validate(num);
            if (num === 0) throw new Error('0으로 나눌 수 없습니다!');
            result /= num;
            return this;
        },
        
        getResult() {
            return result;
        },
        
        reset() {
            result = 0;
            return this;
        }
    };
})();

// 사용 예시
secureCalculator.add(10).multiply(2).subtract(5);
console.log(secureCalculator.getResult()); // 15

// ✅ 보안 강화: private 변수에 직접 접근 불가
console.log(secureCalculator.result); // undefined
secureCalculator.result = 999; // 영향 없음!
console.log(secureCalculator.getResult()); // 여전히 15

🎯 모듈 패턴의 핵심

Public/Private 개념으로 객체를 나누는 캡슐화 및 은닉화가 핵심입니다.

특징설명
캡슐화관련된 데이터와 메서드를 하나의 단위로 묶음
은닉화내부 구현을 숨기고 필요한 부분만 노출
네임스페이스전역 변수 오염 방지

3. 싱글톤 패턴 👑

싱글톤 패턴(Singleton Pattern)한 클래스에서 인스턴스를 단 1개만 생성하도록 제한하는 패턴입니다.

🔍 싱글톤이 필요한 경우

  • 애플리케이션 전역에서 하나의 상태만 관리해야 할 때
  • 데이터베이스 연결, 로거, 설정 관리자

💡 싱글톤 구현 예시

// 싱글톤 패턴 구현
const Database = (function() {
    let instance; // private 변수로 인스턴스 저장
    
    function createInstance() {
        // 실제 데이터베이스 객체
        return {
            connection: null,
            
            connect() {
                if (!this.connection) {
                    this.connection = '데이터베이스 연결됨 🔗';
                    console.log(this.connection);
                }
            },
            
            query(sql) {
                if (!this.connection) {
                    throw new Error('먼저 연결해주세요!');
                }
                console.log(`쿼리 실행: ${sql}`);
            },
            
            disconnect() {
                this.connection = null;
                console.log('연결 종료 ❌');
            }
        };
    }
    
    return {
        getInstance() {
            if (!instance) {
                instance = createInstance();
                console.log('✨ 새 인스턴스 생성');
            } else {
                console.log('♻️ 기존 인스턴스 반환');
            }
            return instance;
        }
    };
})();

// 사용 예시
const db1 = Database.getInstance(); // ✨ 새 인스턴스 생성
db1.connect(); // 데이터베이스 연결됨 🔗

const db2 = Database.getInstance(); // ♻️ 기존 인스턴스 반환

console.log(db1 === db2); // true (같은 인스턴스!)

🎯 싱글톤의 장단점

장점:

  • 메모리 효율성 (인스턴스가 하나만 존재)
  • 전역 상태 관리 용이
  • 리소스 공유 최적화

단점:

  • 전역 상태로 인한 테스트 어려움
  • 의존성 증가
  • 멀티스레드 환경에서 주의 필요

4. 팩토리 패턴 🏭

팩토리 패턴(Factory Pattern)비슷한 객체를 공장에서 찍어내듯 반복적으로 생성할 수 있도록 하는 패턴입니다.

🔍 팩토리 패턴의 특징

new 키워드를 사용한 생성자 함수가 아니라, 일반 함수에서 객체를 반환하는 것을 팩토리 함수라고 합니다.

💡 예시 1: 사용자 생성 팩토리

// 팩토리 함수
function createUser(name, role) {
    // 공통 속성
    const user = {
        name: name,
        role: role,
        createdAt: new Date(),
        
        // 공통 메서드
        getInfo() {
            return `${this.name} (${this.role})`;
        }
    };
    
    // 역할별 특수 메서드 추가
    if (role === 'admin') {
        user.deleteUser = function(userId) {
            console.log(`관리자 ${this.name}가 사용자 ${userId}를 삭제했습니다.`);
        };
    } else if (role === 'editor') {
        user.editContent = function(contentId) {
            console.log(`편집자 ${this.name}가 콘텐츠 ${contentId}를 수정했습니다.`);
        };
    }
    
    return user;
}

// 사용 예시
const admin = createUser('Alice', 'admin');
const editor = createUser('Bob', 'editor');
const viewer = createUser('Charlie', 'viewer');

console.log(admin.getInfo()); // "Alice (admin)"
admin.deleteUser(123); // "관리자 Alice가 사용자 123을 삭제했습니다."

console.log(editor.getInfo()); // "Bob (editor)"
editor.editContent(456); // "편집자 Bob가 콘텐츠 456을 수정했습니다."

💡 예시 2: UI 컴포넌트 팩토리

// UI 컴포넌트 팩토리
function createButton(type, text) {
    const button = {
        type: type,
        text: text,
        
        render() {
            return `<button class="btn-${this.type}">${this.text}</button>`;
        },
        
        onClick(handler) {
            console.log(`${this.text} 버튼 클릭 이벤트 등록`);
            this.clickHandler = handler;
        }
    };
    
    // 타입별 스타일 설정
    switch(type) {
        case 'primary':
            button.style = 'background: blue; color: white;';
            break;
        case 'danger':
            button.style = 'background: red; color: white;';
            break;
        case 'success':
            button.style = 'background: green; color: white;';
            break;
        default:
            button.style = 'background: gray; color: black;';
    }
    
    return button;
}

// 사용 예시
const submitBtn = createButton('primary', '제출');
const deleteBtn = createButton('danger', '삭제');
const saveBtn = createButton('success', '저장');

console.log(submitBtn.render());
// <button class="btn-primary">제출</button>

deleteBtn.onClick(() => console.log('삭제 확인'));
// "삭제 버튼 클릭 이벤트 등록"

🎯 팩토리 패턴의 장점

  • 객체 생성 로직을 한 곳에 집중
  • 생성자 대신 명확한 함수명 사용 (예: createUser, createButton)
  • 조건부 객체 생성이 쉬움
  • 코드 재사용성 향상

5. 믹스인 패턴 🎨

믹스인 패턴(Mixin Pattern)한 객체의 프로퍼티를 다른 객체에 복사해 사용하는 패턴으로, 코드를 재사용하는 효과를 냅니다.

🔍 믹스인의 목적

기존 객체의 기능을 그대로 보존하면서 다른 객체에 추가할 때 사용합니다.

💡 예시 1: 기본 믹스인 구현

// 믹스인 함수
function mixin(target, source) {
    for (let key in source) {
        if (source.hasOwnProperty(key)) {
            target[key] = source[key];
        }
    }
    return target;
}

// 공통 기능들
const canEat = {
    eat(food) {
        console.log(`${this.name}이(가) ${food}를 먹습니다. 🍽️`);
    }
};

const canWalk = {
    walk() {
        console.log(`${this.name}이(가) 걷습니다. 🚶`);
    }
};

const canSwim = {
    swim() {
        console.log(`${this.name}이(가) 수영합니다. 🏊`);
    }
};

// 사용 예시
const person = { name: '철수' };
mixin(person, canEat);
mixin(person, canWalk);

person.eat('사과'); // "철수이(가) 사과를 먹습니다. 🍽️"
person.walk(); // "철수이(가) 걷습니다. 🚶"

const duck = { name: '오리' };
mixin(duck, canEat);
mixin(duck, canWalk);
mixin(duck, canSwim);

duck.eat('빵'); // "오리이(가) 빵을 먹습니다. 🍽️"
duck.swim(); // "오리이(가) 수영합니다. 🏊"

💡 예시 2: Object.assign을 활용한 믹스인

// Object.assign을 사용한 믹스인
const eventEmitterMixin = {
    on(event, handler) {
        if (!this._events) this._events = {};
        if (!this._events[event]) this._events[event] = [];
        this._events[event].push(handler);
    },
    
    emit(event, ...args) {
        if (!this._events || !this._events[event]) return;
        this._events[event].forEach(handler => handler(...args));
    },
    
    off(event, handler) {
        if (!this._events || !this._events[event]) return;
        this._events[event] = this._events[event].filter(h => h !== handler);
    }
};

const loggingMixin = {
    log(message) {
        console.log(`[${new Date().toISOString()}] ${message}`);
    },
    
    error(message) {
        console.error(`[ERROR] ${message}`);
    }
};

// 여러 믹스인을 한 번에 적용
class Component {
    constructor(name) {
        this.name = name;
    }
}

Object.assign(Component.prototype, eventEmitterMixin, loggingMixin);

// 사용 예시
const myComponent = new Component('MyComponent');

myComponent.on('dataLoaded', (data) => {
    myComponent.log(`데이터 로드됨: ${data}`);
});

myComponent.emit('dataLoaded', '사용자 목록'); 
// "[2024-01-15T12:00:00.000Z] 데이터 로드됨: 사용자 목록"

myComponent.error('데이터 로드 실패!');
// "[ERROR] 데이터 로드 실패!"

🎯 믹스인 패턴의 특징

장점:

  • 다중 상속과 유사한 효과
  • 코드 재사용성 극대화
  • 유연한 기능 조합

단점:

  • 메서드 이름 충돌 가능성
  • 출처 추적이 어려울 수 있음

6. 위임/상속 패턴 🔗

Behavior Delegation (위임) / Inheritance (상속)은 부모 프로토타입에 저장되어있는 변수나 메소드를 자식 쪽에서 위임받아 사용하는 패턴입니다.

🔍 프로토타입 체인의 이해

하위 클래스(객체)에서는 상위 프로토타입에 있는 변수나 메소드들을 위임받아 언제든지 사용할 수 있습니다.

💡 예시 1: 프로토타입 상속

// 상위 프로토타입 (부모)
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function(food) {
    console.log(`${this.name}이(가) ${food}를 먹습니다.`);
};

Animal.prototype.sleep = function() {
    console.log(`${this.name}이(가) 잠을 잡니다. 💤`);
};

// 하위 프로토타입 (자식)
function Dog(name, breed) {
    Animal.call(this, name); // 부모 생성자 호출
    this.breed = breed;
}

// 프로토타입 체인 연결
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Dog만의 메서드 추가
Dog.prototype.bark = function() {
    console.log(`${this.name}: 멍멍! 🐕`);
};

// 사용 예시
const myDog = new Dog('바둑이', '진돗개');

myDog.eat('사료'); // "바둑이이(가) 사료를 먹습니다." (Animal에서 상속)
myDog.sleep(); // "바둑이이(가) 잠을 잡니다. 💤" (Animal에서 상속)
myDog.bark(); // "바둑이: 멍멍! 🐕" (Dog 자체 메서드)

console.log(myDog.breed); // "진돗개"

💡 예시 2: ES6 Class 문법으로 상속

// 부모 클래스
class Vehicle {
    constructor(name, speed) {
        this.name = name;
        this.speed = speed;
    }
    
    move() {
        console.log(`${this.name}이(가) ${this.speed}km/h로 이동합니다. 🚗`);
    }
    
    stop() {
        console.log(`${this.name}이(가) 정지합니다. 🛑`);
    }
}

// 자식 클래스
class Car extends Vehicle {
    constructor(name, speed, brand) {
        super(name, speed); // 부모 생성자 호출
        this.brand = brand;
    }
    
    // 메서드 오버라이딩
    move() {
        console.log(`${this.brand} ${this.name}이(가) 도로 위를 달립니다! 🏎️`);
        super.move(); // 부모 메서드 호출
    }
    
    // 자식만의 메서드
    honk() {
        console.log(`${this.name}: 빵빵! 📯`);
    }
}

class Airplane extends Vehicle {
    constructor(name, speed, altitude) {
        super(name, speed);
        this.altitude = altitude;
    }
    
    fly() {
        console.log(`${this.name}이(가) ${this.altitude}m 상공을 비행합니다. ✈️`);
    }
}

// 사용 예시
const myCar = new Car('소나타', 180, '현대');
myCar.move();
// "현대 소나타이(가) 도로 위를 달립니다! 🏎️"
// "소나타이(가) 180km/h로 이동합니다. 🚗"

myCar.honk(); // "소나타: 빵빵! 📯"
myCar.stop(); // "소나타이(가) 정지합니다. 🛑"

const myPlane = new Airplane('보잉747', 900, 10000);
myPlane.move(); // "보잉747이(가) 900km/h로 이동합니다. 🚗"
myPlane.fly(); // "보잉747이(가) 10000m 상공을 비행합니다. ✈️"

💡 예시 3: Delegation 패턴 (위임)

// 위임 방식: 상속 대신 객체 간 연결
const AnimalBehavior = {
    init(name) {
        this.name = name;
    },
    
    eat(food) {
        console.log(`${this.name}이(가) ${food}를 먹습니다.`);
    }
};

const DogBehavior = Object.create(AnimalBehavior);

DogBehavior.setup = function(name, breed) {
    this.init(name);
    this.breed = breed;
};

DogBehavior.bark = function() {
    console.log(`${this.name}: 멍멍!`);
};

// 사용 예시
const dog = Object.create(DogBehavior);
dog.setup('바둑이', '진돗개');

dog.eat('간식'); // AnimalBehavior에 위임
dog.bark(); // DogBehavior 자체 메서드

🎯 상속 vs 위임

구분상속 (Inheritance)위임 (Delegation)
관계"is-a" 관계"has-a" 관계
구조부모-자식 계층객체 간 연결
유연성상대적으로 경직더 유연함
사용클래스 기반프로토타입 기반

7. 실전 활용 예시 ⚡

🎯 패턴 조합: 싱글톤 + 팩토리

// 앱 설정 관리자 (싱글톤 + 팩토리)
const ConfigManager = (function() {
    let instance;
    
    function createConfig() {
        let config = {
            apiUrl: 'https://api.example.com',
            timeout: 5000,
            theme: 'light'
        };
        
        return {
            get(key) {
                return config[key];
            },
            
            set(key, value) {
                config[key] = value;
                console.log(`설정 변경: ${key} = ${value}`);
            },
            
            getAll() {
                return { ...config };
            }
        };
    }
    
    return {
        getInstance() {
            if (!instance) {
                instance = createConfig();
            }
            return instance;
        }
    };
})();

// 사용
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();

console.log(config1 === config2); // true

config1.set('theme', 'dark');
console.log(config2.get('theme')); // 'dark' (같은 인스턴스!)

🎯 패턴 조합: 모듈 + 믹스인

// 기능별 믹스인
const validationMixin = {
    validate(data, rules) {
        for (let field in rules) {
            if (!rules[field](data[field])) {
                return { valid: false, field };
            }
        }
        return { valid: true };
    }
};

const ajaxMixin = {
    async request(url, options = {}) {
        try {
            const response = await fetch(url, options);
            return await response.json();
        } catch (error) {
            this.handleError(error);
        }
    }
};

// 모듈 패턴으로 전체 구조 생성
const UserModule = (function() {
    let users = [];
    
    const module = {
        addUser(user) {
            const result = this.validate(user, {
                name: (val) => val && val.length > 0,
                email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
            });
            
            if (result.valid) {
                users.push(user);
                console.log('✅ 사용자 추가 성공');
            } else {
                console.log(`❌ 유효성 검사 실패: ${result.field}`);
            }
        },
        
        async fetchUsers() {
            const data = await this.request('/api/users');
            users = data;
            return users;
        },
        
        handleError(error) {
            console.error('에러 발생:', error.message);
        },
        
        getUsers() {
            return [...users];
        }
    };
    
    // 믹스인 적용
    Object.assign(module, validationMixin, ajaxMixin);
    
    return module;
})();

// 사용
UserModule.addUser({ name: '철수', email: 'chulsoo@example.com' });
// ✅ 사용자 추가 성공

UserModule.addUser({ name: '', email: 'invalid' });
// ❌ 유효성 검사 실패: name
profile
프론트엔드 입문 개발자입니다.

0개의 댓글