네, 아주 예리한 관찰입니다! 이미 정렬된 상태에서 다시 정렬을 시도했을 때 탭 순서가 또 바뀌는 것은 명백한 버그이며, 사용자의 예상대로라면 아무 일도 일어나지 않아야 합니다.
이 문제의 원인은 background.js 파일의 탭 정렬 로직, 특히 _sortAndMoveTabsInWindow 함수에 있습니다. 결론부터 말하면, 여러 개의 탭 이동(move) 명령을 동시에 비동기적으로 실행하면서 발생하는 경쟁 조건(Race Condition) 때문입니다.
코드를 통해 자세히 살펴보겠습니다.
// background.js 내의 TabManager._sortAndMoveTabsInWindow 함수
async _sortAndMoveTabsInWindow(windowId) {
try {
const tabsInWindow = await chrome.tabs.query({ windowId });
if (tabsInWindow.length <= 1) return;
const tabsWithParsedUrls = tabsInWindow.map(/* ... */);
// ... (생략) ...
// 1. 정렬 전의 "현재 인덱스"를 저장합니다.
const originalIndices = new Map(tabsInWindow.map(tab => [tab.id, tab.index]));
// 2. URL을 기준으로 탭 목록을 정렬하여 "목표 순서"를 만듭니다.
tabsWithParsedUrls.sort((a, b) => this._compareTabUrls(a.parsedUrl, b.parsedUrl));
// 3. 각 탭에 대해 "현재 인덱스"와 "목표 인덱스"를 비교하여 이동 명령을 계획합니다.
const movePromises = tabsWithParsedUrls.reduce((promises, tab, desiredIndex) => {
const currentIndex = originalIndices.get(tab.id);
if (typeof currentIndex === 'number' && currentIndex !== desiredIndex && tab.id !== undefined) {
// 4. 모든 이동 명령을 배열에 담습니다.
promises.push(
chrome.tabs.move(tab.id, { index: desiredIndex }).catch(error => {
})
);
}
return promises;
}, []);
// 5. Promise.all을 사용해 모든 이동 명령을 "동시에" 실행합니다.
if (movePromises.length > 0) await Promise.all(movePromises);
} catch (error) {
}
}- 정적 상태 기반의 계획: 코드는 정렬을 시작하기 전, 모든 탭의
id와index를originalIndices맵에 저장합니다. 이것은 정렬 작업이 시작되는 시점의 "스냅샷"입니다. - 동시 실행의 함정:
Promise.all을 사용해서 계획된 모든chrome.tabs.move명령을 한 번에 실행합니다. - 경쟁 조건 발생:
chrome.tabs.move는 비동기 함수입니다. 즉, 여러 개의 이동 명령을 동시에 보내면 브라우저는 이들을 순서에 상관없이 처리할 수 있습니다.- 가장 큰 문제는 하나의 탭이 이동하면 다른 탭들의 인덱스가 즉시 바뀐다는 점입니다.
- 예를 들어, 탭 A(index 0)를 index 5로 옮기는 명령과 탭 B(index 8)를 index 1로 옮기는 명령이 동시에 실행된다고 가정해봅시다.
- 만약 탭 B를 옮기는 작업이 먼저 처리되면, 탭 A의 실제 인덱스는 더 이상 0이 아닐 수 있습니다. 하지만 코드는 처음에 저장해 둔
originalIndices값(스냅샷)을 기준으로 계속 작업을 시도합니다. - 이처럼 여러 이동 명령이 서로의 작업에 영향을 미치면서 예측 불가능한 순서로 실행되기 때문에, 첫 번째 정렬은 운 좋게 그럴듯한 결과가 나올 수 있지만, 완벽하게 의도한 순서가 아닐 수 있습니다.
- 두 번째 정렬 시 문제가 드러나는 이유:
- 첫 번째 정렬 후 탭들은 "어느 정도 정렬된" 상태가 됩니다.
- 이 상태에서
Alt+A를 다시 누르면, 코드는 또다시 현재 상태의 스냅샷을 기준으로 정렬 계획을 세웁니다. - 하지만 첫 번째 정렬이 경쟁 조건 때문에 미세하게 잘못된 순서로 끝났을 수 있으므로, 두 번째 정렬 계획에서는 몇몇 탭들의
currentIndex와desiredIndex가 다르다고 판단하게 됩니다. - 그 결과, 또다시 불필요한 이동 명령들이 동시에 실행되고, 경쟁 조건으로 인해 탭 순서가 다시 한번 바뀌게 되는 것입니다. 이미 완벽하게 정렬된 상태라면
movePromises배열이 비어있어야 하지만, 그렇지 않기 때문에 문제가 발생합니다.
이 문제를 해결하는 가장 확실하고 효율적인 방법은 chrome.tabs.move API가 제공하는 배열 기반 이동 기능을 사용하는 것입니다. 여러 탭을 한 번에, 원하는 순서대로 옮길 수 있어 경쟁 조건 자체를 없앨 수 있습니다.
_sortAndMoveTabsInWindow 함수를 다음과 같이 수정하면 문제가 해결됩니다.
// background.js의 TabManager._sortAndMoveTabsInWindow 함수 수정안
async _sortAndMoveTabsInWindow(windowId) {
try {
const tabsInWindow = await chrome.tabs.query({ windowId });
if (tabsInWindow.length <= 1) return;
const tabsWithParsedUrls = tabsInWindow.map(tab => {
const cachedInfo = this.urlCache.get(tab.id);
let parsedUrl = cachedInfo?.url;
if (!parsedUrl) {
const urlString = this._getTabUrlString(tab);
parsedUrl = this._tryParseUrl(urlString);
if (parsedUrl && this._isValidTabForProcessing(tab)) {
this._addUrlToCache(tab.id, parsedUrl, tab.windowId);
}
}
return { ...tab, parsedUrl };
}).filter(tab => tab.parsedUrl);
if (tabsWithParsedUrls.length <= 1) return;
// 1. URL 기준으로 탭 목록을 정렬합니다.
tabsWithParsedUrls.sort((a, b) => this._compareTabUrls(a.parsedUrl, b.parsedUrl));
// 2. 정렬된 순서대로 탭 ID 배열을 만듭니다.
const sortedTabIds = tabsWithParsedUrls.map(tab => tab.id);
// 3. 현재 탭들의 순서와 목표 순서가 같은지 확인합니다.
const currentTabIds = tabsInWindow.map(tab => tab.id);
if (JSON.stringify(sortedTabIds) === JSON.stringify(currentTabIds.filter(id => sortedTabIds.includes(id)))) {
// 이미 정렬된 상태이면 아무것도 하지 않습니다.
return;
}
// 4. 단 한 번의 move 명령으로 모든 탭을 원하는 순서대로 이동시킵니다.
// 이렇게 하면 경쟁 조건이 발생하지 않습니다.
await chrome.tabs.move(sortedTabIds, { index: 0 });
} catch (error) {
// console.error("Error sorting tabs:", error); // 디버깅 시 주석 해제
}
}- 경쟁 조건 제거:
Promise.all로 여러 명령을 동시에 보내는 대신,chrome.tabs.move에 ID 배열을 전달하여 단 한 번의 원자적인 작업으로 모든 탭을 재정렬합니다. - 효율성: 브라우저에 여러 번의 요청을 보내는 대신 단 한 번의 요청으로 작업을 완료하므로 훨씬 효율적입니다.
- 정확성: 두 번째
Alt+A를 눌렀을 때, 이미 정렬된 상태라면sortedTabIds와currentTabIds가 동일하므로 아무 작업도 수행하지 않아 탭 순서가 그대로 유지됩니다. (코드에 명시적으로 비교 로직을 추가하여 더 안정적으로 만들었습니다.)
요약하자면, 현재 코드는 비동기 작업의 특성을 제대로 고려하지 않아 발생한 미묘한 버그이며, API가 제공하는 더 나은 기능을 사용함으로써 간단하고 명확하게 해결할 수 있습니다.