<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cloud_oort.log</title>
        <link>https://velog.io/</link>
        <description> 최선을 다한다는 것은 할 수 있는 한 가장 핵심을 향한다는 것</description>
        <lastBuildDate>Sun, 23 Mar 2025 07:26:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cloud_oort.log</title>
            <url>https://velog.velcdn.com/images/cloud_oort/profile/8805e04b-874c-43fa-9225-ee1635515fec/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cloud_oort.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cloud_oort" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Web Worker 사용해 이미지 압축 및 포멧 변환 최적화]]></title>
            <link>https://velog.io/@cloud_oort/Web-Worker-%EC%82%AC%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%95%95%EC%B6%95-%EB%B0%8F-%ED%8F%AC%EB%A9%A7-%EB%B3%80%ED%99%98-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@cloud_oort/Web-Worker-%EC%82%AC%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%95%95%EC%B6%95-%EB%B0%8F-%ED%8F%AC%EB%A9%A7-%EB%B3%80%ED%99%98-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 23 Mar 2025 07:26:45 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>사용자가 이미지를 업로드하면 preview 이미지로 drag and drop할 수 있다.
최대 5장, 10mb 이하로 제한했지만 버범임이 발생하는 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/cloud_oort/post/325121c1-e791-4b5f-b42b-96028e9063f0/image.gif" alt=""></p>
<h2 id="최적화-이전">최적화 이전</h2>
<p>처음에는 Canvas API 활용해서 이미지 압축하고, jpeg 또는 png 이미지를 webp로 변환했다. </p>
<p>하지만 해당 작업을 메인 스레드에서 작업하다보니, 속도가 느렸다.</p>
<p>웹 브라우저에서 메인 스레드는 UI 렌더링, 이벤트 처리, DOM 조작 등의 작업을 담당한다. Canvas API를 활용한 이미지 압축은 데이터를 변환하는 과정에서 CPU 사용량이 증가한다. 특히 고해상도 이미지를 처리할 경우 연산 부담이 커져 UI가 멈추거나 렌더링 지연이 발생할 수 있다.</p>
<h2 id="최적화-이후">최적화 이후</h2>
<p>Web Worker를 사용해 이미지 압축(메인 스레드에서 실행되는 연산)을 백그라운드에서 병렬 처리함으로써 최적화할 수 있다.</p>
<p>Web Worker는 스크립트 연산을 웹 메인 스레드와 분리된 별도의 백그라운드 스레드에서 실행할 수 있는 기술이다.</p>
<h3 id="web-worker-종류">Web Worker 종류</h3>
<h4 id="dedicated-worker">Dedicated Worker</h4>
<ul>
<li>메인 스레드와 독립적으로 실행.</li>
<li>특정 페이지(스코프 내)에서만 사용 가능.</li>
<li>postMessage로 메인 스레드와 통신.</li>
<li>이미지 압축, 데이터 처리, CPU 집약적 작업.<h4 id="shared-worker">Shared Worker</h4>
</li>
<li>여러 브라우저 탭에서 공유 가능.</li>
<li>같은 도메인 내에서만 사용 가능.</li>
<li>MessagePort로 통신.</li>
<li>여러 탭에서 동일한 상태 유지.<h4 id="service-worker">Service Worker</h4>
</li>
<li>백그라운드에서 실행되며 네트워크 요청 가로챌 수 있음.</li>
<li>브라우저 종료 후에도 동작 가능.</li>
<li>HTTPS 환경에서만 작동</li>
<li>캐싱, 오프라인 지원, API 요청 가로채기 (MSW)<h3 id="dedicated-worker-특징">Dedicated Worker 특징</h3>
</li>
<li>별도의 스레드에서 동작하며 메인 스레드와 독립적으로 실행된다.</li>
<li>비동기적으로 동작하여 메인 스레드가 작업 완료를 기다리지 않아도 된다.</li>
<li>DOM에 직접 접근할 수 없다. 대신 메인 스레드에서 DOM 작업을 처리해야 한다.</li>
<li>Web Worker와 메인 스레드는 postMessage와 onmessage 이벤트 핸들러를 통해 데이터를 교환한다.<h3 id="과정">과정</h3>
<h4 id="포멧-변환">포멧 변환</h4>
사용자로부터 JPEG, PNG, WebP 등 다양한 포멧을 가진 이미지를 받는데 이를 압축 효율이 높은 WebP로 변환한다. </li>
</ul>
<p>WebP는 2010년에 구글이 개발한 이미지 포멧으로 손실, 무손실 압축과 투명도를 모두 지원하고,  GIF와 동일하게 애니메이션을 지원한다. 이미지 내에서 인접한 픽셀들은 대개 유사한 색상을 가지는 경향이 있는데, WebP는 이러한 상관관계를 활용하여 픽셀 색상을 인접 픽셀 색상에 기반하여 예측할 수 있는 효율적인 예측 알고리즘이 있다.</p>
<p>WebP는 2024년 12월 기준 전체 사용자의 약 97퍼를 지원하고 있고, 아직은 문제가 없기 때문에 호환성 측면에서 현재 프로젝트에서는 별다른 조치를 하고 있지 않다. </p>
<p>하지만 WebP를 지원하지 않는 브라우저를 사용하는 사용자가 있을 경우, 사용자로부터 원본 이미지를 받고 CDN에 이미지 요청할 때 유저의 user-agent 확인하여 webp 지원하면 변환하고 아니면 원본이미지를 보내는 방법으로 바꿔 문제를 해결하고자 한다.</p>
<h4 id="worker-script-작성">worker script 작성</h4>
<ul>
<li><code>/// &lt;reference lib=&quot;webworker&quot; /&gt;</code> 지시자(Triple-Slash Directive) 사용<ul>
<li>타입스크립트 환경에서 Web Worker 파일 작성할 때 사용한다.</li>
<li>기본적으로 타입스크립트는 브라우저 환경에서 사용되는 DOM API 타입(<code>window</code>, <code>document</code> 등)을 참조한다. 하지만 Web Worker는 DOM이 없고, Worker 전용 API만 사용 가능하기 때문에, TypeScript가 이를 정확히 이해하려면 Worker 전용 타입 선언이 필요하다.</li>
</ul>
</li>
<li>OffscreenCanvas<ul>
<li>Web Worker에서는 document, window와 같은 DOM API에 직접 접근할 수 없다.</li>
<li>OffscreenCanvas는 이러한 제약을 해결하기 위해 도입된 API로, 메인 스레드가 아닌 Worker 스레드에서 Canvas 렌더링을 가능하게 한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-ts">// imageCompressionWorker.ts

/// &lt;reference lib=&quot;webworker&quot; /&gt;

import { toast } from &#39;sonner&#39;;

interface WorkerMessage {
  fileData: ArrayBuffer;
  fileName: string;
}

