<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lim-bora.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 29 Nov 2024 17:27:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>lim-bora.log</title>
            <url>https://velog.velcdn.com/images/lim-bora/profile/0d404990-50e0-4d85-9d5f-d7788d1ece3e/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. lim-bora.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lim-bora" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Next js] spotify api 데이터 supabase 업로드 에러]]></title>
            <link>https://velog.io/@lim-bora/Next-js-spotify-api%EB%8D%B0%EC%9D%B4%ED%84%B0</link>
            <guid>https://velog.io/@lim-bora/Next-js-spotify-api%EB%8D%B0%EC%9D%B4%ED%84%B0</guid>
            <pubDate>Fri, 29 Nov 2024 17:27:44 GMT</pubDate>
            <description><![CDATA[<p>🔥 <strong>이슈</strong>
api 노래목록별로 추가버튼 생성 후 클릭시 supabase에 데이터가 올라가야하는데 에러</p>
<blockquote>
<p>supabase - playlist 테이블구성
id / user_id(로그인유저 아이디) / is_main(메인노래) / created(시점) / track_name(노래이름) / artist_name(가수이름) / track_id / album_image(앨범이미지링크)</p>
</blockquote>
<pre><code class="language-ts">&quot;use client&quot;;
import browserClient from &quot;@/utils/supabase/client&quot;;

const PlaylistAll = ({ playlist, setIsShowModal }: PlaylistAllProps) =&gt; {

  const [search, setSearch] = useState&lt;string&gt;(&quot;&quot;);

  const handleAddPlayList = async (track: SpotifyTrack) =&gt; {
    try {
      const { data, error } = await browserClient.from(&quot;playlist&quot;).insert({
        track_id: track.id,
        track_name: track.name,
        artist_name: track.artists[0].name,
        album_image: track.album.images[0]?.url
      });
      if (error) console.error(&quot;추가중 오류 발생:&quot;, error);
      else console.log(&quot;트랙 추가&quot;, data);
    } catch (error) {
      console.error(&quot;그 외 에러:&quot;, error);
    }
  };

  return (
                    ... 
           &lt;button className=&quot;btn&quot; onClick={() =&gt; handleAddPlayList(list.track)}&gt;
              +
           &lt;/button&gt;
  );
};
export default PlaylistAll;
</code></pre>
<p>🔎 원인
테이블의 각 행별로 설정상 null은 허용하지않는다 해두었다.
 playlist에 값을 insert 해주면서 user_id 의 값을 넘겨주지않아, user_id 값이 null로 넘어가면서 생긴문제</p>
<pre><code class="language-ts">&quot;use client&quot;;
import browserClient from &quot;@/utils/supabase/client&quot;;

const PlaylistAll = ({ playlist, setIsShowModal }: PlaylistAllProps) =&gt; {

  const [search, setSearch] = useState&lt;string&gt;(&quot;&quot;);

  const handleAddPlayList = async (track: SpotifyTrack) =&gt; {
    try {
      // 현재 로그인된 사용자 user_id
      const { data: user } = await browserClient.auth.getUser();
      if (!user) {
        console.error(&quot;로그인한 유저가 없습니다.&quot;);
        return;
      }

      const { data, error } = await browserClient.from(&quot;playlist&quot;).insert({
        track_id: track.id,
        track_name: track.name,
        artist_name: track.artists[0].name,
        user_id: user.user.id,
        album_image: track.album.images[0]?.url
      });
      if (error) console.error(&quot;추가중 오류 발생:&quot;, error);
      else console.log(&quot;트랙 추가&quot;, data);
    } catch (error) {
      console.error(&quot;그 외 에러:&quot;, error);
    }
  };

  return (
                    ... 
           &lt;button className=&quot;btn&quot; onClick={() =&gt; handleAddPlayList(list.track)}&gt;
              +
           &lt;/button&gt;
  );
};
export default PlaylistAll;</code></pre>
<p>✅ 해결</p>
<ul>
<li><code>browserClient.auth.getUser()</code> : 로그인한 유저의 정보를 가져온다.<ul>
<li>만약 로그인한 유저의 정보가 없다면 유효성 처리</li>
</ul>
</li>
<li>playlist에 값을 insert하면서 <code>user_id</code> 도 같이 넘겨준다.<ul>
<li><code>user_id: user.user.id,</code><ul>
<li>처음에 <code>user_id:use.id</code> 라고 적었다가 에러가 떠서 다시 데이터값을 확인했을때 
user &gt; user{id : ..,name:…} 으로 되어있는걸 확인하여 재수정하였다.</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 중복코드개선, 무한루프 ]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%EC%A4%91%EB%B3%B5%EC%BD%94%EB%93%9C%EA%B0%9C%EC%84%A0-%EB%AC%B4%ED%95%9C%EB%A3%A8%ED%94%84</link>
            <guid>https://velog.io/@lim-bora/Next-js-%EC%A4%91%EB%B3%B5%EC%BD%94%EB%93%9C%EA%B0%9C%EC%84%A0-%EB%AC%B4%ED%95%9C%EB%A3%A8%ED%94%84</guid>
            <pubDate>Fri, 29 Nov 2024 16:12:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>카페모아 프로젝트에서 반복되는 코드와 전역으로 관리해야할 필요성을 느낀점을 기록화하였다.</p>
</blockquote>
<h3 id="1-카테고리별-필터링된-리스트-목록화">1. 카테고리별 필터링된 리스트 목록화</h3>
<p>🔎 문제점<br>필터링된 리스트를 목록화하는건 카테고리/지역/메인배너/추천리스트 등에 모두 같은 동작으로 중복코드가 다수 발생한다.</p>
<p>✅ 해결 
전역함수로 인자만 전달하는 방식으로 전환했을때 중복코드를 개선할 수 있었다.</p>
<pre><code class="language-ts">//utils.jsx

//전체데이터에서 전달받은 필터값을 가진 포스트만 추려서 list에 출력
export const cateListHandle = (cate, articleAllData, setCateInLists, navigate, filterText) =&gt; {
  const filterCateInList = articleAllData.filter(list =&gt; list[filterText] === cate);
  setCateInLists(filterCateInList);
  const cateInListsParam = encodeURIComponent(JSON.stringify(filterCateInList));
  navigate(`/list-category?cate=${cate}&amp;cateInLists=${cateInListsParam}`);
};</code></pre>
<ul>
<li>호출했을때 실행할 함수 유틸페이지에 빼주기<ul>
<li><code>cate</code> = 검색카테고리</li>
<li><code>articleAllData</code> = 모든 article데이터</li>
<li><code>setCateInLists</code> = 필터링된리스트</li>
<li><code>filterText</code> = 필터링한 데이터키값<pre><code class="language-ts">//MainCategory.jsx(AF)
</code></pre>
</li>
</ul>
</li>
</ul>
<p>import { useState, useEffect } from &#39;react&#39;;
import axios from &#39;axios&#39;;
import { useNavigate } from &#39;react-router-dom&#39;;
import { cateListHandle } from &#39;../utils/utils&#39;;</p>
<p>const category = [&#39;모각코&#39;, &#39;뷰맛집&#39;, &#39;24시&#39;, &#39;디저트맛집&#39;, &#39;애견동반&#39;, &#39;한옥&#39;, &#39;분좋카&#39;];</p>
<p>const MainCategory = () =&gt; {
  const [articleAllData, setArticleAllData] = useState([]); //article 전체데이터 상태저장
  const setCateInLists = []; //필터링된 리스트 저장
  const filterText = &#39;category&#39;; //필터링된 리스트 저장
  const navigate = useNavigate();</p>
<p>  //article 데이터 전체 가져오기
  useEffect(() =&gt; {
    const getArticle = async () =&gt; {
      const { data: articleData } = await axios.get(&#39;<a href="http://localhost:888/article&#39;">http://localhost:888/article&#39;</a>);
      setArticleAllData(articleData);
    };
    getArticle();
  }, []);</p>
<p>  const onClickfilter = cate =&gt; {
    cateListHandle(cate, articleAllData, setCateInLists, navigate, filterText);
  };</p>
<p>  return (
    <div className="p-[20px] flex flex-col gap-[20px] max-w-[1500px] mx-auto">
      <h2>카테고리</h2>
      <ul className="flex gap-[10px] w-[100%] h-[300px]">
        {category.map((cate, index) =&gt; {
          return (
            &lt;li key={index} className=&quot;w-[20%] cursor-pointer&quot; onClick={() =&gt; onClickfilter(cate)}&gt;
              <span className="flex w-[200px] h-[200px] rounded-[50%] bg-slate-400 items-center justify-center">
                {cate}
              </span>
            </li>
          );
        })}
      </ul>
    </div>
  );
};</p>
<pre><code>- ```import { cateListHandle } from &#39;../utils/utils&#39;;```
-&gt; util함수 임폴트하기
- li 클릭했을때 클릭함수에 ```cateListHandle``` 실행함수 호출하기
    - **인자로 클릭한 li의 텍스트, article전체데이터, 필터링상태관리, 필터링할 article의 키값 전달하기**


---

### 2. 무한루프

🔎 문제점  
메뉴가 열린상태로 페이지 이동시 계속 펼쳐져 있는 문제
상태변경과 페이지 이동이 같이 실행될 때 무한루프 문제

✅ 해결 
Zustand 스토어에 메뉴별 active상태를 초기화하는 함수를 만들어 
스토어를 임폴트하여 메뉴 초기화 함수를 불러온 후 필요한 이벤트 함수들에 같이 불러온다. 

```ts
 // bearStore.js (Zustand)
 const initialState = {
  userInfo: null,
  isMenuOpen: false,
  activeTab: 0,
};
const useUserStore = create(
    persist(
    (set, get) =&gt; ({
      ...initialState,
      ...
            closeMenu: () =&gt; set({ isMenuOpen: false }),
            setActiveTab: index =&gt; set({ activeTab: index }),
            removeTab: () =&gt; set({ activeTab: false }),
        }),
    ...
    )</code></pre><ul>
<li>메뉴가 있는 컴포넌트에 
const { closeMenu, toggleMenu, isMenuOpen} = useUserStore(); 로 메뉴초기화 함수를 불러온 후 메뉴가 닫혀야할 이동 및 클릭이벤트에 함수를 불러온다.</li>
</ul>
<hr>
<h3 id="정리옮기기">정리옮기기</h3>
<p><a href="https://www.notion.so/2b2bfa16fff841edaf46f53281c0d226?v=510cdfa0f86b479eb7f2e2f6a329bafe&amp;p=1032d534ecca80dbae40fd92ba2a2e7b&amp;pm=s">노션정리 보기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js]낙관적 업데이트]]></title>
            <link>https://velog.io/@lim-bora/Next-js%EB%82%99%EA%B4%80%EC%A0%81-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@lim-bora/Next-js%EB%82%99%EA%B4%80%EC%A0%81-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Wed, 20 Nov 2024 03:42:13 GMT</pubDate>
            <description><![CDATA[<h4 id="기존_앨범추가-코드">기존_앨범추가 코드</h4>
<pre><code class="language-ts">// 앨범 추가
export const usePostAlbumMutation = () =&gt; {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: postAlbum,
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: QUERY_KEY.ALBUM });
    },
    onError: (error) =&gt; {
      console.error(&#39;MutationError:&#39;, error);
    }
  });
};</code></pre>
<h4 id="낙관적업데이트로-변경">낙관적업데이트로 변경</h4>
<pre><code class="language-ts">// 앨범 추가 : 낙관적 업데이트
export const usePostAlbumMutation = () =&gt; {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: postAlbum,
    onMutate: async (newData) =&gt; {
      await queryClient.cancelQueries({ queryKey: QUERY_KEY.ALBUM });
      // 활성쿼리 취소
      const previousAlbums = queryClient.getQueryData(QUERY_KEY.ALBUM);
      //이전 앨범정보 가져오기(에러발생시 롤백용)
      queryClient.setQueryData(QUERY_KEY.ALBUM, (old: any) =&gt; [...old, newData]);
      //UI를 먼저 업데이트, 실패시 데이터복원(첫번째인자:업뎃할 쿼리키, 두번째인자:업데이트함수로 old현재+new추가할꺼)
      return { previousAlbums };
    },
    onError: (error, newAlbum, context) =&gt; {
      queryClient.setQueryData(QUERY_KEY.ALBUM, context?.previousAlbums);
      // 에러발생시 데이터복원
      console.error(&#39;MutationError:&#39;, error);
    },
    onSuccess: () =&gt; {
      console.log(&#39;앨범이 성공적으로 추가되었습니다.&#39;);
    },
    onSettled: () =&gt; {
      //성공실패 상관없이 호출
      queryClient.invalidateQueries({ queryKey: QUERY_KEY.ALBUM });
    }
  });
};</code></pre>
<hr>
<h4 id="기존_앨범삭제-코드">기존_앨범삭제 코드</h4>
<pre><code class="language-ts">// 앨범 삭제
export const useDeleteAlbumMutation = () =&gt; {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: deleteAlbum,
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: QUERY_KEY.ALBUM });
    },
    onError: (error) =&gt; {
      console.error(&#39;삭제 중 오류 발생:&#39;, error);
    }
  });
};</code></pre>
<h4 id="낙관적업데이트로-변경-1">낙관적업데이트로 변경</h4>
<pre><code class="language-ts">// 앨범 삭제 : 낙관적 업데이트
export const useDeleteAlbumMutation = () =&gt; {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: deleteAlbum,
    onMutate: async (albumId) =&gt; {
      await queryClient.cancelQueries({ queryKey: QUERY_KEY.ALBUM });
      // 활성쿼리 취소
      const previousAlbums = queryClient.getQueryData(QUERY_KEY.ALBUM);
      //이전 앨범정보 가져오기(에러발생시 롤백용)
      queryClient.setQueryData(QUERY_KEY.ALBUM, (old: any) =&gt; old.filter((photo: any) =&gt; photo.id !== albumId));
      //UI를 먼저 업데이트, 실패시 데이터복원(첫번째인자:업뎃할 쿼리키, 두번째인자:이전데이터 아이디기준 필터링)
      return { previousAlbums };
    },
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: QUERY_KEY.ALBUM });
    },
    onError: (error, albumId, context) =&gt; {
      queryClient.setQueryData(QUERY_KEY.ALBUM, context?.previousAlbums);
      console.error(&#39;삭제 중 오류 발생:&#39;, error);
    },
    onSettled: () =&gt; {
      //성공실패 상관없이 호출
      queryClient.invalidateQueries({ queryKey: QUERY_KEY.ALBUM });
    }
  });
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] browser-image-compression를 사용한 이미지 성능최적화_(이미지 압축 라이브러리,AVIF 파일변환)]]></title>
            <link>https://velog.io/@lim-bora/Next-js-browser-image-compression%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%95%95%EC%B6%95-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</link>
            <guid>https://velog.io/@lim-bora/Next-js-browser-image-compression%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%84%B1%EB%8A%A5%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%95%95%EC%B6%95-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</guid>
            <pubDate>Tue, 19 Nov 2024 12:57:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>갤러리에서 처음 사용자가 이미지를 올릴 때 용량제한을 두지않아 이미지가 용량이 크고 양이 많아질수록 페이지 로드되는 속도가 현저히 낮아지는 현상발생</p>
</blockquote>
<h4 id="이전코드-문제점">이전코드 문제점</h4>
<ol>
<li><p>압축 또는 이미지 최적화 안함</p>
</li>
<li><p>반복적인 상태 업데이트 
<code>setImgSrc((prev) =&gt; [...prev, e.target!.result as string]);</code></p>
<pre><code class="language-ts">// 이미지 파일받기
const OnChangePhoto = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
 if (onClickUserCheck(e)) return;

 const files = e.target.files;
 setCurrentRegion(e.target.id.split(&#39;-&#39;)[1]);
 if (!files) return;

 Array.from(files).forEach((file) =&gt; {
   const fileReader = new FileReader();
   fileReader.readAsDataURL(file);

   fileReader.onload = (e) =&gt; {
     if (typeof e.target?.result === &#39;string&#39; &amp;&amp; e.target.result) {
       if (activeTab === &#39;allTab&#39;) {
         setImgSrc((prev) =&gt; [...prev, e.target!.result as string]);
         // setIsRigionModal(true);
         openModal();
       } else if (activeTab === &#39;rigionTab&#39;) {
         setImgSrc((prev) =&gt; [...prev, e.target!.result as string]);
         setRegionCate(item);
       }
     }
   };
 });
};</code></pre>
</li>
</ol>
<hr>
<h4 id="수정코드">수정코드</h4>
<ol>
<li>파일 읽기와 압축을 병렬로 처리해 전체 시간을 단축한다.</li>
<li>상태 업데이트 최소화하여 불필요한 렌더링을 방지한다.</li>
</ol>
<pre><code class="language-ts">  // 이미지 파일받기
  const OnChangePhoto = async (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    if (onClickUserCheck(e)) return;

    const files = e.target.files;
    setCurrentRegion(e.target.id.split(&#39;-&#39;)[1]);
    if (!files) return;

    const imgSrcArray: string[] = await Promise.all(
      Array.from(files).map(async (file) =&gt; {
        // 파일 압축
        const compressedImage = await imageCompression(file, {
          maxSizeMB: 1, // 1MB
          maxWidthOrHeight: 1024, // 이미지의 최대 가로 또는 세로 길이를 1024로 제한
          useWebWorker: true // 압축 작업이 메인 스레드에 영향을 미치지 않도록 설정
        });

        // 압축파일 AVIF 형식으로 변환
        const avifImage = await convertImageToAvif(compressedImage);

        // 압축된 파일 읽기
        return new Promise&lt;string&gt;((resolve) =&gt; {
          const reader = new FileReader();
          reader.readAsDataURL(avifImage);
          reader.onload = () =&gt; {
            if (typeof reader.result === &#39;string&#39;) {
              resolve(reader.result);
            }
          };
        });
      })
    );

    // 상태 업데이트를 한 번에 처리
    setImgSrc((prev) =&gt; [...prev, ...imgSrcArray]);

    if (activeTab === &#39;allTab&#39;) {
      openModal();
    } else if (activeTab === &#39;rigionTab&#39;) {
      setRegionCate(item);
    }
  };</code></pre>
<ul>
<li><p><code>Promise.all</code> 사용해 파일 압축 &amp; 읽기 -&gt; 병렬로 처리</p>
</li>
<li><blockquote>
<p>병렬로 처리하면 하나끝날때까지 안기다리고 여러개를 동시에 처리가 가능해 시간단축이 가능하고, 코드가 좀 더 깔끔해진다.</p>
</blockquote>
</li>
<li><p>반복됬던 상태 업데이트를 한 번에 처리하여 불필요한 렌더링을 방지
<code>setImgSrc((prev) =&gt; [...prev, ...imgSrcArray]);</code></p>
</li>
<li><p>전달받은 압축된이미지를 한 번 더 <code>AVIF 형식으로 변환</code></p>
</li>
</ul>
<h4 id="avif-형식으로-변환하는-함수">AVIF 형식으로 변환하는 함수</h4>
<pre><code class="language-ts">// AVIF 형식으로 변환하는 함수
export const convertImageToAvif = (file: File): Promise&lt;File&gt; =&gt; {
  return new Promise((resolve) =&gt; {
    const img = new Image();
    img.src = URL.createObjectURL(file);
    img.onload = () =&gt; {
      const canvas = document.createElement(&#39;canvas&#39;);
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext(&#39;2d&#39;);
      ctx?.drawImage(img, 0, 0);
      canvas.toBlob(
        (blob) =&gt; {
          if (blob) {
            const webpFile = new File([blob], `${file.name.split(&#39;.&#39;)[0]}.webp`, { type: &#39;image/webp&#39; });
            resolve(webpFile);
          } else {
            console.warn(&#39;WebP 변환 실패, 원본 파일 사용&#39;);
            resolve(file);
          }
        },
        &#39;image/webp&#39;,
        0.8
      );
    };
    img.onerror = () =&gt; {
      console.warn(&#39;이미지 로드 실패, 원본 파일 사용&#39;);
      resolve(file);
    };
  });
};
</code></pre>
<hr>
<h4 id="결과">결과</h4>
<p>실제로 이미지용량이 줄었다.
성능이 좀 올라갔다.
화면에 앨범제외하고도 pc에서만 나타나는 푸터로 큰레이아웃변경이 원인일 수 있다.
(BF)
<img src="https://velog.velcdn.com/images/lim-bora/post/fcc8ee59-508c-464f-828b-9a287aec9292/image.png" alt="BF">
<img src="https://velog.velcdn.com/images/lim-bora/post/146d1a44-1633-4249-9f9b-3ad06856f057/image.webp" alt=""></p>
<p>(AF)
<img src="https://velog.velcdn.com/images/lim-bora/post/ddf0f760-d09b-4dcb-84ea-be237d1c7a02/image.png" alt="AF">
<img src="https://velog.velcdn.com/images/lim-bora/post/f025bc16-5da6-444e-a0d0-141b93243c9d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] Link vs Router]]></title>
            <link>https://velog.io/@lim-bora/Next-js-Link-vs-Router</link>
            <guid>https://velog.io/@lim-bora/Next-js-Link-vs-Router</guid>
            <pubDate>Tue, 19 Nov 2024 07:02:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>스탬프를 추가하는 작업 후 바로 스탬프목록으로 이동했을 때 추가한 스탬프까지 바로 반영된 화면이 보여야하는데 반영안된상태로 새로고침이 되야 반영이되는 문제가 있다. </p>
