<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>히옹스 벨로그</title>
        <link>https://velog.io/</link>
        <description>프론트엔드 공부하는 개발자입니다.</description>
        <lastBuildDate>Mon, 22 Sep 2025 16:28:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>히옹스 벨로그</title>
            <url>https://velog.velcdn.com/images/hee0ne_2/profile/ef2f26aa-084b-43b1-9f19-2c6006fea261/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 히옹스 벨로그. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hee0ne_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[10주차 회고] 코드 관점의 성능 최적화]]></title>
            <link>https://velog.io/@hee0ne_2/10%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EC%BD%94%EB%93%9C-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@hee0ne_2/10%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EC%BD%94%EB%93%9C-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 22 Sep 2025 16:28:54 GMT</pubDate>
            <description><![CDATA[<h1 id="내가-배운-점들📚">내가 배운 점들📚</h1>
<h2 id="메모이제이션은-도미노-같은-존재다">메모이제이션은 도미노 같은 존재다</h2>
<p>코치님이 말씀해주신 &quot;메모이제이션을 한 번 적용하기 시작하면 연달아 적용해야 한다&quot;는 말이 정말 와닿았다. React.memo를 컴포넌트에 적용하면, 그 컴포넌트로 전달되는 props들도 모두 메모이제이션해야 한다는 뜻이다.</p>
<pre><code class="language-javascript">const ParentComponent = () =&gt; {
  // 이 데이터가 변하지 않으면 재계산하지 않음
  const expensiveData = useMemo(() =&gt; {
    return heavyCalculation();
  }, [dependency]);

  // 이 함수도 매번 새로 만들어지지 않도록 메모이제이션
  const handleClick = useCallback(() =&gt; {
    doSomething();
  }, []);

  return &lt;MemoizedChild data={expensiveData} onClick={handleClick} /&gt;;
};</code></pre>
<p>처음에는 왜 이렇게 복잡하게 해야 하나 싶었는데, 직접 해보니 이해가 됐다. 하나의 컴포넌트를 최적화하려면 그와 연결된 모든 부분을 함께 고려해야 한다는 것이다.</p>
<h2 id="usereducer의-숨겨진-장점">useReducer의 숨겨진 장점</h2>
<p>복잡한 상태 관리에서 useReducer를 사용해보니 예상치 못한 장점을 발견했다. 바로 테스트하기가 훨씬 쉽다는 점이다. reducer 함수는 순수 함수라서 입력만 주면 항상 같은 결과가 나온다. 각각의 액션에 대해서도 독립적으로 테스트할 수 있어서 디버깅할 때도 편했다.</p>
<p>상태가 3-4개 이상 연결되어 있을 때는 useState보다 useReducer가 훨씬 관리하기 편한 것 같다. 특히 여러 필드가 동시에 업데이트되어야 하는 경우에는 더욱 그렇다.</p>
<h2 id="깨달은-점">깨달은 점</h2>
<h3 id="추측하지-말고-측정하라">추측하지 말고 측정하라</h3>
<p>성능 최적화의 첫 번째 원칙은 &#39;측정&#39;이라는 것을 뼈저리게 느꼈다. 내 생각에 느려 보이는 부분과 실제로 느린 부분이 다를 수 있다는 걸 여러 번 경험했다.</p>
<p>React DevTools의 Profiler를 사용해서 실제 렌더링 시간을 측정하고, Performance API로 함수 실행 시간을 체크하면서 정확한 데이터를 바탕으로 최적화 방향을 정할 수 있었다. &quot;이 부분이 문제인 것 같다&quot;가 아니라 &quot;이 부분이 17ms 걸리므로 최적화가 필요하다&quot;고 말할 수 있게 됐다.</p>
<h3 id="context-api는-양날의-검">Context API는 양날의 검</h3>
<p>ScheduleContext를 만들면서 전역 상태 관리의 딜레마를 직접 체험했다. Context를 사용하면 props drilling 문제는 해결되지만, 그 Context를 사용하는 컴포넌트가 많아질수록 상태가 바뀔 때마다 더 많은 컴포넌트가 리렌더링된다.</p>
<p>코치님이 말씀하신 것처럼 Context API는 디자인 시스템이나 특정 컴포넌트 트리 안에서 데이터를 공유할 때 유용하다. 하지만 진짜 &quot;전역&quot; 상태로 사용할 때는 신중하게 생각해야 한다는 걸 깨달았다.</p>
<h3 id="사용자-관점에서-생각하기">사용자 관점에서 생각하기</h3>
<p>성능 최적화를 하면서 자꾸 개발자 관점에서만 생각하고 있다는 걸 깨달았다. &quot;렌더링이 몇 ms 줄어들었다&quot;보다는 &quot;사용자가 버튼을 눌렀을 때 얼마나 빨리 반응하는가&quot;가 더 중요하다.</p>
<p>API 호출이 실패했을 때도 &quot;콘솔에 에러가 찍혔으니까 됐다&quot;가 아니라 &quot;사용자에게 어떤 메시지를 보여줄까&quot;, &quot;다시 시도할 수 있게 해줄까&quot;를 고민해야 한다는 걸 느꼈다.</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<h3 id="타입스크립트를-제대로-활용하지-못했다">타입스크립트를 제대로 활용하지 못했다</h3>
<p>TypeScript를 사용했으면서도 그 장점을 충분히 살리지 못했다. <code>Lecture</code> 타입을 여러 파일에 중복으로 정의한다거나, 유니온 타입이나 타입 가드를 적극적으로 활용하지 못했다.</p>
<p>런타임에서 발생할 수 있는 오류들을 컴파일 타임에 잡을 수 있는 기회를 많이 놓쳤다는 생각이 든다. 타입 시스템을 단순히 &#39;변수의 타입을 명시하는 것&#39; 정도로만 생각했던 것 같다.</p>
<h3 id="에러-상황을-너무-간단하게-생각했다">에러 상황을 너무 간단하게 생각했다</h3>
<p>API 호출이 실패했을 때나 예외 상황이 발생했을 때, 개발자 입장에서만 생각했다. 콘솔에 에러 로그만 남기고 끝내는 게 아니라, 사용자에게 의미 있는 피드백을 주거나 복구할 수 있는 방법을 제시했어야 한다.</p>
<p>&quot;네트워크 오류가 발생했습니다&quot;보다는 &quot;잠시 후 다시 시도해주세요&quot; 같은 구체적이고 행동 가능한 메시지를 보여줬으면 더 좋았을 것 같다.</p>
<h2 id="다음-목표">다음 목표</h2>
<h3 id="접근성도-함께-고려하는-개발자가-되고-싶다">접근성도 함께 고려하는 개발자가 되고 싶다</h3>
<p>성능과 접근성을 모두 고려할 수 있는 개발자가 되고 싶다. 
빠르기만 한 웹사이트가 아니라, 모든 사람이 편하게 사용할 수 있으면서도 빠른 웹사이트를 만들고 싶다.</p>
<h3 id="팀-전체의-성능-문화를-만들어보고-싶다">팀 전체의 성능 문화를 만들어보고 싶다</h3>
<p>나 혼자만 성능을 신경 쓰는 게 아니라, 팀 전체가 성능을 고려하며 개발할 수 있는 문화를 만들어보고 싶다. 성능 예산을 정하고, 코드 리뷰할 때도 성능 관점을 포함하고, CI/CD에서 성능 회귀를 자동으로 감지하는 시스템을 만들어보고 싶다.</p>
<hr>
<p>이번 프로젝트는 단순히 기능을 만드는 것을 넘어서 &#39;좋은 소프트웨어란 무엇인가&#39;에 대해 고민해볼 수 있는 시간이었다. 빠른 것도 중요하지만, 모든 사용자가 접근할 수 있고, 안정적이며, 지속 가능한 소프트웨어를 만드는 것이 진짜 목표라는 걸 깨달았다.</p>
<p>아직 갈 길이 멀지만, 이런 고민을 할 수 있게 된 것만으로도 큰 성장이라고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[9주차 회고] 성능 최적화(SSR, SSG, Hydration)]]></title>
            <link>https://velog.io/@hee0ne_2/9%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94SSR-SSG-Hydration</link>
            <guid>https://velog.io/@hee0ne_2/9%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94SSR-SSG-Hydration</guid>
            <pubDate>Thu, 18 Sep 2025 08:07:52 GMT</pubDate>
            <description><![CDATA[<h1 id="ssr-ssg-hydration-개념-정리">SSR, SSG, Hydration 개념 정리</h1>
<p><a href='https://velog.io/@hee0ne_2/SSR-SSG-CSR-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0'>[SSR, SSG, CSR 이해하기]</a></p>
<h2 id="ssr-server-side-rendering">SSR (Server-Side Rendering)</h2>
<p><strong>정의</strong>: 서버에서 HTML을 미리 생성하여 클라이언트에 전달하는 렌더링 방식</p>
<p><strong>특징</strong>:</p>
<ul>
<li>서버에서 완전한 HTML을 생성하여 전송</li>
<li>초기 로딩 속도가 빠름 (FCP 개선)</li>
<li>SEO에 유리</li>
<li>서버 부하가 높음</li>
</ul>
<p><strong>언제 사용하는가</strong>:</p>
<ul>
<li>SEO가 중요한 페이지 (블로그, 뉴스, 상품 페이지)</li>
<li>초기 로딩 속도가 중요한 경우</li>
<li>소셜 미디어 공유 시 메타데이터가 필요한 경우</li>
</ul>
<p><strong>구현 예시</strong>:</p>
<pre><code class="language-javascript">// packages/vanilla/server.js
app.use(&quot;*all&quot;, async (req, res) =&gt; {
  const rendered = await render(url, req.query);
  const html = template
    .replace(`&lt;!--app-head--&gt;`, rendered.head ?? &quot;&quot;)
    .replace(`&lt;!--app-html--&gt;`, rendered.html ?? &quot;&quot;)
    .replace(
      `&lt;/head&gt;`,
      `&lt;script&gt;window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData || {})};&lt;/script&gt;&lt;/head&gt;`,
    );
  res.status(200).set({ &quot;Content-Type&quot;: &quot;text/html&quot; }).send(html);
});</code></pre>
<h2 id="ssg-static-site-generation">SSG (Static Site Generation)</h2>
<p><strong>정의</strong>: 빌드 타임에 미리 정적 HTML 파일들을 생성하는 렌더링 방식</p>
<p><strong>특징</strong>:</p>
<ul>
<li>빌드 시점에 모든 페이지를 미리 생성</li>
<li>CDN 캐싱에 최적화</li>
<li>서버 부하가 거의 없음</li>
<li>동적 데이터 업데이트가 어려움</li>
</ul>
<p><strong>언제 사용하는가</strong>:</p>
<ul>
<li>콘텐츠가 자주 변경되지 않는 사이트 (블로그, 문서, 랜딩 페이지)</li>
<li>대량의 페이지가 필요한 경우</li>
<li>최고의 성능이 필요한 경우</li>
</ul>
<p><strong>구현 예시</strong>:</p>
<pre><code class="language-javascript">// packages/vanilla/static-site-generate.js
const { products } = await getProducts();
generateStaticSite(&quot;/&quot;, {});
generateStaticSite(&quot;/404&quot;, {});
for (let i = 0; i &lt; products.length; i++) {
  generateStaticSite(`/product/${products[i].productId}`, {});
}</code></pre>
<h2 id="hydration">Hydration</h2>
<p><strong>정의</strong>: 서버에서 렌더링된 정적 HTML을 클라이언트에서 인터랙티브하게 만드는 과정</p>
<p><strong>왜 사용하는가</strong>:</p>
<ul>
<li>서버 렌더링의 SEO/성능 이점 + 클라이언트의 인터랙티브 기능 결합</li>
<li>초기 로딩 후 JavaScript가 로드되어 이벤트 핸들러 등록</li>
<li>사용자 경험 향상</li>
</ul>
<p><strong>효과</strong>:</p>
<ul>
<li><strong>초기 로딩 속도 개선</strong>: 서버에서 완성된 HTML을 받아 즉시 표시</li>
<li><strong>SEO 최적화</strong>: 검색 엔진이 완전한 HTML을 크롤링 가능</li>
<li><strong>사용자 경험 향상</strong>: 로딩 후 즉시 인터랙션 가능</li>
<li><strong>메타데이터 지원</strong>: 소셜 공유 시 올바른 미리보기 표시</li>
</ul>
<p><strong>구현 예시</strong>:</p>
<pre><code class="language-javascript">// 서버에서 초기 데이터 주입
window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData || {})};

