<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kang-bit.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 31 Dec 2025 11:20:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kang-bit.log</title>
            <url>https://velog.velcdn.com/images/kang-bit/profile/48f0a069-189d-494c-9c4c-926ebfb574c2/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kang-bit.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kang-bit" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Chrome built-in AI - Translator]]></title>
            <link>https://velog.io/@kang-bit/Chrome-built-in-AI-Translator</link>
            <guid>https://velog.io/@kang-bit/Chrome-built-in-AI-Translator</guid>
            <pubDate>Wed, 31 Dec 2025 11:20:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/f4bf9649-57f7-40bc-8ff8-aa4af909ca59/image.png" alt=""></p>
<h1 id="translator">Translator</h1>
<p>Chrome 브라우저에는 AI가 내장되어 있으며, API를 통해 AI 기반 작업을 실행할 수 있도록 지원합니다.</p>
<p>Translator API를 이용하면 입력 텍스트를 원하는 언어로 번역할 수 있습니다.</p>
<h2 id="0-준비">0. 준비</h2>
<ul>
<li><p>브라우저 지원 현황을 확인하세요
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Translator#browser_compatibility">MDN</a> 에서 브라우저별 지원 현황을 확인할 수 있습니다.</p>
</li>
<li><p>Typescript를 사용한다면
<a href="https://www.npmjs.com/package/@types/dom-chromium-ai">@types/dom-chromium-ai</a> npm 패키지를 사용하여 TypeScript 타이핑을 가져오세요.</p>
</li>
</ul>
<h2 id="1-브라우저가-translator-api를-지원하는지-확인">1. 브라우저가 Translator API를 지원하는지 확인</h2>
<pre><code class="language-js">const isBrowserSupported = &quot;Translator&quot; in self;</code></pre>
<h2 id="2-ai-모델이-준비되었는지-확인">2. AI 모델이 준비되었는지 확인</h2>
<pre><code class="language-js">const availability = await Translator.availability({
  sourceLanguage: sourceLanguage, // &#39;en&#39;
  targetLanguage: targetLanguage, // &#39;ko&#39;
});</code></pre>
<p>이 함수는 다음 값 중 하나가 포함된 프로미스를 반환합니다.</p>
<ul>
<li><code>available</code> : 모델이 이미 다운로드되어 있고 사용할 수 있습니다.</li>
<li><code>downloading</code> : 모델이 다운로드 중입니다.</li>
<li><code>downloadable</code> : 모델을 다운로드 할 수 있습니다.</li>
<li><code>unavailable</code> : 지원되지 않습니다. 기기의 전원이나 디스크 공간이 부족할 수 있습니다. <code>sourceLanguage</code>와 <code>targetLanguage</code>가 같거나 지원되지 않는 언어일 수 있습니다.</li>
</ul>
<h2 id="3-모델-다운로드-및-인스턴스-생성">3. 모델 다운로드 및 인스턴스 생성</h2>
<p>모델 다운로드 및 인스턴스 생성을 위해 <code>create()</code> 함수를 호출합니다.</p>
<p>원본 언어와 번역할 언어를 전달하고, 작업을 중단하기 위한 시그널도 전달할 수 있습니다.</p>
<pre><code class="language-ts">const abortController = new AbortController();
const translator = await Translator.create({
  sourceLanguage: sourceLanguage,
  targetLanguage: targetLanguage,
  monitor(m) {
    m.addEventListener(&quot;downloadprogress&quot;, (e) =&gt; {
      console.log(`Downloaded ${e.loaded * 100}%`);
    });
  },
  signal: abortController.signal,
});</code></pre>
<h2 id="4-번역">4. 번역</h2>
<p>번역을 위해 <code>translate()</code> 함수를 호출합니다.</p>
<p>이 함수는 번역된 텍스트를 반환합니다.</p>
<pre><code class="language-ts">const translatedText = await translator.translate(text);</code></pre>
<h2 id="5-인스턴스-해제">5. 인스턴스 해제</h2>
<p>작업이 완료되면 인스턴스를 해제합니다.</p>
<pre><code class="language-ts">translator.destroy();</code></pre>
<p><code>create()</code> 를 호출할 때 전달하는 <code>signal</code>을 통해서 작업을 중단해도 같은 효과를 얻을 수 있습니다</p>
<p>( <code>create()</code> 가 완료되기 전에 <code>abort</code>할 경우에는 <code>create</code> 작업이 취소됩니다. )</p>
<pre><code class="language-ts">abortController.abort();</code></pre>
<h2 id="react-예제">React 예제</h2>
<p>사용자의 상호작용 없이 AI 모델을 다운로드하려고 하면 에러가 발생할 수 있습니다.</p>
<p>따라서 컴포넌트가 마운트되었을 때 인스턴스를 생성하는 것보다는</p>
<p>사용자의 상호작용이 있을 때 인스턴스를 생성할 수 있도록 유틸 함수를 작성합니다.</p>
<pre><code class="language-ts">export const getTranslator = async (
  sourceLanguage: string,
  targetLanguage: string,
  signal: AbortSignal
) =&gt; {
  const isBrowserSupported = &quot;Translator&quot; in self;
  if (!isBrowserSupported) {
    throw new Error(&quot;Translator is not supported in this browser&quot;);
  }

  const availability = await Translator.availability({
    sourceLanguage: sourceLanguage,
    targetLanguage: targetLanguage,
  });

  if (availability === &quot;unavailable&quot;) {
    throw new Error(&quot;Model is not available for the given languages&quot;);
  }

  return Translator.create({
    sourceLanguage: sourceLanguage,
    targetLanguage: targetLanguage,
    signal: signal,
  });
};</code></pre>
<p>컴포넌트가 유지되는 동안은 Translator 인스턴스가 유지되도록 작성했습니다.</p>
<p>실제로 사용할 때는 원본 언어와 번역할 언어가 변경될 때 인스턴스를 새로 생성해야 할 수 있습니다.</p>
<pre><code class="language-tsx">import { getTranslator } from &quot;@/utils/translator&quot;;

export const Translator = () =&gt; {
  const translatorRef = useRef&lt;Translator | null&gt;(null);
  const abortControllerRef = useRef&lt;AbortController | null&gt;(null);

  const [isLoading, setIsLoading] = useState(false);
  const [translatedText, setTranslatedText] = useState&lt;string&gt;(&quot;&quot;);

  useEffect(() =&gt; {
    return () =&gt; {
      abortControllerRef.current?.abort();
    };
  }, []);

  const translateText = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();

    const formData = new FormData(e.target as HTMLFormElement);
    const text = formData.get(&quot;text&quot;) as string;

    try {
      setIsLoading(true);
      if (!translatorRef.current) {
        abortControllerRef.current = new AbortController();
        translatorRef.current = await getTranslator(
          &quot;en&quot;,
          &quot;ko&quot;,
          abortControllerRef.current.signal
        );
      }

      const results = (await translatorRef.current?.translate(text)) ?? &quot;&quot;;
      setTranslatedText(results);
    } catch (error) {
      console.error(&quot;Error translating text&quot;, error);
    } finally {
      setIsLoading(false);
    }
  };
  return (
    &lt;div&gt;
      &lt;form onSubmit={translateText}&gt;
        &lt;input type=&quot;text&quot; name=&quot;text&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Translate&lt;/button&gt;
      &lt;/form&gt;
      &lt;div&gt;{translatedText}&lt;/div&gt;
    &lt;/div&gt;
  );
};</code></pre>
<h2 id="react-예제--language-detector--translator">React 예제 : Language Detector + Translator</h2>
<p><a href="https://kangbit.github.io/posts/chrome-ai/language-detector.html">Language Detector</a>를 함께 사용하면 원본 언어를 알지 못해도 번역할 수 있습니다.</p>
<p>우선 <code>sourceLanguage</code>와 <code>targetLanguage</code>를 선택할 수 있도록 하겠습니다.</p>
<pre><code class="language-tsx">export const Translator = () =&gt; {
  // ...
  const translateText = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();

    const formData = new FormData(e.target as HTMLFormElement);
    const text = formData.get(&quot;text&quot;) as string;
    const sourceLanguage = formData.get(&quot;sourceLanguage&quot;) as string;
    const targetLanguage = formData.get(&quot;targetLanguage&quot;) as string;

    // ...
  };
  return (
    &lt;div&gt;
      &lt;form onSubmit={translateText}&gt;
        &lt;select name=&quot;sourceLanguage&quot; id=&quot;sourceLanguage&quot;&gt;
          &lt;option value=&quot;auto&quot;&gt;Detect Language&lt;/option&gt;
          &lt;option value=&quot;en&quot;&gt;English&lt;/option&gt;
          &lt;option value=&quot;ko&quot;&gt;Korean&lt;/option&gt;
        &lt;/select&gt;
        &lt;select name=&quot;targetLanguage&quot; id=&quot;targetLanguage&quot;&gt;
          &lt;option value=&quot;ko&quot;&gt;Korean&lt;/option&gt;
          &lt;option value=&quot;en&quot;&gt;English&lt;/option&gt;
        &lt;/select&gt;
        &lt;input type=&quot;text&quot; name=&quot;text&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Translate&lt;/button&gt;
      &lt;/form&gt;
      &lt;div&gt;{translatedText}&lt;/div&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>sourceLanguage가 <code>auto</code>인 경우에는 Language Detector를 사용해서 원본 언어를 감지해야 합니다.</p>
<p>원본 언어를 감지하기 위한 함수를 작성합니다.</p>
<p>마찬가지로 cleanup을 위해 <code>detector</code>와 <code>detectorAbortController</code>는 useRef로 선언합니다.</p>
<pre><code class="language-tsx">export const Translator = () =&gt; {
  const translatorRef = useRef&lt;Translator | null&gt;(null);
  const translatorAbortControllerRef = useRef&lt;AbortController | null&gt;(null);
  const detectorRef = useRef&lt;LanguageDetector | null&gt;(null);
  const detectorAbortControllerRef = useRef&lt;AbortController | null&gt;(null);

  const [isLoading, setIsLoading] = useState(false);
  const [translatedText, setTranslatedText] = useState&lt;string&gt;(&quot;&quot;);

  useEffect(() =&gt; {
    return () =&gt; {
      translatorAbortControllerRef.current?.abort();
      detectorAbortControllerRef.current?.abort();
    };
  }, []);

  const detectLanguage = async (text: string) =&gt; {
    if (!detectorRef.current) {
      detectorAbortControllerRef.current = new AbortController();
      detectorRef.current = await getDetector(
        [],
        detectorAbortControllerRef.current.signal
      );
    }
    const results = await detectorRef.current?.detect(text);
    if (!results?.[0]?.detectedLanguage) {
      throw new Error(&quot;Failed to detect language&quot;);
    }
    return results[0].detectedLanguage;
  };
  // ...
};</code></pre>
<p>선택된 <code>sourceLanguage</code>와 <code>targetLanguage</code>를 이용해서 translate 하도록 수정합니다.</p>
<pre><code class="language-tsx">export const Translator = () =&gt; {
  // ...

  const translateText = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    // ...
    let sourceLanguage = formData.get(&quot;sourceLanguage&quot;) as string;
    const targetLanguage = formData.get(&quot;targetLanguage&quot;) as string;

    try {
      setIsLoading(true);

      // sourceLanguage가 auto인 경우에는 Language Detector를 사용해서 원본 언어를 감지합니다.
      if (sourceLanguage === &quot;auto&quot;) {
        const detectedLanguage = await detectLanguage(text);
        sourceLanguage = detectedLanguage;
      }

      // 원본 언어와 번역할 언어가 같으면 번역하지 않습니다.
      if (sourceLanguage === targetLanguage) {
        setTranslatedText(text);
        return;
      }


      // 기존 요청을 취소하고 
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }

      // 새로운 인스턴스를 생성합니다.
      abortControllerRef.current = new AbortController();
      translatorRef.current = await getTranslator(
        sourceLanguage,
        targetLanguage,
        abortControllerRef.current.signal
      );


      const results = (await translatorRef.current?.translate(text)) ?? &quot;&quot;;
      setTranslatedText(results);
  };
  // ...
};</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://developer.chrome.com/docs/ai/translator-api">AI on Chrome</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Translator">MDN - Translator</a></li>
<li><a href="https://kangbit.github.io/posts/chrome-ai/translater.html">Kangbit Blog</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chrome Built-in AI -  Language Detector]]></title>
            <link>https://velog.io/@kang-bit/Chrome-Built-in-AI-Language-Detector</link>
            <guid>https://velog.io/@kang-bit/Chrome-Built-in-AI-Language-Detector</guid>
            <pubDate>Tue, 30 Dec 2025 09:44:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/c987fc1b-bacd-40a9-815f-bcf64efe2bd5/image.png" alt=""></p>