self.onmessage = async (e: MessageEvent&lt;WorkerMessage&gt;) =&gt; {
  const { fileData, fileName } = e.data;

  try {
    const blob = new Blob([fileData]);
    const imageBitmap = await createImageBitmap(blob);

    const maxWidth = imageBitmap.width &gt; 750 ? 750 : imageBitmap.width;
    const scale = maxWidth / imageBitmap.width;
    const targetWidth = maxWidth;
    const targetHeight = imageBitmap.height * scale;

    const offscreen = new OffscreenCanvas(targetWidth, targetHeight);
    const ctx = offscreen.getContext(&#39;2d&#39;);

    if (!ctx) {
      toast.error(&#39;이미지 업로드에 실패했습니다.&#39;);
      return;
    }

    ctx.drawImage(imageBitmap, 0, 0, targetWidth, targetHeight);

    // quality 설정
    const quality = 0.9;
    const webpBlob = await offscreen.convertToBlob({
      type: &#39;image/webp&#39;,
      quality
    });

    // 변환된 Blob을 소유권 이전 형태로 메인 스레드로 전송
    const arrayBuffer = await webpBlob.arrayBuffer();
    self.postMessage({ arrayBuffer, fileName }, [arrayBuffer]);
  } catch (error) {
    // 에러 발생 시 메인 스레드에 알림
    self.postMessage({ error: (error as Error).message });
  }
};
</code></pre>
<h4 id="이미지-압축-및-포멧-변환-함수-작성">이미지 압축 및 포멧 변환 함수 작성</h4>
<ul>
<li>promise를 리턴해서 여러 개의 이미지를 병렬 처리하도록 한다.</li>
<li>ArrayBuffer 사용<ul>
<li>메인 스레드와 Worker는 별개의 실행 환경을 갖는다. 두 환경은 서로 전역 변수를 공유하거나 DOM 객체를 직접 주고받을 수 없으며, <code>postMessage</code>를 통해서만 통신한다. <code>postMessage</code>를 통해 객체를 Worker로 보내면 브라우저는 <code>Structured Clone</code>을 사용해 객체를 복제한다. <ul>
<li>단순 숫자나 문자열, JSON으로 표현 가능한 객체는 쉽게 복제되지만, 대용량 바이너리 데이터(<code>File</code>, <code>Blob</code>, <code>ArrayBuffer</code>)를 다룰 때는 효율적인 전송 방법이 필요하다.</li>
<li>File 객체는 브라우저에서 입력받은 파일이며, 메타데이터(파일명, 수정 시간, 타입)와 실제 바이너리 데이터가 함께 담겨 있다. 이를 Worker에 바로 넘길 수도 있지만, Worker 쪽으로 복사(copy)되는 형태로 전달되어 대용량 파일일수록 복사 과정이 비용이 많이 들 수 있다.</li>
<li>ArrayBuffer 객체는 순수 바이너리 데이터를 담는 컨테이너이다. 어떤 파일이든 <code>file.arrayBuffer()</code>를 통해 순수한 이진 데이터로 읽어낼 수 있다. 이 <code>ArrayBuffer</code>를 <code>postMessage</code>에 전달할 때, 두 번째 인자로 <code>ArrayBuffer</code>를 Transferable로 지정하면, 복사가 아닌 소유권 이전(transfer)이 일어난다.</li>
<li>소유권 이전이란 메모리를 복사하지 않고, 메인 스레드에 있던 <code>ArrayBuffer</code>의 소유권을 Worker에게 넘겨버리는 것이다. 이로써 메인 스레드 쪽 <code>arrayBuffer</code>는 더 이상 유효하지 않고(길이 0짜리로 보이거나 사용 불가), Worker 측에서 해당 바이너리 데이터를 바로 사용할 수 있게 된다.    </li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-ts">// compressAndConvertToWebP.ts
import { toast } from &#39;sonner&#39;;

export const compressAndConvertToWebP = async (file: File): Promise&lt;File&gt; =&gt; {
  try {
    const fileArrayBuffer = await file.arrayBuffer();

    const worker = new Worker(
      new URL(&#39;../lib/imageCompressionWorker.ts&#39;, import.meta.url),
      { type: &#39;module&#39; }
    );

    return new Promise&lt;File&gt;((resolve, reject) =&gt; {
      worker.onmessage = (e: MessageEvent) =&gt; {
        const { arrayBuffer, fileName } = e.data;

        const blob = new Blob([arrayBuffer], { type: &#39;image/webp&#39; });
        const webpFile = new File([blob], fileName, { type: &#39;image/webp&#39; });
        resolve(webpFile);
        worker.terminate();
      };

      worker.onerror = (err) =&gt; {
        toast.error(&#39;이미지 압축 worker error!&#39;);
        reject(err);
        worker.terminate();
      };

      // 소유권 이전 형태로 work로 전송.
      worker.postMessage({ fileData: fileArrayBuffer, fileName: file.name }, [
        fileArrayBuffer
      ]);
    });
  } catch {
    toast.error(&#39;이미지를 읽지 못했습니다.&#39;);
    throw new Error(&#39;이미지를 읽지 못했습니다.&#39;);
  }
};</code></pre>
<h4 id="이미지-병렬-처리">이미지 병렬 처리</h4>
<p>이미지 압축 및 포멧 변환 작업을 병렬 처리함으로써 처리 속도를 유의미하게 개선할 수 있다.</p>
<pre><code class="language-ts">const addImages = async (newFiles: File[]) =&gt; {
    const compressedAllFiles = await Promise.all(
      newFiles.map(async (file) =&gt; {
        const compressed = await compressAndConvertToWebP(file);
        return compressed;
      })
    );
    setState(compressedAllFiles);
  };</code></pre>
<h2 id="결과">결과</h2>
<h3 id="성능-개선">성능 개선</h3>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/28c743cc-bcec-42f9-8c53-8dcbdecf6b0a/image.jpg" alt=""></p>
<h3 id="버벅임-제거">버벅임 제거</h3>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/9f3fb73c-3e33-471d-8a1e-10cfb28edbf1/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SVG Sprite로 네트워크 요청 절반 줄이기]]></title>
            <link>https://velog.io/@cloud_oort/SVG-sprite-%EC%83%9D%EC%84%B1-%ED%86%B5%ED%95%B4-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@cloud_oort/SVG-sprite-%EC%83%9D%EC%84%B1-%ED%86%B5%ED%95%B4-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sat, 22 Mar 2025 06:31:34 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-사용-방식-및-네트워크-요청">기존 사용 방식 및 네트워크 요청</h2>
<p>JSX에 SVG 파일을 직접 import해서 사용하면 편리하지만 JS 번들 크기 늘어나고 불필요한 네트워크 요청 증가한다.</p>
<pre><code class="language-tsx">import NotFoundIcon from &#39;@/assets/icons/404_sign.svg&#39;;

&lt;img src={NotFoundIcon} alt=&quot;NotFoundLogo&quot; className=&quot;size-40&quot; /&gt;</code></pre>
<p>아래는 랜딩 페이지의 네트워크 탭인데, 네트워크 요청의 절반이 아이콘 요청이다.
<img src="https://velog.velcdn.com/images/cloud_oort/post/482698ec-db1b-4cf3-880e-cd20a3674bac/image.png" alt=""></p>
<h2 id="svg-sprite">SVG Sprite</h2>
<p>Sprite는 그래픽 리소스를 한 파일 안에 모아서 필요한 부분만 뽑아 쓰는 기법을 가리킨다.
SVG Sprite 파일을 생성하면 SVG 파일을 한 파일에 모아서 필요한 부분만 뽑아서 사용할 수 있다.</p>
<h3 id="이점">이점</h3>
<ul>
<li>아이콘 HTTP 요청 줄임으로써 페이지 로딩 시간 단축할 수 있다.</li>
<li>새로운 아이콘 요소 쉽게 추가할 수 있다.</li>
<li>SVG sprite 파일을 따로 생성하기 때문에 JS 번들 크기 줄어든다.</li>
</ul>
<h3 id="생성-및-사용-방법">생성 및 사용 방법</h3>
<ol>
<li><code>&lt;svg /&gt;</code> 태그를 <code>&lt;symbol /&gt;</code> 로 바꾼 svg 콘텐츠를 모아 하나의<code>&lt;svg /&gt;</code>로 래핑한다.</li>
<li>symbol 태그의 id로 아이콘을 구분한다.</li>
<li><code>&lt;svg /&gt;</code>태그로 감싼 <code>&lt;use /&gt;</code> 태그의 href 속성에서 id를 사용하여 아이콘 사용한다.</li>
</ol>
<p>이를 통해 아이콘이 아무리 많아도 스프라이트 파일 한 번만 요청하면 되고, 이후엔 <code>&lt;use /&gt;</code>를 통해 각 아이콘을 재사용할 수 있어 성능과 관리 효율이 좋아진다.</p>
<h3 id="자동화-작업">자동화 작업</h3>
<h4 id="스크립트-작성">스크립트 작성</h4>
<p>프로젝트에서 사용하는 SVG 파일들을 모아 단일 SVG Sprite로 결합하는 스크립트를 작성한다.</p>
<pre><code class="language-ts">// generateSprite.ts
import fs from &#39;fs&#39;;
import { globSync } from &#39;glob&#39;;
import { HTMLElement, parse } from &#39;node-html-parser&#39;;
import path from &#39;path&#39;;

// 프로젝트 내에 있는 모든 SVG 파일 모은다.
const svgFiles = globSync(&#39;src/shared/assets/icons/*.svg&#39;);
const symbols: string[] = [];

// 각 SVG 파일을 읽고 svg 태그를 symbol 태그로 바꾸고 필요한 속성만 취한다.
svgFiles.forEach((file) =&gt; {
  const code = fs.readFileSync(file, &#39;utf-8&#39;);
  const svgElement = parse(code).querySelector(&#39;svg&#39;) as HTMLElement;
  const symbolElement = parse(`&lt;symbol/&gt;`).querySelector(
    &#39;symbol&#39;
  ) as HTMLElement;
  const fileName = path.basename(file, &#39;.svg&#39;);

  svgElement.childNodes.forEach((child) =&gt; symbolElement.appendChild(child));

  symbolElement.setAttribute(&#39;id&#39;, fileName);
  if (svgElement.attributes.viewBox) {
    symbolElement.setAttribute(&#39;viewBox&#39;, svgElement.attributes.viewBox);
  }

  symbols.push(symbolElement.toString());
});

// 정적 폴더에 작성한다.
const svgSprite = `&lt;svg&gt;${symbols.join(&#39;&#39;)}&lt;/svg&gt;`;
fs.writeFileSync(&#39;public/sprite.svg&#39;, svgSprite);</code></pre>
<h4 id="빌드-전에-스크립트-실행">빌드 전에 스크립트 실행.</h4>
<pre><code>&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;generateSprite&quot;: &quot;tsx generateSvgSprite.ts&quot;,
    &quot;build&quot;: &quot;npm run generateSprite &amp;&amp; tsc -b &amp;&amp; vite build&quot;,
  },</code></pre><h4 id="사용">사용</h4>
<p>추가적인 import 없이, 아이콘 컴포넌트를 생성해서 사용한다.</p>
<pre><code class="language-tsx">interface IconProps {
  name: string;
  style?: string;
  ariaLabel?: string;
}

export const Icon = ({ name, style, ariaLabel = name }: IconProps) =&gt; {
  return (
    &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; className={style} aria-label={ariaLabel}&gt;
      &lt;use href={`/sprite.svg#${name}`} /&gt;
    &lt;/svg&gt;
  );
}

&lt;Icon name=&#39;404_sign&#39; style=&#39;size-40&#39; ariaLabel=&quot;NotFoundLogo&quot; /&gt;</code></pre>
<h2 id="적용-이후">적용 이후</h2>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/47a220ad-779f-4612-8a9f-29ee1854e6b9/image.jpg" alt=""></p>
<h2 id="출처">출처</h2>
<p><a href="https://hackernoon.com/lang/ko/%EC%95%84%EC%9D%B4%EC%BD%98%EC%9C%BC%EB%A1%9C-svg-%EC%8A%A4%ED%94%84%EB%9D%BC%EC%9D%B4%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95">아이콘으로 SVG 스프라이트를 만드는 방법</a>
<a href="https://velog.io/@adultlee/Svg-sprite-%EA%B8%B0%EB%B2%95%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%82%B4%EA%B0%80-%EC%89%AC%EC%9A%B4-Icon-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B0%9C%EB%B0%9C">SVG Sprite 기법을 사용해 나만의 특별한 Icon 컴포넌트 개발
</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Hook Form과 Zod를 이용한 효율적인 Form 관리]]></title>
            <link>https://velog.io/@cloud_oort/React-Hook-Form%EA%B3%BC-Zod%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Form-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%A0%9C%EC%9E%91-%EB%B0%8F-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@cloud_oort/React-Hook-Form%EA%B3%BC-Zod%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Form-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%A0%9C%EC%9E%91-%EB%B0%8F-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Mon, 25 Nov 2024 04:02:09 GMT</pubDate>
            <description><![CDATA[<h2 id="필요성">필요성</h2>
<p>리액트에서 단순한 form은 쉽게 관리할 수 있지만 대규모 form으로 발전하면 다음과 같은 문제가 발생한다.</p>
<ul>
<li>관리 복잡성 증가: 입력 필드가 추가될 때마다 새로운 상태와 로직이 추가되어 코드가 방대해진다.</li>
<li>유효성 검사 관리 부담: 유효성 검사 로직이 분산되고 복잡하여 유지보수가 어려워진다.</li>
</ul>
<p>이를 해결하기 위해 React Hook Form과 Zod를 활용하면 다음과 같은 장점이 있다.</p>
<ul>
<li>간결한 코드 작성: Hook 기반으로 직관적이고 깔끔한 코드 작성이 가능하다.</li>
<li>유효성 검사 통합: 유효성 검사를 간단히 처리하며, 타입과 데이터 구조 관리를 쉽게 할 수 있다.</li>
</ul>
<h2 id="react-hook-form의-특징">React Hook Form의 특징</h2>
<h3 id="성능-최적화">성능 최적화</h3>
<ul>
<li>비제어 컴포넌트 방식을 기반으로 작동하여 DOM의 참조(ref)를 활용해 불필요한 렌더링을 줄인다.</li>
<li><code>watch</code> 기능으로 실시간 상태 추적이 가능하여 제어 컴포넌트의 장점을 함께 제공한다.</li>
</ul>
<h3 id="간결한-코드">간결한 코드</h3>
<ul>
<li><code>useForm</code> 같은 Hook 기반 API로 직관적이고 간결한 코드 작성이 가능하다.</li>
<li><code>register</code> 를 사용해 필드 값과 유효성 검사를 자동으로 관리한다.<h3 id="유연한-유효성-검사">유연한 유효성 검사</h3>
</li>
<li>HTML5 기본 유효성 검사를 지원한다.</li>
<li>Zod와 같은 Schema 기반 유효성 검사 라이브러리 호환을 지원한다. <a href="https://react-hook-form.com/get-started#SchemaValidation">공식 문서</a></li>
<li>비동기 유효성 검사를 지원한다.<h3 id="외부-라이브러리와의-호환성">외부 라이브러리와의 호환성</h3>
<code>Material-UI</code> 같은 UI 라이브러리나 <code>shadcn/ui</code> 같은 component collection과 쉽게 통합 가능하다. 
<a href="https://react-hook-form.com/get-started#IntegratingwithUIlibraries">공식 문서</a></li>
</ul>
<h3 id="제어-컴포넌트-vs-비제어-컴포넌트">제어 컴포넌트 vs 비제어 컴포넌트</h3>
<h4 id="제어-컴포넌트">제어 컴포넌트</h4>
<p>리액트 내부에서 값이 제어되는 컴포넌트를 의미하며, 리액트의 상태를 통해 입력 필드의 값을 관리하는 방식이다. </p>
<ul>
<li>장점: 상태와 UI가 동기화되며 실시간 유효성 검사가 쉽다.</li>
<li>단점: 상태 관리 로직으로 인해 코드가 복잡해지고 리렌더링으로 인해 성능 문제가 발생할 수 있다.<h4 id="비제어-컴포넌트">비제어 컴포넌트</h4>
DOM이 직접 입력값을 괸리하는 방식으로 <code>useRef</code>를 활용한다.</li>
</ul>
<blockquote>
<p><code>useRef</code>는 heap 영역에 저장되는 일반적인 자바스크립트 객체로, 애플리케이션이 종료되거나 가비지 컬렉팅될 때까지 참조할 때마다 같은 메모리 값을 가진다. 값이 변경되어도 같은 메모리 주소를 가지고 있기 때문에 리액트는 변경사항을 감지할 수 없어 리렌더링하지 않는다. 이를 통해 렌더링 횟수를 줄이고 성능을 최적화할 수 있다.</p>
</blockquote>
<ul>
<li>장점: 코드가 간단하고 리렌더링이 최소화되어 성능적으로 유리하다.</li>
<li>단점: 상태와 유효성 검사를 직접 관리해야 하므로 복잡해질 수 있고 데이터 흐름이 직관적이지 않다.<h2 id="zod-스키마-기반-유효성-검사-라이브러리">Zod: 스키마 기반 유효성 검사 라이브러리</h2>
Zod는 타입스크립트를 우선으로 설계된 스키마(Schema) 선언 및 유효성 검사 라이브러리이다.
데이터 스키마를 선언적으로 정의하여 사용하는데, 여기서 스키마란 데이터의 형태, 데이터의 타입 그리고 데이터가 충족해야 할 조건들을 지정한다.
해당 스키마를 기준으로 Zod는 주어진 데이터를 검증하고 검증에 실패하면 에러를 리턴한다. 이를 통해 데이터의 무결성을 유지하고 예상치 못한 데이터 구조로 발생하는 에러를 방지할 수 있다.<h3 id="특징">특징</h3>
</li>
<li>스키마 정의: 스키마를 통해 Object, Array, String, Number 등의 타입과 그에 따른 조건을 정의한다.</li>
<li>유효성 검사: <code>parse()</code>와 <code>safeParse()</code> 메서드로 데이터를 검증한다.</li>
<li>타입 추론: 타입스크립트 타입을 스키마로부터 자동으로 추론한다.</li>
<li>동적 스키마 구성: 함수형 API로 복잡한 조건부 유효성 검사를 처리할 수 있다.</li>
</ul>
<h2 id="react-hook-form과-zod-연결">React Hook Form과 Zod 연결</h2>
<p><a href="https://www.npmjs.com/package/@hookform/resolvers"><code>@hookform/resolvers</code></a> 라이브러리를 사용하여 React Hook Form과 Zod의 스키마를 연결한다.</p>
<ul>
<li>Zod는 타입스크립트 우선 라이브러리이기 때문에 스키마를 통해 타입과 유효성 검사를 동시에 처리할 수 있다.</li>
<li>스키마에서 작성한 에러 메시지를 React Hook Form의 error 객체에서 바로 활용할 수 있다.</li>
<li>동일한 스키마를 여러 form에서 재활용할 수 있어 코드 중복을 줄이고 유지보수가 용이해진다.</li>
<li>입력 필드의 조건부 유효성 검사나 복잡한 데이터 구조 처리하는데 매우 유연하다.<h2 id="사용-예시">사용 예시</h2>
<h3 id="스키마-선언">스키마 선언</h3>
</li>
<li>데이터 구조와 유효성 검사를 정의한다.</li>
<li><code>superRefine()</code> 메서드를 사용하면 여러 이슈를 추가하여 조건부 검사를 진행할 수 있다.<pre><code class="language-ts">import { z } from &#39;zod&#39;;
</code></pre>
</li>
</ul>
<p>export const RegisterSchema = z.object({
  productName: z.string().superRefine((value, ctx) =&gt; {
    const name = value.replaceAll(&#39; &#39;, &#39;&#39;);
    if (name.length === 0 || name.length &lt; 2) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: &#39;제목은 공백을 제외하고 2자 이상 입력해 주세요.&#39;,
      });
    }
    if (name.length &gt; 30) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: &#39;제목은 최대 30자 이하로 입력해 주세요.&#39;,
      });
    }
  }),
  minPrice: z.string().superRefine((value, ctx) =&gt; {
    const num = Number(value.replace(/[^\d]/g, &#39;&#39;));
    if (num &lt; 1000) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: &#39;최소 1000원 이상 입력해 주세요.&#39;,
      });
    }</p>
<pre><code>if (num &gt; 2_000_000) {
  ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: &#39;2,000,000원 이하로 입력해 주세요.&#39;,
  });
}

if (num % 1000 !== 0) {
  ctx.addIssue({
    code: z.ZodIssueCode.custom,
    message: &#39;1000원 단위로 입력해 주세요.&#39;,
  });
}</code></pre><p>  }),
  description: z
    .string()
    .superRefine((value, ctx) =&gt; {
      const name = value.replaceAll(&#39; &#39;, &#39;&#39;);
      const newLineCount = (value.match(/\n/g) || []).length;
      if (name.length &gt; 1000) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: &#39;상품 설명은 최대 1000자 이하로 입력해 주세요.&#39;,
        });
      }
      if (newLineCount &gt; 10) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: &#39;상품 설명은 줄바꿈을 10개 이하로 입력해 주세요.&#39;,
        });
      }
    })
    .or(z.literal(&#39;&#39;)),
});</p>
<pre><code>### FormField 컴포넌트 구현
재사용가능한 컴포넌트로 분리하여 재사용성을 높인다.
```tsx
import { ReactElement } from &#39;react&#39;;
import { Control, Controller, ControllerRenderProps, FieldValues, Path } from &#39;react-hook-form&#39;;
import { ErrorMessage } from &#39;./ErrorMessage&#39;;

interface FormFieldProps&lt;T extends FieldValues&gt; {
  name: Path&lt;T&gt;;
  control: Control&lt;T&gt;;
  label?: string;
  render: (field: ControllerRenderProps&lt;T&gt;) =&gt; ReactElement;
  error?: string;
}

export const FormField = &lt;T extends FieldValues&gt;({ name, control, label, render, error }: FormFieldProps&lt;T&gt;) =&gt; {
  return (
    &lt;div className=&#39;relative flex flex-col gap-2&#39;&gt;
      &lt;label htmlFor={label} className=&#39;cursor-pointer text-body2 web:text-heading3&#39;&gt;
        {label}
      &lt;/label&gt;
      &lt;Controller name={name} control={control} render={({ field }) =&gt; render(field)} /&gt;
      {error &amp;&amp; &lt;ErrorMessage message={error} /&gt;}
    &lt;/div&gt;
  );
};</code></pre><h3 id="form-컴포넌트-구현">Form 컴포넌트 구현</h3>
<p>React Hook Form과 Zod를 통합해 Form을 작성한다.</p>
<pre><code class="language-tsx">import { FormField } from &quot;@/shared&quot;;
import { zodResolver } from &quot;@hookform/resolvers/zod&quot;;
import { SubmitHandler, useForm } from &quot;react-hook-form&quot;;
import { z } from &quot;zod&quot;;

// 작성한 스키마로 타입 추론
type FormFields = z.infer&lt;typeof RegisterSchema&gt;;

export const Form = () =&gt; {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm&lt;FormFields&gt;({
    defaultValue: {
      productName: &#39;&#39;,
      description: &#39;&#39;,
      minPrice: &#39;&#39;,
    },
    // zodResolver로 연결
    resolver: zodResolver(RegisterSchema),
  });

  const onSubmit: SubmitHandler&lt;FormFields&gt; = async (data) =&gt; {
    // submit 로직
  };

  return (
    &lt;Layout.Main&gt;
      &lt;form onSubmit={handleSubmit(onSubmit)}&gt;
        &lt;FormField
          label=&#39;제목*&#39;
          name=&#39;productName&#39;
          control={control}
          error={errors.productName?.message}
          render={(field) =&gt; &lt;Input id=&#39;제목*&#39; type=&#39;text&#39; placeholder=&#39;제목을 입력해주세요.&#39; {...field} /&gt;}
          /&gt;
        &lt;FormField
          label=&#39;시작 가격*&#39;
          name=&#39;minPrice&#39;
          control={control}
          error={errors.minPrice?.message}
          render={(field) =&gt; (
            &lt;Input
              id=&#39;시작 가격*&#39;
              type=&#39;number&#39;,
              placeholder=&#39;최소 시작가는 1,000원입니다.&#39;
              {...field}
              /&gt;
          )}
          /&gt;
        &lt;FormField
          label=&#39;상품 설명&#39;
          name=&#39;description&#39;
          control={control}
          error={errors.description?.message}
          render={(field) =&gt; (
            &lt;Textarea
              id=&#39;상품 설명&#39;
              placeholder=&#39;경매에 올릴 상품에 대해 자세히 설명해주세요.(최대 1,000자)&#39;
              {...field}
              /&gt;
          )}
          /&gt;
      &lt;/form&gt;
    &lt;/Layout.Main&gt;
  );
}</code></pre>
<h2 id="결과">결과</h2>
<p>매우 깔끔하다.
<img src="https://velog.velcdn.com/images/cloud_oort/post/5c1129de-5b68-4396-8e0d-f040f597997e/image.gif" alt=""></p>
<h2 id="출처">출처</h2>
<p><a href="https://react-hook-form.com/">react hook form 공식문서</a>
<a href="https://zod.dev/?id=introduction">zod 공식문서</a>
<a href="https://www.youtube.com/watch?v=cc_xmawJ8Kg">React Hook Form - Complete Tutorial (with Zod)
</a> <a href="https://velog.io/@hyongti/%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90-Zod-vs.-Yup">[번역]스키마 유효성 검사 라이브러리 비교: Zod vs. Yup
</a><a href="https://velog.io/@yukyung/React-%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%99%80-%EB%B9%84%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0">React: 제어 컴포넌트와 비제어 컴포넌트의 차이점
</a><a href="https://articles.wesionary.team/react-hook-form-schema-validation-using-zod-80d406e22cd8">React Hook Form: Schema validation using Zod
</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[복잡한 Modal 설계,  Compound Component로 해결하기]]></title>
            <link>https://velog.io/@cloud_oort/%EB%B3%B5%EC%9E%A1%ED%95%9C-Modal-%EC%84%A4%EA%B3%84-Compound-Component%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cloud_oort/%EB%B3%B5%EC%9E%A1%ED%95%9C-Modal-%EC%84%A4%EA%B3%84-Compound-Component%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 22 Nov 2024 02:06:31 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-modal의-문제점">기존 Modal의 문제점</h2>
<p>리액트에서 일반적으로 Modal은 상태를 선언하고 이를 조작하여 열고 닫는 형태로 구현된다.</p>
<pre><code class="language-jsx">const App = () =&gt; {
  const [isOpen, setIsOpen] = useState(false);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setIsOpen(true)}&gt;모달 열기&lt;/button&gt;
      {isOpen &amp;&amp; &lt;Modal onClose={() =&gt; setIsOpen(false)} /&gt;}
    &lt;/div&gt;
  );
};</code></pre>
<h3 id="문제점">문제점</h3>
<p>이 방식은 직관적이고 간단하지만, Modal이 늘어날수록 같은 상태와 조작 로직을 중복 작성하게 된다.
특히 Modal을 여러 곳에서 사용해야 하는 경우, 유지 보수성이 떨어지고 상태 관리가 번거로워진다.</p>
<h4 id="해결방안">해결방안</h4>
<p>Compound Component Pattern과 React Portal을 사용하면 Modal을 보다 유연하고 재사용 가능하며 유지 보수성이 높은 컴포넌트로 개선할 수 있다.</p>
<h2 id="compound-component-pattern">Compound Component Pattern</h2>
<h3 id="기본-개념">기본 개념</h3>
<p>Compound Component Pattern은 부모 컴포넌트와 여러 자식 컴포넌트가 하나의 상태를 공유하여 협력적으로 동작하도록 만드는 패턴이다. 예를 들어 HTML의 <code>select</code>와 <code>option</code>처럼 동작한다.</p>
<pre><code class="language-html">&lt;label for=&quot;pet-select&quot;&gt;Choose a pet:&lt;/label&gt;

&lt;select name=&quot;pets&quot; id=&quot;pet-select&quot;&gt;
  &lt;option value=&quot;&quot;&gt;--Please choose an option--&lt;/option&gt;
  &lt;option value=&quot;dog&quot;&gt;Dog&lt;/option&gt;
  &lt;option value=&quot;cat&quot;&gt;Cat&lt;/option&gt;
  &lt;option value=&quot;hamster&quot;&gt;Hamster&lt;/option&gt;
  &lt;option value=&quot;parrot&quot;&gt;Parrot&lt;/option&gt;
  &lt;option value=&quot;spider&quot;&gt;Spider&lt;/option&gt;
  &lt;option value=&quot;goldfish&quot;&gt;Goldfish&lt;/option&gt;
&lt;/select&gt;</code></pre>
<h3 id="리액트에서의-compound-component-구현-예시">리액트에서의 Compound Component 구현 예시</h3>
<h4 id="1-context-생성-및-부모-컴포넌트-구현">1. Context 생성 및 부모 컴포넌트 구현</h4>
<p>컴포넌트 간 상태를 공유하기 위해 Context를 생성한다.</p>
<pre><code class="language-jsx">const FlyOutContext = createContext();

function FlyOut(props) {
  const [open, toggle] = useState(false);

  const providerValue = { open, toggle };

  return (
    &lt;FlyOutContext.Provider value={providerValue}&gt;
      {props.children}
    &lt;/FlyOutContext.Provider&gt;
  );
}</code></pre>
<h4 id="2-자식-컴포넌트-생성">2. 자식 컴포넌트 생성</h4>
<p>FlyOut에서 사용할 자식 컴포넌트를 구현한다. 자식 컴포넌트는 Context를 통해 부모의 상태를 공유한다.</p>
<pre><code class="language-jsx">function Toggle() {
  const { open, toggle } = useContext(FlyOutContext);

  return (
    &lt;div onClick={() =&gt; toggle(!open)}&gt;
      &lt;span&gt;Toggle&lt;/span&gt;
    &lt;/div&gt;
  );
}

function List({ children }) {
  const { open } = useContext(FlyOutContext);

  return open &amp;&amp; &lt;ul&gt;{children}&lt;/ul&gt;;
}

function Item({ children }) {
  return &lt;li&gt;{children}&lt;/li&gt;;
}</code></pre>
<h4 id="3-부모-컴포넌트에-자식-컴포넌트-연결">3. 부모 컴포넌트에 자식 컴포넌트 연결</h4>
<p>부모 컴포넌트의 static 속성으로 자식 컴포넌트를 추가하여 부모 컴포넌트만 import하면 자식 컴포넌트를 사용할 수 있게 한다.</p>
<pre><code class="language-jsx">FlyOut.Toggle = Toggle;
FlyOut.List = List;
FlyOut.Item = Item;</code></pre>
<h4 id="사용-예시">사용 예시</h4>
<p>FlyOut 컴포넌트를 사용하여 FlyoutMenu를 구성한다. FlyoutMenu 컴포넌트는 자체적인 상태를 가지지 않는다.</p>
<pre><code class="language-jsx">import { FlyOut } from &#39;./FlyOut&#39;

function FlyoutMenu() {
  return (
    &lt;FlyOut&gt;
      &lt;FlyOut.Toggle /&gt;
      &lt;FlyOut.List&gt;
        &lt;FlyOut.Item&gt;Edit&lt;/FlyOut.Item&gt;
        &lt;FlyOut.Item&gt;Delete&lt;/FlyOut.Item&gt;
      &lt;/FlyOut.List&gt;
    &lt;/FlyOut&gt;
  )
}</code></pre>
<h2 id="react-portal">React Portal</h2>
<p>React Portal은 컴포넌트를 부모 컴포넌트의 계층 구조 밖에 있는 DOM 노드의 자식 컴포넌트로 렌더링하는 방법을 제공한다.
<img src="https://velog.velcdn.com/images/cloud_oort/post/3f368f6e-84f7-4b02-9bcb-1416ce6418ce/image.avif" alt=""></p>
<h3 id="특징">특징</h3>
<h4 id="독립적-스타일링">독립적 스타일링</h4>
<p>부모 컴포넌트의 계층 구조 밖에서 컴포넌트를 렌더링하기 때문에 부모 컴포넌트의 스타일에 영향을 받지 않는다.
상위 컴포넌트의 <code>overflow: hidden</code> 과 같은 설정과의 충돌을 피할 수 있고 <code>z-index</code>와 같은 속성을 쉽게 제어할 수 있어 스타일링이 간단해진다.</p>
<h4 id="이벤트-버블링">이벤트 버블링</h4>
<p>Portal로 렌더링된 컴포넌트는 시각적으로 부모 컴포넌트 외부에서 렌더링되지만, 여전히 리액트의 상태나 이벤트와 연결되어 있어 이벤트 버블링이 정상적으로 동작한다.</p>
<h3 id="사용-예시-1">사용 예시</h3>
<p>ReactDOM의 <code>createPortal</code> 함수를 사용하여 JSX와 렌더링할 위치의 부모 DOM node 를 인수로 전달한다.</p>
<pre><code class="language-jsx">import React from &#39;react&#39;;
import { createPortal } from &#39;react-dom&#39;;

function Portal({ children }) {
  return createPortal(
    &lt;div className=&quot;modal&quot;&gt;{children}&lt;/div&gt;,
    document.body
  );
}</code></pre>
<h2 id="modal-구현">Modal 구현</h2>
<h3 id="목표">목표</h3>
<ol>
<li>Compound Component Pattern을 활용하여 상태와 로직을 Modal 컴포넌트 내부로 캡슐화함으로써 재사용성을 높인다.</li>
<li>React Portal을 활용하여 스타일 충돌 방지 및 유연한 Modal 렌더링을 가능하게 한다.<h3 id="구현-과정">구현 과정</h3>
<h4 id="1-context-생성">1. Context 생성</h4>
<pre><code class="language-tsx">const ModalContext = createContext({
openName: &#39;&#39;,
open: (name: string) =&gt; { },
close: () =&gt; { }
});</code></pre>
<h4 id="2-부모-컴포넌트-생성">2. 부모 컴포넌트 생성</h4>
Modal의 상태와 함수를 Context Provider로 관리한다.</li>
</ol>
<pre><code class="language-tsx">export const Modal = ({ children }: { children: ReactNode }) =&gt; {
  const [openName, setOpenName] = useState(&#39;&#39;)
  const close = () =&gt; setOpenName(&#39;&#39;)
  const open = (name: string) =&gt; setOpenName(name);

  return &lt;ModalContext.Provider value={{ openName, open, close }}&gt;
    {children}
  &lt;/ModalContext.Provider&gt;
}</code></pre>
<h4 id="3-open-컴포넌트-생성">3. Open 컴포넌트 생성</h4>
<p>Modal을 열기 위한 트리거 컴포넌트로, 버튼 같은 트리거 요소를 감싼다.</p>
<pre><code class="language-tsx">const Open = ({ children, name }: { children: ReactElement, name: string }) =&gt; {
  const { open } = useContext(ModalContext)

  return cloneElement(children, { onClick: () =&gt; open(name) })
}</code></pre>
<h4 id="4-window-컴포넌트-생성">4. Window 컴포넌트 생성</h4>
<p>Modal UI를 렌더링하며 Portal을 활용한다.</p>
<pre><code class="language-tsx">const Window = ({ children, name }: { children: ReactElement, name: string }) =&gt; {
  const { openName, close } = useContext(ModalContext)

  if (openName !== name) return null

  return createPortal(
    &lt;div className=&quot;absolute inset-0 z-50 flex items-center justify-center&quot; onClick={handleClose}&gt;
      &lt;div
        className=&quot;relative z-50 flex items-center justify-center h-full w-web min-w-mobile bg-black/50&quot;
      &gt;
        {cloneElement(children, { onCloseModal: close })}
      &lt;/div&gt;
    &lt;/div &gt;,
    document.body,
  );
};</code></pre>
<h4 id="부모와-자식-연결">부모와 자식 연결</h4>
<p>Modal에 Open과 Window를 연결한다.</p>
<pre><code class="language-tsx">Modal.Open = Open;
Modal.Window = Window;</code></pre>
<h3 id="사용-예시-2">사용 예시</h3>
<p>버튼을 클릭하면 ConfirmModal을 띄우는 예제이다.</p>
<pre><code class="language-tsx">interface ConfirmProps {
  children: ReactNode
  onCloseModal?: () =&gt; void
}

// Modal 컴포넌트에서 onCloseModal 속성에 close 함수를 정의한다.
const Confirm = ({ children, onCloseModal }: ConfirmProps) =&gt; {
  return (
    &lt;div onClick={(e) =&gt; e.stopPropagation()} &gt;
      &lt;div&gt;
        Modal 내용...
      &lt;/div&gt;
      &lt;div&gt;
        &lt;button type=&#39;button&#39; onClick={onCloseModal} &gt;
          닫기
        &lt;/button&gt;
        {children}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

const Details = () =&gt; {
  const { mutate: cancelBid } = useCancelBid()
  const clickCancel = () =&gt; cancelBid()

  return (
    &lt;Modal&gt;
      &lt;Modal.Open name=&quot;cancelBid&quot;&gt;
        &lt;button&gt;
          참여 취소
        &lt;/button&gt;
      &lt;/Modal.Open&gt;
      &lt;Modal.Window name=&quot;cancelBid&quot;&gt;
        &lt;Confirm type=&quot;cancelBid&quot; &gt;
          &lt;button onClick={clickCancel}&gt;
            참여 취소
          &lt;/button&gt;
        &lt;/Confirm&gt;
      &lt;/Modal.Window&gt;
    &lt;/Modal&gt;
  );
}</code></pre>
<h2 id="결과">결과</h2>
<p>개발자 도구에서 확인하면 <code>body</code>태그에 Modal이 렌더링된 것을 볼 수 있다.</p>
<h5 id=""><img src="https://velog.velcdn.com/images/cloud_oort/post/0ba2037b-ca6d-412f-9cea-95b602821c13/image.gif" alt=""></h5>
<h2 id="출처">출처</h2>
<p><a href="https://patterns-dev-kr.github.io/design-patterns/compound-pattern/">Compound Pattern</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select">MDN HTML select</a>
<a href="https://techhub.iodigital.com/articles/what-are-react-portals?utm_source=importreact.beehiiv.com&amp;utm_medium=newsletter&amp;utm_campaign=react-s-hidden-leaks&amp;_bhlid=8e0c816fc5dc6ace6b11aad026247174eecac160">What are React Portals?</a>
<a href="https://ko.react.dev/reference/react-dom/createPortal">React Portals 공식문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[FE] FSD(Feature-Sliced Design) 폴더 구조]]></title>
            <link>https://velog.io/@cloud_oort/FE-FSDFeature-Sliced-Design-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@cloud_oort/FE-FSDFeature-Sliced-Design-%ED%8F%B4%EB%8D%94-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Sat, 02 Nov 2024 03:19:41 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-폴더-구조">기존 폴더 구조</h2>
<pre><code>// 현재 프로젝트의 폴더 구조
└─src
    ├─api
    ├─assets
    ├─components
    ├─constants
    ├─hooks
    ├─lib
    ├─mocks
    ├─pages
    ├─provider
    ├─store
    ├─utils
    └─types</code></pre><p>현재 프로젝트의 폴더 구조는 Monolith 구조로, 모든 코드와 기능이 기술적 요소별로 한곳에 모여있다. Monolith 구조는 기능적 역할보다는 코드의 기술적 속성에 중점을 두어 폴더를 구성하기 때문에, 프로젝트 초기에는 코드 탐색이 단순하고 명확하다는 장점이 있다. 그러나 프로젝트 규모가 점차 커질수록 의존성 관리, 확장성, 협업 효율성 등의 문제가 발생하며 Monolith 구조의 한계가 명확해진다.</p>
<h2 id="monolith-구조의-문제점">Monolith 구조의 문제점</h2>
<h3 id="의존성-문제">의존성 문제</h3>
<p>서로 다른 폴더의 코드들이 서로를 참조하는 과정에서 순환 참조 문제가 발생할 수 있다. 이는 예기치 못한 동작을 유발하거나 디버깅을 어렵게 만들어 각 기능이 명확히 분리되지 않는 상황에서 특정 기능과 관련된 코드를 추적하고 의존 관계를 파악하는 데 많은 시간이 소요된다.</p>
<h3 id="확장성-문제">확장성 문제</h3>
<p>Monolith 구조에서는 코드가 기술적 요소로만 구분되어 있어 기능별로 캡슐화되지 않는다. 따라서 새로운 기능을 추가하거나 기존 기능을 수정할 때 예상치 못한 의존성 문제가 발생할 수 있다. 결과적으로 프로젝트가 커질수록 복잡도가 증가하고 유지보수가 어려워진다.</p>
<h3 id="협업-비효율성">협업 비효율성</h3>
<p>기술 기반으로 분류된 폴더에서는 개발자들이 같은 폴더 내에서 작업해야 하는 경우가 많아지면서 기능 간 충돌이 발생할 가능성이 커진다. PR을 통해 코드 리뷰를 해서 이러한 문제를 막을 수 있지만, 코드 리뷰가 길어지는 시간 또한 리소스 낭비이다.
서로 다른 개발자가 특정 기능을 구현할 때 일관된 기준 없이 폴더와 파일을 생성하다 보면, 구조의 일관성이 깨지기 쉬워진다. 예를 들어 한 개발자는 components 폴더에 모든 컴포넌트를 넣는 반면, 다른 개발자는 페이지에 종속된 컴포넌트를 따로 관리하고 싶어할 수도 있기 때문에 협업 시 불필요한 코드 중복과 혼란이 발생할 수 있다.</p>
<h3 id="유지-보수의-어려움">유지 보수의 어려움</h3>
<p>기술 기반으로 분류된 폴더에서는 폴더 구조를 변경하는 작업이 코드 전체에 영향을 미치기 쉽다. 특히, 각 폴더에 분산된 코드가 서로 얽혀 있다면 폴더 구조를 바꾸는 작업은 순환 참조나 의존성 문제를 더 심화시킬 수 있다.
기술적인 기준만을 고려한 구조에서는 기능별로 코드가 분리되지 않아 재사용성이 떨어지고 응집력 또한 낮아진다. 이로 인해 코드의 유지보수가 어려워지고, 기능 확장 시에도 새로운 폴더나 파일을 만들면서 계속해서 복잡해진다.</p>
<h3 id="정리">정리</h3>
<p>Monolith 구조는 간단한 프로젝트에서는 빠르게 사용할 수 있지만, 프로젝트가 커질수록 유지보수, 협업, 확장성 등 다양한 측면에서 한계를 드러내는 구조다. 이를 해결하기 위해 각 기능 단위로 코드를 분리하고 의존 관계를 명확히 관리할 수 있는 FSD(Feature Sliced Design)와 같은 구조가 점점 더 필요한 이유가 된다.</p>
<h1 id="fsdfeature-sliced-design-소개">FSD(Feature Sliced Design) 소개</h1>
<p>FSD(Feature Sliced Design)는 애플리케이션의 기능을 중심으로 코드를 구조화하는 설계 패턴이다. 여기서 기능 중심이란, 코드를 기술적인 요소로 분류하는 대신 비즈니스 로직에 따라 각 기능별로 독립된 모듈로 나눈다는 의미이다. 이러한 기능 단위의 모듈화를 통해 각 기능이 독립적으로 <strong>개발, 테스트, 유지보수</strong>될 수 있도록 만든다. FSD는 코드가 확장성과 유지보수성이 뛰어나도록 설계되어 있어, 개발 팀이 협업하는 과정에서 효율성을 크게 증진시켜준다.</p>
<h2 id="주요-목적">주요 목적</h2>
<p>FSD의 주요 목적은 코드를 기능 단위로 모듈화하여, <strong>확장성과 유지보수성을 높이는 것</strong>이다. 기능 중심의 코드 구조는 팀 단위로 작업할 때 특정 기능의 코드에만 집중할 수 있게 하며, 기능별로 코드가 모듈화되어 있어 <strong>변경사항이 다른 기능에 미치는 영향을 최소화</strong>할 수 있다. 이를 통해 코드의 가독성을 높이고, 복잡성을 줄여 <strong>효율적인 협업</strong>을 가능하게 해준다.</p>
<h2 id="fsd의-핵심-원칙">FSD의 핵심 원칙</h2>
<ol>
<li><p>기능 중심 설계
애플리케이션을 기능 단위로 구분하여 설계한다. 이는 각 기능이 독립적인 모듈로 구분되어, 개발과 테스트, 유지보수가 기능별로 독립적으로 이루어질 수 있게 하는 방식이다.</p>
</li>
<li><p>계층화
코드를 여러 계층으로 나누어 관심사를 분리한다. 예를 들어, UI 계층과 비즈니스 로직 계층을 구분하여 코드 간섭을 줄이고, 특정 계층만 수정해도 다른 계층에 영향을 주지 않도록 한다. 이를 통해 코드를 탐색하거나 유지보수할 때 필요하지 않은 코드까지 볼 필요가 없어져 코드의 일관성을 유지할 수 있다.</p>
</li>
<li><p>단방향 의존성
상위 계층은 하위 계층에만 의존할 수 있도록 제한한다. 이는 특정 기능이 상위 계층으로 올라가면서도 하위 계층의 기능을 참조하는 구조를 막아주어, 의존 관계가 복잡하게 얽히지 않도록 한다.</p>
</li>
<li><p>공개 API
각 모듈이 명확한 공개 API를 통해 다른 모듈과 상호작용하게 한다. 각 모듈이 제공하는 기능은 공개 API를 통해서만 접근할 수 있고, 그 외의 세부 구현은 외부로 노출되지 않아 캡슐화가 잘 유지된다. 이로 인해 코드 수정이 필요한 경우 외부에 미치는 영향을 최소화할 수 있다.</p>
<h2 id="fsd의-구성-요소">FSD의 구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/5a51f59b-9a97-4ca8-9a12-653b76bd9ae2/image.jpg" alt=""></p>
<h3 id="레이어">레이어</h3>
<p>레이어는 FSD의 가장 상위 수준의 구조 6개로 제한되어 있으며 표준화되어 있다.
상위 레벨에 있는 레이어는 하위 레벨을 의존성으로 가질 수 있지만 그 반대는 성립될 수 없고, 하위 레이어로 갈수록 추상화가 심화되며 상위 레이어로 갈수록 비즈니스 로직이 심화된다.</p>
<h4 id="app">app</h4>
<p>애플리케이션 초기화 및 글로벌 설정을 정의한다. 애플리케이션의 진입점, 전역 상태 관리, 라우팅 설정, 전역 스타일 파일이 있다.
슬라이스 없이 세그먼트로 나눈다.</p>
<h4 id="pages">pages</h4>
<p>각 라우트에 해당하는 페이지 컴포넌트를 정의한다.</p>
<h4 id="widgets선택적-레이어">widgets(선택적 레이어)</h4>
<p>독립적으로 작동하는 대규모 기능 또는 UI 컴포넌트, 보통 하나의 완전한 기능을 정의한다.
즉 UI와 로직이 결합된 독립적인 기능 단위로 페이지의 특정 기능을 담당한다.</p>
<h4 id="features">features</h4>
<p>사용자의 특정 상호작용과 비즈니스 로직을 정의한다. 특정 작업과 관련된 로직과 UI 요소를 포함하며, 사용자 시나리오에 따른 기능을 정의한다.</p>
<h4 id="entities선택적-레이어">entities(선택적 레이어)</h4>
<p>애플리케이션 전반에서 <strong>반복적으로 사용되는 핵심 비즈니스 데이터 모델 및 도메인 객체</strong>를 정의한다.
예를 들어 데이터의 타입 정의와 기본적인 데이터 구조, 데이터에 대한 기본적인 연산이나 로직 관련된 유틸리티 함수나 상수가 있다.</p>
<h4 id="shared">shared</h4>
<p>비즈니스 로직에 의존하지 않으면서 애플리케이션 전반에서 사용되는 <strong>공통 유틸리티 및 UI 컴포넌트</strong>를 정의한다.
슬라이스 없이 세그먼트로 나눈다.</p>
</li>
</ol>
<h3 id="슬라이스">슬라이스</h3>
<p>슬라이스는 각 레이어 내에서 특정 기능 영역이나 도메인을 나타내는 하위 디렉토리이다.</p>
<h4 id="목표">목표</h4>
<ul>
<li><p>슬라이스를 통해 특정 비즈니스 도메인이나 기능을 캡슐화한다.</p>
</li>
<li><p>독립적으로 개발 및 테스트를 한다.</p>
</li>
<li><p>다른 슬라이스와의 의존성을 명시적으로 관리한다.</p>
</li>
<li><p>레이어에서 슬라이스끼리 기능을 공유하지 않고 독립적으로 사용한다.</p>
<h3 id="세그먼트">세그먼트</h3>
<p>세그먼트는 슬라이스 내부의 코드를 목적에 따라 더 세분화한 것이다.
세그먼트의 이름은 ui, model, lib, api등이 될 수 있으며 예시는 아래와 같다.</p>
</li>
<li><p>api - 외부 서비스와의 통신.</p>
</li>
<li><p>UI - UI 컴포넌트.</p>
</li>
<li><p>model - 비즈니스 로직,상태와의 상호 작용, actions 및 selectors 등.</p>
</li>
<li><p>lib - 슬라이스 내에서 사용되는 보조 기능, 유틸리티 함수, 헬퍼 함수, 훅.</p>
</li>
<li><p>config - 설정 및 상수</p>
<h3 id="public-api">Public API</h3>
<p>Public API는 각 슬라이스나 세그먼트가 외부에 노출하는 인터페이스이다.
index.js 또는 index.ts 파일이며, 이 파일을 통해 슬라이스 또는 세그먼트에서 필요한 기능만 외부로 추출하고 불필요한 기능은 격리할 수 있다.</p>
<h4 id="목표-1">목표</h4>
</li>
<li><p><strong>캡슐화</strong>: Public API는 폴더 외부로 필요한 것만 노출하도록 하여 내부 구조를 감추는 역할을 한다. 만약 모든 파일을 외부에서 직접 import하도록 허용하면, 해당 feature의 구조가 외부에 그대로 노출되어 캡슐화가 깨진다.</p>
</li>
<li><p><strong>일관성 유지</strong>: 여러 팀원이 작업할 때, Public API를 통한 import 경로가 있으면 모든 코드에서 동일한 경로를 사용하게 되어 일관성이 유지된다.</p>
</li>
<li><p><strong>유지보수성</strong>: Public API를 통해 노출된 파일만 공식 API로 간주하고, 내부 파일의 구조가 변경되더라도 외부에서는 이를 알 필요가 없기 때문에 유지보수가 용이하다.</p>
</li>
<li><p><strong>간결한 import 경로</strong>: Public API 덕분에 import 경로가 간결해지며, 특정 feature의 모든 export를 한 경로로 모을 수 있다.</p>
</li>
</ul>
<h3 id="장점">장점</h3>
<ol>
<li><p><strong>확장성</strong>  </p>
<ul>
<li>새로운 기능을 추가하거나 기존 기능을 수정하기에 용이하다. 기능이 명확하게 분리되어 있어 코드베이스가 커지더라도 각 기능을 독립적으로 다룰 수 있으며, 전체 코드에 영향을 주지 않으면서 특정 기능을 확장할 수 있기 때문이다.</li>
</ul>
</li>
<li><p><strong>모듈성</strong>  </p>
<ul>
<li>각 기능이 독립적인 모듈로 구성되어 있어 재사용성과 유지보수성이 높아진다. 다른 기능에 종속되지 않고 독립적으로 동작하기 때문에 기능 단위로 쉽게 유지보수할 수 있는 구조이다.</li>
</ul>
</li>
<li><p><strong>테스트 용이성</strong>  </p>
<ul>
<li>기능별로 코드가 분리되어 있어 각 기능을 독립적으로 테스트하기가 용이하다. 슬라이스와 세그먼트 단위로 테스트가 가능해 에러 발생 시 디버깅이 수월하다는 장점이 있다.</li>
</ul>
</li>
<li><p><strong>팀 협업 효율성</strong>  </p>
<ul>
<li>기능 단위로 작업을 분배하여 병렬로 개발할 수 있다. 각 개발자가 독립된 기능을 담당하므로 작업 간섭이 줄어들고, 협업 시 코드 충돌 가능성도 낮아진다.</li>
</ul>
</li>
<li><p><strong>코드 탐색과 유지보수의 편리함</strong>  </p>
<ul>
<li>일관된 구조 덕분에 코드 탐색이 쉽다. 프로젝트 내에서 필요한 코드의 위치를 빠르게 파악할 수 있어 유지보수가 수월하고, 새로운 팀원이 합류해도 구조를 이해하기가 용이하다.</li>
</ul>
</li>
<li><p><strong>의존성 관리</strong>  </p>
<ul>
<li>명시적으로 의존성을 관리하여 코드의 예측 가능성이 높아진다. 단방향 의존성을 유지해 구조가 명확해지므로 코드 복잡성을 줄일 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="단점">단점</h3>
<ol>
<li><p><strong>리팩토링 비용</strong>  </p>
<ul>
<li>처음부터 FSD를 사용하지 않았다면, 마이그레이션 과정에서 큰 리팩토링 비용이 발생할 수 있다. 특정 코드를 어디에 배치해야 할지 고민해야 하는 등 초기 생산성이 낮아질 가능성도 크다.</li>
</ul>
</li>
<li><p><strong>오버엔지니어링 위험</strong>  </p>
<ul>
<li>작은 프로젝트에서는 FSD 구조가 오히려 복잡성을 높일 수 있다. 작은 프로젝트에서 기능 중심 구조는 단순한 구조보다 효율이 떨어질 수 있어 불필요하게 복잡해질 수 있다.</li>
</ul>
</li>
<li><p><strong>Public API 관리</strong>  </p>
<ul>
<li>Public API를 설정해 각 모듈에 접근할 때 진입점을 정의하지만, 파일 경로나 폴더 구조가 변경되면 Public API 파일도 수정해야 해 추가적인 리팩토링 작업이 발생할 수 있다.</li>
</ul>
</li>
<li><p><strong>협업 과정에서의 온보딩 시간</strong>  </p>
<ul>
<li>FSD 규칙을 프로젝트 전반에 일관되게 적용해야 하므로 팀원 온보딩에 시간이 소요될 수 있다. 모든 팀원이 규칙에 따라 코드를 작성해야 해 협업 초기에는 일관성 유지를 위해 학습 곡선이 존재한다.</li>
</ul>
</li>
</ol>
<h2 id="barrel-파일-순환-참조-에러-발생">barrel 파일 순환 참조 에러 발생</h2>
<h3 id="barrel-file이란">barrel file이란?</h3>
<p>barrel file은 하나의 파일에서 여러 모듈을 re-export 하는 역할만 하는 파일을 의미한다.
보통 <code>index.ts</code> 또는 <code>index.js</code>라는 이름을 사용하며, 폴더 내부의 구조를 외부에 감추고 단일 엔트리 포인트로 노출하기 위해 사용한다. </p>
<p>이를 통해 내부에서 모듈 위치가 변경되어도 소비자 측에서는 폴더 경로 변경 없이 일관된 import를 유지할 수 있으며, import 문을 깔끔하게 정리할 수 있다.</p>
<h3 id="barrel-file의-문제점">barrel file의 문제점</h3>
<h4 id="순환-참조-및-트리-쉐이킹-문제">순환 참조 및 트리 쉐이킹 문제</h4>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/b9a47f2d-7cda-47b2-b0b4-e7ba7a61c7f0/image.png" alt=""></p>
<p>폴더 내부의 모듈이 내부 다른 모듈을 import할 때 import문을 잘못 작성(대부분 자동 경로를 사용)하는 경우, 직접 참조가 아닌 배럴 파일 통해 import하기 때문에 순환 참조가 발생할 수 있다.</p>
<p>순환 참조는 두 개 이상의 모듈이 서로를 직접 또는 간접적으로 참조할 때 발생하는 상황을 말한다.
이런 순환 참조는 코드 가독성과 유지보수 어렵게 하고, 빌드 및 번들링 과정에서 트리 쉐이킹과 같은 최적화가 제대로 이루어지지 않을 가능성 크다.  결과적으로 불필요한 코드가 번들에 포함되거나, 번들 크기가 비정상적으로 커질 수 있다.</p>
<h4 id="불필요한-모듈-로딩">불필요한 모듈 로딩</h4>
<p>barrel file에서 특정 모듈 불러오면 자바스크립트는 먼저 barrel file을 열어 내부의 모든 export를 확인한다. 이 과정에서 파일 내에 존재하는 모든 모듈들이 동기적으로 로드된다. 몇 개의 export만 있다면 상관없는데, 많은 모듈을 export하거나 일부 모듈이 다시 다른 barrel file을 포함하고 있다면 한 번의 import 로 실제로는 사용하지 않는 많은 모듈까지 모두 로드된다. 불필요하게 많은 모듈이 동시에 로드되면, 특히 개발 모드에서 페이지 시작 시 로딩 시간이 급격히 늘어날 수 있다.(빌드 환경에서는 트리 쉐이킹으로 실제 사용되지 않는 코드는 제외될 수 있음.)</p>
<h3 id="주의사항-및-권장사항">주의사항 및 권장사항</h3>
<ul>
<li>라이브러리 작성 제외하고는 barrel file 사용 지양할 것을 추천.</li>
<li>내부 참조 시 barrel file 대신 직접 경로 사용.</li>
<li>import/no-cycle eslint 규칙 적용<ul>
<li>eslint-plugin-import에서 제공하는 규칙 중 하나로, 모듈 간에 순환 참조(circular dependency)가 발생하는 것을 감지하고 경고 또는 에러를 발생시킨다. </li>
<li>ESLint가 코드 내의 <code>import</code> 구문들을 분석하여, 모듈 간의 의존성 그래프를 생성한다.</li>
</ul>
</li>
<li><a href="https://github.com/pahen/madge">madge</a> 라이브러리 통해 시각적 의존성 확인</li>
</ul>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://tkdodo.eu/blog/please-stop-using-barrel-files">tkdodo barrel files</a></p>
<h2 id="출처">출처</h2>
<p><a href="https://feature-sliced.design/kr/docs/get-started/overview">FSD 공식 문서</a>
<a href="https://emewjin.github.io/feature-sliced-design/">(번역) 기능 분할 설계 - 최고의 프런트엔드 아키텍처</a>
<a href="https://github.com/feature-sliced/examples/tree/master/todo-app">feature-sliced/examples</a>
<a href="https://developers.hyundaimotorgroup.com/blog/399">기능 분할 설계(FSD)를 이용한 FE 아키텍처 구성</a>
<a href="https://velog.io/@jay/fsd">FSD 아키텍처 알아보기</a>
<a href="https://j-ho.dev/28/">기능 분할 설계(Feature-Sliced Design, FSD)</a>
<a href="https://xionwcfm.tistory.com/462">Feature-Sliced Design을 직접 사용하면서 느낀 장점과 단점</a>
<a href="https://velog.io/@teo/separation-of-concerns-of-frontend">프론트엔드 개발자 관점으로 바라보는 관심사의 분리와 좋은 폴더 구조 (feat. FSD)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTML input의 type이 number일 때 한글  입력 막기.]]></title>
            <link>https://velog.io/@cloud_oort/HTML-input%EC%9D%98-type%EC%9D%B4-number%EC%9D%BC-%EB%95%8C-%ED%95%9C%EA%B8%80-%EC%9E%85%EB%A0%A5-%EB%A7%89%EA%B8%B0</link>
            <guid>https://velog.io/@cloud_oort/HTML-input%EC%9D%98-type%EC%9D%B4-number%EC%9D%BC-%EB%95%8C-%ED%95%9C%EA%B8%80-%EC%9E%85%EB%A0%A5-%EB%A7%89%EA%B8%B0</guid>
            <pubDate>Fri, 25 Oct 2024 17:17:38 GMT</pubDate>
            <description><![CDATA[<p>HTML <code>input</code>의 <code>type</code>이 <code>number</code>일 때, 일부 브라우저에서 한글 입력 시 첫 글자가 입력되는 현상은 브라우저가 한글의 입력 방식을 처리하는 과정에서 발생하는 문제이다.</p>
<p>이 현상을 이해하려면 한글의 입력 방식과 브라우저의 처리 방식 차이를 살펴봐야 한다.</p>
<h2 id="한글-입력-방식">한글 입력 방식</h2>
<p>한글은 다른 언어와 다르게 자음과 모음이 조합되어 완성된 한 글자가 만들어진다. 예를 들어, &#39;ㅎ&#39;과 &#39;ㅏ&#39;를 입력하면 &#39;하&#39;가 되는 식이다. 이 과정에서 사용자가 키보드로 자음과 모음을 입력할 때마다 입력 중인 문자를 브라우저에 보낸다.</p>
<h2 id="브라우저의-숫자-입력-처리">브라우저의 숫자 입력 처리</h2>
<p><code>input</code> 타입이 <code>number</code>일 때, 브라우저는 기본적으로 숫자 외의 입력을 막도록 설계되어 있다. 하지만 한글 입력의 특수성 때문에  자음 또는 모음 하나만 먼저 전송하는 경우가 생겨서 한글의 첫 자음 또는 모음이 입력창에 들어가게 된다. 브라우저는 이 상황을 제대로 처리하지 못하고, 첫 글자를 일시적으로 보여준 다음에서야 숫자가 아닌 입력임을 인지하고 차단하게 된다.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>여러 방법이 있을 수 있겠으나, 이 글에서는<code>input</code>에서 숫자가 아닌 문자가 입력되는 것을 실시간으로 감지하는 oninput 이벤트를 사용한다.</p>
<h2 id="예제">예제</h2>
<p>리액트에서는 onInput 이벤트를 사용한다.</p>
<pre><code class="language-js"> const preventInvalidInput = (event: ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    // 숫자가 아닌 입력값과 한글 입력을 필터링한다.
    event.target.value = event.target.value.replace(/[^0-9]/g, &#39;&#39;);
  };

&lt;input
  type=&#39;number&#39;
  onInput={preventInvalidInput}
/&gt;</code></pre>
<h2 id="oninput과-onchange의-차이">onInput과 onChange의 차이</h2>
<ul>
<li><p><strong><code>onInput</code></strong>: 값이 변경될 때마다 호출된다. 
즉, 사용자가 키를 누를 때마다 실시간으로 이벤트가 발생하므로 실시간 검증이나 필터링 작업에 적합하다. 
키 입력마다 호출되기 때문에 복잡한 처리가 들어가면 성능에 영향을 줄 수 있다.</p>
</li>
<li><p><strong><code>onChange</code></strong>: 입력을 마치고 포커스를 잃거나 <code>Enter</code>를 눌러야만 호출된다. 
입력이 완료된 후의 처리가 필요한 경우에 적합하다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[번역] Fault Tolerance(장애 허용성)]]></title>
            <link>https://velog.io/@cloud_oort/%EB%B2%88%EC%97%AD-Fault-Tolerance%EC%9E%A5%EC%95%A0-%ED%97%88%EC%9A%A9%EC%84%B1</link>
            <guid>https://velog.io/@cloud_oort/%EB%B2%88%EC%97%AD-Fault-Tolerance%EC%9E%A5%EC%95%A0-%ED%97%88%EC%9A%A9%EC%84%B1</guid>
            <pubDate>Mon, 23 Sep 2024 07:38:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.brandondail.com/posts/fault-tolerance-react">https://www.brandondail.com/posts/fault-tolerance-react</a></p>
</blockquote>
<h1 id="fault-tolerance">Fault Tolerance</h1>
<p>모던 웹 애플리케이션을 구축하는 것은 변화하는 요소가 많은 복잡한 과정입니다. 때로는 이러한 요소들이 변화를 멈추면서 문제가 발생하기도 합니다.</p>
<p>우리는 이러한 문제가 발생하지 않도록 할 수 있는 모든 것을 하지만, 완전히 에러가 없는 상태를 유지하는 것은 현실적으로 불가능합니다. 즉 예상치 못한 방식으로 문제가 발생할 수 있다는 것을 항상 염두에 두어야 하며, 그럴 때 이를 우아하게 처리할 수 있어야 합니다.</p>
<p>다시 말해, 우리는 <a href="https://en.wikipedia.org/wiki/Fault_tolerance">Fault Tolerance(장애 허용성)</a>가 필요합니다.</p>
<blockquote>
<p>Fault Tolerance 란 시스템의 일부 구성 요소에 장애가 발생하더라도 시스템이 정상적으로 작동을 계속할 수 있게 하는 특성을 말합니다.</p>
</blockquote>
<p>내 경험상 Fault Tolerance은 웹 애플리케이션에서 종종 간과되고 과소평가됩니다. 잠재적으로 문제가 발생하지 않을 것이라는 자신감을 주는 수백 개의 테스트가 있을 수 있지만, 불가피한 장애가 발생했을 때 어떻게 대응할지에 대해서는 충분히 고려하지 않습니다. 이는 <a href="https://en.wikipedia.org/wiki/High_availability">High availability(고가용성)</a>을 우선시하는 경우 특히 중요합니다</p>
<p>그렇다면 리액트 애플리케이션에서 Fault Tolerance를 어떻게 구현할 수 있을까요?</p>
<h1 id="error-boundaries">Error Boundaries</h1>
<p>답은 바로 <a href="https://legacy.reactjs.org/docs/error-boundaries.html">error boundaries</a>입니다. 현재 이 API는 클래스 컴포넌트에서만 사용 가능하며, 다음과 같은 형태로 보입니다.</p>
<pre><code class="language-jsx">class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return &lt;h1&gt;Something went wrong.&lt;/h1&gt;;
    }

    return this.props.children; 
  }
}</code></pre>
<blockquote>
<p>💬 리액트에서 error boundary는 <code>getDerivedStateFromError</code>메서드와 <code>componentDidCatch</code> 메서드를 가진 클래스 컴포넌트입니다. 처음 사용하기에 좋은 예시를 원하신다면, 다음을 확인해보세요. <a href="https://github.com/bvaughn/react-error-boundary">react-error-boundary</a></p>
</blockquote>
<p>리액트 공식 문서에서 error boundaries가 무엇인지와 사용하는 방법에 대해 매우 잘 설명하고 있으니, 제가 이 부분에 대해 길게 설명하지는 않겠습니다. 먼저 공식 문서를 읽어보시고, 기본적인 내용을 잘 이해한 후 다시 돌아오시면 좋을 것 같습니다.</p>
<h1 id="drawing-the-right-fault-lines">Drawing the Right Fault Lines</h1>
<p>애플리케이션에 error boundary를 추가하는 것은 쉽습니다. 몇 줄의 코드만으로 구현할 수 있죠. 어려운 점은 <strong>error boundary를 배치할 적절한 곳을 찾는 것</strong>입니다. 보통은 <a href="https://en.wikipedia.org/wiki/Goldilocks_principle">골디락스 원칙</a>을 따르고, &quot;적당한 양&quot;의 error boundaries를 구현하는 것이 이상적이지만 그 &quot;적당한 양&quot;이란 무엇일까요?</p>
<p>먼저, 두 가지 극단적인 상황을 살펴보고 각각의 단점을 이해해 봅시다.</p>
<h2 id="not-enough-error-boundaries">Not Enough Error Boundaries</h2>
<p>첫 번째 극단적인 상황: 애플리케이션 최상단에 단 하나의 error boundary를 배치하는 경우입니다.</p>
<pre><code class="language-jsx">// ⚠️ 이번 예시들에서는 react-error-boundary 라이브러리를 사용할 것입니다.
import ErrorBoundary from &quot;react-error-boundary&quot;;
import App from &quot;./App.js&quot;;

ReactDOM.render(
  &lt;ErrorBoundary&gt;
    &lt;App /&gt;
  &lt;/ErrorBoundary&gt;,
  document.getElementById(&quot;root&quot;)
);</code></pre>
<p>이 방법은 아마 대부분의 사람들이 사용하는 방식에 가까울 것입니다. 이는 서버 렌더링 애플리케이션이 실패할 때 발생하는 상황과 본질적으로 비슷합니다. 결코 나쁘지 않지만, 최선의 방법이라고는 할 수 없습니다. 문제는 하나에서 장애가 발생하면, 애플리케이션의 나머지 부분도 모두 장애가 발생한다는 점입니다.</p>
<p>애플리케이션 일부에서 발생한 장애가 애플리케이션 전체를 사용할 수 없음을 의미할 때 이는 옳은 방법일 수 있습니다. 앞선 접근 방식이 옳은 경우가 분명 있지만, 일반적인 경우는 아니라고 생각합니다.</p>
<p>Fault Tolerance의 정의로 돌아가 보면</p>
<blockquote>
<p>Fault Tolerance는 시스템의 일부에서 장애가 발생해도 시스템이 정상적으로 작동을 계속할 수 있게 하는 특성을 말합니다.</p>
</blockquote>
<p>단일 error boundary 방식은 하나의 장애가 전체 애플리케이션을 무너트리기 때문에 <strong>Fault Tolerance</strong>을 제대로 구현할 수 없다는 것을 알 수 있습니다.</p>
<h2 id="too-many-error-boundaries">Too Many Error Boundaries</h2>
<p>다른 극단적인 경우로 넘어가 보면, 모든 컴포넌트를 error boundary로 감싸는 방법을 시도할 수 있습니다. 이 접근 방식의 문제는 쉽게 규정 지을 수 없기 때문에, 이것이 왜 단일 error boundary보다 나쁠 수 있는지 구체적인 예시를 통해 살펴보겠습니다.</p>
<p>예를 들어, 사용자가 장바구니를 확인하고, 신용카드 정보를 입력하며, 구매를 완료할 수 있는 <CheckoutForm /> 컴포넌트가 있다고 가정해 봅시다.</p>
<pre><code class="language-jsx">function CheckoutForm(props) {
  return (
    &lt;form&gt;
      &lt;CartDescription items={props.items} /&gt;
      &lt;CreditCardInput /&gt;
      &lt;CheckoutButton cartId={props.id} /&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p>이제 모든 컴포넌트를 error boundary로 감싸봅시다.</p>
<pre><code class="language-jsx">&lt;form&gt;
  &lt;ErrorBoundary&gt;
    &lt;CartDescription items={props.items} /&gt;
  &lt;/ErrorBoundary&gt;
  &lt;ErrorBoundary&gt;
    &lt;CreditCardInput /&gt;
  &lt;/ErrorBoundary&gt;
  &lt;ErrorBoundary&gt;
    &lt;CheckoutButton cartId={props.id} /&gt;
  &lt;/ErrorBoundary&gt;
&lt;/form&gt;</code></pre>
<blockquote>
<p>🤚 실제 예시에서는 각 컴포넌트가 자신의 export 부분을 error boundary로 감싸는 방식일 것입니다. (예를 들어, <a href="https://www.npmjs.com/package/react-error-boundary#witherrorboundary-hoc">react-error-boundary의 withErrorBoundary HOC 패턴</a> 을 사용하는 방식처럼요.) 하지만 여기서는 그 부분은 무시하고 보셔도 됩니다.  🙂</p>
</blockquote>
<p>처음에는 이것이 괜찮은 아이디어처럼 보일 수 있습니다. error boundaries를 더 세분화할수록, 하나의 장애가 애플리케이션 전체에 미치는 영향이 줄어들기 때문이죠. 이를 <strong>Fault Tolerance</strong>라고 할 수도 있습니다! 그러나 문제는 error의 영향을 최소화하는 것이 곧 <strong>Fault Tolerance</strong>을 의미하지는 않는다는 점입니다.</p>
<p>예를 들어, <CreditCardInput /> 컴포넌트에서 어떤 문제가 발생했다고 가정해 봅시다.</p>
<pre><code class="language-jsx">&lt;form&gt;
  &lt;ErrorBoundary&gt;
    &lt;CartDescription items={props.items} /&gt;
  &lt;/ErrorBoundary&gt;
  &lt;ErrorBoundary&gt;
    {/* 에러 발생 😢 */}
    &lt;CreditCardInput /&gt;
  &lt;/ErrorBoundary&gt;
  &lt;ErrorBoundary&gt;
    &lt;CheckoutButton cartId={props.id} /&gt;
  &lt;/ErrorBoundary&gt;
&lt;/form&gt;</code></pre>
<p>이 상황이 사용자 경험(UX)에 어떤 영향을 미칠지 살펴보면, 이것이 사용자에게 큰 불편을 줄 수 있다는 것을 알 수 있습니다.</p>
<h3 id="half-broken-ui-is-full-broken-ux">Half-Broken UI is Full-Broken UX</h3>
<p><code>&lt;CreditCardInput /&gt;</code> 컴포넌트가 자체적인 error boundary를 가지고 있기 때문에, 에러가 발생해도 <code>&lt;CreditCardForm /&gt;</code> 컴포넌트의 나머지 부분으로 전파되지 않습니다. 하지만 <code>&lt;CreditCardInput /&gt;</code> 컴포넌트 없이는 <code>&lt;CreditCardForm /&gt;</code> 컴포넌트가 제대로 작동할 수 없죠 🤔. <code>&lt;CheckoutButton /&gt;</code> 컴포넌트와 <code>&lt;CardDescription /&gt;</code>컴포넌트는 여전히 렌더링되기 때문에 사용자는 항목을 보고 결제를 시도할 수 있지만, 신용카드 정보를 다 입력하지 않았다면 어떻게 될까요? 만약 <code>&lt;CreditCardInput /&gt;</code> 컴포넌트에서 장애가 발생하기 전에 신용카드 정보를 입력했다면 그 상태는 유지될까요? 사용자가 결제를 시도하면 무슨 일이 일어날까요?</p>
<p>아마도 이 컴포넌트들의 작성자조차도 이 상황에서 어떤 일이 일어날지 확신하지 못할 겁니다. 하물며 사용자야 말할 것도 없죠. 이는 사용자에게 혼란스럽고 실망스러울 수 있습니다.</p>
<h3 id="generic-error-boundary-generic-fallback">Generic Error Boundary, Generic Fallback</h3>
<p>이 상황이 얼마나 혼란스럽고 답답한지는 <em>fallback으로 무엇을 렌더링하느냐</em> 에 따라 달라집니다. 만약 컴포넌트가 경고 없이 화면에서 갑자기 사라진다면, 대부분의 사용자들은 매우 혼란스러울 것입니다.</p>
<p>만약 그렇지 않다면, 아마도 공통 fallback UI를 사용하고 있을 겁니다. 아마도, sad face 😭 와 함께 에러에 대한 유용한 정보를 제공할 것입니다. 없는 것보다는 낫지만, <em>모든 컴포넌트를 error boundary로 감싼다면</em> fallback이 가능한 모든 UI 요소에 맞게 적절하게 렌더링되어야 한다는 의미입니다. 그러나 이는 거의 불가능에 가깝습니다. 왜냐하면 각각의 요소들은 서로 다른 레이아웃 요구 사항을 가지고 있기 때문이죠. page-level의 section(예: header)에 적합한 fallback은 작은 아이콘 버튼에 적합하지 않을 수 있으며, 그 반대의 경우도 마찬가지입니다.</p>
<p>문제의 핵심: <strong>모든 컴포넌트를 error boundary로 감싸는 것은 사용자에게 혼란스럽고 불완전한 사용자 경험을 초래할 수 있습니다.</strong> 애플리케이션이 일관성 없는 상태로 변해, 사용자를 혼란스럽게 할 수 있습니다. 흥미로운 점은, 이러한 &quot;망가진 상태&quot;를 피하는 것이 바로 <a href="https://legacy.reactjs.org/blog/2017/07/26/error-handling-in-react-16.html#new-behavior-for-uncaught-errors">error boundary가 존재하는 주요 이유 중 하나</a>라는 것입니다.</p>
<blockquote>
<p>이 결정을 두고 많은 고민을 했지만, 우리의 경험상 망가진 UI를 그대로 남겨두는 것보다 완전히 제거하는 것이 더 나은 선택이라고 판단했습니다.</p>
</blockquote>
<blockquote>
<p>🏎 <strong>성능 저하</strong>
Error Boundaries는 어느 정도의 오버헤드를 수반하기 때문에, 남용할 경우 성능에 부정적인 영향을 미칠 수 있습니다. 하지만 이것은 error boundary를 <strong>모든 곳</strong>에 사용할 때만 문제가 되므로, 이 때문에 error boundary 사용을 두려워할 필요는 없습니다.</p>
</blockquote>
<h2 id="the-right-amount-of-error-boundaries">The Right Amount of Error Boundaries</h2>
<p>정리하자면, error boundaries가 너무 적으면 불필요하게 애플리케이션의 더 많은 부분이 중단되고, 너무 많으면 <strong>망가진 UI state</strong>로 이어질 수 있습니다. 그렇다면 error boundaries는 몇 개가 적당할까요?</p>
<p>이는 애플리케이션에 따라 다르기 때문에 정확한 숫자를 말하기는 어렵습니다. 제가 찾은 최선의 방법은 애플리케이션의 <strong>feature boundaries</strong>를 식별한 후, 그 경계 지점에 error boundaries를 배치하는 것입니다.</p>
<h2 id="finding-features">Finding Features</h2>
<p>임의의 애플리케이션을 보고 그 경계를 식별하는 데 사용할 수 있는 &#39;기능&#39;에 대한 보편적인 정의는 없습니다. <a href="https://en.wikipedia.org/wiki/I_know_it_when_I_see_it">&quot;보면 안다&quot;</a>는 것이 일반적으로 우리가 할 수 있는 최선이지만, 참고할 수 있는 몇 가지 공통적인 패턴이 있습니다.</p>
<p>대부분의 애플리케이션은 개별 섹션들이 모여서 구성됩니다. 예를 들어 header, navigation, main content, sidebars, footers 등이 있죠. 이 각각의 섹션은 전체적인 사용자 경험에 기여하면서도 어느 정도 독립성을 유지합니다.</p>
<p>트위터(현 X)를 예로 들어 살펴보겠습니다:</p>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/d762372d-d226-4f8c-a321-38405e0540d8/image.png" alt="twitter"></p>
<p>한눈에 봐도 페이지에는 분명히 구분된 섹션들과 기능들이 존재합니다. 트윗을 보여주는 메인 타임라인, 팔로우 추천, 트렌드 섹션, 네비게이션 바가 있습니다. 이 섹션들의 레이아웃과 스타일링만 봐도 섹션 간의 분리가 있다는 것을 알 수 있으며, 이는 error boundaries를 배치하기에 좋은 시작점입니다. <strong>시각적으로 독립적인 섹션은 기능적으로도 독립적일 가능성이 크고, 그런 곳에 error boundaries를 두는 것이 적절합니다.</strong></p>
<p>만약 여러 섹션 중 하나의 섹션의 특정 컴포넌트에서 에러가 발생한다면, 다른 섹션까지 같이 중단되지 않아야 한다는 것은 타당해보입니다. 예를 들어, 팔로우 추천 섹션에서 팔로우 버튼이 장애를 일으켜도 메인 타임라인까지 멈추게 해서는 안 됩니다.</p>
<h2 id="a-recursive-line-of-questioning">A Recursive Line of Questioning</h2>
<p>UI는 종종 <a href="https://en.wikipedia.org/wiki/Recursion_(computer_science)">재귀적 구조</a>를 가집니다. 페이지 레벨에서는 사이드바나 타임라인 같은 큰 섹션들이 있지만, 이 섹션들 내부에도 헤더나 리스트 같은 하위 섹션들이 있고, 그 안에도 또 다른 섹션들이 있습니다.</p>
<p>error boundaries를 어디에 배치할지 고민할 때, 스스로에게 던질 좋은 질문은 <strong>&quot;이 컴포넌트에서 발생한 에러가 형제 컴포넌트들에게 어떤 영향을 미치는가?&quot;</strong> 입니다. <CheckoutForm /> 예시에서 우리가 고려했던 질문도 바로 이것이었죠: 만약 CreditCardInput 이 실패하면 CheckoutButton과 CardDescription은 어떤 영향을 받을까요?</p>
<p>이 질문을 재귀적으로 컴포넌트 트리에 적용하면, 각 feature boundires를 쉽게 식별할 수 있고, 그 경계에 맞춰 error boundaries를 배치할 수 있습니다.</p>
<h3 id="twitter-deep-dive-the-page">Twitter Deep Dive: The Page</h3>
<p>다시 트위터를 예로 들어 이 접근 방식이 어떻게 작동하는지 살펴보겠습니다. 최상단부터 시작해서 팔로우 추천 섹션까지 자세히 파고들어 보겠습니다.</p>
<blockquote>
<p>이 분석에는 제가 이 기능들이 어떻게 작동하는지에 대한 인식과 기대를 바탕으로 한 의견과 편견이 많이 포함되어 있습니다. 이는 &quot;정답&quot;을 제시하려는 것이 아니라 과정 자체에 대한 감을 잡기 위한 것입니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/db587fe4-677e-438b-9758-40b60f2ebf75/image.png" alt=""></p>
<p>우리는 최상단부터 시작해서 세 가지 메인 컨텐츠 섹션을 확인할 수 있습니다: <strong>Home</strong>, <strong>Trends for you</strong>, 그리고 <strong>Who to follow</strong>. 이제 <strong>Who to follow</strong> 섹션을 자세히 살펴보겠습니다.</p>
<p>먼저 스스로에게 질문합니다:</p>
<blockquote>
<p><strong>이 컴포넌트에서 발생한 에러가 형제 컴포넌트들에게 어떤 영향을 미치는가?</strong></p>
</blockquote>
<p>이 질문을 더 구체적으로 바꿔보면:</p>
<blockquote>
<p><strong>이 컴포넌트가 고장나면 형제 컴포넌트도 고장나야 할까?</strong></p>
</blockquote>
<p>만약 <strong>Who to follow</strong> 섹션에서 장애로 인해 멈췄을 때 <strong>Home</strong>과 <strong>Trends for you</strong> 섹션도 함께 멈춰야 하는지 고민해보자면, 저는 명확하게 아니라고 생각합니다. 각 섹션은 서로 의존하지 않기 때문에, 이곳에 error boundary를 배치하는 것은 적절한 선택입니다.</p>
<h3 id="twitter-deep-dive-who-to-follow">Twitter Deep Dive: Who To Follow</h3>
<p>이제 같은 질문을 <strong>Who to follow</strong> 섹션에도 적용해 봅니다.</p>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/7b6623b2-d80c-4ae9-87b3-01b5364f45ca/image.png" alt=""></p>
<p><strong>Who to follow</strong> 섹션을 보면, 명확하게 구분된 세 개의 섹션이 있습니다: <strong>제목</strong>, <strong>팔로우할 사용자 목록</strong>, 그리고 <strong>더 보기 버튼</strong>. 이제 사용자 목록을 자세히 들여다보며 동일한 질문을 다시 던져봅니다: <strong>만약 팔로워 목록에 장애가 발생한다면, 제목과 더 보기 버튼도 함께 장애가 발생해야 할까요?</strong> 이번 경우는 조금 덜 명확하지만, 제 생각에는 역시 그럴 필요가 없을 것 같습니다. 제목을 유지한다고 해서 문제가 될 건 없고, <strong>더 보기</strong> 버튼은 작동 중일 수 있는 다른 페이지로 연결되기 때문입니다. 그래서 팔로워 목록에 또 하나의 error boundary를 추가하는 것이 좋겠습니다!</p>
<h3 id="twitter-deep-dive-follower-recommendation">Twitter Deep Dive: Follower Recommendation</h3>
<p>한 번 더 해봅시다. 이번에는 팔로우 추천 섹션을 살펴보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/e262a668-84e9-4570-b71f-61c0da1a19b5/image.png" alt=""></p>
<p>여기에는 두 가지 섹션만 있으니 간단하게 질문할 수 있습니다: <strong>만약 사용자의 이름과 고유 사용자명(@)이 장애를 일으킨다면, 팔로우 버튼도 함께 장애가 발생해야 할까요? 반대의 경우에는요?</strong></p>
<p>저는 <strong>그렇다</strong>라고 생각됩니다! 사용자의 이름과 고유 사용자명(@)이 사라지면 우리가 누구를 팔로우하려는지 알 수 없게 됩니다. 그리고 팔로우 버튼이 사라지면 사용자는 추천만 받고 아무런 행동을 할 수 없기 때문에 불만족스러운 경험이 될 수 있습니다.</p>
<h1 id="testing-your-fault-tolerance">Testing your Fault Tolerance</h1>
<p>이제 error boundaries와 Fault Tolerance에 대해 좀 더 알게 되었으니, 제가 이 과정에서 가장 좋아하는 부분을 하나 공유하고 싶습니다: 바로 <strong>이 모든 것을 테스트하는 방법</strong>입니다. 제가 발견한 애플리케이션의 Fault Tolerance를 테스트하는 가장 쉽고 간단한 방법은 <strong>일부러 에러를 일으키는 것</strong>입니다.</p>
<pre><code class="language-jsx">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>
<p>이건 제가 새로운 컴포넌트를 추가할 때마다 했던 작업인데, 애플리케이션이 장애를 어떻게 처리하는지 확인하는 데 정말 유용했습니다. 다만, throw 문을 사용해서 일부러 장애를 발생시킨 코드를 커밋하지 않도록 주의하세요 🙂</p>
<blockquote>
<p>🤔 <strong>참고: 카오스 엔지니어링 (Chaos Engineering)</strong><br>의도적으로 에러를 발생시켜 fault tolerance를 테스트하는 것은 <strong><a href="https://en.wikipedia.org/wiki/Chaos_engineering">카오스 엔지니어링</a></strong> 의 매우 가벼운 예시입니다. 리액트 커뮤니티에서도 이 개념을 활용한 도구들이 더 많이 생겼으면 좋겠어요.
만약 <strong>React.ChaosMode</strong> 같은 것이 있어서, 랜덤으로 일부 컴포넌트들을 고장 내서 fault tolerance를 테스트할 수 있다면 어떨까요?</p>
</blockquote>
<h2 id="summary">Summary</h2>
<p>모든 내용을 요약하자면, 다음과 같습니다:</p>
<ul>
<li><p><strong>최상단에 단일 error boundary만 두는 것은 피하세요.</strong> 이런 방식이 장애를 처리하는 최선의 방법인 경우는 드뭅니다.</p>
</li>
<li><p><strong>error boundaries를 과도하게 사용하지 마세요.</strong> 이것은 사용자 경험을 저하시키고 성능에 악영향을 미칠 수 있습니다.</p>
</li>
<li><p><strong>애플리케이션의 feature boundaries를 식별하고, 그곳에 error boudaries를 배치하세요.</strong> 리액트 앱은 트리 구조로 되어 있으니, 상단에서 시작해 하위로 내려가며 error boundary를 추가하는 것이 좋은 접근법입니다.</p>
</li>
<li><p><strong>&quot;이 컴포넌트가 고장나면 형제 컴포넌트도 고장나야 할까?&quot;라는 질문을 재귀적으로 던져보세요.</strong> 이 방법은 feature boundaries를 찾는 데 좋은 기준이 됩니다.</p>
</li>
<li><p><strong>에러 상태를 사전에 고려하여 애플리케이션을 설계하세요.</strong> feature boundaries에 error boudaries를 배치하면, 사용자에게 문제 발생을 알리고, 사용자 친화적인 fallback UI를 제공하기가 쉬워집니다. 또한, 기능별로 재시도 로직을 구현하면 사용자가 페이지 전체를 새로고침하지 않고도 해당 섹션만 새로고침할 수 있습니다.</p>
</li>
<li><p><strong>일부러 에러를 발생시키고, 어떤 일이 일어나는지 확인해 보세요</strong></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Server-Sent Events(SSE)를 활용한 실시간 알림 테스트 코드 및 기능 구현]]></title>
            <link>https://velog.io/@cloud_oort/Server-Sent-EventsSSE%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%A6%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%B0%8F-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@cloud_oort/Server-Sent-EventsSSE%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%A6%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%B0%8F-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Thu, 12 Sep 2024 03:39:40 GMT</pubDate>
            <description><![CDATA[<h2 id="목적">목적</h2>
<p>실시간 알림 기능을 구현하기 위해 서버와의 실시간 통신을 구축한다.</p>
<h2 id="실시간-통신-방법">실시간 통신 방법</h2>
<h3 id="polling">Polling</h3>
<p>클라이언트가 주기적으로 서버에 요청을 보내는 방식.</p>
<h4 id="장점">장점</h4>
<ul>
<li>구현이 간편하고 추가적인 서버 설정이 필요 없다.</li>
<li>일정하게 갱신되는 서버 데이터의 경우 유용하게 사용할 수 있는 방식이다.<h4 id="단점">단점</h4>
</li>
<li>주기적인 요청으로 발생할 수 있는 불필요한 요청과 응답으로 인해 서버에 부하가 생길 수 있다.</li>
<li>요청을 보내는 주기 만큼의 지연이 발생하여 실시간 데이터를 기대하기 힘들다.</li>
<li>주기가 짧아질수록 서버 및 네트워크 부하가 높아질 수 있다.</li>
<li>HTTP 통신 시 매번 보내는 요청과 응답 헤더의 크기가 불필요하게 크다.<h4 id="사용-예시">사용 예시</h4>
일정 시간 간격으로 데이터를 업데이트하는 경우에 유용하다.
ex) 실시간 야구 문자 중계, 뉴스 헤드라인<h3 id="long-polling">Long Polling</h3>
클라이언트가 서버에 요청을 보내면, 서버는 클라이언트의 요청에 바로 응답하지 않고 업데이트가 발생할 때까지 기다린다. 
업데이트가 발생하거나 설정된 시간이 지나면 서버는 응답을 보내고 클라이언트는 응답을 받아  연결을 종료한 뒤 곧바로 다시 요청을 보내 다음 응답을 기다린다.</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>polling 보다 데이터의 업데이트에 반응하는 속도가 빨라질 수 있고 서버의 부담이 줄어들 수 있다.</li>
<li>실시간성이 필요한 적은 수의 클라이언트와 연결되어 있는 경우 사용하는 것이 좋다.<h4 id="단점-1">단점</h4>
</li>
<li>클라이언트의 요청 주기가 짧다면 일반 polling과 별 차이가 없으며, 다수의 클라이언트에게 동시에 이벤트가 발생될 경우 한 번에 응답을 하고 요청을 다시 보내야 하기 때문에 서버에 순간적인 부담이 가해질 수 있다.</li>
<li>HTTP 통신 시 매번 보내는 요청과 응답 헤더의 크기가 불필요하게 크다.<h4 id="사용-예시-1">사용 예시</h4>
ex) 적은 인원의 채팅 앱, 알림<h3 id="websocket">WebSocket</h3>
웹소켓 프로토콜을 사용하면 클라이언트와 서버가 지속적인 연결을 통해 서로가 원할 때 데이터를 주고 받을 수 있다. 
즉 웹소켓은 데이터의 송수신을 동시에 처리하는 양방향 통신 기법이다.</li>
</ul>
<p>기존 HTTP 요청 응답 방식은 요청한 그 클라이언트에만 응답이 가능했는데, ws 프로토콜을 통해 웹소켓 포트에 접속해있는 모든 클라이언트에게 이벤트 방식으로 응답할 수 있다.</p>
<p>프레임으로 구성된 메시지라는 논리적 단위로 송수신하며, 메시지에 포함될 수 있는 교환 가능한 메시지는 텍스트와 바이너리이다.</p>
<h4 id="장점-2">장점</h4>
<ul>
<li>최초 접속시 HTTP 프로토콜을 통해 연결하기 때문에 별도의 포트 없이 기존의 포트(HTTP: 80, HTTPS: 443) 를 사용할 수 있어 추가 방화벽을 열지 않아도 양방향 통신이 가능하고 HTTP 규격인 CORS 적용, 인증 등을 기존과 동일하게 보장받을 수 있다.</li>
<li>기존 HTTP 통신과 달리 Connection을 지속하므로 요청 및 응답 헤더와 같은 연결 비용을 줄일 수 있다.<h4 id="단점-2">단점</h4>
</li>
<li>연결을 계속 유지하는 만큼 서버의 부하가 생길 수 있고, 많은 사용자들이 동시에 접속해 있을 수록 유지해야 하는 TCP 연결이 많아져 메시지들이 오가는 빈도가 높다면 네트워크 대역폭과 CPU의 사용량 또한 증가할 수 있다.</li>
<li>서버의 설계에 따라 구현이 복잡해질 수 있다. 특히 로드밸런싱이 적용된 서버에서는 이를 위해 고려하고 설정할 부분이 많아진다.</li>
<li>HTTP와는 다른 프로토콜이기 때문에 처음 사용할 때 구현 및 학습 난이도가 높다.</li>
<li>HTML5 이후에 등장했기 때문에, 이전의 기술로 구현된 서비스에서는 Socket.io와 같은 라이브러리를 사용해야 한다.</li>
<li>웹소켓은 문자열들을 주고 받을 수 있게 해줄 뿐 그 이상의 일은 하지 못한다.<h4 id="사용-예시-2">사용 예시</h4>
주식, 채팅 등 연속된 데이터를 빠르게 노출해야 하는 실시간 서비스 분야에 활용된다.
ex) 온라인 게임, 실시간 협업 도구에서 여러 사용자의 동시 문서 편집, 화상 채팅 및 회의에서 실시간 데이터 송수신<h3 id="sseserver-sent-event">SSE(Server Sent Event)</h3>
SSE를 사용하면 서버에서 클라이언트로의 <strong>실시간 단방향 통신</strong>을 할 수 있다.</li>
</ul>
<p>한 번의 HTTP 요청을 통해 연결이 이루어지면, 그 이후로 별도의 요청 없이 실시간으로 서버에서 클라이언트로 데이터를 송신할 수 있다.</p>
<p>메시지에 포함될 수 있는 교환 가능한 메시지는 텍스트이다.</p>
<h4 id="장점-3">장점</h4>
<ul>
<li>HTML5 표준 기술로서 모든 브라우저가 지원한다.(IE는 사라졌으니 말이다.)</li>
<li>HTTP 프로토콜 기반으로 동작하기 때문에 학습 및 구현 난이도가 비교적 낮다.</li>
<li>자동 재연결 기능이 있다.(기본값은 3000ms)<h4 id="단점-3">단점</h4>
</li>
<li>단방향 통신이다.</li>
<li>HTTP/1.1의 경우 브라우저당 6개의 접속만을 허가하며 HTTP/2에서는 100개까지의 접속을 허용한다.<h4 id="사용-예시-3">사용 예시</h4>
ex) 주식 거래, 뉴스 피드, 실시간 알림<h2 id="sse-선택-이유">SSE 선택 이유</h2>
</li>
</ul>
<p>기능에 따라 양방향 통신이 필요하지 않은 경우도 많다. 예를 들어, 서버에서 시간이 걸리는 작업의 진행 상태를 보여주는 진행 바(progress bar), 실시간 소식을 전하는 SNS, 뉴스, 주식 거래 서비스, 또는 기타 실시간 모니터링과 같은 경우가 그렇다.</p>
<p>내가 구현하려는 실시간 알림 기능 역시 양방향 통신이 불필요하다. 클라이언트가 지속적으로 데이터를 보낼 필요 없이, 서버에서 업데이트가 발생할 때만 실시간으로 데이터를 보내주면 충분하기 때문이다.</p>
<blockquote>
<p>즉, 단방향 통신으로도 충분한 서비스에는 SSE가 적합하고, 양방향 통신이 필수적인 경우에는 웹소켓을 사용하는 것이 더 적절하다.</p>
</blockquote>
<h2 id="sse-동작-및-사용-방법">SSE 동작 및 사용 방법</h2>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/9713c4c7-36a3-4715-abe6-ec309954685c/image.png" alt=""></p>
<h3 id="동작-순서">동작 순서</h3>
<ul>
<li>클라이언트가 서버에 SSE로 통신하자는 요청을 보낸다.</li>
<li>서버는 이 요청을 수신하고 수락했음을 알리는 메시지를 보낸다.</li>
<li>클라이언트는 이를 받고, 서버가 보내주는 데이터들에 반응할 준비를 한다.</li>
<li>이 시점부터 서버는 정해진 이벤트가 있을 때마다 클라이언트에게 메시지를 보낸다. (단방향 통신이기 때문에 클라이언트는 이에 응답을 할 수는 없다.)</li>
<li>클라이언트는 서버로부터 메시지가 도착할 때마다 이에 반응하여 필요한 작업을 한다.</li>
<li>이 과정들은 하나의 연결 안에서 계속 이루어지고, 만약 연결이 끊기면 클라이언트는 자동으로 재연결을 요청하여 통신을 재개한다.</li>
<li>필요 작업을 마치면 클라이언트 또는 서버에서 상대방에게 종료를 통보하는 메시지를 보냄으로써 연결이 끝나게 된다.<h3 id="사용-방법">사용 방법</h3>
</li>
<li>클라이언트가 브라우저일 경우, SSE 연결 요청을 위해 Web API의 <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource"> EventSource 인터페이스</a>를 사용한다.</li>
<li>클라이언트는 지정된 URL로 서버에게 요청을 보낸다.<ul>
<li>SSE 통신을 통해 이벤트 스트림 타입의 메시지를 수신하겠다는 의미의 헤더가 실린다.</li>
<li><code>Accept: text/event-stream</code></li>
</ul>
</li>
<li>이를 수신한 서버는 이벤트 스트림 타입의 메시지임을 명시하는 헤더를 실은 응답을 보낸다.<ul>
<li><code>Content-Type: text/event-stream</code></li>
<li>연결을 계속 유지한다는 항목도 실린다. 이 헤더 덕분에 클라이언트가 요청을 보내면서 만들어진 연결이 서버가 응답을 보낸 이후로도 계속 유지되는 것이다.</li>
<li><code>Connection: keep-alive</code></li>
<li>서버의 첫 번째 응답 이후로는 메시지에 HTTP 헤더가 실릴 필요가 없게 된다.</li>
</ul>
</li>
<li>서버는 지정된 이벤트가 발생할 때마다 실시간으로 클라이언트에게 메시지를 보내고 클라이언트에서는 각 메시지를 받아 특정 작업을 실행한다.</li>
<li>서버에서는 이벤스 소스에 이벤트 이름을 기입하여 토픽 기반으로 여러 이벤트를 발행할 수 있고, 클라이언트에서는 이벤트 리스너 설정을 통해 해당 토픽 이벤트를 구독할 수 있다.</li>
<li>EventSource는 retry를 사용하여 자동으로 재접속할 수 있다.<ul>
<li>클라이언트가 따로 구현하지 않아도 연결이 비정상적으로 끊길 때마다 자동으로 연결 요청을 보낸다.</li>
<li>연결이 끊어질 시 재접속을 몇 ms 후에 시도할 지 지정할 수 있고, 기본값은 3000ms이다.</li>
<li>메시지에 id를 지정할 수도 있는데, 연결이 끊어졌다가 재개될 경우 클라이언트가 마지막에 받은 id를 Last-Event-ID 헤더에 담아 요청에 실어보냄으로써 서버는 그 다음에 해당하는 메시지부터 보낼 수 있다.</li>
</ul>
</li>
<li>클라이언트에서 연결 종료할 때는 EventSource 객체의 close 메서드를 호출한다.</li>
<li>서버에서 연결 종료할 때는 전송을 중단하거나 합의된 메시지를 보내서 연결을 통지한다.<ul>
<li>서버에서 이벤트 소스의 이름을 end로 하고, 클라이언트에서는 end의 이벤트를 실행할 때 close 메서드를 호출하는 방법이 있다.<h3 id="클라이언트-예시-코드">클라이언트 예시 코드</h3>
</li>
</ul>
</li>
</ul>
<pre><code class="language-javascript">import React, { useEffect, useState } from &#39;react&#39;;

function Notifications() {
    const [messages, setMessages] = useState([]);

    useEffect(() =&gt; {
        // 서버의 SSE 엔드포인트를 설정.
        const eventSource = new EventSource(&#39;http://서버주소/sse-endpoint&#39;);

        // onmessage는 이벤트 이름을 따로 명시하지 않은 모든 데이터를 처리한다.
        eventSource.onmessage = function(event) {
            const newMessage = JSON.parse(event.data);  // 이벤트 데이터는 JSON 형식일 수 있습니다.
            setMessages(prevMessages =&gt; [...prevMessages, newMessage]);
        };

        // 연결 성공시 실행
        eventSource.onopen = function() {}

        // 연결이 끊기거나 에러 발생시 실행
        eventSource.onerror = function() {}

        // 이벤트 이름을 명시한 메시지를 받을 때 사용
          // 아래 예시는 이벤트 이름이 customEvent인 메시지를 처리하는 경우
        eventSource.addEventListenr(&#39;customEvent&#39;, function(e) {})

        // 컴포넌트 언마운트 시 EventSource 연결 종료.
        return () =&gt; {
            eventSource.close();
        };
    }, []);

    return (
        &lt;div&gt;
            &lt;h2&gt;알림&lt;/h2&gt;
            &lt;ul&gt;
                {messages.map((msg, index) =&gt; (
                    &lt;li key={index}&gt;{msg}&lt;/li&gt;
                ))}
            &lt;/ul&gt;
        &lt;/div&gt;
    );
}

export default Notifications;</code></pre>
<h1 id="테스트-코드-및-기능-구현">테스트 코드 및 기능 구현</h1>
<h2 id="설계-과정">설계 과정</h2>
<p><a href="https://velog.io/@cloud_oort/FE-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%9D%98-%EB%AA%A9%EC%A0%81%EA%B3%BC-%EB%B0%A9%ED%96%A5#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4">[FE] 기능 테스트와 TDD 및 테스트 코드 작성 가이드라인</a> 참고</p>
<ol>
<li>실사간 알림 기능이 제공하는 서비스에 대한 use case coverage 작성.</li>
<li>MSW 사용하여 API mocking.</li>
<li>테스트 환경 설정 및 코드 작성.</li>
<li>기능 구현.<h2 id="use-case-coverage-작성">use case coverage 작성</h2>
</li>
</ol>
<ul>
<li><input disabled="" type="checkbox"> 실시간 알림이 도착하면 전체 화면에 알림 팝업이 발생하고, 그 안에 제목, 메시지, 버튼이 존재한다.</li>
<li><input disabled="" type="checkbox"> 팝업이 발생하고 바깥 배경을 클릭하면 팝업이 사라진다.</li>
<li><input disabled="" type="checkbox"> 팝업 버튼이 확인 버튼인 경우 버튼 클릭시 팝업이 사라진다.</li>
<li><input disabled="" type="checkbox"> 팝업 버튼이 아닌 확인 버튼이 아닌 경우, 버튼 클릭시 특정 화면으로 이동한다.</li>
<li><input disabled="" type="checkbox"> 알림 확인 도중 또 다른 실시간 알림 발생시 팝업을 닫자마자 새로운 팝업이 발생한다.</li>
</ul>
<h2 id="api-mocking">API mocking.</h2>
<p>실시간 알림 테스트를 위해 서버로부터 특정 시간 후에 메시지를 수신하는 상황을 구현하고자 한다.</p>
<p>그러나 실제 API 요청을 통한 테스트는 API 개발이 완료된 후에만 가능하므로, 백엔드 개발과 병렬적으로 진행하기 어렵다는 문제가 있다. 또한 테스트 과정에서 불필요한 요청으로 인한 비용 발생과 API 의존성 문제도 고려해야 한다.</p>
<p>그래서 실제 API 요청을 통한 테스트가 아니라 네트워크 수준에서 mocking한 API를 이용하여 테스트하고자 한다. 이를 위해 MSW 라이브러리를 사용한다.</p>
<h3 id="어떤-api를-mocking해야-하는가">어떤 API를 mocking해야 하는가?</h3>
<p>EventSource API는 클라이언트가 서버로부터 지속적인 데이터를 받는 것을 처리한다. 서버가 클라이언트에 이벤트를 계속해서 푸시하는 방식인데, 이 방식을 테스트 환경에서 mocking해야 한다.</p>
<p>실제 서버는 지속적으로 데이터를 전송해 주지만, mocking 상황에서는 실제 서버가 없기 때문에 서버가 데이터를 스트리밍하는 것을 흉내내야 한다.</p>
<p>MSW 공식문서에서 data streaming을 mocking하는 <a href="https://mswjs.io/docs/recipes/streaming/">recipe</a>를 제공하고 있는데, 그 방법으로 ReadableStream을 말한다.</p>
<h3 id="readablestream을-사용하여-api-mocking">ReadableStream을 사용하여 API mocking</h3>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Streams_API">Streams API</a>의 <a href="https://developer.mozilla.org/ko/docs/Web/API/ReadableStream">ReadableStream</a> 인터페이스는 바이트 데이터를 읽을 수 있는 Stream을 제공한다.</p>
<p>즉 EventSource API가 클라이언트에서 서버의 데이터를 실시간으로 받는 역할이라면, 서버에서 데이터를 생성하고 전달하는 역할은 ReadableStream API가 한다.</p>
<ol>
<li>백엔드 개발자와 EventSource 메시지 이벤트 이름과 메시지 타입을 합의하여 mock data 생성.</li>
<li>ReadableStream 생성자를 통해 응답값으로 Stream을 생성.</li>
<li>TextEncoder의 encode 메서드를 이용하여 문자열 데이터를 UTF-8 형식의 바이트 데이터로 변환.</li>
<li>controller의 enqueue 메서드를 이용하여 바이트 데이터를 Stream에 저장.</li>
<li>Stream을 <code>Content-Type: text/event-stream</code> 헤더와 함께 클라이언트에 응답.</li>
</ol>
<p>실제 서버처럼 시간차를 두고 데이터를 순차적으로 전송할 수 있는 흐름을 제어하기 위해 setTimeout을 이용하여 1초, 4초, 7초 마다 메시지가 도착하도록 만들었다.</p>
<pre><code class="language-tsx">import { HttpHandler, HttpResponse, http } from &#39;msw&#39;;

import { API_END_POINT } from &#39;@/constants/api&#39;;

const realTimeNotificationData = [
  &#39;id:1_1722845403085\nevent:notification\ndata:{ &quot;notificationId&quot;: 1, &quot;message&quot;: &quot;경매에 올린 test가 낙찰되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_SUCCESS&quot;, &quot;auctionId&quot;: 59}\n\n&#39;,
  &#39;id:1_1722845403090\nevent:notification\ndata:{ &quot;notificationId&quot;: 2, &quot;message&quot;: &quot;경매에 올린 test가 미낙찰되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_FAILURE&quot;}\n\n&#39;,
  &#39;id:1_1722845403175\nevent:notification\ndata:{ &quot;notificationId&quot;: 3, &quot;message&quot;: &quot;축하합니다! 입찰에 참여한 경매 test의 낙찰자로 선정되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_WINNER&quot;, &quot;auctionId&quot;: 62}\n\n&#39;,
];

const encoder = new TextEncoder();

export const realTimeNotificationsHandler: HttpHandler = http.get(
  `${API_END_POINT.REALTIME_NOTIFICATIONS}`,
  () =&gt; { 
    const stream = new ReadableStream({
      start(controller) {
        realTimeNotificationData.forEach((message, idx) =&gt; {
          setTimeout(() =&gt; {
            controller.enqueue(encoder.encode(message));
          }, [1000, 4000, 7000][idx]);
        });
      },
    });

    return new HttpResponse(stream, {
      headers: {
        &#39;Content-Type&#39;: &#39;text/event-stream&#39;,
      },
    });
  },
);
</code></pre>
<h2 id="테스트-환경-설정-및-코드-작성">테스트 환경 설정 및 코드 작성</h2>
<h3 id="테스트-환경-설정">테스트 환경 설정</h3>
<h4 id="rendering-component">rendering component</h4>
<p>어느 화면에서나 실시간 알림이 발생해야 하기 때문에 모든 페이지가 공유하는 <code>GlobalLayout</code> 컴포넌트를 작성하고 해당 컴포넌트에서 SSE 작업을 수행한다.</p>
<h4 id="setup-함수-정의">setup 함수 정의</h4>
<p>render 메서드와 userEvent는 거의 매번 사용하기 때문에 setup 함수를 만들어 사용한다.</p>
<pre><code class="language-tsx">const setup = () =&gt; {
    const utils = render(&lt;GlobalLayout /&gt;);
    const user = userEvent.setup();

    return {
      user,
      ...utils,
    };
};</code></pre>
<h4 id="eventsource-api-mocking">EventSource API mocking</h4>
<p>EventSource는 브라우저의 내장 객체로, 브라우저가 아닌 Node.js 테스트 환경에는 존재하지 않는다. 그래서 테스트를 실행해보면 <code>EventSource is not defined</code> 에러가 발생한다.</p>
<p>Node.js에서는 브라우저의 window 객체 대신 global 객체가 사용되는데, 테스트 환경에서 EventSource와 같은 브라우저 전용 객체를 사용하기 위해서는 EventSource를 mocking하여 global 객체에 직접 추가해줘야 한다.</p>
<p>아래 설정을 통해, 테스트 환경에서 <code>EventSource</code>를 사용하는 코드가 실제 브라우저의 <code>EventSource</code> 대신 우리가 정의한 <code>EventSource</code>를 사용하게 된다.</p>
<p>또한 addEventListener를 조작하여 내가 원하는 이벤트에서 원하는 데이터가 원하는 시간 후에 메시지를 수신하도록 만들었다.</p>
<p>나는 <code>EventSource</code> 객체를 <code>GlobalLayout</code> 컴포넌트 테스트 파일에서만 사용하기 때문에 해당 테스트 파일에서 정의하여 사용 범위를 제한했다.</p>
<pre><code class="language-tsx">const realTimeNotificationData = [
  &#39;id:1_1722845403085\nevent:notification\ndata:{ &quot;notificationId&quot;: 1, &quot;message&quot;: &quot;경매에 올린 test가 낙찰되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_SUCCESS&quot;, &quot;auctionId&quot;: 59}\n\n&#39;,
  &#39;id:1_1722845403090\nevent:notification\ndata:{ &quot;notificationId&quot;: 2, &quot;message&quot;: &quot;경매에 올린 test가 미낙찰되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_FAILURE&quot;}\n\n&#39;,
  &#39;id:1_1722845403175\nevent:notification\ndata:{ &quot;notificationId&quot;: 3, &quot;message&quot;: &quot;축하합니다! 입찰에 참여한 경매 test의 낙찰자로 선정되었습니다.&quot;, &quot;type&quot;: &quot;AUCTION_WINNER&quot;, &quot;auctionId&quot;: 62}\n\n&#39;,
];

describe(&#39;Layout 알림 테스트&#39;, () =&gt; {
  // 테스트 시작 전에 EventSource 모킹
  beforeAll(() =&gt; {
    global.EventSource = vi.fn(() =&gt; ({
      addEventListener: vi.fn((event, callback) =&gt; {
        if (event === &#39;notification&#39;) {
          realTimeNotificationData.forEach((messageString, idx) =&gt; {
            // data 값 추출하고 callback이 처리.
            const dataMatch = messageString.match(/data:(.*)\n/);
            if (dataMatch &amp;&amp; dataMatch[1]) {
              const message = JSON.parse(dataMatch[1].trim()); // 문자열을 객체로 변환
              setTimeout(() =&gt; {
                callback({ data: JSON.stringify(message) }); // JSON 문자열로 다시 전송
              }, [1000, 4000, 7000][idx]);
            }
          });
        }
      }),
      close: vi.fn(),
    })) as unknown as typeof EventSource; // 타입스크립트가 global.EventSource의 타입이 브라우저의 EventSource 타입과 같다고 간주한다.
  });
}</code></pre>
<h3 id="테스트-코드-작성">테스트 코드 작성</h3>
<h4 id="실시간-알림이-도착하면-전체-화면에-알림-팝업이-발생하고-그-안에-제목-메시지-버튼이-존재한다">실시간 알림이 도착하면 전체 화면에 알림 팝업이 발생하고, 그 안에 제목, 메시지, 버튼이 존재한다.</h4>
<pre><code class="language-tsx">test(&#39;실시간 알림이 도착하면 전체 화면에 알림 팝업이 발생하고, 그 안에 제목, 메시지, 버튼이 존재한다.&#39;, async () =&gt; {
    render(&lt;GlobalLayout /&gt;);

    // 1초 후에 발생하는 popup을 찾기 timeout을 1100ms로 설정한다.
    const popup = await screen.findByLabelText(
      /알림 박스/,
      {},
      { timeout: 1100 },
    );

    const title = screen.getByRole(&#39;heading&#39;, { name: /제목/ });
    const message = screen.getByLabelText(/메시지/);
    const button = screen.getByRole(&#39;button&#39;, {
      name: /경매 참여자 목록 보러가기/,
    });

    expect(popup).toContainElement(title);
    expect(popup).toContainElement(message);
    expect(popup).toContainElement(button);
  });</code></pre>
<h4 id="팝업이-발생하고-바깥-배경을-클릭하면-팝업이-사라진다">팝업이 발생하고 바깥 배경을 클릭하면 팝업이 사라진다.</h4>
<pre><code class="language-tsx">test(&#39;팝업이 발생하고 바깥 배경을 클릭하면 팝업이 사라진다.&#39;, async () =&gt; {
    const { user } = setup();

    const popup = await screen.findByLabelText(
      /알림 박스/,
      {},
      { timeout: 1100 },
    );

    const popupBackground = screen.getByLabelText(&#39;팝업 배경&#39;);
    await user.click(popupBackground);

    expect(popup).not.toBeInTheDocument();
  });</code></pre>
<h4 id="팝업-버튼이-확인-버튼인-경우-버튼-클릭시-팝업이-사라진다">팝업 버튼이 확인 버튼인 경우 버튼 클릭시 팝업이 사라진다.</h4>
<p>mock 데이터에서 두 번째 알림의 경우 버튼의 종류가 확인 버튼이다.
확인 버튼을 테스트하기 위해 두 번째 알림을 기다리려면 최소 4초를 기다리는 과정이 필요하다.</p>
<p>처음에 나는 findByLabelText의 timeout의 기능을 바로 알지 못해 5000ms로 설정을 해놓고 두 번째 알림을 기다렸다. 하지만 내가 원하는 두 번째 알림이 아닌 첫 번째 알림을 찾았다.</p>
<p>그 이유는 findByLabelText의 timeout의 기능은 설정한 시간 내에 가장 먼저 찾은 요소를 선택하기 때문이다.</p>
<p>그래서 4초 후에 오는 두 번째 알림을 받으려면 우선 1초 후에 오는 메시지를 찾고, setTimeout으로 3초 간 기다린다음 작업을 해야 한다.</p>
<p>여기서 일반적인 setTimeout을 이용하면 안된다. setTimeout은 비동기작업이기 때문에 리액트의 상태 변화나 DOM 작업을 기다리지 않기 때문이다.</p>
<p>이를 해결하기 위해 act와 Promise를 결합하여 setTimeout을 사용한다.
RTL의 act 함수는 리액트 내의 상태나 DOM이 업데이트되는 동안 발생하는 사이드 이펙트를 묶어서 처리한다. act는 비동기 함수도 지원하는데, 비동기 함수 내의 비동기 작업이 완료될 때까지 기다린다.</p>
<p>기본 테스트 시간을 넘어가는 경우, test의 timeout 시간을 넉넉히 잡아 테스트를 실행한다.</p>
<pre><code class="language-tsx"> test(
      &#39;팝업 버튼이 확인 버튼인 경우 버튼 클릭시 팝업이 사라진다.&#39;,
      { timeout: 8000 },
      async () =&gt; {
        const { user } = setup();

        // 첫 번째 알림을 찾고, 알림을 닫는다.
        const popup = await screen.findByLabelText(
          /알림 박스/,
          {},
          { timeout: 1100 },
        );
        const popupBackground = screen.getByLabelText(&#39;팝업 배경&#39;);
        await user.click(popupBackground);

        await act(async () =&gt; {
          await new Promise((resolve) =&gt; {
            setTimeout(resolve, 3000);
          });
        });

        // 두 번째 알림을 찾아 동작을 수행.
        const button = screen.getByRole(&#39;button&#39;, {
          name: /확인/,
        });
        await user.click(button);

        expect(popup).not.toBeInTheDocument();
      },
    );</code></pre>
<h4 id="팝업-버튼이-아닌-확인-버튼이-아닌-경우-버튼-클릭시-특정-화면으로-이동한다">팝업 버튼이 아닌 확인 버튼이 아닌 경우, 버튼 클릭시 특정 화면으로 이동한다.</h4>
<p>render component에서 useNavigate hook을 사용하는 경우 useNavigate mocking이 필수이다.</p>
<p>나는 다른 테스트에서도 useNavigate hook을 사용했기 때문에 setupTests.ts 파일에서 mocking하고 export하여 모든 테스트 파일에서 사용했다.</p>
<pre><code class="language-tsx">// setupTests.ts
export const mockedUseNavigate = vi.fn();
vi.mock(&#39;react-router-dom&#39;, async () =&gt; {
  // importActual은 실제 모듈의 원래 구현을 가져오는 기능을 한다.
  // 일부 기능은 실제 동작을 그대로 유지하면서 useNavigate만 모킹하려는 경우에 유용하다.
  const mod =
    await vi.importActual&lt;typeof import(&#39;react-router-dom&#39;)&gt;(
      &#39;react-router-dom&#39;,
    );
  return {
    ...mod,
    // useNavigate 훅이 호출될 때마다 mockedUseNavigate 라는 모의 함수가 호출된다.
    useNavigate: () =&gt; mockedUseNavigate,
  };
});</code></pre>
<p>mocking한 useNavigate를 import하고 mockedUseNavigate가 실행되었음을 확인한다.</p>
<pre><code class="language-tsx">import { mockedUseNavigate } from &#39;@/setupTests&#39;;

test(&#39;확인 버튼이 아닌 알림인 경우, 버튼 클릭시 특정 화면으로 이동한다.&#39;, async () =&gt; {
      const { user } = setup();

      await screen.findByLabelText(/알림 박스/, {}, { timeout: 1100 });

      const button = screen.getByRole(&#39;button&#39;, {
        name: /경매 참여자 목록 보러가기/,
      });
      await user.click(button);

      expect(mockedUseNavigate).toHaveBeenCalledOnce();
});</code></pre>
<h4 id="알림-확인-도중-또-다른-실시간-알림-발생시-팝업을-닫자마자-새로운-팝업이-발생한다">알림 확인 도중 또 다른 실시간 알림 발생시 팝업을 닫자마자 새로운 팝업이 발생한다.</h4>
<pre><code class="language-tsx">test(
    &#39;알림 확인 도중 또 다른 실시간 알림 발생시 팝업을 닫자마자 새로운 팝업이 발생한다.&#39;,
    { timeout: 8000 },
    async () =&gt; {
      const { user } = setup();

      await screen.findByLabelText(/알림 박스/, {}, { timeout: 1100 });

      await act(async () =&gt; {
        await new Promise((resolve) =&gt; {
          setTimeout(resolve, 3000);
        });
      });

      const button = screen.getByRole(&#39;button&#39;, {
        name: /경매 참여자 목록 보러가기/,
      });
      await user.click(button);

      const box = await screen.findByLabelText(
        /알림 박스/,
        {},
        { timeout: 1100 },
      );
      expect(box).toBeInTheDocument();
    },
  );</code></pre>
<h3 id="기능-구현">기능 구현</h3>
<h4 id="sse-연결-및-custom-hook-작성">SSE 연결 및 custom hook 작성</h4>
<p>useEffect 내에서 SSE 연결을 하고 notification event로 메시지를 수신하여 notifications 배열 상태에 저장한다.</p>
<p>위에서 mocking한 API handler와 동일한 URL로 요청을 하면 MSW가 이를 감지하여 내가 설정한 데이터를 전송해준다.</p>
<p>간단한 작업이기 때문에 useSSE hook을 만들어주었다.</p>
<pre><code class="language-tsx">import { useEffect, useState } from &#39;react&#39;;

export const useSSE = &lt;T&gt;(url: string) =&gt; {
  const [state, setState] = useState&lt;T[]&gt;([]);

  useEffect(() =&gt; {
    const eventSource = new EventSource(url);

    eventSource.onopen = () =&gt; {

    };
    eventSource.onerror = (error) =&gt; {

    };

    eventSource.addEventListener(&#39;notification&#39;, (e) =&gt; {
      const data = JSON.parse(e.data);
      setState((prev) =&gt; [...prev, data]);
    });

    return () =&gt; eventSource.close();
  }, [url]);

  return { state, setState };
};</code></pre>
<h4 id="알림이-발생시-팝업-발생">알림이 발생시 팝업 발생</h4>
<h5 id="목표">목표</h5>
<ul>
<li>알림이 발생하면 팝업이 발생하고, 바깥이나 버튼을 클릭하면 팝업을 닫는다.</li>
<li>팝업 읽는 도중 새로운 알림이 발생하면, 팝업을 닫자마자 새로운 팝업이 생성된다.<h5 id="구현">구현</h5>
</li>
</ul>
<ol>
<li>currentNotification 변수 생성</li>
<li>useSSE가 리턴한 notifications 배열과 currentNotification을 dependency로 가지는 useEffect 생성</li>
<li>currentNotification이 null이면서 notificaions 값이 있을 때 currentNotification 값을 notifications의 첫번째 값으로 설정하고 notifications의 첫번째 값을 제거.</li>
</ol>
<pre><code class="language-tsx">// GlobalLayout
import { useEffect, useState } from &#39;react&#39;;

import { API_END_POINT } from &#39;@/constants/api&#39;;
import { Outlet } from &#39;react-router-dom&#39;;
import Popup from &#39;../common/Popup&#39;;
import RealTimeNotification from &#39;./RealTimeNotification&#39;;
import type { RealTimeNotificationType } from &#39;Notification&#39;;
import { useSSE } from &#39;@/hooks/useSSE&#39;;

const GlobalLayout = () =&gt; {
  const { state: notifications, setState: setNotifications } =
    useSSE&lt;RealTimeNotificationType&gt;(`${API_END_POINT.REALTIME_NOTIFICATIONS}`);

  const [currentNotification, setCurrentNotification] =
    useState&lt;RealTimeNotificationType | null&gt;(null);

  const closePopup = () =&gt; {
    setCurrentNotification(null);
  };

  useEffect(() =&gt; {
    const showNextNotification = () =&gt; {
      setCurrentNotification(notifications[0]);
      setNotifications((prev) =&gt; prev.slice(1));
    };
    if (currentNotification === null &amp;&amp; notifications.length &gt; 0) {
      showNextNotification();
    }
  }, [currentNotification, notifications, setNotifications]);

  return (
    &lt;div className=&quot;flex justify-center w-full h-screen&quot;&gt;
      &lt;div className=&quot;relative w-[46rem] min-w-[23rem] h-full&quot;&gt;
        &lt;Outlet /&gt;
        {currentNotification &amp;&amp; (
          &lt;Popup onClose={closePopup}&gt;
            &lt;RealTimeNotification
              onClose={closePopup}
              notification={currentNotification}
            /&gt;
          &lt;/Popup&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default GlobalLayout;</code></pre>
<h4 id="알림-팝업">알림 팝업</h4>
<p>테스트 코드를 보면 getByRole, getByLabelText 등의 접근성 handler로 요소를 찾는 것을 알 수 있다.</p>
<p>RTL(React Testing Library)은 어플리케이션의 접근성을 높이고 사용자가 원하는 방식으로 구성요소를 사용하는 방식에 가까운 테스트를 수행할 수 있도록 하여 테스트를 통해 실제 사용자가 어플리케이션을 사용할 때 어플리케이션이 작동할 것이라는 확신을 주는 것을 목표로 하기 때문이다.</p>
<p>이를 위해 팝업 코드 작성시 적절한 role을 사용하고 aria-label을 작성하는 등 접근성 향상에 공을 들여야 한다.</p>
<ul>
<li>특정 role이 없는 경우 aria-label을 명시하여 해당 tag가 무슨 역할을 하는 지 명확히 한다.</li>
<li>제목은 heading tag를 사용한다.</li>
<li>이미지를 사용하는 경우 img 태그를 사용하고 alt 속성을 반드시 명시한다.</li>
<li>버튼을 사용하는 경우 button 태그를 사용한다.</li>
</ul>
<pre><code class="language-tsx">// 코드를 간소화하여 role과 접근성만 강조했다.
const Popup = () =&gt; {
  return createPortal(
    &lt;div&gt;
      &lt;div aria-label=&quot;팝업 배경&quot;&gt;
        &lt;div aria-label=&quot;알림 박스&quot; &gt;
          &lt;div&gt;
            &lt;h2 aria-label=&quot;알림 제목&quot; &gt;
              {title}
            &lt;/h2&gt;
            &lt;div aria-label=&quot;알림 메시지&quot; &gt;
              {message}
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;Button
            ariaLabel={buttonName}
            type=&quot;button&quot;
            hoverColor=&quot;black&quot;
            className=&quot;w-full py-3&quot;
            color=&quot;cheeseYellow&quot;
          &gt;
            {buttonName}
          &lt;/Button&gt;
        &lt;/div&gt;      
      &lt;/div&gt;
    &lt;/div&gt;,
    document.body,
  );
};</code></pre>
<h2 id="출처-및-참고-자료">출처 및 참고 자료</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">MDN EventSource</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/ReadableStream">MDN ReadbleStream</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/Streams_API">MDN Streams API</a>
<a href="https://mswjs.io/docs/recipes/streaming/">MSW Streaming</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events">Using server-sent events</a>
<a href="https://www.youtube.com/watch?v=Fjj4ZmLlrP8">테코톡 주드 SSE</a>
<a href="https://velog.io/@green9930/%EC%8B%A4%EC%A0%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-React%EC%99%80-SSE">[실전 프로젝트] React와 SSE</a>
<a href="https://www.youtube.com/watch?v=i4-MNzNML_c">SSE 얄코</a>
<a href="https://chaeyoung2.tistory.com/135">React SSE 실시간 알림 구현하기 (폴리필 문제해결)</a> 
<a href="https://yooneeee.tistory.com/101">리액트 실시간 알림 SSE 헤더에 토큰 담아 보내기</a>
<a href="https://yceffort.kr/2020/11/server-side-events">서버사이드이벤트(SSE)</a>
<a href="https://boxfoxs.tistory.com/403">실시간 서버 데이터 구독하기</a>
<a href="https://ko.javascript.info/server-sent-events">javascriptInfo SSE</a>
<a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Polling-Long-Polling-Server-Sent-Event-WebSocket-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC">polling / long polling / SSE / websocket 정리</a>
<a href="https://velog.io/@apparatus1/sse">SSE(Server Sent Event)로 실시간 알림 받아오기</a>
<a href="https://rladydqls99.tistory.com/m/86">SSE 실시간 알림 기능 구현</a>
<a href="https://gilssang97.tistory.com/69">알림 기능을 구현해보자 - SSE(Server-Sent-Events)!</a>
<a href="https://www.youtube.com/watch?v=2oMPf-ueQic">웹소켓을 알아봅시다</a>
<a href="https://www.youtube.com/watch?v=MPQHvwPxDUw">테코톡 코일 웹소켓</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 비동기 작업의 선언적 처리: Error boundary 및 Suspense 그리고 대수적 효과]]></title>
            <link>https://velog.io/@cloud_oort/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%9D%98-%EC%84%A0%EC%96%B8%EC%A0%81-%EC%B2%98%EB%A6%AC-Error-boundary-%EB%B0%8F-Suspense-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8C%80%EC%88%98%EC%A0%81-%ED%9A%A8%EA%B3%BC</link>
            <guid>https://velog.io/@cloud_oort/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%9D%98-%EC%84%A0%EC%96%B8%EC%A0%81-%EC%B2%98%EB%A6%AC-Error-boundary-%EB%B0%8F-Suspense-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%8C%80%EC%88%98%EC%A0%81-%ED%9A%A8%EA%B3%BC</guid>
            <pubDate>Tue, 27 Aug 2024 01:52:05 GMT</pubDate>
            <description><![CDATA[<h1 id="전통적인-비동기-작업-처리-방법">전통적인 비동기 작업 처리 방법</h1>
<h2 id="fetch-on-render">Fetch-on-render</h2>
<p>컴포넌트가 화면에 렌더링 된 후에, useEffect 혹은 ComponentDidMount 등의 라이프사이클 메서드를 활용해서 비동기 작업을 처리한다.</p>
<pre><code class="language-jsx">function ProfilePage() {
  const [user, setUser] = useState(null);

  useEffect(() =&gt; {
    fetchUser().then(u =&gt; setUser(u));
  }, []);

  if (user === null) {
    return &lt;p&gt;Loading profile...&lt;/p&gt;;
  }
  return (
    &lt;&gt;
      &lt;h1&gt;{user.name}&lt;/h1&gt;
      &lt;ProfileTimeline /&gt;
    &lt;/&gt;
  );
}

function ProfileTimeline() {
  const [posts, setPosts] = useState(null);

  useEffect(() =&gt; {
    fetchPosts().then(p =&gt; setPosts(p));
  }, []);

  if (posts === null) {
    return &lt;h2&gt;Loading posts...&lt;/h2&gt;;
  }
  return (
    &lt;ul&gt;
      {posts.map(post =&gt; (
        &lt;li key={post.id}&gt;{post.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<h3 id="실행-순서">실행 순서</h3>
<ul>
<li>ProfilePage 렌더링.</li>
<li>fetchUser 비동기 작업 시작.</li>
<li>fetchUser 비동기 작업 끝.</li>
<li>ProfileTimeline 렌더링.</li>
<li>fetchPost 비동기 작업 시작.</li>
<li>fetchPost 비동기 작업 끝.</li>
</ul>
<h3 id="문제점">문제점</h3>
<h4 id="warterfall-문제">warterfall 문제</h4>
<p>위 방법은 매우 직관적인 방법이지만 Warterfall 문제를 야기한다.
Warterfall은 이전 fetch 요청에 대한 응답이 와야 다음 fetch 요청을 보낼 수 있는 구조를 말한다.
각 작업이 3초가 걸린다면, 총 6초가 지나야 모든 비동기 작업이 완료된 화면을 볼 수 있다.</p>
<h4 id="상태-분기-처리-문제">상태 분기 처리 문제</h4>
<p>현재 비동기 작업이 하나기 때문에 하나의 로딩 상태만을 다루지만,
비동기 작업이 2개 이상으로 늘어나고, 에러 상태도 처리하게 되는 순간 하나의 컴포넌트에서 너무 많은 일을 하게 되어 비즈니스 로직을 파악하기 힘들다.</p>
<h2 id="fetch-then-render">Fetch-then-render</h2>
<p>비동기 작업을 한 번에 처리하고, 모두 완료한 후 화면을 렌더링한다.
여러 비동기 작업을 한 번에 처리함으로써 Warterfall 문제를 방지할 수 있다.</p>
<pre><code class="language-jsx">// Kick off fetching as early as possible
const promise = fetchProfileData();

function ProfilePage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);

  useEffect(() =&gt; {
    promise.then(data =&gt; {
      setUser(data.user);
      setPosts(data.posts);
    });
  }, []);

  if (user === null) {
    return &lt;p&gt;Loading profile...&lt;/p&gt;;
  }
  return (
    &lt;&gt;
      &lt;h1&gt;{user.name}&lt;/h1&gt;
      &lt;ProfileTimeline posts={posts} /&gt;
    &lt;/&gt;
  );
}

// The child doesn&#39;t trigger fetching anymore
function ProfileTimeline({ posts }) {
  if (posts === null) {
    return &lt;h2&gt;Loading posts...&lt;/h2&gt;;
  }
  return (
    &lt;ul&gt;
      {posts.map(post =&gt; (
        &lt;li key={post.id}&gt;{post.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<h3 id="실행-순서-1">실행 순서</h3>
<ul>
<li>ProfilePage 렌더링.</li>
<li>user data fetching 시작.</li>
<li>posts data fetching 시작.</li>
<li>user data fetching 끝.</li>
<li>posts data fetching 끝.</li>
<li>ProfilePage 리렌더링.</li>
</ul>
<h3 id="문제점-1">문제점</h3>
<h4 id="최종-렌더링-지연">최종 렌더링 지연</h4>
<p>Warterfall 문제는 해결했지만, 모든 비동기 작업이 완료될 때까지 ProfilePage는 렌더링하지 못한다.</p>
<h4 id="컴포넌트-비대화">컴포넌트 비대화</h4>
<p>ProfilePage와 ProfileTimeline 컴포넌트에서 처리할 데이터와 로딩 상태를 모두 하나의 컴포넌트에서 처리하기 때문에 ProfilePage 컴포넌트가 비대화되는 문제가 발생한다.</p>
<p>코드의 가독성과 유지보수성이 떨어지며 비즈니스 로직을 파악하기 힘들어지는 문제가 발생한다.</p>
<h1 id="비동기-작업의-선언적-처리">비동기 작업의 선언적 처리</h1>
<h2 id="리액트의-선언적-프로그래밍">리액트의 선언적 프로그래밍</h2>
<p>리액트의 특징 중 하나는 선언적 프로그래밍을 할 수 있다는 점이다.
선언적 프로그래밍이 가능하다는 것은 추상화 레벨이 높은 프로그래밍이 가능하다는 의미이기도 하다.</p>
<p>추상화에는 설계자와 사용자가 존재한다.
구체적인 기능 구현은 설계자의 책임이고, 기능 사용자는 구체적인 기능 구현을 알 필요 없이 원하는 기능을 사용하기만 하면 된다.</p>
<p>즉 리액트 사용자는 어떤 UI를 렌더링할 것인지에만 집중하고 이를 어떻게 렌더링할 것인지에 대한 책임은 설계자 역할인 리액트에게 넘긴다.</p>
<p>리액트 공식문서의 <a href="https://ko.legacy.reactjs.org/docs/design-principles.html#scheduling">Design Principles</a>에 다음과 같은 문구가 있다.</p>
<blockquote>
<p>개발자는 함수로 컴포넌트를 작성할 때, <strong>무엇을 렌더링할 것인지</strong>에 대한 설명에만 집중하고 <strong>해당 컴포넌트를 렌더하고 DOM에 적용하는 것은 리액트의 책임</strong>이므로 함수 호출에는 관여하지 않는다.</p>
</blockquote>
<p>이러한 추상화로 인해 리액트 사용자인 개발자는 어떤 화면을 렌더링할 것인지를 고민하는 데에 더 많은 시간을 사용할 수 있으며, 이 화면을 어떻게 렌더링해야 할 지에 대한 고민은 리액트에게 온전히 맡길 수 있다.</p>
<p>그리고 우리는 Suspense와 ErrorBoundary를 사용하여 비동기 작업 또한 선언적으로 처리할 수 있다.
<strong>데이터가 로딩되었는지를 판단하는 책임은 Suspense</strong>에 맡기고, <strong>에러에 대한 책임을 ErrorBoundary</strong>에 맡김으로써, <strong>컴포넌트는 정상적으로 데이터가 로딩되었을 때의 UI 선언에만 집중</strong>할 수 있다.</p>
<h2 id="render-as-you-fetchwith-suspense">Render-as-you-fetch(with Suspense)</h2>
<p>Render-as-you-fetch 방법은 비동기 작업과 렌더링을 동시에 시작한다. 
Suspense는 모든 비동기 작업이 완료되기 전까지 fallback UI를 렌더링하고, 비동기 작업의 완료를 알아채면 중단 시점부터 컴포넌트를 렌더링한다.</p>
<p>즉 비동기 작업의 상태가 로딩상태인지 판단할 필요없이 Suspense에 fallback UI를 전달하여 로딩 상태에 따른 렌더링을 선언적으로 처리할 수 있다.</p>
<pre><code class="language-jsx">// 리액트 공식문서에서는 비동기 작업을 아래와 같이 promise로 wrapping 하여 상위 Suspense 컴포넌트와 소통한다.
// 컨셉 코드이기 때문에 상위 Suspense와 소통하는 개념에 집중하자.
function fetchUser() {
  console.log(&quot;fetch user...&quot;);
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      console.log(&quot;fetched user&quot;);
    }, 1000);
  });
}

function fetchPosts() {
  console.log(&quot;fetch posts...&quot;);
  return new Promise((resolve) =&gt; {
    setTimeout(() =&gt; {
      console.log(&quot;fetched posts&quot;);
    }, 1100);
  });
}

export function fetchProfileData() {
  let userPromise = fetchUser();
  let postsPromise = fetchPosts();
  return {
    user: wrapPromise(userPromise),
    posts: wrapPromise(postsPromise)
  };
}

// Suspense를 사용하기 위해서는 promise 객체로 인자를 받아 상태에 따라 리턴값을 달리해주는 함수이다.
// 구현체가 복잡하지만 tanstack query와 같은 비동기 통신 라이브러리에서 Suspense 기능을 쉽게 사용할 수 있도록 도와준다.
function wrapPromise(promise) {
  let status = &quot;pending&quot;;
  let result;
  let suspender = promise.then(
    (r) =&gt; {
      status = &quot;success&quot;;
      result = r;
    },
    (e) =&gt; {
      status = &quot;error&quot;;
      result = e;
    }
  );
  return {
    read() {
      if (status === &quot;pending&quot;) {
        throw suspender;
      } else if (status === &quot;error&quot;) {
        throw result;
      } else if (status === &quot;success&quot;) {
        return result;
      }
    }
  };
}


const resource = fetchProfileData();

function ProfilePage() {
  return (
    &lt;Suspense fallback={&lt;h1&gt;Loading profile...&lt;/h1&gt;}&gt;
      &lt;ProfileDetails /&gt;
      &lt;Suspense fallback={&lt;h1&gt;Loading posts...&lt;/h1&gt;}&gt;
        &lt;ProfileTimeline /&gt;
      &lt;/Suspense&gt;
    &lt;/Suspense&gt;
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return &lt;h1&gt;{user.name}&lt;/h1&gt;;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    &lt;ul&gt;
      {posts.map(post =&gt; (
        &lt;li key={post.id}&gt;{post.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<h3 id="실행-순서-2">실행 순서</h3>
<ul>
<li>비동기 작업과 렌더링 동시에 시작.</li>
<li>비동기 작업 완료.</li>
<li>리렌더링.</li>
</ul>
<h3 id="비동기-작업과-렌더링을-동시에-시작할-수-있는-이유">비동기 작업과 렌더링을 동시에 시작할 수 있는 이유</h3>
<p>모든 비동기 작업이 완료되지 않아 Suspense가 fallback UI를 보여줄 때, 리액트는 hidden 모드로 자식 컴포넌트를 렌더하게 되며 이로 인해 부모 컴포넌트가 아직 로딩중이더라도 자식 컴포넌트는 자신의 비동기 작업을 Concurrent하게 수행할 수 있다.</p>
<p>때문에 부모 컴포넌트와 자식 컴포넌트의 비동기 작업과 렌더링을 동시에 할 수 있다.</p>
<h3 id="suspense-동작-원리">Suspense 동작 원리</h3>
<p>Suspense를 사용하면 비동기 작업의 로딩 상태를 판단할 필요 없이 로딩 상태에 따른 렌더링을 Suspense가 책임진다고 앞서 말했다.</p>
<p>그렇다면 Suspense는 어떻게 자식 컴포넌트의 비동기 작업에 대한 상태를 확인하는 것일까.
Suspense가 동작하는 방식은 다음과 같다.</p>
<ol>
<li>render method에서 캐시로부터 값을 읽기를 시도한다.</li>
<li>value가 캐시되어 있으면 정상적으로 render 한다.</li>
<li>value가 캐시되어 있지 않으면 캐시는 promise를 throw 한다.</li>
<li>해당 promise는 가까운 상위 Suspense 컴포넌트로 전파되고, Suspense는 이 promise를 받아 fallback UI를 렌더링한다.</li>
<li>promise가 resolve되면 리액트는 promise를 throw 한 곳으로부터 재시작한다.</li>
</ol>
<p>앞서 본 ProfilePage 예제에 적용하면 다음과 같다.</p>
<ol>
<li>Suspense가 ProfilePage를 render 할 때 캐시로부터 데이터를 읽으려고 시도한다.</li>
<li>데이터가 없으므로 ProfilePage의 캐시는 promise를 throw 한다.</li>
<li>Suspense는 이 promise를 받아 fallback UI를 render 하고, 정상적으로 resolve 되었다면 다시 ProfilePage를 render 한다.</li>
</ol>
<p>즉 비동기 작업 중일 때 컴포넌트는 가장 가까운 상위 Suspense에 promise를 throw 한고, 비동기 작업이 완료되면(Promise가 resolve되면) Suspense는 fallback UI를 render 하는 것을 멈추고 다시 정상적으로 컴포넌트를 rener 한다.</p>
<p>Suspense의 핵심은 <strong>자식 컴포넌트의 비동기 작업에서 throw한 promise를 catch</strong>한다는 것이다.</p>
<h4 id="개념적-구현">개념적 구현</h4>
<p><a href="https://gist.github.com/sebmarkbage/2c7acb6210266045050632ea611aebee">Prev React Core Team Sebastian Markbåge Example</a></p>
<pre><code class="language-js">_// Infrastructure.js_  
let cache = new Map();  
let pending = new Map();

// 실제 url로부터 데이터를 fetch하는 함수.
// fetch 요청의 완료 여부에 따라 리턴하는 값이 다르다.
// 로딩 중이면 resolve되지 않은 promise를 리턴하고, resolve되면 set한 캐시의 value를 리턴한다.

// 동일한 함수를 동일한 url을 가지고 여러 번 호출했을 때 promise의 resolve 여부에 따라서 리턴하는 값이 다르다.
// 즉 promise를 throw 했을 때 Suspense에서 이를 잡아서 resolve될 때까지 fallbackUI를 보여주다가 resolve가 되면 다시 throw한 곳으로부터 정상적인 컴포넌트 로딩을 처리할 수 있게 된다.
function fetchTextSync(url) {  
  if (cache.has(url)) {  
    return cache.get(url);  
  }  
  if (pending.has(url)) {  
    throw pending.get(url);  
  }  
  let promise = fetch(url).then(  
    response =&gt; response.text()  
  ).then(  
    text =&gt; {  
      pending.delete(url);  
      cache.set(url, text);  
    }  
  );  
  pending.set(url, promise);  
  throw promise;  
}

async function runPureTask(task) {  
  for (;;) {  
    try {  
      return task();  
    } catch (x) {  
      if (x instanceof Promise) {  
        await x;  
      } else {  
        throw x;  
      }  
    }  
  }  
}</code></pre>
<h3 id="suspense와-대수적-효과algebraic-effects">Suspense와 대수적 효과(Algebraic Effects)</h3>
<p>Suspense가 사용하는 개념적인 모델은 데이터의 상태를 확인하면서 pending 상태인 경우에는 원하는 비동기 작업에 대한 <strong>promise를 throw</strong>해서 suspense가 fallback UI를 render 하도록 하고, success 상태인 경우에는 컴포넌트를 render 하도록 하는 방식이다.</p>
<p>이 모델 자체는 대수적 효과는 아니지만, 대수적 효과의 영향을 받은 기술이라고 한다.
대수적 효과가 무엇일까.</p>
<blockquote>
<p>Matija Pretnar의 “An Introduction to Algebraic Effects and Handlers Invited tutorial paper”에 의하면 대수적 효과란 <strong>exception throw 등을 포함하는 연산들의 집합으로부터 순수하지 못한 행위들이 발생할 수 있다는 전제를 바탕으로 computational effect에 대해 접근하는 방식</strong>을 의미한다.</p>
</blockquote>
<p>위의 정의는 상당히 추상화되어 있어 쉽게 풀어보면 다음과 같다.
대수적 효과란 <strong>대수를 사용해서 연산을 수행할 때, 순수하지 못한 행위들이 일어날 수 있음을 전제로 하고 computational effect에 대해 접근하는 방식</strong>을 말한다.</p>
<p>여기서 computational effect는 <strong>자신의 환경을 변경하는 모든 연산을 포함하는 개념</strong>이다. 예를 들어 로컬 함수에서 부모 환경으로 작업을 넘겨 처리하게 하고 처리가 끝나면 로컬 함수의 실행이 멈췄던 곳으로 다시 돌아와 작업을 하는 경우, 이는 computational effect가 발생한 것이다.</p>
<p><strong>작업을 넘긴다는 것은 로컬 함수에서 부모 컴포넌트로 promise를 throw하는 것으로 대치</strong>할 수 있으며, 이것이 바로 suspense가 promise의 pending 상태를 처리하는 방식과 동일하다.</p>
<p>다시 정리하면 대수적 효과는 <strong>서로 다른 환경(로컬 함수와 부모 환경)간의 상호작용에서 부수 효과가 발생할 수 있는데, 이를 적절한 handler를 통해 처리하는 접근 방식</strong>을 의미한다. </p>
<p>여기서 말하는 적절한 handler는 <strong>로컬함수에서 발생시킨 effect를 부모 환경에서 잡아 적절하게 처리한 뒤 다시 로컬 함수가 멈췄던 곳으로 실행 흐름을 되돌리는 역할</strong>을 한다. 이러한 handler가 바로 우리가 앞서 살펴본 Suspense 이다.</p>
<p>대수적 효과를 Suspense와 연관시켜 적용하면 다음과 같다.</p>
<blockquote>
<p>대수적 효과는 <strong>부모 컴포넌트(Suspense)와 자식 컴포넌트(비동기 작업 처리 컴포넌트) 간의 상호작용에서 발생한 side effect(throw promise)를 적절한 handler(catch promise in suspense)를 사용해 해결하는 접근 방식</strong>을 의미하는 것이고, 이 handler는 <strong>자식 컴포넌트에서 발생시킨 effect(throw promise)를 부모 컴포넌트(suspense)에서 잡아 적절하게 처리한 뒤, 다시 자식 컴포넌트가 멈췄던 곳으로 흐름을 되돌려주는 것</strong>을 말한다.</p>
</blockquote>
<p>대수적 효과는 어떤 코드 조각을 감싸는 맥락으로 side effect에 대한 책임을 분리하고, 분리된 책임에 대한 처리는 감싸는 맥락에서 처리하게 함으로써 &quot;무엇&quot;과 &quot;어떻게&quot;를 분리한다. 이렇게 책임을 분리하는 방식은 코드를 선언적으로 작성할 수 있는 관점을 제공하며 이는 앞서 말한 리액트의 설계 원칙과 동일하다.</p>
<p>즉 대수적 효과에서 영감을 받아 Suspense에게 비동기 작업의 로딩 상태에 대한 책임을 넘겼으며, 비동기 작업 처리 컴포넌트는 로딩 상태의 책임으로부터 벗어나 UI 선언에만 집중하도록 만들어줬다.</p>
<h2 id="errorboundary">ErrorBoundary</h2>
<p>ErrorBoundary는 리액트에서 에러를 선언적으로 처리하기 위해 제공하는 컴포넌트이다.
자식 컴포넌트에서 발생한 에러를 전달받아 핸들링하여 애플리케이션이 중단되는 것을 막는다.</p>
<p>ErrorBoundary 코드는 <a href="https://ko.legacy.reactjs.org/docs/error-boundaries.html">리액트 공식 문서</a>에 제공된 기본 코드를 참고하여 사용한다.</p>
<p>ErrorBoundary에서 핵심은<code>getDerivedStateFromError</code> 메서드와 <code>componentDidCatch</code> 메서드이며, 어느 코드든 이 두 메서드만 가지고 있으면 에러 바운더리가 될 수 있다.</p>
<h3 id="getderivedstatefromerror">getDerivedStateFromError</h3>
<ul>
<li>render 단계에서 실행된다. =&gt; 순수함수여야 한다.</li>
<li>자식 컴포넌트가 에러를 던질 때 호출된다.</li>
<li>fallback UI를 표시하기 위해 state를 업데이트해야 한다.<h3 id="componentdidcatch">componentDidCatch</h3>
</li>
<li>commit 단계에서 실행된다. =&gt; side effect 허용된다.</li>
<li>자식 컴포넌트가 에러를 던질 때 호출된다.</li>
<li>에러 로그를 기록할 때 사용된다.</li>
</ul>
<pre><code class="language-jsx">class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToMyService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }

    return this.props.children;
  }
}</code></pre>
<h3 id="사용-방법">사용 방법</h3>
<p>에러가 발생할 수 있는 컴포넌트를 ErrorBoundary 컴포넌트로 감싸서 사용한다.
필요에 따라 에러 발생시 사용자에게 보여줄 fallback UI를 props로 전달할 수 있다.</p>
<pre><code class="language-jsx">&lt;ErrorBoundary fallback={&lt;Error /&gt;}&gt;
  &lt;App /&gt;
&lt;/ErrorBoundary&gt;</code></pre>
<h3 id="errorboundary가-캐치하지-못하는-에러">ErrorBoundary가 캐치하지 못하는 에러</h3>
<p>ErrorBoundary가 모든 에러를 캐치할 수 있는 것은 아니다.
<a href="https://ko.legacy.reactjs.org/docs/error-boundaries.html">리액트 공식문서</a>에서 아래와 같은 에러는 ErrorBoundary가 캐치할 수 없다고 한다.</p>
<ul>
<li>이벤트 핸들러</li>
<li>비동기 작업(fetch API, setTimeout, requestAnimationFrame 콜백 등)</li>
<li>서버 사이드 렌더링</li>
<li>자식이 아닌 에러 바운더리 자체에서 발생하는 에러</li>
</ul>
<p>이는 자바스크립트와 리액트를 잘 안다면 당연한 결과라 생각할 수 있다.</p>
<p>자바스크립트에서는 코드가 실행되면 실행 컨텍스트가 생성되고, 자바스크립트 엔진의 콜스택에 순차적으로 쌓여 실행이 끝나면 pop이 되어 없어진다.</p>
<p>이때 하나의 컨텍스트에서 에러가 발생하면 에러는 상위 컨텍스트로 전파된다. 상위 컨텍스트에서 에러가 처리되지 않으면 최상위 실행 컨텍스트로 전파되고 전체 애플리케이션이 중돤된다.</p>
<pre><code class="language-js">try {
    function throwErrorFn(a) {
        throw new Error(&#39;error&#39;)
    }
    setTimeout(throwErrorFn, 1000);
} catch(e) {
    console.log(e)    
}</code></pre>
<p>위 예제를 보고 다시 정리하면 setTimeout 내의 throwErrorFn 콜백 함수는 1초 후에 실행 컨텍스트에서 실행된다. 하지만 그때는 이미 try-catch 문이 있는 실행 컨텍스트가 pop된 상황이기 때문에 catch할 수 없어 상단에 에러를 전파한다.</p>
<p>즉 try-catch 문의 컨텍스트 내에서 throwErrorFn 함수가 실행되지 않기 때문에 에러를 캐치할 수 없는 것이다.</p>
<p>ErrorBoundary 또한 try-catch와 동일한 원리로 동작한다.
<strong>ErrorBoundary 실행 컨텍스트안에서 에러가 발생한다면 캐치할 수 있고 그 안에 없다면 캐치할 수 없다.</strong> 매우 단순하다.</p>
<h4 id="이벤트-핸들러">이벤트 핸들러</h4>
<p>이벤트 핸들러 내에서 발생한 에러는 에러 바운더리가 캐치하지 못한다.
자바스크립트에서는 보통 <code>addEventListenr</code>를 이용하여 DOM node에 직접 이벤트를 등록한다.</p>
<pre><code class="language-js">document.getElementById(&quot;main_button&quot;).addEventListener(&quot;click&quot;, callbackFn)</code></pre>
<p>하지만 리액트에서의 모든 이벤트는 root element에서 핸들링된다.</p>
<pre><code class="language-js">getEventListeners(document.getElementById(&#39;root&#39;))</code></pre>
<p>위 코드를 콘솔에 찍어보면 모든 이벤트가 root에 등록되어 있는 것을 알 수 있다.</p>
<p>root는 최상위 Html tag이고, 이곳에서 모든 이벤트 핸들링이 발생한다. 
그렇기 때문에 이벤트 핸들링 과정에서 에러가 발생한다면, 이 코드는 ErrorBoundary 바깥에 있을 것이기 때문에 에러를 캐치하지 못한다.</p>
<p>이벤트 핸들러에서 에러를 핸들링하고 싶다면 try-catch 문을 사용하여 의도적으로 내부에서 에러를 캐치해야 한다.</p>
<h4 id="비동기-작업">비동기 작업</h4>
<p>이는 위에서 본 setTimeout 예제와 같은 예시이다.
setTimeout의 콜백함수는 1초 후에 실행되며, 이 시점은 이미 ErrorBoundary의 실행 컨텍스트가 끝난 시점이다.
그래서 ErrorBoundary는 비동기 작업의 에러를 캐치하지 못한다.</p>
<p>ErrorBoundary를 이용하여 비동기 작업의 에러를 처리하기 위해서는 별도의 에러 상태를 선언하고, 비동기 작업 내에서 에러 상태를 업데이트시킨다.
이를 통해 리렌더링이 발생하고 에러 바운더리 컨텍스트 내에서 에러를 동기적으로 throw 한다.</p>
<pre><code class="language-jsx">const [error, setError] = useState(null)

const handler = async () =&gt; {
 try {
     const response = await fetch(&#39;url&#39;)
 } catch(e) {
    setError(e)
 } 
}

if(error) throw error</code></pre>
<p>tanstack query와 같은 비동기 작업 라이브러리에서 제공해주는 throwOnError 옵션을 true로 사용하면 비동기 작업 실패시 에러를 던지기 때문에 별도의 상태 처리 없이 비동기 작업에서 발생하는 에러를 에러 바운더리에서 캐치할 수 있다.
<a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#the-useerrorboundary-option-has-been-renamed-to-throwonerror">tanstack query throwOnError</a></p>
<h4 id="서버-사이드-렌더링">서버 사이드 렌더링</h4>
<p>ErrorBoundary는 상태값을 변경하는 getDerivedStateFromError 메서드를 기반으로 동작한다.</p>
<p>해당 메서드는 에러 인자를 받아, 해당 ErrorBoundary에서 처리할 에러인지 판단하고 처리할 에러라면 <code>hasError</code>와 같은 상태값을 true로 변경시켜 fallback UI를 렌더링한다.</p>
<p>이와 같이 <strong>상태값을 변경하는 메서드는 상태 변화가 존재하는 클라이언트 환경에서만 실행</strong>되고 서버 환경에서는 실행되지 않기 때문에 서버 사이드 렌더링에서 발생하는 에러는 당연히 캐치할 수 없다.</p>
<h4 id="자식이-아닌-errorboundary-자체에서-발생하는-에러">자식이 아닌 ErrorBoundary 자체에서 발생하는 에러</h4>
<p>ErrorBoundary에서 에러를 받고 다시 에러를 throw 한다면 해당 에러는 상위 ErrorBoundary로 전파되기 때문에 ErrorBoundary 자체에서 발생하는 에러는 캐치할 수 없다.</p>
<h2 id="출처-및-참고자료">출처 및 참고자료</h2>
<p><a href="https://17.reactjs.org/docs/concurrent-mode-suspense.html">Suspense for data fetching react docs</a>
<a href="https://ko.legacy.reactjs.org/docs/error-boundaries.html">Error Boundary react docs</a>
<a href="https://www.youtube.com/watch?v=v69zRgDCjjs">보아즈, 에러 바운더리가 캐치, 캐치 못하는 에러</a>
<a href="https://www.youtube.com/watch?v=FvRtoViujGg">토스, 프론트엔드에서 우아하게 비동기 처리하기</a>
<a href="https://jooonho.dev/react/2022-12-03-js-error-errorboundary/">error와 error handling</a>
<a href="https://www.freecodecamp.org/news/effective-error-handling-in-react-applications/#how-to-implement-global-error-handling">리액트 앱이 에러를 처리하는 방법</a>
<a href="https://www.youtube.com/watch?v=89WK1xJ7CMU">테코톡, 아인노아 에러핸들링</a>
<a href="https://www.youtube.com/watch?v=hQekqGONSlY">테코톡, 밧드 에러핸들링</a>
<a href="https://www.youtube.com/watch?v=FXtooPhupr4">테코톡, 티케 에러핸들링</a>
<a href="https://www.youtube.com/watch?v=yQDsd1OVR08">테코톡 클린 서스펜스와 에러바운더리</a>
<a href="https://www.youtube.com/watch?v=o9JnT4sneAQ">유인동, 비동기 프로그래밍 및 실전 에러 핸들링</a>
<a href="https://www.youtube.com/watch?v=012IPbMX_y4">sentry를 이용한 에러 추적</a>
<a href="https://www.youtube.com/watch?v=_FuDMEgIy7I">Learn React Error Boundary In 7 Minutes</a>
<a href="https://www.jbee.io/articles/react/React%EC%97%90%EC%84%9C%20%EC%84%A0%EC%96%B8%EC%A0%81%EC%9C%BC%EB%A1%9C%20%EB%B9%84%EB%8F%99%EA%B8%B0%20%EB%8B%A4%EB%A3%A8%EA%B8%B0">React에서 선언적으로 비동기 다루기</a>
<a href="https://velog.io/@himprover/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC">프론트엔드 에러 핸들링에 대한 고민</a>
<a href="https://fe-developers.kakaoent.com/2022/221110-error-boundary/">React의 Error Boundary를 이용하여 효과적으로 에러 처리하기</a>
<a href="https://homebody-coder.tistory.com/entry/React-18%EC%9D%98-Suspense%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">React 18의 Suspense에 대해서 알아보자</a>
<a href="https://romantech.net/1233">React 대수적 효과</a>
<a href="https://blog.mathpresso.com/conceptual-model-of-react-suspense-a7454273f82e">Conceptual Model of React Suspense</a>
<a href="https://blog.mathpresso.com/algebraic-effects-of-react-suspense-157b49807ea0">Algebraic Effects of React Suspense</a>
<a href="https://sckimynwa.medium.com/suspense-deep-dive-code-implementation-e766582a7366">Suspense Deep Dive (Code Implementation)</a>
<a href="https://maxkim-j.github.io/posts/suspense-argibraic-effect/">Suspense for Data Fetching의 작동 원리와 컨셉 (feat.대수적 효과)</a>
<a href="https://gusrb3164.github.io/computer-science/2022/05/12/algebraic-effect/">추상화를 위한 대수적 효과(Algebraic Effect)와 React Suspense</a>
<a href="https://toss.tech/article/frontend-declarative-code">선언적인 코드 작성하기</a>
<a href="https://sckimynwa.medium.com/declarative-react-and-inversion-of-control-7b95f3fbddf5">Declarative React, and Inversion of Control</a>
<a href="https://velog.io/@seeh_h/suspense%EC%9D%98-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC">Suspense의 동작 원리</a>
<a href="https://happysisyphe.tistory.com/66">ErrorBoundary가 포착할 수 없는 에러와 그 이론적 원리 분석</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[FE] 기능 테스트와 TDD 및 테스트 코드 작성 가이드라인]]></title>
            <link>https://velog.io/@cloud_oort/FE-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%9D%98-%EB%AA%A9%EC%A0%81%EA%B3%BC-%EB%B0%A9%ED%96%A5</link>
            <guid>https://velog.io/@cloud_oort/FE-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%9D%98-%EB%AA%A9%EC%A0%81%EA%B3%BC-%EB%B0%A9%ED%96%A5</guid>
            <pubDate>Mon, 29 Jul 2024 12:00:04 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.zentao.pm/blog/things-you-should-know-about-functional-testing-826.html">썸네일 출처</a></p>
<h2 id="테스트-코드의-목적-및-종류">테스트 코드의 목적 및 종류</h2>
<h3 id="테스트란">테스트란?</h3>
<p>테스트는 우리가 작성한 코드가 예상대로 작동하는지 확인하는 과정을 의미한다.</p>
<p>가장 기본적이고 확실한 테스트 방법은 기능을 직접 사용하여 기능이 의도한대로 제대로 작동하는지 확인하는 것이다.
그러나 코드가 수정될 때마다 직접 테스트를 수행하는 것은 시간이 많이 걸리고, 실수할 가능성이 있다.</p>
<p>그래서 <strong>테스트 코드를 작성하여 테스트 과정을 자동화</strong>한다.</p>
<h3 id="프론트엔드에서의-테스트">프론트엔드에서의 테스트</h3>
<p>프론트엔드 테스트는 일반적인 테스트와 다르다. </p>
<p>비즈니스 로직을 담은 함수의 입출력(I/O)을 검증하거나 세부 구조 및 동작을 검증하는 것과 달리, 프론트엔드 테스트는 <strong>사용자 경험에 초점</strong>을 맞춘다.</p>
<p><strong>즉 사용자가 발생시키는 이벤트(click, scroll, focus 등)에 대한 화면의 반응을 확인하는 과정</strong>이다. 이를 통해 <strong>적절한 화면이 출력되었는지</strong> 테스트한다.</p>
<p>이벤트 트리거는 react testing library와 같은 도구를 사용해 쉽게 수행할 수 있지만, 출력된 화면을 확인하는 것은 간단하지 않다. 
사람이 직접 확인할 때는 화면이 기대와 동일한지 눈으로 확인하지만, 컴퓨터는 픽셀 단위로 비교해야 하며 이는 많은 시간을 소요하고 거짓 음성을 발생시킬 수 있다.</p>
<blockquote>
<p>거짓 음성(false nagetive)이란 실제 결과는 성공했지만 테스트는 실패한 경우이다.
이는 사람의 눈에는 원하는 화면이 출력되었지만 픽셀에서 차이가 발생하기 때문에 발생한다.</p>
</blockquote>
<p>따라서 픽셀 비교 대신 <strong>기능의 중요한 내용을 비교 단위로 삼는 방식</strong>이 존재한다. 
예를 들어, 버튼을 누르면 특정 텍스트가 추가되는지 확인하는 것이다. 
이는 픽셀 비교 방식보다는 정확성이 떨어질 수 있지만, 시간 소요와 거짓음성 문제를 해결할 수 있다.</p>
<p>픽셀 비교는 <strong>시각적 테스트</strong>, 기능 비교는 <strong>기능적 테스트</strong>라고 한다.</p>
<p>시각적 테스트에는 <strong>스냅샷 테스트</strong>와 <strong>시각 회귀 테스트</strong>가 있고,
기능적 테스트에는 <strong>단위 테스트</strong>, <strong>통합 테스트</strong>, <strong>E2E 테스트</strong>가 있다.</p>
<h3 id="테스트-코드의-목적">테스트 코드의 목적</h3>
<h4 id="자동화된-검증">자동화된 검증</h4>
<ul>
<li>신규 기능 개발 시, 이를 검증하기 위한 QA 과정에 많은 시간과 노력이 소요된다.</li>
<li>또한 기존 코드와 연관된 새로운 기능을 개발하거나 리팩토링할 때, 테스트 코드가 없으면 반복적인 QA 과정을 거쳐야 한다.</li>
<li>서비스의 안정성을 보장할 수 있는 테스트 코드가 있다면 QA 과정에서 에러 발생 확률이 줄어들고, 자유롭게 리팩토링하거나 새로운 기능을 추가할 수 있다.</li>
</ul>
<h4 id="제품에-대한-신뢰성-확보">제품에 대한 신뢰성 확보</h4>
<p>테스트 코드를 작성하기 전에 지원하는 기능을 명확히 정의함으로써 사용자가 사용하는 기능이 제대로 작동할 것이라는 신뢰성이 생긴다.</p>
<blockquote>
<p>TDD를 제안한 켄트 벡은 테스트 코드 작성을 불안함을 지루함으로 바꾸는 과정이라 했다.</p>
</blockquote>
<h4 id="협업-효율성-증가">협업 효율성 증가</h4>
<p>애플리케이션이 지원하는 기능을 명확히 정의하여 테스트 코드를 정의하기 때문에 팀원이 어떤 기능을 개발하고 테스트하는지 한 눈에 확인할 수 있다.
즉 <strong>테스트 코드 파일 자체가 잘 작성된 개발 문서</strong>로 활용될 수 있다. 이는 협업에 있어 소통의 안정성을 더해준다.</p>
<h3 id="테스트-코드의-종류">테스트 코드의 종류</h3>
<h4 id="단위-테스트unit-test">단위 테스트(Unit test)</h4>
<ul>
<li><p>단위테스트란 애플리케이션 안에 있는 개별적인 코드 단위를 테스트하는 방식이다.</p>
</li>
<li><p>보통 함수, 리액트 컴포넌트, 커스텀 훅을 테스트한다.</p>
</li>
<li><p>다른 코드의 유닛과 상호작용하는 것을 테스트하지 않는다.</p>
</li>
<li><p>입력값만을 이용해 출력값을 결정짓는 순수함수이면 좋다.</p>
<ul>
<li>순수함수의 경우 오로지 입력값만으로 출력값이 정해지기 때문에 외부 상태값을 가짜로 만들어내지 않아도 되어서 테스트하기 쉽고, 가짜 값을 만들지 않다보니 가짜 값이 유효한 값인지 신경쓰지 않아도 되기 때문이다.</li>
<li>외부에 의존성이 있다면 의존성을 mocking(가짜로 대체)한다. 외부 의존성의 실패가 아닌 특정 유닛의 실패임을 단정하기 위함</li>
<li><strong>격리된 unit test에서 실패를 쉽고 정확하게 파악할 수 있다.</strong></li>
</ul>
</li>
<li><p>근데 이는 <strong>사용자가 소프트웨어와 상호작용하는 방식과는 거리가 멀다</strong>.</p>
</li>
<li><p>또한 리팩토링하는 과정에서 실패할 수 있다.
=&gt; <strong>동작은 그대로이나 구현의 변경이 테스트 실패의 원인이 될 수 있다.</strong></p>
</li>
<li><p>도메인과 관련이 높고, 의존성이 낮으면서 로직이 복잡한 함수가 최고의 대상이다.</p>
</li>
<li><p>지나치게 복잡한 코드는 리팩토링의 신호이다.</p>
</li>
<li><p>컨트롤러와 같이 외부의존성만을 다루는 코드는 테스트의 우선순위가 낮다.</p>
</li>
</ul>
<h5 id="unit-test-예시">unit test 예시</h5>
<pre><code class="language-javascript">// 컴포넌트 테스트 예시
const TextField = ({ title, onChange }) =&gt; {
  return (
    &lt;div&gt;
      &lt;label htmlFor={title}&gt;{title}&lt;/label&gt;
      &lt;input id={title} onChange={onChange}&gt;&lt;/input&gt;
    &lt;/div&gt;
  )
}

// 테스트
test(&quot;label을 클릭하면 input이 foucus 됩니다&quot;, async () =&gt; {
  const title = &quot;일련번호&quot;
  const onChange = jest.fn()

  render(&lt;TextField title=&quot;일련번호&quot; onChange={onChange} /&gt;)

  userEvent.click(screen.getByText(&quot;일련번호&quot;))

  expect(screen.getByLabelText(title)).toHaveFocus()
})

test(&quot;input에 값을 넣은 만큼 onChange 핸들러가 호출 됩니다&quot;, async () =&gt; {
  const title = &quot;일련번호&quot;
  const onChange = jest.fn()

  render(&lt;TextField title={title} onChange={onChange} /&gt;)

  userEvent.click(screen.getByText(title))
  userEvent.type(screen.getByLabelText(title), &quot;hello&quot;)

  expect(onChange).toHaveBeenCalledTimes(5)
})

// 커스텀 훅
export const useCarousel = (initialIndex: number, carouselLength: number) =&gt; {
  const [step, setStep] = useState(
    initialIndex &lt; carouselLength ? initialIndex : 0
  )

  const prevStep = () =&gt; {
    if (step === 0) return setStep(carouselLength - 1)
    setStep(step - 1)
  }

  const nextStep = () =&gt; {
    if (step === carouselLength - 1) return setStep(0)
    setStep(step + 1)
  }

  return { step, prevStep, nextStep }
}

// 테스트
it(&quot;초기값이 캐러샐 길이보다 긴 경우 첫번째 step을 0으로 설정한다&quot;, () =&gt; {
  const { result } = renderHook(() =&gt; useCarousel(5, 3))

  expect(result.current.step).toBe(0)
})

})</code></pre>
<h4 id="통합-테스트integration-test">통합 테스트(Integration test)</h4>
<ul>
<li><p>통합 테스트는 여러 유닛을 통합하여 함께 동작하는 방식을 테스트하는 것을 의미한다.</p>
</li>
<li><p>유닛(컴포넌트)을 통합한 결과는 페이지가 될 수도 있고 페이지 내의 특정 영역이 될 수도 있다.</p>
</li>
<li><p>통합테스트가 시간이 좀 오래걸리더라도 단위테스트보다 어플리케이션의 신뢰성을 확보하는데 훨씬 효과적이다.</p>
</li>
<li><p>통합 테스트 단계에서는 실제 Api를 사용하게 되면 시간이 오래 걸리는것 뿐만 아니라 테스트가 백엔드에 의존성을 가져서 백엔드의 문제 때문에 실패하는 상황이 발생 할 수 있다. Api의 경우 테스트를 위해 모킹을 하는것이 좋다.</p>
</li>
<li><p>통합테스트를 위한 API 모킹시에는 msw라는 라이브러리가 유용하다.</p>
<h5 id="integration-test-예시">Integration test 예시</h5>
<pre><code class="language-javascript">it(&quot;검색어 입력후 검색 버튼을 클릭하면 검색 결과를 보여준다.&quot;, async () =&gt; {
render(&lt;App /&gt;)

const inputBox = screen.getByRole(&quot;textbox&quot;)
const searchButton = screen.getByRole(&quot;button&quot;)

act(() =&gt; {
  userEvent.type(inputBox, &quot;테스트&quot;)
  userEvent.click(searchButton)
})

await waitFor(() =&gt; {
  expect(screen.getAllByText(&quot;테스트&quot;, { exact: false }).length).toBe(20)
})
})
</code></pre>
</li>
</ul>
<p>it(&quot;검색어 입력후 검색 버튼을 클릭하였을때 결과가 없을 경우 결과가 없음을 보여준다&quot;, async () =&gt; {
  render(<App />)</p>
<p>  const inputBox = screen.getByRole(&quot;textbox&quot;)
  const searchButton = screen.getByRole(&quot;button&quot;)</p>
<p>  act(() =&gt; {
    userEvent.type(inputBox, &quot;검색 결과 없는 검색어&quot;)
    userEvent.click(searchButton)
  })</p>
<p>  await waitFor(() =&gt; {
    expect(screen.getByText(&quot;검색 결과가 없습니다&quot;)).toBeInTheDocument()
  })
})</p>
<pre><code>
#### E2E 테스트
- E2E 테스트는 End To End 테스트의 약자로 cypress, playwrite와 같은 E2E 테스트 도구를 이용하여 애플리케이션의 흐름을 처음부터 끝까지 테스트하여 애플리케이션의 무결성을 검증하는 테스트를 의미한다.
- 실제 브라우저 환경에서 실제 api를 사용하기 때문에 테스트 하는데 매우 오랜 시간이 걸린다. 보통 배포 직전에 테스트한다.

##### E2E 테스트 예시(Cypress)
```javascript
it(&quot;사용자가 로그인 페이지에 진입해 아이디 비밀번호입력후 엔터키를 눌러 로그인에 성공하는경우 홈으로 이동해 유저 프로필을 확인한다&quot;, function () {
  // destructuring assignment of the this.currentUser object
  const username = &quot;test&quot;

  const password = &quot;1234&quot;

  // 로그인 페이지 진입
  cy.visit(&quot;/login&quot;)

  // 아이디 입력 input에 username 입력
  cy.get(&quot;input[name=username]&quot;).type(username)

  // 패스워드 입력 input에 password 입력 및 enter 입력
  cy.get(&quot;input[name=password]&quot;).type(`${password}{enter}`)

  // home으로 리다이렉션
  cy.url().should(&quot;include&quot;, &quot;/home&quot;)

  // home에 username이 있는지 확인
  cy.get(&quot;h1&quot;).should(&quot;contain&quot;, username)
})</code></pre><h4 id="시각적-테스트">시각적 테스트</h4>
<ul>
<li><p>앞서 설명한 기능적 테스트 만으로 우리가 만든 애플리케이션이 정상 동작함을 보장할 수 없다.</p>
</li>
<li><p>기능은 동작할지 몰라도 글자가 너무 작아서 보이지 않거나 혹은 색상이 의도와 달라 눈에 잘 안 들어올 수 있다. </p>
</li>
<li><p>이를 위해 프론트엔드에서는 시각적 테스트를 수행해야 한다.</p>
</li>
<li><p>시각적 테스트에는 크게 <strong>스냅샷 테스트</strong>와 <strong>시각적 회귀 테스트</strong>가 있다.</p>
<h5 id="스냅샷-테스트">스냅샷 테스트</h5>
</li>
<li><p>스냅샷이란 특정 시점의 컴포넌트 혹은 함수의 결과를 직렬화하여 텍스트 형태로 저장해두고 매 테스트 마다 변경되었는지를 확인하여 변경이 실제 의도한것인지 확인하는 것이다.</p>
</li>
<li><p>스냅샷 테스트를 사용하여 간단하게 컴포넌트의 출력을 검증할 수 있지만 html 속성이 아닌 css 속성이 달라진경우를 잡아낼수 없고, 스냅샷이 html태그로 구성된 텍스트 데이터를 보여주기 때문에 비교에 실패하더라도 실제 의도한 출력이 맞는지 확인하기 어려우며, 스냅샷 실패시 테스트를 성공상태로 만들기 매우 쉬워 실제 의도한 출력이 아니더라도 기대하는 테스트 결과로 저장되면 이후 거짓 음성이 나오게되어 테스트에대한 신뢰도가 떨어질 수 있다.</p>
</li>
</ul>
<h5 id="시각적-회귀-테스트">시각적 회귀 테스트</h5>
<ul>
<li>앞서 설명한 스냅샷 테스트의 상위 버전이라고 부를수 있으면서 실제 프론트엔드에서 테스트와 가장 유사한 테스트 이다.</li>
<li>많은 비용이 드는 테스트 이므로 주로 CI환경에 테스트 플로우를 세팅해두어 사용한다.</li>
<li>플로우는 다음과 같은데, 매 커밋시마다 테스트 대상의 화면과 저장된 화면을 비교하여 차이가 있는지 파악하고, 차이가 존재한다면 사용자의 approve 혹은 코드 수정을 통해 테스트를 통과시키는 방식이다.</li>
<li>시각적 회귀 테스트는 여러 도구로 수행할수 있다. storybook과 chromatic의 조합으로 사용 하는 방식이 있다.</li>
<li>시각적 회귀 테스트를 수행하지 않더라도 storybook을 이용하면 개별 컴포넌트를 시각적으로 확인하기 용이하기 때문에 만약 시각적 회귀 테스트 비용이 부담된다면 storybook을 이용해 매 코드 변경시 컴포넌트의 UI를 확인하거나 혹은 개발시 UI를 확인하면서 작업할때 용이하다.</li>
</ul>
<h4 id="기능-테스트functional-test">기능 테스트(Functional test)</h4>
<ul>
<li>위에서 언급한 테스트 종류와 별개로 기능 테스트라는 것이 있다.</li>
<li>우리가 지향해야 하는 테스트가 바로 기능 테스트이다.</li>
<li>기능 테스트는 소프트웨어의 특정 기능이 명세서나 요구사항에 맞게 동작하는지를 확인한다.</li>
<li>소프트웨어가 <strong>실제 사용자 경험과 유사하게 작동하는지를 테스트</strong>하여, 사용자와 소프트웨어의 상호작용 방식을 평가한다.</li>
<li>코드가 아닌 실제 사용자의 소프트웨어 사용 경험을 중점으로 테스트한다.<ul>
<li>코드가 리팩토링되더라도 동작이 동일하면 테스트는 통과한다.</li>
<li>테스트가 통과하면 사용자 경험상 문제가 없음을 보장한다.</li>
<li>장점: 사용자의 실제 소프트웨어 사용 방식을 반영하기 때문에, 사용자의 입장에서 소프트웨어가 제대로 동작하는지를 확인할 수 있다.</li>
<li>단점: 테스트가 실패할 경우 디버깅이 어려울 수 있다.</li>
<li><strong>RTL 철학과의 유사성</strong>: 기능 테스트는 내부 코드 구현보다는 사용자의 소프트웨어 사용을 테스트하는 것을 중시합니다. 이는 테스트 도구인 RTL(React Testing Library)의 철학과 일치한다.<h4 id="kent-c-dodds">Kent C. Dodds</h4>
</li>
</ul>
</li>
<li>신뢰성 확보<ul>
<li>사용자가 애플리케이션을 사용할 때 제대로 작동할 것이라는 신뢰성을 위해 테스트를 작성한다.</li>
<li><strong>테스트는 소프트웨어 사용 방식과 유사할수록 신뢰도가 높아진다.</strong></li>
<li>코드를 테스트하기보다는 서비스가 지원하는 기능인 <strong>Use Case</strong>를 테스트해야 한다.</li>
<li>구현 세부 사항을 테스트하면 딴 길로 새기 쉽다.</li>
<li>즉 <strong>코드 내부의 동작 방식을 테스트하는 것이 아니라 코드가 사용자에게 제공하는 기능을 테스트하는 것에 초점을 맞춰야 한다.</strong></li>
<li>unit test 든 integration test 든 테스트의 종류와 상관없다.</li>
</ul>
</li>
<li>테스트 대상 선정 방법<ul>
<li>테스트 코드 커버리지는 크게 신경써야 할 지표가 아니다.</li>
<li>대신 <strong>use case 커버리지</strong>가 중요하다. 이는 우리가 수동으로 작성해야 한다.</li>
<li>테스트 코드 작성하기 전에 모든 use case를 생각해야 한다.</li>
<li>즉 애플리케이션이 지원하는 기능의 목록을 작성하고 이 기준에 따라 우선순위를 정하는 것이 좋다.<ul>
<li>애플리케이션에서 에러가 발생하면 큰일이 나는 부분부터 테스트 코드를 우선해야 한다.</li>
<li>회의를 통해 모든 프론트엔드 개발자들이 테스트의 중요성을 이해하고 우선순위를 확립하도록 한다.</li>
</ul>
</li>
<li>use case 커버리지를 작성하고 이를 기반으로 테스트 코드를 작성하고, 구현 코드를 작성한다. 이 과정으로 인해 테스트 파일 자체가 기능 명세서가 될 수 있다.</li>
<li>즉 <strong>코드를 테스트하는 것이 아니라 use case를 테스트해야 한다.</strong></li>
</ul>
</li>
<li>리팩토링<ul>
<li>리팩터링은 기존에 의도한 동작은 유지하면서 구현만 변하는 것을 의미한다.</li>
<li>구현 세부사항 테스트에 집중하면 false positive, false negative가 발생할 수 있다. </li>
<li><strong>false negative</strong>는 동작은 성공하지만 테스트 코드가 실패하는 경우이다. <ul>
<li>리팩터링 시 false negative를 줄 수 있다. </li>
<li>즉 기존 동작은 변경하지 않으면서 구현만 변경을 했음에도 불구하고 테스트가 실패한다.</li>
<li>그 결과 유지 관리가 어렵고 성가신 테스트 코드가 많이 생성된다.</li>
</ul>
</li>
<li><strong>false positive</strong>는 테스트는 성공했지만, 원래는 실패했어야 한다는 뜻이다.<ul>
<li>이를 방지하기 위해 구현 세부 사항을 테스트하지 않도록 API를 제한하면서 모든 테스트를 다시 작성해야 한다.</li>
<li>RTL을 사용하면 그의 철학으로 인해 구현 세부 사항을 테스트하는 것이 아니라 기능 자체를 테스트할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="tdd와-테스트-코드-작성-가이드라인">TDD와 테스트 코드 작성 가이드라인</h2>
<h3 id="테스트-주도-개발tdd">테스트 주도 개발(TDD)</h3>
<p>TDD란 Test Driven Development의 약자로 켄트 벡이 1999년 익스트림 프로그래밍의 일부로 제안하며 널리 알려졌다.
동작하는 코드 작성 이전에 테스트를 먼저 작성하고 그 테스트를 통과하는 코드를 작성함으로써 테스트된 동작하는 코드를 얻는 개발 방법이다.</p>
<h4 id="흐름">흐름</h4>
<ul>
<li>테스트 코드를 작성한다. (빨간 막대)<ul>
<li>구현하려는 동작을 설명하는 테스트 코드를 작성한다.<ul>
<li>동작을 구현하는 코드는 아직 작성하지 않았으니 당연히 실패해야 한다.</li>
</ul>
</li>
</ul>
</li>
<li>실행 가능하게 만든다. (초록 막대)</li>
<li>올바르게 만든다. (리팩토링)<h4 id="tdd와-use-case-커버리지-작성">TDD와 use case 커버리지 작성</h4>
</li>
<li>Kent C. Dodds이 말한 테스트 코드 작성 이전 use case 커버리지 작성은 TDD와 궁합이 잘 맞는다.</li>
<li>use case 커버리지 작성 이후, 테스트 코드를 작성하고 구현 코드를 작성한다.</li>
</ul>
<h3 id="테스트-코드-작성-프로세스">테스트 코드 작성 프로세스</h3>
<ol>
<li>개발 회의를 통해 애플리케이션 기능의 우선순위를 정한다.</li>
<li>기능을 배정받은 개발자는 각 기능의 use case coverage를 작성한다.</li>
<li>각 use case마다 테스트 코드를 작성한다.</li>
<li>테스트 코드에 맞게 구현 코드를 작성한다.</li>
<li>리팩토링한다.</li>
</ol>
<h3 id="가이드라인">가이드라인</h3>
<ul>
<li>제목을 알아보기 쉽게 설정한다. 다른 개발자가 보고 즉시 의미를 알아챌 수 있어야 한다.</li>
<li>하나의 테스트에는 하나의 기능만 테스트한다.</li>
<li>가독성을 위해 <strong>AAA 패턴</strong>을 지킨다.</li>
<li><strong>내부 구현을 테스트하는 것이 아니라 사용자가 실제로 사용하는 방식으로 소프트웨어를 테스트하는 것이 목표</strong>이다.</li>
<li>단위 테스트(Unit Test), 통합 테스트(Integration Test), E2E 테스트 등 여러 테스트의 종류가 있지만 이들 모든 테스트의 목적은 특정 기능이 올바르게 작동하는지를 검증하는 것이다.</li>
<li>보통 통합 테스트를 먼저 작성하고, 단위 테스트나 E2E 테스트를 작성하는 방식으로 한다.</li>
<li>새로운 기능 추가 또는 리팩토링시 코드 구현이 변경되어도 의도된 동작이 그대로 수행되어 테스트가 통과되어야 한다.</li>
<li>테스트 코드는 중복이 다소 발생하더라도 직관적이고 명확하게 이해되도록 테스트 코드를 작성해야 한다.</li>
<li>100% Code Coverage를 목표로 삼지 말아야 한다. 그럴 가치가 없다.</li>
<li>테스트 코드는 완벽하지 않아도 된다.</li>
<li>E2E 테스트는 비용이 많이 들기 때문에 비즈니스적으로 중요한 영역에 적용한다.</li>
<li>시각적 테스트는 시각적 요소가 중요한 페이지에만 적용한다.</li>
<li>모킹이 많은 것은 이상적이지 않다.</li>
<li>스냅샷 테스트보다는 시각적 회귀 테스트를 사용하는 것이 좋다.</li>
<li>정기적인 코드리뷰를 통해 테스트 코드에 대한 피드백을 제공하고 개선점을 논의한다. 이를 통해 모든 팀원이 테스트 코드를 작성하는 문화가 자리잡음으로써 전체적인 코드 품질과 안정성 향상을 도모할 수 있다.<h4 id="aaa-패턴">AAA 패턴</h4>
</li>
<li>Arrange<ul>
<li>테스트가 목표로 하는 시나리오에 대한 시스템을 제공하기 위한 모든 설정 코드 작성</li>
</ul>
</li>
<li>Act<ul>
<li>테스트 실행, 필요시 결과 저장</li>
<li>테스트 대상 시스템 메서드 호출하고 결과, 출력값을 저장.</li>
</ul>
</li>
<li>Assert<ul>
<li>받은 예상값이 충족하는지 검증</li>
</ul>
</li>
</ul>
<h3 id="사용-테스트-도구">사용 테스트 도구</h3>
<h4 id="react-testing-library">React Testing Library</h4>
<p><a href="https://testing-library.com/docs/react-testing-library/intro/">https://testing-library.com/docs/react-testing-library/intro/</a></p>
<h5 id="가상-dom-제공">가상 DOM 제공</h5>
<p>브라우저 없이 테스트하기 위해 시뮬레이션된 DOM을 생성하고, 이를 활용하여 DOM과 상호작용할 수 있는 유틸리티를 제공한다.</p>
<p>렌더링 이후, react testing library의 전역 객체 screen을 통해 시뮬레이션된 DOM에 액세스할 수 있다.</p>
<h5 id="사용자-중심의-테스트">사용자 중심의 테스트</h5>
<p>사용자가 실제로 소프트웨어를 사용하는 방식과 동일하게 테스트할 수 있도록 돕는다.</p>
<p>테스트에서 사용자가 버튼 클릭과 같은 작업을 하고 버튼을 클릭한 후 DOM이 어떤 모습인지 확인할 수 있다.</p>
<h5 id="모범-사례-지향">모범 사례 지향</h5>
<p>구현 세부 정보보다는 구성 요소의 동작을 테스트하는 데 중점을 두며, HTML 태그를 최대한 활용하여 테스트한다.</p>
<p>RTL이 공식문서에서 강조하는 원칙은 다음과 같다.</p>
<blockquote>
<p>&quot;테스트 코드를 작성하고 테스트를 수행함에 있어서 테스트가 서비스의 사용 방식과 유사할수록 더 많은 신뢰를 얻을 수 있다&quot;</p>
</blockquote>
<p>이는 즉 우리가 추구하는 테스트 목표와 동일하다.</p>
<h5 id="rtl이-요소를-찾는-방법">RTL이 요소를 찾는 방법</h5>
<ol>
<li><strong>접근성 높은 메서드</strong><ul>
<li><code>getByRole</code>: 접근성 트리에 노출된 모든 요소를 조회하며, 가장 접근성과 사용자 경험을 고려하는 메서드이다.</li>
<li><code>getByLabelText</code>: form 필드 내부 요소들을 각자의 label로 찾는다.</li>
<li><code>getByPlaceholderText</code>: placeholder를 대체자로 사용한다.</li>
<li><code>getByText</code>: 텍스트로 요소를 찾는다.</li>
<li><code>getByDisplayValue</code>: form 내부에서 이미 값이 입력된 요소를 찾는다.</li>
</ul>
</li>
<li><strong>ARIA 관련 props 사용 메서드</strong><ul>
<li><code>getByAltText</code>: 요소 내에 alt 속성을 조회한다.</li>
<li><code>getByTitle</code>: Title 속성을 조회한다.</li>
</ul>
</li>
<li><strong>Test-id</strong><ul>
<li><code>getByTestId</code>: 위의 방법이 모두 불가할 때 최후의 수단으로 사용한다.</li>
</ul>
</li>
</ol>
<h4 id="vitest">Vitest</h4>
<p><a href="https://vitest.dev/">https://vitest.dev/</a>
테스트를 찾고, 실행하며, 통과 여부를 결정하는 도구이다.
Jest와 문법이 동일하고 빌드 도구인 vite와 호환성이 좋다.
RTL은 시뮬레이션된 DOM을 리턴하며, Vitest와 같은 테스트 러너를 사용하여 테스트를 실행한다.</p>
<h4 id="mswmocking-service-worker">MSW(Mocking Service Worker)</h4>
<p><a href="https://mswjs.io/">MSW 공식 문서</a>
MSW는 브라우저 및 Node.js 환경에서 사용 가능한 API 모킹 라이브러리이다.</p>
<h5 id="사용-배경">사용 배경</h5>
<ul>
<li>프로덕션 코드에서 백엔드 API 개발 속도와 프론트엔드 개발 속도가 맞지 않아 프론트엔드 개발 진행에 어려움이 있을 수 있다.</li>
<li>테스트 코드를 작성하고 실행할 때 실제 API 요청을 할 경우 비용, 속도 및 의존성 문제가 발생할 수 있다.</li>
<li>MSW 라이브러리를 이용하면 Mock 서버를 구축하지 않아도 API를 네트워크 수준에서 Mocking 할 수 있다.</li>
<li>MSW는 네트워크 요청 과정에서 Request에 대한 Mocking이 가능한 라이브러리이며 네트워크 요청을 가로채서 모의 응답을 보내주는 역할을 한다.<h5 id="특징">특징</h5>
</li>
<li>독립성
라이브러리나 프레임워크에 영향을 받지 않게 설계되었다.
별도의 플러그인 설치 없이 모든 브라우저와 Node.js 환경에서 사용 가능하다.</li>
<li>실제 네트워크 요청 모킹
fetch와 같이 네트워크 요청 함수를 mocking하는 것에 그치지 않고 실제로 보내진 네트워크 요청을 가로챈다.</li>
<li>재사용성
프로덕션 코드 및 테스트 코드에서 모두 사용 가능하다.<h5 id="원리">원리</h5>
<img src="https://velog.velcdn.com/images/cloud_oort/post/24858321-ebac-4be2-84cb-96216ba95b1c/image.png" alt=""></li>
</ul>
<p>브라우저에서는 브라우저에서 제공하는 Service Worker를 이용해서 mocking 한다.
(Node.js 환경에서는 http, XMLHttpRequest와 같은 네트워크 프로토콜을 mocking 한다.)</p>
<ul>
<li>Service Worker는 웹 애플리케이션의 메인 스레드와 분리된 별도의 백그라운드 스레드에서 실행시킬 수 있는 기술 중 하나이다. Service Worker 덕분에 애플리케이션의 UI Block 없이 연산을 처리할 수 있다.</li>
<li>locoahost가 아닌 환경에서는 HTTPS가 기본적으로 제공되는 환경에서만 사용할 수 있다.</li>
</ul>
<p>브라우저에서의 원리</p>
<ol>
<li>브라우저가 요청을 한다.</li>
<li>Service Worker가 이를 인지한다.</li>
<li>Service Worker는 요청을 실제 서버로 보내지 않고 요청을 복사하여 클라이언트 사이드에 있는 MSW 라이브러리로 보낸다.</li>
<li>MSW는 해당 요청에 대한 handler를 찾아서 등록된 모의 응답값을 Service Worker를 통해 브라우저에게 전달한다.</li>
<li>이를 통해 실제 서버 존재 여부와 상관없이 실제 요청으로 이어지지 않고 예상할 수 있는 요청에 대해 Mocking이 가능해진다.</li>
</ol>
<h5 id="개발-흐름">개발 흐름</h5>
<ul>
<li>기획자 요구 전달.</li>
<li>프론트엔드 개발자와 백엔드 개발자가 API 스펙 합의.</li>
<li>프론트엔드 개발자는 MSW를 통해 네트워크 레벨에서의 Mocking을 진행한 후 앱 개발.</li>
<li>API 없이도 프론트엔드 개발자는 높은 완성도를 갖고 있는 수준에서 기획자와 미리 프론트엔드 애플리케이션 확인하며 피드백 주고받고 그 사이 백엔드 개발자는 API 개발을 진행.</li>
<li>백엔드 개발자가 실제 API를 개발하면, 프론트엔드 개발자는 MSW를 스위치 오프하여 production으로 배포할 수 있는 형태의 개발을 진행.</li>
</ul>
<h2 id="출처-및-참고-자료">출처 및 참고 자료</h2>
<p><a href="https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme.kr.md">https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme.kr.md</a></p>
<p><a href="https://techblog.woowahan.com/17721/">https://techblog.woowahan.com/17721/</a></p>
<p><a href="https://techblog.woowahan.com/17404/">https://techblog.woowahan.com/17404/</a></p>
<p><a href="https://www.youtube.com/watch?v=R7spoJFfQ7U">https://www.youtube.com/watch?v=R7spoJFfQ7U</a></p>
<p><a href="https://www.youtube.com/watch?v=mIO4Rbe_M74">https://www.youtube.com/watch?v=mIO4Rbe_M74</a></p>
<p><a href="https://www.youtube.com/watch?v=rkTt1uQ1YHI">https://www.youtube.com/watch?v=rkTt1uQ1YHI</a></p>
<p><a href="https://www.jbee.io/articles/developments/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90%20%EB%8C%80%ED%95%9C%20%EC%98%A4%ED%95%B4%EC%99%80%20%EC%82%AC%EC%8B%A4">https://www.jbee.io/articles/developments/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90%20%EB%8C%80%ED%95%9C%20%EC%98%A4%ED%95%B4%EC%99%80%20%EC%82%AC%EC%8B%A4</a></p>
<p><a href="https://tech.kakaopay.com/post/implementing-tdd-in-practical-applications/">https://tech.kakaopay.com/post/implementing-tdd-in-practical-applications/</a></p>
<p><a href="https://tech.kakao.com/posts/458">https://tech.kakao.com/posts/458</a></p>
<p><a href="https://blog.lemonbase.team/%EC%9A%B0%EB%A6%AC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%8C%80%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-a3ea48207cd4">https://blog.lemonbase.team/%EC%9A%B0%EB%A6%AC-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%8C%80%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-a3ea48207cd4</a></p>
<p><a href="https://tech.madup.com/mock-service-worker/">https://tech.madup.com/mock-service-worker/</a></p>
<p><a href="https://tech.madup.com/front-test-tips/">https://tech.madup.com/front-test-tips/</a></p>
<p><a href="https://developer-bandi.github.io/post/frontend-testing/">https://developer-bandi.github.io/post/frontend-testing/</a></p>
<p>// kent c. dodds 글 번역
<a href="https://soojae.tistory.com/74">https://soojae.tistory.com/74</a>
<a href="https://soojae.tistory.com/82?category=1010060">https://soojae.tistory.com/82?category=1010060</a>
<a href="https://soojae.tistory.com/83?category=1010060">https://soojae.tistory.com/83?category=1010060</a>
<a href="https://soojae.tistory.com/84?category=1010060">https://soojae.tistory.com/84?category=1010060</a>
<a href="https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/">https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/</a>
<a href="https://jymini.tistory.com/73">https://jymini.tistory.com/73</a></p>
<p><a href="https://yozm.wishket.com/magazine/detail/2435/">https://yozm.wishket.com/magazine/detail/2435/</a>
<a href="https://yozm.wishket.com/magazine/detail/2483/">https://yozm.wishket.com/magazine/detail/2483/</a></p>
<p><a href="https://im-developer.tistory.com/226">https://im-developer.tistory.com/226</a></p>
<p><a href="https://velog.io/@sehyunny/a-compilation-of-outstanding-testing-articles">https://velog.io/@sehyunny/a-compilation-of-outstanding-testing-articles</a></p>
<p><a href="https://musma.github.io/2023/07/24/front-end-test-code.html">https://musma.github.io/2023/07/24/front-end-test-code.html</a></p>
<p><a href="https://team.modusign.co.kr/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EC%9D%98%EB%AF%B8%EC%9E%88%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-4992409c7f2d">https://team.modusign.co.kr/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EC%9D%98%EB%AF%B8%EC%9E%88%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-4992409c7f2d</a></p>
<p><a href="https://ykss.netlify.app/translation/unit-testing-with-jest-react-and-typescript/">https://ykss.netlify.app/translation/unit-testing-with-jest-react-and-typescript/</a></p>
<p><a href="https://github.com/ssi02014/react-test-reference-documentation?tab=readme-ov-file">https://github.com/ssi02014/react-test-reference-documentation?tab=readme-ov-file</a></p>
<p><a href="https://techblog.pet-friends.co.kr/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%8F%84%EC%9E%85%EA%B8%B0-c3a1865250ee">https://techblog.pet-friends.co.kr/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%8F%84%EC%9E%85%EA%B8%B0-c3a1865250ee</a></p>
<p>AHA 법칙 Avoid Hasting Abstraction
<a href="https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/">https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/</a>
<a href="https://kentcdodds.com/blog/aha-testing">https://kentcdodds.com/blog/aha-testing</a></p>
<p><a href="https://fe-developers.kakaoent.com/2022/220825-msw-integration-testing/">MSW 사용 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github flow 전략]]></title>
            <link>https://velog.io/@cloud_oort/Github-flow-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@cloud_oort/Github-flow-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Fri, 19 Jul 2024 13:52:20 GMT</pubDate>
            <description><![CDATA[<h3 id="github-flow-사용-방법">GitHub Flow 사용 방법</h3>
<ol>
<li><p><strong>Main 브랜치 준비</strong></p>
<ul>
<li><code>main</code> 브랜치는 항상 배포 가능한 상태를 유지합니다.</li>
<li>모든 변경 사항은 Pull Request(PR)를 통해 <code>main</code> 브랜치에 병합됩니다.</li>
</ul>
</li>
<li><p><strong>새로운 브랜치 생성</strong></p>
<ul>
<li>새로운 작업을 시작할 때 <code>main</code> 브랜치에서 새로운 브랜치를 생성합니다.</li>
<li>브랜치 이름은 작업 내용을 잘 나타낼 수 있도록 명확하게 짓습니다.<pre><code class="language-bash">git checkout -b feature-branch</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="3">
<li><strong>커밋 및 작업</strong><ul>
<li>작업을 진행하면서 변경 사항을 로컬 브랜치에 커밋합니다.</li>
<li>커밋 메시지는 간결하고 명확하게 작성하여 변경 사항을 쉽게 이해할 수 있도록 합니다.<pre><code class="language-bash">git add .
git commit -m &quot;Add new feature&quot;</code></pre>
</li>
</ul>
</li>
</ol>
<ol start="4">
<li><p><strong>원격 저장소에 푸시</strong></p>
<ul>
<li>로컬 브랜치의 변경 사항을 GitHub의 원격 저장소에 푸시합니다.<pre><code class="language-bash">git push origin feature-branch</code></pre>
</li>
</ul>
</li>
<li><p><strong>Pull Request 생성</strong></p>
<ul>
<li>GitHub 웹사이트에서 PR을 생성하여 코드 변경 사항을 검토받습니다.</li>
<li>PR에서 변경 사항에 대한 설명을 추가하고 리뷰어를 지정합니다.</li>
<li>PR 제목과 설명은 변경 사항의 목적과 내용을 명확하게 전달하도록 작성합니다.</li>
</ul>
</li>
<li><p><strong>코드 리뷰 및 피드백</strong></p>
<ul>
<li>다른 팀원들이 PR을 리뷰하고 피드백을 제공합니다.</li>
<li>필요시 수정사항을 로컬 브랜치에 커밋하고 다시 푸시합니다.<pre><code class="language-bash">git add .
git commit -m &quot;Address review feedback&quot;
git push origin feature-branch</code></pre>
</li>
</ul>
</li>
<li><p><strong>PR 병합</strong></p>
<ul>
<li>모든 리뷰어가 PR을 승인하면, <code>main</code> 브랜치에 병합합니다.</li>
<li>병합은 GitHub 웹사이트에서 수행합니다.</li>
<li>병합 후 로컬 <code>main</code> 브랜치를 업데이트합니다.<pre><code class="language-bash">git checkout main
git pull origin main</code></pre>
</li>
</ul>
</li>
<li><p><strong>브랜치 삭제</strong></p>
<ul>
<li>병합된 브랜치는 더 이상 필요하지 않으므로 삭제합니다.<pre><code class="language-bash">git branch -d feature-branch
git push origin --delete feature-branch</code></pre>
</li>
</ul>
</li>
</ol>
<h3 id="github-flow-예제">GitHub Flow 예제</h3>
<h4 id="예제-시나리오-새로운-기능-개발">예제 시나리오: 새로운 기능 개발</h4>
<ol>
<li><p><strong>메인 브랜치로 이동하고 최신 상태로 업데이트</strong></p>
<pre><code class="language-bash">git checkout main
git pull origin main</code></pre>
</li>
<li><p><strong>새로운 브랜치 생성</strong></p>
<pre><code class="language-bash">git checkout -b add-login-feature</code></pre>
</li>
<li><p><strong>코드 작업 및 커밋</strong></p>
<pre><code class="language-bash"># 작업 수행
git add .
git commit -m &quot;Add login feature&quot;</code></pre>
</li>
<li><p><strong>원격 저장소에 푸시</strong></p>
<pre><code class="language-bash">git push origin add-login-feature</code></pre>
</li>
<li><p><strong>GitHub에서 PR 생성</strong></p>
<ul>
<li>GitHub 웹사이트로 이동하여 새로운 PR을 생성합니다.</li>
<li>PR 제목과 설명을 작성하고 리뷰어를 지정합니다.</li>
</ul>
</li>
<li><p><strong>코드 리뷰 및 피드백 반영</strong></p>
<ul>
<li>리뷰어의 피드백을 반영하여 코드 수정<pre><code class="language-bash">git add .
git commit -m &quot;Fix issues from code review&quot;
git push origin add-login-feature</code></pre>
</li>
</ul>
</li>
<li><p><strong>PR 병합</strong></p>
<ul>
<li>리뷰어가 PR을 승인하면 <code>main</code> 브랜치에 병합합니다.</li>
<li>병합 후 로컬 <code>main</code> 브랜치를 업데이트합니다.<pre><code class="language-bash">git checkout main
git pull origin main</code></pre>
</li>
</ul>
</li>
<li><p><strong>브랜치 삭제</strong></p>
<pre><code class="language-bash">git branch -d add-login-feature
git push origin --delete add-login-feature</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[vite + typescript 환경에서 eslint, prettier, husky, lint-staged 설정]]></title>
            <link>https://velog.io/@cloud_oort/vite-typescript-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-eslint-prettier-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@cloud_oort/vite-typescript-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-eslint-prettier-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 10 Jul 2024 15:08:19 GMT</pubDate>
            <description><![CDATA[<h2 id="eslint">Eslint</h2>
<p>eslint는 자바스크립트 코드에서 발견된 문제 패턴을 식별하기 위한 정적 코드 분석 도구이다.</p>
<p>vite 프로젝트를 생성하면 아래와 같이 eslint가 기본적으로 설치된다.
에디터에서 사용하기 위해 vscode eslint extension을 설치한다.</p>
<pre><code class="language-js">// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    &#39;eslint:recommended&#39;,
    &#39;plugin:@typescript-eslint/recommended&#39;,
    &#39;plugin:react-hooks/recommended&#39;,
  ],
  ignorePatterns: [&#39;dist&#39;, &#39;.eslintrc.cjs&#39;],
  parser: &#39;@typescript-eslint/parser&#39;,
  plugins: [&#39;react-refresh&#39;],
  rules: {
    &#39;react-refresh/only-export-components&#39;: [
      &#39;warn&#39;,
      { allowConstantExport: true },
    ],
  },
}

// package.json
&quot;devDependencies&quot;: {
    &quot;@types/react&quot;: &quot;^18.3.3&quot;,
    &quot;@types/react-dom&quot;: &quot;^18.3.0&quot;,
    &quot;@typescript-eslint/eslint-plugin&quot;: &quot;^7.13.1&quot;,
    &quot;@typescript-eslint/parser&quot;: &quot;^7.13.1&quot;,
    &quot;@vitejs/plugin-react&quot;: &quot;^4.3.1&quot;,
    &quot;eslint&quot;: &quot;^8.57.0&quot;,
    &quot;eslint-plugin-react-hooks&quot;: &quot;^4.6.2&quot;,
    &quot;eslint-plugin-react-refresh&quot;: &quot;^0.4.7&quot;,
    &quot;typescript&quot;: &quot;^5.2.2&quot;,
    &quot;vite&quot;: &quot;^5.3.1&quot;
  }</code></pre>
<h3 id="eslint-config-airbnb">eslint-config-airbnb</h3>
<p>eslint 규칙을 직접 설정할 수도 있지만 airbnb 스타일 가이드를 사용하고자 한다.</p>
<h4 id="사용-이유">사용 이유</h4>
<ul>
<li>Airbnb 스타일 가이드는 코드 품질을 보장하기 위해 포괄적인 규칙 세트를 제공한다.</li>
<li>Airbnb는 대규모 소프트웨어 프로젝트에서 널리 사용되며, 많은 회사들이 Airbnb의 스타일 가이드를 자체 프로젝트에 적용한다.</li>
<li>Airbnb 팀은 스타일 가이드를 지속적으로 업데이트하고 유지보수한다.</li>
<li>Airbnb 스타일 가이드는 자바스크립트 뿐만 아니라 리액트 규칙도 함께 있다.</li>
<li>ESLint와 쉽게 통합될 수 있으며, Prettier와 같은 다른 도구와 함께 사용하기에도 용이하다.</li>
<li>문제 해결이나 설정에 관한 정보를 쉽게 찾을 수 있다.<h4 id="사용-방법">사용 방법</h4>
<a href="https://www.npmjs.com/package/eslint-config-airbnb">https://www.npmjs.com/package/eslint-config-airbnb</a>
eslint-config-airbnb 라이브러리는 <strong>기본적으로 ECMAScript 6+와 리액트를 포함하는 airbnb ESLint 규칙을 제공하고 아래 5개의 패키지들을 필수로 한다.</strong>  </li>
<li><code>eslint</code></li>
<li><code>eslint-plugin-import</code> : ES6의 import, export 구문을 지원하는 필수 플러그인</li>
<li><code>eslint-plugin-react</code> : react 규칙이 들어있는 플러그인</li>
<li><code>eslint-plugin-react-hooks</code>: react hook 규칙이 들어있는 플러그인</li>
<li><code>eslint-plugin-jsx-a11y</code>: JSX 요소의 접근성 규칙에 대한 정적 검사 플러그인</li>
</ul>
<h4 id="설치">설치</h4>
<p><code>npm install -D eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks</code></p>
<h4 id="타입스크립트">타입스크립트</h4>
<p><a href="https://www.npmjs.com/package/eslint-config-airbnb-typescript">https://www.npmjs.com/package/eslint-config-airbnb-typescript</a>
타입스크립트 사용하면 eslint-config-airbnb-typescript도 사용해준다.
<code>npm install -D eslint-config-airbnb-typescript
            @typescript-eslint/eslint-plugin@
            @typescript-eslint/parser</code></p>
<h4 id="eslint-적용">eslint 적용</h4>
<p>다음과 같이 extends를 설정해준다.</p>
<pre><code class="language-js">// .eslintrc.cjs
extends: [
    &#39;airbnb&#39;,
    &#39;airbnb-typescript&#39;,
    &#39;airbnb/hooks&#39;,
    &#39;plugin:react/recommended&#39;,
    &#39;plugin:@typescript-eslint/recommended&#39;,
    &#39;plugin:prettier/recommeded&#39;
  ],</code></pre>
<h2 id="prettier">Prettier</h2>
<p>prettier는 일관성 있는 코드 스타일을 유지할 수 있게 도와주는 도구이다.</p>
<h3 id="사용">사용</h3>
<ul>
<li><code>npm i -D prettier</code></li>
<li>prettier extension 설치</li>
<li>vscode prettier 적용<h3 id="prettier-설정">prettier 설정</h3>
<pre><code class="language-js">// .prettierrc
{
&quot;singleQuote&quot;: true,
&quot;trailingComma&quot;: &quot;all&quot;,
&quot;printWidth&quot;: 80,
&quot;tabWidth&quot;: 2,
&quot;semi&quot;: true,
&quot;bracketSpacing&quot;: true,
&quot;arrowParens&quot;: &quot;always&quot;,
&quot;endOfLine&quot;: &quot;auto&quot;
}</code></pre>
<h3 id="eslint와-prettier-동시-설정">eslint와 prettier 동시 설정</h3>
eslint는 prettier의 formatting 기능도 포함되어 있어 충돌 발생 가능성이 있다.
이를 해결하기 위해 추가적인 plugin을 설치해준다.</li>
</ul>
<p><code>eslint-config-prettier</code>는 prettier와 충돌 가능성이 있는 옵션을 모두 off해주는 역할을 한다.</p>
<p><code>eslint-plugin-prettier</code>는 prettier에 위반된 부분을 eslint error로 걸리도록 하는 역할을 하는데, 에러메시지가 지나치게 많아지고 느려지는 문제때문에 사용하지 않는 것을 권장한다.</p>
<h3 id="설치-1">설치</h3>
<p><code>npm i -D eslint-config-prettier</code></p>
<pre><code class="language-js">// .eslintrc.cjs
extends: [
    &#39;airbnb&#39;,
    &#39;airbnb-typescript&#39;,
    &#39;airbnb/hooks&#39;,
    &#39;plugin:react/recommended&#39;,
    &#39;plugin:@typescript-eslint/recommended&#39;,
    &#39;plugin:prettier/recommeded&#39;
  ],</code></pre>
<h2 id="husky">husky</h2>
<p><a href="https://typicode.github.io/husky/">https://typicode.github.io/husky/</a>
협업 과정에서 개발자들끼리 합의한 eslint나 prettier 등 지켜야 하는 규칙들이 존재하는데, 이런 코드 스타일은 개개인의 에디터에서 적용한다.</p>
<p>개인 사용자가 error나 warning을 무시한 채, 협업 프로젝트에 코드를 커밋하고 푸시하는 것에 제한이 필요하다.
이를 제한하지 않으면 규칙없이 작성된 코드가 머지되고 배포될 가능성이 생긴다.</p>
<p>git에는 hook 기능이 존재하는데, 특정 이벤트(add, commit, push 등)를 실행할 때, 그 이벤트에 hook을 설정하여 hook에 설정된 스크립트를 실행할 수 있다.</p>
<p>git hook을 관리할 수 있게 해주는 husky라는 패키지가 있다.
쉽게 말해서 git 명령어가 실행되면 미리 지정해놓은 스크립트가 실행되도록 관리해주는 도구이다.
여기서는 commit 전에 eslint와 prettier를 작동하게 하고자 한다.</p>
<h3 id="설치-2">설치</h3>
<pre><code class="language-js">// 설치
npm install husky --save-dev

// git hook 활성화
npx husky init</code></pre>
<p>npx husky init 명령어 실행하면 package.json 파일에 아래와 같은 스크립트가 생성된다.</p>
<pre><code class="language-js">&quot;scripts&quot;: {
  &quot;prepare&quot;: &quot;husky install&quot; 
}</code></pre>
<pre><code class="language-js">// 새로운 hook 추가
npx husky add .husky/pre-commit &quot;npm lint --fix&quot;</code></pre>
<p>이제 커밋을 하면 lint 스크립트 실행 후 커밋이 진행된다.</p>
<p>추가적으로 협업 개발자가 npm install할 때 husky도 install하게 해주는 스크립트도 추가해준다.
postinstall은 npm install 직후 자동으로 실행된다.</p>
<pre><code class="language-js">&quot;scripts&quot;: {
  &quot;prepare&quot;: &quot;husky install&quot;,
  &quot;postinstall&quot;: &quot;husky install&quot;
}</code></pre>
<h2 id="lint-staged">lint-staged</h2>
<p><a href="https://github.com/lint-staged/lint-staged">https://github.com/lint-staged/lint-staged</a>
위 작업에서 끝내면 모든 파일을 대상으로 lint 스크립트가 실행된다.
프로젝트 파일 수가 많아질수록 실행 속도도 늘어날 것이다.
그래서 변경된 소스에서만 hook이 동작되도록 설정하는 것이 필요하다.
이를 해결하기 위해 lint-staged 패키지를 사용한다.</p>
<h3 id="설치-3">설치</h3>
<pre><code class="language-js">npm install lint-staged --save-dev</code></pre>
<p>설치 이후 package.json에 원하는 스크립트를 추가한다.
나는 아래의 스크립트를 추가했다.
커밋시에 lint와 prettier를 동시에 진행할 수 있다.</p>
<pre><code class="language-js">// package.json
&quot;lint-staged&quot;: {
    &quot;*.{ts, tsx}&quot;: [
      &quot;eslint --fix&quot;
    ],
    &quot;*&quot;: [
      &quot;prettier --write --ignore-unknown&quot;
    ]
 },</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Same-Origin Policy와 CORS]]></title>
            <link>https://velog.io/@cloud_oort/Same-Origin-Policy%EC%99%80-CORS</link>
            <guid>https://velog.io/@cloud_oort/Same-Origin-Policy%EC%99%80-CORS</guid>
            <pubDate>Tue, 21 May 2024 04:16:27 GMT</pubDate>
            <description><![CDATA[<h1 id="same-origin-policy동일-출처-정책">Same-Origin Policy(동일 출처 정책)</h1>
<p>Same-Origin Policy는 웹 보안 메커니즘 중 하나로 실행중인 애플리케이션의 출처와 다른 출처의 리소스를 요청하는 것을 제한하는 정책이다.</p>
<p>브라우저는 Same-Origin Policy(동일 출처 정책)에 따라 실행중인 애플리케이션의 출처와 다른 출처의 리소스를 요청하면 차단한다.
여기서 말하는 출처는 프로토콜, 호스트, 포트 번호를 말하며, 이 세 가지 요소가 모두 동일한 경우 두 리소스가 동일한 출처를 가지고 있다고 판단한다.</p>
<p>XMLHTTPRequest, Fetch API, 쿠키, 로컬 스토리지 등과 같은 웹 API에 적용된다.</p>
<p>하지만 서로 다른 출처를 아예 차단해버리는 SOP(Same-Origin Policy)를 곧이 곧대로 따르면 상당히 불편할 수 있다.</p>
<p>출처가 서로 다른 요청도 가능하게 하면서 보안상의 문제도 해결하기 위해서 등장한 것이 바로 CORS이다.</p>
<h1 id="cors">CORS</h1>
<p>CORS는 Cross Origin Resource Sharing의 약자이며, 교차 출처 리소스 공유를 의미한다.</p>
<h2 id="동작-흐름">동작 흐름</h2>
<ol>
<li>브라우저는 다른 출처의 리소스를 요청할 때 <code>Origin</code> 헤더에 요청 출처를 기록한다.</li>
<li>서버는 <code>Access-Control-Allow-Origin</code> 헤더에 허용할 출처를 기록하여 응답 헤더에 담아 보낸다.</li>
<li>브라우저는 <code>Origin</code> 의 출처와 <code>Access-Control-Allow-Origin</code> 헤더의 출처를 비교하여 동일하지 않은 출처로 판단하면 CORS 에러를 발생시킨다.</li>
</ol>
<p>여기서 중요한 점은 서버가 응답값과 상태코드 200을 보내도, 브라우저가 CORS 위반 사실을 확인하면 애써 받은 응답값을 버린다는 점이다.
(대부분의 요청은 사전 요청 방식(preflight)으로 이루어져, 실제 요청이 이루어지기 전 CORS 위반 에러를 만날 수 있다.)</p>
<blockquote>
<p>즉 에러 발생 주체는 서버가 아니라 브라우저라는 사실을 알아야 한다.</p>
</blockquote>
<h2 id="요청-종류">요청 종류</h2>
<h3 id="단순-요청">단순 요청</h3>
<p>아래 조건을 충족하는 단순 요청은 사전 요청을 트리거하지 않는다.
하지만 대부분의 경우 조건을 충족하지 못하고 사전 요청을 한다.</p>
<ul>
<li>GET, POST, HEAD 메서드 중에 하나 사용</li>
<li>자동 설정 헤더를 제외하고 아래의 헤더값만 수동으로 설정된 것<ul>
<li>Accept</li>
<li>Accept-Language</li>
<li>Content-Language</li>
<li>Content-Type(헤더 값이 아래 값 중 하나일 때)<ul>
<li>application/x-www-form-urlencoded</li>
<li>multipart/form-data<ul>
<li>text-plain<h3 id="사전-요청preflight">사전 요청(preflight)</h3>
사전 요청이란 실제 요청을 보내기 전, 해당 출처 리소스에 접근 권한이 있는지 브라우저와 서버가 서로 확인하기 위한 요청을 의미한다.
사전 요청은 일반적인 상황에서는 브라우저에서 자동으로 발생한다.<h3 id="흐름">흐름</h3>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<ol>
<li>브라우저에서 서버로 예비 요청 전송</li>
</ol>
<ul>
<li>브라우저는 OPTIONS 메서드로 요청을 보낸다.</li>
<li><code>Origin</code>에 요청 출처를 기입한다.</li>
<li>지원하고자 하는 메서드는 <code>Access-control-Request-Method</code> 헤더에 기입한다.</li>
<li>사용자 정의 헤더를 설정했을 경우 <code>Access-Control-Request-Headers</code> 헤더에 기입한다.</li>
</ul>
<ol start="2">
<li>서버는 어떤 것을 허용하고 금지하는지 헤더에 담아서 응답한다.</li>
</ol>
<ul>
<li><code>Access-Control-Allow-Origin</code> 헤더에 허용 출처를 기입한다.</li>
<li><code>Access-Control-Allow-Methods</code> 헤더에 허용 메서드를 기입한다.</li>
<li><code>Access-Control-Allow-Headers</code> 헤더에 허용 헤더를 기입한다.<h4 id="캐시">캐시</h4>
매번 사전 요청을 보내는 것은 본 요청만 보내는 단순 요청에 비해 네트워크 비용이 더 크다.
이를 해결 하기 위해 서버의 승인 하에 브라우저가 사전 요청을 생략할 수 있다.
서버는 <code>Access-Control-Max-Age</code> 헤더에 초단위로 캐시 사용 기한을 설정할 수 있다.</li>
</ul>
<h3 id="자격-증명-포함-요청">자격 증명 포함 요청</h3>
<p>기본적으로 브라우저는 보안상의 이유로 교차 출처 요청에 자격 증명을 포함하지 않는다.
여기서 말하는 자격 증명은 쿠키 또는 Authorization 헤더를 의미한다.
이를 가능하게 하기 위해서 프론트와 서버 양측에서 CORS를 설정해야 한다.</p>
<h4 id="프론트">프론트</h4>
<p>Fetch API를 사용할 경우 credentials 옵션을 설정한다.</p>
<ul>
<li>same-origin(기본값): 같은 출처 사이에만 인증 정보를 담는다.</li>
<li>include: 모든 요청에 인증 정보를 담는다.</li>
<li>omit: 모든 요청에 인증 정보를 담지 않는다.</li>
</ul>
<p>Axios를 사용할 경우 withCredentials 옵션을 설정한다.</p>
<ul>
<li>기본값은 false이다.</li>
<li>자격 증명 요청을 위해서 값을 true로 설정한다.<h4 id="서버">서버</h4>
</li>
</ul>
<ol>
<li>응답 헤더에 <code>Access-Control-Allow-Credentials : true</code> 를 추가한다.</li>
<li>응답 헤더의 <code>Access-Control-Allow-Origin</code> 을 정확하게 설정한다. 즉, 와일드카드(*)를 사용하지 않는다.</li>
<li>응답 헤더의 <code>Access-Control-Allow-Methods</code>의 값을 지정해야 할 경우 와일드카드 문자(*)는 사용할 수 없다.</li>
<li>응답 헤더의 <code>Access-Control-Allow-Headers</code>의 값을 지정해야 할 경우 와일드카드 문자*)를 는 사용할 수 없다.</li>
</ol>
<p>예외적으로 사전 요청은 credential 정보를 포함할 수 없다. 하지만 응답 헤더에는 <code>Access-Control-Allow-Credentials : true</code> 를 넣어줘야 한다.</p>
<h2 id="레퍼런스">레퍼런스</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">https://developer.mozilla.org/ko/docs/Web/HTTP/CORS</a>
<a href="https://developer.mozilla.org/ko/docs/Glossary/CORS">https://developer.mozilla.org/ko/docs/Glossary/CORS</a>
<a href="https://jeonghwan-kim.github.io/2023/12/12/cors">https://jeonghwan-kim.github.io/2023/12/12/cors</a>
<a href="https://evan-moon.github.io/2020/05/21/about-cors/#sopsame-origin-policy">https://evan-moon.github.io/2020/05/21/about-cors/#sopsame-origin-policy</a>
<a href="https://velog.io/@garcon/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-CORS%EC%99%80-credentials">https://velog.io/@garcon/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-CORS%EC%99%80-credentials</a>
<a href="https://junglast.com/blog/http-ajax-withcredential">https://junglast.com/blog/http-ajax-withcredential</a>
<a href="https://codingmoon.io/posts/interview-questions/describe-cors/">https://codingmoon.io/posts/interview-questions/describe-cors/</a>
<a href="https://codingmoon.io/posts/interview-questions/describe-same-origin-policy/">https://codingmoon.io/posts/interview-questions/describe-same-origin-policy/</a>
<a href="https://velog.io/@cszzi1006/%EC%B0%A9%ED%95%98%EA%B3%A0-%EC%A2%8B%EC%9D%80-%EC%B9%9C%EA%B5%AC-CORS?utm_source=oneoneone">https://velog.io/@cszzi1006/%EC%B0%A9%ED%95%98%EA%B3%A0-%EC%A2%8B%EC%9D%80-%EC%B9%9C%EA%B5%AC-CORS?utm_source=oneoneone</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 자바스크립트에서 비동기 코드를 실행하는 방법]]></title>
            <link>https://velog.io/@cloud_oort/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@cloud_oort/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%8B%A4%ED%96%89%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 10 Jan 2024 08:03:05 GMT</pubDate>
            <description><![CDATA[<h2 id="자바스크립트-런타임">자바스크립트 런타임</h2>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/0b15118a-2420-4017-9725-d03a986cd8f3/image.png" alt=""></p>
<ul>
<li>모든 자바스크립트 코드는 자바스크립트 런타임에서 실행된다.</li>
<li>자바스크립트 런타임은 자바스크립트 코드를 실행하는 데 필요한 모든 부품이 포함된 컨테이너이다. <a href="https://velog.io/@cloud_oort/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%9F%B0%ED%83%80%EC%9E%84">자바스크립트 런타임 정리글</a></li>
<li>런타임의 핵심은 단연 자바스크립트 엔진이다. 이곳이 바로 코드가 실제로 실행되고 객체가 메모리에 저장되는 곳이다.</li>
<li>Web API는 엔진이 사용할 수 있는 API로 자바스크립트 언어와는 구별된다.</li>
<li>Callback queue는 이벤트에 부착된 모든 실행준비가 끝난 콜백함수를 갖고 있는 데이터 구조이다.</li>
<li>Event loop<ul>
<li>엔진의 call stack이 비면 이벤트 루프가 콜백큐의 첫 번째 콜백함수부터 차례대로 하나씩 콜스택에 넣어 실행한다. 이러한 작업을 event loop tick이라 한다.</li>
<li><strong>이벤트 루프가 자바스크립트에서 비동기 작업을 가능하게 하는 핵심 부품</strong>이다.</li>
</ul>
</li>
</ul>
<h2 id="비동기-코드가-백그라운드에서-실행되는-방법">비동기 코드가 백그라운드에서 실행되는 방법</h2>
<p>자바스크립트는 single threaded 언어로 한 번에 한 가지 일만 할 수 있는데, 어떻게 비동기 코드를 실행할 수 있을까?
<img src="https://velog.velcdn.com/images/cloud_oort/post/2f15e92b-7179-47a1-8fac-e4148a157480/image.png" alt=""></p>
<ul>
<li>DOM 관련된 모든 것은 자바스크립트 언어가 아니고 Web API의 일부이다.</li>
<li>즉 DOM 관련 비동기 작업은 자바스크립트 엔진의 call stack이 아니라 브라우저의 Web API 환경에서 실행된다. (DOM, Timer, AJAX, fetch 호출 등)</li>
<li>예를 들어 이미지 로딩은 call staack이 아니라 Web API 환경에서 비동기적으로 발생하고, load 이벤트가 끝나면 이벤트에 부착된 콜백함수가 콜백큐로 들어간다.
그리고 콜스택이 빌 경우, 이벤트 루프에 의해 순서에 맞게 콜스택으로 들어가 실행된다.</li>
<li>즉 load 이벤트에 부착된 콜백함수는 load 이벤트가 끝날 때까지 Web API 환경에 등록되어 이벤트가 끝나기를 기다리다가 이벤트가 끝나면 콜백큐에 들어간다.</li>
<li>콜백큐의 데이터 구조는 말 그대로 큐이기 때문에 FIFO(First In First Out) 원칙을 지킨다. 그래서 가장 먼저 들어간 콜백함수가 먼저 콜스택에 들어감으로써 실행순서를 보장한다.</li>
</ul>
<blockquote>
<p>즉 Web API 환경과 콜백큐, 이벤트 루프를 이용하여 비동기 코드가 single threaded 환경에서도 non-blocking하게 실행될 수 있다.</p>
</blockquote>
<h3 id="timer-사용-관련">Timer 사용 관련</h3>
<ul>
<li>Web API에서 setTimeout과 같은 timer API를 사용할 때 약간의 문제가 발생한다.</li>
<li>시간을 5초로 설정해두었을 때, 5초 이후에 콜백함수가 무조건 실행된다고 보장할 수 없기 때문이다.</li>
<li>타이머에 부착된 콜백함수는 5초 동안 Web API에서 대기하다가 콜백큐도 들어가는데, 콜백큐에 많은 수의 콜백함수가 대기하고 있을 경우 곧바로 실행하지 못하는 상황이 발생한다.</li>
<li>즉 콜백함수가 5초 이후에 실행된다는 것은 보장하지만, 정확히 5초가 끝나자마자 실행된다는 것은 보장할 수 없다.</li>
</ul>
<blockquote>
<p>그렇다면 다른 콜백함수보다 우선순위를 높일 수 있는 promise를 사용해보자.</p>
</blockquote>
<h2 id="promise-callback">promise callback</h2>
<ul>
<li>promise 관련 콜백함수는 콜백큐에 들어가지 않는다. 대신 promise의 콜백이 들어가는 특별한 큐가 있다.</li>
<li>이를 <strong>Microtasks Queue</strong>라 한다.</li>
<li>이것이 특별한 이유는 콜백큐보다 높은 우선순위를 가지기 때문이다.</li>
<li>즉 이벤트 루프는 콜백큐에 있는 콜백함수를 실행하기 전에 마이크로태스크 큐에 콜백함수가 있는지 확인하고, 있으면 마이크로태스크 큐에 있는 콜백을 모두 실행하고 정규 콜백큐로 넘어간다.</li>
</ul>
<blockquote>
<p>즉 promise 콜백이 다른 콜백보다 우선순위가 높다.</p>
</blockquote>
<h3 id="예시">예시</h3>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/dbbbb46f-d999-4240-8bdf-1b675733fe3d/image.png" alt=""></p>
<ul>
<li>비동기 작업을 제외한 작업을 순서대로 실행하고, promise callbakc을 실행된 뒤에야 setTimeout이 실행된다.</li>
<li>0초 타이머라고 해서 0초 이후에 바로 실행된다는 것을 보장하지 않음을 증명한다.</li>
<li>만약 promise의 작업이 5초 이상 걸리는 작업이라면 0초 타이머라 해도 5초 이후에 실행된다.</li>
<li>때문에 타이머로 정밀한 작업을 하려면 promisify 해서 사용하는 것이 좋다.</li>
</ul>
<h2 id="참조">참조</h2>
<p><a href="https://dkrnfls.tistory.com/362">https://dkrnfls.tistory.com/362</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 비동기 통신(AJAX, Promise)]]></title>
            <link>https://velog.io/@cloud_oort/JS-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%86%B5%EC%8B%A0AJAX-Promise</link>
            <guid>https://velog.io/@cloud_oort/JS-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%86%B5%EC%8B%A0AJAX-Promise</guid>
            <pubDate>Tue, 09 Jan 2024 15:50:37 GMT</pubDate>
            <description><![CDATA[<h1 id="ajaxasynchronous-javascript-and-xml">AJAX(Asynchronous Javascript And XML)</h1>
<p>AJAX는 자바스크립트를 이용하여 웹 서버와 브라우저가 비동기적으로 통신할 수 있는 개발 기법이다.
AJAX 통신을 사용하면, 페이지의 새로고침 없이도 URL에서 데이터를 가져올 수 있어서 페이지의 일부분만 업데이트가 가능하다.</p>
<h2 id="흐름">흐름</h2>
<ul>
<li>클라이언트는 데이터를 가지고 있는 웹서버에 AJAX로 HTTP 요청을 할 수 있다.</li>
<li>서버는 클라이언트가 요청한 데이터를 포함하는 응답을 보낸다.</li>
<li>클라이언트와 서버 사이의 작업은 모두 백그라운드에서 비동기적으로 발생한다.</li>
</ul>
<h2 id="xmlhttprequest">XMLHttpRequest</h2>
<ul>
<li><p>XMLHttpRequest는 옛날 AJAX 프로그래밍에 많이 사용되었다.</p>
<pre><code class="language-js">const getCountry = (country) =&gt; {
const request = new XMLHttpRequest();
request.open(&#39;GET&#39;, `https://restcountries.eu/rest/v2/name/${country}`)
request.send();

// 비동기 통신 load가 모두 완료되면 실행
request.addEventListener(&#39;load&#39;, function() {
  const [data] = JSON.parse(this.responseText)      
})
}
getCountry(&#39;south korea&#39;)</code></pre>
<h2 id="callback-hell">Callback Hell</h2>
<p>비동기 작업을 연속으로 실행하기 위해 콜백함수를 중첩하는 것을 의미한다.</p>
</li>
<li><p>코드가 지저분해진다.</p>
</li>
<li><p>이해하기 어렵기 때문에 유지 및 보수가 어렵다.</p>
<pre><code class="language-js">setTimeout(() =&gt; {
console.log(&#39;1 second passed&#39;);
setTimeout(() =&gt; {
  console.log(&#39;2 seconds passed&#39;);
  setTimeout(() =&gt; {
    console.log(&#39;3 second passed&#39;);
    setTimeout(() =&gt; {
      console.log(&#39;4 second passed&#39;);
    }, 1000);
  }, 1000);
}, 1000);
}, 1000);</code></pre>
<blockquote>
<p>ES6의 promise를 통해 callback hell을 해결할 수 있다.</p>
</blockquote>
</li>
</ul>
<h2 id="promise">Promise</h2>
<p>promise란 비동기 작업의 미래의 결과가 담길 장소이다.</p>
<ul>
<li>비동기 결과를 처리하기 위해 이벤트와 콜백함수에 의존할 필요가 없다. 그 결과값은 promise에 담길 것이기 때문.</li>
<li>비동기 작업의 순서를 위해 chain promises를 사용함으로써 콜백지옥을 피할 수 있다.<h3 id="promise-state">Promise state</h3>
비동기 작업은 시간이 지나변서 값이 변하는 특징이 있다 이를 표시하기 위해 여러 state와 cycle이 존재한다.</li>
<li>pending(비동기 작업으로 결과의 값을 사용할 수 있기 전의 상태)</li>
<li>settle(비동기 작업이 끝난 상태, 이제 상태값을 바꾸는 것은 불가능하다.)<ul>
<li>fulfilled(성공)</li>
<li>rejected(실패)<h3 id="fetch-함수">fetch 함수</h3>
</li>
</ul>
</li>
<li>fetch 함수는 promise를 리턴한다.</li>
<li>then 메서드로 promise를 처리하고 받은 값은 response 객체이다.</li>
<li>response 객체에서 실제 데이터를 읽기 위해서 json 메서드를 사용한다.</li>
<li>json 메서드는 또다시 promise를 리턴한다. 다시 then 메서드로 promise를 처리한다.<pre><code class="language-js">const getCountryData = (country) =&gt; {
  fetch(`https://restcountries.com/v2/name/${country}`)
        .then(response =&gt; response.json());
        .then(data =&gt; console.log(data));
}</code></pre>
<h3 id="error-handling">Error Handling</h3>
</li>
<li>catch 메서드로 에러를 캐치할 수 있고 catch 또한 promise를 리턴한다.</li>
<li>finally 메서드는 성공하든 실패하든 무조건 실행하는 메서드이다.<pre><code class="language-js">const getCountryData = (country) =&gt; {
  fetch(`https://restcountries.com/v2/name/${country}`)
        .then(response =&gt; response.json())
        .then(data =&gt; console.log(data))
      .catch(err =&gt; console.error(err))
      .finally(() =&gt; console.log(&#39;finish&#39;));
}</code></pre>
<h3 id="building-promise">Building Promise</h3>
</li>
<li>생성자를 이용하여 promise를 직접 만들 수 있다.</li>
<li>promise 생성자는 한 개의 인수를 받는다.<ul>
<li>그 인수는 promise 생성자 함수에서 바로 실행되는 실행자 함수이다.</li>
<li>이 실행자 함수는 promise의 비동기 결과값을 생성한다.</li>
</ul>
</li>
<li>실행자 함수는 두 개의 인수 <code>resolve</code> 함수와 <code>reject</code> 함수를 받는다.<ul>
<li>이 두 함수는 promise의 결과값을 처리하는 함수이며 response 객체를 리턴한다.</li>
<li>조건을 충족하면 <code>resolve</code> 함수를 호출하고 state를 <code>fufilled</code>로 바꾼다.</li>
<li>조건이 불충족하면 <code>reject</code>함수를 호출하고 state를 <code>rejected</code>로 바꾼다.<h4 id="예시">예시</h4>
</li>
</ul>
</li>
<li>기존의 콜백함수 기반의 비동기 작업을 promise를 이용하여 캡슐화하고 추상화를 쉽게 할 수 있다.</li>
<li>즉 기존의 콜백함수를 promisify하여 우선순위를 더 높일 수 있다.</li>
<li>예를 들어 콜백함수를 인수로 받는 setTimeout과 같은 비동기작업을 promise를 리턴하는 함수로 캡슐화할 수 있다.</li>
<li>보통 fetch 처럼 promise를 리턴하는 함수 형태로 캡슐화를 한다.</li>
<li>이를 통해 콜백지옥을 해결할 수 있고 유지보수가 더 쉬워졌다.<pre><code class="language-js">// 콜백지옥, 코드 이해하기 힘들다.
setTimeout(() =&gt; {
  console.log(&#39;waited more 1 second&#39;);
      setTimeout(() =&gt; {
      console.log(&#39;waited more 2 second&#39;);
          setTimeout(() =&gt; {
          console.log(&#39;waited more 3 second&#39;);
              setTimeout(() =&gt; {
              console.log(&#39;waited more 4 second&#39;);
          }, 1000);
      }, 1000);
  }, 1000);
}, 1000);
</code></pre>
</li>
</ul>
<p>// promisify를 통해 콜백지옥 해결
const wait = (seconds) =&gt; {
    return new Promise((resolve) =&gt; {
        setTimeout(resolve, seconds*1000);
    })
}</p>
<p>wait(1)
    .then(() =&gt; {
        console.log(&#39;Wait for 1 seconds&#39;);
          // promise를 리턴해서 then 메서드로 처리
          return wait(1);
    })
    .then(() =&gt; {
        console.log(&#39;waited for 2 seconds&#39;);
        return wait(1);
    })
    .then(() =&gt; {
        console.log(&#39;waited for 3 seconds&#39;);
        return wait(1);
    })</p>
<pre><code>### Async Await
- 함수에 async 키워드를 붙이면 그 함수는 백그라운드에서 실행되는 비동기 함수가 된다.
- async 함수는 promise를 리턴한다.
- async 함수 안에서 한 개 또는 그 이상의 await문을 사용할 수 있다.
    - await문은 promise의 상태가 fulfill될 때까지 실행을 멈춘다.
    - **중요한 것은 async 함수는 비동기 함수이기 때문에 백그라운드에서 실행되어 메인 쓰레드의 실제 코드 실행을 멈추지 않는다는 것이다.**
    - 즉 await이 자바스크립트 엔진의 call stack 실행을 막는다는 뜻이 아니다.
    - 이것이 async/await이 특별한 이유다!
    **async 함수이 실행 과정을 보면, await문 때문에 동기적 코드로 보인다. 하지만 함수 자체가 백그라운드에서 실행되는 비동기함수이다.**
    - 작업이 모두 끝나면 await문은 resolve된 값이 되고 그것을 변수에 저장할 수 있다.
    - 동기적 코드처럼 콜백함수 없이 promise의 결과값을 변수에 저장할 수 있기 때문에 코드가 더 쉽고 깔끔해졌다.
- **async/await은 새로운 것이 아니라 예전 then 메서드의 syntax sugar일 뿐이다. 즉 여전히 promise를 사용**한다.

#### Error Handling
- async/await은 try...catch를 통해 error handling을 한다.
- catch block은 try block에서 발생한 error에 권한을 갖는다.
```js
const test = async () =&gt; {
    try {
        const data = await getData();
        console.log(data);
    } catch(err) {
        console.error(err);
    }
}</code></pre><h4 id="returning-values-from-async-functions">Returning values from async functions</h4>
<ul>
<li>일반 promise를 사용하면 then 메서드를 통해 promise를 처리한다.</li>
<li>async function은 promise를 리턴한다.</li>
<li>async function과 then 메서드를 섞어 사용하면 불편하다.</li>
<li>이럴 경우에는 IIFE(Immediately Invoked Function Expression) 패턴을 사용해보자</li>
<li>await을 사용하려면 async 함수가 필요하니 한 번 쓰고 사라지는 IIFE 패턴을 사용하면 좋다.<pre><code class="language-js">(async funciton() {
   try {
       const data = await getData();
   } catch(err) {
      console.error(error);
  }
})();</code></pre>
<h3 id="promise-combinator">Promise Combinator</h3>
<h4 id="all-method">all method</h4>
</li>
<li>all method를 사용하면 실행순서가 상관없는 Promise를 parallel하게 처리할 수 있기 때문에 로딩 시간을 줄일 수 있다.</li>
<li>all method는 promise 배열을 받아서 처리한 결과를 배열로 받는다.</li>
<li>하나라도 에러가 발생하면 에러가 발생한다.<pre><code class="language-js">const getJSON = function (url) {
  return fetch(url).then(response =&gt; {
      return response.json();
  })
}
</code></pre>
</li>
</ul>
<p>// 3개의 promise를 동시에 처리하고 3개의 결과를 동시에 받는다.
(async function () {
    const res = await Promise.all([
        getJSON(<code>https://restcountries.com/v2/name/italy</code>),
        getJSON(<code>https://restcountries.com/v2/name/egypt</code>),<br>        getJSON(<code>https://restcountries.com/v2/name/mexico</code>),
    ])
})()</p>
<pre><code>#### race method
- promise 배열을 받아서 그 중에서 가장 먼저 settle이 된 promise의 결과값을 리턴한다.
- settle은 fulfill이라 reject와는 상관없다. 가장 먼저 settle이 된 promise만 리턴한다.
- 예를 들어서 사용자의 인터넷 연결이 안좋을 경우, 일정 시간이 지나면 reject를 리턴하는 promise를 실행하도록 한다.
```js
// 일정 시간이 지나면 error를 리턴
const timeout = function (sec) {
    return new Promise((resolve, reject) {
        setTimeout(() =&gt; {
            reject(new Error(&#39;Request took too long!&#39;))&#39;
        }, sec*1000);
    })
}

// 1초 이상 지나면 timeout promise가 실행
Promise.race([
    getJSON(`https://restcountries.com/v2/name/tanzania`),
    timeout(1),
])
  .then(res =&gt; console.log(res[0]))
  .catch(err =&gt; console.error(err));</code></pre><h4 id="allsettled">allSettled</h4>
<ul>
<li>promise 배열을 받아서 settle된 promise 결과를 리턴한다.</li>
<li>all method는 어느 하나라도 reject가 되면 에러가 발생했지만, allSettled method는 결과 상관없이 settle되기만 하면 다 리턴한다.<pre><code class="language-js">Promise.allSettled([
  Promise.resolve(&#39;success&#39;),
  Promise.reject(&#39;fail&#39;),
  Promise.resolve(&#39;success&#39;),
]).then(res =&gt; console.log(res));</code></pre>
<h4 id="any">any</h4>
</li>
<li>race method와 비슷하지만 any는 reject는 무시하고 가장 먼저 fulfill된 promise 하나를 리턴한다.</li>
<li>만약 모든 promise가 reject되어 리턴할 fulfiil promise가 없다면 error가 발생한다.<pre><code class="language-js">// 가장 먼저 fulfill된 &#39;success2&#39;를 리턴받는다.
Promise.any([
  Promise.reject(&#39;fail&#39;),
  Promise.resolve(&#39;success2&#39;),
  Promise.resolve(&#39;success1&#39;)
])
.then(res =&gt; console.log(res))    
.catch(err =&gt; console.error(err));</code></pre>
<h2 id="참조">참조</h2>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function</a>
<a href="https://poiemaweb.com/js-ajax">https://poiemaweb.com/js-ajax</a>
<a href="https://poiemaweb.com/js-async">https://poiemaweb.com/js-async</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 프로토타입 상속을 이용한 생성자 함수와 ES6 Class]]></title>
            <link>https://velog.io/@cloud_oort/JS-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%83%81%EC%86%8D%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-ES6-Class</link>
            <guid>https://velog.io/@cloud_oort/JS-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%83%81%EC%86%8D%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-ES6-Class</guid>
            <pubDate>Mon, 01 Jan 2024 17:29:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>생성자 함수와 class를 통해 프로그래밍적으로 객체를 생성할 수 있다.</p>
</blockquote>
<h2 id="생성자-함수">생성자 함수</h2>
<ul>
<li>정규 함수와 같지만, 유일한 차이는 반드시 <code>new</code> 연산자를 사용해 호출한다는 것이다.</li>
<li>함수 이름의 첫글자는 대문자여야 한다.</li>
<li>화살표 함수는 고유의 this keyword가 없기 때문에 함수 선언식과 함수 표현식으로만 가능하다. <a href="https://velog.io/@cloud_oort/JS-This-keyword">this keyword 정리</a></li>
<li><strong><code>new</code> 연산자로 생성자 함수를 호출하면 함수 기능뿐만 아니라 그 이상의 일을 한다.</strong><ol>
<li>생성자 함수 타입의 빈 객체를 즉시 생성한다.</li>
<li>함수가 호출되면, 함수 안의 this keyword가 새로 생성된 객체를 가리킨다. 생성자 함수 내에서 <code>console.log(this)</code> 로 확인해보면 빈 객체가 나온다. (<code>this = {}</code>)</li>
<li>새로 생성된 객체가 생성자 함수의 프로토타입과 연결된다.(아래 예시에서는 객체가 <code>Person.prototype</code>과 연결된다.)</li>
<li>생성자 함수 내에서는 입력받은 값을 this keyword의 property로 설정한다.</li>
<li>생성자 함수는 this 객체를 리턴한다.</li>
<li>리턴된 객체가 바로 생성자 함수로부터 생성된 객체이다.</li>
</ol>
</li>
</ul>
<pre><code class="language-js">const Person = function(firstName, lastName) {
    console.log(this); // {}
    this.firstName = firstName;
      this.lastName = lastName;
}

const oort = new Person(cloud, oort);
// Person {firstName: &#39;cloud&#39; , lastName: &#39;oort&#39;}

// oort는 Person의 instance이다.
console.log(oort instanceof Person); // true</code></pre>
<ul>
<li>자바스크립트에는 전통적인 OOP의 클래스가 없다. 하지만 생성자 함수를 통해 만든 객체를 인스턴스라 한다.</li>
<li>생성자 함수 내에서 property를 받는 것은 좋지만, method를 정의하는 것은 매우 비효율적인 일이다. 
method를 가지고 있는 생성자 함수가 1000개의 인스턴스를 만들면 1000개의 함수 복사본을 만드는 것과 같기 때문에 코드 실행에 최악이다.<blockquote>
<p><strong>이 문제를 해결하기 위해 prototypal inheritance를 이용한다.</strong></p>
</blockquote>
<h3 id="prototypal-inheritance프로토타입-상속">Prototypal Inheritance(프로토타입 상속)</h3>
</li>
<li>프로토타입 상속은 method를 공유할 때 강점을 드러낸다.</li>
<li>특정 생성자 함수로 생성된 모든 객체는 생성자 함수의 prototype과 연결되어 있다.</li>
<li>그렇기 때문에 <strong>생성자 함수를 통해 생성된 객체는 생성자 함수의 prototype에 정의된 모든 property와 method에 접근할 수 있다.</strong><blockquote>
<p>즉 <strong>생성자 함수의 prototype에 method를 정의해서, 생성자 함수와 연결된 객체가 생성자 함수의 prototype에 정의된 method를 끌어당겨 쓰는 것이 프로토타입 상속</strong>이다. 코드를 재사용하는 메커니즘이라 할 수 있다.</p>
</blockquote>
<h4 id="예시">예시</h4>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/d40b9c0b-a334-495a-aef5-0048c0f30cc8/image.png" alt=""></p>
<ul>
<li>자바스크립트의 모든 함수(생성자 함수 포함)는 <code>prototype</code>이라는 propery를 자동으로 가지고 있다.</li>
<li>자바스크립트의 모든 객체는 <code>__proto__</code>라는 property를 갖고 있다.</li>
<li><code>new</code> 키워드로 생성자 함수를 생성할 때, 함수 내의 this 키워드가 생성자 함수의 프로토타입과 연결된다고 했고 그 this 키워드가 리턴된다고 했다.</li>
<li>리턴된 this 키워드가 바로 인스턴스이며 인스턴스의 <code>__proto__</code> property와 생성자 함수의 <code>prototype</code>은 연결되었기 때문에 값이 같다.<blockquote>
<p><code>oort.__proto__</code>은 항상 <code>Person.prototype</code>을 가리킨다.</p>
</blockquote>
</li>
<li>즉 생성자 함수의 property에 method를 한 번만 정의해두면, 그 뒤로 생성된 객체는 생성자 함수의 <code>prototype</code>과 연결된 객체의 <code>__proto__</code> property를 통해 method를 사용할 수 있다.</li>
</ul>
<h3 id="프로토타입-체인">프로토타입 체인</h3>
<ul>
<li>생성자 함수로 생성한 객체가 자기 자신에 없는 property 또는 method를 사용할 때, 객체의 프로토타입 즉 생성자 함수의 프로토타입에서 property 또는 method를 찾는다.</li>
<li>생성자 함수의 프로토타입 또한 객체이기 때문에 프로토타입을 가지고 있다.</li>
<li>프로토타입으로 null을 가진 객체에 도달할 때까지 이 연결은 계속되며 이를 프로토타입 체인이라 한다.</li>
</ul>
<h3 id="주의할-점">주의할 점</h3>
<ul>
<li>생성자 함수의 <code>prototype</code>이 생성자 함수의 프로토타입은 아니라는 점을 유의해야 한다.</li>
<li>즉 생성자 함수의 <code>prototype</code> property는 생성자 함수의 프로토타입이 아니라 생성자 함수를 통해 생성된 모든 객체와 연결된 프로토타입이다.</li>
<li>그래서 생성자 함수로 만든 모든 인스턴스 객체의 <code>__proto__</code> property는 생성자 함수의 <code>prototype</code> property와 같은 값을 가진다.</li>
<li>생성자 함수의 prototype의 constructor가 생성자 함수와 같다.</li>
<li>모든 객체는 프로토타입을 가지고 있기 때문에 객체 타입의 생성자 함수의 prototype도 프로토타입이 있는데, 바로 Object.prototype이다. 이 Object는 프로토타입 체인의 최상단이기 때문에 Object의 프로토타입은 null값을 가진다.</li>
</ul>
<h2 id="생성자-함수와-es6-class">생성자 함수와 ES6 class</h2>
<ul>
<li>자바스크립트의 ES6 class는 전통적인 OOP의 클래스처럼 동작하지 않으며, 생성자 함수의 syntax sugar일 뿐이다.</li>
<li><strong>내부적으로는 생성자 함수와 class 모두 동일하게 프로토타입 상속을 구현</strong>하지만 다른 프로그래밍 언어를 사용하는 사람들이 이해하기 쉽도록 <strong>class라는 synstax sugar</strong>로 표현한 것일 뿐이다. 
그렇기 때문에 class는 단지 특별한 형식의 함수일 뿐이다.</li>
</ul>
<h3 id="차이">차이</h3>
<ul>
<li>생성자 함수는 property를 정의할 때, 생성자 함수 안에 정의한다.</li>
<li>class는 constructor 함수안에 정의한다. <blockquote>
<p>하지만 둘 다 method는 그 밖에서 정의한다. 이는 위에서 말한 프로토타입 상속과 방법이 일치한다.</p>
</blockquote>
</li>
<li>즉 <strong>method를 생성자 함수에서 정의하는 것은 비효율적</strong>이기 때문에 
=&gt; <strong>method를 생성자 함수의 prototype에 직접 정의하는 것</strong>과 
=&gt; <strong>class를 사용할 때 method를 constructor 함수 바깥에서 정의하는 것</strong>은 프로토타입 상속을 이용하는 원리가 같지만 문법적으로만 다르게 보일 뿐인 syntax sugar이다.<pre><code class="language-js">const Person = function(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}
// method를 생성자 함수의 prototype에 직접 정의하는 것
Person.prototype.hello = function() {
  console.log(&#39;hi&#39;)
}
</code></pre>
</li>
</ul>
<p>const oort = new Person(&#39;cloud&#39;, &#39;oort&#39;);</p>
<p>// 위 방법과 아래 방법은 원리상 같지만 문법적으로만 다른 systax sugar임을 잘 보여준다.
class ClassPerson {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // class를 사용할 때 method를 constructor 함수 바깥에서 정의하는 것
    hello() {
        console.log(&#39;hi&#39;)
    }
}</p>
<p>const oort = new ClassPerson(&#39;cloud&#39;, &#39;oort&#39;);</p>
<pre><code>
## 정리
- 생성자 함수와 class는 모두 같은 원리로 객체를 생성한다.
- 생성자 함수는 method를 정의할 때 prototype 속성에 직접 정의해야 했지만, class의 경우 constructor 함수 바깥에만 정의하면 클래스의 prototype 속성에 자동으로 정의된다.
- 생성자 함수보다 클래스의 사용이 훨씬 편하지만, 그 이전에 생성자 함수, 프로토타입, 프로토타입 체인을 잘 이해하는 것이 필요하다.

&gt; 즉 생성자 함수와 클래스 모두 사용해도 되지만 프로토타입에 대한 완전한 이해가 뒤따라야 한다.

## 참조

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/new
https://poiemaweb.com/js-object-oriented-programming
https://www.udemy.com/course/the-complete-javascript-course/</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] OOP(Object-Oriented Programming) & Class]]></title>
            <link>https://velog.io/@cloud_oort/JS-OOPObject-Oriented-Programming</link>
            <guid>https://velog.io/@cloud_oort/JS-OOPObject-Oriented-Programming</guid>
            <pubDate>Fri, 29 Dec 2023 08:02:11 GMT</pubDate>
            <description><![CDATA[<h2 id="what-is-oop">What is OOP?</h2>
<ul>
<li>OOP(Object-Oriented Programming, 객체 지향 프로그래밍)란 객체 개념에 기반을 둔 프로그래밍 패러다임을 의미한다.</li>
<li>객체를 이용하면 property에 해당하는 데이터와 method에 해당하는 동작을 하나의 큰 블록(객체)에 담을 수 있다.</li>
<li>데이터와 동작이 담긴 객체는 독립적인 작은 앱처럼 동작하며 그 객체들끼리 상호작용한다.</li>
<li>OOP는 더 유연하고 유지 보수하기 쉽도록 코드 조직화를 하기 위해 개발된 프로그래밍 패러다임이며 대규모 소프트웨어 엔지니어링 분야에서 광범위하게 사용되고 있다.</li>
<li><strong>전통적인 OOP는 클래스를 사용하지만 자바스크립트는 클래스가 아닌 프로토타입을 사용한다.</strong><h2 id="class-and-instance">Class and Instance</h2>
OOP에서는 객체 리터럴뿐만 아니라 객체를 프로그래밍적으로 생성할 방법이 필요하다.
전통적인 OOP에서는 클래스라는 것을 이용한다. (클래스 기반 OOP)<h3 id="class">Class</h3>
</li>
<li>클래스는 보통 청사진(blue print)으로 묘사된다.</li>
<li>클래스는 단순 설계도이기 때문에 실체가 없다.(데이터와 데이터관련 동작이 있지만 데이터 자체는 아니다.)</li>
<li>클래스에 묘사된 청사진에 따라 새로운 객체를 만들 수 있다.</li>
<li>JavaScript에는 실제 클래스를 지원하지 않는다.(클래스 문법이 있지만 전통적인 클래스와는 다르게 동작한다.)</li>
</ul>
<h3 id="instance">Instance</h3>
<ul>
<li>인스턴스는 클래스라는 청사진을 보고 만든 실제 객체를 의미한다.</li>
<li>클래스를 통해 필요한만큼 많은 객체를 생성할 수 있다.(이 과정을 Instanciation이라 한다.)</li>
<li>같은 클래스(설계도)를 보고 만든 것이기 때문에 모두 같은 기능을 가지고 있으면서, 서로 다른 데이터를 가질 수 있다.</li>
</ul>
<h2 id="the-4-fundatmental-oop-principles">The 4 Fundatmental OOP Principles</h2>
<p>좋은 클래스는 어떻게 디자인되어야 할까.
4가지 고려 사항이 있다.</p>
<h3 id="abstraction추상화">Abstraction(추상화)</h3>
<ul>
<li>추상화란 사용자에게 필요한 정보만 제공하고 부가적인 것은 최소화하는 것을 의미한다.
  프로그래밍을 할 때도 우리는 프로그래밍 언어의 사용자이기 때문에 언어를 완전히 이해하거나 직접 구현하지 않아도 추상화를 통해 언어가 제공하는 기능을 간단히 사용할 수 있다.<h3 id="encapsulation캡슐화">Encapsulation(캡슐화)</h3>
</li>
<li>일부 property와 method를 외부에서 접근하지 못하도록 막는 것을 의미한다.</li>
<li>외부 코드가 내부의 데이터를 직접 조작할 수 있을 경우 많은 종류의 버그가 발생할 수 있다. 그래서 항상 객체 내의 일부 property와 method를 캡슐화하고 핵심적인 method만 공개한다.</li>
<li>공개된 method는 public API라고도 부른다.<h3 id="inheritance상속">Inheritance(상속)</h3>
</li>
<li>유사한 두 클래스가 있을 때, 중복된 코드를 작성하지 않고 하나의 클래스가 다른 클래스를 상속할 수 있게 한다.</li>
<li>parent class와 child class가 있고, child class가 부모로부터 상속받게 될 경우 parent class의 모든 property와 method를 물려받는다.</li>
<li>상속을 통해 두 클래스 사이에 계층구조가 형성되고, 공통 로직을 재사용할 수 있다.<h3 id="polymorphism다형성">Polymorphism(다형성)</h3>
</li>
<li>그리스어에서 유래, &#39;다수의 형태&#39;라는 뜻이다.</li>
<li>간단히 말해서 parent class에서 물려받은 method를 바꿔야 하는 경우 원하는대로 커스텀할 수 있다.</li>
</ul>
<h2 id="oop-in-javascript">OOP in JavaScript</h2>
<h3 id="oop가-javascript에서-동작하는-원리">OOP가 Javascript에서 동작하는 원리</h3>
<p>자바스크립트에서 OOP는 전통적인 클래스가 아닌 프로토타입을 이용한다.</p>
<h4 id="prototypal-inheritance프로토타입-상속-이용">Prototypal Inheritance(프로토타입 상속) 이용.</h4>
<ul>
<li>자바스크립트의 모든 객체는 특정 프로토타입 객체에 연결되어 있다.(즉 모든 객체는 프로토타입이 있다.)</li>
<li>프로토타입 객체는 프로토타입에 연결된 모든 객체가 접근하고 사용할 수 있는 property와 method를 가지고 있다.</li>
<li><strong>프로토타입 상속은 특정 프로토타입 객체에 연결된 모든 객체는 그 프로토타입에서 정의된 property와 method를 사용할 수 있음을 의미</strong>한다.</li>
<li>예를 들어 <code>Array.prototype</code>은 자바스크립트에서 만든 모든 배열의 프로토타입 객체이다. 즉 모든 배열은 <code>Array.prototype</code>에 있는 모든 method를 상속받는다. </li>
<li><strong>우리가 이때까지 forEach, map과 같은 method를 정의하지 않아도 배열의 method로 사용할 수 있는 동작 원리이다.</strong>
<img src="https://velog.velcdn.com/images/cloud_oort/post/d022101a-8289-4337-9439-e9938a0199bd/image.png" alt=""></li>
<li>배열을 선언하고 콘솔로 찍어보면 배열이 우리가 사용했던 methode들이 포함된 prototype에 연결된 것을 볼 수 있다. 저 prototype이 바로 <code>Array.prototype</code>이다.</li>
</ul>
<h4 id="클래스-상속과-프로토타입-상속의-차이">클래스 상속과 프로토타입 상속의 차이</h4>
<ul>
<li>클래스 상속은 클래스가 다른 클래스로부터 상속받는 것을 의미한다.</li>
<li><strong>프로토타입 상속은 객체가 자신과 연결된 프로토타입의 property와 method를 사용할 수 있음을 의미</strong>한다.</li>
<li>전통적인 클래스에서는 method가 인스턴스로 복사되지만, 프로로타입 상속은 프로토타입 객체와 연결된 객체에 method를 위임한다.<h3 id="객체-생성-방법">객체 생성 방법</h3>
자바스크립트에는 클래스 개념이 없기 때문에 별도의 객체 생성 방법이 존재한다.</li>
<li>객체 리터럴</li>
<li>생성자 함수 <a href="https://velog.io/@cloud_oort/JS-%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%81%B4%EB%9E%98%EC%8A%A4">생성자 함수 정리글</a><ul>
<li>생성자 함수와 <code>new</code>키워드를 통해 인스턴스를 생성할 수 있다.</li>
<li>이때 생성자 함수는 클래스이자 생성자(constructor)의 역할을 한다.</li>
</ul>
</li>
<li>ES6 Class <a href="https://velog.io/@cloud_oort/JS-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%83%81%EC%86%8D%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-ES6-Class">Class 정리글</a><ul>
<li>ES6에 도입된 class는 자바스크립트에서 OOP를 구현하는 현대적인 방법이다.</li>
<li>주의할 점은 도입된 class문법은 우리가 알고 있던 전통적인 class가 아닌 생성자 함수의 syntax sugar라는 사실이다. 생긴 모습만 class이고 실제 동작은 생성자 함수와 같으며 프로토타입 상속을 이용한다.</li>
</ul>
</li>
<li>Object.create()<ul>
<li>객체를 프로토타입 객체에 연결하는 가장 쉽고 직관적인 방법이다.</li>
</ul>
</li>
</ul>
<h2 id="참고">참고</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object-oriented_programming">https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object-oriented_programming</a>
<a href="https://poiemaweb.com/js-object-oriented-programming">https://poiemaweb.com/js-object-oriented-programming</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Closure 정리]]></title>
            <link>https://velog.io/@cloud_oort/JS-Closure-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@cloud_oort/JS-Closure-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 18 Dec 2023 06:12:51 GMT</pubDate>
            <description><![CDATA[<h1 id="closure">Closure</h1>
<ul>
<li><code>secureBooking</code> 함수는 로컬 스코프의  <code>passengerCount</code> 변수를 업데이트하는 함수를 리턴한다.</li>
<li>전역변수 <code>booker</code>는 <code>secureBooking</code> 함수의 리턴값으로 <code>passengerCount</code> 변수를 업데이트하는 함수를 받는다.</li>
<li>그리고 <code>booker</code> 실행하면 결과가 어떻게 될까.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/cloud_oort/post/1a85ea8b-05a1-45de-822e-0f115d09f491/image.png" alt=""></p>
<ul>
<li><p><code>passengerCount</code> 변수가 업데이트되고 <code>console.log</code> 가 실행된다.</p>
<blockquote>
<p>이미 실행이 끝난 <code>secureBooking</code> 의 함수 내에 선언된 <code>passengerCount</code> 변수를 전역 스코프의<code>booker</code> 함수가 사용할 수 있었던 이유가 무엇일까.</p>
</blockquote>
</li>
<li><p><code>booker</code> 함수는 전역 스코프에 존재하는 함수이다.</p>
</li>
<li><p><code>secureBooking</code> 함수는 실행이 끝났기 때문에 execution context의 변수 환경도 완전히 사라졌다.</p>
</li>
<li><p>하지만 <code>booker</code> 함수는 당시 존재했던 <code>passengerCount</code> 변수에 계속 접근할 수 있다.</p>
</li>
<li><p><code>booker</code> 함수는 전역 스코프이기 때문에 scope chain으로도 <code>passengerCount</code> 변수에는 접근할 수 없다.</p>
</li>
</ul>
<blockquote>
<p>이를 가능하게 한 것은 바로 <strong>closure</strong>이다.</p>
</blockquote>
<h2 id="closure-동작-원리">Closure 동작 원리</h2>
<ul>
<li><code>booker</code> 에 할당된 함수는 call stack에서 사라진 <code>secureBooking</code> 함수의 execution context에서 선언되었다.</li>
<li><strong>하지만 <code>booker</code> 함수는 자신이 선언된 함수의 execution context가 사라져도 그 execution context의 변수 환경에 항상 접근할 수 있다.</strong></li>
<li><strong>이러한 연결고리를 closure</strong>라 한다.</li>
<li>즉 <code>booker</code> 함수는 closure를 이용하여 자신이 선언된 곳인 <code>secureBooking</code> 함수의 변수 환경에 항상 접근할 수 있다.</li>
</ul>
<blockquote>
<p><strong>다시 정리하면, 함수는 함수 자신의 변수 환경과 자신이 선언된 함수의 변수 환경에 접근할 수 있고 이러한 연결고리를 closure라 한다.</strong></p>
</blockquote>
<ul>
<li>더 쉽게 말하면 <strong>부모 함수가 사라져도, 부모 함수의 모든 변수에 접근할 수 있게 해주는 것</strong>이 closure이다.</li>
<li>이러한 closure는 부모 함수의 변수 환경에 접근하는 자식 함수가 존재하는 경우 계속 유지된다. 이때 변수의 복사본이 아니라 실제 변수에 접근한다는 것에 주의하여야 한다.</li>
<li>자바스크립트 엔진은 로컬 스코프(자신의 변수 환경) -&gt; closure(자신이 선언된 함수의 변수 환경) -&gt; scope chain 순으로 변수를 탐색한다.</li>
</ul>
<h2 id="closure-관찰">closure 관찰</h2>
<ul>
<li>closure 관련 작업은 자바스크립트가 내부적으로 처리하기 때문에 우리는 수동으로 closure 관련 작업을 할 수 없다.</li>
<li>closure는 우리가 접근할 수 있는 객체가 아니고, 명시적으로 closure에 접근할 방법 또한 없다.</li>
<li>즉 closure는 실재하는 것이 아니다. 함수의 내부 속성일 뿐이다.</li>
</ul>
<p>하지만 콘솔을 통해 함수 내부의 closure 속성을 직접 볼 수 있다.
<img src="https://velog.velcdn.com/images/cloud_oort/post/30fef5b8-007d-41d6-b145-4085696b1fe2/image.png" alt=""></p>
<ul>
<li>scopes는 booker의 변수 환경을 나타내는데, closure가 우선인 것을 볼 수 있다.</li>
<li>closure를 통해 함수가 선언된 execution context가 사라져도 변수 환경을 보존할 수 있다.</li>
<li><code>[[]]</code> 로 표시된 것은 내부 속성이기 때문에 코드로 접근할 수 없다는 의미이다.</li>
</ul>
<h2 id="한-가지-의문">한 가지 의문</h2>
<p>closure는 자신이 선언된 부모 함수의 변수 환경에 항상 접근할 수 있다고 하는데, 부모 함수의 부모 함수, 즉 조부모 함수의 변수 환경에는 어떤 방식으로 접근될까
<img src="https://velog.velcdn.com/images/cloud_oort/post/93bf44e3-3798-467c-b48e-c513333df31a/image.png" alt=""></p>
<p>결과는 다음과 같다</p>
<ul>
<li>조부모 함수에서 선언된 변수 <code>i</code> 와 부모 함수에서 선언된 변수 <code>j</code> 에 정상적으로 접근할 수 있었다.</li>
<li>또한 closure는 각각 어느 함수의 변수환경인지 명시적으로 알려주고 있으며, 부모 함수와 조부모 함수 순으로 나열되어 있다.
이를 통해 closure가 하나가 아니라는 사실을 알 수 있었다. </li>
<li><em>closure는 함수마다 하나씩 존재*</em>했다.</li>
<li>즉 위 상황에서 자바스크립트 엔진이 변수를 찾는 과정이 다음과 같다.
로컬 스코프(자신의 변수 환경) -&gt; closure(부모 함수의 변수 환경) -&gt; closure(조부모 함수의 변수 환경) -&gt; scope chain 순이다.</li>
</ul>
<h2 id="추가-예시">추가 예시</h2>
<h3 id="예시-1">예시 (1)</h3>
<ul>
<li>꼭 함수를 리턴하지 않아도 된다.</li>
<li>함수가 변수 자체일때도 closure가 발생한다.</li>
<li>또한 변수가 재할당되면 closure 또한 변한다.<pre><code class="language-js">let f;
const g = () =&gt; {
const a = 4;
f = function() {
    console.log(a*5)
}
}
</code></pre>
</li>
</ul>
<p>const h = () =&gt; {
  const b = 7;
  f = function() {
      console.log(b*2);
  }
}</p>
<p>g(); // 함수 할당
f(); // 20</p>
<p>h(); // 함수 재할당
f(); // 14</p>
<pre><code>### 예시 (2)
- 타이머 또한 함수를 리턴하지 않아도 closure를 볼 수 있는 좋은 예시이다.
- 파라미터 또한 로컬 변수인점을 기억해야 한다.
- 함수 실행이 끝나고 execution context가 사라져도 함수는 자신이 선언된 함수의 변수 환경을 기억하기 때문에 정상 동작하는 것을 알 수 있다.
```js
const boardPassengers = (n, wait) =&gt; {
  const perGroup = n / 3;
  setTimeOut(() =&gt; {
      console.log(`We are now boarding all ${n} passengers`);
    console.log(`There are 3 groups. each with ${perGroup} 
  }, wait * 1000);
};

boardPassengers(180, 3);</code></pre><h3 id="예시-3">예시 (3)</h3>
<ul>
<li><p>IIFE 패턴에서의 이벤트 콜백함수 예시에서도 closure를 볼 수 있다.</p>
</li>
<li><p>콜백이 실행될 때쯤이면 IIFE 즉, 즉시 호출된 함수 식은 실행된 후 사라진다.</p>
</li>
<li><p>하지만 콜백함수는 자신이 생성된 장소에서 생성된 변수에 여전히 접근할 수 있다.</p>
<pre><code class="language-js">(function() {
const header = document.querySelector(&#39;h1&#39;);
header.style.color = &#39;red&#39;;

// 콜백함수는 여전히 header 변수에 접근할 수 있다.
document.body.addEventListener(&#39;click&#39;, () =&gt; {
    header.style.color = &#39;blue&#39;;
})
})()</code></pre>
</li>
</ul>
<h2 id="참조">참조</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures">https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures</a>
<a href="https://poiemaweb.com/js-closure">https://poiemaweb.com/js-closure</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Immediately Invoked Function Expression(IIFE)]]></title>
            <link>https://velog.io/@cloud_oort/JS-Immediately-Invoked-Function-ExpressionIIFE</link>
            <guid>https://velog.io/@cloud_oort/JS-Immediately-Invoked-Function-ExpressionIIFE</guid>
            <pubDate>Fri, 15 Dec 2023 06:50:06 GMT</pubDate>
            <description><![CDATA[<h1 id="immediately-invoked-function-expressioniife">Immediately Invoked Function Expression(IIFE)</h1>
<p>IIFE는 즉시 실행 함수 표현을 의미한다. 
즉시 실행하고 바로 사라진다.</p>
<h2 id="사용">사용</h2>
<pre><code class="language-js">(function() {
    console.log(&#39;IIFE&#39;);
})()</code></pre>
<ul>
<li>이름 없는 함수 식을 괄호로 래핑하면 함수식(function statement)이 아닌 표현(expression)으로 속일 수 있다.</li>
<li>그리고 다른 함수들과 마찬가지로 뒤에 ()를 붙여 호출한다.</li>
<li>이는 자바스크립트만의 특징이 아니고 개발자들이 생각해낸 패턴에 가깝다.</li>
</ul>
<h2 id="등장-이유">등장 이유</h2>
<ul>
<li>ES6 이전에는 전역 스코프와 함수 스코프만 존재했다.
그래서 if나 for문과 같이 블록(중괄호)안에서 선언된 변수는 외부에서 마음껏 사용할 수 있었다.</li>
<li>개발 도중, 블록 안에서 선언한 변수를 외부에서 사용하지 못하게 하고 싶었다.</li>
<li><strong>블록 스코프를 인위적으로 만들어내기 위해 IIFE와 같은 패턴이 등장</strong>했다.</li>
<li>스코프를 만들기 위해 함수를 선언하여 호출했고, 스코프만을 위한 함수였기 때문에 한 번 호출 이후 바로 사라지게 만들었다.</li>
<li>하지만 ES6 이후 블록 스코프가 등장했기 때문에 모던 자바스크립트에서는 위와 같은 용도로 IIFE 패턴을 사용하지 않는다.</li>
<li>그래도 <strong>한 번만 사용하는 함수를 만들 때는 유용</strong>하다.<ul>
<li>전역 변수 제한</li>
<li>비동기 함수 실행(top-level await 구현 가능)<h2 id="참조">참조</h2>
<a href="https://developer.mozilla.org/ko/docs/Glossary/IIFE">https://developer.mozilla.org/ko/docs/Glossary/IIFE</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Call, Apply and Bind methods]]></title>
            <link>https://velog.io/@cloud_oort/JS-Call-Apply-and-Bind-methods</link>
            <guid>https://velog.io/@cloud_oort/JS-Call-Apply-and-Bind-methods</guid>
            <pubDate>Wed, 13 Dec 2023 07:08:16 GMT</pubDate>
            <description><![CDATA[<h1 id="first-class-citizen--this-keyword">first-class citizen &amp; this keyword</h1>
<ul>
<li>함수는 객체의 한 종류이기 때문에 메서드를 가질 수 있다. 
<a href="https://velog.io/@cloud_oort/JS-First-Class-citizen-and-Higher-Order-functions">일급시민과 고차원함수 정리</a></li>
<li>정규 함수(화살표 함수 제외)에서 this 키워드를 사용하게 되면 strict mode에 한에서 this 값은 <code>undefined</code>이다.</li>
<li>this 키워드는 호출될 때 값이 정해지는 동적인 값이다. <a href="https://velog.io/@cloud_oort/JS-This-keyword">this 키워드 정리</a></li>
<li>여기서 <strong>this 키워드를 수동적으로 정의할 수 있는 방법</strong>이 있다.
함수에 부착된 메서드인 Call, Apply and Bind method를 이용하면 된다.</li>
</ul>
<h2 id="call-method">Call method</h2>
<pre><code class="language-js">const a = {
  name: &#39;oort&#39;,
  booking(one, two) {
    // 여기서 this는 a를 가리킨다.
      console.log(this.name + `${one} ${two}`
  }
}

// book 변수에 a의 booking 메서드를 재할당
const book = a.booking;

// 여기서 this는 undefined이다.
book(1, 2)

// call 메서드의 첫번째 인수는 this 키워드가 가졌으면 하는 값이다.
// 그리고 뒤에 메서드에 필요한 인수들을 입력한다.
book.call(a, 1, 2);
// 정상동작 =&gt; oort12</code></pre>
<ul>
<li>위 예시를 보면 <code>book</code> 이라는 변수에 <code>a</code> 의 메서드인 <code>booking</code> 을 재할당했다.</li>
<li>그리고 <code>book</code> 함수를 호출하면 <code>book</code> 의 소유주는 없기 때문에 <code>book</code>에서 사용한 this 키워드는 <code>undefined</code> 가 된다.</li>
<li><strong>this 키워드를 수동으로 정의해주기 위해 call method를 사용</strong>한다.<ul>
<li>예시를 보면 <code>book</code> 함수를 직접 호출한 것이 아니다.<ul>
<li>call 메서드가 <code>a</code> 라는 키워드로 <code>book</code> 함수를 호출한 것이다.</li>
</ul>
</li>
</ul>
</li>
<li>모던 자바스크립트에서는 apply 보다는 call 을 사용한다.<h2 id="apply-method">Apply method</h2>
</li>
<li>call method와 동작이 같다.</li>
<li>다른 점은 뒤에 올 <strong>인수를 배열로 받는다</strong>는 것이다.<pre><code class="language-js">const a = {
name: &#39;oort&#39;,
booking(one, two) {
  // 여기서 this는 a를 가리킨다.
    console.log(this.name + `${one} ${two}`
}
}
const book = a.booking;
</code></pre>
</li>
</ul>
<p>// 여기서 this는 undefined이다.
book(1, 2)</p>
<p>const argu = [1, 2];
book.call(a, argu);</p>
<pre><code>## Bind method
```js
const a = {
    name: &#39;oort&#39;,
    book(num, value) {
          // this === a
        console.log(`${num} ${this.name} ${value}`)
    }
}
const b = {
    name: &#39;www&#39;,
}

// this === undefined
const book = a.book

// this === b
const bookB = book.bind(b)
// 첫번째 키워드는 call과 마찬가지로 this에 할당할 값
// 그 뒤에 인수는 입력이 자유롭다.
// 전부 넣어도 되고, 나중에 넣어도 되고, 일부만 넣어줘도 된다.

// 일부 인수 미리 지정해줄 경우
const bookB = book.bind(b, 12)
bookB(&#39;a&#39;)</code></pre><ul>
<li><p>bind method 또한 call method와 마찬가지로 this 키워드를 수동으로 설정한다.</p>
</li>
<li><p>차이점은 call은 즉시 함수를 호출하지만 bind는 즉시 그 함수를 호출하지 않는다는 것이다.</p>
</li>
<li><p>대신 <strong>원하는 this 키워드가 바인딩된 새 함수를 리턴</strong>한다.</p>
<h3 id="bind-함수에서-인수를-미리-지정해줄-수도-있다-나머지-인수는-호출할-때-넣어주면-된다partial-application">bind 함수에서 <strong>인수를 미리 지정해줄 수도 있다.</strong> 나머지 인수는 호출할 때 넣어주면 된다.(partial application)</h3>
</li>
<li><p>이 특성을 활용하여 인수가 미리 지정된 새로운 함수를 만들 때 사용할 수 있다.</p>
<ul>
<li><p>아래 예시처럼 this 키워드가 필요없을 때는 <code>null</code>을 넣는 것이 관습이다.</p>
</li>
<li><p>인수의 순서가 중요하다.</p>
</li>
<li><p>default parameter와 결과가 비슷해보이지만, bind는 범용 함수에 근거해서 인수를 미리 지정해놓은 새로운 함수를 리턴하는 것이기 때문에 다르다.</p>
<pre><code class="language-js">// 범용 함수
const addTex = (rate, value) =&gt; value + rate*value

// 범용함수에 근거해서 인수를 미리 지정해준 새로운 함수
const addVAT = addTax.bind(null, 0.23);</code></pre>
<h3 id="외부-객체의-메서드를-이벤트리스너의-콜백함수로-사용하는-경우-유용하다"><strong>외부 객체의 메서드를 이벤트리스너의 콜백함수로 사용하는 경우 유용</strong>하다.</h3>
</li>
</ul>
</li>
<li><p>이벤트리스너의 콜백함수에서 this를 사용할 경우, this는 해당 DOM element를 가리킨다.</p>
<ul>
<li>때문에 외부 객체의 메서드를 콜백함수로 사용하고자 할 때, this 값을 수동으로 설정할 필요가 있다.
=&gt; <strong>인수로 입력한 값을 this 키워드로 가지는 함수를 리턴하는 bind 메서드를 사용</strong>한다.<pre><code class="language-js">document.querySelector(&#39;.buy&#39;).addEventListener(&#39;click&#39;, a.book.bind(b))</code></pre>
<h3 id="이벤트리스너의-콜백함수에-인수를-추가할-때-유용하다"><strong>이벤트리스너의 콜백함수에 인수를 추가할 때 유용</strong>하다.</h3>
</li>
</ul>
</li>
<li><p>이벤트리스너의 콜백함수는 실행하면 안되고 함수 자체를 넘겨줘야 한다.</p>
<ul>
<li>그렇기 때문에 기존 파라미터인 event를 제외하면 추가 인수를 넘기는 것은 불가능하다.</li>
</ul>
</li>
<li><p>하지만 <strong>bind 메서드를 이용하여 추가하고 싶은 인수를 this에 할당하는 패턴으로 구현</strong>할 수 있다.</p>
<pre><code class="language-js">const nav = document.querySelector(&#39;.nav&#39;)
 // this를 사용하고 싶으면 화살표 함수가 아닌 정규 함수를 사용해야 한다.
 const clickHandler = function(event) {
    console.log(event.currentTarget) // nav element

    // 추가하고 싶은 인수를 bind 메서드를 이용하여 this에 할당하여 사용한다. 
  console.log(this) // 20
 }
 // 추가하고 싶은 인수를 this에 할당 =&gt; this === 20
 nav.addEventListenr(&#39;click&#39;, clickHandler.bind(20)                              </code></pre>
<h2 id="참조">참조</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/call</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/apply</a>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind</a>
<a href="https://www.udemy.com/course/the-complete-javascript-course/">https://www.udemy.com/course/the-complete-javascript-course/</a></p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>