이전에 아무도 링크한 적이 없는 대담한 링크: 텍스트 프래그먼트

텍스트 프래그먼트를 사용하면 URL 프래그먼트에서 텍스트 스니펫을 지정할 수 있습니다. 이러한 텍스트 프래그먼트가 있는 URL로 이동할 때 브라우저는 이를 강조하거나 사용자의 주의를 끌 수 있습니다.

프래그먼트 식별자

Chrome 80은 중요한 버전이었습니다. 여기에는 웹 워커의 ECMAScript 모듈, nullish 병합, 선택적 체이닝 등 많은 기대를 모으던 기능이 포함되어 있습니다. 이번 출시는 평소와 같이 Chromium 블로그의 블로그 게시물을 통해 발표되었습니다. 아래 스크린샷에서 블로그 게시물의 발췌 부분을 확인할 수 있습니다.

id 속성이 있는 요소 주위에 빨간색 상자가 있는 Chromium 블로그 게시물

빨간색 상자가 무엇을 의미하는지 궁금하실 수 있습니다. 이는 DevTools에서 다음 스니펫을 실행한 결과입니다. id 속성이 있는 모든 요소를 강조 표시합니다.

document.querySelectorAll('[id]').forEach((el) => {
  el.style.border = 'solid 2px red';
});

프래그먼트 식별자를 사용하여 빨간색 상자로 강조 표시된 모든 요소에 딥 링크를 배치할 수 있으며, 이 딥 링크는 페이지 URL의 해시에서 사용합니다. 측면의 제품 포럼에서 의견 보내기 상자에 딥 링크를 설정하려면 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1을 직접 만들어야 합니다. 개발자 도구의 Elements 패널에서 볼 수 있듯이 문제의 요소에는 값이 HTML1id 속성이 있습니다.

요소의 id를 보여주는 개발 도구

JavaScript의 URL() 생성자로 이 URL을 파싱하면 다양한 구성요소가 표시됩니다. 값이 #HTML1hash 속성을 확인합니다.

new URL('https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1');
/* Creates a new `URL` object
URL {
  hash: "#HTML1"
  host: "blog.chromium.org"
  hostname: "blog.chromium.org"
  href: "https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1"
  origin: "https://blog.chromium.org"
  password: ""
  pathname: "/2019/12/chrome-80-content-indexing-es-modules.html"
  port: ""
  protocol: "https:"
  search: ""
  searchParams: URLSearchParams {}
  username: ""
}
*/

하지만 개발자 도구를 열어야 요소의 id를 찾을 수 있었다는 사실은 이 페이지의 특정 섹션이 블로그 게시물 작성자가 연결하려고 했던 것일 가능성을 말해줍니다.

id 없이 항목에 연결하려면 어떻게 해야 하나요? Web Workers의 ECMAScript 모듈 제목에 링크를 추가한다고 가정해 보겠습니다. 아래 스크린샷에서 볼 수 있듯이 문제의 <h1>에는 id 속성이 없으므로 이 제목에 연결할 방법이 없습니다. 텍스트 프래그먼트는 이 문제를 해결합니다.

id가 없는 제목을 보여주는 개발자 도구

텍스트 프래그먼트

텍스트 프래그먼트 제안서에서는 URL 해시에 텍스트 스니펫을 지정하는 지원을 추가합니다. 이러한 텍스트 프래그먼트가 있는 URL로 이동할 때 사용자 에이전트는 이를 강조하거나 사용자의 주의를 환기할 수 있습니다.

브라우저 호환성

브라우저 지원

  • Chrome: 89
  • Edge: 89.
  • Firefox: 131.
  • Safari: 18.2

소스

보안상의 이유로 이 기능을 사용하려면 링크를 noopener 컨텍스트에서 열어야 합니다. 따라서 <a> 앵커 마크업에 rel="noopener"를 포함하거나 Window.open() 창 기능 목록에 noopener를 추가해야 합니다.

start