<h1 id="language-detector">Language Detector</h1>
<p>Chrome 브라우저에는 AI가 내장되어 있으며, API를 통해 AI 기반 작업을 실행할 수 있도록 지원합니다.</p>
<p>Language Detector는 입력 텍스트의 언어를 감지하여 확률이 높은 언어부터 낮은 언어 순으로 반환합니다.</p>
<p>이를 이용해 Translator API에 번역할 텍스트의 입력 언어를 전달할 수 있습니다.</p>
<h2 id="0-준비">0. 준비</h2>
<ul>
<li><p>브라우저 지원 현황을 확인하세요
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Translator_and_Language_Detector_APIs#browser_compatibility">MDN</a> 에서 브라우저별 지원 현황을 확인할 수 있습니다.</p>
</li>
<li><p>Typescript를 사용한다면
<a href="https://www.npmjs.com/package/@types/dom-chromium-ai">@types/dom-chromium-ai</a> npm 패키지를 사용하여 TypeScript 타이핑을 가져오세요.</p>
</li>
</ul>
<h2 id="1-브라우저가-language-detector-api를-지원하는지-확인">1. 브라우저가 Language Detector API를 지원하는지 확인</h2>
<pre><code class="language-js">const isBrowserSupported = &quot;LanguageDetector&quot; in self;</code></pre>
<h2 id="2-api가-준비되었는지-확인">2. API가 준비되었는지 확인</h2>
<p>API가 준비되었는지 확인하려면 비동기 <code>availability()</code> 함수를 호출합니다.</p>
<pre><code class="language-js">const availability = await LanguageDetector.availability({
  expectedInputLanguages: languages,
});</code></pre>
<p>이 함수는 다음 값 중 하나가 포함된 프로미스를 반환합니다.</p>
<ul>
<li><code>available</code> : 모델이 이미 다운로드되어 있고 사용할 수 있습니다.</li>
<li><code>downloading</code> : 모델이 다운로드 중입니다.</li>
<li><code>downloadable</code> : 모델을 다운로드 할 수 있습니다.</li>
<li><code>unavailable</code> : 지원되지 않습니다. 기기의 전원이나 디스크 공간이 부족할 수 있습니다.</li>
</ul>
<h2 id="3-모델-다운로드-및-인스턴스-생성">3. 모델 다운로드 및 인스턴스 생성</h2>
<p>모델 다운로드 및 인스턴스 생성을 위해 <code>create()</code> 함수를 호출합니다.</p>
<p>이 때 예상 언어를 전달하거나, 작업을 중단하기 위한 시그널을 전달할 수 있습니다.</p>
<pre><code class="language-ts">const abortController = new AbortController();
const detector = await LanguageDetector.create({
  expectedInputLanguages: languages,
  monitor(m) {
    m.addEventListener(&quot;downloadprogress&quot;, (e) =&gt; {
      console.log(`Downloaded ${e.loaded * 100}%`);
    });
  },
  signal: abortController.signal,
});</code></pre>
<h2 id="4-언어-감지">4. 언어 감지</h2>
<p>언어 감지를 위해 <code>detect()</code> 함수를 호출합니다.</p>
<pre><code class="language-js">const detectedLanguages = await detector.detect(text);</code></pre>
<p>이 함수는 다음과 같은 목록을 포함하는 프로미스를 반환합니다.</p>
<pre><code class="language-ts">[
  {
      &quot;confidence&quot;: 0.9427181482315063,
      &quot;detectedLanguage&quot;: &quot;en&quot;
  },
  {
      &quot;confidence&quot;: 0.016950147226452827,
      &quot;detectedLanguage&quot;: &quot;es&quot;
  },
  ...
]</code></pre>
<h2 id="5-인스턴스-삭제">5. 인스턴스 삭제</h2>
<p>작업이 완료되면 인스턴스를 삭제합니다.</p>
<pre><code class="language-ts">detector.destroy();</code></pre>
<p><code>create</code> 함수를 호출할 때 전달하는 <code>signal</code>을 통해서 작업을 중단해도 같은 효과를 얻을 수 있습니다</p>
<p>( <code>create()</code> 가 완료되기 전에 <code>abort</code>할 경우에는 <code>create</code> 작업이 취소됩니다. )</p>
<pre><code class="language-ts">abortController.abort();</code></pre>
<h2 id="react-예제">React 예제</h2>
<p>사용자의 상호작용 없이 AI 모델을 다운로드하려고 하면 에러가 발생할 수 있습니다.</p>
<p>따라서 컴포넌트가 마운트되었을 때 인스턴스를 생성하는 것보다는</p>
<p>사용자의 상호작용이 있을 때 인스턴스를 생성할 수 있도록 유틸 함수를 작성합니다.</p>
<pre><code class="language-ts">export const getLanguageDetector = async (languages: string[], signal: AbortSignal) =&gt; {
  const isBrowserSupported = &quot;LanguageDetector&quot; in self;
  if (!isBrowserSupported) {
    throw new Error(&quot;Language Detector is not supported in this browser&quot;);
  }

  const availability = await LanguageDetector.availability({
    expectedInputLanguages: languages,
  });

  if (availability === &quot;unavailable&quot;) {
    throw new Error(&quot;Model is not available for the given languages&quot;);
  }

  return LanguageDetector.create({
    expectedInputLanguages: languages,
    monitor(m) {
      m.addEventListener(&quot;downloadprogress&quot;, (e) =&gt; {
        console.log(`Downloaded ${e.loaded * 100}%`);
      });
    },
    signal: signal,
  });</code></pre>
<p>컴포넌트가 유지되는 동안은 Language Detector 인스턴스가 유지되도록 작성했습니다.</p>
<p>클린업도 신경써주도록 합니다.</p>
<pre><code class="language-tsx">import { useEffect, useState } from &quot;react&quot;;
import { getLanguageDetector } from &quot;@/utils/languageDetector&quot;;

export const LanguageDetector = () =&gt; {
  const detectorRef = useRef&lt;LanguageDetector | null&gt;(null);
  const abortControllerRef = useRef&lt;AbortController | null&gt;(null);

  const [isLoading, setIsLoading] = useState(false);
  const [detectedLanguage, setDetectedLanguage] = useState&lt;
    LanguageDetectionResult[]
  &gt;([]);

  useEffect(() =&gt; {
    return () =&gt; {
      abortControllerRef.current?.abort();
    };
  }, []);

  const detectLanguage = async (e: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);
    const text = formData.get(&quot;text&quot;) as string;

    try {
      setIsLoading(true);
      if (!detectorRef.current) {
        abortControllerRef.current = new AbortController();
        detectorRef.current = await getDetector(
          [&quot;en&quot;],
          abortControllerRef.current.signal
        );
      }

      const results = (await detectorRef.current?.detect(text)) ?? [];
      setDetectedLanguage(results);
    } catch (error) {
      console.error(&quot;Error detecting language&quot;, error);
    } finally {
      setIsLoading(false);
    }
  };
  return (
    &lt;div&gt;
      &lt;form onSubmit={detectLanguage}&gt;
        &lt;input type=&quot;text&quot; name=&quot;text&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Detect&lt;/button&gt;
      &lt;/form&gt;
      {detectedLanguage.map((language) =&gt; (
        &lt;div key={language.detectedLanguage}&gt;
          {language.detectedLanguage}: {language.confidence}
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://developer.chrome.com/docs/ai/language-detection?hl=ko">AI on Chrome</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/LanguageDetector">MDN - Language Detector</a></li>
<li><a href="https://kangbit.github.io/posts/chrome-ai/language-detector.html">Kangbit Blog</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chrome Built-in AI - 소개]]></title>
            <link>https://velog.io/@kang-bit/Chrome-Built-in-AI-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@kang-bit/Chrome-Built-in-AI-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Mon, 29 Dec 2025 06:54:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/74040450-b153-4542-9271-9ed8b0ba215b/image.png" alt=""></p>
<h2 id="소개">소개</h2>
<p>Chrome 브라우저에는 AI가 내장되어 있으며, API를 통해 AI 기반 작업을 실행할 수 있도록 지원합니다.</p>
<p>Google은 이러한 API가 모든 브라우저에서 작동하도록 표준화하기 위해 노력하고 있습니다.</p>
<h2 id="장점">장점</h2>
<p>자체 AI 모델을 배포하거나 관리할 필요 없이 AI 기반 기능을 제공할 수 있습니다.</p>
<p>사용자의 브라우저에서 실행되고 추론을 실행하기 때문에</p>
<p>지연 시간 감소, 사용자 개인 정보 보호 강화, 오프라인 액세스 등의 이점을 누릴 수 있습니다.</p>
<h2 id="api-종류">API 종류</h2>
<p>개발 단계에 따라 사용할 수 있는 여러 API가 있습니다.</p>
<p>일부 API는 Chrome 안정화 버전에 있지만,</p>
<p>일부는 오리진 트라이얼에서 모든 개발자가 사용할 수 있고,</p>
<p>일부는 초기 미리보기 프로그램 (EPP) 참여자만 사용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>API</th>
<th>설명</th>
<th>웹</th>
<th>확장 프로그램</th>
</tr>
</thead>
<tbody><tr>
<td>Translator</td>
<td>번역 API</td>
<td>Chrome 138</td>
<td>Chrome 138</td>
</tr>
<tr>
<td>Language Detector</td>
<td>언어 감지 API</td>
<td>Chrome 138</td>
<td>Chrome 138</td>
</tr>
<tr>
<td>Summarizer</td>
<td>요약 API</td>
<td>Chrome 138</td>
<td>Chrome 138</td>
</tr>
<tr>
<td>Writer</td>
<td>글쓰기 API</td>
<td>오리진 트라이얼</td>
<td>오리진 트라이얼</td>
</tr>
<tr>
<td>Rewriter</td>
<td>텍스트 수정 API</td>
<td>오리진 트라이얼</td>
<td>오리진 트라이얼</td>
</tr>
<tr>
<td>Prompt</td>
<td>Gemini Nano 프롬프트 API</td>
<td>오리진 트라이얼</td>
<td>Chrome 138</td>
</tr>
<tr>
<td>Proofreader</td>
<td>텍스트 교정 API</td>
<td>오리진 트라이얼</td>
<td>오리진 트라이얼</td>
</tr>
</tbody></table>
<h2 id="하드웨어-요구사항">하드웨어 요구사항</h2>
<p>일부 API는 휴대기기에서 작동하지 않으며</p>
<p>운영체제나 저장소 용량, GPU 또는 CPU, 네트워크 조건을 충족해야 할 수 있습니다.</p>
<p>이 조건은 <a href="https://developer.chrome.com/docs/ai/get-started?hl=ko#hardware">여기서</a> 확인할 수 있습니다.</p>
<h2 id="주의사항">주의사항</h2>
<h3 id="특정-용도에-맞게-만들기">특정 용도에 맞게 만들기</h3>
<p>내장 AI는 서버에서 실행되는 보통의 AI보다 작습니다.</p>
<p>특정 작업에서는 서버 측 모델보다 성능이 우수할 수 있지만,</p>
<p>일반적인 작업에서는 성능이 떨어질 수 있습니다.</p>
<h3 id="다운로드-크기-고려하기">다운로드 크기 고려하기</h3>
<p>AI 모델은 크기가 커서 모바일 데이터와 저장 공간을 많이 사용할 수 있습니다.</p>
<h3 id="상호작용-필요">상호작용 필요</h3>
<p>내장 AI API를 지원하지만 모델이 아직 다운로드되지 않은 경우,</p>
<p>모델을 다운로드 하기 위해서는 페이지 로드가 완료된 후 사용자가 페이지와 직접 상호작용해야 합니다.</p>
<p>이러한 상호작용 없이 모델을 다운로드 하려고 하면 다음과 같은 오류가 발생할 수 있습니다.</p>
<pre><code class="language-bash">NotAllowedError: Requires a user gesture when availability is &quot;downloading&quot; or &quot;downloadable&quot;</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://developer.chrome.com/docs/ai/built-in?hl=ko">AI on Chrome</a></li>
<li><a href="https://kangbit.github.io/posts/chrome-ai/">Kangbit Blog</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PIP Tools - PIP로 떠다니는 도구들!]]></title>
            <link>https://velog.io/@kang-bit/PIP-Tools-PIP%EB%A1%9C-%EB%96%A0%EB%8B%A4%EB%8B%88%EB%8A%94-%EB%8F%84%EA%B5%AC%EB%93%A4</link>
            <guid>https://velog.io/@kang-bit/PIP-Tools-PIP%EB%A1%9C-%EB%96%A0%EB%8B%A4%EB%8B%88%EB%8A%94-%EB%8F%84%EA%B5%AC%EB%93%A4</guid>
            <pubDate>Sun, 14 Sep 2025 09:55:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/86e41ed2-0c3b-422a-b8db-470638af408d/image.gif" alt=""></p>
<p><a href="https://www.pip-tools.com/tools">PIP-TOOLS</a></p>
<p>브라우저의 Picture-in-Picture 기능을 활용해서 만든 도구 모음을 소개합니다.</p>
<p>도구 아이콘을 클릭하면 PIP 모드로 열립니다.</p>
<p>가끔 색상 선택기를 사용할 때 브라우저 창(혹은 탭) 을 옮겨야 하는 불편한 경우가 있어 만들게 되었습니다.</p>
<p>현재 지원하는 기능은 다음과 같습니다.</p>
<ul>
<li>시계: 현재 시간 표시</li>
<li>타이머: 카운트다운 타이머</li>
<li>색상 선택기: 색상 코드 추출 및 복사</li>
<li>이미지 리사이즈: 이미지 크기 조절</li>
<li>번역기: 다국어 번역 ( 크롬 번역 API 활용 )</li>
<li>메모: 간단한 텍스트 메모</li>
<li>룰렛: 랜덤 선택 게임</li>
</ul>
<p>계산기, 단위 변환기, 스크립터, 이미지 워터마크 등을 추가할 계획중에 있고,</p>
<p>좌측 하단의 버튼을 통해 의견 주시면, 적극 반영하겠습니다.</p>
<p>필요한 도구가 있다면 의견 부탁드려요.</p>
<p>감사합니다.</p>
<hr>
<p><code>Next.js</code> <code>React</code> <code>TypeScript</code> <code>react-document-pip</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-document-pip]]></title>
            <link>https://velog.io/@kang-bit/React-pip</link>
            <guid>https://velog.io/@kang-bit/React-pip</guid>
            <pubDate>Tue, 29 Jul 2025 05:33:16 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/kang-bit/post/fdc55b7b-5a7d-465f-af1d-ee122da5e119/image.jpg">

<blockquote>
</blockquote>
<p>Document Picture in Picture API는 아직 실험적 기능입니다. ( 2025-07 )
일부 브라우저에서 동작하지 않을 수 있습니다.
<a href="https://caniuse.com/mdn-api_documentpictureinpicture">CanIUse</a> 에서 브라우저별 지원 현황을 확인할 수 있습니다.</p>
<blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">Secure Context</a> 에서만 사용할 수 있습니다.</p>
</blockquote>
<p><a href="https://kangbit.github.io/posts/document-pip/html.html">BitPage - Document PIP</a> 를 먼저 읽으시면 내용을 이해하는 데 도움이 됩니다.</p>
<p><a href="https://kangbit.github.io/posts/document-pip/react.html">BitPage - react pip</a>에서도 확인할 수 있습니다.</p>
<p><a href="https://www.npmjs.com/package/react-document-pip">react-document-pip</a>를 이용하면, 구현된 컴포넌트를 쉽게 사용할 수 있습니다.</p>
<h2 id="react에서의-document-pip">React에서의 Document PIP</h2>
<p><a href="https://kangbit.github.io/posts/document-pip/html.html">Document PIP - HTML</a>에서는 DOM 요소를 직접 선택하여 PIP 창으로 이동시켰습니다.</p>
<pre><code class="language-javascript">const element = document.getElementById(&quot;my-element&quot;);
pipWindow.document.body.appendChild(element);</code></pre>
<p>하지만 DOM Element만 복사해서는 컴포넌트의 상태나 이벤트를 활용할 수 없습니다.</p>
<p>따라서 아래의 방법을 사용합니다.</p>
<h2 id="pip에-새로운-react-앱-마운트하기">PIP에 새로운 React 앱 마운트하기</h2>
<p>PIP 창에 독립적인 React 앱을 렌더링할 수 있습니다.</p>
<p>다음과 같은 모습이 될 것 같습니다.</p>
<pre><code class="language-javascript">export const useDocumentPIP = (
  width: number,
  height: number,
  component: React.ComponentType
) =&gt; {
  const openPIPWindow = async () =&gt; {
    const pip = await window.documentPictureInPicture.requestWindow({
      width,
      height,
    });

    const appDiv = pip.document.createElement(&quot;div&quot;);
    appDiv.id = &quot;pip-app&quot;;
    pip.document.body.appendChild(appDiv);

    const root = createRoot(pip.document.getElementById(&quot;pip-app&quot;)!);
    root.render(React.createElement(component));
  };

  return {
    openPIPWindow,
  };
};</code></pre>
<p>이 방법으로도 어떤 컴포넌트에서는 충분할 것 같습니다.</p>
<p>하지만 외부 상태에 영향을 받는 컴포넌트라면 다음 방법을 사용하는 것이 나을 것 같습니다.</p>
<h2 id="pip로-portal-하기">PIP로 Portal 하기</h2>
<h3 id="목표">목표</h3>
<p><a href="https://react.dev/reference/react-dom/createPortal">React Portal</a>을 이용하면 동일한 인스턴스에서 컴포넌트를 렌더링할 수 있습니다.</p>
<p>동일한 인스턴스에 존재하기 때문에 Store에도 간단히 접근할 수 있고,</p>
<p>다음과 같이 간단하게 props를 전달하고 event를 청취할 수 있습니다.</p>
<!-- prettier-ignore -->
<pre><code class="language-jsx">&lt;&gt;
  &lt;MyComponent someProp={someProp} onSomeEvent={handleSomeEvent} /&gt;
  {createPortal(
      &lt;MyComponent someProp={someProp} onSomeEvent={handleSomeEvent} /&gt;,
      pipRoot
  )}
