<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>1-blue.log</title>
        <link>https://velog.io/</link>
        <description>블로그 이전했습니다 !! ( https://blog.story-dict.com )</description>
        <lastBuildDate>Sat, 08 Oct 2022 10:44:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 1-blue.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/1-blue" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TOC ( Table Of Contents ) 구현]]></title>
            <link>https://velog.io/@1-blue/TOC-Table-Of-Contents-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/TOC-Table-Of-Contents-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 08 Oct 2022 10:44:43 GMT</pubDate>
            <description><![CDATA[<p>!codepen[1-blue/embed/poVxrYq?default-tab=html%2Cresult]</p>
<blockquote>
<p>해당 프로젝트는 <code>Vanilla JS</code>에 <code>TypeScript</code>만을 사용한 프로젝트입니다.</p>
</blockquote>
<blockquote>
<p><code>TOC</code>란 <code>Table Of Contents</code>로 목차 스크롤을 의미합니다.</p>
</blockquote>
<h2 id="구현-순서">구현 순서</h2>
<ol>
<li><code>h*</code>를 순서대로 모두 탐색</li>
<li>각 <code>h*</code>에 <code>id</code>로 <code>innerText</code>를 준다. ( 여기서 <code>text</code>가 긴 경우를 대비해서 <code>css</code> <code>ellipsis</code> 처리 )</li>
<li>각 <code>h*</code>에 해당하는 <code>a</code>를 만들고 <code>href</code>로 <code>innerText</code>를 준다.</li>
<li>모든 <code>a</code>를 <code>nav</code>에 넣는다.</li>
<li>각 <code>h*</code>에 <code>IntersectionObserver</code>를 적용해서 뷰포트에 들어오는 시점과 스크롤 방향을 이용해서 현재 영역의 <code>h*</code>에 <code>activate</code>라는 클래스를 준다.</li>
</ol>
<p>나머지는 각자 필요한 스타일링을 적용하면 생각보다 쉽게 <code>TOC</code>를 구현할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Intersection Observer API]]></title>
            <link>https://velog.io/@1-blue/Intersection-Observer-API</link>
            <guid>https://velog.io/@1-blue/Intersection-Observer-API</guid>
            <pubDate>Sat, 08 Oct 2022 10:42:13 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-intersection-observer-api">🤔 Intersection Observer API</h2>
<p>루트 요소와 타겟 요소의 교차점을 관찰하는 <code>API</code>입니다.</p>
<blockquote>
<p>루트 요소를 뷰포트로 하고 타겟 요소를 애니메이션을 실행할 태그로 지정한다면 스크롤에 의해서 뷰포트와 태그가 교차하는 경우에 원하는 애니메이션을 실행할 수 있습니다.</p>
</blockquote>
<h2 id="😮-intersection-observer-api-사용법">😮 Intersection Observer API 사용법</h2>
<ul>
<li>옵션값들<ol>
<li><code>root</code>: 루트 요소를 지정하는 속성 ( 생략 시 뷰포트 )</li>
<li><code>rootMargin</code>: 루트 요소의 범위를 확장하는 속성 ( 단위 지정 )</li>
<li><code>threshold</code>: 교차점의 범위를 지정하는 속성 <code>0.0</code>~<code>1.0</code> ( 배열도 가능 )</li>
</ol>
</li>
<li>콜백의 매개변수<ol>
<li><code>boundingClientRect</code>: 타겟 요소의 경계 사각형 정보</li>
<li><code>intersectionRatio</code>: 루트 요소와 타겟 요소의 교차 비율 ( <code>0.0</code>~<code>1.0</code> )</li>
<li><code>intersectionRect</code>: ?</li>
<li><code>rootBounds</code>: 루트 요소의 경계 사각형 정보</li>
<li><code>target</code>: 타겟 요소</li>
<li><code>time</code>: 교차 시간</li>
<li><code>isIntersecting</code>: 타겟 요소와 루트 요소의 <code>threshold</code>만큼의 교차 여부</li>
</ol>
</li>
</ul>
<h2 id="🧐-react에서-intersection-observer-api-예시">🧐 React에서 Intersection Observer API 예시</h2>
<ul>
<li>예시<pre><code class="language-tsx">const IO = new IntersectionObserver(callback, options);
</code></pre>
</li>
</ul>
<p>IO.observe(element);</p>
<pre><code>
+ 사용 예시
```tsx
const onScroll = useCallback((entries: IntersectionObserverEntry[], observer: IntersectionObserver) =&gt; {
  // 특정 태그가 뷰포트의 &quot;threshold&quot;값만큼 보여지면 현재 콜백함수 실행
}, [])

useEffect(() =&gt; {
  if(!elementRef.current) return;

  // 콜백함수와 옵션값 지정
  let observer = new IntersectionObserver(onScroll, { threshold: 0.1 });
  // 특정 요소 감시 시작
  observer.observe(elementRef.current);

  // 감시 종료
  return () =&gt; observer?.disconnect();
}, [elementRef]);</code></pre><ul>
<li>실제 사용 예시 ( <a href="https://darrengwon.tistory.com/1179">참고 포스트</a> )<pre><code class="language-tsx">import { useRef, useEffect, useCallback } from &quot;react&quot;;
</code></pre>
</li>
</ul>
<p>type Direction = &quot;up&quot; | &quot;down&quot; | &quot;left&quot; | &quot;right&quot;;</p>
<p>type Props = {
  direction?: Direction;
  duration?: number;
  delay?: number;
};
type ReturnType = {
  ref: React.MutableRefObject<any>;
  style: {
    opacity: number;
    transform: string | undefined;
  };
};</p>
<p>const useScrollFadeIn = ({
  direction = &quot;up&quot;,
  duration = 1,
  delay = 0,
}: Props): ReturnType =&gt; {
  // 2022/06/14 - 애니메이션을 실행할 태그Ref - by 1-blue
  const elementRef = useRef&lt;HTMLElement | null&gt;(null);</p>
<p>  // 2022/06/14 - 지정한 방향에 따른 트랜지션 반환 - by 1-blue
  const handleDirection = useCallback((dir: Direction) =&gt; {
    switch (dir) {
      case &quot;up&quot;:
        return &quot;translate3d(0, 50%, 0)&quot;;
      case &quot;down&quot;:
        return &quot;translate3d(0, -50%, 0)&quot;;
      case &quot;left&quot;:
        return &quot;translate3d(50%, 0, 0)&quot;;
      case &quot;right&quot;:
        return &quot;translate3d(-50%, 0, 0)&quot;;
      default:
        return &quot;&quot;;
    }
  }, []);</p>
<p>  // 2022/06/14 - IntersectionObserver에 등록할 콜백함수 - by 1-blue
  const onScroll = useCallback(
    ([{ isIntersecting }]: IntersectionObserverEntry[]) =&gt; {
      const { current } = elementRef;
      if (!current) return;</p>
<pre><code>  // 지정한 엘리먼트가 &quot;threshold&quot;만큼을 제외하고 뷰포트에 들어왔다면 실행
  if (isIntersecting) {
    current.style.transitionProperty = &quot;all&quot;;
    current.style.transitionDuration = `${duration}s`;
    current.style.transitionTimingFunction = &quot;cubic-bezier(0, 0, 0.2, 1)&quot;;
    current.style.transitionDelay = `${delay}s`;
    current.style.opacity = &quot;1&quot;;
    current.style.transform = &quot;translate3d(0, 0, 0)&quot;;
  }
  // 지정한 엘리먼트가 &quot;threshold&quot;만큼을 제외하고 뷰포트 밖에 존재한다면 실행
  // 아래 부분을 넣어주면 교차범위 이하일 경우 애니메이션이 반대로 실행됨
  else {
    current.style.transitionProperty = &quot;all&quot;;
    current.style.transitionDuration = `${duration}s`;
    current.style.transitionTimingFunction = &quot;cubic-bezier(0, 0, 0.2, 1)&quot;;
    current.style.transitionDelay = `${delay}s`;
    current.style.opacity = &quot;0&quot;;
    current.style.transform = handleDirection(direction);
  }
},
[delay, duration, handleDirection, direction]</code></pre><p>  );</p>
<p>  // 2022/06/14 - IntersectionObserver 등록 - by 1-blue
  useEffect(() =&gt; {
    if (!elementRef.current) return;</p>
<pre><code>// 좌우로 움직이는 방식을 선택했을 경우 주의해야 할 점이 전체 크기의 50%를 이동해서 뷰포트 밖으로 많이 나가버리면
// 애니메이션 실행이 안 될 수 있다.
// 왜냐하면 이미 뷰포트 밖으로 지정한 교차범위 이상을 나가버렸기 때문에 스크롤과 관계없이 실행되지 않음
// 그럴 경우 &quot;threshold&quot; 값을 줄이거나, &quot;rootMargin&quot;을 늘리면 된다.
let observer = new IntersectionObserver(onScroll, { threshold: 0.1, rootMargin: &quot;20px&quot; });
observer.observe(elementRef.current);

return () =&gt; observer?.disconnect();</code></pre><p>  }, [onScroll]);</p>
<p>  return {
    ref: elementRef,
    style: { opacity: 0, transform: handleDirection(direction) },
  };
};</p>
<p>export default useScrollFadeIn;</p>
<p>// &lt;article {...useScrollFadeIn({ direction: &quot;right&quot;, duration: 1.6 })}&gt; 같은 형식으로 사용
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[움직이는 배경화면 구현]]></title>
            <link>https://velog.io/@1-blue/%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EB%B0%B0%EA%B2%BD%ED%99%94%EB%A9%B4-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EB%B0%B0%EA%B2%BD%ED%99%94%EB%A9%B4-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 04 Oct 2022 13:20:50 GMT</pubDate>
            <description><![CDATA[<p>!codepen[1-blue/embed/ZEoRmjV?default-tab=html%2Cresult]</p>
<blockquote>
<p>해당 프로젝트는 <code>Vanilla JS</code>에 <code>TypeScript</code>만을 사용한 프로젝트입니다.</p>
</blockquote>
<blockquote>
<p>해당 포스트는 <a href="https://gurtn.tistory.com/164">https://gurtn.tistory.com/164</a> 여기의 게시글을 참고로 만들었습니다.</p>
</blockquote>
<h2 id="구현-원리">구현 원리</h2>
<blockquote>
<p><a href="https://velog.io/@1-blue/svg-%EC%A0%95%EB%A6%AC">svg에 대해서는 여기를</a> 참고해주세요</p>
</blockquote>
<ol>
<li>가로/세로중에서 더 큰 값을 구함</li>
<li>이전에 구했던 값의 1.5배의 사이즈로 <code>background</code>의 크기를 늘려줌</li>
<li><code>&lt;svg&gt;</code>의 내부에 랜덤한 크기의 랜덤한 위치에 랜덤한 개수의 <code>&lt;circle&gt;</code>을 그려줌</li>
<li><code>animation</code> <code>rotate</code>를 이용해서 <code>background</code>를 회전시킴</li>
</ol>
<pre><code class="language-ts">// 참고 사이트 ( https://gurtn.tistory.com/164 )
// 백그라운드 세팅 ( &quot;&lt;svg&gt;&quot;에 랜덤으로 &quot;&lt;circle&gt;&quot;을 그려넣음 )
const backgroundSetting = () =&gt; {
  const $background = document.querySelector(&quot;#background&quot;);
  const $sky = document.querySelector(&quot;#sky&quot;);

  if (!($background instanceof HTMLElement)) return;
  if (!($sky instanceof SVGElement)) return;

  // 브라우저의 가로/세로 중 가장 큰 크기
  const maxSize = Math.max(window.innerWidth, window.innerHeight) * 1.5;
  // 랜덤한 X/Y 위치 값
  const getRandomX = () =&gt; Math.random() * maxSize + &quot;&quot;;
  const getRandomY = () =&gt; Math.random() * maxSize + &quot;&quot;;
  // 랜덤한 크기 ( &quot;&lt;circle&gt;&quot;는 반지름이 크기)
  const randomRadius = () =&gt; Math.random() * 0.7 + 0.6 + &quot;&quot;;
  // 생성할 별 개수
  const starCount = Math.floor(maxSize / 2);

  // &quot;background&quot;와 &quot;sky&quot;크기 재정의
  // 만약 2000px/1200px의 사이즈에서 실행한다면 2000px/2000px의 랜덤한 위치에 별이 찍힘
  // 하지만 rotate를 해주기 때문에 돌리다 보면 빈공간이 생기게 됨
  // ( 정사각형을 45% 돌리면 모서리와 모서리가 연결되는 부분은 튀어나오고, 그 외의 부분은 부족하게 됨 )
  // 따라서 본래 크기보다 1.5배 더 크게 만들어서 돌려도 빈공간이 생기지 않도록 작성한 것
  $background.style.width = `${maxSize}px`;
  $background.style.height = `${maxSize}px`;
  $sky.style.width = `${maxSize}px`;
  $sky.style.height = `${maxSize}px`;

  // 랜덤한 위치에 랜덤한 크기로 랜덤한 개수의 별 생성 ( 사실 매우 작은 원 )
  const svgCircleList = new Array(starCount).fill(null).map(() =&gt; {
    const $$circle = document.createElementNS(
      &quot;http://www.w3.org/2000/svg&quot;,
      &quot;circle&quot;
    );

    $$circle.classList.add(&quot;star&quot;);
    // 랜덤 X/Y 위치 / 반지름
    $$circle.setAttributeNS(null, &quot;cx&quot;, getRandomX());
    $$circle.setAttributeNS(null, &quot;cy&quot;, getRandomY());
    $$circle.setAttributeNS(null, &quot;r&quot;, randomRadius());

    return $$circle;
  });

  $sky.innerHTML = &quot;&quot;;
  $sky.append(...svgCircleList);
};

(() =&gt; {
  window.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
    backgroundSetting();
  });
  window.addEventListener(&quot;resize&quot;, () =&gt; {
    cardHeightSetting();
  });
})()</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[image carousel 구현]]></title>
            <link>https://velog.io/@1-blue/image-carousel-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/image-carousel-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 04 Oct 2022 12:30:08 GMT</pubDate>
            <description><![CDATA[<p>!codepen[1-blue/embed/zYjamKE?default-tab=html%2Cresult]</p>
<blockquote>
<p>해당 프로젝트는 <code>Vanilla JS</code>에 <code>TypeScript</code>만을 사용한 프로젝트입니다.</p>
</blockquote>
<h2 id="구현-목적">구현 목적</h2>
<p>이전에 <code>image carousel</code>를 사용할 때는 <code>react-slick</code>같은 라이브러리를 이용해서 쉽게 구현했습니다.
어차피 앞으로 사용할 때는 라이브러리에서 제공해주는 기능을 쓸거니까 한번쯤은 직접 구현해보면서 동작 원리를 고민하고 이해해보는게 좋지 않을까 생각해서 구현하게 되었습니다.</p>
<h2 id="세팅">세팅</h2>
<p>구현 방법을 설명하기 앞서 사용하는 용어를 먼저 정리하겠습니다.</p>
<ol>
<li><code>.img-window</code>: 이미지를 보여줄 엘리먼트</li>
<li><code>.img-container</code>: 이미지들을 갖는 부모 엘리먼트</li>
<li><code>.img</code>: 이미지를 백그라운드로 갖는 엘리먼트</li>
</ol>
<ul>
<li><code>html</code><pre><code class="language-html">&lt;div class=&quot;img-window&quot;&gt;
&lt;div class=&quot;img-container&quot;&gt;
  &lt;figure class=&quot;img&quot; style=&quot;background-image: url(&#39;&#39;)&quot;&gt;
    &lt;img src=&quot;&quot; alt=&quot;&quot; hidden /&gt;
  &lt;/figure&gt;
  &lt;figure class=&quot;img&quot; style=&quot;background-image: url(&#39;&#39;)&quot;&gt;
    &lt;img src=&quot;&quot; alt=&quot;&quot; hidden /&gt;
  &lt;/figure&gt;
  &lt;figure class=&quot;img&quot; style=&quot;background-image: url(&#39;&#39;)&quot;&gt;
    &lt;img src=&quot;&quot; alt=&quot;&quot; hidden /&gt;
  &lt;/figure&gt;
  &lt;!-- ...이미지들 --&gt;
&lt;/div&gt;
&lt;/div&gt;
</code></pre>
</li>
</ul>
<div class="btn-container">
  <button type="button" data-direction="1">이전 이미지</button>
  <button type="button" data-direction="-1">다음 이미지</button>
</div>
```

<ul>
<li><p><code>css</code></p>
<pre><code class="language-css">.img-window {
position: relative;
width: 100%;
min-width: 300px;
max-width: 550px;
padding-top: 100%;
margin: 0 auto 0.6em;

border-radius: 0.1em;

overflow: hidden;
}
.img-window .img-container {
position: absolute;
inset: 0;
margin: auto;

display: flex;

background-color: #000;
}
.img-window .img-container .img {
width: 100%;
height: 100%;

display: inline-block;

background-size: contain;
background-position: center;
background-repeat: no-repeat;
}</code></pre>
</li>
</ul>
<h2 id="구현-방법">구현 방법</h2>
<ol>
<li><code>.img-container</code>의 내부 이미지 개수 * 100%를 <code>width</code>로 <code>.img-container</code>의 크기를 등록 ( 이미지들을 한줄로 배치 )</li>
<li><code>insertBefore()</code>를 이용해서 첫번째 이미지를 두번째 위치로 이동</li>
<li><code>translateX</code>를 이용해서 <code>-100%</code> 이동해서 두번째 위치부터 보여주도록 설정</li>
<li>좌/우측 이동 버튼을 클릭하면 방향에 따라서 <code>translateX</code>를 <code>100%</code>, <code>-100%</code>로 이동</li>
<li>이동하고 나면 ( <code>transitionend</code> 이벤트 사용 ) 이동 방향에 따라서 이전 이미지를 마지막으로 / 마지막 이미지를 처음으로 이동 ( 좌/우측으로 무한으로 이동하기 위함 )</li>
</ol>
<blockquote>
<p>자세한 <code>TS</code>코드는 <code>codepen</code>를 참고해주세요.</p>
</blockquote>
<h2 id="후기">후기</h2>
<p>처음에는 어떤 방식으로 구현해야할지 막막했습니다.
구글링으로 찾아본 다음에는 원리는 알겠는데 이걸 어떻게 코드로 표현하지? 라는 생각을 하면서 일단 적으면서 생각해보자는 마음으로 시작했습니다.
이미지 배치/이동/순서 변경 등 모든게 처음에는 원하는 대로 안됐지만 계속 시도하면서 완성했습니다.</p>
<p>이전에는 동작 원리는 전혀 모른채 그냥 라이브러리에서 제공하는 방법만 사용해서 동작시켰는데 원리를 생각하면서 직접 만들어 보는게 좋은 경험이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Accordion menu 구현]]></title>
            <link>https://velog.io/@1-blue/Accordion-menu-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/Accordion-menu-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 04 Oct 2022 11:16:53 GMT</pubDate>
            <description><![CDATA[<p>!codepen[1-blue/embed/LYmrXGK?default-tab=html%2Cresult]</p>
<blockquote>
<p>해당 프로젝트는 <code>Vanilla JS</code>에 <code>TypeScript</code>만을 사용한 프로젝트입니다.</p>
</blockquote>
<p>기본적으로는 닫아놨다가 누르면 아래로 열리고 추가 정보를 볼 수 있는 기능을 가진 <code>Accodian menu</code>를 만들어봤습니다.
예전에 <code>HTML5</code>를 공부하다가 보기만 하고 지나갔던 <code>&lt;details&gt;</code>와 <code>&lt;summary&gt;</code>가 생각나서 관련해서 구글링해보면서 방법을 찾고 적용해봤습니다.</p>
<h2 id="애니메이션-적용-전">애니메이션 적용 전</h2>
<p>아래 <code>gif</code>는 <code>&lt;details&gt;</code>/<code>&lt;summary&gt;</code>에 스타일링만 적용한 결과물입니다.
... 애니메이션 없는 gif 추가 
<code>transition</code>을 적용한 것처럼 서서히 열리고 서서히 닫히는 기능을 원했지만 원하는대로 동작하지 않아서 애니메이션이 적용되는 방법을 찾아봤습니다.</p>
<h2 id="🤔-resizeobserver-사용">🤔 ResizeObserver 사용</h2>
<p>다른 방법을 찾다가 <code>ResizeObserver</code>의 존재를 알게 되었습니다.
<code>ResizeObserver</code>는 특정 엘리먼트의 크기 변화를 감지합니다.
해당 기능과 <code>&lt;details&gt;</code>를 클릭하면 <code>height</code>가 늘어나는 것을 이용해서 <code>transition</code>을 적용한 것처럼 효과를 줄 수 있습니다.</p>
<ul>
<li>ResizeObserver 사용법<pre><code class="language-ts">const RO = new ResizeObserver(callback)
RO.observe(element)
</code></pre>
</li>
</ul>
<p>/*</p>
<ul>
<li>&quot;callback&quot;의 인자는 &quot;ResizeObserverEntry[]&quot;값이 들어옵니다.</li>
<li>관찰하는 모든 엘리먼트의 특정 정보를 담은 &quot;ResizeObserverEntry&quot;의 배열입니다.
*/<pre><code></code></pre></li>
</ul>
<ul>
<li><code>ResizeObserverEntry</code>의 속성들 ( 자세한 정보는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry">mdn</a> )<ol>
<li><code>target</code>: 관찰 대상입니다.</li>
<li><code>contentRect</code>: 관찰 대상의 사각형 정보입니다.</li>
<li>나머지는 사용 안해봐서 생략...</li>
</ol>
</li>
</ul>
<h2 id="🧐-실제-적용-예시">🧐 실제 적용 예시</h2>
<p>아래의 예시를 보면서 어떻게 적용했는지 설명하겠습니다. ( <a href="https://css-tricks.com/how-to-animate-the-details-element/">참고한 포스트</a> )</p>
<pre><code class="language-ts">export const setDetailsHeight = (wrapper: HTMLElement) =&gt; {
  // 특정 &lt;details&gt;의 닫힘과 열림의 &quot;height&quot;값을 미리 기록함 ( &quot;--expanded&quot;와 &quot;--collapsed&quot;에 기록 )
  // &quot;width&quot;를 &quot;dataset&quot;에 넣어두는 이유는 처음 실행인지 아닌지 즉, 닫힘과 열림의 크기를 미리 기록하는 것인지 판단을 위해서
  const setHeight = (detail: HTMLDetailsElement, open = false) =&gt; {
    detail.open = open;
    const { width, height } = detail.getBoundingClientRect();
    detail.dataset.width = width + &quot;&quot;;
    detail.style.setProperty(
      open ? `--expanded` : `--collapsed`,
      `${height}px`
    );
  };

  // ResizeObserver 객체 생성
  const RO = new ResizeObserver((entries) =&gt;
    entries.forEach((entry) =&gt; {
      const detail = entry.target as HTMLDetailsElement;
      const width = detail.dataset.width ? +detail.dataset.width : -1;

      // 처음 실행이라면 즉, 클릭에 의한 실행이 아니라면 실행
      // 단, 여기서 주의해야 할 점이 &quot;entry.contentRect.width&quot;값에는 padding이나 border를 포함하지 않은 값임
      // 따라서 항상 width값이 맞지 않아 if문 코드를 실행해서 &lt;details&gt;가 안열릴 수 있음
      // 그러므로 details에 직접적으로 padding이나 border를 안주는 것이 좋고 만약 값이 준다면 수치를 계산해서 직접 더해줘야 정상적으로 작동함
      if (width !== entry.contentRect.width) {
        detail.removeAttribute(&quot;style&quot;);
        // 닫힘 크기 기록
        setHeight(detail);
        // 열림 크기 기록
        setHeight(detail, true);
        // 원래 상태로 되돌림
        detail.open = false;
      }
    })
  );

  // wrapper 내부의 모든 &lt;details&gt;을 찾아서 관찰 대상으로 지정
  const details = wrapper.querySelectorAll(&quot;details&quot;);
  details.forEach((detail) =&gt; RO.observe(detail));
};

// setDetailsHeight()에 &lt;details&gt;들을 포함하는 가장 상위 태그를 인자로 넣어주면 됨
// 물론 document를 바로 넣어줘도 되지만 전체 탐색보다는 &lt;details&gt;를 사용하는 제일 상위태그를 넣어주는 게 더 효율적이라고 생각함</code></pre>
<ul>
<li><code>css</code> 적용하기<pre><code class="language-css">details {
/* 닫힌 &quot;height&quot; 값을 적용 */
height: var(--collapsed);
overflow: hidden;
/* 닫힌 값과 열린 값이 다르기 때문에 &quot;transition&quot;이 적용됩니다. */
transition: height 300ms cubic-bezier(0.4, 0.01, 0.165, 0.99);
}
details[open] {
/* 열린 &quot;height&quot; 값을 적용 */
height: var(--expanded);
}</code></pre>
</li>
</ul>
<h2 id="😮-resizeobserver-적용-결과물">😮 ResizeObserver 적용 결과물</h2>
<p>... 결과 gif 넣기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Scroll Animation 구현]]></title>
            <link>https://velog.io/@1-blue/Scroll-Animation-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/Scroll-Animation-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 04 Oct 2022 11:04:37 GMT</pubDate>
            <description><![CDATA[<p>!codepen[1-blue/embed/ZEoRmXq?default-tab=html%2Cresult]</p>
<blockquote>
<p>해당 프로젝트는 <code>Vanilla JS</code>에 <code>TypeScript</code>만을 사용한 프로젝트입니다.</p>
</blockquote>
<h2 id="코드-예시">코드 예시</h2>
<p>직접 스크롤에 의한 애니메이션을 적용하기 위해서는 생각보다 많은 처리가 필요합니다.
아래 코드는 많긴 하지만 최소한으로 작성한 코드입니다.</p>
<ul>
<li><p>코드의 실행 흐름</p>
<ol>
<li>스크롤 이벤트를 실행할 영역의 높이를 정한다.</li>
<li>이벤트를 발생할 엘리먼트/시작지점/끝지점, 시작/끝 값을 정한다.</li>
<li>스크롤 이벤트로 영역에서 스크롤된 비율을 구해서 시작지점/끝지점에 해당하는지 판단하고 그에 맞는 시작/끝 값을 <code>inline-style</code>로 넣는다.</li>
</ol>
</li>
<li><p><code>index.ts</code></p>
<pre><code class="language-ts">/**
* 현재 애니메이션이 실행될 비율 구하기
* @range 애니메이션 실행 스크롤의 범위 ( 0 ~ 1 )
* @value 애니메이션 실행 값의 범위
* @currentSceneScrollHeight 현재 &quot;scene&quot;에서 스크롤한 크기
* @currentSceneHeight 현재 &quot;scene&quot;의 전체 높이
* @returns 현재 애니메이션 실행 값
*/
const getTimelineValue = ({
range,
value,
currentSceneScrollHeight,
currentSceneHeight,
}: TimelineValueProps) =&gt; {
// 애니메이션 시작지점/끝지점/지속영역
const animationStartHeight = currentSceneHeight * range.start;
const animationEndHeight = currentSceneHeight * range.end;
const animationHeight = animationEndHeight - animationStartHeight;

// 애니메이션 실행 영역에 들어온 경우
if (
  animationStartHeight &lt;= currentSceneScrollHeight &amp;&amp;
  animationEndHeight &gt;= currentSceneScrollHeight
) {
  // &quot;((currentSceneScrollHeight - animationStartHeight) / animationHeight)&quot; =&gt; 현재 &quot;scene&quot;에서 스크롤된 비율
  // &quot;* (value.end - value.start) + value.start&quot;는 지정된 범위로 변환시켜주는 연산 ( 0~1사이의 값(v)이 10~100(value.start, value.end) 사이로 변해야 한다면 &quot;v * ( start - end ) + end&quot;를 해주면 됨 )
  return (
    ((currentSceneScrollHeight - animationStartHeight) / animationHeight) *
      (value.end - value.start) +
    value.start
  );
}
// 애니메이션 실행 영역 이전인 경우
else if (animationStartHeight &gt; currentSceneScrollHeight) {
  return value.start;
}
// 애니메이션 실행 영역 이후인 경우
else {
  return value.end;
}
};
</code></pre>
</li>
</ul>
<p>(() =&gt; {
  // 모든 &quot;scene&quot;을 찾고 타입 확정
  const scenes = [...document.querySelectorAll(&quot;#scene&quot;)].filter(
    (scene): scene is HTMLElement =&gt; scene instanceof HTMLElement
  );
  // &quot;scene0&quot;의 &quot;message&quot;들 ( 스크롤에 의한 애니메이션을 지정할 &quot;element&quot;들 )
  const messagesOfScene0 = [
    ...document.querySelectorAll(&quot;.scene-a .message&quot;),
  ].filter((message): message is HTMLElement =&gt; message instanceof HTMLElement);</p>
<p>  // 각 &quot;scene&quot;에 대한 정보
  const sceneInfos = [
    // 첫 번째 &quot;scene&quot;의 정보
    {
      // &quot;레이아웃에 영향을 미치지 않는 배치&quot;를 의미
      type: &quot;fixed&quot;,
      // &quot;뷰포트 높이의 3배를 가짐&quot;을 의미
      heightNumber: 3,
    },
    // 두 번째 &quot;scene&quot;의 정보
    {
      // &quot;레이아웃에 영향을 미치는 자연스러운 배치&quot;를 의미
      type: &quot;nomal&quot;,
      heightNumber: 1,
    },</p>
<pre><code>// ...</code></pre><p>  ] as const;</p>
<p>  // 각 &quot;scene&quot;의 &quot;animation&quot;에 대한 정보
  const animationInfos = [
    // 첫 번째 &quot;scene&quot;에서 실행할 애니메이션
    {
      // 애니메이션을 실행할 타겟
      messageA: {
        // &quot;opacity&quot;
        opacityIn: {
          // 시작/끝 위치 ( 해당 &quot;scene&quot;의 스크롤 0.1 ~ 0.3 사이에 실행한다는 의미 )
          range: { start: 0.1, end: 0.3 },
          // 시작/끝 값 ( 값이 0 ~ 1사이로 변한다는 의미 )
          value: { start: 0, end: 1 },
        },
        // 위 값의 예시로 설명하자면 &quot;scene&quot;의 &quot;height&quot;가 1000px이라고 가정했을 때 100px에 위치할땐 0, 200px일땐 0.5, 300px일땐 1의 값을 얻는다는 의미, 그 값은 &quot;getTimelineValue()&quot;을 이용해서 얻음
        opacityOut: {
          range: { start: 0.7, end: 1 },
          value: { start: 1, end: 0 },
        },
        // &quot;translateY&quot;
        translateYIn: {
          range: { start: 0.1, end: 0.3 },
          value: { start: 60, end: 0 },
        },
        translateYOut: {
          range: { start: 0.7, end: 1 },
          value: { start: 0, end: -60 },
        },
      },
      // ... 같은 &quot;scene&quot;의 다른 요소들의 애니메이션 설정값
    } as const,
    // ... 다른 &quot;scene&quot;의 특정 요소들 애니메이션 설정값
  ] as const;</p>
<p>  // 현재 위치한 &quot;scene&quot; / 이전 &quot;scene&quot;들의 &quot;height&quot;의 합
  let currentScene = 0;
  let prevSceneHeight = 0;</p>
<p>  // 초기 세팅
  const init = () =&gt; {
    // 각 &quot;scene&quot;의 높이 설정하기
    sceneInfos.forEach((sceneInfo, i) =&gt; {
      if (sceneInfo.type === &quot;nomal&quot;) return;</p>
<pre><code>  // 각 &quot;scene&quot;의 높이를 &quot;현재 브라우저의 높이 * heightNumber&quot;로 지정
  scenes[i].style.height = innerHeight * sceneInfo.heightNumber + &quot;px&quot;;
});</code></pre><p>  };</p>
<p>  // 현재 어느 &quot;scene&quot;인지 구하는 이벤트 함수 ( + &quot;prevSceneHeight&quot;도 구함 )
  const onScrollEvent = () =&gt; {
    prevSceneHeight = 0;
    // 이전 &quot;scene&quot;의 높이의 합
    for (let i = 0; i &lt; currentScene; i++) {
      prevSceneHeight += scenes[i].clientHeight;
    }</p>
<pre><code>// 다음 &quot;scene&quot;으로 넘어가면 실행 ( 바뀌는 시점에 마지막 애니메이션이 적용 안되기 때문에 바뀌기전 마지막에 애니메이션 적용 )
if (scrollY &gt; prevSceneHeight + scenes[currentScene].clientHeight) {
  currentScene++;
  playAnimation();
}
if (scrollY &lt; prevSceneHeight) {
  currentScene--;
  playAnimation();
}</code></pre><p>  };</p>
<p>  // 애니메이션 실행
  const playAnimation = () =&gt; {
    // 현재 &quot;scene&quot;의 정보들
    const scene = scenes[currentScene];</p>
<pre><code>// 애니메이션의 값을 계산할 때 필요한 현재 &quot;scene&quot;의 정보 ( 현재 &quot;scene&quot;에서 스크롤된 높이 크기 / 현재 &quot;scene&quot;의 전체 높이 / 현재 &quot;scene&quot;에서 스크롤된 높이의 비율 )
// 현재 &quot;scene&quot;에서 스크롤된 높이 크기 ( 전체 스크롤된 높이 - 이전 &quot;scene&quot;들의 높이 )
const currentSceneScrollHeight = scrollY - prevSceneHeight;
// 현재 &quot;scene&quot;의 전체 높이
// &quot;innerHeight&quot;즉 브라우저 높이를 빼준 이유는 &quot;scrollY&quot;가 0일 때도 &quot;innerHeight&quot;만큼 영역을 차지하기 때문... 글로 설명하기 너무 애매해서 &quot;innerHeight&quot;를 빼준 값과 안빼준 값을 비교해서 실행해보면 이해할 수 있음
const currentSceneHeight = scene.clientHeight - innerHeight;
// 현재 &quot;scene&quot;에서 스크롤된 비율 ( 현재 &quot;scene&quot;에서 스크롤한 높이 / 현재 &quot;scene&quot;의 전체 높이 )
const ratio = currentSceneScrollHeight / currentSceneHeight;

// 값을 넣을 변수
let opacity = 0;

// 첫 번째 &quot;scene&quot;
if (currentScene === 0) {
  const animationInfo = animationInfos[currentScene];

  // &quot;messageA&quot;의 &quot;opacity&quot; 애니메이션
  if (animationInfo.messageA.opacityIn.range.end &gt; ratio) {
    opacity = getTimelineValue({
      ...animationInfo.messageA.opacityIn,
      currentSceneScrollHeight,
      currentSceneHeight,
    });
  } else {
    opacity = getTimelineValue({
      ...animationInfo.messageA.opacityOut,
      currentSceneScrollHeight,
      currentSceneHeight,
    });
  }
  messagesOfScene0[0].style.opacity = `${opacity}`;

  // ... 다른 애니메이션 설정들
}</code></pre><p>  };</p>
<p>  window.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
    init();
  });
  window.addEventListener(&quot;scroll&quot;, () =&gt; {
    onScrollEvent();
    playAnimation();
  });
})()</p>
<p>```</p>
<h2 id="후기">후기</h2>
<p>평소에 다른분들의 포트폴리오 웹사이트를 구경해보면 대부분 스크롤의 비율에 의해서 애니메이션이 실행되는 구조로 많이 구현한걸 봤었습니다. 저도 다음에 포트폴리오 웹사이트를 만들때는 저런 방식으로 구현해야겠다고 항상 생각했어서 이번에 직접 구현하게 되었습니다.</p>
<p>기존에 스크롤 이벤트를 사용한적은 있지만 이렇게 특정 지점에서 스크롤의 비율에 의해서 애니메이션의 실행의 비율이 결정되는 것을 해본적은 없습니다. 만약 해봤어도 특정 라이브러리를 이용해서 쉽게 구현했을텐데 <code>JavaScript</code>만을 이용해서 바닥부터 구현하려니 헷갈리고 어려운 부분이 정말 많았습니다.</p>
<p>이렇게 시간이 많이 소모되고 읽기도 힘든 코드를 작성해보는게 도움이 될지는 정확하게 알 수는 없지만 그래도 직접 원리를 이해하고 구현해봤다는 점이 언젠간은 도움이 되지 않을까 생각합니다.</p>
<ul>
<li><a href="https://www.inflearn.com/course/%EC%95%A0%ED%94%8C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%9D%B8%ED%84%B0%EB%9E%99%EC%85%98-%ED%81%B4%EB%A1%A0/dashboard">참고한 강의 - 인프런 1분 코딩</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[<svg> 사용]]></title>
            <link>https://velog.io/@1-blue/svg-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@1-blue/svg-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 20 Sep 2022 11:58:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스트는 <a href="https://www.inflearn.com/course/mastering-svg/dashboard">인프런 - 1분코딩 SVG 마스터</a>강의를 들으면서 정리한 포스트입니다.</p>
</blockquote>
<blockquote>
<p>해당 포스트에서 작성한 속성은 대부분 <code>css</code>에서 적용할 수 있습니다.</p>
</blockquote>
<h2 id="svg">SVG</h2>
<p>확장 가능한 벡터 그래픽</p>
<p>벡터 그래픽: 점과 점 사이의 계산을 이용한 그림</p>
<p>이미지와의 차이점은 픽셀을 사용하는 것이 아닌 계산을 이용해서 그리는 것이므로 크기에 따라서 화질이 변하지 않습니다.</p>
<p>이미지의 경우 화질이 높을수록 픽셀의 수가 많아집니다. 따라서 용량이 고화질일수록 커지게 됩니다.
하지만 <code>SVG</code>는 크기에 상관없이 현재 크기에 맞게 계산을 통해서 그리는 것이므로 크기에 영향을 받지 않고 항상 좋은 화질을 유지합니다.
하지만 계산이 필요하기 때문에 복잡할수록 용량이 커지게 되기 때문에 간단한 아이콘에 주로 사용합니다.</p>
<p>계산을 통해 그려지기 때문에 <code>html</code> 속성을 변경시키면 그것에 맞게 색상이나 모양을 쉽게 바꿀 수 있습니다.</p>
<h2 id="svg-사용-방법">SVG 사용 방법</h2>
<ol>
<li>css <code>bakcground-image: url(&quot;...경로&quot;)</code></li>
<li>inline <code>&lt;svg&gt;&lt;/svg&gt;</code></li>
<li><code>&lt;object data=&quot;...경로&quot; type=&quot;image/svg+xml&quot;&gt;&lt;/object&gt;</code></li>
<li><code>&lt;img src=&quot;경로&quot; /&gt;</code></li>
</ol>
<h2 id="svg-속성">SVG 속성</h2>
<p><code>viewBox</code>: 실제 크기와 관계없이 <code>&lt;svg&gt;</code> 내부에서 보여줄 크기를 지정</p>
<p>아래 예시처럼 선언되어 있다면 실제 <code>&lt;svg&gt;</code>의 크기는 가로/세로가 <code>400px</code>입니다.
하지만 <code>&lt;svg&gt;</code> 내부에서 그려지는 크기는 가로/세로가 <code>200px</code>이라고 생각하고 그려지기 때문에 <code>&lt;rect&gt;</code>는 <code>50px</code>의 크기로 그려지게 됩니다.</p>
<p>실제 크기는 <code>400px</code>, <code>&lt;svg&gt;</code>의 내부에서 그려지는 크기는 <code>200px</code>
<code>400px</code>의 도화지에 <code>100px</code>짜리 사각형을 그리라고 했으나, 실제로는 <code>200px</code>의 도화지이기 때문에 <code>50px</code>로 축소해서 그립니다.</p>
<pre><code class="language-html">&lt;svg
  width=&quot;400&quot;
  height=&quot;400&quot;
  viewBox=&quot;0 0 200 200&quot;
&gt;
  &lt;rect x=&quot;0&quot; y=&quot;0&quot; width=&quot;100&quot; height=&quot;100&quot; /&gt;
&lt;/svg&gt;</code></pre>
<h2 id="css-js-적용하기">css, js 적용하기</h2>
<p><code>&lt;defs&gt;</code>: 나중에 사용할 그래픽 객체, css, js들을 작성하는 공간</p>
<p><code>&lt;defs&gt;</code> 내부에서의 작성 방식은 <code>html</code>에서 <code>inline</code>으로 작성하던 방식대로 작성하면 됩니다. ( <code>&lt;style&gt;</code>, <code>&lt;script&gt;</code> )</p>
<pre><code class="language-html">&lt;svg
  id=&quot;face&quot;
  xmlns=&quot;http://www.w3.org/2000/svg&quot;
  xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot;
  x=&quot;0px&quot;
  y=&quot;0px&quot;
  viewBox=&quot;0 0 40 40&quot;
&gt;
  &lt;defs&gt;
    &lt;style&gt;
      @keyframes eye-ani {
        100% {
          transform: scaleY(0.2);
        }
      }

      #eye.start {
        transform-origin: 50%;
        animation: eye-ani 0.5s infinite alternate;
      }
    &lt;/style&gt;
    &lt;script&gt;
      window.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {
        const $face = document.querySelector(&quot;#face&quot;);

        $face.addEventListener(&quot;click&quot;, () =&gt; {
          const $eye = document.querySelector(&quot;#eye&quot;);
          $eye.classList.toggle(&quot;start&quot;);
        });
      });
    &lt;/script&gt;
  &lt;/defs&gt;

    &lt;path id=&quot;contour&quot; d=&quot;M20,8c6.6,0,12,5.4,12,12s-5.4,12-12,12S8,26.6,8,20S13.4,8,20,8 M20,6C12.3,6,6,12.3,6,20s6.3,14,14,14s14-6.3,14-14
            S27.7,6,20,6L20,6z&quot;/&gt;
    &lt;g id=&quot;eye&quot;&gt;
        &lt;path d=&quot;M14,18c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S15.1,18,14,18L14,18z&quot;/&gt;
        &lt;path d=&quot;M26,18c-1.1,0-2,0.9-2,2s0.9,2,2,2s2-0.9,2-2S27.1,18,26,18L26,18z&quot;/&gt;
    &lt;/g&gt;
    &lt;path id=&quot;mouth&quot; d=&quot;M28.3,24.6c-1.7,3-4.9,4.9-8.3,4.9s-6.6-1.9-8.3-4.9l-0.9,0.5c1.9,3.3,5.4,5.4,9.2,5.4c3.8,0,7.3-2.1,9.2-5.4
        L28.3,24.6z&quot;/&gt;
    &lt;path id=&quot;hair&quot; class=&quot;st0&quot; d=&quot;M30.8,8.1c-0.9-2-3.6-3.5-6.8-3.5c-0.3,0-0.5,0-0.8,0c-1.2-0.6-2.7-1-4.4-1c-1.6,0-3.2,0.4-4.4,1
        c-0.3,0-0.6,0-0.9,0c-3.4,0-6.3,1.7-7,3.9C4.2,9.4,2.6,11.1,2.6,13c0,2.8,3.2,5,7.2,5c1.9,0,3.5-0.5,4.8-1.3c0.1,0,0.2,0,0.3,0
        c1.3,0.7,2.9,1.2,4.7,1.2c1.9,0,3.6-0.5,4.9-1.4c1.3,0.9,3.1,1.5,5.1,1.5c4,0,7.2-2.2,7.2-5C36.8,10.5,34.2,8.5,30.8,8.1z&quot;/&gt;
&lt;/svg&gt;</code></pre>
<h2 id="그리기">그리기</h2>
<blockquote>
<p>속성을 <code>inline-style</code>로 사용하지 않고 <code>css</code>(<code>&lt;style&gt;</code> or <code>xxx.css</code>파일 )을 이용해서 지정할 수 있습니다.
우선순위 <code>css</code> &gt; <code>inline-style</code></p>
</blockquote>
<ul>
<li>속성<ol>
<li><code>x</code>, <code>y</code>: 시작점 위치</li>
<li><code>width</code>, <code>height</code>: 사각형 크기</li>
<li><code>fill</code>: 색상</li>
<li><code>stroke</code>: 테두리 색상</li>
<li><code>stroke-width</code>: 테두리 굵기</li>
<li><code>stroke-linecap</code>: 선의 마무리 형태 ( <code>butt</code>, <code>round</code> , <code>square</code> )</li>
<li><code>stroke-linejoin</code>: 모서리 형태 ( <code>miter</code>, <code>round</code> , <code>bevel</code> )</li>
</ol>
</li>
</ul>
<h3 id="1-사각형--rect-">1. 사각형 ( <code>&lt;rect&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>rx</code>, <code>ry</code>: 모서리 둥글림 정도</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;rect
    x=&quot;40&quot;
    y=&quot;40&quot;
    width=&quot;200&quot;
    height=&quot;200&quot;
    fill=&quot;purple&quot;
    stroke=&quot;black&quot;
    stroke-width=&quot;20&quot;

    stroke-linejoin=&quot;round&quot;

    rx=&quot;20&quot;
    ry=&quot;40&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="2-원--circle-">2. 원 ( <code>&lt;circle&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>cx</code>, <code>cy</code>: 원의 중점</li>
<li><code>r</code>: 반지름</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;circle
    cx=&quot;400&quot;
    cy=&quot;120&quot;
    r=&quot;80&quot;
    fill=&quot;purple&quot;
    stroke=&quot;black&quot;
    stroke-width=&quot;20&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="3-타원--ellipse-">3. 타원 ( <code>&lt;ellipse&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>cx</code>, <code>cy</code>: 원의 중점</li>
<li><code>rx</code>, <code>ry</code>: x/y축 반지름</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;ellipse
    cx=&quot;400&quot;
    cy=&quot;320&quot;
    rx=&quot;30&quot;
    ry=&quot;60&quot;
    fill=&quot;purple&quot;
    stroke=&quot;black&quot;
    stroke-width=&quot;20&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="4-직선--line-">4. 직선 ( <code>&lt;line&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>x1</code>, <code>x2</code>: x축의 시작/끝 점</li>
<li><code>y1</code>, <code>y2</code>: y축의 시작/끝 점</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;line
    x1=&quot;100&quot;
    x2=&quot;200&quot;
    y1=&quot;300&quot;
    y2=&quot;350&quot;
    stroke=&quot;purple&quot;
    stroke-width=&quot;20&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="5-선--polyline-">5. 선 ( <code>&lt;polyline&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>points</code>: x, y축의 시작과 끝점들을 <code>,</code>로 구분해서 작성</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;polyline
    points=&quot;20 260, 20 370, 250 370, 220 280&quot;
    stroke=&quot;blue&quot;
    stroke-width=&quot;20&quot;
    fill=&quot;transparent&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="6-다각형--ploygon-">6. 다각형 ( <code>&lt;ploygon&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>points</code>: x, y축의 시작과 끝점들을 <code>,</code>로 구분해서 작성</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;ploygon
    points=&quot;20 260, 20 370, 250 370, 220 280&quot;
    stroke=&quot;blue&quot;
    stroke-width=&quot;20&quot;
    fill=&quot;transparent&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="7-자유-그리기--path-">7. 자유 그리기 ( <code>&lt;path&gt;</code> )</h3>
<ul>
<li>속성<ol>
<li><code>d</code>
1-1. <code>M</code>: 시작점
1-2. <code>L</code>: 직선 ( 시작점, 끝점 )
1-3. <code>H</code>: 가로 직선 ( 이동할 <code>x</code>위치 )
1-4. <code>V</code>: 세로 직선 ( 이동할 <code>y</code>위치 )
1-5. <code>Z</code>: 현재 위치와 시작점 잇기
1-6. <code>C</code>: 곡선 ( 시작점, 휘어짐 정도, 끝점 )</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;path
    d=&quot;M300 200 L 500 100 H 200 V 200 Z C 350 250, 200 300, 400 300&quot;
    stroke=&quot;blue&quot;
    stroke-width=&quot;10&quot;
    fill=&quot;transparent&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="8-텍스트">8. 텍스트</h3>
<ul>
<li>속성<ol>
<li><code>x</code>, <code>y</code>: 시작과 끝위치</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;text
    x=&quot;20&quot;
    y=&quot;100&quot;
    font-size=&quot;2rem&quot;
    font-weight=&quot;bold&quot;
    fill=&quot;blue&quot;
  &gt;
    Hello, SVG
  &lt;/text&gt;
&lt;/svg&gt;</code></pre>
<h2 id="특수한-효과들">특수한 효과들</h2>
<h3 id="1-그레디언트">1. 그레디언트</h3>
<ol>
<li><code>&lt;linearGradient&gt;</code>: 직선 형태의 그레디언트 ( 좌 -&gt; 우 )</li>
<li><code>&lt;radialGradient&gt;</code>: 원형 형태의 그레디언트 ( 상, 하, 좌, 우 -&gt; 중간 )</li>
</ol>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;defs&gt;
    &lt;linearGradient id=&quot;my-gradient&quot;&gt;
      &lt;stop offset=&quot;0%&quot; stop-color=&quot;red&quot; /&gt;
      &lt;stop offset=&quot;50%&quot; stop-color=&quot;green&quot; /&gt;
      &lt;stop offset=&quot;100%&quot; stop-color=&quot;blue&quot; /&gt;
    &lt;/linearGradient&gt;
  &lt;/defs&gt;

  &lt;text
    x=&quot;20&quot;
    y=&quot;100&quot;
    font-size=&quot;2rem&quot;
    font-weight=&quot;bold&quot;
    fill=&quot;url(#my-gradient)&quot;
  &gt;
    Hello, SVG
  &lt;/text&gt;
&lt;/svg&gt;</code></pre>
<h3 id="2-패턴--pattern-">2. 패턴 ( <code>&lt;pattern&gt;</code> )</h3>
<blockquote>
<p><strong>반복할 요소의 크기 * 반복 횟수 === <code>&lt;svg&gt;</code>의 크기</strong>가 맞지 않으면 의도한 모양의 패턴과 다른 모양이 됩니다.
아래 예시는 원크기(80) * 반복 횟수(10) === <code>&lt;svg&gt;</code>크기(800)</p>
</blockquote>
<ul>
<li>속성<ol>
<li><code>x</code>, <code>y</code>: 패턴의 시작 위치</li>
<li><code>width</code>, <code>height</code>: 가로/세로 반복할 개수 ( 1: 1개, 0.5: 2개, 0.1: 10개 ... )</li>
<li><code>&lt;pattern&gt; 내부</code>: 반복할 요소</li>
</ol>
</li>
</ul>
<pre><code class="language-html">&lt;svg width=&quot;100%&quot; height=&quot;100%&quot; viewBox=&quot;0 0 800 800&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;defs&gt;
    &lt;pattern
      id=&quot;my-pattern&quot;
      x=&quot;0&quot;
      y=&quot;0&quot;
      width=&quot;0.1&quot;
      height=&quot;0.1&quot;
    &gt;
      &lt;circle cx=&quot;40&quot; cy=&quot;40&quot; r=&quot;40&quot; fill=&quot;blue&quot; /&gt;
    &lt;/pattern&gt;
  &lt;/defs&gt;

  &lt;rect
    x=&quot;0&quot;
    y=&quot;0&quot;
    width=&quot;100%&quot;
    height=&quot;100%&quot;
    fill=&quot;url(#my-pattern)&quot;
  /&gt;
&lt;/svg&gt;</code></pre>
<h3 id="3-마스크--mask-">3. 마스크 ( <code>&lt;mask&gt;</code> )</h3>
<p>특정 영역만 보이게 하는 효과입니다.</p>
<p><code>&lt;mask&gt;</code> 내부의 요소가 흰색에 가까울수록 진하게 보이게 됩니다.</p>
<pre><code class="language-html">&lt;svg
  x=&quot;0&quot;
  y=&quot;0&quot;
  width=&quot;100%&quot;
  height=&quot;100%&quot;
  viewBox=&quot;0 0 400 400&quot;
&gt;
  &lt;defs&gt;
    &lt;mask id=&quot;my-mask&quot;&gt;
      &lt;circle id=&quot;circle&quot; cx=&quot;20&quot; cy=&quot;80&quot; r=&quot;60&quot; fill=&quot;#fff&quot;/&gt;
    &lt;/mask&gt;

    &lt;style&gt;
      @keyframes move {
        0% { cx: 20; }
        100% { cx: 140; }
      }

      #circle {
        animation: move 1s infinite alternate;
      }
    &lt;/style&gt;
  &lt;/defs&gt;

  &lt;g mask=&quot;url(#my-mask)&quot;&gt;
    &lt;text x=&quot;20&quot; y=&quot;40&quot; font-size=&quot;2rem&quot; font-weight=&quot;bold&quot; fill=&quot;red&quot;&gt;
      Hello, SVG
    &lt;/text&gt;
    &lt;text x=&quot;20&quot; y=&quot;80&quot; font-size=&quot;2rem&quot; font-weight=&quot;bold&quot; fill=&quot;green&quot;&gt;
      Hello, Next.js
    &lt;/text&gt;
    &lt;text x=&quot;20&quot; y=&quot;120&quot; font-size=&quot;2rem&quot; font-weight=&quot;bold&quot; fill=&quot;blue&quot;&gt;
      Hello, Velog
    &lt;/text&gt;
  &lt;/g&gt;
&lt;/svg&gt;</code></pre>
<h3 id="4-텍스트-부분-스타일링--tspan-">4. 텍스트 부분 스타일링 ( <code>&lt;tspan&gt;</code> )</h3>
<pre><code class="language-html">&lt;svg width=&quot;600&quot; height=&quot;400&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;text
    x=&quot;20&quot;
    y=&quot;100&quot;
    font-size=&quot;2rem&quot;
    font-weight=&quot;bold&quot;
    fill=&quot;blue&quot;
  &gt;
    Hello, &lt;tspan fill=&quot;green&quot;&gt;SVG&lt;/tspan&gt;
  &lt;/text&gt;
&lt;/svg&gt;</code></pre>
<h3 id="5-선-위에-글자-입히기--textpath-">5. 선 위에 글자 입히기 ( <code>&lt;textPath&gt;</code> )</h3>
<pre><code class="language-html">&lt;svg width=&quot;1000&quot; height=&quot;800&quot; style=&quot;background-color: #ddd;&quot;&gt;
  &lt;defs&gt;
    &lt;path
      id=&quot;my-text&quot;
      d=&quot;M 50 400 C 50 400, 300 500, 400 400 C 400 400, 600 170, 700 300&quot;
      fill=&quot;transparent&quot;
      stroke=&quot;red&quot;
      stroke-width=&quot;10&quot;
    /&gt;
  &lt;/defs&gt;

  &lt;text
    x=&quot;20&quot;
    y=&quot;100&quot;
    font-size=&quot;2rem&quot;
    font-weight=&quot;bold&quot;
    fill=&quot;blue&quot;
  &gt;
    &lt;textPath href=&quot;#my-text&quot;&gt;
      Hello, SVG Lorem ipsum dolor, sit amet consectetur
    &lt;/textPath&gt;
  &lt;/text&gt;
&lt;/svg&gt;</code></pre>
<h2 id="마무리">마무리</h2>
<p><code>svg</code>는 대부분 툴을 이용해서 만들어진 상태로 사용하기 때문에 직접적으로 코드를 작성하거나 수정할 경우는 거의 없다고 생각합니다.</p>
<p>저도 <a href="https://heroicons.com/">heoricon</a>, <a href="https://fontawesome.com/">fontawesome</a> 같은 사이트에서 사용할 아이콘을 찾아서 복사 붙여넣기를 이용해서 사용했지만 사용했습니다.</p>
<p>하지만 이게 어떤 코드인지 모르고 사용했기 때문에 불안했기도 했고, 아이콘에서 색상, 크기, 애니메이션 같은 작은 부분을 수정하는데도 검색을 통해서 찾고 테스트하는 과정을 계속하면서 그때만 대충 이해하고 넘어갔던 부분을 하나하나 공부하면서 원리를 이해할 수 있어서 도움이 많이 됐습니다.</p>
<p>물론 위의 코드들을 모두 외우거나 실제로 작성하는 일은 없겠지만 최소한 어떤 코드에 어떤 동작을 하는지 얕게 이해하고 있으니 다음 문제가 생겼을 때 시간 낭비를 줄이고 금방 해결할 수 있을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[bleshop v 1.0.0 마무리]]></title>
            <link>https://velog.io/@1-blue/bleshop-v-1.0.0-%EB%A7%88%EB%AC%B4%EB%A6%AC</link>
            <guid>https://velog.io/@1-blue/bleshop-v-1.0.0-%EB%A7%88%EB%AC%B4%EB%A6%AC</guid>
            <pubDate>Fri, 16 Sep 2022 11:08:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://bleshop.shop/">bleshop 배포 링크</a></p>
</blockquote>
<h2 id="bleshop">BleShop</h2>
<p>해당 프로젝트를 시작한 목적은 세 가지였습니다.
첫 번째는 모바일 형태의 레이아웃을 데스크탑에서도 적용되도록 구현하는 것
두 번째는 조금이라도 실제 결제 기능을 사용해보는 것
세 번째는 가독성 좋은 코드와 적절한 타입 선언과 경고/에러가 전혀 없는 코드를 작성하는 것</p>
<p>결과물을 제가 보고 판단하긴 했지만 어느 정도 원하던 결과물이 나왔다고 생각합니다.
모바일 ~ 데스크탑까지 모든 사이즈에 반응하는 앱 같은 레이아웃을 적용했고, 테스트 결제긴 하지만 결제가 되고 결제 기록을 등록할 수 있는 로직을 작성했고, 타입을 구분하고 유틸리티 타입을 사용해서 나름 적절하게 타입을 사용했다고 생각합니다.</p>
<h2 id="사용한-라이브러리">사용한 라이브러리</h2>
<h3 id="1-nextjs">1. Next.js</h3>
<p><code>React.js</code>의 프레임워크인 만큼 많은 기능들을 쉽게 사용할 수 있도록 지원해주고, 사용자에 의도에 따라서 적절하게 <code>SSG</code>, <code>SSR</code>, <code>CSR</code>을 적용할 수 있도록 도와주기 때문에 선택했습니다.</p>
<p>대부분 <code>SSG</code> or <code>SSR</code>을 적용했고, 적용의 근거는 <code>SEO</code>나 렌더링에 사용하는 데이터가 자주 변하는지 여부입니다.
예를 들어서 로그인/회원가입 페이지 같은 경우에는 데이터의 변화가 없고 <code>SEO</code>가 필요하지 않아서 <code>SSG</code>를 적용했습니다.
메인/특정 게시글 페이지같은 경우에는 렌더링 데이터가 자주 바뀌고, <code>SEO</code>가 필요하므로 <code>SSR</code>을 적용했습니다.</p>
<p>추가로 내 정보 페이지 같이 데이터가 자주 변하지만 <code>SEO</code>가 필요 없는 페이지들은 <code>CSR</code>이나 <code>SSR</code> 중에 선택이라고 생각하는데 현재는 <code>SSR</code>로 구현했습니다. 하지만 <code>SSR</code>은 서버에 부담을 주기 때문에 다음에 <code>CSR</code>로 바꾸려고 합니다.</p>
<h3 id="2-recoil">2. Recoil</h3>
<p>상태 관리 라이브러리로 사용하기 위해서 선택했습니다.
리액트의 훅과 사용법이 거의 비슷하기 때문에 러닝커브가 매우 낮고, <code>Redux</code>처럼 어디든 호환하지 않고 <code>React.js</code> 전용이기 때문에 세팅하기도 매우 간단하기 때문에 빠르게 공부하고 사용할 수 있다고 판단해서 선택했습니다.</p>
<p>다만 사용하면서 얻은 이점보다는 불편함이 더 많다고 느꼈습니다.
<code>Redux</code>처럼 정보가 많지 않고, <code>Next.js</code>와 사용하면 <code>SSR</code>에서 키 중복 경고가 발생하는 것, <code>SSR</code>에서 사용 시 쿠키 문제 등의 문제가 있어서 불편했습니다. 물론 사용이 미숙해서 그렇겠지만 그것을 극복할 만큼의 많은 정보가 없다고 느꼈습니다.</p>
<p>결과적으로는 비동기 <code>selector</code>를 모두 없애고 컴포넌트간의 데이터 공유를 위해서만 사용했습니다.</p>
<h3 id="3-next-auth">3. next-auth</h3>
<blockquote>
<p><a href="https://velog.io/@1-blue/%EC%9D%B8%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84-next-auth">next-auth 적용 방법 포스트</a></p>
</blockquote>
<p>로그인을 처리하기 위한 인증 로직을 위해 선택했습니다.
여러 가지 로그인 방식을 지원해주고 그중에서 <code>Credentials</code> 방식을 선택했습니다. ( <code>email</code>, <code>password</code>를 이용한 인증 방식 )
기존에 이용해봤던 <code>passport</code>보다는 더 쉽다고 느껴졌고, 프론트/서버에서 상황에 맞게 로그인한 유저의 정보를 가져올 수 있어서 좋았습니다.</p>
<h3 id="4-prisma">4. prisma</h3>
<p>데이터베이스를 관리하기 위해서 선택했습니다.
<code>sequelize</code>보다 더 쉽고, <code>typescript</code>에서 사용 시 작성한 모델에 맞는 타입을 자동으로 생성해주기 때문에 정말 좋습니다.</p>
<p>데이터의 <code>CRUD</code>를 처리할 수 있는 메서드들을 모두 지원해주고, <code>_count</code>, <code>skip</code>, <code>orderBy</code> 등의 유틸 기능들까지 지원해줘서 편합니다. 또한 <code>TypeScript</code>를 이용하면 타입이 적용되어 자동 완성을 지원해주고, 모델 타입 + <code>TypeScript utility</code>를 활용하여 원하는 타입을 쉽게 만들고, 타입 변화에 맞게 자동으로 업데이트되어서 사용하기 정말 편합니다.</p>
<h3 id="5-tailwindcss">5. tailwindcss</h3>
<p><code>css</code>를 쉽게 적용하기 위해서 사용했습니다.
아직 <code>styled-components</code>밖에 사용해보지 않았지만 다른 <code>css</code> 전처리기, 프레임워크, <code>css-in-js</code>들 중에서 가장 편하지 않을까 생각합니다.
처음에 적응하기가 조금 불편할 수 있지만 <code>css</code>를 어느 정도 안다면 예측 가능한 이름이고, 검색하면 충분히 결과를 얻을 수 있습니다. 또한 플러그인을 설치하면 자동 완성을 지원해주고, 커스터마이징하고 쉽고, <code>styled-components</code>처럼 새로운 컴포넌트를 만들 필요가 없어 코드가 적고, 이름이 이미 정해져 있어서 네이밍을 생각하지 않아도 되는 이점이 있습니다.</p>
<p>아무튼 만족도가 매우 높은 <code>css</code> 프레임워크입니다.</p>
<h3 id="6-aws-s3">6. AWS-S3</h3>
<blockquote>
<p><a href="https://velog.io/@1-blue/AWS-S3%EC%9D%98-preSignedUrl-%EC%82%AC%EC%9A%A9"><code>AWS-S3</code>의 <code>presigned Url</code></a></p>
</blockquote>
<p>정적 파일인 이미지를 저장하기 위해 선택했습니다.</p>
<p>이미지는 파일의 변화는 없지만, 서버에 저장하게 되면 용량을 크게 차지하게 됩니다. 유저는 개인이지만 서버는 하나를 사용하기 때문에 하나의 서버에 모든 유저의 이미지를 저장하면 그만큼 서버의 메모리를 사용하기 때문에 프리티어 <code>AWS-EC2</code>를 사용하는 입장에서는 서버의 메모리가 충분하기 않기 때문에 다른 저장 공간을 찾다가 알게 되어서 선택했습니다.</p>
<p>프리티어 기간동안 정해진 용량은 무료로 사용할 수 있고, <code>presigned Url</code> 기능을 지원해주기 때문에 실제로 사용하는 서버에는 이미지를 전달하는 큰 작업을 하지 않아도 되기 때문에 서버의 부담을 줄일 수 있어서 유용하다고 생각합니다.</p>
<ul>
<li>이미지 처리 흐름<ol>
<li>브라우저에서 이미지 업로드 시 <code>presigned Url</code>을 얻어서 <code>S3</code>의 <code>temporary</code> 폴더에 이미지 저장</li>
<li>저장된 이미지 <code>url</code>을 이용해서 브라우저에 이미지 프리뷰 생성</li>
<li>이미지 사용 확정 시 이미지를 <code>temporary</code> 폴더에서 사용 폴더로 이동 ( <code>user</code>, <code>product</code>, <code>benner</code>, <code>review</code> 등 )</li>
<li>이동된 이미지 <code>url</code>을 <code>DB</code>에 저장</li>
<li>이미지 제거 시 <code>remove</code> 폴더로 이동</li>
</ol>
</li>
</ul>
<h3 id="7-iamport">7. iamport</h3>
<blockquote>
<p><a href="https://velog.io/@1-blue/%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5#%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D">iamport 웹 결제 기능</a>
<a href="https://velog.io/@1-blue/iamport-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%9B%B9-%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5">iamport 모바일 결제 기능</a></p>
</blockquote>
<p>결제 기능을 처리하기 위해 선택했습니다.
문서가 한글로 사용법에 따라서 정리가 잘되어 있어서 쉽게 사용할 수 있습니다.</p>
<p>실제 결제는 아니고 테스트 결제 기능까지만 구현했습니다. 돈이 나가지는 않지만 실제 결제처럼 결제 기록은 생기고, 결제 기록을 <code>DB</code>에 저장하도록 구현했습니다.
데스크탑과 모바일의 동작 방식이 약간 다르기 때문에 배포 후에 모바일로 테스트하다가 문제를 발견해서 해결했습니다.</p>
<h3 id="8-나머지-라이브러리들">8. 나머지 라이브러리들</h3>
<ol>
<li><code>react-hook-form</code>: <code>form</code>관련 코드를 편하게 사용하기 위해서 선택</li>
<li><code>axios</code>: 비동기 처리를 편하게 사용하기 위해서 선택</li>
<li><code>react-slick</code>: <code>carousel</code> 구현을 위해서 선택</li>
<li><code>react-toastify</code>: <code>toast-message</code>를 위해서 선택</li>
</ol>
<h2 id="구현한-기능들">구현한 기능들</h2>
<ol>
<li>유저 CRUD</li>
<li>상품 CRD</li>
<li>리뷰 CRD</li>
<li>장바구니 CRD</li>
<li>찜하기 CRD</li>
<li>결제 기능 CRD</li>
<li>결제 기록 CRD</li>
<li>상품 검색</li>
<li>카테고리, 필터링</li>
</ol>
<h2 id="🚀-배포">🚀 배포</h2>
<blockquote>
<p><a href="https://velog.io/@1-blue/blegram-v-3.0.0#-%EB%B0%B0%ED%8F%AC">배포 방법</a>
<a href="">빌드 최적화 적용 방법</a></p>
</blockquote>
<p>배포 방법은 위 링크에 정리한 대로 실행했습니다.</p>
<p>무료로 사용할 수 있는 <code>AWS-EC2</code>를 사용했으며, 배포하면서 두 가지 문제가 발생했습니다.</p>
<p>첫 번째는 메모리 부족으로 빌드가 안 되는 문제입니다.
빌드하면 서버가 자꾸 멈추거나 렉이 걸려서 강제 중지하고 다시 실행해야해서 일단 빌드 최적화를 적용했습니다. 하지만 빌드 최적화를 적용해도 호전되긴 했지만 빌드가 안 되는 문제는 그대로여서 결국 빌드 결과물을 <code>GitHub</code>에 올려서 받아 쓰는 방법을 사용했습니다.</p>
<p>두 번째 문제는 정상 작동하다가 서버가 갑자기 멈추는 문제입니다.
에러 로그 조차 남기지 않아서 뭐가 문제인지 알아내기 힘들었지만 이것 저것 적용해본 결과 프리티어를 사용하기 때문에 <code>AWS-EC2</code>의 <code>RAM</code>이 부족해서 생기는 문제인 것으로 결론지었습니다. <a href="https://dundung.tistory.com/284">AWS-EC2 메모리 스왑</a>을 보고 그대로 적용해본 결과 문제없이 동작했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💵 모바일 웹 결제 기능 ( by iamport )]]></title>
            <link>https://velog.io/@1-blue/iamport-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%9B%B9-%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@1-blue/iamport-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%9B%B9-%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Wed, 14 Sep 2022 12:22:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/96de4ccf-563e-4a66-8d79-a9e8433f46ee/image.gif" alt=""></p>
<blockquote>
<p><a href="https://docs.iamport.kr/implementation/payment#mobile-web">iamport - 모바일 웹 결제</a>를 참고했습니다.</p>
</blockquote>
<h2 id="구현-동기">구현 동기</h2>
<p>개발 모드일 때는 모바일로 테스트하기 힘들어서 데스크탑 웹브라우저만을 이용해서 테스트하고 넘어갔습니다.
하지만 <code>AWS-EC2</code>에 배포하고 모바일로 들어가서 결제 기능을 사용해보니 이상하게도 결제 로직은 정상적으로 작동하는데 결제 정보가 <code>DB</code>에 기록되지 않는 문제가 발생했습니다.
이 문제를 해결하기 위해서 <code>iamport</code> 공식 문서를 찾아보면서 모바일 웹 결제 기능을 개발하게 되었습니다.</p>
<h2 id="문제-원인">문제 원인</h2>
<p>카카오페이 결제를 기준으로 모바일 웹과 데스크탑 웹은 결제 방식이 조금 다릅니다.
데스크탑 웹의 경우에는 QR코드나 휴대폰 번호/생년월일을 이용해서 카카오톡으로 결제하게 해줍니다.
이렇게 결제하게 되면 웹브라우저는 가만히 있고 다른 곳에서 결제를 처리하고 결과만 받게 됩니다.
그 결제 결과를 <code>IMP.request_pay(data, callback)</code>에서 <code>callback</code>의 첫 번째 인자로 받아서 결제 기록을 <code>DB</code>에 남기도록 구현했습니다. ( 예시에서 <code>rsp</code>가 결제 결과 데이터 )</p>
<ul>
<li><p>예시</p>
<pre><code class="language-ts">const onPayment = useCallback(
(pg: &quot;kakaopay&quot; | &quot;tosspay&quot;) =&gt; () =&gt; {
  if (!process.env.NEXT_PUBLIC_IAMPORT_CODE)
    return toast.error(&quot;iamport의 가맹점 식별코드가 없습니다.&quot;);
  if (!paymentData) {
    toast.error(
      &quot;결제할 상품의 정보가 존재하지 않습니다. 메인 화면으로 이동됩니다.&quot;
    );
    return router.push(&quot;/&quot;);
  }

  // iamport를 사용하기 위해 가맹점 식별코드 등록
  window.IMP.init(process.env.NEXT_PUBLIC_IAMPORT_CODE);

  const callback = async (rsp) =&gt; {
    if (
      !rsp.buyer_name ||
      !rsp.buyer_addr ||
      !rsp.paid_amount ||
      !rsp.buyer_email ||
      !rsp.buyer_tel ||
      !rsp.pg_provider
    )
      return toast.warning(&quot;결제에 필요한 데이터가 부족합니다.&quot;);

    if (rsp.success) {
      try {
        // &gt;&gt;&gt; 결제 완료 DB 저장
        await apiService.orderService.apiCreateOrder({
          singleData: rsp.custom_data.singleData,
          orderData: {
            name: rsp.buyer_name,
            address: rsp.buyer_addr,
            residence: rsp.custom_data.residence,
            message: rsp.custom_data.message,
            amount: rsp.paid_amount,
            email: rsp.buyer_email,
            phone: rsp.buyer_tel,
            provider: rsp.pg_provider,
          },
        });

        toast.success(
          &quot;결제가 완료되었습니다. 2초뒤에 결제내역 페이지로 이동합니다.&quot;,
          { autoClose: 2000 }
        );

        setTimeout(() =&gt; router.push(&quot;/information/order&quot;), 2000);
      } catch (error) {
        console.error(&quot;error &gt;&gt; &quot;, error);

        if (error instanceof AxiosError) {
          toast.error(error.response?.data.message);
        } else {
          toast.error(&quot;알 수 없는 문제로 인해 결제에 실패했습니다.&quot;);
        }
      }
    } else {
      toast.error(&quot;결제에 실패했습니다. &quot; + rsp.error_msg, {
        autoClose: 2000,
      });
    }
  };

  window.IMP.request_pay({ ...paymentData, pg, pay_method: &quot;card&quot; }, callback);
},
[router, paymentData]
);</code></pre>
</li>
</ul>
<p>하지만 모바일 웹으로 접근해서 결제하는 경우에는 휴대폰에 카카오톡이 설치되어 있으므로 어떤 카카오톡 유저가 결제를 원하는지를 판단할 필요 없이 카카오톡으로 바로 들어가서 로그인한 유저에게 결제 요청을 보내게 됩니다.
그렇게 되면 브라우저에서 카카오톡으로 화면 전환이 발생하게 되며 결제를 성공적으로 마치면 결제할 때 입력했던 <code>m_redirect_url</code>을 이용해서 해당 <code>url</code> 리다이렉션 됩니다.</p>
<p>따라서 위 데스크탑 웹에서 결제를 진행했을 때와 다르게 <code>callback</code>이 실행되지 않아 <code>DB</code>에 결제 정보가 등록되지 않는 문제가 발생했던 것입니다.
( 다른 결제 방식을 선택해서 다른 웹사이트나 어플리케이션으로 리다이렉션 되어서 결제를 진행합니다. )</p>
<h2 id="해결-방법">해결 방법</h2>
<p>위의 경우를 대비해서 <code>iamport</code>에서 특정 결제에 대한 결과를 얻는 방법을 제공해줍니다.</p>
<p>기본적인 흐름은 아래와 같습니다.</p>
<ol>
<li><code>m_redirect_url</code>에서 결제 결과를 저장할 <code>url</code>를 지정</li>
<li><code>iamport</code>에서 <code>m_redirect_url</code>로 리다이렉션 할 때 <code>imp_uid</code>와 <code>merchant_uid</code> 값을 <code>query string</code>으로 첨부</li>
<li>해당 <code>url</code>에서 <code>REST API KEY</code>와 <code>REST API SECRET KEY</code>를 이용해서 <code>access_token</code>을 얻음</li>
<li><code>access_token</code>과 <code>imp_uid</code>을 이용해서 특정 결제에 대한 정보를 얻음</li>
<li>얻은 정보를 이용해서 <code>DB</code>에 결제 정보를 기록함</li>
<li>결제 정보 페이지로 리다이렉션 함</li>
</ol>
<p>공식 페이지에 위와 같은 방식으로 설명이 매우 잘되어 있어서 굳이 코드 예시를 작성하지는 않겠습니다.</p>
<p>다만 토큰과 결제정보를 주고받는 타입과 api 요청 함수들을 첨부하겠습니다.</p>
<p>또한 사용하면서 주의해야 할 점으로 <code>custom_data</code>를 사용하는 경우에는 <code>string</code> 타입이므로 <code>JSON.parse()</code>를 이용해서 파싱하고 사용해야 합니다.</p>
<ul>
<li>토큰과 결제 정보를 얻는 함수<pre><code class="language-ts">import axios from &quot;axios&quot;;
</code></pre>
</li>
</ul>
<p>// type
import type {
  // IamportGetTokenBody,
  IamportGetTokenResponse,
  IamportGetPaymentDataBody,
  IamportGetPaymentDataResponse,
} from &quot;@src/types&quot;;</p>
<p>const iamportInstance = axios.create({
  baseURL: &quot;<a href="https://api.iamport.kr&quot;">https://api.iamport.kr&quot;</a>,
  timeout: 10000,
});</p>
<p>/**</p>
<ul>
<li>2022/09/14 - iamport 엑세스 토큰 발급 받기 - by 1-blue</li>
<li>@returns 엑세스 토큰</li>
<li>/
const apiGetToken = () =&gt;
iamportInstance.post<IamportGetTokenResponse>(
  <code>/users/getToken</code>,
  {<pre><code>imp_key: process.env.IAMPORT_REST_API_KEY,
imp_secret: process.env.IAMPORT_REST_API_SECRET,</code></pre>  },
  {<pre><code>headers: {
  &quot;Content-Type&quot;: &quot;application/json&quot;,
},</code></pre>  }
);</li>
</ul>
<p>/**</p>
<ul>
<li>2022/09/14 - iamport 엑세스 토큰을 이용해 결제 정보 조회 - by 1-blue</li>
<li>@returns 결제 정보</li>
<li>/
const apiGetPaymentData = ({
imp_uid,
access_token,
}: IamportGetPaymentDataBody) =&gt;
iamportInstance.get<IamportGetPaymentDataResponse>(<code>/payments/${imp_uid}</code>, {
  headers: { Authorization: access_token },
});</li>
</ul>
<p>/**</p>
<ul>
<li>2022/09/14 - iamport 관련 api 요청 객체 - by 1-blue</li>
<li>/
const iamportService = {
apiGetToken,
apiGetPaymentData,
};</li>
</ul>
<p>export default iamportService;</p>
<pre><code>
+ type ( 위에서 import한 타입 )
```ts
type RequestPaymentData = {
  amount: number;
  apply_num: string;
  bank_code: string | null;
  bank_name: string | null;
  buyer_addr: string;
  buyer_email: string;
  buyer_name: string;
  buyer_postcode: string;
  buyer_tel: string;
  cancel_amount: number;
  cancel_history: [];
  cancel_reason: string | null;
  cancel_receipt_urls: [];
  cancelled_at: number;
  card_code: string | null;
  card_name: string | null;
  card_number: string | null;
  card_quota: number;
  card_type: string | null;
  cash_receipt_issued: false;
  channel: string;
  currency: string;
  custom_data: string;
  customer_uid: string | null;
  customer_uid_usage: string | null;
  emb_pg_provider: string | null;
  escrow: false;
  fail_reason: string | null;
  failed_at: number;
  imp_uid: string;
  merchant_uid: string;
  name: string;
  paid_at: number;
  pay_method: string;
  pg_id: string;
  pg_provider: string;
  pg_tid: string;
  receipt_url: string;
  started_at: number;
  status: string;
  user_agent: string;
  vbank_code: string | null;
  vbank_date: number;
  vbank_holder: string | null;
  vbank_issued_at: number;
  vbank_name: string | null;
  vbank_num: string | null;
};

/**
 * 2022/09/14 - iamport 엑세스 토큰 요청 송신 타입 - by 1-blue
 */
export type IamportGetTokenBody = {};
/**
 * 2022/09/14 - iamport 엑세스 토큰 요청 수신 타입 - by 1-blue
 */
export type IamportGetTokenResponse = { response: { access_token: string } };
/**
 * 2022/09/14 - iamport 결제 정보 요청 송신 타입 - by 1-blue
 */
export type IamportGetPaymentDataBody = {
  imp_uid: string;
  access_token: string;
};
/**
 * 2022/09/14 - iamport 결제 정보 요청 수신 타입 - by 1-blue
 */
export type IamportGetPaymentDataResponse = { response: RequestPaymentData };</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[next.js 빌드 최적화]]></title>
            <link>https://velog.io/@1-blue/next.js-%EB%B9%8C%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@1-blue/next.js-%EB%B9%8C%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Tue, 13 Sep 2022 07:05:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><code>next.js</code>를 사용하는 프로젝트입니다.</p>
</blockquote>
<h2 id="빌드-최적화-적용-이유">빌드 최적화 적용 이유</h2>
<p>프로젝트는 배포해서 사용할 때는 <code>npm run build</code> 명령어를 이용합니다.
위 명령어를 이용해서 빌드를 하고 나면 빌드에 대한 결과를 아래 이미지처럼 나타내줍니다.
<img src="https://velog.velcdn.com/images/1-blue/post/9fad78a0-29bf-4ff4-9121-de88d6bc8148/image.jpg" alt="">
위 이미지는 최적화하기 전의 빌드 결과인데 <code>First Load JS</code>를 보면 모두 빨간색으로 표시됩니다. 처음 로드되는 자바스크립트의 용량을 의미하는 것인데 요즘 웹브라우저 or 모바일로 접근 시 적정한 크기가 정해져있습니다. 빨간색으로 표시되는 것은 크기가 너무 크다(로딩 시간이 길다)는 의미이고 최적화를 해서 줄여주는 것이 좋다는 의미입니다.</p>
<h2 id="빌드-최적화-방법">빌드 최적화 방법</h2>
<p>아래의 두 가지 방법을 적용했고 아래 이미지는 적용한 빌드 결과입니다.
<img src="https://velog.velcdn.com/images/1-blue/post/2f0f9a93-68f3-470a-be91-8b068533176a/image.jpg" alt=""></p>
<h3 id="1-tree-shaking">1. Tree Shaking</h3>
<blockquote>
<p><code>Tree shaking</code>은 사용되지 않는 코드를 제거하기 위해 JavaScript 컨텍스트에서 일반적으로 사용되는 용어입니다. ( <a href="https://webpack.kr/guides/tree-shaking/">webpack - Tree Shaking</a> )</p>
</blockquote>
<p>구글링을 통해서 방법을 찾은 방법으로 사용되지 않는 코드를 제거하는 방법입니다.
실제로 사용하지 않는 코드는 <code>import</code>하지 않고 정확하게 필요한 컴포넌트/함수들만 사용한다고 생각했는데 <code>Tree Shaking</code>를 적용하는 것으로만으로도 대부분의 컴포넌트의 <code>First Load JS</code>가 적절한 용량으로 바꿔졌습니다.</p>
<ul>
<li><p><code>package.json</code></p>
<pre><code class="language-json">{
// ... 나머지 생략

// 아래 구문만 추가해주면 빌드 시 웹팩에서 자동적으로 &quot;Tree shaking&quot;을 적용해줍니다.
&quot;sideEffects&quot;: false
}</code></pre>
</li>
</ul>
<h2 id="2-nextjs의-dynamic-import">2. Next.js의 Dynamic Import</h2>
<blockquote>
<p><code>Dynamic Import</code>란 외부 라이브러리를 지연 로딩하는 방법을 말합니다. ( <a href="https://nextjs.org/docs/advanced-features/dynamic-import">next.js - dynamic import</a> )</p>
</blockquote>
<p>지연 로딩이란 해당 라이브러리가 필요한 상황에 로딩하는 방법을 말합니다.</p>
<p>예를 들면 어떤 버튼을 클릭했을 경우 모달이 보여지는 기능을 구현할 때를 생각해보면 버튼을 클릭하기 이전에는 모달을 굳이 가져올 필요가 없습니다. 만약 미리 가져온 경우에 사용자가 버튼을 클릭하지 않으면 쓸데없이 용량을 차지하는 모달을 가져오는 낭비가 발생하게 됩니다.</p>
<p>이런 경우에 사용하기 위해서 <code>next.js</code>에서 제공해주는 <code>Dynamic Import</code> 기능을 통해서 즉시 필요한 컴포넌트/라이브러리가 아니면 지연 로딩을 통해서 해당 컴포넌트/라이브러리가 필요한 경우에 가져와서 사용하는 방법입니다.</p>
<p>이 방법을 사용하면 초기에 모든 컴포넌트/라이브러리를 가져올 필요가 없으니 <code>First Load JS</code>이 확연하게 줄어들게 됩니다.</p>
<ul>
<li>예시<pre><code class="language-tsx">import { useState } from &quot;react&quot;;
import dynamic from &quot;next/dynamic&quot;;
</code></pre>
</li>
</ul>
<p>// 사용법에 대한 자세한 방법은 공식페이지에서 확인하고 사용하면 됩니다.
const Modal = dynamic(() =&gt; import(&quot;@src/components/common/Modal&quot;), {
  suspense: true,
});</p>
<p>const TestComponent = () =&gt; {
  const [isShowModal, setIsShowModal] = useState(false);</p>
<p>  return (
    &lt;&gt;
      &lt;button type=&quot;button&quot; onClick={() =&gt; setIsShowModal((prev) =&gt; !prev)}&gt;
        show modal
      </button></p>
<pre><code>  // 버튼을 클릭하면 모달이 렌더링되는 구조이므로, 버튼이 클릭될 때 &quot;Modal&quot;컴포넌트를 가져옴
  {isShowModal &amp;&amp; (
    &lt;Modal&gt;
      &lt;h1&gt;대충 모달 내용&lt;/h1&gt;
    &lt;/Modal&gt;
  )}
&lt;/&gt;</code></pre><p>  );
};</p>
<p>export default TestComponent;
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[💵 웹 결제 기능 ( by iamport )]]></title>
            <link>https://velog.io/@1-blue/%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@1-blue/%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Sun, 11 Sep 2022 10:14:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/326c11dc-5a35-4e06-9121-aa06a8c37323/image.gif" alt=""></p>
<blockquote>
<p><code>next.js</code>에서 <code>recoil</code>과 <code>iamport</code>를 이용한 테스트 결제 기능 구현입니다.</p>
</blockquote>
<blockquote>
<p><code>iamport</code>에 대한 내용은 <a href="https://www.iamport.kr/">공식 페이지</a>에 들어가면 한국어로 각 상황에 맞는 메뉴얼이 있으니 참고하시면 됩니다.</p>
</blockquote>
<blockquote>
<p><code>iamport</code>의 타입에 대한 코드는 <a href="https://velog.io/@hyseoseo/TIL16-React-Typescript-%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8-%EA%B2%B0%EC%A0%9C-%EC%9A%94%EC%B2%AD">여기(velog 포스트)</a>를 참고했습니다.</p>
</blockquote>
<h2 id="동작-방식">동작 방식</h2>
<ol>
<li>로그인한 유저가 상품 구매 버튼 클릭</li>
<li>해당 유저가 등록한 배송지중에 하나 선택</li>
<li><code>recoil</code>에 구매에 필요한 정보들 등록 ( <code>pg</code>, <code>amount</code>, <code>buyer_tel</code> 등 단, 여러 상품을 한번에 결제할 수 있으니 공통 정보(<code>orderData</code>)와 개별 정보(<code>singleData</code>)를 분리해서 저장함 )</li>
<li>결제 페이지로 이동 ( 결제 방법 선택... 현재는 <code>kakaopay</code> or <code>toss</code> )</li>
<li>결제에 필요한 정보와 함께 <code>iamport</code>에 결제 요청</li>
<li>선택한 결제 방법대로 결제 ( 실제 결제가 아닌 테스트용 결제 )</li>
<li>결제가 성공하면 결괏값으로 <code>DB</code> 컬럼 추가</li>
<li>결제 결과 테이블을 이용해서 구매 목록 페이지 구성</li>
</ol>
<h2 id="코드">코드</h2>
<ul>
<li><a href="https://github.com/1-blue/bleshop/src/types/iamport.ts">결제 관련 타입</a><pre><code class="language-ts">import type { ApiCreateOrderBody } from &quot;@src/types&quot;;
</code></pre>
</li>
</ul>
<p>interface RequestPayAdditionalParams {
  digital?: boolean;
  vbank_due?: string;
  m_redirect_url?: string;
  app_scheme?: string;
  biz_num?: string;
}</p>
<p>interface Display {
  card_quota?: number[];
}</p>
<p>interface CustomData {
  residence: string;
  message: string;
  // 결제에 필요한 각자 정보의 배열 ( 동시에 여러 상품을 결제할 수 있으니, 각자 상품들에 대하 결제 정보 )
  singleData: ApiCreateOrderBody[&quot;singleData&quot;];
}</p>
<p>export interface RequestPayParams extends RequestPayAdditionalParams {
  pg: &quot;kakaopay&quot; | &quot;tosspay&quot;;
  pay_method: &quot;kakaopay&quot; | &quot;tosspay&quot; | string;
  escrow?: boolean;
  merchant_uid: string;
  name?: string;
  amount: number;
  // 필요 시 추가하는 커스텀 데이터
  custom_data: CustomData;
  tax_free?: number;
  currency?: string;
  language?: string;
  buyer_name?: string;
  buyer_tel: string;
  buyer_email?: string;
  buyer_addr?: string;
  buyer_postcode?: string;
  notice_url?: string | string[];
  display?: Display;
}</p>
<p>interface RequestPayAdditionalResponse {
  apply_num?: string;
  vbank_num?: string;
  vbank_name?: string;
  vbank_holder?: string | null;
  vbank_date?: number;
}</p>
<p>interface RequestPayResponse extends RequestPayAdditionalResponse {
  success: boolean;
  error_code: string;
  error_msg: string;
  imp_uid: string | null;
  merchant_uid: string;
  pay_method?: string;
  paid_amount?: number;
  status?: string;
  name?: string;
  pg_provider?: string;
  pg_tid?: string;
  buyer_name?: string;
  buyer_email?: string;
  buyer_tel?: string;
  buyer_addr?: string;
  buyer_postcode?: string;
  custom_data: CustomData;
  paid_at?: number;
  receipt_url?: string;
}</p>
<p>type RequestPayResponseCallback = (response: RequestPayResponse) =&gt; void;</p>
<p>export interface Iamport {
  init: (accountID: string) =&gt; void;
  request_pay: (
    params: RequestPayParams,
    callback?: RequestPayResponseCallback
  ) =&gt; void;
}</p>
<pre><code>
+ [결제 페이지](https://github.com/1-blue/bleshop/src/pages/payment/index.tsx)
```tsx
import { useCallback, useEffect } from &quot;react&quot;;
import { useRouter } from &quot;next/router&quot;;
import { useRecoilValue } from &quot;recoil&quot;;
import { toast } from &quot;react-toastify&quot;;

// api
import apiService from &quot;@src/api&quot;;

// state
import stateService from &quot;@src/states&quot;;

// component
import HeadInfo from &quot;@src/components/common/HeadInfo&quot;;
import Nav from &quot;@src/components/common/Nav&quot;;
import Support from &quot;@src/components/common/Support&quot;;
import Tool from &quot;@src/components/common/Tool&quot;;

// type
import type { Iamport } from &quot;@src/types&quot;;
import { AxiosError } from &quot;axios&quot;;

declare global {
  interface Window {
    IMP: Iamport;
  }
}

const Payment = () =&gt; {
  const router = useRouter();

  // 2022/09/04 - iamport 사용을 위한 cdn - by 1-blue
  useEffect(() =&gt; {
    toast.info(&quot;테스트용 결제이며, 실제로 결제가 되지 않습니다.&quot;);

    const jquery = document.createElement(&quot;script&quot;);
    jquery.src = &quot;https://code.jquery.com/jquery-1.12.4.min.js&quot;;

    const iamport = document.createElement(&quot;script&quot;);
    iamport.src = &quot;https://cdn.iamport.kr/js/iamport.payment-1.1.8.js&quot;;

    document.head.appendChild(jquery);
    document.head.appendChild(iamport);

    return () =&gt; {
      document.head.removeChild(jquery);
      document.head.removeChild(iamport);
    };
  }, []);

  // 2022/09/04 - 결제를 위한 데이터 - by 1-blue
  const paymentData = useRecoilValue(stateService.paymentService.productToPayment);

  // 2022/09/04 - iamport를 이용한 결제 - by 1-blue
  const onPayment = useCallback(
    (pg: &quot;kakaopay&quot; | &quot;tosspay&quot;) =&gt; () =&gt; {
      if (!process.env.NEXT_PUBLIC_IAMPORT_CODE)
        return toast.error(&quot;iamport의 가맹점 식별코드가 없습니다.&quot;);
      if (!paymentData) {
        toast.error(
          &quot;결제할 상품의 정보가 존재하지 않습니다. 메인 화면으로 이동됩니다.&quot;
        );
        return router.push(&quot;/&quot;);
      }

      // iamport를 사용하기 위해 가맹점 식별코드 등록
      window.IMP.init(process.env.NEXT_PUBLIC_IAMPORT_CODE);

      window.IMP.request_pay(
        { ...paymentData, pg, pay_method: &quot;card&quot; },
        async (rsp) =&gt; {
          if (
            !rsp.buyer_name ||
            !rsp.buyer_addr ||
            !rsp.paid_amount ||
            !rsp.buyer_email ||
            !rsp.buyer_tel ||
            !rsp.pg_provider
          )
            return toast.warning(&quot;결제에 필요한 데이터가 부족합니다.&quot;);

          if (rsp.success) {
            try {
              // &gt;&gt;&gt; 결제 완료 DB 저장 ( 실제 결제라면 유효성 검사를 실시하고 DB에 저장해야함 )
              await apiService.orderService.apiCreateOrder({
                // 각 상품의 개별 결제 데이터
                singleData: rsp.custom_data.singleData,
                // 해당 결제의 공통 결제 데이터
                orderData: {
                  name: rsp.buyer_name,
                  address: rsp.buyer_addr,
                  residence: rsp.custom_data.residence,
                  message: rsp.custom_data.message,
                  amount: rsp.paid_amount,
                  email: rsp.buyer_email,
                  phone: rsp.buyer_tel,
                  provider: rsp.pg_provider,
                },
              });

              toast.success(
                &quot;결제가 완료되었습니다. 2초뒤에 결제내역 페이지로 이동합니다.&quot;,
                { autoClose: 2000 }
              );

              setTimeout(() =&gt; router.push(&quot;/information/order&quot;), 2000);
            } catch (error) {
              console.error(&quot;error &gt;&gt; &quot;, error);

              if (error instanceof AxiosError) {
                toast.error(error.response?.data.message);
              } else {
                toast.error(&quot;알 수 없는 문제로 인해 결제에 실패했습니다.&quot;);
              }
            }
          } else {
            toast.error(&quot;결제에 실패했습니다. &quot; + rsp.error_msg, {
              autoClose: 2000,
            });
          }
        }
      );
    },
    [router, paymentData]
  );

  return (
    &lt;&gt;
      &lt;HeadInfo
        title=&quot;BleShop - 결제&quot;
        description=&quot;BleShop의 결제 페이지입니다.&quot;
      /&gt;

      &lt;article className=&quot;pt-4 space-y-4&quot;&gt;
        &lt;Nav.TitleNav title=&quot;돌아가기&quot; /&gt;

        &lt;Support.Background className=&quot;space-y-2 sm:space-y-4&quot; hasPadding&gt;
          &lt;Support.Title text=&quot;결제&quot; /&gt;

          &lt;ul className=&quot;space-y-2 sm:space-y-4&quot;&gt;
            &lt;li className=&quot;flex flex-col&quot;&gt;
              &lt;Support.SubTitle text=&quot;토스로 결제&quot; /&gt;
              &lt;Tool.Button
                type=&quot;button&quot;
                onClick={onPayment(&quot;tosspay&quot;)}
                className=&quot;bg-blue-400 p-2 rounded-md text-white sm:text-lg font-bold hover:bg-blue-500 focus:outline-none focus:bg-blue-500 transition-colors&quot;
                text=&quot;toss&quot;
              /&gt;
            &lt;/li&gt;
            &lt;li className=&quot;flex flex-col&quot;&gt;
              &lt;Support.SubTitle text=&quot;카카오페이로 결제&quot; /&gt;
              &lt;Tool.Button
                type=&quot;button&quot;
                onClick={onPayment(&quot;kakaopay&quot;)}
                className=&quot;bg-yellow-300 p-2 rounded-md text-black sm:text-lg font-bold hover:bg-yellow-400 focus:outline-none focus:bg-yellow-400 transition-colors&quot;
                text=&quot;kakaopay&quot;
              /&gt;
            &lt;/li&gt;
          &lt;/ul&gt;
        &lt;/Support.Background&gt;
      &lt;/article&gt;
    &lt;/&gt;
  );
};

export default Payment;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[🧺 최근 본 상품 기능]]></title>
            <link>https://velog.io/@1-blue/%EC%B5%9C%EA%B7%BC-%EB%B3%B8-%EC%83%81%ED%92%88-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@1-blue/%EC%B5%9C%EA%B7%BC-%EB%B3%B8-%EC%83%81%ED%92%88-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Sun, 11 Sep 2022 09:31:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/a5c9f29f-212f-4f02-a75f-e9adf88620a4/image.gif" alt=""></p>
<blockquote>
<p><code>next.js</code>와 <code>localStorage</code>를 이용했습니다.</p>
</blockquote>
<h2 id="기능-구현에-대한-구상">기능 구현에 대한 구상</h2>
<p>처음에 이 기능을 구현해야겠다고 생각했을 때는 로그인한 유저가 특정 상품의 페이지에 들어갔을 경우마다 <code>DB</code>에 기록 및 업데이트해서 최근에 접근한 상품이 무엇인지 알아내는 방법으로 구현하면 될 것으로 생각했습니다.
그래서 추가할 코드가 많고 시간이 좀 걸릴거라고 생각해서 다른 기능을 모두 구현하고 마지막에 구현하려고 미뤄놨던 기능입니다.</p>
<p>하지만 막상 구현하려고 찾아보니 생각보다 간단하게 구현이 가능했습니다.
바로 <code>localStorage</code>를 이용하는 방법입니다. 굳이 <code>DB</code>에 저장할 만큼의 정보도 아니고, 노출되어도 크게 상관없고, 삭제되어도 상관없는 정보면서, 큰 용량을 차지하지 않기 때문에 <code>localStorage</code> or <code>sessionStorage</code>를 사용하는 게 좋은 방법이라고 느꼈습니다.</p>
<h2 id="기능-구현">기능 구현</h2>
<p>일단 <code>localStorage</code>에 넣을 값은 식별자, 대표 이미지 경로, 이름 3가지로 정했고, 이름은 <code>watched</code>입니다.
그리고 기존에 봤던 상품이라면 순서만 바꾸고, 새로 본 상품이라면 추가하는 방법으로 구현했습니다.</p>
<ul>
<li><code>localStorage</code>에 배열이나 객체를 넣을 경우에는 강제로 <code>String</code>으로 변경시켜서 넣기 때문에 <code>JSON.stringify()</code>를 사용해주지 않으면 <code>[Object]</code>같은 형식으로 데이터가 들어가게 됩니다.
따라서 넣고 가져올 때는 <code>JSON.stringify()</code>와 <code>JSON.parse()</code>를 사용하고 있습니다.</li>
</ul>
<pre><code class="language-tsx">export type RecentProduct = {
  idx: number;
  photo: string;
  name: string;
};

useEffect(() =&gt; {
  if (!product) return;

  const previousWatched: RecentProduct[] = JSON.parse(
    localStorage.getItem(&quot;watched&quot;) || &quot;[]&quot;
  );
  if (!Array.isArray(previousWatched)) return;

  let currentWatched: RecentProduct[] = [];
  const targetIndex = previousWatched.findIndex(
    (watched) =&gt; watched.idx === product.idx
  );

  // 처음 보는 상품이라면
  if (targetIndex === -1) {
    const { idx, photo, name, ...rest } = product;
    currentWatched = [{ idx, photo, name }, ...previousWatched];
  }
  // 최근에 본적이 있는 상품이라면
  else {
    const target = previousWatched.splice(targetIndex, 1);
    currentWatched = [...target, ...previousWatched];
  }

  // 목록이 10개가 넘으면 자르기
  if (currentWatched.length &gt; 10) currentWatched.pop();

  localStorage.setItem(&quot;watched&quot;, JSON.stringify(currentWatched));
}, [product]);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[getServerSideProps의 쿠키 문제 해결 방법]]></title>
            <link>https://velog.io/@1-blue/getServerSideProps%EC%9D%98-%EC%BF%A0%ED%82%A4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@1-blue/getServerSideProps%EC%9D%98-%EC%BF%A0%ED%82%A4-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 03 Sep 2022 11:21:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><code>next-auth</code>, <code>axios</code>, <code>next.js</code>를 사용한 프로젝트입니다.</p>
</blockquote>
<ul>
<li><a href="https://velog.io/@1-blue/%EC%9D%B8%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84-next-auth">next-auth 세팅 방법</a></li>
</ul>
<h2 id="😕-사전-지식">😕 사전 지식</h2>
<p><code>next.js</code>의 <code>next-auth</code>와 <code>getServerSideProps()</code>를 사용하면서 발생한 문제입니다.</p>
<p><code>next-auth</code>의 <code>Credentials</code>방식 ( <code>id</code>와 <code>password</code>를 이용한 인증 방식 )을 사용해서 구현했고, 서버측에서는 아래와 같이 <code>getSession()</code>을 사용해서 로그인한 유저의 데이터를 받아올 수 있습니다.</p>
<ul>
<li><code>/api/user/me.ts</code><pre><code class="language-ts">// 임시로 만든 api ( 나의 상세 정보를 요청한다고 가정 )
import { getSession } from &quot;next-auth/react&quot;;
</code></pre>
</li>
</ul>
<p>// type
import type { NextApiRequest, NextApiResponse } from &quot;next&quot;;</p>
<p>export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { method } = req;
  // &quot;getSession({ req })&quot;와 같은 형태로 요청하면 본인이 지정한 데이터를 받아올 수 있습니다.
  // 여기서 만약 &quot;next-auth.session-token&quot;의 이름을 가진 쿠키가 존재하지 않는다면 서버에서 비로그인 상태로 인식하게 됩니다.
  const session = await getSession({ req });</p>
<p>  if (!session || !session.user) return res.status(403).json({ message: &quot;접근 권한이 없습니다.&quot; });</p>
<p>  const userIdx = session.user.idx;</p>
<p>  try {
    // ... 로그인한 유저의 상세 데이터를 찾는 코드 작성
  } catch (error) {
    console.error(error);</p>
<pre><code>return res.status(500).json({ message: &quot;서버측 에러입니다. 잠시후에 다시 시도해주세요!&quot; });</code></pre><p>  }
}</p>
<pre><code>
`getSession()`이 로그인한 유저의 데이터를 가져오는 방법은 브라우저에서 보내주는 세션 쿠키를 이용해서 유저를 식별합니다.
하지만 세션 쿠키가 없다면 로그인하지 않은 유저라고 인식하게 됩니다.
이 부분을 먼저 이해하고 넘어가야 문제의 원인을 파악할 수 있습니다.

## 🤔 문제 원인
프론트에서 `axios`를 이용해서 백엔드로 특정 요청을 할때 쿠키를 같이 넘겨주는 방법은 `withCredentials`을 `true`로 세팅하면 됩니다.
그렇게 하면 프론트에서 자동으로 쿠키를 첨부에서 서버로 요청을 보내게 됩니다.

```ts
// &quot;axios&quot;의 인스턴스를 만들어서 공통 세팅을 하고 모든 요청을 인스턴스를 통해서 처리하도록 만들었습니다.
export const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_FRONT_URL + &quot;/api&quot;,
  withCredentials: true,
  timeout: 10000,
});</code></pre><p>하지만 <code>next.js</code>의 <code>SSR</code>을 위한 <code>getServerSideProps()</code>를 사용하는 경우에는 실행되는 위치가 프론트측(브라우저)이 아닌 서버측에서 실행되고 실행 결과를 이용한 완성된 페이지를 프론트로 넘겨주게 됩니다.</p>
<ul>
<li><p><code>CSR</code> 방식 실행 순서</p>
<ul>
<li>브라우저 <code>axios</code> 요청 --&gt; 쿠키 첨부 --&gt; 서버에 전달 --&gt; 결과 반환 --&gt; 받은 결괏값으로 브라우저 레이아웃 완성 --&gt; 렌더링</li>
</ul>
</li>
<li><p><code>SSR</code> 방식 실행 순서</p>
<ul>
<li>서버에서 <code>getServerSideProps()</code> 실행 --&gt; 결과로 페이지 완성 --&gt; 완성된 페이지 반환 --&gt; 브라우저에서 완성된 페이지 렌더링</li>
</ul>
</li>
</ul>
<p><code>getServerSideProps()</code>는 서버에서 실행되기 때문에 브라우저에 저장되어 있는 쿠키를 알지 못합니다. 따라서 요청할 때도 쿠키를 같이 보내지 못하게 되기 때문에 서버측에서는 항상 비로그인상태로 인식하게 되는 문제를 찾았습니다.</p>
<h2 id="🙂-해결-방법">🙂 해결 방법</h2>
<p>자동적으로 쿠키를 첨부해주지 않으니 아래와 같이 직접 쿠키를 동봉해서 서버에 요청하는 방식으로 해결했습니다.</p>
<pre><code class="language-tsx">// type
import type { GetServerSideProps, GetServerSidePropsContext, NextPage } from &quot;next&quot;;
// &quot;Basket&quot;, &quot;ApiGetBasketProductsResponse&quot;은 그냥 특정 타입 ( 중요한 부분이 아니라 생략 )

