팀 프로젝트에서 Konva.js를 사용하여 사용자가 직접 객체를 편집하는 인터랙티브 UI를 구현했습니다.

실제로 작성한 코드를 바탕으로, 두 가지 핵심 기능인 '드래그 앤 드롭으로 객체 생성'과 'Transformer로 객체 변형하기'를 구현한 방법을  공유하는 글을 작성합니다.


드래그 앤 드롭으로 객체 생성하기

이미지를 캔버스 위로 드래그 앤 드롭하면 마우스를 놓은 위치에 새로운 사각형이 나타나야 합니다.

브라우저가 제공하는 마우스 좌표(clientX, clientY)는 전체 화면 기준입니다. 이 좌표를 Konva 캔버스 내부의 로컬 좌표로 변환해야만 정확한 위치에 객체를 생성해야 합니다.

// EditRoomPage.tsx
const stageRef = React.useRef<Konva.Stage | null>(null);

const handleDrop = (e: React.DragEvent) => {
  e.preventDefault();

  if (!stageRef.current) return;
  const stage = stageRef.current.getStage();

  // 1. 캔버스 DOM 요소의 화면상 위치 정보 가져오기
  const stageBox = stage.container().getBoundingClientRect();

  // 2. 브라우저 좌표를 캔버스 내부 좌표로 변환
  const dropX = e.clientX - stageBox.left;
  const dropY = e.clientY - stageBox.top;

  // 3. 변환된 좌표로 새로운 객체 생성
  const newRoom: RoomDto = {
    id: `new_${Date.now()}`,
    positionX: dropX,
    positionY: dropY,
    width: 120,
    height: 100,
    name: '새로운 방',
    detail: '',
  };
  setRooms((prev) => [...prev, newRoom]);
};

 

  1. stage.container().getBoundingClientRect(): 이 함수는 Konva 캔버스를 감싸는 <div> 컨테이너의 화면 내 위치 정보(시작 left, top 좌표 등)를 가져옵니다. 이는 좌표 변환의 기준점이 됩니다.
  2. e.clientX - stageBox.left: 이 코드는 좌표 변환의 핵심입니다. 브라우저 왼쪽 끝에서 마우스까지의 거리(e.clientX)에서, 브라우저 왼쪽 끝에서 캔버스까지의 거리(stageBox.left)를 빼면 캔버스 내부에의  X좌표가 계산됩니다.
  3. setRooms(...): 계산된 dropX, dropYpositionX, positionY로 사용하여 새로운 방 객체를 만들고, React의 useState를 통해 rooms 배열 상태를 업데이트하여 화면에 렌더링합니다.

Transformer로 객체 크기 및 위치 조절하기

사용자가 사각형을 클릭하면 크기 조절 핸들이 나타나고, 핸들을 조작하여 변경한 크기와 위치가 화면에 나타나야 합니다.

Konva의 Transformer는 객체의 크기와 위치를 변경합니다. 이 변경 사항을 onTransformEnd 이벤트가 끝나는 시점에 찾아내어 rooms 배열과 동기화 합니다

