<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>dev-daejlee</title>
    <link>https://dev-daejlee.tistory.com/</link>
    <description>개발하며 겪은 것들을 공유합니다.</description>
    <language>ko</language>
    <pubDate>Tue, 26 May 2026 23:13:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Daejlee</managingEditor>
    <image>
      <title>dev-daejlee</title>
      <url>https://tistory1.daumcdn.net/tistory/6865493/attach/696326b05af84ee5a73d185e32d5563e</url>
      <link>https://dev-daejlee.tistory.com</link>
    </image>
    <item>
      <title>[FE] Event Dispatcher 패턴으로 중앙에서 이벤트 관리하기</title>
      <link>https://dev-daejlee.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 만들다 보면 복잡도가 증가하게 되어있는데, 이때 이벤트 관리 또한 유지보수가 힘든 부분 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 이벤트 디스패처 패턴이 도움이 될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Event Dispatcher 패턴&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddOVcW/dJMcaii8QKW/c4jm6cpKiiWQB1H9Mh6UEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddOVcW/dJMcaii8QKW/c4jm6cpKiiWQB1H9Mh6UEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddOVcW/dJMcaii8QKW/c4jm6cpKiiWQB1H9Mh6UEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddOVcW%2FdJMcaii8QKW%2Fc4jm6cpKiiWQB1H9Mh6UEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;339&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 디스패처 패턴&lt;/b&gt;은 Publisher-Subscriber 구조를 기반으로 발행자와 구독자 사이에 Dispatcher를 두어 둘을 완전히 분리하는 설계 패턴을 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Publisher&lt;/b&gt;: 이벤트를 발생시키는 주체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event Dispatcher&lt;/b&gt;: 이벤트를 수신하고, 등록된 리스너 목록을 보고 해당 이벤트를 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Subscriber&lt;/b&gt;: 특정 이벤트를 구독하고, 해당 이벤트가 오면 콜백을 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 사용하는건가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 A &amp;rarr; B로 호출하는 경우 A, B 둘이 강하게 결합된다. 하지만 이벤트 디스패처 패턴을 활용하면 Publisher와 Subscriber가 서로 몰라도 되므로 코드 결합도가 낮아지고, 새 구독자를 추가할 때 기존 코드를 건드리지 않아도 된다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;활용 예시&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwV3Y2/dJMcaayFUpz/OQsd43ucx2uFGPt02NKPZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwV3Y2/dJMcaayFUpz/OQsd43ucx2uFGPt02NKPZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwV3Y2/dJMcaayFUpz/OQsd43ucx2uFGPt02NKPZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwV3Y2%2FdJMcaayFUpz%2FOQsd43ucx2uFGPt02NKPZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;444&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진과 같이 앱을 개발중인 상황,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 컴포넌트 내부의 초록색 버튼과 C 컴포넌트 내부의 초록색 버튼을 눌렀을 때 특정 위치로 스크롤이 되어야 한다는 요구사항을 만족해야 한다고 가정하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 디스패처 없이 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1779705260672&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Header.tsx
