🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Phaser 게임 레벨 진행과 체크포인트 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 5. · 11 Views

Phaser 게임 레벨 진행과 체크포인트 완벽 가이드

Phaser 게임에서 레벨 클리어 조건 설정부터 체크포인트 저장, localStorage를 활용한 진행 상황 관리까지 게임 진행 시스템의 모든 것을 다룹니다. 플레이어가 게임을 껐다 켜도 이어서 플레이할 수 있는 완성도 높은 게임을 만들어 봅시다.


목차

  1. 레벨 클리어 조건
  2. 다음 레벨 전환
  3. 체크포인트 저장
  4. 레벨 선택 화면
  5. 진행 상황 저장
  6. localStorage 활용

1. 레벨 클리어 조건

김개발 씨가 처음으로 플랫포머 게임을 만들고 있습니다. 캐릭터도 움직이고, 적도 등장하는데 뭔가 허전합니다.

"게임은 언제 끝나는 거지?" 레벨을 클리어하는 조건을 어떻게 만들어야 할지 막막해졌습니다.

레벨 클리어 조건은 플레이어가 해당 스테이지를 성공적으로 완료했는지 판단하는 기준입니다. 마치 시험에서 합격 점수를 정해두는 것과 같습니다.

목표 지점 도달, 모든 적 처치, 아이템 수집 완료 등 다양한 조건을 설정할 수 있으며, 이를 통해 게임에 명확한 목표와 성취감을 부여합니다.

다음 코드를 살펴봅시다.

class GameScene extends Phaser.Scene {
  create() {
    this.coinsCollected = 0;
    this.totalCoins = 10;
    this.enemiesDefeated = 0;
    this.totalEnemies = 5;

    // 골인 지점 생성
    this.goalZone = this.add.zone(750, 300, 50, 100);
    this.physics.add.existing(this.goalZone, true);

    // 플레이어가 골인 지점에 도달했을 때
    this.physics.add.overlap(this.player, this.goalZone, () => {
      if (this.checkClearConditions()) {
        this.levelCleared();
      }
    });
  }

  checkClearConditions() {
    // 모든 조건을 만족해야 클리어
    const coinsComplete = this.coinsCollected >= this.totalCoins;
    const enemiesComplete = this.enemiesDefeated >= this.totalEnemies;
    return coinsComplete && enemiesComplete;
  }

  levelCleared() {
    this.scene.pause();
    this.showClearScreen();
  }
}

김개발 씨는 입사 3개월 차 게임 개발자입니다. 회사에서 첫 프로젝트로 간단한 플랫포머 게임을 맡게 되었습니다.

캐릭터 이동, 점프, 적과의 충돌까지 구현했는데 정작 "게임을 어떻게 끝내야 하지?"라는 근본적인 질문 앞에서 막혔습니다. 선배 개발자 박시니어 씨가 김개발 씨의 화면을 보더니 말했습니다.

"아, 레벨 클리어 조건을 안 만들었구나. 게임의 목표가 없으면 플레이어는 금방 지루해져요." 그렇다면 레벨 클리어 조건이란 정확히 무엇일까요?

쉽게 비유하자면, 레벨 클리어 조건은 마치 학교 시험의 합격 기준과 같습니다. 60점 이상이면 합격, 미만이면 불합격.

게임도 마찬가지입니다. 특정 조건을 충족하면 클리어, 그렇지 않으면 계속 플레이해야 합니다.

이 명확한 기준이 있어야 플레이어는 무엇을 해야 하는지 알 수 있습니다. 레벨 클리어 조건이 없던 시절을 상상해 봅시다.

아니, 정확히 말하면 조건이 불명확했던 게임들이 있었습니다. 플레이어는 "내가 뭘 해야 하지?"라며 방황하고, 결국 게임을 껐습니다.

목표 없는 게임은 모래사장에서 목적지 없이 걷는 것과 같습니다. 바로 이런 문제를 해결하기 위해 명확한 클리어 조건 시스템이 필요합니다.

클리어 조건을 사용하면 플레이어에게 명확한 목표를 제시할 수 있습니다. 또한 성취감과 보상 시스템의 기반이 됩니다.

무엇보다 게임의 난이도를 조절하는 핵심 요소가 됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 coinsCollectedtotalCoins 변수로 코인 수집 현황을 추적합니다. 이 부분이 핵심입니다.