// EditRoomPage.tsx
const handleTransformEnd = (room: RoomDto, node: Konva.Rect) => {
  // 1. Transformer가 변경한 scale 값을 가져온다.
  const scaleX = node.scaleX();
  const scaleY = node.scaleY();

  // 2. 새로운 너비와 높이를 계산하고, 변경된 위치도 가져온다.
  const updatedRoom: RoomDto = {
    ...room,
    positionX: node.x(),
    positionY: node.y(),
    width: Math.max(30, node.width() * scaleX), // 최소 크기
    height: Math.max(30, node.height() * scaleY),
  };

  // 3. 다음 변형을 위해, 노드의 시각적 scale을 1로 초기화한다.
  node.scaleX(1);
  node.scaleY(1);
  
  // 4. 계산된 최종 값으로 React 상태를 업데이트한다.
  setRooms((prev) => {
    const updatedRooms = prev.map((r) =>
      r.id === room.id ? updatedRoom : r
    );
    return updatedRooms;
  });
};
  1. node.scaleX(), node.scaleY() : onTransformEnd의 콜백 인자로 들어온 node는 변형이 적용된 Konva 객체입니다. 이 객체의 scale 속성을 통해 얼마나 크기가 변했는지 알 수 있습니다.
  2. node.width() * scaleX : Transformer는 객체의 width 자체를 바꾸는 것이 아니라 scale을 조절합니다. 따라서 최종 너비는 기존 너비 * 스케일 값으로 직접 계산해주어야 합니다. node.x()node.y()를 통해 변경된 최종 위치도 가져옵니다.
  3. node.scaleX(1) :  상태 업데이트 후 Konva 노드의 scale 값을 다시 1로 초기화해야 합니다. 그렇지 않으면 다음 크기 조절 시, 이전 scale 값에 새로운 scale이 곱해져서 원치 않은 결과가 나타납니다. 시각적 표현은 React 상태에 맡기고, Konva 노드의 scale은 항상 1로 유지해야 합니다.
  4. setRooms(...): 계산된 updatedRoom 객체로 rooms 배열에서 해당 ID를 가진 요소를 찾아 교체한 후, 새로운 배열로 상태를 업데이트합니다.

정리

위의 구현을 통하여 라이브러리가 제공하는 편리한 기능의 내부 동작 원리를 이해하는 것 또한 중요하다는 것을 배웠습니다.
특히,
canvas 환경에서의 좌표계 문제나 상태 동기화 이슈를 직접 겪고 해결하는 것은 지금까지와 다른 구현과는 많이 다른 새로운 경험이였습니다.

 

728x90

'React' 카테고리의 다른 글

[React] LocalStorage(로컬스토리지) - 저장, 불러오기, 삭제  (1) 2025.05.29
[React] Props(프롭스)란?  (0) 2025.05.28
[React] 리액트 타입 연결하기  (0) 2025.05.28
[React] UseState란?  (0) 2025.05.27
[React] UseMemo란?  (0) 2025.05.22

로컬 스토리지란?

로컬스토리지는 브라우저에 정보를 저장하는 기능입니다.
예를 들어, 내가 입력한 데이터를 새로고침해도 남아있게 하고 싶을 때 사용할 수 있습니다.
내 컴퓨터 브라우저에 데이터를 저장하는 상자라고 생각할 수 있습니다.

    • 브라우저를 껐다 켜도 데이터가 존재합니다.
    • 문자(문자열)만 저장할 수 있습니다.
    • 로그인 정보, 테마 설정, 임시 데이터 저장 등에 사용합니다.
    • 로컬스토리지는 문자(문자열)만 저장할 수 있습니다.

그러나, 우리가 사용하는 객체배열은 문자열이 아니기 때문에 바로 저장할 수 없으므로

저장할 때는 JSON.stringify()로 문자열로 바꿔서 저장하고

불러올 때는 JSON.parse()로 다시 원래 객체/배열로 변환해야 합니다.

 

로컬스토리지 사용법

1. 데이터 저장하기

// 'username'이라는 이름으로 '김강현' 저장
localStorage.setItem('username', '김강현');

2. 데이터 불러오기

// 'username'이라는 이름의 데이터 꺼내오기
const name = localStorage.getItem('username');
console.log(name); // '김강현'

3. 데이터 삭제하기

// 'username' 데이터 삭제
localStorage.removeItem('username');

4. 객체 저장 & 불러오기

// 객체를 문자열로 변환해서 저장
const user = { name: '김강현', age: 30 };
localStorage.setItem('user', JSON.stringify(user));

// 저장된 데이터는 실제로 이렇게 보입니다:
// '{"name":"김강현","age":30}'

// 불러올 때는 다시 객체로 변환
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // '김강현'
console.log(savedUser.age);  // 30

5. 배열 저장 & 불러오기

// 배열을 문자열로 변환해서 저장
const fruits = ['사과', '바나나', '포도'];
localStorage.setItem('fruits', JSON.stringify(fruits));