가장 간단한 형태로 텍스트 프래그먼트의 문법은 다음과 같습니다. 해시 기호 # 다음에 :~:text=, 마지막으로 start이 오는 형식입니다. 여기서 start은 연결하려는 퍼센트 인코딩된 텍스트를 나타냅니다.

#:~:text=start

예를 들어 Chrome 80의 기능을 발표하는 블로그 게시물웹 워커의 ECMAScript 모듈 제목에 링크를 추가하려고 한다고 가정해 보겠습니다. 이 경우 URL은 다음과 같습니다.

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers

텍스트 프래그먼트는 이와 같이 강조 표시됩니다. Chrome과 같은 지원되는 브라우저에서 링크를 클릭하면 텍스트 프래그먼트가 강조표시되고 스크롤하여 표시됩니다.

텍스트 프래그먼트가 스크롤되어 표시되고 강조 표시됩니다.

startend

제목뿐만 아니라 웹 워커의 ECMAScript 모듈이라는 섹션 전체에 연결하려면 어떻게 해야 하나요? 섹션의 전체 텍스트를 퍼센트 인코딩하면 결과 URL이 실행할 수 없을 만큼 길어집니다.

다행히 더 나은 방법이 있습니다. 전체 텍스트 대신 start,end 구문을 사용하여 원하는 텍스트를 프레이밍할 수 있습니다. 따라서 원하는 텍스트의 시작 부분에 퍼센트 인코딩된 단어를 몇 개 지정하고 원하는 텍스트의 끝 부분에 퍼센트 인코딩된 단어를 몇 개 지정하여 쉼표 ,로 구분합니다.

이런 모습입니다.

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers,ES%20Modules%20in%20Web%20Workers..

start의 경우 ECMAScript%20Modules%20in%20Web%20Workers, 쉼표 ,, ES%20Modules%20in%20Web%20Workers.(end) 순으로 표시됩니다. Chrome과 같이 지원되는 브라우저에서 클릭하면 전체 섹션이 강조 표시되고 스크롤하여 볼 수 있습니다.

텍스트 프래그먼트가 스크롤되어 표시되고 강조 표시됩니다.

이제 startend를 선택한 이유가 궁금할 수 있습니다. 사실 양쪽에 단어 2개만 있는 약간 더 짧은 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers.도 작동합니다. startend를 이전 값과 비교합니다.

한 단계 더 나아가 startend에 모두 하나의 단어만 사용하면 문제가 발생합니다. 이제 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers.이 더 짧아졌지만 강조 표시된 텍스트 프래그먼트가 더 이상 원래 원하는 프래그먼트가 아닙니다. 강조 표시는 Workers. 단어가 처음으로 표시되는 지점에서 중지됩니다. 이는 올바르지만 강조 표시하려는 부분이 아닙니다. 문제는 원하는 섹션이 현재 한 단어 startend 값으로 고유하게 식별되지 않는다는 점입니다.

의도하지 않은 텍스트 프래그먼트가 스크롤되어 표시되고 강조 표시됩니다.

prefix--suffix

startend에 충분히 긴 값을 사용하는 것이 고유한 링크를 얻는 한 가지 방법입니다. 하지만 경우에 따라 불가능할 수도 있습니다. 참고로 Chrome 80 출시 블로그 게시물을 예로 든 이유는 무엇인가요? 답은 이번 출시에서 텍스트 프래그먼트가 도입되었다는 것입니다.

블로그 게시물 텍스트: 텍스트 URL 프래그먼트 이제 사용자 또는 작성자는 URL에 제공된 텍스트 프래그먼트를 사용하여 페이지의 특정 부분에 연결할 수 있습니다. 페이지가 로드되면 브라우저가 텍스트를 강조 표시하고 프래그먼트를 스크롤하여 뷰로 가져옵니다. 예를 들어 아래 URL은 &#39;고양이&#39;의 위키 페이지를 로드하고 &#39;text&#39; 매개변수에 나열된 콘텐츠로 스크롤합니다.
텍스트 프래그먼트 공지사항 블로그 게시물 발췌본

