리액트 공식 자습서로 처음 제시하는 과제는 ‘틱택토’ 게임입니다. 자습서라는 이름에 맞지 않게 이해하기 어렵고 구현 난이도가 높은 편입니다.
이 자습서의 구성은 먼저 자습서가 제시하는 범위의 예제를 완성한 후에, 추가 구현사항 6가지를 스스로 풀어보도록 하고 있습니다.
기존 코드는 다음과 같습니다.
이 게임에서 이미 구현된 기능은 다음과 같습니다.
- 틱택토를 할 수 있게 해주고,
- 게임에서 승리했을 때를 알려주며,
- 게임이 진행됨에 따라 게임 기록을 저장하고,
- 플레이어가 게임 기록을 확인하고 게임판의 이전 버전을 볼 수 있도록 허용합니다.
추가 요구사항은 다음과 같습니다.
- 이동 기록 목록에서 특정 형식(행, 열)으로 각 이동의 위치를 표시해주세요.
- 이동 목록에서 현재 선택된 아이템을 굵게 표시해주세요.
- 사각형들을 만들 때 하드코딩 대신에 두 개의 반복문을 사용하도록 Board를 다시 작성해주세요.
- 오름차순이나 내림차순으로 이동을 정렬하도록 토글 버튼을 추가해주세요.
- 승자가 정해지면 승부의 원인이 된 세 개의 사각형을 강조해주세요.
- 승자가 없는 경우 무승부라는 메시지를 표시해주세요.
제시된 코드를 바탕으로 위의 요구 사항을 구현해보도록 하겠습니다. 저의 풀이 방법에는 한계가 있으므로 개선점이 있다면 고치도록 하겠습니다.
1. 이동 기록 목록에서 특정 형식(행, 열)으로 각 이동의 위치를 표시해주세요.
먼저 현재 놓인 O, X 가 어느 위치인지 알아내기 위한 변수를 state 에 추가합니다.
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
currentSquareIndex: null, // question 1
}],
stepNumber: 0,
xIsNext: true,
isDisplayOrderByAsc: true, // question 4
}
}
// ... 이하 생략 ... //
}
배열의 인덱스를 통해 행렬 좌표를 알아내는 함수를 작성합니다.
// 문제 1
function getCoordBySquareIndex(i) {
return {
x: Math.floor(i / 3),
y: i % 3,
}
}
목록이 행렬 정보를 표시하도록 변경합니다.
const moves = history.map((step, move) => {
// 1. 이동 기록 목록에서 특정 형식(행, 열)으로 각 이동의 위치를 표시해주세요.
const { x, y } = getCoordBySquareIndex(step.currentSquareIndex); // 1
const desc = move ?
`Go to move #${move} - (${x}, ${y})` :
'Go to game start';
// ... 이하 생략 ... //
2. 이동 목록에서 현재 선택된 아이템을 굵게 표시해주세요.
Game 컴포넌트에서 this.state.stepNumber와 move 가 같다면 현재 선택된 아이템입니다. 1번 문제의 moves 함수의 render() 부분에서 style 객체를 이용해 다음과 같이 바꿔줍니다.
const moves = history.map((step, move) => {
// ... //
// *2. 이동 목록에서 현재 선택된 아이템을 굵게 표시해주세요.
return (
<li key={move}>
<button
onClick={() => this.jumpTo(move)}
style={ {"fontWeight": (this.state.stepNumber === move ? "bold" : "normal")} }
>
{desc}
</button>
</li>
);
});

3. 사각형들을 만들 때 하드코딩 대신에 두 개의 반복문을 사용하도록 Board를 다시 작성해주세요.
Board 컴포넌트의 render() 함수를 변경합니다.
여기서 반복문이란 for, while 등 기존 프로그래밍 개념에서 존재하는 구문을 말합니다. 반복문 대신 forEach, map 등의 배열 메소드를 사용하거나 반복문을 한 개로 줄일 수도 있지만 요구사항에서 반복문 두 개를 사용하라고 했으므로 두 개를 사용하겠습니다.
render() {
// 3. 사각형들을 만들 때 하드코딩 대신에 두 개의 반복문을 사용하도록 Board를 다시 작성해주세요.
const boardRows = [];
for(let i = 0, len = Math.sqrt(this.props.squares.length); i < len; i++) {
const innerCols = [];
for(let j = 0; j < len; j++) {
innerCols.push(this.renderSquare((i * len) + j));
}
boardRows.push(
<div className="board-row" key={`row-${i}`}>
{innerCols}
</div>
);
}
return (
<div>
{boardRows}
</div>
);
}
여기서 반복문을 통해 요소를 추가할 때마다 key 값을 요구하므로 적어줍니다.
또 renderSquare에서도 key값이 요구되므로 해당 속성을 추가합니다.
renderSquare(i) {
const isWinningIndex = this.props.winningIndex && this.props.winningIndex.indexOf(i) !== -1
return (
<Square
key={`button-${i}`}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
backgroundColor={isWinningIndex && "deepskyblue"}
/>
);
}
4. 오름차순이나 내림차순으로 이동을 정렬하도록 토글 버튼을 추가해주세요.
오름/내림차순 정렬을 토글할 버튼을 Game 컴포넌트의 렌더링 부분에 추가합니다. 그리고 클릭 시 이벤트를 부여하겠습니다.
<button onClick={toggleMoves}>
{this.state.isDisplayOrderByAsc ? "오름차순 ▲" : "내림차순 ▼"}
</button>
토글을 하는 방법은 오름차순이라면 기존의 moves 배열을 그대로 놔두고, 내림차순이라면 배열을 reverse() 하여 뒤집는 방법이 있습니다. 그리고 이 조건을 토글하는 방법은 state에 오름차순인지 내림차순인지 알려주는 boolean 변수를 추가한 뒤 버튼이 클릭될 때마다 변수의 true/false를 토글하는 이벤트를 추가하면 됩니다.
// Game 컴포넌트
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
currentSquareIndex: null, // question 1
}],
stepNumber: 0,
xIsNext: true,
isDisplayOrderByAsc: true, // question 4
}
}
// Game 컴포넌트 render() 에 있는 기존 move 함수의 밑에 추가
if(!this.state.isDisplayOrderByAsc) {
moves.reverse()
}
// 4. 오름차순이나 내림차순으로 이동을 정렬하도록 토글버튼을 추가해주세요.
const toggleMoves = () => {
this.setState({
isDisplayOrderByAsc: !this.state.isDisplayOrderByAsc
})
}