다음으로 goalZone을 만들어 물리적인 골인 지점을 설정합니다. 플레이어가 이 영역에 닿으면 checkClearConditions 함수가 호출되어 모든 조건을 검사합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 모바일 퍼즐 게임을 개발한다고 가정해봅시다.

별 3개 시스템을 구현할 때 "기본 클리어 조건 + 시간 보너스 + 최소 이동 횟수"처럼 여러 조건을 조합합니다. 이렇게 하면 같은 레벨도 다양한 방식으로 즐길 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 조건을 너무 복잡하게 만드는 것입니다.

처음에는 단순하게 시작하세요. "목표 지점 도달"이라는 기본 조건부터 구현하고, 점차 코인 수집이나 적 처치 조건을 추가하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 김개발 씨는 먼저 간단한 골인 지점을 만들었습니다.

그리고 코인 10개를 모아야만 골인할 수 있도록 조건을 추가했습니다. 테스트해보니 훨씬 게임다운 느낌이 났습니다.

"이게 게임이지!"

실전 팁

💡 - 클리어 조건은 UI로 명확하게 표시하세요. 플레이어가 현재 진행 상황을 항상 알 수 있어야 합니다.

  • 여러 조건을 조합할 때는 AND 조건과 OR 조건을 적절히 섞어 난이도를 조절하세요.

2. 다음 레벨 전환

레벨 클리어 조건을 만든 김개발 씨가 다음 고민에 빠졌습니다. "클리어 화면이 뜨고 나서 다음 레벨로 어떻게 넘어가지?" 현재는 클리어 후 화면이 멈춰버립니다.

자연스러운 레벨 전환이 필요했습니다.

레벨 전환은 현재 씬을 종료하고 다음 씬을 시작하는 과정입니다. Phaser에서는 Scene Manager가 이 역할을 담당합니다.

마치 영화관에서 한 편의 영화가 끝나고 다음 영화가 시작되는 것처럼, 게임도 레벨 간 매끄러운 전환이 필요합니다.

다음 코드를 살펴봅시다.

class GameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'GameScene' });
  }

  init(data) {
    // 이전 씬에서 전달받은 데이터
    this.currentLevel = data.level || 1;
  }

  levelCleared() {
    // 클리어 연출 후 다음 레벨로
    this.cameras.main.fade(500, 0, 0, 0);

    this.time.delayedCall(500, () => {
      if (this.currentLevel < this.maxLevel) {
        // 다음 레벨로 전환
        this.scene.restart({ level: this.currentLevel + 1 });
      } else {
        // 모든 레벨 클리어 - 엔딩 씬으로
        this.scene.start('EndingScene', {
          totalScore: this.score
        });
      }
    });
  }

  goToLevelSelect() {
    this.scene.start('LevelSelectScene', {
      unlockedLevel: this.currentLevel + 1
    });
  }
}

김개발 씨는 레벨 1을 완성했습니다. 그런데 문제가 생겼습니다.

클리어하면 화면이 그냥 멈춰버립니다. "레벨 2로 넘어가야 하는데..." 어떻게 해야 할지 몰라 다시 박시니어 씨를 찾아갔습니다.

박시니어 씨가 웃으며 말했습니다. "씬 전환을 아직 안 배웠구나.

Phaser의 Scene Manager를 알아야 해요." 씬 전환이란 정확히 무엇일까요? 쉽게 비유하자면, 씬 전환은 마치 연극의 막이 바뀌는 것과 같습니다.

1막이 끝나면 무대 뒤에서 배경을 바꾸고, 2막이 시작됩니다. 관객은 자연스럽게 새로운 이야기로 넘어갑니다.

게임의 레벨 전환도 이와 같습니다. 현재 레벨의 모든 요소를 정리하고, 새로운 레벨의 요소를 불러옵니다.

씬 전환 없이 게임을 만든다면 어떻게 될까요? 모든 레벨을 하나의 거대한 씬에 넣어야 합니다.

코드는 복잡해지고, 메모리 사용량은 치솟습니다. 레벨 하나를 수정하려면 전체 코드를 뒤져야 합니다.

