<!-- index.html -->
<header>
<img src="game-logo.png" alt="Hand-drawn tic tac toe game board" />
<h1>Tic-Tac-Toe</h1>
</header>
빌드 프로세스에 의해 처리되지 않는 이미지는 public/ 폴더를 사용해야하고 대체적으로 사용가능하다. (ex. index.html, favicon)
컴포넌트 내에서 사용되는 이미지는 일반적으로 src/ 폴더에 저장되야한다.
// App.jsx
import Player from "./components/Player.jsx";
function App() {
return (
<main>
<div id="game-container">
<ol id="players">
<Player name="Player 1" symbol="X" />
<Player name="Player 2" symbol="O" />
</ol>
GAME BOARD
</div>
LOG
</main>
);
}
export default App;
handleClick() 함수가 동작하여 setIsEditing(!Editing)을 실행 → 현재 상태의 반대로 상태를 업데이트.buttonText는 isEditing이 true이면 Save를, false이면 Edit을 출력.// Player.jsx
import { useState } from "react";
export default function Player({ name, symbol }) {
const [isEditing, setIsEditing] = useState(false); // 시작할 때에는 수정 중이 아니니까!
let buttonText = isEditing ? "Save" : "Edit";
function handleClick() {
setIsEditing(!isEditing);
}
return (
<li>
<span className="player">
{!isEditing && <span className="player-name">{name}</span>}
{isEditing && <input type="text"></input>}
<span className="playaer-symbol">{symbol}</span>
</span>
<button onClick={() => handleClick()}>{buttonText}</button>
</li>
);
}
handleEditClick 함수가 실행되고 setIsEditing(true)가 실행되면서 헤당 Player 컴포넌트가 재실행된다.playerName을 변수로 설정하여 기본값은 플레이어의 이름을 출력한다.isEdting이 true이면 playerName을 input으로 업데이트한다.import { useState } from "react";
export default function Player({ name, symbol }) {
const [isEditing, setIsEditing] = useState(false); // 시작할 때에는 수정 중이 아니니까!
function handleEditClick() {
setIsEditing(true);
}
let playerName = <span className="player-name">{name}</span>;
if (isEditing) {
playerName = <input type="text" required></input>;
}
return (
<li>
<span className="player">
{playerName}
<span className="playaer-symbol">{symbol}</span>
</span>
<button onClick={handleEditClick}>Edit</button>
</li>
);
}

