Skip to content

Repository files navigation

Better Gesture

Mac 트랙패드에서 두 손가락으로 좌우 제스처를 하면, 현재 탭에서 실제로 방문했던 이전·다음 페이지 목록을 웹페이지 위에 보여 주는 Chrome 확장 프로그램입니다.

Chrome 104 이상이 필요합니다.

설치

  1. Chrome 주소창에 chrome://extensions를 입력합니다.
  2. 오른쪽 위의 개발자 모드를 켭니다.
  3. 압축해제된 확장 프로그램을 로드합니다를 누릅니다.
  4. 이 프로젝트 폴더(ChromeExtension)를 선택합니다.
  5. 이미 열려 있던 일반 웹페이지는 한 번 새로고침합니다.

사용법

  1. 같은 탭에서 웹페이지를 두세 군데 방문합니다.
  2. 페이지의 가로 스크롤 영역이 아닌 곳에서 두 손가락을 오른쪽으로 짧게 튕기고 놓으면 뒤로 한 단계, 반대쪽으로 하면 앞으로 한 단계 이동합니다.
  3. 같은 방향으로 0.18초 동안 계속 당기면 그 자리에서 바로 기록 메뉴가 열립니다. 손을 떼기를 기다리지 않습니다. 당기는 동안 화면 가장자리의 화살표 표시가 손가락을 따라 움직이며 0.18초까지 얼마나 남았는지 보여 줍니다.
  4. 0.18초의 40%(72ms)를 넘게 당긴 뒤 손가락을 그대로 대고 멈추면 기본 0.45초 뒤 메뉴가 열립니다. 0.18초까지 당기지 않아도 됩니다. Chrome 151 이상에서만 동작합니다(아래 참고).
  5. 메뉴가 열린 상태에서 트랙패드를 위·아래로 움직여 항목을 고르고 손을 떼면 해당 페이지로 이동합니다.
  6. 항목을 클릭하거나 키보드 ·Enter로도 선택할 수 있습니다.
  7. 메뉴는 닫기 버튼이나 바깥 클릭, Esc로 닫습니다. 메뉴가 열린 동안 좌우 제스처는 무시됩니다.
  8. 뒤로 갈 기록이 없는 탭에서는 짧게 튕기든 길게 당기든 메뉴를 열지 않고 그 탭을 닫습니다. 링크로 열린 탭이면 그 탭을 연 탭이 앞으로 나오고, 아니면 닫기만 하고 다음 탭은 Chrome이 정합니다. 그 탭 안에서 페이지를 옮겨 다녀 기록이 생긴 뒤라면 평소처럼 메뉴가 열리고, 돌아갈 기록만 없을 때는 연 탭이 탭을 연 페이지로 한 줄 표시됩니다.

4번의 기다리는 시간이 손에 안 맞으면 팝업에서 0.30초 · 0.45초 · 0.70초 중에서 고르세요. 짧게 잡으면 멈추자마자 메뉴가 뜨지만 당기다 잠깐 멈칫한 것까지 메뉴가 되고, 길게 잡으면 확실히 멈춰야 열립니다.

3번의 0.18초는 설정에 없습니다. 실제 당김 폭(대략 100~250ms) 한가운데라 손버릇에 따라 갈리지 않기 때문입니다. 바꾸려면 content/gesture-controller.jsPULL_HOLD_MS를 고칩니다.

macOS의 스크롤 방향 설정에 따라 뒤로가기와 앞으로가기가 반대로 인식되면 Chrome 툴바의 확장 아이콘을 눌러 뒤로가기 방향을 바꾸세요. 팝업에는 이 사이트에서 사용, 뒤로가기 방향, 언어, 기다리는 시간 넷만 있고 접혀 있는 항목은 없습니다.

사이트가 가로 스와이프를 자기 용도로 쓴다면 그 사이트에 들어간 채로 확장 아이콘을 눌러 이 사이트에서 사용을 끄세요. 지금 보고 있는 탭의 호스트명이 토글 아래에 그대로 보이고, 끄면 새로고침 없이 바로 적용됩니다. 다시 들어가서 켜면 풀립니다. 전체를 한 번에 끄는 스위치는 없습니다 — 모든 사이트에서 멈추려면 chrome://extensions에서 확장을 사용 중지하세요.

