<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>0_jin.log</title>
        <link>https://velog.io/</link>
        <description>가독성 좋은 코드, 성능 개선, 좋은 dx 경험, 자동화 를 생각합니다.</description>
        <lastBuildDate>Sun, 17 Aug 2025 05:31:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>0_jin.log</title>
            <url>https://velog.velcdn.com/images/0_jin/profile/c58d542b-3029-4af4-9355-117e5f818426/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 0_jin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/0_jin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[React] 작은 설계 차이가 만드는 큰 유지보수성]]></title>
            <link>https://velog.io/@0_jin/%ED%8C%9D%EC%97%85-%ED%98%B8%EC%B6%9C-%EC%9E%91%EC%9D%80-%EC%B0%A8%EC%9D%B4%EA%B0%80-%EB%A7%8C%EB%93%9C%EB%8A%94-%ED%81%B0-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@0_jin/%ED%8C%9D%EC%97%85-%ED%98%B8%EC%B6%9C-%EC%9E%91%EC%9D%80-%EC%B0%A8%EC%9D%B4%EA%B0%80-%EB%A7%8C%EB%93%9C%EB%8A%94-%ED%81%B0-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sun, 17 Aug 2025 05:31:51 GMT</pubDate>
            <description><![CDATA[<p>서비스 개발을 하다 보면, 사용자에게 팝업을 띄우는 것처럼 단순해 보이는 기능조차도 의외로 많은 고민을 요구합니다.</p>
<p>예를 들어, openModal() 함수를 어떻게 설계하느냐에 따라 코드의 확장성과 유지보수성이 크게 달라질 수 있습니다.</p>
<h3 id="1-데이터-객체를-통째로-주입하는-방식">1. 데이터 객체를 통째로 주입하는 방식</h3>
<pre><code class="language-tsx">openModal(data);</code></pre>
<ul>
<li>호출부에서는 데이터만 넘겨주면 된다.</li>
<li>함수 내부에서 알아서 분기해 UI를 결정한다.</li>
<li>사용자는 단순히 openModal만 호출하면 되니 편리하다.</li>
</ul>
<h3 id="2-mode-별-interface를-명시적으로-주입하는-방식">2. Mode 별 interface를 명시적으로 주입하는 방식</h3>
<pre><code class="language-tsx">openModal({ mode: &#39;card&#39;, ... });</code></pre>
<ul>
<li>어떤 모달 UI가 노출될지 명확하게 알 수 있다.</li>
<li>interface 확장 시 기존 로직에 영향을 주지 않는다.</li>
<li>불필요한 데이터가 주입되는 것을 막아 유지보수성이 높다.</li>
</ul>
<hr>
<h2 id="왜-interface-분리가-더-좋은가">왜 interface 분리가 더 좋은가?</h2>
<p>단순히 “깔끔해 보인다”는 이유 때문이 아닙니다.
<code>예측 가능성</code> 과 <code>확장성</code> 이라는 중요한 원칙을 지킬 수 있기 때문입니다.</p>
<h2 id="1-예측-가능성">1. 예측 가능성</h2>
<p>호출부에서 mode만 봐도 어떤 UI가 렌더링될지 알 수 있습니다.
이 작은 차이가 협업 시 오해를 줄이고, 버그 발생 가능성을 낮춥니다.</p>
<h2 id="2-확장에-유리함-solid-open-close-principle">2. 확장에 유리함 (SOLID: Open-Close Principle)</h2>
<p>새로운 모달 타입이 추가되더라도 기존 호출부에는 영향을 주지 않습니다.
반면, data 객체를 통째로 넘기는 방식은 기존 필드와 충돌하거나 불필요한 필드 관리가 필요해져 유지보수 비용이 커집니다.</p>
<p>다음은 예제 코드입니다.</p>
<pre><code class="language-tsx">// 공통 data 객체
interface ModalData {
  title?: string;
  description?: string;
  cardNumber?: string;
  productId?: string;
  // 앞으로 계속 추가될 수 있음...
}

function openModal(data: ModalData) {
  if (data.cardNumber) {
    // 카드 모달
  } else if (data.productId) {
    // 상품 모달
  } else {
    // 기본 모달
  }
}
</code></pre>
<p>새로운 모달 타입(예: couponModal)이 추가된다면,
ModalData에 { couponId?: string } 같은 필드를 추가하고,
openModal() 내부에도 else if (data.couponId) 분기를 또 작성해야 합니다.</p>
<p>이 경우 호출부는 openModal이 내부 조건 분기에 따라 어떻게 동작할지 예측하기 어렵습니다.<br>또한 새로운 타입 추가로 인해 불필요한 필드 주입까지 요구되면서, 호출부 개발자는 어떤 데이터를 넘겨야 할지 혼란스러워집니다.<br>결국 이는 연관 없는 모달 로직에도 사이드 이펙트를 유발합니다.</p>
<h2 id="3-불필요한-의존성-제거-solid-interface-segregation-principle">3. 불필요한 의존성 제거 (SOLID: Interface Segregation Principle)</h2>
<p>각 모달이 필요로 하는 데이터만 interface로 정의됩니다.
덕분에 한 모달이 다른 모달의 데이터 구조에 불필요하게 의존하지 않게 됩니다.</p>
<pre><code class="language-tsx">// 모달별 interface 정의
interface CardModalProps {
  mode: &#39;card&#39;;
  cardNumber: string;
}

interface ProductModalProps {
  mode: &#39;product&#39;;
  productId: string;
}

interface CouponModalProps {
  mode: &#39;coupon&#39;;
  couponId: string;
}

// union 타입으로 확장
type ModalProps = CardModalProps | ProductModalProps | CouponModalProps;

function openModal(props: ModalProps) {
  switch (props.mode) {
    case &#39;card&#39;:
      // 카드 모달
      break;
    case &#39;product&#39;:
      // 상품 모달
      break;
    case &#39;coupon&#39;:
      // 쿠폰 모달
      break;
  }
}</code></pre>
<ul>
<li>새로운 모달을 추가하더라도 기존 interface나 호출부를 수정할 필요가 없습니다.</li>
<li>ModalProps union에 새로운 타입을 추가하는 것으로 확장이 끝납니다.</li>
<li>OCP 원칙(기존 코드 닫혀 있음, 확장에는 열려 있음)에 부합.</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>팝업 호출이라는 단순한 기능에서도 원칙 있는 설계는 큰 차이를 만듭니다.<br>저는 interface 분리 방식을 통해 <strong>예측 가능한 코드</strong>, <strong>변경에 강한 구조</strong>를 지향합니다.  </p>
<p>결국 좋은 아키텍처는 화려한 기술이 아니라, 작은 선택에서 비롯되기에 간단한 코드 작성 또한 심의를 기울여야합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ChatGPT를 사용하는 번역 기능에서 악의적인 요청과 과금 문제 해결 방법]]></title>
            <link>https://velog.io/@0_jin/ChatGPT%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%88%EC%97%AD-%EA%B8%B0%EB%8A%A5%EC%97%90%EC%84%9C-%EC%95%85%EC%9D%98%EC%A0%81%EC%9D%B8-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EA%B3%BC%EA%B8%88-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@0_jin/ChatGPT%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%88%EC%97%AD-%EA%B8%B0%EB%8A%A5%EC%97%90%EC%84%9C-%EC%95%85%EC%9D%98%EC%A0%81%EC%9D%B8-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EA%B3%BC%EA%B8%88-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 06 Jul 2025 11:16:09 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트를 운영하면서 사용자가 작성한 <strong>게시글이나 댓글을 자동으로 번역</strong>해주는 기능을 구현했습니다. </p>
<p>이 기능은 ChatGPT API를 사용하며, 사용자 경험 향상에 큰 도움이 되었습니다.</p>
<p>하지만 서비스를 운영하면서 몇 가지 <strong>보안 및 비용 이슈</strong>에 대한 우려가 생겼고, 이에 대한 대응이 필요했습니다.</p>
<hr>
<h2 id="문제-정의">문제 정의</h2>
<h3 id="✅-1-정상적인-사용">✅ 1. 정상적인 사용</h3>
<ul>
<li>로그인한 사용자가 직접 게시글이나 댓글을 작성</li>
<li>번역 요청은 사용자의 실제 행위에 의해 발생</li>
</ul>
<p>→ 이 경우 번역 기능을 제공하는 데 문제가 없습니다.</p>
<h3 id="⚠️-2-문제-상황">⚠️ 2. 문제 상황</h3>
<p>두 가지 우려가 있었습니다:</p>
<ol>
<li><p><strong>악의적인 요청</strong><br>시스템을 의도적으로 과부하시키거나 비정상적인 방식으로 사용하는 경우</p>
</li>
<li><p><strong>과도한 비용</strong><br>악성 요청이나 예상 외 트래픽으로 인해 OpenAI API 사용 비용이 급증하는 경우</p>
</li>
</ol>
<hr>
<h2 id="대응-전략">대응 전략</h2>
<h3 id="🔐-1-악의적인-요청을-정의하고-제한하기">🔐 1. 악의적인 요청을 정의하고 제한하기</h3>
<h4 id="✅-케이스-1-요청자가-bot일-경우">✅ 케이스 1: 요청자가 Bot일 경우</h4>
<ul>
<li><code>Gatling</code>, <code>JMeter</code>, <code>curl</code>, <code>python-requests</code> 과 같이 브라우저가 아닌 요청이거나, User-Agent가 의심스러운 경우 <strong>Bot으로 간주</strong></li>
</ul>
<p>→ 이러한 요청에 대해 번역 기능을 제공하지 않음</p>
<h4 id="✅-케이스-2-짧은-시간-내-과도한-요청">✅ 케이스 2: 짧은 시간 내 과도한 요청</h4>
<ul>
<li>IP 단위로 일정 시간 안에 너무 많은 요청이 들어올 경우 비정상적인 행동으로 판단</li>
</ul>
<pre><code class="language-text">예시 조건:
- URL: /api/translate
- Method: POST
- 5분 동안 IP당 200건 이상 요청 시 자동 차단</code></pre>
<p>→ </p>
<h5 id="🖥️-서버-인스턴스가-1대인-경우">🖥️ 서버 인스턴스가 1대인 경우</h5>
<ul>
<li><code>Nginx</code>의 <code>limit_req</code> 기능으로 IP당 요청 횟수 제한</li>
</ul>
<h5 id="☁️-서버-인스턴스가-2대-이상인-경우">☁️ 서버 인스턴스가 2대 이상인 경우</h5>
<ul>
<li><code>Application Load Balancer(ALB)</code> 앞에 <code>AWS WAF</code>를 붙여 IP 기반 Rate Limit 적용</li>
</ul>
<h3 id="💸-2-요금-과다-청구를-방지하기">💸 2. 요금 과다 청구를 방지하기</h3>
<h4 id="✅-사용자당-일일-번역-횟수-제한">✅ 사용자당 일일 번역 횟수 제한</h4>
<ul>
<li>로그인한 사용자만 번역 기능 사용 가능 ( 기존 기획 ) </li>
<li>Redis를 사용하여 여러 인스턴스에서 사용자 번역 횟수 공유</li>
<li>하루 기준 요청 횟수를 초과하면 번역 기능 제공 X</li>
</ul>
<h3 id="📡-3-악의적인-요청-추적-및-slack-알림">📡 3. 악의적인 요청 추적 및 Slack 알림</h3>
<p>단순히 차단만 하는 것이 아니라, 이벤트 발생 시 Slack으로 실시간 알림을 받아 운영 상태를 가시화합니다.</p>
<p>✅ 1. WAF 기반 악성 요청 알림</p>
<pre><code>WAF Rate Limit 발생
 → CloudWatch Logs
 → Metric Filter: 차단 로그 감지
 → CloudWatch Alarm
 → SNS Topic 발행
 → Slack 알림 전송</code></pre><p>✅ 2. 사용자 기반 번역 제한 알림</p>
<p>Redis로 사용자 번역 횟수를 추적 후 
→ 일일 한도 초과 시 Slack Webhook으로 알림</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react native expo ios 배포하기 5]]></title>
            <link>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-5</link>
            <guid>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-5</guid>
            <pubDate>Tue, 24 Jun 2025 14:41:43 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>app store에 build한 bundle file을 Upload 하여 앱 출시 하기</p>