유지보수의 악몽이 시작되는 것입니다. Phaser의 Scene Manager는 이런 문제를 우아하게 해결합니다.

**this.scene.start()**는 현재 씬을 종료하고 새 씬을 시작합니다. **this.scene.restart()**는 현재 씬을 처음부터 다시 시작합니다.

데이터를 함께 전달할 수 있어서 레벨 번호나 점수 같은 정보를 넘겨줄 수 있습니다. 위의 코드에서 init 메서드가 중요합니다.

이 메서드는 씬이 시작될 때 가장 먼저 호출되며, 이전 씬에서 전달한 데이터를 받습니다. data.level로 현재 레벨 번호를 받아옵니다.

없으면 기본값 1을 사용합니다. levelCleared 함수를 보면 먼저 카메라 페이드 효과를 줍니다.

화면이 서서히 어두워지면서 0.5초 후에 다음 씬으로 전환됩니다. 이런 전환 효과가 있어야 플레이어가 자연스럽게 다음 레벨로 넘어갔다고 느낍니다.

갑자기 화면이 바뀌면 어색하니까요. 실제 현업에서는 레벨 전환 시 로딩 화면을 넣기도 합니다.

특히 다음 레벨의 에셋이 크다면 로딩 씬을 중간에 배치합니다. "Loading Level 2..."라는 화면이 뜨는 동안 필요한 리소스를 미리 불러오는 것입니다.

주의할 점이 있습니다. 씬 전환 시 현재 씬의 이벤트 리스너나 타이머를 제대로 정리하지 않으면 메모리 누수가 발생합니다.

Phaser는 대부분 자동으로 처리해주지만, 직접 추가한 전역 이벤트는 shutdown 메서드에서 명시적으로 제거해야 합니다. 김개발 씨는 코드를 적용해보았습니다.

레벨 1을 클리어하니 화면이 서서히 어두워지더니 레벨 2가 시작되었습니다. "와, 진짜 게임 같다!" 드디어 게임다운 흐름이 만들어졌습니다.

실전 팁

💡 - 씬 전환 시 페이드나 슬라이드 같은 효과를 넣어 자연스러움을 더하세요.

  • 전환 중에는 플레이어 입력을 비활성화하여 의도치 않은 동작을 방지하세요.

3. 체크포인트 저장

김개발 씨의 게임이 점점 발전하고 있습니다. 그런데 테스트 중 문제를 발견했습니다.

레벨 중간에 죽으면 처음부터 다시 시작해야 합니다. 긴 레벨에서는 너무 답답했습니다.

"중간 저장 지점이 필요해!"

체크포인트는 게임 내 특정 지점에서 플레이어의 상태를 임시 저장하는 시스템입니다. 마치 등산할 때 중간 휴게소에서 쉬어가는 것처럼, 플레이어가 죽거나 실패했을 때 처음이 아닌 체크포인트에서 다시 시작할 수 있게 해줍니다.

이는 게임의 난이도와 플레이어 경험에 직접적인 영향을 미칩니다.

다음 코드를 살펴봅시다.

class GameScene extends Phaser.Scene {
  create() {
    this.checkpoints = [];
    this.currentCheckpoint = null;

    // 체크포인트 깃발들 생성
    this.checkpointGroup = this.physics.add.staticGroup();
    this.levelData.checkpoints.forEach((pos, index) => {
      const flag = this.checkpointGroup.create(pos.x, pos.y, 'flag');
      flag.checkpointId = index;
      flag.activated = false;
    });

    // 플레이어가 체크포인트에 닿았을 때
    this.physics.add.overlap(this.player, this.checkpointGroup,
      (player, flag) => {
        if (!flag.activated) {
          this.activateCheckpoint(flag);
        }
      }
    );
  }

  activateCheckpoint(flag) {
    flag.activated = true;
    flag.setTint(0x00ff00); // 활성화 표시

    // 현재 상태 저장
    this.currentCheckpoint = {
      x: flag.x,
      y: flag.y,
      coins: this.coinsCollected,
      health: this.player.health,
      time: this.elapsedTime
    };

    this.showCheckpointMessage();
  }