// 불러올 때는 다시 배열로 변환
const savedFruits = JSON.parse(localStorage.getItem('fruits'));
console.log(savedFruits[0]); // '사과'

정리

  • 로컬스토리지는 브라우저에 간단한 정보를 저장할 때 사용합니다.
  • 문자열만 저장 가능하므로 객체/배열은 JSON.stringifyJSON.parse를 꼭 사용해야 합니다.
  • 새로고침이나 브라우저를 껐다 켜도 데이터가 남아있어서 편리합니다.
  • 브라우저에 데이터가 존재하므로 중요한 개인정보(비밀번호, 토큰 등)등의 저장에 주의해야 합니다.
리액트에서는 useEffectuseState를 활용해 로컬스토리지와 상태를 연동할 수 있습니다.
(입력값을 저장하고, 새로고침해도 입력값이 남아있게 만들기가 가능합니다.)
728x90

'React' 카테고리의 다른 글

[Konva.js] 드래그 앤 드롭과 객체 변형 구현하기  (2) 2025.07.27
[React] Props(프롭스)란?  (0) 2025.05.28
[React] 리액트 타입 연결하기  (0) 2025.05.28
[React] UseState란?  (0) 2025.05.27
[React] UseMemo란?  (0) 2025.05.22

리액트 Props(프롭스)란?

Props는 "properties"의 줄임말로, 부모 컴포넌트가 자식 컴포넌트에 값을 전달할 때 사용하는 데이터입니다.
함수의 매개변수처럼, 컴포넌트에 값을 넘겨주고, 자식 컴포넌트는 그 값을 받아서 화면에 표시하거나 로직에 사용할 수 있습니다.
props는 읽기 전용이며, 자식 컴포넌트에서 직접 값을 변경할 수 없습니다.


Props 사용법

1. 부모 컴포넌트에서 값 전달

function Parent() {
  return <Child name="김강현" age={30} />;
}
  • nameage라는 props를 Child 컴포넌트에 전달합니다.
  • 문자열은 따옴표("), 숫자나 변수, 객체는 중괄호({})로 감쌉니다.

2. 자식 컴포넌트에서 props 받기

function Child(props) {
  return (
    <div>
      <p>이름: {props.name}</p>
      <p>나이: {props.age}</p>
    </div>
  );
}
  • 함수의 첫 번째 인자로 props 객체를 받습니다.
  • props.이름으로 전달받은 값을 사용할 수 있습니다.

3. 구조분해 할당으로 props 받기

function Child({ name, age }) {
  return (
    <div>
      <p>이름: {name}</p>
      <p>나이: {age}</p>
    </div>
  );
}
  • props 객체에서 필요한 값만 바로 꺼내서 사용할 수 있어 코드가 더 간결해집니다.

4. 변수/객체/배열도 전달 가능

function Parent() {
  const user = { name: "김강현", age: 30 };
  return <Child user={user} />;
}

function Child({ user }) {
  return (
    <div>
      <p>이름: {user.name}</p>
      <p>나이: {user.age}</p>
    </div>
  );
}

5. props를 활용한 컴포넌트 재사용

function Button({ label, color }) {
  return <button style={{ color: color }}>{label}</button>;
}

function App() {
  return (
    <div>
      <Button label="저장" color="blue" />
      <Button label="취소" color="red" />
    </div>
  );
}
  • props를 다르게 주면 같은 컴포넌트도 다양한 역할을 할 수 있습니다.

정리

  • props는 부모 → 자식 방향으로 데이터를 전달하는 통로입니다.
  • props는 읽기 전용입니다(자식에서 직접 변경 불가).
  • props를 활용하면 컴포넌트를 더 재사용성 있게 만들 수 있습니다.
  • props는 컴포넌트의 "함수 인자"와 같은 역할을 합니다.

Tip: props의 타입을 명확히 하고 싶다면 PropTypes 또는 TypeScript를 활용할 수 있습니다.
(예시: Child.propTypes = { name: PropTypes.string.isRequired };)

728x90

리액트 타입 연결 방법과 사용법 (React + TypeScript)

리액트 프로젝트에 타입스크립트를 적용하고, 컴포넌트에 타입을 연결하는 방법을 단계별로 정리했습니다.
처음 리액트와 타입스크립트를 접하는 분도 쉽게 따라할 수 있도록 설명합니다.

타입스크립트와 리액트를 함께 사용하면 타입 안정성, 가독성, 유지보수성이 모두 향상됩니다.


1. 타입스크립트 적용 (설치 및 환경설정)

새 프로젝트 생성 시

npx create-react-app my-app --template typescript
yarn create react-app my-app --template typescript
위 명령어로 타입스크립트가 적용된 리액트 프로젝트를 바로 시작할 수 있습니다.

기존 프로젝트에 타입스크립트 추가

  1. 타입스크립트 및 타입 패키지 설치
    npm install --save typescript @types/node @types/react @types/react-dom
    yarn add typescript @types/node @types/react @types/react-dom
  2. tsconfig.json 생성 및 설정
    주요 옵션 예시:
    {
      "compilerOptions": {
        "target": "es5",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react-jsx"
      },
      "include": ["src"]
    }
  3. 파일 확장자 변경
    .js.ts, .jsx.tsx로 변경하세요.

2. 리액트 컴포넌트에 타입 연결 (Props 타입 선언)

컴포넌트에 타입을 연결하는 방법 3가지 입니다.

1) interface 또는 type을 사용한 Props 타입 선언

