<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sun_rim.log</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 개발자 전해림입니다</description>
        <lastBuildDate>Sat, 28 Feb 2026 10:08:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. sun_rim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sun_rim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[좋아요 버튼이 사실 조회수 버튼이었던 건에 대하여]]></title>
            <link>https://velog.io/@sun_rim/%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B2%84%ED%8A%BC%EC%9D%B4-%EC%82%AC%EC%8B%A4-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%B2%84%ED%8A%BC%EC%9D%B4%EC%97%88%EB%8D%98-%EA%B1%B4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@sun_rim/%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B2%84%ED%8A%BC%EC%9D%B4-%EC%82%AC%EC%8B%A4-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%B2%84%ED%8A%BC%EC%9D%B4%EC%97%88%EB%8D%98-%EA%B1%B4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sat, 28 Feb 2026 10:08:33 GMT</pubDate>
            <description><![CDATA[<h3 id="1-기획자님-게시글에-좋아요를-눌렀다가-취소했더니-조회수가-올라가요">1. 기획자님 게시글에 좋아요를 눌렀다가 취소했더니 조회수가 올라가요</h3>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/46bb0102-4947-4849-8b55-ccac87226472/image.png" alt="">커뮤니티 서비스 오픈을 앞두고 게시글의 좋아요 버튼을 눌렀다가 취소했을 때 데이터가 새로고침되면서 조회수가 올라가고 있었다. 당연히 버그라고 생각했지만 서비스 오픈 초반.. 유저가 적을 때 게시글의 조회수를 높게 보일 수 있는 수단이라는 이상한 생각으로 기획자에게 그대로 두자고 했다가 바로 빠꾸를 먹고 고치면서 공부해본 
ReactQuery의 캐시 업데이트에 대해 이야기 해보려 합니다.</p>
<h3 id="2-이전의-상태">2. 이전의 상태</h3>
<pre><code class="language-javascript">const { mutate: toggleLike, isPending: isLikePending } = useMutation({
  mutationFn: async () =&gt; {
    const query = contentBody?.myLikeFlag ? postsQueries.deleteLike(postId) : postsQueries.postLike(postId);

    return await query.queryFn();
  },
  // 이미 공감상태면 toast
  onSuccess: () =&gt; {
    setLikeInfo({ likeCount: likeCount + 1, isLiked: true });
  },
});

const handleToggleLike = useThrottle(() =&gt; toggleLike(), 200);</code></pre>
<pre><code class="language-javascript"> &lt;Button
   size={&#39;sm&#39;}
   variant={&#39;line-secondary&#39;}
   startIcon={isLiked ? &lt;ThumbsFillIcon /&gt; : &lt;ThumbsIcon /&gt;}
   className={clsx(&#39;rounded-full px-3 py-2&#39;)}
   onClick={handleToggleLike}
  &gt;
    {isLikePending ? &lt;CircularProgress /&gt; : &lt;span className={&#39;heading-sm-500&#39;}&gt;공감해요&lt;/span&gt;}
 &lt;/Button&gt;</code></pre>
<p>이전의 상태에서는 ContentBody(게시글 상세 데이터)의 myLikeFlag(내가 좋아요했는지 상태)의 상태에 따라 좋아요, 좋아요 취소  api를 호출하고 있었는데 서버에서는 contentBody가 호출되면 조회수를 +1 하게 되는데
deleteLike api를 호출하면 contentBody?.myLikeFlag가 변경되어 refetch 되면서 조회수가 끝없이 올라가는 
문제가 있었다. </p>
<p>그리하여 어떤 상황에서 ReactQuery에서 자동 refetch가 일어나는지 찾아보게 되었는데 
ReactQuery는 stale 상태인 쿼리가 아래의 조건을 만족할 때 refetch를 실행하게 된다.
**
<em>stale 상태 : 캐시된 데이터가 만료되어 서버에서 새로운 데이터를 다시 가져와야 하는 오래된 데이터 상태</em>**</p>
<blockquote>
<ol>
<li>쿼리를 사용하는 컴포넌트가 마운트될 때</li>
<li>윈도우가 포커스 될 때</li>
<li>네트워크가 재연결 될 때</li>
<li>refetchInterval 설정을 통해 요청할 때</li>
</ol>
</blockquote>
<p>즉, 위의 조건이 아닌 상태에서는 쿼리가 stale 상태이더라도 refetch되지 않고 이전 데이터를 계속 보여주게 된다. </p>
<p>그리하여 mutation은 결과에 따른 어떤 쿼리가 영향을 받을지 모르기 때문에  Server만 바꾸고 Cache는 자동으로 바꾸지 않아 개발자가 직접 캐시 업데이트를 지정해주어야해서 쿼리가 stable상태가 되어  deleteLike api를 호출 
했을 때 stale상태인 쿼리가 refetch가 필요하다고 판단하여 refetch되며 게시글의 조회수가 올라가게 되었다.</p>
<h3 id="3-이후의-상태">3. 이후의 상태</h3>
<p>이전의 조회수 증가 문제를 해결하기 위해서는 refetch 자체를 일어나지 않게 해야했는데</p>
<pre><code class="language-javascript">const { mutate: toggleLike, isPending: isLikePending } = useMutation({
    mutationFn: async () =&gt; {
      const query = isLiked ? postsQueries.deleteLike(postId) : postsQueries.postLike(postId);

      return await query.queryFn();
    },

    onSuccess: () =&gt; {
      const newLikeFlag = !isLiked;
      const newLikeCount = newLikeFlag ? likeCount + 1 : likeCount - 1;

      setLikeInfo({
        likeCount: newLikeCount,
        isLiked: newLikeFlag,
        isScraped: isScraped,
      });

      queryClient.setQueryData&lt;IContentBodyResponse&gt;(postsQueries.getContentBody(postId).queryKey, (old) =&gt; {
        if (!old) return old;
        return {
          ...old,
          myLikeFlag: newLikeFlag,
          likeCount: newLikeCount,
        };
      });
    },

    onError: ({ message }) =&gt; {
      toast({ message: `공감하기중 오류가 발생하였습니다. ${message}` });
    },
  });</code></pre>
<ol>
<li>먼저 mutation의 기준을 서버에서 로컬 state로 변경하여 UI state 기준으로 stale cache 영향을 받지 않도록 안정적이게 수정했다.</li>
</ol>
<pre><code class="language-javascript">//as-is
const query = contentBody?.myLikeFlag ? postsQueries.deleteLike(postId) : postsQueries.postLike(postId);

//to-be
const query = isLiked ? postsQueries.deleteLike(postId) : postsQueries.postLike(postId);</code></pre>
<ol start="2">
<li>mutation 성공 후 cache를 직접 갱신했다. 
mutation 성공
→ cache 직접 최신화
→ stale 아님
→ refetch 필요 없음
→ GET post 안함
→ 조회수 증가 X<pre><code class="language-javascript">queryClient.setQueryData&lt;IContentBodyResponse&gt;(postsQueries.getContentBody(postId).queryKey, (old) =&gt; {
if (!old) return old;
return {
 ...old,
 myLikeFlag: newLikeFlag,
 likeCount: newLikeCount,
};
});</code></pre>
</li>
</ol>
<p>queryKey는 cache주소로 해당 queryKey지정으로 특정 cache만 수정하고 updater function 구조로 현재 cahce 데이터를 전달했다. </p>
<ol>
<li>Query 찾기 queryCache.find(queryKey)</li>
<li>data 교체 query.data = newData</li>
<li>timestamp 갱신 query.updatedAt = Date.now()</li>
<li>observer notify observer.forEach(re-render)</li>
</ol>
<p>setQueryData로 서버 refetch 없이 React Query Cache를 직접 최신 상태로 덮어쓰게 했다</p>
<h3 id="3-1-왜-invalidatequeries-대신-setquerydata가-맞았냐">3-1 왜 invalidateQueries 대신 setQueryData가 맞았냐</h3>
<p><strong>invalidateQueries</strong>는 
→ active query라면 refetch (inactive query는 stale 처리만)
→ GET post 호출
→ 조회수 증가</p>
<p><strong>setQueryData</strong>는
→ cache 최신화
→ refetch 없음
→ 조회수 증가 없음</p>
<h3 id="4-manual-cache-sync">4. Manual Cache Sync</h3>
<p>결론적으로 자동으로 refetch 하게 두지 않고, 내가 원하는 시점에 캐시 데이터를 직접 수정하는 방식을 
Manual Cache Sync라고 볼 수 있다.</p>
<p>React Query는 기본적으로 “서버 상태 동기화”를 자동화해주는 라이브러리지만, 모든 상황에서 자동 동기화가 정답은 아니다.</p>
<p>특히 이번 사례처럼:</p>
<ul>
<li><p>mutation 후 서버 데이터가 크게 변하지 않고</p>
</li>
<li><p>UI에 필요한 값만 부분적으로 바뀌며</p>
</li>
<li><p>불필요한 refetch가 비용(조회수 증가, 네트워크 낭비)을 만든다면</p>
</li>
</ul>
<p>자동 refetch보다는 의도적으로 캐시를 직접 동기화하는 전략이 더 적절하다.</p>
<p>Manual Cache Sync의 핵심은 다음과 같다:</p>
<ol>
<li><p>서버는 mutation으로 변경</p>
</li>
<li><p>클라이언트 캐시는 setQueryData로 직접 수정</p>
</li>
<li><p>쿼리는 stale 상태가 되지 않음</p>
</li>
<li><p>불필요한 GET 요청이 발생하지 않음</p>
</li>
</ol>
<blockquote>
<p>즉, &quot;서버를 신뢰하되, 네트워크는 아낀다&quot; 는 전략이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[뒤로가기의 무한굴레 ]]></title>
            <link>https://velog.io/@sun_rim/%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0%EC%9D%98-%EB%AC%B4%ED%95%9C%EA%B5%B4%EB%A0%88</link>
            <guid>https://velog.io/@sun_rim/%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0%EC%9D%98-%EB%AC%B4%ED%95%9C%EA%B5%B4%EB%A0%88</guid>
            <pubDate>Sun, 07 Sep 2025 06:46:11 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 오늘은 회사에서 프로젝트를 진행하면서 약 5번의 배포와 n번의 QA가 나왔던 뒤로가기 router.back()에 대한 이야기를 써보려고 합니다.</p>
<h3 id="여러분들은-routerback을-어떻게-쓰고-계신가요">여러분들은 router.back()을 어떻게 쓰고 계신가요?</h3>
<p>저는 보통 페이지간 이동이 필요할때는 router.push(&#39;url&#39;)로 이동하고 뒤로가기가 필요한 순간에 router.back()을 썼는데요 분명 다른 팀원들이 만든 페이지에서는 뒤로가기가 잘 되는데 제 페이지 에서는 3 -&gt; 2 -&gt; 1 구조로 뒤로 가는게 아닌 3 -&gt; 2 -&gt; 3 -&gt; 2 이렇게 뒤로가기의 무한굴레에 빠지기 일쑤였습니다.
그로인해 약 일주일동안 2개의 방식을 적용했었는데요 그 방법을 소개하고 결국 해낸 방법에 대해 써보겠습니다. </p>
<h3 id="문제">문제</h3>
<p>먼저 간단하게 router.back()의 구조에 대해 설명해보자면 </p>
<blockquote>
<p>💡브라우저의 History API (window.history.back())을 호출하는 얇은 래퍼</p>
</blockquote>
<p>즉, 사용자가 직전에 방문한 URL이 있으면 해당 페이지로 이동합니다. </p>
<p>이러한 router.back에서 무한굴레의 문제가 발생한 이유는 정말 단순하게 router.push와 router.replace의
차이점을 모르고 썼고, router.back과 router.push를 혼합해서 쓰면서 무한굴레의 문제가 발생했습니다.</p>
<ul>
<li><strong>router.push :</strong> 히스토리에 새로운 기록 추가</li>
<li><strong>router.replace :</strong> 히스토리에 새로운 기록을 추가하지 않고, 현재 기록을 덮어씀</li>
</ul>
<p>또한 뒤로가기를 할때 <code>router.back</code> 으로 히스토리를 하나씩 지우면서 뒤로가기를 해야하지만,
중간중간 무조건 이동하는 페이지가 있을 때는(수익창출에서 뒤로가기를 하면 프로필이 꼭 나와야함) <code>router.push</code>로 히스토리에 새로운 기록을 추가하면서 뒤로가기를 하고 있었습니다. </p>
<p>그러면 A → B → C → D 로 갔다가 뒤로가기 시 
D 이동 히스토리 지우기 → C 이동 히스토리 지우기 → B 이동 히스토리 지우기 → A 이동 히스토리 지우기 
히스토리를 하나씩 지우면서 뒤로가기가 되어야 하지만 C에서 router.push(”B”)를 쓰게된다면</p>
<p>D 이동 히스토리 지우기 → C 이동 히스토리 지우고 히스토리 내역 맨 뒤에 B가 추가됨 → B에서 히스토리 지우기 → A 이동 히스토리 지우기 → 다시 B로 이동 </p>
<p>그래서 과거 페이지로 돌아가고 싶을 때 <code>router.push</code>를 쓰면 <strong>스택의 “뒤쪽”에 과거 페이지가 새로 생겨버려서</strong> 뒤로가기가 꼬여버리게 되는데요</p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/d3d9009e-960e-46c1-9bbe-e068f90dae5f/image.png" alt=""></p>
<h3 id="첫-번째-해결-시도--모바일-브릿지-통신으로-페이지를-모두-앱에서-띄워주기">첫 번째 해결 시도 : 모바일 브릿지 통신으로 페이지를 모두 앱에서 띄워주기</h3>
<p>제목만 들어도 손이 많이가고 복잡한 코드일거 같은데요..
웹뷰를 쓰고있는 서비스 특성 상 모바일과 브릿지통신을 하는 경우가 많아 첫 번째 방법으로 플러터 개발자분께
새창을 여는 브릿지를 만들어달라고 부탁했습니다.</p>
<p>브릿지 통신을 이용해서 openAppScreen으로 새창을 띄우고 closeAppScreen을 사용해서 새창을 닫는 방법으로 </p>
<pre><code class="language-javascript">브릿지 코드에 대한 설명은 다음 포스팅에서 자세히 다뤄보겠습니다. 

/**
 * 앱 새창 열기
 * @param url 새창으로 띄울 페이지 URL
 */
export const openAppScreen = async (url: string) =&gt; {
  const data = url;
  return await flutterBridge(data, &#39;openAppScreen&#39;);
};</code></pre>
<p>openAppScreen의 url로 새창으로 띄울 페이지 url을 넘기면 플러터측에서 새창으로 페이지를 띄워주었습니다.</p>
<p>하지만 요청 시 흰 화면이 보이는 시간이 길고 버벅거리는 문제와 스와이프가 안되는 문제로 인해 못쓰게 되었고,
프로필  이후의 페이지를 앱에게 위임했던것 인데요 모바일에서도 매번 새창을 띄워줘야하기때문에 메모리 문제도 있었습니다.</p>
<h3 id="두-번째-해결-시도--프로필에서-넘어가는-모든-페이지에-windowopen-새창-띄우기">두 번째 해결 시도 : 프로필에서 넘어가는 모든 페이지에 window.open 새창 띄우기</h3>
<p>두번째 방법으로는 window.open을 사용해서 프로필에서 이동하는 모든 페이지에 새 창을 띄우는 방법이었는데요.</p>
<pre><code class="language-javascript"> const handleRouterProfile = () =&gt; {
    window.open(&#39;/프로필 페이지&#39;, &#39;_blank&#39;);
  };</code></pre>
<p>새창을 여는건 큰 문제가 없었지만 AOS에서 window.close가 안되는 문제가 있었고, AOS에서는 </p>
<pre><code>&lt;a href=&quot;&quot;/&gt;&lt;/a&gt;</code></pre><p>이렇게 a태그를 활용해 &quot;&quot;로 페이지를 이동시키는 방법밖에 없었습니다. 
또한 스와이프 기능이 새 창을 다 꺼버리는 문제가 있어 UX 적으로 좋지 않다는 판단을 하여 
두 번째 방법도 쓰지 못하게 되었습니다.
<img src="https://velog.velcdn.com/images/sun_rim/post/6b365e5a-8370-4986-8a11-23452d306b4e/image.png" alt=""><img src="https://velog.velcdn.com/images/sun_rim/post/df15edde-1ded-41c5-990a-b63be1d3f31e/image.png" alt=""><img src="https://velog.velcdn.com/images/sun_rim/post/ab37d2d6-f10d-4299-8a76-1faff81a3a37/image.png" alt=""><img src="https://velog.velcdn.com/images/sun_rim/post/d4f6b218-554f-4e30-86ad-886ad9b1b46a/image.png" alt="">(험난한 뒤로가기 수정 히스토리………)</p>
<h2 id="결론과-마지막-시도">결론과 마지막 시도</h2>
<h3 id="세-번째-시도--더이상-물러날-곳이-없다튜닝의-끝은-순정">세 번째 시도 : 더이상 물러날 곳이 없다.(튜닝의 끝은 순정)</h3>
<p>이제는 더이상 물러설 방법이 없기때문에 router.push와 router.replace에 대해 더 자세히 찾아보니 
router.back과 router.push를 혼합해서 쓰면서 히스토리 배열이 꼬이고 있다는 문제를 발견하게 되었고</p>
<ul>
<li>페이지 이동 시 : router.push</li>
<li>페이지 뒤로가기 시 : router.back</li>
<li>페이지에서 api 통신 후 결과값을 가지고 페이지를 이동 시 : router.replace</li>
</ul>
<p>router.push와 router.replace를 적절히 써야하는 상황을 잘 알아야 하고 그 안에 구조가 어떻게 흘러가는지 
잘 알지 못하면 이러한 문제가 생길 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내 세상에는 react-query밖에 없었어]]></title>
            <link>https://velog.io/@sun_rim/%EB%82%B4-%EC%84%B8%EC%83%81%EC%97%90%EB%8A%94-react-query%EB%B0%96%EC%97%90-%EC%97%86%EC%97%88%EC%96%B4</link>
            <guid>https://velog.io/@sun_rim/%EB%82%B4-%EC%84%B8%EC%83%81%EC%97%90%EB%8A%94-react-query%EB%B0%96%EC%97%90-%EC%97%86%EC%97%88%EC%96%B4</guid>
            <pubDate>Sun, 13 Jul 2025 05:59:31 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 프론트엔드 개발을 시작한지는 3년차 현업에서 프론트엔드 개발자로 일한지 1년차인 주니어 입니다. 지금까지의 제 개발을 되돌아 볼 겸 react-query와 swr 내용을 준비했습니다.</p>
<h3 id="1-내-세상에는-react-query만-존재했어">1. 내 세상에는 React-Query만 존재했어</h3>
<p>저는 개발을 하며 상태관리 라이브러리를 선택할때 항상 react-query만을 썼었는데요 누군가 너는 왜 
react-query만 써? 라고 물으면 고장이 나버리면서 어.. 나도 몰라 라고 답할 수 밖에 없었습니다. 
왜냐면 처음 상태관리 라이브러리를 접할때 react-query를 접한 뒤 다른걸 쓸 생각도 하지 못했습니다. 
익숙함이 좋았으니까요. 그런데 회사를 다니다보면 내가 들어보지도 못하거나 한번도 쓰지 않았던 스택을 쓰는 경우가 허다했고 쓰다보니 각각의 장단점이 보였습니다. 그래서 크게 느꼈던 swr 내용을 소개하고싶었습니다.</p>
<h3 id="2-swr">2. SWR</h3>
<p>&quot;데이터 가져오기를 위한 React Hooks&quot; swr 공식 홈페이지를 보면 나와있는 타이틀입니다. 잘 기억해주세요
SWR은 Stale-While-Revalidate의 약자로, React 컴포넌트에서 사용할 수 있는 훅 형태의 SWR 라이브러리 입니다. SWR을 사용하면 간편하게 데이터를 가져오고, 캐시 하여 이전 데이터를 재사용하며, 신선한 데이터를 가져와서 사용자 경험을 향상시킬 수 있습니다.
사용자 프로필을 GET하는 코드로 예시를 들어보겠습니다.</p>
<pre><code class="language-typescript">export function useMemberProfile(id?: number) {

  const key = useMemo(() =&gt; {
    const base = profileId != null ? `/member/profile/${id}` : token ? &#39;/member/profile&#39; : null;
    return base ? [base, Date.now()] : null;
  }, [profileId, token]);

  const fetcher = async ([url]: [string, number]) =&gt; {
    const res = await clientFetcher&lt;ProfileIdRes&gt;(url, &#39;GET&#39;, undefined);
    return res.response;
  };

  const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, {
    revalidateOnFocus: false,
  });

  return {
    data,
    error,
    isLoading,
    isValidating,
    mutate,
  };
}</code></pre>
<p>코드를 간단히 설명하자면 내 프로필과, 타인 프로필을 profileId에 따라서 분기처리를 하는 코드입니다.
useMemo함수는 조금 뒤에 설명을 하기로 하고, fetcher 부터 설명하자면.
key를 받아 api를 호출하는 함수입니다. 여기선 key가 [url, timestamp]형태이므로, 구조 분해로 url만 사용합니다. clientFetcher는 공통 fetch wrapper입니다. react-query와 유사한 형태로 사용됩니다.</p>
<p>마지막 useSWR을 호출하는 곳의 구조는 아래와 같습니다.</p>
<ul>
<li>data: 정상적으로 받아온 응답 데이터</li>
<li>error: 요청 실패 시 에러 객체</li>
<li>isLoading: 최초 요청 로딩 여부</li>
<li>isValidating: 백그라운드 재검증 중 여부</li>
<li>mutate: 수동으로 캐시를 갱신하거나 재요청을 트리거하는 함수</li>
</ul>
<p>옵션에 대해서도 설명하겠습니다.</p>
<ul>
<li>revalidateOnFocus: 탭(또는 창)이 다시 포커스를 받을 때 데이터를 자동으로 재요청할지 여부</li>
<li>revalidateOnReconnect:    네트워크 재연결 시 자동 재요청    true</li>
<li>refreshWhenHidden:    페이지가 숨겨져 있어도 refreshInterval 작동할지    false</li>
<li>refreshWhenOffline:    오프라인일 때도 refreshInterval 작동할지    false</li>
<li>focusThrottleInterval:    포커스 간 재요청 최소 간격 (ms)</li>
</ul>
<p>여기서 문제점이 하나 있었는데요 메인 페이지에서 상대방을 팔로우 한뒤 내 프로필로 들어오게되면 useSWR의 장점인 같은 url로 여러번 요청해도 바뀐점이 없다면 캐싱되어있던 데이터를 내려주는것 때문에 내 팔로워 수가 업데이트 되지 않는 문제가 있었습니다.</p>
<p>팔로우를 하는 api에서 내 프로필에 대해 강제 Mutate를 해줄수도 있지만 그렇게 되면 모든 api에 대한 Mutate를 해줘야 했기 때문에 비효율적이라고 생각해서 다른 방법을 고민하던 중 매번 다른 요청으로 인지하게 해서 매번 데이터를 가져오게 하는 방법을 선택했습니다. </p>
<p>now.Date()로 url요청 뒤에 시간을 넣어 매번 다른 요청으로 인지하게 하여 내 프로필에 접근하게 되면 매번 새로운 데이터를 가져오도록 했습니다.</p>
<p>하지만 이 방법은 전역 mutate를 하지 못한다는 단점과, 서버에 부하가 갈 수 있다는 문제점을 가지고 있기 때문에 필요한 부분에만 써야합니다.</p>
<h3 id="3-swr-vs-react-query">3. SWR vs React-Query</h3>
<h4 id="1-기본-사용-방식">1) 기본 사용 방식</h4>
<p><strong>💡 Provider 사용</strong>
<strong>SWR</strong> : 별도의 provider 없이 컴포넌트에서 바로 사용할 수 있다. -&gt; 설정이 간단해 프로젝트 구성 시 빠르게 시작 </p>
<p><strong>React-Query</strong> : app 파일에서 반드시 Provider로 컴포넌트를 감싸야한다. -&gt; 이 구조는 애플리케이션 전체에 걸친 데이터 관리를 일관되게 해주며, 쿼리 상태를 더 잘 통합할 수 있게 해준다</p>
<h4 id="2-데이터-관리와-처리">2) 데이터 관리와 처리</h4>
<p><strong>💡 데이터 전송, mutation</strong>
<strong>SWR</strong> : <code>useSWR()</code>는 기본적으로 데이터를 읽어오는(Read)데 사용되며, 데이터를 클라이언트 측에서 
직접 변경할 때는 <code>mutate()</code> 함수를 사용한다. <code>mutate()</code>는 캐시된 데이터를 업데이트 하고, 서버 요청 없이 클라이언트에서 즉시 데이터를 변경하는 데 사용된다. 예를 들어, 사용자가 특정 데이터를 업데이트하면 이를 즉시 화면에 반영하고, 나중에 서버와 동기화한다. 
SWR의 이 방식은 클라이언트 측에서 데이터가 자주 변경되거나, 서버 요청과 관계없이 빠른 UI 업데이트가 필요할 때 유리하다.</p>
<p><strong>React-Query</strong> : useMutation을 사용하여 서버와 직접 상호작용하여 데이터를 전송하고 변경한다. 이 방식은 서버 상태를 동기화하고 관리하는 데 중점을 두기 떄문에, 서버의 데이터를 변경하는 작업이 더 명확하게 처리된다. 
예를 들어, 사용자가 특정 데이터를 업데이트하면, 서버로 직접 요청을 보내어 데이터를 변경하고, 성공 시 해당 쿼리의 데이터를 다시 가져오도록 하여 클라이언트와 서버 간의 상태가 일관되게 유지되도록 한다.</p>
<p><strong>💡 상태관리</strong>
<strong>SWR</strong> : 클라이언트 전역 상태로 데이터를 관리하여 모든 컴포넌트에서 데이터를 공유할 수 있다. 주로 CSR 에서 데이터를 빠르게 사용할 수 있는 환경에서 유리하다</p>
<p><strong>React-Query</strong> : 서버 상태와 클라이언트 상태를 명확히 구분하여 서버에서 데이터를 받아오는 작업과 이를 관리하는 작업이 분리된다. 이는 서버 데이터의 동기화가 중요한 대규모 애플리케이션에 유리하다.</p>
<h4 id="3-데이터-안정성과-오류처리">3) 데이터 안정성과 오류처리</h4>
<p><strong>💡 데이터 안정성 **
**SWR</strong> : 최신 데이터를 즉시 업데이트하는 방식이므로 빠른 데이터 업데이트가 가능하지만, 
이로 인해 race condition이 발생할 수 있다.</p>
<p><strong>React-Query</strong> : 데이터 요청 빈도와 캐싱을 제어할 수 있어 데이터 안정성이 더 높으며 race condition 
발생 가능성을 줄인다.</p>
<p><strong>💡 에러 핸들링</strong>
<strong>SWR</strong> : 오류 처리 시스템이 없어, 사용자가 직접 오류 처리를 구현해야 한다.</p>
<p><strong>React-Query</strong> : 내장된 에러 핸들링 시스템이 있어, 애플리케이션에서 발생하는 오류를 쉽게 관리할 수 있다. 복잡한 오류 처리가 필요한 애플리케이션에서 유리하다.</p>
<h4 id="4-성능-및-최적화">4) 성능 및 최적화</h4>
<p><strong>💡 랜더링 최적화</strong>
<strong>SWR</strong> : 쿼리마다 개별적으로 컴포넌트를 업데이트 하기 때문에 쿼리 개수가 많으면 렌더링 성능이 떨어질 수 있다.</p>
<p><strong>React-Query</strong> : 여러 컴포넌트가 동일한 쿼리를 사용한 경우, 한번에 묶어서 업데이트 하여 성능이 더 뛰어나다. </p>
<p><strong>💡 캐싱 및 Garbage Collection</strong>
<strong>SWR</strong> : 자동으로 데이터를 캐싱하여 네트워크 요청을 줄일 수 있지만, 오래된 데이터를 관리하는 방법이 부족하다. </p>
<p><strong>React-Query</strong> : 캐싱에 대한 세밀한 제어가 가능하며, 사용되지 않는 쿼리를 자동으로 Garbage Collection 
할 수 있다. 데이터가 빈번하게 업데이트되는 환경에서 React-Query가 더 유리하다.</p>
<h3 id="5-결론">5. 결론</h3>
<p><strong>SWR</strong> : next.js에서 권장, 단순하고 가벼운 솔루션을 찾을 때 적합하다. 데이터가 자주 변경되지 않거나 캐싱이 간단한 프로젝트에 적절한다.</p>
<p><strong>React-Query</strong> : 데이터 캐싱 및 상태 관리를 세밀하게 제어해야 하거나 복잡한 애플리케이션에서 상태를 효율적으로 관리해야 할 때 적합하다. </p>
<p>참고 : <a href="https://velog.io/@clydehan/SWR-vs-Tanstack-QueryReact-Query-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C#-1-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EC%8B%9D">https://velog.io/@clydehan/SWR-vs-Tanstack-QueryReact-Query-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C#-1-%EA%B8%B0%EB%B3%B8-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EC%8B%9D</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-Query Optimistic UI]]></title>
            <link>https://velog.io/@sun_rim/React-Query-Optimistic-UI</link>
            <guid>https://velog.io/@sun_rim/React-Query-Optimistic-UI</guid>
            <pubDate>Thu, 20 Mar 2025 10:33:15 GMT</pubDate>
            <description><![CDATA[<h3 id="react-query-optimistic-updates">React-Query Optimistic Updates</h3>
<p>사용자 경험을 향상시키기 위해 서버 응답을 기다리지 않고 UI를 미리 업데이트하는 기법이다. 이를 통해 사용자는 서버 응답을 기다리는 동안 지연 시간을 느끼지 않고 피드백을 받을 수 있다.</p>
<p>장바구니 기능을 예로 들면</p>
<ol>
<li>사용자가 장바구니 담기 or 삭제를 누른다.</li>
<li>장바구니 api 요청을 서버로 전송한다.</li>
<li>서버로부터 요청에 대한 응답을 받는다.</li>
<li>받은 응답을 바탕으로 UI를 업데이트한다.</li>
</ol>
<p>Optimistic UI를 적용한 과정</p>
<ol>
<li>사용자가 장바구니 담기 or 삭제를 누른다.</li>
<li>장바구니 담기 api가 성공할 것이라고 가정하고 UI를 먼저 업데이트한다.</li>
<li>만약 에러가 발생하면 에러 메세지를 보여주고 UI 변경사항을 롤백한다.</li>
</ol>
<p>React-Query의 useMutation hook에서 제공하는 onMutate, onError, onSuccess, onSettled등의 옵션을 활용하면 UI를 미리 업데이트한 후 에러가 발생했을 경우 롤백하는 것도 가능해진다.</p>
<h3 id="optimistic-ui를-사용하는-이유">Optimistic UI를 사용하는 이유</h3>
<p>장바구니 기능을 사용할때 대부분은 즉각적인 반응을 보이지만, 다만 성능이 좋지 않는 디바이스를 사용하거나, PC에서 많은 작업을 동시에 처리 중이어서 일시적으로 느려진 상태, 또는 네트워크 환경이 좋지 않은 경우에는 즉각적인 반응이 나타나지 않아 오류가 발생한 것처럼 보일 수 있다.</p>
<p>여기서 OPtimistic Updates를 활용하여 API 응답ㅇ이 도착하기 전에 미리 UI에 변경사항을 반영해 놓고, 이후에 에러 처리나 상태 값을 업데이트하는 방식을 적용하면 실제 API 처리 시간은 비슷하더라도 사용자가 경험하는 체감 속도는 크게 개선될 것이다.</p>
<h3 id="optimistic-ui를-사용">Optimistic UI를 사용</h3>
<pre><code class="language-jsx">export const useShoppingCart = (
    options?: Omit&lt;UseMutationOptions&lt;AxiosResponse&lt;void&gt;, AxiosError, string&gt;, &#39;mutateKey&#39; | &#39;mutateFn&#39;&gt;,
) =&gt; {
    return useMutation([SHOPPING_CART_MUTATION_KEY], fetchShoppingCart, options);
};
</code></pre>
<pre><code class="language-jsx">const {mutateAsync: addShoppingCartAsync, isLoading: isLoadingAddShoppingCart } = useShoppingCart({
    onMutate: async() =&gt; {
        //뮤테이션 시작 전에 실행되는 콜백 함수
        //현재 쿼리 데이터를 백업하고 optimistic 업데이트를 수행
        await qeuryClinet.cancelQuerys([MATCH_LIST_QUERY_KEY, pageParams]);
        const previousData = queryClient.getQueryData&lt;{
            pages:{
                matchingList: MatchingStatuesListVO;
                page: PageState;
            }[];
        }&gt;([MATCH_LIST_QUERY_KEY, page Params]);

        //Optimistic 업데이트 수행
        queryClient.setQueryData([MATCH_LIST_QUERY_KEY, pageParams], () =&gt; {
            return{
                ...previusData,
                pages:[
                    {
                        ...previousData?.pages[0],
                        matchingList:{
                            ...previouData?.pages[0].matchingList.matchingList.map((item) =&gt; {
                                if(item.matchingSn === matchingSn){
                                    return{
                                        ...item,
                                        ShoppingYn:true,
                                    };
                                }
                                return item;
                            }),
                        },
                    },
                ],
            };
        });

        return {previuseDat};
    },

    onError:(_error, _newData, context) = &gt; {
        //뮤테이션 실패 시 실행되는 콜백 함수
        //백업된 데이터를 사용하여 이전 상태로 롤백
        queryClient.setQueryData(
            [MATCH_LIST_QUERY_KEY, pageParams],
            (context as {previousData: MatchingStatusDto[]}).previusData,
        );
    openToast({ iconType: &#39;info&#39;, type: &#39;error&#39;, content: &#39;에러 발생 시 나타나는 토스트&#39; })
   },
   onSuvvess: () =&gt; {
     openToast({ iconType: &#39;info&#39;, type: &#39;error&#39;, content: &#39;장바구니 담기 성공&#39; })
   },
   onSettled: () =&gt; {
        //뮤테이션 완료 후 실행되는 콜백 함수
        //쿼리 무효화 또는 리페칭 등의 작업 수행
        queryClient.invalidateQueries([MATCH_LIST_QUERY_KEY, pageParams]);
      },
    });</code></pre>
<ul>
<li>onMutate<ul>
<li>Optimistic Updates를 덮어씌우지 않기 위해 refetch하려는 쿼리를 cancel한다.</li>
<li>refetch 하려는 쿼리의 getQueryData 메서드로 이전 데이터를 가져오고, setQueryData메서드로 데이터를 업데이트 한다.</li>
</ul>
</li>
<li>onError<ul>
<li>에러가 발생한 경우 onMutate로 넘겨준 previousDatafmf setQueryData를 통해 이전 데이터로 롤백한다.</li>
</ul>
</li>
<li>onSettled<ul>
<li>모든 error 또는 success 이후에 원하는 쿼리를 refetch하도록 한다.</li>
</ul>
</li>
</ul>
<h3 id="결과">결과</h3>
<p>네트워크 속도가 빠를때와 에러가 발생하는 경우에는 두 가지 방식이 비슷하게 동작한다. 즉시 에러 발생한다면 적용 전에는 단순히 onError만 호출하면 되는 반면, 적용 후에는 onMutate에서 setQueryData로 UI관련 업데이트를하고, 이후 onError에서 다시 setQueryData를 호출하여 데이터를 롤백하는 차이가 있다.</p>
<p>UI가 먼저 업데이트 되니 사용자 입장에서는 빠르다고 느낄 수 있다.</p>
<h3 id="정리">정리</h3>
<p>Optimistic Updates를 활용하면 빠른 사용자 피드백이라는 이점도 얻으면서 혹시 에러가 발생하더라도 추가적인 Ui표시와 데이터 롤백이 가능하기에 좋은사용자 경험을 만들 수 있을 것 같다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://velog.io/@apparatus1/optimistic-updates">https://velog.io/@apparatus1/optimistic-updates</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Minimals UI]]></title>
            <link>https://velog.io/@sun_rim/Minimals-UI</link>
            <guid>https://velog.io/@sun_rim/Minimals-UI</guid>
            <pubDate>Fri, 15 Nov 2024 07:17:57 GMT</pubDate>
            <description><![CDATA[<h2 id="minimals-ui">Minimals UI</h2>
<h3 id="minimals-ui-폴더-구조">Minimals UI 폴더 구조</h3>
<ul>
<li>next-ts 또는 vite-ts 둘 중 하나를 선택해 프로젝트를 시작한다(starter 폴더는 테스트 폴더이다.)</li>
</ul>
<pre><code>Minimal_Typescript
  ├─ next-ts
  ├─ vite-ts
  ├─ starter-next-ts
  ├─ starter-vite-ts</code></pre><ul>
<li>.env 파일 생성(다른 서버를 사용할때는 그 서버에 맞는 API 넣음)</li>
</ul>
<pre><code class="language-html">//.env
NEXT_PUBLIC_HOST_API = http://localhost:7272/ (mock server Host API)</code></pre>
<ul>
<li>실행</li>
</ul>
<pre><code class="language-html">yarn run dev

or

npm run dev</code></pre>
<h3 id="mock-server-api-연동">Mock Server API 연동</h3>
<ul>
<li>프로젝트 시작 시 나오는 화면 → 로그인 클릭
<img src="https://velog.velcdn.com/images/sun_rim/post/3695e8a6-2f04-4c5b-8405-25affb7e04f5/image.png" alt=""></li>
</ul>
<ul>
<li><p>mock server 포트를 제대로 넣어도 404 또는 500이 뜸
<img src="https://velog.velcdn.com/images/sun_rim/post/abfe2e6c-2ac7-4272-91f0-dd180fcefd23/image.png" alt=""></p>
</li>
<li><p>mock server 프로젝트의 폴더 명을 endpoint와 동일하게 수정
<img src="https://velog.velcdn.com/images/sun_rim/post/52fba3de-60a4-4387-8ea4-7c76fef8d1d7/image.png" alt=""></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/c092a007-313b-49b7-95c0-60a8e38d2092/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/59a5f52c-29cd-4dbb-a9f8-ff441841b0c3/image.png" alt=""></p>
<ul>
<li>로그인이 안된다면 mock server의 _auth.ts 파일 확인</li>
</ul>
<h2 id="mock-server">Mock Server</h2>
<h3 id="1-mock-server">1. Mock Server?</h3>
<p>mock server는 실제 서버가 나오기 전 각종 기능들을 테스트하고 임의의 값을 받아오는 모의 서버 이다.</p>
<h3 id="2-setting">2. setting</h3>
<ul>
<li>Dropbox안 minimal-api-dev-v6.10.zip을 다운받는다(최신 버전)</li>
</ul>
<ul>
<li>yarn or npm 설치</li>
</ul>
<pre><code>yarn 

or

npm i</code></pre><ul>
<li>포트가 <a href="http://localhost:7272">localhost:7272</a> 올바른 포트에서 실행하고 있는지 확인</li>
</ul>
<pre><code>$&quot;dev&quot; : &quot;next dev -p 7272&quot;$
&quot;start: &quot;next start -p 7272&quot;</code></pre><ul>
<li>서버 실행</li>
</ul>
<pre><code>yarn run dev

or

npm run dev</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[호스팅]]></title>
            <link>https://velog.io/@sun_rim/%ED%98%B8%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@sun_rim/%ED%98%B8%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Wed, 23 Oct 2024 02:19:57 GMT</pubDate>
            <description><![CDATA[<h3 id="호스팅이란">호스팅이란?</h3>
<p>호스팅이란 웹사이트를 다른 사람들이 접속해 이용하도록 하기 위해서는 웹사이트의 글이나 이미지, 동영상 등을 저장할 수 있는 서버가 필요하다. 이 서버를 직접 구축하고 운영하는데 많은 큰 비용과 인력이 소모되는데 그때 외부의 어떤 서비스를 빌려서 서버를 사용한다는 말이다.</p>
<blockquote>
<p>서버를 관리하기 위해서는 24시간 내내 안정적으로 전기를 공급해야 하고, 빠르고 안정적인 인터넷 회선을 사용해야 하며, 철저한 보안 시스템을 갖추고 있어야 한다. 따라서 개인이 서버를 관리하기보다 전문 업체의 호스팅 서비스를 사용하는 것이 일반적이다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/a9c971e9-8f53-4f57-88d0-4c0930dd9bf7/image.png" alt=""></p>
<h3 id="웹-호스팅">웹 호스팅</h3>
<p><strong><code>여러 고객이 하나의 서버를 함께 사용하는 형태이다.</code></strong> 하나의 서버를 나누어 쓰기 때문에 저렴하게 이용할 수 있고, 호스팅 업체의 통합 관리를 받기에 편리하다. 그러나 사용할 수 있는 하드웨어가 제한적이라는 단점도 있다.</p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/2ea0cd03-a7ac-4549-91fc-e77decc1d90e/image.png" alt=""></p>
<blockquote>
<p>HTML CSS 코드를 이용해 웹 페이지를 만들었다고 해도 웹 페이지를 하나 제작했다고 해서 누구나 내 사이트에 접속할 수 있는 것이 아니다. 배포 과정을 거치고 도메인까지 연결해야 비로소 하나의 웹 페이지가 웹 사이트로 거듭나는 것이다. 이 배포 과정을 전문 업체에 맡기는 것이다.</p>
</blockquote>
<p>저렴한 가격으로 서버 및 인프라의 구축이 필요 없지만 서버의 일부분만 사용하기 때문에 사용량이 제한되고 서버 관리 권한이 없다. 따라서 소규모 웹사이트에 주로 사용된다.</p>
<h3 id="서버-호스팅">서버 호스팅</h3>
<p>고객이 단독 서버를 사용하는 형태이다. 넓은 하드웨어 공간을 사용할 수 있고 서버 운영/관리에 대한 직접적인 권한을 가질 수 있다. 또한, 빠른 데이터 전송 속도도 누릴 수 있다. 하지만 단독으로 서버를 이용하는 만큼 비용이 높은 편이다.</p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/5f51c8e6-3a67-4914-9688-d68bd10c3756/image.png" alt=""></p>
<p>웹 호스팅은 서버 일부만 빌리는 서비스라면 서버 호스팅은 서버 하나를 통째로 구매하여 서버 운영에 필요한 인프라와 기술력까지 제공받을 수 있는 서비스다. 서버 관리에 대한 직접권한을 갖고 서버를 단독으로 사용하기 떄문에 보안상으로도 유리하지만 초기 구축단계에서 웹 호스팅에 비해 시간과 비용이 많이 든다는 단점이 있다. 주로 회사 인트라넷, 대형 소핑몰 등 고정적으로 대용량 트래픽과 DB가 많이 사용되는 곳에 사용된다.</p>
<h3 id="클라우드-호스팅">클라우드 호스팅</h3>
<p>서버 호스팅과 비슷하지만 물리적 서버 장비가 아닌 가상 서버를 임대한다는 것에 차이가 있다. 따라서 자유롭게 서비스팩을 조절할 수 있고, 이용한 만큼만 금액을 지불하면 된다는 장점이 있다. 일시적인 트래픽 변동량이 많은 사이트에 적합한 호스팅이다.</p>
<p>클라우드 호스팅은 웹 호스팅의 장점과 서버 호스팅의 장점을 모두 가지고 있는 호스팅이며 최근 많은 주목을 받는 호스팅 방법이다. 아마존의 EC2와 구글의 클라우드 플랫폼 등 다양한 서비스가 존재하고 앉은 자리에서 클릭 몇번으로 누구나 서버를 생성하고 관리할 수 있고 트래픽 변동에 유연하게 대처 가능</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 18 주요 업데이트]]></title>
            <link>https://velog.io/@sun_rim/React-18-%EC%A3%BC%EC%9A%94-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@sun_rim/React-18-%EC%A3%BC%EC%9A%94-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Mon, 21 Oct 2024 04:43:36 GMT</pubDate>
            <description><![CDATA[<h2 id="필요-조건">필요 조건</h2>
<p>React 18의 새로운 기능을 사용하기 위해서는 createRoot API를 사용해야 한다.</p>
<pre><code class="language-jsx">//이전 버전
import React fron &#39;react&#39;;
import ReactDOM from &#39;react-dom&#39;;

ReactDOM.render(
    &lt;React.StrictMode&gt;
        &lt;App/&gt;
    &lt;/React.StrictMode&gt;
    document.getElementById(&#39;root&#39;)
);</code></pre>
<pre><code class="language-jsx">//리액트18 버전
import React fron &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;

const rootNode = document.getElementById(&#39;root&#39;)

ReactDOM.createRoot(rootNode).render(
    &lt;React.strictMode&gt;
        &lt;App/&gt;
    &lt;/React.strictMode&gt;
);</code></pre>
<p>기본 버전에서는 render만을 사용해 root를 생성했는데 React 18버전에서는 createRoot라는 새로운 Root API를 통해 root를 생성할 수 있게 되었다. createRoot API를 사용해 React 18버전의 새로운 기능과 API를 사용할 수 있게 된다.</p>
<p>ReactDOM을 불러오는 경로 또한 react-dom에서 <code>react-dom/client</code>로 변경된 것을 볼 수 있다.</p>
<h2 id="관련-기술">관련 기술</h2>
<p>React 18 새로운 기능 키워드</p>
<ul>
<li>Automatic Batching</li>
<li>Concurrent Feature</li>
<li>New Hooks</li>
</ul>
<h2 id="1-automactic-batching">1. Automactic Batching</h2>
<p>여러 상태 업데이트(setState)를 통합하여 단일 리렌더링으로 처리해 리렌더링의 성능을 개선한 기능이다.
React 18이전엔 useState, setState를 통해 상태를 업데이트하고 업데이트된 결과를 바탕으로 리렌더링을 진행했다. 이때 , state의 개수가 많아지면 그만큼 리렌더링도 많이 발생하기 때문에 불필요한 리렌더링이 많아지고 성능 이슈가 발생할 수 있었다. 그래서 도입된 기능이 배치처리인데 배치처리는 여러 상태 업데이트(setState)가 발생해도 상태를 하나로 통합해 리렌더링을 진행하는 방식이다. React 18이전 버전에서는 이벤트 핸들러와 생명주기 메소드를 사용할 댄 배치처리를 지원하지만 콜백 함수가 포함된 경우 배치 처리를 지원하지 않는다.</p>
<pre><code class="language-jsx">    //react 17 (리렌더링 두번 유발)
    function handleClick() {
        fetchSomething().then(() =&gt; {
            // React 17 이전의 버전에서는 해당 작업을 Batching 처리하지 않는다.
            // 왜냐하면 해당 작업은 이벤트가 종료된 이후 (100ms 뒤) 에 실행되기 때문이다.
            setCount((c) =&gt; c + 1); // 리렌더링 유발
            setFlag((f) =&gt; !f); // 리렌더링 유발
        });
    }</code></pre>
<pre><code class="language-jsx">//react 18 (리렌더링 한번으로 배칭)
function handleClick() {
        fetchSomething().then(() =&gt; {
            // React 18 이후에서는 해당 작업을 Batching 처리 한다.
            setCount((c) =&gt; c + 1);
            setFlag((f) =&gt; !f);
            // React 는 해당 작업을 일괄 처리하여 한 번의 리렌더링만 진행한다.
        });
    }</code></pre>
<h3 id="각각의-batching">각각의 batching</h3>
<pre><code class="language-jsx">const changeState = async () =&gt; {
    if (testRef.current) {
      clearTimeout(testRef.current);
      testRef.current = null;
    }

      // 요 안에서 한 개의 update function 은 하나로 batching 됨.
      trigger();
      trigger();

      // 1.5초 후에는 4개의 update function 을 하나로 batching 시킴.
      setTimeout(() =&gt; {
        trigger();
        trigger();
      }, 1500);
      setTimeout(() =&gt; {
        trigger();
        trigger();
      }, 1500);

      testRef.current = setTimeout(() =&gt; {
        // 1초 후에는 두 개의 update function 을 하나로 batching 시킴
        trigger();
        trigger();
      }, 1000);
      return;
  }</code></pre>
<p>changeState 함수는 onClick 이벤트 핸들러를 통해 호출되며, 버튼 클릭 시 두 차례에 걸쳐 리렌더링이 발생한다. 이유는 이벤트 핸들러 내에 위치한 state update 작업은 하나의 queue에 묶여 batching처리되고, 
이후 timeout이 끝난 후 실행되는 두 개의 state update 작업은 따로 queue에 묶여 리렌더링을 유발시키기 
때문이다. 중요한 것은 위 작업이 하나의 queue에 batching 되지 않는다는 점이다. 이벤트 핸들러에서 실행된 setState와 setTimeout을 기반으로 실행된 setState는 서로 batching 되지 않고 독립적으로 실행된다. 
한 가지 신기한 점은 이벤트 핸들러에서 실행되었고 딜레이가 같은 두 개의 setTimeut 내 update funcion도 하나로 batching 되었다는 점이다.</p>
<h3 id="reactdomflushsync">ReactDOM.flushSync()</h3>
<p>react-dom 라이브러리에 추가된 ReactDOM.flushSync() 메서드는 Auto Batching을 무시하고 즉시 DOM을 렌더링해준다. React에서는 공식적으로 해당 메서드의 사용을 추천하진 않으며 필요한 상황이 있을 경우에만 사용할 것을 강조했다.</p>
<pre><code class="language-jsx">import { flushSync } from &quot;react-dom&quot;;

function handleClick() {
    // React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
    flushSync(() =&gt; {
        setCounter((c) =&gt; c + 1);
    });
    // React 는 flushSync 메서드가 실행되는 즉시 DOM을 업데이트 한다.
    flushSync(() =&gt; {
        setFlag((f) =&gt; !f);
    });
    // 따라서 해당 함수가 실행될 경우 React는 총 두 번의 리렌더링을 수행한다.
}</code></pre>
<h2 id="2-concurrent-feature">2. Concurrent Feature</h2>
<p>Concurrent Feature을 알아보기 전, Concurrent Mode라는 개념에 대해 먼저 알아야한다. Concurrent Mode는 자바스크립트가 싱글 스레드 기반의 언어이기 때문에 여러 작업을 동시에 처리할 수 없는 문제점을 보완하기 위해 나왔다. </p>
<p>React 또한 JS 기반이기 때문에 동일한 문제점이 발생했고 Concurrent Mode(동시성)을 통해 이를 해결하고자 했다. Concurrent Mode는 여러 작업을 작은 단위로 나눠 우선순위를 정해 그에 따라 작업을 번갈아 수행하는 것을 말한다. 동시에 수행되는 것은 아니지만 작업 간 전환이 빠르기 때문에 동시에 수행되는 것처럼 보여 사용자 경험을 UX적으로 향상시킨다. </p>
<p>이러한 배경에서 탄생한 Concurrent Feature은 Concurrent Mode에서 용어가 바뀐 것으로 이번 React 18 업데이트에서 가장 중요한 추가 기능이다. 또한  동시성을 효율적으로 진행할 수 있는 기능인 Suspense와 Transitions를 지원한다. 해당 기능들은 한 번에 여러 동작을 수행해 사용자 경험을 향상시킬 수 있다.</p>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/33a2cd7a-ac48-426e-b754-c4f853eff370/90f8e57e-5a64-475d-9f2d-73938bef3632/image.png" alt="image.png"></p>
<h2 id="2-1-suspense">2-1. Suspense</h2>
<p>Suspense는 사용자에게 보여주고 싶은 컴포넌트를 먼저 렌더링 할 수 있게 하는 기능이다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>렌더링 작업과 비동기 데이터 호출 과정이 동시에 이루어진다.<ul>
<li>비동기 데이터를 호출하는 과정, fallback UI를 보여주는 과정, 완성된 UI를 보여주는 과정 등 기존의 렌더링 과정들이 여러 작은 태스크들로 쪼개진 뒤 번갈아가며 진행된다.</li>
</ul>
</li>
<li>비동기 데이터 호출을 통해 로딩이 발생하면 Suspense가 이를 포착하여 UI는 fallback으로 보여주고 로딩이 완료되면 완성된 UI를 보여준다.<ul>
<li>이를 통해 컴포넌트 내부에선 로딩 상태에 대한 분기 처리가 필요없어져 코드의 가독성이 높아진다.</li>
<li>비동기 데이터에 대한 분기처리로 인해 waterfall 현상 역시 사라진다.</li>
</ul>
</li>
</ul>
<h2 id="2-2-transitions">2-2. Transitions</h2>
<p>Transition은 setState(상태 업데이트)의 우선순위를 구분한다. Urgent updates는 입력, 클릭, 누르기와 같은 사용자가 직접적인 상호 작용을 하는 기능을 말하고 Transition updates(non-urgent)는 UI 전환과 같은 기능을 말한다.</p>
<h3 id="usetransition새롭게-나온-리액트-훅">useTransition(새롭게 나온 리액트 훅)</h3>
<p>isPending 상태 값을 가져와 렌더링 결과를 분기 처리하는 기능이다. 여기서 isPending은 state를 업데이트했음에도 리렌더링 하지 않고 UI를 잠시 유지하는 상태를 말한다. isPending은 boolean 값으로 transition 완료를 알려주게 된다.</p>
<h3 id="starttranstion">startTranstion</h3>
<p>내부에 존재하는 상태 업데이트는 모두 전환 업데이트일때 우선순위가 높은 상태 업데이트가 발생할 경우 내부에 선언한 상태 업데이트는 중단된다.</p>
<pre><code class="language-jsx">const [isPending, startTransition] = useTransition();
const [one, setOne] = useState&lt;number&gt;(0);
const [two, setTwo] = useState&lt;number&gt;(0);

const onClick = () =&gt; {
    setOne(one + 1);// 긴급 업데이트// startTransition으로 전환 업데이트 선언// 내부에 있는 상태 업데이트는 모두 전환 업데이트
    startTransition(() =&gt; {
          setTwo(two + 2);// 전환 업데이트로 setOne보다 우선순위가 낮음
    });
};

return (
    &lt;&gt;
        &lt;button onClick={onClick} /&gt;
        &lt;button disabled={isPending} /&gt;
    &lt;&gt;
);</code></pre>
<p>isPending의 값이 true일 경우 startTransition 내부에 있는 setTow가 우선순위에서 밀리게 된다. setOne의 상태 업데이트 진행시 isPending의 값은 ture이다. setOne의 상태 업데이트가 완료되면 isPending의 값이 false가 되며 setTwo의 상태 업데이트를 시작한다.</p>
<h2 id="3-new-hooks">3. New Hooks</h2>
<h3 id="useid">useId()</h3>
<p>서버와 클라이언트에서 사용될 고유 ID를 생성하며 key를 생성하는 것은 아니다.</p>
<ul>
<li>id를 하나만 전달</li>
</ul>
<pre><code class="language-jsx">function CreateID() {
  const id = useId();

  return &lt;input id={id} type=&quot;text&quot; /&gt;;
};</code></pre>
<ul>
<li>id를 두 개 이상 전달할 때는 접미사를 추가해 아이디 구분</li>
</ul>
<pre><code class="language-jsx">function CreateID() {
  const id = useId();

  return (
        &lt;input id={id + &#39;-firstID&#39;} type=&quot;text&quot; /&gt;
        &lt;input id={id + &#39;-secondID&#39;} type=&quot;text&quot; /&gt;
    );
};</code></pre>
<h3 id="usedeferredvalue">useDeferredValue()</h3>
<p>트리에서 긴급하지 않은 부분의 리렌더링을 연기하는 기능을 제공한다. 고정된 지연 시간이 없고 긴급한 작업이 완료된 후 즉시 실행하며 사용자 입력을 차단하지 않는다. </p>
<pre><code class="language-jsx">import { useDeferredValue, useState } from &quot;react&quot;;

const SlowUI = () =&gt; (
  &lt;&gt;
    {Array(50000)
      .fill(1)
      .map((_, index) =&gt; (
        &lt;span key={index}&gt;{100000} &lt;/span&gt;
      ))}
  &lt;/&gt;
);

function App() {
  const [value, setValue] = useState&lt;string&gt;(&quot;&quot;);
  const deferredValue = useDeferredValue(value);// 긴급하지 않은 값으로 지정

  const handleClick = (e: any) =&gt; {
    setValue(e.target.value);
  };

  return (
    &lt;&gt;
      &lt;input onChange={handleClick} /&gt;
      &lt;div&gt;DeferredValue: {deferredValue}&lt;/div&gt;
      &lt;div&gt;
        &lt;SlowUI /&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre>
<p>value가 변경되더라도 deferredValue에는 변경된 값이 아닌 이전 값을 리턴한다. 사용자의 인터랙션이 종료되면 그때 새로운 값을 리턴하게 된다.</p>
<h3 id="ref">Ref.</h3>
<p><a href="https://tech.nexr.kr/e14cd3fa-58be-4983-9e54-a1d157af08c2">https://tech.nexr.kr/e14cd3fa-58be-4983-9e54-a1d157af08c2</a></p>
<p><a href="https://velog.io/@seungchan__y/React-18-Concurrent-%EB%A7%9B%EB%B3%B4%EA%B8%B0">https://velog.io/@seungchan__y/React-18-Concurrent-맛보기</a></p>
<p><a href="https://velog.io/@rookieand/React-18%EC%97%90%EC%84%9C-%EC%B6%94%EA%B0%80%EB%90%9C-Auto-Batching-%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80#%EF%B8%8F-batching-%EC%9D%B4%EB%9E%80">https://velog.io/@rookieand/React-18에서-추가된-Auto-Batching-은-무엇인가#️-batching-이란</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[attribute와 property]]></title>
            <link>https://velog.io/@sun_rim/attribute%EC%99%80-property</link>
            <guid>https://velog.io/@sun_rim/attribute%EC%99%80-property</guid>
            <pubDate>Thu, 13 Jun 2024 04:52:57 GMT</pubDate>
            <description><![CDATA[<h3 id="html의-attribute">HTML의 Attribute</h3>
<p>어트리뷰트는 HTML의 속성이다. 엘리먼트에 아이디나 클래스와 같은 추가적인 정보를 일컫는다고 보면 된다.</p>
<pre><code class="language-html">&lt;!-- div 엘리먼트의 id와 class 속성은 어트리뷰트 이다 --&gt;
&lt;div id=&quot;inpa&quot; class=&quot;bold&quot;&gt;&lt;/div&gt;

&lt;!-- input 엘리먼트의 type과 value 속성은 어트리뷰트 이다 --&gt;
&lt;input type=&quot;text&quot; value=&quot;0&quot;&gt;</code></pre>
<h3 id="dom의-property">DOM의 Property</h3>
<p>프로퍼티는 DOM의 속성이다. 즉, html의 attribute를 DOM 내에서 대신해서 표현이라고 보면 된다.</p>
<pre><code class="language-html">&lt;div class=&#39;my-class&#39; style=&quot;color: red;&quot;&gt;&lt;/div&gt;

&lt;script&gt;
    // className과 style은 프로퍼티이다
    document.querySelector(&#39;div&#39;).className; // &quot;my-class&quot;
    document.querySelector(&#39;div&#39;).style.color; // &quot;red&quot;
&lt;/script&gt;</code></pre>
<h2 id="attribute-vs-property-기능-차이">Attribute vs Property 기능 차이</h2>
<h3 id="엘리먼트-속성-접근-차이">엘리먼트 속성 접근 차이</h3>
<p>attribute는 html 안에서 property는 DOM tree 안에서 존재한다. 이것이 뜻하는 바는 attribute는 정적으로 변하지 않고 property는 동적으로 그 값이 변할 수 있다는 의미를 내포하고 있다. 예를 들어 체크 박스 태그가 있을 때 유저가 체크박스에 체크를 하면 attribute의 상태는 변하지 않지만 property의 상태는 checked로 변하게 된다.</p>
<p>아래 input 태그가 있고 value 어트리뷰트로 0 값을 가지고 있다.</p>
<pre><code class="language-html">&lt;input type=&quot;text&quot; value=&quot;0&quot;&gt;</code></pre>
<p>이를 자바스크립트 DOM 문법을 통해서 input 엘리먼트에 접근해 value 속성 값을 가져올 수 있다. 여기서 input.value는 프로퍼티이다.</p>
<pre><code class="language-jsx">const input = document.querySelector(&#39;input&#39;);
console.log(input.value); // &#39;0&#39;</code></pre>
<p>이번엔 value 속성값을 임의의 값으로 변경해보자. input.value의 값을 재설정하고 다시 확인해보면 변경된 값으로 설정 되었음을 확인할 수 있다.</p>
<pre><code class="language-jsx">input.value = 66;
console.log(input.value); // &#39;66&#39;</code></pre>
<p>그런데 HTML 태그를 보면 여전히 value의 값은 0인채 그대로 있는걸 볼 수 있다.</p>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/33a2cd7a-ac48-426e-b754-c4f853eff370/ee97cf68-3baa-45f1-b382-cf9b21979042/Untitled.png" alt="Untitled"></p>
<p>위에서 말한 어트리뷰트는 정적이고 프로퍼티는 동적 이라는 말이 바로 이것이다. 어트리뷰트와 프로퍼티는 둘이 연결되어 있긴 하지만 엄밀히 다른 녀석이라, 프로퍼티의 값을 변경해도 DOM객체만 업데이트 되고 HTML은 업데이트 되지 않아 어트리뷰트의 값은 그대로 인 것이다.</p>
<aside>
💡 DOM은 JavaScript 모델이므로 굳이 HTML 속성을 계속 업데이트할 필요가 없다. 실제로 리소스 낭비가 될 수 있어 이런식으로 설계 되었다고 보면 된다.

</aside>

<p>따라서 DOM과 HTML 둘다 특정 속성에 대해 값을 변경하고 싶다면, setAttribute() 메서드를 통해 업데이트 해주면 된다.</p>
<pre><code class="language-jsx">const input = document.querySelector(&#39;input&#39;);

input.setAttribute(&#39;value&#39;, 99);

console.log(input.value); // &#39;99&#39;</code></pre>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/33a2cd7a-ac48-426e-b754-c4f853eff370/78c93517-3ee1-4f7d-bd59-49b845cfd3ce/Untitled.png" alt="Untitled"></p>
<h3 id="사용자-정의-속성-접근-차이">사용자 정의 속성 접근 차이</h3>
<p>HTML에 미리 정의된 태그의 속성 뿐만 아니라 개발자가 임의로 엘리먼트에 사용자 정의 속성을 만들어 넣을 수도 있다.</p>
<pre><code class="language-jsx">&lt;!-- 실제 html에 없는 사용자 정의 임의 속성 custom --&gt;
&lt;input type=&quot;text&quot; value=&quot;0&quot; custom=&quot;9999&quot;&gt;</code></pre>
<p>그런데 프로퍼티의 특징으론 엘리먼트가 가진 모든 속성이 프로퍼티로 되지는 않다는 점이다. HTML에 지정된 속성들은 프로퍼티로도 표현되지만, 엘리먼트에 임의로 지정한 사용자 정의 속성은 프로퍼티로 자동으로 표현되지 않는다. 실제로 자바스크립트로 접근해보면 9999가 아닌 undegined라는 결과괎을 내뱉는다.</p>
<pre><code class="language-jsx">const input = document.querySelector(&#39;input&#39;);
console.log(input.custom); // undefined</code></pre>
<p>이러한 사용자 정의 속성 값에 접근하려면 getArrtibute()와 setAttribute()를 사용하면 된다.</p>
<pre><code class="language-jsx">console.log(input.getAttribute(&#39;custom&#39;)); // &#39;9999&#39;</code></pre>
<h3 id="성능-차이">성능 차이</h3>
<p>일반적으로 프로퍼티 접근이 getAttribute() 와 setAttribute() 메서드보다 약간 더 빠르다. 따라서 사용자 정의 속성과 같이 별도의 프로퍼티가 없는 경우에만 메서드를 이용하고 그외에는 프로퍼티로 접근하는 방법이 이상적이라고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안녕 recoil...]]></title>
            <link>https://velog.io/@sun_rim/%EC%95%88%EB%85%95-recoil</link>
            <guid>https://velog.io/@sun_rim/%EC%95%88%EB%85%95-recoil</guid>
            <pubDate>Wed, 20 Mar 2024 01:32:42 GMT</pubDate>
            <description><![CDATA[<p>상태관리 라이브러리인 Recoil이 업데이트를 중단했다... 한참 전 부터 기질이 있긴 했지만 정말 중단되며 나는 큰 고민에 빠졌다. atom에 익숙해져있던 나는 Redux를 쓰기에는 배우는데 시간이 꽤 걸릴것 같아 고민을 하던중 atom 기반의 Jotai를 발견하게 되었다!
Jotai에 대해 알아보기 전 친구는 나에게 물었다.</p>
<blockquote>
</blockquote>
<p>🤷🏻‍♀️ : 라이브러리가 업데이트 중단 됐다고 해도 걍 쓰면 되는거 아님? 왜 바꿈?
나 : 엄.... 그러게... </p>
<p>그래서 알아보았다 업데이트 중단된 라이브러리를 쓰면 안되는이유</p>
<ol>
<li>보안 
개발자들은 보안 취약점이 발견될 때마다 라이브러리 업데이트를 통해 이를 수정하는데 더 이상 업데이트가 되지 않는다면 라이브러리는 취약점에 대한 보안 패치를 받지 못한다. 이는 시스템 또는 어플에 보안 위험을 초래할 수 있다.</li>
<li>지원종료
더 이상 업데이트되지 않는 라이브러리의 개발자가 지원을 중단한다면 멀쩡히 살아있던 내 프로젝트가 한순간에 띡- 하고 오류를 내뱉을 수 있다. 만약 프로젝트 규모가 크고 사용자가 많은 서비스라면....? 더 이상 알아보지 않겠다..</li>
<li>호환성 
새로운 버전의 언어나 다른 의존성 라이브러리의 업데이트에 따라 이전 버전의 라이브러리가 호환성 문제를 일으킬 수 있다. 이로 인해 어플이 예기치 않게 동작하거나 오류가 발생할 수 있다<h3 id="jotai에-대해-알아보자">Jotai에 대해 알아보자</h3>
Jotai는 2020년 Pedro Nauck에 의해 처음 소개된 React를 위한 상태 관리 라이브러리로, Redux나 Modx와 같은 다른 인기있는 상태 관리 라이브러리의 대안으로 등장하게 되었다. Jotai의 주요 아이디어는 최소한의 API를 제공하여 React에서 상태 관리를 간단하게 만드는 것이다.</li>
</ol>
<p>Jotai의 사용법을 알아보기 전 Redux..Modx와 같은 상태관리에 대한 접근 방식을 알아보겠다</p>
<h3 id="1-상태관리-flux-vs-atomic">1. 상태관리, Flux vs Atomic</h3>
<blockquote>
<ul>
<li>Flux (Redux, Zusstand)</li>
</ul>
</blockquote>
<ul>
<li>Proxy (Mobx, Valtio)</li>
<li>Atomic (Jotai, Recoil)</li>
</ul>
<p><strong>Flux</strong>
일반적으로 많이 쓰는 Flux 패턴인 리덕스를 보면 액션을 통해 앱 상태를 변화시키고, 컴포넌트는 selector를 사용해 전역 상태의 일부를 subscribing 하는 형태로 동작한다. 리덕스 환경에서 stor에 있는 값을 변경하기 위해서는 액션 함수를 실행하고 특정 액션 타입을 리듀서에 전달하는 방식으로 변화를 전달해야 한다. 그러다보니 보일러플레이트가 아주 많고 데이터를 변화시키기 위해 작성해야 하는 코드 양도 많다.</p>
<p><strong>Proxy</strong>
Mobx에서는 전체 상태에 대한 엑세스를 제공하고, 컴포넌트에서 사용되는 일부 상태를 자동으로 감지하고 업데이트만 인지해서 사용한다. 리덕스보다 디버깅은 어렵지만 store에 있는 데이터를 바로 변경할 수 있는 Mobx가 더 편하게 느껴질 때도 많을것을다.</p>
<p><strong>Atomic</strong>
Jotai와 recoil 같은 Atomic 접근 방식은 React애서 사용되는 state와 비슷하게 리액트 트리안에서 상태를 저장하고 관리하는 방법이다. </p>
<h3 id="jotai를-사용해보자">Jotai를 사용해보자</h3>
<pre><code class="language-javascript">import {atom} from &quot;jotai&quot;

const countAtom = atom(0)
const mangaAtom = atom({&#39;label&#39;: &#39;&#39;, &#39;content&#39;:&#39;&#39;});

const [value, setValue] = useAtom(countAtom)
</code></pre>
<p>이런식으로 Jotai를 사용하면되는데 기존의 Recoil 방식은 key와 디폴트값을 정해서 코드를 선언해놓는 반면 Jotai는 한줄로 선언이 가능하다. <del>recoil보다 편한데..?</del></p>
<pre><code class="language-javascript">import {useAtom} from &quot;jotai&quot;

function Counter(){
  const [count, setcount] = useAtom(countAtom);

  function increment(){
    setCount((c) =&gt; c + 1);
  }

  return(
   &lt;div&gt;
     &lt;p&gt;Count : {count}&lt;/p&gt;
     &lt;button onClick={increment}&gt;Imcrement&lt;/button&gt;
   &lt;/div&gt;
  );
}</code></pre>
<p>간단하게 Counter 컴포넌트를 만들어보았다. useAtom 훅을 사용하여 상태에 접근하고, setCount 함수를 사용해서 상태를 업데이트한다. setCount 함수가 호출될 때마다 counterAtom을 사용하는 모든 컴포넌트가 리랜더링된다.</p>
<h3 id="jotai-장점">Jotai 장점</h3>
<ol>
<li>Jotai는 React에서 상태를 간단하게 관리할 수 있도록 해주는 최소한의 API를 제공한다. 따라서 Redux와 같은 다른 상태 관리 라이브러리보다 더 가볍고 간단하게 사용할 수 있다.</li>
<li>Atom은 여러 컴포넌트에서 사용할 수 있으므로, Redux와 같이 중앙 집중식 저장소에 모든 상태를 보관하는 것보다 더 유여한게 상태를 관리할 수 있다.</li>
</ol>
<h3 id="jotai를-사용할-때-고려해야-할-점">Jotai를 사용할 때 고려해야 할 점</h3>
<p>Jotai는 아직 상대적으로 새로운 라이브러리며, 커뮤니티가 크지 않다. 또한 Redux와 같은 다른 상태관리 라이브러리보다 덜 검증되었다. 따라서 Jotai를 사용하기 전에 프로젝트의 요구 사항과 함께 검토해 보는 것이 좋을 것 같다.</p>
<h3 id="마치며">마치며</h3>
<p>Recoil을 jotai로 모두 리팩토링 해야하는데 진짜 큰일났다... </p>
<p>참고자료
<a href="https://youngme92.vercel.app/blog/react-jotai">https://youngme92.vercel.app/blog/react-jotai</a>
<a href="https://velog.io/@ggong/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-jotai">https://velog.io/@ggong/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-jotai</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[라이브러리 의존성 없애기]]></title>
            <link>https://velog.io/@sun_rim/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%86%EC%95%A0%EA%B8%B0</link>
            <guid>https://velog.io/@sun_rim/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%97%86%EC%95%A0%EA%B8%B0</guid>
            <pubDate>Thu, 08 Feb 2024 08:36:19 GMT</pubDate>
            <description><![CDATA[<p>글을 시작하기 전 썸네일 이거 제가 만들었어요 <del>~</del>😏
<a href="https://github.com/HAERIM0/Velog_Thumbnail_Maker">썸네일 메이커가 궁금하다면 ?</a></p>
<p>저는 라이브러리를 정말 좋아합니다. 왜냐구요? 개발하기 편하니까요..</p>
<p>하지만 우연히 링크드인에서 백혜인님의 라이브러리에 의존하면 안되는 이유를 보게 되었습니다. 
라이브러리는 언제든지 사라질 수 있고 유지보수 적으로도 좋지 않다는 것을 알게되었습니다. 
개발하는 그 잠시 편하려고 라이브러리를 쓰면 배보다 배꼽이 커지는 일이 발생할 수 있다는것을요... 
하지만 그렇다고 해서 라이브러리가 &quot;나쁘다&quot;가 아닌 잘~ 사용하자 라는 생각으로 글을 쓰게 되었습니다.</p>
<p><strong>이번에 뿌실 라이브러리는 react-calendar입니다.</strong></p>
<pre><code class="language-javascript">import Calendar from &quot;react-calendar&quot;;
import &quot;react-calendar/dist/Calendar.css&quot;;
import moment from &quot;moment&quot;;

type ValuePiece = Date | null;

type Value = ValuePiece | [ValuePiece, ValuePiece];

interface CalendarProps {
  setUploadDate: Dispatch&lt;SetStateAction&lt;string&gt;&gt;;
}

const Calendars = ({ setUploadDate }: CalendarProps) =&gt; {
  const [value, onChange] = useState&lt;Value&gt;(new Date());

  const handleDateChange = (newValue: Value) =&gt; {
    if (newValue instanceof Date) {
      const formattedDate = new Date(
        newValue.getTime() - newValue.getTimezoneOffset() * 60000
      )
        .toISOString()
        .split(&quot;T&quot;)[0];

      setUploadDate(formattedDate);
      onChange(newValue);
    }
  };

  return (
    &lt;S.CalendarContainer&gt;
      &lt;Calendar
        onChange={handleDateChange}
        value={value}
        className=&quot;Calender&quot;
        formatDay={(locale, date) =&gt; moment(date).format(&quot;DD&quot;)}
        minDetail=&quot;month&quot;
        maxDetail=&quot;month&quot;
        showNeighboringMonth={false}
      /&gt;
      &lt;div style={{ display: &quot;flex&quot;, justifyContent: &quot;flex-end&quot; }}&gt;
        &lt;button
          style={{
            position: &quot;absolute&quot;,
            top: &quot;37.5%&quot;,
            width: &quot;50px&quot;,
            border: 0,
            zIndex: 999,
          }}
        &gt;
          X
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/S.CalendarContainer&gt;
  );
};

export default Calendars; </code></pre>
<p>원래는 react-calendar를 사용하여 이런식으로 달력을 사용했습니다. 하지만 css 커스터마이징에 제약사항이 존재해 개발자가 대충 프로젝트에 갖다 쓰는 용도로는 적합하지만 디자이너가 있는 저희 팀에서는 부적합했습니다.</p>
<p>디자이너님께 달력 디자인을 바꿔달라 할까 고민을 많이 했지만 라이브러리도 뿌실겸 직접 만들어 보게 되었습니다.</p>
<p>아 그런데요 저는 캘린더 라이브러리를 쓰지 않겠다고 했지 날짜 라이브러리를 쓰지 않겠다고는 안했습니다..ㅋ😏</p>
<p>또한 자바스크립트의 Date객체가 있지만 동작을 쉽게 예측할 수 없으니 검증된 라이브러리를 쓰는것이 더 옳다고 생각했습니다!!</p>
<pre><code class="language-javascript">// useCalendars.ts
import { useState } from &quot;react&quot;;
import { getDaysInMonth } from &quot;date-fns&quot;;

const DATE_MONTH_FIXER = 1;
const CALENDER_LENGTH = 35;
const DEFAULT_TRASH_VALUE = 0;
const DAY_OF_WEEK = 7;
const DAY_LIST = [&quot;일&quot;, &quot;월&quot;, &quot;화&quot;, &quot;수&quot;, &quot;목&quot;, &quot;금&quot;, &quot;토&quot;];

const useCalendars = () =&gt; {
  const [currentDate, setCurrentDate] = useState(new Date());
  const totalMonthDays = getDaysInMonth(currentDate);

  const prevDayList = Array.from({
    length: Math.max(0, currentDate.getDay()),
  }).map(() =&gt; DEFAULT_TRASH_VALUE);
  const currentDayList = Array.from({ length: totalMonthDays }).map(
    (_, i) =&gt; i + 1
  );
  const nextDayList = Array.from({
    length: CALENDER_LENGTH - currentDayList.length - prevDayList.length,
  }).map(() =&gt; DEFAULT_TRASH_VALUE);

  const currentCalendarList = prevDayList.concat(currentDayList, nextDayList);
  const weekCalendarList = currentCalendarList.reduce(
    (acc: number[][], cur, idx) =&gt; {
      const chunkIndex = Math.floor(idx / DAY_OF_WEEK);
      if (!acc[chunkIndex]) {
        acc[chunkIndex] = [];
      }
      acc[chunkIndex].push(cur);
      return acc;
    },
    []
  );

  return {
    weekCalendarList: weekCalendarList,
    currentDate: currentDate,
    setCurrentDate: setCurrentDate,
  };
};

export default useCalendars;</code></pre>
<p>이렇게 커스텀 훅을 만들었습니다. 핵심은 달력의 시작과 끝에 적절한 트래쉬값을 붙여주고
이를 reduce를 이용하여 달력으로 렌더링하기 편한 2중배열 형태로 변환해주는 작업이라고 할 수 있습니다.</p>
<p>useState의 반환값과 현재 위치한 날짜의 달력정보를 담고있는 2차원 배열
weekCalendarList가 반환되고 길이 7인 배열 5개를 튜플로 가지는 2차원 배열이 담겨있습니다.</p>
<p>차례대로 일,월,화 .... 이고 만약 차례대로 날짜가 채워지지 않는다면 트래쉬값 0이 들어가게 됩니다.</p>
<p><strong>저는 월요일이 시작이 아닌 일요일을 시작으로 잡았기 때문에 prevDayList에서 getDay() - 1을 해주지 않고 getDay()만 불러왔습니다. 만약 월요일부터 시작하려면 getDay() - 1을 해주면 날짜가 월요일 부터 시작 할 수 있습니다.</strong></p>
<p>또한 위에 있는 constants는 렌더링 또는 함수 호출 시 다시 계산될 여지가 있어 따로 선언해서 import 해오시기 바랍니다.</p>
<pre><code class="language-javascript">import * as S from &quot;./style&quot;;
import useCalendars from &quot;hooks/common/Calendars/useCalendars&quot;;
import { isToday, subMonths } from &quot;date-fns&quot;;
import RightButton from &quot;assets/img/common/R.svg&quot;;

const Calendar = () =&gt; {
  const { weekCalendarList, currentDate, setCurrentDate, DAY_LIST } =
    useCalendars();

  return (
    &lt;S.CalendarContainer&gt;
      &lt;S.CalendarTitleBox&gt;
        &lt;이전버튼
          onClick={() =&gt; {
            setCurrentDate(subMonths(currentDate, 1));
          }}
        &gt;
          이전으로
        &lt;/이전버튼&gt;

        &lt;년도&gt;
          {currentDate.toLocaleDateString(&quot;ko-KR&quot;, {
            year: &quot;numeric&quot;,
            month: &quot;long&quot;,
          })}
        &lt;/년도&gt;

        &lt;다음버튼
          src={RightButton}
          onClick={() =&gt; {
            setCurrentDate(subMonths(currentDate, -1));
          }}
        &gt;
          다음으로
        &lt;/다음버튼&gt;
      &lt;/S.CalendarTitleBox&gt;

      &lt;월&gt;
        {DAY_LIST.map((day, index) =&gt; (
          &lt;th key={index}&gt;{day}&lt;/th&gt;
        ))}
      &lt;/월&gt;

      &lt;날짜&gt;
        {weekCalendarList.map((week, weekIndex) =&gt; (
          &lt;tr key={weekIndex}&gt;
            {week.map((day, dayIndex) =&gt; (
              &lt;div
                key={dayIndex}
                className={
                  isToday(
                    new Date(
                      currentDate.getFullYear(),
                      currentDate.getMonth(),
                      day
                    )
                  )
                    ? &quot;today&quot;
                    : &quot;&quot;
                }
              &gt;
                {day &gt; 0 ? day : &quot;&quot;}
              &lt;/div&gt;
            ))}
          &lt;/tr&gt;
        ))}
      &lt;/날짜&gt;
    &lt;/S.CalendarContainer&gt;
  );
};

export default Calendar;</code></pre>
<p>여러분이 이해하기 쉽도록 쓰는 곳에서는 이렇게 사용을 해봤습니다. 가져와서 쓰는 코드가 복잡해보이지만 생각보다 간단합니다. date-fns 라이브러리의 isToday를 사용하여 오늘의 날짜에 색을 입혔고, 지금은 딱 레이아웃만 잡은 상태이며 styled-component를 사용해서 디자인을 조금 더 입히면 완성입니다.</p>
<h3 id="마치며">마치며</h3>
<p>많은 양을 구글링과 함께해 잘짠 코드라고 할 순 없지만 이렇게 또 라이브러리를 떄려봤습니다.
제가 작성한 코드가 여러분에게 큰 영향을 미칠 수 있을때 까지 열심히 하겠습니다 🙇‍♀️</p>
<p>참고 :
<a href="https://xionwcfm.tistory.com/420">https://xionwcfm.tistory.com/420</a>
<a href="https://www.linkedin.com/feed/update/urn:li:activity:7160305839780589568/">https://www.linkedin.com/feed/update/urn:li:activity:7160305839780589568/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HeadlessComponent 리팩토링]]></title>
            <link>https://velog.io/@sun_rim/%EC%9D%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%93%9C%EB%94%94%EC%96%B4-%ED%83%88%EC%B6%9C%ED%95%A9%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@sun_rim/%EC%9D%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%93%9C%EB%94%94%EC%96%B4-%ED%83%88%EC%B6%9C%ED%95%A9%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Sun, 26 Nov 2023 15:42:58 GMT</pubDate>
            <description><![CDATA[<p>필자는 프론트엔드 개발을 한지 1년이 조금 넘었다. 나는 개발을 하면서 항상 공통컴포넌트.. 공통컴포넌트.. 하면서 어떻게 하면 공통된 UI 코드를 효율적이게 짤 수 있을까? 항상 고민했던것 같다. </p>
<p>하지만 공통컴포넌트를 만들었지만 recoil을 사용해서 전역으로 state를 관리했었고 컴포넌트 
style을 바꾸고싶다면 props로 넘기며 분기처리를 해야만 했다. </p>
<pre><code class="language-javascript">//CheckBox.tsx
const CheckBox = () =&gt; {
  const [isChecked, setIsChecked] = useRecoilState&lt;boolean&gt;(CheckBoxAtom);

  const toggleCheckbox = () =&gt; {
    setIsChecked(!isChecked);
  };
  return (
    &lt;&gt;
      &lt;S.Label&gt;
        &lt;S.Input
          type=&quot;checkbox&quot;
          checked={isChecked}
          onChange={toggleCheckbox}
        /&gt;

        {isChecked ? (
          &lt;Lottie
            animationData={CheckBoxLottie}
            loop={false}
            play
            style={{ width: 20, height: 20 }}
            direction={1}
          /&gt;
        ) : (
          &lt;Lottie
            animationData={CheckBoxLottie}
            loop={false}
            play
            style={{ width: 20, height: 20 }}
            direction={-1}
          /&gt;
        )}
      &lt;/S.Label&gt;
    &lt;/&gt;
  );
};

export default CheckBox;</code></pre>
<p><strong>이러한 방법은 분기처리를 여러번 해야된다는 문제점과 state을 전역으로 관리해야된다는 문제점이 있었다..</strong> 
그렇다 공통의 저주에 걸린것이다.</p>
<p>공통의 저주에서 벗어나기위해 여러 블로그를 찾아보던중 Headless Components을 보게 되었다.</p>
<blockquote>
</blockquote>
<h3 id="headless란">Headless란?</h3>
<p> Headless를 그대로 번역하면 &#39;머리가 없는&#39;이라는 뜻이다. 
 좀 더 자세히 이야기 하면 스타일이 없고 로직만 있는 컴포넌트를 의미한다.</p>
<p>공통 컴포넌트에 스타일을 입히지 않고 로직만을 짠 뒤 컴포넌트를 사용하는 곳에서 스타일을 사용한다고 생각하면 좀 더 쉬울것 같다.</p>
<p>나는 예전에 짰던 공통 체크박스를 Headless Components의 Function as Child Component로 리팩토링을 했다.</p>
<pre><code class="language-javascript">//CheckBoxHeadless.ts
import { useState } from &quot;react&quot;;

type CheckboxHeadlessProps = {
  isChecked: boolean;
  onChange: () =&gt; void;
};

const CheckBoxHeadless = (props: {
  children: (args: CheckboxHeadlessProps) =&gt; JSX.Element;
}) =&gt; {
  const [isChecked, setIsChecked] = useState(false);

  if (!props.children || typeof props.children != &quot;function&quot;) return null;

  return props.children({
    isChecked,
    onChange: () =&gt; setIsChecked(!isChecked),
  });
};

export default CheckBoxHeadless;</code></pre>
<p>function as child Component는 자식에 어떤 것이 들어올지 예상할 수 없기 때문에 children prop으로 받아 그대로 전달하는 것이다. </p>
<pre><code class="language-javascript">//App.tsx
const App = () =&gt; {
  return(
    &lt;CheckBoxHeadless&gt;
            {({ isChecked, onChange }) =&gt; {
              return (
                &lt;S.Label&gt;
                  &lt;input
                    type=&quot;checkbox&quot;
                    checked={isChecked}
                    onChange={onChange}
                    style={{ display: &quot;none&quot; }}
                  /&gt;

                  {isChecked ? (
                    &lt;Lottie
                      animationData={TE}
                      loop={false}
                      play
                      style={{ width: 20, height: 20 }}
                      direction={1}
                    /&gt;
                  ) : (
                    &lt;Lottie
                      animationData={TE}
                      loop={false}
                      play
                      style={{ width: 20, height: 20 }}
                      direction={-1}
                    /&gt;
                  )}

                  &lt;p&gt;로그인 상태 유지&lt;/p&gt;
                &lt;/S.Label&gt;
              );
            }}
          &lt;/CheckBoxHeadless&gt;
      )</code></pre>
<p>이러한 방식은 사용하려는 state 값을 위에서 따로 선언할 필요가 없어, 다른 컴포넌트에 해당 state를 실수로 넣을 일이 적어진다. 그리고 관련된 코드가 한 곳에 모여 있어 읽기 편하다. 하지만 다른 곳에서 해당 state를 공유할 경우, CheckboxHeadless가 감싸야 할 코드량이 많아지는 단점이 있다.</p>
<h2 id="결론">결론</h2>
<p>이 체크박스는 style이 변경될 일이 별로 없기 때문에 HeadlessComponent를 사용하지 않아도 되지만 자주쓰이는 버튼같은  경우는 HeadlessComponent를 사용하면 공통컴포넌트를 쉽게 만들 수 있을것 같다.
필자는 HeadlessComponent를 처음 접했을때 공통컴포넌트에 씨게 걸려있어서  와...이거 사용했으면 공통컴포넌트 저주 탈출하겠다..x되네.. 라고 생각을 했다 ^^</p>
<p>참고자료
<a href="https://www.howdy-mj.me/design/headless-components">https://www.howdy-mj.me/design/headless-components</a>
<a href="https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC">https://velog.io/@baby_dev/front-end-%EA%B3%B5%ED%86%B5%EC%9D%98-%EC%A0%80%EC%A3%BC</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[고딩 개발자의 삶 모아보기]]></title>
            <link>https://velog.io/@sun_rim/%EA%B3%A0%EB%94%A9-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%82%B6-%EB%AA%A8%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@sun_rim/%EA%B3%A0%EB%94%A9-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%82%B6-%EB%AA%A8%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 15 Oct 2023 15:24:46 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 여러분
다들 잘 지내고 계신가요? 
네이버 블로그에서 넘어온지 3달이 넘어가고 있는데 계속 기술 블로그만 쓴 것 같아 
스근하게 제 이야기 들고왔습니다.</p>
<p>(1년 스토리는 2023 회고글에서 적기 위해 8월 이야기 부터 가져왔습니다..)</p>
<h3 id="바인드">바인드</h3>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/c99b1a78-aef5-4675-ab2b-33a6746e245c/image.png" alt="">
먼저 기다리고 기다렸던 바인드 명함이 나왔습니다.
작년 부끄러웠던 직접 제작한 명함이 아닌 팀 명함으로 토크콘서트 갈 생각이 가슴이 도키도키 하고
싱글벙글 했던것 같습니다~ ㅎㅎ
<img src="https://velog.velcdn.com/images/sun_rim/post/10d1b62a-d2e6-4406-a6f8-7b378247dbe3/image.png" alt="">
그다음은 바인드7기를 이을 8기 인턴을 뽑았습니다.. 작년에 내가 앉았던 자리에 후배들이 앉고 선배들이 앉았던 자리에 제가 앉으니 기분이 묘하기도 하고 참 시간이 빠르구나.. 했던 생각이 많이 들었어요🥲
오랜만에 바인드 팀원들과 사진을 찍으니 몽글몽글 인턴때로 돌아간 것 같아 기분 좋았습니다 ㅎㅎ
<img src="https://velog.velcdn.com/images/sun_rim/post/38a8fb59-b442-481e-9481-9a0e245c5a83/image.png" alt="">
<img src="https://velog.velcdn.com/images/sun_rim/post/5073f9c2-6789-436b-b11f-d41f8a3aa117/image.png" alt=""></p>
<p>마지막으로는 바인드 SUMMER 컨퍼런스를 진행했습니다. 저는 올바른 경재을 하는 법 이라는 주제로 컨퍼런스를 진행했는데 1학년 친구들이 많이 공감하고 유익했다는 소리에 또 싱글벙글 했습니다.
컨퍼런스가 궁금하다면 여기로! -&gt;  <a href="https://www.youtube.com/watch?v=Qy59DM4VFjo&amp;ab_channel=%EB%B0%94%EC%9D%B8%EB%93%9CB1ND">바인드 컨퍼런스</a></p>
<h3 id="생일">생일</h3>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/e5a8ab9c-0fa8-46c2-a8f4-6d1d31d846f2/image.jpeg" alt=""></p>
<p>9월 20일 생일이었습니다 ㅎㅎ 많은 분들께 축하도 받고 친구들이 몰래 케이크 들고 파티해줘서 정말 이렇게 행복해도 되나 싶을 정도로  행복했습니다. 이 자리를 빌어 생일축하해주신 모든 분들께 다시 감사하다고 말하고싶네요 감사합니다🙇‍♀️🙇</p>
<h3 id="개발">개발</h3>
<p>개발을 이야기 할게 정말 많지만 조금 간추려 이야기 하자면.. 스택 수상을 했습니다 ㅎㅎ!!!
<img src="https://velog.velcdn.com/images/sun_rim/post/7b65782e-d792-4df8-ab28-4779f50cce6e/image.png" alt="">
아직 최우수상일지 대상일지 상격은 11월에 나오지만 수상했는 사실을 자랑하고싶었습니다 ^^
정말 좋은 친구들을 만나 팀을 이뤘고 그 팀이 수상을 해 제 기분은 말로 설명하지 못할 만큼 좋았습니다 ㅎㅎㅎ
제일 많이 고생한 팀장과 그 팀원들 모두 고맙고 고생했다고 말해주고 싶네요</p>
<p>저희 툰게더 팀은 아직 더 많은 꿈을 이루기 위해 노력하고 있습니다. 저희의 꿈을 응원해주세요! 
많은 관심 부탁드립니다 ㅎㅎ!
<a href="https://toongether.kr/">툰게더 웹 바로가기</a> 
<a href="https://download.toongether.kr/">툰게더 모바일 다운로드 바로가기</a></p>
<h3 id="연합해커톤">연합해커톤</h3>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/3724d447-84d9-4642-b1f9-7f9313a3a8dd/image.png" alt="">
<img src="https://velog.velcdn.com/images/sun_rim/post/c053ece8-e1fa-4003-a701-6f9311a299fd/image.png" alt=""></p>
<p>마지막으로 2023 연합해커톤에 참가했습니다. 아쉽게도 상을 받진 못했지만 정말 좋은 팀원들을 만나 2박3일동안 힘들지 않고 재밌었던 기억밖에 남지 않은 것 같네요 ㅎㅎ 
<del>호텔밥이 한끼에 13만원씩 했는데 진짜 개맛있었다는....</del> 
호텔에서 바다보면서 낭만 그득~ 하게 개발했습니다 😏
(아 팀원이 저한테 사투리가 심하다고 해서 사투리를 고치기로 마음 먹은 해커톤이었습니다 ㅎㅎ)</p>
<p>이렇게 제 8~10월 이야기는 스근하게 끝내겠습니다.
벌써 2023년이 끝나가네요 다들 마무리 잘 하시고 2023년이 끝날때는 다들 이루고 싶었던 일 하고싶었던 일 모두 이뤘으면 좋겠습니다 ㅎㅎ 🙇</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT에 대하여]]></title>
            <link>https://velog.io/@sun_rim/%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@sun_rim/%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Mon, 04 Sep 2023 01:17:39 GMT</pubDate>
            <description><![CDATA[<h2 id="토큰이란">토큰이란?</h2>
<p>클라이언트에서 인증 정보를 보관하는 방법</p>
<h2 id="jwt-란">JWT 란?</h2>
<p>Json Wep Token의 약자로 웹에서 쓰이는 json토큰이다.                                                                                                    종류는 access token, refresh token 두가지다</p>
<h2 id="access-token--refresh-token">Access Token &amp; refresh Token</h2>
<p>보호된 정보에 접근할 수 있는 권한부여에 사용됨</p>
<p>클라이언트가 처음 인증을 받게 될때(로그인), access token, refresh token 둘다 받게 되지만 실제로 권한을 얻는데 사용되는 토큰은 access token이다.</p>
<p>권한을 부여받는 데엔 access token만 있으면 된다. 하지만 토큰이 탈취된다면, 로그인을 해서 여러 나쁜 행위를 할 수 있다. 그래서 access token이 만료기간을 짧게 주고 오랫동안 사용할 수 없도록 한다.</p>
<p>access token이 만료될 때마다 refresh token을 통해 말 그대로 access token을 refresh 한다.</p>
<p><strong>access token은 로그인에 접근할 수 있는 카드키, refresh token은 카드키 재발급 이라고 생각하면 편하다.</strong></p>
<h2 id="access-token--refresh-token-인증-과정">Access Token + Refresh Token 인증 과정</h2>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/c3120247-7176-451f-a454-7d4d0a87a1c1/image.png" alt=""></p>
<ol>
<li>사용자가 ID , PW를 통해 로그인.</li>
</ol>
<ol start="2">
<li>서버에서는 회원 DB에서 값을 비교</li>
</ol>
<p>3~4. 로그인이 완료되면 Access Token, Refresh Token을 발급. 이때 일반적으로 회원DB에 Refresh Token을 저장해둔다.</p>
<ol start="5">
<li>사용자는 Refresh Token은 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보낸다.</li>
</ol>
<p>6~7. Access Token을 검증하여 이에 맞는 데이터를 보낸다.</p>
<ol start="8">
<li>시간이 지나 Access Token이 만료되었다고 보겠음</li>
</ol>
<ol start="9">
<li>사용자는 이전과 동일하게 Access Token을 헤더에 실어 요청을 보낸다.</li>
</ol>
<p>10~11. 서버는 Access Token이 만료됨을 확인하고 권한없음을 신호로 보낸다.</p>
<p>** Access Token 만료가 될 때마다 계속 과정 9~11을 거칠 필요는 없다</p>
<p> 사용자(프론트엔드)에서 Access Token의 Payload를 통해 유효기간을 알 수 있다. 따라서 프론트엔드 단에서 API 요청 전에 토큰이 만료됐다면 바로 재발급 요청을 할 수도 있다</p>
<ol start="12">
<li>사용자는 Refresh Token과 Access Token을 함께 서버로 보낸다.</li>
</ol>
<ol start="13">
<li>서버는 받은 Access Token이 조작되지 않았는지 확인한후, Refresh Token과 사용자의 DB에 저장되어 있던 Refresh Token을 비교. Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해준다.</li>
</ol>
<ol start="14">
<li>서버는 새로운 Access Token을 헤더에 실어 다시 API 요청을 진행. </li>
</ol>
<h3 id="장점">(장점)</h3>
<p>기존의 Access Token만 있을 때보다 안전하다.</p>
<h3 id="단점">(단점)</h3>
<ol>
<li>구현이 복잡하다. 검증 프로세스가 길게 때문에 자연스레 구현하기가 힘들어졌다.</li>
<li>Access Token이 만료될 때마다 새롭게 발급하는 과정에서 생기는 HTTP 요청 횟수가 많다. 이는 서버의 자원 낭비로 이어진다</li>
</ol>
<h2 id="보안은-어떻게-뚫리나">보안은 어떻게 뚫리나</h2>
<ol>
<li><p>XSS 공격</p>
<p> 공격자가 클라이언트 브라우저에 JavaScript를 삽입해 실행하는 공격이다. 공격자가 input 태그를 사용해 JavaScript를 서버로 전송해 서버에서 스크립트를 실행하거나, url에 javascript를 적어 클라이언트에서 스크립트 실행이 가능하다면 공격자가 사이트에 스크립트를 삽입해 XSS 공격을 할 수 있다. 이때 공격자는 javascript를 통해 사이트의 글로벌 변숫값을 가져오거나 그 값을 이용해 사이트인 척 API 콜을 요청할 수도 있다. 공격자의 코드가 내 사이트의 로직인 척 행동할 수 있다는 것</p>
</li>
<li><p>CSRF 공격</p>
<p> 공격자가 다른 사이트에서 우리 사이트의 API 콜을 요청해 실행하는 공격이다. API 콜을 요청할 수 있는 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않다면 CSRF가 가능한데, 이때 공격자가 클라이언트에 저장된 유저 인증정보를 서버에 보낼 수 있다면, 제대로 로그인한 것처럼 유저의 정보를 변경하거나 유저만 가능한 액션들을 수행할 수 있다. 예를 들어 CSRF에 취약한 은행 사이트가 있다면 로그인한 척 계좌 비밀번호를 바꾸거나 송금을 보낼 수 있는 것이다.</p>
</li>
</ol>
<h2 id="브라우저-저장소-종류--보안-이슈">브라우저 저장소 종류 &amp; 보안 이슈</h2>
<p><strong>1. localStorage 저장 방식</strong></p>
<p>   브라우저 저장소에 저장하는 방식, JavaScript 내 글로벌 변수로 읽기/쓰기 접근 가능</p>
<blockquote>
<p> 👿 : LocalStorage 안에 세션 id, refreshToken 또는 accessToken을 저장해두면 XSS 취약점을 통해 그 안에 담긴 값을 불러오거나, 불러온 값을 이용해 API 콜을 위조할 수 있다.</p>
</blockquote>
<p><strong>2. 쿠키 저장 방식</strong></p>
<p>   브라우저에 쿠키로 저장되는데, 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다. Javascript 내 글로벌 변수로 읽기/쓰기 접근 가능</p>
<blockquote>
<p> 👿 : 세션 id, refreshToken, accessToken을 저장해두면 XSS 취약점이 있을 때 담긴 값들을 불러오거나, API 콜을 보내면 쿠키에 담긴 값들이 함께 전송되어 로그인한 척 공격을 수행할 수 있다.
👿 : 쿠키에 세션 id나 accessToken을 저장해 인증에 이용하는 구조에 CSRF 취약점이 있다면 인증 정보가 쿠키에 담겨 서버로 보내진다. 공격자는 유저 권한으로 정보를 가져오거나 액션을 수행할 수 있다</p>
</blockquote>
<blockquote>
</blockquote>
<pre><code>&gt; 😇 : 쿠키에 refreshToken만 저장하고 새로운 accessToken을 받아와 인증에 이용하는 구조에서는 CSRF 취약점 공격을 방어할 수 있다. refreshToken으로 accessToken을 받아도 accessToken을 스크립트에 삽입할 수 없다면 accessToken을 사용해 유저 정보를 가져올 수 없기 때문이다.</code></pre><p><strong>3. secure httpOnly 쿠키 저장 방식</strong></p>
<p>   브라우저에 쿠키로 저장되는 것 같지만, Javascript 내에서 접근이 불가능하다. secure을 적용하면 https 접속에서만 동작한다.</p>
<blockquote>
</blockquote>
<p> 😇 : httpOnly 쿠기 방식으로 저장된 정보는 XSS 취약점 공격으로 담긴 값을 불러올 수 없다.</p>
<blockquote>
<p>😇 : 쿠키에 refreshToken만 저장하고 accessToken을 받아와 인증에 이용하는 구조로 CSRF      공격 방어가 가능하다</p>
</blockquote>
<blockquote>
</blockquote>
<p>👿 : 쿠키 저장 방식과 같은 이우로 세션 id, accessToken은 저장하면 안 된다.
  👿 : httpOnly 쿠키에 담긴 값에 접근할 수는 없지만 XSS 취약점을 노려 API 콜을 요청하면 httpOnly 
       쿠키에 담긴 값들도 함께 보내져 유저인 척 정보를 빼오거나 액션을 수행할 수 있다</p>
<blockquote>
<p>👿 : LocalStorage 안에 세션 id, refreshToken 또는 accessToken을 저장해두면 XSS 취약점을 통해 그 안에 담긴 값을 불러오거나, 불러온 값을 이용해 API 콜을 위조할 수 있다.</p>
</blockquote>
<p>_
<strong>어떤 저장 방식을 택해도 XSS 취약점이 있다면 보안 이슈가 발생한다.(XSS로 API 콜을 보내는 방식으로 다 뚫린다) 그러므로 유저 정보 저장 방식을 바꾸는 것만으로는 방어할 수 없고, 클라이언트와 서버에서 추가적으로 XSS 방어 처리가 필수다.</strong>
_</p>
<h4 id=""></h4>
<p>Access Token과 Refresh Token을 어디에 저장해야 하는지는 아래의 포스팅을 참고하면 좋을 것 같다.</p>
<p><a href="https://velog.io/@ohzzi/Access-Token%EA%B3%BC-Refresh-Token%EC%9D%84-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C">https://velog.io/@ohzzi/Access-Token과-Refresh-Token을-어디에-저장해야-할까</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR,CSR 한번에 이해하기]]></title>
            <link>https://velog.io/@sun_rim/SSRCSR-%ED%95%9C%EB%B2%88%EC%97%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sun_rim/SSRCSR-%ED%95%9C%EB%B2%88%EC%97%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 28 Jul 2023 12:45:32 GMT</pubDate>
            <description><![CDATA[<h3 id="mpamulti-page-application">MPA(multi page application)</h3>
<p>여러 페이지로 구성된 웹 어플리케이션. 사용자의 클릭과 같이 인터렉션이 발생할 때마다 서버로부터 새로운 html을 받아와서 해당 링크로 이동하여 페이지 전체를 새로 렌더링하는 전통적인 웹 페이지 구성 방식</p>
<blockquote>
<p>mpa는 srr방식텍스트을 채택</p>
</blockquote>
<h3 id="spasingle-page-application">SPA(single page application)</h3>
<p>하나의 페이지로 구성된 웹 어플리케이션. 브라우저에 최초에 한번 페이지 전체를 로드하고, 이후부터는 특정 부부만 Ajax를 통해 데이터를 바인딩한 방식</p>
<blockquote>
<p>spa는 csr방식을 채택</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/668cce57-ecfe-47d8-9b1d-ba0b6ae9a665/image.png" alt=""></p>
<h3 id="ssrserver-side-rendering">SSR(server side rendering)</h3>
<blockquote>
<p>서버로부터 완전하게 만들어진 html파일을 받아와 페이지 전체를 렌더링 하는 방식.</p>
</blockquote>
<p>클라이언트가 초기 화면 로드위해 서버 요청 → 서버는 화면을 표시하는데 필요한 데이터 모드 얻어와 삽입,css 까지 모두 적용해 렌더링 준비를 마친 html,js코드를 브라우저에 응답으로 전달 → 브라우저에서 바로 전달 받은 페이지를 띄우고 → 브라우저가 js 코드를 다운로드 하고 html에 실행시킨다</p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/ff1af2ab-08f1-4c0e-a7ec-ed8748f1d007/image.png" alt=""></p>
<p><strong>장점</strong></p>
<p><strong>1)</strong> seo(검색 엔진 최적화) 검색엔진이 웹을 크롤링 하면서 페이지에 컨텐츠 색인을 생성하는 과정.                    ssr 렌더링을 채택하는 멀티 페이지 어플리케이션은 화면을 구성하는 각각의 페이지가 있기 때문에 seo의 유리한 장점이 있다(모든 데이터가 이미 html에 담겨진채로 브라우저에 전달되기 때문에 검색엔지 최적화에 유리)</p>
<p><strong>2)</strong> 빠른 초기 로딩. 서버로부터 화면을 렌더 하기 위한 필수적인 요소를 먼저 가져오기 때문에 csr보다 초기 로딩속도가 빠르다 → 사용자가 기다리는 시간이 적다</p>
<p><strong>단점</strong></p>
<p><strong>1)</strong> TTV(time to view)와 TTI(time to interact) 간의 시간 간격이 존재하게 된다. 사용자가 버튼을 클릭하거나 이동하려고 해도 아무런 반응이 없을 수 있다.</p>
<p><strong>2)</strong> 매번 페이지를 요청할 때마다 새로 고침 되기 때문에 사용자 경험이 다소 떨어진다 → 깜박임 생김</p>
<p><strong>3)</strong> 서버측 부하가 증가한다. 페이지를 요청할 때마다 서버에서 페이지를 구성하는 모든 리소스를 준비해서 응답하므로 서버 부담이 증가 된다. → 새로운 요청이 생길때 마다 바뀌지 않아도 되는 부분도 렌더링된다</p>
<h3 id="csrclient-side-rendering">CSR(client side rendering)</h3>
<blockquote>
<p>사용자의 요청에 따라 필요한 부분만 응답 받아 렌더링 하는 방식.</p>
</blockquote>
<p>클라이언트에서 초기화면 로드하기 위해 서버에 요청을 보냄 → 서버는 화면에 표시하는 데 필요한 완전한 리소소를 응답 → js파일을 다운받아야 하기 때문에 초기 로딩 시간이 더 오래 걸린다.</p>
<p><img src="https://velog.velcdn.com/images/sun_rim/post/caa22ff5-d45e-4e7e-9fb1-6bf86f41b866/image.png" alt=""></p>
<p><strong>장점</strong></p>
<p><strong>1)</strong> 빠른 속도와 서버 부하 감소. 변경된 부분과 관련된 데이터만 가져오므로 ssr보다 빠른 속도를 보인다</p>
<p><strong>2)</strong> 변경된 부분만 요청함으로써 서버의 부담을 줄일 수 있다.</p>
<p><strong>3)</strong> 사용자 친화적이다. 페이지 안에서 이벤트가 일어났을때 다음 단계로 전환 하는 과정에서 링크가 없기 때문에 깜박임 없이 부드러운 이동을 경험할 수 있다.</p>
<p><strong>단점</strong></p>
<p><strong>1)</strong> seo(검색 엔진 최적화)가 불리하다. csr을 채택한 싱글 페이지 어플리케이션은 자바스크립트를 사용하여 사용자와 상호 작용 후에 페이지 내용을 로드하기 때문에 웹 크롤러 가 페이지를 색인화 하려고 하면 내용의 빈 페이지 처럼 보이게 된다(열심히 서비스를 만들었는데 검색 사이트에 노출되지 않는 다면 좋지 않은 상황이다)</p>
<p><strong>2)</strong> 초기 로딩 속도가 느리다. crs은 초기에 모든 js파일을 다운받아 와야 하기 때문에 초기 로딩 시간이 오래 걸린다</p>
<h3 id="렌더링-방식-선택-기준">렌더링 방식 선택 기준</h3>
<p><strong>SSR</strong></p>
<p><strong>1)</strong> 상위 노출이 필요한 경우</p>
<p><strong>2)</strong> 누구에게나 동일한 내용을 노출할 경우</p>
<p><strong>3)</strong> 페이지마다 데이터가 자주 바뀔 경우</p>
<p><strong>CSR</strong></p>
<p><strong>1)</strong> 개인정보 데이터를 기준으로 구성되는 경우</p>
<p><strong>2)</strong> 보다 나은 사용자 경험을 제공하고 싶을 경우</p>
<p><strong>3)</strong> 상위노출보다 고객의 데이터 보호가 더 중요할 경우</p>
<p>출처 
tistory : <a href="https://url.kr/ng2zd4">https://url.kr/ng2zd4</a></p>
]]></description>
        </item>
    </channel>
</rss>