<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>k_ddaddi.log</title>
        <link>https://velog.io/</link>
        <description>완전완전완전초보초보초보</description>
        <lastBuildDate>Wed, 23 Apr 2025 19:59:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>k_ddaddi.log</title>
            <url>https://velog.velcdn.com/images/k_ddaddi/profile/cba420d1-ef6f-43c6-bbe8-b64aba2328e0/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. k_ddaddi.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/k_ddaddi" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[메모리가 새고 있다]]></title>
            <link>https://velog.io/@k_ddaddi/%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B0%80-%EC%83%88%EA%B3%A0-%EC%9E%88%EB%8B%A4</link>
            <guid>https://velog.io/@k_ddaddi/%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B0%80-%EC%83%88%EA%B3%A0-%EC%9E%88%EB%8B%A4</guid>
            <pubDate>Wed, 23 Apr 2025 19:59:52 GMT</pubDate>
            <description><![CDATA[<h1 id="문제상황">문제상황</h1>
<hr>
<p>어느날, 개발 중이던 페이지가 너무 버벅이는 느낌이 들었다. 이상하다고 생각하던 찰나, 메모리 사용량을 우연치않게 확인해보니 무려 700MB 가 넘어가고 있었다. 분명 페이지를 처음 로드했을 땐 200MB 정도였는데 말이다. </p>
<p>한번 의심이 들자 제대로 확인해보고 싶어졌다. 페이지를 켜둔 채로 6시간 후에 다시 확인해보니, 메모리 사용량은 무려 <strong>1.2GB</strong> 까지 올라가 있었다. 분명 뭔가 문제가 있다.</p>
<br/>

<h2 id="메모리-스냅샷">메모리 스냅샷</h2>
<p>크롬 개발자 도구의 Performanc 탭에서 메모리 스냅샷을 찍어보았다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/0e971c22-b271-4b67-96e1-fb22f7ea119b/image.png" alt=""></p>
<p>회색은 가비지콜렉터가 수집해간 메모리, 파란색은 누수된 메모리이다. 슬슬 <code>메모리 누수(Memory Leak)</code> 가 의심되기 시작했다.</p>
<br/>
<br/>

<h1 id="원인은-swiper">원인은 Swiper</h1>
<hr>
<p>페이지에서 사용 중인 슬라이더는 Swiper.js 였고, 특히 <code>loop</code>, <code>onSlideChange</code>, <code>observer</code>, <code>observeParents</code> 옵션들이 의심됐다.</p>
<br/>

<h2 id="usecallback-을-쓴-이유">useCallback 을 쓴 이유</h2>
<p>버튼 클릭 핸들러는 다음과 같이 <code>useCallback</code> 으로 감싸주었다.</p>
<pre><code class="language-jsx">const handleNextClick = useCallback(() =&gt; {
  const next = (activeIndex + 1) % champions.length;
  setActiveIndex(next);
}, [activeIndex, champions.length]);</code></pre>
<p><code>useCallback</code> 은 컴포넌트가 리렌더링될 때마다 새로운 함수를 만드는 것이 아니라, 의존성 배열이 바뀌지 않는 이상 기존 함수를 재사용한다. 즉, 불필요한 렌더링을 막고 성능을 조금이라도 개선할 수 있다.</p>
<br/>

<h2 id="observer-옵션-제거">observer 옵션 제거</h2>
<p>Swiper 의 <code>observer</code> , <code>observeParents</code> 옵션은 DOM 변화가 있는지 계속 감시한다. 반응형 대응에는 좋지만, 계속 DOM을 지켜보고 있는 자체가 메모리 사용량을 야금야금 늘리는 원인이 될 수도 있다고 생각했다.</p>
<p>그래서 나는 반응형 대신 고정형 슬라이드로 구조를 바꿨다.</p>
<br/>

<h2 id="리팩토링-후-코드">리팩토링 후 코드</h2>
<pre><code class="language-jsx">useEffect(() =&gt; {
  const interval = setInterval(() =&gt; {
    setActiveIndex((prev) =&gt; (prev + 1) % champions.length);
  }, 3000);
  return () =&gt; clearInterval(interval);
}, [champions.length]);

useEffect(() =&gt; {
  bgSwiperRef.current?.slideTo(activeIndex);
  contentSwiperRef.current?.slideTo(activeIndex);
}, [activeIndex]);</code></pre>
<p>Swiper 컴포넌트도 간결하게 정리했다. 불필요한 옵션을 제거하고, <code>loop</code>, <code>autoplay</code>, <code>observer</code> 없이도 기본적인 기능만으로도 충분히 매끄러운 동작이 가능했다.</p>
<br/>
<br/>

<h1 id="결과">결과</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/2c951de4-3265-42cd-a9e5-28d81ecb961e/image.png" alt=""></p>
<p>수정 후에는 슬라이드가 인터랙션을 할 때만 메모리를 사용하고, 일정 시간이 지나도 메모리 사용량이 눈에 띄게 증가하지 않았다. 메모리 누수를 어느정도 잡은 것이라 생각한다.</p>
<br/>

<h1 id="마무리">마무리</h1>
<hr>
<p>이번 경험을 통해 내가 얻은 교훈은 다음과 같다: </p>
<ul>
<li><p><code>observer</code> , <code>autoplay</code> , <code>loop</code> 같은 Swiper 편의기능이 때로는 독이 될 수 있다.</p>
</li>
<li><p>메모리 문제는 눈에 안 보이는 누수가 많기 때문에, 직접 확인하고 스냅샷을 찍어봐야 확실해진다.</p>
</li>
<li><p>불필요한 렌더링을 줄이는 <code>useCallback</code> 같은 훅도 작은 도움이 된다.</p>
</li>
</ul>
<p>물론 아직 완벽하게 잡지는 못했다. 사실 어느정도 까지 잡아야 될지는 잘모르겠다. 그래도 어느정도 잡은거에 대해서 만족할 수 있었다.</p>
<p>무심코 넘어갔던 슬라이더 하나가, 1.2GB의 메모리 누수를 만들고 있었다.
앞으로 이 <code>useCallback</code> 이라던지 <code>useMemo</code> 에 대해서 좀 더 관심을 가지고 잘 활용해 봐야할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[배포 환경에서만 팀 구분이 잘못됐던 이유]]></title>
            <link>https://velog.io/@k_ddaddi/%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EB%A7%8C-%ED%8C%80-%EA%B5%AC%EB%B6%84%EC%9D%B4-%EC%9E%98%EB%AA%BB%EB%90%90%EB%8D%98-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@k_ddaddi/%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EB%A7%8C-%ED%8C%80-%EA%B5%AC%EB%B6%84%EC%9D%B4-%EC%9E%98%EB%AA%BB%EB%90%90%EB%8D%98-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 22 Apr 2025 12:46:39 GMT</pubDate>
            <description><![CDATA[<p>프론트 단계에서 <code>.rofl</code> 리플레이 파일을 업로드하고, 백엔드에서 이를 파싱해 팀을 나누는 기능을 만들었었다. 내가 이때까지 로컬 환경에서만 경기기록을 업로드 하고 배포사이트에서는 업로드 하지않았기에 버그가 있는 줄 몰랐다.</p>
<p>로컬에서는 정상적으로 작동했지만, 배포 서버에선 모든 플레이어가 레드팀으로만 분류되는 이상한 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/6f2cab0e-9bce-44e3-873c-85d211fbdcb5/image.png" alt=""></p>
<br/>

<h1 id="문제-상황">문제 상황</h1>
<hr>
<p>로컬 환경에서는 플레이어 10명이 블루팀과 레드팀으로 5:5 로 잘 나눠졌었다.
하지만 배포된 서버에서는 모든 플레이어가 레드팀으로만 처리되고 있었다.</p>
<p>프론트에서 올리는 <code>.rofl</code> 파일 자체는 동일했기 때문에, 문제는 <strong>백엔드 코드나 배포 환경</strong> 에 있을 것이라 예상했다.</p>
<p>로컬에서는 아래와 같이 Blue와 Red로 팀이 잘 나눠졌었다.</p>
<pre><code class="language-json">[
  { &quot;nick&quot;: &quot;player1&quot;, &quot;team&quot;: &quot;Blue&quot; },
  { &quot;nick&quot;: &quot;player2&quot;, &quot;team&quot;: &quot;Red&quot; },
]</code></pre>
<p>그런데 배포 서버에서는 모든 팀이 이렇게 나와버렸다.</p>
<pre><code class="language-json">[
  { &quot;nick&quot;: &quot;player1&quot;, &quot;team&quot;: &quot;Red&quot; },
  { &quot;nick&quot;: &quot;player2&quot;, &quot;team&quot;: &quot;Red&quot; },
]</code></pre>
<br/>

<h2 id="기존-코드">기존 코드</h2>
<pre><code class="language-js">team: player:Team === &quot;100&quot; ? &quot;Blue&quot; : &quot;Red&quot;</code></pre>
<p><code>.rofl</code> 파일을 파싱했을 때, <code>player.TEAM</code> 값은 <code>&quot;100&quot;</code> 이라는 문자열로 되어 있었다.
그래서 <code>&quot;100&quot;</code> 과 <code>&quot;100&quot;</code> 을 비교하면 당연히 true 이고, Blue 팀으로 잘 분류되었었다.</p>
<p>하지만 배포 환경에서는 이 비교가 통하지 않았다.
즉, <code>player.TEAM === &quot;100&quot;</code> 이 조건이 실패하면서 전부 Red로 분류된 것이다.</p>
<br/>

<h2 id="왜-이런-일이-생겼을까">왜 이런 일이 생겼을까?</h2>
<p>여기서 떠올린 가능성은 다음과 같았다.</p>
<blockquote>
<p>혹시 JSON 파싱 과정이나 통신 중에
<code>&quot;100&quot;</code> 이라는 문자열이 숫자 <code>100</code> 으로 자동으로 타입 변환된 건 아닐까?</p>
</blockquote>
<p>예를 들어, 배포된 서버에서는 아래와 같은 상태였을 수 있다.</p>
<pre><code class="language-js">typeof player.TEAM     // &quot;number&quot;
player.TEAM            // 100</code></pre>
<p>그러면 <code>&quot;100&quot; === 100</code> 은 <code>false</code> 가 되니깐 조건이 통과되지 않는다.
결국 모든 유저가 Red팀으로 들어가게 된다.</p>
<br/>

<h1 id="해결-방법">해결 방법</h1>
<hr>
<p>그래서 조건을 아래처럼 수행했다.</p>
<pre><code class="language-js">team: String(player.TEAM) === &quot;100&quot; ? &quot;Blue&quot; : &quot;Red&quot;;</code></pre>
<p>이렇게 하면 <code>player.TEAM</code> 이 <code>&quot;100&quot;</code> 이든 <code>100</code> 이든
무조건 문자열 <code>&quot;100&quot;</code>으로 변환해서 비교하기 때문에 타입 차이로 인한 오류를 막을 수 있다.</p>
<p>타입 안정성을 높이는 좋은 습관이긴 했지만,
결과적으로는 이 코드 자체가 배포 서버에 반영되지 않았던 것이 진짜 원인이었다.</p>
<h2 id="진짜-원인">진짜 원인</h2>
<p>나는 프론트는 Netlify로 배포하고 있고, <code>.rofl</code> 파일의 파싱을 담당하고 있는 백은 CloudType 을 쓰고 있다.</p>
<p>내 착각인건지는 잘 모르겠는데 일단 Netlify 는 커밋 및 푸쉬를 하면 자동으로 재빌드 를 해주었다. 그래서 CloudType 도 그렇게 하면 재빌드를 해주었던것 같았는데 그게 아니었다.</p>
<p>확인을 해보니 예전 빌드를 쓰고있었다.</p>
<pre><code class="language-js">team: player.TEAM === 100 ? &quot;Blue&quot; : &quot;Red&quot;;</code></pre>
<p>이 부분이 문제였다. 당연히 <code>player.TEAM</code> 은 <code>String</code> 으로 밖에 오질 않는데 <code>Number</code> 값과 비교하니 <code>false</code> 일 수 밖에 없었다. 그러니 당연히 <code>Red</code> 인것이고.</p>
<p>로컬은 따로 <code>node</code> 에서 로컬서버를 돌리고 있기 때문에 수정된 버전을 사용하고 있었는데 배포사이트에서는 예전 버전을 사용하고 있었다.</p>
<p>따라서, 백서버를 재 빌드 하고 테스트 해보니 배포사이트에서도 잘 되는 것을 확인할 수 있었다.</p>
<br/>
<br/>

<h1 id="느낀-점">느낀 점</h1>
<hr>
<p>이번 경험을 통해 몇 가지 중요한 교훈을 얻었다.</p>
<ul>
<li><p>타입 안정성의 중요성 : 문자열과 숫자는 엄연히 다르며, 조건 비교 시 주의가 필요하다.</p>
</li>
<li><p>타입스크립트 도입의 필요성 : 타입스크립트였다면 이런 실수를 훨씬 더 빨리 캐치할 수 있었을 것이다.</p>
</li>
<li><p>CI/CD 파이프라인에 대한 이해 부족 : 프론트는 Netlify 로 자동 배포되지만, 백엔드는 CloudType 에서 수동으로 배포해야 한다는 것을 몰랐다. 앞으로는 자동화된 CI/CD 설정을 적용해서 커밋할 때마다 백엔드도 자동으로 최신화되도록 구성하고 싶다.</p>
</li>
</ul>
<br/>

<p>이번 해프닝은 작은 실수였지만,
타입과 배포 환경의 중요성을 몸으로 배울 수 있었던 좋은 계기가 되었다.</p>
<p>그리고, 나는 다시 테스트한다고 엉망이 된 Elo 점수들을 복구하기 위해서 다시 매치들을 기록해야된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스와이퍼 반응형 이슈 해결: 왜 직접 swiper 인스턴스를 써야 할까?]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%8A%A4%EC%99%80%EC%9D%B4%ED%8D%BC-%EB%B0%98%EC%9D%91%ED%98%95-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-%EC%99%9C-%EC%A7%81%EC%A0%91-swiper-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@k_ddaddi/%EC%8A%A4%EC%99%80%EC%9D%B4%ED%8D%BC-%EB%B0%98%EC%9D%91%ED%98%95-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-%EC%99%9C-%EC%A7%81%EC%A0%91-swiper-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 21 Apr 2025 18:10:55 GMT</pubDate>
            <description><![CDATA[<p>Swiper로 반응형 슬라이드를 구현하면서, 리사이징을 빠르게 할 때 슬라이드들이 무너지는 현상을 만났다. 이번 글에서는 그 이유와 해결 방법을 간단하게 정리했다.</p>
<br/>

<h1 id="문제-상황">문제 상황</h1>
<hr>
<p>아래 그림은 현재 처한 문제이다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/5e9cdd11-b45d-4f1d-864e-63e73dbbf871/image.gif" alt=""></p>
<p>Swiper로 슬라이드를 만들 때 다음과 같이 설정했다.</p>
<pre><code class="language-jsx">&lt;Swiper
  modules={[Navigation, Autoplay]}
  navigation={{
    nextEl: &quot;.swiper-custom-next&quot;,
    prevEl: &quot;.swiper-custom-prev&quot;,
  }}
  spaceBetween={30}
  breakpoints={{
    1536: { slidesPerView: 4 },
    1280: { slidesPerView: 3 },
    768: { slidesPerView: 2 },
    0: { slidesPerView: 1 },
  }}
  loop={true}
  autoplay={{ delay: 3000, disableOnInteraction: false }}
  onSlideChange={(swiper) =&gt; setActiveIndex(swiper.realIndex)}
  speed={1000}
  allowTouchMove={false}
  onSwiper={(swiper) =&gt; (contentSwiperRef.current = swiper)}
&gt;</code></pre>
<p>리사이징이 빠르게 발생하면 슬라이드 레이아웃이 깨졌다. 현재 처한 상황은 내가 적용한 두 번째 슬라이드가 한 차례 밀린 현상이다. 이유를 몰라 구글링을 해봤더니 다음 두 가지 방법이 있었다.</p>
<ul>
<li><p><code>observer</code> , <code>observerParents</code> 옵션 추가</p>
</li>
<li><p>swiper 내부 상태 대신 swiper 인스턴스를 직접 사용하기</p>
</li>
</ul>
<p>처음에 oberver 옵션을 사용했으나 해결되지 않았다. 결국 두 번째 방법으로 해결했다.</p>
<h2 id="왜-observer-옵션이-안-먹혔을까">왜 observer 옵션이 안 먹혔을까?</h2>
<p>swiper 내부에서 자체 상태 관리를 하고 있다. 리액트에서 <code>setActiveIndex</code>    와 같은 상태를 별도로 두고 swiper 내부 상태에 의존하면, 리사이징처럼 빠르게 업데이트가 일어나는 상황에서는 상태 간 동기화가 안맞을 때가 있다.</p>
<p>즉, 리액트에서 설정한 <code>activeIndex</code>와 swiper 내부의 실제 index 상태가 서로 달라질 수 있다는 이야기다. 그래서 observer 옵션을 써도, 리액트의 상태가 늦게 반영되면서 순간적인 화면 깨짐 현상이 발생했던 것이다.</p>
<br/>

<h2 id="swiper-인스턴스를-직접-쓰면-왜-해결되나">swiper 인스턴스를 직접 쓰면 왜 해결되나?</h2>
<p>swiper 내부 상태를 리액트 상태로 바꾸는 대신, swiper 가 직접 관리하는 인스턴스를 사용하는 방식으로 바꿔주었다.</p>
<pre><code class="language-jsx">onSlideChange={(swiper) =&gt; {
  const realIdx = swiper.realIndex;
  setActiveIndex(realIdx);
  bgSwiperRef.current?.slideToLoop(realIdx);
}}</code></pre>
<p>이렇게 swiper 인스턴스에서 직접 현재 인덱스를 가져오면, 리액트 상태 업데이트의 지연 없이 즉시 swiper 의 실제 상태를 가져올 수 있다.</p>
<p>즉, swiper가 자체 관리하는 실제 슬라이드 상태에 직접 접근해서 화면이 깨지는 현상을 방지한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/117c59e5-1546-4c22-a849-05c42494ff69/image.gif" alt=""></p>
<p>(화질은 용량이슈로 인해...)</p>
<br/>
<br/>

<h1 id="마무리">마무리</h1>
<hr>
<p>결론적으로, Swiper를 사용할 때는 swiper 인스턴스를 통해 실제 상태에 접근하는 게 더 안정적이다. 리액트의 상태를 별도로 관리하기보다는 swiper 가 자체 관리하는 상태를 직접 쓰는 방식을 기억하자.</p>
<p>이 간단한 팁 하나가 슬라이드 레이아웃이 무너지는 스트레스를 없애줬다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Context로 토스트 메시지 관리하기]]></title>
            <link>https://velog.io/@k_ddaddi/React-Context%EB%A1%9C-%ED%86%A0%EC%8A%A4%ED%8A%B8-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@k_ddaddi/React-Context%EB%A1%9C-%ED%86%A0%EC%8A%A4%ED%8A%B8-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 20 Apr 2025 23:28:18 GMT</pubDate>
            <description><![CDATA[<h1 id="문제상황">문제상황</h1>
<hr>
<p>개발하다 보면 사용자에게 작업의 성공, 실패 등을 알려주는 토스트 메세지 (mui 에서는 snackbar)가 필요하다.처음에는 단순하게 alert 으로 알려주었다가 그래도 프론트엔드 개발자를 꿈꾸는데 언제까지 alert 으로 알려주기에는 가오(?)가 안살기에 한번 달아보았다. 처음에는 단순하게 각 컴포넌트에서 토스트 메세지 상태를 관리했다. 아래 코드처럼 말이다.</p>
<pre><code class="language-jsx">// 토스트 관리를 위한 상태
const [snackMessage, setSnackMessage] = useState(&quot;&quot;);
const [snackColor, setSnackColor] = useState(&quot;success&quot;);
const [state, setState] = useState({
  open: false,
  Transition: SlideTransition,
});

// 토스트 핸들러
const showSnack = (message, type = &quot;success&quot;) =&gt; {
  setSnackMessage(message);
  setSnackColor(type);
  setState({ open: true, Transition: SlideTransition });
};

const handleSnackClose = () =&gt; {
  setState({ ...state, open: false });
};

return (
  {/* 위에서 컴포넌트를 만든 후  */}
    &lt;ToastContext.Provider value={{ showSnack }}&gt;
      {children}
      &lt;Snackbar
        open={state.open}
        autoHideDuration={3000}
        onClose={handleSnackClose}
        anchorOrigin={{ vertical: &quot;top&quot;, horizontal: &quot;right&quot; }}
        slots={{ transition: state.Transition }}
      &gt;
        &lt;Alert onClose={handleSnackClose} severity={snackColor}&gt;
          {snackMessage}
        &lt;/Alert&gt;
      &lt;/Snackbar&gt;
    &lt;/ToastContext.Provider&gt;
  );</code></pre>
<br/>

<p>이렇게 해서 토스트를 만들어주었다. 그리고 여기저기 컴포넌트에 토스트가 필요한 곳에 넣어주었다. 그러다가 문득 생각이 들었다. 왜 여기저기에 복붙해서 넣어줘야하나? 이것도 훅 비스무리로 만들 수 없을까 라는 생각말이다.</p>
<p>여기서 문제점이 들어난 것이다.</p>
<p>이 방식의 문제점은 토스트 메세지가 필요한 모든 컴포넌트에서 동일한 코드를 반복해야 한다는 것이다. 같은 코드를 여러 곳에 복붙하는것은 비효율적이며 유지보수를 어렵게 만든다. 어떻게 하면 효율적으로 관리할 수 있을까?</p>
<h1 id="react-context-와-provider란">React Context 와 Provider란?</h1>
<hr>
<p>이런 상황에서 React의 Context API가 해결책이 될 수 있다. React Context는 컴포넌트 트리 전체에 데이터를 제공하는 방법이다. 마치 지금 내가 쓰고 있는 Jotai 느낌이지만 조금 다르긴 하다. 이에 대해서는 후술할 예정이다.</p>
<p>일반적으로 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 데이터를 전달하는데, 여러 단계의 컴포넌트를 거쳐야 하는 경우 번거로워진다. Context를 사용하면 트리 구조내 어느 컴포넌트에서든 필요한 데이터에 직접 접근할 수 있다.</p>
<p>Context API의 주요 구성 요소는 다음과 같다:</p>
<ol>
<li><p>createContext: Context 객체를 생성한다.</p>
</li>
<li><p>Provider: Context의 값을 하위 컴포넌트에 제공한다.</p>
</li>
<li><p>useContext: Context의 값을 읽는 Hook 이다.</p>
</li>
</ol>
<br/>

<h2 id="토스트-메세지-관리를-위한-context-구현">토스트 메세지 관리를 위한 Context 구현</h2>
<p>먼저 토스트 메세지 관리를 위한 Context와 Provider를 만들었다.</p>
<pre><code class="language-jsx">&quot;use client&quot;;

import { Alert, Slide, Snackbar } from &quot;@mui/material&quot;;
import { createContext, useContext, useState } from &quot;react&quot;;

const ToastContext = createContext(null);

function SlideTransition(props) {
  return &lt;Slide {...props} direction=&quot;left&quot; /&gt;;
}

export function ToastProvider({ children }) {
  // 토스트 관리를 위한 상태
  const [snackMessage, setSnackMessage] = useState(&quot;&quot;);
  const [snackColor, setSnackColor] = useState(&quot;success&quot;);
  const [state, setState] = useState({
    open: false,
    Transition: SlideTransition,
  });

  // 토스트 핸들러
  const showSnack = (message, type = &quot;success&quot;) =&gt; {
    setSnackMessage(message);
    setSnackColor(type);
    setState({ open: true, Transition: SlideTransition });
  };

  const handleSnackClose = () =&gt; {
    setState({ ...state, open: false });
  };

  return (
    &lt;ToastContext.Provider value={{ showSnack }}&gt;
      {children}
      &lt;Snackbar
        open={state.open}
        autoHideDuration={3000}
        onClose={handleSnackClose}
        anchorOrigin={{ vertical: &quot;top&quot;, horizontal: &quot;right&quot; }}
        slots={{ transition: state.Transition }}
      &gt;
        &lt;Alert onClose={handleSnackClose} severity={snackColor}&gt;
          {snackMessage}
        &lt;/Alert&gt;
      &lt;/Snackbar&gt;
    &lt;/ToastContext.Provider&gt;
  );
}

export const useToast = () =&gt; useContext(ToastContext);</code></pre>
<p><code>createContext</code> 함수를 사용하여 <code>Toastcontext</code> 라는 새로운 Context를 생성했다.</p>
<p><code>ToastProvider</code> 컴포넌트는 토스트 메세지 상태를 관리하고, <code>showSnack</code> 함수를 Context 값으로 제공한다. 또한 실제 Snackbar 컴포넌트를 렌더링한다.</p>
<p><code>useToast</code> 라는 커스텀 훅을 만들어서 어디서든 쉽게 Context 값을 가져올 수 있게 하였다.</p>
<pre><code class="language-jsx">&lt;body className={`${notoSans.variable} antialiased max-w-[1920px] w-full`}&gt;
    &lt;ClientProvider&gt;
        &lt;ToastProvider&gt;{children}&lt;/ToastProvider&gt;
    &lt;/ClientProvider&gt;
&lt;/body&gt;</code></pre>
<p>다음은 layout 에 Provider 를 적용시켜줬다.</p>
<h3 id="실제-적용">실제 적용</h3>
<p>다음은 토스트 메세지를 사용한 사례이다.</p>
<pre><code class="language-jsx">import { useToast } from &quot;/ToastContext&quot;

export default function Page() {
  const { showSnack } = useToast();

  const handleCopy = async () =&gt; {
    // copy 로직 실행

    try {
      ...
    } catch (error) {
      console.error(&quot;Failed to copy: &quot;, error)
      showSnack(&quot;클립보드 복사에 실패했습니다.&quot;, &quot;error&quot;)
    } finally {
      showSnack(&quot;복사 완료&quot;, &quot;success&quot;);
    }
 };

 return (
   {/* 컴포넌트 */}
   &lt;button onClick={handleCopy}&gt; 복사하기 &lt;/button&gt;
   );
}</code></pre>
<p>이렇게 하면 각 컴포넌트에서 반복적으로 토스트 관련 코드를 작성할 필요가 없다. <code>useToast</code> 훅을 사용하면 어디서든 쉽게 토스트 메세지를 표시할 수 있다.</p>
<p>기존 코드와 비교했을 때 useToast() 한번이면 끝나니깐 굉장히 편리하고 보기에도 좋아졌다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/426b4eca-3948-43c2-8cc7-13f64be30eea/image.gif" alt=""></p>
<h1 id="context-api-vs-jotai-간단하게">Context API vs Jotai 간단하게</h1>
<hr>
<p>Context API 가 적합한 경우:</p>
<ul>
<li><p>간단한 전역 상태를 관리할 때</p>
</li>
<li><p>추가 라이브러리 의존성을 피하고 싶을 때</p>
</li>
<li><p>UI 테마, 사용자 인증 등 자주 변경되지 않는 상태</p>
</li>
<li><p>토스트 메세지처럼 중앙에서 관리되는 UI 상태</p>
</li>
</ul>
<br/>

<p>Jotai 가 적합한 경우:</p>
<ul>
<li><p>복잡한 상태 로직이 필요할 때</p>
</li>
<li><p>성능 최적화가 중요할 때</p>
</li>
<li><p>여러 작은 상태들이 독립적으로 변경될 때</p>
</li>
<li><p>상태 간 파생 관계가 많을 때</p>
</li>
</ul>
<br/>

<p>내가 쓴 토스트 메세지 같은 간단한 UI 상태 관리는 Context API 로도 충분하다. 하지만 애플리케이션이 성장하고 상태 관리가 복잡해진다면 Jotai 같은 라이브러리를 고려해볼 수 있다.</p>
<br/>

<h1 id="마치며">마치며</h1>
<hr>
<p>React Context 와 Provider 패턴을 사용하면 토스트 메세지와 같은 글로벌 상태를 효율적으로 관리할 수 있다. 이 패턴은 다음과 같은 이점을 있다:</p>
<ol>
<li><p>코드 중복을 줄일 수 있다.</p>
</li>
<li><p>컴포넌트 간 props 전달을 최소화할 수 있다.</p>
</li>
<li><p>관심사를 분리하여 코드 구조를 개선할 수 있다.</p>
</li>
<li><p>유지보수가 더 쉬워진다.</p>
</li>
</ol>
<p>이 패턴은 토스트 메시지뿐만 아니라 테마, 사용자 인증, 언어 설정 등 애플리케이션 전체에서 공유해야 하는 상태를 관리하는 데도 유용하게 활용할 수 있다.</p>
<p>더 복잡한 상태 관리가 필요할 경우 Jotai, Recoil, Zustand 같은 라이브러리를 검토해볼 수 있지만, 간단한 사용 사례에는 React의 기본 기능만으로도 충분하다.</p>
<p>React Context API나 적절한 상태 관리 라이브러리를 활용하면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있다. 프로젝트의 복잡성과 요구사항에 맞게 적절한 도구를 선택하는 것이 중요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Promise.all 을 활용한 API 성능 개선]]></title>
            <link>https://velog.io/@k_ddaddi/Promise.all-%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-API-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@k_ddaddi/Promise.all-%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-API-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Sun, 20 Apr 2025 19:26:11 GMT</pubDate>
            <description><![CDATA[<h1 id="문제상황">문제상황</h1>
<hr>
<p>게임 데이터 조회 API 를 개발하면서 성능 이슈를 겪고 있었다. 사용자 정보와 최근 경기 기록을 불러오는 과정이 생각보다 오려 걸려서 화면 로딩이 느려지는 문제가 있었다. 특히 MongoDB 쿼리를 여러 번 순차적으로 실행하면서 시간이 많이 소요되었다.</p>
<br/>

<h1 id="해결과정">해결과정</h1>
<hr>
<h2 id="처음-해결해보려고-했던-방법">처음 해결해보려고 했던 방법</h2>
<p>일단 tanstack query 에서 placeholder data 를 제공해준다. </p>
<pre><code class="language-js">const {
    data: userDetail = [],
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: [&quot;userDetail&quot;, player?.name],
    queryFn: () =&gt; fetchPlayerDetail(player?._id),
    enabled: !!player?._id,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
    staleTime: 1000 * 60 * 5,
    placeholderData: keepPreviousData,
  });</code></pre>
<p>이처럼 placeholderData 를 쓰면 새 데이터가 로딩되기 전까지 &quot;잠깐 대체용&quot;으로 이전 데이터를 보여주는 역할을 한다. 그래서 로딩 상태를 안 보이게 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/75b26766-434c-485c-9928-e0888dee3c5b/image.gif" alt=""></p>
<p>이러면서 UX 를 개선할 수 있지만 단순 눈속임일 뿐이라 생각했다. 실질적으로 데이터 통신을 더 빠르게 할 수 있는 방법이 없나 검색해보다가 Promise 를 써서 병렬 처리를 하면 좀 더 빠르게 데이터 통신을 할 수 있다고 알았다.</p>
</br>

<h2 id="기존-방식-순차적-데이터-로딩">기존 방식: 순차적 데이터 로딩</h2>
<p>처음에는 아래와 같이 순차적으로 데이터를 불러오는 방식을 사용했다.</p>
<ol>
<li><p>사용자 기본 정보 조회</p>
</li>
<li><p>가장 많이 플레이한 챔피언 조회</p>
</li>
<li><p>최근 5경기 데이터 조회</p>
</li>
<li><p>최근 5경기 승률 데이터 조회</p>
</li>
</ol>
<pre><code class="language-js">// 유저 기본 정보 조회
const user = await User.findById(id)
  .select(&quot;name nickName position eloRating&quot;)
  .lean();

// 유저의 가장 많이 플레이한 챔피언
const mostPlayedChampion = await Match.aggregate([
  // 집계 파이프라인...
]);

// 유저의 최근 5경기
const recentMatches = await Match.find({
  &quot;players.userNickname&quot;: user.nickName,
})
  .sort({ createdAt: -1 })
  .limit(5);

// 유저의 최근 5경기의 승률
const recentMatchesData = await Match.aggregate([
  // 집계 파이프라인...
]);</code></pre>
<p>이렇게 순차적으로 데이터를 불러오면 한 쿼리가 완료된 후 다음 쿼리가 실행되어 전체 처리 시간이 길어졌다.</p>
<h3 id="성능-측정">성능 측정</h3>
<p>순차적 처리 방식의 성능을 측정하기 위해 <code>console.time</code> 과 <code>console.timeEnd</code> 를 사용했다.</p>
<pre><code class="language-js">console.time(&quot;순차적&quot;);
// 모든 순차적 쿼리 실행
console.time(&quot;순차적&quot;);</code></pre>
<p>10번의 테스트 결과, 순차적 처리 방식은 평균적으로 다음과 같은 시간이 소요되었다.</p>
<ul>
<li>208ms, 180ms, 205ms, 204ms, 210ms, 219ms, 208ms, 204ms, 209ms, 209ms</li>
<li>평균: 205.6ms</li>
</ul>
<br/>

<h2 id="개선-방식-promiseall-을-활용한-병렬-처리">개선 방식: Promise.all 을 활용한 병렬 처리</h2>
<p>성능 개선을 위해 <code>Promise.all</code> 을 사용하여 여러 쿼리를 병렬로 실행하는 방식으로 변경했다.</p>
<pre><code class="language-js">// 유저 정보는 먼저 따로 불러와야 함
const user = await User.findById(id)
  .select(&quot;name nickName position eloRating&quot;)
  .lean();

// 닉네임을 기준으로 다른 쿼리를 병렬 처리
const [mostPlayedChampionAgg, recentMatches, recentMatchesDataAgg] = 
  await Promise.all([
    Match.aggregate([/* 첫 번째 쿼리 */]),
    Match.find({ /* 두 번째 쿼리 */ }),
    Match.aggregate([/* 세 번째 쿼리 */])
  ]);</code></pre>
<p>병렬 처리 방식의 성능을 동일하게 측정했다.</p>
<pre><code class="language-js">console.time(&quot;병렬&quot;)
// 병렬 쿼리 실행
console.timeEnd(&quot;병렬&quot;);</code></pre>
<p>10번의 테스트 결과, 병렬 처리 방식은 다음과 같은 시간이 소요되었다.</p>
<ul>
<li>127ms, 86ms, 130ms, 127ms, 124ms, 126ms, 124ms, 127ms, 108ms, 126ms</li>
<li>평균: 120.5ms</li>
</ul>
<br/>

<h2 id="성능-비교-결과">성능 비교 결과</h2>
<p>두 방식의 성능을 비교한 결과: </p>
<ul>
<li>순차적 처리: 평균 205.6ms</li>
<li>병렬 처리: 평균 120.5ms</li>
<li>개선율: 약 41.4% 향상</li>
</ul>
<br/>

<h1 id="개선-원리-promiseall-의-활용">개선 원리: Promise.all 의 활용</h1>
<hr>
<p>Promise.all 메서드는 여러 프로미스를 병렬로 실행하고 모든 프로미스가 완료되었을 때 결과를 반환한다. 이를 활용하면 서로 의존성이 없는 여렁 데이터베이스 쿼리를 동시에 실행할 수 있다.</p>
<p>주의할 점은 병렬 처리를 하려면 쿼리 간의 의존성을 파악해야 한다는 것이다. 내 경우에는 유저 기본 정보(특히 닉네임)가 다른 쿼리에 필요했기 때문에 유저 정보만 먼저 조회한 후, 나머지 세 가지 쿼리를 병렬로 처리했다.</p>
<pre><code class="language-js">// 1. 유저 정보 먼저 조회 (다른 쿼리들이 이 정보에 의존)
const user = await User.findById(id).lean();

// 2. 나머지 쿼리들은 병렬로 처리
const [query1Result, query2Result, query3Result] = await Promise.all([
  // 여러 쿼리 동시 실행
]);</code></pre>
<br/>

<h1 id="결론">결론</h1>
<hr>
<p><code>Promise.all</code>을 활용한 병렬 처리는 API 성능을 크게 향상시킬 수 있는 간단하면서도 효과적인 방법이다. 내 프로젝트에서는 약 41%의 성능 향상을 달성했고, 이는 사용자 경험 개선으로 이어졌다.</p>
<p>특히 여러 개의 독립적인 데이터베이스 쿼리가 필요한 경우, 순차적 처리 대신 병렬 처리를 고려해보는 것이 좋다. 다만 쿼리 간의 의존성을 파악하여 필요한 경우 일부 쿼리는 순차적으로 실행해야 한다는 점을 명심해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 이미지 로딩 최적화: Swiper 활용하기]]></title>
            <link>https://velog.io/@k_ddaddi/Next.js-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94-Swiper-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@k_ddaddi/Next.js-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94-Swiper-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 18 Apr 2025 02:17:54 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 Next.js의 Image 컴포넌트를 활용하여 이미지 로딩 시 발생하는 깜빡임 현상을 해결하는 방법을 소개했다. 하지만 실제 적용 후에도 콘텐츠와 배경 이미지가 처음 로드될 때 약간의 딜레이가 남아있었다. 이번 포스팅에서는 이 문제를 완벽하게 해결하기 위한 개선된 방법을 소개한다.</p>
<br/>
<br/>

<h1 id="남아있는-문제점">남아있는 문제점</h1>
<hr>
<p>기존 해결책(Next.js Image 컴포넌트 활용)으로 깜빡임 현상은 많이 개선되었지만, 다음과 같은 문제가 여전히 존재했다:</p>
<ol>
<li><p>콘텐츠와 배경 이미지 사이에 약간의 로딩 시간 차이가 발생</p>
</li>
<li><p>이미지 전환 시 완벽한 동기화가 이루어지지 않음</p>
</li>
</ol>
<br/>

<h2 id="새로운-접근법-swiper로-배경-이미지-처리하기">새로운 접근법: Swiper로 배경 이미지 처리하기</h2>
<p>기존에는 콘텐츠만 Swiper로 관리하고 배경 이미지는 단일 Image 컴포넌트로 처리했다. 이를 개선하기 위해 배경 이미지도 Swiper를 사용하여 관리하는 방식을 도입했다.</p>
<br/>

<h3 id="배경-이미지용-swiper-구현">배경 이미지용 Swiper 구현</h3>
<pre><code class="language-js">{/* 배경 이미지 Swiper */}
&lt;Swiper
  modules={[Autoplay, EffectFade]}
  effect=&quot;fade&quot;
  autoplay={{ delay: 3000, disableOnInteraction: false }}
  loop
  speed={1000}
  slidesPerView={1}
  onSlideChange={(swiper) =&gt; setActiveIndex(swiper.realIndex)}
  allowTouchMove={false}
  onSwiper={(swiper) =&gt; (bgSwiperRef.current = swiper)}
  className=&quot;absolute inset-0 opacity-70 z-0 rounded-2xl&quot;
&gt;
  {shiftedChampions.map((champion, idx) =&gt; {
    return (
      &lt;SwiperSlide key={idx}&gt;
        &lt;div className=&quot;relative w-full h-[37.5rem]&quot;&gt;
          &lt;Image
            src={champion.logo}
            alt={champion.name}
            fill
            priority
            className=&quot;object-cover&quot;
          /&gt;
        &lt;/div&gt;
      &lt;/SwiperSlide&gt;
    );
  })}
&lt;/Swiper&gt;</code></pre>
<br/>

<h3 id="콘텐츠용-swiper-구현">콘텐츠용 Swiper 구현</h3>
<pre><code class="language-js">{/* Swiper 콘텐츠 */}
&lt;div className=&quot;absolute inset-0 z-10 py-10 px-5&quot;&gt;
  &lt;Swiper
    modules={[Navigation, Autoplay]}
    navigation={{
      nextEl: &quot;.swiper-custom-next&quot;,
      prevEl: &quot;.swiper-custom-prev&quot;,
    }}
    spaceBetween={30}
    slidesPerView={4}
    loop={true}
    autoplay={{ delay: 3000, disableOnInteraction: false }}
    onSlideChange={(swiper) =&gt; setActiveIndex(swiper.realIndex)}
    speed={1000}
    allowTouchMove={false}
    onSwiper={(swiper) =&gt; (contentSwiperRef.current = swiper)}
    className=&quot;w-full h-full z-10&quot;
  &gt;
    {/* 슬라이드 콘텐츠 */}
  &lt;/Swiper&gt;
&lt;/div&gt;</code></pre>
<br/>

<h2 id="두-swiper-동기화하기">두 Swiper 동기화하기</h2>
<p>두 Swiper 간의 완벽한 동기화를 위해 참조 객체(useRef)를 활용했다.</p>
<pre><code class="language-js">// 스와이퍼 참조 객체
const contentSwiperRef = useRef(null);
const bgSwiperRef = useRef(null);

// 버튼 동기화
const handleNext = () =&gt; {
  contentSwiperRef.current?.slideNext();
  bgSwiperRef.current?.slideNext();
};

const handlePrev = () =&gt; {
  contentSwiperRef.current?.slidePrev();
  bgSwiperRef.current?.slidePrev();
};</code></pre>
<p>이 코드를 통해 네비게이션 버튼을 클릭할 때 콘텐츠와 배경 이미지가 동시에 전환되도록 처리했다.</p>
<br/>
<br/>

<h1 id="주요-개선-사항">주요 개선 사항</h1>
<hr>
<p><strong>1. EffectFade 모듈 활용</strong>
배경 이미지 Swiper에 <code>effect=&quot;fade&quot;</code> 옵션을 적용하여 부드러운 전환 효과를 구현했다. 이를 위해 Swiper의 EffectFade 모듈을 import 했다.</p>
<p><strong>2. 동일한 전환 속도 설정</strong>
두 Swiper 모두 <code>speed={1000}</code>으로 설정하여 전환 속도를 동일하게 맞췄다. 이를 통해 시각적으로 더 자연스러운 전환이 가능해졌다.</p>
<p><strong>3. 사용자 조작 방지</strong>
<code>allowTouchMove={false}</code> 옵션을 통해 사용자가 직접 슬라이드를 조작하는 것을 방지하고, 네비게이션 버튼을 통해서만 슬라이드를 전환할 수 있도록 했다.</p>
<p><strong>4. 이미지 최적화 유지</strong>
배경 이미지에도 여전히 Next.js의 Image 컴포넌트를 사용하고 <code>priority</code> 속성을 적용하여 로딩 최적화의 이점을 유지했다.</p>
<br/>
<br/>

<h2 id="결과-및-성능-향상">결과 및 성능 향상</h2>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/6695a936-3330-42d2-b2e5-2ac10caa934b/image.gif" alt=""></p>
<p>이 개선된 방식을 적용한 결과:</p>
<ol>
<li><p>배경 이미지와 콘텐츠 간의 로딩 시간 차이가 사라졌다.</p>
</li>
<li><p>이미지 전환 시 깜빡임이 완전히 제거되었다.</p>
</li>
<li><p>fade 효과로 인해 시각적으로 더 부드러운 전환이 가능해졌다.</p>
</li>
<li><p>네비게이션 버튼을 통한 조작 시 두 요소가 완벽하게 동기화되었다.</p>
</li>
</ol>
<br/>
<br/>

<h1 id="결론">결론</h1>
<hr>
<p>Next.js의 Image 컴포넌트와 Swiper를 함께 활용하면 이미지 로딩과 전환 시 발생하는 문제를 효과적으로 해결할 수 있다. 특히 배경 이미지와 콘텐츠를 각각 별도의 Swiper로 관리하고 동기화하는 방식은 사용자 경험을 크게 향상시키는 효과적인 방법이다.</p>
<p>이미지가 많이 사용되는 현대 웹 애플리케이션에서 이러한 최적화는 사용자 만족도를 높이고 웹사이트의 품질을 향상시키는 중요한 요소가 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js에서 이미지 로딩 속도 문제 해결하기]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%86%8D%EB%8F%84</link>
            <guid>https://velog.io/@k_ddaddi/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%86%8D%EB%8F%84</guid>
            <pubDate>Thu, 17 Apr 2025 18:25:32 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<p>웹 애플리케이션을 개발하다 보면 이미지 로딩과 관련된 문제에 종종 직면하게 된다. 특히 슬라이드 형태로 이미지가 전환될 때 아래와 같은 깜빡임 현상이 발생했다.