// 클라이언트에서 상태 복원
if (window.__INITIAL_DATA__) {
  productStore.setState(window.__INITIAL_DATA__);
}</code></pre>
<h1 id="react-ssrssg-직접-구현해보며-느낀-점들">React SSR/SSG 직접 구현해보며 느낀 점들</h1>
<p>Next.js 없이 React SSR/SSG를 처음부터 만들어보는 프로젝트를 진행했다. 처음에는 &quot;그냥 <code>renderToString</code> 쓰면 끝이겠지?&quot;라고 생각했는데, 막상 구현해보니 생각보다 복잡하고 고려할 것들이 너무 많았다. 이번 회고를 통해 내가 배운 것들과 아직 부족한 부분들을 정리해보려고 한다.</p>
<p><strong>TMI</strong>) 처음에 어떻게 구현해보지?? 했을 때 지훈님이 SSR 구현은 PHP 랑 똑같다고 보면 된다구 하셨다. SSG도 첨에 겁먹었는데, 해보니 너무 별거 없었다. (오히려 더 쉬웠음!)</p>
<h2 id="🤔-처음-마주친-문제들">🤔 처음 마주친 문제들</h2>
<h3 id="서버에-window가-없다">서버에 window가 없다!</h3>
<p>가장 기본적인 실수였는데, 클라이언트에서 잘 되던 코드를 서버에서 실행하니까 <code>window is not defined</code> 에러가 계속 났다. 당연히 서버에는 브라우저가 없으니까 window 객체가 없는건데, 이거에 대한 분기처리를 계속 넣어줬다.</p>
<pre><code class="language-javascript">// 이런 식으로 환경을 체크해야 했다
if (typeof window !== &#39;undefined&#39;) {
  // 브라우저에서만 실행되는 코드
  window.addEventListener(&#39;scroll&#39;, handleScroll);
} else {
  // 서버에서만 실행되는 코드
  console.log(&#39;서버에서 렌더링 중...&#39;);
}</code></pre>
<p>나는 이 경험을 통해 <strong>Universal JavaScript</strong>라는게 단순히 같은 코드를 공유하는 게 아니라, 환경에 따라 다르게 동작해야 한다는 걸 깨달았다.</p>
<h2 id="📚-새롭게-배운-개념들">📚 새롭게 배운 개념들</h2>
<h3 id="ssrloadmodule을-왜-사용하는가">ssrLoadModule을 왜 사용하는가?</h3>
<blockquote>
<p>ssrLoadModule은 Vite가 제공하는 편리한 기능으로, 서버에서 클라이언트용 코드를 실행할 수 있게 해주는 변환 과정을 자동화해준다. 없으면 SSR 구현이 훨씬 복잡해진다.</p>
</blockquote>
<details>
  <summary>ssrLoadModule이 필요한 이유</summary>
  <!-- 띄우기 -->

<p>1.ES 모듈을 Node.js에서 직접 실행할 수 없다</p>
<pre><code class="language-javascript">// 이렇게 하면 안 된다.
import { render } from &#39;./src/main-server.js&#39;; // ❌ 에러 발생</code></pre>
<p>Node.js는 기본적으로 ES 모듈을 직접 import할 수 없다. 특히 TypeScript 파일이나 Vite의 특별한 기능들을 사용한 파일들은 더욱 그렇다.</p>
<p>2.Vite의 변환 과정을 거쳐야 한다.
ssrLoadModule은 Vite가 파일을 다음과 같이 변환해준다</p>
<ul>
<li>TypeScript → JavaScript 변환</li>
<li>ES 모듈 → CommonJS 변환</li>
<li>Vite의 특별한 기능들 (예: import.meta.env) 처리</li>
<li>의존성 해결</li>
</ul>
<p>3.개발/프로덕션 환경 분기 처리
코드를 보면</p>
<pre><code class="language-javascript">if (!prod) {
  // 개발 환경: Vite가 실시간으로 변환
  render = (await vite.ssrLoadModule(&quot;/src/main-server.js&quot;)).render;
} else {
  // 프로덕션 환경: 미리 빌드된 파일 사용
  render = (await import(&quot;./dist/react-ssr/main-server.js&quot;)).render;
}</code></pre>
<ul>
<li>개발 환경: ssrLoadModule로 실시간 변환</li>
<li>프로덕션 환경: 이미 빌드된 파일을 직접 import</li>
</ul>
<p>4.실제 사용 예시</p>
<pre><code class="language-javascript">// packages/react/server.js에서
const { mswServer } = await vite.ssrLoadModule(&quot;./src/mocks/node.ts&quot;);
const render = (await vite.ssrLoadModule(&quot;/src/main-server.js&quot;)).render;</code></pre>
<p>이렇게 하면:</p>
<ul>
<li>TypeScript 파일(.ts)을 JavaScript로 변환</li>
<li>ES 모듈 문법을 Node.js가 이해할 수 있게 변환</li>
<li>Vite의 특별한 기능들 처리</li>
</ul>
<p>5.없다면 어떻게 해야 할까?
<code>ssrLoadModule</code>이 없다면:</p>
<ul>
<li>모든 파일을 수동으로 빌드해야 함</li>
<li>TypeScript 컴파일러를 직접 설정해야 함</li>
<li>ES 모듈을 CommonJS로 변환해야 함</li>
<li>개발 중에 파일 변경 시마다 수동으로 재빌드해야 함</li>
</ul>
</details>

<h3 id="ssr과-ssg의-명확한-차이점">SSR과 SSG의 명확한 차이점</h3>
<p>이론적으로는 알고 있었지만, 직접 구현해보니 차이가 확실히 느껴졌다.</p>
<p><strong>SSR (Server-Side Rendering)</strong>: 사용자가 페이지를 요청할 때마다 서버에서 HTML을 만든다. 실시간 데이터를 반영할 수 있지만, 매번 서버에서 계산해야 해서 느리고 서버에 부담이 된다.</p>
<p><strong>SSG (Static Site Generation)</strong>: 빌드할 때 미리 모든 HTML을 만들어둔다. </p>
<pre><code class="language-javascript">// static-site-generate.js에서 구현한 SSG
const { products } = await getProducts();
generateStaticSite(&quot;/&quot;, {});
generateStaticSite(&quot;/404&quot;, {});

// 모든 상품 페이지를 빌드 시점에 미리 생성
for (let i = 0; i &lt; products.length; i++) {
  generateStaticSite(`/product/${products[i].productId}`, {});
}</code></pre>
<p>나는 이 코드를 작성하면서 SSG의 강력함을 느꼈다. 상품이 1000개든 10000개든 빌드만 한 번 하면 CDN에서 초고속으로 서빙할 수 있으니까.</p>
<p>하지만 단점도 명확했다. 상품 정보가 바뀌면 전체를 다시 빌드해야 한다는 점이었다. 나는 이때 <strong>언제 SSR을 쓰고 언제 SSG를 써야 하는지</strong> 감이 잡혔다.</p>
<h3 id="데이터-하이드레이션hydration이-왜-필요한가">데이터 하이드레이션(Hydration)이 왜 필요한가?</h3>
<p>처음에는 하이드레이션이 뭔지 몰랐다. 서버에서 HTML을 만들었으니 그걸로 끝 아닌가? 라고 생각했다.</p>
<p>그런데 문제가 있었다. 서버에서 만든 HTML은 &quot;정적인 문서&quot;일 뿐이다. 버튼을 클릭해도 반응이 없고, 상태 변화도 없다. React의 인터랙티브한 기능들이 전혀 동작하지 않더라.</p>
<p>그래서 하이드레이션이 필요했다. 서버에서 렌더링한 HTML에 JavaScript를 &quot;주입&quot;해서 살아있는 React 앱으로 만드는 과정이었다.</p>
<pre><code class="language-javascript">// 서버에서 데이터를 클라이언트로 전달
window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData || {})};</code></pre>
<p>나는 이 과정에서 가장 중요한 것이 <strong>서버와 클라이언트의 상태를 정확히 일치시키는 것</strong>이라는 걸 배웠다. 조금이라도 다르면 React가 &quot;어? 이거 뭔가 다른데?&quot;라고 생각해서 전체 DOM을 다시 그리더라.</p>
<p>이때 <strong>FOUC(Flash of Unstyled Content)</strong> 라는 용어도 알게 되었다. 페이지가 로드될 때 스타일이 깜빡이는 현상인데, 하이드레이션을 제대로 하지 않으면 이런 문제가 생긴다.</p>
<h2 id="🏗️-아키텍처-설계하면서-배운-것들">🏗️ 아키텍처 설계하면서 배운 것들</h2>
<h3 id="하이브리드-라우팅의-필요성">하이브리드 라우팅의 필요성</h3>
<p>처음에는 &quot;모든 페이지를 SSR로 하면 되겠지&quot;라고 생각했다. 하지만 실제로는 그렇지 않았다.</p>
<ul>
<li>메인 페이지: 자주 변하니까 SSR</li>
<li>상품 페이지: 잘 안 변하니까 SSG  </li>
<li>관리자 페이지: 인증이 필요하니까 CSR</li>
</ul>
<p>나는 페이지의 특성에 따라 <strong>렌더링 방식을 다르게 선택</strong>해야 한다는 걸 깨달았다.</p>
<h3 id="라우터-아키텍처의-진화">라우터 아키텍처의 진화</h3>
<p>프로젝트를 진행하면서 라우터 구조를 여러 번 리팩토링했다. </p>
<p>처음에는 클라이언트용 라우터만 있었는데, SSR을 구현하면서 서버용 라우터가 따로 필요하다는 걸 알게 되었다. 서버에서는 브라우저의 History API를 사용할 수 없으니까.</p>
<p>그래서 <code>serverRouter.js</code>를 따로 만들었다. 202줄이나 되는 긴 코드였는데, 서버 환경에서의 라우팅 로직이 생각보다 복잡하더라.</p>
<pre><code class="language-javascript">// 서버와 클라이언트에서 다른 라우터 로직
if (typeof window === &#39;undefined&#39;) {
  // 서버용 라우터 사용
  router = new ServerRouter(routes);
} else {
  // 클라이언트용 라우터 사용  
  router = new ClientRouter(routes);
}</code></pre>
<p>나는 이 과정에서 <strong>Universal Router</strong> 패턴의 중요성을 배웠다. 가능한 한 같은 로직을 공유하되, 환경별로 다른 부분은 깔끔하게 분리하는 것이 핵심이었다.</p>
<h2 id="😅-힘들었던-부분들">😅 힘들었던 부분들</h2>
<h3 id="react-rendertostring의-한계">React renderToString의 한계</h3>
<p>가장 당황스러웠던 부분이었다. <code>renderToString</code>이 <strong>동기 함수</strong>라는 점 때문에 컴포넌트 안에서 비동기 데이터를 가져올 수 없었다.</p>
<pre><code class="language-javascript">// 이런 코드는 동작하지 않는다
function ProductPage() {
  const [product, setProduct] = useState(null);

  useEffect(() =&gt; {
    // renderToString에서는 useEffect가 실행되지 않음!
    fetchProduct().then(setProduct);
  }, []);

  return &lt;div&gt;{product?.name}&lt;/div&gt;;
}</code></pre>
<p>그래서 서버에서 미리 데이터를 준비한 후 컴포넌트에 props로 전달하는 방식을 사용해야 했다.</p>
<pre><code class="language-javascript">// 서버에서 미리 데이터 준비
const data = await fetchProductData();

// props로 전달
const html = renderToString(
  &lt;ProductProvider productStore={createProductStore(data)}&gt;
    &lt;App /&gt;
  &lt;/ProductProvider&gt;
);</code></pre>
<p>나는 이때 Next.js의 <code>getServerSideProps</code>나 <code>getStaticProps</code>가 왜 필요한지 완벽하게 이해했다.</p>
<h3 id="hydration-에러와의-전쟁">Hydration 에러와의 전쟁</h3>
<p>서버에서 렌더링한 HTML과 클라이언트에서 렌더링한 결과가 조금만 달라도 React가 화를 내더라. </p>
<p>특히 이런 코드가 문제였다:</p>
<pre><code class="language-javascript">// 서버와 클라이언트에서 다른 결과
function CurrentTime() {
  const [time, setTime] = useState(new Date().toString());
  return &lt;div&gt;{time}&lt;/div&gt;;
}</code></pre>
<p>서버에서 렌더링할 때의 시간과 클라이언트에서 하이드레이션할 때의 시간이 달라서 에러가 났다.</p>
<p>해결책은 조건부 렌더링이었다:</p>
<pre><code class="language-javascript">// 클라이언트에서만 시간 표시
function CurrentTime() {
  const [mounted, setMounted] = useState(false);

  useEffect(() =&gt; {
    setMounted(true);
  }, []);

  if (!mounted) return &lt;div&gt;Loading...&lt;/div&gt;;

  return &lt;div&gt;{new Date().toString()}&lt;/div&gt;;
}</code></pre>
<p>나는 이 경험을 통해 <strong>하이드레이션의 엄격함</strong>을 깨달았다. 서버와 클라이언트의 결과가 정확히 일치해야 한다는 것이다.</p>
<h2 id="🚀-성능-최적화에서-느낀-점들">🚀 성능 최적화에서 느낀 점들</h2>
<h3 id="빌드-시간-최적화">빌드 시간 최적화</h3>
<p>빌드 시간 최적화
상품이 많아질수록 SSG 빌드 시간이 기하급수적으로 늘어났다. 처음에는 이런 식으로 순차 처리를 했다:</p>
<pre><code class="language-javascript">// Before: 순차 처리 (엄청 느림)
await generateStaticSite(&quot;/404.html&quot;);
await generateStaticSite(&quot;/&quot;);

const { getProducts } = await vite.ssrLoadModule(&quot;./src/api/productApi.ts&quot;);
const { products } = await getProducts();

// 상품 하나씩 순차적으로 처리
for (const product of products) {
  await generateStaticSite(`/product/${product.productId}/`);
}</code></pre>
<p>상품이 100개면 100번의 순차 작업이니까 정말 오래 걸렸다. 한 페이지 생성에 0.5초씩 걸린다면 100개는 50초나 걸리는 거였다.
그래서 병렬 처리로 바꿨다:</p>
<pre><code class="language-javascript">// After: 병렬 처리 (훨씬 빠름)
await generateStaticSite(&quot;/404.html&quot;);
await generateStaticSite(&quot;/&quot;);

const { getProducts } = await vite.ssrLoadModule(&quot;./src/api/productApi.ts&quot;);
const { products } = await getProducts();

