지금까지 한 걸 중간 정리하는 느낌으로 리액트 공식 사이트 자습서에 나온 tictactoe 게임을 만들어보자.
단계별로 이게 뭘 하는건지, 어떤 원리가 사용되었는지, 데이터의 흐름은 어떻게 되는지에
주의하면서 따라가면 될 것 같다.
전체 컴포넌트는 3가지 레벨으로 나눌 수 있는데, <Game/> 컴포넌트 안에 게임판인 </Board>,
그 </Board> 안에 각각의 정사각형을 이루는 <Squares/> 컴포넌트가 있다.
우선 html을 셋업해주자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 이 안에 html element 생성-->
<script type="text/babel">
class Game extends React.Component {
render() {
return(
<div className='game'>
hello
</div>
)
}
}
ReactDOM.render(
<Game/>,
document.querySelector('#root')
)
</script>
</body>
</html>
hello가 잘 나오는지 확인되었으면
<Game/> 컴포넌트의 <div> 내부 값 hello를 <Board/> 로 바꿔주고
Board 컴포넌트를 다음과 같이 작성한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 이 안에 html element 생성-->
<script type="text/babel">
class Board extends React.Component {
render() {
const status = 'Next player : X'
return (
<div>
<div className='status'>{status}</div>
<div className='board-row'>
{0}
{1}
{2}
</div>
<div className='board-row'>
{3}
{4}
{5}
</div>
<div className='board-row'>
{6}
{7}
{8}
</div>
</div>
)
}
}
class Game extends React.Component {
render() {
return(
<div className='game'>
<div id = 'game-board'>
<Board/>
</div>
</div>
)
}
}
ReactDOM.render(
<Game/>,
document.querySelector('#root')
)
</script>
</body>
</html>
이렇게 <Board/> 컴포넌트를 넣고 불러오면 3x3 의 게임판이 만들어진 것이다.
이 <Board/>는 다시 3x3 등분 <Square/>이 될 예정인데,
이는 함수를 하나 선언해 그 함수를 호출하는 방식으로 이루어진다.
다음과 같이 Board 컴포넌트 안에 함수를 선언해주자.
class Board extends React.Component {
renderSquare(i) {
return <Squares/>
}
// 중략
}
이제 이 함수로 {0}, {1}...을 대체하는데, 각각의 <Square/> 컴포넌트를 구분하기 위해
각 컴포넌트의 매개변수를 다르게 줄 것이다.
이를 코드로 구현하면 다음과 같이 된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 이 안에 html element 생성-->
<script type="text/babel">
class Board extends React.Component {
renderSquare(i) {
return <Square/>
}
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>
)
}
}
class Game extends React.Component {
render() {
return(
<div className='game'>
<div id = 'game-board'>
<Board/>
</div>
</div>
)
}
}
ReactDOM.render(
<Game/>,
document.querySelector('#root')
)
</script>
</body>
</html>
현재 <Square/> 컴포넌트가 정의되지 않았으므로 에러가 발생할 것이다.
<Board/> 컴포넌트 위에 <Squares/> 컴포넌트를 정의해주자.
class Square extends React.Component {
render() {
return (
<button className='square'>
{ }
</button>
)
}
}
아무 값도 갖지 않는 버튼이 생성되었다면 문제 없이 작동하는 것이다.
1. 변수값 전달
이제 <Board/>에서 변수값을 <Square/>로 전달해주자.
renderSquare 함수를 다음과 같이 수정한다.
renderSquare(i) {
return <Square value={i}>
}
이렇게 <Board/>에서 <Squares/>로 보낸 데이터를 받아주는 과정이 필요하다.
<Squares/> 컴포넌트를 다음과 같이 수정하자.
class Square extends React.Component {
render() {
return (
<button className='square'>
{this.props.value}
</button>
)
}
}
여기까지 하고 불러왔을 때, 0~8까지 숫자가 있는 버튼이 있다면 문제가 없는 것이다.
이쯤해서 head 부분에 스타일링을 추가해주자.
<style>
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
</style>
2. 사용자와의 상호작용 추가
JS의 addEventListener와 비슷하게 사용자의 클릭 등 입력에 반응해 실행되는 함수를 추가해줄 것이다.
우선 가장 간단한 함수로 클릭하면 클릭되었다고 console.log로 출력하는 함수를 삽입한다.
class Sqaure extends React.Component {
render() {
return (
<button
className='sqaure'
onClick = { () => console.log('clicked') }
>
{this.props.value}
</button>
)
}
}
화면에 보이는 값을 바꾸려면 state 변수를 주고, 클릭에 따라 state 변수를 업데이트 해주어야한다.
state 변수를 선언하고, 클릭시 state 변수를 업데이트하는 함수를 넣어준다.
class Square extends React.Component {
state = {
value:null
}
render() {
return (
<button
className = 'square'
onClick = { () => this.setState({value:'X'}) }
>
{this.state.value}
</button>
)
}
}
여기서는 잠시 <Board/>에서의 값을 가져오는 것이 아닌, state의 값을 사용할 것이기 때문에
button의 값도 {this.state.value}로 바꿔줘야한다.
이제 각 <Square/> 에는 null (공백), X 만이 있게 된다. (숫자는 가지고 있지만 더 이상 렌더링되지 않음)
3. parent, child 컴포넌트 간 상태 공유 (parent <> child, child<> child)
board가 각 squares의 state를 가져와 그걸 합쳐 게임의 상태를 인지할 수도 있지만,
이는 코드의 유지, 보수 측면에서 좋지 않다.
따라서 <Square/>가 아닌 <Board/> 컴포넌트에 게임의 진행 상황 (state)를 저장하고,
숫자를 넘겨주는 것과 동일한 방식으로 게임의 진행 상황도 각 Square에 넘겨줄 것이다.
<Board/> 에 다음과 같이 state를 정의해주자.
class Board extends React.Component {
state = {
squares: Array(9).fill(null)
}
// ..이하 생략
}
각 <Square>들의 상태를 배열로 저장한다. 게임이 진행됨에 따라 null 값들이 O 혹은 X로 대체된다.
이 정보 (배열의 각 요소)들을 <Square>로 전달하기 위해 다음과 같이 renderSquare 함수를 수정한다.
renderSquare(i) {
return <Square value= {this.state.squares[i]}/>
}
여기에 더해 Square가 클릭되었을 때 일어나는 이벤트를 변경해야한다.
현재 Board 컴포넌트는 어떤 <Square>가 채워져있는지에 대한 정보를 담고 있는데, (squares 배열)
<Square/>를 클릭했을 때 <Square/>로 부터 <Board/>를 변경할 방법이 필요하다.
이는 <Board/>에서 <Square/>로 함수를 전달하고,
<Square/>를 클릭 할 때 함수를 호출하는 방식으로 구현할 수 있다.
renderSquare를 수정해보자.
renderSquare(i) {
return(
<Square
value={this.state.squares[i]}
onClick = { () => this.handleClick(i) }
/>
)
}
클릭시 발동하는 handleClick 함수를 <Square/>에 전달한다. (아직 handleClick은 선언하지 않음)
이에 맞춰 <Square/>도 다시 수정을 해줘야한다.
class Square extends React.Component {
render() {
return(
<button
className = 'square'
onClick = { () => this.props.onClick() }
>
{ this.props.value }
</button>
)
}
}
원래 <Square/>의 click시 발동하는 setState 함수를 <Board/>에서 전달한 함수인 this.props.onClick으로 대체했으며,
button의 value도 다시 <Board/>에서 전달한 {this.props.value}로 대체되었다.
이제 <Square/>를 클릭하면 <Board/>에서 받은 onClick 함수가 실행된다.
handleClick 함수는 <Board/> 컴포넌트 안에 다음과 같이 선언해준다.
handleClick(i) {
const squares = this.state.squares.slice()
// state에서 선언한 배열과 동일한 배열을 복사해온다.
squares[i] = 'X'
this.setState({squares:squares})
}
이제 <Squares/> 자체적으로는 가진 변수 (state)가 없고, 모든 변수를 <Board/>에서 받아 사용한다.
4. 순서 만들기
이제 클릭에 따라 X, O가 번갈아 나타나도록 코드를 수정해보자.
다음과 같이 <Board/>의 state 객체 안에 변수를 추가한다.
state = {
squares:Array(9).fill(null),
next : true
}
이제 한 번의 클릭이 일어나면 next의 T/F가 바뀌도록 handleClick 함수를 조정한다.
if문은 여기서 사용할 수 없으므로 삼항 연산자를 사용해야함에 유의한다.
handleClick(i) {
const squares = this.state.squares.slice()
// state의 배열을 복사해온다.
squares[i] = this.state.next ? 'X' : 'O'
// next가 true라면 X, false하면 O를 i번째 인덱스에 넣는다.
this.setState({
squares: squares,
next : !this.state.next
})
// Board 컴포넌트의 state를 update 한다.
// squares 배열은 X/O가 i번째 인덱스에 삽입되고, next는 T/F가 반전된다.
}
<Board/>의 render 함수 내의 status도 순서에 따라 업데이트되도록 바꿔준다.
const status = 'Next player: ' + (this.state.next ? 'X' : 'O')
5. 승자 결정
이제 승패가 결정되었을 때와 더 이상 둘 곳이 없을 때를 알려주는 함수를 추가한다.
다음과 같은 함수를 script element 최상단에 추가한다.
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
}
lines의 배열들대로 배치가 된다면 승자가 결정된다.
예를 들어 lines[0] = [0,1,2] 인데, 그럼 a, b, c가 각각 0, 1, 2의 값을 가진다.
여기서 squares[0] 이 null이 아닌 값 (X or O)이고, squares[1], square[2], squares[0]이 전부 같은 값이라면
승자가 결정된 것이다. (일렬로 같은 값이 3개 배치되었으므로)
이 때 squares[0] 값을 return 해준다.
조건을 만족하는 배열이 없을 경우 null 값을 return 한다.
<Board/>의 render 함수를 calculateWinner 함수를 넣어 수정해준다.
render() {
const winner = calculateWinner(this.state.squares)
let status
if (winner) {
status = 'Winner: ' + winner
}
else {
status = 'Next player: ' + (this.state.next ? 'X' : 'O')
}
// ..이하 생략
}
마지막으로 승자가 결정되면 클릭을 해도 반응이 없도록 handleClick 함수를 수정한다.
handleClick(i) {
const squares = this.state.squares.slice()
if (calculateWinner(squares) || squares[i]) {
return
}
squares[i] = this.state.next ? 'X' : 'O'
this.setState({
squares:squares,
next: !this.state.next
})
}
지금까지 한 코드를 정리하면 다음과 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<style>
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
</style>
</head>
<body>
<div id="root"> </div>
<!-- 이 안에 html element 생성-->
<script type="text/babel">
class Square extends React.Component {
state = {
value:null
}
render() {
return (
<button
className = 'square'
onClick = { () => this.props.onClick() }
>
{ this.props.value }
</button>
)
}
}
class Board extends React.Component {
state = {
squares: Array(9).fill(null),
next: true
}
handleClick(i) {
const squares = this.state.squares.slice()
if (calculateWinner(squares) || squares[i]) {
return
}
squares[i] = this.state.next ? 'X' : 'O'
this.setState({
squares: squares,
next : !this.state.next
})
}
renderSquare(i) {
return <Square
value={this.state.squares[i]}
onClick= { () => this.handleClick(i) }
/>
}
render() {
const status = 'Next player : ' + (this.state.next ? '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>
)
}
}
class Game extends React.Component {
render() {
return(
<div className='game'>
<div id = 'game-board'>
<Board/>
</div>
</div>
)
}
}
ReactDOM.render(
<Game/>,
document.querySelector('#root')
)
</script>
</body>
</html>
'React' 카테고리의 다른 글
#6 Styling (0) | 2022.04.22 |
---|---|
#5 Webpack (0) | 2022.04.22 |
React #3 Class Component - data 변화 인식 (0) | 2022.04.13 |
React #2 Class Component (0) | 2022.04.13 |
React #1 Intro (0) | 2022.04.12 |