React

#4 tictectoe 게임 만들기

Sila 2022. 4. 17. 01:46

지금까지 한 걸 중간 정리하는 느낌으로 리액트 공식 사이트 자습서에 나온 tictactoe 게임을 만들어보자.

 

단계별로 이게 뭘 하는건지, 어떤 원리가 사용되었는지, 데이터의 흐름은 어떻게 되는지에

 

주의하면서 따라가면 될 것 같다.

 

전체 컴포넌트는 3가지 레벨으로 나눌 수 있는데, <Game/> 컴포넌트 안에 게임판인 </Board>,

 

그 </Board> 안에 각각의 정사각형을 이루는 <Squares/> 컴포넌트가 있다.

 

우선 html을 셋업해주자.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> 
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> 
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
    <!-- 이 안에 html element 생성-->

    <script type="text/babel">
        class Game extends React.Component {
            render() {
                return(
                    <div className='game'>
                    hello
                    </div>
                )
            }
        }

        ReactDOM.render(
            <Game/>,
            document.querySelector('#root')
        )
    </script>
</body>
</html>

hello가 잘 나오는지 확인되었으면

 

<Game/> 컴포넌트의 <div> 내부 값 hello를 <Board/> 로 바꿔주고

 

Board 컴포넌트를 다음과 같이 작성한다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> 
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
    <!-- 이 안에 html element 생성-->

    <script type="text/babel">

        class Board extends React.Component {
            render() {
                const status = 'Next player : X'

                return (
                    <div>
                        <div className='status'>{status}</div>
                        <div className='board-row'>
                            {0}
                            {1}
                            {2}
                        </div>
                        <div className='board-row'>
                            {3}
                            {4}
                            {5}
                        </div>
                        <div className='board-row'>
                            {6}
                            {7}
                            {8}
                        </div>
                    </div>
                )
            }
        }

        class Game extends React.Component {
            render() {
                return(
                    <div className='game'>
                        <div id = 'game-board'>
                            <Board/>
                        </div>
                    </div>
                )
            }
        }

        ReactDOM.render(
            <Game/>,
            document.querySelector('#root')
        )
    </script>
</body>
</html>

 

이렇게 <Board/> 컴포넌트를 넣고 불러오면 3x3 의 게임판이 만들어진 것이다.

 

이 <Board/>는 다시 3x3 등분 <Square/>이 될 예정인데,

 

이는 함수를 하나 선언해 그 함수를 호출하는 방식으로 이루어진다.

 

다음과 같이 Board 컴포넌트 안에 함수를 선언해주자.

 

class Board extends React.Component {
   renderSquare(i) {
      return <Squares/>
   }

// 중략
}

 

이제 이 함수로 {0}, {1}...을 대체하는데, 각각의 <Square/> 컴포넌트를 구분하기 위해

 

각 컴포넌트의 매개변수를 다르게 줄 것이다.

 

이를 코드로 구현하면 다음과 같이 된다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> 
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
    <div id="root"></div>
    <!-- 이 안에 html element 생성-->

    <script type="text/babel">

        class Board extends React.Component {

            renderSquare(i) {
                return <Square/>
            }

            render() {
                const status = 'Next player : X'

                return (
                    <div>
                        <div className='status'>{status}</div>
                        <div className='board-row'>
                            {this.renderSquare(0)}
                            {this.renderSquare(1)}
                            {this.renderSquare(2)}
                        </div>
                        <div className='board-row'>
                            {this.renderSquare(3)}
                            {this.renderSquare(4)}
                            {this.renderSquare(5)}
                        </div>
                        <div className='board-row'>
                            {this.renderSquare(6)}
                            {this.renderSquare(7)}
                            {this.renderSquare(8)}
                        </div>
                    </div>
                )
            }
        }

        class Game extends React.Component {
            render() {
                return(
                    <div className='game'>
                        <div id = 'game-board'>
                            <Board/>
                        </div>
                    </div>
                )
            }
        }

        ReactDOM.render(
            <Game/>,
            document.querySelector('#root')
        )
    </script>
</body>
</html>

 

현재 <Square/> 컴포넌트가 정의되지 않았으므로 에러가 발생할 것이다.

 