type Props = {
  baskets: Basket[];
};

const BasketComponent: NextPage&lt;Props&gt; = ({ baskets }) =&gt; {
  return (
    &lt;&gt;
      // ... 생략
      // &quot;baskets&quot; 데이터를 이용한 화면 구성
    &lt;/&gt;
  )
}

export default BasketComponent;

export const getServerSideProps: GetServerSideProps&lt;Props&gt; = async (context: GetServerSidePropsContext) =&gt; {
  /**
   * SSR 요청일 경우에는 서버에서 페이지를 완성한 후에 반환하는 방식임
   * 하지만 서버에서는 클라이언트(브라우저)에 대한 정보를 알 수 없으니 &quot;cookie&quot;(세션 쿠키)를 이용해서 유저를 식별함
   * 컴포넌트에 내부에서 하는 요청들은 &quot;axios&quot;의 &quot;withCredentials&quot; 옵션을 통해서 자동으로 쿠키를 넣어서 전달하지만
   * &quot;getServerSideProps()&quot;에서 전달하는 요청은 서버에서 서버로 보내는 요청이므로 쿠키의 존재를 몰라서 첨부하지 않고 요청을 보냄
   *
   * 따라서 직접적으로 쿠키를 넣어주는 행동을 하지 않으면 요청에 쿠키가 들어가지 않아서 서버가 어떤 유저인지 식별할 수 없기에 직접적으로 쿠키를 넣어주는 구문임
   */
  let { cookie } = context.req.headers;
  cookie = cookie ? cookie : &quot;&quot;;
  axiosInstance.defaults.headers.Cookie = cookie;

  let baskets: Basket[] = [];

  try {
    // &quot;axios&quot;를 이용한 api 요청
    const { data } = await axiosInstance.get&lt;ApiGetBasketProductsResponse&gt;(`/products/basket`);
    baskets = data.baskets;
  } catch (error) {
    console.error(&quot;getServerSideProps basket/wish &gt;&gt; &quot;, error);
  } finally {
    /**
     * &quot;axios&quot;에 등록한 특정 유저의 쿠키 제거
     * ( 브라우저에서의 요청이 아니라 서버에서의 요청이므로 다른 유저도 같은 서버를 사용하기에 쿠키가 공유되는 문제가 생김 )
     */
    axiosInstance.defaults.headers.Cookie = &quot;&quot;;
  }

  return {
    props: {
      baskets,
    },
  };
};</code></pre>
<p>만약 <code>typescript</code>를 사용한다면 <code>axiosInstance.defaults.headers.Cookie</code>에서 타입 에러가 발생하기 때문에 아래와 같은 파일을 생성하면 해결됩니다.</p>
<ul>
<li><a href="https://github.com/1-blue/bleshop/@types/axios.d.ts"><code>@types/axios.d.ts</code></a><pre><code class="language-ts">/**
* &quot;getServerSideProps&quot;에서 로그인한 유저에 관한 요청을 하는 경우에 &quot;cookie&quot;를 넣어줘야 어떤 유저의 요청인지 알 수 있기 때문에 재정의
*/
import &quot;axios&quot;;
</code></pre>
</li>
</ul>
<p>declare module &quot;axios&quot; {
  export interface HeadersDefaults {
    Cookie?: string;
  }
}</p>
<pre><code></code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Recoil과 IntersectionObserver를 이용한 무한 스크롤링]]></title>
            <link>https://velog.io/@1-blue/Recoil%EA%B3%BC-IntersectionObserver%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%A7%81</link>
            <guid>https://velog.io/@1-blue/Recoil%EA%B3%BC-IntersectionObserver%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%A7%81</guid>
            <pubDate>Mon, 22 Aug 2022 09:10:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/6bd0c391-5e6c-4f2e-ba57-3c10746b2ca3/image.gif" alt=""></p>
