Canvas API와 WebSocket을 활용한 실시간 드로잉

김슭삵·2025년 2월 12일
post-thumbnail

들어가며..

이번 글에서는 Canvas APIWebSocket을 활용하여 실시간으로 여러 사용자가 함께 그림을 그릴 수 있는 애플리케이션을 구현하는 방법에 대해 알아보겠습니다.

목차 📑

  1. 기본 구조 설정
  2. Canvas 초기화 및 설정
  3. WebSocket 연결 설정
  4. 그리기 기능 구현
  5. 실시간 데이터 전송
  6. 주요 이슈와 해결 방안

1. 기본 구조 설정 🏗️

먼저 React 컴포넌트의 기본 구조를 설정합니다. Canvas 컴포넌트와 필요한 상태들을 정의합니다.

const Canvas = () => {
  const canvasRef = useRef(null);
  const contextRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [lastPoint, setLastPoint] = useState(null);
  
  // 그리기 도구 상태
  const [drawingColor, setDrawingColor] = useState("#000000");
  const [drawingWidth, setDrawingWidth] = useState(2);
  const [isEraserMode, setIsEraserMode] = useState(false);
}

각 상태의 역할:

  • canvasRef: Canvas DOM 요소 참조
  • contextRef: Canvas 2D 컨텍스트 참조
  • isDrawing: 현재 그리기 중인지 여부
  • lastPoint: 마지막 그리기 위치 저장
  • drawingColor: 현재 선택된 색상
  • drawingWidth: 선 굵기
  • isEraserMode: 지우개 모드 여부

2. Canvas 초기화 및 설정 🎯

Canvas를 초기화하고 기본 설정을 적용합니다.

useEffect(() => {
  const canvas = canvasRef.current;
  if (!canvas) return;

  // 캔버스 크기 설정
  const parent = canvas.parentElement;
  canvas.width = parent.clientWidth;
  canvas.height = parent.clientHeight;

  // 컨텍스트 설정
  const context = canvas.getContext("2d");
  context.lineCap = "round";
  contextRef.current = context;

  // 초기 스타일 설정
  context.strokeStyle = getCurrentColor();
  context.lineWidth = getCurrentWidth();
}, []);
  • 부모 요소 크기에 맞게 캔버스 크기를 설정
  • 선 끝부분을 둥글게 처리
  • 초기 색상과 선 굵기 설정

3. WebSocket 연결 설정 🔌

STOMP 프로토콜을 사용하여 WebSocket 연결을 설정합니다.

const { client, connected } = useCatchSocket(roomId);

useEffect(() => {
  if (!client || !connected || !roomId) return;

  try {
    if (subscriptionRef.current) {
      subscriptionRef.current.unsubscribe();
    }

    subscriptionRef.current = client.subscribe(
      `/topic/catch-mind/${roomId}`,
      handleDrawingData
    );
  } catch (error) {
    console.error("구독 설정 실패:", error);
  }

  return () => {
    if (subscriptionRef.current) {
      subscriptionRef.current.unsubscribe();
    }
  };
}, [client, connected, roomId, handleDrawingData]);
  • WebSocket 연결 상태 관리
  • 특정 방에 대한 구독 설정
  • 컴포넌트 언마운트 시 구독 해제

4. 그리기 기능 구현 ✏️

마우스 이벤트를 처리하여 그리기 기능을 구현합니다.

const getMousePosition = useCallback((canvas, event) => {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  
  return {
    x: (event.clientX - rect.left) * scaleX,
    y: (event.clientY - rect.top) * scaleY
  };
}, []);

const startDrawing = ({ nativeEvent }) => {
  if (!canDraw() || !connected) return;

  const canvas = canvasRef.current;
  const { x, y } = getMousePosition(canvas, nativeEvent);
  
  contextRef.current.beginPath();
  contextRef.current.moveTo(x, y);
  setIsDrawing(true);
  setLastPoint({ x, y });

  sendDrawingData({
    type: "start",
    x,
    y,
  });
};
  • 정확한 마우스 좌표 계산
  • 그리기 시작점 설정
  • WebSocket을 통한 데이터 전송