언어

사용자에게 보이는 문구는 영어·한국어·일본어·중국어(간체) 를 지원합니다. _locales/<언어>/messages.json에만 문구를 두고 chrome.i18n.getMessage로 꺼냅니다. 기본값은 Chrome UI 언어이고, 팝업의 언어 항목에서 직접 고를 수도 있습니다. 웹 스토어 등록 정보의 이름과 설명도 같은 파일에서 가져갑니다.

  • 스토어 목록에 뜨는 제목은 manifest의 name을 그대로 씁니다. 긴 제목을 따로 다는 칸이 없으므로, 사람들이 실제로 검색하는 말을 이름 뒤에 붙였습니다(Better Gesture — trackpad swipe to browse tab history). 브랜드는 번역하지 않고 뒤의 설명만 언어를 따릅니다 — 검색어는 언어마다 다르기 때문입니다. 상한은 75자이고 넘으면 업로드가 거부됩니다.

  • 화면 안에서는 짧은 이름만 씁니다. 폭이 좁은 팝업 헤더에 한 줄 설명까지 들어가면 자리를 다 먹습니다.

  • 조회는 shared/i18n.jst(키) 하나를 거칩니다. 없는 키는 빈 문자열이 아니라 키 이름을 그대로 돌려주므로 번역 누락이 화면에 드러납니다.

  • HTML은 manifest처럼 __MSG_키__ 치환을 해 주지 않으므로, 마크업에는 data-i18n="키"만 적고 localizeDocument()가 채웁니다. 문구를 마크업에 복제하지 않습니다.

  • 선택지 라벨의 초 단위는 언어마다 다릅니다(0.45s / 0.45초 / 0.45秒). 숫자는 shared/settings.js가 만들고 단위는 thresholdSeconds 문구가 붙이므로, 선택지 값을 바꿔도 네 언어가 함께 따라옵니다.

  • tests/i18n.test.js가 네 언어의 키 목록·자리표시자가 같은지, 소스에서 부르는 키가 모두 정의됐는지, 정의한 키가 모두 쓰이는지를 검사합니다.

  • 언어를 추가하려면 _locales에 폴더 하나를 만들고 tests/i18n.test.jsLOCALES에 넣으면 됩니다. 빠진 키는 테스트가 알려 줍니다.

  • 언어를 직접 고르면 chrome.i18n을 덮어쓸 수 없으므로 같은 메시지 파일을 읽어 조회표로 씁니다(useMessages). 표가 없으면 자동으로 돌아가므로, 표를 불러오는 중에 부른 문구도 빈칸이 되지 않습니다. 자동일 때는 아무것도 읽지 않습니다chrome.i18n이 이미 답입니다.

  • 콘텐츠 스크립트는 _locales 파일을 직접 읽을 수 없어 서비스 워커에 물어봅니다(GET_MESSAGES). 워커는 자기가 만드는 오류 문구에도 같은 표를 쓰고, 설정이 바뀌면 chrome.storage.onChanged로 다시 읽습니다.

  • 언어를 바꾸면 이미 만들어 둔 메뉴 DOM에는 옛 문구가 박혀 있으므로 resetUi()로 버립니다. 다음 제스처에서 새 언어로 다시 만들어집니다. 열려 있던 탭을 새로고침할 필요는 없습니다.

  • 언어 이름(English, 한국어, 日本語, 简体中文)은 번역하지 않습니다. 지금 읽을 수 없는 언어로 적혀 있으면 자기 언어를 찾을 수 없기 때문입니다.

일본어·중국어는 원어민 검수를 받지 않은 초안입니다.