<img src="https://velog.velcdn.com/images/k_ddaddi/post/6a84b6db-5eb6-4530-b622-51f9ee848185/image.gif" alt=""></p>
<p>이런 깜빡임은 사용자 경험을 저해하는 요소로, 웹사이트의 품질을 떨어뜨리는 원인이 된다.</p>
<h1 id="기존-코드-분석">기존 코드 분석</h1>
<pre><code class="language-js">{/* 배경 이미지 */}
      &lt;div
        className=&quot;absolute inset-0 bg-black z-0 transition-all
        duration-300 rounded-xl&quot;
        style={{
          backgroundImage: `url(${background})`,
          backgroundSize: &quot;cover&quot;,
          backgroundPosition: &quot;center&quot;,
          opacity: 0.5,
        }}
      /&gt;

{/* 배경 이미지 preloading */}
      &lt;div className=&quot;hidden&quot;&gt;
        {champions.map((champion, idx) =&gt; (
          &lt;Image
            key={idx}
            src={champion.logo}
            alt=&quot;&quot;
            width={1}
            height={1}
            priority
          /&gt;
        ))}
      &lt;/div&gt;</code></pre>
<p>이 코드에서 발견된 문제점:</p>
<ol>
<li>배경 이미지는 일반 div의 style 속성으로 처리되어 최적화가 부족했다.</li>
<li>이미지 preloading을 위해 hidden div 안에 모든 이미지를 넣어두었지만 효율적이지 않았다.</li>
<li>width와 height가 1로 설정되어 실제 이미지 크기와 맞지 않았다.</li>
</ol>
<br/>
<br/>

