📘 18.4.2 이벤트 시스템 만들기 - 우리만의 알림 방송국
안녕하세요, 여러분. 지난 시간에 옵저버 패턴이 무엇인지, 학교 방송실처럼 여러 곳에 소식을 전달하는 방법을 배웠어요. 이번 시간에는 그 지식을 바탕으로 더욱 멋진 것을 만들어 보겠습니다. 바로 우리만의 이벤트 시스템이에요.
이것은 학교의 방송실처럼 여러 가지 소식을 동시에 많은 사람들에게 전달할 수 있는 시스템입니다. 차근차근 함께 만들어보실까요?
🧠 새로운 단어들과 친해지기
코딩의 세계로 들어가기 전에, 우리가 오늘 배울 특별한 용어들과 먼저 친구가 되어 보겠습니다.
단어 | 쉬운 설명 |
---|---|
이벤트 시스템 | 프로그램에서 일어나는 여러 가지 사건들을 감지하고 처리하는 구조입니다. |
이벤트 | 프로그램에서 일어나는 특별한 일이나 변화를 말합니다. |
이벤트 리스너 | 특정 이벤트가 일어났을 때 실행되는 특별한 함수입니다. |
이벤트 에미터 | 이벤트를 발생시키고 리스너들에게 알림을 보내는 역할을 합니다. |
이 단어들은 우리가 이벤트 시스템을 만들어 나가는 과정에서 자주 만나게 될 친구들이에요. 하나씩 차근차근 알아가다 보면 어느새 친숙해질 거예요.
✨ 이벤트 시스템의 핵심 개념
이벤트 시스템은 우리 학교의 방송실과 같습니다. 방송실에서는 아침 조회, 점심시간, 하교시간 등 여러 가지 다른 종류의 방송을 할 수 있죠. 각 교실에는 스피커가 있어서 방송을 들을 수 있고, 방송 내용에 따라 학생들이 다르게 반응합니다.
프로그래밍에서 이벤트 시스템도 이와 똑같이 동작해요. 이벤트 에미터(방송실)가 여러 종류의 이벤트(방송)를 발생시키면, 이벤트 리스너(스피커가 있는 교실)들이 그 이벤트를 감지하고 각자의 방식으로 반응합니다.
가장 멋진 점은 방송실과 교실이 서로를 자세히 알 필요가 없다는 것입니다. 방송실은 그저 정해진 시간에 방송만 하면 되고, 교실은 방송이 나오면 그에 맞게 반응하기만 하면 되죠. 새로운 교실이 생기면 방송 시스템에 연결만 하면 되고, 필요 없어진 교실은 연결을 끊으면 됩니다.
재미있는 비유: 우리 동네 소식 알림판
이벤트 시스템을 더 쉽게 이해하기 위해 '우리 동네 소식 알림판' 이야기를 들려드릴게요.
우리 동네에는 아파트 입구에 큰 알림판이 있어요. 관리사무소에서는 여러 가지 중요한 소식들을 이 알림판에 붙입니다. "새로운 가족이 이사 왔어요", "엘리베이터 점검해요", "운동회가 열려요" 같은 소식들이죠.
동네 사람들은 각자 관심 있는 소식만 골라서 확인해요. 아이가 있는 가족은 운동회 소식을, 헬스장을 자주 이용하는 사람은 시설 점검 소식을, 새로 이사 온 가족들은 환영 행사 소식을 주로 봅니다.
가장 좋은 점은 관리사무소가 누가 어떤 소식을 보는지 일일이 확인할 필요가 없다는 것이에요. 그냥 소식을 알림판에 붙이기만 하면, 관심 있는 사람들이 알아서 보고 각자 필요한 준비를 하는 거죠. 새로운 가족이 이사 와도 알림판 사용법만 알려주면 되고, 이사 가는 가족은 더 이상 알림판을 보지 않으면 그만입니다.
우리가 만들 이벤트 시스템도 이 동네 알림판과 똑같이 동작해요.
🎯 이벤트 시스템을 만드는 이유
그렇다면 우리는 왜 이런 특별한 이벤트 시스템을 만들어야 할까요? 여러 가지 좋은 이유들이 있어요.
첫 번째 이유는 코드 부분들이 서로 독립적이 되기 때문입니다. 버튼을 클릭했을 때 소리가 나고, 화면이 바뀌고, 점수가 올라가야 한다고 해봅시다. 이벤트 시스템이 없다면 버튼 코드에서 소리, 화면, 점수 코드를 모두 알고 있어야 해요. 하지만 이벤트 시스템이 있으면 버튼은 그저 "클릭됐어!"라는 이벤트만 보내면, 나머지는 각자 알아서 반응하는 거예요.
두 번째는 새로운 기능을 쉽게 추가할 수 있기 때문입니다. 나중에 "버튼을 클릭할 때 반짝반짝 효과도 넣고 싶어"라고 생각한다면, 기존 코드를 건드리지 않고도 새로운 리스너만 추가하면 됩니다.
세 번째는 실제 세상과 비슷하기 때문입니다. 우리가 실제로 스마트폰 알림, 학교 방송, 초인종 소리 등에 반응하는 것처럼, 프로그램도 이런 방식으로 동작하면 이해하기 쉬워집니다.
마지막으로는 문제가 생겨도 다른 부분에 영향을 주지 않는다는 점입니다. 소리 재생에 문제가 생겨도 화면은 정상적으로 바뀌고, 점수도 정상적으로 올라갈 수 있어요.
⚙️ 이벤트 시스템의 기본 구조
이벤트 시스템을 만들기 위해서는 몇 가지 중요한 부분들이 필요해요. 방송국을 만들 때 필요한 장비들처럼 말이죠.
1. 이벤트 에미터 (방송국)
이벤트를 발생시키고 리스너들을 관리하는 중심 역할을 해요. 방송국의 메인 컨트롤 룸 같은 거죠.
2. 이벤트 저장소 (방송 채널 목록)
어떤 종류의 이벤트들이 있는지, 각 이벤트를 듣고 있는 리스너들이 누구인지 기록하는 곳이에요.
3. 리스너 등록 시스템 (구독 신청)
새로운 리스너가 "이 이벤트를 듣고 싶어요!"라고 신청할 수 있는 방법이에요.
4. 이벤트 발생 시스템 (방송 송출)
실제로 이벤트가 일어났을 때 모든 관련 리스너들에게 알림을 보내는 기능이에요.
🧪 직접 해보면서 배우기
이제 실제로 이벤트 시스템을 사용한 재미있는 예제들을 만들어 보겠습니다. 처음에는 간단한 것부터 시작해서 점점 더 멋진 것들을 만들어 보겠어요.
🔹 첫 번째 예시: 우리 반 소식 알림 시스템
첫 번째 예제에서는 우리 반에서 일어나는 여러 가지 소식을 친구들에게 알려주는 시스템을 만들어 보겠습니다.
// 우리 반 소식을 알려주는 이벤트 시스템
class ClassNewsSystem {
constructor() {
// 이벤트와 리스너들을 저장하는 상자
this.events = {};
}
// 소식을 듣고 싶은 친구 등록하기
on(eventName, listener) {
// 이 종류의 소식을 처음 듣는 거라면 새로운 목록 만들기
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 듣고 싶어하는 친구를 목록에 추가하기
this.events[eventName].push(listener);
console.log(`"${eventName}" 소식 알림에 새로운 친구가 등록되었어요!`);
// 연결해서 쓸 수 있게 자기 자신을 돌려주기
return this;
}
// 소식 알림 그만 받기
off(eventName, listener) {
// 해당 소식 종류가 없으면 그냥 돌아가기
if (!this.events[eventName]) {
return this;
}
// 특정 친구만 목록에서 제거하기
if (listener) {
this.events[eventName] = this.events[eventName].filter(l => l !== listener);
console.log(`"${eventName}" 소식 알림에서 친구가 나갔어요.`);
}
// 모든 친구들 목록에서 제거하기
else {
delete this.events[eventName];
console.log(`"${eventName}" 소식 알림이 완전히 없어졌어요.`);
}
return this;
}
// 소식 발생시키기 (방송하기)
emit(eventName, ...data) {
// 해당 소식을 듣고 있는 친구가 없으면 false 돌려주기
if (!this.events[eventName]) {
console.log(`"${eventName}" 소식을 듣고 있는 친구가 없어요.`);
return false;
}
console.log(`\n📢 "${eventName}" 소식을 알리는 중...\n`);
// 듣고 있는 모든 친구들에게 소식 전하기
this.events[eventName].forEach(listener => {
try {
// 소식 전달하기
listener(...data);
} catch (error) {
console.error(`소식 전달 중 문제가 생겼어요: ${error.message}`);
}
});
return true;
}
// 한 번만 듣고 싶은 소식 등록하기
once(eventName, listener) {
// 한 번 실행되면 자동으로 사라지는 특별한 리스너 만들기
const oneTimeListener = (...data) => {
// 원래 함수 실행하기
listener(...data);
// 실행 후 자동으로 제거하기
this.off(eventName, oneTimeListener);
};
// 특별한 리스너 등록하기
return this.on(eventName, oneTimeListener);
}
// 현재 듣고 있는 친구 수 알아보기
listenerCount(eventName) {
return this.events[eventName] ? this.events[eventName].length : 0;
}
}
// 우리 반 소식 알림 시스템 사용해보기
const classNews = new ClassNewsSystem();
// 여러 친구들이 관심사에 따라 소식 구독하기
const minji = (news) => {
console.log(`민지: "${news}" 소식을 들었어요! 재미있네요! 😊`);
};
const junho = (news) => {
console.log(`준호: "${news}" 소식 확인했습니다! 👍`);
};
const soyoung = (news) => {
console.log(`소영: 오! "${news}"라니! 저도 참여하고 싶어요! ✨`);
};
// 각자 관심 있는 소식 종류 구독하기
classNews.on('시험소식', minji);
classNews.on('시험소식', junho);
classNews.on('놀이활동', minji);
classNews.on('놀이활동', soyoung);
// 한 번만 듣고 싶은 소식 등록하기
classNews.once('특별소식', (news) => {
console.log(`🎉 특별 소식: "${news}" (이 메시지는 한 번만 나와요!)`);
});
// 소식 발생시켜보기
classNews.emit('시험소식', '다음 주 수학 시험이 있어요');
console.log("\n" + "=".repeat(50) + "\n");
classNews.emit('놀이활동', '이번 주 금요일에 체육대회가 있어요');
console.log("\n" + "=".repeat(50) + "\n");
// 특별 소식 두 번 발생시켜보기
classNews.emit('특별소식', '이번 달 반장 선거를 시작합니다');
classNews.emit('특별소식', '반장 선거 결과 발표'); // 이건 아무도 안 들어요
console.log("\n" + "=".repeat(50) + "\n");
// 민지가 시험 소식 그만 듣기
classNews.off('시험소식', minji);
classNews.emit('시험소식', '과학 시험도 추가되었어요'); // 민지는 안 들어요
// 현재 구독자 수 확인하기
console.log(`\n현재 시험소식 구독자 수: ${classNews.listenerCount('시험소식')}명`);
console.log(`현재 놀이활동 구독자 수: ${classNews.listenerCount('놀이활동')}명`);
이 예제를 단계별로 살펴보면, 먼저 우리 반의 소식을 관리하는 ClassNewsSystem
클래스를 만들었어요. 이 클래스는 소식의 종류별로 듣고 싶어하는 친구들의 목록을 관리하고, 소식이 생기면 해당하는 모든 친구들에게 알려주는 역할을 합니다. 각 친구들은 자신이 관심 있는 소식만 구독해서 원하는 정보만 받을 수 있어요. 또한 한 번만 듣고 싶은 특별한 소식도 설정할 수 있답니다.
🔹 두 번째 예시: 게임 점수 관리 시스템
두 번째 예제에서는 게임에서 점수가 변할 때마다 여러 가지 다른 반응을 보이는 시스템을 만들어 보겠습니다.
// 게임 점수를 관리하는 이벤트 시스템
class GameScoreManager extends ClassNewsSystem {
constructor() {
// 부모 클래스의 기능들 가져오기
super();
// 현재 점수 저장하기
this.currentScore = 0;
// 최고 점수 저장하기
this.highScore = 0;
// 현재 레벨 저장하기
this.level = 1;
}
// 점수 추가하기
addScore(points) {
// 이전 점수 기억해두기
const oldScore = this.currentScore;
// 새로운 점수 계산하기
this.currentScore += points;
console.log(`🎯 점수 획득! +${points}점 (총 ${this.currentScore}점)`);
// 점수 변경 이벤트 발생시키기
this.emit('scoreChanged', {
oldScore: oldScore, // 이전 점수
newScore: this.currentScore, // 새로운 점수
addedPoints: points // 추가된 점수
});
// 최고 점수 갱신 체크하기
if (this.currentScore > this.highScore) {
this.highScore = this.currentScore;
// 새로운 최고 점수 이벤트 발생시키기
this.emit('newHighScore', {
score: this.highScore, // 새로운 최고 점수
oldHighScore: this.highScore - points // 이전 최고 점수
});
}
// 레벨업 체크하기 (1000점마다 레벨업)
const newLevel = Math.floor(this.currentScore / 1000) + 1;
if (newLevel > this.level) {
const oldLevel = this.level;
this.level = newLevel;
// 레벨업 이벤트 발생시키기
this.emit('levelUp', {
oldLevel: oldLevel, // 이전 레벨
newLevel: this.level, // 새로운 레벨
requiredScore: this.level * 1000 // 다음 레벨까지 필요한 점수
});
}
return this.currentScore;
}
// 점수 초기화하기
resetScore() {
// 이전 점수 기억해두기
const oldScore = this.currentScore;
// 점수와 레벨 초기화하기
this.currentScore = 0;
this.level = 1;
console.log(`🔄 게임 초기화! 점수가 0점으로 돌아갔어요.`);
// 게임 초기화 이벤트 발생시키기
this.emit('gameReset', {
oldScore: oldScore, // 이전 점수
oldLevel: Math.floor(oldScore / 1000) + 1 // 이전 레벨
});
return true;
}
// 현재 게임 상태 가져오기
getGameStatus() {
return {
score: this.currentScore, // 현재 점수
highScore: this.highScore, // 최고 점수
level: this.level, // 현재 레벨
nextLevelScore: this.level * 1000 // 다음 레벨까지 필요한 점수
};
}
}
// 게임 점수 관리자 만들기
const gameScore = new GameScoreManager();
// 점수 변경에 반응하는 UI 업데이트 함수
const updateScoreDisplay = (data) => {
console.log(`📱 화면 업데이트: 점수 ${data.oldScore} → ${data.newScore}`);
console.log(` (${data.addedPoints > 0 ? '+' : ''}${data.addedPoints}점 변화)`);
};
// 새 최고 점수에 반응하는 축하 함수
const celebrateHighScore = (data) => {
console.log(`🎉 새로운 최고 점수 달성!`);
console.log(` 이전 최고: ${data.oldHighScore}점`);
console.log(` 새 최고: ${data.score}점`);
console.log(` 🎊 축하합니다! 정말 대단해요!`);
};
// 레벨업에 반응하는 함수
const handleLevelUp = (data) => {
console.log(`⭐ 레벨업! ${data.oldLevel} → ${data.newLevel}`);
console.log(` 🎵 레벨업 음악이 재생됩니다! 🎵`);
console.log(` ✨ 레벨업 효과가 화면에 나타납니다! ✨`);
console.log(` 다음 레벨까지: ${data.requiredScore - gameScore.currentScore}점 필요`);
};
// 게임 초기화에 반응하는 함수
const handleGameReset = (data) => {
console.log(`🔄 게임 초기화됨`);
console.log(` 이전 점수: ${data.oldScore}점`);
console.log(` 이전 레벨: ${data.oldLevel}레벨`);
console.log(` 🆕 새로운 게임을 시작해보세요!`);
};
// 이벤트 리스너들 등록하기
gameScore.on('scoreChanged', updateScoreDisplay);
gameScore.on('newHighScore', celebrateHighScore);
gameScore.on('levelUp', handleLevelUp);
gameScore.on('gameReset', handleGameReset);
// 게임 플레이 시뮬레이션하기
console.log("🎮 게임을 시작합니다!\n");
// 점수 조금씩 얻기
gameScore.addScore(100);
console.log("\n" + "-".repeat(40) + "\n");
gameScore.addScore(250);
console.log("\n" + "-".repeat(40) + "\n");
gameScore.addScore(300);
console.log("\n" + "-".repeat(40) + "\n");
// 큰 점수 얻어서 레벨업하기
gameScore.addScore(500); // 총 1150점이 되어 레벨 2로 올라감
console.log("\n" + "-".repeat(40) + "\n");
// 더 많은 점수 얻기
gameScore.addScore(1000); // 총 2150점이 되어 레벨 3으로 올라감
console.log("\n" + "-".repeat(40) + "\n");
// 현재 게임 상태 확인하기
console.log("📊 현재 게임 상태:");
const status = gameScore.getGameStatus();
console.log(` 점수: ${status.score}점`);
console.log(` 최고점수: ${status.highScore}점`);
console.log(` 레벨: ${status.level}레벨`);
console.log(` 다음 레벨까지: ${status.nextLevelScore - status.score}점 필요`);
console.log("\n" + "-".repeat(40) + "\n");
// 게임 초기화하기
gameScore.resetScore();
이 게임 점수 관리 시스템은 더욱 복잡하고 현실적인 예제를 보여줍니다. 점수가 변할 때마다 여러 가지 다른 일들이 동시에 일어날 수 있어요. 화면 업데이트, 최고 점수 체크, 레벨업 확인 등이 모두 독립적으로 동작하지만 같은 이벤트에 반응합니다. 이처럼 하나의 이벤트가 여러 가지 다른 반응을 불러일으킬 수 있다는 것이 이벤트 시스템의 큰 장점이에요.
🔹 세 번째 예시: 스마트 홈 제어 시스템
세 번째 예제는 집안의 여러 기기들이 서로 연동되는 스마트 홈 시스템입니다. 하나의 행동이 여러 기기들에 연쇄적으로 영향을 주는 모습을 볼 수 있어요.
// 스마트 홈을 제어하는 이벤트 시스템
class SmartHomeController extends ClassNewsSystem {
constructor() {
// 부모 클래스의 기능들 가져오기
super();
// 각 기기들의 상태를 저장하는 객체
this.deviceStates = {
lights: { living: false, bedroom: false, kitchen: false }, // 조명 상태
temperature: { current: 22, target: 22 }, // 온도 상태
security: { armed: false, doorLocked: true }, // 보안 상태
music: { playing: false, volume: 50 }, // 음악 상태
curtains: { open: true } // 커튼 상태
};
}
// 집에 도착했을 때
arriveHome() {
console.log("🏠 집에 도착했어요!");
// 여러 기기들이 반응할 수 있도록 이벤트 발생시키기
this.emit('homeArrival', {
time: '지금', // 현재 시간 (간단하게 표현)
action: 'arrive' // 행동 종류
});
return true;
}
// 집을 떠날 때
leaveHome() {
console.log("🚪 집을 떠나요!");
// 집을 떠날 때 이벤트 발생시키기
this.emit('homeDeparture', {
time: '지금', // 현재 시간 (간단하게 표현)
action: 'leave' // 행동 종류
});
return true;
}
// 잠자리에 들 때
goToBed() {
console.log("😴 잠자리에 들어요!");
// 잠자리 모드 이벤트 발생시키기
this.emit('bedtimeMode', {
time: '밤', // 시간 (간단하게 표현)
action: 'bedtime' // 행동 종류
});
return true;
}
// 아침에 일어날 때
wakeUp() {
console.log("☀️ 좋은 아침이에요!");
// 기상 모드 이벤트 발생시키기
this.emit('morningMode', {
time: '아침', // 시간 (간단하게 표현)
action: 'wakeup' // 행동 종류
});
return true;
}
// 현재 기기 상태 확인하기
getDeviceStates() {
// 복사본을 만들어서 원본이 변경되지 않게 하기
return JSON.parse(JSON.stringify(this.deviceStates));
}
// 기기 상태 업데이트하기
updateDeviceState(device, state, value) {
// 해당 기기가 있는지 확인하고 상태 업데이트하기
if (this.deviceStates[device]) {
this.deviceStates[device][state] = value;
}
}
}
// 스마트 홈 컨트롤러 만들기
const smartHome = new SmartHomeController();
// 조명 시스템
const lightingSystem = {
// 집에 도착했을 때 조명 제어하기
handleHomeArrival: (data) => {
console.log("💡 조명 시스템: 거실 불을 켭니다!");
smartHome.updateDeviceState('lights', 'living', true);
console.log("💡 조명 시스템: 따뜻한 조명으로 설정했어요 🌅");
},
// 집을 떠날 때 조명 제어하기
handleHomeDeparture: (data) => {
console.log("💡 조명 시스템: 모든 불을 끕니다!");
smartHome.updateDeviceState('lights', 'living', false);
smartHome.updateDeviceState('lights', 'bedroom', false);
smartHome.updateDeviceState('lights', 'kitchen', false);
console.log("💡 조명 시스템: 전력 절약을 위해 모든 조명이 꺼졌어요 🌙");
},
// 잠자리에 들 때 조명 제어하기
handleBedtime: (data) => {
console.log("💡 조명 시스템: 침실 조명을 어둡게 조절합니다!");
smartHome.updateDeviceState('lights', 'living', false);
smartHome.updateDeviceState('lights', 'bedroom', true);
console.log("💡 조명 시스템: 편안한 수면을 위한 조명으로 바뀌었어요 🌛");
},
// 아침에 일어날 때 조명 제어하기
handleMorning: (data) => {
console.log("💡 조명 시스템: 상쾌한 아침 조명을 켭니다!");
smartHome.updateDeviceState('lights', 'living', true);
smartHome.updateDeviceState('lights', 'kitchen', true);
console.log("💡 조명 시스템: 활기찬 하루를 위한 밝은 조명이에요 ☀️");
}
};
// 에어컨 시스템
const airConditioningSystem = {
// 집에 도착했을 때 온도 제어하기
handleHomeArrival: (data) => {
console.log("❄️ 에어컨 시스템: 쾌적한 온도로 조절합니다!");
smartHome.updateDeviceState('temperature', 'target', 24);
console.log("❄️ 에어컨 시스템: 24도로 설정했어요! 시원하죠? 😊");
},
// 집을 떠날 때 온도 제어하기
handleHomeDeparture: (data) => {
console.log("❄️ 에어컨 시스템: 절전 모드로 전환합니다!");
smartHome.updateDeviceState('temperature', 'target', 26);
console.log("❄️ 에어컨 시스템: 에너지를 아끼기 위해 26도로 올렸어요 🌱");
},
// 잠자리에 들 때 온도 제어하기
handleBedtime: (data) => {
console.log("❄️ 에어컨 시스템: 수면에 적합한 온도로 조절합니다!");
smartHome.updateDeviceState('temperature', 'target', 23);
console.log("❄️ 에어컨 시스템: 23도로 조절해서 편안한 잠을 자세요 💤");
}
};
// 보안 시스템
const securitySystem = {
// 집을 떠날 때 보안 제어하기
handleHomeDeparture: (data) => {
console.log("🔒 보안 시스템: 집을 안전하게 지키겠습니다!");
smartHome.updateDeviceState('security', 'armed', true);
smartHome.updateDeviceState('security', 'doorLocked', true);
console.log("🔒 보안 시스템: 문이 잠겼고 보안 모드가 켜졌어요 🛡️");
},
// 집에 도착했을 때 보안 제어하기
handleHomeArrival: (data) => {
console.log("🔒 보안 시스템: 주인님 돌아오신 것을 환영합니다!");
smartHome.updateDeviceState('security', 'armed', false);
console.log("🔒 보안 시스템: 보안 모드를 해제했어요 🏠");
}
};
// 음악 시스템
const musicSystem = {
// 집에 도착했을 때 음악 재생하기
handleHomeArrival: (data) => {
console.log("🎵 음악 시스템: 환영 음악을 재생합니다!");
smartHome.updateDeviceState('music', 'playing', true);
smartHome.updateDeviceState('music', 'volume', 40);
console.log("🎵 음악 시스템: 기분 좋은 음악이 흘러나와요 🎶");
},
// 잠자리에 들 때 음악 제어하기
handleBedtime: (data) => {
console.log("🎵 음악 시스템: 잔잔한 수면 음악을 재생합니다!");
smartHome.updateDeviceState('music', 'playing', true);
smartHome.updateDeviceState('music', 'volume', 20);
console.log("🎵 음악 시스템: 평화로운 수면을 위한 음악이에요 🌙");
},
// 아침에 일어날 때 음악 재생하기
handleMorning: (data) => {
console.log("🎵 음악 시스템: 활기찬 모닝 음악을 재생합니다!");
smartHome.updateDeviceState('music', 'playing', true);
smartHome.updateDeviceState('music', 'volume', 60);
console.log("🎵 음악 시스템: 신나는 하루를 시작해봐요! 🎉");
}
};
// 커튼 시스템
const curtainSystem = {
// 잠자리에 들 때 커튼 제어하기
handleBedtime: (data) => {
console.log("🪟 커튼 시스템: 커튼을 닫아서 편안한 잠자리를 만들어요!");
smartHome.updateDeviceState('curtains', 'open', false);
console.log("🪟 커튼 시스템: 바깥 빛을 차단해서 깊은 잠을 잘 수 있어요 🌃");
},
// 아침에 일어날 때 커튼 제어하기
handleMorning: (data) => {
console.log("🪟 커튼 시스템: 커튼을 열어서 자연광을 들여보내요!");
smartHome.updateDeviceState('curtains', 'open', true);
console.log("🪟 커튼 시스템: 상쾌한 아침 햇살이 들어와요! ☀️");
}
};
// 모든 시스템의 이벤트 리스너 등록하기
smartHome.on('homeArrival', lightingSystem.handleHomeArrival);
smartHome.on('homeArrival', airConditioningSystem.handleHomeArrival);
smartHome.on('homeArrival', securitySystem.handleHomeArrival);
smartHome.on('homeArrival', musicSystem.handleHomeArrival);
smartHome.on('homeDeparture', lightingSystem.handleHomeDeparture);
smartHome.on('homeDeparture', airConditioningSystem.handleHomeDeparture);
smartHome.on('homeDeparture', securitySystem.handleHomeDeparture);
smartHome.on('bedtimeMode', lightingSystem.handleBedtime);
smartHome.on('bedtimeMode', airConditioningSystem.handleBedtime);
smartHome.on('bedtimeMode', musicSystem.handleBedtime);
smartHome.on('bedtimeMode', curtainSystem.handleBedtime);
smartHome.on('morningMode', lightingSystem.handleMorning);
smartHome.on('morningMode', musicSystem.handleMorning);
smartHome.on('morningMode', curtainSystem.handleMorning);
// 스마트 홈 시나리오 실행해보기
console.log("🏡 스마트 홈 시스템을 시작합니다!\n");
// 집에 도착하는 시나리오
smartHome.arriveHome();
console.log("\n" + "=".repeat(60) + "\n");
// 잠자리에 드는 시나리오
smartHome.goToBed();
console.log("\n" + "=".repeat(60) + "\n");
// 아침에 일어나는 시나리오
smartHome.wakeUp();
console.log("\n" + "=".repeat(60) + "\n");
// 집을 떠나는 시나리오
smartHome.leaveHome();
console.log("\n" + "=".repeat(60) + "\n");
// 최종 기기 상태 확인하기
console.log("📊 현재 스마트 홈 기기 상태:");
const finalStates = smartHome.getDeviceStates();
console.log("💡 조명:", finalStates.lights);
console.log("❄️ 온도:", finalStates.temperature);
console.log("🔒 보안:", finalStates.security);
console.log("🎵 음악:", finalStates.music);
console.log("🪟 커튼:", finalStates.curtains);
이 스마트 홈 제어 시스템은 가장 복잡하고 현실적인 예제입니다. 하나의 행동(집에 도착, 잠자리에 듦 등)이 여러 가지 기기들에 동시에 영향을 주는 모습을 볼 수 있어요. 각 기기 시스템은 서로를 몰라도 되고, 자신이 관심 있는 이벤트에만 반응하면 됩니다. 새로운 기기를 추가하거나 제거하는 것도 아주 쉽게 할 수 있답니다.
🔄 이벤트 시스템 만들기 과정 정리
지금까지 배운 이벤트 시스템 만들기 과정을 차근차근 정리해 보겠습니다.
첫 번째 단계는 이벤트 저장소 설계하기입니다. 어떤 종류의 이벤트들이 있을지, 각 이벤트를 듣고 있는 리스너들을 어떻게 관리할지 계획을 세우는 거예요. 보통 events
라는 객체를 만들어서 이벤트 이름을 키로, 리스너 배열을 값으로 저장합니다.
두 번째는 리스너 등록 시스템 만들기 단계입니다. on()
메서드를 만들어서 새로운 리스너가 특정 이벤트를 구독할 수 있게 하는 거예요. 이때 같은 이벤트에 여러 리스너가 등록될 수 있도록 배열로 관리합니다.
세 번째는 리스너 제거 시스템 만들기입니다. off()
메서드를 만들어서 더 이상 이벤트를 듣고 싶지 않은 리스너를 제거할 수 있게 해요. 특정 리스너만 제거하거나 모든 리스너를 제거하는 기능을 제공합니다.
네 번째는 이벤트 발생 시스템 만들기입니다. emit()
메서드를 만들어서 실제로 이벤트가 발생했을 때 등록된 모든 리스너들에게 알림을 보내는 기능을 구현해요.
다섯 번째는 추가 기능 구현하기입니다. once()
(한 번만 실행), listenerCount()
(리스너 개수 확인) 같은 편리한 기능들을 추가로 만들어 줍니다.
마지막으로는 오류 처리 추가하기 단계입니다. 리스너 실행 중에 오류가 발생해도 다른 리스너들이 정상적으로 동작할 수 있도록 try-catch
문을 사용해서 안전하게 만듭니다.
🧚♀️ 이야기로 다시 배우기: 우리 동네 소식통
지금까지 배운 내용을 하나의 재미있는 이야기로 다시 정리해볼까요?
우리 동네에는 친절한 소식통 할머니가 계세요. 할머니는 동네에서 일어나는 모든 소식을 가장 먼저 아시고, 관심 있어하는 이웃들에게 알려주시는 일을 하세요.
할머니 집 앞에는 여러 가지 종류의 소식 게시판이 있어요. '아이들 소식 게시판', '할인 정보 게시판', '동네 행사 게시판', '안전 알림 게시판' 등등 정말 많은 종류가 있죠.
동네 사람들은 할머니께 "저는 할인 정보만 알려주세요!" 또는 "우리 아이 관련 소식을 듣고 싶어요!"라고 말씀드려요. 그러면 할머니는 그 사람의 이름을 해당 게시판 옆에 적어두세요.
신기한 일이 생기면, 할머니는 관련된 모든 사람들에게 한꺼번에 전화를 걸어서 소식을 알려주세요. 예를 들어 "마트에서 대할인 행사를 해요!"라는 소식이 생기면, 할인 정보를 듣고 싶어했던 모든 사람들에게 동시에 전화를 거시는 거죠.
가장 편리한 점은 각 사람이 다른 사람들을 알 필요가 없다는 것이에요. 김씨 아줌마는 할인 정보만 신청하면 되고, 박씨 아저씨는 아이 소식만 신청하면 되거든요. 서로 어디 사는지, 뭘 좋아하는지 몰라도 필요한 정보는 정확히 받을 수 있어요.
새로운 이웃이 이사 오면 쉽게 소식 알림에 참여할 수 있고, 이사를 가는 이웃은 신청을 취소하기만 하면 돼요. 심지어는 "이 소식은 한 번만 들려주세요"라는 특별한 요청도 할 수 있답니다.
우리가 만든 이벤트 시스템도 이 친절한 소식통 할머니와 똑같이 동작해요. 코드의 각 부분들이 필요한 소식만 구독하고, 필요한 때에 정확한 정보를 받을 수 있는 멋진 시스템이죠!
✏️ 연습문제로 개념 다지기
이제 연습문제를 함께 풀어보면서 이벤트 시스템에 대해 더 깊이 이해해볼까요? 차근차근 따라와 주세요.
Ex1) 버튼 클릭하면 "축하해요!" 메시지와 함께 색깔이 바뀌는 버튼 만들기
// 버튼 이벤트 시스템 만들기
class ButtonSystem {
constructor() {
// 이벤트와 리스너들을 저장하는 상자
this.events = {};
// 현재 버튼 색깔을 저장하는 변수
this.currentColor = "blue";
}
// 이벤트 리스너 등록하는 함수
on(eventName, listener) {
// 해당 이벤트가 처음이면 새로운 배열 만들기
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 리스너를 목록에 추가하기
this.events[eventName].push(listener);
return this;
}
// 이벤트 발생시키는 함수
emit(eventName, ...data) {
// 해당 이벤트를 듣고 있는 리스너가 있는지 확인하기
if (this.events[eventName]) {
// 모든 리스너들에게 차례대로 알림 보내기
this.events[eventName].forEach(listener => {
// 오류가 생겨도 다른 리스너들이 계속 동작하도록 보호하기
try {
listener(...data);
} catch (error) {
console.error('리스너 실행 중 오류:', error.message);
}
});
}
}
// 버튼 클릭하는 함수
clickButton() {
console.log("🔘 버튼이 클릭되었어요!");
// 버튼 클릭 이벤트 발생시키기
this.emit('buttonClicked', {
clickTime: '지금', // 클릭한 시간 (간단하게 표현)
currentColor: this.currentColor // 현재 색깔
});
}
// 버튼 색깔 바꾸는 함수
changeColor(newColor) {
// 이전 색깔 기억해두기
const oldColor = this.currentColor;
// 새로운 색깔로 변경하기
this.currentColor = newColor;
console.log(`🎨 버튼 색깔이 ${oldColor}에서 ${newColor}로 바뀌었어요!`);
// 색깔 변경 이벤트 발생시키기
this.emit('colorChanged', {
oldColor: oldColor, // 이전 색깔
newColor: newColor // 새로운 색깔
});
}
}
// 버튼 시스템 만들기
const button = new ButtonSystem();
// 축하 메시지를 보여주는 함수
const showCelebration = (data) => {
console.log("🎉 축하해요! 버튼을 클릭했네요!");
console.log(`⏰ 클릭 시간: ${data.clickTime}`);
console.log(`🌈 현재 버튼 색깔: ${data.currentColor}`);
};
// 랜덤 색깔로 바꾸는 함수
const changeToRandomColor = (data) => {
// 여러 가지 색깔 중에서 랜덤하게 선택하기
const colors = ["red", "blue", "green", "yellow", "purple", "orange", "pink"];
// 현재 색깔과 다른 색깔 중에서 선택하기
const availableColors = colors.filter(color => color !== data.currentColor);
// 랜덤하게 하나 선택하기
const randomColor = availableColors[Math.floor(Math.random() * availableColors.length)];
console.log("🎲 랜덤하게 색깔을 바꿔볼게요!");
// 실제로 색깔 바꾸기
button.changeColor(randomColor);
};
// 클릭 횟수를 세는 함수
let clickCount = 0;
const countClicks = (data) => {
// 클릭 횟수 증가시키기
clickCount++;
console.log(`📊 총 클릭 횟수: ${clickCount}번`);
// 10번 클릭하면 특별 메시지 보여주기
if (clickCount === 10) {
console.log("🏆 와! 10번이나 클릭했네요! 정말 대단해요!");
}
};
// 색깔 변경을 기록하는 함수
const logColorChange = (data) => {
console.log(`📝 색깔 변경 기록: ${data.oldColor} → ${data.newColor}`);
};
// 이벤트 리스너들 등록하기
button.on('buttonClicked', showCelebration); // 버튼 클릭 시 축하 메시지
button.on('buttonClicked', changeToRandomColor); // 버튼 클릭 시 색깔 변경
button.on('buttonClicked', countClicks); // 버튼 클릭 시 횟수 세기
button.on('colorChanged', logColorChange); // 색깔 변경 시 기록하기
// 버튼 테스트해보기
console.log("=== 버튼 테스트 시작! ===\n");
// 여러 번 클릭해보기
for (let i = 1; i <= 5; i++) {
console.log(`\n--- ${i}번째 클릭 ---`);
button.clickButton();
console.log("");
}
🎯 17단원까지 배운 내용 복습하기
18단원을 배우기 전에 그동안 배운 중요한 내용들을 복습해보겠습니다!
복습 문제 1: 모듈 시스템 활용하기 (17단원 복습)
// math-utils.js 모듈을 시뮬레이션해보겠습니다
// (실제로는 별도 파일로 만들어야 해요)
// 간단한 계산 함수들을 export하는 모듈
const MathUtils = {
// 두 수를 더하는 함수
add: function(a, b) {
return a + b;
},
// 두 수를 곱하는 함수
multiply: function(a, b) {
return a * b;
},
// 배열의 평균을 구하는 함수
average: function(numbers) {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((total, num) => total + num, 0);
return sum / numbers.length;
}
};
// 모듈을 사용해보기
const result1 = MathUtils.add(10, 20);
const result2 = MathUtils.multiply(5, 6);
const result3 = MathUtils.average([80, 90, 70, 85, 95]);
console.log("복습 문제 1 - 모듈 시스템:");
console.log(`10 + 20 = ${result1}`);
console.log(`5 × 6 = ${result2}`);
console.log(`평균 점수: ${result3.toFixed(1)}점`);
// 💡 해설: 17단원에서 배운 모듈 시스템을 활용해서 계산 기능들을 분리하고 재사용했습니다.
// export와 import를 통해 코드를 깔끔하게 정리할 수 있어요.
복습 문제 2: 클래스와 상속 활용하기 (15단원 복습)
// 동물 기본 클래스
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 기본 행동
speak() {
console.log(`${this.name}이(가) 소리를 냅니다.`);
}
// 정보 출력
getInfo() {
return `이름: ${this.name}, 나이: ${this.age}살`;
}
}
// 강아지 클래스 (Animal을 상속받음)
class Dog extends Animal {
constructor(name, age, breed) {
super(name, age); // 부모 클래스의 생성자 호출
this.breed = breed;
}
// 메서드 오버라이딩
speak() {
console.log(`${this.name}이(가) 멍멍 짖습니다! 🐕`);
}
// 강아지만의 특별한 행동
fetch() {
console.log(`${this.name}이(가) 공을 가져옵니다!`);
}
// 정보에 견종 추가
getInfo() {
return super.getInfo() + `, 견종: ${this.breed}`;
}
}
// 고양이 클래스 (Animal을 상속받음)
class Cat extends Animal {
constructor(name, age, color) {
super(name, age);
this.color = color;
}
// 메서드 오버라이딩
speak() {
console.log(`${this.name}이(가) 야옹 웁니다! 🐱`);
}
// 고양이만의 특별한 행동
purr() {
console.log(`${this.name}이(가) 그르렁거립니다~`);
}
// 정보에 색깔 추가
getInfo() {
return super.getInfo() + `, 색깔: ${this.color}`;
}
}
console.log("\n복습 문제 2 - 클래스와 상속:");
// 동물들 만들기
const buddy = new Dog("버디", 3, "골든 리트리버");
const whiskers = new Cat("수염이", 2, "회색");
// 정보 출력
console.log(buddy.getInfo());
console.log(whiskers.getInfo());
// 각자의 소리 내기
buddy.speak();
whiskers.speak();
// 특별한 행동하기
buddy.fetch();
whiskers.purr();
// 💡 해설: 15단원에서 배운 클래스와 상속을 활용해서 공통 기능은 부모 클래스에,
// 각자의 특별한 기능은 자식 클래스에 구현했습니다.
🤔 조금 더 어려운 문제로 실력 확인하기
기본 연습을 마쳤다면, 이제 조금 더 깊이 있는 문제들을 통해 이벤트 시스템에 대한 이해를 확인해보겠습니다.
Q1. 이벤트 기반 프로그래밍이 언제 유용하고 언제 사용하지 않는 것이 좋은지 설명해보세요.
답:
이벤트 기반 프로그래밍이 유용한 상황:
사용자가 클릭하거나 입력할 때: 웹사이트에서 버튼을 누르거나 텍스트를 입력할 때 여러 가지 일이 동시에 일어나야 할 때 유용해요.
게임 만들 때: 게임에서 몬스터를 잡으면 점수도 오르고, 소리도 나고, 애니메이션도 나와야 하잖아요. 이럴 때 이벤트 시스템이 perfect!
여러 부분이 서로 알림을 주고받을 때: 채팅 프로그램처럼 메시지가 오면 화면에 보여주고, 알림음도 나고, 읽지 않은 메시지 수도 업데이트해야 할 때요.
이벤트 기반 프로그래밍을 사용하지 않는 것이 좋은 상황:
간단한 계산할 때: 그냥 2+3을 계산하는 것처럼 단순한 일에는 이벤트 시스템이 오히려 복잡해요.
순서가 정말 중요할 때: 1번 → 2번 → 3번 순서대로 정확히 해야 하는 일에는 일반 함수가 더 좋아요.
Q2. 다음 코드의 문제점을 찾아서 더 안전하게 만들어보세요.
// 문제가 있는 코드
class UnsafeEventSystem {
constructor() {
this.events = {};
}
on(event, callback) {
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => {
callback(data); // 여기서 오류가 생기면 다른 콜백들이 실행 안 될 수 있어요!
});
}
}
}
개선된 코드:
// 개선된 안전한 이벤트 시스템
class SafeEventSystem {
constructor() {
this.events = {};
}
on(event, callback) {
// 콜백이 함수인지 확인하기
if (typeof callback !== 'function') {
console.error('리스너는 함수여야 해요!');
return this;
}
// 이벤트 배열 초기화하기
if (!this.events[event]) {
this.events[event] = [];
}
// 리스너 추가하기
this.events[event].push(callback);
console.log(`"${event}" 이벤트에 새로운 리스너가 등록되었어요!`);
return this;
}
// 안전한 이벤트 발생시키기
emit(event, ...data) {
if (!this.events[event]) {
console.log(`"${event}" 이벤트를 듣고 있는 리스너가 없어요.`);
return false;
}
// 각 리스너를 안전하게 실행하기
this.events[event].forEach((callback, index) => {
try {
// 리스너 실행하기
callback(...data);
} catch (error) {
console.error(`리스너 ${index + 1}번에서 오류 발생: ${error.message}`);
// 오류가 생겨도 다른 리스너들은 계속 실행돼요!
}
});
return true;
}
// 리스너 제거하기
off(event, callback) {
if (!this.events[event]) {
return this;
}
if (callback) {
// 특정 콜백만 제거하기
this.events[event] = this.events[event].filter(cb => cb !== callback);
} else {
// 모든 콜백 제거하기
delete this.events[event];
}
return this;
}
}
// 개선된 이벤트 시스템 사용해보기
const safeEventSystem = new SafeEventSystem();
// 정상적인 리스너
const goodListener = (message) => {
console.log(`😊 정상 리스너: ${message}`);
};
// 문제가 있는 리스너 (일부러 오류 발생시키기)
const buggyListener = (message) => {
console.log(`🐛 버그 리스너: ${message}`);
throw new Error('일부러 발생시킨 오류');
};
// 또 다른 정상적인 리스너
const anotherGoodListener = (message) => {
console.log(`🌟 다른 정상 리스너: ${message}`);
};
// 리스너들 등록하기
safeEventSystem.on('test', goodListener);
safeEventSystem.on('test', buggyListener); // 이 리스너가 오류를 일으킬 거예요
safeEventSystem.on('test', anotherGoodListener); // 이 리스너는 여전히 실행되어야 해요
console.log("\n🧪 이벤트 발생 테스트:");
safeEventSystem.emit('test', '안녕하세요!');
console.log("\n🔄 버그 리스너 제거 후 다시 테스트:");
safeEventSystem.off('test', buggyListener);
safeEventSystem.emit('test', '이제 안전해요!');
🎉 이번 시간 배운 것들
이번 시간 우리가 함께 배운 내용들을 정리해보면, 먼저 이벤트 시스템이 무엇인지 이해했습니다. 학교 방송실이나 동네 소식 알림판처럼 하나의 사건이 여러 곳에 동시에 알림을 보낼 수 있는 시스템이라는 것을 배웠어요.
둘째로는 이벤트 시스템의 구성 요소들을 배웠습니다. 이벤트 에미터(방송국), 이벤트 리스너(스피커), 그리고 구독과 알림 메커니즘이 어떻게 협력해서 동작하는지 알아보았습니다.
셋째로는 다양한 실전 예제를 통해 이벤트 시스템의 활용법을 익혔습니다. 우리 반 소식 알림, 게임 점수 관리, 스마트 홈 제어 등 실생활과 가까운 예제들로 시스템의 유용함을 체험해 보았어요.
넷째로는 17단원까지 배운 내용들을 복습했습니다. 모듈 시스템과 클래스 상속을 다시 한번 연습하면서 기초를 탄탄히 다졌어요.
마지막으로는 자주 하는 실수들과 더 안전한 이벤트 시스템을 만드는 방법을 알아보았습니다. 이런 내용들을 기억하면 앞으로 더 견고하고 효율적인 이벤트 시스템들을 만들 수 있을 거예요.
다음 시간에는 더 흥미진진한 프로그래밍 주제들이 기다리고 있습니다. 오늘 배운 이벤트 시스템을 꾸준히 연습해서 멋진 코딩 실력을 키워나가세요!
✅ 학습 완료 체크리스트
이번 시간에 배운 내용들을 모두 이해했는지 확인해보세요!
학습 내용 | 이해했나요? |
---|---|
이벤트 시스템의 기본 개념 | ✅ |
on, off, emit 메서드 사용법 | ✅ |
이벤트 리스너 등록과 제거 | ✅ |
안전한 이벤트 처리 방법 | ✅ |
실전 예제 이해 | ✅ |
17단원까지 복습 내용 | ✅ |
📂 마무리 정보
오늘 배운 18.4.2
내용이 여러분의 자바스크립트 지식 상자에 잘 저장되었나요? 다음 시간에는 더 재미있는 내용으로 만나요!
기억할 점: 오늘 배운 내용을 꼭 연습해보시고, 궁금한 점이 있으면 언제든 다시 돌아와서 읽어보세요.
무료 JavaScript 학습 플랫폼에서 단계별 학습과 실시간 코드 실행을 통해
더욱 효과적이고 재미있게 학습하실 수 있습니다.
'18. 똑똑한 코드 패턴 (디자인 패턴) > 18.4 옵저버 패턴' 카테고리의 다른 글
18.4.1 옵저버 패턴이란? - 서로를 바라보는 따뜻한 마음들 (0) | 2025.07.28 |
---|