<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leave_a_comment.log</title>
        <link>https://velog.io/</link>
        <description>나도 성장하고파</description>
        <lastBuildDate>Wed, 27 May 2026 05:44:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leave_a_comment.log</title>
            <url>https://velog.velcdn.com/images/leave_a_comment/profile/09bebd7c-7f0a-469f-8978-ba1804ed35a0/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leave_a_comment.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/leave_a_comment" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JavaScript 엔진 최적화 — JIT, Hidden Class, Inline Cache]]></title>
            <link>https://velog.io/@leave_a_comment/JavaScript-%EC%97%94%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94-JIT-Hidden-Class-Inline-Cache</link>
            <guid>https://velog.io/@leave_a_comment/JavaScript-%EC%97%94%EC%A7%84-%EC%B5%9C%EC%A0%81%ED%99%94-JIT-Hidden-Class-Inline-Cache</guid>
            <pubDate>Wed, 27 May 2026 05:44:38 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 성능 최적화를 공부하다 보면 이런 이야기를 자주 듣게 된다.</p>
<ul>
<li>객체 구조(shape)를 일정하게 유지하라</li>
<li>동적 프로퍼티 추가를 조심하라</li>
<li>객체를 렌더마다 새로 만들지 마라</li>
</ul>
<p>처음엔 그냥 &quot;최적화 팁&quot;처럼 보이지만, 사실 이건 전부 JavaScript 엔진(V8)의 내부 최적화 방식과 연결되어 있다.</p>
<p>이번 글에서는</p>
<ul>
<li>JIT 컴파일</li>
<li>Hidden Class</li>
<li>Inline Cache</li>
</ul>
<p>가 실제로 어떻게 동작하는지, 그리고 왜 React 성능과 연결되는지 정리해보려 한다.</p>
<p>(V8 기준으로 설명)</p>
<hr>
<h2 id="먼저-큰-흐름">먼저 큰 흐름</h2>
<p>JavaScript는 기본적으로 동적 언어다.</p>
<p>즉 이런 코드가 있을 때</p>
<pre><code class="language-js">obj.a</code></pre>
<p>엔진은 런타임마다</p>
<ul>
<li><code>a</code> 프로퍼티가 진짜 있는지</li>
<li>객체 구조가 뭔지</li>
<li>타입이 뭔지</li>
</ul>
<p>확인해야 할 수도 있다.</p>
<p>원래라면 굉장히 느릴 수 있는 구조다.</p>
<p>그래서 현대 JS 엔진은 내부적으로 엄청난 최적화를 수행한다.</p>
<hr>
<h2 id="전체-동작-흐름">전체 동작 흐름</h2>
<pre><code class="language-txt">JavaScript
  ↓
Parser
  ↓
AST 생성
  ↓
Interpreter (Bytecode 실행)
  ↓
Profiler (실행 패턴 수집)
  ↓
JIT Compiler
  ↓
Optimized Machine Code</code></pre>
<p>핵심은:</p>
<blockquote>
<p>처음엔 빠르게 실행하고,
자주 쓰이는 코드를 나중에 최적화한다</p>
</blockquote>
<p>는 점이다.</p>
<hr>
<h1 id="1-jit-컴파일-just-in-time">1. JIT 컴파일 (Just-In-Time)</h1>
<h2 id="왜-필요할까">왜 필요할까?</h2>
<p>예전 JavaScript 엔진은</p>
<pre><code class="language-txt">코드 한 줄 해석 → 실행</code></pre>
<p>방식이었다.</p>
<p>즉 인터프리터 기반.</p>
<p>하지만 이 방식은 반복 실행되는 코드에서 비효율적이었다.</p>
<hr>
<h2 id="현대-엔진-방식">현대 엔진 방식</h2>
<p>요즘 엔진은</p>
<ol>
<li>처음엔 빠르게 인터프리트</li>
<li>실행 패턴 수집</li>
<li>자주 실행되는 코드 발견</li>
<li>최적화된 머신코드 생성</li>
</ol>
<p>흐름으로 동작한다.</p>
<p>이걸 JIT(Just-In-Time) 컴파일이라고 한다.</p>
<hr>
<h2 id="예시">예시</h2>
<pre><code class="language-js">function add(a, b) {
  return a + b;
}</code></pre>
<p>처음엔 일반적인 방식으로 실행된다.</p>
<p>하지만 계속</p>
<pre><code class="language-js">add(1, 2);
add(3, 4);
add(5, 6);</code></pre>
<p>처럼 숫자만 들어온다면 엔진은 판단한다.</p>
<blockquote>
<p>&quot;이 함수는 숫자 계산 전용이네?&quot;</p>
</blockquote>
<p>그 순간 JIT가 최적화를 시작한다.</p>
<hr>
<h2 id="최적화되는-부분">최적화되는 부분</h2>
<p>원래 JS는 매 호출마다</p>
<ul>
<li>타입 검사</li>
<li>연산 방식 결정</li>
</ul>
<p>같은 작업을 해야 한다.</p>
<p>하지만 숫자만 들어온다고 확신하면</p>
<pre><code class="language-txt">타입 검사 생략</code></pre>
<p>가능해진다.</p>
<p>즉 거의 네이티브 코드 수준으로 빨라진다.</p>
<hr>
<h2 id="그런데-문제-발생">그런데 문제 발생</h2>
<p>갑자기</p>
<pre><code class="language-js">add(&quot;a&quot;, &quot;b&quot;);</code></pre>
<p>가 들어오면?</p>
<p>기존 최적화 가정이 깨진다.</p>
<p>엔진은</p>
<pre><code class="language-txt">Deoptimization (deopt)</code></pre>
<p>을 수행한다.</p>
<p>즉</p>
<ul>
<li>최적화 취소</li>
<li>일반 인터프리트 모드 복귀</li>
</ul>
<p>가 발생한다.</p>
<hr>
<h2 id="핵심">핵심</h2>
<p>JavaScript 엔진은</p>
<blockquote>
<p>&quot;예측 가능한 코드&quot;</p>
</blockquote>
<p>를 굉장히 좋아한다.</p>
<p>즉</p>
<ul>
<li>타입 안정성</li>
<li>일관된 호출 패턴</li>
</ul>
<p>이 중요하다.</p>
<hr>
<h1 id="2-hidden-class">2. Hidden Class</h1>
<p>이건 V8 최적화의 핵심 중 하나다.</p>
<hr>
<h2 id="javascript-객체의-문제">JavaScript 객체의 문제</h2>
<p>JS 객체는 동적으로 구조가 바뀔 수 있다.</p>
<pre><code class="language-js">const user = {};

user.name = &quot;me&quot;;</code></pre>
<p>런타임에 프로퍼티가 계속 추가된다.</p>
<p>문제는 CPU는 원래</p>
<pre><code class="language-c">struct User {
  char* name;
}</code></pre>
<p>같은 고정 구조를 좋아한다는 점이다.</p>
<hr>
<h2 id="그래서-등장한-hidden-class">그래서 등장한 Hidden Class</h2>
<p>V8은 객체를 내부적으로</p>
<blockquote>
<p>&quot;고정 구조처럼&quot;</p>
</blockquote>
<p>관리한다.</p>
<p>예를 들어</p>
<pre><code class="language-js">const user = {
  name: &quot;me&quot;,
  age: 20,
};</code></pre>
<p>를 만들면 내부적으로</p>
<pre><code class="language-txt">HiddenClass1
- name → offset 0
- age → offset 1</code></pre>
<p>같은 구조를 생성한다.</p>
<hr>
<h2 id="왜-빠를까">왜 빠를까?</h2>
<pre><code class="language-js">user.name</code></pre>
<p>접근 시</p>
<pre><code class="language-txt">offset 0 접근</code></pre>
<p>만 하면 된다.</p>
<p>즉 일반 해시 탐색이 아니라:</p>
<blockquote>
<p>거의 C 구조체 접근처럼 동작한다.</p>
</blockquote>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<pre><code class="language-js">const a = { x: 1, y: 2 };

const b = { y: 2, x: 1 };</code></pre>
<p>겉보기엔 같아 보이지만</p>
<ul>
<li>프로퍼티 생성 순서가 다름</li>
<li>Hidden Class도 다르게 생성됨</li>
</ul>
<p>즉 엔진 입장에서는 서로 다른 객체 구조다.</p>
<hr>
<h2 id="react에서-왜-중요할까">React에서 왜 중요할까?</h2>
<p>예</p>
<pre><code class="language-js">const state = {
  name,
  age,
};</code></pre>
<p>객체 구조가 계속 일정하게 유지되면 엔진 최적화가 잘 유지된다.</p>
<p>반대로</p>
<pre><code class="language-js">const obj = {};

if (condition) {
  obj.name = &quot;me&quot;;
}</code></pre>
<p>처럼 구조가 계속 바뀌면 최적화가 깨질 가능성이 높아진다.</p>
<p>즉</p>
<blockquote>
<p>객체 shape 안정성</p>
</blockquote>
<p>이 성능과 연결된다.</p>
<hr>
<h1 id="3-inline-cache-ic">3. Inline Cache (IC)</h1>
<p>이건 Hidden Class와 연결되는 최적화다.</p>
<hr>
<h2 id="문제">문제</h2>
<pre><code class="language-js">user.name</code></pre>
<p>를 매번</p>
<ul>
<li>프로퍼티 탐색</li>
<li>위치 검색</li>
</ul>
<p>하면서 찾으면 느리다.</p>
<hr>
<h2 id="해결-방식">해결 방식</h2>
<p>엔진은 캐싱한다.</p>
<p>예를 들어</p>
<pre><code class="language-txt">&quot;이 객체는 HiddenClass1 이었지?&quot;</code></pre>
<p>를 기억해둔다.</p>
<p>그러면 다음 접근부터는</p>
<pre><code class="language-txt">바로 offset 접근</code></pre>
<p>가능하다.</p>
<p>이걸 Inline Cache(IC)라고 한다.</p>
<p>즉</p>
<blockquote>
<p>&quot;이 형태의 객체는 여기 접근하면 된다&quot;</p>
</blockquote>
<p>를 캐싱하는 구조다.</p>
<hr>
<h2 id="동작-흐름">동작 흐름</h2>
<p>처음 접근</p>
<pre><code class="language-txt">lookup 발생</code></pre>
<p>두 번째부터</p>
<pre><code class="language-txt">cache hit</code></pre>
<p>즉 훨씬 빨라진다.</p>
<hr>
<h2 id="문제-상황--megamorphic">문제 상황 — Megamorphic</h2>
<pre><code class="language-js">foo(obj1);
foo(obj2);
foo(obj3);</code></pre>
<p>근데 매번 객체 shape가 다르다면?</p>
<p>엔진 입장에서는</p>
<pre><code class="language-txt">&quot;왜 맨날 다른 객체가 들어오지?&quot;</code></pre>
<p>상태가 된다.</p>
<p>이를</p>
<pre><code class="language-txt">Megamorphic</code></pre>
<p>상태라고 부른다.</p>
<p>즉 Inline Cache가 너무 많은 형태를 처리해야 해서 최적화 효율이 떨어진다.</p>
<hr>
<h1 id="react와-연결되는-부분">React와 연결되는 부분</h1>
<p>예를 들어</p>
<pre><code class="language-tsx">&lt;Component style={{ color: &quot;red&quot; }} /&gt;</code></pre>
<p>이 코드는 렌더마다</p>
<pre><code class="language-js">{ color: &quot;red&quot; }</code></pre>
<p>새 객체를 생성한다.</p>
<p>즉</p>
<ul>
<li>새 reference</li>
<li>새 allocation</li>
<li>GC 증가</li>
<li>shape 최적화 방해 가능성</li>
</ul>
<p>이 생긴다.</p>
<p>그래서</p>
<ul>
<li>useMemo</li>
<li>useCallback</li>
<li>객체 memoization</li>
</ul>
<p>같은 패턴이 중요해진다.</p>
<hr>
<h1 id="실제-프론트엔드-예시">실제 프론트엔드 예시</h1>
<h2 id="좋지-않은-패턴">좋지 않은 패턴</h2>
<pre><code class="language-js">items.map((item) =&gt; ({
  ...item,
  active: true,
}));</code></pre>
<p>렌더마다</p>
<ul>
<li>새 객체 생성</li>
<li>새 allocation</li>
<li>GC 비용 증가</li>
</ul>
<p>가 발생한다.</p>
<p>물론 무조건 나쁘다는 건 아니지만:</p>
<blockquote>
<p>자주 렌더되는 대규모 리스트</p>
</blockquote>
<p>에서는 성능에 영향을 줄 수 있다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<h2 id="1단계--인터프리트">1단계 — 인터프리트</h2>
<p>일단 빠르게 실행</p>
<hr>
<h2 id="2단계--패턴-수집">2단계 — 패턴 수집</h2>
<p>엔진이 관찰한다.</p>
<pre><code class="language-txt">- 숫자만 오네?
- 객체 구조 일정하네?
- 호출 패턴 안정적이네?</code></pre>
<hr>
<h2 id="3단계--hidden-class-생성">3단계 — Hidden Class 생성</h2>
<p>객체 구조 최적화</p>
<hr>
<h2 id="4단계--inline-cache-적용">4단계 — Inline Cache 적용</h2>
<p>프로퍼티 접근 캐싱</p>
<hr>
<h2 id="5단계--jit-최적화">5단계 — JIT 최적화</h2>
<p>최적화된 머신코드 생성</p>
<hr>
<h2 id="6단계--예측-실패">6단계 — 예측 실패</h2>
<p>타입이나 구조가 바뀌면</p>
<pre><code class="language-txt">deopt 발생</code></pre>
<p>다시 일반 모드로 복귀</p>
<hr>
<h1 id="결국-핵심은-하나">결국 핵심은 하나</h1>
<p>JavaScript 엔진 최적화는 결국</p>
<blockquote>
<p>&quot;예측 가능한 코드&quot;</p>
</blockquote>
<p>를 좋아한다.</p>
<p>즉</p>
<ul>
<li>타입 안정성</li>
<li>객체 구조 안정성</li>
<li>일관된 호출 패턴</li>
</ul>
<p>이 성능 최적화의 핵심이다.</p>
<p>이걸 이해하면</p>
<ul>
<li>왜 객체 shape 유지가 중요한지</li>
<li>왜 dynamic property 추가가 느릴 수 있는지</li>
<li>왜 React re-render 최적화가 필요한지</li>
<li>왜 useMemo/useCallback을 쓰는지</li>
</ul>
<p>를 단순 React 레벨이 아니라</p>
<blockquote>
<p>&quot;엔진 수준&quot;</p>
</blockquote>
<p>에서 이해할 수 있게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Three.js + Next.js  requestAnimationFrame 메모리 누수 방지]]></title>
            <link>https://velog.io/@leave_a_comment/Three.js-Next.js-requestAnimationFrame-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%EB%B0%A9%EC%A7%80</link>
            <guid>https://velog.io/@leave_a_comment/Three.js-Next.js-requestAnimationFrame-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%EB%B0%A9%EC%A7%80</guid>
            <pubDate>Tue, 26 May 2026 10:20:02 GMT</pubDate>
            <description><![CDATA[<p>Three.js를 Next.js에서 처음 붙여보면 금방 이상한 걸 느끼게 된다.</p>
<blockquote>
<p>&quot;페이지를 이동했다 돌아오면 왜 점점 느려지지?&quot;</p>
</blockquote>
<p>처음엔 렌더링 문제처럼 보이지만 실제 원인은 대부분 cleanup 누락이다.</p>
<p>특히 <code>requestAnimationFrame</code>은 컴포넌트가 사라져도 자동으로 멈추지 않는다.</p>
<p>명시적으로 취소하지 않으면 이전 루프가 계속 살아 있고, GPU 메모리도 계속 쌓인다.</p>
<p>App Router처럼 페이지 전환이 잦은 환경에서는 특히 더 잘 드러난다.</p>
<ul>
<li>이전 페이지의 animation loop가 계속 실행되고</li>
<li>geometry / material / texture가 GPU 메모리에 남아 있으며</li>
<li>결국 탭이 느려지거나 WebGL context limit에 걸리기도 한다</li>
</ul>
<p>이번 글에서는 Three.js를 React <code>useEffect</code> 안에서 안전하게 사용하는 방법과 
정확히 무엇을 cleanup 해야 하는지 정리해보려 한다.</p>
<hr>
<h2 id="문제--멈추지-않는-루프">문제 — 멈추지 않는 루프</h2>
<p>보통 Three.js를 처음 세팅하면 이렇게 작성한다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const renderer = new THREE.WebGLRenderer({ canvas });

  const scene = new THREE.Scene();

  const geometry = new THREE.BoxGeometry();

  const material = new THREE.MeshBasicMaterial({
    color: &quot;blue&quot;,
  });

  const mesh = new THREE.Mesh(geometry, material);

  scene.add(mesh);

  const animate = () =&gt; {
    requestAnimationFrame(animate);

    mesh.rotation.y += 0.01;

    renderer.render(scene, camera);
  };

  animate();
}, []);</code></pre>
<p><code>[]</code> 덕분에 마운트 시 한 번만 실행된다.</p>
<p>문제는:</p>
<blockquote>
<p>언마운트 시 아무것도 정리하지 않는다는 것</p>
</blockquote>
<p>이다.</p>
<p>컴포넌트가 사라져도 내부적으로는:</p>
<pre><code class="language-txt">animate()
  ↓
