Chrome에 Arc식 탭 전환을 구현한 과정

26. 10. 26. (14시간 전)

TL;DR: Arc에서 익숙해진 탭 전환을 Chrome에서도 쓰려고 개인용 도구를 만들었어요. 확장 프로그램만으로 처리할 수 없던 키 입력 때문에 macOS 앱을 더하고, 빠른 입력과 탭 전환 응답이 엇갈리는 문제를 풀어간 과정이에요.

Chrome으로 돌아오면서 가장 아쉬웠던 건 탭 전환이었어요. 같은 Ctrl+Tab을 눌러도 Chrome은 탭이 놓인 순서대로, Arc는 최근에 본 탭 순서대로 이동해요. 개발 문서와 참고 자료를 번갈아 볼 때, Arc에서는 단축키 한 번으로 직전 탭에 돌아갈 수 있었어요. Chrome에서는 그 탭이 있는 위치까지 단축키를 여러 번 눌러 찾아가야 했고요.

브라우저를 옮겨도 이 조작만큼은 그대로 쓰고 싶었어요. 그래서 Chrome에서도 최근에 본 탭으로 바로 돌아가는 도구를 만들었어요. 키 입력은 macOS 보조 앱에 맡겼어요. 짧은 입력에는 전환창을 띄우지 않도록 했어요. 연속 입력에서는 마지막 선택을 유지하고, 캡처한 이미지가 다른 탭에 연결되지 않도록 검사했어요.

직접 만든 도구로 탭을 전환하는 모습
  • 짧게 눌렀다 놓으면 전환창 없이 직전 탭으로 돌아가요. 백그라운드로 새 탭을 연 직후에는 그 탭으로 이동해요.
  • 길게 누르면 전환창이 열려요. Control을 누른 채 Tab으로 후보를 고르는 동안, 뒤의 실제 페이지는 바뀌지 않아요.
  • Control을 놓으면 선택한 탭으로 전환하고, Esc를 누르면 취소해요.

Chrome으로 돌아간 이유

Arc를 떠나기로 한 계기는 공식 지원 종료 공지였어요. macOS와 Windows 버전은 2026년 12월 22일에 종료될 예정이에요.

Arc에서는 한 창 안에서도 Spaces로 업무용·개인용 작업을 나눌 수 있었어요. 두 페이지를 한 창에 나란히 놓고 보는 분할 보기도 편했고요.

Arc의 사용 경험을 최대한 유지하고 싶다면 Zen Browser라는 선택지도 있었어요. 다만 Firefox의 Gecko 엔진을 기반으로 만든 브라우저라, 일부 웹에서 레이아웃이 깨지거나 기능이 동작하지 않는 호환성 문제를 겪게 될까 걱정됐어요. 그래서 익숙한 Chromium 기반의 Chrome으로 돌아가기로 했어요.

Chrome도 세로 탭을 공식 지원하기 시작했어요. 써보니 탭 목록과 그룹을 옆에 놓는 기능에 가까웠어요. Spaces로 작업 환경을 나누고, 키보드로 제어하고, 작업 상태를 복구하던 Arc와는 차이가 있었어요. 분할 보기도 Mac에서는 ⌘+Option+N으로 시작할 수 있었고요.

macOS와 Windows 지원 종료를 알리는 Arc 공식 공지
Arc의 공식 지원 종료 안내.

같은 단축키를 위한 역할 분담

웹스토어에서 최근에 본 탭(MRU) 순서로 전환하는 확장 프로그램 2~3개를 찾아 써봤어요. 전환 자체가 매끄럽지 않거나, Control을 누른 채 Tab으로 후보를 고르고 Control을 놓아 확정하는 동작이 어색했어요.

전환창이 열리고 닫힐 때마다 재생되는 애니메이션도 답답했어요. 탭을 수시로 오가다 보니, 매번 효과가 끝나기를 기다리는 느낌이 들었거든요. 자주 쓰는 조작일수록 애니메이션을 줄이거나 생략하고, 입력에 바로 반응해야 한다고 생각해요. 여러 경험을 통해 쌓아 온 제 애니메이션 원칙이에요.

Ctrl+Q 같은 새 단축키를 익히기보다는 손에 익은 Ctrl+Tab을 그대로 쓰고 싶었어요. 그래서 평소 Arc에서 쓰던 방식에 맞춰 직접 만들기로 했어요.

전환 후보는 현재 창의 탭 중 최대 다섯 개로 제한했어요. 다른 창이나 프로필의 탭은 섞지 않고, Control을 누른 동안에는 목록 순서를 고정했어요. 탭을 고르는 도중 순서가 바뀌면 같은 횟수로 Tab을 눌러도 다른 탭에 도착할 수 있으니까요.