// 모든 상품 페이지를 동시에 생성
await Promise.all(
  products.map(async ({ productId }) =&gt; 
    await generateStaticSite(`/product/${productId}/`)
  )
);</code></pre>
<p>나는 이 과정에서 <strong>성능 최적화는 trade-off의 연속</strong>이라는 걸 배웠다.</p>
<h2 id="🎬-마무리하며">🎬 마무리하며</h2>
<p>Next.js나 다른 프레임워크를 사용하면 이런 복잡함들을 숨겨준다. 하지만 직접 구현해보니 <strong>왜 그런 기능들이 필요한지</strong> 몸소 체험할 수 있었다.</p>
<p>특히 이런 것들을 깨달았다:</p>
<ul>
<li><strong>환경별 분기 처리</strong>가 Universal JavaScript의 핵심이다</li>
<li><strong>서버와 클라이언트의 상태 동기화</strong>가 생각보다 까다롭다</li>
<li><strong>성능 최적화</strong>는 측정 가능한 지표로 검증해야 한다</li>
</ul>
<p>이번 경험을 통해 나는 프레임워크의 편의성에만 의존하지 않고, 내부 동작 원리를 이해하는 개발자가 되고 싶다는 생각이 들었다. </p>
<p>앞으로는 이번에 배운 내용을 바탕으로 회사 프로젝트에서 더 나은 성능 최적화를 해보고 싶다. 그리고 언젠가는 나만의 작은 프레임워크도 만들어보고 싶다는 욕심이 생겼다. 🚀</p>
<blockquote>
<p>TMI) 항상 나를 포기하지 않고 도와주는 팀원분들에게 감사하다.
8주차부터 지쳐가지고 과제를 놓을까 했었는데 8주차에는 휘린님이 직접 버그 봐주시면서 도와주셨고, 9주차에는 지훈님이 과제 시작 전 SSR 강의를 2시간 넘게 해주셨다. 덕분에 9주차에선 과제 내용을 이해 하고 내가 다른 사람들에게 다시 알려주면서 뿌듯함과 자신감이 많이 붙었다. 8주차는 지치고 힘들었다면, 9주차부터는 다시 활력이 생겨 재밌게 과제를 진행했다.
(물론 CI 통과가 안되서 밤새느라 너무 힘들었다.. 빈 커밋하면 해결된다했는데... 정확한 문제점을 몰라서 나는 테스트 코드에 타임아웃을 줘서 통과를 시켰다.)</p>
</blockquote>
<p>감사하게도 우수과제까지 받았다. vV
<img src="https://velog.velcdn.com/images/hee0ne_2/post/6393d09f-c41d-4075-9dd2-2cc240443091/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR, SSG, CSR 이해하기]]></title>
            <link>https://velog.io/@hee0ne_2/SSR-SSG-CSR-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hee0ne_2/SSR-SSG-CSR-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Sep 2025 06:08:45 GMT</pubDate>
            <description><![CDATA[<h1 id="csr-클라이언트-사이드-렌더링">CSR (클라이언트 사이드 렌더링)</h1>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/9dd75e96-036d-4e43-88c4-6a59403e7685/image.png" alt=""></p>
<p>SPA(Single Page Application) 시대를 연 대표적인 방식입니다.
처음에는 빈 HTML만 내려주고, 브라우저가 큰 JS 번들을 받아와 DOM을 생성합니다.</p>
<p>✅ 장점: 페이지 이동이 빠르고 동적인 UX 구현에 강함
❌ 단점: 초기 로딩 속도가 느리고 SEO에 약함</p>
<h2 id="csr-보완-기술">CSR 보완 기술</h2>
<p>CSR의 한계를 보완하기 위해 등장한 기법들입니다.</p>
<p>Code Splitting / Tree Shaking → JS 번들을 쪼개고 불필요한 코드 제거</p>
<p>Pre-rendering → 특정 페이지는 HTML을 미리 만들어 SEO 보완</p>
<p>하지만 이 방식만으로는 SEO와 초기 로딩 문제를 완전히 해결할 수 없었습니다.</p>
<h1 id="ssr-서버-사이드-렌더링">SSR (서버 사이드 렌더링)</h1>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/b4123f70-c3fb-41a0-bb28-258704f019de/image.png" alt=""></p>
<p>서버에서 HTML을 만들어 보내주는 방식입니다.
첫 화면을 빠르게 보여주고 SEO에도 강점이 있습니다.</p>
<p>✅ 장점: 초기 로딩 빠름, SEO 최적화 가능
❌ 단점: 페이지 이동 시 새로고침, 서버 부하 증가</p>
<h1 id="ssg-정적-사이트-생성">SSG (정적 사이트 생성)</h1>
<p>빌드 시점에 HTML을 미리 생성해두는 방식입니다. CDN에 올려두면 아주 빠릅니다.</p>
<p>✅ 장점: 속도, SEO 모두 강력 / 서버 부하 없음
❌ 단점: 데이터가 자주 바뀌는 페이지에는 부적합</p>
<p>그런데 문제는…</p>
<p>SSR이나 SSG를 쓰면 완성된 HTML을 처음부터 내려주니까, CSR의 단점(빈 화면, SEO 문제)을 극복할 수 있습니다.
하지만 여기서 새로운 문제가 생깁니다.</p>
<p>HTML은 보이는데, 이벤트가 안 먹힌다?</p>
<p>SSR/SSG로 렌더링된 페이지는 단순히 정적인 HTML일 뿐입니다.
클릭 이벤트, 상태 관리, 동적 UI 같은 JS 로직은 아직 살아있지 않음 → 이걸 연결해주는 과정이 필요합니다.</p>
<h1 id="hydration-왜-필요할까">Hydration: 왜 필요할까?</h1>
<p>여기서 등장한 개념이 <strong>Hydration(하이드레이션)</strong>입니다.</p>
<p>SSR/SSG로 미리 만든 HTML을 브라우저가 받아옴</p>
<p>이후 JS 번들을 다운로드 → 기존 HTML에 이벤트 핸들러와 상태를 주입</p>
<p>이렇게 해서 화면이 정적 HTML → 동적인 SPA로 변신</p>
<p>즉, Hydration은 HTML과 JS를 “결합”하는 과정이라고 이해하면 쉽습니다.</p>
<p>✅ Hydration의 장점</p>
<ul>
<li><p>SSR/SSG의 장점(빠른 초기 로딩, SEO)을 유지</p>
</li>
<li><p>CSR의 장점(인터랙티브 UI)도 살릴 수 있음</p>
</li>
</ul>
<p>❌ 한계</p>
<ul>
<li>Hydration 과정에서 JS 번들을 전부 다시 실행하기 때문에, 규모가 커질수록 느려짐</li>
</ul>
<p>그래서 최근에는 Partial Hydration, Streaming, Islands Architecture 같은 개선 기법들이 연구되고 있음</p>
<h1 id="언제-어떤-방식을-써야-할까">언제 어떤 방식을 써야 할까?</h1>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/c129b1a5-5165-4080-90fb-375d86496865/image.png" alt=""></p>
<p>CSR → 대시보드, 내부 툴 (SEO 중요하지 않음, 사용자 상호작용 많음)</p>
<p>SSR → 뉴스, 쇼핑몰 (빠른 초기 로딩 + SEO 필요)</p>
<p>SSG → 블로그, 문서 사이트 (변경이 적고, 빠른 서비스가 핵심)</p>
<p>SSR/SSG + Hydration → 대부분의 현대 웹앱 (SEO + UX + 동적 UI 모두 필요)</p>
<h2 id="csr-ssr-ssg-and-isr-시각화-비교">CSR, SSR, SSG and ISR 시각화 비교</h2>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/e9e7d851-539b-4c9c-bbe0-e7325223e4bd/image.png" alt=""></p>
<h1 id="정리">정리</h1>
<p>CSR은 SPA 시대를 열었지만 초기 로딩과 SEO에 약했다.</p>
<p>이를 보완하기 위해 SSR/SSG가 등장했다.</p>
<p>하지만 SSR/SSG만으로는 동적인 UI가 안 되기 때문에, Hydration이 필요해졌다.</p>
<p>요즘은 Next.js, Nuxt.js 같은 프레임워크에서 CSR + SSR/SSG + Hydration을 조합해 상황에 맞게 쓰는 것이 일반적임</p>
<h2 id="참고-사이트">참고 사이트</h2>
<p><a href='https://velog.io/@ka0son/%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%82%BC%ED%98%95%EC%A0%9C-CSR-SSR-SSG-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EA%B7%B8%EB%A0%87%EB%8B%A4%EB%A9%B4-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94%EA%B0%80'>렌더링 삼형제 CSR, SSR, SSG 이해하기</a></p>
<p><a href='https://www.elancer.co.kr/blog/detail/263'>SSR을 사용할 수 밖에 없는 이유 (Feat. CSR과의 차이)</a></p>
<p><a href='https://sonblog.vercel.app/blogs/blog/nextjs/csr-ssg-isr-ssr'>CSR, SSG, ISR, SSR</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[8주차 회고] 테스트 전략 (TDD)]]></title>
            <link>https://velog.io/@hee0ne_2/8%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-TDD</link>
            <guid>https://velog.io/@hee0ne_2/8%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-TDD</guid>
            <pubDate>Wed, 17 Sep 2025 08:32:31 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/12d4a081-f597-4b62-90a4-00e2ab430e74/image.png" alt=""></p>