위의 스크린샷에서 '텍스트'라는 단어가 4번 표시되는 것을 볼 수 있습니다. 네 번째는 녹색 코드 글꼴로 작성됩니다. 이 특정 단어로 연결하려면 starttext로 설정합니다. '텍스트'라는 단어는 단어 하나이므로 end는 있을 수 없습니다. 시작하기 전에 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=text은 제목에 이미 있는 '텍스트'라는 단어가 처음으로 등장하는 지점에서 일치합니다.

'텍스트'가 처음으로 발생하는 위치에 일치하는 텍스트 프래그먼트입니다.

다행히 해결 방법이 있습니다. 이 경우 prefix​--suffix를 지정할 수 있습니다. 녹색 코드 글꼴 '텍스트' 앞에 있는 단어는 'the'이고 뒤에 있는 단어는 'parameter'입니다. 'text'라는 단어가 다른 세 번 등장하지만 주변 단어가 동일하지는 않습니다. 이 정보를 바탕으로 이전 URL을 조정하고 prefix--suffix를 추가할 수 있습니다. 다른 매개변수와 마찬가지로 백분율로 인코딩해야 하며 두 개 이상의 단어를 포함할 수 있습니다. https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=the-,text,-parameter를 탭합니다. 파서가 prefix--suffix를 명확하게 식별할 수 있도록 하려면 start 및 선택적 end에서 대시 -로 구분해야 합니다.

원하는 'text'가 발생하는 위치에 일치하는 텍스트 프래그먼트입니다.

전체 문법

텍스트 프래그먼트의 전체 문법은 다음과 같습니다. 대괄호는 선택적 매개변수를 나타냅니다. 모든 매개변수의 값은 퍼센트로 인코딩되어야 합니다. 이는 대시 -, 앰퍼샌드 &, 쉼표 , 문자의 경우에 특히 중요합니다. 따라서 이러한 문자는 텍스트 디렉티브 문법의 일부로 해석되지 않습니다.

#:~:text=[prefix-,]start[,end][,-suffix]

prefix-, start, end, -suffix는 각각 단일 블록 수준 요소 내의 텍스트와만 일치하지만 전체 start,end 범위는 여러 블록에 걸쳐 있을 수 있습니다. 예를 들어 시작 문자열인 'The quick'가 단일의 중단되지 않은 블록 수준 요소 내에 표시되지 않으므로 다음 예에서는 :~:text=The quick,lazy dog가 일치하지 않습니다.

<div>
  The
  <div></div>
  quick brown fox
</div>
<div>jumped over the lazy dog</div>

하지만 다음 예에서는 일치합니다.

<div>The quick brown fox</div>
<div>jumped over the lazy dog</div>

브라우저 확장 프로그램으로 텍스트 프래그먼트 URL 만들기

텍스트 프래그먼트 URL을 직접 만드는 것은 특히 고유성을 보장하는 데 있어서 번거롭습니다. 정말로 원하는 경우 사양에 몇 가지 팁이 있으며 정확한 텍스트 프래그먼트 URL 생성 단계가 나와 있습니다. Google에서는 텍스트 프래그먼트 링크라는 오픈소스 브라우저 확장 프로그램을 제공합니다. 이 확장 프로그램을 사용하면 텍스트를 선택한 다음 컨텍스트 메뉴에서 '선택한 텍스트의 링크 복사'를 클릭하여 텍스트에 연결할 수 있습니다. 이 확장 프로그램은 다음 브라우저에서 사용할 수 있습니다.

텍스트 프래그먼트 링크 browser extension.

하나의 URL에 여러 텍스트 프래그먼트

하나의 URL에 여러 텍스트 프래그먼트가 표시될 수 있습니다. 특정 텍스트 프래그먼트는 앰퍼샌드 문자 &로 구분해야 합니다. 다음은 텍스트 프래그먼트가 3개인 링크의 예입니다. https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=Text%20URL%20Fragments&text=text,-parameter&text=:~:text=On%20islands,%20birds%20can%20contribute%20as%20much%20as%2060%25%20of%20a%20cat's%20diet

