시간여행 추가 부분을 추가하려고 하니, 문서 길이가 너무 길어서 그런지 오류가 발생한다.
그래서 어쩔 수 없이 2로 새로 글로 작성하게 되었다.
시간여행 추가
이제 마지막으로, 게임 플레이 중 이전 턴으로 돌아갈 수 있도록 해보자.
턴 저장하기
우리가 squares
배열을 .slice()
를 통해서 새로운 복사본을 만들면서 사용하기 때문에 우리는 각 턴마다의 상태를 저장할 수 있다. 우리는 history
라는 새로운 배열에 이전 턴의 squares
배열을 저장할 것이다. history
배열은 처음부터 마지막 턴까지의 게임 보드판의 모습을 다 저장하게 될 것이다. 코드로 보자면 아래와 같은 형태가 될 것이다:
history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
우리는 history
의 상태를 어떤 컴포넌트에서 관리해야 할 지 정해야 한다.
한 번 더 상태 전달하기
앞서 말한 history
의 상태를 제일 최상단 컴포넌트인 Game
컴포넌트에서 관리하려고 한다.
그래서 state
를 부모(parent) 컴포넌트에게 전달하기 에서Square
컴포넌트의 상태를 Board
로 props
를 통해 전달한 것처럼, Board
의 상태를 Game
으로 props
를 통해 전달할 것이다.
( 쉽게 설명하자면, 하위 컴포넌트의 state 로 쓰던 것-> 상위 컴포넌트에서 관리 및 props 를 통해 하위 컴포넌트로 전달)
먼저, 우리는 Game
컴포넌트에 생성자를 추가하자:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
다음으로, Board
컴포넌트가 Game
컴포넌트로부터 squares
와 onClick
을 props
로 받게 하자.
아래의 단계를 통해 Board
컴포넌트를 수정하자:
Board
생성자 삭제Board
의renderSquare
함수에서this.state.squares[i]
를this.props.squares[i]
로 교체Board
의renderSquare
함수에서this.handleClick(i)
를this.props.onClick(i)
로 교체
그러면 Board
컴포넌트는 이렇게 될 것이다:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
그리고 게임 진행 상태와 승자 파악하는 부분을 history
의 최근 상태를 이용하도록 수정하자.Game
컴포넌트의 render
함수를 아래처럼 바꾸자:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
이제 Game
컴포넌트 안에서 status
를 관리하기 때문에 Board
에 있는 status
부분을 지우자.Board
의 render
함수를 아래처럼 수정하면 된다:
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
마지막으로, Board
컴포넌트에 있던 handleClick
함수를 Game
컴포넌트로 옮길 것이다. 이제 history
를 가지고 보여줄 것이므로 아래처럼 좀 수정해야한다:
handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
여기서 잠깐! push() vs concat()
우리는 턴이 지날 때마다 history
배열에 추가하기 위해서 concat()
을 사용했다. 왜 push()
를 안 쓰고 concat()
을 사용했을까?
push() : The push() method adds one or more elements to the end of an array and returns the new length of the array. (MDN)
var arr1 = [‘a’, ‘b’, ‘c’];
var arr2 = [‘d’, ‘e’, ‘f’];
var arr3 = arr1.push(arr2);
console.log(arr3); // 4
console.log(arr1); // [“a”, “b”, “c”, [“d”, “e”, “f”]]
concat() : The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array. (MDN)
var arr1 = [‘a’, ‘b’, ‘c’];
var arr2 = [‘d’, ‘e’, ‘f’];
var arr3 = arr1.concat(arr2);
console.log(arr3); //[“a”, “b”, “c”, “d”, “e”, “f”]
쉽게 설명하자면, push()
를 쓰면 기존 배열에 영향이 있고, concat()
은 영향이 없다. 우리는 이전 history
를 유지해야하므로 concat()
을 쓰는 것이다.
Board
컴포넌트에서 handleClick
을 Game
컴포넌트로 옮겼으므로, Board
에는 이제 renderSquare
와 render
함수만 있으면 된다.
이전 턴 보여주기
이제 우리는 tic tac toe 게임의 히스토리를 저장하고 있기 때문에, 플레이어에게 과거 턴들을 리스트로 보여줄 수 있다.
자바스크립트에서 배열은 아래와 같이 배열 내 데이터를 매핑하기 위해서 map()
메소드를 쓸 수 있다:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
우리는 map()
함수를 가지고 history
배열을 화면에 보여줄 버튼으로 매핑할 수 있다.Game
의 render
함수에서 아래와 같이 수정하자:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
턴이 지날 때마다, 우리는 버튼을 포함한 태그를 만들게 된다. 버튼은 this.jumpTo()
라는 함수를 호출하는데, 이 함수는 아직 만들지 않았기 때문에, 아래와 같은 경고창이 뜰 것이다:
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.
키(Key) 고르기
우리가 리스트를 화면에 보여줄 때, 리액트는 화면에 보여줄 리스트들의 정보를 저장하고 있다. 우리가 리스트의 값을 업데이트하면, 리액트는 무엇이 바뀌었는 지 알아내어서 다시 화면에 보여주게 된다.
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
위의 리스트를 아래와 같이 바꾼다고 생각해보자.
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
우리는 Alexa
와 Ben
의 순서가 바뀌고 이 사이에 Claudia
가 추가되었다는 것을 알 수 있다. 하지만 리액트는 우리의 의도를 쉽게 알 수가 없기 때문에, 우리는 비슷한 리스트들 중에 key
를 통해서 무엇이 다른지 리액트에게 알려줄 필요가 있다.
예를 들면 Alexa',
Ben,
Claudia` 의 데이터베이스 ID를 알고 있다면 다음과 같이 표현할 수 있다:
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
이렇게 key
를 가지고 만들게되면, 리액트는 리스트 아이템의 키가 이전에 있던 것인지 확인하고 다시 화면에 그리게 된다.
이전에 없던 key
면 리액트는 새로운 컴포넌트를 만든다. key
가 없어졌다면 리액트는 만들었던 컴포넌트를 제거한다.
그리고 기존의 key
가 변경되면 리액트는 기존의 컴포넌트를 없애고 새로운 컴포넌트를 만들게 된다.
key
가 props
에 종속되어 this.props.key
처럼 사용된다고 생각할 수도 있지만 그렇지 않다. 컴포넌트는 key
에 this.props.key
처럼 접근할 수 없고, 리액트가 알아서 key
를 통해 업데이트 할 컴포넌트를 결정하게 된다.
동적 리스트(dynamic lists)를 만들 때, 적절한 key
를 쓰는 것을 추천한다.
key
를 정하지 않으면, 앞서 본 것처럼 리액트는 경고창을 띄워주고 배열의 index를 기본 key
로 사용한다.
배열의 index를 key
로 쓰는 것은 리스트를 재정렬하거나 기존 배열 가운데 새롭게 추가/삭제하게 될 경우 문제가 될 수 있다.
시간 여행 구현하기
우리가 만든 tic tac toe 게임에서는 플레이어의 턴이 재정렬되거나 중간에 추가, 삭제 될 일은 없기 때문에 우리는 history
의 index를 key
로 사용할 것이다.
Game
컴포넌트의 render
함수에서 <li key={move}>
와 같이 key
를 추가하자:
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
그리고 아직 `this.jumpTo(move)` 에서 `jumpTo` 함수를 만들지 않았다. 이 함수를 만들기 전에 `Game` 컴포넌트에 `stepNumber`를 추가할 것이다.
먼저, `Game`의 생성자에 `stepNumber: 0`을 추가하자:
class Game extends React.Component {
constructor(props) {
super(props);
[this.state](this.state) = {
history: \[{
squares: Array(9).fill(null),
}\],
stepNumber: 0,
xIsNext: true,
};
}
그리고 stepNumber
를 업데이트 해줄 jumpTo
함수를 만들자. 그리고 stepNumber
가 짝수 일 때, xIsNext
가 true
가 되도록 수정하자:
handleClick(i) {
// this method has not changed
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
render() {
// this method has not changed
}
이제 우리는 Game
의 handleClick
함수를 조금 바꿔야 한다.
먼저, 플레이어가 사각형을 클릭했을 때 stepNumber
가 업데이트 되도록 this.setState
부분에 stepNumber: history.length,
를 추가하자. 이걸 추가해야 플레이어가 사각형을 누를 때마다 턴이 증가하게 된다.
그리고 this.state.history
를 this.state.history.slice(0, this.state.stepNumber + 1)
로 바꾸자.
이로써 게임을 플레이하다가 이전 턴으로 돌아갔을 경우에 그 이후의 턴들의 값을 다 날려버릴 수 있다.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
마지막으로, Game
컴포넌트의 render
함수를 수정해서 stepNumber
에 맞는 현재 상태를 보여주도록 하자:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
// the rest has not changed
이제, 턴을 표시하는 리스트 버튼을 누르게 되면 게임 화면이 바로 그 때의 턴으로 바뀌게 된다.
마무리
이제 드디어 tic tac toe 게임을 완성했다.
여기서 좀 아쉽다면 아래의 것들을 추가해보는 것도 공부에 도움이 될 것이다:
- 히스토리에 선택한 (행,열) 표시하기
- 히스토리에 현재 상태는 진하게 표시하기
Board
컴포넌트calculateWinner
함수에서 하드코딩한 부분 2중 루프로 다시 작성하기- 오름차순/내림차순 으로 히스토리를 볼 수 있는 토글 버튼 만들기
- 플레이어가 이겼을 경우, 승리하게 된 3개의 사각형을 하이라이트 해주기
- 승패가 안가려지고 사각형이 다 채워졌을 경우, 비겼다는 메세지 보여주기
기초 문법, 리액트 컴포넌트와 같은 문서들이 많으니 참고하면서 진행하면 될 것 같다.
이상 뿅!
'개발 > React.js' 카테고리의 다른 글
리액트 공식 튜토리얼을 쉽게 해보자 (0) | 2019.03.29 |
---|---|
React.js 개발 환경 (0) | 2019.03.19 |