<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Bobostown</title>
    <link>https://bobostown.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 01:29:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>월월월월</managingEditor>
    <image>
      <title>Bobostown</title>
      <url>https://tistory1.daumcdn.net/tistory/5696816/attach/69eb584175b8443f830522a0829260ad</url>
      <link>https://bobostown.tistory.com</link>
    </image>
    <item>
      <title>[Inteview-lab] AudioContext를 사용하여 오실리에이터 UI 만들기</title>
      <link>https://bobostown.tistory.com/47</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사용자의 음성이 인식되고 있다는 것을 표현하기 위해 &lt;span style=&quot;color: #ef6f53;&quot;&gt;Oscillator 컴포넌트&lt;/span&gt;를 만들었습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;oscillator.png&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AaCnd/dJMcaaYs1c5/nR094KYIJdnG1ADvk7iIG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AaCnd/dJMcaaYs1c5/nR094KYIJdnG1ADvk7iIG1/img.png&quot; data-alt=&quot;피그마 디자인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AaCnd/dJMcaaYs1c5/nR094KYIJdnG1ADvk7iIG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAaCnd%2FdJMcaaYs1c5%2FnR094KYIJdnG1ADvk7iIG1%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;218&quot; height=&quot;210&quot; data-filename=&quot;oscillator.png&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;210&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;&lt;b&gt;음성이 인식되고 있다는 것을 사용자에게 피드백하기 위해&lt;/b&gt; 위와 같은 컴포넌트를 디자인 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 디자인의 핵심은 &lt;u&gt;사용자의 음성을 주파수의 파형&lt;/u&gt;으로 보여주는 &lt;span style=&quot;color: #ef6f53;&quot;&gt;오실리에이터&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;오실리에이터&lt;/span&gt;를 제가 어떻게 구현했는지 설명드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Media Capture and Streams API를 사용하여 음성데이터 가져오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 음성을 시각화하는 것이므로 가장 먼저 해야하는 것은 음성 데이터를 가져오는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;span style=&quot;color: #ef6f53;&quot;&gt;Media Capture and Streams API&lt;/span&gt;의 &lt;span style=&quot;color: #f89009;&quot;&gt;MediaDevices&lt;/span&gt;의 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;getUserMedia&lt;/a&gt; 메서드를 사용하여 마이크 권한을 요청하고 반환값으로 MediaStream을 받습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1770292147721&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AudioContext를 사용하여 MediaStreamAudioSourceNode 객체 생성하기&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AudioContext란?&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;Web Audio API&lt;/span&gt;는 &lt;b&gt;노드 기반 아키텍처&lt;/b&gt;를 사용합니다. 오디오 소스(Source) &amp;rarr; 처리(Processing) &amp;rarr; 출력(Destination)으로 이어지는 노드들을 연결하여 &lt;u&gt;오디오 파이프라인을 구성&lt;/u&gt;하는데, &lt;span style=&quot;color: #ef6f53;&quot;&gt;AudioContext&lt;/span&gt;가 이 &lt;b&gt;노드들을 생성하고 연결하는 역할&lt;/b&gt;을 합니다.&lt;br /&gt;&lt;br /&gt;[MediaStream] &amp;rarr; [SourceNode] &amp;rarr; [AnalyserNode] &amp;rarr; [Destination] &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;uarr; AudioContext가 생성&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaStream을 재생이나 조작하기 위해서는 &lt;span style=&quot;color: #ef6f53;&quot;&gt;MediaStreamAudioSourceNode&lt;/span&gt; 객체를 생성해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1770296727577&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const source = audioContext.createMediaStreamSource(mediaStream)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주파수 분석을 위해 AnalyserNode 생성하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;AnalyserNode&lt;/span&gt; 인터페이스는 &lt;b&gt;실시간 주파수와 시간 영역 분석 정보를 제공 가능한 노드를 표현&lt;/b&gt;합니다. 이것은 변경되지 않은 오디오 스트림을 입력에서 출력으로 전달하지만, 여러분은 생성된 데이터를 얻고, 그것을 처리하고, 오디오 시각화를 생성할 수 있습니다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770297427149&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const analyser = audioContext.createAnalyser();
analyser.fftSize = 64;

source.connect(analyser);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FFT(Fast Fourier Transform)는 시간 영역의 오디오 신호를 주파수 영역으로 변환하는 알고리즘입니다. &lt;b&gt;fftSize는 한번의 FFT 분석에 사용할 샘플 수를 결정&lt;/b&gt;하며, 값이 클수록 주파수 해상도는 높아지지만 시간 반응성은 낮아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 주파수 데이터를 저장할 배열 생성하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AnalyserNode에서 추출한 주파수 데이터를 저장할 Uint8Array를 생성합니다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770298216729&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dataArray = new Uint8Array(analyser.frequencyBinCount);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;frequencyBinCount&lt;/span&gt;는 fftSize / 2 값으로, 분석 결과로 얻을 수 있는 &lt;b&gt;주파수 구간의 개수&lt;/b&gt;입니다. fftSize가 64이므로 32개의 주파수 데이터를 얻게 됩니다. 각 요소는 0~255 범위의 값을 가지며, 해당 주파수 대역의 볼륨을 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. requestAnimationFrame으로&amp;nbsp;시각화&amp;nbsp;애니메이션&amp;nbsp;구현하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실시간 시각화를 위해 requestAnimationFrame을 사용하여 매 프레임마다 주파수 데이터를 갱신하고 화면에 그립니다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770298303971&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const draw = () =&amp;gt; {
    analyser.getByteFrequencyData(dataArray);
    drawBars(ctx, width, height, dataArray);
    animationIdRef.current = requestAnimationFrame(draw);
  };
  draw();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 프로젝트에서 React를 사용해서 animationId를 ref에 저장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6.&amp;nbsp;drawBars&amp;nbsp;함수로&amp;nbsp;막대&amp;nbsp;그래프&amp;nbsp;그리기&lt;/h2&gt;