  respawnAtCheckpoint() {
    if (this.currentCheckpoint) {
      this.player.setPosition(
        this.currentCheckpoint.x,
        this.currentCheckpoint.y
      );
      this.coinsCollected = this.currentCheckpoint.coins;
      this.player.health = this.currentCheckpoint.health;
    } else {
      // 체크포인트 없으면 시작 지점
      this.player.setPosition(this.startX, this.startY);
    }
  }
}

김개발 씨가 만든 레벨 3은 꽤 길었습니다. 끝까지 가는 데 5분 정도 걸립니다.

그런데 끝부분에서 죽으면? 처음부터 다시.

테스트하던 김개발 씨도 짜증이 났습니다. "내가 만들었는데 나도 짜증나면 유저들은 얼마나 화가 나겠어..." 박시니어 씨가 지나가다 말했습니다.

"체크포인트 시스템 넣어야지. 그거 없으면 유저들 이탈해요." 체크포인트란 정확히 무엇일까요?

쉽게 비유하자면, 체크포인트는 마치 책갈피와 같습니다. 책을 읽다가 잠깐 멈출 때 책갈피를 끼워둡니다.

다음에 읽을 때 처음부터 읽을 필요 없이 책갈피 위치에서 시작하면 됩니다. 게임의 체크포인트도 마찬가지입니다.

특정 지점을 통과하면 그 위치를 기억해두었다가, 실패 시 그곳에서 다시 시작합니다. 체크포인트가 없던 고전 게임들을 떠올려 봅시다.

슈퍼 마리오 브라더스 초기작은 체크포인트가 없어서 월드 처음부터 다시 시작해야 했습니다. 지금 생각하면 정말 고통스럽죠.

요즘 게임에서 그랬다간 앱 삭제 버튼이 눌릴 것입니다. 위의 코드를 살펴봅시다.

먼저 checkpointGroup으로 체크포인트 깃발들을 생성합니다. 레벨 데이터에 정의된 위치에 깃발을 배치합니다.

플레이어가 깃발에 닿으면 activateCheckpoint 함수가 호출됩니다. activateCheckpoint 함수가 핵심입니다.

깃발을 초록색으로 바꿔 활성화를 표시하고, currentCheckpoint 객체에 현재 상태를 저장합니다. 여기서 중요한 것은 위치뿐만 아니라 코인 수, 체력, 경과 시간까지 함께 저장한다는 점입니다.

체크포인트에서 재시작할 때 이 모든 상태가 복원됩니다. respawnAtCheckpoint 함수는 플레이어가 죽었을 때 호출됩니다.

저장된 체크포인트가 있으면 그 위치와 상태로 복원하고, 없으면 레벨 시작 지점으로 보냅니다. 단순하지만 플레이어 경험에 큰 차이를 만듭니다.

실무에서는 체크포인트 배치가 레벨 디자인의 핵심입니다. 너무 많으면 긴장감이 없고, 너무 적으면 답답합니다.

보통 어려운 구간 직전에 배치하여 "여기서 연습하세요"라는 신호를 줍니다. 보스전 직전에 체크포인트가 있는 것도 이런 이유입니다.

주의할 점이 있습니다. 체크포인트 저장 시 어디까지 저장할지 신중하게 결정해야 합니다.

예를 들어 적의 위치까지 저장하면 버그가 생길 수 있습니다. 체크포인트 통과 후 적이 부활했는데, 재시작하니 적이 이미 죽어있는 상태가 되는 식입니다.

김개발 씨는 레벨 3의 중간중간에 깃발을 배치했습니다. 테스트해보니 확실히 덜 답답했습니다.

어려운 구간에서 죽어도 바로 앞 체크포인트에서 시작하니 "한 번 더 해볼까?"라는 생각이 들었습니다.

실전 팁

💡 - 체크포인트 활성화 시 시각적, 청각적 피드백을 주어 플레이어가 인지하도록 하세요.

  • 저장할 상태를 최소화하여 버그 가능성을 줄이세요. 위치와 핵심 변수만 저장하는 것이 안전합니다.

4. 레벨 선택 화면

김개발 씨의 게임에 레벨이 10개가 되었습니다. 그런데 매번 레벨 1부터 순서대로 플레이해야만 합니다.

"레벨 7을 테스트하고 싶은데 앞에 6개를 다 깨야 하나?" 레벨을 자유롭게 선택할 수 있는 화면이 필요해졌습니다.