구현 방식

  • 두 동작은 손가락이 실제로 움직인 시간으로만 갈립니다. 당긴 거리는 판정에 쓰지 않고 인디케이터를 미는 데에만 씁니다. 거리를 함께 보면 크게 당겼다 놓는 동작이 "메뉴"와 "한 단계"로 갈려서, 손을 뗐는지와 무관한 두 번째 기준을 사용자가 감으로 익혀야 합니다. 시간만 쓰면 규칙이 "떼면 한 단계, 멈추면 메뉴" 두 줄로 줄어듭니다.
  • 인디케이터와 메뉴가 붙는 가장자리는 뒤로가기면 왼쪽, 앞으로가기면 오른쪽입니다. 뒤로가기 방향 설정과 무관합니다 — 왼쪽으로 밀어서 뒤로 가게 바꿔도 뒤로가기는 왼쪽에서 나옵니다. 그래서 표시를 밀어내는 부호는 손가락이 아니라 그 가장자리가 정합니다. 당긴 거리의 부호를 그대로 쓰면 설정을 뒤집은 사람에게서 둘이 반대가 되어, 당긴 시간은 밀어내는데 당긴 거리는 끌어당겨 표시가 나왔다 들어갔다 합니다.
  • 그래서 당김 판정 시간은 실제 당김 시간(대략 100~250ms)의 한가운데인 180ms로 잡았습니다. 거리를 보조하던 예전 값(250/350/500ms)은 느리게 당기는 사람만 구제하면 됐으므로 단독 기준으로는 너무 깁니다.
  • 사용자에게 여는 값은 이쪽이 아니라 "멈춘 뒤 기다리는 시간"입니다. 당김 시간은 트랙패드 입력 폭이 정하는 값이라 손버릇으로 갈리지 않지만, 멈춤은 다릅니다 — 당기다 멈칫하는 사람과 확실히 멈추고 기다리는 사람이 갈립니다. 두 값을 다 열면 서로 얽혀서 어느 쪽을 만져야 하는지 알 수 없어지므로, 손버릇이 실제로 다른 하나만 0.30 / 0.45 / 0.70초로 엽니다.
  • 브라우저는 트랙패드에서 손가락을 뗀 순간(kPhaseEnded)을 웹페이지에 전달하지 않고, 손가락을 대고 가만히 있으면 wheel 이벤트 자체가 오지 않습니다. 그래서 재는 것은 "누르고 있는 시간"이 아니라 움직인 시간입니다.
  • 손을 뗀 뒤에도 macOS는 관성(momentum) wheel 이벤트를 1초 넘게 계속 보냅니다. 이걸 시간에 포함하면 짧게 튕긴 제스처가 길게 당긴 것과 구분되지 않으므로, 관성 구간은 계산에서 제외합니다. Chrome 151부터는 표준 WheelEvent.momentum으로 정확히 알 수 있고, 그 이전 버전에서는 속도 감쇠 비율이 다섯 표본 연속 일정 구간에 머무는지로 판정합니다.
  • 당긴 거리는 판정에서 빠졌지만 속도 상한(2px/ms) 은 유지합니다. 세게 튕기면 손가락도 실제로 멀리 움직이므로, 상한이 없으면 화살표 표시가 손가락보다 훨씬 앞서 튀어 나갑니다.
  • 감쇠 판정이 확정되기 전 구간의 거리는 보류해 둡니다. 다시 빨라지면 합치고, 관성으로 확정되면 버립니다. 다만 보류는 확정에 필요한 최근 6개 이벤트까지만 합니다. 무한정 붙잡으면 잠깐씩 느려지는 정상적인 당김의 절반이 통째로 버려집니다.
  • 이 보류는 WheelEvent.momentum이 없는 Chrome에서만 필요합니다. 151+에서는 관성 여부가 이벤트마다 정확히 오므로 손가락 이동이 거의 그대로 반영됩니다(같은 입력 기준 107px 대 156px).
  • 관성이 시작됐다는 것은 손가락을 뗐다는 뜻이므로, 180ms를 넘지 못한 제스처는 그 시점에 곧바로 한 단계 이동합니다. 튕기지 않고 천천히 놓아 관성이 아예 없을 때만 입력이 멈춘 시점을 손을 뗀 것으로 봅니다.
  • 손가락을 대고 있어도 움직이지 않으면 wheel 이벤트가 오지 않아, "쉬는 중"과 "손을 뗀 뒤"가 똑같아 보입니다. 손을 뗐다는 확실한 신호는 관성뿐입니다.
  • WheelEvent.momentum이 있으면 이 모호함이 한쪽으로 풀립니다. 튕겼다면 반드시 관성이 오므로, 관성이 하나도 없이 조용해진 것은 손가락이 아직 닿아 있다는 뜻입니다. 그래서 180ms의 40%를 넘게 당겨 둔 뒤 고른 시간만큼 멈추면 "당긴 채 멈춤"으로 보고 메뉴를 엽니다(기본 0.45초). 손을 떼지 않아도 열리므로 그대로 위·아래로 골라 이동할 수 있습니다. 감쇠 추정은 짧은 튕김의 관성을 놓칠 수 있어 이 판정을 쓰지 않습니다 — 손을 뗀 제스처가 홀드로 오해받기 때문입니다.
  • 감쇠 추정 경로에서는 기다리는 시간을 진행 정도에 맞춥니다. 180ms의 40%를 넘게 당겨 둔 제스처는 이어질 가능성이 높으니 1.2초까지 참고, 이제 막 시작한 작은 제스처는 고른 시간만에 정리해 반응이 굼떠지지 않게 합니다. 관성으로 손 뗌을 정확히 알 수 있으면 참을 이유가 없으므로 어느 쪽이든 고른 시간으로 통일합니다. 오래 참는 쪽도 고른 시간보다 짧아지지는 않습니다 — 기다리겠다고 고른 시간을 확장이 먼저 끊는 셈이기 때문입니다.
  • 어느 경로든 멈칫한 시간은 "당긴 시간"에 그대로 들어가므로, 쉬었다가 다시 움직이면 그 순간 180ms를 넘어 메뉴가 열립니다.
  • 제스처 피드백은 확장이 직접 그리는 화살표 표시만 움직입니다. 페이지 본문에 translate 같은 변환을 걸면 그 요소가 position: fixed 자손의 containing block이 되어 사이트의 고정 헤더·배너가 튀기 때문에, 웹페이지의 레이아웃은 건드리지 않습니다.
  • 확장 기능이 켜진 동안에는 페이지 루트의 가로 오버스크롤 탐색을 차단해 Chrome 기본 제스처와의 충돌을 방지합니다.
  • 끄기는 사이트별로만 있습니다. 전역 스위치는 두지 않았습니다. 제스처가 방해가 되는 이유는 그 사이트가 가로 스와이프를 자기 용도로 쓰기 때문이지 사용자가 확장을 그만 쓰고 싶어서가 아니고, 후자라면 Chrome의 확장 사용 중지가 이미 그 일을 합니다. 제외 여부는 호스트명으로만 봅니다. 스킴이나 경로까지 따지면 같은 사이트가 http와 https로, /a/b로 갈라져 사용자가 끈 것과 실제로 꺼지는 곳이 어긋납니다.
  • 제외한 사이트에서는 제스처를 무시하는 데에서 그치지 않고 위의 오버스크롤 차단도 함께 풉니다. 안 그러면 껐는데 Chrome 원래 스와이프까지 죽어서, 끄기 전보다 못한 상태가 됩니다.
  • 목록은 chrome.storage.sync에 호스트명 배열로 두고 300개까지만 보관합니다. sync는 항목 하나가 8KB를 넘으면 통째로 거부하므로 상한이 필요하고, 넘치면 오래 전에 끈 것부터 밀어냅니다. 팝업은 host_permissions<all_urls> 덕분에 tabs 권한 없이 현재 탭 주소를 읽습니다 — 권한을 하나 더 요구하면 설치 화면의 경고가 늘어납니다.
  • 한 단계 이동은 chrome.tabs.goBack() 또는 chrome.tabs.goForward()로 실행합니다.
  • window.history.length가 1이면 이 탭에는 앞뒤 어느 쪽으로도 보여 줄 기록이 없으므로, 180ms를 넘겨 당겨도 메뉴를 열지 않고 한 단계 이동으로 처리합니다. 서비스 워커에 물어보고 나서야 알면 빈 메뉴가 떴다 사라지는데, 이 값은 왕복 없이 콘텐츠 스크립트에서 바로 읽을 수 있습니다.
  • 뒤로 갈 기록이 없어 goBack()이 실패하면 현재 탭을 닫습니다. 돌아갈 기록이 없다는 것은 이 탭에서 볼 것이 끝났다는 뜻이고, 사용자가 원하는 것은 그 전에 보던 화면이기 때문입니다. 링크로 열린 탭인지는 따지지 않습니다 — 주소를 직접 친 탭이든 아니든 뒤로 갈 곳이 없기는 마찬가지입니다. 앞으로가기에는 적용하지 않습니다.
  • 이 탭을 연 탭(openerTabId)이 있으면 닫기 전에 그 탭을 활성화하고, 다른 창에 있으면 그 창도 앞으로 가져옵니다. 연 탭이 없으면 닫기만 하고 다음에 어느 탭이 보일지는 Chrome이 정합니다.
  • 단, 연 탭이 없고 창에 이 탭 하나만 남았으면 아무것도 하지 않습니다. 닫는 순간 창이 통째로 사라지는데 돌아갈 곳도 정해져 있지 않아서, 뒤로가기 한 번으로 시킬 만한 일이 아닙니다. 연 탭이 있으면 이야기가 다릅니다 — 어디서 왔는지가 분명하니 창이 닫혀도 그쪽으로 이어집니다.
  • 탭 관계는 chrome.tabs.onCreated 시점에 세션 저장소에도 적어 둡니다. Chrome은 사용자가 탭을 손으로 옮겨 다니면 openerTabId를 지우는데, 그 뒤에도 돌아갈 곳을 알 수 있어야 하기 때문입니다. openerTabId가 남아 있으면 그쪽을 우선합니다. 탭이 닫히면 그 탭과 관련된 기록을 함께 지우고, 놓친 경우에 대비해 최근 300개까지만 보관합니다.
  • 탭을 닫는 것은 응답을 보낸 뒤에 합니다. 기다렸다가 닫으면 페이지의 beforeunload 확인창에 막혔을 때 곧 사라질 탭의 메뉴가 응답을 기다린 채 멈춥니다.
  • 아무 일도 할 수 없는 한 단계 제스처는 오류로 표시하지 않고 조용히 무시합니다. 그 밖의 실패는 짧은 토스트로 알립니다.
  • 가로로 당긴 직후 이어지는 세로 wheel 값을 누적해 선택 막대를 움직이고, 입력이 끝난 시점을 손을 뗀 것으로 판정합니다. 세로 판정 기준은 가로와 대칭(|dy| > |dx| × 1.25)이라, 비스듬히 당기는 동작이 당김과 선택으로 동시에 잡히지 않습니다.
  • 가로 스크롤 요소 위에서는 스크롤 위치나 방향과 관계없이 기록 제스처를 시작하지 않습니다. 다만 문서 자체의 가로 스크롤은 그 방향으로 실제 더 스크롤될 때만 양보하므로, 몇 px만 가로로 넘치는 페이지에서도 제스처가 동작합니다.
  • 지도나 화이트보드처럼 페이지가 휠 입력을 직접 쓰는 영역에서는 물러납니다. 사이트가 아니라 영역 단위이므로, 같은 페이지의 지도 밖에서는 제스처가 그대로 동작합니다. 그런 영역은 wheel의 기본 동작을 막거나 전파를 끊는데, 확장이 그 표시를 보려면 페이지의 처리기가 먼저 돌아야 합니다. 그래서 wheel은 캡처가 아니라 버블 단계에서 듣습니다 — 기본 동작인 가로 스크롤은 전파가 다 끝난 뒤에 일어나므로, 버블에서 막아도 늦지 않습니다.
  • 트랙패드로 확대해 둔 화면을 옆으로 미는 동안에는 물러납니다. 이때 움직이는 것은 시각 뷰포트라 문서의 스크롤 위치는 그대로여서, 위의 가로 스크롤 판정으로는 잡히지 않습니다. visualViewport의 배율과 위치로 따로 판정하되 규칙은 문서 가로 스크롤과 같습니다 — 그 방향으로 실제 더 밀 수 있을 때만 양보하므로, 끝까지 밀어 놓은 상태에서는 확대한 채로도 제스처가 동작합니다. 브라우저 확대(Cmd +)는 이 배율을 바꾸지 않으므로 아예 해당되지 않습니다.
  • 페이지에 어떤 휠 처리기가 붙어 있는지는 확장이 직접 볼 수 없습니다. 이벤트를 실제로 가져갔는지가 유일하게 관측 가능한 신호라 판정도 그것 하나로 합니다. 제스처 도중에 가져가기 시작하면 판정까지 가지 않고 접습니다 — 페이지가 쓴 입력으로 페이지를 옮겨 버리면 안 됩니다.
  • 합성 경로의 계산된 스타일을 읽는 판정은 같은 제스처 안에서 한 번만 수행하고 재사용합니다.
  • 메뉴 UI는 닫힌 Shadow DOM 안에 만들어 웹사이트의 CSS/JavaScript와 충돌을 줄였습니다.
  • 각 방문 기록은 Chrome Favicon API로 사이트 아이콘을 표시하고, 아이콘을 가져오지 못하면 도메인의 첫 글자를 표시합니다.
  • 서비스 워커가 잠깐 chrome.debugger를 연결하고 Page.getNavigationHistory로 현재 탭의 실제 세션 히스토리를 읽습니다. 같은 탭의 작업은 한 줄로 세워 실행해 연결이 겹치지 않게 합니다.
  • 이전·다음 기록에서 선택한 항목은 Page.navigateToHistoryEntry로 이동하며, 작업 직후 디버거 연결을 해제합니다. 뒤로 갈 기록이 하나도 없을 때만 이 탭을 연 탭을 가상 항목(id: -1)으로 덧붙이고, 이 항목은 디버거를 붙이지 않고 처리합니다.
  • 알려지지 않은 내부 오류 원문은 페이지에 노출하지 않고 일반 안내 문구로 바꿔서 전달합니다.

