Etc

북마클릿은 아직 죽지 않았다 — 무엇이 막히고 무엇이 되는지 직접 재봤다

한 줄로

북마클릿은 북마크에 주소 대신 자바스크립트를 넣어 둔 것이다.
누르면 지금 보고 있는 페이지 안에서 그 코드가 돈다.

“요즘은 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 가 났다
  • 다만 “비동기로 넘어가면 끊긴다”는 내 예상은 틀렸다. 실제로 클릭 후 setTimeout 1.5초
    뒤에도 복사가 됐다. 그런데 권한이 granted 여도 제스처 없이 부르면 여전히 실패한다 —
    기준은 시간이 아니라 사람이 누른 흐름에서 출발했는가
  • # 를 인코딩하지 않으면 뒤쪽 코드가 통째로 잘린다. 증상이 “아무 일도 안 일어남”이라 찾기 어렵다
  • 길어지면 확장으로 옮긴다. 북마클릿의 장점은 설치가 필요 없다는 것 하나인데
    길어지면 그게 사라진다
  • ⚠️ 남이 준 북마클릿은 내 로그인 상태로 실행된다. 읽고 넣는다

📌 위 실측은 Chrome 152 / macOS 에서 확인한 것이다. CSP 정책은 사이트가 언제든
바꿀 수 있고, 브라우저 정책도 바뀐다. “GitHub 에서 되더라”가 아니라 “쓰려는 사이트에서
직접 확인한다”
가 맞다.

같이 읽을 글:
navigator.clipboard는 HTTPS에서만 동작한다