본문 바로가기

공부일지/Project

비전공자의 프로젝트 만들기) TicTacToe App (Class Type)

728x90
반응형

TicTacToeApp

 

React 공식 사이트에 있는 틱택톡 예제를 만들었다. 

 

프로젝트에는 css내용은 웬만해서 적지 않을 예정이다.

npx create-react-app tictactoe_app

우선 틱택톡 앱을 만들기 위해서 리액트 프로젝트를 만들어 주었다.

 

기본틀부터 작업하였다.

 

App.js

import './App.css';

function App() {
  return (
    <div className="game">
      <div className='game-board'></div>
      <div className='game-info'></div>
    </div>
  );
}

export default App;

 

이제 컴포넌트를 생성해 app.js에 출력되게 만들 예정이다.

src하위 폴더에 컴포넌트폴더를 생성 후 Board와 Square를 만들어준다.

tictactoe 폴더 트리

 

Square.js

import React, { Component } from 'react'

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

 

 

Board.js

import React, { Component } from 'react'
import Square from './Square'

export default class Board extends 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>
        )
    }
}

renderSquare를 만든 후 render 해준다. 

<Square />은 컴포넌트에 만든 square파일을 불러왔다.

square를 선언할 때 중괄호{}가 없는 이유는 square파일에는 기본 square가 export default로 작성을 하였다.

그러면 굳이 renderSquare라는 함수를 만들지 않고 

import React, { Component } from 'react'
import Square from './Square'

export default class Board extends Component {
    render() {
        const status = 'Next Player : X'
        return (
        <div>
            <div className='status'>{status}</div>
            <div className='board-row'>
                <Square />
                <Square />
                <Square />
            </div>
            <div className='board-row'>
                <Square />
                <Square />
                <Square />
            </div>
            <div className='board-row'>
                <Square />
                <Square />
                <Square />
            </div>
        </div>
        )
    }
}

위에 코드처럼 작업해도 괜찮지만 지금은 틀만 잡는 거라 따로 이벤트나 기능은 작업하지 않아서 그렇게 보이지만 추후 작업을 진행하게 되면 똑같은 props 및 onclick을 여러 번 적어야 되고 수정할 때도 한 곳만 수정하면 되는데 여러 곳을 수정해야 된다.

 

현재 렌더링 된 화면은 아래 이미지처럼 저렇게 나온다. 

나는 css를 작성을 해서 배경이미지와 버튼들이 저렇게 정렬된 모습으로 나오는데 css가 없으면 버튼이 9개 나오고 Next Player라는 글자가 나오면 성공이다.

 

그럼 이제 board.js에서 만들었던 renderSquare에 props를 주고 Square에서는 props가 잘 전달되었는지 확인해 보려고 한다.

 

Board.js

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

만들었던 renderSquare에 value라는 props를 전달해 주고

 

Square.js

import React, { Component } from 'react'
import './Square.css'

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

Square에서 해당 props를 전달받는다.

 

클래스형 컴포넌트에서는 함수형과 다르게 {this.props.value}를 적어서 props를 전달받는다. value는 부모컴포넌트에서 전달한 속성의 이름값을 그대로 적으면 된다.

 

이제 버튼을 클릭했을 때 값을 전달하는 작업을 진행할 것이다.

 

Board.js

import React, { Component } from 'react'
import Square from './Square'

export default class Board extends Component {

    constructor(props){
        super(props);
        this.state={
            squares : Array(9).fill(null)
        }
    }
    
    handleClick(i){
        const squares = this.state.squares.slice();
        squares[i] = 'X';
        this.setState({squares: squares});
    }

    renderSquare(i){
        return <Square value={this.state.squares[i]} onClick={()=>this.handleClick(i)} />
    }
    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>
        )
    }
}

새롭게 작성한 코드를 이야기해보자면

    constructor(props){
        super(props);
        this.state={
            squares : Array(9).fill(null)
        }
    }
    
    handleClick(i){
        const squares = this.state.squares.slice();
        squares[i] = 'X';
        this.setState({squares: squares});
    }

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

extends 한 class 컴포넌트에서 this를 사용하기 위해선 super를 먼저 선언해주어야 한다고 한다.

super에서 어떤 것을 선언해 줄지는 프로젝트마다 다르겠지만 지금은 props를 지정해 주어서 this에 status에 squares라는  키를 만들고 값은 배열을 만들고 null값으로 채운다.

 