<blockquote>
<p><code>next.js</code>, <code>prisma</code>, <code>axios</code>를 사용하는 프로젝트입니다.</p>
</blockquote>
<blockquote>
<p><a href="https://velog.io/@1-blue/Intersection-Observer-API">Intersection Observer API</a>의 사용법에 대한 포스트</p>
</blockquote>
<h2 id="🤝-무한-스크롤링-구현을-위해-선택한-것">🤝 무한 스크롤링 구현을 위해 선택한 것</h2>
<p>기존에 무한 스크롤링을 구현할 때는 항상 스크롤 이벤트를 사용해서 구현했습니다.
하지만에 최근에 공부를 하면서 <code>Intersection Observer API</code>의 존재에 대해 알게 되었고, 응용하면 무한 스크롤링을 구현할 수 있겠다고 생각해서 선택했습니다.
( 아직 어떤 원리로 동작하는지, 어떤 상황에 어떤 방법을 선택해야하는지에 대한 이해도는 없습니다. )</p>
<p>상태 관리 라이브러리로는 <code>recoil</code>을 선택했기에 사용했습니다.
하지만 현재 구현한 방식에는 굳이 <code>recoil</code>을 사용하지 않아도 구현할 수 있기에 아직 사용하는 이유나 사용의 편의성은 못느꼈습니다. 그래도 익숙해지기 위해서 사용했습니다.</p>
<h2 id="😕-처음-시도">😕 처음 시도</h2>
<p>처음 생각에는 상품 데이터를 15개씩 받아오는 비동기 <code>selector</code> ( 이하 <code>productsState</code> )와 마지막 상품의 식별자를 갖는 <code>selector</code> ( 이하 <code>productLastIdxState</code> )를 만들어서 사용하면 된다고 생각했습니다.</p>
<p><code>productsState</code>에서 상품들을 화면에 렌더링하고 <code>IntersectionObserver</code>를 이용해서 마지막 상품을 감시해서 뷰포트 내부에 들어온다면 <code>productLastIdxState</code>값을 이용해서 다음 상품들을 요청하도록 구조를 잡았습니다.</p>
<p>하지만 이렇게 했을 경우 발생하는 문제가 새로운 데이터를 받아오고 나서 기존 데이터를 유지할 수 없다는 문제가 생겼습니다.
상품들을 추가로 10개를 받아왔을 경우에 기존의 15개에 대한 데이터는 버리고 10개만 유지하게 되는 문제가 발생해서 다른 방법을 생각해봤습니다.</p>
<p>이후에 많은 삽질을 했지만 그 내용은 생략하겠습니다.</p>
<h2 id="🙂-실제-구현-방법">🙂 실제 구현 방법</h2>
<p><code>atom</code>( 이하 <code>productsState</code> )을 이용해서 상품들의 데이터를 갖고 컴포넌트에서 상품들의 데이터를 요청하면 누적해서 <code>productsState</code>에 추가해주는 방법으로 구현했습니다.</p>
<ul>
<li><p><a href="https://github.com/1-blue/bleshop/blob/master/src/states/index.ts"><code>state</code></a></p>
<pre><code class="language-ts">export const productsState = atom&lt;Product[]&gt;({
key: &quot;productsState - &quot; + v1(),![](https://velog.velcdn.com/images/1-blue/post/796ccff3-d79a-4731-a73c-891aef05db97/image.gif)

default: [],
});
/**
* 2022/08/22 - 최근 상품들의 마지막 상품의 식별자 - by 1-blue
*/
export const productLastIdxState = selector&lt;number&gt;({
key: &quot;productLastIdxState&quot;,
get: ({ get }) =&gt; {
  const products = get(productsState);

  if (products.length === 0) return -1;

  return products[products.length - 1].idx;
},
set: ({ set }) =&gt; {
  set(productsState, []);
},
});</code></pre>
</li>
<li><p><a href="https://github.com/1-blue/bleshop/tree/master/src/components/Main/Products.tsx">사용 컴포넌트</a></p>
<pre><code class="language-tsx">// 상품 요청 개수
const limit: LIMIT = 15;
</code></pre>
</li>
</ul>
<p>// 2022/08/22 - 화면에 랜더링할 상품들 - by 1-blue
const [products, setProducts] = useRecoilState(productsState);
// 2022/08/22 - 가장 최근에 요청한 상품의 마지막 식별자 ( 해당 식별자를 기준으로 다음 상품들의 데이터를 요청 ) - by 1-blue
const [productLastIdx, setProductLastIdx] = useRecoilState(productLastIdxState);
// 2022/08/22 - 마지막 상품의 ref - by 1-blue
// 원래 ref는 주로 &quot;useRef()&quot;를 이용하지만, 현재 상황에서는 상품을 다시 요청할 때마다 ref가 변경되고 그에 맞게 리렌더링을 해야 하기 때문에 &quot;useState()&quot;를 사용했습니다.
const [lastProductRef, setLastProductRef] = useState&lt;HTMLLIElement | null&gt;(null);</p>
<p>// 2022/08/22 - 처음 한번 상품들의 데이터 요청 - by 1-blue
useEffect(() =&gt; {
  (async () =&gt; {
    // 이전에 상품 데이터들을 받아왔을 경우를 대비해서 미리 초기화
    setProductLastIdx(-1);</p>
<pre><code>// axios로 상품들을 요청하는 메서드
const { data: { products } } = await apiService.productService.apiGetProducts({ limit, lastIdx: -1 });

// 받아온 상품들을 atom에 넣음
setProducts(products);</code></pre><p>  })();
}, [setProducts, setProductLastIdx]);</p>
<p>// 2022/08/22 - observer로 인해 실행할 이벤트 함수 ( 제일 마지막 상품이 뷰포트에 들어오면 실행할 이벤트 함수 ) - by 1-blue
const onScroll = useCallback(
  async ([{ isIntersecting }]: IntersectionObserverEntry[]) =&gt; {
    if (!lastProductRef) return;</p>
<pre><code>// 지정한 엘리먼트가 &quot;threshold&quot;만큼을 제외하고 뷰포트에 들어왔다면 실행
if (isIntersecting) {
  const { data: { products } } = await apiService.productService.apiGetProducts({ limit, lastIdx: productLastIdx });

  // 기존 데이터 + 새로운 데이터
  setProducts((prev) =&gt; [...prev, ...products]);
}</code></pre><p>  },
  [lastProductRef, productLastIdx, setProducts]
);</p>
<p>// 2022/08/22 - observer 등록 ( 제일 마지막 상품이 뷰포트에 들어오면 실행할 이벤트 함수를 등록 ) - by 1-blue
useEffect(() =&gt; {
  if (!lastProductRef) return;
  if (products.length % limit !== 0) return;</p>
<p>  let observer = new IntersectionObserver(onScroll, {
    threshold: 0.1,
    rootMargin: &quot;20px&quot;,
  });
  observer.observe(lastProductRef);</p>
<p>  return () =&gt; observer?.disconnect();
}, [lastProductRef, onScroll, products]);</p>
<p>// 나머지 레이아웃과 주제에 맞지 않는 구현부분은 제외함</p>
<pre><code>
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[인증 로직 구현 ( next-auth )]]></title>
            <link>https://velog.io/@1-blue/%EC%9D%B8%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84-next-auth</link>
            <guid>https://velog.io/@1-blue/%EC%9D%B8%EC%A6%9D-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84-next-auth</guid>
            <pubDate>Sat, 13 Aug 2022 05:03:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/daeb75ec-0357-4d0b-bf9e-4ed8260f75dd/image.gif" alt=""></p>
