React 공식문서 자습서에 나와있는 틱택토를 타입스크립트와 함수형 컴포넌트로 작업했습니다.
- buildTool:
React-Create-App
- style:
styled-component
- type-check:
TypeScript
- github : https://github.com/moolbum/react-tic-tac-toe
- 배포 주소 : https://react-tic-tac-toe-moolbum.vercel.app
구현 목록입니다. 순서대로 진행해보겠습니다!
3x3 게임판을 만들어주기 위해 9개의 배열을 이용하려고합니다.
클릭을 할 때 마다 상태값이 변하기 때문에 useState를 이용합니다.
app.tsx
import React, {useState,FC} from 'react';
const app:FC = ():JSX.Element => {
const [state, setState] = useState<SquareState>({
squares: Array(9).fill(null), //9개의 빈 배열 생성
isNext: true, // 다음클릭 인식
});
// 사각형 컴포넌트 생성 함수, 숫자를 인자로 받아 value, onClick전달
const renderSquare = (i: number) => {
return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
};
return (
<Container>
<article>
{renderSquare(0)} //숫자를 전달
{renderSquare(1)}
{renderSquare(2)}
</article>
<article>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</article>
<article>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</article>
</Container>
)
}
사각형 컴포넌트는 app.tsx에서 renderSquare
함수의 인자를 받아 생성된 컴포넌트입니다.
Square.tsx
import React from "react";
import styled from "styled-components";
const Square = ({value, onClick,}:{value: string; onClick: (i: React.MouseEvent<HTMLButtonElement>) => void;
}): JSX.Element => {
return <Button onClick={onClick}>{value}</Button>;
};
export default Square;
위에서는 사각형 컴포넌트는 만들었지만 onClick에서 전달하는 함수인 handleClick
는 선언하지 않았습니다. 클릭시 사용되는 함수를 만들어 보겠습니다.
app.tsx
const handleClick = (i: number) => {
const square = state.squares.slice();
if(square[i]){
return; //중복클릭 제한
}
square[i] = state.isNext ? IsNext.x : IsNext.o;
setState({
squares: square,
isNext: !state.isNext,
});
};
type.tsx
export enum IsNext {
x = "X",
o = "O",
}
승자를 결정하는 방법은 9칸중에서 3칸이 X,X,X 또는 O,O,O가 나와야합니다.
각 lines에 번호는 사각형 컴포넌트가 생길때 생긴 value값입니다.
app.tsx
const [winner, setWinner] = useState<Winner>(null);
const [state, setState] = useState<SquareState>({
squares: Array(9).fill(null),
isNext: true,
});
const handleClick = (i: number) => {
const square = state.squares.slice();
if (calculate(square) || square[i]) { // 승자결정, 중복선택 불가
return;
}
square[i] = state.isNext ? IsNext.x : IsNext.o;
setState({
squares: square,
isNext: !state.isNext,
});
};
const calculate = (squares: Array<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]; // i= 1일때 a=0, b=1, ,c=2 로
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c] // [x,x,x]인지 확인
) {
return squares[a]; // 3가지가 같다면 같은 문구 출력 X or O
}
}
return null; // 하나라도 틀리면 null 반환
};
useEffect(() => {
setWinner(calculate(state.squares));
}, [state.squares]); // state.squares값이 변할때마다 winner 계산
리셋버튼은 버튼을 클릭하면 초기값으로 되돌아가게 하면됩니다!
setState
함수를 이용해 초기값으로 되돌립니다.
const [winner, setWinner] = useState<Winner>(null);
const [state, setState] = useState<SquareState>({
squares: Array(9).fill(null),
isNext: true,
});
const handleResetClick = (): void => {
setState({
squares: Array(9).fill(null),
isNext: true,
});
};
추가적으로 히스토리를 만들겠습니다.
버튼을 클릭하면 상태값을 저장할 수 있도록합니다.
useState를 이용해 history를 추가합니다.
const App: FC = (): JSX.Element => {
const [winner, setWinner] = useState<Winner>(null);
const [history, setHistory] = useState<History[]>([]); // history State추가
const [state, setState] = useState<SquareState>({
squares: Array(9).fill(null),
isNext: true,
});
const handleClick = (i: number) => {
const square = state.squares.slice(); // 얕은복사
if (calculate(square) || square[i]) {
return;
}
square[i] = state.isNext ? IsNext.x : IsNext.o;
setState({
squares: square,
isNext: !state.isNext,
});
setHistory([...history, { state }]); //클릭을 할 수록[{state},{state}, {state} ....] 이런 배열을 가진다.
};
const handleResetClick = (): void => {
setState({
squares: Array(9).fill(null),
isNext: true,
});
setHistory([]); //history 초기값 추가
};
const renderSquare = (i: number) => {
return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
};
const calculate = (squares: Array<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;
};
const handleHistoryClick = (state: any) => {
setState({
squares: state.squares,
isNext: state.isNext,
});
};
useEffect(() => {
setWinner(calculate(state.squares));
}, [state.squares]);
return (
<Container>
<Label radius="round" background="black" fontColor="white">
Tic Tac Toe
</Label>
{winner ? (
<Section>
<h2>winner : {winner}</h2>
<Button onClick={handleResetClick}>Reset</Button>
</Section>
) : (
<Section>
<h2>순서 : {state.isNext ? IsNext.x : IsNext.o}</h2>
<Button onClick={handleResetClick}>Reset</Button>
</Section>
)}
<Game>
<Board>
<article>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</article>
<article>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</article>
<article>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</article>
</Board>
<Ul>
{history.map(({ state }: any, idx: any) => {
return (
<li key={idx}>
<button onClick={() => handleHistoryClick(state)}>
{idx + 1} 번째
</button>
</li>
);
})}
</Ul>
</Game>
</Container>
);
};
export default App;
app.tsx
import React, { FC, useEffect, useState } from "react";
import styled from "styled-components";
import Label from "./components/Label";
import Square from "./components/Square";
import { IsNext, SquareState, Winner, History } from "./types";
const App: FC = (): JSX.Element => {
const [winner, setWinner] = useState<Winner>(null);
const [history, setHistory] = useState<History[]>([]);
const [state, setState] = useState<SquareState>({
squares: Array(9).fill(null),
isNext: true,
});
const handleClick = (i: number) => {
const square = state.squares.slice();
if (calculate(square) || square[i]) {
return;
}
square[i] = state.isNext ? IsNext.x : IsNext.o;
setState({
squares: square,
isNext: !state.isNext,
});
setHistory([...history, { state }]);
};
const handleResetClick = (): void => {
setState({
squares: Array(9).fill(null),
isNext: true,
});
setHistory([]);
};
const renderSquare = (i: number) => {
return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
};
const calculate = (squares: Array<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;
};
const handleHistoryClick = (state: any) => {
setState({
squares: state.squares,
isNext: state.isNext,
});
};
useEffect(() => {
setWinner(calculate(state.squares));
}, [state.squares]);
return (
<Container>
<Label radius="round" background="black" fontColor="white">
Tic Tac Toe
</Label>
{winner ? (
<Section>
<h2>winner : {winner}</h2>
<Button onClick={handleResetClick}>Reset</Button>
</Section>
) : (
<Section>
<h2>순서 : {state.isNext ? IsNext.x : IsNext.o}</h2>
<Button onClick={handleResetClick}>Reset</Button>
</Section>
)}
<Game>
<Board>
<article>
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</article>
<article>
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</article>
<article>
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</article>
</Board>
<Ul>
{history.map(({ state }: any, idx: any) => {
return (
<li key={idx}>
<button onClick={() => handleHistoryClick(state)}>
{idx + 1} 번째
</button>
</li>
);
})}
</Ul>
</Game>
</Container>
);
};
export default App;
const Container = styled.article`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100%;
margin: 0 auto;
h1 {
padding: 8px 15px;
border-radius: 36px;
background: black;
color: white;
font-size: 28px;
}
h2 {
font-size: 20px;
padding: 20px 0;
}
`;
const Game = styled.section`
width: 30%;
display: flex;
justify-content: space-between;
`;
const Board = styled.section`
article {
display: flex;
padding: 0;
text-align: center;
}
`;
const Section = styled.section`
width: 220px;
display: flex;
justify-content: space-between;
align-items: center;
`;
const Button = styled.button`
height: 35px;
padding: 5px 20px;
border-radius: 20px;
border: none;
background: black;
color: white;
font-size: 18px;
cursor: pointer;
`;
const Ul = styled.ul`
button {
padding: 5px 10px;
margin: 2px 0;
border-radius: 4px;
border: none;
background: #eeeeee;
cursor: pointer;
}
`;
square.tsx
import React from "react";
import styled from "styled-components";
const Square = ({
value,
onClick,
}: {
value: string;
onClick: (i: React.MouseEvent<HTMLButtonElement>) => void;
}): JSX.Element => {
return <Button onClick={onClick}>{value}</Button>;
};
export default Square;
const Button = styled.button`
width: 100px;
height: 100px;
margin: -1px -1px 0 0;
background: #fff;
border: 1px solid #999;
font-size: 40px;
font-weight: bold;
line-height: 34px;
cursor: pointer;
&:focus {
outline: none;
}
`;