<h1 id="해결-방법">해결 방법</h1>
<hr>
<p>Next.js의 Image 컴포넌트를 제대로 활용하여 문제를 해결했다.</p>
<pre><code class="language-js">{/* 배경 이미지 */}
      &lt;Image
        src={background}
        alt=&quot;배경 이미지&quot;
        fill
        className=&quot;object-cover z-0 rounded-xl opacity-50 
        transition-opacity duration-300&quot;
        priority
      /&gt;</code></pre>
<p>해결 후에는 깜빡임이 사라진 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/0fd69ccf-b04e-44cd-8733-6765e43ef9fb/image.gif" alt=""></p>
<br/>
<br/>

<h1 id="핵심-개선-사항">핵심 개선 사항</h1>
<hr>
<p><strong>1. Next.js Image 컴포넌트 활용</strong></p>
<ul>
<li>기존 div + backgroundImage 방식 대신 Next.js의 최적화된 Image 컴포넌트 사용<br/>

</li>
</ul>
<p><strong>2. fill 속성 적용</strong></p>
<ul>
<li>부모 요소의 크기에 맞게 이미지가 채워지도록 설정<br/>

</li>
</ul>
<p><strong>3. priority 속성 활용</strong></p>
<ul>
<li>LCP(Largest Contentful Paint) 요소로 인식하여 더 높은 우선순위로 로딩<br/>