방문 기록은 외부로 전송하거나 저장하지 않습니다. 메뉴를 표시하는 동안 현재 탭에서만 읽고 사용합니다. Favicon 리소스는 use_dynamic_url로 공개해, 웹페이지가 고정된 확장 주소로 방문 기록을 추측하지 못하게 했습니다(Chrome 130 이상에서 적용되며 그 이전 버전에서는 무시됩니다).

알아둘 점

  • chrome:// 페이지, Chrome 새 탭, Chrome 웹 스토어 등 보호된 페이지에는 콘텐츠 스크립트를 넣을 수 없어 동작하지 않습니다.
  • 콘텐츠 스크립트는 최상위 프레임에서만 실행되므로, 페이지에 삽입된 다른 사이트의 iframe(임베드 영상, 문서 등) 위에서는 제스처가 동작하지 않습니다.
  • 휠 입력을 페이지 전체에서 가로채는 사이트(부드러운 스크롤을 직접 구현한 페이지 등)에서는 제스처가 아예 동작하지 않습니다. 확장 입장에서 이런 사이트는 화면 전체가 지도인 것과 구분되지 않습니다. 반대로 페이지 안의 작은 위젯이 휠을 가져가는 곳에서도 그 위젯 위에서만 동작하지 않습니다.
  • debugger는 강한 권한입니다. 정확한 현재 탭의 앞뒤 이동 순서를 얻기 위해 필요합니다.
  • favicon 권한은 방문 기록 메뉴에 각 사이트의 아이콘을 표시하는 데만 사용합니다.
  • 해당 탭에서 DevTools가 이미 열려 있거나 다른 디버거가 연결되어 있으면 동시에 사용할 수 없습니다. 이때 뜨는 오류 안내는 잠시 뒤 스스로 닫혀 다음 제스처를 막지 않습니다.
  • Chrome이 디버거 연결 안내를 잠깐 표시할 수 있습니다.
  • 지속 시간이 같은 두 제스처는 이동 거리가 아무리 달라도 같게 처리됩니다. 실측 예로 89px을 214ms에 천천히 당긴 것과 465px을 224ms에 빠르게 당긴 것은 5배 차이인데도 둘 다 메뉴가 열립니다. 거리를 함께 보던 예전에는 갈렸습니다. 시간만으로 판정하는 대가입니다.
  • 크게 당겼다가 손을 떼면 길이와 무관하게 항상 한 단계 이동입니다. 180ms를 넘기지 못했다면 500px을 당겼어도 한 단계입니다. 메뉴는 계속 당기거나 멈추는 동작에만 붙습니다.
  • 가로 제스처로 인식되면 크기와 상관없이 반드시 한 단계 이동하거나 메뉴를 엽니다. 아주 작은 가로 움직임도 무시하지 않으므로, 세로 스크롤 중 손가락이 옆으로 쏠리면 페이지가 넘어갈 수 있습니다.
  • 트랙패드와 Magic Mouse는 브라우저에서 모두 가로 wheel 이벤트로 전달되므로 완벽히 구분할 수 없습니다.
  • 브라우저가 웹페이지에 손가락 떼기 이벤트를 제공하지 않습니다. 튕겨서 관성이 생기면 그 시작을 손 뗀 시점으로 정확히 잡지만, 관성 없이 천천히 놓으면 마지막 입력 후 약 0.19초, 메뉴 내 선택은 약 0.3초가 지난 시점을 손을 뗀 것으로 처리합니다.
  • Chrome 151 이상에서 180ms의 40%를 넘게 당긴 뒤 관성 없이 아주 천천히 손을 떼면, 손을 뗐음에도 멈춘 것으로 보여 메뉴가 열립니다. 손가락이 닿아 있는지는 알 수 없기 때문에 남는 오차이며, 기다리는 시간을 0.70초로 늘리면 덜 걸립니다. 이때는 Esc나 바깥 클릭으로 닫으면 됩니다. 조금이라도 튕겨서 놓으면 관성이 생겨 정확히 한 단계 이동합니다.
  • 기록이 하나도 없는 탭에서는 길게 당겨도 메뉴가 뜨지 않습니다. 뒤로 제스처는 탭을 닫습니다.
  • 탭이 닫히므로 입력하던 폼 내용 같은 상태는 사라집니다. 세로 스크롤 중 손가락이 옆으로 쏠려 제스처가 잘못 잡히면, 뒤로 갈 기록이 없는 탭에서는 그대로 닫힙니다. Cmd+Shift+T로 되살릴 수 있지만 그 탭을 연 탭과의 관계는 복원되지 않습니다 — Chrome이 그 관계를 복원 데이터에 담지 않기 때문입니다. 페이지가 beforeunload로 확인창을 띄우면 탭은 그대로 남습니다.
  • 그 탭 안에서 페이지를 이동했거나 사이트가 pushState·리다이렉트로 기록을 남겼다면, 그 기록을 모두 되짚은 뒤에야 탭이 닫힙니다.
  • 확장 프로그램을 새로고침한 뒤 기존 탭에 남은 이전 콘텐츠 스크립트는 서비스 워커와 연결할 수 없습니다. 이때 한 단계 이동은 조용히 무시하며, 해당 탭을 새로고침하면 다시 동작합니다.
  • 웹사이트와 Chrome 버전에 따라 브라우저 자체 뒤로가기 애니메이션을 완전히 억제하지 못하는 경우가 있을 수 있습니다.