requestAnimationFrame(animate)
  ↓
animate()
  ↓
...</code></pre>
<p>루프가 계속 반복된다.</p>
<p>즉 페이지를 이동해도 이전 animation loop가 백그라운드에서 계속 살아 있게 된다.</p>
<hr>
<h2 id="해결--cleanup-함수-사용">해결 — cleanup 함수 사용</h2>
<p><code>useEffect</code>는 cleanup 함수를 반환할 수 있다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  // 초기화

  return () =&gt; {
    // cleanup
  };
}, []);</code></pre>
<p>이 함수는:</p>
<ul>
<li>컴포넌트 언마운트 시</li>
<li>의존성 변경 직전</li>
</ul>
<p>자동으로 실행된다.</p>
<p>Three.js 리소스 정리는 모두 여기서 수행해야 한다.</p>
<hr>
<h2 id="requestanimationframe-취소">requestAnimationFrame 취소</h2>
<p>먼저 animation loop를 멈춰야 한다.</p>
<pre><code class="language-tsx">let rafId = 0;

const animate = () =&gt; {
  rafId = requestAnimationFrame(animate);

  renderer.render(scene, camera);
};

animate();

return () =&gt; {
  cancelAnimationFrame(rafId);
};</code></pre>
<p>핵심은:</p>
<pre><code class="language-ts">rafId = requestAnimationFrame(animate);</code></pre>
<p>처럼:</p>
<blockquote>
<p>매 프레임마다 최신 ID를 저장하는 것</p>
</blockquote>
<p>이다.</p>
<p>cleanup 시 마지막 예약 ID를 취소하면 다음 프레임부터 루프가 멈춘다.</p>
<hr>
<h2 id="중요한-문제--gpu-메모리는-자동-해제되지-않는다">중요한 문제 — GPU 메모리는 자동 해제되지 않는다</h2>
<p>루프를 멈췄다고 끝난 게 아니다.</p>
<p>Three.js의:</p>
<ul>
<li>geometry</li>
<li>material</li>
<li>texture</li>
</ul>
<p>같은 객체들은 내부적으로 GPU(WebGL)에 업로드된다.</p>
<p>즉 JavaScript 객체가 GC 대상이 되더라도:</p>
<blockquote>
<p>GPU 메모리는 별도로 dispose 해야 한다.</p>
</blockquote>
<hr>
<h2 id="dispose">dispose()</h2>
<pre><code class="language-tsx">return () =&gt; {
  cancelAnimationFrame(rafId);

  geometry.dispose();

  material.dispose();

  texture.dispose();

  renderer.dispose();
};</code></pre>
<p>각각 의미는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>리소스</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>geometry.dispose()</td>
<td>GPU 버텍스 버퍼 해제</td>
</tr>
<tr>
<td>material.dispose()</td>
<td>셰이더 / 유니폼 해제</td>
</tr>
<tr>
<td>texture.dispose()</td>
<td>텍스처 메모리 해제</td>
</tr>
<tr>
<td>renderer.dispose()</td>
<td>WebGL context 정리</td>
</tr>
</tbody></table>
<p>특히 <code>texture.dispose()</code>는 자주 빠뜨린다.</p>
<p><code>material.dispose()</code>를 호출해도 texture는 자동 해제되지 않는다.</p>
<hr>
<h2 id="실제-패턴">실제 패턴</h2>
<p>보통은 이런 형태로 정리한다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const canvas = canvasRef.current;

  if (!canvas) return;

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
  });

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(
    60,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    100,
  );

  camera.position.z = 3;

  const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);

  const material = new THREE.MeshBasicMaterial({
    color: &quot;#2563eb&quot;,
    wireframe: true,
  });

  const mesh = new THREE.Mesh(geometry, material);

  scene.add(mesh);

  let rafId = 0;

  const animate = () =&gt; {
    rafId = requestAnimationFrame(animate);

    mesh.rotation.y += 0.01;

    renderer.render(scene, camera);
  };

  animate();

  return () =&gt; {
    cancelAnimationFrame(rafId);

    geometry.dispose();
    material.dispose();

    renderer.dispose();
  };
}, []);</code></pre>
<hr>
<h2 id="데모-전환-시--의존성-변경-패턴">데모 전환 시 — 의존성 변경 패턴</h2>
<p>탭 기반으로 여러 데모를 교체하는 경우에는:</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const cleanup = initDemo(activeDemo, canvas);

  return () =&gt; {
    cleanup();
  };
}, [activeDemo]);</code></pre>
<p>형태를 자주 사용한다.</p>
<p>흐름은:</p>
<pre><code class="language-txt">이전 cleanup 실행
  ↓
raf 취소 + dispose
  ↓
새 데모 초기화</code></pre>
<p>순서로 진행된다.</p>
<p>즉 데모를 빠르게 전환해도 이전 WebGL 리소스가 남지 않는다.</p>
<hr>
<h2 id="orbitcontrols--gui도-cleanup-필요">OrbitControls / GUI도 cleanup 필요</h2>
<p>Three.js 자체뿐 아니라 부가 라이브러리도 정리해야 한다.</p>
<table>
<thead>
<tr>
<th>리소스</th>
<th>cleanup</th>
</tr>
</thead>
<tbody><tr>
<td>OrbitControls</td>
<td>controls.dispose()</td>
</tr>
<tr>
<td>lil-gui</td>
<td>gui.destroy()</td>
</tr>
<tr>
<td>Stats.js</td>
<td>dom 제거</td>
</tr>
<tr>
<td>postprocessing composer</td>
<td>dispose()</td>
</tr>
</tbody></table>
<p>특히 <code>OrbitControls</code>는 이벤트 리스너를 등록하기 때문에 dispose하지 않으면 마우스 이벤트가 계속 살아 있다.</p>
<hr>
<h2 id="window-이벤트-리스너도-주의">window 이벤트 리스너도 주의</h2>
<p>이건 Three.js 외부 문제지만 정말 자주 놓친다.</p>
<pre><code class="language-tsx">const onResize = () =&gt; {
  // resize 처리
};