import React from 'react';

interface CircleProps {
  bgColor: string;
  borderColor?: string;
  text?: string;
}

const Circle: React.FC<CircleProps> = ({ bgColor, borderColor, text }) => (
  <div style={{
    backgroundColor: bgColor,
    border: borderColor ? `2px solid ${borderColor}` : undefined,
    borderRadius: '50%',
    width: 100,
    height: 100,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
    {text}
  </div>
);

export default Circle;

2) React.FC 제네릭 사용

type AppProps = { title: string };

const App: React.FC<AppProps> = ({ title }) => {
  return <h1>{title}</h1>;
};

3) 인라인 타입 선언

const App = ({ title }: { title: string }) => {
  return <h1>{title}</h1>;
};

3.  예시 코드

App.tsx

import React from 'react';
import Circle from './Circle';

function App() {
  return (
    <div>
      <Circle bgColor="red" borderColor="black" />
      <Circle bgColor="blue" text="text" />
    </div>
  );
}

export default App;

Circle.tsx

interface CircleProps {
  bgColor: string;
  borderColor?: string;
  text?: string;
}

const Circle: React.FC<CircleProps> = ({ bgColor, borderColor, text }) => (
  <div style={{
    backgroundColor: bgColor,
    border: borderColor ? `2px solid ${borderColor}` : undefined,
    borderRadius: '50%',
    width: 100,
    height: 100,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
    {text}
  </div>
);

export default Circle;
타입스크립트를 적용하면 props의 타입을 확인하여 잘못된 타입 전달 시 컴파일 단계에서 에러를 확인할 수 있습니다.

정리

  • 타입스크립트 적용: 패키지 설치 → tsconfig 설정 → 파일 확장자 변경
  • 컴포넌트 타입 연결: interface/type 선언 후 React.FC<Props> 또는 인라인 타입 활용
  • 실무에서는 interface를 활용한 props 타입 선언이 가장 많이 사용됨
  • styled-components, CSS 모듈 등과 함께 사용할 때는 추가 타입 선언이 필요할 수 있습니다.
  • 예: CSS 모듈 사용 시 global.d.ts에 declare module "*.module.css"; 추가
  • 타입스크립트와 리액트의 조합은 props, state, ref, 이벤트 등 다양한 부분에 타입을 적용할 수 있습니다.
  • 공식 문서나 cheatsheet를 참고하면 실무에서 자주 쓰는 패턴을 빠르게 익힐 수 있습니다.
728x90

'React' 카테고리의 다른 글

[React] LocalStorage(로컬스토리지) - 저장, 불러오기, 삭제  (1) 2025.05.29
[React] Props(프롭스)란?  (0) 2025.05.28
[React] UseState란?  (0) 2025.05.27
[React] UseMemo란?  (0) 2025.05.22
[React] React Router 설정방법  (2) 2025.05.21

리액트 useState란?

useState는 리액트에서 제공하는 훅(Hook) 중 하나로, 함수형 컴포넌트에서 상태(state)를 관리할 수 있게 해줍니다.
상태란 시간이 지남에 따라 변경될 수 있는 값으로, 예를 들어 버튼 클릭 횟수, 입력 값, 토글 상태 등이 모두 상태에 해당합니다.
useState를 사용하면 컴포넌트가 기억해야 하는 값을 저장하고, 그 값이 바뀔 때마다 컴포넌트가 다시 렌더링됩니다.


useState 사용법

기본 문법

const [상태변수, 상태변경함수] = useState(초기값);
  • 상태변수: 현재 상태 값을 저장합니다.
  • 상태변경함수: 상태를 변경할 때 사용하는 함수입니다.
  • 초기값: 상태의 초기값을 설정합니다.

 

예시: 카운터 컴포넌트

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const addCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={addCount}>Add</button>
    </div>
  );
}
  • count는 현재 카운트 값을 저장하는 상태 변수입니다.
  • setCount는 count 값을 변경할 때 사용하는 함수입니다.
  • 버튼을 클릭하면 addCount 함수가 실행되고, setCount를 통해 count 값이 1 증가합니다.
  • count 값이 바뀌면 컴포넌트가 자동으로 리렌더링되어 새로운 값이 화면에 표시됩니다.

 

