Tutorial: Tic-Tac-Toe
You will build a small tic-tac-toe game during this tutorial. This tutorial does not assume any existing React knowledge. The techniques you’ll learn in the tutorial are fundamental to building any React app, and fully understanding it will give you a deep understanding of React.
이 튜토리얼 동안 작은 tic-tac-toe 게임을 만들것입니다. 이 튜토리얼은 기존에 알고있던 React 지식을 필요로 하지 않습니다. 이 튜토리얼에서 배울 기술은 React app을 만드는데 기초가 될 것이고, React에 대해 깊게 완전히 이해할 수 있도록 합니다.
The tutorial is divided into several sections:
- Setup for the tutorial will give you a starting point to follow the tutorial.
- Overview will teach you the fundamentals of React: components, props, and state.
- Completing the game will teach you the most common techniques in React development.
- Adding time travel will give you a deeper insight into the unique strengths of React.
이 튜토리얼은 여러 섹션으로 나뉩니다:
- 튜토리얼을 위한 설정은 튜토리얼을 진행하는데 시작점이 될 것입니다.
- 개요는 React의 기초를 가르칩니다: components, props, 그리고 state
- 게임을 완성하는 것은 React 개발에 있어서 가장 기본적인 테크닉을 가르칩니다.
- 시간 여행을 추가하는 것(?)은 React의 유니크한 강점들에 있어서 더 깊은 통찰력을 줄 것입니다.
What are you building? (무엇을 만들 것인지)
In this tutorial, you’ll build an interactive tic-tac-toe game with React.
You can see what it will look like when you’re finished here:
이 튜토리얼에서, React로 상호작용하는 tic-tac-toe 게임을 만들 것이다.
다음과 같은 것을 끝냈을 때, 이와 같은 것을 볼 수 있을 것이다.
If the code doesn’t make sense to you yet, or if you are unfamiliar with the code’s syntax, don’t worry! The goal of this tutorial is to help you understand React and its syntax.
만약 아직 이 코드가 이해가 안된다면, 또는 문법이 익숙하지 않다면 걱정하지 않아도 된다! 이 튜토리얼의 목표는 React와 그 문법을 이해하는 것을 돕는 것이다.
We recommend that you check out the tic-tac-toe game above before continuing with the tutorial. One of the features that you’ll notice is that there is a numbered list to the right of the game’s board. This list gives you a history of all of the moves that have occurred in the game, and it is updated as the game progresses.
튜토리얼과 함께 진행하기 전에 위 tic-tac-toe 게임을 확인하는 것을 추천한다. 게임판의 오른쪽에 있는 번호 리스트들이 눈에띄는 특징중에 하나이다. 이 리스트는 게임중에 일어난 모든 움직임의 기록을 제공하고, 게임을 진행함에 따라 업데이트 된다.
Once you’ve played around with the finished tic-tac-toe game, keep scrolling. You’ll start with a simpler template in this tutorial. Our next step is to set you up so that you can start building the game.
완성된 tic-tac-toe 게임을 플레이 했다면, 계속 스크롤 하십시오. 이 튜토리얼에서 더 간단한 템플릿으로 시작할 것이다. 우리의 다음단계는 게임을 만드는것을 시작할 수 있도록 세팅하는 것이다.
Setup for the tutorial (튜토리얼을 위한 설정)
In the live code editor below, click Fork in the top-right corner to open the editor in a new tab using the website CodeSandbox. CodeSandbox lets you write code in your browser and preview how your users will see the app you’ve created. The new tab should display an empty square and the starter code for this tutorial.
아래 라이브 코드 편집기에서 오른쪽 상단에 모서리에 있는 Fork를 클릭해서 CodeSandbox 웹사이트를 사용해 새로운 탭을 연다. CodeSandbox는 브라우저에서 코드를 작성하고, 앱을 만들었을 때 사용자들에게 어떻게 보일지 미리 볼 수 있다. 새 탭에는 빈 사각형과 이 튜토리얼을 위한 시작 코드가 보여야 한다.
< Note >
You can also follow this tutorial using your local development environment. To do this, you need to:
- Install Node.js
- In the CodeSandbox tab you opened earlier, press the top-left corner button to open the menu, and then choose File > Export to ZIP in that menu to download an archive of the files locally
- Unzip the archive, then open a terminal and cd to the directory you unzipped
- Install the dependencies with npm install
- Run npm start to start a local server and follow the prompts to view the code running in a browser
If you get stuck, don’t let this stop you! Follow along online instead and try a local setup again later.
로컬 환경에서 이 튜토리얼을 진행할 수 있다. 다음과 같이 하면 된다:
- Node.js 설치
- 열었던 CodeSandbox 탭에서 왼쪽 상단에 있는 버튼을 눌러서 메뉴를 연다음, File > Export to ZIP 를 선택해서 파일 아카이브를 로컬로 다운로드 한다.
- 아카이브의 압축을 푼다음, 터미널에서 압축을 푼 폴더로 이동한다.
- npm install 을 통해 dependency 들을 설치한다.
- npm start 로 로컬 서버를 시작하고 프롬프트에 따라 브라우저에서 실행중인 코드를 본다.
막혀도 멈추지 말고 온라인으로 진행한 뒤 나중에 해볼 것!
Overview (개요)
Now that you’re set up, let’s get an overview of React!
세팅 다했으니 이제 React에 대한 개요를 봅시당!
Inspecting the starter code (스타터 코드 검사)
In CodeSandbox you’ll see three main sections:
CodeSandbox에서 세 가지 주요 섹션들을 볼 수 있다:
- The Files section with a list of files like App.js, index.js, styles.css and a folder called public
- The code editor where you’ll see the source code of your selected file
- The browser section where you’ll see how the code you’ve written will be displayed
- Files 섹션은 App.js, index.js, styles.css, 그리고 public 이라 불리는 폴더와 같은 파일 리스트들이 존재한다.
- code editor는 선택한 파일의 소스코를 볼 수 있다.
- browser 섹션은 내가 작성한 코드가 어떻게 보여지는지 볼 수 있다.
The App.js file should be selected in the Files section. The contents of that file in the code editor should be:
App.js 파일을 Files 섹션에서 선택해야 한다. code editor에 있는 파일의 내용은 다음과 같아야 한다:
export default function Square() {
return <button className="square">X</button>;
}
The browser section should be displaying a square with a X in it like this:
브라우저에선 이와 같이 X가 있는 사각형이 보일 것 이다:
이제 스타터 코드에 있는 파일들을 보자.
App.js
The code in App.js creates a component. In React, a component is a piece of reusable code that represents a part of a user interface. Components are used to render, manage, and update the UI elements in your application. Let’s look at the component line by line to see what’s going on:
App.js에 있는 코드는 컴포넌트를 만든다. React 에서 컴포넌트는 UI(User Interface)를 보여주는 재사용 가능한 코드의 한 부분이다. 컴포넌트들은 애플리케이션에서 UI 요소들을 렌더링, 관리, 그리고 업데이트 하는데 사용된다. 무슨일이 일어나는지 보기위해 한줄씩 컴포넌트를 보도록 하자.
export default function Square() { // <-
return <button className="square">X</button>;
}
The first line defines a function called Square. The export JavaScript keyword makes this function accessible outside of this file. The default keyword tells other files using your code that it’s the main function in your file.
첫번째 줄은 Square라 불리는 함수를 정의한다. export JavaScript 키워드는 이 함수를 이 파일 밖에서 이 함수에 접근 가능하도록 해준다. default 키워드는 다른 파일들에게 코드를 사용해서 파일의 주요 함수라는 것을 알린다.
export default function Square() {
return <button className="square">X</button>; // <-
}
The second line returns a button. The return JavaScript keyword means whatever comes after is returned as a value to the caller of the function. <button> is a JSX element. A JSX element is a combination of JavaScript code and HTML tags that describes what you’d like to display. className="square" is a button property or prop that tells CSS how to style the button. X is the text displayed inside of the button and </button> closes the JSX element to indicate that any following content shouldn’t be placed inside the button.
두번째 줄은 버튼을 반환한다. return 이라는 JavaScript 키워드는 뒤에 오는 모든것을 값으로 함수의 호출자에게 반환해준다. <button>은 JSX 요소이다. JSX 요소는 보여주려고 하는 것을 묘사한 JavaScript 코드와 HTML 태그들의 조합이다. className="square"는 button을 어떻게 꾸밀지 CSS에게 얄려주는 속성 혹은 prop이다. X는 버튼 안에 보여지는 텍스트이고 </button>은 버튼 안에 어떠한 다음 컨텐트가 올 수 없다고 가리키는 JSX 요소를 닫는 태그이다.
styles.css
Click on the file labeled styles.css in the Files section of CodeSandbox. This file defines the styles for your React app. The first two CSS selectors (* and body) define the style of large parts of your app while the .square selector defines the style of any component where the className property is set to square. In your code, that would match the button from your Square component in the App.js file.
CodeSandbox에 Files 부분에 있는 styles.css를 눌러보자. 이 파일은 React 앱을 위한 style들을 정의한다. 처음에 있는 두 선택자들(*와 body)은 .square 셀렉터가 className 속성이 square라고 되어있는 모든 컴포넌트에 대해 스타일을 정의하는 반면에 앱의 큰 부분의 스타일을 정의한다. 코드에서, App.js 파일안의 Square 컴포넌트의 버튼과 일치한다.
index.js
Click on the file labeled index.js in the Files section of CodeSandbox. You won’t be editing this file during the tutorial but it is the bridge between the component you created in the App.js file and the web browser.
CodeSandbox에 Files 부분에 있는 index.js를 눌러보자. 튜토리얼을 진행하는 동안 이 파일을 편집하는 일은 없을 것이지만 이것은 App.js 파일안에 만든 컴포넌트와 웹 브라우저의 다리 역할을 한다.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
Lines 1-5 brings all the necessary pieces together:
- React
- React’s library to talk to web browsers (React DOM)
- the styles for your components
- the component you created in App.js.
The remainder of the file brings all the pieces together and injects the final product into index.html in the public folder.
1-5번째 줄들은 필요한 모든 부분을 함께 가져온다.
- React
- 웹 브라우저와 대화하기 위한 React 라이브러리 (React DOM)
- 컴포넌트들의 스타일들
- App.js에서 만든 컴포넌트
파일의 나머지는 모든 부분들을 함께 가져오고 public 폴더에 있는 index.html 안으로 결과물을 주입한다.
Building the board (보드 만들기)
Let’s get back to App.js. This is where you’ll spend the rest of the tutorial.
App.js 로 돌아가보자. 튜토리얼의 남은 부분을 보낼 곳이다.
Currently the board is only a single square, but you need nine! If you just try and copy paste your square to make two squares like this:
현재 보드는 단지 한 개의 정사각형만 존재하지만, 9개가 필요하다! 두 개의 정사각형을 위해 다음과 같이 복사 후 붙여넣기를 할 수 있다:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
그러면 다음과 같은 에러를 얻을 것이다:
/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag.
Did you want a JSX fragment <>...</>?
React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use fragments (<> and </>) to wrap multiple adjacent JSX elements like this:
React 컴포넌트들은 단 한개의 JSX 요소를 반환할 필요가 있고 두개의 버튼같이 여러개의 인접한 JSX 요소들을 반환하면 안된다. 이것을 고치기 위해서 다음과 같이 여러개의 인접한 JSX 요소들을 감싸기 위해 fragment(<> and </>)를 사용할 수 있다:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
이제 다음과 같이 보일 것이다:
그리고 9개의 정사각형을 만들기 위해 몇 번 복사, 붙여넣기를 하면된다. 그런데...
Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you’ll need to group your squares into rows with divs and add some CSS classes. While you’re at it, you’ll give each square a number to make sure you know where each square is displayed.
정사각형들이 우리의 보드에 필요한 것 처럼 격자판에 존재하는 것이 아니라 한 줄에 존재한다. 이것을 고치기 위해서 정사각형들을 div를 이용해서 한 행으로 그룹화하고 몇몇의 CSS 클래스들을 추가하면 된다. 그 동안 각각의 보여지는 정사각형이 어디에 있는지 알기 위해서 각각의 정사각형에 숫자를 부여하면 된다.
App.js 파일에서 Square 컴포넌트를 다음과 같이 업데이트 하면 된다:
export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
The CSS defined in styles.css styles the divs with the className of board-row. Now that you’ve grouped your components into rows with the styled divs you have your tic-tac-toe board:
styles.css에 정의된 CSS는 div들을 board-row 라는 className과 함께 스타일링 하도록 정의되어 있다. 이제 컴포넌트들을 스타일이 지정된 div 행으로 그룹화했으므로 tic-tac-toe 보드를 얻을 수 있다:
But you now have a problem. Your component named Square, really isn’t a square anymore. Let’s fix that by changing the name to Board:
하지만 문제가 있다. Square 컴포넌트는 정말로 더이상 정사각형이 아니다. Board로 이름을 바꿔서 고쳐보자:
export default function Board() {
//...
}
여기서 코드는 이런것과 같이 보여야 한다:
Passing data through props (props를 통해 데이터 전달하기)
Next, you’ll want to change the value of a square from empty to “X” when the user clicks on the square. With how you’ve built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React’s component architecture allows you to create a reusable component to avoid messy, duplicated code.
다음으로, 유저가 정사각형을 클릭했을 때 비어있는 것에서 "X"로 정사각형의 값을 바꾸고 싶을때가 있을 수도 있다. 지금까지 보드를 만든 방법으로 정사각형을 9번 업데이트하는 코드를 복사해서 붙여넣어야 한다(각 정사각형마다 한번씩)! 복사 후 붙여넣기 하는것 대신, React의 컴포넌트 아키텍쳐는 지저분하고 중복된 코드를 피하기 위해서 재사용가능한 컴포넌트를 만들 수 있다.
First, you are going to copy the line defining your first square (<button className="square">1</button>) from your Board component into a new Square component:
먼저, 새로운 Square 컴포넌트에 Board에 있는 첫번째 정사각형(<button className="square">1</button>)을 정의한 라인을 복사할 것이다:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
그리고 JSX 문법을 이용한 Square 컴포넌트를 렌더링하기 위해 Board 컴포넌트를 업데이트 시켜야 한다:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Note how unlike the browser divs, your own components Board and Square must start with a capital letter.
브라우저 div와 다른것에 유의해야 한다. 우리가 만든 Board와 Square 컴포넌트들은 반드시 대문자로 시작해야 한다.
다음을 보자:
Oh no! You lost the numbered squares you had before. Now each square says “1”. To fix this, you will use props to pass the value each square should have from the parent component (Board) to its child (Square).
이전에 가지고 있던 숫자가 있던 정사각형들을 잃어버렸다. 이제 각각의 정사각형은 "1"만 나타낸다. 이것을 고치기 위해서, 부모 컴포넌트인 Board 로부터 그들의 자식인 Square 컴포넌트로 값을 전달하기 위해서 props를 사용해야 한다.
Update the Square component to read the value prop that you’ll pass from the Board:
Board로부터 전달받은 prop의 값을 읽기 위해서 Square 컴포넌트를 업데이트하자:
function Square({ value }) { // <-
return <button className="square">1</button>;
}
function Square({ value }) indicates the Square component can be passed a prop called value.
Now you want to display that value instead of 1 inside every square. Try doing it like this:
function Square({ value})는 Square 컴포넌트가 value 라는 prop을 전달받을 수 있다는것을 나타낸다.
이제 모든 정사각형안에서 1대신에 value를 보여주고 싶을 것이다. 다음과 같이 해보자:
function Square({ value }) {
return <button className="square">value</button>; // <-
}
원하는 대로 나오지 않는다,,,:
You wanted to render the JavaScript variable called value from your component, not the word “value”. To “escape into JavaScript” from JSX, you need curly braces. Add curly braces around value in JSX like so:
"value"라는 단어가 아니라 컴포넌트로부터 받은 value라는 JavaScript 변수를 렌더링 하고 싶다. JSX에서 "JavaScript로 탈출" 하려면 중괄호가 필요하다. 다음과 같이 JSX 값에 중괄호를 씌워주자:
function Square({ value }) {
return <button className="square">{value}</button>; // <-
}
이제, 빈 보드를 보게 될 것이다:
This is because the Board component hasn’t passed the value prop to each Square component it renders yet. To fix it you’ll add the value prop to each Square component rendered by the Board component:
이렇게 된 이유는 Board 컴포넌트가 각각의 Square 컴포넌트가 렌더링 되기 전에 value prop을 전달하지 못했기 때문이다. 이것을 고치기 위해서 Board 컴포넌트에 의해 렌더링 되는 각각의 Square 컴포넌트에 value prop을 추가해야 한다:
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}
이제 다시 숫자가 보일 것이다:
업데이트된 코드는 다음과 같이 보일것이다:
Making an interactive component (상호작용 하는 컴포넌트 만들기)
Let’s fill the Square component with an X when you click it. Declare a function called handleClick inside of the Square. Then, add onClick to the props of the button JSX element returned from the Square:
Square 컴포넌트를 클릭했을 때 X로 채워보자. Square 내부에 handleClick 이라는 함수 하나를 선언하자. 그리고, Square로부터 반환되는 JSX 요소인 버튼의 prop에 onClick을 추가하자:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
If you click on a square now, you should see a log saying "clicked!" in the Console tab at the bottom of the Browser section in CodeSandbox. Clicking the square more than once will log "clicked!" again. Repeated console logs with the same message will not create more lines in the console. Instead, you will see an incrementing counter next to your first "clicked!" log.
이제 정사각형을 클릭한다면, CodeSandbox안의 Browser 섹션의 밑에 있는 Console 탭에서 "clicked!" 라는 로그를 볼 수 있을것이다. 한번 더 클릭하면 다시 "clicked!"로그가 뜨는 것을 볼 수 있을 것이다. 같은 메시지로 반복되는 콘솔 로그들은 콘솔에 더 많은 행을 생성하지 않는다. 대신에, 첫번째 "clicked!" 로그 옆에 증가하는 카운터를 볼 수 있을것이다.
As a next step, you want the Square component to “remember” that it got clicked, and fill it with an “X” mark. To “remember” things, components use state.
다음 단계로, Square 컴포넌트가 클릭된 것을 "기억"하고, "X" 표시로 채우길 원할 것이다. "기억"하기 위해서 컴포넌트들은 state를 사용해야 한다.
React provides a special function called useState that you can call from your component to let it “remember” things. Let’s store the current value of the Square in state, and change it when the Square is clicked.
React는 컴포넌트에서 호출해서 "기억" 할 수 있는 특별한 함수인 useState를 제공한다. Square의 현재 값을 state에 저장하고, Square가 클릭되었을때 그 값을 바꿔보자.
Import useState at the top of the file. Remove the value prop from the Square component. Instead, add a new line at the start of the Square that calls useState. Have it return a state variable called value:
파일의 최상단에 useState를 import 한다. Square 컴포넌트로부터의 value prop을 제거한다. 대신에, Square의 시작에 useState라 불리는것을 새로 추가한다. value라 불리는 state 변수를 반환하도록 한다:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value stores the value and setValue is a function that can be used to change the value. The null passed to useState is used as the initial value for this state variable, so value here starts off equal to null.
value는 값을 저장하고 setValue는 value를 바꿀 때 사용할 수 있는 함수이다. useState에 전달된 null은 이 state 변수에 초기값으로 사용된다. 그리고 value는 똑같이 null로 초기화된다.
Since the Square component no longer accepts props anymore, you’ll remove the value prop from all nine of the Square components created by the Board component:
Square 컴포넌트가 더이상 prop들을 받아들이게 되지 않은 이후로, Board 컴포넌트에 의해 만들어진 9개의 Square 컴포넌트들로부터의 value prop을 제거할것이다:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Now you’ll change Square to display an “X” when clicked. Replace the console.log("clicked!"); event handler with setValue('X');. Now your Square component looks like this:
이제 Square가 클릭되었을때 "X"를 보여주도록 바꿀것이다. console.log("clicked!");를 setValue('X');로 바꾸자. 이제 Square 컴포넌트는 다음과 같을것이다:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
By calling this set function from an onClick handler, you’re telling React to re-render that Square whenever its <button> is clicked. After the update, the Square’s value will be 'X', so you’ll see the “X” on the game board. Click on any Square, and “X” should show up:
set 핸들러에서 이 함수를 호출하면 React가 <button>을 클릭할 때마다 onClick 핸들러에게 다시 렌더링하도록 한다. 업데이트가 된 후에, Square의 값은 'X'가 되고, 게임 보드에서 "X"를 볼 수 있을것이다. 어떤 Square를 클릭하더라도 "X"가 보일것이다:
Each Square has its own state: the value stored in each Square is completely independent of the others. When you call a set function in a component, React automatically updates the child components inside too.
각각의 Square는 그들 각각의 state를 가진다: 각각의 Square에 저장된 value는 다른것과 완전히 독립적이다. 컴포넌트에서 set 함수를 부를 때, React는 자동적으로 안에 있는 자식 컴포넌트들 또한 업데이트 시킨다.
위의 변경사항들을 바꾸면 코드는 다음과 같을 것이다:
React Developer Tools (React 개발 도구들)
React DevTools let you check the props and the state of your React components. You can find the React DevTools tab at the bottom of the browser section in CodeSandbox:
React DevTools는 React 컴포넌트들의 prop들과 state를 확인할 수 있도록 해준다. CodeSandbox안의 browser 섹션의 밑에 있는 탭에서 React DevTools를 찾을 수 있다:
화면에 있는 특별한 컴포넌트를 검사하기 위해, React DevTools의 좌상단에 있는 버튼을 이용할 수 있다:
( * 흔히 로컬 환경에서 개발할때는 크롬의 f12를 누르면 나오는 개발자도구를 이용한다)
Completing the game (게임 완성하기)
By this point, you have all the basic building blocks for your tic-tac-toe game. To have a complete game, you now need to alternate placing “X”s and “O”s on the board, and you need a way to determine a winner.
이제, tic-tac-toe 게임을 위한 모든 기본 단계는 마쳤다. 이 게임을 완성하기 위해선, 보드에 있는 "X"들을 "O"로 바꿀 필요가 있다. 그리고 승자를 결정하는 방법도 필요하다.
Lifting state up (상태 끌어올리기)
Currently, each Square component maintains a part of the game’s state. To check for a winner in a tic-tac-toe game, the Board would need to somehow know the state of each of the 9 Square components.
지금은 각각의 Square 컴포넌트가 게임의 상태의 일부분을 유지한다. tic-tac-toe 게임에서 승자를 확인하기 위해선 Board가 9개의 Square 컴포넌트 각각의 상태를 알 필요가 있다.
How would you approach that? At first, you might guess that the Board needs to “ask” each Square for that Square’s state. Although this approach is technically 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, like you did when you passed a number to each Square.
어떻게 접근할 수 있을까? 첫번째로, Square의 상태에 대해 각각의 Square에 "물어보는 것"이 필요하다가 생각할것이다. 이 접근이 React에서 기술적으로 가능하지만, 코드가 이해하기어렵고, 버그에 취약하고, 리팩토링 하기에 어려워지기 때문에 좋은 방법이라 생각되진 않는다. 대신에, 가장 좋은 접근은 각각의 Square 대신에 부모 Board 컴포넌트에서 게임의 상태를 저장하는것이다. Board 컴포넌트가 각각의 Square에 숫자를 전달할때 처럼 각각의 Square에게 prop을 전달함으로써 무엇을 보여줄지 알려줄 수 있다.
To collect data from multiple children, or to have two child components communicate with each other, declare the shared state in their parent component instead. The parent component can pass that state back down to the children via props. This keeps the child components in sync with each other and with their parent.
다수의 자식으로부터 데이터를 모으기 위해서 혹은 두개의 자식 컴포넌트끼리 서로 소통하기 위해서는 대신 부모 컴포넌트에서 공유된 state를 선언하도록 한다. 부모 컴포넌트는 props를 통해서 state를 다시 자식에게 내려줄 수 있다. 이것은 자식 컴포넌트 서로서로 동기화가 되고, 그들의 부모와도 동기화를 유지한다.
Lifting state into a parent component is common when React components are refactored.
부모 컴포넌트로 상태를 끌어올리는것은 React 컴포넌트들이 리팩토링 될 때 일반적이다.
Let’s take this opportunity to try it out. Edit the Board component so that it declares a state variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares:
이 기회에 해보도록 하자. 9개의 사각형에 해당하는 9개의 null 배열의 디폴트 값을 가지는 상태 변수를 선언하도록 Board 컴포넌트를 편집한다:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null) creates an array with nine elements and sets each of them to null. The useState() call around it declares a squares state variable that’s initially set to that array. Each entry in the array corresponds to the value of a square. When you fill the board in later, the squares array will look like this:
Array(9).fill(null)은 각각 null로 설정된 9개의 요소를 가진 배열을 만든다. useState()는 만든 배열로 squares 상태 변수를 초기화시킨다. 배열에 들어있는 각각의 요소는 정사각형의 값과 상응한다. 나중에 보드를 채울때, squares 배열은 다음과 같이 될 것이다:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
이제 Board 컴포넌트는 각각의 Square로 value prop을 전달해줄 필요가 있다:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
Next, you’ll edit the Square component to receive the value prop from the Board component. This will require removing the Square component’s own stateful tracking of value and the button’s onClick prop:
다음으로, Board 컴포넌트로부터 value prop을 받기 위해서 Square 컴포넌트를 편집해야 한다. Square 컴포넌트 자신의 value 상태 추적과 버튼의 onClick prop을 지워야한다:
function Square({value}) {
return <button className="square">{value}</button>;
}
이 시점에서 빈 tic-tac-toe 보드를 볼 수 있을것이다:
그리고 코드는 다음과 같을 것이다:
Each Square will now receive a value prop that will either be 'X', 'O', or null for empty squares.
각각의 Square는 빈 정사각형들을 위해서 'X', 'O', 또는 null을 value prop으로 받을것이다.
Next, you need to change what happens when a Square is clicked. The Board component now maintains which squares are filled. You’ll need to create a way for the Square to update the Board’s state. Since state is private to a component that defines it, you cannot update the Board’s state directly from Square.
다음으로, Square가 클릭되었을때 일어날 일을 바꿀 필요가 있다. Board 컴포넌트는 이제 정사각형들이 꽉 차있도록 유지해야 한다. Board의 상태를 업데이트 하도록 Square를 위한 방법을 만들 필요가 있다. 상태는 정의한 컴포넌트 전용이기 때문에, Square로부터 Board의 상태를 바로 업데이트 할 수는 없다.
Instead, you’ll pass down a function from the Board component to the Square component, and you’ll have Square call that function when a square is clicked. You’ll start with the function that the Square component will call when it is clicked. You’ll call that function onSquareClick:
대신에, Board 컴포넌트로부터 Square 컴포넌트로 함수를 내려줄 수 있고, 정사각형이 클릭되었을 때 Square에서 함수를 호출하게 된다. 이제 클릭되었을 때 Square 컴포넌트가 호출하는 함수를 가지고 시작할 수 있다. 이 함수를 onSquareClick이라고 하자:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
다음으로, onSquareClick 함수를 Square 컴포넌트의 prop에 넣어준다.
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Now you’ll connect the onSquareClick prop to a function in the Board component that you’ll name handleClick. To connect onSquareClick to handleClick you’ll pass a function to the onSquareClick prop of the first Square component:
이제 Board 컴포넌트 안에서 handleClick 이라고 이름 지은 함수와 onSquareClick prop과 연결한다. onSquareClick과 handleClick을 연결하기 위해 첫번째 Square 컴포넌트의 onSquareClick 프롭으로 전달한다:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
Lastly, you will define the handleClick function inside the Board component to update the squares array holding your board’s state:
마지막으로, 보드의 상태를 가지고 있는 squares 배열을 업데이트 하기 위해 Board 컴포넌트 안에서 handleClick 함수를 정의한다:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
The handleClick function creates a copy of the squares array (nextSquares) with the JavaScript slice() Array method. Then, handleClick updates the nextSquares array to add X to the first ([0] index) square.
handleClick 함수는 JavaScript의 slice() 배열 메소드로 squares 배열의 복사본(nextSquares)를 만든다. 그리고 나서, handleClick은 x를 첫번째(인덱스 0) 정사각형에 더하기 위해 nextSquares를 업데이트한다.
Calling the setSquares function lets React know the state of the component has changed. This will trigger a re-render of the components that use the squares state (Board) as well as its child components (the Square components that make up the board).
setSquares라 불리는 함수는 React가 컴포넌트가 바뀌는 상태를 알 수 있도록 한다. 이것은 squares 상태를 사용하는 컴포넌트(Board), 또한 자식 컴포넌트들(보드에 나타나는 Square 컴포넌트들)의 리렌더링의 트리거가 된다.
Now you can add X’s to the board… but only to the upper left square. Your handleClick function is hardcoded to update the index for the upper left square (0). Let’s update handleClick to be able to update any square. Add an argument i to the handleClick function that takes the index of the square to update:
이제 보드에 X를 더할 수 있지만... 좌상단 정사각형(첫번째)에만 가능하다. handleClick 함수는 좌상단 정사각형만 업데이트 하도록 하드코딩 되어 있다. handleClick을 어떠한 정사각형도 업데이트 할 수 있도록 업데이트 해보자. 업데이트 하기위해 정사각형의 인덱스를 나타내는 i 인자를 handlClick 함수에 추가하자:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
Next, you will need to pass that i to handleClick. You could try to set the onSquareClick prop of square to be handleClick(0) directly in the JSX like this, but it won’t work:
다음으로, i를 handleClick으로 전달해야 한다. 정사각형의 onSquareClick prop을 JSX 파일에 다음과 같이 직접적으로handleClick(0)으로 지정해보자. 하지만 동작하지는 않을것이다:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
Here is why this doesn’t work. The handleClick(0) call will be a part of rendering the board component. Because handleClick(0) alters the state of the board component by calling setSquares, your entire board component will be re-rendered again. But this runs handleClick(0) again, leading to an infinite loop:
다음은 동작하지 않는 이유이다. handleClick(0)은 보드 컴포넌트 렌더링의 일부가 된다. handleClick(0)가 setSquares를 호출함으로써 보드 컴포넌트의 상태를 바꾸기 때문에, 모든 보드 컴포넌트는 다시 리렌더링된다. 하지만 이것은 handleClick(0)을 다시 실행하고, 무한루프에 빠진다:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
왜 이 문제가 더 일찍 일어나지 않았을까?
When you were passing onSquareClick={handleClick}, you were passing the handleClick function down as a prop. You were not calling it! But now you are calling that function right away—notice the parentheses in handleClick(0)—and that’s why it runs too early. You don’t want to call handleClick until the user clicks!
onSquareClick={handleClick}을 전달했을 때, handleClick 함수를 prop으로 내려줬을 것이다. 호출한것이 아니다! 하지만 이제는 함수를 직접 호출한다 -- handleClick(0) 의 괄호에 유의하자 -- 이것이 너무 빨리 함수를 실행하는 이유이다. handleClick을 유저가 클릭할때까지 호출하고 싶지 않을것이다!
You could fix by creating a function like handleFirstSquareClick that calls handleClick(0), a function like handleSecondSquareClick that calls handleClick(1), and so on. You would pass (rather than call) these functions down as props like onSquareClick={handleFirstSquareClick}. This would solve the infinite loop.
handleClick(0)을 호출하는 handleFirstSquareCllick과 같은 함수를 만들어서 고칠수 있다. handleSecondSquareClick은 handleClick(1)을 호출하고, 이런 방식의 함수이다. 이 함수들을 호출하는것보다 onSquareClick={handleFirstSquareClick}과 같이 prop으로 내려줄 수 있다. 이것은 무한루프를 해결한다.
However, defining nine different functions and giving each of them a name is too verbose. Instead, let’s do this:
하지만, 다른 9개의 함수를 선언하는것과 각각 이름을 지어주는것은 너무 길다. 대신에 다음과 같이 하자:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
Notice the new () => syntax. Here, () => handleClick(0) is an arrow function, which is a shorter way to define functions. When the square is clicked, the code after the => “arrow” will run, calling handleClick(0).
새로운 () => 문법을 보자. () => handleClick(0)은 함수를 더 짧게 정의하는 방법인 화살표 함수이다. 정사각형이 클릭되었을때, => "화살표" 뒤에 있는 코드가 실행되고, handleClick(0)을 호출한다.
Now you need to update the other eight squares to call handleClick from the arrow functions you pass. Make sure that the argument for each call of the handleClick corresponds to the index of the correct square:
이제 전달한 화살표 함수로부터 handleClick을 호출하도록 다른 8개의 정사각형들을 업데이트하면된다. handleClick을 호출한 각 호출에 대한 인자가 올바른 정사각형의 인덱스에 해당하는 확인해야한다.
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
};
이제 클릭함으로써 보드의 어느 정사각형에라도 X를 더할 수 있다:
하지만 이번에 모든 상태 관리는 Board 컴포넌트에 의해 관리된다!
코드는 다음과 같이 될 것이다:
Now that your state handling is in the Board component, the parent Board component passes props to the child Square components so that they can be displayed correctly. When clicking on a Square, the child Square component now asks the parent Board component to update the state of the board. When the Board’s state changes, both the Board component and every child Square re-renders automatically. Keeping the state of all squares in the Board component will allow it to determine the winner in the future.
이제 Board 컴포넌트에서 상태를 다루기 때문에 부모 Board 컴포넌트가 자식 Square 컴포넌트들에게 props로 전달해준다. 그래서 정확하게 보일 수 있다. Square를 클릭할 때, 자식 Square 컴포넌트는 이제 부모인 Board 컴포넌트에게 보드의 상태를 업데이트하도록 요청한다. Board의 상태가 바뀌었을 때, Board 컴포넌트와 모든 자식 Square 컴포넌트는 자동적으로 리렌더링 된다. Board 컴포넌트의 모든 정사각형들의 상태를 유지하는것은 후에 승자를 결정하도록 한다.
Let’s recap what happens when a user clicks the top left square on your board to add an X to it:
- Clicking on the upper left square runs the function that the button received as its onClick prop from the Square. The Square component received that function as its onSquareClick prop from the Board. The Board component defined that function directly in the JSX. It calls handleClick with an argument of 0.
- handleClick uses the argument (0) to update the first element of the squares array from null to X.
- The squares state of the Board component was updated, so the Board and all of its children re-render. This causes the value prop of the Square component with index 0 to change from null to X.
X를 추가하기 위해 보드에 있는 좌상단 정사각형을 클릭했을 때 어떤 일이 일어나는지 다시 보자:
- 좌상단 정사각형을 누르는것은 Square로부터 onClick prop으로 받은 함수를 실행시킨다. Square 컴포넌트는 Board로부터 onSquareClick prop으로 함수를 받는다. Board 컴포넌트는 JSX에 직접적으로 함수를 정의했다. 0의 인자를 가진 handleClick을 부른다.
- handleClick은 인자(0)를 이용해서 null에서 X로 squares 배열의 첫번째 인자를 업데이트한다.
- Board 컴포넌트의 squares 상태가 업데이트 되고, 그래서 Board와 모든 자식들은 리렌더링 된다. 인덱스가 0인 Square 컴포넌트의 value prop이 null에서 X로 바뀌는 이유이다.
마지막엔 유저는 좌상단 정사각형을 클릭하면 빈 요소에서 X로바뀌는 것을 볼 수 있다.
Note
The DOM <button> element’s onClick attribute has a special meaning to React because it is a built-in component. For custom components like Square, the naming is up to you. You could give any name to the Square’s onSquareClick prop or Board’s handleClick function, and the code would work the same. In React, it’s conventional to use onSomething names for props which represent events and handleSomething for the function definitions which handle those events.
DOM의 <button> 요소의 onClick 속성은 React에게 특별한 의미이다. 왜냐하면 컴포넌트에 내장되어있기 때문이다. Square같은 사용자 지정 컴포넌트와 같은 이름들은 내가 이름짓기에 달려있다. Square의 onSquareClick prop이나 Board의 handleClick 함수에는 아무 이름이나 지어줄 수 있다. 그래도 코드는 똑같이 동작한다. React 에선, 이벤트를 나타내는 prop에는 onSomething과 같은 이름을 사용하고 이 이벤트를 다루는 함수는 handleSomething과 같이 이름짓는것이 관례적이다.
Why immutability is important (불변성이 중요한 이유)
Note how in handleClick, you call .slice() to create a copy of the squares array instead of modifying the existing array. To explain why, we need to discuss immutability and why immutability is important to learn.
handleClick에서 이미 존재하는 배열을 변경하는것 대신에 정사각형들의 배열의 복사본을 만들기 위해 .slice()를 호출한것에 집중하자. 왜그랬는지 설명하기 위해, 불변성과 왜 불변성이 중요한지 배울 필요가 있다.
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. Here is what it would look like if you mutated the squares array:
데이터를 변경하는데 보통 두가지 접근이 있다. 첫번째 접근은 데이터의 값을 직접 바꿈으로써 데이터를 변경하는것이다. 두번째 접근은 원하는 변경사항이 있는 새로운 복사본을 이용해서 데이터를 바꾸는것이다. 만약 squares 배열을 변경하면 다음과 같다:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];
그리고 squares 배열을 변경하지 않고 데이터를 변경한다면 다음과 같을것이다.
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
The result is the same but by not mutating (changing the underlying data) directly, you gain several benefits.
결과는 같지만 값을 직접 바꾸지 않음으로써 여러 이점을 얻을 수 있다.
Immutability makes complex features much easier to implement. Later in this tutorial, you will implement a “time travel” feature that lets you review the game’s history and “jump back” to past moves. This functionality isn’t specific to games—an ability to undo and redo certain actions is a common requirement for apps. Avoiding direct data mutation lets you keep previous versions of the data intact, and reuse them later.
불변성은 복잡한 기능을 더 쉽게 구현하게 만든다. 이 튜토리얼 나중에, 게임의 기록을 복기할 수 있는 "time travel" 기능과 지난 움직임으로 돌아가기 위해 "jump back" 기능을 구현할 것이다. 이 기능은 게임에게만 특정된 것이 아니다 -- 특정 행동을 되돌리고 반복하는것은 앱의 일반적인 요구사항이다. 직접적인 데이터 변경을 하지않는것은 이전의 데이터의 버전을 건드리지 않고, 나중에 다시 사용할 수 있도록 한다.
There is also another benefit of immutability. By default, all child components re-render automatically when the state of a parent component changes. This includes even the child components that weren’t affected by the change. Although re-rendering is not by itself noticeable to the user (you shouldn’t actively try to avoid it!), you might want to skip re-rendering a part of the tree that clearly wasn’t affected by it for performance reasons. Immutability makes it very cheap for components to compare whether their data has changed or not. You can learn more about how React chooses when to re-render a component in the memo API reference.
불변성의 이점으로 또 다른것도 있다. 기본적으로, 부모 컴포넌트의 상태가 변할때 자동적으로 모든 자식 컴포넌트들은 리렌더링 된다. 이것은 변경에 영향을 받지 않는 자식 컴포넌트들도 포함한다. 리렌더링이 유저에게 눈에띄지 않더라도 (적극적으로 피하려고 하면 안된다!), 성능을 위해서 완전히 영향을 받지않은 트리의 일부는 리렌더링을 건너뛰고 싶을 수 있다. 불변성은 데이터가 바뀌었는지 비교하는데 매우 적은 비용을 들인다. memo API reference에서 React가 컴포넌트를 리렌더링할 시기를 선택하는 방법에 대해 더 알아볼 수 있다.
Taking turns (번갈아가며)
It’s now time to fix a major defect in this tic-tac-toe game: the “O”s cannot be marked on the board.
이제 이 tic-tac-toe 게임에서 가장 큰 문제를 고칠 차례이다: "O"가 보드에 표시될 수 없다.
You’ll set the first move to be “X” by default. Let’s keep track of this by adding another piece of state to the Board component:
기본적으로 첫번째 이동을 "X"로 설정한다. Board 컴포넌트에 또다른 상태를 추가함으로써 이를 추적해보자:
function Board() {
const [xIsNext, setXIsNext] = useState(true); // <-
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
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. You’ll update the Board’s handleClick function to flip the value of xIsNext:
플레이어가 움직일때마다, xIsNext (boolean 형)는 어떤 플레이어가 다음에 할지 결정하기 위해 값을 뒤집을 것이고 게임의 상태는 저장될것이다. Board의 handleClick 함수를 xIsNext의 값을 뒤집기 위해 업데이트 할 것이다:
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
return (
//...
);
}
이제 다른 정사각형들을 클릭할 때, X와 O 사이를 바꿀 수 있을것이다!
하지만, 여기엔 문제가 있다. 같은 정사각형을 여러번 눌러보자:
The X is overwritten by an O! While this would add a very interesting twist to the game, we’re going to stick to the original rules for now.
X가 O로 덮어씌워진다! 게임을 매우 흥미롭게 하도록 꼬을수 있지만, 지금은 원래 규칙대로 할것이다.
When you mark a square with a X or an O you aren’t first checking to see if the square already has a X or O value. You can fix this by returning early. You’ll check to see if the square already has a X or an O. If the square is already filled, you will return in the handleClick function early—before it tries to update the board state.
X나 O로 정사각형을 마킹할때 만약 정사각형이 이미 X나 O로 마킹이 되어있는지 확인하지 않는다. 먼저 반환함으로써 고칠 수 있다. 만약 정사각형이 이미 X나 O를 가지고 있는지 보기위해 확인해야 한다. 만약 정사각형이 이미 채워져 있다면, handleClick 함수에서 return 해야 한다 -- 보드의 상태를 업데이트 하려 하기 전에.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
이제 빈 정사각형에만 X나 O를 추가할 수 있다! 이 시점에서 코드는 다음과 같을것이다:
Declaring a winner (승자 선언하기)
Now that the players can take turns, you’ll want to show when the game is won and there are no more turns to make. To do this you’ll add a helper function called calculateWinner that takes an array of 9 squares, checks for a winner and returns 'X', 'O', or null as appropriate. Don’t worry too much about the calculateWinner function; it’s not specific to React:
이제 플레이어들은 번갈아가면서 진행할 수 있고, 게임을 이겼을 때나 더 진행할 수 없을 때 보여주고 싶을것이다. 이를 하기 위해 9개의 정사각형들의 배열을 얻어서 승자를 확인하고 'X'나 'O'나 null을 적절히 반환하는 calculateWinner 도우미 함수를 추가할 것이다. calculatorWinner 함수에 대해 너무 걱정하지 않아도 된다; React에만 국한된것이 아니다:
export default function Board() {
//...
}
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;
}
You will call calculateWinner(squares) in the Board component’s handleClick function to check if a player has won. You can perform this check at the same time you check if a user has clicked a square that already has a X or and O. We’d like to return early in both cases:
플레이어가 이겼는지 확인하기 위해 Board 컴포넌트의 handleClick 함수안에서 calculateWinner(squares)를 호출할것이다. 사용자가 이미 X나 O가 있는 정사각형을 클릭했는지 확인하는 동시에 이 확인을 수행할 수 있다. 두가지 경우 모두 빨리 return 하고싶을것이다:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
To let the players know when the game is over, you can display text such as “Winner: X” or “Winner: O”. To do that you’ll add a status section to the Board component. The status will display the winner if the game is over and if the game is ongoing you’ll display which player’s turn is next:
플레이어들에게 게임이 끝났다는것을 알리기 위해서, "Winner: X"나 "Winner: O"같은 텍스트를 보여줄 수 있다. 이것을 하기 위해 Board 컴포넌트에 status 부분을 추가할 것이다. 이 status는 게임이 끝나면 승자를 표시하고 게임이 진행중이라면 다음 플레이어를 표시할것이다:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
이제 tic-tac-toe 게임이 잘 동작 될 것이다! 그리고 React의 기초들을 배웠다. 코드는 다음과 같을 것이다:
Adding time travel (시간 여행 더하기)
마지막 연습으로, 게임에서 이전의 움직임으로 "시간을 거슬러 가기"가 가능하도록 만들어보자.
Storing a history of moves (움직인 기록 저장하기)
If you mutated the squares array, implementing time travel would be very difficult.
만약 squares 배열을 바꾼다면, 시간 여행을 구현하는 것은 매우 어려울 것이다.
However, you used slice() to create a new copy of the squares array after every move, and treated it as immutable. This will allow you to store every past version of the squares array, and navigate between the turns that have already happened.
하지만, 모든 움직임 후에 squares 배열의 새로운 복사본을 만들기 위해 slice()를 사용했고, 불변성을 가지도록 만들었다. 이것은 squares 배열의 모든 지난 버전을 저장하도록 했고, 이미 지났던 턴들 사이를 탐색할 수 있다.
You’ll store the past squares arrays in another array called history, which you’ll store as a new state variable. The history array represents all board states, from the first to the last move, and has a shape like this:
이전 squares 배열들을 history라는 또다른 배열에 저장할 것이고, 새로운 상태 변수로써 저장할 것이다. history 배열은 처음부터 끝까지의 모든 보드의 상태를 나타내고, 다음과 같은 모양을 가질것이다:
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
Lifting state up, again (다시 상태 끌어올리기)
You will now write a new top-level component called Game to display a list of past moves. That’s where you will place the history state that contains the entire game history.
이제 지난 움직임들의 리스트를 보여주기 위해 Game 이라는 새로운 최상위 컴포넌트를 작성할 것이다. 모든 게임 기록을 포함하는 history 상태가 위치할 곳이다.
Placing the history state into the Game component will let you remove the squares state from its child Board component. Just like you “lifted state up” from the Square component into the Board component, you will now lift 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.
Game 컴포넌트에 history 상태를 위치시키는것은 자식인 Board 컴포넌트에서 squares 상태를 지우도록 해준다. 단지 Square 컴포넌트에서 Board 컴포넌트로 "상태를 끌어올리기" 한 것 처럼, Board에서 최상위 Game 컴포넌트로 끌어올릴 것이다. 이것은 Game 컴포넌트가 Board의 데이터들을 완전히 제어하고 history로부터 이전의 턴들을 렌더링하도록 Board에게 지시하도록 한다.
First, add a Game component with export default. Have it render the Board component and some markup:
첫번째로, export default로 Game 컴포넌트를 추가한다. Board 컴포넌트와 일부 마크업을 렌더링하도록 한다:
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
Note that you are removing the export default keywords before the function Board() { declaration and adding them before the function Game() { declaration. This tells your index.js file to use the Game component as the top-level component instead of your Board component. The additional divs returned by the Game component are making room for the game information you’ll add to the board later.
function Board() { 선언 전에 export default 키워드를 지우고 있고 function Game() { 선언 전에 추가한것을 보자. 이것은 index.js 파일에게 Board 컴포넌트 대신에 Game 컴포넌트를 최상위 컴포넌트로 사용하겠다고 알려준다. Game 컴포넌트에 의해 반환되는 추가적인 div들은 나중에 보드에 추가할 게임 정보를 위한 공간을 만든다.
다음 플레이어와 이동 기록을 추적하기 위해 Game 컴포넌트에 일부 상태를 추가한다:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
Notice how [Array(9).fill(null)] is an array with a single item, which itself is an array of 9 nulls.
[Array(9).fill(null)]이 9개의 null들이 있는 배열이 있는 단일 항목인 배열이 어떻게 되는지 주목해야 한다.
To render the squares for the current move, you’ll want to read the last squares array from the history. You don’t need useState for this—you already have enough information to calculate it during rendering:
현재 움직임에 대한 정사각형을 렌더링하려면, history에서 마지막 정사각형들의 배열을 읽어야 한다. 이것을 위해 useState를 사용할 필요가 없다 -- 렌더링 하는동안 계산된 충분한 정보가 이미 있다:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
Next, create a handlePlay function inside the Game component that will be called by the Board component to update the game. Pass xIsNext, currentSquares and handlePlay as props to the Board component:
다음으로, Game 컴포넌트 안에 게임을 업데이트 하도록 Board 컴포넌트에의해 호출되는 handlePlay 함수를 만들자. xIsNext, currentSquares 그리고 handlePlay를 Board 컴포넌트에게 prop으로 전달하자:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
// TODO
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}
Let’s make the Board component fully controlled by the props it receives. Change the Board component to take three props: xIsNext, squares, and a new onPlay function that Board can call with the updated squares array when a player makes a move. Next, remove the first two lines of the Board function that call useState:
Board 컴포넌트가 받게되는 prop들에 의해 완전히 제어되도록 해보자. Board 컴포넌트를 세가지 prop을 받도록 바꾸자: xIsNext, squares, 그리고 플레이어가 움직일 때 정사각형 배열을 업데이트 하도록 Board가 호출할 수 있는 새로운 onPlay 함수이다. 다음으로, useState를 호출하는 함수인 Board의 첫 두줄을 지우자:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
Now replace the setSquares and setXIsNext calls in handleClick in the Board component with a single call to your new onPlay function so the Game component can update the Board when the user clicks a square:
이제 Board 컴포넌트 안에서 setSquares와 setXIsNext 호출 대신에 단일 호출인 새로운 onPlay 함수로 바꾸면 Game 컴포넌트는 사용자가 정사각형을 클릭할 때 Board를 업데이트시킬 수 있다:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
The Board component is fully controlled by the props passed to it by the Game component. You need to implement the handlePlay function in the Game component to get the game working again.
Board 컴포넌트는 Game 컴포넌트에 의해 전달되는 prop들에 의해 완전히 제어된다. 게임이 다시 돌아가기 위해서 Game 컴포넌트 안에 handlePlay 함수를 구현할 필요가 있다.
What should handlePlay do when called? Remember that Board used to call setSquares with an updated array; now it passes the updated squares array to onPlay.
handlePlay를 호출하면 어떻게 될까? 보드는 업데이트된 배열과 함께 setSquares를 호출하는것을 기억해야 한다; 이제 업데이트된 squares 배열을 onPlay로 전달한다.
The handlePlay function needs to update Game’s state to trigger a re-render, but you don’t have a setSquares function that you can call any more—you’re now using the history state variable to store this information. You’ll want to update history by appending the updated squares array as a new history entry. You also want to toggle xIsNext, just as Board used to do:
리렌더링하는 트리거가 되기위해 handlePlay 함수는 Game의 상태를 업데이트 시킬 필요가 있지만, 더이상 setSquares 함수를 호출할 수 없다 -- 이제 이 정보를 저장하기 위해 history 상태 변수를 사용한다. 새로운 기록으로써 업데이트된 squares 배열에 추가함으로써 history 를 업데이트할 것이다. 또한 Board가 했던 것처럼 xIsNext를 토글링 할 수 있다:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
Here, [...history, nextSquares] creates a new array that contains all the items in history, followed by nextSquares. (You can read the ...history spread syntax as “enumerate all the items in history”.)
여기에 [...history, nextSquares] 는 history에 있는 모든 요소를 포함하고, 다음에 nextSquares를 포함하는 새로운 배열을 만든다. (...history 전개 문법을 "history 안의 모든 요소를 열거한다"라고 읽어도 된다)
For example, if history is [[null,null,null], ["X",null,null]] and nextSquares is ["X",null,"O"], then the new [...history, nextSquares] array will be [[null,null,null], ["X",null,null], ["X",null,"O"]].
예를 들어, 만약 history가 [[null,null,null], ["X",null,null]] 이고 nextSquares가 ["X",null,"O"] 라면, 새로운 [...history, nextSquares] 배열은 [[null,null,null], ["X",null,null], ["X",null,"O"]] 가 될 것이다.
At this point, you’ve moved the state to live in the Game component, and the UI should be fully working, just as it was before the refactor. Here is what the code should look like at this point:
이 시점에서, 상태를 Game 컴포넌트 안에 있게 이동했고, UI는 리팩토링 전과 마찬가지로 완전히 작동해야 한다. 이 시점에서 코드는 다음과 같을것이다:
Showing the past moves (지난 움직임들 보여주기)
Since you are recording the tic-tac-toe game’s history, you can now display a list of past moves to the player.
tic-tac-toe 게임의 기록을 기록하기 시작한 후로부터, 플레이어에게 지난 움직임의 목록을 보여줄 수 있게 되었다.
React elements like <button> are regular JavaScript objects; you can pass them around in your application. To render multiple items in React, you can use an array of React elements.
<button> 같은 React 요소들은 보통의 JavaScript 객체이다; 애플리케이션에서 전달할 수 있다. React의 많은 요소들을 렌더링하기 위해, React 요소들의 배열을 사용할 수 있다.
You already have an array of history moves in state, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the array map method:
이미 state에 움직인 기록들의 배열을 가지고 있고, 이제 React 요소들의 배열로 변환할 필요가 있다. JavaScript 에선, 한 배열을 또다른 것으로 변환하기 위해서 배열 map 메소드를 사용한다:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
You’ll use map to transform your history of moves into 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 component:
움직인 history를 화면에 보여지는 버튼 React 요소로 변환하고, 지난 움직임들로 "jump" 하기 위해 버튼들의 목록을 보여주기 위해 map을 사용할 수 있다. Game 컴포넌트에 history에 map을 써보자:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {
// TODO
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
You can see what your code should look like below. Note that you should see an error 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`. You’ll fix this error in the next section.
코드가 아래와 같이 보일것이다. 개발자 도구의 콘솔에 이렇게 보이는것을 보자: 배열이나 반복자에 있는 각각의 자식은 유일무이한 "key" prop을 가져야 한다. 'Game'의 렌더링 메소드를 확인해보자. 이 에러는 다음 섹션에서 고칠것이다.
As you iterate through history array inside the function you passed to map, the squares argument goes through each element of history, and the move argument goes through each array index: 0, 1, 2, …. (In most cases, you’d need the actual array elements, but to render a list of moves you will only need indexes.)
map에 전달한 함수 내에서 history 배열을 반복할 때 squares 인자는 history의 각 요소를 통과하고, move 인자는 각 배열의 인덱스: 0, 1, 2, ... 를 통과한다. (대부분의 경우, 실제로 배열 요소가 필요하지만 이동 목록을 렌더링하려면 인덱스만 필요하다)
For each move in the tic-tac-toe game’s history, you create a list item <li> which contains a button <button>. The button has an onClick handler which calls a function called jumpTo (that you haven’t implemented yet).
tic-tac-toe 게임의 기록의 각각의 움직임을 해 <button> 버튼을 포함하는 리스트 아이템인 <li>를 만들 수 있다. 버튼은 jumpTo라는 함수를 호출하는 onClick 핸들러를 가진다. (아직 구현하지 않았다)
For now, you should see a list of the moves that occurred in the game and an error in the developer tools console. Let’s discuss what the “key” error means.
지금은, 게임에서 일어난 이동 목록과 개발자 도구콘솔의 오류가 표시되어야 한다. "key" 오류가 무엇을 의미하는지 논의해보자.
Picking a key (key 선택하기)
When you render a list, React stores some information about each rendered list item. When you update a list, React needs to determine what has changed. You could have added, removed, re-arranged, or updated the list’s items.
리스트를 렌더링할때, React는 각각의 렌더링된 리스트 아이템들에 대한 몇몇 정보를 저장한다. 리스트를 업데이트 할 때, React는 무엇이 바뀌었는지 확인해야 한다. 리스트의 아이템들을 더하고, 지우고, 다시 나열하고, 혹은 업데이트 했을 수 있다.
아래와 같은 코드에서
<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>
In addition to the updated counts, a human reading this would probably say that you swapped Alexa and Ben’s ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and can’t know what you intended, so you need to specify a key property for each list item to differentiate each list item from its siblings. If your data was from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.
업데이트된 카운트 외에도 사람이 이것을 읽으면 Alexa와 Ben의 순서를 바꾸고 Alexa와 Ben 사이에 Claudia를 삽입했다고 말할 수 있다. 하지만, React는 컴퓨터 프로그램이고 무엇을 의도했는지 알수없기 때문에 형제 요소와 각각 구분할 수 있는 특별한 key 속성을 지정할 필요가 있다. 만약 데이터가 데이터베이스로부터 왔다면, Alexa, Ben, 그리고 Claudia의 데이터베이스의 ID는 key로 활용될 것이다.
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
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.
리스트가 리렌더링 될 때, React는 각각의 리스트 아이템의 키를 얻고 키를 매칭시키기 위해 이전의 리스트의 아이템을 찾는다. 만약 현재 리스트가 이전에 존재하지 않았던 키를 가지고 있다면, React는 컴포넌트를 만든다. 만약 현재 리스트가 이전 리스트에 존재했던 키를 잃어버렸다면, React는 이전의 컴포넌트를 없애버린다. 만약 두개의 키가 일치한다면, 해당 컴포넌트가 이동된다.
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들은 React에게 각각의 컴포넌트의 id에 대해 알려주므로, React가 리렌더링 된것들 사이에 상태를 유지하도록 해준다. 만약 컴포넌트의 키가 바뀐다면, 컴포넌트는 파괴될것이고 새로운 상태로 다시 만들어질 것이다.
key is a special and reserved property in React. 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 is passed as props, React automatically uses key to decide which components to update. There’s no way for a component to ask what key its parent specified.
key는 React에서 특별하고 예약된 속성이다. 한 요소가 만들어질때, React는 key 속성을 내보내고 반환되는 요소에 직접 key를 저장한다. key가 props로 전달되는것처럼 보여도, React는 자동적으로 어떤 컴포넌트들이 업데이트할지 정하는데 사용한다. 컴포넌트는 부모가 지정한 키를 요청할 방법이 없다.
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.
동적인 리스트들을 만들 때 key를 할당하는 것을 매우 추천한다. 만약 적절한 키를 가지고 있지 않다면, 아마도 그렇게 되도록 데이터를 재구성하는것을 고려해야한다.
If no key is specified, React will report an error 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 error but has the same problems as array indices and is not recommended in most cases.
만약 키를 지정하지 않는다면, React는 에러를 보여줄 것이고 기본적으로 배열의 인덱스를 키로 사용한다. 배열의 인덱스를 키로 사용하는것은 아이템을 다시 정렬하거나 리스트의 아이템을 삽입/삭제할때 문제가 된다. 명시적으로 key={i} 라고 전달하는 것은 에러를 보여주지 않지만 같은 문제가 있을것이고 대부분 추천하지 않는다.
Keys do not need to be globally unique; they only need to be unique between components and their siblings.
키는 전역적으로 유일무이할 필요는 없다; 단지 컴포넌트들과 그들 형제들 사이에만 유일무이하면 된다.
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. Moves will never be re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.
tic-tac-toe 게임의 기록에서, 각각의 지난 움직임들은 다음과 연관된 유일무이한 ID를 갖는다: 움직임의 일련된 숫자이다. 움직임들은 절대 다시 정렬되지 않고, 지워지지 않고, 중간에 삽입되지 않기때문에 움직인 index를 키로 사용하는것은 안전하다.
In the Game function, you can add the key as <li key={move}>, and if you reload the rendered game, React’s “key” error should disappear:
Game의 함수 안에서, <li key={move}>와 같이 키를 추가할 수 있고, 만약 렌더링된 게임을 다시 불러온다면, React의 "key"의 에러는 사라질것이다:
Before you can implement jumpTo, you need the Game component to keep track of which step the user is currently viewing. To do this, define a new state variable called currentMove, defaulting to 0:
jumpTo를 구현하기 전에, 유저가 현재 보고있는 단계를 추적하는것을 유지하기 위해서 Game 컴포넌트가 필요하다. 이것을 하기 위해, 기본값이 0인 currentMove라는 새로운 상태변수를 정의해야 한다:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[history.length - 1];
//...
}
Next, update the jumpTo function inside Game to update that currentMove. You’ll also set xIsNext to true if the number that you’re changing currentMove to is even.
다음으로, currentMove를 업데이트 하기위해 Game 내부에 있는 jumpTo 함수를 업데이트 한다. 또한 currentMove를 변경하는 숫자가 짝수면 xIsNext를 true로 설정해야한다.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
You will now make two changes to the Game’s handlePlay function which is called when you click on a square.
- If you “go back in time” and then make a new move from that point, you only want to keep the history up to that point. Instead of adding nextSquares after all items (... spread syntax) in history, you’ll add it after all items in history.slice(0, currentMove + 1) so that you’re only keeping that portion of the old history.
- Each time a move is made, you need to update currentMove to point to the latest history entry.
정사각형을 클릭할 때 호출되는 Game의 handlePlay 함수에 두가지를 바꿀것이다.
- 만약 "시간을 거슬로" 이동한 다음에 해당 지점에서 새로 이동하는 경우, 해당 지점까지의 기록만 유지하려고 한다. 기록의 모든 항목 뒤에 nextSquares를 추가하는 대신에 history.slice(0, currentMove + 1)의 모든 항목 뒤에 추가해서 이전 기록의 해당 부분만 유지하도록 한다.
- 이동할 때마다 최신 기록 항목을 가리키도록 currentMove를 업데이트해야 한다.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
Finally, you will modify the Game component to render the currently selected move, instead of always rendering the final move:
마지막으로, 항상 마지막 움직임을 렌더링 하는것 대신에, 현재 선택된 움직임을 렌더링하기 위해 Game 컴포넌트를 변경할 것이다:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[currentMove]; // <-
// ...
}
If you click on any step in the game’s history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred.
만약 게임의 기록에서 아무 단계나 클릭한다면, tic-tac-toe 보드는 그 단계가 일어난 다음 인것처럼 보드를 보여주기 위해 즉시 업데이트한다.
Final cleanup (최종 정리)
If you look at the code very closely, you may notice that xIsNext === true when currentMove is even and xIsNext === false when currentMove is odd. In other words, if you know the value of currentMove, then you can always figure out what xIsNext should be.
만약 코드를 매우 면밀하게 본다면, currentMove가 짝수일때 xIsNext === true이고, 홀수일때는 xIsNext === false 라는것을 알아차릴것이다. 다른 말로, 만약 currentMove의 값을 알고있다면, xIsNext가 무엇이 올지 항상 알 수 있다.
There’s no reason for you to store both of these in state. In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand. Change Game so that it doesn’t store xIsNext as a separate state variable and instead figures it out based on the currentMove:
상태에 이 두가지를 모두 저장할 필요는 없다. 사실, 항상 state를 많이 쓰는것은 피해야 한다. 상태를 저장하는것을 단순화하면 버그를 줄이고 코드를 더 이해하기 쉽도록 만든다. xIsNext를 상태 변수로 분리해서 저장하지 않고 대신에 currentMove를 기반으로 파악하도록 Game을 변경한다:
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// ...
}
You no longer need the xIsNext state declaration or the calls to setXIsNext. Now, there’s no chance for xIsNext to get out of sync with currentMove, even if you make a mistake while coding the components.
더이상 xIsNext 상태 선언이 필요하지 않거나 setXIsNext를 호출할 필요가 없다. 이제, 만약 컴포넌트를 코딩하는 동안 실수가 생기더라도 currentMove와 동기화 되지 않는 일은 없을 것이다.
Wrapping up (마무리)
이제 tic-tac-toe 게임을 다만들었다! 다음과 같은것들을 만들었다:
- tic-tac-toe를 플레이 할 수 있고,
- 플레이어가 이겼을 때 보여주고,
- 게임 진행에 따라 게임의 기록을 저장하고,
- 플레이어들에게 게임의 기록을 다시 볼 수 있고, 이전의 게임판을 볼 수 있다.
마지막 결과를 확인하자:
시간이 더 있거나 새로운 React 기술을 연습하고 싶다면 tic-tac-toe 게임을 개선할 수 있는 몇 가지 아이디어가 난이도가 높아지는 순서대로 나열되어 있다.
- 현재의 움직임 만을 위해서 버튼 대신 "You are at move #..."와 같은것을 보여주자.
- Board를 정사각형들을 만들기 위해 하드코딩 하는것 대신에 이중 반복문을 사용해서 다시 작성해보자.
- 움직임들을 오름차순이나 내림차순으로 정렬할 수 있는 버튼 추가하기.
- 누군가 이겼을 때, 이기게 만든 3개의 정사각형 하이라이팅하기 (승자가 없을때는 비겼다는 결과를 메시지로 보여주기)
- 움직임 기록 리스트에 모든 움직임의 위치를 보여주기
이 튜토리얼에서, 요소, 컴포넌트, prop, 그리고 상태를 포함하는 React 개념들을 다루었다. 이제 게임을 만들 때 이 개념들이 어떻게 작동하는지 봐왔으므로 Thinking in React를 확인하여 동일한 React 개념의 앱이 UI를 빌드할 때 어떻게 작동하는지 확인하자.
'React 공식문서 (번역, 공부) > Quick Start' 카테고리의 다른 글
Quick Start (0) | 2023.04.26 |
---|