Skip to content

마우스 제스처로 원하는 웹 동작을 빠르게 실행할 수 있는 Chrome 확장 프로그램

Notifications You must be signed in to change notification settings

Eun0713/swaii-extension

Repository files navigation

Swaii Logo

사이트마다 다른 단축키, 외우기 불편하지 않나요?
Swaii는 웹 브라우저에서 마우스 제스처로 반복 작업을 빠르게 실행할 수 있는 Chrome 확장 프로그램입니다.
사이트마다 단축키를 외울 필요 없이 익숙한 제스처 한 번으로 동작을 실행할 수 있고,
사용자는 자신만의 제스처를 만들어 원하는 기능을 자유롭게 연결할 수 있습니다.

마우스가 곧 당신의 단축키가 됩니다.


🧾 목차

  1. 💭 기획 배경
  2. ⛓ 주요 기능 흐름
  3. ⚙️ 기술 스택 및 도입 이유
  4. 🔬 개발 과정
  5. 💥 트러블 슈팅
  6. 🌿 개인 회고

💭 기획 배경

단축키는 빠르지만, 왜 매번 외우긴 어려울까요?

사이트마다 단축키가 달라 익숙해지기도 전에 다시 찾아봐야 하는 경우가 많습니다.

탭 이동이나 새 페이지 열기처럼 자주 사용하는 기능조차 사이트마다 단축키가 달라 일관되게 사용하기 어려웠고,
결국 직접 메뉴를 클릭하거나 아이콘을 찾아 실행해야 하는 경우가 많았습니다.
이런 사소한 비효율들이 모여, 사용 흐름에 잦은 끊김을 만들어냈습니다.

Swaii는 이러한 흐름을 사용자 스스로 정의할 수 있게 하기 위해 시작됐습니다.
직접 만든 제스처 하나로, 자주 쓰는 동작을 사이트마다 내 방식대로 실행할 수 있다면
브라우저 사용은 훨씬 더 직관적이고 유연해질 수 있을 것이라고 생각했습니다.

Swaii intro page

위처럼 사용자는 원형 제스처를 YouTube에서는 "볼륨 조절", Notion에서는 "페이지 추가" 등에 연결할 수 있습니다.

단순히 기능을 설정하는 도구가 아니라,
반복되는 작업 흐름을 "내 손에 맞게" 설계하는 경험을 제공하는 것이 Swaii의 핵심입니다.


⛓ 주요 기능 흐름

① 커스텀 제스처 생성

📸 미리보기

커스텀 제스처 생성 (별모양 패턴)     커스텀 제스처 생성 (Z자 패턴)

  • 자유 입력: Canvas 위에 마우스로 직접 궤적을 그려, 제한 없이 원하는 모양의 제스처를 정의할 수 있습니다.
  • 실시간 미리보기: 그리는 동안 선이 즉시 시각화되어 입력 결과를 바로 확인할 수 있습니다.
  • 되돌리기/다시 그리기: 버튼을 클릭하여 입력한 제스처를 초기화하고 원하는 모양으로 다시 그릴 수 있습니다.
  • 저장: 저장 버튼 클릭 시 “저장되었습니다” 알림과 함께 제스처가 저장되며, 이후 사이트-제스처-동작 매핑에서 즉시 사용할 수 있습니다.

② 기본 제스처 제공 및 동작 매핑

📸 미리보기

패턴 설정     동작 매팡

  • 기본 제스처 5종을 제공해, 별도의 커스텀 제스처를 만들지 않아도 원하는 동작과 바로 연결하여 사용할 수 있습니다.
구분 원형 패턴 삼각형 패턴 S자 패턴 무한대 패턴 N자 패턴
형태 예시 O S N
  • 사이트-제스처-동작 매핑은 특정 사이트를 선택하고, 해당 사이트에서 사용할 제스처와 실행할 동작을 연결하는 과정입니다.
  • 기본 제스처뿐 아니라 사용자가 직접 정의한 제스처도 매핑할 수 있어, 사이트별로 원하는 제스처와 동작을 자유롭게 조합할 수 있습니다.

