한 줄로
북마클릿은 북마크에 주소 대신 자바스크립트를 넣어 둔 것이다.
누르면 지금 보고 있는 페이지 안에서 그 코드가 돈다.
“요즘은 CSP 때문에 안 된다”는 말을 자주 보는데, 절반만 맞다.
직접 재보니 갈리는 지점이 분명했다.
| GitHub (엄격한 CSP) | CSP 없는 사이트 | |
|---|---|---|
| 북마클릿 본문 실행 | 된다 | 된다 |
<script> 를 만들어 꽂기 |
차단 | 된다 |
| 외부 스크립트 로드 | 차단 | 된다 |
경계는 네트워크가 아니라 “DOM 에 남기느냐” 였다.
Chrome 152 에서 실측. 아래에 어떻게 쟀는지 적는다.
생김새
주소창에 넣는 URL 은 보통 https: 로 시작한다. 북마클릿은 그 자리에 javascript: 를 쓴다.
javascript:(function(){ alert(document.title); })();
이걸 북마크의 URL 칸에 넣는다. 이름 칸에는 아무거나 적으면 된다.
누르면 지금 열려 있는 페이지 위에서 저 코드가 실행된다.
세 부분으로 나뉜다.
| 조각 | 역할 |
|---|---|
javascript: |
“이건 주소가 아니라 코드다” 라고 알리는 표시 |
(function(){ … })() |
즉시 실행 함수 — 변수를 페이지에 남기지 않는다 |
; |
문장 끝 |
가운데를 감싸는 이유는 오염을 막기 위해서다. 감싸지 않고 var x = 1 이라고 쓰면
그 페이지의 전역에 x 가 생긴다. 페이지가 이미 x 를 쓰고 있었다면 덮어쓴다.
void 0 을 붙이는 이유
옛날 북마클릿에 void 0 이나 void(0) 이 붙어 있는 걸 본다.
javascript:document.body.style.zoom='1.5';void 0;
javascript: URL 이 값을 반환하면 브라우저가 그 값으로 페이지를 갈아치우던 동작이 있었다.
document.title 을 반환하면 화면이 제목 문자열만 남은 흰 페이지가 됐다.
void 0 은 반환값을 undefined 로 만들어 그걸 막는다.
즉시 실행 함수가 return 없이 끝나면 이미 undefined 라 요즘은 대개 필요 없다.
옛 코드에 있으면 지우지 말고 두는 쪽이 안전하다.
⚠️ 이 문단은 재보지 않았다. 지금 브라우저에서 저 동작을 재현하려면 옛 버전이 필요한데
확인하지 않았다. 관례의 유래로만 읽는 게 맞다 — 위의 CSP·클립보드 수치와 달리 실측이 아니다.
무엇이 진짜로 막히는가
여기가 이 글을 쓴 이유다. “CSP 때문에 북마클릿은 끝났다”는 말이 자주 보이는데,
정확하지 않다. 막히는 것과 안 막히는 것이 갈린다.
CSP(Content Security Policy)는 사이트가 응답 헤더로 보내는 “이 페이지에서 어떤 코드를
실행해도 되는지” 규칙이다. GitHub 은 꽤 엄격한 편이다.
content-security-policy: default-src 'none'; base-uri 'self'; …
default-src 'none' 은 “기본적으로 아무 출처도 허용하지 않는다”는 뜻이다.
그러면 북마클릿도 죽어야 할 것 같은데, 실제로는 그렇지 않았다.
실측
GitHub 을 열어 놓고 북마클릿이 하는 것과 같은 일을 시켰다.
북마클릿 본문의 코드 → 된다 (eval("1+1") === 2)
<script> 를 만들어 꽂기 → 차단
<script src=…> 로 로드 → 차단 (onerror 발생)
첫 줄만 통과한다. 북마클릿이 사용자가 직접 실행한 코드로 취급되기 때문이다.
CSP 는 페이지가 실행하는 코드를 통제하지, 사용자가 브라우저에서 직접 실행한 것까지
막으라고 만들어진 규칙이 아니다.
그래서 이렇게 갈린다.
| 하는 일 | 엄격한 CSP 사이트에서 |
|---|---|
DOM 읽기 · 고치기, 계산, alert
|
된다 |
<script> 를 새로 만들어 넣기 |
차단된다 |
외부 라이브러리(<script src=…>) 끌어오기 |
차단된다 |
💡 그래서 북마클릿은 의존성 없이 한 덩어리로 쓰는 게 맞다.
“jQuery 를 불러와서 쓰자”는 방식이 막히는 것이지, 북마클릿 자체가 막히는 게 아니다.
CSP 가 없는 사이트에서는 외부 스크립트도 그대로 로드된다 — 같은 코드를 이 블로그에서
돌려보니 loaded 였다. 사이트마다 다르다는 게 결론이다.
왜 같은 코드가 한쪽에서만 도는가
위 결과를 보면 이상하다. eval("1+1") 은 되는데 <script src=…> 는 막힌다.
둘 다 자바스크립트를 실행하는 일인데 왜 하나만 걸리나.
CSP 가 검사하는 대상이 “자바스크립트인가”도, “네트워크를 타는가”도 아니기 때문이다.
“이 코드를 누가 실행시켰는가” 를 본다.
같은 페이지에서 세 가지를 나눠서 재봤다.
| 실행 경로 | 네트워크 | 결과 |
|---|---|---|
| 북마클릿 본문의 인라인 코드 | 안 탐 | 된다 |
북마클릿이 만든 <script> (코드를 직접 넣음) |
안 탐 | 차단 |
북마클릿이 만든 <script src=…>
|
탐 | 차단 |
가운데 줄이 중요하다. 네트워크를 전혀 안 쓰는데도 막힌다.
그러니 “네트워크를 타면 걸린다”가 아니다. <script> 를 DOM 에 꽂는 순간
그건 페이지가 실행하는 코드가 되고, 페이지가 실행하는 코드는 CSP 의 검사 대상이다.
반면 북마클릿 본문은 문서에 들어가지 않는다. 브라우저가 사용자 동작으로
직접 돌리는 것이라 애초에 검사 지점을 안 지난다.
“DOM 에 남기면 걸리고, 그냥 실행하면 안 걸린다” 가 정확한 선이다.
그래서 우회라고 부를 만한 게 아니다. 막는 대상 자체가 다르다.
CSP 는 페이지에서 어떤 코드가 실행되는지를 통제하려고 만든 규칙이고,
브라우저 주인이 자기 브라우저에서 코드를 실행하는 것은 애초에 그 규칙이 겨냥한 대상이 아니다.
막았다면 개발자 도구 콘솔도 같이 막아야 하는데, 그건 브라우저 주인의 권한이다.
⚠️ 그래서 “CSP 를 피하는 방법”으로 읽으면 안 된다. 이건 경계선이 어디인지에 대한
설명이다. CSP 가 지키려는 것(외부 코드 주입·데이터 유출)은 북마클릿으로도 그대로 유효하다 —
아래 “남이 준 북마클릿” 항목이 그 얘기다. 내가 읽고 내가 실행하는 것과
페이지가 모르는 곳에서 코드를 끌어오는 것은 위험이 다르고, CSP 는 뒤쪽을 막는다.
클립보드는 따로 논다
붙여넣기용 북마클릿을 만들 때 가장 많이 걸리는 부분이라 따로 적는다.
GitHub(HTTPS)에서 navigator.clipboard.writeText() 를 그냥 부르면 이렇게 실패했다.
NotAllowedError
HTTPS 인데도 실패한다. 원인이 두 가지로 나뉘기 때문이다.
| 실패 원인 | 조건 |
|---|---|
| secure context 아님 | HTTP 로 서비스 중 (localhost 는 예외) |
| 사용자 제스처 없음 | 클릭 같은 동작 없이 호출 |
내가 만난 건 아래쪽이다. 클립보드는 사용자가 실제로 누른 동작에서 시작된 흐름에서만
쓸 수 있다. 위 실패는 아무것도 누르지 않고 코드만 실행시켜서 난 것이다.
어디까지 허용되는지 직접 재봤다. 버튼을 실제로 클릭한 뒤 두 시점에서 복사를 시도했다.
클릭 직후 바로 → ok
setTimeout 1.5초 뒤 → ok ← 예상과 달랐다
아무것도 안 누르고 호출 → NotAllowedError
가운데가 의외였다. “비동기로 넘어가면 제스처가 끊긴다”고 알고 있었는데,
실제로는 1.5초 뒤에도 통과했다. 권한 상태를 같이 찍어보니 granted 였다 —
한 번 진짜 제스처로 허용되면 그 뒤로는 좀 느슨해진다.
다만 권한이 granted 인 상태에서도 제스처 없이 부르면 여전히 실패했다.
그러니 “권한을 받았으니 아무 때나 된다”도 아니다. 사람이 누른 흐름에서 출발했는가가 기준이다.
💡 북마크를 누르는 것은 그 출발점이 된다. 그래서 실제 북마클릿에서는 대체로 동작한다.
경계가 정확히 어디인지는 브라우저 구현에 달려 있으니, 복사는 되도록 앞쪽에 두는 편이
안전하다 — 위 실측은 여유가 있다는 뜻이지 무한하다는 뜻은 아니다.
위쪽(HTTPS 문제)은 예전에 따로 겪어서 적어 뒀다 —
navigator.clipboard는 HTTPS에서만 동작한다.
두 개는 다른 원인이라 증상이 같아도 해법이 다르다.
만들 때 걸리는 것 — 인코딩
코드를 그대로 북마크에 넣으면 깨지는 경우가 있다. URL 로 들어가기 때문에
URL 에서 뜻이 있는 문자는 그대로 못 쓴다.
| 문자 | 바꿔야 하는 값 | 안 바꾸면 |
|---|---|---|
% |
%25 |
인코딩 시작 문자로 해석된다 |
# |
%23 |
뒤가 전부 잘린다 (프래그먼트로 인식) |
" |
%22 |
웹페이지의 링크(href="…")로 배포할 때만 해당 |
| 줄바꿈 | %0A |
URL 에 들어가지 못한다 |
# 가 제일 잘 당한다. '#fff' 같은 색상값 하나 때문에 뒤쪽 코드가 통째로 사라진다.
증상이 “아무 일도 안 일어남”이라 원인을 찾기 어렵다.
% 도 만만치 않다. CSS 를 다루는 북마클릿이면 '150%' 같은 값을 자연스럽게 쓰게 되는데,
%15 가 인코딩 시퀀스로 해석되면서 URL 자체가 깨진다.
decodeURI("javascript:a='150%';") // URIError
decodeURI("javascript:a='#fff';") // 통과 — 대신 '#' 뒤가 잘린다
둘의 증상이 다르다. % 는 URL 이 아예 깨지고, # 는 조용히 뒤만 잘린다.
%25 로 바꾸거나, 위 예제처럼 zoom 에 '1.5' 를 쓰는 식으로 % 를 피해서 쓴다.
한글은 URL 에 그대로 두면 브라우저가 알아서 처리하는 경우가 많지만,
확실하게 하려면 인코딩한다. 다만 전부 인코딩하면 길이가 늘어난다 —
javascript:alert('복사했습니다'); 는 27자인데 전부 인코딩하면 87자가 됐다.
필요한 문자만 바꾸는 게 낫다.
길이 제한
“북마클릿은 2000자 제한” 같은 말이 도는데, 표준에 정해진 값은 아니다.
브라우저·OS 마다 다르다. 다만 길어질수록 관리가 어려워지는 건 확실하다 —
한 줄로 뭉쳐 있어 나중에 고치기가 힘들다.
⚠️ 나는 상한을 직접 재보지 않았다. 그러니 “어디까지 되는지”를 이 글에서 가져가면 안 된다.
애초에 상한을 궁금해할 만큼 길어졌다면, 그게 확장으로 옮길 신호다.
길어지면 북마클릿으로 버티지 말고 확장 프로그램(익스텐션)이나 유저스크립트로 옮기는 게 맞다.
북마클릿의 장점은 설치가 필요 없다는 것 하나인데, 길어지면 그 장점이 사라진다.
실제로 쓸 만한 예
지금 페이지를 마크다운 링크로 복사하는 것. 블로그 글 쓸 때 참고 링크를 모을 때 쓴다.
javascript:(function(){
const md = '[' + document.title + '](' + location.href + ')';
navigator.clipboard.writeText(md);
})();
이 블로그 메인에서 돌려보니 이렇게 나왔다.
[전은성 Dev](https://dmstjd1024.github.io/)
한 줄로 붙이면 이렇게 된다. 이게 북마크 URL 칸에 들어갈 최종 형태다.
javascript:(function(){const md='['+document.title+']('+location.href+')';navigator.clipboard.writeText(md);})();
113자다. 앞에서 말한 인코딩이 필요한 문자(%, #, ")가 하나도 없어서
이대로 넣으면 된다.
언제 쓰고 언제 쓰지 않나
| 상황 | 판단 |
|---|---|
| 지금 이 페이지에서 한 번에 끝나는 일 | 북마클릿이 맞다 |
| 여러 사이트에서 자동으로 돌아야 함 | 유저스크립트 · 확장 |
| 로그인 · 토큰이 필요한 작업 | 확장 (북마클릿에 비밀값을 넣지 않는다) |
| 코드가 수십 줄 넘어감 | 확장으로 옮긴다 |
⚠️ 남이 준 북마클릿은 코드를 읽고 넣는다. 북마클릿은 그 페이지에서 내 로그인 상태로
실행된다. 쿠키도 읽고 요청도 보낼 수 있다. 짧아 보인다고 그냥 넣으면 안 되고,
읽어서 이해되지 않으면 쓰지 않는 게 맞다.
정리
- 북마클릿은 북마크 URL 칸에
javascript:로 시작하는 코드를 넣은 것이다.
누르면 지금 페이지 위에서 돈다 -
(function(){…})()로 감싸는 이유는 페이지의 전역을 오염시키지 않기 위해서다 -
“CSP 때문에 안 된다”는 절반만 맞다. 실측하니 엄격한 CSP 사이트에서도
북마클릿 본문은 실행되고,<script>를 만들어 꽂는 순간 차단됐다.
외부 로드뿐 아니라 코드를 직접 넣은 인라인<script>도 막힌다 — 네트워크와 무관하다.
경계는 “DOM 에 남기느냐” 이고, 그래서 의존성 없이 한 덩어리로 쓰면 대체로 동작한다 - 클립보드 실패는 원인이 둘이다 — HTTPS 가 아니거나, 사용자 제스처가 없거나.
HTTPS 에서도NotAllowedError가 났다 - 다만 “비동기로 넘어가면 끊긴다”는 내 예상은 틀렸다. 실제로 클릭 후
setTimeout1.5초
뒤에도 복사가 됐다. 그런데 권한이granted여도 제스처 없이 부르면 여전히 실패한다 —
기준은 시간이 아니라 사람이 누른 흐름에서 출발했는가다 -
#를 인코딩하지 않으면 뒤쪽 코드가 통째로 잘린다. 증상이 “아무 일도 안 일어남”이라 찾기 어렵다 - 길어지면 확장으로 옮긴다. 북마클릿의 장점은 설치가 필요 없다는 것 하나인데
길어지면 그게 사라진다 - ⚠️ 남이 준 북마클릿은 내 로그인 상태로 실행된다. 읽고 넣는다
📌 위 실측은 Chrome 152 / macOS 에서 확인한 것이다. CSP 정책은 사이트가 언제든
바꿀 수 있고, 브라우저 정책도 바뀐다. “GitHub 에서 되더라”가 아니라 “쓰려는 사이트에서
직접 확인한다” 가 맞다.
같이 읽을 글:
navigator.clipboard는 HTTPS에서만 동작한다