window.addEventListener(&quot;resize&quot;, onResize);</code></pre>
<p>cleanup에서 반드시 제거해야 한다.</p>
<pre><code class="language-tsx">return () =&gt; {
  window.removeEventListener(&quot;resize&quot;, onResize);
};</code></pre>
<p>canvas 이벤트도 동일하다.</p>
<pre><code class="language-tsx">canvas.addEventListener(&quot;mousemove&quot;, onMouseMove);</code></pre>
<p>↓</p>
<pre><code class="language-tsx">canvas.removeEventListener(&quot;mousemove&quot;, onMouseMove);</code></pre>
<p>정리하지 않으면:</p>
<ul>
<li>stale closure</li>
<li>중복 이벤트 실행</li>
<li>메모리 누수</li>
</ul>
<p>문제가 발생할 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>누락 시 문제</th>
<th>해결 방법</th>
</tr>
</thead>
<tbody><tr>
<td>cancelAnimationFrame</td>
<td>animation loop 지속</td>
<td>rafId 추적 후 취소</td>
</tr>
<tr>
<td>geometry.dispose()</td>
<td>GPU 버퍼 누수</td>
<td>cleanup에서 해제</td>
</tr>
<tr>
<td>material.dispose()</td>
<td>shader 누수</td>
<td>cleanup에서 해제</td>
</tr>
<tr>
<td>texture.dispose()</td>
<td>texture 메모리 누수</td>
<td>명시적 dispose</td>
</tr>
<tr>
<td>renderer.dispose()</td>
<td>WebGL context 누수</td>
<td>마지막에 dispose</td>
</tr>
<tr>
<td>removeEventListener</td>
<td>stale callback</td>
<td>cleanup에서 제거</td>
</tr>
</tbody></table>
<p>Three.js를 React에서 사용할 때 핵심은 결국 하나다.</p>
<blockquote>
<p>초기화한 것은 반드시 cleanup에서 역순으로 정리한다.</p>
</blockquote>
<ul>
<li>animation loop 중단</li>
<li>GPU 리소스 dispose</li>
<li>이벤트 리스너 제거</li>
</ul>
<p>이 세 가지만 제대로 관리해도 Next.js 환경에서 Three.js를 훨씬 안정적으로 사용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[채팅 메시지에 Optimistic Update 적용하기]]></title>
            <link>https://velog.io/@leave_a_comment/%EC%B1%84%ED%8C%85-%EB%A9%94%EC%8B%9C%EC%A7%80%EC%97%90-Optimistic-Update-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leave_a_comment/%EC%B1%84%ED%8C%85-%EB%A9%94%EC%8B%9C%EC%A7%80%EC%97%90-Optimistic-Update-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 22 May 2026 02:21:36 GMT</pubDate>
            <description><![CDATA[<p>채팅 기능을 구현하다 보면 가장 먼저 체감되는 문제가 하나 있다.</p>
<blockquote>
<p>&quot;전송 버튼을 눌렀는데 내 메시지가 바로 안 뜬다.&quot;</p>
</blockquote>
<p>WebSocket 기반이든 HTTP 기반이든,
서버 응답 이후에만 메시지를 추가하면 그 사이 UI가 멈춘 것처럼 보인다.</p>
<p>특히 네트워크가 느릴수록:</p>
<ul>
<li>버튼을 눌렀는데 반응이 없고</li>
<li>메시지가 늦게 나타나며</li>
<li>UX가 답답하게 느껴진다.</li>
</ul>
<p>이번 글에서는 채팅 메시지 전송에 Optimistic Update를 적용하는 방법과,
서버 메시지와 로컬 메시지를 어떻게 동기화하는지 정리해보려 한다.</p>
<hr>
<h2 id="기본-구조--pending-상태-추가">기본 구조 — pending 상태 추가</h2>
<p>먼저 메시지 타입에 <code>pending</code> 상태를 추가한다.</p>
<pre><code class="language-ts">type LocalMessage = ChatMessage &amp; {
  pending?: boolean;
};</code></pre>
<ul>
<li>서버 메시지 → <code>pending</code> 없음 또는 <code>false</code></li>
<li>아직 서버 확인 전인 메시지 → <code>pending: true</code></li>
</ul>
<p>즉:</p>
<blockquote>
<p>&quot;아직 서버 검증이 끝나지 않은 임시 메시지&quot;</p>
</blockquote>
<p>라는 상태를 표현하는 용도다.</p>
<p>실제로는 이 한 줄의 타입 확장만으로도 Optimistic Update의 기반이 만들어진다.</p>
<hr>
<h2 id="메시지-전송--먼저-렌더링하고-나중에-전송">메시지 전송 — 먼저 렌더링하고 나중에 전송</h2>
<pre><code class="language-tsx">const sendMessage = useCallback((content: string) =&gt; {
  const ws = wsRef.current;

  if (!ws || ws.readyState !== WebSocket.OPEN) return;

  // 1. 먼저 UI에 추가
  const optimistic: LocalMessage = {
    id: `pending_${Date.now()}`,
    content,
    author: currentUser,
    createdAt: new Date().toISOString(),
    pending: true,
  };

  setMessages((prev) =&gt; [...prev, optimistic]);

  // 2. 실제 전송은 그 다음
  ws.send(
    JSON.stringify({
      type: &quot;chat:send&quot;,
      payload: { content },
    }),
  );
}, []);</code></pre>
<p>핵심은 순서다.</p>
<p>기존 방식은:</p>
<pre><code class="language-txt">전송 → 서버 응답 → 렌더</code></pre>
<p>였다면,</p>
<p>Optimistic Update는:</p>
<pre><code class="language-txt">렌더 → 전송 → 서버 검증</code></pre>
<p>순서로 동작한다.</p>
<p>즉 유저 입장에서는 버튼을 누르는 즉시 내 메시지가 화면에 보인다.</p>
<hr>
<h2 id="서버-응답-처리--pending-메시지-교체">서버 응답 처리 — pending 메시지 교체</h2>
<p>이후 서버에서 <code>chat:message</code> 이벤트가 도착하면:</p>
<ol>
<li>내가 보낸 pending 메시지인지 확인</li>
<li>맞다면 실제 서버 메시지로 교체</li>
<li>아니라면 다른 유저 메시지로 추가</li>
</ol>
<p>처리한다.</p>
<pre><code class="language-tsx">case &quot;chat:message&quot;: {
  const incoming = data.payload.message;

  setMessages((prev) =&gt; {
    const pendingIdx = prev.findIndex(
      (m) =&gt;
        m.pending &amp;&amp;
        m.content === incoming.content &amp;&amp;
        m.author.id === incoming.author.id,
    );

    // 1. pending 메시지 교체
    if (pendingIdx !== -1) {
      const next = [...prev];
      next[pendingIdx] = incoming;
      return next;
    }

    // 2. 중복 방지
    if (prev.some((m) =&gt; m.id === incoming.id)) {
      return prev;
    }

    // 3. 다른 유저 메시지 추가
    return [...prev, incoming];
  });
}</code></pre>
<hr>
<h2 id="전체-흐름">전체 흐름</h2>
<pre><code class="language-txt">전송 버튼 클릭
  ↓
pending 메시지 즉시 추가
(id: pending_xxx, pending: true)
  ↓
WebSocket 전송
  ↓
서버에서 chat:message 이벤트 도착
  ↓
pending 메시지 탐색
  ↓
일치하면 → 서버 메시지로 교체
불일치하면 → 새 메시지 추가</code></pre>
<hr>
<h2 id="핵심--pending-메시지-매칭-기준">핵심 — pending 메시지 매칭 기준</h2>
<p>가장 중요한 건:</p>
<blockquote>
<p>&quot;서버 메시지와 어떤 기준으로 매칭할 것인가&quot;</p>
</blockquote>
<p>다.</p>
<p>현재 구현에서는:</p>
<pre><code class="language-ts">m.pending &amp;&amp;
m.content === incoming.content &amp;&amp;
m.author.id === incoming.author.id</code></pre>
<p>조건으로 찾고 있다.</p>
<hr>
<h3 id="왜-id로-비교하지-않을까">왜 id로 비교하지 않을까?</h3>
<p>pending 메시지의 id는:</p>
<pre><code class="language-txt">pending_123123</code></pre>
<p>같은 임시값이다.</p>
<p>반면 서버 메시지는:</p>
<pre><code class="language-txt">msg_87asd7</code></pre>
<p>처럼 실제 DB 기반 id를 가진다.</p>
<p>즉 서로 값이 다르기 때문에 id 비교로는 매칭할 수 없다.</p>
<p>그래서:</p>
<ul>
<li>content</li>
<li>author.id</li>
</ul>
<p>조합으로 &quot;같은 메시지&quot;인지 판단한다.</p>
<hr>
<h2 id="ui에서-pending-상태-표현하기">UI에서 pending 상태 표현하기</h2>
<p><code>pending</code> 플래그를 사용하면 전송 중 상태를 자연스럽게 표시할 수 있다.</p>
<pre><code class="language-tsx">{messages.map((msg) =&gt; (
  &lt;div
    key={msg.id}
    style={{
      opacity: msg.pending ? 0.5 : 1,
    }}
  &gt;
    {msg.content}

    {msg.pending &amp;&amp; (
      &lt;span&gt;전송 중...&lt;/span&gt;
    )}
  &lt;/div&gt;
))}</code></pre>
<p>유저 입장에서는:</p>
<ol>
<li>메시지가 즉시 나타나고</li>
<li>서버 응답 전까지 흐리게 표시되며</li>
<li>서버 확인 후 정상 상태로 변경</li>
</ol>
<p>되는 흐름으로 보인다.</p>
<hr>
<h2 id="주의할-점--rollback-처리">주의할 점 — rollback 처리</h2>
<p>현재 구조는 사실상:</p>
<pre><code class="language-ts">ws.send(...)</code></pre>
<p>후 응답을 기다리지 않는 fire-and-forget 방식이다.</p>
<p>즉 전송 실패 시 pending 메시지가 계속 남아 있을 수 있다.</p>
<hr>
<h2 id="실패-처리-방법">실패 처리 방법</h2>
<p>예를 들어 연결이 끊겼다면:</p>
<pre><code class="language-tsx">setMessages((prev) =&gt;
  prev.filter((m) =&gt; !m.pending),
);</code></pre>
<p>처럼 rollback 처리할 수 있다.</p>
<p>또는:</p>
<pre><code class="language-ts">pending: &quot;failed&quot;</code></pre>
<p>같은 상태를 추가해:</p>
<ul>
<li>재전송 버튼</li>
<li>실패 표시 UI</li>
</ul>
<p>를 제공할 수도 있다.</p>
<hr>
<h2 id="케이스별-처리-전략">케이스별 처리 전략</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>처리 방식</th>
</tr>
</thead>
<tbody><tr>
<td>서버 응답 성공</td>
<td>pending → 실제 메시지 교체</td>
</tr>
<tr>
<td>연결 끊김</td>
<td>pending 제거 또는 실패 표시</td>
</tr>
<tr>
<td>중복 메시지 수신</td>
<td>id 기준 중복 방지</td>
</tr>
<tr>
<td>전송 실패</td>
<td>rollback 또는 재전송</td>
</tr>
</tbody></table>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존 방식</th>
<th>Optimistic Update</th>
</tr>
</thead>
<tbody><tr>
<td>메시지 표시 시점</td>
<td>서버 응답 후</td>
<td>전송 즉시</td>
</tr>
<tr>
<td>전송 상태 표시</td>
<td>없음</td>
<td>pending 상태</td>
</tr>
<tr>
<td>서버 응답 처리</td>
<td>단순 추가</td>
<td>pending 메시지 교체</td>
</tr>
<tr>
<td>실패 처리</td>
<td>필요 없음</td>
<td>rollback 필요</td>
</tr>
<tr>
<td>UX</td>
<td>응답 지연 체감</td>
<td>즉각적인 반응</td>
</tr>
</tbody></table>
<p>채팅에서 Optimistic Update의 핵심은 결국 두 가지다.</p>
<blockquote>
<p>먼저 보여주고,</p>
<p>나중에 서버 상태와 맞춘다.</p>
</blockquote>
<p>그리고 서버 응답이 도착하면:</p>
<blockquote>
<p>임시 메시지를 실제 메시지로 교체한다.</p>
</blockquote>
<p>이 흐름만 잘 설계해도 채팅 UX 체감 속도는 꽤 크게 개선된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 React에서 자료구조/알고리즘이 중요한가?]]></title>
            <link>https://velog.io/@leave_a_comment/%EC%99%9C-React%EC%97%90%EC%84%9C-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80</link>
            <guid>https://velog.io/@leave_a_comment/%EC%99%9C-React%EC%97%90%EC%84%9C-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80</guid>
            <pubDate>Thu, 21 May 2026 01:38:50 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드는 단순히 UI만 그리는 영역이라고 생각하기 쉽지만,
실제로는 렌더링 과정에서 자료구조와 알고리즘 개념이 굉장히 많이 사용된다.</p>
<p>특히 React를 사용하다 보면:</p>
<ul>
<li>왜 <code>useMemo</code>를 쓰는지</li>
<li>왜 <code>.map()</code> 남발이 성능에 안 좋은지</li>
<li>왜 <code>key</code>가 중요한지</li>
<li>왜 <code>Set</code>으로 중복 제거를 하는지</li>
</ul>
<p>같은 것들이 결국 자료구조/알고리즘 개념과 연결된다.</p>
<hr>
<h2 id="1-왜-usememo가-필요한가">1. 왜 <code>useMemo</code>가 필요한가</h2>
<p>핵심은:</p>
<blockquote>
<p>같은 계산을 매 렌더마다 다시 하지 않기 위해</p>
</blockquote>
<p>자료구조/알고리즘 관점에서는:</p>
<ul>
<li>연산 비용 감소</li>
<li>시간복잡도 최소화</li>
<li>캐싱(Cache)</li>
</ul>
<p>개념에 가깝다.</p>
<p>예를 들어:</p>
<pre><code class="language-tsx">const sorted = users.sort(...)</code></pre>
<p>이 코드가 렌더마다 실행되면:</p>
<blockquote>
<p>렌더 횟수 × 정렬 비용</p>
</blockquote>
<p>이 계속 발생하게 된다.</p>
<p>정렬은 일반적으로:</p>
<pre><code class="language-txt">O(n log n)</code></pre>
<p>복잡도를 가지기 때문에 데이터가 많아질수록 부담이 커진다.</p>
<hr>
<h3 id="usememo는-결국-캐싱">useMemo는 결국 캐싱</h3>
<p><code>useMemo</code>는 내부적으로 보면:</p>
<pre><code class="language-ts">{
  deps: [...],
  cachedValue: result
}</code></pre>
<p>처럼 이전 계산 결과를 저장해두는 개념에 가깝다.</p>
<p>예를 들면:</p>
<pre><code class="language-tsx">const sorted = useMemo(() =&gt; {
  return users.sort(...)
}, [users])</code></pre>
<p>이렇게 작성하면:</p>
<ul>
<li><code>users</code>가 변경되지 않는 동안</li>
<li>이전 계산 결과를 재사용</li>
</ul>
<p>하게 된다.</p>
<p>즉:</p>
<blockquote>
<p>불필요한 O(n log n) 연산을 줄이는 것</p>
</blockquote>
<p>이 핵심이다.</p>
<hr>
<h2 id="2-왜-map-남발하면-느릴까">2. 왜 <code>.map()</code> 남발하면 느릴까</h2>
<p>핵심은:</p>
<blockquote>
<p>배열 순회는 공짜가 아니다</p>
</blockquote>
<p><code>.map()</code>은 기본적으로:</p>
<pre><code class="language-txt">O(n)</code></pre>
<p>복잡도를 가진다.</p>
<p>예를 들어:</p>
<pre><code class="language-tsx">users.map(...)
posts.map(...)
comments.map(...)</code></pre>
<p>이런 코드가 렌더마다 반복되면:</p>
<pre><code class="language-txt">O(n) + O(n) + O(n)</code></pre>
<p>비용이 계속 발생한다.</p>
<hr>
<h3 id="더-위험한-건-중첩-반복문">더 위험한 건 중첩 반복문</h3>
<p>예:</p>
<pre><code class="language-tsx">users.map(user =&gt; {
  posts.map(post =&gt; ...)
})</code></pre>
<p>이 경우는:</p>
<pre><code class="language-txt">O(n²)</code></pre>
<p>까지 증가할 수 있다.</p>
<p>React에서 특히 문제가 되는 이유는:</p>
<ul>
<li>state 변경</li>
<li>props 변경</li>
<li>context 변경</li>
</ul>
<p>등으로 렌더링이 자주 발생하기 때문이다.</p>
<p>즉 <code>.map()</code>이 렌더마다 계속 반복 실행된다.</p>
<hr>
<h2 id="실무에서-사용하는-해결-방법">실무에서 사용하는 해결 방법</h2>
<h3 id="1-memoization">1. Memoization</h3>
<pre><code class="language-tsx">const items = useMemo(() =&gt; {
  return data.map(...)
}, [data])</code></pre>
<p>불필요한 반복 계산을 줄인다.</p>
<hr>
<h3 id="2-virtualization">2. Virtualization</h3>
<p>예:</p>
<ul>
<li>FlashList</li>
<li>react-window</li>
</ul>
<p>화면에 보이는 아이템만 렌더링해 렌더 비용을 줄인다.</p>
<hr>
<h3 id="3-normalization">3. Normalization</h3>
<p>배열 대신 객체나 <code>Map</code> 형태로 관리한다.</p>
<p>예:</p>
<pre><code class="language-ts">usersById[user.id]</code></pre>
<p>이런 방식은 탐색 비용을 줄일 수 있다.</p>
<hr>
<h2 id="3-왜-key가-중요한가">3. 왜 <code>key</code>가 중요한가</h2>
<p>이건 React의 Diffing 알고리즘과 연결된다.</p>
<p>React는 이전 배열과 새 배열을 비교할 때:</p>
<blockquote>
<p>&quot;누가 누구인지&quot;</p>
</blockquote>
<p>판별해야 한다.</p>
<p>예:</p>
<pre><code class="language-ts">[
  { id: 1 },
  { id: 2 },
  { id: 3 }
]</code></pre>
<p>React는 <code>key</code> 기반으로 비교한다.</p>
<pre><code class="language-tsx">key={item.id}</code></pre>
<hr>
<h3 id="그런데-index를-쓰면">그런데 <code>index</code>를 쓰면?</h3>
<p>예를 들어:</p>
<pre><code class="language-txt">A B C</code></pre>
<p>상태에서 앞에 <code>D</code>가 추가되면:</p>
<pre><code class="language-txt">D A B C</code></pre>
<p>가 된다.</p>
<p>하지만 <code>key={index}</code>를 사용하면 React는:</p>
<pre><code class="language-txt">0 = A → D로 착각
1 = B → A로 착각
2 = C → B로 착각</code></pre>
<p>하게 된다.</p>
<p>그 결과:</p>
<ul>
<li>DOM 재사용 꼬임</li>
<li>state 꼬임</li>
<li>불필요한 re-render 증가</li>
</ul>
<p>같은 문제가 발생할 수 있다.</p>
<p>자료구조 관점으로 보면 결국:</p>
<blockquote>
<p>고유 식별자(primary key)</p>
</blockquote>
<p>문제에 가깝다.</p>
<p>DB에서 <code>id</code>가 중요한 이유와 비슷하다.</p>
<hr>
<h2 id="4-왜-set으로-중복-제거를-할까">4. 왜 <code>Set</code>으로 중복 제거를 할까</h2>
<p>배열에서 중복 검사를 할 때 보통:</p>
<pre><code class="language-ts">arr.includes(x)</code></pre>
<p>를 사용한다.</p>
<p>하지만 <code>includes</code>는:</p>
<pre><code class="language-txt">O(n)</code></pre>
<p>복잡도를 가진다.</p>
<p>예를 들어:</p>
<pre><code class="language-ts">const result = []

