chrome과 browser 네임스페이스

배경
chrome.*은 2009~2010년경 구글이 크롬 확장 프로그램 시스템을 만들면서 도입한 원조 네임스페이스입니다. 당시에는 브라우저 확장에 표준이라는 게 없었고, 각 브라우저가 완전히 다른 확장 모델(Firefox는 XUL, IE는 BHO/ActiveX)을 갖고 있었습니다.
크롬은 확장도 그냥 웹 기술(HTML/CSS/JS)로 만든다는 접근을 택했고, 그 위에 브라우저 기능에 접근하는 특권 API를 chrome이라는 전역 객체로 노출했습니다.
표준화 시도
추후에, Firefox가 2015년경 WebExtensions API를 도입하면서, browser.* 네임스페이스를 만들었습니다. Firefox는 기존의 XUL/XPCOM 기반 확장 시스템을 버리고 크롬 확장 모델을 받아들이기로 했는데, 그대로 복사하는 대신 두 가지를 개선했어요.
- 특정 브라우저 이름(
chrome)이 아니라 중립적인browser를 사용함 - 콜백 기반이었던
chromeapi와 달리 처음부터Promise기반으로 설계chrome.*은 원래 콜백 기반:chrome.tabs.query({}, (tabs) => {...})- 다만 이 차이도 이제 거의 사라졌습니다. Manifest V3 기준으로 크롬도 대부분의
chrome.*API에서 콜백을 생략하면Promise를 반환하도록 변경되었습니다. 그래서 요즘은await chrome.tabs.query({})가 크롬에서도 가능합니다.
- 다만 이 차이도 이제 거의 사라졌습니다. Manifest V3 기준으로 크롬도 대부분의
browser.*는 처음부터 Promise 기반:const tabs = await browser.tabs.query({})
현재 표준화 진행 정도
chrome의 browser 네임스페이스 제공
WECG에서 진행된 표준화 작업의 일환으로, Chrome 148부터 모든 확장 API가 browser 네임스페이스로도 제공됩니다. 별칭 수준이 아니라, browser과 chrome은 이제 완전히 같은 객체를 가리킵니다. 따라서,chrome.tabs === browser.tabs가 성립합니다.
크롬이 원래 갖고 있던 API 객체에 browser라는 이름표를 하나 더 붙인 겁니다. 자바스크립트 코드로 비유하면 아래와 비슷한 상황입니다.
const chrome = { tabs: {...}, runtime: {...} }; // 원래 있던 것
const browser = chrome; // 같은 객체에 별명 추가Firefox의 browser를 가져와서 심은 것이 아닌, 크롬의 자신의 API인 chrome.* API를 browser.* 이라는 이름으로 노출시킨 것입니다. 앞서 말했듯 콜백 함수 기반이었던 chrome.* API가 이제는 promise 문법으로 거의 대부분 지원됩니다.
따라서, Firefox, Safari뿐만 아니라 크롬도 모두 browser.*를 네이티브로 가지게 되었습니다. chrome.*도 계속 유지됩니다.
chrome은 Chrome 136에서 네임스페이스만 먼저 출시했던 적이 있었습니다. 하지만 버그가 있어 이때는 완벽하지 않았는데, Chrome 148부터 완전히 지원됩니다. 자세한 사항은 아래 내용에 정리했습니다.
비동기 promise 문법 지원
chrome.*은 MV3가 처음 출시된 Chrome 88(2021년 1월)부터, 점진적으로 Promise를 지원했습니다. 즉, 네임스페이스 통합(chrome 148) 때 한번에 전환이 이뤄진 것이 아닙니다. 아래와 같이 browser.*가 지원하는 Promise 문법을 지원하기 위해 전환을 해 나갔습니다.
// MV2 시절 스타일
chrome.tabs.query({ active: true }, (tabs) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
return;
}
// ...
});
// MV3 스타일
try {
const tabs = await chrome.tabs.query({ active: true });
} catch (err) {
// lastError 확인 대신 그냥 catch
}chrome.*은 콜백 방식과 promise 두 방식 모두 지원되는 방향으로 전환을 진행해왔기 때문에, 기존 콜백 코드도 깨지지 않습니다. 그런데 여기서 예외가 하나 있었습니다. chrome.runtime.onMessage.addListener 같은 일부 이벤트 리스너에서 응답을 다루는 부분이 Promise화가 제대로 되지 않아 버그가 발생했습니다.
브라우저 확장 프로그램에서, 각 구성요소의 스크립트는 각자 다른 환경에서 실행되기 때문에, 메세지를 주고 받아야 소통을 할 수 있습니다. 그때 사용하는 API 중 하나가 chrome.runtime.onMessage 입니다. 메세지를 주고 받을 때, 받자마자 바로 답할 수 있는 동기 응답의 경우는 아래와 같이 간단하게 처리할 수 있습니다.
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ answer: 42 }); // 리스너 안에서 즉시 응답
});비동기가 되는 순간 함정이 등장합니다. 예를 들어, 번역 확장이라면 background에서 번역 API를 fetch로 호출하고 그 결과를 돌려줘야겠죠.
// ❌ 조용히 실패하는 코드
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(`https://api.example.com/translate?q=${message.text}`)
.then(res => res.json())
// 메세지 채널이 이미 막혀버려, then 핸들러 안의 sendResponse가 실행되어도 메세지가 전달되지 못함.
.then(data => sendResponse({ translated: data.result }))
// 이벤트 핸들러 함수는 이미 return 되어 종료
});왜 실패할까요? 이벤트 핸들러 함수가 반환(return)되는 시점에 크롬은 응답할 일이 끝났다고 판단하고 메시지 채널을 닫아버립니다. fetch가 완료되어 sendResponse를 호출할 때는 이미 메세지를 전달할 채널이 존재하지 않습니다. 따라서 보낸 쪽에서는 응답이 undefined로 오게 됩니다. 이걸 막는 방법이 return true였습니다.
// ✅ 기존 패턴, MV2 시절부터 지금까지
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(`https://api.example.com/translate?q=${message.text}`)
.then(res => res.json())
.then(data => sendResponse({ translated: data.result }));
return true; // 나중에 sendResponse 부를 거니까 채널 열어둬라는 신호
});이벤트 핸들러가 true 를 반환하면, 메세지를 보내는 채널을 종료하지 않고, 유지합니다. 이런 우회 방법(Escape Hatch)이 있긴 했지만, 일반적인 문법이 아니기 때문에, 개발자는 실수하기 쉬웠습니다. 또한 async/await 문법으로 깔끔하게 처리하고 싶을 수 있는데, async 함수의 반환값은 항상 Promise이지 true가 아니기 때문에, 이런 우회 방법(Escape Hatch)은 작동하지 않습니다. 그래서 이 당시에는 onMessage 리스너에 async를 붙이지 마라가 정석이었습니다.
// ❌ 그럴듯해 보이지만 148 이전 크롬에서는 안 됨
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
const res = await fetch(...);
sendResponse({ translated: await res.json() });
return true; // 의미 없음!
});재미있는 건 Firefox의 확장 API인 browser.*는 처음부터 이벤트 핸들러가 Promise를 반환하면 resolve된 값이 그대로 응답으로 전달하여 잘 처리했습니다.return true와 같은 우회 방식이나 sendResponse와 같은 로직은 필됴하지 않았습니다.
크롬에서도 이 편한 방식을 쓰고 싶었던 개발자들은 Mozilla가 만든 webextension-polyfill이라는 라이브러리를 썼습니다. 폴리필이란 브라우저에 아직 없는 기능을 자바스크립트로 흉내 내서 채워주는 코드를 말합니다. 이 폴리필의 시작 부분에는 대략 이런 감지 로직이 있습니다.
// webextension-polyfill 내부 (개념적으로 단순화)
if (globalThis.browser?.runtime?.id) {
// browser가 이미 있음 → 브라우저가 네이티브로 제공하는 것
// (Firefox라고 가정) → 감싸지 않고 그대로 사용
module.exports = globalThis.browser;
} else {
// browser가 없음 → 크롬이구나 → chrome.*을 감싸서 browser를 만듦
module.exports = wrapChromeApis(chrome);
}크롬이라고 판단되면 폴리필은 사용자가 등록한 리스너를 한 번 감싸서, 리스너가 Promise를 반환하면 자기가 대신 return true를 하고 Promise가 resolve되면 sendResponse를 호출해줬습니다.
// 폴리필이 내부적으로 해주던 일 (개념 코드)
function wrapListener(userListener) {
return (message, sender, sendResponse) => {
const result = userListener(message, sender);
if (result instanceof Promise) {
result.then(sendResponse);
return true; // 폴리필이 대신 채널을 열어둠
}
};
}덕분에 폴리필 사용자들은 크롬에서도 Firefox 스타일로 편하게 코드를 써왔습니다. 하지만, webextension-polyfill의 브라우저 종류 판단 방식은 추후에 버그로 이어지게 됩니다.
구글은 크롬에도 browser 네임스페이스(browser.tabs처럼 API를 묶어 부르는 이름 공간)를 도입하기로 했고, Chrome 136에서 네임스페이스만 먼저 출시했습니다.
이제 크롬에 globalThis.browser가 생기게 되었습니다. 하지만 webextension-polyfill 폴리필에서, 브라우저 종류가 chrome인지 firefox인지를 browser 객체의 존재 여부로 판단했었습니다. 결국 “어, browser가 이미 있네? 그럼 브라우저가 다 알아서 하겠지" 하고 래핑을 건너뛰게됩니다. 하지만 여전히 Chrome 136 시점의 크롬 browser.runtime.onMessage는 Promise 반환 이벤트 핸들러를 지원하지 않았습니다.
browser라는 네임스페이스만 노출됏지 왔지 동작은 옛날 그대로였던 것입니다. 결과적으로 async (message) => { return data; } 스타일로 짜여 있던 수많은 확장에서, async 함수가 반환한 Promise를 크롬이 true를 반환하지 않은 리스너로 해석해 채널을 즉시 닫았고, 비동기 응답이 일제히 깨졌습니다. 구글은 이를 인지하고, 이 버전을 롤백했습니다.
교훈을 얻은 구글은 Chrome 148에서 네임스페이스와 네이티브 Promise 리스너 지원을 한 세트로 출시했습니다. 폴리필이 물러나도 바닥이 실제로 있도록요. 148부터는 runtime.onMessage 리스너가 Promise를 반환하면 그 resolve 값이 곧 응답이 됩니다. chrome.*으로 부르든 browser.*로 부르든 동일하게 동작합니다.
// 예전 방식, 여전히 동작함
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
translate(msg.text).then(sendResponse);
return true; // 채널 열어둬 신호
});
// Chrome 148+, async 함수 자체가 응답
chrome.runtime.onMessage.addListener(async (msg, sender) => {
return await translate(msg.text); // resolve 값이 sender에게 전달됨
});이제는 반환값이 Promise면 그 Promise가 끝날 때까지 채널을 유지하고, resolve되면 그 값을 보내고, reject되면(async 함수 안에서 throw하면) 보낸 쪽 sendMessage의 Promise가 함께 reject됩니다.
// content-script.js
const response = await browser.runtime.sendMessage({ text: selectedText });
console.log(response.translated);대신, async 함수인 이벤트 리스너의 반환 값의 resolve된 결과를 항상 반환하므로, 정확히 전달하고자 하는 바를 return 해야합니다.
만약 그냥 return 하게 되면, 그 Promise는 undefined로 resolve되고, sender에게는 null이 응답으로 갑니다. 이게 onMessage에 이벤트 핸들러가 여러 개여도 응답은 단 하나만 보낼 수 있다는 규칙과 결합되면 문제가 됩니다.
// 리스너 A, 이 메시지 타입은 내 담당이 아니라서 그냥 통과시킬 생각
chrome.runtime.onMessage.addListener(async (msg) => {
if (msg.type !== 'SAVE_HISTORY') return; // ← 함정!
await saveHistory(msg);
});
// 리스너 B, 실제로 응답해야 하는 쪽
chrome.runtime.onMessage.addListener(async (msg) => {
if (msg.type === 'TRANSLATE') return await translate(msg.text);
});리스너 A의 return;은 응답 안 함이 아니라 null로 응답함이 됩니다. A가 먼저 응답 슬롯을 차지해버리면 B의 진짜 응답이 무시될 수 있어요. 그래서 여러 메시지 타입을 여러 리스너로 나눠 처리하는 구조라면, 자기 담당이 아닌 메시지를 받는 리스너는 async 대신 동기 함수로 두고 undefined를 반환하거나(이게 응답하지 않겠다는 뜻입니다), 아예 리스너를 하나로 합쳐 내부에서 분기하는 게 안전합니다.
chrome.*와 browser.* 의 구조
chrome.* 크로미움이 만든 확장 API의 원본 네임스페이스로, 확장 프로그램 컨텍스트에서만 존재하는 전역 객체입니다. chrome.tabs(열려 있는 탭 목록 조회), chrome.storage, chrome.contextMenus(우클릭 메뉴 추가) 같은 건 일반 웹페이지가 절대 할 수 없는 일인데, 확장 프로그램에서 이러한 동작이 가능한 API를 제공해줍니다.
chrome.tabs // 탭 제어
chrome.storage // local / sync / session
chrome.runtime // 메시징, 생명주기, manifest
chrome.scripting // 스크립트 주입 (MV3)
chrome.action // 툴바 버튼 (MV3)
chrome.alarms // 스케줄링
chrome.contextMenus // 우클릭 메뉴
chrome.i18n // 다국어
chrome.webRequest // 네트워크 관찰
chrome.declarativeNetRequest // 선언적 요청 차단/수정 (MV3)
chrome.sidePanel // 사이드 패널 (크로미움 전용)
chrome.offscreen // 오프스크린 문서 (MV3, 크로미움 전용)핵심 API는 browser.*와 겹치지만, 마지막 두세 개처럼 크로미움에서 먼저(혹은 크로미움에만) 나오는 API들이 있습니다. 크롬이 확장 플랫폼의 진화를 주도하는 위치라서, 새 API는 대체로 chrome.*에 먼저 등장하고 다른 브라우저가 따라오는 패턴으로 주로 이뤄집니다.
크롬으로 연 일반 웹 페이지에도 chrome 객체가 있다
browser와 달리, 크롬에서는 일반 웹페이지에도 window.chrome이라는 객체가 존재합니다. 다만 이건 확장 API가 아니라 chrome.loadTimes() 같은 극소수 레거시 기능만 담긴 빈 껍데기에 가까워요. 이 때문에 브라우저가 크롬인지 감지하는 용도로 window.chrome 존재 여부를 체크하는 코드가 흔했습니다(봇 탐지에서도 씁니다). 확장 컨텍스트에서만 chrome.tabs 같은 실제 API가 채워지는 구조라고 이해하면 됩니다.
chrome.*와 마찬가지로 browser.* 역시 확장 프로그램 컨텍스트에서만 존재하는 전역 객체입니다. 기능별 하위 네임 스페이스는 아래와 같습니다.
browser.tabs // 탭 조회, 생성, 업데이트
browser.storage // local / sync / session 저장소
browser.runtime // 메시징, 확장 생명주기, manifest 접근
browser.scripting // 스크립트/CSS 주입 (MV3)
browser.action // 툴바 버튼 (MV3, MV2에서는 browserAction)
browser.i18n // 다국어 처리
browser.alarms // 주기적 작업
browser.contextMenus // 우클릭 메뉴
browser.webRequest // 네트워크 요청 관찰/수정사용할 수 있는 browser.* 와 chrome.* 범위
컨텍스트마다 쓸 수 있는 범위가 다릅니다. background script(서비스 워커)에서는 권한만 있으면 전부 접근 가능하고, popup이나 options 페이지도 마찬가지예요. 반면 content script는 웹페이지에 주입되는 특성상 runtime(메시징), storage, i18n 등 일부만 직접 쓸 수 있고, 나머지는 background에 메시지를 보내서 대신 실행해야 합니다. 이 제약은 chrome.*이든 browser.*든 동일합니다.
| 컨텍스트 | 쓸 수 있는 chrome.* |
|---|---|
| Background | 전부 (권한만 있으면) |
| Popup / Options | 사실상 전부 |
| Content script | runtime(메시지), storage, i18n 등 극소수 |
| 메인 월드 / 일반 페이지 | 없음 |
권한 명시
chrome.* 또는 browser.* 의 API를 그냥 쓸 수 있는 것은 아닙니다. 기본적으로 사용 가능한 API도 있지만, 강력한 API일수록 manifest에 권한 선언을 한 후 사용이 가능합니다. 만약 선언하지 않으면 해당 네임스페이스가 undefined이거나 호출이 거부됩니다. 또한 선언한 권한은 설치 시 사용자에게 고지되고 스토어 심사 기준이 되니, 필요한 최소만 선언하는 게 원칙입니다.
{
"permissions": ["storage", "alarms", "contextMenus"],
"host_permissions": ["https://api.mytranslator.com/*"]
}확장 자기 자신에 관한 작업이거나, 사용자 데이터에 닿지 않는 작업인 경우 주로 기본 API로 제공됩니다.
| API | 용도 |
|---|---|
chrome.runtime | • 확장을 구성하는 실행 컨텍스트들 간 메시징 (sendMessage/onMessage/connect)• 생명주기 이벤트( onInstalled), 확장 내 파일 URL 얻기(getURL) |
chrome.i18n | 확장 UI 다국어화 |
chrome.tabs 중 비민감 기능 | 탭 생성·이동, tabs.sendMessage, url 없이 하는 query |
chrome.windows | 브라우저 창 생성·조회 |
확장 프로그램은 하나의 프로그램처럼 보이지만 실제로는 서로 격리된 여러 실행 환경이 조합된 구조입니다:
background(서비스 워커) — 브라우저 수준에서 도는 확장의 두뇌. DOM이 없음content script— 웹페이지에 주입되어 그 페이지의 DOM을 만질 수 있는 코드popup/options/side panel— 확장이 띄우는 자체 UI 페이지들
이들은 각자 별도의 JavaScript 환경에서 돕니다. 전역 변수를 공유하지 않고, 서로의 함수를 직접 호출할 수도 없어요. content script에서 background.js에 있는 함수를 부르는 건 불가능합니다. 그래서 유일한 통로가 chrome.runtime의 메시징이 됩니다.
아래와 같이 컨텍스트들 사이의 통신은 chrome.runtime을 통해서 이뤄집니다.
// content script (페이지 세계) → background (확장 세계)
const res = await browser.runtime.sendMessage({ type: 'TRANSLATE', text: '...' });
// background에서 수신
browser.runtime.onMessage.addListener(async (message) => {
if (message.type === 'TRANSLATE') return await translate(message.text);
});각 구성요소에 대한 자세한 설명은 아래 블로그 글에서 정리했습니다 😊
Gyuri브라우저 확장 프로그램의 구성요소
권한 선언 후 사용할 수 있지만, 설치 시 사용자에게 경고하지 않습니다.
| API | 권한 | 용도 |
|---|---|---|
chrome.storage | "storage" | 확장 전용 저장소. local(영속)/session(워커 재시작 간 유지)/sync(계정 동기화) |
chrome.alarms | "alarms" | 워커가 죽어도 동작하는 예약 작업. setTimeout의 대체재 |
chrome.contextMenus | "contextMenus" | 우클릭 메뉴 항목 추가 |
chrome.offscreen | "offscreen" | 워커에서 못 하는 DOM 파싱·오디오용 보이지 않는 문서 |
chrome.activeTab | "activeTab" | 권한이라기보다 클릭한 그 탭, 그 순간만 접근권을 주는 특수 API |
여기서부터 사용자 데이터에 닿기 때문에 설치 화면에 경고가 뜨고, 심사도 까다로워집니다.
| API | 권한 | 용도 | 뜨는 경고의 성격 |
|---|---|---|---|
chrome.tabs의 민감 정보 | "tabs" | 탭의 url·title 읽기 | "브라우징 활동 읽기" |
chrome.scripting | "scripting" + host 권한 | 코드 동적 주입(executeScript) | host 범위에 따름 |
chrome.notifications | "notifications" | OS 알림 표시 | "알림 표시" |
chrome.declarativeNetRequest | "declarativeNetRequest" | 네트워크 요청 차단·수정 규칙 | "네트워크 트래픽" 계열 |
chrome.cookies | "cookies" + host 권한 | 쿠키 읽기·쓰기 | 강한 경고 |
chrome.history, chrome.bookmarks, chrome.downloads | 각 동명 권한 | 방문 기록·북마크·다운로드 | 각각 해당 데이터 접근 경고 |
chrome.action은 위 셋 어디에도 안 들어가는 네 번째 유형으로 "permissions" 배열이 아닌 manifest에 "action": { ... } 키를 사용해 별도 선언합니다.
chrome.action은 한 문장으로 하면 툴바에 있는 내 확장 버튼, 그 자체를 다루는 API입니다.
브라우저 툴바의 확장 아이콘 하나를 뜯어보면 이런 구성 요소를 갖고 있어요:
툴바 버튼 (= action)
├── 아이콘 (버튼에 그려지는 그림)
├── 툴팁 (title) (마우스 올리면 뜨는 텍스트)
├── 배지 (badge) (아이콘 위 모서리의 작은 숫자/글자, 예: 알림 개수)
├── 활성/비활성 상태 (회색으로 죽어있거나 살아있거나)
└── 클릭했을 때의 동작 ← 여기서 팝업 or onClicked 둘 중 하나chrome.action API는 이 각각을 조작하는 메서드 모음입니다. 아래 API를 통해 프로그래밍적으로 변경할 수 있습니다.
browser.action.setIcon(...) // 아이콘 바꾸기
browser.action.setTitle(...) // 툴팁 바꾸기
browser.action.setBadgeText(...) // 배지 텍스트
browser.action.enable() / disable() // 버튼 활성/비활성
browser.action.setPopup(...) // 클릭 시 열릴 팝업 지정/해제
browser.action.onClicked // (팝업이 없을 때) 클릭 이벤트보시면 popup은 이 목록의 한 줄일 뿐이에요. 버튼을 클릭하면 뭐가 일어나는가를 정하는 두 가지 선택지 중 하나가 팝업이고, 다른 하나가 onClicked 이벤트인 거죠.
"action": {
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png"
},
"default_title": "AI Translator — 클릭해서 번역 설정 열기",
"default_popup": "popup.html"
},default_icon— 툴바에 표시될 아이콘. 해상도별로 여러 장 주면 브라우저가 알아서 고릅니다default_title— 마우스를 올렸을 때 뜨는 툴팁default_popup— 버튼 클릭 시 열릴 HTML
위 manifest처럼 default_popup을 지정하면, 버튼 클릭의 동작은 팝업 열기로 고정됩니다. popup.html은 평범한 HTML이고, 그 안의 JS는 확장 API를 쓸 수 있는 확장 컨텍스트입니다.
"action": {
"default_icon": { "16": "icons/icon-16.png" },
"default_title": "이 페이지 번역"
}// background.js
browser.action.onClicked.addListener(async (tab) => {
// 클릭된 시점의 활성 탭 정보가 인자로 들어옴
await browser.tabs.sendMessage(tab.id, { type: 'TRANSLATE_PAGE' });
});default_popup을 빼면 클릭이 action.onClicked 이벤트로 background에 전달됩니다. ‘버튼 누르면 현재 페이지를 즉시 번역’ 같은 원클릭 동작을 만들 때 이 패턴을 사용할 수 있습니다.
주의할 게 하나 있는데, default_popup이 설정되어 있으면 onClicked는 background.js에서 절대 실행되지 않습니다. popup.js가 클릭을 처리하기 때문입니다.
위 기능도 아래와 같이 manifest에 별도로 선언해야지 사용할 수 있습니다.
omnibox는 주소창을 내 확장의 입력창으로 빌려 쓰는 기능입니다.commands는 확장에 전역 키보드 단축키를 등록하는 API입니다. 페이지 어디에 포커스가 있든, 어떤 사이트를 보고 있든 동작하는 브라우저 수준의 단축키입니다.- 키 이름(
translate-selection)은 내가 정하는 명령 식별자입니다. 이벤트에서 이 문자열로 어떤 단축키가 눌렸는지 구합니다. suggested_key는 이름 그대로 "제안"입니다. 강제가 아닙니다. 사용자가 이미 다른 확장이 같은 조합을 선점했으면 내 단축키는 조용히 할당되지 않고, 사용자는chrome://extensions/shortcuts에서 언제든 재배치할 수 있습니다. 그래서 단축키가 반드시 있다고 가정하는 설계는 위험합니다.- 플랫폼별 분기(
default,mac,windows,linux,chromeos)가 가능합니다. 맥에서는Command조합을 주는 게 관례고요. description은 단축키 설정 페이지에 표시됩니다.
"commands": { "translate-selection": { "suggested_key": { "default": "Ctrl+Shift+T", "mac": "Command+Shift+T" }, "description": "선택한 텍스트 번역" } }, "omnibox": { "keyword": "tr" }- 키 이름(