상태를 업데이트하는 두 가지 방법

  1. 값 직접 업데이트
    const addCount = () => {
      setCount(1);
    };
    count 값을 1로 직접 설정합니다.
  2. 함수형 업데이트
    const addCount = () => {
      setCount(prevCount => prevCount + 1);
    };
    이전 상태 값을 기반으로 새로운 상태를 계산할 때 사용합니다. 여러 번 연속해서 상태를 변경할 때, 또는 최신 상태 값을 안전하게 사용하고 싶을 때 권장됩니다.

 

여러 상태를 객체로 관리하기

여러 개의 입력 필드나 관련된 데이터를 하나의 객체로 묶어서 관리할 수도 있습니다.

import React, { useState } from 'react';

const EventPractice = () => {
  const [form, setForm] = useState({
    username: '',
    message: ''
  });

  const { username, message } = form;

  const onChange = e => {
    const { name, value } = e.target;
    setForm({
      ...form,
      [name]: value
    });
  };

  return (
    <div>
      <input
        type="text"
        name="username"
        placeholder="유저명"
        value={username}
        onChange={onChange}
      />
      <input
        type="text"
        name="message"
        placeholder="메시지"
        value={message}
        onChange={onChange}
      />
    </div>
  );
};
  • 여러 입력 값을 하나의 상태 객체(form)로 관리할 수 있습니다.
  • onChange 핸들러에서 setForm을 통해 각 필드의 값을 업데이트합니다.

정리

  • useState는 함수형 컴포넌트에서 상태를 관리할 수 있게 해주는 훅입니다.
  • 상태가 변경될 때마다 컴포넌트가 자동으로 리렌더링됩니다.
  • 상태 변경 함수는 비동기적으로 동작하므로, 연속해서 상태를 변경할 때는 함수형 업데이트 방식을 사용하는 것이 안전합니다.
  • 여러 상태를 객체로 묶어 관리하면 코드가 더 깔끔해지고 유지보수가 쉬워집니다.

useState를 잘 활용하면 리액트 컴포넌트의 동적인 UI와 데이터를 효과적으로 관리할 수 있습니다.

728x90

'React' 카테고리의 다른 글

[React] Props(프롭스)란?  (0) 2025.05.28
[React] 리액트 타입 연결하기  (0) 2025.05.28
[React] UseMemo란?  (0) 2025.05.22
[React] React Router 설정방법  (2) 2025.05.21
[React] Vite로 프론트엔드 프로젝트 시작하기  (0) 2025.05.08