공부/React

[React] Fragments, Feature, 틱택토 게임 등

hyunh404 2024. 1. 16. 02:42
728x90
Fragment

 

JSX 표현식은 하나의 상위 혹은 부모 요소를 가지고 있어야 한다.

값을 하나만 반환할 수 있기 때문이다.

따라서 두개이상의 값을 반환하려고 하면 상위 요소로 묶어 주어야한다.

 

div태그와 같이 불필요하게 코드가 늘어나는 것을 방지하기위한 대안으로 fragment 컴포넌트가 있다.

 

Fragment : 형제 컴포넌트를 감싸는 용도로 사용하는 특수한 태그(대안 : 빈태그<></>)

 

 

Feature 및 State로 컴포넌트 분리

 

예를들어,

App컴포넌트를 간단하게 하기위해 새로운 파일을 형성해 컴포넌트를 분리해주었다. (import, useState 등 활용)

 

따라서 App컴포넌트에서는 컴포넌트 파일만 불러와서 사용하므로 코드가 많이 간결해진 것을 확인할 수 있다.

function App() {
return (
<div>
<Header />
<main>
<CoreConcepts />
<Examples />
</main>
</div>
);
}

 

 

감싸진 요소에 Props 전달

 

스프레드 연산자를 이용하면 모든 props들이 하나의 자바스크립트 객체에 모여 컴포넌트로 들어간다. (...props)

 

예를들어,

...props를 사용하면 props로 명명한 속성 개체에 사용할 수 있다. 커스텀 컴포넌트에 설정된 모든 props는 빌트인 요소로 넘어가게 된다.

반복되는 구조를 이용하여 props을 전달해보았다.

export default function Section({ title, children, ...props }) {
return (
<section {...props}>
<h2>{title}</h2>
{children}
</section>
);
}
<section title="Examples" id="examples">

 

 

여러 JSX 슬롯 & 동적 컴포넌트 타입

 

여러개의 JSX 슬롯을 이용해 TabButton부분을 간결하게 수정할 수 있다.

또한 wrapper를 다양하게 사용하기 위해 컴포넌트 타입을 동적으로 받으면 여러 요소에서 사용이 가능해 진다.

따라서 ButtonsContainers prop을 받아서 타입을 결정해준다. (주의해야할 점은 태그를 소문자로 시작하면 내장요소를 찾게 되어 바로 작동하지 않는다. 따라서 대문자로 시작해주면 된다.)

export default function Tabs({ children, buttons, ButtonsContainer }) {
return (
<>
<ButtonsContainer>{buttons}</ButtonsContainer>
{children}
</>
);
}
<Tabs
ButtonsContainer="menu"
buttons={
<>
<TabButton
isSelected={selectedTopic === "components"}
onClick={() => handleSelect("components")}
>
Components
</TabButton>
<TabButton
isSelected={selectedTopic === "jsx"}
onClick={() => handleSelect("jsx")}
>
JSX
</TabButton>
<TabButton
isSelected={selectedTopic === "props"}
onClick={() => handleSelect("props")}
>
Props
</TabButton>
<TabButton
isSelected={selectedTopic === "state"}
onClick={() => handleSelect("state")}
>
State
</TabButton>
</>
}
>
{TabContent}
</Tabs>

 

 

이미지 저장소 public/ vs src/

 

1. public/

이미지를 public 폴더에 저장하면 파일 내에서 직접참조 가능하다.

개발 서버 및 빌드 프로세스에 의해 공개적으로 제공되기 때문이다.

 

2. src/assets/

이미지를 src/assets/ 폴더에 저장하면 빌드프로세스에 의해 인식되어 최적화되며, 웹사이트에 제공하기 직전에 public폴더에 삽입된다.

 

따라서 빌드프로세스에 의해 처리되지 않는 이미지는 public/ 폴더를 사용해야하고, 컴포넌트 내에서 사용되는 이미지는 src/폴더에 저장되어야 한다.

 

 

Tic-Tac-Toe 게임(틱택토 게임)

 

리액트를 활용하여 간단한 게임을 구축해보았다.

 

게임을 구현하면서 리액트에서 중요한 부분으로 다음과 같은 점들이 있다.

 

1. state를 변경하는 데 해당 상태의 이전 값을 변경하는 경우에는 업데이트 함수로 새로운 함수를 만들어야 한다.

 

예를들어,

플레이어의 이름을 수정하는 버튼을 만들 때, 편집하는 상황에서의 버튼과 편집하지 않는 상태에서의 버튼을 구분하기 위해서 업데이트 함수를 만들어 state를 변경해주었다.

function handleEditClick() {
    setIsEditing((editing) => !editing);
  }

 

이렇게 코드를 작성하면 리액트에서 상태에 대한 변화의 스케줄을 조율한다.

setIsEditing과 같은 상태 변경 함수를 통해 실행하므로 상태변경은 즉각적으로 수행되는 것이 아니라 스케줄을 조율하는 것이다.