</blockquote>
<h4 id="이전코드">이전코드</h4>
<pre><code class="language-ts">//스탬프 추가 후 목록으로 넘어가는 버튼
&lt;Link href={&#39;/stamp-all&#39;}&gt;
      &lt;p className=&quot;w-full rounded-[12px] bg-secondary-500 py-[21px] text-center font-semiBold text-[#004157] hover:bg-[#BDEFFF]&quot;&gt;
         알겠어요!
      &lt;/p&gt;
&lt;/Link&gt;</code></pre>
<p>이 전 코드는 단순 링크이동을 위해 <code>Link</code>를 사용하여 이동되도록 했다.</p>
<h4 id="수정코드">수정코드</h4>
<pre><code class="language-ts">&lt;button onClick={() =&gt; {
            router.push(&#39;/stamp-all&#39;);
            router.refresh();
          }}&gt;
          &lt;p className=&quot;w-full rounded-[12px] bg-secondary-500 py-[21px] text-center font-semiBold text-[#004157] hover:bg-[#BDEFFF]&quot;&gt;
            알겠어요!
          &lt;/p&gt;
        &lt;/button&gt;</code></pre>
<p>수정된 코드는 <code>Link</code> -&gt; <code>button</code>로 변경 후<br><code>router.push(&#39;/stamp-all&#39;)</code> 로 페이지이동하게 하고 
<code>router.refresh()</code>; 로 데이터를 갱신하면서 강제로드되도록 하였다.</p>
<blockquote>
<p><strong>router.refresh()</strong>
: 현재 페이지를 클라이언트에서 새로고침 없이 강제로 다시 로드하는 기능</p>
</blockquote>
<h4 id="언제-사용할까-">언제 사용할까 ?</h4>
<ul>
<li>데이터 갱신 : 데이터를 수정, 삭제, 추가한 후 서버에서 최신 상태를 가져와야 할 때</li>
<li>서버 상태 동기화 : 클라이언트와 서버 간 데이터 불일치를 해결할 때</li>
</ul>
<p>-&gt; 난 두 경우 모두 해당되기때문에 refresh()를 사용하여 해결하였다.</p>
<hr>
<h2 id="link-vs-router">Link vs Router</h2>
<ul>
<li>두 개 모두 Next.js에서 페이지 간 이동을 처리할 때 사용한다.
그럼 차이점은?</li>
</ul>
<blockquote>
<h3 id="link">Link</h3>
<p>사용자가 클릭할 수 있는 <strong>정적인 내비게이션</strong> 링크를 만들 때 사용한다.</p>
</blockquote>
<h4 id="언제-사용">언제 사용?</h4>
<ul>
<li>페이지 간 이동이 주로 클릭으로 이루어지는 경우</li>
<li>사용자 경험 개선 
Link는 Next.js의 Prefetching 기능을 활용하여 해당 경로 데이터를 미리 가져옵니다. 이를 통해 이동 속도가 빨라집니다.</li>
</ul>
<blockquote>
<h3 id="router">Router</h3>
<p>Next.js의 훅으로, 프로그래밍 방식으로 경로 이동을 처리할 때 사용한다.</p>
</blockquote>
<h4 id="언제-사용-1">언제 사용?</h4>
<ul>
<li><p>특정 이벤트가 발생했을 때 코드에서 경로를 변경해야 할 경우.
(폼 제출 후 이동, 데이터 업데이트 후 이동 등)</p>
</li>
<li><p>조건부 라우팅, URL 파라미터 동적 설정 등 코드에서 더 많은 제어가 필요한 경우.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] SSR로 초기로딩속도 높이기 (성능개선)]]></title>
            <link>https://velog.io/@lim-bora/Next-js-SSR%EB%A1%9C-%EC%B4%88%EA%B8%B0%EB%A1%9C%EB%94%A9%EC%86%8D%EB%8F%84-%EB%86%92%EC%9D%B4%EA%B8%B0-%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@lim-bora/Next-js-SSR%EB%A1%9C-%EC%B4%88%EA%B8%B0%EB%A1%9C%EB%94%A9%EC%86%8D%EB%8F%84-%EB%86%92%EC%9D%B4%EA%B8%B0-%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Tue, 19 Nov 2024 06:42:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기능을 완성시키고 테스트하는중애 화면에 그려지는 속도가 너무 느려서 작동안되나..?싶을정도로 반영이 느린점을 개선해보려한다.</p>
</blockquote>
<h3 id="🖍️-csr-vs-ssr">🖍️ CSR vs. SSR</h3>
<p><strong>CSR</strong>의 경우,
서버로부터 빈 HTML과 JavaScript 파일을 넘겨 받은 다음에 Query가 실행된다.
<code>Markup &gt; JS &gt; Query</code></p>
<p><strong>SSR</strong>의 경우.
서버에서 내용을 채워서 HTML을 보내줘야 하므로, Query는 Markup을 전달하기 전에 실행된다.
<code>Query &gt; JS &gt; Markup</code></p>
<h4 id="기존코드-csr방식">기존코드 (CSR방식)</h4>
<pre><code class="language-ts">import browserClient from &#39;@/utils/supabase/client&#39;;

//앨범전체데이터 가져오기
export const getAlbumList = async (userId: string) =&gt; {
  const { data, error } = await browserClient.from(&#39;album&#39;).select(&#39;*&#39;).eq(&#39;user_id&#39;, userId);

  if (error) {
    console.error(&#39;포토앨범 리스트 가져오기 오류 :&#39;, error.message);
    throw new Error(&#39;포토앨범 리스트 데이터를 가져오는 중 오류가 발생했습니다.&#39; + error.message);
  }
  return data;
};
</code></pre>
<h4 id="수정코드ssr방식">수정코드(SSR방식)</h4>
<pre><code class="language-ts">//serverAction
&#39;use server&#39;;

import { createClient } from &#39;@/utils/supabase/server&#39;;
//앨범전체데이터 가져오기
export const getAlbumList = async (userId: string) =&gt; {
  const serverClient = createClient();

  const { data, error } = await serverClient.from(&#39;album&#39;).select(&#39;*&#39;).eq(&#39;user_id&#39;, userId);

  if (error) {
    console.error(&#39;포토앨범 리스트 가져오기 오류 :&#39;, error.message);
    throw new Error(&#39;포토앨범 리스트 데이터를 가져오는 중 오류가 발생했습니다.&#39; + error.message);
  }
  return data;
};
</code></pre>
<p>앨범데이터를 서버에서 가져옴 -&gt; 캐싱, 상태관리 추가 </p>
<ul>
<li>staleTime(신선시간)<pre><code class="language-ts">//useGetAlbumListQuery.tsx
export const useGetAlbumListQuery = (userId: string) =&gt; {
return useQuery({
  queryKey: QUERY_KEY.ALBUM,
  queryFn: async () =&gt; {
    if (userId) {
      return await getAlbumList(userId);
    } else {
      return null;
    }
  },
  enabled: !!userId,
  staleTime: 60 * 1000 //데이터가 신선하게 유지되는 시간 (1분)
});
};
</code></pre>
</li>
</ul>
<pre><code>
- HydrationBoundary
- dehydrate
```ts
//page.tsx

const Album = async () =&gt; {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1000
      }
    }
  });

  const user = await getUser(); //유저아이디가져오는 훅으로 불러오기

  //프리패치
  if (user) {
    await queryClient.prefetchQuery({
      queryKey: QUERY_KEY.ALBUM,
      queryFn: () =&gt; getAlbumList(user.id)
    });
  }

  return (
    &lt;HydrationBoundary state={dehydrate(queryClient)}&gt;
      &lt;div className=&quot;lg:pt-[64px]&quot;&gt;
        &lt;AlbumList /&gt;
      &lt;/div&gt;
    &lt;/HydrationBoundary&gt;
  );
};</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] useState의 비동기  ]]></title>
            <link>https://velog.io/@lim-bora/Next-js-useState%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-State-Batch-Update-%EB%B0%B0%EC%B9%98%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@lim-bora/Next-js-useState%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-State-Batch-Update-%EB%B0%B0%EC%B9%98%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Sat, 16 Nov 2024 07:47:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>추천관광지에서 로그인한 유저의 북마크가 있으면 체크된상태로 되야하는데 
북마크체크가 되어있지않은 상태로 보여진다. </p>
</blockquote>
<pre><code class="language-ts">const TourismPage = () =&gt; {
  const userId = useUserId(); //유저아이디 가져오는 훅
  const { data: tourismList, isLoading } = useGetTourismListQuery(userId); //쿼리로 받아온 투어리스트 데이터

// 도시별 데이터 그룹화
  const groupedPlaces = tourismList ? groupTourismByCity(tourismList) : [];

return(
...
  {groupedPlaces.length !== 0 ? (
          Object.entries(groupedPlaces).map(([city, tourismList]) =&gt; (
            &lt;section key={city}&gt;
              &lt;div className=&quot;pc-inner-width mb-[6px] flex items-center justify-between&quot;&gt;
                &lt;h2 className=&quot;font-semiBold text-[24px] text-[#1D1D1D]&quot;&gt;
                  {tourismList[0]?.citytitle || &#39;도시 정보 없음&#39;}
                &lt;/h2&gt;
              &lt;/div&gt;
              &lt;p className=&quot;pc-inner-width mb-[14px] text-sm font-normal text-[#696969]&quot;&gt;{city}&lt;/p&gt;

              {/* TouristSwiper로 도시별 여행지 목록을 스와이프 가능하게 표시 */}
              &lt;TourismSwiper tourismList={tourismList} userId={userId} /&gt;
            &lt;/section&gt;
          ))
        ) : (
          &lt;p className=&quot;text-sm text-alert lg:mt-7 lg:flex lg:items-center lg:justify-center&quot;&gt;텅&lt;/p&gt;
        )}
)</code></pre>
<p>우선 값이 제대로 들어오는지 체크</p>
<ul>
<li>투어리스트 데이터 잘가져와짐 </li>
<li>유저아이디 훅안에서 아이디값 잘 가져와짐</li>
<li>유저아이디값 처음엔 null이었다가 가져와짐..!</li>
</ul>
<p>Loading, Pendding상태에 따른 처리도 해주었지만 해결안됨</p>
<h3 id="원인">원인)</h3>
<p>로그인 유저데이터를 가져오는 속도보다 투어리스트가 UI에 업데이트되는 속도가 더 빨라서 투어리스트가 업데이트된 후 로그인유저를 받아와 북마크 체크가 안되는것</p>
<blockquote>
<h4 id="usestate는-비동기로-작동한다">useState는 비동기로 작동한다.</h4>
</blockquote>
<h3 id="setstate의-비동기-문제-해결-방법">setState의 비동기 문제 해결 방법)</h3>
<p>우선 로그인한상태에서의 투어리스트를 담을 state를 새로 만들고
useEffect의 의존성 배열에 유저아이디와, 기존 쿼리로 담아왔던 투어리스트를 추가하면 두 개가 업데이트 된 후 새로 만들어준 state에 값을 담아준다.</p>
<pre><code class="language-ts">const [tourismListData, setTourismListData] = useState([]); //로그인했을때 북마크된 투어리스트상태들이 담겨

  useEffect(() =&gt; {
    if (userId &amp;&amp; tourismList) {
      //유저아이디와 투어리스트가 있으면 tourismListData 상태에 저장
      setTourismListData(tourismList);
    }
  }, [userId, tourismList]); </code></pre>
<p>이 후 분기처리로 로그인했을때 안했을때 삼항연산자를 사용해 화면에 보여준다.
<code>tourismListData(로그인O,북마크O) / tourismList(로그인X,북마크X)</code></p>
<pre><code class="language-ts">// 도시별 데이터 그룹화 : 유저아이디 유무에 따라 할당값 다르게
const groupedPlaces = userId ? groupTourismByCity(tourismListData) : groupTourismByCity(tourismList || []);

{Object.keys(groupedPlaces).length !== 0 ? ( //groupedPlaces객체라 length사용불가, -&gt; 객체의 키 개수를 확인하는걸로 대체
          Object.entries(groupedPlaces).map(([city, tourismList]) =&gt; (
            &lt;section key={city}&gt;
              &lt;div className=&quot;pc-inner-width mb-[6px] flex items-center justify-between&quot;&gt;
                &lt;h2 className=&quot;font-semiBold text-[24px] text-[#1D1D1D]&quot;&gt;
                  {tourismList[0]?.citytitle || &#39;도시 정보 없음&#39;}
                &lt;/h2&gt;
              &lt;/div&gt;
              &lt;p className=&quot;pc-inner-width mb-[14px] text-sm font-normal text-[#696969]&quot;&gt;{city}&lt;/p&gt;

              {/* TouristSwiper로 도시별 여행지 목록을 스와이프 가능하게 표시 */}
              &lt;TourismSwiper tourismList={tourismList} userId={userId} /&gt;
            &lt;/section&gt;
          ))
        ) : (
          &lt;p className=&quot;text-sm text-alert lg:mt-7 lg:flex lg:items-center lg:justify-center&quot;&gt;텅&lt;/p&gt;
        )}</code></pre>
<hr>
<blockquote>
<h4 id="state-batch-update">State Batch Update</h4>
<p>리액트는 컴포넌트의 렌더링 횟수를 최소화하기 위해서 State Batch Update를 사용한다. 이는 리액트의 이벤트 핸들러를 사용하여 여러 번의 setState를 호출할 때 이를 하나의 업데이트로 일괄 처리하여 리렌더링이 한 번만 발생하도록 하는 것이다. </p>
</blockquote>
<p>예시)</p>
<pre><code class="language-ts">const [num,setNum] = useState(0); 
const onClickCount = () =&gt; {
  setNum(num+1)
  setNum(num+1)
}
...
&lt;button onClick={onClickCount}&gt;&lt;/button&gt;
-&gt; 한번 클릭했을때 일괄처리하기때문에 결과는 1만 증가한다. </code></pre>
<h4 id="참고자료">참고자료)</h4>
<p><a href="https://sunho-doing.tistory.com/entry/Reactjs-useState%EC%9D%98-%ED%8A%B9%EC%A7%95State-Batch-Update-%EB%B9%84%EB%8F%99%EA%B8%B0">관련블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 위치권한 상태 확인]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%EC%9C%84%EC%B9%98%EA%B6%8C%ED%95%9C-%EC%83%81%ED%83%9C-%ED%99%95%EC%9D%B8</link>
            <guid>https://velog.io/@lim-bora/Next-js-%EC%9C%84%EC%B9%98%EA%B6%8C%ED%95%9C-%EC%83%81%ED%83%9C-%ED%99%95%EC%9D%B8</guid>
            <pubDate>Wed, 13 Nov 2024 12:38:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>사용자의 위치권환상태에 따라 서비스를 제공하는데, 위치권한을 허용하지않았을경우 상태와 메세지를 추가하였다.</p>
</blockquote>
<h4 id="bf">BF</h4>
<p>이 코드는 요청은 원활하지만 단순히 요청허용에 한정으로만 실행된다.
요청거부시 계속로딩되어 사용자에게 불편함을 줄 수 있다.</p>
<pre><code class="language-ts">  // Geolocation API 로 유저의 위도,경도값 추출
  useEffect(() =&gt; {
    if (&#39;geolocation&#39; in navigator) {
      //현 브라우저가 Geolocation API를 지원하는지 확인
      navigator.geolocation.getCurrentPosition(
        //사용자의 현재 위치를 요청
        async (position) =&gt; {
          const { latitude, longitude } = position.coords;
          setLocation({ lat: latitude, lng: longitude });
          await getAddress(latitude, longitude);
        },
        (err) =&gt; {
          showErrorMsg(err.message, setError);
        },
        {
          enableHighAccuracy: true, // 정확도 우선 모드
          timeout: 60000, // 1분 이내에 응답 없으면 에러 발생
          maximumAge: 0 // 항상 최신 위치 정보 수집
        }
      );
    } else {
      console.log(&#39;오류가 발생했습니다.&#39;);
    }
  }, []);
</code></pre>
<h4 id="af">AF</h4>
<p>사용자가 거부했을시 거부했기때문에 서비스를 이용할 수 없다는 메세지를 전달하여 허용해야하는 이유를 설명해주었다.</p>
<pre><code class="language-ts">  useEffect(() =&gt; {
    const checkGeolocation = async () =&gt; {
      if (&#39;permissions&#39; in navigator) {
        //현 브라우저가 Geolocation API를 지원하는지 확인
        try {
          const result = await navigator.permissions.query({ name: &#39;geolocation&#39; }); //사용자의 위치정보에 대한 권한을 요청
          if (result.state === &#39;granted&#39; || result.state === &#39;prompt&#39;) {
            //허용했을때, 처음위치권한요청시
            navigator.geolocation.getCurrentPosition(
              //사용자의 현재 위치를 요청
              async (position) =&gt; {
                const { latitude, longitude } = position.coords;
                setLocation({ lat: latitude, lng: longitude });
                await getAddress(latitude, longitude);
              },
              (err) =&gt; {
                showErrorMsg(err.message, setError);
                alert(&#39;위치 권한이 거부되었습니다. 허용하지 않으면 스탬프를 찍을 수 없습니다.&#39;);
              },
              {
                enableHighAccuracy: true, // 정확도 우선 모드
                timeout: 60000, // 1분 이내에 응답 없으면 에러 발생
                maximumAge: 0 // 항상 최신 위치 정보 수집
              }
            );
          } else if (result.state === &#39;denied&#39;) {
            // 위치 권한이 거부된 경우
            alert(&#39;위치 권한이 거부되었습니다. 허용하지 않으면 스탬프를 찍을 수 없습니다.&#39;);
            showErrorMsg(&#39;위치 권한이 거부되었습니다.&#39;, setError);
          }
        } catch (error) {
          console.log(&#39;권한을 확인하는 중 오류가 발생했습니다.&#39;, error);
        }
      } else {
        console.log(&#39;Geolocation API를 지원하지 않는 브라우저입니다.&#39;);
      }
    };
    checkGeolocation();
  }, []);</code></pre>
<ul>
<li><p><code>permissions</code> : </p>
</li>
<li><p><code>navigator.permissions.query</code> : 위치 권한 상태를 확인</p>
</li>
<li><p><code>granted</code> : 허용</p>
</li>
<li><p><code>prompt</code> : 권한요청중</p>
</li>
<li><p><code>denied</code> : 거부</p>
</li>
</ul>
<ol>
<li>try catch문으로 1차적으로 오류상태와 정상상태를 걸러준다.</li>
<li>try중 허용/요청중/거부 상태에 따른 조건문을 추가하여 상태에 따라 코드를 분리한다.
```ts
result.state === &#39;granted&#39; || result.state === &#39;prompt&#39;</li>
</ol>
<p>-&gt; 허용 || 권한요청중 =&gt; 위치 정보가져오는 기존코드 유지</p>
<pre><code>
```ts
result.state === &#39;denied&#39;
-&gt; 거부 =&gt; 관련알람메세지, 콘솔에러 출력</code></pre><p>참고자료
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API/Using_the_Permissions_API">Using the Permissions API- MDN</a>
<a href="https://passionfruit6.tistory.com/283">geolocation 위치 권한 요청</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 인풋과 라벨 연결값 고정문제]]></title>
            <link>https://velog.io/@lim-bora/%EC%9D%B8%ED%92%8B%EA%B3%BC-%EB%9D%BC%EB%B2%A8-%EC%97%B0%EA%B2%B0%EA%B0%92-%EA%B3%A0%EC%A0%95%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@lim-bora/%EC%9D%B8%ED%92%8B%EA%B3%BC-%EB%9D%BC%EB%B2%A8-%EC%97%B0%EA%B2%B0%EA%B0%92-%EA%B3%A0%EC%A0%95%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 11 Nov 2024 00:50:48 GMT</pubDate>
            <description><![CDATA[<p>  //인풋과 라벨 연결값이 텍스트로 넣어놓음 -&gt; 고정값으로 지칭하는 지역이름이 같아 계속 처음 지역에만 이미지가 넣어짐
  //인풋과 라벨 연결값은 유니크한값으로 지역이름의 변수로 변경 -&gt; 각 지역별로 이미지는 들어가지만 전체보기에서 파일자체가 열리지않음
  //-&gt;기존 고정변수명+유니크한값으로 변수명을 수정함</p>
<pre><code>  &lt;input
        id={`fileInput-${item}`}
        className=&quot;hidden&quot;
        type=&quot;file&quot;
        accept=&quot;image/*&quot;
        multiple
        onChange={OnChangePhoto}
      /&gt;
      &lt;label
        htmlFor={`fileInput-${item}`}
        className=&quot;flex aspect-square cursor-pointer items-center justify-center bg-[#004157] text-[50px] text-white hover:bg-[#1b4755]&quot;
      &gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 년-월-일 날짜변환]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%EB%85%84-%EC%9B%94-%EC%9D%BC-%EB%82%A0%EC%A7%9C%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@lim-bora/Next-js-%EB%85%84-%EC%9B%94-%EC%9D%BC-%EB%82%A0%EC%A7%9C%EB%B3%80%ED%99%98</guid>
            <pubDate>Mon, 04 Nov 2024 00:59:37 GMT</pubDate>
            <description><![CDATA[<h4 id="bf-2024-7-11">(BF) 2024. 7. 11.</h4>
<pre><code class="language-javascript">new Date(list.created_at).toLocaleDateString()</code></pre>
<h4 id="af-2024년-7월-11일">(AF) 2024년 7월 11일</h4>
<pre><code class="language-javascript">new Date(list.created_at).toLocaleDateString(&#39;ko-KR&#39;, {
                    year: &#39;numeric&#39;,
                    month: &#39;long&#39;,
                    day: &#39;numeric&#39;
                  })</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] Dropdown 커스텀 훅]]></title>
            <link>https://velog.io/@lim-bora/Next-js-Dropdown-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85</link>
            <guid>https://velog.io/@lim-bora/Next-js-Dropdown-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85</guid>
            <pubDate>Mon, 04 Nov 2024 00:57:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>리스트가 길 경우를 대비해 드롭다운 기능을 넣고 커스텀훅으로 만들어서 재사용가능하도록 했다.
스르륵 애니메이션도 추가하겠다.</p>
</blockquote>
<h4 id="드롭다운-커스텀훅-만들기">드롭다운 커스텀훅 만들기</h4>
<pre><code class="language-javascript">//useDropdoun.ts
import { useState, useRef } from &#39;react&#39;;

const useDropdoun = () =&gt; {
  const [isOpen, setIsOpen] = useState(false);//디폴트-닫힘
  const dropdownRef = useRef(null); //기준점

  const toggleDropdown = () =&gt; { //토글할때마다 현재상태의 반대로
    setIsOpen((prev) =&gt; !prev);
  };

  return { isOpen, setIsOpen, toggleDropdown, dropdownRef };
};

export default useDropdoun;
</code></pre>
<h4 id="드롭다운-사용할-컴포넌트에서-임폴트하기">드롭다운 사용할 컴포넌트에서 임폴트하기</h4>
<pre><code class="language-javascript">//사용할 컴포넌트
import useDropdoun from &#39;@/hooks/useDropdoun&#39;;

const StampItemDetail = () =&gt; {
  const { isOpen, toggleDropdown, dropdownRef } = useDropdoun(); //리턴한 state,이벤트 등 가져오기

   return (
        &lt;li ref={dropdownRef}&gt; //ref 기준점 연결해주기
        &lt;div onClick={toggleDropdown}&gt; //클릭시 토글이벤트
          &lt;h2&gt;히스토리&lt;/h2&gt;
          &lt;button&gt;
            &lt;Icon name=&quot;ArrowIcon&quot; size={28} /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        {isOpen &amp;&amp; ( //isOpen = true = 열렸을때만 아래코드적용
            리스트들...
        )}
    ...
   )

};

export default StampItemDetail;</code></pre>
<h4 id="애니메이션-적용하기">애니메이션 적용하기</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 커스텀훅]]></title>
            <link>https://velog.io/@lim-bora/%EC%BB%A4%EC%8A%A4%ED%85%80%ED%9B%85</link>
            <guid>https://velog.io/@lim-bora/%EC%BB%A4%EC%8A%A4%ED%85%80%ED%9B%85</guid>
            <pubDate>Fri, 01 Nov 2024 05:42:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기존 컴포넌트에서 state로 이벤트를 다 만들었는데 링크로 이동하는 디테일페이지에서도 이 동작이 필요해서 재사용성에 용이한 커스텀훅으로 쪼개보기로했다!</p>
</blockquote>
<h3 id="이미지-팝업열기-이벤트">이미지 팝업열기 이벤트</h3>
<h4 id="기존">기존</h4>
<pre><code class="language-javascript">// AlbumList.jsx
const [imgModal, setImgModal] = useState(false);
const [selectedImgUrl, setSelectedImgUrl] = useState(&#39;&#39;);

//이미지팝업 상태에따라 모달이벤트
const onClickImgModal = (url: string) =&gt; {
    setSelectedImgUrl(url);
    setImgModal(true);
  };

return(
...
  //이미지 클릭시 모달창열리게
  &lt;Image onClick={() =&gt; onClickImgModal(item.photoImg)}
      src={item.photoImg}
      alt=&quot;&quot;
      width={200}
      height={200}
      priority
      className=&quot;h-full w-full object-cover&quot;
  /&gt;
 ...
 //모달창
 {imgModal &amp;&amp; &lt;ImgModal setImgModal={setImgModal} selectedImgUrl={selectedImgUrl} /&gt;}
 ...
)</code></pre>
<h4 id="커스텀-훅-적용">커스텀 훅 적용</h4>
<pre><code class="language-javascript">//useImgModal.ts(커스텀훅)
import { useState } from &#39;react&#39;;

const useImgModal = () =&gt; {
    const [imgModal, setImgModal] = useState(false);
    const [selectedImgUrl, setSelectedImgUrl] = useState(&#39;&#39;);

    const onClickImgModal = (url: string) =&gt; {
        setSelectedImgUrl(url);
        setImgModal(true);
      };
  return {
   selectedImgUrl,
    imgModal,
    onClickImgModal,
    setImgModal,
  }
}

export default useImgModal;</code></pre>
<pre><code class="language-javascript">// AlbumList.jsx
import useImgModal from &#39;@/hooks/useImgModal&#39;;//임폴트

const AlbumList = () =&gt; {
    const { selectedImgUrl, imgModal, onClickImgModal, setImgModal } = useImgModal(); //사용할거 가져오기
...
//그대로 사용가능
}</code></pre>
<p>참고자료
<a href="https://onlydev.tistory.com/156">커스텀 훅</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 갤러리구현2 - 다중이미지 업로드]]></title>
            <link>https://velog.io/@lim-bora/%EA%B0%A4%EB%9F%AC%EB%A6%AC2-%EB%8B%A4%EC%A4%91%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@lim-bora/%EA%B0%A4%EB%9F%AC%EB%A6%AC2-%EB%8B%A4%EC%A4%91%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Tue, 29 Oct 2024 12:38:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 전 글에서 단일 이미지를 업로드하는건 가능했다.
이제 다중이미지를 업로드하도록 수정해야한다.
선택한 이미지 수만큼 데이터가 n개씩 추가되야한다.</p>
</blockquote>
<h2 id="파일-여러개-올리기">파일 여러개 올리기</h2>
<ul>
<li><code>multiple</code> 다중 이미지 선택<pre><code class="language-javascript">//AddPhotoBtn.tsx
const [imgSrc, setImgSrc] = useState(&quot;&quot;); 
</code></pre>
</li>
</ul>
<p>return (
    <li>
      <input id="fileInput" className="hidden" type="file" accept="image/*" multiple onChange={OnChangePhoto} />
      <label htmlFor="fileInput" >+</label>
    </li>
  );</p>
<pre><code>
#### (BF) 파일 업로드 시 액션
1. files변수에 클릭한 대상의 파일을 저장한다. 
2. 불러온값이 n개로 Array.from()로 배열화하고 반복문을 돌린다
3. 이미지데이터를 웹에서 미리보기할 수 있게 변환 후
4. Base64 인코딩된 데이터가 로드되고 타겟의 값이 문자열 + 존재할때 실행한다(유효성처리)
5. imgSrc = url담을 상태관리에 업데이트(이미지링크)
6. 파일을 선택했다면 카테고리 선택 모달창 열리게
```javascript
//AddPhotoBtn.tsx
  const OnChangePhoto = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const files = e.target.files; //클릭한대상의 이미지를 저장
    if (!files) return;//유효성체크

    Array.from(files).forEach((file) =&gt; {
      //N개를 배열로 만들어서 + 파일여러개일때 갯수만큼 순회
      const fileReader = new FileReader(); //파일읽기용 객체
      fileReader.readAsDataURL(file); //file저장
      fileReader.onload = (e) =&gt; {
        if (typeof e.target?.result === &#39;string&#39; &amp;&amp; e.target.result) {
          setImgSrc(e.target.result); 
          setIsRigionModal(true); //모달열기
        }
      };
    });
  };</code></pre><p><strong>1번째 문제)</strong>
반복문을 돌렸는데도 값이 마지막 하나만 데이터에 추가된다.
<strong>원인) **
반복하면서 값이 마지막꺼로 씌워져서 마지막파일만 업로드되는것이다.
**해결방법)</strong>
스프레드연산자로 이전값 + 다음데이터값 으로 값이 쌓이도록 변경
<code>setImgSrc((prev) =&gt; [...prev, e.target.result]);</code></p>
<h4 id="bf-모달에서-카테고리-선택후-업로드-버튼-클릭시">(BF) 모달에서 카테고리 선택후 업로드 버튼 클릭시</h4>
<pre><code class="language-javascript">//AddPhotoBtn.tsx
   const onHandleUpload = () =&gt; {
    if (imgSrc.length &gt; 0) {
        AlbumAddMutation.mutate({ imgSrc, regionCate //추가이벤트 호출
      });
      alert(&#39;앨범이 추가되었습니다.&#39;);
      setIsRigionModal(false);
    }
  };</code></pre>
<p><strong>2번째 문제)</strong>
데이터테이블에 값이 주소 문자열값만 들어가야하는데 [&quot;주소&quot;] 이렇게 들어가서 정상적인 url이 아니라는 오류가 발생한다.
<strong>원인) **
1번째 트러블슈팅에서 다수의 이미지일 경우 값을 묶기 위해 배열을 사용해 묶어서 
imgSrc 상태에 저장하였다. 배열로 묶은 통으로 값이 넘어가 정상url이 아닌것이다.
**해결방법)</strong>
다수의 이미지일 경우 값을 배열로 쌓이게 묶고, 데이터테이블로 값을 전달할때 
추가이벤트 호출하는 부분만 반복문으로 감싸 이미지url의 배열에서 N번째를 전달</p>
<pre><code>imgSrc.forEach((src) =&gt; {
     AlbumAddMutation.mutate({ imgSrc: src, regionCate });
});</code></pre><hr>
<hr>
<h2 id="해결된-코드">해결된 코드</h2>
<h4 id="af-파일-업로드-시-액션">(AF) 파일 업로드 시 액션</h4>
<pre><code class="language-javascript">//AddPhotoBtn.tsx
  const OnChangePhoto = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const files = e.target.files; //클릭한대상의 이미지를 저장
    if (!files) return;//유효성체크

    Array.from(files).forEach((file) =&gt; {
      //N개를 배열로 만들어서 + 파일여러개일때 갯수만큼 순회
      const fileReader = new FileReader(); //파일읽기용 객체
      fileReader.readAsDataURL(file); //file저장
      fileReader.onload = (e) =&gt; {
        if (typeof e.target?.result === &#39;string&#39; &amp;&amp; e.target.result) {
          setImgSrc((prev) =&gt; [...prev, e.target.result]); //상태저장 + 순회하면서 저장하니가 값 쌓이게
          setIsRigionModal(true); //모달열기
        }
      };
    });
  };</code></pre>
<h4 id="af-모달에서-카테고리-선택후-업로드-버튼-클릭시">(AF) 모달에서 카테고리 선택후 업로드 버튼 클릭시</h4>
<pre><code class="language-javascript">//AddPhotoBtn.tsx
  const onHandleUpload = () =&gt; {
    if (imgSrc.length &gt; 0) {
      imgSrc.forEach((src) =&gt; {
        AlbumAddMutation.mutate({ imgSrc: src, regionCate });
      });
      alert(&#39;앨범이 추가되었습니다.&#39;);
      setIsRigionModal(false);
    }
  };</code></pre>
<hr>
<blockquote>
<ul>
<li><code>new FileReader()</code> : 웹 애플리케이션에서 파일을 비동기적으로 읽을 수 있게 하는 역할로 주로 사용자가 업로드한 파일을 읽을 수 있게 해준다.</li>
</ul>
</blockquote>
<ul>
<li><code>readAsDataURL(파일)</code> : 파일을 Base64 인코딩된 데이터 URL로 변환
=&gt; Base64 인코딩되면 웹에서 미리보기가능(엄<del>~</del>청 김!)</li>
</ul>
<p><del>구글링 잘하자..
값 잘 확인하자..디버깅 잘하자..</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] Cannot read property 'default' of null]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Cannot-read-property-default-of-null</link>
            <guid>https://velog.io/@lim-bora/Next-js-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Cannot-read-property-default-of-null</guid>
            <pubDate>Tue, 29 Oct 2024 07:53:31 GMT</pubDate>
            <description><![CDATA[<p><strong>오류메세지)</strong></p>
<ul>
<li>TypeError: Cannot read properties of null (reading &#39;default&#39;)<pre><code class="language-javascript">&lt;AddPhotoBtn setImgSrc={setImgSrc} AlbumAddMutation={AlbumAddMutation} /&gt;
        {albumListData?.map((item, index) =&gt; (
          &lt;li key={item.id} className=&quot;h-[200px] overflow-hidden border border-black&quot;&gt;
              &lt;Image src={item.photoImg} alt=&quot;&quot; width={200} height={150} priority className=&quot;h-full&quot; /&gt;
          &lt;/li&gt;
        ))}</code></pre>
</li>
</ul>
<p><strong>원인)</strong>
전체앨범리스트에서 photoImg의 src값이 null/undefined이 있기때문에
src가 유효한 이미지로 정의되지않았던것이다.(빈 문자열도 작동)
<img src="https://velog.velcdn.com/images/lim-bora/post/93b869b5-a306-4c74-9ea7-b5906c9dc4ed/image.png" alt="">
<del>-&gt; 데이터 테스트를 한다고 이미지추가구현과 동시에 하다보니 null값이 하나 들어가있었다....</del></p>
<p><strong>해결방법)</strong>
<code>{item.photoImg &amp;&amp; (이미지태그)}</code></p>
<ol>
<li>이미지url이 있을경우만 유효성처리해주기</li>
<li>데이터 테이블에 값 추가할때 이미지Url null값 안들어가게 유효성처리추가하기<h4 id="수정코드">수정코드)</h4>
<pre><code>&lt;AddPhotoBtn setImgSrc={setImgSrc} AlbumAddMutation={AlbumAddMutation} /&gt;
       {albumListData?.map((item, index) =&gt; (
         &lt;li key={item.id} className=&quot;h-[200px] overflow-hidden border border-black&quot;&gt;
           {item.photoImg &amp;&amp; (
             &lt;Image src={item.photoImg} alt=&quot;&quot; width={200} height={150} priority className=&quot;h-full&quot; /&gt;
           )}
         &lt;/li&gt;
       ))}</code></pre></li>