<Board/> 컴포넌트 위에 <Squares/> 컴포넌트를 정의해주자.

 

class Square extends React.Component {
   render() {
      return (
         <button className='square'>
            { } 
         </button>
      )
   }
}

 

아무 값도 갖지 않는 버튼이 생성되었다면 문제 없이 작동하는 것이다.

 

1. 변수값 전달

 

이제 <Board/>에서 변수값을 <Square/>로 전달해주자.

 

renderSquare 함수를 다음과 같이 수정한다.

 

renderSquare(i) {
   return <Square value={i}>
}

 

이렇게 <Board/>에서 <Squares/>로 보낸 데이터를 받아주는 과정이 필요하다.

 

<Squares/> 컴포넌트를 다음과 같이 수정하자.

 

class Square extends React.Component {
   render() {
      return (
         <button className='square'>
            {this.props.value}
         </button>
      )
   }
}

 

여기까지 하고 불러왔을 때, 0~8까지 숫자가 있는 버튼이 있다면 문제가 없는 것이다.

 

이쯤해서 head 부분에 스타일링을 추가해주자.

 

<style>
     body {
        font: 14px "Century Gothic", Futura, sans-serif;
        margin: 20px;
     }

     ol, ul {
        padding-left: 30px;
     }

     .board-row:after {
        clear: both;
        content: "";
        display: table;
     }

     .status {
        margin-bottom: 10px;
     }

     .square {
        background: #fff;
        border: 1px solid #999;
        float: left;
        font-size: 24px;
        font-weight: bold;
        line-height: 34px;
        height: 34px;
        margin-right: -1px;
        margin-top: -1px;
        padding: 0;
        text-align: center;
        width: 34px;
     }

     .square:focus {
        outline: none;
     }

     .kbd-navigation .square:focus {
        background: #ddd;
     }

     .game {
        display: flex;
        flex-direction: row;
     }

     .game-info {
        margin-left: 20px;
     }

</style>

 

2. 사용자와의 상호작용 추가

JS의 addEventListener와 비슷하게 사용자의 클릭 등 입력에 반응해 실행되는 함수를 추가해줄 것이다.

 

우선 가장 간단한 함수로 클릭하면 클릭되었다고 console.log로 출력하는 함수를 삽입한다.

 

class Sqaure extends React.Component {
   render() {
      return (
         <button
            className='sqaure'
            onClick = { () => console.log('clicked') }
         >
            {this.props.value}
         </button>
      )
   }
}

 

화면에 보이는 값을 바꾸려면 state 변수를 주고, 클릭에 따라 state 변수를 업데이트 해주어야한다.

 

state 변수를 선언하고, 클릭시 state 변수를 업데이트하는 함수를 넣어준다.

 

class Square extends React.Component {
   state = {
      value:null
   }
   
   render() {
      return (
         <button
            className = 'square'
            onClick = { () => this.setState({value:'X'}) }
         >
            {this.state.value}
         </button>
      )
   }
}

 

여기서는 잠시 <Board/>에서의 값을 가져오는 것이 아닌, state의 값을 사용할 것이기 때문에

 

button의 값도 {this.state.value}로 바꿔줘야한다.

 

이제 각 <Square/> 에는 null (공백), X 만이 있게 된다. (숫자는 가지고 있지만 더 이상 렌더링되지 않음)

 

 

3. parent, child 컴포넌트 간 상태 공유 (parent <> child, child<> child)

board가 각 squares의 state를 가져와 그걸 합쳐 게임의 상태를 인지할 수도 있지만,

 

이는 코드의 유지, 보수 측면에서 좋지 않다.

 

따라서 <Square/>가 아닌 <Board/> 컴포넌트에 게임의 진행 상황 (state)를 저장하고,

 

숫자를 넘겨주는 것과 동일한 방식으로 게임의 진행 상황도 각 Square에 넘겨줄 것이다.

 

<Board/> 에 다음과 같이 state를 정의해주자.

 

class Board extends React.Component {
   state = {
      squares: Array(9).fill(null)
   }
   
   // ..이하 생략
   
}

각 <Square>들의 상태를 배열로 저장한다. 게임이 진행됨에 따라 null 값들이 O 혹은 X로 대체된다.

 