&lt;pre id=&quot;code_1770298403640&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const BAR_WIDTH = 4;
  const BAR_GAP = 2;
  const BAR_MIN_HEIGHT = 8;
  const BAR_RADIUS = 2;
  const BAR_COLOR = 'rgba(255, 255, 255, 0.6)';

  const drawBars = (
    ctx: CanvasRenderingContext2D,
    width: number,
    height: number,
    dataArray: Uint8Array,
  ) =&amp;gt; {
    ctx.clearRect(0, 0, width, height);

    const barCount = Math.min(
      Math.floor(width / (BAR_WIDTH + BAR_GAP)),
      dataArray.length,
    );
    const totalWidth = barCount * (BAR_WIDTH + BAR_GAP) - BAR_GAP;
    const startX = (width - totalWidth) / 2;
    const centerY = height / 2;

    ctx.fillStyle = BAR_COLOR;

    const mul = Math.floor(dataArray.length / barCount);

    for (let i = 0; i &amp;lt; barCount; i++) {
      let sum = 0;
      for (
        let dataIndex = i * mul;
        dataIndex &amp;lt; (i + 1) * mul &amp;amp;&amp;amp; dataIndex &amp;lt; dataArray.length;
        dataIndex++
      ) {
        sum += dataArray[dataIndex];
      }
      const avg = sum / mul;

      const barHeight = (avg / 255) * (height * 0.8);
      const clampedHeight = Math.max(barHeight, BAR_MIN_HEIGHT);
      const x = startX + i * (BAR_WIDTH + BAR_GAP);
      const y = centerY - clampedHeight / 2;

      ctx.beginPath();
      ctx.roundRect(x, y, BAR_WIDTH, clampedHeight, BAR_RADIUS);
      ctx.fill();
    }
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=4MB5F7jrZc0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=4MB5F7jrZc0&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=4MB5F7jrZc0&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/fh1Py/dJMb9c9tnoC/wC9XsCsxXQplbbxZemk3JK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/IVyVo/dJMb9kTYpSU/dJKP90vOIusMFINVbcrb6k/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/dhMYTb/dJMb9c9tnoD/7nkfSNtbnAX1XLfUCWXoOk/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;interview-lab 오실리에이터&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/4MB5F7jrZc0&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 사용자가 말하면 주파수 파형을 보여주는 컴포넌트를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 다루어보는 Web API라 우여곡절이 많았지만 잘 작동하네요.☺️&lt;/p&gt;</description>
      <category>사이드 프로젝트/interview-lab</category>
      <category>AudioContext</category>
      <category>Media Capture and Streams API</category>
      <category>mediadevices</category>
      <category>oscillator</category>
      <category>오실리에이터</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/47</guid>
      <comments>https://bobostown.tistory.com/47#entry47comment</comments>
      <pubDate>Thu, 5 Feb 2026 22:49:31 +0900</pubDate>
    </item>
    <item>
      <title>[Claude code] Cluade code 알림 설정하기</title>
      <link>https://bobostown.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Claude&amp;nbsp;Code에게&amp;nbsp;오래&amp;nbsp;걸리는&amp;nbsp;작업을&amp;nbsp;명령한&amp;nbsp;후,&amp;nbsp;작업이&amp;nbsp;완료되기만을&amp;nbsp;기다리며&amp;nbsp;화면만&amp;nbsp;바라본&amp;nbsp;경험이&amp;nbsp;있지&amp;nbsp;않으신가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저&amp;nbsp;역시&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;/init&lt;/b&gt;&amp;nbsp;&lt;/span&gt;같은&amp;nbsp;명령어를&amp;nbsp;입력하고&amp;nbsp;작업이&amp;nbsp;끝날&amp;nbsp;때까지&amp;nbsp;무작정&amp;nbsp;기다린&amp;nbsp;적이&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;이&amp;nbsp;시간이&amp;nbsp;아깝지&amp;nbsp;않나요?&amp;nbsp;Claude가&amp;nbsp;작업을&amp;nbsp;빠르게&amp;nbsp;마친다면&amp;nbsp;문제없겠지만,&amp;nbsp;오랜&amp;nbsp;시간이&amp;nbsp;걸린다면&amp;nbsp;&lt;b&gt;그&amp;nbsp;시간&amp;nbsp;동안&amp;nbsp;다른&amp;nbsp;일을&amp;nbsp;하는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;생산적&lt;/b&gt;일&amp;nbsp;것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Claude code에서는 알림을 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 설정 방법은 2가지 방법으로 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;terminal의 알림 기능 사용하기 (&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;iterm2&lt;/span&gt;만 가능&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;Claude code 훅 사용하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Terminal의 알림 기능 사용하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;terminal의 notification 기능을 사용하는 것은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;iterm2&lt;/b&gt;&lt;/span&gt;만 가능합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Iterm2의 설정에 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.39.29.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmjPYK/dJMcaaDXUfc/ISKWM9Yt1LhRlrf94cLqDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmjPYK/dJMcaaDXUfc/ISKWM9Yt1LhRlrf94cLqDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmjPYK/dJMcaaDXUfc/ISKWM9Yt1LhRlrf94cLqDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmjPYK%2FdJMcaaDXUfc%2FISKWM9Yt1LhRlrf94cLqDK%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;1794&quot; height=&quot;770&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.39.29.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;Profile&lt;/b&gt; 탭에서 &lt;b&gt;Terminal&lt;/b&gt; 탭을 들어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.41.10.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKkXyM/dJMcabQpblE/SVmPAawbODxn1P5XVaIKC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKkXyM/dJMcabQpblE/SVmPAawbODxn1P5XVaIKC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKkXyM/dJMcabQpblE/SVmPAawbODxn1P5XVaIKC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKkXyM%2FdJMcabQpblE%2FSVmPAawbODxn1P5XVaIKC1%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;2168&quot; height=&quot;1488&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.41.10.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스크롤을 아래로 내려서 &lt;b&gt;Silence bell&lt;/b&gt;을 체크합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.45.42.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3khCJ/dJMcaf6lqw9/09DxMJx7exqIKUoXoxLdH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3khCJ/dJMcaf6lqw9/09DxMJx7exqIKUoXoxLdH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3khCJ/dJMcaf6lqw9/09DxMJx7exqIKUoXoxLdH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3khCJ%2FdJMcaf6lqw9%2F09DxMJx7exqIKUoXoxLdH1%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;2168&quot; height=&quot;1488&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.45.42.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;Notification Center Alerts&lt;/b&gt;를 체크하고 Filter Alerts 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.48.52.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ttyk7/dJMcaaYhijD/GsO3YTTnKm9VsXgAbgCckk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ttyk7/dJMcaaYhijD/GsO3YTTnKm9VsXgAbgCckk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ttyk7/dJMcaaYhijD/GsO3YTTnKm9VsXgAbgCckk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fttyk7%2FdJMcaaYhijD%2FGsO3YTTnKm9VsXgAbgCckk%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;2168&quot; height=&quot;1488&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.48.52.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. &lt;b&gt;Send escape sequence-generated alerts&lt;/b&gt;를 체크합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.48.07.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJ50z0/dJMcafrKddO/m21O07bdyBkLoDxhyFJ2Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJ50z0/dJMcafrKddO/m21O07bdyBkLoDxhyFJ2Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJ50z0/dJMcafrKddO/m21O07bdyBkLoDxhyFJ2Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJ50z0%2FdJMcafrKddO%2Fm21O07bdyBkLoDxhyFJ2Z1%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;2168&quot; height=&quot;1488&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.48.07.png&quot; data-origin-width=&quot;2168&quot; data-origin-height=&quot;1488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 완료하면 Claude code가 작업을 완료하면 다음과 같이 알림을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.50.37.png&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQSKzD/dJMcac2OLsR/ai54hI97sfRBhWEOLcbtn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQSKzD/dJMcac2OLsR/ai54hI97sfRBhWEOLcbtn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQSKzD/dJMcac2OLsR/ai54hI97sfRBhWEOLcbtn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQSKzD%2FdJMcac2OLsR%2Fai54hI97sfRBhWEOLcbtn1%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;922&quot; height=&quot;538&quot; data-filename=&quot;스크린샷 2026-01-05 오후 3.50.37.png&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude code 훅 사용하여 알림 받기&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Claude code 훅&lt;span&gt;&amp;nbsp;이란?&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Claude Code 훅은 Claude Code의 라이프사이클의 다양한 지점에서 실행되는 사용자 정의 셸 명령어입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Claude code&lt;/b&gt;&lt;/span&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;/li&gt;
&lt;li&gt;자동 포맷팅&lt;/li&gt;
&lt;li&gt;로깅&lt;/li&gt;
&lt;li&gt;피드백&lt;/li&gt;
&lt;li&gt;사용자 정의 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Claude code&lt;/b&gt;&lt;/span&gt;는 다음의 훅 이벤트를 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PreToolUse: 도구 호출 전에 실행됩니다(차단 가능).&lt;/li&gt;
&lt;li&gt;PostToolUse: 도구 호출 완료 후 실행됩니다.&lt;/li&gt;
&lt;li&gt;UserPromptSubmit: 사용자가 프롬프트를 제출할 때, Claude가 처리하기 전에 실행됩니다.&lt;/li&gt;
&lt;li&gt;Notification: Claude Code가 알림을 보낼 때 실행됩니다.&lt;/li&gt;
&lt;li&gt;Stop: Claude Code가 응답을 마칠 때 실행됩니다.&lt;/li&gt;
&lt;li&gt;SubagentStop: 서브에이전트 작업이 완료될 때 실행됩니다.&lt;/li&gt;
&lt;li&gt;PreCompact: Claude Code가 컴팩트 작업을 실행하려고 할 때 실행됩니다.&lt;/li&gt;
&lt;li&gt;SessionStart: Claude Code가 새 세션을 시작하거나 기존 세션을 재개할 때 실행됩니다.&lt;/li&gt;
&lt;li&gt;SessionEnd: Claude Code 세션이 종료될 때 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Claude code&lt;/b&gt;&lt;/span&gt; 훅은 다음 파일에서 작성할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;~/.claude/settings.json - 사용자 설정&lt;/li&gt;
&lt;li&gt;.claude/settings.json - 프로젝트 설정&lt;/li&gt;
&lt;li&gt;.claude/settings.Local.json - 로컬 프로젝트 설정 (커밋되지 않음)&lt;/li&gt;
&lt;li&gt;Enterprise 관리 정책 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Claude code 훅&lt;span&gt;을 사용하여 알림 받기 (맥 버전)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;저는 &lt;b&gt;전역적&lt;/b&gt;으로 설정을 하고 싶기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;~/.claude/settings.json&lt;/b&gt;&lt;/span&gt;에 작성을 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;setting.json에 다음의 내용을 붙여 넣습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767597998930&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  ...
  &quot;hooks&quot;: {
    &quot;Notification&quot;: [
      {
        &quot;matcher&quot;: &quot;*&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay /System/Library/Sounds/Funk.aiff&quot;
          }
        ]
      }
    ],
    &quot;Stop&quot;: [
      {
        &quot;matcher&quot;: &quot;*&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;afplay /System/Library/Sounds/Submarine.aiff&quot;
          }
        ]
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;/System/Library/Sounds&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디렉토리에는 다양한 소리 파일이 있는데 취향 것 설정하시면 될 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Claude code 훅을 통한 알림 설정은 이 블로그를 참고했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://velog.io/@leochoo/Claude-Code-%EC%95%8C%EB%A6%BC%EC%9D%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%A9%8D%EB%95%8C%EB%A6%AC%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B7%B8%EB%A7%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@leochoo/Claude-Code-%EC%95%8C%EB%A6%BC%EC%9D%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%A9%8D%EB%95%8C%EB%A6%AC%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B7%B8%EB%A7%8C&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767598198358&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  Claude Code 알림음 설정하기: &amp;quot;터미널 멍때리기&amp;quot;는 이제 그만!&quot; data-og-description=&quot;요약: Claude Code가 작업이 끝나거나 입력이 필요할 때, 소리로 알려주는 Hook 설정 꿀팁입니다.Claude Code에게 작업을 시켜두면 짧게는 몇 초, 길게는 몇 분, 혹은 정말 긴 작업은 몇십분이 걸리기도 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@leochoo/Claude-Code-%EC%95%8C%EB%A6%BC%EC%9D%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%A9%8D%EB%95%8C%EB%A6%AC%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B7%B8%EB%A7%8C&quot; data-og-url=&quot;https://velog.io/@leochoo/Claude-Code-알림음-설정하기-터미널-멍때리기는-이제-그만&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bYpbqT/dJMb9lL4iwW/MfTJHxTt3Dhwl1CXh42yd1/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447,https://scrap.kakaocdn.net/dn/eKaGl/dJMb8Z3jT7p/9lezCZg9JV5TYVYSq2FK6k/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447,https://scrap.kakaocdn.net/dn/cbWsoL/dJMb8PGoOk4/GhGG5hQC0aAOZ8SXODSgKK/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447&quot;&gt;&lt;a href=&quot;https://velog.io/@leochoo/Claude-Code-%EC%95%8C%EB%A6%BC%EC%9D%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%A9%8D%EB%95%8C%EB%A6%AC%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B7%B8%EB%A7%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@leochoo/Claude-Code-%EC%95%8C%EB%A6%BC%EC%9D%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%ED%84%B0%EB%AF%B8%EB%84%90-%EB%A9%8D%EB%95%8C%EB%A6%AC%EA%B8%B0%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EA%B7%B8%EB%A7%8C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bYpbqT/dJMb9lL4iwW/MfTJHxTt3Dhwl1CXh42yd1/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447,https://scrap.kakaocdn.net/dn/eKaGl/dJMb8Z3jT7p/9lezCZg9JV5TYVYSq2FK6k/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447,https://scrap.kakaocdn.net/dn/cbWsoL/dJMb8PGoOk4/GhGG5hQC0aAOZ8SXODSgKK/img.png?width=910&amp;amp;height=447&amp;amp;face=0_0_910_447');&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;  Claude Code 알림음 설정하기: &quot;터미널 멍때리기&quot;는 이제 그만!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;요약: Claude Code가 작업이 끝나거나 입력이 필요할 때, 소리로 알려주는 Hook 설정 꿀팁입니다.Claude Code에게 작업을 시켜두면 짧게는 몇 초, 길게는 몇 분, 혹은 정말 긴 작업은 몇십분이 걸리기도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 블로그에 맥뿐만이 아니라 윈도우와 리눅스 설정 방법도 있으니 참고하시면 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>Claude</category>
      <category>claude code</category>
      <category>nofitication</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/46</guid>
      <comments>https://bobostown.tistory.com/46#entry46comment</comments>
      <pubDate>Mon, 5 Jan 2026 16:33:44 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Next.js 어플리케이션에 i18n 구현하기</title>
      <link>https://bobostown.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 일주일 동안 &lt;a href=&quot;https://bobostown.tistory.com/42&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;저번 포스팅&lt;/a&gt;에서 만든 &lt;a href=&quot;https://github.com/Runzipper/ui&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UI 라이브러리&lt;/a&gt;를 가지고 &lt;u&gt;&lt;b&gt;압축 사이트&lt;/b&gt;를 만들었습니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.runzipper.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.runzipper.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765802975105&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Runzipper&quot; data-og-description=&quot;ZIP, TAR, TAR.GZ 형식을 지원하는 브라우저 기반 파일 압축 및 아카이브 도구입니다. 서버 업로드 없이, 설치 불필요하며, 완벽한 개인정보 보호를 제공합니다. 모든 압축 처리는 사용자의 기기에서&quot; data-og-host=&quot;www.runzipper.app&quot; data-og-source-url=&quot;https://www.runzipper.app/&quot; data-og-url=&quot;https://www.runzipper.app/ko/compress&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b2xReV/hyZPwj4Hqr/TayVNOKROSZNeLlNOPX7v0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/LaQnU/hyZPxDhwL7/7KZBbT0IK43FJupKk66GJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.runzipper.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.runzipper.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b2xReV/hyZPwj4Hqr/TayVNOKROSZNeLlNOPX7v0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/LaQnU/hyZPxDhwL7/7KZBbT0IK43FJupKk66GJk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Runzipper&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ZIP, TAR, TAR.GZ 형식을 지원하는 브라우저 기반 파일 압축 및 아카이브 도구입니다. 서버 업로드 없이, 설치 불필요하며, 완벽한 개인정보 보호를 제공합니다. 모든 압축 처리는 사용자의 기기에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.runzipper.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 이 프로젝트를 시작한 계기는 간단한 툴을 웹서비스 형태로 제공하여 &lt;b&gt;방문자수를 모니터링&lt;/b&gt;하고 싶어서였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스를 사용하는 사람이 충분하다면 광고를 추가하여 &lt;b&gt;수익을 창출하는 것도 고려하고 있습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;❓ 다양한 툴 중에 왜 압축 사이트를 만들었나요?&lt;br /&gt;&lt;br /&gt;일반 컴퓨터 사용자는 보통 압축, 압축해제를 하려면 반디집 같은 압축 프로그램을 설치하여 사용합니다.&lt;br /&gt;&lt;br /&gt;압축 프로그램을 브라우저에서 구현하면 &lt;b&gt;설치도 필요 없고, 서버로 정보가 넘어가지도 않으니 프라이버시도 지킬 수 있습니다&lt;/b&gt;.&lt;br /&gt;특히, &lt;u&gt;설치가 필요 없다는 점에서 상용 소프트웨어에 비해 경쟁력&lt;/u&gt;을 가질 수 있지 않을까 하여 브라우저에서 실행되는 압축 프로그램을 만들었습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문자수를 높일 수 있는 방법이 뭐가 있을까 고민했을 때 첫 번째로 떠오르는 것은 &lt;b&gt;전 세계 사람들을 대상으로 서비스&lt;/b&gt;하는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;a href=&quot;https://www.runzipper.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;runzipper&lt;/a&gt;에 &lt;b&gt;국제화를 적용&lt;/b&gt;하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dictionary 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;사실 저는 i18n을 구현하는 게 처음입니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;a href=&quot;https://nextjs.org/docs/app/guides/internationalization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;next.js 공식문서의 internationalization 섹션&lt;/a&gt;을 참고하였고, 많은 부분을 따라서 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 가장 먼저 한 것은 &lt;b&gt;하드 코딩된 텍스트를 json 형태의 dictionary로 정리&lt;/b&gt;하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;en.json&lt;/li&gt;