③ 매핑된 제스처 실행

📸 미리보기

① 사이트-제스처-동작 매핑

 
Arrow
 

② 동작 실행

  • 매핑한 사이트에서 제스처를 그리면 지정한 동작이 실행되며, 화면에 표시된 궤적은 실행 직후 자연스럽게 사라집니다.
  • 제스처 인식은 사용자의 마우스 움직임을 기준으로 실시간 처리되며, 연결된 동작이 브라우저 상에서 즉시 반영됩니다.

⚙️ 기술 스택 및 도입 이유

클라이언트

기술 도입 이유
JavaScript 확장 기능과 UI, 서버 로직까지 전반을 구성하는 핵심 언어
React 다양한 설정 페이지와 제스처 관련 UI를 컴포넌트 단위로 유연하게 관리
React Router SPA에서 라우팅을 처리하며, 페이지 전환 흐름을 관리
tailwindcss 클래스 기반의 유틸리티 CSS 프레임워크로 빠른 UI 구현
Vite 빠른 번들링 및 개발 환경
Chrome Extension Manifest V3 기반으로 브라우저 상에서 동작하는 확장 프로그램 구성

서버

기술 도입 이유
Node.js 브라우저 밖에서 JavaScript로 빠르게 서버 구축
Express REST API 서버 구성에 최적화되어 있어, 사용자 제스처·매핑 정보를 빠르게 처리
Supabase 로그인한 사용자의 제스처 및 매핑 데이터를 저장하고, 기기 간 동기화를 지원

🔬 개발 과정

1. 제스처는 사용자마다 전부 다른데, 어떻게 비교가 가능할까?

동일한 모양의 제스처도, 사람마다 그리는 위치·크기·속도는 전부 다릅니다.
이런 입력을 그대로 비교하면, 같은 제스처조차 서로 다르다고 판단하게 됩니다.

이를 해결하기 위해, 모든 입력 제스처를 정규화(normalization)하여
위치, 크기, 밀도에 관계없이 일관된 기준으로 비교할 수 있도록 설계했습니다.
이 과정을 통해 사용자의 자유로운 입력을 정확한 매핑 실행으로 연결할 수 있습니다.

1-1. 시작 위치를 없애는 이유

사용자가 $(x, y)$ 좌표상에서 제스처를 시작한 지점은 매번 달라집니다.
하지만 제스처를 인식할 때 중요한 것은 제스처의모양(shape) 이며, 입력된 위치(position)는 비교 대상에서 제외되어야 합니다.

목적

사용자의 입력 궤적을 정확히 판단하기 위해, 절대 좌표를 제거하고 궤적의 모양만 남기는 처리가 필요했습니다.
같은 제스처를 왼쪽 아래에서 그리든, 오른쪽 위에서 그리든, 모양이 같다면 동일한 제스처로 인식되어야 하기 때문입니다.

기술적 접근 방식

입력된 제스처는 하나의 좌표 배열로 표현됩니다.
이를 다음과 같이 정의합니다:

$$ P = [p_0, p_1, \dots, p_n] $$

  • $P$는 사용자가 마우스로 그린 제스처의 모든 궤적 좌표들의 집합입니다.
  • $p_i = (x_i, y_i)$는 제스처를 그리는 동안 마우스 커서가 지나간 한 점의 좌표입니다.
  • 그중 첫 번째 좌표 $p_0 = (x_0, y_0)$제스처를 처음으로 그리기 시작한 위치를 나타냅니다.

제스처를 그린 위치에 관계없이 동일한 형태로 비교할 수 있도록, 모든 좌표를 시작점 $p_0$을 기준으로 상대 좌표로 변환합니다.
즉, 제스처의 시작점을 원점 $(0, 0)$으로 옮기고, 나머지 좌표들도 그에 맞춰 이동시키는 방식입니다.


