디자인 패턴(Design Pattern)은 소프트웨어 개발 과정에서 반복적으로 발생하는 문제들을 해결하기 위한 검증된 솔루션입니다.
💡 "이런 상황에서는 이런 패턴을 사용하면 좋다"는 일종의 방향성을 제시하는 것입니다.
디자인 패턴은 다음과 같은 이점을 제공합니다:
JavaScript/프론트엔드에서 자주 사용되는 패턴들:
모듈 패턴(Module Pattern)은 코드를 논리적 단위로 분리하고, 캡슐화와 은닉화를 통해 보안성을 높이는 패턴입니다.
가장 간단한 모듈 생성 방법으로, 객체 리터럴을 사용합니다.
// 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이라 보안에 취약
즉시실행함수(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 개념으로 객체를 나누는 캡슐화 및 은닉화가 핵심입니다.
| 특징 | 설명 |
|---|---|
| 캡슐화 | 관련된 데이터와 메서드를 하나의 단위로 묶음 |
| 은닉화 | 내부 구현을 숨기고 필요한 부분만 노출 |
| 네임스페이스 | 전역 변수 오염 방지 |
싱글톤 패턴(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 (같은 인스턴스!)
장점:
단점:
팩토리 패턴(Factory Pattern)은 비슷한 객체를 공장에서 찍어내듯 반복적으로 생성할 수 있도록 하는 패턴입니다.
new 키워드를 사용한 생성자 함수가 아니라, 일반 함수에서 객체를 반환하는 것을 팩토리 함수라고 합니다.
// 팩토리 함수
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을 수정했습니다."
// 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)믹스인 패턴(Mixin Pattern)은 한 객체의 프로퍼티를 다른 객체에 복사해 사용하는 패턴으로, 코드를 재사용하는 효과를 냅니다.
기존 객체의 기능을 그대로 보존하면서 다른 객체에 추가할 때 사용합니다.
// 믹스인 함수
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(); // "오리이(가) 수영합니다. 🏊"
// 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] 데이터 로드 실패!"
장점:
단점:
Behavior Delegation (위임) / Inheritance (상속)은 부모 프로토타입에 저장되어있는 변수나 메소드를 자식 쪽에서 위임받아 사용하는 패턴입니다.
하위 클래스(객체)에서는 상위 프로토타입에 있는 변수나 메소드들을 위임받아 언제든지 사용할 수 있습니다.
// 상위 프로토타입 (부모)
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); // "진돗개"
// 부모 클래스
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 상공을 비행합니다. ✈️"
// 위임 방식: 상속 대신 객체 간 연결
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 자체 메서드
| 구분 | 상속 (Inheritance) | 위임 (Delegation) |
|---|---|---|
| 관계 | "is-a" 관계 | "has-a" 관계 |
| 구조 | 부모-자식 계층 | 객체 간 연결 |
| 유연성 | 상대적으로 경직 | 더 유연함 |
| 사용 | 클래스 기반 | 프로토타입 기반 |
// 앱 설정 관리자 (싱글톤 + 팩토리)
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