티스토리 에디터에서 실시간으로 글자수를 카운팅해주는 크롬 확장 프로그램을 제작했습니다.
이번 포스팅은 이 글자수세기 크롬 확장 프로그램에 대한 테크 리뷰입니다. 제가 직접 개발하면서 부딪혔던 고민과 해결 과정들을 공유하려 합니다. 직접 크롬 확장 프로그램을 만들어보고 싶거나, 티스토리 에디터의 텍스트를 추출하는 원리가 궁금하신 분들에게 도움이 되기를 바랍니다.
이 프로그램의 다운로드 및 설치 방법과 기능 소개가 궁금하다면 아래 링크를 클릭하세요.
[IT & 프로그래밍] - [티스토리 꿀팁] 에디터 화면에서 글자수세기 - 크롬 확장 프로그램 무료 배포 & 설치 방법
[티스토리 꿀팁] 에디터 화면에서 글자수세기 - 크롬 확장 프로그램 무료 배포 & 설치 방법
더 이상 글자수세기 사이트에 들락날락하며 일일이 복사/붙여넣기할 필요가 없습니다! 구글 애드센스 승인(일명 애드고시) 승인을 위해서는 최소 2000자 이상 글을 써야 한다는 이야기, 귀에 못
swan-lake.tistory.com
1. 코드 다운로드 방법
코드 설명에 앞서 먼저 시원하게 전체 코드부터 공개하겠습니다.
본인의 입맛에 맞게 위젯의 색상이나 기능을 수정하고 싶으신 분들은 코드를 다운로드하셔서 사용하시면 됩니다.
상업적으로 이용하거나, 처음부터 자기가 만든 것처럼 배포하지만 않으면 됩니다.
이 코드를 사용하실 때나 이 프로그램을 공유하실 때에는 반드시 이 글의 링크를 남겨주세요.
이 포스팅의 상단에 있는 `[티스토리 꿀팁] 에디터 화면에서 글자수세기 - 크롬 확장 프로그램 무료 배포 & 설치 방법` 포스팅 링크를 타면 제 이전 포스팅으로 바로 이동합니다. 거기에 다운로드 파일이 있습니다.
제 크롬 확장 프로그램은 3개의 파일로 구성됩니다. `manifest.json`, `style.css`, `content.js`입니다.
1-1. manifest.json (확장 프로그램 기본 설정)
`manifest.json`은 크롬 브라우저에게 이 프로그램의 이름, 버전, 설명, 작동할 사이트를 알려주는 역할을 합니다. 비유하자면 여권 같은 녀석입니다. 또한, 확장 프로그램의 아이콘도 `manifest.json`에서 설정합니다.
1-2. style.css (UI 디자인)
`style.css`는 확장 프로그램의 디자인과 레이아웃을 담당합니다. 프로그램의 외관을 담당하는 스타일리스트라고 볼 수 있습니다.
제 코드에는 위젯이 화면에 둥둥 떠다니도록 만드는 기능, 뒤쪽 글씨가 은은하게 비치는 글래스모피즘 효과, 그리고 드래그 튕김을 방지하는 `#drag-overlay`가 포함되어 있습니다.
1-3. content.js (핵심 동작 로직)
`content.js`는 확장 프로그램의 핵심 기능들의 동작 로직이 담긴 자바스크립트 파일입니다.
제 코드에는 티스토리 에디터에서 텍스트를 추출하고, 위젯을 드래그하며, 글자 수를 실시간으로 계산해서 화면에 뿌려주는 등의 기능이 구현되어 있습니다.
2. 코드의 핵심 로직 분석
이 확장 프로그램을 개발하면서 고민을 했던 부분이자 코드의 핵심 로직 4가지를 설명해 드리겠습니다.
2-1. 티스토리 에디터의 텍스트를 가져오는 방법 (textarea와 iframe)
처음 이 프로그램을 기획할 때만 해도 저는 아주 오만(?) 했습니다. "글자 수 세기? 그냥 `document.querySelector('textarea').value`로 텍스트 긁어오면 끝나는 거 아니야?"라고 생각했죠. 그래서 그렇게 코드를 짜고 돌려봤는데, 아무리 글자를 작성해도 글자 수는 0에서 멈춰있었습니다.
에디터 창에서 F12를 눌러서 보니까 `mce-`로 시작하는 클래스가 많았습니다. 티스토리는 TinyMCE 웹 에디터를 커스텀해서 쓰는 것 같다고 판단이 섰습니다. 이런 고급 에디터들은 굵기, 색상, 사진 첨부 등 복잡한 서식을 화면에 실시간으로 보여주기 위해서 독특한 방식을 씁니다. (이런 식으로 사용자가 에디터 화면에서 보는 그대로 발행되는 방식을 WYSIWYG(What You See Is What You Get)이라고 합니다.)
- 실제 보관함 `<textarea>`: `<textarea>`는 우리가 쓴 글을 저장해두고 서버로 전송할 때만 쓰려고 숨겨둔 보관함입니다. 에디터가 로딩되면 TinyMCE는 textarea에 `display: none`스타일을 적용하여 화면에서 숨깁니다.
- 우리가 보고 편집하는 공간 `<iframe>`: 메인 웹페이지 안에 또 다른 웹페이지(iframe)를 생성하여, 메인 페이지의 CSS로부터 독립된 환경을 제공합니다. 우리가 실제로 타자를 치고 글자를 꾸미고 하면서 포스팅을 편집하는 공간이 여기입니다.
이것들이 실제 티스토리 에디터의 기본모드에서 글쓰기를 할 때 어떻게 작동하냐면
- 초기화: TinyMCE가 실행되면 `<textarea>`에 들어있던 내용을 꺼내와서 `<iframe>`에 렌더링합니다.
- 편집: 사용자가 `<iframe>` 영역에서 타이핑하면 굵기, 색상, 이미지 등이 에디터 뒷면에서 뒤에서 HTML 코드로 실시간으로 변환됩니다.
- 동기화: TinyMCE는 변환된 HTML 코드를 가져와서 `<textarea>`에 넣습니다. 발행할 때 티스토리 서버로 넘어가는 것은 HTML 코드입니다.
그러니까 티스토리 에디터 기본모드에서 우리는 `<iframe>`을 수정하고 있는 것입니다.
에디터를 HTML 모드로 바꾸면 `<textarea>` 내부의 내용을 직접 보고 수정할 수 있는 것이고요.
저는 HTML이 저장되는 숨겨진 공간인 `<textarea>`에 대고 "글자 수 내놔!"라고 하고 있었으니 대답이 돌아오지 않고 있었던 겁니다. 일반 모드에서 우리가 쓴 오리지널 텍스트는 `<iframe>`에 있으니까요.
일반적인 DOM 접근법으로는 `<iframe>` 안을 들여다보기 어렵습니다. 그래서 저는 `contentDocument`를 통해 뚫고 들어가는 방식으로 코드를 작성했습니다. 아래는 그 부분을 담은 코드 일부입니다.
// 1. textarea는 무시하고, iframe을 찾는다
const iframe = document.querySelector('iframe.txc-layout') || document.querySelector('iframe');
// 2. iframe이 존재하고, 그 내부 문서(contentDocument)에 접근 가능하다면
if (iframe && iframe.contentDocument) {
// 내부 문서의 body 안에 있는 '순수 텍스트(innerText)'만 가져옴
let text = iframe.contentDocument.body.innerText || "";
// 3. 엔터 문자 제거한 text
const cleanText = text.replace(/[\r\n]/g, "");
// 4. 스페이스바까지 전부 제거한 text
const lengthWithoutSpace = cleanText.replace(/\s/g, "").length;
}
2-2. 숫자 증가 애니메이션 (requestAnimationFrame)
사실 애니메이션이 없어도 기능상으로는 아무 문제도 없습니다. 중요한 건 글자수를 세는 기능이고 예쁘게 보이는 건 부수적인 기능일 뿐이니까요. 하지만 대량의 텍스트를 붙여넣을 때 숫자가 너무 뚝뚝 끊겨서 올라가는 것을 보니까 좀 답답하더라고요. 그래서 숫자가 자동차 계기판처럼 촤르륵 올라가는 애니메이션을 넣기로 했습니다. 1에서 바로 100으로 바뀌는 것이 아니라, 1, 2, 3, ..., 100 이런 식으로 올라가게 하자는 것이죠.
처음에는 `setInterval`을 쓰려고 했습니다. 그런데 이건 무조건 0.1초마다 실행하라는 명령이라서 애니메이션이 뚝뚝 끊겨 보이거나 단조로워 보일 수 있습니다. 그리고 브라우저가 다른 탭에 가려져 있을 때도 백그라운드에서 연산이 계속 돌아가기 때문에 CPU를 미세하게나마 계속 갉아먹는 문제도 있습니다.
그래서 `requestAnimationFrame`을 썼습니다. `requestAnimationFrame`은 브라우저에게 어떤 애니메이션을 수행할 것인지 미리 알리고, 다음 화면을 그리기 직전에 미리 계산해 놓으라고 부탁하는 역할을 합니다. 사용자의 모니터 주사율에 동기화되기 때문에 부드럽고, 사용자가 다른 탭으로 이동하면 자동으로 연산을 멈춥니다.
아래는 제 프로그램에 쓰인 `requestAnimationFrame` 코드입니다.
// 애니메이션 함수
function animateNumber(element, start, end, duration) {
// 시작과 끝이 같으면 굳이 애니메이션을 돌릴 필요가 없음 (CPU 낭비 방지)
if (start === end) return;
let startTime = null;
const step = (currentTime) => {
if (!startTime) startTime = currentTime;
// 진행률 계산 (0에서 시작해 1이 되면 끝)
const progress = Math.min((currentTime - startTime) / duration, 1);
// Ease-out 공식 (목표에 다가갈수록 부드럽게 감속)
const easeProgress = 1 - Math.pow(1 - progress, 3);
// 화면에 숫자 찍기 (소수점은 버림)
element.innerText = Math.floor(easeProgress * (end - start) + start);
// 아직 목표에 도달하지 않았다면, 다음 프레임에 또 실행
if (progress < 1) {
window.requestAnimationFrame(step);
} else {
element.innerText = end; // 마지막엔 정확한 목표 숫자로 끝남
}
};
window.requestAnimationFrame(step);
}
코드의 소소한 디테일 2가지를 설명드리겠습니다.
- 코드 중간에 있는 Ease-out 공식(`1 - Math.pow(1 - progress, 3)`): Cubic Ease-out 효과를 수식으로 구현해서 넣은 것입니다. 자동차가 멈출 때 브레이크를 밟으며 스무스하게 감속하듯 목표 숫자에 다가갈수록 속도가 확 줄어들며 멈춥니다. 만약 이 공식을 빼버리면 숫자가 처음부터 끝까지 똑같은 속도로 올라갑니다.
- 버림하는 `Math.floor()`: 애니메이션으로 숫자를 증가시킬 때 30.5 같은 소수점이 위젯에 나오면 대참사겠죠. 그런 일을 방지하기 위해 숫자를 버림해줍니다.
제가 설정해 둔 애니메이션 속도가 마음에 안 드신다면, 코드를 수정해서 여러분이 직접 커스텀해보세요.
2-3. 마우스 드래그 끊김 방지 (iframe 이벤트 스틸 방지, 오버레이)
테스트 도중, 위젯을 마우스로 잡고 엄청 빠르게 움직여보니까 위젯이 마우스 포인터의 속도를 못 따라가서 갑자기 드래그가 툭 끊겨버리는 일이 발생했습니다. 특히 에디터 영역을 지날 때는 거의 확실히 끊기더라고요.
처음에는 제가 좌표 계산 코드(`clientX`, `clientY`) 로직을 잘못 짠 줄 알았습니다. 그런데 범인은 따로 있었습니다. iframe이 또! 문제였습니다. iframe이 이벤트 스틸(Event Stealing)을 하고 있었기 때문입니다.
웹 브라우저에서 마우스 드래그를 구현할 때는 보통 전체 화면(`document`)에 `mousemove`이벤트를 걸어둡니다. 마우스가 움직일 때마다 위젯도 따라오게 해 달라과 명령을 내리는 것이죠.
그런데 마우스 포인터가 에디터(iframe) 영역 위로 올라가는 순간 대참사가 발생합니다. 앞서 2-1에서 살펴봤다시피, iframe은 메인 페이지(에디터 화면)와는 독립된 웹페이지입니다. 그래서 마우스가 iframe 위로 넘어가는 순간, 메인 페이지는 마우스가 어디 있는지 위치를 잃어버리고 혼란스러워합니다. 이런 식으로 iframe이 마우스 이벤트를 먹어치워 버리는 문제가 바로 Event Stealing입니다.
이 문제를 해결하기 위해 다소 고전적이지만 100% 확실한 꼼수를 좀 썼습니다. 드래그를 시작하는 순간, 에디터(iframe)와 마우스 사이에 투명 유리판을 끼워 넣는 것입니다. 그 투명 유리판이 바로 오버레이입니다.
아래가 오버레이를 구현한 코드입니다.
/* style.css에 있는 코드입니다. */
#drag-overlay {
position: fixed; /* 화면 전체 고정 */
top: 0; left: 0;
width: 100vw; height: 100vh;
z-index: 999998; /* 위젯(999999)보다 아래, iframe보다는 위에 깔리게끔 999998로 설정 */
display: none; /* 오버레이는 안 보여야 함 */
cursor: grabbing;
}
// content.js에 있는 코드입니다.
// 드래그 시작 (마우스 누름)
widget.addEventListener('mousedown', (e) => {
// ... (좌표 계산 생략) ...
isDragging = true;
overlay.style.display = 'block'; // 드래그 시작 시 오버레이 적용
});
// 드래그 종료 (마우스 뗌)
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
overlay.style.display = 'none'; // 드래그 종료 시 오버레이 해제
}
});
위젯을 마우스로 누르는(`mousedown`) 순간, `z-index`가 아주 높은 투명한 div(`drag-overlay`)가 화면 전체를 덮어버립니다. 이제 사용자가 마우스를 아무리 빠르게 움직여도 마우스 포인터는 iframe에 닿지 못하고 메인 페이지 소속인 오버레이 위에서만 움직이게 됩니다. 마우스를 떼는(`mouseup`) 순간 오버레이를 치워버리면 본문에 글을 쓰는 데도 전혀 지장이 없죠.
2-4. 껐다 켜도 브라우저가 위젯의 위치를 기억하게 만들기 (LocalStorage)
글자수세기 위젯을 내 눈에 제일 편한 우측 상단에 놔두고, 목표 글자 수도 내 기준에 맞춰서 변경해 두었습니다. 그런데 다음 날 새 글을 쓰려고 에디터를 열었더니, 위젯이 다시 초기 자리로 돌아가있고 목표 숫자가 처음 설치했을 때의 값으로 초기화되어 있다면 어떨까요. 다시 설정하기 매우 귀찮고, 당연히 UX(User eXperience)는 최악일 겁니다.
이런 상황을 막기 위해 브라우저가 가진 고유의 기억력인 `LocalStorage`를 사용합니다. 로컬 스토리지는 사용자가 브라우저 캐시를 지우지 않는 이상 데이터를 반영구적으로 보관해주는 브라우저 내장 DB입니다.
// 1. 값 저장하기
// 위젯 드래그가 끝났을 때 (mouseup)
localStorage.setItem('tistory_widget_x', widget.getBoundingClientRect().left);
localStorage.setItem('tistory_widget_y', widget.getBoundingClientRect().top);
// 목표 글자 수를 변경했을 때 (input)
localStorage.setItem('tistory_widget_target', e.target.value);
위의 코드를 통해 위젯을 마우스로 끌어다 놓을 때, 목표 글자 수 입력창의 숫자를 바꿀 때 `localStorage.setItem`을 호출하여 현재의 X/Y 좌표와 목표값을 로컬 스토리지에 집어넣습니다.
// 2. 값 불러오기 (에디터를 새로 켤 때마다)
const savedX = localStorage.getItem('tistory_widget_x');
const savedY = localStorage.getItem('tistory_widget_y');
const savedTarget = localStorage.getItem('tistory_widget_target');
// 저장된 좌표가 있다면 그 위치로 위젯을 강제 이동
if (savedX !== null && savedY !== null) {
widget.style.left = `${savedX}px`;
widget.style.top = `${savedY}px`;
widget.style.right = 'auto'; // 초기 CSS의 right 속성 무력화
}
// 저장된 목표 글자 수가 있다면 입력창에 세팅
if (savedTarget !== null) {
document.getElementById('target-input').value = savedTarget;
}
위의 코드를 통해 사용자가 티스토리 에디터에 접속해서 확장 프로그램이 실행될 때마다, 가장 먼저 `localStorage.getItem`을 통해 스토리지에 무엇이 저장되어 있는지 가져옵니다. 만약 저장된 위치와 목표값이 있다면 초기 세팅을 무시하고 사용자가 마지막에 남겨둔 그 상태 그대로 위젯을 렌더링합니다.
이건 단순한 기능 같지만, 실제로 제가 사용해 보니까 이런 디테일 하나 덕분에 사용하기 정말 편리했습니다.
3. 글을 마무리하며
글자수세기 크롬 확장 프로그램은 단순히 "글 쓰면서 글자수세기 사이트 왔다갔다 하기 귀찮다!"는 가벼운 불만에서 시작한 토이 프로젝트였습니다. 하지만 막상 직접 코드를 작성해보니까 생각보다 생각해볼 게 많았습니다. DOM 구조 분석, `iframe`, 렌더링 최적화(`requestAnimationFrame`), 이벤트 스틸 방어(`Overlay`), 그리고 로컬 스토리지(`LocalStorage`)까지 알차게 공부할 수 있었습니다.
내가 겪은 일상의 작은 불편함을 내가 짠 코드 몇 줄로 해결해내는 경험만큼 짜릿한 건 없습니다. 공개된 코드를 가지고 디자인을 수정하거나, 목표 달성 시 알람 소리가 울리게 하는 등 여러분만의 아이디어를 담아서 커스텀 프로그램으로 만들어보시길 바랍니다. 코드의 주석과 여러분의 배포 포스팅에 이 글의 링크 반드시 명시하는 거 잊지 마시고요.
긴 글 끝까지 읽어주셔서 감사합니다. 저는 이제 글자수세기 위젯 띄워두고 홀가분하게 다음 포스팅 작성하러 가보겠습니다.
'IT & 프로그래밍' 카테고리의 다른 글
| [인공지능 선형대수] 주성분 분석(PCA; Principal Component Analysis) 쉽게 이해하기 (파이썬 코드 포함) (0) | 2026.04.07 |
|---|---|
| [인공지능 선형대수] 벡터 정사영(Vector Projection) - 개념, 공식 유도 과정, NumPy 구현 (0) | 2026.04.01 |
| [티스토리 꿀팁] 에디터 화면에서 글자수세기 - 크롬 확장 프로그램 무료 배포 & 설치 방법 (0) | 2026.03.30 |
| [인공지능 선형대수] 코사인 유사도(Cosine Similarity): 추천 시스템의 비밀 (1) | 2026.03.27 |
| [인공지능 선형대수] 벡터의 연산 (덧셈, 뺄셈, 상수배, 아다마르 곱, 내적, 외적) (1) | 2026.03.26 |