function Header() {
  const handleScroll = () =&amp;gt; {
    window.scrollBy({ top: 500, behavior: 'smooth' });
  };
  return (
    &amp;lt;header&amp;gt;
      &amp;lt;button onClick={handleScroll}&amp;gt;▼&amp;lt;/button&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}

// C.tsx
function C() {
  const handleScroll = () =&amp;gt; {
    window.scrollBy({ top: 500, behavior: 'smooth' });  // 로직 중복
  };
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleScroll}&amp;gt;▼&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로따로 구현했을 때의 케이스다. 스크롤 동작이 바뀌면 (예: 스크롤 정도, easing, 대상 엘리먼트) 두 군데를 다 고쳐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재로써는 이걸로도 충분하지만 뎁스가 깊어지거나 동일 이벤트를 다른 지점에서 추가해야 하는 경우 N번의 로직 중복이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수성이 좋다고 할 수 없다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;커스텀 디스패처를 통해 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1779705503373&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// dispatcher.ts
const listeners = new Map&amp;lt;string, Set&amp;lt;Function&amp;gt;&amp;gt;();

export const dispatcher = {
  on(event: string, handler: Function) {
    if (!listeners.has(event)) listeners.set(event, new Set());
    listeners.get(event)!.add(handler);
    return () =&amp;gt; listeners.get(event)!.delete(handler);
  },
  
  emit(event: string, payload?: any) {
    listeners.get(event)?.forEach(fn =&amp;gt; fn(payload));
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 전역적으로 사용할 디스패처를 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 구독자들에 대해 이벤트를 발행하는 로직만을 가진 디스패처다.&lt;/p&gt;
&lt;pre id=&quot;code_1779705612441&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Header.tsx
function Header() {
  return (
    &amp;lt;button onClick={() =&amp;gt; dispatcher.emit(SCROLL_DOWN)}&amp;gt;▼&amp;lt;/button&amp;gt;
  );
}

// C.tsx
function C() {
  return (
    &amp;lt;button onClick={() =&amp;gt; dispatcher.emit(SCROLL_DOWN)}&amp;gt;▼&amp;lt;/button&amp;gt;
  );
}

// ScrollController.tsx
export const SCROLL_DOWN = 'scroll:down';

function ScrollController() {
  useEffect(() =&amp;gt; {
    return dispatcher.on(SCROLL_DOWN, () =&amp;gt; {
      window.scrollBy({ top: 500, behavior: 'smooth' });
    });
  }, []);

  return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 디스패처 패턴을 사용하며 코드 복잡도가 줄어들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용처인 컴포넌트에서는 디스패처만을 import하여 해당하는 이벤트를 발행하면 된다.&lt;/li&gt;
&lt;li&gt;구독부인 ScrollController 파일에서는 이벤트 자체 로직을 정의하고, 구독시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 이벤트들을 구분할 키 값으로 상수 매크로를 선언하여 export하면 유지보수성을 더욱 높일 수 있다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>Event Dispatcher</category>
      <category>이벤트 관리</category>
      <category>커스텀 이벤트</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/44</guid>
      <comments>https://dev-daejlee.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 25 May 2026 19:49:50 +0900</pubDate>
    </item>
    <item>
      <title>AI를 대하며 느끼는 것들</title>
      <link>https://dev-daejlee.tistory.com/43</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJRQN/dJMcaflVs5z/KocnNKsChiYpfFSAxR9qYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJRQN/dJMcaflVs5z/KocnNKsChiYpfFSAxR9qYK/img.png&quot; data-alt=&quot;Anthropic이 AI의 보안 위협에 대해 내놓은 Project Glasswing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJRQN/dJMcaflVs5z/KocnNKsChiYpfFSAxR9qYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAJRQN%2FdJMcaflVs5z%2FKocnNKsChiYpfFSAxR9qYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;338&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Anthropic이 AI의 보안 위협에 대해 내놓은 Project Glasswing&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 4월 Anthropic이 mythos라는 새 모델의 벤치마킹 결과를 내놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 이 AI 모델이 얼마나 보안 취약점을 잘 찾아내며, 안전장치들을 우회하는데 뛰어난 지에 대해서도 공유했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 모델은 너무나 뛰어나고 위험해서, 지금은 공개할 수 없고 아마존, 구글 등의 회사에 소수의 인원에게만 공개한 상태이다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 세상의 종말이 다가오는 것처럼 말하는데, 공포를 이용한 마케팅인지. 아니면 정말로 &quot;위험한 녀석&quot;이라서 그런 것인지는 모르겠지만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 급속도로 발전하면서 개발자로써 느껴지는 특유의 피로감에 대해 다시 생각하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미친듯이 빠르게 흐르는 AI의 흐름에서 뒤쳐지는 것에 대한 불안감이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 AI의 발전에 대해 항상 &quot;우린 얼마 안남았다&quot;, &quot;너무 위험한 모델이 나왔다&quot; 등등 부정적인 시각으로 다뤄지는 케이스가 많기에, 이런 새로운 소식이 나올 때 마다 기쁨보다는 걱정이 앞서며 피로감을 느끼게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자라면 항상 성장해야 한다는 압박은 누구나 있는 거 아냐? 라고 할 수 있겠지만, 압박의 정도가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;새로운 FE 프레임워크가 나왔는데 리액트는 쉽게 대체할 거야~&quot; 라는 소식은 질리게 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 대체가 되었는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 내가 가진 기술과 지식에 대한 확신을 유지한채 추가적인 지식을 얻으면 좋은 정도였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금은? 1달 전의 기술과 지금의 기술의 차이가 크다. 실제로 잘 쓰느냐 못 쓰느냐에 따라 업무 효율이 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 하던 기존 업무의 상당 부분이 &quot;실제로 대체된다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 정말 대체되는건가? 라는 질문에는 난 아직까지는 아니라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적 깊이가 없고, 사고하는 것을 귀찮아하는 개발자와 그렇지 않은 개발자에게 각각 에이전트를 주어줬을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 다르지 않다면 모든 개발자는 대체될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇던가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사고가 필요한, 난이도가 높은 작업을 하려고 한다면 개발자는 기본적으로 이것을 이해하고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것에 관련된 구현사항과 기술을 이해하고, 변경할 줄 아는 상태에서 AI의 고삐를 잡아야 AI를 바른 곳으로 끌고갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않은 개발자가 고삐를 잡게 된다면 도랑에 빠져서 허우적대게 된다. (내가 자주 이런다..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 AI의 발전에 대한 기사가 나왔을 때 두려워하지 않고, 기뻐할 수 있게..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 그래왔던 것 처럼 개발자는 즐거워하며 성장하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 그렇게 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌었던 영상: &lt;a href=&quot;https://www.youtube.com/watch?v=XRgGFQ0EgM0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=XRgGFQ0EgM0&lt;/a&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/43</guid>
      <comments>https://dev-daejlee.tistory.com/43#entry43comment</comments>
      <pubDate>Sun, 12 Apr 2026 22:05:05 +0900</pubDate>
    </item>
    <item>
      <title>[Fluent React] 서버 사이드 리액트</title>
      <link>https://dev-daejlee.tistory.com/42</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전문가를 위한 리액트(Fluent-React)를 읽고 서버 사이드 리액트에 대해 정리합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 클라이언트 사이드 라이브러리로 시작했습니다. 이 방식은 웹 생태계에 성능이 뛰어나고 반응이 빠른 사용자 경험을 구현했습니다. 하지만 웹이 발전함에 따라 클라이언트 사이드 렌더링의 한계가 명확해졌습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CSR의 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. SEO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR의 가장 큰 한계 중 하나는 검색 엔진의 크롤러가 JS를 실행하지 않아 콘텐츠를 색인하기 어렵다는 점이었습니다. 실행되더라도 예상대로 실행되지 않을 수 있어 콘텐츠를 올바르게 색인할 수 못할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 검색 엔진의 크롤러의 구현 방식이 다양하고, 상당수가 폐쇄적이며 공개되지 않아 더욱 명확하지 않은 부분이 되었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2015년 검색 엔진 랜드가 기사를 통해 다양한 검색 엔진이 CSR 앱에서 어떻게 동작하는지에 대한 실험에 대해 밝혔는데,&lt;br /&gt;일련의 테스트를 통해 구글이 다양한 구현 방식으로 JS를 실행하고 색인할 수 있다는 사실을 확인했습니다. 또한, 구글이 전체 페이지를 렌더링하고 DOM을 읽어 동적으로 생성된 컨텐츠를 색인할 수 있다는 사실도 확인했습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 기사는 구글과 빙(Bing)이 CSR 앱을 색인할만큼 발전했다는 것을 나타내지만, 방대한 비공개 지식의 조각에 불과한 연구 프로젝트일 뿐, 아직 우리가 알지 못하는 부분이 훨씬 큽니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR 앱은 클라이언트에서 렌더링되기에, 느린 네트워크 환경이나 낮은 성능의 기기 환경에서 문제를 겪을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 넓은 관점에서 다시 보면, CSR 앱의 SEO와 성능 최적화 문제는 웹 표준과 모범 사례를 준수하는 것이 얼마나 중요한지 다시 보여줍니다. 특히 콘텐츠가 많은 앱의 경우, 성능이 뛰어나고 접근성이 좋은 방식으로 컨텐츠를 제공하고자 할 때 SSR, 혹은 SSG가 더 신뢰할 수 있는 대안임을 가르키고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 사용자 경험 전체가 CSR의 JS로만 구성되는건 웹의 설계 의도에 부합하지 않습니다. JS의 역할은 웹 페이지를 보완하고 개선하는 것이지, 그 자체가 되어버리는 것이 아닙니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t387B/dJMcaaLpw8H/rwlhs1nQ83dooBVesjzHL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t387B/dJMcaaLpw8H/rwlhs1nQ83dooBVesjzHL1/img.png&quot; data-alt=&quot;CSR 앱의 네트워크 워터폴&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t387B/dJMcaaLpw8H/rwlhs1nQ83dooBVesjzHL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft387B%2FdJMcaaLpw8H%2Frwlhs1nQ83dooBVesjzHL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;164&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CSR 앱의 네트워크 워터폴&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR 앱에서 흔히 나타나는 네트워크 워터폴입니다. 네트워크 연결이 제한적인 경우에는 애플리케이션이 상당한 시간동안 응답이 전혀 없을 수 있습니다. SSR을 이용하면 사용자가 컨텐츠를 즉시 볼 수 있도록 개선할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdRg8l/dJMcaduxard/ttWCteW59WJlkqVzlkjcAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdRg8l/dJMcaduxard/ttWCteW59WJlkqVzlkjcAK/img.png&quot; data-alt=&quot;SSR 앱은 사용자가 컨텐츠를 즉시 볼 수 있도록 개선합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdRg8l/dJMcaduxard/ttWCteW59WJlkqVzlkjcAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdRg8l%2FdJMcaduxard%2FttWCteW59WJlkqVzlkjcAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;58&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SSR 앱은 사용자가 컨텐츠를 즉시 볼 수 있도록 개선합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 데이터를 가져와 서버에서 렌더링했기 때문에 처음 페이지를 로딩하는 순간부터 컨텐츠를 담고 있습니다. 네트워크 워터폴이 존재하지 않으며, 사용자는 모든 정보를 즉각적으로 얻습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwp2Uv/dJMcaivR0vI/j1kQpv5WMeBQxRV2LRILGK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwp2Uv/dJMcaivR0vI/j1kQpv5WMeBQxRV2LRILGK/img.jpg&quot; data-alt=&quot;하이드레이션을 기다리는 HTML이 생각나서 가져왔습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwp2Uv/dJMcaivR0vI/j1kQpv5WMeBQxRV2LRILGK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwp2Uv%2FdJMcaivR0vI%2Fj1kQpv5WMeBQxRV2LRILGK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;270&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하이드레이션을 기다리는 HTML이 생각나서 가져왔습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SSR된 HTML은 정적이며, JS를 불러오지 않은 상태여서 상호 작용 지원이 부족합니다. 사용자 상호 작용을 비롯한 동적 기능을 활성화하기 위해, 필요한 JS를 정적 HTML에 &quot;촉촉&quot;하게 공급해주어야 합니다. 하이드레이션이 필요합니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 하이드레이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이드레이션은 서버에서 생성되어 클라이언트로 전송되는 정적 HTML에 이벤트 리스너와 여러 JS 기능을 추가하는 프로세스를 의미하는 용어입니다. 하이드레이션의 목적은 브라우저가 서버 렌더링 애플리케이션을 읽어 들인 후 여기에 상호 작용을 추가해서 사용자에게 빠르고 원활한 경험을 제공하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 하이드레이션 과정을 통해 정적 HTML의 DOM 구조를 리액트 컴포넌트의 JSX 구조와 일치시킵니다. 이 부분은 매우 중요한데, 불일치하면 리액트는 이벤트 리스너를 올바르게 연결할 수 없으며 리액트 엘리먼트가 어떤 DOM 엘리먼트에 직접 매핑되어야 하는지 인식하지 못해 애플리케이션이 예상대로 동작하지 않게 됩니다. (이를 &quot;하이드레이션 에러&quot;라고 부릅니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 하이드레이션에 대한 비판&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjqCfa/dJMcabXQ1dN/S10LGiaD7GdkKCJis1nCc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjqCfa/dJMcabXQ1dN/S10LGiaD7GdkKCJis1nCc1/img.png&quot; data-alt=&quot;하이드레이션 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjqCfa/dJMcabXQ1dN/S10LGiaD7GdkKCJis1nCc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjqCfa%2FdJMcabXQ1dN%2FS10LGiaD7GdkKCJis1nCc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;242&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하이드레이션 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부는 하이드레이션이 필요 이상으로 느리다며, 재개 가능성(resumability)을 대안으로 꼽습니다. 하이드레이션은 SSR의 결과물로 HTML을 렌더링한 후 JS 번들을 다운로드, 이벤트 리스너를 추가하는 작업을 거쳐야 합니다. 많은 작업을 필요로 하며 콘텐츠가 표시되는 시점과 사용자가 실제로 사이트를 사용할 수 있는 지점 사이에 지연이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8Rz0c/dJMcadOP4ZK/7kgYJjUlR9RGNLK8kn0nC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8Rz0c/dJMcadOP4ZK/7kgYJjUlR9RGNLK8kn0nC1/img.png&quot; data-alt=&quot;재개 가능성 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8Rz0c/dJMcadOP4ZK/7kgYJjUlR9RGNLK8kn0nC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8Rz0c%2FdJMcadOP4ZK%2F7kgYJjUlR9RGNLK8kn0nC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;245&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재개 가능성 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재개 가능성을 활용하면 전체 앱이 서버에서 렌더링되고, 브라우저로 스트리밍됩니다. 여기서 하이드레이션과 다른 점은, 모든 인터렉티브 동작이 직렬화되어 브라우저에 전송된다는 점입니다. 이 동작들은 하이드레이션 없이 역직렬화 후 반응합니다. 이로 인해 TTI(Time to Interactive)가 짧아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 재개 가능성에 장점이 있음은 자명하지만, 구현에 따르는 복잡성을 감수할 가치가 있냐고 개발자 커뮤니티는 묻습니다. 하이드레이션보다 더 복잡한 접근 방식이며, 장점이 비용보다 더 큰지는 명확하지 않습니다. 인터렉티브 시간이 몇 밀리초 빠르긴 하지만, 복잡성을 감수하면서 재개 가능성을 구현할 가치가 있는지는 아직 논란이 되고 있습니다.&lt;/p&gt;</description>
      <category>개발 서적 기록</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/42</guid>
      <comments>https://dev-daejlee.tistory.com/42#entry42comment</comments>
      <pubDate>Sun, 22 Mar 2026 19:46:45 +0900</pubDate>
    </item>
    <item>
      <title>[함께 자라기]를 읽고</title>
      <link>https://dev-daejlee.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drquwN/dJMcajnuIa7/tpTKRowrpkkHNkwi8qG8j1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drquwN/dJMcajnuIa7/tpTKRowrpkkHNkwi8qG8j1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drquwN/dJMcajnuIa7/tpTKRowrpkkHNkwi8qG8j1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrquwN%2FdJMcajnuIa7%2FtpTKRowrpkkHNkwi8qG8j1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;307&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김창준 저자의 &quot;함께 자라기&quot;라는 책을 읽었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;애자일로 가는 길&quot;이 부제로 적혀있는데, 막상 책의 내용은 성장 방법론, 일하는 방식 등의 내용이 주를 이루고 있습니다. 책의 핵심적인 내용은 &lt;b&gt;&quot;어떻게 다같이(함께) 성장(자라기)할 것인가?&quot;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 목차는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;자라기&lt;/li&gt;
&lt;li&gt;함께&lt;/li&gt;
&lt;li&gt;애자일&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 1챕터인 &quot;자라기&quot; 챕터를 인상깊게 읽었습니다. 이 부분을 기록합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 자라기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;자라기&quot; 챕터는 어떻게 성장을 할 수 있는지, 성장에 대한 오해들을 깨는 것에 집중합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 오해인 &quot;경력은 실력과 비례한다&quot;는 주장을 반박하며 챕터를 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 업무 퍼포먼스를 통계적으로 나타냈을 때, 경력이 실력과 그리 비례하지 않으며 실력을 키우기 위해선 경력이 아닌 &quot;의도적 수련&quot;이 동반되어야 한다고 주장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일례로 사람은 매일 양치질을 반복하는데, 왜 시간이 지날수록 양치질의 달인이 되지 않느냐는 말이었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;골프 퍼팅 연습을 하는데, 공이 어디로 가는지 전혀 보지 않고 1,000개의 공을 친다고 생각해 보죠. 이건 도대체 뭘 연습하고 있는 걸까요? 뭔가 연습이 되긴 하겠죠. 하지만 정확하게 퍼팅하는 부분은 연습이 되질 않을 겁니다. 내가 잘했나 못 했나 알지 못하면 행동을 조정할 수가 없죠. 그래서 학습에서는 피드백이 중요합니다. -p.28-29&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적 수련이란 자신의 약점을 지속적으로 개선시키려 노력하는 것을 의미하는데, 애자일적인 방법을 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 실력을 개선하려는 확실한 동기가 필요하며, &lt;b&gt;구체적인 피드백을 적절한 시기&lt;/b&gt;에 받을 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피드백을 기반으로 약점을 개선하려 노력하고, 또 다시 피드백을 받습니다. 이 사이클을 반복하여 실력을 향상시킬 수 있다고 말합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실력이 제자리인 이유&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnZu2X/dJMcah4luNZ/a7yWL8Bkk2UtRVQ0V1S6k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnZu2X/dJMcah4luNZ/a7yWL8Bkk2UtRVQ0V1S6k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnZu2X/dJMcah4luNZ/a7yWL8Bkk2UtRVQ0V1S6k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnZu2X%2FdJMcah4luNZ%2Fa7yWL8Bkk2UtRVQ0V1S6k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;290&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;자신이 업무 시간 중에 불안함이나 지루함을 느끼는 때가 대부분이라면, 실력이 도무지 늘지 않는 환경에 있는 겁니다. 더 무서운 건 점차 이런 환경에 익숙해지고 행동이 습관화된다는 점이죠. 그때는 자기 인식도 잘 되지 않습니다. -p.64&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 효과적인 학습은 몰입하는 환경에서 발생합니다. 평소에 몰입하지 못한다면, 그 이유에는 크게 2가지가 있을 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업에서 지루함을 느끼는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;a1: 실력 낮추기(?)&lt;br /&gt;작업에 비해 실력이 높아 지루함을 느끼게 된다면, 내 실력을 낮추는 게 도움이 될 수도 있습니다. 모래주머니를 달고 작업하는 것입니다. 평소에 사용하던 AI 에이전트를 사용하지 않고 제시간 안에 작업을 끝내려 시도한다던지, 기존과 아예 다른 방식으로 작업을 해결하려 시도한다던지 등으로요.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;a2: 작업 난이도 높이기&lt;br /&gt;혹은 작업의 난이도를 높이는 방법이 있습니다. 기존의 1시간 분량의 작업을 30분만에 끝내려고 시도한다던지, 공식적으로 하지 않아도 되는 일을 스스로 한다던지 등으로요. 혹은 나만의 도구를 만들어 활용하는 것도, 전문성의 척도로 중요하게 작용하는 부분이라고 합니다. 작업의 반복 패턴을 파악하고 분석하여 부족한 시간에도 짬을 내어 도구를 만들어 내는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업에서 불안함을 느끼는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;b2: 실력 높이기&lt;br /&gt;장기적으로 실력을 높이는 방법은 많지만, 지금 당장 불안함을 느낀다면 시도해볼 방법들이 존재합니다.&lt;br /&gt;주변 동료의 도움을 구하거나, 오픈소스 라이브러리의 도움을 받는다던가, 비슷한 일을 했던 경험을 되살려 비슷하게 해결하려 시도하는 것입니다. 이 경우는 실제로 실력이 올라가지는 않지만 몰입 영역으로 들어가기 쉽게 만들어줍니다.&lt;/li&gt;
&lt;li&gt;b1: 작업 난이도 낮추기&lt;br /&gt;작업의 난이도를 낮추는 방법 중 강력한 방법은, 만들 작업물의 가장 기초적인 버전을 만들어보는 것입니다. 어려운 문제와 쉬운 문제가 존재할 때, 쉬운 문제부터 해결하고 어려운 문제를 해결하는 것이 몰입하기 쉬운 방법입니다. 이 경우 어려운 것을 먼저 해결했을 때 보다 결함 수도 현저히 적어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실수는 예방이 아니라 관리하는 것&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여러분이 실수에 대해 갖는 생각은 어떻습니까?&lt;br /&gt;어떻게든 피해야하고 숨겨야 할 망신에 가깝습니까, 아니면 좋은 학습의 기회에 가깝습니까? -p.88&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhfmif/dJMcaiIT4dO/qePBPYXBD3aFIYTLSkYTVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhfmif/dJMcaiIT4dO/qePBPYXBD3aFIYTLSkYTVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhfmif/dJMcaiIT4dO/qePBPYXBD3aFIYTLSkYTVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhfmif%2FdJMcaiIT4dO%2FqePBPYXBD3aFIYTLSkYTVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;269&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수를 대하는 태도에 2가지가 존재합니다. &quot;실수 예방&quot;과 &quot;실수 관리&quot;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수 예방은 실수를 하지 않도록 방지하는 것입니다. 하지만 이것은 불가능에 가깝습니다. 전문가도 1시간에 3~5개의 실수를 합니다. 실수 관리는 실수를 조기에 발견하고, &quot;나쁜 결과&quot;가 되기 전에 조치를 취하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수 예방 문화에서는 실수한 사람을 비난하고, 실수를 감추려고 하는 경향을 보여 실수로부터 배우기가 힘듭니다. 실수 관리 문화에서는, 실수를 공개하고 그 실수를 어떻게 관리할 것인지 다같이 논의하며 실수로부터 배우려는 태도를 지닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수를 관리하는 문화에서는 학습도 더욱 효과적으로 일어납니다. 불확실한 상황에서 실수는 일어날 수 밖에 없습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 좋은 이야기지만 실제 업무에 적용하기란 말처럼 쉽지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 각자의 상황에 맞게 조금씩이라도 이러한 방법론들을 적용하기 시작하면 유의미한 변화가 찾아올 것이라 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책이 좋으니 나중에 한번 읽어보시길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 모두 함께 자라봐요~&lt;/p&gt;</description>
      <category>개발 서적 기록</category>
      <category>함께자라기</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/41</guid>
      <comments>https://dev-daejlee.tistory.com/41#entry41comment</comments>
      <pubDate>Sun, 1 Feb 2026 20:26:26 +0900</pubDate>
    </item>
    <item>
      <title>6개월 인턴십 회고</title>
      <link>https://dev-daejlee.tistory.com/40</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kw9jl/btsQ3zp4yxG/M90syKPvJcqnKINQG2kka1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kw9jl/btsQ3zp4yxG/M90syKPvJcqnKINQG2kka1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kw9jl/btsQ3zp4yxG/M90syKPvJcqnKINQG2kka1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKw9jl%2FbtsQ3zp4yxG%2FM90syKPvJcqnKINQG2kka1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;533&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월간의 인턴십이 끝을 고했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 회사였고, 그만큼 제가 잘 할 수 있을지 걱정부터 앞섰던 기억이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 것을 겪었고, 그 이상의 값진 것들을 배운 멋진 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋았던 점, 겪었던 문제, 개선점을 회고하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Keep - 좋았던 점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 일지 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 일지를 썼던 것입니다.&lt;br /&gt;일종의 TIL이었는데, 인턴십 중반부터 동기의 권유로 같이 쓰게되었어요. 하루의 업무를 마무리할 때 오늘은 어떤 문제가 있었는지, 문제를 어떻게 해결했는지, 느낀 점들을 적었습니다. 일지는 모두에게 공개되는 사내 위키에 작성했는데, 사측에서도 인턴들이 스스로 일지를 작성하는 걸 굉장히 좋게 봤어요. 심지어 저희의 일지를 보시고 본인도 일지를 쓰기 시작하신 직원분도 계셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성한 일지는 추후 본인 평가서를 작성할 때 내 과거 작업을 트레킹할 때 사용하기도 했고, 일지에서 등장했던 이슈와 비슷한 과제를 해결할 때도 사용되곤 했습니다. 작성하는 것 자체로 기억에 한번 더 각인되기도 했어요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 작업 시작 전의 히스토리 파악&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 시작하기 전 히스토리를 찾아보는 습관을 들인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 회사는 FE 커뮤니티가 잘 발달되있는 편이라 FE 관련 자료가 사내 위키에 잘 정리되어 있다는 장점이 있었습니다. 또 FE 코드베이스가 오픈되어 있기 때문에, 유사한 문제에 대해 각 팀에서 어떻게 접근해서 해결해냈는지 조사하기도 편했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무작정 구현에 들어가는 것과, 시간을 내어 히스토리를 충분히 파악하고 구현에 들어가는 것은 큰 차이가 있었습니다. 이 부분은 작업 전 설계 과정에 포함될 수도 있을 것 같은데, 설계와 구상이 실제 작업보다 더 중요하게 작용할 수 있다는 것을 의미하기도 하는 것 같아요!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 태도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추진력 있는, 적극적인 자세를 유지한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 태도에 대한 이야기인데, 저는 빠른 템포로 업무를 해나가는걸 선호합니다. 한 지점에 오래 머물다 보면 축 가라앉고 무기력한 느낌이 드는 것이 싫더라구요. 항상 동적인 자세를 유지하고, 가능하다면 저에게 주어진 것보다 더 많은 걸 성취하고자 했습니다. 이런 태도에 대한 긍정적인 피드백이 많았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Problem - 겪은 문제들&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 일정 산정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 아쉬웠던 점은, 일정 산정에 대한 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 할당받고, 이 작업에 소요될 일정을 산정합니다. 이 과정에서 작업을 0.5~1 업무일 정도의 작은 단위로 나누어 전체 작업에 필요한 리소스를 산정하게 되는데요, 이것을 정확하게 해내지 못했습니다. 제 생각에 이건 상당한 패착입니다. 이렇게 되면 실제 개발에 필요한 일정보다 부족하게 되고, 그러다보니 서두르게 됩니다. 서두르면 실수가 많아지고 코드 퀄리티가 떨어집니다. 결국 악순환의 수렁에 빠져 역량을 발휘하지 못하는 경우가 종종 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 사내 생산성 기여&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 생산성에 기여하지 못한 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 위키에 문서를 작성하거나, 공통 라이브러리에 기술 개선점을 기여하는 등의 활동을 하지 못했어요. 항상 해야지 해야지 하다가 계속 들어오는 업무에 우선순위가 밀리는 경우가 많았습니다. 조직 생산성에 기여하는 건 작업 하나를 해결하는 것 보다 훨씬 큰 임팩트를 준다고 생각하는터라 아쉬움이 컸습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 적극적인 질문&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 적극적으로 질문하지 못한 점입니다. 제가 끙끙 앓던 문제들이, 동료에게 질문했다면 단순하게 해결할 수 있었던 경우들이 많았어요. 물론 시도때도 없이 찾아가 물어보는 것도 문제지만, 적당한 고민을 했다면 문제를 공유하고 의견을 구하는 게 전체에게 더 나은 결과를 가져오는 케이스가 많았던 것으로 생각합니다. &amp;ldquo;이 질문을 하면 바보같아 보이지 않을까?&amp;rdquo; 하는 생각에 주춤거렸던 때도 많은데, 그냥 물어보는게 낫습니다. 모르는 채로 있는게 바보같은 거였어요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Try - 개선점들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 정확히 산정하고 버퍼를 고려하여 최종 리소스를 결정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 러프하게 작업을 나누거나, 혹은 그것조차 생략해 눈대중으로 작업의 규모를 판단하고 뛰어드는 경우가 많았습니다. 또는 타인이 산정해준 의견에 의존해서 스스로 판단하지 않은 경우도 있었구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 산정에 충분한 시간을 들어야 한다는 것을 배웠습니다. 어쩌면 실제 개발보다 더 중요한 게 리소스 산정과 사전 설계가 될 수 있겠구요. 실제로 경험이 많은 개발자일수록 문제를 파악하는데에 더 많은 시간을 쓴다고 하네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 생산성 증진에 도움이 되는 일들은 즉시 합니다. 당장 배포가 나가야 할정도로 급한 일이 아니라면 그 날의 개선 과제들을 해결하는게 맞을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 자기 계발에 있어서 완벽한 환경이 만들어지기를 기다리지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무가 많을수도, 몸이 좋지 않을수도, 개인적인 일로 힘들수도 있습니다. 그래도 내 바운더리 안에서 최선의 환경을 만들고, 그 환경에서 할 수 있는 걸 해야한다고 느꼈어요. 실천하기 쉽지만은 않은 부분이지만 의식적으로 노력해보려 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제.. 전환 결과를 기다려야겠네요. 행운을 빌어주세욧!&lt;/p&gt;</description>
      <category>회고</category>
      <category>kpt</category>
      <category>인턴 회고</category>
      <category>회고</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/40</guid>
      <comments>https://dev-daejlee.tistory.com/40#entry40comment</comments>
      <pubDate>Sat, 4 Oct 2025 17:56:33 +0900</pubDate>
    </item>
    <item>
      <title>[plopjs] plop을 통한 코드 생성 자동화 (React)</title>
      <link>https://dev-daejlee.tistory.com/39</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;1. 코드 생성 자동화의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트를 개발한다고 합시다. 컴포넌트의 이름을 정하고,&amp;nbsp; index.tsx, index.module.scss, stories.tsx를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 항상 똑같은 코드를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트의 이름만 바뀌고, 같은 코드가 반복됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749998969534&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.tsx
import classNames from 'classnames/bind';
import styles from './styles.module.scss';
const cx = classNames.bind(styles);
interface Props {}
export const Component = ({}: Props) =&amp;gt; {
	return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
};

// index.stories.tsx
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { Component } from '.';
export default {
	title: '',
    component: Component,
} as Meta&amp;lt;typeof Component&amp;gt;;
export const 기본: StoryObj&amp;lt;typeof Component&amp;gt; = {
	name: 'Component',
    args: {},
};

// styles.module.scss
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 이런 거까지 자동화를 해? 별다자(별걸 다 자동화 하네)라고 생각하실 수 있지만, 생각보다 절약되는 시간이 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 많은 작업자들이 관여하는 모노레포의 경우 일관된 코드 스타일을 구축하는 데에 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 규모가 거대한 모노레포의 특징을 고려했을 때, 내가 생성할 컴포넌트의 적절한 위치를 찾는 것 또한 피곤한 과정이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plopjs가 이런 작업을 위해 존재합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. plopjs를 활용한 코드 생성 자동화&lt;/h2&gt;
&lt;figure id=&quot;og_1749900616181&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Plop: Consistency Made Simple&quot; data-og-description=&quot;A little tool that saves you time and helps your team build new files with consistency. Generate code when you want, how you want.&quot; data-og-host=&quot;plopjs.com&quot; data-og-source-url=&quot;https://plopjs.com/&quot; data-og-url=&quot;https://plopjs.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/o4JPC/hyY46VbKTf/FGTugOFlKerjNhzozpKZp1/img.jpg?width=560&amp;amp;height=300&amp;amp;face=0_0_560_300&quot;&gt;&lt;a href=&quot;https://plopjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://plopjs.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/o4JPC/hyY46VbKTf/FGTugOFlKerjNhzozpKZp1/img.jpg?width=560&amp;amp;height=300&amp;amp;face=0_0_560_300');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Plop: Consistency Made Simple&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A little tool that saves you time and helps your team build new files with consistency. Generate code when you want, how you want.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;plopjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Plop is what I like to call a &quot;micro-generator framework.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plop은 일관된 형식으로 코드를 생성할 수 있게 하는 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 리액트 컴포넌트 생성 과정을 plop으로 자동화 해볼까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화를 위해 필요한 정보는 2가지 입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트의 이름&lt;/li&gt;
&lt;li&gt;컴포넌트가 생성될 폴더&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지 정보로 컴포넌트를 자동 생성해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 plop 셋업&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #2b2b2b; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ pnpm install --save-dev plop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plop을 설치하고, 프로젝트의 루트 디렉토리에 propfile.mjs 파일을 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749967809846&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { execSync } from 'child_process';

export default function (plop) {
  plop.setGenerator('component', {
    description: '컴포넌트를 생성합니다.',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: '컴포넌트 이름을 입력하세요:',
      },
      {
        type: 'input',
        nane: 'path',
        message: '컴포넌트 폴더가 생성될 경로를 입력하세요:',
      },
    ],
    actions: () =&amp;gt; {
      const actions = [
        {
          type: 'addMany',
          destination: '{{path}}/{{name}}',
          base: 'plop-templates',
          templateFiles: 'plop-templates/*.hbs',
          abortOnFail: true,
        },
        function formatGeneratedFiles(answers) {
          try {
            const generatedDir = `${answers.path}/${answers.name}`;

            console.log(`코드 스타일을 정리합니다: ${generatedDir}`);
            execSync(`eslint --fix '${generatedDir}' | prettier --write '${generatedDir}'`);
            return '코드 스타일 정리가 완료되었습니다.';
          } catch (error) {
            return '코드 포맷팅을 건너뛰었습니다.';
          }
        },
      ];
      return actions;
    },
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 생성할 파일들의 포맷을 정의할 plop-templates 폴더를 루트에 만들고, 아래 파일들을 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749968126270&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.tsx.hbs
import classNames from 'classnames/bind';
import styles from './styles.module.scss';
const cx = classNames.bind(styles);
interface Props {}
export const {{pascalCase name}} = ({}: Props) =&amp;gt; {
	return &amp;lt;&amp;gt;This is {{pascalCase name}}&amp;lt;/&amp;gt;
};

// index.stories.tsx.hbs
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { {{pascalCase name}} } from '.';
export default {
	title: '',
    component: {{pascalCase name}},
} as Meta&amp;lt;typeof {{pascalCase name}}&amp;gt;;
export const 기본: StoryObj&amp;lt;typeof {{pascalCase name}}&amp;gt; = {
	name: '{{pascalCase name}}',
    args: {},
};

// styles.module.scss.hbs
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 package.json에 plop용 스크립트를 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749968201288&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;scripts&quot;: {
    &quot;plop&quot;: &quot;plop&quot;
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CLI에 pnpm plop을 입력해서 컴포넌트를 생성하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749999799895&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ pnpm plop
&amp;gt; 컴포넌트 이름을 입력하세요: installment-chip
&amp;gt; 컴포넌트 폴더가 생성될 경로를 입력하세요: src/app/main/_component&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Infra</category>
      <category>Plop</category>
      <category>plopjs</category>
      <category>react 코드 자동화</category>
      <category>코드 생성 자동화</category>
      <category>코드생성</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/39</guid>
      <comments>https://dev-daejlee.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 16 Jun 2025 00:06:08 +0900</pubDate>
    </item>
    <item>
      <title>[패키지 매니저] npm, Yarn, pnpm에 대해</title>
      <link>https://dev-daejlee.tistory.com/38</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;들어가기에 앞서.. 패키지 매니저란?&lt;br /&gt;개발에서 외부 라이브러리, 프레임워크 및 도구를 관리하는 도구다.&lt;br /&gt;의존성 관리, 버전 관리, 패키지 업데이트 등을 수행한다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. npm(Node Package Manager)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MrxeG/btsNB7i7Qjw/eKk8AtAa8xIQpxt2kSd9G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MrxeG/btsNB7i7Qjw/eKk8AtAa8xIQpxt2kSd9G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MrxeG/btsNB7i7Qjw/eKk8AtAa8xIQpxt2kSd9G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMrxeG%2FbtsNB7i7Qjw%2FeKk8AtAa8xIQpxt2kSd9G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;150&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 기본 패키지 매니저다. 가장 오래되었고 널리 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json 파일을 통해 의존성을 관리한다. node_modules 폴더에 &lt;b&gt;중첩 구조로 의존성을 설치&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 속도가 yarn, pnpm에 비해 느리다. 이는 &lt;u&gt;중첩 구조&lt;/u&gt;로 인한 중복 패키지 설치 때문이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중첩 구조로 설치한다는 것이 단점인데, 중첩 구조란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 패키지가 자신의 의존성을 자신의 node_modules 폴더 내에 설치하는 방식이다. 예를 들어,&lt;/p&gt;
&lt;pre id=&quot;code_1745673928683&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;프로젝트/
├── node_modules/
│   ├── A/
│   │   ├── node_modules/
│   │   │   └── C@1.0.0/
│   │   └── index.js
│   ├── B/
│   │   ├── node_modules/
│   │   │   └── C@2.0.0/
│   │   └── index.js
│   └── ...
└── package.json&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키지 A는 C의 1.0.0 버전을 사용&lt;/li&gt;
&lt;li&gt;패키지 B는 C의 2.0.0 버전을 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;두 버전의 C가 별도로 설치됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm3 이상부터는 호이스팅 기법을 활용해 가능한 많은 패키지를 루트 node_modules로 끌어올려 중복을 제거하려 노력했지만, 구조적인 한계가 존재하기에 Yarn이나 pnpm보다 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 특히 대규모 프로젝트에서 설치 시간 차이가 크게 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 &lt;b&gt;Yarn은 병렬 설치와 캐싱&lt;/b&gt;을 이용해 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Yarn(Yet Another Resource Negotiator)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/77N3D/btsNANTAuZq/3V3w5ONHSNgrzGJ7S8DGL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/77N3D/btsNANTAuZq/3V3w5ONHSNgrzGJ7S8DGL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/77N3D/btsNANTAuZq/3V3w5ONHSNgrzGJ7S8DGL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F77N3D%2FbtsNANTAuZq%2F3V3w5ONHSNgrzGJ7S8DGL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;135&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Facebook에서 npm의 단점을 보완하기 위해 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yarn은 npm과 달리 여러 패키지를 동시에 다운로드하고 설치한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병렬 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1745681231611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm 방식(순차적)
패키지 A 다운로드 &amp;rarr; 패키지 A 설치 &amp;rarr; 
패키지 B 다운로드 &amp;rarr; 패키지 B 설치 &amp;rarr; 
... (순차적으로 계속)​

Yarn 방식(병렬)
패키지 A, B, C, D, E... 동시에 다운로드 시작 &amp;rarr;
다운로드 완료된 패키지부터 설치 작업 진행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 npm도 병렬 다운로드를 지원하지만, 구현 방식과 효율성 측면에서 Yarn과 차이가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐싱 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yarn은 다운로드한 모든 패키지를 로컬 캐시에 저장하여 재사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 오프라인 환경에서도 이전에 설치한 패키지를 사용할 수 있으며, CI/CD 환경에서 동일 패키지를 반복 설치할 때 시간을 절약한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745681358566&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;~/.yarn/cache/
├── react-16.13.1.tgz
├── react-dom-16.13.1.tgz
├── lodash-4.17.20.tgz
└── ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Yarn Berry(Yarn 2+)의 Plug'n'Play(PnP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yarn Berry에서 추가된 Plug'n'Play는 node_modules 폴더 없이 의존성을 관리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745685224658&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[기존 node_modules 방식]
프로젝트/
├── node_modules/       # 수천 개의 작은 파일로 구성된 매우 큰 폴더
│   ├── react/
│   ├── lodash/
│   └── ...
└── package.json

[PnP 방식]
프로젝트/
├── .yarn/
│   ├── cache/         # 패키지 zip 파일들
│   └── unplugged/     # 특수한 경우 필요한 실제 파일
├── .pnp.cjs          # 의존성 맵핑 정보를 담은 파일
└── package.json&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 패키지는 zip 파일로 압축되어 yarn/cache 폴더에 저장된다.&lt;/li&gt;
&lt;li&gt;pnp.cjs 파일에 모든 패키지의 위치와 의존성 관계 정보를 매핑한다.&lt;/li&gt;
&lt;li&gt;Node.js는 이 매핑 정보를 활용해 zip 파일에서 직접 모듈을 로드한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node_modules 생성이 필요 없기 때문에 설치가 빠르고, 파일이 zip으로 압축되기 때문에 공간 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 PnP 방식은 특히 CI/CD 환경이나 모노레포 구조에서 큰 성능 이점을 발휘한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI/CD 환경에서의 PnP&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;설치 단계 생략 가능: yarn/cache와 pnp.cjs를 Git에 포함시키면 CI 환경에서 yarn install 단계를 완전 생략한다.&lt;/li&gt;
&lt;li&gt;캐시 무효화 감소: 패키지 변경이 없으면 캐시 무효화가 발생하지 않아 CI 캐시 활용도가 높아진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모노레포 구조에서의 PnP&lt;/h3&gt;
&lt;pre id=&quot;code_1745685651490&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[node_modules 방식]
모노레포/
├── packages/
│   ├── app1/
│   │   ├── node_modules/ (크기: 200MB)
│   │   └── package.json
│   ├── app2/
│   │   ├── node_modules/ (크기: 180MB)
│   │   └── package.json
│   └── ... 48개의 추가 패키지
└── package.json
총 저장 공간: ~10GB
초기 설치 시간: 15-20분

[PnP 방식]
모노레포/
├── .yarn/
│   └── cache/ (크기: 500MB - 모든 패키지가 공유)
├── packages/
│   ├── app1/
│   │   └── package.json
│   ├── app2/
│   │   └── package.json
│   └── ... 48개의 추가 패키지
├── .pnp.cjs
└── package.json
총 저장 공간: ~600MB
초기 설치 시간: 2-3분&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. pnpm(&lt;span style=&quot;background-color: #ffffff; color: #202122; text-align: start;&quot;&gt;Performant Node Package Manager)&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLdQM6/btsNBdxpTHD/TyOXZOPnTLYIdODtXXltIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLdQM6/btsNBdxpTHD/TyOXZOPnTLYIdODtXXltIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLdQM6/btsNBdxpTHD/TyOXZOPnTLYIdODtXXltIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLdQM6%2FbtsNBdxpTHD%2FTyOXZOPnTLYIdODtXXltIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;226&quot; height=&quot;160&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm과 Yarn의 대안으로 등장한 패키지 매니저다. 특히 디스크 공간 효율성과 의존성 관리 엄격성에 초점을 맞춘다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 주소 지정 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm의 가장 혁신적인 기능으로, 모든 패키지를 전역 저장소에 한 번만 저장하고 프로젝트에서는 심볼릭 링크를 통해 이를 참조한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745766287525&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;~/.pnpm-store/           # 글로벌 저장소 (모든 프로젝트가 공유)
  └── v3/
      └── files/
          └── 00/
              └── 많은 패키지 파일들...

프로젝트/
└── node_modules/
    ├── .pnpm/          # 실제 패키지들이 저장된 위치
    │   ├── react@16.13.1/
    │   ├── lodash@4.17.20/
    │   └── ...
    ├── react -&amp;gt; .pnpm/react@16.13.1/node_modules/react          # 심볼릭 링크
    ├── lodash -&amp;gt; .pnpm/lodash@4.17.20/node_modules/lodash       # 심볼릭 링크
    └── ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 공간이 절약되며, 이미 저장소에 있는 패키지는 다운로드 없이 링크만 생성되어 설치 속도가 빠르다.&lt;/p&gt;
&lt;pre id=&quot;code_1745766356722&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 500개 패키지를 포함한 모노레포 설치 시
npm: 약 2GB 디스크 공간 사용, 설치 시간 15분
yarn: 약 1.5GB 디스크 공간 사용, 설치 시간 10분
pnpm: 약 600MB 디스크 공간 사용, 설치 시간 5분&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엄격한 의존성 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 &quot;유령 의존성&quot; 문제 해결을 위해 엄격한 의존성 관리를 제공한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;유령 의존성이란? - 직접 의존하지 않는 패키지를 사용할 수 있는 문제를 뜻한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1745766548187&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json에 express만 명시, axios는 명시하지 않음

// npm/yarn에서는 동작함 (express가 의존하는 패키지가 노출됨)
const axios = require('axios');

// pnpm에서는 에러 발생 (package.json에 명시된 패키지만 사용 가능)
// Error: Cannot find module 'axios'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745766584002&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node_modules/
├── .pnpm/
│   ├── express@4.17.1/
│   │   └── node_modules/
│   │       ├── express/
│   │       └── (express의 모든 의존성)
│   └── axios@0.21.1/
│       └── node_modules/
│           └── axios/
├── express -&amp;gt; .pnpm/express@4.17.1/node_modules/express
└── (package.json에 명시된 다른 패키지들)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm의 구조에선 express가 사용하는 의존성들이 express/node_modules에 격리되어 있어, 직접 접근이 불가하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, npm에서 문제가 되었던 중첩 구조가 다시 나타난 것으로 보인다. 성능 문제는 없을까?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm의 구조는 npm의 중첩 구조와 상당히 다른 접근이다.&lt;br /&gt;pnpm의 구조에서는 pnpm-store에 실제 파일이 한 번만 저장되므로 파일 중복이 없으며, 패키지마다의 node_modules는 성능 문제가 아닌 의도적인 보안/의존성 격리 설계다.&lt;br /&gt;따라서 겉보기에는 중첩 구조로 보이지만, 실제로는 성능과 보안을 모두 개선한 우아한 접근법이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우, 특히 모노레포 구조에서는 되도록 pnpm을 사용하는 것이 효율적일 것이다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>npm</category>
      <category>pnpm</category>
      <category>Yarn</category>
      <category>yarn berry</category>
      <category>패키지 매니저</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/38</guid>
      <comments>https://dev-daejlee.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 27 Apr 2025 01:59:41 +0900</pubDate>
    </item>
    <item>
      <title>[모던리액트-DeepDive] 가상 DOM, 리액트 파이버와 렌더링</title>
      <link>https://dev-daejlee.tistory.com/37</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 가상 DOM과 리액트 파이버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM의 모든 변경 사항을 추적하는 것은 너무 수고스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우 모든 DOM의 변경사항보다는 &lt;u&gt;최종 DOM 결과물 하나만 알고 싶을 것&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 것이 가상 DOM이다. 가상 DOM은 웹페이지가 &lt;u&gt;표시할 DOM을 메모리에 저장&lt;/u&gt;하고 리액트가 실제 변경에 대한 준비가 끝나면 브라우저 DOM에 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 &lt;u&gt;DOM 계산을 메모리에서 계산&lt;/u&gt;하는 과정을 거치면 여러 번 발생했을 &lt;u&gt;렌더링 과정을 최소화&lt;/u&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 리액트의 &lt;b&gt;가상 DOM 방식이 일반적인 DOM을 관리하는 브라우저보다 빠르다는 것은 오해다&lt;/b&gt;.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;가상 DOM이 실제 DOM보다 빠르지도 않다면, 왜 사용하는건가?&lt;br /&gt;브라우저의 &lt;u&gt;리플로우, 리페인트를 최적화&lt;/u&gt;한다. 즉, CPU 사용률, 메모리 사용량, 렌더링 성능을 최적화한다.&lt;br /&gt;가상 DOM은 소규모 앱에서는 오버헤드로 인해 오히려 성능이 떨어질 수 있지만, &lt;u&gt;중~대규모 앱에서 진가를 발휘&lt;/u&gt;한다.&lt;br /&gt;메모이제이션을 할 때 메모 비용과 메모 효율을 비교하는 것과 비슷한 논리다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2.3 가상 DOM을 위한 아키텍처, 리액트 파이버&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 파이버는 리액트에서 관리하는 JS 객체다. 이는 파이버 재조정자(fiber reconciler)가 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 가상 DOM과 실제 DOM을 비교해 변경 사항을 비교해 변경 정보를 가진 파이버를 기준으로 화면에 렌더링을 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 파이버의 목표는 &lt;u&gt;리액트 앱에서의 반응성 문제를 해결&lt;/u&gt;하는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업을 작은 단위로 분할해 우선순위를 설정&lt;/li&gt;
&lt;li&gt;작업들은 일시 정지 후 나중에 시작 가능&lt;/li&gt;
&lt;li&gt;이전 작업을 재사용할 수 있으며 필요 없을시 폐기 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 모든 과정이 &lt;u&gt;비동기적&lt;/u&gt;으로 일어난다. (만약 동기적이라면 웹사이트의 성능이 처참할 것..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이버는 하나의 작업 단위로 구성되어 있으며, 리액트는 이를 하나씩 처리하고 finishedWork()라는 작업으로 마무리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 작업을 커밋해 실제 DOM에 가시적인 변경을 만든다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;렌더 단계에서 사용자에게 &lt;u&gt;노출되지 않는 비동기 작업&lt;/u&gt; 수행 - 파이버의 작업(우선순위 지정 및 중지, 폐기)&lt;/li&gt;
&lt;li&gt;커밋 단계에서 DOM에 &lt;u&gt;실제 변경 사항을 반영&lt;/u&gt;하기 위한 작업 수행 - commitWork() 실행(동기적, 중단 X)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이버는 컴포넌트가 최초 마운트되는 시점에 생성되어 이후 가급적 재사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이버 트리에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 내부에 총 두 개가 존재하는데, 하나는 &lt;u&gt;현 모습&lt;/u&gt;을 담은 것이고, 다른 하나는 &lt;u&gt;작업 중인 상태&lt;/u&gt;를 나타내는 workInProgress 트리다. &lt;u style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파이버 작업이 끝나면 리액트는 포인터만 변경&lt;/u&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;해 workInProgess 트리를 현 트리로 바꾼다. 이 기술을 더블 버퍼링이라 한다. 이 더블 버퍼링은 커밋 단계에서 수행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOzTbo/btsMUYhs5GQ/RmrumFZ88QcRwgx6o4Mus0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOzTbo/btsMUYhs5GQ/RmrumFZ88QcRwgx6o4Mus0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOzTbo/btsMUYhs5GQ/RmrumFZ88QcRwgx6o4Mus0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOzTbo%2FbtsMUYhs5GQ%2FRmrumFZ88QcRwgx6o4Mus0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;375&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 UI 렌더링을 위한 트리인 &lt;u&gt;current를 기준으로 업데이트가 발생&lt;/u&gt;하면 파이버는 리액트에서 새로 받은 데이터로 새로운 workInProgress 트리를 빌드하고, &lt;u&gt;작업이 끝나면 다음 렌더링에 이 트리를 사용&lt;/u&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 렌더링되어 &lt;u&gt;반영이 끝나면 current가 workInProgress로 변경&lt;/u&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 파이버 트리와 파이버가 어떻게 작동하는지 흐름을 살펴본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;리액트는 beginWork() 함수를 호출해 파이버 작업을 수행한다. 자식이 없는 파이버를 만날 때까지 트리 형식으로 시작된다.&lt;/li&gt;
&lt;li&gt;끝나면 completeWork() 함수를 실행해 파이버 작업을 완료한다.&lt;/li&gt;
&lt;li&gt;형제가 있다면 형제로 넘어간다.&lt;/li&gt;
&lt;li&gt;위 작업이 모두 끝나면 return으로 돌아가 작업 완료를 알린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 업데이트가 발생하면 workInProgress 트리를 다시 빌드하는데 이때는 이미 파이버가 존재하므로 props를 받아 파이버 내부에서 처리한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2.4 파이버와 가상 DOM&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 파이버는 리액트 네이티브 같은 비-브라우저 환경에서도 사용 가능하기 때문에 &lt;u&gt;파이버 === 가상 DOM은 아니다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 DOM과 리액트의 핵심은 브라우저 DOM을 빠르게 반영하는 것이 아니라 &lt;u&gt;값으로 UI를 표현&lt;/u&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI를 JS 문자열, 배열 처럼 값으로 관리하여 이러한 흐름을 효율적으로 관리하기 위한 메커니즘이 리액트의 핵심이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 렌더링은 어떻게 일어나는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 렌더링은 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정을 말한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.4.2 리액트의 렌더링이 일어나는 이유&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;최초 렌더링: 사용자가 처음 앱에 진입하면 보일 결과물이 필요하기에 최초 렌더링을 수행&lt;/li&gt;
&lt;li&gt;리렌더링: 최초 렌더링 이후 발생하는 모든 렌더링
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;useState()의 setter가 실행되는 경우&lt;/li&gt;
&lt;li&gt;useReducer()의 dispatch가 실행되는 경우&lt;/li&gt;
&lt;li&gt;컴포넌트의 key props가 변경되는 경우&lt;br /&gt;ex) &amp;lt;li key={index}&amp;gt;{index}&amp;lt;/li&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 redux와 react-redux가 둘 다 필요한 이유이다. redux는 자체적으로 &lt;u&gt;상태를 관리하지만 리렌더링을 일으키지는 않기에&lt;/u&gt; react-redux가 위 방법 중 &lt;u&gt;하나를 선택해 리렌더링을 일으켜주는 것&lt;/u&gt;이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.4.3 리액트의 렌더링 프로세스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링이 시작되면 루트 컴포넌트부터 차례로 내려가며 &lt;u&gt;업데이트가 필요한 컴포넌트를 찾아 FunctionComponent()를 호출하고 결과를 저장&lt;/u&gt;한다. 각 컴포넌트의 &lt;u&gt;렌더링 결과물을 수집하고 가상 DOM과 비교&lt;/u&gt;해 실제 DOM에 반영하기 위한 모든 변경 사항을 차례차례 수집한다. 이것이 재조정(Reconciliation)이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.4.4 렌더와 커밋&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더 단계는 &lt;u&gt;컴포넌트를 렌더링하고 변경 사항을 계산&lt;/u&gt;하는 모든 작업이다. 이전 가상 DOM을 비교하는 과정을 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교하는 것은 크게 type, props, key다. 커밋 단계는 &lt;u&gt;렌더 단계의 변경 사항을 실제 DOM에 적용&lt;/u&gt;해 사용자에게 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 모든 DOM 노드 및 인스턴스를 가리키도록 &lt;u&gt;리액트 내부 참조를 업데이트&lt;/u&gt;한다. 이후 useLayoutEffect 훅을 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니다.&lt;/p&gt;</description>
      <category>개발 서적 기록</category>
      <category>가상 DOM</category>
      <category>리액트 렌더링</category>
      <category>리액트 파이버</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/37</guid>
      <comments>https://dev-daejlee.tistory.com/37#entry37comment</comments>
      <pubDate>Tue, 25 Mar 2025 16:52:00 +0900</pubDate>
    </item>
    <item>
      <title>[NextJS] 이미지 파일 관리, 최적화 (SVG, SVGO)</title>
      <link>https://dev-daejlee.tistory.com/36</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;들어가기에 앞서, 프론트엔드에 많은 양의 이미지를 저장하는 것은 유지보수가 어렵고, 권장되지 않습니다.&lt;br /&gt;많은 이미지를 수반한다면 CDN 서비스를 사용하는 것이 좋습니다.&lt;br /&gt;이 포스팅은 프론트엔드 서버를 사용하지 않는 정적 배포를 기준으로 작성합니다 !!&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이미지 파일을 어떻게 관리할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS 환경에서 이미지 저장 경로는 크게 2종류로 나눌 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;public 폴더&lt;/li&gt;
&lt;li&gt;src 폴더 하위 경로&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;You can store static files, like images and fonts, under a folder called public in the root directory. Files inside public can then be referenced by your code starting from the base URL (/).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 이미지나 폰트같은 '정적 파일'들은 public 폴더에 보관할 수 있다고 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 이와 달리 알려진 방법으로 src/assets 폴더를 활용하여 src 폴더 안에 이미지를 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 더 나은 방법일까요? 차이점을 명확하게 구별해봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;public에 저장&lt;/h3&gt;
&lt;pre id=&quot;code_1739032238596&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Image from 'next/image';

export default function HomeImg() {
  return &amp;lt;Image src=&quot;/image/home.svg&quot; alt=&quot;home&quot; /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) http://localhost:3000/image/home.svg&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자원이 빌드 과정에 포함되지 않는다.&lt;/li&gt;
&lt;li&gt;빌드를 건너뛰므로, &lt;u&gt;해당 경로에 이미지가 없어도 검증이 이뤄지지 않아 알 수가 없다&lt;/u&gt;.&lt;/li&gt;
&lt;li&gt;번들링되지 않으므로 파일 이름이 변경되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;src에 저장&lt;/h3&gt;
&lt;pre id=&quot;code_1739032378891&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Image from 'next/image';
import home from '@/assets/home.svg';

export function HomeImg() {
  return &amp;lt;Image src={home} alt=&quot;home&quot; /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) http://localhost:3000/_next/static/media/ home.84b1efa.svg&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자원이 Webpack, Vite 등의 빌드 과정에 포함된다.&lt;/li&gt;
&lt;li&gt;빌드 과정에 포함되므로, &lt;u&gt;해당 이미지가 올바로 import 되었는지 검증이 일어난다&lt;/u&gt;.&lt;/li&gt;
&lt;li&gt;번들링되므로 파일 이름이 변경되며, 번들러의 최적화가 적용된다. (번들러 옵션을 어떻게 지정하냐에 따라 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 방법을 쓰든 NextJS의 Image 최적화 이점은 동일하게 누릴 수 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지는 결국 dist 폴더에 담겨 정적으로 브라우저에 서빙됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이미지를 src 폴더에 소스 코드와 함께 넣어두고 번들링의 이점을 누리는 것을 선호합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public 폴더에는 favicon, robot.txt 등의 에셋들을 넣어두고 관리합니다. 정답은 없습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. NextJS의 이미지 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 NextJS의 &amp;lt;Image&amp;gt; 컴포넌트의 이점에 대해 이렇게 간단히 설명합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;크기 최적화: WebP와 AVIF같은 모던 이미지 포맷을 사용해 사용자 기기에 맞는 크기의 이미지를 제공합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시각적 안정: 이미지 로딩 중 레이아웃 이동을 막습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 페이지 로딩: 브라우저의 lazy-loading을 활용해 뷰포인트에 이미지가 들어왔을 때만 로딩합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에셋 유동성: 원격 서버에 저장된 이미지까지 필요에 따라 이미지를 리사이징합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 브라우저의 &amp;lt;img&amp;gt; 태그에 비해 여러 이점을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 자동으로 최적화되기 때문에 개발자가 크게 신경쓸 부분이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 NextJS의 &amp;lt;Image&amp;gt; 컴포넌트도 &quot;이미지 자체&quot;에 대한 최적화는 해주지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SVGO를 활용한 SVG 이미지 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 자체를 최적화하고 거기에 NextJS의 &amp;lt;Image&amp;gt; 컴포넌트를 사용하면 더할 나위 없겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 SVG 이미지를 사용하는 프로젝트에서 package.json 빌드 과정에 SVGO 스크립트를 추가하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739036554480&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev&quot;,
    &quot;svgo&quot;: &quot;svgo -rf src/assets/ -o src/assets/&quot;,
    &quot;build&quot;: &quot;npm run svgo &amp;amp;&amp;amp; next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;lint&quot;: &quot;next lint&quot;
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N7vGC/btsMcY1NPLw/kvcUKUMXTspTsCoKtemIP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N7vGC/btsMcY1NPLw/kvcUKUMXTspTsCoKtemIP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N7vGC/btsMcY1NPLw/kvcUKUMXTspTsCoKtemIP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN7vGC%2FbtsMcY1NPLw%2FkvcUKUMXTspTsCoKtemIP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;88&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHZILR/btsMaR4vTeW/JgR04jpeXgoFcpkACSkn20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHZILR/btsMaR4vTeW/JgR04jpeXgoFcpkACSkn20/img.png&quot; data-alt=&quot;55kb 정도의 크기를 절약했습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHZILR/btsMaR4vTeW/JgR04jpeXgoFcpkACSkn20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHZILR%2FbtsMaR4vTeW%2FJgR04jpeXgoFcpkACSkn20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;208&quot; height=&quot;85&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;55kb 정도의 크기를 절약했습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>NextJS</category>
      <category>public</category>
      <category>src/assets</category>
      <category>SVG</category>
      <category>svgo</category>
      <category>이미지 최적화</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/36</guid>
      <comments>https://dev-daejlee.tistory.com/36#entry36comment</comments>
      <pubDate>Sun, 9 Feb 2025 02:47:38 +0900</pubDate>
    </item>
    <item>
      <title>[React] 조건부 렌더링 방식 (얼리 리턴, 삼항 연산자, 논리 AND 연산자)</title>
      <link>https://dev-daejlee.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 조건부 렌더링 방식에는 여러가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 쓰이는 얼리 리턴, 삼항 연산자, 논리 AND 연산자(&amp;amp;&amp;amp;) 패턴을 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;얼리 리턴 (Early Return)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 만족하면 즉시 컴포넌트를 반환합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caOzhf/btsLIOl7c6Z/6010d4PP4Q7DUTYjQDvl21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caOzhf/btsLIOl7c6Z/6010d4PP4Q7DUTYjQDvl21/img.png&quot; data-alt=&quot;자리에 유저가 있다면 유저를, 없다면 빈 자리를 렌더링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caOzhf/btsLIOl7c6Z/6010d4PP4Q7DUTYjQDvl21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaOzhf%2FbtsLIOl7c6Z%2F6010d4PP4Q7DUTYjQDvl21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;298&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자리에 유저가 있다면 유저를, 없다면 빈 자리를 렌더링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1736434436675&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function SingleSeat({
  clusterUser,
  seatNumber,
}: {
  clusterUser?: ActiveClusterUser;
  seatNumber: number;
}) {
  const { user } = useUserStore();
  if (!clusterUser) // 해당 Seat 컴포넌트에 전달받은 유저가 없다면 빈 자리를 리턴합니다.
    return (
      &amp;lt;button
        type=&quot;button&quot;
        className=&quot;flex h-10 w-9 cursor-default flex-col items-center justify-center gap-1 rounded-md md:size-14 2xl:size-20&quot;
      &amp;gt;
        &amp;lt;Image
          src=&quot;/image/seats/seat.svg&quot;
          alt=&quot;seat&quot;
          width={32}
          height={32}
          className=&quot;size-5 md:size-8 2xl:size-12&quot;
        /&amp;gt;
        &amp;lt;p className=&quot;text-[8px] md:text-xs 2xl:text-base&quot;&amp;gt;{seatNumber}&amp;lt;/p&amp;gt;
      &amp;lt;/button&amp;gt;
    );

  // 전달받은 유저가 있다면 유저 정보가 담긴 자리를 리턴합니다.
  return (
    &amp;lt;DropdownMenu&amp;gt;
      &amp;lt;DropdownMenuTrigger asChild&amp;gt;
      	...
      &amp;lt;/DropdownMenuTrigger&amp;gt;
      &amp;lt;DropdownMenuContent className=&quot;rounded-xl p-2 md:p-4&quot;&amp;gt;
        &amp;lt;div className=&quot;flex flex-row items-center gap-4 md:gap-4&quot;&amp;gt;
          &amp;lt;ProfilePic user={dummyUser} type=&quot;userCard&quot; /&amp;gt;
          &amp;lt;div className=&quot;flex flex-col items-start gap-1&quot;&amp;gt;
            &amp;lt;LocationBtn user={dummyUser} /&amp;gt;
            &amp;lt;h2 className=&quot; text-xl text-darkblue md:text-2xl 2xl:text-3xl&quot;&amp;gt;
              {dummyUser.intraName}
            &amp;lt;/h2&amp;gt;
            &amp;lt;p className=&quot;md:text-md text-sm text-baseblue&quot;&amp;gt;{dummyUser.comment}&amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/DropdownMenuContent&amp;gt;
    &amp;lt;/DropdownMenu&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하고 직관적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 조건부 렌더링 상황에서 쓸만한 패턴입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삼항 연산자 (Ternary Operator)&lt;/h2&gt;
&lt;pre id=&quot;code_1736435254379&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{isLoading ? (
    &amp;lt;&amp;gt;loading&amp;lt;/&amp;gt;
):(
  &amp;lt;Box&amp;gt;
      My Component
  &amp;lt;/Box&amp;gt;
)}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가독성이 다른 패턴에 비해 떨어지는 편입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;삼항 연산자는 논리 AND 연산자로 완전 대체가 가능합니다&lt;/u&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;논리 AND 연산자 (&amp;amp;&amp;amp;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리 연산자의 개념 자체는 익숙합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 만족한다면 렌더링하고, 만족하지 못하면 렌더링 하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rvKJD/btsLIeskQl1/vm5JtlilPc3K9hsriQeFrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rvKJD/btsLIeskQl1/vm5JtlilPc3K9hsriQeFrk/img.png&quot; data-alt=&quot;친구가 아니면 친구 추가 버튼을 렌더링합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rvKJD/btsLIeskQl1/vm5JtlilPc3K9hsriQeFrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrvKJD%2FbtsLIeskQl1%2Fvm5JtlilPc3K9hsriQeFrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;246&quot; data-origin-width=&quot;393&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;친구가 아니면 친구 추가 버튼을 렌더링합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;333&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCJvR8/btsLIGn6r50/afaeIH1K3kuNTU7GkEuXoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCJvR8/btsLIGn6r50/afaeIH1K3kuNTU7GkEuXoK/img.png&quot; data-alt=&quot;이미 친구 추가가 되있다면 렌더링하지 않습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCJvR8/btsLIGn6r50/afaeIH1K3kuNTU7GkEuXoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCJvR8%2FbtsLIGn6r50%2FafaeIH1K3kuNTU7GkEuXoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;243&quot; data-origin-width=&quot;333&quot; data-origin-height=&quot;243&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미 친구 추가가 되있다면 렌더링하지 않습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1736435608043&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &amp;lt;DropdownMenuContent className=&quot;rounded-xl p-2 md:p-4&quot;&amp;gt;
    &amp;lt;div className=&quot;flex flex-row items-center gap-4 md:gap-4&quot;&amp;gt;
      &amp;lt;ProfilePic user={dummyUser} type=&quot;userCard&quot; /&amp;gt;
      &amp;lt;div className=&quot;flex flex-col items-start gap-1&quot;&amp;gt;
        &amp;lt;LocationBtn user={dummyUser} /&amp;gt;
        &amp;lt;h2 className=&quot; text-xl text-darkblue md:text-2xl 2xl:text-3xl&quot;&amp;gt;
          {dummyUser.intraName}
        &amp;lt;/h2&amp;gt;
        &amp;lt;p className=&quot;md:text-md text-sm text-baseblue&quot;&amp;gt;{dummyUser.comment}&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
      // 만약 친구가 아니라면 친구 추가 버튼을 렌더링합니다.
      {!clusterUser.isFriend &amp;amp;&amp;amp; (
        &amp;lt;FriendAddBtn
          member={{
            ...dummyUser,
            friend: clusterUser.isFriend,
            inOrOut: true,
          }}
          isClusterView
        /&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  &amp;lt;/DropdownMenuContent&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 조건부 렌더링 방식을 실제 사례로 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간결한 패턴을 채택해 일관적으로 사용하는 것이 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 패턴들 말고도 여러 조건부 렌더링 방식을 소개한 글이 있으니 살펴보셔도 좋을 것 같습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1736436493251&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Refactoring React(리팩토링 리액트) : Conditional Rendering(조건부 렌더링)&quot; data-og-description=&quot;리액트의 조건부 렌더링 구문을 프로처럼 사용하는 방법을 알아봅시다. TL;DR - 3줄요약 얼리 리턴 삼항 연산자보다 &amp;amp;&amp;amp;, 연쇄 조건은 js 변수에 할당 부모의 조건부 렌더링 책임을 자식에게 (null 리&quot; data-og-host=&quot;itchallenger.tistory.com&quot; data-og-source-url=&quot;https://itchallenger.tistory.com/898&quot; data-og-url=&quot;https://itchallenger.tistory.com/898&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eAFdg/hyXWsLGopF/s0JqfUjN0KZ5pDijvATnnK/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/b9PG3d/hyX0yKdHNq/0DZJoYajwj57BIi5Kn2lGk/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/bURv3U/hyX0sQMv3H/SbzsbNCB2Rj14G41WtPl80/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://itchallenger.tistory.com/898&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://itchallenger.tistory.com/898&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eAFdg/hyXWsLGopF/s0JqfUjN0KZ5pDijvATnnK/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/b9PG3d/hyX0yKdHNq/0DZJoYajwj57BIi5Kn2lGk/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/bURv3U/hyX0sQMv3H/SbzsbNCB2Rj14G41WtPl80/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Refactoring React(리팩토링 리액트) : Conditional Rendering(조건부 렌더링)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;리액트의 조건부 렌더링 구문을 프로처럼 사용하는 방법을 알아봅시다. TL;DR - 3줄요약 얼리 리턴 삼항 연산자보다 &amp;amp;&amp;amp;, 연쇄 조건은 js 변수에 할당 부모의 조건부 렌더링 책임을 자식에게 (null 리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;itchallenger.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Frontend</category>
      <category>conditional rendering</category>
      <category>early return</category>
      <category>ternary operator</category>
      <category>삼항 연산자</category>
      <category>얼리 리턴</category>
      <category>조건부 렌더링</category>
      <author>Daejlee</author>
      <guid isPermaLink="true">https://dev-daejlee.tistory.com/35</guid>
      <comments>https://dev-daejlee.tistory.com/35#entry35comment</comments>
      <pubDate>Fri, 10 Jan 2025 00:30:20 +0900</pubDate>
    </item>
  </channel>
</rss>