-
Notifications
You must be signed in to change notification settings - Fork 0
⏰ Rxjs로 실시간 방 타이머 관리
노현진 edited this page Dec 1, 2024
·
1 revision
마피아 게임이 각 방마다 진행될 때, 게임 진행 상황을 알려주기 위해 타이머 기능이 필요했습니다.
예를 들어, 토론 150초 -> 투표 15초 -> 최종 변론 90초 -> 최종 투표 15초 -> 마피아 30초 -> 의사 20초 -> 경찰 20초 -> 토론 150초로 계속 진행되는데 해당 150초, 15초, 90초 등이 사용자마다 다르지 않아야 하고 모두 똑같은 시간에 똑같은 초가 카운트다운 되어야 합니다.
그리고 경찰이나 의사같은 경우 한번 고르면 카운트다운이 끝날 때 까지 기다리지 않고 다음 페이즈로 넘어가게 하는게 UX가 더 향상된다는 판단이 들어 어떤 방식으로 타이머를 구성해야 할 지 고민하게 되었습니다.
그래서 setInterval과 Rxjs 중 Rxjs를 선택했습니다. Rxjs는 비동기 및 이벤트 기반 프로그래밍을 위한 라이브러리이고, Observable 패턴을 기반으로 진행됩니다.
- Observable
- 시간이 지남에 따라 발생하는 데이터의 스트림을 나타냅니다
- 비동기적으로 데이터를 생성하고 전달합니다
- 구독자(Subscriber)가 있어야 데이터를 방출하기 시작합니다
- Observer
- Observable을 구독하여 데이터를 받는 소비자입니다
- next(), error(), complete() 메서드를 통해 데이터를 처리합니다
- Operators
- 데이터 스트림을 변환하고 조작하는 함수들입니다
- map, filter, merge, concat 등 다양한 연산자를 제공합니다
- 파이프라인 형태로 연결하여 복잡한 데이터 처리가 가능합니다
중간에 파이프라인을 조작하여 멈출 수도 있기에 Rxjs의 Interval을 통해 관리했습니다.
@Injectable()
export class MafiaCountdownTimer implements CountdownTimer {
private readonly stopSignals = new Map<GameRoom, Subject<any>>();
private readonly pauses = new Map<GameRoom, boolean>();
async start(room: GameRoom, situation: string): Promise<any> {
if (this.stopSignals.has(room)) {
throw new DuplicateTimerException();
}
this.stopSignals.set(room, new Subject());
this.pauses.set(room, false);
let timeLeft: number = TIMEOUT_SITUATION[situation];
room.sendAll('countdown-start', situation);
const currentSignal = this.stopSignals.get(room);
let paused = this.pauses.get(room);
return new Promise<void>((resolve) => {
interval(1000)
.pipe(
takeUntil(currentSignal),
takeWhile(() => timeLeft > 0 && !paused),
)
.subscribe({
next: () => {
paused = this.pauses.get(room);
room.sendAll('countdown', {
situation: situation,
timeLeft: timeLeft,
});
timeLeft--;
},
complete: () => {
this.stop(room);
resolve();
},
});
});
}
private pause(room: GameRoom): void {
this.pauses.set(room, true);
}
private cleanup(room: GameRoom): void {
const signal = this.stopSignals.get(room);
if (!signal) {
throw new NotFoundTimerException();
}
signal.next(null);
signal.complete();
this.stopSignals.delete(room);
this.pauses.delete(room);
}
stop(room: GameRoom): void {
if (!this.pauses.has(room)) {
return;
}
this.pause(room);
this.cleanup(room);
}
}
이 코드에서 중요하게 살펴볼 부분이 있습니다.
return new Promise<void>((resolve) => {
interval(1000)
.pipe(
takeUntil(currentSignal),
takeWhile(() => timeLeft > 0 && !paused),
)
.subscribe({
next: () => {
paused = this.pauses.get(room);
room.sendAll('countdown', {
situation: situation,
timeLeft: timeLeft,
});
timeLeft--;
},
complete: () => {
this.stop(room);
resolve();
},
});
});
- interval을 통해 1초마다 진행하게 했으며
pipe
를 통해takeUtil
과takeWhile
을 통해 제어를 진행합니다 - paused신호를 통해 중간에 해당 타이머를 멈출 수 있습니다
- next를 통해 해당 이벤트를 구독하는 구독자들에게 콜백함수를 실행하는데 해당 콜백함수는 각 방마다 현재 페이즈와 남은 시간을 보내는 소켓 통신을 진행합니다
private pause(room: GameRoom): void {
this.pauses.set(room, true);
}
private cleanup(room: GameRoom): void {
const signal = this.stopSignals.get(room);
if (!signal) {
throw new NotFoundTimerException();
}
signal.next(null);
signal.complete();
this.stopSignals.delete(room);
this.pauses.delete(room);
}
stop(room: GameRoom): void {
if (!this.pauses.has(room)) {
return;
}
this.pause(room);
this.cleanup(room);
}
- stop method를 통해 this.pause 메서드를 호출하여 각 방마다의 타이머를 멈추고 타이머를 삭제합니다. 이렇게 Rxjs를 통해 견고하고 간편하게 각 방마다 중앙통제식 타이머를 구축하여 사용자에게 전부 같은 시간을 보내줄 수 있게 되었습니다
web12-MafiaCamp