&lt;li&gt;ko.json&lt;/li&gt;
&lt;li&gt;ja.json&lt;/li&gt;
&lt;li&gt;zh.json&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 dictionary를 만들면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 무장적 json 파일을 작성하면 실수할 가능성이 높겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 안정성을 위해 &lt;a href=&quot;https://json-schema.org/learn/getting-started-step-by-step&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;json schema&lt;/a&gt; 파일을 먼저 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;json schema 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;json schema는 타입스크립트에 비유할 수 있습니다&lt;/b&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;b&gt;json schema&lt;/b&gt;는 &lt;b&gt;json 버전의 인터페이스&lt;/b&gt;라고 이해하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json schema를 작성하는 방법은 &lt;a href=&quot;https://json-schema.org/learn/getting-started-step-by-step&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 &lt;b&gt;dictionary.schema.json&lt;/b&gt; 파일을 만들어서 스키마를 정의했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765805554246&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// dictionary.schema.json
{
	&quot;$schema&quot;: &quot;https://json-schema.org/draft/2020-12/schema&quot;,
	&quot;title&quot;: &quot;Dictionary Schema&quot;,
	&quot;description&quot;: &quot;Schema for application dictionary files&quot;,
	&quot;type&quot;: &quot;object&quot;,
	&quot;properties&quot;: {
		&quot;meta&quot;: {
			//...
		},
		&quot;compress&quot;: {
			//...
		},
		&quot;header&quot;: {
			//...
		},
		&quot;footer&quot;: {
			//...
	},
	&quot;required&quot;: [
		//...
	]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 레이아웃, 메타 데이터, 페이지 단위로 데이터를 정의하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;필수로 넣어야 하는 필드인 경우 required 배열에 key&lt;/u&gt;를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;json dictionary 작성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json schema를 정의했으니 &lt;b&gt;이를 사용하여 dictionary를 작성&lt;/b&gt;하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딕셔너리를 작성할 때 먼저 $schema 키에 값으로 &lt;b&gt;앞서 작성한 json schema의 url&lt;/b&gt;을 넣어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765806051546&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ko.json
{
	&quot;$schema&quot;: &quot;./dictionary.schema.json&quot;,
	//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 에디터에서 자동완성이 지원되고, 필수 값이 누락됐을 경우 경고 표시를 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dictionary 유틸 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dictionary를 정의했으니 이를 사용하기 위한 유틸 함수들을 만들겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드 또한 &lt;a href=&quot;https://nextjs.org/docs/app/guides/internationalization#localization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;next.js 공식문서&lt;/a&gt;에서 가져왔습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765806707559&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// utils/dictionary.ts

// 앞서 정의한 dictionary를 로딩하는 함수
export const dictionaries = {
	ko: () =&amp;gt;
		import('@/docs/dictionanaries/kr.json').then((module) =&amp;gt; module.default),
	en: () =&amp;gt;
		import('@/docs/dictionanaries/en.json').then((module) =&amp;gt; module.default),
	ja: () =&amp;gt;
		import('@/docs/dictionanaries/ja.json').then((module) =&amp;gt; module.default),
	zh: () =&amp;gt;
		import('@/docs/dictionanaries/zh.json').then((module) =&amp;gt; module.default),
	de: () =&amp;gt;
		import('@/docs/dictionanaries/de.json').then((module) =&amp;gt; module.default),
	pl: () =&amp;gt;
		import('@/docs/dictionanaries/pl.json').then((module) =&amp;gt; module.default),
	es: () =&amp;gt;
		import('@/docs/dictionanaries/es.json').then((module) =&amp;gt; module.default),
	fr: () =&amp;gt;
		import('@/docs/dictionanaries/fr.json').then((module) =&amp;gt; module.default),
	it: () =&amp;gt;
		import('@/docs/dictionanaries/it.json').then((module) =&amp;gt; module.default),
} as const;

export type Locale = keyof typeof dictionaries;

// string 타입을 좁히는 함수
export const hasLocale = (locale: string): locale is Locale =&amp;gt; {
	return Object.keys(dictionaries).includes(locale);
};

// locale에 따른 dictionary를 get하는 함수
export const getDictionary = async (locale: Locale) =&amp;gt; dictionaries[locale]();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Route 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 언어를 지원하려면 언어별로 route가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 /compress이라는 경로가 있고&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;p data-ke-size=&quot;size16&quot;&gt;를 지원한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/en/compression&lt;/li&gt;
&lt;li&gt;/ja/compression&lt;/li&gt;
&lt;li&gt;/ko/compression&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 route가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;폴더 구조를 /compress에서 /[slug]/compress 형태로 변경&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Proxy.ts에서 언어 감지하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 &lt;b&gt;&lt;u&gt;지원하지 않는 언어로 접근하거나 url에 언어(slug)가 포함이 안 돼있을 시를&lt;/u&gt; 처리하기 위해 next.js의 proxy를 사용&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 /[lang]/compress가 아닌 &lt;b&gt;/compress로 접속했을 때는&lt;/b&gt; &lt;b&gt;요청 헤더의 accept-language를 파싱 하여&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 언어를 지원하면 /[해당 언어]/compress로 리다이렉트 시키고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지원하지 않는다면 /en/compress로 리다이렉트 시키도록 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765807606090&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// proxy.ts

import { dictionaries } from '@/utils/dictionary';
import { type NextRequest, NextResponse } from 'next/server';

// 지원하지 않는 언어로 접속시 기본 언어
const DEFAULT_LANGUAGE = 'en';

// 지원하는 언어 목록
const localList = Object.keys(dictionaries);

export function proxy(request: NextRequest) {
	const { pathname } = request.nextUrl;
	const pathnameHasLocale = localList.some(
		(locale) =&amp;gt; pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
	);

	// 사용자의 요청이 /lang/~ 일 때
	if (pathnameHasLocale) return;

	// 사용자의 요청이 /lang/~ 형식이 아닐 때
	const acceptLanguage = request.headers.get('accept-language') || '';
	const primaryLanguage = acceptLanguage
		.split(',')[0]
		.split(';')[0]
		.trim()
		.split('-')[0];

	const locale =
		localList.find((locale) =&amp;gt; locale === primaryLanguage) || DEFAULT_LANGUAGE;

	request.nextUrl.pathname = `/${locale}${pathname}`;

	return NextResponse.redirect(request.nextUrl);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;언어별 route에 static site generation 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dictionary에 저희가 페이지에서 사용할 텍스트가 모두 정의돼 있으니 동적 렌더링을 할 이유가 전혀 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSG를 적용하기 위해 [lang] 폴더의 layout.tsx에 generateStaticparams를 선언하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765808513336&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/app/[lang]/layout.tsx

export async function generateStaticParams() {
	return Object.keys(dictionaries).map((lang) =&amp;gt; ({ lang }));
}

export default async function Layout({
	children,
	params,
}: LayoutProps&amp;lt;'/[lang]'&amp;gt;) {
	const locale = (await params).lang;

	if (!hasLocale(locale)) notFound();

	return (
		&amp;lt;html lang={(await params).lang}&amp;gt;
			&amp;lt;body className={bodyStyle}&amp;gt;
				&amp;lt;DictionaryProvider dictionary={await dictionaries[locale]()}&amp;gt;
					&amp;lt;Header lang={locale} /&amp;gt;
					{children}
					&amp;lt;Footer lang={locale} /&amp;gt;
				&amp;lt;/DictionaryProvider&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;다음과 같이 지원하는 언어들에 대한 &lt;b&gt;정적페이지를 생성&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가적으로&lt;/b&gt; &lt;b&gt;layout 컴포넌트에서 알아낸 딕셔너리 정보를 자식 컴포넌트에게 전달해야 하는데 저 같은 경우 Provider를 사용하여 전달했습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언어에 따른 메타데이터 제공하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 눈에 보이는 정보뿐만이 아니라 메타데이터도 국제화를 적용하고 싶어서 dictionary를 정의할 때 메타데이터도 각 국가의 언어로 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;generateMetadata 함수를 선언하여 언어에 따른 메타데이터를 생성&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765809014196&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/app/[lang]/layout.tsx

type MetadataProps = {
	params: Promise&amp;lt;{ lang: string }&amp;gt;;
};

export async function generateMetadata({
	params,
}: MetadataProps): Promise&amp;lt;Metadata&amp;gt; {
	const locale = (await params).lang;

	if (!hasLocale(locale)) {
		return {};
	}

	const dictionary = await dictionaries[locale]();

	return {
		title: 'Runzipper',
		description: dictionary.meta.description,
		manifest: '/site.webmanifest',
		appleWebApp: {
			title: 'Runzipper',
		},
		icons: {
			icon: [
				{ url: '/favicon.ico' },
				{ url: '/favicon.svg', type: 'image/svg+xml' },
				{ url: '/favicon-96x96.png', sizes: '96x96', type: 'image/png' },
			],
			apple: { url: '/apple-touch-icon.png', sizes: '180x180' },
			shortcut: '/favicon.ico',
		},
	};
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SEO를 위해 언어별 sitemap 정의하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 목표는 방문자수 확보이므로 &lt;b&gt;SEO&lt;/b&gt;는 필수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사이트가 다양한 언어를 지원한다는 것을 검색엔진에게 알리기 위해서는 &lt;u&gt;&lt;b&gt;hreflang 태그&lt;/b&gt;를&amp;nbsp;html head에 설정&lt;/u&gt;해야하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next.js에서는 &lt;b&gt;sitemap 함수를 사용하여 설정&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765809235486&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// sitemap.ts

import type { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
	return [
		{
			url: 'https://runzipper.app/compress',
			lastModified: new Date(),
			changeFrequency: 'monthly',
			priority: 1,
			alternates: {
				languages: {
					ko: 'https://runzipper.app/ko/compress',
					de: 'https://runzipper.app/de/compress',
					pl: 'https://runzipper.app/pl/compress',
					fr: 'https://runzipper.app/fr/compress',
					it: 'https://runzipper.app/it/compress',
					ja: 'https://runzipper.app/ja/compress',
					zh: 'https://runzipper.app/zh/compress',
					es: 'https://runzipper.app/es/compress',
					en: 'https://runzipper.app/en/compress',
				},
			},
		},
	];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 할 경우 빌드 결과물로 다음과 같은 sitemap.xml이 생성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765809799796&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot; xmlns:xhtml=&quot;http://www.w3.org/1999/xhtml&quot;&amp;gt;
  &amp;lt;url&amp;gt;
    &amp;lt;loc&amp;gt;https://acme.com&amp;lt;/loc&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;es&quot;
      href=&quot;https://acme.com/es&quot;/&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;de&quot;
      href=&quot;https://acme.com/de&quot;/&amp;gt;
    &amp;lt;lastmod&amp;gt;2023-04-06T15:02:24.021Z&amp;lt;/lastmod&amp;gt;
  &amp;lt;/url&amp;gt;
  &amp;lt;url&amp;gt;
    &amp;lt;loc&amp;gt;https://acme.com/about&amp;lt;/loc&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;es&quot;
      href=&quot;https://acme.com/es/about&quot;/&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;de&quot;
      href=&quot;https://acme.com/de/about&quot;/&amp;gt;
    &amp;lt;lastmod&amp;gt;2023-04-06T15:02:24.021Z&amp;lt;/lastmod&amp;gt;
  &amp;lt;/url&amp;gt;
  &amp;lt;url&amp;gt;
    &amp;lt;loc&amp;gt;https://acme.com/blog&amp;lt;/loc&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;es&quot;
      href=&quot;https://acme.com/es/blog&quot;/&amp;gt;
    &amp;lt;xhtml:link
      rel=&quot;alternate&quot;
      hreflang=&quot;de&quot;
      href=&quot;https://acme.com/de/blog&quot;/&amp;gt;
    &amp;lt;lastmod&amp;gt;2023-04-06T15:02:24.021Z&amp;lt;/lastmod&amp;gt;
  &amp;lt;/url&amp;gt;
&amp;lt;/urlset&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;i18n 적용 후기&lt;/h3&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/459957248&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/dLcrYp/hyZPyhU52d/OjkvUw11pMeP6TNCRT86g0/img.jpg?width=1920&amp;amp;height=1028&amp;amp;face=0_0_1920_1028,https://scrap.kakaocdn.net/dn/eAoWS/hyZPw5tMUu/kgcErITaYuizkhUCgxNkK0/img.jpg?width=1920&amp;amp;height=1028&amp;amp;face=0_0_1920_1028&quot; data-video-width=&quot;860&quot; data-video-height=&quot;460&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;460&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/459957248?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;460&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 다국어 지원을 구현했는데 next.js 공식문서에 설명이 잘 되어있어서 어려움 없이 구현할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 만든 컴포넌트 라이브러리가 국제화를 고려하지 않고 만들어서, 텍스트가 하드코딩 되있어서 이를 수정하는데 시간이 걸렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음부터 컴포넌트 라이브러리를 만들때는 사용자가 국제화를 할 수 있다는 것도 고려해야할 것 같습니다.  &lt;/p&gt;</description>
      <category>개발</category>
      <category>I18N</category>
      <category>json-schema</category>
      <category>Next.js</category>
      <category>runzipper</category>
      <category>국제화</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/45</guid>
      <comments>https://bobostown.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 15 Dec 2025 22:12:30 +0900</pubDate>
    </item>
    <item>
      <title>[TroubleShooting] Vite Library mode로 빌드시 타입 파일이 포함 안되는 문제</title>
      <link>https://bobostown.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/42&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.12.02 - [개발] - [vite] vite로 라이브러리 빌드하기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765208450417&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[vite] vite로 라이브러리 빌드하기&quot; data-og-description=&quot;최근 제가 사이드 프로젝트로 진행하는 프로젝트에서 사용할 목적으로 UI 컴포넌트 라이브러리를 만들었습니다. UI 컴포넌트를 만들 때, 프로젝트를 vite의 react 템플릿을 사용하여 생성하였습니&quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/42&quot; data-og-url=&quot;https://bobostown.tistory.com/42&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bozxAH/hyZO7j8kvH/zC5kunKuEp6qomvdXP1nr1/img.jpg?width=410&amp;amp;height=404&amp;amp;face=0_0_410_404,https://scrap.kakaocdn.net/dn/Egb0f/hyZPlJtqDR/qqXpCMGMarNStON94QzBkk/img.jpg?width=410&amp;amp;height=404&amp;amp;face=0_0_410_404&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/42&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/42&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bozxAH/hyZO7j8kvH/zC5kunKuEp6qomvdXP1nr1/img.jpg?width=410&amp;amp;height=404&amp;amp;face=0_0_410_404,https://scrap.kakaocdn.net/dn/Egb0f/hyZPlJtqDR/qqXpCMGMarNStON94QzBkk/img.jpg?width=410&amp;amp;height=404&amp;amp;face=0_0_410_404');&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;[vite] vite로 라이브러리 빌드하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최근 제가 사이드 프로젝트로 진행하는 프로젝트에서 사용할 목적으로 UI 컴포넌트 라이브러리를 만들었습니다. UI 컴포넌트를 만들 때, 프로젝트를 vite의 react 템플릿을 사용하여 생성하였습니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 &lt;u&gt;vite로 라이브러리를 빌드한 경험&lt;/u&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서도 가장 심각했던 버그는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;제네릭 컴포넌트에서 타입 추론이 제대로 동작하지 않는 것&lt;/b&gt;이었습니다.&lt;span style=&quot;color: #cccccc; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;문제상황&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를&amp;nbsp;들어,&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;컴포넌트가&amp;nbsp;있습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1765208597250&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Typography.Regular textType=&quot;span&quot; className={textStyle}&amp;gt;
    이용약관
&amp;lt;/Typography.Regular&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컴포넌트는 'p', 'span', 'label' 등 다양한 &lt;b&gt;HTML 텍스트 엘리먼트를 textType으로 받을 수 있는 제네릭 컴포넌트&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전달된 textType에 따라 해당 &lt;u&gt;HTML 엘리먼트에 특화된 속성들을 props로 전달&lt;/u&gt;할 수 있도록 설계&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;textType=&quot;label&quot;일 때는 htmlFor 속성 사용 가능&lt;/li&gt;
&lt;li&gt;textType=&quot;span&quot;일&amp;nbsp;때는&amp;nbsp;span의&amp;nbsp;고유&amp;nbsp;속성들&amp;nbsp;사용&amp;nbsp;가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;빌드된&amp;nbsp;라이브러리에서는&amp;nbsp;이러한&amp;nbsp;타입&amp;nbsp;추론이&amp;nbsp;정상적으로&amp;nbsp;작동하지&amp;nbsp;않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;그래서 빌드된 코드를 보니 &lt;b&gt;textType&lt;/b&gt;을 정의한 &lt;b&gt;Text &lt;/b&gt;타입이 빌드 결과물에 포함되지 않고 있었습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;b&gt;TextType&lt;/b&gt;을 &lt;b&gt;d.ts&lt;/b&gt; 파일에 정의한 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text.d.ts에 다음과 같이 텍스트 element들을 정리해 놨습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765209258589&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export type Text =
	| 'h1'
	| 'h2'
	| 'h3'
	| 'h4'
	| 'h5'
	| 'h6'
	| 'p'
	...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일의 확장자를 &lt;u&gt;&lt;b&gt;.d.ts&lt;/b&gt;에서 &lt;b&gt;.ts&lt;/b&gt;로 변경하니 정상적으로 빌드 결과물에 해당 타입이 포함&lt;/u&gt;되었습니다.&lt;/p&gt;</description>
      <category>TroubleShooting</category>
      <category>VITE</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/44</guid>
      <comments>https://bobostown.tistory.com/44#entry44comment</comments>
      <pubDate>Tue, 9 Dec 2025 00:57:34 +0900</pubDate>
    </item>
    <item>
      <title>2025 DevFest Incheon 2025 후기 (feat. baseline)</title>
      <link>https://bobostown.tistory.com/43</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiTcgg/dJMcadmVCDS/zwg4rEkVS3LXhjhDTGjFbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiTcgg/dJMcadmVCDS/zwg4rEkVS3LXhjhDTGjFbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiTcgg/dJMcadmVCDS/zwg4rEkVS3LXhjhDTGjFbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiTcgg%2FdJMcadmVCDS%2Fzwg4rEkVS3LXhjhDTGjFbk%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;300&quot; height=&quot;399&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 12월 6일에 인천 송도에서 열린 &lt;b&gt;DevFest&lt;/b&gt;에 다녀왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 DevFest에서는 웹 프론트엔드, 백엔드, AI, 플러터 등 다양한 주제의 세션이 열렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 오늘 열리는 세션중에 &lt;b&gt;&quot;&amp;lt;form/&amp;gt;의 추억&quot;&lt;/b&gt;, &lt;b&gt;&quot;모던 자바스크립트 패키지 개발하기&quot;&lt;/b&gt;, &lt;b&gt;&quot;이력서 첨삭 세션&quot;&lt;/b&gt;을 청강하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션에 들어가기에 앞서 &lt;b&gt;네트워킹 존에서 열리는 다양한 이벤트에 참여&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EUaVx/dJMcacuQCbg/KRFUJuqFwyGInwh3qvHtRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EUaVx/dJMcacuQCbg/KRFUJuqFwyGInwh3qvHtRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EUaVx/dJMcacuQCbg/KRFUJuqFwyGInwh3qvHtRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEUaVx%2FdJMcacuQCbg%2FKRFUJuqFwyGInwh3qvHtRk%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;600&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워킹 존에는 모두의 연구소, Upstage, Mondiran AI 등 기업들의 부스가 있어서 간단한 설명을 들으면 소정의 경품을 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부스행사 참여후에 첫 번째 세션인 &lt;b&gt;&amp;lt;form/&amp;gt;의 추억: HTML부터 RHF, Server Actions&lt;/b&gt;까지를 청강했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;form/&amp;gt;의 추억: HTML부터 RHF, Server Actions&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd2aBI/dJMcadHfaVK/DW0KNDdSk2F9TU4gU0AXQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd2aBI/dJMcadHfaVK/DW0KNDdSk2F9TU4gU0AXQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd2aBI/dJMcadHfaVK/DW0KNDdSk2F9TU4gU0AXQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd2aBI%2FdJMcadHfaVK%2FDW0KNDdSk2F9TU4gU0AXQk%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;600&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 세션에서는 다른 HTML Tag와 구분되는 Form Tag의 특징, Ajax의 등장으로 대체된 form의 기본 동작부터 Server action에 대한 내용을 다루었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님이 프론트엔드에 대한 내용을 몰라도 쉽게 이해할 수 있도록 설명해 주신 것이 인상적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모던 자바스크립트 패키지 개발하기: ESM, Typescript, 의존성, 그리고 DX&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CwCHl/dJMcacuQBFc/g7HdY9CtHDFtjYiTdLphF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CwCHl/dJMcacuQBFc/g7HdY9CtHDFtjYiTdLphF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CwCHl/dJMcacuQBFc/g7HdY9CtHDFtjYiTdLphF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCwCHl%2FdJMcacuQBFc%2Fg7HdY9CtHDFtjYiTdLphF1%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;600&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세션은 eslint와 react community에 기여하고 있는 개발자분께서 진행하셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지를 개발할때 가장 중요한 모듈 시스템에 대한 설명부터,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수를 위한 Typescript와 Package.json의 dev dependency와 dependecy에 대한 설명들을 해주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세션에서 흥미로웠던 것은 &lt;b&gt;구글(크롬 팀)의 &lt;a href=&quot;https://web.dev/baseline&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;webbase line&lt;/a&gt; 프로젝트&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 기술은 특정 버전의 브라우저나 node.js에서 지원하지 않아서 사용하지 못하는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 최신 기술을 사용할 때 &lt;a href=&quot;https://caniuse.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;caniuse&lt;/a&gt; 라는 사이트에 기술을 검색하여, 이 기술이 어떤 브라우저에서 지원하는지 확인하곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://web.dev/baseline&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;webbase line&lt;/b&gt;&lt;/a&gt;&lt;b&gt; 프로젝트에서는 &lt;a href=&quot;https://caniuse.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;caniuse&lt;/a&gt;에서 제공하는 호환성 리스트를 보여주는 것에서 더 발전된 것 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;webbase line 프로젝트에서는 호환성을 2가지 단계로 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Limited availability&lt;/b&gt; : &lt;b&gt;주요 브라우저 전반&lt;/b&gt;에서 구현되지 않았습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Newly available&lt;/b&gt; : &amp;nbsp;이&amp;nbsp;기능은&amp;nbsp;&lt;b&gt;모든&amp;nbsp;주요&amp;nbsp;브라우저에서&amp;nbsp;지원&lt;/b&gt;되므로&amp;nbsp;상호&amp;nbsp;운용이&amp;nbsp;가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Widely available : &lt;/b&gt;새로&amp;nbsp;상호운용&amp;nbsp;가능한&amp;nbsp;날짜로부터&amp;nbsp;30개월이&amp;nbsp;지났습니다.&amp;nbsp;&lt;b&gt;대부분의&amp;nbsp;사이트에서&amp;nbsp;지원&amp;nbsp;문제를&amp;nbsp;걱정하지&amp;nbsp;않고&amp;nbsp;이&amp;nbsp;기능을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;최신 기술을 사용하고 싶으면 baseline에서 Widely available인 기술을 사용하면 호환성을 충족&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;lt;dialog/&amp;gt; 태그는 &lt;b&gt;Widely available&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;87&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kSStV/dJMcacBCmLh/YOh8NjfeSmBkhjYXaOwiK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kSStV/dJMcacBCmLh/YOh8NjfeSmBkhjYXaOwiK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kSStV/dJMcacBCmLh/YOh8NjfeSmBkhjYXaOwiK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkSStV%2FdJMcacBCmLh%2FYOh8NjfeSmBkhjYXaOwiK0%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;600&quot; height=&quot;87&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;87&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;❗️ baseline 프로젝트에서 &lt;b&gt;주요 브라우저&lt;/b&gt;라고 정의하는 것은 다음과 같습니다.&lt;br /&gt;- Chrome&lt;br /&gt;- Edge&lt;br /&gt;- Safari&lt;br /&gt;- Firefox&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;b&gt;당첨된 지원자의 이력서를 공개 첨삭&lt;/b&gt;했습니다. (개인정보는 마스킹 처리했습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨삭은 파트별로 현직자분께서 나와서 진행했고, 지원자의 이력서를 첨삭한 뒤에는 &lt;b&gt;현직자분의 이력서를 보여주셨습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bls6v9/dJMcahCUHZG/9C2phjByuMtnkJNIaXsQf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bls6v9/dJMcahCUHZG/9C2phjByuMtnkJNIaXsQf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bls6v9/dJMcahCUHZG/9C2phjByuMtnkJNIaXsQf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbls6v9%2FdJMcahCUHZG%2F9C2phjByuMtnkJNIaXsQf0%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;600&quot; height=&quot;450&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;b&gt;현직자분의 이력서와 제 이력서를 비교하니 가독성 부분에서 떨어지는 것을 느낄 수 있었습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DevFest 후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 네이버 Dan을 시작으로, 이런 개발자 행사에 참여할 때마다 &lt;b&gt;무언가 한 가지씩 배워가는 것 같습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 행사에서는 &lt;u&gt;webbase line이라는&lt;/u&gt; 프로젝트를 배울 수 있었습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 프로젝트나 취업준비를 하면 사용하던 기술만 사용하거나 자기 생각에 갇히는 등 발전이 힘든데 이런 행사에서 한 번씩 트렌드를 읽을 수 있으니 좋은 것 같습니다.&lt;/p&gt;</description>
      <category>일상</category>
      <category>devfest 2025</category>
      <category>GDG</category>
      <category>songdo</category>
      <category>webbase line</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/43</guid>
      <comments>https://bobostown.tistory.com/43#entry43comment</comments>
      <pubDate>Sun, 7 Dec 2025 23:26:10 +0900</pubDate>
    </item>
    <item>
      <title>[vite] vite로 라이브러리 빌드하기</title>
      <link>https://bobostown.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 제가 사이드 프로젝트로 진행하는 프로젝트에서 사용할 목적으로 UI 컴포넌트 라이브러리를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 컴포넌트를 만들 때, 프로젝트를 vite의 react 템플릿을 사용하여 생성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vite로 라이브러리를 빌드할 때는 react web app을 빌드할 때와 달리 추가적인 설정이 필요합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 제가 vite로 라이브러리를 build 하며 겪었던 과정을 말씀드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이브러리 entry point 만들어주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite의 react 템플릿을 사용하여 프로젝트를 생성했을 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;vite는 index.html을 entry point로 하여 프로젝트를 빌드&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 저희는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;UI 라이브러리를 만드는 것이기 때문에 index.html이 존재하지 않습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 저는 &lt;b&gt;src 디렉토리 하위에 main.ts로 entry point&lt;/b&gt;를 만들어줬습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764643654659&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/main.ts
export { default as Icon } from './components/base/icon';
export { default as Typography } from './components/base/typography';
export { default as UI } from './components/ui';
export { vars as Theme } from './styles/theme.css';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이브러리 빌드를 위해 vite.config.js 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vite.config.js를 수정해서 entry point를 비롯한 설정을 추가해 줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764643769348&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/// &amp;lt;reference types=&quot;vitest/config&quot; /&amp;gt;
// import 구문들...
export default defineConfig({
	// ...
	build: {
		lib: {
			name:'ui',
			entry: ['src/main.ts'],
			cssFileName: 'ui-style',
			formats: ['es','umd'],
		},
		rollupOptions: {
			external: ['react', 'react-dom'],
			output: {
				globals: {
					react: 'React',
					'react-dom': 'ReactDOM',
				},
			},
		},
	},
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 옵션의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lib.name : 노출된 전역변수로 formats이 'umd' 혹은 'iife' 일 때 필요&lt;/li&gt;
&lt;li&gt;l&lt;b&gt;ib.entry&lt;/b&gt; : &lt;b&gt;entry point 지정, 여러 entry point 지정도 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;lib.formats : 빌드 결과물의 형식, 'es', 'umd', 'cjs', 'iife', 'system' 중에 선택&lt;/li&gt;
&lt;li&gt;lib.cssFileName : css 파일 출력 이름 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollup 옵션의 의미는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rollupOptions.external : 라이브러리에 포함하지 않을 디펜던시 명시&lt;/li&gt;
&lt;li&gt;rollupOptions.output : 라이브러리 외부에 존재하는 디펜던시를 위해 UMD 번들링 시 전역 변수 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저는 react를 빌드 패키지에 포함하지 않을 것이므로 'react'와 'react-dom'을 rollupOptions.external에 추가했습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vite-config-dts 설치 (optional. typescript 프로젝트만 해당)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript 프로젝트에서 위에까지 설정하고 빌드를 하면 타입은 다 날아가고 자바스크립트 파일만 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;d.ts 파일을 생성하기 위해 &lt;a href=&quot;https://github.com/qmhc/unplugin-dts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;vite-config-dts&lt;/a&gt; 플러그인을 설치해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764645590214&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i vite-config-dts -D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite.config.js에 plugin에 vite-config-dts를 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764645682803&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default defineConfig({
	plugins: [
		react(),
		dts({
			tsconfigPath: './tsconfig.json',
			outDir: 'dist',
			rollupTypes: false,
		}),
        // ...
	],
    // ... 다른 옵션들
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsconfigPath는 공식문서에서 보면, 공식 vite template인 프로젝트일 때 tsconfig.json의 경로를 작성하라고 명시되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollupTypes는 여러 d.ts 파일을 하나의 d.ts 파일로 만드는 옵션인데, 이 옵션을 켰을 때 저 같은 경우 타입이 이상해져서 껐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Package.json에 라이브러리 entry point 명시하기&lt;/h3&gt;
&lt;pre id=&quot;code_1764646111359&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# package.json
{
	&quot;name&quot;: &quot;@runzipper/ui&quot;,
	&quot;version&quot;: &quot;0.0.15&quot;,
	&quot;author&quot;: {
		&quot;name&quot;: &quot;BHyeonKim&quot;,
		&quot;email&quot;: &quot;rlaqhguse@gmail.com&quot;,
		&quot;url&quot;: &quot;https://github.com/BHyeonKim&quot;
	},
	&quot;description&quot;: &quot;UIkit for zipper app&quot;,
	&quot;repository&quot;: {
		&quot;type&quot;: &quot;git&quot;,
		&quot;url&quot;: &quot;https://github.com/Runzipper/ui&quot;
	},
	&quot;packageManager&quot;: &quot;yarn@4.10.3&quot;,
	&quot;files&quot;: [
		&quot;dist&quot;
	],
	&quot;exports&quot;: {
		&quot;.&quot;: {
			&quot;types&quot;: &quot;./dist/src/main.d.ts&quot;,
			&quot;import&quot;: &quot;./dist/main.mjs&quot;,
			&quot;require&quot;: &quot;./dist/main.umd.js&quot;
		},
		&quot;./styles&quot;: &quot;./dist/ui-style.css&quot;
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json의 exports 필드에 entry point를 명시합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;❗️ types 필드는 항상 첫 번째에 있어야 합니다.&lt;br /&gt;&lt;a href=&quot;https://nodejs.org/api/packages.html#community-conditions-definitions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;types&quot;&amp;nbsp;- can be used by typing systems to resolve the typing file for the given export.&amp;nbsp;This condition should always be included first.&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;types 필드를 통해 생성된 타입의 위치를 지정하고, &lt;a href=&quot;https://nodejs.org/api/packages.html#conditional-exports&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;conditional exports&lt;/a&gt;를 통해 &lt;b&gt;import 키워드와 require 키워드 모두 지원하도록 설정하였습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 files 필드를 설정하여 npm package에는 소스코드를 제외한 빌드 결과물만 올라가도록 설정하였습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>Library</category>
      <category>package.json</category>
      <category>VITE</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/42</guid>
      <comments>https://bobostown.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 2 Dec 2025 14:39:57 +0900</pubDate>
    </item>
    <item>
      <title>[npm] Github packages로 private package 배포하기</title>
      <link>https://bobostown.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 개인적으로 사용할 UI 라이브러리를 개발해서 배포할 상황이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 라이브러리는 제가 특정 프로젝트에 사용할 용도로 만들었기 때문에 npm registry에 public으로 배포하는 것은 적절하지 않았습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 npm registry에 private으로 배포하자니, &lt;u&gt;유료 플랜(7$)&lt;/u&gt;를 사용해야 해서 이 또한 선택지에서 제외했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 github packages를 사용하여 private package로 배포하기로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github packages 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Packages는 컨테이너 및 기타 종속성을 포함한 &lt;b&gt;패키지를 호스팅 하고 관리하는 플랫폼입니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Packages는 소스 코드와 패키지를 한 곳에 통합하여 권한 관리 및 결제를 제공하므로 GitHub에서 소프트웨어 개발을 중앙 집중화할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Github packages는 public packages에 대하여 무료이고, &lt;b&gt;private packages는 github 계정의 plan에 따라 일정량의 무료 storage와 데이터 전송을 제공합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저같은 경우 Github 무료 사용자여서 500MB의 storage와 1GB의 월당 데이터 전송을 사용할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;a href=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-packages#free-use-of-github-packages&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.github.com/en/billing/concepts/product-billing/github-packages#free-use-of-github-packages&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764587244114&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitHub Packages billing - GitHub Docs&quot; data-og-description=&quot;Learn how usage of GitHub Packages is measured against your free allowance and how to pay for additional use.&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-packages#free-use-of-github-packages&quot; data-og-url=&quot;https://docs-internal.github.com/en/billing/concepts/product-billing/github-packages&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/efUNwg/hyZOR9dcmQ/h2Zs4GZLkeAKwcwKyS4pt0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/W3m7s/hyZO2JElAg/u8y27Z8I7TkN6rmZBtY7q1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-packages#free-use-of-github-packages&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-packages#free-use-of-github-packages&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/efUNwg/hyZOR9dcmQ/h2Zs4GZLkeAKwcwKyS4pt0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/W3m7s/hyZO2JElAg/u8y27Z8I7TkN6rmZBtY7q1/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&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;GitHub Packages billing - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how usage of GitHub Packages is measured against your free allowance and how to pay for additional use.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;github packages는 무료지만, github packages의 registry는 npm registry를 사용할 때보다 패키지를 배포하는 쪽과 사용하는 쪽 모두 추가적인 설정이 필요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github packages에 npm package 배포하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github packages에 배포하는 방법은 사용자의 터미널에서 &lt;b&gt;npm publish로 수동배포하는 방법과 github workflos을 통한 자동 배포&lt;/b&gt;가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 자동 배포를 하실 테니 자동 배포 방식으로 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Github registry를 가리키도록 하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;먼저 배포할 패키지가 github registry를 가르키도록 해야 합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 2가지 방법이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;package.json의 publishConfig 필드에 github registry 명시&lt;/li&gt;
&lt;li&gt;.npmrc에 github registry 명시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 파일을 하나 더 추가하는 게 싫어서 &lt;b&gt;1번 방법으로 진행&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764588045561&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# package.json
{
    ...
    &quot;publishConfig&quot;: {
        &quot;@runzipper:registry&quot;: &quot;https://npm.pkg.github.com&quot;
    },
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 scope를 사용하지 않을 것이면&lt;/p&gt;
&lt;pre id=&quot;code_1764588170739&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;registry&quot;: &quot;https://npm.pkg.github.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 작성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 같은 경우 runzipper라는 organization에 포함된 프로젝트여서 스코프를 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Github workflow 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 자동화하기 위해 github workflow 스크립트를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트의 registry-url에도 github registry의 url을 명시해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764588352911&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .github/workflows/deploy.yml

name: Publish to github package registry

on:
  push:
    tags:
      - v*

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22.20'
          cache: 'yarn'
          registry-url: 'https://npm.pkg.github.com'

      - name: Install dependencies
        run: yarn install --immutable

      - name: Build
        run: yarn build

      - name: Publish to GitHub Packages
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;secrets.GITHUB_TOKEN은 worflow 실행 시 자동으로 생성&lt;/b&gt;되는 토큰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 따로 github에서 토큰을 발급받지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;a href=&quot;https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764588684189&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Publishing and installing a package with GitHub Actions - GitHub Docs&quot; data-og-description=&quot;GitHub Actions help you automate your software development workflows in the same place you store code and collaborate on pull requests and issues. You can write individual tasks, called actions, and combine them to create a custom workflow. With GitHub Act&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token&quot; data-og-url=&quot;https://docs-internal.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/drr5bn/hyZOo1RcOl/wRxEq930F5yfFLSdNiYVtK/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/gOZn7/hyZOzCmI7P/ubO91l3WXVjHrfYqpOBT21/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/drr5bn/hyZOo1RcOl/wRxEq930F5yfFLSdNiYVtK/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/gOZn7/hyZOzCmI7P/ubO91l3WXVjHrfYqpOBT21/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&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;Publishing and installing a package with GitHub Actions - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Actions help you automate your software development workflows in the same place you store code and collaborate on pull requests and issues. You can write individual tasks, called actions, and combine them to create a custom workflow. With GitHub Act&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;workflow 트리거하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 작성한 workflow를 트리거하면 github의 클라우드에서 패키지가 빌드 후 배포됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 tag가 push 되면 트리거 되는 것을 조건으로 설정했으므로 태그를 업데이트 후 푸시합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764588962914&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm version patch

git push origin --tags&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github packages에 배포돼있는 패키지 다운로드 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;github packages에 배포돼있는 패키지를 사용하는것은 배포하는 것보다 까다롭습니다.&lt;/b&gt; (저는 개인적으로 그렇게 느꼈습니다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Github token 발급하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 때와는 달리 &lt;b&gt;라이브러리를 다운로드 할 때는 github personal token이 필요합니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github developer setting에 들어가 personal token을 발급받아 한쪽에 잘 기억해둡시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발급할 때 read:packages 권한은 반드시 체크해야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Github token 환경변수에 등록하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 token을 &lt;u&gt;패키지 매니저의 설정파일(.npmrc, yarnrc 등)에 넣어야하는데 여기에 하드코딩하여 코드를 github에 푸시하면 그대로 토큰이 &lt;b&gt;노출&lt;/b&gt;됩니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;b&gt; 저는 토큰 유출을 막기위해 토큰을 쉘의 환경변수로 등록하여 사용했습니다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1764589796445&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# zsh를 사용할 경우
echo 'export GITHUB_TOKEN=&quot;value&quot;' &amp;gt;&amp;gt; ~/.zshrc
source ~/.zshrc

# bash를 사용할 경우
echo 'export GITHUB_TOKEN=&quot;value&quot;' &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Github registry 가르키기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제일먼저 배포할 때와 마찬가지로 Github registry를 가르켜야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github registry를 가르키지 않고 패키지 설치 명령어를 실행하면 패키지를 찾을 수 없다고 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 패키지는 npm registry가 아닌 github registry에 있으니 당연합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용하는 패키지매니저의 설정파일에 registry의 url과 앞서 환경변수에 등록해놨던 토큰의 키(GITHUB_TOKEN)를 작성합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764589935437&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .yarnrc.yml
npmScopes:
  &quot;runzipper&quot;:
    npmRegistryServer: 'https://npm.pkg.github.com'
    npmAuthToken: '${GITHUB_TOKEN}'
    
# .npmrc
@runzipper:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;패키지 다운로드 하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 매니저에 따른 명령어를 입력해 패키지를 다운로드 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764590040809&quot; class=&quot;dockerfile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;yarn add &amp;lt;package&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 github packages에 배포해봤는데 설정에 생각보다 시간이 많이 걸렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포된 라이브러리를 실제로 다른 프로젝트에서 돌아가는지 테스트하고, 고치고, 다시 배포하는 것이 생각보다 시간을 많이 잡아먹었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 라이브러리가 실행되는지 테스트하는데 든 생각은, &quot;나만 사용할 라이브러리이고 한 프로젝트에 종속적이라면 private package에 배포할 필요가 있을까? 수정하고 재배포하는 시간이 개발하는 시간보다 더 드는 것 같은데?&quot; 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 UI 부분을 라이브러리화 한게 UI와 비즈니스 로직 패키지로 분리하고 싶어서 였습니다. 근데 그 방식이 굳이 라이브러리로 배포하는것이 아니어도 괜찮지 않나 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 케이스 때문에 모노레포가 등장한것 같습니다.&lt;/p&gt;</description>
      <category>npm</category>
      <category>github</category>
      <category>github package</category>
      <category>npm</category>
      <category>private package</category>
      <category>yarn</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/41</guid>
      <comments>https://bobostown.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 1 Dec 2025 21:10:50 +0900</pubDate>
    </item>
    <item>
      <title>[package.json] peer dependency (feat. UI 라이브러리)</title>
      <link>https://bobostown.tistory.com/40</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;peer dependency는 어떠한 상황에서 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 개발하면서 peer dependency에 대해 몰랐는데, 최근 UI 라이브러리를 개발하면서 이에 대해 학습하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 라이브러리는 React를 사용하여 개발했습니다. 그런데 이 라이브러리를 설치하는 프로젝트에도 이미 React가 설치되어 있다면, React가 중복으로 설치되는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 같은 문제를 해결하는 것이 peer dependency입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peer dependency는 &quot;&lt;b&gt;이 라이브러리를 사용하려면 특정 패키지가 필요합니다&lt;/b&gt;&quot;라고 명시하는 방법입니다. 패키지를 직접 포함하지 않고. 사용자가 이미 설치했을 것으로 기대하는 의존성을 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;peer dependency 적용 전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peer dependency를 적용하기 전에 제 UI 라이브러리의 package.json을 보면 다음과 같이 돼있습니다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
    ...
    &quot;dependencies&quot;: {
        &quot;@vanilla-extract/css&quot;: &quot;^1.17.4&quot;,
        &quot;@vanilla-extract/vite-plugin&quot;: &quot;^5.1.1&quot;,
        &quot;clsx&quot;: &quot;^2.1.1&quot;,
        &quot;react&quot;: &quot;^19.2.0&quot;,
        &quot;react-dom&quot;: &quot;^19.2.0&quot;
    },
    &quot;devDependencies&quot;: {
     ...
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 UI 라이브러리의 개발에 사용된 react와 react-dom이 dependencies 필드에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 상태에서 누군가가 이 UI 라이브러리를 설치하면 어떻게 될까요?&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;node_modules/
  react@19.1.0          &amp;larr; 프로젝트에 이미 있던 React
  react-dom@19.1.0
  @runzipper/ui/
    node_modules/
      react@19.2.0      &amp;larr; UI 라이브러리가 설치한 React (중복!)
      react-dom@19.2.0  &amp;larr; (중복!)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dependencies에 명시된 패키지는 라이브러리 설치 시 함께 설치되기 때문에, React가 중복으로 설치됩니다. 이는 디스크 공간 낭비는 물론, React의 여러 인스턴스로 인한 런타임 에러를 발생시킬 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;peer dependency 적용 후&lt;/h4&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
    ...
    &quot;dependencies&quot;: {
        &quot;@vanilla-extract/css&quot;: &quot;^1.17.4&quot;,
        &quot;clsx&quot;: &quot;^2.1.1&quot;
    },
    &quot;peerDependencies&quot;: {
        &quot;react&quot;: &quot;^19.2.0&quot;,
        &quot;react-dom&quot;: &quot;^19.2.0&quot;
    },
    &quot;devDependencies&quot;: {
        ...
        &quot;react&quot;: &quot;^19.2.0&quot;,
        &quot;react-dom&quot;: &quot;^19.2.0&quot;,
        ...
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이제 사용자가 라이브러리를 설치하면:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;프로젝트/
node_modules/
  react@19.1.0          &amp;larr; 하나만 설치됨!
  react-dom@19.1.0
  @runzipper/ui/        &amp;larr; React를 포함하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 중복 설치되지 않고, 프로젝트의 React를 공유하여 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데 말입니다..?  &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peer dependency를 적용했을 때랑 하지 않았을 때랑 빌드 결과물의 차이가 나지 않았습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# peer dependency 미적용
dist/ui-style.css  9,207.24 kB │ gzip: 6,850.07 kB
dist/main.mjs         23.58 kB │ gzip:     6.99 kB
dist/ui-style.css  9,207.24 kB │ gzip: 6,850.07 kB
dist/main.umd.js      18.00 kB │ gzip:     6.49 kB
✓ built in 4.03s


# peer dependency 적용
ist/ui-style.css  9,207.24 kB │ gzip: 6,850.07 kB
dist/main.mjs         23.58 kB │ gzip:     6.99 kB
dist/ui-style.css  9,207.24 kB │ gzip: 6,850.07 kB
dist/main.umd.js      18.00 kB │ gzip:     6.49 kB
✓ built in 4.01s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 peer dependency를 적용한 버전의 빌드 크기가 당연히 작을 줄 알아서 당황했습니다.  &lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 peer dependency 적용 유무와 상관없이 결과물의 크기가 같지?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니 제가 이전에 vite 설정에서 build 옵션으로 react와 react-dom를 번들에서 제외하도록 설정해 놔서 peer dependency와 관계없이 빌드 결과물에서 제외됐던 겁니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;rollupOptions: {
    # 이 부분
    external: ['react', 'react-dom'],
    output: {
        globals: {
            react: 'React',
            'react-dom': 'ReactDOM',
        },
    },
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 peer dependency의 역할에 대해 다시 생각해 보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;peer dependency의 역할&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 peer dependency가 빌드 크기를 줄여준다고 생각했지만, 실제로는 그렇지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;peer dependency에 라이브러리를 명시한다고 해당 라이브러리가 빌드에서 제외되는 게 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peer dependency와 빌드 설정은 서로 다른 목적을 가지고 있습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Peer Dependency의 역할&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;사용자에게 &quot;이 버전이 필요합니다&quot;라고 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite의 external 설정 역할&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;p data-ke-size=&quot;size16&quot;&gt;따라서 번들 크기를 줄이는 것이 목적이면 적절한 번들러 설정과 peer dependency 명시 둘 다 필요합니다.&lt;/p&gt;</description>
      <category>npm</category>
      <category>npm</category>
      <category>package.json</category>
      <category>peer dependency</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/40</guid>
      <comments>https://bobostown.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 1 Dec 2025 17:07:58 +0900</pubDate>
    </item>
    <item>
      <title>[React 19] &amp;lt;Activity&amp;gt; 컴포넌트를 사용하여 conditional rendering의 문제를 해결하기</title>
      <link>https://bobostown.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 어떤 요소를 상황에 따라 보여주거나 숨길 때 어떤 방식을 사용하나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 조건부 렌더링을 하지 않으신가요?&lt;/p&gt;
&lt;pre id=&quot;code_1764073635037&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return (
	&amp;lt;&amp;gt;
    	{isShowing &amp;amp;&amp;amp; &amp;lt;Compenent/&amp;gt;
    	&amp;lt;&amp;gt;
)&lt;/code&gt;&lt;/pre&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존&amp;nbsp; 조건부 렌더링의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 조건부 렌더링의 문제를 설명하기 위해 예시 코드를 작성해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;code_1764075387145&quot; data-ke-type=&quot;html&quot; data-source=&quot;&amp;lt;iframe width='100%' height='400' src=&amp;quot;https://stackblitz.com/edit/vitejs-vite-dkayf6dw?embed=1&amp;amp;file=src%2FApp.css&amp;quot; /&amp;gt;&quot;&gt;&lt;iframe src=&quot;https://stackblitz.com/edit/vitejs-vite-dkayf6dw?embed=1&amp;amp;file=src%2FApp.css&quot; width=&quot;100%&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서는 숫자를 증가시키거나 줄일 수 있는 카운터가 있고, 이 카운터를 토글 할 수 있는 버튼이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자식 컴포넌트의 상태가 유지되지 않습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카운터를 열고, 숫자를 증가시키거나 감소 후에 카운터를 닫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 카운터를 열면 카운터는 기존에 내가 조작했던 숫자가 아닌, 0으로 초기화가 되었을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건부 렌더링을 하면 자식 컴포넌트가 언마운트 후에 다시 마운트 되어서, &lt;u&gt;언마운트 되기 전에 가지고 있던 상태를 잃어버립니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;display 속성으로 자식 컴포넌트의 상태 유지하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 자식 컴포넌트의 상태를 유지하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 전체를 조건부로 렌더링 하지 않고 display 속성을 이용하면 됩니다.&lt;/p&gt;
&lt;div id=&quot;code_1764076558779&quot; data-ke-type=&quot;html&quot; data-source=&quot;&amp;lt;iframe width='100%' height='400' src='https://stackblitz.com/edit/vitejs-vite-ok1zs8e3?embed=1&amp;amp;file=src%2FApp.tsx'/&amp;gt;&quot;&gt;&lt;iframe src=&quot;https://stackblitz.com/edit/vitejs-vite-ok1zs8e3?embed=1&amp;amp;file=src%2FApp.tsx&quot; width=&quot;100%&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제와 같이 display 속성을 사용하여 조건부 렌더링을 하면&amp;nbsp; &lt;u&gt;Counter 컴포넌트의 내부 상태는 유지됩니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;위 방법의 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 방법도 시나리오에 따라 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법은 display 속성을 통해 화면에서 안 보이는 것이지, 완전히 언마운트를 한 것이 아니기 때문에 clean up 함수가 작동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;u&gt;조건부 렌더링마다 clean up 함수의 실행이 필요한 시나리오 경우, 위의 방법은 적절하지 않습니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;Activity /&amp;gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건부 렌더링에서 컴포넌트의 상태를 유지하면서, clean up 함수의 호출이 필요한 시나리오 경우,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19에서 도입 된 &amp;lt;Activity /&amp;gt; 컴포넌트를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&amp;lt;Activity /&amp;gt; 컴포넌트는 display 속성을 사용해 조건부 렌더링을 하면서도 clean up 함수를 실행할 수 있게 해 줍니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Activity /&amp;gt; 컴포넌트는 2개의 props를 가집니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;children: 표시하거나 숨길 UI&lt;/li&gt;
&lt;li&gt;mode: 'visible' 또는 'hidden'입니다. 기본값은 'visible'입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div id=&quot;code_1764078211974&quot; data-ke-type=&quot;html&quot; data-source=&quot;&amp;lt;iframe src='https://stackblitz.com/edit/vitejs-vite-jgnvkypp?embed=1&amp;amp;file=src%2FApp.tsx' width='100%' height='400' /&amp;gt;&quot;&gt;&lt;iframe src=&quot;https://stackblitz.com/edit/vitejs-vite-jgnvkypp?embed=1&amp;amp;file=src%2FApp.tsx&quot; width=&quot;100%&quot; height=&quot;400&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;콘텐츠 사전 렌더링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;lt;Activity /&amp;gt; 컴포넌트는 조건부 렌더링에 사용하는 것 외에 콘텐츠를 사전 렌더링 하는 데 사용할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;lt;Activity /&amp;gt;의 mode가 hidden인 상태에서는, 자식 컴포넌트는 페이지에 보이지 않지만 여전히 렌더링 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이때, 보이는 콘텐츠보다 낮은 우선순위로 렌더링 되며, Effect는 마운트 되지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이러한 특성을 통해 자식 컴포넌트가 필요한 코드나 데이터를 미리 로드할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;상호작용 속도 높이기&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;lt;Activity /&amp;gt;를 사용하여 상호작용의 속도를 높일 수도 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;lt;Suspense /&amp;gt; 를 사용하여 상호작용 속도를 높일 수도 있지만, &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;lt;Suspense /&amp;gt;는 UI가 변경되기 때문에 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;UI 변경을 원치 않는 경우 &amp;lt;Activity /&amp;gt;를 사용하는 것이 방법이 될 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764079654972&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;&amp;lt;Activity&amp;gt; &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; data-og-url=&quot;https://ko.react.dev/reference/react/Activity&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bPcMBG/hyZOBx9nzz/ALngBaCGpckLYyWI6HDluK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dUs9Ax/hyZOHd35BZ/nCTiDGnPXf3LhN9fKWdkCk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bPcMBG/hyZOBx9nzz/ALngBaCGpckLYyWI6HDluK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dUs9Ax/hyZOHd35BZ/nCTiDGnPXf3LhN9fKWdkCk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&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;&amp;lt;Activity&amp;gt; &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;참고한 자료&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/reference/react/Activity&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764079385631&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;&amp;lt;Activity&amp;gt; &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; data-og-url=&quot;https://ko.react.dev/reference/react/Activity&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bPcMBG/hyZOBx9nzz/ALngBaCGpckLYyWI6HDluK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dUs9Ax/hyZOHd35BZ/nCTiDGnPXf3LhN9fKWdkCk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/reference/react/Activity#speeding-up-interactions-during-page-load&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bPcMBG/hyZOBx9nzz/ALngBaCGpckLYyWI6HDluK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dUs9Ax/hyZOHd35BZ/nCTiDGnPXf3LhN9fKWdkCk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&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;&amp;lt;Activity&amp;gt; &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=IiGIoi8esXY&amp;amp;list=LL&amp;amp;index=3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=IiGIoi8esXY&amp;amp;list=LL&amp;amp;index=3&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=IiGIoi8esXY&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bL3YsJ/hyZNzBWUxp/AVTi9hfQHohLSode5idpWk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/fUM45/hyZN5nFoYv/YG3Vzs0Jav04SzEQU4D5k0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;The NEW Way to Conditional Render in React (Game Changer)&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/IiGIoi8esXY&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <category>activity</category>
      <category>react 19</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/39</guid>
      <comments>https://bobostown.tistory.com/39#entry39comment</comments>
      <pubDate>Tue, 25 Nov 2025 23:10:07 +0900</pubDate>
    </item>
    <item>
      <title>[우테코 8기 프리코스] 오픈 미션 후기</title>
      <link>https://bobostown.tistory.com/38</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기 미션 목표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축/압축해제 프로그램의 구현 방법에는 크게 2가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 파일 들을 각자 압축 후 하나의 파일로 합치기 (zip 방식)&lt;/li&gt;
&lt;li&gt;여러 파일 들을 하나의 파일로 합치고, 이 파일을 압축하기 (tar.gz 방식)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 프로젝트를 구상할 때는 2번 방식으로 구상했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 최종적으로 만들고 싶은 것은 zip 압축 프로그램이어서 1번 방식으로 선회했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;압축해제 시에 합쳐진 파일들은 어떻게 다시 여러 파일들을 나눌 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인터넷에 자료를 찾아보다가 ZIP의 &lt;a href=&quot;https://pkwaredownloads.blob.core.windows.net/pkware-general/Documentation/APPNOTE-6.3.9.TXT&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;specification&lt;/a&gt;에서 해답을 찾을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 파일들을 하나의 파일로 합치는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZIP 포맷을 만든 Pkware사의 &lt;a href=&quot;https://pkwaredownloads.blob.core.windows.net/pkware-general/Documentation/APPNOTE-6.3.9.TXT&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;specification&lt;/a&gt;에는 다음과 같이 나와있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ZIP 파일 내부&lt;/h4&gt;
&lt;pre id=&quot;code_1763884973198&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;4.3.6 Overall .ZIP file format:

  [local file header 1]
  [encryption header 1]
  [file data 1]
  [data descriptor 1]
  . 
  .
  .
  [local file header n]
  [encryption header n]
  [file data n]
  [data descriptor n]
  [archive decryption header] 
  [archive extra data record] 
  [central directory header 1]
  .
  .
  .
  [central directory header n]
  [zip64 end of central directory record]
  [zip64 end of central directory locator] 
  [end of central directory record]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;각 파일들의 헤더&lt;/h4&gt;
&lt;pre id=&quot;code_1763885057685&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;4.3.7  Local file header:

  local file header signature     4 bytes  (0x04034b50)
  version needed to extract       2 bytes
  general purpose bit flag        2 bytes
  compression method              2 bytes
  last mod file time              2 bytes
  last mod file date              2 bytes
  crc-32                          4 bytes
  compressed size                 4 bytes
  uncompressed size               4 bytes
  file name length                2 bytes
  extra field length              2 bytes

  file name (variable size)
  extra field (variable size)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 형식을 보고&amp;nbsp; 비슷하게 구현하면 될 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.compressed&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 ZIP파일의 형식보다 간소화된 .compressed의 포맷을 정의했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763885636712&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.compressed file format:

  [filecount] (4byte)
  [pathLength] (4byte)
  [pathBuffer] 
  [compressedLength] (4byte)
  [compressedContent]
  [pathLength] (4byte)
  [pathBuffer] 
  [compressedLength] (4byte)
  [compressedContent]
  . 
  .
  .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZIP 형식이나 .compressed 형식같이 (데이터의 길이 - 데이터)로 나타내는 것을 TLV라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763885727405&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;형식-길이-값 - 위키백과, 우리 모두의 백과사전&quot; data-og-description=&quot;위키백과, 우리 모두의 백과사전. 형식-길이-값 또는 TLV(type-length-value 혹은 tag-length-value의 약자)는 통신 프로토콜에서 필수적이지 않은 항목의 자료를 부호화하는 방식이다. 항목의 형식(자료형)&quot; data-og-host=&quot;ko.wikipedia.org&quot; data-og-source-url=&quot;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&quot; data-og-url=&quot;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;위키백과, 우리 모두의 백과사전. 형식-길이-값 또는 TLV(type-length-value 혹은 tag-length-value의 약자)는 통신 프로토콜에서 필수적이지 않은 항목의 자료를 부호화하는 방식이다. 항목의 형식(자료형)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.wikipedia.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;h4 data-ke-size=&quot;size20&quot;&gt;압축 영상&lt;/h4&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/459465623&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bASiBt/hyZOjqCQrz/S1kNQSQjCz73G11LK6EqFK/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844,https://scrap.kakaocdn.net/dn/fR4DR/hyZOuZY67I/DQzPSAGFhkg0lbviqkzNIk/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844&quot; data-video-width=&quot;860&quot; data-video-height=&quot;378&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;378&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/459465623?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;378&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;압축 해제 영상&lt;/h4&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/459465677&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/d9bPZD/hyZOxh6CNP/4P6KWyGhpyqBg843fx6gl1/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844,https://scrap.kakaocdn.net/dn/cFh5JL/hyZOt7Qj7L/XjVnBJ0YKLky3251v9mXo0/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844&quot; data-video-width=&quot;860&quot; data-video-height=&quot;378&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;378&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/459465677?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;378&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중간 구현에서 아쉬운 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 압축/압축해제를 한다는 1차 목표는 달성했지만 아쉬운점이 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;근데 막상 구현을 해보니 생각보다 직관적이지 않고, 이 프로그램을 개발한 저조차 사용할 때 햇갈렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 이 프로그램을 사용하려면 해당 리포지토리를 클론하고, 빌드해서 실행해야 하는것도 불편했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;s&gt;나중에 변심해서 이 방식이 더 좋았다고 생각할 수 있잖아요?&lt;/s&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 해당 코드를 지우기보다, 해당 코드에서 인터페이스를 추출후에 인터페이스의 구현체를 만드는, &lt;b&gt;전략패턴&lt;/b&gt;을 적용하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;b&gt;InteractiveFileSelector&lt;/b&gt; 클래스에서 &lt;b&gt;FileSelector&lt;/b&gt; 인터페이스를 추출했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763887322781&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default interface FileSelector {
  selectTarget: () =&amp;gt; Promise&amp;lt;string&amp;gt;;
  selectTargetDirectory: () =&amp;gt; Promise&amp;lt;string&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스를 구현하여 &lt;b&gt;TextFileSelector&lt;/b&gt;를 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763887365494&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import ERROR_MESSAGE from '../constants/errorMessage.js';
import Input from '../view/Input.js';
import FileSelector from './FileSelector.js';
import fs from 'node:fs';
import path from 'node:path';

class TextFileSelector implements FileSelector {
  async selectTarget() {
    const userInput = (await Input.readLineAsync()).trim();

    const resolvedPath = path.resolve(userInput);
    this.isExistFileOrDirectory(resolvedPath);

    return userInput;
  }
  async selectTargetDirectory() {
    const userInput = (await Input.readLineAsync()).trim();

    const resolvedPath = path.resolve(userInput);
    this.isExistDirectory(resolvedPath);

    return userInput;
  }

  private isExistFileOrDirectory(pathString: string) {
    if (!fs.existsSync(pathString)) {
      throw new Error(ERROR_MESSAGE.NOT_EXISTING_FILE_OR_DIRECTORY);
    }
  }

  private isExistDirectory(pathString: string) {
    if (!fs.existsSync(pathString)) {
      throw new Error(ERROR_MESSAGE.NOT_DIRECTORY(pathString));
    }
  }
}

export default TextFileSelector;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 App.ts에서 &lt;b&gt;TextFileSelector&lt;/b&gt;를 의존성으로 주입했습니다&lt;b&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763887581876&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class App {
  ...

  constructor() {
    ...
    this.compressController = new CompressController(new TextFileSelector());
    ...
  }
	...
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 방식과 사용자가 경로를 넣는 방식을 비교했을 때 사용성이 개선되었습니다.&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/459466397&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/CeB6Y/hyZOa9R490/R3mk9Mm6TKDIkx2YOPzSa0/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844,https://scrap.kakaocdn.net/dn/gRy5s/hyZOqcduPU/MdWf6pR8oa6QkzRgSZafk1/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844&quot; data-video-width=&quot;860&quot; data-video-height=&quot;378&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;378&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/459466397?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;378&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[개선] 빌드 &amp;amp; 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 실행하려면 프로젝트를 clone하고 빌드후에 실행해야해서 불편했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 개선하기 위해 npm에 프로젝트를 배포했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(npm에 어떻게 배포했는지는 아래 글을 봐주세요!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/37&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 통해 사용자는 npx 명령어를 통해 제 프로그램을 실행할 수 있게 되었지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 배포를 위해 npm publish 명령어를 입력해야 한다는 불편함이 남아 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;npm 배포 자동화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 자동화하기 위해 Github action을 사용하여 배포 자동화를 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763888967593&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy to NPM

on:
  push:
    tags:
      - v*

jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '22.19'
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies
        run: npm ci

      - name: Test package
        run: npm run test

      - name: Build package
        run: npm run build

      - name: Publish to NPM
        run: npm publish --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;tsup으로 빌드 결과물 용량 줄이기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 배포에서 아쉬운점이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsc로 typescript를 javascript로 컴파일하다 보니 파일들이 번들링이 안되있고, minification과 tree-shaking이 안되있어서 빌드 결과물의 용량이 크다는 점에 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 결과물의 용량이 크다는 것은 사용자가 프로그램을 실행할 때 npm registry에서 다운로드하는 시간이 길어진다는 것을 의미하기에 이를 개선해야된다고 생각했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHIR2b/dJMcadUGzkJ/kw5GgTLM5whBUpKQgpIAA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHIR2b/dJMcadUGzkJ/kw5GgTLM5whBUpKQgpIAA0/img.png&quot; data-alt=&quot;tsc로 빌드했을때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHIR2b/dJMcadUGzkJ/kw5GgTLM5whBUpKQgpIAA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHIR2b%2FdJMcadUGzkJ%2Fkw5GgTLM5whBUpKQgpIAA0%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;700&quot; height=&quot;378&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;tsc로 빌드했을때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mmk3B/dJMcaihrsoB/vk76sCzuL4aHWtE4fsAHhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mmk3B/dJMcaihrsoB/vk76sCzuL4aHWtE4fsAHhk/img.png&quot; data-alt=&quot;tsc로 빌드했을때 용량&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mmk3B/dJMcaihrsoB/vk76sCzuL4aHWtE4fsAHhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmmk3B%2FdJMcaihrsoB%2Fvk76sCzuL4aHWtE4fsAHhk%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;735&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;tsc로 빌드했을때 용량&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 개선하기 위해 tsup을 사용하여 프로젝트를 빌드했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;tsup으로 빌드를 하니 빌드 결과물의 크기가 &lt;b&gt;46%&lt;/b&gt; 감소했습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnVQHc/dJMcaaX0THm/Z2aY9aREknd6vaR2Tdcc1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnVQHc/dJMcaaX0THm/Z2aY9aREknd6vaR2Tdcc1k/img.png&quot; data-alt=&quot;tsup로 빌드했을때 용량&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnVQHc/dJMcaaX0THm/Z2aY9aREknd6vaR2Tdcc1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnVQHc%2FdJMcaaX0THm%2FZ2aY9aREknd6vaR2Tdcc1k%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;735&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;tsup로 빌드했을때 용량&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;a href=&quot;https://www.npmjs.com/package/open-mission-compressor&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/open-mission-compressor&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/459467252&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/dljzbw/hyZOwKhpMt/k1h6c3VKdS1orXh2rM5xgK/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844,https://scrap.kakaocdn.net/dn/BBIAN/hyZOyORqTR/P7laGT5cLdaTmSaamaR9MK/img.jpg?width=1920&amp;amp;height=844&amp;amp;face=0_0_1920_844&quot; data-video-width=&quot;860&quot; data-video-height=&quot;378&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;378&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/459467252?service=daum_tistory&quot; width=&quot;860&quot; height=&quot;378&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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 data-ke-size=&quot;size16&quot;&gt;처음 프로젝트를 시작할 때 세운 계획과 비교하면 아쉬움이 많이 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈미션을 계획할때는 ZIP을 압축하고 압축해제할 수 있는 프로그램을 만들고, 다양한 압축 포맷 지원, 그리고 이를 rust로 재 작성하여 node.js 프로그램과 rust 프로그램간의 성능 비교를 해볼 생각이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아무래도 프론트엔드를 개발하는 입장로써 node.js를 상대적으로 깊게 사용해본 것이 이번이 처음이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축/압축해제를 구현하기 위해 사전조사를 하느라 시간이 많이 소모되어서 node.js 압축 프로그램 개발까지만 끝낼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;표준이 아닙니다. 제 프로그램으로 압축한 파일만 압축 해제 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;경우에 따라 압축된 결과물의 용량이 더 클 수 있습니다.&lt;/li&gt;
&lt;li&gt;압축 시간이 오래 걸립니다.&lt;/li&gt;
&lt;li&gt;node.js의 filesystem과 path module을 사용하는 부분등 많은 부분을 테스트 하지 못했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물은 아쉽지만 개발 과정에서 압축/압축해제를 수행하는 다양한 알고리즘(inflate, lz77, huffman)들을 알 수 있었고, 알고리즘을 실제로 구현해볼 수 있는 기회가 되었습니다. (&lt;s&gt;node.js의 zlib 사용없이 직접 구현해보고 싶었습니다.&lt;/s&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 배포도 이번에 처음 해봤는데, CLI 프로그램이나 라이브러리를 배포하는 방법을 배울 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, node.js로 프로그램 개발하라고 하면 할 수 있는 자신감도 생겼습니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과제를 구현하며 조사한 자료&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;압축 알고리즘 정리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/32&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763892317773&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[압축 알고리즘] LZ77 알고리즘 이란?&quot; data-og-description=&quot;LZ77 알고리즘은 ZIP 압축에 사용되는 Deflate 알고리즘을 구성하는 압축 알고리즘입니다.Deflate 알고리즘은 LZ77 알고리즘으로 1차 압축 후에 2차로 허프만 코드 알고리즘을 사용하여 압축합니다. 현&quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/32&quot; data-og-url=&quot;https://bobostown.tistory.com/32&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MF3PK/hyZOvLnO0c/UYzH0kxc6cyq8yHqk3iHe1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/S9Qk8/hyZNzodPGT/k7BiY1W3N7nSGn5pvxtDi0/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/32&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MF3PK/hyZOvLnO0c/UYzH0kxc6cyq8yHqk3iHe1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/S9Qk8/hyZNzodPGT/k7BiY1W3N7nSGn5pvxtDi0/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379');&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;[압축 알고리즘] LZ77 알고리즘 이란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;LZ77 알고리즘은 ZIP 압축에 사용되는 Deflate 알고리즘을 구성하는 압축 알고리즘입니다.Deflate 알고리즘은 LZ77 알고리즘으로 1차 압축 후에 2차로 허프만 코드 알고리즘을 사용하여 압축합니다. 현&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/33&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763892300691&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[압축 알고리즘] LZ77 구현&quot; data-og-description=&quot;압축 알고리즘을 Node.js로 구현해 봤습니다.Node.js의 Buffer와 LCS알고리즘을 사용하여 구현했습니다. LZ77 구현Search buffer size와 Look ahead buffer size는 임의의 값이 아닌 Deflate 알고리즘의 Spec인 32KB와 258&quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/33&quot; data-og-url=&quot;https://bobostown.tistory.com/33&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnz8sF/hyZOmHFLsY/JZg2JrelaDUXoJkRPfD0t1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cHOLV3/hyZN4hyafb/yKsZ7nbh2grzjRReq9OZpK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/33&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/33&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnz8sF/hyZOmHFLsY/JZg2JrelaDUXoJkRPfD0t1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cHOLV3/hyZN4hyafb/yKsZ7nbh2grzjRReq9OZpK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[압축 알고리즘] LZ77 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;압축 알고리즘을 Node.js로 구현해 봤습니다.Node.js의 Buffer와 LCS알고리즘을 사용하여 구현했습니다. LZ77 구현Search buffer size와 Look ahead buffer size는 임의의 값이 아닌 Deflate 알고리즘의 Spec인 32KB와 258&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;node.js 모듈 및 클래스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/34&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763892360284&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[node.js] node:fs&quot; data-og-description=&quot;fs modulenode.js에서 파일 시스템에 접근할 수 있는 방법은 2가지입니다.promise-based API인 node:fs/promisescallback 및 sync API인 node:fs모든 파일 시스템 작업은 동기식, 콜백 방식, 프로미스 기반 형태로 제공&quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/34&quot; data-og-url=&quot;https://bobostown.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dzETST/hyZOns2Q7l/AxP9zWEN0sUugqkB50ipzK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bObC6B/hyZOrPKgt7/3OCw6zQcSGYYPazHXOd950/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dzETST/hyZOns2Q7l/AxP9zWEN0sUugqkB50ipzK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bObC6B/hyZOrPKgt7/3OCw6zQcSGYYPazHXOd950/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[node.js] node:fs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;fs modulenode.js에서 파일 시스템에 접근할 수 있는 방법은 2가지입니다.promise-based API인 node:fs/promisescallback 및 sync API인 node:fs모든 파일 시스템 작업은 동기식, 콜백 방식, 프로미스 기반 형태로 제공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/35&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763892368475&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[node.js] node:path&quot; data-og-description=&quot;path modulepath 모듈은 파일과 디렉터리 경로 작업을 위한 유틸리티를 제공합니다. path 모듈은 node.js가 실행되는 운영체제에 따라 기본 동작이 달라집니다.예를 들어, Windows 운영체제에서 path 모듈&quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/35&quot; data-og-url=&quot;https://bobostown.tistory.com/35&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c1y1o2/hyZN7SS2SS/y44ZkMJ0djOJgzSSqJQeUK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cAVDjN/hyZOcNpL5H/KsWKOmYBQj0elFa3spF331/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/35&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/35&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c1y1o2/hyZN7SS2SS/y44ZkMJ0djOJgzSSqJQeUK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cAVDjN/hyZOcNpL5H/KsWKOmYBQj0elFa3spF331/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[node.js] node:path&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;path modulepath 모듈은 파일과 디렉터리 경로 작업을 위한 유틸리티를 제공합니다. path 모듈은 node.js가 실행되는 운영체제에 따라 기본 동작이 달라집니다.예를 들어, Windows 운영체제에서 path 모듈&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763892376962&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Bobostown&quot; data-og-description=&quot; &quot; data-og-host=&quot;bobostown.tistory.com&quot; data-og-source-url=&quot;https://bobostown.tistory.com/&quot; data-og-url=&quot;https://bobostown.tistory.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nW0fW/hyZNI6xoiv/Snw2yNIoT907yieu4nmS50/img.jpg?width=296&amp;amp;height=298&amp;amp;face=0_0_296_298,https://scrap.kakaocdn.net/dn/bXWzyf/hyZNIyHbNM/PkQdKhuPFH6ckTzTVfek80/img.jpg?width=296&amp;amp;height=298&amp;amp;face=0_0_296_298&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bobostown.tistory.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nW0fW/hyZNI6xoiv/Snw2yNIoT907yieu4nmS50/img.jpg?width=296&amp;amp;height=298&amp;amp;face=0_0_296_298,https://scrap.kakaocdn.net/dn/bXWzyf/hyZNIyHbNM/PkQdKhuPFH6ckTzTVfek80/img.jpg?width=296&amp;amp;height=298&amp;amp;face=0_0_296_298');&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;Bobostown&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bobostown.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;npm에 CLI 프로그램 배포하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bobostown.tistory.com/37&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bobostown.tistory.com/37&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ZIP Specification&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pkwaredownloads.blob.core.windows.net/pkware-general/Documentation/APPNOTE-6.3.9.TXT&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pkwaredownloads.blob.core.windows.net/pkware-general/Documentation/APPNOTE-6.3.9.TXT&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한 테크코스 8기 프리코스</category>
      <category>오픈미션</category>
      <category>우테코</category>
      <author>월월월월</author>
      <guid isPermaLink="true">https://bobostown.tistory.com/38</guid>
      <comments>https://bobostown.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 23 Nov 2025 17:47:15 +0900</pubDate>
    </item>
  </channel>
</rss>