React - 리액트 틱택토 만들기(함수형)

Moolbum·2022년 6월 29일
0

React

목록 보기
16/23
post-thumbnail

틱택토 만들기

React 공식문서 자습서에 나와있는 틱택토를 타입스크립트와 함수형 컴포넌트로 작업했습니다.



구현목록

구현 목록입니다. 순서대로 진행해보겠습니다!

  • 3 x 3 게임판 구현
  • 클릭시 X, 다음 클릭시 O
  • 중복클릭 제한
  • 승자결정
  • 리셋버튼
  • 히스토리

3x3 게임판

3x3 게임판을 만들어주기 위해 9개의 배열을 이용하려고합니다.
클릭을 할 때 마다 상태값이 변하기 때문에 useState를 이용합니다.

app.tsx

import React, {useState,FC} from 'react';

const app:FC = ():JSX.Element => {
    const [state, setState] = useState<SquareState>({
    squares: Array(9).fill(null), //9개의 빈 배열 생성
    isNext: true,                // 다음클릭 인식
  });
  
  // 사각형 컴포넌트 생성 함수, 숫자를 인자로 받아 value, onClick전달
   const renderSquare = (i: number) => {
    return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
  };
  
  return (
    <Container>
  		<article>
    	{renderSquare(0)} //숫자를 전달
    	{renderSquare(1)}          
    	{renderSquare(2)}          
	    </article>
      <article>
    	{renderSquare(3)}
    	{renderSquare(4)}          
    	{renderSquare(5)}          
	    </article>
      <article>
    	{renderSquare(6)}
    	{renderSquare(7)}          
    	{renderSquare(8)}          
	    </article> 
    </Container>
    
  )
  
}

사각형 컴포넌트는 app.tsx에서 renderSquare 함수의 인자를 받아 생성된 컴포넌트입니다.
Square.tsx

import React from "react";
import styled from "styled-components";

const Square = ({value, onClick,}:{value: string; onClick: (i: React.MouseEvent<HTMLButtonElement>) => void;
}): JSX.Element => {
  return <Button onClick={onClick}>{value}</Button>;
};

export default Square;

클릭시 (X, O), 중복클릭 제한

위에서는 사각형 컴포넌트는 만들었지만 onClick에서 전달하는 함수인 handleClick 는 선언하지 않았습니다. 클릭시 사용되는 함수를 만들어 보겠습니다.

app.tsx

const handleClick = (i: number) => {
  const square = state.squares.slice();
  if(square[i]){
  	return; //중복클릭 제한
  }
  square[i] = state.isNext ? IsNext.x : IsNext.o;
  setState({
    squares: square,
    isNext: !state.isNext,
  });
};

enum을 활용해 X,O를 표현했습니다.

type.tsx

export enum IsNext {
  x = "X",
  o = "O",
}

승자결정

승자를 결정하는 방법은 9칸중에서 3칸이 X,X,X 또는 O,O,O가 나와야합니다.
각 lines에 번호는 사각형 컴포넌트가 생길때 생긴 value값입니다.

app.tsx

  const [winner, setWinner] = useState<Winner>(null);
  const [state, setState] = useState<SquareState>({
    squares: Array(9).fill(null),
    isNext: true,
  });


  const handleClick = (i: number) => {
    const square = state.squares.slice();
    if (calculate(square) || square[i]) { // 승자결정, 중복선택 불가
      return;
    }
    square[i] = state.isNext ? IsNext.x : IsNext.o;
    setState({
      squares: square,
      isNext: !state.isNext,
    });
  };

  const calculate = (squares: Array<string>) => {
    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]; // i= 1일때 a=0, b=1, ,c=2 로
						
      if (
        squares[a] && 
        squares[a] === squares[b] &&
        squares[a] === squares[c]  // [x,x,x]인지 확인 
      ) {
        return squares[a]; // 3가지가 같다면 같은 문구 출력 X or O
      }
    }
    return null; // 하나라도 틀리면 null 반환
  };

  useEffect(() => {
    setWinner(calculate(state.squares));
  }, [state.squares]); // state.squares값이 변할때마다 winner 계산

리셋버튼

리셋버튼은 버튼을 클릭하면 초기값으로 되돌아가게 하면됩니다!
setState함수를 이용해 초기값으로 되돌립니다.

  const [winner, setWinner] = useState<Winner>(null);
  const [state, setState] = useState<SquareState>({
    squares: Array(9).fill(null),
    isNext: true,
  });

  const handleResetClick = (): void => {
    setState({
      squares: Array(9).fill(null),
      isNext: true,
    });
  };

추가적으로 히스토리를 만들겠습니다.
버튼을 클릭하면 상태값을 저장할 수 있도록합니다.

히스토리

useState를 이용해 history를 추가합니다.


