[나만무/포스터 세션] 질문 아카이빙

CHO WanGi·2025년 7월 27일

KRAFTON JUNGLE 8th

목록 보기
88/89
post-thumbnail

1. 다중 레이어로 구성한 이유는 무엇인가?

일단 처음에 캔버스 하나로만 하려고 했다.
그 당시에는 구현된 기능도 색칠 기능 딱 하나이기도 했고
캔버스 태그에 관련된 로직들도 별로 많지도 않아서,
렌더링 지연도 없었다.

그런데 미리보기 기능, 픽셀 경계 효과 등을 추가하면서
캔버스가 무거워지기 시작했다.
동료 개발자와 캔버스 하나를 더 쌓니 마니 하면서
논쟁을 하다가, 코치님 피드백을 받았다.

"그 생각보다 여러겹 쌓아도 괜찮지 않을까요?"
라는 피드백이었다.

  <canvas ref={canvasRef} id='starfield-canvas' />; // 배경
  <canvas ref={renderCanvasRef} /> // 실제 픽셀 색칠
//..
  const canvas = document.createElement('canvas'); // 이미지 오버레이
  <canvas ref={previewCanvasRef} /> // 픽셀 경계효과, 미리보기
  <canvas ref={interactionCanvasRef} /> // 사용자 이벤트 핸들링

그래서 이렇게 각각의 캔버스 태그의 역할을 정의하고, 관련 로직들과 책임들을 분리하였다.

그리고 다섯개의 캔버스 중 interaction 과 Preview만 그려도 되지만,
draw 함수가 모든 캔버스를 다 그리는 로직이었다.

이를 분리해서 불필요한 함수 호출을 줄이고자 하였다.

  • 실제 픽셀 데이터를 서버와 주고 받는 RenderCanvas 만 그릴때 호출하는 함수
  const drawBaseLayer = useCallback(() => {
    const renderCtx = renderCanvasRef.current?.getContext('2d');
    const sourceCanvas = sourceCanvasRef.current;
    if (!renderCtx || !sourceCanvas) return;

    const canvas = renderCtx.canvas;
    renderCtx.save();
    renderCtx.clearRect(0, 0, canvas.width, canvas.height);
    renderCtx.translate(viewPosRef.current.x, viewPosRef.current.y);
    renderCtx.scale(scaleRef.current, scaleRef.current);

    DrawingUtils.drawCanvasBackground(renderCtx, canvasSize, canvasType);
    renderCtx.imageSmoothingEnabled = false;
    renderCtx.drawImage(sourceCanvas, 0, 0);

    if (
      !isImageFixed &&
      imageCanvasRef.current &&
      !(imageCanvasRef.current as any)._isGroupImage
    ) {
      DrawingUtils.drawGrid(renderCtx, canvasSize);
    }

    renderCtx.restore();
  }, [canvasSize, canvasType, isImageFixed]);
  • 사용자가 목표 도안을 업로드할때 호출되는 함수(ImageOverlay)
  const drawImageLayer = useCallback(() => {
    const renderCtx = renderCanvasRef.current?.getContext('2d');
    const imageCanvas = imageCanvasRef.current;
    if (!renderCtx || !imageCanvas) return;

    renderCtx.save();
    renderCtx.translate(viewPosRef.current.x, viewPosRef.current.y);
    renderCtx.scale(scaleRef.current, scaleRef.current);

    DrawingUtils.drawAttachedImage(
      renderCtx,
      imageCanvas,
      imagePosition,
      imageSize,
      isImageFixed,
      imageTransparencyRef.current
    );

    renderCtx.restore();
  }, [imagePosition, imageSize, isImageFixed]);
  • 사용자 커서위치에 따른 픽셀 경계 표시효과, 색상 선택시 미리보기 효과를 위한 함수(Preview)
  const drawPreviewLayer = useCallback(() => {
    const pctx = previewCanvasRef.current?.getContext('2d');
    if (!pctx) return;

    const preview = pctx.canvas;
    pctx.save();
    pctx.clearRect(0, 0, preview.width, preview.height);
    pctx.translate(viewPosRef.current.x, viewPosRef.current.y);
    pctx.scale(scaleRef.current, scaleRef.current);

    if (fixedPosRef.current && fixedPosRef.current.color !== 'transparent') {
      const { x, y, color: fx } = fixedPosRef.current;
      pctx.fillStyle = fx;
      pctx.fillRect(x, y, 1, 1);
    }

    if (fixedPosRef.current) {
      const { x, y } = fixedPosRef.current;
      pctx.strokeStyle = 'rgba(255,255,0,0.9)';
      pctx.lineWidth = 3 / scaleRef.current;
      pctx.strokeRect(x, y, 1, 1);
    }
    if (previewPixelRef.current) {
      const { x, y, color: px } = previewPixelRef.current;
      pctx.fillStyle = px;
      pctx.fillRect(x, y, 1, 1);
    }

    if (flashingPixelRef.current) {
      const { x, y } = flashingPixelRef.current;
      const isVisible = Math.floor(Date.now() / 500) % 2 === 0;
      if (isVisible) {
        pctx.strokeStyle = 'rgba(255, 0, 0, 0.9)';
        pctx.lineWidth = 4 / scaleRef.current;
        pctx.strokeRect(x, y, 1, 1);
      }
    }

    pctx.restore();
  }, []);
  • 초기 로딩시, 혹은 전체를 그릴 필요가 있을때 호출하는 drawAll
  const drawAll = useCallback(() => {
    // const startTime = performance.now(); // 측정 시작

    drawBaseLayer();
    drawImageLayer();
    drawPreviewLayer();

    // const endTime = performance.now(); // 측정 종료
    // const renderTime = endTime - startTime;
    // console.log(`Canvas render time: ${renderTime.toFixed(2)} ms`); // 콘솔에 출력
  }, [drawBaseLayer, drawImageLayer, drawPreviewLayer]);

