<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sky.log</title>
        <link>https://velog.io/</link>
        <description>스벨트로 개발하는 것을 좋아합니다.</description>
        <lastBuildDate>Tue, 15 Apr 2025 05:19:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sky.log</title>
            <url>https://velog.velcdn.com/images/_sky/profile/97c1ed1a-9e00-4df1-bcf2-70b8cbb706b5/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sky.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_sky" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[PDF 전자서명 개발기]]></title>
            <link>https://velog.io/@_sky/PDF-%EB%B7%B0%EC%96%B4%EC%99%80-%EC%A0%84%EC%9E%90%EC%84%9C%EB%AA%85-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@_sky/PDF-%EB%B7%B0%EC%96%B4%EC%99%80-%EC%A0%84%EC%9E%90%EC%84%9C%EB%AA%85-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Tue, 15 Apr 2025 05:19:59 GMT</pubDate>
            <description><![CDATA[<p>최근 진행한 프로젝트에서 PDF 문서를 웹 상에서 <strong>미리보기</strong>, <strong>전자서명</strong>, 그리고 <strong>다운로드</strong>까지 가능한 기능을 개발했습니다. 이 포스트에서는 어떤 기술을 사용했는지, 그리고 어떤 고민과 해결 과정을 거쳤는지 기록해보려 합니다.</p>
<p><a href="https://quick-sign-rho.vercel.app/">프로젝트 미리보기</a></p>
<hr>
<h2 id="🧩-목표-기능">🧩 목표 기능</h2>
<ul>
<li>PDF 파일 업로드 및 페이지별 미리보기  </li>
<li>원하는 페이지에 도장(이미지) 찍기  </li>
<li>전자서명 포함된 PDF 다운로드  </li>
</ul>
<hr>
<h2 id="🔧-사용한-기술-스택">🔧 사용한 기술 스택</h2>
<ul>
<li><strong>React 19</strong></li>
<li><strong>TypeScript</strong></li>
<li><strong>pdf-lib</strong> – PDF 생성 및 수정</li>
<li><strong>pdfjs-dist</strong> – PDF 페이지 렌더링</li>
<li><strong>fabric.js</strong> – 캔버스 기반 도장 배치 및 사용자 입력</li>
<li><strong>zustand</strong> – 전역 상태 관리</li>
<li><strong>Vite</strong> – 번들러</li>
</ul>
<hr>
<h2 id="📄-pdf-렌더링-pdfjs-dist와-fabricjs-조합">📄 PDF 렌더링: pdfjs-dist와 fabric.js 조합</h2>
<p>PDF의 각 페이지를 이미지로 변환하기 위해 <code>pdfjs-dist</code>의 <code>getViewport</code>와 <code>page.render</code>를 사용했습니다. 이미지로 변환 후에는 <code>fabric.Canvas</code> 위에 백그라운드 이미지로 설정해 사용자 입력이 가능하게 구성했습니다.</p>
<pre><code class="language-ts">export const getImageByPdf = async (
  pdf: pdfjsLib.PDFDocumentProxy,
  pageIndex: number,
  scale = 3
): Promise&lt;string&gt; =&gt; {
  const pageNumber = pageIndex + 1;

  try {
    const page = await pdf.getPage(pageNumber);
    const viewport = page.getViewport({ scale });

    const canvas = document.createElement(&#39;canvas&#39;);
    const context = canvas.getContext(&#39;2d&#39;);

    canvas.width = viewport.width;
    canvas.height = viewport.height;

    await page.render({ canvasContext: context!, viewport }).promise;

    return canvas.toDataURL(&#39;image/png&#39;);
  } catch (error) {
    console.error(`Error rendering page ${pageNumber}:`, error);
    throw new Error(`Failed to render page ${pageNumber}.`);
  }
};</code></pre>
<pre><code class="language-ts">export const CanvasProvider = ({ children }: { children: ReactNode }) =&gt; {
  ...생략

  const initializeCanvas = async (file: File, selectedPageFileIndex: number) =&gt; {
    ...생략
    const image = await getImageByPdf(pdf, selectedPageFileIndex);
    const img = await fabric.FabricImage.fromURL(image!);

    const scaleX = FABRIC_CANVAS_WIDTH / img.width;
    const scaleY = FABRIC_CANVAS_HEIGHT / img.height;

    img.set({
      scaleX,
      scaleY,
      left: 0,
      top: 0,
      objectCaching: false
    });

    if (fabricCanvasRef.current) {
      fabricCanvasRef.current.backgroundImage = img;
      fabricCanvasRef.current.requestRenderAll();
      fabricCanvasRef.current.renderAll();
    }
  };</code></pre>
<h3 id="성능-이슈-⚡">성능 이슈 ⚡</h3>
<p>PDF가 50페이지 이상인 경우, 전체 페이지를 이미지로 변환하는 데 8초 이상이 걸렸습니다. 해결을 위해:</p>
<ul>
<li>Promise.all을 사용한 병렬 처리</li>
<li>렌더링 해상도 조절 (scale: 5 → 2)</li>
<li>이미지 변환 대신 PDF 페이지 직접 썸네일로 표시하는 방법도 고려</li>
</ul>
<hr>
<h2 id="🖼️-도장전자서명-기능">🖼️ 도장(전자서명) 기능</h2>
<p>사용자가 업로드한 도장 이미지를 optimizeImage 함수로 리사이징하고, fabric.js에서 드래그 &amp; 드롭 등 배치가 가능하게 만들었습니다.</p>
<pre><code class="language-ts">export const optimizeImage = (file: File, maxWidth = 200, maxHeight = 200): Promise&lt;string&gt; =&gt; {
  return new Promise((resolve, reject) =&gt; {
    const img = new Image();
    img.crossOrigin = &#39;anonymous&#39;;
    const objectUrl = URL.createObjectURL(file);
    img.src = objectUrl;

    const processImage = () =&gt; {
      try {
        const canvas = document.createElement(&#39;canvas&#39;);
        const ctx = canvas.getContext(&#39;2d&#39;);

        let { width, height } = img;

        if (width &gt; maxWidth || height &gt; maxHeight) {
          if (width / height &gt; maxWidth / maxHeight) {
            height = (height * maxWidth) / width;
            width = maxWidth;
          } else {
            width = (width * maxHeight) / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;

        if (!ctx) {
          throw new Error(&#39;Canvas context not available&#39;);
        }

        ctx.drawImage(img, 0, 0, width, height);
        resolve(canvas.toDataURL(&#39;image/png&#39;));
      } catch (error) {
        reject(error);
      } finally {
        URL.revokeObjectURL(objectUrl);
      }
    };

    img.onload = processImage;
    img.onerror = (error) =&gt; {
      reject(error);
    };
  });
};</code></pre>
<ul>
<li>사용자의 편의를 고려하여 이미지 포맷이라면 모두 가능하도록 처리했습니다. 함수 내부에서 png로 컨버팅합니다.</li>
<li>URL.createObjectURL 사용 후 URL.revokeObjectURL()로 메모리 누수 방지</li>
</ul>
<p>그리고 사용자가 도장을 찍으면 해당 PDF 페이지만 업데이트 되도록 처리했습니다.</p>
<pre><code class="language-ts">export const applyStampToPdf = async ({
  canvas,
  originFile,
  pageIndex
}: {
  canvas: fabric.Canvas;
  originFile: File;
  pageIndex: number;
}) =&gt; {
  const fileArrayBuffer = await originFile.arrayBuffer();
  const pdfDoc = await PDFDocument.load(new Uint8Array(fileArrayBuffer));
  const dataUrl = canvas.toDataURL({
    format: &#39;png&#39;,
    multiplier: 3
  });

  const [, imageBytes] = dataUrl.split(&#39;,&#39;);
  const pngImage = await pdfDoc.embedPng(imageBytes);

  const page = pdfDoc.getPages()[pageIndex];
  const { width, height } = page.getSize();

  page.drawImage(pngImage, {
    x: 0,
    y: 0,
    width,
    height
  });

  const newPdfBytes = await pdfDoc.save();
  return new File([newPdfBytes], originFile.name, { type: &#39;application/pdf&#39; });
};
</code></pre>
<hr>
<h2 id="📥-최종-pdf-다운로드">📥 최종 PDF 다운로드</h2>
<p>도장이 찍혀 최종 업데이트된 file 요소를 pdf-lib를 활용하여 다운로드 가능한 PDF 형태로 생성합니다.</p>
<pre><code class="language-ts">export const downloadPdf = async (file: File) =&gt; {
  try {
    const pdfDoc = await PDFDocument.create();
    const { pdf, totalPages } = await loadPdf(file);

    const imageDataUrls = await Promise.all(
      Array.from({ length: totalPages }, (_, i) =&gt; getImageByPdf(pdf, i))
    );
    const imageBuffers = await Promise.all(
      imageDataUrls.map((url) =&gt; fetch(url).then((res) =&gt; res.arrayBuffer()))
    );
    const embeddedImages = await Promise.all(imageBuffers.map((bytes) =&gt; pdfDoc.embedPng(bytes)));

    for (const img of embeddedImages) {
      const { width, height } = img.scale(1);
      const page = pdfDoc.addPage([width, height]);

      page.drawImage(img, {
        x: 0,
        y: 0,
        width,
        height
      });
    }

    const pdfBytes = await pdfDoc.save();

    const blob = new Blob([pdfBytes], { type: &#39;application/pdf&#39; });

    download(blob, file.name);
  } catch (error) {
    console.error(&#39;Error generating PDF:&#39;, error);
  }
};</code></pre>
<hr>
<h2 id="📌-고민과-정리">📌 고민과 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>🐢 렌더링 속도 개선</td>
<td>PDF가 50장 이상일 경우 이미지 추출에만 6~8초 소요 → <code>Promise.all</code>로 병렬 처리하여 500ms 속도 개선</td>
</tr>
<tr>
<td>🧼 URL 메모리 누수 방지</td>
<td><code>URL.createObjectURL()</code> 사용 후, 필요 시 <code>URL.revokeObjectURL()</code>로 해제하여 브라우저 메모리 누수 방지</td>
</tr>
<tr>
<td>🎯 UX &amp; 최적화 고민</td>
<td>도장을 찍는 액션 등으로 랜더링 전환에 병목이 걸리는 경우, 로딩을 추가하여 사용자 경험 개선</td>
</tr>
</tbody></table>
<hr>
<h2 id="🚀-마무리">🚀 마무리</h2>
<p>이번 경험을 통해 PDF를 다루는 데 있어 성능과 유저 경험 사이에서 어떻게 밸런스를 잡아야 할지 많은 고민을 하게 되었습니다. 실제 사용하는 유저 입장에서 빠른 렌더링, 직관적인 UI, 안정적인 다운로드를 구현하는 것이 얼마나 중요한지를 체감했고, 다음 프로젝트에도 잘 적용해보고 싶습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[생존을 위한 사이드 프로젝트 개발기]]></title>
            <link>https://velog.io/@_sky/%EC%83%9D%EC%A1%B4%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@_sky/%EC%83%9D%EC%A1%B4%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Fri, 28 Feb 2025 14:00:51 GMT</pubDate>
            <description><![CDATA[<h3 id="작년-10월-직장인인-내게-매서운-바람이-불어닥쳤다-🌪️">작년 10월, 직장인인 내게 매서운 바람이 불어닥쳤다. 🌪️</h3>
<p>구조조정으로 인해 희망퇴직 대상이 된 것이다. 이후 이력서를 업데이트하고 본격적으로 구직 활동에 나섰지만, 어느덧 4개월이 흘렀다. ⏳ <del>아직은 괜찮지만</del> 시간이 길어질수록 리스크도 커진다는 사실을 실감했다.</p>
<p>상황의 심각성을 깨닫고, 실무 경험을 유지하기 위해 사이드 프로젝트를 시작하기로 했다. 하지만 어떤 주제로 할지부터 고민이 많았다. 🤔 주변에서는 웹에디터나 동영상 편집기처럼 도전적인 프로젝트를 해보라고 조언했다. 하지만 난이도가 높은 프로젝트를 시작하면 과연 끝까지 해낼 수 있을까? 내가 정말 좋아하는 주제가 아니면 지속하기 어렵지 않을까? 이런 고민이 쉽게 결정을 내리지 못하게 했다.</p>
<p>그러던 중 문득 📖 성경 앱이 떠올랐다. 실행에 옮기기 위해 관련 API를 찾아보았고, 몇 가지 후보를 발견했다.</p>
<h4 id="🔍-api-후보-목록">🔍 API 후보 목록</h4>
<ul>
<li><a href="https://m.ibibles.net/index10.htm">https://m.ibibles.net/index10.htm</a></li>
<li><a href="https://scripture.api.bible/">https://scripture.api.bible/</a></li>
<li><a href="https://bible-api.com/">https://bible-api.com/</a></li>
<li><a href="https://getbible.net/docs">https://getbible.net/docs</a></li>
</ul>
<p>내가 원하는 조건은 ✅ 한글 성경을 지원할 것, ✅ 사용에 제약이 없거나 적을 것이었다.
이 조건을 모두 만족하는 GetBible을 최종적으로 선택했다. 🎯</p>
<p>그 다음으로는 API 응답의 key값을 참고하여 변수명을 명확히 했다.
(GPT로 재차 확인도 했다)</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>변수명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성경(Bible)</strong></td>
<td><code>bible</code></td>
</tr>
<tr>
<td><strong>성경 번역본(NRSV, KJV 등)</strong></td>
<td><code>translation</code></td>
</tr>
<tr>
<td><strong>성경 책(창세기, 마태복음 등)</strong></td>
<td><code>book</code></td>
</tr>
<tr>
<td><strong>성경 장(1장, 2장 등)</strong></td>
<td><code>chapter</code></td>
</tr>
<tr>
<td><strong>성경 절(1절, 2절 등)</strong></td>
<td><code>verse</code></td>
</tr>
</tbody></table>
<p>이후에 계속...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SEO가 잘 되는 Next.js 국제화(i18n) 적용하기]]></title>
            <link>https://velog.io/@_sky/SEO%EA%B0%80-%EC%9E%98-%EB%90%98%EB%8A%94-Next.js-%EA%B5%AD%EC%A0%9C%ED%99%94i18n-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_sky/SEO%EA%B0%80-%EC%9E%98-%EB%90%98%EB%8A%94-Next.js-%EA%B5%AD%EC%A0%9C%ED%99%94i18n-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 13 Feb 2025 13:04:14 GMT</pubDate>
            <description><![CDATA[<h1 id="작업-배경">작업 배경</h1>
<p>취업 인터뷰 과정에서 다음의 질문을 받았다.</p>
<blockquote>
<p>Next.js 환경에서 국제화(i18n) 적용 경험이 있습니까?</p>
</blockquote>
<p>하지만 나는 Next.js에서 국제화를 적용해본 경험이 없었다.
인터뷰를 마친 후, 직접 구현해보면서 익혀야겠다는 생각이 들어 관련 라이브러리를 찾아보았다.</p>
<p>국제화 라이브러리로는 react-i18next와 next-i18next가 주로 사용되었고, next-i18next가 Next.js에 최적화되어 있다는 점에서 이를 적용해보기로 했다.</p>
<h1 id="문제-발생">문제 발생</h1>
<p>국제화 적용 이후 정상적으로 출력되었지만, 내 작업 환경에서는 몇 가지 문제가 있었다.</p>
<ul>
<li>새로고침 시 영어(fallback)에서 한글로 깜빡이며 전환됨<ul>
<li>잘못된 언어로 색인되는 등 SEO에 치명적인 문제를 초래할 수 있었다.</li>
</ul>
</li>
<li>HTML lang 속성에 locale을 선언하기 위해 app/[locale]/layout.tsx를 생성하여 html과 body 요소를 이동했더니 오류 발생
<img src="https://velog.velcdn.com/images/_sky/post/57512167-3d49-4e59-87fa-01d2a28f2a84/image.png" alt=""></li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>문제 해결을 위해 Claude.ai에 조언을 구했고, 이 과정에서 next-intl이라는 라이브러리를 알게 되었다.
특히 next-intl은 App Router 환경을 지원한다는 점이 큰 장점이었다. (현재 프로젝트는 Next.js 15 기반이었다.)</p>
<p>이후 next-intl을 적용했는데, <a href="https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing">공식 문서</a>가 매우 친절하게 구성되어 있어 쉽게 구현할 수 있었다.</p>
<h1 id="결과">결과</h1>
<ul>
<li>기능이 정상적으로 동작</li>
<li>네트워크 탭 -&gt; 미리보기에서도 번역된 데이터로 올바르게 표시. 즉, 잘못된 언어로 색인되는 문제가 해결됨 SEO 개선</li>
<li>이제 최소한의 SEO는 된다고 말할 수 있게 됨</li>
</ul>
<p>이제 국제화 적용에 대해 자신 있게 이야기할 수 있을 것 같다. 😃
<img src="https://velog.velcdn.com/images/_sky/post/ca4df250-ed7e-440f-b910-d2b75a6f73c7/image.gif" alt="">
<img src="https://velog.velcdn.com/images/_sky/post/686672b0-92cc-4292-bbb2-31bb5b5f814d/image.png" alt="">
<img src="https://velog.velcdn.com/images/_sky/post/f2d556d2-980c-4a82-a08b-757976550b16/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js Image Warn]]></title>
            <link>https://velog.io/@_sky/Next.js-Image-Warn</link>
            <guid>https://velog.io/@_sky/Next.js-Image-Warn</guid>
            <pubDate>Fri, 15 Nov 2024 04:20:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>아~ 이제 나도 리액트 해봐야지</p>
</blockquote>
<p>하는 마음으로 Next.js를 설치해보니 다음의 경고가 발생합니다.</p>
<p><img src="https://velog.velcdn.com/images/_sky/post/aab5994a-9fab-4fb8-8f6c-fb418df3a463/image.png" alt=""></p>
<p>코드를 추적해보니 다음의 부분에서 발생하네요.</p>
<pre><code class="language-js">&lt;Image
  className=&quot;dark:invert&quot;
  src=&quot;/next.svg&quot;
  alt=&quot;Next.js logo&quot;
  width={180}
  height={38}
  priority
/&gt;</code></pre>
<p>오류 내용으로 찾아보니 다음과 같이 style prop을 추가하라네요. </p>
<pre><code class="language-js">&lt;Image
  className=&quot;dark:invert&quot;
  src=&quot;/next.svg&quot;
  alt=&quot;Next.js logo&quot;
  width={180}
  height={38}
  priority
  style={{ width: 180, height: 38 }} // 추가된 코드
/&gt;</code></pre>
<p>경고는 사라졌지만 반복되는 성격의 코드(width, height)가 있어서 여전히 찜찜합니다. 그래서 좀 더 찾아보았습니다.</p>
<p>랜더링된 이미지 실제 높이는 37인데 38로 잘못기입되어 있었네요.😨
<img src="https://velog.velcdn.com/images/_sky/post/f392ccd5-ecbb-4e01-8c41-01248fb21e9e/image.png" alt=""></p>
<p>다음과 같이 올바른 크기로 수정하니 말끔히 해결됩니다.</p>
<pre><code class="language-js">&lt;Image
  className=&quot;dark:invert&quot;
  src=&quot;/next.svg&quot;
  alt=&quot;Next.js logo&quot;
  width={180}
  height={37} // 38 -&gt; 37
  priority
/&gt;</code></pre>
<p>여기서 한 가지 팁을 드리자면, 이미지 크기를 기입하는 것은 웹 서비스 최적화와 관련이 있습니다. 크기를 브라우저에 미리 알려주므로 Reflow가 방지되기 때문이죠.</p>
<h3 id="결론">결론</h3>
<blockquote>
<p>Next.js Image를 사용시 이미지 크기를 정확히 기입해야 경고가 뜨지 않는다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[시리즈를 작성하게 된 계기]]></title>
            <link>https://velog.io/@_sky/%EC%8B%9C%EB%A6%AC%EC%A6%88%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EA%B2%8C-%EB%90%9C-%EA%B3%84%EA%B8%B0</link>
            <guid>https://velog.io/@_sky/%EC%8B%9C%EB%A6%AC%EC%A6%88%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EA%B2%8C-%EB%90%9C-%EA%B3%84%EA%B8%B0</guid>
            <pubDate>Fri, 15 Nov 2024 03:46:55 GMT</pubDate>
            <description><![CDATA[<h3 id="소개">소개</h3>
<p>제 프론트엔드 경력의 4년은 Nuxt.js(Vue.js) 그리고 2년 4개월은 SvelteKit로 개발을 했습니다. 7년차 프론트엔드 개발자이지만 결국 React.js(이하 &#39;리액트&#39;) 실무 경험이 없습니다.😲</p>
<h3 id="현재의-나">현재의 나</h3>
<p>그렇다고 리액트를 전혀 모르는건 아닙니다.
개발자 신입으로 취업을 준비하던 시절에 리액트 공부(클래스형 컴포넌트, Redux 등...😨)를 했었고, 꾸준히 개발 트렌드를 접하기에 최근의 리액트 생태계도 알고 있습니다.</p>
<h3 id="되돌아보니">되돌아보니</h3>
<p>만약 제가 퇴근 후 여가 시간을 이용하여 사이드 프로젝트로 리액트 경험을 꾸준히 쌓았다면 지금처럼 되지 않을 수도 있었을 겁니다. 되돌아보니 그 시간을 회사 업무에 쏟았었네요.😅</p>
<h3 id="앞으로의-다짐">앞으로의 다짐</h3>
<p>구직 활동을 다시 해보니 리액트 실무 무경험이 아킬레스건이라는 느낌을 받습니다. 그래서 이제는 진짜 리액트를 해야겠다는 다짐을 하였고, 내일의 나를 위해 경험으로 기록하려고 합니다. 누군가에게도 도움이 되었으면 좋겠네요. 앞으로 지켜봐 주세요.😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 최적화를 해보자]]></title>
            <link>https://velog.io/@_sky/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@_sky/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 20 Mar 2024 10:03:11 GMT</pubDate>
            <description><![CDATA[<p>비트맵 이미지(.jpg, .jpeg, .png, .gif)의 품질을 유지하면서 최적화하는 일은 매우 번거롭습니다.
최적화를 하지 않으면 웹 성능 분석시 다음의 메시지를 보게 됩니다.
<img src="https://velog.velcdn.com/images/_sky/post/e7fe3052-7a73-4fe9-93a9-289257a29ee3/image.png" alt=""></p>
<p>다음은 우리가 이미지 한 벌을 최적화하기 위한 코드입니다.
크기별로 확장자별로 총 9벌의 이미지가 필요하며 반복되는 코드량도 상당합니다.🤯</p>
<pre><code class="language-html">&lt;picture&gt;
  &lt;source
    type=&quot;image/avif&quot;
    srcset=&quot;image.avif 375w, image@2x.avif 750w, image@3x.avif 1125w&quot;
  /&gt;
  &lt;source
    type=&quot;image/webp&quot;
    srcset=&quot;image.webp 375w, image@2x.webp 750w, image@3x.webp 1125w&quot;
  /&gt;
  &lt;source
    type=&quot;image/png&quot;
    srcset=&quot;image.png 375w, image@2x.png 750w, image@3x.png 1125w&quot;
  /&gt;
  &lt;img
    src=&quot;image@3x.png&quot;
    width=&quot;375&quot;
    height=&quot;400&quot;
    alt=&quot;&quot;
    loading=&quot;lazy&quot;
    decoding=&quot;async&quot;
  /&gt;
&lt;/picture&gt;</code></pre>
<p>이를 위해, 이미지와 구조를 자동으로 만들어주는 도와주는 🎉<a href="https://github.com/JonasKruckenberg/imagetools/tree/main/packages/vite">vite-imagetools</a>이라는 요긴한 물건이 있습니다.
가장 큰 사이즈 이미지(<a href="mailto:image@3x.png">image@3x.png</a>)로 하나만 준비하면 다음과 같은 데이터를 자동으로 생성해줍니다. 
<img src="https://velog.velcdn.com/images/_sky/post/b94ad735-caa6-4cb0-994d-778adef297e1/image.png" alt=""></p>
<p>이제 이미지 경로를 쏙쏙 붙여 넣기만 하면 됩니다.👍</p>
<pre><code class="language-html">&lt;picture&gt;
  &lt;source
    type=&quot;image/avif&quot;
    srcset=&quot;/@imagetools/bb8fe1264c939f73fef787b6ba1b6eb1171cb920 375w, /@imagetools/c86ac14339cb95b6a322fb78017a0060f4fc93d2 750w, /@imagetools/6f6b3ec05e89d1f91dfc68fa29c8ebae42c6206e 1125w&quot;
  /&gt;
  &lt;source
    type=&quot;image/webp&quot;
    srcset=&quot;/@imagetools/7fbaac7cc148042343ea297902889c4af6bc3d39 375w, /@imagetools/ab44fc472334e6084e42aeaeef7cf6f7387aeda7 750w, /@imagetools/24ed5b16866cd59ce1116af7bd3202c685f36ce9 1125w&quot;
  /&gt;
  &lt;source
    type=&quot;image/png&quot;
    srcset=&quot;/@imagetools/7360b8836cb238449bbcd8045fbedc8ff6b4f9ed 375w, /@imagetools/6ff07977bdce52d2949d7bce663d131a9aef81f7 750w, /@imagetools/0c29b1de7b805fd5adb1b910320563232410b436 1125w&quot;
  /&gt;
  &lt;img
    src=&quot;/@imagetools/0c29b1de7b805fd5adb1b910320563232410b436&quot;
    width=&quot;375&quot;
    height=&quot;400&quot;
    alt=&quot;&quot;
    loading=&quot;lazy&quot;
    decoding=&quot;async&quot;
  /&gt;
&lt;/picture&gt;</code></pre>
<p>그 결과, 이미지 품질은 유지하면서 파일 크기가 대폭 감소(351kB -&gt; 13.7kB)하는 효과를 얻었습니다.
<img src="https://velog.velcdn.com/images/_sky/post/4401f382-6a9b-432c-8baa-56ad32dc22e0/image.png" alt="">
<img src="https://velog.velcdn.com/images/_sky/post/f55d916b-bbcb-4b12-b2a6-b8830f21209c/image.png" alt=""></p>
<p>이로써 이미지는 쉽게 생성했지만, 반복되는 코드는 상당하므로 여전히 번거롭습니다.
저의 경우는 <a href="https://github.com/JonasKruckenberg/imagetools/tree/main/packages/vite">vite-imagetools</a>의 옵션을 적절히 사용하고, 가장 좋아하는 <a href="https://svelte.dev/">스벨트</a>로 컴포넌트를 만들었습니다.
그래서 다음의 코드만으로 위의 최종 코드가 완성되도록 처리하여 사용하고 있습니다.</p>
<pre><code class="language-ts">import MY_IMAGE from &#39;$src/images/my-image.png?1x=375&amp;as-picture&#39;

&lt;Picture meta={MY_IMAGE} /&gt;</code></pre>
<p>자세한 내용은 깃허브에서 <a href="https://github.com/hckang80/svelte-inventory/blob/main/src/stories/Picture/Picture.svelte">코드</a>를 참고해주세요.👋</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[배포 버전 출력하기]]></title>
            <link>https://velog.io/@_sky/%EB%B0%B0%ED%8F%AC-%EB%B2%84%EC%A0%84-%EC%B6%9C%EB%A0%A5%ED%95%98%EA%B8%B0-258v5ofd</link>
            <guid>https://velog.io/@_sky/%EB%B0%B0%ED%8F%AC-%EB%B2%84%EC%A0%84-%EC%B6%9C%EB%A0%A5%ED%95%98%EA%B8%B0-258v5ofd</guid>
            <pubDate>Fri, 29 Dec 2023 13:23:11 GMT</pubDate>
            <description><![CDATA[<p>배포 후 테스트 할 때, 외관상 변경 점이 없는 업데이트의 경우는 반영이 마쳐진 상태인지 장담이 어려울 수 있습니다. 이 때, 배포 버전을 출력함으로써 해결이 가능합니다.</p>
<p>먼저 다음과 같이 package.json에서 버전을 불러와서 전역 변수로 주입합니다.</p>
<pre><code class="language-ts">// vite.config.ts

import { defineConfig } from &#39;vitest/config&#39;
import pkg from &#39;./package.json&#39; assert { type: &#39;json&#39; }

export default defineConfig({
  ...,
  define: {
    __VERSION__: `&quot;${pkg.version}&quot;`,
  },
})</code></pre>
<p>typescript를 쓴다면 전역 변수임을 알려줍니다.</p>
<pre><code class="language-ts">// global.d.ts

declare global {
  ...

  declare const __VERSION__: string
}</code></pre>
<p>이제 설정은 모두 마쳤으니 브라우저 콘솔에 출력해봅시다.</p>
<pre><code class="language-ts">const version = __VERSION__
console.log(`%cVersion: v${version}`, &#39;color: #7066f4&#39;)</code></pre>
]]></description>
        </item>
    </channel>
</rss>