본문 바로가기

TIL

TIL - 이벤트 핸들러 연습

React 수업 과제에서 개인 과제로 "피카츄 놀이터 만들기"를 받았는데 이 과제는 이벤트 핸들러를 연습하는 과제였습니다.

배경이 있고 피카츄가 그 배경에 키다운으로 위치를 이동하는 것이고 맵 크기는 10*10, 점프 기능 구현을 해야 해서 그리드로 배경이미지를 입혀서 컴포넌트 맵을 만드는 방법으로 생각했습니다.

// App.jsx

import "./App.css";
import Map from "./components/Map";

function App() {
  const MAP_WIDTH = 10;
  const MAP_HEIGHT = 10;
  const TILE_SIZE = 50;

  return (
    <>
      <div>
        <Map mapWidth={MAP_WIDTH} mapHeight={MAP_HEIGHT} tileSize={TILE_SIZE} />
      </div>
    </>
  );
}

export default App;
// Map.jsx
import PropTypes from "prop-types";
import "./Map.css";

function Map({ mapWidth, mapHeight, tileSize }) {
  return (
    <div
      className="map-container"
      style={{
        gridTemplateColumns: `repeat(${mapHeight}, ${tileSize}px)`,
        gridTemplateRows: `repeat(${mapWidth}, ${tileSize}px)`,
        width: mapWidth * tileSize,
        height: mapHeight * tileSize,
      }}
    >
      {Array.from({ length: mapWidth * mapHeight }).map((_, i) => (
        <div
          key={i}
          className="map-tile"
          style={{
            width: tileSize,
            height: tileSize,
          }}
        >
          {/* {renderPikachu(i)} */}
        </div>
      ))}
    </div>
  );
}

Map.propTypes = {
  mapWidth: PropTypes.number.isRequired,
  mapHeight: PropTypes.number.isRequired,
  tileSize: PropTypes.number.isRequired,
};

export default Map;
// Map.css
.map-container {
  display: inline-grid;
}

.map-tile {
  background-image: url("../../assets/grass.png");
  background-size: cover;
  position: relative;
}

 

 

10 * 10 배경 한 픽셀당 50px

 

 

 

 

function App() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0,
    direction: "right",
    prevDirection: "right",
  });

  const handleKeyDown = (event) => {
    const newPosition = { ...position };

    switch (event.key) {
      case "ArrowLeft":
        newPosition.x = Math.max(newPosition.x - 1, 0);
        newPosition.direction = "left";

        console.log(newPosition);
        break;
      case "ArrowRight":
        newPosition.x = Math.min(newPosition.x + 1, MAP_WIDTH - 1);
        newPosition.direction = "right";

        console.log(newPosition);
        break;
      case "ArrowUp":
        newPosition.y = Math.max(newPosition.y - 1, 0);
        console.log(newPosition);
        break;
      case "ArrowDown":
        newPosition.y = Math.min(newPosition.y + 1, MAP_HEIGHT - 1);
        console.log(newPosition);
        break;
      case " ":
        setPosition(newPosition);
        setTimeout(() => {
          setPosition({ ...newPosition, y: newPosition.y - 1 });
          setTimeout(() => {
            setPosition(newPosition);
          }, 300);
        }, 300);
        break;
    }

    setPosition((prevState) => ({
      ...newPosition,
      prevDirection: prevState.direction,
    }));
  };

 

키다운 이벤트 핸들러는 Math.max와 Math.min을 이용해서 맵 밖으로 피카츄가 나가지 못하게 로직을 작성했고 맵에 있는 타일이 배열의 요소여서 1씩 더하거나 빼줘서 이동시켜서 한 칸씩 이동하게 구현했습니다.

  const renderPikachu = (index) => {
    const x = index % MAP_WIDTH;
    const y = Math.floor(index / MAP_WIDTH);
    if (x === position.x && y == position.y) {
      let flipClass = "";
      if (position.direction !== position.prevDirection) {
        flipClass = " flip";
      }
      return (
        <img
          src={pikachuImage}
          alt="Pikachu"
          className={`rotate-${position.direction}${flipClass}`}
          style={{
            width: "50px",
            height: "50px",
            position: "absolute",
            top: 0,
            left: 0,
          }}
        />
      );
    }
  };

  const MAP_WIDTH = 10;
  const MAP_HEIGHT = 10;
  const TILE_SIZE = 50;

  return (
    <>
      <div>
        <Map
          mapWidth={MAP_WIDTH}
          mapHeight={MAP_HEIGHT}
          tileSize={TILE_SIZE}
          handleKeyDown={handleKeyDown}
          renderPikachu={renderPikachu}
        />
      </div>
    </>
  );
}

export default App;

%연산자로 나머지와  Math.floor를 이용해서 x축과 y축의 위치가 일치하는 곳에 이미지를 렌더링해서 화면에 표시하게 했습니다. 그리고 화면이 전환 될 때 피카츄가 뒤집혀야하는 조건을 충족시키기 위해 prevDirection를 추가해서 이전 방향을 기억해서 prevDirection가 direction랑 다르면 클래스 flip을 추가해서 css 애니메이션을 추가했습니다.

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.rotate-left {
  transition: transform 0.3s ease-in-out;
  transform: rotateY(180deg);
}

.rotate-right {
  transition: transform 0.3s ease-in-out;
  transform: rotateY(0deg);
}

.rotate-left.flip {
  animation: flipToLeft 0.3s ease-in-out;
}

.rotate-right.flip {
  animation: flipToRight 0.3s ease-in-out;
}

@keyframes flipToLeft {
  0% {
    transform: rotateY(0deg);
  }
  100% {
    transform: rotateY(180deg);
  }
}

@keyframes flipToRight {
  0% {
    transform: rotateY(180deg);
  }
  100% {
    transform: rotateY(0deg);
  }
}

구현하면서 느낀 점

이벤트 핸들러로 피카츄를 이동시키는 것과 맵을 만드는 것은 금방 구현 했지만 피카츄를 뒤집는데 시간이 많이 걸린 것 같습니다. 같은 방향으로 이동할 때도 회전하고 위 아래 키를 눌러도 회전했던 문제를  prevDirection로 이전 방향을 기억한 다음 조건문을 통해 해결했습니다.