React의 브라우저 기반 멀티플레이어 틱택토 게임
Published: 2024-03-10 22:41:29

**아카이브에 있는 이 게시물은 React에서 틱택토 게임을 만드는 단계를 안내하지만, 사용된 라이브러리 버전이 더 이상 최신 버전이 아니라는 점에 유의하세요.** 특히 이 글에서는 [React SDK](https://www.pubnub.com/docs/sdks/react) 버전 1을 사용하지만 아래에 나열된 모든 단계와 원칙은 오늘날에도 여전히 유효합니다.

틱택토는 전형적인 어린 시절 [게임입니다](https://www.pubnub.com/solutions/gaming/). 이 게임에는 쓸 수 있는 무언가와 함께 쓸 수 있는 무언가만 있으면 됩니다. 하지만 다른 장소에 있는 사람과 함께 놀고 싶다면 어떻게 해야 할까요? 이 경우에는 사용자와 다른 플레이어를 게임에 연결해주는 애플리케이션을 사용해야 합니다.

이 애플리케이션은 실시간 경험을 제공해야 하므로 내 모든 움직임이 다른 플레이어에게 즉시 표시되고 그 반대의 경우도 마찬가지입니다. 애플리케이션이 이러한 경험을 제공하지 않는다면 여러분을 비롯한 많은 사람들이 더 이상 사용하지 않을 것입니다.

그렇다면 개발자는 어떻게 플레이어가 전 세계 어디에 있든 틱택토를 비롯한 모든 게임을 플레이할 수 있는 [연결된 경험을](https://www.pubnub.com/blog/connected-shared-experiences-a-developers-guide/) 제공할 수 있을까요?

실시간 멀티플레이어 게임 콘셉트
-----------------

멀티플레이어 게임에 실시간 인프라를 제공하는 방법에는 여러 가지가 있습니다. [Socket.IO](https://www.pubnub.com/learn/glossary/what-is-socketio/), [SignalR](https://pubnub.com/guides/what-is-signalr/), [WebSockets](https://www.pubnub.com/learn/glossary/what-is-websocket/) 등의 기술 및 오픈 소스 프로토콜을 사용하여 처음부터 자체 인프라를 구축하는 방법을 선택할 수 있습니다.

이 방법은 매력적인 방법처럼 보일 수 있지만 몇 가지 문제에 직면하게 되는데, 그중 하나가 확장성 문제입니다. 100명의 사용자를 처리하는 것은 어렵지 않지만 100,000명 이상의 사용자를 어떻게 처리할 수 있을까요? 인프라 문제 외에도 게임 유지 관리에 대해서도 걱정해야 합니다.

결국 중요한 것은 게임 플레이어에게 훌륭한 경험을 제공하는 것입니다. 그렇다면 인프라 문제를 어떻게 해결할 수 있을까요? 바로 이때 PubNub이 등장합니다.

PubNub은 글로벌 [데이터 스트림 네트워크를](https://pubnub.com/products/pubnub-platform/) 통해 모든 애플리케이션을 구동할 수 있는 실시간 인프라를 제공합니다. 가장 인기 있는 프로그래밍 언어를 포함한 [70개 이상의 SDK를](https://pubnub.com/docs/) 통해 PubNub은 모든 디바이스로 메시지를 **100ms 이내에** 간편하게 송수신할 수 있습니다. 안전하고 확장 가능하며 안정적이므로 자체 인프라를 만들고 유지 관리하는 것에 대해 걱정할 필요가 없습니다.

PubNub을 사용하여 멀티플레이어 게임을 개발하는 것이 얼마나 쉬운지 보여드리기 위해 [PubNub React SDK를](https://pubnub.com/docs/sdks/javascript/react) 사용하여 간단한 React 틱택토 게임을 만들어 보겠습니다. 이 게임에서는 두 명의 플레이어가 고유한 게임 채널에 연결하여 서로 대결을 펼칩니다. 플레이어의 모든 움직임은 채널에 게시되어 실시간으로 다른 플레이어의 보드가 업데이트됩니다.

앱 개요
----

개발이 완료되면 앱의 모습은 다음과 같습니다.

![Screen shot of the React Tic Tac Toe Game](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpy/5d2874d8b7c577a9ce9a5acc6ade755f/React-tic-tac-toe-overview.png "Screen shot of the React Tic Tac Toe Game")플레이어는 먼저 로비에서 채널을 만들거나 채널에 참여할 수 있습니다. 플레이어가 채널을 생성하면 다른 플레이어와 공유할 수 있는 _방 ID를_ 받게 됩니다. 채널을 생성한 플레이어는 _플레이어 X가_ 되어 게임이 시작되면 첫 번째로 움직입니다.

[![Create a room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif "Create a room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jpz/d6a1b08ddc0924624f35e779c0c0059a/create-room.gif)

받은 _방 ID로_ 채널에 참여한 플레이어는 _플레이어 O가_ 됩니다. 플레이어는 채널에 다른 사람이 한 명 있을 때만 채널에 참여할 수 있습니다. 한 명 이상이면 해당 채널에서 게임이 진행 중이며 플레이어는 참여할 수 없습니다. 채널에 두 명의 플레이어가 있으면 게임이 시작됩니다.

[![Join the room channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif "Join the room channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq0/96a0ccd6b75f19b211747f353b27bd39/join-channel.gif)

게임이 끝나면 승자의 점수가 1점씩 올라갑니다. 게임이 동점으로 끝나면 두 플레이어 모두 점수를 얻지 못합니다. 새 라운드를 시작하거나 게임을 종료할지 묻는 모달이 _플레이어 X에게_ 표시됩니다. _플레이어 X가_ 게임을 계속하면 새 라운드를 위해 보드가 초기화됩니다. 그렇지 않으면 게임이 종료되고 두 플레이어는 로비로 돌아갑니다.

[![Exit to lobby](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif " Exit to lobby")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq1/f475cc1aeed8a4184348569634ed968d/exit-to-lobby-1.gif)

로비 설정
-----

로비를 설정하기 전에 무료 PubNub 계정에 가입하여 [PubNub 관리자 대시보드에서](https://admin.pubnub.com) 무료 Pub/Sub API 키를 받습니다.

키를 받으면 **App.js의** 생성자에 키를 삽입합니다.

```js
// App.js
import React, { Component } from 'react';
import Game from './Game';
import Board from './Board';
import PubNubReact from 'pubnub-react';
import Swal from "sweetalert2";
import shortid  from 'shortid';
import './Game.css';

class App extends Component {
  constructor(props) {
    super(props);
    // REPLACE with your keys
    this.pubnub = new PubNubReact({
      publishKey: "YOUR_PUBLISH_KEY_HERE",
      subscribeKey: "YOUR_SUBSCRIBE_KEY_HERE"
    });

    this.state = {
      piece: '', // X or O
      isPlaying: false, // Set to true when 2 players are in a channel
      isRoomCreator: false,
      isDisabled: false,
      myTurn: false,
    };

    this.lobbyChannel = null; // Lobby channel
    this.gameChannel = null; // Game channel
    this.roomId = null; // Unique id when player creates a room
    this.pubnub.init(this); // Initialize PubNub
  }

  render() {
    return ();
    }
  }

  export default App;
```

또한 생성자에서 상태 객체와 변수가 초기화됩니다. 파일 전체에 걸쳐 객체와 변수가 등장할 때 살펴볼 것입니다. 마지막으로 생성자 마지막에 PubNub를 초기화했습니다.

**렌더** 메소드 내부와 **반환** 문 안에 로비 컴포넌트에 대한 마크업을 추가합니다.

```js
return (
    <div>
      <div className="title">
        <p> React Tic Tac Toe </p>
      </div>

      {
        !this.state.isPlaying &&
        <div className="game">
          <div className="board">
            <Board
                squares={0}
                onClick={index => null}
              />

            <div className="button-container">
              <button
                className="create-button "
                disabled={this.state.isDisabled}
                onClick={(e) => this.onPressCreate()}
                > Create
              </button>
              <button
                className="join-button"
                onClick={(e) => this.onPressJoin()}
                > Join
              </button>
            </div>

          </div>
        </div>
      }

      {
        this.state.isPlaying &&
        <Game
          pubnub={this.pubnub}
          gameChannel={this.gameChannel}
          piece={this.state.piece}
          isRoomCreator={this.state.isRoomCreator}
          myTurn={this.state.myTurn}
          xUsername={this.state.xUsername}
          oUsername={this.state.oUsername}
          endGame={this.endGame}
        />
      }
    </div>
);
```

로비 컴포넌트는 제목, 빈 틱택 토 보드(플레이어가 사각형을 누르면 아무 일도 일어나지 않음), '만들기_'_ 및 '참여_'_ 버튼으로 구성됩니다. 이 구성 요소는 상태 값이 _Playing인_ 경우에만 표시됩니다. 참으로 설정되어 있으면 게임이 시작된 것이고 구성 요소는 튜토리얼의 두 번째 부분에서 살펴볼 게임 구성 요소로 변경됩니다.

보드 컴포넌트는 로비 컴포넌트의 일부이기도 합니다. [보드](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Board.js) 컴포넌트 안에는 [스퀘어](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Square.js) 컴포넌트가 있습니다. 로비 및 게임 구성 요소에 집중하기 위해 이 두 구성 요소에 대해서는 자세히 설명하지 않겠습니다.

플레이어가 '만들기' 버튼을 누르면 버튼이 비활성화되어 플레이어가 여러 채널을 만들 수 없습니다. '가입' 버튼은 플레이어가 채널에 가입하기로 결정할 경우를 대비하여 비활성화되지 않습니다. '만들기' 버튼을 누르면 **onPressCreate()** 메서드가 호출됩니다.

### 채널 만들기

**onPressCreate(** )에서 가장 먼저 하는 일은 5자로 잘린 임의의 문자열 아이디를 생성하는 것입니다. 이를 위해 [**shortid()**](https://www.npmjs.com/package/shortid)를 사용합니다. 플레이어가 구독하는 고유한 로비 채널이 될 '_tictactoelobby-_'에 문자열을 추가합니다.

```js
// Create a room channel
onPressCreate = (e) => {
  // Create a random name for the channel
  this.roomId = shortid.generate().substring(0,5);
  this.lobbyChannel = 'tictactoelobby--' + this.roomId; // Lobby channel name

  this.pubnub.subscribe({
    channels: [this.lobbyChannel],
    withPresence: true // Checks the number of people in the channel
  });
}
```

특정 채널에 두 명 이상의 플레이어가 참여하는 것을 방지하기 위해 [프레즌스를](https://pubnub.com/docs/sdks/javascript/react) 사용합니다. 나중에 채널의 점유를 확인하는 로직을 살펴보겠습니다.

플레이어가 로비 채널에 가입하면 다른 플레이어가 해당 채널에 참여할 수 있도록 방 ID가 포함된 모달이 표시됩니다.

[![Share the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png "Share the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq2/2e171d056766c2d974cb5b9f3001b5a3/share-roomid-1.png)

이 모달과 이 앱에서 사용되는 모든 모달은 JavaScript의 기본 alert() 팝업 상자를 대체하기 위해 [**SweetAlert2에**](https://sweetalert2.github.io/#download) 의해 생성됩니다.

```js
// Inside of onPressCreate()
// Modal
Swal.fire({
  position: 'top',
  allowOutsideClick: false,
  title: 'Share this room ID with your friend',
  text: this.roomId,
  width: 275,
  padding: '0.7em',
  // Custom CSS to change the size of the modal
  customClass: {
      heightAuto: false,
      title: 'title-class',
      popup: 'popup-class',
      confirmButton: 'button-class'
  }
})
```

**onPressCreate()**가 끝나면 앱의 새 상태를 반영하도록 상태 값을 변경합니다.

```js
this.setState({
  piece: 'X',
  isRoomCreator: true,
  isDisabled: true, // Disable the 'Create' button
  myTurn: true, // Player X makes the 1st move
});
```

플레이어가 방을 생성하면 다른 플레이어가 해당 방에 참여할 때까지 기다려야 합니다. 방에 참가하는 로직을 살펴봅시다.

### 채널에 참여하기

플레이어가 '참가' 버튼을 누르면 **onPressJoin(** ) 호출이 호출됩니다. 플레이어에게 입력 필드에 _방 ID를_ 입력하라는 모달이 표시됩니다.

[![Enter the room id](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png "Enter the room id")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq3/97e46c029a2583ac018ab14f44ce8572/enter-roomid.png)

플레이어가 _방_ ID를 입력하고 '확인' 버튼을 누르면 **joinRoom(value** )가 호출되며, 여기서 **값은** _방 ID입니다_. 입력 필드가 비어 있거나 플레이어가 '취소' 버튼을 누르면 이 메서드는 호출되지 않습니다.

```js
// The 'Join' button was pressed
onPressJoin = (e) => {
  Swal.fire({
    position: 'top',
    input: 'text',
    allowOutsideClick: false,
    inputPlaceholder: 'Enter the room id',
    showCancelButton: true,
    confirmButtonColor: 'rgb(208,33,41)',
    confirmButtonText: 'OK',
    width: 275,
    padding: '0.7em',
    customClass: {
      heightAuto: false,
      popup: 'popup-class',
      confirmButton: 'join-button-class',
      cancelButton: 'join-button-class'
    }
  }).then((result) => {
    // Check if the user typed a value in the input field
    if(result.value){
      this.joinRoom(result.value);
    }
  })
}
```

**joinRoom(** )에서 가장 먼저 하는 작업은 **onPressCreate(**)에서 한 것과 유사하게 '_tictactoelobby-_'에 **값을** 추가하는 것입니다.

```js
// Join a room channel
joinRoom = (value) => {
  this.roomId = value;
  this.lobbyChannel = 'tictactoelobby--' + this.roomId;
}
```

플레이어가 로비 채널을 구독하기 전에 [hereNow()](https://pubnub.com/docs/sdks/javascript/react)를 사용하여 채널의 총 점유율을 확인해야 합니다. 총 점유율이 2보다 작으면 플레이어는 로비 채널에 성공적으로 구독할 수 있습니다.

```js
// Check the number of people in the channel
this.pubnub.hereNow({
  channels: [this.lobbyChannel],
}).then((response) => {
    if(response.totalOccupancy < 2){
      this.pubnub.subscribe({
        channels: [this.lobbyChannel],
        withPresence: true
      });

      this.setState({
        piece: 'O', // Player O
      });

      this.pubnub.publish({
        message: {
          notRoomCreator: true,
        },
        channel: this.lobbyChannel
      });
    }
}).catch((error) => {
  console.log(error);
});
```

플레이어가 로비 채널을 구독하면 _조각의_ 상태 값이 'O'로 변경되고 해당 로비 채널에 메시지가 게시됩니다. 이 메시지는 다른 플레이어가 채널에 가입했음을 _플레이어 X에게_ 알립니다. 곧 설명할 **componentDidUpdate(**)에서 메시지 리스너를 설정합니다.

총 점유 인원이 2보다 크면 게임이 진행 중이며 채널에 참여하려는 플레이어의 액세스가 거부됩니다. 다음 코드는 **hereNow(**)의 if 문 아래에 있습니다.

```js
// Below the if statement in hereNow()
else{
  // Game in progress
  Swal.fire({
    position: 'top',
    allowOutsideClick: false,
    title: 'Error',
    text: 'Game in progress. Try another room.',
    width: 275,
    padding: '0.7em',
    customClass: {
        heightAuto: false,
        title: 'title-class',
        popup: 'popup-class',
        confirmButton: 'button-class'
    }
  })
}
```

이제 **componentDidUpdate(**)를 살펴봅시다.

### 게임 시작

**componentDidUpdate(**)에서는 플레이어가 채널에 연결되어 있는지, 즉 _this.lobbyChannel이_ **null이** 아닌지 확인합니다. **null이** 아니라면 채널에 도착하는 모든 메시지를 수신하는 리스너를 설정합니다.

```js
componentDidUpdate() {
  // Check that the player is connected to a channel
  if(this.lobbyChannel != null){
    this.pubnub.getMessage(this.lobbyChannel, (msg) => {
      // Start the game once an opponent joins the channel
      if(msg.message.notRoomCreator){
        // Create a different channel for the game
        this.gameChannel = 'tictactoegame--' + this.roomId;

        this.pubnub.subscribe({
          channels: [this.gameChannel]
        });
      }
    });
  }
}
```

도착한 메시지가 채널에 참여한 플레이어가 게시한 _msg.message.notRoomCreator인지_ 확인합니다. 그렇다면 문자열에 _방 아이디를_ 추가하여 '_tictactoegame-_'이라는 새 채널을 만듭니다. 게임 채널은 보드를 업데이트할 플레이어의 모든 움직임을 게시하는 데 사용됩니다.

마지막으로 게임 채널에 가입하면 _isPlaying의_ 상태 값이 true로 설정됩니다. 이렇게 하면 로비 컴포넌트가 게임 컴포넌트로 대체됩니다.

```js
 this.setState({
   isPlaying: true
 });

 // Close the modals if they are opened
 Swal.close();
}
```

게임 컴포넌트가 표시되면 _Swal.close()_를 수행하여 로비 컴포넌트에서 열려있는 모든 모달을 닫으려고 합니다.

이제 두 명의 플레이어가 고유한 게임 채널에 연결되었으므로 틱택토 게임을 시작할 수 있습니다! 다음 섹션에서는 게임 컴포넌트의 UI와 로직을 구현해 보겠습니다.

게임 기능 빌드하기
----------

**Game.js에서** 가장 먼저 할 일은 **기본 생성자를** 설정하는 것입니다:

```js
// Game.js
import React from 'react';
import Board from './Board';
import Swal from "sweetalert2";

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(''), // 3x3 board
      xScore: 0,
      oScore: 0,
      whosTurn: this.props.myTurn // Player X goes first
    };

    this.turn = 'X';
    this.gameOver = false;
    this.counter = 0; // Game ends in a tie when counter is 9
  }

  render() {
    return ();
  }
 }
export default Game;
```

상태 객체의 경우, 보드에서 플레이어의 위치를 저장하는 데 사용되는 배열 _사각형_ 프로퍼티를 초기화합니다. 이에 대해서는 아래에서 더 자세히 설명하겠습니다. 또한 플레이어 점수를 0으로 설정하고 _누구의 턴_ 값을 _myTurn으로_ 설정하여 _플레이어 X의_ 경우 **참으로**, _플레이어 O의_ 경우 **거짓으로** 초기화합니다.

_턴과_ _카운터_ 변수의 값은 게임이 진행되는 동안 변경됩니다. 게임이 끝나면 _게임오버는_ **참으로** 설정됩니다.

### UI 추가

다음으로 **렌더** 메서드 내부의 게임 컴포넌트에 대한 마크업을 설정해 보겠습니다.

```js
render() {
  let status;
  // Change to current player's turn
  status = `${this.state.whosTurn ? "Your turn" : "Opponent's turn"}`;

  return (
    <div className="game">
      <div className="board">
        <Board
            squares={this.state.squares}
            onClick={index => this.onMakeMove(index)}
          />
          <p className="status-info">{status}</p>
      </div>

      <div className="scores-container">
        <div>
          <p>Player X: {this.state.xScore} </p>
        </div>

        <div>
          <p>Player O: {this.state.oScore} </p>
        </div>
      </div>
    </div>
  );
}
```

플레이어가 움직일 차례인지 아니면 다른 플레이어의 차례인지 알 수 있도록 UI에 _상태_ 값을 표시합니다. _state의_ 부울 값은 이동이 이루어질 때마다 업데이트됩니다. 나머지 UI는 보드 구성 요소와 플레이어의 점수로 구성됩니다.

### 로직 추가하기

플레이어가 보드에서 이동을 하면 **onMakeMove(****index** )가 호출되며, 여기서 **index는** 보드에서 말이 놓인 위치입니다. 보드에는 3개의 행과 3개의 열이 있으므로 총 9개의 사각형이 있습니다. 각 사각형은 0으로 시작하여 8로 끝나는 고유한 **인덱스** 값을 갖습니다.

```js
onMakeMove = (index) =>{
  const squares = this.state.squares;

  // Check if the square is empty and if it's the player's turn to make a move
  if(!squares[index] && (this.turn === this.props.piece)){
    squares[index] = this.props.piece;

    this.setState({
      squares: squares,
      whosTurn: !this.state.whosTurn
    });

    // Other player's turn to make a move
    this.turn = (this.turn === 'X') ? 'O' : 'X';

    // Publish move to the channel
    this.props.pubnub.publish({
      message: {
        index: index,
        piece: this.props.piece,
        turn: this.turn
      },
      channel: this.props.gameChannel
    });

    // Check if there is a winner
    this.checkForWinner(squares)
  }
}
```

배열 _사각형의_ 상태를 얻은 후 조건문을 사용하여 플레이어가 터치한 사각형이 비어 있는지, 그리고 플레이어가 움직일 차례인지 확인합니다. 조건 중 하나 또는 두 가지 조건이 모두 충족되지 않으면 플레이어의 말이 사각형에 놓이지 않습니다. 그렇지 않으면 플레이어의 말이 말이 놓인 인덱스의 배열 _사각형에_ 추가됩니다.

예를 들어, _플레이어 X가_ 행 0, 열 2에서 이동을 하고 조건문이 참이면 _squares\[2\]_ 의 값은 "X"가 됩니다.[![Example with the squares array](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png "Example with the squares array")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq4/a8487d17f078fae4ccfa874baac16013/squares-example.png)다음으로, 게임의 새로운 상태를 반영하도록 상태가 변경되고 상대 플레이어가 움직일 수 있도록 _턴이_ 업데이트됩니다. 다른 플레이어의 보드가 현재 데이터로 업데이트될 수 있도록 데이터를 게임 채널에 게시합니다. 이 모든 과정은 실시간으로 이루어지므로 두 플레이어는 유효한 이동이 이루어지는 즉시 자신의 보드가 업데이트되는 것을 볼 수 있습니다. 이 메서드에서 마지막으로 해야 할 일은 **체크포위너(squares)** 를 호출하여 승자가 있는지 확인하는 것입니다.

그 전에 게임 채널에 도착하는 새 메시지에 대한 리스너를 설정하는 **componentDidMount****(** )를 살펴봅시다.

```js
componentDidMount(){
  this.props.pubnub.getMessage(this.props.gameChannel, (msg) => {
    // Update other player's board
    if(msg.message.turn === this.props.piece){
      this.publishMove(msg.message.index, msg.message.piece);
    }
  });
}
```

두 플레이어가 동일한 게임 채널에 연결되어 있으므로 두 플레이어 모두 이 메시지를 수신하게 됩니다. 여기서 **인덱스는** 말이 놓인 위치, **피스는** 이동을 한 플레이어의 말입니다. 이 메서드는 현재 이동으로 보드를 업데이트하고 승자가 있는지 확인합니다. 현재 이동을 한 플레이어가 이 과정을 다시 반복하지 않도록 _if_ 문은 플레이어의 말이 _턴의_ 값과 일치하는지 확인합니다. 일치하면 보드가 업데이트됩니다.

```js
// Opponent's move is published to the board
publishMove = (index, piece) => {
  const squares = this.state.squares;

  squares[index] = piece;
  this.turn = (squares[index] === 'X')? 'O' : 'X';

  this.setState({
    squares: squares,
    whosTurn: !this.state.whosTurn
  });

  this.checkForWinner(squares)
}
```

보드를 업데이트하는 로직은 **onMakeMove()**와 동일합니다. 이제 **checkForWinner(**)를 살펴봅시다.

```js
checkForWinner = (squares) => {
  // Possible winning combinations
  const possibleCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  // Iterate every combination to see if there is a match
  for (let i = 0; i < possibleCombinations.length; i += 1) {
    const [a, b, c] = possibleCombinations[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      this.announceWinner(squares[a]);
      return;
    }
  }
}
```

모든 승리 조합은 가능한 모든 배열이 게임에서 승리할 수 있는 조합인 이중 배열 _possibleCombinations에_ 있습니다. _possibleCombinations의_ 모든 배열은 배열 _사각형과_ 비교하여 확인됩니다. 일치하는 조합이 있으면 승자가 있는 것입니다. 이를 더 명확하게 이해하기 위해 예제를 살펴보겠습니다.

플레이어 X가 행 2 열 0에서 승리한 수를 두었다고 가정해 봅시다. 해당 위치의 _인덱스는_ 6입니다. 이제 바둑판은 다음과 같이 보입니다:

[![Example of a winning move](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png "Example of a winning move")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq5/77dcc8ab49b7e7b4e41ac6f1365a2df9/winning-move-example-1.png)_플레이어 X의_ 승리 조합은 \[2,4,6\]입니다. 배열 _사각형은_ 다음과 같이 업데이트됩니다: \["O", "", "X", "O", "X", "", "X", "", ""\].

_for_ 루프에서 _\[a,b,c\]_ 의 값이 \[2,4,6\]인 경우, \[2,4,6\]이 _모두_ 같은 값인 _X를_ 가지므로 _for_ 루프의 _if_ 문은 true가 됩니다. 우승자의 점수를 업데이트해야 하므로 우승자를 시상하기 위해**announceWinner(** )가 호출됩니다.

게임이 동점으로 끝나면 해당 라운드의 승자는 없습니다. 동점 게임을 확인하기 위해 보드에서 이동이 이루어질 때마다 1씩 증가하는 카운터를 사용합니다.

```js
// Below the for loop in checkForWinner()
// Check if the game ends in a draw
this.counter++;
// The board is filled up and there is no winner
if(this.counter === 9){
  this.gameOver = true;
  this.newRound(null);
}
```

카운터가 9에 도달하면 플레이어가 보드의 마지막 칸에서 승리한 수를 두지 않았기 때문에 게임이 무승부로 끝납니다. 이 경우 승자가 없으므로 **null** 인수를 사용하여 **newRound()** 메서드가 호출됩니다.

이 메서드로 이동하기 전에**announceWinner(**)로 돌아가 보겠습니다.

```js
// Update score for the winner
announceWinner = (winner) => {
  let pieces = {
    'X': this.state.xScore,
    'O': this.state.oScore
  }

  if(winner === 'X'){
    pieces['X'] += 1;
    this.setState({
      xScore: pieces['X']
    });
  }
  else{
    pieces['O'] += 1;
    this.setState({
      oScore: pieces['O']
    });
  }
  // End the game once there is a winner
  this.gameOver = true;
  this.newRound(winner);
}
```

이 메서드의 매개변수는 게임에서 승리한 플레이어인 _winner입니다_. 승자가 'X' 또는 'O'인지 확인하고 승자의 점수를 1점씩 증가시킵니다. 게임이 종료되었으므로 _게임 오버_ 변수가 true로 설정되고 **newRound()** 메서드가 호출됩니다.

### 새 라운드 시작

_플레이어 X는_ 다른 라운드를 플레이하거나 게임을 종료하고 로비로 돌아갈 수 있습니다.

[![Endgame modal for Player X](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png "Endgame modal for Player X")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq6/f7b9623c4b0b628d51d8271f249747ba/playerx-endgame-modal.png)

다른 플레이어는 _플레이어 X가_ 어떻게 할지 결정할 때까지 기다리라고 말합니다.

[![Endgame modal for Player O](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png "Endgame modal for Player O")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq7/d96413b49ec1b85838aca013cfba174b/playero-endgame-modal.png)

_플레이어 X가_ 무엇을 해야 할지 결정하면 게임 채널에 메시지가 게시되어 다른 플레이어에게 알립니다. 그러면 UI가 업데이트됩니다.

```js
newRound = (winner) => {
  // Announce the winner or announce a tie game
  let title = (winner === null) ? 'Tie game!' : `Player ${winner} won!`;
  // Show this to Player O
  if((this.props.isRoomCreator === false) && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Waiting for a new round...',
      confirmButtonColor: 'rgb(208,33,41)',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
      } ,
    });
    this.turn = 'X'; // Set turn to X so Player O can't make a move
  }

  // Show this to Player X
  else if(this.props.isRoomCreator && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Continue Playing?',
      showCancelButton: true,
      confirmButtonColor: 'rgb(208,33,41)',
      cancelButtonColor: '#aaa',
      cancelButtonText: 'Nope',
      confirmButtonText: 'Yea!',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
          cancelButton: 'button-class'
      } ,
    }).then((result) => {
      // Start a new round
      if (result.value) {
        this.props.pubnub.publish({
          message: {
            reset: true
          },
          channel: this.props.gameChannel
        });
      }

      else{
        // End the game
        this.props.pubnub.publish({
          message: {
            endGame: true
          },
          channel: this.props.gameChannel
        });
      }
    })
  }
 }
```

메시지가 _리셋되면_ 플레이어의 점수를 제외한 모든 상태 값과 변수가 초기값으로 재설정됩니다. 아직 열려 있는 모달은 모두 닫히고 두 플레이어의 새 라운드가 시작됩니다.

_endGame_ 메시지의 경우 모든 모달이 닫히고 **endGame()** 메서드가 호출됩니다. 이 메서드는 **App.js에** 있습니다.

```js
// Reset everything
endGame = () => {
  this.setState({
    piece: '',
    isPlaying: false,
    isRoomCreator: false,
    isDisabled: false,
    myTurn: false,
  });

  this.lobbyChannel = null;
  this.gameChannel = null;
  this.roomId = null;

  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

모든 상태 값과 변수가 초기값으로 재설정됩니다. 채널 이름은 플레이어가 방을 생성할 때마다 새 이름이 생성되므로 null로 재설정됩니다. 채널 이름이 더 이상 유용하지 않으므로 플레이어는 로비와 게임 채널 모두에서 구독을 취소합니다. _isPlaying의_ 값은 거짓으로 재설정되므로 게임 컴포넌트가 로비 컴포넌트로 대체됩니다.

**App.js에** 포함할 마지막 메서드는 두 채널에서 플레이어의 구독을 취소하는 **componentWillUnmount(**)입니다.

```js
componentWillUnmount() {
  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}
```

이제 게임이 작동하기 위해 필요한 모든 작업이 완료되었습니다! 게임의 CSS 파일은 [리포지토리에서](https://github.com/PubNubDevelopers/react-tictactoe/blob/master/src/Game.css) 얻을 수 있습니다. 이제 게임을 실행해 보겠습니다.

게임 실행
-----

게임을 실행하기 전에 몇 가지 작은 단계를 수행해야 합니다. 먼저, 채널에 있는 사람의 수를 얻기 위해 [프레즌스 기능을](https://pubnub.com/docs/sdks/javascript/react) 활성화해야 합니다(로비 채널을 구독할 때 _위드프레즌스를_ 사용했습니다). [PubNub 관리자 대시보드로](https://admin.pubnub.com/#/login) 이동하여 애플리케이션을 클릭합니다. **키셋을** 클릭하고 **애플리케이션 추가** 기능까지 아래로 스크롤합니다. **프레즌스** 스위치를 **켜기로** 전환합니다. 기본값은 그대로 유지합니다.

[![Enable presence in PubNub Admin Dashboard](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png "Enable presence in PubNub Admin Dashboard")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq8/fb24ea6026b76234f79ee65d2426ac71/enable-presence-1.png)

앱에 사용되는 세 가지 종속성을 설치하고 앱을 실행하려면 앱의 루트 디렉터리에 있는 **의존성.sh** 스크립트를 실행하면 됩니다.

```js
# dependencies.sh
npm install --save pubnub pubnub-react
npm install --save shortid
npm install --save sweetalert2

npm start
```

터미널에서 앱의 루트 디렉토리로 이동하여 다음 명령을 입력하여 스크립트를 실행할 수 있도록 합니다:

```js
chmod +x dependencies.sh
```

이 명령으로 스크립트를 실행합니다:

```js
./dependencies.sh
```

앱이 http://localhost:3000 에서 열리고 로비 구성 요소가 표시됩니다.![Run the React app locally](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jq9/fb4f1a520b9d68e1c283a61d5fa04653/run-the-app.png "Run the React app locally")다른 탭 또는 가급적 창을 열고 http://localhost:3000 을 복사하여 붙여넣습니다. 한 창에서 '만들기' 버튼을 클릭하여 채널을 만듭니다. _방 아이디를_ 표시하는 모달이 나타납니다. 해당 아이디를 복사하여 붙여넣습니다. 다른 창으로 이동하여 '가입' 버튼을 클릭합니다. 모달이 나타나면 입력 필드에 _방 아이디를_ 입력하고 '확인' 버튼을 누릅니다.

[![Create and join the channel](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png "Create and join the channel")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqa/01343a31a36c16277b86d8a1ed92570b/create-join-lobby.png)

플레이어가 연결되면 게임이 시작됩니다. 채널을 만들 때 사용한 창에서 첫 번째 이동이 이루어집니다. 보드의 아무 칸이나 누르면 양쪽 창에 실시간으로 **X** 조각이 보드에 표시되는 것을 확인할 수 있습니다. 같은 보드의 다른 사각형을 누르려고 하면 더 이상 움직일 차례가 아니므로 아무 일도 일어나지 않습니다. 다른 창에서 보드의 아무 칸이나 누르면 조각 **_O가_** 그 칸에 놓입니다.

[![Place the piece on the board](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png "Place the piece on the board")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqb/454174588fc9acf9c2f086c3e9c53a19/place-piece-on-board.png)

승자가 나오거나 동점이 될 때까지 게임을 계속합니다. 그러면 라운드의 승자를 알리는 모달이 표시되거나 게임이 동점으로 끝났음을 알리는 모달이 표시됩니다. 같은 모달에서 _플레이어 X는_ 게임을 계속할지 아니면 게임을 종료할지 결정해야 합니다. _플레이어 O를_ 위한 모달은 새 라운드를 기다리라고 알려줍니다.

[![End of game modals](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png "End of game modals")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1jqc/71b832403fecb058be460282895e3f10/end-of-game.png)

_플레이어 X가_ 게임을 계속하면 점수를 제외한 모든 것이 초기화됩니다. 그렇지 않으면 두 플레이어는 로비로 돌아가 새로운 채널을 만들거나 참여할 수 있습니다. 아래에서 게임 데모를 확인하세요:

**이 게시물 내용에 대한 제안이나 질문이 있으신가요? [devrel@pubnub.com](mailto:devrel@pubnub.com) 으로 문의해 주세요.**

#### 콘텐츠

[실시간 멀티플레이어 게임 개념앱](#h-0)[개요로비](#h-2)[](#h-1)[설정하기채널](#h-3)[만들기채널에](#h-4)[가입하기게임](#h-5)[시작하기게임 기능](#h-6)[구축하기UI](#h-7)[추가하기로직](#h-8)[추가하기새 라운드](#h-9)[시작하기게임](#h-10)[실행하기](#h-9)

펍넙이 어떤 도움을 줄 수 있나요?
===================

이 문서는 원래 [PubNub.com에](https://www.pubnub.com/blog/browser-based-multiplayer-tic-tac-toe-game-in-react/) 게시되었습니다.

유니티 플랫폼은 개발자가 웹 앱, 모바일 앱, IoT 디바이스를 위한 실시간 인터랙티브를 제작, 제공, 관리할 수 있도록 지원합니다.

저희 플랫폼의 기반은 업계에서 가장 크고 확장성이 뛰어난 실시간 에지 메시징 네트워크입니다. 전 세계 15개 이상의 PoP가 월간 8억 명의 활성 사용자를 지원하고 99.999%의 안정성을 제공하므로 중단, 동시 접속자 수 제한 또는 트래픽 폭증으로 인한 지연 문제를 걱정할 필요가 없습니다.

PubNub 체험하기
-----------

[라이브 투어를](https://www.pubnub.com/tour/introduction/) 통해 5분 이내에 모든 PubNub 기반 앱의 필수 개념을 이해하세요.

설정하기
----

PubNub [계정에](https://admin.pubnub.com/signup/) 가입하여 PubNub 키에 무료로 즉시 액세스하세요.

시작하기
----

사용 사례나 [SDK에](https://www.pubnub.com/docs) 관계없이 [PubNub 문서를](https://www.pubnub.com/docs) 통해 바로 시작하고 실행할 수 있습니다.