<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>CHANCETHECODER</title>
    <link>https://chancethecoder.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 08:43:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>chancethecoder</managingEditor>
    <item>
      <title>SSOT(Single Source of Truth)란?</title>
      <link>https://chancethecoder.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Single_source_of_truth&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SSOT&lt;/a&gt;는 Single Source of Truth의 약어로, 데이터베이스, 애플리케이션, 프로세스 등의 모든 데이터에 대해 하나의 출처를 사용하는 개념을 의미합니다. 이는 데이터의 정확성, 일관성, 신뢰성을 보장하고, 일관성 있는 의사결정 및 작업 효율성을 높이는 데 도움을 줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSOT의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSOT는 기업에서 데이터 및 정보의 일관성을 유지하고 보장하기 위해 매우 중요합니다. 이를 통해 다음과 같은 장점을 얻을 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 정확성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터에 대한 일관성과 정확성을 유지하므로, 잘못된 데이터로 인한 잘못된 결정을 내리는 경우를 줄일 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 일관성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사용자가 동일한 정보를 사용하도록 보장하기 때문에, 업무 프로세스 및 의사결정에 일관성을 유지할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 효율성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사용자가 동일한 정보를 사용하므로, 중복된 작업을 방지하고, 시간과 비용을 절약할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSOT의 구현 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSOT를 구현하기 위해서는 먼저 모든 데이터와 정보를 이해하고, 이를 표준화해야 합니다. 이는 매우 중요한 단계로, 데이터의 표준화를 통해 일관성 있는 정보를 얻을 수 있고, 데이터의 정확성을 보장할 수 있습니다. 이를 위해 데이터의 유형과 속성을 분석하고, 중복되는 데이터를 제거하며, 데이터 표준화 규칙을 작성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 하나의 출처를 지정하고, 모든 데이터를 해당 출처에서 관리하도록 합니다. 이는 데이터의 일관성을 유지하고, 중복된 작업을 방지하며, 데이터의 보안성을 높일 수 있습니다. 이를 위해 데이터의 출처를 식별하고, 데이터의 생성, 저장 및 유지 보수를 담당할 담당자를 지정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 변경 및 업데이트를 관리하기 위해서는 데이터 변경 규정을 작성하고, 데이터의 변경 이력을 관리해야 합니다. 이를 통해 데이터 변경 과정에서 발생할 수 있는 문제를 최소화하고, 데이터의 신뢰성과 일관성을 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 모든 사용자가 동일한 데이터에 액세스할 수 있도록 데이터 액세스 권한을 설정해야 합니다. 이를 위해 데이터 보안 규칙을 작성하고, 데이터의 액세스 권한을 지정해야 합니다. 이를 통해 데이터의 무단 접근을 방지하고, 데이터의 보안성을 높일 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSOT는 데이터 일관성, 신뢰성, 효율성을 보장하는 중요한 개념입니다. 이를 위해서는 데이터와 정보를 이해하고 표준화하며, 출처를 지정하고 데이터 관리 규정을 작성해야 합니다. 이를 통해 기업은 데이터 관리의 복잡성을 줄이고 효율적인 업무 프로세스를 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <category>SSOT</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/45</guid>
      <comments>https://chancethecoder.tistory.com/45#entry45comment</comments>
      <pubDate>Fri, 3 Mar 2023 16:04:31 +0900</pubDate>
    </item>
    <item>
      <title>SCIM(System for Cross-domain Identity Management)이란?</title>
      <link>https://chancethecoder.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SCIM(System for Cross-domain Identity Management)은 클라우드 기반 인증 및 권한 부여 서비스의 표준화된 방법을 제공하는 프로토콜입니다. 이는 사용자 계정, 그룹 및 기타 리소스와 같은 식별자와 관련된 데이터를 관리하고 교환하는 데 사용됩니다. 이는 특히 기업의 IT 관리자들이 클라우드 기반 서비스에 대한 접근을 간소화하고, 보안을 유지하며, 사용자 데이터를 효율적으로 관리하는 데 사용됩니다. SCIM은 IETF(Internet Engineering Task Force)에서 표준으로 지정되어 있으며, 이는 현재 많은 클라우드 서비스 제공업체(예: Google, Microsoft, Salesforce 등)에서 지원됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SCIM의 이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCIM은 기업에서 사용자 계정과 그룹을 관리하는 데 사용되는 기존의 방법과 비교하여 다음과 같은 이점을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표준화 : SCIM은 업계에서 인정하는 표준 프로토콜입니다. 이는 IT 관리자들이 여러 클라우드 서비스 제공업체에서 사용자 계정을 관리할 때 일관된 방식으로 관리할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;간소화된 사용자 관리 : SCIM은 사용자 계정 생성, 수정 및 삭제와 같은 관리 작업을 자동화합니다. 이는 IT 관리자들이 시간과 비용을 절약할 수 있도록 도와줍니다.&lt;/li&gt;
&lt;li&gt;보안 : SCIM은 사용자 데이터를 안전하게 전송하고 저장할 수 있도록 보안을 제공합니다. 이는 사용자 계정 정보가 노출되지 않도록 보호하는 데 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SCIM의 사용 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCIM은 기업에서 다양한 사용 사례에 사용됩니다. 몇 가지 예시는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 기반 서비스에 대한 사용자 액세스 관리&lt;/li&gt;
&lt;li&gt;사용자 계정 동기화&lt;/li&gt;
&lt;li&gt;사용자 계정 보안 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SCIM의 동작 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCIM은 RESTful API를 사용하여 작동합니다. 이는 HTTP 프로토콜을 사용하여 애플리케이션 간에 데이터를 교환합니다. SCIM API는 다음과 같은 기능을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자, 그룹 및 리소스와 같은 식별자와 관련된 데이터를 가져오기&lt;/li&gt;
&lt;li&gt;사용자, 그룹 및 리소스와 같은 식별자와 관련된 데이터를 생성, 수정 및 삭제하기&lt;/li&gt;
&lt;li&gt;사용자, 그룹 및 리소스와 같은 식별자와 관련된 데이터를 검색하기&lt;/li&gt;
&lt;li&gt;SCIM API를 사용하여 클라우드 기반 서비스에서 사용자 계정을 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SCIM의 버전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 SCIM의 최신 버전은 2.0입니다. 이전 버전의 SCIM(1.0)은 사용자 계정, 그룹 및 리소스와 같은 식별자와 관련된 데이터를 관리하는 데 사용되었지만, 이는 일부 제한 사항이 있었습니다. SCIM 2.0은 이러한 한계를 극복하고, 더욱 강력하고 유연한 인증 및 권한 부여 서비스를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCIM은 클라우드 기반 인증 및 권한 부여 서비스에 대한 표준화된 방법을 제공하는 프로토콜입니다. 이는 기업의 IT 관리자들이 클라우드 기반 서비스에 대한 접근을 간소화하고, 보안을 유지하며, 사용자 데이터를 효율적으로 관리하는 데 사용됩니다. SCIM은 표준화 및 간소화된 사용자 관리, 보안 및 다양한 사용 사례에 대한 지원을 제공합니다. SCIM은 RESTful API를 사용하여 작동하며, 현재 최신 버전은 2.0입니다.&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <category>cloud</category>
      <category>SCIM</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/44</guid>
      <comments>https://chancethecoder.tistory.com/44#entry44comment</comments>
      <pubDate>Fri, 3 Mar 2023 15:56:23 +0900</pubDate>
    </item>
    <item>
      <title>Puppeteer를 활용해서 웹사이트 클릭 자동화하기</title>
      <link>https://chancethecoder.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eigZJ8/btr1VdpryC6/5VkTT4f5zRNDez8EFkNzh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eigZJ8/btr1VdpryC6/5VkTT4f5zRNDez8EFkNzh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eigZJ8/btr1VdpryC6/5VkTT4f5zRNDez8EFkNzh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeigZJ8%2Fbtr1VdpryC6%2F5VkTT4f5zRNDez8EFkNzh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;422&quot; data-filename=&quot;29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pptr.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Puppeteer&lt;/a&gt;는 Node.js 기반의 headless 브라우저 automation 도구입니다. 이를 사용하면 브라우저에서 수동으로 수행하는 작업을 자동화 할 수 있습니다. 이번 글에서는 Puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하는 방법에 대해 설명하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Puppeteer를 사용한 웹사이트 클릭 자동화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하려면 다음과 같은 단계를 따라야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;puppeteer를 설치합니다.&lt;/li&gt;
&lt;li&gt;puppeteer를 사용하여 브라우저를 엽니다.&lt;/li&gt;
&lt;li&gt;페이지를 로드합니다.&lt;/li&gt;
&lt;li&gt;원하는 요소를 찾습니다.&lt;/li&gt;
&lt;li&gt;클릭 이벤트를 트리거합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 구글 검색 페이지에서 &quot;puppeteer&quot;를 검색하고 첫 번째 결과 링크를 클릭하는 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const puppeteer = require('puppeteer');

(async () =&amp;gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('&amp;lt;https://www.google.com&amp;gt;');
  await page.type('input[name=q]', 'puppeteer');
  await page.click('input[name=btnK]');
  await page.waitForNavigation();
  const linkHandlers = await page.$x('//a[contains(text(),&quot;puppeteer.org&quot;)]');
  if (linkHandlers.length &amp;gt; 0) {
    await linkHandlers[0].click();
  } else {
    throw new Error(&quot;Link not found&quot;);
  }
  await browser.close();
})();

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 puppeteer를 사용하여 구글 검색 페이지에서 &quot;puppeteer&quot;를 검색하고 첫 번째 결과 링크를 클릭하는 방법을 보여줍니다. 이 코드에서는 puppeteer의 다양한 함수를 사용하여 브라우저 작업을 자동화합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Puppeteer를 사용한 웹사이트 클릭 자동화의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하면 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르고 정확한 자동화 - 수동으로 수행하는 작업을 자동화하면 반복적인 작업을 빠르게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;완전한 컨트롤 - puppeteer를 사용하면 브라우저에서 수행하는 작업을 정확하게 제어할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테스트 자동화 - puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하면 테스트 작업을 자동화 할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Puppeteer를 사용한 웹사이트 클릭 자동화의 예&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하는 방법을 보여주는 예시를 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 페이지 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 쇼핑몰의 로그인 페이지에서 puppeteer를 사용하여 자동으로 로그인하는 방법을 보여주는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const puppeteer = require('puppeteer');