</li>
</ul>
<p><strong>4. transition-opacity 적용</strong></p>
<ul>
<li>이미지 전환 시 부드러운 효과 제공</li>
</ul>
<br/>
<br/>

<h1 id="결론">결론</h1>
<hr>
<p>Next.js의 Image 컴포넌트를 적절히 활용하면 이미지 로딩 시 발생하는 깜빡임 현상을 효과적으로 해결할 수 있다. 특히 <code>priority</code> 속성과 <code>fill</code> 속성을 함께 사용하면 사용자 경험을 크게 향상시킬 수 있다. 이미지가 많은 웹 애플리케이션에서는 로딩 최적화가 매우 중요하므로, Next.js가 제공하는 이미지 최적화 기능을 적극적으로 활용하는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express 라우팅에서 고정 경로와 동적 경로의 선언 순서가 중요한 이유]]></title>
            <link>https://velog.io/@k_ddaddi/Express-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%97%90%EC%84%9C-%EA%B3%A0%EC%A0%95-%EA%B2%BD%EB%A1%9C%EC%99%80-%EB%8F%99%EC%A0%81-%EA%B2%BD%EB%A1%9C%EC%9D%98-%EC%84%A0%EC%96%B8-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@k_ddaddi/Express-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%97%90%EC%84%9C-%EA%B3%A0%EC%A0%95-%EA%B2%BD%EB%A1%9C%EC%99%80-%EB%8F%99%EC%A0%81-%EA%B2%BD%EB%A1%9C%EC%9D%98-%EC%84%A0%EC%96%B8-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 23 Oct 2024 17:36:47 GMT</pubDate>
            <description><![CDATA[<p>개인 프로젝트 중 Express로 api 주소 선언 중 다음과 같은 문제가 생겼다.</p>
<blockquote>
<p>CastError: Cast to ObjectId failed for value &quot;reset-today&quot; (type string) at path &quot;_id&quot; for model &quot;user&quot;
    at SchemaObjectId.cast </p>
</blockquote>
<p>GPT에 물어본 결과 이 에러는 <code>Mongoose</code>가 <code>reset-today</code> 라는 문자열을 MongoDB의 <code>_id</code> 필드로 해석하려고 시도하는 과정에서 발생한 것이다. MongoDB에서 <code>_id</code> 필드는 기본적으로 <code>ObjectId</code> 타입이어야 하지만, 지금 전달된 값이 문자열 <code>&quot;reset-today&quot;</code> 여서 문제가 발생한 것이다.</p>
<p>따라서 해결 방법으로는 </p>
<ol>
<li>라우팅 경로를 명확하게 고정 경로로 설정</li>
<li>경로 우선 순위 확인</li>
</ol>
<p>이 두 가지 방법을 추천하였다. 첫 번째 방법은 경로를 확실히 해줬으므로 문제가 되지않았다. 따라서, 두 번째 방법으로 해결을 하였다. 고정된 경로들을 동적 경로들보다 먼저 선언하도록 라우팅 파일을 수정해줬다.</p>
<pre><code class="language-js">// 고정된 경로
router.put(&quot;/reset-today&quot;, userController.resetUserTeam);

// 동적 경로
router.put(&quot;/:id&quot;, userController.updateUser);</code></pre>
<p>이렇게 하니 잘 작동을 하였다. 하지만, 왜 이렇게 해야 작동이 되는지 궁금해서 좀 더 찾아보았다.</p>
<br>

<hr>
<h1 id="1-고정-경로와-동적-경로의-차이">1. 고정 경로와 동적 경로의 차이</h1>
<h3 id="고정-경로">고정 경로</h3>
<p>고정 경로는 특정 URL에 대해서만 매칭된다. 예를 들어, 다음과 같은 경로는 URL이 정확히 <code>/reset-today</code> 일 때만 매칭된다.</p>
<pre><code class="language-js">router.put(&quot;/reset-today&quot;, (req, res) =&gt; {
  res.send(&quot;Resetting today&#39;s data&quot;);
});</code></pre>
<p>이 경로는 URL이 정확하게 <code>/reset-today</code>일 때만 실행된다. 다른 URL이 들어오면 이 경로는 매칭되지 않는다.</p>
<br>

<h3 id="동적-경로">동적 경로</h3>
<p>동적 경로는 경로에 들어오는 다양한 값들을 변수로 처리할 수 있다. 예를 들어, <code>/:id</code> 는 다양한 값이 들어올 수 있는 유연한 경로이다.</p>
<pre><code class="language-js">router.put(&quot;/:id&quot;, (req, res) =&gt; {
  res.send(`Updating user with ID: ${req.params.id}`);
});</code></pre>
<p>이 경로는 <code>/user/123</code> 같은 경로에서 <code>123</code> 을 <code>id</code> 로 받아 처리하는 등 유연하게 동작한다.</p>
<br>

<hr>
<h1 id="2-왜-고정-경로를-먼저-선언해야-할까">2. 왜 고정 경로를 먼저 선언해야 할까?</h1>
<p>Express는 라우터를 위에서 아래로 순차적으로 검사합니다. 즉, 요청이 들어왔을 때 가장 먼저 일치하는 경로가 실행됩니다. 고정 경로와 동적 경로가 함께 있을 때 고정 경로가 먼저 선언되지 않으면, 동적 경로가 고정 경로보다 먼저 매칭될 수 있습니다.</p>
<h3 id="예시-잘못된-선언-순서">예시: 잘못된 선언 순서</h3>
<pre><code class="language-js">// 동적 경로가 먼저 선언된 경우
router.put(&quot;/:id&quot;, (req, res) =&gt; {
  res.send(`Updating user with ID: ${req.params.id}`);
});

router.put(&quot;/reset-today&quot;, (req, res) =&gt; {
  res.send(&quot;Resetting today&#39;s data&quot;);
});</code></pre>
<p>이 경우, 만약 클라이언트가 <code>/reset-today</code> 로 요청을 보낸다면 Express는 첫 번째로 선언된 <code>/:id</code> 경로와 먼저 매칭된다. 따라서 <code>&quot;reset-today&quot;</code> 라는 문자열이 <code>id</code> 로 해석되어, 예상치 못한 동작이 발생하게 된다.</p>
<p>만약 이 <code>id</code> 값이 MongoDB의 <code>_id</code> 와 같이 특정 형식(예: <code>ObjectId</code> )으로 사용된다면, 다음과 같은 오류가 발생할 수 있다.</p>
<pre><code class="language-js">CastError: Cast to ObjectId failed for value &quot;reset-today&quot; at path &quot;_id&quot;</code></pre>
<p>이 오류는 MongoDB가 <code>reset-today</code> 라는 문자열을 <code>ObjectId</code> 로 변환하려고 시도했지만 실패했기 때문에 발생한다.</p>
<br>

<h3 id="해결책-고정-경로를-먼저-선언">해결책: 고정 경로를 먼저 선언</h3>
<p>이 문제를 해결하려면 고정 경로를 먼저 선언하여 동적 경로가 이를 덮어쓰지 않도록 해야 한다.</p>
<pre><code class="language-js">// 고정 경로를 먼저 선언
router.put(&quot;/reset-today&quot;, (req, res) =&gt; {
  res.send(&quot;Resetting today&#39;s data&quot;);
});

// 동적 경로는 그 이후에 선언
router.put(&quot;/:id&quot;, (req, res) =&gt; {
  res.send(`Updating user with ID: ${req.params.id}`);
});</code></pre>
<p>이제 <code>/reset-today</code> 로 요청이 들어오면 먼저 고정된 경로가 처리되고, 이후에 동적 경로가 처리된다. 이 방식으로 동적 경로가 고정된 경로와 충돌하는 문제를 방지할 수 있다.</p>
<br>

<hr>
<h1 id="3-결론">3. 결론</h1>
<p>Express에서는 고정 경로와 동적 경로를 선언할 때, 고정 경로를 먼저 선언해야 한다. 그렇지 않으면 동적 경로가 고정된 경로와 일치하여 잘못된 값이 변수로 처리되거나, 예상치 못한 에러가 발생할 수 있다.</p>
<p>다음과 같은 규칙을 기억:</p>
<ul>
<li>고정 경로는 특정 URL에만 매칭되므로 최우선으로 처리해야 한다.</li>
<li>동적 경로는 유연하게 다양한 값들을 처리할 수 있지만, 고정 경로를 덮어쓰지 않도록 주의해야 한다.</li>
</ul>
<p>Express의 라우팅 순서를 잘 이해하고 사용하면, 이런 문제를 피하고 애플리케이션이 의도한 대로 작동하도록 만들 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[secretOrPrivateKey must have a value 에러]]></title>
            <link>https://velog.io/@k_ddaddi/secretOrPrivateKey-must-have-a-value-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@k_ddaddi/secretOrPrivateKey-must-have-a-value-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Mon, 21 Oct 2024 07:49:19 GMT</pubDate>
            <description><![CDATA[<p>Nodejs 공부하던 중 로그인 테스트 하던 중 계속 <code>secretOrPrivateKey must have a value</code> 에러가 떴다. </p>
<p>일단 이 에러는 jsonwebtoken 라이브러리를 사용하여 토큰을 생성하거나 검증할 때 발생하는 오류이다. 찾아보니깐 이 에러는 주로 <code>jwt.sign()</code> 또는 <code>jwt.verify()</code> 함수에 비어 있거나 <code>undefined</code>인 <code>secretOrPrivateKey</code> 값을 전달했을 때 발생한다고 나온다. </p>
<pre><code class="language-js">require(&#39;dotenv&#39;).config();
const jwt = require(&#39;jsonwebtoken&#39;);
const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY;

UserSchema.methods.generateToken = function() {
  const token = jwt.sign({ _id: this.id }, JWT_SECRET_TOKEN);
  return token;
}</code></pre>
<p>이런 식으로 토큰을 반환해줬지만 막상 로그인에서 에러가 뜨는것이다. 분명 나는 .env 에 JWT_SECRET_KEY 도 정의해줬고 dotenv를 통해 process.env 를 node 상에서 사용할 수 있게 해줬는데 인식을 못하는 것이었다.</p>
<p>해결방법을 계속 찾아보는데 스택플로우에서 백틱을 써보라고 하였다.</p>
<p>그래서 다음과 같이 코드를 고쳤다.</p>
<pre><code class="language-js">const JWT_SECRET_KEY = `${process.env.JWT_SECRET_KEY}`;</code></pre>
<p>이랬더니 에러가 뜨지 않고 정상작동을 하는것을 확인하였다. 백틱을 쓰는것과 안쓴것의 차이를 모르겠다. gpt에 물어본 결과, 백틱을 감싸면 undefined 일지라도 템플릿 리터럴로 문자열로 처리되기 때문에 에러가 발생하지 않는다고 한다.</p>
<p>하지만, api 호출하는데 있어서 undefined가 되면 로그인 에러가 떠야 함에도 불구하고 잘 작동되는것을 보면 원래는 api 주소가 맞았다는 뜻일텐데 애매한 부분만 남고 해결이 되어버렸다.</p>
<p>ref: <a href="https://stackoverflow.com/questions/58673430/error-secretorprivatekey-must-have-a-value">https://stackoverflow.com/questions/58673430/error-secretorprivatekey-must-have-a-value</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js로 RESTful API 구축 및 사용]]></title>
            <link>https://velog.io/@k_ddaddi/Next.js%EB%A1%9C-RESTful-API-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@k_ddaddi/Next.js%EB%A1%9C-RESTful-API-%EA%B5%AC%EC%B6%95-%EB%B0%8F-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Thu, 27 Jun 2024 08:11:13 GMT</pubDate>
            <description><![CDATA[<p>Next.js는 React 기반의 프레임워크로, 파일 기반 라우팅 시스템과 서버 사이드(SSR) 기능을 제공한다. 이 글에서는 Next.js를 사용하여 RESTful API를 구축하고, 이를 클라이언트에서 호출하여 데이터를 가져오는 방법을 예제를 통해 알아보겠다.</p>
<br>

<h1 id="데이터베이스-연결-설정">데이터베이스 연결 설정</h1>
<hr>
<p>이전 글에서 MongoDB를 연결하는 방법을 알아보았다. 그 중, Mongoose를 통한 DB 연결을 할 것이다.</p>
<p><a href="https://velog.io/@k_ddaddi/Next%EC%97%90-MongoDB-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0#mongoose%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%97%B0%EA%B2%B0">Next.js 에서 MongoDB 연결 방법</a></p>
<pre><code class="language-js">// app/lib/utils.js

import mongoose from &#39;mongoose&#39;;

const connection = {};

export const connectToDb = async () =&gt; {
  try {
    if (connection.isConnected) {
      console.log(&#39;Using existing connection&#39;);
      return;
    }
    const db = await mongoose.connect(process.env.MONGO);
    connection.isConnected = db.connections[0].readyState;
  } catch (error) {
    console.log(error);
    throw new Error(error);
  }
};</code></pre>
<br>

<h1 id="api-정의">API 정의</h1>
<hr>
<p>이제 Next.js의 API Routes 기능을 사용하여 블로그 포스트를 가져오는 API를 정의한다. 이를 위해 <code>app/api/blog/[slug]/route.js</code> 파일을 생성하고, 특정 슬러그(slug)에 해당하는 포스트를 데이터베이스에서 가져오는 GET 핸들러를 작성한다.</p>
<pre><code class="language-js">// app/api/blog/[slug]/route.js

import { Post } from &#39;@/lib/models&#39;;
import { connectToDb } from &#39;@/lib/utils&#39;;
import { NextResponse } from &#39;next/server&#39;;

export const GET = async (request, { params }) =&gt; {
  const { slug } = params;
  try {
    await connectToDb();

    const post = await Post.findOne({ slug });
    return NextResponse.json(post);
  } catch (err) {
    console.log(err);
    throw new Error(&#39;Failed to fetch post!&#39;);
  }
};</code></pre>
<br>

<h1 id="api-사용">API 사용</h1>
<hr>
<p>API를 정의한 후, 클라이언트 측에서 이 API를 호출하여 데이터를 가져오는 방법을 살펴보겠다. 이를 위해 <code>app/blog/[slug]/page.jsx</code> 파일을 생성하고, 해당 파일에서 특정 슬러그에 대한 블로그 포스트 데이터를 가져와 렌더링한다.</p>
<pre><code class="language-jsx">// app/blog/[slug]/page.jsx

const getData = async (slug) =&gt; {
  const res = await fetch(`http://localhost:3000/api/blog/${slug}`);

  if (!res.ok) {
    throw new Error(&#39;Something went wrong!&#39;);
  }

  return res.json();
};

export default async function SinglePost({ params }) {
  const { slug } = params;
  const post = await getData(slug);

  return (
    &lt;div&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<br>

<h1 id="전체-코드-구조">전체 코드 구조</h1>
<hr>
<p>이번 예제에서 사용한 전체 코드 구조는 다음과 같다.</p>
<pre><code class="language-bash">/app
  /api
    /blog
      [slug]
        route.js
  /blog
    [slug]
      page.jsx


/lib
  utils.js
  models.js</code></pre>
<br>

<h1 id="결론">결론</h1>
<hr>
<p>이번 글에서는 Next.js를 사용하여 RESTful API를 정의하고, 이를 클라이언트에서 호출하여 데이터를 가져오는 방법을 블로그 포스트 예제를 통해 알아보았다. Next.js의 파일 기반 라우팅 시스템을 활용하면 API 엔드포인트와 페이지 컴포넌트를 효율적으로 관리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[next 데이터 패칭]]></title>
            <link>https://velog.io/@k_ddaddi/next-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD</link>
            <guid>https://velog.io/@k_ddaddi/next-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD</guid>
            <pubDate>Tue, 25 Jun 2024 00:09:58 GMT</pubDate>
            <description><![CDATA[<h2 id="data-fetching">Data fetching</h2>
<hr>
<pre><code class="language-tsx">async function testAPI() {
  try {
    const response = await fetch(&quot;http://localhost:3000/api/movies&quot;, {
      method: &quot;GET&quot;,
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();
    return result;
  } catch (error) {
    console.error(&#39;Error fetching API:&#39;, error);
    return [];
  }
}</code></pre>
<br>

<h2 id="mongodb-에-data-fetching">MongoDB 에 Data fetching</h2>
<hr>
<p>MongoDB에 데이터를 패칭하는 법</p>
<pre><code class="language-ts">// lib/fetchAndInsert.ts
import fetch from &#39;node-fetch&#39;;
import clientPromise from &#39;./mongodb&#39;;

async function fetchExternalData(apiUrl: string) {
  const response = await fetch(apiUrl);
  if (!response.ok) {
    throw new Error(`Failed to fetch data from ${apiUrl}: ${response.statusText}`)
  }

  const data = await response.json();
  return data;
}

async function insertDataToMongoDB(data: any) {
  const client = await clientPromise;
  const db = client.db(&#39;sample_mflix&#39;);
  const collection = db.collection(&#39;nomad&#39;);

  const result = await collection.insertMany(data);
  return result;
}

async function fetchDataAndInsertToMongoDB(apiUrl: string) {
  try {
    const data = await fetchExternalData(apiUrl);
    console.log(&#39;Data fetched from API: &#39;, data);

    const result = await insertDataToMongoDB(data);
    console.log(&#39;Data inserted to MongoDB: &#39;, result);
  } catch (error) {
    console.error(&quot;Error fetching data and inserting to MongoDB: &quot;, error);
  }
}

export default fetchDataAndInsertToMongoDB;</code></pre>
<br>


<pre><code class="language-ts">// app/api/fetch-and-insert/route.ts
import { NextRequest, NextResoponse } from &quot;next/server&quot;;
import clientPromise from &#39;../../../lib/mongodb&#39;;

export async function GET (request: NextRequest) {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL;
  try {
    await fetchDataAndInsertToMongoDB(apiUrl);
    return NextResponse.json({ message: &quot;데이터 패치 및 삽입 완료&quot; });
  } catch(error) {
    return NextResponse.json({ message: &quot;데이터 패치 및 삽입 실패&quot;, error: error.message });
  }
}</code></pre>
<br>

<pre><code class="language-tsx">// app/(home)/page.tsx
async function testAPI() {
  try {
    const response = await fetch(&quot;http://localhost:3000/api/fetch-and-insert&quot;, {
      method: &quot;GET&quot;,
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();
    return result;
  } catch (error) {
    console.error(&quot;Error fetching API: &quot;, error);
    return [];
  }
}</code></pre>
<p>데이터 패칭하는 법(캐싱)</p>
<pre><code class="language-jsx">const getData = async() =&gt; {
  const response = await fetch(&quot;https://jsonplaceholder.typicode.com/posts&quot;);

  if (!response.ok) {
    throw new Error(&quot;Something went wrong!&quot;);
  }

  return response.json();
}</code></pre>
<p>데이터 캐싱 하지않는 법</p>
<pre><code class="language-jsx">const getData = async() =&gt; {
  const response = await fetch(&quot;https://jsonplaceholder.typicode.com/posts&quot;, {
    cache: &#39;no-cache&#39;});

  if (!response.ok) {
    throw new Error(&quot;Something went wrong!&quot;);
  }

  return response.json();
}</code></pre>
<p>데이터 캐싱 유효기간 설정</p>
<pre><code class="language-jsx">const getData = async() =&gt; {
  const response = await fetch(&quot;https://jsonplaceholder.typicode.com/posts&quot;, {
    next: { revalidate: 3600 });

  if (!response.ok) {
    throw new Error(&quot;Something went wrong!&quot;);
  }

  return response.json();
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버-클라이언트 동기화를 위한 Next.js의 세 가지 접근법]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%84%9C%EB%B2%84-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Next.js%EC%9D%98-%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%A0%91%EA%B7%BC%EB%B2%95</link>
            <guid>https://velog.io/@k_ddaddi/%EC%84%9C%EB%B2%84-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Next.js%EC%9D%98-%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%A0%91%EA%B7%BC%EB%B2%95</guid>
            <pubDate>Mon, 24 Jun 2024 18:13:53 GMT</pubDate>
            <description><![CDATA[<h1 id="소개">소개</h1>
<hr>
<ul>
<li>Next.js 의 SSR과 CSR을 효과적으로 관리하는 방법에 대해 설명한다.</li>
<li><code>dynamic</code> , <code>useEffect</code> , <code>suppressHydrationWarning</code> 의 차이점과 각각의 사용 상황을 다룬다.</li>
</ul>
<br>

<h1 id="1-dynamic을-사용하여-동적-컴포넌트-로딩">1. dynamic을 사용하여 동적 컴포넌트 로딩</h1>
<hr>
<h2 id="-설명-">[ 설명 ]</h2>
<ul>
<li><code>dynamic</code> 함수는 클라이언트 전용 컴포넌트를 동적으로 로드할 수 있게 해준다.</li>
<li>SSR을 비활성화하여 초기 로딩 시간을 단축하고, 특정 컴포넌트를 필요할 때 로드한다.</li>
</ul>
<br>

<h2 id="-사용-예시-">[ 사용 예시 ]</h2>
<pre><code class="language-js">import dynamic from &#39;next/dynamic&#39;;

const HydrationTestNoSSR = dynamic(() =&gt; import(&#39;@/components/hydrationTest&#39;), 
                                   {ssr: false});

const Page = () =&gt; {
  &lt;div&gt;
      &lt;h1&gt;My Page&lt;/h1&gt;
      &lt;HydrationTestNoSSR /&gt;
  &lt;/div&gt;
}

export default Page;</code></pre>
<br>

<h2 id="-언제-사용하면-좋은가-">[ 언제 사용하면 좋은가? ]</h2>
<ul>
<li>클라이언트 전용 컴포넌트를 로드할 때</li>
<li>초기 로딩 성능을 최적화할 때</li>
<li>브라우저 API를 사용하는 컴포넌트를 다룰 때</li>
</ul>
<br>

<h1 id="2-useeffect를-사용하여-클라이언트-사이드-로직-실행">2. useEffect를 사용하여 클라이언트 사이드 로직 실행</h1>
<hr>
<h2 id="-설명--1">[ 설명 ]</h2>
<ul>
<li><code>useEffect</code> 훅은 컴포넌트가 마운트된 후 클라이언트 사이드에서 실행된다.</li>
<li>서버와 클라이언트 간의 상태를 동기화하거나 클라이언트에서 특정 로직을 실행하는 데 유용하다.</li>
</ul>
<br>

<h2 id="-사용-예시--1">[ 사용 예시 ]</h2>
<pre><code class="language-js">import { useState, useEffect } from &#39;react&#39;;

const TimeComponent = () =&gt; {
  const [time, setTime] = useState(&#39;&#39;);

  useEffect(() =&gt; {
    setTime(new Date().toLocalTimeString());
  }, []);

  return (
    &lt;div&gt;
        The Current time is: {time}
    &lt;/div&gt;
    );
};

export default TimeComponent;</code></pre>
<br>

<h2 id="-언제-사용하면-좋은가--1">[ 언제 사용하면 좋은가? ]</h2>
<ul>
<li>클라이언트에서 실행되어야 하는 로직이 있을 때</li>
<li>데이터 패칭, DOM 조작, 이벤트 리스너 설정 등 클라이언트 전용 작업</li>
</ul>
<br>

<h1 id="3-suppresshydrationwarning-으로-경고-억제">3. suppressHydrationWarning 으로 경고 억제</h1>
<hr>
<h2 id="-설명--2">[ 설명 ]</h2>
<ul>
<li>서버와 클라이언트 간의 HTML 불일치로 인해 발생하는 경고를 억제한다.</li>
<li>주로 클라이언트에서만 동적으로 업데이트되는 컴포넌트에서 사용된다.</li>
</ul>
<br>

<h2 id="-사용-예시--2">[ 사용 예시 ]</h2>
<pre><code class="language-js">import { useState, useEffect } from &#39;react&#39;;

const TimeComponent = () =&gt; {
  const [time, setTime] = useState(&#39;&#39;);

  useEffect(() =&gt; {
    setTime(new Date().toLocalTimeString());
  }, []);

  return (
    &lt;div suppressHydrationWarning&gt;
        The Current time is: {time}
    &lt;/div&gt;
    );
};

export default TimeComponent;</code></pre>
<br>

<h2 id="-언제-사용하면-좋은가--2">[ 언제 사용하면 좋은가? ]</h2>
<ul>
<li>서버와 클라이언트 간의 타이밍 차이로 인해 HTML 불일치가 발생할 때</li>
<li>클라이언트에서만 동적으로 변하는 데이터를 다룰 때</li>
</ul>
<br>

<h1 id="결론">결론</h1>
<hr>
<p>Next.js 에서 서버와 클라이언트 간의 동기화는 중요한 과제이다. </p>
<p><code>dynamic</code> 은 동적 컴포넌트 로딩과 초기 로딩 성능 최적화에 유용하며, 
<code>useEffect</code> 는 클라이언트 사이드 로직 실행에 필수적이다. 
<code>suppressHydrationWarning</code> 은 불필요한 경고를 억제하여 클라이언트에서만 동적으로 변하는 데이터를 처리할 때 유용하다. </p>
<p>각 방법의 적절한 사용을 통해 더 나은 사용자 경험을 제공할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSR SSR ]]></title>
            <link>https://velog.io/@k_ddaddi/CSR-SSR</link>
            <guid>https://velog.io/@k_ddaddi/CSR-SSR</guid>
            <pubDate>Mon, 24 Jun 2024 12:27:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/98c8a4c0-0746-4887-9884-4d467a178d6a/image.png" alt=""></p>
<h1 id="csr-client-side-rendering">CSR (Client-Side Rendering)</h1>
<ul>
<li>사용자가 <a href="http://lama.dev/about">http://lama.dev/about</a> 페이지를 요청한다.</li>
<li>서버는 빈 <code>index.html</code> 파일을 클라이언트에게 보낸다.</li>
<li>클라이언트는 받은 <code>index.html</code> 파일을 통해 JavaScript 파일을 로드한다.</li>
<li>JavaScript가 실행되어 클라이언트 측에서 페이지가 렌더링된다.</li>
</ul>
<br>

<h2 id="-궁금점-">[ 궁금점 ]</h2>
<p>클라이언트가 받은 빈 <code>index.html</code> 파일을 통해 JavaScript 파일을 로드한다는게 정확히 무슨 뜻인지 이해가 안갔다. 그래서 다음과 같이 질문을 하였다.</p>
<blockquote>
<p>CSR 에서 클라이언트가 받은 빈 index.html 을 통해서 어떻게 JavaScript 파일을 로드하는가? html에 JavaScript 파일이 있나?</p>
</blockquote>
<blockquote>
<p>네, 정확히 맞습니다. CSR(Client-Side Rendering)에서는 서버가 빈 index.html 파일을 클라이언트에 보내지만, 이 HTML 파일에는 필요한 JavaScript 파일을 로드하는 스크립트 태그가 포함되어 있습니다. 이 스크립트 태그를 통해 클라이언트가 JavaScript 파일을 로드하고, 그 후에 해당 JavaScript 파일이 실행되어 페이지를 렌더링하게 됩니다.</p>
</blockquote>
<p>CSR의 경우, 기본적인 흐름은 다음과 같습니다:</p>
<ul>
<li>사용자가 특정 URL(<a href="http://lama.dev/about)%EC%9D%84">http://lama.dev/about)을</a> 요청합니다.</li>
<li>서버는 빈 index.html 파일을 클라이언트에게 보냅니다. 이 파일에는 필요한 JavaScript 파일을 로드하는 스크립트 태그가 포함되어 있습니다.</li>
<li>클라이언트는 받은 index.html 파일을 파싱하여, <code>&lt;script&gt;</code> 태그를 통해 JavaScript 파일을 로드합니다.</li>
<li>로드된 JavaScript 파일이 실행되면서, 클라이언트 측에서 페이지의 콘텐츠를 생성하고 렌더링합니다.</li>
</ul>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;title&gt;My App&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
  &lt;script src=&quot;/static/js/main.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<p>이 경우, <code>main.js</code> 파일이 로드되고 실행되면서, React 또는 다른 JavaScript 프레임워크가 <code>#root</code> 요소 안에 필요한 콘텐츠를 생성하게 됩니다.</p>
<p>CSR의 장점을 클라이언트 측에서 빠르게 인터렉티브한 페이지를 만들 수 있다는 것이며, 단점은 초기 로딩 속도가 SSR 보다 느릴 수 있고, SEO에 불리할 수 있다는 점입니다. 하지만 Next.js 와 같은 프레임워크를 사용하면 CSR과 SSR을 혼합하여 사용할 수 있기 때문에, 이러한 단점을 보완할 수 있습니다.</p>
</blockquote>
<br>

<h1 id="ssr-server-side-rendering">SSR (Server-Side Rendering)</h1>
<ul>
<li>사용자가 <a href="http://lama.dev/about">http://lama.dev/about</a> 페이지를 요청한다.</li>
<li>서버는 요청을 처리하여 완성된 <code>about.html</code> 파일을 클라이언트에게 보낸다.</li>
<li>클라이언트는 받은 <code>about.html</code> 파일을 그대로 렌더링하여 사용자에게 보여준다.</li>
<li>추가적인 인터렉티브 기능을 위해 JavaScript 파일이 로드된다.</li>
</ul>
<br>

<h2 id="-주요-차이점-">[ 주요 차이점 ]</h2>
<ul>
<li><p>CSR
초기 로딩 시 서버에서 빈 HTML 파일을 보내고, 클라이언트 측에서 JavaScript를 통해 콘텐츠를 렌더링한다. 따라서 초기 로딩 시간이 다소 길 수 있으며, 검색 엔진 최적화(SEO)에 불리할 수 있다.</p>
</li>
<li><p>SSR
서버에서 완성된 HTML 파일을 보내므로 초기 로딩 시간이 빠르고, SEO에 유리하다. 클라이언트 측에서도 JavaScript를 통해 추가적인 인터렉티브 기능을 제공할 수 있다.</p>
</li>
</ul>
<br>

<h3 id="출처">출처</h3>
<hr>
<p>!youtube[vCOSTG10Y4o]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[next config]]></title>
            <link>https://velog.io/@k_ddaddi/next-config</link>
            <guid>https://velog.io/@k_ddaddi/next-config</guid>
            <pubDate>Sat, 22 Jun 2024 10:05:13 GMT</pubDate>
            <description><![CDATA[<p><code>&lt;Image&gt;</code> 태그에 외부 이미지 가져오려면</p>
<p>```js
/** @type {import(&#39;next&#39;).NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: &#39;https&#39;,
        hostname: &#39;<a href="http://www.example.com&#39;">www.example.com&#39;</a>,
      }
    ]
  }
}</p>
<p>module.exports = nextConfig</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js Hook 사용법: usePathName, useRouter, useSearchParams]]></title>
            <link>https://velog.io/@k_ddaddi/next-hook</link>
            <guid>https://velog.io/@k_ddaddi/next-hook</guid>
            <pubDate>Fri, 21 Jun 2024 21:20:16 GMT</pubDate>
            <description><![CDATA[<p>Next.js 에서는 클라이언트 사이드 네비게이션을 쉽게 처리할 수 있도록 다양한 훅을 제공한다. 이 글에서는 <code>usePathname</code> , <code>useRouter</code> , <code>useSearchParams</code> 의 사용법과 예제를 소개한다.</p>
<h1 id="usepathname">usePathName</h1>
<hr>
<p><code>usePathName</code> 훅은 현재 URL의 경로(pathname)를 반환한다. 쿼리 파라미터를 포함하지 않고 오직 경로만 나타내기 때문에 URL의 구조를 확인할 때 유용하다.</p>
<pre><code class="language-jsx">// app/navigationTest/page.jsx

&#39;use client&#39;;
import { usePathName } from &quot;next/navigation&quot;;

const Navigation = () =&gt; {
  const pathname = usePathName();

  console.log(pathname); // 현재 경로 출력

  return (
    &lt;div&gt;
      localhost:3000/navigationTest
    &lt;/div&gt;
  )
}

export default Navigation;</code></pre>
<blockquote>
<p>콘솔창 결과
/navigationTest</p>
</blockquote>
<p>위 예제에서 <code>usePathName</code> 을 사용하여 현재 경로를 가져와 콘솔에 출력한다.</p>
<br>

<h1 id="userouter">useRouter</h1>
<hr>
<p><code>useRouter</code> 훅은 라우터 인스턴스를 반환하여 네비게이션을 프로그래밍 방식으로 제어할 수 있다. 페이지 이동, 페이지 새로고침, 뒤로가기, 앞으로가기 등의 작업을 수행할 수 있다.</p>
<pre><code class="language-jsx">&#39;use client&#39;;
import { useRouter } from &quot;next/navigation&quot;;

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

  const handleClick = () =&gt; {
    console.log(&quot;clicked&quot;);
    router.push(&#39;/&#39;); // 지정된 URL로 이동
    // router.replace(&#39;/&#39;); // 히스토리를 남기지 않고 이동
    // router.refresh(); // 현재 페이지 새로고침
    // router.back(); // 이전 페이지로 이동
    // router.forward(); // 다음 페이지로 이동
  }

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;Write and Redirect&lt;/button&gt;
    &lt;/div&gt;
  )
}

export default Navigation;</code></pre>
<p>이 예제에서는 버튼 클릭 시 <code>router.push(&#39;/&#39;)</code> 를 호출하여 루트 페이지로 이동한다.
<code>useRouter</code> 훅을 사용하여 다양한 네비게이션 동작을 제어할 수 있다.</p>
<br>

<h1 id="usesearchparams">useSearchParams</h1>
<hr>
<p><code>useSearchParams</code> 훅은 URL의 쿼리 파라미터를 읽고 설정하는 데 사용된다. 쿼리 파라미터를 가져오거나 변경할 때 유용하다.</p>
<pre><code class="language-jsx">&#39;use client&#39;;
import { useSearchParams } from &quot;next/navigation&quot;;

const Navigation = () =&gt; {
  const searchParams = useSearchParams();

  const q = searchParams.get(&quot;q&quot;);
  console.log(q); // &#39;q&#39; 파라미터 값 출력

  return (
    &lt;div&gt;
      localhost:3000/navigationTest
    &lt;/div&gt;
  )
}

export default Navigation;</code></pre>
<p><code>localhost:3000/navigationTest?q=test</code> URL에서 <code>q</code> 파라미터의 값을 가져와 콘솔에 출력한다.</p>
<blockquote>
<p>test</p>
</blockquote>
<br>

<h3 id="-쿼리-파라미터-설정-예시-">[ 쿼리 파라미터 설정 예시 ]</h3>
<p><code>set</code> 메서드를 사용하여 쿼리 파라미터를 설정하는 방법도 살펴보겠다.</p>
<br>

<pre><code class="language-jsx">&#39;use client&#39;;
import { useSearchParams } from &quot;next/navigation&quot;;

const Navigation = () =&gt; {
  const searchParams = useSearchParams();

  const q = searchParams.get(&#39;q&#39;);
  console.log(q); // 현재 &#39;q&#39; 파라미터 값 출력

  const handleClick = () =&gt; {
    const newSearchParams = new URLSearchParams(searchParams.toString());
    newSearchParams.set(&#39;q&#39;, &#39;hello&#39;); // &#39;q&#39; 파라미터를 &#39;hello&#39;로 설정

    router.push(`?${newSearchParams.toString()}`);
  }

  return (
    &lt;div&gt;
      &lt;button onClick={handleClick}&gt;
        Write and Redirect
      &lt;/button&gt;
    &lt;/div&gt;
  )
}

export default Navigation;</code></pre>
<p>버튼 클릭 시 <code>localhost:3000/navigationTest?q=test</code> 의 쿼리가 <code>localhost:3000/navigationTest?q=hello</code> 로 변경된다.</p>
<br>

<h1 id="요약">요약</h1>
<hr>
<ul>
<li>usePathName
현재 경로를 가져오는 훅</li>
<li>useRouter
네비게이션을 제어할 수 있는 훅</li>
<li>useSearchParams
쿼리 파라미터를 읽고 설정할 수 있는 훅</li>
</ul>
<p>이 세 가지 훅을 활용하면 Next.js 애플리케이션에서 클라이언트 사이드 네비게이션을 효과적으로 처리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next 캐싱 문제]]></title>
            <link>https://velog.io/@k_ddaddi/Next-%EC%BA%90%EC%8B%B1-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@k_ddaddi/Next-%EC%BA%90%EC%8B%B1-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 20 Jun 2024 12:22:37 GMT</pubDate>
            <description><![CDATA[<p>MongoDB의 Collection에 데이터를 패칭하고 패칭한 데이터를 이제 컴포넌트에 불러오는 것을 공부 중에 있었다.</p>
<h3 id="appapimoviesroutets">[app&gt;api&gt;movies&gt;route.ts]</h3>
<pre><code class="language-ts">import { NextRequest, NextResponse } from &quot;next/server&quot;;
import clientPromise form &quot;../../../lib/mongodb&quot;;

export async function GET(request: NextRequest) {
  console.log(&#39;--------&#39;);
  try {
    const client = await clientPromise;
    const db = client.db(&#39;sample_mflix&#39;);
    const movies = await db.collection(&#39;nomad&#39;).find({}).toArray();
    return NextResponse.json(movies);
  } catch (error) {
    return NextResponse.error();
  }
}</code></pre>
<br>

<h3 id="apphomepagetsx">[app&gt;(home)&gt;page.tsx]</h3>
<pre><code class="language-tsx">async function getMovies() {
  const response = await fetch(&#39;http://localhost:3000/api/movies&#39;, {
    method: &quot;GET&quot;,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }

  const movies = await response.json();
  return movies;
}


export default async function HomePage() {
  const movies = await getMovies();
  return (
    &lt;div&gt;
      {movies.map(movie =&gt; (
        &lt;Movie key={movie.id} id={movie.id} title={movie.title} /&gt;
        ))}
      &lt;/div&gt;
    )
}</code></pre>
<p>이렇게 하면 잘 렌더링 된다.</p>
<p>그러다 문뜩 MongoDB 의 데이터를 조작하면 어떻게 되는지 궁금해졌다.
그래서 collection 의 데이터를 조금 변형해봤다.</p>
<br>

<h2 id="inside-out2--inside-out3">Inside Out2 &gt; Inside Out3</h2>
<hr>
<p>제목을 바꿔봤고 전체 데이터를 갖고오는 것이 아닌 5개의 데이터를 갖고 오도록 하였다.
코드를 다음과 같이 바꿨다.</p>
<h3 id="appapimoviesroutets-1">[app&gt;api&gt;movies&gt;route.ts]</h3>
<pre><code class="language-ts">const movies = await db.collection(&#39;nomad&#39;).find({}).limit(5).toArray();</code></pre>
<p>이제 내 예상은 Inside Out의 제목은 3으로 바뀌어야했고 데이터는 5개만 가져와야했다.
하지만, 예상과 반대로 전과 다름없는 화면을 렌더링 하고 있었다.</p>
<p>무슨 문제인가 하고 곰곰히 생각해보았다.</p>
<h3 id="-1-mongodb-의-변경된-데이터가-반영이-안되었다-">[ 1. MongoDB 의 변경된 데이터가 반영이 안되었다. ]</h3>
<p>혹시나 MongoDB 에서만 적용이 되고 api fetch 할 때는 전혀 반영이 안되었을 수도 있다.
그래서 다음과 같은 url 주소에 접속해보았다.</p>
<pre><code>http://localhost:3000/api/movies</code></pre><p>이 곳에 들어가면 내가 패칭한 json이 있기 때문이다.
확인해본 결과, MongoDB 에서 변경한 데이터가 잘 패칭되었다. 그렇다면 api 의 문제는 아니다.</p>
<h3 id="-2-브라우저-캐싱-문제-">[ 2. 브라우저 캐싱 문제 ]</h3>
<p>그렇다면 캐싱문제가 아닐까 라는 생각을 하였다. 코드상에서는 문제가 없어 보였기 때문이다.
따라서, 강력새로고침 (Ctrl + F5) 를 해보았다.</p>
<p>그랬더니, Inside Out3 의 제목과 5개의 데이터들이 렌더링 되었다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 에서 MongoDB 연결 방법]]></title>
            <link>https://velog.io/@k_ddaddi/Next%EC%97%90-MongoDB-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@k_ddaddi/Next%EC%97%90-MongoDB-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Jun 2024 11:26:14 GMT</pubDate>
            <description><![CDATA[<p>Next.js 애플리케이션에서 MongoDB에 연결하는 방법에는 두 가지가 있다. <code>MongoClient</code> 를 사용하는 방법과 <code>Mongoose</code> 를 사용하는 방법이다. 이 블로그 글에서는 두 가지 방법을 모두 다룰 것이다.</p>
<br>

<h1 id="mongoclient를-이용한-방법">MongoClient를 이용한 방법</h1>
<hr>
<p>먼저, <code>MongoClient</code> 를 이용하여 MongoDB에 연결하는 방법을 살펴보겠다.</p>
<h2 id="코드">코드</h2>
<pre><code class="language-ts">import { MongoClient } from &#39;mongodb&#39;;

const uri = process.env.MONGODB_URI || &#39;&#39;;
const options = {}

if (!uri) {
  throw new Error(&#39;Please add your Mongo URI to .env local&#39;);
}

let client;
let clientPromise: Promise&lt;MongoClient&gt;;

if (process.env.NODE_ENV === &#39;development&#39;) {

  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
    global._mongoClientPromise = client.connect();
  }
  clientPromise = global._mongoClientPromise;
} else {
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}

export default clientPromise;</code></pre>
<br>

<h2 id="코드-설명">코드 설명</h2>
<hr>
<h3 id="-mongodb-클라이언트-가져오기-">[ MongoDB 클라이언트 가져오기 ]</h3>
<pre><code class="language-ts">import { MongoClient } from &#39;mongodb&#39;;</code></pre>
<p><code>mongodb</code> 패키지에서 <code>MongoClient</code> 를 가져온다. 이를 통해 MongoDB 클라이언트를 생성할 수 있다.</p>
<br>

<h3 id="-uri-및-옵션-설정-">[ URI 및 옵션 설정 ]</h3>
<pre><code class="language-ts">const uri = process.env.MONGODB_URI || &#39;&#39;;
const options = {}

console.log(&#39;MONGODB_URI: &#39;, uri);</code></pre>
<p>환경 변수 <code>MONGODB_URI</code> 에서 MongoDB URI 를 가져오고, 이 URI 가 설정되지 않은 경우 빈 문자열을 기본값으로 사용한다. 연결 옵션은 빈 객체로 설정되어 있다. 또한, URI 를 콘솔에 출력하여 확인한다.</p>
<br>

<h3 id="-uri-유효성-검사-">[ URI 유효성 검사 ]</h3>
<pre><code class="language-ts">if (!uri) {
  throw new Error(&#39;Please add your Mongo URI to .env local&#39;);
}</code></pre>
<p>URI 가 비어 있는 경우, 에러를 발생시켜 사용자가 올바른 URI 를 설정하지 않았음을 알린다.</p>
<br>

<h3 id="-클라이언트-및-클라이언트-프라미스-변수-선언-">[ 클라이언트 및 클라이언트 프라미스 변수 선언 ]</h3>
<pre><code class="language-ts">let client;
let clientPromise: Promise&lt;MongoClient&gt;;</code></pre>
<p>MongoDB 클라이언트와 클라이언트 연결 프라미스를 저장할 변수를 선언한다.</p>
<br>

<h3 id="-개발-환경에서의-설정-">[ 개발 환경에서의 설정 ]</h3>
<pre><code class="language-ts">if (process.env.NODE_ENV === &#39;development&#39;) {
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
    global._mongoClientPromise = client.connect();
  }
  clientPromise = global._mongoClientPromise;
} else {
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}</code></pre>
<ul>
<li><p>개발 환경 (<code>process.env.NODE_ENV === &#39;development&#39;</code>) 에서는 전역 객체 <code>global</code> 에 <code>MongoClient</code> 연결 프라미스를 저장한다. 이렇게 하면 개발 환경에서 코드가 여러 번 실행되더라도 같은 연결을 재사용할 수 있다.</p>
</li>
<li><p><code>global._mongoClientPromise</code> 가 설정되지 않은 경우, 새로운 클라이언트를 생성하고 연결한다.</p>
</li>
<li><p>개발 환경이 아닐 때는, 클라이언트를 생성하고 바로 연결한다.</p>
</li>
</ul>
<br>

<h3 id="-클라이언트-프라미스-내보내기-">[ 클라이언트 프라미스 내보내기 ]</h3>
<pre><code class="language-ts">export default clientPromise;</code></pre>
<p><code>clientPromise</code> 를 기본 내보내기로 설정하여 다른 모듈에서 이 클라이언트를 사용할 수 있게 한다.</p>
<br>

<h1 id="mongoose를-이용한-연결">Mongoose를 이용한 연결</h1>
<hr>
<p>다음은 <code>Mongoose</code> 를 이용하여 MongoDB에 연결하는 방법이다.</p>
<h2 id="코드-1">코드</h2>
<pre><code class="language-js">import mongoose from &#39;mongoose&#39;;

const connection = {};

export const connectToDb = async () =&gt; {
  try {
    if (connection.isConnected) {
      console.log(&#39;Using existing connection&#39;);
      return;
    }
    const db = await mongoose.connect(process.env.MONGO);
    connection.isConnected = db.connections[0].readyState;
  } catch (error) {
    console.log(error);
    throw new Error(error);
  }
};</code></pre>
<h2 id="코드-설명-1">코드 설명</h2>
<h3 id="-mongoose-가져오기-">[ Mongoose 가져오기 ]</h3>
<pre><code class="language-js">import Mongoose from &#39;mongoose&#39;;</code></pre>
<p><code>mongoose</code> 패키지를 가져와 MongoDB와 상호작용할 수 있는 도구를 제공한다.</p>
<br>

<h3 id="-연결-상태를-저장할-객체-선언-">[ 연결 상태를 저장할 객체 선언 ]</h3>
<pre><code class="language-js">const connection = {};</code></pre>
<p>연결 상태를 저장하기 위한 객체를 선언한다.</p>
<br>

<h3 id="-데이터베이스-연결-함수-">[ 데이터베이스 연결 함수 ]</h3>
<pre><code class="language-js">export const connectToDb = async() =&gt; {
  try {
    if (connection.isConnected) {
      console.log(&quot;Using existing connection&quot;);
      return;
    }
    const db = await mongoose.connect(process.env.MONGODB);
    connection.isConnected = db.connections[0].readyState;
  } catch (err) {
    console.error(err);
    throw new Error(err);
  }
};</code></pre>
<ul>
<li>이미 연결된 상태라면 기존 연결을 재사용하도록 설정한다.</li>
<li><code>mongoose.connect</code> 를 통해 MongoDB에 연결한다.</li>
<li>연결이 성공하면, 연결 상태를 업데이트한다.</li>
<li>연결 과정에서 에러가 발생하면 이를 콘솔에 출력하고 에러를 발생시킨다.</li>
</ul>
<br>

<h1 id="장단점">장단점</h1>
<hr>
<h2 id="mongoclient">MongoClient</h2>
<h3 id="-장점-">[ 장점 ]</h3>
<ol>
<li><p>직접적인 제어
MongoClient는 MongoDB의 기본 클라이언트 라이브러리로, MongoDB와의 상호작용을 직접 제어할 수 있다. 이를 통해 최적화된 쿼리와 데이터베이스 작업을 수행할 수 있다.</p>
</li>
<li><p>경량
MongoClient는 가벼운 라이브러리로, 추가적인 추상화 계층이 없다. 따라서 메모리 사용량이 적고, 성능이 빠르다.</p>
</li>
<li><p>유연성
MongoDB의 모든 기능을 직접 활용할 수 있으며, 피룡한 기능을 선택적으로 사용할 수 있다.</p>
</li>
</ol>
<h3 id="-단점-">[ 단점 ]</h3>
<ol>
<li><p>추가 코드 필요
데이터 모델링, 스키마 관리, 검증 등의 작업을 직접 구현해야 하므로 코드 작성량이 증가할 수 있다.</p>
</li>
<li><p>복잡성 증가
복잡한 쿼리나 데이터 변환을 처리할 때 추가적인 로직이 필요하므로, 코드가 복잡해질 수 있다.</p>
</li>
</ol>
<br>

<h2 id="mongoose">Mongoose</h2>
<h3 id="-장점--1">[ 장점 ]</h3>
<ol>
<li><p>스키마 기반 모델링
Mongoose는 스키마 기반 데이터 모델링을 제공하여 데이터 구조를 명확하게 정의하고, 데이터의 일관성을 유지할 수 있다.</p>
</li>
<li><p>내장 검증 및 후크
데이터 유효성 검사를 자동으로 수행하고, 데이터베이스 작업 전후에 실행되는 후크(hook)를 지원한다. 이를 통해 데이터 무결성을 보장할 수 있다.</p>
</li>
<li><p>편리한 쿼리 API
Mongoose는 MongoDB 쿼리를 보다 간결하게 작성할 수 있는 API를 제공한다. 이를 통해 복잡한 쿼리도 쉽게 작성할 수 있다.</p>
</li>
<li><p>가상 필드 및 메소드
스키마에 가상 필드와 커스텀 메소드를 추가하여 데이터를 가공하거나 확장할 수 있다.</p>
</li>
</ol>
<h3 id="-단점--1">[ 단점 ]</h3>
<ol>
<li><p>추상화 오버헤드
Mongoose는 MongoDB 위에 추가적인 추상화 계층을 두기 때문에, 성능이 조금 저하될 수 있다.</p>
</li>
<li><p>추가 학습 필요
Mongoose의 고유한 문법과 기능을 익혀야 하므로, 초기 학습 비용이 발생할 수 있다.</p>
</li>
<li><p>유연성 감소
스키마 기반 모델링으로 인해, 스키마를 유연하게 변경하거나 동적 데이터를 처리하는 데 제약이 있을 수 있다.</p>
</li>
</ol>
<br>

<h2 id="결론">결론</h2>
<ul>
<li><p>MongoClient는 MongoDB와의 상호작용을 직접 제어하고 싶고, 추가적인 추상화 없이 경량의 클라이언트를 선호하는 경우에 적합하다. 예를 들어, 간단한 CRUD 애플리케이션이나 특정 성능 최적화가 필요한 경우에 유용하다.</p>
</li>
<li><p>Mongoose는 스키마 데이터 모델링, 데이터 검증, 편리한 쿼리 API 등을 제공하므로, 데이터 구조가 명확하고 복잡한 비즈니스 로직을 구현하는 데 유리하다. 예를 들어, 대규모 애플리케이션이나 데이터 무결성이 중요한 프로젝트에서 유용하다.</p>
</li>
</ul>
<br>

<h1 id="마무리">마무리</h1>
<hr>
<p>이 글에서는 Next.js 애플리케이션에서 MongoDB에 연결하는 두 가지 방법을 소개했다.
<code>MongoClient</code> 와 <code>Mongoose</code> 를 사용한 방법 모두 장단점이 있으므로, 프로젝트의 필요에 따라 적절한 방법을 선택하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드 스플리팅 (Code Splitting)]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85-Code-Splitting</link>
            <guid>https://velog.io/@k_ddaddi/%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%94%8C%EB%A6%AC%ED%8C%85-Code-Splitting</guid>
            <pubDate>Fri, 26 Apr 2024 14:51:10 GMT</pubDate>
            <description><![CDATA[<h2 id="코드-스플리팅이란">코드 스플리팅이란?</h2>
<hr>
<p>코드 스플리팅은 웹 애플리케이션의 성능을 개선하기 위한 기법 중 하나로, 전체 애플리케이션을 작은 청크로 나누어 필요할 때마다 로드할 수 있도록 하는 방식이다. 이 방법은 애플리케이션의 초기 로딩 시간을 크게 줄일 수 있으며, 사용자 경험을 향상시키는데 기여한다.</p>
<br>

<h2 id="모듈화">모듈화</h2>
<hr>
<h3 id="모듈">[모듈?]</h3>
<p>IDE( 또는 코드에디터) 에서 작성한 자바스크립트 파일 하나가 모듈 하나다.
개발자는 <code>export</code> 와 <code>import</code> 를 사용해서 모듈(파일) 간 코드를 공유한다.</p>
<h3 id="모듈화-1">[모듈화]</h3>
<p>자바스크립트 버전이 ES5 에서 ES6 으로 넘어오면서 변화 중 하나는 모듈화가 가능해졌다는 점이다.
ES5 까지는 <code>전역 스코프</code>만 가능했지만, ES6 부터는 <code>모듈 스코프</code>가 가능해져 특정 파일만 불러올 수 있다.</p>
<p>다음은 정적으로 <code>import</code> 하는 법이다.</p>
<pre><code class="language-js">import { FieldValues, SubmitHandler, useForm } from &#39;react-hook-form&#39;;</code></pre>
<p>이와 같이 선언된 스크립트는 실제로 사용되지 않더라도 전부 불러오게 된다.</p>
<p>다음은 동적으로 <code>import</code> 하는 법이다.</p>
<pre><code class="language-js">const { useForm } = await import(&#39;react-hook-form&#39;);

const { register } = useForm({ mode: &#39;onChange&#39;});</code></pre>
<p>이와 같이 동적으로 선언된 스크립트는, 런타임시 실제로 필요할 때 스크립트 파일을 볼러올 수 있다.</p>
<p>동적 <code>import</code> 로 작성된 코드는 WebPack, Rollup, Vite 등 코드 스플리팅을 지원하는 번들러에서 bundle 시 코드가 나누어져 번들링 된다.</p>
<p>이렇게 코드를 나누어 번들링하고, 런타임시 필요한 모듈을 불러오게 하는 것을 코드 스플리팅(Code Splitting) 이라고 한다.</p>
<br>

<h2 id="주요-이점">주요 이점</h2>
<hr>
<ul>
<li><p><span style="color:indianred"><strong>성능 최적화</strong></span>
사용자가 실제로 필요로 하는 코드만 로드하기 때문에 불필요한 데이터 전송을 줄이고, 애플리케이션의 응답성을 향상시킨다.</p>
</li>
<li><p><span style="color:indianred"><strong>효울적인 자원 관리</strong></span>
처음 애플리케이션을 로드할 때 모든 스크립트를 다운로드할 필요가 없으므로, 네트워크 자원과 처리 파워를 절약할 수 있다.</p>
</li>
<li><p><span style="color:indianred"><strong>유지 관리의 용이성</strong></span>
애플리케이션을 작은 청크로 나누어 관리하면, 특정 부분의 수정이 전체 애플리케이션에 미치는 영향을 최소화할 수 있다.</p>
</li>
</ul>
<br>

<h2 id="적용-사례">적용 사례</h2>
<hr>
<p>다음은 리모델링 프로젝트 때 썼던 라우팅 방법이다.</p>
<pre><code class="language-ts">{
    id: &#39;Main&#39;,
    path: &#39;/&#39;,
    text: &#39;메인 페이지&#39;,
    lazy: async () =&gt; {
      const Module = await import(&#39;@/pages/Main/Main&#39;);
      return { Component: Module.default };
    },
}</code></pre>
<br>

<p>다음은 부캠 파이널 프로젝트 때 썼던 라우팅 방법이다.</p>
<pre><code class="language-ts">const router = createBrowserRouter([
  // 루트 페이지 (메인)
  {
    path: &#39;/&#39;,
    element: &lt;RootLayout /&gt;,
    errorElement: &lt;MissingPage /&gt;,
    children: [
      {
        index: true,
        element: &lt;IntroPage /&gt;,
      },
      {
        path: &#39;main&#39;,
        element: (
          &lt;SetPage&gt;
            &lt;MainPage /&gt;
          &lt;/SetPage&gt;
        ),
      }</code></pre>
<p>둘의 차이점은 뭘까?</p>
<br>


<h2 id="차이점">차이점</h2>
<hr>
<h3 id="라우팅-방식의-차이">[라우팅 방식의 차이]</h3>
<p><span style="color:#648CFF"><strong>리모델링 프로젝트</strong></span>
비동기로 컴포넌트를 로드하는 코드 스플리팅이다. <code>lazy</code> 키워드를 통해 비동기 함수로 컴포넌트를 로드하고 있으며, 페이지 로드 시간을 줄이기 위해 사용된다. 해당 페이지가 필요할 때만 해당 컴포넌트를 로드하게 해서 초기 로딩 속도를 개선한다.</p>
<p><span style="color:#648CFF"><strong>파이널 프로젝트</strong></span>
<code>createBrowserRouter</code> 를 사용하여 라우터를 정의하고 있다.</p>
<p><span style="color:indianred">createBrowserRouter란?</span>
<code>React Router v6</code> 에서 도입된 API 중 하나로, 라우터를 생성한다. 더 선언적이고 직관적인 방식으로 라우트를 정의할 수 있게 해준다.
또한 코드의 가독성과 유지 보수성을 향상시킨다.</p>
<ul>
<li><p><span style="color:indianred">선언적 라우팅 구조</span>
라우트를 배열 형태로 선언적으로 정의할 수 있다. 각 라우트는 경로, 컴포넌트, 자식 라우트 등을 포함할 수 있으며, 이러한 구조는 애플리케이션의 라우팅 로직을 한 눈에 파악하기 쉽게 만들어 준다.</p>
</li>
<li><p><span style="color:indianred">네스팅된 라우트</span>
라우트 내에 <code>children</code> 속성을 사용하여 하위 라우트를 정의할 수 있다. 이를 통해 복잡한 라우팅 구조를 계층적으로 관리할 수 있으며, 각 레벨에서 필요한 컴포넌트를 렌더링할 수 있다.</p>
</li>
<li><p><span style="color:indianred">에러 핸들링</span>
<code>errorElement</code> 프로퍼티를 사용하여 특정 경로에서 발생할 수 있는 오류를 처리하는 컴포넌트를 정의할 수 있다. 이는 사용자에게 보다 친절한 에러 페이지를 제공하고, 예외 상황을 더욱 효과적으로 관리할 수 있게 해준다.</p>
</li>
</ul>
<br>

<h3 id="구성-요소의-사용">[구성 요소의 사용]</h3>
<p><span style="color:#648CFF"><strong>리모델링 프로젝트</strong></span>
단일 <code>lazy</code> 함수를 통해 비동기적으로 모듈을 로드하고, 모듈에서 기본으로 내보낸 컴포넌트를 사용한다. 이 방법은 라우트별로 모듈을 분리하여 필요할 때만 로드하는 코드 스플리팅을 통해 성능 최적화를 꾀할 수 있다.</p>
<p><span style="color:#648CFF"><strong>파이널 프로젝트</strong></span>
라우트별로 <code>element</code>, <code>errorElement</code>, <code>children</code> 구조를 사용하여, 더 세밀하고 계층적인 라우팅 구조를 제공한다. 각 라우트는 자체적으로 페이지 레이아웃, 에러 페이지, 하위 라우트 등을 정의할 수 있다. 이는 라우트 관리를 좀 더 유연하게 하며, 복잡한 애플리케이션에서 라우트 관리를 쉽게 할 수 있게 한다.</p>
<br>

<h3 id="성능-및-최적화">[성능 및 최적화]</h3>
<p><span style="color:#648CFF"><strong>리모델링 프로젝트</strong></span>
초기 로드 시간에 집중한 지연 로딩 방식을 사옹하여, 사용자가 실제로 방문하는 페이지의 컴포넌트만 로드한다.</p>
<p><span style="color:#648CFF"><strong>파이널 프로젝트</strong></span>
더 구조화된 접근 방식을 통해 복잡한 라우트를 관리하면서도 <code>React Router</code> 의 최신 기능을 활용하여 애플리케이션의 유지보수성을 향상시킨다.</p>
<br>

<h2 id="단점">단점</h2>
<hr>
<p>이렇게 보면 lazy loading 을 무조건 쓰는 것이 최적화에 도움이 되고 안 쓸 이유가 없을 것 같은데 단점이 없는것이 아닌가? 궁금해서 단점에 대해서 한번 찾아보았다.</p>
<ul>
<li><p><span style="color:indianred"><strong>지연 시간 발생</strong></span>
사용자가 특정 기능이나 페이지에 접근할 때 그 시점에서야 필요한 리소스를 로드하기 시작하므로, 사용자는 해당 컴포넌트가 로드될 때까지 기다려야 할 수 있다.</p>
</li>
<li><p><span style="color:indianred"><strong>코드 복잡성 증가</strong></span>
코드 스플리팅과 레이지 로딩을 구현하기 위해서는 추가적인 로직이 필요하며, 이로 인해 코드의 복잡성이 증가하며 개발과 유지 보수가 어려워질 수 있다.</p>
</li>
<li><p><span style="color:indianred"><strong>SEO 영향</strong></span>
검색 엔진 최적화에 부정적 영향을 줄 수 있다. 검색 엔진 크롤러가 자바스크립트를 완전히 실행하지 않고 페이지를 크롤링할 경우, 로드되지 않은 컨텐츠는 인덱싱되지 않을 수 있다.</p>
</li>
</ul>
<p>이렇게 단점들을 찾아보았는데, 이렇게 보면 사실 큰 단점이 없는 것 같다.</p>
<p>지연 시간 발생은 어차피 처음에 모든 스크립트들을 로드하는 것도 마찬가지일 것 같고,
코드 복잡성 증가는 사실 개발자 입장에서만 그렇지, 사용자들은 더 나은 경험을 할 것이다.
SEO 영향은 <code>React Helmet</code> 을 씌워도 안되는지 사실 잘 모르겠다. 해봐야 알 것 같다. 사실 그냥 리액트의 문제 아닌가?</p>
<br>

<h2 id="그럼-어느-때에-써야하나">그럼 어느 때에 써야하나?</h2>
<hr>
<p>그럼 최적의 사용 시나리오는 어떤 경우일까 궁금해졌다. 찾아보니 다음과 같았다.</p>
<ul>
<li><p><span style="color:indianred"><strong>대형 에플리케이션</strong></span>
많은 페이지와 기능을 가진 대형 애플리케이션에서는 사용자가 접근할 가능성이 낮은 부분까지 모두 초기에 로드하는 것은 비효율적이다. 이런 경우 <code>lazy loading</code> 을 통해 필요한 부분만 로드하도록 설정하는 것이 좋다.</p>
</li>
<li><p><span style="color:indianred"><strong>특정 기능이 중요도가 낮은 경우</strong></span>
예를 들어, 애플리케이션의 특정이 기능이 일부 사용자에게만 필요하다면, 그 기능의 로딩을 지연시키고 기본 기능에 더 빠르게 접근할 수 있도록 할 수 있다.</p>
</li>
</ul>
<p>대형 애플리케이션에 적용한다는 거는 조금만 생각하면 금방 알 수 있었던 이유다. 하지만 중요도는 생각치도 못했다. 중요도가 낮은 기능은 스크립트 로드시간을 초기에 하지말고 그 기능이 쓰일 때 로드될 때 하면 기본적인 기능의 접근은 더 빠르게 될 것이다. </p>
<p>작은 프로젝트 때는 굳이 <code>lazy</code> 를 써야할까 싶었는데, 전부 적용하는 것이 아닌 중요도가 낮은 기능에는 충분히 적용할만 한 것 같다.</p>
<br>

<h2 id="요약">요약</h2>
<hr>
<p>코드 스플리팅은 웹 애플리케이션의 초기 로딩 시간을 줄이고 사용자 경험을 개선하기 위해 전체 애플리케이션을 작은 청크로 나누어 필요할 때 로드할 수 있게 하는 기법이다. 모듈화는 ES6 이후 자바스크립트에서 가능해졌으며, <code>export</code>와 <code>import</code>를 통해 코드를 모듈 간에 공유할 수 있다. 정적 <code>import</code>는 코드가 항상 로드되지만, 동적 <code>import</code>는 코드가 실제 필요할 때만 로드되도록 하여 코드 스플리팅을 가능하게 한다.</p>
<p>코드 스플리팅의 이점으로는 성능 최적화, 효율적인 자원 관리, 유지 관리의 용이성 등이 있다. 하지만 단점으로는 지연 시간 발생, 코드 복잡성 증가, SEO에 대한 부정적 영향 등이 있다. 이 기법은 특히 대형 애플리케이션에서 효과적이며, 사용자가 자주 방문하지 않는 페이지나 중요도가 낮은 기능에 적용하면 초기 로드 시간을 줄이면서도 필요한 기능은 적시에 제공할 수 있다.</p>
<p>결론적으로, 코드 스플리팅은 애플리케이션의 성능과 사용자 경험을 향상시키는 중요한 전략이지만, 적용할 때는 애플리케이션의 구조와 사용자의 접근 패턴을 고려해 신중하게 결정해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이전 캐싱데이터 렌더링 문제]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%9D%B4%EC%A0%84-%EC%BA%90%EC%8B%B1%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@k_ddaddi/%EC%9D%B4%EC%A0%84-%EC%BA%90%EC%8B%B1%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 10 Apr 2024 04:50:31 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터-캐싱문제">데이터 캐싱문제</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/5d9fd966-704e-46de-82df-b2e041ee5a9f/image.png" alt=""></p>
<p>HealthyP 리팩토링 중 
건강식 | 다이어트 | 벌크업 | 비건 등 대분류 카테고리에서 캐싱문제가 있다는 것을 발견하였다.</p>
<p>예를 들어, 건강식을 누르면 데이터가 캐싱되고 다음으로 다이어트 카테고리를 누른다.
그러면, 건강식의 데이터들이 잠깐 나온다음 다이어트 데이터들이 나온다.</p>
<p>이처럼, 이전 카테고리의 데이터들이 잠깐씩 렌더링 된다는 것이 문제였다. 원인이 무엇인가 하니, tanstack query 에서 쓰이는 캐싱로직이 잘못되었다는 것을 알게되었다.</p>
<br>
<br>

<h2 id="기존-코드">[기존 코드]</h2>
<pre><code class="language-tsx">const { data, status, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryKey: &#39;recipes&#39;,
    queryFn: callbackFn,
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) =&gt; {
      const nextPage = lastPage.length ? allPages.length + 1 : undefined;
      return nextPage;
    },
  });</code></pre>
<p>어디가 문제인가하니 <code>querkKey</code> 부분이 문제였다.
건강식, 다이어트, 벌크업, 비건 심지어 사진에는 보이지 않지만 북마크 페이지 조차 <code>queryKey</code> 를 <code>recipes</code> 를 쓰고 있었다.</p>
<p>그래서, 5개의 페이지가 같은 데이터들을 캐싱하고 덮어씌우는 과정에 있었던 것이었다.</p>
<p>이 문제를 해결하기 위해, 각각 카테고리들에게 다른 <code>queryKey</code> 를 적용시킬 필요가 있었다.
다음은 수정한 코드이다.</p>
<br>

<h2 id="수정한-코드">[수정한 코드]</h2>
<pre><code class="language-tsx">interface infinityCardProps {
  callbackFn: (pageParam: { pageParam: number | undefined }) =&gt; Promise&lt;RecordModel[]&gt;;
  title: string;
}

export function useInfinityCard({ callbackFn, title }: infinityCardProps) {

  const { data, status, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({
    queryKey: [title],
    queryFn: callbackFn,
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) =&gt; {
      const nextPage = lastPage.length ? allPages.length + 1 : undefined;
      return nextPage;
    },
  });</code></pre>
<p>props 로 <code>title</code> 을 추가해줬다. 그리고 useInfinityCard 를 쓰는 CategoryPage 에서 <code>title</code> 을 추가해줬다.</p>
<br>

<pre><code class="language-tsx">export function CategoryPage() {
  const { title } = useParams();

  async function getRecipeData({ pageParam = 1 }) {
    if (title === &#39;오늘의 레시피&#39;) {
      const recordsData = await db.collection(&#39;recipes&#39;).getList(pageParam, 6, {
        expand: &#39;rating, profile&#39;,
        sort: &#39;-views&#39;,
      });
      return recordsData.items;
    } else {
      const recordsData = await db.collection(&#39;recipes&#39;).getList(pageParam, 5, {
        expand: &#39;rating, profile&#39;,
        filter: `category = &quot;${title}&quot;`,
        sort: &#39;-created&#39;,
      });

      return recordsData?.items;
    }
  }

  const { data, status, isFetchingNextPage, userData, ref } = useInfinityCard({
    callbackFn: getRecipeData,
    title: title || &#39;recipes&#39;,
  });</code></pre>
<p>그 결과, <a href="https://reactrouter.com/en/main/hooks/use-params">useParamas()</a> 에 의해 얻은 title 을 통해 각각의 카테고리에 알맞은 <code>queryKey</code> 를 배정해 줄 수 있었다. </p>
<p>그리고 각 <code>queryKey</code> 를 배정해준 결과, 이전 데이터들이 더이상 렌더링 되지 않고 정상적으로 현 카테고리들의 데이터들만 렌더링이 되었다.</p>
<br>
<br>

<h1 id="결과">결과</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/c12442bb-233b-4a43-a007-a09be147aed6/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스켈레톤 UI 적용하기]]></title>
            <link>https://velog.io/@k_ddaddi/%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@k_ddaddi/%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 28 Mar 2024 14:25:32 GMT</pubDate>
            <description><![CDATA[<h1 id="skeleton-ui">Skeleton UI</h1>
<hr>
<p>스켈레톤 컴포넌트는 데이터를 가져오는 동안 사용자가 로드 중인 상태를 시각적으로 알려주는 디자인 패턴이다. 이 방법은 데이터가 완전히 로드되기 전에 페이지의 레이아웃을 대략적인 형태로 보여준다. 이 형태는 일반적으로 회색 블록이나 줄들로 이루어져 있으며, 최종 컨텐츠가 들어갈 자리를 대략적으로 표현한다.</p>
<br>

<p><strong><span style="color:#648CFF"> 스켈레톤 UI의 주요 목적 </span></strong></p>
<ul>
<li><p><strong><span style="color:indianred">로딩 인디케이터</span></strong>
페이지나 앱이 여전히 작동 중이고 데이터가 로딩되고 있다는 것을 사용자에게 알린다. 이는 단순히 로딩 스피너나 진행 바를 보여주는 것보다 사용자에게 더 많은 정보를 제공한다.</p>
</li>
<li><p><strong><span style="color:indianred">인지적 부하 감소</span></strong>
사용자가 최종 컨텐츠의 형태와 구조를 예상할 수 있게 하여, 실제 컨텐츠가 로드될 때 정보를 더 빠르게 처리할 수 있도록 돕는다.</p>
</li>
<li><p><strong><span style="color:indianred">보다 나은 사용자 경험</span></strong>
빈 화면 대신 구조화된 플레이스홀더를 보여주어 사용자의 인내심을 돕고, 페이지가 로드되는 동안의 불편함을 줄여준다.</p>
</li>
</ul>
<br>

<p>스켈레톤 UI 는 데이터 로딩 시간이 긴 웹 애플리케이션, 특히 복잡한 데이터와 인터랙션이 많은 대시보드, 피드, 또는 목록이 있는 경우에 특히 유용하다. 이 패턴은 사용자에게 페이지 로딩 중인 상태를 더 명확하게 전달하고, 컨텐츠가 곧 등장할 것임을 암시하여 사용자의 관심을 유지하는 데 도움을 준다.</p>
<br>
<br>

<h1 id="tailwind-스켈레톤-ui-사용법">tailwind 스켈레톤 UI 사용법</h1>
<hr>
<p>tailwind 에는 따로 스켈레톤을 위한 유틸리티 css 가 없으니 직접 만들어 써야한다.</p>
<p>따라서 <code>tailwind.confing.js</code> 에 다음과 같은 코드를 추가해줘야한다.</p>
<br>

<pre><code class="language-js">// tailwind.config.js
extend: {
      animation: {
        shimmer: &#39;shimmer 1.5s infinite linear&#39;,
      },
      keyframes: {
        shimmer: {
          &#39;0%&#39;: { backgroundPosition: &#39;200%&#39; },
          &#39;100%&#39;: { backgroundPosition: &#39;-200%&#39; },
        },
      },
      backgroundImage: {
        &#39;gradient-custom&#39;: &#39;linear-gradient(to right, #D9D9D9 0%, #EDEEF1 50%, #D9D9D9 100%)&#39;,
      },
      backgroundSize: {
        custom: &#39;300% 100%&#39;,
      },
}     </code></pre>
<br>

<p>애니메이션은 본인 입맛대로 수정 및 작성해주면 된다.
그리고 이대로 쓸 수 있지만 여러곳에서 쓰일 것을 대비해 <code>utility css</code> 로 등록해둔다.</p>
<br>

<pre><code class="language-css">// index.css
@layer components {
  .skeleton {
    @apply animate-shimmer bg-gradient-custom bg-custom;
  }
}</code></pre>
<br>

<p><code>utility css</code> 로 등록을 한 다음, 다음과 같이 작성하면 된다.</p>
<br>

<pre><code class="language-tsx">// Skeleton.tsx
export default function SkeletonLargeCard() {
  return (
    &lt;div className=&quot;h-max overflow-hidden p-14pxr bg-white max-w-430pxr shrink-0 shadow-default&quot;&gt;
      &lt;div className=&quot;flex justify-between min-h-54pxr items-center&quot;&gt;
        &lt;div className=&quot;flex gap-4pxr items-center&quot;&gt;
          &lt;div className=&quot;size-30pxr bg-gray_400 rounded-[30px] skeleton&quot; /&gt;
          &lt;div className=&quot;w-60pxr h-25pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
        &lt;/div&gt;
        &lt;div className=&quot;w-30pxr h-25pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;w-full h-181pxr bg-gray_400 rounded-[5px] mb-20pxr skeleton&quot; /&gt;
      &lt;div className=&quot;flex gap-5pxr mb-20pxr&quot;&gt;
        &lt;div className=&quot;w-50pxr h-25pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
        &lt;div className=&quot;w-50pxr h-25pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;w-170pxr h-25pxr bg-gray_400 rounded-[5px] mb-10pxr skeleton&quot; /&gt;
      &lt;div className=&quot;w-full h-25pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
      &lt;div className=&quot;flex px-2pxr pt-20pxr pb-36pxr gap-4pxr items-center&quot;&gt;
        &lt;div className=&quot;w-50pxr h-20pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
        &lt;div className=&quot;w-50pxr h-20pxr bg-gray_400 rounded-[5px] skeleton&quot; /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<br>

<p>뼈대를 만든 다음 <code>skeleton</code> tailwind css 를 추가해주면 된다.
그리고 로딩 상태일 때 <code>Skeleton</code> 을 불러오면 다음과 같이 애니메이션이 작동한다.</p>
<p><img src="https://velog.velcdn.com/images/k_ddaddi/post/a562407c-fae9-45a8-a5cb-54a4b0174d80/image.gif" alt=""></p>
<br>
<br>

<h1 id="느낀점">느낀점</h1>
<hr>
<p>스켈레톤 을 적용하기 전에는 로고만 뚜둥 하고 뜨는 정도였다. 그마저도, 1초도 안되는 짧은 시간이었다. 네트워크를 통해 로딩속도를 조절하면 로고가 뜨고 빈 화면이 몇초간 유지가 되었다. </p>
<p>하지만, 스켈레톤 을 적용하였더니 일단 다음과 같은 장점을 느낄 수가 있었다.</p>
<ul>
<li><p><strong><span style="color:indianred">로딩중</span></strong>
일단, 데이터가 로딩중 이라는 것을 로고만 뜨는 것보다 확실히 알 수가 있었다. 더구나, 그냥 회색 스켈레톤 만 띄우면 잘 알 수가 없을것이다. 하지만, 하얀 빛이 오른쪽으로 이동하고 있는 것을 통해 <code>로딩중</code> 이라는 것을 확실히 알 수 있었다.</p>
</li>
<li><p><strong><span style="color:indianred">앞으로의 레이아웃</span></strong>
레이아웃이 예상이 된다. 이 점이 생각보다 꽤 큰 도움이 되는 것 같다. 레이아웃을 미리 볼 수 있음으로써 정보들의 배치 혹은 내가 원하는 정보의 위치를 예상할 수 있었다. 스켈레톤 골격 자체가 데이터가 생기고 난 후의 골격을 따라가다보니 배치에서의 큰 오차가 없었다.</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>