-
[React document] tutorial : Completing the gameFront-end/React.js 2020. 2. 16. 18:57
https://reactjs.org/tutorial/tutorial.html#completing-the-game
Tutorial: Intro to React – React
A JavaScript library for building user interfaces
reactjs.org
Lifting State Up
더보기Currently, each Square component maintains the game’s state. To check for a winner, we’ll maintain the value of each of the 9 squares in one location.
We may think that Board should just ask each Square for the Square’s state. Although this approach is possible in React, we discourage it because the code becomes difficult to understand, susceptible to bugs, and hard to refactor. Instead, the best approach is to store the game’s state in the parent Board component instead of in each Square. The Board component can tell each Square what to display by passing a prop, just like we did when we passed a number to each Square.
To collect data from multiple children, or to have two child components communicate with each other, you need to declare the shared state in their parent component instead. The parent component can pass the state back down to the children by using props; this keeps the child components in sync with each other and with the parent component.
Lifting state into a parent component is common when React components are refactored — let’s take this opportunity to try it out.
Add a constructor to the Board and set the Board’s initial state to contain an array of 9 nulls corresponding to the 9 squares:
현재 각 Square 컴포넌트는 게임의 state를 갖고 있다. 승자를 확인하기 위해 우리는 9개 스퀘어의 값을 한 곳으로 모을 것이다.
우리는 Board가 단순히 각 Square에서 state를 요청해야 한다고 생각할 수도 있지만 이러한 접근법은 가능하기는 하지만 코드 이해를 어렵게 하고 버그에 취약하며 리팩토링이 어렵게 한다. 대신에 가장 좋은 접근법은 게임의 state를 각 Square보다 부모 Board 컴포넌트에 저장하는 것이다. Board 컴포넌트는 각 Square에 prop을 전달함으로써 무엇을 표시할지 말할 수 있다.
다수의 자식 컴포넌트로부터 정보를 모으기 위해 또는 두개의 자식 컴포넌트끼리 서로 통신하게 하기 위해 부모 컴포넌트에 공유되는 state를 선언해야 한다. 부모 컴포넌트는 props를 이용하여 자식에게 state를 전달한다. 이것은 자식 컴포넌트들이 서로 또는 부모와 동기화되도록 만든다.
state를 부모 컴포넌트로 끌어올리는 것은 리액트 컴포넌트들을 리팩토링 할 때 흔히 사용된다.
Board 컴포넌트에 생성자를 만들고 Board의 초기 상태를 9개의 정사각형에 해당하는 9개의 null을 갖는 배열로 설정하자.
class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } renderSquare(i) { return <Square value={i} />; }
나중에 board를 채우게 되면 this.state.squares는 아래와 같을 것
[ 'O', null, 'X', 'X', 'X', 'O', 'O', null, null, ]
더보기In the beginning, we passed the value prop down from the Board to show numbers from 0 to 8 in every Square. In a different previous step, we replaced the numbers with an “X” mark determined by Square’s own state. This is why Square currently ignores the value prop passed to it by the Board.
We will now use the prop passing mechanism again. We will modify the Board to instruct each individual Square about its current value ('X', 'O', or null). We have already defined the squares array in the Board’s constructor, and we will modify the Board’s renderSquare method to read from it:
초기에 우리는 value prop을 Board에서 0부터 8까지 숫자들을 보여주기 위해 모든 Square로 전달했다. 또 다른 이전의 단계에서는 우리는 각 숫자들을 X로 바꾸었다. 그래서 Square는 전달된 value prop을 무시하고 있다.
우리는 prop 전달 메커니즘을 다시 사용한다. 각 개별적인 square에게 현재 값을 지시하기 위해 Board를 수정할 것이다. 이미 생성자에서 squares 배열을 정의했다. 그리고 renderSquare 메소드를 수정한다.
renderSquare(i) { return <Square value={this.state.squares[i]} />; }
더보기Each Square will now receive a value prop that will either be 'X', 'O', or null for empty squares.
Next, we need to change what happens when a Square is clicked. The Board component now maintains which squares are filled. We need to create a way for the Square to update the Board’s state. Since state is considered to be private to a component that defines it, we cannot update the Board’s state directly from Square.
Instead, we’ll pass down a function from the Board to the Square, and we’ll have Square call that function when a square is clicked. We’ll change the renderSquare method in Board to:
각 스퀘어는 빈 스퀘어에 대해 X, O, null인 이제 value prop을 받을 것이다.
다음에는 Square가 클릭되었을 때 발생되는 것들을 변경해야 한다. Board 컴포넌트는 이제 스퀘어들이 채워지는 것들을 갖고 있으므로 Square가 Board의 상태를 변경할 방법이 필요하다. 컴포넌트는 자신이 정의한 state에만 접근할 수 있으므로 Square에서는 바로 변경할 수 없다. 대신에 우리는 Board에서 Square로 함수를 전달한다. 그리고 Square가 클릭되면 그 함수를 호출할 것이다.
Board의 renderSquare 메소드를 다음과 같이 변경한다.
renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); }
더보기Now we’re passing down two props from Board to Square: value and onClick. The onClick prop is a function that Square can call when clicked. We’ll make the following changes to Square:
-
Replace this.state.value with this.props.value in Square’s render method
-
Replace this.setState() with this.props.onClick() in Square’s render method
-
Delete the constructor from Square because Square no longer keeps track of the game’s state
After these changes, the Square component looks like this:
이제 Board에서 Square로 우리는 두 개의 props(value, onClick)를 전달한다. onClick prop은 Square가 클릭되었을 때 호출하는 함수이다. 우리는 Square에 다음과 같이 변경한다.
- this.state.value를 this.props.value로 변경 (Square’s render method)
- this.setState()를 with this.props.onClick()로 변경 (Square’s render method)
- constructor 삭제 (Square)
class Square extends React.Component { render() { return ( <button className="square" onClick={() => this.props.onClick()} > {this.props.value} </button> ); } }
더보기When a Square is clicked, the onClick function provided by the Board is called. Here’s a review of how this is achieved:
-
The onClick prop on the built-in DOM <button> component tells React to set up a click event listener.
-
When the button is clicked, React will call the onClick event handler that is defined in Square’s render() method.
-
This event handler calls this.props.onClick(). The Square’s onClick prop was specified by the Board.
-
Since the Board passed onClick={() => this.handleClick(i)} to Square, the Square calls this.handleClick(i) when clicked.
-
We have not defined the handleClick() method yet, so our code crashes. If you click a square now, you should see a red error screen saying something like “this.handleClick is not a function”.
Square가 클릭되면, onClick 함수가 호출된다. 우리가 달성한 것들에 대해 리뷰하자.
- 내장된 DOM button 컴포넌트에 있는 onClick prop은 React에게 click event listener를 설정하라고 알려줌
- 버튼이 클릭되었을 때 React는 Square의 render() 함수에 정의된 onClick 이벤트 핸들러를 호출한다.
- 이 이벤트 핸들러는 this.props.onClick()를 호출한다. Sq의 onClick prop는 Board에 정의되었음
- Board가 Square에게 전달했기 때문에 Square는 클릭되었을 때 handleClick(i)를 호출할 수 있다.
- 아직 handleClick()을 정의하지 않았으므로 코드가 crash될 것임.
주의 : 이벤트를 나타내는 prop은 on[Event], 이벤트를 처리하는 함수에는 handle[Event]를 사용하는 것이 일반적
이제 handleClick을 작성해봅시다.
class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; 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> ); } }
더보기After these changes, we’re again able to click on the Squares to fill them, the same as we had before. However, now the state is stored in the Board component instead of the individual Square components. When the Board’s state changes, the Square components re-render automatically. Keeping the state of all squares in the Board component will allow it to determine the winner in the future.
Since the Square components no longer maintain state, the Square components receive values from the Board component and inform the Board component when they’re clicked. In React terms, the Square components are now controlled components. The Board has full control over them.
Note how in handleClick, we call .slice() to create a copy of the squares array to modify instead of modifying the existing array. We will explain why we create a copy of the squares array in the next section.
이렇게 변경하면 우리는 Square들을 이전처럼 다시 클릭할 수 있다. 그러나 이제 state가 개별적인 square 컴포넌트들이 아니라 Board 컴포넌트에 저장된다. Board의 state가 변경될 때마다 Square 컴포넌트들은 자동적으로 리-렌더링을 한다. 모든 square들의 상태를 Board 컴포넌트에 유지하는 것은 나중에 승자를 결정하게 할 수 있다.
Square 컴포넌트들은 더 이상 state를 유지하지 않기 때문에 Square 컴포넌트들은 Board 컴포넌트로부터 값을 받고 그들이 클릭되었을 때 알려준다. React 용어로 Square 컴포넌트들은 이제 제어되는 컴포넌트들이다. Board가 모두 제어한다.
handleClick에서 slice()를 호출하여 squares 배열의 복사본을 생성하는 것을 주의하자. 왜 복사본을 생성했는지는 다음 섹션에서 설명할 것이다!
Why Immutability Is Important
더보기In the previous code example, we suggested that you use the .slice() method to create a copy of the squares array to modify instead of modifying the existing array. We’ll now discuss immutability and why immutability is important to learn.
There are generally two approaches to changing data. The first approach is to mutate the data by directly changing the data’s values. The second approach is to replace the data with a new copy which has the desired changes.
The end result is the same but by not mutating (or changing the underlying data) directly, we gain several benefits described below.
이전의 예시 코드에서 우리는 바로 배열을 수정하는 대신에 복사본 생성을 위해 slice 함수를 사용하기를 제안했다. 우리는 이제 불변성에 대해 이야기하고 왜 불변성이 중요한지 배워볼 것이다.
일반적으로 변화하는 데이터에 대해 두 가지 접근법이 있다.
첫 번째는 데이터의 값을 직접 변경하는 것이다. 두 번째는 변경하고자 하는 값을 가지는 복사본으로 교체하는 것이다.
최종 값은 동일하지만 직접적으로 값을 변경하지 않으면 아래의 장점을 가진다.
// Data Change with Mutation var player = {score: 1, name: 'Jeff'}; player.score = 2; // Now player is {score: 2, name: 'Jeff'} // Data Change without Mutation var player = {score: 1, name: 'Jeff'}; var newPlayer = Object.assign({}, player, {score: 2}); // Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'} // Or if you are using object spread syntax proposal, you can write: // var newPlayer = {...player, score: 2};
더보기Complex Features Become Simple
Immutability makes complex features much easier to implement. Later in this tutorial, we will implement a “time travel” feature that allows us to review the tic-tac-toe game’s history and “jump back” to previous moves. This functionality isn’t specific to games — an ability to undo and redo certain actions is a common requirement in applications. Avoiding direct data mutation lets us keep previous versions of the game’s history intact, and reuse them later.
Detecting Changes
Detecting changes in mutable objects is difficult because they are modified directly. This detection requires the mutable object to be compared to previous copies of itself and the entire object tree to be traversed.
Detecting changes in immutable objects is considerably easier. If the immutable object that is being referenced is different than the previous one, then the object has changed.
Determining When to Re-Render in React
The main benefit of immutability is that it helps you build pure components in React. Immutable data can easily determine if changes have been made which helps to determine when a component requires re-rendering.
You can learn more about shouldComponentUpdate() and how you can build pure components by reading Optimizing Performance.
- 복잡한 특징들이 단순해진다.
불변성은 복잡한 특징들의 구현을 쉽게 만들 수 있다. 이 튜토리얼에서는 "time traverse" 기능을 구현하여 이전 동작으로 되돌아 갈 수 있다. 특정 행동을 취소하고 다시 실행하는 기능은 게임에만 국한되지 않는 일반적인 요구사항이다. 직접적인 데이터 변경을 피하는 것은 이전 버전을 유지하고 나중에 재사용할 수 있게 한다.
- 변화를 감지할 수 있다.
직접적으로 수정되기 때문에 가변적인 객체에서 변화를 감지하는 것은 어렵다. 이러한 디텍션은 가변적인 객체들이 객체 자신의 이전 버전과 비교되어야 하고 전체 트리를 돌아야 한다.
- React에서 다시 렌더링하는 것을 결정
불변성의 가장 주요한 장점은 불변성이 puer한 컴포넌트들을 구축하는 것을 돕는다는 것이다. 불변적인 데이터는 변화가 발생했는지 결정하는 것을 쉽게 판단하게 하고 컴포넌트를 다시 렌더링할 지 결정할 수 있다.
shouldComponentUpdate()나 pure 컴포넌트들을 작성하는 방법을 알고 있다면 Optimizing Performance.참고
Function Components
더보기We’ll now change the Square to be a function component.
In React, function components are a simpler way to write components that only contain a render method and don’t have their own state. Instead of defining a class which extends React.Component, we can write a function that takes props as input and returns what should be rendered. Function components are less tedious to write than classes, and many components can be expressed this way.
Replace the Square class with this function:
{code}
We have changed this.props to props both times it appears.
이제 Square가 함수 컴포넌트가 되도록 바꿔보자.
리액트에서 function components란 오직 하나의 render 메소드만 갖고 그들 자체의 state는 갖지 않는 컴포넌트를 작성하는 하나의 단순한 방식이다. React.Component를 확장하는 클래스를 정의하는 대신에 props를 인풋으로 취하고 어떤 것이 렌더링 되어야 하는지를 리턴하는 함수를 작성하는 것이다. Function 컴포넌트들은 클래스보다 작성하는 것이 빠르고 많은 컴포넌트들이 이 방식으로 표현된다.
Square 클래스를 이 함수로 교체해보자.
function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); }
모든 this.props를 props로 변경했다.
Taking Turns
더보기We now need to fix an obvious defect in our tic-tac-toe game: the “O”s cannot be marked on the board.
We’ll set the first move to be “X” by default. We can set this default by modifying the initial state in our Board constructor:
Each time a player moves, xIsNext (a boolean) will be flipped to determine which player goes next and the game’s state will be saved. We’ll update the Board’s handleClick function to flip the value of xIsNext:
With this change, “X”s and “O”s can take turns. Try it!
Let’s also change the “status” text in Board’s render so that it displays which player has the next turn:
이제 지금까지 만든 틱택토 게임에서 틀린 부분을 고쳐야 한다 : "O"가 보드판에 표시될 수가 없다.
기본값으로 X가 첫번째 순서로 설정하자. 이 기본값을 Board 생성자의 초기 state를 수정함으로써 설정할 수 있다.
class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), xIsNext: true, }; }
플레이어가 이동할 때마다 xIsNext (불린타입) 값이 뒤집혀서 다음 플레이어를 결정하고 게임의 상태가 저장될 것이다.
xisNext의 값이 뒤집어지도록 Board의 handleClick 함수를 업데이트하자.
handleClick(i) { const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); }
변경하면 X와 O가 번갈아가면서 나타난다. 시도해보자!
또한 Board의 렌더 안에 있는 status도 변경해서 다음 차례 플레이어가 누구인지 나타내자.
render() { const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); return ( // the rest has not changed
수정을 다 하면 Board 컴포넌트의 전체 코드는 아래와 같다.
class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), xIsNext: true, }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const 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> ); } }
Declaring a Winner
더보기Now that we show which player’s turn is next, we should also show when the game is won and there are no more turns to make. Copy this helper function and paste it at the end of the file:
Given an array of 9 squares, this function will check for a winner and return 'X', 'O', or null as appropriate.
We will call calculateWinner(squares) in the Board’s render function to check if a player has won. If a player has won, we can display text such as “Winner: X” or “Winner: O”. We’ll replace the status declaration in Board’s render function with this code:
We can now change the Board’s handleClick function to return early by ignoring a click if someone has won the game or if a Square is already filled:
Congratulations! You now have a working tic-tac-toe game. And you’ve just learned the basics of React too. So you’re probably the real winner here.
이제 우리는 다음 플레이어가 누구인지 나타냈고, 승부가 나는 때와 더 이상 수를 둘 곳이 없을 때를 알려주어야 한다.
이 helper 함수를 파일의 끝에 카피&페이스트 하라.
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]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
9개 square의 배열에서 이 함수는 승자를 확인하고 적절하게 X, O, null을 리턴할 것이다.
Board의 렌더 함수에서 플레이어가 이겼는지 확인하기 위해 calculateWinner 함수를 호출할 것이다. 어떤 플레이어가 이겼다면 Winner X, Winner O로 나타낸다. status 선언을 아래 코드로 바꾼다.
render() { const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( // the rest has not changed
이제 누군가가 게임에서 이기거나, 스퀘어가 이미 채워졌다면 클릭을 무시하도록 Board의 handleClick 함수를 변경하자.
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, }); }
축하한다! 이제 정상적인 틱택토 게임을 만들었다. 그리고 리액트의 기본도 배웠다.
진정한 승리자는 당신일 것이다.
'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 : Adding Time Travel (0) 2020.02.17 [React document] tutorial : Overview (0) 2020.02.16 -