<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>꾸준히를 목표로</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 30 Jun 2025 04:22:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 꾸준히를 목표로. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/well_log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[ Portfolio ] GitHub → AWS S3 데이터 이전]]></title>
            <link>https://velog.io/@well_log/Portfolio-GitHub-AWS-S3-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@well_log/Portfolio-GitHub-AWS-S3-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Mon, 30 Jun 2025 04:22:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프로젝트 데이터 관리할 때 GitHub에서 관리했었는데, GitHub가 정적 리소스를 서빙하는 용도가 아니라서 새로운 데이터 추가/수정 할 때 레포 로딩 지옥이 열리고, 또 데이터 추가할 때 용량 제한같은 문제 사항이 있어서 AWS S3로 관리하기로 함.</p>
</blockquote>
<h2 id="문제-사항-정리">문제 사항 정리</h2>
<ul>
<li>GitHub에 JSON 형식의 프로젝트 데이터 저장하고 프론트엔드에서 직접 요청하여 로딩</li>
<li>GitHub의 정적 파일 로딩 속도가 느려서 사용자 경험 저하</li>
<li>데이터 추가/수정 작업 위해 레포 여는 경우, 레포 로딩 지옥 발생</li>
<li>GitHub 개별 파일 용량 제한으로 인한 푸시 거부</li>
</ul>
<h2 id="해결">해결</h2>
<ul>
<li>JSON 데이터를 AWS S3 버킷으로 이전</li>
<li>각 데이터를 S3에 폴더별로 분리하여 정적 객체 업로드하고, 퍼블릭 읽기 권한 설정</li>
<li>프론트엔드에서 Axios로 동적 패칭하여 콘텐츠 로딩</li>
</ul>
<h2 id="효과">효과</h2>
<ul>
<li>응답 속도 향상</li>
<li>비용 효율적</li>
<li>확장성 확보</li>
<li>GitHub 레포 부담 해소 </li>
</ul>
<h2 id="기타">기타</h2>
<ul>
<li><a href="https://velog.io/@well_log/DR-PORTFOLIO">Portfolio 프로젝트 리뷰</a></li>
<li><a href="https://velog.io/@well_log/AWS-S3-%EB%B2%84%ED%82%B7-%EC%83%9D%EC%84%B1">AWS S3 버킷 생성</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ React ] Rendered fewer hooks than expected. This may be caused by an accidental early return statement.]]></title>
            <link>https://velog.io/@well_log/React-Rendered-fewer-hooks-than-expected.-This-may-be-caused-by-an-accidental-early-return-statement</link>
            <guid>https://velog.io/@well_log/React-Rendered-fewer-hooks-than-expected.-This-may-be-caused-by-an-accidental-early-return-statement</guid>
            <pubDate>Sat, 28 Jun 2025 08:07:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Rendered fewer hooks than expected. This may be caused by an accidental early return statement.</strong></p>
</blockquote>
<p>React에서 훅(Hooks) 을 사용하는 컴포넌트가 렌더링 흐름 중에 조건문 등으로 인해 일부 훅이 실행되지 않았을 때 발생.</p>
<h3 id="주요-원인">주요 원인</h3>
<ol>
<li>훅이 조건문 안에 들어가 있는 경우</li>
</ol>
<ul>
<li><code>if</code>, <code>else</code>, <code>switch</code>, <code>while</code>, <code>for</code>, 삼항연산자(<code>? :</code>), 논리연산자(<code>&amp;&amp;</code>) 등
→ 훅이 실행될 수도 있고, 안 될 수도 있기 때문에 순서가 꼬임<pre><code>if (someCondition) {
const [x, setX] = useState(0); // ❌ 에러 발생
}</code></pre></li>
</ul>
<ol start="2">
<li><p>조기 <code>return</code> 이후 훅 사용</p>
<pre><code>if (!data) return null;
const [value, setValue] = useState(); // ❌ 실행되지 않을 수 있음</code></pre></li>
<li><p><code>useX()</code> 형태의 커스텀 훅 내부에 조건문이 있을 경우</p>
<pre><code>function useSomething(enabled: boolean) {
if (!enabled) return;

const [val, setVal] = useState(0); // ❌ 조건문 안에 있음
}</code></pre></li>
</ol>
<hr />

<p>나의 경우 조기 <code>return</code> 이후 훅 사용이었음.</p>
<h4 id="에러">에러</h4>
<pre><code>const { data, isLoading } = useGetServeralCategories({ limit: 20, offset: 0 });
const { ref, inView } = useInView();

if (isLoading) return &lt;LoadingSpinner /&gt;;

useEffect(() =&gt; {}, []); // 에러 발생</code></pre><h4 id="수정">수정</h4>
<pre><code>const { data, isLoading } = useGetServeralCategories({ limit: 20, offset: 0 });
const { ref, inView } = useInView();

useEffect(() =&gt; {}, []);

