emotion을 적용하기 위해 tsconfig.json에

"jsxImportSource": "@emotion/react"

를 입력하자 마자 오류가 생겼다. 

오류 내용 : 

Cannot find module '@emotion/react/jsx-runtime' or its corresponding type declarations

알고 보니까 이건 VS code의 버전 문제였다.

>TypeScript의 버전을 WorkSpace 버전으로 바꾸면 바로 에러가 사라진다.

 

다음으로 CSS  Props 방식으로  사용해보자

 

그리고 그냥 쓰지 말고 최상단에

/** @jsxImportSource @emotion/react */

 

해당 코드를 적어줘야하는데 이는 JSX pragma라고 하는데 Babel 트랜스파일러한테 JSX 코드를 변환할 때, React의 jsx()함수가 아니라 Emotion의 jsx()함수를 대신 사용하라고 알려주기 위해 사용한다.

이를 입력하면 css Props 형식으로 만든 스타일이 적용된다. 

 

이렇게 해서 

// 문자형
const styled = css`
	스타일 값
`

// 객체형
const styled = {css({
	스타일 값
})}

 

해당 방식으로 정의한 후 엘리멘트에 Props 형식으로 보내주면 된다.

 

시간이 부족한 관계로 각 div 크기 고정만하고 마무리했다.

 

 

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

 

자습서: 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을 이용해 꾸민 다음에 완성을 할 예정이다.

개발을 하기 전 왜 이 기술들을 사용하는지 먼저 알아보는 것이 중요하다고 생각한다. 각 기술들에 대해 정리를 한 후 틱택토 만들기를 시작할 것이다.

 

React

1. Virtual DOM 사용

 기존의 웹사이트 구현 방식은 웹 브라우저가 웹사이트의 텍스트 문서를 읽어서 DOM이라는 트리 구조로 바꾸어 사용자에게 보여주고, 웹 개발자는 사용자들에 반응에 따라 웹사이트를 갱신하는 방식이다. 하지만 작은 변화들을 매번 갱신하는 것은 효율적이지 못하고 성능에 좋지 않다. 이러한 문제를 해결하기 위해 React는 Virtual DOM을 사용하여 개발자들이 변화를 주고 싶은 부분만 Virtual DOM에서 수정하고 원래  DOM과의 차이점을 비교해 그 차이점만 반영하며 성능을 향상 시킨다.

2. 기존 DOM의 문제를 해결하기 위해 나온 기술들 중 유일하게 라이브러리임
 사실 기존의 DOM의 방식에 한계를 느껴 Vitual DOM을 적용한 것은 React만이 아니다. React를 제외하고 대표적으로 Angular.js와 Vue.js가 있다. 근데 이들보다 React를 많이 사용하는 이유는 무엇일까? Vue와 Angular는 프레임워크인 반면 React는 라이브러리이기 때문이다. 전체적인 구조와 규칙이 강하게 정해져 있어 그들의 규칙을 따라야하는 프레임워크에 비해 기존 자바스크립트 기반의 문법을 사용할 수 있는 React를 더 선호하는 것이다.

 

 대표적인 부분만 설명하고 넘어가겠지만 이것 이외에도 컴포넌트 기반 아키텍처를 채택하여 컴포넌트를 쉽게 재사용하여 코드를 짜는 과정에서의 효율을 높여주는 것이나 리액트 네이티브의 강점 등 개발자들이 React를 채택한 이유는 정말 많고 다양하다.

 

TypeScript

 타입스크립트란 자바스크립트 기반의 정적 타입 문법을 추가한 프로그래밍 언어이다. 더 자세히 말하자면 자바스크립트의 상위 확장자로 자바스크립트의 엔진을 사용하며 자신이 원하는 변수의 타입을 정의하고 프로그래밍 할 시 자바스크립트로 컴파일하여 실행할 수 있도록 만들어진 언어이다.

 동적으로 타입을 지정하는 자바스크립트의 문제를 해결하고자 나온 언어이며 타입스크립트를 사용하며 컴파일 에러를 예방하고 손쉽게 디버깅을 할 수 있다. 그리고 자바스크립트의 슈퍼셋 즉 상위 확장자 이기 때문에 모든 자바스크립트 프로젝트를 커버할 수 있기 때문에 타입스크립트를 사용하는 것이다.

 

이러한 타입스크립트가 장점만 있는 것은 아니다. 매번 타입 지정을 해야하기에 이 과정에서 발생하는 에러를 해결하며 시간을 뺏기는 등의 생산성 저하가 발생할 수 있다. 근데 이를 해결하기 위한 방법 중 하나가 컴포넌트 기반 개발을 지원하는 라이브러리나 프레임워크를 사용하는 것인데, React가 바로 컴포넌트 기반 개발을 지원하는 라이브러이이다.

 

이 부분에서 React와 타입스크립트를 왜 같이 사용하는 알 수 있다.

- 타입의 종류

number

string

boolean

null

undefined

any(사용하지 않는 것이 좋다.)

...

 

- 간단한 타입 지정 방식 정리

 

▷ 기본 방식

  let 변수:타입지정 = 값

  ex) let b: string = "my Type";