이 정보 (배열의 각 요소)들을 <Square>로 전달하기 위해 다음과 같이 renderSquare 함수를 수정한다.

 

renderSquare(i) {
   return <Square value= {this.state.squares[i]}/>
}

 

여기에 더해 Square가 클릭되었을 때 일어나는 이벤트를 변경해야한다.

 

현재 Board 컴포넌트는 어떤 <Square>가 채워져있는지에 대한 정보를 담고 있는데,  (squares 배열)

 

<Square/>를 클릭했을 때 <Square/>로 부터 <Board/>를 변경할 방법이 필요하다.

 

이는 <Board/>에서 <Square/>로 함수를 전달하고,

 

<Square/>를 클릭 할 때 함수를 호출하는 방식으로 구현할 수 있다.

 

renderSquare를 수정해보자.

 

renderSquare(i) {
   return(
      <Square
         value={this.state.squares[i]}
         onClick = { () => this.handleClick(i) }
      />
   )
}

 

클릭시 발동하는 handleClick 함수를 <Square/>에 전달한다. (아직 handleClick은 선언하지 않음)

 

이에 맞춰 <Square/>도 다시 수정을 해줘야한다.

 

class Square extends React.Component {
   render() {
      return(
         <button
            className = 'square'
            onClick = { () => this.props.onClick() }
         >
            { this.props.value }
         </button>
      )
   }
}

 

원래 <Square/>의 click시 발동하는 setState 함수를 <Board/>에서 전달한 함수인 this.props.onClick으로 대체했으며,

 

button의 value도 다시 <Board/>에서 전달한 {this.props.value}로 대체되었다.

 

이제 <Square/>를 클릭하면 <Board/>에서 받은 onClick 함수가 실행된다.

 

handleClick 함수는 <Board/> 컴포넌트 안에 다음과 같이 선언해준다.

 

handleClick(i) {
   const squares = this.state.squares.slice()
   // state에서 선언한 배열과 동일한 배열을 복사해온다.
   squares[i] = 'X'
   this.setState({squares:squares})
}

 

이제 <Squares/> 자체적으로는 가진 변수 (state)가 없고, 모든 변수를 <Board/>에서 받아 사용한다.

 

4. 순서 만들기

이제 클릭에 따라 X, O가 번갈아 나타나도록 코드를 수정해보자.

 

다음과 같이 <Board/>의 state 객체 안에 변수를 추가한다.

 

state = {
   squares:Array(9).fill(null),
   next : true
}

 

이제 한 번의 클릭이 일어나면 next의 T/F가 바뀌도록 handleClick 함수를 조정한다.

 

if문은 여기서 사용할 수 없으므로 삼항 연산자를 사용해야함에 유의한다.

 

handleClick(i) {
   const squares = this.state.squares.slice()
   // state의 배열을 복사해온다.
   
   squares[i] = this.state.next ? 'X' : 'O'
   // next가 true라면 X, false하면 O를 i번째 인덱스에 넣는다.
   
   this.setState({
      squares: squares,
      next : !this.state.next
   })
   // Board 컴포넌트의 state를 update 한다.
   // squares 배열은 X/O가 i번째 인덱스에 삽입되고, next는 T/F가 반전된다.
}

 

<Board/>의 render 함수 내의 status도 순서에 따라 업데이트되도록 바꿔준다.

 

const status = 'Next player: ' + (this.state.next ? 'X' : 'O')

 

5. 승자 결정

이제 승패가 결정되었을 때와 더 이상 둘 곳이 없을 때를 알려주는 함수를 추가한다.

 

다음과 같은 함수를 script element 최상단에 추가한다.

 

function calculateWinner (squares) {
   const lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
   ];
   for (let i=0; i < lines.length; i++) {
      const [a, b, c] = lines[i]
      if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
         return squares[a]
      }
   }
   return
}

 

lines의 배열들대로 배치가 된다면 승자가 결정된다.

 

예를 들어 lines[0] = [0,1,2] 인데, 그럼  a, b, c가 각각 0, 1, 2의 값을 가진다.

 