if (isLoading) return &lt;LoadingSpinner /&gt;;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AWS S3 버킷 생성]]></title>
            <link>https://velog.io/@well_log/AWS-S3-%EB%B2%84%ED%82%B7-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@well_log/AWS-S3-%EB%B2%84%ED%82%B7-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Fri, 27 Jun 2025 14:47:02 GMT</pubDate>
            <description><![CDATA[<p>개인 포트폴리오 사이트에서 프로젝트 데이터들을 별도의 GitHub 레포를 파서 관리했었음.
그런데 문제가 새로운 데이터를 추가할 때나, 수정할 때 용량 제한과 또 레포 자체가 무거워져서 레포 로딩 지옥이 열렸음.</p>
<p>또 Axios로 받아올 때도 속도가 느렸었음.</p>
<p>그래서 이러한 문제로 GitHub에서 AWS S3로 변경하여 프로젝트 데이터들을 관리하기로 함.</p>
<hr />

<h2 id="aws-s3-버킷-생성">AWS S3 버킷 생성</h2>
<ol>
<li>AWS에서 S3 버킷 검색하고, 버킷 생성 누르고 버킷 이름까지 지정
<img src="https://velog.velcdn.com/images/well_log/post/a85087b9-3a99-4809-b85c-6eb562ade430/image.png" alt=""></li>
<li><img src="https://velog.velcdn.com/images/well_log/post/76375386-7f8e-44bc-8d99-3e52a815623b/image.png" alt=""></li>
<li>퍼블릭 설정
모든 퍼블릭 액세스를 차단하면 모든 접근을 차단하기 때문에, 일단 나는 모두 허용으로 변경함. (세부 사항을 체크하면서 설정하는게 좋음)
<img src="https://velog.velcdn.com/images/well_log/post/b9db3a35-40ca-4d5e-ac0b-d8a1a671ecf0/image.png" alt=""> </li>
<li>그리고 나머지들은 손 안대고 버킷 생성</li>
</ol>
<blockquote>
<p>나는 이미 만들어둔 버킷이 있어서 그런지 지역 설정이 없는데, 지역 설정은 버킷이 많이 사용 될 지역을 설정하는게 좋음</p>
</blockquote>
<hr />

<h2 id="파일-올리기">파일 올리기</h2>
<p>버킷이 생성되었으면, 필요한 파일들을 올려줌
나는 각 프로젝트명으로 폴더를 분리해서 올려줬음.</p>
<hr/>

<p>여기까지 아무 설정없이 버킷을 생성하고 axios로 데이터 불러오려고 하면, <code>access Denied</code>임.
이제 퍼블릭 액세스 차단을 해제하기 위해 권한 설정에 들어가야함.</p>
<hr/>

<h2 id="퍼블릭-액세스-차단">퍼블릭 액세스 차단</h2>
<p>이건 처음에 버킷 만들 때, 퍼블릭 액세스 차단 설정을 모두 해제했기 때문에 <code>비활성</code>으로 표시되어 있음.
<img src="https://velog.velcdn.com/images/well_log/post/57e305b8-f315-4027-ae51-8f3553accb3d/image.png" alt=""></p>
<h2 id="버킷-정책">버킷 정책</h2>
<p><img src="https://velog.velcdn.com/images/well_log/post/764a3760-c0f8-4df1-ae04-9d7f05443b14/image.png" alt=""></p>
<p>편집 누르면 버킷 정책 편집기 화면이 나옴</p>
<h3 id="정책-편집기">정책 편집기</h3>
<p><img src="https://velog.velcdn.com/images/well_log/post/c071b2d6-fd38-414e-94af-ca7ac206851f/image.png" alt=""></p>
<p>그리고 정책 생성기로 이동</p>
<h3 id="정책-생성기">정책 생성기</h3>
<p><img src="https://velog.velcdn.com/images/well_log/post/ace262d7-a0c4-4fa8-a3aa-7fdb3df4a701/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/well_log/post/a7b826b3-1c90-4f33-842c-ed39aa022a8b/image.png" alt=""></p>
<ul>
<li>Select policy type : S3 Bucket Policy</li>
<li>Effect : Allow</li>
<li>Principal : *</li>
<li>Actions: GetObject</li>
<li>ARN : 정책 편집기에 있음</li>
</ul>
<p>그리고 생성하기 누르면 JSON 형태 문서가 나오는데
Resource의 맨 마지막에 <code>/*</code> 추가하고 정책 편집기에 해당 JSON 붙여넣기</p>
<hr />

<p>그리고 나는 <code>CORS</code> 에러도 떴었는데, 버킷 권한 설정 맨 마지막 <code>CORS</code>에 해당 내용 추가함
<img src="https://velog.velcdn.com/images/well_log/post/6be2d9ad-2967-43cc-90c1-587975c3589f/image.png" alt=""></p>
<pre><code>[
    {
        &quot;AllowedHeaders&quot;: [
            &quot;*&quot;
        ],
        &quot;AllowedMethods&quot;: [
            &quot;GET&quot;,
            &quot;HEAD&quot;
        ],
        &quot;AllowedOrigins&quot;: [
            &quot;*&quot;
        ],
        &quot;ExposeHeaders&quot;: [
            &quot;x-amz-server-side-encryption&quot;,
            &quot;x-amz-request-id&quot;,
            &quot;x-amz-id-2&quot;
        ],
        &quot;MaxAgeSeconds&quot;: 3000
    }
]</code></pre><hr />

<h2 id="결과">결과</h2>
<p>이렇게 버킷 설정하고 나면, 해당 S3에 있는 데이터들을 불러올 수 있음.</p>
<blockquote>
<p><strong>cloudFront</strong>
<code>cloudFront</code>는 아마존에서 제공하는 CDN 서비스인데, 이걸 해주면 훨씬 더 빠르게 글로벌 서빙이 가능함.
그리고 정적 리소스 캐싱도 가능하고, 커스텀 도메인 연결도 가능, 보안에도 좋음</p>
</blockquote>
<h4 id="🚀-어떤-경우-cloudfront를-쓰는가">🚀 어떤 경우 CloudFront를 쓰는가:</h4>
<p><strong>서비스 / 배포용 / 속도 중요 → S3 + CloudFront 추천</strong></p>
<ul>
<li>전 세계 사용자 대상이면 (지리적 거리 단축)</li>
<li>같은 이미지 요청이 반복돼서 캐싱 이점이 큰 경우</li>
<li>커스텀 CDN 도메인 (cdn.내도메인.com)을 쓰고 싶을 때</li>
<li>S3 접근을 CloudFront로만 허용하고, S3는 비공개로 만들고 싶을 때<blockquote>
<p>그런데 지금 내 포트폴리오 같은 경우에는, 나 혼자 쓰는 소규모 프로젝트라 S3만으로도 충분해서 CloudFront는 안쓰기로 함.</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 핸들링 with Tanstack Query #2]]></title>
            <link>https://velog.io/@well_log/%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-with-Tanstack-Query-2</link>
            <guid>https://velog.io/@well_log/%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-with-Tanstack-Query-2</guid>
            <pubDate>Tue, 24 Jun 2025 07:06:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Tanstack Query의 에러 핸들링을 위해 블로그들을 찾아보면 <code>useCallback</code>을 사용하여 핸들러 훅을 만드는 것을 찾아볼 수 있음.</p>
<p>훅을 만들어서 사용하는 방법과 이전에 작성한 <a href="https://velog.io/@well_log/%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-with-Tanstack-Query-1">에러 핸들링 with Tanstack Query #1</a>의 방식을 비교하고자 함.</p>
</blockquote>
<hr />

<h2 id="일단">일단,</h2>
<ol>
<li>React 훅을 사용하려면 컴포넌트 함수나 커스텀 훅 내부에서만 사용할 수 있다
→ <code>index.tsx</code>에서 사용 불가</li>
<li>그래서 훅을 이용한 핸들러를 적용하고 싶다면?
→ <code>App.tsx</code>에서 사용해야한다</li>
</ol>
<hr />

<h2 id="핸들러-훅usecallback-사용하기">핸들러 훅(useCallback) 사용하기</h2>
<pre><code>// utils
export const useApiError = () =&gt; {
  const handleError = useCallback((error: unknown) =&gt; {
    // 에러 핸들링 with Tanstack Query #1 코드 참고
  }, []);

  return { handleError };
};</code></pre><pre><code>// App.tsx

function App() {

const { handleError } = useApiError();

const [queryClient] = useState(() =&gt; new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) =&gt; handleApiError(error),
   }),
   mutationCache: new MutationCache({
    onError: (error, _variables, _context, mutation) =&gt; handleApiError(error),
  }),
}));

return (
  &lt;QueryClientProvider client={queryClient}&gt;
      {/* ... */}
  &lt;/QueryClientProvider&gt;
  );
}</code></pre><h4 id="handleerror를-왜-usecallback으로-썼는지"><code>handleError</code>를 왜 <code>useCallback</code>으로 썼는지</h4>
<ol>
<li>일단 컴포넌트 내부에서 부름 → 이 말은, 컴포넌트가 리렌더링 할 때마다 함수가 새로 만들어진다는 의미 이를 방지하기 위해 <code>useCallback</code>으로 만들어짐</li>
<li><code>React Query</code> 훅의 <code>onError</code>로 넘김
참조 동일성 유지가 중요한데, <code>useCallback</code>으로 안하면 함수가 새로 만들어져서 React Query 내에서 불필요한 리패칭이나 리렌더링이 생길 수 있음</li>
</ol>
<h4 id="queryclient에-usestate-사용-이유">QueryClient에 useState 사용 이유</h4>
<blockquote>
<p><strong>목적 : 컴포넌트 리렌더링 시 <code>QueryClient</code> 재생성되는거 방지하기 위해</strong></p>
</blockquote>
<ul>
<li><code>new QueryClient</code>도 객체임 → 렌더링시마다 새로운 인스턴스가 계속 생성됨 → 참조값이 다르니까 React는 새로운 값으로 인식해서 리렌더링함 → 같은 인스턴스를 계속 써야 캐시/쿼리 상태가 유지가 정상적으로 되는데, 새로운거를 만드니까
문제 : 이전 캐시 날아감, 상태 리셋, 전역 에러 핸들링도 리셋, refetch도 꼬임 등</li>
<li><code>useState(() =&gt; ...)</code>로 첫 렌더링 시 한 번만 실행되고 값 유지 → 객체 재생성 방지</li>
</ul>
<blockquote>
</blockquote>
<h4 id="✅-전역-queryclientprovider-vs-컴포넌트-내부-queryclientprovider-차이">✅ 전역 QueryClientProvider vs 컴포넌트 내부 QueryClientProvider 차이</h4>
<ul>
<li><code>전역 QueryClientProvider</code>에서는 <ul>
<li><code>index.tsx</code>나 앱 최상단에 한 번만 <code>QueryClient</code> 인스턴스 생성하고, <code>QueryClientProvider</code>로 감쌈</li>
<li>이 경우 <code>queryClient</code> 객체는 앱 라이프사이클 내내 한 번만 생성되고 재사용됨</li>
<li>그래서 전역 <code>onError</code> 핸들러도 한 번만 등록되고 함수 참조가 계속 동일하니까 <code>useCallback</code> 필요없음</li>
</ul>
</li>
<li><code>컴포넌트 내부 QueryClientProvider</code>에서는<ul>
<li><code>App.tsx</code> 같은 컴포넌트 내부에서 <code>QueryClientProvider</code>와 <code>QueryClient</code>를 생성하면</li>
<li>컴포넌트가 리렌더링 될 때마다 새로운 인스턴스 생성하니까 훅을 <code>useCallback</code>으로 감쌌고,</li>
<li><code>queryClient</code>도 객체니까 <code>useState</code>로 한 번만 생성하도록 해서 재사용<blockquote>
</blockquote>
<h4 id="결론--전역은-앱-시작-시-1회-queryclient-생성하고-컴포넌트-내부는-매-렌더-시마다-생성하니까-usecallback이나-usestate가-필수">결론 : 전역은 앱 시작 시 1회 <code>queryClient</code> 생성하고, 컴포넌트 내부는 매 렌더 시마다 생성하니까 <code>useCallback</code>이나 <code>useState</code>가 필수</h4>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 핸들링 with Tanstack Query #1]]></title>
            <link>https://velog.io/@well_log/%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-with-Tanstack-Query-1</link>
            <guid>https://velog.io/@well_log/%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-with-Tanstack-Query-1</guid>
            <pubDate>Tue, 24 Jun 2025 06:10:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Tanstack Query는 API 호출에서 발생할 수 있는 에러도 강력하게 핸들링할 수 있는 기능을 제공함.</p>
</blockquote>
<h4 id="전역-에러-핸들링이-필요한-이유">전역 에러 핸들링이 필요한 이유</h4>
<ul>
<li>중복된 에러 로직 처리</li>
<li>사용자에게 일관된 UX 제공 가능(예: toast 알림)</li>
<li>인증 실패, 서버 오류, 네트워크 오류 등의 케이스에 공통 대응 가능</li>
</ul>
<hr />

<h2 id="핸들링-적용">핸들링 적용</h2>
<pre><code>// index.tsx

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) =&gt; handleApiError(error),
  }),
  mutationCache: new MutationCache({
    onError: (error, _variables, _context, mutation) =&gt; handleApiError(error),
  }),
});

root.render (
  &lt;React.StrictMode&gt;
    ...
      &lt;QueryClientProvider client={queryClient}&gt;
          &lt;App /&gt;
      &lt;/QueryClientProvider&gt;
    ...
  &lt;/React.StrictMode&gt;
)</code></pre><h4 id="v5-이전">V5 이전</h4>
<ul>
<li>이전에는 <code>defaultOptions</code>로 에러를 제어했는데, v5로 업데이트된 이후에는 <code>onError</code>와 같은 리스너 함수가 기본 옵션에 직접 설정할 수 없도록 제한됨.</li>
<li>즉, <code>onError</code>, <code>onSuccess</code>, <code>onSettled</code> 등이 기본 옵션으로 허용 안함.</li>
</ul>
<h4 id="v5-이후">v5 이후</h4>
<ul>
<li><p><code>QueryCache</code>로 관리하는게 공식적인 방법</p>
</li>
<li><p><code>queryCache</code>: 전역 쿼리 에러</p>
<ul>
<li>읽기 쿼리(<code>fetch</code>)에 적용</li>
<li><code>useQuery</code>, <code>useInfiniteQuery</code></li>
</ul>
</li>
<li><p><code>mutationCache</code>: 전역 뮤테이션 에러</p>
<ul>
<li>쓰기 작업(<code>post</code>, <code>put</code>, <code>delete</code>등)에 적용</li>
<li><code>useMutation</code></li>
</ul>
</li>
</ul>
<hr />

<h2 id="핸들러-유틸-함수">핸들러 유틸 함수</h2>
<pre><code>import axios from &quot;axios&quot;;
import { toast } from &quot;react-toastify&quot;;

const statusHandlers: {
  [key: number]: (msg: string, status: number | null) =&gt; void;
  default: (msg: string) =&gt; void;
} = {
  400: (msg: string) =&gt;
    toast.error(&quot;잘못된 요청입니다. 다시 시도해 주세요.&quot;, {
      toastId: &quot;fetch-400-error&quot;,
    }),
  401: (msg: string, status) =&gt; {
    toast.error(&quot;로그인 정보가 유효하지 않거나 세션이 만료되었습니다.&quot;, {
      toastId: &quot;fetch-401-error&quot;,
    });
  },
  ...,

  500: () =&gt;
    toast.error(&quot;서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.&quot;, {
      toastId: &quot;fetch-500-error&quot;,
    }),
  default: () =&gt;
    toast.error(&quot;예기치 못한 오류가 발생했습니다. 다시 시도해 주세요.&quot;, {
      toastId: &quot;fetch-unknown-error&quot;,
    }),
};

interface ErrorResponse {
  message: string;
  status?: number;
}
export const handleApiError = (error: unknown) =&gt; {
  if (axios.isAxiosError(error)) {
    if (error.response) {
      const httpStatus = error.response?.status;
      const errorResponse = error.response?.data?.error as ErrorResponse;
      const httpMessage = errorResponse.message;
      const httpErrorCode = errorResponse?.status || null;

      const handle =
        httpStatus !== undefined &amp;&amp; statusHandlers[httpStatus] ? statusHandlers[httpStatus] : statusHandlers.default;
      handle(httpMessage, httpErrorCode);
    } else {
      toast.error(&quot;서버 연결이 원활하지 않습니다.&quot;);
      toast.clearWaitingQueue();
      return;
    }
  } else {
    toast.error(&quot;네트워크 연결 오류 또는 기타 오류가 발생했습니다.&quot;);
    toast.clearWaitingQueue();
    return;
  }
};</code></pre><blockquote>
<p>이 유틸 함수는 이 <a href="https://velog.io/@ebokyung/TanStack-Query-V5%EC%97%90%EC%84%9C-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81%ED%95%98%EA%B8%B0#-queryclient%EC%9D%98-defaultoptions">블로그</a>에서 도움을 받은 글임.</p>
</blockquote>
<ul>
<li>API status는 현재 내가 사용하고 있는 API 응답값을 기반으로 만들었음.</li>
</ul>
<hr />