오름차순

내림차순
5. 승자가 정해지면 승부의 원인이 된 세 개의 사각형을 강조해주세요.
먼저 calculateWinner 함수의 변경이 필요합니다. 특정 플레이어가 이겼을 경우 어느 블록이 승리의 원인이 되었는지는 알려주는 winningIndex 변수를 추가해서 객체 형태로 리턴하도록 변경합니다.
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];
// 한 라인이 O , X 일치하는 경우
if (squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]) {
return {
player: squares[a],
winningIndex: [a, b, c]
};
}
}
return null;
}
승자 판별 부분에서 winningIndex 변수를 생성하고 정보가 props를 타고 Square 까지 넘어가도록 파도타기를 합니다.
Game 컴포넌트
let status;
let winningIndex; // 이기게 된 원인이 된 스퀘어 번호들
// null 아니고 O, X 인 경우
// 6. 승자가 없는 경우 무승부라는 메시지를 표시해주세요.
if (winner) {
status = `Winner: ${winner.player}`;
// 5. 승자가 정해지면 승부의 원인이 된 세 개의 사각형을 강조해주세요.
winningIndex = winner.winningIndex
} else if (this.state.stepNumber !== current.squares.length) {
status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`
} else {
status = "Draw game!"
}
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
winningIndex={winningIndex}
/>
</div>
Board 컴포넌트
renderSquare(i) {
const isWinningIndex = this.props.winningIndex && this.props.winningIndex.indexOf(i) !== -1
return (
<Square
key={`button-${i}`}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
backgroundColor={isWinningIndex && "deepskyblue"}
/>
);
}
Square 컴포넌트
function Square({ value, onClick, backgroundColor }) {
return (
<button
className="square"
onClick={onClick}
style={{backgroundColor}}>
{value}
</button>
);
}

6. 승자가 없는 경우 무승부라는 메시지를 표시해주세요.
틱택토 게임은 3 * 3 = 9 판으로 마지막 턴이 될 때까지 승리자가 나오지 않으면 비긴 것입니다. 따라서 stepNumber가 9인데 우승자가 나오지 않았다면 비긴 것으로 보고 메시지를 출력합니다.
let status;
let winningIndex; // 이기게 된 원인이 된 스퀘어 번호들
// null 아니고 O, X 인 경우
// 6. 승자가 없는 경우 무승부라는 메시지를 표시해주세요.
if (winner) {
status = `Winner: ${winner.player}`;
// 5. 승자가 정해지면 승부의 원인이 된 세 개의 사각형을 강조해주세요.
winningIndex = winner.winningIndex
} else if (this.state.stepNumber !== current.squares.length) {
status = `Next player: ${this.state.xIsNext ? 'X' : 'O'}`
} else {
status = "Draw game!"
}






0개의 댓글