📐 변환 수식

$$ (x'_i, y'_i) = (x_i - x_0, \ y_i - y_0) $$

  • $(x_i, y_i)$: 변환 전의 원래 좌표
  • $(x_0, y_0)$: 시작점 (기준점)
  • $(x'_i, y'_i)$: 변환 후 상대 좌표

⮕ 각 좌표에서 시작점의 좌표를 뺀 값이 새로운 상대 좌표가 됩니다.

예시:

항목
시작점 $p_0$ $(120,\ 200)$
원래 좌표 $p_1$ $(135,\ 220)$
변환 후 $p'_1$ $(135 - 120,\ 220 - 200)$$(15,\ 20)$

이 계산은 실제 코드로 다음과 같이 구현됩니다:

const normalizedPoint = {
  x: point.x - startX,
  y: point.y - startY,
};

위 코드에서 startX, startY는 시작점 $p_0$의 x, y 좌표이며, point는 현재 변환하려는 좌표입니다.


1-2. 크기를 기준에 맞게 맞추는 이유

사용자가 제스처를 그릴 때, 속도에 따라 제스처의 전체 크기는 매번 달라질 수 있습니다.

하지만 제스처 인식에서는 크기 차이 역시 모양 판별에 영향을 주지 않아야 합니다.
즉, 작게 그린 원과 크게 그린 원이 동일한 패턴으로 인식되어야 합니다.

목적

입력된 궤적의 크기 차이를 제거하고, 제스처의 모양 자체만으로 판단할 수 있도록 하기 위해
전체 제스처를 정해진 크기에 맞게 축소 또는 확대하는 정규화 작업이 필요했습니다.

기술적 접근 방식

앞서 정리한 상대 좌표 배열:

$$ P' = [p'_0,\ p'_1,\ \dots,\ p'_n] $$

이 좌표들에 대해, 가장 넓은 축(가로 또는 세로)의 길이를 기준 크기로 맞추는 방식을 활용했습니다.

예를 들어 최대 너비와 높이를 계산한 뒤, 모든 좌표를 아래 비율로 나눕니다:

$$ (x''_i,\ y''_i) = \left( \frac{x'_i}{s},\ \frac{y'_i}{s} \right) $$

  • $s = \max(\text{width},\ \text{height})$
  • 즉, 제스처의 가장 긴 변의 길이를 1로 맞춥니다.

예시:

항목
상대 좌표 $p'_1$ $(15,\ 20)$
전체 width / height $30,\ 40$
스케일 비율 $s$ $\max(30, 40) = 40$
정규화 후 $p''_1$ $(15 / 40,\ 20 / 40)$$(0.375,\ 0.5)$

이 계산은 실제 코드로 다음과 같이 구현됩니다:

const scale = Math.max(maxX - minX, maxY - minY);

const scaledPoint = {
  x: point.x / scale,
  y: point.y / scale,
};
  • scale은 전체 제스처의 가장 긴 축 길이입니다.
  • point는 상대 좌표 기준의 점입니다.
    이를 통해 모든 점이 0~1 범위 안에 정규화되며, 모양 자체만으로 일관된 비교가 가능해집니다.

1-3. 좌표 수를 통일해야 하는 이유

사용자가 제스처를 그리는 속도는 일정하지 않기 때문에,
입력된 좌표의 개수는 사용자마다, 혹은 같은 제스처라도 매번 달라질 수 있습니다.

예를 들어 같은 삼각형을 그리더라도
천천히 그리면 수백 개의 좌표가 생성되고, 빠르게 그리면 수십 개만 기록될 수 있습니다.

목적

제스처를 비교하거나 유사도를 계산할 때,
좌표 수가 다르면 일대일 비교가 불가능해지고, 결과적으로 모양이 같아도 다른 제스처로 인식될 수 있습니다.

따라서 모든 제스처의 좌표 개수를 고정된 수로 맞춰주는 정규화 작업이 필요했습니다.