(async () =&amp;gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('&amp;lt;https://www.example.com/login&amp;gt;');
  await page.type('input[name=email]', 'example@example.com');
  await page.type('input[name=password]', 'mypassword');
  await page.click('button[type=submit]');
  await page.waitForNavigation();
  console.log('로그인 성공');
  await browser.close();
})();

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 예제 로그인 페이지에 대한 puppeteer 코드입니다. 이 코드를 실행하면 이메일과 비밀번호를 자동으로 입력하고 로그인 버튼을 클릭하여 로그인을 수행합니다. 이를 통해 반복적인 로그인 작업을 자동화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 스크래핑 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;puppeteer를 사용하여 웹사이트에서 데이터를 스크래핑하는 방법을 보여주는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const puppeteer = require('puppeteer');

(async () =&amp;gt; {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('&amp;lt;https://www.example.com&amp;gt;');
  const title = await page.title();
  const bodyHTML = await page.evaluate(() =&amp;gt; document.body.innerHTML);
  console.log('Title:', title);
  console.log('Body:', bodyHTML);
  await browser.close();
})();

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 예제 웹사이트에서 페이지 타이틀과 HTML 본문을 추출하는 puppeteer 코드입니다. 이를 통해 웹페이지에서 데이터를 자동으로 추출하고 저장할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Puppeteer를 사용하여 웹사이트에서 클릭 이벤트를 자동화하는 방법에 대해 살펴보았습니다. 이를 통해 반복적인 작업을 자동화하고 브라우저 작업을 자세히 제어할 수 있습니다. 이를 사용하여 테스트 작업을 자동화하면 개발 프로세스를 더욱 효율적으로 만들 수 있습니다.&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <category>puppeteer</category>
      <category>자동화</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/43</guid>
      <comments>https://chancethecoder.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 3 Mar 2023 15:35:19 +0900</pubDate>
    </item>
    <item>
      <title>CentOS7에서 Docker 데몬 설치 시 무한 대기 현상 (iptable_nat 이슈)</title>
      <link>https://chancethecoder.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 CentOS7 서버에서 겪었던 Docker 설치 후 &lt;b&gt;무한 대기하는 현상&lt;/b&gt; 관련해서 공유하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TL;DR&lt;/b&gt;: 결론적으로 이런 경우 &lt;b&gt;iptable_nat&lt;/b&gt; 커널 모듈이 서버에 정상적으로 로딩되어있는지 체크하시기 바랍니다. (&lt;a href=&quot;https://ko.wikipedia.org/wiki/Modprobe#%EB%B8%94%EB%9E%99%EB%A6%AC%EC%8A%A4%ED%8A%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;modprobe&lt;/a&gt;를 통해 체크 가능) 이는 OS 레벨의 &lt;a href=&quot;https://support.huaweicloud.com/intl/en-us/tngg-kunpengcdns/kunpengcdn_05_0012.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;퍼포먼스 튜닝&lt;/a&gt;목적으로 NAT를 disable한 경우일 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 리눅스 서버에서 CLI를 통해 아래와 같이 Docker 패키지를 설치할 수 있습니다. (repository 방식)&lt;/p&gt;
&lt;pre id=&quot;code_1650119243508&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# https://docs.docker.com/engine/install/centos/#install-using-the-repository
# on root user

yum install -y yum-utils
yum-config-manager \
  --add-repo \
  https://download.docker.com/linux/centos/docker-ce.repo
 
yum install -y docker-ce docker-ce-cli containerd.io
groupadd docker
systemctl start docker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 설치 후 Docker 데몬이 정상적으로 실행 되었다면 CLI로 docker 명령어도 수행이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이상하게도 docker 명령어를 실행했을 때 아무런 반응이 없고 무한정 기다리는 상황이 발생했습니다. 따라서 Docker 데몬에 이상이 있는지 확인하기 위해 systemd의 status 로그를 확인해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;screenshot01.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byvvA8/btrzxyXaPUg/Uq5OHMlrJsKGX1QnkXugWK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byvvA8/btrzxyXaPUg/Uq5OHMlrJsKGX1QnkXugWK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byvvA8/btrzxyXaPUg/Uq5OHMlrJsKGX1QnkXugWK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyvvA8%2FbtrzxyXaPUg%2FUq5OHMlrJsKGX1QnkXugWK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2440&quot; height=&quot;590&quot; data-filename=&quot;screenshot01.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Active 항목이 activating (start)로 나타나고, 명령어 중 dockerd 위에 iptables 명령어가 수행되고 있는 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적이라면 Active 항목에 active (running)으로 초록색 불이 들어와야 하는데, 아마도 어떤 원인(iptables 명령어로 의심)으로 인해 hanging 상태에서 진행되지 않고 있는 것이라고 추측, 자세한 상황을 파악하기 시작했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 systemd 로그에서 보이는 iptables의 명령어를 그대로 실행했고, 실제로 iptables 명령어 자체가 진행되지 않고 멈춰있는 것을 볼 수 있었습니다. 또한, iptables 키워드로 프로세스를 grep해보니 아래와 같이 조회되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;screenshot02.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D6OPX/btrzvzI24cS/LKIYNzOCysA0qkRxGnkzb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D6OPX/btrzvzI24cS/LKIYNzOCysA0qkRxGnkzb1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D6OPX/btrzvzI24cS/LKIYNzOCysA0qkRxGnkzb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD6OPX%2FbtrzvzI24cS%2FLKIYNzOCysA0qkRxGnkzb1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2440&quot; height=&quot;640&quot; data-filename=&quot;screenshot02.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;modprobe -q -- iptable_nat라는 명령어가 다수 보여, 해당 명령어를 조사해보기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/Modprobe#%EB%B8%94%EB%9E%99%EB%A6%AC%EC%8A%A4%ED%8A%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;modprobe&lt;/a&gt; 문서를 통해 일부 커널 모듈의 블랙리스트 지정이 가능하다는 것을 알았고, iptable_nat라는 모듈이 블랙리스트에 들어있다는 사실이 확인 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;screenshot03.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blSwUX/btrzBrvqeiz/15MKGgohH2saRBfsFnT1uk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blSwUX/btrzBrvqeiz/15MKGgohH2saRBfsFnT1uk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blSwUX/btrzBrvqeiz/15MKGgohH2saRBfsFnT1uk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblSwUX%2FbtrzBrvqeiz%2F15MKGgohH2saRBfsFnT1uk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2440&quot; height=&quot;840&quot; data-filename=&quot;screenshot03.jpg&quot; data-origin-width=&quot;2440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 NAT를 사용하지 않는 리눅스 서버에서 퍼포먼스 튜닝을 목적으로 구성한 것으로 추측됩니다. (&lt;a href=&quot;https://support.huaweicloud.com/intl/en-us/tngg-kunpengcdns/kunpengcdn_05_0012.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 문서&lt;/a&gt; 참고)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 정상적으로 수행하기 위해서는 NAT 구성이 필수적으로 필요합니다. 따라서, modprobe의 blacklist에 설정되어있는 iptable_nat 관련 값들을 제거하고, 리눅스 서버를 재기동 함으로써 iptable_nat 모듈의 로딩을 수행해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, docker에서 NAT를 왜 사용해야만 하는지 &lt;a href=&quot;https://docs.docker.com/network/iptables/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 통해 확인 가능합니다. (호스트와 docker의 네트워크 격리를 위함이라고 함)&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <category>docker</category>
      <category>iptables</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/42</guid>
      <comments>https://chancethecoder.tistory.com/42#entry42comment</comments>
      <pubDate>Sun, 17 Apr 2022 00:47:46 +0900</pubDate>
    </item>
    <item>
      <title>Fluentd에서 Azure Event Hubs 연동하기</title>
      <link>https://chancethecoder.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파일 데이터를 실시간으로 Azure Event Hub로 전송하기위한 Fluentd 파이프라인 구성 방법 및 주의사항을 다뤄보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 파이프라인 이해를 위해서 필요한 사전지식을 아래에 정리했으니 기본적인 사항을 숙지하고 구성하시기 바랍니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AEH(Azure Event Hubs)란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.microsoft.com/ko-kr/azure/event-hubs/event-hubs-about&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Azure Event Hubs&lt;/a&gt;는 빅데이터의 실시간 스트리밍 처리를 위한 Azure 클라우드 제품 중 하나입니다. AEH는 Apache Kafka의 Managed service라고 볼 수 있습니다. 실제로 카프카 프로토콜을 지원하여 동일한 방식으로 인터페이스 가능하며 이 외의 다른 연동 방법도 지원합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AEH SAS(Shared Access Signature)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AEH는 기본적으로 신뢰 가능한 통신을 수행합니다. 이를 위해 수신자는 유효 자격을 증명해야 하며, 이에 필요한 Key를 제공하여 증명하는 방식이 &lt;a href=&quot;https://docs.microsoft.com/ko-kr/azure/event-hubs/authenticate-shared-access-signature&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SAS(Shared Access Signature)&lt;/a&gt;입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fluentd 플러그인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fluentd는 사용자가 원하는 기능을 추가할 수 있도록 플러그인 방식을 채택하고 있습니다. Source(입력), Filter(처리), Output(출력) 구조에서 각 단계에 원하는 플러그인을 적용할 수 있으며, 플러그인을 직접 개발도 가능하도록 되어있습니다. Fluentd는 출력 플러그인으로 &lt;a href=&quot;https://github.com/fluent/fluent-plugin-kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kafka 플러그인&lt;/a&gt;, &lt;a href=&quot;https://github.com/htgc/fluent-plugin-azureeventhubs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AEH 플러그인&lt;/a&gt;을 지원하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 AEH 플러그인은 공식 지원이 아닌 개인이 개발한 것이므로 본문에서는 공식 오픈소스인 Kafka 플러그인을 통해 구성해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fluentd 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는 서버 환경에 따라 다르므로 모든 케이스를 커버하지 않겠습니다. 여기서 작성하는 것은 CentOS 7+ 기준입니다. &lt;a href=&quot;https://docs.fluentd.org/installation/install-by-rpm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;설치 방법&lt;/a&gt;은 다양하지만 그 중 td-agent를 통해 설치해보겠습니다. (fluentd의 배포판 패키지)&lt;/p&gt;