하나의 URL에 텍스트 프래그먼트 3개가 있습니다.

요소 및 텍스트 프래그먼트 혼합

기존 요소 프래그먼트는 텍스트 프래그먼트와 결합할 수 있습니다. 예를 들어 페이지의 원본 텍스트가 변경되어 텍스트 프래그먼트가 더 이상 일치하지 않는 경우 의미 있는 대체 텍스트를 제공하기 위해 동일한 URL에 둘 다 포함하는 것은 괜찮습니다. 제품 포럼 섹션에 연결되는 URL https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1:~:text=Give%20us%20feedback%20in%20our%20Product%20Forums.에는 요소 프래그먼트 (HTML1)와 텍스트 프래그먼트(text=Give%20us%20feedback%20in%20our%20Product%20Forums.)가 모두 포함되어 있습니다.

요소 프래그먼트와 텍스트 프래그먼트 모두와 연결합니다.

프래그먼트 지시문

아직 설명하지 않은 구문 요소가 하나 있습니다. 바로 프래그먼트 디렉티브 :~:입니다. 위와 같이 기존 URL 요소 프래그먼트와의 호환성 문제를 방지하기 위해 텍스트 프래그먼트 사양에서는 프래그먼트 디렉티브를 도입합니다. 프래그먼트 디렉티브는 코드 시퀀스 :~:로 구분된 URL 프래그먼트의 일부입니다. text=와 같은 사용자 에이전트 안내용으로 예약되어 있으며, 로드 중에 URL에서 제거되므로 작성자 스크립트가 이를 직접 상호작용할 수 없습니다. 사용자 에이전트 안내는 지시어라고도 합니다. 구체적인 경우 text=텍스트 디렉티브라고 합니다.

기능 감지

지원을 감지하려면 document에서 읽기 전용 fragmentDirective 속성을 테스트합니다. 프래그먼트 지시는 URL이 문서가 아닌 브라우저로 향하는 안내를 지정하는 메커니즘입니다. 이는 작성자 스크립트와의 직접적인 상호작용을 피하여 향후 사용자 에이전트 안내를 추가할 때 기존 콘텐츠에 중대한 변경사항이 도입되는 것을 방지하기 위한 것입니다. 향후 추가될 수 있는 예로는 번역 힌트가 있습니다.

if ('fragmentDirective' in document) {
  // Text Fragments is supported.
}

기능 감지는 주로 링크를 지원하지 않는 브라우저에 텍스트 프래그먼트 링크를 게재하지 않도록 링크가 동적으로 생성되는 경우 (예: 검색엔진에서)를 위해 고안되었습니다.

텍스트 프래그먼트 스타일 지정

기본적으로 브라우저는 mark의 스타일을 지정하는 것과 동일한 방식으로 텍스트 프래그먼트의 스타일을 지정합니다 (일반적으로 노란색 바탕에 검은색, mark의 CSS 시스템 색상). 사용자 에이전트 스타일시트에는 다음과 같은 CSS가 포함되어 있습니다.

:root::target-text {
  color: MarkText;
  background: Mark;
}

보시다시피 브라우저는 적용된 강조 표시를 맞춤설정하는 데 사용할 수 있는 가상 선택기 ::target-text를 노출합니다. 예를 들어 텍스트 프래그먼트를 빨간색 배경에 검은색 텍스트로 디자인할 수 있습니다. 항상 그렇듯이 재정의 스타일이 접근성 문제를 일으키지 않도록 색상 대비를 확인하고 강조 표시가 실제로 나머지 콘텐츠에서 시각적으로 눈에 띄는지 확인하세요.

:root::target-text {
  color: black;
  background-color: red;
}

폴리필 가능 여부

텍스트 프래그먼트 기능은 어느 정도까지 폴리필할 수 있습니다. 기능이 JavaScript로 구현된 텍스트 프래그먼트에 대한 기본 제공 지원을 제공하지 않는 브라우저의 경우 확장 프로그램에서 내부적으로 사용하는 polyfill을 제공합니다.