&lt;/&gt;</code></pre>
<p>재사용성을 위해 <code>DocumentPIP</code> 컴포넌트를 만들고, <code>children</code>을 활용해 PIP 창에 렌더링하겠습니다.</p>
<p>이렇게 사용하는 것이 목표입니다.</p>
<pre><code class="language-jsx">return (
  &lt;DocumentPIP isPipOpen={isPipOpen} size={{ width: 500, height: 200 }}&gt;
    &lt;PipContent /&gt;
  &lt;/DocumentPIP&gt;
);</code></pre>
<h3 id="pip-창-열기">PIP 창 열기</h3>
<p>먼저 PIP 창을 열고 컴포넌트가 <code>Portal</code>될 요소를 준비해야 합니다.</p>
<pre><code class="language-jsx">function DocumentPIP() {
  const openPIPWindow = async () =&gt; {
    const pip = await window.documentPictureInPicture.requestWindow({
      width: 500,
      height: 200,
    });

    const rootDiv = pip.document.createElement(&quot;div&quot;);
    rootDiv.id = &quot;pip-root&quot;;
    pip.document.body.appendChild(rootDiv);
  };
}</code></pre>
<p>우리는 이 pip 창이 열렸을 때 다음과 같이 컴포넌트를 조건부로 렌더링해야 합니다.</p>
<!-- prettier-ignore -->
<pre><code class="language-jsx">function DocumentPIP({ children }) {
  // ...

  if (pipRoot) {
    return createPortal(children, pipRoot);
  }

  return children;
}</code></pre>
<p><code>pipRoot</code>가 변경되었을 때 컴포넌트가 다시 렌더링되어야 합니다.</p>
<p>다음과 같이 수정하겠습니다.</p>
<!-- prettier-ignore -->
<pre><code class="language-jsx">function DocumentPIP({ children, isPipOpen }) {
  const [pipWindow, setPipWindow] = useState(null);

  const openPIPWindow = async () =&gt; {
    const pip = await window.documentPictureInPicture.requestWindow({
      width: 500,
      height: 200,
    });

    const rootDiv = pip.document.createElement(&quot;div&quot;);
    rootDiv.id = &quot;pip-root&quot;;
    pip.document.body.appendChild(rootDiv);

    setPipWindow(pip);
  };

  const pipContent = () =&gt; {
    const pipRoot = pipWindow?.document.getElementById(&quot;pip-root&quot;);
    if (!pipRoot || !isPipOpen) {
      return children;
    }

    return createPortal(children, pipRoot);
  };

  return pipContent();
}</code></pre>
<p>이제 props의 변화를 감지하고, 그 값에 따라 PIP 창을 열고 닫습니다.</p>
<pre><code class="language-jsx">function DocumentPIP({ children, isPipOpen }) {
  // ...

  useEffect(() =&gt; {
    togglePictureInPicture(isPipOpen);
  }, [isPipOpen]);

  const togglePictureInPicture = (open: boolean) =&gt; {
    if (open) {
      openPIPWindow();
    } else {
      closePIPWindow();
    }
  };

  const openPIPWindow = async () =&gt; {
    // ...
  };

  const closePIPWindow = () =&gt; {
    pipWindow.close();
    setPipWindow(null);
  };
}</code></pre>
<h2 id="styled-component-사용하기">Styled Component 사용하기</h2>
<p><code>styled-components</code>를 사용하고 있다면, 전체 스타일을 <code>pipWindow</code>로 복사하지 않아도 됩니다.</p>
<p><code>StyleSheetManager</code>를 이용해 <code>pipWindow</code>에 스타일을 주입할 수 있습니다.</p>
<pre><code class="language-jsx">import { StyleSheetManager } from &quot;styled-components&quot;;

const pipContent = () =&gt; {
  // ...

  return createPortal(
    &lt;StyleSheetManager target={pipWindow?.document.head}&gt;
      {children}
    &lt;/StyleSheetManager&gt;,
    pipRoot
  );
};</code></pre>
<h2 id="전체-코드">전체 코드</h2>
<p>전체 코드는 <a href="https://kangbit.github.io/posts/document-pip/react.html#%E1%84%8C%E1%85%A5%E1%86%AB%E1%84%8E%E1%85%A6-%E1%84%8F%E1%85%A9%E1%84%83%E1%85%B3">BitPage</a>에서 확인할 수 있습니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://react.dev/reference/react-dom/createPortal">React Portal</a></li>
<li><a href="https://developer.chrome.com/docs/web-platform/document-picture-in-picture">MDN</a></li>
<li><a href="https://developer.chrome.com/blog/document-pip-use-case?hl=ko">Chrome for developers</a></li>
<li><a href="https://caniuse.com/mdn-api_documentpictureinpicture">Can I Use</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue-pip]]></title>
            <link>https://velog.io/@kang-bit/Vue-pip</link>
            <guid>https://velog.io/@kang-bit/Vue-pip</guid>
            <pubDate>Wed, 23 Jul 2025 00:29:30 GMT</pubDate>
            <description><![CDATA[<h1 id="document-picture-in-picture">Document Picture In Picture</h1>
<img src="https://velog.velcdn.com/images/kang-bit/post/2e2b7b02-7404-47c1-913e-4c84689b6b4a/image.jpg">

<blockquote>
</blockquote>
<p>Document Picture in Picture API는 아직 실험적 기능입니다. ( 2025-07 )
일부 브라우저에서 동작하지 않을 수 있습니다.
<a href="https://caniuse.com/mdn-api_documentpictureinpicture">CanIUse</a> 에서 브라우저별 지원 현황을 확인할 수 있습니다.</p>
<blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">Secure Context</a> 에서만 사용할 수 있습니다.</p>
</blockquote>
<p> <a href="https://kangbit.github.io/posts/document-pip/html.html">BitPage - Document PIP</a> 를 먼저 읽으시면 내용을 이해하는 데 도움이 됩니다.</p>
<p> <a href="https://kangbit.github.io/posts/document-pip/vue.html">BitPage - vue pip</a>에서도 확인할 수 있습니다.</p>
<p> <a href="https://www.npmjs.com/package/vue-pip">vue-pip</a>를 이용하면, 구현된 컴포넌트를 쉽게 사용할 수 있습니다.</p>
<h2 id="vue에서의-document-pip">Vue에서의 Document PIP</h2>
<p><a href="https://kangbit.github.io/posts/document-pip/html.html">Document PIP - HTML</a>에서는 DOM 요소를 직접 선택하여 PIP 창으로 이동시켰습니다.</p>
<pre><code class="language-javascript">const element = document.getElementById(&quot;my-element&quot;);
pipWindow.document.body.appendChild(element);</code></pre>
<p>하지만 Vue의 반응성이나 이벤트를 활용하기 위해서는 다른 방법을 사용해야 합니다.</p>
<h2 id="pip에-새로운-vue-앱-마운트하기">PIP에 새로운 Vue 앱 마운트하기</h2>
<p>PIP 창에 독립적인 Vue 앱을 마운트 할 수 있습니다.</p>
<p>다음과 같은 모습이 될 것 같습니다.</p>
<pre><code class="language-javascript">export const useDocumentPIP = (
  width: number,
  height: number,
  component: Component
) =&gt; {
  //...
  const openPIPWindow = async () =&gt; {
    const pip = await window.documentPictureInPicture.requestWindow({
      width,
      height,
    });

    const appDiv = pip.document.createElement(&#39;div&#39;);
    appDiv.id = &#39;pip-app&#39;;
    pip.document.body.appendChild(appDiv);

    const pipApp = createApp(component);
    pipApp.mount(pip.document.getElementById(&quot;pip-app&quot;)!);
  };

  return {
    openPIPWindow,
  };
};</code></pre>
<p>이 방법으로도 어떤 컴포넌트에서는 충분할 것 같습니다.</p>
<p>하지만 외부 상태에 영향을 받는 컴포넌트라면 다음 방법을 사용하는 것이 나을 것 같습니다.</p>
<h2 id="pip로-teleport-하기">PIP로 Teleport 하기</h2>
<h3 id="목표">목표</h3>
<p><a href="https://ko.vuejs.org/guide/built-ins/teleport.html">Teleport</a>를 이용하면 동일한 인스턴스에서 컴포넌트를 렌더링 할 수 있습니다.</p>
<p>동일한 인스턴스에 존재하기 때문에 Store에도 간단히 접근할 수 있고,</p>
<p>다음과 같이 간단하게 props를 전달하고 event를 청취할 수 있습니다.</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;MyComponent @someEvent=&quot;handleSomeEvent&quot; :someProp=&quot;someProp&quot; /&gt;
  &lt;Teleport :to=&quot;pipTarget&quot;&gt;
    &lt;MyComponent @someEvent=&quot;handleSomeEvent&quot; :someProp=&quot;someProp&quot; /&gt;
  &lt;/Teleport&gt;
&lt;/template&gt;</code></pre>
<p>재사용성을 위해 <code>DocumentPip</code> 컴포넌트를 만들고, <code>slot</code>을 활용해 PIP 창에 렌더링하겠습니다.</p>
<p>이렇게 사용하는 것이 목표입니다.</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;DocumentPip :isPipOpen=&quot;isPipOpen&quot; :size=&quot;{ width: 500, height: 200 }&quot;&gt;
    &lt;PipContent /&gt;
  &lt;/DocumentPip&gt;
&lt;/template&gt;</code></pre>
<h3 id="pip-창-열기">PIP 창 열기</h3>
<p>먼저 PIP 창을 열고 컴포넌트가 <code>Teleport</code>될 요소를 준비해야 합니다.</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
const openPIPWindow = async () =&gt; {
  const pip = await window.documentPictureInPicture.requestWindow({
    width: 500,
    height: 200,
  });

  const rootDiv = pip.document.createElement(&quot;div&quot;);
  rootDiv.id = &quot;pip-root&quot;;
  pip.document.body.appendChild(rootDiv);

  const pipRoot = pip.document.getElementById(&quot;pip-root&quot;); // Teleport Target
};
&lt;/script&gt;</code></pre>
<p>우리는 이 pip 창이 열렸을 때 다음과 같이 컴포넌트를 조건부로 렌더링해야 합니다.</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;slot v-if=&quot;!pipRoot&quot;&gt;&lt;/slot&gt;
  &lt;Teleport v-else :to=&quot;pipRoot&quot;&gt;
    &lt;slot&gt;&lt;/slot&gt;
  &lt;/Teleport&gt;
&lt;/template&gt;</code></pre>
<p><code>pipRoot</code>가 변경되었을 때 컴포넌트가 다시 렌더링되어야 합니다.</p>
<p>다음과 같이 수정하겠습니다.</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
const pipWindow = ref&lt;Window | null&gt;(null);
const pipRoot = computed(() =&gt; {
  return pipWindow.value?.document.getElementById(&quot;pip-root&quot;) || null;
});

const openPIPWindow = async () =&gt; {
  const pip = await window.documentPictureInPicture.requestWindow({
    width: 500,
    height: 200,
  });

  const rootDiv = pip.document.createElement(&quot;div&quot;);
  rootDiv.id = &quot;pip-root&quot;;
  pip.document.body.appendChild(rootDiv);

  pipWindow.value = pip;
};
&lt;/script&gt;</code></pre>
<p>이제 props의 변화를 감지하고, 그 값에 따라 PIP 창을 열고 닫습니다.</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
// ...

watch(
  () =&gt; props.isPipOpen,
  (newVal: boolean) =&gt; {
    togglePictureInPicture(newVal);
  }
);

const togglePictureInPicture = (isPipOpen: boolean) =&gt; {
  if (isPipOpen) {
    openPIPWindow();
  } else {
    closePIPWindow();
  }
};

const openPIPWindow = async () =&gt; {
  // ...
};

const closePIPWindow = () =&gt; {
  pipWindow.value.close();
  pipWindow.value = null;
};
&lt;/script&gt;</code></pre>
<h2 id="전체-코드">전체 코드</h2>
<p>전체 코드는 <a href="https://kangbit.github.io/posts/document-pip/vue.html#%E1%84%8C%E1%85%A5%E1%86%AB%E1%84%8E%E1%85%A6-%E1%84%8F%E1%85%A9%E1%84%83%E1%85%B3">BitPage</a>에서 확인할 수 있습니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://ko.vuejs.org/guide/built-ins/teleport.html">Vue Teleport</a></li>
<li><a href="https://developer.chrome.com/docs/web-platform/document-picture-in-picture">MDN</a></li>
<li><a href="https://developer.chrome.com/blog/document-pip-use-case?hl=ko">Chrome for developers</a></li>
<li><a href="https://caniuse.com/mdn-api_documentpictureinpicture">Can I Use</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Document Picture In Picture]]></title>
            <link>https://velog.io/@kang-bit/Document-Picture-In-Picture</link>
            <guid>https://velog.io/@kang-bit/Document-Picture-In-Picture</guid>
            <pubDate>Thu, 17 Jul 2025 06:57:40 GMT</pubDate>
            <description><![CDATA[<h2 id="picture-in-picture란">Picture In Picture란?</h2>
<img src='https://velog.velcdn.com/images/kang-bit/post/4de00047-0245-453e-9366-5683b1c9f519/image.jpg' >

<p><strong>Picture in Picture</strong> (<strong>PIP</strong>)는 사용자가 다른 작업을 수행하면서도 특정 콘텐츠를 계속 볼 수 있도록 도와주는 기능입니다.</p>
<p>기존에는 주로 동영상 재생 시에 활용되어, 사용자가 웹 서핑이나 문서 작업을 하면서도 영상을 계속 시청할 수 있게 해주었습니다. </p>
<p>이 작은 팝업 창은 항상 다른 창들 위에 떠 있어서 필요한 정보를 지속적으로 제공합니다.</p>
<h2 id="document-picture-in-picture">Document Picture In Picture</h2>
<p><strong>Document PIP API</strong>의 등장으로 이제는 <strong>웹 문서 전체를 작은 창으로 분리</strong>할 수 있게 되었습니다. </p>
<p>이는 단순한 비디오 재생을 넘어서, 채팅창, 대시보드, 문서 편집기 등 다양한 웹 애플리케이션을 독립적인 창으로 분리하여 멀티태스킹을 극대화할 수 있게 해줍니다.</p>
<p>Document PIP의 핵심은 <strong>콘텐츠의 완전한 이동</strong>과 <strong>스타일의 완벽한 복사</strong>에 있습니다. </p>
<p>사용자가 보고 있던 화면의 특정 부분을 그대로 PIP 창으로 이동시키고, 모든 스타일시트를 복사하여 원본과 동일한 모습을 유지할 수 있습니다.</p>
<p><a href="https://kangbit.github.io/posts/document-pip/html.html">예제와 함께 보기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chrome135+ 에서 CSS로 캐러셀 만들기]]></title>
            <link>https://velog.io/@kang-bit/Chrome135-%EC%97%90%EC%84%9C-CSS%EB%A1%9C-%EC%BA%90%EB%9F%AC%EC%85%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kang-bit/Chrome135-%EC%97%90%EC%84%9C-CSS%EB%A1%9C-%EC%BA%90%EB%9F%AC%EC%85%80-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 06 Apr 2025 11:38:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>여기서 설명하는 캐러셀은 최신 브라우저에서만 작동합니다.
<a href="https://caniuse.com/mdn-css_selectors_scroll-button">CanIUse</a> 에서 브라우저별 지원 현황을 확인할 수 있습니다.</p>
<p><a href="https://kangbit.github.io/posts/css-carousel/">BitPage</a> 에서는 예제와 함께 확인할 수 있습니다.</p>
<h2 id="캐러셀이란">캐러셀이란?</h2>
<p>캐러셀은 여러개의 컨텐츠를 슬라이드 형태로 보여주는 UI 요소입니다.</p>
<p>캐러셀은 보통 자동으로 슬라이드되거나 드래그를 통해 컨텐츠를 이동하지만,</p>
<p>좌우 버튼이나 페이지네이션을 포함하여 특정 컨텐츠로 이동할 수도 있습니다.</p>
<p>기존에도 javascript 없이 캐러셀을 구현할 수 있지만,</p>
<p>좌우 버튼이나 페이지네이션을 포함시킬 수는 없었습니다.</p>
<p>Chrome 135부터는 <a href="https://drafts.csswg.org/css-overflow-5/">CSS Overflow Module Level 5</a>을 사용하여</p>
<p>css만으로 좌우 버튼과 페이지네이션을 포함하는 캐러셀을 구현할 수 있습니다.</p>
<p>이 포스트에서는 이 기능을 사용하여 캐러셀을 구현하는 방법을 소개합니다.</p>
<h2 id="캐러셀-예제">캐러셀 예제</h2>
<p><a href="https://chrome.dev/carousel/">CSS Carousel Gallery</a> 에서는 CSS로 만든 다양한 캐러셀 예제를 확인할 수 있습니다.</p>
<h2 id="캐러셀-만들기">캐러셀 만들기</h2>
<p>우선, 기본적인 형태의 캐러셀을 만들어 보겠습니다.</p>
<pre><code class="language-html">&lt;div class=&quot;scroll-layout&quot;&gt;
  &lt;ul class=&quot;carousel&quot;&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
    &lt;li class=&quot;carousel-item&quot;&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">li {
  list-style-type: none;
}