최근 탭을 찾고 활성화하는 일은 확장 프로그램만으로 할 수 있었어요. Ctrl+Tab 입력과 Control을 놓는 순간을 모두 감지해야 했어요. Chrome의 Commands API는 단축키로 Tab을 지원하지 않고, 키를 놓는 이벤트도 제공하지 않아요.

웹페이지 안에서 키 입력을 받고 전환창을 띄우는 방식도 생각했어요. 하지만 주소창이나 Chrome 내부 페이지에서는 같은 방식으로 동작하게 만들기 어려웠어요. 어느 페이지를 보고 있든 익숙한 단축키 하나로 전환하려면 입력과 전환창을 웹페이지 밖에서 처리해야 했어요.

방식감수해야 하는 제약
확장 프로그램만 사용원하는 단축키와 Control 해제 시점을 처리하기 어려워요
보조 앱 + 페이지 안의 전환창주소창·Chrome 내부 페이지에서 같은 전환창을 쓰기 어려워요
보조 앱 + 네이티브 전환창 + 확장 프로그램보조 앱 설치와 macOS 권한 설정이 추가돼요

키 입력과 전환창은 Swift/AppKit 앱에 맡겼어요. 탭 정보 조회와 활성화는 TypeScript로 작성한 Manifest V3 확장 프로그램에서 처리하도록 나눴어요. 앱은 CGEventTap으로 입력을 받고, Chrome의 포커스를 가져오지 않는 NSPanel로 후보를 보여줘요. 선택한 탭은 확장 프로그램이 tabs.update로 활성화해요. 간단한 탭 카드 UI라 별도의 웹 런타임을 더하지 않고 AppKit으로 직접 그렸어요.

macOS 앱이 키 입력과 전환창을 처리하고, 프로필별 Native Host가 Unix socket과 Native Messaging으로 Chrome 확장 프로그램의 탭 정보·활성화 요청을 중계하는 구조
프로필별 연결은 Native Host가 중계하고, 입력과 전환창은 하나의 앱이 맡아요.

확장 프로그램과 앱 사이는 Native Messaging으로 연결했어요. 확장 프로그램은 connectNative()로 중계 프로세스인 Native Host와 연결을 유지해요. Native Host는 사용자 전용 Unix socket으로 상주 앱에 메시지를 전달해요. 프로필이 늘어도 키 입력을 감시하는 앱과 전환창이 중복되지 않게 했어요.

확장 프로그램과 앱을 설치하면서 macOS 접근성 권한도 허용해야 했어요. 개발용 서명이 바뀌면 이 권한을 다시 등록해야 했고, 앱을 직접 종료해도 Native Host가 다시 실행하는 버그도 만났어요. 연결 복구와 직접 종료를 구분하도록 종료 상태를 기록하고, 앱을 직접 다시 열었을 때만 해제하게 고쳤어요.

탭 전환을 다듬으며 만난 문제

새로 연 탭으로 바로 이동하지 못하는 문제

링크를 백그라운드 새 탭으로 열었을 때는, 그 탭을 Ctrl+Tab으로 바로 보고 싶었어요. 최근 방문 순서만 따르면 아직 보지 않은 새 탭보다 직전에 보던 탭이 먼저 선택돼요.

백그라운드에서 새 탭이 생성되면 현재 탭 바로 다음에 넣도록 바꿨어요. 문서 A에서 B를 새 탭으로 열면, B를 직접 클릭하지 않아도 Ctrl+Tab으로 B에 갈 수 있어요. 다시 누르면 A로 돌아와요.

여러 개를 열면 가장 나중에 연 탭부터 후보에 넣었어요. 제목이나 로딩 상태가 바뀌는 이벤트에서는 순서를 바꾸지 않았어요.

짧은 입력에서도 전환창이 나타나는 문제

두 탭을 빠르게 오갈 때마다 전환창이 나타나는 건 불필요했어요. 후보를 고를 때만 전환창을 보여주고 싶어서, 창을 여는 시점과 탭을 전환하는 시점을 분리했어요.

첫 입력을 받으면 100ms 뒤에 전환창을 열도록 예약해요. 짧게 누르는 동작을 구분하면서도 후보를 빠르게 보여주려면 100ms가 적당하다고 판단했어요. 이 시간이 지나기 전에 Control을 놓으면 표시 예약을 취소해요. Control을 누른 채 Tab을 한 번 더 누르면 후보를 고르려는 동작으로 보고 대기 없이 전환창을 보여줘요.

탭 전환 요청은 표시 예약과 별도로 처리해요. Control을 놓으면 표시 대기와 관계없이 선택한 탭으로 전환을 요청해요. 조작마다 식별자를 붙여, 끝난 조작의 타이머가 뒤늦게 전환창을 띄우지 못하게 했어요. Control을 놓거나 Esc를 누르면 열린 전환창도 바로 닫아요.

