본문 바로가기

👨‍💻 FrontEnd/🔵 리액트 [React]

[ React ] 공식문서 읽고 틱택톡 예제 따라해보기

📌 이 글은 리액트 공식 문서를 읽으며 이해를 목적으로 작성한 글입니다. 

 

1. 사각형을 클릭하면 사각형 안의 값이 'X'로 변경됨. (useState 사용)

- 각 작은사각형들은 각각의 독립적인 상태관리 변수를 갖고 있다. 

import { useState } from "react";

function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue("X");
    console.log("click!");
  }

  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

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>
    </>
  );
}

 

2. state 끌어올리기 

각각의 작은 사각형의 state 를 부모 컴포넌트에서 관리하여 공유할 수 있도록 설정 변경하기  

Board  컴포넌트는 각각의 Square 컴포넌트들에게 props 로 values를 전달해야한다.  

 

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  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>
    </>
  );
}

 

import { useState } from "react";

function Square({ value }) {
  // const [value, setValue] = useState(null); -> 부모 컴포넌트에서 관리하기로 : 이유 -> 각 상태가 공유되어야 하기 때문

  function handleClick() {
    console.log("click!");
  }

  return (
    <button className="square" onClick={handleClick}>
      {value}
    </button>
  );
}

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  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>
    </>
  );
}

이렇게 작성을 했는데 

그럼 작은 사각형을 클릭했을 때 어떻게 Board 컴포넌트의 값을 변경할 것인가? 

 

클릭을 할 때마다 사각형 안의 값을 x, o, null 등으로 할당을 해줘야 하는데

 

state 는 자기 컴포넌트 안에서 선언한 state에 접근할 수 있기 때문에 Square에서 Board컴포넌트의 값을 바꿀 수는 없다. 

값을 바꿀 수는 없지만 함수를 전달하고, 

사각형이 클릭될 때 Square 가 해당 함수를 호출하도록 하면 된다. 

 

즉, props로 함수를 전달하라는 말! 그 함수를 전달한 상태에서 Square 의 버튼에 onclick 이벤트에 해당 함수를 호출하도록 하는것이다. 

 

function Square({ value, onSquareClick }) {
  // const [value, setValue] = useState(null); -> 부모 컴포넌트에서 관리하기로 : 이유 -> 각 상태가 공유되어야 하기 때문

  function handleClick() {
    console.log("click!");
  }

  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

 

이렇게 Board 컴포넌트에서 props를 넘겨주도록 작성한다. 

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick} />
        <Square value={squares[1]} onSquareClick={handleClick} />
        <Square value={squares[2]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={handleClick} />
        <Square value={squares[4]} onSquareClick={handleClick} />
        <Square value={squares[5]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={handleClick} />
        <Square value={squares[7]} onSquareClick={handleClick} />
        <Square value={squares[8]} onSquareClick={handleClick} />
      </div>
    </>
  );
}

 

 

이렇게 함으로써 왼쪽 위의 사각형의 값을 변경할 수 있게 되었다. 

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  function handleClick() {
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성
    nextSquares[0] = "X";
    setSquares(nextSquares); // 복사한 배열이 값을 변경
  }

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick} />
        <Square value={squares[1]} onSquareClick={handleClick} />
        <Square value={squares[2]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onSquareClick={handleClick} />
        <Square value={squares[4]} onSquareClick={handleClick} />
        <Square value={squares[5]} onSquareClick={handleClick} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onSquareClick={handleClick} />
        <Square value={squares[7]} onSquareClick={handleClick} />
        <Square value={squares[8]} onSquareClick={handleClick} />
      </div>
    </>
  );
}

 

보드 컴포넌트에서 Square 컴포넌트를 클릭했을 때 handleClick으로 해당 컴포넌트의 인덱스 번호를 전달할 방법을 고민해봐야 한다. 

 

그렇다고 

 <Square value={squares[0]} onSquareClick={ handleClick(0)} />

이렇게 함수를 호출할 경우 무한 로딩 오류에 걸릴 수 있다. 

왜냐하면 해당 컴포넌트를 렌더링 하는 과정에서 handleClick(0) 함수를 마주치게 될 경우

해당 함수를 호출하게되면서  

  function handleClick(i) {
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성
    nextSquares[i] = "X";
    setSquares(nextSquares); // 복사한 배열이 값을 변경
  }

 

이 로직이 실행되는데 이는 상태 값을 변경하는 함수이기때문에 