.scroll-layout {
  width: 100vw;
  display: flex;
  justify-content: center;
}

.carousel {
  width: 80%;
  display: grid;
  grid: 30vmin / auto-flow 40%;
  gap: 15px;
  padding: 0;
  margin: 0;

  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}

.carousel-item {
  border: 3px solid #888;
  scroll-snap-align: center;
}</code></pre>
<h2 id="좌우-버튼-추가하기">좌우 버튼 추가하기</h2>
<p><code>::scroll-button</code>의사 요소를 이용해 캐러셀에 좌우 버튼을 추가할 수 있습니다.</p>
<pre><code class="language-css">.carousel {
  &amp;::scroll-button(*) {
    position: fixed;
    position-anchor: --carousel;

    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 2px solid #999;

    margin: 5px;
  }

  &amp;::scroll-button(left) {
    position-area: inline-start center;
    content: &quot;⬅&quot; / &quot;Scroll Left&quot;;
  }

  &amp;::scroll-button(right) {
    position-area: inline-end center;
    content: &quot;⮕&quot; / &quot;Scroll Right&quot;;
  }
}</code></pre>
<p>좌우 버튼은 기본적으로 스크롤 영역의 85% 를 스크롤합니다.</p>
<p>하나의 컨텐츠씩 스크롤하려면 아래와 같이 설정합니다.</p>
<pre><code class="language-css">.carousel-item {
  scroll-snap-stop: always;
}</code></pre>
<h2 id="페이지네이션-추가하기">페이지네이션 추가하기</h2>
<p><code>::scroll-marker-group</code> 의사 요소와</p>
<p><code>::scroll-marker</code> 의사 요소를 이용해 페이지네이션을 추가할 수 있습니다.</p>
<pre><code class="language-css">.carousel {
  scroll-marker-group: after;

  &amp;::scroll-marker-group {
    position: fixed;
    position-anchor: --carousel;
    position-area: block-end;

    display: grid;
    place-content: safe center;
    grid: 16px / auto-flow 16px;
    gap: 15px;
    padding: 15px;
  }
}

.carousel-item {
  &amp;::scroll-marker {
    content: &quot; &quot;;

    border: 1px solid #bbb;
    border-radius: 50%;
    outline-offset: 4px;
  }

  &amp;::scroll-marker:is(:hover, :focus-visible) {
    border-color: LinkText;
  }

  &amp;::scroll-marker:target-current {
    background: LinkText;
    border-color: LinkText;
  }
}</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://drafts.csswg.org/css-overflow-5/">CSS Overflow Module Level 5</a></li>
<li><a href="https://developer.chrome.com/blog/carousels-with-css?hl=ko#add_scroll_markers_with_scroll-marker">CSS Carousels with CSS</a></li>
<li><a href="https://chrome.dev/carousel-configurator/">CSS Carousel Configurator</a></li>
<li><a href="https://chrome.dev/carousel/">CSS Carousel Gallery</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[반복되는 워터마크 작업으로 고생하는 디자이너를 위해 웹페이지를 개발하는 동료 개발자를 위해 만들어본 Watermark FolderAction]]></title>
            <link>https://velog.io/@kang-bit/%EB%B0%98%EB%B3%B5%EB%90%98%EB%8A%94-%EC%9B%8C%ED%84%B0%EB%A7%88%ED%81%AC-%EC%9E%91%EC%97%85%EC%9C%BC%EB%A1%9C-%EA%B3%A0%EC%83%9D%ED%95%98%EB%8A%94-%EB%94%94%EC%9E%90%EC%9D%B4%EB%84%88%EB%A5%BC-%EC%9C%84%ED%95%B4-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A5%BC-%EA%B0%9C%EB%B0%9C%ED%95%98%EB%8A%94-%EB%8F%99%EB%A3%8C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-Watermark-FolderAction</link>
            <guid>https://velog.io/@kang-bit/%EB%B0%98%EB%B3%B5%EB%90%98%EB%8A%94-%EC%9B%8C%ED%84%B0%EB%A7%88%ED%81%AC-%EC%9E%91%EC%97%85%EC%9C%BC%EB%A1%9C-%EA%B3%A0%EC%83%9D%ED%95%98%EB%8A%94-%EB%94%94%EC%9E%90%EC%9D%B4%EB%84%88%EB%A5%BC-%EC%9C%84%ED%95%B4-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A5%BC-%EA%B0%9C%EB%B0%9C%ED%95%98%EB%8A%94-%EB%8F%99%EB%A3%8C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-Watermark-FolderAction</guid>
            <pubDate>Fri, 28 Mar 2025 07:51:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/0bbd1e36-6dc3-4ba0-8a1c-ef5f22cb7897/image.gif" alt=""></p>
<p><a href="https://kangbit.github.io/posts/watermark-folderaction/">https://kangbit.github.io/posts/watermark-folderaction/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ref vs Reactive]]></title>
            <link>https://velog.io/@kang-bit/Ref-vs-Reactive</link>
            <guid>https://velog.io/@kang-bit/Ref-vs-Reactive</guid>
            <pubDate>Sun, 19 Jan 2025 07:45:56 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Vue3의 Composition Api에서 반응형 상태를 선언하는 방법은 <code>ref</code>와 <code>reactive</code> 두가지가 있습니다.</p>
<p>두 방법이 존재하다 보니 어떤 방법을 사용할지 헷갈려 하는 경우가 많습니다.</p>
<p>이 글이 생각을 정리하는 데 도움이 되었으면 좋겠습니다.</p>
<p>결론만 보고 싶은 분은 <a href="#%EA%B7%B8%EB%9E%98%EC%84%9C-%EB%AD%90-%EC%93%B0%EB%9D%BC%EA%B3%A0">그래서 뭐 쓰라고</a> 로 이동해주세요</p>
<h2 id="공식-문서의-제안">공식 문서의 제안</h2>
<p>공식 문서에서는 반응형 기초를 설명하는 첫 줄에서 <code>ref</code>를 사용할 것을 권장하고 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/c7691bd4-fc8f-4606-be2f-256f2097fdf9/image.jpeg" width="100%" alt="vue3 공식 문서의 반응형 기초">

<p><code>reactive</code>의 제한사항을 설명하면서도 <code>ref</code>를 기본으로 사용하기를 권장합니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/2e00d6b1-0670-46b1-8e8a-09dc518b9912/image.jpeg" width="100%" alt="vue3 공식 문서의 reactive 제한사항">

<p>그렇다면 <code>reactive</code>는 필요 없는 것 아닐까요?</p>
<p><code>ref</code>와 <code>reactive</code>의 특징에 대해 살펴보면서 더 알아보겠습니다.</p>
<h2 id="ref">ref</h2>
<h3 id="1-value-속성">1. value 속성</h3>
<p><code>ref</code> 객체는 <code>value</code> 속성을 갖습니다.</p>
<pre><code class="language-js">const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0</code></pre>
<p>이 때문에 반응성 값임을 쉽게 확인할 수 있어서 선호한다는 의견이 있는 반면,</p>
<p><code>.value</code>로 접근하는 것이 번거로워 <code>ref</code>를 선호하지 않는다는 의견도 있습니다.</p>
<p>확실히 다음과 같은 코드는 상당히 부자연스러워 보일 수 있습니다.</p>
<pre><code class="language-js">const arr = ref([1,2,3]);
arr.value.push(4)</code></pre>
<h3 id="2-깊은-반응성">2. 깊은 반응성</h3>
<p><code>ref</code> 는 깊게 반응합니다.</p>
<p>중첩된 객체나 배열이 변경되어도 적절하게 변경을 감지합니다.</p>
<p>다음과 같이 <code>isReactive</code>를 통해 확인해 보면, </p>
<p>객체 내부의 속성중 객체 유형은 <code>reactive</code>를 통해 Proxy 객체로 반환되는 것을 확인할 수 있습니다.</p>
<pre><code class="language-js">const obj = ref({property: [1,2,3], property2: 4});