기술적 접근 방식

상대 좌표 배열 $P' = [p'_0,\ p'_1,\ \dots,\ p'_n]$은 제스처를 입력한 방식에 따라 좌표 개수가 매번 달라질 수 있습니다.

이를 고정된 길이 $k$로 맞추기 위해, 인덱스를 균등 간격으로 나눠 샘플링 하는 방식을 적용했습니다.

⮕ 전체 좌표 배열을 등분하고, 각 구간의 인덱스에 해당하는 좌표를 하나씩 선택하여 목표 개수만큼 추출합니다.

예시:

항목
정규화 전 좌표 수 85개
목표 좌표 수 ($k$) 32개
샘플링 전체 좌표 중 0번째, 2번째, 5번째, ... 순으로 총 32개 선택됨
결과 좌표 수가 32개로 줄어들고, 전체 궤적에서 고르게 퍼진 위치들이 선택됨

이 계산은 실제 코드로 다음과 같이 구현됩니다:

const step = points.length / targetCount;

for (let i = 0; i < targetCount; i++) {
  const index = Math.floor(i * step);
  result.push(points[Math.min(index, points.length - 1)]);
}
  • points: 원본 좌표 배열
  • targetCount: 원하는 정규화 좌표 개수 (기본값은 32)
  • step: 원본 좌표 배열에서 몇 개마다 하나씩 추출할지 정하는 간격

ex: 전체 좌표 수가 64이고 목표 개수가 32이면, step = 2가 되어 2칸마다 하나씩 좌표를 추출

  • Math.floor(i * step): 전체 좌표를 일정 간격으로 나눠, 해당 위치의 좌표를 순차적으로 선택

Important

정규화 좌표 수는 왜 32개인가요?

32는 제스처의 모양을 충분히 표현하면서도
계산 비용이 낮고, 입력의 미세한 차이를 허용할 수 있는 유연성을 갖춘 정확도와 자유도의 균형점입니다.

요소 낮은 정밀도 높은 정밀도 정규화 좌표 수 예시
사용자 자유도 높음 낮음 16 이하
인식 정확도 낮음 (유사 제스처 혼동 가능) 높음 (작은 차이도 구분됨) 128 이상
처리 비용 낮음 높음

이를 통해 제스처마다 동일한 개수의 좌표를 사용함으로써 좌표 간 일대일 비교가 가능해집니다.


2. 사용자가 그린 제스처가 어떻게 "기능 실행"으로 이어질까?

제스처 입력은 단순한 궤적이지만, 이를 기능 실행으로 연결하려면 입력된 궤적을 해석하고, 등록된 제스처와 비교한 뒤
가장 유사한 항목에 매핑된 동작을 정확히 찾아내야 합니다.

2-1. 입력 제스처 → 정규화 좌표로 변환

먼저 사용자의 마우스 움직임을 통해 좌표 배열이 수집됩니다.

이 좌표 배열은 내부적으로 다음 세 가지 정규화 과정을 거칩니다:

  • 개수 보정: 좌표 수를 32개로 일정하게 맞춤
  • 크기 보정: 좌표들을 100x100 정사각형 내로 스케일링
  • 위치 보정: 시작점을 기준으로 좌표를 이동

이 과정을 통해 사용자마다 다르게 그린 제스처도 비교 가능한 상태로 변환됩니다.


2-2. 사용자 입력과 등록된 제스처 간 유사도 계산

정규화된 입력 제스처는 저장된 제스처들과 하나씩 직접 비교됩니다.
이때 사용되는 비교 방식은 좌표 간 거리 차이를 기반으로 한 유사도 계산입니다.

🔍 유사도 계산 방식

각 제스처는 x, y 좌표로 이루어진 점들의 배열입니다.
이때 두 제스처의 같은 인덱스의 점끼리 짝지어, 거리 차이를 계산합니다.

예를 들어 두 정규화된 제스처가 아래처럼 생겼다고 가정합니다:

제스처 A: [{x: 12, y: 30}, {x: 18, y: 35}, ...]
제스처 B: [{x: 14, y: 32}, {x: 20, y: 33}, ...]

이때 두 제스처를 비교하려면, 같은 순서에 있는 점들끼리 짝을 지어, 두 점 사이의 실제 거리의 차이를 계산해야 합니다.
📐 이 거리는 유클리디안 거리 공식을 이용해 계산합니다:

두 점 (x₁, y₁), (x₂, y₂) 간 거리 = √((x₁ - x₂)² + (y₁ - y₂)²)

이 공식을 통해, 각 점 쌍마다 거리를 구하고
→ 그 거리들을 모두 더한 뒤
→ 전체 점 개수(예: 32개)로 나누면
두 제스처의 평균 거리를 계산할 수 있습니다.

즉, 이 평균 거리가 곧 두 제스처의 유사도를 나타내는 수치가 됩니다.


Note

유사도 수치가 의미하는 것

  • 값이 작을수록 → 두 제스처의 모양이 매우 비슷함
  • 값이 클수록 → 두 제스처가 서로 많이 다름

따라서, 이 유사도 수치가 특정 기준값보다 작을 경우
충분히 비슷하다고 판단하고 해당 제스처에 연결된 기능을 실행합니다.


2-3. 가장 유사한 제스처를 찾아 기능으로 연결하는 과정

정규화된 사용자 제스처는 저장된 제스처들과 비교되어, 가장 유사한 하나의 제스처가 선택됩니다.
이후, 이 제스처에 사용자가 직접 지정해 둔 기능 정보를 바탕으로 실제 동작이 실행됩니다.

연결된 기능은 어디서 찾을까?

각 제스처는 다음과 같은 정보와 함께 매핑되어 저장해 둡니다:

제스처 이름 (ex: "N자 패턴") ㅡ 사이트 정보 (ex: "notion.so") ㅡ 실행할 기능 이름 (ex: "스크롤 맨 위로")

비슷한 제스처가 선택되면, 이 제스처 이름을 키로 삼아 매핑된 기능 정보를 조회합니다:

const matchedGestureMapping = await getMatchedMapping(matched.name);

기능은 어떻게 실행될까?

매핑된 기능 정보를 기반으로, 내부에 정의된 동작 목록 중 해당 기능에 대응되는 함수를 찾아 실행합니다.

이 흐름은 다음과 같은 순서로 진행됩니다:

  1. 사용자가 "N자 패턴" 제스처를 “notion.so”에서 “스크롤 맨 위로” 기능을 실행하도록 등록해두었다고 가정
  2. 이 등록 정보를 바탕으로, "스크롤 맨 위로"라는 기능 이름이 내부적으로 어떤 실행 키에 매핑되어 있는지 확인
  3. 해당 키를 이용해 미리 정의된 기능 실행 함수 목록 중 하나를 찾아 실행
  4. 내부에서는 scrollTop > 0인 모든 DOM 요소를 찾아, 각 요소를 scrollTo({ top: 0 })로 스크롤 이동시킴

사용자가 지정한 매핑 정보가 있다면, 이후 제스처 한 번으로도 해당 기능이 자동 실행되는 구조입니다.

📽️ 제스처 입력 후 동작 실행 예시

제스처 입력 후 동작 실행


💥 트러블 슈팅

1. 제스처 궤적이 이어져 그려지는 문제

이전에 그렸던 제스처와 새 제스처가 하나의 선으로 이어져 그려지는 문제

문제 상황

  • 마우스로 제스처를 그리고 마우스를 뗀 뒤, 새로운 위치에서 다시 제스처를 시작했더니,
    이전 제스처의 마지막 점과 연결된 선이 이어서 그려지는 현상이 나타났습니다.
📽️ 문제 발생 장면 보기

제스처 궤적 이어짐 문제 영상


잘못된 해결 시도

