===== Tic Tac Toe 만들기 ===== ===== 시작하기 ===== [[https://codepen.io/gaearon/pen/oWWQNa?editors=0010|시작 코드]]를 가지고 시작해봅시다. 이 코드는 우리가 구현할 틱택토 게임의 틀을 가지고 있습니다. 필요한 스타일들을 준비해두었기 때문에 JavaScript만 신경쓰면 됩니다. 세 가지 컴포넌트로 구성되어 있습니다. * Square * Board * Game Square컴포넌트는 하나의 ); } } 변경전 : {{:react:tic_tac_toc_before.png}} 변경후 : 랜더링된 결과에서는 각 사각형 안에 숫자가 위치합니다. {{:react:tic_tac_toc_after.png}} 지금까지의 코드는 [[https://codepen.io/gaearon/pen/aWWQOG?editors=0010|이곳]]에서 볼 수 있습니다. ==== 대화형 컴포넌트 ==== 클릭시 "X"로 채워지는 Square컴포넌트를 만들어봅시다. Square의 render()함수에서 반환된 버튼 태그를 다음과 같이 변경해 주세요. class Square extends React.Component { render() { return ( ); } } 이제 사각형을 클릭하면 브라우저에서 알럿창이 뜨는 것 확인할 수 있습니다. 새로운 JavaScript문법인 화살표 함수를 사용하였습니다. onClick prop에 함수를 전달하였습니다. onClick={alert('click')}코드를 작성하고 버튼을 클릭하면 알럿창 대신 경고각 뜨게됩니다. React컴포넌트는 생성자에서 this.state를 설정하여 상태를 가질 수 있습니다. 상태는 각 컴포넌트마다 가지고 있습니다. 사각형의 현재 value값을 상태에 저장하고 클릭할 때 바뀌도록 만들어봅시다. 먼저 상태를 초기화하기 위행 클래스에 생성자를 추가해주세요. class Square extends React.Component { constructor(props) { super(props); this.state = { value: null, } } render() { return ( ); } } JavaScript클래스에서 서브클래서의 생성자를 정의할 때 super();메서드를 명시적으로 호출해줘야 합니다. Square의 render메서드에서 현재 상태의 value값을 표시하고 클릭할 때 바뀌도록 수정해주세요. - ); } } this.setState가 호출될 때마다 컴포넌트가 업데이트되므로 업데이트된 상태가 전달되어 React가 이를 병합하여 하위 컴포넌트와 함께 다시 랜더링합니다. 컴포넌트가 랜더링될 때 this.state.value는 'X'가 되어 그리드 안에 X가 보이게 됩니다. 이제 사각형을 클릭하면 그 안에 X가 표시됩니다. 지금까지의 코드는 [[https://codepen.io/gaearon/pen/VbbVLg?editors=0010|이곳]]에서 볼 수 있습니다. ==== 개발자 도구 ==== 크롬과 파이어폭스의 React개발자 도구 확장 프로그램은 React컴포넌트 트리를 브라우저의 개발자 도구안에서 검사할 수 있게 해줍니다. {{:react:chrome_DevTools.png}} 트리 안의 컴포넌트들의 props와 상태를 검사할 수 있습니다. 설치 후 페이지에서 검사하길 원하는 컴포넌트를 오른쪽 클릭하고 "Inspect"를 클릭하여 개발자도구를 열면 오른쪽 마지막탭에 React탭이 보입니다. CodePen을 사용하여 이 확장 프로그램을 동작시키고 싶다면 추가적으로 필요한 작업들이 있습니다. - 로그인 혹은 회원가입을 하고 이메일을 인증받으세요. - "Fork"버튼을 클릭하세요. - "Change View"를 클릭하고 "Debug mode"를 선택하세요. - 새롭게 열린 탭에서 React탭이 있는 개발자 도구를 볼 수 있습니다. ==== 상태 들어올리기 ==== 이제 틱택토 게임을 위한 기본 블록들이 있습니다. 하지만 아직 각 Square컴포넌트 안에 상태들이 캡슐화되어 있습니다. 더 원활하게 동작하는 게임을 만들기 위한 플레이어가 게임에서 이겼는지를 확인하고 사각형 안에 X와 O를 번갈아 표시해야 합니다. 누가 게임에서 이겼는지 확인하기 위해 Square컴포넌트들을 쪼개지 않고 한 장소에서 9개의 사각형의 value값을 모두 가지고 있어야 합니다. Board가 각 Square의 현재 상태가 무엇인지만 확인해야 한다고 생각할 수도 있습니다. 이 방법은 기술적으로 React에서 가능하기는 하나 코드를 이해하기 어렵고 불안정하고 리팩토링하기 힘들게 만듭니다. 각 Square에 상태를 저장하는 대신에 Board컴포넌트에 이 상태를 저장하는 것이 가장 좋은 방법입니다. 이 Board컴포넌트는 이전에 각 사각형에 인덱스를 표시한 방법과 동일한 방법으로 무엇을 표시할지 각 Square에게 알릴 수 있습니다. 이와 같이 상탤르 상위 컴포넌트로 들어올리는 것은 React컴포넌트들을 리팩토링할 때 가장 많이 사용하는 방법입니다. 이 기회를 통해 연습해봅시다. Board에 생성자를 추가하고 9개의 사각형과 일치하는 9개의 null을 가진 배열을 포함한 상태로 초기화하세요. class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), } } renderSquare(i) { return ; } render() { const status = 'Next player: X'; return (
{status}
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } }
나중에 이것을 다음과 같이 생긴 보드로 채울 예정입니다. [ 'O',null,'X', 'X','X','O', 'O',null, null, ] 현재 Board의 renderSquare메서드는 다음과 같습니다. renderSquare(i) { return ; } Square에 value prop를 전달하도록 수정하세요. renderSquare(i) { return ; } 지금까지의 코드는 [[https://codepen.io/gaearon/pen/gWWQPY?editors=0010|이곳]]에서 볼 수 있습니다. 이제 우리는 사각형이 클릭되면 발생할 변경 사항을 구현해야 합니다. Board컴포넌트는 어떤 사각형이 채워졌는지 저장하고 있습니다. 그렇기 때문에 Square가 Board가 가지고 있는 상태로 업데이트할 방법이 필요합니다. 사각형의 컴포넌트 상태가 각자 정의되고 있기 때문에 Board가 Square의 상태를 가지고 올 수 없습니다. 보통의 패턴은 사각형이 클릭될 때 호출되는 함수를 Board로부터 Square에 전달하는 것입니다. Board안의 renderSquare를 다시 변경해봅시다. renderSquare(i) { return ( this.handleClick(i)}/> ); } 가독성을 위해 리턴 안의 요소들을 여러줄로 나누고, 괄호를 추가하여 JavaScript가 세미콜론 없이 코드를 마무리하도록 했습니다. Board에서 Square로 value와 onClick두 개의 props를 전달합니다. onClick Square의 render에 있는 this.state.value를 this.props.value로 변경하세요. * Square의 render에 있는 this.state.value를 this.props.value로 변경하세요. * Square의 render에 있는 this.setState()를 this.props.onClick()로 변경하세요. * 더이상 각 Square가 상태를 가지지 않도록 Square에 정의한 constructor를 삭제하세요. 모든 변경 사항을 구현한 Square컴포넌트는 다음과 같습니다. class Square extends React.Component { render() { return ( ); } } 이제 사각형이 클릭될 때 Board로부터 전달되는 onClick함수를 호출합니다. 어떤 일이 일어나는지 되짚어 봅시다. - 내장된 DOM ); } 여기서는 this.props를 둘 다 props로 바꿔야 합니다. 애플리케이션에 있는 여러 컴포넌트들을 함수 컴포넌트로 구현할 수 있습니다. 함수 컴포넌트는 더 쉽게 작성할 수 있고 React가 더 효율적으로 최적화할 수 있습니다. 코드를 깔끔하게 만들면서 onClick={()=>props.onClick()}을 onClick={props.onClick}으로 바꿨습니다. 함수를 전달하는 것은 이 코드만으로 분합니다. onClick={props.onClick()}는 props.onClick을 호출하기 때문에 동작하지 않습니다. 지금까지의 코드는 [[https://codepen.io/gaearon/pen/QvvJOv?editors=0010|이곳]]에서 보실 수 있습니다. ==== 변화 가져오기 ==== 지금 우리의 게임의 단점은 오로지 X만 플레이할 수 있다는 점입니다. 고쳐봅시다. 기본적으로 첫 이동을 'X'가 되도록 설정해봅시다. Board생성자에서 초기 상태를 수정해주세요. class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), xlsNext: true, }; } 이동할 때마다 xlsNext의 불린 값은 바뀌면서 상태에 저장되어야 합니다. Board의 handleClick함수를 xlsNext값이 바뀔 수 있도록 수정해봅시다. handleClick(i) { const squares=this.state.squares.slice(); squares[i] = this.state.xlsNext?'X':'O'; this.setState({ squares: squares, xlsNext: !this.state.xlsNext, }); } 이제 X와 O가 순서대로 번갈아 나타납니다. 다음에 무엇이 표시될 때 보여주기 위해 Board의 render에서 "status"텍스트를 바꿔봅시다. render() { const status = 'Next player: ' + (this.state.xlsNext?'X':'O'); return( // the rest has not changed 지금까지의 코드는 [[https://codepen.io/gaearon/pen/KmmrBy?editors=0010|이곳]]에서 볼 수 있습니다. ==== 승자 알려주기 ==== 언제 게임에서 이기는지 표시해봅시다. 파일 맨 하단에 헬퍼 함수를 추가해주세요. 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 Board의 render함수에서 누가 게임에서 이겼는지 확인할 수 있도록 호출할 수 있습니다. 또 누군가 이겼을때 "Winner:[X/O]" 상태 텍스트를 표시할 수 있습니다. Board의 render에서 status를 선언을 수정해주세요. render() { const winner = calculateWinner(this.state.squares); let status; if(winner){ status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xlsNext?'X':'O'); } return ( // the rest has not changed Board에서 handleClick을 일찍 반환하여 이미 누군가 이긴 게임에서 클릭하거나 이미 칠해진 사각형을 클릭하는 경우 무시하도록 변경할 수 있습니다. 축하합니다.! 틱택토 게임을 완성하였습니다! 이제 React의 기초를 알았습니다. 여기서 진짜 승자는 여러분입니다. 지금까지의 코드는 [[https://codepen.io/gaearon/pen/LyyXgK?editors=0010|이곳]]에서 볼 수 있습니다. ==== 히스토리 저장하기 ==== 보드의 이전 상태로 되돌려 이전 상태가 표시되도록 만들어봅시다. 이동이 있을때마다 새 squares배열을 만들었습니다. 덕분에 이전 상태의 보드를 쉽게 저장할 수 있습니다. 상태에 이와 같은 객체를 저장해봅시다. history = [ { square: [null,null,null, null,null,null, null,null,null] }, { square: [null,null,null, null,null,null, null,null,null] }, // ... ] 우리는 이동 리스트를 표시하여 응답할 수 있는 더 수준 높은 Game컴포넌트를 만들고 싶습니다. 그래서 Square상태를 Board로 들어올린 것처럼 Board의 상태를 Game으로 들어올려 최상위 레벨에서 필요한 모든 정보를 저장해봅시다. 먼저 생성자를 추가해 Game의 초기상태를 설정해주세요. class Game extends React.Component { constructor(props) { super(props); this.state={ history:[{squares:Array(9).fill(null)}], xlsNext: true, } } render() { return (
{/* status */}
    {/* TODO */}
); } }
그 다음 Board를 수정하여 props를 거쳐 squares를 가져오고 이전에 Square에서 했던 것처럼 Game에서 지정한 onClick prop를 만들어줍시다. 각 사각형의 위치를 클릭 핸들러로 전달하여 어떤 사각형이 클릭되었는지 알 수 있습니다. 필요한 변경 사항은 다음과 같습니다. * Board의 constructor를 삭제하세요. * Board의 renderSquare에 있는 this.state.squares[i]를 this.props.squares[i]로 대체하세요. * Board의 renderSquare에 있는 this.handleClick(i)를 this.props.onClick(i)로 대체하세요. 변경사항을 반영한 Borad컴포넌트는 다음과 같습니다. class Board extends React.Component { handleClick(i) { const squares=this.state.squares.slice(); if( calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xlsNext?'X':'O'; this.setState({ squares: squares, xlsNext: !this.state.xlsNext, }); } renderSquare(i) { return ( this.handleClick(i)}/> ); } render() { const winner = calculateWinner(this.state.squares); let status; if(winner){ status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xlsNext?'X':'O'); } return (
{status}
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } }
Game의 render는 히스토리 전체를 보고 게임상태를 계산하여 가져올 수 있어야 합니다. class Game extends React.Component { constructor(props) { super(props); this.state={ history:[{squares:Array(9).fill(null)}], xlsNext: true, } } render() { const history = this.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if(winner){ status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xlsNext?'X':'O'); } return (
{status}
    {/* TODO */}
); } }
Game에 상태를 랜더링하고 있기 때문에
{status}
를 지우고 Board의 render함수로부터 상태를 계산하는 코드를 지울 수 있습니다.
render() { return (
{status}
{this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)}
{this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)}
{this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)}
); } }
그 다음 Board에서 Game으로 handleClick메서드를 옮겨야 합니다. Board클래스에서 잘라내기를 하고 Game클래스로 붙여넣을 수 있습니다. Game상태는 다르기 때문에 수정해야 할 것이 조금 있습니다. Game의 handleClick은 히스토리 항목을 연결하여 새로운 배열을 만들어 스택에 푸시해야 합니다. handleClick(i) { const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice(); if( calculateWinner(squares) || squares[i] ){ return; } squares[i] = this.state.xlsNext?'X':'O'; this.setState({ history:history.concat([{ squares:squares, }]), xlsNext: !this.state.xlsNext, }); } 여기에서 Board는 readerSquare와 render만 필요합니다. 상태 초기화와 클릭 핸들러는 둘 다 Game에서 동작합니다. 지금까지의 코드는 [[https://codepen.io/gaearon/pen/EmmOqJ?editors=0010|이곳]]에서 보실 수 있습니다. ==== 이동 표시하기 ==== 지금까지 게임에서 진행된 이동을 표시해봅시다. 이전에 React컴포넌트가 클래스로 JS객체이고 그 덕에 데이터를 저장하고 전잘할 수 있다고 배웠습니다. React에서 여러 아이템들을 랜더링하기 위해 React요소의 배열을 전달했습니다. 배열을 빌드하는 가장흔한 방법은 데이터 배열에서 map을 이용하는 것입니다. Game의 render메서드에서 해봅시다. render() { const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move?"Go to move #" + move : "Go to game startg"; return (
  • ); }); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return (
    this.handleClick(i)} />
    {status}
      {/* TODO */}
    ); }
    지금까지의 코드는 [[https://codepen.io/gaearon/pen/EmmGEa?editors=0010|이곳]]에서 볼 수 있습니다. 히스토리의 각 단계에서 ); }); 지금까지의 코드는 [[https://codepen.io/gaearon/pen/PmmXRE?editors=0010|이곳]]에서 보실 수 있습니다. 아직 jumpTo가 정의되지 않았기 때문에 이동 버튼을 클릭하면 에러가 발생합니다. 지금 표시된 단계가 무엇인지 알기 위해 Game상태에 새로운 키를 추가해봅시다. 먼저 Game의 constructor에 stepNumber: 0를 추가해주세요. class Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null) }], stepNumber: 0, xIsNext: true, }; } 그 다음 각 상태를 업데이트하기 위해 Game의 jumpTo메서드를 정의해봅시다. 이 메서드에서는 xlsNext를 업데이트하고, 이동의 인덱스가 짝수라면 xlsNext를 true로 설정합니다. Game클래스에 jumpTo메서드를 추가해주세요. handleClick(i) { // this method has not changed } jumpTo(step) { this.setState({ stepNumber: step, xlsNext: (step % 2) === 0, }) } render() { // this method has not changed } Game handleClick에 상태를 업데이트하기 위해 stepNumber:history.length를 추가하여 새로운 이동이 있을 때마다 stepNumber를 업데이트합니다. 현재 보드의 상태를 읽을 때 handleClick이 stepNumber라고 보고 클릭하는 시간대로 상태를 되돌릴 수 있습니다. handleClick(i) { const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ history: history.concat([{ squares: squares }]), stepNumber: history.length, xIsNext: !this.state.xIsNext, }); } 이제 히스토리의 각 단계를 알기 위해 Game의 render를 수정할 수 있습니다. render() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); // the rest has not changed 지금까지의 코드는 [[https://codepen.io/gaearon/pen/gWWZgR?editors=0010|이곳]]에서 보실 수 있습니다. 이제 이동 버튼을 클릭하면 보드는 즉시 그때 표시된 게임으로 변경됩니다. ==== 마무리 ==== 틱택토 게임을 플레이 해보세요. * 틱택토 게임을 플레이 해보세요. * 한 명의 플레이어가 게임에서 이길 때 이를 알려줍니다. * 게임이 진행되는 동안 이동 기록이 저장됩니다. * 게임 보드의 에전 버전을 표시하기 위해 시간을 되돌릴 수 있습니다. 잘 동작하네요! React가 어떻게 동작하는지 잘 아셨기를 바랍니다. 최종 결과물은 [[https://codepen.io/gaearon/pen/gWWZgR?editors=0010|여기]]에서 확인하세요. **시간이 더 있거나 새로운 스킬들을 연습해보고 싶다면 해볼 수 있는 몇 가지 아이디어가 있습니다. 점점 더 어려운 순으로 배치해두었습니다.** - 움직임 리스트에서 (col,row)형태에 각 움직임 위치를 표시하세요. - 움직임 리스트의 선택된 아이템을 볼드처리하세요. - 하드코딩한 것들 대신 사각형을 두 개의 루프를 사용하여 Board를 다시 작성하세요. - 오름차순 혹은 내림차순 뭐든지 움직임을 정렬하는 버튼을 추가해보세요. - 누군가 이겼을 때 무엇 때문에 이겼는지 세 개의 사각형을 하이라이트하세요. 튜토리얼이 진행되는 동안 우리는 엘리먼트, 컴포넌트, props, 상태를 포함한 React의 수많은 컨셉들을 다뤘습니다. 각 주제에 대한 깊은 설명을 원한다면 [[https://reactjs.org/docs/hello-world.html|남은문서]]를 확인하세요. 컴포넌트정의에 대해 더 많이 배우고 싶다면 [[https://reactjs.org/docs/react-component.html|이 문서]]를 확인하세요.