<p>이번에 캘린더 애플리케이션을 구현하면서 TDD(Test-Driven Development)를 처음 제대로 경험해봤다. 그동안 테스트는 &#39;개발이 끝난 후에 하는 것&#39;이라고 생각했는데, 이번 과제를 통해 테스트가 단순히 버그를 찾는 도구가 아니라 <strong>설계를 개선하는 방법론</strong>이라는 것을 깨달았다.</p>
<h2 id="팀과-함께한-테스트-전략-선택-과정">팀과 함께한 테스트 전략 선택 과정</h2>
<h3 id="각자의-관점-다른-전략들">각자의 관점, 다른 전략들</h3>
<p>프로젝트 시작 전, 팀원들과 함께 어떤 테스트 전략을 사용할지 논의했다. 처음에는 각자 다른 접근법을 제안했다.</p>
<ul>
<li><strong>김유현</strong>: 작은 규모의 애플리케이션이니까 단위 테스트 중심의 &#39;테스트 피라미드&#39; 전략</li>
<li><strong>한아름</strong>: 사용자 시나리오 플로우가 중요하니까 통합 테스트 중심의 &#39;테스트 트로피&#39; 전략  </li>
<li><strong>신희원</strong>: 요구사항이 자주 변경되는 환경에서는 통합 테스트가 더 안정적이라고 생각해서 &#39;테스트 트로피&#39; 전략</li>
</ul>
<h3 id="합의-과정에서의-깨달음">합의 과정에서의 깨달음</h3>
<p>토론 과정에서 흥미로웠던 점은, 같은 애플리케이션을 보면서도 각자 다른 관점으로 접근했다는 것이다. 나는 처음에 &#39;작은 규모 = 단위 테스트&#39;라는 단순한 공식을 생각했는데, 다른 팀원들의 의견을 들으면서 <strong>애플리케이션의 특성</strong>을 더 깊이 생각하게 되었다.</p>
<p>결국 <strong>테스트 트로피 전략</strong>으로 합의했는데, 그 이유는:</p>
<ol>
<li><strong>사용자 중심적 사고</strong>: 캘린더 앱은 복잡한 알고리즘보다는 &#39;일정 생성→수정→삭제&#39; 같은 사용자 플로우가 핵심</li>
<li><strong>변경에 대한 대응력</strong>: 요구사항이 자주 바뀌는 환경에서 통합 테스트가 더 안정적</li>
<li><strong>실제 가치 검증</strong>: 단위 테스트로는 놓칠 수 있는 컴포넌트 간 상호작용을 검증</li>
</ol>
<p>이 합의 과정에서 배운 것은, <strong>테스트 전략은 정답이 있는 게 아니라 프로젝트의 맥락에 따라 달라진다</strong>는 점이었다.</p>
<h2 id="실제-tdd-적용-경험">실제 TDD 적용 경험</h2>
<h3 id="red-green-refactor의-실제-체험">Red-Green-Refactor의 실제 체험</h3>
<p>이론으로만 알고 있던 TDD 사이클을 실제로 경험해보니, 생각보다 어려웠다. 특히 &#39;빨간불&#39;을 보는 것에 익숙해지는 게 쉽지 않았다.</p>
<pre><code class="language-javascript">// 먼저 실패하는 테스트부터 작성
test(&#39;종료일을 지정하지 않았을 때 반복 시작일이 10월 30일 이후이면 시작 날짜에 대한 일정 하나만 생성&#39;, () =&gt; {
  // Given: 10월 30일 이후 시작 날짜
  // When: 반복 일정 생성
  // Then: 하나의 일정만 생성되어야 함
});</code></pre>
<p>처음에는 &quot;이런 것도 테스트해야 하나?&quot;라는 생각이 들었는데, 막상 구현하면서 이런 엣지 케이스들이 실제로 버그를 만들어낸다는 것을 깨달았다.</p>
<h3 id="설계를-개선하는-테스트">설계를 개선하는 테스트</h3>
<p>가장 놀라웠던 점은 <strong>테스트가 설계를 더 좋게 만든다</strong>는 것이었다. 테스트하기 어려운 코드는 보통 결합도가 높거나 책임이 명확하지 않은 코드였다. 테스트를 먼저 작성하니까 자연스럽게 다음과 같은 질문들을 하게 되었다:</p>
<ul>
<li>&quot;이 함수는 정확히 무엇을 책임져야 하는가?&quot;</li>
<li>&quot;이 컴포넌트의 의존성을 어떻게 줄일 수 있을까?&quot;</li>
<li>&quot;사용자 관점에서 이 기능은 어떻게 동작해야 하는가?&quot;</li>
</ul>
<h2 id="기술적-도전과-해결-과정">기술적 도전과 해결 과정</h2>
<h3 id="msw로-현실적인-테스트-환경-구축">MSW로 현실적인 테스트 환경 구축</h3>
<p>백엔드 없이 테스트하는 방법이 처음에는 막막했다. MSW(Mock Service Worker)를 도입하면서 실제 API와 유사한 환경을 구축할 수 있었다.</p>
<pre><code class="language-javascript">// handlers.ts에서 반복 일정 API 모킹
export const handlers = [
  http.post(&#39;/api/events/repeat&#39;, ({ request }) =&gt; {
    // 실제 API 응답과 동일한 구조로 모킹
  })
];</code></pre>
<p>이 과정에서 <strong>모킹의 철학</strong>에 대해 많이 고민했다. 너무 현실과 다르면 테스트의 의미가 없고, 너무 복잡하면 유지보수가 어려워지니까.</p>
<h3 id="비동기-로직과-상태-변화-테스트">비동기 로직과 상태 변화 테스트</h3>
<p>React의 비동기 상태 변화를 테스트하는 것이 가장 어려웠다. <code>waitFor</code>, <code>act</code> 같은 유틸리티들을 사용하면서 비동기 테스트의 어려움을 체감했다.</p>
<pre><code class="language-javascript">test(&#39;반복 생성된 일정들과 이미 존재하는 일정이 겹칠 때 경고 표시&#39;, async () =&gt; {
  // 비동기 상태 변화를 기다려야 하는 복잡함
  await waitFor(() =&gt; {
    expect(screen.getByText(/일정이 겹칩니다/)).toBeInTheDocument();
  });
});</code></pre>
<p>특히 타임아웃 이슈로 몇 번 고생했는데, 이는 근본적인 성능 문제일 수도 있고 내 컴퓨터 환경의 문제일 수도 있었다. 결국 임시방편으로 timeout 값을 늘렸지만, 실무에서는 이런 식으로 해결하면 안 되겠다는 생각이 들었다.</p>
<h2 id="아쉬웠던-점들-🎯">아쉬웠던 점들 🎯</h2>
<h3 id="e2e-테스트-미경험의-아쉬움">E2E 테스트 미경험의 아쉬움</h3>
<p>이번 과제에서 가장 아쉬웠던 점은 <strong>E2E 테스트를 경험하지 못한 것</strong>이다. 단위 테스트와 통합 테스트에만 집중하다 보니, 실제 사용자가 경험하는 전체 플로우를 검증하는 E2E 테스트를 놓쳤다.</p>
<p>특히 캘린더 애플리케이션 같은 경우, 사용자가 &quot;일정 생성 → 캘린더에서 확인 → 수정 → 삭제&quot;하는 전체 여정을 테스트하는 것이 정말 중요한데, 이 부분을 놓친 게 아쉽다. </p>
<p>심화과제 평가에서도 이 부분이 지적되었는데, 이론적으로만 알고 있던 E2E 테스트의 중요성을 실제로 느끼게 되었다. Cypress나 Playwright 같은 도구를 사용해서 실제 브라우저 환경에서의 테스트를 경험해보지 못한 것이 정말 아쉽다.</p>
<h3 id="시각적-회귀-테스트의-부재">시각적 회귀 테스트의 부재</h3>
<p>또한 UI 컴포넌트의 시각적 변화를 감지하는 시각적 회귀 테스트도 고려하지 못했다. 캘린더 UI는 시각적 요소가 중요한데, 코드 변경으로 인한 UI 깨짐을 자동으로 감지하는 테스트가 있었다면 더 완전한 테스트 전략이 되었을 텐데.</p>
<h3 id="테스트-성능-최적화">테스트 성능 최적화</h3>
<p>앞서 언급한 timeout 이슈도 근본적으로 해결하지 못한 아쉬움이 있다. 테스트가 느리면 개발자 경험이 나빠지고, 결국 TDD 사이클 자체가 지연되니까. 실무에서는 이런 성능 문제를 어떻게 해결하는지 더 알아보고 싶다.</p>
<h2 id="실무-관점에서의-고민들">실무 관점에서의 고민들</h2>
<h3 id="회사에서-tdd가-현실적일까">회사에서 TDD가 현실적일까?</h3>
<p>코치님의 답변을 들으면서 현실을 알게 되었다. 대부분의 회사에서 완전한 TDD 도입은 쉽지 않다는 것, 특히 프론트엔드에서는 더욱 어렵다는 것을 알게 되었다.</p>
<p>하지만 그렇다고 해서 TDD를 배운 것이 의미 없다고 생각하지는 않는다. <strong>설계를 개선하는 사고방식</strong>을 배웠고, 이는 TDD를 완전히 적용하지 않더라도 코드 품질을 높이는 데 도움이 될 것 같다.</p>
<h3 id="팀원과의-협업에서-테스트">팀원과의 협업에서 테스트</h3>
<p>실무에서 혼자 개발하는 경우가 많은 프론트엔드지만, 테스트 코드가 있으면 <strong>코드 리뷰나 인수인계 과정에서 의도를 파악하기 쉬워진다</strong>는 점을 깨달았다. 특히 복잡한 비즈니스 로직의 경우, 테스트 코드가 일종의 명세서 역할을 한다는 것을 체감했다.</p>
<h2 id="배운-것들과-앞으로의-계획">배운 것들과 앞으로의 계획</h2>
<h3 id="핵심-인사이트">핵심 인사이트</h3>
<ol>
<li><strong>TDD는 테스트 도구가 아니라 설계 방법론</strong>이다</li>
<li><strong>테스트 전략은 프로젝트 맥락에 따라 달라져야 한다</strong></li>
<li><strong>실패하는 테스트를 보는 것에 익숙해져야 한다</strong></li>
<li><strong>테스트하기 어려운 코드는 보통 설계에 문제가 있다</strong></li>
</ol>
<h3 id="앞으로-더-학습하고-싶은-것들">앞으로 더 학습하고 싶은 것들</h3>
<ol>
<li><strong>E2E 테스트</strong>: Cypress, Playwright 등을 활용한 사용자 시나리오 테스트</li>
<li><strong>시각적 회귀 테스트</strong>: Storybook이나 Chromatic을 활용한 UI 테스트</li>
<li><strong>성능 테스트</strong>: 테스트 실행 시간 최적화 방법들</li>
<li><strong>실무 적용</strong>: 레거시 프로젝트에 점진적으로 테스트 도입하는 전략</li>
</ol>
<h2 id="마무리하며">마무리하며</h2>
<p>첫 TDD 경험이었지만, 많은 것을 배우고 느낄 수 있었다. 특히 팀원들과 함께 테스트 전략을 논의하고 합의하는 과정에서, 혼자서는 생각하지 못했을 관점들을 배울 수 있어서 좋았다.</p>
<p>아직 E2E 테스트나 시각적 회귀 테스트 같은 부분들이 아쉽지만, 이것들은 앞으로의 학습 목표로 삼고 계속 공부해나가려고 한다.</p>
<p>무엇보다 <strong>&quot;테스트는 개발을 더 즐겁게 만든다&quot;</strong>는 것을 깨달았다. 안전하게 리팩토링할 수 있다는 자신감, 요구사항 변경에 대한 두려움 감소 등... 이런 것들이 개발자의 삶을 더 나아지게 만든다고 생각한다.</p>
<p>다음에는 E2E 테스트까지 포함한 완전한 테스트 전략을 구현해보고 싶다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[7주차 회고] 테스트 코드]]></title>
            <link>https://velog.io/@hee0ne_2/7%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@hee0ne_2/7%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Wed, 17 Sep 2025 08:09:10 GMT</pubDate>
            <description><![CDATA[<h2 id="배운-내용-정리-📚">배운 내용 정리 📚</h2>
<h3 id="1-테스트-환경-구성에-대한-고민과-이해">1. 테스트 환경 구성에 대한 고민과 이해</h3>
<p>이번 과제를 진행하면서 &quot;왜 이런 설정들이 필요한가?&quot;라는 근본적인 질문부터 시작했다.</p>
<p><strong>Mock에 대한 고민과 깨달음</strong>
처음에는 &quot;왜 실제 함수를 쓰지 않고 가짜를 만드는거지?&quot;라는 의문이 들었다. 하지만 실제로 사용해보니:</p>
<ul>
<li><code>toastFn</code>: 실제 토스트를 표시하면 테스트 환경에서 UI가 복잡해지고, 정작 확인하고 싶은 건 &quot;호출되었는지&quot;이지 &quot;실제 토스트가 보이는지&quot;가 아니라는걸 깨달았다</li>
<li><code>enqueueSnackbarFn</code>: 함수 호출을 추적하고 기록하는 것만으로도 충분히 기능을 검증할 수 있다는 점</li>
<li><code>notistack mock</code>: 실제 라이브러리 의존성 없이도 테스트가 가능하다는 것이 얼마나 큰 장점인지 체감했다</li>
</ul>
<p><strong>Provider 사용에 대한 의문과 해답</strong>
통합 테스트에서 <code>ChakraProvider</code>로 컴포넌트를 감싸는 부분을 보고 &quot;이게 정말 의미가 있을까?&quot;라고 의문을 가졌다. 하지만 실제로는:</p>
<ul>
<li><code>ThemeProvider</code>, <code>SnackbarProvider</code>, <code>CssBaseline</code> 등이 단순한 스타일링이 아니라 컴포넌트의 핵심 기능 작동에 필요하다는걸 알게 되었다</li>
<li>Provider 없이는 렌더링은 되지만 기능이 제대로 작동하지 않을 수 있다는 점이 충격적이었다</li>
<li>&quot;겉보기에는 괜찮아 보이지만 실제로는 망가진 상태&quot;를 방지하는 중요한 역할을 한다는걸 깨달았다</li>
</ul>
<p><strong>handlersUtils 활용에서 느낀 효율성</strong>
각 이벤트 생성/수정/삭제를 위한 mock API 설정을 보면서 &quot;이미 짜여져 있는 함수를 가져다 쓰는거였지만 미디움에서 편리하게 사용했다&quot;고 생각했다. 실제 API 호출은 너무 많은 리소스를 사용한다고 생각이 들었고, 가짜 서버와 데이터를 만들어 사용하는 것이 얼마나 효율적인지 몸소 체험할 수 있었다.</p>
<p><strong>시간 고정에 대한 이해</strong>
<code>vi.setSystemTime</code>을 보고 &quot;왜 이 시간을 설정해주는 걸까?&quot;라는 궁금증에서 시작했다. 고정된 시간 설정이 날짜/시간 의존 로직의 예측 가능성과 재현성을 보장한다는걸 이해하고 나니, 테스트의 안정성이 얼마나 중요한지 깨달았다.</p>
<h3 id="2-비동기-테스트에서-겪은-시행착오와-이해">2. 비동기 테스트에서 겪은 시행착오와 이해</h3>
<p><code>act</code>와 <code>waitFor</code>를 처음 접했을 때 &quot;언제 뭘 써야 하는거지?&quot;라는 혼란이 컸다.</p>
<p><strong>초기 고민들</strong></p>
<ul>
<li>&quot;act와 waitFor이 둘 다 비동기 처리를 하는 것 같은데 차이점이 뭐지?&quot;</li>
<li>&quot;언제는 act를 쓰고 언제는 waitFor을 써야 하는거야?&quot;</li>
<li>&quot;타이머 관련 테스트에서 실제로 시간이 흘러야 하나, 아니면 즉시 처리할 수 있나?&quot;</li>
</ul>
<p><strong>실제 경험을 통한 깨달음</strong></p>
<ul>
<li><code>act</code>는 동기/비동기 모두 사용 가능하며, React의 상태 업데이트를 즉시 처리한다는걸 체험했다</li>
<li><code>waitFor</code>은 항상 비동기이며, 실제 시간이 흘러야 하는 상황에서 사용한다는걸 이해했다</li>
</ul>
<pre><code class="language-javascript">// 동기 act - &quot;이거 정말 즉시 처리되나?&quot; 의심하며 사용했는데 실제로 즉시 처리됨!
act(() =&gt; {
   vi.advanceTimersByTime(1000);
});

// waitFor - &quot;정말 2초를 기다려야 하나?&quot; 걱정했는데 필요한 경우였음
await waitFor(() =&gt; {
    expect(screen.getByText(&#39;1초 지남&#39;)).toBeInTheDocument();
}, { timeout: 2000 });</code></pre>
<h3 id="3-데이터-로딩-방식에-대한-고민과-선택">3. 데이터 로딩 방식에 대한 고민과 선택</h3>
<p>두 가지 데이터 로딩 방식을 보고 &quot;어떤게 더 좋은 방법일까?&quot;라는 고민이 들었다.</p>
<p><strong>1번 방법에 대한 의문</strong></p>
<pre><code class="language-javascript">await act(async () =&gt; {
    await new Promise((resolve) =&gt; setTimeout(resolve, 0));
});</code></pre>
<p>&quot;이게 왜 작동하는거지? setTimeout을 0으로 설정하는게 무슨 의미가 있을까?&quot;라는 의문에서 시작했다.</p>
<p><strong>2번 방법의 명확성</strong></p>
<pre><code class="language-javascript">await act(async () =&gt; {
    await result.current.fetchEvents();
});</code></pre>
<p>&quot;이게 더 직관적이고 명확한 것 같은데, 2번 방법을 택한게 맞을까?&quot;라고 고민했다.</p>
<p><strong>코치님 피드백을 통한 이해</strong></p>
<ul>
<li>1번은 &quot;다음 이벤트 루프로 넘어가며 pending Promise들을 기다리는&quot; 해킹 기법이라는걸 알게 되었다</li>
<li>2번이 의도가 더 명확하고 일반적으로 선호된다는 확신을 얻을 수 있었다</li>
<li>&quot;뭔가 비동기적으로 실행될 것 같은데 정확히 언제 실행될지 모를 때&quot; 1번을 사용한다는 기준을 이해했다</li>
</ul>
<h2 id="깨달은-점">깨달은 점</h2>
<h3 id="1-ai에-대한-의존도-조절의-중요성">1. AI에 대한 의존도 조절의 중요성</h3>
<p>몇 시간 동안 MUI Icons 모킹 문제로 삽질했던 경험이 가장 큰 깨달음을 줬다. AI에게 계속 에러를 고쳐달라고 했지만, 답이 수렴되지 않고 오히려 더 복잡해지기만 했다. </p>
<p>결국 직접 구글링해서 찾은 해결책은 단순했다:</p>
<pre><code class="language-javascript">// 기존
import { LocationCity } from &quot;@mui/icons-material&quot;;
// 수정
import LocationCity from &quot;@mui/icons-material/LocationCity&quot;;</code></pre>
<p>코치님의 팁처럼, AI를 사용했을 때 답이 수렴되지 않는다면 AI가 방향성을 제대로 짚지 못한 징조로 받아들이고, 직접 문제 해결에 나서야 한다는 것을 체감했다.</p>
<h3 id="2-의미있는-테스트의-기준">2. 의미있는 테스트의 기준</h3>
<p>코치님의 피드백을 통해 테스트 코드 작성의 기준을 명확히 할 수 있었다:</p>
<ol>
<li><strong>기획 관점에서의 요구사항</strong>: 기획서에 해당하는 요구사항이 있다면 뻔해 보여도 테스트해야 함</li>
<li><strong>실제 발생 가능성</strong>: 사용자로 인해 실제로 발생할 수 있는 상황인가?</li>
<li><strong>사용자 영향도</strong>: 이 테스트가 실패하면 사용자에게 문제가 되는가?</li>
</ol>
<p>경계값 테스트의 경우, 달력 Form을 사용한다면 큰 문제가 없을 수 있지만, 코드로 입력하는 엑셀 같은 환경이라면 반드시 검증해야 한다는 관점이 인상적이었다.</p>
<h3 id="3-통합-테스트의-재미">3. 통합 테스트의 재미</h3>
<p>처음엔 막막했던 통합 테스트가 오히려 단위 테스트보다 재미있었다. 상태를 기반으로 기획서와 코딩을 맞춰가는 과정에서 전체적인 플로우를 이해할 수 있었고, 실제 사용자 관점에서 기능을 검증하는 느낌이 들었다.</p>
<h3 id="4-git-명령어의-위험성">4. Git 명령어의 위험성</h3>
<p><code>git reset --hard HEAD~1</code> 명령어로 커밋하지 않은 파일들이 모두 날아간 경험... 정말 뼈아픈 교훈이었다. 스테이징에 올려두지 않은 것이 화근이었지만, 덕분에 git 명령어의 위험성과 중요성을 체감할 수 있었다.</p>
<h2 id="아쉬운-점과-다음-목표">아쉬운 점과 다음 목표</h2>
<h3 id="아쉬운-점">아쉬운 점</h3>
<ol>
<li><strong>Hard 레벨 도전 실패</strong>: Git 실수로 인해 Hard 레벨을 완주하지 못한 것이 아쉽다</li>
<li><strong>AI 의존도</strong>: 초기에 AI에만 의존하려 했던 점, 직접 문제 해결을 시도하지 않았던 점</li>
<li><strong>기본기 부족</strong>: MUI Import 방식 같은 기본적인 부분에서 막혔던 점</li>
</ol>
<h3 id="다음-목표">다음 목표</h3>
<ol>
<li><strong>문제 해결 프로세스 개선</strong>: AI 사용 시 답이 수렴되지 않으면 즉시 직접 조사로 전환</li>
<li><strong>Git 사용 숙련도 향상</strong>: 위험한 명령어 사용 전 충분한 확인, 정기적인 커밋 습관화</li>
<li><strong>테스트 설계 능력 향상</strong>: 요구사항 기반의 의미있는 테스트 케이스 설계 능력 기르기</li>
<li><strong>Hard 레벨 재도전</strong>: 다음 기회에는 반드시 Hard 레벨까지 완주하기</li>
</ol>
<h3 id="마무리">마무리</h3>
<p>이번 과제는 단순히 테스트 코드 작성법을 배우는 것을 넘어서, 문제 해결 접근법, 도구 사용의 균형, 그리고 실패로부터 배우는 것의 중요성을 깨닫게 해준 의미있는 경험이었다. </p>
<p>&quot;내가 웃는게 웃는게 아니야.. ㅠ&quot;라고 해주신 코치님의 따뜻한 피드백처럼, 실패와 삽질의 경험도 결국 성장의 밑거름이 된다는 것을 느꼈다. 테스트 코드에 대한 심리적 장벽은 확실히 허물어졌고, 다음 과제에서는 더 체계적이고 효율적인 접근을 해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[6주차 회고] 관심사 분리와 폴더구조]]></title>
            <link>https://velog.io/@hee0ne_2/5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-397ejc3v</link>
            <guid>https://velog.io/@hee0ne_2/5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-397ejc3v</guid>
            <pubDate>Mon, 15 Sep 2025 02:35:24 GMT</pubDate>
            <description><![CDATA[<h2 id="이번-주-주제-📖">이번 주 주제 📖</h2>
<h3 id="과제의-핵심취지">과제의 핵심취지</h3>
<p>목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기</p>
<ul>
<li>전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해</li>
<li>Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기</li>
<li>FSD(Feature-Sliced Design)에 대한 이해</li>
<li>FSD를 통한 관심사의 분리에 대한 이해</li>
<li>단일책임과 역할이란 무엇인가?</li>
<li>관심사를 하나만 가지고 있는가?</li>
<li>어디에 무엇을 넣어야 하는가?</li>
<li>TanstackQuery의 사용법에 대한 이해</li>
<li>TanstackQuery를 이용한 비동기 코드 작성에 대한 이해</li>
<li>비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해</li>
</ul>
<h2 id="배운-내용-정리-📚">배운 내용 정리 📚</h2>
<h3 id="fsd-아키텍쳐">FSD 아키텍쳐</h3>
<p>FSD가 생각보다 어렵지 않았다
<img src="https://velog.velcdn.com/images/hee0ne_2/post/d9fde1ee-c799-4520-98f9-709497659d48/image.png" alt=""></p>
<p>처음 FSD 폴더 구조를 봤을 때는 정말 복잡해 보였다.<code>entities</code>, <code>features</code>, <code>widgets</code> 같은 용어들이 생소했고, 파일을 이렇게까지 세분화해서 나눠야 하나 싶었다. 하지만 실제로 적용해보니 생각이 완전히 바뀌었다.
가장 인상 깊었던 건 <strong>&quot;엔티티는 정보, 피처는 행동&quot;</strong>이라는 개념이었다. 
댓글 기능을 예로 들면:</p>
<pre><code class="language-typescript">// entities/comment/model/types.ts - 댓글이 &quot;무엇&quot;인지 정의
export interface Comment {
  id: number
  content: string
  author: string
}

// features/comment/add-comment/hooks.ts - 댓글을 &quot;어떻게&quot; 추가하는지
export const useAddComment = () =&gt; {
  return useMutation({
    mutationFn: (commentData) =&gt; CommentAPI.createComment(commentData)
  })
}

// widgets/comment-list/CommentList.tsx - 댓글을 &quot;어떻게&quot; 보여줄지
export const CommentList = () =&gt; {
  return &lt;div&gt;{/* 댓글 목록 UI */}&lt;/div&gt;
}</code></pre>
<p>이제는 댓글 관련 수정이 필요하면 features/comment 폴더만 들여다보면 된다. </p>
<h3 id="tanstack-query">Tanstack Query</h3>
<p>TanStack Query의 간결함에 놀랐다
useState와 useEffect로 API 상태를 관리하던 기존 방식이 얼마나 번거로웠는지 새삼 깨달았다.</p>
<pre><code class="language-javascript">// 기존에 이렇게 복잡하게 작성하던 걸...
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

useEffect(() =&gt; {
  setLoading(true)
  fetch(&#39;/api/posts&#39;)
    .then(res =&gt; res.json())
    .then(data =&gt; setPosts(data))
    .catch(err =&gt; setError(err))
    .finally(() =&gt; setLoading(false))
}, [])

// 이렇게 간단하게!
const { data: posts, isLoading, error } = useQuery({
  queryKey: [&#39;posts&#39;],
  queryFn: () =&gt; fetch(&#39;/api/posts&#39;).then(res =&gt; res.json())
})</code></pre>
<p>특히 낙관적 업데이트가 진짜 게임 체인저였다. 사용자가 댓글을 작성하면 서버 응답을 기다리지 않고 바로 화면에 표시되니까 체감 속도가 확실히 빨라졌다. queryClient.setQueryData로 캐시를 직접 조작할 수 있다는 점도 신기했다.</p>
<h3 id="zustand-상태-관리">Zustand 상태 관리</h3>
<p>Zustand로 다이얼로그 지옥에서 탈출
이번 과제에서 가장 공들인 부분이 바로 다이얼로그 상태 관리였다. 처음에는 각 컴포넌트마다 useState로 다이얼로그를 열고 닫았는데, 이게 진짜 문제였다. 게시물 추가 다이얼로그와 댓글 추가 다이얼로그가 동시에 열리거나, 하나를 닫았는데 다른 곳에서는 여전히 열려있다고 인식하는 상황이 발생했다.</p>
<pre><code class="language-typescript">// 해결책: 모든 다이얼로그 상태를 중앙에서 관리
export const useDialogStore = create&lt;DialogState&gt;((set) =&gt; ({
  showAddDialog: false,
  showEditDialog: false,
  showAddCommentDialog: false,
  // ... 6개 다이얼로그 상태

  openAddDialog: () =&gt; set({ showAddDialog: true }),
  closeAddDialog: () =&gt; set({ showAddDialog: false }),

  // 핵심: 모든 다이얼로그를 한 번에 닫는 함수
  closeAllDialogs: () =&gt; set({
    showAddDialog: false,
    showEditDialog: false,
    showAddCommentDialog: false,
    // ... 모든 상태 초기화
  }),
}))</code></pre>
<p>이제 다이얼로그 관련 버그는 거의 사라졌고, 상태 동기화 문제도 해결됐다.</p>
<h2 id="깨달은-점-💡">깨달은 점 💡</h2>
<ol>
<li>구조가 복잡해 보이는 이유는 따로 있었다
피드백을 받고 나서 깨달은 건, 내가 FSD 구조를 완전히 지키지 못했기 때문에 더 복잡해 보였다는 것이다.
예를 들어, features에서 entities의 API를 직접 호출하고 있었다:<pre><code class="language-typescript">// 내가 작성한 코드
import CommentAPI from &quot;../../../entities/comment/api/CommentAPI&quot;
export const useDeleteCommentFeature = () =&gt; {
const deleteCommentMutation = useMutation({
 mutationFn: (id: number) =&gt; CommentAPI.deleteComment(id),
})
}</code></pre>
이렇게 하니까 HTTP 클라이언트를 바꾸려면 entities의 API 파일뿐만 아니라 features에서 직접 호출하는 파일들도 모두 수정해야 한다. 피드백에 따르면 현재 구조에서는 약 8-15개 파일을 추가로 수정해야 하지만, 올바른 구조라면 entities의 API 파일 6-8개만 수정하면 된다고 한다.
올바른 방식은 이런 거였다:<pre><code class="language-typescript">// entities에서 훅을 제공하고
export const useDeleteComment = () =&gt; useMutation({ 
mutationFn: (id) =&gt; CommentAPI.deleteComment(id) 
})
</code></pre>
</li>
</ol>
<p>// features에서는 그 훅을 사용
import { useDeleteComment } from &quot;../../../entities/comment&quot;
export const useDeleteCommentFeature = () =&gt; {
  const deleteCommentMutation = useDeleteComment()
  // 비즈니스 로직만 여기서 처리
}</p>
<pre><code>
2. Props Drilling 완전 제거가 생각보다 어렵다
Zustand로 다이얼로그 상태는 깔끔하게 분리했지만, 댓글 데이터와 관련된 함수들은 여전히 props로 전달하는 부분이 남아있다. 어떤 상태는 전역으로 관리하고, 어떤 상태는 로컬로 두어야 하는지에 대한 명확한 기준이 부족했던 것 같다.
특히 댓글 입력 폼이나 선택된 댓글 같은 단기간의 UI 상태까지 전역으로 관리할 필요는 없다는 걸 배웠다. 이런 건 컴포넌트 내부에서 `useState`로 관리하는 게 더 적절하다.

3. 일관성이 정말 중요하다
피드백에서 지적받은 건 내 코드에 일관성이 부족하다는 점이었다. 쿼리 키를 어떤 곳에서는 상수로 쓰고, 어떤 곳에서는 문자열 리터럴로 직접 써버렸다. 훅 네이밍도 제각각이었고, 파일 위치 규칙도 혼재되어 있었다.
이런 작은 불일치들이 쌓이면 나중에 팀으로 작업할 때 큰 문제가 될 수 있겠다는 생각이 들었다. 특히 새로운 팀원이 합류했을 때 혼란스러워할 것 같다.

## 아쉬운 점과 다음 목표 🎯
### 아쉬운 점

1. 계층 경계를 제대로 지키지 못했다
가장 아쉬운 부분은 FSD의 핵심 규칙인 계층 경계를 완전히 지키지 못했다는 것이다. features에서 entities의 내부 구현에 직접 접근하는 코드들이 여러 곳에 있었다. 이 때문에 변경의 영향 범위가 넓어지고, 모듈 간의 결합도가 높아졌다.

2. 쿼리 키 관리가 엉성했다
TanStack Query를 사용하면서 쿼리 키 관리를 체계적으로 하지 못했다. 어떤 곳에서는 QUERY_KEYS.POSTS를 쓰고, 어떤 곳에서는 [&#39;comments&#39;, postId]처럼 직접 배열을 작성했다. 피드백에서 제안받은 중앙화된 쿼리 키 팩토리가 정말 필요하겠다는 생각이 든다:
```typescript
export const queryKeys = {
  posts: () =&gt; [&#39;posts&#39;] as const,
  postsList: (params) =&gt; [...queryKeys.posts(), params] as const,
  comments: (postId) =&gt; [&#39;comments&#39;, postId] as const,
}</code></pre><ol start="3">
<li><p>낙관적 업데이트 구현이 불완전했다
현재는 queryClient.setQueryData로 캐시를 직접 조작하는 방식을 사용하고 있는데, 실패했을 때 롤백 처리가 제대로 되지 않는다. onMutate/onError/onSettled 패턴을 사용해서 더 안전하게 구현했어야 했다.</p>
</li>
<li><p>다이얼로그 상태 구조의 확장성 부족
6개의 다이얼로그를 각각 boolean 변수로 관리하는 방식은 새로운 다이얼로그를 추가할 때마다 store를 수정해야 한다. 피드백에서 제안받은 키/맵 기반 구조가 훨씬 확장성이 좋아 보인다:</p>
<pre><code class="language-typescript">// 현재 방식
interface DialogState { 
showAddDialog: boolean
showEditDialog: boolean
// 새 다이얼로그마다 여기에 추가해야 함
}
</code></pre>
</li>
</ol>
<p>// 개선된 방식
interface DialogState { 
  dialogs: Record&lt;string, boolean&gt;
  open: (key: string) =&gt; void
  close: (key: string) =&gt; void
}</p>
<p>```</p>
<h3 id="다음-목표">다음 목표</h3>
<p>즉시 해결하고 싶은 것들</p>
<p>바렐(index.ts) 파일 도입: entities, features 폴더마다 public API를 명확하게 노출하는 index.ts 파일을 만들어야겠다. 이렇게 하면 내부 구현과 외부 인터페이스를 확실하게 분리할 수 있다.
쿼리 키 중앙화: shared/api/queryKeys.ts 파일을 만들어서 모든 쿼리 키를 한 곳에서 관리하고 싶다. 이렇게 하면 키 변경이나 캐시 무효화 로직을 훨씬 쉽게 관리할 수 있을 것 같다.
features → entities 의존성 정리: 현재 features에서 직접 API를 호출하는 부분들을 모두 찾아서 entities의 public 훅을 사용하도록 수정하겠다.</p>
<p>한 달 안에 달성하고 싶은 것들</p>
<p>낙관적 업데이트 패턴 표준화: onMutate/onError/onSettled 패턴으로 모든 mutation을 표준화하고, 실패 시 안전한 롤백이 가능하도록 구현하고 싶다.
ESLint 규칙으로 아키텍처 보호: import/no-restricted-paths 같은 규칙을 도입해서 계층 위반을 CI에서 자동으로 잡아낼 수 있게 하고 싶다.
현대적인 React 패턴 도입: Suspense와 ErrorBoundary를 도입해서 선언적인 로딩/에러 처리를 구현해보고 싶다.</p>
<p>장기적으로 도전해보고 싶은 것들
앞으로 더 큰 프로젝트를 할 때는 모노레포 구조로 도메인별 패키지를 분리하는 것도 고려해보고 싶다. 그리고 완전한 Props Drilling 제거를 위한 상태 분리 정책도 명확하게 정립하고 싶다.
무엇보다 테스트를 제대로 도입해보고 싶다. 이번에는 기능 구현에만 집중했는데, 다음에는 테스트하기 쉬운 구조로 설계하는 것부터 시작해야겠다.</p>
<h2 id="마무리하며">마무리하며</h2>
<p>이번 과제를 통해 단순히 기능이 돌아가는 코드를 넘어서, 유지보수하기 쉽고 확장 가능한 구조에 대해 깊이 생각해볼 수 있었다. 특히 피드백을 통해 내가 놓치고 있던 부분들을 구체적으로 알 수 있어서 정말 값진 시간이었다.
아직 부족한 점이 많지만, 이제는 &quot;왜 이런 구조가 좋은가?&quot;에 대한 나름의 기준이 생겼다. 변경의 영향 범위를 최소화하고, 일관된 패턴으로 개발 생산성을 높이는 것. 이게 바로 좋은 아키텍처의 핵심이라는 걸 배웠다.
앞으로도 이런 구조적 사고를 계속 발전시켜서, 혼자서만 잘 돌아가는 코드가 아니라 팀과 함께 성장할 수 있는 코드를 작성하는 개발자가 되고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[5주차 회고] 디자인 패턴과 함수형 프로그래밍]]></title>
            <link>https://velog.io/@hee0ne_2/5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@hee0ne_2/5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Mon, 15 Sep 2025 01:45:46 GMT</pubDate>
            <description><![CDATA[<h2 id="이번-주-주제-📖">이번 주 주제 📖</h2>
<h3 id="과제의-핵심취지">과제의 핵심취지</h3>
<ul>
<li>React의 hook 이해하기</li>
<li>함수형 프로그래밍에 대한 이해</li>
<li>액션과 순수함수의 분리</li>
</ul>
<h2 id="배운-내용-정리-📚">배운 내용 정리 📚</h2>
<p>이번 주차는 React Hook에 대해 깊이 있게 학습하고, 함수형 프로그래밍의 핵심 개념들을 실제 코드로 구현해보는 시간이었다. 특히 상태 관리와 컴포넌트 설계에서 많은 고민과 배움이 있었다.</p>
<h3 id="주요-학습-포인트">주요 학습 포인트</h3>
<p><strong>상태 추상화와 Hook 설계</strong></p>
<ul>
<li>Jotai atoms에서 커스텀 hooks로의 전환</li>
<li>컴포넌트와 훅의 계층 분리</li>
<li>재사용 가능한 훅 인터페이스 설계</li>
</ul>
<p><strong>도메인 기반 모듈화</strong></p>
<ul>
<li>Product, Cart, Coupon 도메인별 폴더 구조</li>
<li>높은 응집도, 낮은 결합도를 위한 컴포넌트 분리</li>
<li>UI와 도메인 로직의 책임 분리</li>
</ul>
<p><strong>전역 상태 관리</strong></p>
<ul>
<li>localStorage와 React State 동기화 패턴 구현</li>
<li>Notification API 설계와 상태 관리</li>
<li>테스트 격리를 위한 Jotai Provider 활용</li>
</ul>
<h2 id="어려웠던-점-🤯">어려웠던 점 🤯</h2>
<h3 id="1-커스텀-훅-상태-공유-실수">1. 커스텀 훅 상태 공유 실수</h3>
<p>Props drilling을 피하려다가 큰 함정에 빠졌다. 하위 컴포넌트에서 같은 커스텀 훅을 다시 호출하면 상태가 공유될 거라 생각했는데, 완전히 독립적인 상태가 생성되었다.</p>
<pre><code class="language-javascript">function ParentComponent() {
  const { addNotification } = useNotification();

  addNotification(&#39;성공&#39;);
  return (
    &lt;div&gt;
      &lt;ChildComponent /&gt;  {/* props로 전달하지 않음 */}
    &lt;/div&gt;
  );
}

function ChildComponent() {
  const { addNotification } = useNotification(); // 같은 훅을 다시 선언
  // 하지만 Parent의 상태와는 완전히 분리됨!
}</code></pre>
<p>이 실수를 2번이나 반복하면서 깨달았다: <strong>커스텀 훅은 로직 재사용을 위한 도구이지, 상태를 공유하는 것이 아니다.</strong> 각 호출마다 새로운 상태 인스턴스가 생성된다는 React Hook의 기본 원칙을 체감했다.</p>
<h3 id="2-localstorage와-react-state-동기화-딜레마">2. localStorage와 React State 동기화 딜레마</h3>
<p>두 개의 독립적인 상태 저장소를 동기화하는 과정에서 많은 고민이 있었다:</p>
<ul>
<li>React state: 컴포넌트 생명주기에 따라 생성/소멸</li>
<li>localStorage: 브라우저 세션을 넘어 영구 지속</li>
</ul>
<p>처음 접근한 방식은 useEffect를 통한 단방향 동기화였다:</p>
<pre><code class="language-javascript">const [value, setValue] = useState(() =&gt; {
  const saved = localStorage.getItem(key);
  return saved ? JSON.parse(saved) : initialValue;
});

useEffect(() =&gt; {
  localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);</code></pre>
<p>하지만 이 방법은 상태 업데이트 시점에서 일관성 문제가 발생했다. 팀원들과의 코드 공유를 통해 <strong>Single Source of Truth</strong> 패턴을 적용한 해결책을 찾을 수 있었다:</p>
<pre><code class="language-javascript">const setValue = (value: T | ((val: T) =&gt; T)) =&gt; {
  setStoredValue((prev) =&gt; {
    const newValue =
      typeof value === &quot;function&quot; ? (value as (val: T) =&gt; T)(prev) : value;

    if (
      newValue === undefined ||
      (Array.isArray(newValue) &amp;&amp; newValue.length === 0)
    ) {
      localStorage.removeItem(key);
    } else {
      localStorage.setItem(key, JSON.stringify(newValue));
    }

    return newValue;
  });
};</code></pre>
<p>핵심 차이점은 실행 시점이었다:</p>
<ul>
<li><strong>이전</strong>: React state 업데이트 → 렌더링 → useEffect 실행</li>
<li><strong>개선</strong>: localStorage 저장 → React state 업데이트 (동기)</li>
</ul>
<h2 id="깨달은-점-💡">깨달은 점 💡</h2>
<h3 id="학습의-본질-되돌아보기">학습의 본질 되돌아보기</h3>
<p>4주차에 바이브코딩을 하면서 큰 현타가 왔었다. AI에게 의존하면서 테스트 통과만을 목표로 하다 보니, &quot;내가 뭘 하고 있는 거지?&quot;라는 생각이 들었다. 교육의 진짜 목적은 <strong>스스로 배워가는 것</strong>인데, 언제부터 테스트 통과가 최우선이 되었을까?</p>
<p>5주차에서는 다시 초심으로 돌아가 AI 도움을 최소화하고, 팀원들과의 코드 리뷰와 페어코딩을 통해 하나씩 부딪혀가며 배웠다. 처음엔 내 질문이 상대방의 시간을 빼앗는다고 생각했지만, 오히려 서로 설명하고 토론하면서 <strong>win-win 관계</strong>가 형성되었다.</p>
<h3 id="컴포넌트-설계에-대한-고민">컴포넌트 설계에 대한 고민</h3>
<p>이번 과제에서 가장 신경 쓴 부분은 컴포넌트 분리였다:</p>
<ul>
<li>UI와 Hooks의 분리</li>
<li>도메인별 컴포넌트 구조 (Product, Cart, Coupon)</li>
<li><strong>낮은 결합도, 높은 응집도</strong>를 지키기 위한 설계</li>
</ul>
<p>&quot;더 세세하게 나누는 게 좋을까? (ProductImg, ProductDiscount, ProductPrice 등) 아니면 큼직하게 나누는 게 좋을까? (ProductCard &lt; ProductList)&quot;라는 고민이 계속 있었다. 결국 재사용성과 유지보수성을 기준으로 판단해야 한다는 걸 배웠다.</p>
<h3 id="hook의-진정한-의미">Hook의 진정한 의미</h3>
<p>커스텀 훅을 직접 설계하면서 깨달은 것:</p>
<ul>
<li>훅은 단순한 로직 재사용 도구가 아니라, <strong>상태와 부수효과를 캡슐화하는 강력한 추상화 도구</strong></li>
<li>훅의 public API 설계가 전체 애플리케이션의 결합도를 좌우함</li>
<li>전역 상태의 &#39;소유자&#39;를 명확히 하는 것의 중요성</li>
</ul>
<h2 id="아쉬운-점과-다음-목표-🎯">아쉬운 점과 다음 목표 🎯</h2>
<p>원래 목표는 몰랐던 부분을 천천히 학습하며 과제를 진행하는 것이었다. 그러나 진도가 늦어지면서 제출일이 다가올수록 테스트 통과에만 집중하게 되었다.</p>
<p>다른 사람의 PR을 참고하거나 AI를 활용하면서 스스로 고민할 기회가 줄어든 점이 아쉬웠다.</p>
<p>더 많은 시간을 투자하여 온전히 내 힘으로 과제를 진행했다면 더 깊은 배움과 성장이 있었을 것이다. 클린코드 과제에서는 특히 이런 아쉬움이 크게 남았다.</p>
<p>다음에는 반드시 스스로 생각하며 내 힘으로만 과제를 완성하는 것을 목표로 한다. 그렇게 해야 진정으로 내 것이 될 수 있다.</p>
<hr>
<p><em>이번 주는 비록 완벽하지 않았지만, 팀원들과 함께 학습하는 즐거움을 다시 느낄 수 있었다. 다음 주차에서도 더 많은 페어코딩과 협업을 통해 성장하고 싶다!</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[4주차 회고] 클린코드와 리팩토링]]></title>
            <link>https://velog.io/@hee0ne_2/4%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C%EC%99%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@hee0ne_2/4%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C%EC%99%80-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Sun, 10 Aug 2025 11:00:32 GMT</pubDate>
            <description><![CDATA[<h2 id="이번-주-주제">이번 주 주제</h2>
<h3 id="클린코드-원칙과-리액트-컴포넌트-리팩토링">클린코드 원칙과 리액트 컴포넌트 리팩토링</h3>
<p>회사에서 업무를 하면서 항상 내가 짠 코드에 대해 생각을 했다.
더 가독성 있게 짤 수는 없을까? 성능을 더 높일 수 있는 코드를 작성할 수 있지 않을까?
내 코드에 대해 리뷰를 받을 수 있는 환경도 아니었고, 더 좋은 코드가 있을 거 같은데... 생각하면서도 AI한테 물어봐도 만족할 수 있는 답을 받아보지 못했다.</p>
<p>그래서 클린코드와 리팩토링에 대한 주제가 나에게 너무 큰 기대가 되었다.
하지만 결과는 큰 실망만 한 채로 끝났다...
왜냐고? 완전히 바이브코딩으로 진행했기 때문에.</p>
<p>바이브코딩으로 진행해서 문제였다라기 보다는, 과제 테스트 통과를 위해 바이브코딩을 하면서 배워나간점이 없다고 생각이 들어 큰 실망을 했다. 
테스트 통과에 급급해 기능이 제대로 동작하는지, AI가 짜준 코드가 정말 맞는건지, 가독성이 정말로 좋은지 확인도 안한채 넘어갔기 때문이다.</p>
<p>이런 실패(?) 과정 덕분에 나는 다음주차 과제를 AI에 너무 의존하지 말고, 도움만 받는 정도로 사용해야겠다. 라는 생각을 가지게 되었고 &quot;앞으로 더 잘할 수 있다&quot; 라고 생각하며 스스로를 토닥였다. (더 잘하면 되지!)</p>
<p><img src="https://velog.velcdn.com/images/hee0ne_2/post/e516a436-5238-4221-89f1-a9212eeff148/image.jpeg" alt=""></p>
<h2 id="배운-내용-정리-📚">배운 내용 정리 📚</h2>
<h3 id="클린코드-핵심-원칙">클린코드 핵심 원칙</h3>
<aside>
💡 **클린코드 핵심 원칙 짧게 정리**

<ul>
<li>의미 있는 이름 사용</li>
<li>함수를 작고 단일 책임을 가지도록 만들기</li>
<li>중복 코드 제거</li>
<li>주석 대신 자체 설명적인 코드 작성</li>
<li>일관된 포맷팅 유지</li>
<li>복잡한 조건문 단순화</li>
<li>적절한 추상화 수준 유지</aside>

</li>
</ul>
<h3 id="🧐-리팩토링의-목적">🧐 리팩토링의 목적</h3>
<blockquote>
<p>리팩토링의 목적 = <strong>유지보수!</strong></p>
</blockquote>
<ol>
<li>이슈해결을 하기 위해서 코드를 이해하고 투자하는 시간을 줄이기 위해서 하는것</li>
<li>코드를 수정했을때 문제가 없음을 보장하는 시간을 줄이기 위해서 하는것</li>
</ol>
<p>처음에는 작동하는 코드를 굳이 바꿀 필요가 있을까 의문이 들었다.
하지만 몇 번의 수정과 기능 추가를 거치면서, ‘작동하는 코드’와 ‘좋은 코드’는 다르다는 사실을 깨달았다.</p>
<p>회사에서 MVC 패턴을 이용한 프로젝트를 진행한 적이 있었는데, 기존 구조가 MVC 원칙에 맞지 않아 전반적인 리팩토링을 진행했다.
Model, View, Controller의 역할을 다시 정의하고 각 기능을 해당 계층에 맞게 분리했다.
Model에는 데이터 구조와 비즈니스 로직을, View에는 화면 렌더링과 UI 표현을, Controller에는 두 계층을 연결하는 역할만 맡겨 불필요하게 로직이 섞이지 않도록 했다.</p>
<p>이렇게 구조를 재정리하니 각 부분이 자신의 역할만 수행하도록 단순화되어 코드 이해도가 높아지고, 재사용성이 향상되었다.
특히, 중복된 기능을 공통 모듈로 묶어 여러 Controller나 View에서 재활용할 수 있도록 하면서 기능 변경 시 수정 범위를 최소화하고 버그 발생 가능성도 줄일 수 있었다.</p>
<p>위 같은 경험을 통해 리팩토링은 단순한 코드 정리가 아니라, 유지보수와 확장을 고려한 구조적 개선 작업이라는 점을 깊이 느꼈다.
앞으로는 코드를 처음 작성할 때부터 ‘미래의 나’와 ‘이 코드를 읽을 다른 사람’을 생각하며, 이해하기 쉬운 구조를 잡는 습관을 들이고자 한다.</p>
<h3 id="eslint">ESLint</h3>
<p>ESLint는 JavaScript 코드의 품질을 향상시키는 강력한 정적 코드 분석 도구
코드 스타일 문제, 잠재적 버그, 안티 패턴 등을 식별하고 수정할 수 있다.</p>
<h3 id="prettier">Prettier</h3>
<p>Prettier는 코드 포맷터로, 일관된 코드 스타일을 자동으로 적용
들여쓰기, 줄 바꿈, 공백 등의 스타일 문제를 자동으로 해결해준다.</p>
<h2 id="어려웠던-점-🤯">어려웠던 점 🤯</h2>
<p>AI를 잘 사용하는 법...
*<em>AI한테 어떻게 잘 질문해야 내가 원하는 것만 쏙쏙 뽑아낼 수 있을까?
*</em>
바이브코딩을 하면서도 정말 어려웠다. 
기존의 코드는 최대한 많이 건들지 않으며 필요한 부분만 쏙쏙 바꾸고 싶었는데, AI가 맘대로 UI도 바꿔버리고 기능 테스트시 버그도 만들고...</p>
<p>이번에 심지어 Cursor 도 결제했는데 (다행히 무료체험이 있어서 공짜였음)
최대한 작은 단계별로 진행하면서 나를 이해시켜줘. 라고 질문을 하니까 
토큰은 최대한으로 잡아먹으면서 스텝별로 나를 이해시켜주는게 아니라 AI 혼자 진행하고 있었다. (답답쓰)
-&gt; 결국 토큰 5시간만에 다 써버린;;;</p>
<h2 id="깨달은-점-💡">깨달은 점 💡</h2>
<p>다른 사람들은 <strong>AI를 어떻게 사용하고 있을까?</strong>에 대해 팀 회의를 하였다.</p>
<p>정리 된 내용!</p>
<blockquote>
</blockquote>
<ol>
<li>롤플레잉 (구원자 스타일)
 a. 내가 굉장히 위험한 상황에 처해있다는 것을 어필한다…. 뒤에서 누가 총을 겨누고 있다던지…. 뭐 그런거 다들 알잖슴~~</li>
<li>git diff 커밋A 커밋B 명령어 때린 뒤에 변경점에 대해서 문서화 &gt; PR 또는 기업 과제 작성 시 활용 (비서 스타일)</li>
<li>코딩스타일을 지침서에 넣는다 (가이드 스타일)</li>
<li>구현한 코드 프로세스 도식화 (가이드 스타일)</li>
<li>AI에게 공부한 내용을 먹이고 간단한 퀴즈를 내달라고 합니다. (선생님 스타일)</li>
</ol>
<p>다른 사람들 내용에서 또 좋다고 생각한 방법</p>
<blockquote>
</blockquote>
<ol>
<li>요구 사항 상세하게 설명하기
 a. AI가 알아듣기 좋은 형태로 요청하기 (커서룰, mdc파일, MCP 활용 등) </li>
<li>한 번에 큰 단위로 요청하지 말고 나누어 요청하기</li>
<li>목적에 따라 모델 바꿔가면서 요청하기</li>
<li>GPT와 제미나이 병행 사용, 같은 질문 반복 비교하며 &quot;AI끼리 싸움 붙임&quot;</li>
<li>하기 전에 AI 로 구현사항 정리 + 컨벤션 작성 &gt; 그후에 입력</li>
</ol>
<p>다들 다양하게 AI를 활용하고 있었기에 배울 게 많았다.
이후 나도 롤플레잉 적용하면서 진행하구 있다.
<code>나는 리액트 초보고(인턴이고) 너는 리액트 고수(시니어)야. ~~에 대해서 자세하고 쉽게 설명해주고 나를 이해시켜줘.</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[3주차 회고] React, Beyond the Basics]]></title>
            <link>https://velog.io/@hee0ne_2/3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-React-Beyond-the-Basics</link>
            <guid>https://velog.io/@hee0ne_2/3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-React-Beyond-the-Basics</guid>
            <pubDate>Sun, 27 Jul 2025 14:50:31 GMT</pubDate>
            <description><![CDATA[<h1 id="3주차-과제를-진행하면서-겪은-문제-😵💫">3주차 과제를 진행하면서 겪은 문제 😵‍💫</h1>
<p>일단, 나는 타입스크립트를 써본 적이 없어서 문법 자체들이 너무 생소했다. 
그리고 리액트 훅에 대해서도 useRef, useState 등 만 써봤지.. 
메모이제이션 관련해서 왜 사용하고, 어떻게 동작하는지도 몰랐다.
.
.
.
.
회고 마저 쓸게요,,,, 일단 링크만 올리기...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React.js] useSyncExternalStore]]></title>
            <link>https://velog.io/@hee0ne_2/React.js-useSyncExternalStore</link>
            <guid>https://velog.io/@hee0ne_2/React.js-useSyncExternalStore</guid>
            <pubDate>Wed, 23 Jul 2025 19:21:45 GMT</pubDate>
            <description><![CDATA[<h1 id="💡-usesyncexternalstore-란">💡 useSyncExternalStore 란?</h1>
<p><code>useSyncExternalStore</code>는 React 18에서 도입된 <strong>외부 저장소(state)</strong>의 변화를 React 컴포넌트에서 정확하고 일관성 있게 구독하기 위해 만든 훅이다.</p>
<h2 id="핵심-목적">핵심 목적</h2>
<p>외부 상태와 React 렌더링 사이의 시점을 정확히 동기화해서,
버그 없이 일관된 UI를 만들 수 있도록 해주기 위함.</p>
<p>Redux, Zustand, 전역 event 기반 store 등에서 사용할 수 있다.</p>
<h1 id="🧨-왜-usesyncexternalstore가-필요했는가">🧨 왜 useSyncExternalStore가 필요했는가?</h1>
<h2 id="⚠️-기존-방식의-문제-useeffect--usestate">⚠️ 기존 방식의 문제: useEffect + useState</h2>
<p>기존에는 외부 상태(store)를 React 컴포넌트에서 사용하려면 이런 방식으로 처리했다.</p>
<pre><code class="language-tsx">const [value, setValue] = useState(store.getValue());

useEffect(() =&gt; {
  const unsubscribe = store.subscribe(() =&gt; {
    setValue(store.getValue());
  });
  return unsubscribe;
}, []);</code></pre>
<p>겉보기에는 잘 동작하는 것 같지만...
React 18에서는 렌더 도중 외부 상태가 바뀔 경우, 렌더링이 취소되거나 다시 시작될 수 있는데, 이 과정에서 useEffect는 렌더 이후 동작하기 때문에 중간 상태 오류가 발생할 수 있다.</p>
<h3 id="😱-실제-문제점들">😱 실제 문제점들</h3>
<h4 id="1-렌더-시점과-구독-시점의-불일치">1. 렌더 시점과 구독 시점의 불일치</h4>
<p>useEffect는 렌더링 이후에 실행됨.</p>
<p>하지만 외부 상태는 그 전에 바뀌었을 수도 있음.</p>
<p>그럼 render → subscribe → value 변경 순이 되면, 초기 렌더에서 잘못된 값이 표시될 수 있음.</p>
<p>즉, 렌더링 시점에 스냅샷을 잡지 못해서 UI가 이전 값을 보여주거나 깜빡임이 생김.</p>
<h4 id="2-concurrent-mode에서의-부작용">2. Concurrent Mode에서의 부작용</h4>
<p>React 18 이후, Concurrent Rendering이 활성화됨에 따라 다음 문제가 중요해졌다.</p>
<blockquote>
<p><strong>Concurrent Rendering</strong>이란 ? 
React가 여러 개의 UI 작업을 병렬로 처리하고, 사용자 경험에 더 좋은 작업을 먼저 렌더링할 수 있도록 만든 렌더링 방식</p>
</blockquote>
<ul>
<li><p>React는 여러 번의 렌더를 미리 예약하고 나중에 커밋함.</p>
</li>
<li><p>그런데 그 사이에 외부 상태가 바뀌면, 예상과 다른 값이 렌더에 반영될 수 있음.</p>
</li>
</ul>
<p>즉, 렌더링 도중 외부 값이 바뀌었는지 React가 알 수 없어 버그가 생김.</p>
<p><strong>🛠️ 그래서 만들어진 useSyncExternalStore
이러한 문제를 해결하기 위해 useSyncExternalStore를 도입했다.</strong></p>
<h2 id="📌-정리하면">📌 정리하면</h2>
<blockquote>
<p><code>useSyncExternalStore</code>는
외부 상태를 React 렌더링 흐름 안에서 일관되고 안전하게 사용하는 유일한 방법이다.
이는 React가 Concurrent Rendering과 SSR(Server Side Rendering)을 완전히 지원하게 된 중요한 기반 중 하나이다.</p>
</blockquote>
<h1 id="🔍-기본-사용법">🔍 기본 사용법</h1>
<pre><code class="language-tsx">const state = useSyncExternalStore(subscribe, getSnapshot);</code></pre>
<p>📘 파라미터 설명 </p>
<table>
<thead>
<tr>
<th>인자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>subscribe</code></td>
<td>스토어 변경을 구독하는 함수. 변경이 발생하면 컴포넌트를 다시 렌더링함</td>
</tr>
<tr>
<td><code>getSnapshot</code></td>
<td>현재 스토어의 상태를 반환하는 함수 (렌더링 시 호출됨)</td>
</tr>
<tr>
<td><code>getServerSnapshot</code> <em>(선택)</em></td>
<td>서버 렌더링에서 사용할 상태 반환 함수 (SSR 시 필요)</td>
</tr>
</tbody></table>
<h3 id="🚀-usesyncexternalstore-사용법-4가지-실전-패턴-정리">🚀 useSyncExternalStore 사용법: 4가지 실전 패턴 정리</h3>
<p>다음은 실무에서 자주 사용되는 4가지 유형의 사용법입니다. 
** React 공식 문서 참고</p>
<h4 id="1️⃣-외부-store-구독-예-redux-zustand-등">1️⃣ 외부 Store 구독 (예: Redux, Zustand 등)</h4>
<p>✅ 목적
외부 상태 관리 라이브러리(Redux, Zustand 등)의 변경 사항을 안전하게 구독하기.</p>
<p>🧩 사용법</p>
<pre><code class="language-tsx">import { useSyncExternalStore } from &#39;react&#39;;
import { todosStore } from &#39;./todoStore.js&#39;;

function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  // ...
}</code></pre>
<p>store에 있는 데이터의 snapshot을 반환합니다. 두 개의 함수를 인수로 전달해야 합니다.</p>
<p>subscribe 함수는 store에 구독하고 구독을 취소하는 함수를 반환해야 합니다.
getSnapshot 함수 함수는 store에서 데이터의 스냅샷을 읽어야 합니다.</p>
<p>💡<code>snapshot</code> : 지금 이 순간의 외부 상태값</p>
<p>React는 이 함수를 사용해 컴포넌트를 store에 구독한 상태로 유지하고 변경 사항이 있을 때 리렌더링합니다.</p>
<h4 id="2️⃣-브라우저-api-구독-예-window-location-등">2️⃣ 브라우저 API 구독 (예: window, location 등)</h4>
<p>✅ 목적
window, matchMedia, geolocation 등의 브라우저 상태를 추적하고 반응형 UI 구성</p>
<p>🧩 사용법</p>
<pre><code class="language-tsx">import { useSyncExternalStore } from &#39;react&#39;;

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}</code></pre>
<p>getSnapshot 함수를 구현하려면 브라우저 API에서 현재 값을 읽습니다.</p>
<pre><code class="language-tsx">function getSnapshot() {
  return navigator.onLine;
}</code></pre>
<p>subscribe 함수를 구현</p>
<pre><code class="language-tsx">function subscribe(callback) {
  window.addEventListener(&#39;online&#39;, callback);
  window.addEventListener(&#39;offline&#39;, callback);
  return () =&gt; {
    window.removeEventListener(&#39;online&#39;, callback);
    window.removeEventListener(&#39;offline&#39;, callback);
  };
}</code></pre>
<h4 id="3️⃣-custom-hook으로-로직-추출하기">3️⃣ Custom Hook으로 로직 추출하기</h4>
<p>✅ 목적
공통 로직을 재사용 가능한 Hook으로 분리해서 여러 컴포넌트에서 활용하기</p>
<p>🧩 사용법
이 custom useOnlineStatus Hook은 네트워크가 온라인 상태인지 여부를 추적</p>
<p><strong>useOnlineStatus.js</strong></p>
<pre><code class="language-tsx">import { useSyncExternalStore } from &#39;react&#39;;

export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return isOnline;
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener(&#39;online&#39;, callback);
  window.addEventListener(&#39;offline&#39;, callback);
  return () =&gt; {
    window.removeEventListener(&#39;online&#39;, callback);
    window.removeEventListener(&#39;offline&#39;, callback);
  };
}</code></pre>
<p><strong>app.js</strong></p>
<pre><code class="language-tsx">import { useOnlineStatus } from &#39;./useOnlineStatus.js&#39;;

function StatusBar() {
  const isOnline = useOnlineStatus();
  return &lt;h1&gt;{isOnline ? &#39;✅ Online&#39; : &#39;❌ Disconnected&#39;}&lt;/h1&gt;;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log(&#39;✅ Progress saved&#39;);
  }

  return (
    &lt;button disabled={!isOnline} onClick={handleSaveClick}&gt;
      {isOnline ? &#39;Save progress&#39; : &#39;Reconnecting...&#39;}
    &lt;/button&gt;
  );
}

export default function App() {
  return (
    &lt;&gt;
      &lt;SaveButton /&gt;
      &lt;StatusBar /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>📌 이처럼 커스텀 훅으로 추출하면 테스트와 유지보수가 쉬워진다.</p>
<h4 id="4️⃣-서버-렌더링ssr-지원-추가">4️⃣ 서버 렌더링(SSR) 지원 추가</h4>
<p>✅ 목적
서버 렌더링 시 window, document 접근 오류 방지 및 초기 값 제공</p>
<p>🌐 예: useWindowWidth에 SSR 대응 추가</p>
<pre><code class="language-tsx">function useWindowWidth() {
  const subscribe = (callback: () =&gt; void) =&gt; {
    window.addEventListener(&quot;resize&quot;, callback);
    return () =&gt; window.removeEventListener(&quot;resize&quot;, callback);
  };

  const getSnapshot = () =&gt; window.innerWidth;

  const getServerSnapshot = () =&gt; 1024; // SSR 시 기본값

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}</code></pre>
<p>☑️ getServerSnapshot은 선택적 인자지만, SSR 환경에서는 필수!
💡 이 패턴은 Next.js, Remix 등 SSR 프레임워크를 사용할 때 매우 유용!</p>
<h2 id="📖-실제-연동-예제-모음">📖 실제 연동 예제 모음</h2>
<h3 id="✅-1-zustand와-usesyncexternalstore-연동하기">✅ 1. Zustand와 useSyncExternalStore 연동하기</h3>
<p>Zustand는 자체적으로 subscribe, getState 메서드를 제공하기 때문에 쉽게 연동 가능하다.</p>
<pre><code class="language-tsx">// 📁 store.ts
import { createStore } from &#39;zustand/vanilla&#39;;

export const counterStore = createStore(() =&gt; ({
  count: 0,
  increase: () =&gt; counterStore.setState((s) =&gt; ({ count: s.count + 1 })),
}));</code></pre>
<pre><code class="language-tsx">// 📁 useCounter.ts
import { useSyncExternalStore } from &quot;react&quot;;
import { counterStore } from &quot;./store&quot;;

export function useCounter() {
  return useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState
  );
}</code></pre>
<pre><code class="language-tsx">// 📁 Counter.tsx
import { useCounter } from &quot;./useCounter&quot;;

export default function Counter() {
  const { count, increase } = useCounter();
  return (
    &lt;div&gt;
      &lt;p&gt;🧮 Count: {count}&lt;/p&gt;
      &lt;button onClick={increase}&gt;+ 증가&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<blockquote>
<p>🧠 zustand/vanilla는 훅을 직접 만들고 싶을 때 사용한다.
React 전용 useStore 훅을 안 쓰고 useSyncExternalStore로 직접 구현하는 예제</p>
</blockquote>
<h3 id="✅-2-redux와-usesyncexternalstore-연동하기">✅ 2. Redux와 useSyncExternalStore 연동하기</h3>
<p>Redux도 store.subscribe와 store.getState를 그대로 활용 가능하다.</p>
<pre><code class="language-tsx">// 📁 store.ts
import { legacy_createStore } from &quot;redux&quot;;

const initialState = { count: 0 };

function reducer(state = initialState, action: any) {
  switch (action.type) {
    case &quot;INCREMENT&quot;:
      return { count: state.count + 1 };
    default:
      return state;
  }
}

export const store = legacy_createStore(reducer);</code></pre>
<pre><code class="language-tsx">// 📁 useReduxSelector.ts
import { useSyncExternalStore } from &quot;react&quot;;
import { store } from &quot;./store&quot;;

export function useReduxSelector&lt;T&gt;(selector: (state: any) =&gt; T): T {
  return useSyncExternalStore(
    store.subscribe,
    () =&gt; selector(store.getState())
  );
}</code></pre>
<pre><code class="language-tsx">// 📁 Counter.tsx
import { store } from &quot;./store&quot;;
import { useReduxSelector } from &quot;./useReduxSelector&quot;;

export default function Counter() {
  const count = useReduxSelector((state) =&gt; state.count);

  return (
    &lt;div&gt;
      &lt;p&gt;🧮 Redux Count: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; store.dispatch({ type: &quot;INCREMENT&quot; })}&gt;+ 증가&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre>
<blockquote>
<p>📌 이 방식은 React-Redux의 useSelector를 직접 대체하거나, 고도화된 커스텀 훅으로 활용할 수 있다.</p>
</blockquote>
<h3 id="✅-3-브라우저-api-예-matchmedia-구독-예제">✅ 3. 브라우저 API (예: matchMedia) 구독 예제</h3>
<p>미디어 쿼리를 반응형으로 다루고 싶을 때 활용합니다.</p>
<pre><code class="language-tsx">// 📁 useMediaQuery.ts
export function useMediaQuery(query: string): boolean {
  const subscribe = (callback: () =&gt; void) =&gt; {
    const media = window.matchMedia(query);
    media.addEventListener(&quot;change&quot;, callback);
    return () =&gt; media.removeEventListener(&quot;change&quot;, callback);
  };

  const getSnapshot = () =&gt; window.matchMedia(query).matches;
  const getServerSnapshot = () =&gt; false; // SSR 대비

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}</code></pre>
<pre><code class="language-tsx">// 📁 Component.tsx
import { useMediaQuery } from &quot;./useMediaQuery&quot;;

export default function ResponsiveBox() {
  const isMobile = useMediaQuery(&quot;(max-width: 768px)&quot;);

  return &lt;div&gt;{isMobile ? &quot;📱 모바일 화면&quot; : &quot;🖥️ 데스크탑 화면&quot;}&lt;/div&gt;;
}</code></pre>
<blockquote>
<p>💡 기존 useEffect + useState 방식보다 훨씬 간결하고, SSR에서도 안전하게 작동한다.</p>
</blockquote>
<h3 id="✅-4-서버-렌더링ssr-대응-예제-with-nextjs">✅ 4. 서버 렌더링(SSR) 대응 예제 (with Next.js)</h3>
<pre><code class="language-tsx">// 📁 useWindowWidth.ts
export function useWindowWidth(): number {
  const getSnapshot = () =&gt; window.innerWidth;
  const getServerSnapshot = () =&gt; 1024; // 서버 기본값 (예: 데스크탑)

  const subscribe = (callback: () =&gt; void) =&gt; {
    window.addEventListener(&quot;resize&quot;, callback);
    return () =&gt; window.removeEventListener(&quot;resize&quot;, callback);
  };

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}</code></pre>
<pre><code class="language-tsx">// 📁 Home.tsx (Next.js page)
import { useWindowWidth } from &quot;./useWindowWidth&quot;;

export default function Home() {
  const width = useWindowWidth();

  return (
    &lt;main&gt;
      &lt;h1&gt;현재 창 너비: {width}px&lt;/h1&gt;
    &lt;/main&gt;
  );
}</code></pre>
<blockquote>
<p>✅ Next.js 같은 SSR 환경에서는 반드시 getServerSnapshot을 넣어야 window 에러 없이 렌더링된다.</p>
</blockquote>
<h1 id="📖-참고문서">📖 참고문서</h1>
<p>React 공식 문서</p>
<blockquote>
<p><a href="https://ko.react.dev/reference/react/useSyncExternalStore#subscribing-to-a-browser-api">https://ko.react.dev/reference/react/useSyncExternalStore#subscribing-to-a-browser-api</a></p>
</blockquote>
<p>티어링 이슈 알아보기 &amp; 외부 상태 관리 라이브러리 구현 예시</p>
<blockquote>
<p><a href="https://ted-projects.com/react-use-sync-external-store">https://ted-projects.com/react-use-sync-external-store</a></p>
</blockquote>
<p>useSyncExternalStore 훅 보기 전에 먼저 알면 좋은 개념들</p>
<blockquote>
<p><a href="https://velog.io/@yeonoey/useSyncExternalStore%EB%9D%BC%EB%8A%94-%ED%9B%85%EC%9D%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94#usesyncexternalstore">https://velog.io/@yeonoey/useSyncExternalStore%EB%9D%BC%EB%8A%94-%ED%9B%85%EC%9D%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94#usesyncexternalstore</a></p>
</blockquote>
<p>하늘님이 작성해주신 useSyncExternalStore 포스트 인데, 설명이 이해하기 쉽게 잘 되어있다.</p>
<blockquote>
<p><a href="https://velog.io/@eveneul/React.js-useSyncExternalStore%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Zustand%EC%97%90%EC%84%9C-useSyncExternalStore%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D#-zustand-%EC%A0%84%EC%B2%B4-%ED%94%8C%EB%A1%9C%EC%9A%B0">https://velog.io/@eveneul/React.js-useSyncExternalStore%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Zustand%EC%97%90%EC%84%9C-useSyncExternalStore%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D#-zustand-%EC%A0%84%EC%B2%B4-%ED%94%8C%EB%A1%9C%EC%9A%B0</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프레임워크 없이 SPA 만들기[2주차] 회고]]></title>
            <link>https://velog.io/@hee0ne_2/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EC%9D%B4-SPA-%EB%A7%8C%EB%93%A4%EA%B8%B02%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee0ne_2/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EC%9D%B4-SPA-%EB%A7%8C%EB%93%A4%EA%B8%B02%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 20 Jul 2025 14:32:45 GMT</pubDate>
            <description><![CDATA[<h2 id="2주차-과제를-진행하면서-배운-것들">2주차 과제를 진행하면서 배운 것들</h2>
<h3 id="1️⃣-virtualdom">1️⃣ VirtualDOM?</h3>
<p>Virtual DOM은 실제 DOM과 똑같은 구조를 가진 메모리 상의 가상 트리다. 주로 React, Vue 등의 프레임워크에서 사용되며, 실제 DOM을 직접 조작하는 대신 Virtual DOM을 이용해 성능을 향상시킨다.</p>
<h3 id="🔍-실제-dom과-virtual-dom-비교">🔍 실제 DOM과 Virtual DOM 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>실제 DOM</th>
<th>Virtual DOM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>위치</strong></td>
<td>브라우저 내부</td>
<td>메모리 내부 (JS 객체)</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>느림 (무거움)</td>
<td>빠름 (가벼움)</td>
</tr>
<tr>
<td><strong>사용 비용</strong></td>
<td>높음 (재렌더링 부담)</td>
<td>낮음 (차이점만 계산해서 적용)</td>
</tr>
<tr>
<td><strong>업데이트 방식</strong></td>
<td>직접 조작</td>
<td>차이 계산 후 최소 변경</td>
</tr>
</tbody></table>
<h3 id="2️⃣-createvnode">2️⃣ CreateVNode</h3>
<ul>
<li>평탄화(flattening) 해주는 이유
중첩된 배열을 단일 배열로 펼쳐서 다루기 쉽게 만들기 위해서</li>
</ul>
<p>아래 코드를 보면</p>
<p>안에 자식 노드가 있다.
map은 배열을 반환하고</p>
<pre><code class="language-js">&lt;div&gt;
   &lt;div&gt;hello&lt;/div&gt;
   Array.map((name) =&gt; { &lt;div&gt;${name}&lt;/div&gt; });
&lt;/div&gt;
----
위와 같은 코드
&lt;div&gt;
   &lt;div&gt;hello&lt;/div&gt;
   &lt;button&gt;name1&lt;/button&gt;
   &lt;button&gt;name2&lt;/button&gt;
&lt;/div&gt;
---
[{ type: div , props: &#39;&#39;, children: &#39;hello&#39;}, [{.type: button, props: &#39;&#39;, children: name1 }, { .type: button, props: &#39;&#39;, children: name2  }]]
</code></pre>
<h3 id="3️⃣-normalizevnode">3️⃣ NormalizeVNode</h3>
<p>정규화를 해주는 이유 : 객체/배열 구조 데이터를 평탄하고 효율적으로 구성하는 것</p>
<pre><code class="language-js">// 정규화 전 (비정규화 상태)
// company 정보가 사용자마다 중복되어 있다. 이렇게 되면 수정 시 모든 곳을 찾아서 바꿔야 한다.
const users = [
  {
    id: 1,
    name: &quot;Alice&quot;,
    company: { id: 100, name: &quot;OpenAI&quot; }
  },
  {
    id: 2,
    name: &quot;Bob&quot;,
    company: { id: 100, name: &quot;OpenAI&quot; }
  }
];</code></pre>
<pre><code class="language-js">// 회사 정보가 한 곳에만 있어서 변경, 추적이 쉽고 실수도 줄어든다.
const users = {
  1: { id: 1, name: &quot;Alice&quot;, companyId: 100 },
  2: { id: 2, name: &quot;Bob&quot;, companyId: 100 }
};

const companies = {
  100: { id: 100, name: &quot;OpenAI&quot; }
};</code></pre>
<h3 id="4️⃣-diffing-알고리즘">4️⃣ Diffing 알고리즘</h3>
<h4 id="🔍-1-diffing-알고리즘이란">🔍 1. Diffing 알고리즘이란?</h4>
<p>간단히 말하면:</p>
<p><strong>이전 상태(Old State)</strong>와 <strong>새로운 상태(New State)</strong>를 비교해서,
&quot;무엇이 변경되었는지(diff)&quot;를 계산하는 알고리즘이다.</p>
<p>UI에서는 일반적으로 Virtual DOM을 비교할 때 사용되며,
React, Vue 등 대부분의 모던 프론트엔드 프레임워크에서 핵심 역할을 한다.</p>
<h4 id="🔧-2-왜-사용하는가">🔧 2. 왜 사용하는가?</h4>
<p>✅ 문제
브라우저의 DOM 조작은 느립니다.</p>
<p>상태가 바뀔 때마다 전체 DOM을 다시 렌더링하면 성능 저하가 크다.</p>
<p>✅ 해결 방법: Diffing
바뀐 부분만 찾아서(=diff)</p>
<p>해당 부분만 최소한의 DOM 조작으로 업데이트함</p>
<p>즉, 최적화된 UI 업데이트 방식
🧠 핵심 요약
| 항목            | 내용   |
|-----------------|----------------------------------------------------------------------|
| <strong>무엇인가?</strong>   | 두 구조 간 차이점을 찾아내는 알고리즘                                |
| <strong>왜 필요한가?</strong> | 성능 최적화를 위해 (전체 DOM 대신 변경된 부분만 업데이트)              |
| <strong>어디서 쓰나?</strong> | Virtual DOM 기반 UI 라이브러리 (React, Vue 등)                   |
| <strong>이점은?</strong>     | 빠른 렌더링, 부드러운 사용자 경험                                   |</p>
<h3 id="5️⃣-map--weakmap">5️⃣ Map , WeakMap</h3>
<p><strong>eventManager</strong></p>
<ul>
<li>이벤트 위임
Map, eventMap
JavaScript에서 키-값 쌍을 저장하는 자료구조지만, 사용 목적과 내부 동작 방식에 큰 차이<h4 id="📊-map-vs-weakmap-차이-요약">📊 Map vs WeakMap 차이 요약</h4>
</li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th><code>Map</code></th>
<th><code>WeakMap</code></th>
</tr>
</thead>
<tbody><tr>
<td>키 타입</td>
<td>객체, 원시값 모두 가능</td>
<td>객체만 가능</td>
</tr>
<tr>
<td>반복</td>
<td>가능</td>
<td>불가능</td>
</tr>
<tr>
<td>크기 확인</td>
<td><code>.size</code> 가능</td>
<td>불가능</td>
</tr>
<tr>
<td>GC 영향</td>
<td>수동 제거 필요</td>
<td>자동 제거 (키가 없으면 값도 제거됨)</td>
</tr>
<tr>
<td>사용 목적</td>
<td>일반적인 키-값 저장</td>
<td>프라이빗 데이터나 메모리 누수 방지 등</td>
</tr>
</tbody></table>
<p>💡 왜 WeakMap을 쓰는가?
메모리 누수 방지 (가비지 컬렉션)
DOM 요소가 삭제되면 WeakMap의 키(= 요소)에 대한 참조도 사라짐
그러면 JavaScript 엔진이 해당 entry를 자동으로 메모리에서 제거함</p>
<pre><code class="language-js">const el = document.createElement(&quot;div&quot;);
addEvent(el, &quot;click&quot;, handler);
document.body.removeChild(el); // el DOM에서 제거됨</code></pre>
<p>만약 Map을 사용했다면 eventMap이 el을 계속 참조 → 메모리 누수 발생
하지만 WeakMap은 el이 참조되지 않으면 자동으로 해당 데이터 제거됨</p>
<p>참고링크 : <a href="https://devocean.sk.com/blog/techBoardDetail.do?ID=165601">https://devocean.sk.com/blog/techBoardDetail.do?ID=165601</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프레임 없이 SPA 만들기 [1주차] 회고]]></title>
            <link>https://velog.io/@hee0ne_2/%ED%94%84%EB%A0%88%EC%9E%84-%EC%97%86%EC%9D%B4-SPA-%EB%A7%8C%EB%93%A4%EA%B8%B0-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hee0ne_2/%ED%94%84%EB%A0%88%EC%9E%84-%EC%97%86%EC%9D%B4-SPA-%EB%A7%8C%EB%93%A4%EA%B8%B0-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 12 Jul 2025 21:37:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1주차-과제를-진행하면서-겪은-문제-😵💫">1주차 과제를 진행하면서 겪은 문제 😵‍💫</h2>
<h3 id="1️⃣-spa-구조-설계-생각보다-훨씬-복잡했다">1️⃣ SPA 구조 설계, 생각보다 훨씬 복잡했다</h3>
<p>이번 과제에서는 SPA(Single Page Application) 구조를 설계하는 과정에서 많은 어려움을 겪었다. 특히 SPA의 전반적인 구조를 어떻게 구성해야 할지에 대한 감이 전혀 잡히지 않았다.</p>
<h3 id="2️⃣-라우터-구현">2️⃣ 라우터 구현</h3>
<p>라우팅 시스템을 직접 구현해야 했기 때문에 라우터에 대한 개념이 부족하여 초반에 한참 헤맸다.</p>
<h3 id="3️⃣-부족한-개념들">3️⃣ 부족한 개념들</h3>
<p>더불어 옵저버 패턴, 컴포넌트의 생애주기, 상태 관리와 같은 프론트엔드 개발의 핵심 개념들에 대한 이해도 부족해서, 전체적인 흐름을 설계하고 구현하는 데 특히 어려움을 느꼈다.</p>
<p>이러한 개념들이 제대로 정립되지 않다 보니, 상태가 변경될 때 화면을 어떻게 렌더링해야 하는지, 어떤 구조로 코드를 나누고 흐름을 설계해야 하는지 명확하지 않아 어려움을 크게 느꼈다.</p>
<h2 id="문제를-해결하기-위해-노력한-점">문제를 해결하기 위해 노력한 점</h2>
<h3 id="1️⃣-처음에는-혼자-문제를-해결하기-위해-관련-개념을-찾아보았다">1️⃣ 처음에는 혼자 문제를 해결하기 위해 관련 개념을 찾아보았다.</h3>
<p>하지만 개념을 이해하고 코드에 적용하려고 해도 정확한 이해가 부족했는지 제대로 활용하지 못했고, 시간이 점점 흘러갔다.
그래서 다른 사람이 작성한 코드를 찾아보고 어떻게 구현했는지 자세히 살펴본 뒤, 그 내용을 참고하여 내 코드에 적용하려고 노력했다.</p>
<h3 id="2️⃣-물음표-살인마가-되어-다른-사람들에게-질문하기">2️⃣ 물음표 살인마가 되어, 다른 사람들에게 질문하기</h3>
<p>타인의 코드가 이해되지 않을 때는 직접 코드를 작성한 사람 또는 다른 사람들에게 코드의 흐름 및 설명을 요청했고, 모르는 개념이 나오면 질문하며 개념에 대한 이해를 높이기 위해 노력했다.</p>
<h2 id="문제를-어떻게-해결했는가">문제를 어떻게 해결했는가?</h2>
<h3 id="1️⃣-spa-구조-설계--폴더-구조만이라도-잡기">1️⃣ SPA 구조 설계 : 폴더 구조만이라도 잡기</h3>
<p>SPA 구조 설계에서 가장 먼저 폴더 구조부터 명확하게 정의하는 작업을 진행했다.
UI 컴포넌트를 어떻게 효율적으로 분리할지 고민하며, 컴포넌트 기반 아키텍처 원칙에 따라 폴더와 파일을 체계적으로 나누어 전체 프로젝트의 뼈대를 구성했다.
[참고문서] (<a href="https://velog.io/@teo/folder-structure#%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%8F%85%EB%A6%BD">https://velog.io/@teo/folder-structure#%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EB%8F%85%EB%A6%BD</a>)</p>
<h3 id="2️⃣-라우터-구현--url을-직접-변경해주고-변경된-url에-따라-화면을-재렌더링하는-방식을-사용">2️⃣ 라우터 구현 : URL을 직접 변경해주고, 변경된 URL에 따라 화면을 재렌더링하는 방식을 사용</h3>
<p>옵저버 패턴 등 복잡한 상태 관리에 대한 이해가 부족해 이를 도입하지 못했고, 대신 이벤트 핸들러에서 상태가 변경될 때마다 수동으로 렌더링을 실행하는 방식으로 문제를 해결했다.</p>
<h2 id="과제를-통해-알게-된-개념">과제를 통해 알게 된 개념</h2>
<h3 id="1-옵저버-패턴">1. 옵저버 패턴</h3>
<p>옵저버 패턴(observer pattern)은 주체가 어떤 객체(subject)의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
<img src="https://velog.velcdn.com/images/hee0ne_2/post/30a86857-c297-45d1-bc3b-798912c205b3/image.png" width="400px" />
여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 ‘추가 변화 사항’이 생기는 객체들을 의미한다.
<img src="https://velog.velcdn.com/images/hee0ne_2/post/827a21ec-d36c-4d9f-9bd4-4257913f9125/image.png" width="400px" /></p>
<p>🧠 주요 개념 요약
<strong>Observable</strong>: 상태를 가지고 있고, 상태 변경 시 구독자에게 알림을 보냄</p>
<p><strong>Observer</strong>: 상태가 변경되면 알림을 받고 동작하는 쪽</p>
<p><strong>subscribe()</strong>: 구독자 등록</p>
<p><strong>notify()</strong>: 구독자들에게 알림 전송</p>
<p>예시 코드 </p>
<pre><code class="language-js">// Subject (관찰 대상)
class Observable {
  constructor() {
    this.observers = []; // 구독자 리스트
    this.state = null;
  }

  // 구독자 추가
  subscribe(observer) {
    this.observers.push(observer);
  }

  // 구독자 제거
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs =&gt; obs !== observer);
  }

  // 상태 변경 및 알림
  setState(newState) {
    this.state = newState;
    this.notify();
  }

  // 구독자들에게 알림 전송
  notify() {
    this.observers.forEach(observer =&gt; observer.update(this.state));
  }
}
</code></pre>
<p>과제에서 사용한 예시</p>
<ul>
<li><p>src/utils/createObserver.js</p>
<pre><code class="language-js">export const createObserver = () =&gt; {
const observers = new Set();

const subscribe = (callback) =&gt; {
  observers.add(callback);
};

const unsubscribe = (callback) =&gt; {
  observers.delete(callback);
};

const notify = (...args) =&gt; {
  observers.forEach((callback) =&gt; callback(...args));
};

return { subscribe, unsubscribe, notify };
};</code></pre>
</li>
<li><p>src/utils/createState.js</p>
<pre><code class="language-js">import { createObserver } from &quot;./createObserver.js&quot;;
</code></pre>
</li>
</ul>
<p>export function createState(initialState) {
  let state = { ...initialState };
  const observer = createObserver();</p>
<p>  const subscribe = (callback) =&gt; {
    observer.subscribe(callback);
    return () =&gt; observer.unsubscribe(callback);
  };</p>
<p>  const setState = (newState) =&gt; {
    const prevState = { ...state };
    state = { ...state, ...newState };</p>
<pre><code>observer.notify(state, prevState);</code></pre><p>  };</p>
<p>  const getState = () =&gt; ({ ...state });</p>
<p>  return {
    subscribe,
    setState,
    getState,
  };
}</p>
<p>```</p>
<h2 id="목표-달성--과제-후-느낀-점">목표 달성 &amp; 과제 후 느낀 점</h2>
<p>기존 목표 : 기본 과제 통과 (기본 기능 모두 구현하기)
변경 목표 : 기본 과제에서 최대한 많은 기능을 구현하기 (통과 여부 신경 X)</p>
<h4 id="느낀점">느낀점</h4>
<p>이번 과제를 하면서 가장 크게 느낀 점은, 혼자 고민하기보다 함께 문제를 해결해 나가는 과정 속에서 많은 도움을 받을 수 있었고, 그만큼 배움도 더 깊어졌다는 것이다.</p>
<p>이번 과제에는 심화 과제도 있었지만, 개념이 제대로 잡히지 않았던 나는 우선 기본 과제부터 완성하자는 마음으로 시작했다.
하지만 과제를 처음 시작할 때는 막막함이 컸고, 혼자 해결해보려다 보니 약 이틀 동안 거의 진행이 되지 않았다.
‘기본 과제조차 나 혼자 끝낼 수 있을까?’ 하는 걱정이 들기도 했다.</p>
<p>그래도 포기하지 않고 하나씩 해결해보기로 마음먹고, 다른 사람들에게 기능 구현에 필요한 개념이나 방법을 질문하면서 차근차근 과제를 진행해 나갔다.
진행 속도는 느렸지만, 그 과정에서 단순히 문제를 해결하는 것보다도 이전에 잘 몰랐던 개념들을 이해해가며 기능을 구현하는 경험 자체가 더 중요하다는 것을 느꼈다.
무작정 구현하기보다는, 개념을 정확히 이해하고 직접 적용해보는 과정이 훨씬 의미 있었다.</p>
<p>과제 제출일이 다가왔을 때는 <strong>&quot;하나라도 더 구현해보자&quot;</strong>는 마음으로 최대한 마무리를 지었다.
비록 모든 기능을 완성하지는 못했지만, 하나의 기능을 제대로 구현하기 위해 그 동작 원리를 이해하고 직접 개발해보는 과정 자체가 큰 배움의 시간이 되었다.</p>
<h2 id="다음-목표-설정">다음 목표 설정</h2>
<ul>
<li>시간 분배를 잘 하자!</li>
<li>과제에서 필요한 개념을 선수학습하자!</li>
<li>2주차 과제는 무조건 통과를 목표로 잡기!</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>