본문 바로가기

공부일지/Project

비전공자의 프로젝트 만들기) TicTacToeApp (Function Type)

728x90
반응형

TicTacToe 만들기 2탄

 

어제 만들었던 TicTacToe App을 class문법이 아닌 function 문법으로 변경 작업을 진행했다.

단 이번에는 굳이 CRA 해서 로컬에서 만들지 않고 CodeSandBox를 이용해서 진행한다.

리액트 16 버전부터 생긴 ReactHook을 사용해 변경 작업을 진행한다.

 

먼저 Board부터 변경하였다.

import React, { useState } from "react";
import Square from "./Square";

const Board = () => {
  const [squares, setSquares] = useState(Array(9).fill(null));

  const handleClick = (i) => {
    const squaresClone = squares.slice();
    squaresClone[i] = "X";

    setSquares(squaresClone);
  };

  const renderSquare = (i) => {
    return <Square value={squares[i]} onClick={() => handleClick(i)} />;
  };
  const status = `Now Player : Player1 Player2`;
  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};

export default Board;

constructor는 삭제하고 state는 useState로 변경해 주었다.

그리고 click 및 square은 변수에 각각 지정해 주었다.

render도 class문법에서 출력을 위해 사용되는 메소드로 삭제해 주었다.

크게 많이 바뀌진 않았다. class내부에서는 this를 사용해서 선언해주었다면.

function에서는 그냥 const로 지정하고 이름만 불러와서 똑같이 사용했기 때문이다.

 

Square.js

import React from "react";
import "./Square.css";

const Square = ({ value, onClick }) => {
  return (
    <button
      className="square"
      onClick={() => {
        onClick();
      }}
    >
      {value}
    </button>
  );
};

export default Square;

이제 다음으로 변경한 건 NextPlayer와 Winner이다.

import React, { useState } from "react";
import Square from "./Square";

const Board = () => {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [playerNext, setPlayerNext] = useState(true);

  let status;
  const calculateSquare = (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] === "X" ? "player1" : "player2";
      }
    }
    return null;
  };

  let winner = calculateSquare(squares);
  if (winner) {
    status = `Winner : ${winner}`;
  } else {
    status = `Now Player : ${playerNext ? "Player1" : "Player2"}`;
  }

  const handleClick = (i) => {
    if (squares[i] || winner) return false;
    const squaresClone = squares.slice();
    squaresClone[i] = playerNext ? "X" : "O";
    setPlayerNext(!playerNext);
    setSquares(squaresClone);
  };

  const renderSquare = (i) => {
    return <Square value={squares[i]} onClick={() => handleClick(i)} />;
  };

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

export default Board;

class에서 사용했던 코드방식이랑 비슷한데 살짝 다르다.

이번에는 calculateSquare라는 변수를 만들어 안에 lines를 넣고 for문을 돌려 이번에는 a, b, c에 각각 lines[i]에 들어오는 3개의 값을 넣어주고 if문을 주었다. 처음에 square[a]가 null값이 아니어야 하고 a === b, a===c가 같은 같이면 return을 해주고 아무것도 없으면 마지막에 null을 리턴한다.

 

winner라는 변수에 위에 만든 이벤트를 넣어주고 반환받은 값이 있으면 winner가 나오고 없으면 nextplayer가 나오는 방식이다.

 

그리고 클릭이벤트에서 setPlayerNext에서 (!playerNext)를 작성한 이유는 ! 가 부정이기 때문에 기존에 값의 부정값을 입력하게 만들었다.

prev => !prev 해도 되는데 prev는 굳이 이번에는 사용할 필요가 없다고 느꼈다.

prev는 숫자를 1씩 증가시키거나 배열을 다시 넣거나 할 때 사용하고 boolean에서는 그냥 !만 작성하는 게 직관적이고 이해하기도 쉬웠다. 

당연히 기존에 클릭했던 곳은 클릭할 수 없게 하고 winner가 결정되면 클릭 안되게 클릭이벤트에서 조건을 생성하였다.

 

이렇게 작성하면 o, x로 값이 변경되고 winner가 결정된다.

 

이제는 게임에서 잘못 클릭했을 때 이전 턴으로 변경돼야 될 때가 있는데 그 부분을 작업하였다.

 

수정해야 될 것이 조금 많은 거 같다.

 

기존에 Board.js에 있던 이벤트와 변수, 함수들을 전부 App.js로 옮겨 와야 한다.

 

그런데 이런 간단한 앱을 만들 때도 오타 때문에 무엇이 잘못 적혔는지 찾는데 시간이 더 오래 걸린 거 같다....

 

우리가 클릭할 때마다 턴이 저장되어야 하니 배열 안에 Object를 넣어서 클릭할 때마다 key에 배열이 저장되도록 만들어야 한다.

const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);

우리가 처음에 square로 만들었던 state를 history로 변경하면서 useState에 배열을 만들고 안에 squares의 키값에 빈배열 9개 있는 값을 넣었다.

 

그러면 처음 작업할 때 그냥 square의 값을 받던 곳들도 수정을 해주어야 한다.

그냥 여기서 history를 사용하게 되면 클릭하는 곳의 키의 배열 안에 값이 들어가는 게 아니고 그냥 history전체 배열 안에서 조작하려고 해서 에러가 날것이다.

  const current = history[history - 1];
  const winner = calculateSquare(current.squares);

이렇게 currnet를 하나 만들어서 history의 마지막 배열을 찾아 선언해 주고 squares는 앞에 current를 붙여준다.

그러면 배열의 마지막 객체의 squares의 값을 찾는다.

 

