[react] tic tac toe 게임 만들기
리액트 공식 문서에 tic tac toe 게임을 만드는 튜토리얼이 있다.
이걸 처음 만든것은 hooks 가 나오고 이걸 연습해보고 싶은데 뭐가 좋을까 하다가 만들어 봤었다.
아주 복잡한 프로그램은 아니지만 리액트의 흐름을 이해하고 다양한 생각을 해보기에 좋다.
“공식문서 만으로 iOS 개발 배우기” 를 읽고 공식 문서들을 좀 더 자주 읽어봐야겠다 싶었는데 생각났을 때 오랜만에 게임을 다시 한번 만들어보면서 정리하자 싶어서 구현해 봤다.
history 를 추가하기 전까지의 코드는 다음과 같다.
tic tac toe
import React, { useState, useCallback } from "react";
import "./App.css";function Square({ value, onClick }) {
return (
<button className="square" onClick={onClick}>
{value}
</button>
);
}function Board() {
const [state, setState] = useState({
squares: Array(9).fill(null),
xIsNext: true
}); console.log(state.squares.slice()); const onClick = useCallback(
i => {
const square = state.squares.slice();
if (calc(square) || square[i]) {
return null;
}
square[i] = state.xIsNext ? "x" : "o";
setState({
...state,
squares: square,
xIsNext: !state.xIsNext
});
},
[state]
); const renderSquare = i => {
return <Square value={state.squares[i]} onClick={() => onClick(i)} />;
}; const winner = calc(state.squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next Player: " + (state.xIsNext ? "X" : "o");
} 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>
);
}function calc(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 null;
}function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}export default Game;
공식 문서의 기본 세팅 파일의 구조는 클래스형 컴포넌트로 구성되어 있다.
해당 컴포넌트들을 함수형으로 변경하고, 그에 맞게 props, this 등을 정리한다.
그리고 state 는 useState 를 통해 선언하며, 여기서는 state 에 squares 와 xIsNext 를 같이 담아서 진행했는데, 따로 나누어서 각각의 useState 를 통해 구현할 수도 있다.
하나의 state 에 담아서 사용할 경우 특정 값의 변경이 필요할 경우 전개 연산자를 사용하여 기존 값들을 유지시켜주면 된다.
setState({
...state,
// change value
})
이 정도를 통해 기존 클래스형 컴포넌트를 함수형 컴포넌트로 변환이 가능하다.
hooks 와 함수형 컴포넌트만으로 구현하는게 처음이라면 헷갈릴 수 있다. 개인적으로도 처음 hooks 로 컨버팅 해봐야지라고 시작했을 때 생각보다 간단하지 않았던 기억이 있다.
이것저것 바꿔보고 작동방식을 이해하고 나면 간편하고 강력함을 느낄 수 있을것이라 생각한다.
history 기능을 추가한 코드는 다음과 같다.
import React, { useState, useCallback } from "react";
import "./App.css";function Square({ value, onClick }) {
return (
<button className="square" onClick={onClick}>
{value}
</button>
);
}function Board({ state, onClick }) {
const renderSquare = i => {
return <Square value={state[i]} onClick={() => onClick(i)} />;
}; return (
<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>
);
}function calc(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 null;
}function Game() {
const [state, setState] = useState({
history: [{ squares: Array(9).fill(null) }],
xIsNext: true,
stepNumber: 0
}); const current = state.history[state.stepNumber];
const winner = calc(current.squares); const onClick = useCallback(
i => {
const history = state.history.slice(0, state.stepNumber + 1);
const square = current.squares.slice();
if (calc(square) || square[i]) {
return null;
}
square[i] = state.xIsNext ? "x" : "o";
setState({
...state,
history: history.concat([
{
squares: square
}
]),
xIsNext: !state.xIsNext,
stepNumber: history.length
});
},
[state, current]
); const jumpTo = step => {
setState({
...state,
stepNumber: step,
xIsNext: step % 2 === 0
});
}; const moves = state.history.map((step, move) => {
const desc = move ? "Go to move #" + move : "Go to game start";
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next Player: " + (state.xIsNext ? "X" : "o");
} return (
<div className="game">
<div className="game-board">
<Board state={current.squares} onClick={i => onClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}export default Game;
history 기능은 처음 구현해봤는데 사실 이 부분은 잘 이해하고 구현했다기 보다는 기존 코드를 컨버팅해서 붙여 넣은 정도이다.
그렇기는 하지만 코드를 작성하고 작동하는 것을 보니 정말 좋고, 다양하게 활용하기 좋겠다는 생각이 들어 조금 더 봐야겠다.
여기까지하면 리액트 문서에서 제공하는 튜토리얼의 과정을 다 따라갈 수 있다.
이 프로그램을 만들며 리액트의 기본 개념들을 확인할 수 있어 좋다.
각각을 아주 자세하게는 아니더라도 기본 구조를 잘 이해하며 따라갈 수 있게 돕는다.
그리고 사실 이러한 간단하지만 명료한 이해가 중요하다고 생각한다.
이것을 기반으로 필요한 기능을 붙여갈 때, 프로그램이 커지더라도 단단한 프로그램을 작성할 수 있지 않을까 기대한다.
리액트 문서에서도 이렇게 게임이 완료된 후에도 추가적으로 해볼만한 과제들을 제공하는 데 그 과제들과 더불어서 테스트해보고 싶은 다양한 기능들을 필요맞게 수정해가며 테스트해보기 좋은 잘 만들어진 폼이라고 생각한다.
typescript 추가 코드
import React, { useState, useCallback } from "react";
import "./App.css";type Square = {
value: string;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};function Square({ value, onClick }: Square) {
return (
<button className="square" onClick={onClick}>
{value}
</button>
);
}type Board = {
state: string[];
onClick: (i: number) => void;
};function Board({ state, onClick }: Board) {
const renderSquare = (i: number) => {
return <Square value={state[i]} onClick={() => onClick(i)} />;
}; return (
<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>
);
}function calc(squares: string[]) {
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 null;
}function Game() {
const [state, setState] = useState({
history: [{ squares: Array(9).fill(null) }],
xIsNext: true,
stepNumber: 0
});
const current = state.history[state.stepNumber];
const winner = calc(current.squares);
const onClick = useCallback(
i => {
const history = state.history.slice(0, state.stepNumber + 1);
const square = current.squares.slice();
if (calc(square) || square[i]) {
return null;
}
square[i] = state.xIsNext ? "x" : "o";
setState({
...state,
history: history.concat([
{
squares: square
}
]),
xIsNext: !state.xIsNext,
stepNumber: history.length
});
},
[state, current]
); const jumpTo = (step: number) => {
setState({
...state,
stepNumber: step,
xIsNext: step % 2 === 0
});
}; const moves = state.history.map((step: object, move: number) => {
const desc = move ? "Go to move #" + move : "Go to game start";
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next Player: " + (state.xIsNext ? "X" : "o");
}
return (
<div className="game">
<div className="game-board">
<Board state={current.squares} onClick={(i: number) => onClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}export default Game;