console.log(isReactive(obj)); // false
console.log(isReactive(obj.value)); // true
console.log(isReactive(obj.value.property)); // true
console.log(isReactive(obj.value.property2)); // false</code></pre>
<h2 id="reactive">reactive</h2>
<h3 id="1-객체-유형만-가능">1. 객체 유형만 가능</h3>
<p><code>reactive</code>는 객체 유형에만 작동합니다.</p>
<p>간혹 이 내용을 보고 객체 유형에는 <code>reactive</code>를 사용해야 한다는 오해를 하시는 분도 있는 것 같습니다.</p>
<h3 id="2-깊은-반응성-1">2. 깊은 반응성</h3>
<p><code>ref</code>와 마찬가지로 깊은 반응성을 제공합니다.</p>
<h3 id="3-전체-객체를-대체할-수-없음">3. 전체 객체를 대체할 수 없음</h3>
<p>다음과 같이 객체 전체를 대체하려고 하면 <code>reactive</code> 객체에 대한 반응성이 끊기게 됩니다.</p>
<pre><code class="language-js">let user = reactive({ name: &#39;Kim&#39;, age: 30 });
user = reactive({ name: &#39;Kim&#39;, age: 30 });</code></pre>
<p>따라서, 전체 객체를 대체해야 할 필요가 있는 경우에 다음과 같이 사용하게 되는데,</p>
<pre><code class="language-js">const data = reactive({ user: { name: &#39;Kim&#39;, age: 30 } });
data.user = { name: &#39;Kim&#39;, age: 30 } ;</code></pre>
<p><code>ref</code>를 사용하는 게 더 나아 보입니다.</p>
<pre><code class="language-js">const user = ref({ name: &#39;Kim&#39;, age: 30 });
user.value = { name: &#39;Kim&#39;, age: 30 };</code></pre>
<h3 id="4-분해-할당에-친화적이지-않음">4. 분해 할당에 친화적이지 않음</h3>
<p>공식문서의 <code>reactive</code>의 제한 사항에서는 분해 할당에 친화적이지 않다는 내용이 있습니다.</p>
<p>그렇다면 과연 <code>reactive</code>만 분해 할당에 친화적이지 않을지</p>
<p>다음 예제를 통해 살펴보면,</p>
<pre><code class="language-vue">&lt;script setup&gt;
import { ref, reactive } from &quot;vue&quot;;

// Vue에서 ref와 reactive 사용 예제
const refData = ref({ number: 1 });
const reactiveData = reactive({ number: 1 });

// 객체 속성을 개별 변수로 추출하며 반응성 유지
let { number: refNumber } = refData.value; // ref에서 값 추출
let { number: reactiveNumber } = reactiveData; // reactive에서 값 추출

const incrementValues = () =&gt; {
  refData.value.number++;
  reactiveData.number++;
};
&lt;/script&gt;

&lt;template&gt;
&lt;div class=&quot;example&quot;&gt;
  &lt;div&gt;
    &lt;button @click=&quot;incrementValues&quot;&gt;Increment Values&lt;/button&gt;
  &lt;/div&gt;
  &lt;p&gt;ref: {{ refData }}, refNumber: {{ refNumber }}&lt;/p&gt;
  &lt;p&gt;reactive: {{ reactiveData }}, reactiveNumber: {{ reactiveNumber }}&lt;/p&gt;
&lt;/div&gt;
&lt;/template&gt;</code></pre>
<p><code>ref</code> 또한 분해할당에 친화적이지 않은 것을 볼 수 있습니다.</p>
<p>다만, <code>ref</code>와 <code>reactive</code> 모두 <code>toRefs</code>를 통해 반응성을 유지할 수 있습니다.</p>
<pre><code class="language-js">// const refNumber = toRef(refData.value, &quot;number&quot;); // 반응성 유지
const { number: refNumber } = toRefs(refData.value); // 반응성 유지
const { number: reactiveNumber } = toRefs(reactiveData); // 반응성 유지</code></pre>
<h2 id="그래서-뭐-쓰라고">그래서 뭐 쓰라고</h2>
<p>사실 무엇을 사용해도 크게 문제되지 않습니다.</p>
<p>나만의 규칙을 만들어 보세요.</p>
<ul>
<li><p>공식문서에서 제안하고 있으니 <code>ref</code>를 기본으로 사용하고  특수한 상황에서만 <code>reactive</code>를 사용한다.</p>
</li>
<li><p>원시 타입에만 <code>ref</code>를 사용하고 객체 유형은 <code>reactive</code>를 사용한다.</p>
</li>
<li><p>헷갈리지 않게 <code>ref</code>만 사용한다.</p>
</li>
<li><p>헷갈리지 않게 <code>reactive</code>만 사용한다.</p>
</li>
<li><p><code>.value</code> 가 보기 싫으니 <code>reactive</code>를 사용한다.</p>
</li>
</ul>
<p>프로젝트에 합류했다면 규칙을 따르고, 확신이 있다면 규칙을 수정할 것을 제안해 보세요.</p>
<h2 id="확실한-기준을-세우기">확실한 기준을 세우기</h2>
<p>어떤 기준으로 직면한 문제를 해결할지 고민하는 순간이 많이 찾아옵니다.</p>
<p>프로젝트 큐모에 맞는 폴더 구조를 선택하는 것부터,</p>
<p>새로운 기술 도입이 우리 조직과 프로젝트에 적합한지를 판단하는 기준까지 깊이 있는 고민이 필요합니다.</p>
<p>코드의 중복을 어느 선까지 허용할지, </p>
<p>낮은 결합도를 위해 의도적으로 중복을 허용하는 때는 언제일지 고민해야 합니다.</p>
<p>이런 기준들을 세우고 제시할 수 있게 되는 것이 개발자로서 성장하는 길 중 하나가 아닐까 합니다.</p>
<hr>
<p><a href="https://kangbit.github.io/posts/vue-reactivity/">https://kangbit.github.io/posts/vue-reactivity/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[가속합성으로 인한 화면 깜빡임 해결]]></title>
            <link>https://velog.io/@kang-bit/%EA%B0%80%EC%86%8D%ED%95%A9%EC%84%B1</link>
            <guid>https://velog.io/@kang-bit/%EA%B0%80%EC%86%8D%ED%95%A9%EC%84%B1</guid>
            <pubDate>Thu, 29 Aug 2024 11:23:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://kangbit.github.io/posts/hardware-acceleration/">https://kangbit.github.io/posts/hardware-acceleration/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VitePress + Github Pages로 블로그 만들기 - SEO]]></title>
            <link>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-SEO</link>
            <guid>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-SEO</guid>
            <pubDate>Sat, 25 May 2024 09:21:37 GMT</pubDate>
            <description><![CDATA[<h2 id="검색-엔진에-블로그-등록하기">검색 엔진에 블로그 등록하기</h2>
<p><strong><em>이 글은 VitePress의 실험적인 기능을 포함합니다</em></strong></p>
<hr>
<h3 id="sitemap-생성">Sitemap 생성</h3>
<p>Vitepress에서는 <a href="https://vitepress.vuejs.kr/guide/sitemap-generation">사이트맵 생성</a>을 지원합니다.</p>
<p>아래와 같이 <code>/.viteprss/config.mjs</code>에 sitemap을 작성합니다.</p>
<pre><code class="language-js">export default defineConfig({
  sitemap: {
    hostname: &quot;https://{userName}.github.io/&quot;,
  },
});</code></pre>
<p>이후에 빌드를 해보면 다음과 같이 <code>sitemap.xml</code> 파일이 포함되어 있는 것을 볼 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/c45beab5-999b-4147-ac53-c684c5d9a1a8/image.jpeg" width="40%" style="margin: 1rem 0;">


<p>이 사이트맵을 <code>Google Search Console</code>과 <code>Naver 웹마스터 도구</code>에 등록하면</p>
<p>구글과 네이버의 검색 결과에 내 블로그의 콘텐츠를 노출할 수 있습니다.</p>
<h3 id="google-검색-엔진에-등록">Google 검색 엔진에 등록</h3>
<p><a href="https://search.google.com/search-console/welcome">Google Search Console</a> 에 점속해서 내 블로그의 URL을 입력합니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/c3888990-1415-4ebc-838e-51c0178bd613/image.jpeg" width="80%" style="margin: 1rem 0;">

<p>입력 후 계속 버튼을 클릭하면 홈페이지의 소유권을 확인하기 위한 화면이 나타납니다.</p>
<p>저는 Google 애널리틱스를 통해 소유권을 인증하겠습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/c729e8f0-85c2-4f83-80a5-d11bc2eb1753/image.jpeg" width="80%" style="margin: 1rem 0;">

<p><a href="https://analytics.google.com/analytics/web">구글 애널리틱스</a>에 접속해서 계정과 속성을 생성 후, 태그 ID를 확인합니다.</p>
<p>태그 ID를 생성하고 확인하는 설명은 링크로 대체하겠습니다.</p>
<blockquote>
<p><a href="https://support.google.com/analytics/answer/9304153?hl=ko&amp;ref_topic=14088998&amp;sjid=17674989375840628322-AP">1.웹사이트 및 앱용 애널리틱스 설정</a><br><a href="https://support.google.com/analytics/answer/10110290?hl=ko&amp;ref_topic=14088998&amp;sjid=17674989375840628322-AP">2.설정 어시스턴스를 사용하여 GA4 속성 구성하기</a><br><a href="https://support.google.com/analytics/answer/9539598?hl=ko&amp;ref_topic=14088998&amp;sjid=17674989375840628322-AP">3.Google 태그 ID 찾기</a></p>
</blockquote>
<p>태그 ID를 찾았다면, 다음 스크립트를 <code>/.viteprss/config.mjs</code>에 삽입합니다.</p>
<pre><code class="language-js">export default defineConfig({
  head: [
    [
      &quot;script&quot;,
      {
        async: &quot;&quot;,
        src: &quot;https://www.googletagmanager.com/gtag/js?id={태그 ID}&quot;, // 태그 ID
      },
    ],
    [
      &quot;script&quot;,
      {},
      `window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag(&#39;js&#39;, new Date());
      gtag(&#39;config&#39;, &#39;{태그 ID}&#39;);`, // 태그 ID
    ],
  ],
});</code></pre>
<p>배포를 진행한 후, <code>Google Search Console</code>으로 돌아가 확인을 누르면 다음과 같은 팝업을 화인할 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/5f73d534-af99-4477-8764-5ca68d41ad1c/image.jpeg" width="80%" style="margin: 1rem 0;">

<p>속성으로 이동 버튼을 클릭해 속성 페이지로 이동합니다.</p>
<p>좌측 메뉴의 Sitemaps 메뉴를 통헤 페이지에 진입한 후, 사이트맵의 주소를 입력합니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/21cef782-2600-4fac-9206-ae80baebcf3f/image.jpeg" width="80%" style="margin: 1rem 0;">

<p>제출이 완료되면, 제출된 사이트맵에 다음과 같이 노출됩니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/4f3b03a6-05a8-49d7-b053-9e54c20d5eb6/image.jpeg" width="80%" style="margin: 1rem 0;">
며칠 기다리면, 구글 검색에 내 블로그가 노출되는 것을 확인할 수 있습니다.

<h3 id="naver-검색-엔진에-등록">Naver 검색 엔진에 등록</h3>
<p><a href="https://searchadvisor.naver.com/console/board">Naver Search Advisor</a>에 접속해서 내 블로그의 URL을 입력합니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/47ae08e8-ebf1-42f2-a52e-010f2e6ba94b/image.jpeg" width="80%" style="margin: 1rem 0;">

<p>구글과 마찬가지로 홈페이지의 소유권을 확인하기 위한 화면이 나타납니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/01b28b81-13e1-4ddb-9846-3a2321e08bf0/image.jpeg" width="80%" style="margin: 1rem 0;">


<p>HTML 태그를 통해 소유권을 확인하도록 하겠습니다.</p>
<p>content를 복사해서 <code>/.viteprss/config.mjs</code>에 스크립트를 삽입해 줍니다</p>
<pre><code class="language-js">export default defineConfig({
  head: [
    ...,
    [
      &quot;meta&quot;,
      {
        name: &quot;naver-site-verification&quot;,
        content: &#39;{content}&#39;,
      },
    ],
  ],
});</code></pre>
<p>배포 후 소유확인 버튼을 누르면 다음과 같은 팝업을 확인할 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/2b0ece02-ffeb-4b86-9c52-f2eb20706f34/image.jpeg" width="60%" style="margin: 1rem 0;">

<p>요청 &gt; 사이트맵 제출 메뉴로 이동하여 사이트맵 URL을 입력해 줍니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/9b0247b8-0bcf-484a-ae06-542a0b5b7b7c/image.jpeg" width="80%" style="margin: 1rem 0;">

<p>며칠 기다리면, 네이버 검색에 내 블로그가 노출되는 것을 확인할 수 있습니다.</p>
<hr>
<blockquote>
</blockquote>
<p><a href="https://kangbit.github.io/posts/vitepress/seo.html">VitePress + Github Pages로 블로그 만들기 - SEO</a></p>
<blockquote>
</blockquote>
<p><a href="https://vitepress.dev/">VitePress | Vite &amp; Vue로 구동되는 정적 사이트 생성기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VitePress + Github Pages로 블로그 만들기 - 배포]]></title>
            <link>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Fri, 17 May 2024 07:22:14 GMT</pubDate>
            <description><![CDATA[<h2 id="워크플로우-작성">워크플로우 작성</h2>
<p><code>.github/workflows</code> 폴더를 생성 후 <code>deplay.yml</code> 파일을 생성해 줍니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/afb20ff9-6a12-485d-a946-949cb746846e/image.png" width="40%" style="margin: 1rem 0;">

<p><code>deploy.yml</code> 파일에 <a href="https://vitepress.dev/guide/deploy#github-pages">공식 문서</a>에 있는 샘플 워크플로우를 복사해서 붙여줍니다.</p>
<p>다만 우리는 공식 문서의 가이드와는 폴더 구조가 다르기 때문에 <code>Upload artifact</code> 설정을 수정해 주어야 합니다.</p>
<pre><code>jobs:
  # 빌드 작업
  build:
    runs-on: ubuntu-latest
    steps:
      ...
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: .vitepress/dist 
          # path: docs/.vitepress/dist </code></pre><hr>
<h2 id="github-저장소에-소스-코드-올리기">Github 저장소에 소스 코드 올리기</h2>
<p>우선 새로운 저장소를 생성해 줍니다.</p>
<p>이 때 저장소의 이름을 <code>{username}.github.io/</code> 로 해주지 않으면,</p>
<p>블로그의 주소가 <code>https://{username}.github.io/{RepositoryName}</code> 이 됩니다.</p>
<p>주소에 <code>RepositoryName</code>이 포함되게 되면 SEO 설정 시에 어려움이 있을 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/66ba1e99-b344-49d4-ac55-1c5c2f911493/image.png" width="50%" style="margin: 1rem 0;">

<p>생성된 저장소의 설정을 수정해 줍니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/b1f13d11-7692-42bc-8680-d7d701a745bc/image.png" width="80%" style="margin: 1rem 0;">

<p>이제 터미널을 통해 저장소에 소스 코드를 업로드합니다.</p>
<pre><code>$ git init
$ git add --all
$ git commit -m &quot;first commit&quot;
$ git branch -M main
$ git remote add origin {깃허브 저장소 주소}
$ git push -u origin main</code></pre><p>업로드 후에 저장소의 Actions 페이지에 접근해 보면, 
배포 상황을 확인할 수 있습니다.</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/f2eef239-99ee-4334-a6ec-ebd106869268/image.png" width="80%" style="margin: 1rem 0;">

<p>위와 같이 배포가 성공적으로 완료되었다면, 
<code>https://{username}.github.io/</code>로 접근해서 홈페이지를 확인할 수 있습니다.</p>
<hr>
<h2 id="전체-코드">전체 코드</h2>
<p>전체 코드는 <a href="https://github.com/KangBit/kangbit.github.io">https://github.com/KangBit/kangbit.github.io</a> 에서 확인할 수 있습니다.</p>
<hr>
<blockquote>
</blockquote>
<p><a href="https://kangbit.github.io/posts/vitepress/github-deploy.html">VitePress + Github Pages로 블로그 만들기 - 배포</a></p>
<blockquote>
</blockquote>
<p><a href="https://vitepress.dev/">VitePress | Vite &amp; Vue로 구동되는 정적 사이트 생성기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VitePress + Github Pages로 블로그 만들기 - 생성]]></title>
            <link>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@kang-bit/VitePress-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Mon, 13 May 2024 16:02:19 GMT</pubDate>
            <description><![CDATA[<h2 id="설치-환경">설치 환경</h2>
<pre><code>Node version: v20.11.1
Vitepress version: v1.1.0</code></pre><hr>
<h2 id="프로젝트-생성">프로젝트 생성</h2>
<p>프로젝트 폴더를 생성한 후 폴더 내부에서 터미널을 통해 다음 명령어를 입력합니다.
최신 버전의 vitepress 프로젝트를 생성합니다.</p>
<pre><code class="language-bash">$ npx vitepress init</code></pre>
<p>프로젝트를 생성하기 위한 몇가지 설정을 미리 해주어야 합니다.</p>
<p>설정 파일과 컨텐츠들이 위치할 폴더를 정합니다.
현재 디렉토리로 설정합니다.</p>
<pre><code>◇  Where should VitePress initialize the config?
│  ./</code></pre><p>블로그의 타이틀을 정합니다. 
설정 파일에서 손쉽게 수정할 수 있으므로 무엇으로 하든 무방합니다.</p>
<pre><code>◇  Site title:
│  MyBlog</code></pre><p>블로그의 설명을 정합니다. ( html의 meta 태그로 렌더링 됩니다. )
설정 파일에서 손쉽게 수정할 수 있으므로 무엇으로 하든 무방합니다.</p>
<pre><code>◇  Site description:
│  My VitePress Blog</code></pre><p>테마를 커스텀해서 사용할지 말지 정합니다. 
저는 배포 후에 커스텀할 생각이므로 두번째 옵션을 선택했습니다.</p>
<pre><code>◇  Theme:
│  ○ Default Theme
│  ● Default Theme + Customization
│  ○ Custom Theme</code></pre><p>개인 블로그용이라면 타입스크립트까지는 필요하지 않을 것 같습니다.</p>
<pre><code>◇  Use TypeScript for config and theme files?
│  No</code></pre><p>프로젝트를 실행, 빌드, 프리뷰하기 위한 스크립트를 자동으로 작성하도록 합니다.</p>
<pre><code>◇  Add VitePress npm scripts to package.json?
│  Yes</code></pre><p>프로젝트를 완료하면 다음과 같은 팁이 나타나는데, 
지금은 잊어도 좋습니다. </p>
<pre><code>Tips:
- Since you&#39;ve chosen to customize the theme, you should also explicitly install vue as a dev dependency.</code></pre><hr>
<h2 id="프로젝트-실행">프로젝트 실행</h2>
<p>프로젝트 생성이 완료되었습니다.
이제 다음과 같은 구조로 프로젝트가 생성되었을 겁니다.</p>
<p>이제 터미널에서 프로젝트를 실행해 보겠습니다.</p>
<pre><code>$ npm run docs:dev</code></pre><p>다음과 같은 메세지가 나타나며 실행에 실패했다면, 정상입니다. </p>
<pre><code>sh: vitepress: command not found</code></pre><p>프로젝트에 vitepress를 설치해 주겠습니다.</p>
<pre><code>$ npm install -D vitepress</code></pre><p>다시 프로젝트를 실행하면 </p>
<pre><code>$ npm run docs:dev</code></pre><p>프로젝트가 실행되고, 접속할 수 있는 주소가 나타납니다.</p>
<pre><code>  vitepress v1.1.4

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help</code></pre><p>브라우저에서 해당 주소로 접속하면, 성공적으로 실행된 화면을 볼 수 있습니다.</p>
<hr>
<h2 id="폴더-구조-변경">폴더 구조 변경</h2>
<p>생성된 프로젝트의 폴더 구조를 보면, 다음과 같습니다.</p>
<img src='https://velog.velcdn.com/images/kang-bit/post/75017d06-b3b6-4dbb-989d-4cc18151ee9a/image.png' width="40%" style="margin: 1rem 0;"/>


<p><code>.md</code> 확장자의 파일들은 블로그에 추가될 페이지들입니다. 
최상위 디렉토리에 위치할 경우 페이지가 늘어날수록 관리하기 힘들 수 있습니다.
폴더를 만들어 넣어주도록 하겠습니다.
저는 pages로 폴더를 만들었습니다.</p>
<img src='https://velog.velcdn.com/images/kang-bit/post/9ff0e0f2-e703-4b8c-b3c1-32ce88c37de3/image.png' width="40%" style="margin: 1rem 0;"/>

<p>하지만 폴더 구조를 수정하니, 페이지에 접근할 수 없습니다.</p>
<img src='https://velog.velcdn.com/images/kang-bit/post/55076a7d-fc8d-474f-8642-2c4966408bf1/image.png' width="50%" style="margin: 1rem 0;"/>

<p>페이지의 폴더 구조가 변경되었음을 프로젝트에 알려주어야 합니다. 
설정 파일인 <code>/.viteprss/config.mjs</code> 에 srcDir을 추가해 주면,</p>
<pre><code class="language-js">export default defineConfig({
...
  srcDir: &quot;./pages&quot;,
...
})</code></pre>
<p>페이지에 다시 접근할 수 있습니다.</p>
<img src='https://velog.velcdn.com/images/kang-bit/post/06b011ae-6015-4fe6-9af2-a6d30564c234/image.png' width="50%" style="margin: 1rem 0;"/>

<hr>
<blockquote>
</blockquote>
<p><a href="https://kangbit.github.io/posts/vitepress/create-project.html">VitePress + Github Pages로 블로그 만들기 - 소개</a></p>
<blockquote>
</blockquote>
<p><a href="https://vitepress.dev/">VitePress | Vite &amp; Vue로 구동되는 정적 사이트 생성기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VitePress + Github Pages로 블로그 만들기 - 소개]]></title>
            <link>https://velog.io/@kang-bit/VitePress%EC%99%80-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kang-bit/VitePress%EC%99%80-Github-Pages%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 12 May 2024 12:08:02 GMT</pubDate>
            <description><![CDATA[<h2 id="vitepress">VitePress</h2>
<p>2024년 3월 21일 <a href="https://vitepress.dev/">VitePress</a>가 release 되었습니다
<a href="https://vitepress.dev/guide/what-is-vitepress">VitePress</a>는 빠르고 컨텐츠 중심의 웹사이트를 구축하기 위해 설계된 &#39;정적 사이트 생성기&#39; 입니다.</p>
<p>VitePress는 개인 블로그 뿐 아니라 팀 블로그를 구성하거나
가이드 문서로 사용하기에도 아주 좋은 도구로 보입니다.</p>
<hr>
<h2 id="왜-vitepress인가">왜 VitePress인가</h2>
<p>개발자가 아니여도 쉽게 생성할 수 있고, 
개발자라면 더 강력하고 아름다운 정적 사이트를 생성할 수 있습니다.</p>
<h3 id="문서">문서</h3>
<p>문서가 깔끔하고 번역이 잘 되어있습니다.</p>
<h3 id="vite--vue3-애플리케이션의-개발자-경험-지원">Vite + Vue3 애플리케이션의 개발자 경험 지원</h3>
<p>마크다운 내부에서도 Vue를 사용할 수 있습니다.
Vue3에 익숙한 개발자라면 손쉽게 테마를 커스텀 할 수 있고,
다양한 컴포넌트를 생성해 마크다운 내부에서 사용할 수 있습니다.</p>
<h3 id="레이아웃">레이아웃</h3>
<p>네비게이션 바, 사이드 바, Anchor aside, 푸터를 쉽게 추가할 수 있습니다. </p>
<h3 id="검색">검색</h3>
<p>검색 기능을 쉽게 추가할 수 있습니다.
<img src="https://velog.velcdn.com/images/kang-bit/post/ec750c20-b40f-4b39-a1b4-9836ea78dc07/image.png" alt=""></p>
<h3 id="코드-블록-기능">코드 블록 기능</h3>
<p>하이라이트, 포커싱, 라인 번호, 그룹화 등 다양한 코드 블록 기능을 제공합니다.
<img src="https://velog.velcdn.com/images/kang-bit/post/fdf915a2-e674-4eab-9e36-c7cef301a810/image.png" alt=""></p>
<h3 id="git-연동-기능">Git 연동 기능</h3>
<p><code>editLink</code>를 통해 페이지를 편집하는 링크를 표시할 수 있습니다.
<code>lastUpdated</code>를 통해 각 페이지의 마지막 Git 업데이트 타임스탬프를 가져올 수 있습니다.</p>
<hr>
<blockquote>
</blockquote>
<p><a href="https://kangbit.github.io/posts/vitepress/why-vitepress.html">VitePress + Github Pages로 블로그 만들기 - 소개</a></p>
<blockquote>
</blockquote>
<p><a href="https://vitepress.dev/">VitePress | Vite &amp; Vue로 구동되는 정적 사이트 생성기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 개발자의 안드로이드 컨퍼런스 후기]]></title>
            <link>https://velog.io/@kang-bit/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@kang-bit/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 15 Apr 2024 07:05:22 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>우연히 접한 안드로이드 컨퍼런스 행사 소식!</p>
<p>친한 네이티브 개발자를 꼬셔서</p>
<p>4월 6일 역삼에서 진행한 &#39;Native vs Flutter 그리고 KMP&#39; 에 다녀왔습니다.</p>
<hr>
<h2 id="주요-세션">주요 세션</h2>
<table>
<thead>
<tr>
<th>발표자</th>
<th>제목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>양수장</td>
<td>Flutter가 몸에 좋은 이유  : 개발자 건강을 위한 최고의 방법</td>
<td>Flutter가 개발자들에게 &#39;몸에 좋은 이유&#39;는 단순히 생산성 향상을 넘어섭니다. <br>Flutter와 함께라면, 개발자의 삶은 더욱 건강하고, 행복해질 수 있습니다.</td>
</tr>
<tr>
<td>유광무</td>
<td>코틀린으로 멀티플랫폼 만들기</td>
<td>코틀린으로도 멀티플랫폼을 만들 수 있다는 사실을 알고 계시나요?<br>KMP가 무엇인지와 다른 크로스플랫폼과의 차별점에 대해 알아봅니다.</td>
</tr>
<tr>
<td>박상권</td>
<td>튜닝의 끝은 결국 순정</td>
<td>지금까지 수많은 크로스 플랫폼들이 나타났다가 사라졌습니다.<br>지금까지 그래왔고 앞으로도 그럴것입니다.<br>돌고돌아 결국 네이티브 개발입니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="참가한-이유">참가한 이유</h2>
<p>네이티브 개발 경험이 있는 프론트엔드 개발자입니다.</p>
<p>평소에 &#39;자원이 충분하다면 네이티브로 개발하는게 최고&#39; 라고 생각하고 있었고,</p>
<p>다른 의견이 있다면 들어보고 싶었습니다.</p>
<p>또한 개발자로서 하고 있는 많은 선택에 대한 근거를 선명하게 하는 데 도움이 되지 않을까 했습니다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>###
<strong>&#39;크로스 플랫폼 프레임워크 개발도 네이티브 개발에 대한 이해가 바탕이 되어야 한다&#39;</strong></p>
<p><strong>&#39;결국 네이티브 개발 공부를 하게 된다&#39;</strong></p>
<p>모든 세션에서 동일하게 나온 내용입니다.</p>
<p>크로스 플랫폼 프레임워크를 이용해 개발하더라도 결국에는 각각의 플랫폼 위에서 동작하게 되고, </p>
<p>플랫폼의 자원을 사용하게 됩니다.</p>
<p>따라서 플랫폼에 대한 이해가 바탕이 되어야만 더 잘 개발할 수 있습니다.</p>
<p>하지만, 우리에겐 동료라는 자원이 있습니다.</p>
<p>옆에 네이티브 개발에 대해 깊게 이해하고 있는 개발자가 있다면</p>
<p>당장은 플랫폼에 대한 이해도가 크지 않아도 될 겁니다.</p>
<p>###
<strong>&#39;네이티브급 성능이라는 말 자체가 결국 네이티브보다 좋은 성능을 가질 수 없다는 뜻이다&#39;</strong></p>
<p>주 세션 이후의 라이트닝 토크에서 언급되었지만, 행사 중 가장 인상깊었던 말입니다.</p>
<p>그동안 머리 속에서만 떠돌던 내용이 문장으로 정립되었습니다.</p>
<p>&#39;급&#39;, &#39;처럼&#39; 이라는 말 안에 한계를 내포하고 있다라는 것.</p>
<p>위에서 이야기했듯이 결국은 플랫폼의 자원을 사용하게 되고, </p>
<p>이 자원을 사용하는 데 네이티브 앱보다 좋은 퍼포먼스를 내기는 쉽지 않을 겁니다.</p>
<p>하지만, 그렇게 큰 차이가 나지는 않습니다.</p>
<p>많은 경우에 이정도의 성능 차이가 큰 문제가 되지는 않을 겁니다.</p>
<p>###
<strong>결국엔 네이티브! 를 바탕으로 한 경험입니다.</strong></p>
<p>개발 범위가 어느 정도이고 활용할 수 있는 자원이 어느 정도인지 판단할 수 있어야 합니다.</p>
<p>자원을 적절하게 활용할 수 있으면서도 요구조건을 구현하는 데 어려움이 없는 기술이 무엇일지 잘 선택해야 합니다.</p>
<hr>
<p>프론트엔드에서도 선택해야 하는 것들이 상당히 많아졌습니다.</p>
<p>React vs Vue vs Angular, CSR vs SSR vs SSG, TS 사용 여부, 테스트 코드 작성 여부 등등.</p>
<p>마찬가지로, 틀린 기술은 없습니다. </p>
<p>충분히 작은 프로젝트에 React를 사용할 필요가 있을까요?</p>
<p>충분하지 않은 시간 내에 개발해야 하고 유지보수되지 않을 프로젝트에 TS나 테스트 코드를 적용해야 할까요?
(물론, 프로젝트 규모에 따라서 필요할 수 있습니다)</p>
<p>상황에 맞는 기술을 선택할 수 있도록 경험을 쌓고 안목을 길러야 합니다.</p>
<hr>
<p><a href="https://event-us.kr/ted/event/79581">행사 정보</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[vue3] useScrollLoad]]></title>
            <link>https://velog.io/@kang-bit/Vue3-useScrollLoad</link>
            <guid>https://velog.io/@kang-bit/Vue3-useScrollLoad</guid>
            <pubDate>Fri, 14 Apr 2023 08:06:52 GMT</pubDate>
            <description><![CDATA[<p><code>Infinite Scroll</code> 은 페이지 하단에 도달할 때마다 새로운 컨텐츠를 계속해서 로드하는  방법입니다</p>
<p>이를 위해 스크롤이 페이지 하단에 도달했음을 감지해서 데이터를 불러오는 로직이 필요합니다</p>
<p>이런 로직이 여러 페이지에서 반복되지 않도록 하기 위해  <code>composable</code>로 분리한 내용을 공유합니다
<br></p>
<p><code>vue3</code> + <code>typescript</code> 로 작성했지만, 원활한 이해를 돕기 위해 초반 설명은 <code>typescript</code> 없이 진행합니다</p>
<p>테스트용 API 주소는 <a href="https://jsonplaceholder.typicode.com/">JSONPlaceholder</a> 를 활용했습니다</p>
<hr>
<h2 id="주-기능-구현">주 기능 구현</h2>
<h3 id="이렇게-사용하고-싶습니다">이렇게 사용하고 싶습니다</h3>
<p>컨테이너와 주소를 파라미터로 전달하면, </p>
<p>컨테이너 하단에 도달했을 때 자동으로 업데이트 되는 리스트를 반환해야 합니다</p>
<pre><code class="language-javascript">const { list } = useScrollLoad(scrollContainer, url);</code></pre>
<p>API 사양에 따라 페이징 방법이 다를 수 있으므로 주소는 함수의 반환값으로 전달하도록 하겠습니다</p>
<p>JSONPlaceholder를 활용한 주소는 다음과 같습니다</p>
<pre><code class="language-javascript">const { list } = useScrollLoad(scrollContainer, (start, size) =&gt; {
  return `https://jsonplaceholder.typicode.com/todos?_start=${start}&amp;_limit=${size}`;
});</code></pre>
<br>

<h3 id="구현">구현</h3>
<p>데이터를 로드하는 함수를 마운트되었을 때 한번 호출합니다</p>
<pre><code class="language-javascript">// @/composilbes/scrollLoad.js
import { onMounted, onUnmounted, ref, unref } from &quot;vue&quot;;

export const useScrollLoad = (scrollContainer, url) =&gt; {
  const size = 10;

  const start = ref(0);
  const list = ref([]);

  const loadMore = () =&gt; {
    start.value = start.value + size;
    load();
  };

  const load = () =&gt; {
    fetchData();
  };

  const fetchData = () =&gt; {
    fetch(url(start.value, size))
      .then((res) =&gt; res.json())
      .then((res) =&gt; {
        list.value.push(...res);
      });
  };

  onMounted(() =&gt; {
    load();
  });
}</code></pre>
<br>

<p>스크롤이 하단에 도달하는 이벤트를 감지하기 위해 이벤트 리스너를 추가합니다</p>
<pre><code class="language-javascript">  // 컨테이너가 ref인 것으로 가정합니다
  // 뒤에서 ref가 아닌 경우를 다루겠습니다
  onMounted(() =&gt; {
    load();
    scrollContainer.value.addEventListener(&quot;scroll&quot;, handleScrollEvent);
  });
  onUnmounted(() =&gt; {
    scrollContainer.value.removeEventListener(&quot;scroll&quot;, handleScrollEvent);
  });</code></pre>
<pre><code class="language-javascript">  const handleScrollEvent = (e) =&gt; {
    const bottom = getScrollRest(e.target);
    if (bottom === 0) {
      loadMore();
    }
  };

  // 스크롤 영역 최하단까지의 남은 거리를 구합니다
  const getScrollRest = (element) =&gt; {
    const clientHeight = element.clientHeight;
    const scrollHeight = element.scrollHeight;
    const scrollTop = element.scrollTop;

    return scrollHeight - scrollTop - clientHeight;
  };</code></pre>
<br>

<h3 id="이제-다음과-같이-사용할-수-있습니다">이제, 다음과 같이 사용할 수 있습니다</h3>
<pre><code class="language-vue">&lt;!-- @/components/ScrollBox.vue --&gt;
&lt;template&gt;
  &lt;div class=&quot;container&quot; ref=&quot;scrollContainer&quot;&gt;
    &lt;div v-for=&quot;(item, idx) in list&quot; :key=&quot;idx&quot;&gt;
      &lt;p&gt;{{ item.title }}&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { useScrollLoad } from &quot;@/composibles/scrollLoad&quot;;
import { ref } from &quot;vue&quot;;

const scrollContainer = ref(null);

const { list } = useScrollLoad(scrollContainer, (start, size) =&gt; {
  return `https://jsonplaceholder.typicode.com/todos?_start=${start}&amp;_limit=${size}`;
});
&lt;/script&gt;</code></pre>
<br>

<p>여기까지 완성된 코드는 <a href="https://github.com/KangBit/vue-composible/blob/2f38c217b707d5d4973e888d8c397e8668a127de/src/composibles/scrollLoad.js">GITHUB</a>에서 확인할 수 있습니다</p>
<hr>
<h2 id="컨테이너가-window라면">컨테이너가 window라면?</h2>
<p>스크롤이 적용되는 컨테이너가 항상 <code>ref</code>로 주어지진 않을 것 같습니다</p>
<p><code>window</code>로 한번 적용해보겠습니다</p>
<pre><code class="language-javascript">const { list } = useScrollLoad(window, (start, size) =&gt; {
  return `https://jsonplaceholder.typicode.com/todos?_start=${start}&amp;_limit=${size}`;
});</code></pre>
<br>

<p><code>useScrollLoad</code> 로 전달되는 <code>scrollContainer</code> 는 더이상 <code>ref</code>가 아니므로 다음과 같이 수정할 수 있습니다</p>
<pre><code class="language-js">  onMounted(() =&gt; {
    scrollContainer.addEventListener(&quot;scroll&quot;, handleScrollEvent);
  });</code></pre>
<br>

<p>하지만 <code>ref</code>로 전달되는 경우도 배제할 수 없습니다</p>
<p>다시 수정합니다</p>
<pre><code class="language-js">  onMounted(() =&gt; {
    const el = unref(scrollContainer);

    if (el) {
      load();
      el.addEventListener(&quot;scroll&quot;, handleScrollEvent);
    }
  });

  onUnmounted(() =&gt; {
    const el = unref(scrollContainer);

    if (el) {
        el.removeEventListener(&quot;scroll&quot;, handleScrollEvent);
    }
  });</code></pre>
<br>

<p><code>handleScrollEvent</code>에서 <code>event.target</code> 을 잘 찾지 못합니다</p>
<p>다음과 같이 수정합니다</p>
<pre><code class="language-js">  const handleScrollEvent = (e) =&gt; {
    let el = e.target;
    if (el.documentElement) {
      el = el.documentElement;
    }

    const bottom = getScrollRest(el);
    if (bottom === 0) {
      loadMore();
    }
  };</code></pre>
<br>

<p>여기까지 완성된 코드는 <a href="https://github.com/KangBit/vue-composible/blob/e8b600569cd7e394e614fb498e2f855c423c2392/src/composibles/scrollLoad.js">GITHUB</a>에서 확인할 수 있습니다</p>
<hr>
<h2 id="typescript-적용">Typescript 적용</h2>
<p>설명을 위해 빼두었던 Typescript를 다시 적용해보겠습니다</p>
<pre><code class="language-typescript">...
import type { Ref } from &quot;vue&quot;;

type ContainerType = HTMLElement | Window | Document | null;

export const useScrollLoad = (
  scrollContainer: ContainerType | Ref&lt;ContainerType&gt;,
  url: (start: number, size: number) =&gt; string
) =&gt; {
  ...
  const list: Ref&lt;any[]&gt; = ref([]); // `any`가 조금 거슬립니다 뒤에서 수정하도록 하겠습니다
  ...
  const getScrollRest = (element: HTMLElement) =&gt; {
    ...
  }
  ...
}</code></pre>
<br>

<p>아래에서는 오류가 발생합니다
<code>HTMLElement</code>는 <code>documentElement</code>를 프로퍼티로 가지지 않습니다</p>
<pre><code class="language-typescript">  const handleScrollEvent = (e: Event) =&gt; {
    const element = e.target as HTMLElement;
    if (element.documentElement) {        // ts Error!!
      element = element.documentElement;  // ts Error!!
    }

    const bottom = getScrollRest(element);
    if (bottom === 0) {
      loadMore();
    }
  };</code></pre>
<p>다음처럼 변경합니다</p>
<pre><code class="language-ts">  const handleScrollEvent = (e: Event) =&gt; {
    let element = e.target as HTMLElement;
    if ((e.target as Document).documentElement) {
      element = (e.target as Document).documentElement as HTMLElement;
    }

    const bottom = getScrollRest(element);

    if (bottom === 0) {
      loadMore();
    }
  };</code></pre>
<p>이제 다시 정상적으로 동작합니다!
<br></p>
<p>그럼 이제 위에서 신경쓰였던 <code>any</code>를 수정해 보겠습니다</p>
<pre><code class="language-typescript">// @/composilbes/scrollLoad.ts
export const useScrollLoad =  &lt;ListItem&gt;(
  scrollContainer: ContainerType | Ref&lt;ContainerType&gt;,
  url: (start: number, size: number) =&gt; string
) =&gt; {
  ...
  const list: Ref&lt;ListItem[]&gt; = ref([]); 
  ...
}</code></pre>
<pre><code class="language-typescript">// @/components/ScrollBox.vue
type ListItem = {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
};

const { list } = useScrollLoad&lt;ListItem&gt;(window, (start, size) =&gt; {
  return `https://jsonplaceholder.typicode.com/todos?_start=${start}&amp;_limit=${size}`;
});</code></pre>
<p>여기까지 완성된 코드는 <a href="https://github.com/KangBit/vue-composible/blob/e911160c53ebe4940b3e24ba090a1c85443e24c7/src/composibles/scrollLoad.ts">GITHUB</a>에서 확인할 수 있습니다</p>
<hr>
<h2 id="개선">개선</h2>
<h3 id="최하단에-도달하기-전에-미리-데이터-불러오기">최하단에 도달하기 전에 미리 데이터 불러오기</h3>
<p>스크롤이 바닥에 닿기를 기다렸다가 데이터를 불러오면 
사용자는 데이터를 불러오는 시간을 기다려야 합니다</p>
<p>조금 이른 시점에 새로운 컨텐츠를 미리 로드할 수 있도록 수정합니다</p>
<pre><code class="language-typescript">const handleScrollEvent = (e: Event) =&gt; {
  let element = e.target as HTMLElement;
  if ((e.target as Document).documentElement) {
    element = (e.target as Document).documentElement as HTMLElement;
  }

  const bottom = getScrollRest(element);
  if (bottom &lt; element.clientHeight * 2) { // 수정됨
    loadMore();
  }
};</code></pre>
<p>그런데 이게 웬걸, 한번에 여러번의 api 호출이 발생합니다</p>
<p>새로운 컨텐츠가 추가되어서 스크롤 영역이 넓어지기도 전에 또다른 스크롤 이벤트가 발생하기 때문입니다</p>
<p>데이터를 가져오는 동안에는 새로운 데이터를 요청하지 않도록 합니다</p>
<pre><code class="language-typescript">const isLoading = ref(false);

const loadMore = () =&gt; {
  if (isLoading.value) {
    return;
  }
  ...
}

const fetchData = () =&gt; {
  isLoading.value = true;
  fetch(url(start.value, size))
      .then((res) =&gt; res.json())
      .then((res) =&gt; {
        list.value.push(...res);
      })
      .catch((err) =&gt; {
        start.value = start.value - size;
      })
      .finally(() =&gt; {
        isLoading.value = false;
      });
};</code></pre>
<br>

<h3 id="스크롤-최적화">스크롤 최적화</h3>
<p><code>handleScrollEvent</code>에서 로딩 상태를 체크할 수도 있을텐데 그러지 않은 이유는 무엇일까요</p>
<pre><code class="language-ts">const handleScrollEvent = (e: Event) =&gt; {
  //if (isLoading.value) {
  //  return;
  //}

  let element = e.target as HTMLElement;
  if ((e.target as Document).documentElement) {
    element = (e.target as Document).documentElement as HTMLElement;
  }

  const bottom = getScrollRest(element);
  if (bottom &lt; element.clientHeight * 2) {
    loadMore();
  }
};</code></pre>
<p><code>handleScrollEvent</code>는 <code>target</code>을 가져오고, 영역 하단까지의 거리를 계산하는 비용을 계속 지불하고 있습니다</p>
<p>여기서 로딩 상태를 체크한다고 해도 로딩중이 아닐 때의 비용은 줄일 수 없습니다</p>
<p>이 비용은 <code>Throttle</code>을 이용해 줄이려고 합니다</p>
<hr>
<h2 id="마치며">마치며</h2>
<p><code>Throttle</code> 처리를 포함하여 몇가지 아이디어와 수정사항이 있지만, 글이 너무 길어져서 여기까지만 작성합니다</p>
<p>감사합니다</p>
<h4 id="적지-못한-아이디어">적지 못한 아이디어</h4>
<pre><code class="language-js">// @/components/ScrollBox.vue
const url = &quot;https://jsonplaceholder.typicode.com/todos&quot;;
const params = ref({})
const getQueryString = (start: number, size: number) =&gt; {
  const qeuryParams = {
    ...params,
    _start: start,
    _limit: size,
  }

  return &#39;?&#39; + Object.entries(qeuryParams).map(([key, value])=&gt;{
    return `${key}=${value}`
  }).join(&#39;&amp;&#39;);
}
const { list } = useScrollLoad&lt;ListItem&gt;(scrollContainer, url, getQueryString);</code></pre>
<hr>
<pre><code class="language-js">// @/composilbes/scrollLoad.ts
useEventListener(element, &quot;scroll&quot;, handleScrollEvent);</code></pre>
<hr>
<h2 id="github"><a href="https://github.com/KangBit/vue-composible">GITHUB</a></h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 - Lv2] 당구 연습 (JS)]]></title>
            <link>https://velog.io/@kang-bit/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv2-%EB%8B%B9%EA%B5%AC-%EC%97%B0%EC%8A%B5-JS</link>
            <guid>https://velog.io/@kang-bit/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv2-%EB%8B%B9%EA%B5%AC-%EC%97%B0%EC%8A%B5-JS</guid>
            <pubDate>Mon, 20 Mar 2023 06:15:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kang-bit/post/251cf1b8-9bc3-42ab-984b-ffd2941005dc/image.png" alt=""></p>
<p><strong><em>( Level 1에서 Level 2로 변경되었네요)</em></strong></p>
<p>몇가지 실수로 인해 어렵게 느껴졌던 문제입니다</p>
<hr>
<h2 id="1-접근방법">1. 접근방법</h2>
<p>입사각과 반사각이 같고, 벽에 부딪히기 전과 후의 이동거리가 같습니다. </p>
<p>다음과 같이 흰색 공의 위치를 벽을 축으로 대칭이동시켰을 때, 흰색 공이 이동한 거리는 노란 공과 검은 공의 거리와 같을 것이라는 것을 쉽게 상상할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kang-bit/post/0962581b-b076-492d-845b-4ca1705e5ac8/image.png" alt=""></p>
<h2 id="2-문제를-어렵게-느끼게-했던-실수들">2. 문제를 어렵게 느끼게 했던 실수들</h2>
<h3 id="문제를-꼼꼼히-읽지-않았다">문제를 꼼꼼히 읽지 않았다</h3>
<p>문제에 &#39;머쓱이는 항상 같은 위치에 공을 놓고 쳐서 리스트에 담긴 위치에 놓인 공을 맞춥니다.&#39; 라는 내용이 있습니다.</p>
<p>그런데도 각 수행마다 공의 위치가 변하는 것으로 생각했습니다. 머쓱합니다...</p>
<p>그래서 처음에는 <code>balls.map</code> 이 아니라 <code>balls.reduce</code> 로 방향을 잡고 시작해버렸네요.</p>
<h3 id="네-코너에-인접한-경우의-수를-생각했다">네 코너에 인접한 경우의 수를 생각했다</h3>
<p>문제에 다음과 같이 공이 코너에 맞는 경우 어떻게 진행되는지에 대해 설명하기 위한 그림이 있습니다.</p>
<p>하지만 애초에 코너에 맞는 경우를 생각할 필요가 없습니다.</p>
<p>잘 생각해보면, 코너에 맞고 나오는 거리보다 가까운 경우가 항상 존재함을 알 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/kang-bit/post/ee8301ea-d708-4adf-a043-9e67e6ca3889/image.png" alt=""></p>
<h3 id="코드를-더-더-줄이고-싶었다">코드를 더 더 줄이고 싶었다</h3>
<p>각 네 면에 대해서 각각 조건문을 달지 않고 싶었습니다만, 위 두가지 실수까지 겹치며 너무 오랜 시간이 흘러서 패스!</p>
<h2 id="3-코드">3. 코드</h2>
<p>각 수행마다 최소 이동거리를 계산하여 배열로 반환합니다</p>
<pre><code class="language-javascript">function solution(m, n, startX, startY, balls) {
    return balls.map(([x, y]) =&gt; {
        return getNearlist(m, n, startX, startY, x, y);
    });
}</code></pre>
<p>(StartX, StartY)를 네 벽에 대칭이동시켜 목적구와의 거리를 계산합니다.</p>
<p>이 때 대칭이동을 하려는 좌표와의 연장선상에 목적구가 있을 경우에는 거리를 계산하지 않습니다.</p>
<p>이렇게 계산한 거리들의 최솟값을 반환합니다.</p>
<pre><code class="language-javascript">
const getNearlist = (m, n, x1, y1, x2, y2) =&gt; {
    let symmetryPoints = [];

    if (x1 !== x2 || y1 &lt; y2) {
        const bottom = [x1, -y1];
        symmetryPoints.push(bottom);
    }
    if (x1 !== x2 || y1 &gt; y2) {
        const top = [x1, n + n - y1];
        symmetryPoints.push(top);
    }
    if (y1 !== y2 || x1 &lt; x2) {
        const left = [-x1, y1];
        symmetryPoints.push(left);
    }
    if (y1 !== y2 || x1 &gt; x2) {
        const right = [m + m - x1, y1];
        symmetryPoints.push(right);
    }

    return symmetryPoints.reduce((min, [x,y])=&gt;{
        return Math.min(min, (x-x2)**2 + (y-y2)**2);
    }, m**2 + n**2);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[vue2] 리스트 스켈레톤 UI 적용 과정]]></title>
            <link>https://velog.io/@kang-bit/vue2-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@kang-bit/vue2-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 26 Nov 2022 05:26:23 GMT</pubDate>
            <description><![CDATA[<h2 id="불편함">불편함</h2>
<p>최근에 아래와 같은 컴포넌트를 수정할 일이 있었습니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/9fd479ba-cff3-4c2f-b4cc-41ce860837b4/image.png" width="60%">


<p>하나의 리스트는 탭을 선택할 때마다 목록을 비운 뒤에 api 호출을 통해 새로운 목록을 가져옵니다</p>
<p>이 때 리스트의 크기가 변화하면서 부자연스럽게 동작했습니다</p>
<img src= "https://velog.velcdn.com/images/kang-bit/post/bb44a080-7fe1-4788-b32e-897a93a1a1c8/image.gif" width="60%">

<p>부자연스러움을 해결하는 것이 일의 목적은 아니었지만,</p>
<p>그냥 보기 싫어서 어떻게 해결하면 좋을지 생각해 보게 됐습니다</p>
<hr>
<h2 id="생각의-흐름">생각의 흐름</h2>
<h3 id="고려사항">고려사항</h3>
<p>가장 쉽게 생각할 수 있는 것은 Api 호출 전 목록을 비우지 않는 것입니다</p>
<p>현재는 리스트의 높이가 x -&gt; 0 -&gt; y 로 변하지만, </p>
<p>목록을 비우지 않은 상태에서 다음 목록을 바로 로드한다면 높이의 변화는 x -&gt; y 로 한단계 줄어듭니다</p>
<p>하지만 그렇게 되면 사용자는 해당 리스트가 로딩중인지 아닌지 알 수 없게 됩니다</p>
<p>그래서 처음 생각한 것은 목록을 비우지 않은 상태로 로딩 상태를 전달할 수 있도록 하는 것이었습니다</p>
<h3 id="첫번째-아이디어">첫번째 아이디어</h3>
<p>Progress Layer로 전체 화면을 덮는 것이었습니다</p>
<p>서비스 내부에서 대부분의 Data Fetch를 그렇게 처리하기도 했고, 가장 간편했습니다</p>
<p>그러나 목록을 빠르게 가져왔을 때 Progress Layer가 순식간에 사라지며 화면이 깜빡이는 것처럼 보였습니다</p>
<p>일정 시간동안은 Layer가 유지되도록 할 수도 있겠지만, 이 외에도 문제가 더 있었습니다</p>
<p>Layer에 접근하는 코드가 전체 소스 여기저기 흩어져 있어서 정확한 동작을 보장할 수도 없으며</p>
<p>탭을 변경하지 않은 다른 컴포넌트에 영향이 있다는 점에서 고려사항에서 제외했습니다</p>
<h3 id="두번째-아이디어">두번째 아이디어</h3>
<p>각 컴포넌트 내부에서 Progress Bar를 표현하는 것이었습니다.</p>
<p>그러나 서비스 내에 이렇게 표현하는 화면이 전혀 없어서 이질적인 느낌이 들었습니다 </p>
<p>디자인 협의 없이 독단적으로 결정할 수도 없었기에 다른 방안이 필요했습니다</p>
<h3 id="세번째-아이디어">세번째 아이디어</h3>
<p>로딩중인 화면의 크기를 0이 아닌 값으로 고정하고자 했습니다</p>
<p>no-data 화면과 로딩중인 화면의 높이가 같아진다면 </p>
<p>no-data 에서 출발하거나 no-data 가 되는 경우에는 훨씬 자연스럽게 보일 것 같았습니다</p>
<p>데이터가 있는 경우에도 30 -&gt; 0 -&gt; 80 의 변화보다는 30 -&gt; 50 -&gt; 80 의 변화가 더 자연스러울거라고 생각했습니다</p>
<p>( 지금 보면 터무니없는 생각입니다 </p>
<p>첫번째 탭의 리스트 높이도 10이고 두번째 탭의 리스트 높이도 10이라고 했을 때 </p>
<p>10 -&gt; 50 -&gt; 10 보다는  10 -&gt; 0 -&gt; 10의 변화가 더 자연스러울 것 같습니다 )</p>
<h3 id="네번째-아이디어">네번째 아이디어</h3>
<p>이왕이면 스켈레톤 아이템을 이용해서 로딩중이라는 티도 조금 더 내고</p>
<p>화면이 심심하지 않게 보이도록 하고 싶었습니다</p>
<hr>
<h2 id="구현">구현</h2>
<h3 id="1-스켈레톤-아이템-생성">1. 스켈레톤 아이템 생성</h3>
<p>다음과 같이 실제 아이템과 같은 모양의 스켈레톤 아이템을 만들었습니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/601b8f55-2ebe-4707-a19f-1c1d73cd6994/image.png" width="60%">

<img src="https://velog.velcdn.com/images/kang-bit/post/5ddb2077-c88e-480e-a6fb-aee74627817c/image.png" width="60%">

<p>각각의 리스트 컴포넌트는 다음과 같이 구성했습니다</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;h2&gt;{{ title }}&lt;/h2&gt;
    &lt;tag-menu
      :menus=&quot;menus&quot;
      @onChangeMenu=&quot;handleChageMenu&quot;
    &gt;&lt;/tag-menu&gt;

    &lt;ul v-show=&quot;list.length === 0&quot; class=&quot;list-container&quot;&gt;
      &lt;skeleton-item v-for=&quot;idx in 5&quot; :key=&quot;idx&quot;&gt;&lt;/skeleton-item&gt;
    &lt;/ul&gt;
    &lt;ul v-show=&quot;list.length &gt; 0&quot; class=&quot;list-container&quot;&gt;
      &lt;list-item
        v-for=&quot;(item, idx) in list&quot;
        :key=&quot;idx&quot;
        :item=&quot;item&quot;
      &gt;&lt;/list-item&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><pre><code>&lt;script&gt;
export default {
  ..., // 생략
  props: {
    title: {
      type: String,
      default: &quot;&quot;,
    },
    menus: {
      type: Array,
      default: () =&gt; [],
    },
    totalList: {
      type: Array,
      default: () =&gt; [],
    },
  },

  data(){
    return {
      list: [],
    }
  },

  methods: {
    handleChageMenu(idx) {
      this.list = [];

      setTimeout(() =&gt; { // 로딩 딜레이는 타임아웃으로으로 대체했습니다 
        this.list = this.totalList[idx];
      }, 500);
    },
  },
}
&lt;/script&gt;</code></pre><img src="https://velog.velcdn.com/images/kang-bit/post/b86ffbdc-e38e-4d08-b4cf-09c69ef0a56f/image.gif" width="60%">

<p>조금 덜 부자연스러운 것 같고, 로딩중이구나 하는 느낌도 드시나요?</p>
<p>하지만, 아이템의 갯수에 따라서 여전히 부자연스럽다고 느꼈습니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/f3c45e42-39e4-4577-ac65-94a7c004fa86/image.gif" width="60%">

<h3 id="2-변경-전-리스트-갯수를-활용">2. 변경 전 리스트 갯수를 활용</h3>
<p>vue의 watch속성을 이용하면 변경 전의 리스트 갯수를 알 수 있고,</p>
<p>이것을 이용해 높이의 변화를 줄일 수 있을 것 같았습니다</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;h2&gt;{{ title }}&lt;/h2&gt;
    &lt;tag-menu
      :menus=&quot;menus&quot;
      @onChangeMenu=&quot;handleChageMenu&quot;
    &gt;&lt;/tag-menu&gt;

    &lt;ul v-show=&quot;list.length === 0&quot; class=&quot;list-container&quot;&gt;
      &lt;skeleton-item v-for=&quot;idx in skeletonSize&quot; :key=&quot;idx&quot;&gt;&lt;/skeleton-item&gt;
    &lt;/ul&gt;
    &lt;ul v-show=&quot;list.length &gt; 0&quot; class=&quot;list-container&quot;&gt;
      &lt;list-item
        v-for=&quot;(item, idx) in list&quot;
        :key=&quot;idx&quot;
        :item=&quot;item&quot;
      &gt;&lt;/list-item&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><pre><code>&lt;script&gt;
export default {
  ..., // 생략
  data(){
    return {
      list: [],
      skeletonSize: 5
    }
  },
  watch: {
    list(curr, prev){
      this.skeletonSize = prev.length;
    }
  },
  methods: {
    handleChageMenu(idx) {
      this.list = [];

      setTimeout(() =&gt; {
        this.list = this.totalList[idx];
      }, 500);
    },
  },
}
&lt;/script&gt;</code></pre><img src="https://velog.velcdn.com/images/kang-bit/post/384792ea-7311-4271-90e9-85f212760468/image.gif" width="60%">

<p>조금 더 나아졌나요?</p>
<h3 id="3-noitem-처리">3. NoItem 처리</h3>
<p>만약 서버에서 가져온 리스트가 비어있다면, 스켈레톤 아이템이 노출될겁니다. </p>
<p>로딩 상태와 데이터가 없는 상태의 구분이 필요하겠네요</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;h2&gt;{{ title }}&lt;/h2&gt;
    &lt;tag-menu
      :menus=&quot;menus&quot;
      @onChangeMenu=&quot;handleChageMenu&quot;
    &gt;&lt;/tag-menu&gt;

    &lt;ul v-show=&quot;isShowSkeletonItems&quot; class=&quot;list-container&quot;&gt;
      &lt;skeleton-item v-for=&quot;idx in skeletonSize&quot; :key=&quot;idx&quot;&gt;&lt;/skeleton-item&gt;
    &lt;/ul&gt;
    &lt;ul v-show=&quot;isShowNoItem&quot; class=&quot;list-container&quot;&gt;
      &lt;h1&gt;NO-ITEM&lt;/h1&gt;
    &lt;/ul&gt;
    &lt;ul v-show=&quot;isShowItems&quot; class=&quot;list-container&quot;&gt;
      &lt;list-item
        v-for=&quot;(item, idx) in list&quot;
        :key=&quot;idx&quot;
        :item=&quot;item&quot;
      &gt;&lt;/list-item&gt;
    &lt;/ul&gt;

  &lt;/div&gt;
&lt;/template&gt;</code></pre><pre><code>&lt;script&gt;
export default {
  ..., // 생략
  data(){
    return {
      list: null,
      skeletonSize: 5
    }
  },

  computed: {
    isShowSkeletonItems() {
      return this.list === null;
    },
    isShowNoItem() {
      return Array.isArray(this.list) &amp;&amp; this.list.length === 0;
    },
    isShowItems() {
      return !this.isShowSkeletonItems &amp;&amp; !this.isShowNoItem;
    },
  },

  watch: {
    list(curr, prev){
      if (!Array.isArray(prev)) {
        this.skeletonSize = 5;
        return;
      }

      if (prev.length === 0) {
        this.skeletonSize = 1;
      } else {
        this.skeletonSize = prev.length;
      }
    }
  },
  methods: {
    handleChageMenu(idx) {
      this.list = null;

      setTimeout(() =&gt; {
        this.list = this.totalList[idx];
      }, 500);
    },
  },
}
&lt;/script&gt;</code></pre><hr>
<h2 id="대단한건-아니지만">대단한건 아니지만</h2>
<p>대단한 기술도 아니고 대단한 발상도 아니지만</p>
<p>어떤 사고의 과정을 통해 결과물을 도출했는지 기록하고 공유하고 싶었습니다</p>
<hr>
<h2 id="전체-코드">전체 코드</h2>
<ul>
<li><a href="https://github.com/KangBit/SkeletonItems">Kangbit Github</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github pages로 웹페이지 자동 배포하기 (no-Jekyll)]]></title>
            <link>https://velog.io/@kang-bit/Github-pages</link>
            <guid>https://velog.io/@kang-bit/Github-pages</guid>
            <pubDate>Sun, 23 Oct 2022 05:57:05 GMT</pubDate>
            <description><![CDATA[<h2 id="00-들어가며">00. 들어가며</h2>
<p>예전에 설정해 두었던 github pages가 동작하지 않더군요</p>
<p>그래서 그냥 새로 만들기로 했습니다</p>
<p>생각보다 삽질을 많이 하게 되어서 이번엔 정리를 좀 해두려구요</p>
<h2 id="01-프로젝트-생성">01. 프로젝트 생성</h2>
<p>저는 다음과 같이 프로젝트를 생성했습니다.</p>
<pre><code class="language-bash">npx create-react-app bitpage --template typescript</code></pre>
<h2 id="02-git-연결">02. Git 연결</h2>
<p>새로운 Repository를 생성합니다.</p>
<img src = "https://velog.velcdn.com/images/kang-bit/post/7fa489bc-0a12-4668-9bbf-448c50018374/image.png">

<p>아래의 주소를 복사하고</p>
<img src = "https://velog.velcdn.com/images/kang-bit/post/fc3d56ea-3d35-4aa7-a1f4-bcbcaa86c844/image.png">

<p>프로젝트 폴더로 이동해서 깃을 연결합니다 </p>
<pre><code class="language-bash">cd bitpage
git remote add origin &#39;복사한 주소&#39;
git branch -M main
git push -u origin main</code></pre>
<h2 id="03-gh-pages-설정">03. gh-pages 설정</h2>
<pre><code class="language-bash">npm install -D gh-pages</code></pre>
<p>gh-pages 모듈을 설치하고 </p>
<p>프로젝트의 package.json에 homepage와 deploy 스크립트를 작성합니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/4045ac2c-ba3e-404e-847f-886d1fcf4c0f/image.png">

<p>이제 배포를 하고 나니,</p>
<pre><code class="language-bash">npm run deploy</code></pre>
<p>다음과 같이 gh-pages라는 브랜치가 생겼습니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/41dbc11d-376f-48f8-a3d2-6635e8111f05/image.png">

<p>이제 settings로 이동하여 Pages 설정을 다음과 같이 변경해주면,</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/9bd8dc9b-1ef2-406d-9ac5-3f4c54336b53/image.png">

<p>다음과 같이 페이지를 확인할 수 있습니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/04178925-e361-4b34-8a6b-4411c86d9ac7/image.png">

<hr>
<h2 id="04-배포-자동화">04. 배포 자동화</h2>
<p>하지만 매번 deploy 명령어를 통해 배포하지 않고,</p>
<p>git의 main 브랜치에 push하는 것만으로 배포가 되게끔 하고 싶습니다</p>
<p>다음 경로에 yml 파일을 생성하고,</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/33574ca5-c046-4af0-b204-fbed6059c7cf/image.png" width=30%>

<p>다음과 같이 작성합니다</p>
<pre><code class="language-yml">name: gh-pages Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build
</code></pre>
<p>이제 소스를 수정하고 push를 하면 다음과 같이 새로운 workflow가 동작하고</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/b3617405-8cba-457e-bbf2-11bcf9477de0/image.png">

<p>변경사항이 자동으로 페이지에 반영됩니다</p>
<img src="https://velog.velcdn.com/images/kang-bit/post/a073d48a-42bd-4c16-972b-7291cc5bd43e/image.png">
]]></description>
        </item>
    </channel>
</rss>