리액트 자습서를 참고해 만든다 

 

자습서: React 시작하기 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

리액트에 대한 기초 설명을 알려주면 진행된다. 리액트는 우리가 아는 것과 같이 최상위 컴포넌트에서 하위 컴포넌트로 Props를 전달할 수 있다. 

 

사용할 리액트 컴포넌트 

1. Square : button 렌더링
2. Board : 9개의 사각형 렌더링

3. Game

 

클릭 시 박스의 값이 바뀌는 상태를 만들어주고 플레이어 X와 Y를 번갈아가며 띄워주면 끝나는 기능이다. 기능을 구현한 후에 자습서를 보며 잘 되어 있는지 확인 후 emotion을 이용해 스타일을 꾸미고 마무리할 예정이다.

 

먼저 자식 컴포넌트부터 구현해보자

가장 하위 자식 컴포넌트인 Square은 부모 컴포넌트에서 주는 값을 받아 채워 넣어주기만 하면 된다.

 

일단 플레이어가 서로 바뀌며 판을 놓는 기능을 완성했다. 클래스형으로 짜여진 코드를 함수형으로 바뀌면서 시간이 좀 오래 걸렸고, 컴포넌트 사이의 이펙트를 잘 못 이해해서 조금 오래 걸렸다ㅠㅠ 틱택토를 빨리 완성하고 리액트 블로그를 만들어보며 토스 사전 과제를 준비해 볼 예정이다. 아직 기본기가 많이 부족한 것을 느꼈다.

 

문제 1. 특정 버튼을 클릭하고 다른 버튼을 클리할 때 플레이어가 변경되어야 하는데 변경되지 않았고 특정 버튼을 두 번 눌러야 플레이어가 변경됐다.

 

처음엔 보드 컴포넌트에 들어오는 판의 위치값인 i가 스퀘어 컴포넌트로 전달되지 않아 생기는 오류라고 생각해서 두 컴포넌트 사이에 i를 연결할려고 노력했다. 스퀘어에 지정된

onClick : () => void

이 타입에서 void가 아니라 리턴값을 가진 함수 타입으로 바꿀려고 했는데 해결책을 못찾았다. (사실 없는거였다. 하위 컴포넌트에서는 클릭되었다는 신호만 올려주면 되는거였다.) 결론적으로 게임 컴포넌트에서 받은 판의 위치값인 i는 보드 컴포넌트의 핸들클릭 함수에 파라미터 값으로 들어가게 하면 되는 문제라 핸들클릭에 i를 적어 number로 ts를 지정해주고, porps인 i를 보드 컴포넌트의 return에서 받아서 핸들클릭 함수로 보내주면 되는거였다.

 

그리고 특정 버튼을 두 번 눌러야 바뀌는 이유는 최상위 컴포넌트인 게임 컴포넌트에서 플레이어의 값을 확인하고 보내줘야하는데 게임 컴포넌트에서 플레이어값을 정한게 아니라 보드 컴포넌트에서 정했기 때문에 특정 버튼을 누른 후 다른 버튼을 눌러도 바뀌지 않던 것이었다. 그래서 플레이어 값 스테이트를 게임 컴포넌트로 올리며 문제를 해결했다.

 

하하 너무 주절주절 적은것 같지만 일단 생각나는 데로 쭉 적고 있다. 추후에 정리해서 다시 올릴 예정이다.

 

코드에 기록한 내용 ▼

    // 핸들클릭 함수의 i를 스퀘어 컴포넌트에서 어떻게 받는가 고민하고 있었는데 생각해보니
    //여기서 i는 그저 Props로 받은 i를 핸들클릭 함수로 보내기 위한 파라미터여서
    //하위 컴포넌트인 스퀘어에 있는 onClick 함수하고는 상관이 없는 거였다.
    //그냥 클릭된거를 전달해주면 보드 컴포넌트에서 알아서 해주니까 i를 넘길 생각은 안해도됨

    //이제 문제는 두 번 클릭해야 플레이어의 값이 바뀌는건데 아마 i를 파라미터로 전달해주면서 해결이 되지 않을까 싶다.
    //왜냐하면 i를 파라미터로 받기 전에는 비동기적으로 클릭할때만 i를 받아왔는데,
    //파라미터로 전달해줌으로써 0-8을 파라미터로 받는 모든 함수에서 작동한다는 거니까 가능하지 않을려나..?
    // 안되네...
    //드디어 됐다 제일 최산단 컴포넌트인 Game 컴포넌트에서 playerCheck state를 만들어주고 props로 넘기니까 된다.

 

조금 부끄럽다..하핳

 

문제 2. 컴포넌트 에러 

    //오류 1
    // Board.tsx:18 Warning: Cannot update a component (`Game`) while rendering a different component (`Board`).
    //컴포넌트를 렌더링 하면서 다른 컴포넌트가 또 렌더링되어 생기는 문제다
    //오류 코드
    //     const handleClick = (i:number) => {
    //        setSquares((prevSquare)=>{
    //            const copySquare = [...prevSquare];
    //            copySquare[i] = playerCheck ? "O" : "X";
    //            setPlayerCheck(!playerCheck);
    //            return copySquare;
    //        });
    //    }
    //위와 같이 짜버리면 Board 컴포넌트를 렌더링하는 도중에 playCheck가 바뀌며 Game 컴포넌트도 렌더링된다
    //그래서 playCheck를 바꿔주는 걸 squares가 바뀐 후에 실행하며 문제를 해결했다.

이것도 당연한건데 아휴..ㅠㅠ 이제 제대로 알았으니까 된거라고 생각한다.

 

계속 똑같은 개념에서 실수하고 있었다. 