for (const item of items) {
  if (!result.includes(item)) {
    result.push(item)
  }
}</code></pre>
<p>이 방식은 최악의 경우:</p>
<pre><code class="language-txt">O(n²)</code></pre>
<p>까지 증가할 수 있다.</p>
<hr>
<h3 id="set은-해시-기반">Set은 해시 기반</h3>
<p>반면 <code>Set</code>은 해시 기반 자료구조라 탐색 비용이 매우 낮다.</p>
<pre><code class="language-ts">const set = new Set(items)</code></pre>
<p>검색은 평균적으로:</p>
<pre><code class="language-txt">O(1)</code></pre>
<p>에 가깝다.</p>
<p>그래서 실무에서는:</p>
<pre><code class="language-ts">const unique = [...new Set(items)]</code></pre>
<p>패턴을 굉장히 자주 사용한다.</p>
<p>예를 들어:</p>
<pre><code class="language-ts">onlineUsers = [...new Set(users)]</code></pre>
<p>처럼 websocket 중복 유저 제거에도 많이 사용된다.</p>
<hr>
<p>결국 React 성능 최적화도 대부분 자료구조와 알고리즘 개념 위에서 동작한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 Next.js App Router에서 Emotion을 쓰면 오류가 자주 발생할까?]]></title>
            <link>https://velog.io/@leave_a_comment/%EC%99%9C-Next.js-App-Router%EC%97%90%EC%84%9C-Emotion%EC%9D%84-%EC%93%B0%EB%A9%B4-%EC%98%A4%EB%A5%98%EA%B0%80-%EC%9E%90%EC%A3%BC-%EB%B0%9C%EC%83%9D%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@leave_a_comment/%EC%99%9C-Next.js-App-Router%EC%97%90%EC%84%9C-Emotion%EC%9D%84-%EC%93%B0%EB%A9%B4-%EC%98%A4%EB%A5%98%EA%B0%80-%EC%9E%90%EC%A3%BC-%EB%B0%9C%EC%83%9D%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 19 May 2026 12:24:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leave_a_comment/post/6584b14d-e811-4d01-94bd-5f29db8820c3/image.png" alt=""></p>