</ol>
<p>레퍼런스
<a href="https://ggarden.tistory.com/entry/ReactNextjs-%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9E%85%EB%A0%A5%EB%B0%9B%EA%B8%B0">이미지 입력받기</a>
<a href="https://shiro21.tistory.com/355">이미지 트러블슈팅1</a>
<a href="https://github.com/vercel/next.js/discussions/29545">이미지 트러블슈팅2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 갤러리 구현1  - 이미지 업로드]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%EA%B0%A4%EB%9F%AC%EB%A6%AC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lim-bora/Next-js-%EA%B0%A4%EB%9F%AC%EB%A6%AC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 29 Oct 2024 07:51:46 GMT</pubDate>
            <description><![CDATA[<h4 id="supabase에서-앨범-전체-불러오기">supabase에서 앨범 전체 불러오기</h4>
<pre><code class="language-javascript">// fatchAlbumList.ts
export const fetchAlbum = async () =&gt; {
  const { data, error } = await browserClient.from(&#39;album&#39;).select(&#39;*&#39;);

  if (error) {
    console.error(&#39;가져오기 오류4:&#39;, error.message);
  }
  console.log(&#39;data&#39;, data);
  return data;
};</code></pre>
<h4 id="앨범목록-usequery로-data담기">앨범목록 useQuery로 data담기</h4>
<pre><code class="language-javascript">//AlbumList.tsx
import { fetchAlbum } from &#39;@/apis/fetchAlbumList&#39;;

const {
    data: albumListData,
    isLoading,
    isError
  } = useQuery({
    queryKey: [&#39;photo&#39;],
    queryFn: fetchAlbum
  });</code></pre>
<hr>
<h4 id="탭기능-구현-전체보기--지역별">탭기능 구현 (전체보기 | 지역별)</h4>
<pre><code class="language-javascript">//AlbumList.tsx
const AlbumList = () =&gt; {
  const [imgSrc, setImgSrc] = useState&lt;string&gt;(&#39;/images/default-image.png&#39;); //이미지url
  const [activeTab, setActiveTab] = useState(&#39;allTab&#39;); //탭상태

  //탭엑션
  const onClickTab = (tab: string) =&gt; {
    setActiveTab(tab);
  };

  //유저가 등록한 지역이름들(중복_지역이름제거)
  const filterRigionTitle = albumListData ? [...new Set(albumListData?.map((item) =&gt; item.region))] : [];
  //유저가 등록한 지역의 포토들
  const filterRigionPhoto = filterRigionTitle.map(
    (title) =&gt; albumListData?.filter((item) =&gt; item.region === title) || []
  );

  return (
    &lt;div&gt;
      {/* 전체보기-지역별 탭버튼 */}
      &lt;ul&gt;
        &lt;li
          className={`albumTab ${activeTab === &#39;allTab&#39; ? &#39;active&#39; : &#39;&#39;}`}
          onClick={() =&gt; onClickTab(&#39;allTab&#39;)}
        &gt;전체보기&lt;/li&gt;
        &lt;li
          className={`albumTab ${activeTab === &#39;rigionTab&#39; ? &#39;active&#39; : &#39;&#39;}`}
          onClick={() =&gt; onClickTab(&#39;rigionTab&#39;)}
        &gt;지역별&lt;/li&gt;
      &lt;/ul&gt;
      {/* 전체보기 */}
      {activeTab === &#39;allTab&#39; &amp;&amp; (
        &lt;ul&gt;
          &lt;AddPhotoBtn setImgSrc={setImgSrc} AlbumAddMutation={AlbumAddMutation} /&gt;
         ...
        &lt;/ul&gt;
      )}
      {/* 지역별 */}
      {activeTab === &#39;rigionTab&#39; &amp;&amp; (
        &lt;section&gt;
          &lt;div&gt;
          ...
          &lt;/div&gt;
        &lt;/section&gt;
      )}
    &lt;/div&gt;
  );
};</code></pre>
<hr>
<h2 id="파일-1개-업로드">파일 1개 업로드</h2>
<h4 id="파일에서-이미지-가져오기">파일에서 이미지 가져오기</h4>
<ul>
<li><code>accept=&quot;image/*&quot;</code> : 파일 중에서 이미지 파일만 입력</li>
</ul>
<pre><code class="language-javascript">const [imgSrc, setImgSrc] = useState(&#39;&#39;); 
//AddPhotoBtn.tsx
return (
    &lt;li&gt;
      &lt;input id=&quot;fileInput&quot; className=&quot;hidden&quot; type=&quot;file&quot; accept=&quot;image/*&quot; onChange={OnChangePhoto} /&gt;
      &lt;label htmlFor=&quot;fileInput&quot; &gt;+&lt;/label&gt;
    &lt;/li&gt;
  );</code></pre>
<h4 id="업로드-이벤트">업로드 이벤트</h4>
<p> 1.이미지선택 후 
 2.모달열기</p>
<pre><code class="language-javascript">//CategoryModal.tsx
  const OnChangePhoto = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const file = e.target.files?.[0];//클릭한대상의 이미지를 저장
    if (!file) return;
    const fileReader = new FileReader();//파일읽기용 객체
    fileReader.readAsDataURL(file);//파일읽기용에 file값 저장
    fileReader.onload = (e) =&gt; {
      if (typeof e.target?.result === &#39;string&#39;) {
        setImgSrc(e.target.result);//상태저장
        SetIsRigionModal(true);//모달열기
      }
    };
  };</code></pre>
<h4 id="모달에서-카테고리-선택후-업로드-버튼-클릭시">모달에서 카테고리 선택후 업로드 버튼 클릭시</h4>
<p> 3.모달에서 카테고리 선택 후 버튼클릭시 데이터 추가하기</p>
<pre><code class="language-javascript">//AddPhotoBtn.tsx
const onHandleUpload = () =&gt; {
    if (imgSrc.length &gt; 0) {
      AlbumAddMutation.mutate({ imgSrc, regionCate }); //데이터추가뮤테이션 호출
      alert(&#39;앨범이 추가되었습니다.&#39;);
      SetIsRigionModal(false); // 모달 닫기
    }
  };
</code></pre>
<hr>
<p>레퍼런스
<a href="https://ggarden.tistory.com/entry/ReactNextjs-%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9E%85%EB%A0%A5%EB%B0%9B%EA%B8%B0">이미지 입력받기</a>
<a href="https://shiro21.tistory.com/355">이미지 트러블슈팅1</a>
<a href="https://github.com/vercel/next.js/discussions/29545">이미지 트러블슈팅2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript - 타입에러]]></title>
            <link>https://velog.io/@lim-bora/%ED%83%80%EC%9E%85%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@lim-bora/%ED%83%80%EC%9E%85%EC%97%90%EB%9F%AC</guid>
            <pubDate>Mon, 28 Oct 2024 05:36:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>주로 나타나는 타입에러들을 정리해보니 대부분 기대한타입이 아닌 undefined일 경우의 에러다. 
예외처리를 꼭 한 번 더 체크하자.</p>
</blockquote>
<h4 id="1-typescript가-set-객체를-반복할-수-없다는-경고">1. TypeScript가 Set 객체를 반복할 수 없다는 경고</h4>
<pre><code class="language-javascript">const stampImg = [...new Set(stampList.filter((item) =&gt; item.region === list).map((item) =&gt; item.stampimg))];
//Set 형식은 --downlevelIteration 플래그 또는 es2015 이상의 --target 을 사용하는 경우에만 반복할 수 있습니다.</code></pre>
<p>tsconfig.json</p>
<pre><code>    &quot;target&quot;: &quot;es6&quot;,
    &quot;downlevelIteration&quot;: true, //이전 버전의 JS로 컴파일할 때, ES6 이상의 반복기능을 사용할 수 있도록(Set, Map, Array 사용가능)</code></pre><hr>
<h4 id="2-undefined일-때-할당불가-경고">2. &#39;undefined&#39;일 때 할당불가 경고</h4>
<pre><code class="language-javascript">if (!stampList) return console.log(&#39;데이터가 없습니다.&#39;); //유효성 검사 추가(화면에는 안나오게 console 사용)</code></pre>
<hr>
<h4 id="3-컴포넌트가-올바른-jsx-요소-형식이-아니라는-경고">3. 컴포넌트가 올바른 JSX 요소 형식이 아니라는 경고</h4>
<p>&#39;StampList&#39;은(는) JSX 구성 요소로 사용할 수 없습니다.
  해당 &#39;() =&gt; void | React.JSX.Element&#39; 형식은 올바른 JSX 요소 형식이 아닙니다.</p>
<hr>
<h4 id="4-string-형식은-string-형식에-할당할-수-없습니다">4. &#39;string[]&#39; 형식은 &#39;string&#39; 형식에 할당할 수 없습니다.</h4>
<pre><code class="language-javascript">//BF
const region = decodeURIComponent(params.id);</code></pre>
<ul>
<li>타입단언추가, params.id문자열로 반환</li>
</ul>
<pre><code class="language-javascript">//AF
const region = decodeURIComponent((params.id as string[]).toString()); </code></pre>
<hr>
<h4 id="5-이-타입형식에-이-없습니다">5. 이 타입형식에 &#39;~~&#39;이 없습니다</h4>
<ul>
<li>호출하는것의 타입 다시보기<pre><code class="language-javascript">//BF
const [stampData, setStampData] = useState&lt;StampDetailPropsType | null&gt;(null);
</code></pre>
</li>
</ul>
<p>//AF
const [stampData, setStampData] = useState&lt;StampDetailPropsType[]&gt;([]); //null값 안받고, 기본값도 배열로 변경</p>
<pre><code>---

#### 6. 이 호출과 일치하는 오버로드가 없습니다.
- undefined일 수 있다 
- undefined여부 확인 / 예외 처리를 추가
```javascript
{stampData.map((list) =&gt; (
            &lt;li key={list.id} className=&quot;flex items-center justify-between&quot;&gt;
              &lt;p&gt;{list.address}&lt;/p&gt;
              &lt;span&gt;{new Date(list.created_at).toLocaleDateString()}&lt;/span&gt; //이 호출과 일치하는 오버로드가 없습니다.
            &lt;/li&gt;
          ))}</code></pre><pre><code class="language-javascript">&lt;span&gt;{list.created_at ? new Date(list.created_at).toLocaleDateString() : &#39;N/A&#39;}&lt;/span&gt;</code></pre>
<hr>
<h4 id="6-1-이-호출과-일치하는-오버로드가-없습니다">6-1. 이 호출과 일치하는 오버로드가 없습니다.</h4>
<ul>
<li>존재여부 확인 후 예외처리 추가<pre><code class="language-javascript">//BF
const oldestDate = stampData.reduce((oldest, current) =&gt; {
  const oldestDate = new Date(oldest.created_at);
  const currentDate = new Date(current.created_at);
  return currentDate &lt; oldestDate ? current : oldest;
}, stampData[0]);
</code></pre>
</li>
</ul>
<p>//AF
  const oldestDate = stampData.reduce((oldest, current) =&gt; {
    const oldestDate = oldest.created_at ? new Date(oldest.created_at) : new Date();
    const currentDate = current.created_at ? new  Date(current.created_at) : new Date(); //있냐? &gt; 있으면 앞에꺼 없으면 뒤에꺼
    return currentDate &lt; oldestDate ? current : oldest;
  }, stampData[0]);</p>
<pre><code>
---
#### 7. &#39;undefined&#39; 형식은 &#39;SetStateAction&lt;StampDetailPropsType[]&gt;&#39; 형식에 할당할 수 없습니다.
- 배열을 기대했지만 undefined일 수 있다. 
- undefined일때 빈배열이 할당되게 수정
```javascript
//BF
const stampFilterList = res?.filter((item) =&gt; item.region === decodedParams);
//AF
const stampFilterList = res?.filter((item) =&gt; item.region === decodedParams) || [];</code></pre><hr>
<h4 id="8-setstateaction-이름을-찾을-수-없습니다dispatch-이름을-찾을-수-없습니다">8. SetStateAction 이름을 찾을 수 없습니다.&#39;Dispatch&#39; 이름을 찾을 수 없습니다.</h4>
<p><img src="https://velog.velcdn.com/images/lim-bora/post/6afa45d9-fbe5-4f39-b65b-916ac3c30993/image.png" alt=""></p>
<ul>
<li>setVisit 값을 Prop으로 넘겨줬을때 interface 에서
setVisit 가 받는 타입을 알림창으로 정의해준다.
붙여줬을때 Dispatch, SetStateAction은 리엑트 패키지에서 불러와야한다.<pre><code class="language-javascript">import React, { useEffect, useState, Dispatch, SetStateAction } from &#39;react&#39;;
</code></pre>
</li>
</ul>
<p>interface StampActivePropsType {
  address: AddressPropsType;
  setVisit: Dispatch&lt;SetStateAction<Boolean>&gt;;
  visit: Boolean;
  stampList: any[] | null | undefined; //TODO: any 추후수정
}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 인코딩과 디코딩]]></title>
            <link>https://velog.io/@lim-bora/Next-js-%EC%9D%B8%EC%BD%94%EB%94%A9%EA%B3%BC-%EB%94%94%EC%BD%94%EB%94%A9</link>
            <guid>https://velog.io/@lim-bora/Next-js-%EC%9D%B8%EC%BD%94%EB%94%A9%EA%B3%BC-%EB%94%94%EC%BD%94%EB%94%A9</guid>
            <pubDate>Sun, 27 Oct 2024 12:30:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>useParams 로 가져온 값 기준으로 데아터를 더 좁혀서 추출해야한다.
하지만 계속 빈값으로 추출된다. 원인은?</p>
</blockquote>
<h4 id="원인">원인</h4>
<p>: 난 당연히 useParams 로 가져왔을때 <code>/서울</code> 이라면 서울로 가져올 줄 알고 비교하는 코드를 짰는데 자꾸 빈값을 추출하길래 console을 찍어보니 아래 사진처럼 값이 나왔다..</p>
<p><img src="https://velog.velcdn.com/images/lim-bora/post/3ff4dcac-7702-4eeb-94ba-d62654babfbe/image.png" alt=""></p>
<h4 id="해결법">해결법</h4>
<p>-&gt; 이거는 <code>URL 인코딩된 문자열</code>이라고 하는데 
<code>인코딩</code> : URL에서 사용할 수 없는 문자를 안전하게 변환하는 방법
-&gt; 이런 이유로 내가 원하는 서울 그대로 쓰려면 디코딩을 해야한다.
<code>디코딩</code> : 인코딩된 데이터를 원래의 형식으로 변환하는 과정
<code>decodeURIComponent</code> 함수 사용</p>
<pre><code class="language-javascript">const params = useParams(); //서울
const region = decodeURIComponent(params.id); //디코딩으로 새로 저장

  useEffect(() =&gt; {
    if (userId) { //로그인한 유저아이디가 있고
      const fetchData = async () =&gt; {
        try {
          const res = await fetchStampActive(userId); //로그인 유저의 스탬프 테이블 데이터 전체 가져오기
          const decodedParams = region; //디코딩된것 저장
          const res2 = res?.filter((item) =&gt; item.region === decodedParams); //전체데이터 중 디코딩된것과 값이 같은거만 추출
          setStampData(res2); //상태에 저장
        } catch (error) {
          console.error(error);
        }
      };
      fetchData();
    }
  }, [params.id, userId]);</code></pre>
<p>코드 짜기전에 꼭 콘솔로 값 다 확인하기..꼭꼭.....</p>
<hr>
<p>참고자료
<a href="https://boringariel.tistory.com/78">자바스크립트에서 URL 인코딩, 디코딩 알아보기</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent">mdn) decodeURIComponent()</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js]가장 오래된 날짜 구하기]]></title>
            <link>https://velog.io/@lim-bora/%EA%B0%80%EC%9E%A5-%EC%98%A4%EB%9E%98%EB%90%9C-%EB%82%A0%EC%A7%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@lim-bora/%EA%B0%80%EC%9E%A5-%EC%98%A4%EB%9E%98%EB%90%9C-%EB%82%A0%EC%A7%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 27 Oct 2024 11:52:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>날짜를 가지고있는 객체를 담은 배열에서 가장 오래된 날짜를 추출해야한다.</p>
</blockquote>
<h4 id="stampdata_">stampData_</h4>
<p><img src="https://velog.velcdn.com/images/lim-bora/post/e78083b3-5fae-4924-b302-54b6b98a5de8/image.png" alt="stampData"></p>
<hr>
<h4 id="시도1_x">시도1_(X)</h4>
<pre><code class="language-javascript">  // 가장 오래된 날짜 구하기
  const oldestDate = stampData.map((list) =&gt; {
    return list.created_at.reduce((oldest, current) =&gt; {
      return current &lt; oldest ? current : oldest;
    });
  });
  console.log(&#39;oldestDate&#39;, oldestDate);</code></pre>
<h4 id="문제점">문제점)</h4>
<ul>
<li><code>TypeError: list.created_at.reduce is not a function</code></li>
<li>list.created_at.reduce 이게 없다고 나옴 <h4 id="원인">원인)</h4>
</li>
<li>이유는 스트링이라 비교불가</li>
</ul>
<hr>
<h4 id="시도2_o">시도2_(O)</h4>
<pre><code class="language-javascript">  // 가장 오래된 날짜 구하기
  const oldestDate =
    stampData.reduce((oldest, current) =&gt; {
      const oldestDate = new Date(oldest.created_at); //비교대상날짜
      const currentDate = new Date(current.created_at); //비교할 기준점날짜
      return currentDate &lt; oldestDate ? current : oldest;
    });</code></pre>
<ul>
<li>reduce는 기존에 map을 따로 사용안해도 reduce자체에서 순회하며 값을 비교하기때문에 map을 제거</li>
<li>문자열로 저장된 날짜를 값 비교를 위해 new Date 로 새로 변수에 담아준다. </li>
<li>날짜를 기준으로 삼항연산자 :
<code>currentDate &lt; oldestDate</code>
기준점(currentDate)이 oldestDate보다 오래된날짜라면 
<code>ccurrent : oldest</code>
<code>true</code> : current가 가장 오래된 날짜
<code>false</code> : oldest가 가장 오래된 날짜</li>
</ul>
<p>참고자료
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce">mdn) Array.prototype.reduce()</a>
<a href="https://5kdk.tistory.com/2">reduce 함수란?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next js] 패치 비동기 함수에서 유저아이디가 안가져오는 문제(null, undefind)]]></title>
            <link>https://velog.io/@lim-bora/%ED%8C%A8%EC%B9%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%A8%EC%88%98%EC%97%90%EC%84%9C-%EC%9C%A0%EC%A0%80%EC%95%84%EC%9D%B4%EB%94%94%EA%B0%80-%EC%95%88%EA%B0%80%EC%A0%B8%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9Cnull-undefind</link>
            <guid>https://velog.io/@lim-bora/%ED%8C%A8%EC%B9%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%A8%EC%88%98%EC%97%90%EC%84%9C-%EC%9C%A0%EC%A0%80%EC%95%84%EC%9D%B4%EB%94%94%EA%B0%80-%EC%95%88%EA%B0%80%EC%A0%B8%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9Cnull-undefind</guid>
            <pubDate>Fri, 25 Oct 2024 13:27:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>userId는 잘 전해졌는데 계속 null 또는 undefind로 뜨는 문제</p>
</blockquote>
<h4 id="fetchstampactivets-패치-비동기-함수">fetchStampActive.ts 패치 비동기 함수</h4>
<pre><code class="language-javascript">//로그인유저의 스템프 항목 전부 + 스탬프 활성화된 데이터만
export const fetchStampActive = async (userId: string) =&gt; {
  const { data, error } = await browserClient.from(&#39;stamp&#39;).select(&#39;*&#39;).eq(&#39;user_id&#39;, userId).eq(&#39;visited&#39;, true);
  if (error) {
    console.error(&#39;가져오기 오류1:&#39;, error.message);
  }
  return data;
};
</code></pre>
<hr>
<h4 id="stamplist-컴포넌트be">StampList 컴포넌트(BE)</h4>
<p><strong>수정01</strong>_
queryFn: () =&gt; fetchStampActive(userId!) 
-&gt; null이 아님을 보장하는 ! 사용</p>
<pre><code class="language-javascript">&#39;use client&#39;;
...
import { fetchStampActive } from &#39;@/server/fetchStampList&#39;;

const StampList = () =&gt; {
  const [userId, setUserId] = useState&lt;string | null&gt;(null);

  useEffect(() =&gt; {
    //유저아이디 가져오기
  }, []);

  const {
    data: stampList,
    isLoading,
    error
  } = useQuery({
    queryKey: [&#39;stamp&#39;], //고유키
    queryFn: () =&gt; fetchStampActive(userId!),
  });
};

export default StampList;
</code></pre>
<hr>
<p><strong>수정02</strong>_
enabled: !!userId 
-&gt; enabled: userId가 있을 때만 쿼리 실행
<em>해결X :  undefind 로 불러와짐..</em></p>
<pre><code class="language-javascript">queryKey: [&#39;stamp&#39;], 
queryFn: () =&gt; fetchStampActive(userId!),
enabled: !!userId // userId가 있을 때만 쿼리 실행</code></pre>
<hr>
<p><strong>수정03</strong>_
-&gt; 비동기문제인거같아 똑같이 async/await 붙여줌
-&gt; userId값이 있는경우, 없는경우의 if문 추가</p>
<h4 id="stamplist-컴포넌트af---수정-완료">StampList 컴포넌트(AF) - 수정 완료</h4>
<pre><code class="language-javascript">&#39;use client&#39;;
...
import { fetchStampActive } from &#39;@/server/fetchStampList&#39;;

const StampList = () =&gt; {
  const [userId, setUserId] = useState&lt;string | null&gt;(null);

  useEffect(() =&gt; {
    //유저아이디 가져오기
  }, []);

  const {
    data: stampList,
    isLoading,
    error
  } = useQuery({
    queryKey: [&#39;stamp&#39;], //고유키
    queryFn: async () =&gt; {
      if (userId) {
        return await fetchStampActive(userId);
      } else {
        return null;
      }
    },
    enabled: !!userId // userId가 있을 때만 쿼리 실행
   enabled: !!userId // userId가 있을 때만 쿼리 실행
  });
};

export default StampList;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[supabase 로그인 유저 정보 가져오기]]></title>
            <link>https://velog.io/@lim-bora/Next-.js-supabase-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A0%80-%EC%A0%95%EB%B3%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@lim-bora/Next-.js-supabase-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A0%80-%EC%A0%95%EB%B3%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Fri, 25 Oct 2024 11:27:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>로그인 유저정보를 가져오는 방법으로 한 파일에 모두 불러오거나 파일분리로 하는법 두 가지를 모두 써보았는데 보기에는 파일분리가 더 깔끔해보인다. 
텐스텍쿼리로 값을 연결지어서 쓰는법까지 같이 익혔다.
상황에 따라 맞는 방법을 쓰겠지만 난 이번에 2번째방법을 쓸거같다.</p>
</blockquote>
<h3 id="1번째-방법---한-컴포넌트에-getuser">1번째 방법 - 한 컴포넌트에 getUser</h3>
<ol>
<li>userId 상태값 저장할 state 생성</li>
<li>useEffect 로 browserClient.auth.getUser() 감싸주기</li>
</ol>
<p>-&gt;처음 랜더링됬을때 유저값 가져오기
-&gt;browserClient.auth.getUser() : supabase에서 로그인된유저값 가져오는거
3. setUserId(데이터) 넣어서 userId값에 저장되도록
4. mutaion실행하는 이벤트함수(삭제클릭,추가클릭이벤트)에 인자로 userId 넘겨주기
5. supabase에 추가,삭제하는 뮤테이션함수(비동기)에서 인자로 userId 받기</p>
<h4 id="--stampactive-컴포넌트">- StampActive 컴포넌트</h4>
<pre><code class="language-javascript">&#39;use client&#39;;
...
//뮤테이션 함수 만들기(수파베이스 값 추가)
const addStampList = async ({
  ...
  userId
}) =&gt; {
  const { data, error } = await browserClient.from(&#39;stamp&#39;).insert({
    user_id: userId,
    ...
  });
  if (error) console.log(&#39;error&#39;, error);
  return data;
};

//뮤테이션 함수 만들기(수파베이스 값 삭제)
const deleteStampList = async ({ address, userId }) =&gt; {
  const { data, error } = await browserClient.from(&#39;stamp&#39;).delete().eq(&#39;address&#39;, address).eq(&#39;user_id&#39;, userId);
  if (error) console.error(&#39;삭제중 오류 발생:&#39;, error);
  return data;
};


const StampActive = ({ address }: StampActivePropsType) =&gt; {
  const queryClient = useQueryClient();
  const router = useRouter();

  const [userId, setUserId] = useState&lt;string | null&gt;(null);

  useEffect(() =&gt; {
    const fetchUser = async () =&gt; {
      const { data, error } = await browserClient.auth.getUser();
      if (error || !data?.user) {
        router.push(&#39;/logIn&#39;);
      } else {
        setUserId(data.user.id);
      }
    };
    fetchUser();
  }, [router]);

  //useMutation(삭제)
  const StampDeleteMutation = useMutation({
    mutationFn: deleteStampList,
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: [&#39;nowStamp&#39;] });
    }
  });
  //useMutation(추가)
  const StampAddMutation = useMutation({
    mutationFn: addStampList,
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: [&#39;nowStamp&#39;] });
    }
  });

  //mutate 추가이벤트(방문안한 상태에서 누르면)
  const onClickVisitedAdd = (address: string, regionName: string) =&gt; {
    const visitedConfirmed = window.confirm(&#39;스탬프를 찍겠습니까?&#39;);
    if (visitedConfirmed) {
      StampAddMutation.mutate({ address, regionName, userId });
      console.log(&#39;스탬프가 찍혔습니다.&#39;);
    } else {
      return;
    }
  };
  //mutate 삭제이벤트(방문한 상태에서 누르면)
  const onClickVisitedCencle = (address: string) =&gt; {
    const cancelConfirmed = window.confirm(&#39;스탬프를 취소하시겠습니까?&#39;);
    if (cancelConfirmed) {
      StampDeleteMutation.mutate({ address, userId });
      console.log(&#39;스탬프가 취소되었습니다.&#39;);
    } else {
      return;
    }
  };

  //useQuery
  const {
    data: stampList,
    isLoading,
    error
  } = useQuery({
    queryKey: [&#39;nowStamp&#39;, address.address_name], //고유키값
    queryFn: () =&gt; fetchStampList(address.address_name) // 주소를 인자로 넘김
  });
  if (isLoading) return &lt;div&gt;Loading...&lt;/div&gt;;
  if (error) return &lt;div&gt;Failed to load&lt;/div&gt;;
  console.log(&#39;address&#39;, address);
  const REGIONimageUrl = STAMPIMG_REGION_NAME[address.region_1depth_name];
...
</code></pre>
<h3 id="2번째-방법---파일분리-함수-활용">2번째 방법 - 파일분리 ,함수 활용</h3>
<ol>
<li>패치비동기함수(.auth.getUser()) 파일분리 - fetchUser.ts</li>
<li>사용할 컴포넌트에서 import로 fetchUser.ts 불러오기</li>
<li>userId state 생성</li>
<li>useEffect로 처음 랜더링됬을때 패치함수 호출</li>
<li>setUserId(데이터) 넣어서 userId값에 저장되도록</li>
<li>useQuery에서 쿼리함수에 userId 인자로 넘겨주기</li>
<li>쿼리함수에서 인자로 userId 받아서 사용하기</li>
</ol>
<h4 id="--fetchuserts">- fetchUser.ts</h4>
<pre><code class="language-javascript">import browserClient from &#39;@/utils/supabase/client&#39;;

//로그인 유저 아이디 가져오기
export const fetchUser = async () =&gt; {
  const { data, error } = await browserClient.auth.getUser();
  if (error || !data?.user) {
    console.log(&#39;error&#39;);
  }
  return data.user?.id;
};
fetchUser();
</code></pre>
<h4 id="--fetchstamplist-컴포넌트">- fetchStampList 컴포넌트</h4>
<pre><code class="language-javascript">&#39;use client&#39;;

import React, { useEffect, useState } from &#39;react&#39;;
import StampItem from &#39;@/components/stamp/StampItem&#39;;
import { useQuery } from &#39;@tanstack/react-query&#39;;
import browserClient from &#39;@/utils/supabase/client&#39;;
import { fetchUser } from &#39;@/utils/fetchUser&#39;;

//로그인유저의 스템프 항목 전부 가져오기
const fetchStampList = async (userId: string) =&gt; {
  const { data, error } = await browserClient.from(&#39;stamp&#39;).select(&#39;*&#39;).eq(&#39;userid&#39;, userId).eq(&#39;visited&#39;, true);
  if (error) console.error(&#39;가져오기 오류:&#39;, error.message);
  return data;
};

const StampList = () =&gt; {
  const [userId, setUserId] = useState&lt;string | null&gt;(null);

  useEffect(() =&gt; {
    const checkUser = async () =&gt; {
      const user = await fetchUser();
      if (!user) return;
      else setUserId(user);
    };
    checkUser();
  }, []);

  const {
    data: stampList,
    isLoading,
    error
  } = useQuery({
    queryKey: [&#39;stamp&#39;], //고유키
    queryFn: () =&gt; fetchStampList(userId!) //!null이 아님 보장
  });
  if (isLoading) return &lt;div&gt;Loading...&lt;/div&gt;;
  if (error) return &lt;div&gt;Failed to load&lt;/div&gt;;

  return (
    &lt;ul className=&quot;grid grid-cols-2 gap-4&quot;&gt;{stampList?.map((stamp) =&gt; &lt;StampItem key={stamp.id} stamp={stamp} /&gt;)}&lt;/ul&gt;
  );
};

export default StampList;
</code></pre>
<p>레퍼런스
<a href="https://supabase.com/docs/guides/auth/server-side/nextjs?queryGroups=router&amp;router=app">supabase 공식문서</a></p>
]]></description>
        </item>
    </channel>
</rss>