slice에 아무 값을 적지 않으면 배열 그대로 복사된다.

우리가 클릭하면 해당 배열에서 i번째 값이 X로 변경한 뒤 state에 있는 squares에 적용한다. state값을 변경하기 위해선 setState를 사용하여 변경되고 리렌더링 된다.

 

그리고 square에게 클릭함수와, value를 전달한다.

 

Square.js

import React, { Component } from 'react'
import './Square.css'

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

전달받은 handleClick 함수와 value를 작성해 준다.

 

화면에 위의 이미지처럼 나온다.

 

여기까지 패스트캠퍼스에 있는걸 클론코딩 하였다.

 

이제 O, X로 값이 바뀌고 어떤 플레이어가 이겼는지 작업할 것이다.

class문법 관련해서는 내가 이해가 힘들어서 많이 약한 거 같다.

일단 완성된 코드를 보면

import React, { Component } from 'react'
import Square from './Square'

export default class Board extends Component {

    constructor(props){
        super(props);
        this.state={
            squares : Array(9).fill(null),
            playerNext : true,
            winner:false,
            player1:[],
            player2:[]
        }
    }
    
    handleClick(i){
        if(this.state.winner || this.state.squares[i] !== null) return false;

        const squares = this.state.squares.slice();
        let player1 = this.state.player1.slice();
        let player2 = this.state.player2.slice();
        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],
        ];

        squares[i] = this.state.playerNext ? 'O' : 'X';
        this.state.playerNext === true ? player1.push(i) : player2.push(i)
        let nowPlayer = this.state.playerNext ? player1 : player2;

        if(nowPlayer.length > 2){
            let result;
            for(const line of lines){
                result = line.filter(x => nowPlayer.includes(x));
                if(result.length === 3 ){
                    this.setState({winner:true})
                }
            }
        }


        this.setState({
            squares: squares,
            playerNext: !this.state.playerNext,
            player1: player1,
            player2: player2
        });


    }

    renderSquare(i){
        return <Square value={this.state.squares[i]} onClick={()=>this.handleClick(i)} />
    }
    render() {
        const status = !this.state.winner 
                        ? `Now Player ${this.state.playerNext ? 'Player1' : 'Player2'}` 
                        : `Winner Player ${this.state.playerNext ? 'Player2' : 'Player1'}`
        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>
        )
    }
}

 

state에 playerNext, winner와 player1, player2를 만들었다.

그리고 handleClick 함수에서 winner와 player 별로 각각의 배열을 만들어 주었다.

 

클릭 함수에 player1,2를 각각 복사를 하고 

square에서 playerNext를 정하는 부분에 작성한 코드처럼 boolean의 여부로 player1이나 2에게 클릭한 값을 넣었다.

그리고 시간이 제일 오래 걸린 for문 부분이다.

if(nowPlayer.length > 2){
            let result;
            for(const line of lines){
                result = line.filter(x => nowPlayer.includes(x));
                if(result.length === 3 ){
                    this.setState({winner:true})
                }
            }
        }

if문으로 일단 클릭해서 받은 player의 배열에 개수가 3개 이상인지부터 확인하고 3개 이상일 때 for문을 사용하였다.

 

처음에는 for문이 아니라 filter로 includes를 해보았는데 전부다 false만 뜨고 그다음은 indexOf를 사용했는데 내가 원하는 결과값을 얻기에는 맞지 않는 결과값을 리턴을 해서 지우고 구글링 한 끝에 for문을 사용해 하나씩 나오게 하고 거기에 nowPlayer에 있는 값이랑 맞는지 비교하는 방식을 사용하니 내가 원하는 결과값을 얻을 수 있었다.

nowPlayer라는 변수도 처음에는 작성하지 않았던 변수인데 for문을 사용하면서 만들게 되었다.

그런데 for문을 끝내려고 break를 사용하니 클릭 이벤트 자체가 그냥 다 끝나버리는 거 같았다.

 

그래서 break도 작업 중 삭제하였다.

 

3개 이상인데 리턴 받은 값도 true면 winner을 true를 주면서 이벤트도 실행되지 않게 하였다.

그리고 status의 값도 Winner Player로 변경하였다.

비록 어려운 프로젝트는 아니었지만 인강에서 나온 거 이외에 추가 작업으로 진행한 첫 프로젝트라 뭔가 기분이 좋고 앞으로 어떤 부분을 보안해야 될지 알 수 있게 되었다.

 

 

 

 

728x90
반응형