<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sarang_daddy.log</title>
        <link>https://velog.io/</link>
        <description>한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.</description>
        <lastBuildDate>Sat, 30 Dec 2023 15:48:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sarang_daddy.log</title>
            <url>https://velog.velcdn.com/images/sarang_daddy/profile/27ea0c70-ecbb-4f8c-a67a-8eae3b586900/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sarang_daddy.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sarang_daddy" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[React - 파일 업로드(Drag & Drop)]]></title>
            <link>https://velog.io/@sarang_daddy/React-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9CDrag-Drop</link>
            <guid>https://velog.io/@sarang_daddy/React-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9CDrag-Drop</guid>
            <pubDate>Sat, 30 Dec 2023 15:48:09 GMT</pubDate>
            <description><![CDATA[<p>웹에 파일을 업로드할 때 <strong>Drag &amp; Drop</strong> 기능을 제공한다면 사용자 경험을 향상시킬 수 있다.
React에서 <strong>Drag &amp; Drop</strong>을 이용한 파일 업로드하는 방법을 구현해 보자.</p>
<h2 id="1-부모-컴포넌트에서의-사용">1. 부모 컴포넌트에서의 사용</h2>
<ul>
<li>여러 컴포넌트에서 재사용이 가능하도록 구현해준다.</li>
<li>부모 컴포넌트로부터 파일 선택 핸들러를 받아온다.</li>
</ul>
<pre><code class="language-tsx">// 부모 컴포넌트
export default function AdVideoUploadContainer() {
  // 선택된 파일을 관리한다.
  const [file, setFile] = useState&lt;File | null&gt;(null);

  // 구현할 InputDragDrop에서 파일이 선택될 때 상태를 업데이트 한다.
  const handleFileSelect = (file: File | null) =&gt; {
    setFile(file);
  };  

  // 파일 업로드를 처리하는 로직
  const handleUpload = () =&gt; {
    if (file) {
      // Drag &amp; Drop으로 가져온 파일 처리 로직 (API 호출 등)
    }
  };

  // ...

return 
 // ...
 &lt;InputDragDrop
   onChangeFile={handleFileSelect}
   description=&quot;등록하실 비디오를 선택하거나 올려주세요.&quot;
 &gt;&lt;/InputDragDrop&gt;
 // ...</code></pre>
<h2 id="2-컴포넌트-생성">2. 컴포넌트 생성</h2>
<ul>
<li>Drag &amp; Drop 이벤트에는 Enter, Leave, Over, Drop 4개의 이벤트가 필요하다.</li>
<li>위 이벤트들은 이벤트 기본 동작과 이벤트 버블링 효과를 차단한다.</li>
</ul>
<pre><code class="language-tsx">interface DragDropProps {
  onChangeFile: (file: File | null) =&gt; void;
  description?: string;
}

const DragDrop = ({
  onChangeFile,
  description = &quot;파일 첨부&quot;,
}: DragDropProps) =&gt; {

  // 사용자가 파일을 드래드 중임을 상태로 관리 UI 변경을 위해 사용
  const [dragOver, setDragOver] = useState&lt;boolean&gt;(false);

  // 드래그 중인 요소가 목표 지점 진입할때
  const handleDragEnter = (e: React.DragEvent&lt;HTMLLabelElement&gt;) =&gt; {
    e.preventDefault();
    e.stopPropagation();
    setDragOver(true);
  };

  // 드래그 중인 요소가 목표 지점을 벗어날때
  const handleDragLeave = (e: React.DragEvent&lt;HTMLLabelElement&gt;) =&gt; {
    e.preventDefault();
    e.stopPropagation();
    setDragOver(false);
  };

  // 드래그 중인 요소가 목표 지점에 위치할때
  const handleDragOver = (e: React.DragEvent&lt;HTMLLabelElement&gt;) =&gt; {
    e.preventDefault();
    e.stopPropagation();
  };

  // 드래그 중인 요소가 목표 지점에서 드롭될때
  const handleDrop = (e: React.DragEvent&lt;HTMLLabelElement&gt;) =&gt; {
    e.preventDefault();
    e.stopPropagation();
    setDragOver(false);

    // 드래그되는 데이터 정보와 메서드를 제공하는 dataTransfer 객체 사용
    if (e.dataTransfer) {
      const file = e.dataTransfer.files[0];
      onChangeFile(file);
    }
  };

  // Drag &amp; Drop이 아닌 클릭 이벤트로 업로드되는 기능도 추가
  const handleChange = (e: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const file = e.target.files ? e.target.files[0] : null;
    onChangeFile(file);

    // input 요소의 값 초기화
    e.target.value = &quot;&quot;;
  };

  return (
    &lt;div className=&quot;flex flex-col justify-center items-center w-full&quot;&gt;
      &lt;div className=&quot;w-3/4&quot;&gt;
        &lt;Label
          className={`w-full flex-col gap-3 h-32 border-2 ${
            dragOver
              ? &quot;border-blue-500 bg-blue-100 text-blue-500 font-semibold&quot;
              : &quot;border-gray-300&quot;
          } rounded-md flex items-center justify-center cursor-pointer`}
          htmlFor=&quot;fileUpload&quot;
          // Label에 드래그 앤 드랍 이벤트 추가
          onDragEnter={handleDragEnter}
          onDragLeave={handleDragLeave}
          onDragOver={handleDragOver}
          onDrop={handleDrop}
        &gt;
          {description}
          &lt;div className=&quot;w-9 h-9 pointer-events-none&quot;&gt;
            &lt;svg
              xmlns=&quot;http://www.w3.org/2000/svg&quot;
              fill=&quot;none&quot;
              viewBox=&quot;0 0 24 24&quot;
              strokeWidth={1}
              stroke=&quot;currentColor&quot;
            &gt;
              &lt;path
                strokeLinecap=&quot;round&quot;
                strokeLinejoin=&quot;round&quot;
                d=&quot;M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z&quot;
              /&gt;
            &lt;/svg&gt;
          &lt;/div&gt;
        &lt;/Label&gt;
        &lt;Input
          id=&quot;fileUpload&quot;
          type=&quot;file&quot;
          className=&quot;hidden&quot;
          onChange={handleChange}
        &gt;&lt;/Input&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};


export default DragDrop;</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/e8280e21-835f-40b5-b4df-d0a90388e8bf/image.gif" alt=""></p>
<h2 id="3-파일-확장자-제한과-알림-기능-추가하기">3. 파일 확장자 제한과 알림 기능 추가하기</h2>
<ul>
<li>업로드 파일의 확장자를 제한하는 기능을 추가해보자.</li>
<li>허용되지 않은 확장자의 경우 알림을 통해 사용자에게 정보를 제공해보자.</li>
</ul>
<pre><code class="language-tsx">// InputDragDrop 컴포넌트
interface InputDragDropProps {
  onChangeFile: (file: File | null) =&gt; void;
  description?: string;
  validExtensions?: string[]; // 확장자 정보를 받아온다.
}

const InputDragDrop = ({
  onChangeFile,
  description = &quot;파일 첨부&quot;,
  validExtensions = [&quot;*&quot;], // 디폴트는 모든 확장자 허용.
}: InputDragDropProps) =&gt; {

  const { toast } = useToast(); // 알림 컴포넌트

  // ... 기존 코드

  // 파일 확장자 검증 함수
  const isValidExtension = (file: File) =&gt; {
    const fileName = file.name;
    const fileNameSplit = fileName.split(&quot;.&quot;);
    const fileExtension = fileNameSplit[fileNameSplit.length - 1];
    return validExtensions.includes(fileExtension);
  };

  // 허용된 확장자라면 업로드, 아니라면 사용자에게 알림
  const handleFileChange = (file: File | null) =&gt; {
    if (file &amp;&amp; isValidExtension(file)) {
      onChangeFile(file);
    } else {
      toast({
        title: &quot;잘못된 파일 형식&quot;,
        description: `지원하지 않는 파일 형식입니다. (${validExtensions.join(
          &quot;, &quot;
        )})로 등록해주세요.`,
        className: &quot;bg-red-500 text-white&quot;,
      });
      onChangeFile(null);
    }
  };


  // ...기존 코드</code></pre>
<ul>
<li>부모 컴포넌트에서는 props로 허용 확장자만 알려주면 된다.</li>
</ul>
<pre><code class="language-tsx">// 부모 컴포넌트
return 
 // ...
 &lt;InputDragDrop
   onChangeFile={handleFileSelect}
   description=&quot;등록하실 비디오를 선택하거나 올려주세요.&quot;
   validExtensions={[&quot;mp4&quot;]} // 확장자 정보 추가
 &gt;&lt;/InputDragDrop&gt;
 // ...</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/52465f4f-beae-4c87-aaf0-ce1ababb15c2/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 알림 (Polling)]]></title>
            <link>https://velog.io/@sarang_daddy/React-%EC%95%8C%EB%A6%BC-Polling</link>
            <guid>https://velog.io/@sarang_daddy/React-%EC%95%8C%EB%A6%BC-Polling</guid>
            <pubDate>Sat, 16 Dec 2023 17:05:01 GMT</pubDate>
            <description><![CDATA[<p>처리해야하는 작업이 남아 있다면 아이콘을 활성화하여 사용자가 알 수 있도록 인터페이스(알림)을 제공하는 기능을 구현해보자.</p>
<h2 id="처리-작업이-남아있다면-true-반환-api">처리 작업이 남아있다면 true 반환 API</h2>
<ul>
<li>해당 기능을 위해 서버에서 제공해주는 API는 클라이언트 요청시 반환을 해준다.</li>
</ul>
<pre><code class="language-tsx">{
  &quot;code&quot;: 200,
  &quot;creator_application&quot;: true,
  &quot;message&quot;: &quot;SUCCESS&quot;
}</code></pre>
<h2 id="1차-구현">1차 구현</h2>
<ul>
<li>아이콘은 처리 작업이 필요한 사이드바 메뉴에 활성화 한다.</li>
</ul>
<pre><code class="language-tsx">export default function AdminSidebarContainer() {
  const { data: session } = useSession();
  const { data: newRequest } = useSWR(session ? `/main/side` : null, () =&gt;
    session ? checkSidebar(session.user.token) : null
  );

  return (
    &lt;div className=&quot;flex flex-col&quot;&gt;
      &lt;AdminSidebar menus={SIDEBAR_LIST} newRequest={newRequest} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<pre><code class="language-tsx">// AdminSidebar

// ...
// 새로운 요청 사항 있으면 아이콘 활성화
{newData?.creator_application &amp;&amp;
  subitem.href === &quot;/admin/account/creator&quot; &amp;&amp; (
    &lt;span
      className={` text-red-500 ${
        isSubmenuItemActive(subitem)
          ? &quot;text-white &quot;
          : &quot;&quot;
      },`}
    &gt;
      &lt;WarningIcon /&gt;
    &lt;/span&gt;
  )}</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/9f48b8cb-4d5c-45fc-8194-0e3b25686572/image.png" alt=""></p>
<ul>
<li>아이콘 활성화는 구현되었지만 아래와 같은 문제점이 남아있다.</li>
</ul>
<h2 id="추가-고려사항">추가 고려사항</h2>
<ul>
<li>처리해야하는 작업은 실시간으로 업데이트 된다.</li>
<li>언제 API를 요청해서 새로운 작업이 있는지 확인해야 할까?</li>
</ul>
<blockquote>
<ul>
<li>기획상 실시간 알림까지의 필요성을 없다.</li>
<li>최대한 서버의 부담이 적은 방향으로 30분 간격의 확인으로 협의.</li>
</ul>
</blockquote>
<ul>
<li>처리 작업이 완료되면 즉시 아이콘이 사라져야 한다.</li>
<li>작업 처리는 다른 컴포넌트에 존재한다.</li>
</ul>
<blockquote>
<p>커스텀훅으로 구현하여 여러 컴포넌트에서 동일한 요청이 가능하도록 구현</p>
</blockquote>
<h2 id="커스텀훅-생성">커스텀훅 생성</h2>
<pre><code class="language-tsx">// hooks/useAdminSidebar.js
import useSWR from &quot;swr&quot;;
import { checkSidebar } from &quot;../services/adminSidebar.service&quot;;
import { useSession } from &quot;next-auth/react&quot;;

export function useAdminSidebar() {
  const { data: session } = useSession();
  const { data, mutate } = useSWR(
    session ? `/admin/side` : null, // key
    () =&gt; (session ? checkSidebar(session.user.token) : null), // fetcher

    { refreshInterval: 1800000 } // 30분 간격으로 리플레쉬
  );

  return { data, refresh: mutate };
}</code></pre>
<ul>
<li>처리 작업 존재 여부 값을 반환한다.</li>
<li><code>useSWR</code>의 <code>refreshInterval</code> 옵션으로 30분 간격으로 데이터를 새로고침한다.</li>
<li>수동적으로 새로고침을 실행하기 위해 mutate(refresh)를 반환한다.</li>
</ul>
<h2 id="2차-구현">2차 구현</h2>
<ul>
<li>사이드바는 커스텀훅을 통해 남은 작업 여부를 확인하고 30분마다 재확인한다.</li>
</ul>
<pre><code class="language-tsx">import { AdminSidebar } from &quot;@/admin/components/sidebar/AdminSidebar&quot;;
import { SIDEBAR_LIST } from &quot;@/admin/constants/sidebarList&quot;;
import { useAdminSidebar } from &quot;@/admin/hooks/useAdminSidebar&quot;;

export default function AdminSidebarContainer() {
  const { data: newData } = useAdminSidebar();

  return (
    &lt;div className=&quot;flex flex-col&quot;&gt;
      &lt;AdminSidebar menus={SIDEBAR_LIST} newData={newData} /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li>작업 처리가 완료되면 refrash를 통해 데이터를 즉시 업데이트한다.</li>
</ul>
<pre><code class="language-tsx">export default function EditCreatorContainer({
  applyId,
  offStateSettingModal,
}: EditCreatorContainerProps) {

  // useAdminSidebar 커스텀훅의 refresh 가져오기
  const { refresh } = useAdminSidebar();

  // 이벤트 핸들러
  const handleUpdate = async (data: FormData) =&gt; {
    if (!session) return;

    try {
      const response = await updateCreator(session.user.token, data);
      if (response) {
        mutate();
        offStateSettingModal();

        toast({
          title: &quot;크리에이터 승인 요청이 처리 되었습니다.&quot;,
          className: &quot;bg-white&quot;,
        });
      }
    } catch (error) {
      if (error instanceof Error &amp;&amp; &quot;response&quot; in error) {
        const responseError = error as any;

        let errorMessage = &quot;크리에이터 승인 요청 처리에 실패했습니다.&quot;;
        if (
          responseError.response &amp;&amp;
          responseError.response.data &amp;&amp;
          responseError.response.data.message
        ) {
          errorMessage = responseError.response.data.message;
        }

        toast({
          title: errorMessage,
          className: &quot;bg-red-500 text-white&quot;,
        });
      } else {
        toast({
          title: &quot;알 수 없는 오류가 발생했습니다.&quot;,
          className: &quot;bg-red-500 text-white&quot;,
        });
      }
    }
    refresh(); // 작업이 처리되면 남은 작업 여부 데이터를 새로고침 해준다.
  };</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/9de1f18d-8b56-4ad5-838d-fe753e9134df/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 파일 다운로드]]></title>
            <link>https://velog.io/@sarang_daddy/React-%ED%8C%8C%EC%9D%BC-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C</link>
            <guid>https://velog.io/@sarang_daddy/React-%ED%8C%8C%EC%9D%BC-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C</guid>
            <pubDate>Sat, 16 Dec 2023 15:18:38 GMT</pubDate>
            <description><![CDATA[<p>사용자의 요청을 처리하는 기능 구현 중 사용자의 첨부 자료를 다운로드하여 확인해야하는 기능이 필요하다. React에서 파일을 다운로드 하는 방법을 구현하고 정리해보자.</p>
<h2 id="서버로부터-파일-소스-받아오기">서버로부터 파일 소스 받아오기</h2>
<ul>
<li>사용자 정보에서 첨부된 파일의 소스를 알 수 있다.</li>
<li>서버주소에 해당 파일소스를 연결하여 요청하면 파일 다운로드가 가능하다.</li>
<li><code>${serverUrl}${creatorDetailData.file_src}</code></li>
</ul>
<pre><code class="language-json">{
  ...
  &quot;affiliation&quot;: &quot;Tesggr&quot;,
  &quot;grade&quot;: &quot;초3&quot;,
  &quot;nickname&quot;: &quot;스파냐&quot;,
  &quot;name&quot;: &quot;테스트&quot;,
  ...
  &quot;state&quot;: 1,
  &quot;file_src&quot;: &quot;/creator/20069242-dec1-4284-be9c-2bc030ce0aa9.jpg&quot;,
  &quot;category&quot;: [24],
  ...
}</code></pre>
<h2 id="1차-시도--windowopen-메서드">1차 시도 : window.open() 메서드</h2>
<ul>
<li>처음 시도는 window.open() 메서드로 해당 주소로 이동하면 다운로드가 가능하다고 생각했다.</li>
</ul>
<pre><code class="language-tsx">const handleFileDownload = () =&gt; {
    window.open(`${serverUrl}/${creatorDetailData.file_src}`);
  };</code></pre>
<ul>
<li>첨부된 파일의 확인이 가능하지만 새로운 브라우저 탭에서의 내용 확인으로 동작되었다.</li>
<li>기획서에서 요구하는 구현방식은 컴퓨터에 파일이 직접 다운로드되는 방식이다.</li>
</ul>
<h2 id="2차-시도--a-요소의-download">2차 시도 : <code>&lt;a&gt;</code> 요소의 download</h2>
<ul>
<li>링크 이동이 아닌 파일을 직접 다운로드하기 위해서는 <code>&lt;a&gt;</code> 요소의 다운로드 속성을 사용한다.</li>
</ul>
<pre><code class="language-tsx">  const handleFileDownload = () =&gt; {
    const a = document.createElement(&quot;a&quot;);
    a.href = `${process.env.NEXT_PUBLIC_BASIC_URL}${creatorDetailData.file_src}`;
    a.download = creatorDetailData.file_src.split(&quot;/&quot;).pop() || &quot;download&quot;;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };</code></pre>
<ul>
<li>하지만, window.open()과 같이 다운로드가 아닌 새 탭에서 파일을 보여주는 방식으로 동작한다.</li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/HTML/Element/a#download">mdn</a>을 자세히 살펴보니 <code>CORS</code> 문제로 다른 출처의 URL을 가리키면 보안상의 이유로 download 속성을 무시하고 파일을 새 탭이나 창에서 열도록 동작함을 알 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/55253b1f-5abf-4f66-a105-7edd5dc6cd51/image.png" alt=""></p>
<h3 id="cors란">CORS란?</h3>
<ul>
<li>Cross-Oring Resource Sharing (교차 출처 정책)</li>
<li>웹 페이지는 기본적으로 자신과 다른 도메인의 리소스 접근을 제한한다.</li>
<li>이것을 동일 출처 정책(same-orgin policy)이라 한다.</li>
<li>하지만 현대의 웹은 다양한 서비스가 통합되면서 다른 출처의 리소스 요청이 필수적이다.</li>
<li>이를 <strong>해결</strong>하기 위해 CORS가 등장했다.</li>
<li>서버에서 특정 출처 요청을 수락하고 이를 HTTP 응답 헤더를 통해 브라우저에게 알려준다.</li>
<li>브라우저는 서버로부터 받은 CORS 헤더를 확인하고 특정 출처의 요청을 허용한다.<blockquote>
<p>CORS는 SOP의 제한을 안전하게 완화하기 위한 방법이다.</p>
</blockquote>
</li>
</ul>
<h2 id="3차-시도--fetch-api">3차 시도 : fetch API</h2>
<ul>
<li>fetch API를 사용해 파일을 <code>blob</code>형태로 가져오고 이 <code>blob</code>을 이용해 브라우저에서 직접 다운로드할 수 있는 URL을 생성할 수 있다.</li>
</ul>
<pre><code class="language-tsx">const handleFileDownload = () =&gt; {
    setIsDownloading(true);
    const fileUrl = `${serverUrl}${creatorDetailData.file_src}`;

    fetch(fileUrl)
      .then((response) =&gt; response.blob())
      .then((blob) =&gt; {
        const url = window.URL.createObjectURL(new Blob([blob]));
        const a = document.createElement(&quot;a&quot;);
        a.href = url;
        a.download = creatorDetailData.file_src.split(&quot;/&quot;).pop() || &quot;download&quot;;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a); 
        setIsDownloading(false);
      })
      .catch((error) =&gt; {
        console.error(&quot;파일 다운로드 오류:&quot;, error);
        setIsDownloading(false);
      });
  };</code></pre>
<h3 id="🧐-fetch-api-방식을-사용한-이유">🧐 fetch API 방식을 사용한 이유</h3>
<ul>
<li>CORS 설정 변경 없이도 클라이언트 측에서 다운로드가 가능하다.</li>
<li>promise 메서드를 사용하여 에러 핸들링이 가능하다.</li>
<li>promise 객체의 상태값으로 다운로드 중임과 완료를 시각적으로 알려주어 UX를 향상시킨다.</li>
</ul>
<h2 id="결과">결과</h2>
<ul>
<li>Axios를 사용중임으로 fecth를 Axios로 변경</li>
</ul>
<pre><code class="language-tsx">export function CreatorSettingForm({
  creatorDetailData,
  categoryList,
}: CreatorSettingFormProps) {
  const [isDownloading, setIsDownloading] = useState(false);

  const handleFileDownload = () =&gt; {
    setIsDownloading(true);
    const fileUrl = `${creatorDetailData.file_src}`;

    client // AxiosInstance
      .get(fileUrl, { responseType: &quot;blob&quot; })
      .then((response) =&gt; {
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const a = document.createElement(&quot;a&quot;);
        a.href = url;
        a.download = creatorDetailData.file_src.split(&quot;/&quot;).pop() || &quot;download&quot;;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a); 
        setIsDownloading(false);
      })
      .catch((error) =&gt; {
        console.error(&quot;파일 다운로드 오류:&quot;, error);
        setIsDownloading(false);
      });
  };

  return (
    &lt;div className=&quot;space-y-6&quot;&gt;
      &lt;div&gt;
        &lt;h3 className=&quot;text-lg font-medium sticky&quot;&gt;크리에이터 신청&lt;/h3&gt;
        &lt;CreatorDetailTable
          creatorDetailData={creatorDetailData}
          categoryList={categoryList}
        /&gt;
        &lt;div&gt;
          {isDownloading ? (
            &lt;Button disabled&gt;
              &lt;Loader2 className=&quot;mr-2 h-4 w-4 animate-spin&quot; /&gt;
              다운로드 중
            &lt;/Button&gt;
          ) : (
            &lt;Button onClick={handleFileDownload}&gt;첨부파일 다운로드&lt;/Button&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/a67fc5d8-740b-451c-a4e0-927cc86854b6/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux - RTK Query]]></title>
            <link>https://velog.io/@sarang_daddy/Redux-RTK-Query</link>
            <guid>https://velog.io/@sarang_daddy/Redux-RTK-Query</guid>
            <pubDate>Wed, 13 Dec 2023 01:30:07 GMT</pubDate>
            <description><![CDATA[<h1 id="rtk-query로-서버상태-관리하기">RTK Query로 서버상태 관리하기</h1>
<p>앞서 <a href="https://velog.io/@sarang_daddy/Redux-middleware">middleware</a>를 사용하여 비동기 처리를 통해 Redux 상태관리에 중앙집중화할 수 있었다. 다만, 현재 프로젝트에서 관리하던 상태는 서버 상태로서 관리하는 것이 효율성과 단순화에 좋다고 판단되었다.</p>
<p><code>RTK Query</code>는 미들웨어, 액션, 리듀서 작성을 간소화하고 서버 데이터 <code>캐싱</code>, <code>업데이트</code>, <code>리패칭</code>을 효율적으로 관리하도록 도와준다.</p>
<h2 id="1-rtk-query-주요-특징">1. RTK Query 주요 특징</h2>
<ul>
<li>자동 데이터 캐싱<br>: 서버 데이터를 받고 자동으로 캐싱해준다. 이는 동일한 데이터 요청에 대해 불필요한 네트워크 요청을 방지한다.</li>
<li>데이터 패칭 및 동기화<br>: <code>useQuery</code>, <code>useMutation</code>의 React Hooks을 제공한다. 이는 컴포넌트 내에서 API 요청을 쉽게 처리하고 데이터 상태를 관리하게 해준다.</li>
<li>데이터 업데이트 및 재검증<br>: 데이터가 변경되었을 때, RTK Query는 자동으로 데이터를 재검증하고 캐시를 무효화 한다.</li>
</ul>
<blockquote>
<p>위의 특징은 React Query, SWR에서 제공해주는 특징과 비슷하다.</p>
</blockquote>
<h2 id="2-rtk-query만의-특징">2. RTK Query만의 특징</h2>
<p>RTK Query는 <code>React Query</code>, <code>SWR</code>과 같은 데이터 패칭 기술과 비슷하지만 특별한 접근 방식을 API 디자인에 통합했다.</p>
<ul>
<li>Redux 통함<br>: RTK Query는 Redux 상태 관리 시스템과 통합되어 있다.<br>이는 Redux를 기반으로 하는 애플리케이션에서 <strong>전체 애플리케이션 상태를 하나의 저장소에서 관리</strong>할 수 있도록 해준다.</li>
<li>Redux DevTools 통합<br>: Redux DevTools와의 통합을 통해 상태 변화를 추적하고 디버깅에 용이하게 해준다.</li>
</ul>
<blockquote>
<ul>
<li>Redux를 사용하는 프로젝트라면 RTK Query를 함께 사용하여 일관된 상태 관리를 가능토록 할 수 있다.  </li>
<li>Redux에 의존하지 않는 프로젝트에는 상대적으로 가벼운 React Query, SWR의 사용이 적합할 수 있다.</li>
</ul>
</blockquote>
<h2 id="3-rtk-query-사용하기">3. RTK Query 사용하기</h2>
<ul>
<li>RTK Query는 API 슬라이스를 정의하여 사용한다.</li>
<li>이는 서버와의 통신을 위한 여러 엔드포인트를 정의하는 방식으로 작동한다.</li>
</ul>
<pre><code class="language-tsx">import { IUser, IUpdateUsers } from &#39;@/modules/usersType&#39;;
import { createApi, fetchBaseQuery } from &#39;@reduxjs/toolkit/query/react&#39;;

const BASE_API_URL = &#39;http://localhost:9000&#39;;

export const userApi = createApi({
  reducerPath: &#39;userApi&#39;,
  baseQuery: fetchBaseQuery({ baseUrl: BASE_API_URL }),
  endpoints: (builder) =&gt; ({
    getUsers: builder.query&lt;IUser[], void&gt;({
      query: () =&gt; &#39;/user_data&#39;,
    }),
    addUser: builder.mutation&lt;IUser, IUser&gt;({
      query: (user) =&gt; ({
        url: &#39;/user_data&#39;,
        method: &#39;POST&#39;,
        body: user,
      }),
    }),
    updateUsers: builder.mutation&lt;IUpdateUsers, IUpdateUsers&gt;({
      query: ({ ids, updateValue }) =&gt; ({
        url: `/user_data`,
        method: &#39;PATCH&#39;,
        body: { ids, isDeleted: updateValue },
      }),
    }),
  }),
});

export const { useGetUsersQuery, useAddUserMutation, useUpdateUsersMutation } =
  userApi;</code></pre>
<h2 id="4-rtk-query를-통해-관리되는-서버-상태-사용하기">4. RTK Query를 통해 관리되는 서버 상태 사용하기</h2>
<ul>
<li>RTK Query를 사용하면 컴포넌트내에서 데이터 조회, 업데이트가 가능하다.</li>
<li>상태는 전역적으로 관리되기에 필요한 컴포넌트 내에서 사용하기 용이하다.</li>
<li>데이터 캐싱과 리패칭을 통한 동기화 처리에 효율적이다.</li>
</ul>
<pre><code class="language-tsx">// 유저 데이터 조회 요청
const { data: users, isLoading, isError } = useGetUsersQuery();
// 유저 정보 업데이트 요청
const [updateUser] = useUpdateUsersMutation();
// 유저 추가 요청
const [addUser] = useAddUserMutation();

const restoreUser = async (ids: number[]) =&gt; {
  await updateUser({ ids: ids, updateValue: false });
};

const onSubmit = async (data: IUser) =&gt; {
  const newUserData: IUser = {
    id: lastId + 1,
    nickname: data.nickname,
    birthday: data.birthday,
    sex: data.sex,
    isDeleted: false,
  };

  await addUser(newUserData);
  onClose();
};</code></pre>
<h2 id="5-미들웨어-사용후-느낀점">5. 미들웨어 사용후 느낀점</h2>
<ul>
<li>RTK Query를 사용하면 서버와의 상호작용을 처리하는 코드를 줄여 프로젝트의 복잡성을 낮출 수 있다.</li>
<li><code>자동 캐싱</code>, <code>데이터 무효화</code>, <code>리패칭</code> 기능은 성능 최적화에 도움을 준다.</li>
<li>RTK Query의 사용은 Redux의 원래 목적인 <strong>예측 가능한 상태 관리를 유지하면서 비동기 작업을 쉽고 효율적으로 처리할 수 있게 해준다.</strong></li>
<li>RTK Query는 Redux를 이용한 상태 관리에서 비동기 로직이 필요한 서버 상태 관리에 매우 효율적인 도구다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux - middleware]]></title>
            <link>https://velog.io/@sarang_daddy/Redux-middleware</link>
            <guid>https://velog.io/@sarang_daddy/Redux-middleware</guid>
            <pubDate>Sun, 26 Nov 2023 11:01:21 GMT</pubDate>
            <description><![CDATA[<h1 id="redux에-미들웨어-추가하기">Redux에 미들웨어 추가하기</h1>
<p>앞서 구현한 <a href="https://velog.io/@sarang_daddy/React-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0">Redux의 상태관리 로직</a>은 <code>Context API</code>와 <code>useReducer</code> 훅을 사용한 방식과의 큰 차이점이 없다. Redux는 Context API에는 존재하지 않는 <code>미들웨어(Middleware)</code>가 존재한다. Middleware에 대해 학습하고 적용해보자.</p>
<h2 id="1-미들웨어란">1. 미들웨어란?</h2>
<ul>
<li>Redux의 미들웨어를 사용하면 액션 객체가 리듀서에서 처리되기 전에 다른 작업을 수행할 수 있다.</li>
</ul>
<blockquote>
<p>즉, action -&gt; middleware -&gt; dispatch -&gt; reducer 과정으로 액션이 디스페치되면 리듀서에서 해당 액션을 실행하기 전에 추가적인 작업을 수행한다.</p>
</blockquote>
<h2 id="2-미들웨어에서-사용하는-다양한-작업들">2. 미들웨어에서 사용하는 다양한 작업들</h2>
<ul>
<li>특정 조건에 따라 액션이 무시되게 만든다.</li>
<li>액션이 디스패치 됐을 때 이를 수정해서 리듀서에 전달되도록 한다.</li>
<li>특정 액션이 발생했을 때 특정 함수를 실행되도록 한다.</li>
</ul>
<blockquote>
<p>이러한 특징은 특히 <strong>비동기 작업 처리</strong>에서 많이 사용된다.</p>
</blockquote>
<h2 id="3-미들웨어-만들어보기">3. 미들웨어 만들어보기</h2>
<pre><code class="language-ts">// myLogger.ts
import { Action, AnyAction, Dispatch, Middleware, MiddlewareAPI } from &#39;redux&#39;;
import { IRootState } from &#39;@/modules/types&#39;;

const myLogger: Middleware&lt;
  Record&lt;string, never&gt;,
  IRootState,
  Dispatch&lt;AnyAction&gt;
&gt; =
  (store: MiddlewareAPI&lt;Dispatch&lt;AnyAction&gt;, IRootState&gt;) =&gt;
  (next: Dispatch&lt;AnyAction&gt;) =&gt;
  (action: Action&lt;string&gt;) =&gt; {
    console.log(action); // 현재 디스패치되는 액션 출력
    const result = next(action); // 다음 미들웨어 (없다면 리듀서)에게 액션 전달
    console.log(store.getState()); // 리듀서에서 처리된 상태 출력
    return result; // dispatch(action)의 결과물 반환
  };

export default myLogger;</code></pre>
<pre><code class="language-ts">// root/index.ts
import { Provider } from &#39;react-redux&#39;;
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import myLogger from &#39;./middlewares/myLogger&#39;;

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =&gt; getDefaultMiddleware().concat(myLogger),
});

export type AppDispatch = typeof store.dispatch;</code></pre>
<ul>
<li>단순히 전달 받은 액션을 출력하고 다음으로 넘기는 미들웨어</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/634f87ec-5550-4c6e-aec5-7da04b8f8efa/image.png" width="50%">

<blockquote>
<p>미들웨어 안에서는 어떤 작업이든 가능하다.<br>미들웨어는 여러개를 만들어서 적용할 수 있다.</p>
</blockquote>
<pre><code class="language-ts">middleware: (getDefaultMiddleware) =&gt;
  getDefaultMiddleware().concat(myLogger, anotherMiddleware),</code></pre>
<h2 id="4-redux-thunk-프로젝트에-적용하기">4. redux-thunk 프로젝트에 적용하기</h2>
<ul>
<li>redux 비동기 작업으로는 <code>redux-thunk</code>, <code>redux-saga</code>가 많이 사용다.</li>
<li><code>redux-thunk</code>는 리덕스에서 비동기 작업을 처리 할 때 가장 많이 사용되는 미들웨어다.</li>
<li>이 미들웨어를 사용하면 <strong>액션객체가 아닌 함수를 디스패치</strong> 할 수 있다.</li>
</ul>
<blockquote>
<p>thunk(미들웨어)를 사용하여 apis 파일에서 비동기 처리하던 로직을 redux내에서 처리되도록 수정해보자.</p>
</blockquote>
<pre><code class="language-ts">// modules/users.ts
// 비동기 액션 (thunks) 함수
export const getUsersData = createAsyncThunk(
  SET_USERS,
  async (_, { rejectWithValue }) =&gt; {
    try {
      const res = await axiosInstance.get&lt;IUser[]&gt;(&#39;/user_data&#39;);
      return res.data;
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);

export const addUserData = createAsyncThunk(
  ADD_USER,
  async (user: IUser, { rejectWithValue }) =&gt; {
    try {
      await axiosInstance.post&lt;IUser&gt;(&#39;/user_data&#39;, user);
      return user;
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);

export const updateUsersData = createAsyncThunk(
  UPDATE_USERS,
  async (
    { ids, updateValue }: { ids: number[]; updateValue: boolean },
    { rejectWithValue },
  ) =&gt; {
    try {
      const userToUpdate = { isDeleted: updateValue };
      const queryString = ids.join(&#39;,&#39;);
      await axiosInstance.patch(`/user_data?ids=${queryString}`, userToUpdate);
      return { ids, updateValue };
    } catch (err: unknown) {
      return rejectWithValue((err as AxiosError)?.response?.data);
    }
  },
);</code></pre>
<pre><code class="language-ts">// slice 생성 (reducer)
const usersSlice = createSlice({
  name: &#39;users&#39;,
  initialState: initialState,
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      .addCase(getUsersData.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(getUsersData.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(getUsersData.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || null;
      })
      .addCase(addUserData.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(addUserData.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users.push(action.payload);
      })
      .addCase(addUserData.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || null;
      })
      .addCase(updateUsersData.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(updateUsersData.fulfilled, (state, action) =&gt; {
        state.loading = false;
        const { ids, updateValue } = action.payload;
        state.users = state.users.map((user) =&gt; {
          if (ids.includes(user.id)) {
            return { ...user, isDeleted: updateValue };
          }
          return user;
        });
      })
      .addCase(updateUsersData.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || null;
      });
  },
});</code></pre>
<h2 id="5-미들웨어-사용후-느낀점">5. 미들웨어 사용후 느낀점</h2>
<ul>
<li>Redux의 기본적인 액션은 동기적인 페이로드를 전달하는 객체다.</li>
<li>즉, 리듀서에 전달되었을 때 최종 값을 가지고 있어야한다.</li>
<li>하지만, 데이터 요청과 같은 비동기 작업은 응답을 기다려야 한다.</li>
<li>이를 위해 사용되는 것이 <code>middleware</code>다.</li>
<li><code>middleware</code> 덕분에 액션은 <code>Promise</code> 결과를 기다리고 리듀서에 전달 될 수 있다.</li>
<li><code>Promise</code>의 <code>pending</code>, <code>fulfilled</code>, <code>rejected</code>의 상태에 따른 활용도 가능하다.</li>
<li>기존의 비동기 함수 내부에서 <code>dispatch</code>를 호출하는 방법도 동일한 동작을 지원했으나</li>
<li>애플리케이션의 상태를 예측 가능하게 만들자는 Redux 철학에는 <code>middleware</code>를 이용하여 비즈니스 로직을 중앙집중화하는게 적합하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux - 시작하기]]></title>
            <link>https://velog.io/@sarang_daddy/React-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sarang_daddy/React-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 04 Nov 2023 17:56:40 GMT</pubDate>
            <description><![CDATA[<h2 id="why-redux">Why Redux?</h2>
<p>리액트 프로젝트에서 useContext와 useState, useReducer를 활용하여 상태관리를 진행해왔다. 하지만 이들 만으로는 약간의 아쉬움이 존재했다. 특히 전역 상태 관리를 도와주는 Context의 경우 하위 컴포넌트가 모두 리렌더링되기에 Provider마다 하나의 상태값만을 주게되어 전역 상태가 늘어날 수록 추가되는 Provider로 번거로움과 코드 가독성이 좋지 못함을 경험하게 되었다. </p>
<p>전역 상태관리 라이브러리를 사용해보고 싶어졌다.
그렇다면 어떤 라이브러리를 선택해야 할까? 
프로젝트 규모와 팀원들과의 이해관계가 필요하겠지만 학습 단계인 나는 가장 많이 사용되고 있는 Redux를 적용해 보려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/f64013c4-0506-4d62-bcb0-6eaa16d392d7/image.png" alt=""></p>
<p><a href="https://npmtrends.com/jotai-vs-mobx-vs-recoil-vs-redux-vs-zustand">npm trends</a>
<a href="https://www.nextree.io/riaegteu-sangtaegwanri-teurendeuyi-byeonhwa-2/">리액트 상태관리 트렌드의 변화</a></p>
<h2 id="redux의-철학">Redux의 철학</h2>
<p>Redux를 사용하기에 앞서 Redux가 지향하고자 하는 철학에 대해서 알아보자.</p>
<h3 id="1-single-source-of-truth">1. Single Source of Truth</h3>
<p>리덕스는 애플리케이션의 모든 상태를 하나의 스토어(store) 내에 있는 하나의 객체 트리(tree)에 저장한다. 이것은 애플리케이션의 <strong>상태를 예측 가능</strong>하게 만들고, 디버깅과 검사를 용이하게 한다.</p>
<h3 id="2-state-is-read-only">2. State is Read-Only</h3>
<p>상태를 변경할 수 있는 유일한 방법은 액션(action)을 발행(emit)하는 것이다. 액션은 무엇이 일어나야 하는지를 설명하는 일반 <strong>객체</strong>다. 이러한 제한은 <strong>상태 변경을 일관되게 추적</strong>할 수 있게 해주며, 애플리케이션에서 <strong>예측 가능한 동작을 보장</strong>한다.</p>
<h3 id="3-changes-are-made-with-pure-functions">3. Changes are Made with Pure Functions</h3>
<p>액션에 의해 상태 트리가 어떻게 변화하는지를 지정하기 위해 <strong>리듀서(reducer)라 불리는 순수 함수</strong>를 사용한다. 리듀서는 이전 상태와 액션을 인자로 받아 새로운 상태를 반환하는 함수다. 이 원칙은 <strong>상태 변화의 로직을 명확하고 예측 가능하게 만들며, 테스트와 디버깅을 용이</strong>하게 해준다.</p>
<blockquote>
<p>위와 같은 철학은 단방향 상태관리 <strong>Flux 아키텍쳐</strong>를 기반으로 Redux가 발전해왔기 때문이다.</p>
</blockquote>
<p>상태관리로 유명한 패턴 중 하나인 <a href="https://velog.io/@sarang_daddy/JS-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-1.-MVC">MVC 패턴</a>의 경우 양방향 데이터 바인딩이 가진 복잡성과 예측 불가능성으로 view가 많이 필요한 프론트엔드 개발에서는 어려움이 많다. 이를 해결하기 위해, Flux 아키텍처의 단방향 데이터 흐름 개념을 가져와서 발전한게 Redux 라이브러리다.</p>
<h2 id="redux-사용해보기">Redux 사용해보기</h2>
<h3 id="redux-스토어-설정하기">Redux 스토어 설정하기</h3>
<ul>
<li>Redux 스토어를 설정하는 첫 번째 단계는 프로젝트의 루트(index.tsx) 파일에서 생성한다. </li>
<li>아래는 단일 루트 리듀서를 사용하여 스토어를 생성하는데, 이 리듀서는 다양한 모듈의 리듀서들을 가지고 있을 수 있다.</li>
</ul>
<pre><code class="language-jsx">// index.tsx
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import { Provider } from &#39;react-redux&#39;;
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import rootReducer from &#39;./modules&#39;;
import App from &#39;./components/App&#39;;

const store = configureStore({
  reducer: rootReducer,
});

export type RootState = ReturnType&lt;typeof store.getState&gt;;
export type AppDispatch = typeof store.dispatch;

const root = ReactDOM.createRoot(
  document.getElementById(&#39;root&#39;) as HTMLElement,
);

root.render(
  &lt;React.StrictMode&gt;
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
  &lt;/React.StrictMode&gt;,
);</code></pre>
<h3 id="루트-리듀서">루트 리듀서</h3>
<ul>
<li>리듀서들을 포함하고 있는 modules 폴더에서 index.ts 파일을 통해 모든 리듀서들을 결합한다.</li>
<li>예시에서는 유저 데이터와 관련된 모든 액션을 처리하는 users 리듀서를 가지고 있다. </li>
<li>전역에서 관리하고자 하는 상태의 다른 리듀서를 추가할 수 있다.</li>
</ul>
<pre><code class="language-ts">// modules/index.ts
import { combineReducers } from &#39;redux&#39;;
import usersReducer from &#39;./users&#39;;

const rootReducer = combineReducers({
  usersReducer,
  // + 다른 리듀서들
});

export default rootReducer;</code></pre>
<h3 id="리듀서-생성">리듀서 생성</h3>
<ul>
<li>리듀서에 인자로 전달될 액션을 정의한다.</li>
<li>usersReducer는 세 가지 액션 타입 <code>SET_USERS</code>, <code>ADD_USER</code>, <code>UPDATE_USERS</code>에 대응하여 각각 다른 상태 변경을 수행하도록 설계되었다.</li>
</ul>
<pre><code class="language-ts">// modules/users.ts
export const setUsers = (users: IUser[]) =&gt; ({
  type: SET_USERS,
  payload: users,
});

export const addUser = (user: IUser) =&gt; ({
  type: ADD_USER,
  payload: user,
});

export const updateUsers = (userIds: number[]) =&gt; ({
  type: UPDATE_USERS,
  payload: userIds,
});

const initialState: IUserDataState = {
  users: [],
};

const usersReducer = (
  state = initialState,
  action: IUserDataActions,
): IUserDataState =&gt; {
  switch (action.type) {
    case SET_USERS:
      return {
        ...state,
        users: action.payload,
      };
    case ADD_USER:
      return {
        ...state,
        users: [...state.users, action.payload],
      };
    case UPDATE_USERS:
      return {
        ...state,
        users: state.users.map((user) =&gt;
          action.payload.includes(user.id)
            ? { ...user, isDeleted: !user.isDeleted }
            : user,
        ),
      };
    default:
      return state;
  }
};

export default usersReducer;</code></pre>
<h3 id="dispatch로-리듀서에-액션-전달하여-상태-관리하기">dispatch로 리듀서에 액션 전달하여 상태 관리하기</h3>
<ul>
<li>useDispatch로 리듀서에 액션을 전달할 수 있다.</li>
<li>리듀서는 전달 받은 액션 타입에 따라 현재 상태를 반환, 변경, 추가 한다.</li>
<li>유저 정보는 서버에서 관리되는 데이터로 서버와의 통신이 필요하다.</li>
<li>때문에 아래 예제는 비동기 요청 함수 내에서 dispatch를 적용하고 있다.</li>
</ul>
<pre><code class="language-jsx">const getUsersData = async (dispatch: AppDispatch) =&gt; {
  try {
    const res = await axiosInstance.get(`/user_data`);
    if (res.status === 200) {
      // 유저 데이터 반환
      dispatch(setUsers(res.data));
      return res.data;
    }
  } catch (err) {
    throw err;
  }
};

const addUserData = async (data: IUser, dispatch: AppDispatch) =&gt; {
  try {
    const res = await axiosInstance.post(`/user_data`, data);

    if (res.status === 200) {
      // 유저 추가
      dispatch(addUser(res.data));
    }
  } catch (err) {
    return err;
  }
};

const updateUserData = async (
  ids: number[],
  updateValue: boolean,
  dispatch: AppDispatch,
) =&gt; {
  try {
    const userToUpdate = { isDeleted: updateValue };
    const queryString = ids.join(&#39;,&#39;);
    const res = await axiosInstance.patch(
      `/user_data?ids=${queryString}`,
      userToUpdate,
    );

    if (res.status === 200) {
      // 유저 상태 변경
      dispatch(updateUsers(ids));
    }
  } catch (err) {
    return err;
  }
};</code></pre>
<h2 id="redux를-활용한-전역-상태-활용하기">Redux를 활용한 전역 상태 활용하기</h2>
<ul>
<li>전역 단일 스토어에서 관리되는 리듀서이기에 어느 컴포넌트에서든 액션을 통해 서버로 부터 상태를 가져오거나 변경할 수 있다.</li>
</ul>
<pre><code class="language-jsx">getUsersData(dispatch)
updateUserData(ids, false, dispatch);
addUserData(newUserData, dispatch);</code></pre>
<ul>
<li>useSelector로 상태를 구독하여 UI를 렌더링할 수 있다.</li>
</ul>
<pre><code class="language-jsx">const users = useSelector((state: IRootState) =&gt; state.users.users);

// jsx
users.map((user) =&gt; (
    &lt;Thumbnail
     key={user.id}
     user={user}
     isChecked={checkedUserIds.includes(user.id)}
     onCheckboxChange={onCheckboxChange}
     isActive={isActive}
    /&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Framer Motion의 Custom 속성을 활용한 동적 애니메이션 구현]]></title>
            <link>https://velog.io/@sarang_daddy/Framer-Motion%EC%9D%98-Custom-%EC%86%8D%EC%84%B1%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%A0%81-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@sarang_daddy/Framer-Motion%EC%9D%98-Custom-%EC%86%8D%EC%84%B1%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%A0%81-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 21 Oct 2023 06:59:15 GMT</pubDate>
            <description><![CDATA[<p>React에서 Framer Motion 라이브러리로 애니메이션 효과를 적용하던 중 의도치 않은 버그가 발생되었다. 어떻게 버그가 해결되었는지 그리고 왜 이런 버그가 발생했는지 기록하고 정리해보자.</p>
<h2 id="🐛-슬라이더-애니메이션-효과-버그-발생">🐛 슬라이더 애니메이션 효과 버그 발생</h2>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/16703687-5e80-422f-84c1-561514b995e5/image.gif" alt=""></p>
<ul>
<li>반대 방향으로 슬라이더를 전환하면 이전 슬라이더 페이지가 사라지는 방향이 이전 속성 기반으로 움직이는 버그가 발생.</li>
</ul>
<h2 id="❌-문제-코드">❌ 문제 코드</h2>
<pre><code class="language-tsx">const [direction, setDirection] = useState&lt;&#39;right&#39; | &#39;left&#39;&gt;(&#39;right&#39;);

&lt;S.Slider&gt;
      &lt;AnimatePresence initial={false} onExitComplete={toggleLeaving}&gt;
        &lt;S.SliderControl&gt;
          &lt;S.StyledIcon
            onClick={() =&gt; handleSliderPage(ControlKeys.LEFT)}
            icon=&quot;chevron-left&quot;
            size=&quot;3x&quot;
          /&gt;
          &lt;S.StyledIcon
            onClick={() =&gt; handleSliderPage(ControlKeys.RIGHT)}
            icon=&quot;chevron-right&quot;
            size=&quot;3x&quot;
          /&gt;
        &lt;/S.SliderControl&gt;
        &lt;S.Row
          variants={rowVariants[direction]}
          initial=&quot;hidden&quot;
          animate=&quot;visible&quot;
          exit=&quot;exit&quot;
          transition={{ type: &#39;tween&#39;, duration: 1 }}
          key={sliderPage}
        &gt;

export const rowVariants = {
  left: {
    hidden: { x: -window.innerWidth },
    visible: { x: 0 },
    exit: { x: window.innerWidth },
  },
  right: {
    hidden: { x: window.innerWidth },
    visible: { x: 0 },
    exit: { x: -window.innerWidth },
  },
};</code></pre>
<ul>
<li>방향 <code>상태</code>를 만들고 이벤트가 일어나면 해당 상태를 애니메이션 속성으로 선택<ul>
<li>오른쪽 버튼을 누르면 variants의 <code>right</code>를 선택</li>
<li>왼쪽 버튼을 누르면 variants의 <code>left</code>를 선택</li>
</ul>
</li>
</ul>
<h2 id="✅-해결-코드">✅ 해결 코드</h2>
<ul>
<li><strong>AnimatePresence의 custom 속성 개념을 확인</strong></li>
<li>custom을 이용하여 motion의 애니메이션에 <code>prop(사용자 정의값)</code>을 전달</li>
</ul>
<pre><code class="language-tsx">const [direction, setDirection] = useState&lt;&#39;right&#39; | &#39;left&#39;&gt;(&#39;right&#39;);

&lt;S.Slider&gt;
      &lt;AnimatePresence
        initial={false}
        custom={direction}
        onExitComplete={toggleLeaving}
      &gt;
        &lt;S.SliderControl&gt;
          &lt;S.StyledIcon
            onClick={() =&gt; handleSliderPage(ControlKeys.LEFT)}
            icon=&quot;chevron-left&quot;
            size=&quot;3x&quot;
          /&gt;
          &lt;S.StyledIcon
            onClick={() =&gt; handleSliderPage(ControlKeys.RIGHT)}
            icon=&quot;chevron-right&quot;
            size=&quot;3x&quot;
          /&gt;
        &lt;/S.SliderControl&gt;
        &lt;S.Row
          custom={direction}
          variants={rowVariants}
          initial=&quot;hidden&quot;
          animate=&quot;visible&quot;
          exit=&quot;exit&quot;
          transition={{ type: &#39;tween&#39;, duration: 1 }}
          key={sliderPage}
        &gt;

export const rowVariants = {
  hidden: (direction: string) =&gt; ({
    x: direction === &#39;right&#39; ? window.innerWidth : -window.innerWidth,
  }),
  visible: { x: 0 },
  exit: (direction: string) =&gt; ({
    x: direction === &#39;right&#39; ? -window.innerWidth : window.innerWidth,
  }),
};</code></pre>
<ul>
<li>버그가 사라지도 의도하던 애니메이션이 잘 적용되고 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/6429e847-aae3-48c8-8f8c-0ff9af477c50/image.gif" alt=""></p>
<h2 id="📝-결론">📝 결론</h2>
<ul>
<li>두 코드의 차이점은 S.Row 컴포넌트에서 <code>variants</code> prop을 어떻게 사용하는가가 다르다.</li>
</ul>
<h3 id="첫-번째-코드">첫 번째 코드</h3>
<ul>
<li>첫 번째 코드에서 <code>rowVariants</code>는 <code>direction</code> 값에 따라 다른 객체를 반환한다.</li>
<li><code>S.Row</code> 컴포넌트에서 <code>variants</code> prop은 <code>rowVariants[direction]</code>을 통해 직접 해당 객체를 가져온다.</li>
<li>즉, <code>S.Row</code> 컴포넌트가 리렌더링될 때마다 <code>variants</code> prop의 참조(객체)가 변경된다.</li>
</ul>
<h3 id="두-번째-코드">두 번째 코드</h3>
<ul>
<li>두 번째 코드에서는 <code>custom</code> prop을 통해 <code>direction</code> 값을 전달하고 <code>rowVariants</code>가 함수를 통해 값을 계산한다.</li>
<li>동일한 객체에서 값을 다르게 계산한다.</li>
<li><code>S.Row</code> 컴포넌트의 <code>variants</code> prop은 <code>rowVariants</code>를 직접 참조하고 있다.</li>
<li>즉, <code>S.Row</code> 컴포넌트가 리렌더링되더라도 <code>variants</code>  prop의 참조(객체)는 동일하여 추적이 된다.</li>
</ul>
<blockquote>
</blockquote>
<p>Framer Motion은 내부적으로 애니메이션 상태를 <code>추적</code>하기 위해 참조를 사용한다.
동적으로 애니메이션의 속성을 변경하고자 할때 객체를 할당하면 리렌더링마다 새로운 객체로 참조하게된다.
따라서, 객체의 참조는 일정하게 유지하면서 동적인 값을 주기 위해 custom 속성을 이용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - Suspense]]></title>
            <link>https://velog.io/@sarang_daddy/React-Suspense</link>
            <guid>https://velog.io/@sarang_daddy/React-Suspense</guid>
            <pubDate>Mon, 16 Oct 2023 05:14:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/28b4adb0-dde3-48f8-951f-ebfae0f6eb25/image.png" alt=""></p>
<h1 id="why-suspense">Why Suspense?</h1>
<ul>
<li>기존에는 각 컴포넌트에서 데이터를 비동기로 요청할 때, 해당 컴포넌트 내에서 로딩 상태를 관리했다.</li>
</ul>
<pre><code class="language-tsx">// Home
export const Home = () =&gt; {
  const { data: characters, status } = useFetch({
    fetchFunction: fetchCharacters,
    args: [50],
    cacheKey: ROUTE_PATH.HOME,
  });
  const charactersList = characters?.results;

  return (
    &lt;S.Container&gt;
      // 비동기 요청이 처리될 때까지 Loading 컴포넌트 렌더링
      {status === &#39;pending&#39; ? (
        &lt;Loading /&gt;
      ) : (
        &lt;S.Characters&gt;</code></pre>
<pre><code class="language-tsx">// Detail
export const Detail = () =&gt; {
  const { id } = useParams();
  const { data: characterDetail, status } = useFetch({
    fetchFunction: fetchCharacterDetail,
    args: [id],
    cacheKey: `${ROUTE_PATH.DETAIL}/${id}`,
  });
  const detailsInfo = characterDetail?.results[0];

 return (
    &lt;&gt;
      // 비동기 요청이 처리될 때까지 Loading 컴포넌트 렌더링
      {status === &#39;pending&#39; ? (
        &lt;Loading /&gt;
      ) : (
        &lt;S.Container&gt;</code></pre>
<ul>
<li>이는 각 컴포넌트마다 로딩 상태를 표시하는 로직을 포함하게 되어, 코드의 중복과 복잡성을 증가시킨다.</li>
</ul>
<blockquote>
<p>React의 <code>Suspense</code>와 <code>lazy</code> 기능을 사용하여 이러한 문제를 효과적으로 해결할 수 있다.</p>
</blockquote>
<h2 id="suspense"><a href="https://react-ko.dev/reference/react/Suspense">Suspense</a></h2>
<ul>
<li>Suspense는 React에서 <code>비동기 작업의 결과를 기다리는 경계</code>를 설정하는 컴포넌트다.</li>
<li>주로 lazy와 함께 사용하여 코드 분할 및 비동기 컴포넌트 로딩을 처리하며,</li>
<li>데이터를 불러오는 동안 보여줄 대체 컴포넌트 (보통 로딩 인디케이터)를 설정하는 데 사용된다.</li>
</ul>
<h3 id="suspense의-주요-특징">Suspense의 주요 특징</h3>
<ul>
<li><p>비동기 경계 설정<br>: Suspense 내부의 컴포넌트들이 데이터를 불러오는 동안 대체 컴포넌트를 보여준다.<br> 이를 통해 비동기 작업 중에도 사용자에게 즉시 반응하는 UI를 제공할 수 있다.</p>
</li>
<li><p>통합된 로딩 상태 관리<br>: 전통적으로 각 컴포넌트 내에서 로딩 상태를 관리해야 했으나,<br> Suspense를 사용하면 애플리케이션 전체에서 중앙 집중식으로 로딩 상태를 관리할 수 있다.</p>
</li>
<li><p>코드 분할과 연계<br>: React의 lazy 함수와 결합하여 컴포넌트의 코드 분할을 쉽게 할 수 있다.<br> 이렇게 하면 특정 컴포넌트가 실제로 렌더링 될 때까지 해당 컴포넌트의 코드를 로딩하지 않아, 초기 로딩 성능이 향상된다.</p>
</li>
</ul>
<h3 id="suspense-사용법">Suspense 사용법</h3>
<pre><code class="language-tsx">import { Suspense } from &#39;react&#39;;
import Albums from &#39;./Albums.js&#39;;

export default function ArtistPage({ artist }) {
  return (
    &lt;&gt;
      &lt;h1&gt;{artist.name}&lt;/h1&gt;
      &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
        &lt;Albums artistId={artist.id} /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}

function Loading() {
  return &lt;h2&gt;🌀 Loading...&lt;/h2&gt;;
}</code></pre>
<ul>
<li>Suspense 경계 내의 컴포넌트 Albums는 비동기적으로 데이터를 요청한다.</li>
<li><strong>Albums는 비동기 작업 중일 때 해당 <code>Promise</code>를 <code>throw</code> 해야한다.</strong></li>
<li>Albums는 데이터를 아직 받지 못했다면 Promise 가 <code>pending</code>를 <code>throw</code>한다.</li>
<li>Suspense는 Albums가 던진 Promise 상태를 받아 <code>pending</code>이면 <code>fallback</code>에 지정된 컴포넌트를 렌더링 한다.</li>
<li>데이터가 준비되면 (Promise가 <code>fulfilled</code>되면) 해당 컴포넌트(Albums)를 렌더링 한다.</li>
</ul>
<h2 id="lazy"><a href="https://react-ko.dev/reference/react/lazy">lazy</a></h2>
<ul>
<li>lazy는 React의 기능 중 하나로, 컴포넌트를 동적으로 로드하는 데 사용된다.</li>
<li>이는 앱의 초기 로드 시 필요하지 않은 컴포넌트를 로드하지 않아 번들 크기를 줄이고 초기 로딩 속도를 개선할 수 있다.</li>
<li>사용자가 특정 기능이나 뷰에 액세스할 때만 해당 컴포넌트를 로드한다.</li>
</ul>
<h3 id="lazy-사용법">lazy 사용법</h3>
<pre><code class="language-tsx">import { Suspense, lazy } from &#39;react&#39;;

const OtherComponent = lazy(() =&gt; import(&#39;./OtherComponent&#39;));

function MyComponent() {
  return (
    &lt;div&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
        &lt;OtherComponent /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li>동적으로 불러내고자 하는 컴포넌트(OtherComponent)를 lazy 함수로 호출한다.</li>
<li>lazy는 Suspense 컴포넌트와 함께 사용된다.</li>
<li>사용자가 OtherComponent를 요청하면 로드되기에 로드되는 동안 Suspense의 fallback 컴포넌트를 보여준다.</li>
</ul>
<h1 id="🚀-코드에-적용하기">🚀 코드에 적용하기</h1>
<h2 id="1-데이터-요청-usefetch에서-promise를-throw-하도록-수정">1. 데이터 요청 useFetch에서 Promise를 throw 하도록 수정</h2>
<pre><code class="language-tsx">import { useState, useEffect, useRef } from &#39;react&#39;;
import { useCacheContext } from &#39;@/contexts/CacheContext&#39;;

type Status = &#39;initial&#39; | &#39;pending&#39; | &#39;fulfilled&#39; | &#39;rejected&#39;;

interface UseFetch&lt;T&gt; {
  data?: T;
  status: Status;
  error?: Error;
  cacheKey: string;
}

interface FetchOptions&lt;T&gt; {
  fetchFunction: (...args: any[]) =&gt; Promise&lt;T&gt;;
  args: any[];
  cacheKey: string;
}

export const useFetch = &lt;T&gt;({
  fetchFunction,
  args,
  cacheKey,
}: FetchOptions&lt;T&gt;): UseFetch&lt;T&gt; =&gt; {
  const [state, setState] = useState&lt;UseFetch&lt;T&gt;&gt;({
    status: &#39;initial&#39;,
    data: undefined,
    error: undefined,
    cacheKey,
  });

  const { cacheData, isDataValid } = useCacheContext();
  const activePromise = useRef&lt;Promise&lt;void&gt; | null&gt;(null);

  useEffect(() =&gt; {
    let ignore = false;

    const fetchData = async () =&gt; {
      if (ignore) return;

      try {
        const response = await fetchFunction(...args);
        cacheData(cacheKey, response);

        setState((prevState) =&gt; ({
          ...prevState,
          status: &#39;fulfilled&#39;,
          data: response,
          cacheKey,
        }));
      } catch (error) {
        setState((prevState) =&gt; ({
          ...prevState,
          status: &#39;rejected&#39;,
          error: error as Error,
          cacheKey,
        }));
      }
    };

    if (state.status === &#39;initial&#39;) {
      if (isDataValid(cacheKey)) {
        setState((prevState) =&gt; ({
          ...prevState,
          status: &#39;fulfilled&#39;,
          data: cacheData(cacheKey),
          cacheKey,
        }));
      } else {
        setState((prevState) =&gt; ({
          ...prevState,
          status: &#39;pending&#39;,
        }));
        activePromise.current = fetchData();
      }
    }

    return () =&gt; {
      ignore = true;
    };
  }, [fetchFunction, cacheKey, cacheData, isDataValid, state.status]);

  if (state.status === &#39;pending&#39; &amp;&amp; activePromise.current) {
    throw activePromise.current;
  }
  if (state.status === &#39;rejected&#39; &amp;&amp; state.error) {
    throw state.error;
  }

  return state;
};</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/eec76573-9b94-4e47-b051-e2fa6423e0c2/image.png" width="50%">

<ul>
<li>상태 업데이트에 따라 리렌더링을 방지하기 위해 <code>throw</code>할 Promise를 <code>useRef</code>에 저장한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/abf27cfb-17c7-4dd7-ae44-04dfbd907c2c/image.png" width="50%">

<ul>
<li>Promise가 pending 상태가 되면 activePromise를 업데이트 해준다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/31d3eeee-c447-48c8-aa48-897e9ab8f31f/image.png" width="50%">

<ul>
<li>Promise 상태가 <code>pending</code>이고 activePromise.current에 값이 있으면 해당 Promise를 던져 Suspense를 활성화시킨다.</li>
<li>Promise 상태가 <code>rejected</code>이고 오류가 있으면 해당 오류를 던져서 Error Boundary에서 처리할 수 있게 한다.</li>
</ul>
<h2 id="2-각-컴포넌트에서-관리하던-로딩-상태-로직을-제거한다">2. 각 컴포넌트에서 관리하던 로딩 상태 로직을 제거한다.</h2>
<pre><code class="language-tsx">// Home
export const Home = () =&gt; {
  const { data: characters } = useFetch({
    fetchFunction: fetchCharacters,
    args: [50],
    cacheKey: ROUTE_PATH.HOME,
  });
  const charactersList = characters?.results;

  return (
    &lt;S.Container&gt;
        &lt;S.Characters&gt;
        // 렌더링 코드 중략</code></pre>
<pre><code class="language-tsx">// Detail
export const Detail = () =&gt; {
  const { id } = useParams();
  const { data: characterDetail } = useFetch({
    fetchFunction: fetchCharacterDetail,
    args: [id],
    cacheKey: `${ROUTE_PATH.DETAIL}/${id}`,
  });
  const detailsInfo = characterDetail?.results[0];

 return (
    &lt;&gt;
      {detailsInfo &amp;&amp; (
        &lt;S.Container&gt;
        // 렌더링 코드 중략</code></pre>
<h2 id="3-router-로직에서-비동기-요청-처리로-인한-로딩-상태를-관리한다">3. router 로직에서 비동기 요청 처리로 인한 로딩 상태를 관리한다.</h2>
<pre><code class="language-tsx">import { Suspense, lazy } from &#39;react&#39;;
import { createBrowserRouter } from &#39;react-router-dom&#39;;
import { Layout } from &#39;@/routes/Layout&#39;;
import { ROUTE_PATH } from &#39;@/router/routePath&#39;;
import { NotFound } from &#39;@/routes/NotFound&#39;;
import { ErrorBoundary } from &#39;@/components/ErrorBoundary&#39;;
import { Loading } from &#39;@/components/Loading&#39;;
import Home from &#39;@/routes/Home&#39;;

const Detail = lazy(() =&gt; import(&#39;@/routes/Detail&#39;));

export const router = createBrowserRouter([
  {
    element: &lt;Layout /&gt;,
    path: ROUTE_PATH.ROOT,
    errorElement: &lt;NotFound /&gt;,
    children: [
      {
        path: ROUTE_PATH.HOME,
        element: (
          &lt;ErrorBoundary&gt;
            &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
              &lt;Home /&gt;
            &lt;/Suspense&gt;
          &lt;/ErrorBoundary&gt;
        ),
      },
      {
        path: ROUTE_PATH.DETAIL,
        element: (
          &lt;ErrorBoundary&gt;
            &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
              &lt;Detail /&gt;
            &lt;/Suspense&gt;
          &lt;/ErrorBoundary&gt;
        ),
      },
    ],
  },
]);</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/b55b50c8-a2ef-42b7-ac41-eef466ea411d/image.png" width="50%">

<ul>
<li>lazy를 사용하여 Detail 컴포넌트는 초기 로드에서 제외하고 사용자가 엑세스 하는 경우 로드한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Recoil - Selectors]]></title>
            <link>https://velog.io/@sarang_daddy/Recoil-Selectors</link>
            <guid>https://velog.io/@sarang_daddy/Recoil-Selectors</guid>
            <pubDate>Mon, 09 Oct 2023 04:26:25 GMT</pubDate>
            <description><![CDATA[<p>Recoil의 <code>Selectors</code>와 <code>React Hook Form</code>을 학습하며 ToDoList를 만들어 보자.</p>
<h2 id="selectors란">Selectors란?</h2>
<ul>
<li>앞서 학습한 <code>Atoms</code>와 함께 Recoil에는 <code>Selectors</code>라는 개념이 존재한다.</li>
<li><code>Selectors</code>는 Atoms 상태 값을 동기 또는 비동기 방식을 통해 <code>변환</code>한다.</li>
</ul>
<blockquote>
<p>Recoil의 selectors는 파생 상태(derived state) 또는 상태 변환을 계산하고 관리하기 위한 도구다.<br>selectors는 기본적으로 하나 이상의 atom 또는 다른 selectors의 상태를 기반으로 새로운 상태 값을 계산하는 <code>순수 함수</code>다.</p>
</blockquote>
<h2 id="selectors의-특징">Selectors의 특징</h2>
<ul>
<li>Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수(pure function)다.</li>
<li>상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다.</li>
<li>컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링 된다.</li>
<li>컴포넌트의 관점에서 보면 selectors와 atoms는 동일한 인터페이스를 가지므로 서로 대체할 수 있다.</li>
</ul>
<h3 id="1-파생-상태-get">1. 파생 상태 (get)</h3>
<ul>
<li>여러 <code>atom</code>의 값을 결합하여 새로운 상태 값을 계산할 수 있다.</li>
</ul>
<pre><code class="language-jsx">const lengthState = selector({
  key: &#39;lengthState&#39;,
  get: ({ get }) =&gt; {
    const text = get(textState);
    return text.length;
  },
});</code></pre>
<h3 id="2-비동기-쿼리-get">2. 비동기 쿼리 (get)</h3>
<ul>
<li><code>selectors</code>는 비동기 작업을 수행하고 결과를 상태로 반환할 수 있다.</li>
</ul>
<pre><code class="language-jsx">const userDataSelector = selector({
  key: &#39;userData&#39;,
  get: async ({ get }) =&gt; {
    const response = await fetch(&#39;/api/user&#39;);
    return response.json();
  },
});</code></pre>
<h3 id="3-다른-상태-변경-set">3. 다른 상태 변경 (set)</h3>
<ul>
<li>selectors는 set 함수를 사용하여 연관된 atom 또는 다른 selectors의 상태를 변경할 수 있다.</li>
</ul>
<pre><code class="language-jsx">const resetAllData = selector({
  key: &#39;resetAllData&#39;,
  set: ({ reset }) =&gt; {
    reset(dataAtom1);
    reset(dataAtom2);
  },
});</code></pre>
<h3 id="4-캐싱">4. 캐싱</h3>
<ul>
<li>selectors는 계산된 결과를 자동으로 캐시한다.</li>
<li>같은 입력에 대한 결과가 이미 캐시되어 있으면, selectors는 캐시된 값을 재사용한다.</li>
</ul>
<h3 id="5-의존성-관리">5. 의존성 관리</h3>
<ul>
<li>selectors는 자동으로, 의존하는 atom 또는 selectors의 변화를 감지한다.</li>
<li>따라서 의존하는 상태가 변경되면 해당 selectors도 자동으로 업데이트된다.</li>
</ul>
<h2 id="todolist-만들기">ToDoList 만들기</h2>
<h3 id="1-recoil-상태-설정-atoms-selectors">1. Recoil 상태 설정 (Atoms, Selectors)</h3>
<ul>
<li>전역 상태로 관리할 toDoList 배열을 atoms로 정의한다.</li>
<li>toDoList에 존재하는 toDo들의 상태를 기반으로 filter하기 위한 Selectors를 정의한다.</li>
</ul>
<pre><code class="language-tsx">import { atom, selector } from &#39;recoil&#39;;

export enum FilterKeys {
  &#39;TOGO&#39; = &#39;TOGO&#39;,
  &#39;BEEN&#39; = &#39;BEEN&#39;,
  &#39;LIKE&#39; = &#39;LIKE&#39;,
  &#39;DEL&#39; = &#39;DEL&#39;,
}

export interface IToDo {
  id: number;
  text: string;
  filterKey: FilterKeys;
}

export const toDoListFilterState = atom&lt;FilterKeys&gt;({
  key: &#39;toDoListFilterState&#39;,
  default: FilterKeys.TOGO,
});

export const toDoListState = atom&lt;IToDo[]&gt;({
  key: &#39;toDoListState&#39;,
  default: JSON.parse(localStorage.getItem(&#39;toDoList&#39;) || &#39;[]&#39;),
});

export const toDoSelector = selector({
  key: &#39;toDoSelector&#39;,
  get: ({ get }) =&gt; {
    const toDos = get(toDoListState);

    return [
      toDos.filter((toDo) =&gt; toDo.filterKey === FilterKeys.TOGO),
      toDos.filter((toDo) =&gt; toDo.filterKey === FilterKeys.BEEN),
      toDos.filter((toDo) =&gt; toDo.filterKey === FilterKeys.LIKE),
    ];
  },
});</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/ec2e3248-e06c-43c8-b4d4-b55061195560/image.png" width="50%">

<ul>
<li>ToDo가 되는 개체의 타입을 정의한다.</li>
<li>Filter로 사용될 key값을 enum을 사용해 안정성을 높여준다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/f9ba1912-d95d-4e16-bb56-ecd5acd536b1/image.png" width="50%">

<ul>
<li>사용자가 입력하는 ToDo들을 가지는 리스트 상태다.</li>
<li>로컬스토리지에 저장된 값이 있다면 불러오고 없다면 빈 배열로 초기화 한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/70e11749-aff5-4741-ad4b-0f70f98d7b37/image.png" width="50%">

<ul>
<li>toDo의 filterKey 속성에 들어갈 값</li>
<li>최초 toDo 생성시 TOGO 키값으로 초기화 한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/ac85de0a-288a-4e87-b788-871da71a2ff8/image.png" width="50%">

<ul>
<li><code>toDoListState</code> atom의 전체 할 일 목록을 가져와서, 각 filterKey에 따라 필터링하여 새로운 배열을 반환한다.</li>
<li>이렇게 <strong>원본 상태를 기반으로 새로운 값을 계산하는</strong> 기능이 selector의 핵심적인 특징이다.</li>
</ul>
<h3 id="2-form-컴포넌트-구현">2. Form 컴포넌트 구현</h3>
<ul>
<li>React Hook Form의 useForm 훅을 사용하여 폼을 구현한다.</li>
<li>폼 요소(예: input)에 register 함수를 연결하여 폼과 관련된 상태와 함수를 관리한다.</li>
<li>필요한 경우 유효성 검사 또는 에러 핸들링을 구현한다.</li>
</ul>
<pre><code class="language-tsx">import { useEffect } from &#39;react&#39;;
import { useRecoilValue } from &#39;recoil&#39;;
import { toDoSelector, toDoListState } from &#39;@/atoms&#39;;
import ToDo from &#39;./ToDo&#39;;
import CreateToDo from &#39;./CreateToDo&#39;;

export default function ToDoList() {
  // 전역 상태로 관리되는 toDoListState 상태값을 불러온다.
  const toDoList = useRecoilValue(toDoListState);
  // toDoSelector로 필터링된 배열값들을 불러온다.
  const [toGo, been, like] = useRecoilValue(toDoSelector);

  // toDoList의 최신 상태를 로컬스토리지에 저장한다.
  useEffect(() =&gt; {
    localStorage.setItem(&#39;toDoList&#39;, JSON.stringify(toDoList));
  }, [toDoList]);

  return (
    &lt;main&gt;
      &lt;h2&gt;내가 가고싶은 나라들&lt;/h2&gt;
      &lt;CreateToDo /&gt;
      &lt;ul&gt;{toGo?.map((toDo) =&gt; &lt;ToDo key={toDo.id} {...toDo} /&gt;)}&lt;/ul&gt;
      &lt;h2&gt;내가 가본 나라들&lt;/h2&gt;
      &lt;ul&gt;{been?.map((toDo) =&gt; &lt;ToDo key={toDo.id} {...toDo} /&gt;)}&lt;/ul&gt;
      &lt;h2&gt;내가 좋아하는 나라들&lt;/h2&gt;
      &lt;ul&gt;{like?.map((toDo) =&gt; &lt;ToDo key={toDo.id} {...toDo} /&gt;)}&lt;/ul&gt;
    &lt;/main&gt;
  );
}</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/542c8f44-4d00-4b1c-ac06-c3d3f57c593a/image.png" width="50%">

<ul>
<li>toDoSelector로 필터링된 값들을 UI 요소로 사용하고 있다.</li>
<li>실제 상태의 원본값은 필터링 되어 분리되어 있지 않다.</li>
<li>원본 상태의 훼손과 변경없이 selector를 활용한 매우 유용한 기능이다.</li>
</ul>
<h3 id="3-컴포넌트에서-recoil-상태의-사용">3. 컴포넌트에서 Recoil 상태의 사용</h3>
<ul>
<li>폼의 제출 함수(onSubmit) 내에서 Recoil 상태를 업데이트하거나, 폼의 초기 값으로 Recoil 상태를 사용한다.</li>
<li>필요한 경우 selectors를 사용하여 폼 데이터를 변환하거나 비동기 작업을 수행한다.</li>
</ul>
<pre><code class="language-tsx">// CreateToDo 컴포넌트
const CreateToDo = () =&gt; {
  // toDoList에 새로운 toDo를 추가하기 위해 useSetRecoilState 함수 호출
  const setToDoList = useSetRecoilState(toDoListState);
  // 새로운 toDo의 필터키 값을 주기위해 초기화 filterKey값 호출
  const filterKey = useRecoilValue(toDoListFilterState);

  const { register, handleSubmit, setValue, formState } = useForm&lt;IForm&gt;();

  const handleValid = ({ toDo }: IForm) =&gt; {
    setToDoList((prevToDos) =&gt; [
      ...prevToDos,
      { text: toDo, id: Date.now(), filterKey },
    ]);
    setValue(&#39;toDo&#39;, &#39;&#39;);
  };

  return (
    &lt;form onSubmit={handleSubmit(handleValid)}&gt;
      &lt;input
        {...register(&#39;toDo&#39;, {
          required: &#39;Please write a To Do&#39;,
        })}
        placeholder=&quot;나라 이름&quot;
      /&gt;
      &lt;button&gt;가자!&lt;/button&gt;
      &lt;span&gt;{formState.errors.toDo?.message}&lt;/span&gt;
    &lt;/form&gt;
  );
};

export default CreateToDo;</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/5b293c0b-e3b9-4c06-b403-b0ed5c63a8ec/image.png" width="50%">

<ul>
<li>RHF로 제출된 toDo를 기존 toDoList에 추가하는 함수 로직</li>
<li>최초 filterKey 값은 전역 상태에서 지정한 TODO가 된다.</li>
<li>전역 상태로 관리하기에 전역 상태에서만 변경해주면 구독 중인 모든 컴포넌트에 동일하기 적용된다.</li>
<li>즉, 다른 컴포넌트에서 filterKey를 활용한 기능 추가에 더욱 유용하다.</li>
</ul>
<pre><code class="language-tsx">// ToDo 컴포넌트
const ToDo = ({ text, filterKey, id }: IToDo) =&gt; {
  // toDoList의 toDo 상태를 변경하기 위해 useSetRecoilState 함수 호출
  const setToDoList = useSetRecoilState(toDoListState);

  const onClick = (setFilterKey: IToDo[&#39;filterKey&#39;]) =&gt; {
    setToDoList((prevToDos) =&gt; {
      if (setFilterKey === FilterKeys.DEL) {
        return prevToDos.filter((toDo) =&gt; toDo.id !== id);
      }

      return prevToDos.map((toDo) =&gt;
        toDo.id === id ? { ...toDo, filterKey: setFilterKey } : toDo,
      );
    });
  };

  return (
    &lt;li&gt;
      &lt;span&gt;{text}&lt;/span&gt;
      {filterKey == FilterKeys.TOGO &amp;&amp; (
        &lt;&gt;
          &lt;button onClick={() =&gt; onClick(FilterKeys.BEEN)}&gt;BEEN&lt;/button&gt;
          &lt;button onClick={() =&gt; onClick(FilterKeys.DEL)}&gt;DEL&lt;/button&gt;
        &lt;/&gt;
      )}
      {filterKey == FilterKeys.BEEN &amp;&amp; (
        &lt;&gt;
          &lt;button onClick={() =&gt; onClick(FilterKeys.TOGO)}&gt;TOGO&lt;/button&gt;
          &lt;button onClick={() =&gt; onClick(FilterKeys.LIKE)}&gt;LIKE&lt;/button&gt;
        &lt;/&gt;
      )}
      {filterKey == FilterKeys.LIKE &amp;&amp; (
        &lt;button onClick={() =&gt; onClick(FilterKeys.BEEN)}&gt;UNLIKE&lt;/button&gt;
      )}
    &lt;/li&gt;
  );
};

export default ToDo;</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/a6557d5d-9f8d-4a4b-a7df-5230d53d7af6/image.png" width="50%">

<ul>
<li>ToDo 컴포넌트는 앞서 toDoList에서 필터링된 상태값들을 props로 받아오고 있다.</li>
<li>원본 상태를 변경하지 않고 selector로 새로운 상태값을 파생하여 만든 컴포넌트가 된다.</li>
</ul>
<blockquote>
<p>원본 상태를 필터링하여 렌더링한 UI, 원본 상태를 파생하여 생성한 컴포넌트 모두 원본 상태와 의존되어 있다.</p>
</blockquote>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/0643c229-c87b-4693-a3c5-7ed68af8c7fd/image.png" width="50%">

<ul>
<li>파생된 값으로 만들어진 컴포넌트 ToDo에서도 전역 상태의 toDoList를 변경할 수 있다.</li>
<li>toDoList 상태를 변경 하면 의존성에 의해서 toDoList를 구독한 컴포넌트들은 모두 리렌더링 된다.</li>
</ul>
<h2 id="todolist-결과물">ToDoList 결과물</h2>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/27114a9c-8708-45f5-998f-4e83fffa84fc/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Recoil - Atoms]]></title>
            <link>https://velog.io/@sarang_daddy/Recoil-Atoms</link>
            <guid>https://velog.io/@sarang_daddy/Recoil-Atoms</guid>
            <pubDate>Thu, 05 Oct 2023 07:04:56 GMT</pubDate>
            <description><![CDATA[<h1 id="why-recoil">Why Recoil?</h1>
<ul>
<li>Recoil은 React 애플리케이션의 상태 관리를 위한 라이브러리다.</li>
<li>호환성과 단숨함을 위해서는 라이브러리보다는 React 자체에 내장된 상태 관리 기능을 사용함이 좋다.</li>
<li>하지만, React의 자체 상태 관리 기능에는 다음과 같은 한계가 존재한다.<ul>
<li>컴포넌트의 상태는 공통된 상위요소까지 끌어올려야만 공유된다. 이 과정에서 공유되는 모든 컴포넌트가 리렌더링 된다.</li>
<li>전역 상태 관리를 지원하는 Context API는 여러 개의 독립적인 상태 값을 동시에 효율적으로 관리하기 어렵다.</li>
<li>이 두 가지 특성이 트리의 최상단부터 트리의 말단까지의 코드 분할을 어렵게 한다.</li>
</ul>
</li>
</ul>
<h2 id="context-api의-한계">Context API의 한계</h2>
<ul>
<li>Context는 하나의 &#39;value&#39; 속성만 가질 수 있다.</li>
<li>즉, 사용자 정보과 테마 설정을 동시에 Context를 통해 관리하고자 한다면 이 두 상태를 하나의 객체로 묶어서 전달해야 한다.</li>
</ul>
<pre><code class="language-jsx">const MyContext = React.createContext();

&lt;MyContext.Provider value={{ userInfo, themeSettings }}&gt;
  ...
&lt;/MyContext.Provider&gt;;</code></pre>
<ul>
<li>이는 userInfo만의 상태를 변경하더라도 Context의 변화를 감지하고 themeSettings를 가지는 컴포넌트로 리렌더링된다.</li>
<li>때문에 이 두 상태를 분리하고 각각의 상태에 대해 다른 컴포넌트가 구독하고자 한다면 각 상태에 대해 별도의 Context를 생성해야 한다.</li>
<li>이는 코드의 복잡성이 증가하고 관리가 어려워 진다.</li>
</ul>
<h2 id="atoms">Atoms</h2>
<ul>
<li>Atoms는 Recoil의 핵심 개념으로 컴포넌트의 상태를 따로 분리하여 관리한다.</li>
<li>Atoms라는 별도의 파일에서 상태를 관리하고 여러 컴포넌트에서 사용되는 경우 상태를 공유한다.</li>
<li>즉, atoms를 간단하게 이애한다면 전역에 존재하는 context와 유사하다.</li>
<li>다만, 컴포넌트는 특정 atom을 구독하는 매커니즘으로 해당 atom을 구독한 컴포넌트들만 리렌더링된다.</li>
</ul>
<blockquote>
<p>Recoil의 핵심 장점 중 하나는 효율적인 구독 메커니즘이 있어서 해당 atom을 직접 구독하는 컴포넌트만 해당 atom의 변화에 반응하여 리렌더링된다.</p>
</blockquote>
<h2 id="recoil로-다크모드-적용하기">Recoil로 다크모드 적용하기</h2>
<ul>
<li>Recoil의 atom을 사용하여 다크모드 기능을 구현해 보자.</li>
</ul>
<h3 id="1-recoil-설치">1. recoil 설치</h3>
<pre><code class="language-sh">npm install recoil</code></pre>
<h3 id="2-recoil-적용">2. recoil 적용</h3>
<ul>
<li>앱 전역에 recoil을 적용한다.</li>
</ul>
<pre><code class="language-tsx">// index
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import App from &#39;./App&#39;;
import { RecoilRoot } from &#39;recoil&#39;;

const root = ReactDOM.createRoot(
  document.getElementById(&#39;root&#39;) as HTMLElement,
);
root.render(
  &lt;React.StrictMode&gt;
    &lt;RecoilRoot&gt;
      &lt;App /&gt;
    &lt;/RecoilRoot&gt;
  &lt;/React.StrictMode&gt;,
);</code></pre>
<h3 id="3-atoms-생성">3. Atoms 생성</h3>
<pre><code class="language-tsx">import { atom } from &#39;recoil&#39;;

export const isDarkAtom = atom({
  key: &#39;isDark&#39;,
  default: false,
});</code></pre>
<ul>
<li><code>key</code> : Atoms는 디버깅, 지속성 및 모든 atoms의 map을 볼 수 있는 <code>고유한 키</code>가 필요하다.</li>
<li><code>default</code> : 컴포넌트의 상태처럼 기본값을 가진다.</li>
</ul>
<h3 id="4-atoms-상태값-사용">4. Atoms 상태값 사용</h3>
<pre><code class="language-tsx">// App
import { useRecoilValue } from &#39;recoil&#39;;
import { isDarkAtom } from &#39;./atoms&#39;;

function App() {
  const isDark = useRecoilValue(isDarkAtom);

  return (
    &lt;ThemeProvider theme={isDark ? theme.dark : theme.light}&gt;</code></pre>
<ul>
<li><code>useRecoilValue()</code> 훅을 통해서 어느 컴포넌트에서든 사용이 가능하다.</li>
<li>다크 모드의 경우 모든 App에 전달되어야 하므로 App에서 사용한다.</li>
</ul>
<h3 id="5-atoms-상태값-변경">5. Atoms 상태값 변경</h3>
<pre><code class="language-tsx">// coins
import { isDarkAtom } from &#39;@/atoms&#39;;
import { useSetRecoilState } from &#39;recoil&#39;;

const Coins = () =&gt; {
  const setDarkAtom = useSetRecoilState(isDarkAtom);
  const toggleDarkAtom = () =&gt; setDarkAtom((prev) =&gt; !prev);

return (
    &lt;S.Container&gt;
      &lt;S.Header&gt;
        &lt;S.Title&gt;Coins&lt;/S.Title&gt;
        &lt;S.DarkModeIcon onClick={toggleDarkAtom} icon=&quot;moon&quot; size=&quot;2x&quot; /&gt;</code></pre>
<ul>
<li><code>useSetRecoilState()</code> 훅을 통해 Atom의 상태를 변경하는 함수를 만들 수 있다.</li>
<li><code>const [state, setState] = useState()</code>의 setState와 유사하다.</li>
<li>다만, <strong>다른 컴포넌트에서도 사용할 수 있다는 차이가 있다.</strong></li>
</ul>
<h3 id="6-구현-결과">6. 구현 결과</h3>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/d8303efe-860f-4928-bb50-72e215fc85f7/image.gif" alt=""></p>
<h2 id="recoil의-훅">Recoil의 훅</h2>
<h3 id="1-userecoilstate">1. useRecoilState</h3>
<ul>
<li>useState와 유사하게 동작하는 훅. 주어진 atom의 상태와 해당 상태를 설정하는 함수를 반환한다.</li>
</ul>
<pre><code class="language-jsx">const [value, setValue] = useRecoilState(myAtom);</code></pre>
<h3 id="2-userecoilvalue">2. useRecoilValue</h3>
<ul>
<li>주어진 atom 또는 selector의 <code>현재 값</code>을 반환하는 훅. 상태만을 읽을 때 사용한다.</li>
</ul>
<pre><code class="language-jsx">const value = useRecoilValue(myAtom);</code></pre>
<h3 id="3-usesetrecoilstate">3. useSetRecoilState</h3>
<ul>
<li>주어진 atom의 상태를 설정하는 함수만을 반환하는 훅.</li>
<li>상태를 변경할 때만 필요하고, 현재 값을 알 필요가 없는 경우에 사용한다.</li>
</ul>
<pre><code class="language-jsx">const setValue = useSetRecoilState(myAtom);</code></pre>
<h3 id="4-useresetrecoilstate">4. useResetRecoilState</h3>
<ul>
<li>주어진 atom의 상태를 초기 상태로 재설정하는 함수를 반환하는 훅.</li>
</ul>
<pre><code class="language-jsx">const resetValue = useResetRecoilState(myAtom);</code></pre>
<h3 id="5-userecoilcallback">5. useRecoilCallback</h3>
<ul>
<li>Recoil 상태에 액세스하거나 변경하는 콜백을 생성하는 훅.</li>
<li>이 콜백은 비동기 작업에 유용하며, Recoil 상태를 바깥 스코프에서 사용할 수 있게 해준다.</li>
</ul>
<pre><code class="language-jsx">const someCallback = useRecoilCallback(({ snapshot, set }) =&gt; async () =&gt; {
  const currentValue = snapshot.getLoadable(myAtom).contents;
  // Do something with currentValue or set new value
  set(myAtom, newValue);
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - React Hook Form]]></title>
            <link>https://velog.io/@sarang_daddy/React-React-Hook-Form</link>
            <guid>https://velog.io/@sarang_daddy/React-React-Hook-Form</guid>
            <pubDate>Wed, 27 Sep 2023 06:26:51 GMT</pubDate>
            <description><![CDATA[<h1 id="react-hook-form">React Hook Form</h1>
<ul>
<li>React Hook Form (RHF)은 React 기반 애플리케이션에서 폼을 쉽게 관리할 수 있도록 도와주는 라이브러리다.</li>
<li>일반적인 방법으로 Form을 만든다면 매우 많은 state와 검증단계가 필요하다.</li>
<li>RHF을 사용한다면 다양한 Input의 상태와 검증을 간단하게 관리할 수 있다.</li>
</ul>
<h2 id="일반적인-방법의-코드">일반적인 방법의 코드</h2>
<ul>
<li>하나의 input에도 많은 양의 코드가 필요하다.</li>
</ul>
<pre><code class="language-tsx">import { useState } from &#39;react&#39;;

const ToDoList = () =&gt; {
  const [toDo, setToDo] = useState(&#39;&#39;);
  const [toDoError, setToDoError] = useState(&#39;&#39;);

  const onChange = (event: React.FormEvent&lt;HTMLInputElement&gt;) =&gt; {
    const {
      currentTarget: { value },
    } = event;
    setToDoError(&#39;&#39;);
    setToDo(value);
  };

  const onSubmit = (event: React.FormEvent&lt;HTMLFormElement&gt;) =&gt; {
    event.preventDefault();
    console.log(toDo);
    if (toDo.length &lt; 10) {
      return setToDoError(&#39;To do should be longer&#39;);
    }
    console.log(&#39;submit&#39;);
  };

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;form onSubmit={onSubmit}&gt;
          &lt;input onChange={onChange} value={toDo} placeholder=&quot;Write a to do&quot; /&gt;
          &lt;button&gt;Add&lt;/button&gt;
          {toDoError !== &#39;&#39; ? toDoError : null}
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default ToDoList;</code></pre>
<h1 id="react-hook-form-1">React Hook Form</h1>
<ul>
<li><a href="https://react-hook-form.com/">React Hook Form</a></li>
</ul>
<h1 id="설치">설치</h1>
<pre><code class="language-bash">npm install react-hook-form</code></pre>
<h1 id="사용방법">사용방법</h1>
<h2 id="1-useform">1. useForm</h2>
<ul>
<li>RHF의 핵심 훅으로 폼과 관련된 여러 메서드와 속성을 가져올 수 있다.</li>
</ul>
<pre><code class="language-tsx">import { useForm } from &#39;react-hook-form&#39;;

const ToDoList = () =&gt; {
  const {} = useForm();
  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;form&gt;
          &lt;input placeholder=&quot;Write a to do&quot; /&gt;
          &lt;button&gt;Add&lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default ToDoList;</code></pre>
<ul>
<li><code>const {} = useForm()</code> 에서 다양한 메서드(도구)를 사용할 수 있다.</li>
<li><code>register</code>: 입력 요소를 폼에 등록하는 함수</li>
<li><code>handleSubmit</code>: 폼 제출을 처리하는 함수</li>
<li><code>formState</code>: 폼의 상태에 관한 정보를 담고 있다. (예: <code>errors</code>)</li>
<li><code>setError</code>: 수동으로 폼의 오류를 설정하는 함수</li>
<li><code>setValue</code> : 폼 제출 후 입력값을 설정하는 함수</li>
</ul>
<h2 id="2-register">2. register</h2>
<ul>
<li>입력 요소를 RHF에 등록하는데 사용된다.</li>
</ul>
<pre><code class="language-tsx">import { useForm } from &#39;react-hook-form&#39;;

const ToDoList = () =&gt; {
  const { register } = useForm();

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;form&gt;
          &lt;input {...register(&#39;toDo&#39;)} placeholder=&quot;Write a to do&quot; /&gt;
          &lt;button&gt;Add&lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default ToDoList;</code></pre>
<ul>
<li>register에는 input에서 사용하던 다양한 속성이 들어있다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/d703785f-42bb-442d-91e0-0e13283b17cd/image.png" width="50%">

<ul>
<li><p><code>input {...register(&#39;toDO&#39;)}</code>한번으로 전부 적용할 수 있다.</p>
</li>
<li><p>필수입력, 최소 길이 등의 조건 추가가 가능하다.</p>
<pre><code class="language-tsx">  &lt;input
    {...register(&#39;email&#39;, {
      required: &#39;Email is required&#39;,
      minLength: 10,
      pattern: {
        value: /^[A-Za-z0-9._%+-]+@naver.com$/,
        message: &#39;Only naver.com emails allowed&#39;,
      },
    })}
    placeholder=&quot;Email&quot;
  /&gt;</code></pre>
</li>
</ul>
<h2 id="3-watch">3. watch</h2>
<ul>
<li>입력 요소의 값을 확인할 때 사용된다.</li>
</ul>
<pre><code class="language-tsx">const ToDoList = () =&gt; {
  const { register, watch } = useForm();

console.log(watch())</code></pre>
<ul>
<li>form 입력값들의 변화를 관찰 할 수 있게 해주는 함수</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/c0ed8d2b-de67-452e-afc5-cd11e7f11d56/image.png" width="20%">

<h2 id="4-handlesubmit">4. handleSubmit</h2>
<ul>
<li>폼 제출을 처리하는 데 사용된다.</li>
<li>인자로 콜백 함수를 받아서 폼이 <code>유효하다면</code> 해당 콜백 함수를 실행한다.</li>
<li>기존의 onSubmit, preventDefault 를 담당한다고 생각하면 된다.</li>
</ul>
<pre><code class="language-tsx">import { useForm } from &#39;react-hook-form&#39;;

const ToDoList = () =&gt; {
  const { register, handleSubmit } = useForm();
  // 모든 검증이 끝나면 일어난다.
  const onVaild = (data: any) =&gt; {
    console.log(data);
  };

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;form onSubmit={handleSubmit(onVaild)}&gt;
          &lt;input {...register(&#39;email&#39;)} placeholder=&quot;email&quot; /&gt;
          &lt;input {...register(&#39;firstName&#39;)} placeholder=&quot;firstName&quot; /&gt;
          &lt;input {...register(&#39;lastName&#39;)} placeholder=&quot;lastName&quot; /&gt;
          &lt;input {...register(&#39;userName&#39;)} placeholder=&quot;userName&quot; /&gt;
          &lt;input {...register(&#39;password&#39;)} placeholder=&quot;password&quot; /&gt;
          &lt;button&gt;Add&lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default ToDoList;</code></pre>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/4919c4a1-2b85-4378-a936-aa4d0cb82b88/image.png" width="50%">


<h2 id="5-formstate">5. formState</h2>
<ul>
<li>폼의 현재 상태와 관련된 정보를 제공한다.</li>
<li><code>isDirty</code>: 사용자가 폼의 어떤 입력 필드라도 변경한 경우 <code>true</code>를 반환</li>
<li><code>isSubmitting</code>: 폼이 현재 제출 중인 경우 <code>true</code>를 반환</li>
<li><code>isValid</code>: 폼이 유효한 경우 <code>true</code>를 반환</li>
<li><code>submitCount</code>: 폼이 제출된 횟수</li>
<li><code>touchedFields</code>: 사용자가 접근하거나 수정한 입력 필드의 목록</li>
<li><code>errors</code>: 폼의 유효성 검사 오류에 대한 정보.</li>
</ul>
<pre><code class="language-tsx">import { useForm } from &#39;react-hook-form&#39;;

const ToDoList = () =&gt; {
  const { register, handleSubmit, formState } = useForm();
  // 모든 검증이 끝나면 일어난다.
  const onValid = (data: any) =&gt; {
    // console.log(data);
  };

  // formState의 errors 속성 확인
  console.log(formState.errors);

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;form
          style={{ display: &#39;flex&#39;, flexDirection: &#39;column&#39; }}
          onSubmit={handleSubmit(onValid)}
        &gt;
          &lt;input
            {...register(&#39;email&#39;, { required: true })}
            placeholder=&quot;email&quot;
          /&gt;
          &lt;input
            {...register(&#39;firstName&#39;, { required: true, minLength: 10 })}
            placeholder=&quot;firstName&quot;
          /&gt;
          &lt;input
            {...register(&#39;lastName&#39;, { required: true })}
            placeholder=&quot;lastName&quot;
          /&gt;
          &lt;input
            {...register(&#39;userName&#39;, { required: true })}
            placeholder=&quot;userName&quot;
          /&gt;
          &lt;input
            {...register(&#39;password&#39;, {
              required: &#39;Password is required&#39;,
              minLength: {
                value: 5,
                message: &#39;Your password too short&#39;,
              },
            })}
            placeholder=&quot;password&quot;
          /&gt;
          &lt;button&gt;Add&lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default ToDoList;</code></pre>
<ul>
<li>formState의 errors 속성을 이용하면 현재 어떤 input에서 에러가 발생했는지 알 수 있다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/fa6e8bba-d6da-47d0-8fff-3473dd2e1bc6/image.png" width="50%">

<h2 id="6-errors">6. errors</h2>
<ul>
<li>formState의 errors를 활용해서 사용자에게 잘못된 입력값을 알려 줄 수 있다.</li>
<li>register에서 등록한 조건을 충족하지 못한다면 formState의 errors 속성에 등록된다.</li>
</ul>
<h3 id="errors의-속성">errors의 속성</h3>
<ul>
<li><code>type</code> : 오류의 유형. (예: &quot;required&quot;, &quot;minLength&quot;, &quot;pattern&quot; 등)</li>
<li><code>message</code> : 오류에 대한 메시지. <code>register</code> 함수에서 설정한 오류 메시지나 <code>setError</code> 함수를 통해 수동으로 설정한 오류 메시지를 포함할 수 있다.</li>
<li>특정 register에 errors가 있다면 message를 통해 사용자에게 보여 줄 수 있다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/7326099f-624b-45db-a15b-65623f0edcaf/image.png" width="50%">

<h2 id="7-seterror">7. setError</h2>
<ul>
<li>수동으로 입력 필드에 오류를 설정하는 데 사용된다.</li>
<li><code>API 응답</code> 또는 <code>사용자 정의 로직</code>을 기반으로 오류를 설정할 때 유용하다.</li>
</ul>
<pre><code class="language-tsx">setError(name, error, options);</code></pre>
<ol>
<li>name(필수) : 오류를 설정할 입력 필드의 이름</li>
<li>error(필수) : 설정할 오류의 정보<ul>
<li><code>type</code>: 오류의 유형 (예: &quot;manual&quot;, &quot;required&quot; 등)</li>
<li><code>message</code>: 오류 메시지</li>
</ul>
</li>
<li>options(선택) : 추가적인 옵션을 설정할 수 있다.<ul>
<li><code>shouldFocus</code>: 오류가 설정된 입력 필드로 포커스를 이동할지 결정하는 불리언 값.</li>
</ul>
</li>
</ol>
<ul>
<li>onValid에 setError를 추가하여 폼 제출전에 검증 단계를 추가할 수 있다.</li>
</ul>
<pre><code class="language-tsx">const onValid = (data: IForm) =&gt; {
    if (data.password !== data.rePassword) {
      setError(
        &#39;rePassword&#39;,
        { message: &#39;Password are not the same&#39; },
        { shouldFocus: true },
      );
    }
  };</code></pre>
<h2 id="8-custom-validation">8. Custom Validation</h2>
<ul>
<li>사용자 정의 유효성 검사로 <code>register</code> 함수의 <code>validate</code> 옵션을 사용해서 정의할 수 있다.</li>
<li>예) 특정 아이디는 사용이 불가하다는 사용자 정의 유효성</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/a5f3c761-ce6c-4dda-9174-4a4db1d9a7c7/image.png" width="50%">

<h2 id="9-pattern-vs-validate">9. Pattern VS Validate</h2>
<h3 id="pattern">pattern</h3>
<ul>
<li><code>pattern</code>은 정규 표현식을 기반으로 입력 값의 형식을 검사한다.</li>
<li>주로 특정 형식(예: 이메일 주소, 전화번호)을 가진 값이 입력되었는지 확인할 때 사용된다.</li>
<li><code>pattern</code> 속성의 값은 정규 표현식과 관련된 오류 메시지를 포함하는 객체가 될 수 있다.</li>
</ul>
<p>예시:</p>
<pre><code class="language-jsx">register(&#39;email&#39;, {
  pattern: {
    value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
    message: &#39;Invalid email format&#39;
  }
})</code></pre>
<h3 id="validate">validate</h3>
<ul>
<li><code>validate</code>는 사용자 정의 검증 함수를 제공하여 좀 더 복잡한 유효성 검사를 수행할 수 있다.</li>
<li>함수는 입력 값이 유효한 경우 <code>true</code> 또는 유효하지 않은 경우 오류 메시지를 반환해야 한다.</li>
<li>여러 개의 사용자 정의 검증 함수를 객체 형태로 제공하여 여러 규칙을 동시에 적용할 수 있다.</li>
</ul>
<p>예시:</p>
<pre><code class="language-jsx">register(&#39;username&#39;, {
  validate: {
    isUnique: value =&gt; isUsernameUnique(value) || &#39;Username is already taken&#39;,
    notEmpty: value =&gt; value.trim().length &gt; 0 || &#39;Username cannot be empty&#39;
  }
})
</code></pre>
<h3 id="차이점">차이점</h3>
<ul>
<li><code>pattern</code>은 정규 표현식을 기반으로 입력 값의 형식을 간단하게 검사하는 데 사용되는 반면, <code>validate</code>는 복잡한 로직이나 여러 검증 규칙을 적용하는 데 사용된다.</li>
<li><code>pattern</code>은 주로 특정 형식의 값이 입력되었는지 확인하는 데 사용되고, <code>validate</code>는 다양한 조건을 기반으로 입력 값을 검증하는 데 사용된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - Data Caching]]></title>
            <link>https://velog.io/@sarang_daddy/React-Data-Caching</link>
            <guid>https://velog.io/@sarang_daddy/React-Data-Caching</guid>
            <pubDate>Sun, 24 Sep 2023 20:00:47 GMT</pubDate>
            <description><![CDATA[<h1 id="data-caching">Data Caching?</h1>
<p><code>Data Caching</code>이란 반복적으로 엑세스되는 데이터를 빠르게 사용할 수 있도록 메모리에 <code>일시적</code>으로 저장하는 프로세스를 의미한다. </p>
<p>데이터 캐싱으로 얻을 수 있는 이점이 많기에 성능 향상과 효율적인 리소스 사용을 위해 필수적으로 사용되고 있다.</p>
<h3 id="데이터-캐싱의-이점">데이터 캐싱의 이점</h3>
<ul>
<li><p>속도 향상
: 캐시 메모리는 RAM과 같은 저장소에 위치하므로 외부 서버에 접근하는 것보다 빠르게 엑세스할 수 있다.</p>
</li>
<li><p>부하 감소
: 반복적으로 요청되는 데이터를 캐시에 저장함으로써 서버로의 데이터 요청 수를 줄일 수 있다.</p>
</li>
<li><p>사용자 접근성 향상
: 캐시된 데이터로 빠르게 화면에 사용자가 원하는 데이터를 보여줄 수 있다.</p>
</li>
</ul>
<p>데이터 캐싱이 없다면 아래 화면처럼 페이지가 렌더링 될 때마다 데이터를 서버로 부터 가져 오기에 loading 메시지가 반복된다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/84e71191-cc92-47f3-a369-8d66921ff959/image.gif" alt=""></p>
<p><code>React Query</code>라는 라이브러리에서는 데이터 캐싱을 자동으로 지원해주지만, 
이번에는 <code>Context API</code>를 사용하여 데이터 캐싱 로직을 직접 구현해보자.</p>
<h1 id="data-caching-구현">Data Caching 구현</h1>
<p>데이터 캐싱 구현에는 몇 가지 주의 사항이 있다.</p>
<ul>
<li><p>캐시 일관성 
: 캐시된 데이터는 원본 데이터와 동기화 되어야 한다.
즉, 캐시된 데이터가 오래되거나 변경되면, 업데이트 혹은 무효화해야 한다.</p>
</li>
<li><p>메모리 사용
: 캐싱은 추가적인 메모리를 사용하므로, 캐시 크기와 관리 전략에 대한 고려가 필요하다.</p>
</li>
<li><p>캐시 전략
: 어떤 데이터를 캐시에 저장할지, 얼마나 오래 저장할지, 언제 캐시를 무효화 할지 등의 정의가 필요하다.</p>
</li>
</ul>
<p>이번 구현에는 만료 시간을 할당하여 캐시의 유효성을 판단하도록 했다.</p>
<h2 id="1-캐싱-메니저-함수-생성">1. 캐싱 메니저 함수 생성</h2>
<ul>
<li>캐싱 로직을 관리하는 헬퍼 함수를 생성한다.</li>
<li>이 함수는 캐싱할 데이터와 해당 데이터의 만료 시간을 관리한다.</li>
<li>즉, 캐시된 데이터의 유효성을 검사하고 데이터를 반환한다.</li>
</ul>
<pre><code class="language-tsx">const ONE_MINUTE_MS = 60 * 1000;

const cacheManager = (cacheExpirationDuration: number = ONE_MINUTE_MS * 10) =&gt; {
  const cache: Record&lt;string, { data: any; expireTime: number }&gt; = {};

  return {
    cacheData: (key: string, data?: any) =&gt; {
      if (cache[key]) {
        const { data: cachedData, expireTime } = cache[key];
        if (expireTime &gt; Date.now()) {
          return cachedData;
        }
      }
      cache[key] = { data, expireTime: Date.now() + cacheExpirationDuration };
      return data;
    },
    isDataValid: (key: string) =&gt; {
      if (!cache[key]) return false;
      const { expireTime } = cache[key];
      return expireTime &gt; Date.now();
    },
  };
};

export default cacheManager;
</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/34fa2aae-b8c1-4e57-a173-3adce5895b90/image.png" alt=""></p>
<ul>
<li>ONE_MINUTE_MS : 1분을 밀리초로 표현한 상수.</li>
<li>cacheManager : 캐싱 로직을 관리하는 함수. 캐시 만료 시간은 10분으로 지정.</li>
<li><code>cacheData</code>와 <code>isDataValid</code> 두 메서드를 포함하는 객체를 반환한다.</li>
<li><strong>외부에서 cacheManager를 통해 캐시된 데이터를 가져오거나 새로운 데이터를 저장하고, 유효성 검사가 가능하다.</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/dfa146c1-d6e7-43e4-b37e-ac19fdb62c93/image.png" alt=""></p>
<ul>
<li>캐시를 저장할 객체. </li>
<li>각 키는 문자열이며, 값은 { data: any; expireTime: number } 형태를 가진다.</li>
<li>여기서 data는 캐싱할 데이터를 나타내고, expireTime은 해당 데이터의 만료 시간을 나타낸다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/a7ef0577-f064-451a-9cb1-a8b452201afb/image.png" alt=""></p>
<ul>
<li>주어진 키에 해당하는 데이터를 캐시에서 가져오거나 새로운 데이터를 캐시에 저장하는 함수.</li>
<li>주어진 키가 존재하고 만료 시간이 현재 시간보다 미래라면 캐시된 데이터를 반환한다.</li>
<li>주어진 키가 없거나 만료된 경우, 새로운 데이터와 만료 시간을 저장한다.</li>
<li>캐시된 데이터 혹은 새로 캐시된 데이터를 반환한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/e8d31cd9-5a86-4a94-8b2d-5b819aa9e146/image.png" alt=""></p>
<ul>
<li>주어진 키의 캐시 데이터의 유효성을 검사하는 함수.</li>
<li>주어진 키에 해당하는 데이터가 캐시에 있고 그 데이터의 만료 시간이 아직 지나지 않았다면 true를 반환한다.</li>
<li>그렇지 않은 경우 false를 반환한다.</li>
</ul>
<h2 id="2-cachecontext-생성">2. CacheContext 생성</h2>
<ul>
<li>앱 전체에서 데이터 캐싱을 관리하기 위해 Context API로 CacheContext를 생성한다.</li>
</ul>
<pre><code class="language-tsx">import { createContext, useContext } from &#39;react&#39;;
import cacheManager from &#39;@/helpers/cacheManager&#39;;

interface ICacheContext {
  cacheData: (key: string, data?: any) =&gt; any;
  isDataValid: (key: string) =&gt; boolean;
}

export const CacheContext = createContext&lt;ICacheContext&gt;({} as ICacheContext);

interface CacheContextProviderProps {
  children: React.ReactNode;
}

export const CacheContextProvider = ({
  children,
}: CacheContextProviderProps) =&gt; {
  const { cacheData, isDataValid } = cacheManager();

  return (
    &lt;CacheContext.Provider value={{ cacheData, isDataValid }}&gt;
      {children}
    &lt;/CacheContext.Provider&gt;
  );
};

export const useCacheContext: () =&gt; ICacheContext = () =&gt;
  useContext(CacheContext);</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/0fff9d06-0c91-42f1-a1a8-d1ae541bcc5c/image.png" alt=""></p>
<ul>
<li>cacheManager에서 반환되는 두 메서드를 사용할 수 있는 context를 생성한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/fc974327-7bc5-45b5-98a8-b35dae5b2d97/image.png" alt=""></p>
<ul>
<li>contextProvider를 사용하여 cacheData, isDataValid를 제공한다.</li>
<li>contextProvider의 하위 컴포넌트들은 cacheData, isDataValid 함수에 접근할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/d760d835-b854-4fef-a03a-c11aee6ff617/image.png" alt=""></p>
<ul>
<li>함위 컴포넌트에서 context를 사용할 useContext를 커스텀 훅으로 만들어준다.</li>
<li>각 컴포넌트에서 useContext를 import하고 호출할 필요가 없어진다.</li>
<li>useCacheContext는 CacheContext의 값을 가져와서 반환하는 함수(훅)이다.</li>
<li>이 함수의 반환 값의 타입은 ICacheContext 타입이다.</li>
</ul>
<h2 id="3-usefetch-훅-작성">3. useFetch 훅 작성</h2>
<ul>
<li>데이터 패치와 캐싱 로직을 관리하는 useFetch 훅을 작성한다.</li>
</ul>
<pre><code class="language-tsx">import { useState, useEffect } from &#39;react&#39;;
import { useCacheContext } from &#39;@/contexts/CacheContext&#39;;

type Status = &#39;initial&#39; | &#39;pending&#39; | &#39;fulfilled&#39; | &#39;rejected&#39;;

interface UseFetch&lt;T&gt; {
  data?: T;
  status: Status;
  error?: Error;
  cacheKey: string;
}

interface FetchOptions&lt;T&gt; {
  fetchFunction: (...args: any[]) =&gt; Promise&lt;T&gt;;
  args: any[];
  cacheKey: string;
}

export const useFetch = &lt;T&gt;({
  fetchFunction,
  args,
  cacheKey,
}: FetchOptions&lt;T&gt;): UseFetch&lt;T&gt; =&gt; {
  const [state, setState] = useState&lt;UseFetch&lt;T&gt;&gt;({
    status: &#39;initial&#39;,
    data: undefined,
    error: undefined,
    cacheKey,
  });

  const { cacheData, isDataValid } = useCacheContext();
  useEffect(() =&gt; {
    let ignore = false;

    const fetchData = async () =&gt; {
      if (ignore) return;

      setState((state) =&gt; ({ ...state, status: &#39;pending&#39; }));

      try {
        const response = await fetchFunction(...args);

        cacheData(cacheKey, response);
        setState((state) =&gt; ({
          ...state,
          status: &#39;fulfilled&#39;,
          data: response,
          cacheKey,
        }));
      } catch (error) {
        setState((state) =&gt; ({
          ...state,
          status: &#39;rejected&#39;,
          error: error as Error,
          cacheKey,
        }));
      }
    };

    if (state.status === &#39;initial&#39;) {
      if (isDataValid(cacheKey)) {
        setState((state) =&gt; ({
          ...state,
          status: &#39;fulfilled&#39;,
          data: cacheData(cacheKey),
          cacheKey,
        }));
      } else {
        fetchData();
      }
    }

    return () =&gt; {
      ignore = true;
    };
  }, [fetchFunction, cacheKey, cacheData, isDataValid, state.status]);

  return state;
};</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/8e2b608e-6d90-4030-8a08-82e93c697346/image.png" alt=""></p>
<ul>
<li>Status는 데이터 가져오기 상태를 나타내는 문자열 유니언 타입이다.</li>
<li>UseFetch는 useFetch 훅이 반환하는 객체의 타입이다.</li>
<li>FetchOptions는 useFetch 훅에 전달되는 인자의 타입이다.</li>
</ul>
<pre><code class="language-tsx">// 기존 Home 컴포넌트
const { data: characters, status } = useFetch(fetchCharacters, 50);

// 수정된 Home 컴포넌트
export const Home = () =&gt; {
  const { data: characters, status } = useFetch({
    fetchFunction: fetchCharacters,
    args: [50],
    cacheKey: ROUTE_PATH.HOME,
  });</code></pre>
<ul>
<li>useFetch 커스텀훅을 사용하는 컴포넌트에서 useFetch 인자를 객체로 전달하도록 수정.</li>
<li><code>...rest</code>는 마지막 인자로 전달되어야 하기에 순서를 고려하지 않고 전달하기 위해 수정했다.</li>
</ul>
<blockquote>
<p>함수에 전달되는 모든 인자들을 객체의 속성으로 묶어서 전달하면, 순서에 구애받지 않고 인자들을 함수에 전달할 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/2b6c0589-1dbe-497b-a3cd-9ee734d3b91b/image.png" alt=""></p>
<ul>
<li>useFetch 함수에 전달되는 인자 정의.</li>
<li>cacheKey가 추가되었다.</li>
</ul>
<pre><code class="language-tsx">// 수정 전 useFetch 인자 정의
export const useFetch = &lt;T&gt;(
  fetchFunction: (...args: any[]) =&gt; Promise&lt;T&gt;,
  ...args: any[]
): UseFetch&lt;T&gt; =&gt; {</code></pre>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/59083906-3557-48a9-83e2-4858918694b3/image.png" alt=""></p>
<ul>
<li>state와 setState는 useFetch 훅의 내부 상태를 관리한다.</li>
<li>cacheData와 isDataValid 함수는 캐시 컨텍스트에서 가져온다. </li>
<li>이 함수들은 데이터를 캐시에 저장하고, 캐시된 데이터의 유효성을 확인하는데 사용된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/cfbfc56b-9afa-4c95-9397-af62661ea97d/image.png" alt=""></p>
<ul>
<li>useEffect를 사용하여 API에서 데이터를 가져온다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/33f29d87-905c-4636-af7a-26b88f2e1b35/image.png" alt=""></p>
<ul>
<li>초기 상태(initial) 확인하여 캐시에서 유요한 데이터가 있는지 확인.</li>
<li>유효한 데이터가 있으면 해당 데이터로 상태를 업데이트 한다.</li>
<li>유요한 데이터가 없다면 fetchData 함수를 호출하여 API 데이터를 가져온다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/8901c537-d22f-45db-9e7e-d5caff23a6bb/image.png" alt=""></p>
<ul>
<li>클린업 함수로 언마운트 됬을 경우 네트워크 요청으로 인한 상태 변경을 방지한다.</li>
<li>useEffect의 의존성 배열을 정의한다.</li>
<li>useFetch 훅은 현재 상태를 반환한다. 이 상태는 데이터 가져오기의 결과 및 상태 정보를 포함한다.</li>
</ul>
<h2 id="4-앱-구조-작성-app">4. 앱 구조 작성 (App)</h2>
<ul>
<li>앱의 최상위 컴포넌트에서 CacheContextProvider를 사용하여 캐싱 관련 함수를 앱 전체에 제공한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/2736e0c5-e4d6-4490-bb14-3a2fb3d2f58e/image.png" alt=""></p>
<h1 id="data-caching-결과">Data Caching 결과</h1>
<ul>
<li>API로부터 데이터를 받는 동안 화면에 보이면 &quot;Loading...&quot;이 동일 페이지 이동시 보이지 않는다.</li>
<li>한번 가져온 데이터를 메모리에 캐싱하여 동일한 데이터가 필요한 경우 API 요청이 생략되었다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/fdb490e7-5165-4088-92fd-a4c32b867c7b/image.gif" alt=""></p>
<blockquote>
<p>데이터 캐싱 매니저 함수와 Context API를 사용한 로직을 useFetch 훅에 적용하여 API에서 데이터를 가져오는 기능과 함께 데이터 캐싱 기능이 가능하도록 구현되었다. </p>
</blockquote>
<p>React Query를 사용하면 기본으로 제공되는 기능이지만, 기본 원리를 이해하는 것은 라이브러리를 더욱 효율적으로 사용하고 문제가 발생 했을때 디버깅에도 용이하다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - React Query(TanStack Query)]]></title>
            <link>https://velog.io/@sarang_daddy/React-React-QueryTanStack-Query</link>
            <guid>https://velog.io/@sarang_daddy/React-React-QueryTanStack-Query</guid>
            <pubDate>Sat, 23 Sep 2023 07:55:14 GMT</pubDate>
            <description><![CDATA[<h1 id="react-query">React Query</h1>
<ul>
<li>React Query는 웹 애플리케이션에서 서버 <code>데이터를 효율적으로 가져오고</code>, <code>캐싱</code>하며 동기화하는 데 도움을 주는 라이브러리다.</li>
<li>이 라이브러리의 도움으로 개발자는 서버와의 <code>데이터 동기화</code>, <code>데이터 리프레시</code>, <code>오류 처리</code> 등의 복잡한 작업을 훨씬 간결하게 처리할 수 있다.</li>
</ul>
<h2 id="react-query의-장점">React Query의 장점</h2>
<ul>
<li>자동 데이터 리프레시 : 사용자에게 항상 최신의 데이터를 제공한다.</li>
<li>백그라운드 데이터 동기화 : 애플리케이션이 백그라운드에서도 데이터를 동기화한다.</li>
<li>오류 처리 : API 호출에서 오류가 발생하면 error 및 isError 상태를 통해 쉽게 처리할 수 있다.</li>
<li>캐싱 : 데이터를 자동으로 캐시하여 반복적인 요청을 피하고 성능을 향상시킨다.</li>
</ul>
<h2 id="기존-fetch-방식">기존 fetch 방식</h2>
<ul>
<li>전통적인 데이터 가져오기 방법은 useState와 useEffect를 사용하여 데이터 상태를 관리하고 API 호출을 수행한다.</li>
</ul>
<pre><code class="language-tsx">// 기존 코드 예시
const Coins = () =&gt; {
  const [coins, setCoins] = useState&lt;ICoin[]&gt;([]);
  const [loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    let isIgnore = false;

    (async () =&gt; {
      const response = await fetch(&#39;https://api.coinpaprika.com/v1/coins&#39;);
      const json = await response.json();

      if (!isIgnore) {
        setCoins(json.slice(0, 100));
        setLoading(false);
      }
    })();

    return () =&gt; {
      isIgnore = true;
    };
  }, []);

  // 렌더링 로직
};</code></pre>
<h2 id="react-query-사용">React Query 사용</h2>
<h3 id="1-설치">1. 설치</h3>
<pre><code class="language-bash">npm i @tanstack/react-query</code></pre>
<h3 id="2-queryclient-queryclientprovider-생성">2. QueryClient, QueryClientProvider 생성</h3>
<ul>
<li>React Query를 사용하려면 먼저 QueryClient를 생성하고 이를 QueryClientProvider에 전달해야 한다.</li>
</ul>
<pre><code class="language-tsx">import { QueryClient, QueryClientProvider } from &#39;@tanstack/react-query&#39;;

const queryClient = new QueryClient();

function App() {
  return (
    &lt;ThemeProvider theme={theme.light}&gt;
      &lt;QueryClientProvider client={queryClient}&gt;
        &lt;GlobalStyle /&gt;
        &lt;RouterProvider router={router} /&gt;
      &lt;/QueryClientProvider&gt;
    &lt;/ThemeProvider&gt;
  );
}

export default App;</code></pre>
<h3 id="3-usequery-훅으로-fetcher-함수-전달">3. useQuery 훅으로 fetcher 함수 전달</h3>
<ul>
<li>데이터를 가져오기 위해 useQuery 훅을 사용하고, 데이터 가져오기 함수를 제공한다.</li>
</ul>
<pre><code class="language-tsx">const Coins = () =&gt; {
  const { isLoading, data } = useQuery([&#39;allCoins&#39;], fetchCoins);

    /* 아래 코드는 모두 삭제된다. */
  // const [coins, setCoins] = useState&lt;CoinInterface[]&gt;([]);
  // const [loading, setLoading] = useState(true);

  // useEffect(() =&gt; {
  //   let isIgnore = false;

  //   (async () =&gt; {
  //     const response = await fetch(&#39;https://api.coinpaprika.com/v1/coins&#39;);
  //     const json = await response.json();

  //     if (isIgnore) {
  //       setCoins(json.slice(0, 100));
  //       setLoading(false);
  //     }
  //   })();

  //   return () =&gt; {
  //     isIgnore = true;
  //   };
  // }, []);</code></pre>
<ul>
<li>API 요청 함수는 따로 파일을 분리해서 관리한다.</li>
</ul>
<pre><code class="language-tsx">// api fetcher 파일
export const fetchCoins = () =&gt; {
  return fetch(&#39;https://api.coinpaprika.com/v1/coins&#39;).then((response) =&gt;
    response.json(),
  );
};</code></pre>
<h3 id="4-usequery-훅-설명">4. useQuery 훅 설명</h3>
<pre><code class="language-tsx">const { isLoading, data } = useQuery([&#39;allCoins&#39;], fetchCoins);</code></pre>
<ul>
<li>isLoading : 기존에 state로 따로 관리해주던 로딩 불리언 값을 react query에서 반환해준다.</li>
<li>data : fetch후 받아온 데이터를 반환해준다.</li>
<li>[’allCoins’] : Query Key - query를 고유하게 식별해주는 key로 필수 항목이다.</li>
<li>React Query는 쿼리 키를 기반으로 쿼리 캐싱을 관리한다.</li>
</ul>
<h3 id="5-렌더링-로직에-적용">5. 렌더링 로직에 적용</h3>
<pre><code class="language-tsx">import { Link } from &#39;react-router-dom&#39;;
import { useQuery } from &#39;@tanstack/react-query&#39;;
import { fetchCoins } from &#39;@/api/fetcher&#39;;
import * as S from &#39;./styles&#39;;

interface ICoin {
  id: string;
  name: string;
  symbol: string;
  rank: number;
  is_new: boolean;
  is_active: boolean;
  type: string;
}

const Coins = () =&gt; {
  const { isLoading, data } = useQuery&lt;ICoin[]&gt;([&#39;allCoins&#39;], fetchCoins);

  return (
    &lt;S.Container&gt;
      &lt;S.Header&gt;
        &lt;S.Title&gt;Coins&lt;/S.Title&gt;
      &lt;/S.Header&gt;
      {isLoading ? (
        &lt;S.Loader&gt;Loading...&lt;/S.Loader&gt;
      ) : (
        &lt;S.CoinsList&gt;
          {data?.slice(0, 100).map((coin) =&gt; (
            &lt;S.Coin key={coin.id}&gt;
              &lt;Link to={`/${coin.id}`} state={coin}&gt;
                &lt;S.Img
                  src={`https://coinicons-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
                /&gt;
                &lt;span&gt;{coin.name}&lt;/span&gt;
                &lt;span&gt;&amp;rarr;&lt;/span&gt;
              &lt;/Link&gt;
            &lt;/S.Coin&gt;
          ))}
        &lt;/S.CoinsList&gt;
      )}
    &lt;/S.Container&gt;
  );
};

export default Coins;</code></pre>
<blockquote>
<p>useState와 useEffect로 상태를 관리하고 데이터를 호출하던 로직을 react query를 통해 한번에 해결할 수 있다.<br>💡 React Query는 <code>데이터를 자동으로 캐싱</code>하므로 동일한 요청을 여러 번 수행할 필요가 없다.</p>
</blockquote>
<h2 id="여러-usequery-사용하기">여러 useQuery 사용하기</h2>
<ul>
<li>때로는 하나의 컴포넌트에서 여러 데이터 소스를 동시에 요청해야 할 수 있다.</li>
<li>React Query의 useQuery는 이러한 상황을 간단히 처리할 수 있게 해준다.</li>
</ul>
<h3 id="query-key-구성">Query key 구성</h3>
<ul>
<li>각 쿼리는 고유한 키를 가져야 한다.</li>
<li>이 키를 사용하여 내부적으로 캐싱된 데이터를 관리한다.</li>
<li>복잡한 쿼리의 경우, 쿼리 키를 배열로 구성하여 고유성을 보장할 수 있다.</li>
<li>예를 들어, 특정 코인의 정보와 티커를 동시에 가져오는 경우 다음과 같이 작성할 수 있다.</li>
</ul>
<pre><code class="language-tsx">const { isLoading: infoLoading, data: infoData } = useQuery&lt;IInfoData&gt;(
  [&#39;info&#39;, coinId],
  () =&gt; fetchCoinInfo(coinId.coinId),
);

const { isLoading: tickersLoading, data: tickersData } = useQuery&lt;IPriceData&gt;(
  [&#39;tickers&#39;, coinId],
  () =&gt; fetchCoinTickers(coinId.coinId),
);</code></pre>
<ul>
<li>위의 코드에서 coinId를 쿼리 키의 일부로 사용하여 각 코인에 대한 정보와 티커 쿼리를 고유하게 식별한다.</li>
</ul>
<h3 id="여러-쿼리의-상태-관리">여러 쿼리의 상태 관리</h3>
<ul>
<li>두 개 이상의 useQuery를 사용할 때, 각 쿼리의 로딩 상태나 에러 상태를 개별적으로 처리할 수 있다.</li>
<li>예를 들어, 위의 예시에서는 infoLoading과 tickersLoading을 통해 각 쿼리의 로딩 상태를 파악할 수 있다.</li>
<li>필요한 경우, 이러한 상태들을 조합하여 컴포넌트의 전체 로딩 상태를 결정할 수도 있다.</li>
</ul>
<pre><code class="language-tsx">const loading = infoLoading || tickersLoading;</code></pre>
<ul>
<li>이렇게하면 Coin 컴포넌트의 렌더링 로직에서 loading 변수를 사용하여 전체 로딩 상태를 처리할 수 있다.</li>
</ul>
<br/>

<h1 id="react-query-devtools">React Query Devtools</h1>
<ul>
<li>앞서 React Query는 데이터를 자동으로 캐시하므로 동일한 요청을 여러 번 수행할 필요가 없다고 했다.</li>
<li>React Query Devtools는 <code>캐싱된 쿼리</code>와 <code>관련된 정보</code>를 시각적으로 확인할 수 있게 도와주는 도구다.</li>
</ul>
<h2 id="react-query-devtools-설치">React Query Devtools 설치</h2>
<pre><code class="language-bash">npm i @tanstack/react-query-devtools</code></pre>
<h2 id="react-query-devtools-적용">React Query Devtools 적용</h2>
<pre><code class="language-tsx">import router from &#39;./router&#39;;
import { RouterProvider } from &#39;react-router-dom&#39;;
import { QueryClient, QueryClientProvider } from &#39;@tanstack/react-query&#39;;
import { ReactQueryDevtools } from &#39;@tanstack/react-query-devtools&#39;;

import { ThemeProvider } from &#39;styled-components&#39;;
import GlobalStyle from &#39;./styles/GlobalStyle&#39;;
import theme from &#39;@/styles/theme&#39;;

const queryClient = new QueryClient();

function App() {
  return (
    &lt;ThemeProvider theme={theme.light}&gt;
      &lt;QueryClientProvider client={queryClient}&gt;
        &lt;GlobalStyle /&gt;
        &lt;RouterProvider router={router} /&gt;
        &lt;ReactQueryDevtools initialIsOpen={false} /&gt;
      &lt;/QueryClientProvider&gt;
    &lt;/ThemeProvider&gt;
  );
}

export default App;</code></pre>
<ul>
<li>애플리케이션에서 캐싱된 데이터를 보여주는 도구가 추가된다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/b10eac6a-e355-40b5-bdda-063eeb63ae4e/image.png" width="80%">

<blockquote>
<p>Devtools는 개발 모드에서만 번들에 포함되므로, 프로덕션 빌드 중에 제외하는 것에 대해 걱정할 필요가 없다.</p>
</blockquote>
<h4 id="학습자료">학습자료</h4>
<p><a href="https://nomadcoders.co/react-masterclass/lobby">노마드코더-리액트 마스터</a>
<a href="https://tanstack.com/query/latest">TanStack Query 공식문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - Error Boundary]]></title>
            <link>https://velog.io/@sarang_daddy/React-Error-Boundary</link>
            <guid>https://velog.io/@sarang_daddy/React-Error-Boundary</guid>
            <pubDate>Sun, 17 Sep 2023 18:11:19 GMT</pubDate>
            <description><![CDATA[<p>웹 앱을 구축하는 것은 복잡한 과정이다. 때때로 문제가 발생하게 되며, 이런 문제를 완전히 막을 수는 없다. 예상치 못한 방식으로 오류가 발생할 것을 <code>예상</code>하고 그에 대응할 <code>준비</code>가 되어 있어야 한다. 
이를 위한 Error Boundary를 알아보고 직접 만들어 보자.</p>
<h1 id="error-boundary-란">Error Boundary 란?</h1>
<p><code>에러에 대한 경계</code>를 의미하는 Error Boundary는 경계 내의 구간에서 에러가 발생하면 그 에러를 잡아내서 처리할 수 있는 역할을 한다. 기본적인 로직은 아래와 같다.</p>
<ol>
<li>React에서는 특정 컴포넌트 렌더링 도중 에러가 발생하면 해당 컴포넌트 UI를 제거한다.</li>
<li>에러에 대한 <code>준비</code>가 따로 되어 있지 않다면 React에서 제공하는 에러페이지로 넘어간다.<blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/68f84c3a-bc42-45c2-bbe4-71d927dc2bdd/image.png" alt=""></p>
<ul>
<li>실제 환경에서는 아무 페이지도 뜨지 않을 것이다. 사용자는 사라진다..😢</li>
</ul>
</blockquote>
</li>
<li>이를 방지하기 위해 컴포넌트를 Error Boundary로 감싸준다.</li>
<li>경계 내의 컴포넌트에서 에러가 발생한다면 준비된 UI를 보여 줄 수 있다.</li>
<li>준비된 UI를 fallback이라 한다.<blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/4fad2d9d-4569-4ee9-8c61-a9c42d00d152/image.png" alt=""> </p>
<ul>
<li>예상치 못한 에러가 발생하면 사용자에게 다시 홈부터 시작하도록 유도할 수 있다.</li>
</ul>
</blockquote>
</li>
</ol>
<h1 id="error-boundary-생성">Error Boundary 생성</h1>
<p><a href="https://react-ko.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary">공식문서</a>를 살펴보면 Error Boundary는 현재 함수형 컴포넌트로 작성할 수 있는 방법이 없으며 클래스로 예시 코드를 알려주고 있다.</p>
<blockquote>
<p>클래스를 직접 작성하기 싫다면 <a href="https://github.com/bvaughn/react-error-boundary">react-error-boundary</a>를 제공해 준다.</p>
</blockquote>
<p>클래스 예시 코드를 참고하여 코드를 생성해보자.</p>
<pre><code class="language-tsx">import { Component, ErrorInfo, ReactNode } from &#39;react&#39;;

// 에러바운더리를 사용할 때 fallback prop을 제공하지 않으면 사용되는 기본 UI
import { DefaultErrorFallback } from &#39;./DefaultErrorFallback&#39;;

// 에러바운더리 컴포넌트 props 타입 정의
interface ErrorBoundaryProps {
  children: ReactNode; // 렌더링될 자식 컴포넌트들
  fallback?: ReactNode; // 에러 발생시 보여주는 컴포넌트
}

// 에러바운더리 컴포넌트 state 타입 정의
interface ErrorBoundaryState {
  hasError: boolean; // 에러 발생 여부
  error?: Error; // 에러 객체
}

export class ErrorBoundary extends Component&lt;
  ErrorBoundaryProps,
  ErrorBoundaryState
&gt; {
  // 생성자에서 초기 상태 설정
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: undefined };
  }

  // React의 생명주기 메서드 &#39;getDerivedStateFromError&#39;
  // 하위 컴포넌트에서 에러가 발생하면 호출된다.
  // 에러 객체를 받아서 새로운 상태를 반환한다.
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  // React의 생명주기 메서드 &#39;componentDidCatch&#39;
  // 에러와 에러 정보를 받아서 처리한다.
  // 분석 서비스에 에러를 기록하는 등 추가 작업이 가능하다.
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error(error, errorInfo);
  }

  // 컴포넌트 렌더링
  render() {
    // 에러가 있으면 fallback 렌더링
    if (this.state.hasError &amp;&amp; this.state.error) {
      return (
        this.props.fallback || &lt;DefaultErrorFallback error={this.state.error} /&gt;
      );
    }
    // 에러 없으면 children 렌더링
    return this.props.children;
  }
}</code></pre>
<h1 id="error-boundary-적용하기">Error Boundary 적용하기</h1>
<ul>
<li>Error Boundary를 적용하고 싶은 컴포넌트를 감싸주면 된다.</li>
</ul>
<h3 id="전체-페이지에-적용">전체 페이지에 적용</h3>
<pre><code class="language-tsx">// Layout
import { Outlet } from &#39;react-router-dom&#39;;
import { ErrorBoundary } from &#39;@/components/ErrorBoundary&#39;;

export const Layout = () =&gt; {
  return (
    &lt;ErrorBoundary&gt;
      &lt;Outlet /&gt;
    &lt;/ErrorBoundary&gt;
  );
};</code></pre>
<h3 id="개별-라우트-내에-적용">개별 라우트 내에 적용</h3>
<pre><code class="language-tsx">// Router
import { createBrowserRouter } from &#39;react-router-dom&#39;;

import { Home } from &#39;@/routes/Home&#39;;
import { Detail } from &#39;@/routes/Detail&#39;;
import { Layout } from &#39;@/routes/Layout&#39;;
import { ROUTE_PATH } from &#39;@/router/routePath&#39;;
import { NotFound } from &#39;@/routes/NotFound&#39;;
import { ErrorBoundary } from &#39;@/components/ErrorBoundary&#39;;

export const router = createBrowserRouter([
  {
    element: &lt;Layout /&gt;,
    path: ROUTE_PATH.ROOT,
    errorElement: &lt;NotFound /&gt;,
    children: [
      {
        path: ROUTE_PATH.HOME,
        element: &lt;Home /&gt;,
      },
      {
        path: ROUTE_PATH.DETAIL,
        element: (
          &lt;ErrorBoundary&gt;
            &lt;Detail /&gt;
          &lt;/ErrorBoundary&gt;
        ),
      },
    ],
  },
]);</code></pre>
<ul>
<li><code>Detail</code> 라우트에서만 발생하는 오류를 <code>ErrorBoundary</code>로 캡처한다. <code>Home</code> 라우트에서 발생하는 오류는 캡처되지 않는다.</li>
</ul>
<h3 id="특정-컴포넌트-내에-적용">특정 컴포넌트 내에 적용</h3>
<pre><code class="language-tsx">// Component
import { ErrorBoundary } from &#39;@/components/ErrorBoundary&#39;;

const Detail = () =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;Detail Page&lt;/h1&gt;
      &lt;ErrorBoundary&gt;
        &lt;Profile /&gt;
      &lt;/ErrorBoundary&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<ul>
<li><code>Profile</code> 컴포넌트 내에서 발생하는 오류만 <code>ErrorBoundary</code>로 캡처된다.</li>
</ul>
<h1 id="error-boundary의-한계">Error Boundary의 한계</h1>
<h3 id="error-boundary는-비동기-통신-에러는-잡지-못한다">Error Boundary는 비동기 통신 에러는 잡지 못한다</h3>
<pre><code class="language-tsx">export const Detail = () =&gt; {

  // 중략

  // 에러 바운더리 확인을 위한 에러 유도 코드
  useEffect(() =&gt; {
    async function test() {
      throw new Error(&#39;에러바운더리 테스트&#39;);
    }
    test();
  }, []);

  return (
  // 중략
  )</code></pre>
<ul>
<li>Detail 컴포넌트에서 useEffect를 사용하여 비동기 통신 중 에러가 발생했다.</li>
<li>하지만 Error Boundary의 fallback UI가 아닌 정상적인 화면이 렌더링 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/d0b835ab-7bac-4e65-a5cc-c7e43d2d9b08/image.gif" alt=""></p>
<h3 id="error-boundary는-렌더링-중-발생한-에러만을-잡는다">Error Boundary는 렌더링 중 발생한 에러만을 잡는다</h3>
<p>Error Boundary는 컴포넌트 트리 내에서 하위 <code>컴포넌트의 렌더링에서 발생</code>하는 에러를 잡는다.
즉, 아래의 경우에는 에러를 포착할 수 없다.</p>
<ul>
<li><p>이벤트 핸들러
Error Boundary는 이벤트 핸들러 내에서 발생하는 오류를 포착하지 않는다. 예를 들어, <code>onClick</code> 또는 <code>onChange</code>와 같은 이벤트 핸들러에서 발생하는 오류는 Error Boundary로 잡히지 않는다.</p>
</li>
<li><p>비동기 코드
<code>setTimeout</code>, <code>requestAnimationFrame</code>, <code>서버 사이드 렌더링</code> 등 비동기 코드에서 발생하는 오류는 Error Boundary에서 포착되지 않는다.</p>
</li>
<li><p>Server Side Rendering (SSR)
<code>서버 사이드 렌더링 중</code>에 발생하는 오류는 Error Boundary에서 잡히지 않는다.</p>
</li>
<li><p>Error Boundary 자체의 오류
Error Boundary 컴포넌트 내부에서 발생하는 오류는 해당 Error Boundary에서 포착되지 않는다. 이러한 오류를 포착하려면 상위 컴포넌트에서 다른 Error Boundary를 사용해야 한다.</p>
</li>
<li><p>외부 라이브러리와 코드
Error Boundary는 React 컴포넌트 트리 내에서 발생하는 오류만 포착한다. 따라서 React 외부에서 발생하는 오류나 외부 라이브러리에서 발생하는 오류는 포착되지 않는다.</p>
</li>
</ul>
<h1 id="error-boundary의-사용">Error Boundary의 사용</h1>
<p>위 내용들처럼 Error Boundary는 에러에 대한 준비를 가능하게 해줌으로서 애플리케이션의 안정성을 향상 시켜주지만, 한계점이 분명하기에 전체 오류 관리 전략의 <code>일부</code>로서 사용되도록 해야한다.</p>
<h4 id="참고자료"><a href="https://stackoverflow.com/questions/74019392/using-react-error-boundary-with-react-router">참고자료</a></h4>
<h3 id="error-boundary의-적절한-사용">Error Boundary의 적절한 사용</h3>
<ul>
<li><p><strong>오류 경계를 너무 적게 사용하는 경우</strong>
애플리케이션의 상단에 단 하나의 오류 경계만 있으면, 한 부분에서의 오류가 전체 애플리케이션에 영향을 미칠 수 있다.</p>
</li>
<li><p><strong>오류 경계를 과도하게 사용하는 경우</strong>
각 컴포넌트마다 오류 경계를 설정하면, 사용자 경험이 혼란스러워질 수 있다. 또한 성능에도 영향을 미칠 수 있다.</p>
</li>
</ul>
<h3 id="error-boundary의-적절한-위치-찾기">Error Boundary의 적절한 위치 찾기</h3>
<p>애플리케이션의 <code>기능 경계</code>를 확인하고 그곳에 오류 경계를 배치하는 것이 가장 좋다.</p>
<p>Twitter를 예로 들면, 메인 콘텐츠 섹션은 <code>Home</code>, <code>Trends for you</code>, <code>Who to follow</code> 등 세 가지로 구분된다.</p>
<p>Who to Follow 섹션 내에서도 여러 하위 섹션이 있으며, <strong>각각의 섹션에서 오류가 발생할 경우 다른 섹션에 영향을 주어야 하는지를 고려하여 오류 경계를 설정</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/98e2d2c4-1c96-4fd2-9f7f-9bcfc0daf3d4/image.png" alt=""></p>
<h3 id="fault-tolerance-테스트">Fault Tolerance 테스트</h3>
<p>애플리케이션의 오류 허용성을 테스트하는 가장 좋은 방법은 일부러 오류를 발생시켜보는 것이다.</p>
<pre><code class="language-tsx">function CreditCardInput(props) {
  // What happens if I messed up here? Let&#39;s find out!
  throw new Error(&quot;oops, I made a mistake!&quot;)
  return &lt;input className=&quot;credit-card&quot; /&gt;
}</code></pre>
<h3 id="요약">요약</h3>
<ol>
<li>애플리케이션 상단에 단 하나의 오류 경계만 두는 것은 피해야 한다.</li>
<li>오류 경계를 과도하게 사용하는 것도 피해야 한다.</li>
<li>애플리케이션의 기능 경계를 파악하고 그곳에 오류 경계를 배치한다.</li>
<li>&quot;이 컴포넌트가 충돌하면 형제 컴포넌트도 충돌해야 하는가?&quot;라는 질문을 통해 기능 경계를 찾을 수 있다.</li>
<li>오류 상태를 위해 애플리케이션을 의도적으로 설계해야 한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - useFetch]]></title>
            <link>https://velog.io/@sarang_daddy/React-useFetch</link>
            <guid>https://velog.io/@sarang_daddy/React-useFetch</guid>
            <pubDate>Fri, 15 Sep 2023 18:41:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/2ca83fa5-6aba-4675-b5d6-d27c34249994/image.png" alt=""></p>
<p><code>외부 리소스와의 상호 작용 중</code> 외부 API 호출할 때는 <code>useEffect</code> 훅을 사용한다.
이는 컴포넌트가 렌더링 된 후에 외부 데이터를 가져와 컴포넌트의 상태를 업데이트 되도록 해준다.</p>
<p>다만, 여러 컴포넌트에서 동일한 <code>데이터 패치 로직</code>을 사용할 경우 코드 중복이 발생할 수 있다. 이러한 중복을 줄이고 코드의 재사용성과 유지보수성을 높이기 위해 <code>공통로직</code>을 <code>커스텀 훅</code>으로 분리해서 사용할 수 있다.</p>
<h1 id="커스텀-훅">커스텀 훅</h1>
<p>커스텀 훅은 React의 기본 훅을 활용하여 사용자가 만든 재사용 가능한 함수다.
기존 프로젝트에서 반복되는 로직을 따로 함수로 만들어서 사용하던 방식에 React의 기본 훅이 추가되었다고 생각할 수 있다.</p>
<ul>
<li>훅을 만들기에 이름 앞에는 &quot;use&quot;로 시작한다.</li>
<li>커스텀 훅은 <code>상태 자체</code>를 공유하는 것이 아닌 <code>상태 로직</code>을 공유한다.</li>
<li>커스텀 훅을 사용하는 컴포넌트는 커스텀 훅이 반환해준 <code>결과</code>에만 의존하면 된다.</li>
</ul>
<blockquote>
<p>즉, 데이터를 가져오는 방식은 커스텀 훅에서 고민하고 결과 값을 반환해준다.
컴포넌트에서는 가져온 <code>데이터를 어떻게 보여줄까</code>만 고민하면 된다.
<em><strong>이는 컴포넌트를 보다 더 선언적으로 만들어 준다.</strong></em></p>
</blockquote>
<h1 id="usefetch-커스텀-훅-만들기">useFetch 커스텀 훅 만들기</h1>
<h2 id="1-useeffect-사용하던-컴포넌트">1. useEffect 사용하던 컴포넌트</h2>
<ul>
<li>아래 코드는 <code>useEffect</code>를 사용해 외부 데이터를 받아오고 있다.</li>
<li>데이터를 받아오면 컴포넌트 상태를 업데이트 후 화면을 변경한다.</li>
</ul>
<pre><code class="language-tsx">// Home
export const Home = () =&gt; {
  const [loading, setLoading] = useState&lt;boolean&gt;(true);
  const [characters, setCharacters] = useState&lt;CharacterTypes[]&gt;([]);

  useEffect(() =&gt; {
    let ignore = false;

    const getCharacters = async () =&gt; {
      const data = await fetchCharacters(50);

      if (!ignore) {
        setCharacters(data.results);
        setLoading(false);
      }
    };

    getCharacters();

    return () =&gt; {
      ignore = true;
    };
  }, []);

  return (
    // --중략
  );
};

// Detail
export const Detail = () =&gt; {
  const { id } = useParams();
  const navigate = useNavigate();
  const [loading, setLoading] = useState(true);
  const [characterDetail, setCharacterDetail] =
    useState&lt;CharacterDetailTypes&gt;();

  useEffect(() =&gt; {
    let ignore = false;

    const getCharacterDetail = async () =&gt; {
      const data = await fetchCharacterDetail(id);

      if (!ignore) {
        setCharacterDetail(data.results[0]);
        setLoading(false);
      }
    };

    getCharacterDetail();

    return () =&gt; {
      ignore = true;
    };
  }, [id]);

  return (
    &lt;&gt;
        // -- 중략
    &lt;/&gt;
  );
};</code></pre>
<ul>
<li>데이터를 받아오고 컴포넌트를 업데이트하는 작업에는 문제가 없다.</li>
<li>다만, 컴포넌트에서 데이터를 어떻게 가져오는지, 에러 처리는 어떻게 해야할지, 로딩 상태는 어떻게 관리하는지를 관여하고 있다.</li>
<li>또, 두 컴포넌트에서 요청 API만 다를 뿐 같은 로직을 중복 사용하고 있다.</li>
</ul>
<h2 id="2-usefetch-커스텀-훅을-만들기-전-고려사항">2. useFetch 커스텀 훅을 만들기 전 고려사항</h2>
<ul>
<li>useFetch는 <code>API 호출 함수</code>와 <code>해당 함수에 전달될 인수</code>를 받아야 한다.</li>
<li>useEffect를 사용하여 컴포넌트 마운트 시 데이터를 로드한다.</li>
<li>status, data, error 상태를 관리한다.</li>
<li>API 호출 중에 오류가 발생할 경우를 대비하여 적절한 <code>에러 처리</code>를 고려해야 한다.</li>
<li>컴포넌트가 언마운트될 때 진행 중인 비동기 작업을 취소하거나 무시해서 메모리 누수를 방지 해야한다.</li>
</ul>
<h2 id="3-usefetch-생성-과정">3. useFetch 생성 과정</h2>
<h3 id="3-1-타입을-정의한다">3-1. 타입을 정의한다</h3>
<pre><code class="language-tsx">type Status = &#39;initial&#39; | &#39;pending&#39; | &#39;fulfilled&#39; | &#39;rejected&#39;;

interface UseFetch&lt;T&gt; {
  data?: T;
  status: Status;
  error?: Error;
}</code></pre>
<ul>
<li><code>Status</code>는 요청의 상태를 나타내며, 초기 상태(<code>initial</code>), 로딩 중(<code>pending</code>), 요청 성공(<code>fulfilled</code>), 요청 실패(<code>rejected</code>) 중 하나의 값을 갖는다.</li>
<li><code>UseFetch&lt;T&gt;</code>는 <code>useFetch</code> 훅이 반환하는 객체의 타입이다. 여기서 <code>T</code>는 fetch 함수의 반환 타입을 나타낸다.</li>
</ul>
<h3 id="3-2-usefetch-함수-선언">3-2. useFetch 함수 선언</h3>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/617e44a4-7f3c-48d1-a856-ffc259a5d70a/image.png" alt=""></p>
<ul>
<li>1️⃣ : <code>fetchFunction</code>: 데이터를 가져오는 함수(<code>API 호출 함수</code>)다. 이 함수는 <code>Promise</code>를 반환해야 한다.</li>
<li>2️⃣ : <code>...args</code>: <code>fetchFunction</code>에 전달될 인수들이다.</li>
<li>3️⃣ : 함수는 <code>UseFetch&lt;T&gt;</code> 타입의 객체를 반환한다.</li>
</ul>
<h3 id="3-3-상태-초기화">3-3. 상태 초기화</h3>
<pre><code class="language-tsx">const [state, setState] = useState&lt;UseFetch&lt;T&gt;&gt;({
  status: &#39;initial&#39;,
  data: undefined,
  error: undefined
});</code></pre>
<ul>
<li>초기 상태는 <code>status</code>가 <code>initial</code>이며, <code>data</code>와 <code>error</code>는 정의되지 않은 상태다.</li>
</ul>
<h3 id="3-4-useeffect를-사용한-데이터-로드">3-4. useEffect를 사용한 데이터 로드</h3>
<pre><code class="language-tsx">useEffect(() =&gt; {
  let ignore = false;</code></pre>
<ul>
<li><code>ignore</code> 변수는 컴포넌트가 언마운트된 후에 상태를 변경하려는 시도를 방지한다.</li>
</ul>
<pre><code class="language-tsx">const fetchData = async () =&gt; {
  setState({ ...state, status: &#39;pending&#39; });

  try {
    const data = await fetchFunction(...args);
    if (!ignore) {
      setState({ status: &#39;fulfilled&#39;, data });
    }
  } catch (error) {
    if (!ignore) {
      setState({ status: &#39;rejected&#39;, error });
    }
  }
};</code></pre>
<ul>
<li>비동기 함수 <code>fetchData</code>를 선언한다.</li>
<li>함수가 호출되면 상태를 <code>pending</code>으로 설정하여 로딩 중임을 나타낸다.</li>
<li><code>fetchFunction</code>을 호출하여 데이터를 가져온다.<ul>
<li>성공적으로 데이터를 가져오면 상태를 <code>fulfilled</code>로 설정하고 데이터를 저장한다.</li>
<li>오류가 발생하면 상태를 <code>rejected</code>로 설정하고 오류를 저장한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-tsx">fetchData();

return () =&gt; {
  ignore = true;
};</code></pre>
<ul>
<li><code>useEffect</code> 내에서 <code>fetchData</code> 함수를 호출하여 데이터 로드를 시작한다.</li>
<li>컴포넌트가 언마운트 되면 클린업 함수에서 <code>ignore</code>를 <code>true</code>로 설정하여 비동기 작업이 일어나도 상태가 변경되는 것을 방지한다.</li>
</ul>
<h3 id="3-5-상태-반환">3-5. 상태 반환</h3>
<pre><code class="language-tsx">return state;</code></pre>
<ul>
<li>마지막으로 현재 상태를 반환한다. 이 상태에는 <code>data</code>, <code>status</code>, <code>error</code>가 포함된다.</li>
</ul>
<h2 id="4-usefetch-전체-코드">4. useFetch 전체 코드</h2>
<pre><code class="language-tsx">import { useState, useEffect } from &#39;react&#39;;

type Status = &#39;initial&#39; | &#39;pending&#39; | &#39;fulfilled&#39; | &#39;rejected&#39;;

interface UseFetch&lt;T&gt; {
  data?: T;
  status: Status;
  error?: Error;
}

export const useFetch = &lt;T&gt;(
  // API 호출 함수 그리고 함께 전달될 인수.
  fetchFunction: (...args: any[]) =&gt; Promise&lt;T&gt;,
  ...args: any[]
): UseFetch&lt;T&gt; =&gt; {
  // useFetch에서 관리하는 상태 -&gt; 최종 반환값이 된다.
  const [state, setState] = useState&lt;UseFetch&lt;T&gt;&gt;({
    status: &#39;initial&#39;,
    data: undefined,
    error: undefined,
  });

  // useEffect를 통해 데이터를 가져온다.
  useEffect(() =&gt; {
    // 컴포넌트가 언마운트 되면 비동기 작업으로 인한 상태 변경 방지.
    let ignore = false;

    const fetchData = async () =&gt; {
      // 비동기 함수 fetchData()를 실행하면 &quot;로딩중&quot;이 된다.
      setState({ ...state, status: &#39;pending&#39; });

      // 요청 API(fetchFunction)를 실행한다.
      try {
        // 요청 API 인수에 함께 전달받았던 &quot;...args&quot;를 준다.
        const data = await fetchFunction(...args);
        if (!ignore) {
          // 성공하면 상태를 &#39;fulfilled로 변경하고 데이터를 저장한다.
          setState({ status: &#39;fulfilled&#39;, data });
        }
      } catch (error) {
        if (!ignore) {
          // 실패하면 상태를 &#39;rejected&#39;로 변경하고 오류를 저장한다.
          setState({ status: &#39;rejected&#39;, error: error as Error });
        }
      }
    };

    fetchData();

    return () =&gt; {
      ignore = true;
    };
  }, [fetchFunction]);

  return state;
};</code></pre>
<h2 id="5usefetch를-사용하는-컴포넌트">5.useFetch를 사용하는 컴포넌트</h2>
<pre><code class="language-tsx">// Home
export const Home = () =&gt; {
  const { data: characters, status } = useFetch(fetchCharacters, 50);
  const charactersList = characters?.results;

  return (
        // -- 중략
  );
};

// Details
export const Detail = () =&gt; {
  const { id } = useParams();
  const navigate = useNavigate();
  const { data: characterDetail, status } = useFetch(fetchCharacterDetail, id);
  const detailsInfo = characterDetail?.results[0];

  const handleBackPage = () =&gt; {
    navigate(-1);
  };

  return (
    &lt;&gt;
      // -- 중략
    &lt;/&gt;
  );
};</code></pre>
<ul>
<li>두 컴포넌트에서 중복으로 사용되던 데이터 패치 로직이 생략되었다.</li>
<li>데이터 패치 관련 로직은 useFetch에서만 관리하면 된다.</li>
<li><em><strong>각 컴포넌트는 데이터를 가져오고 어떻게 보여줄지만 고려하면 된다.</strong></em></li>
</ul>
<blockquote>
<p>데이터를 받아오는 과정, 과정 중의 상태, 에러 여부는 컴포넌트가 고려하지 않아도 된다.
컴포넌트는 받아온 결과 값과 상태만을 고려해서 화면을 보여주면 된다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 클린업으로 엄격모드 로그인 문제 해결하기]]></title>
            <link>https://velog.io/@sarang_daddy/React-%ED%81%B4%EB%A6%B0%EC%97%85%EC%9C%BC%EB%A1%9C-%EC%97%84%EA%B2%A9%EB%AA%A8%EB%93%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sarang_daddy/React-%ED%81%B4%EB%A6%B0%EC%97%85%EC%9C%BC%EB%A1%9C-%EC%97%84%EA%B2%A9%EB%AA%A8%EB%93%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 01 Sep 2023 16:10:45 GMT</pubDate>
            <description><![CDATA[<p><code>소셜 로그인</code>으로 구현한 로그인 로직에서 <strong>동일한 사용자</strong>로 로그인을 시도했음에도 로그인이 잘되는 경우와 에러가 발생하는 경우가 발생했다. 성공이면 성공, 에러면 에러가 나와야 하는데, 잘 되기도 하고 안되기도 하고 개발 입장에서는 원인을 파악하기 어려운 상황이었다. 다행히 그 원인을 <a href="https://react-ko.dev/learn/synchronizing-with-effects">리액트 공식문서</a> 학습에서 <code>엄격 모드</code> 때문임을 알게 되었다. </p>
<h1 id="😢-문제-상황">😢 문제 상황</h1>
<p>동일한 사용자로 로그인을 시도하면 성공하는 경우도 있고 실패하는 경우도 발생</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/56a2870a-b1c4-4020-843d-7ac41f05f81a/image.gif" alt=""></p>
<h1 id="👀-문제-파악하기">👀 문제 파악하기</h1>
<p>개발자 도구 네트워크를 확인하니 로그인 요청이 <em><strong>두 번</strong></em> 발생하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/b03a7797-d7e4-41ba-80a6-19b78349b016/image.png" alt=""></p>
<ul>
<li>요청이 두 번 발생하기에 잘못된 인증코드가 서버로 가는 경우가 발생</li>
<li><code>엄격모드</code>는 컴포넌트의 부수효과를 두 번 호출한다.</li>
<li>이는 상용 환경에서는 발생하지 않는 문제이지만, 엄격모드를 제거한다면 개발 단계에서 잠재적 문제를 발견하기 어렵다.</li>
<li>useEffect에 <code>클립업</code> 함수를 추가해서 두 번째 요청을 무시하도록 해야한다.</li>
</ul>
<h1 id="💡-문제-해결">💡 문제 해결</h1>
<h2 id="로그인-로직-점검">로그인 로직 점검</h2>
<h3 id="1-사용자에게-github-로그인을-제안">1. 사용자에게 GitHub 로그인을 제안</h3>
<ul>
<li>웹에서 &quot;GitHub으로 로그인&quot; 버큰 제공</li>
<li>사용자 로그인 버튼 클릭</li>
</ul>
<h3 id="2-사용자를-github-인증-페이지로-리다이렉트">2. 사용자를 GitHub 인증 페이지로 리다이렉트</h3>
<ul>
<li>GitHub의 OAuth 인증 페이지로 리다이렉트</li>
</ul>
<h3 id="3-사용자-인증-및-권한-부여">3. 사용자 인증 및 권한 부여</h3>
<ul>
<li>사용자는 GitHub에 로그인하고 애플리케이션에 필요한 권한 부여</li>
</ul>
<h3 id="4-github에서-웹으로-리다이렉트">4. GitHub에서 웹으로 리다이렉트</h3>
<ul>
<li>인증 및 권한 부여가 성공하면 GitHub는 사용자를 웹으로 다시 리다이렉트</li>
<li><em><strong>인증 코드(AUTHORIZATION_CODE)가 URL의 쿼리 파라미터로 전달</strong></em></li>
</ul>
<h3 id="5-서버로-authorization_code와-함께-로그인-요청">5. 서버로 AUTHORIZATION_CODE와 함께 로그인 요청</h3>
<ul>
<li>리다이렉트 되면서 클라이언트에서 서버측으로 AUTHORIZATION_CODE 코드와 함께 로그인 요청</li>
<li>서버는 AUTHORIZATION_CODE코드와 &quot;client_secret&quot;을 함께 GitHub OAuth 서버로 전송하여 액세스 토큰 획득</li>
</ul>
<h3 id="6-액세스-토큰을-이용하여-사용자-정보-조회">6. 액세스 토큰을 이용하여 사용자 정보 조회</h3>
<ul>
<li>서버는 액세스 토큰을 이용하여 GitHub API를 통해 사용자 정보를 가져온다</li>
<li>사용자 정보가 &quot;신규&quot; 회원인지 &quot;기존회원&quot; 인지 서버에서 판단하고 클라이언트에게 응답</li>
</ul>
<h3 id="7-서버-응답으로-회원가입-or-로그인-진행">7. 서버 응답으로 회원가입 or 로그인 진행</h3>
<ul>
<li>서버로 부터 &quot;응답&quot;과 함께 JWT을 받는다</li>
<li>응답이 &quot;신규&quot; 이면 JWT로 회원가입 진행</li>
<li>응답이 &quot;기존회원&quot; 결과를 받으면 JWT로 로그인 진행</li>
</ul>
<h2 id="5번-서버로-로그인-요청에서-디버깅">5번, 서버로 로그인 요청에서 디버깅</h2>
<pre><code class="language-tsx">const Callback = () =&gt; {
  const searchParams = new URLSearchParams(window.location.search);
  const code = searchParams.get(AUTHORIZATION_CODE);
  const { data } = useAsync(() =&gt; postLogin(code));
  const { handleLogin } = useAuthContext();
  const navigate = useNavigate();

  useEffect(() =&gt; {
    if (data?.status === &#39;FORBIDDEN&#39;) {
      const { nickname, profileUrl, oauthId } = data.data;
      navigate(
        `${REGISTER}?nickname=${nickname}&amp;profileUrl=${profileUrl}&amp;oauthId=${oauthId}`,
      );
    }

    if (data?.status === &#39;OK&#39;) {
      const { jwt } = data.data;
      handleLogin(jwt);
      navigate(HOME);
    }
  }, [data]);

  return (
  // 중략</code></pre>
<ul>
<li><code>const { data } = useAsync(() =&gt; postLogin(code))</code>이 원인으로 판단</li>
</ul>
<pre><code class="language-tsx">function useAsync&lt;T&gt;(
 // 중략 

  const fetchData = async (): Promise&lt;void&gt; =&gt; {
    dispatch({ type: &#39;LOADING&#39; });
    try {
      const response: AxiosResponse&lt;T&gt; = await callback();
      dispatch({ type: &#39;SUCCESS&#39;, data: response.data });
    } catch (e) {
      dispatch({ type: &#39;ERROR&#39;, error: e as AxiosError });
    }
  };

  useEffect(() =&gt; {
    if (skip) return;
    fetchData();
  }, deps);


// 중략
}</code></pre>
<ul>
<li>두 번째 로그인 요청에서는 <code>fetchData()</code>가 호출되지 않도록 <code>클립업</code>함수를 추가한다.</li>
</ul>
<pre><code class="language-tsx">  let ignore = false;

  useEffect(() =&gt; {
    if (skip) return;

    if (!ignore) {
      fetchData();
    }
    return () =&gt; {
      ignore = true;
    };
  }, deps);</code></pre>
<ul>
<li><code>fetchData()</code>는 <code>ignore</code>이 false 인 경우(첫 요청)에만 호출된다.</li>
<li><code>엄격모드</code>로 useEffect가 재실행될 때는 <code>ignore</code>이 true가 되어 두 번째 <code>fetchData()</code>호출을 방지한다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/0348f935-4a80-4e01-8fe2-59bb49235d46/image.png" alt=""></p>
<ul>
<li><code>엄격모드</code> 활성화 환경에서도 한 번의 요청만 처리되도록 수정하여 문제 해결</li>
</ul>
<h1 id="🧐-느낀점">🧐 느낀점</h1>
<p>계속해서 Effect의 좋은 점만을 생각하면서 사용을 해온 것 같다.
리액트 학습과 프로젝트 진행을 병행해 가면서 Effect 사용의 조심성이 계속 커지고 있다.
데이터 Fetch에서 특히 많이 사용하고 있는데, 이미 발생한 네트워크 요청은 실행 취소가 안되니, 클린업 함수에 대한 이해와 필요성을 많이 고민해 봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 상태 업데이트와 리렌더링 과정]]></title>
            <link>https://velog.io/@sarang_daddy/React-%EC%83%81%ED%83%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%99%80-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@sarang_daddy/React-%EC%83%81%ED%83%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%99%80-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 30 Aug 2023 06:56:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/6718c87a-bd56-4800-931a-270f7cfd2fc5/image.png" alt=""></p>
<p>React에서 상태가 변경되면 다시 렌더링 해준다는 개념으로 useState를 사용해왔다. 하지만 사용중에 원하던 상태값이 아직 반영되지 않았거나 상태가 변경되어도 렌더링이 되지 않는 등의 문제를 접할 수 있었다. </p>
<p><code>&quot;상태가 변하면 리렌더링&quot;</code>으로만 이해하지 말고 상태 업데이트로 인해 React에서는 어떠한 과정이 일어나는지 조금 더 깊게 이해해보자.</p>
<h2 id="🔍-상태-업데이트와-리렌더링-과정">🔍 상태 업데이트와 리렌더링 과정</h2>
<h3 id="1-사용자에-의해-이벤트-발생">1. 사용자에 의해 이벤트 발생</h3>
<ul>
<li>사용자가 버튼을 클릭하거나 입력을 하는 등의 이벤트가 발생한다.</li>
</ul>
<h3 id="2-이벤트-핸들러-함수-실행">2. 이벤트 핸들러 함수 실행</h3>
<ul>
<li>해당 이벤트에 대한 핸들러 함수가 실행된다. 예를 들어 버튼 클릭에 대한 <code>handleClick</code> 함수가 있다.</li>
</ul>
<h3 id="3-이벤트-핸들러-안에서-usestate의-setter-함수-실행">3. 이벤트 핸들러 안에서 useState의 setter 함수 실행</h3>
<ul>
<li>이벤트 핸들러 함수 내에서 <code>useState</code>의 setter 함수(ex:setAge)가 호출된다.</li>
<li>이 함수 호출로 인해 React는 상태 변경을 <code>예약</code>한다.</li>
<li>실제로 상태는 바로 업데이트 되지 않고 예약 된다. (setter 함수가 스택에 쌓이는 개념과 비슷하다.)</li>
<li><em><strong>상태 변경을 <code>일괄적</code>으로 처리하기 위해서 예약을 한다.</strong></em></li>
<li>예약된 setter 함수들은 이벤트 루프가 완료될 때까지 대기한다.</li>
</ul>
<h3 id="4-일괄-처리-batching">4. 일괄 처리 (Batching)</h3>
<ul>
<li>일괄 처리에서 대기 중인 setter 함수가 모두 처리된다.</li>
<li>setter 함수가 <code>직접적인 상태 할당</code>인 경우 상태에 새로운 값을 <code>직접</code> 제공한다.</li>
<li>setter 함수가 <code>업데이터 함수</code>인 경우 이전 상태를 기반으로 새로운 상태를 <code>계산</code>한다. </li>
<li>이렇게 일괄 처리로 변경된 상태들이 <code>최종 상태</code>가 결정된다.</li>
<li>일괄 처리 덕분에 여러 상태값이 변경되어도 렌더링은 한번만 일어난다.</li>
<li><em><strong>React는 여러 상태 업데이트를 일괄 처리하는 방식을 사용하여 효율을 높인다.</strong></em></li>
<li>아직 <code>최종 상태</code> 값이 반환된 것은 아니다.</li>
</ul>
<h3 id="5-리렌더링-컴포넌트-함수-실행">5. 리렌더링 (컴포넌트 함수 실행)</h3>
<ul>
<li>일괄 처리에서의 <code>최종상태</code>가 기존 상태와 동일하다면 리렌더링은 생략된다.</li>
<li>상태가 변경되었다고 판단되면 React는 컴포넌트를 리렌더링 한다.</li>
<li><strong><em>이 과정에서 useState는 최신 상태를 반환하고 변수에 할당한다.</em></strong></li>
</ul>
<h3 id="6-jsx-스냅샷-virtual-dom-생성">6. JSX 스냅샷 (Virtual DOM 생성)</h3>
<ul>
<li>리렌더링이 일어나면 새로운 JSX 구조가 생성된다.</li>
<li>리액트는 새로운 JSX 구조를 스냅샷으로 저장한다.</li>
<li>이것은 Virtual DOM의 형태로 <code>메모리 상에</code> 존재한다.</li>
</ul>
<h3 id="7-react가-jsx를-실제-dom에-반영">7. React가 JSX를 실제 DOM에 반영</h3>
<ul>
<li>React는 <code>이전 상태의 Virtual DOM</code>과 <code>새로운 Virtual DOM</code>을 비교하여 (Diffing 알고리즘), 실제 DOM에 반영할 변경 사항을 결정한다.</li>
<li><em><strong>즉, 변경이 필요한 DOM만이 업데이트 된다.</strong></em></li>
</ul>
<h3 id="실제-코드로-리렌더링-과정을-알아보기">실제 코드로 리렌더링 과정을 알아보기</h3>
<pre><code class="language-jsx">import React, { useState } from &#39;react&#39;;

function MyComponent() {
  const [count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment&lt;/button&gt;
      &lt;p&gt;{count}&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li>button을 클릭하면 이벤트가 실행 setCount로 상태 업데이트 예약</li>
<li>이벤트 루프가 종료되면 상태 업데이트 일괄처리</li>
<li>count + 1 로 최종상태 결정</li>
<li>이전 상태값과 다르기에 리렌더링 실행</li>
<li>count + 1 최종상태 값 반환</li>
<li>새로운 JSX 스냅샷 저장</li>
<li>이전 JSX 스냅샷과 새로운 JSX 스냅샷 비교</li>
<li><code>&lt;p&gt;</code> 태그 내부의 텍스트가 변경되므로, 실제 <code>&lt;p&gt;</code> DOM 변경</li>
<li><code>&lt;button&gt;</code> 태그는 변경되지 않았으므로, 이 부분은 그대로 유지</li>
</ul>
<blockquote>
<p>이 과정을 통해 React는 효율적으로 UI를 업데이트하며, 개발자에게 <code>선언적인 프로그래밍</code> 방식을 가능하게 한다.</p>
</blockquote>
<h2 id="⛔️-usestate-사용에-주의사항">⛔️ useState 사용에 주의사항</h2>
<ul>
<li>set 함수는 다음 렌더링에 대한 state 변수만 업데이트 한다.</li>
<li><strong>set 함수를 호출한 후에도 렌더링 과정에서 state 변수에는 변경 전 값이 담겨져 있다.</strong></li>
<li>set 함수로 호출한 값이 이전 값과 같다면 리렌더링 하지 않는다.</li>
<li><strong>React는 state 업데이트를 <code>일괄 처리</code>한다. 모든 이벤트 핸들러가 실행되고 set 함수를 호출한 후에 화면을 업데이트 한다.</strong></li>
<li>DOM에 접근하기 위해 React가 화면을 일찍 업데이트하도록 강제해야 하는 경우 <code>flushSync</code>를 사용한다.</li>
<li>React는 렌더링 도중 set 함수를 만나면 렌더링 중인 내용을 버리고 새로운 state로 다시 렌더링을 시도한다.</li>
</ul>
<h2 id="📝-예제로-이해하는-usestate">📝 예제로 이해하는 useState</h2>
<h3 id="이전-state를-기반으로-state-업데이트하기-업데이터-함수">이전 state를 기반으로 state 업데이트하기 (업데이터 함수)</h3>
<pre><code class="language-jsx">const [age, setAge] = useState(40);

const handleClick = () =&gt; {
  setAge(age + 1);
  setAge(age + 1);
  setAge(age + 1);
};</code></pre>
<ul>
<li>위 예제 처럼 설정자 함수로 이전 state를 기반으로 state 값을 업데이트 할 수 있다.</li>
<li>하지만 결과는 43이 아닌 41로 나온다.</li>
</ul>
<blockquote>
<p>렌더링 중 state age값은 업데이트 되지 않기 때문이다.</p>
</blockquote>
<ul>
<li>업데이트 된 state를 렌더링 중에 사용하고 싶다면 설정자 함수에 <code>업데이터 함수</code>를 전달해야 한다.</li>
</ul>
<pre><code class="language-jsx">function handleClick() {
  setAge((a) =&gt; a + 1); // setAge(40 =&gt; 41)
  setAge((a) =&gt; a + 1); // setAge(41 =&gt; 42)
  setAge((a) =&gt; a + 1); // setAge(42 =&gt; 43)
}</code></pre>
<ul>
<li><code>a =&gt; a + 1</code> 은 업데이터 함수다.</li>
<li>업테이터 함수는 대기 중인 state를 가져와서 다음 state를 계산한다.</li>
</ul>
<ol>
<li>a =&gt; a + 1은 대기 중인 state로 40를 받고 다음 state로 41을 반환한다.</li>
<li>a =&gt; a + 1은 대기 중인 state로 41을 받고 다음 state로 42를 반환한다.</li>
<li>a =&gt; a + 1은 대기 중인 state로 42를 받고 다음 state로 43를 반환한다.</li>
<li>대기 중인 다른 업데이트가 없으므로, React는 결국 43을 현재 state로 저장한다.</li>
</ol>
<blockquote>
<p>설정하려는 state가 이전 state에서 계산되는 경우 업데이터 함수를 전달하자.</p>
</blockquote>
<blockquote>
<p>❓ 만약 어떤 state가 <code>다른 state</code> 변수의 이전 state로부터 계산된다면?<br>💡 이를 하나의 객체로 결합하고 <code>reducer</code>를 사용하는 것이 좋다.</p>
</blockquote>
<h3 id="이전-렌더링에서-얻은-정보-저장하기">이전 렌더링에서 얻은 정보 저장하기</h3>
<ul>
<li>보통 이벤트 핸들러에서 state를 업데이트 한다.</li>
<li>드물게 렌더링된 값을 기반으로 state를 조정해야 하는 경우가 있다. ❓</li>
</ul>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count &gt; prevCount ? &quot;increasing&quot; : &quot;decreasing&quot;);
  }
  return (
    &lt;&gt;
      &lt;h1&gt;{count}&lt;/h1&gt;
      {trend &amp;&amp; &lt;p&gt;The count is {trend}&lt;/p&gt;}
    &lt;/&gt;
  );
}</code></pre>
<ul>
<li>count는 props로 전달 받은 값이다.</li>
<li>trend는 count의 값이 증가했는지 감소했는지 알고 싶다. ❗️</li>
<li>trend는 count의 렌더링 결과 값(렌더링된 값)을 가지고 판단해야한다.</li>
</ul>
<blockquote>
<p>일반적인 패턴은 아니지만 useEffect를 사용하는 것 보다 효율적이다. (두 번 렌더링 방지)</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JS - 브라우저 렌더링 과정]]></title>
            <link>https://velog.io/@sarang_daddy/JS-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@sarang_daddy/JS-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Mon, 28 Aug 2023 08:23:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/5334fba8-c6b0-4cba-8e71-169b01ce6ef9/image.png" alt=""></p>
<p>대부분의 프로그래밍 언어는 운영체제나 가성머신 위에서 실행된다.<br>하지만 웹 애플리케이션의 클라이언트 사이드 자바스크립트는 <strong>브라우저에서 HTML, CSS와 함께 실행된다.</strong></p>
<blockquote>
<p>브라우저 환경을 고려할 때 더 효율적인 클라이언트 사이드 자바스크립트 프로그래밍이 가능하다.</p>
</blockquote>
<p>브라우저가 HTML, CSS, 자바스크립트로 작성된 텍스트 문서를 어떻게 <code>파싱(해석)</code>하여 브라우저에 <code>렌더링</code>하는지 알아보자.</p>
<ul>
<li><p>파싱(parsing)<br>: 파싱은 <code>구문분석</code>이라 하며, 텍스트 문서를 읽어 들여 실행하기 위해 텍스트 문서의 문자열을 <code>토큰으로 분해(어휘 분석)</code>하고,<br>토큰에 문법적 의미와 구조를 반영하여 <code>파스 트리</code>를 생성하는 과정이다. 생성된 <code>파스 트리</code>를 기반으로 바이트 코드를 생성하고 실행한다.</p>
</li>
<li><p>렌더링(rendering)<br>: 렌더링은 HTML, CSS, 자바스크립트로 작성된 문서를 파싱하여 브라우저에 <code>시각적으로 출력</code>하는 것을 말한다.</p>
</li>
</ul>
<h1 id="브라우저의-렌더링-과정">브라우저의 렌더링 과정</h1>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/36e21781-d186-423a-833c-39b1e5c43625/image.png" alt=""></p>
<ol>
<li><code>브라우저</code>는 렌더링에 필요한 HTML, CSS, JS, Image 등의 리소스를 서버에 요청하고 응답 받는다.</li>
<li><code>브라우저 렌더링 엔진</code>은 서버로 부터 받은 HTML, CSS를 파싱하고(DOM, CSSOM) 결합하여 렌더 트리를 만든다.</li>
<li><code>브라우저 자바스크립트 엔진</code>은 응답된 JS를 파싱하여 AST를 생성하고 바이트코드로 변환하여 실행한다.</li>
<li>이때 JS는 <code>DOM API</code>를 통해 DOM, CSSOM을 변경할 수 있다. 변경된 DOM, CSSOM은 다시 렌더 트리로 결합된다.</li>
<li>위 과정에서 생성된 렌더 트리를 기반으로 HTML 요소의 레이아웃을 계산하고 브라우저 화면에 페인팅 한다.</li>
</ol>
<pre><code>브라우저의 핵심 기능
: 브라우저의 핵심 기능은 필요한 리소스(HTML, CSS, JS, img, font 등의 정적 파일 또는 서버가 동적으로 생성한 데이터)를 
서버에 요청하고 응답 받아 브라우저에 시각적으로 렌더링 하는 것이다.</code></pre></br>

<h1 id="1-요청과-응답">1. 요청과 응답</h1>
<p>리소스를 받기위해 브라우저는 서버에 어떻게 <code>요청</code>을 할까?<br>우리가 특정 웹사이트에 접속을 할때 사용하는 <code>주소창</code>과 <code>주소</code>를 통해 브라우저는 서버에 <code>요청</code>을 한다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/c123df23-0c1e-4a49-a35f-ffd667ae4c6f/image.png" alt=""></p>
<p>주소창에 주소(URL)을 입력하면 URL의 호스트(Domain) 이름이 <code>DNS</code>를 통해 IP 주소로 변환되고 이 IP 주소를 갖는 서버에 요청을 전송한다.</p>
<ul>
<li>IP : 인터넷상에서 내 컴퓨터 위치를 나타낼 수 있는 통신 규칙 (숫자 주소 형태)</li>
<li>호스트(Domain) : 사용자가 기억하기 쉽도록 IP를 문자로 바꾼 것</li>
<li>프로토콜 : 컴퓨터들 간 정보를 주고 받는 방법을 정리한 하나의 규칙 (ex.HTTP)</li>
<li>DNS : 도메인 네임 시스템으로 인터넷의 주소록. 기억하기 어려운 IP 대신 사용하는 도메인 이름을 IP 주소로 변환해주는 역할을 한다.</li>
</ul>
</br>

<h2 id="1-1-http-11과-http-20">1-1. HTTP 1.1과 HTTP 2.0</h2>
<ul>
<li>HTTP(HyperText Transfer Protocol)는 웹에서 브라우저와 서버가 통신하기 위한 프로토톨(규약)이다.</li>
<li>1991년 팀 버너스 리에 의해서 최초로 문서화 되었다.</li>
<li>1996년 HTTP/1.0 발표</li>
<li>1999년 HTTP/1.1 발표</li>
<li>2015년 HTTP/2 발표</li>
</ul>
<h3 id="http11">HTTP/1.1</h3>
<ul>
<li>커넥션(connection)당 하나의 요청과 응답만 처리한다.</li>
<li><strong>여래 개의 요청을 한번에 전송할수 없고 응답 또한 마찬가지다.</strong></li>
<li>즉, HTML 내의 리소스 요청(link 태그)이 개별적으로 전송되고 응답 또한 개별적으로 전송된다.</li>
</ul>
<h3 id="http2">HTTP/2</h3>
<ul>
<li>커넥션(connection)당 여러 개의 요청과 응답이 가능하다.</li>
<li><strong>HTTP/1.1과는 다르게 다중 요청/응답이 가능하다.</strong></li>
<li>여러 리소스의 동시 전송이 가능하므로 HTTP/1.1에 비해 페이지 로드 속도가 약 50% 빠르다.</li>
</ul>
</br>

<h1 id="2-html-파싱과-dom-생성">2. HTML 파싱과 DOM 생성</h1>
<p>앞서 url을 통해 브라우저가 서버에 요청을 보내면 응답으로 리소스가 온다.<br>리소스중 HTML 문서는 문자열로 이루어진 순수한 텍스트다.</p>
<pre><code>❗️ 순수한 텍스트인 HTML 문서를 브라우저에 시각적인 픽셀로 렌더링 하려면 
브라우저가 이해할 수 있는 자료구조(객체)로 변환하여 메모리에 저장해야 한다.</code></pre><h3 id="✅-브라우저-렌더링-엔진의-dom-생성">✅ 브라우저 렌더링 엔진의 DOM 생성</h3>
<p>브라우저의 렌더링 엔진이 HTML 문서를 브라우저가 이해할 수 있는 자료구조인 <code>DOM</code>으로 만들어준다.</p>
<ol>
<li>브라우저가 HTML 파일을 서버에 요청한다.</li>
<li>서버는 브라우저가 요청한 HTML 파일을 읽어 들여 메모리에 저장한 다음 메모리에 저장된 바이트(2진수)를 인터넷을 통해 응답한다.</li>
<li>브라우저는 서버가 응답한 HTML 문서를 바이트(2진수) 형태로 응답 받는다.</li>
<li>응답된 바이트 형태의 HTML 문서는 <strong>meta 태그의 charset 속성에서 지정된 인코딩 방식 기준</strong>으로 문자열된다. (UTF-8)</li>
<li>인코딩 방식은 <code>content-type : text/html; charset-utf-8</code>과 같이 <code>응답 헤더</code>에 담겨 응답된다.</li>
<li>브라우저는 응답 헤더를 확인하고 문자열로 변환한다.</li>
<li>문자열로 변환된 HTML 문서를 문법적 의미를 갖는 코도의 최소 단위인 <code>토큰</code>들로 분해한다.</li>
<li>각 토큰들을 <code>객체</code>로 변환하여 <code>노드</code>들을 생성한다. 노드는 DOM을 구성하는 기본 요소다.</li>
<li>HTML 문서는 HTML 요소들의 집합으로 이루어지며 HTML 요소는 중첩 관계를 갖는다.</li>
<li>중접관계란 HTML 요소안에 다른 HTML 요소가 존재함을 말한다. (부모 자식 관계)</li>
<li>모든 HTML 요소 관계를 반영하여 모든 노드들을 <code>트리 자료구조</code>로 구성한다. 이를 <code>DOM</code>이라 부른다.</li>
</ol>
<blockquote>
<p>DOM (Document Object Model) 문서 객체 모델은 HTML 문서를 파싱한 결과물이다.</p>
</blockquote>
</br>

<h2 id="3-css-파싱과-cssom-생성">3. CSS 파싱과 CSSOM 생성</h2>
<p>브라우저 렌더링 엔진은 HTML을 순차적으로 파싱하여 DOM을 생성해 나간다.<br>이 과정 중 CSS를 로드하는 <code>link 태그</code>나 <code>style 태그</code>를 만나면 DOM 생성을 일시 중지한다.</p>
<h3 id="✅-브라우저-렌더링-엔진의-cssom-생성">✅ 브라우저 렌더링 엔진의 CSSOM 생성</h3>
<ol>
<li>DOM을 만들어간다.</li>
<li>link, style 태그를 만난다.</li>
<li>link 태그의 href 속성을 통해 지정된 CSS 파일을 서버에 요청한다.</li>
<li>서버로 부터 응답받은 CSS 파일이나 style 내의 CSS를 HTML과 동일한 파싱 과정으로 <code>CSSOM</code>을 생성한다.</li>
<li>CSS 파싱이 완료되면 중지된 DOM 생성을 이어나간다.</li>
</ol>
</br>

<h2 id="4-렌더-트리-생성">4. 렌더 트리 생성</h2>
<p><code>브라우저 렌더링 엔진</code>은 서버로부터 응답된 <code>HTML</code>과 <code>CSS</code>를 위 과정을 거쳐 <code>DOM</code>과 <code>CSSOM</code>을 생성한다.<br>이 두 DOM과 CSSOM을 렌더링을 위해 <code>렌터 트리</code>로 결합된다.</p>
<ul>
<li>렌더 트리는 렌더링을 위한 <code>트리 구조</code>의 자료구조다.</li>
<li>브라우저 화면에 렌더링 되지 않는 노드(meta, script태드 등)와 CSS에 의해 비표시(disply:none)되는 노드들은 제외된다.</li>
<li><strong>즉, 렌더 트리는 브라우저 화면에 렌더링되는 노드만으로 구성된다.</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/d65c02c5-51c8-44c6-b387-2c1b4841e5be/image.png" alt=""></p>
<ul>
<li>완성된 렌더 트리는 각 HTML 요소의 레이아웃(위치와 크기)을 계산하는 데 사용된다. (<code>layout</code>)</li>
<li>계산 후에는 브라우저 화면에 픽셀을 렌더링하는 페인팅 처리에 입력된다. (<code>paint</code>)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/63fd6530-1ee4-4ef7-88b5-1dca20932392/image.png" alt=""></p>
<h3 id="렌더링의-반복-실행-리플로우-리페인팅">렌더링의 반복 실행 (리플로우, 리페인팅)</h3>
<ul>
<li>페인팅까지의 렌더링 과정은 반복해서 실행될 수 있다.</li>
<li>DOM, CCSOM이 변경되면 다시 렌더 트리로 결합되고 레이아웃과 페인트 과정이 다시 일어난다. 이를 <code>리플로우</code>라 한다.</li>
<li>레이아웃에 영향이 없는 변경을 페인트만 일어난다. 이를 <code>리페인트</code>라 한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/8e9706ac-410f-4722-9ccc-65f5c8be112b/image.png" width="50%">

<h4 id="리플로우가-일어나는-경우">리플로우가 일어나는 경우</h4>
<ul>
<li>자바스크립트에 의한 노드 추가 또는 삭제</li>
<li>브라우저 창의 리사이징에 의한 뷰포트 크기 변경</li>
<li>HTML 요소의 레이아웃에 변경을 발생시키는 width, margin 등의 스타일 변경</li>
</ul>
<h4 id="리페인트가-일어나는-경우">리페인트가 일어나는 경우</h4>
<ul>
<li>레이아웃을 제외한 스타일 변경</li>
<li>color, border-radius 등</li>
</ul>
<p><a href="https://docs.google.com/spreadsheets/u/0/d/1Hvi0nu2wG3oQ51XRHtMv-A_ZlidnwUYwgQsPQUg1R2s/pub?single=true&amp;gid=0&amp;output=html">CSS 스타일이 렌더링에 미치는 리스트</a></p>
<blockquote>
<p>레이아웃 계산과 페인팅을 다시 실행하는 리렌더링을 비용이 많이 발생하여 성능에 영향을 주는 요인이다.<br>가급적 리렌더링이 빈번하게 발생하지 않도록 주의할 필요가 있다.</p>
</blockquote>
<p><a href="https://velog.io/@sarang_daddy/CSS-translateX-vs-left">왜 &quot;left&quot;가 아닌 &quot;translateX&quot;를 사용할까?</a></p>
<pre><code>💡 리렌더링이란?
&quot;리렌더링&quot; 이라는 용어는 리플로우와 리페인팅 두 단계를 합쳐서 부르는 용어다.
즉, 브라우저가 화면에 반영해야할 변경 사항을 다시 계산하고 다시 페인팅하는 과정을 말한다.

DOM과 CSSOM이 다시 생성되는 것을 리렌더링이라고 부르기 보단 DOM과 CSSOM의 재구성이라고 말하는 것이 맞다.
다만, DOM과 CSSOM이 재구성되면 리플로우, 리페인팅도 일어나기에 통칭해서 리렌더링이라 부르는 경우가 많다.</code></pre></br>

<h2 id="5-자바스크립트-파싱과-실행">5. 자바스크립트 파싱과 실행</h2>
<ul>
<li>HTML 문서를 파싱한 결과물로서 생성된 DOM은 HTML 문서의 구조와 정보를 가진다.</li>
<li>또한, HTML 요소와 스타일 등을 변경할 수 있는 프로그래밍 인터페이스인 <code>DOM API</code>를 제공한다.</li>
<li>자바스크립트 코드는 <code>DOM API</code>를 사용하여 <code>생성된 DOM</code>을 동적으로 <code>조작</code>할 수 있다.</li>
</ul>
<h3 id="✅-브라우저-렌더링-엔진의-js-실행">✅ 브라우저 렌더링 엔진의 JS 실행</h3>
<ol>
<li>브라우저 렌더링 엔진은 DOM을 생성해나간다.</li>
<li>생성 중 JS 파일을 로드하는 script 태그를 만나면 DOM 생성을 일시 중지 한다.</li>
<li>script 태그 src 속성의 JS 파일을 서버에 요청한다.</li>
<li>응답된 JS 코드를 파싱하기 위해 <code>자바스크립트 엔진</code>에 제어권을 넘겨준다.</li>
<li>JS 코드 파싱과 실행이 종료되면 <code>브라우저 렌더링 엔진</code>으로 제어권이 돌아온다.</li>
<li>중단된 DOM 생성을 이어나간다.</li>
</ol>
<blockquote>
<p>자바스크립트 파싱과 실행은 브라우저 렌더링 엔진이 아닌 자바스크립트 엔진이 처리한다.</p>
</blockquote>
<h4 id="자바스크립트-엔진">자바스크립트 엔진</h4>
<ul>
<li>자바스크립트 코드를 파싱하여 CPU가 이해할 수 있는 저수준 언어로 변환하고 실행하는 역할을 한다.</li>
<li>자바스크립트 엔진으로는 구글 크롬, Node.js의 <code>V8</code>, 파이어폭스의 <code>SpiderMonkey</code>, 사파리의 <code>JavaScriptCore</code>등이 있으며 모두 ECMAscript 사양을 준수한다.</li>
<li>자바스크립트 엔진은 자바스크립트를 해석하여 <code>AST(추상적 구문 트리)</code>를 생성한다.</li>
<li>AST를 기반으로 <code>인터프리터</code>가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다.</li>
</ul>
<h4 id="인터프리터">인터프리터</h4>
<p>인터프리터는 프로그래밍 언어의 실행 모델 중 하나로, 소스 코드나 중간 코드를 한 줄씩 읽으면서 <code>즉시 실행</code>하는 방식의 프로그램 또는 환경을 의미한다. 이는 컴파일러와 대비되는 개념으로, 컴파일러는 전체 소스 코드를 미리 기계어로 번역한 후 실행하는 반면, 인터프리터는 코드를 미리 번역하지 않고 <code>실행 시점</code>에 <code>해석</code>하며 실행한다.</p>
<blockquote>
<p>소스 코드의 해석과 실행 과정을 최적화하고 효율화하는 데 도움을 준다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/387f24a1-1be0-4169-ae26-a33d09291f7c/image.png" alt=""></p>
<h4 id="토크나이징">토크나이징</h4>
<ul>
<li>단순한 문자열인 자바스크립트 소스코드를 <code>어휘분석</code>하여 문법적 의미를 갖는 최소 단위인 토큰으로 분해한다.</li>
</ul>
<h4 id="파싱">파싱</h4>
<ul>
<li>토큰들의 집합을 <code>구문분석</code>하여 AST를 생성한다.</li>
<li>AST는 토큰에 문법적 의미와 구조를 반영한 트리 구조의 자료구조다.</li>
</ul>
<h4 id="바이트코드-생성과-실행">바이트코드 생성과 실행</h4>
<ul>
<li>파싱의 결과물인 AST는 인터프리터가 실행할 수 있는 중간 코드인 바이트코드로 반환되고 실행된다.</li>
<li>V8 엔진의 경우 자주 사용되는 코드는 터보팬이라 불리는 컴퍼일러에 의해 최적화된 머신 코드로 컴파일되어 성능을 최적화 한다.</li>
</ul>
</br>

<h2 id="6-자바스크립트-파싱에-의한-html-파싱-중단">6. 자바스크립트 파싱에 의한 HTML 파싱 중단</h2>
<ul>
<li>브라우저 렌더링 엔진과 자바스크립트 엔진은 병렬이 아닌 직렬적으로 파싱을 수행한다.</li>
<li>즉, 브라우저는 동기적으로 위에서 아래 순서대로 HTML, CSS, JS를 파싱하고 실행한다.</li>
<li>때문에, link와 script 태그의 위치에 영향을 받는다.</li>
<li><strong>특히 DOM API를 사용하는 JS코드는 DOM이 생성되기전에 실행되면 문제가를 발생시킨다.</strong></li>
</ul>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot; /&gt;
    &lt;script&gt;
      /*
      DOM API인 document.getElementById는 DOM에서 id가 &#39;apple&#39;인 HTML 요소를
      취득한다. 아래 DOM API가 실행되는 시점에는 아직 id가 &#39;apple&#39;인 HTML 요소를 파싱하지
      않았기 때문에 DOM에는 id가 &#39;apple&#39;인 HTML 요소가 포함되어 있지 않다.
      따라서 아래 코드는 정상적으로 id가 &#39;apple&#39;인 HTML 요소를 취득하지 못한다.
      */
      const $apple = document.getElementById(&quot;apple&quot;);

      // id가 &#39;apple&#39;인 HTML 요소의 css color 프로퍼티 값을 변경한다.
      // 이때 DOM에는 id가 &#39;apple&#39;인 HTML 요소가 포함되어 있지 않기 때문에 에러가 발생한다.
      $apple.style.color = &quot;red&quot;; // TypeError: Cannot read property &#39;style&#39; of null
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;ul&gt;
      &lt;li id=&quot;apple&quot;&gt;Apple&lt;/li&gt;
      &lt;li id=&quot;banana&quot;&gt;Banana&lt;/li&gt;
      &lt;li id=&quot;orange&quot;&gt;Orange&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<blockquote>
<p>script(JS 코드)를 body 요소 가장 아래에 두는 이유다.</p>
</blockquote>
</br>

<h2 id="7-script-태그의-asyncdefer-어트리뷰트">7. script 태그의 async/defer 어트리뷰트</h2>
<p>자바스크립트 파싱에 의한 DOM 생성 중단 문제를 해결하기 위해 HTML5부터 script 태그에 <code>async</code>와 <code>defer</code> 속성이 추가되었다.</p>
<pre><code class="language-js">&lt;script async src=&quot;extern.js&quot;&gt;&lt;/script&gt;
&lt;script defer src=&quot;extern.js&quot;&gt;&lt;/script&gt;</code></pre>
<p><code>async</code>와 <code>defer</code> 속성을 사용하면 HTML 파싱과 외부 자바스크립트 파일의 <strong>로드</strong>가 <code>비동기적</code>으로 동시에 진행된다.</p>
<blockquote>
<p>자바스크립트 파일의 <code>로드</code>가 동시에 진행된다는 점을 주의해야 한다.</p>
</blockquote>
<h4 id="async">async</h4>
<ul>
<li>HTML 파싱과 외부 자바스크립트 파일의 로드가 동시에 진행된다.</li>
<li>자바스크립트 <code>파싱</code>과 <code>실행</code>은 <code>자바스크립트 파일 로드</code>가 완료된 <code>직후</code> 진행되며, HTML 파싱은 중단된다.</li>
<li>여러개의 script가 존재한다면 <strong>로드가 완료된 자바스크립트</strong>부터 실행된다.</li>
<li>즉, 실행 순서를 보장되지 않는다.</li>
</ul>
<h4 id="defer">defer</h4>
<ul>
<li>HTML 파싱과 외부 자바스크립트 파일의 로드가 동시에 진행된다.</li>
<li>자바스크립트의 <code>파싱</code>과 <code>실행</code>은 <code>HTML 파싱이 완료된 직후</code>, DOM 생성 직후 진행된다.</li>
<li>DOM 생성이 완료된 이후 실행되어야 할 자바스크립트에 유용하다.</li>
</ul>
<img src="https://velog.velcdn.com/images/sarang_daddy/post/efef4ed4-214d-48a5-a0ef-c7a0908f3bbb/image.png" width="500px">

<blockquote>
<p>어떤 방식이 더 좋다는 구별은 없다.<br>상황에 따라 사용하며 보통 DOM 전체가 필요한 스크립트나 실행순서가 중요하면 defer.<br>방문자 수 카운터나 광고 관련 스크립트 같이 독집적인 스크립트 혹은 순서가 중요하지 않으면 async를 사용한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 불필요한 Effect 제거하기 part.2]]></title>
            <link>https://velog.io/@sarang_daddy/React-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-Effect-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-part.2</link>
            <guid>https://velog.io/@sarang_daddy/React-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-Effect-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-part.2</guid>
            <pubDate>Fri, 25 Aug 2023 04:36:14 GMT</pubDate>
            <description><![CDATA[<p>Effect의 <code>의존성 배열</code>에 <code>객체</code>를 사용하면 생기는 문제점을 이해하고 내 코드를 수정해보자. </p>
<ul>
<li>참고자료 <a href="https://react-ko.dev/learn/removing-effect-dependencies">리액트 공식문서</a><h2 id="의존성-배열에-객체-배열-사용하면-생기는-문제점">의존성 배열에 <code>객체</code>, <code>배열</code> 사용하면 생기는 문제점</h2>
</li>
</ul>
<p>반응형 값으로 <code>객체</code>, <code>배열</code>을 사용한 경우 <strong>변경된 값이 없어도 Effect가 실행될 수 있다.</strong></p>
<pre><code class="language-jsx">import { useState, useEffect } from &quot;react&quot;;
import { createConnection } from &quot;./chat.js&quot;;

const serverUrl = &quot;https://localhost:1234&quot;;

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState(&quot;&quot;);

  const options = {
    serverUrl: serverUrl,
    roomId: roomId,
  };

  useEffect(() =&gt; {
    const connection = createConnection(options);
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [options]);

  return (
    &lt;&gt;
      &lt;h1&gt;Welcome to the {roomId} room!&lt;/h1&gt;
      &lt;input value={message} onChange={(e) =&gt; setMessage(e.target.value)} /&gt;
    &lt;/&gt;
  );
}

export default function App() {
  const [roomId, setRoomId] = useState(&quot;general&quot;);
  return (
    &lt;&gt;
      &lt;label&gt;
        Choose the chat room:{&quot; &quot;}
        &lt;select value={roomId} onChange={(e) =&gt; setRoomId(e.target.value)}&gt;
          &lt;option value=&quot;general&quot;&gt;general&lt;/option&gt;
          &lt;option value=&quot;travel&quot;&gt;travel&lt;/option&gt;
          &lt;option value=&quot;music&quot;&gt;music&lt;/option&gt;
        &lt;/select&gt;
      &lt;/label&gt;
      &lt;hr /&gt;
      &lt;ChatRoom roomId={roomId} /&gt;
    &lt;/&gt;
  );
}</code></pre>
<ul>
<li>위 예제에서 roomId가 변경되면 options가 변경되고 Effect가 일어난다 (연결).</li>
<li>문제는 의존성 배열로 options 객체가 사용되었다.</li>
<li>이 예제는 <strong>setMessage로 메시지만 변경되어도 Effect가 실행된다.</strong></li>
<li>사용자의 의도와는 다른 불필요한 Effect가 발생하게 된다.</li>
</ul>
<h2 id="문제원인">문제원인</h2>
<blockquote>
<p>자바스크립트의 객체는 평가될 때마다 새로운 객체를 생성한다.</p>
</blockquote>
<p>즉, 두 객체가 동일한 값을 가지로 있더라도 다른 메모리제 저장된 별개의 객체다.</p>
<pre><code class="language-js">var person1 = {
  name: &#39;Lee&#39;
};

var person2 = {
  name: &#39;Lee&#39;
};

console.log(person1 === person2); // false</code></pre>
<ul>
<li>때문에 roomId가 변경되지 않아도 자바스크립트는 새로운 객체(options)로 판단하고 Effect를 실행하기 때문에 생기는 문제다.</li>
</ul>
<h2 id="해결방법--객체에서-원시-값-읽기">해결방법 : 객체에서 원시 값 읽기</h2>
<blockquote>
<p>별개의 객체라도 프로퍼티가 가리키는 <code>원시값</code>을 비교 평가 할 수 있다.</p>
</blockquote>
<pre><code class="language-js">// 프로퍼티 값을 참조하는 person1.name과 person2.name은 값으로 평가될 수 있는 표현식이다.
// 두 표현식 모두 원시 값 &#39;Lee&#39;로 평가된다.
console.log(person1.name === person2.name); // true</code></pre>
<p>Effect 외부의 객체에서 비교할 원시값을 읽고 동일한 <code>값</code>을 가진 객체인지 판단할 수 있다.</p>
<pre><code class="language-jsx">function ChatRoom({ options }) {
  const [message, setMessage] = useState(&#39;&#39;);

  const { roomId, serverUrl } = options;
  useEffect(() =&gt; {
    const connection = createConnection({
      roomId: roomId,
      serverUrl: serverUrl
    });
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [roomId, serverUrl]); // ✅ All dependencies declared
  // ...</code></pre>
<ul>
<li>Effect 외부의 객체에서 정보를 읽어서 객체 의존성을 피한다.</li>
<li>Effect 외부의 객체에서 비교 대상값을 읽고 Effect 내부에 동일한 값을 가진 객체를 만든다.</li>
<li><strong>이 경우 Effect가 어떤 정보에 의존하는지 명확하게 알 수 있다.</strong></li>
</ul>
<h2 id="내-코드에-적용하기">내 코드에 적용하기</h2>
<h3 id="잘못된-사용">잘못된 사용</h3>
<pre><code class="language-jsx">  useEffect(() =&gt; {
    const postObjectToStore = { ...postObject };
    localStorage.setItem(&#39;postObject&#39;, JSON.stringify(postObjectToStore));
    console.log(&#39;변경사항 렌더링 체크&#39;);
  }, [postObject]);</code></pre>
<ul>
<li>postObject 객체를 로컬스토리지에 저장하는데 useEffect를 사용</li>
<li>로컬스토리지 저장에는 문제가 없지만, 의존성 배열에 postObject를 사용</li>
</ul>
<blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/639b6f6e-c053-44bf-8ef7-7963abc2413a/image.gif" alt=""></p>
<ul>
<li><code>변경내용이 없는</code> postObject의 경우에도 Effect가 실행되고 있다.</li>
<li>postObject가 아닌 변경에도 Effect가 실행될 수 있다.</li>
</ul>
</blockquote>
<h3 id="수정-내용">수정 내용</h3>
<pre><code class="language-jsx">  const { title, price, content, categoryId, locationId, files } = postObject;

  useEffect(() =&gt; {
    const postObjectToStore = {
      title,
      price,
      content,
      categoryId,
      locationId,
      files,
    };
    localStorage.setItem(&#39;postObject&#39;, JSON.stringify(postObjectToStore));
    console.log(&#39;렌더링 체크&#39;);
  }, [title, price, content, categoryId, locationId, files]);</code></pre>
<ul>
<li>의존성 배열에서 객체를 제거한다.</li>
<li>Effect 외부에서 <code>객체의 원시값</code>을 읽어준다.</li>
<li>Effect 의존성 배열에서 <code>원시값</code>을 비교한다.</li>
</ul>
<blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/f314ec70-b318-459f-ba7c-c41f09cd2a1a/image.gif" alt=""></p>
</blockquote>
<ul>
<li>동일한 값을 가진 객체인 경우 Effect가 일어나지 않는다.</li>
</ul>
<h2 id="느낀점">느낀점</h2>
<p>자바스크립트 객체의 특성을 알고 있고 자바스크립트로 구현할때는 조심하려고 신경 썻던 부분을 리액트에서는 안일하게 생각하고 있었던 것 같다. 그래서 더욱 글로 남겨두고 싶었다. 리액트는 자바스크립트의 특징과 문법을 활용한 점을 잊지말자.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React - 불필요한 Effect 제거하기 part.1]]></title>
            <link>https://velog.io/@sarang_daddy/React-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-Effect-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-part.1</link>
            <guid>https://velog.io/@sarang_daddy/React-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-Effect-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-part.1</guid>
            <pubDate>Fri, 18 Aug 2023 16:55:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/sarangdaddy/FE-study/blob/main/React_OfficialDocument/06_%ED%83%88%EC%B6%9C%EA%B5%AC.md">리액트 공식문서를 학습</a>하면서 나는 너무나 잘못된 Effect를 사용하고 있음을 알게 되었다.</p>
<p>렌더링 후 이미 존재하는 데이터가 있다면 값으로 불러오기 위해 Effect를 사용하고 있었다. </p>
<h2 id="잘못된-사용">잘못된 사용</h2>
<ul>
<li>inputForm (부모 - postObject 상태 관리)<ul>
<li>title (props : postObject)</li>
<li>price (props : postObject)</li>
<li>content (props : postObject)</li>
<li>categoryId (props : postObject)</li>
<li>locationId (props : postObject)</li>
<li>files (props : postObject)</li>
</ul>
</li>
</ul>
<ul>
<li>자식들에서 입력된 값들이 부모 postObject에 모인다.</li>
<li>부모에서 postObject를 POST 요청을 보낸다.</li>
<li>여기서 사용자 데이터를 보존하기 위해 localStorage를 사용</li>
<li>POST 요청 전에 postObject 내용을 localStorage에 저장</li>
<li>사용자가 POST 요청 없이 페이지를 이동</li>
<li>다시 POST 요청 페이지로 돌아왔을 로컬스토리지에 보존된 데이터를 초기값으로 할당</li>
<li>각 자식 컴포넌트들에서는 초기 렌더링 이후 상태값이 존재한다면 다시 렌더링 되도록 Effect 사용</li>
</ul>
<pre><code class="language-jsx">const { postObject, setPostObject } = useContext(postSalesItemContext);
const [inputComment, setInputComment] = useState&lt;string | null&gt;(null);

  useEffect(() =&gt; {
    if (postObject.content) {
      const storedContent = postObject.content;
      setInputComment(storedContent);
    }
  }, []);</code></pre>
<ul>
<li>사용자 입력으로 postObject가 변경되면 부모 컴포넌트로 부터 변경된 props가 전달되면서 렌더링이 일어난다. (1번)</li>
<li>렌더링 후 useEffect가 호출되면서 렌더링이 일어난다. (2번)</li>
<li>보존 데이터를 업데이트 하기 위한 용도로 useEffect를 사용한 문제다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/c642219d-3506-4f12-af35-05adfe386e43/image.png" alt=""></p>
<blockquote>
<p>잘못된 Effect 사용으로 불필요한 렌더링이 한 번 더 일어나고 있다.</p>
</blockquote>
<h2 id="수정-내용">수정 내용</h2>
<h3 id="렌더링을-위해-데이터를-변환하는-경우-effect는-필요하지-않다">렌더링을 위해 데이터를 변환하는 경우 Effect는 필요하지 않다.</h3>
<ul>
<li>부모 컴포넌트에서 localStorage 값을 postObject 초기값으로 가져온다.</li>
</ul>
<pre><code class="language-jsx">// 페이지 이동 후 localStorage 값이 존재한다면 가져온다.
  const [postObject, setPostObject] = useState&lt;PostObjectType&gt;(
    storedPostObject ? JSON.parse(storedPostObject) : initialPostObject,
  );</code></pre>
<ul>
<li>부모 컴포넌트로부터 전달 받은 props를 자식 컴포넌트 초기값으로 설정한다.</li>
</ul>
<pre><code class="language-jsx">  const [inputComment, setInputComment] = useState&lt;string | null&gt;(
    postObject.content ? postObject.content : null,
  );</code></pre>
<ul>
<li>자식 컴포넌트의 모든 Effect를 제거.</li>
</ul>
<blockquote>
<p>부모 컴포넌트에서 localStorage에 값을 보존하기 위한 Effect 하나를 제외하고 모든 Effect를 제거 할 수 있었다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/c1fdfb11-2259-4734-8781-619e98022d01/image.png" alt=""></p>
<blockquote>
<p>두 번씩 일어나던 렌더링도 해결되었다.</p>
</blockquote>
<p>수정 내용을 간단한 측정이지만 테스트해보니 사용자가 체감하기에는 미비한 수치지만 개선되었음을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sarang_daddy/post/56e2e929-767a-4716-81a4-a2ca1eaa7be4/image.png" alt=""></p>
<h2 id="느낀점">느낀점</h2>
<p>불필요한 useEffect의 사용은 성능 문제, 유지 보수의 어려움, 가독성 저하 등 웹 프로그래밍에 치명적임을 알게 되었다. 특히 스스로가 너무 무지성으로 Effect를 사용하고 있었음에 충격을 받았다. 리액트 공식문서의 Effect 파트를 반복해서 읽고, 불필요한 Effect를 사용하고 있는 것은 아닐까? 라는 고민을 항상 하도록 하자. </p>
]]></description>
        </item>
    </channel>
</rss>