<h2 id="적용하기">적용하기</h2>
<p>모든 API는 <code>try ~ catch(error) { throw new Error(error) }</code>문으로 에러를 잡아냈었음.</p>
<p>전역 쿼리로 에러를 핸들러 하려면, <code>catch(error) { throw error }</code> 로 해야 잡힘.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useCallback 사용하기]]></title>
            <link>https://velog.io/@well_log/useCallback-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@well_log/useCallback-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 23 Jun 2025 13:08:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="usecallback">useCallback</h3>
</blockquote>
<pre><code>const memoizedFn = useCallback(() =&gt; {
  // some logic
}, [deps]);</code></pre><ul>
<li>React 16.8에서 도입된 기본 Hook</li>
<li>함수 자체를 메모이제이션해서, 동일한 함수를 재사용하도록 하는 Hook</li>
<li>재렌더링 시 불필요한 함수가 재생성되는 것을 방지함</li>
<li>그로인해, 성능 최적화에 도움을 줌</li>
</ul>
<hr />

<h2 id="동일한-함수-재사용">동일한 함수 재사용?</h2>
<p>동일한 함수라고 하면 내가 선언한 함수라고 생각할 수 있음.
뭐 이것도 어떻게 보면 맞는 말이긴 함.
그런데 여기서 말하고자 하는 바는, <code>참조값</code>임.</p>
<p>자바스크립트 함수는 객체로 취급되기 때문에, 변수에 값(value)가 아니라 메모리 주소(참조값)를 저장함.</p>
<p>그래서 <code>useCallback</code> 없이 함수를 불러올 때, 눈에 보이는 건 동일한 함수를 불러오는 것 같지만? <strong>까보면 참조값이 다르기 때문에 React에서는 다른 함수로 간주함.</strong></p>
<p>그래서 리렌더링 할 때마다 새로운 함수가 다시 정의되면서(새로운 참조값), React는 props가 변경된 것으로 판단해 의도치 않은 리렌더링이 발생하는거.</p>
<hr />

<h2 id="그래서-이런걸-방지하고자">그래서 이런걸 방지하고자</h2>
<p><code>useCallback</code>을 사용함.</p>
<p><code>useCallback</code>은 함수의 참조값을 고정해줘서 리렌더링이 되어도 <strong>의존성 배열</strong>이 바뀌지 않는 한 같은 함수를 유지함 </p>
<hr />

<h2 id="그럼-이걸-언제-사용">그럼 이걸 언제 사용?</h2>
<h3 id="1-자식-컴포넌트에-props로-함수를-전달하는-경우">1) 자식 컴포넌트에 props로 함수를 전달하는 경우</h3>
<p>고유 함수를 생성하고 이걸 부모를 통해 자식에게 전달되면, props를 통해 함수를 전달받은 자식 컴포넌트는 props가 변경되었다고 인식해서 리렌더링을 발생시킴.</p>
<p>이런걸 방지하고자 <code>useCallback</code>으로 함수를 만들어 자식 컴포넌트에게 전달하고,
자식컴포넌트는 함수 재사용하는 것으로 인식해 리렌더링을 방지</p>
<pre><code>const Parent = () =&gt; {
  const handleClick = () =&gt; {
    console.log(&quot;Clicked&quot;);
  };

  return &lt;Child onClick={handleClick} /&gt;;
};</code></pre><ul>
<li>이렇게 하면, <code>Parent</code>가 리렌더링 될 때마다 <code>handleClick</code>도 새로 만들어지고, <code>Child</code>는 매번 리렌더링 됨.</li>
</ul>
<pre><code>const handleClick = useCallback(() =&gt; {
  console.log(&quot;Clicked&quot;);
}, []);</code></pre><p>그래서 이렇게 <code>useCallback</code>으로 리렌더링을 방지</p>
<h3 id="2-useeffect-usememo-useref-등-deps에-함수가-들어가는-경우">2) useEffect, useMemo, useRef 등 deps에 함수가 들어가는 경우</h3>
<p>무슨 말이냐면, 각 <code>Hooks</code>의 의존성 배열에 함수가 들어가는 경우를 말함.</p>
<pre><code>function App() {
  const [count, setCount] = useState(0);

  const doSomething = () =&gt; {
    console.log(&quot;doing something&quot;);
  };

  useEffect(() =&gt; {
    doSomething(); // 매번 실행됨
  }, [doSomething]); // 함수가 의존성 배열에 들어감
}</code></pre><p>예를 들어 이런 상황이다?</p>
<ul>
<li><code>doSomething</code> 함수는 컴포넌트가 리렌더링 될 때마다 새로 생성됨</li>
<li>그래서 <code>useEffect</code>는 <code>doSomething</code>의 참조값이 매번 달라진다고 판단</li>
<li>결론 : <code>useEffect</code>는 의미 없이 매번 실행됨</li>
</ul>
<p>이러한 불필요 리렌더링 방지하기 위해 <code>useCallback</code>을 사용한다면?</p>
<pre><code>const doSomething = useCallback(() =&gt; {
  console.log(&quot;doing something&quot;);
}, []);</code></pre><ul>
<li><code>doSomething</code>은 리렌더링돼도 같은 참조값 유지</li>
<li>의존성 배열에 넣어도 <code>useEffect</code>가 실행되지 않음</li>
</ul>
<blockquote>
<p><em>이건 <code>useMemo</code>나 <code>useRef</code>도 마찬가지임.</em></p>
</blockquote>
<ul>
<li>useMemo(() =&gt; doHeavyThing(fn), [fn])</li>
<li>useRef(fn)에서 fn이 리렌더링마다 바뀌면 ref.current도 바뀜</li>
</ul>
<h3 id="3-외부-의존성인-경우외부에서-값을-가져오는-api-호출">3) 외부 의존성인 경우(외부에서 값을 가져오는 api 호출)</h3>
<pre><code>const [userId, setUserId] = useState(&quot;123&quot;);

const fetchUserData = () =&gt; {
     const res = await axios.get(`/api/users/${userId}`);
}

useEffect(() =&gt; {
    ...
}, [fetchUserData])</code></pre><p>이렇게 되면,</p>
<ol>
<li><code>fetchUserData</code>에 새로운 함수(참조값) 할당</li>
<li><code>useEffect</code> 함수가 호출</li>
<li>의존성 배열에 의해 새로운 <code>fetchUserData</code> 계속 생성</li>
<li><strong>무한루프</strong> 발생</li>
</ol>
<p>그래서 이런걸 방지하고자</p>
<pre><code>const [userId, setUserId] = useState(&quot;123&quot;);

const fetchUserData = useCallback(async () =&gt; {
  const res = await axios.get(`/api/users/${userId}`);
  console.log(res.data);
}, [userId]);

useEffect(() =&gt; {
    ...
}, [fetchUserData])</code></pre><ul>
<li><code>useCallback</code>에서 <code>userId</code>가 변경될 때만 새로운 <code>fetchUserData</code>가 할당되고</li>
<li>새로운 <code>fetchUserData</code>가 할당될 때 <code>useEffect</code> 실행되게 해서 무한루프 방지</li>
</ul>
<hr />

<h2 id="그럼-이거-사용하면-무조건-성능-좋아짐">그럼 이거 사용하면 무조건 성능 좋아짐?</h2>
<p><strong>그렇지는 않음</strong>. 모든 함수를 <code>useCallback</code>으로 감싸면 오히려 코드 복잡도만 높이고 성능 이점은 거의 없음</p>
<p>그래서 <strong>꼭 필요한 경우에만</strong> 사용!</p>
<hr />

<h2 id="번외--tanstack-query와는">번외 : Tanstack Query와는?</h2>
<blockquote>
<p>✅ 같이 써도는 됨. 근데 이것도 필요한 경우에만!</p>
</blockquote>
<p><strong>일반적으로는 굳이 <code>useCallback</code>을 같이 쓸 필요는 없음</strong></p>
<p>왜냐면,</p>
<ol>
<li><code>queryFn</code>은 대부분 컴포넌트 밖에서 정의함<ul>
<li>일반적으로 <code>useQuery</code>의 <code>queryFn</code>은 외부에서 한 번 정의하고 그대로 사용함
→ 렌더링마다 함수가 새로 만들어지는 문제가 애초에 없음</li>
</ul>
</li>
<li>React Query는 함수 참조가 바뀌어도 무조건 refetch하지 않음<ul>
<li><code>queryKey</code>가 바뀌지 않는 한, 함수 참조값이 바뀌었다고 해서 자동 refetch 안 함</li>
<li>내부적으로 캐시와 <code>queryKey</code>를 기준으로 동작하기 때문에</li>
</ul>
</li>
<li>리렌더링을 유발하지 않음<ul>
<li><code>useQuery</code>, <code>useMutation</code> 등은 내부 상태를 자체적으로 관리해서</li>
<li><code>queryFn</code>이 새로 만들어져도 React의 일반적인 props처럼 리렌더링 트리거로 작용하지 않음</li>
</ul>
</li>
</ol>
<blockquote>
<h3 id="✅-usecallback을-굳이-함께-쓰지-않아도-되는-이유">✅ <code>useCallback</code>을 굳이 함께 쓰지 않아도 되는 이유</h3>
<p><code>TanStack Query</code>에서는 <code>queryFn</code>을 컴포넌트 외부에서 정의하거나,
함수가 새로 생성되어도 내부 캐시 전략으로 인해 불필요한 refetch나 리렌더링이 발생하지 않기 때문에, 일반적으로 useCallback을 굳이 함께 쓸 필요는 없다.</p>
</blockquote>
<h3 id="그럼에도-같이-써야하는-경우는">그럼에도 같이 써야하는 경우는?</h3>
<ol>
<li><p><code>queryFn</code>이 외부 의존성을 가지고, 컴포넌트 내부에서 정의 될 때</p>
<pre><code>const fetchData = useCallback(() =&gt; {
 return axios.get(`/api/data?filter=${filter}`);
}, [filter]);

