개요
Electron + React + Matter.js로 만든 AI 바탕화면 펫 프로젝트의 주요 개선 사항을 정리했습니다. 좌우 반전 문제 해결부터 실행 스크립트 최적화까지, 실제 개발 과정에서 마주한 문제들과 해결 방법을 공유합니다.
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
주요 개선 사항
1. 좌우 반전 이미지 적용 문제 해결
문제점:
- 고양이가 왼쪽으로 이동할 때 이미지가 좌우 반전되지 않는 문제
- Matter.js의 기본 sprite 렌더러가 xScale 음수 값을 제대로 지원하지 않음
해결 방법:
커스텀 렌더링 함수를 구현하여 Canvas context의 scale() 메서드로 직접 이미지 반전 처리
// 커스텀 렌더링: 좌우 반전을 확실하게 처리
const originalRenderBodies = Render.bodies;
Render.bodies = function (render, bodies, context) {
const state = interactionState.current;
const cat = catRef.current;
// 고양이를 제외한 나머지 바디 렌더링
const otherBodies = bodies.filter((body) => body !== cat);
if (otherBodies.length > 0) {
originalRenderBodies(render, otherBodies, context);
}
// 고양이 스프라이트 커스텀 렌더링 (좌우 반전 적용)
if (cat && cat.render && cat.render.sprite && cat.render.sprite.texture) {
const sprite = cat.render.sprite;
const texture = sprite.texture;
const img = loadImage(texture);
if (img && img.complete && img.naturalWidth > 0) {
context.save();
// 고양이 위치로 이동 및 회전
context.translate(cat.position.x, cat.position.y);
context.rotate(cat.angle);
// 좌우 반전 적용
const baseScale = 0.5;
const scaleX = state.facingLeft ? -1 : 1;
const scaleY = 1;
// 스프라이트 크기 계산
const imgWidth = img.naturalWidth * baseScale;
const imgHeight = img.naturalHeight * baseScale;
// 이미지 그리기 (반전 적용)
context.scale(scaleX, scaleY);
context.drawImage(
img,
-imgWidth / 2,
-imgHeight / 2,
imgWidth,
imgHeight
);
context.restore();
}
}
};
핵심 포인트:
facingLeft상태를 기반으로context.scale(-1, 1)또는context.scale(1, 1)적용- 이미지 캐싱을 통해 성능 최적화
- 방향 감지를 AI 명령과 속도 기반으로 이중 체크
2. 방향 전환 빈도 개선 및 화면 끝 감지 시스템
문제점:
- 한 방향으로 너무 오래 이동하여 화면 끝에 계속 부딪히는 현상
- 이동 범위가 너무 커서 방향 전환이 드묾
해결 방법:
서버 측 개선 (Python)
@app.get("/ai-command")
def get_ai_command(cat_x: float, cat_y: float, screen_width: float = 1920, screen_height: float = 1080):
# 화면 끝 감지 (여유 공간 50픽셀)
MARGIN = 50
is_at_left_edge = cat_x <= MARGIN
is_at_right_edge = cat_x >= screen_width - MARGIN
# 화면 끝에 닿으면 즉시 방향 전환
if is_at_left_edge or is_at_right_edge:
cat_state["wander_target"] = -1 # 목표 초기화
if is_at_left_edge:
cat_state["wander_target"] = cat_x + random.randint(80, 150)
elif is_at_right_edge:
cat_state["wander_target"] = cat_x - random.randint(80, 150)
if cat_state["mode"] == "WANDER":
# 목표 도달 판정을 30픽셀으로 줄임 (기존 50픽셀)
if cat_state["wander_target"] == -1 or abs(cat_state["wander_target"] - cat_x) < 30:
# 이동 범위를 50~150으로 대폭 줄임 (기존 100~300)
move_range = random.randint(50, 150)
# ... 나머지 로직
개선 사항:
- 이동 범위: 100
300픽셀 → 50150픽셀 - 목표 도달 판정: 50픽셀 → 30픽셀
- 화면 끝 감지 및 자동 방향 전환 시스템 추가
클라이언트 측 개선 (React)
// 화면 크기를 서버에 전달
axios.get("http://127.0.0.1:8000/ai-command", {
params: {
cat_x: cat.position.x,
cat_y: cat.position.y,
screen_width: window.innerWidth,
screen_height: window.innerHeight,
},
});
3. UI 정리 및 마우스 이벤트 버그 수정
문제점:
- 왼쪽 위에 표시되는 상태 텍스트가 불필요
- 투명 윈도우에서 마우스 이벤트가 가끔 작동하지 않는 버그
해결 방법:
상태 텍스트 제거
// 제거된 코드
// const [serverStatus, setServerStatus] = useState("Conn...");
// <div className="status-text">{serverStatus}</div>
마우스 이벤트 개선
// 초기 상태 설정: 처음에는 마우스 이벤트를 받도록 설정
ipcRenderer.send("set-ignore-mouse-events", false, {
forward: true,
});
// 마우스 이동 이벤트 처리 (Throttling 적용)
let lastUpdateTime = 0;
const UPDATE_THROTTLE = 50; // 50ms마다 최대 1번 업데이트
Events.on(mouseConstraint, "mousemove", (e) => {
const now = Date.now();
if (now - lastUpdateTime < UPDATE_THROTTLE) {
return;
}
lastUpdateTime = now;
// 정확한 충돌 감지 (여유 공간 추가)
const bounds = cat.bounds;
const margin = 10;
const isMouseOver =
e.mouse.position.x >= bounds.min.x - margin &&
e.mouse.position.x <= bounds.max.x + margin &&
e.mouse.position.y >= bounds.min.y - margin &&
e.mouse.position.y <= bounds.max.y + margin;
if (isMouseOver !== wasMouseOver) {
ipcRenderer.send("set-ignore-mouse-events", !isMouseOver, {
forward: true,
});
wasMouseOver = isMouseOver;
}
});
// 마우스가 캔버스 밖으로 나갔을 때 처리
render.canvas.addEventListener("mouseleave", () => {
if (wasMouseOver) {
ipcRenderer.send("set-ignore-mouse-events", true, {
forward: true,
});
wasMouseOver = false;
}
});
개선 포인트:
- 초기 상태 명시적 설정
- Throttling으로 성능 최적화
- 여유 공간(margin) 추가로 감지 정확도 향상
- mouseleave 이벤트 처리 추가
4. 화면 밖으로 나가는 문제 해결
문제점:
- 마우스로 클릭했을 때 고양이가 화면 밖으로 날아가서 사라지는 현상
- 경계 벽이 제대로 작동하지 않음
해결 방법:
클릭 시 힘 제한
Events.on(mouseConstraint, "mousedown", (event) => {
if (Matter.Bounds.contains(cat.bounds, event.mouse.position)) {
if (!interactionState.current.isHit) {
interactionState.current.isHit = true;
// 힘을 줄이고 화면 경계 내에서만 작동하도록 제한
const safeY = Math.max(50, Math.min(cat.position.y - 3, height - 150));
Body.setPosition(cat, { x: cat.position.x, y: safeY });
Body.setVelocity(cat, { x: cat.velocity.x * 0.5, y: -3 });
}
}
});
화면 경계 자동 복귀 시스템
Events.on(engine, "beforeUpdate", (event) => {
// 화면 경계 체크 및 자동 복귀
const MARGIN = 50;
const catX = cat.position.x;
const catY = cat.position.y;
const catWidth = cat.bounds.max.x - cat.bounds.min.x;
const catHeight = cat.bounds.max.y - cat.bounds.min.y;
let needsReposition = false;
let newX = catX;
let newY = catY;
// 좌우 경계 체크
if (catX - catWidth / 2 < MARGIN) {
newX = MARGIN + catWidth / 2;
needsReposition = true;
} else if (catX + catWidth / 2 > width - MARGIN) {
newX = width - MARGIN - catWidth / 2;
needsReposition = true;
}
// 상하 경계 체크
if (catY - catHeight / 2 < MARGIN) {
newY = MARGIN + catHeight / 2;
needsReposition = true;
} else if (catY + catHeight / 2 > height - MARGIN) {
newY = height - MARGIN - catHeight / 2;
needsReposition = true;
}
// 화면 밖으로 나갔으면 강제로 복귀
if (needsReposition) {
Body.setPosition(cat, { x: newX, y: newY });
// 속도도 제한
const maxSpeed = 5;
Body.setVelocity(cat, {
x: Math.max(-maxSpeed, Math.min(maxSpeed, v.x)),
y: Math.max(-maxSpeed, Math.min(maxSpeed, v.y)),
});
}
});
경계 벽 강화
// 경계 벽을 더 두껍고 확실하게 설정
const wallOpts = { isStatic: true, render: { visible: false } };
const wallThickness = 100; // 벽 두께 증가 (기존 50)
const ground = Bodies.rectangle(
width / 2,
height + wallThickness / 2,
width,
wallThickness,
wallOpts
);
const left = Bodies.rectangle(
-wallThickness / 2,
height / 2,
wallThickness,
height * 2,
wallOpts
);
const right = Bodies.rectangle(
width + wallThickness / 2,
height / 2,
wallThickness,
height * 2,
wallOpts
);
const top = Bodies.rectangle(
width / 2,
-wallThickness / 2,
width,
wallThickness,
wallOpts
);
5. 실행 스크립트 최적화
문제점:
- 여러 개의 검은 CMD 창이 동시에 뜨어서 화면이 지저분함
- 실행 후 수동으로 창을 닫아야 함
해결 방법:
@echo off
chcp 65001 > nul
echo ====================================
echo AI Desktop Pet - 개발 모드 실행
echo ====================================
echo.
set "ROOT_DIR=%~dp0"
REM [1/3] Python 서버 실행 (최소화)
echo [1/3] Python 서버 시작 중...
start /min "Python Server" cmd /k "cd /d "%ROOT_DIR%server" && python main.py"
timeout /t 2 /nobreak > nul
REM [2/3] Vite 개발 서버 실행 (최소화)
echo [2/3] Vite 개발 서버 시작 중...
start /min "Vite Dev Server" cmd /k "cd /d "%ROOT_DIR%client" && npm run dev"
timeout /t 3 /nobreak > nul
REM Vite 서버 준비 대기
echo Vite 서버 준비 대기 중...
set "WAIT_COUNT=0"
:WAIT_LOOP
timeout /t 1 /nobreak > nul
set /a WAIT_COUNT+=1
curl -s http://localhost:5173 > nul 2>&1
if %ERRORLEVEL% == 0 (
echo Vite 서버 준비 완료!
goto SERVER_READY
)
if %WAIT_COUNT% geq 15 (
echo 경고: Vite 서버 응답이 없지만 계속 진행합니다...
goto SERVER_READY
)
echo 대기 중... (%WAIT_COUNT%/15초)
goto WAIT_LOOP
:SERVER_READY
REM [3/3] Electron 앱 실행
echo [3/3] Electron 앱 시작 중...
cd /d "%ROOT_DIR%client"
start "Electron App" cmd /k "npm run electron"
echo.
echo ====================================
echo 실행 완료!
echo - Python 서버: http://localhost:8000 (최소화됨)
echo - Vite 개발 서버: http://localhost:5173 (최소화됨)
echo - Electron 창이 곧 열립니다
echo.
echo Python 서버와 Vite 서버는 작업 표시줄에 최소화되어 실행 중입니다.
echo 필요시 작업 표시줄에서 창을 열어 로그를 확인할 수 있습니다.
echo ====================================
timeout /t 2 /nobreak > nul
개선 포인트:
start /min으로 서버 창을 최소화하여 작업 표시줄에만 표시- Vite 서버 준비 상태를 확인하는 로직 추가
- 메인 bat 파일 자동 종료 (
pause→timeout)
배운 점
1. Matter.js 커스텀 렌더링
- 기본 렌더러의 한계를 커스텀 렌더링으로 해결
- Canvas API를 직접 사용하여 더 세밀한 제어 가능
2. 물리 엔진 경계 처리
- 경계 벽만으로는 부족할 수 있음
- 매 프레임 경계 체크 + 자동 복귀 시스템이 더 안정적
3. 투명 윈도우 마우스 이벤트
- Electron의
setIgnoreMouseEvents는 초기 상태 설정이 중요 - Throttling과 정확한 충돌 감지가 필수
4. 실행 스크립트 최적화
- 사용자 경험을 고려한 UI/UX 개선
- 백그라운드 프로세스 관리의 중요성
코드 변경 요약
주요 파일 수정 내역
- client/src/App.jsx
- 커스텀 렌더링 함수 추가
- 화면 경계 체크 및 자동 복귀 로직
- 마우스 이벤트 개선
- 상태 텍스트 제거
- server/main.py
- 화면 끝 감지 로직 추가
- 이동 범위 및 목표 도달 판정 조정
- 화면 크기 파라미터 추가
- start-dev.bat / start-dev-advanced.bat
- 서버 창 최소화
- Vite 서버 준비 상태 확인
- 자동 종료 기능
실행 방법
# 프로젝트 루트에서
start-dev-advanced.bat
또는 수동 실행:
# 터미널 1: Python 서버
cd server
python main.py
# 터미널 2: Vite 개발 서버
cd client
npm run dev
# 터미널 3: Electron 앱
cd client
npm run electron
🔗 기술 스택
- Frontend: React 19, Vite 7
- Desktop: Electron 39
- Physics: Matter.js 0.20
- Backend: Python FastAPI, Uvicorn
- Build: Electron Builder
향후 개선 가능한 부분
- 이미지 프리로딩 최적화
- 물리 엔진 성능 튜닝
- 에러 핸들링 강화
- 설정 파일을 통한 커스터마이징
작성일: 2025년 12월
프로젝트: AI Desktop Pet
목적: 학습 및 포트폴리오
'5. [개인] 프로젝트 및 공모전 > 4-2 바탕화면 AI 펫 프로그램' 카테고리의 다른 글
| 살아있는 AI 고양이 프로젝트 만들어보기(3) - 배포(.exe) (0) | 2025.12.08 |
|---|---|
| 살아있는 AI 고양이 프로젝트 만들어보기(2) (0) | 2025.12.07 |
| 살아있는 AI 고양이 프로젝트 만들어보기(1) (0) | 2025.12.06 |