<blockquote>
<p>본 게시글은 <code>next.js</code>, <code>next-auth</code>, <code>prisma</code>, <code>tailwindcss</code>를 사용하는 것을 기반으로 작성됩니다.</p>
</blockquote>
<h2 id="회원가입">회원가입</h2>
<p>회원가입은 브라우저에서 <code>id</code>, <code>password</code>, <code>email</code>, <code>name</code>, <code>phone</code>, <code>photo</code>를 입력받아서 <code>prisma</code>를 이용해서 DB에 추가하는 비교적 단순한 부분이기 때문에 설명 대신 깃헙 링크로 대체하겠습니다.</p>
<ul>
<li>프론트 엔드 부분: <a href="https://github.com/1-blue/bleshop/tree/master/src/pages/signup.tsx"><code>/src/pages/signup.tsx</code></a></li>
<li>백엔드 부분: <a href="https://github.com/1-blue/bleshop/tree/master/src/pages/api/signup.tsx"><code>/src/pages/api/signup.tsx</code></a></li>
</ul>
<h2 id="로그인">로그인</h2>
<p>로그인 로직을 구현하기 위해서 <code>next-auth</code>를 사용했습니다.
<code>next-auth</code>는 보다 쉽게 인증 로직을 구현하기 위한 <code>Next.js</code> 전용 라이브러리입니다.
<code>google</code>, <code>kakao</code> 등이 <code>OAuth</code>도 쉽게 구현할 수 있지만, <code>id</code>와 <code>password</code>를 이용한 로그인을 구현하는 것이 목표이기 때문에 <code>Credentials</code> 방식을 선택했습니다.</p>
<h3 id="1-세팅-코드-예시">1. 세팅 코드 예시</h3>
<ul>
<li><a href="https://github.com/1-blue/bleshop/tree/master/src/pages/api/auth/%5B...nextauth%5D.ts"><code>src/pages/api/auth/[...nextauth].ts</code></a><pre><code class="language-tsx">import prisma from &quot;@src/prisma&quot;;
import NextAuth from &quot;next-auth&quot;;
import CredentialsProvider from &quot;next-auth/providers/credentials&quot;;
import bcrypt from &quot;bcrypt&quot;;
</code></pre>
</li>
</ul>
<p>export default NextAuth({
  providers: [
    // 인증 방식 선택 ( 현재는 &quot;id&quot; + &quot;password&quot; )
    CredentialsProvider({
      // 여기서 입력한 이름을 &quot;signIn(이름)&quot; 형태로 사용
      name: &quot;Credentials&quot;,
      // 여기서 작성한 타입 그대로 아래 &quot;authorize()&quot;의 &quot;credentials&quot;의 타입 적용
      // 또한 &quot;next-auth&quot;에서 생성해주는 로그인창에서 사용 ( <a href="http://localhost:3000/api/auth/signin">http://localhost:3000/api/auth/signin</a> )
      credentials: {
        id: {
          label: &quot;아이디&quot;,
          type: &quot;text&quot;,
          placeholder: &quot;아이디를 입력하세요.&quot;,
        },
        password: {
          label: &quot;비밀번호&quot;,
          type: &quot;password&quot;,
          placeholder: &quot;비밀번호를 입력하세요.&quot;,
        },
      },</p>
<pre><code>  // 로그인 유효성 검사
  // 로그인 요청인 &quot;signIn(&quot;credentials&quot;, { id, password })&quot;에서 넣어준 &quot;id&quot;, &quot;password&quot;값이 그대로 들어옴
  async authorize(credentials, req) {
    if (!credentials)
      throw new Error(&quot;잘못된 입력값으로 인한 오류가 발생했습니다.&quot;);

    const { id, password } = credentials;

    const exUser = await prisma.user.findUnique({
      where: { id },
      include: { photo: true },
    });
    if (!exUser) throw new Error(&quot;존재하지 않는 아이디입니다.&quot;);

    const result = await bcrypt.compare(password, exUser.password);
    if (!result) throw new Error(&quot;비밀번호가 불일치합니다.&quot;);

    // 반환하는 값중에 name, email, image만 살려서 &quot;session.user&quot;로 들어감
    return exUser;
  },
}),</code></pre><p>  ],
  callbacks: {
    async jwt({ token }) {
      return token;
    },
    // 세션에 로그인한 유저 데이터 입력
    async session({ session }) {
      const exUser = await prisma.user.findUnique({
        where: { name: session.user?.name },
        select: {
          idx: true,
          id: true,
          name: true,
          email: true,
          phone: true,
          address: true,
          photo: {
            select: {
              path: true,
            },
          },
        },
      });</p>
<pre><code>  // 로그인한 유저 데이터 재정의
  // 단, 기존에 &quot;user&quot;의 형태가 정해져있기 때문에 변경하기 위해서는 타입 재정의가 필요함
  session.user = exUser;

  // 여기서 반환한 session값이 &quot;useSession()&quot;의 &quot;data&quot;값이 됨
  return session;
},</code></pre><p>  },
  secret: process.env.SECRET,
});</p>
<pre><code>
+ [`/src/pages/login.tsx`](https://github.com/1-blue/bleshop/tree/master/src/pages/login.tsx)
```tsx
// 다른 부분은 전부 생략하고 로그인 요청 부분만 살림

// 2022/08/12 - 로그인 요청 - by 1-blue
const onSubmit = useCallback(
  async (body: ApiLogInBody) =&gt; {
    try {
      // body에 로그인을 위해 입력한 id와 password가 들어있음
      const result = await signIn(&quot;credentials&quot;, {
        // 로그인 실패 시 새로고침 여부
        redirect: false,
        id: body.id,
        password: body.password,
        // ...body
      });

      // &quot;authorize()&quot;에서 날린 &quot;throw new Error(&quot;&quot;)&quot;가 &quot;result.error&quot;로 들어옴
      if (result?.error) return toast.error(result.error);

      // 만약 에러가 없다면 로그인 성공
      // 세션 쿠키가 생성됨
      toast.success(&quot;로그인 성공. 메인 페이지로 이동합니다.&quot;);
      router.push(&quot;/&quot;);
    } catch (error) {
      console.error(&quot;error &gt;&gt; &quot;, error);

      toast.error(
        &quot;알 수 없는 에러로 로그인에 실패했습니다. 잠시후에 다시 시도해주세요!&quot;
      );
    }
  },
  [router]
);</code></pre><h3 id="2-프론트-사용-예시">2. 프론트 사용 예시</h3>
<pre><code class="language-tsx">import type { NextPage } from &quot;next&quot;;
import { useSession } from &quot;next-auth/react&quot;;

const Home: NextPage = () =&gt; {
  const { data, status } = useSession();

  // status는 &quot;authenticated&quot; | &quot;loading&quot; | &quot;unauthenticated&quot;를 가짐 ( 로그인 여부 판단에 사용 )
  // data는 &quot;expires&quot;와 &quot;user&quot;( &quot;callbacks&quot;의 &quot;session()&quot;에서 반환한 값 )를 가짐

  return (
    &lt;&gt;
      &lt;h1&gt;Home!!!&lt;/h1&gt;
    &lt;/&gt;
  );
};

export default Home;</code></pre>
<h3 id="3-user-타입-재정의">3. user 타입 재정의</h3>
<ul>
<li><a href="https://github.com/1-blue/bleshop/tree/master/src/@types/next-auth.d.ts"><code>@types/next-auth.d.ts</code></a><pre><code class="language-ts">import NextAuth from &quot;next-auth&quot;;
</code></pre>
</li>
</ul>
<p>// type
import type { UserWithPhoto } from &quot;@src/types&quot;;</p>
<p>// 여기서 재정의한 타입이 &quot;session.user&quot;의 타입으로 정의됨
declare module &quot;next-auth&quot; {
  interface Session {
    user: {
      idx: number;
      id: string;
      name: string;
      // ...
    }
  }
}</p>
<pre><code>
### 4. 접근 제한 ( middleware )
&gt; 로그인과 회원가입 페이지에 접근할 때만 이미 로그인했는지 여부를 판단하고 접근을 제한하는 코드입니다.

+ [`/src/middleware.ts`](https://github.com/1-blue/bleshop/tree/master/src/middleware.ts)
```ts
import type { NextRequest, NextFetchEvent } from &quot;next/server&quot;;
import { NextResponse } from &quot;next/server&quot;;
import { getToken } from &quot;next-auth/jwt&quot;;

const secret = process.env.SECRET;

export async function middleware(req: NextRequest, event: NextFetchEvent) {
  // 로그인 했을 경우에만 존재함 ( &quot;next-auth.session-token&quot; 쿠키가 존재할 때 )
  const session = await getToken({ req, secret, raw: true });
  const { pathname } = req.nextUrl;

  // 2022/08/13 - 로그인/회원가입 접근 제한 - by 1-blue
  if (pathname.startsWith(&quot;/login&quot;) || pathname.startsWith(&quot;/signup&quot;)) {
    if (session) {
      return NextResponse.redirect(new URL(&quot;/&quot;, req.url));
    }
  }
}

export const config = {
  matcher: [&quot;/login&quot;, &quot;/signup&quot;],
};</code></pre><h2 id="추가-oauth-로그인--kakao-google-">(추가) OAuth 로그인 ( kakao, google )</h2>
<p>처음에는 <code>OAuth</code>를 고려하지 않았기 때문에 유저 모델의 재정의가 필요합니다.
<code>Credentials</code> 방식의 경우에는 반드시 <code>id</code>, <code>password</code> 등이 필요하지만 <code>OAuth</code>의 경우에는 다른 페이지에서 인증을 대신 처리해주기 때문에 필요치 않습니다. 따라서 아래와 같이 유저 모델을 변경했습니다.</p>
<pre><code>enum Provider {
  Credentials
  KAKAO
  GOOGLE
}

// 유저
model User {
  idx      Int       @id @default(autoincrement())
  name     String
  email    String    @unique
  id       String?   @unique
  password String?
  phone    String?   @unique
  photo    String?
  role     Role?     @default(USER)
  provider Provider? @default(Credentials)

  // ... 나머지 생략
}</code></pre><p>로그인해서 받는 필수 정보인 <code>name</code>, <code>email</code>만 필수로 만들고 나머지는 옵셔널로 변경했습니다.
또한 어떤 방식의 로그인인지 구분하기 위해서 <code>provider</code> 컬럼을 추가했습니다.
그리고 아래와 같이 사용할 로그인 방식을 추가하고 <code>callbacks</code>에서 유저를 등록하도록 코드를 작성합니다.</p>
<pre><code class="language-tsx">export default NextAuth({
  providers: [
    // ... Credentials 생략

    // 카카오 로그인
    KakaoProvider({
      clientId: process.env.KAKAO_ID,
      clientSecret: process.env.KAKAO_SECRET,
    }),

    // 구글 로그인
    googleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      // &quot;account.provider&quot;는 로그인을 요청할 때만 값이 존재하기 때문에
      // 로그인 요청을 하는 경우에 체크해서 첫 로그인이라면 등록합니다.

      // 카카오 로그인일 경우
      if (account?.provider === &quot;kakao&quot;) {
        const exUser = await prisma.user.findFirst({
          where: { provider: &quot;KAKAO&quot;, name: token.name!, email: token.email! },
        });

        // 등록된 유저가 아니라면 회원가입
        if (!exUser) {
          await prisma.user.create({
            data: {
              name: token.name!,
              email: token.email!,
              photo: token.picture,
              provider: &quot;KAKAO&quot;,
            },
          });
        }
      }
      // 구글 로그인일 경우
      if (account?.provider === &quot;google&quot;) {
        const exUser = await prisma.user.findFirst({
          where: { provider: &quot;GOOGLE&quot;, name: token.name!, email: token.email! },
        });

        // 등록된 유저가 아니라면 회원가입
        if (!exUser) {
          await prisma.user.create({
            data: {
              name: token.name!,
              email: token.email!,
              photo: token.picture,
              provider: &quot;GOOGLE&quot;,
            },
          });
        }
      }

      return token;
    },
    // 세션에 로그인한 유저 데이터 입력
    async session({ session }) {
      const exUser = await prisma.user.findFirst({
        where: { email: session.user.email },
        select: {
          idx: true,
          id: true,
          name: true,
          email: true,
          phone: true,
          role: true,
          photo: true,
          provider: true,
        },
      });

      session.user = exUser!;

      return session;
    },
  },

  // ... 나머지 생략
});</code></pre>
<p>위 로직을 작성하고 로그인 페이지에서 버튼을 만들고 클릭 시 <code>signIn(&quot;kakao&quot;)</code>와 같은 형태로 호출하기만 하면 완성입니다.</p>
<ul>
<li><a href="https://developers.kakao.com/console/app">카카오 비밀키 생성</a></li>
<li><a href="https://console.cloud.google.com/welcome">구글 비밀키 생성</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS - S3의 preSignedUrl 사용]]></title>
            <link>https://velog.io/@1-blue/AWS-S3%EC%9D%98-preSignedUrl-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@1-blue/AWS-S3%EC%9D%98-preSignedUrl-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 12 Aug 2022 04:21:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/559a7ea8-7db2-46c7-8ed8-07a558d4e84d/image.gif" alt=""></p>
