[나만무/개발단계] OffscreenCanvas, 막상 적용하려니 어렵다...

CHO WanGi·2025년 7월 16일

KRAFTON JUNGLE 8th

목록 보기
85/89

OffScreenCavnas?

https://developer.mozilla.org/ko/docs/Web/API/OffscreenCanvas
https://tech.kakao.com/posts/442
https://velog.io/@hoonsbory/offscreencanvas

개념을 알려면

https://web.dev/articles/offscreen-canvas?hl=ko

이 링크가 가장 깔끔하게 설명하니 참고하시길!
그래서 이걸 쓰고 싶었는데 일단 못했다...
이걸 적으면서 흐름을 찾고자 한다.

그래서 저게 뭔데?

한줄로 요약하면 작업자를 한명 더 두는 것.

JS는 흔히 싱글 스레드라고 한다.
데이터를 불러오고, UI를 그리고, 사용자와 인터렉션 하는 것들을 하나의 스레드에서 하게 된다.
즉 Main Thread가 busy 하다면 작업의 속도가 느려진다.

이걸 offScreenCavnas + Web Worker 개념을 통해 하나의 스레드를 더 쓰는 것 처럼 작동하게 하여
더 빠른 작업 속도를 통해 Canvas 태그 내 그려지는 그래픽 렌더링을 더 빠르게 하는 것.

  • 한 짤 요약

막상 적용하려니 어려운 이유

OffscreenCanvas가 성능에 좋다는 건 알지만, 막상 내 프로젝트에 적용하려니 막막했다.

GameCanvas처럼 한 파일 안에서 모든 걸 처리하다가
SimplePixelCanvas처럼 역할을 나누려니 어디서부터 손대야 할지 감이 안 왔다.

이건 코딩 기술의 문제라기보다,
애플리케이션을 설계하는 관점을 바꾸자고 생각했다.


어려움의 근원은 메인 스레드와 워커 스레드가 서로 직접 대화할 수 없는,
철저히 격리된 공간
이라는 사실에서 시작된다.

GameCanvas에서는 그냥 옆에 있는 함수를 호출하면 끝이었지만,
이제는 다른 도시에 있는 동료에게 소포(postMessage)를 보내서 일을 시켜야 하는 상황인 것이다.

1. '상태'가 분리됨을 받아들이는 것

하지만 OffscreenCanvas를 쓰면 상태가 두 종류로 나뉜다.

  • UI 상태 (메인 스레드): GameCanvasshowQuestionModal, isLoading처럼 UI의 모양이나 동작을 결정하는 상태. 이건 여전히 React의 useState로 관리한다.
  • 렌더링 상태 (워커 스레드): 캔버스에 그려질 모든 픽셀의 정보(pixels Map 객체), 현재 뷰포트 위치(viewPos), 확대 배율(scale) 등. 이건 워커 파일 안의 일반 변수로 존재한다.

핵심: 워커의 렌더링 상태를 바꾸고 싶으면, React에서 직접 수정하는 게 아니라 "상태를 바꿔줘"라는 메시지를 워커에게 보내야 한다.

2. DOM 정보는 메인 스레드만의 것

워커는 DOM을 전혀 볼 수 없다. windowdocument 객체가 존재하지 않는 것이다.

  • 문제: "워커에서 캔버스 크기를 알아내서 중앙 정렬해야지" -> 불가능
  • 해결책: SimplePixelCanvas에서 본 것처럼,
    메인 스레드에서 ResizeObserver 등으로 크기를 측정한 뒤,
    그 크기 값(숫자)을 메시지로 워커에게 보내줘야 한다.

3. 모든 소통은 비동기 소포(postMessage)로

메인 스레드와 워커는 오직 postMessage로만 대화한다. 함수 호출처럼 즉시 결과를 반환받을 수 없다.

  • 기존 방식: handleClick() -> draw() 호출 (동기적)
  • 새로운 방식: handleClick() -> postMessage({ type: 'DRAW_PIXEL', ... }) -> (시간차) -> 워커가 메시지 받고 draw() 호출 (비동기적)

이 비동기 흐름에 익숙해져야 한다.


GameCanvasOffscreenCanvas 방식으로 바꾼다고 상상하며 역할을 재정의해 보자.

역할 재정의: 매니저와 실무자

  • 매니저 (메인 스레드 - GameCanvas.tsx)

    • 업무: 손님(사용자) 응대, DOM 관리
    • 주요 책임:
      • 사용자 클릭, 마우스 이동 같은 이벤트를 받는다.
      • 화면 크기가 바뀌는지 감시한다(ResizeObserver).
      • 실무자(워커)에게 "여기 클릭됐으니 픽셀 그려줘", "화면 크기 바뀌었으니 다시 그려줘" 처럼 명령(메시지)만 내린다.
      • 퀴즈 모달을 띄우고 닫는 등 순수 UI 로직을 처리한다.
  • 실무자 (워커 스레드 - game.worker.ts)

    • 업무: 실제 그리기, 데이터 관리
    • 주요 책임:
      • 모든 픽셀 데이터(pixels Map)를 직접 소유하고 관리한다.
      • 매니저에게 받은 명령을 해석해서 캔버스에 그림을 그린다.
      • 그림 그리는 데 필요한 모든 상태(viewPos, scale)를 직접 관리한다.
      • 그림을 다 그리면 "다 그렸고, 현재 뷰포트는 이래" 라고 매니저에게 보고(메시지)할 수도 있다. (SimplePixelCanvasVIEW_UPDATED 메시지처럼)

데이터 흐름 예시: 픽셀 하나 찍기

  1. (Main) 사용자가 interactionCanvas를 클릭. handleClick 함수 실행.
  2. (Main) e.clientX, e.clientY 값을 바탕으로 물리 좌표를 계산.
  3. (Main) 실무자에게 소포를 보냄: worker.postMessage({...})
  4. --- (시간차) ---
  5. (Worker) 메시지를 받음. onmessage 실행.
  6. (Worker) 받은 좌표와 자신이 가진 viewPos, scale을 이용해 어떤 픽셀인지 계산.
  7. (Worker) 자신이 가진 pixels 데이터를 업데이트.
  8. (Worker) draw() 함수를 호출해 OffscreenCanvas에 그림을 다시 그림.
  9. (Browser) OffscreenCanvas의 최신 그림을 화면의 <canvas>에 자동으로 보여줌.

글로 적어도 왜 감이 안올까...
왜 안되는지 이유를 반드시 찾아보자.

profile
제 Velog에 오신 모든 분들이 작더라도 인사이트를 얻어가셨으면 좋겠습니다 :)

0개의 댓글