레벨 선택 화면은 플레이어가 원하는 레벨로 직접 이동할 수 있는 인터페이스입니다. 마치 책의 목차에서 원하는 장으로 바로 넘어가는 것과 같습니다.

보통 클리어한 레벨은 열려있고, 아직 도달하지 못한 레벨은 잠겨있는 형태로 구현합니다. 이를 통해 플레이어는 좋아하는 레벨을 반복 플레이하거나 어려운 레벨에 재도전할 수 있습니다.

다음 코드를 살펴봅시다.

class LevelSelectScene extends Phaser.Scene {
  constructor() {
    super({ key: 'LevelSelectScene' });
  }

  create() {
    // 저장된 진행 상황 불러오기
    const savedData = localStorage.getItem('gameProgress');
    this.unlockedLevel = savedData
      ? JSON.parse(savedData).unlockedLevel
      : 1;

    this.add.text(400, 50, '레벨 선택', { fontSize: '32px' })
      .setOrigin(0.5);

    // 레벨 버튼 생성 (3x4 그리드)
    for (let i = 0; i < 12; i++) {
      const row = Math.floor(i / 4);
      const col = i % 4;
      const x = 150 + col * 180;
      const y = 150 + row * 120;

      this.createLevelButton(i + 1, x, y);
    }
  }

  createLevelButton(level, x, y) {
    const isUnlocked = level <= this.unlockedLevel;
    const color = isUnlocked ? 0x4a90d9 : 0x666666;

    const button = this.add.rectangle(x, y, 100, 80, color)
      .setInteractive({ useHandCursor: isUnlocked });

    this.add.text(x, y, isUnlocked ? level : '🔒', {
      fontSize: '24px'
    }).setOrigin(0.5);

    if (isUnlocked) {
      button.on('pointerdown', () => {
        this.scene.start('GameScene', { level: level });
      });

      button.on('pointerover', () => button.setFillStyle(0x6ab0f9));
      button.on('pointerout', () => button.setFillStyle(0x4a90d9));
    }
  }
}

레벨이 10개가 되자 문제가 생겼습니다. 김개발 씨가 레벨 8의 밸런스를 테스트하려면 매번 레벨 1부터 7까지 클리어해야 했습니다.

30분이나 걸립니다. "이건 미친 짓이야..." 박시니어 씨가 커피를 건네며 말했습니다.

"레벨 선택 화면 만들어. 개발할 때도 편하고, 유저들도 좋아해요." 레벨 선택 화면이란 무엇일까요?

마치 넷플릭스에서 드라마 에피소드를 선택하는 것과 같습니다. 시즌 1을 다 보면 시즌 2가 열리고, 원하는 에피소드를 클릭하면 바로 그 회차를 볼 수 있습니다.

게임의 레벨 선택도 마찬가지입니다. 클리어한 레벨은 언제든 다시 플레이할 수 있고, 아직 도달하지 못한 레벨은 잠겨있습니다.

레벨 선택 화면이 없으면 여러 문제가 생깁니다. 개발자는 테스트하기 힘들고, 플레이어는 좋아하는 레벨을 다시 플레이하려면 처음부터 해야 합니다.

친구에게 특정 레벨을 보여주고 싶어도 불가능합니다. 접근성이 크게 떨어지는 것입니다.

위의 코드를 분석해봅시다. LevelSelectScene이라는 별도의 씬을 만듭니다.

create에서 먼저 localStorage에서 저장된 진행 상황을 불러옵니다. unlockedLevel은 플레이어가 도달한 최고 레벨입니다.

3x4 그리드로 12개의 레벨 버튼을 배치합니다. 각 버튼은 createLevelButton 함수로 생성합니다.

여기서 핵심은 isUnlocked 변수입니다. 현재 레벨이 해금된 레벨 이하면 열려있고, 초과하면 잠겨있습니다.

잠긴 레벨은 회색으로 표시하고 자물쇠 아이콘을 보여줍니다. setInteractiveuseHandCursor: false를 주어 클릭해도 반응하지 않게 합니다.

반면 열린 레벨은 파란색으로 표시하고, 마우스를 올리면 색이 밝아지는 호버 효과를 줍니다. 클릭하면 this.scene.start로 GameScene을 시작하며, level 번호를 함께 전달합니다.