승자를 결정하는 기능을 만드는 과정에서

보드의 배열을 board.tsx에 만들었는데 이건 9개의 다른 배열을 만든거라 당연히 한줄의 값이 같은 배열을 발견할 수가 없기 때문에 승자를 찾을 수 없던 것이었다.

 

배열을 최상위에 만들어야 적용이 되는 걸 계속 생각하지 않고 있어서 거의 2시간을 날렸다 하...ㅠㅠ 빨리 마무리하고 토스 사전 과제용 블로그를 만들어야겠다.

 

이제 완성했다

 

Game.tsx

import { useEffect, useState } from "react";
import Board from "./Board";

export default function Game(){

    const [playerCheck, setPlayerCheck] = useState<boolean>(true);  
    const [gameState, setGameState] = useState<string>("");
    const [squares, setSquares] = useState<string[]>(Array(9).fill(null));
    const [history, setHistory] = useState<string[][]>([Array(9).fill(null)]);

    const forMapArr1:number[] = [0, 1, 2];
    const forMapArr2:number[] = [3, 4, 5];
    const forMapArr3:number[] = [6, 7, 8];

    // const winner = winnerChecker(squares);
    // console.log("승자", winner);
    // if(winner){
    //     setGameState(`Winner : ${winner}`);
    // } else {
    //     setGameState(`Next Player : ${playerCheck ? "O" : "X"}`);
    // }

    // 위의 사례도 이전에 핸드클릭 함수 안에서 위너체크 함수를 실행시키고 winner 유즈스테이트에 넣었기 때문에 
    // 여기서 setGameState로 gameState에 결과를 넣는 순간 setwinner 유즈 스테이트가 동시에 실행되면서
    // 무한 루프를 돈것이다.
    // 드디어 해결

    const jumpTo = (index:number) => {
        setSquares(history[index]);
        if(index === 0) {
            setHistory([Array(9).fill(null)]);
        }
    }
    

    return (<div>
        <div>
            {gameState}
        </div>
        <div>
            <div>
                {forMapArr1.map((forMapArr1)=>{
                    return(<Board
                        i={forMapArr1} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
            <div>
                {forMapArr2.map((forMapArr2)=>{
                    return(<Board
                        i={forMapArr2} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
            <div>
                {forMapArr3.map((forMapArr3)=>{
                    return(<Board
                        i={forMapArr3} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
        </div>
        <div>
            {history.map((history, index)=>{
                const desc = index ? 
                    `Go to Move #${index}` :
                    "Go to Start";
                return(<div key={index}>
                    <button onClick={()=>jumpTo(index)}>{desc}</button>
                </div>)
            })}
        </div>    
    </div>)
}

 

 

Board.tsx

import { useEffect, useState } from "react";
import Square from "./Square";
import winnerChecker from "../utills/WinnerChaecker";

interface Props_board {
    i : number;
    playerCheck : boolean;
    setPlayerCheck : React.Dispatch<React.SetStateAction<boolean>>;
    setGameState : React.Dispatch<React.SetStateAction<string>>;
    squares : string[];
    setSquares : React.Dispatch<React.SetStateAction<string[]>>;
    history : string[][];
    setHistory : React.Dispatch<React.SetStateAction<string[][]>>;
}

export default function Board({i, playerCheck, setPlayerCheck, setGameState, squares, setSquares, history, setHistory}:Props_board) {

    const [whoWinner, setWhoWinner] = useState<string | null>("");    

    const handleClick = (i:number) => {
        
        if(whoWinner || squares[i]){
            console.log("게임 끝");
            return
        }    
        setSquares((prevSquare)=>{
            const copySquare = [...prevSquare];
            copySquare[i] = playerCheck ? "O" : "X";
            return copySquare;
        });

        setHistory((prev) => [...prev, [...squares]]);
        setPlayerCheck(!playerCheck);
    }
    
    useEffect(()=>{
        // console.log("현재 배열", squares);
        
        const winner = winnerChecker(squares);
        setWhoWinner(winner);

        if(winner) {
            setGameState(`Winner : ${winner}`);
        } else {
            setGameState(`Next Player : ${playerCheck ? "O" : "X"}`)
        }
        
    },[squares ,playerCheck]);

    

    return(<>
        <Square 
        value={squares[i]}
        onClick={()=>handleClick(i)}
        />
    </>)
    // 여기서 플레이어값을 지정해버리면 하나의 스퀘어에서만 작동한다
    // 그래서 다른 값을 눌렀을 때 그 플레이어 값은 완전 다른 개별적인 값이기에 유기적이지 못한 상태인것
    // 굳

    // 계속 승자가 나왔는데 안나와서 답답했는데 위의 실수랑 똑같이 했었다. 각 보드에서 9개짜리 보드를 만들어도
    // 그 보드는 9개의 각 스퀘어에서 생성될뿐 서로 유기적으로 연결되어 있지 않다.
    // 그래서 계속 비교를 해도 한줄을 채운 보드의 모음은 확인할 수 없으니까 승자가 안나왔다.
    // 제발 한 번 더 생각하고 만들자
    // 내가 만든 이 보드 컴포넌트는 하나의 스퀘어를 만들기 위한 놈이라서
    // 여기서 보드 배열을 만들어도 전체적으로 연결되지 못한다. 

}

 

 

Square.tsx

import { useState } from "react";

interface Props_square {
    value : string | null;
    onClick : () => void;
}

export default function Square({value, onClick}:Props_square){

    return(<>
        <button onClick={()=>onClick()}>
            {value}
        </button>
    </>)
}

 

여기에 emotion을 이용해 꾸민 다음에 완성을 할 예정이다.

+ Recent posts