-
[React document] tutorial : Adding Time TravelFront-end/React.js 2020. 2. 17. 14:11
https://reactjs.org/tutorial/tutorial.html#adding-time-travel
Storing a History of Moves
더보기If we mutated the squares array, implementing time travel would be very difficult.
However, we used slice() to create a new copy of the squares array after every move, and treated it as immutable. This will allow us to store every past version of the squares array, and navigate between the turns that have already happened.
We’ll store the past squares arrays in another array called history. The history array represents all board states, from the first to the last move, and has a shape like this:
Now we need to decide which component should own the history state.
square 배열이 가변적이었다면 time travel을 구현하는 것은 어려웠을 것이다. 그러나 수를 둔 후에 squares 배열의 새로운 복사본을 만들기 위해 slice() 함수를 이용함으로써 불변성을 유지했다. 모든 squares 배열의 이전 버전들을 저장하고 발생한 턴을 추적할 수 있게 한다.
history라는 또 다른 배열에 이전 버전의 squares 배열을 저장하자. history 배열은 처음부터 마지막 수까지 모든 board 상태들을 나타내고 모양은 다음과 같다.
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 state를 가질지 결정해야 한다.
Lifting State Up, Again
더보기We’ll want the top-level Game component to display a list of past moves. It will need access to the history to do that, so we will place the history state in the top-level Game component.
Placing the history state into the Game component lets us remove the squares state from its child Board component. Just like we “lifted state up” from the Square component into the Board component, we are now lifting it up from the Board into the top-level Game component. This gives the Game component full control over the Board’s data, and lets it instruct the Board to render previous turns from the history.
First, we’ll set up the initial state for the Game component within its constructor:
Next, we’ll have the Board component receive squares and onClick props from the Game component. Since we now have a single click handler in Board for many Squares, we’ll need to pass the location of each Square into the onClick handler to indicate which Square was clicked. Here are the required steps to transform the Board component:
- Delete the constructor in Board.
- Replace this.state.squares[i] with this.props.squares[i] in Board’s renderSquare.
- Replace this.handleClick(i) with this.props.onClick(i) in Board’s renderSquare.
The Board component now looks like this:
We’ll update the Game component’s render function to use the most recent history entry to determine and display the game’s status:
Since the Game component is now rendering the game’s status, we can remove the corresponding code from the Board’s render method. After refactoring, the Board’s render function looks like this:
Finally, we need to move the handleClick method from the Board component to the Game component. We also need to modify handleClick because the Game component’s state is structured differently. Within the Game’s handleClick method, we concatenate new history entries onto history.
At this point, the Board component only needs the renderSquare and render methods. The game’s state and the handleClick method should be in the Game component.
이전의 움직임들에 대한 목록을 보여주려면 top-level 게임 컴포넌트가 필요하다. history를 이용해야 하므로 최상위에 있는 Game 컴포넌트에 history state를 두자.
Game 컴포넌트에 history state를 두면 포넌트의 자식인 Board 컴포넌트에서 squares state를 삭제해도 된다. Square 컴포넌트에서 Board 컴포넌트로 끌어올렸던 것처럼 Board 컴포넌트에서 최상위 Game 컴포넌트로 끌어올린다. Game 컴포넌트에게 Board의 데이터에 대한 제어권을 주고 Board가 history에 있는 이전 기록들을 렌더링할 수 있게 한다.
먼저 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를 받도록 한다. 여러 square에 대해 Board가 하나의 클릭 핸들러를 갖게 했으므로 어떤 Square가 클릭되었는지 알려주기 위해 onClick 핸들러에게 각 스퀘어에 대한 위치를 전달해주어야 한다. Board 컴포넌트를 변형시키기 위해 필요한 스텝들이 있다.
- 보드 컴포넌트의 생성자 삭제
- this.state.squares[i]를 this.props.squares[i]로 변경
- this.handelClick(i)를 this.props.onClick(i)로 변경
보드 컴포넌트는 다음과 같다.
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 entry를 사용하도록 Game 컴포넌트의 렌더 함수를 업데이트해서 game의 status를 확인하자.
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 컴포넌트가 game의 상태를 렌더링하고 있기 때문에 이에 해당하는 Board의 렌더함수를 제거해도 된다. 리팩토링 후에 보드 렌더 함수는 다음과 같다.
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> ); }
handleClick 함수를 Board 컴포넌트에서 Game 컴포넌트로 옮겨야 한다. 또한 Game 컴포넌트의 state가 다르게 구조화되었기 때문에 handleClick를 수정해야 한다. Game 컴포넌트의 handleClick 함수에서 새로운 history를 기존 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()보다는 concat()을 권장함
이 시점에서 보드 컴포넌트는 renderSquare와 render 함수만 필요하다. 게임의 state와 handleClick 메소드는 Game 컴포넌트에 있어야 한다.
Showing the Past Moves
더보기Since we are recording the tic-tac-toe game’s history, we can now display it to the player as a list of past moves.
We learned earlier that React elements are first-class JavaScript objects; we can pass them around in our applications. To render multiple items in React, we can use an array of React elements.
In JavaScript, arrays have a map() method that is commonly used for mapping data to other data, for example:
Using the map method, we can map our history of moves to React elements representing buttons on the screen, and display a list of buttons to “jump” to past moves.
Let’s map over the history in the Game’s render method:
For each move in the tic-tac-toes’s game’s history, we create a list item <li> which contains a button <button>. The button has a onClick handler which calls a method called this.jumpTo(). We haven’t implemented the jumpTo() method yet. For now, we should see a list of the moves that have occurred in the game and a warning in the developer tools console that says:
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.
틱택토 게임의 history를 기록하고 있으므로 이제 플레이어에게 이전에 두었던 수의 목록들을 보여줄 수 있다.
이전에 React 엘리먼트들은 어플리케이션에 전달할 수 있는 자바스크립트 객체들의 클래스형이라고 배웠다. 리액트에서 다양한 아이템들을 렌더링하기 위해 리액트 엘리먼트 배열을 사용한다.
자바스크립트에서 배열은 데이터를 다른 데이터와 매핑할 수 있는 map() 함수를 가진다. 예를 들어
const numbers = [1, 2, 3]; const doubled = numbers.map(x => x * 2); // [2, 4, 6]
map 함수를 이용하여 게임의 이동기록을 화면에 나타내는 리액트 엘리먼트에 매핑하고 과거 이동으로 점프할 수 있는 버튼의 목록들을 나타낼 수 있다. Game에 렌더 함수에서 기록을 매핑해보자.
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> ); }
틱택토 게임 기록에서 각각의 움직임에 대해 버튼태그를 포함하는 <li> 배열을 생성한다. 버튼은 this.jumpTo() 함수를 호출하는 onClick 핸들러를 가진다. 아직 jumpTo() 구현은 하지 않았다.
이제 이동목록이 게임에서 나타나는 것을 볼 수 있다. 그리고 개발자도구에서 경고가 발생한다.
경고 : 배열이나 이터레이터의 자식들은 고유의 “key” prop을 가지고 있어야 한다. “Game”의 render 함수를 확인해라.
Picking a Key
더보기When we render a list, React stores some information about each rendered list item. When we update a list, React needs to determine what has changed. We could have added, removed, re-arranged, or updated the list’s items.
In addition to the updated counts, a human reading this would probably say that we swapped Alexa and Ben’s ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and does not know what we intended. Because React cannot know our intentions, we need to specify a key property for each list item to differentiate each list item from its siblings. One option would be to use the strings alexa, ben, claudia. If we were displaying data from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.
When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key. If the current list has a key that didn’t exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved. Keys tell React about the identity of each component which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.
key is a special and reserved property in React (along with ref, a more advanced feature). When an element is created, React extracts the key property and stores the key directly on the returned element. Even though key may look like it belongs in props, key cannot be referenced using this.props.key. React automatically uses key to decide which components to update. A component cannot inquire about its key.
It’s strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do.
If no key is specified, React will present a warning and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list’s items or inserting/removing list items. Explicitly passing key={i} silences the warning but has the same problems as array indices and is not recommended in most cases.
Keys do not need to be globally unique; they only need to be unique between components and their siblings.
배열을 렌더링할 때 리액트는 렌더링하는 배열의 아이템에 대한 정보를 저장한다. 배열을 업데이트할 때 리액트는 무엇이 변했는지 결정한다. 배열의 아이템들을 더하거나 제거하거나 재정렬하거나 업데이트할 수 있다.
<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>
업데이트된 counts에 더해서 이것을 읽는 사람은 Alexa와 Ben의 순서가 변경되었고 Alexa와 Ben 사이에 Claudia가 추가되었다고 말할 것이다. 그러나 리액트는 컴퓨터 프로그램이고 우리가 의도하는 것을 모른다. 리액트는 우리의 의도를 모르기 때문에 key property를 지정하여 각 아이템들이 다른 것들과 다르다고 알려주어야 한다. 한 가지 방법은 alexa, ben, claudia 문자열을 사용하는 것이다. 데이터베이스로부터 데이터를 보여준다면 Alexa, Ben, Claudia의 데이터베이스 아이디가 key로 사용될 것이다.
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
배열이 리-렌더링될 때 리액트는 각 리스트 아이템의 키를 받고 이전의 배열의 아이템에서 일치하는 키를 찾는다. 현재
리스트에서 이전에 존재하지 않던 키가 있다면, 리액트는 컴포넌트를 생성한다. 현재 배열이 이전 배열에 존재했던 키를 잃어버렸다면 리액트는 이전 컴포넌트를 파괴한다. 두 키가 매칭된다면 일치하는 컴포넌트는 움직인다. Key는 리액트에게 재-렌더러 사이의 state를 유지하게 해주는 각각의 컴포넌트의 동일성에 대해 말해준다. 컴포넌트의 key가 변화하면 컴포넌트는 제거되고 새로운 state를 재생성한다.
리액트에서 심화된 특징인 ref와 동일하게 Key는 특별하고 지정된 property이다. element가 생성될 때 리액트는 key property를 추출하고 반환되는 element에 직접 키를 저장한다. key가 props에 속하는 것 같지만 key는 this.props.key를 사용하여 참조될 수 없다. 리액트는 어떤 컴포넌트를 업데이트할지 결정하기 위해 자동으로 키를 사용한다. 컴포넌트는 key를 조회할 수 없다.
동적 배열을 생성할 때마다 적절한 키를 할당하는 것을 강력하게 권장한다. 적절한 키가 없다면 데이터 재구성을 고려해야 할 수 있다.
키가 지정되지 않았다면 리액트는 경고창을 나타내고 기본값으로 배열 인덱스를 키로 사용한다. 배열 인덱스를 키로 사용하는 것은 배열의 아이템들을 재정렬하거나 아이템을 삽입/제거할 때 문제를 발생시킬 수 있다. 명시적으로 key={i}를 전달하는것은 경고를 안보이게 하지만 위와 동일한 문제를 갖고 있으므로 대부분의 경우에서 권장되지 않는다.
키는 전역에서 유니크할 필요는 없으며 컴포넌트와 관련된 아이템 사이에서 고유해야 한다.
Implementing Time Travel
더보기In the tic-tac-toe game’s history, each past move has a unique ID associated with it: it’s the sequential number of the move. The moves are never re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.
In the Game component’s render method, we can add the key as <li key={move}> and React’s warning about keys should disappear:
Clicking any of the list item’s buttons throws an error because the jumpTo method is undefined. Before we implement jumpTo, we’ll add stepNumber to the Game component’s state to indicate which step we’re currently viewing.
First, add stepNumber: 0 to the initial state in Game’s constructor:
Next, we’ll define the jumpTo method in Game to update that stepNumber. We also set xIsNext to true if the number that we’re changing stepNumber to is even:
We will now make a few changes to the Game’s handleClick method which fires when you click on a square.
The stepNumber state we’ve added reflects the move displayed to the user now. After we make a new move, we need to update stepNumber by adding stepNumber: history.length as part of the this.setState argument. This ensures we don’t get stuck showing the same move after a new one has been made.
We will also replace reading this.state.history with this.state.history.slice(0, this.state.stepNumber + 1). This ensures that if we “go back in time” and then make a new move from that point, we throw away all the “future” history that would now become incorrect.
틱택토 게임의 기록에서 각 이전의 이동은 이동의 순차적인 숫자와 관련된 고유한 ID를 갖고 있다. 이동은 절대 재정렬되거나 삭제되거나 중간에 삽입되지 않으므로 인덱스를 키로 사용하는 것은 안전하다.
Game 컴포넌트의 렌더함수에서 <li key={move}>로 키를 추가할 수 있고 키에 대한 리액트의 경고는 사라질 것이다.
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> ); });
배열의 아무런 버튼을 누르면 jumpTo 함수가 정의되지 않았으므로 에러가 발생한다. jumpTo를 구현하기 전에 현재 어떤 순서인지 알려주기 위해 Game 컴포넌트의 상태에 stepNumber를 더해줄 것이다.
먼저 stepNumber: 0을 초기 상태로 더해주어라.
class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], stepNumber: 0, xIsNext: true, }; }
다음에는 stepNumber를 업데이트하기 위해 Game 컴포넌트에 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 }
square를 클릭할 때마다 실행되는 Game 컴포넌트의 handleClick 함수에 몇 가지 변화를 줄 것이다.
더해지는 stepNumber state는 유저에게 표시되는 이동을 반영한다. 새로 이동을 시키면 stepNumber를 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, }); }
Finally, we will modify the Game component’s render method from always rendering the last move to rendering the currently selected move according to stepNumber:
마지막으로 Game 컴포넌트의 렌더함수를 수정하여 마지막 이동을 렌더링 하는 대신 stepNumber에 맞는 선택된 이동을 렌더링할 것이다.
render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); // the rest has not changed
게임의 기록에서 어떤 차례를 선택한다면 틱택토 게임판을 즉시 업데이트해서 단계가 발생한 직후의 보드를 보여주어야 한다.
Wrapping Up
더보기Congratulations! You’ve created a tic-tac-toe game that:
- Lets you play tic-tac-toe,
- Indicates when a player has won the game,
- Stores a game’s history as a game progresses,
- Allows players to review a game’s history and see previous versions of a game’s board.
Nice work! We hope you now feel like you have a decent grasp on how React works.
Check out the final result here: Final Result.
If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game which are listed in order of increasing difficulty:
- Display the location for each move in the format (col, row) in the move history list.
- Bold the currently selected item in the move list.
- Rewrite Board to use two loops to make the squares instead of hardcoding them.
- Add a toggle button that lets you sort the moves in either ascending or descending order.
- When someone wins, highlight the three squares that caused the win.
- When no one wins, display a message about the result being a draw.
Throughout this tutorial, we touched on React concepts including elements, components, props, and state. For a more detailed explanation of each of these topics, check out the rest of the documentation. To learn more about defining components, check out the React.Component API reference.
ㅊㅋㅊㅋ! 틱택토 게임 만들기 끝!
'Front-end > React.js' 카테고리의 다른 글
react-admin : Field Components (0) 2020.03.03 react-admin 메모 : Admin (0) 2020.03.02 react-admin 메모 : data provider (0) 2020.03.02 [React document] tutorial : Completing the game (0) 2020.02.16 [React document] tutorial : Overview (0) 2020.02.16