# 99줄 코드로 Onetab 충돌 및 원본 데이터 백업 해결

> 서문: 대학 시절, Chrome과 거의 같은 시기에 Onetab을 알게 되었습니다; 하지만 학생으로서 인터넷 서핑에는 별다른 부담이 없어 사용을 거의 하지 않았습니다. 취업 후에는 브라우저에 100개의 탭을 여는 것이 일상다반사가 되었고, 상사/동료가 자료를 묻는 일도 자주 발생했습니다. 이때 Onetab은 웹 페이지 기록장으로서 Ctrl+f로 키워드를 찾는 것과 함께 간단하고 사용하기 쉬웠습니다.…

- 게시일: 2022-07-27
- 태그: Chrome, 엣지, nodejs, onetab, 브라우저, 난제
- HTML 버전 보기: https://bilibili.cat/ko/posts/onetabrestore
- 中文에서 AI 번역: https://bilibili.cat/posts/onetabrestore

---

## 서문

대학 시절, Chrome과 거의 같은 시기에 Onetab을 알게 되었습니다; 하지만 학생으로서 인터넷 서핑에는 별다른 부담이 없어 사용을 거의 하지 않았습니다.  
취업 후에는 브라우저에 100개의 탭을 여는 것이 일상다반사가 되었고, 상사/동료가 자료를 묻는 일도 자주 발생했습니다. 이때 Onetab은 웹 페이지 기록장으로서 Ctrl+f로 키워드를 찾는 것과 함께 간단하고 사용하기 쉬웠습니다.

### **왜 즐겨찾기를 사용하지 않습니까?**

*필자가 즐겨찾기를 사용한 최종 결과는 북마크 바를 펼치면 화면 전체를 가리는 것이었습니다.*

### **왜 방문 기록을 사용하지 않습니까?**

*방문 기록에는 다른 무관한 정보도 남아 있고, Chrome의 방문 기록은 신뢰할 수 없습니다.*

### **왜 다른 지식 관리 도구를 사용하지 않습니까?**

1.  *노트류 앱은 너무 무겁습니다*
2.  *onetab류 앱은 기존 사용 습관에 익숙해진 탓에 충분히 가볍고 더 효율적인 대체품을 찾지 못했습니다*
3.  *onetab은 다크 모드를 업데이트했고, 앞/뒤/현재 탭 캐시가 정말 실용적입니다*

## 상황

닷새 전, Windows 업데이트 후 재부팅하고 edge를 다시 열었더니 onetab이 계속 loading 상태에 있었고, 얼마 후 페이지가 충돌하여 Out of Memory 오류가 발생했습니다.

-   필자는 2020년 초 새로운 Edge가 출시된 때부터 onetab을 계속 사용했으며, 6만 개의 웹 페이지가 기록되어 있고 시스템을 재설치한 적이 없습니다.

<figure class="missing-asset"><figcaption>이미지 「Crash」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.<br><code>/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727131653.png</code></figcaption></figure>

### 세 가지 시도

1.  모든 웹 페이지와 확장 프로그램을 닫고 edge를 다시 시작한 뒤 onetab을 다시 열었습니다.  
    <ins><em>실패했고, Out of Memory 오류가 발생했습니다.</em></ins>
2.  컴퓨터를 재부팅하고 백그라운드가 비어 있는 상태에서 edge를 다시 시작한 뒤 onetab을 다시 열었습니다.  
    <ins><em>실패했고, Out of Memory 오류가 발생했습니다.</em></ins>
3.  "C:\\User" 폴더(Windows의 사용자 데이터 디렉터리)를 완전히 복사하여 새로 Windows10을 설치한 PC에 복원했습니다.  
    <ins><em>실패했고, Out of Memory 오류가 발생했습니다.</em></ins>

이 시점에서 기본적으로 데이터 양이 너무 많아 확장 프로그램이 충돌한 것이라고 단정할 수 있었습니다.