여러 타입 지정

  let 변수: 타입 | 타입 = ~;

배열 지정

  let 변수 : 타입[] = [값, 값, ...]

 

▷ type

 export type 이름 = {

  변수 : 타입 ;

  변수 : 타입 ;

  ...

}

 

▷ generic 방식

  데이터를 부르는 순간에 타입을 정하고 싶을 때 사용

  ex) const [myValue, setMyValue] = useState<타입지정>()

 

▷ interface

보통 Props에 대한 타입을 지정할 때 사용

  inteface 이름 {

  변수 : 타입;

  변수 : 타입;

   ...

 }

 

▷ 함수 타입 지정

  함수를 작성해주고 함수의 리턴타입을 지정해준다. 리턴이 없다면 void로 지정해준다.

 

▷ extends (type에서도 비슷한 기능 존재 >> type 이름 = 기존에 있던 타입 $ {})

interface 이름 extends 기존에 만든 타입 {} 

위와 같이 적어서 기존에 있던 타입을 불러올 수 있다. 이 경우엔 중괄호 안에 아무것도 적지 않아도 된다. 추가된 변수가 있다면 적어준다.

 

▷ Omit

기존에 지정한 타입에서 어떤 것을 뺴고 싶을 때 사용한다.

export type 이름 = Omit<기존 타입, 빼고 싶은 변수 이름>

interface 이름 extends Omit<기존타입, 빼고 싶은 변수이름> {}

 

▷ Pick 

원하는 것만 선택하여 가져오고 싶을 경울 사용 

export type 이름 = Pick<타입 이름, 가져오고싶은 변수이름>

 

▷ API로 호출한 데이터에 타입 지정 (제네릭 문법 활용)

export type 이름<T>{

  data: T[ ],

  같이오는 값 변수 : 타입,

  ...

}

위처럼 지정한 후 API로 데이터를 호출할때 T부분에 불러오는 데이터 타입을 적어주면 된다.


emotion

왜 이모션을 사용하는지에 대해 몇가지를 알아봤다.

  • className이 자동으로 부여되기 때문에 스타일이 겹칠 염려가 없다.
  • 재사용이 가능하다.
  • styled component 사용방식과 css prop 기능을 지원하여 확장에 용이하다.
    • React 사용하기 위해 @emotion/react를 설치하고 styled component를 사용하기 위해 @emotion/styled를 설치한다. 
  • styled component보다 파일 사이즈가 작고, SSR시 서버 작업이 필요없다.

보통 styled component랑 비교를 많이 하는데 styled component보다 크기가 작고 SSR 서버 작업 없이 할 수 있다는 것에서 styled component에 비해 메리트가 있다고 생각했다. 그리고 className이 자동 부여되어 겹치는 것을 방지할 수 있는 것도 유용하기에 emotion을 사용하는 것 같다.

 

- emotion 사용 방법

설치

 

 

 

 

  • CSS Props
yarn add @emotion/react

 

(사용 예시 1)

 /** @jsxImportSource @emotion/react */
 //React의 jsx() 함수를 사용하지 말고 Emotion의 jsx() 함수를 사용한다.
 
import { css } from "@emotion/react";

css style = css`
	color: hotpink;
`

const SomeComponent = ({ children }) => (
	<div css={style}>
    	Some hotpink text.
        {children}
	</div>
);

const anotherStyle = css({
	textDecoration: 'underline'
});

const AnotherComponent = () => (
	<div css={anotherStyle}> Some text with an underline. </div>
)

render(
	<SomeComponent>
    	<AnotherComponent/>
    </SomeComponent>
)

 

(사용 에시 2 : 문자형)

import { css } from "@emotion/react";

export default function MyComponent(){
	return(
    <div
    	css={css`
        	color: blue;
        `}
    >
    문자형
    </div>
    );
}

 

(사용 예시 3 : 객체형)

import { css } from "@emotion/react";

function MyComponent() {
  return (
    <div
      css={css({
        color: "green",
      })}
    >
      객체형
    </div>
  );
}

 

 

  • styled-components
# assuming you already have @emotion/react installed
yarn add @emotion/styled

 

 

개발 환경 셋팅

1. yarn으로 Ts가 적용된 React 설치하기

yarn을 먼저 설치해준다

npm install --global yarn

 

Ts가 적용된 React 설치하기

yarn create react-app tic-tac-to --template typescript

 

 

2. @emotion/react와 @emotion/styled 설치하기

이렇게 React를 설치한 후 폴더로 들어가서 

yarn add @emotion/react @emotion/styled

를 해주면된다.

 

근데 여기서 에러를 만났다

사용할 권한이 없기 때문에 발생하는 에러라고 한다. 권한을 요청하기 위해서 Windows Powershell을 관리자 권한으로 실행해주고 

get-help Set-ExecutionPolicy

를 입력한 후에 Y를 입력하고,

Set-ExecutionPolicy RemoteSigned

를 입력한 후에 Y를 입력하면 권한 설정을 할 수 있다.

 

이렇게 한 후에 다시 입력해보니 별문제 없이 설치되었다.

이제 셋팅을 마무리했으니 개발을 해보자

+ Recent posts