<blockquote>
<p><code>next.js</code>, <code>prisma</code>, <code>tailwindcss</code>를 사용하는 프로젝트입니다.</p>
</blockquote>
<h2 id="😥-이미지-처리-로직의-한계">😥 이미지 처리 로직의 한계</h2>
<p>이전에 <code>AWS-S3</code>를 이용해서 이미지를 저장하는 경우에는 다음과 같은 순서로 처리했습니다.</p>
<ol>
<li>브라우저에서 서버로 이미지 전송</li>
<li>서버에서 받은 이미지와 <code>key</code> 들을 이용해서 <code>S3</code>에 이미지 업로드 요청</li>
<li><code>S3</code>에서 이미지 업로드 후 결과 반환</li>
<li>서버에서 받은 결과를 브라우저에 전달</li>
<li>브라우저에서 받은 결과로 업로드된 이미지를 화면에 렌더링</li>
</ol>
<p>위와 같은 순서로 처리할 때 항상 존재한 문제들은 두 가지 있습니다.</p>
<ol>
<li><code>next.js</code>의 <code>api</code>는 <code>1MB</code> 이상의 이미지를 처리하지 못함</li>
<li>전송하는데 많은 리소스가 낭비되는 이미지를 두 번 연속으로 처리하므로 많은 낭비가 발생</li>
</ol>
<p>브라우저에서 바로 <code>S3</code>로 이미지를 전송하면 문제가 해결되지 않냐고 생각할 수 있지만 <code>S3</code>에 접근하기 위해서는 몇 가지 <code>key</code>가 필요합니다. 그것을 브라우저에 저장하면 <code>key</code>가 누구에게나 노출되는 문제가 발생하기 때문에 가능하지만 사용하지 않았습니다.</p>
<h2 id="😮-presignedurl">😮 preSignedUrl</h2>
<p>위의 한계를 극복할 방법을 찾다가 알게 된 방법이 <code>preSignedUrl</code>입니다.
미리 서명된 URL으로 처리 로직은 아래와 같습니다.</p>
<ol>
<li>브라우저에서 이미지 업로드 요청을 서버에게 보냄 ( 이미지는 보내지 않고 요청만 보냄 )</li>
<li>서버에서 <code>S3</code>에 요청해서 일시적으로 이미지 업로드가 가능한 <code>URL</code>을 받음 ( 해당 <code>URL</code>을 <code>preSignedUrl</code>라고 부릅니다. )</li>
<li>서버에서 브라우저로 <code>preSignedUrl</code>을 전송함</li>
<li>브라우저에서 받은 <code>preSignedUrl</code>에 이미지를 첨부해서 보냄</li>
<li>문제없이 결과가 오면 이미지 업로드 완료이므로 이미지를 화면에 렌더링</li>
</ol>
<p><code>preSignedUrl</code>로 얻은 이점은 이전 방식의 두 가지 문제점을 모두 해결해줍니다.</p>
<h2 id="🧐-presingedurl-사용-방법">🧐 preSingedUrl 사용 방법</h2>
<p><code>S3</code>의 버킷 생성과 권한 부여 등은 생략하겠습니다.</p>
<ul>
<li><p>설치</p>
<pre><code class="language-bash">npm i aws-sdk</code></pre>
</li>
<li><p><code>.env</code> ( 환경변수 )</p>
<pre><code># 백엔드에서만 사용하기 때문에 &quot;NEXT_PUBLIC&quot; 붙일 필요 없음
</code></pre></li>
</ul>
<p>BLESHOP_AWS_REGION=ap-northeast-2 ( 본인이 설정한 지역으로 입력 )
BLESHOP_AWS_ACCESS_KEY=&lt;직접 입력&gt;
BLESHOP_AWS_SECRET_KEY=&lt;직접 입력&gt;</p>
<pre><code>
+ [`src/libs/s3.ts`](https://github.com/1-blue/bleshop/src/libs/s3.ts)
```ts
import AWS from &quot;aws-sdk&quot;;

AWS.config.update({
  region: process.env.BLESHOP_AWS_REGION,
  accessKeyId: process.env.BLESHOP_AWS_ACCESS_KEY,
  secretAccessKey: process.env.BLESHOP_AWS_SECRET_KEY,
});

// 버킷 정책에서 생성된 버전 날짜 그대로 가져와서 사용함
const S3 = new AWS.S3({ apiVersion: &quot;2012-10-17&quot;, signatureVersion: &quot;v4&quot; });

/**
 * &quot;이미지.확장자&quot;를 받아서 &quot;경로/이미지_시간.확장자&quot;으로 변경해주는 함수
 * @param name &quot;이미지.확장자&quot; 형태로 전송
 * @returns &quot;경로/이미지_시간.확장자&quot; 형태로 반환
 */
const getPhotoPath = (name: string) =&gt; {
  const [filename, ext] = name.split(&quot;.&quot;);

  return `photos/${process.env.NODE_ENV}/${filename}_${Date.now()}.${ext}`;
};

/**
 * &quot;preSignedURL&quot;을 생성하는 함수
 * @param name &quot;이미지.확장자&quot; 형태로 전송
 * @returns &quot;preSignedURL&quot;와 &quot;photoURL&quot;을 반환 ( &quot;photoURL&quot;은 정상적으로 완료 시 이미지 url )
 */
export const getSignedURL = (name: string) =&gt; {
  const photoURL = getPhotoPath(name);

  const preSignedURL = S3.getSignedUrl(&quot;putObject&quot;, {
    // 생성한 버킷이름 작성
    Bucket: &quot;bleshop&quot;,
    // 생성할 위치 및 파일명 작성 ( 현재는 &quot;photos/development/파일명_시간.확장자&quot; 형태임 )
    Key: photoURL,
    // URL 유효기간 ( 20초 )
    Expires: 20,
  });

  // preSignedURL과 미래에 생성될 이미지의 URL 반환
  return { preSignedURL, photoURL };
};</code></pre><ul>
<li>사용 예시 ( 코드를 모두 분리해서 필요한 부분만 합쳐서 새로 만들었습니다. )<pre><code class="language-tsx">import { useCallback, useState } from &quot;react&quot;;
import axios, { AxiosError } from &quot;axios&quot;;
import Image from &quot;next/image&quot;;
</code></pre>
</li>
</ul>
<p>// type
import type { ChangeEvent } from &quot;react&quot;;</p>
<p>type ApiGetUrlRespinse = { preSignedURL: string; photoURL: string };</p>
<p>/**</p>
<ul>
<li>현재 웹페이지의 이미지의 경로를 얻는 헬퍼 함수 ( aws-s3 )</li>
<li>@param path 후반부 이미지 경로</li>
<li>@returns 전체 이미지 경로</li>
<li>/
declare function combinePhotoUrl(path: string): string;</li>
</ul>
<p>const TestComponent = () =&gt; {
  const [photoUrl, setPhotoUrl] = useState&lt;string | null&gt;(null);</p>
<p>  const onUploadPhoto = useCallback(
    async (e: ChangeEvent<HTMLInputElement>) =&gt; {
      try {
        if (e.target.files?.length) {
          const photo = e.target.files[0];</p>
<pre><code>      // 해당 api에서는 이전에 &quot;preSignedURL()&quot;를 이용해서 값을 얻고 반환
      const {
        data: { preSignedURL, photoURL },
      } = await axios.get&lt;ApiGetUrlRespinse&gt;(
        `/api/photo?name=${photo.name}`
      );

      // S3로 이미지 생성 요청
      await axios.put(preSignedURL, photo, {
        headers: { &quot;Content-Type&quot;: photo.type },
      });

      // catch구문으로 넘어가지 않았으니 정상 작동
      setPhotoUrl(photoURL);
    }
  } catch (error) {
    console.error(error);

    if (error instanceof AxiosError) {
      // 예측 가능한 에러 ex) 이미지 용량 초과, 전송 시간 초과 등
    }
  }
},
[setPhotoUrl]</code></pre><p>  );</p>
<p>  return (
    &lt;&gt;
      <input type="file" accept="image/*" onChange={onUploadPhoto} /></p>
<pre><code>  // Next.js의 &quot;&lt;Image&gt;&quot;를 사용하기 위해서는 도메인을 등록이 필수
  {photoUrl &amp;&amp; (
    &lt;figure className=&quot;w-80 h-80 relative bg-black rounded-md&quot;&gt;
      &lt;Image
        layout=&quot;fill&quot;
        priority
        src={combinePhotoUrl(photoUrl)}
        className=&quot;object-contain&quot;
        alt=&quot;업로드한 이미지&quot;
      /&gt;
    &lt;/figure&gt;
  )}
&lt;/&gt;</code></pre><p>  );
};</p>
<p>export default TestComponent;</p>
<pre><code>
## AWS-S3 파일 이동 함수
현재 이미지 저장 방식은 다음과 같습니다.
1. 기본 이미지 저장 형태: `photos/모드/사용방식/이미지명.확장자`
2. 이미지 확정 전 방식: `/temporary`
3. 이미지 확정 후 방식: 이미지를 사용하는 형태에 따라 다름 ( `/user`, `/product` 등 )

예를 들어 회원가입하는 경우 회원가입 버튼을 누르기 전에는 `/temporary`에 이미지를 저장하고 회원가입 버튼을 누르고 유효성 검사 후 유저 생성이 확정되면 `/user`로 이미지를 옮깁니다.

이렇게 하면 임시 저장 이미지와 실제 사용하는 이미지를 구분할 수 있고, 임시 저장 이미지도 보관할 수 있습니다.
아래 코드는 위 로직을 쉽게 적용할 수 있도록 이미지 이동을 도와주는 헬퍼 함수입니다.

```ts
/**
 * 2022/08/14 - S3 이미지 제거 - by 1-blue
 * @param photo 이미지 파일 이름
 * @returns
 */
export const deletePhoto = (photo: string) =&gt;
  S3.deleteObject(
    {
      Bucket: &quot;bleshop&quot;,
      Key: photo,
    },
    (error, data) =&gt; {
      if (error) console.error(&quot;S3 이미지 제거 error &gt;&gt; &quot;, error);
    }
  ).promise();

/**
 * 2022/08/14 - S3 이미지 복사 - by 1-blue
 * @param originalSource: 이미지 파일 이름, location: 이미지 복사 위치
 * @returns
 */
export const copyPhoto = (originalSource: string, location: PhotoKinds) =&gt; {
  // 이미지 저장 형태 : photos/모드/사용방식/이미지명.확장자
  // 모드: production or development
  // 사용 방식: temporary or remove or product or review 등
  // 따라서 두 번째 &quot;/&quot;를 찾아서 다음 &quot;/&quot;까지 내용을 바꾸면 됨 ( temporary -&gt; product )

  let Key: unknown = null;
  const firstSlashIndex = originalSource.indexOf(&quot;/&quot;);
  const secondSlashIndex = originalSource.indexOf(&quot;/&quot;, firstSlashIndex + 1);

  switch (location) {
    // 이미지 제거
    case &quot;remove&quot;:
      Key =
        originalSource.slice(0, secondSlashIndex) +
        &quot;/&quot; +
        location +
        originalSource.slice(secondSlashIndex);
      break;

    // 이미지 사용 확정으로 인한 이미지 이동
    default:
      Key = originalSource.replace(&quot;/temporary&quot;, &quot;&quot;);
      break;
  }

  if (typeof Key !== &quot;string&quot;)
    return console.error(&quot;이미지 저장 위치가 올바르지 않습니다.&quot;);

  return S3.copyObject(
    {
      Bucket: &quot;bleshop&quot;,
      CopySource: &quot;bleshop/&quot; + originalSource,
      Key,
    },
    (error, data) =&gt; {
      /**
       * &gt;&gt;&gt; 여기가 가끔씩 두 번 실행됨, 요청은 한 번으로 확인했고, callback이 두 번 실행되면서 에러가 발생함
       * 하지만 첫 번째 실행에 정상작동해서 이미지 복사는 정상적으로 실행되므로 상관은 없지만 에러 로그가 남는 문제가 발생
       */

      if (error) console.error(&quot;S3 이미지 이동 error &gt;&gt; &quot;, error);
    }
  ).promise();
};

/**
 * 2022/08/14 - S3 이미지 이동 ( 복사 후 제거 ) - by 1-blue
 * @param photo: 이미지 파일 이름, location: 이미지 복사 위치
 * @returns
 */
export const movePhoto = async (photo: string, location: PhotoKinds) =&gt; {
  // OAuth의 이미지를 사용하는 경우
  if (photo.includes(&quot;http&quot;)) return;

  try {
    await copyPhoto(photo, location);
    await deletePhoto(photo);
  } catch (error) {
    console.error(&quot;movePhoto &gt;&gt; &quot;, error);
  }
};</code></pre><h2 id="😩-겪은-문제">😩 겪은 문제</h2>
<p><code>preSignedURL</code>로 <code>PUT</code> 요청을 할 때는 <code>File</code> 객체 자체를 그대로 넘겨줘야 하는데 처음에는 <code>FormData</code>를 이용해서 넘겨줬었습니다. <code>FormData</code>를 이용하면 아무 문제 없이 정상 작동을 하지만 이미지가 깨지는 건지는 모르겠는데 이미지가 불러오는 부분에서 문제가 발생합니다.
처음에 이 부분을 몰라서 문제를 찾느라 4시간 정도 삽질하면서 결국 해결했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🧭 네비게이션 바 구현]]></title>
            <link>https://velog.io/@1-blue/%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EB%B0%94-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@1-blue/%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98-%EB%B0%94-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 09 Aug 2022 13:51:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1-blue/post/e724c1d0-ff83-4953-b756-8710a41f3032/image.gif" alt=""></p>