const { data } = useQuery([&#39;data&#39;, filter], fetchData);</code></pre><ul>
<li><code>fetchData</code>가 매번 바뀌면 <code>queryKey</code>는 같아도 <code>queryFn</code>이 달라져서 <code>useQuery</code>가 새로 실행될 수 있음
→ <code>useCallback</code>으로 <code>fetchData</code>를 정의하는게 좋음</li>
</ul>
</li>
<li><p><code>useEffect</code> 등에 deps로 넣고 쓰는 경우,
자식 컴포넌트에 <code>refetch</code> 같은 함수를 props로 넘겨야하는 경우</p>
<pre><code>const { refetch } = useQuery(...);

const handleRefresh = useCallback(() =&gt; {
 refetch();
}, [refetch]);

&lt;Child onRefresh={handleRefresh} /&gt;</code></pre></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[#3 폴더 구조 변경]]></title>
            <link>https://velog.io/@well_log/3-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@well_log/3-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Sun, 22 Jun 2025 17:00:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/well_log/post/2a893b59-59ed-4638-8450-c235eb64bea6/image.png" alt=""></p>
<p>이번 프로젝트에서는 폴더 구조 변경을 진짜 많이 했었음.</p>
<p>처음에는 기존에 사용하던 폴더 구조 방식으로 만들었음.</p>
<pre><code>// 기존 폴더 구조

src/
 ├── components/ # 재사용 컴포넌트
 │        ├── global/ # 공통 컴포넌트
 │        │        ├── ErrorMessage.tsx
 │        │        ├── LoadingSpinner.tsx
 │        │        ├── LoginButton.tsx
 │        │        ├── PlayButton.tsx
 │        │        └── 등...
 │        │
 │        ├── home/ # homepage에서 사용되는 컴포넌트
 │        ├── playlist/ # playlist에서 사용되는 컴포넌트
 │        ├── search/ # search에서 사용되는 컴포넌트
 │        └── side-bar/ # side-bar에서 사용되는 컴포넌트
 │
 ├── hooks/ # 커스텀 훅
 │
 ├── layout/ # 레이아웃 컴포넌트
 │        ├── homepage/ # home에서 사용되는 레이아웃
 │        ├── playlist/ # playlist에서 사용되는 레이아웃
 │        ├── searchpage/ # search에서 사용되는 레이아웃
 │        ├── AppLayout.tsx # AppLayout
 │        └── 등...
 │
 ├── models/ # 타입 정의 폴더
 │
 ├── pages/ # 페이지 컴포넌트(라우팅되는 각 페이지들)
 │
 ├── ...
 │
 ├── ...</code></pre><p>그런데 페이지가 많아지고, 컴포넌트들을 계속 만들게 되면서 이 폴더 구조를 좀 더 명확히 해야겠다(?)라는 생각을 가졌음.</p>
<p>이 생각을 가지게 된 이유는 바로 <code>components</code> 폴더 구조 때문임.
기존에는 라우팅을 기준으로 나누었는데, 이게 <code>components</code>에 대한 명확한 역할이 사라진 것 같다는 느낌이 들었음.</p>
<p>왜냐면 <code>global</code>한 컴포넌트와 각 <code>page</code>에서만 사용되는 컴포넌트가 동일한 <code>components</code> 폴더에 있어, 각 <code>page</code>에 사용되는 컴포넌트들도 모든 페이지에 재사용이 가능한 컴포넌트다라는 인식을 줄 수 있을 것 같았기 때문임.</p>
<p>그래서, 모든 페이지에 재사용이 가능한 컴포넌트들만 두기 위해 폴더 구조 변경에 들어갔음.</p>
<h2 id="상위-components-폴더-구조-변경">상위 components 폴더 구조 변경</h2>
<h3 id="1-global-→-common">1. global/ → common/</h3>
<p>모든 페이지 재사용 가능한 컴포넌트들이다라는 의미를 주기 위해 폴더명 이름을 변경했음.
그리고 그 폴더 안에는 <code>components</code>라는 폴더를 만들어 그 안에 재사용이 가능한 컴포넌트들을 두었음.
<code>components</code> 폴더를 생성한 이유는, 이 <code>common</code>이라는 폴더 안에 <code>ContextAPI</code> 컴포넌트도 있었기 때문에 이것을 구분하기 위해 <code>components</code> 폴더를 생성함.
그리고 그 후에는 공통으로 사용되는 <code>style</code> 폴더도 생성함.</p>
<pre><code>src/
 ├── common/ # 재사용 컴포넌트
 │        ├── components/ # 공통 컴포넌트
 │        │        ├── ErrorMessage.tsx
 │        │        ├── LoadingSpinner.tsx
 │        │        ├── LoginButton.tsx
 │        │        ├── PlayButton.tsx
 │        │        └── 등...
 │        │
 │        ├── style/ # 공통으로 사용되는 style
 │        │        ├── ListContainer.tsx
 │        │        └── SearchStyle.tsx
 │        │
 │        └── ContextProvider.tsx
 │</code></pre><h3 id="2-side-bar">2. side-bar/</h3>
<p><code>side-bar</code> 관련 컴포넌트들은 원래 <code>src/components/side-bar</code> 폴더에 있었음.(<code>Side-bar</code> 레이아웃 제외)
그리고 이 관련 컴포넌트들은 <code>src/common/components/side-bar/</code> 폴더를 생성해 그 안에 관련 컴포넌트들을 두었음.
하지만, 다른 <code>common</code> 컴포넌트들과는 다르게 별도의 <code>side-bar</code> 폴더를 생성하여 구분을 지었음.
그리고 그 <code>side-bar/</code> 안에는 <code>library-body/</code> 폴더를 생성해, <code>side-bar</code>에서 사용되는 컴포넌트를 두었음.</p>
<pre><code>src/
 ├── common/ # 재사용 컴포넌트
 │        ├── components/ # 공통 컴포넌트
 │        │        ├── ErrorMessage.tsx
 │        │        ├── LoadingSpinner.tsx
 │        │        ├── LoginButton.tsx
 │        │        ├── PlayButton.tsx
 │        │        ├── side-bar/
 │        │        │        ├── library-body/
 │        │        │        ├── Library.tsx
 │        │        │        └── LibraryHead.tsx
 │        │        └── 등...
 │        │
 │        ├── style/ # 공통으로 사용되는 style
 │        │        ├── ListContainer.tsx
 │        │        └── SearchStyle.tsx
 │        │
 │        └── ContextProvider.tsx
 │</code></pre><blockquote>
<p><strong>추가</strong>
근데 이 포스팅 쓰면서 폴더 구조를 <code>common</code>에서 <code>layout</code>으로 옮김.
아무리 생각해도 <ins>현재 프로젝트</ins>에서는 재사용 컴포넌트는 아니어서...</p>
</blockquote>
<pre><code>src/
 ├── common/ # 재사용 컴포넌트
 │        ├── components/ # 공통 컴포넌트
 │        │        ├── ErrorMessage.tsx
 │        │        ├── LoadingSpinner.tsx
 │        │        ├── LoginButton.tsx
 │        │        ├── PlayButton.tsx
 │        │        └── 등...
 │        │
 │        ├── style/ # 공통으로 사용되는 style
 │        │        ├── ListContainer.tsx
 │        │        └── SearchStyle.tsx
 │        │
 │        └── ContextProvider.tsx
 │
 ├── ...
 ├── ...
 ├── layout
 │        ├── side-bar
 │        │        ├── library-body/
 │        │        ├── Library.tsx
 │        │        └── LibraryHead.tsx
 │        │
 │        ├── AppLayout.tsx
 │        ├── Side-bar.tsx
 │        ├── 등...</code></pre><blockquote>
<blockquote>
<p><strong>참고</strong> <code>common/style/</code>도 별도 <code>src/style</code>로 폴더 분리함. </p>
</blockquote>
</blockquote>
<h3 id="3-상위-components의-pages-컴포넌트-폴더-위치-변경">3. 상위 components의 pages 컴포넌트 폴더 위치 변경</h3>
<p>폴더 변경의 가장 큰 이유였던, 상위 컴포넌트 폴더에 있던 각 <code>pages</code>의 컴포넌트들의 폴더 위치를 변경함.</p>
<p>일단, 위에서도 썼다시피, 상위 컴포넌트에 있어 모든 페이지에서 재사용이 가능한 컴포넌트라고 오해를 불러 일으킬 수 있어서 각 해당<code>pages</code>에 컴포넌트 폴더를 생성해서 위치를 이동시켰다.
이렇게 하니, 확실히 컴포넌트 관리도 쉬워지고 <code>common</code>이라는 폴더의 역할이 명확해져서 앞으로는 이 폴더 구조 형태를 유지할 것 같음.</p>
<pre><code>src/
 ├── common/ # 재사용 컴포넌트
 ├── layout/
 ├── ...
 ├── ...
 ├── pages
 │        ├── homePage/
 │        │        ├── components/
 │        │        └── HomePage.tsx
 │        │
 │        ├── playlistPage/
 │        │
 │        ├── searchPage/</code></pre><h2 id="마무리">마무리</h2>
<p>폴더 구조 변경은 일단 이렇게 마무리가 되었음.</p>
<p>그리고 이 작업을 하면서 기존에 만들었던 컴포넌트 파일 위치 변경도 많이 이루어졌고, <code>layout</code>도 많이 만들었음.
확실히 컴포넌트들이 많아지니, 폴더 구조 변경 리팩토링은 내가 기존에 했던 폴더 구조 방식에서는 필수적으로 해야했던 부분이었던 것 같음.
이 작업을 하면서 좀 더 효율적인 구조 방식과 폴더마다의 명확한 역할에 대해 좀 더 알게 되었음.</p>
<p>다음 프로젝트에서는 이번 폴더 리팩토링 기억을 되짚어보며 의미가 명확한 구조를 짤거같음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[#2 홈 > 섹션별 데이터 패칭]]></title>
            <link>https://velog.io/@well_log/2-%ED%99%88-%EC%84%B9%EC%85%98%EB%B3%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD</link>
            <guid>https://velog.io/@well_log/2-%ED%99%88-%EC%84%B9%EC%85%98%EB%B3%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%A8%EC%B9%AD</guid>
            <pubDate>Sun, 22 Jun 2025 12:25:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/well_log/post/bed4d751-0281-44a4-bb88-24618a47be22/image.png" alt=""></p>
<h2 id="드디어-홈-데이터-패칭">드디어 홈 데이터 패칭</h2>
<p>한동안 홈 화면은 휑~했었음
새 앨범 발매 섹션만 있었고, 나머지는 없었음.</p>
<p>왜냐, 이 때 플레이리스트 부분의 작업을 하고 있었기 때문.</p>
<p>그래서 <code>npm start</code>하고 화면이 켜졌을 때, 내가 계속 작업은 하고 있지만 뭔가 비었다(?), 볼게 없다(?)라고 느꼈었음.
그때마다 재빠르게 로그인하고 플레이리스트로 이동을 했지만ㅋ
계속 이렇게 둘 수는 없다 싶어서 드디어 <code>홈</code> 작업에 돌입!</p>
<h2 id="섹션에-대한-api를-제공하지-않는다고">섹션에 대한 API를 제공하지 않는다고?</h2>
<p>드디어 홈 작업한다고 어떤 데이터들을 보일까 타이틀을 정했었음.</p>
<ol>
<li>새 앨범 발매(Release Album) → 이건 이미 했으니까</li>
<li>인기 아티스트(Top Artists)</li>
<li>인기 앨범(Popular Albums)</li>
<li>추천 차트(Recommended / Charts)</li>
</ol>
<p>그래서 API Document에서 해당 API들을 제공해주는지 찾아봤는데 제공해주지 않았음.
근데 <code>아니, 이건 좀 기본적으로 제공해주는 API이지않나?</code>, <code>이걸 제공안한다고?</code>라는 생각으로 ChatGPT한테 물어봄.</p>
<p>?? 근데 알려주는거임.
그래서 <code>그래, 이게 없을리가 없지</code>라고 생각하고 api를 만들었음.</p>
<p>그리고 해당 API들은 Token을 꼭 필요로 했기 때문에, <code>clientCredentialToken</code>을 사용함.
홈 화면에서 로그인을 하지 않아도 데이터를 보여야하니까</p>
<blockquote>
<p><strong><code>clientCredentialToken</code></strong>
로그인을 하지 않고, <code>Spotify</code>의 <code>CLIENT_ID</code>와 <code>CLIENT_SECRET</code>을 기반으로 만든 토큰임.</p>
</blockquote>
<p>만들고 호출했는데
???? 왜 안줘?? 왜 404??? 왜 <code>Resource not found</code>???</p>
<p>일단, 나는 여기서 ChatGPT가 준 api 자체는 문제가 없다고 생각했음.
그럼 Token이 제대로 안들어갔나? Network 탭 바로 들어가서 Token 상태 확인함.
<code>undefined</code>인가?? <strong>NO!!</strong> 제대로 들어감.</p>
<p>하지만 결과는 계속 <code>404 Resource not found</code>
그럼 <code>clientCredentialToken</code>으로 안되는건가? 싶어서 로그인한 Token으로 테스트를 했는데도 계속 <code>404</code></p>
<p>아니, gpt가 이 api 맞다고 했는데???
그래서 <code>curl</code>로 직접 테스트를 진행해봄.</p>
<p>??? 근데 여기서도 안나오는거임.
api 문제라는 생각은 또 안하고, 토큰 때문인가 싶어서 이미 만들어 둔 <code>새 앨범 발매</code> api를 <code>curl</code>로 테스트해봄.
정상적으로 호출되고 결과도 나옴.</p>
<p>아 이거 api 문제 있구나, 그래서 gpt한테 물어보니 지역 제한 때문에 발생할 수도 있다고 해서 쿼리 스트링으로 지역 설정까지 해줘봤음.(<code>?market=US</code>) → 안됨.</p>
<blockquote>
<p>그래서 이 문제의 전체적인 맥락을 분석하면,</p>
<ol>
<li>일단 공식 API로 제공하지 않음</li>
<li>인기 차트 관련(트랙/아티스트/앨범 순위 등)은 Spotify에서 만든 내부 차트로 API 제공하지 않음</li>
<li>일부 가능한 API 등도 기능 제한이 있거나, 접근 제한(지역, 토큰 등)이 있음</li>
</ol>
<h4 id="그래서-차트-관련-api를-사용하지-못했다">그래서? 차트 관련 API를 사용하지 못했다.</h4>
</blockquote>
<h2 id="결론--섹션-변경">결론 : 섹션 변경</h2>
<p>저러한 문제들 때문에, 결론적으로 섹션을 변경하기로 했음.</p>
<ol>
<li>새 앨범 발매(Release Album) → 이건 이미 했으니까</li>
<li>아티스트(Artists)</li>
<li>앨범(Albums)</li>
<li>플레이리스트(Playlists)</li>
<li>카테고리(Categories)</li>
</ol>
<p>일단, 이 섹션들에 대해서는 공식 API들은 제공을 했음.
하지만 문제가 뭐냐, 카테고리 제외 나머지 API들은 특정 <code>id</code>가 필요했음.
즉, <strong>아티스트가 랜덤으로 뽑히는게 아니라 특정 아티스트의 id가 필요</strong>로 했음.</p>
<h3 id="어떻게-해결-특정-키워드로-검색하기">어떻게 해결? 특정 키워드로 검색하기</h3>
<p>카테고리는 여러 카테고리를 제공해주는 공식 API가 있기 때문에 카테고리 제외한 나머지 섹션들은 <code>검색</code> API를 사용해 작업을 진행했음.</p>
<p>검색 API는 특정 키워트를 기준으로 결과를 제공해주기 때문에, <code>{q: &#39;k-pop&#39;, limit: 10, type: &#39;섹션별&#39;}</code>을 기준으로 결과를 출력했고</p>
<pre><code> const { data, isLoading } = useSearchCategory({
    q: &quot;k-pop&quot;,
    type: [SEARCH_TYPE.Album],
    limit: 10,
  });</code></pre><p>카테고리는 여러 카테고리를 제공해주는 공식 API가 있어 해당 API를 사용함.</p>
<pre><code>const { data, isLoading } = useGetServeralCategories({ limit: 10, offset: 0 });</code></pre><p>두 API 모두 Token을 필요로 하기에 로그인 없이도 API 호출이 가능한 <code>clientCredentialToken</code>을 사용했음.</p>
<h3 id="스타일-지정">스타일 지정</h3>
<p>결과는 모두 정상적으로 출력됨.
그리고 10개씩 limit을 해두었기 때문에 가로 스크롤을 적용하는 것으로 일단 마무리함.</p>
<h2 id="홈-화면-데이터-패칭-끝">홈 화면 데이터 패칭 끝!</h2>
<p><img src="https://velog.velcdn.com/images/well_log/post/27427c1c-b113-4941-a4ba-019ee3ec85a5/image.png" alt=""></p>
<h2 id="홈-화면-남은-작업">홈 화면 남은 작업</h2>
<p>일단, 홈 화면을 채워서 휑~한 화면은 없지만, 각 이미지를 클릭했을 때의 라우터와 해당 페이지를 만들어야 함.
이 작업은 <code>검색</code> 페이지 완료하고 만들까 생각 중임.
왜냐면, <code>검색</code> 페이지에도 해당 페이지로 이동하는 기능이 필요하기 때문.</p>
<h2 id="작업-후기">작업 후기</h2>
<p>이번 홈 화면을 작업하면서 공식적으로 제공하지 않는 API는 다 이유가 있기 때문에 제공하지 않음을 알게됨.
하지만, <code>curl</code>을 통해서 직접 호출하고 했던 경험은 재밌었음.
전에 회사에서도 해봤지만, 다시 해보니 재밌었고 api 응답 확인할 때 웹 사이트에서 Network 체크 뿐만 아니라, 직접 호출하는 방식으로 확인해보는 것도 괜찮다고 생각함.
그래서 나중에는 이런 방식으로도 체크를 많이 해볼 것 같음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 위치 고정 반응형 적용하기]]></title>
            <link>https://velog.io/@well_log/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9C%84%EC%B9%98-%EA%B3%A0%EC%A0%95-%EB%B0%98%EC%9D%91%ED%98%95-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@well_log/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9C%84%EC%B9%98-%EA%B3%A0%EC%A0%95-%EB%B0%98%EC%9D%91%ED%98%95-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 21 Jun 2025 04:58:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/well_log/post/ee084570-abca-44ca-a1d6-66347936b511/image.gif" alt=""></p>
<pre><code> return (
    &lt;Container className=&quot;container&quot;&gt;
      {data &amp;&amp; data?.pages[0]?.categories?.items.length &gt; 0 ? (
        &lt;Grid container spacing={2}&gt;
          {data?.pages[0]?.categories?.items.map((category: any, idx: number) =&gt; (
            &lt;Content key={category.id} size={{ xs: 12, sm: 6, md: 4 }}&gt;
              &lt;Contentbox&gt;
                &lt;CategoryName variant=&quot;h1&quot;&gt;{category.name}&lt;/CategoryName&gt;
                &lt;CategoryImg src={category.icons[0].url} /&gt;
              &lt;/Contentbox&gt;
            &lt;/Content&gt;
          ))}
        &lt;/Grid&gt;
      ) : (
        &lt;Typography variant=&quot;h2&quot;&gt;No data&lt;/Typography&gt;
      )}
    &lt;/Container&gt;
  );

const Container = styled(&quot;div&quot;)({});
const Content = styled(Grid)({});
const Contentbox = styled(Box)({
  position: &quot;relative&quot;,
  backgroundColor: &quot;#D3ECCD&quot;,
  borderRadius: &quot;10px&quot;,
  overflow: &quot;hidden&quot;,
  aspectRatio: &quot;2 / 1&quot;,
});
const CategoryName = styled(Typography)({
  padding: &quot;1rem&quot;,
});
const CategoryImg = styled(&quot;img&quot;)({
  position: &quot;absolute&quot;,
  width: &quot;45%&quot;,
  bottom: &quot;-10%&quot;,
  right: &quot;-8%&quot;,
  transform: `rotate(25deg)`,
});</code></pre><ul>
<li><code>ContentBox: { position: &quot;relative&quot; }</code>, <code>CategoryImg: { position: &quot;absolute&quot; }</code>
<code>ContentBox</code> 안 이미지 위치 조정 위해 사용</li>
<li><code>ContentBox: { aspectRatio: &quot;2 / 1&quot; }</code>
<code>ContentBox</code> 반응형 비율 유지</li>
<li><code>CategoryImg: { width: &quot;45%&quot; }</code>
이미지 너비를 부모 요소 너비에 대해 45% 차지</li>
<li><code>CategoryImg: { bottom: &quot;-10%&quot;, right: &quot;-8%&quot; }</code>
부모 <code>ContentBox</code>의 높이/너비에 비례해 위치 설정 → 화면이 작아지면서 위치도 함께 비율로 줄어듬
→ 부모 높이의 10%만큼 아래로(밖으로) 내림, 부모 너비의 8%만큼 오른쪽으로(밖으로) 밀어냄</li>
</ul>
<blockquote>
<p><strong><code>CategoryImg: { bottom: &quot;-10%&quot; }</code></strong>
→ 이거 안해주면 이미지 반응형 안됨, 그리고 <code>px</code>로 고정하면 절대 위치이기 때문에 반응형 안됨
→ 결론 : 화면이 작아져도 위치는 그대로고, 이미지가 작아지면서 부모 영역 밖으로 밀려나거나 잘림
<img src="https://velog.velcdn.com/images/well_log/post/1baef880-ab46-4673-b720-d115c06f0553/image.gif" alt=""></p>
</blockquote>
<hr />

<blockquote>
<p><strong><code>aspectRatio</code> 비율</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>비율</th>
<th>의미</th>
<th>형태</th>
</tr>
</thead>
<tbody><tr>
<td><code>&quot;1 / 1&quot;</code></td>
<td>가로:세로 = 1:1</td>
<td>정사각형</td>
</tr>
<tr>
<td><code>&quot;4 / 3&quot;</code></td>
<td>가로:세로 = 4:3</td>
<td>가로가 약간 더 긴 직사각형</td>
</tr>
<tr>
<td><code>&quot;16 / 9&quot;</code></td>
<td>가로:세로 = 16:9</td>
<td>와이드</td>
</tr>
<tr>
<td><code>&quot;2 / 1&quot;</code></td>
<td>가로:세로 = 2:1</td>
<td>더 넓고 얇음 (세로 더 줄임)</td>
</tr>
<tr>
<td><code>&quot;1 / 0.5&quot;</code></td>
<td>가로:세로 = 1:0.5</td>
<td>세로가 가로의 절반 (짧음)</td>
</tr>
</tbody></table>
<hr />

<blockquote>
<p><strong>퍼센트 위치 계산</strong></p>
</blockquote>
<pre><code>aspectRatio: &quot;2 / 1&quot; // 예를 들어 width: 400px, height: 200px</code></pre><ul>
<li><code>bottom: -10%</code> → <code>200px * 0.10 = 20px 아래로</code></li>
<li><code>right: -8%</code> → <code>400px * 0.08 = 32px 오른쪽으로</code></li>
<li><code>width: 45%</code> → <code>400px * 0.45 = 180px</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[mui - Grid]]></title>
            <link>https://velog.io/@well_log/mui-Grid</link>
            <guid>https://velog.io/@well_log/mui-Grid</guid>
            <pubDate>Sat, 21 Jun 2025 04:08:54 GMT</pubDate>
            <description><![CDATA[<h3 id="작동-원리">작동 원리</h3>
<ul>
<li>그리드는 항상 플렉스 아이템 → 플렉스 컨테이너 추가하려면 <code>container</code> props를 추가해야함</li>
<li>기본적으로 12개의 컬럼으로 구성됨</li>
<li>기본 그리드 중단점은 xs, sm, md, lg, xl<pre><code>// 중단점 예시
&lt;Grid size={{ xs: 6, sm: 4, md: 2 }}/&gt;
</code></pre></li>
</ul>
<p>xs:6 → 6/12 → 화면이 작을 때 2개씩 한 줄에
sm:4 → 4/12 → 화면이 조금 넓을 때 3개씩 한 줄
md:2 → 2/12 → 화면이 더 넓을 때 6개씩 한 줄</p>
<pre><code>


### 기본 그리드
- 그리드 레이아웃 그리려면 일단, `container` prop을 사용해서 그리드 항목을 감싸야함.</code></pre><Grid container spacing={2}>
  <Grid size={8}>
    <Item>size=8</Item>
  </Grid>
  <Grid size={4}>
    <Item>size=4</Item>
  </Grid>
  <Grid size={4}>
    <Item>size=4</Item>
  </Grid>
  <Grid size={8}>
    <Item>size=8</Item>
  </Grid>
</Grid>
```]]></description>
        </item>
        <item>
            <title><![CDATA[#1 프로젝트 시작]]></title>
            <link>https://velog.io/@well_log/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91-1</link>
            <guid>https://velog.io/@well_log/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91-1</guid>
            <pubDate>Wed, 18 Jun 2025 12:06:01 GMT</pubDate>
            <description><![CDATA[<p>처음에 이 프로젝트를 시작하게 된 계기는 타입스크립트와 Tanstack Query가 너무 부족하다고 생각해서였음.</p>
<p>이전에 만들었던 TypeScript 프로젝트나 Tanstack Query는 어떻게 보면 
내가 원하는 값, 구조로 만들었기 때문에 실제 현업에 가서 이 스택들을 빠르게 적응하기 어려울 것 같았음.</p>
<p>그래서 실제로 사용되는 OPEN API를 가져와서 이 스택들을 적용해봤는데 내가 너무 간단한 구조들만 지금까지 만들었었구나라고 생각하게 됨.</p>
<hr />

<h2 id="typescript">TypeScript</h2>
<p>일단 TypeScript는 <strong>to.do.focus</strong>에서만 사용해봤고, 
그 프로젝트는 api 연동이 필요가 없었고, TypeScript의 고급 기능 등을 사용해보지 않았었음.
또한, 프로젝트 자체가 내가 원하는 데이터 형식을 만드는 것이었기 때문에 실상 모델이 복잡하지도 않았었음.</p>
<p>그리고나서 이번 프로젝트를 진행하며</p>
<ol>
<li><p>타입을 만드는게 정말 쉽지 않구나</p>
</li>
<li><p>모델을 정말 많이 만들어야하는구나</p>
</li>
<li><p>모델 만들다가 지칠 수가 있구나</p>
</li>
<li><p>제네릭은 미쳤구나</p>
</li>
</ol>
<p>진정 왜 개발자들이 TypeScript를 한 번 쓰면 다시 JavaScript로 못돌아간다고 하는지 알게되었음.
그리고 와 진짜 타입 모델 만드는게 쉽지 않고, 너무 너무 많이 만들어야한다.
그래서 공통적인 타입 구조는 반드시 재사용 가능하도록 제네릭으로 만들어야하겠구나라고 생각함.</p>
<pre><code>// 제네릭은 미쳤어요!!
export interface ApiResponse&lt;T&gt; {
  href: string;
  limit: number;
  next: string | null;
  offset: number;
  previous: string | null;
  total: number;
  items: T[];
}</code></pre><p>그리고 실제로 사용되는 API를 활용하다 보니까 뭔가 좀 더 재밌었던 것 같다.
모르는게 막 나오니까 찾아보는 맛도 좀 있었고, 에러난 걸 해결하면 또 <code>오 이것때문이었음?</code> 이러면서 나름 재밌게 작업을 해나간 프로젝트였음.</p>
<hr />

<h2 id="tanstack-query">Tanstack Query</h2>
<p>그리고 <code>Tanstack Query</code>는 <strong>day6_Fanground</strong>에서 사용해봤는데 분명 이 때는 재밌게 했던 것으로 기억함.
근데 이번에는 로그인에, 데이터 패칭에... 전반적인 부분에 사용하다 보니까 이래저래 에러가 참 많이 발생했었다.
아마 그 전에는 데이터 호출만 했기 때문에 재밌게 했었던 듯...ㅎ</p>
<p>아직 프로젝트 마무리가 된 건 아니지만, 지금까지 진행했던 호출 중에서 제일 골치가 아팠던 부분은 <code>플레이리스트 이름 변경</code>이었음.
아니 분명, 데이터 호출도, <code>query key</code>도, <code>refetch</code>도 되었는데 왜 UI에는 렌더링이 안되는가...?
이 문제는 블로그로 따로 작성도 했음.[ <a href="https://velog.io/@well_log/Tanstack-Query-QueryClient-setQueryData">QueryClient - setQueryData</a> ]</p>
<p>아무튼 이런 경험을 겪으면서 <code>Tanstack Query</code>에 대해 좀 더 깊이 알게 된 계기가 되긴 했음.
그리고 역시 데이터 패칭은 <code>Tanstack Query</code>가 확실히 좋구나, 관리하기가 참 편하구나, 라고 다시 한 번 더 느낌.</p>
<p>그리고 여기서도 타입을 맞추는게 쉽지 않구나를 느낌.</p>
<p>api를 따로 만들고, Tanstack Query로 hook을 만들어서 사용했는데 api와 hook의     <code>request</code>/<code>response</code> 타입을 맞추는게 쉽지 않음을 느꼈음.
그리고 웬만하면 다 <code>undefined</code>에 대한 검증을 했는데, 이 부분은 공통 모듈로 하나 만들어야 할 지 아직도 고민하는 부분임.
직접적으로 <code>if()</code>로 체크하는게 안정적이긴 하지만, 뭐 할때마다 <code>undefine</code> 검증을 하라고해서 이럴거면 공통으로 빼는게 낫지 않을까 생각하게 된 부분임. 하지만, 아직 확정은 못하고 고민만 진행 중임. 
앞으로 작업을 더 이어나가면서 어떻게 해야할지 봐야겠음.</p>
<hr />

<p>어쨌든, 이 프로젝트를 시작하게 된 이유는 기술 사용에 대한 부족감을 느꼈기 때문임.
남은 작업 진행하면서 앞으로 기술들을 더 갈고 닦으면서 프로젝트에 대한 DR/TR과 문제 해결 블로깅을 이어나갈 계획임.</p>
<p>남은 작업들이 많아서 에러를 많이 만나게 되겠지만, 힘내자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[refreshToken 적용하기]]></title>
            <link>https://velog.io/@well_log/refreshToken-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@well_log/refreshToken-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 18 Jun 2025 04:11:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>로그인을 하려는데, 로그인에 필요한 정보는 다 저장이 되었지만 로그인이 되지 않는 문제가 발생함.
이를 해결하기 위해 <code>refreshToken</code>을 적용</p>
</blockquote>
<h2 id="일단-문제사항이-뭐냐">일단, 문제사항이 뭐냐</h2>
<ol>
<li>로그인 → 사용자 권한까지는 문제 없었음</li>
<li>사용자 권한 → 필요한 정보들 받아오기까지는 문제 없었음</li>
<li>정보들 다 받아왔는데 결론: 로그인 안됨 → ❗️ <strong>문제사항 발생</strong></li>
</ol>
<p>문제사항 발생 이유가 뭐였느냐,</p>
<ol>
<li>일단 필요한 정보를 받아오는 부분은 비동기로 실행되는데 렌더가 먼저 되면서 로그인 상태 변경이 반영되지 않았음.</li>
<li>필요한 정보들은 이미 받아왔는데, useEffect의 조건으로 인해 액세스 토큰을 받아오는 부분을 계속 실행하면서 서버에서는 이미 한 번 사용된 정보로 계속 요청하니까 에러 발생 처리(<code>Invalid authorization code</code>)</li>
</ol>
<h2 id="그래서-refresh-token-적용함">그래서 refresh Token 적용함</h2>
<blockquote>
<p>일단, 필요한 정보들이 뭐냐면</p>
</blockquote>
<ul>
<li><code>?code</code></li>
<li><code>access_token</code></li>
</ul>
<p><code>refreshToken</code>을 적용하면서 기존 문제들을 어떻게 해결했느냐</p>
<ol>
<li><code>?code</code>를 한 번 쓰고 다시 못씀 → 로그인 재시도 에러 해결
: 한 번만 쓰고 나머지는 <code>refresh</code>로 해결</li>
<li>로그인 상태 유지 어려움(<code>token</code>만료)
: 자동 갱신</li>
<li>API 요청 전마다 access token 유효성 체크 어려움
: Axios 인터셉터에서 자동 처리</li>
</ol>
<h2 id="처리-과정">처리 과정</h2>
<blockquote>
<p>로그인 버튼 → 사용자 권한 → 권한 승인 후 인증 코드 발급 → 코드로 액세스 및 리프레시 토큰 발급 → 액세스 토큰으로 사용자 정보 받아와 로그인 처리 → 액세스 토큰 만료 시 리프레시 토큰 자동 갱신해서 로그인 유지</p>
</blockquote>
<h3 id="토큰-관련-로직-처리">토큰 관련 로직 처리</h3>
<pre><code>// 액세스 및 리프레시 토큰 발급
const getAccessToken = asyc () =&gt; { ... }
const getRefreshToken = asyc () =&gt; { ... }

// 액세스 토큰 만료 체크 &amp; 리프레시 로직
export const getValidAccessToken = async () =&gt; {
  const accessToken = localStorage.getItem(&quot;access_token&quot;);
  const refreshToken = localStorage.getItem(&quot;refresh_token&quot;);
  const expiresAt = parseInt(localStorage.getItem(&quot;expires_at&quot;) || &quot;0&quot;);

  if (Date.now() &lt; expiresAt) {
    return accessToken;
  }

  const refreshed = await getRefreshToken(refreshToken!);
  localStorage.setItem(&quot;access_token&quot;, refreshed.access_token);
  localStorage.setItem(&quot;expires_at&quot;, (Date.now() + refreshed.expires_in * 1000).toString());

  return refreshed.access_token;
};</code></pre><pre><code>// api.ts
export const api = axios.create({
  baseURL: `${BASE_URL}`,
  timeout: 1000,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
});

// 요청 인터셉터 추가하기
api.interceptors.request.use(async (request) =&gt; {
  const token = await getValidAccessToken();
  request.headers.Authorization = `Bearer ${token}`;

  return request;
});</code></pre><ul>
<li>요청 인터셉터에서 액세스 토큰 만료 여부를 체크하고, 만약 만료되었으면 리프레시 토큰으로 새 액세스 토큰을 받아와서 자동으로 토큰 갱신하도록 함</li>
</ul>
<h2 id="➕추가-localstorage-→-sessionstorage로-변경하여-토큰-관리">➕추가 localStorage → sessionStorage로 변경하여 토큰 관리</h2>
<p>기존에는 <code>Spotify</code>에서 나온대로 <code>localStorage</code>에 토큰을 저장하였다.
그런데 아무리 사이드 프로젝트라지만 이렇게 두면 안될 것 같아서 <code>sessionStorage</code>에 토큰을 저장하여 관리하는 방식으로 변경했다.</p>
<p>사실 토큰은 쿠키로 관리하는게 제일 안전한데 이 프로젝트는 프론트엔드 only라서 쿠키로는 관리하지 못해 차선책인 <code>sessionStorage</code>로 변경함.</p>
<p>변경 후, 로그인 &amp; 로그아웃 모두 테스트 해본 결과 정상적으로 동작 됨까지 확인함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[와...]]></title>
            <link>https://velog.io/@well_log/%EC%99%80</link>
            <guid>https://velog.io/@well_log/%EC%99%80</guid>
            <pubDate>Tue, 17 Jun 2025 15:38:14 GMT</pubDate>
            <description><![CDATA[<p>와.. 포트폴리오에 테스트 계정을 안해두고 올렸다...</p>
<p>개발할 때는 내 개인 계정으로 계속 했어서 포트폴리오에는 테스트용 계정을 만들어야한다는 점을 잊고있었다...ㅠ</p>
<p>아 진짜 이미 이력서 열람한 회사들은 아무 기능이 없는 사이트를 본건데..
나 참 진짜 어이가 없네ㅠㅠ</p>
<p>일단 뒤늦게라도 테스트 계정 추가해두긴 했는데
아 이력서 열람한 회사가 다시 한번 봐줬으면ㅠㅠㅠ</p>
<p><img src="https://velog.velcdn.com/images/well_log/post/295885be-158a-43a3-b452-b589af8f4fd4/image.jpg" alt=""></p>
<blockquote>
<p><strong>자나깨나 포트폴리오 재검토!
올린 포트폴리오도 다시 보자!</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Netlify에 배포 진행 에러 - webpack]]></title>
            <link>https://velog.io/@well_log/Netlify%EC%97%90-%EB%B0%B0%ED%8F%AC-%EC%A7%84%ED%96%89-%EC%97%90%EB%9F%AC-webpack</link>
            <guid>https://velog.io/@well_log/Netlify%EC%97%90-%EB%B0%B0%ED%8F%AC-%EC%A7%84%ED%96%89-%EC%97%90%EB%9F%AC-webpack</guid>
            <pubDate>Tue, 17 Jun 2025 14:36:53 GMT</pubDate>
            <description><![CDATA[<h3 id="배포-진행하기">배포 진행하기</h3>
<p>웹팩을 설정하고 Netlify에 배포를 진행했는데 에러가 발생함.</p>
<h3 id="이유는">이유는??</h3>
<blockquote>
<p><strong>Deploy did not succeed: Deploy directory &#39;build&#39; does not exist</strong>
<em>build 폴더를 찾지 못했다는 에러</em></p>
</blockquote>
<p>웹팩에서 번들링될 파일이 저장될 폴더 이름을 변경했는데 이걸 Netlify에 적용하지 않았음.</p>
<p>대부분은 번들링 파일은 <code>build/</code>에 저장되고, Netlify도 기본적으로 <code>build/</code>로 되어있음.
그런데 내가 저장될 폴더를 <code>dist/</code>라고 웹팩에 지정했는데 Netlify에 <code>dist/</code>로 수정하지 않아 에러를 발생</p>
<h3 id="해결">해결</h3>
<p>Netlify의 Build Setting의 <code>Publish directory</code>에서 <code>build</code> -&gt; <code>dist</code>로 변경!</p>
<blockquote>
<p><strong>참고!!</strong> 이거 이름 제대로 써야함!
나는 제대로 썼다고 했는데 빌드 에러 발생</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/well_log/post/ecdfea5e-9b4b-4435-b16e-bb1959d99cba/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>마지막 보면 <code>\bdist</code>로 되어있는거 보임?
<code>dist</code>가 아니라 <code>b</code>가 붙어서 에러... =&gt; 결국, 오타였음 ㅂ.ㅂ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryClient - setQueryData]]></title>
            <link>https://velog.io/@well_log/Tanstack-Query-QueryClient-setQueryData</link>
            <guid>https://velog.io/@well_log/Tanstack-Query-QueryClient-setQueryData</guid>
            <pubDate>Tue, 17 Jun 2025 13:28:03 GMT</pubDate>
            <description><![CDATA[<hr />

<h2 id="queryclientsetquerydata">queryClient.setQueryData</h2>
<p>쿼리의 캐시된 데이터를 즉시 업데이트하는데 사용할 수 있는 동기 함수</p>
<pre><code>queryClient.setQueryData(queryKey, updater)</code></pre><hr />

<p>플레이리스트의 이름을 변경하는 작업에서 처음에는 <code>invalidateQueries</code>를 사용했음.
그런데 이름이 변경되지 않는 거임.</p>
<ol>
<li>그래서 api 호출이 잘못되었나?싶어 확인해보니 → 이것의 문제는 아니었음.</li>
<li>그럼 <code>invalidateQueries</code>의 <code>qeuryKey</code>가 잘못되었나? → 아님.</li>
<li>그럼 <code>queryKey</code> 선택을 잘못했나? → 아님.</li>
<li>그럼 <code>invalidateQueries</code>으로 refetch가 안되나? → 이것도 아니었음.</li>
</ol>
<p>왜 위 문제들이 다 아니냐? </p>
<ol>
<li>Network 탭에서 확인한 결과, api 요청들은 제대로 이루어지고 있음.</li>
<li>새로 받아온 데이터들이 바로는 적용이 안되지만, 시간이 좀 지나면 새로운 데이터로 패칭되어있음.</li>
</ol>
<p>그럼 문제는 <strong>비동기</strong>.
UI에서 보고 있는 시점은 아직 이전 캐시 데이터가 그대로였기 때문에 fetch가 끝나기 전까지 이름이 안바뀌었던거.</p>
<p>그래서 이 문제를 해결하기 위해 <code>setQueryData</code>를 사용해서 즉시 캐시 업데이트했음.</p>
<pre><code>export const useChangePlaylistDetail = () =&gt; {
     ...,
    onSuccess: async (data, variables) =&gt; {
      queryClient.setQueryData([&quot;playlist-detail&quot;, variables.playlist_id], (old: any) =&gt; ({
        ...old,
        name: variables.name,
      }));
      ...
    },
  });
};</code></pre><p><img src="https://velog.velcdn.com/images/well_log/post/8b5b3003-6e25-4ff1-9079-f6631456a873/image.png" alt="">
ㅇㅇ refetch 되었다ㅜ</p>
<p><a href="https://tanstack.com/query/v5/docs/reference/QueryClient#queryclient">queryClient</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryClient - invalidateQueries]]></title>
            <link>https://velog.io/@well_log/Tanstack-Query-QueryClient</link>
            <guid>https://velog.io/@well_log/Tanstack-Query-QueryClient</guid>
            <pubDate>Tue, 17 Jun 2025 13:01:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="queryclient">QueryClient</h2>
<p>서버 데이터를 효율적으로 캐시하고, 공유하고, 갱신할 수 있도록 도와줌</p>
</blockquote>
<h3 id="역할">역할</h3>
<ul>
<li>캐시 저장소 : 서버에서 가져온 데이터를 메모리에 저장하고, 여러 컴포넌트 간에 공유</li>
<li>요청 중복 제거 : 같은 데이터를 여러 컴포넌트에서 요청해도 하나의 요청으로 처리</li>
<li>자동 리패치 : 포커스 전환 시나 데이터 변경 시 자동으로 데이터를 새로 가져옴</li>
</ul>
<h3 id="예시">예시</h3>
<pre><code>// index.tsx

import { QueryClient, QueryClientProvider } from &quot;@tanstack/react-query&quot;;

const queryClient = new QueryClient();

root.render (
  &lt;React.StrictMode&gt;
    &lt;QueryClientProvider client={queryClient}&gt;
        &lt;App /&gt;
    &lt;/QueryClientProvider&gt;
  &lt;/React.StrictMode&gt;
)</code></pre><pre><code>// 사용 예시
import { useMutation, useQueryClient } from &quot;@tanstack/react-query&quot;;

export const useUnfollowPlaylist = () =&gt; {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (params: UnfollowPlaylistRequest) =&gt; {
      return UnfollowPlaylist(params);
     },
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: [&quot;current-user-playlists&quot;] });
      window.location.replace(&quot;/&quot;);
    },
  });
};</code></pre><hr/>

<h2 id="queryclientinvalidatequeries"><strong>queryClient.invalidateQueries</strong></h2>
<hr/>

<pre><code>await queryClient.invalidateQueries(
  {
    queryKey: [&#39;posts&#39;],
    exact,
    refetchType: &#39;active&#39;,
  },
  { throwOnError, cancelRefetch },
)</code></pre><ul>
<li>캐시에 있는 하나 또는 여러 쿼리를 무효화하고 다시 가져오는데 사용<pre><code>queryClient.invalidateQueries({ queryKey: [&quot;current-user-playlists&quot;] });</code></pre></li>
<li><code>current-user-playlists</code>의 <code>queryKey</code>를 가지고 있는 쿼리를 무효화 시키고 캐싱되어 있던 데이터 대신 새로운 데이터를 서버에 요청</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹팩 설정하기 3 - Page not found]]></title>
            <link>https://velog.io/@well_log/%EC%9B%B9%ED%8C%A9-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-3-Page-not-found</link>
            <guid>https://velog.io/@well_log/%EC%9B%B9%ED%8C%A9-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-3-Page-not-found</guid>
            <pubDate>Tue, 17 Jun 2025 02:46:24 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/well_log/post/4e949db4-6140-4386-afc7-bbd9d24a70e6/image.png" alt=""></p>
<p>Netlify에서 배포 후 페이지 새로고침을 하면 해당 에러가 발생했다.
처음에는 Tanstack Query의 Mutation 후 refetching 때문인 줄 알고 새로운 데이터를 받아오는 부분을 수정하였다.</p>
<p>하지만, 해당 부분 수정 후에도 Page not found 에러가 계속 발생ㅎ</p>
<h2 id="원인">원인</h2>
<p>정적 파일 호스팅(Netlify)에서 동적 라우팅 경로를 직접 새로고침해서 생기는 문제임.
Netlify는 경로에 대한 실제 HTML 파일을 서버에서 찾으려고 하고, 없으니 404 에러</p>
<blockquote>
<p>예시 : <a href="https://example.netlify.app/playlist/12345">https://example.netlify.app/playlist/12345</a> 경로를 찾으려고 함 → 없어서 에러</p>
</blockquote>
<h2 id="해결">해결</h2>
<p>Netlify에서는 동적 라우팅을 SPA처럼 작동하게 하려면 <code>_redirects</code> 파일이 필요함</p>
<ol>
<li>public에 <code>_redirects</code> 파일 생성</li>
<li>그 안에 <code>/* /index.html 200</code> 작성 : 모든 경로를 <code>index.html</code>로 리디렉션 해라</li>
<li><code>webpack</code> 설정<pre><code>new CopyWebpackPlugin({
       patterns: [
         { from: &quot;public/manifest.json&quot;, to: &quot;manifest.json&quot; },
         { from: &quot;public/favicon.ico&quot;, to: &quot;favicon.ico&quot; },
         { from: &quot;public/_redirects&quot;, to: &quot;&quot; }, // dist/_redirect로 복사
       ],
     }),</code></pre></li>
</ol>
<blockquote>
<p>내 프로젝트에서는 <code>src/index.html</code>을 사용하는데
<code>_redirects</code>의 <code>index.html</code>은 <code>public</code>이던 <code>src</code>던 상관없이
<strong>빌드 산출물인 <code>dist</code>에 <code>_redirects</code>가 있어야 한다는 점</strong>임
어차피 Netlify는 <code>dist/_redirects</code>만 인식
(그리고 webpack에서 이미 index.html을 설정함)</p>
<p><code>webpack</code>에서 <code>from</code>의 <code>_redirect</code>경로는 확인해야함.
내가 어디에 넣었는지</p>
</blockquote>
<blockquote>
<h4 id="참고---_redirects-파일-형식과-규칙">참고 - <code>_redirects</code> 파일 형식과 규칙</h4>
</blockquote>
<pre><code>&lt;source path&gt; &lt;destination path&gt; [status code]</code></pre><pre><code>/* /index.html 200</code></pre><ul>
<li>모든 경로(/*)를</li>
<li>/index.html로 리다이렉트</li>
<li>200은 성공적으로 리턴한다는 의미<pre><code>/blog /new-blog 301
/docs/* /documentation/:splat 302</code></pre></li>
<li><code>/blog</code> → <code>/new-blog</code> (영구 리디렉트)</li>
<li><code>/docs/abc</code> → <code>/documentation/abc</code> (임시 리디렉트)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ContextAPI 사용하기]]></title>
            <link>https://velog.io/@well_log/ContextAPI-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@well_log/ContextAPI-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jun 2025 13:09:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="contextapi">ContextAPI</h3>
</blockquote>
<ul>
<li>Context API는 React 16에서 부터 나온 리액트 내장 API</li>
<li>props를 사용하지 않고 필요한 데이터(state)를 전달 할 수 있음</li>
<li><strong>전역 상태를 컴포넌트 트리 전체에 props 없이 공유할 수 있음</strong></li>
</ul>
<hr />

<h2 id="언제-사용">언제 사용?</h2>
<p>단순한 전역 상태에 사용하기 적합
테마, 로그인 정보, 모달 open/close, 언어 설정 등</p>
<h3 id="전역-상태-라이브러리가-있는데-context-api-사용-이유">전역 상태 라이브러리가 있는데 Context API 사용 이유?</h3>
<ul>
<li>가볍고 내장 되어 있음 -&gt; 별도 설치 필요 없음, 외부 의존성 없이도 간단한 전역 상태 관리 가능</li>
<li>단순한 상태 공유에 충분 -&gt; 테마, 로그인 정보, 모달, 언어 설정 등</li>
<li>코드가 직관적이고 추적하기 쉬움</li>
</ul>
<h3 id="하지만-한계는-있지">하지만, 한계는 있지</h3>
<ul>
<li>비동기 로직 처리가 어려움 </li>
<li>상태가 복잡해질수록 관리 어려움 -&gt; 깊은 nesting, 복잡한 업데이트 로직은 한계</li>
</ul>
<h3 id="그래서">그래서</h3>
<p>Context API는 단순한 전역 상태 관리에는 적합하지만, 
비동기 로직, 캐시, 전역 상태가 많고 연관 관계가 많을 때, 성능(렌더 최적화) 문제 생길 때는 전역 상태 라이브러리 사용하는게 적합</p>
<hr />

<h2 id="어떻게-사용">어떻게 사용?</h2>
<ol>
<li>Context 생성<pre><code>import { createContext } from &quot;react&quot;;
</code></pre></li>
</ol>
<p>export const MyContext = createContext&lt;타입&gt;(전달할 데이터 초기값);</p>
<p>// 초기값 null이 권장
export const MyContext = createContext&lt;타입 | null&gt;(null);</p>
<pre><code>&gt; **초기값을 `null`로 권장하는 이유**
&gt; 
&gt;1. 실수 방지
→ 초기값을 넣으면 실수로 `Provider` 없이 사용해도 에러는 안남, 하지만 내부 동작은 꼬일 수 있음
2. Provider 필수 사용을 강제할 수 있음
→ `Provider` 안에서 동작되도록 강제 → 그래서 Context 설정을 잘못해도 에러를 내뱉어서 빠른 디버깅 가능
3. 초기값 없이 동작하게 하고 싶을 때
→ 실제 상태나 함수는 `Provider`에서만 만들어지고 전달됨

2. Provider 컴포넌트 생성</code></pre><p>import { useState } from &quot;react&quot;;
import { MyContext } from &quot;./MyContext&quot;;</p>
<p>export const MyProvider = ({ children }: { children: React.ReactNode }) =&gt; {
  const [value, setValue] = useState(&quot;Hello Context&quot;);</p>
<p>  return (
    &lt;MyContext.Provider value={value}&gt;
      {children}
    &lt;/MyContext.Provider&gt;
  );
};</p>
<pre><code>
3. Provider로 감싸기</code></pre><p>import { MyProvider } from &quot;./MyProvider&quot;;
import App from &quot;./App&quot;;</p>
<p>const Root = () =&gt; (
  <MyProvider>
    <App />
  </MyProvider>
);</p>
<pre><code>
4. Context 사용하기</code></pre><p>import { useContext } from &quot;react&quot;;
import { MyContext } from &quot;./MyContext&quot;;</p>
<p>const MyComponent = () =&gt; {
  const value = useContext(MyContext);
  return <div>{value}</div>;
};
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹팩 설정하기 2 - CopyWebpackPlugin]]></title>
            <link>https://velog.io/@well_log/%EC%9B%B9%ED%8C%A9-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-2-CopyWebpackPlugin</link>
            <guid>https://velog.io/@well_log/%EC%9B%B9%ED%8C%A9-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-2-CopyWebpackPlugin</guid>
            <pubDate>Mon, 16 Jun 2025 03:05:33 GMT</pubDate>
            <description><![CDATA[<h2 id="copywebpackplugin">CopyWebpackPlugin</h2>
<p><code>CopyWebpackPlugin</code> 모듈은 프로젝트를 빌드할 때 정적 파일을 복사하고 디렉토리를 만들기 위한 도구</p>
<h3 id="왜-사용했느냐">왜 사용했느냐..?</h3>
<p>프로젝트 진행 중 메타 태그를 설정해야하는데, 나는 빌드 결과물이 <code>dist</code>에 되는 상황</p>
<p>그래서 <code>public</code>에 있는 메타 태그 관련된 것들을 <code>dist</code> 폴더에 복사해줘야 동작하기 때문에 해당 플러그인을 설치하고 진행</p>
<h3 id="진행">진행</h3>
<pre><code>npm i copy-webpack-plugin --save-dev</code></pre><pre><code>// webpack.config.js

module.exports = (env, argv) =&gt; {
    const isProduction = argv.mode === &quot;production&quot;;

    return {
        mode: isProduction ? &quot;production&quot; : &quot;development&quot;;
        ...code,
        plugins: [
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: path.resolve(__dirname, &quot;src&quot;, &quot;index.html&quot;),
            }),
            new Dotenv({
                ...code
            }),
            new CopyWebpackPlugin({
                   patterns: [
                      { from: &quot;public/manifest.json&quot;, to: &quot;manifest.json&quot; },
                      { from: &quot;public/favicon.ico&quot;, to: &quot;favicon.ico&quot; },
                ],
              }),
        ]
    }
}</code></pre><p><code>webpack</code>에서 해당 설정 해줌으로 인해서 <code>public</code> -&gt; <code>dist</code>로 복사되고,
<code>HtmlWebpackPlugin</code>에서 설정한 <code>index.html</code>로 이동하여 <code>&lt;link /&gt;</code>태그 작성해야함.(<code>/favicon.ico</code>, <code>/manifest.json</code>)</p>
<p>파일 복사해도 브라우저가 읽으려면 <code>HTML</code>의 <code>&lt;link /&gt;</code> 같은 태그가 있어야 함.</p>
<h2 id="결론">결론</h2>
<blockquote>
<p><code>webpack</code>은 파일을 복사해주는 역할
<code>HTML</code>은 브라우저에서 이 파일을 어디서 읽을지 알려주는 역할</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[mui - sx props]]></title>
            <link>https://velog.io/@well_log/mui-sx-props</link>
            <guid>https://velog.io/@well_log/mui-sx-props</guid>
            <pubDate>Mon, 16 Jun 2025 01:27:31 GMT</pubDate>
            <description><![CDATA[<h2 id="sx">sx</h2>
<ul>
<li>MUI에서 제공하는 스타일 넣는 props임</li>
<li>sx는 <code>style={{ }}</code>랑 같은 개념임</li>
<li>근데 MUI 전용인것 뿐</li>
</ul>
<pre><code>&lt;Box sx={{ padding: &quot;0 2rem&quot; }}/&gt;</code></pre><h3 id="style과-sx-차이"><code>style</code>과 <code>sx</code> 차이</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>style={{ ... }}</code></th>
<th><code>sx={{ ... }}</code></th>
</tr>
</thead>
<tbody><tr>
<td>기본 스타일</td>
<td>가능</td>
<td>가능</td>
</tr>
<tr>
<td>MUI 테마 적용</td>
<td>안 됨</td>
<td>됨 (<code>primary.main</code>, <code>theme.spacing()</code>)</td>
</tr>
<tr>
<td>반응형 지원</td>
<td>안 됨</td>
<td>됨 (<code>{ xs: 1, md: 2 }</code> 등)</td>
</tr>
<tr>
<td>hover 같은 중첩 스타일</td>
<td>안 됨</td>
<td>됨 (<code>&amp;:hover</code>, <code>&amp; .child</code> 등 가능)</td>
</tr>
<tr>
<td>MUI 최적화</td>
<td>일반 CSS</td>
<td>MUI 스타일 시스템 기반</td>
</tr>
</tbody></table>
<h3 id="중첩-스타일-예시">중첩 스타일 예시</h3>
<pre><code>&lt;Box sx={{ padding: 2, color: &quot;red&quot;, &quot;&amp;:hover&quot;: { color: &quot;blue&quot; } }}&gt;Hello&lt;/Box&gt;
</code></pre>]]></description>
        </item>
    </channel>
</rss>