리액트 공식 자습서로 처음 제시하는 과제는 ‘틱택토’ 게임입니다. 자습서라는 이름에 맞지 않게 이해하기 어렵고 구현 난이도가 높은 편입니다.
이 자습서의 구성은 먼저 자습서가 제시하는 범위의 예제를 완성한 후에, 추가 구현사항 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개의 댓글