&lt;pre id=&quot;code_1643507704915&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# td-agent 설치 (root 계정 혹은 sudo 권한 필요)
curl -L https://toolbelt.treasuredata.com/sh/install-redhat-td-agent4.sh | sh

# systemd에서 로그파일 경로 변경
vi /usr/lib/systemd/system/td-agent.service
# Environment=TD_AGENT_LOG_FILE 항목을 원하는 경로로 변경
systemctl daemon-reload

# td-agent 설정 변경
vi /etc/td-agent/td-agent.conf

# td-agent 재기동
systemctl restart td-agent&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fluentd 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fluentd 설정은 /etc/td-agent 경로에 있는 td-agent.conf 파일에서 할 수 있습니다. 아래는 기본적인 File to AEH 예시입니다. 주요 필드에 대한 설명은 주석으로 달았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643509095932&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;source&amp;gt;
  @type tail
  # 수집하기위한 파일의 경로를 지정합니다
  path /path/to/target/*.log
  
  # 현재까지 읽어들인 파일의 경로, 오프셋, inode 정보를 저장하는 파일입니다
  pos_file /path/to/data/pos/stream.default.pos
  
  # 프로세스 재기동이 없다면 pos 파일은 증분으로 커지기만 하는데, 이를 주기적으로 압축 정리하기위한 주기를 설정합니다
  pos_file_compaction_interval 24h
  follow_inodes true
  tag logs.stream.default

  &amp;lt;parse&amp;gt;
    @type regexp
    
    # 정규표현식을 통해 원하는 필드를 추출합니다. 예제는 파이프(|) 문자를 기준으로 logid, data 필드를 읽어들입니다
    expression /^(?&amp;lt;logid&amp;gt;[^\|].*?)\|(?&amp;lt;data&amp;gt;.*?)$/
    
    # 파싱에 성공하면 각 필드에 해당하는 값의 타입을 지정해줍니다
    types logid:string,data:string
  &amp;lt;/parse&amp;gt;
&amp;lt;/source&amp;gt;

&amp;lt;match logs.stream.**&amp;gt;
  @type kafka2

  # 브로커 정보에 이벤트허브 주소를 넣어줍니다. 9093 포트를 입력해야 합니다
  brokers &amp;lt;your-namespace&amp;gt;.servicebus.windows.net:9093

  # 브로커에 produce할 토픽 정보를 지정하거나, 이를 지정하지 않은 경우 기본값으로 전송할 default 토픽 명을 지정해줍니다
  topic_key logid
  default_topic default-message
 
  &amp;lt;format&amp;gt;
    @type json
  &amp;lt;/format&amp;gt;

  # 버퍼 사이즈에 따라 카프카로 배치 전송하는 메세지 사이즈가 결정됩니다
  &amp;lt;buffer logid&amp;gt;
    @type file
    path /path/to/data/buffer/kafka
    
    # 버퍼에서 flush된 chunk는 곧바로 출력 queue로 들어가는데, flush를 수행할 주기를 설정합니다
    flush_interval 3s
    chunk_limit_size 800KB
  &amp;lt;/buffer&amp;gt;

  # 카프카 전송이 실패했을 때 해당 메세지를 어디로 전달할지 지정합니다
  &amp;lt;secondary&amp;gt;
    @type file
    path /path/to/data/failed/records
  &amp;lt;/secondary&amp;gt;
 
  # ruby-kafka producer options
  max_send_retries 1
  required_acks -1
 
  ssl_ca_certs_from_system true

  # Azure 포탈의 이벤트허브 탭에서 아래 정보를 확인하여 넣어줍니다
  username &quot;$ConnectionString&quot;
  password &quot;Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName={SHARED.ACCESS.KEY.NAME};SharedAccessKey={SHARED.ACCESS.KEY}&quot;
&amp;lt;/match&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 연동 과정 및 운영에서 발생할 수 있는 이슈와 해결 방안입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메세지 전송 시 Could not connect to broker 에러&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AEH 브로커로 메세지 전송 시 실패 및 전송을 반복하면서 아래와 같은 에러 로그를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1643511148669&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Could not connect to broker your-namespace.servicebus.windows.net:9093 (node_id=0): Connection error Errno::ECONNRESET: Connection reset by peer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 원인 및 해결 방안은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 설정이 잘못된 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AEH 연결이 잘 되는지 &lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://chancethecoder.tistory.com/28&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kafkacat&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt; 명령어를 통해 확인해볼 수 있습니다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;만약 연결이 잘 된다면 아래의 경우를 의심해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AEH 전송 Limit 초과한 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/azure/event-hubs/apache-kafka-troubleshooting-guide#limits&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Azure Event Hubs 내부 Limit&lt;/a&gt;에 따른 전송 한계를 초과한 경우입니다. AEH는 &lt;b&gt;단일 producer&lt;/b&gt;가 &lt;b&gt;초당 1MB&lt;/b&gt; 이상의 메세지를 전송한 경우 강제로 연결을 거부하게 됩니다. 이 때는 버퍼 사이즈를 낮춰서 배치 전송 당 메세지 사이즈를 줄여야 합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;카프카로 전송할 데이터의 양이 초당 1MB를 넘어갈 경우 &lt;a href=&quot;https://docs.fluentd.org/deployment/multi-process-workers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;멀티프로세스 워커&lt;/a&gt;를 통한 멀티 producer를 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fluentd에서의 Kafka 전송 실패 처리 로직&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fluentd는 output에서 전송에 실패할 경우 자동으로 재전송을 수행하도록 retry 로직을 내장하고 있습니다. retry 로직은 기본적으로 시도 횟수 상한, 시도에 대한 interval을 파라미터로 가지고 있으며, interval은 1초부터 매 횟수마다 2배씩 증가하도록 되어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 처음 전송 후 실패하게 되면 1초 뒤 재전송을 시도하고, 이에 실패하면 다시 2초 뒤에 재전송, 실패 시 4초 뒤 재전송 하는 방식으로 늘어나게 됩니다. 이는 무한정 재시도 하도록 옵션을 주지 않는 이상 &lt;b&gt;최대 17회&lt;/b&gt;까지 재전송을 시도하게 되어있습니다. 이러한 값들은 전부 설정 파일에서 옵션 값으로 변경 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;source 데이터의 증가 속도나 서버 내 잔여 디스크 사이즈 등 환경적인 부분을 고려해서 적절한 값을 선택하여 설정하는 것이 운영 상 중요할 수 있습니다.&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/41</guid>
      <comments>https://chancethecoder.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 1 Feb 2022 00:03:55 +0900</pubDate>
    </item>
    <item>
      <title>브라우저에 주소를 입력하면 어떻게 웹사이트 화면이 그려질까?</title>
      <link>https://chancethecoder.tistory.com/40</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;브라우저 주소창에 뭔가 입력하면&amp;nbsp;웹사이트가 나오는데, 어떻게 동작할까요? 그 과정을 아는대로 전부 설명해주세요&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;입사 면접에서 이런 질문을 받았던 적이 있습니다. &lt;/span&gt;정말&lt;span&gt; &lt;/span&gt;대답하기&lt;span&gt; &lt;/span&gt;막막한&lt;span&gt; &lt;/span&gt;질문이라는&lt;span&gt; &lt;/span&gt;생각이&lt;span&gt; &lt;/span&gt;스치는 찰나에&lt;span&gt;, &lt;/span&gt;그래도&lt;span&gt;&amp;nbsp;잘 대답해야겠다는 생각에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어떻게든&lt;span&gt; &lt;/span&gt;아는&lt;span&gt; &lt;/span&gt;것을&lt;span&gt; &lt;/span&gt;설명했습니다&lt;span&gt;. 나름 막힘 없이 설명했지만 중간에 끊고 면접관이 재차 물어본 질문에 당황하여 대답을 정확하게 하지 못했습니다. 그 질문의 정답은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Handshaking&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3-Way Handshake&lt;/a&gt;였는데, 왜 이게 생각이 안났지 싶으면서도 면접에서 떨어진 이후로 계속해서 기억에 남는 질문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 이걸 다시 블로그에 정리해야겠다는 생각이 들었습니다. 정리하는 하는 이유는 숙제처럼 생각했던 문제 복기를 지금이라도 해보고, 이후에도 면접자 혹은 면접관으로서 이와 비슷한 상황에 잘 대답하기 위해서입니다. 특히 이번에는 쉽게 설명하는 것에 초점을 맞춰보려고 합니다. 어떻게 동작하는지 아무것도 모르는 사람에게 이해시킨다는 마음으로 설명해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹사이트 검색 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 웹사이트 검색 과정을 봅시다. 과정은 아래와 크게 다르지 않을겁니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴퓨터(핸드폰)를 킨다&lt;/li&gt;
&lt;li&gt;브라우저를 실행한다&lt;/li&gt;
&lt;li&gt;브라우저 주소창에 &lt;i&gt;google.com&lt;/i&gt; 입력한다&lt;/li&gt;
&lt;li&gt;엔터를 누른다&lt;/li&gt;
&lt;li&gt;조금 기다리면 구글 웹사이트가 출력된다&lt;/li&gt;
&lt;li&gt;웹사이트 내의 기능을 이용한다 (메일 확인, 구글 검색 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인에서 아이피로 치환&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ICAN-Infographic---DNS-Query_Large.gif&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0J0k/btrqLUhm56g/lzysYEqiXxIgAoBMCaDprk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0J0k/btrqLUhm56g/lzysYEqiXxIgAoBMCaDprk/img.gif&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://whois.icann.org/en/dns-and-whois-how-it-works&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0J0k/btrqLUhm56g/lzysYEqiXxIgAoBMCaDprk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/w0J0k/btrqLUhm56g/lzysYEqiXxIgAoBMCaDprk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;388&quot; data-filename=&quot;ICAN-Infographic---DNS-Query_Large.gif&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://whois.icann.org/en/dns-and-whois-how-it-works&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 컴퓨터 혹은 핸드폰을 키고 브라우저를 실행한 상태라고 하겠습니다. 브라우저 주소창에 &lt;i&gt;google.com&lt;/i&gt; 값을 입력하는데, 이를 &lt;a href=&quot;https://en.wikipedia.org/wiki/Domain_name&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;도메인 네임(웹 주소)&lt;/a&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 요청이 인터넷 통신을 거쳐 서버까지 도달하기 위해서는 도메인이&amp;nbsp;&lt;a href=&quot;https://en.wikipedia.org/wiki/IP_address&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;아이피&lt;/a&gt;라는 형태로 변환되어야 하는데요, 변환을 수행해주는 것이 DNS(Domain Name Server) 서버입니다. 즉, 우리는 항상 구글이나 네이버같은 서버와 통신하기 이전에 DNS 서버를 먼저 거치고 아이피를 알아낸다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 상황이라면 DNS를 통해 구글 서버의 아이피 값을 알아내는데 성공했을 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목적지 찾아가기 (라우팅)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;routing-diagram.svg&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tHEZY/btrqTZtZAf6/K3tExWMyWxfrA79LBCqqp1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tHEZY/btrqTZtZAf6/K3tExWMyWxfrA79LBCqqp1/tfile.svg&quot; data-alt=&quot;출처: https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-routing/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tHEZY/btrqTZtZAf6/K3tExWMyWxfrA79LBCqqp1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtHEZY%2FbtrqTZtZAf6%2FK3tExWMyWxfrA79LBCqqp1%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;251&quot; height=&quot;150&quot; data-filename=&quot;routing-diagram.svg&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-routing/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 내 컴퓨터가 인터넷 너머에 존재하는 구글 서버와 통신하려면 실제 서버의 위치까지 내 통신 신호가 도달해야합니다. 내 컴퓨터의 통신 신호가 서버까지 찾아가는 과정을 &lt;a href=&quot;https://en.wikipedia.org/wiki/Routing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;라우팅&lt;/a&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 서울에 있고 서버가 로스엔젤레스에 있다고 치면 그 사이에 통신 신호를 전파해줄 수 있는 수많은 장비들이 배치되어 있습니다. 이를 라우터라고 부릅니다. 내 컴퓨터에서 발생한 통신 신호는 서울에 위치한 라우터를 거치고 중간 과정을 거쳐서 미국 로스엔젤레스에 있는 라우터까지 전달됩니다. 그리고 최종적으로 서버의 엣지에 해당하는 라우터에서 서버로 통신 신호를 전달하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 중요한 사실은 하나의 라우터가 고장나더라도 다른 라우터를 통해 길을 우회할 수 있도록 복잡한 메쉬 형태로 구성되어 있다는 것입니다. 이런 특성 때문에 인터넷 통신에서는 같은 목적지라 해도 매번 도달하는 경로가 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 라우터가 다음 라우터로 신호를 전달할 때 어떤 경로를 선택 하느냐가 인터넷 통신 속도에 영향을 미치며, 최적의 방법으로 목적지까지 도달할 수 있는 길이 무엇인지 알아내는 방법이 중요해집니다. 이와 관련해 문제를 해결하기 위한 다양한 라우팅 알고리즘이 이미 구현되어 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버와 상호작용 하기 (HTTP 프로토콜)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅을 통해 구글 서버와 성공적으로 통신했습니다. 이제 어떻게 서버와 내 브라우저(클라이언트)가 상호작용 하는지에 대해 알아야 합니다. 여기서 우리는 두 가지를 알아야 합니다. 바로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Client%E2%80%93server_model&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;클라이언트-서버 모델&lt;/a&gt;과 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HTTP 프로토콜&lt;/a&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Basic Static App Server.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh8KSW/btrqNfdNleI/wDNhrOgK0eKQMEfpNjrxa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh8KSW/btrqNfdNleI/wDNhrOgK0eKQMEfpNjrxa0/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://developer.mozilla.org/ko/docs/Learn/Server-side/First_steps/Client-Server_overview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh8KSW/btrqNfdNleI/wDNhrOgK0eKQMEfpNjrxa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh8KSW%2FbtrqNfdNleI%2FwDNhrOgK0eKQMEfpNjrxa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;267&quot; data-filename=&quot;Basic Static App Server.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://developer.mozilla.org/ko/docs/Learn/Server-side/First_steps/Client-Server_overview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트-서버 모델은 거창한 것이 아닙니다. 이미 우리가 이해하고있는 서버와 이를 접속해서 서비스를 이용하는 우리들, 그리고 접속에 필요한 네트워크망의 관계를 생각하면 됩니다. 오히려 중요한 것은 HTTP 프로토콜을 통해 클라이언트-서버 모델을 구현한 것이 브라우저와 서버가 상호작용하는 방식이라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지 화면을 그리기 위해서는 HTML, CSS, Javascript 같은 정보들을 서버로부터 받아야 합니다. (이게 무슨 역할을 하는지 여기서는 알 필요 없습니다) 브라우저는 이러한 리소스를 서버로부터 받고자 구글에 요청하게 되는데, 그 때 사용하는 것이 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HTTP 요청&lt;/a&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청은 위에서 알아본 라우팅 과정을 거쳐 구글 서버에 도달하고, 구글 서버는 HTTP 요청를 받아서 그에 대응하는 HTTP 응답을 보내고, 이 것이 라우팅을 통해 다시 클라이언트에 도달하게 되면 응답으로부터 HTML 정보를 읽어서 브라우저가 화면에 그려주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 우리가 HTTP 요청을 언제 했을까요? &lt;b&gt;사실 도메인 주소를 입력하는 행위 자체가 HTTP 요청을 생성&lt;/b&gt;한것과 똑같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요청 결과 그리기 (브라우저 렌더링)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로부터 HTTP 응답을 받았으면 이제 브라우저의 할 일만 남았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 응답에는 다양한 리소스가 포함되는데요, 그 중 화면을 그리는데 필요한 정보인 HTML, CSS, Javascript같은 리소스를 받았다면 이를 해석해서 적절하게 화면에 그려주는 역할을 합니다. 이런 과정을 &lt;b&gt;렌더링&lt;/b&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Browser_engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;렌더링 엔진&lt;/a&gt;이라는 컴포넌트가 담당하는데요, 브라우저 종류에 따라 내장된 엔진이 각각 다르고 성능 또한 천차만별입니다. 심지어 같은 코드인데도 렌더링 엔진이 달라지면 해석이 달라져서 브라우저에 따라서 개발자의 의도와 다르게 동작할 수 있습니다. 이런 문제를 &lt;b&gt;크로스 브라우저 이슈&lt;/b&gt;라고 이야기 하며, 이를 해결하기 위한 다양한 방법들을 적용하기도 합니다.&lt;/p&gt;</description>
      <category>기타</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/40</guid>
      <comments>https://chancethecoder.tistory.com/40#entry40comment</comments>
      <pubDate>Sun, 16 Jan 2022 13:39:27 +0900</pubDate>
    </item>
    <item>
      <title>Databricks Terraform 실행 시 MALFORMED_REQUEST, IAM Role 에러 현상 및 해결 방법</title>
      <link>https://chancethecoder.tistory.com/39</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Terraform Databricks Provider: 0.4.3&lt;/li&gt;
&lt;li&gt;Terraform AWS Provider: 3.38&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현상&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Terraform으로 Databricks 환경을 구성할 때 아래와 같은 에러가 발생합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Error:&amp;nbsp;MALFORMED_REQUEST:&amp;nbsp;Failed&amp;nbsp;credentials&amp;nbsp;validation&amp;nbsp;checks:&amp;nbsp;Spot&amp;nbsp;Cancellation,&amp;nbsp;Create&amp;nbsp;Placement&amp;nbsp;Group,&amp;nbsp;Delete&amp;nbsp;Tags,&amp;nbsp;Describe&amp;nbsp;Availability&amp;nbsp;Zones,&amp;nbsp;Describe&amp;nbsp;instances,&amp;nbsp;Describe&amp;nbsp;Instance&amp;nbsp;Status,&amp;nbsp;Describe&amp;nbsp;Placement&amp;nbsp;Group,&amp;nbsp;Describe&amp;nbsp;Route&amp;nbsp;Tables,&amp;nbsp;Describe&amp;nbsp;Security&amp;nbsp;Groups,&amp;nbsp;Describe&amp;nbsp;Spot&amp;nbsp;Instances,&amp;nbsp;Describe&amp;nbsp;Spot&amp;nbsp;Price&amp;nbsp;History,&amp;nbsp;Describe&amp;nbsp;Subnets,&amp;nbsp;Describe&amp;nbsp;Volumes,&amp;nbsp;Describe&amp;nbsp;Vpcs,&amp;nbsp;Request&amp;nbsp;Spot&amp;nbsp;Instances&lt;br /&gt;(400&amp;nbsp;on&amp;nbsp;/api/2.0/accounts/{UUID}/workspaces)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Terraform으로 Databricks 내의 Log Delivery 세팅을 구성할 때 아래와 같은 에러가 발생합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Error: cannot create mws log delivery: Can not assume iamrole:arn:aws:iam::{your_aws_account_id}:role/databricks_log_delivery_iam_role. Please add Databricks Log Delivery Role to your IAM Role's trust relationship as specified in API docs. Using basic auth: host=&lt;a href=&quot;https://accounts.cloud.databricks.com,&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://accounts.cloud.databricks.com,&lt;/a&gt;&amp;nbsp;username=[MASKED],&amp;nbsp;password=***REDACTED***&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform AWS Provider(v3.28에서 발견)의 버그로 인해 Databricks AWS 교차 계정 정책 (cross account policy) 생성 및 IAM role 연결이 Terraform에 대한 AWS 요청 확인보다 더 오래 걸리면서 발생하는 에러입니다. Terraform에서 Workspace를 생성하는 과정에서 아직 정책이 적용되지 않았기 때문에 자격 증명에 대한 유효성 검사가 실패합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Log Delivery 세팅을 구성할 때 필요한 IAM role 구성에서도 마찬가지로 생성 및 연결에 걸리는 시간이 오래 걸리면서 이후의 유효성 검사에 실패한 경우입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 aws_iam_role 리소스 생성 시 timesleep을 추가합니다. (Log Delivery의 경우 Apply를 재수행만 해도 해결 가능할 수 있습니다.)&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;resource &quot;time_sleep&quot; &quot;wait&quot; {
  depends_on = [
  aws_iam_role.cross_account_role]
  create_duration = &quot;20s&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/databrickslabs/databricks/0.4.2/docs/guides/aws-workspace#credentials-validation-checks-errors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://registry.terraform.io/providers/databrickslabs/databricks/0.4.2/docs/guides/aws-workspace#credentials-validation-checks-errors&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍</category>
      <category>AWS</category>
      <category>Databricks</category>
      <category>terraform</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/39</guid>
      <comments>https://chancethecoder.tistory.com/39#entry39comment</comments>
      <pubDate>Sat, 15 Jan 2022 13:40:31 +0900</pubDate>
    </item>
    <item>
      <title>Medallion 아키텍처란?</title>
      <link>https://chancethecoder.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Medallion 아키텍처란 Databricks에서 제시하는 데이터 파이프라인 모델로 Delta Lake와 함께&amp;nbsp;&lt;a href=&quot;https://en.wikipedia.org/wiki/Change_data_capture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; CDC(Change Data Capture)&lt;/a&gt; 방식의 데이터 웨어하우스 구성 방법을 제시합니다. &lt;a href=&quot;https://docs.databricks.com/delta/delta-change-data-feed.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CDF(Change Data Feed)&lt;/a&gt; 기능을 사용하면 더욱 쉽게 구현 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;medallion-architecture.jpeg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FcrFM/btrnBMfnLHi/pB0XBpx91R9LvrI5dvk5m0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FcrFM/btrnBMfnLHi/pB0XBpx91R9LvrI5dvk5m0/img.jpg&quot; data-alt=&quot;출처:&amp;amp;amp;amp;amp;nbsp;https://databricks.com/notebooks/delta-lake-cdf.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FcrFM/btrnBMfnLHi/pB0XBpx91R9LvrI5dvk5m0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFcrFM%2FbtrnBMfnLHi%2FpB0XBpx91R9LvrI5dvk5m0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; data-filename=&quot;medallion-architecture.jpeg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;amp;amp;nbsp;https://databricks.com/notebooks/delta-lake-cdf.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 데이터 캡쳐 (Change Data Capture)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Medallion 아키텍처를 살펴보기 전에 CDC라는 개념을 살펴봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC는 어떤 데이터소스의 데이터가 변경 되었을 때, 이를 감지하고 이에 필요한 후속 조치를 할 수 있게 자동화하는 방식의 설계 혹은 메커니즘을 이야기 합니다. CDC 매커니즘이 적용된 데이터 파이프라인에서는 원천 데이터 소스의 변경을 타겟에 반영하는 것이 매우 자연스럽고 자동화된 방식으로 이뤄지기 때문에 효과적으로 파이프라인 관리가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Delta Lake에서는 CDC를 구현하기 위해 내부적으로 별도의 메타 데이터들을 파일로 저장하고 관리합니다. 그리고 CDC를 사용자가 쿼리 수준으로 간단하게 관리하기 위한 방법으로 Change Data Feed 기능을 제공합니다. 자세한 내용은 &lt;a href=&quot;https://docs.databricks.com/delta/delta-change-data-feed.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 참고하시기 바랍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3 Layer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Medallion 아키텍처는 아래의 세 가지 레이어를 가지고 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Bronze: Raw Ingestion Table&lt;/li&gt;
&lt;li&gt;Silver: Refined Table&lt;/li&gt;
&lt;li&gt;Gold: Feature/Aggregation Table&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BRONZE&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 소스(External feeds, CDC output, Extracts)로부터 Raw 데이터를 가져와서 Bronze 테이블로 적재합니다. Delta 테이블이기 때문에 스키마 변경에 유연하게 대응할 수 있고 CDF 기능을 활용하면 증분 데이터에 대한 히스토리 정보도 확인 가능합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SILVER&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bronze 테이블로부터 특정 필드값에 대한 filter를 통해 새로운 Silver 테이블에 적재합니다. Schema가 고정된 정형 데이터를 가지고 있기 때문에 머신러닝에 활용 가능합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GOLD&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Silver 테이블로부터 Aggregation을 수행하여 Gole 테이블에 적재합니다. Business Level로 정재된 데이터를 포함하기 때문에 BI(Business Intelligence) 도구와 연동하여 시각화에 활용 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://databricks.com/blog/2021/06/09/how-to-simplify-cdc-with-delta-lakes-change-data-feed.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://databricks.com/blog/2021/06/09/how-to-simplify-cdc-with-delta-lakes-change-data-feed.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/databricks/change-data-feed-in-delta&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.slideshare.net/databricks/change-data-feed-in-delta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.databricks.com/delta/delta-change-data-feed.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.databricks.com/delta/delta-change-data-feed.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://databricks.com/notebooks/delta-lake-cdf.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://databricks.com/notebooks/delta-lake-cdf.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/azure/databricks/delta/delta-change-data-feed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.microsoft.com/en-us/azure/databricks/delta/delta-change-data-feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍</category>
      <category>CDC</category>
      <category>Databricks</category>
      <category>Medallion Architecture</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/38</guid>
      <comments>https://chancethecoder.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 12 Dec 2021 13:23:50 +0900</pubDate>
    </item>
    <item>
      <title>크롬 확장 프로그램 제작 맛보기 - 날아다니는 밈</title>
      <link>https://chancethecoder.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 최근 학습 겸 재미 삼아 만들어본 &lt;a href=&quot;https://chrome.google.com/webstore/detail/flying-meme-animator/flmnecgbkcpalbenlbdfljhepngbdhdp?hl=ko&amp;amp;authuser=0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;날아다니는 밈&lt;/span&gt;&lt;/a&gt;의 제작 과정을 살펴보면서 크롬 확장 프로그램 제작 및 등록 방법을 알아보겠습니다. 본인의 프로그램을 제작하실 때 가이드라인 정도로 봐주시기 바랍니다. (※ 주의 : 저의 앱은 쓸모 있는 용도가 아니라 학습을 목적으로 만든 것입니다!)&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;날아다니는 밈&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;날아다니는 밈 재생기입니다.&quot; data-og-host=&quot;chrome.google.com&quot; data-og-source-url=&quot;https://chrome.google.com/webstore/detail/flying-meme-animator/flmnecgbkcpalbenlbdfljhepngbdhdp?hl=ko&amp;amp;authuser=0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EFZyt/hyMokcxeQz/mnY8D0mJcl3GO4ktNNYYkk/img.jpg?width=128&amp;amp;height=128&amp;amp;face=7_38_70_101&quot; data-og-url=&quot;https://chrome.google.com/webstore/detail/flying-meme-animator/flmnecgbkcpalbenlbdfljhepngbdhdp&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/flying-meme-animator/flmnecgbkcpalbenlbdfljhepngbdhdp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chrome.google.com/webstore/detail/flying-meme-animator/flmnecgbkcpalbenlbdfljhepngbdhdp?hl=ko&amp;amp;authuser=0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EFZyt/hyMokcxeQz/mnY8D0mJcl3GO4ktNNYYkk/img.jpg?width=128&amp;amp;height=128&amp;amp;face=7_38_70_101');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;날아다니는 밈&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;날아다니는 밈 재생기입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chrome.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시작에 앞서 : 꼭 해봐야만 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬 확장 프로그램을 만들어보는 것은 선택 사항입니다. 본인이 판단해서 필요하다고 생각하거나, 그냥 재미삼아 해보고싶으면 따라서 해보면 됩니다. 다만, 다음과 같은 사람에게는 도움이 될 거라고 생각합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 개발 언어(HTML, CSS, Javascript)를 공부하고싶은 사람&lt;/li&gt;
&lt;li&gt;자기만의 프로덕트를 간단하게 만들고 배포해보고싶은 사람&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 만드는데 필요한 지식은 많지 않습니다. 본 포스트의 내용을 참고하여 본인만의 크롬 확장 프로그램을 개발해보고, 이를 마켓플레이스에 등록까지 해보시길 바라겠습니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그램 기획&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연한 이야기지만 프로그램을 만들기 전에 기획을 먼저 해야 합니다. 본인이 만들고자 하는 프로그램의 구상이 있다면, 이를 충분히 구체화시켜 오류가 없도록 미리 준비하시기 바라겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNBHQt/btrjeuwLFtj/Ehajf4l7gPtPxVD5dq7EB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNBHQt/btrjeuwLFtj/Ehajf4l7gPtPxVD5dq7EB0/img.png&quot; data-alt=&quot;출처: https://giphy.com/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNBHQt/btrjeuwLFtj/Ehajf4l7gPtPxVD5dq7EB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNBHQt%2FbtrjeuwLFtj%2FEhajf4l7gPtPxVD5dq7EB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;712&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;1806&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://giphy.com/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 일명 댄싱 도지라고 하는 해외 밈에 착안했습니다. 재밌는 GIF를 Giphy로부터 가져와서 백그라운드에 저장하고, 브라우저 내에서 발생하는 키보드 및 클릭 이벤트를 감지해서 현재 보이는 영역의 랜덤한 위치에 짧게 띄워주고 사라지는 것을 생각했습니다. (네, 정말 쓸데없는 기능입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 기능 및 설명은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;밈 팝업 : 타이핑 or 클릭 이벤트에 맞춰서 GIF를 브라우저 영역에 표시&lt;/li&gt;
&lt;li&gt;밈 변경 : 버튼을 누르면 Giphy API를 활용해 GIF 주소 정보를 백그라운드에 저장&lt;/li&gt;
&lt;li&gt;on/off 스위치 : GIF를 보여줄지 안 보여줄지 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;크롬 확장 프로그램 구조 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구현을 하기 위해 &lt;a href=&quot;https://developer.chrome.com/docs/extensions/mv3/architecture-overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;크롬 확장 프로그램의 구조&lt;/a&gt;를 알아봐야 합니다. 이미 공식 문서에 내용은 잘 나와 있지만, 전부 영어이므로 한글로 간략하게 정리해보겠습니다. 프로젝트 내부의 주요 구성 요소는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chrome-extension-architecture.png&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM2e5E/btrlFJRhXUX/niBfKr78eM32toezVggGZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM2e5E/btrlFJRhXUX/niBfKr78eM32toezVggGZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM2e5E/btrlFJRhXUX/niBfKr78eM32toezVggGZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM2e5E%2FbtrlFJRhXUX%2FniBfKr78eM32toezVggGZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;261&quot; data-filename=&quot;chrome-extension-architecture.png&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;manifest.json
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프로그램의 메타 정보를 저장하는 필수 파일입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;popup.html
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;우상단 툴바 아이콘을 클릭했을 때 나타나는 팝업입니다.&lt;/li&gt;
&lt;li&gt;최소한의 필요한 기능만을 포함한 간결한 UI를 만드는 것을 권장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;popup.js
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;popup.html 뷰에서 발생하는 이벤트를 제어하기위한 자바스크립트입니다.&lt;/li&gt;
&lt;li&gt;background.js, contentscript.js와 메세지를 주고받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;background.js
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프로그램의 전체적인 이벤트 핸들러입니다. 이벤트가 발생하면 특정 코드를 수행합니다.&lt;/li&gt;
&lt;li&gt;이벤트가 발생하는 시점에만 수행되고 나머지 시간에는 유휴상태로 있도록 만드는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;탭 갯수와 상관없이 백그라운드는 한 개만 존재합니다.&lt;/li&gt;
&lt;li&gt;popup.js, contentscript.js와 메세지를 주고받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;contentscript.js
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에 로드된 웹페이지 영역에서 동작하는 자바스크립트입니다.&lt;/li&gt;
&lt;li&gt;DOM을 활용해서 페이지 내의 요소를 추가, 변경, 삭제할 수 있습니다.&lt;/li&gt;
&lt;li&gt;각 탭 마다 1회 로드됩니다.&lt;/li&gt;
&lt;li&gt;background.js, popup.js와 메세지를 주고받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코딩하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드를 살펴보겠습니다. 원본 소스코드는 &lt;a href=&quot;https://github.com/Team-Billionaires/flying-meme-animator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github 레포지토리&lt;/a&gt;를 참고하시기 바랍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. manifest.json 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 해야할 것은 manifest.json 파일을 작성하는 일입니다. 파일에는 제목, 제작자 이름, 설명, 권한 등의 정보가 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 프로그램에 불필요한 권한이 포함되지 않도록 주의해야 합니다. 사용하지 않는 권한을 요청했을 경우 마켓 등록 단계에서 거절될 확률이 있으며 거절되지 않더라도 요청에 대한 사용 이유를 전부 적어줘야 합니다. 저는 GIF 정보를 백그라운드에 저장하기 위해 스토리지 API를 사용했으며, 이에 대한 권한만 지정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 content_scripts 부분의 matches는 contentscript.js가 발동되기 위한 도메인 주소를 지정하는 것인데 저는 모든 페이지에서 해당 기능이 발동되기를 원하기 때문에 이와 같이 지정해주었습니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1637420197595&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;author&quot;: &quot;Youngkyun Kim&quot;,
  &quot;name&quot;: &quot;__MSG_extName__&quot;,
  &quot;description&quot;: &quot;__MSG_extDescription__&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;manifest_version&quot;: 2,
  &quot;default_locale&quot;: &quot;en&quot;,
  &quot;browser_action&quot;: {
    &quot;default_icon&quot;: &quot;images/doge-head-128.png&quot;,
    &quot;icons&quot;: {
       &quot;16&quot;: &quot;images/doge-head-16.png&quot;,
       &quot;48&quot;: &quot;images/doge-head-48.png&quot;,
      &quot;128&quot;: &quot;images/doge-head-128.png&quot;
    },
    &quot;default_popup&quot;: &quot;popup.html&quot;
  },
  &quot;permissions&quot;: [
    &quot;storage&quot;
  ],
  &quot;background&quot;: {
    &quot;scripts&quot;:  [&quot;background.js&quot;],
    &quot;persistent&quot;: false
  },
  &quot;content_scripts&quot;: [
    {
      &quot;matches&quot;: [
        &quot;http://*/*&quot;,
        &quot;https://*/*&quot;
      ],
      &quot;js&quot;: [&quot;contentscript.js&quot;]
    }
  ],
  &quot;web_accessible_resources&quot;: [
    &quot;images/*.gif&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. background.js 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 background.js에서 이벤트 핸들링 코드들을 작성해보겠습니다. 필요한 이벤트 처리는 크게 설치 이벤트, 메세지 이벤트, 탭 활성화 이벤트 세 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;chrome.runtime.onInstalled.addListener : 프로그램의 설치 시점에 설정값들을 세팅합니다.&lt;/li&gt;
&lt;li&gt;chrome.runtime.onMessage.addListener : 메세지를 받았을 때 수행됩니다. request.action 값을 통해 메세지의 의도를 전달하는 방식으로 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;chrome.tabs.onActivated.addListener : 현재 활성화된 탭이 변경되는 시점에 수행됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단에는 Giphy에서 랜덤하게 GIF를 가져오기 위한 코드가 있습니다. API 연동에 필요한 Key 값은 스크립트 내의 변수로 포함시켰습니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1637420921083&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const API_KEY = 'API_KEY';

async function giphyRandom() {
  const res = await fetch(`https://api.giphy.com/v1/gifs/random?api_key=${API_KEY}`)
  const json = await res.json()
  const data = json.data;
  return data;
}

chrome.runtime.onInstalled.addListener(async function (details) {
  if (details.reason === &quot;install&quot;) {
    const initStatus = {
      enabled: true,
      object: null,
      url: chrome.runtime.getURL('images/dancing-doge.gif'),
    };

    console.log(initStatus);

    // Initialize data
    chrome.storage.sync.set(initStatus);
  } else if (details.reason === &quot;update&quot;) {}
});

chrome.runtime.onMessage.addListener(async function(request, sender, sendResponse) {
  // REFRESH: save new git object to storage API
  if (request.action === &quot;REFRESH&quot;) {
    const GIFObject = await giphyRandom();
    chrome.storage.sync.set({
      object: GIFObject.images.fixed_width,
      url: GIFObject.images.fixed_width.url,
    })
    sendResponse({ message: &quot;OK&quot; });
  }
});

chrome.tabs.onActivated.addListener(function(activeInfo) {
  // UPDATE: re-rendering view
  chrome.tabs.sendMessage(activeInfo.tabId, { action: &quot;UPDATE&quot; });
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. popup 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 popup 영역을 작성해보겠습니다. 뷰에 해당하는 popup.html에는 특별한 내용이 없고 구동부에 해당하는 popup.js가 중요합니다. 스토리지 API의 연동 부분, 값을 읽어와서 Giphy의 뷰를 만들어주는 부분, 백그라운드에서 수신할 REFRESH 메세지를 전송하는 부분이 주요 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 tailwind.css를 사용해봤는데 이걸 굳이 사용 안하셔도 됩니다. html 코드 안에 CSS 코드를 직접 작성하셔도 되고, 별도 css 파일로 만들어서 작성해도 충분합니다. tailwind의 자세한 코드는 레포지토리의 README를 참고하시기 바랍니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;popup.html&lt;/p&gt;
&lt;pre id=&quot;code_1637421725010&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Flying Meme Animator&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;css/tailwind.css&quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body id=&quot;root-wrapper&quot;&amp;gt;
    &amp;lt;!-- header section --&amp;gt;
    &amp;lt;header id=&quot;header-wrapper&quot;&amp;gt;
      &amp;lt;label for=&quot;enabled&quot; class=&quot;switch&quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;input id=&quot;enabled&quot; type=&quot;checkbox&quot; class=&quot;sr-only&quot; /&amp;gt;
          &amp;lt;div class=&quot;bar&quot;&amp;gt;&amp;lt;/div&amp;gt;
          &amp;lt;div class=&quot;dot&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/label&amp;gt;
      &amp;lt;button id=&quot;close&quot; class=&quot;text-gray-600&quot;&amp;gt;
        &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-4 w-4&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
          &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M6 18L18 6M6 6l12 12&quot; /&amp;gt;
        &amp;lt;/svg&amp;gt;
      &amp;lt;/button&amp;gt;
    &amp;lt;/header&amp;gt;

    &amp;lt;!-- main section --&amp;gt;
    &amp;lt;main id=&quot;main-wrapper&quot;&amp;gt;&amp;lt;/main&amp;gt;

    &amp;lt;!-- footer section --&amp;gt;
    &amp;lt;footer id=&quot;footer-wrapper&quot;&amp;gt;
      Powered By&amp;amp;nbsp;&amp;lt;a href=&quot;https://giphy.com/&quot; target=&quot;blank&quot; class=&quot;text-blue-500 hover:text-blue-700 hover:underline&quot;&amp;gt;Giphy&amp;lt;/a&amp;gt;
    &amp;lt;/footer&amp;gt;

    &amp;lt;script src=&quot;popup.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;popup.js&lt;/p&gt;
&lt;pre id=&quot;code_1637421770302&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(async function () {

  /** Utility functions */
  function removeAllChildNodes(parent) {
    while (parent.firstChild) {
      parent.removeChild(parent.firstChild);
    }
  }

  /**
   * Make HTMLDivElement from GIFObject
   * @param {Images.fixed_width} object https://developers.giphy.com/docs/api/schema/
   * @returns {HTMLDivElement}
   */
  function makeGiphyElement(object) {
    const container = document.createElement('div');
    const overlay = document.createElement('div');
    const refreshButton = document.createElement('button');

    container.classList.add('giphy-item');
    overlay.classList.add('overlay', 'hidden');

    if (object != null) {
      container.style.backgroundImage = `url('${object.url}')`;
      container.style.width = `${object.width}px`;
      container.style.height = `${object.height}px`;
    } else {
      container.style.backgroundImage = `url('${chrome.runtime.getURL(`images/dancing-doge.gif`)}')`;
      container.style.width = `200px`;
      container.style.height = `202px`;
    }

    container.addEventListener('mouseover', () =&amp;gt; {
      overlay.classList.remove('hidden');
      overlay.classList.add('flex');
    }, false)
    container.addEventListener('mouseout', () =&amp;gt; {
      overlay.classList.add('hidden');
      overlay.classList.remove('flex');
    }, false)

    refreshButton.innerHTML = `&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-6 w-6&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
      &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15&quot; /&amp;gt;
    &amp;lt;/svg&amp;gt;`;

    refreshButton.addEventListener('click', () =&amp;gt; chrome.runtime.sendMessage({ action: &quot;REFRESH&quot; }));

    overlay.append(refreshButton);
    container.append(overlay);

    return container;
  }

  /** Chrome extension API functions */
  async function setStorageData(data) {
    return new Promise(function (resolve, reject) {
      chrome.storage.sync.set(data, function () { resolve(); })
    })
  }

  async function findAll(itemList = [
    'object',
    'enabled',
  ]) {
    return new Promise(function (resolve, reject) {
      chrome.storage.sync.get(itemList, function (data) { resolve(data); })
    })
  }

  window.addEventListener('load', async function () {
    // find elements
    const closeButton = document.getElementById('close');
    const mainWrapper = document.getElementById('main-wrapper');
    const enabled = document.getElementById('enabled');

    /** DOM handler functions */
    async function bindStorageDataToElement() {
      const data = await findAll();

      removeAllChildNodes(mainWrapper);
      mainWrapper.append(makeGiphyElement(data.object));
      enabled.checked = data.enabled;
    }

    /** Header Event handlers */
    closeButton.addEventListener('click', () =&amp;gt; window.close(), false);

    /** Settings form event handlers */
    enabled.addEventListener('change', async function () {
      await setStorageData({ enabled: this.checked });
    }, false)

    // Add event handlers for chrome API
    chrome.storage.onChanged.addListener(bindStorageDataToElement);

    // Initialize
    await bindStorageDataToElement();

  }, false);
})()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. contentscript.js 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 contentscript.js를 작성해보겠습니다. 해당 코드는 웹페이지 영역에서 실행되므로 주의 깊게 작성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 z-index와 스크롤 위치 계산입니다. GIF가 항상 보이기 위해선 z-index에 높은 값을 지정해주는 것이 좋습니다. 물론 충분히 높은 값을 주더라도 모든 페이지에서 항상 보이도록 만들 수 없지만, 그래도 어느 정도 보장을 할 수 있는 값을 넣어주어야 합니다. 그리고 GIF가 표시될 위치는 스크롤 위치에 상대적이기 때문에 이를 포지션에 반영해야 합니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1637422376953&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(function () {
  const image = document.createElement('img');

  let object = null;
  let popupTimer = null;
  let popupDuration = 500;

  /** Initialize styles */
  image.style.position = 'absolute';
  image.style.zIndex = 9999;
  image.style.display = 'none';
  image.style.top = '0px';
  image.style.left = '0px';

  /** Utility functions */
  function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
  }

  /** Visibility functions */
  function popup() {
    const x = getRandomArbitrary(0, window.innerWidth - (object ? object.width : image.style.width));
    const y = getRandomArbitrary(0, window.innerHeight - (object ? object.height : image.style.height));
    const offsetX = window.pageXOffset || document.documentElement.scrollLeft;
    const offsetY = window.pageYOffset || document.documentElement.scrollTop;
    image.style.top  = `${offsetY + y}px`;
    image.style.left = `${offsetX + x}px`;
    image.style.display = 'inline';

    clearTimeout(popupTimer);
    popupTimer = setTimeout(function () {
      image.style.display = 'none';
    }, popupDuration);
  }

  /** DOM handler functions */
  function bindStorageDataToElement() {
    chrome.storage.sync.get([
      'url',
      'object',
      'enabled',
    ], function(items) {
      object = items.object;

      if (items.url) {
        image.src = items.url;
      }

      if (!items.enabled &amp;amp;&amp;amp; document.body.contains(image)) {
        document.body.removeChild(image);
      } else if (items.enabled &amp;amp;&amp;amp; !document.body.contains(image)) {
        document.body.appendChild(image);
      } else {
        // Nothing to do
      }
    });
  }

  // Add event handlers
  window.addEventListener('keydown', popup, false);
  window.addEventListener('click', popup, false);

  // Add event handlers for chrome API
  chrome.storage.onChanged.addListener(bindStorageDataToElement);

  chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    // Re render on update message received
    if (request.action === &quot;UPDATE&quot;) {
      bindStorageDataToElement();
    }
  });

  // Initialize
  bindStorageDataToElement();
})();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;업로드 및 테스트하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성이 완료되었다면 브라우저에 업로드해서 테스트를 해볼 수 있습니다. 업로드 절차는 아래와 같습니다. (영문 기준)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Manage Extensions 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6Mv1k/btrlLvxOYVX/Dgc6FwX0SLkz7WGFrtIP2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6Mv1k/btrlLvxOYVX/Dgc6FwX0SLkz7WGFrtIP2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6Mv1k/btrlLvxOYVX/Dgc6FwX0SLkz7WGFrtIP2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6Mv1k%2FbtrlLvxOYVX%2FDgc6FwX0SLkz7WGFrtIP2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;1135&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 우상단 Developer mode 체크&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJF7sP/btrlIcFxXYh/uleOBwlQ5OAlMjUwDjiEGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJF7sP/btrlIcFxXYh/uleOBwlQ5OAlMjUwDjiEGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJF7sP/btrlIcFxXYh/uleOBwlQ5OAlMjUwDjiEGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJF7sP%2FbtrlIcFxXYh%2FuleOBwlQ5OAlMjUwDjiEGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;276&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 좌상단 Load unpacked 클릭해서 업로드&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgeHqR/btrlLwwJEyg/w4kyN3xAbEWTGAq18EKtv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgeHqR/btrlLwwJEyg/w4kyN3xAbEWTGAq18EKtv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgeHqR/btrlLwwJEyg/w4kyN3xAbEWTGAq18EKtv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgeHqR%2FbtrlLwwJEyg%2Fw4kyN3xAbEWTGAq18EKtv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;212&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마켓 플레이스 배포하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포는 크롬 개발자 대시보드에서 진행할 수 있습니다. 처음에 5$의 수수료를 먼저 결제해야 하고, 결제가 완료되면 아래와 같은 대시보드를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2021-11-21 at 1.17.32 AM.png&quot; data-origin-width=&quot;3122&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VsdvK/btrlDWjSF2k/zHi3gIhMeaKqMkFktXui6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VsdvK/btrlDWjSF2k/zHi3gIhMeaKqMkFktXui6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VsdvK/btrlDWjSF2k/zHi3gIhMeaKqMkFktXui6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVsdvK%2FbtrlDWjSF2k%2FzHi3gIhMeaKqMkFktXui6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3122&quot; height=&quot;1354&quot; data-filename=&quot;Screen Shot 2021-11-21 at 1.17.32 AM.png&quot; data-origin-width=&quot;3122&quot; data-origin-height=&quot;1354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 항목을 눌러 개발한 소스코드를 zip 파일로 압축해서 업로드하면 초안 상태로 항목이 추가되는 것을 볼 수 있습니다. 이제 항목에 들어가서 필요한 내용들을 채워서 제출하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개인정보 보호관행 탭의 내용을 작성할 때 권한 요청에 대한 이유를 상세하게 적어야 통과가 되는 듯합니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;저는 1차로 제출한 것이 거절되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D23Oy/btrjjtC7Fs8/lVkI9Pp5xXuIW6M9MPfow1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D23Oy/btrjjtC7Fs8/lVkI9Pp5xXuIW6M9MPfow1/img.png&quot; data-alt=&quot;거절 사유&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D23Oy/btrjjtC7Fs8/lVkI9Pp5xXuIW6M9MPfow1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD23Oy%2FbtrjjtC7Fs8%2FlVkI9Pp5xXuIW6M9MPfow1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;622&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;1726&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;거절 사유&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 사용하지 않는 tab 권한을 요청했다는 것인데, 제가 background.js 코드 내에서 사용한 chrome.tabs.* 때문에 tab 권한이 필요하다고 생각해서 넣은 것이 거절 이유였습니다. 이를 제거하고 다시 시도했더니 정상적으로 등록이 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레퍼런스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@wisdom_lee/크롬-확장-프로그램Chrome-extension-개발-가이드&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@wisdom_lee/크롬-확장-프로그램Chrome-extension-개발-가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍</category>
      <category>크롬익스텐션</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/35</guid>
      <comments>https://chancethecoder.tistory.com/35#entry35comment</comments>
      <pubDate>Sun, 21 Nov 2021 01:25:14 +0900</pubDate>
    </item>
    <item>
      <title>하이라이트 링크 복사 원리를 파해쳐보자 : Text Fragment</title>
      <link>https://chancethecoder.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본문에서는 크롬 브라우저의 하이라이트 링크 복사 기능을 사용해보고, 어떻게 동작하는지 URL 구조를 분석해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하이라이트 링크 복사(Copy To Link)가 뭔가요?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2021-11-19 at 11.46.44 AM.png&quot; data-origin-width=&quot;2410&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMmUnw/btrlCpEw36e/ndOTDKYqONTx2W0xNcYug0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMmUnw/btrlCpEw36e/ndOTDKYqONTx2W0xNcYug0/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;amp;nbsp;chrome://whats-new/?auto=true&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMmUnw/btrlCpEw36e/ndOTDKYqONTx2W0xNcYug0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMmUnw%2FbtrlCpEw36e%2FndOTDKYqONTx2W0xNcYug0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2410&quot; height=&quot;762&quot; data-filename=&quot;Screen Shot 2021-11-19 at 11.46.44 AM.png&quot; data-origin-width=&quot;2410&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;amp;nbsp;chrome://whats-new/?auto=true&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이라이트 링크 복사는 최신 크롬 브라우저(95+ 버전부터 기본 지원)에서 추가된 기능으로, 웹페이지 본문 내에 존재하는 텍스트로 바로 이동 가능하도록 링크를 복사하는 기능입니다. 복사된 링크는 상당히 긴 URL로 생성되며, 이를 공유할 수도 있고 링크로 만들 수도 있기 때문에 유용하게 쓰일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 해당 기능이 어떻게 동작하는지 궁금해졌고, 링크에 비밀이 숨어있을 거라는 생각이 들어 조금 더 자세하게 파악해보기로 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 구조 살펴보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 하이라이트 링크 복사를 해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyZxQ/btrlw2YGcwi/L5KLbZN1gpqI9QA8tVpq31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyZxQ/btrlw2YGcwi/L5KLbZN1gpqI9QA8tVpq31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyZxQ/btrlw2YGcwi/L5KLbZN1gpqI9QA8tVpq31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyZxQ%2Fbtrlw2YGcwi%2FL5KLbZN1gpqI9QA8tVpq31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1950&quot; height=&quot;673&quot; data-origin-width=&quot;1950&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 텍스트 블록을 우클릭해서 하이라이트 링크 복사하기를 누르고, 탭을 하나 더 켜서 주소창에 붙여 넣기를 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2190&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGe7HQ/btrlwIMTDpM/kroFixezvsl7gVehvaFiO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGe7HQ/btrlwIMTDpM/kroFixezvsl7gVehvaFiO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGe7HQ/btrlwIMTDpM/kroFixezvsl7gVehvaFiO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGe7HQ%2FbtrlwIMTDpM%2FkroFixezvsl7gVehvaFiO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2190&quot; height=&quot;271&quot; data-origin-width=&quot;2190&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 매우 긴 URL이 생성되는 것을 볼 수 있습니다. 한글이 포함된 URL이 길어지는 이유는, 인터넷 통신에서 사용되는 URL이 ASCII 문자열로만 이루어져야 하기 때문입니다. 한글을 ASCII 코드로 표현하기 위해서는 인코딩을 거쳐야 하고, 이 과정에서 길어지게 됩니다. (참고: &lt;a href=&quot;https://www.w3schools.com/tags/ref_urlencode.asp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.w3schools.com/tags/ref_urlencode.asp&lt;/a&gt;) 이는 &lt;i&gt;encodeURIComponent&lt;/i&gt;라는 내장 함수를 통해 재현할 수 있으며, 반대로 인코딩 된 URL을 한글 포맷으로 변환하려면 &lt;i&gt;decodeURIComponent&lt;/i&gt; 함수를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 콘솔에서 디코딩을 통해 한글 포맷으로 변환해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2021-11-19 at 12.58.47 PM.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Y1jT/btrlyX9ZHcd/3qPbqnxQkxVYFI2UAYx2HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Y1jT/btrlyX9ZHcd/3qPbqnxQkxVYFI2UAYx2HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Y1jT/btrlyX9ZHcd/3qPbqnxQkxVYFI2UAYx2HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Y1jT%2FbtrlyX9ZHcd%2F3qPbqnxQkxVYFI2UAYx2HK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2236&quot; height=&quot;324&quot; data-filename=&quot;Screen Shot 2021-11-19 at 12.58.47 PM.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 URL의 구조가 한눈에 들어옵니다. 중간에 보면 특이한 녀석이 보이는데요, 바로 &lt;b&gt;#:~:text=&lt;/b&gt; 입니다. 해당 값 바로 뒤에 문자열이 들어가는 것으로 보니 결국 저 부분이 비밀의 열쇠일 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scroll To Text Fragment&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 찾아낸 #:~:text=의 정체는 &lt;a href=&quot;https://github.com/WICG/scroll-to-text-fragment&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Text Fragment&lt;/a&gt; 였습니다. 해당 내용은 아직 proposal 단계이며, &lt;a href=&quot;https://url.spec.whatwg.org/&quot;&gt;URL Standard&lt;/a&gt;에서는 찾아볼 수 없습니다. (2021.11.19 기준) 이는 웹 문서 내에 존재하는 텍스트를 URL에 포함하고자 하는 시도로 Fragment(#) 뒤에 특정 규칙에 따라 지정할 수 있습니다. 문법은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 : &lt;a href=&quot;https://en.wikipedia.org/w/index.php?title=Cat&amp;amp;oldid=916388819#:~:text=Claws-,Like%20almost,the%20Felidae%2C,-cats&quot;&gt;https://en.wikipedia.org/w/index.php?title=Cat&amp;amp;oldid=916388819#:~:text=Claws-,Like%20almost,the%20Felidae%2C,-cats&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;:~:text=[prefix-,]textStart[,textEnd][,-suffix]
         context  |-------match-----|  context&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬 브라우저에서는 Text Fragment가 포함된 URL을 입력받을 경우, 해당 영역에 대한 스크롤 및 하이라이트를 지원해주는 것으로 보입니다. 이는 아직 모든 브라우저에서 완전 호환되는 기능은 아닙니다만, 일부 테스트가 진행되고 있는 것으로 보입니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2021-11-19 at 2.14.53 PM.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4YLqJ/btrlAtgQjVP/ON6NkKS6S5nAkB7NQ0IVb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4YLqJ/btrlAtgQjVP/ON6NkKS6S5nAkB7NQ0IVb1/img.png&quot; data-alt=&quot;참조:&amp;amp;amp;amp;nbsp;https://wicg.github.io/scroll-to-text-fragment/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4YLqJ/btrlAtgQjVP/ON6NkKS6S5nAkB7NQ0IVb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4YLqJ%2FbtrlAtgQjVP%2FON6NkKS6S5nAkB7NQ0IVb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1250&quot; height=&quot;288&quot; data-filename=&quot;Screen Shot 2021-11-19 at 2.14.53 PM.png&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참조:&amp;amp;amp;nbsp;https://wicg.github.io/scroll-to-text-fragment/&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 data-ke-size=&quot;size23&quot;&gt;Text Fragment가 왜 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이런 기능이 왜 필요한지 궁금해졌습니다. 일반적인 URL에 익숙한 사람으로서, 이런 해괴망측한(?) 표현식을 넣어가면서까지 표준화하려는 이유가 무엇일까요? proposal 내용을 정리하면 아래와 같습니다. (참고: &lt;a href=&quot;https://github.com/WICG/scroll-to-text-fragment#motivating-use-cases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/WICG/scroll-to-text-fragment#motivating-use-cases&lt;/a&gt;)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 검색 엔진의 이점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 구글에 질의하면 그에 연관된 페이지들의 링크 목록을 보여줍니다. 이때 각 페이지의 HTML 요소에 ID 값이 포함되어있을 경우 해당 영역으로 바로 스크롤되도록 &quot;Jump to&quot; 링크를 만들어줍니다. (이는 기존의 Fragment 기능을 사용하는 것이겠죠)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ID 값이 하나도 없는 페이지일 경우 어디로 스크롤해야 할지 모르기 때문에 페이지의 최상단에 머물러 있겠죠. 이럴 때 Text Fragment를 통해 원하는 질의응답에 해당하는 영역을 지정하면 손쉽게 &quot;Jump to&quot; 링크를 만들 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 레퍼런스 효율성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위키 페이지와 같이 인용이 많이 되는 페이지는 종종 특정 문구를 인용하는 것인데도 불구하고 전체 페이지를 살펴보게 만들기도 합니다. 이런 경우 Text Fragment를 통해 정확히 인용하고자 하는 텍스트 영역을 링크로 참조를 건다면 효율성이 올라갑니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 웹 페이지의 특정 구절 유저 간 공유하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 간 웹페이지 문서를 주고받을 때, 특정 부분을 공유하고자 하는 경우 Text Fragment를 통해 정확하게 전달이 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;정리&lt;/h3&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롬 브라우저의 하이라이트 링크 복사하기는 Text Fragment를 통해 구현된 기능입니다.&lt;/li&gt;
&lt;li&gt;Text Fragment는 웹 문서 내의 정확한 텍스트 레퍼런스를 URL에 표현하기 위한 실험적인 URL 표준입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍</category>
      <category>Text Fragment</category>
      <category>URL Standard</category>
      <author>chancethecoder</author>
      <guid isPermaLink="true">https://chancethecoder.tistory.com/36</guid>
      <comments>https://chancethecoder.tistory.com/36#entry36comment</comments>
      <pubDate>Fri, 19 Nov 2021 14:56:23 +0900</pubDate>
    </item>
  </channel>
</rss>