polyfill에는 가져와서 텍스트 프래그먼트 링크를 생성하는 데 사용할 수 있는 fragment-generation-utils.js 파일이 포함되어 있습니다. 이는 아래 코드 샘플에 설명되어 있습니다.

const { generateFragment } = await import('https://unpkg.com/text-fragments-polyfill/dist/fragment-generation-utils.js');
const result = generateFragment(window.getSelection());
if (result.status === 0) {
  let url = `${location.origin}${location.pathname}${location.search}`;
  const fragment = result.fragment;
  const prefix = fragment.prefix ?
    `${encodeURIComponent(fragment.prefix)}-,` :
    '';
  const suffix = fragment.suffix ?
    `,-${encodeURIComponent(fragment.suffix)}` :
    '';
  const start = encodeURIComponent(fragment.textStart);
  const end = fragment.textEnd ?
    `,${encodeURIComponent(fragment.textEnd)}` :
    '';
  url += `#:~:text=${prefix}${start}${end}${suffix}`;
  console.log(url);
}

분석을 위해 텍스트 프래그먼트 가져오기

많은 사이트에서 라우팅에 프래그먼트를 사용하므로 브라우저는 이러한 페이지가 손상되지 않도록 텍스트 프래그먼트를 제거합니다. 예를 들어 분석 목적으로 페이지에 텍스트 프래그먼트 링크를 노출해야 한다는 필요성이 확인되었지만 제안된 솔루션은 아직 구현되지 않았습니다. 지금은 해결 방법으로 아래 코드를 사용하여 원하는 정보를 추출할 수 있습니다.

new URL(performance.getEntries().find(({ type }) => type === 'navigate').name).hash;

보안

텍스트 프래그먼트 디렉티브는 사용자 활성화의 결과인 전체 (동일 페이지가 아닌) 탐색에서만 호출됩니다. 또한 도착 페이지가 충분히 격리된 것으로 알려진 것처럼 도착 페이지와 다른 출처에서 시작된 탐색은 noopener 컨텍스트에서 탐색이 이루어져야 합니다. 텍스트 프래그먼트 디렉티브는 기본 프레임에만 적용됩니다. 즉, 텍스트가 iframe 내에서 검색되지 않으며 iframe 탐색으로 텍스트 프래그먼트가 호출되지 않습니다.

개인 정보 보호

Text Fragments 사양의 구현은 텍스트 프래그먼트가 페이지에서 발견되었는지 여부를 유출해서는 안 됩니다. 요소 프래그먼트는 원본 페이지 작성자가 완전히 제어하지만 텍스트 프래그먼트는 누구나 만들 수 있습니다. 위 예시에서 <h1>id가 없으므로 웹 워커의 ECMAScript 모듈 제목에 연결할 방법이 없었지만, 나를 포함한 누구나 텍스트 프래그먼트를 신중하게 작성하여 어디든지 연결할 수 있었던 것을 기억하세요.

악의적인 광고 네트워크 evil-ads.example.com를 운영한다고 가정해 보겠습니다. 또한 광고 iframe 중 하나에서 사용자가 광고와 상호작용할 때 텍스트 프래그먼트 URL dating.example.com#:~:text=Log%20Out를 사용하여 dating.example.com에 숨겨진 교차 출처 iframe을 동적으로 만들었다고 가정해 보겠습니다. '로그아웃' 텍스트가 발견되면 피해자가 현재 dating.example.com에 로그인되어 있음을 알 수 있으며 이를 사용자 프로파일링에 사용할 수 있습니다. 단순한 Text Fragments 구현은 일치가 성공하면 포커스 전환이 발생해야 한다고 결정할 수 있으므로 evil-ads.example.com에서 blur 이벤트를 리슨하여 일치가 발생한 시점을 알 수 있습니다. Chrome에서는 위 시나리오가 발생하지 않도록 텍스트 프래그먼트를 구현했습니다.