const App: FC = (): JSX.Element => {
  const [winner, setWinner] = useState<Winner>(null);
  const [history, setHistory] = useState<History[]>([]); // history State추가
  const [state, setState] = useState<SquareState>({
    squares: Array(9).fill(null),
    isNext: true,
  });

  const handleClick = (i: number) => {
    const square = state.squares.slice(); // 얕은복사
    if (calculate(square) || square[i]) {
      return;
    }
    square[i] = state.isNext ? IsNext.x : IsNext.o;
    setState({
      squares: square,
      isNext: !state.isNext,
    });
    setHistory([...history, { state }]); //클릭을 할 수록[{state},{state}, {state} ....] 이런 배열을 가진다.
  };

  const handleResetClick = (): void => {
    setState({
      squares: Array(9).fill(null),
      isNext: true,
    });
    setHistory([]); //history 초기값 추가
  };

  const renderSquare = (i: number) => {
    return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
  };

  const calculate = (squares: Array<string>) => {
    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;
  };

  const handleHistoryClick = (state: any) => {
    setState({
      squares: state.squares,
      isNext: state.isNext,
    });
  };

  useEffect(() => {
    setWinner(calculate(state.squares));
  }, [state.squares]);

 return (
    <Container>
      <Label radius="round" background="black" fontColor="white">
        Tic Tac Toe
      </Label>

      {winner ? (
        <Section>
          <h2>winner : {winner}</h2>
          <Button onClick={handleResetClick}>Reset</Button>
        </Section>
      ) : (
        <Section>
          <h2>순서 : {state.isNext ? IsNext.x : IsNext.o}</h2>
          <Button onClick={handleResetClick}>Reset</Button>
        </Section>
      )}

      <Game>
        <Board>
          <article>
            {renderSquare(0)}
            {renderSquare(1)}
            {renderSquare(2)}
          </article>
          <article>
            {renderSquare(3)}
            {renderSquare(4)}
            {renderSquare(5)}
          </article>
          <article>
            {renderSquare(6)}
            {renderSquare(7)}
            {renderSquare(8)}
          </article>
        </Board>

        <Ul>
          {history.map(({ state }: any, idx: any) => {
            return (
              <li key={idx}>
                <button onClick={() => handleHistoryClick(state)}>
                  {idx + 1} 번째
                </button>
              </li>
            );
          })}
        </Ul>
      </Game>
    </Container>
  );
};

export default App;




전체코드

app.tsx

import React, { FC, useEffect, useState } from "react";
import styled from "styled-components";
import Label from "./components/Label";
import Square from "./components/Square";
import { IsNext, SquareState, Winner, History } from "./types";

const App: FC = (): JSX.Element => {
  const [winner, setWinner] = useState<Winner>(null);
  const [history, setHistory] = useState<History[]>([]);
  const [state, setState] = useState<SquareState>({
    squares: Array(9).fill(null),
    isNext: true,
  });

  const handleClick = (i: number) => {
    const square = state.squares.slice();
    if (calculate(square) || square[i]) {
      return;
    }
    square[i] = state.isNext ? IsNext.x : IsNext.o;
    setState({
      squares: square,
      isNext: !state.isNext,
    });
    setHistory([...history, { state }]);
  };

  const handleResetClick = (): void => {
    setState({
      squares: Array(9).fill(null),
      isNext: true,
    });
    setHistory([]);
  };

  const renderSquare = (i: number) => {
    return <Square value={state.squares[i]} onClick={() => handleClick(i)} />;
  };

  const calculate = (squares: Array<string>) => {
    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;
  };

  const handleHistoryClick = (state: any) => {
    setState({
      squares: state.squares,
      isNext: state.isNext,
    });
  };

  useEffect(() => {
    setWinner(calculate(state.squares));
  }, [state.squares]);

  return (
    <Container>
      <Label radius="round" background="black" fontColor="white">
        Tic Tac Toe
      </Label>

      {winner ? (
        <Section>
          <h2>winner : {winner}</h2>
          <Button onClick={handleResetClick}>Reset</Button>
        </Section>
      ) : (
        <Section>
          <h2>순서 : {state.isNext ? IsNext.x : IsNext.o}</h2>
          <Button onClick={handleResetClick}>Reset</Button>
        </Section>
      )}

      <Game>
        <Board>
          <article>
            {renderSquare(0)}
            {renderSquare(1)}
            {renderSquare(2)}
          </article>
          <article>
            {renderSquare(3)}
            {renderSquare(4)}
            {renderSquare(5)}
          </article>
          <article>
            {renderSquare(6)}
            {renderSquare(7)}
            {renderSquare(8)}
          </article>
        </Board>

        <Ul>
          {history.map(({ state }: any, idx: any) => {
            return (
              <li key={idx}>
                <button onClick={() => handleHistoryClick(state)}>
                  {idx + 1} 번째
                </button>
              </li>
            );
          })}
        </Ul>
      </Game>
    </Container>
  );
};

export default App;

const Container = styled.article`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  width: 100%;
  margin: 0 auto;

  h1 {
    padding: 8px 15px;
    border-radius: 36px;
    background: black;
    color: white;
    font-size: 28px;
  }

  h2 {
    font-size: 20px;
    padding: 20px 0;
  }
`;

const Game = styled.section`
  width: 30%;
  display: flex;
  justify-content: space-between;
`;

const Board = styled.section`
  article {
    display: flex;
    padding: 0;
    text-align: center;
  }
`;

const Section = styled.section`
  width: 220px;
  display: flex;
  justify-content: space-between;
  align-items: center;
`;

const Button = styled.button`
  height: 35px;
  padding: 5px 20px;
  border-radius: 20px;
  border: none;
  background: black;
  color: white;
  font-size: 18px;
  cursor: pointer;
`;

const Ul = styled.ul`
  button {
    padding: 5px 10px;
    margin: 2px 0;
    border-radius: 4px;
    border: none;
    background: #eeeeee;
    cursor: pointer;
  }
`;

square.tsx

import React from "react";
import styled from "styled-components";

const Square = ({
  value,
  onClick,
}: {
  value: string;
  onClick: (i: React.MouseEvent<HTMLButtonElement>) => void;
}): JSX.Element => {
  return <Button onClick={onClick}>{value}</Button>;
};

export default Square;

const Button = styled.button`
  width: 100px;
  height: 100px;
  margin: -1px -1px 0 0;
  background: #fff;
  border: 1px solid #999;
  font-size: 40px;
  font-weight: bold;
  line-height: 34px;
  cursor: pointer;

  &:focus {
    outline: none;
  }
`;
profile
Junior Front-End Developer 👨‍💻

0개의 댓글