regex-page-search

웹페이지를 정규식으로 검색하고 강조하는 Chrome 확장(Manifest V3)이다. 엔진은 JS RegExp이고, 부족해지면 Onigmo(WASM)로 바꿀 수 있게 엔진을 한 곳에 모아 두었다.

설치

Chrome 웹 스토어 심사를 통과하면 이 자리에 스토어 링크를 단다. 그 전에는 아래처럼 개발판으로 설치한다.

개발판

  1. Chrome 주소창에 chrome://extensions를 입력한다.
  2. 오른쪽 위 《개발자 모드》를 켠다.
  3. 《압축해제된 확장 프로그램을 로드합니다》를 누르고 이 폴더를 고른다.
  4. 검색할 페이지에서 툴바 아이콘을 누르거나 Alt+Shift+F를 누른다.

쓰는 법

동작 키
검색 / 다음 일치 Enter 또는 ▼
이전 일치 Shift+Enter 또는 ▲
강조 지우기 《지우기》 (《이동해도 계속 검색》도 함께 끈다)
즐겨찾기 저장/빼기 입력창 옆 ☆
저장한 패턴 쓰기 팝업 아래 《즐겨찾기》·《최근 사용》 목록에서 누르기

설계 — 왜 이렇게 했나

「줄」은 화면에 보이는 줄이다

웹페이지의 텍스트 노드를 그대로 이으면 두 가지가 틀어진다. <p> 두 개는 줄바꿈 없이 한 줄로 붙고, HTML 소스의 줄바꿈은 화면상 한 줄인 문단을 여러 줄로 쪼갠다. 그래서 src/core.js의 linearize는 브라우저가 화면에 그리는 방식을 따라 텍스트를 잇는다.

이어 붙인 문자열의 글자마다 「어느 텍스트 노드의 몇 번째 글자인가」를 기록해 둔다(segs[].offs). 일치 위치를 DOM Range로 되돌릴 때 이 표를 쓰고, 가상 글자는 범위의 앞뒤에서 잘라 낸다.

오프셋은 UTF-16 하나뿐이다

브라우저는 페이지가 UTF-8이든 EUC-KR이든 DOM을 만들 때 이미 디코딩한다. 그래서 확장이 보는 문자열·RegExp 결과·Range 오프셋은 모두 UTF-16 코드 단위이고, 인코딩 변환이 끼어들 자리가 없다. Onigmo로 바꿀 때도 ONIG_ENCODING_UTF16_LE로 빌드하면 이 성질이 유지된다. UTF-8로 빌드하면 바이트 오프셋을 변환해야 하므로 그렇게 하지 않는다.

유니코드 모드는 끌 수 없다

확장 B 이후 한자(𠀀 U+20000 등)는 UTF-16 두 단위(서로게이트 쌍)다. 유니코드 모드가 없으면 .이 반쪽 글자와 일치하고, [𠀀-𪛟] 같은 범위식은 문법 오류가 난다. 빈 일치 뒤에 다음 위치로 넘어갈 때도 코드 포인트 단위로 움직여야 서로게이트 중간에서 멈추지 않는다(matchAll이 그렇게 한다).

정규화하지 않는다

페이지 텍스트도 검색어도 NFC·NFD로 바꾸지 않는다. NFC는 CJK 호환 한자(U+F900–FAFF)를 통합 한자로 바꾼다. 예를 들어 독음 구분용 樂(U+F914)이 樂(U+6A02)이 되는데, 한국 자료에서는 이 구분이 의미를 가질 때가 많다. 정규화하면 글자 수도 바뀌어 오프셋 표가 하나 더 필요하다.

그 대가로, 호환 한자로 적힌 페이지를 통합 한자로 검색하면 걸리지 않는다. NFD로 적힌 한글도 완성형 검색어로는 걸리지 않는다. 둘을 같이 찾아야 할 일이 생기면 「접어 찾기」 옵션을 따로 넣는다.

강조는 DOM을 바꾸지 않는다