상태값이 변경하게 되면서 다시 컴포넌트가 렌더링되어 무한 로딩에 빠질 수 있다... 

 

따라서 바로 함수가 호출되지 않도록 함수 호출이 아닌 함수 그 자체를 전달해주면 무한 로딩 문제를 피할 수 있는데 

그럼 첫번째 인덱스를 넘겨서 호출하는 함수를 props 로 넘겨주고 ,,,, 두번째 인덱스를 넘겨서 호출하는 함수  props 로 넘겨주고  하면 되는데.. 

 

이건 너무 비효율적이잖아... 

 

따라서 () => fn_name 을 적어서 익명함수? 개념으로 작성을 하면 된다고 한다. 

 <Square value={squares[0]} onSquareClick={() => handleClick(0)} />

 

이렇게 작성할 경우 함수가 바로 호출되지 않고 함수 자체만 props로 넘어가게 된다. 

 

 

다음과 같이 수정할 수 있겠다. 

export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  function handleClick(i) {
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성
    nextSquares[i] = "X";
    setSquares(nextSquares); // 복사한 배열이 값을 변경
  }

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(1)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(3)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(4)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(6)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(7)} />
        <Square value={squares[0]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

 

전체 코드 

import { useState } from "react";

function Square({ value, onSquareClick }) {
  // const [value, setValue] = useState(null); -> 부모 컴포넌트에서 관리하기로 : 이유 -> 각 상태가 공유되어야 하기 때문

  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

export default function Board() {

  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  function handleClick(i) {
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성
    nextSquares[i] = "X";
    setSquares(nextSquares); // 복사한 배열이 값을 변경
  }

  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>
    </>
  );
}

 

 🎯 props로 함수를 넘겨줄 때는 익명함수를 사용하여 넘겨줘야 무한 로딩에 빠지지 않고 자식 컴포넌트에서 해당 함수를 호출해서 사용할 수 있다는 점을 배웠다.

순서 정하기 

순서를 정하기 위해 순서 상태를 정해줄 상태 변수를 선언해준다. 

export default function Board() {
  // 순서 정하기 위한 변수
  const [xIsNext, setXIsNext] = useState(true);

  const [squares, setSquares] = useState(Array(9).fill(null)); // 길이가 9인 배열을 null로 초기화 -> Square 컴포넌트로 값을 전달

  function handleClick(i) {
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성

    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    setSquares(nextSquares); // 복사한 배열이 값을 변경
    setXIsNext(!xIsNext); // 순서 변경해주기
  }
function handleClick(i) {
const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성

if (xIsNext) {
  nextSquares[i] = "X";
} else {
  nextSquares[i] = "O";
}
setSquares(nextSquares); // 복사한 배열이 값을 변경
setXIsNext(!xIsNext); // 순서 변경해주기
}

이렇게 클릭이벤트가 발생할때 마다 xIsNext 변수의 상태에 따라서 표기할 기호를 다르게 설정해주고, 

마지막에는 순서를 변경하기 위해 값을 바꿔주면된다. 

 

그렇게 되면 클릭시마다 다른 표기가 나타난다. 

 

승자를 계산하기 위한 함수를 작성 

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;
}

 

이긴 사람의 문자를 'x' or 'o' of null 을 반환해준다. 

 

그리고 이긴 사람이 null이 아닌 경우 즉, 이긴사람이 존재할 경우  바로 return 할 수 잇도록 설정한다. 

즉, 값 변경을 못하도록 설정하는 것이다. 

function handleClick(i) {
    if (squares[i] || calculateWinner(squares)) {
      // 만약 값이 있을 경우 리턴하여 해당 값을 변경 못하도록 설정 + 승자가 있을 경우에도 값 변경 x
      return;
    }
    const nextSquares = squares.slice(); // null로 초기화됐던 배열들을 복사하여 nextSquares 배열을 생성

    if (xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    setSquares(nextSquares); // 복사한 배열이 값을 변경
    setXIsNext(!xIsNext); // 순서 변경해주기
  }

 

더불어서 이긴 사람을 출력하는 것과 현재 차례인 사람을 출력하는 조건문을 작성해주었다. 

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = "이긴 사람 : " + winner;
  } else {
    status = "다음 차례 : " + (xIsNext ? "X" : "O");
  }

 

이제 해당 게임의 기보 즉 history를 보는 기능을 만들면 된다.