<p>Next.js App Router 환경에서 Emotion을 사용하다 보면 아래와 같은 오류를 종종 보게 됩니다.</p>
<pre><code class="language-bash">Warning: Prop `className` did not match</code></pre>
<p>또는:</p>
<ul>
<li>Hydration mismatch</li>
<li>스타일 깜빡임(FOUC)</li>
<li>스타일 우선순위 꼬임</li>
<li>서버/클라이언트 스타일 불일치</li>
</ul>
<p>이번 글에서는 왜 이런 문제가 발생하는지 간단하게 정리해보겠습니다.</p>
<hr>
<h2 id="app-router는-기본적으로-server-component-기반">App Router는 기본적으로 Server Component 기반</h2>
<p>Next.js App Router는 기존 Pages Router와 달리 기본적으로 <strong>Server Component 기반 구조</strong>입니다.</p>
<p>즉 컴포넌트가:</p>
<ol>
<li>서버에서 먼저 렌더링되고</li>
<li>HTML이 전달된 뒤</li>
<li>클라이언트에서 hydration 되는 방식입니다.</li>
</ol>
<p>반면 Emotion은:</p>
<ul>
<li>런타임에서 스타일 생성</li>
<li>동적으로 className 생성</li>
<li>스타일 삽입 순서(insertion order) 관리</li>
</ul>
<p>같은 특징을 가지고 있습니다.</p>
<p>즉 Emotion은 기본적으로 클라이언트 환경에서 동작하는 CSS-in-JS 방식에 더 가깝습니다.</p>
<p>여기서 App Router와 충돌이 발생할 수 있습니다.</p>
<hr>
<h2 id="가장-많이-발생하는-문제-classname-불일치">가장 많이 발생하는 문제: className 불일치</h2>
<p>Emotion은 렌더링 시점마다 className을 생성합니다.</p>
<p>예를 들면:</p>
<pre><code class="language-tsx">css-1abcde
css-92kda</code></pre>
<p>같은 형태의 className이 만들어집니다.</p>
<p>그런데 App Router 환경에서는:</p>
<ol>
<li>서버 렌더링</li>
<li>클라이언트 hydration</li>
<li>다시 렌더링</li>
</ol>
<p>과정을 거치면서 스타일 생성 순서가 달라질 수 있습니다.</p>
<p>그 결과:</p>
<pre><code class="language-bash">Warning: Prop `className` did not match</code></pre>
<p>같은 hydration 오류가 발생하게 됩니다.</p>
<hr>
<h2 id="왜-app-router에서-더-자주-발생할까">왜 App Router에서 더 자주 발생할까?</h2>
<p>React 18 + App Router는 아래 기능들을 적극적으로 사용합니다.</p>
<ul>
<li>Streaming SSR</li>
<li>Suspense</li>
<li>Concurrent Rendering</li>
</ul>
<p>이 환경에서는 컴포넌트 렌더링 순서가 항상 동일하지 않을 수 있습니다.</p>
<p>하지만 Emotion은:</p>
<ul>
<li>어떤 컴포넌트가 먼저 렌더됐는지</li>
<li>어떤 스타일이 먼저 삽입됐는지</li>
</ul>
<p>에 영향을 많이 받습니다.</p>
<p>즉:</p>
<blockquote>
<p>렌더링 순서가 바뀌는 환경에서 런타임으로 스타일을 생성하다 보니 문제가 발생하는 것</p>
</blockquote>
<p>에 가깝습니다.</p>
<hr>
<h2 id="실제로-나타나는-현상들">실제로 나타나는 현상들</h2>
<h3 id="1-hydration-mismatch">1. Hydration mismatch</h3>
<pre><code class="language-bash">Warning: Prop `className` did not match</code></pre>
<p>서버와 클라이언트의 className이 달라지는 문제입니다.</p>
<hr>
<h3 id="2-스타일-깜빡임-fouc">2. 스타일 깜빡임 (FOUC)</h3>
<p>초기 렌더 시 스타일이 적용되지 않았다가 뒤늦게 적용되는 현상입니다.</p>
<hr>
<h3 id="3-스타일-우선순위-꼬임">3. 스타일 우선순위 꼬임</h3>
<p>Emotion의 insertion order가 달라지면서 예상과 다른 스타일이 적용될 수 있습니다.</p>
<hr>
<h3 id="4-특정-상황에서-스타일이-사라지는-문제">4. 특정 상황에서 스타일이 사라지는 문제</h3>
<p>재현 조건이 일정하지 않은 경우가 많아 디버깅도 쉽지 않습니다.</p>
<hr>
<h2 id="styled-components도-비슷한데-왜-emotion-이야기가-더-많을까">styled-components도 비슷한데 왜 Emotion 이야기가 더 많을까?</h2>
<p>Emotion은 특히:</p>
<ul>
<li>런타임 의존성</li>
<li>cache 관리</li>
<li>insertion order</li>
</ul>
<p>영향을 크게 받습니다.</p>
<p>그래서 App Router 환경에서는 설정이 조금만 어긋나도 문제가 쉽게 발생합니다.</p>
<p>특히 아래 설정이 중요합니다.</p>
<pre><code class="language-tsx">CacheProvider
useServerInsertedHTML</code></pre>
<p>이 설정 없이 사용하면 스타일 충돌 가능성이 높아집니다.</p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>보통은 Emotion Registry를 직접 구성해서 사용합니다.</p>
<p>예를 들면:</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { CacheProvider } from &#39;@emotion/react&#39;
import createCache from &#39;@emotion/cache&#39;</code></pre>
<p>그리고 Next.js의:</p>
<pre><code class="language-tsx">useServerInsertedHTML()</code></pre>
<p>를 사용해 서버 렌더링 시 style 태그를 직접 삽입합니다.</p>
<p>핵심은:</p>
<blockquote>
<p>서버와 클라이언트의 스타일 생성 순서를 최대한 동일하게 맞추는 것</p>
</blockquote>
<p>입니다.</p>
<hr>
<h2 id="그래서-요즘은-어떤-방식이-많이-쓰일까">그래서 요즘은 어떤 방식이 많이 쓰일까?</h2>
<p>최근 App Router 환경에서는 아래 스타일링 방식 선호도가 높아지고 있습니다.</p>
<ul>
<li>Tailwind CSS</li>
<li>CSS Modules</li>
<li>Panda CSS</li>
<li>Vanilla Extract</li>
</ul>
<p>이유는:</p>
<ul>
<li>hydration 문제 감소</li>
<li>서버 컴포넌트 친화적</li>
<li>런타임 비용 감소</li>
<li>번들 크기 감소</li>
</ul>
<p>같은 장점 때문입니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>Emotion 자체가 문제 있는 라이브러리는 아닙니다.</p>
<p>다만 Next.js App Router + React 18 환경에서는:</p>
<ul>
<li>Streaming SSR</li>
<li>Concurrent Rendering</li>
<li>Server Components</li>
</ul>
<p>구조와 런타임 스타일 생성 방식이 충돌하기 쉬운 편입니다.</p>
<p>그래서 최근에는 빌드 타임 기반 스타일링 방식도 많이 사용되는 추세입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React Native] Sticky Header]]></title>
            <link>https://velog.io/@leave_a_comment/React-Native-Sticky-Header</link>
            <guid>https://velog.io/@leave_a_comment/React-Native-Sticky-Header</guid>
            <pubDate>Sun, 25 Jan 2026 15:33:38 GMT</pubDate>
            <description><![CDATA[<p>자사 서비스 개발을 하다 보니 Sticky Header UI를 구현할 일이 생각보다 많았다.</p>
<p>React Native에서는 성능 이슈가 비교적 쉽게 발생하는 만큼 
리스트 렌더링에는 FlashList를 사용하며 최대한 최적화를 고려하고 있다.</p>
<p>이번 글에서는 FlashList + Sticky Header를 구현하면서 특히 애를 먹었던 부분들과 그 과정에서 정리한 포인트들을 공유해보려고 한다.</p>
<hr>
<h2 id="첫-번째-시도">첫 번째 시도</h2>
<p>FlashList에서 제공하는 <code>stickyHeaderIndices</code> 옵션을 활용해 Sticky Header를 구현해보았다.</p>
<p>하지만 예상과는 다른 문제가 발생했다.</p>
<h3 id="문제점">문제점</h3>
<p><code>stickyHeaderIndices</code>는 <code>renderItem</code>으로 렌더링되는 항목에만 적용되며,
<code>ListHeaderComponent</code>에는 적용할 수 없었다.</p>
<p>그 결과 리스트의 <strong>첫 번째 상품 아이템이 고정되는 현상</strong>이 발생했고,
의도했던 Sticky Header와는 전혀 다른 어색한 UI가 만들어졌다.</p>
<hr>
<h2 id="두-번째-시도">두 번째 시도</h2>
<p>그래서 다음으로는:</p>
<blockquote>
<p><code>ListHeaderComponent</code>를 사용하지 않고 Header 자체를 <code>renderItem</code> 안으로 넣어보자</p>
</blockquote>
<p>라는 방향으로 구현을 변경했다.</p>
<p>이를 위해 리스트 데이터에 타입을 부여해 각 아이템 역할을 구분하도록 구성했다.</p>
<ul>
<li><code>type: &#39;header&#39;</code> → Sticky Header 영역</li>
<li><code>type: &#39;item&#39;</code> → 일반 상품 아이템</li>
</ul>
<p>이렇게 하면 Header 역시 리스트 아이템처럼 취급되기 때문에 <code>stickyHeaderIndices</code>를 적용할 수 있을 것이라 생각했다.</p>
<p>하지만 이 방식 역시 FlashList의 구조와 완전히 잘 맞는 방식은 아니었다.</p>
<p>FlashList 입장에서는 Header 또한 일반 아이템과 동일하게 처리되기 때문에:</p>
<ul>
<li>재사용 대상이 되고</li>
<li>스크롤 상황에 따라 unmount / mount가 발생할 수 있으며</li>
<li>Sticky 상태에서 불필요한 re-render가 발생할 가능성이 있었다.</li>
</ul>
<p>결과적으로 FlashList의 <strong>가상화 장점을 일부 희생하는 구조</strong>에 가까웠다.</p>
<p>또한 성능 관점에서도 아쉬움이 있었다.</p>
<p>FlashList는 기본적으로 아이템 가상화에 최적화된 리스트인데,
<code>stickyHeaderIndices</code>는 내부적으로 레이아웃 계산과 위치 보정 과정을 수행하기 때문에 스크롤 중 추가 비용이 발생할 수 있었다.</p>
<hr>
<h2 id="세-번째-시도">세 번째 시도</h2>
<h2 id="최종-구현-방식">최종 구현 방식</h2>
<p>결국 FlashList에서 제공하는 스크롤 이벤트를 활용해 현재 스크롤 위치를 직접 추적하는 방식으로 구현했다.</p>
<p>스크롤 중 전달받는 현재 Y축 값을 기준으로 Header의 위치를 비교하고,
특정 시점부터 Header를 <code>show / hide</code> 하도록 구성했다.</p>
<p>실제 구현에서는 Header의 <code>opacity</code> 값을 조정해
스크롤 위치에 따라 자연스럽게 나타나고 사라지는 형태로 처리했다.</p>
<p>이를 통해 Sticky Header를 실제로 고정시키지 않더라도,
사용자 입장에서는 자연스러운 Sticky UI처럼 느껴지도록 구현할 수 있었다.</p>
<p>무엇보다 레이아웃 재계산을 최소화하면서 JS → UI 업데이트 비용도 줄일 수 있었고,
결과적으로 성능과 UX 모두 만족할 수 있는 방향으로 마무리할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[cache로 웹사이트 성능을 최적화하기]]></title>
            <link>https://velog.io/@leave_a_comment/cache%EB%A1%9C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5%EC%9D%84-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leave_a_comment/cache%EB%A1%9C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5%EC%9D%84-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 08 Jan 2025 12:18:27 GMT</pubDate>
            <description><![CDATA[<h2 id="캐시란-무엇인가">캐시란 무엇인가?</h2>
<ul>
<li>캐싱은 자주 사용되는 데이터를 한 번 받아온 후에 그 데이터를 임시 저장소에 저장하여 동일한 데이터를 빠르게 불러와서 사용하는 기법을 말한다.</li>
<li>메모리 계층 구조에서 캐시는 디스크나 메인 메모리보다 더 빠르게 데이터를 불러와 사용해야 할 때 쓰인다.</li>
</ul>
<h2 id="캐싱의-종류">캐싱의 종류?</h2>
<ul>
<li>캐싱에는 여러 종류가 존재한다.</li>
</ul>
<h3 id="1-브라우저-캐시">1) 브라우저 캐시</h3>
<ul>
<li><p>변화가 적은 데이터라면 캐싱을 적용해 볼 수 있다. HTML, CSS, JavaScript, 이미지 등 웹 자원을 로컬 디스크에 저장해둔다. 이는 로컬 저장소에 캐시되기 때문에 단일 사용자를 대상으로 하며, 해당 사용자의 정보만을 저장한다.
ex) Etag와 Cache-Control은 브라우저 캐시와 관련된 HTTP 헤더로, 웹 브라우저가 서버에서 자원을 어떻게 캐시할지를 결정하는 데 사용된다.</p>
</li>
<li><p>HTTP 헤더의 <strong>ETag</strong>는 특정 버전의 리소스를 식별한다. 클라이언트는 데이터를 최초로 받은 이후 동일한 리소스를 요청할 때, 이전에 받은 Etag 값을 <strong>If-None-Match</strong> 헤더에 포함하여 요청을 보낸다.</p>
</li>
<li><p><em>If-Modified-Since*</em> 캐시된 리소스의 Last-Modified 값 이후에 서버 리소스가 수정되었는지 확인합니다.
서버는 이 Etag와 리소스의 현재 Etag를 비교하여, 리소스가 변경되지 않았다면 304 Not Modified 상태 코드와 함께 빈 응답을 보내고, 리소스가 변경되었으면 새로운 리소스를 반환한다.</p>
</li>
<li><p>HTTP 헤더의 Cache-Control은 클라이언트(브라우저)와 서버 간의 캐시 동작을 제어한다. </p>
<blockquote>
<p><strong>no-cache</strong>: 서버에서 데이터를 확인해야 하며, 이전에 캐시된 데이터를 사용하지 않도록 합니다.</p>
</blockquote>
</li>
<li><p><em>no-store*</em>: 데이터가 캐시되지 않도록 합니다.</p>
</li>
<li><p><em>max-age*</em>: 리소스가 캐시된 후 최대 몇 초 동안 유효한지 지정합니다.</p>
</li>
<li><p><em>public*</em>: 리소스가 모든 캐시에 저장될 수 있음을 의미합니다.</p>
</li>
<li><p><em>private*</em>: 리소스는 사용자별로 개인적인 캐시에 저장됩니다.</p>
</li>
<li><p><em>must-revalidate*</em>: 캐시된 리소스가 만료되었을 경우, 서버에서 리소스를 다시 확인해야 함을 의미합니다.</p>
</li>
<li><p><em>s-maxage*</em>: 중간 서버에서만 적용되는 max-age 값을 설정하기 위해 사용합니다. 예를 들어, Cache-Control 값을 s-maxage=31536000, max-age=0 과 같이 설정하면 CDN에서는 1년동안 캐시되지만 브라우저에서는 매번 재검증 요청을 보내도록 설정할 수 있습니다.</p>
</li>
</ul>
<pre><code>no-cache 속성은 캐시를 먼저 사용하기 이전에 서버에 해당 캐시를 사용해도 되는지에 관해 검증 요청을 보내는 속성이다. 
no-cache 속성이 없는 경우 캐시가 있다면 바로 캐시를 쓰지만(max-age=0), no-cache 속성이 있는 경우 캐시를 바로 쓰지 않고 서버에 이 캐시를 사용해도 되는지에 대한 허락을 맡기 때문에 요청에 대한 시간이 소요될 수 있다.

no-store는 개인정보 등 private 한 데이터가 있는 경우 이 속성을 사용할 수 있다. (캐시를 만들어서 저장조차 하지 않는다.)
private cache에 반대되는 shared cache 가 존재한다.

Cache-Control max-age 값 대신 Expires 헤더로 캐시 만료 시간을 정확히 지정할 수도 있습니다.</code></pre><ul>
<li><strong>Cache-Control</strong>은 리소스를 <strong>어떻게</strong> 캐시할지에 대한 지침을 제공하며, <strong>Etag</strong>는 리소스의 <strong>변경 여부</strong>를 확인하는 데 사용된다.  </li>
</ul>
<hr>
<h3 id="2-프록시-캐시">2) 프록시 캐시</h3>
<ul>
<li>공유 캐시(shared cache)로 한 명 이상의 사용자에 의해 재사용되는 응답을 저장한다. no-cache는 최신화가 필요한 stale 데이터를 사용하고 싶을 때 사용하고, 이러한 데이터를 사용해서 문제가 발생한다면 504 에러를 반환하는 must-revalidate를 사용하면 된다.</li>
</ul>
<hr>
<h3 id="3-cdn-캐시">3) CDN 캐시</h3>
<ul>
<li>CDN은 성능 향상을 위해 클라이언트의 요청이 같은 서버로 가는 것을 막는다. CDN 캐시는 웹 자원의 정적 파일을 캐시하여 빠른 응답을 제공한다.<blockquote>
<p>일반적으로 캐시를 없애기 위해서 CDN Invalidation을 수행한다고 한다. CDN에 저장되어 있는 캐시를 삭제한다는 뜻이다. 브라우저의 캐시는 다른 곳에 위치하기 때문에 CDN 캐시를 삭제한다고 해서 브라우저 캐시가 삭제되지는 않는다. </p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="데이터-페칭-라이브러리에서는-캐싱을-어떻게-처리할까">데이터 페칭 라이브러리에서는 캐싱을 어떻게 처리할까?</h2>
<h4 id="react-query">React Query</h4>
<ul>
<li>서버에서 받은 데이터를 클라이언트의 로컬 메모리나 로컬 스토리지, 또는 IndexedDB에 저장하여 다음에 동일한 요청이 있을 경우 네트워크 요청을 생략하고 캐시된 데이터를 반환할 수 있습니다.</li>
<li>staleTime을 설정하여 데이터가 &quot;stale&quot; 상태(즉, 오래된 상태)가 되기까지의 시간을 지정할 수 있고, cacheTime을 통해 캐시된 데이터를 얼마나 오래 보존할지 설정할 수 있습니다.</li>
</ul>
<blockquote>
<p><strong>staleTime</strong>: 데이터를 얼마나 오래 캐시할지 지정합니다. 이 시간이 지나면 데이터는 &quot;stale&quot; 상태가 됩니다.
<strong>cacheTime</strong>: 캐시된 데이터를 언제까지 저장할지 지정합니다. 이 시간이 지나면 캐시된 데이터는 삭제됩니다.
<strong>refetchOnWindowFocus</strong>: 브라우저 창이 포커스를 받을 때 데이터를 자동으로 재페칭할지 여부를 설정합니다.</p>
</blockquote>
<p>ref) <a href="https://yozm.wishket.com/magazine/detail/2341/">https://yozm.wishket.com/magazine/detail/2341/</a>
ref) <a href="https://toss.tech/article/smart-web-service-cache">https://toss.tech/article/smart-web-service-cache</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 기타 등등 Hook]]></title>
            <link>https://velog.io/@leave_a_comment/React-%EA%B8%B0%ED%83%80-%EB%93%B1%EB%93%B1-Hook</link>
            <guid>https://velog.io/@leave_a_comment/React-%EA%B8%B0%ED%83%80-%EB%93%B1%EB%93%B1-Hook</guid>
            <pubDate>Fri, 29 Nov 2024 06:30:57 GMT</pubDate>
            <description><![CDATA[<h3 id="리액트-핵심-훅을-제외한-유용한-훅들-소개">리액트 핵심 훅을 제외한 유용한 훅들 소개</h3>
<h4 id="-useimperativehandle">* useImperativeHandle</h4>
<ul>
<li>부모 컴포넌트가 자식 컴포넌트의 특정 기능에 접근하도록 허용. 일반적으로 ref와 함께 사용되며, forwardRef가 필요함. 부모가 자식 컴포넌트의 특정 기능(예: focus, scroll)을 직접 호출.</li>
</ul>
<p>[<strong>사용 예시</strong>]</p>
<pre><code>const FancyInput = React.forwardRef((props, ref) =&gt; {
  const inputRef = React.useRef();
  React.useImperativeHandle(ref, () =&gt; ({
    focus: () =&gt; {
      inputRef.current.focus();
    },
  }));
  return &lt;input ref={inputRef} /&gt;;
});

function Parent() {
  const ref = React.useRef();

  return (
    &lt;div&gt;
      &lt;FancyInput ref={ref} /&gt;
      &lt;button onClick={() =&gt; ref.current.focus()}&gt;포커스&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre><h4 id="-uselayouteffect">* useLayoutEffect</h4>
<ul>
<li>DOM이 렌더링된 직후, 브라우저가 그리기 전에 동기적으로 실행. DOM 변경 후 바로 실행이 필요한 경우에 적합하다. useEffect와 비슷하지만 실행 시점이 다르다. </li>
</ul>
<p>[<strong>사용 시기</strong>]</p>
<p>DOM 요소의 위치, 크기 조정이 필요할 때.
애니메이션 효과나 스크롤 위치 설정.</p>
<p>[<strong>차이점</strong>]</p>
<p>useEffect: 렌더링 이후 실행 (<strong>비동기</strong>).
useLayoutEffect: 렌더링 직전에 실행 (<strong>동기</strong>).</p>
<pre><code>function LayoutEffectExample() {
  const divRef = React.useRef();

  React.useLayoutEffect(() =&gt; {
    console.log(divRef.current.offsetHeight);
  }, []);

  return &lt;div ref={divRef}&gt;레이아웃 확인&lt;/div&gt;;
}
</code></pre><h4 id="-usetransition">* useTransition</h4>
<ul>
<li><p>상태 업데이트를 긴급/비긴급으로 나누어 처리한다. 사용자 인터페이스가 부드럽게 유지. React 18의 Concurrent Mode에서 유용.</p>
<pre><code>function App() {
const [isPending, startTransition] = React.useTransition();
const [count, setCount] = React.useState(0);

const handleClick = () =&gt; {
  startTransition(() =&gt; {
    setCount((c) =&gt; c + 1);
  });
};

return (
  &lt;div&gt;
    &lt;button onClick={handleClick}&gt;클릭&lt;/button&gt;
    {isPending ? &lt;p&gt;로딩 중...&lt;/p&gt; : &lt;p&gt;결과: {count}&lt;/p&gt;}
  &lt;/div&gt;
);
}
</code></pre></li>
</ul>
<pre><code>#### * useId
- 고유한 ID를 생성해서 HTML에 사용한다. 나는 폼에 적용했었다. 컴포넌트간 충돌이 없어 유용하다.</code></pre><p>function Component() {
  const id = React.useId();</p>
<p>  return (
    <label htmlFor={id}>
      입력창
      <input id={id} />
    </label>
  );
}</p>
<pre><code>#### * useDefferedValue
- 부하가 큰 연산의 업데이트를 지연시켜 UI 성능 개선. UI가 느려지지 않도록 함.</code></pre><p>function App({ value }) {
  const deferredValue = React.useDeferredValue(value);</p>
<p>  return <div>{deferredValue}</div>;
}</p>
<pre><code>

#### * useActionState
- 비동기 액션의 상태를 더 명확하고 직관적으로 관리할 수 있다. (기존 useFormState 훅을 개선했다.)</code></pre><p>&quot;use client&quot;;</p>
<p>function App() {
    const [state, submitAction, isPending] = useActionState(customAction, initialState); // initialState like {error: null}</p>
<pre><code>return (
    &lt;form action={submitAction}&gt;
    &lt;button disabled={isPending}&gt;&lt;/button&gt;
    {state.error &amp;&amp; &lt;span&gt;error&lt;/span&gt;}
    &lt;/form&gt;
)</code></pre><p>}</p>
<p>```
리액트 훅이므로 클라이언트 컴포넌트에서 사용해야 한다. pending 상태와 useState, onSubmit 함수를 축약해 개인적으로 가독성과 데이터 응집도가 높아졌다고 생각한다.</p>
<blockquote>
<p>“form, input, button 요소에 action 및 formAction props로 Server Action을 전달하면 JavaScript가 비활성화되었거나 코드가 로드되기 전에도 사용자가 폼을 제출하고, 오류를 표시할 수 있게 되었다.”</p>
</blockquote>
<p>JavaScript가 비활성화되거나 코드가 로드되기 전에 사용자가 form을 제출할 수 있으면 뭐가 좋은걸까?</p>
<p>사용자의 인터넷 연결이 느리거나, 장치 성능이 낮거나, JavaScript가 비활성화된 환경에서도 폼 제출 기능이 정상적으로 작동하도록 보장해줄 수 있다. 이로써 웹사이트의 접근성을 높여 모든 사용자에게 일관된 경험을 제공할 수 있게 되고, 결과적으로 사용자가 겪을 수 있는 잠재적인 문제를 줄이고 더 많은 사용자가 서비스를 이용할 수 있게 하는 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redux 도입기]]></title>
            <link>https://velog.io/@leave_a_comment/Redux-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@leave_a_comment/Redux-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Wed, 11 Sep 2024 10:08:59 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ul>
<li>리액트 상태 관리 라이브러리들 (feat. 장단점)</li>
<li>상태 관리에 Redux를 선택하게 된 계기</li>
<li>Redux 셋팅하기</li>
<li>Next.js에 Redux를 적용하며 마주하게 된 사사로운 오류사항들</li>
<li>번외</li>
</ul>
<p><br><br></p>
<hr>
<p><br><br></p>
<h2 id="🛠️-리액트-상태-관리-라이브러리들-feat-장단점">🛠️ 리액트 상태 관리 라이브러리들 (feat. 장단점)</h2>
<br>

<p>리액트 상태 관리 라이브러리에는 다양한 종류가 있다. 익숙한 것들만 간단히 나열해보자면 Recoil, Redux, Jotai, Mobx, zustand 등등 ...</p>
<p>우리가 사용할 기술을 선정할 때 고려해야 할 것이 몇가지 있다. 각 기술의 특성과 장단점을 무조건 파악을 해야 한다는 거다. </p>
<blockquote>
<p>제일 주의해야하는 건 무작정 &#39;남들이 다 사용하니까&#39; 선택하는 일이다. </p>
</blockquote>
<p>그렇게 되면 내가 만들 프로젝트와 그 기술의 성격이 맞지 않을 수가 있고, 몸집이 커진 프로젝트를 다시 싹 다 갈아 엎어야 하는 대참사가 발생할 수도 있다. </p>
<br>
<br>


<h2 id="상태관리에-redux를-선택하게-된-계기">상태관리에 Redux를 선택하게 된 계기</h2>
<br>


<p>위와 같은 다양한 라이브러리 중 나는 zustand 또는 Redux 사이에서 고민하게 되었다.</p>
<br>

<p>나의 상황을 적어보자면</p>
<p>1) 온전히 혼자 작업을 해야 한다.
2) 최대한 빠르게 작업을 마무리 하면 좋다.
3) 두 라이브러리 모두 사용해 본 경험이 없거나 적다.</p>
<br>




<p>위 같은 상황에서 빠르고 쉽게 작업할 수 있는 zustand 를 선택하는게 더 효율적이었겠지만, 나는 redux를 선택했다!</p>
<br>




<p>왜냐면 redux는 대규모 프로젝트에 더 적합하기 때문이다. (나는 런칭해서 운영까지, 즉 유지보수까지 해보고 싶었기 때문이다.)
그리고 &#39;쉬운걸 먼저 적용해버리면 나중에 더 큰 어려움을 마주하게 되지 않을까? 그러니까 어려운 것부터 먼저 깊게 다뤄보자!&#39; 싶은 마음이었다.</p>
<br>

<p>따라서 redux를 next.js(App Route) + typescript 에 적용하게 되는데 ,,, </p>
<br>
<br>


<h2 id="redux-셋팅하기">Redux 셋팅하기</h2>
<br>



<p>자 차근차근 적용해보도록 하자.</p>
<br>


<p>우선, redux와 관련 패키지를 설치해야 한다.</p>
<p><br><br></p>
<p><strong>Redux 설치</strong>
<br><br></p>
<pre><code>npm install @reduxjs/toolkit react-redux
</code></pre><p><br><br></p>
<p><strong>Redux 설정</strong>
<br><br></p>
<p>일단, store를 만든다. 이때 store는 redux의 모든 상태들을 관리하게 된다. 
<br></p>
<pre><code>// store.ts

import { configureStore } from &quot;@reduxjs/toolkit&quot;;
import rootReducer from &quot;./rootReducer&quot;;

export const makeStore = () =&gt; {
  return configureStore({
    reducer: rootReducer,
  });
};


export type AppStore = ReturnType&lt;typeof makeStore&gt;;
export type RootState = ReturnType&lt;AppStore[&quot;getState&quot;]&gt;;
export type AppDispatch = AppStore[&quot;dispatch&quot;];
</code></pre><p><br><br></p>
<p><strong>slice 생성</strong>
<br></p>
<p>slice는 액션 및 리듀서를 포함한다.
<br></p>
<pre><code>import { createSlice } from &quot;@reduxjs/toolkit&quot;;

const userAuthSlices = createSlice({
  name: &quot;userAuth&quot;, // slice 식별
  initialState: { name: &quot;subeen&quot; },
  reducers: {
    setUserAuth: (state, action) =&gt; {
      state.name = action.payload;
    },
  },
});

export const { setUserAuth } = userAuthSlices.actions;

export default userAuthSlices.reducer;
</code></pre><br>

<p>이때, name에 따라 slice가 식별되니 직관적이게 작명하도록 유의한다. </p>
<p><br><br></p>
<p><strong>Provider 설정</strong>
<br></p>
<p>Provider를 설정해 모든 페이지에서 Redux 상태에 접근할 수 있도록 한다. </p>
<p>Next.js App Router 구조에서는 app/layout.tsx 파일에서 설정한다. </p>
<br>

<pre><code>&quot;use client&quot;;

import React from &quot;react&quot;;
import { Provider } from &quot;react-redux&quot;;
import { makeStore } from &quot;./lib/store&quot;;

const store = makeStore(); // makeStore 함수를 호출하여 스토어 객체를 생성합니다.

const ClientLayout: React.FC&lt;{ children: React.ReactNode }&gt; = ({
  children,
}) =&gt; {
  return (
    &lt;Provider store={store}&gt;
      {/* store 객체를 Provider에 전달합니다. */}
      &lt;main&gt;{children}&lt;/main&gt;
    &lt;/Provider&gt;
  );
};

export default ClientLayout;
</code></pre><pre><code>import Footer from &quot;@/components/Footer&quot;;
import Header from &quot;@/components/Header&quot;;
import type { Metadata, Viewport } from &quot;next&quot;;
import { Inter } from &quot;next/font/google&quot;;
import ClientLayout from &quot;./ClientLayout&quot;;
import &quot;./globals.css&quot;;

const inter = Inter({ subsets: [&quot;latin&quot;] });

export const metadata: Metadata = {
  title: &quot;Create Next App&quot;,
  description: &quot;Generated by create next app&quot;,
};

export const viewport: Viewport = {
  initialScale: 1,
  width: &quot;device-width&quot;,
};

const RootLayout = ({
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;) =&gt; {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body className={inter.className}&gt;
        &lt;ClientLayout&gt;
          &lt;Header /&gt;
          {children}
        &lt;/ClientLayout&gt;
        &lt;Footer /&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
};

export default RootLayout;
</code></pre><br>


<p>위의 코드를 보면 바로 Provider로 감싸는 대신 ClientLayout으로 감쌌는데, 이는 Redux가 클라이언트 컴포넌트에서만 작동하는 훅을 사용해야하기 때문이다. (useSelector, useDispatch)</p>
<br>


<p><em>여기서 잠깐</em></p>
<br>

<p>Redux는 클라이언트 사이드 상태 관리 (브라우저 환경에서 동작하는 상태 관리 도구) 라이브러리이기 때문에 &quot;use client&quot; 지시어가 필요하다. </p>
<p>(&quot;use client&quot; 지시어는 초기 페이지 로딩 속도나 사용자 경험에 부정적인 영향을 미칠 가능성이 있다. 번들 크기가 커져 페이지 로드 시간이 증가하는 단점도 있다.)</p>
<p><br><br></p>
<p>보통 그래서 next-redux-wrapper 라이브러리를 사용한다. </p>
<br>


<p><br><br></p>
<p><strong>Redux 사용 예시</strong></p>
<br>

<p>컴포넌트에서 redux 상태와 액션 사용 방법이다. 
useSelector와 useDispatch 훅을 이용하여 상태를 조회하고, 액션을 디스패치 할 수 있다. </p>
<br>

<pre><code>// app/login/page.tsx
&#39;use client&#39;;

import { useState } from &#39;react&#39;;
import { useDispatch } from &#39;react-redux&#39;;
import { login } from &#39;../../features/userSlice&#39;;
import { useRouter } from &#39;next/navigation&#39;;

const LoginPage = () =&gt; {
  const [email, setEmail] = useState(&#39;&#39;);
  const [password, setPassword] = useState(&#39;&#39;);
  const dispatch = useDispatch();
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) =&gt; {
    e.preventDefault();

    // 실제로는 백엔드에서 로그인 API 호출
    const userData = {
      id: &#39;123&#39;,
      name: &#39;John Doe&#39;,
      email: email,
    };

    // 로그인 성공 시 Redux에 사용자 정보를 저장
    dispatch(login(userData));

    // 대시보드로 리디렉션
    router.push(&#39;/dashboard&#39;);
  };

  return (
    &lt;form onSubmit={handleLogin}&gt;
      &lt;div&gt;
        &lt;label&gt;Email&lt;/label&gt;
        &lt;input
          type=&quot;email&quot;
          value={email}
          onChange={(e) =&gt; setEmail(e.target.value)}
          required
        /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;label&gt;Password&lt;/label&gt;
        &lt;input
          type=&quot;password&quot;
          value={password}
          onChange={(e) =&gt; setPassword(e.target.value)}
          required
        /&gt;
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot;&gt;Login&lt;/button&gt;
    &lt;/form&gt;
  );
};

export default LoginPage;
</code></pre><br>






<br>
<br>

<h2 id="nextjs에-redux를-적용하며-마주하게-된-사사로운-오류사항들">Next.js에 Redux를 적용하며 마주하게 된 사사로운 오류사항들</h2>
<br>



<p><strong>가장 많이 언급되는 불편함: 새로고침 하면 날라가요 ,,,</strong>
<br></p>
<p>이를 해결하기 위한 몇가지 방법이 있다. </p>
<br>



<p>아. 일단 왜 이런 문제가 발생하냐면, 브라우저를 새로고침 하면 페이지 전체를 다시 로드하기 때문에 Redux 상태가 초기화된다.</p>
<br>




<p><strong>첫번째 해결방법) redux-persist 라이브러리 사용</strong></p>
<br>
간편해서 많이 사용하는 방법!
<br><br>

<p>간략한 사용법 설명</p>
<br>
1. 설치

<pre><code>npm install redux-persist
</code></pre><br>

<ol start="2">
<li>스토어 설정<pre><code>// lib/store.ts
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import { persistStore, persistReducer } from &#39;redux-persist&#39;;
import storage from &#39;redux-persist/lib/storage&#39;; // 기본적으로 localStorage 사용
import rootReducer from &#39;./rootReducer&#39;; // 모든 리듀서를 합친 rootReducer
</code></pre></li>
</ol>
<p>const persistConfig = {
  key: &#39;root&#39;,
  storage, // localStorage에 저장
};</p>
<p>const persistedReducer = persistReducer(persistConfig, rootReducer);</p>
<p>export const store = configureStore({
  reducer: persistedReducer,
});</p>
<p>export const persistor = persistStore(store);</p>
<pre><code>

&lt;br&gt;

3. Provider 설정
&lt;br&gt;</code></pre><p>// app/layout.tsx
&#39;use client&#39;;</p>
<p>import &#39;./globals.css&#39;;
import { Provider } from &#39;react-redux&#39;;
import { store, persistor } from &#39;../lib/store&#39;;
import { PersistGate } from &#39;redux-persist/integration/react&#39;;
import Header from &#39;../components/Header&#39;;</p>
<p>export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Provider store={store}>
          <PersistGate loading={null} persistor={persistor}>
            <Header />
            <main>{children}</main>
          </PersistGate>
        </Provider>
      </body>
    </html>
  );
}</p>
<pre><code>
&lt;br&gt;


redux-persist의 PersistGate를 사용하여, 스토어가 복원되기 전까지 앱을 지연시킬 수 있다.

&lt;br&gt;



만약 세션 중에만 상태를 유지하고 싶다면 redux-persist/lib/storage/session을 사용할 수 있다.

&lt;br&gt;


__단점__
- 스토리지의 크기가 제한적이고, 복잡한 상태 관리에는 다소 비효율적일 수 있습니다.
상태가 커질수록 브라우저 로딩 시간이 길어질 수 있음.








&lt;br&gt;&lt;br&gt;

**두번째 해결방법) localStorage나 sessionStorage 직접 사용**
&lt;br&gt;


1. store가 업데이트 될 때마다 localStorage에 상태 저장
&lt;br&gt;</code></pre><p>// lib/store.ts
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import rootReducer from &#39;./rootReducer&#39;;</p>
<p>const loadState = () =&gt; {
  try {
    const serializedState = localStorage.getItem(&#39;reduxState&#39;);
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (e) {
    console.error(&#39;Could not load state&#39;, e);
    return undefined;
  }
};</p>
<p>const saveState = (state: RootState) =&gt; {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem(&#39;reduxState&#39;, serializedState);
  } catch (e) {
    console.error(&#39;Could not save state&#39;, e);
  }
};</p>
<p>const preloadedState = loadState();</p>
<p>const store = configureStore({
  reducer: rootReducer,
  preloadedState, // 저장된 상태를 미리 로드
});</p>
<p>store.subscribe(() =&gt; {
  saveState(store.getState());
});</p>
<p>export default store;</p>
<pre><code>
&lt;br&gt;


__장점__
- redux-persist보다 가볍고, 직접 제어 가능.
- 상태가 단순한 경우 빠르게 적용 가능.
&lt;br&gt;
__단점__
- 수동으로 상태 직렬화/역직렬화를 관리해야 하므로 코드 복잡도가 약간 증가.
- 상태가 클 경우 저장 성능에 영향을 줄 수 있음.






&lt;br&gt;
&lt;br&gt;


**세번째 해결방법) 쿠키 사용**

&lt;br&gt;

쿠키를 사용하여 상태를 저장할 수 있지만, 쿠키는 브라우저가 서버에 전송하는 용도로 주로 사용되므로 큰 데이터를 저장하는 데는 부적합하다. 그러나 인증 정보나 작은 상태값을 저장하는 데에는 적합.

&lt;br&gt;

</code></pre><p>import { parseCookies, setCookie } from &#39;nookies&#39;; // nookies 라이브러리 사용
import { configureStore } from &#39;@reduxjs/toolkit&#39;;
import rootReducer from &#39;./rootReducer&#39;;</p>
<p>const loadStateFromCookies = () =&gt; {
  const cookies = parseCookies();
  return cookies.reduxState ? JSON.parse(cookies.reduxState) : undefined;
};</p>
<p>const saveStateToCookies = (state: RootState) =&gt; {
  setCookie(null, &#39;reduxState&#39;, JSON.stringify(state), {
    maxAge: 30 * 24 * 60 * 60,
    path: &#39;/&#39;,
  });
};</p>
<p>const preloadedState = loadStateFromCookies();</p>
<p>const store = configureStore({
  reducer: rootReducer,
  preloadedState,
});</p>
<p>store.subscribe(() =&gt; {
  saveStateToCookies(store.getState());
});</p>
<p>export default store;</p>
<pre><code>&lt;br&gt;



__장점__
- 인증 정보나 작은 상태 저장에 적합.
- 서버 사이드 렌더링(SSR)과 쉽게 연동 가능.
&lt;br&gt;
__단점__
- 쿠키는 용량 제한이 있으므로 큰 데이터를 저장할 수 없음.
- 보안 이슈가 있을 수 있으며, 민감한 정보는 적절히 암호화 필요.





&lt;br&gt;&lt;br&gt;





**네번째 해결방법) 서버 사이드에서 상태 저장**

&lt;br&gt;

Next.js의 API Routes를 활용하여 서버에 상태를 저장하고, 필요할 때 API를 통해 불러오는 방법.


&lt;br&gt;

</code></pre><p>// pages/api/saveState.ts
import type { NextApiRequest, NextApiResponse } from &#39;next&#39;;</p>
<p>export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === &#39;POST&#39;) {
    // 상태를 데이터베이스나 파일 시스템에 저장
    // 예: db.save(req.body)
    res.status(200).json({ message: &#39;State saved&#39; });
  } else {
    res.status(405).json({ message: &#39;Method not allowed&#39; });
  }
}</p>
<pre><code>


&lt;br&gt;

새로고침 시 상태가 서버로부터 복원될 수 있도록 useEffect로 API를 호출하여 상태를 불러오고, 저장할 때는 API로 상태를 보낸다. 

&lt;br&gt;


__장점__
- 큰 데이터를 저장하거나, 보안이 필요한 상태 관리에 적합.
- 서버에서 관리되므로 클라이언트 측의 저장 용량 한계를 걱정할 필요 없음.
&lt;br&gt;
__단점__
- 상태 저장 및 불러오는 로직이 복잡해질 수 있음.
- 상태 저장/복원 시 네트워크 지연이 발생할 수 있음.


&lt;br&gt;



&lt;br&gt;

**나는 위 방법들 중에 localStorage 직접 설정을 택했다.**

&lt;br&gt;

redux-persist 는 나중에 사용해보도록 하자 ~

&lt;br&gt;&lt;br&gt;




#### localStorage에 직접 설정하기
&lt;br&gt;
</code></pre><p>import { configureStore } from &quot;@reduxjs/toolkit&quot;;
import rootReducer from &quot;./rootReducer&quot;;</p>
<p>const loadState = () =&gt; {
  try {
    const serializedState = localStorage.getItem(&quot;reduxState&quot;);
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (e) {
    console.log(&quot;Could not load state&quot;, e);
    return undefined;
  }
};</p>
<p>const saveState = (state: RootState) =&gt; {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem(&quot;reduxState&quot;, serializedState);
  } catch (e) {
    console.error(&quot;Could not save state&quot;, e);
  }
};</p>
<p>const preloadedState = loadState();</p>
<p>export const makeStore = () =&gt; {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
  });
};</p>
<p>makeStore.subscribe(() =&gt; {
    saveState(makeStore.getState());
  });</p>
<p>export type AppStore = ReturnType<typeof store>;</p>
<p>export type RootState = ReturnType&lt;AppStore[&quot;getState&quot;]&gt;;
export type AppDispatch = AppStore[&quot;dispatch&quot;];</p>
<pre><code>&lt;br&gt;

이런식으로 작성했더니 타입 오류가 났다.

&lt;br&gt;


makeStore 함수에 직접적으로 subscribe 메서드를 호출하려고 했기 때문이다. 

makeStore 함수는 store를 반환하는 함수이지 store 자체가 아니다.

(makeStore 함수가 호출된 후에 반환된 store 인스턴스를 사용해야 함)



&lt;br&gt;




코드를 수정해준 후, 디버깅을 해보았는데 store를 참조하고 있지 않았다.

(그래서 자꾸 데이터가 보존되지 않고 날아감)


&lt;br&gt;


각 store는 name에 따라 식별되고 있는데, localStorage에서 가져온 이 데이터가 어느 store에 대한 데이터인지 매칭이 안되고 있었다. 



&lt;br&gt;

따라서 아래와 같이 수정해주었다. 
</code></pre><p>import { configureStore } from &quot;@reduxjs/toolkit&quot;;
import rootReducer from &quot;./rootReducer&quot;;</p>
<p>const loadState = () =&gt; {
  try {
    const serializedState = localStorage.getItem(&quot;reduxState&quot;);
    if (serializedState === null) {
      return undefined;
    }</p>
<pre><code>const parsedState = JSON.parse(serializedState);

// 로컬 스토리지의 상태를 rootReducer의 구조에 맞게 변환
return {
  userAuth: parsedState, // userAuth로 감싸기
};</code></pre><p>  } catch (e) {
    console.log(&quot;Could not load state&quot;, e);
    return undefined;
  }
};</p>
<p>const saveState = (state: RootState) =&gt; {
  try {
    const serializedState = JSON.stringify(state.userAuth); // userAuth만 저장
    localStorage.setItem(&quot;reduxState&quot;, serializedState);
  } catch (e) {
    console.log(&quot;Could not save state&quot;, e);
    return undefined;
  }
};</p>
<p>const preloadedState = loadState();</p>
<p>export const makeStore = () =&gt; {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
  });
};</p>
<p>// Create store instance
const store = makeStore();</p>
<p>// 아래와 같이 하면 타입 오류가 발생
// const store =  configureStore({
//  reducer: rootReducer,
//  preloadedState
// })</p>
<p>// Subscribe to store updates and save state to localStorage
store.subscribe(() =&gt; {
  const state = store.getState();
  saveState(state);
});</p>
<p>export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType&lt;AppStore[&quot;getState&quot;]&gt;;
export type AppDispatch = AppStore[&quot;dispatch&quot;];</p>
<pre><code>

&lt;br&gt;&lt;br&gt;


이상으로 Redux 적용기 1탄을 마무리해보도록 하겠다. 




&lt;br&gt;&lt;br&gt;


~~velog 예쁘고 가독성 있게 작성하는거 왜케 어려워 ...~~












</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[잊지 않기 위한 기록]]></title>
            <link>https://velog.io/@leave_a_comment/%EC%9E%8A%EC%A7%80-%EC%95%8A%EA%B8%B0-%EC%9C%84%ED%95%9C-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@leave_a_comment/%EC%9E%8A%EC%A7%80-%EC%95%8A%EA%B8%B0-%EC%9C%84%ED%95%9C-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Tue, 09 Jul 2024 03:34:23 GMT</pubDate>
            <description><![CDATA[<h4 id="도커"><strong>도커</strong></h4>
<ul>
<li>도커는 컨테이너 기반의 가상화 플랫폼.</li>
<li>vm ware는 무겁기 때문에 docker를 많이 사용한다.</li>
<li>서비스 운영에 필요한 서버 프로그램, 코드 및 라이브러리, 컴파일된 실행 파일 등을 묶는 형태를 도커 이미지라고 한다. 즉 컨테이너 생성(실행)에 필요한 모든 파일과 환경을 가진 것.
ex) 우분투 이미지는 우분투를 실행하기 위한 모든 파일과 dependency 들을 포함한다.</li>
<li>도커 이미지를 실행하면 도커 컨테이너가 된다고 볼 수 있다.</li>
</ul>
<h4 id="서버"><strong>서버</strong></h4>
<ul>
<li>netstat
현재 서버가 다른 시스템과 어떤 서비스/포트로 연결되어 있는지 확인하는 명령어</li>
<li>netstat -an
연결되었던/연결을 기다리는 목록을 아이피 주소로 바꾸어서 보여줌</li>
<li>서버 주소/hello-world 로 서버가 죽어있는지 확인한다.</li>
</ul>
<h4 id="graphql"><strong>graphQL</strong></h4>
<ul>
<li>유연하게 확장하기 위해 사용</li>
<li>Restful API 는 형태와 데이터 요청 방법이 연결되어 있으나, graphQL은 완전히 분리되어 있음.</li>
<li>REST는 리소스 형태와 크기를 서버에서 결정하나 graphQL은 클라이언트 단에서 요청시 결정한다.</li>
<li>REST는 여러 리소스에 접근 시 여러 번 요청을 하나 graphQL은 한번의 요청을 한다.</li>
</ul>
<h4 id="react"><strong>React</strong></h4>
<ul>
<li>React DOM은 JSX에 삽입된 모든 값을 렌더링하기 전에 이스케이프 하므로, 애플리케이션에서 명시적으로 작성되지 않은 내용은 주입되지 않는다. 모든 항목은 렌더링 되기 전에 문자열로 변환된다. 이런 특성으로 인해 XSS 공격을 방지할 수 있다.</li>
<li>strictMode에서 useEffect의 double call 특성 : 개발시에만 활성화 되고 실제 프로덕션 빌드 할 때는 알아서 꺼진다. 이 모드에서는 컴포넌트를 2번씩 렌더링해서 deprecated된 생명주기 메서드의 사용, 예상치 못한 부작용, 레거시 문자열 ref 등을 확인하는 용도로 사용된다. 개발 과정에서 잠재적인 문제를 조기에 발견하고 수정하는 용도로 사용된다.</li>
</ul>
<h4 id="웹-성능-최적화"><strong>웹 성능 최적화</strong></h4>
<ul>
<li>이미지 최적화</li>
<li>리소스 압축</li>
<li>렌더링 최적화</li>
<li>CDN 사용</li>
<li>캐시 설정</li>
</ul>
<h4 id="각-언어별-프레임워크"><strong>각 언어별 프레임워크</strong></h4>
<ul>
<li>Python: Django, Flask</li>
<li>Node.js: Express.js, Nest.js</li>
<li>Java: Spring, Spring Boot</li>
<li>Ruby: Ruby on Rail</li>
<li>Golang: Gin</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React와 Next 배포 (1)]]></title>
            <link>https://velog.io/@leave_a_comment/React%EC%99%80-Next-%EB%B0%B0%ED%8F%AC-1</link>
            <guid>https://velog.io/@leave_a_comment/React%EC%99%80-Next-%EB%B0%B0%ED%8F%AC-1</guid>
            <pubDate>Thu, 22 Feb 2024 13:08:12 GMT</pubDate>
            <description><![CDATA[<p>블로그가 다 만들어지면 CI/CD 자동화를 구현해보고자 한다.</p>
<p>React는 정적 사이트 배포(bundle.js로 빈 html트리에 채워넣는다.)이지만 Next는 SSR 방식이기 때문에 React와 Next 배포 방식에는 차이가 있다.</p>
<p>그래서 Next를 사용할 때에는 Docker+S3+EC2와 같은 환경에서의 배포를 사용하고 있다. (Github + Jenkins + S3)</p>
<p>Vercel은 서버리스 방식이라고 하여 Vercel로 내가 만든 블로그를 배포해보려고 한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next로 블로그 구현하기 (1)]]></title>
            <link>https://velog.io/@leave_a_comment/Next%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@leave_a_comment/Next%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Wed, 21 Feb 2024 13:35:09 GMT</pubDate>
            <description><![CDATA[<p>오늘 To Do list</p>
<ul>
<li>markdown 에 필요한 라이브러리 서칭 및 설정</li>
<li>html로 markdown하는 기능의 api 작성</li>
<li>포스트 불러오는 api 작성</li>
</ul>
<p>서버리스 이므로 md파일로 post를 저장해놓은 다음에 post내용을 읽어온다.
join 함수로 파일 확장자까지 붙여 정확한 경로를 만든다.
fs 모듈의 readdirSync 메소드 사용하여 경로에 있는 파일명들을 받아온다.</p>
<ul>
<li>fs 모듈은 파일 시스템에 접근하고 파일과 폴더를 다룰 수 있는 기능을 제공하는 내장 모듈입니다. fs 모듈은 Node.js 환경에서 사용되며, 브라우저에서는 사용할 수 없습니다.</li>
<li>path 모듈은 파일 경로와 관련된 유틸리티 기능을 제공하는 내장 모듈입니다. path 모듈은 파일 경로를 조작하고 파싱하는 데 사용됩니다. 
markdown 언어를 해석해주는 matter 함수를 사용한다.</li>
<li>Gray-matter는 JavaScript 기반의 라이브러리로, Markdown 파일과 YAML, TOML, JSON 등의 프론트매터(front matter)를 파싱하는 기능을 제공합니다. 프론트매터는 문서의 메타데이터를 포함하는 머리말 부분으로, 일반적으로 YAML, TOML 또는 JSON 형식으로 작성됩니다.</li>
</ul>
<p>dangerouslySetInnerHTML로 React에서 직접 html을 설정할 수 있다.</p>
<ul>
<li>신뢰할 수 없는 소스에서 가져온 HTML 문자열은 보안상 위험이 있을 수 있기 때문이다.
따라서 이 속성을 사용할 때에는 신뢰할 수 있는 소스로부터의 HTML 문자열만 사용해야 하며, 공격을 방지하기 위해 적절한 XSS 방어 메커니즘을 구현하는 것이 좋다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[RSC (React Server Compoment)]]></title>
            <link>https://velog.io/@leave_a_comment/RSC-React-Server-Compoment</link>
            <guid>https://velog.io/@leave_a_comment/RSC-React-Server-Compoment</guid>
            <pubDate>Tue, 12 Sep 2023 12:02:42 GMT</pubDate>
            <description><![CDATA[<p>리액트 서버 컴포넌트는 react 18에 나온 아주 강력한 기능 중 하나이다.</p>
<p><a href="https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components">https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components</a></p>
<p>Server side rendering을 상호보완해주는 기술인데, Next.js 서버 컴포넌트도 RSC에 기반한다고 한다.</p>
<p>Next에서 권장하는 방법론 !</p>
<ol>
<li><p>server component를 이용하여 서버측에서 데이터를 fetching 합니다.</p>
</li>
<li><p>data fetching을 병렬적으로 수행하는것이 좋습니다.</p>
</li>
<li><p>layout 및 page의 경우 데이터를 사용하는 곳에서 데이터를 가져오는것이 좋습니다.</p>
</li>
<li><p>Suspense, Loading UI를 이용해 점진적으로 렌더링을 수행하는것이 좋습니다.</p>
</li>
</ol>
<p>revalidate는 뭐지 ?? 하다가 ISR과 관련된거라고 알게되었따
특정한 간격마다 캐시된 데이터를 재검증하기 위해 리소스의 캐시 수명을 설정하는 것이다.</p>
<p><a href="https://velog.io/@seungchan__y/NextJS%EC%99%80-ISR">https://velog.io/@seungchan__y/NextJS%EC%99%80-ISR</a></p>
<p>리액트 쿼리를 SSR 환경에서 사용하기 위해서</p>
<p>공식문서에서 추천하는 방법이 있습니다.</p>
<p>두가지 방법이 존재하는데</p>
<p>initialData 속성을 이용한 방법과</p>
<p>Hydration을 이용한 방법이 있습니다.</p>
<p>튜토리얼은 요기</p>
<p><a href="https://codevoweb.com/setup-react-query-in-nextjs-13-app-directory/">https://codevoweb.com/setup-react-query-in-nextjs-13-app-directory/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next는 url 직접 접근이 안돼요..]]></title>
            <link>https://velog.io/@leave_a_comment/Next%EB%8A%94-url-%EC%A7%81%EC%A0%91-%EC%A0%91%EA%B7%BC%EC%9D%B4-%EC%95%88%EB%8F%BC%EC%9A%94</link>
            <guid>https://velog.io/@leave_a_comment/Next%EB%8A%94-url-%EC%A7%81%EC%A0%91-%EC%A0%91%EA%B7%BC%EC%9D%B4-%EC%95%88%EB%8F%BC%EC%9A%94</guid>
            <pubDate>Tue, 05 Sep 2023 12:49:30 GMT</pubDate>
            <description><![CDATA[<p>url로 데이터를 전달할 때 시크릿창에서 직접 접근하면 데이터가 없다.</p>
<p>이를 해결하기 위해선 초기화된 배열로 예외 처리를 해주거나  -&gt; 클라이언트 처리)</p>
<p>getServerSideProps를 통해 pre-rendering 되도록 한다. -&gt; 서버에서 처리 (SEO최적화가 필요하거나, 유저에게 로딩을 보여주고 싶지 않으면)</p>
<p>컴포넌트 내부에서 router를 사용하면, 이는 클라이언트 사이드(프론트)에서만 실행된다.</p>
<p>이 주제 얘기하다보니까 s3에서 직접접근할때 index.html 파일 처리해줘야하는 문제점이 생각났다 ....... </p>
<p>배포할때마다 html 확장자를 제거해줘야하는데, 만들어지는 html 파일이 많아질수록 번거로워지니 이 부분도 처리를 미리미리 해놓아야겠다! </p>
<p>(함수를 추가하면 된다던데...)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[getServerSideProps vs getStaticProps 알고가기]]></title>
            <link>https://velog.io/@leave_a_comment/getServerSideProps-vs-getStaticProps-%EC%95%8C%EA%B3%A0%EA%B0%80%EA%B8%B0</link>
            <guid>https://velog.io/@leave_a_comment/getServerSideProps-vs-getStaticProps-%EC%95%8C%EA%B3%A0%EA%B0%80%EA%B8%B0</guid>
            <pubDate>Tue, 05 Sep 2023 12:26:53 GMT</pubDate>
            <description><![CDATA[<h3 id="getserversideprops-vs-getstaticprops">getServerSideProps vs getStaticProps</h3>
<p>getStaticProps =&gt; SSG
getServerSideProps =&gt; SSR</p>
<p>SSG는 빌드된 시점에 fetch를 하고, SSR은 매 요청시마다 refetch를 한다는 차이점이 있다. SSR은 SSG보다는 효율이 떨어지지만 언제든지 내용을 수정가능하다.</p>
<p>url 내 쿼리를 활용하여 api 요청 후 해당 데이터를 props로 넘겨줄 경우 getServerSideProps를 사용한다.</p>
<p>getServerSideProps를 사용하면 Context Object가 파라미터로 해당 함수에 들어오는데, key로는 아래와 같은게 있다. (함수는 클라이언트가 아닌 서버에서 돌아간다.)</p>
<blockquote>
</blockquote>
<p>params: If this page uses a dynamic route, params contains the route parameters. If the page name is [id].js , then - params will look like { id: ... }.
req: The HTTP IncomingMessage object.
res: The HTTP response object.
query: An object representing the query string.
preview: preview is true if the page is in the Preview Mode and false otherwise.
previewData: The preview data set by setPreviewData.
resolvedUrl: A normalized version of the request URL that strips the _next/data prefix for client transitions and - includes original query values.</p>
<p>예제 코드</p>
<pre><code>export const getServerSideProps: GetServerSideProps = async (context) =&gt; {
  try {
    const { menuId } = context.query;
    const response = await axios.get&lt;CategoryType[]&gt;(
      &quot;http://localhost:8080/category/get/menu&quot;,
      {
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
          &quot;Access-Control-Allow-Origin&quot;: &quot;*&quot;,
        },
        params: {
          menuId: menuId,
        },
      }
    );
    const data = response.data;
    console.log(data);
    return {
      props: {
        CategoryData: data,
      },
    };
  } catch (err) {
    console.log(err);
    return {
      props: {},
    };
  }
};</code></pre><p>참고 레퍼런스 : <a href="https://velog.io/@hhhminme/Next.js%EC%97%90%EC%84%9C-SSR%EB%A1%9C-url-query-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0feat.-typescript">https://velog.io/@hhhminme/Next.js%EC%97%90%EC%84%9C-SSR%EB%A1%9C-url-query-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0feat.-typescript</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA['JavaScript heap out of memory' 에러 핸들링]]></title>
            <link>https://velog.io/@leave_a_comment/JavaScript-heap-out-of-memory-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-ntqxgp5y</link>
            <guid>https://velog.io/@leave_a_comment/JavaScript-heap-out-of-memory-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-ntqxgp5y</guid>
            <pubDate>Tue, 05 Sep 2023 11:43:58 GMT</pubDate>
            <description><![CDATA[<p>xcode랑 안드로이드 스튜디오 설치하고 나니까 잘만 되던 빌드가 잘 안되더라 ,,, 상사분께 여쭤보니까 위 프로그램 설치하고는 무관하다는데 그럼 내가 마지막 빌드 이후에 설치한 라이브러리 때문일까 싶다 ..!</p>
<p>JavaScript heap out of memory 에러를 뱉어냈는데,
검색해보니 해결방법으로는</p>
<p>맥 사용 유저는 </p>
<pre><code>export NODE_OPTIONS=&quot;--max-old-space-size=8192&quot;</code></pre><p>를 입력하고 빌드를 하면 된다 ! 
주의) 저 숫자 아무거나 치면 안됨,,</p>
<p>그런데 빌드할때마다 매번 타이핑을 해주는건 번거로우니 package.json 파일에 cross-env 처리를 하면 된다.</p>
<p>공용 브랜치에 함부로 package.json 파일을 건드는건 위험하니 일단 할때마다 타이핑 해주고 있다 ㅠ.ㅠ</p>
<p>&#39;cross-env를 보니 스크립트뿐만 아니라 환경변수 및 여러 가지 설정값들을 js 파일로 들고 있다가 적용하는 방법도 있는 것 같던데&#39; 라고 다른 블로그에서 보았다 ! 이 부분도 나중에 알아봐야지 !!!!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[server component, client component를 알고가자 !]]></title>
            <link>https://velog.io/@leave_a_comment/server-component-client-component%EB%A5%BC-%EC%95%8C%EA%B3%A0%EA%B0%80%EC%9E%90</link>
            <guid>https://velog.io/@leave_a_comment/server-component-client-component%EB%A5%BC-%EC%95%8C%EA%B3%A0%EA%B0%80%EC%9E%90</guid>
            <pubDate>Thu, 31 Aug 2023 13:06:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="server-component와-client-component가-무엇일까---">Server component와 Client component가 무엇일까 ...  ?</h3>
</blockquote>
<p>Server  side rendering이 핫해지며 서버 컴포넌트와 클라이언트 컴포넌트 개념을 자주 접하게 되었다 !</p>
<p>나는 이런 개념들 정확히 아는걸 좋아하니까 정리해보자 <del>~</del></p>
<p>일반적으로 리액트에선 모든게 클라이언트 단에서 이루어진다. (CSR) </p>
<p>next는 클라이언트 단에서 모든 것을 부담하지 않아도 되게끔 개선했다!
보안을 강화시켜주고, 번들링 될 자바스크립트 양도 줄여주고, data fetching과 렌더링을 동일한 환경에서 수행할 수 있는 점 등등 장점이 꽤나 많은 것 같다ㅏ 
그런데 또 단점은 이렇게 서버 단에서 생성된 html에 결국 클라이언트 단에서 hydrate 작업을 진행해줘야 한다는 것이다. 즉 추가적인 자바스크립트 코드가 필요하다는 소리 ...!</p>
<p>서버사이드 렌더링은 정적으로도, 동적으로도 구현할 수 있다고 한다.</p>
<p>SSG: 빌드된 시점에 데이터 fetch
SSR: 매 요청시마다 데이터 refetch (할때마다 서버사이드에서 페이지 생성)</p>
<blockquote>
<p> 정적(static): 서버 단에서 서버 컴포넌트와 클라이언트 컴포넌트 모두 빌드 시에 미리 렌더링될 수 있다. 일단 요청에 따른 응답 결과를 캐싱해두고, 뒤이은 요청에는 그 캐싱해둔 결과를 재사용하는 방식이다. SSG, ISR 방식이 이에 해당한다.
동적(dynamic): 서버 컴포넌트와 클라이언트 컴포넌트가 매 요청 시 렌더링되며, 응답 결과는 캐싱되지 않는다. SSR 방식이 이에 해당한다.</p>
</blockquote>
<ul>
<li>data fetching이 필요한 경우 👉 서버 컴포넌트</li>
<li>백엔드 자원에 접근해야 하는 경우 👉 서버 컴포넌트</li>
<li>클라이언트에 드러내면 안 되는 민감한 정보가 있을 때 👉 서버 컴포넌트</li>
<li>자바스크립트 코드를 줄여야 할 때 👉 서버 컴포넌트</li>
<li>click, change 리스너 등을 사용하여 대화형(상호작용) 컨텐츠를 구현하려는 경우 👉 클라이언트 컴포넌트</li>
<li>&#39;상태(state)&#39;을 활용하는 경우 👉 클라이언트 컴포넌트</li>
<li>브라우저 상에서만 지원하는 API(예: local storage와 같은 웹 스토리지를 다루는 API)를 사용하는 경우 👉 클라이언트 컴포넌트</li>
</ul>
<p>Next.js에 따르면 가능한 경우에는 서버 컴포넌트로 만들고 따로 클라이언트 컴포넌트로 구현이 필요한 부분들만 추출하는 편이 좋다고 한다.</p>
<h3 id="1-server-component-란-">1. server component 란 ?</h3>
<p>- </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 12, 13]]></title>
            <link>https://velog.io/@leave_a_comment/Next.js-12-13</link>
            <guid>https://velog.io/@leave_a_comment/Next.js-12-13</guid>
            <pubDate>Wed, 30 Aug 2023 12:48:42 GMT</pubDate>
            <description><![CDATA[<p>요즘 Next.js에 관심이 많아졌는데, next12 버전의 페이지 라우터와 next13의 앱 라우터 모두 찍먹해보았다 !</p>
<p>12버전에서 13버전으로 릴리즈되면서 폴더 구조가 많이 바뀌었는데, 처음에 아주 헷갈렸다 ! (이 부분도 추후에 작성해놓아야겠다.)</p>
<p>또 12버전의 getServerSideProps와 getStaticProps 함수도 완벽하게 정복 못한채로 13버전을 맞이하게 되었다 .......</p>
<p>13버전에서는 사용하지 않는다고 하던데, 그래도 정확한 개념은 알고 넘어가야 할 것 같아서 기록해 놓으려 한다 !</p>
<p>next13에서는 data fetching을 할 때 저 두 함수 대신  fetch API 를 사용하면 된다고 한다. </p>
<pre><code>// 직접 무효화 하기 전까지는 이 request는 캐싱됨.
// `getStaticProps`와 비슷! (즉, 빌드 시점에 fetch)
// `force-cache`가 디폴트 값이므로 생략 가능
fetch(URL, { cache: &#39;force-cache&#39; });

// 매번 요청 때마다 refetch 됨.
// `getServerSideProps`와 비슷!
fetch(URL, { cache: &#39;no-store&#39; });

// 이 request는 10초동안 캐싱됨.
// This request should be cached with a lifetime of 10 seconds.
// `revalidate` 옵션을 지정한 `getStaticProps`와 비슷!
fetch(URL, { next: { revalidate: 10 } });</code></pre><p>훨씬 직관적이어진 것 같다.
또 공식 문서에서 서버 컴포넌트니, 클라이언트 컴포넌트니 구분하여 작성하는걸 권장한다고 하던데 이 부분도 추후에 개념 정리를 해놓아야겠다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React query 알고 사용하자 !! (1) - 장점, 필요한 상황]]></title>
            <link>https://velog.io/@leave_a_comment/React-query-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90-1-%EC%9E%A5%EC%A0%90-%ED%95%84%EC%9A%94%ED%95%9C-%EC%83%81%ED%99%A9</link>
            <guid>https://velog.io/@leave_a_comment/React-query-%EC%95%8C%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90-1-%EC%9E%A5%EC%A0%90-%ED%95%84%EC%9A%94%ED%95%9C-%EC%83%81%ED%99%A9</guid>
            <pubDate>Wed, 30 Aug 2023 12:25:38 GMT</pubDate>
            <description><![CDATA[<p>요즘 핫하다는 스택들,,중 React query가 빠지지 않는 것 같아 오늘은 React query에 대해 간단하게 포스팅하려고 한다!</p>
<p>무지성으로 사용하기 전에, React query의 장단점을 알고 사용하는게 좋겠다 !</p>
<h2 id="react-query란-">React query란 ?</h2>
<ul>
<li>공식 문서에선 <strong>fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리</strong> 라고 한다 .</li>
</ul>
<h3 id="--특장점">- 특장점</h3>
<p><strong>데이터를 캐싱</strong>한다.
캐싱이란 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것을 말한다.</p>
<p>같은 내용을 반복적으로 불러오는 불필요한 호출을 방지하여 서버에 대한 부하를 줄이는 것이다 !!</p>
<p>즉 필요한 상황에 적절하게 데이터를 갱신할 수 있다는 소리다 !
_(최신 데이터인지(fresh) 기존 데이터(stale)인지도 판단할 수 있다) _</p>
<h3 id="--그래서-언제-사용하냐-">- 그래서 언제 사용하냐 ???</h3>
<p>다른 블로그 글을 보면 일반적인 data fetching 솔루션에선 필요하지 않을 수 있다고 한다. 더 까다로운 사용사례를 구현할 때, 예를 들면 무한 스크롤 ??? 유용하다고 한다 :)</p>
]]></description>
        </item>
    </channel>
</rss>