파일 구성

ChromeExtension/
├── manifest.json   # Manifest V3 설정과 권한
├── worker.js       # 탭 히스토리 조회 및 이동
├── _locales/       # en·ko·ja·zh_CN 문구 (사용자에게 보이는 것만)
├── shared/
│   ├── i18n.js              # 문구 조회와 data-i18n 치환
│   └── settings.js          # 팝업과 콘텐츠가 공유하는 설정 정의
├── content/
│   ├── menu-styles.js       # Shadow DOM 메뉴 스타일
│   ├── wheel-phase.js       # 손가락 입력과 관성 구간 판정
│   ├── page-styles.css      # Chrome 기본 가로 탐색 차단 스타일
│   ├── history-client.js    # 서비스 워커 메시지 통신
│   ├── history-menu.js      # 페이지 내 히스토리 메뉴 UI
│   ├── gesture-controller.js # 스와이프 상태와 제스처 처리
│   └── index.js             # 콘텐츠 기능 시작점
├── icons/          # 툴바·웹스토어 아이콘 (16·32·48·128)
├── store/          # 웹 스토어에 붙여 넣는 글
│   ├── listing-*.txt        # 등록 정보의 긴 설명 (en·ko·ja·zh_CN)
│   └── permissions-en.txt   # 개인정보 보호 관행 탭의 권한 사유
├── tools/
│   ├── build.js             # 웹 스토어 업로드용 zip 생성기 (npm run build)
│   └── make-icons.js        # 아이콘 PNG 생성기 (npm run icons)
├── popup.html      # 확장 아이콘 설정 화면
├── popup.css
├── popup.js
├── tests/
│   ├── worker.test.js          # 히스토리 처리 단위 테스트
│   ├── content-modules.test.js # 콘텐츠 모듈 조립 및 제스처 테스트
│   ├── gesture-scenarios.test.js # 실제 입력 모양으로 재현한 제스처 판정
│   ├── build.test.js           # 패키지에 담을 목록 검사
│   └── i18n.test.js            # 네 언어의 키 일치 검사
└── README.md