이렇게 하면 원하는 레벨로 바로 이동할 수 있습니다. 실무에서는 레벨 선택 화면에 추가 정보를 표시하기도 합니다.

각 레벨의 별점(별 3개 시스템), 최고 점수, 클리어 시간 등을 보여줍니다. 플레이어에게 "이 레벨은 아직 별 2개밖에 못 받았네, 다시 도전해볼까?"라는 동기를 부여합니다.

주의할 점이 있습니다. 레벨 해금 상태를 클라이언트에만 저장하면 조작이 가능합니다.

싱글 플레이 게임이면 괜찮지만, 리더보드나 업적이 있다면 서버 검증이 필요합니다. 물론 작은 프로젝트에서는 localStorage만으로 충분합니다.

김개발 씨는 레벨 선택 화면을 만들고 나서 테스트가 훨씬 편해졌습니다. 레벨 8을 클릭하면 바로 레벨 8이 시작됩니다.

"이렇게 편할 수가!" 개발 효율이 몇 배는 올라간 느낌이었습니다.

실전 팁

💡 - 개발 중에는 모든 레벨을 해금하는 치트 버튼을 만들어두면 테스트가 편합니다.

  • 레벨 버튼에 별점이나 점수를 함께 표시하면 재도전 동기를 부여할 수 있습니다.

5. 진행 상황 저장

김개발 씨가 게임을 테스트하던 중 브라우저를 껐다가 다시 켰습니다. 그런데 진행 상황이 모두 초기화되어 있었습니다.

레벨 10까지 깼는데 다시 레벨 1부터? "이건 말이 안 돼!" 게임을 껐다 켜도 진행 상황이 유지되어야 합니다.

진행 상황 저장은 플레이어의 게임 데이터를 영구적으로 보존하는 시스템입니다. 마치 일기장에 오늘 한 일을 기록해두는 것처럼, 게임을 종료해도 다음에 이어서 할 수 있도록 합니다.

해금된 레벨, 획득한 점수, 모은 아이템 등 중요한 데이터를 저장하고 불러오는 기능은 현대 게임의 필수 요소입니다.

다음 코드를 살펴봅시다.

class GameProgressManager {
  constructor() {
    this.storageKey = 'myGameProgress';
    this.data = this.loadProgress();
  }

  // 저장된 데이터 불러오기
  loadProgress() {
    const saved = localStorage.getItem(this.storageKey);
    if (saved) {
      return JSON.parse(saved);
    }
    // 기본값 반환
    return {
      unlockedLevel: 1,
      highScores: {},
      totalStars: 0,
      settings: { music: true, sfx: true }
    };
  }

  // 진행 상황 저장
  saveProgress() {
    localStorage.setItem(
      this.storageKey,
      JSON.stringify(this.data)
    );
  }

  // 레벨 클리어 시 호출
  onLevelComplete(level, score, stars) {
    // 해금 레벨 업데이트
    if (level >= this.data.unlockedLevel) {
      this.data.unlockedLevel = level + 1;
    }

    // 최고 점수 갱신
    const prevBest = this.data.highScores[level] || 0;
    if (score > prevBest) {
      this.data.highScores[level] = score;
    }

    this.data.totalStars += stars;
    this.saveProgress();
  }

  // 진행 상황 초기화
  resetProgress() {
    localStorage.removeItem(this.storageKey);
    this.data = this.loadProgress();
  }
}

김개발 씨는 큰일났습니다. QA팀에서 버그 리포트가 왔습니다.

"게임 껐다 키면 처음부터 다시 시작해야 합니다." 당연히 저장되는 줄 알았는데 아니었습니다. 김개발 씨는 진행 상황 저장을 구현한 적이 없었습니다.

박시니어 씨가 한숨을 쉬며 말했습니다. "진행 상황 저장 없이 출시하려고 했어요?

유저들 다 도망가요." 진행 상황 저장이란 무엇일까요? 마치 RPG 게임에서 여관에 들러 세이브하는 것과 같습니다.

다음에 게임을 시작하면 세이브한 시점부터 이어서 할 수 있습니다. 웹 게임에서는 localStorage라는 브라우저 저장소를 사용합니다.

컴퓨터를 껐다 켜도 데이터가 남아있습니다. 진행 상황 저장이 없다면 어떨까요?