5. 실시간 데이터 전송 📡

그리기 데이터WebSocket을 통해 전송합니다.

const sendDrawingData = useCallback(
  (drawingData) => {
    if (!client || !roomId || !connected) return;

    try {
      const message = {
        type: drawingData.type,
        roomId: parseInt(roomId),
        userId: parseInt(userId),
        x: Math.round(drawingData.x),
        y: Math.round(drawingData.y),
        color: getCurrentColor(),
        lineWidth: getCurrentWidth().toString(),
      };

      client.publish({
        destination: `/app/drawing/${roomId}`,
        body: JSON.stringify(message),
        headers: { "content-type": "application/json" },
      });
    } catch (error) {
      console.error("드로잉 데이터 전송 실패:", error);
    }
  },
  [client, roomId, connected, userId, getCurrentColor, getCurrentWidth]
);
  • 그리기 데이터를 JSON 형식으로 변환
  • WebSocket을 통해 서버로 전송
  • 에러 처리

6. 주요 이슈와 해결 방안 🔧

이슈 1: 턴 변경 시 그리기 도구 상태 초기화 문제

문제: 턴이 바뀔 때 그리기 도구의 상태(색상, 굵기, 지우개 모드)가 초기화되지 않음

해결방안:

useEffect(() => {
  if (
    prevCurrentPlayerRef.current &&
    currentPlayer &&
    prevCurrentPlayerRef.current.nickname !== currentPlayer.nickname
  ) {
    // 캔버스 초기화
    clearCanvas();

    // 그리기 도구 상태 초기화
    setDrawingColor("#000000");
    setDrawingWidth(2);
    setIsEraserMode(false);
    
    // context 스타일 초기화
    if (contextRef.current) {
      contextRef.current.strokeStyle = "#000000";
      contextRef.current.lineWidth = 2;
    }
  }
  prevCurrentPlayerRef.current = currentPlayer;
}, [currentPlayer, clearCanvas]);

이슈 2: 레이아웃 변경 시 그리기 좌표 오차 문제

문제: F12나 레이아웃 크기 변경 시 마우스 포인터와 실제 그리기 위치가 어긋나는 현상

해결방안:
1. 리사이즈 이벤트 감지 및 처리:

const resizeCanvas = useCallback(() => {
  const canvas = canvasRef.current;
  if (!canvas) return;

  const parent = canvas.parentElement;
  const rect = parent.getBoundingClientRect();
  
  canvas.width = rect.width;
  canvas.height = rect.height;

  // 컨텍스트 스타일 재설정
  const context = canvas.getContext('2d');
  context.lineCap = "round";
  context.strokeStyle = getCurrentColor();
  context.lineWidth = getCurrentWidth();
}, [getCurrentColor, getCurrentWidth]);
  1. 정확한 마우스 좌표 계산:
const getMousePosition = useCallback((canvas, event) => {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  
  return {
    x: (event.clientX - rect.left) * scaleX,
    y: (event.clientY - rect.top) * scaleY
  };
}, []);

TIL (Today I Learn) 🎯

1. Canvas API 기본 개념

  • Canvas는 JavaScript를 사용하여 그래픽을 그릴 수 있는 HTML5 요소
  • getContext('2d')로 2D 렌더링 컨텍스트를 얻어 그리기 작업 수행
  • lineCap, strokeStyle, lineWidth 등의 속성으로 그리기 스타일 설정 가능

2. WebSocket을 활용한 실시간 통신

  • STOMP 프로토콜을 사용하여 서버와 클라이언트 간 실시간 통신 구현
  • 구독(subscribe)발행(publish) 패턴으로 그리기 데이터 전송
  • 연결 상태 관리재연결 로직의 중요성

3. React에서 Canvas 다루기

  • useRefCanvas DOM 요소와 context 참조 관리
  • useEffect로 Canvas 초기화 및 이벤트 리스너 설정
  • useState로 그리기 도구 상태 관리
profile
비전공자의 개발 적응기

0개의 댓글