<blockquote>
<p><code>Next.js</code>와 <code>tailwindcss</code>를 이용한 프로젝트입니다.</p>
</blockquote>
<h2 id="📑-구조-설계">📑 구조 설계</h2>
<p>네비게이션 바는 최하단에 붙여놓도록 만들었습니다.</p>
<p>스크롤을 내리는 경우에는 현재 페이지의 내용을 읽어보는 경우일 것이기 때문에 영역을 넓히기 위해서 네이게이션 바를 숨기고, 스크롤을 올리는 경우는 다른 페이지로 넘어가기 위함일 경우가 많기 때문에 네비게이션 바를 나타내도록 설계했습니다.</p>
<p>따라서 항상 최하단에 붙어 있다가</p>
<ol>
<li>스크롤을 아래로 내리는 경우 숨기고</li>
<li>위로 올리는 경우 나타나고</li>
<li>스크롤을 아래로 내리더라도 페이지의 최하단이라면 나타나도록 설계했습니다.</li>
</ol>
<h2 id="📃-페이지-구분">📃 페이지 구분</h2>
<p>크게 5가지 페이지로 구분했습니다.</p>
<ol>
<li>카테고리</li>
<li>검색</li>
<li>홈</li>
<li>내 정보</li>
<li>장바구니</li>
</ol>
<p><code>next.js</code>의 <code>useRouter().asPath</code>를 이용해서 현재 <code>URL</code>에 맞는 페이지에 들어올 경우 구분할 수 있도록 네이게이션 바에 특정 색상을 입혔습니다.</p>
<blockquote>
<p>중요하고 어려운 코드가 아니므로 자세한 코드는 생략하겠습니다.</p>
</blockquote>
<h2 id="🔒-네비게이션-바-하단-고정">🔒 네비게이션 바 하단 고정</h2>
<p><code>position: fixed</code>를 이용해서 하단에 고정시켰습니다.
다만 <code>fixed</code>는 공중에 떠있는 느낌으로 다른 요소의 레이아웃에 영향을 미치지 않으므로 실제 컨텐츠가 네비게이션 바에 묻힐 수 있기 때문에 네비게이션 바의 높이만큼 <code>body</code>에 <code>margin-bottom</code>을 부여해서 네비게이션 바가 들어갈 공간을 만들었습니다.</p>
<blockquote>
<p>중요하고 어려운 코드가 아니므로 자세한 코드는 생략하겠습니다.</p>
</blockquote>
<h2 id="🤔-usescrolldirection-훅">🤔 useScrollDirection 훅</h2>
<p>처음 말했던 네비게이션 바를 스크롤에 의해서 숨길지 보여줄지를 결정하는 데 사용할 수 있는 훅을 만들었습니다.</p>
<p>자세한 설명은 주석으로 작성했기 때문에 생략하겠습니다.
다만 스크롤 이벤트 등록하기 때문에 쓰로틀링을 적용했습니다.</p>
<ul>
<li><code>src/hooks/useScroll.tsx</code><pre><code class="language-tsx">import { useCallback, useEffect, useState } from &quot;react&quot;;
</code></pre>
</li>
</ul>
<p>// util
import { throttleHelper } from &quot;@src/libs&quot;;</p>
<p>/**</p>
<ul>
<li><p>마지막 스크롤링의 방향을 알아내는 훅</p>
</li>
<li><p>@returns [스크롤 방향, 스크롤 최하단 여부, 현재 스크롤 위치] 순서로 반환 ( boolean, boolean, number )</p>
</li>
<li><p>/
const useScrollDirection = () =&gt; {
// 2022/08/09 - 마지막 스크롤 방향 - by 1-blue
const [isUp, setIsUp] = useState(false);
// 2022/08/09 - 현재 스크롤 위치값 저장할 변수 - by 1-blue
const [pageY, setPageY] = useState(0);
// 2022/08/09 - 현재 스크롤이 최하단에 있는지 판단할 변수 - by 1-blue
const [isBottom, setIsBottom] = useState(false);</p>
<p>// 2022/08/09 - 현재 스크롤 방향을 확인할 스크롤 이벤트 함수 - by 1-blue
const handleScroll = useCallback(() =&gt; {
  /**</p>
<ul>
<li><p>scrollHeight: 총 컨텐츠 높이</p>
</li>
<li><p>clientHeight: 현재 보이는 높이 ( 현재 화면(컨텐츠)의 높이 )</p>
</li>
<li><p>scrollY: 현재 스크롤 높이</p>
</li>
<li></li>
<li><p>따라서 &quot;총 컨텐츠 높이 === 현재 보이는 높이 + 현재 스크롤 높이&quot; 라면 최하단까지 스크롤을 내린 것</p>
</li>
<li><p>/
const {
scrollY,
document: {
  documentElement: { scrollHeight, clientHeight },
},
} = window;</p>
<p>const deltaY = scrollY - pageY;
const isUp = scrollY !== 0 &amp;&amp; deltaY &gt;= 0;
const isBottom = scrollHeight - scrollY - clientHeight === 0;</p>
<p>setIsUp(isUp);
setPageY(scrollY);
setIsBottom(isBottom);
}, [pageY, setIsUp, setPageY, setIsBottom]);</p>
</li>
</ul>
<p>// 2022/08/09 - 스크롤 이벤트에 스로틀링 적용 - by 1-blue
const throttleScroll = throttleHelper(handleScroll, 50);</p>
<p>// 2022/08/09 - 스크롤 이벤트 등록 - by 1-blue
useEffect(() =&gt; {
  document.addEventListener(&quot;scroll&quot;, throttleScroll);
  return () =&gt; document.removeEventListener(&quot;scroll&quot;, throttleScroll);
}, [throttleScroll]);</p>
<p>return [isUp, isBottom, pageY];
};</p>
</li>
</ul>
<p>export default useScrollDirection;</p>
<pre><code>
+ `src/libs/utils.ts`
```ts
/**
 * 스로틀링 적용 헬퍼 함수
 * @param callback 이후에 실행할 콜백함수
 * @param waitTime 기다릴 시간
 * @returns &quot;waitTime&quot;만큼 스로틀링이 적용된 함수(&quot;callback&quot;) 반환
 */