매번 처음부터 시작해야 합니다. 10시간 플레이한 데이터가 날아갑니다.

아무도 그런 게임을 하지 않을 것입니다. 특히 모바일 게임에서는 치명적입니다.

배터리가 다 되거나 전화가 오면 게임이 종료되는데, 매번 처음부터라니요. 위의 코드에서 GameProgressManager 클래스를 만들었습니다.

이 클래스가 모든 저장/불러오기를 담당합니다. 생성자에서 loadProgress를 호출하여 저장된 데이터를 불러옵니다.

데이터가 없으면 기본값을 반환합니다. data 객체에는 여러 정보가 담깁니다.

unlockedLevel은 해금된 최고 레벨, highScores는 각 레벨별 최고 점수, totalStars는 획득한 총 별 개수, settings는 음악/효과음 설정입니다. 게임에 필요한 모든 영구 데이터를 여기에 저장합니다.

onLevelComplete 함수가 핵심입니다. 레벨을 클리어할 때마다 호출됩니다.

해금 레벨을 업데이트하고, 최고 점수를 갱신하고, 별을 추가합니다. 그리고 saveProgress로 즉시 저장합니다.

이렇게 중요한 순간마다 저장해야 데이터 손실을 방지할 수 있습니다. 실무에서는 저장 타이밍이 중요합니다.

너무 자주 저장하면 성능에 영향을 주고, 너무 적게 저장하면 데이터가 손실됩니다. 보통 레벨 클리어, 중요 아이템 획득, 설정 변경 시에 저장합니다.

일정 시간마다 자동 저장하는 것도 좋은 방법입니다. 주의할 점이 있습니다.

localStorage에는 용량 제한이 있습니다. 보통 5MB 정도입니다.

큰 데이터를 저장하면 에러가 발생할 수 있으니 try-catch로 감싸는 것이 안전합니다. 또한 JSON.parse 시 잘못된 데이터가 있으면 에러가 발생하므로 예외 처리가 필요합니다.

김개발 씨는 GameProgressManager를 구현하고 게임 전체에 적용했습니다. 브라우저를 껐다 켜도 진행 상황이 그대로 유지됩니다.

"이제야 제대로 된 게임 같다!"

실전 팁

💡 - 저장 시 버전 번호를 함께 저장하세요. 나중에 데이터 구조가 바뀌면 마이그레이션에 도움이 됩니다.

  • 중요한 작업 후에는 항상 저장하세요. 레벨 클리어, 아이템 구매 등의 순간에 즉시 저장합니다.

6. localStorage 활용

김개발 씨가 진행 상황 저장을 구현했지만 아직 불안합니다. "localStorage가 정확히 뭐지?

안전한 건가?" 제대로 알지 못하고 사용하는 것 같아 찜찜했습니다. localStorage의 특성과 주의사항을 제대로 알아야 합니다.

localStorage는 브라우저에서 제공하는 웹 저장소로, 키-값 쌍으로 데이터를 저장합니다. 마치 브라우저 안에 있는 작은 금고와 같습니다.

브라우저를 닫아도 데이터가 유지되며, 같은 도메인 내에서 접근할 수 있습니다. 용량 제한이 있고 문자열만 저장할 수 있다는 특성을 이해하고 사용해야 합니다.

다음 코드를 살펴봅시다.

class LocalStorageHelper {
  // 안전하게 데이터 저장
  static save(key, data) {
    try {
      const serialized = JSON.stringify(data);
      localStorage.setItem(key, serialized);
      return true;
    } catch (error) {
      console.error('저장 실패:', error.message);
      // 용량 초과 시 오래된 데이터 정리
      if (error.name === 'QuotaExceededError') {
        this.clearOldData();
        return this.save(key, data); // 재시도
      }
      return false;
    }
  }

  // 안전하게 데이터 불러오기
  static load(key, defaultValue = null) {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      console.error('불러오기 실패:', error.message);
      return defaultValue;
    }
  }

  // 특정 키 삭제
  static remove(key) {
    localStorage.removeItem(key);
  }

  // 현재 사용량 확인
  static getUsage() {
    let total = 0;
    for (let key in localStorage) {
      if (localStorage.hasOwnProperty(key)) {
        total += localStorage[key].length * 2; // UTF-16
      }
    }
    return (total / 1024).toFixed(2) + ' KB';
  }

  // 데이터 존재 여부 확인
  static exists(key) {
    return localStorage.getItem(key) !== null;
  }
}