콘텐츠 스크립트는 Manifest에 선언된 순서로 같은 격리 환경에 로드되며, 별도의 빌드 과정 없이 바로 설치할 수 있습니다. 기본 설정값과 멈춤 시간 선택지, 제외한 사이트 목록을 다루는 함수는 shared/settings.js 한 곳에만 두고 팝업과 콘텐츠 스크립트가 함께 사용합니다. 설정에 저장되는 값도 밀리초 그대로라, 선택지를 바꾸려면 이 파일의 HOLD_STILL_CHOICES만 고치면 됩니다. 당김 판정 시간은 설정이 아니므로 content/gesture-controller.jsPULL_HOLD_MS에 있습니다.

전체 테스트는 프로젝트 폴더에서 아래처럼 실행할 수 있습니다.

npm test   # 또는 node --test

웹 스토어에 올릴 패키지는 아래처럼 만듭니다. 결과는 dist/better-gesture-<버전>.zip입니다.

npm run build
  • 담을 파일은 손으로 적지 않고 manifest에서 출발해 참조를 따라가며 모읍니다. HTML의 <script src>·<link href>와 서비스 워커의 importScripts까지 한 단계씩 끌어오고, _locales 아래는 통째로 담습니다. 목록을 손으로 관리하면 파일을 추가할 때마다 같이 고쳐야 하는데, 안 고쳐도 빌드는 성공하고 Chrome에서만 터집니다.
  • 만들기 전에 먼저 확인하고, 하나라도 걸리면 zip을 쓰지 않고 실패합니다. 빠진 파일, default_locale에 정의되지 않은 __MSG__ 키, manifest와 package.json의 버전 불일치, 그리고 저장소에는 있는데 어디에서도 참조되지 않는 런타임 파일입니다. 마지막 항목이 콘텐츠 스크립트를 만들고 manifest에 넣는 것을 잊은 경우를 잡습니다.
  • _favicon/*은 Chrome이 만들어 주는 경로라 저장소에 파일이 없습니다. 담을 대상에서 빼고 보고만 합니다.
  • 같은 입력이면 같은 바이트가 나옵니다. 파일 수정 시각을 넣지 않고 DOS 기준 시각으로 고정하기 때문입니다. zip은 외부 도구 없이 tools/build.js가 직접 씁니다.
  • tests/build.test.js가 같은 수집 함수를 그대로 불러 검사하므로, 목록이 어긋나면 배포 전에 npm test에서 먼저 걸립니다.

아이콘은 tools/make-icons.js가 외부 의존성 없이 PNG를 직접 래스터화합니다. 색·모서리 반경·화살표 비율을 바꾸려면 이 파일 상단의 상수를 고친 뒤 다시 생성하세요.

npm run icons

About

Navigate backward and forward through your tab history with intuitive Mac trackpad gestures.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages