리액트에 대한 기초 설명을 알려주면 진행된다. 리액트는 우리가 아는 것과 같이 최상위 컴포넌트에서 하위 컴포넌트로 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에서 받아서 핸들클릭 함수로 보내주면 되는거였다.
그리고 특정 버튼을 두 번 눌러야 바뀌는 이유는 최상위 컴포넌트인 게임 컴포넌트에서 플레이어의 값을 확인하고 보내줘야하는데 게임 컴포넌트에서 플레이어값을 정한게 아니라 보드 컴포넌트에서 정했기 때문에 특정 버튼을 누른 후 다른 버튼을 눌러도 바뀌지 않던 것이었다. 그래서 플레이어 값 스테이트를 게임 컴포넌트로 올리며 문제를 해결했다.
하하 너무 주절주절 적은것 같지만 일단 생각나는 데로 쭉 적고 있다. 추후에 정리해서 다시 올릴 예정이다.
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개의 각 스퀘어에서 생성될뿐 서로 유기적으로 연결되어 있지 않다.
// 그래서 계속 비교를 해도 한줄을 채운 보드의 모음은 확인할 수 없으니까 승자가 안나왔다.
// 제발 한 번 더 생각하고 만들자
// 내가 만든 이 보드 컴포넌트는 하나의 스퀘어를 만들기 위한 놈이라서
// 여기서 보드 배열을 만들어도 전체적으로 연결되지 못한다.
}
개발을 하기 전 왜 이 기술들을 사용하는지 먼저 알아보는 것이 중요하다고 생각한다. 각 기술들에 대해 정리를 한 후 틱택토 만들기를 시작할 것이다.
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가 바로 컴포넌트 기반 개발을 지원하는 라이브러이이다.
▷ 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>
)