따라서 가장 최신의 상태값을 업데이트해 변경사항을 실행한다.

 

2. 불변의 객체 state로 업데이트 한다.

 

만약 state가 객체나 배열이라면, 해당 state를 업데이트할 때 변경이 불가능하도록 하는 것이 좋다.

즉, 이전 상태를 복제해서 새 객체, 배열로 저장해두고, 원본 객체나 배열은 변경되지 않도록 하는 것이다.

만약 메모리 속의 기존 값을 바로 변경하게 되면, 리액트가 실행하는 예정된 state 업데이트 보다 이전에 일어나게 되어 알수없는 오류나 버그가 생길 수 있기 때문이다.

 

따라서 새로운 상수나 변수를 만들어 스프레드연산자를 이용해 기존 배열에 있던 요소를 모두 붙여넣고, 기존 상태의 map메소드를 기존의 배열에 불러올 수 있다.

또한 각 innerArray마다 중첩 배열 구조 하나씩을 가져와서 그 안의 innerArray 요소를 분해할 수 있다.

즉, 이 새로운 배열 안에는 새로운 중첩 배열이 가득 차있고, 이전에 저장한 데이터를 아직 가지고 있으나 게임판이 변경되어 업데이트 된 후에 반환된다.

이렇게 함으로써 state를 변경 불가능한 방식으로 업데이트하는 것이다.

function handleSelectSquare(rowIndex, colIndex) {
    setGameBoard((prevGameBoard) => {
      const updatedBoard = [
        ...prevGameBoard.map((innerArray) => [...innerArray]),
      ];
      updatedBoard[rowIndex][colIndex] = activePlayerSymbol;
      return updatedBoard;
    });

 

 

3. state 끌어올리기

 

게임을 할 때 2명의 플레이어가 돌아가면서 플레이할 수 있도록 하는 것과 플레이어의 기호가 게임판에 표시되도록 하기위해 state 끌어올리기를 사용한다.

또한 어떤 플레이어가 게임판에서 진행 중인지에 대한 정보를 가장 가까운 부모 컴포넌트가 state 끌어올리기를 사용해 제어한다.

따라서 activePlayerSymbol을 받아 플레이어가 번갈아서 게임하도록 구분짓는다.

export default function GameBoard({ onSelectSquare, activePlayerSymbol }) {
  const [gameBoard, setGameBoard] = useState(initialGameBoard);

  function handleSelectSquare(rowIndex, colIndex) {
    setGameBoard((prevGameBoard) => {
      const updatedBoard = [
        ...prevGameBoard.map((innerArray) => [...innerArray]),
      ];
      updatedBoard[rowIndex][colIndex] = activePlayerSymbol;
      return updatedBoard;
    });

    onSelectSquare();
  }
<li className={isActive ? "active" : undefined}>
      <span className="player">
        {editablePlayerName}
        <span className="player-symbol">{symbol}</span>
      </span>
      <button onClick={handleEditClick}>{isEditing ? "Save" : "Edit"}</button>
    </li>

 

 

4. 불변성이 중요한 이유

 

게임이 종료된 후 게임을 다시 원상태로 돌아가 재게임하도록 하기 위해 onClick과 onRestart함수를 이용하여 코드를 작성하였으나 제대로 실행이 되지는 않는다.

이유는 gameTurns를 기반으로 하고 있지만 gameBoard[row][col] = player;로 특정 차례를 한 플레이어의 기호로 내부요소를 덮어쓰고 있다.

문제는 이 값을덮어쓰거나 값을 정하는 gameBoard가 배열로 채워진 배열을 기반으로 한다는 점이다.

객체와 배열은 참조값이다. 즉, 메모리에 보관된다.

따라서 항상 메모리의 같은 객체, 배열을 편집하는 것이다. 즉, 특정 행, 열 조합을 플레이어 기호로 설정할 때 원래 배열의 메모리 내에서 수행하고 있는 것이다.

그렇기에 재시작 시 배열은 초기화되지 않아 제대로 실행이 안된다.

 

따라서 gameBoard를 gameTurns로부터 분리해 배열 복사본을 만들어 준다.

이로인해 gameBoard를 도출할 때 메모리의 기존 배열이 아닌 새로운 배열에 추가하도록 진행하는 불변성을 지키면 재시작이 정상적으로 작동한다.

 

게임 진행 영상

 

게임 코드는 깃허브에 올려두었다.

https://github.com/BB545/-react-Tic-Tac-Toe-Game

728x90

'공부 > React' 카테고리의 다른 글

[React] 앱 디버깅(debugging)  (1) 2024.01.24
[React] 연습 프로젝트 1  (0) 2024.01.17
[React] 컴포넌트, JSX, Prop(속성), 상태 등  (0) 2024.01.14
[React] 자바스크립트 복습 2  (0) 2024.01.10
[React] 자바스크립트 복습 1  (0) 2024.01.09