<mark>를 끼워 넣지 않고 CSS Custom Highlight API(CSS.highlights)로 칠한다. 페이지 구조와 그 페이지의 스크립트를 건드리지 않고, 여러 노드에 걸친 일치도 Range 하나로 칠할 수 있다.

속성 이름은 Onigmo 식으로 받아 JS 식으로 바꾼다

JS는 문자 체계 속성을 \p{Script=Hangul}처럼 Script=를 붙이고 대소문자까지 정확히 써야 한다. \p{Hangul}·\p{han}은 문법 오류다. EmEditor에서 쓰던 \p{hangul}+\(\p{han}+\)가 그대로 오류가 난 것이 계기다(2026-10-16, 계약 C-003). 그래서 compile 앞에 translateProps를 두어 이름을 바꾼다.

낡은 결과는 다시 검색해서 버린다

주소만 바뀌고 문서는 그대로인 사이트(SPA)나 내용을 통째로 갈아 끼우는 페이지에서는, 칠해 둔 범위가 이미 사라진 글자를 가리킨다. 처음 판에서는 팝업이 이 낡은 상태를 보고 《이미 검색했다》고 여겨 Enter를 새 검색 대신 낡은 결과 사이 이동으로 처리했다. 그래서 옵션을 껐다 켜야 검색되는 버그가 있었다(2026-10-16 사용자 지적).

엔진 교체 지점

엔진이 하는 일은 compile(패턴 → 검색기)과 findAll(문자열 → [시작, 끝) 목록, UTF-16) 두 함수뿐이다. Onigmo로 바꿀 때는 이 둘만 WASM 호출로 바꾸면 된다. 기본 동작이 다르다는 점에 주의한다. Ruby/Onigmo는 ^$가 항상 줄 단위이고, .이 줄바꿈과 일치하게 하는 플래그가 m이다(JS는 s).

화면 문구는 _locales에 두고, 한국어는 코드에도 남긴다

스토어 등록정보와 화면을 한국어·영어 둘로 낸다(브라우저 언어가 한국어면 한국어, 그 밖에는 영어 — default_locale: en). 문구는 RPSt("열쇠", "한국어", …)로 부른다(src/i18n.js). 확장 안에서는 chrome.i18n이 _locales/<언어>/messages.json의 번역을 주고, 번역이 없으면 둘째 인자의 한국어를 그대로 쓴다.

한국어를 코드에도 남기는 까닭은 core.js가 Node 시험에서 chrome 없이 돌기 때문이다. 시험은 지금처럼 한국어 문구를 본다. 그 대가로 같은 한국어가 두 곳에 있게 되므로, test/i18n.test.js가 코드·popup.html의 한국어와 _locales/ko를 한 글자씩 대조한다. en에 빠진 열쇠가 있는지도 같이 본다.

배포 zip 은 실행 파일만 담는다

scripts/build-zip.mjs의 RUNTIME 목록에 든 파일만 zip에 넣는다. E2E도 같은 목록으로 확장 사본을 만든다. 시험 사본과 스토어 판의 파일 구성이 어긋나면 「시험은 통과했는데 스토어 판은 깨지는」 일이 생기기 때문이다. zip 안의 시각은 1980-01-01로 고정했다. 내용이 같으면 같은 zip이 나온다.

웹 스토어 발행

  1. package.json과 manifest.json의 판 번호를 같이 올린다(C-005가 대조한다).
  2. npm test, node scripts/e2e-capture.mjs(필요하면 RPS_LANG=en으로도 돌린다).
  3. node scripts/build-zip.mjs → dist/regex-page-search-<판>.zip
  4. 화면이 바뀌었으면 node scripts/store-assets.mjs → store/assets/(스크린숏 1280×800, 홍보 타일 440×280)
  5. 개발자 대시보드에 zip을 올리고, 등록 문안은 store/listing.md에서 붙여 넣는다. 개인정보처리방침은 PRIVACY.md이고, GitHub Pages에서 PRIVACY.html로 열린다.
  6. 아이콘을 다시 그릴 때만 py -3 scripts/make-icons.py를 돌린다(Pillow 필요).

알려진 한계

검증

MIT 라이선스(LICENSE).