<h1 id="submit">submit</h1>
<p><code>eas build -p ios --local --profile production</code></p>
<p>ios build를 진행하고 나면  <code>build-*.ipa</code> 라는 파일이 생성된다.</p>
<p>나는 submit script 파일을 하나 생성하여 해당 파일 명을 eas submit을 진행하여
app store에 upload 하도록 하였다.</p>
<pre><code>#!/bin/bash

# 빌드 폴더 경로 (EAS 빌드 결과 기본 경로)
BUILD_OUTPUT_DIR=./
# 새 파일 이름 정의
NEW_FILE_NAME=&quot;build.ipa&quot;

# 원래 IPA 파일 이름 추출
IPA_FILE=$(find &quot;$BUILD_OUTPUT_DIR&quot; -name &quot;build-*.ipa&quot;)


# 파일 이름 변경
if [ -n &quot;$IPA_FILE&quot; ]; then
  echo &quot;IPA file renamed to $NEW_FILE_NAME&quot;
  mv &quot;$IPA_FILE&quot; &quot;$BUILD_OUTPUT_DIR/$NEW_FILE_NAME&quot;
else
  echo &quot;No IPA file found in $BUILD_OUTPUT_DIR&quot;
  exit 1
fi

echo &quot;Submit IOS..&quot;
eas submit -p ios --path &quot;$BUILD_OUTPUT_DIR/$NEW_FILE_NAME&quot;

echo &quot;IOS Submit Complete&quot;</code></pre><p>expo에서 해당 submit을 즉시 실행하는 것은 아니라서 기다려야한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react native expo ios 배포하기 4]]></title>
            <link>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-4</link>
            <guid>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-4</guid>
            <pubDate>Tue, 24 Jun 2025 14:35:12 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>react native expo를 사용하여 ios build 진행하기  </p>
<p><a href="https://github.com/jin-Pro/full-stack-monorepo/tree/develop/apps/rn">참고 레포</a></p>
<p>expo 를 사용하여 react native 를 개발하고 있고
eas 를 사용하여 배포를 진행하고 있다</p>
<pre><code>// app.json
{
  &quot;extra&quot;: {
    &quot;eas&quot;: {
      &quot;projectId&quot;: &quot;projectId&quot;
    },
  },
}</code></pre><p>를 통해 eas 설정을 하고 </p>
<pre><code>// eas.json
{
  &quot;cli&quot;: {
    &quot;version&quot;: &quot;&gt;= 14.0.2&quot;,
    &quot;appVersionSource&quot;: &quot;remote&quot;
  },

  &quot;build&quot;: {
    &quot;development&quot;: {
      &quot;developmentClient&quot;: true,
      &quot;distribution&quot;: &quot;internal&quot;,
      &quot;ios&quot;: {
        &quot;simulator&quot;: true
      }
    },
    &quot;production&quot;: {
      &quot;releaseChannel&quot;: &quot;production&quot;,
      &quot;distribution&quot;: &quot;store&quot;,
      &quot;autoIncrement&quot;: true,
      &quot;ios&quot;: {
        &quot;simulator&quot;: false
      }
    },
    &quot;preview&quot;: {
      &quot;distribution&quot;: &quot;internal&quot;,
      &quot;android&quot;: {
        &quot;buildType&quot;: &quot;apk&quot;
      }
    }
  },
  &quot;submit&quot;: {
    &quot;production&quot;: {}
  }
}</code></pre><p>해당 파일 설정을 진행하면,</p>
<pre><code>eas build -p ios --local --profile production</code></pre><p>해당 명령어를 통해 ios build 를 진행할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react native expo ios 배포하기 3]]></title>
            <link>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-3</link>
            <guid>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-3</guid>
            <pubDate>Tue, 24 Jun 2025 11:43:50 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>App notification을 위한 Firebase 등록하기</p>
<h1 id="-firebase-project-만들기-">[ Firebase Project 만들기 ]</h1>
<p><img src="https://velog.velcdn.com/images/0_jin/post/70bb8ad3-b293-46ad-a000-cf0b30864111/image.png" alt=""></p>
<p>Firebase 프로젝트 만들기 를 클릭합니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/ba8d4386-1ac9-45eb-a9fa-9bf12cc2ab17/image.png" alt=""></p>
<p>Firebase Project 이름을 입력해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/e37dfba6-caaf-4abe-b188-85b1deded109/image.png" alt=""></p>
<p>Firebase Project 설정에 들어갑니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/c67de9fb-eed3-415a-adb3-42d56e348e9f/image.png" alt=""></p>
<p>Firebase Project 설정 &gt; 일반 &gt; 내 앱 ( 하단 ) &gt; ios+ 클릭</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/bb6a049f-f42b-4565-afb3-6d677c0bcfc1/image.png" alt=""></p>
<p>1에서 작성했던 Apple 번들 ID를 입력합니다. ( com.company.appName or com.group.appName )</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/8f4c2c00-8d5a-4c0d-9c18-ae5d4fbbcdfe/image.png" alt=""></p>
<p>GoogleService-Info.plist를 다운하여 project root에 위치합니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/62eed618-c101-4b45-ac5c-1a0d7d0ae700/image.png" alt=""></p>
<p>해당 파일은</p>
<pre><code>// root/app.json 

{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;LinKorea&quot;,
    &quot;slug&quot;: &quot;linkorea&quot;,
    &quot;version&quot;: &quot;1.0.0&quot;,
    &quot;orientation&quot;: &quot;portrait&quot;,
    &quot;icon&quot;: &quot;./assets/images/icon_1024x1024.png&quot;,
    &quot;scheme&quot;: &quot;myapp&quot;,
    &quot;userInterfaceStyle&quot;: &quot;automatic&quot;,
    &quot;newArchEnabled&quot;: true,
    &quot;ios&quot;: {
      &quot;googleServicesFile&quot;: &quot;./GoogleService-Info.plist&quot;,
    }
  }
}</code></pre><p>위와 같이 root에 존재하는 app.json에서 ios의 googleServicesFile에서 참조할 것입니다. </p>
<blockquote>
<p>Android의 경우 google-services.json 를 사용합니다.</p>
</blockquote>
<p>프로젝트 생성을 마무리합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react native expo ios 배포하기 2]]></title>
            <link>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Tue, 24 Jun 2025 11:33:20 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>App Store Connect에 App 생성하기</p>
<ul>
<li><a href="https://appstoreconnect.apple.com/apps">https://appstoreconnect.apple.com/apps</a> 에 접속합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/0_jin/post/2798f66d-7686-4592-a091-19780b132888/image.png" alt=""></p>
<p>... 진행중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[react native  expo ios 배포하기 1]]></title>
            <link>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_jin/react-native-expo-ios-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 24 Jun 2025 11:26:52 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>react-native 의 expo를 사용하여 
app notification 까지 동작하는 webview 서비스를
web app 을 ios 에 배포하기 </p>
<h1 id="-identifiers-생성하기-">[ Identifiers 생성하기 ]</h1>
<p><a href="https://developer.apple.com/account/resources/identifiers/list">https://developer.apple.com/account/resources/identifiers/list</a> 에 접속하여 Identifiers 를 생성해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/f0e7a202-0d69-4a8d-b9c6-30f49a9934c8/image.png" alt=""></p>
<p>App IDs 를 선택하고 Continue를 누릅니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/52653ec0-646c-4aaf-9f34-571257a1f958/image.png" alt=""></p>
<p>App Type을 선택하고 Continue를 누릅니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/9fe7e506-473b-40d2-93d5-32321385c71d/image.png" alt=""></p>
<p>Description과 Bundle ID 를 입력해주고 </p>
<p>저의 경우
Description은 app name을 설정하여주었고
Bundle ID의 경우 com.company.appname ( or com.group.appname ) 으로 작성하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/bb040d67-b673-4349-8785-308b7479fe7f/image.png" alt=""></p>
<p>우리는 app push notification을 사용할 것이기에 Capabilities에서 Push Notifications 를 선택해줍니다.</p>
<h1 id="-identifiers-확인하기-">[ Identifiers 확인하기 ]</h1>
<p><img src="https://velog.velcdn.com/images/0_jin/post/aae8f82f-c044-486b-bed9-d4c93b02e57f/image.png" alt=""></p>
<p>Description 과 Bundle ID 를 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 선언적으로, Type 확장에 용이하게]]></title>
            <link>https://velog.io/@0_jin/React-%EC%84%A0%EC%96%B8%EC%A0%81%EC%9C%BC%EB%A1%9C-Type-%ED%99%95%EC%9E%A5%EC%97%90-%EC%9A%A9%EC%9D%B4%ED%95%98%EA%B2%8C</link>
            <guid>https://velog.io/@0_jin/React-%EC%84%A0%EC%96%B8%EC%A0%81%EC%9C%BC%EB%A1%9C-Type-%ED%99%95%EC%9E%A5%EC%97%90-%EC%9A%A9%EC%9D%B4%ED%95%98%EA%B2%8C</guid>
            <pubDate>Mon, 31 Mar 2025 14:29:30 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발을 하면서, 팀 내에서 나의 역할에 대해 자주 고민하곤 한다.