위에서 코드를 작성하면 사진처럼 동작을 한다.
즉, 완전히 분리된 인스턴스가 각각 생성되어 동일한 로직을 사용할지라도 사용하는 위치가 따로 분리된다.
import { useState } from "react";
export default function Player({ name, symbol }) {
const [isEditing, setIsEditing] = useState(false); // 시작할 때에는 수정 중이 아니니까!
function handleEditClick() {
setIsEditing(!isEditing);
// setIsEditing(isEditing ? false : true); 와 동일한 코드
}
// let btnCaption = 'Edit'
let playerName = <span className="player-name">{name}</span>;
if (isEditing) {
playerName = <input type="text" required value={name}></input>;
// btnCaption = 'Save';
}
return (
<li>
<span className="player">
{playerName}
<span className="playaer-symbol">{symbol}</span>
</span>
<button onClick={handleEditClick}>{isEditing ? "Save" : "Edit"}</button>
</li>
);
}
{playerName}을 제외한 거의 모든 부분이 내가 처음에 작성한 코드와 비슷하다.setIsEditing(()=>{})에서 전달하는 함수를 리액트가 호출하여 자동적으로 현재 상태값을 가지게 되기 때문에 이런 식으로 함수를 이용한다.(즉, 상태 변경 전의 값이 입력되므로)// 이전
function handleEditClick() {
setIsEditing(!isEditing);
}
// 이후
function handleEditClick() {
setIsEditing((editing) => !editing);
}
// Player.jsx
if (isEditing) {
playerName = <input type="text" required value={name}></input>;
}
isEditing===true일 때, playerName을 업데이트 하려고 하는데, 이때 value={name}을 사용했기 때문에 변경하려는 내용을 계속해서 덮어쓰게 된다.defaultValue={name})을 설정하면 해당 문제가 해결된다. → 덮어쓰지 않도록 함.// Player.jsx
import { useState } from "react";
export default function Player({ initialName, symbol }) {
const [playerName, setPlayerName] = useState(initialName);
function handleChange(event) {
setPlayerName(event.target.value);
}
let editablePlayerName = <span className="player-name">{playerName}</span>;
if (isEditing) {
editablePlayerName = (
<input
type="text"
required
defaultValue={playerName}
onChange={handleChange}
></input>
);
}
return (
<li>
<span className="player">
{editablePlayerName}
</span>
</li>
);
}
onChange={handleChange}의 입력값(event)에서 값(event.target.value)을 빼내어 해당값을 다른 값(value={playerName})으로 다시 전달한다.const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
export default function GameBoard() {
return (
<ol id="game-board">
{initialGameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button>{playerSymbol}</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
import Player from "./components/Player.jsx";
import GameBoard from "./components/GameBoard.jsx";
function App() {
return (
<main>
<div id="game-container">
<ol id="players">
<Player initialName="Player 1" symbol="X" />
<Player initialName="Player 2" symbol="O" />
</ol>
<GameBoard />
</div>
LOG
</main>
);
}
export default App;

즉, 이전 상태를 하나 복제해서 새 객체 또는 배열로 저장해놓고 이 복제된 버전을 수정하는 방식을 채용하는 것이 좋다.
import { useState } from "react";
const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
export default function GameBoard() {
const [gameBoard, setGameBoard] = useState(initialGameBoard);
function handleSelectSquare(rowIndex, colIndex) {
setGameBoard((prevGameBoard) => {
const updatedBoard = [
...prevGameBoard.map((innerArray) => [...innerArray]),
];
updatedBoard[rowIndex][colIndex] = "X";
return updatedBoard;
});
}
return (
<ol id="game-board">
{gameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button onClick={() => handleSelectSquare(rowIndex, colIndex)}>
{playerSymbol}
</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
handleSelectSquare함수가 실행되고 해당 함수는 어떤 버튼이 눌렸는지 정보를 받아야하기 때문에, map 함수를 통해 얻은 rowIndex, colIndex를 전달한다.handleSelectSquare : 게임보드의 상태를 업데이트하는데 이전의 상태를 저장하면서 계속 업데이트를 하기 때문에 함수형 사용.const updatedBoard = ...) import { useState } from "react";
function App() {
const [activePlayer, setActivePlayer] = useState("X");
function handleSelectSquare() {
setActivePlayer((curActivePlayer) => (curActivePlayer === "X" ? "O" : "X"));
}
return (
<main>
<div id="game-container">
<ol id="players" className="highlight-player">
<Player
initialName="Player 1"
symbol="X"
isActive={activePlayer === "X"}
/>
<Player
initialName="Player 2"
symbol="O"
isActive={activePlayer === "O"}
/>
</ol>
<GameBoard
onSelectSquare={handleSelectSquare}
activePlayerSymbol={activePlayer}
/>
</div>
</main>
);
}
export default function Player({ initialName, symbol, isActive }) {
return (
<li className={isActive ? "active" : undefined}>
</li>
);
}
export default function GameBoard({ onSelectSquare, activePlayerSymbol }) {
function handleSelectSquare(rowIndex, colIndex) {
setGameBoard((prevGameBoard) => {
const updatedBoard = [
...prevGameBoard.map((innerArray) => [...innerArray]),
];
updatedBoard[rowIndex][colIndex] = activePlayerSymbol; // App에서 받아온 activePlayerSymbol
return updatedBoard;
});
onSelectSquare(); // App에서 받아온 함수 실행
}
}
Log를 출력하기 위해선 다음의 요소가 필요하다.
두번째 요소의 경우 GameBoard.jsx의 State에서 이미 다루었다. 그러나 게임 진행 순서에 대해서는 다루지 않았다. 이를 위해서 App에서 State(상태) 끌어올리기를 한다면 비슷한 정보를 가지고 State를 두 번 쓴 경우가 되므로, 이는 리액트에서 추천하는 것이 아니다.
const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
export default function GameBoard({ onSelectSquare }) {
// const [gameBoard, setGameBoard] = useState(initialGameBoard);
// function handleSelectSquare(rowIndex, colIndex) {
// setGameBoard((prevGameBoard) => {
// const updatedBoard = [
// ...prevGameBoard.map((innerArray) => [...innerArray]),
// ];
// updatedBoard[rowIndex][colIndex] = activePlayerSymbol;
// return updatedBoard;
// });
// onSelectSquare();
// }
return (
<ol id="game-board">
{/* gameBoard.map()~는 향후 수정할 예정 */}
{gameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button onClick={onSelectSquare}>{playerSymbol}</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
import { useState } from "react";
function App() {
const [gameTurns, setGameTurns] = useState([]); // 또다른 State 끌어올리기.
const [activePlayer, setActivePlayer] = useState("X");
function handleSelectSquare(rowIndex, colIndex) {
setActivePlayer((curActivePlayer) => (curActivePlayer === "X" ? "O" : "X"));
setGameTurns((prevTurns) => {
let currentPlayer = "X"; // 초기화
if (prevTurns.length > 0 && prevTurns[0].player === "X") {
currentPlayer = "O"; // 기호가 O인 플레이어가 게임을 할 차례. 가장 최근에 클릭한 버튼은 X 플레이어의 차례였기 때문.
}
const updatedTurns = [
{ square: { row: rowIndex, col: colIndex }, player: currentPlayer },
...prevTurns,
];
return updatedTurns;
});
}
}
function App() {
return(
<GameBoard onSelectSquare={handleSelectSquare} turns={gameTurns}/> // turns라는 속성값을 전달
)
}
const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
export default function GameBoard({ onSelectSquare, turns }) { // App에서부터 turns 속성값을 받아옴.
let gameBoard = initialGameBoard; // 초기값 설정.
// 진행된 turns이 있다면 gameBoard을 오버라이드 할 것이다.
// 반대로 진행된 것이 없다면 gameBoard = initialGameBoard일 것.
for (const turn of turns) {// turns가 있을때만 수행할 반복문
const { square, player } = turn;
const { row, col } = square;
gameBoard[row][col] = player;
}
// ==== 이렇게하면 파생된 상태를 생성하게 되는 것임 ====
return (
<ol id="game-board">
{gameBoard.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button onClick={() => onSelectSquare(rowIndex, colIndex)}>
{/* App에서 handleSelectSquare() 함수를 받아오는데 이 함수는 rowIndex, colIndex를 필요로 함. */}
{playerSymbol}
</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
// App.jsx
<Log turns={gameTurns} />
// Lob.jsx
export default function Log({ turns }) {
return (
<ol id="log">
{turns.map((turn) => {
return (
<li key={`${turn.square.row},${turn.square.col}`}>
{turn.player} 플레이어가 {turn.square.row}, {turn.square.col}을
선택했습니다.
</li>
);
})}
</ol>
);
}

import { useState } from "react";
import Player from "./components/Player.jsx";
import GameBoard from "./components/GameBoard.jsx";
import Log from "./components/Log.jsx";
function App() {
const [gameTurns, setGameTurns] = useState([]); // 또다른 State 끌어올리기.
const [activePlayer, setActivePlayer] = useState("X");
function handleSelectSquare(rowIndex, colIndex) {
setActivePlayer((curActivePlayer) => (curActivePlayer === "X" ? "O" : "X"));
setGameTurns((prevTurns) => {
let currentPlayer = "X";
if (prevTurns.length > 0 && prevTurns[0].player === "X") {
currentPlayer = "O"; // 기호가 O인 플레이어가 게임을 할 차례. 가장 최근에 클릭한 버튼은 X 플레이어의 차례였기 때문.
}
const updatedTurns = [
{ square: { row: rowIndex, col: colIndex }, player: currentPlayer },
...prevTurns,
];
console.log(updatedTurns);
return updatedTurns;
});
}
return (
<main>
<div id="game-container">
<ol id="players" className="highlight-player">
<Player
initialName="Player 1"
symbol="X"
isActive={activePlayer === "X"}
/>
<Player
initialName="Player 2"
symbol="O"
isActive={activePlayer === "O"}
/>
</ol>
<GameBoard onSelectSquare={handleSelectSquare} turns={gameTurns} />
</div>
<Log turns={gameTurns} />
</main>
);
}
export default App;
function deriveActivePlayer(gameTurns) {
let currentPlayer = "X";
if (gameTurns.length > 0 && gameTurns[0].player === "X") {
currentPlayer = "O"; // 기호가 O인 플레이어가 게임을 할 차례. 가장 최근에 클릭한 버튼은 X 플레이어의 차례였기 때문.
}
return currentPlayer;
}
function App() {
const [gameTurns, setGameTurns] = useState([]); // 또다른 State 끌어올리기.
const activePlayer = deriveActivePlayer(gameTurns);
function handleSelectSquare(rowIndex, colIndex) {
setGameTurns((prevTurns) => {
const currentPlayer = deriveActivePlayer(prevTurns);
const updatedTurns = [
{ square: { row: rowIndex, col: colIndex }, player: currentPlayer },
...prevTurns,
];
console.log(updatedTurns);
return updatedTurns;
});
}
}
null이라면 false값이 된다.<button
onClick={() => onSelectSquare(rowIndex, colIndex)}
disabled={playerSymbol !== null} // <== 작성된 부분
>
{playerSymbol}
</button>
import { WINNING_COMBINATIONS } from "./combination.js";
handleSelectSquare함수에 로직을 작성.import { WINNING_COMBINATIONS } from "./combination.js";
const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
function App() {
let gameBoard = initialGameBoard;
// 진행된 turns이 있다면 gameBoard을 오버라이드 할 것이다. 반대로 진행된 것이 없다면 gameBoard = initialGameBoard일 것.
for (const turn of gameTurns) {
// turns가 있을때만 수행할 반복문
const { square, player } = turn;
const { row, col } = square;
gameBoard[row][col] = player;
}
let winner;
for (const combination of WINNING_COMBINATIONS) {
const firstSquareSymbol =
gameBoard[combination[0].row][combination[0].column];
const secondSquareSymbol =
gameBoard[combination[1].row][combination[1].column];
const thirdSquareSymbol =
gameBoard[combination[2].row][combination[2].column];
if (
firstSquareSymbol &&
firstSquareSymbol === secondSquareSymbol &&
firstSquareSymbol === thirdSquareSymbol
) {
winner = firstSquareSymbol;
}
}
return (
<main>
<div id="game-container">
<ol id="players" className="highlight-player">
...
</ol>
{winner && <p>You won, {winner}!</p>}
<GameBoard onSelectSquare={handleSelectSquare} board={gameBoard} />
</div>
...
</main>
);
}
export default function GameBoard({ onSelectSquare, board }) { // board 속성으로 대체
return (
<ol id="game-board">
{board.map((row, rowIndex) => (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex) => (
<li key={colIndex}>
<button
onClick={() => onSelectSquare(rowIndex, colIndex)}
disabled={playerSymbol !== null}
>
{playerSymbol}
</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
gameBoard 형태로 남아있었다. → 이를 App.jsx에서 처리하여 우승 조건을 탐색하도록 함.import GameOver from "./components/GameOver.jsx";
function App(){
// 무승부 로직
const hasDraw = gameTurns.length === 9 && !winner;
return(
{(winner || hasDraw) && <GameOver winner={winner} />}
)
}
export default function GameOver({ winner }) {
return (
<div id="game-over">
<h2>Game Over!</h2>
{winner && <p>{winner} won!</p>}
{!winner && <p>It's a draw!</p>}
<p>
<button>Rematch!</button>
</p>
</div>
);
}

const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
function App(){
let gameBoard = [...initialGameBoard.map((array) => [...array])]; // gameBoard를 도출할 떄 우리가 메모리의 기존의 배열이 아닌 새로운 배열을 추가하도록 함.
for (const turn of gameTurns) {
const { square, player } = turn;
const { row, col } = square;
gameBoard[row][col] = player;
}
function handleRematch() {
setGameTurns([]);
}
return(
{(winner || hasDraw) && (
<GameOver winner={winner} onRestart={handleRematch} />
)}
)
}
export default function GameOver({ winner, onRestart }) {
return (
<button onClick={onRestart}>Rematch!</button>
);
}
handleRematch()를 실행시켜도 게임보드가 비어있는 상태가 되지 않는다! → 이는 App.jsx에서 let gameBoard = initialGameBoard; 이라고만 설정했기 때문.
App 컴포넌트에 최근 설정된 플레이어의 이름을 저장하는 상태를 추가하자.
function App() {
const [players, setPlayers] = useState({
X: "Player 1",
O: "Player 2",
}); // players 상태는 이름 변경을 저장하는 버튼(Save)가 눌릴때마다 호출되야한다.
function handlePlayerNameChange(symbol, newName) {
setPlayers((prevPlayers) => {
return {
...prevPlayers,
[symbol]: newName, // 자바스크립트 문법. 변경된 플레이어의 기호에 대한 이름을 덮어씀.
};
}); // 기존정보를 바탕으로 함. 왜냐하면 바뀌지 않은 플레이어의 이름이 있을 수 있으니까.
}
// 우승자 가려내기 로직 => 이름을 반영하기
for (const combination of WINNING_COMBINATIONS) {
const firstSquareSymbol =
gameBoard[combination[0].row][combination[0].column];
const secondSquareSymbol =
gameBoard[combination[1].row][combination[1].column];
const thirdSquareSymbol =
gameBoard[combination[2].row][combination[2].column];
if (
firstSquareSymbol &&
firstSquareSymbol === secondSquareSymbol &&
firstSquareSymbol === thirdSquareSymbol
) {
winner = players[firstSquareSymbol]; // 우승자의 이름.
}
}
return(
//...
<Player
initialName="Player 1"
symbol="X"
isActive={activePlayer === "X"}
onChangeName={handlePlayerNameChange}
/>
<Player
initialName="Player 2"
symbol="O"
isActive={activePlayer === "O"}
onChangeName={handlePlayerNameChange}
/>
)
}
export default function Player({
initialName,
symbol,
isActive,
onChangeName,
}) {
const [playerName, setPlayerName] = useState(initialName);
function handleEditClick() {
setIsEditing((editing) => !editing);
// 수정될 때 이름이 변경이 되는 것
if(isEditing){
onChangeName(symbol, playerName)
}
}
}