초기에는 아래과 같이 points.length === 1 조건을 사용하여 첫 점에서만 beginPath()를 호출하고,
이후에는 lineTo()로 선을 이어 그리는 방식으로 구현했습니다.

if (points.length === 1) {
  ctx.beginPath();
  ctx.moveTo(x, y);
} else {
  ctx.lineTo(x, y);
  ctx.stroke();
}

Note

이 코드는 HTML 요소의 2D 렌더링 컨텍스트인 CanvasRenderingContext2D 객체의 메서드를 사용합니다.


🔍 왜 points.length === 1 조건을 사용했는가?

points.length는 지금까지 입력된 제스처의 좌표 수를 의미합니다.
따라서 points.length === 1인 경우는 제스처가 시작된 순간을 의미합니다.

이 시점에만 다음 메서드를 호출했습니다:

메서드 설명
beginPath() 이전 경로와 단절하고 새 선을 시작
moveTo(x, y) 선은 그리지 않고, 시작점을 지정

그 이후에는 다음 메서드를 사용해 선을 이어 그렸습니다:

메서드 설명
lineTo(x, y) 선을 그릴 경로를 추가
stroke() 지정된 경로를 화면에 실제로 그림

즉, 처음 점이 입력될 때만 beginPath()moveTo()를 호출하여 선의 시작점을 지정하고,
나머지 점들은 lineTo()를 통해 이어서 그리는 구조였습니다.

⮕ 이 방식을 활용하면 매 제스처마다 하나의 독립된 선이 그려질 것으로 기대했습니다.

하지만 여전히 이전 제스처의 마지막 점과 연결된 선이 그려지는 현상이 발생했습니다.

문제의 원인을 확인하기 위해 beginPath() 호출 여부를 로그로 디버깅한 결과,
해당 분기가 전혀 실행되지 않았고, 콘솔 메시지도 출력되지 않았습니다.
이로 인해 새로운 제스처임에도 불구하고 기존 경로가 그대로 이어졌습니다.