비즈니스 로직은 주로 서버 개발자가 다루는 경우가 많다 보니, 사용자 경험을 고려한 개발을 하더라도 과연 내가 팀에 필수적인 존재일까? 라는 의문이 들 때가 있다.</p>
<p>마치 단순히 외주 개발을 수행하는 것과 다를 바 없다는 생각이 들기도 한다. 하지만 프론트엔드 개발자는 서비스 사용자와 가장 가까운 위치에서 제품을 다룬다. 이 때문에 데이터 분석을 위한 로깅을 설정하고, 빠른 템포로 A/B 테스트를 도입하여 최적의 사용자 경험을 찾아가는 역할이 필수적이다.</p>
<p>이런 고민 속에서 유지보수하기 좋은 코드, 선언적이고 Type 확장에 용이한 코드에 대해 생각하게 된다.</p>
<p>프론트엔드 개발자는 더 나은 서비스를 위해 더 많은 시도를 하고, 더 많은 변화를 만들어내야 하는 직군이다. 즉, 변경이 많을 수밖에 없는 환경에서 유지보수성을 고려한 코드 작성이 곧 실력이며, 개발의 핵심 가치가 된다.</p>
<p>따라서, 앞으로 다룰 내용들은 단순히 &quot;좋은 코드 스타일&quot;을 넘어서, 변화를 유연하게 수용할 수 있는 코드의 중요성과 이를 실현하는 방법에 대해 이야기하려 한다.</p>
<hr>
<p>프론트엔드 개발에서 조건 분기 로직을 작성할 때, 유지보수성과 확장성을 고려한 코드 구조가 중요하다. 
특히, 새로운 타입이 추가되거나 변경해야하는 경우 이를 쉽게 인지하고 대응할 수 있는 구조를 갖추는 것이 필수적이다.</p>
<p>다음과 같은 데이터가 있다고 가정해보자.</p>
<pre><code>enum animal {
  dog = &#39;dog&#39;,
  cat = &#39;cat&#39;,
}

type dogType = {
  name: string;
  age: number;
  bark: () =&gt; void;
};