handleClick

  const handleClick = (i) => {
    if (current.squares[i] || winner) return;
    const squaresClone = current.squares.slice();
    squaresClone[i] = playerNext ? "X" : "O";
    setPlayerNext(!playerNext);
    setHistory([...history, { squares: squaresClone }]);
  };

clone은 위에 winner처럼 current를 사용하면 되고 setHistory부분은 spread를 기존 history의 값을 그대로 넣어준다. 

...은 spread라고 새로운 배열을 만들 때 기존배열의 값을 사용하면서 만들어야 될 때 굳이 똑같은 값을 그대로 하나하나 적을 필요 없이 해당 값을 들고 와서 사용한다. 

쉽게 생각해서 종속받는다고 생각하면 될 거 같다. 종속이라는 뜻이랑 다를 수도 있다.

기존데이터의 값을 수정할 필요도 없고 수정해야 될 땐 기존에 선언했던 배열만 수정하면 수정한 배열을 종속받던 배열들도 동일하게 수정된 값이 적용된다.

 

이제 history에 배열을 수정할 때마다 하나씩 추가되니 이제 버튼을 만들어 이전 턴의 값을 적용시키는 작업을 진행했다.

 

먼저 몇 번째 턴인지 알기 위해  state를 하나 만든다.

  const [stepNumber, setStepNumber] = useState(0);

그리고 moves라는 버튼 리스트를 출력하는 변수를 만든다.

 

  const moves = history.map((step, move) => {
    const desc = move ? `Goto move ${move}` : "Go to game Start";
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });
  
const jumpTo = (step) => {
    setStepNumber(step);
    setPlayerNext(step % 2 === 0);
  };

histroy에 작성했던 배열을 map을 사용해서 li를 하나씩 나오게 하고 react에서는 map을 사용할 때 key값을 사용해야 되는데 key값은 move로 하였다. 

jompTo라는 이벤트를 통해서 기록해 놨던 턴으로 넘어가게 만든다.

setStepNumber에 prop으로 받았던 move의 정보를 저장하고

player은 step을 2로 나누어서 0이 아니면 false 0이면 true로 나오게 한다.

 

이렇게 하면 90프로는 작업이 끝났다. 이제 handleClick만 수정해 주면 된다.

 

  const handleClick = (i) => {
    if (current.squares[i] || winner) return;
    const newHistory = history.slice(0, stepNumber + 1);
    const newCurrent = newHistory[newHistory.length - 1];
    const squaresClone = newCurrent.squares.slice();
    squaresClone[i] = playerNext ? "X" : "O";
    setPlayerNext(!playerNext);
    setHistory([...newHistory, { squares: squaresClone }]);
    setStepNumber(newHistory.length);
  };

조금 많이 바뀐 거 같은데

일단 newHistory를 만들어 history를 slice로 자르는데 0번째부터 현재 stepNumber + 1의 위치를 지정한다.

+ 1 한 이유는 인덱스가 0부터 시작하기 때문에 0이면 -1, 1이면 0이 되기 때문에 현재값 그대로 잘리게 되어 하나씩 더 많이 잘리게 된다.

 

그리고 newHistory를 기준으로 newCurrent를 만들고 newCurrent를 복사한다.

이후는 변경된 게 setHistory에서 history였던 거를 newHistory로 변경해 주고 stepNumber를 newHistory.length값으로 변경만 하면 된다.

 

그리고 winner위에 작성했던 current값도 변경해 준다.

  const current = history[stepNumber];

이렇게 하면 완성이다.

 

App.js

 

import { useState } from "react";
import "./App.css";
import Board from "./components/Board";

function App() {
  const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);
  const [stepNumber, setStepNumber] = useState(0);
  const [playerNext, setPlayerNext] = useState(true);

  const calculateSquare = (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] === "X" ? "player1" : "player2";
      }
    }
    return null;
  };
  const current = history[stepNumber];
  const winner = calculateSquare(current.squares);

  let status;
  if (winner) {
    status = `Winner : ${winner}`;
  } else {
    status = `Now Player : ${playerNext ? "Player1" : "Player2"}`;
  }

  const handleClick = (i) => {
    if (current.squares[i] || winner) return;
    const newHistory = history.slice(0, stepNumber + 1);
    const newCurrent = newHistory[newHistory.length - 1];
    const squaresClone = newCurrent.squares.slice();
    squaresClone[i] = playerNext ? "X" : "O";
    setPlayerNext(!playerNext);
    setHistory([...newHistory, { squares: squaresClone }]);
    setStepNumber(newHistory.length);
  };

  const moves = history.map((step, move) => {
    const desc = move ? `Goto move ${move}` : "Go to game Start";
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });
  const jumpTo = (step) => {
    setStepNumber(step);
    setPlayerNext(step % 2 === 0);
  };

  return (
    <div className="game">
      <div className="status">{status}</div>
      <div className="game-board">
        <Board
          squares={current.squares}
          status={status}
          handleClick={(i) => handleClick(i)}
        />
      </div>
      <ol className="game-info">{moves}</ol>
    </div>
  );
}

export default App;

 

CodeSandBox 주소

https://codesandbox.io/s/tictactoeapp-7eyd83?file=/src/components/Board.css

 

TicTacToeApp - CodeSandbox

TicTacToeApp by piwe2004 using loader-utils, react, react-dom, react-scripts

codesandbox.io

아직 주니어급인 나에게는 시간이 조금 걸리는 작업이었다.

아직 주니어도 실력도 안되는 거 같다.

728x90
반응형