Skip to content

⏰ Rxjs로 실시간 방 타이머 관리

노현진 edited this page Dec 1, 2024 · 1 revision

마피아 게임이 각 방마다 진행될 때, 게임 진행 상황을 알려주기 위해 타이머 기능이 필요했습니다.

예를 들어, 토론 150초 -> 투표 15초 -> 최종 변론 90초 -> 최종 투표 15초 -> 마피아 30초 -> 의사 20초 -> 경찰 20초 -> 토론 150초로 계속 진행되는데 해당 150초, 15초, 90초 등이 사용자마다 다르지 않아야 하고 모두 똑같은 시간에 똑같은 초가 카운트다운 되어야 합니다.

그리고 경찰이나 의사같은 경우 한번 고르면 카운트다운이 끝날 때 까지 기다리지 않고 다음 페이즈로 넘어가게 하는게 UX가 더 향상된다는 판단이 들어 어떤 방식으로 타이머를 구성해야 할 지 고민하게 되었습니다. image

그래서 setInterval과 Rxjs 중 Rxjs를 선택했습니다. Rxjs는 비동기 및 이벤트 기반 프로그래밍을 위한 라이브러리이고, Observable 패턴을 기반으로 진행됩니다.

  1. Observable
  • 시간이 지남에 따라 발생하는 데이터의 스트림을 나타냅니다
  • 비동기적으로 데이터를 생성하고 전달합니다
  • 구독자(Subscriber)가 있어야 데이터를 방출하기 시작합니다
  1. Observer
  • Observable을 구독하여 데이터를 받는 소비자입니다
  • next(), error(), complete() 메서드를 통해 데이터를 처리합니다
  1. 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를 통해 takeUtiltakeWhile을 통해 제어를 진행합니다
  • 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를 통해 견고하고 간편하게 각 방마다 중앙통제식 타이머를 구축하여 사용자에게 전부 같은 시간을 보내줄 수 있게 되었습니다

MafiaCamp

📔소개
🎯프로젝트 규칙
💻프로젝트 기획
🍀기술 스택
📚그룹 회고
🌈개발 일지
🍀문제 해결 경험
🔧트러블 슈팅
Clone this wiki locally