자습서: React 시작하기

리액트 공식 자습서로 처음 제시하는 과제는 ‘틱택토’ 게임입니다. 자습서라는 이름에 맞지 않게 이해하기 어렵고 구현 난이도가 높은 편입니다.

이 자습서의 구성은 먼저 자습서가 제시하는 범위의 예제를 완성한 후에, 추가 구현사항 6가지를 스스로 풀어보도록 하고 있습니다.

기존 코드는 다음과 같습니다.

이 게임에서 이미 구현된 기능은 다음과 같습니다.

  • 틱택토를 할 수 있게 해주고,
  • 게임에서 승리했을 때를 알려주며,
  • 게임이 진행됨에 따라 게임 기록을 저장하고,
  • 플레이어가 게임 기록을 확인하고 게임판의 이전 버전을 볼 수 있도록 허용합니다.

 

추가 요구사항은 다음과 같습니다.

  1. 이동 기록 목록에서 특정 형식(행, 열)으로 각 이동의 위치를 표시해주세요.
  2. 이동 목록에서 현재 선택된 아이템을 굵게 표시해주세요.
  3. 사각형들을 만들 때 하드코딩 대신에 두 개의 반복문을 사용하도록 Board를 다시 작성해주세요.
  4. 오름차순이나 내림차순으로 이동을 정렬하도록 토글 버튼을 추가해주세요.
  5. 승자가 정해지면 승부의 원인이 된 세 개의 사각형을 강조해주세요.
  6. 승자가 없는 경우 무승부라는 메시지를 표시해주세요.

 

제시된 코드를 바탕으로 위의 요구 사항을 구현해보도록 하겠습니다. 저의 풀이 방법에는 한계가 있으므로 개선점이 있다면 고치도록 하겠습니다.

 

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.stepNumbermove 가 같다면 현재 선택된 아이템입니다. 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 판으로 마지막 턴이 될 때까지 승리자가 나오지 않으면 비긴 것입니다. 따라서 stepNumber9인데 우승자가 나오지 않았다면 비긴 것으로 보고 메시지를 출력합니다.

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!"
}

 

완성본


문의 | 코멘트 또는 yoonbumtae@gmail.com  donaricano-btn

카테고리: React

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다