if (points.length === 1) {
  console.log("beginPath 호출"); //콘솔 메시지 출력❌
  ctx.beginPath();

📌 실제 흐름 예시

  1. 제스처 A → 잘 그려짐
  2. 기존 제스처가 시각적으로 점차 사라짐
  3. 새로운 B 제스처를 그릴 때, beginPath()가 제대로 호출되지 않아,
  4. B가 이전에 그렸던 A와 이어진 선으로 그려짐

해결 방법

points.length === 1 조건이 실행되지 않아 beginPath()가 호출되지 않는 문제를 해결하기 위해,
조건문을 제거하고 모든 점마다 명시적으로 새로운 경로를 시작하도록 수정했습니다.

ctx.beginPath();
const [prevX, prevY] = points[points.length - 2] || [x, y];
ctx.moveTo(prevX, prevY);
ctx.lineTo(x, y);
ctx.stroke();
  • 항상 beginPath()를 호출하여 이전 경로와의 연결을 명확히 끊고,
  • points의 직전 점에서 현재 점까지의 선만을 그리도록 변경했습니다.
  • 만약 직전 점이 존재하지 않는 경우(points.length === 1일 때)는 현재 점을 시작점으로 처리하여 예외를 방지하였습니다.

🚨 새로운 문제 발생

위와 같이 beginPath() 호출 위치를 수정해 이전 제스처와 새롭게 그린 제스처가 연결되는 문제는 해결했지만,
다른 유형의 시각적 문제가 새롭게 발생하였습니다.


2. 새로운 제스처가 실시간으로 보이지 않는 문제

제스처를 그리는 동안 선이 보이지 않고, 끝난 직후에야 잠시 나타났다 사라지는 문제

문제 상황

beginPath() 호출 위치를 모든 점마다 명시적으로 처리하도록 수정한 이후,
제스처 궤적이 이전 선과 물리적으로 이어지는 문제는 해결되었습니다.

하지만 그 과정에서 새로운 시각적 문제가 발생했습니다:

  • 처음 마우스를 움직이며 선을 그릴 때 정상적으로 실시간 출력됨
  • 제스처 입력이 끝나면 화면에 남아 있던 선이 천천히 사라짐
  • 이후 다시 제스처를 그리기 시작하면:
    • 선을 그리고 있음에도 아무것도 보이지 않고,
    • 제스처가 끝나는 순간 선이 한꺼번에 나타났다 곧바로 사라짐

이로 인해 제스처를 그리고 있는 동안에는 아무런 피드백이 없고,
제스처가 끝난 뒤에야 선이 한꺼번에 나타나는 부자연스러운 동작이 발생했습니다.

📽️ 문제 발생 장면 보기

제스처 궤적이 보이지 않는 문제 영상


디버깅 과정

1. 마우스 움직임에 따라 선을 그리는 동작이 실행되고 있는지 확인

처음에는 "선을 그리고 있음에도 화면에 아무것도 나타나지 않는다"는 현상이
화면에 선을 출력하는 로직이 실행되지 않아서 생긴 문제일 수 있다고 판단했습니다.

이를 확인하기 위해,
마우스를 움직일 때마다 선을 그리는 함수가 실제로 실행되고 있는지 확인하는 로그를 추가했습니다.

console.log(`좌표: (${x}, ${y})`);

이 로그는 제스처 중 마우스를 이동할 때마다 정상적으로 출력되었으며,
그리기 동작 자체는 실행되고 있었음을 확인할 수 있었습니다.
즉, 사용자의 실시간 제스처가 화면에 보이지 않는 이유는 그리기 로직이 호출되지 않는 문제가 아니라는 것을 알게 되었습니다.

2. 캔버스의 투명 상태 확인

그리기 동작 자체에 문제가 아니라면, "선이 그려지는 캔버스가 완전히 투명한 상태로 남아 있는 것이 아닐까?"라는 의문이 들었습니다.
이를 확인하기 위해, 선을 그리는 시점에 해당 캔버스의 투명도(opacity) 값을 출력하는 로그를 추가하였습니다.

const gestureCanvas = document.getElementById("gesture-canvas");
console.log(`현재 opacity: ${gestureCanvas?.style.opacity}`);

확인된 결과는 다음과 같았습니다:

구분 상황 opacity 값 동작 결과
처음 그린 제스처 정상적으로 캔버스가 생성됨 1 사용자가 그리는 제스처 선이 실시간으로 렌더링됨
제스처 종료 후 fadeOutCanvas() 실행 점점 감소 → 0 선이 점차 사라짐
두 번째 제스처 기존 캔버스가 남아 있음
→ 새로 만들지 않음
-3.19189e-16 선을 그리고 있음에도 화면에는 보이지 않음

🔍 opacity 확인 로그

stroke 실행 - 현재 opacity: -3.19189e-16

이 값은 음수 값으로, 브라우저에서는 완전히 투명한 상태와 동일하게 처리되고 있었습니다.


해당 문제는 선을 실제로 화면에 렌더링하는 stroke() 함수가 실행되지 않았던 것이 아니라,
완전히 투명해진 캔버스 위에 선이 그려지고 있었기 때문에 화면에 아무것도 보이지 않았던 것입니다.

또한, 이전에 사용했던 캔버스가 여전히 DOM에 남아 있었기 때문에
새로운 캔버스가 생성되지 않았고, 결국 opacity 값이 0인 투명한 캔버스가 그대로 재사용되고 있었습니다.


Important

왜 제스처를 다 그리고 나서야 선이 잠깐 보였다가 사라졌을까요?

이는 캔버스를 서서히 투명하게 만드는 함수에서 opacity 값을 다시 1로 초기화하고 시작하기 때문입니다.

let opacity = 1; // 서서히 사라지는 애니메이션을 시작하기 위한 초기값

이전에 그려진 선들은 opacity = 0 상태의 캔버스에만 존재하고 있었기 때문에 그리는 동안에는 보이지 않았습니다.

그러나 사용자가 마우스를 떼는 순간, 이 함수가 실행되며 opacity가 1로 되살아나고,
그동안 그려진 선이 잠시 보였다가 곧바로 다시 0까지 줄어들며 사라지는 것처럼 보이는 현상이 발생한 것입니다.

즉, 보이지 않던 선이 한 번에 나타났다가 사라지는 이유는
opacity가 코드상에서 다시 1로 설정되며 잠시 렌더링되었기 때문입니다.


최종 해결 방법

문제의 원인은, 완전히 투명해진 기존 캔버스가 DOM에 남아 있어,
새로운 제스처를 시작하더라도 투명한 캔버스 위에 선을 그리고 있었기 때문에 아무것도 보이지 않았던 것이었습니다.

이를 해결하기 위해, 캔버스의 투명도가 0에 도달했을 때 해당 캔버스를 DOM에서 제거하고,
그리기에 사용되는 context도 함께 초기화되도록 수정하였습니다.

🔧 수정된 로직

if (opacity <= 0) {
  clearInterval(fade);   // 점차 투명하게 만드는 애니메이션 종료
  canvas.remove();       // 완전히 투명해진 캔버스를 DOM에서 제거
  ctx = null;            // 다음 제스처를 위한 context 초기화
}

적용 결과

  • 다음 제스처 시작 시 새로운 캔버스가 정상적으로 생성되고,
  • 새로운 context를 통해 선을 실시간으로 그릴 수 있게 되었습니다.
  • 이를 통해 투명한 캔버스가 재사용되어 아무것도 보이지 않던 문제를 해결할 수 있었습니다.

🌿 개인 회고

“작동하는 기능”에서 “도구로서의 완성도”를 고민하게 된 프로젝트

단축키 사용의 불편함을 줄이고자 시작한 이 프로젝트는, 단순한 아이디어에서 출발했지만 실제로 사용자 정의 제스처가 유용하게 작동하려면 어떤 구조와 흐름이 필요한지 깊이 고민하고 구현해보는 계기가 되었습니다.

특히 사용자별 데이터 분리, 제스처 정규화 처리, 실시간 시각화 기능 등을 하나씩 설계하고 완성해가며, 단순한 기능 구현을 넘어 하나의 도구를 완성도 있게 만드는 과정을 직접 경험할 수 있었습니다. 필요하다고 느낀 기능을 직접 구조화하고 눈앞에서 실제로 작동하게 만들어보는 과정은 단순한 기술 습득 이상의 가치가 있었고, 문제를 정의하고 끝까지 해결하는 힘을 기를 수 있었습니다.

다만 아쉬운 점도 있습니다. 초기 설계에서 사용자 유형을 고려하지 않은 저장 구조로 인해 클라이언트와 서버의 역할을 새롭게 분리하는 과정이 필요했고, 이로 인해 전반적인 흐름을 다시 설계해야 했습니다. 또 UX 측면에서도 제스처 색상이나 두께 설정 같은 사용자 맞춤 기능을 더 담지 못한 점은 아쉬움으로 남습니다. 그러나 이러한 시행착오는 구조와 흐름을 명확하게 설계하는 것의 중요성을 실감하게 해주었고, 각 기능의 위치와 책임을 더 명확하게 바라보는 시야를 키워주는 계기가 되었습니다.

이번 경험을 통해, 앞으로는 전체 흐름과 구조를 명확히 설계하고 초기 단계부터 사용자 경험까지 함께 고려하는 개발을 실천해 나갈 것입니다. 또한 제스처 인식의 정확도를 높이기 위해 회전 불변성 도입 등 기술적인 측면에서도 지속적인 개선을 이어가며, 문제를 해결하고 개선하는 개발자로 더욱 단단해지고자 합니다.

About

마우스 제스처로 원하는 웹 동작을 빠르게 실행할 수 있는 Chrome 확장 프로그램

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published