스크롤 위치를 기반으로 네트워크 트래픽을 악용하는 것도 또 다른 공격일 수 있습니다. 회사 인트라넷의 관리자처럼 피해자의 네트워크 트래픽 로그에 액세스할 수 있다고 가정해 보겠습니다. 이제 다음과 같은 증상이 나타나면 어떻게 해야 하나요?라는 긴 인사부 문서와 번아웃, 불안과 같은 상태 목록이 있다고 가정해 보겠습니다. 목록의 각 항목 옆에 추적 픽셀을 배치할 수 있습니다. 그런 다음 문서 로드가 번아웃 항목 옆에 있는 추적 픽셀의 로드와 시간적으로 일치하는 것으로 확인되면 인트라넷 관리자는 직원이 :~:text=burn%20out가 포함된 텍스트 프래그먼트 링크를 클릭하여 기밀이라고 생각하고 아무에게도 표시되지 않는다고 생각했을 수 있음을 확인할 수 있습니다. 이 예는 처음부터 다소 인위적이며 악용하려면 매우 구체적인 사전 조건이 충족되어야 하므로 Chrome 보안팀은 탐색 시 스크롤 구현의 위험을 관리 가능한 것으로 평가했습니다. 다른 사용자 에이전트는 대신 수동 스크롤 UI 요소를 표시할 수 있습니다.

선택 해제하려는 사이트의 경우 Chromium은 사용자 에이전트가 텍스트 프래그먼트 URL을 처리하지 않도록 전송할 수 있는 문서 정책 헤더 값을 지원합니다.

Document-Policy: force-load-at-top

텍스트 프래그먼트 사용 중지

이 기능을 사용 중지하는 가장 쉬운 방법은 HTTP 응답 헤더를 삽입할 수 있는 확장 프로그램(예: ModHeader(Google 제품 아님))을 사용하여 다음과 같이 응답(요청이 아님) 헤더를 삽입하는 것입니다.

Document-Policy: force-load-at-top

더 복잡한 방법으로 선택 해제하려면 엔터프라이즈 설정 ScrollToTextFragmentEnabled을 사용하면 됩니다. macOS에서 이렇게 하려면 터미널에 아래 명령어를 붙여넣습니다.

defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false

Windows에서는 Google Chrome Enterprise 도움말 지원 사이트의 문서를 따르세요.

일부 검색어의 경우 Google 검색엔진에서 관련 웹사이트의 콘텐츠 스니펫을 활용하여 빠른 답변이나 요약을 제공합니다. 이러한 추천 스니펫은 질문 형식으로 검색했을 때 표시될 가능성이 큽니다. 추천 스니펫을 클릭하면 소스 웹페이지의 추천 스니펫 텍스트로 바로 이동합니다. 이는 자동으로 생성된 텍스트 프래그먼트 URL 덕분에 가능합니다.

추천 스니펫이 표시된 Google 검색엔진 결과 페이지 상태 표시줄에 텍스트 프래그먼트 URL이 표시됩니다.
클릭하면 페이지의 관련 섹션이 스크롤되어 표시됩니다.

결론

텍스트 프래그먼트 URL은 웹페이지의 임의 텍스트에 연결하는 강력한 기능입니다. 학술 커뮤니티는 이를 사용하여 매우 정확한 인용이나 참조 링크를 제공할 수 있습니다. 검색엔진은 이를 사용하여 페이지의 텍스트 결과로 딥링크할 수 있습니다. 소셜 네트워킹 사이트에서는 사용자가 액세스할 수 없는 스크린샷 대신 웹페이지의 특정 구절을 공유할 수 있도록 할 수 있습니다. 텍스트 프래그먼트 URL을 사용해 보고 저와 마찬가지로 유용하다고 생각해 주시기 바랍니다. 텍스트 프래그먼트 링크 브라우저 확장 프로그램을 설치해야 합니다.

감사의 말씀

텍스트 프래그먼트는 닉 버리스데이비드 보칸이 구현하고 그랜트 왕이 참여하여 지정했습니다. 이 도움말을 꼼꼼하게 검토해 주신 조 미들리님께 감사드립니다. Unsplash그렉 라코지님이 제공한 히어로 이미지