React/ReactThreeFiber

Edit Mode 구현 (2) - 상태 관리 내 데이터 추가, 컴포넌트 물리엔진 적용, Edit Mode에서의 Object 이동, UI 크기 조정과 시각화 개선

최 수빈 2025. 3. 15. 02:38

 

Edit Mode (편집 모드) 구현

 

  1. 객체 추가 및 이동 기능 구현 (객체 리스트 동적 관리/ 동물 및 공룡을 사용자가 마우스로 선택, 원하는 위치에 배치할 수 있는 기능 구현)
  2. 오브젝트 컴포넌트에 물리엔진 적용
  3. gridHelper 추가하여 편집 모드 시각화 개선
  4. UI 크기 조정

 

상태관리에 데이터 추가

  • Environment 컴포넌트에서 기존에 하드코딩된 동물 제거
  • EditContext.jsx 파일에 데이터를 관리할 objects state생성 후 데이터를 objects state의 초기값으로 넣어줌
  • 데이터 구성
    crypto 객체를 통해 생성된 고유한 uuid
    동물인지 공룡인지 나타내는 type
    position
    rotation

 

data 코드조각

const data = [
  {
    id: crypto.randomUUID(),
    name: "Alpaca",
    type: "animal",
    position: [17, START_Y, 0],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Bull",
    type: "animal",
    position: [23, START_Y, 0],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Cow",
    type: "animal",
    position: [24, START_Y, 0],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Deer",
    type: "animal",
    position: [29, START_Y, 0],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Donkey",
    type: "animal",
    position: [14, START_Y, 10],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Fox",
    type: "animal",
    position: [13, START_Y, 22],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Horse",
    type: "animal",
    position: [15, START_Y, 1],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Husky",
    type: "animal",
    position: [67, START_Y, 10],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "ShibaInu",
    type: "animal",
    position: [40, START_Y, 22],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Stag",
    type: "animal",
    position: [22, START_Y, 40],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "WhiteHorse",
    type: "animal",
    position: [10, START_Y, 10],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Wolf",
    type: "animal",
    position: [4, START_Y, 50],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Apatosaurus",
    type: "dino",
    position: [34, START_Y, 16],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Parasaurolophus",
    type: "dino",
    position: [-15, START_Y, 20],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Stegosaurus",
    type: "dino",
    position: [11, START_Y, 23],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "TRex",
    type: "dino",
    position: [-20, START_Y, 18],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Triceratops",
    type: "dino",
    position: [9, START_Y, 44],
    rotation: [0, 0, 0],
  },
  {
    id: crypto.randomUUID(),
    name: "Velociraptor",
    type: "dino",
    position: [34, START_Y, 7],
    rotation: [0, 0, 0],
  },
];

 

 

 

 

 

상태 관리 (EditContext.jsx)

EditContext : 편집 모드 상태 및 객체 목록을 관리

선택된 동물의 ID와 드래그된 위치를 저장할 수 있도록 구현

 

 

EditContext.jsx

import { createContext, useState } from "react";
import { data } from "./data";

export const EditContext = createContext();

export const EditProvider = ({ children }) => {
  const [isEditMode, setEditMode] = useState(false);
  const [objects, setObjects] = useState(data);
  const [selectedId, setSelectedId] = useState(null);
  const [draggedPosition, setDraggedPosition] = useState(null);

  const onObjectClicked = (id) => (e) => {
    e.stopPropagation();
    setSelectedId(id === selectedId ? null : id); // 선택 해제 가능
  };

  const onPointMove = (e) => {
    setDraggedPosition(Object.values(e.point));
  };

  return (
    <EditContext.Provider
      value={{
        isEditMode,
        setEditMode,
        objects,
        setObjects,
        selectedId,
        setSelectedId,
        draggedPosition,
        setDraggedPosition,
        onObjectClicked,
        onPointMove,
      }}
    >
      {children}
    </EditContext.Provider>
  );
};

 

  • EditProvider 내부에 selectedId, draggedPosition state 추가
    selectedId : 현재 선택된 동물 ID
    draggedPosition : 동물을 드래그하여 이동시킨 최종 위치 저장
  • 동물 객체를 클릭했을 때, selectedId를 설정하는 클릭 이벤트 사용
    클릭한 Id를 seletedId에 넣어주는 함수 구현
    objectId와 seletedId가 같은지 확인하는 isSelected 값을 컴포넌트 내부에 추가
  • gridHelper에 onPointerMove 이벤트를 추가, 마우스 포인터의 위치 받아옴
  • 받아온 이벤트를 draggedPosition 상태에 업데이트

 

 

객체 렌더링 (Environment.jsx)

Environment 컴포넌트에서 objects 배열을 map()을 사용해 동적으로 렌더링

편집 모드에서는 gridHelper를 추가하여 편집 가능 상태를 시각적으로 표시

 

 

Environment.jsx

import { useContext, Fragment } from "react";
import { EditContext } from "./EditContext";
import { Animal } from "./Animal";
import { Dino } from "./Dino";

export const Environment = () => {
  const { isEditMode, objects, onPointMove } = useContext(EditContext);

  return (
    <>
      {isEditMode && (
        <gridHelper onPointerMove={onPointMove} args={[500, 100]} position={[0, 0, 0]} />
      )}

      {objects.map(({ id, ...object }) => (
        <Fragment key={id}>
          {object.type === "animal" ? (
            <Animal objectId={id} {...object} />
          ) : (
            <Dino objectId={id} {...object} />
          )}
        </Fragment>
      ))}
    </>
  );
};

 

 

 

동물 렌더링 & 편집 기능 추가 (Animal.jsx / Dino.jsx)

  • isEditMode 상태에 따라 RigidBody 적용 여부 결정
  • 선택된 동물은 초록색 mesh로 강조 표시
  • 동물을 클릭하면 위치 변경 가능

 

Animal.jsx/Dino.jsx

import { useContext, useRef } from "react";
import { EditContext } from "./EditContext";
import { RigidBody } from "@react-three/rapier";

export const Animal = ({ name, objectId, position, ...props }) => {
  const { isEditMode, selectedId, draggedPosition, onObjectClicked } = useContext(EditContext);
  const group = useRef();
  const isSelected = objectId === selectedId;

  return (
    <>
      {isEditMode ? (
        <group
          scale={[2.5, 2.5, 2.5]}
          onClick={onObjectClicked(objectId)}
          position={isSelected ? draggedPosition : position}
          {...props}
          ref={group}
        >
          {isSelected && (
            <mesh>
              <boxGeometry args={[3, 1, 4]} />
              <meshBasicMaterial transparent opacity={0.7} color={"green"} />
            </mesh>
          )}
          <primitive object={clone}></primitive>
        </group>
      ) : (
        <RigidBody {...props} colliders={"hull"} enabledRotations={[false, false, false]}>
          <group onClick={onObjectClicked(objectId)}>
            <primitive object={clone}></primitive>
          </group>
        </RigidBody>
      )}
    </>
  );
};
  • Animal과 Dino 컴포넌트 내부에서 useContext사용
  • EditContext를 불러온 뒤, RigidBody import
  • isEditMode에 따라 RigidBody와 일반 primitive 객체 분기 처리
  • colliders={"hull"}

 

EditMode에서의 객체 이동