type catType = {
  name: string;
  age: number;
  leg: number;
};
const useHook = () =&gt; {
  return {
    [animal.dog]: {
      name: &#39;jenny&#39;,
      age: 8,
      bark: () =&gt; &#39;낑낑&#39;,
    },
    [animal.cat]: {
      name: &#39;nabi&#39;,
      age: 3,
      leg: 4,
    },
  };
};</code></pre><p>이 데이터를 기반으로 useHook의 반환값을 상세하게 렌더링하는 Component가 존재한다고 가정하자.</p>
<hr>
<h3 id="삼항-연산자">삼항 연산자</h3>
<pre><code>const Component = ({ target }: { target: animal }) =&gt; {
  const data = useHook();
  return (
    &lt;div&gt;
      {target === animal.dog ? (
        &lt;div&gt;
          &lt;span&gt;{data[target].name}&lt;/span&gt;
          &lt;span&gt;{data[target].age}&lt;/span&gt;
          &lt;button onClick={data[target].bark}&gt;click&lt;/button&gt;
        &lt;/div&gt;
      ) : (
        &lt;div&gt;
          &lt;span&gt;{data[target].name}&lt;/span&gt;
          &lt;span&gt;{data[target].age}&lt;/span&gt;
          &lt;span&gt;{data[target].leg}&lt;/span&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};</code></pre><p>위와 같은 방식은 코드가 간결하다는 장점이 있지만, Animal 타입이 확장될 경우 문제가 발생할 수 있다.
새로운 타입이 추가되어도 TypeScript가 경고를 발생시키지 않기 때문에, 개발자가 이를 기억하고 수동으로 수정해야 하는 문제가 발생한다.</p>
<p>이처럼 &quot;기억에 의존한다&quot; 는 것은 유지보수성을 저해하는 요소가 될 수 있다.</p>
<hr>
<h3 id="switchcase">SwitchCase</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/0d0c85d3-8008-4de2-b05d-20f3c91070c1/image.png" alt=""></p>
<p>switch-case 구문을 컴포넌트에 접목시켜 caseBy의 객체 타입이 value로 추론이 가능한 점에 대해 
훌륭하다고 생각한다.</p>
<p>하지만, 각 case에 대한 타입 추론이 안된다는게 너무 아쉽다.</p>
<p>결국, as 를 사용한 타입 단언이 필요하다는 것인데, 이것 또한 에러 유발 코드가 된다고 생각한다.</p>
<hr>
<h3 id="연산자">&amp;&amp; 연산자</h3>
<pre><code>const Component = ({ target }: { target: animal }) =&gt; {
  const data = useHook();
  return (
    &lt;div&gt;
      {target === animal.dog &amp;&amp; (
        &lt;div&gt;
          &lt;span&gt;{data[target].name}&lt;/span&gt;
          &lt;span&gt;{data[target].age}&lt;/span&gt;
          &lt;button onClick={data[target].bark}&gt;click&lt;/button&gt;
        &lt;/div&gt;
      )}
      {target === animal.cat &amp;&amp; (
        &lt;div&gt;
          &lt;span&gt;{data[target].name}&lt;/span&gt;
          &lt;span&gt;{data[target].age}&lt;/span&gt;
          &lt;span&gt;{data[target].leg}&lt;/span&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};</code></pre><p><code>target</code> 값에 따라 렌더링하고자 하는 컴포넌트가 명확하게 분리 되어있다.<br>심지어 <code>SwitchCase</code> Component에서 불가했던 타입 추론이 가능하다.
하지만, animal의 타입 확장에 대해서 우리는 알아차릴 수 없을 것이며 아무런 데이터도 렌더링하지 않을 것이다.</p>
<hr>
<h3 id="switch-case">Switch-Case</h3>
<p>내가 생각한 최고의 방법이다.</p>
<pre><code>function TypeExtend(value: never) {
  return null;
}

const Component = ({ target }: { target: animal }) =&gt; {
  const data = useHook();
  return (
    &lt;div&gt;
      {(() =&gt; {
        switch (target) {
          case animal.dog:
            return (
              &lt;div&gt;
                &lt;span&gt;{data[target].name}&lt;/span&gt;
                &lt;span&gt;{data[target].age}&lt;/span&gt;
                &lt;button onClick={data[target].bark}&gt;click&lt;/button&gt;
              &lt;/div&gt;
            );
          case animal.cat:
            return (
              &lt;div&gt;
                &lt;span&gt;{data[target].name}&lt;/span&gt;
                &lt;span&gt;{data[target].age}&lt;/span&gt;
                &lt;span&gt;{data[target].leg}&lt;/span&gt;
              &lt;/div&gt;
            );
          default:
            return TypeExtend(target);
        }
      })()}
    &lt;/div&gt;
  );
};</code></pre><p>프론트엔드 개발을 하면서, switch-case와 즉시실행함수는 많이 사용한 경험이 없었다.</p>
<p>그럼에도 내가 즉시실행함수를 사용한 이유는 <a href="https://frontend-fundamentals.com/code/examples/user-policy.html">시점 이동 줄이기</a> 에 대해 많은 공감이 되기 때문이다.</p>
<blockquote>
<p>모든 상수 변수는 컴포넌트 내부에 선언해야해! 가 아니라,상황에 맞게 선언하면 될 것 같다.</p>
</blockquote>
<p>뿐만아니라, switch-case는 case 별로 type을 털어낼 수 있는데,
결국 switch-case 구문의 default에 다다르면 해당 value의 type은 never가 된다.</p>
<p>이 때, <code>TypeExtend</code> 라는 never 타입의 value를 인자로 받는 함수를 통해,
<code>animal</code> Type이 확장 될 경우, default에는 never type의 <code>target</code> 이 아닌 확장된 type의 <code>target</code> 변수가 존재할것이다.</p>
<p>따라서,</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/331016f6-d8e0-4832-9d41-515a5b510eb0/image.png" alt=""></p>
<p>다음과 같이 dx적으로 type 확장을 쉽게 파악할 수 있어 변경에 유연하게 대처할 수 있는 코드가 된다.</p>
<hr>
<p>프론트엔드 개발자는 확장과 변경에 용이한 코드를 작성해야 한다고 생각하며,
확장과 변경에 용이한 코드가 어떤 코드일까에 대해 고찰을 작성해보았습니다.</p>
<p>더 나은 방법이나 아이디어가 있다면, 댓글로 소통을 진행해주면 좋을 것 같습니다.</p>
<p>읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ React ] 디버깅을 방해하는 Early Return, 이렇게 잡는다]]></title>
            <link>https://velog.io/@0_jin/React-%EB%94%94%EB%B2%84%EA%B9%85%EC%9D%84-%EB%B0%A9%ED%95%B4%ED%95%98%EB%8A%94-Early-Return-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%9E%A1%EB%8A%94%EB%8B%A4</link>
            <guid>https://velog.io/@0_jin/React-%EB%94%94%EB%B2%84%EA%B9%85%EC%9D%84-%EB%B0%A9%ED%95%B4%ED%95%98%EB%8A%94-Early-Return-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%9E%A1%EB%8A%94%EB%8B%A4</guid>
            <pubDate>Mon, 31 Mar 2025 08:17:54 GMT</pubDate>
            <description><![CDATA[<p>React에서 JSX는 함수 실행 결과로 렌더링되기 때문에, 종종 예상치 못한 동작이 발생할 수 있습니다. 
특히 컴포넌트 내부에 early return이 존재할 경우, JSX 상에는 존재하는 컴포넌트가 실제 화면에서는 렌더링되지 않는 상황이 발생합니다.</p>
<h2 id="🧩-문제-상황">🧩 문제 상황</h2>
<p>다음과 같은 코드가 있다고 가정합니다.</p>
<pre><code>&lt;Flex direction=&#39;col&#39; gap=&#39;gap-y-[20px]&#39;&gt;
  &lt;CreatorBrowse /&gt;
  &lt;GameCommunityBrowse /&gt;
  &lt;CreatorsCampaign /&gt;
  &lt;CampaignBrowse /&gt;
  &lt;RankingBrowse /&gt;
  &lt;BottomBanner /&gt;
&lt;/Flex&gt;</code></pre><p>해당 코드로 렌더링 된 페이지는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/21242b17-d2de-46b1-ad35-33eae7f57d43/image.png" alt=""> </p>
<p>해당 코드는 페이지를 블럭 단위로 구성한 간단한 예제입니다. </p>
<p>코드와 렌더링된 페이지를 유추해보면, <code>&lt;CampaignBrowse /&gt;</code> 는 분명 JSX로 렌더링이 명시되어 있지만, 실제 페이지에는 렌더링되지 않습니다.</p>
<p>이는 해당 컴포넌트 내부에서 조건에 따라 null을 반환하는 early return 로직 때문입니다.</p>
<blockquote>
<p>위 예제 코드는 블럭 단위로 페이지를 쌓았기에 비교적 파악이 쉽지만, 조금 더 복잡한 코드에서는 파악이 더욱 어려워집니다.</p>
</blockquote>
<h2 id="🔍-원인-분석">🔍 원인 분석</h2>
<p>CampaignBrowse 컴포넌트의 내부는 다음과 같습니다.</p>
<pre><code>const CampaignBrowse = () =&gt; {
  const data = use데이터핸들링hook();
  const a = use또다른hook1();
  const b = use또다른hook2();

  if (data.length === 0) return null;

  return &lt;div&gt;...&lt;/div&gt;;
};</code></pre><p>이 코드는 다음과 같은 문제를 일으킵니다.</p>
<ul>
<li>컴포넌트가 렌더링되지 않는 이유를 외부에서 직관적으로 파악하기 어렵다.</li>
<li>디버깅 시 JSX와 실제 DOM 간 불일치로 혼란을 유발한다.</li>
<li>조건을 만족하지 않아도 불필요한 훅이 실행되어 리소스 낭비가 발생할 수 있다.</li>
<li>컴포넌트 본연의 역할이 아닌, 렌더링 조건까지 책임지고 있어 응집도가 떨어진다.</li>
</ul>
<h2 id="🛠️-첫-번째-개선-시도-조건을-외부로-분리">🛠️ 첫 번째 개선 시도: 조건을 외부로 분리</h2>
<p>렌더링 조건을 Page 컴포넌트에서 관리하도록 개선할 수 있습니다.</p>
<pre><code>const Page = () =&gt; {
  const data = use데이터핸들링hook();
  const isCampaignBrowseRender = data.length !== 0;

  return (
    &lt;Flex direction=&#39;col&#39; gap=&#39;gap-y-[20px]&#39;&gt;
      &lt;CreatorBrowse /&gt;
      &lt;GameCommunityBrowse /&gt;
      &lt;CreatorsCampaign /&gt;
      {isCampaignBrowseRender &amp;&amp; &lt;CampaignBrowse /&gt;}
      &lt;RankingBrowse /&gt;
      &lt;BottomBanner /&gt;
    &lt;/Flex&gt;
  );
};

const CampaignBrowse = () =&gt; {
  const data = use데이터핸들링hook();
  const a = use또다른hook1();
  const b = use또다른hook2();
  return &lt;div&gt;...&lt;/div&gt;;
};</code></pre><p>이렇게 하면 CampaignBrowse가 렌더링될지 여부를 외부에서 명확하게 판단할 수 있습니다. </p>
<p>그러나 이 방식은 </p>
<ul>
<li>Page 컴포넌트가 지나치게 많은 렌더링 조건과 상태를 갖게 되어 가독성이 떨어질 수 있다.</li>
<li>로직이 복잡해질 경우 상태 관리와 데이터 흐름 파악이 어려워진다.</li>
</ul>
<p>라는 한계를 가집니다.</p>
<h2 id="💡-궁극적인-해결-방안-function-as-child-component">💡 궁극적인 해결 방안: Function as Child Component</h2>
<p>렌더링 여부에 필요한 로직을 별도의 컴포넌트로 분리하고, Function as Child 패턴을 활용합니다.</p>
<pre><code>const Page = () =&gt; {
  return (
    &lt;Flex direction=&#39;col&#39; gap=&#39;gap-y-[20px]&#39;&gt;
      &lt;CreatorBrowse /&gt;
      &lt;GameCommunityBrowse /&gt;
      &lt;CreatorsCampaign /&gt;
      &lt;CampaignBrowseRenderProvider&gt;
        {({ isRender }) =&gt; isRender &amp;&amp; &lt;CampaignBrowse /&gt;}
      &lt;/CampaignBrowseRenderProvider&gt;
      &lt;RankingBrowse /&gt;
      &lt;BottomBanner /&gt;
    &lt;/Flex&gt;
  );
};

const CampaignBrowseRenderProvider = ({
  children,
}: {
  children: ({ isRender }: { isRender: boolean }) =&gt; React.ReactNode;
}) =&gt; {
  const data = use데이터핸들링hook();
  const isRender = data.length !== 0;
  return children({ isRender });
};</code></pre><h2 id="✅-이-방식의-장점">✅ 이 방식의 장점</h2>
<ul>
<li>CampaignBrowse는 렌더링 조건을 신경 쓰지 않고 자신의 역할에 집중한다.</li>
<li>Page 컴포넌트는 필요한 영역에만 로직을 캡슐화하여 전체 가독성을 유지한다.</li>
<li>조건 분기 로직이 분리되고 재사용성 또한 확보할 수 있다.</li>
<li>리소스 낭비 없이 불필요한 hook 호출도 방지한다.</li>
</ul>
<h2 id="🧵-마치며">🧵 마치며</h2>
<p>컴포넌트 단위의 책임을 분리하고 렌더링 조건을 명확하게 관리하는 방식은 규모가 커질수록 더욱 빛을 발한다고 생각합니다.
단순한 개선처럼 보일 수 있지만, 이러한 패턴은 실제 개발 생산성과 유지보수성에 큰 영향을 미칩니다.</p>
<hr>
<p>다른 의견이나 더 좋은 방안이 있다면 댓글 작성해주시면 감사드리겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 로딩 최적화 - githru]]></title>
            <link>https://velog.io/@0_jin/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94-githru</link>
            <guid>https://velog.io/@0_jin/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94-githru</guid>
            <pubDate>Sun, 13 Nov 2022 16:18:13 GMT</pubDate>
            <description><![CDATA[<p>github commit 그래프 시각화 프로젝트인 <code>githru</code> 의 issue 작업 중 <code>이미지 로딩 최적화</code> 문제를 해결하며 기여해본 경험을 작성해보았습니다.</p>
<hr>
<h2 id="이슈">이슈</h2>
<p>사용자의 github profile Image를 렌더링 하는 과정에서 error가 발생할 경우 ( profile Image가 존재하지 않는 경우 ) <code>gravatar</code>를 사용하여 random 이미지를 렌더링 해주는데 로딩 속도가 느리다는 이슈가 발생하였다.</p>
<blockquote>
<p>로딩속도는 사용자의 경험에 큰 영향을 준다고 생각하여 해당 이슈를 지나칠 수 없어 담당하게되었다.</p>
</blockquote>
<p><a href="https://github.com/githru/githru-vscode-ext/issues/258">https://github.com/githru/githru-vscode-ext/issues/258</a></p>
<hr>
<h2 id="최적화-작업-논의">최적화 작업 논의</h2>
<p>프로필 이미지 요청 시, Gravatar는 사용자별 MD5 해시 값을 기반으로 경로를 생성하여 이미지를 제공한다. 이에 따라, 프로필 이미지가 존재하지 않는 사용자가 많을 경우, 개별적으로 생성된 다수의 요청이 네트워크 지연을 유발할 수 있다는 우려가 제기되었다. 이를 해결하기 위해, 사용자별 랜덤한 해시 값을 직접 사용하는 대신, 약 5개의 고정된 값을 설정하여 캐싱을 적용하는 방안이 제안되었다.</p>
<p>그러나, 네트워크 환경을 <strong>&quot;제한 없음&quot;, &quot;빠른 3G&quot;, &quot;느린 3G&quot;</strong>로 설정하여 성능을 측정한 결과, Gravatar의 Fallback URL 요청으로 인해 발생하는 지연 현상은 확인되지 않았다.</p>
<p>오히려, 제안된 고정 해시 값 기반의 캐싱 방식 적용 시 프로필 이미지의 렌더링 속도가 더 느려지는 현상이 관찰되었다.</p>
<hr>
<h2 id="또-다른-문제점-발견">또 다른 문제점 발견</h2>
<p>github image가 존재하는 유저와 github image가 존재하지 않아 fallback을 렌더링하는 환경이다.</p>
<p>이 때, github image가 존재하지 않아 fallback image를 렌더링하는 것보다도 
github image가 존재하는 유저의 렌더링이 더욱 느린 것을 발견했다.</p>
<p>그 이유는, github image를 요청하면 내부적으로 302 redirect를 발생시켜 요청을 한번 더 진행하게 되어 지연이 발생하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/6fc6ccc0-712e-4767-a1eb-7f0b2e8e52df/image.png" alt="">
<img src="https://velog.velcdn.com/images/0_jin/post/56558bd2-7bcb-470e-afc8-e72029d7aa88/image.png" alt=""></p>
<hr>
<h2 id="해결방안">해결방안</h2>
<h3 id="redirect-url로-요청을-보내자">redirect URL로 요청을 보내자</h3>
<p>애초부터 Redirect url로 요청을 보낸다면, redirect를 진행하지 않기에 더욱 빨라질 것이라 여겼다.
하지만, 사용자 고유 값을 알 수 없어 해당 방법을 통해 해결할 수 없었다.</p>
<h3 id="이미지-크기-리사이징">이미지 크기 리사이징</h3>
<p>그럼, 다른 방법으로 문제를 해결해봐야겠다고 생각을 했고.</p>
<p>내가 찾은 문제와 해결 방법은, 이미지 크기 리사이징이었다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/95d65954-4e78-4e84-b2ed-9868fb342fc7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/e2352bcc-3c93-48e0-8832-0dbaf58bb87b/image.png" alt=""></p>
<p>해당 이미지는 나의 깃허브 프로필 사진이다.
콘텐츠 다운로드 시간이 약 800ms가 발생하였는데,
github image는 리사이징이 가능하다고 한다.</p>
<p>나는 <code>githru</code>에서 사용하는 이미지 크기인 <code>?size=30</code>을 사용하여 이미지 크기 리사이징을 진행하였다.</p>
<p>그 결과,</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/0a1e1265-1532-4c23-8a56-14972ab598b4/image.png" alt=""></p>
<p>콘텐츠 다운로드 시간이 0.32ms 가 소모되는 것을 확인할 수 있다.</p>
<hr>
<h2 id="결과">결과</h2>
<p>네트워크 환경은 <code>빠른 3g</code> 였으며, onError를 발생한 유저는 11명 , 리다이렉트 발생하는 유저는 13명이었다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/19ac7bb7-7a19-4ee3-859d-868a47bb9a00/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/7ecdc2c2-185f-4ae2-b948-10e3c3a8d72a/image.png" alt=""></p>
<p>리사이징 이전에는 약 6000ms ~ 8300ms =&gt; 2300ms 였지만,
리사이징 이후에는 약 5800ms ~ 7600ms =&gt; 1800ms 로 약 500ms가 감소하게 되었다.</p>
<hr>
<p><a href="https://github.com/githru/githru-vscode-ext/pull/268">https://github.com/githru/githru-vscode-ext/pull/268</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Polling으로 실시간 채팅 구현하기]]></title>
            <link>https://velog.io/@0_jin/Polling%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_jin/Polling%EC%9C%BC%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 20 Oct 2022 10:27:43 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/jin-Pro/realTime/tree/main/polling">repo</a></p>
<h1 id="polling">polling</h1>
<ul>
<li>하나의 화면에서 사용자 2명의 채팅방이 보인다.</li>
<li>채팅 내용을 입력하고 send 버튼을 누르면 채팅이 전송된다.</li>
<li>POLLING_INTERVAL_TIME에 맞춰서 setInterval을 사용하여 http get 요청을 보내 서버에 저장된 message를 요청받는다.</li>
<li>전달받은 message를 dom에 그려준다.</li>
<li>error가 발생하면 POLLING_INTERVAL_TIME의 10 배 시간 뒤에 다시 요청을 진행한다.</li>
<li>폴링 요청에 서버 지연이 발생할 수 있기 때문에, Promise.race를 사용하여 1000 ms</li>
</ul>
<hr>
<h2 id="구현하면서-발생-이슈">구현하면서 발생 이슈</h2>
<ul>
<li>API Request Timeout =&gt; Promise.race로 해결</li>
</ul>
<hr>
<h2 id="이미지">이미지</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/313afe6b-bf43-45af-89a4-57e904b166f1/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/27bd8ca2-d6b4-432e-97d9-3dad0529f092/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/f1976100-95be-47aa-9489-2b0b776df02e/image.png" alt=""></p>
<hr>
<h2 id="나의-생각">나의 생각</h2>
<h3 id="성능-문제">성능 문제</h3>
<p>폴링 기법을 사용할 경우 HTTP 요청을 매번 보내야한다.
HTTP 요청을 매번 보낸다는것은 Socket Connection을 매번 연결하고 끊어야 한다. ( HTTP 3.0이 아니기 때문에 TCP )
하지만 Socket Connection ( TCP Connection ) 을 연결하고 끊는데 발생하는 병목 문제를 해결하기 위해 HTTP 1.0+ 부터는 지속 커넥션이 등장했다 한다.
HTTP 1.1 부터는 지속커넥션이 default로 동작한다고 한다.
지속커넥션을 사용하여 TCP Connection의 단점인 Slow Start ( 혼잡제어를 위해 다시 1개의 request로 시작 )를 해결했다고는 한다.
하지만 채팅을 전송하는 POST 메세지는 멱등성을 가지지 않기 때문에 지속 커넥션을 유지할 수 없다고 한다.
주기적으로 GET 요청을 보내다가 사용자가 POST 요청을 보내게 되면 지속커넥션이 끝난다고 생각한다.
즉, 다시 Connection을 열어줘야하고 Slow Start로 인한 지연이 발생할 것이라 생각한다.</p>
<blockquote>
<p>Web Socket을 사용하여 TCP Connection을 끊지않고 유지한다면, POST 메서드에 대한 지속 커넥션 유지도 동작할 것이라 생각한다.</p>
</blockquote>
<blockquote>
<p>지속커넥션을 유지한다해도,,, Header는 어떻게 할것인지..!!! ( HTTP2.0으로,,, )</p>
</blockquote>
<blockquote>
<p>뿐만아니라, 요청이 너무 많아 pending이 발생하게되면 api 요청이 지연이 되어 원활한 메세지 채팅이 불가능하다. 이러한 이슈를 해결하기 위한 코드를 작성해주어야한다. ( 본인은 Promise.race로 해결 )</p>
</blockquote>
<h3 id="서비스-문제">서비스 문제</h3>
<p>내가 사용하면서도 실시간이 실시간이 아니다.
약간의 텀이 존재하는 것 같다. 폴링 타임을 1초에서 100ms로 변경하여도 동일한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[노션처럼 Resize 최적화하기]]></title>
            <link>https://velog.io/@0_jin/%EB%85%B8%EC%85%98%EC%B2%98%EB%9F%BC-Resize-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@0_jin/%EB%85%B8%EC%85%98%EC%B2%98%EB%9F%BC-Resize-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 12 Oct 2022 17:53:22 GMT</pubDate>
            <description><![CDATA[<p>내가 지금 진행하고 있는 프로젝트에서 </p>
<p>window.innerWidth가 가변적으로 변하는 즉, resize가 발생할때
component의 수가 width에 맞춰서 <code>가변적</code>으로 변해야하는 경우에 대해 
어떻게 처리 했는가? 를 기록하기 위해 이 글을 작성했다.</p>
<hr>
<h3 id="ref-사용하기">ref 사용하기</h3>
<p>ref를 사용하여 dom 객체를 꺼내와서 <code>getBoundingClientRect</code>를 사용하여 width값을 가지고 올까 했는데, 애초에 item이 10개라면 10개 모두 렌더링되어 ref를 사용하여 가져오는 dom에는 10개의 item이 렌더링되어 width값이 길어져 길어진 값을 가져올 것이라 판단하였다.</p>
<p>즉, <code>getBoundingClientRect</code>를 사용하여 얻은 width 크기는 모든 item이 렌더링된 크기 일거라 생각하였다.</p>
<hr>
<h3 id="windowonresize-이용하기">window.onresize 이용하기</h3>
<p>이전에, 노션페이지는 width크기가 가변적으로 변경되면 멈출때 딱 한번 resize가 되어서 알아본적이 있는데, window.onresize를 이용했다고 한다.</p>
<p>따라서, 노션은 <code>window.onresize</code>를 이용하여 리플로우를 방지하였는데, 나도 이것을 한번 사용해보고 싶다고 생각만 했었는데, 드디어 사용해보게 되엇다!!</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/88af12dc-9da7-46aa-8039-cacf72037e7c/image.gif" alt=""></p>
<hr>
<h3 id="사용법">사용법</h3>
<h4 id="첫번째">첫번째,</h4>
<pre><code class="language-jsx">// getItemsOfList 는 전체 list에서 갯수를 입력해주면 그 만큼의 아이템을 가져오는 함수이다.

...

const [items,setItems] = useState(getItemsOfList(totalItems,window.innerWidth / SIZE));

window.onresize = () =&gt; {
 setItems(getItemsOfList(totalItems,window.innerWidth / SIZE))
}
...</code></pre>
<p><img src="https://velog.velcdn.com/images/0_jin/post/bc7a2a08-100e-49c3-93a0-12ddfd321da0/image.gif" alt=""></p>
<p>잘동작한다! 근데 어딘가 이상하다.
초기 렌더링이 나타나질 않는다..!</p>
<hr>
<h3 id="아차차-useeffect">아차차 useEffect..!</h3>
<pre><code class="language-jsx">useEffect(() =&gt; {
   setItems(getItemsOfList(totalItems,window.innerWidth / SIZE))
},[totalItems])</code></pre>
<p>를 추가해주니 바로 잘 동작한다 ㅎㅎ..</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/d8d12510-10aa-4ab8-9ef1-9a3ceb4ca8a1/image.gif" alt=""></p>
<p>엥? 근데 콘솔창을 보니 ??가 72번 찍혔다.. (onresize발생할때 console.log를 찍었습니다.)</p>
<hr>
<h3 id="debounce-">Debounce !</h3>
<p>그렇다! Debounce를 구현하여 Resize를 최소화할 예정이다!</p>
<p>debounce 코드는 아래와 같다.</p>
<pre><code class="language-tsx">export const debounce = (func: Function, ms: number) =&gt; {
  let timeout: ReturnType&lt;typeof setTimeout&gt;;

  return () =&gt; {
    if (timeout) clearTimeout(timeout);

    timeout = setTimeout(() =&gt; {
      func();
    }, ms);
  };
};</code></pre>
<p>다음은 debounce를 적용한 resize 코드이다!</p>
<pre><code class="language-tsx">window.onresize = debounce(() =&gt; {
 setItems(getItemsOfList(totalItems,window.innerWidth / SIZE))
},500)</code></pre>
<p><img src="https://velog.velcdn.com/images/0_jin/post/fa0406b3-6b47-4510-aadb-749745cb78d1/image.gif" alt=""></p>
<p>잘 동작한다!</p>
<hr>
<h3 id="resize는-얼마나-줄었을까">resize는 얼마나 줄었을까??</h3>
<h4 id="debounce-적용하기-전">debounce 적용하기 전</h4>
<p><img src="https://velog.velcdn.com/images/0_jin/post/69c10c19-3f5b-479a-8227-166195039929/image.png" alt=""></p>
<p>약 16ms (주사율) 마다 resize event가 발생하여 브라우저가 살려달라고 말하는것 같다. ( 사실, 이정도까지는 무리 없는 걸로 안다. )</p>
<h4 id="debounce-적용하기-후">debounce 적용하기 후</h4>
<p><img src="https://velog.velcdn.com/images/0_jin/post/21b0a273-63b5-4e38-a8d0-2c19dfc63506/image.png" alt=""></p>
<p>width값의 변경이 멈춰서야 비로소 렌더링이 진행된다!</p>
<p>정확한 기준으로 비교를 하지는 못했지만, 
위 이미지를 통해 resize를 멈춰야 reflow가 진행되는 것과 매 16ms 마다 reflow가 발생하는건 차이가 있다는 것을 알 수 있다.</p>
<hr>
<h2 id="추가">추가</h2>
<p>window.onresize는 useEffect로 감싸주세요! cleanUp도 필수입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Laregest Content Paint 단축시키기!]]></title>
            <link>https://velog.io/@0_jin/Laregest-Content-Paint-%EB%8B%A8%EC%B6%95%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
            <guid>https://velog.io/@0_jin/Laregest-Content-Paint-%EB%8B%A8%EC%B6%95%EC%8B%9C%ED%82%A4%EA%B8%B0</guid>
            <pubDate>Fri, 19 Aug 2022 05:32:13 GMT</pubDate>
            <description><![CDATA[<h2 id="원인-분석-하기">원인 분석 하기</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/166179ff-dcae-4aa4-badf-6b2bbac242ea/image.png" alt=""></p>
<p>성능 분석을 진행한 결과이다.</p>
<p>FCP부터 LCP까지</p>
<p>Logo.svg와 meetingImage.png이다.</p>
<p>Largest Content는 meetingImage.png이기 때문에 Pre-loading을 진행하면 될 것 같다.
Logo.svg 또한 마찬가지이다.</p>
<hr>
<h2 id="pre-loading-">Pre-loading ?</h2>
<pre><code class="language-jsx">// in App.tsx
...

const img = new Image();
img.src = &quot;~~&quot;

...</code></pre>
<p>위와 같이 작성하여 미리 Largest Content인 Img파일을 App에서 불러와서 시간을 단축시켜보고자 한다.
하지만, </p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/fed063fd-4397-49f7-b0d4-306a1674651f/image.png" alt=""></p>
<p>계속 동일한 img를 요청할때마다 계속 네트워크 요청을 진행한다.</p>
<p>즉, Pre-load를 할 필요가 없게된다.</p>
<hr>
<p>캐시 사용을 중지하고 보면 따단</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/ca83a547-fa0b-459e-aca0-a9a92a5de829/image.png" alt=""></p>
<p>img 요청을 한번만 한다!</p>
<p>네트워크 요청을 보면</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/3f8ba451-924d-4e53-b03e-70547c9089e5/image.png" alt=""></p>
<p>초기에 image를 전부 요청하는걸 알 수 있다.</p>
<hr>
<h2 id="preload-전후-비교">Preload 전후 비교</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/edecbe41-d946-4f96-a027-6d88daaae5f5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/1e3f0849-e552-4ff6-9308-b54a2910fa33/image.png" alt=""></p>
<p>FCP =&gt; 110.7ms =&gt; 110.3ms ( LCP 비교를 위한 기준 값 )
LCP =&gt; 201.3ms =&gt; 147.9ms ( 53.4 ms  약 27% 감소 )</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web Vitals  10% 단축시키기]]></title>
            <link>https://velog.io/@0_jin/WebVitals-10-%EB%8B%A8%EC%B6%95%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
            <guid>https://velog.io/@0_jin/WebVitals-10-%EB%8B%A8%EC%B6%95%EC%8B%9C%ED%82%A4%EA%B8%B0</guid>
            <pubDate>Thu, 18 Aug 2022 03:16:33 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Code splitting 전<ul>
<li>번들 파일 다운로드 소모 시간</li>
<li>번들 파일 크기 </li>
<li>번들 파일 동작 소모 시간</li>
<li>web vitals Time stamp</li>
<li>Build 파일 크기 측정</li>
<li>Build 시간 측정</li>
<li>Analyzer 분석</li>
</ul>
</li>
<li>Code splitting 후<ul>
<li>번들 파일 다운로드 소모 시간</li>
<li>번들 파일 크기 </li>
<li>번들 파일 동작 소모 시간</li>
<li>web vitals Time stamp</li>
<li>Build 파일 크기 측정</li>
<li>Build 시간 측정</li>
<li>Analyzer 분석</li>
</ul>
</li>
<li>결과</li>
</ul>
<hr>
<p>csr의 단점이라하면, 초기 페이지에 접근할 때, 웹 페이지에 필요한 모든 파일을 받아온다는 것이다.
사용자가 접근하지 않는 페이지의 데이터를 위해 모든 파일을 받아온다는 것이다.</p>
<p>그로인해, 초기 페이지 접근하는데 소요되는 시간이 비교적 ssr 보다 많이 소모된다.
이러한 csr의 단점을 보완하기 위해 code splitting을 사용하는데,
데이터를 필요로할 때 리소스를 요청하는 방법으로
초기 페이지 접근하는데 요청하는 js파일의 크기를 줄여 초기 렌더링 시간을 단축할 수 있다.</p>
<h1 id="code-splitting-전">Code splitting 전</h1>
<h2 id="번들-파일-다운로드-소모-시간">번들 파일 다운로드 소모 시간</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/b140653b-486f-4921-9c2a-581a0c4bb510/image.png" alt=""></p>
<p>위 이미지에서 알 수 있듯, 초기 렌더링을 위한 js 콘텐츠를 요청하고 다운로드까지 45ms 가 소모되었다.</p>
<hr>
<h2 id="번들-파일-크기">번들 파일 크기</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/1ea1cb54-d55e-4038-a3c5-2cb50166e69e/image.png" alt=""></p>
<p>콘텐츠 크기는 약 422kb 로, 70ms 소모 된 것을 알 수 있다.</p>
<hr>
<h2 id="번들-파일-동작-소모-시간">번들 파일 동작 소모 시간</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/c0f0ddd1-ef6d-4e06-94db-e94f7d3b7f62/image.png" alt=""></p>
<p>html을 파싱 하고나서, js 파일을 파싱하여 동작하는데 15ms ~ 85ms로 70ms 동안 동작함을 알 수 있다.</p>
<hr>
<h2 id="web-vitals-time-stamp">web vitals Time stamp</h2>
<h3 id="domcontentloaded-이벤트">DOMContentLoaded 이벤트</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/747086b3-e383-4fad-8df0-e18c5f9854b6/image.png" alt=""></p>
<p>119.5ms에 DOMContentLoaded 이벤트가 동작하였음을 알 수 있다.</p>
<h3 id="first-paint">First Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/228c5e39-c910-454a-8759-521a5ff8b9a8/image.png" alt=""></p>
<p>웹 페이지에 첫 Paint작업은 
사용자가 웹페이지 접속 후 126.9ms가 되어야 동작을 하였다.</p>
<h3 id="first-content-paint">First Content Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/c3f9973d-6b02-44bb-ae7e-27e66ead4bf1/image.png" alt=""></p>
<p>사용자에게 유의미한 Content가 paint된 시점 또한, 127.0ms 였다.</p>
<h3 id="largest-content-paint">Largest Content Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/f1bacbc3-ca6b-484c-95ac-1dc59f30e540/image.png" alt=""></p>
<p>가장 큰 Content 를 paint 해주는 시점은 214.8ms 임을 확인할 수 있다.</p>
<hr>
<h2 id="gzip-build-파일-크기-측정">gzip Build 파일 크기 측정</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/b801308b-d1a3-4564-99c4-073da548bbe7/image.png" alt=""></p>
<p>gzip으로 Build 를 하였을 때, File Size는 125.95kB임을 알 수 있다.</p>
<h2 id="build-시간-측정">Build 시간 측정</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/330bf067-47ac-44de-8935-c7a706b3ff1c/image.png" alt=""></p>
<p>package install을 제외하고, 단순 build할때 소모된 시간이다. 15.91 s 가 소모되었는데,
analyzer 분석을 위한 source-map-loader 때문에 많은 시간이 잡히게 되었다.
이 부분은 내가 production build를 진행하지 않았기 때문이지 않나 싶다.</p>
<h2 id="analyzer-분석">Analyzer 분석</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/28910e3d-8db0-4565-be35-0bf2cdab7b6a/image.png" alt=""></p>
<p>다른 블로그를 통해 본 Analyzer 시각 자료는 지금 보는 이미지와 매우 다를 것이다.
나는 CRA를 사용하여 webpack을 사용하였는데,
cra-analyzer library가 path alias 설정을 인식을 못해서, eject보다 쉽게 하는 방법을 찾다가 
source-map-loader를 사용하여 위와 같이 해결을 하였다.</p>
<hr>
<blockquote>
<p>지금까지는 코드스플리팅 적용하지 않고, 아무런 작업을 진행하지 않았을 때의 결과물이였다.
다음은, 코드 스플리팅을 적용하여 측정결과를 확인하고, 성능 비교를 해보겠다.</p>
</blockquote>
<hr>
<h1 id="code-splitting-후">Code splitting 후</h1>
<p>사용 방법은 <a href="https://ko.reactjs.org/docs/code-splitting.html">react 공식문서</a>에 잘 나와있다.</p>
<p>나는 Page 단위로 Code splitting을 적용하였는데,</p>
<hr>
<h2 id="번들-파일-다운로드-소모-시간-1">번들 파일 다운로드 소모 시간</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/d34bd913-4dd6-4b10-8219-d96684470ab8/image.png" alt=""></p>
<p>순수 콘텐츠 다운로드 시간은 30ms 이며,
총 콘텐츠 요청부터 소모 시간은 48ms 이다.</p>
<h2 id="번들-파일-크기-1">번들 파일 크기</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/d5577dec-3565-499f-97b1-13eba04d78aa/image.png" alt=""></p>
<p>초기에 받아오는 번들 파일의 크기는 408 KB이다.</p>
<h2 id="번들-파일-동작-소모-시간-1">번들 파일 동작 소모 시간</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/e4e2db5b-7747-41bd-9127-b826fc71e292/image.png" alt=""></p>
<p>번들 파일을 동작하는데 소모된 시간은 20ms ~ 73ms 가 소모되었으며 총 53ms를 동작하였다.</p>
<h2 id="web-vitals-time-stamp-1">web vitals Time stamp</h2>
<h3 id="domcontentloaded-이벤트-1">DOMContentLoaded 이벤트</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/2162aa14-46c4-46ea-8a0f-8774d939f2ce/image.png" alt=""></p>
<p>처음 DoOMContentLoaded 이벤트는 104ms에 발생하였고,</p>
<h3 id="first-paint-1">First Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/8d28bf0b-577d-4256-becc-285eb39d32a0/image.png" alt=""></p>
<p>첫 Paint작업은 111.2ms 에 동작하였다.</p>
<h3 id="first-content-paint-1">First Content Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/17934c42-6204-4eac-a5ba-fffce7b99672/image.png" alt=""></p>
<p>첫 Content 를 Paint 하는 작업 또한 111.2ms 에 동작을 하였고,</p>
<h3 id="largest-content-paint-1">Largest Content Paint</h3>
<p><img src="https://velog.velcdn.com/images/0_jin/post/9454fd60-ca32-4fff-a38f-1cc992efd33a/image.png" alt=""></p>
<p>가장 큰 컨텐츠를 Paint하는 시점은 175.5ms 였다.</p>
<hr>
<h2 id="gzip-build-파일-크기-측정-1">gzip Build 파일 크기 측정</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/e20a5bfa-76d7-4186-80c3-4881252d351d/image.png" alt=""></p>
<p>build를 실행하였을때 하나의 파일이 여러개의 chunk로 분리 되었음을 확인할 수 있다.</p>
<hr>
<h2 id="build-시간-측정-1">Build 시간 측정</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/5398f934-e5f0-41ca-be7e-3fe10d1ccb48/image.png" alt=""></p>
<p>build 시간이 단축된건 이해가 가질 않는다. 
다음 build 최적화를 진행할때 다시 분석해보겠다!</p>
<hr>
<h2 id="analyzer-분석-1">Analyzer 분석</h2>
<p><img src="https://velog.velcdn.com/images/0_jin/post/08c0d510-937b-435e-aff7-24a73b89fa2c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/1a748b8f-6e45-498b-a853-fab0de5d37e2/image.png" alt=""></p>
<p>Analyzer 분석 결과로
하나의 Bundle File이 여러개의 File로 분리됨을 알 수 있다.</p>
<p>총 11개의 page 관려 File과 라이브러리 관련 File로 분리됨을 알 수 있다.</p>
<hr>
<h1 id="결과">결과</h1>
<ul>
<li>번들 파일 다운로드 소모 시간 : 32 ms =&gt; 30 ms ( 6 % 단축 )</li>
<li>번들 파일 크기 : 422 kB =&gt; 408 kB ( 14 kB 감소 )</li>
<li>번들 파일 동작 소모 시간 : 15ms ~ 85ms ( 70 ms ) =&gt; 20ms ~ 73ms ( 53 ms ) : 초기 번들 파일 동작 시간 24% 감소</li>
<li>web vitals Time stamp<ul>
<li>DOMContentLoaded : 119.5 ms =&gt; 104 ms ( 12.9 % 단축 )</li>
<li>First Paint : 126.9 ms =&gt; 111.2 ms ( 12.3 % 단축 )</li>
<li>First Content Paint : 127.0 ms =&gt; 111.2 ms ( 12.4 % 단축 )</li>
<li>Largest Content Paint : 214.8 ms =&gt; 175.5ms ( 18.3 % 단축 )</li>
</ul>
</li>
<li>gzip Build 파일 크기 측정 : 125.95kB =&gt; 121.61kB ( 3.4 % 단축 )</li>
</ul>
<hr>
<p>페이지단위로 Code-splitting을 진행하여,</p>
<p>내가 진행한 프로젝트에서 사용한 라이브러리가 거의 없었기 때문에 ( Recoil , Emotion , Socket )
라이브러리를 초기에 모두 받아오게 되어 드라마틱한 성능적 차이를 발생하진 못한것 같다.</p>
<p>하지만, webVital 의 성능을 10% 단축 시킨 부분에서 만족한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[무한스크롤 성능 개선]]></title>
            <link>https://velog.io/@0_jin/%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@0_jin/%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Tue, 16 Aug 2022 09:43:43 GMT</pubDate>
            <description><![CDATA[<p>이전에 내가 진행했던 프로젝트에서 무한스크롤을 구현한적이있었다.
그 부분을 개선해보고자 글을 작성한다.</p>
<p>코드 수정 순서는 
기본 scroll Event 
=&gt; Throttle
=&gt; IntersectionObserver</p>
<p>순서로 비교를 해볼 예정이다.</p>
<h1 id="scroll-event">Scroll Event</h1>
<pre><code class="language-jsx">const addDatas = useCallback(async () =&gt; {
    try {
      const filterCategory = makeCategory(category);
      const item = await getCowDogInfo(person, dataIndex, filterCategory);
      const temp = item.length === 0 ? makeDummyProfileData() : item;
      setDatas([...datas, ...temp]);
      setDataIndex((prev) =&gt; prev + 1);
    } catch (e) {
      setError((e as any).message);
    }
  }, [person, dataIndex, category]);

const onScroll = useCallback(() =&gt; {
  const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
  if (scrollTop + clientHeight &gt;= scrollHeight) addDatas();
}, [addDatas]);

useEffect(() =&gt; {
  document.addEventListener(&quot;scroll&quot;, onScroll);
  return () =&gt; {
    document.removeEventListener(&quot;scroll&quot;, onScroll);
  };
}, [dataIndex, category]);
</code></pre>
<p>위와 같은 코드로 진행을 하였다.</p>
<p>성능을 측정해보니</p>
<p>scroll event에 대한 콜백함수가 동작한다는 것을 확인할 수 있다.
그렇다, 위 함수가 addEventListener로 등록해준 onScroll 함수이다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/a48bd270-7e61-4ec0-9b7b-52aae8a6906d/image.png" alt=""></p>
<p>다음 사진은 1번의 api로 100개의 데이터를 갖고오는 api를 10번 요청했을 경우 성능 지표이다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/4a80dc73-ee90-4a1a-9f23-d2fd8e940090/image.png" alt=""></p>
<p>스크립트 2810ms
렌더링 144ms
페인팅 271ms</p>
<h1 id="throttle-적용">Throttle 적용</h1>
<p>다음은 Throttle을 구현하여 적용해보았다.</p>
<pre><code class="language-tsx">export const useThrottle = (callback: () =&gt; void, time: number) =&gt; {
  let timer: NodeJS.Timeout | null;

  return () =&gt; {
    if (!timer) {
      timer = setTimeout(() =&gt; {
        callback();
        timer = null;
      }, time);
    }
  };
};

const addDatas = useCallback(async () =&gt; {
  try {
    const filterCategory = makeCategory(category);
    const item = await getCowDogInfo(person, dataIndex, filterCategory);
    const temp = item.length === 0 ? makeDummyProfileData() : item;
    setDatas([...datas, ...temp]);
    setDataIndex((prev) =&gt; prev + 1);
  } catch (e) {
    setError((e as any).message);
  }
}, [person, dataIndex, category]);

const onScroll = useCallback(() =&gt; {
  const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
  if (scrollTop + clientHeight &gt;= scrollHeight) addDatas();
}, [addDatas]);

const throttleScrollEvent = useCallback(useThrottle(onScroll, 300), [onScroll]);

useEffect(() =&gt; {
  document.addEventListener(&quot;scroll&quot;, throttleScrollEvent);
  return () =&gt; {
    document.removeEventListener(&quot;scroll&quot;, throttleScrollEvent);
  };
}, [throttleScrollEvent]);</code></pre>
<p>throttle을 구현한것을 제외하고 코드가 달라진 것은 없다.</p>
<p>약 300ms 의 setTimeout을 적용해주었기에 scroll event 발동부터 300ms 이후 api 요청을 진행하게되었다.</p>
<hr>
<p>하지만, 성능적인 부분에 이슈가 발생하였다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/86a4d72f-ab17-4c33-8bba-76005e7d5acb/image.png" alt=""></p>
<p>스크립트 2874ms
렌더링 199ms
페인팅 251ms</p>
<blockquote>
<p>오히려 기본 scrollEvent보다 성능이 더 좋지 못한 이슈가 발생하였다.</p>
</blockquote>
<p>throttle을 사용하는 이유는 수 많은 scroll Event가 발생할때 
지정한 시간동안 발생한 여러 요청을 단 한번의 요청으로 끝내기 위함이다.
그러기 위하여 setTimeout을 사용해주고있다.</p>
<p>하지만, 내가 구현한 scroll Event에는 조건을 만족하지 않으면 바로 return을 작성한다.
때문에, setTimeout을 사용하여 발생한 비용보다 height값 계산하는게 더욱 값이 싸기때문에
오히려 throttle을 사용하는 것이 bad case가 되었다.</p>
<h1 id="intersectionobserver-적용">IntersectionObserver 적용</h1>
<p>그렇다면, scroll Event를 사용하는 것이 아니라, IntersectionObserver를 사용해보고 비교를 해보겠다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/bf620e3f-814c-4085-8614-0067f8c38ae1/image.png" alt=""></p>
<p>위 이미지에서 보듯 scroll Event에 대한 콜백함수가 동작하지 않음을 알 수 있다.
분명 무수히 많은 scroll Event에 대해서 callback 함수가 발생했었는데,
그것이 작동안하니 훨씬 성능이 좋을 것이라 기대한다.</p>
<p><img src="https://velog.velcdn.com/images/0_jin/post/90f8c5a0-8538-412f-87f9-9e3e3a3e3034/image.png" alt=""></p>
<p>스크립트 2735ms
렌더링 193ms
페인팅 278ms</p>
<p>성능을 측정해보니
큰 차이를 가늠할 수 있는 정도는 아닌 것 같다.</p>
<hr>
<h1 id="결론">결론</h1>
<p>[ 측정 환경 ]</p>
<p>API 1회당 100개 데이터 
총 10번 API 요청
한 화면에 8개의 데이터가 view에 나타남</p>
<hr>
<p>[ scroll Event ]</p>
<p>스크립트 2810ms
렌더링 144ms
페인팅 271ms</p>
<hr>
<p>[ scroll Event Throttle ]</p>
<p>스크립트 2874ms
렌더링 199ms
페인팅 251ms</p>
<hr>
<p>[ intersectionObserver ]</p>
<p>스크립트 2735ms
렌더링 193ms
페인팅 278ms</p>
<hr>
<p>측정 동작과 측정 범위를 내가 직접 하다보니 오차가 발생할 수 있다고는 생각했다.
하지만, 성능의 차이가 이렇게 없을줄 몰랐는데,</p>
<p>내가 생각한 결론은 아래와 같다.</p>
<p>scroll Event로 인해 발생한 콜백함수의 동작이 무겁지 않아서 ( 높이 계산 후 종료 )
오히려 Throttle의 setTimeout이 더 무거워서 성능이 좋지 않았던 것 같다.
그러나, intersectionObserver의 경우 scroll event에 대한 콜백함수가 동작하지않아 스크립트 시간이 더욱 줄었다.
그러나, observer를 위한 div 태그를 그려주기때문에 페인팅 시간이 증가하였다.</p>
<p>무조건 이게 좋고, 이게 나쁘다가 아닌 것 같다.
scroll Event로 인해 동작하는 Callback 함수에 따라 달라진다.
나의 경우 동작하는 callback 함수가 매우 가벼웟을 뿐이라 생각한다.</p>
<p><a href="https://github.com/twobin/react-lazyload/blob/master/src/index.jsx#L158">무거운 callback</a> 위 Url을 들어가면 scroll Event가 발생할때마다 reflow가 동작하는 코드를 확인할 수 있다.</p>
<hr>
<h1 id="참고">참고</h1>
<p><a href="https://www.slideshare.net/deview/d2-1">참고</a>
<a href="https://ha-young.github.io/2021/frontend/infinite-scroll/">하옹의 프론트앤드 이야기</a>
<a href="https://jbee.io/web/optimize-scroll-event/">JBEE 스크롤 이벤트 최적화</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Animation이 렌더링에 미치는 영향]]></title>
            <link>https://velog.io/@0_jin/Composite-Reflow</link>
            <guid>https://velog.io/@0_jin/Composite-Reflow</guid>
            <pubDate>Thu, 11 Aug 2022 07:04:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/0_jin/post/1a5689ba-9780-4d85-a344-d1a60c3b4af3/image.png" alt=""></p>
<h1 id="github">Github</h1>
<ul>
<li><a href="https://github.com/jin-Pro/Compare_Animation">repo</a></li>
</ul>
<h1 id="transform-비교-코드">transform 비교 코드</h1>
<ul>
<li><a href="https://codesandbox.io/s/vigilant-grass-rtnqce?file=/index.html">code sandbox</a></li>
</ul>
<h1 id="position-비교-코드">position 비교 코드</h1>
<ul>
<li><a href="https://codesandbox.io/s/recursing-northcutt-deidds?file=/index.html">code sandbox</a></li>
</ul>
<hr>
<h1 id="browser-렌더링">Browser 렌더링</h1>
<p>브라우저의 렌더링 과정은</p>
<p>HTML Parsing &amp; CSS Parsing =&gt; Dom Tree &amp; CSSOM Tree 생성
Attachment =&gt; Render Tree 생성 
Reflow =&gt; Layout 에 따른 요소의 크기와 위치 계산
Repaint =&gt; Reflow에서 계산해주는 값을 기반하여 Paint
Composite =&gt; Layer 계층을 병합하여 화면에 나타냄
<br/>
으로 이루어진다.</p>
<hr>
<h1 id="reflow의-특징">reflow의 특징</h1>
<ul>
<li><p>reflow는 브라우저 창 크기를 조절하여도 발생하게 된다.</p>
</li>
<li><p>또한, 하나의 요소가 reflow가 발생하면 상.하위 Layer내부에 존재하는 요소들도 reflow가 발생한다. </p>
<p>(즉, 대부분의 reflow는 페이지 전체의 렌더링을 발생한다고 한다. )</p>
</li>
<li><p>스레드 큐에 자바스크립트 실행이 끝나야 reflow를 진행한다.</p>
</li>
</ul>
<hr>
<p>아래 영상을 보자,</p>
<p><a href="https://velog.velcdn.com/images/0_jin/post/862611b1-e2cf-478d-98c1-6d5a3fec6717/image.mov">https://velog.velcdn.com/images/0_jin/post/862611b1-e2cf-478d-98c1-6d5a3fec6717/image.mov</a></p>
<p>두 원은 겉보기에는 별반 차이가 없다.
하지만, 브라우저 렌더링 과정을 확인해보면 차이가 분명하다!</p>
<p>개발자 도구의 성능을 사용하여 비교해보았다.</p>
<hr>
<h1 id="performance-비교">Performance 비교</h1>
<p>우선 Reflow가 발생하는 작업부터 알아보면,</p>
<img width="1440" alt="스크린샷 2022-08-11 오후 2 10 42" src="https://user-images.githubusercontent.com/70205497/184068418-e687098d-710d-4c26-9cda-7d913b31761e.png">

<p>작업이 빽뺵하게 진행 중인 것을 알 수 있고, <br/>
이 작업들을 자세히 들여다 보면,</p>
<img width="1440" alt="스크린샷 2022-08-11 오후 2 10 57" src="https://user-images.githubusercontent.com/70205497/184068469-5e8d2586-9ed4-4452-8294-9f34ac013b82.png">

<p>위와 같이 스타일 계산과 레이아웃, 페인트 과정이 진행되는 것을 알 수 있다. <br/>
<br/>
이렇게 되면 문제가 발생하게 되는데, <br/>
레이아웃은 위에 설명한 것처럼, 스레드 큐에 존재하는 자바스크립트 실행이 끝나야 실행이 된다. <br/>
혹여나 reflow가 진행이 되어야 하는데, 자바스크립트 로직이 진행중이라면 사용자들은 애니메이션이 느리다고 판단할 것이다. <br/></p>
<br />

<p>아래 영상을 보아 확인해보자 <br/></p>
<p><a href="https://user-images.githubusercontent.com/70205497/184068769-d3cff74d-a381-4271-8065-d2bf73f3ca3f.mov">https://user-images.githubusercontent.com/70205497/184068769-d3cff74d-a381-4271-8065-d2bf73f3ca3f.mov</a></p>
<p>alert가 발동하고 나니, alert창으로 인해 스크립트가 동작하지 못하여 reflow 또한 진행이 되질 않는다. <br/>
따라서, 애니메이션은 동작을 중단하게 된다. <br/></p>
<hr>
<p>그럼, Reflow가 진행되지 않는 animation을 보면,</p>
<img width="1439" alt="스크린샷 2022-08-11 오후 2 19 52" src="https://user-images.githubusercontent.com/70205497/184069053-40b6a088-caed-49f0-bdb7-1e00d6767dc7.png">

<p>스타일 계산, 레이아웃 , 페인트 아무런 동작을 진행하지 않는다.</p>
<img width="1440" alt="스크린샷 2022-08-11 오후 2 12 01" src="https://user-images.githubusercontent.com/70205497/184069131-77ad24f8-ee50-40f4-acb8-cafa5c13b5f4.png">
<img width="1440" alt="스크린샷 2022-08-11 오후 2 20 28" src="https://user-images.githubusercontent.com/70205497/184069136-50ff16b3-ae38-4a2d-b6d5-9ac97d84fe6a.png">

<p>위 두 이미지를 통해, 렌더링 시간과 페인팅 시간 소요에 대한 차이를 명확하게 확인할 수 있다. <br/>
<br/>
그렇다면, 과연 alert창이 나타날때 애니메이션은 동작할까? <br/></p>
<p><a href="https://user-images.githubusercontent.com/70205497/184069257-798c03b0-30ad-4c04-8824-b25dfc6e278c.mov">https://user-images.githubusercontent.com/70205497/184069257-798c03b0-30ad-4c04-8824-b25dfc6e278c.mov</a></p>
<p>당연히, reflow가 발생하지 않으므로 애니메이션은 계속 동작하게 된다.</p>
<hr>
<h1 id="추가">추가</h1>
<h2 id="position--fixed">Position : fixed</h2>
<pre><code class="language-jsx">&lt;div id=&quot;container&quot;&gt;
  &lt;div id=&quot;circle&quot;&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>일반 상황의 경우에서는 circle이 reflow발동이 되면 container 또한 동시에 같이 reflow가 발생한다. <br/>
<img width="1440" alt="스크린샷 2022-08-11 오후 2 50 30" src="https://user-images.githubusercontent.com/70205497/184072409-5569adef-9f30-42bd-b5ed-45a9d64bbe8f.png"></p>
<p>하지만 circle에 fixed를 넣어준다면, 아래와 같이 Layer가 분리됨을 확인할 수 있으며 <br/></p>
<img width="1439" alt="스크린샷 2022-08-11 오후 2 54 51" src="https://user-images.githubusercontent.com/70205497/184072459-4f0de889-7832-4ebd-90aa-fffbf3b0c989.png">

<p>따라서, circle의 reflow로 인한 container요소의 reflow를 막을 수 있다. <br/></p>
<p>아래 이미지로 성능을 확인할 수 있다. <br/></p>
<img width="1439" alt="스크린샷 2022-08-11 오후 2 52 23" src="https://user-images.githubusercontent.com/70205497/184072520-96ad0620-2c83-46c2-89ea-06c7c258a187.png">

<img width="1440" alt="fixed" src="https://user-images.githubusercontent.com/70205497/184072526-40f63457-c8de-4257-8d46-3b56073c0d95.png">

<hr>
<h1 id="결론">결론</h1>
<p>동일해 보이는 Animation일지라도, 내부를 비교해보면 reflow의 여부에 따른 엄청난 성능차이가 발생하고, 그 차이로 인해 향후 동작에 영향으 끼치게 된다. <br/>
결국, reflow를 발생하지 않도록 해야 한다. <br/>
<br/>
reflow를 발생하지 않게 하기 위해서는, width와 height같은 layout 계산이 필요한 값을 변경하지 않게 해야한다. <br/>
만약, reflow가 발생하게된다면, 자식 요소와 부모 요소를 동일한 layer에 두지 않는 것이 중요하므로, Position의 fixed를 사용하여 layer 계층을 분리하여 부모 또는 자식 요소에 대하여 reflow를 줄여야 한다. <br/></p>
<p>추가로, 다른 글을 보면 reflow가 발동하면 자식 요소와 부모 요소가 reflow가 발동한다고 한다.
하지만, 나는 동일한 Layer에 존재하는 요소가 reflow 진행된다고 말하는게 더 명확한 표현이라고 생각한다.</p>
<p>GPU를 통해 각각의 Layer끼리 reflow와 repaint가 진행되고,
Layer를 Composite하여 합쳐주기 때문이다.</p>
]]></description>
        </item>
    </channel>
</rss>