김개발 씨는 localStorage를 사용하긴 하는데, 정확히 어떻게 동작하는지 몰랐습니다. 그냥 "데이터 저장하는 거" 정도로만 알고 있었습니다.

박시니어 씨에게 물어보았습니다. "localStorage가 정확히 뭔가요?" 박시니어 씨가 화이트보드 앞으로 갔습니다.

"localStorage를 제대로 이해하려면 몇 가지 특성을 알아야 해요." localStorage란 무엇일까요? 마치 브라우저 안에 있는 작은 사물함과 같습니다.

각 웹사이트마다 자기만의 사물함이 있고, 거기에 물건(데이터)을 넣어둘 수 있습니다. 브라우저를 닫아도 사물함은 그대로 남아있습니다.

다만 다른 웹사이트는 여러분의 사물함을 열어볼 수 없습니다. 같은 도메인만 접근 가능합니다.

localStorage의 핵심 특성을 알아봅시다. 첫째, 문자열만 저장할 수 있습니다.

숫자, 배열, 객체를 직접 저장하면 문자열로 변환됩니다. 객체를 저장하면 "[object Object]"가 되어버립니다.

그래서 JSON.stringify로 직렬화하고, JSON.parse로 역직렬화해야 합니다. 둘째, 용량 제한이 있습니다.

브라우저마다 다르지만 보통 5MB입니다. 게임 데이터 정도는 충분하지만, 이미지나 동영상을 저장하려면 부족합니다.

용량을 초과하면 QuotaExceededError가 발생합니다. 셋째, 동기적으로 동작합니다.

데이터를 읽고 쓰는 동안 다른 코드가 멈춥니다. 작은 데이터는 괜찮지만, 큰 데이터를 자주 읽고 쓰면 성능에 영향을 줄 수 있습니다.

위의 LocalStorageHelper 클래스를 봅시다. save 함수는 try-catch로 감싸 에러를 처리합니다.

용량 초과 에러가 발생하면 오래된 데이터를 정리하고 재시도합니다. load 함수도 마찬가지로 JSON.parse 에러를 처리하고, 문제가 생기면 기본값을 반환합니다.

getUsage 함수는 현재 사용 중인 용량을 확인합니다. 디버깅할 때 유용합니다.

localStorage에 얼마나 많은 데이터가 쌓였는지 모니터링할 수 있습니다. 실무에서 자주 겪는 문제가 있습니다.

시크릿 모드(프라이빗 브라우징)에서는 localStorage가 제한되거나 아예 사용 불가능한 경우가 있습니다. 따라서 localStorage 사용 전에 가용 여부를 확인하는 것이 안전합니다.

또 다른 주의점은 데이터 무결성입니다. 사용자가 개발자 도구로 localStorage를 직접 수정할 수 있습니다.

게임에서 점수를 조작하거나 잠긴 레벨을 열 수 있습니다. 싱글 플레이 게임이면 괜찮지만, 온라인 리더보드가 있다면 서버에서 검증해야 합니다.

sessionStorage와의 차이도 알아두세요. sessionStorage는 브라우저 탭을 닫으면 데이터가 사라집니다.

반면 localStorage는 직접 삭제하기 전까지 영구 보존됩니다. 게임 진행 상황은 당연히 localStorage에 저장해야 합니다.

김개발 씨는 localStorage의 특성을 이해하고 나니 더 안전한 코드를 작성할 수 있게 되었습니다. 예외 처리를 추가하고, 용량 관리 기능도 넣었습니다.

"이제 localStorage 마스터다!"

실전 팁

💡 - 항상 try-catch로 예외를 처리하세요. localStorage가 비활성화되어 있거나 용량이 초과될 수 있습니다.

  • 저장하기 전에 데이터 크기를 확인하고, 필요하면 오래된 데이터를 정리하세요.
  • 민감한 정보(비밀번호, 토큰 등)는 localStorage에 저장하지 마세요. 암호화 없이 평문으로 저장됩니다.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Phaser#GameProgress#LevelSystem#Checkpoint#localStorage#Game

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.