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

텍스트 프래그먼트를 사용하면 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을 파싱하면 다양한 구성요소가 표시됩니다. hash 속성의 값이 #HTML1인 것을 확인합니다.

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가 없는 항목에 연결하려면 어떻게 해야 하나요? 웹 작업자의 ECMAScript 모듈 제목에 연결하려 한다고 가정해 보겠습니다. 아래 스크린샷에서 볼 수 있듯이 문제의 <h1>에는 id 속성이 없습니다. 즉, 이 제목에 연결할 방법이 없습니다. 이것이 텍스트 프래그먼트가 해결하는 문제입니다.

id 없이 제목이 표시된 개발자 도구

텍스트 프래그먼트

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

브라우저 호환성

브라우저 지원

  • 89
  • 89
  • x
  • x

소스

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

start

가장 간단한 형태의 텍스트 프래그먼트 문법은 해시 기호 # 다음에 :~:text=이 오고 마지막으로 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 중 어떤 것을 선택했는지 궁금할 것입니다. 사실 양쪽에 단어 두 개만 있는 약간 짧은 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; 매개변수에 나열된 콘텐츠로 스크롤합니다.
텍스트 프래그먼트 공지사항 블로그 게시물 발췌

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

'Text'가 처음 발생할 때의 텍스트 프래그먼트 일치

다행히 해결 방법이 있습니다. 이 경우 prefix​--suffix를 지정할 수 있습니다. 녹색 코드 글꼴 'text' 앞에 있는 단어는 '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 범위는 여러 블록에 걸쳐 있을 수 있습니다. 예를 들어 다음 예에서는 :~:text=The quick,lazy dog가 일치하지 않습니다. 시작 문자열 'The quick'가 중단 없는 단일 블록 수준 요소 내에 나타나지 않기 때문입니다.

<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이 고유한지 확인하는 경우에는 특히 더 그렇습니다. 원하는 경우 사양에 몇 가지 팁이 있으며 텍스트 프래그먼트 URL을 생성하는 정확한 단계가 나열되어 있습니다. Google은 텍스트 프래그먼트에 링크라는 오픈소스 브라우저 확장 프로그램을 제공합니다. 이 확장 프로그램을 사용하면 텍스트를 선택한 다음 컨텍스트 메뉴에서 '선택한 텍스트에 링크 복사'를 클릭하여 텍스트를 링크할 수 있습니다. 이 확장 프로그램은 다음 브라우저에서 사용할 수 있습니다.

Text Fragment 링크 브라우저 확장 프로그램입니다.

하나의 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;
}

폴리필 가능 여부

텍스트 프래그먼트 기능은 어느 정도 폴리필할 수 있습니다. Google에서는 기능이 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 탐색이 텍스트 프래그먼트를 호출하지 않습니다.

개인 정보 보호

텍스트 프래그먼트 사양의 구현이 페이지에서 텍스트 프래그먼트가 발견되었는지 여부와 관계없이 유출되지 않는 것이 중요합니다. 요소 프래그먼트는 원본 페이지 작성자가 완전히 제어하지만 텍스트 프래그먼트는 누구나 만들 수 있습니다. 위 예에서는 <h1>id가 없었기 때문에 웹 작업자의 ECMAScript 모듈 제목에 연결할 방법이 없었다는 점을 기억하시나요? 하지만 저를 비롯한 모든 사람이 텍스트 프래그먼트를 신중하게 만들어 아무 곳에나 링크할 수 있었던 방법을 떠올려 보세요.

나쁜 광고 네트워크(evil-ads.example.com)를 실행했다고 가정해 보겠습니다. 또한 광고 iframe 중 하나에서 사용자가 광고와 상호작용한 후 텍스트 프래그먼트 URL dating.example.com#:~:text=Log%20Out를 사용하여 dating.example.com에 대한 숨겨진 교차 출처 iframe을 동적으로 생성했다고 가정해 보겠습니다. 'Log Out'이라는 텍스트가 발견되면 피해자가 현재 사용자 프로파일링에 사용할 수 있는 dating.example.com에 로그인한 상태입니다. 기본 텍스트 프래그먼트 구현은 성공적인 일치로 인해 포커스 전환이 발생한다고 결정할 수 있으므로 evil-ads.example.com에서 blur 이벤트를 수신 대기하여 일치가 발생한 시점을 알 수 있습니다. Chrome에서는 위의 시나리오가 발생할 수 없도록 텍스트 프래그먼트를 구현했습니다.

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

선택 해제하려는 사이트의 경우 Chromium은 사용자 에이전트가 Text Fragment URL을 처리하지 않도록 전송할 수 있는 Document Policy(문서 정책) 헤더 값을 지원합니다.

Document-Policy: force-load-at-top

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

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

Document-Policy: force-load-at-top

더 복잡한 또 다른 방법은 엔터프라이즈 설정 ScrollToTextFragmentEnabled를 사용하는 것입니다. macOS에서 이 작업을 수행하려면 터미널에 아래 명령어를 붙여넣습니다.

defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false

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

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

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

결론

텍스트 프래그먼트 URL은 웹페이지의 임의의 텍스트로 연결되는 강력한 기능입니다. 학계에서는 이 링크를 사용하여 매우 정확한 인용 또는 참조 링크를 제공할 수 있습니다. 검색엔진은 이를 사용하여 페이지의 텍스트 결과에 딥 링크를 설정할 수 있습니다. 소셜 네트워킹 사이트에서는 이 기능을 통해 액세스할 수 없는 스크린샷이 아닌 웹페이지의 특정 문구를 공유할 수 있습니다. 텍스트 프래그먼트 URL 사용을 시작하여 제가 유용하게 활용하셨기를 바랍니다. Link to Text Fragment 브라우저 확장 프로그램을 설치해야 합니다.

감사의 말

텍스트 프래그먼트는 닉 버리스데이비드 보칸이 구현 및 지정했으며, Grant Wang의 기여로 이루어졌습니다. 이 문서를 자세히 검토해 주신 Joe Medley님께 감사드립니다. Unsplash에 있는 Greg Rakozy의 히어로 이미지