export const throttleHelper = (callback: () =&gt; void, waitTime: number) =&gt; {
  let timerId: ReturnType&lt;typeof setTimeout&gt; | null = null;

  return () =&gt; {
    if (timerId) return;

    timerId = setTimeout(() =&gt; {
      callback();
      timerId = null;
    }, waitTime);
  };
};</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[백준 - 알고리즘 기초 1/2 ( 201 - 자료구조 1 ( 참고 ) )]]></title>
            <link>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-201-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-1-%EC%B0%B8%EA%B3%A0</link>
            <guid>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-201-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-1-%EC%B0%B8%EA%B3%A0</guid>
            <pubDate>Mon, 08 Aug 2022 03:42:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://code.plus/course/41">백준 알고리즘 기초 강의</a>에 명시된 문제를 풀이한 포스트입니다</p>
</blockquote>
<h2 id="1-1918번---후위-표기식">1. <a href="https://www.acmicpc.net/problem/1935">1918번 - 후위 표기식</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = &quot;&quot;;

rl.on(&quot;line&quot;, (line) =&gt; {
  input = line;

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * 중위 표기식에서 후위 표기식 변환 방법
   * 1. 피연산자는 바로 출력
   * 2. 연산자는 스택의 상태에 따라 출력
   *   2.1 스택이 비었다면 스택에 넣기
   *   2.2 연산자 우선순위가 낮은 연산자 or 닫는 괄호를 만날 때까지 &quot;pop()&quot; 실행 후 스택에 넣기
   *   2.3 닫는 괄호라면 여는 괄호가 나올 때까지 &quot;pop()&quot; 실행
   * 3. 수식의 끝까지 왔다면 스택의 연산자들을 모두 꺼냄
   */

  const stack = [];
  let answer = &quot;&quot;;

  input.split(&quot;&quot;).forEach((v) =&gt; {
    // 연산자라면
    if (v.match(/[\(\)\+\*\-\/]/)) {
      // 2. 연산자는 스택의 상태에 따라 출력
      switch (v) {
        case &quot;(&quot;:
          stack.push(v);
          break;
        case &quot;)&quot;:
          // 2.3 닫는 괄호라면 여는 괄호가 나올 때까지 &quot;pop()&quot; 실행
          while (stack.length &amp;&amp; stack[stack.length - 1] !== &quot;(&quot;) {
            answer += stack.pop();
          }
          stack.pop();
          break;
        case &quot;+&quot;:
        case &quot;-&quot;:
          // 2.2 연산자 우선순위가 낮은 연산자를 만날 때까지 &quot;pop()&quot; 실행 후 스택에 넣기 ( &quot;+&quot;, &quot;-&quot;는 우선순위가 제일 낮아서 비교할 필요 없이 닫는 괄호가 나오는지만 확인하면 됨 )
          while (stack.length &amp;&amp; stack[stack.length - 1] !== &quot;(&quot;) {
            answer += stack.pop();
          }
          stack.push(v);
          break;
        case &quot;*&quot;:
        case &quot;/&quot;:
          // 2.2 연산자 우선순위가 낮은 연산자를 만날 때까지 &quot;pop()&quot; 실행 후 스택에 넣기 ( &quot;*&quot;, &quot;/&quot;만 비교하므로 닫는 괄호를 생각해 줄 필요 없음 )
          while (
            stack.length &amp;&amp;
            (stack[stack.length - 1] === &quot;*&quot; || stack[stack.length - 1] === &quot;/&quot;)
          ) {
            answer += stack.pop();
          }
          stack.push(v);
          break;
      }
    }
    // 피연산자라면
    else {
      // 1. 피연산자는 바로 출력
      answer += v;
    }
  });

  // 3. 수식의 끝까지 왔다면 스택의 연산자들을 모두 꺼냄
  while (stack.length) answer += stack.pop();

  console.log(answer);

  process.exit();
});</code></pre>
<h2 id="2-1935번---후위-표기식2">2. <a href="https://www.acmicpc.net/problem/1935">1935번 - 후위 표기식2</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  if (+input[0] + 2 === input.length) rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * 후위 표현식 계산법
   * 1. 피연산자는 스택에 넣는다.
   * 2. 연산자가 나올 경우 스택에서 피연산자 2개를 꺼낸 후 계산한 뒤 다시 넣는다. ( 단, 연산 순서는 &quot;나중&quot; &quot;연산자&quot; &quot;먼저&quot; )
   * 3. 모든 연산이 끝난 후 스택에 담겨있는 피연산자가 연산의 결과다.
   */

  const [, expression, ...temp] = input;
  const values = temp.map((v) =&gt; +v);
  const stack = [];
  let firstValue = null;
  let lastValue = null;

  expression.split(&quot;&quot;).forEach((v) =&gt; {
    // 연산자일 경우
    if (v.match(/[\+\-\*\/]/)) {
      lastValue = stack.pop();
      firstValue = stack.pop();

      switch (v) {
        case &quot;+&quot;:
          stack.push(firstValue + lastValue);
          break;
        case &quot;-&quot;:
          stack.push(firstValue - lastValue);
          break;
        case &quot;*&quot;:
          stack.push(firstValue * lastValue);
          break;
        case &quot;/&quot;:
          stack.push(firstValue / lastValue);
          break;
      }
    }
    // 피연산자일 경우
    else {
      stack.push(values[v.charCodeAt() - 65]);
    }
  });

  console.log(stack[0].toFixed(2));

  process.exit();
});</code></pre>
<h2 id="3-10808번---알파벳-개수">3. <a href="https://www.acmicpc.net/problem/10808">10808번 - 알파벳 개수</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = null;