2. Three.js, WebGL을 선택하지 않은 이유는 뭔가?

이 선택지들도 고려하지 않은 것은 아니다.
그렇지만 세가지 이유가 있어서 선택하지 못했다.

  1. 일단 3D 적인 요소가 들어가지 않는다.
    2D 평면에서 픽셀에 색을 칠하거나,
    경계에 효과만 넣는 등의 요구사항을 고려해봤을 때 오히려 오버스펙이라고 생각했다.
    2D Context로 충분히 요구사항을 구현할 수 있을 것이라고 판단했다.

  2. WebGL 의 러닝커브가 높다고 생각했다.
    WebGL 기초 용어들을 보면, 메쉬, 셰이더, 등등 새롭게 배워야할 개념들이 많다.
    일단 나 조차도 타입스크립트를 처음 써보는 상황에서 TS에 대한 학습을 하고 있었고,
    동료 개발자도 리액트를 처음 써보기 때문에
    최대한 학습을 빠르게 끝내고 실 개발을 하는게 맞다고 판단하였다.
    그리고 우리 프로젝트는 MVP를 빠르게 개발해서 실제 배포 후 사용자 피드백을
    반드시 받아야 했다.

  3. 실개발 기간이 3~4주 뿐이었다.
    주어진 기간은 5주긴 한데,
    기획 1주, 마지막 테스트 및 리허설 준비 1주 빼면 사실상 실제 코드를 치고
    개발할 기간이 3~4주 뿐이었다.

3. React를 왜 쓰는가?

9기인지 10기인지 쭈뼛쭈뼛 다가와서 물어본 질문이라 기억이 남는다.
여튼 2가지 이유가 있다.

  1. 내가 쓰고 싶었다.
    React로 프로젝트를 제대로 한지 2년이 넘어서,
    사실상 새로 배워야하는 상황이었다.
    정글에서 몰입해서 3~4주간 사용하면 좋을 것 같았다.

  2. 레퍼런스가 일단 많고 유용한 라이브러리가 많다.
    리액트는 실제 현업에서도 많이 쓰고 있고,
    어려운 기능들을 많은 사람들이 라이브러리로 구현해 놓은 것들이 많다.
    기간은 짧은 상황에서 라이브러리를 활용하여 개발 기간을 단축하고자 하였다.

4. Worker는 Dedicate 인가 Shared 인가?

사실 웹 워커에 종류가 있다는걸 알기는 했는데
정확히 구분하지 못했다.

https://oliveyoung.tech/2025-04-25/web-worker-for-image-processing/

그래서 이 질문에는 답변을 잘 못했다.

  • Dedicated Worker
    단일 페이지에서 동작하는 독립적인 Worker, 1:1 통신으로 데이터 처리에 최적화

  • Shared Worker
    여러 탭/창에서 공유되는 Worker, 상태 공유나 실시간 데이터 처리에 활용

  • Service Worker
    네트워크 프록시 역할을 하는 Worker, 오프라인 지원, 캐싱, 푸시 알림등에 활용

지금 답변을 한다면 Dedicated Worker 를 활용했다.
그런데 어떤 사용자가 탭을 100개 열게된다면 지금은 그만큼의 소켓 연결을 시도한다.

만약, Shared Worker를 통해 특정 사용자가 탭을 100개씩 열어도 소켓 연결 하나로
처리해서 서버 부하를 줄여줄 수도 있겠다는 생각이 들었다.

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

2개의 댓글

comment-user-thumbnail
2025년 7월 27일

너무 고생많았어!
301호 픽픽셀이 발표장 분위기를 너무 좋게 만들고 끝내줘서 고마워
완성도 자체도 높아서 뭔가 내가 다 자랑스러웠음(301호로써)

1개의 답글