전환 응답보다 다음 입력이 먼저 들어오는 문제

문서 A에서 참고 자료 B로 전환을 요청한 직후, 다시 A로 돌아가려고 단축키를 눌렀어요. Chrome API가 B 활성화 요청에 응답하기 전이었어요.

응답을 받은 뒤에만 최근 탭 순서를 바꾸면 두 번째 입력도 A를 기준으로 해석해서 B를 고르게 돼요. 그렇다고 모든 전환 요청을 쌓아두면 키를 놓은 뒤에도 탭이 뒤늦게 바뀔 수 있고요.

다음 입력에 사용할 최근 탭 순서와 Chrome에서 확인한 상태를 분리했어요. 앱은 Control을 놓으면 선택한 탭을 최근 탭 순서의 맨 앞으로 옮겨요. Chrome의 응답을 기다리지 않고 이 순서를 다음 입력에 사용해요. 활성화 요청은 한 번에 하나만 보내요. 요청을 처리하는 동안 선택이 바뀌면 대기 목적지를 마지막 선택으로 교체해요.

B 활성화 응답을 기다리는 동안 A, B, A 입력이 이어져도 다음 입력의 기준이 되는 최근 탭 순서는 매번 갱신해요. 대기 목적지는 마지막 A만 남기고 B 응답 뒤에 요청해요.
전환 요청을 처리하는 동안, 대기 중인 목적지는 마지막 선택으로 바꿔요.

오래된 연결에서 온 응답이 새 선택을 덮어쓰는 일도 막아야 했어요. 응답의 연결 세대와 요청 순번을 현재 요청과 대조했어요. 활성화 직전에는 대상 탭이 현재 창에 남아 있는지도 확인하고, 다른 창이나 앱으로 이동했다면 전환을 취소해요.

다른 탭의 화면이 미리보기에 붙는 문제

전환창에는 탭 제목과 함께 웹페이지의 축소 이미지인 탭 미리보기를 보여줘요. 탭 미리보기를 만들려고 배경 탭을 잠깐 활성화하면, 사용자가 고르는 도중 뒤의 페이지까지 바뀌게 돼요. 그래서 현재 보고 있는 탭만 비동기로 캡처해두고, 전환창에서는 마지막으로 저장한 이미지를 쓰기로 했어요.

A를 캡처하는 동안 B로 이동할 수 있었어요. 완료 시점의 활성 탭만 보고 이미지를 저장하면 A의 화면을 B의 미리보기로 잘못 연결할 위험이 있어요.

캡처를 시작할 때의 상태를 저장하고, 캡처와 이미지 축소가 끝날 때마다 같은지 다시 확인했어요. 탭 ID가 같아도 페이지는 바뀔 수 있어요. 이미지 키에 연결·창·탭과 함께 페이지 이동을 구분하는 탐색 세대도 넣었어요. 상태가 달라진 결과는 버리고, 저장된 이미지가 없거나 캡처할 수 없는 페이지에서는 제목과 아이콘만 보여줘요.

캡처가 끝난 뒤와 이미지 축소가 끝난 뒤에 시작 상태와 대조해요. 연결·창·탭·탐색 세대가 같을 때만 미리보기를 저장하고, 달라졌다면 결과를 버려요.
캡처 도중 탭이나 페이지가 바뀌면 결과를 버려요.

사용 결과와 측정의 한계

제 맥북에서 빠른 왕복과 후보 선택·취소, 전체화면·다중 모니터 표시, 탭 이동·삭제와 연결 복구를 확인했어요. 지금은 이 도구로 탭을 전환하고 있어요.

Chrome API별 응답 시간을 측정했어요. 탭 정보와 포커스를 확인한 뒤 활성화하는 과정에 포커스 조회가 두 번 있었어요. 첫 조회 뒤에도 비동기 작업이 더 실행되니, 그 결과로는 활성화 직전의 포커스를 보장할 수 없었어요. 마지막 확인을 남기고 중복 조회 한 번을 제거해 API 왕복을 네 번에서 세 번으로 줄였어요.

Control 해제 → Chrome API 응답변경 전변경 후
중앙값24.2ms20.8ms
p95140.3ms59.7ms

같은 Mac에서 초당 4~5회 정도 왕복하며 기록한 응답 75개와 129개를 비교했어요. 일부 응답 기록이 빠졌고 입력 속도와 부하도 달라, 수치 차이를 모두 코드 수정의 효과로 볼 수는 없어요.

지금은 탭 전환을 기다린다는 느낌 없이 쓰고 있어요.