rl.on(&quot;line&quot;, (line) =&gt; {
  input = line;

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  const alphabet = Array(26).fill(0);
  const word = input;

  word.split(&quot;&quot;).forEach((v) =&gt; (alphabet[v.charCodeAt() - 97] += 1));

  console.log(alphabet.join(&quot; &quot;));

  process.exit();
});</code></pre>
<h2 id="4-10809번---알파벳-찾기">4. <a href="https://www.acmicpc.net/problem/10809">10809번 - 알파벳 찾기</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = null;

rl.on(&quot;line&quot;, (line) =&gt; {
  input = line;

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  const alphabet = Array(26).fill(-1);
  const word = input;

  word.split(&quot;&quot;).forEach((v, i) =&gt; {
    if (alphabet[v.charCodeAt() - 97] !== -1) return;

    alphabet[v.charCodeAt() - 97] = i;
  });

  console.log(alphabet.join(&quot; &quot;));

  process.exit();
});</code></pre>
<h2 id="5-10820번---문자열-분석">5. <a href="https://www.acmicpc.net/problem/10820">10820번 - 문자열 분석</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];
let temp = null;
let answer = &quot;&quot;;
const table = {
  upper: 0,
  lower: 0,
  number: 0,
  space: 0,
};

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  // rl.close();
}).on(&quot;close&quot;, () =&gt; {
  // 전체 탐색
  input.forEach((str, i) =&gt; {
    // 문장 단위로 탐색
    str.split(&quot;&quot;).forEach((v) =&gt; {
      temp = v.charCodeAt();

      if (temp &gt;= 97 &amp;&amp; temp &lt;= 122) table.upper++;
      if (temp &gt;= 65 &amp;&amp; temp &lt;= 90) table.lower++;
      if (temp &gt;= 48 &amp;&amp; temp &lt;= 57) table.number++;
      if (temp === 32) table.space++;
    });

    // 문장 탐색 후 정답 작성
    answer += Object.values(table).join(&quot; &quot;);

    // 마지막 아니면 줄바꿈
    if (i !== input.length - 1) answer += &quot;\n&quot;;

    // 초기화
    Object.keys(table).forEach((key) =&gt; (table[key] = 0));
  });

  console.log(answer);

  process.exit();
});</code></pre>
<h2 id="6-2743번---단어-길이-재기">6. <a href="https://www.acmicpc.net/problem/2743">2743번 - 단어 길이 재기</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  const [word] = input;

  console.log(word.length);

  process.exit();
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 - 알고리즘 기초 1/2 ( 303 - 수학 ( 참고 ) )]]></title>
            <link>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-303-%EC%88%98%ED%95%99-%EC%B0%B8%EA%B3%A0</link>
            <guid>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-303-%EC%88%98%ED%95%99-%EC%B0%B8%EA%B3%A0</guid>
            <pubDate>Sun, 07 Aug 2022 02:12:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://code.plus/course/41">백준 알고리즘 기초 강의</a>에 명시된 문제를 풀이한 포스트입니다</p>
</blockquote>
<h2 id="1-11005번---진법-변환-2">1. <a href="https://www.acmicpc.net/problem/11005">11005번 - 진법 변환 2</a></h2>
<pre><code class="language-ts">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  const [N, B] = input[0].split(&quot; &quot;).map((v) =&gt; +v);

  console.log(N.toString(B).toUpperCase());

  process.exit();
});</code></pre>
<h2 id="2-2745번---진법-변환">2. <a href="https://www.acmicpc.net/problem/2745">2745번 - 진법 변환</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  const [N, B] = input[0].split(&quot; &quot;);

  console.log(parseInt(N, +B).toString(10));

  process.exit();
});</code></pre>
<h2 id="3-11576번---base-conversion">3. <a href="https://www.acmicpc.net/problem/11576">11576번 - Base Conversion</a></h2>
<blockquote>
<p>문제 이해 못함</p>
</blockquote>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  if (+input[0] === input.length - 1) rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * [입력]
   * A진법, B진법
   * M: 입력 숫자의 자리수의 개수
   * A진법 숫자 M개 ( 공백 구분 )
   *
   * [출력]
   * A진법 숫자들 ---&gt; B진법 숫자들
   *
   * 결론: A진법 숫자들 --&gt; 10진법 숫자들 --&gt; B진법 숫자들
   */

  process.exit();
});</code></pre>
<h2 id="4-11653번---소인수분해">4. <a href="https://www.acmicpc.net/problem/11653">11653번 - 소인수분해</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  let number = +input[0];
  let divideNumber = 2;
  const answer = [];

  while (number !== 1) {
    if (number % divideNumber === 0) {
      answer.push(divideNumber);
      number = number / divideNumber;
    } else {
      divideNumber++;
    }
  }

  console.log(answer.join(&quot;\n&quot;));

  process.exit();
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 - 알고리즘 기초 1/2 ( 301 - 수학 1 (연습) )]]></title>
            <link>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-301-%EC%88%98%ED%95%99-1-%EC%97%B0%EC%8A%B5</link>
            <guid>https://velog.io/@1-blue/%EB%B0%B1%EC%A4%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B8%B0%EC%B4%88-12-301-%EC%88%98%ED%95%99-1-%EC%97%B0%EC%8A%B5</guid>
            <pubDate>Fri, 05 Aug 2022 03:06:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://code.plus/course/41">백준 알고리즘 기초 강의</a>에 명시된 문제를 풀이한 포스트입니다</p>
</blockquote>
<h2 id="1-9613번---gcd-합">1. <a href="https://www.acmicpc.net/problem/9613">9613번 - GCD 합</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  if (+input[0] === input.length - 1) rl.close();
}).on(&quot;close&quot;, () =&gt; {
  // 유클리드 알고리즘
  const getGCD = (x, y) =&gt; {
    const bigger = Math.max(x, y);
    const smaller = Math.min(x, y);

    const remainder = bigger % smaller;

    if (remainder === 0) return smaller;
    else return getGCD(smaller, remainder);
  };

  // 전체 입력 개수 분리
  const [count, ...list] = input;
  // 입력 개수만큼 배열 생성
  const answer = Array(+count).fill(0);

  list.forEach((value, index) =&gt; {
    // 세부 입력 개수 분리 및 숫자로 변환 후 배열화
    const [, ...numbers] = value.split(&quot; &quot;).map((n) =&gt; +n);

    // 모든 경우를 포함해서 최대공약수(GCD) 계산 후 합치기
    numbers.forEach((v, i, arr) =&gt; {
      for (let j = i + 1; j &lt; arr.length; j++) {
        answer[index] += getGCD(v, arr[j]);
      }
    });
  });

  console.log(answer.join(&quot;\n&quot;));

  process.exit();
});</code></pre>
<h2 id="2-17087번---숨바꼭질-6">2. <a href="https://www.acmicpc.net/problem/17087">17087번 - 숨바꼭질 6</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  if (2 === input.length) rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * 핵심은 한번에 &quot;D만큼만 이동이 가능&quot;이다.
   * 몇번이든 상관없이 D만큼 이동해서 동생과 같은 위치에 도달할 수 있으면 된다.
   * 즉, &quot;|수빈위치 - 동생위치|&quot;인 값들을 D로 나눠서 나누어 떨어지는 최댓값인 최대공약수를 구하는 문제이다.
   */

  // 유클리드 알고리즘
  const getGCD = (x, y) =&gt; {
    const bigger = Math.max(x, y);
    const smaller = Math.min(x, y);

    const remainder = bigger % smaller;

    if (remainder === 0) return smaller;
    else return getGCD(smaller, remainder);
  };

  // 수빈 위치 구하기
  const [, subinLocation] = input[0].split(&quot; &quot;).map((v) =&gt; +v);
  // 수빈과 동생들의 거리 계산
  const locations = input[1]
    .split(&quot; &quot;)
    .map((v) =&gt; Math.abs(+v - subinLocation));

  // 수빈과 동생들의 거리의 최대공약수 구하기
  const distance = locations.reduce((p, c) =&gt; getGCD(p, c));

  console.log(distance);

  process.exit();
});</code></pre>
<h2 id="3-1373번---2진수-8진수">3. <a href="https://www.acmicpc.net/problem/1373">1373번 - 2진수 8진수</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = &quot;&quot;;

rl.on(&quot;line&quot;, (line) =&gt; {
  input = line;

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * 2진수 -&gt; 10진수 -&gt; 8진수 =&gt; 시간초과
   * 2진수를 뒤에서부터 3개씩 끊어서 8진수로 변환 후 붙이면 됨
   */

  const binary = input.split(&quot;&quot;);
  const answer = [];

  // 문자열을 뒤에서부터 3개씩 끊어서 2진수로 만들고 8진수로 변환하는 과정을 계속 반복
  while (binary.length &gt;= 3) {
    answer.push(
      parseInt(binary.splice(binary.length - 3).join(&quot;&quot;), 2).toString(8)
    );
  }

  if (binary.length) answer.push(parseInt(binary.join(&quot;&quot;), 2).toString(8));

  console.log(answer.reverse().join(&quot;&quot;));

  process.exit();
});</code></pre>
<h2 id="4-1212번---8진수-2진수">4. <a href="https://www.acmicpc.net/problem/1212">1212번 - 8진수 2진수</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

let input = &quot;&quot;;

rl.on(&quot;line&quot;, (line) =&gt; {
  input = line;

  rl.close();
}).on(&quot;close&quot;, () =&gt; {
  /**
   * 8진수를 한자리씩 끊어서 2진수로 변환 후 합쳐줌
   * 단, 마지막을 제외하고 3자리가 아닐경우 빈칸에 0을 채워줌
   */
  const numbers = input.split(&quot;&quot;).map((v) =&gt; +v);

  const answer = numbers.map((v, i) =&gt; {
    let binary = v.toString(2);

    if (i !== 0) binary = &quot;0&quot;.repeat(3 - binary.length) + binary;

    return binary;
  });

  console.log(answer.join(&quot;&quot;));

  process.exit();
});</code></pre>
<h2 id="5-2089번----2진수">5. <a href="https://www.acmicpc.net/problem/2089">2089번 - -2진수</a></h2>
<blockquote>
<p>문제 풀이 실패</p>
</blockquote>
<h2 id="6-17103번---골드바흐-파티션">6. <a href="https://www.acmicpc.net/problem/17103">17103번 - 골드바흐 파티션</a></h2>
<pre><code class="language-js">const readline = require(&quot;readline&quot;);
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const input = [];

rl.on(&quot;line&quot;, (line) =&gt; {
  input.push(line);

  if (+input[0] === input.length - 1) rl.close();
}).on(&quot;close&quot;, () =&gt; {
  // 에라토스테네스의 체로 범위 내 소수 구하기
  const getPrimeNumbers = (range) =&gt; {
    const candidates = Array(range + 1).fill(true);
    let loopIndex = 1;

    candidates.forEach((v, index, arr) =&gt; {
      // 0은 제외, 1은 예외
      if (index === 1 || index === 0) {
        arr[index] = false;
        return;
      }

      loopIndex = 1;
      for (let i = index; i &lt; arr.length; i = loopIndex++ * index) {
        // 본인은 소수에서 제외
        if (i === index) continue;
        // 이미 소수인 것도 제외
        if (arr[i] === false) continue;

        arr[i] = false;
      }
    });

    return candidates;
  };

  // 홀수 제외 숫자 배열로 변환
  const [, ...numbers] = input.map((v) =&gt; +v);

  // 특정 범위 소수 구하기
  const primeNumbers = getPrimeNumbers(Math.max(...numbers));

  const answers = numbers.map((v) =&gt; {
    let count = 0;

    for (let i = 2; i &lt;= v / 2; i++) {
      if (primeNumbers[i] &amp;&amp; primeNumbers[v - i]) {
        count++;
      }
    }

    return count;
  });

  console.log(answers.join(&quot;\n&quot;));

  process.exit();
});</code></pre>
]]></description>
        </item>
    </channel>
</rss>