여기서 squares[0] 이 null이 아닌 값 (X or O)이고, squares[1], square[2], squares[0]이 전부 같은 값이라면

 

승자가 결정된 것이다. (일렬로 같은 값이 3개 배치되었으므로)

 

이 때 squares[0] 값을 return 해준다.

 

조건을 만족하는 배열이 없을 경우 null 값을 return 한다.

 

<Board/>의 render 함수를 calculateWinner 함수를 넣어 수정해준다.

 

render() {
   const winner = calculateWinner(this.state.squares)
   let status
   if (winner) {
      status = 'Winner: ' + winner
   }
   else {
      status = 'Next player: ' + (this.state.next ? 'X' : 'O')
   }
   
   // ..이하 생략
}

 

마지막으로 승자가 결정되면 클릭을 해도 반응이 없도록 handleClick 함수를 수정한다.

 

handleClick(i) {
   const squares = this.state.squares.slice()
   if (calculateWinner(squares) || squares[i]) {
      return
   }
   squares[i] = this.state.next ? 'X' : 'O'
   this.setState({
      squares:squares,
      next: !this.state.next
   })
}

 

지금까지 한 코드를 정리하면 다음과 같다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> 
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <style>
        body {
        font: 14px "Century Gothic", Futura, sans-serif;
        margin: 20px;
        }

        ol, ul {
        padding-left: 30px;
        }

        .board-row:after {
        clear: both;
        content: "";
        display: table;
        }

        .status {
        margin-bottom: 10px;
        }

        .square {
        background: #fff;
        border: 1px solid #999;
        float: left;
        font-size: 24px;
        font-weight: bold;
        line-height: 34px;
        height: 34px;
        margin-right: -1px;
        margin-top: -1px;
        padding: 0;
        text-align: center;
        width: 34px;
        }

        .square:focus {
        outline: none;
        }

        .kbd-navigation .square:focus {
        background: #ddd;
        }

        .game {
        display: flex;
        flex-direction: row;
        }

        .game-info {
        margin-left: 20px;
        }

    </style>
</head>
<body>
    <div id="root"> </div>
    <!-- 이 안에 html element 생성-->

    <script type="text/babel">

        class Square extends React.Component {

            state = {
                value:null
            }

            render() {
                return (
                    <button 
                        className = 'square'
                        onClick = { () => this.props.onClick() }
                    >
                        { this.props.value } 
                    </button>
                )
            }
        }

        class Board extends React.Component {

            state = {
                squares: Array(9).fill(null),
                next: true
            }

            handleClick(i) {
                const squares = this.state.squares.slice()
                if (calculateWinner(squares) || squares[i]) {
                    return
                }
                squares[i] = this.state.next ? 'X' : 'O'
                this.setState({ 
                    squares: squares,
                    next : !this.state.next
                })
            }

            renderSquare(i) {
                return <Square 
                   value={this.state.squares[i]}
                   onClick= { () => this.handleClick(i) }
                />
            }

            render() {
                const status = 'Next player : ' + (this.state.next ? 'X' : 'O')

                return (
                    <div>
                        <div className='status'>{status}</div>
                        <div className='board-row'>
                            {this.renderSquare(0)}
                            {this.renderSquare(1)}
                            {this.renderSquare(2)}
                        </div>
                        <div className='board-row'>
                            {this.renderSquare(3)}
                            {this.renderSquare(4)}
                            {this.renderSquare(5)}
                        </div>
                        <div className='board-row'>
                            {this.renderSquare(6)}
                            {this.renderSquare(7)}
                            {this.renderSquare(8)}
                        </div>
                    </div>
                )
            }
        }

        class Game extends React.Component {
            render() {
                return(
                    <div className='game'>
                        <div id = 'game-board'>
                            <Board/>
                        </div>
                    </div>
                )
            }
        }

        ReactDOM.render(
            <Game/>,
            document.querySelector('#root')
        )
    </script>
</body>
</html>

 

'React' 카테고리의 다른 글

#6 Styling  (0) 2022.04.22
#5 Webpack  (0) 2022.04.22
React #3 Class Component - data 변화 인식  (0) 2022.04.13
React #2 Class Component  (0) 2022.04.13
React #1 Intro  (0) 2022.04.12