-   사실 이전에 5만 건의 데이터일 때도 여는 것이 매우 느려 30초 이상 걸렸습니다.
-   다만 메모리 괴물 Chrome이 자신이 관리하는 메모리 사용량에도 제한을 두었을 줄은 몰랐습니다.

![Chrome meme](https://i.pinimg.com/originals/95/11/f8/9511f847798db5ec08a51a3fa597d4c5.jpg)

## 목표

이제 문제는 찾았고, 다행히 데이터는 유실되지 않았습니다. 완전성은 알 수 없습니다.  
2년 넘게 축적된 데이터는 일상 사용 빈도도 매우 높기 때문에 반드시 복원해야 합니다.

1.  가능한 한 Onetab의 데이터를 복구하고, 기계와 사람이 인식할 수 있는 데이터 형식으로 내보냅니다
2.  1.을 바탕으로 Onetab을 완전히 백업하는 자동화 도구를 구현합니다  
    ***Onetab에 내장된 내보내기는 순수 txt 파일이라 수동으로 복사해야 하고, tab 그룹 생성 시간 필드가 빠져 있습니다***
3.  Onetab 호환 App을 구현하여 데이터 검색 기능을 강화하고, 1.에서 내보낸 데이터 형식과 Ontab의 기존 데이터 형식에 호환되도록 합니다

## 조치

원인과 목표가 이미 있으므로 구현하는 일도 더 이상 무작정 헤매지 않아도 되었습니다.

### 1\. Google에서 선행 사례 검색

"onetab data location"을 검색했습니다.  
곧 onetab, 아니면 chromium 계열 브라우저 확장 프로그램의 데이터 저장 위치를 찾았습니다  

<figure class="missing-asset"><figcaption>이미지 「QQ图片20220727175536」은 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.<br><code>/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727175536.png</code></figcaption></figure>

  
다시 "onetab data export"를 검색하자, 또 다른 선행 개발자의 오픈 소스 방안을 찾았습니다. Go 기반이었습니다.  
[![jianyuan/onetab-export-to-json](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/jianyuan/onetab-export-to-json)](https://github.com/jianyuan/onetab-export-to-json)  
원래는 여기서 끝나고 CV의 길로 들어서는 줄 알았지만, issue에 더 이상 작동하지 않는다는 기록이 있었고 필자도 Go 디버깅 경험이 없었습니다. 코드를 보니 100줄이 채 되지 않아 직접 한 번 구현하기로 했습니다.

### 2\. leveldb 알아보기

[![google/leveldb](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/google/leveldb)](https://github.com/google/leveldb)

-   Google이 오픈 소스로 공개한 NoSQL 데이터베이스
-   단일 스레드, 고성능, 로컬 인스턴스만 존재
-   실행 과정에서 메모리를 대량으로 사용해 가속합니다

#### 표준 levedb 데이터베이스 인스턴스는 폴더 형태입니다

<figure class="missing-asset"><figcaption>이미지 「leveldb实例」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.<br><code>/upload/2022/07/QQ%E5%9B%BE%E7%89%8720220727190212.png</code></figcaption></figure>

-   LOCK 데이터베이스 잠금
-   MANIFEST 헤더 파일
-   \*.ldb 주 데이터 파일

오픈 소스 방안의 소개에서 알 수 있듯이, onetab은 leveldb 방안을 사용해 사용자 데이터를 저장합니다.  
필자도 Chrome 확장 프로그램을 개발한 적이 있는데, 브라우저에서 실행되는 확장 프로그램은 파일 읽기/쓰기나 tcp 연결 같은 능력이 없기 때문에 localStorge와 indexDB 두 가지 API만 사용해 로컬화할 수 있었다는 기억이 납니다.  
그중 localStorge는 최대 5M라서 5만 건의 비교적 긴 JSON 기록은 아무리 해도 저장할 수 없습니다. onetab은 indexDB를 사용해 데이터를 저장해야 마땅한데, 이 leveldb는 대체 무슨 상황인지, 게다가 기여자도 Google입니다.  
검색을 해보니 leveldb가 바로 indexDB의 하위 구현이며, indexDB는 실제로 상위 API라는 것을 알게 되었습니다 (이름이 오해를 부르는 시리즈).

> Google Chrome의 Local Storage는 SQLite 형식(.localstorage 파일)으로 폴더에 일부 데이터를 저장합니다.  
> IndexedDB 폴더에 저장되는 일부 다른 데이터(각 프로필별)는 LevelDB 형식을 사용합니다. 이는 Google이 개발한 오픈 소스 키-값 저장 형식이며 GitHub에 호스팅됩니다.  
> Chrome 외부에서 LevelDB 형식의 파일을 수정하는 것은 간단한 과정이 아닙니다. Chrome의 Indexed DB leveldb 인스턴스를 확인하려면 호환되는 비교기를 구현해야 하기 때문입니다.

### 3\. leveldb의 GUI 도구

사정을 파악한 후, 먼저 ~확(tou)인(lan)~ 데이터 상황을 확인할 목적으로 필자는 leveldb의 GUI 도구를 찾아보았습니다.  
주류인 Navicat과 DataGrip은 당연히 지원하지 않을 것이므로, 계속 GitHub을 뒤졌습니다.

[![heapwolf/levelui](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/heapwolf/levelui)](https://github.com/heapwolf/levelui)

#### 하나의 electron GUI 구현, 컴파일 실패

[![klausgao/electron-linvodb-manager](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/klausgao/electron-linvodb-manager)](https://github.com/klausgao/electron-linvodb-manager)

#### 또 다른 electron GUI 구현, 데이터를 읽지 못함

[![fastogt/fastonosql](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/fastogt/fastonosql)](https://github.com/fastogt/fastonosql)

#### 하나의 C++/Qt GUI 구현, 데이터 읽기 성공

<figure class="missing-asset"><figcaption>이미지 「软件截图」는 유실되었습니다: 원본 사이트와 인터넷 아카이브 모두에서 찾지 못했습니다.<br><code>/upload/2022/07/QQ%E6%88%AA%E5%9B%BE20220727183055.png</code></figcaption></figure>

  
또한 onetab이 사용하는 키 이름을 명확히 볼 수 있습니다.

-   *참고: 이 소프트웨어는 GPL v3를 기반으로 오픈 소스이지만, 바이너리 버전은 유료이며 신규 사용자는 체험할 수 있습니다. 필자가 이 글을 작성할 당시 공식 웹사이트가 다운되어 등록할 수 없었으므로, [구버전 다운로드](https://wwc.lanzouf.com/idgFq08i16va)를 통해 실행할 수 있으며 등록과 로그인이 필요 없습니다.*

### 4\. 코드 구현, node.js 16.15 기반

앞의 두 electron GUI 구현은 성과를 얻지 못했지만, 그들이 의존하는 level 패키지가 매우 편리한 API를 제공하며 바로 사용할 수 있다는 것도 발견했습니다.

-   *여기서 필자는 처음에 python으로 구현하려 했지만, python의 level 의존성은 linux에서 컴파일 설치해야 하므로 그만두었습니다.*

```shell
$ npm install level
$ npm install mongodb
```

```JavaScript
const {promises: {writeFile, mkdir}} = requir('fs');
const {Level} = requir('level')
const MongoClient = requir('mongodb').MongoClient
// halo有点bug, 出现require会崩溃

const USER_HOME = (process.env.HOME || process.env.USERPROFILE).replaceAll("\\", "/")

const chrome = {
    name: "Google/Chrome",
    uuid: "chphlpgkkbolifaimnlloiipkdnihall"
}
const edge = {
    name: "Microsoft/Edge", // Edge || Edge Beta || Edge Dev || Edge Canary
    uuid: "hoimpamkkoehapgenciaoajfkfkpgfop"
}
// const firefox = "" // 火狐的扩展和目录结构与以上两位好兄弟不太一样, 无使用经验, 请自行修改

const winPath = `${USER_HOME}/AppData/Local/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`
// 以下两条为 Copilot 智能添加的代码, 请自行核对路径是否正确
// const macPath = `${USER_HOME}/Library/Application Support/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`
// const linuxPath = `${USER_HOME}/AppData/Local/${edge.name}/User Data/Default/Local Extension Settings/${edge.uuid}`

const ldb = new Level(winPath)
const url = "mongodb://alpha:123456aB@localhost:27017/?authSource=admin"

// 异步读取onetab的leveldb中的数据
const getOnetabData = async () => {
    await ldb.open()
    const str = await ldb.get('state')
    return JSON.parse(JSON.parse(str))
}

// 异步处理数据, 获得一个每条记录的数组
const getItems = async (onetabs) => {
    const items = []
    onetabs.forEach((ele, index) => {
        console.log(ele.id)
        ele.tabsMeta.map(ele => items.push(ele))
    })
    return items
}

// 异步连接mongo
const getMongoDB = async () => {
    const db = await MongoClient.connect(url)
    return await db
}

// 异步写入tab组
const insertGroups = async (db, onetabs) => {
    const dbo = await db.db("onetab")
    await dbo.collection("onetab_groups").insertMany(onetabs)
}

// 异步写入每条onetab记录
const insertItems = async (db, items) => {
    const dbo = db.db("onetab")
    await dbo.collection("onetab_items").insertMany(items)
}

// 生成用于备份的json数据文件和方便查看的CSV文件
const genExtFiles = async (items, onetabs) => {
    /*
     * 生成表头，\ufeff 是防止乱码
     * csv中以 `,` 换列，`\n`换行
     */
    let title = Object.keys(items[1])
    let csvContent = '\ufeff' + title.join(',') + '\n'

    // 添加表体
    items.forEach((item, index) => {
        let c = Object.values(item).join(',') + '\n'
        csvContent += c
    })
    // 生成文件夹
    await mkdir('onetab')

    // 生成csv文件
    await writeFile('./onetab/tab_items.csv', csvContent)

    // 生成JSON
    await writeFile('./onetab/tab_ori.json', JSON.stringify(onetabs))
    console.log('数据文件生成完毕, 请打开当前目录下的onetab文件夹查看.')
}

// 主函数
const main = async () => {
    const json = await getOnetabData()
    const db = await getMongoDB()
    const items = await getItems(json.tabGroups)
    await insertItems(db, items)
    await insertGroups(db, json.tabGroups)
    await genExtFiles(items, json)

    await db.close()
    console.log("写入MongoDB完成, 程序执行完毕. ")
}

main()
```

## 결과

마지막으로 필자는 최근 매우 인기 있는 Tauri를 기반으로 바이너리 데스크톱 애플리케이션을 패키징했습니다.

> 아직 Tauri를 모르십니까?  
> 여기를 보십시오 [Electron을 버리고 Rust 기반으로 개발된 Tauri를 받아들이십시오](https://segmentfault.com/a/1190000041440043)

### 다음 기능을 구현했습니다

-   Onetab의 기존 스타일로 내보낸 모든 데이터를 조회합니다
-   대량 데이터에 대응하기 위해 페이지네이션을 추가했습니다
-   날짜 및 키워드 필터를 추가했습니다

### 곧 추가할 예정

-   rust로 자동 백업을 구현하여 별도로 node를 설치할 필요가 없습니다
-   클라우드 동기화 webDAV 또는 기타
-   데이터 분석, 워드 클라우드, NLP

### 저장소 주소

[![endcloud/Onetab Re](https://opengraph.githubassets.com/e99b6d5fccc2c9ee0d3a4d515f8b337bd98f88bbb64bf7cb3102d0ec121f84f5/endcloud/onetab_restore_tauri)](https://github.com/endcloud/onetab_restore_tauri)
