<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Lighthouse of FE beginner</title>
    <link>https://kangs-develop.tistory.com/</link>
    <description>주니어 프론트엔드 개발자의 등대 같은 블로그가 되고 싶습니다.</description>
    <language>ko</language>
    <pubDate>Sun, 28 Jun 2026 08:03:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>[FE] Lighthouse</managingEditor>
    <item>
      <title>[Web] AX 시대의 웹 접근성</title>
      <link>https://kangs-develop.tistory.com/98</link>
      <description>&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-path-to-node=&quot;7&quot;&gt;&lt;b&gt;AX 시대의 웹 접근성&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;AX(AI Experience) 시대가 도래하면서 웹 접근성은 단순히 인간을 위한 배려를 넘어, 인공지능 에이전트(LLM)와 소통하기 위한 필수적인 기술 규격으로 진화하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;과거에는 사용자가 직접 눈으로 화면을 보고 요소를 클릭했지만, 이제는 AI 에이전트가 웹사이트의 소스 코드를 읽고 사용자의 복잡한 명령을 대신 수행한다. 따라서 웹 접근성 가이드라인을 준수하는 것은 AI가 웹사이트를 얼마나 정확하게 이해하고 조작할 수 있는지를 결정하는 AI 색인 최적화(AIO: AI Optimization)의 핵심이 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;AI 에이전트는 화려한 시각적 디자인이 아니라 코드 뒤에 숨은 의미인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시맨틱&lt;/b&gt;을 읽는다. 시맨틱 마크업과 ARIA 속성이 잘 갖춰진 사이트는 AI에게 명확한 지도와 매뉴얼 역할을 한다. 반대로 구조가 불분명한 사이트에서 AI는 특정 요소가 결제 버튼인지 아니면 단순한 광고 이미지인지 판단하지 못해 작업을 실패할 가능성이 크다. 이는 곧&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비즈니스 기회의 확장&lt;/b&gt;과도 연결된다. 향후 많은 소비자가 AI 에이전트를 통해 비행기 표 예매나 상품 구매 같은 대행 서비스를 이용할 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;접근성이 준수된 사이트&lt;/b&gt;만이 AI의 선택을 받아 매출을 일으킬 수 있다. 접근성을 무시하는 것은 미래의 거대한 AI 기반 시장에서 스스로를 고립시키는 것과 다름없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;AI 에이전트의 원활한 탐색을 돕기 위해 개발자가 제공해야 할 구체적인 기술적 장치는 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;첫째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;명확한 역할 정의&lt;/b&gt;가 필요하다. AI는 의미 없는 태그로 만들어진 가짜 버튼을 이해하는 데 어려움을 겪는다. 이때 특정 역할이나 라벨 속성을 부여하면 AI는 해당 요소가 구매하기 기능을 수행하는 인터랙티브 요소임을 명확히 인지하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;둘째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;논리적이고 예측 가능한 구조&lt;/b&gt;를 설계해야 한다. 헤더, 메인, 내비게이션 등 랜드마크 태그를 사용하면 AI 에이전트는 페이지 전체를 무작위로 분석하는 대신 필요한 정보가 있는 위치를 즉시 찾아낼 수 있다. 이를 통해 불필요한 광고를 건너뛰고 핵심 콘텐츠에 빠르게 접근하는 것이 가능해진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;셋째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실시간으로 변하는 상태를 통보&lt;/b&gt;해야 한다. 페이지 새로고침 없이 내용만 바뀌는 동적 환경에서 AI는 변화를 놓치기 쉽다. 이때 실시간 알림 속성을 활용하면 AI에게 결과 값이 업데이트되었음을 신호로 줄 수 있고, AI는 바뀐 내용을 즉각 반영하여 다음 작업을 수행한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;넷째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;입력 폼의 레이블링&lt;/b&gt;을 철저히 해야 한다. 입력창과 연결된 레이블이 없다면 AI는 해당 칸에 이름, 카드 번호, 주소 중 무엇을 입력해야 하는지 판단하지 못한다. 명확한 레이블링은 AI가 각 입력창의 용도를 파악하여 사용자 대신 서류를 작성하거나 결제를 진행하는 토대가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;결국 AX 시대의 접근성은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기계와 인간이 공유하는 공용어이자 미래의 표준 언어&lt;/b&gt;다. 개발자가 작성하는 접근성 코드는 장애를 가진 사용자에게는 보조 공학 기기의 목소리가 되고, AI 에이전트에게는 정교한 업무 수행 매뉴얼이 된다. 이제 접근성 준수는 단순히 친절한 개발자가 되는 차원을 넘어, 미래의 인공지능 사용자를 새로운 고객으로 맞이할 준비가 되었는지를 결정짓는 비즈니스의 핵심 경쟁력이 될 것이다.&lt;/p&gt;</description>
      <category>Aria</category>
      <category>ax</category>
      <category>frontend</category>
      <category>Web</category>
      <category>접근성</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/98</guid>
      <comments>https://kangs-develop.tistory.com/98#entry98comment</comments>
      <pubDate>Sat, 18 Apr 2026 15:05:44 +0900</pubDate>
    </item>
    <item>
      <title>[Web] 웹 접근성과 비즈니스 전략의 접근성</title>
      <link>https://kangs-develop.tistory.com/97</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 접근성은 단순히 장애인을 위한 추가 기능이 아니라, 웹이 마땅히 갖추어야 할 본질적인 품질이다. W3C와 MDN, web.dev 등의 공식 문서에 따르면, 접근성이란 &lt;b&gt;사용자가 처한 신체적 상태나 환경적 제약에 상관없이 누구나 동등하게 정보에 접근할 수 있도록 보장하는 것&lt;/b&gt;을 의미한다. 이는 손을 다쳐 마우스를 쓰지 못하는 일시적 상황이나, 소음이 심한 곳에서 영상을 봐야 하는 환경적 제약까지 모두 포함하는 보편적 사용자 경험(UX)의 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;접근성 높은 웹을 만드는 핵심은 크게 의미 있는 마크업, 키보드 조작 가능성, 보조 기술 호환성이라는 세 가지 기둥으로 지탱된다. 그중에서도 개발자가 가장 정교하게 설계해야 할 영역은 바로 &lt;b&gt;키보드 지원과 포커스(Focus) 관리&lt;/b&gt;다. 대부분의 인터랙션은 포커스 이동과 조작으로 모델링되기 때문이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;포커스 순서와 DOM 구조의 중요성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;포커스는 현재 키보드 입력을 받는 지점을 의미하며, 주로 버튼, 링크, 폼 요소 등 인터랙티브한 요소가 그 대상이 된다. 사용자는 Tab 키로 다음 요소를 탐색하고, Shift + Tab으로 이전 요소로 돌아온다. 이때 가장 중요한 원칙은 포커스의 순서가 화면에 보이는 시각적 순서보다 DOM(문서 객체 모델)의 소스 코드 순서를 우선해야 한다는 점이다.&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;6&quot; data-ke-style=&quot;style3&quot;&gt;Tab 키는 키보드 포커스를 DOM 위로 이동한다. Shift + Tab은 포커스를 DOM 아래로 이동합니다.&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;web.dev의 가이드는 기본 탭 순서가 코드 작성 순서를 따른다고 강조한다. 만약 CSS의 flex-direction이나 order 속성을 사용해 시각적 위치만 억지로 바꾸면, 키보드 사용자는 예측할 수 없는 포커스 이동을 경험하게 되어 큰 혼란에 빠진다. 따라서 논리적인 콘텐츠 흐름에 맞게 HTML 구조를 설계하는 것이 접근성의 첫걸음이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;포커스 표시기와 시각적 피드백&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;실무에서 자주 범하는 실수 중 하나는 디자인을 이유로 &lt;b&gt;outline: none&lt;/b&gt; 속성을 사용하여 포커스 스타일을 제거하는 것이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;포커스 표시기는 &lt;b&gt;키보드 사용자가 현재 자신의 위치를 파악할 수 있는 유일한 단서&lt;/b&gt;다. 브라우저의 기본 스타일을 그대로 사용하거나, 서비스의 테마에 맞춘 맞춤형 포커스 스타일을 제공하여 반드시 시각적으로 명확히 드러나게 해야 한다. 특히 배경색과의 대비가 3:1 이상이 되도록 설계하여 저시력 사용자도 쉽게 인지할 수 있도록 배려해야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10&quot;&gt;tabindex 속성의 올바른 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;정교한 포커스 제어를 위해 tabindex 속성을 활용할 때는 다음의 세 가지 기준을 명확히 지켜야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;첫째, tabindex=&quot;0&quot;은 원래 포커스를 받지 않는 div나 span 같은 요소에 자연스러운 탭 순서를 부여하고 싶을 때 사용한다. 다만, 포커스만 주는 것에 그치지 않고 해당 요소가 Enter나 Space 키로 실제 동작하도록 키보드 이벤트 처리를 반드시 병행해야 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;둘째, tabindex=&quot;-1&quot;은 탭 키로는 도달하지 않지만, 자바스크립트의 .focus() 메서드를 통해 프로그래밍적으로 포커스를 보낼 수 있게 만든다. 이는 본문 바로 가기(Skip Link) 기능을 구현하거나, 아래에서 설명할 모달 창 제어 시 매우 유용하다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;셋째, &lt;b data-index-in-node=&quot;4&quot; data-path-to-node=&quot;14&quot;&gt;1 이상의 양수 tabindex&lt;/b&gt;는 사용을 지양해야 한다. 이는 브라우저의 자연스러운 흐름을 무너뜨려 예측 불가능한 사용자 경험을 유발하고 유지보수를 매우 어렵게 만든다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;모달과 Focus&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;모달을 사용할 때 가장 흔히 발생하는 결함 중 하나는, 모달이 열렸음에도 포커스가 모달 뒤편의 본문에 머물거나 빠져나가는 현상이다. 이를 해결하기 위해 모달 내부에서만 포커스가 순환하도록 하는 &lt;b data-index-in-node=&quot;102&quot; data-path-to-node=&quot;17&quot;&gt;'포커스 가두기(Focus Trap)'&lt;/b&gt; 설계가 필요하다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;아래는 useEffect와 useRef를 활용해 모달 내부의 포커스를 제어하는 간단한 예제 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1776490476342&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useRef } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () =&amp;gt; void;
  title: string;
  children: React.ReactNode;
}

const AccessibilityModal: React.FC&amp;lt;ModalProps&amp;gt; = ({ isOpen, onClose, title, children }) =&amp;gt; {
  const modalRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  useEffect(() =&amp;gt; {
    if (!isOpen) return;

    // 1. 모달이 열리면 모달 자체 혹은 첫 번째 요소에 포커스 부여 (tabindex=&quot;-1&quot; 필요)
    modalRef.current?.focus();

    const handleKeyDown = (e: KeyboardEvent) =&amp;gt; {
      if (e.key === 'Escape') onClose();
      if (e.key !== 'Tab') return;

      const focusableElements = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex=&quot;-1&quot;])'
      );
      
      if (!focusableElements || focusableElements.length === 0) return;

      const firstElement = focusableElements[0] as HTMLElement;
      const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

      // 2. Tab 순환 로직 (Shift + Tab 포함)
      if (e.shiftKey) { // Shift + Tab: 첫 번째 요소에서 마지막 요소로 이동
        if (document.activeElement === firstElement) {
          e.preventDefault();
          lastElement.focus();
        }
      } else { // Tab: 마지막 요소에서 첫 번째 요소로 이동
        if (document.activeElement === lastElement) {
          e.preventDefault();
          firstElement.focus();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () =&amp;gt; window.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    &amp;lt;div 
      className=&quot;modal-overlay&quot; 
      role=&quot;dialog&quot; 
      aria-modal=&quot;true&quot; 
      aria-labelledby=&quot;modal-title&quot;
    &amp;gt;
      &amp;lt;div 
        className=&quot;modal-content&quot; 
        ref={modalRef} 
        tabIndex={-1} // JS로 포커스를 주기 위해 필요
      &amp;gt;
        &amp;lt;h2 id=&quot;modal-title&quot;&amp;gt;{title}&amp;lt;/h2&amp;gt;
        {children}
        &amp;lt;button onClick={onClose}&amp;gt;닫기&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&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;이 코드의 핵심은 tabindex=&quot;-1&quot;을 활용해 모달 컨테이너에 초기 포커스를 주고, 키보드 이벤트를 감지하여 &lt;b data-index-in-node=&quot;68&quot; data-path-to-node=&quot;21&quot;&gt;첫 번째와 마지막 포커스 요소 사이를 강제로 연결&lt;/b&gt;하는 데 있다.&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;&lt;b&gt;시각적 요소(img)와 대체 텍스트(alt) 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;이미지 태그의 alt 속성은 단순히 기계적으로 채우는 것이 아니라, 정보의 성격에 따라 전략적으로 작성해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,0,0&quot;&gt;의미 있는 이미지:&lt;/b&gt;&amp;nbsp;단순히 &quot;로고&quot;라고 적기보다 &quot;NHN Cloud 서비스 로고&quot;처럼 구체적인 정보를 제공해야 한다. 이미지 안에 중요한 텍스트가 포함되어 있다면 해당 내용을 그대로 alt 값에 포함한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,1,0&quot;&gt;장식용 이미지:&lt;/b&gt; 디자인을 위한 배경이나 장식용 아이콘이라면 alt=&quot;&quot;와 같이 빈 값을 주어 스크린 리더가 해당 요소를 무시하고 지나가게 해야 한다. (aria-hidden을 사용해 스크린 리더가 탐색할 수 없게 만드는 것도 방법이다.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,2,0&quot;&gt;아이콘 버튼:&lt;/b&gt; 텍스트 없이 아이콘만 존재하는 버튼은 버튼 태그에 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;25,2,0&quot;&gt;aria-label&lt;/b&gt; 속성을 사용하여 해당 버튼의 기능을 명확히 설명해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;시멘틱 태그와 ARIA 속성의 관계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;웹 접근성에서 가장 중요한 점은 &quot;&lt;b&gt;ARIA를 쓰지 않아도 된다면 쓰지 않는 것(No ARIA is better than Bad ARIA)&lt;/b&gt;&quot;이다. HTML5가 제공하는 시맨틱 태그(button, nav, main 등)는 그 자체로 브라우저와 보조 기술에 자신의 역할(Role)을 알린다. 불필요하게 div에 role=&quot;button&quot;을 부여하기보다 실제 button 태그를 사용하는 것이 훨씬 안전하다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;ARIA는 시맨틱 태그만으로는 설명하기 어려운 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;28&quot;&gt;동적인 상태&lt;/b&gt;를 표현할 때 보조적으로 사용해야 한다. 아코디언 메뉴의 펼침 상태를 알리는 aria-expanded, 여러 탭 중 현재 선택된 탭을 나타내는 aria-selected 등이 대표적이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-path-to-node=&quot;32&quot;&gt;&lt;b&gt;비즈니스 전략과 웹 접근성&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;많은 의사결정권자가 접근성을 비용으로만 간주하지만, 실질적으로 접근성은 시장 점유율을 넓히고 브랜드 가치를 높이는 전략적 투자에 가&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;깝다. 다음과 같은 관점에서 접근성은 비즈니스의 매출 성장에 기여한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-path-to-node=&quot;4&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4&quot;&gt;잠재 고객층의 극대화&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;전 세계 인구의 약 15%는 어떤 형태로든 장애를 가지고 있다. 접근성을 무시하는 것은 전 세계 수억 명의 잠재 고객을 스스로 포기하는 것과 같다. 고령화 사회로 진입하면서 시력이나 인지 기능이 저하된 실버 세대가 주요 소비 계층으로 부상하고 있는 점을 고려하면, 접근성은 더 이상 '틈새시장'이 아닌 '메인스트림'을 잡기 위한 필수 조건이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-path-to-node=&quot;5&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;검색 엔진 최적화(SEO)와의 시너지&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;검색 엔진의 크롤러는 화면을 보지 못하는 사용자(스크린 리더 사용자)와 매우 유사한 방식으로 웹 사이트를 탐색한다. 시맨틱 마크업을 준수하고 이미지에 적절한 대체 텍스트를 제공하는 접근성 개선 작업은 곧 검색 엔진이 우리 사이트의 콘텐츠를 더 잘 이해하게 만든다. 이는 검색 결과 상위 노출로 이어지며, 광고비 지출 없이도 유입량과 매출을 늘리는 결과로 나타난다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-path-to-node=&quot;6&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;법적 리스크 방지와 브랜드 신뢰도 구축&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;미국이나 유럽 등 글로벌 시장에서는 웹 접근성 미준수로 인한 소송이 매년 급증하고 있으며, 한국 역시 '장애인차별금지법'에 따라 웹 접근성 준수가 의무화되어 있다. 소송 비용이나 브랜드 이미지 실추로 인한 손실을 예방하는 것은 매출을 지키는 가장 확실한 방법이다. 반대로 접근성이 뛰어난 서비스는 '모두를 배려하는 전문적인 브랜드'라는 이미지를 심어주어 고객 충성도를 높인다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-path-to-node=&quot;7&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7&quot;&gt;전반적인 사용자 경험(UX) 개선 및 이탈률 감소&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;접근성 가이드라인은 본질적으로 '사용하기 쉬운 설계'를 지향한다. 로딩 속도 최적화, 명확한 포커스 표시, 논리적인 레이아웃은 장애가 없는 사용자에게도 더 쾌적한 환경을 제공한다. 모바일 기기를 사용하거나 인터넷 속도가 느린 환경에서도 원활하게 작동하는 사이트는 사용자 이탈률을 낮추고 구매 전환율(Conversion Rate)을 높이는 핵심 동력이 된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;29&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29&quot;&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;개발 중인 UI가 접근성을 충실히 따르고 있는지 확인하려면 다음 세 가지 질문을 던져보아야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;31&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-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로 웹 접근성은 프로젝트의 끝에서 점검하는 체크리스트가 아니라, 설계의 시작부터 고려해야 할 철학이다. 또 웹 접근성은 비즈니스 전략과 가장 가깝게 위치하고 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;접근성을 준수하기 위해서 포커스를 주는 이유와 그에 따른 조작 방식이 논리적으로 설계될 때, 비로소 웹은 장벽 없이 모든 이에게 열린 공간이 될 수 있다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>Aria</category>
      <category>frontend</category>
      <category>Web</category>
      <category>접근성</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/97</guid>
      <comments>https://kangs-develop.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 18 Apr 2026 15:01:02 +0900</pubDate>
    </item>
    <item>
      <title>두서없는 2026년 2월 회고</title>
      <link>https://kangs-develop.tistory.com/96</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그냥 회고&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두서없이 작성하는 2026년 2월 회고..&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;2월 한달은 워킹데이가 짧았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일 자체도 28일 밖에 없는데 설 연휴까지 껴있어서 정말 빠르게 지나간 한 달이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육과 NES 시험도 있었어서 정말 정신없이 지나갔던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1월 회고를 작성한게 엊그제 같은데 벌써 2월 회고라니..&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;DnD기능이 있는 컴포넌트여서 DnD 라이브러리를 사용할지, 아니면 DnD 기능도 직접 구현해서 제공할지 결정을 했어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들과 함께 결정을 하기 위해서는 여러가지의 근거 자료가 필요했고, DnD기능도 직접 구현한다면 인터페이스를 작성해 리뷰를 받아야 했기 때문에 설계를 진행해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 나의 설계대로 구현이 가능할지 PoC를 해보면서 첫 단계(기획 - 설계)부터 너무 딥하게 들어가서 여러가지 우여곡절도 있기는 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 DnD 기능을 직접 구현하기로 결정했고, 설계는 리서치 과정에서 딥다이브 했던 dnd-kit의 내부 설계를 참고해 구현하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 dnd-kit은 새로운 아키텍처가 릴리즈되어 완전 새로운 라이브러리가 됐던데, 내부적으로 시그널과 TC39의 데코레이터를 사용했더라. 새로운 아키텍처를 오마주 하기 보다는 내부 설계된 모듈(센서, 레지스트리, 충돌 전략 등)을 참고하여 DnD 기능을 구현하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PoC에서 기능 동작을 검증했지만 스크롤 상황에서 DOM Rect가 측정된 registry 계산에서 미스가 있는 등 여러가지 버그가 존재했다. 리서치를 딥하게 진행했으니 3월 중순까지 DnD 기능과 컴포넌트를 구현하는 것을 목표로 한다!&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;이 과정에서 dnd-kit 아키텍처에 대해서 딥다이브 했었는데 다음달에 블로그 글로 정리해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 새로운 아키텍처에서 signal을 사용했고 framework-agnostic하게 설계가 됐던데 이 부분도 한번 딥하게 살펴봐야겠다.&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;이 부분도 시간이 지나면 해결되지 않을까?&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;&lt;b&gt;AI 시대의 도래&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-28 오후 3.34.03.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;732&quot;&gt;&lt;a href=&quot;https://www.google.com/search?sca_esv=8acefd044d7ad0e4&amp;amp;sxsrf=ANbL-n44WdTNo4sUYi2Wh3neE3duC4N1pw:1772260415571&amp;amp;udm=2&amp;amp;fbs=ADc_l-bD_nyrjATWBKup7flJ4rea5XFXsPHwMjGsTekJ1HCohBAQ3Hh19DqzlO7wr7YUgTdA6AIvvuoLcS3uB5TUiBhAbf2Esh7hmQcamAOq029JiMYURUYevoAeUkw2Njxy_NdTWI6sC47XNf2bPTAE4b61wvrTzd8tGAWDZJ0IqBAi6kRQMmtCHRZwwxCVMNU23wM7cHKyw4MQ_Bke5a4PmOZ4q8JTEQ&amp;amp;q=%EC%9D%B8%EA%B0%84+%EC%8B%9C%EB%8C%80%EC%9D%98+%EB%81%9D%EC%9D%B4+%EB%8F%84%EB%9E%98%ED%96%88%EB%8B%A4&amp;amp;sa=X&amp;amp;ved=2ahUKEwiD6ZPEyPuSAxVEhlYBHf_vADkQtKgLegQIExAB&amp;amp;biw=1920&amp;amp;bih=1064&amp;amp;dpr=1.8#sv=CAMSVhoyKhBlLUFSQ0YteXFOcmFrQ3pNMg5BUkNGLXlxTnJha0N6TToON2NpZ2ZJUEZ2T2h6bU0gBCocCgZtb3NhaWMSEGUtQVJDRi15cU5yYWtDek0YADABGAcgs77qxQwwAkoIEAIYAiACKAI&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SLgNW/dJMcabDjhGe/3KfntH4DkSRaOPCqb5FDk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSLgNW%2FdJMcabDjhGe%2F3KfntH4DkSRaOPCqb5FDk0%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;786&quot; height=&quot;567&quot; data-filename=&quot;스크린샷 2026-02-28 오후 3.34.03.png&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;출처: 구글 이미지 검색&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인간 시대의 끝이 도래했다~&lt;/span&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;요즘 직접 코드를 작성하는 시간이 전보다 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 LLM과 함께 일하다보니 몇 일이 걸려서 검증해야 할 PoC나 코드 작성의 시간이 획기적으로 단축됐다.&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;그럼 개발자가 집중해야 할 영역은 어떤 부분일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 아키텍처, 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LLM이 더욱 코드를 잘 만들 수 있도록 프롬프트 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LLM이 만든 코드를 리뷰&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;그럼 LLM이 집중해야 할 영역은 어떤 부분일까?&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;요즘은 OpenClaw와 같은 나만의 AI 비서 오픈소스도 존재하고 하네스 엔지니어링으로 완전히 프로젝트를 AI에게 위임하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 AI와 함께 프로젝트를 빠르게 빌드하고 출시하고 시장의 반응을 보고 빠르게 버리는 (Agile이 AX를 만나니 더욱 미친 속도이다.) 엔지니어링이 유행이다. (바이브 코딩)&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;과연 코드를 작성하고 시스템을 설계하고 유지보수 하는, 전통적인 개발자가 3년 후에도 존재할까?&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;개발자라는 직업이 N년후에 없어질거다 ????&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;대신 Maker, Solver와 같은 형태의 직군이 생기지 않을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 빠르게 기획하고 기획과 동시에 AI와 함께 코드를 작성하여 MVP로 바로 출시하는 직군이지 않을까. (물론 지금도 몇몇 스타트업에서는 이렇게 정의를 해 채용을 하는 것 같다.)&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;결국 개발자보다는 조금 더 서비스와 밀접하고 예민하고, 뾰족하게 문제를 정의할 수 있는 (뾰족하다는 뜻은 핵심을 찌른다 라고 생각이 든다) 사람이 어울리는 직군이 Maker, Solver가 되지 않을까 생각한다.&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;그리고 메이커(Maker), 솔버(Solver)라는 직군이 생겨날 것이다. 메이커, 솔버도 코드를 작성해야 하고 유지보수 해야하기 때문에 반드시 컴퓨터 공학을 전공해야 한다고 생각한다. (앞으로는 &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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;ㄴ 더더욱 도메인에 특화된 인재가 귀해지지 않을까&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;ㄴ 시장의 반응이 좋은 기능이라면 안정적인 시스템이 필요할텐데 빌드(시작)단계부터 잡고 가야 한다고 생각한다.&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;ㄴ 당연히 필요하다.&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;ㄴ 당연히 필요하다. 근데 인간과의 커뮤니케이션도 중요하지만 AI와 커뮤니케이션 할 수 있는 능력이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- AI 엔지니어링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ AX 시대엔 LLM을 개발하는 것도 중요하지만 LLM을 활용할 줄 아는 엔지니어링 능력도 중요하다. skill, hook, agent, team 기능을 잘 활용해 좋은 워크 플로우를 만드는 엔지니어링 능력이 중요할 것 같다.&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;나도 앞으로 AI 엔지니어링에 대해 더 깊게 공부해야겠다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰면서 당장 skill, hook, agent, team과 같은 기능을 더욱 깊게 공부해보고 활용할 수 있는 내용들을 공부해보며 블로그에 포스팅해야겠다는 생각이 들었다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>2월 회고</category>
      <category>AI</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/96</guid>
      <comments>https://kangs-develop.tistory.com/96#entry96comment</comments>
      <pubDate>Sat, 28 Feb 2026 15:36:04 +0900</pubDate>
    </item>
    <item>
      <title>26년 1월 회고 (부재. 이직 일 개월 차 회고)</title>
      <link>https://kangs-develop.tistory.com/95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 1월 5일, 이직한 회사에서 첫 달을 보냈다.&lt;br&gt;개인적으로 적응하는 과정은 쉽지 않았고, 배운 것도 느낀 것도 많았던 한 달이였다.&lt;br&gt;회고를 통해 이번 달에 느꼈던 점, 잘했던 점, 아쉬웠던 점을 남겨본다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;새로운 회사, 새로운 팀&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 가고 싶은 회사였고 퇴사하고 쉬는 기간동안 빨리 입사일이 다가왔으면 좋겠다 생각했었다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그때 더 열심히 놀았어야 했거늘....&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;대망의 1월 5일 입사일이 다가왔고 판교까지 집에서 1시간 30분이 걸려 출근을 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;첫날엔 신규 입사자 온보딩이 진행됐고, 오후에 인사 팀원분과 티타임을 진행한 후 팀 내 선임(시니어)님의 안내 하에 팀원분들을 처음 뵙게 됐다.&lt;br&gt;인사 팀원분과 티타임때 이런 이야기를 드렸었는데,&lt;br&gt;&lt;i&gt;&quot;아무래도 신입이 아닌 경력직 (그래도 올해 4월이 나름 만 4년이니..)으로 입사한 만큼 퍼포먼스도 있어야 할 것 같고 기대도 하실거 같아서 부담이 있긴 하다.&quot;&lt;/i&gt;&lt;br&gt;추후에 이야기를 하겠지만 아주 오만한(?) 생각이였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;현재 팀에서 함께 일하는 분들은 나 포함 총 6분이시고, 대부분 시니어 분들이시고 함께 일한지 꽤나 오래된 것 같았다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;요즘 기준으로 한 회사에서 3년 이상 일했으면 장기근속이지 않을까 생각하고 있다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그간 팀원분들께서 하신 업무를 살펴보니, 이 사람들 진짜 엄청난걸? 이라는 생각이 들었다.&lt;br&gt;그와 동시에, 나도 이 정도 퍼포먼스를 낼 수 있을까? 라는 생각이 들며 심리적으로 엄청 위축됐다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 입사와 동시에 신규 입사자 교육이 시작됐다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;교육, 그리고 초연한 마음 가짐&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서 계획한 교육은 2주 기간이였고, 여러가지 세션으로 진행됐다.&lt;br&gt;브라우저 랜더링 과정부터 시작해 팀의 기술 스택인 React로 Todo 구현하기 실습까지다.&lt;br&gt;&amp;nbsp;&lt;br&gt;브라우저 랜더링 과정은 여러번 공부해서 잘 알고 있다고 생각했다. 근데 Chrome의 새로운 아키텍처인 RenderingNG가 있는건 처음 알았다. 내 세상은 모던 자바스크립트 Deep Dive 까지였나보다..&amp;nbsp;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;RenderingNG 아키텍처 &amp;nbsp;|&amp;nbsp; Chromium &amp;nbsp;|&amp;nbsp; Chrome for Developers&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;렌더링 아키텍처의 구성요소와 렌더링 파이프라인이 이를 통해 흐르는 방식을 알아봅니다.&quot; data-og-host=&quot;developer.chrome.com&quot; data-og-source-url=&quot;https://developer.chrome.com/docs/chromium/renderingng-architecture?hl=ko&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/vakhl/dJMb8SpCNnR/AAAAAAAAAAAAAAAAAAAAANquTSI-G2gnKMeoTS-sq9w1krrmrAPxw6PNgIPuwt-y/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=PkwX%2BN7QUcqWJFCzf7yDMDOzmkU%3D&quot; data-og-url=&quot;https://developer.chrome.com/docs/chromium/renderingng-architecture?hl=ko&quot;&gt;&lt;a href=&quot;https://developer.chrome.com/docs/chromium/renderingng-architecture?hl=ko&quot; target=&quot;_blank&quot; data-source-url=&quot;https://developer.chrome.com/docs/chromium/renderingng-architecture?hl=ko&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/vakhl/dJMb8SpCNnR/AAAAAAAAAAAAAAAAAAAAANquTSI-G2gnKMeoTS-sq9w1krrmrAPxw6PNgIPuwt-y/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=PkwX%2BN7QUcqWJFCzf7yDMDOzmkU%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;RenderingNG 아키텍처 &amp;nbsp;|&amp;nbsp; Chromium &amp;nbsp;|&amp;nbsp; Chrome for Developers&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;렌더링 아키텍처의 구성요소와 렌더링 파이프라인이 이를 통해 흐르는 방식을 알아봅니다.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;developer.chrome.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;테스트 코드를 업무에서 작성해본 경험은 없다.&lt;br&gt;직전 회사에서 인터렉션 테스트 코드를 작성해봤지만 리뷰를 받으며 작성한 것이 아닌, 구현한 디자인 시스템의 컴포넌트가 어떻게 동작하는지 Storybook으로 작성한 것이 대부분이였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또 TDD는 말로, 글로, 이론으로 배웠지 실제로 TDD를 해본 경험도 없다.&lt;br&gt;캔트 백(Kent Beck) 선생님의 Test Driven Developement 책도 읽어야지 읽어야지 하다가 처음으로 읽어봤고, 이해가 안되어(벌써 에이징커브?) 2번 읽어보게 됐다.&lt;br&gt;&amp;nbsp;&lt;br&gt;드래그 앤 드랍은 구현된 라이브러리를 사용해보기만 했지 구현해볼 생각은 해보지도 않았고, 처음 실습 과제를 봤을때 &quot;이걸 하라고????&quot; 라는 생각이 먼저 든건 비밀이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그나마 React로 Todo 구현하기는 채용 사전 과제에 익숙한 나에게 제일 반가운 과제였다.(ㅋㅋ)&lt;br&gt;&amp;nbsp;&lt;br&gt;이 교육 과정을 거치며 팀장님, 선임님께 여러 리뷰를 받았고 리뷰를 받는 과정에서 또 다시 같은 리뷰를 받기 싫어서 굉장히 신경을 많이 쓰면서 교육 과정을 진행했다. (그럼에도 같은 포인트에서 리뷰를 받았을 때는 리뷰어에게 미안한 마음과 나 자신이 한심해지는 기분도 들었다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;여러가지 압박감이 한 2주정도 지속되니 나 스스로도 심리적으로 엄청 힘들었고 회사에서 한 시간씩 더 있으면서 공부하고 일하던 시간들이 누적되어 오늘 마지막날은 퍼플 타임을 이용해 병원을 두 곳을 다녀오게 됐다. (영양제 잘 먹어야지 ...)&lt;br&gt;&amp;nbsp;&lt;br&gt;오늘(1월 30일)로써 입사한지 벌써 4주가 지나간다.&lt;br&gt;2주차까진 이런 저런 생각도 많이 들며 스트레스를 받았지만, 오늘은 또 이런 생각이 든다.&lt;br&gt;뭐.. 그래도 이유가 있으니 날 채용한 것이고, 그래도 팀원들은 나를 평가하기 보다는 함께 일하기 위해 발을 맞추는 과정일 뿐이다.&lt;br&gt;정말 맞지 않는 부분이 있거나, 내가 해쳐나가야 할 부분이 있다면 면담을 통해 말씀하시지 않을까 라는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;생각이 이렇게 드니 마음도 초연해진다.&lt;br&gt;&amp;nbsp;&lt;br&gt;첫 날 들었던 오만한 생각은 쏙 들어가고, &quot;나는 신입이다!&quot; 라는 마음을 장착해버렸다.&lt;br&gt;사실 아직 주니어기도 하고 배워야 할 것이 많은게 당연하다.&lt;br&gt;그래도 만 4년 경력의 주니어지만, 이제부터는 지금까지 내가 알고 있고 일했던 모든 것을 내려놓고 새로 배운다는 마음가짐으로 일하려고 하고있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래야 새로운 팀에서 더 잘 (빠르게 라는 단어는 적었다 지웠다.) 적응할 수 있지 않을까 라는 생각도 들고, 지금까지 내가 일했던 방식과 다른 일하는 방식을 더욱 잘 습득할 수 있지 않을까 라는 생각에서다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;물론 지금까지 내가 해왔던 일을 부정하는 것은 아니다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;결국 나를 백지로 바꿨고, &quot;이걸 해야하지 않을까?&quot; 에서 &quot;잘 배우고 깊게 고민하여 새로운 베이스에서 좋은 퍼포먼스를 내보자.&quot;로 어느 정도 생각이 바뀌게 됐다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AI는 너무 빨라&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이전 회사에서는 AI를 거의 사용하지 못했다.&lt;br&gt;뭐.. 회사에서 쓰지 말라고 한건 아닌데 AI 쓰라고 지원해주지 않다보니까 여러가지 도구를 사용해볼 기회가 없었기 때문이다. (&lt;s&gt;아직 나에겐 너무 비싼 클코..&lt;/s&gt;)&lt;br&gt;&amp;nbsp;&lt;br&gt;지금 회사는 다른 관점이다. AI를 쓰라고 지원해준다.&lt;br&gt;이전에는 기껏해봤자 Chat GPT, Copilot, Codex(이것도 얼마 안써봄)였는데, 여기서는 Claude Code를 지원해준다!&lt;br&gt;&amp;nbsp;&lt;br&gt;사실 들어는 봤는데.. 그래서 어떻게 쓰는건데? 사실 잘 몰랐다.&lt;br&gt;&amp;nbsp;&lt;br&gt;새로운 팀에서는 Claude Code를 적극 활용한다. 이미 실무에서 사용한지 꽤 됐으며 이 도구를 사용해 생산성을 극도로 올리기 위해 치열하게 고민을 하고 있다.&lt;br&gt;Sub Agents, Hooks, Skills, MCP ... 여러가지 방법들을 동원하여 작업을 하고 생산성을 높인다.&lt;br&gt;&amp;nbsp;&lt;br&gt;처음 들었던 생각은..&lt;br&gt;나 지금까지 칼이랑 활들고 싸워왔네? 였다.&lt;br&gt;최근 Redis의 창시자(맞나? 메인테이너인가?)가 AI도구를 사용해 7일 걸릴 일을 2시간만에 끝내는 일이 화제가 됐었고, omc와 같은 멀티 에이전트 오케스트레이션 도구까지 나오는 등 요즘 IT 업계의 주 이슈는 AI를 사용해 생산성을 극도로 끌어올리는 것 이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;실제로 링크드인을 봐도 AI를 사용해 생산성을 극도로 끌어올리는 포스팅이 많은 좋아요와 퍼가요~ 받고 있고,&lt;br&gt;이제 AI 도구와 함께 일하는 것은 당연하고 얼마나 더 효율적으로(토큰을 아끼면서) 도구를 잘 써서 팀, 개인의 생산성을 끌어올리느냐의 싸움이 되고있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;근데 AI 도구의 발전 속도가 너무 빠르다.&lt;br&gt;Skill 쓰세요!! 가 화제가 된지 얼마 안됐는데, Skill 보다는 다른걸 써야하는데? 라는 이야기가 또 나오고,&lt;br&gt;분명 Claude Code에 어제는 없었는데, 오늘 갑자기 패치를 하니 어떤 기능(feature)이 생기고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;몇일전 팀원분께서 스크럼 시간에 이런 말씀을 하셨다.&lt;br&gt;AI 발전 속도에 스트레스를 받지 않겠다고, 그냥 어린 아이들이 어떤 것을 보면 그냥 받아들이는 것 처럼 받아들이겠다고.&lt;br&gt;그리고 그냥 해보겠다고.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;굉장히 멋진 생각이였다!&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;본론으로 돌아와서 처음 팀에 합류 했을 때 Claude Code 써봤어요? 라는 질문에 수줍게 &quot;아니요..&quot;라고 대답했다.&lt;br&gt;한 4주 팀에 있어보니까 이제 Claude Code로 개발하는 것이 부끄러운 일이 아니고, 이 도구를 어떻게 사용해서 우리의 생산성을 끌어올리냐, 이 도구를 사용해 다른 팀(회사)와의 경쟁에서 우위를 점할 수 있냐? 의 싸움이 된 것이다.&lt;br&gt;그리고 여러번 Claude Code를 사용해보니 (처음엔 와 미친놈아님? 이였지만) 이 도구를 잘 쓰는게 왜 팀의 생산성을 끌어올리려고 치열하게 노력하는지 알 것 같다. 이런 노력이 우리 팀과 다른 팀과의 격차를 만들어줄테고, 결국 그 생산성으로 인해 격차는 따라잡을수 없을만큼 벌어지지 않을까 라는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 음. 내가 든 생각은 그거다.&lt;br&gt;Claude Code 잘쓰자. 근데 그냥 코딩만 시키는게 아니라 계속 티키타카 하면서 계획 잘 쌓고, 코딩은 쟤한테 시키고 나는 좀 더 큰 관점에서 문제를 보려고 노력해보자.&lt;br&gt;&amp;nbsp;&lt;br&gt;아직 사용해본지 얼마 안되어서 여기까지밖에 모르는 것일수도..?&lt;br&gt;두 달 뒤에 또 어떤 생각이 있을지 궁금하다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결국 소프트웨어 개발자는&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 AI의 발전으로 결국 소프트웨어 개발자는 40%만 살아남지 않을까 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;내가 직접 코드를 짜고 코드에서 예술을 찾는 시대는 지나갔다고 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;AI 도구를 사용해 생산성을 극대화 하고, 더욱 빠르게 기능을 시장에 내놓고, 시장을 선점해 타사와의 격차를 점점 벌려가지 않을까 생각한다.&lt;br&gt;소프트웨어 생애주기는 점점 짧아질 것이고, 만들고 버리는 시간이 더욱 짧아지며, 이전의 MVP라는 개념은 점점 바뀌지 않을까라는 생각을 한다. (결국 이득을 보는 사람은 소비자고 피로감을 느끼는건 메이커겠지만)&lt;br&gt;&amp;nbsp;&lt;br&gt;이 빠르고 빠른 생애주기 속에서 AI와 함께 뭉개지지 않을 소프트웨어를 만드는 개발자가 살아남지 않을까 생각한다.&lt;br&gt;그러려면 기본기는 더욱 탄탄해야 하고, 소프트웨어를 설계자로써 바라보는 시야를 길러야할 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결론은 AI에게 코딩을 시키지만, 그 코드를 설계하는건 나 자신이고, AI가 작성한 결과물을 책임지는 것도 개발자인 나 자신이라는 것이다.&lt;br&gt;그리고 결국 이 모든 과정을 책임질 수 있는 개발자만이 살아남을 것이다.&lt;br&gt;그래서 개인적으로는 AI 도구의 발전이 시니어 개발자에게 유리하다. 주니어 개발자에게 유리하다. 라는 말은 의미가 없는 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;소프트웨어 3.0 시대에는 &quot;이 물결에 잘 적응할 수 있고 AI를 어시스턴스로 인정하고 시니어 개발자의 마음가짐으로 소프트웨어 아키텍처가 되는 사람에게 유리하지 않을까?&quot;&lt;/b&gt; 라는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래는 최근에 토스 블로그에서 좋은 글이 나와서.. 공유..&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;소프트웨어 3.0 시대를 맞이하며&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;레이어드 아키텍처에 익숙한 개발자가 Claude Code를 바라보는 방법&quot; data-og-host=&quot;toss.tech&quot; data-og-source-url=&quot;https://toss.tech/article/44539&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/WwmBe/dJMb84XTCJm/AAAAAAAAAAAAAAAAAAAAAHjmxMDCNS2O6d5KuTge5Q6ORhqs5Tb0UZmZFYWgo97Y/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=qc5EzVTLMBibQBMhWwhCTYEeVsg%3D&quot; data-og-url=&quot;https://toss.tech/article/44539&quot;&gt;&lt;a href=&quot;https://toss.tech/article/44539&quot; target=&quot;_blank&quot; data-source-url=&quot;https://toss.tech/article/44539&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/WwmBe/dJMb84XTCJm/AAAAAAAAAAAAAAAAAAAAAHjmxMDCNS2O6d5KuTge5Q6ORhqs5Tb0UZmZFYWgo97Y/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=qc5EzVTLMBibQBMhWwhCTYEeVsg%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;소프트웨어 3.0 시대를 맞이하며&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;레이어드 아키텍처에 익숙한 개발자가 Claude Code를 바라보는 방법&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;toss.tech&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;잘, 아 점&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 그간 있었던 일들과 느꼈던 점을 이야기 했다면, 이번엔 잘했던 점과 아쉬웠던 점에 대해서 남겨본다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;잘했던 점&lt;/b&gt;&lt;/h4&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;잘 적응하려고 노력했고, 교육기간 많이 공부했고, 실습은 아쉬웠지만 그래도 노력했다!&lt;/span&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 4주 동안 부단히도 팀에 녹아들려고 노력했고 (I지만.. 말 많이 했던 것 같다),&lt;br&gt;교육기간 동안 딱 내가 공부해야 했던 것들을 회사에서 시켜서 많이 공부했고,&lt;br&gt;실습은 아쉬웠지만 설계단부터 더욱 명확하게 하고 들어가야겠다 라는 생각이 들었고,&lt;br&gt;Todo 실습은 나름 결과물이 나쁘지 않았다고 생각이 들어서 만족스럽긴 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번주에 실습까지 교육 과정이 끝나고 작은 업무를 할당받아 진행했다.&lt;br&gt;앞으로 쭉 컴포넌트를 유지보수 하는 업무를 진행할 것 같고, 가장 작고 쉬운 업무를 할당해주셨지만 코드를 수정하고 PR을 올리고 첫 달을 마친게 의미가 크다고 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;굉장히 치열한 한 달이였고 약 4년동안 야근 한번 하지 않았던 내가 거의 매일 1시간씩 더 학습, 일하며 적응하려고 노력했던 내 자신 많이 칭찬한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 피드백 많이 받으려고 노력했고, 피드백과 리뷰를 들었을 때 받아들일려고 노력했던 내 자신도 칭찬한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아침 일찍 일어나서 루틴 만들고, 하루를 시작한 내 자신도 칭찬한다!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;아쉬웠던 점&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;맨탈리티&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;맨탈이 좀 많이 약한게 여전히 아쉽다.&lt;br&gt;이직은 맨탈 싸움!!!! 외쳤지만 내 멘탈에 내가 져버렸고 (물론 성공하긴 했지만)&lt;br&gt;적응도 맨탈 싸움!!!! 이라고 외쳤지만 수습 3달 후 백수가 된 나를 상상하는 식으로 자해를 했던 내 자신의 맨탈이 아쉽다.&lt;br&gt;사실 스트레스 관리라는게 정말 어렵지만 요즘은 뼈저리게 느끼고 있다. &lt;s&gt;(스트레스 관리 안하다가 나 죽을거같아 ㅠㅠ)&lt;/s&gt;&lt;s&gt;&lt;/s&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;i&gt;&lt;b&gt;앞으로 힘들때마다 아래 두개의 명짤방을 계속 상기시키자..&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djiKRk/dJMcagYDpbw/NkXPOuMmQn3VmMNeVEqtl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djiKRk/dJMcagYDpbw/NkXPOuMmQn3VmMNeVEqtl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djiKRk/dJMcagYDpbw/NkXPOuMmQn3VmMNeVEqtl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjiKRk%2FdJMcagYDpbw%2FNkXPOuMmQn3VmMNeVEqtl1%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;348&quot; height=&quot;354&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;790&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;850&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LpKMF/dJMcadU62U9/TCIp2kdaserMkGGcRbOTJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LpKMF/dJMcadU62U9/TCIp2kdaserMkGGcRbOTJk/img.png&quot; data-alt=&quot;중요한 건 꺾이지 않는 마음 - 데프트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LpKMF/dJMcadU62U9/TCIp2kdaserMkGGcRbOTJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLpKMF%2FdJMcadU62U9%2FTCIp2kdaserMkGGcRbOTJk%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;850&quot; height=&quot;914&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;중요한 건 꺾이지 않는 마음 - 데프트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;설계 설계 그리고 또 설계..&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Drag &amp;amp; Drop 실습을 진행하며 처음부터 설계를 잘 하지 않았던 점이 아쉽다.&lt;br&gt;또 UX를 잘 정리하지 않고 코드를 작성하니, 리뷰에서 내가 생각했던 드래그 앤 드랍이 일반적인 드래그 앤 드랍과 달랐다는걸 알게 됐다.&lt;br&gt;그리고 전체적으로 코드 수정이 들어갔고, 시간과 기한은 그만큼 더 연장되었다.&lt;br&gt;또 이벤트 관리 방식이나, UX적인 리뷰를 받은 내용을 보충하며 코드의 복잡성이 높아졌고 이런 부분을 초반 설계 시점에 잘 잡고 갔다면 작업의 기한도 줄었고 코드의 복잡도도 더 적지 않았을까 라는 아쉬움이 들었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;업무를 하기 전에 더욱 치밀하게 고민하고, 설계하고 산출물을 작성하고, 검증하고 리뷰하고 작업을 시작하자!&lt;br&gt;앞단에서 고민을 더 깊게해야 전체적인 업무의 일정을 줄일 수 있다..&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위에 남겨 놓았듯이 1월에는 멘탈이 전반적으로 좋지 못했던 것 같다.&lt;br&gt;근데 요즘 도움이 많이 됐던 밈이 있다.&lt;br&gt;바로 김동현 밈~!!!&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rcF0n/dJMcacPveYp/IMsU73J1cbrLWo5tZMzq90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rcF0n/dJMcacPveYp/IMsU73J1cbrLWo5tZMzq90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rcF0n/dJMcacPveYp/IMsU73J1cbrLWo5tZMzq90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrcF0n%2FdJMcacPveYp%2FIMsU73J1cbrLWo5tZMzq90%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;535&quot; height=&quot;614&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;스트레스 받을 때&lt;br&gt;&lt;b&gt;말 안하지만 지금 스트레스 되게 받는다.&lt;/b&gt;&lt;br&gt;&lt;b&gt;근데 그런 스트레스도 필요하다.&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;쉬고 싶을 때&lt;br&gt;&lt;b&gt;한판 쉴래? 근데 남들은 안쉬어&lt;/b&gt;&lt;br&gt;&lt;b&gt;운동 많이 된다 (도움 많이 된다로 받아들여짐)&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;뭐 대충 누웠을 때?&lt;br&gt;&lt;b&gt;편하게 살자~ 편하게&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;쩌는 사람(팀원) 봤을 때&lt;br&gt;&lt;b&gt;예술이다 예술&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;너무 대충 위로보다는 동현이형 같은 마인드가 요즘은 더 도움이 많이 되는것 같다.&lt;br&gt;긍정적인 마인드?&lt;br&gt;어차피 다 힘든데 &quot;힘들지?&quot; 보다는 &quot;그런 스트레스도 필요하다.&quot; 가 필요한 시점인 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이런 시점에 이런 밈이 유행? (&lt;s&gt;혹시 우주의 기운이 나에게?)&lt;/s&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/95</guid>
      <comments>https://kangs-develop.tistory.com/95#entry95comment</comments>
      <pubDate>Fri, 30 Jan 2026 18:49:26 +0900</pubDate>
    </item>
    <item>
      <title>(번역) 훌륭한 프런트엔드 테스트 작성법: 37가지 팁과 요령</title>
      <link>https://kangs-develop.tistory.com/94</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;How to write good frontend tests: 37 tips and tricks&lt;/b&gt;&lt;br /&gt;(번역) 훌륭한 프런트엔드 테스트 작성법: 37가지 팁과 요령&lt;br /&gt;&lt;br /&gt;해당 글은 아래 원글을 번역한 글 입니다. 블로그는 번역된 글로 인해 취하는 영리적 이득이 없습니다. 문제가 된다면 번역된 글을 내리도록 하겠습니다.&lt;br /&gt;&lt;br /&gt;This article is a translation of the original text below. This blog does not receive any commercial benefit from the translation. If it becomes a problem, the translation will be removed.&lt;br /&gt;&lt;br /&gt;원글: &lt;a href=&quot;https://howtotestfrontend.com/resources/how-to-write-good-frontend-tests&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;How to write good frontend tests: 37 tips and tricks&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1769221768576&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;How to write good frontend tests: 37 tips and tricks | How To Test Frontend&quot; data-og-description=&quot;  Become the best at testing FE apps: Get exclusive testing tips &amp;amp; tricks delivered to your inbox Stop wasting hours debugging flaky tests. Join hundreds of developers who get practical, battle-tested testing strategies straight to their inbox. Every is&quot; data-og-host=&quot;howtotestfrontend.com&quot; data-og-source-url=&quot;https://howtotestfrontend.com/resources/how-to-write-good-frontend-tests&quot; data-og-url=&quot;https://howtotestfrontend.com/resources/how-to-write-good-frontend-tests&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://howtotestfrontend.com/resources/how-to-write-good-frontend-tests&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://howtotestfrontend.com/resources/how-to-write-good-frontend-tests&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;How to write good frontend tests: 37 tips and tricks | How To Test Frontend&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Become the best at testing FE apps: Get exclusive testing tips &amp;amp; tricks delivered to your inbox Stop wasting hours debugging flaky tests. Join hundreds of developers who get practical, battle-tested testing strategies straight to their inbox. Every is&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;howtotestfrontend.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;해당 번역글은 원글의 번역된 글이 없어 직접 번역을 진행했습니다. (with. AI)다소 의역된 번역이나 잘못 번역된 내용이 있을수도 있으니, 전체 흐름을 읽어보시고 원 글을 읽어보시면 의도 파악이 더 잘 되실겁니다.&lt;br /&gt;&lt;br /&gt;This original text was translated directly because it was unavailable. (With AI) Because it contains some inaccuracies and inaccuracies, it's better to go to the original text, which is far removed from the original text, to better understand it.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;저는&amp;nbsp;테스트와&amp;nbsp;함께&amp;nbsp;일하는&amp;nbsp;것을&amp;nbsp;좋아합니다.&amp;nbsp;테스트를&amp;nbsp;작성하는&amp;nbsp;것은&amp;nbsp;항상&amp;nbsp;즐거운&amp;nbsp;과정이고,&amp;nbsp;작성한&amp;nbsp;테스트가&amp;nbsp;통과되어&amp;nbsp;초록&amp;nbsp;불이&amp;nbsp;켜지는&amp;nbsp;순간은&amp;nbsp;언제나&amp;nbsp;기분&amp;nbsp;좋습니다.&lt;br /&gt;&lt;br /&gt;다만&amp;nbsp;기존&amp;nbsp;테스트들이&amp;nbsp;정확히&amp;nbsp;무엇을&amp;nbsp;하는지&amp;nbsp;파악하느라&amp;nbsp;애먹는&amp;nbsp;것만큼&amp;nbsp;답답한&amp;nbsp;일은&amp;nbsp;없습니다.&amp;nbsp;(가끔은&amp;nbsp;그&amp;nbsp;테스트의&amp;nbsp;원래&amp;nbsp;작성자가&amp;nbsp;바로&amp;nbsp;저였을&amp;nbsp;때도&amp;nbsp;있으니&amp;nbsp;더&amp;nbsp;황당합니다!)&lt;br /&gt;&lt;br /&gt;오늘은&amp;nbsp;고품질의&amp;nbsp;프론트엔드&amp;nbsp;테스트를&amp;nbsp;작성하기&amp;nbsp;위한&amp;nbsp;제&amp;nbsp;팁들을&amp;nbsp;공유하고자&amp;nbsp;합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;핵심을&amp;nbsp;더&amp;nbsp;쉽게&amp;nbsp;전달하기&amp;nbsp;위해&amp;nbsp;프론트엔드에&amp;nbsp;특화된&amp;nbsp;부분을&amp;nbsp;걷어내고&amp;nbsp;단순화한&amp;nbsp;예시들입니다.&amp;nbsp;이&amp;nbsp;글은&amp;nbsp;프론트엔드&amp;nbsp;앱&amp;nbsp;테스트를&amp;nbsp;주로&amp;nbsp;다루고&amp;nbsp;있지만,&amp;nbsp;사실&amp;nbsp;대부분의&amp;nbsp;깔끔한&amp;nbsp;테스트&amp;nbsp;원칙은&amp;nbsp;프론트엔드든&amp;nbsp;백엔드든&amp;nbsp;모두&amp;nbsp;동일하게&amp;nbsp;적용됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;좋은 테스트는 무엇을 검증하는지 명확합니다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를&amp;nbsp;테스트할&amp;nbsp;때는&amp;nbsp;각&amp;nbsp;단언(assertion)마다&amp;nbsp;별도의&amp;nbsp;`test()`&amp;nbsp;(또는&amp;nbsp;`it()`)를&amp;nbsp;구성하면&amp;nbsp;유지보수가&amp;nbsp;훨씬&amp;nbsp;수월합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고:&amp;nbsp;저는&amp;nbsp;'테스트&amp;nbsp;하나&amp;nbsp;=&amp;nbsp;단언&amp;nbsp;하나'라는&amp;nbsp;엄격한&amp;nbsp;규칙을&amp;nbsp;강요하려는&amp;nbsp;것이&amp;nbsp;아닙니다.&lt;br /&gt;일부 개발자가 지나치게 집착하는 터무니없는 원칙입니다. 다만 서로 관련 없는 단언들은 각각 다른 `test()` 호출로 분리하세요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예를 들어, 다음과 같은 코드는 지저분하고 유지보수하기 어렵습니다. 테스트가 실패했을 때 정확히 어떤 부분을 검증하려던 것인지 명확하지 않거든요.&lt;/p&gt;
&lt;pre id=&quot;code_1769220112715&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;component works ok&quot;, async () =&amp;gt; {
  render(&amp;lt;SomeComponent /&amp;gt;);

  // test data loads:
  expect(screen.getByText(&quot;Loading...&quot;)).toBeInTheDocument();

  await waitFor(() =&amp;gt; expect(fetch).toHaveBeenCalled());
  expect(
    await screen.findByText(&quot;your mock data from api&quot;)
  ).toBeInTheDocument();

  expect(screen.queryByText(&quot;Loading...&quot;)).toBeNull();

  // test we can create
  await userEvent.click(
    screen.getByRole(&quot;button&quot;, {
      name: &quot;Create&quot;,
    })
  );
  expect(await screen.findByText(&quot;Created ok message&quot;)).toBeInTheDocument();
  expect(fetch).toHaveBeenCalledWith(&quot;/create&quot;);

  // test we can delete
  await userEvent.click(
    screen.getByRole(&quot;button&quot;, {
      name: &quot;Delete&quot;,
    })
  );
  expect(screen.getByText(&quot;Deleted item&quot;)).toBeInTheDocument();
  expect(fetch).toHaveBeenCalledWith(&quot;/delete&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해당&amp;nbsp;테스트에서는:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;데이터를&amp;nbsp;불러오거나&amp;nbsp;`fetch`를&amp;nbsp;호출하는지&amp;nbsp;검증하고,&lt;br /&gt;-&amp;nbsp;생성&amp;nbsp;폼을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있는지&amp;nbsp;검증하고,&lt;br /&gt;-&amp;nbsp;삭제&amp;nbsp;폼을&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있는지&amp;nbsp;검증합니다.&lt;br /&gt;&lt;br /&gt;다소&amp;nbsp;억지스러운&amp;nbsp;예시이긴&amp;nbsp;하지만,&amp;nbsp;이런&amp;nbsp;식으로&amp;nbsp;여러&amp;nbsp;것을&amp;nbsp;한&amp;nbsp;번에&amp;nbsp;테스트하는&amp;nbsp;패턴은&amp;nbsp;실제로&amp;nbsp;매우&amp;nbsp;자주&amp;nbsp;발생합니다(특히&amp;nbsp;한&amp;nbsp;번에&amp;nbsp;작은&amp;nbsp;기능을&amp;nbsp;추가할&amp;nbsp;때는&amp;nbsp;기존&amp;nbsp;테스트를&amp;nbsp;수정해서&amp;nbsp;같은&amp;nbsp;테스트&amp;nbsp;블록에서&amp;nbsp;새&amp;nbsp;기능까지&amp;nbsp;함께&amp;nbsp;검사하는&amp;nbsp;것이&amp;nbsp;더&amp;nbsp;편하거든요).&lt;br /&gt;&lt;br /&gt;다만&amp;nbsp;이런&amp;nbsp;식의&amp;nbsp;테스트는&amp;nbsp;유지보수가&amp;nbsp;번거롭다는&amp;nbsp;문제가&amp;nbsp;있습니다.&amp;nbsp;테스트가&amp;nbsp;실패했을&amp;nbsp;때&amp;nbsp;정확히&amp;nbsp;어느&amp;nbsp;부분이&amp;nbsp;문제인지&amp;nbsp;즉시&amp;nbsp;파악하기&amp;nbsp;어렵기&amp;nbsp;때문입니다.&amp;nbsp;따라서&amp;nbsp;이&amp;nbsp;예시는&amp;nbsp;세&amp;nbsp;개의&amp;nbsp;독립적인&amp;nbsp;테스트로&amp;nbsp;분리하는&amp;nbsp;것을&amp;nbsp;제안합니다.&lt;br /&gt;&lt;br /&gt;매번&amp;nbsp;컴포넌트를&amp;nbsp;다시&amp;nbsp;렌더링해야&amp;nbsp;하는&amp;nbsp;세&amp;nbsp;개의&amp;nbsp;테스트로&amp;nbsp;인한&amp;nbsp;오버헤드는&amp;nbsp;더&amp;nbsp;나은&amp;nbsp;개발자&amp;nbsp;경험과&amp;nbsp;더욱&amp;nbsp;깔끔한&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;얻기&amp;nbsp;위해&amp;nbsp;충분히&amp;nbsp;감수할&amp;nbsp;가치가&amp;nbsp;있습니다.&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;&lt;b&gt;구현&amp;nbsp;세부&amp;nbsp;사항&amp;nbsp;테스트는&amp;nbsp;피하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현&amp;nbsp;세부&amp;nbsp;사항을&amp;nbsp;테스트하지&amp;nbsp;마세요.&amp;nbsp;백엔드&amp;nbsp;테스트에서는&amp;nbsp;보통&amp;nbsp;클래스의&amp;nbsp;공개&amp;nbsp;메서드만&amp;nbsp;테스트하려고&amp;nbsp;합니다.&amp;nbsp;프론트엔드&amp;nbsp;테스트도&amp;nbsp;동일한&amp;nbsp;원칙을&amp;nbsp;따릅니다.&amp;nbsp;다만&amp;nbsp;컴포넌트가&amp;nbsp;렌더링하는&amp;nbsp;결과와&amp;nbsp;사용자가&amp;nbsp;버튼&amp;nbsp;같은&amp;nbsp;요소를&amp;nbsp;통해&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;상호작용으로만&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;검증해야&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;이&amp;nbsp;팁은&amp;nbsp;자주&amp;nbsp;언급되는&amp;nbsp;만큼&amp;nbsp;원칙적으로는&amp;nbsp;명확하지만,&amp;nbsp;실제&amp;nbsp;작업에서는&amp;nbsp;구현&amp;nbsp;세부&amp;nbsp;사항을&amp;nbsp;테스트하기&amp;nbsp;시작하기&amp;nbsp;정말&amp;nbsp;쉽습니다.&amp;nbsp;그렇게&amp;nbsp;되면&amp;nbsp;UI의&amp;nbsp;실제&amp;nbsp;사용자나&amp;nbsp;공개&amp;nbsp;API가&amp;nbsp;사용할&amp;nbsp;동작이&amp;nbsp;아니라,&amp;nbsp;내부&amp;nbsp;구현&amp;nbsp;로직만&amp;nbsp;테스트하게&amp;nbsp;되는&amp;nbsp;함정에&amp;nbsp;빠집니다.&lt;br /&gt;&lt;br /&gt;예를 들어: 커스텀 훅 자체를 테스트하는 것 vs 해당 훅을 사용하는 컴포넌트를 테스트하는 것.&lt;/p&gt;
&lt;pre id=&quot;code_1769220176356&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ bad - testing implementation details
test(&quot;component uses useState correctly&quot;, () =&amp;gt; {
  const { result } = renderHook(() =&amp;gt; useState(0));
  expect(result.current[0]).toBe(0);
});

// ✅ good - testing user-facing behavior
test(&quot;counter displays initial value of 0&quot;, () =&amp;gt; {
  render(&amp;lt;Counter /&amp;gt;);
  expect(screen.getByText(&quot;Count: 0&quot;)).toBeInTheDocument();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;훅만&amp;nbsp;테스트한다면&amp;nbsp;컴포넌트가&amp;nbsp;실제로&amp;nbsp;예상대로&amp;nbsp;동작한다는&amp;nbsp;것을&amp;nbsp;보장할&amp;nbsp;수&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;게다가&amp;nbsp;컴포넌트를&amp;nbsp;리팩터링할&amp;nbsp;때(기능은&amp;nbsp;동일하지만&amp;nbsp;다른&amp;nbsp;훅으로&amp;nbsp;상태&amp;nbsp;관리&amp;nbsp;방식을&amp;nbsp;변경하는&amp;nbsp;등)&amp;nbsp;기존&amp;nbsp;테스트는&amp;nbsp;여전히&amp;nbsp;통과할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;참고:&amp;nbsp;물론&amp;nbsp;컴포넌트를&amp;nbsp;테스트하는&amp;nbsp;것이&amp;nbsp;항상&amp;nbsp;최선은&amp;nbsp;아니며,&amp;nbsp;내부의&amp;nbsp;일부&amp;nbsp;로직을&amp;nbsp;격리해서&amp;nbsp;따로&amp;nbsp;테스트하는&amp;nbsp;것이&amp;nbsp;합리적일&amp;nbsp;때도&amp;nbsp;있습니다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;컴포넌트가&amp;nbsp;사용하는&amp;nbsp;훅이&amp;nbsp;반환하는&amp;nbsp;다양한&amp;nbsp;조합들을&amp;nbsp;테스트하는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;간편할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;다만&amp;nbsp;그&amp;nbsp;훅을&amp;nbsp;다른&amp;nbsp;구현으로&amp;nbsp;교체했을&amp;nbsp;때&amp;nbsp;컴포넌트&amp;nbsp;레벨의&amp;nbsp;테스트&amp;nbsp;커버리지가&amp;nbsp;사라질&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;점을&amp;nbsp;반드시&amp;nbsp;기억하세요...&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;&lt;b&gt;서드파티&amp;nbsp;코드가&amp;nbsp;아닌&amp;nbsp;애플리케이션&amp;nbsp;로직을&amp;nbsp;테스트하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크나&amp;nbsp;라이브러리(React,&amp;nbsp;Next.js)&amp;nbsp;고유의&amp;nbsp;코드를&amp;nbsp;테스트하지&amp;nbsp;마세요.&lt;br /&gt;프레임워크나 라이브러리를 직접 테스트하는 것은 큰 의미가 없습니다.&lt;br /&gt;물론 그것을 사용하는 여러분의 코드는 테스트하세요.&lt;br /&gt;&lt;br /&gt;이는&amp;nbsp;React&amp;nbsp;앱의&amp;nbsp;경우&amp;nbsp;다음을&amp;nbsp;의미합니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;JSX를&amp;nbsp;받아&amp;nbsp;렌더링하는지&amp;nbsp;테스트할&amp;nbsp;필요가&amp;nbsp;없습니다.&lt;br /&gt;-`style={someStyleObject}`처럼 스타일 객체를 전달했을 때 React가 스타일을 올바르게 설정하는지 테스트해도 소용없습니다.&lt;br /&gt;&lt;br /&gt;조금 덜 명확한 부분은 Next.js 앱이 페이지를 변경하는지 확인하는 방법 같은 경우입니다. 유일하게 신뢰할 수 있는 방법은 Playwright나 Cypress 같은 엔드투엔드 테스트를 실행해 URL이 변경되었는지 확인하는 것임을 알게 됩니다.&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;&lt;b&gt;React&amp;nbsp;Testing&amp;nbsp;Library&amp;nbsp;또는&amp;nbsp;Vitest&amp;nbsp;브라우저&amp;nbsp;모드에서&amp;nbsp;올바른&amp;nbsp;쿼리&amp;nbsp;함수&amp;nbsp;사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;사이트에&amp;nbsp;이&amp;nbsp;주제로&amp;nbsp;많은&amp;nbsp;글을&amp;nbsp;작성했지만,&amp;nbsp;적절한&amp;nbsp;쿼리&amp;nbsp;함수를&amp;nbsp;사용하는&amp;nbsp;것은&amp;nbsp;정말&amp;nbsp;중요합니다(예:&amp;nbsp;`getByText()`,&amp;nbsp;`getByPlaceholderText()`&amp;nbsp;등).&lt;br /&gt;&lt;br /&gt;올바른&amp;nbsp;쿼리&amp;nbsp;함수를&amp;nbsp;사용하면&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;이점이&amp;nbsp;있습니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;읽기가&amp;nbsp;훨씬&amp;nbsp;쉬워집니다(테스트의&amp;nbsp;의도가&amp;nbsp;더욱&amp;nbsp;명확해집니다).&lt;br /&gt;-&amp;nbsp;더욱&amp;nbsp;현실적인&amp;nbsp;테스트를&amp;nbsp;작성하게&amp;nbsp;됩니다(예:&amp;nbsp;라벨&amp;nbsp;텍스트로&amp;nbsp;요소를&amp;nbsp;찾는&amp;nbsp;방식은&amp;nbsp;실제&amp;nbsp;사용자가&amp;nbsp;폼을&amp;nbsp;다루는&amp;nbsp;방식과&amp;nbsp;동일합니다).&lt;br /&gt;-&amp;nbsp;시맨틱하게&amp;nbsp;올바른&amp;nbsp;HTML&amp;nbsp;요소를&amp;nbsp;사용하도록&amp;nbsp;자연스럽게&amp;nbsp;유도되어&amp;nbsp;접근성&amp;nbsp;향상에&amp;nbsp;도움이&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;예시 코드:&lt;/p&gt;
&lt;pre id=&quot;code_1769220260817&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ avoid this - getByTestId is a fallback when there is nothing better to use
const button = screen.getByTestId(&quot;submit-btn&quot;);
const heading = screen.getByTestId(&quot;main-title&quot;);
const input = screen.getByTestId(&quot;email-input&quot;);

// ✅ Good - using semantic queries in order of priority
const button = screen.getByRole(&quot;button&quot;, { name: &quot;Submit&quot; });
const heading = screen.getByRole(&quot;heading&quot;, { name: &quot;Sign Up&quot; });
const input = screen.getByLabelText(&quot;Email address&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;RTL&amp;nbsp;쿼리&amp;nbsp;우선순위는&amp;nbsp;다음과&amp;nbsp;같습니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;`getByRole`&amp;nbsp;-&amp;nbsp;버튼,&amp;nbsp;제목,&amp;nbsp;폼에&amp;nbsp;가장&amp;nbsp;적합&lt;br /&gt;-&amp;nbsp;`getByLabelText`&amp;nbsp;-&amp;nbsp;폼&amp;nbsp;입력에&amp;nbsp;가장&amp;nbsp;적합&lt;br /&gt;-&amp;nbsp;`getByPlaceholderText`&amp;nbsp;-&amp;nbsp;레이블이&amp;nbsp;없는&amp;nbsp;입력에&amp;nbsp;적합&lt;br /&gt;-&amp;nbsp;`getByText`&amp;nbsp;-&amp;nbsp;비인터랙티브&amp;nbsp;콘텐츠용&lt;br /&gt;-&amp;nbsp;`getByDisplayValue`&amp;nbsp;-&amp;nbsp;값이&amp;nbsp;있는&amp;nbsp;폼&amp;nbsp;요소용&lt;br /&gt;-&amp;nbsp;`getByAltText`&amp;nbsp;-&amp;nbsp;이미지용&lt;br /&gt;-&amp;nbsp;`getByTitle`&amp;nbsp;-&amp;nbsp;title&amp;nbsp;속성이&amp;nbsp;있는&amp;nbsp;요소용&lt;br /&gt;-&amp;nbsp;`getByTestId`&amp;nbsp;-&amp;nbsp;최후의&amp;nbsp;수단&lt;br /&gt;&lt;br /&gt;참고:&amp;nbsp;`getByRole`은&amp;nbsp;다른&amp;nbsp;것들&amp;nbsp;중&amp;nbsp;가장&amp;nbsp;느릴&amp;nbsp;수&amp;nbsp;있으므로&amp;nbsp;우선순위가&amp;nbsp;높더라도&amp;nbsp;요소가&amp;nbsp;많은&amp;nbsp;컴포넌트에서&amp;nbsp;테스트가&amp;nbsp;느려지는&amp;nbsp;경우&amp;nbsp;이를&amp;nbsp;염두에&amp;nbsp;두어야&amp;nbsp;합니다.&amp;nbsp;테스트를&amp;nbsp;작성할&amp;nbsp;때&amp;nbsp;가장&amp;nbsp;쉬운&amp;nbsp;방법은&amp;nbsp;모든&amp;nbsp;요소에&amp;nbsp;`data-testid`를&amp;nbsp;추가하고&amp;nbsp;`getByTestId(...)`를&amp;nbsp;사용하는&amp;nbsp;것입니다.&amp;nbsp;하지만&amp;nbsp;조금만&amp;nbsp;더&amp;nbsp;노력하면&amp;nbsp;더&amp;nbsp;나은&amp;nbsp;쿼리&amp;nbsp;함수를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;참고: `data-testid`는 분명 활용처가 있습니다. 때로는 테스트를 작성하고 유지보수하는 일을 훨씬 수월하게 만듭니다. 다만 남용하지 마세요.&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;&lt;b&gt;모킹보다는&amp;nbsp;실제&amp;nbsp;구현을&amp;nbsp;테스트하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는&amp;nbsp;React&amp;nbsp;컴포넌트를&amp;nbsp;테스트하면서&amp;nbsp;그&amp;nbsp;안에&amp;nbsp;포함된&amp;nbsp;모든&amp;nbsp;자식&amp;nbsp;컴포넌트를&amp;nbsp;모킹해&amp;nbsp;둔&amp;nbsp;테스트를&amp;nbsp;마주하게&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;이는&amp;nbsp;단위&amp;nbsp;테스트에&amp;nbsp;대한&amp;nbsp;구식&amp;nbsp;사고방식에서&amp;nbsp;비롯된&amp;nbsp;것으로,&amp;nbsp;대상을&amp;nbsp;완전히&amp;nbsp;격리해서&amp;nbsp;테스트해야&amp;nbsp;한다는&amp;nbsp;원칙에서&amp;nbsp;나온&amp;nbsp;것입니다.&amp;nbsp;제&amp;nbsp;생각에는&amp;nbsp;이러한&amp;nbsp;접근&amp;nbsp;방식은&amp;nbsp;피해야&amp;nbsp;할&amp;nbsp;구식&amp;nbsp;테스트&amp;nbsp;패턴입니다.&lt;br /&gt;&lt;br /&gt;물론&amp;nbsp;때로는&amp;nbsp;초반에&amp;nbsp;모킹으로&amp;nbsp;작성하는&amp;nbsp;것이&amp;nbsp;더&amp;nbsp;쉬울&amp;nbsp;수&amp;nbsp;있습니다(자식&amp;nbsp;컴포넌트들에&amp;nbsp;신경&amp;nbsp;쓸&amp;nbsp;필요가&amp;nbsp;없으니까요.&amp;nbsp;그냥&amp;nbsp;동작하지&amp;nbsp;않도록&amp;nbsp;모킹하면&amp;nbsp;되거든요).&lt;br /&gt;&lt;br /&gt;저는 AI에게 컴포넌트를 테스트해 달라고 하면 과도하게 모킹하는 경향이 있다는 점을 눈여겨봤습니다(아마도 다른 컴포넌트들이 실제로 어떻게 작동해야 할지 확신이 서지 않아서 그런 것으로 추측됩니다).&lt;/p&gt;
&lt;pre id=&quot;code_1769220307234&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - over-mocking everything
vi.mock(&quot;./Header&quot;, () =&amp;gt; ({
  Header: () =&amp;gt; &amp;lt;div data-testid=&quot;header&quot;&amp;gt;Mocked Header&amp;lt;/div&amp;gt;,
}));

vi.mock(&quot;./Sidebar&quot;, () =&amp;gt; ({
  Sidebar: () =&amp;gt; &amp;lt;div data-testid=&quot;sidebar&quot;&amp;gt;Mocked Sidebar&amp;lt;/div&amp;gt;,
}));

vi.mock(&quot;./Footer&quot;, () =&amp;gt; ({
  Footer: () =&amp;gt; &amp;lt;div data-testid=&quot;footer&quot;&amp;gt;Mocked Footer&amp;lt;/div&amp;gt;,
}));

vi.mock(&quot;./UserProfile&quot;, () =&amp;gt; ({
  UserProfile: () =&amp;gt; &amp;lt;div data-testid=&quot;user-profile&quot;&amp;gt;Mocked User&amp;lt;/div&amp;gt;,
}));

test(&quot;dashboard renders correctly&quot;, () =&amp;gt; {
  render(&amp;lt;Dashboard /&amp;gt;);

  expect(screen.getByTestId(&quot;header&quot;)).toBeInTheDocument();
  expect(screen.getByTestId(&quot;sidebar&quot;)).toBeInTheDocument();
  expect(screen.getByTestId(&quot;footer&quot;)).toBeInTheDocument();
  expect(screen.getByTestId(&quot;user-profile&quot;)).toBeInTheDocument();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;테스트는&amp;nbsp;실제&amp;nbsp;구현을&amp;nbsp;검증하는&amp;nbsp;것이&amp;nbsp;아닙니다.&amp;nbsp;단지&amp;nbsp;테스트용&amp;nbsp;목(mock)을&amp;nbsp;테스트하고&amp;nbsp;있을&amp;nbsp;뿐이라서,&amp;nbsp;저는&amp;nbsp;그&amp;nbsp;테스트에서&amp;nbsp;실질적인&amp;nbsp;가치를&amp;nbsp;찾기&amp;nbsp;어렵습니다.&lt;br /&gt;&lt;br /&gt;다음은&amp;nbsp;개선된&amp;nbsp;버전입니다&amp;nbsp;&amp;mdash;&amp;nbsp;여전히&amp;nbsp;일부를&amp;nbsp;목(mock)으로&amp;nbsp;처리하지만&amp;nbsp;이번에는&amp;nbsp;데이터&amp;nbsp;가져오기&amp;nbsp;부분만&amp;nbsp;격리합니다.&amp;nbsp;또한&amp;nbsp;헤더나&amp;nbsp;사이드바&amp;nbsp;같은&amp;nbsp;각&amp;nbsp;요소가&amp;nbsp;DOM에&amp;nbsp;특정&amp;nbsp;`data-testid`를&amp;nbsp;가지고&amp;nbsp;있는지만&amp;nbsp;확인하는&amp;nbsp;것이&amp;nbsp;아니라,&amp;nbsp;실제로&amp;nbsp;예상한&amp;nbsp;값을&amp;nbsp;정확히&amp;nbsp;포함하고&amp;nbsp;있는지도&amp;nbsp;함께&amp;nbsp;검증합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220326784&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vi.mock(&quot;./api/userService&quot;, () =&amp;gt; ({
  fetchUserData: vi.fn().mockResolvedValue({
    name: &quot;John Doe&quot;,
    email: &quot;john@example.com&quot;,
  }),
}));

test(&quot;dashboard displays user information after loading&quot;, async () =&amp;gt; {
  render(&amp;lt;Dashboard /&amp;gt;);

  // Test that user data appears
  expect(await screen.findByText(&quot;Welcome, John Doe&quot;)).toBeInTheDocument();
  expect(screen.getByText(&quot;john@example.com&quot;)).toBeInTheDocument();

  // Test that loading indicator disappears
  expect(screen.queryByText(&quot;Loading...&quot;)).not.toBeInTheDocument();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제가&amp;nbsp;`mock`&amp;nbsp;또는&amp;nbsp;`spyOn`을&amp;nbsp;사용하는&amp;nbsp;경우는&amp;nbsp;다음과&amp;nbsp;같습니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;API&amp;nbsp;응답&amp;nbsp;모킹&lt;br /&gt;-&amp;nbsp;오류&amp;nbsp;상황&amp;nbsp;모킹(컴포넌트가&amp;nbsp;오류를&amp;nbsp;제대로&amp;nbsp;처리하는지&amp;nbsp;검증하기&amp;nbsp;위해)&lt;br /&gt;-&amp;nbsp;타사&amp;nbsp;라이브러리나&amp;nbsp;컴포넌트&amp;nbsp;모킹&lt;br /&gt;-&amp;nbsp;컴포넌트가&amp;nbsp;React&amp;nbsp;Testing&amp;nbsp;Library에서&amp;nbsp;지원하지&amp;nbsp;않는&amp;nbsp;Canvas&amp;nbsp;같은&amp;nbsp;웹&amp;nbsp;API를&amp;nbsp;사용할&amp;nbsp;때&amp;nbsp;모킹(이상적으로는&amp;nbsp;해당&amp;nbsp;부분을&amp;nbsp;컴포넌트에서&amp;nbsp;분리해&amp;nbsp;별도로&amp;nbsp;export하면&amp;nbsp;컴포넌트의&amp;nbsp;특정&amp;nbsp;부분만&amp;nbsp;모킹할&amp;nbsp;수&amp;nbsp;있습니다.)&lt;br /&gt;-&amp;nbsp;다른&amp;nbsp;곳에서&amp;nbsp;이미&amp;nbsp;충분히&amp;nbsp;테스트되었으며&amp;nbsp;현재&amp;nbsp;컴포넌트의&amp;nbsp;테스트와는&amp;nbsp;무관한&amp;nbsp;부분&amp;nbsp;모킹&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;&lt;b&gt;모킹이&amp;nbsp;필요하다면&amp;nbsp;과도한&amp;nbsp;모킹은&amp;nbsp;피하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`vi.mock()`&amp;nbsp;또는&amp;nbsp;`jest.mock()`(혹은&amp;nbsp;`.doMock()`&amp;nbsp;같은&amp;nbsp;변형)을&amp;nbsp;사용하기로&amp;nbsp;결정한다면,&amp;nbsp;해당&amp;nbsp;모듈&amp;nbsp;전체를&amp;nbsp;모킹하게&amp;nbsp;된다는&amp;nbsp;점을&amp;nbsp;반드시&amp;nbsp;인식해야&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;즉,&amp;nbsp;그&amp;nbsp;모듈이&amp;nbsp;새로운&amp;nbsp;내보내기를&amp;nbsp;추가하기&amp;nbsp;시작하면,&amp;nbsp;모킹해&amp;nbsp;둔&amp;nbsp;부분들을&amp;nbsp;모두&amp;nbsp;업데이트해야&amp;nbsp;할&amp;nbsp;가능성이&amp;nbsp;높습니다.&lt;br /&gt;&lt;br /&gt;모듈을&amp;nbsp;모킹해&amp;nbsp;둔&amp;nbsp;뒤&amp;nbsp;몇&amp;nbsp;개월이&amp;nbsp;지나&amp;nbsp;새로운&amp;nbsp;export가&amp;nbsp;추가되면서&amp;nbsp;모든&amp;nbsp;테스트가&amp;nbsp;깨지는&amp;nbsp;상황을&amp;nbsp;여러&amp;nbsp;번&amp;nbsp;목격했습니다.&lt;br /&gt;&lt;br /&gt;`vi.importActual()`(Vitest의&amp;nbsp;경우)이나&amp;nbsp;Jest에서는&amp;nbsp;`jest.requireActual()`을&amp;nbsp;활용한&amp;nbsp;우회&amp;nbsp;방법이&amp;nbsp;도움이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220375292&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - mocking entire module, hard to maintain
vi.mock(&quot;./userService&quot;, () =&amp;gt; ({
  fetchUser: vi.fn().mockResolvedValue({
    name: &quot;John&quot;,
  }),
  updateUser: vi.fn(),
  deleteUser: vi.fn(),
}));

// ✅ Better - import actual module and only mock what you need
vi.mock(&quot;./userService&quot;, async () =&amp;gt; {
  const actual = await vi.importActual(&quot;./userService&quot;);
  return {
    ...actual,
    fetchUser: vi.fn().mockResolvedValue({
      name: &quot;John&quot;,
    }),
    // Keep all other exports as they are
  };
});

// Jest version:
jest.mock(&quot;./userService&quot;, () =&amp;gt; ({
  ...jest.requireActual(&quot;./userService&quot;),
  fetchUser: jest.fn().mockResolvedValue({
    name: &quot;John&quot;,
  }),
  // Keep all other exports as they are
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;이렇게&amp;nbsp;하면&amp;nbsp;`userService`&amp;nbsp;모듈에&amp;nbsp;새로운&amp;nbsp;`export`가&amp;nbsp;추가되더라도&amp;nbsp;모킹할&amp;nbsp;특정&amp;nbsp;함수만&amp;nbsp;오버라이드하므로&amp;nbsp;테스트가&amp;nbsp;깨지지&amp;nbsp;않습니다.&lt;br /&gt;&lt;br /&gt;또&amp;nbsp;다른(더&amp;nbsp;나은)&amp;nbsp;우회책은&amp;nbsp;모킹할&amp;nbsp;항목들을&amp;nbsp;별도의&amp;nbsp;파일로&amp;nbsp;분리해서&amp;nbsp;해당&amp;nbsp;파일&amp;nbsp;전체를&amp;nbsp;안전하게&amp;nbsp;모킹하는&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;하지만 훨씬 더 좋은 해결책은 `vi.spyOn()` (또는 `jest.spyOn()`)을 사용하는 것입니다. 자동으로 TypeScript 타입 정보를 제공하고, 테스트 후 쉽게 복원할 수 있으며, 테스트를 읽을 때 정확히 어떤 일이 일어나는지 훨씬 더 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220388693&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ Good - spy on specific methods instead of mocking entire modules
beforeEach(() =&amp;gt; {
  vi.spyOn(userService, &quot;fetchUser&quot;).mockResolvedValue({
    name: &quot;John&quot;,
  });
});&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;간헐적&amp;nbsp;실패&amp;nbsp;테스트&amp;nbsp;해결을&amp;nbsp;최우선으로&amp;nbsp;하세요&lt;br /&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로&amp;nbsp;저는&amp;nbsp;간헐적으로&amp;nbsp;실패하는&amp;nbsp;테스트를&amp;nbsp;프로덕션에&amp;nbsp;올라간&amp;nbsp;버그만큼이나&amp;nbsp;심각하게&amp;nbsp;봅니다.&lt;br /&gt;&lt;br /&gt;테스트&amp;nbsp;중&amp;nbsp;일부가&amp;nbsp;때로는&amp;nbsp;통과했다가&amp;nbsp;때로는&amp;nbsp;실패해서&amp;nbsp;계속&amp;nbsp;재실행해야&amp;nbsp;한다면:&lt;br /&gt;&lt;br /&gt;첫째,&amp;nbsp;명백한&amp;nbsp;시간&amp;nbsp;낭비입니다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;간헐적&amp;nbsp;실패&amp;nbsp;테스트는&amp;nbsp;단순히&amp;nbsp;테스트&amp;nbsp;코드의&amp;nbsp;문제뿐만&amp;nbsp;아니라&amp;nbsp;앱&amp;nbsp;자체의&amp;nbsp;실제&amp;nbsp;버그&amp;nbsp;때문일&amp;nbsp;수도&amp;nbsp;있습니다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;결국&amp;nbsp;엔지니어들은&amp;nbsp;특정&amp;nbsp;테스트&amp;nbsp;파일이&amp;nbsp;항상&amp;nbsp;실패한다고&amp;nbsp;판단해&amp;nbsp;CI&amp;nbsp;검증을&amp;nbsp;무시하고&amp;nbsp;병합해&amp;nbsp;버립니다.&amp;nbsp;그러면&amp;nbsp;자신도&amp;nbsp;모르게&amp;nbsp;또&amp;nbsp;다른&amp;nbsp;실패한&amp;nbsp;테스트를&amp;nbsp;함께&amp;nbsp;병합하게&amp;nbsp;되는&amp;nbsp;악순환이&amp;nbsp;쉽게&amp;nbsp;발생합니다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;간헐적으로&amp;nbsp;실패하는&amp;nbsp;테스트를&amp;nbsp;계속&amp;nbsp;재실행하기보다&amp;nbsp;삭제하는&amp;nbsp;것이&amp;nbsp;나을&amp;nbsp;때도&amp;nbsp;있습니다.&lt;br /&gt;하지만&amp;nbsp;이상적으로는&amp;nbsp;테스트의&amp;nbsp;불안정성을&amp;nbsp;근본적으로&amp;nbsp;제거해야&amp;nbsp;합니다!&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;성공 경로만 테스트하지 마세요&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모든&amp;nbsp;것이&amp;nbsp;정상적으로&amp;nbsp;작동하는&amp;nbsp;경우만&amp;nbsp;테스트하지&amp;nbsp;말라는&amp;nbsp;의미입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;'성공&amp;nbsp;경로'는&amp;nbsp;가장&amp;nbsp;중요한&amp;nbsp;경로입니다&amp;nbsp;&amp;mdash;&amp;nbsp;오류&amp;nbsp;없이&amp;nbsp;의도한&amp;nbsp;대로&amp;nbsp;'성공적으로'&amp;nbsp;실행되는&amp;nbsp;흐름을&amp;nbsp;말합니다.&lt;br /&gt;&lt;br /&gt;예를&amp;nbsp;들어,&amp;nbsp;'문의&amp;nbsp;양식이&amp;nbsp;정상적으로&amp;nbsp;제출되어&amp;nbsp;양식&amp;nbsp;정보를&amp;nbsp;백엔드로&amp;nbsp;전송한다'는&amp;nbsp;것이&amp;nbsp;성공&amp;nbsp;경로입니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;실패&amp;nbsp;경로도&amp;nbsp;똑같이&amp;nbsp;중요합니다.&lt;br /&gt;&lt;br /&gt;실패&amp;nbsp;경로는&amp;nbsp;성공&amp;nbsp;경로의&amp;nbsp;반대편입니다.&amp;nbsp;즉,&amp;nbsp;뭔가&amp;nbsp;잘못된&amp;nbsp;상황입니다.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;오류&amp;nbsp;상태&lt;br /&gt;-&amp;nbsp;양식&amp;nbsp;검증&amp;nbsp;실패&lt;br /&gt;-&amp;nbsp;네트워크&amp;nbsp;문제&lt;br /&gt;-&amp;nbsp;인증&amp;nbsp;또는&amp;nbsp;권한&amp;nbsp;문제&lt;br /&gt;-&amp;nbsp;그리고&amp;nbsp;엣지&amp;nbsp;케이스&lt;br /&gt;&lt;br /&gt;이&amp;nbsp;모든&amp;nbsp;것은&amp;nbsp;실제&amp;nbsp;사용자가&amp;nbsp;경험하게&amp;nbsp;될&amp;nbsp;상황의&amp;nbsp;일부입니다.&amp;nbsp;그리고&amp;nbsp;이런&amp;nbsp;경우들은&amp;nbsp;어떤&amp;nbsp;QA&amp;nbsp;부서도&amp;nbsp;성공&amp;nbsp;경로의&amp;nbsp;버그만큼&amp;nbsp;쉽게&amp;nbsp;발견하지&amp;nbsp;못하는&amp;nbsp;유형의&amp;nbsp;문제들입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220456953&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ Good - testing the happy path
test(&quot;submits contact form successfully&quot;, async () =&amp;gt; {
  render(&amp;lt;ContactForm /&amp;gt;);

  // ... fill out form
  // ... click submit

  expect(await screen.findByText(&quot;Message sent successfully!&quot;)).toBeVisible();
});

// ✅ Also good - testing the sad path
test(&quot;shows error when form submission fails&quot;, async () =&amp;gt; {
  // Mock API to return error
  vi.mocked(fetch).mockRejectedValueOnce(new Error(&quot;Network error&quot;));

  render(&amp;lt;ContactForm /&amp;gt;);

  // ... fill out form
  // ... click submit

  expect(
    await screen.findByText(&quot;Failed to send message. Please try again.&quot;)
  ).toBeVisible();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;좋은&amp;nbsp;테스트는&amp;nbsp;스냅샷을&amp;nbsp;과도하게&amp;nbsp;사용하지&amp;nbsp;않습니다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는&amp;nbsp;`.toMatchInlineSnapshot()`을&amp;nbsp;정말&amp;nbsp;좋아합니다.&amp;nbsp;개발할&amp;nbsp;때&amp;nbsp;항상&amp;nbsp;활용합니다.&lt;br /&gt;&lt;br /&gt;TDD를&amp;nbsp;할&amp;nbsp;때도,&amp;nbsp;때로는&amp;nbsp;복잡한&amp;nbsp;객체를&amp;nbsp;`.toStrictEqual(...)`에&amp;nbsp;일일이&amp;nbsp;작성하는&amp;nbsp;것보다&amp;nbsp;인라인&amp;nbsp;스냅샷을&amp;nbsp;사용하는&amp;nbsp;편이&amp;nbsp;훨씬&amp;nbsp;빠르다는&amp;nbsp;것을&amp;nbsp;압니다.&lt;br /&gt;&lt;br /&gt;다만&amp;nbsp;제&amp;nbsp;생각에는&amp;nbsp;스냅샷이&amp;nbsp;많은&amp;nbsp;문제의&amp;nbsp;원인이&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;코드베이스가&amp;nbsp;스냅샷을&amp;nbsp;과도하게&amp;nbsp;사용하면&amp;nbsp;우리는&amp;nbsp;모두&amp;nbsp;yarn&amp;nbsp;test:watch에&amp;nbsp;익숙해져서&amp;nbsp;u를&amp;nbsp;눌러&amp;nbsp;스냅샷을&amp;nbsp;업데이트하는&amp;nbsp;습관이&amp;nbsp;생깁니다.&amp;nbsp;그러면&amp;nbsp;버그가&amp;nbsp;그대로&amp;nbsp;통과하기&amp;nbsp;쉽고,&amp;nbsp;발견하는&amp;nbsp;것도&amp;nbsp;정말&amp;nbsp;어렵습니다.&lt;br /&gt;&lt;br /&gt;저는&amp;nbsp;오류&amp;nbsp;메시지&amp;nbsp;문자열&amp;nbsp;같은&amp;nbsp;경우에는&amp;nbsp;스냅샷을&amp;nbsp;좋아합니다.&amp;nbsp;특히&amp;nbsp;향후&amp;nbsp;수정&amp;nbsp;시&amp;nbsp;자주&amp;nbsp;바뀔&amp;nbsp;것으로&amp;nbsp;예상될&amp;nbsp;때는&amp;nbsp;더욱&amp;nbsp;그렇습니다.&amp;nbsp;이&amp;nbsp;정도면&amp;nbsp;거의&amp;nbsp;단점&amp;nbsp;없이&amp;nbsp;개발&amp;nbsp;속도를&amp;nbsp;높여줍니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;거대한&amp;nbsp;객체나&amp;nbsp;방대한&amp;nbsp;배열,&amp;nbsp;직렬화된&amp;nbsp;DOM과&amp;nbsp;함께&amp;nbsp;사용하면&amp;nbsp;테스트가&amp;nbsp;금세&amp;nbsp;매우&amp;nbsp;복잡해집니다.&amp;nbsp;테스트와&amp;nbsp;직접&amp;nbsp;관련된&amp;nbsp;부분만&amp;nbsp;검증하는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;낫습니다.&lt;br /&gt;&lt;br /&gt;예를 들어 과도하게 복잡한 스냅샷을 사용하는 대신:&lt;/p&gt;
&lt;pre id=&quot;code_1769220490980&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;updates theme to dark mode&quot;, () =&amp;gt; {
  updateTheme(mockUserId, &quot;dark&quot;);

  const userProfile = getUserProfile(mockUserId);

  expect(userProfile).toMatchInlineSnapshot(`
    {
      &quot;id&quot;: &quot;user123&quot;,
      &quot;name&quot;: &quot;John Doe&quot;,
      &quot;email&quot;: &quot;john@example.com&quot;,
      &quot;preferences&quot;: {
        &quot;theme&quot;: &quot;dark&quot;,
        &quot;language&quot;: &quot;en&quot;,
        &quot;notifications&quot;: {
          &quot;email&quot;: true,
          &quot;push&quot;: false,
          &quot;sms&quot;: true,
          &quot;marketing&quot;: false,
          &quot;newsletter&quot;: true,
        },
        &quot;privacy&quot;: {
          &quot;showEmail&quot;: false,
          &quot;showPhone&quot;: true,
          &quot;allowAnalytics&quot;: true,
          &quot;cookieConsent&quot;: true,
        },
      },
      &quot;profile&quot;: {
        &quot;bio&quot;: &quot;Software engineer passionate about testing&quot;,
        &quot;location&quot;: &quot;San Francisco, CA&quot;,
        &quot;website&quot;: &quot;https://johndoe.dev&quot;,
        &quot;socialMedia&quot;: {
          &quot;twitter&quot;: &quot;@johndoe&quot;,
          &quot;github&quot;: &quot;johndoe123&quot;,
          &quot;linkedin&quot;: &quot;john-doe-engineer&quot;,
        },
      },
      &quot;metadata&quot;: {
        &quot;createdAt&quot;: &quot;2023-01-15T10:30:00Z&quot;,
        &quot;updatedAt&quot;: &quot;2024-03-20T14:45:30Z&quot;,
        &quot;lastLoginAt&quot;: &quot;2024-03-21T09:15:22Z&quot;,
        &quot;loginCount&quot;: 247,
        &quot;accountStatus&quot;: &quot;active&quot;,
      },
    }
  `);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그&amp;nbsp;테스트가&amp;nbsp;실패하기&amp;nbsp;시작할&amp;nbsp;때,&amp;nbsp;우리가&amp;nbsp;거기서&amp;nbsp;실제로&amp;nbsp;무엇을&amp;nbsp;검증하고&amp;nbsp;있었는지&amp;nbsp;어떻게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있을까요?&lt;br /&gt;&lt;br /&gt;저는&amp;nbsp;다음&amp;nbsp;버전이&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;깔끔하고&amp;nbsp;이&amp;nbsp;특정&amp;nbsp;테스트에서&amp;nbsp;중요한&amp;nbsp;부분에만&amp;nbsp;초점을&amp;nbsp;맞춘다고&amp;nbsp;생각합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220503440&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;updates theme to dark mode&quot;, () =&amp;gt; {
  updateTheme(mockUserId, &quot;dark&quot;);

  const userProfile = getUserProfile(mockUserId);

  expect(userProfile.preferences.theme).toBe(&quot;dark&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;두&amp;nbsp;번째&amp;nbsp;버전이&amp;nbsp;훨씬&amp;nbsp;읽기&amp;nbsp;쉬우며&amp;nbsp;테스트가&amp;nbsp;실제로&amp;nbsp;무엇을&amp;nbsp;검증하는지&amp;nbsp;명확하게&amp;nbsp;이해할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;또&amp;nbsp;다른&amp;nbsp;장점은&amp;nbsp;사용자&amp;nbsp;프로필&amp;nbsp;데이터&amp;nbsp;구조에&amp;nbsp;새로운&amp;nbsp;속성이&amp;nbsp;추가되거나&amp;nbsp;일부&amp;nbsp;속성이&amp;nbsp;제거되더라도(단,&amp;nbsp;`userProfile.preferences.theme`이&amp;nbsp;변경되지&amp;nbsp;않는&amp;nbsp;한)&amp;nbsp;이&amp;nbsp;테스트는&amp;nbsp;영향을&amp;nbsp;받지&amp;nbsp;않는다는&amp;nbsp;점입니다.&lt;br /&gt;&lt;br /&gt;스냅샷을&amp;nbsp;사용한다면&amp;nbsp;제가&amp;nbsp;드릴&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;주요&amp;nbsp;팁이&amp;nbsp;있습니다:&lt;br /&gt;&lt;br /&gt;테스트&amp;nbsp;실패를&amp;nbsp;보고&amp;nbsp;'u'&amp;nbsp;키를&amp;nbsp;눌러(스냅샷을&amp;nbsp;업데이트한&amp;nbsp;뒤)&amp;nbsp;바로&amp;nbsp;커밋하는&amp;nbsp;개발자들을&amp;nbsp;특히&amp;nbsp;경계하세요.&lt;br /&gt;&lt;br /&gt;버그가&amp;nbsp;도입된&amp;nbsp;것을&amp;nbsp;알아차리지&amp;nbsp;못하기&amp;nbsp;쉽고,&amp;nbsp;결과적으로&amp;nbsp;우리는&amp;nbsp;테스트만&amp;nbsp;업데이트해서&amp;nbsp;버그를&amp;nbsp;통과시킨&amp;nbsp;꼴이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;HTML의&amp;nbsp;스냅샷을&amp;nbsp;찍는&amp;nbsp;것은&amp;nbsp;피하세요.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;`expect(screen.getByText('hi')).toMatchInlineSnapshot(...)`&amp;nbsp;같은&amp;nbsp;방식&amp;nbsp;말입니다.&lt;br /&gt;&lt;br /&gt;이렇게&amp;nbsp;하면&amp;nbsp;결국&amp;nbsp;유지보수하기&amp;nbsp;어려운&amp;nbsp;거대한&amp;nbsp;HTML&amp;nbsp;덩어리가&amp;nbsp;될&amp;nbsp;것입니다(업데이트하려면&amp;nbsp;'u'를&amp;nbsp;눌러&amp;nbsp;갱신하는&amp;nbsp;것&amp;nbsp;외에는&amp;nbsp;방법이&amp;nbsp;없거든요).&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;&lt;b&gt;명확하게&amp;nbsp;명명된&amp;nbsp;테스트와&amp;nbsp;잘&amp;nbsp;정리된&amp;nbsp;테스트&amp;nbsp;파일&amp;nbsp;구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩된&amp;nbsp;`describe()`&amp;nbsp;블록과&amp;nbsp;명확한&amp;nbsp;제목을&amp;nbsp;가진&amp;nbsp;`test()`&amp;nbsp;(또는&amp;nbsp;`it()`)를&amp;nbsp;적절히&amp;nbsp;조합하고,&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;`.each()`를&amp;nbsp;활용하면&amp;nbsp;테스트&amp;nbsp;유지보수가&amp;nbsp;훨씬&amp;nbsp;수월합니다.&lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;통해&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;이점을&amp;nbsp;얻을&amp;nbsp;수&amp;nbsp;있습니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;테스트를&amp;nbsp;빠르고&amp;nbsp;쉽게&amp;nbsp;찾을&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;관련&amp;nbsp;동작들이&amp;nbsp;서로&amp;nbsp;가깝게&amp;nbsp;테스트되도록&amp;nbsp;새&amp;nbsp;테스트를&amp;nbsp;어디에&amp;nbsp;추가해야&amp;nbsp;할지&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;-&amp;nbsp;각&amp;nbsp;테스트가&amp;nbsp;구현&amp;nbsp;코드를&amp;nbsp;읽지&amp;nbsp;않아도&amp;nbsp;무엇을&amp;nbsp;검증하는지&amp;nbsp;명확하게&amp;nbsp;이해할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;-&amp;nbsp;`.only()`를&amp;nbsp;사용해&amp;nbsp;개발&amp;nbsp;중&amp;nbsp;특정&amp;nbsp;`describe`&amp;nbsp;블록의&amp;nbsp;테스트&amp;nbsp;그룹만&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;구조가&amp;nbsp;없는&amp;nbsp;거대한&amp;nbsp;테스트&amp;nbsp;파일(또&amp;nbsp;다른&amp;nbsp;코드&amp;nbsp;냄새)을&amp;nbsp;다루다&amp;nbsp;보면&amp;nbsp;중복된&amp;nbsp;테스트를&amp;nbsp;추가하거나&amp;nbsp;특정&amp;nbsp;부분에&amp;nbsp;테스트가&amp;nbsp;전혀&amp;nbsp;없다는&amp;nbsp;것을&amp;nbsp;놓칠&amp;nbsp;수&amp;nbsp;있습니다.&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;&lt;b&gt;좋은&amp;nbsp;테스트는&amp;nbsp;빠른&amp;nbsp;테스트입니다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가&amp;nbsp;실행될&amp;nbsp;때까지&amp;nbsp;30초&amp;nbsp;이상&amp;nbsp;기다려야&amp;nbsp;한다는&amp;nbsp;것만큼&amp;nbsp;비생산적인&amp;nbsp;일도&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;물론&amp;nbsp;어느&amp;nbsp;정도&amp;nbsp;규모가&amp;nbsp;되는&amp;nbsp;앱에서는&amp;nbsp;모든&amp;nbsp;테스트를&amp;nbsp;실행하는&amp;nbsp;데&amp;nbsp;30초&amp;nbsp;이상&amp;nbsp;걸리는&amp;nbsp;것이&amp;nbsp;당연합니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;단일&amp;nbsp;테스트나&amp;nbsp;두&amp;nbsp;개의&amp;nbsp;테스트&amp;nbsp;파일만&amp;nbsp;실행하거나&amp;nbsp;한두&amp;nbsp;개&amp;nbsp;컴포넌트의&amp;nbsp;변경사항만&amp;nbsp;확인할&amp;nbsp;때는&amp;nbsp;거의&amp;nbsp;즉시&amp;nbsp;피드백을&amp;nbsp;받는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;쾌적합니다.&lt;br /&gt;&lt;br /&gt;팁: 테스트를 실행할 때 `--watch` 모드를 사용하고 특정 파일명으로 필터링하세요. 예: `vitest --watch UserProfile.test.tsx`&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;&lt;b&gt;클래스&amp;nbsp;이름이나&amp;nbsp;유사한&amp;nbsp;속성을&amp;nbsp;기반으로&amp;nbsp;요소를&amp;nbsp;쿼리하지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 테스트에서 자주 보는 흔한 실수는 클래스 이름과 같은 속성으로 DOM 요소를 쿼리하려는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220552442&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { container } = render(&amp;lt;SomeComponent /&amp;gt;);
// ❌ Bad - selecting by class name
const submitButton = container.querySelector(&quot;.submit-btn&quot;);
const errorMessage = container.querySelector(&quot;.error-text&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(그리고&amp;nbsp;그&amp;nbsp;클래스&amp;nbsp;이름들이&amp;nbsp;CSS-in-JS에서&amp;nbsp;자동으로&amp;nbsp;생성된&amp;nbsp;임의의&amp;nbsp;문자열일&amp;nbsp;때는&amp;nbsp;더욱&amp;nbsp;심각합니다)&lt;br /&gt;&lt;br /&gt;`.querySelector()`와&amp;nbsp;같은&amp;nbsp;메서드를&amp;nbsp;사용하는&amp;nbsp;것은&amp;nbsp;정말&amp;nbsp;필요할&amp;nbsp;때만&amp;nbsp;사용하는&amp;nbsp;마지막&amp;nbsp;수단,&amp;nbsp;즉&amp;nbsp;탈출구로&amp;nbsp;봐야&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;보통은&amp;nbsp;React&amp;nbsp;Testing&amp;nbsp;Library(또는&amp;nbsp;Vitest의&amp;nbsp;Browser&amp;nbsp;모드)의&amp;nbsp;표준&amp;nbsp;쿼리(예:&amp;nbsp;`getByRole()`&amp;nbsp;또는&amp;nbsp;`getByText()`)를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;이런&amp;nbsp;식으로&amp;nbsp;테스트를&amp;nbsp;작성하면&amp;nbsp;유지보수가&amp;nbsp;번거로워질&amp;nbsp;뿐만&amp;nbsp;아니라,&amp;nbsp;요소를&amp;nbsp;시맨틱하게&amp;nbsp;쿼리해야&amp;nbsp;한다는&amp;nbsp;React&amp;nbsp;Testing&amp;nbsp;Library의&amp;nbsp;핵심&amp;nbsp;원칙을&amp;nbsp;위반하게&amp;nbsp;됩니다.&amp;nbsp;하지만&amp;nbsp;이&amp;nbsp;글에서&amp;nbsp;이미&amp;nbsp;이&amp;nbsp;부분을&amp;nbsp;충분히&amp;nbsp;다뤘으니&amp;nbsp;다시&amp;nbsp;자세히&amp;nbsp;설명하지는&amp;nbsp;않겠습니다.&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;&lt;b&gt;요소가&amp;nbsp;특정&amp;nbsp;클래스명을&amp;nbsp;갖고&amp;nbsp;있다는&amp;nbsp;것을&amp;nbsp;단언하지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의&amp;nbsp;팁에서&amp;nbsp;클래스명을&amp;nbsp;기준으로&amp;nbsp;요소를&amp;nbsp;조회하는&amp;nbsp;것이&amp;nbsp;왜&amp;nbsp;좋지&amp;nbsp;않은지&amp;nbsp;언급했습니다.&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;br /&gt;&lt;br /&gt;물론&amp;nbsp;예외는&amp;nbsp;있습니다.&amp;nbsp;저는&amp;nbsp;연간&amp;nbsp;몇&amp;nbsp;차례&amp;nbsp;정도만&amp;nbsp;그렇게&amp;nbsp;합니다.&amp;nbsp;테마&amp;nbsp;관련&amp;nbsp;코드를&amp;nbsp;테스트하고&amp;nbsp;있다면&amp;nbsp;이렇게&amp;nbsp;해도&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;일반적인&amp;nbsp;테스트에서는&amp;nbsp;`.toHaveClass`를&amp;nbsp;피해야&amp;nbsp;할&amp;nbsp;코드&amp;nbsp;냄새로&amp;nbsp;봅니다.&lt;br /&gt;&lt;br /&gt;시각적&amp;nbsp;회귀&amp;nbsp;테스트(실제&amp;nbsp;스크린샷을&amp;nbsp;찍어&amp;nbsp;비교하는&amp;nbsp;테스트)가&amp;nbsp;없다면,&amp;nbsp;이러한&amp;nbsp;클래스들이&amp;nbsp;실제로&amp;nbsp;어떤&amp;nbsp;영향을&amp;nbsp;미치는지&amp;nbsp;증명하지&amp;nbsp;못하는&amp;nbsp;경우가&amp;nbsp;많습니다.&lt;br /&gt;&lt;br /&gt;다양한&amp;nbsp;내장&amp;nbsp;매처가&amp;nbsp;있으므로&amp;nbsp;보통&amp;nbsp;클래스명으로&amp;nbsp;하려던&amp;nbsp;작업을&amp;nbsp;매처로&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;몇&amp;nbsp;가지&amp;nbsp;예시를&amp;nbsp;살펴보면:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769220585505&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - testing implementation details
expect(button).toHaveClass(&quot;sc-bdVaJa-d&quot;);
expect(button).toHaveClass(&quot;border-red-500&quot;);

// ✅ Better - test visibility and interaction states
expect(button).toBeVisible();
expect(button).toBeEnabled();

// ✅ Good - test semantic attributes
expect(button).toHaveAttribute(&quot;aria-pressed&quot;, &quot;false&quot;);

// ✅ Good - test actual content users see
expect(button).toHaveTextContent(&quot;Submit Form&quot;);

// ✅ Good - test form states
expect(input).toBeRequired();
expect(input).toHaveValue(&quot;john@example.com&quot;);&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redux와&amp;nbsp;같은&amp;nbsp;상태&amp;nbsp;관리&amp;nbsp;라이브러리의&amp;nbsp;내부&amp;nbsp;구현&amp;nbsp;테스트는&amp;nbsp;피하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redux나&amp;nbsp;Zustand&amp;nbsp;같은&amp;nbsp;상태&amp;nbsp;관리&amp;nbsp;라이브러리를&amp;nbsp;사용하는&amp;nbsp;컴포넌트를&amp;nbsp;테스트할&amp;nbsp;때는&amp;nbsp;컴포넌트의&amp;nbsp;동작을&amp;nbsp;검증하되&amp;nbsp;상태&amp;nbsp;관리&amp;nbsp;시스템의&amp;nbsp;내부&amp;nbsp;구현은&amp;nbsp;테스트하지&amp;nbsp;않아야&amp;nbsp;합니다.&amp;nbsp;이&amp;nbsp;조언은&amp;nbsp;이&amp;nbsp;글&amp;nbsp;앞부분의&amp;nbsp;팁과&amp;nbsp;매우&amp;nbsp;유사하지만,&amp;nbsp;Redux나&amp;nbsp;Zustand&amp;nbsp;같은&amp;nbsp;도구를&amp;nbsp;사용하는&amp;nbsp;대규모&amp;nbsp;앱에서&amp;nbsp;이&amp;nbsp;문제가&amp;nbsp;실제로&amp;nbsp;자주&amp;nbsp;발생하는&amp;nbsp;것을&amp;nbsp;여러&amp;nbsp;번&amp;nbsp;목격했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769220613567&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - testing Redux internals
test(&quot;dispatches correct action&quot;, () =&amp;gt; {
  const store = createMockStore();
  render(
    &amp;lt;Provider store={store}&amp;gt;
      &amp;lt;TodoList /&amp;gt;
    &amp;lt;/Provider&amp;gt;
  );

  const addButton = screen.getByRole(&quot;button&quot;, { name: &quot;Add Todo&quot; });
  userEvent.click(addButton);

  expect(store.getActions()).toEqual([
    {
      type: &quot;ADD_TODO&quot;,
      payload: &quot;New todo&quot;,
    },
  ]);
});

// ✅ Good - same test, but this time we're testing what is rendered
// to know that we added a new todo item
test(&quot;adds new todo when button is clicked&quot;, () =&amp;gt; {
  render(
    &amp;lt;Provider store={mockStore}&amp;gt;
      &amp;lt;TodoList /&amp;gt;
    &amp;lt;/Provider&amp;gt;
  );

  const addButton = screen.getByRole(&quot;button&quot;, { name: &quot;Add Todo&quot; });
  userEvent.click(addButton);

  expect(screen.getByText(&quot;New todo&quot;)).toBeInTheDocument();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;구현 세부 사항이 아니라 실제 사용자가 경험하는 것을 테스트하세요. 이렇게 하면 테스트가 더욱 가치 있고 유지보수하기 수월해집니다.&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;&lt;b&gt;`.toBeDefined()`만으로는&amp;nbsp;테스트하지&amp;nbsp;마세요&amp;nbsp;&amp;mdash;&amp;nbsp;더&amp;nbsp;구체적인&amp;nbsp;단언을&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수의&amp;nbsp;반환값을&amp;nbsp;테스트할&amp;nbsp;때,&amp;nbsp;값이&amp;nbsp;존재한다는&amp;nbsp;것만&amp;nbsp;증명하기&amp;nbsp;위해&amp;nbsp;단순히&amp;nbsp;`expect(something).toBeDefined()`만&amp;nbsp;붙여&amp;nbsp;넣고&amp;nbsp;마무리하고&amp;nbsp;싶은&amp;nbsp;유혹이&amp;nbsp;있을&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;거기서&amp;nbsp;멈출&amp;nbsp;이유가&amp;nbsp;뭘까요?&amp;nbsp;대부분의&amp;nbsp;경우&amp;nbsp;테스트로서의&amp;nbsp;가치도&amp;nbsp;높고(무슨&amp;nbsp;일이&amp;nbsp;일어나고&amp;nbsp;있는지&amp;nbsp;바로&amp;nbsp;이해할&amp;nbsp;수&amp;nbsp;있으므로&amp;nbsp;읽기에도&amp;nbsp;훨씬&amp;nbsp;쉬워집니다)&amp;nbsp;그&amp;nbsp;값이&amp;nbsp;정확히&amp;nbsp;무엇이어야&amp;nbsp;하는지&amp;nbsp;단언하는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;낫습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769220638523&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - testing basic existence
test(&quot;user profile has data&quot;, () =&amp;gt; {
  const user = getUserProfile();

  expect(user).toBeDefined();
  expect(user.name).toBeDefined();
  expect(user.email).toBeDefined(); // not proving that it's an email here... it could be anything - including null
});

// ✅ Good - testing actual values and behavior
test(&quot;user profile contains correct data&quot;, () =&amp;gt; {
  const user = getUserProfile();

  expect(user.name).toBe(&quot;John Doe&quot;);
  expect(user.email).toBe(&quot;john@example.com&quot;);
  expect(user.isActive).toBe(true);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;첫&amp;nbsp;번째&amp;nbsp;테스트는&amp;nbsp;이름과&amp;nbsp;이메일&amp;nbsp;값을&amp;nbsp;가진&amp;nbsp;무언가를&amp;nbsp;반환하는지만&amp;nbsp;확인하고&amp;nbsp;있었습니다.&amp;nbsp;하지만&amp;nbsp;우리가&amp;nbsp;그&amp;nbsp;값을&amp;nbsp;올바르게&amp;nbsp;설정했는지는&amp;nbsp;증명하지&amp;nbsp;못합니다.&lt;br /&gt;&lt;br /&gt;`user.name`이&amp;nbsp;빈&amp;nbsp;문자열이라면?&amp;nbsp;또는&amp;nbsp;`user.email`이&amp;nbsp;유효하지&amp;nbsp;않다면?&amp;nbsp;테스트는&amp;nbsp;여전히&amp;nbsp;통과할&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;참고로&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;테스트에서는&amp;nbsp;`expect(user).toBeDefined()`를&amp;nbsp;단언조차&amp;nbsp;하지&amp;nbsp;않았습니다.&amp;nbsp;그것은&amp;nbsp;단지&amp;nbsp;불필요한&amp;nbsp;코드일&amp;nbsp;뿐이고,&amp;nbsp;이후의&amp;nbsp;단언들이&amp;nbsp;어쨌든&amp;nbsp;그것이&amp;nbsp;객체라는&amp;nbsp;것을&amp;nbsp;증명합니다.&lt;br /&gt;&lt;br /&gt;그리고&amp;nbsp;`.toBeDefined()`는&amp;nbsp;`null`이&amp;nbsp;반환되더라도&amp;nbsp;통과합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220653635&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// This function is buggy - it returns null
const getUserById = (id) =&amp;gt; {
  // Bug: returning null instead of user data
  return null;
};

// ❌ Bad - this test passes even though the function is broken!
test(&quot;getUserById returns user data&quot;, () =&amp;gt; {
  const user = getUserById(&quot;123&quot;);

  expect(user).toBeDefined(); // This passes. Because null is classed as defined
});

// ✅ Good - test the actual expected structure
test(&quot;getUserById returns user with correct properties&quot;, () =&amp;gt; {
  const user = getUserById(&quot;123&quot;);

  expect(user).toEqual({
    id: &quot;123&quot;,
    name: &quot;John Doe&quot;,
    email: &quot;john@example.com&quot;,
  });
  // This would fail and catch the bug
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;빈 배열이나 빈 문자열을 반환하는 함수에서도 동일한 문제가 발생합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220663550&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const getErrorMessages = () =&amp;gt; {
  return []; // Bug - should return actual error messages
};

// ❌ Bad - passes even with empty array
test(&quot;returns error messages&quot;, () =&amp;gt; {
  const errors = getErrorMessages();
  expect(errors).toBeDefined(); // [] is defined!
});

// ✅ Good - test the actual content
test(&quot;returns validation error messages&quot;, () =&amp;gt; {
  const errors = getErrorMessages();
  expect(errors).toEqual([
    &quot;Email is required&quot;,
    &quot;Password must be at least 8 characters&quot;,
  ]);
});&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TypeScript에서&amp;nbsp;as&amp;nbsp;any의&amp;nbsp;과도한&amp;nbsp;사용을&amp;nbsp;피하세요&amp;nbsp;&amp;mdash;&amp;nbsp;더&amp;nbsp;구체적으로&amp;nbsp;작성하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서는&amp;nbsp;잘못된&amp;nbsp;타입&amp;nbsp;사용에&amp;nbsp;대해&amp;nbsp;조금&amp;nbsp;더&amp;nbsp;너그러워도(lenient)&amp;nbsp;괜찮다고&amp;nbsp;생각합니다.&lt;br /&gt;&lt;br /&gt;예를 들어, `userCanCreatePost()`에는 `active: false`만 전달하면 된다는 것을 안다면, 다음과 같이 처리해도 됩니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220692418&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// this function userCanCreatePost expects an entire `User` object, with tons of more properties
expect(
  userCanCreatePost({
    active: false,
  } as any)
).toBe(false);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만&amp;nbsp;같은&amp;nbsp;노력으로&amp;nbsp;실제&amp;nbsp;타입에&amp;nbsp;대한&amp;nbsp;타입&amp;nbsp;단언을&amp;nbsp;사용하는&amp;nbsp;것이&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;안전합니다&amp;nbsp;&amp;mdash;&amp;nbsp;예:&amp;nbsp;`{active:&amp;nbsp;false}&amp;nbsp;as&amp;nbsp;User`&lt;br /&gt;&lt;br /&gt;그렇게 하면 오타가 있을 경우 TypeScript에서 에러를 발생시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220709515&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expect(
  userCanCreatePost({
    isActive: false,
  } as any)
).toBe(false);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가장&amp;nbsp;좋은&amp;nbsp;해결책은&amp;nbsp;다음&amp;nbsp;팁에&amp;nbsp;있습니다&amp;nbsp;&amp;mdash;&amp;nbsp;픽스처와&amp;nbsp;헬퍼&amp;nbsp;함수를&amp;nbsp;사용하는&amp;nbsp;것입니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트를&amp;nbsp;읽고&amp;nbsp;쓰기&amp;nbsp;쉽게&amp;nbsp;만들기&amp;nbsp;위해&amp;nbsp;픽스처와&amp;nbsp;헬퍼&amp;nbsp;함수를&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mock&amp;nbsp;데이터를&amp;nbsp;생성하기&amp;nbsp;위해&amp;nbsp;자주&amp;nbsp;유사한&amp;nbsp;코드를&amp;nbsp;작성한다면,&amp;nbsp;그&amp;nbsp;타입의&amp;nbsp;기본&amp;nbsp;객체를&amp;nbsp;미리&amp;nbsp;선언해&amp;nbsp;두거나&amp;nbsp;헬퍼&amp;nbsp;함수로&amp;nbsp;생성하는&amp;nbsp;방식을&amp;nbsp;사용해&amp;nbsp;보세요.&lt;/p&gt;
&lt;pre id=&quot;code_1769220746276&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
  preferences: {
    theme: &quot;light&quot; | &quot;dark&quot;,
    notifications: boolean,
  };
}

// helper fn to create dummy data, with optional override:
export const createUser = (overrides?: Partial&amp;lt;User&amp;gt;): User =&amp;gt; {
  return {
    id: &quot;user-123&quot;,
    name: &quot;John Doe&quot;,
    email: &quot;john@example.com&quot;,
    isActive: true,
    preferences: {
      theme: &quot;light&quot;,
      notifications: true,
    },
    ...overrides,
  };
};

export const createInactiveUser = (): User =&amp;gt; {
  return createUser({
    isActive: false,
  });
};

export const createAdminUser = (): User =&amp;gt; {
  return createUser({
    name: &quot;Admin User&quot;,
    email: &quot;admin@example.com&quot;,
  });
};

test(&quot;displays user profile correctly&quot;, () =&amp;gt; {
  const user = createUser({
    name: &quot;Jane Smith&quot;,
    email: &quot;jane@example.com&quot;,
  });

  render(&amp;lt;UserProfile user={user} /&amp;gt;);

  expect(screen.getByText(&quot;Jane Smith&quot;)).toBeInTheDocument();
  expect(screen.getByText(&quot;jane@example.com&quot;)).toBeInTheDocument();
});

test(&quot;shows inactive status for inactive users&quot;, () =&amp;gt; {
  const inactiveUser = createInactiveUser();

  render(&amp;lt;UserProfile user={inactiveUser} /&amp;gt;);

  expect(screen.getByText(&quot;Status: Inactive&quot;)).toBeInTheDocument();
});

test(&quot;shows list of users&quot;, () =&amp;gt; {
  const users = [
    createUser({ name: &quot;User 1&quot; }),
    createUser({ name: &quot;User 2&quot; }),
    createInactiveUser(),
  ];

  render(&amp;lt;UserList users={users} /&amp;gt;);

  expect(screen.getByText(&quot;User 1&quot;)).toBeInTheDocument();
  expect(screen.getByText(&quot;User 2&quot;)).toBeInTheDocument();
  expect(screen.getByText(&quot;Status: Inactive&quot;)).toBeInTheDocument();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게&amp;nbsp;하면&amp;nbsp;테스트가&amp;nbsp;훨씬&amp;nbsp;읽기&amp;nbsp;쉬워집니다.&amp;nbsp;`createUser({isActive:&amp;nbsp;false})`를&amp;nbsp;전달하면&amp;nbsp;이&amp;nbsp;테스트에서&amp;nbsp;중요한&amp;nbsp;것은&amp;nbsp;오직&amp;nbsp;`isActive:&amp;nbsp;false`&amp;nbsp;부분이라는&amp;nbsp;것을&amp;nbsp;명확하게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;이는&amp;nbsp;테스트가&amp;nbsp;실패했을&amp;nbsp;때(헬퍼&amp;nbsp;함수에서&amp;nbsp;필수&amp;nbsp;필드만&amp;nbsp;사용되기&amp;nbsp;때문에)&amp;nbsp;테스트의&amp;nbsp;의도가&amp;nbsp;더욱&amp;nbsp;명확해진다는&amp;nbsp;의미입니다.&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;&lt;b&gt;공통&amp;nbsp;컨텍스트&amp;nbsp;제공자를&amp;nbsp;설정하기&amp;nbsp;위한&amp;nbsp;렌더&amp;nbsp;헬퍼&amp;nbsp;함수의&amp;nbsp;적절한&amp;nbsp;활용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 실제 앱에서는 결국 `app.tsx`나 `main.tsx`에 다음과 같은 코드가 들어가게 됩니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220769104&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return (
  &amp;lt;ThemeProvider currentTheme=&quot;dark&quot;&amp;gt;
    &amp;lt;ReduxProvider&amp;gt;
      &amp;lt;UserOptionsProvider&amp;gt;
        &amp;lt;FeatureFlagProvider enabled={query.featureSwitch}&amp;gt;
          &amp;lt;AuthProvider currentUser={user}&amp;gt;{props.children}&amp;lt;/AuthProvider&amp;gt;
        &amp;lt;/FeatureFlagProvider&amp;gt;
      &amp;lt;/UserOptionsProvider&amp;gt;
    &amp;lt;/ReduxProvider&amp;gt;
  &amp;lt;/ThemeProvider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 테스트에서는 컴포넌트가 정상적으로 작동하는 데 필수적인 일부 상위 프로바이더와 함께 `render`를 호출하게 되는 경우가 많습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220780459&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;render(
  &amp;lt;ThemeProvider currentTheme=&quot;dark&quot;&amp;gt;
    &amp;lt;ReduxProvider&amp;gt;
      &amp;lt;FeatureFlagProvider&amp;gt;
        &amp;lt;AuthProvider currentUser={mockUser}&amp;gt;
          &amp;lt;YourActualComponentHere /&amp;gt;
        &amp;lt;/AuthProvider&amp;gt;
      &amp;lt;/FeatureFlagProvider&amp;gt;
    &amp;lt;/ReduxProvider&amp;gt;
  &amp;lt;/ThemeProvider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(또는&amp;nbsp;때때로&amp;nbsp;이러한&amp;nbsp;프로바이더들과&amp;nbsp;그들의&amp;nbsp;`useContext()`&amp;nbsp;훅들이&amp;nbsp;모킹된&amp;nbsp;것을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있습니다!)&lt;br /&gt;&lt;br /&gt;표준 `render()`와 동일하게 작동하면서 모든 상위 컴포넌트를 자동으로 전달할 수 있는 이러한 헬퍼 함수가 있으면 훨씬 편할 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769220792707&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// put this in a shared test helper file
const renderWithProviders = (component) =&amp;gt; {
  return render(component, {
    wrapper: (props) =&amp;gt; {
      return (
        &amp;lt;ThemeProvider currentTheme=&quot;dark&quot;&amp;gt;
          &amp;lt;ReduxProvider&amp;gt;
            &amp;lt;UserOptionsProvider&amp;gt;
              &amp;lt;FeatureFlagProvider enabled={query.featureSwitch}&amp;gt;
                &amp;lt;AuthProvider currentUser={user}&amp;gt;{props.children}&amp;lt;/AuthProvider&amp;gt;
              &amp;lt;/FeatureFlagProvider&amp;gt;
            &amp;lt;/UserOptionsProvider&amp;gt;
          &amp;lt;/ReduxProvider&amp;gt;
        &amp;lt;/ThemeProvider&amp;gt;
      );
    },
  });
};

// then in your tests
renderWithProviders(&amp;lt;YourActualComponentHere /&amp;gt;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 특정 사용자(`&amp;lt;AuthProvider currentUser={adminUser}&amp;gt;...&amp;lt;/AuthProvider&amp;gt;`)를 테스트하거나 기능 플래그(`&amp;lt;FeatureFlagProvider enabled=&quot;demo&quot;&amp;gt;...&amp;lt;/FeatureFlagProvider&amp;gt;`)로 인해 여전히 몇몇 프로바이더를 제공해야 한다면, 렌더 헬퍼에 이들을 옵션으로 추가하면 훨씬 읽기 쉬워집니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220806830&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// put this in your test helper file(s)
const renderWithProviders = (component, options) =&amp;gt; {
  return render(component, {
    wrapper: (props) =&amp;gt; {
      return (
        &amp;lt;ThemeProvider currentTheme={options?.currentTheme ?? &quot;dark&quot;}&amp;gt;
          &amp;lt;ReduxProvider&amp;gt;
            &amp;lt;UserOptionsProvider&amp;gt;
              &amp;lt;FeatureFlagProvider enabled={options?.featureFlag}&amp;gt;
                &amp;lt;AuthProvider currentUser={options?.currentUser ?? user}&amp;gt;
                  {props.children}
                &amp;lt;/AuthProvider&amp;gt;
              &amp;lt;/FeatureFlagProvider&amp;gt;
            &amp;lt;/UserOptionsProvider&amp;gt;
          &amp;lt;/ReduxProvider&amp;gt;
        &amp;lt;/ThemeProvider&amp;gt;
      );
    },
  });
};

// then your tests are much cleaner:
renderWithProviders(&amp;lt;YourActualComponentHere /&amp;gt;, {
  featureFlag: &quot;demo&quot;,
  currentUser: adminUser,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제&amp;nbsp;`renderWithProviders()`&amp;nbsp;함수를&amp;nbsp;작성하는&amp;nbsp;것은&amp;nbsp;다소&amp;nbsp;번거롭지만,&amp;nbsp;이를&amp;nbsp;수정해야&amp;nbsp;할&amp;nbsp;일은&amp;nbsp;거의&amp;nbsp;없습니다.&amp;nbsp;게다가&amp;nbsp;테스트가&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;깔끔해지고&amp;nbsp;읽고&amp;nbsp;쓰기&amp;nbsp;수월해집니다.&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;&lt;b&gt;테스트는&amp;nbsp;프로덕션&amp;nbsp;코드처럼&amp;nbsp;깔끔해야&amp;nbsp;합니다&amp;nbsp;(약간의&amp;nbsp;중복은&amp;nbsp;허용됩니다)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부&amp;nbsp;코드베이스는&amp;nbsp;테스트를&amp;nbsp;품질&amp;nbsp;기준이&amp;nbsp;없는&amp;nbsp;지저분한&amp;nbsp;코드로&amp;nbsp;취급하고&amp;nbsp;소홀히&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;저는&amp;nbsp;테스트&amp;nbsp;파일도&amp;nbsp;프로덕션&amp;nbsp;코드&amp;nbsp;못지않게&amp;nbsp;높은&amp;nbsp;품질을&amp;nbsp;유지해야&amp;nbsp;한다고&amp;nbsp;생각합니다.&lt;br /&gt;&lt;br /&gt;'못지않게'가&amp;nbsp;아니라&amp;nbsp;'거의'라고&amp;nbsp;표현하는&amp;nbsp;이유는&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;예외가&amp;nbsp;있기&amp;nbsp;때문입니다:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;TypeScript&amp;nbsp;코드베이스에서는&amp;nbsp;`any`나&amp;nbsp;기타&amp;nbsp;타입&amp;nbsp;단언을&amp;nbsp;더&amp;nbsp;자주&amp;nbsp;사용해도&amp;nbsp;괜찮습니다.&amp;nbsp;때로는&amp;nbsp;그것이&amp;nbsp;테스트를&amp;nbsp;읽기&amp;nbsp;더&amp;nbsp;명확하게&amp;nbsp;만들어줄&amp;nbsp;수&amp;nbsp;있습니다(예:&amp;nbsp;`const&amp;nbsp;mockData&amp;nbsp;=&amp;nbsp;{&amp;nbsp;disabled:&amp;nbsp;true&amp;nbsp;}&amp;nbsp;as&amp;nbsp;SomeProduct`&amp;nbsp;&amp;mdash;&amp;nbsp;이&amp;nbsp;경우&amp;nbsp;이&amp;nbsp;테스트는&amp;nbsp;`disabled`&amp;nbsp;속성만&amp;nbsp;신경&amp;nbsp;쓴다는&amp;nbsp;것이&amp;nbsp;명확합니다).&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;테스트를&amp;nbsp;더&amp;nbsp;읽기&amp;nbsp;쉽게&amp;nbsp;만든다면&amp;nbsp;약간의&amp;nbsp;중복을&amp;nbsp;허용해도&amp;nbsp;괜찮습니다.&amp;nbsp;때로는&amp;nbsp;테스트에서&amp;nbsp;코드를&amp;nbsp;공통&amp;nbsp;함수로&amp;nbsp;추상화하는&amp;nbsp;것이&amp;nbsp;오히려&amp;nbsp;가독성을&amp;nbsp;해칠&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;보통&amp;nbsp;주석은&amp;nbsp;'왜'를&amp;nbsp;설명하지&amp;nbsp;않으면&amp;nbsp;큰&amp;nbsp;의미가&amp;nbsp;없습니다.&amp;nbsp;하지만&amp;nbsp;테스트에서는&amp;nbsp;주석이&amp;nbsp;테스트가&amp;nbsp;무엇을&amp;nbsp;하는지&amp;nbsp;명확히&amp;nbsp;하는&amp;nbsp;데&amp;nbsp;정말&amp;nbsp;큰&amp;nbsp;도움이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;테스트하려는&amp;nbsp;함수에&amp;nbsp;복잡한&amp;nbsp;객체를&amp;nbsp;전달할&amp;nbsp;때,&amp;nbsp;테스트에&amp;nbsp;중요한&amp;nbsp;특정&amp;nbsp;속성을&amp;nbsp;주석으로&amp;nbsp;표시해&amp;nbsp;두면&amp;nbsp;유지보수가&amp;nbsp;훨씬&amp;nbsp;수월해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;단언된&amp;nbsp;값을&amp;nbsp;설명하세요&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세율을 계산하는 순수 함수를 테스트할 때, 다음과 같이 하고 싶을 수도 있습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220854037&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expect(calculateTaxInUsd(someProduct, country)).toBe(16.4);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만&amp;nbsp;그것이&amp;nbsp;실패했을&amp;nbsp;때&amp;nbsp;&amp;mdash;&amp;nbsp;16.40이라는&amp;nbsp;세금&amp;nbsp;금액이&amp;nbsp;정확한지&amp;nbsp;어떻게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있을까요?&lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;도출하는&amp;nbsp;방법을&amp;nbsp;설명하기&amp;nbsp;위해&amp;nbsp;주석을&amp;nbsp;달거나,&amp;nbsp;수학으로&amp;nbsp;계산하거나(이상적으로는&amp;nbsp;구현과&amp;nbsp;동일하지&amp;nbsp;않겠지만).&lt;br /&gt;&lt;br /&gt;이러한&amp;nbsp;설명&amp;nbsp;없이는&amp;nbsp;구현에&amp;nbsp;약간의&amp;nbsp;변화를&amp;nbsp;주고,&amp;nbsp;결과가&amp;nbsp;달라지는&amp;nbsp;것을&amp;nbsp;보며,&amp;nbsp;실패한&amp;nbsp;테스트&amp;nbsp;결과에서&amp;nbsp;예상값을&amp;nbsp;복사해서&amp;nbsp;붙여넣는&amp;nbsp;것이&amp;nbsp;일반적일&amp;nbsp;것입니다.&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;&lt;b&gt;단언하는&amp;nbsp;값을&amp;nbsp;계산하기&amp;nbsp;위해&amp;nbsp;구현&amp;nbsp;로직을&amp;nbsp;사용하지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을&amp;nbsp;출력하는&amp;nbsp;몇몇&amp;nbsp;컴포넌트를&amp;nbsp;테스트하고&amp;nbsp;있다면(어떤&amp;nbsp;세금&amp;nbsp;값을&amp;nbsp;출력하는&amp;nbsp;컴포넌트라고&amp;nbsp;가정해봅시다),&lt;br /&gt;&lt;br /&gt;일반적으로 컴포넌트를 렌더링하고 다음과 같이 할 것입니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220884369&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const expected = 16.4;
expect(screen.getByRole(&quot;heading&quot;)).toHaveTextContent(`Tax: $${expected}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;앱&amp;nbsp;코드와&amp;nbsp;동일한&amp;nbsp;함수를&amp;nbsp;호출해&amp;nbsp;$16.40을&amp;nbsp;계산하는&amp;nbsp;것은&amp;nbsp;피해야&amp;nbsp;합니다.&amp;nbsp;만약&amp;nbsp;그&amp;nbsp;함수에&amp;nbsp;버그가&amp;nbsp;있다면&amp;nbsp;우리는&amp;nbsp;그&amp;nbsp;버그가&amp;nbsp;있는&amp;nbsp;코드로&amp;nbsp;동작이&amp;nbsp;정상이라고&amp;nbsp;잘못&amp;nbsp;검증하게&amp;nbsp;될&amp;nbsp;것입니다.&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;&lt;b&gt;쓸데없는&amp;nbsp;테스트는&amp;nbsp;피하고,&amp;nbsp;실패할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;것은&amp;nbsp;테스트하지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 테스트는 명확한 목적이 있어야 합니다.&lt;br /&gt;&lt;br /&gt;중요하지&amp;nbsp;않은&amp;nbsp;것을&amp;nbsp;검사하는&amp;nbsp;테스트나,&amp;nbsp;버그가&amp;nbsp;있어도&amp;nbsp;실패하지&amp;nbsp;않도록&amp;nbsp;작성된&amp;nbsp;테스트는&amp;nbsp;아무런&amp;nbsp;가치가&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;예를 들면:&lt;/p&gt;
&lt;pre id=&quot;code_1769220957550&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;this is a pointless test&quot;, async () =&amp;gt; {
  const { container } = render(&amp;lt;Greeting /&amp;gt;);
  expect(container).toBeDefined();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위&amp;nbsp;테스트가&amp;nbsp;실패하는&amp;nbsp;것은&amp;nbsp;정말&amp;nbsp;어렵습니다.&amp;nbsp;(기술적으로는&amp;nbsp;렌더링&amp;nbsp;중에&amp;nbsp;오류가&amp;nbsp;발생할&amp;nbsp;때만&amp;nbsp;실패할&amp;nbsp;수&amp;nbsp;있습니다.)&lt;br /&gt;&lt;br /&gt;또한 개인적으로는 절대 실패할 수 없는 테스트를 피합니다. 예를 들어:&lt;/p&gt;
&lt;pre id=&quot;code_1769220973768&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Greeting = ({ name }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Hello, ${name}&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Welcome to the site&amp;lt;/h2&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의&amp;nbsp;(단순한)&amp;nbsp;컴포넌트는&amp;nbsp;테스트의&amp;nbsp;명확한&amp;nbsp;가치가&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;그&amp;nbsp;(단순화된)&amp;nbsp;컴포넌트에&amp;nbsp;대해서는&amp;nbsp;`&amp;lt;SomeComponent&amp;nbsp;name=&quot;Fred&quot;/&amp;gt;`가&amp;nbsp;&quot;Hello,&amp;nbsp;Fred&quot;를&amp;nbsp;렌더링하는지&amp;nbsp;테스트하는&amp;nbsp;것이&amp;nbsp;분명히&amp;nbsp;의미가&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;저는&amp;nbsp;&quot;Welcome&amp;nbsp;to&amp;nbsp;the&amp;nbsp;site&quot;는&amp;nbsp;절대&amp;nbsp;테스트하지&amp;nbsp;않을&amp;nbsp;것입니다.&amp;nbsp;그&amp;nbsp;텍스트&amp;nbsp;이외에&amp;nbsp;보여줄&amp;nbsp;로직이&amp;nbsp;전혀&amp;nbsp;없거든요.&lt;br /&gt;&lt;br /&gt;참고:&amp;nbsp;이&amp;nbsp;컴포넌트가&amp;nbsp;부모&amp;nbsp;컴포넌트의&amp;nbsp;일부로&amp;nbsp;렌더링될&amp;nbsp;가능성이&amp;nbsp;있다면&amp;nbsp;&quot;Welcome&amp;nbsp;to&amp;nbsp;the&amp;nbsp;site&quot;가&amp;nbsp;렌더링되는지&amp;nbsp;확인하는&amp;nbsp;것은&amp;nbsp;충분히&amp;nbsp;타당할&amp;nbsp;수&amp;nbsp;있습니다!&lt;br /&gt;&lt;br /&gt;다음은&amp;nbsp;&quot;Welcome&amp;nbsp;to&amp;nbsp;the&amp;nbsp;site&quot;가&amp;nbsp;적절하게&amp;nbsp;검증될&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;예입니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769220985261&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ParentPage = ({ isLoggedIn, username }) =&amp;gt; {
  return &amp;lt;div&amp;gt;{isLoggedIn ? &amp;lt;Greeting name={username} /&amp;gt; : &amp;lt;JoinUp /&amp;gt;}&amp;lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Mock을&amp;nbsp;테스트하거나&amp;nbsp;&quot;테스트를&amp;nbsp;테스트&quot;하는&amp;nbsp;것을&amp;nbsp;피하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는&amp;nbsp;mock을&amp;nbsp;과도하게&amp;nbsp;사용하기&amp;nbsp;쉽고,&amp;nbsp;그&amp;nbsp;결과&amp;nbsp;테스트가&amp;nbsp;단지&amp;nbsp;mock만을&amp;nbsp;검증하게&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;테스트에서&amp;nbsp;함수를&amp;nbsp;다시&amp;nbsp;구현해야&amp;nbsp;한다면,&amp;nbsp;실제로&amp;nbsp;유용한&amp;nbsp;것을&amp;nbsp;테스트하고&amp;nbsp;있는지&amp;nbsp;다시&amp;nbsp;생각해볼&amp;nbsp;시점입니다.&lt;br /&gt;&lt;br /&gt;이런&amp;nbsp;상황의&amp;nbsp;코드&amp;nbsp;냄새는&amp;nbsp;복잡한&amp;nbsp;`.mockImplementation()`을&amp;nbsp;사용하는&amp;nbsp;`spyOn`이&amp;nbsp;있을&amp;nbsp;때입니다.&lt;br /&gt;&lt;br /&gt;예를 들어, 세금을 계산하기 위해 `taxService`를 사용하는 `PriceCalculator` 컴포넌트가 있다고 하겠습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769221012183&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - testing the mock implementation, not the actual behavior
test(&quot;displays correct price with tax&quot;, () =&amp;gt; {
  const mockCalculateTax = vi
    .spyOn(taxService, &quot;calculateTax&quot;)
    .mockImplementation((price, region) =&amp;gt; {
      // We're reimplementing the entire tax logic in our test!
      const taxRates = {
        US: 0.08,
        UK: 0.2,
        CA: 0.13,
      };
      return price * (taxRates[region] || 0);
    });

  render(&amp;lt;PriceCalculator basePrice={100} region=&quot;US&quot; /&amp;gt;);

  expect(screen.getByText(&quot;Total: $108.00&quot;)).toBeInTheDocument();
  expect(mockCalculateTax).toHaveBeenCalledWith(100, &quot;US&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;예제에서는&amp;nbsp;세금&amp;nbsp;계산&amp;nbsp;로직을&amp;nbsp;mock&amp;nbsp;객체에&amp;nbsp;다시&amp;nbsp;구현했습니다.&lt;br /&gt;&lt;br /&gt;제 생각에는 이를 더 간단하게 테스트하는 더 나은 방법은 특정 반환값만 mock으로 지정하고, mock 객체 안에 로직을 다시 구현하지 않는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221023028&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ Better - mock with simple return values, test the component behavior
test(&quot;displays price with tax from tax service&quot;, () =&amp;gt; {
  vi.spyOn(taxService, &quot;calculateTax&quot;).mockReturnValue(8.0); // Simple mock return value

  render(&amp;lt;PriceCalculator basePrice={100} region=&quot;US&quot; /&amp;gt;);

  expect(screen.getByText(&quot;Total: $108.00&quot;)).toBeInTheDocument();
  expect(taxService.calculateTax).toHaveBeenCalledWith(100, &quot;US&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그럼에도&amp;nbsp;세금&amp;nbsp;서비스를&amp;nbsp;따로&amp;nbsp;분리해서&amp;nbsp;테스트하는&amp;nbsp;것을&amp;nbsp;잊으면&amp;nbsp;안&amp;nbsp;됩니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769221033457&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;tax service calculates US tax correctly&quot;, () =&amp;gt; {
  const result = taxService.calculateTax(100, &quot;US&quot;);
  expect(result).toBe(8.0);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;비록&amp;nbsp;단순한&amp;nbsp;예시이지만,&amp;nbsp;제가&amp;nbsp;중요하다고&amp;nbsp;생각하는&amp;nbsp;일반적인&amp;nbsp;규칙은&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mock에서&amp;nbsp;복잡한&amp;nbsp;로직을&amp;nbsp;작성하고&amp;nbsp;있다는&amp;nbsp;것을&amp;nbsp;발견했다면,&amp;nbsp;그&amp;nbsp;로직을&amp;nbsp;별도로&amp;nbsp;테스트하거나&amp;nbsp;mock을&amp;nbsp;단순화해야&amp;nbsp;한다는&amp;nbsp;신호일&amp;nbsp;수&amp;nbsp;있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트&amp;nbsp;간&amp;nbsp;상태&amp;nbsp;누수&amp;nbsp;방지하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각&amp;nbsp;테스트는&amp;nbsp;독립적으로&amp;nbsp;실행될&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;하며,&amp;nbsp;어떤&amp;nbsp;순서로&amp;nbsp;실행되더라도&amp;nbsp;결과가&amp;nbsp;달라져서는&amp;nbsp;안&amp;nbsp;됩니다.&amp;nbsp;다른&amp;nbsp;테스트의&amp;nbsp;영향을&amp;nbsp;받아서는&amp;nbsp;안&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;특정&amp;nbsp;순서로만&amp;nbsp;실행되어야&amp;nbsp;통과하는&amp;nbsp;테스트가&amp;nbsp;있다면&amp;nbsp;이는&amp;nbsp;나쁜&amp;nbsp;코드&amp;nbsp;냄새입니다.&lt;br /&gt;&lt;br /&gt;다음과&amp;nbsp;같은&amp;nbsp;상황은&amp;nbsp;피하세요:&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;하나의&amp;nbsp;테스트에서&amp;nbsp;`mock`이나&amp;nbsp;`spy`를&amp;nbsp;설정(예:&amp;nbsp;`vi.spyOn(something,&amp;nbsp;'someFn').mockReturnValue(true)`)하고,&amp;nbsp;다른&amp;nbsp;테스트들이&amp;nbsp;해당&amp;nbsp;mock이&amp;nbsp;`true`를&amp;nbsp;반환한다고&amp;nbsp;기대하는&amp;nbsp;경우.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;한&amp;nbsp;테스트에서&amp;nbsp;데이터베이스&amp;nbsp;데이터를&amp;nbsp;설정해&amp;nbsp;두고,&amp;nbsp;그&amp;nbsp;다음의&amp;nbsp;다른&amp;nbsp;테스트가&amp;nbsp;그&amp;nbsp;데이터베이스&amp;nbsp;행이&amp;nbsp;존재한다고&amp;nbsp;가정하는&amp;nbsp;경우.&lt;br /&gt;&lt;br /&gt;vitest를&amp;nbsp;구성하여&amp;nbsp;테스트를&amp;nbsp;임의의&amp;nbsp;순서로&amp;nbsp;실행할&amp;nbsp;수&amp;nbsp;있습니다(`c`).&amp;nbsp;이렇게&amp;nbsp;하면&amp;nbsp;다른&amp;nbsp;테스트로&amp;nbsp;상태가&amp;nbsp;유출되거나&amp;nbsp;이전&amp;nbsp;테스트에&amp;nbsp;의존하는&amp;nbsp;테스트를&amp;nbsp;병합할&amp;nbsp;가능성이&amp;nbsp;줄어듭니다.&lt;br /&gt;&lt;br /&gt;참고:&amp;nbsp;테스트&amp;nbsp;전에&amp;nbsp;설정이&amp;nbsp;필요하면&amp;nbsp;`beforeAll()/beforeEach()`를&amp;nbsp;사용하고,&amp;nbsp;다른&amp;nbsp;테스트가&amp;nbsp;실행되기&amp;nbsp;전에&amp;nbsp;모든&amp;nbsp;상태를&amp;nbsp;초기화하려면&amp;nbsp;`afterEach()/afterAll()`를&amp;nbsp;사용하세요.&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;&lt;br /&gt;&lt;b&gt;하드코딩된&amp;nbsp;서명된&amp;nbsp;해시들이&amp;nbsp;없도록&amp;nbsp;하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 하드코딩된 magic number를 사용하는 것과 유사합니다. 하지만 JWT와 같은 서명된 토큰을 다룰 때, 새로운 데이터로 서명된 토큰을 다시 생성하는 방법이 명확하지 않다면, 서명된 토큰 값을 하드코딩하는 것은 정말 번거로운 일입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221096939&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - hard coded JWT token with no way to regenerate
test(&quot;verifies valid JWT token&quot;, () =&amp;gt; {
  const hardCodedToken =
    &quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30&quot;;

  const decodedToken = verifyAndDecodeToken(hardCodedToken);
  expect(decodedToken.valid).toBe(true);
  expect(decodedToken.payload.name).toBe(&quot;John Doe&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;그&amp;nbsp;다음&amp;nbsp;상태&amp;nbsp;속성과&amp;nbsp;같이&amp;nbsp;토큰에&amp;nbsp;새로운&amp;nbsp;값을&amp;nbsp;추가해야&amp;nbsp;한다고&amp;nbsp;가정해&amp;nbsp;봅시다.&amp;nbsp;그래서&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;추가하고&amp;nbsp;싶은데:&lt;/p&gt;
&lt;pre id=&quot;code_1769221111216&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expect(result.payload.status).toBe(&quot;active&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만&amp;nbsp;이제는&amp;nbsp;원래의&amp;nbsp;토큰을&amp;nbsp;재생성하는&amp;nbsp;것이&amp;nbsp;꽤&amp;nbsp;번거롭습니다.&lt;br /&gt;&lt;br /&gt;이상적으로는&amp;nbsp;테스트에서&amp;nbsp;토큰을&amp;nbsp;생성하고&amp;nbsp;디코딩할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;로직이&amp;nbsp;있어서&amp;nbsp;테스트&amp;nbsp;자체가&amp;nbsp;자명하도록(self-documenting)&amp;nbsp;하는&amp;nbsp;것입니다.&amp;nbsp;그리고&amp;nbsp;간단한&amp;nbsp;주석은&amp;nbsp;업데이트가&amp;nbsp;필요할&amp;nbsp;때&amp;nbsp;많은&amp;nbsp;시간을&amp;nbsp;절약할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;그리고&amp;nbsp;이&amp;nbsp;블로그&amp;nbsp;포스트를&amp;nbsp;위해:&amp;nbsp;저는&amp;nbsp;jwt.io에서&amp;nbsp;생성했습니다&amp;nbsp;:)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;저는 jwt.io에서 생성했습니다 :)&lt;/b&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트에서&amp;nbsp;if/else&amp;nbsp;문을&amp;nbsp;피하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에는&amp;nbsp;조건문(if/else)이&amp;nbsp;존재해서는&amp;nbsp;안&amp;nbsp;됩니다&amp;nbsp;(예외는&amp;nbsp;아래에서&amp;nbsp;확인하세요!)&lt;br /&gt;&lt;br /&gt;다음은 쓸모없는 테스트의 예인데, if (result) 부분이 절대 실행되지 않기 때문에 우리는 버그가 있다는 것을 깨닫지 못합니다!&lt;/p&gt;
&lt;pre id=&quot;code_1769221168915&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// note: typed to return either:
// undefined, or
// {success: boolean} (not isSuccess)
const maybeReturnsSomething = (): undefined | { success: boolean } =&amp;gt; {
  return undefined;
};
test(&quot;it returns isSuccess&quot;, () =&amp;gt; {
  const result = maybeReturnsSomething(); // &amp;lt;&amp;lt; might return something, might not
  if (result) {
    expect(result.isSuccess).toBe(true);
  }

  // this test passes... but that is because `result` was empty so the expect() did not fail
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해결책은 if(result)를 제거하는 것입니다. 만약 TypeScript 에러가 발생한다면, non-null 단언 연산자를 사용해 에러를 피할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221179105&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test('it returns isSuccess', () =&amp;gt; {
  const result =
    maybeReturnsSomething(); // &amp;lt;&amp;lt; might return something, might not
  expect(result!.isSuccess).toBe(true); // &amp;lt;&amp;lt; this will now always run (so it will catch the bug)
});&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;조건문을 사용할 때&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 `each()`를 사용한다면, 다음과 같이 매우 단순한 경우에 조건문을 사용하는 것은 유용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221204328&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;it.each([true, false])(&quot;includes button when someProp = %s&quot;, (isEnabled) =&amp;gt; {
  render(&amp;lt;Component someProp={isEnabled} /&amp;gt;);
  const maybeButton = screen.queryByRole(&quot;button&quot;);

  if (isEnabled) {
    expect(maybeButton).toBeInTheDocument();
  } else {
    expect(maybeButton).toBeNull();
  }
});&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트의&amp;nbsp;의도를&amp;nbsp;설명하는&amp;nbsp;데&amp;nbsp;도움이&amp;nbsp;되도록&amp;nbsp;주석이나&amp;nbsp;명확한&amp;nbsp;변수&amp;nbsp;이름을&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를&amp;nbsp;읽을&amp;nbsp;때는&amp;nbsp;설명적인&amp;nbsp;변수&amp;nbsp;이름이&amp;nbsp;있으면(프로덕션&amp;nbsp;코드처럼)&amp;nbsp;테스트의&amp;nbsp;의도를&amp;nbsp;이해하기가&amp;nbsp;훨씬&amp;nbsp;쉽습니다.&amp;nbsp;때로는(중복되더라도)&amp;nbsp;추가&amp;nbsp;주석이&amp;nbsp;우리가&amp;nbsp;정확히&amp;nbsp;무엇을&amp;nbsp;테스트하는지,&amp;nbsp;또는&amp;nbsp;예상값이&amp;nbsp;왜&amp;nbsp;그런지를&amp;nbsp;명확히&amp;nbsp;하는&amp;nbsp;데&amp;nbsp;도움이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;다음은&amp;nbsp;이해하기&amp;nbsp;어려운&amp;nbsp;테스트의&amp;nbsp;예입니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769221226164&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;handles logic&quot;, () =&amp;gt; {
  const result = calculateCost(5);
  expect(result).toBe(9);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;테스트는&amp;nbsp;우리가&amp;nbsp;실제로&amp;nbsp;무엇을&amp;nbsp;테스트하는지&amp;nbsp;전혀&amp;nbsp;알려주지&amp;nbsp;않습니다.&amp;nbsp;&quot;handles&amp;nbsp;logic&quot;은&amp;nbsp;무슨&amp;nbsp;뜻인가요?&amp;nbsp;왜&amp;nbsp;예상값이&amp;nbsp;9인가요?&lt;br /&gt;&lt;br /&gt;다음은 변수 이름과 주석을 더 명확히 해서 다시 작성한 동일한 테스트입니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769221238962&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test(&quot;calculates total cost including tax and shipping&quot;, () =&amp;gt; {
  const baseItemCost = 5;
  const expectedTotalWithTaxAndShipping = 9; // $5 base + $2 tax + $2 shipping

  const result = calculateCost(baseItemCost);

  expect(result).toBe(expectedTotalWithTaxAndShipping);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;비록&amp;nbsp;이것은&amp;nbsp;단순하고&amp;nbsp;인위적인&amp;nbsp;예이지만,&amp;nbsp;요지는&amp;nbsp;분명할&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;누군가가&amp;nbsp;이&amp;nbsp;테스트를&amp;nbsp;수정하거나(혹은&amp;nbsp;자신의&amp;nbsp;변경으로&amp;nbsp;문제가&amp;nbsp;생겨&amp;nbsp;고쳐야&amp;nbsp;할&amp;nbsp;경우)&amp;nbsp;다시&amp;nbsp;확인할&amp;nbsp;때,&amp;nbsp;이&amp;nbsp;테스트가&amp;nbsp;무엇을&amp;nbsp;의도했는지&amp;nbsp;명확하게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;참고:&amp;nbsp;이&amp;nbsp;섹션은&amp;nbsp;코드에&amp;nbsp;주석을&amp;nbsp;달지&amp;nbsp;말고&amp;nbsp;깔끔하게&amp;nbsp;유지하라는&amp;nbsp;이&amp;nbsp;글의&amp;nbsp;다른&amp;nbsp;조언들과는&amp;nbsp;다소&amp;nbsp;상충합니다.&amp;nbsp;상황에&amp;nbsp;따라&amp;nbsp;판단해서&amp;nbsp;사용하세요!&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;&lt;b&gt;접근성&amp;nbsp;테스트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드&amp;nbsp;애플리케이션을&amp;nbsp;개발하는&amp;nbsp;소프트웨어&amp;nbsp;엔지니어로써&amp;nbsp;접근성을&amp;nbsp;고려하는&amp;nbsp;것은&amp;nbsp;중요합니다.&lt;br /&gt;&lt;br /&gt;하지만&amp;nbsp;접근성을&amp;nbsp;자주&amp;nbsp;간과하거나&amp;nbsp;무시하기&amp;nbsp;쉽고,&amp;nbsp;더&amp;nbsp;나쁜&amp;nbsp;경우에는&amp;nbsp;오히려&amp;nbsp;접근성&amp;nbsp;문제를&amp;nbsp;새로&amp;nbsp;도입하기도&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;React&amp;nbsp;Testing&amp;nbsp;Library(RTL)의&amp;nbsp;`getByLabelText()`,&amp;nbsp;`getByRole()`&amp;nbsp;같은&amp;nbsp;쿼리를&amp;nbsp;사용하면&amp;nbsp;시맨틱하게&amp;nbsp;올바른&amp;nbsp;마크업을&amp;nbsp;작성하고&amp;nbsp;있는지&amp;nbsp;확인하는&amp;nbsp;데&amp;nbsp;도움이&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;또한&amp;nbsp;RTL에는&amp;nbsp;`getRoles()`와&amp;nbsp;`isInaccessible()`&amp;nbsp;같은&amp;nbsp;헬퍼&amp;nbsp;함수도&amp;nbsp;있습니다.&amp;nbsp;솔직히&amp;nbsp;말하면&amp;nbsp;이&amp;nbsp;함수들은&amp;nbsp;거의&amp;nbsp;사용되지&amp;nbsp;않습니다.&lt;br /&gt;&lt;br /&gt;곧&amp;nbsp;다른&amp;nbsp;도구와&amp;nbsp;라이브러리를&amp;nbsp;사용해&amp;nbsp;(일부)&amp;nbsp;접근성&amp;nbsp;테스트를&amp;nbsp;자동화하는&amp;nbsp;방법에&amp;nbsp;관한&amp;nbsp;글을&amp;nbsp;올릴&amp;nbsp;예정입니다.&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;&lt;b&gt;React&amp;nbsp;Testing&amp;nbsp;Library에서는&amp;nbsp;`waitFor`와&amp;nbsp;`getBy`를&amp;nbsp;함께&amp;nbsp;사용하기보다&amp;nbsp;`findBy`를&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React&amp;nbsp;Testing&amp;nbsp;Library를&amp;nbsp;사용&amp;nbsp;중이고&amp;nbsp;타임아웃이나&amp;nbsp;재렌더링&amp;nbsp;등으로&amp;nbsp;인해&amp;nbsp;어떤&amp;nbsp;요소가&amp;nbsp;나타날&amp;nbsp;때까지&amp;nbsp;기다려야&amp;nbsp;한다면,&amp;nbsp;`await&amp;nbsp;waitFor(...)`&amp;nbsp;안에서&amp;nbsp;`expect`를&amp;nbsp;사용해&amp;nbsp;대기하는&amp;nbsp;방법을&amp;nbsp;시도할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;하지만 이러한 비동기 동작을 테스트할 때 더 '올바른' 방법은 `await screen.findBy`... 함수들을 사용하는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221276676&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Unnecessary - waitFor + getBy
await waitFor(() =&amp;gt; {
  expect(screen.getByText(&quot;Data loaded&quot;)).toBeInTheDocument();
});

// ✅ Better - findBy handles waiting automatically
expect(await screen.findByText(&quot;Data loaded&quot;)).toBeInTheDocument();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`waitFor`을 사용하는 것은 DOM에 나타나지 않는 항목들을 확인할 때, 예를 들어 어떤 함수나 spy가 호출되었는지 검증하는 경우에 더 적합합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1769221287149&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const someFn = vi.fn();

await waitFor(() =&amp;gt; expect(window.fetch).toHaveBeenCalled());
await waitFor(() =&amp;gt; expect(someFn).toHaveBeenCalled());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용자가&amp;nbsp;컴포넌트를&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;한다고&amp;nbsp;단언할&amp;nbsp;때&amp;nbsp;`toBeInTheDocument()`&amp;nbsp;대신&amp;nbsp;`toBeVisible()`를&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM에서&amp;nbsp;요소를&amp;nbsp;추가하거나&amp;nbsp;제거하는&amp;nbsp;대신&amp;nbsp;CSS로&amp;nbsp;요소를&amp;nbsp;숨기거나&amp;nbsp;표시하는&amp;nbsp;컴포넌트를&amp;nbsp;테스트할&amp;nbsp;때&amp;nbsp;흔히&amp;nbsp;발생하는&amp;nbsp;실수는&amp;nbsp;`toBeInTheDocument()`와&amp;nbsp;`toBeVisible()`를&amp;nbsp;구분하지&amp;nbsp;않는&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;Jest나&amp;nbsp;Vitest를&amp;nbsp;사용하고&amp;nbsp;인라인&amp;nbsp;스타일(예:&amp;nbsp;`style={{&amp;nbsp;display:&amp;nbsp;'none'&amp;nbsp;}}`)이나&amp;nbsp;hidden&amp;nbsp;HTML&amp;nbsp;속성으로&amp;nbsp;요소를&amp;nbsp;숨긴다면&amp;nbsp;`toBeVisible()`는&amp;nbsp;매우&amp;nbsp;유용하고&amp;nbsp;신뢰할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;하지만 클래스에 의존하는 경우(예: `className=&quot;hidden&quot;` 등)에는...CSS 파일에 있는 `.hidden { display: none; }` 규칙을 테스트 환경에서 로드하지 않으면 Jest나 Vitest는 해당 요소를 숨겨진 것으로 간주하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221317605&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Element exists but might be hidden
expect(screen.getByText(&quot;Success message&quot;)).toBeInTheDocument();

// ✅ Element is actually visible to users
// (Assuming Jest or Vitest can correctly determine if it is visible!)
expect(screen.getByText(&quot;Success message&quot;)).toBeVisible();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런&amp;nbsp;종류의&amp;nbsp;동작을&amp;nbsp;테스트할&amp;nbsp;때,&amp;nbsp;전적으로&amp;nbsp;CSS&amp;nbsp;기반이라면&amp;nbsp;Playwright,&amp;nbsp;Cypress&amp;nbsp;또는&amp;nbsp;Vitest의&amp;nbsp;Browser&amp;nbsp;모드와&amp;nbsp;같은&amp;nbsp;도구만&amp;nbsp;사용하는&amp;nbsp;것을&amp;nbsp;권장합니다.&amp;nbsp;다만&amp;nbsp;테스트&amp;nbsp;환경에서&amp;nbsp;CSS가&amp;nbsp;올바르게&amp;nbsp;로드되는지&amp;nbsp;반드시&amp;nbsp;확인해야&amp;nbsp;합니다.&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;&lt;b&gt;fetch&amp;nbsp;호출을&amp;nbsp;모킹하기&amp;nbsp;&amp;mdash;&amp;nbsp;fetch&amp;nbsp;모킹&amp;nbsp;또는&amp;nbsp;MSW&amp;nbsp;사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 실제(real) HTTP 요청을 하지 마세요! 항상 API 호출을 모킹해야 합니다.&lt;br /&gt;&lt;br /&gt;제&amp;nbsp;개인&amp;nbsp;선호는&amp;nbsp;`window.fetch`를&amp;nbsp;mock&amp;nbsp;하거나,&amp;nbsp;MSW를&amp;nbsp;사용해&amp;nbsp;API&amp;nbsp;호출을&amp;nbsp;mock&amp;nbsp;하는&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;실제&amp;nbsp;API&amp;nbsp;호출은&amp;nbsp;모킹보다&amp;nbsp;느리고,&amp;nbsp;신뢰할&amp;nbsp;수&amp;nbsp;없으며,&amp;nbsp;불필요하며,&amp;nbsp;다른&amp;nbsp;상황을&amp;nbsp;테스트하기가&amp;nbsp;더&amp;nbsp;어렵습니다&amp;nbsp;(예:&amp;nbsp;오류&amp;nbsp;사례).&lt;br /&gt;&lt;br /&gt;실제&amp;nbsp;API&amp;nbsp;호출을&amp;nbsp;하려면&amp;nbsp;E2E&amp;nbsp;(또는&amp;nbsp;API&amp;nbsp;계약)&amp;nbsp;테스트로&amp;nbsp;넘어가야&amp;nbsp;합니다.&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;&lt;b&gt;fireEvent&amp;nbsp;대신&amp;nbsp;userEvent를&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React&amp;nbsp;Testing&amp;nbsp;Library를&amp;nbsp;사용&amp;nbsp;중이라면&amp;nbsp;요소&amp;nbsp;클릭이나&amp;nbsp;텍스트&amp;nbsp;입력&amp;nbsp;같은&amp;nbsp;상호작용을&amp;nbsp;트리거하는&amp;nbsp;데&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;방법이&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;권장되는&amp;nbsp;방법은&amp;nbsp;userEvent(예:&amp;nbsp;userEvent.click(someButton))입니다.&amp;nbsp;userEvent는&amp;nbsp;마우스&amp;nbsp;이동,&amp;nbsp;호버,&amp;nbsp;mousedown&amp;nbsp;등&amp;nbsp;실제&amp;nbsp;사용자&amp;nbsp;동작에&amp;nbsp;가까운&amp;nbsp;관련&amp;nbsp;이벤트를&amp;nbsp;모두&amp;nbsp;발생시키므로&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;현실적입니다.&lt;br /&gt;&lt;br /&gt;fireEvent&amp;nbsp;함수들도&amp;nbsp;클릭&amp;nbsp;같은&amp;nbsp;동작을&amp;nbsp;시뮬레이션할&amp;nbsp;수는&amp;nbsp;있지만,&amp;nbsp;이러한&amp;nbsp;추가적인&amp;nbsp;현실적&amp;nbsp;동작들을&amp;nbsp;모두&amp;nbsp;포함하지는&amp;nbsp;않습니다.&amp;nbsp;가능하면&amp;nbsp;userEvent를&amp;nbsp;사용하세요.&amp;nbsp;fireEvent를&amp;nbsp;사용하고&amp;nbsp;싶거나(또는&amp;nbsp;사용해야)&amp;nbsp;하는&amp;nbsp;경우도&amp;nbsp;있긴&amp;nbsp;하지만,&amp;nbsp;그런&amp;nbsp;경우는&amp;nbsp;드뭅니다.&amp;nbsp;제가&amp;nbsp;본&amp;nbsp;좋은&amp;nbsp;예로는&amp;nbsp;복잡한&amp;nbsp;드롭다운을&amp;nbsp;테스트할&amp;nbsp;때로,&amp;nbsp;userEvent가&amp;nbsp;지나치게&amp;nbsp;현실적으로&amp;nbsp;동작하면서&amp;nbsp;onBlur를&amp;nbsp;발생시켜&amp;nbsp;테스트를&amp;nbsp;너무&amp;nbsp;복잡하게&amp;nbsp;만든&amp;nbsp;경우입니다.&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;&lt;b&gt;커스텀&amp;nbsp;매처를&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 객체에 대해 단언을 하는 유사한 테스트가 많다면, `expect(...)`에 사용하는 Jest나 Vitest용 커스텀 매처를 작성할 수 있다는 것을 잊지 마세요.&lt;br /&gt;&lt;br /&gt;예를 들어, 테스트 스위트 전반에서 사용자 객체를 테스트하고 있다면:&lt;/p&gt;
&lt;pre id=&quot;code_1769221374478&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// add this in your jest or vitest setup file
expect.extend({
  toBeValidUser(received) {
    const pass =
      received &amp;amp;&amp;amp;
      typeof received.id === &quot;string&quot; &amp;amp;&amp;amp;
      typeof received.name === &quot;string&quot; &amp;amp;&amp;amp;
      received.email.includes(&quot;@&quot;);

    if (pass) {
      return {
        message: () =&amp;gt; `expected ${received} not to be a valid user`,
        pass: true,
      };
    } else {
      return {
        message: () =&amp;gt; `expected ${received} to be a valid user`,
        pass: false,
      };
    }
  },
});

// then you can use it in tests:
test(&quot;creates user successfully&quot;, () =&amp;gt; {
  const user = createUser(&quot;John&quot;, &quot;john@example.com&quot;);

  expect(user).toBeValidUser();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;`expect.extend()`로&amp;nbsp;설정한&amp;nbsp;커스텀&amp;nbsp;매처들은&amp;nbsp;Jest와&amp;nbsp;Vitest&amp;nbsp;모두에서&amp;nbsp;작동합니다.&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;&lt;b&gt;여러&amp;nbsp;시간대를&amp;nbsp;테스트하는&amp;nbsp;것을&amp;nbsp;잊지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜와&amp;nbsp;관련된&amp;nbsp;것은&amp;nbsp;모두&amp;nbsp;까다롭습니다.&amp;nbsp;프론트엔드&amp;nbsp;애플리케이션을&amp;nbsp;테스트할&amp;nbsp;때&amp;nbsp;모든&amp;nbsp;사용자가&amp;nbsp;UTC&amp;nbsp;시간대에&amp;nbsp;있다고&amp;nbsp;가정할&amp;nbsp;수는&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;영국의&amp;nbsp;겨울철(UTC+0)에는&amp;nbsp;앱이&amp;nbsp;잘&amp;nbsp;작동하는&amp;nbsp;것을&amp;nbsp;본&amp;nbsp;적이&amp;nbsp;있습니다.&amp;nbsp;그런데&amp;nbsp;영국의&amp;nbsp;서머타임(UTC+1)에는&amp;nbsp;테스트는&amp;nbsp;계속&amp;nbsp;통과하더라도&amp;nbsp;실제&amp;nbsp;사용자&amp;nbsp;환경에서는&amp;nbsp;문제가&amp;nbsp;발생하곤&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;날짜와&amp;nbsp;관련된&amp;nbsp;기능이&amp;nbsp;있다면,&amp;nbsp;특히&amp;nbsp;시간&amp;nbsp;차이를&amp;nbsp;계산하는&amp;nbsp;경우에는&amp;nbsp;반드시&amp;nbsp;여러&amp;nbsp;시간대에서&amp;nbsp;테스트해야&amp;nbsp;합니다.&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;&lt;b&gt;fake&amp;nbsp;timer를&amp;nbsp;사용하고&amp;nbsp;실시간을&amp;nbsp;기다리지&amp;nbsp;마세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jest와&amp;nbsp;Vitest에서&amp;nbsp;fake&amp;nbsp;timer를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;시스템&amp;nbsp;시간을&amp;nbsp;특정&amp;nbsp;날짜로&amp;nbsp;설정하고,&amp;nbsp;대기&amp;nbsp;중인&amp;nbsp;모든&amp;nbsp;설정&amp;nbsp;또는&amp;nbsp;인터벌&amp;nbsp;타임아웃을&amp;nbsp;실행합니다.&lt;br /&gt;&lt;br /&gt;테스트는&amp;nbsp;시뮬레이션될&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;즉시&amp;nbsp;실행되어야&amp;nbsp;할&amp;nbsp;무언가를&amp;nbsp;위해&amp;nbsp;실시간을&amp;nbsp;기다려서는&amp;nbsp;안&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;100ms만&amp;nbsp;기다리더라도,&amp;nbsp;테스트&amp;nbsp;스위트가&amp;nbsp;방대하면&amp;nbsp;그&amp;nbsp;시간이&amp;nbsp;모두&amp;nbsp;누적됩니다.&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;&lt;b&gt;mock&amp;nbsp;데이터에&amp;nbsp;고유한&amp;nbsp;문자열을&amp;nbsp;사용하세요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤&amp;nbsp;값이&amp;nbsp;&quot;mock-value&quot;일&amp;nbsp;것으로&amp;nbsp;예상된다는&amp;nbsp;오류가&amp;nbsp;발생하고,&amp;nbsp;코드베이스를&amp;nbsp;찾아보니&amp;nbsp;&quot;mock-value&quot;를&amp;nbsp;정의한&amp;nbsp;곳이&amp;nbsp;15곳이나&amp;nbsp;있다면&amp;nbsp;더&amp;nbsp;답답한&amp;nbsp;일은&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;모든&amp;nbsp;mock&amp;nbsp;데이터나&amp;nbsp;값을&amp;nbsp;어느&amp;nbsp;정도&amp;nbsp;고유하게&amp;nbsp;만들어&amp;nbsp;두면,&amp;nbsp;실패했을&amp;nbsp;때&amp;nbsp;원인을&amp;nbsp;훨씬&amp;nbsp;더&amp;nbsp;쉽게&amp;nbsp;추적할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;작업이&amp;nbsp;훨씬&amp;nbsp;수월해집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221434471&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ Bad - generic mock values make debugging hard
const mockUser = {
  id: &quot;test-id&quot;, // &amp;lt;&amp;lt; same id as below
  name: &quot;Test&quot;,
  email: &quot;test@example.com&quot;,
};

const mockProduct = {
  id: &quot;test-id&quot;, // &amp;lt;&amp;lt; same id as above
  name: &quot;Test&quot;,
  price: 100,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;테스트가&amp;nbsp;`&quot;expected&amp;nbsp;'test-id'&amp;nbsp;but&amp;nbsp;received&amp;nbsp;'undefined'&quot;`와&amp;nbsp;같이&amp;nbsp;실패하면&amp;nbsp;어떤&amp;nbsp;mock&amp;nbsp;값이&amp;nbsp;문제를&amp;nbsp;일으키는지&amp;nbsp;쉽게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769221448249&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ Good - unique mock values for easier debugging
const mockUser = {
  id: &quot;user-john-123&quot;,
  name: &quot;Bart&quot;,
  email: &quot;bart@example.com&quot;,
};

const mockProduct = {
  id: &quot;product-laptop-456&quot;,
  name: &quot;Macbook Pro&quot;,
  price: 2999,
};&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;id나&amp;nbsp;name이&amp;nbsp;일치하지&amp;nbsp;않아&amp;nbsp;테스트가&amp;nbsp;실패하면&amp;nbsp;어떤&amp;nbsp;항목이&amp;nbsp;오류를&amp;nbsp;일으키는지&amp;nbsp;쉽게&amp;nbsp;파악할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;</description>
      <category>이것저것</category>
      <category>frontend</category>
      <category>Test</category>
      <category>번역</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/94</guid>
      <comments>https://kangs-develop.tistory.com/94#entry94comment</comments>
      <pubDate>Sat, 24 Jan 2026 11:00:05 +0900</pubDate>
    </item>
    <item>
      <title>새해 첫 개발 서적 [리액트 훅을 활용한 마이크로 상태 관리]</title>
      <link>https://kangs-develop.tistory.com/93</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_8476.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yMCDv/dJMcadm5ZOn/dKZ6NT9WIc3qJvGV8wILe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yMCDv/dJMcadm5ZOn/dKZ6NT9WIc3qJvGV8wILe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yMCDv/dJMcadm5ZOn/dKZ6NT9WIc3qJvGV8wILe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyMCDv%2FdJMcadm5ZOn%2FdKZ6NT9WIc3qJvGV8wILe1%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;505&quot; height=&quot;673&quot; data-filename=&quot;IMG_8476.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&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;작년 10월 생일날에 회사 후배가 생일 선물로 사준 책이긴 한데,&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;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;2022년에는 Redux로 상태관리를 했고, 2023년에는 Recoil과 react-query를 사용해 상태를 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년에는 Redux를 사용하던 애플리케이션에서 Redux를 Zustand로 마이그레이션 하고 Zustand로 전역 상태를 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 최근에 진행했던 프로젝트에는 Jotai를 사용해 상태를 다뤘다.&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;, &quot;유명하니깐&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;전역 상태 관리 라이브러리를 사용하는 이유를 단순히 &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;두 번째로 인상 깊었던 점은 Jotai 사용 패턴이다.&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;Jotai에서는 배열 데이터를 더 쉽게 다룰 수 있는 패턴이 있는데, Atoms in Atom 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;atom을 만들 때 atom 배열을 담는 atom을 만들고, atom을 자식 컴포넌트에 props로 내려주어 자식 컴포넌트에서는 전달받은 atom을 useAtom을 사용해 각각의 atom으로 다루는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 패턴을 사용하면 기존 배열 데이터에서 현재의 index에 위치한 데이터를 찾지 않아도 데이터를 다룰 수 있다.&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;마지막으로 인상 깊었던 점은 React-Traked이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트를 진행하며 이 상태가 과연 전역 상태일까? 라는 고민을 했고, 전역 상태를 남발하고 싶지 않아서 Context API를 사용해 데이터를 지엽적으로 공유하고 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Context API는 상태를 관리하는 라이브러리가 아닌 상태를 추적하는 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context는 상태를 공유하기 위해 Provider을 만들고 state를 주입하여 자식 컴포넌트 트리에 상태를 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Context Value를 복잡한 객체로 다룰 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 자식 컴포넌트가 구독하지 않는 값이 B 자식 컴포넌트에서 변경된다면 A 컴포넌트에서도 리랜더링이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 최악의 경우 애플리케이션의 성능을 저하시키는 요인이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React-Traked는 상태 추적 라이브러리로써 선택자 패턴으로 내부에서 상태 사용을 추적하며 자동으로 리랜더링 최적화를 해준다.&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;</description>
      <category>이것저것</category>
      <category>독서</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/93</guid>
      <comments>https://kangs-develop.tistory.com/93#entry93comment</comments>
      <pubDate>Sun, 4 Jan 2026 17:18:54 +0900</pubDate>
    </item>
    <item>
      <title>FSD 아키텍처의 참조 규칙</title>
      <link>https://kangs-develop.tistory.com/92</link>
      <description>&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b6677e8e-1e72-4417-8fb5-f7dbc8a67b72&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7440aa48-f18a-4ed9-a79b-3adc1c435622&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 프로젝트가 커질수록 우리는 코드의 구조와 의존성 관리에 대해 고민하게 됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7440aa48-f18a-4ed9-a79b-3adc1c435622&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b5748985-1bff-4e82-b3e7-5263c86953e4&quot; data-ke-size=&quot;size16&quot;&gt;&quot;이 컴포넌트가 저 레이어의 코드를 참조해도 될까?&quot;, &quot;같은 기능 내 파일끼리는 어떻게 import 해야 할까?&quot; 같은 질문들이 늘어나고, 일관성 없는 import 패턴은 프로젝트의 복잡도를 기하급수적으로 증가시킵니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b5748985-1bff-4e82-b3e7-5263c86953e4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;dff74fe6-035e-45a6-828c-15a676c61840&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 FSD(Feature-Sliced Design) 아키텍처에서 다른 소스 코드를 참조할 때 절대 경로, 상대 경로를 혼재해 사용하는 전략에 대해 살펴보겠습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;dff74fe6-035e-45a6-828c-15a676c61840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;03b65581-e790-4202-add1-0f0d4046e524&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FSD 아키텍처에 대한 간단한 이야기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7b4647f8-c196-443d-a2f5-cdb3d830d1b1&quot; data-ke-size=&quot;size16&quot;&gt;FSD(Feature-Sliced Design) 아키텍처는 &lt;b&gt;레이어,&lt;/b&gt;&amp;nbsp;&lt;b&gt;슬라이스, 세그먼트&lt;/b&gt; 단위로 나눠지며 프론트엔드 애플리케이션을 도메인(기능 - feature) 단위로 작게 쪼개(slice)어 디자인(design)하는 아키텍처입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7b4647f8-c196-443d-a2f5-cdb3d830d1b1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e49317b3-1852-45f8-be49-15525a9776d6&quot; data-ke-size=&quot;size16&quot;&gt;이때 각 레이어는 명확한 책임(역할)을 가지며, 레이어는 아래의 구조로 나눠집니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e49317b3-1852-45f8-be49-15525a9776d6&quot; data-ke-style=&quot;style3&quot;&gt;app # 애플리케이션 전역 설정 &lt;br /&gt;pages # 페이지 레벨 컴포넌트 &lt;br /&gt;widgets # 독립된 UI 블록의 모음 &lt;br /&gt;features # 사용자 시나리오와 기능 &lt;br /&gt;entities # 비즈니스 엔티티 &lt;br /&gt;shared # 공용 유틸리티, 상수(혹은 설정), UI 컴포넌트, library 코드&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7c4bc6ba-6792-4a3c-aad7-d50484593e7f&quot; data-ke-size=&quot;size16&quot;&gt;이런 구조를 통해 각 레이어는 독립적으로 발전할 수 있으며, 의존성 방향이 명확해집니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;c14a0019-81c4-4b11-939d-8795dec92cd8&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처에 대한 자세한 내용은 생략하겠습니다! 문서가 잘 되어있으니 아래 문서를 참고해보세요!&lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765873951852&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;개요 | Feature-Sliced Design&quot; data-og-description=&quot;Feature-Sliced Design (FSD) 는 프론트엔드 애플리케이션 구조를 위한 아키텍처 방법론입니다.&quot; data-og-host=&quot;feature-sliced.design&quot; data-og-source-url=&quot;https://feature-sliced.design/kr/docs/get-started/overview&quot; data-og-url=&quot;https://feature-sliced.design/kr/docs/get-started/overview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cPymav/hyZPm9UsJT/2886NyPzFCkdkKDoWKVGiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=298_67_408_177,https://scrap.kakaocdn.net/dn/dD4MtC/hyZPnuchUe/gdIRKQz7eWokFp8I5tmbC1/img.png?width=1200&amp;amp;height=630&amp;amp;face=298_67_408_177,https://scrap.kakaocdn.net/dn/xxPJ7/hyZPAmKmhy/46jMdALr0pp9atu2S1yyN1/img.png?width=2920&amp;amp;height=1040&amp;amp;face=0_0_2920_1040&quot;&gt;&lt;a href=&quot;https://feature-sliced.design/kr/docs/get-started/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://feature-sliced.design/kr/docs/get-started/overview&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cPymav/hyZPm9UsJT/2886NyPzFCkdkKDoWKVGiK/img.png?width=1200&amp;amp;height=630&amp;amp;face=298_67_408_177,https://scrap.kakaocdn.net/dn/dD4MtC/hyZPnuchUe/gdIRKQz7eWokFp8I5tmbC1/img.png?width=1200&amp;amp;height=630&amp;amp;face=298_67_408_177,https://scrap.kakaocdn.net/dn/xxPJ7/hyZPAmKmhy/46jMdALr0pp9atu2S1yyN1/img.png?width=2920&amp;amp;height=1040&amp;amp;face=0_0_2920_1040');&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;개요 | Feature-Sliced Design&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Feature-Sliced Design (FSD) 는 프론트엔드 애플리케이션 구조를 위한 아키텍처 방법론입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;feature-sliced.design&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;da8ec9fe-3539-43e6-b449-4e4f883aebc0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;177c9cbc-0240-444c-a715-4aafd3dfd10a&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FSD 아키텍처 내 규칙&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6c40666b-6e94-49a7-9342-bc77a84f6c31&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처는 몇 가지 핵심 규칙을 통해 코드의 품질을 유지합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6c40666b-6e94-49a7-9342-bc77a84f6c31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;bd37f0e7-79aa-486e-9a6a-635a77999e2a&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레이어에서 레이어로 흐르는 엄격한 의존성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a7ccae9e-0cc4-4fa6-9515-0a48c636eac2&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처에서 의존성은 항상 &lt;b&gt;위에서 아래로만&lt;/b&gt; 흐릅니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a7ccae9e-0cc4-4fa6-9515-0a48c636eac2&quot; data-ke-style=&quot;style3&quot;&gt;app &amp;rarr; pages &amp;rarr; widgets &amp;rarr; features &amp;rarr; entities &amp;rarr; shared&lt;/blockquote&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0d8ead29-a695-44d9-ae67-2cd0c0300641&quot; data-ke-size=&quot;size16&quot;&gt;상위 레이어는 하위 레이어를 참조할 수 있지만, 하위 레이어는 상위 레이어를 참조할 수 없습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;1c4c733f-26c7-4e42-a664-f9854369a32b&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 entities 레이어의 코드는 features 레이어를 절대 참조할 수 없으며, shared 레이어는 app 레이어를 참조할 수 없습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4a2b584e-9946-420c-80b5-2e7b8c471b53&quot; data-ke-size=&quot;size16&quot;&gt;이런 단방향 의존성 규칙은 순환 참조를 방지하고, 코드의 흐름을 예측 가능하게 만듭니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4a2b584e-9946-420c-80b5-2e7b8c471b53&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;3333ffb9-5f6a-4d43-a9f0-341b34fbd102&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공개 API와 슬라이스 캡슐화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;321ee0f2-339b-464e-90a6-2bc60d00729e&quot; data-ke-size=&quot;size16&quot;&gt;각 슬라이스는 index.ts 파일을 통해 외부에 공개할 API만을 내보냅니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;321ee0f2-339b-464e-90a6-2bc60d00729e&quot; data-ke-style=&quot;style3&quot;&gt;features/&lt;br /&gt;&amp;nbsp; # 슬라이스&lt;br /&gt;&amp;nbsp; ㄴpost/&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;# 이하 세그먼트&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴui/ &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴmodel/ &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴconstants/ &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;ㄴindex.ts # 공개 API&lt;/blockquote&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a4078599-1855-4e44-805b-6e5cdb61abfe&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a4078599-1855-4e44-805b-6e5cdb61abfe&quot; data-ke-size=&quot;size16&quot;&gt;배럴 파일(index.ts)을 통해 슬라이스 내 코드를 외부에 노출하고, 노출되지 않은 소스 코드의 구현은 다른 레이어에 노출하지 않음으로써 내부 소스 코드는 캡슐화가 됩니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a4078599-1855-4e44-805b-6e5cdb61abfe&quot; data-ke-size=&quot;size16&quot;&gt;이는 다른 레이어의 소스가 해당 레이어에서 관심사가 아님으로 결합을 최소화 하는 원칙과도 같습니다. 이를 통해 슬라이스 내부 구현을 자유롭게 리팩토링할 수 있으며, 외부 코드에 영향을 주지 않습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a4078599-1855-4e44-805b-6e5cdb61abfe&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4e870e2c-7d79-416a-bc53-64e65e7aa784&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;같은 레이어 내 슬라이스 참조 금지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;85ccc9ff-2bac-4dbe-8a9b-5e1e225c1809&quot; data-ke-size=&quot;size16&quot;&gt;같은 레이어에 속한 슬라이스끼리는 서로를 참조할 수 없습니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;85ccc9ff-2bac-4dbe-8a9b-5e1e225c1809&quot; data-ke-style=&quot;style3&quot;&gt;features/ &lt;br /&gt;&amp;nbsp; &amp;nbsp;ㄴpost/ # ❌ comment를 직접 참조 불가 &lt;br /&gt;&amp;nbsp; &amp;nbsp;ㄴcomment/ # ❌ post를 직접 참조 불가&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2aabef81-24fb-421e-83c4-d75f34944563&quot; data-ke-size=&quot;size16&quot;&gt;만약 두 슬라이스가 서로 의존해야 한다면, 이는 설계상의 문제일 가능성이 높습니다. 공통 로직은 하위 레이어(entities 또는 shared)에 위치하거나, 상위 레이어(pages)에서 조합하는 방식으로 재설계 해야합니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2aabef81-24fb-421e-83c4-d75f34944563&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;1de7eee3-4d28-4b67-9a92-98dab9a92024&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예외 레이어: shared, app&lt;/b&gt;&lt;/h4&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0067403c-620e-4e00-a770-958a2f512ac8&quot; data-ke-size=&quot;size16&quot;&gt;shared, app 레이어는 슬라이스가 곧 세그먼트 입니다. 슬라이스 내 소스코드는 세분화 되지 않으며 세그먼트의 역할을 할 수 있습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0067403c-620e-4e00-a770-958a2f512ac8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e4a8b61c-e2cc-4702-986d-c894b71e96bc&quot; data-ke-size=&quot;size16&quot;&gt;또 shared 레이어 속 소스 코드는 서로를 참조할 수 있습니다. app 레이어 역시 전역 설정을 담당하므로 특별한 제약 없이 필요한 모든 레이어를 참조할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;33f09a51-26c9-4934-92a0-3cc1b2446971&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;662df51a-0f44-498d-81bb-fb90ac311b0d&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;공개 API&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7d80bdf3-a830-40d8-8b98-0205e7ed721f&quot; data-ke-size=&quot;size16&quot;&gt;슬라이스의 배럴 파일(index.ts) 파일은 해당 슬라이스의 공개 API 역할을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873739076&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/post/index.ts
export { PostList, PostDetail } from './ui'
export { usePostQuery } from './model'
export { POST_STATUS } from './constants'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2df4e376-228b-4abb-8a7b-a6448c39128e&quot; data-ke-size=&quot;size16&quot;&gt;외부에서는 반드시 이 공개 API를 통해서만 슬라이스 내부에 접근해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873751285&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// pages/post-page/ui/post-page.tsx
import { PostList } from '@/features/post'  // ✅ 공개 API 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;1b796074-050f-4ad4-9410-44427a19f1b9&quot; data-ke-size=&quot;size16&quot;&gt;내부 구현에 직접 접근하는 것은 금지됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873765009&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ 내부 경로 직접 접근
import { PostList } from '@/features/post/ui/post-list'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6f0acecb-a4cf-485b-a923-4e5bc3378ab3&quot; data-ke-size=&quot;size16&quot;&gt;이런 캡슐화를 통해 슬라이스 내부 구조가 변경되어도 외부 코드는 영향을 받지 않습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6f0acecb-a4cf-485b-a923-4e5bc3378ab3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;936f1e9c-285c-4c53-9d6c-a5032973a51d&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;import 규칙&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4e98e32c-a0ee-40cd-9d6f-d1eb9abe3608&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처에서는 절대 경로와 상대 경로를 혼재해서 사용하는 것을 권장합니다. 이는 단순한 스타일의 문제가 아니라, &lt;b&gt;코드의 의존성 범위를 시각적으로 명확하게 구분&lt;/b&gt;하기 위함입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4e98e32c-a0ee-40cd-9d6f-d1eb9abe3608&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;29c80217-cec8-4b41-af30-949d9c40a9eb&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;import 선언 순서&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;72271cb0-96a4-4419-9fed-f6bd6785bb5b&quot; data-ke-size=&quot;size16&quot;&gt;import는 다음의 3단계 순서를 따릅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873788687&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 외부 라이브러리 (node_modules)
import { useMemo } from 'react'
import { useNavigate } from 'react-router'

// 2. 타 레이어의 슬라이스 (절대 경로)
import { Comment } from '@/entities/comment'

// 3. 같은 슬라이스 내부 (상대 경로)
import { Post } from '../model/type'
import { PostButton } from './post-button'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7151e7fc-c5e2-4d73-816c-a6d8f1818c26&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;절대 경로를 사용하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a08ff796-fe01-437f-8081-ecafc34c830e&quot; data-ke-size=&quot;size16&quot;&gt;타 레이어의 슬라이스를 참조할 때는 &lt;b&gt;반드시 절대 경로&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873802999&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/post/ui/post.tsx
import { Comment } from '@/entities/comment'
import { Button } from '@/shared/ui'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ee69fcaf-1c86-4e13-9f14-7d047bab29fc&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로는 항상 슬라이스의 공개 API(index.ts)를 통해 접근해야 하며, 내부 경로를 직접 명시해서는 안 됩니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ee69fcaf-1c86-4e13-9f14-7d047bab29fc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;d1639711-6333-4d16-93a3-5fc5905869e9&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;상대 경로를 사용하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e356302a-009f-48ea-abed-a5bae19687f9&quot; data-ke-size=&quot;size16&quot;&gt;같은 슬라이스 내부의 코드를 참조할 때는 &lt;b&gt;상대 경로&lt;/b&gt;를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873819585&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/post/ui/post.tsx
import { Post } from '../model/type'
import { usePostQuery } from '../api/query'
import { PostButton } from './post-button'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;5d13085e-8e8d-4955-837f-8dd0262a9094&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처에서 슬라이스는 2단계 깊이(슬라이스/세그먼트)로 평탄하게 유지되므로, 상대 경로는 항상 ../ 한 단계 또는 ./ 같은 경로로만 표현됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873835535&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;features/post/
  ├── ui/
  │   ├── post.tsx
  │   └── post-button.tsx
  ├── model/
  │   ├── type.ts
  │   └── query.ts
  └── index.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;edaa9b14-c29a-4a93-8974-bf835dcf2b4a&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 평탄한 구조를 유지하면 ../../ 같은 복잡한 경로가 등장하지 않아 코드의 가독성이 높아집니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;edaa9b14-c29a-4a93-8974-bf835dcf2b4a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b51c9cec-7fb2-498c-8038-3465bc7bd8d2&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;df28064b-e00f-4a21-b159-86d48aded540&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로와 상대 경로를 혼재해서 사용하는 전략은 다음과 같은 이점을 제공합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;df28064b-e00f-4a21-b159-86d48aded540&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;04886f46-b627-49c2-a0fa-31d9d734356e&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 의존성 범위를 한눈에 파악&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1765873862933&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from 'react'
import { Comment } from '@/entities/comment'
import { Post } from '../model/type'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;5587d58d-8e6a-4139-a561-47532a02ee2c&quot; data-ke-size=&quot;size16&quot;&gt;이 코드를 보는 순간, 우리는 즉시 알 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;8b32f32b-5e07-43b6-a562-26ac8b078a16&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;96439bf5-a4f7-445c-a9ee-c5d517c41cc0&quot;&gt;react는 외부 라이브러리&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;c482bd89-607d-4d02-b21d-f03e3f2d6d74&quot;&gt;@/entities/comment는 다른 레이어에 대한 의존성&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ce431335-ad5b-4287-a72b-c3cf2fe955bf&quot;&gt;../model/type은 같은 슬라이스 내부 코드&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7a9925b1-2923-402e-b719-4055fae35540&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 리팩토링 영향 범위 최소화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;342d9e55-b9ec-446c-9b64-65587b80cb44&quot; data-ke-size=&quot;size16&quot;&gt;상대 경로로 참조되는 코드는 같은 슬라이스 내부이므로, 수정 시 &lt;b&gt;외부에 영향을 주지 않습니다&lt;/b&gt;. 반대로 절대 경로로 참조되는 코드를 수정하면 다른 레이어에 영향을 줄 수 있다는 것을 즉시 인지할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;342d9e55-b9ec-446c-9b64-65587b80cb44&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b5ea2d5e-b055-482b-b278-94fcaae873a7&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 캡슐화 원칙 강화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;8e9d27fb-b10e-461b-b6f3-50a3f4e69199&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로는 반드시 공개 API를 통해서만 사용하도록 강제할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873877930&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ✅ 공개 API 사용
import { PostList } from '@/features/post'

// ❌ 내부 구조 직접 접근 (린트로 차단 가능)
import { PostList } from '@/features/post/ui/post-list'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2faad40d-6f30-4f7d-8f77-1ba5119df14a&quot; data-ke-size=&quot;size16&quot;&gt;ESLint 규칙을 통해 내부 경로 직접 접근을 차단할 수 있습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2faad40d-6f30-4f7d-8f77-1ba5119df14a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;8bde3c4f-cd0f-4d63-a484-c51381cd3361&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 예측 가능한 경로 패턴&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;44eaff2d-3c00-4325-9ff4-e58e7a854751&quot; data-ke-size=&quot;size16&quot;&gt;FSD의 평탄한 구조에서는 항상 ../세그먼트/파일 또는 ./파일 형태로만 상대 경로가 나타납니다. 이는 개발자가 경로를 예측하기 쉽게 만들고, 인지 부하를 크게 줄여줍니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;44eaff2d-3c00-4325-9ff4-e58e7a854751&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;cc7622fa-e08c-4467-9e2e-91ee2dca7f2f&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주의해야 할 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b8cc7108-a561-45fe-a5fa-0399d160279a&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로와 상대 경로를 혼재해서 사용할 때 주의해야 할 몇 가지 사항이 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;b8cc7108-a561-45fe-a5fa-0399d160279a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e2b58fe2-512f-47aa-b49f-fcdf56d8b26a&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 팀 컨벤션의 명확한 공유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;190062f8-5124-43c3-912c-8b73f8ad02d0&quot; data-ke-size=&quot;size16&quot;&gt;이런 규칙은 팀 전체가 이해하고 따라야 효과가 있습니다. 프로젝트 초기에 충분히 논의하고, 문서화하여 공유해야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;190062f8-5124-43c3-912c-8b73f8ad02d0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a6798fe4-d566-4ab2-bdcc-fdfdc2efed85&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. ESLint 규칙으로 강제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e6d87270-67d0-4fdd-92d8-a902aadf7e63&quot; data-ke-size=&quot;size16&quot;&gt;사람이 실수 없이 규칙을 지키기는 어렵습니다. eslint-plugin-boundaries 같은 도구를 사용해 import 규칙을 자동으로 검증하고 강제하는 것이 좋습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873895829&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .eslintrc.js 예시
{
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [
        {
          group: ['@/features/*/ui/*', '@/features/*/model/*'],
          message: '슬라이스 내부 경로는 상대 경로를 사용하세요'
        }
      ]
    }]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;a95b1857-f255-47a0-b4d3-e3ea71c08bc9&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 일관성 유지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e9b9ed99-de90-4b9a-8885-9dc66f482496&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로는 타 레이어 참조, 상대 경로는 같은 슬라이스 내부 참조라는 원칙을 반드시 지켜야 합니다. 예외를 두는 순간 규칙의 의미가 사라지고 혼란만 가중됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e9b9ed99-de90-4b9a-8885-9dc66f482496&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;bc509dd7-b1cd-4c4f-80ff-cd86114824aa&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예시로 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;344a3dd0-eade-4312-a4fa-b324c362fbca&quot; data-ke-size=&quot;size16&quot;&gt;실제 코드 예시를 통해 import 규칙을 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1765873911961&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// features/post/ui/post.tsx

import { useMemo } from 'react'
import { useNavigate } from 'react-router'

import { Comment } from '@/entities/comment'
import { Button } from '@/shared/ui'

import { Post } from '../model/type'
import { usePostQuery } from '../api/query'
import { POST_STATUS } from '../constants'
import { PostButton } from './post-button'

export const PostComponent = () =&amp;gt; {
  const navigate = useNavigate()
  const { data: post } = usePostQuery()
  
  const isPublished = useMemo(
    () =&amp;gt; post?.status === POST_STATUS.PUBLISHED,
    [post]
  )

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{post?.title}&amp;lt;/h1&amp;gt;
      &amp;lt;Button onClick={() =&amp;gt; navigate('/posts')}&amp;gt;
        목록으로
      &amp;lt;/Button&amp;gt;
      {isPublished &amp;amp;&amp;amp; (
        &amp;lt;&amp;gt;
          &amp;lt;Comment commentId={post.commentId} /&amp;gt;
          &amp;lt;PostButton post={post} /&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/div&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-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9ecc9cee-91eb-478b-b26b-87266032fb28&quot; data-ke-size=&quot;size16&quot;&gt;이 코드를 보면 아래 내용을 즉시 파악할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e37606df-15df-4778-a733-674734bc93b4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;188fcbfa-71ab-4c10-8817-7c47fca032b9&quot;&gt;react, react-router는 외부 라이브러리 의존성&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;e9184b41-77a0-4790-81fb-2e9db66acc57&quot;&gt;@/entities/comment, @/shared/ui는 다른 레이어에 대한 의존성&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;c6fccbc6-6d70-42c2-889f-eb743c8583eb&quot;&gt;../model/type, ../api/query 등은 같은 슬라이스 내부 코드&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0a67beff-3fa3-47ab-ac4c-690e80540be7&quot; data-ke-size=&quot;size16&quot;&gt;만약 Comment 컴포넌트를 수정해야 한다면, entities 레이어 전체를 확인해야 한다는 것을 알 수 있습니다. 반대로 PostButton을 수정한다면 같은 슬라이스 내부에서만 영향을 확인하면 됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;0a67beff-3fa3-47ab-ac4c-690e80540be7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2a748569-bf71-4975-a963-15b20f3bb909&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6a3e9318-b71b-48d6-abda-7f77a5b6c4e6&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 프로젝트의 복잡도가 증가할수록 명확한 아키텍처와 일관된 import 규칙의 중요성은 더욱 커집니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9d10ce73-6c9f-4bc2-94d6-1029c6f678d7&quot; data-ke-size=&quot;size16&quot;&gt;절대 경로와 상대 경로를 혼재해서 사용하는 전략은 단순히 &quot;어떻게 import 할 것인가&quot;를 넘어서, &lt;b&gt;코드의 의존성 범위를 시각적으로 명확하게 드러내는&lt;/b&gt; 강력한 도구입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9d10ce73-6c9f-4bc2-94d6-1029c6f678d7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ce340fed-0a65-416f-ac9f-210051b93cea&quot; data-ke-size=&quot;size16&quot;&gt;FSD 아키텍처의 레이어 구조와 결합된 import 규칙은 다음을 가능하게 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;08f60d12-d58c-440d-ac90-17f96cbb4634&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;88a83d47-dcc1-4d70-91ab-c0a28a38fa9a&quot;&gt;코드 리뷰 시 의존성을 즉시 파악&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;80d61034-64ec-4ea0-8aad-f18de8f9b8bf&quot;&gt;리팩토링 시 영향 범위를 명확하게 인지&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;7ede6589-e2f5-4286-b1cd-a82c038525f3&quot;&gt;캡슐화 원칙을 코드 레벨에서 강제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;523be126-81b7-422f-9199-bb2323e37f83&quot; data-ke-size=&quot;size16&quot;&gt;물론 이런 규칙을 도입하기 위해서는 팀 전체의 이해와 합의, 그리고 린트 도구를 통한 자동화가 필요합니다. 하지만 일단 자리 잡고 나면, 프로젝트의 유지보수성과 확장성이 크게 향상되는 것을 경험할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;523be126-81b7-422f-9199-bb2323e37f83&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;4c73696a-8282-46d2-958d-3ad7263b1687&quot; data-ke-size=&quot;size16&quot;&gt;여러분의 프로젝트에도 명확한 import 규칙을 도입하여, 더 나은 코드 품질과 개발 경험을 만들어가시길 바랍니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;93bae7a0-1877-43ef-9d7e-81b7a302391e&quot; data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/92</guid>
      <comments>https://kangs-develop.tistory.com/92#entry92comment</comments>
      <pubDate>Tue, 16 Dec 2025 17:36:24 +0900</pubDate>
    </item>
    <item>
      <title>2025년 연말 개인 회고</title>
      <link>https://kangs-develop.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 연말 개인 회고를 해보려고 한다. 개인적으로&amp;nbsp;2025년은&amp;nbsp;많은&amp;nbsp;의미가&amp;nbsp;있는&amp;nbsp;해이자&amp;nbsp;많은&amp;nbsp;변화가&amp;nbsp;있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;대학원&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;대학원에&amp;nbsp;진학해&amp;nbsp;석사&amp;nbsp;과정을&amp;nbsp;시작했다. &lt;br&gt;처음&amp;nbsp;개발자로&amp;nbsp;일을&amp;nbsp;시작했을&amp;nbsp;땐&amp;nbsp;비전공자로&amp;nbsp;시작했었다. &lt;br&gt;첫 회사를 다니면서 한국방송통신대학교에 편입을 해서 컴퓨터과학을 전공했고, 24년&amp;nbsp;2월에&amp;nbsp;졸업을&amp;nbsp;하며&amp;nbsp;컴퓨터과학&amp;nbsp;학사&amp;nbsp;졸업을&amp;nbsp;했다. &lt;br&gt;&lt;br&gt;22년 Chat GPT을 시작으로 다양한 AI 도구가 등장했고, 이제는&amp;nbsp;AI가&amp;nbsp;인간의&amp;nbsp;일자리를&amp;nbsp;대체하고&amp;nbsp;있다는&amp;nbsp;이야기로&amp;nbsp;세간의&amp;nbsp;개발자,&amp;nbsp;직장인들은&amp;nbsp;두려움에&amp;nbsp;떨고있다. &lt;br&gt;나&amp;nbsp;또한&amp;nbsp;언젠가&amp;nbsp;AI에게&amp;nbsp;대체될&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;인력이고.. &lt;br&gt;여기서&amp;nbsp;나만의&amp;nbsp;무기를&amp;nbsp;가지려면&amp;nbsp;어떤&amp;nbsp;것을&amp;nbsp;해야할까&amp;nbsp;고민하던&amp;nbsp;중&amp;nbsp;소프트웨어에&amp;nbsp;대해서&amp;nbsp;더욱&amp;nbsp;깊게&amp;nbsp;배워보고&amp;nbsp;싶었다. &lt;br&gt;&lt;br&gt;&lt;b&gt;결국&amp;nbsp;AI도&amp;nbsp;소프트웨어&amp;nbsp;아닌가?&lt;/b&gt;&lt;br&gt;&lt;br&gt;소프트웨어를 설계할 줄 알고 생애주기 전반을 관리할 줄 아는 사람으로 성장한다면 AI 물결속에도 내 자리를 굳건하게 만들 수 있지 않을까 라는 생각이었다. &lt;br&gt;&lt;br&gt;그렇게&amp;nbsp;숭실대학교&amp;nbsp;정보과학대학원&amp;nbsp;소프트웨어공학과에&amp;nbsp;25년&amp;nbsp;후기에&amp;nbsp;입학하게된다. &lt;br&gt;&lt;br&gt;첫&amp;nbsp;학기로&amp;nbsp;빅데이터베이스와&amp;nbsp;강화학습을&amp;nbsp;들었다. &lt;br&gt;그리고&amp;nbsp;12월&amp;nbsp;둘째&amp;nbsp;주,&amp;nbsp;기말고사로&amp;nbsp;학기를&amp;nbsp;종강했다. &lt;br&gt;&lt;br&gt;짧은&amp;nbsp;후기를&amp;nbsp;남기자면&amp;nbsp;생각보다&amp;nbsp;만족스러웠다는&amp;nbsp;점이다. &lt;br&gt;컴퓨터과학을&amp;nbsp;전공했지만&amp;nbsp;AI나&amp;nbsp;머신러닝에&amp;nbsp;대해서는&amp;nbsp;배웠던&amp;nbsp;것이&amp;nbsp;없었다. &lt;br&gt;그래서&amp;nbsp;강화학습이라는&amp;nbsp;수업을&amp;nbsp;처음&amp;nbsp;들었을&amp;nbsp;땐&amp;nbsp;무슨&amp;nbsp;말씀인지&amp;nbsp;하나도&amp;nbsp;몰랐다. &lt;br&gt;토론식&amp;nbsp;수업인지라&amp;nbsp;교수님이&amp;nbsp;제작한&amp;nbsp;온라인&amp;nbsp;강의를&amp;nbsp;미리&amp;nbsp;들어오고,&amp;nbsp;수업에서는&amp;nbsp;질문&amp;nbsp;주제를&amp;nbsp;뽑아서&amp;nbsp;타&amp;nbsp;팀이&amp;nbsp;우리&amp;nbsp;팀의&amp;nbsp;질문에&amp;nbsp;대답을&amp;nbsp;하는&amp;nbsp;방식으로&amp;nbsp;수업이&amp;nbsp;진행됐다.&amp;nbsp;&lt;br&gt;교수님의&amp;nbsp;설명과&amp;nbsp;함께. &lt;br&gt;수업을 들으며 매 수업마다 최선을 다했고 매 주말 진도 나갈 내용을 공부하며 강의를 듣고, 수업을 들으며 수업을 최대한 따라가려고 집중했다. &lt;br&gt;매 수업 결석하지 않고 첫 수업부터 끝 수업까지 열심히 들었다.&lt;br&gt;수업을&amp;nbsp;해주시는&amp;nbsp;교수님도&amp;nbsp;정말&amp;nbsp;존경스러웠고,&amp;nbsp;강의&amp;nbsp;내용도&amp;nbsp;흥미가&amp;nbsp;있었기&amp;nbsp;때문이다. &lt;br&gt;&lt;br&gt;이번 학기의 성적은 아주 잘 받았다.&lt;br&gt;두 과목 모두 A+를 받았고, 첫&amp;nbsp;학기&amp;nbsp;무사히&amp;nbsp;마쳤다는게&amp;nbsp;조금은&amp;nbsp;자랑스러운&amp;nbsp;것&amp;nbsp;같다. &lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drS6KS/dJMcaa4YRLT/sVXT3xY5QLNLVPzNf9NI1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drS6KS/dJMcaa4YRLT/sVXT3xY5QLNLVPzNf9NI1K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drS6KS/dJMcaa4YRLT/sVXT3xY5QLNLVPzNf9NI1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrS6KS%2FdJMcaa4YRLT%2FsVXT3xY5QLNLVPzNf9NI1K%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;1115&quot; height=&quot;498&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;br&gt;석사&amp;nbsp;과정의&amp;nbsp;목표는&amp;nbsp;두&amp;nbsp;가지가&amp;nbsp;있다.&amp;nbsp;누구나&amp;nbsp;다&amp;nbsp;마찬가지겠지만.. &lt;br&gt;&lt;br&gt;&lt;b&gt;1.&amp;nbsp;학점&amp;nbsp;잘&amp;nbsp;챙기기&lt;/b&gt; &lt;br&gt;&lt;b&gt;2.&amp;nbsp;석사&amp;nbsp;논문&amp;nbsp;잘&amp;nbsp;써보기&lt;/b&gt; &lt;br&gt;&lt;br&gt;아직&amp;nbsp;첫&amp;nbsp;학기인지라&amp;nbsp;많은&amp;nbsp;것이&amp;nbsp;어설펐지만,&amp;nbsp;다음&amp;nbsp;학기에는&amp;nbsp;더&amp;nbsp;발전된&amp;nbsp;대학원생으로&amp;nbsp;학교&amp;nbsp;생활을&amp;nbsp;즐겨보도록&amp;nbsp;하겠다.&amp;nbsp;(과연?)&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br&gt;&lt;b&gt;업무&lt;/b&gt;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;현 회사&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;25년은&amp;nbsp;선임&amp;nbsp;연구원으로&amp;nbsp;승진한&amp;nbsp;해이다. &lt;br&gt;작년&amp;nbsp;인사&amp;nbsp;고과&amp;nbsp;또한&amp;nbsp;A로&amp;nbsp;좋은&amp;nbsp;평가를&amp;nbsp;받은&amp;nbsp;해였다. &lt;br&gt;선임&amp;nbsp;연구원으로&amp;nbsp;승진한&amp;nbsp;만큼&amp;nbsp;더욱&amp;nbsp;열심히&amp;nbsp;일을&amp;nbsp;해야겠다라는&amp;nbsp;생각을&amp;nbsp;했었고&amp;nbsp;후배들에게&amp;nbsp;좋은&amp;nbsp;영향력을&amp;nbsp;주려고,&amp;nbsp;업무에&amp;nbsp;있어서&amp;nbsp;긍정적인&amp;nbsp;영향력을&amp;nbsp;미치고자&amp;nbsp;노력을&amp;nbsp;했던&amp;nbsp;것&amp;nbsp;같다. &lt;br&gt;&lt;br&gt;가장&amp;nbsp;기억에&amp;nbsp;남는&amp;nbsp;업무는&amp;nbsp;두&amp;nbsp;가지다. &lt;br&gt;&lt;br&gt;첫&amp;nbsp;번째&amp;nbsp;업무는&amp;nbsp;연구센터&amp;nbsp;내&amp;nbsp;&lt;b&gt;라이선스&amp;nbsp;시스템&amp;nbsp;도입&lt;/b&gt;&amp;nbsp;업무이다.&amp;nbsp;&lt;br&gt;현재&amp;nbsp;센터&amp;nbsp;내&amp;nbsp;솔루션을&amp;nbsp;고객사에&amp;nbsp;납품할&amp;nbsp;때&amp;nbsp;라이선스&amp;nbsp;체계&amp;nbsp;없이&amp;nbsp;수작업으로&amp;nbsp;라이선스를&amp;nbsp;발급해&amp;nbsp;고객사에&amp;nbsp;납품하고는&amp;nbsp;했다. &lt;br&gt;이런&amp;nbsp;업무를&amp;nbsp;자동화&amp;nbsp;하고,&amp;nbsp;라이선스&amp;nbsp;유무에&amp;nbsp;따라&amp;nbsp;솔루션을&amp;nbsp;블로킹&amp;nbsp;하기&amp;nbsp;위해&amp;nbsp;라이선스&amp;nbsp;체계&amp;nbsp;도입&amp;nbsp;업무를&amp;nbsp;리딩했다. &lt;br&gt;Jira와&amp;nbsp;Confluence를&amp;nbsp;활용해&amp;nbsp;프로젝트를&amp;nbsp;계획하고,&amp;nbsp;주&amp;nbsp;단위&amp;nbsp;스프린트를&amp;nbsp;통해&amp;nbsp;일을&amp;nbsp;계획적으로&amp;nbsp;진행하려고&amp;nbsp;노력했다. &lt;br&gt;첫&amp;nbsp;스프린트는&amp;nbsp;팀원&amp;nbsp;간&amp;nbsp;합을&amp;nbsp;맞추기&amp;nbsp;위한&amp;nbsp;스프린트라고&amp;nbsp;생각했고,&amp;nbsp;매&amp;nbsp;주&amp;nbsp;금요일마다&amp;nbsp;회고와&amp;nbsp;스토리&amp;nbsp;포인트&amp;nbsp;기반&amp;nbsp;플래닝을&amp;nbsp;통해&amp;nbsp;일을&amp;nbsp;정확히&amp;nbsp;계획하려고&amp;nbsp;노력했으며,&amp;nbsp;그&amp;nbsp;결과&amp;nbsp;스프린트&amp;nbsp;이슈&amp;nbsp;커버율&amp;nbsp;100%를&amp;nbsp;달성하게&amp;nbsp;됐다. &lt;br&gt;내가&amp;nbsp;리딩한&amp;nbsp;라이선스&amp;nbsp;체계&amp;nbsp;구축&amp;nbsp;업무로&amp;nbsp;인해&amp;nbsp;영업팀에서&amp;nbsp;수작업으로&amp;nbsp;했던&amp;nbsp;업무들을&amp;nbsp;어느정도&amp;nbsp;자동화&amp;nbsp;했고,&amp;nbsp;솔루션&amp;nbsp;영업에&amp;nbsp;있어서도&amp;nbsp;기한이&amp;nbsp;지난&amp;nbsp;솔루션을&amp;nbsp;사용하지&amp;nbsp;못하게&amp;nbsp;하는&amp;nbsp;근거를&amp;nbsp;만들어&amp;nbsp;줬다고&amp;nbsp;생각하여&amp;nbsp;많은&amp;nbsp;기여를&amp;nbsp;했다고&amp;nbsp;생각이&amp;nbsp;들었다. &lt;br&gt;&lt;br&gt;두 번째 업무는 &lt;b&gt;마이그레이션 업무&lt;/b&gt;였다.&lt;br&gt;미리&amp;nbsp;말하지만&amp;nbsp;돌아오는&amp;nbsp;주&amp;nbsp;금요일인&amp;nbsp;19일에&amp;nbsp;퇴사를&amp;nbsp;하게&amp;nbsp;되었다.&amp;nbsp;퇴사&amp;nbsp;사유는&amp;nbsp;이직. &lt;br&gt;이직하기&amp;nbsp;전&amp;nbsp;진행했던&amp;nbsp;feature(기능)&amp;nbsp;업무가&amp;nbsp;있었는데&amp;nbsp;VMWare의&amp;nbsp;VM을&amp;nbsp;Openstack의&amp;nbsp;VM으로&amp;nbsp;마이그레이션&amp;nbsp;하는&amp;nbsp;기능을&amp;nbsp;개발하는&amp;nbsp;것&amp;nbsp;이었다. &lt;br&gt;사내&amp;nbsp;솔루션의&amp;nbsp;디자인은&amp;nbsp;현재&amp;nbsp;ONE-UI&amp;nbsp;2.0이라는&amp;nbsp;디자인&amp;nbsp;시스템이&amp;nbsp;적용된&amp;nbsp;UI/UX로&amp;nbsp;개발이&amp;nbsp;진행되고&amp;nbsp;있지만,&amp;nbsp;우리&amp;nbsp;솔루션에는&amp;nbsp;아직&amp;nbsp;ONE-UI&amp;nbsp;2.0이&amp;nbsp;도입된&amp;nbsp;상태가&amp;nbsp;아니였다. &lt;br&gt;디자인/기획&amp;nbsp;팀에서&amp;nbsp;산출물&amp;nbsp;작성&amp;nbsp;완료&amp;nbsp;후&amp;nbsp;확인&amp;nbsp;미팅에서&amp;nbsp;ONE-UI&amp;nbsp;2.0이&amp;nbsp;적용된&amp;nbsp;디자인을&amp;nbsp;확인했고,&amp;nbsp;지금&amp;nbsp;당장&amp;nbsp;우리&amp;nbsp;솔루션에는&amp;nbsp;적용이&amp;nbsp;어렵다&amp;nbsp;말씀을&amp;nbsp;드렸다.&amp;nbsp;(공수가&amp;nbsp;어마어마&amp;nbsp;하기&amp;nbsp;때문이다.) &lt;br&gt;하지만&amp;nbsp;우리팀의&amp;nbsp;레포는&amp;nbsp;모노레포로&amp;nbsp;되어있었고,&amp;nbsp;솔루션&amp;nbsp;내&amp;nbsp;기능이&amp;nbsp;구현되지&amp;nbsp;않고&amp;nbsp;다른&amp;nbsp;포트를&amp;nbsp;통해&amp;nbsp;기능을&amp;nbsp;구현해도&amp;nbsp;되었기&amp;nbsp;때문에&amp;nbsp;모노레포&amp;nbsp;내&amp;nbsp;프로젝트를&amp;nbsp;새로&amp;nbsp;만들어&amp;nbsp;해당&amp;nbsp;프로젝트로&amp;nbsp;기능을&amp;nbsp;구현하겠다고&amp;nbsp;의견을&amp;nbsp;드렸다. &lt;br&gt;그렇게&amp;nbsp;디자인/기획&amp;nbsp;팀과&amp;nbsp;협의를&amp;nbsp;완료한&amp;nbsp;후&amp;nbsp;2주라는&amp;nbsp;시간동안&amp;nbsp;프로젝트&amp;nbsp;초기&amp;nbsp;셋팅부터&amp;nbsp;기능&amp;nbsp;구현까지&amp;nbsp;구현을&amp;nbsp;마쳤다. &lt;br&gt;2주동안&amp;nbsp;백앤드&amp;nbsp;개발자,&amp;nbsp;프론트엔드&amp;nbsp;개발자,&amp;nbsp;디자이너,&amp;nbsp;기획자,&amp;nbsp;센터장님&amp;nbsp;등&amp;nbsp;많은&amp;nbsp;사람들과&amp;nbsp;협업을&amp;nbsp;했고,&amp;nbsp;이&amp;nbsp;과정에서&amp;nbsp;회사에서&amp;nbsp;근무하면서&amp;nbsp;가장&amp;nbsp;일다운&amp;nbsp;일을&amp;nbsp;했다는&amp;nbsp;생각이&amp;nbsp;들었다. &lt;br&gt;&lt;br&gt;올해 큼직한 업무 두 건을 끝마쳤고, 업무 성과도 괜찮은 한 해 였다고 생각이 든다. &lt;br&gt;&lt;br&gt;내년에는&amp;nbsp;새로운&amp;nbsp;팀에서&amp;nbsp;새로운&amp;nbsp;도전을&amp;nbsp;할텐데&amp;nbsp;잘&amp;nbsp;적응하여&amp;nbsp;많은&amp;nbsp;성과를&amp;nbsp;내고&amp;nbsp;싶다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사이드&amp;nbsp;프로젝트&amp;nbsp;ssalon-de&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;와이프를 위해 사이드 프로젝트를 진행했다.&lt;br&gt;와이프는 미용사인데 매출을 액셀로 관리했고, 입력하는&amp;nbsp;매출은&amp;nbsp;한달&amp;nbsp;총&amp;nbsp;매출을&amp;nbsp;관리하기&amp;nbsp;위해&amp;nbsp;사용하는&amp;nbsp;데이터였다. &lt;br&gt;나는&amp;nbsp;그&amp;nbsp;데이터를&amp;nbsp;쉽게&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있고,&amp;nbsp;그&amp;nbsp;데이터를&amp;nbsp;통해&amp;nbsp;인사이트를&amp;nbsp;준다면&amp;nbsp;매출&amp;nbsp;관리에&amp;nbsp;더욱&amp;nbsp;효율적이지&amp;nbsp;않을까&amp;nbsp;라는&amp;nbsp;생각을&amp;nbsp;했다. &lt;br&gt;그렇게&amp;nbsp;ssalon-de라는&amp;nbsp;사이드&amp;nbsp;프로젝트를&amp;nbsp;시작했고,&amp;nbsp;약&amp;nbsp;한달&amp;nbsp;만에&amp;nbsp;프론트엔드,&amp;nbsp;서버,&amp;nbsp;인프라를&amp;nbsp;모두&amp;nbsp;구축하게&amp;nbsp;됐다. &lt;br&gt;&lt;br&gt;그렇게&amp;nbsp;운영하는&amp;nbsp;도중&amp;nbsp;10월에&amp;nbsp;쉽지&amp;nbsp;않은&amp;nbsp;일을&amp;nbsp;마주쳤다.&lt;br&gt;&lt;br&gt;9월에 공격을 당한것인지 LB와 VPC에 어림도 없는 요금이 부여된 것이다. &lt;br&gt;평상시&amp;nbsp;1만&amp;nbsp;2천원이&amp;nbsp;나온&amp;nbsp;요금이&amp;nbsp;갑자기&amp;nbsp;12만원이&amp;nbsp;나오는&amp;nbsp;슬픈&amp;nbsp;상황이였다.. &lt;br&gt;그때&amp;nbsp;당시&amp;nbsp;서버&amp;nbsp;모니터링을&amp;nbsp;전혀&amp;nbsp;하지&amp;nbsp;않았고,&amp;nbsp;요금&amp;nbsp;또한&amp;nbsp;용돈으로&amp;nbsp;내고&amp;nbsp;있던&amp;nbsp;상황인지라..&amp;nbsp;매우&amp;nbsp;뼈&amp;nbsp;아픈&amp;nbsp;상황이였다. &lt;br&gt;그럼에도 공격을 당한건 내 잘못이다.&lt;br&gt;설마 공격을 하겠어? 라는 안일한 생각으로 WAF를 붙여놓지도 않았고, 서버를&amp;nbsp;모니터링&amp;nbsp;하지도&amp;nbsp;않았다.&lt;br&gt;&lt;br&gt;그&amp;nbsp;일이&amp;nbsp;있고난&amp;nbsp;후&amp;nbsp;ALB에&amp;nbsp;WAF를&amp;nbsp;붙여놨고,&amp;nbsp;2달&amp;nbsp;정도&amp;nbsp;운영을&amp;nbsp;하다가&amp;nbsp;와이프가&amp;nbsp;더&amp;nbsp;이상&amp;nbsp;사용하지&amp;nbsp;않는다고&amp;nbsp;하여&amp;nbsp;서버를&amp;nbsp;다&amp;nbsp;내려놓게&amp;nbsp;됐다. &lt;br&gt;&lt;br&gt;사이드&amp;nbsp;프로젝트를&amp;nbsp;약&amp;nbsp;1년간&amp;nbsp;운영하며&amp;nbsp;여러가지&amp;nbsp;일들이&amp;nbsp;있었다. &lt;br&gt;여전히 제품을 만들고 배포하고 사용자의 피드백을 받아가며 제품을 개선하는 과정은 즐겁지만, 사용자가&amp;nbsp;제품을&amp;nbsp;사용하게&amp;nbsp;끔&amp;nbsp;마켓팅을&amp;nbsp;하거나&amp;nbsp;알리는&amp;nbsp;것은&amp;nbsp;어려운&amp;nbsp;일이라는&amp;nbsp;것을&amp;nbsp;느꼈다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;퇴사&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;회사 이야기를 하면서 남겨놨듯 19일에 퇴사하게 되었다.&lt;br&gt;이유는&amp;nbsp;이직이다. &lt;br&gt;현&amp;nbsp;회사에서&amp;nbsp;1년&amp;nbsp;9개월을&amp;nbsp;근무했고,&amp;nbsp;짧다면&amp;nbsp;짧고&amp;nbsp;길다면&amp;nbsp;긴&amp;nbsp;시간을&amp;nbsp;보냈다. &lt;br&gt;클라우드 IaaS 솔루션을 개발하면서 많은 배움이 있었고, 좋은 팀원과 즐거운 시간을 보내며 제품을 개발했지만 스스로&amp;nbsp;발전을&amp;nbsp;하기&amp;nbsp;위해서는&amp;nbsp;팀을&amp;nbsp;떠나&amp;nbsp;더&amp;nbsp;큰&amp;nbsp;팀으로&amp;nbsp;이동&amp;nbsp;해야한다는&amp;nbsp;생각이&amp;nbsp;들었다. &lt;br&gt;&lt;br&gt;첫&amp;nbsp;번째&amp;nbsp;이유는&amp;nbsp;&lt;b&gt;팀장의&amp;nbsp;부재&lt;/b&gt;였다.&amp;nbsp;&lt;br&gt;작년&amp;nbsp;8월&amp;nbsp;이후로&amp;nbsp;팀&amp;nbsp;내&amp;nbsp;팀장이&amp;nbsp;공석인&amp;nbsp;상황이었다.&amp;nbsp;&lt;br&gt;1&amp;nbsp;on&amp;nbsp;1은&amp;nbsp;당연히&amp;nbsp;기대할&amp;nbsp;수&amp;nbsp;없었고,&amp;nbsp;팀장이&amp;nbsp;없었기&amp;nbsp;때문에&amp;nbsp;업무의&amp;nbsp;교통&amp;nbsp;정리가&amp;nbsp;잘&amp;nbsp;되지&amp;nbsp;않는다고&amp;nbsp;생각했다. &lt;br&gt;이&amp;nbsp;과정속에서&amp;nbsp;팀의&amp;nbsp;일이&amp;nbsp;잘&amp;nbsp;진행되기&amp;nbsp;위해&amp;nbsp;타&amp;nbsp;팀과의&amp;nbsp;커뮤니케이션으로&amp;nbsp;적극적으로&amp;nbsp;하기도,&amp;nbsp;릴리즈를&amp;nbsp;주도적으로&amp;nbsp;해보기도&amp;nbsp;했다.&amp;nbsp;오죽했으면&amp;nbsp;새로&amp;nbsp;입사하신&amp;nbsp;팀원분께서&amp;nbsp;PM&amp;nbsp;역할을&amp;nbsp;하고&amp;nbsp;있냐고&amp;nbsp;물어볼&amp;nbsp;정도였다. &lt;br&gt;이러한&amp;nbsp;시간들이&amp;nbsp;오래&amp;nbsp;지속되다&amp;nbsp;보니깐&amp;nbsp;나도&amp;nbsp;지친&amp;nbsp;모양이다.&amp;nbsp;&lt;br&gt;아직은&amp;nbsp;주니어&amp;nbsp;개발자로써&amp;nbsp;내가&amp;nbsp;하고&amp;nbsp;있는&amp;nbsp;일에서&amp;nbsp;성장해보고&amp;nbsp;싶기도&amp;nbsp;했고,&amp;nbsp;피드백을&amp;nbsp;받아보고&amp;nbsp;싶기도&amp;nbsp;했다.&amp;nbsp;&lt;br&gt;또&amp;nbsp;큰&amp;nbsp;팀에서&amp;nbsp;많은&amp;nbsp;역할을&amp;nbsp;맡아보며&amp;nbsp;내가&amp;nbsp;꿈꾸는&amp;nbsp;개발자로&amp;nbsp;성장하고&amp;nbsp;싶은&amp;nbsp;욕망도&amp;nbsp;있었다. &lt;br&gt;&lt;br&gt;두&amp;nbsp;번째&amp;nbsp;이유는&amp;nbsp;&lt;b&gt;퍼블릭한&amp;nbsp;서비스&lt;/b&gt;이다.&amp;nbsp;&lt;br&gt;이번에&amp;nbsp;합류할&amp;nbsp;팀은&amp;nbsp;CSP사로서&amp;nbsp;퍼블릭&amp;nbsp;클라우드를&amp;nbsp;제공하고&amp;nbsp;있는&amp;nbsp;회사이다. &lt;br&gt;현&amp;nbsp;회사에서&amp;nbsp;프라이빗&amp;nbsp;클라우드&amp;nbsp;제품을&amp;nbsp;개발했는데,&amp;nbsp;프라이빗&amp;nbsp;클라우드의&amp;nbsp;특성&amp;nbsp;상&amp;nbsp;프론트엔드&amp;nbsp;개발자의&amp;nbsp;비중이&amp;nbsp;적었다.&lt;br&gt;또 유저의 피드백이 없어서 제품을 개선하기 어려웠고, 유저의 사용성을 상상에 의존하여 제품을 개선할 수 밖에 없었다. &lt;br&gt;따라서&amp;nbsp;퍼블릭한&amp;nbsp;서비스에&amp;nbsp;기여를&amp;nbsp;해보고&amp;nbsp;싶었고,&amp;nbsp;클라우드에&amp;nbsp;관심이&amp;nbsp;있고&amp;nbsp;클라우드&amp;nbsp;생태계에&amp;nbsp;기여하고&amp;nbsp;싶다는&amp;nbsp;생각으로&amp;nbsp;CSP사로&amp;nbsp;이직을&amp;nbsp;결정하게&amp;nbsp;됐다. &lt;br&gt;&lt;br&gt;현&amp;nbsp;회사에서&amp;nbsp;힘든&amp;nbsp;일도&amp;nbsp;많았지만,&amp;nbsp;힘든&amp;nbsp;일&amp;nbsp;속에서&amp;nbsp;많은&amp;nbsp;배움이&amp;nbsp;있었다고&amp;nbsp;생각한다. &lt;br&gt;&lt;br&gt;첫&amp;nbsp;번째로&amp;nbsp;&lt;b&gt;리딩의&amp;nbsp;경험&lt;/b&gt;이다.&amp;nbsp;&lt;br&gt;팀&amp;nbsp;내&amp;nbsp;공식적으로&amp;nbsp;파트장&amp;nbsp;이라는&amp;nbsp;직책은&amp;nbsp;없지만,&amp;nbsp;프론트-백으로&amp;nbsp;나뉘어진&amp;nbsp;개발자&amp;nbsp;속에서&amp;nbsp;프론트엔드&amp;nbsp;파트를&amp;nbsp;리딩을&amp;nbsp;했다.&amp;nbsp;&lt;br&gt;리딩을&amp;nbsp;하며&amp;nbsp;더욱&amp;nbsp;주도적으로&amp;nbsp;일을&amp;nbsp;해보기도,&amp;nbsp;팀원에게&amp;nbsp;일을&amp;nbsp;할당해보기도,&amp;nbsp;팀원의&amp;nbsp;마음을&amp;nbsp;이해하기도&amp;nbsp;했었다.&amp;nbsp; &lt;br&gt;리딩은&amp;nbsp;무척이나&amp;nbsp;어려운&amp;nbsp;일이다.&amp;nbsp;&lt;br&gt;팀원은&amp;nbsp;단순히&amp;nbsp;내&amp;nbsp;일만&amp;nbsp;잘하면&amp;nbsp;되지만,&amp;nbsp;리더는&amp;nbsp;내&amp;nbsp;일&amp;nbsp;뿐만&amp;nbsp;아니라&amp;nbsp;팀의&amp;nbsp;일도&amp;nbsp;잘&amp;nbsp;되도록&amp;nbsp;노력해야&amp;nbsp;한다.&amp;nbsp;&lt;br&gt;이&amp;nbsp;속에서&amp;nbsp;책임감이라는&amp;nbsp;무거운&amp;nbsp;단어를&amp;nbsp;배우게&amp;nbsp;됐고,&amp;nbsp;커뮤니케이션이라는&amp;nbsp;즐거운&amp;nbsp;단어도&amp;nbsp;배우게&amp;nbsp;됐다. &lt;br&gt;새로운&amp;nbsp;팀에서는&amp;nbsp;다시&amp;nbsp;팀원의&amp;nbsp;입장으로&amp;nbsp;돌아간다.&amp;nbsp;&lt;br&gt;팀원이지만&amp;nbsp;내&amp;nbsp;일만&amp;nbsp;잘하는&amp;nbsp;것이&amp;nbsp;아닌,&amp;nbsp;팀의&amp;nbsp;일까지&amp;nbsp;잘&amp;nbsp;챙기는&amp;nbsp;팀원으로&amp;nbsp;근무하고&amp;nbsp;싶다. &lt;br&gt;&lt;br&gt;두&amp;nbsp;번째는&amp;nbsp;&lt;b&gt;일&amp;nbsp;속에서의&amp;nbsp;매너&lt;/b&gt;이다. &lt;br&gt;지난&amp;nbsp;7월&amp;nbsp;엔지니어&amp;nbsp;팀에서의&amp;nbsp;메세지를&amp;nbsp;받았다.&amp;nbsp;&lt;br&gt;고객사에&amp;nbsp;PoC를&amp;nbsp;나갔는데&amp;nbsp;기존에&amp;nbsp;작동하던&amp;nbsp;기능이&amp;nbsp;동작하지&amp;nbsp;않는다는&amp;nbsp;것이다. &lt;br&gt;책임감을&amp;nbsp;가지고&amp;nbsp;일을&amp;nbsp;하라는&amp;nbsp;이야기를&amp;nbsp;들었고,&amp;nbsp;창피하다는&amp;nbsp;이야기를&amp;nbsp;들었다. &lt;br&gt;못이&amp;nbsp;박힌&amp;nbsp;텍스트를&amp;nbsp;공식&amp;nbsp;채팅방을&amp;nbsp;통해&amp;nbsp;팀대&amp;nbsp;팀으로&amp;nbsp;전달&amp;nbsp;받은&amp;nbsp;후,&amp;nbsp;우리&amp;nbsp;팀은&amp;nbsp;많은&amp;nbsp;상처를&amp;nbsp;받게&amp;nbsp;됐다. &lt;br&gt;사실&amp;nbsp;핑계아닌&amp;nbsp;핑계지만,&amp;nbsp;현&amp;nbsp;우리팀은&amp;nbsp;사실&amp;nbsp;히스토리가&amp;nbsp;끊긴&amp;nbsp;상황이다.&amp;nbsp;&lt;br&gt;작년&amp;nbsp;폭풍우&amp;nbsp;속&amp;nbsp;많은&amp;nbsp;팀원이&amp;nbsp;퇴사하게&amp;nbsp;됐고,&amp;nbsp;어려운&amp;nbsp;상황속에서&amp;nbsp;어떻게든&amp;nbsp;일을&amp;nbsp;해나가고자&amp;nbsp;하는&amp;nbsp;우리&amp;nbsp;팀은&amp;nbsp;그래도&amp;nbsp;결속된&amp;nbsp;상황이었다. &lt;br&gt;이러한&amp;nbsp;상황속에서&amp;nbsp;저런&amp;nbsp;이야기를&amp;nbsp;들었고,&amp;nbsp;윗&amp;nbsp;사람들의&amp;nbsp;중재로&amp;nbsp;인해&amp;nbsp;상황은&amp;nbsp;마무리&amp;nbsp;됐지만,&amp;nbsp;타인과&amp;nbsp;커뮤니케이션을&amp;nbsp;할&amp;nbsp;때&amp;nbsp;작은&amp;nbsp;매너&amp;nbsp;속에서&amp;nbsp;협업이&amp;nbsp;피우는구나&amp;nbsp;라는&amp;nbsp;것을&amp;nbsp;배운&amp;nbsp;것&amp;nbsp;같다. &lt;br&gt;당연히&amp;nbsp;일을&amp;nbsp;하면&amp;nbsp;힘든&amp;nbsp;것들이&amp;nbsp;있다.&amp;nbsp;&lt;br&gt;그래도, 아무리 일이 힘들어도, 타인과 커뮤니케이션을 하는 상황이라면 내 감정보다는 타인의 감정을 더 소중하게 생각해야 하고 그 속에서 작은 매너도 챙겨야 한다는 것을 배운 것 같다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;아쉬웠던 점&lt;/b&gt;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;완주&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;업무에 있어서 아쉬웠던 점은 일의 완성도 측면이다.&lt;br&gt;프론트엔드 파트 내에서 개선해야 하는 점이나 고도화가 필요해 리팩토링을 진행할 때, 윗분들께서 이 일을 왜 해야하는지 이해를 못하셨고, 설득을 위해 리뷰할 수 있는 자료와 시간을 마련했지만 일정이 있다는 이유로 항상 미루기에 급급했었다. 그러다보니 업무를 100% 하지 못했는데 계속해서 다른 업무가 치소 들어오는 상황에 마주했고 스택에 데이터가 쌓이듯 이전에 했던 업무들을 완성도 있게 쳐내지 못했다.&lt;br&gt;물론 중간중간 업무가 pop 되면 기존에 쌓여있는 업무를 쳐내며 하나씩 해결하긴 했지만, 끌고가는 업무를 집중하며 끝내지 못했던 순간들이 아쉬웠다.&lt;br&gt;그럼에도 이런 상황에 다시 마주한다면, 매니저를&lt;br&gt;설득할 수 있는 능력을 키워야겠다는 생각이 들었다. 시간이 없으시면 끝까지 찾아가서 설득하고, 내 업무는 내가 챙기고, 내가 파트를 리드하는 상황이라면 더 많이 다가가야 하지 않을까 라는 생각이다&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;건강&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;올해 건강이 많이 나빠졌다. 10월부터 11월, 약 두 달 만에 살이 10kg가 빠졌다. 원인은 갑상선 기능 항진증이다.&lt;br&gt;두 달 동안 스트레스를 많이 받았다. 이직, 업무, 학교 등 여러가지 상황이 복합적으로 놓이다보니 정작 제일 중요한 내 건강을 챙기지 못했다.&lt;br&gt;다행히도 진행했던 모든 일들은 성공적이었고 상황이 모두 지나고 약을 먹으며 건강이 어느정도 회복된 상황이다.&lt;br&gt;스트레스를 잘 관리할 줄 안다고 생각했는데 그러질 못했고 그로 인해 많이 힘든 상황에 놓여졌었다.&lt;br&gt;내년에는 나를 조금 더 챙겨야겠다는 생각이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;내년 ACTION ITEMS&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;내년의&amp;nbsp;액션&amp;nbsp;아이템을&amp;nbsp;큰&amp;nbsp;틀에서&amp;nbsp;생각해보면&amp;nbsp;다음과&amp;nbsp;같다.&lt;br&gt;&lt;br&gt;1.&amp;nbsp;2,&amp;nbsp;3학기&amp;nbsp;진행 &lt;br&gt;2.&amp;nbsp;회사&amp;nbsp;적응&amp;nbsp;및&amp;nbsp;기여 &lt;br&gt;3.&amp;nbsp;최소&amp;nbsp;매달&amp;nbsp;블로그&amp;nbsp;글&amp;nbsp;하나&amp;nbsp;작성 &lt;br&gt;4.&amp;nbsp;석사&amp;nbsp;논문&amp;nbsp;Item&amp;nbsp;선정 &lt;br&gt;5.&amp;nbsp;(Option)&amp;nbsp;자격증&amp;nbsp;취득&amp;nbsp;-&amp;nbsp;Linux&amp;nbsp;Master&amp;nbsp;2급 &lt;br&gt;&lt;br&gt;새로운&amp;nbsp;직장에서&amp;nbsp;적응도&amp;nbsp;해야하고,&amp;nbsp;대학원도&amp;nbsp;2학기&amp;nbsp;3학기를&amp;nbsp;진행해야&amp;nbsp;하기&amp;nbsp;때문에&amp;nbsp;더&amp;nbsp;많은&amp;nbsp;일을&amp;nbsp;하기는&amp;nbsp;어렵다고&amp;nbsp;생각한다. &lt;br&gt;그래도&amp;nbsp;매달&amp;nbsp;블로그&amp;nbsp;글을&amp;nbsp;하나씩&amp;nbsp;작성해보고,&amp;nbsp;석사&amp;nbsp;논문&amp;nbsp;Item도&amp;nbsp;고민해보는&amp;nbsp;것은&amp;nbsp;꼭&amp;nbsp;진행해보도록&amp;nbsp;하겠다. &lt;br&gt;&lt;br&gt;내년에&amp;nbsp;대해&amp;nbsp;너무&amp;nbsp;짧은&amp;nbsp;이야기일지?&lt;br&gt;&lt;br&gt;위에&amp;nbsp;대한&amp;nbsp;이야기는&amp;nbsp;내년&amp;nbsp;상반기에&amp;nbsp;회고를&amp;nbsp;하면서&amp;nbsp;곱씹어&amp;nbsp;보도록&amp;nbsp;하겠다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;뭐&amp;nbsp;이런&amp;nbsp;저런&amp;nbsp;상황들이&amp;nbsp;있었고,&amp;nbsp;이런&amp;nbsp;저런&amp;nbsp;이유로&amp;nbsp;이직을&amp;nbsp;결정하게&amp;nbsp;됐다. &lt;br&gt;하반기에&amp;nbsp;대학원을&amp;nbsp;다니고,&amp;nbsp;이직&amp;nbsp;프로세스를&amp;nbsp;진행하고,&amp;nbsp;업무&amp;nbsp;속에서&amp;nbsp;스트레스를&amp;nbsp;받다&amp;nbsp;보니&amp;nbsp;건강이&amp;nbsp;많이&amp;nbsp;나빠졌다. &lt;br&gt;9월에&amp;nbsp;78kg였던&amp;nbsp;내가&amp;nbsp;11월에&amp;nbsp;68kg로&amp;nbsp;살이&amp;nbsp;10kg가&amp;nbsp;빠져서&amp;nbsp;병원&amp;nbsp;검사를&amp;nbsp;받게&amp;nbsp;됐고, &lt;br&gt;갑상선&amp;nbsp;기능&amp;nbsp;항진증이라는&amp;nbsp;진단을&amp;nbsp;받았다. &lt;br&gt;약을&amp;nbsp;먹으며&amp;nbsp;스트레스&amp;nbsp;관리를&amp;nbsp;잘&amp;nbsp;하면&amp;nbsp;좋아진다는&amp;nbsp;말씀을&amp;nbsp;해주셔서&amp;nbsp;열심히&amp;nbsp;관리하고&amp;nbsp;있는&amp;nbsp;상황이다.&amp;nbsp;(그럼에도&amp;nbsp;스트레스&amp;nbsp;관리는&amp;nbsp;어렵다.) &lt;br&gt;내년에는&amp;nbsp;판교로&amp;nbsp;출퇴근을&amp;nbsp;해야&amp;nbsp;하는&amp;nbsp;상황이다.&amp;nbsp;집에서&amp;nbsp;약&amp;nbsp;한시간&amp;nbsp;반&amp;nbsp;정도&amp;nbsp;소요되는데,&amp;nbsp;좀&amp;nbsp;걱정이&amp;nbsp;되기는&amp;nbsp;한다. &lt;br&gt;그래도&amp;nbsp;차도&amp;nbsp;있으니&amp;nbsp;출퇴근이&amp;nbsp;정&amp;nbsp;힘들면&amp;nbsp;차로&amp;nbsp;출퇴근&amp;nbsp;하는&amp;nbsp;방향도&amp;nbsp;검토하는&amp;nbsp;중이다. &lt;br&gt;&lt;br&gt;정말&amp;nbsp;많은&amp;nbsp;일들이&amp;nbsp;있던&amp;nbsp;해였다! &lt;br&gt;&lt;br&gt;내년에도&amp;nbsp;많이&amp;nbsp;힘들겠지만&amp;nbsp;그&amp;nbsp;속에서&amp;nbsp;많은&amp;nbsp;좋은&amp;nbsp;일들이&amp;nbsp;일어났으면&amp;nbsp;하는&amp;nbsp;바램이다. &lt;br&gt;&lt;br&gt;&lt;b&gt;이 글을 읽는 모든 분들에게도 2025년 많이 고생하셨고, 2026년에는 더욱 행복한 일들만 가득하길 빌어본다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/91</guid>
      <comments>https://kangs-develop.tistory.com/91#entry91comment</comments>
      <pubDate>Mon, 15 Dec 2025 16:49:58 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 원시 값과 메서드</title>
      <link>https://kangs-develop.tistory.com/90</link>
      <description>&lt;h2 data-path-to-node=&quot;1&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 &lt;b&gt;원시 값(Primitive Value)&lt;/b&gt;(예: 문자열, 숫자, 불리언)은 객체가 아니며 메서드를 가질 수 없는 &lt;b&gt;불변(Immutable)한&lt;/b&gt; 값입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;하지만 우리는 코드를 작성할 때 &amp;ldquo;hello&amp;rdquo;.toUpperCase()처럼 원시 값의 메서드를 흔하게 사용하고 있습니다. 원시 값은 객체가 아니어서 메서드를 가질 수 없다고 했는데 어떻게 각 타입의 메서드를 사용할 수 있을까요?&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;이는 자바스크립트 엔진이 내부적으로 &lt;b&gt;래퍼 객체(Wrapper Object)&lt;/b&gt;라는 임시 객체를 생성하여 원시 값을 객체처럼 다룰 수 있게 해주는 내부 메커니즘 덕분입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size23&quot;&gt;원시 값 메서드 사용 메커니즘&lt;/h3&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;원시 값에 메서드를 호출하는 코드가 실행될 때, 자바스크립트 엔진은 &lt;b&gt;자동 박싱(Auto-boxing)&lt;/b&gt; 메커니즘을 사용합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;원시 값의 메서드 호출 시도 감지&lt;/b&gt;: 개발자가 원시 값에 마침표(.) 표기법을 사용하여 프로퍼티나 메서드에 접근하려고 시도하는 코드를 엔진이 감지합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764232732111&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const str = &quot;hello&quot;;
str.toUpperCase(); // 엔진이 이 메서드 호출을 감지&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;2. &lt;b&gt;임시 래퍼 객체(Wrapper Object) 생성&lt;/b&gt;: 엔진은 메서드를 호출해야 하는 원시 값의 타입에 상응하는 &lt;b&gt;표준 빌트인 래퍼 객체&lt;/b&gt;( String &lt;span data-math=&quot;\text{String}, \text{Number}, \text{Boolean}&quot;&gt;, Number , Boolean&lt;/span&gt;)를 사용하여 임시 객체를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원시 문자열은 &quot;hello&quot; 에서 new String(&quot;hello&quot;) &amp;nbsp;으로 객체를 생성합니다.&lt;/li&gt;
&lt;li&gt;이 임시 래퍼 객체는 String.prototype 등이 제공하는 모든 메서드를 상속받게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;래퍼 객체에서 메서드 호출 및 결과 반환&lt;/b&gt;: 생성된 임시 &lt;b&gt;래퍼 객체&lt;/b&gt;를 통해 원하는 메서드(toUpperCase())가 호출되고 결과가 반환됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-27 174030.png&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baK5hG/dJMcadmR0ki/17pk3AFFwY1eJa13UgkQb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baK5hG/dJMcadmR0ki/17pk3AFFwY1eJa13UgkQb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baK5hG/dJMcadmR0ki/17pk3AFFwY1eJa13UgkQb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaK5hG%2FdJMcadmR0ki%2F17pk3AFFwY1eJa13UgkQb0%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;595&quot; height=&quot;45&quot; data-filename=&quot;스크린샷 2025-11-27 174030.png&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;45&quot;/&gt;&lt;/span&gt;&lt;/figure&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;4. &lt;b&gt;임시 래퍼 객체 즉시 파괴&lt;/b&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; 원시 값의 불변성 유지 &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메커니즘 덕분에 원시 값 자체는 불변(Immutable)하게 유지됩니다. 원시 값에 프로퍼티를 추가하려고 시도하면, 그 프로퍼티는 잠시 생성되었다가 사라지는 &lt;b&gt;임시 객체&lt;/b&gt;에만 적용되고 즉시 파괴되므로, &lt;b&gt;원시 값에는 영구적인 영향을 주지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764232929983&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const num = 123;

// ① new Number(123) 임시 객체 생성 &amp;rarr; 프로퍼티 추가 &amp;rarr; 즉시 파괴
num.testProp = 'This is a test'; 

// ② 새로운 new Number(123) 임시 객체 재.생.성. &amp;rarr; 'testProp'이 없으므로 undefined 반환 &amp;rarr; 즉시 파괴
console.log(num.testProp); // undefined&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;프로퍼티 접근 시 매번 새로운 임시 객체가 생성되므로, 원시 값 num 자체는 변하지 않고 불변 상태를 유지합니다.&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;&lt;b&gt; 래퍼 객체를 직접 생성하여 사용하면 안되는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new String(), new Number()와 같이 생성자 함수를 사용하여 래퍼 객체를 &lt;b&gt;명시적으로&lt;/b&gt; 생성하는 것은 일반적인 자바스크립트 프로그래밍에서 &lt;b&gt;권장되지 않습니다.&lt;/b&gt; 그 이유는 &lt;b&gt;데이터 타입의 차이&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 데이터 타입의 차이 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 원시 값과 생성자 함수를 사용해 만든 래퍼 객체는 &lt;b&gt;데이터 타입&lt;/b&gt;이 다릅니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;생성 방식&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;typeof 결과&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;데이터 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;원시 값&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;리터럴 (&quot;123&quot;)&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;string&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;원시 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;래퍼 객체&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;생성자 (new String(&quot;123&quot;))&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;object&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;객체 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764233187842&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const primitive = '123';
const wrapper = new String('123');

console.log(typeof primitive); // 'string'
console.log(typeof wrapper);  // 'object'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;값 비교 (===)&lt;/b&gt;&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원시 값&lt;/b&gt;: 저장된 &lt;b&gt;값 자체&lt;/b&gt;를 비교합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체&lt;/b&gt;: 메모리에 저장된 &lt;b&gt;참조 값(주소)&lt;/b&gt;을 비교합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; new&lt;/b&gt; 키워드로 생성된 래퍼 객체는 내부 값이 같더라도 메모리상에서 서로 다른 주소에 저장된 &lt;b&gt;별개의 객체&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764233251382&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const primitive1 = '123';
const wrapper1 = new String('123');
const wrapper2 = new String('123');

// ① 원시 값과 객체의 비교: 타입이 다르므로 false
console.log(primitive1 === wrapper1); // false

// ② 객체와 객체의 비교: 서로 다른 참조(주소)를 가지는 별개의 객체이므로 false
console.log(wrapper1 === wrapper2); // false&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; new String(&quot;123&quot;) vs String(&quot;123&quot;)의 차이점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String을 호출해 값을 생성할 때 new 연산자를 사용하는 것이 어떤 차이가 있는지 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String과 같은 표준 빌트인 생성자 함수는 사용 방식에 따라 &lt;b&gt;객체를 생성&lt;/b&gt;하거나 &lt;b&gt;원시 값을 반환&lt;/b&gt;하는 두 가지 역할을 수행합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 63px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;사용 방식&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;코드 예시&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;반환 값의 타입(typeof)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;생성자 함수&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;new String(&quot;123&quot;)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;래퍼 객체를 생성하여 반환&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;일반 함수&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;String(&quot;123&quot;)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;인수를 문자열로 변환하여 반환&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;string&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. new String(&quot;123&quot;) (생성자 함수 호출)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new 키워드를 붙여 호출하면, String 함수는 문자열 &quot;123&quot;을 감싸는 &lt;b&gt;String 타입의 객체(래퍼 객체)&lt;/b&gt;를 생성하여 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764233405443&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const objStr = new String(&quot;123&quot;); 
console.log(typeof objStr); // 'object'&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;b&gt;객체&lt;/b&gt;이므로, 위에 설명된 대로 원시 값과 비교하거나 다른 래퍼 객체와 비교할 때 false가 나올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. String(&quot;123&quot;) (일반 함수 호출) &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new 키워드 없이 호출하면, String 함수는 생성자가 아닌 타입 &lt;b&gt;변환 함수(Casting Function)&lt;/b&gt;로 동작합니다. 인수로 전달된 값을 &lt;b&gt;강제로 원시 문자열 타입으로 변환&lt;/b&gt;하여 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764233462409&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const primitiveStr = String(&quot;123&quot;);
console.log(typeof primitiveStr); // 'string'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;위 방식은 일반적인 &lt;b&gt;원시 문자열 값&lt;/b&gt;이므로, &lt;b&gt;객체를 직접 생성하는 혼란을 피하기 위해&lt;/b&gt; 원시 타입으로의 변환이 필요할 때 사용됩니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, 자바스크립트에서 원시 값을 다룰 때는 new 키워드 없이 리터럴 방식이나 &lt;b&gt;타입 변환 함수&lt;/b&gt;(String(), Number())를 사용하여 &lt;b&gt;원시 값&lt;/b&gt;을 유지하는 것이 가장 안전하고 권장되는 방법입니다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/90</guid>
      <comments>https://kangs-develop.tistory.com/90#entry90comment</comments>
      <pubDate>Thu, 27 Nov 2025 17:56:03 +0900</pubDate>
    </item>
    <item>
      <title>오픈소스를 잘 사용하는 프론트엔드 개발자</title>
      <link>https://kangs-develop.tistory.com/89</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;오픈소스보안생태계.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c63epO/btsQYrNhMYP/uRkXEcR1614Ep4UOVgvGM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c63epO/btsQYrNhMYP/uRkXEcR1614Ep4UOVgvGM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c63epO/btsQYrNhMYP/uRkXEcR1614Ep4UOVgvGM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc63epO%2FbtsQYrNhMYP%2FuRkXEcR1614Ep4UOVgvGM0%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;1024&quot; height=&quot;1024&quot; data-filename=&quot;오픈소스보안생태계.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;요즘 소프트웨어 개발에 있어서 오픈소스는 빠질 수 없는 필수 불가결한 존재가 됐습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;웹 프론트엔드 개발에 있어서 주로 사용하는 React도 오픈소스, 상태 관리를 위해 사용하는 Zustand, Jotai, Redux 라이브러리도 오픈소스, 심지어 배포를 위한 서버의 OS로 사용하는 Linux도 오픈소스 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;최근에는 오픈소스 기여 경험이 취업의 스펙이 되는 등 오픈소스 생태계가 정말 활발해지고 있는데, 여러분들은 오픈소스를 어떻게 사용 중에 계신가요?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;최근 프론트엔드 개발자라면 누구나 한번은 사용해봤을 라이브러리인 eslint-config-prettier 플러그인이 해킹되는 충격적인 사건이 발생했습니다. 또 프론트엔드의 인기 프레임워크인 Next.js에서 보안 취약점이 발견이 되는 등 오픈소스에서 많은 취약점이 발견되고 있는데요. 여러가지 사건 사고가 있어도 우리는 오픈소스를 절대 포기할 수 없습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 국내 오픈소스 생태계와 프론트엔드의 오픈소스 생태계에 대해서 살펴보고, 오픈소스를 도입하는 기준에 대해서 살펴보며 마지막으로 프로젝트에서 오픈소스 보안 취약점을 어떻게 다뤄볼 수 있을까 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;국내의 유명한 오픈소스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;국내의 유명한 오픈소스 생태계를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;es-toolkit&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;먼저 요즘 제일 핫한 국내 오픈소스는 es-toolkit이 아닐까 싶습니다. Toss에서 시작된 es-toolkit은 lodash를 대체하는 JavaScript 유틸리티 라이브러리 인데요, 패키지 매니저인 Yarn과 Storybook에서 es-toolkit을 도입하는 등 해외에서도 많은 반응을 얻고 있는 라이브러리 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TOAST-UI&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;NHN에서 시작된 TOAST-UI는 Chart, Editor, Grid 등 다양한 라이브러리로 인기를 얻은 오픈소스 입니다. 특히 TOAST-UI Editor은 에디터 라이브러리 중 레퍼런스도 많고 사용성이 좋아 많은 사랑을 받고 있는 라이브러리 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;billboard.js&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Naver에서 시작된 billboard.js 라이브러리는 D3 기반의 차트 라이브러리 입니다. npm을 살펴보면 25/10/1 기준 weekly download 횟수가 30k가 넘어가는 등 꾸준히 많은 사랑을 받고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;billboard.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnDSH4/btsQZvHLZsi/5rrTGO62LpcnGgs7fbDtr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnDSH4/btsQZvHLZsi/5rrTGO62LpcnGgs7fbDtr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnDSH4/btsQZvHLZsi/5rrTGO62LpcnGgs7fbDtr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnDSH4%2FbtsQZvHLZsi%2F5rrTGO62LpcnGgs7fbDtr0%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;1210&quot; height=&quot;701&quot; data-filename=&quot;billboard.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 세 개의 라이브러리를 살펴봤는데요, 국내의 오픈소스 생태계 역시 꾸준히 성장하며 많은 사랑을 받고 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프론트엔드, 뭐로 개발해?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;만약 여러분이 프론트엔드 개발자라면, 뭐로 개발하시나요? React? Next? Vue? Angular? Svelte? 아니면 jQuery?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위에 나열한 그 어떤 것을 사용하던 여러분은 오픈소스를 사용해 프로젝트를 진행하고 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그럼 React를 사용한다고 가정하고 다시 질문해볼까요. 상태 관리 라이브러리로 어떤 것을 사용하시나요? Zustand, Jotai, Redux, 서버 상태를 위해 tanstack/react-query, SWR.. 어떤 것을 선택하든 우리는 오픈소스 생태계에 푹 빠져있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;요즘 프론트엔드에서 어떤 기능을 구현하려고 할 때 우리는 그 기능을 직접 구현하는 일은 드뭅니다. (물론 기존에 구현된 오픈소스가 마음에 안들어 직접 구현하는 케이스들도 더러 존재하지만요.)&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;기존에 구현된 라이브러리를 찾아보고, 라이브러리의 장단점을 비교해가며 실제 프로젝트에 도입하여 기능 구현을 진행합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;React 생태계가 이렇게 성장할 수 있었던 것도 오픈소스 생태계의 성장과 서드 파티 라이브러리를 선택함에 있어서 자유도가 있었기 때문이라고 생각하고요.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이렇게 자유롭게 선택할 수 있는 오픈소스 생태계에서 오픈소스를 도입하려 할 때 여러분들은 어떤 기준점을 가지고 도입하나요? 그럼 프로젝트에 오픈소스를 도입하려 할 때 왜 기준을 가지고 도입을 해야 할까요? 왜 아무 오픈소스나 막 도입하면 안될까요?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;오픈소스 도입의 기준&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에 오픈소스를 도입하는데 있어서 많은 기준 점이 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 웹 프론트엔드에서 상태 관리 라이브러리를 도입하는 상황이라면 Zustand, Jotai를 선택할 수 있는데요, 프로젝트의 규모, 상태 관리 중 선호하는 방식 (Top-down or Bottom-up), 번들링 사이즈, 미들웨어를 지원하는 여부 등이 될 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;만약 여러분이 dnd(drag and drop) 기능을 개발하는 상황이고, 오픈소스 라이브러리를 도입하기로 결정했다고 가정합시다. dnd 기능을 지원하는 오픈소스 라이브러리는 많습니다. react-beautiful-dnd, dnd-kit, 혹은 drag로 resize를 해야하는 상황이라면 react-drag-sizing, re-resizable 등 여러 선택지가 존재합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;유지 보수&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 오픈소스 라이브러리가 메인테이너에 의해 유지 보수가 되고 있는지에 대해 체크해봐야 합니다. 도입을 결정한 시점을 기준으로 몇 년 동안 라이브러리의 패치가 없었다면 해당 오픈소스는 EOL(End of Lifecycle)이 됐을 확률이 높습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;만약 해당 라이브러리를 프로젝트에 도입을 결정한다면, 기능을 구현한 뒤 해당 프로젝트를 유지보수 할 때 라이브러리에 문제가 생겼을 경우 도움을 받기 어려울 뿐더러, 메인 라이브러리의 버전이 올라갔을 때 대응이 없을 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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;react-drag-sizing.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzbmI/btsQW4E1mgg/GVKz3vkLyFKSOWekIFXCyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzbmI/btsQW4E1mgg/GVKz3vkLyFKSOWekIFXCyK/img.png&quot; data-alt=&quot;react-drag-sizing 라이브러리의 마지막 퍼블리싱은 3년 전 입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzbmI/btsQW4E1mgg/GVKz3vkLyFKSOWekIFXCyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzbmI%2FbtsQW4E1mgg%2FGVKz3vkLyFKSOWekIFXCyK%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;1220&quot; height=&quot;840&quot; data-filename=&quot;react-drag-sizing.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;react-drag-sizing 라이브러리의 마지막 퍼블리싱은 3년 전 입니다.&lt;/figcaption&gt;
&lt;/figure&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-filename=&quot;re-resizable.png&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci9R7l/btsQXV2cd0p/zedbAIE5IJ6ZKFY68stlkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci9R7l/btsQXV2cd0p/zedbAIE5IJ6ZKFY68stlkK/img.png&quot; data-alt=&quot;대안인 re-resizable 라이브러리는 7달 전 퍼블리싱이 됐으며 React 19버전이 커버 됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci9R7l/btsQXV2cd0p/zedbAIE5IJ6ZKFY68stlkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci9R7l%2FbtsQXV2cd0p%2FzedbAIE5IJ6ZKFY68stlkK%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;1192&quot; height=&quot;793&quot; data-filename=&quot;re-resizable.png&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대안인 re-resizable 라이브러리는 7달 전 퍼블리싱이 됐으며 React 19버전이 커버 됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리 호환성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;메인 라이브러리와 호환성 여부를 체크해야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;유지 보수와 비슷한 맥락으로 해당 오픈소스 라이브러리가 프로젝트에서 사용하는 라이브러리의 버전을 지원하는지 확인이 필요합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;도입하고자 하는 오픈소스 라이브러리가 오래전에 퍼블리싱 됐다면 해당 라이브러리는 메인 라이브러리의 최신 버전을 지원하지 못할 가능성이 존재합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;취약점의 여부&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 오픈소스 라이브러리에 취약점이 존재하는지 여부도 체크의 대상이 됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스 보안 취약점은 SBOM을 생성해 취약점을 점검할 수 있고, 프론트엔드 환경이라면 npm audit를 활용해 빠르게 취약점을 검출하고 취약점이 수정된 버전으로 업그레이드도 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에서 오픈소스 라이브러리의 쓰임새, 러닝 커브(학습 곡선), 인기도 등 다양한 요소가 오픈소스 도입의 기준이 될 수 있지만 반드시 체크해야 할 부분은 유지 보수, 호환성, 취약점 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;오픈소스 보안 취약점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스 보안 취약점을 사례와 함께 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Log4j &amp;mdash; 로그가 공격 통로가 된 사건&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;log4j 취약점 사태는 2021년 11월에 발생한 사건으로 취약점 심각도 10점 만점 중 10점을 받은 심각한 사태였습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Log4j는 자바 진영에서 가장 많이 쓰이는 로그 라이브러리입니다. 문제는 로그 메시지를 처리하면서 특정 문자열을 &lt;b&gt;동적으로 해석&lt;/b&gt;하는 기능이 들어있었다는 점입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;공격자는 이 기능을 악용해 단순히 로그에 특정한 문자열을 남겼을 뿐인데, 서버가 그 문자열을 코드 실행처럼 처리해버렸습니다. 결국 원격에서 마음대로 명령을 실행할 수 있는, 흔히 말하는 &amp;ldquo;원격 코드 실행&amp;rdquo; 취약점이 생긴겁니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 사건(Log4Shell)은 전 세계적으로 큰 파장을 일으켰습니다. 수많은 서비스가 Log4j를 사용하고 있었고, 단순한 로그 한 줄이 시스템 전체를 뚫는 결과를 가져왔습니다. &lt;b&gt;편리함을 위해 넣은 기능 하나가 보안의 아킬레스건&lt;/b&gt;이 된 대표적인 사례입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Next.js middleware 보안 취약점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Next.js 미들웨어 보안 취약점은 미들웨어 기능을 사용할 때 내부적으로 사용하는 특수한 헤더가 있었는데, 이 헤더를 외부에서 조작할 수 있는 여지가 남아 있었습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;결국 공격자가 이 헤더를 직접 넣어버리면, 원래라면 거쳐야 할 인증이나 권한 검증이 우회되는 상황이 벌어졌습니다. 즉, &amp;ldquo;프레임워크 내부용&amp;rdquo;이라고 생각했던 값이 실제로는 누구나 넣을 수 있었던 것입니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1759307735120&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;Next.js 버전 15.2.3 보안 취약점 해결 출시 | GeekNews&quot; data-og-description=&quot;CVE-2025-29927Next.js 버전 15.2.3이 보안 취약점(CVE-2025-29927)을 해결하기 위해 출시됨. next start와 output: 'standalone'을 사용하는 모든 자체 호스팅 Next.js 배포는 즉시 업데이트할 것을 권장함.타임라인2025-&quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=19922&quot; data-og-url=&quot;https://news.hada.io/topic?id=19922&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ciWvRV/hyZJZ130Tj/1Hlw2PzubuKmj8hiUkiLu0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/jpmQX/hyZKp5VHHj/gOZFaYOfauSD5pfCdjnACK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=19922&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=19922&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ciWvRV/hyZJZ130Tj/1Hlw2PzubuKmj8hiUkiLu0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/jpmQX/hyZKp5VHHj/gOZFaYOfauSD5pfCdjnACK/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;Next.js 버전 15.2.3 보안 취약점 해결 출시 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CVE-2025-29927Next.js 버전 15.2.3이 보안 취약점(CVE-2025-29927)을 해결하기 위해 출시됨. next start와 output: 'standalone'을 사용하는 모든 자체 호스팅 Next.js 배포는 즉시 업데이트할 것을 권장함.타임라인2025-&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Linux xz-utils 사태&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;XZ Utils는 리눅스 환경에서 파일을 압축하거나 풀 때 흔히 사용하는 아주 기본적인 도구입니다. 2024년에 발견된 사건은 충격적이었는데, 프로젝트 메인테이너의 권한을 노려서 &lt;b&gt;악성 코드가 포함된 버전&lt;/b&gt;이 공식 배포물로 올라왔습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 악성 버전은 평범한 것처럼 보였지만 실제로는 SSH 같은 중요한 통신을 가로채서 인증을 우회할 수 있는 백도어를 심어두고 있었습니다. 다시 말해, 리눅스 배포판을 설치했을 뿐인데 공격자가 원격에서 접속할 수 있는 뒷문이 열려 있는 상태가 될 수 있었던 겁니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 사건은 단순한 취약점이 아니라 2021년부터 공격자로 인해 치밀하게 계획된 사건이었고 &lt;b&gt;공급망 자체가 공격에 뚫린 경우&lt;/b&gt;였습니다. &amp;ldquo;공식 저장소에서 내려받으면 안전하다&amp;rdquo;는 믿음을 완전히 무너뜨렸고, 오픈소스 신뢰 체계에 큰 경종을 울렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1759307735733&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;xz-utils 백도어 사건&quot; data-og-description=&quot;CVE-2024-3094 메인 개발자 Lasse Collin의 회고 2024년 3월 29일 , 마이크로소프트&quot; data-og-host=&quot;namu.wiki&quot; data-og-source-url=&quot;https://namu.wiki/w/xz-utils%20%EB%B0%B1%EB%8F%84%EC%96%B4%20%EC%82%AC%EA%B1%B4&quot; data-og-url=&quot;https://namu.wiki/w/xz-utils%20%EB%B0%B1%EB%8F%84%EC%96%B4%20%EC%82%AC%EA%B1%B4&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pFP6P/hyZKg9gMpv/NaCdtwff614KFmkh2pE4dk/img.png?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://namu.wiki/w/xz-utils%20%EB%B0%B1%EB%8F%84%EC%96%B4%20%EC%82%AC%EA%B1%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://namu.wiki/w/xz-utils%20%EB%B0%B1%EB%8F%84%EC%96%B4%20%EC%82%AC%EA%B1%B4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pFP6P/hyZKg9gMpv/NaCdtwff614KFmkh2pE4dk/img.png?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&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;xz-utils 백도어 사건&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CVE-2024-3094 메인 개발자 Lasse Collin의 회고 2024년 3월 29일 , 마이크로소프트&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;namu.wiki&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;eslint-config-prettier&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;비교적 최근인 25년 7월 18일에 프론트엔드 개발자라면 한번은 들어봤거나 사용했을 eslint-config-prettier 패키지가 피싱 공격으로 계정이 탈취되어 악성코드가 유포되는 공급망 공격(Supply Chain Attack)이 발생했습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;공격자는 가짜 NPM 사이트를 만들어 패키지 매니저의 계정을 탈취했고, 악성코드가 삽입된 버전을 배포하여 수많은 프로젝트 공격을 시도했습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;eslint-config-prettier 패키지는 주로 개발 환경에서만 쓰이기 때문에 &amp;ldquo;이 정도는 안전하겠지&amp;rdquo; 하고 넘어가기 쉽습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 현실은 달랐습니다. 개발 환경에서 설치되는 패키지라 해도 결국 빌드 서버나 CI/CD 환경을 거쳐 최종 산출물로 이어지기 때문에 공격자는 dev-dependency도 훌륭한 공격 통로로 삼을 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 사례는 &lt;b&gt;&amp;ldquo;개발용 도구니까 괜찮다&amp;rdquo;는 안일한 생각이 얼마나 위험한지&lt;/b&gt; 보여줍니다. 실제 업무에서는 개발 도구까지 포함해 모든 오픈소스를 보안 점검 대상에 넣어야 한다는 교훈을 남겼습니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://kitpa.org/news/182?title=eslint-config-prettier%20NPM%20%ED%8C%A8%ED%82%A4%EC%A7%80%20%ED%95%B4%ED%82%B9%2C%20%EC%A0%84%20%EC%84%B8%EA%B3%84%20%EA%B0%9C%EB%B0%9C%EC%9E%90%207%EC%B2%9C%208%EB%B0%B1%EB%A7%8C%20%EB%AA%85%20%ED%94%BC%ED%95%B4&amp;amp;tags=%EC%A0%95%EB%B3%B4%EB%B3%B4%EC%95%88&amp;amp;image=4cde8e4b-ae51-4b69-ad26-abfc421c3f2a&amp;amp;createdAt=Mon%20Aug%2004%202025%2020%3A50%3A44%20GMT-0700%20(Pacific%20Daylight%20Time)%EF%BB%BF&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;eslint-config-prettier NPM 패키지 해킹, 전 세계 개발자 7천 8백만명 피해&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;여러가지 사례를 통해 살펴듯이 우리가 사용하는 오픈소스도 주의해서 사용해야 하며 항상 보안 취약점에 관심을 가지고 있어야 함을 알 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프론트엔드에서 오픈소스 보안 취약점 검증, 조치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드에서 보안 취약점을 쉽게 탐지할 수 있는 방법이 있을까요?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;또 보안 취약점이 발견된다면 어떻게 조치를 해야할까요?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;npm audit&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;우리가 사용하는 패키지 매니저에는 &lt;b&gt;audit&lt;/b&gt;이라는 훌륭한 기능이 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 명령어는 프로젝트에서 사용되는 라이브러리의 취약점을 검사하고, 보안 문제를 해결할 수 있는 업데이트 정보를 제공하는 기능입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;취약점의 위험도에 따라 critical &amp;gt; high &amp;gt; moderate &amp;gt; low 로 취약점을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-01 172333.png&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5Oel8/btsQYQ0pqXq/wxc3FrBE3vKwnd64mlknH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5Oel8/btsQYQ0pqXq/wxc3FrBE3vKwnd64mlknH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5Oel8/btsQYQ0pqXq/wxc3FrBE3vKwnd64mlknH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5Oel8%2FbtsQYQ0pqXq%2Fwxc3FrBE3vKwnd64mlknH0%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;616&quot; height=&quot;466&quot; data-filename=&quot;스크린샷 2025-10-01 172333.png&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1759308088525&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;blog&quot; data-og-title=&quot;npm에 새로 추가된 audit 기능 :: Outsider's Dev Story&quot; data-og-description=&quot;[npm v6가 나오면서](https://medium.com/npm-inc/announcing-npm-6-5d0b1799a905) &amp;#96;npm audit&amp;#96;이라는 기능이 추가되었다. 이는 사용하는 npm 모듈의 취약점을 검사해주는 [Node Security Platform](https://node...&quot; data-og-host=&quot;blog.outsider.ne.kr&quot; data-og-source-url=&quot;https://blog.outsider.ne.kr/1375&quot; data-og-url=&quot;https://blog.outsider.ne.kr/1375&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/THEo5/hyZKwjGrXv/H6bFzK2PjGhs6J5BOXkdwk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/Aa3QV/hyZKBkZRT3/EVq7eA53eN40QWpppuLkmk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://blog.outsider.ne.kr/1375&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.outsider.ne.kr/1375&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/THEo5/hyZKwjGrXv/H6bFzK2PjGhs6J5BOXkdwk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/Aa3QV/hyZKBkZRT3/EVq7eA53eN40QWpppuLkmk/img.jpg?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&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;npm에 새로 추가된 audit 기능 :: Outsider's Dev Story&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[npm v6가 나오면서](https://medium.com/npm-inc/announcing-npm-6-5d0b1799a905) `npm audit`이라는 기능이 추가되었다. 이는 사용하는 npm 모듈의 취약점을 검사해주는 [Node Security Platform](https://node...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.outsider.ne.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;--fix&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;아래 내용은 pnpm을 기준으로 기술합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt; 플래그를 추가하면, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pnpm&lt;/span&gt;은 다음 작업을 자동으로 수행하여 발견된 취약점을 수정하려 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;취약점 분석&lt;/b&gt;: 취약점이 발견된 패키지와 해당 취약점을 해결할 수 있는 &lt;b&gt;안전한 버전&lt;/b&gt;을 식별합니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;버전 업데이트&lt;/b&gt;: 안전한 버전으로 업데이트하기 위해 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pnpm-lock.yaml&lt;/span&gt;&lt;b&gt; 파일을 수정&lt;/b&gt;하고, 해당 패키지의 의존성 트리를 변경합니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;패키지 설치&lt;/b&gt;: 변경된 잠금 파일을 기반으로 새로운 패키지를 설치하여 취약점이 해결된 버전이 프로젝트에 실제로 적용되도록 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt;의 주요 역할: 버전 업그레이드&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt;의 주요 목적은 &lt;b&gt;호환성을 깨지 않는 선에서&lt;/b&gt; 취약점이 있는 패키지를 보안 패치가 적용된 &lt;b&gt;가장 낮은 버전으로 업그레이드&lt;/b&gt;하는 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;자동 마이너/패치 업데이트&lt;/b&gt;: 대부분의 취약점은 해당 패키지의 &lt;b&gt;마이너 버전(Minor)&lt;/b&gt; 또는 &lt;b&gt;패치 버전(Patch)&lt;/b&gt; 업데이트만으로 해결됩니다. &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt;는 이러한 업데이트를 자동으로 수행하며, 일반적으로는 프로젝트의 기존 기능에 영향을 미치지 않습니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;주요 버전(Major)&lt;/b&gt; 업데이트는 수행하지 않습니다. 주요 버전 업데이트는 호환성이 깨질 수 있는 변경사항(Breaking&amp;nbsp;Changes)을 포함할 수 있으므로, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt;는 개발자의 명시적인 승인 없이 이를 자동으로 처리하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pnpm audit --fix&lt;/span&gt;는 취약점 수정에 매우 유용하지만, 다음과 같은 이유로 &lt;b&gt;자동 수정 후 반드시 테스트&lt;/b&gt;가 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;잠재적 호환성 문제&lt;/b&gt;: 패치가 마이너 버전 내에서 이루어졌다고 하더라도, 드물게 다른 의존성이나 프로젝트 코드와 &lt;b&gt;예기치 않은 충돌&lt;/b&gt;을 일으킬 수 있습니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;완벽한 해결 불가&lt;/b&gt;: 만약 취약점을 해결하기 위해 &lt;b&gt;주요 버전(Major)&lt;/b&gt; 업그레이드가 필요하거나, 취약한 패키지가 프로젝트의 루트 package.json에 명시된 범위(&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;&quot;~1.0.0&quot;&lt;/span&gt;, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;&quot;^1.0.0&quot;&lt;/span&gt;)를 벗어나는 경우, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;--fix&lt;/span&gt;만으로는 해결되지 않을 수 있습니다. 이때는 개발자가 수동으로 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;package.json&lt;/span&gt;&lt;b&gt; 파일을 수정&lt;/b&gt;하거나 pnpm의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;resolutions&lt;/span&gt; 기능을 사용하여 버전을 명시적으로 지정해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Github Actions를 활용한 CI&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;pnpm audit를 활용해 CI 파이프라인에서 취약점을 점검해봅시다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;아래 작성된 CI 파이프라인은 high 이상의 취약점이 발견된 경우 CI를 중단합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1759307799528&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI

on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
        with:
          version: 9
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      - name: Run pnpm audit
        run: pnpm audit --audit-level=high&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;--audit-level 플러그인을 사용해 high 이상의 취약점이 검출될 시 파이프라인을 종료합니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;우리는 이번 글을 통해 오픈소스가 현대 소프트웨어 개발의 핵심 동력이자 동시에 &lt;b&gt;가장 큰 보안 취약점의 통로&lt;/b&gt;가 될 수 있음을 여러 사례를 통해 확인했습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Log4j 사태는 편의성 이면에 숨겨진 기능의 위험성을, XZ&amp;nbsp;Utils 사태는 개발 생태계 전체를 겨냥한 치밀한 공급망 공격의 현실을 보여주었습니다. 특히 프론트엔드 개발자에게 경고를 던진 Next.js의 내부 헤더 우회 취약점과 eslint-config-prettier의 개발 도구 해킹 사건은 '개발용이니까 안전하겠지'라는 안일한 인식이 얼마나 위험한지 분명히 깨닫게 해주었습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이러한 사건 사고에도 불구하고 우리는 오픈소스를 포기할 수 없습니다. 핵심은 &lt;b&gt;맹목적인 신뢰 대신 신중한 관리&lt;/b&gt;로 나아가야 한다는 것입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에 오픈소스를 도입할 때는 단순히 기능 구현 여부뿐만 아니라 유지 보수 상태, 호환성, 그리고 취약점 여부를 핵심 기준으로 삼아야 합니다. 나아가, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;npm audit&lt;/span&gt; 같은 패키지 매니저의 기본 기능을 활용하고, CI/CD 파이프라인에 pnpm audit --audit-level=high 와 같은 자동 검증 단계를 의무화해야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스는 자유와 성장의 토대이지만, 그 자유에는 책임이 따릅니다. 모든 의존성은 잠재적인 위험 요소라는 인식을 가지고, 시스템적인 검증과 지속적인 관심을 기울일 때, 비로소 우리는 오픈소스의 무한한 가능성을 안전하게 누릴 수 있을 것입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;읽어봐주셔서 감사합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고하면 좋은 블로그&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;SBOM에 관해 잘 정리되어 있는 블로그 글 입니다. SBOM이 무엇인지, 프로젝트 내 오픈소스 보안 취약점을 검출하는 방법을 상세하게 기술해놓은 글 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://velog.io/@csb1320/%EC%9A%B0%EB%A6%AC%EA%B0%80-%EC%93%B0%EB%8A%94-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%95%88%EC%A0%84%ED%95%A0%EA%B9%8C-SBOM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;우리가 쓰는 오픈소스, 안전할까?(쓰봄sbom...)&lt;/b&gt;&lt;/a&gt;&lt;/blockquote&gt;</description>
      <category>이것저것</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/89</guid>
      <comments>https://kangs-develop.tistory.com/89#entry89comment</comments>
      <pubDate>Wed, 1 Oct 2025 17:48:32 +0900</pubDate>
    </item>
    <item>
      <title>[MFA] Micro App의 라우팅</title>
      <link>https://kangs-develop.tistory.com/88</link>
      <description>&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Micro Frontend는 하나의 Host App과 여러 개의 Remote App으로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;개별 Remote App의 라우팅은 당연하며 Host App을 통해 통합된 여러 App의 라우팅도 자연스럽게 발생해야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각 App의 라우팅이 자연스럽게 발생하기 위해 각 App은 독자적인 라우터가 존재하며, 이 라우터들을 통합 하기 위해 Host App의 라우터가 필요하고, Host App은 통합된 App의 라우팅을 이어줄 수 있는 브릿지 역할을 해야합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;추상적으로 통합이지만 물리적으로는 각 App이 각자의 라우터를 소유하고 있습니다.&lt;/blockquote&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-filename=&quot;1.png&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clUPZK/btsPZvhkpni/QqgHUKU0lO3e1Q0FGjdpj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clUPZK/btsPZvhkpni/QqgHUKU0lO3e1Q0FGjdpj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clUPZK/btsPZvhkpni/QqgHUKU0lO3e1Q0FGjdpj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclUPZK%2FbtsPZvhkpni%2FQqgHUKU0lO3e1Q0FGjdpj0%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;933&quot; height=&quot;485&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;4개의 MFA 애플리케이션을 예시를 들어보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Host App은 CMP, Openstackit, PaaS, App을 통합하고 있습니다. 그림을 살펴보면 각 App 에서의 라우팅이 발생하고 App 간의 라우팅 (eg. cmp/monitoring &amp;gt; paas/pod)도 발생하고 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;독자적인 App(eg. CMP App)의 라우팅은 기존의 SPA의 방식 (react-router-dom)으로 가능합니다. 하지만 Host App은 Remote App의 &lt;b&gt;라우터를 알 수 없고, 알 필요도 없습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Host App은 단순히 아키텍처 내의 &lt;b&gt;라우팅 요청을 처리만 해주는 브릿지 역할만 담당&lt;/b&gt;할 뿐 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이벤트 기반의 라우팅&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Host App은 Remote App의 통합을 위한 App 입니다. Host App은 Shell App으로 명명합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-pm-slice=&quot;3 5 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App의 라우터는 Browser Router 입니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;통합된 App 내부의 Remote App의 라우터는 Memory Router 입니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;App을 구동합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;Shell App에 Remote App을 통합하는 경우&lt;/b&gt; Remote App은 &lt;b&gt;Memory Router&lt;/b&gt;로 구동 됩니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;Remote App이 Standalone 인 경우&lt;/b&gt;, &lt;b&gt;Browser Router&lt;/b&gt;로 구동 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App의 라우팅을 위해 이벤트를 구독(리스닝)하고 있습니다. 이 이벤트의 이름은 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[shell] navigate&lt;/span&gt; 입니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App은 Shell-Remote의 라우팅을 위해 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt; 이벤트를 구독(리스닝) 하고 있습니다. 해당 이벤트가 발생할 경우 Remote 앱 내부에서 라우팅이 발생합니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;location이 변경된 경우 Remote App에서 이벤트를 Dispatch 합니다. 이벤트는 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;${type} navigated&lt;/span&gt; 입니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Host App에서 Remote App을 래핑한 컴포넌트에서 해당 이벤트를 구독하고 있습니다. Remote App의 Memory Router 에서 발생한 라우팅(이벤트)을 실제 Path와 동기화(Browser Router에서의 라우팅) 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;시퀀스 다이어그램으로 살펴보기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2MFA Routing Sequence Diagram.png&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRiqgr/btsPYNv2FfB/lp9rXJjM9PlMg1WJsTzcV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRiqgr/btsPYNv2FfB/lp9rXJjM9PlMg1WJsTzcV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRiqgr/btsPYNv2FfB/lp9rXJjM9PlMg1WJsTzcV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRiqgr%2FbtsPYNv2FfB%2Flp9rXJjM9PlMg1WJsTzcV1%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;3840&quot; height=&quot;2277&quot; data-filename=&quot;2MFA Routing Sequence Diagram.png&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-pm-slice=&quot;3 9 [&amp;quot;blockquote&amp;quot;,{&amp;quot;localId&amp;quot;:null}]&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App에서 [shell]navigate 이벤트를 발생 (&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useShellNavigate&lt;/span&gt;)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App에서 [shell]navigate 이벤트를 구독 (&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useShellNavigateListener&lt;/span&gt;)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App에서 Event를 통해 전달 받은 pathname으로 navigate&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App의 Remote App을 래핑한 컴포넌트에서 해당 Remote App의 주소로 navigation이 발생했는지 체크
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;만약 해당 Remote App의 주소로 navigation이 발생했다면 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt; 이벤트 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App에서 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt; 이벤트 구독
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App에서 발생한 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt; 이벤트를 통해 Event를 통해 전달 받은 pathname과 메모리 라우터에서 관리하는 pathname이 일치하는지 판단
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;pathname이 일치하면 return&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;일치하지 않다면 Remote App (Memory Router)에서 navigate 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App에서 location이 변경될 경우 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[${type}] navigated&lt;/span&gt; 이벤트 발생
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Shell App의 Remote App을 래핑한 컴포넌트에서 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[${type}] navigated&lt;/span&gt; 이벤트 구독
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Event를 통해 전달받은 pathname과 Shell App의 Browser Router의 location과 비교
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;pathname이 일치할 경우 return&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;일치하지 않을 경우 Shell App(Browser Router)에서 navigate (실제 url path 변경)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이벤트 정의&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 106px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;&lt;span&gt;&lt;b&gt;이벤트 명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;&lt;span&gt;&lt;b&gt;Listening Hook&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;&lt;span&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[shell] navigate&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;Shell App의 navigate를 요청&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;use-shell-navigator-listener&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;backgroundColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-background-custom-color=&quot;#dcdfe4&quot;&gt;Shell App에서 해당 이벤트를 구독&lt;/span&gt;하고 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;Shell App의 라우팅을 담당합니다. 이벤트가 발생 시 Shell App의 Browser Router에서 라우팅 (navigate)가 발생합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;Shell App의 특정 Remote App의 location이 변경(navigate)이 발생함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;use-app-events&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;backgroundColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-background-custom-color=&quot;#dcdfe4&quot;&gt;Remote App에서 해당 이벤트를 구독&lt;/span&gt;하고 있습니다.&lt;br /&gt;&lt;/span&gt; &lt;br /&gt;&lt;span&gt;Shell App의 location이 변경되는 경우 이벤트가 dispatch 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;이벤트가 발생한 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pathname&lt;/span&gt;과 Remote App의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;location&lt;/span&gt;의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pathname&lt;/span&gt;을 비교합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;다르다면 이벤트가 발생한 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pathname&lt;/span&gt;으로 Remote App에서 라우팅 합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;MFA로 통합된 Remote App은 &lt;span data-prosemirror-mark-name=&quot;textColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-text-custom-color=&quot;#ff5630&quot;&gt;Memory Router&lt;/span&gt;로 주소의 변경 없이 메모리 상에서 라우팅이 발생하고, Standalone Remote App은 &lt;span data-prosemirror-mark-name=&quot;textColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-text-custom-color=&quot;#bf2600&quot;&gt;Browser Router&lt;/span&gt;로 실제 주소가 변경되며 라우팅 됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[${type}] navigated&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Remote 앱의 navigate가 발생함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;use-shell-events&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-prosemirror-mark-name=&quot;backgroundColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-background-custom-color=&quot;#dcdfe4&quot;&gt;Shell App의 Remote App을 래핑한 컴포넌트에서 해당 이벤트를 구독&lt;/span&gt;하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Remote App에 위치한 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useAppEvent&lt;/span&gt; Hook에서 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;location&lt;/span&gt;이 변경되면 해당 이벤트를 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;dispatch&lt;/span&gt; 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;각각의 Remote App은 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;/&lt;/span&gt;로 라우팅이 시작되기에 이벤트를 통해 전달 받은 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;pathname&lt;/span&gt;에 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;basename&lt;/span&gt;을 prefix로 붙입니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;prefix를 붙인 경로가 Shell App의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;location.pathname&lt;/span&gt;과 일치하지 않다면 새로운 경로로 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;navigate&lt;/span&gt; 합니다. (화면의 Memory Router의 경로와 주소 창의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;path&lt;/span&gt;를 동기화 시킵니다.)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;shell-router 패키지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;injectFactory&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;팩토리 함수를 반환합니다. routes 객체를 받아 Router을 만들고 rootElement를 만들어 랜더링 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596520469&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function injectFactory({ routes }: { routes: RouteObject[] }) {
  return ({
    rootElement,
    basePath,
    routerType,
  }: {
    rootElement: HTMLElement;
    basePath?: string;
    routerType: RouterType;
  }) =&amp;gt; {
    const router = createRouter({
      type: routerType,
      routes,
      basePath,
    });

    const root = createRoot(rootElement);
    root.render(&amp;lt;RouterProvider router={router} /&amp;gt;);

    return () =&amp;gt; queueMicrotask(() =&amp;gt; root.unmount());
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;createRouter&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;injectFactory 함수에서 사용하는 라우터 Factory 함수 입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App이 MFA로 통합되는 경우 Memory Router로 생성합니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Remote App이 Standalone 일 경우 Browser Router로 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1755596534328&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createBrowserRouter, createMemoryRouter } from &quot;react-router-dom&quot;;

import { type CreateRouterProps } from &quot;./types&quot;;

type Router =
  | ReturnType&amp;lt;typeof createBrowserRouter&amp;gt;
  | ReturnType&amp;lt;typeof createMemoryRouter&amp;gt;;

export function createRouter({
  type,
  routes,
  basePath,
}: CreateRouterProps): Router {
  switch (type) {
    case &quot;browser&quot;:
      return createBrowserRouter(routes);
    case &quot;memory&quot;:
      return createMemoryRouter(routes, { initialEntries: [basePath || &quot;/&quot;] });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useShellNavigateListener&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Shell App에서 [shell] naviagte 이벤트를 구독합니다. Shell App의 Browser Router에서 라우팅을 통해 주소를 변경합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596547536&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function useShellNavigateListener() {
  const navigate = useNavigate();

  useEffect(() =&amp;gt; {
    const shellNavigateListener = (event: Event) =&amp;gt; {
      const pathname = (event as CustomEvent).detail;
      navigate(pathname);
    };

    window.addEventListener(&quot;[shell] navigate&quot;, shellNavigateListener);

    return () =&amp;gt; {
      window.removeEventListener(&quot;[shell] navigate&quot;, shellNavigateListener);
    };
  }, [navigate]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;AppRoutingManager&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Remote App의 최상위 Path &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;/&lt;/span&gt; 의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;Element&lt;/span&gt; 입니다. &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;type&lt;/span&gt;은 Remote App의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;Basename&lt;/span&gt;을 넣어주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596561147&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface AppRoutingManagerProps {
  type: string;
}

const AppRoutingManager: React.FC&amp;lt;AppRoutingManagerProps&amp;gt; = ({ type }) =&amp;gt; {
  useAppEvent(type);

  return &amp;lt;Outlet /&amp;gt;;
};

export default AppRoutingManager;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1755596570935&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const routes: RouteObject[] = [
  {
    path: &quot;/&quot;,
    element: (
      &amp;lt;Suspense&amp;gt;
        &amp;lt;Layout&amp;gt;
          &amp;lt;AppRoutingManager type=&quot;remote2&quot; /&amp;gt;
        &amp;lt;/Layout&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    ),
    errorElement: &amp;lt;div&amp;gt;error&amp;lt;/div&amp;gt;,
    children: [
      { index: true, element: &amp;lt;Navigate to=&quot;home&quot; /&amp;gt; },
      { path: &quot;home&quot;, element: &amp;lt;Home /&amp;gt; },
      { path: &quot;list&quot;, element: &amp;lt;List /&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-pm-slice=&quot;1 2 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useAppEvent&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;AppRoutingManager&lt;/span&gt;에서 참조하는 Hook 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;[app-shell] navigated 이벤트를 구독하고 있으며, 이벤트에 바인딩 된 path로의 라우팅을 담당합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Remote App 에서 location이 변경된 경우 [${type}] navigated 이벤트를 dispatch 합니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;[app-shell] navigated 이벤트는 Shell App에서 location이 변경됐을 때 해당 Remote App의 basename으로 라우팅이 된 경우 발생합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre id=&quot;code_1755596586270&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function useAppEvent(type: string) {
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() =&amp;gt; {
    function shellNavigationHandler(event: Event) {
      const pathname = (event as CustomEvent&amp;lt;string&amp;gt;).detail;

      if (location.pathname === pathname) {
        return;
      }

      navigate(pathname);
    }

    window.addEventListener(`[app-shell] navigated`, shellNavigationHandler);

    return () =&amp;gt; {
      window.removeEventListener(
        `[app-shell] navigated`,
        shellNavigationHandler
      );
    };
  }, [location, navigate]);

  useEffect(() =&amp;gt; {
    window.dispatchEvent(
      new CustomEvent(`[${type}] navigated`, { detail: location.pathname })
    );
  }, [location, type]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useShellEvent&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Shell App에서 Remote App을 래핑하는 컴포넌트에서 사용하는 Hook 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[${type}] navigated&lt;/span&gt; 이벤트를 구독하며 Remote App에서 발생한 라우팅을 Shell App의 Browser Router와 동기화 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Shell App의 location이 바뀌었을 때 location의 pathname이 파라미터로 전달 받은 basename으로 시작할 경우 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[app-shell] navigated&lt;/span&gt; 이벤트를 dispatch 해 Shell App의 location이 변경 됐다는 것을 Remote App에 알립니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596602650&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect } from &quot;react&quot;;
import { useLocation, useNavigate } from &quot;react-router-dom&quot;;

export default function useShellEvent(type: string, basename: string) {
  const location = useLocation();
  const navigate = useNavigate();

  useEffect(() =&amp;gt; {
    const appNavigationEventHandler = (event: Event) =&amp;gt; {
      const pathname = (event as CustomEvent&amp;lt;string&amp;gt;).detail;
      const newPathname =
        pathname === &quot;/&quot; ? basename : `${basename}${pathname}`;

      if (newPathname === location.pathname) {
        return;
      }

      navigate(newPathname);
    };
    window.addEventListener(`[${type}] navigated`, appNavigationEventHandler);

    return () =&amp;gt; {
      window.removeEventListener(
        `[${type}] navigated`,
        appNavigationEventHandler
      );
    };
  }, [basename, location, navigate, type]);

  useEffect(() =&amp;gt; {
    if (location.pathname.startsWith(basename)) {
      window.dispatchEvent(
        new CustomEvent(&quot;[app-shell] navigated&quot;, {
          detail: location.pathname.replace(basename, &quot;&quot;),
        })
      );
    }
  }, [basename, location]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;useShellNavigate&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Remote App에서 Shell App의 라우팅을 발생 시켜 다른 Remote App으로 라우팅 하고자 할 경우 사용하는 훅 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596617684&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function useShellNavigate() {
  const navigate = (pathname: string) =&amp;gt; {
    window.dispatchEvent(
      new CustomEvent(&quot;[shell] navigate&quot;, {
        detail: pathname,
      })
    );
  };
  return navigate;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Use Case&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;handleClickList&lt;/span&gt; 함수는 Remote App 내부의 라우팅을, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;handleClickRemote&lt;/span&gt; 함수는 Shell App의 라우팅 이벤트를 발생 시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1755596633919&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useShellNavigate } from &quot;@mfa/shell-router&quot;;
import { useNavigate } from &quot;react-router-dom&quot;;

const Home = () =&amp;gt; {
  const shellNavigate = useShellNavigate();
  const navigate = useNavigate();

  const handleClickList = () =&amp;gt; {
    navigate(&quot;/list&quot;);
  };
  const handleClickRemote = () =&amp;gt; {
    shellNavigate(&quot;remote&quot;);
  };
  return (
    &amp;lt;div&amp;gt;
      home
      &amp;lt;button onClick={handleClickList}&amp;gt;go to list&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={handleClickRemote}&amp;gt;go to remote&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micro App의 라우팅 통합 과정은 굉장히 복잡합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 이벤트 기반의 라우팅 통합 과정에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA에 대해서 궁금하시다면 아래 포스팅을 참고해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MFA (Micro Frontend Architecture)&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1755597000324&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;MFA (Micro Frontend Architecture)&quot; data-og-description=&quot;Overview해당 포스팅은 MFA 강좌를 수강하고 MFA에 대해서 공부한 내용을 기록하기 위한 포스팅 입니다.MFA가 무엇인지 살펴보고 조직에서 MFA 도입을 고려하면 좋은 시점에 대해서 고민해보도록 하&quot; data-og-host=&quot;kangs-develop.tistory.com&quot; data-og-source-url=&quot;https://kangs-develop.tistory.com/54&quot; data-og-url=&quot;https://kangs-develop.tistory.com/54&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PfBJY/hyZyekylF4/HLKv26EAkU81uc2oaxNIl1/img.png?width=800&amp;amp;height=608&amp;amp;face=0_0_800_608,https://scrap.kakaocdn.net/dn/1g4mG/hyZC0x8D5x/7CHf9p9CT1Q8lkjn8Mbt2K/img.png?width=800&amp;amp;height=608&amp;amp;face=0_0_800_608,https://scrap.kakaocdn.net/dn/NJrU8/hyZCXIaVlm/KoILNoZ3Uut7Oq0qy5NnS0/img.png?width=1376&amp;amp;height=1046&amp;amp;face=0_0_1376_1046&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kangs-develop.tistory.com/54&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PfBJY/hyZyekylF4/HLKv26EAkU81uc2oaxNIl1/img.png?width=800&amp;amp;height=608&amp;amp;face=0_0_800_608,https://scrap.kakaocdn.net/dn/1g4mG/hyZC0x8D5x/7CHf9p9CT1Q8lkjn8Mbt2K/img.png?width=800&amp;amp;height=608&amp;amp;face=0_0_800_608,https://scrap.kakaocdn.net/dn/NJrU8/hyZCXIaVlm/KoILNoZ3Uut7Oq0qy5NnS0/img.png?width=1376&amp;amp;height=1046&amp;amp;face=0_0_1376_1046');&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;MFA (Micro Frontend Architecture)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Overview해당 포스팅은 MFA 강좌를 수강하고 MFA에 대해서 공부한 내용을 기록하기 위한 포스팅 입니다.MFA가 무엇인지 살펴보고 조직에서 MFA 도입을 고려하면 좋은 시점에 대해서 고민해보도록 하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kangs-develop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 MFA에 대해서 자세하게 알고 싶으시면 아래 후기를 통해 강의를 참고해보시면 좋을 것 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MFA 강의를 수강하고&lt;/a&gt;&lt;/blockquote&gt;
&lt;figure id=&quot;og_1755597033267&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;MFA 강의를 수강하고&quot; data-og-description=&quot;지난 7월 21일 MFA 강의 수강을 시작하면서 글을 작성했다.MFA, Monorepo 강의 수강을 시작하며 두 달이 지난 시점에서 강의를 완강했다. 부록 한 파트가 남긴 했는데 레거시 환경에서의 점진적 전환&quot; data-og-host=&quot;kangs-develop.tistory.com&quot; data-og-source-url=&quot;https://kangs-develop.tistory.com/53&quot; data-og-url=&quot;https://kangs-develop.tistory.com/53&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAav5k/hyZzCkX1wM/hBjBHCEpd3mnCVZUFfF13K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/77PQR/hyZzyv5FO5/b4UI1WMXFITIzeFFgjyBK1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/h7071/hyZC0kBLAV/M2TO3X8wRoMT6yHXq6O45k/img.png?width=286&amp;amp;height=724&amp;amp;face=0_0_286_724&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kangs-develop.tistory.com/53&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAav5k/hyZzCkX1wM/hBjBHCEpd3mnCVZUFfF13K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/77PQR/hyZzyv5FO5/b4UI1WMXFITIzeFFgjyBK1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/h7071/hyZC0kBLAV/M2TO3X8wRoMT6yHXq6O45k/img.png?width=286&amp;amp;height=724&amp;amp;face=0_0_286_724');&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;MFA 강의를 수강하고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지난 7월 21일 MFA 강의 수강을 시작하면서 글을 작성했다.MFA, Monorepo 강의 수강을 시작하며 두 달이 지난 시점에서 강의를 완강했다. 부록 한 파트가 남긴 했는데 레거시 환경에서의 점진적 전환&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kangs-develop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>MFA</category>
      <category>react</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/88</guid>
      <comments>https://kangs-develop.tistory.com/88#entry88comment</comments>
      <pubDate>Tue, 19 Aug 2025 18:51:28 +0900</pubDate>
    </item>
    <item>
      <title>Rollup manualChunks를 활용한 브라우저 캐싱 전략</title>
      <link>https://kangs-develop.tistory.com/87</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Vite는 번들러로 Rollup을 사용합니다.&lt;br&gt;Rollup에는 manualChunks라는 옵션이 존재하는데, 해당 옵션을 사용하면 chunk 파일을 전략적으로 분리할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 chunk파일을 분리해야 할까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;manualChunks를 사용하지 않고 빌드를 할 시 프로젝트에서 사용하는 모든 라이브러리 파일이 하나의 chunk파일로 번들링 되어 떨어집니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhc5me/btsPO8IzAsO/HMK58Ek3mlPYjI0xFpHv6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhc5me/btsPO8IzAsO/HMK58Ek3mlPYjI0xFpHv6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhc5me/btsPO8IzAsO/HMK58Ek3mlPYjI0xFpHv6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhc5me%2FbtsPO8IzAsO%2FHMK58Ek3mlPYjI0xFpHv6K%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;529&quot; height=&quot;151&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이 경우 프로젝트가 커질수록 사용자가 플랫폼에 접근할 때 리소스를 다운로드 받기 위해 기다리는 시간이 기하수급적으로 늘어납니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 경우 manualChunks 기능을 활용해 라이브러리 단위로 chunk 파일을 분리하기만 해도 초기 접근 당시 필요한 파일만을 불러와 로딩 시간을 단축할 수 있어서 사용자 경험에 유리합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cobDgV/btsPR5Kp42Z/kOHvc0cHwh5ZllClVmqfo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cobDgV/btsPR5Kp42Z/kOHvc0cHwh5ZllClVmqfo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cobDgV/btsPR5Kp42Z/kOHvc0cHwh5ZllClVmqfo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcobDgV%2FbtsPR5Kp42Z%2FkOHvc0cHwh5ZllClVmqfo1%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;538&quot; height=&quot;208&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;chunk 파일을 분리하는 방법은 manualChunks 외에 한 가지 더 존재합니다.&lt;br&gt;Code Splitting 기술을 활용해 chunk 파일을 분리하는 것 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;React에는 lazy라는 API를 제공합니다. 해당 API를 사용하면 Lazy Loading 이라는 Code Splitting 기술을 활용할 수 있고 분리된 chunk 파일은 사용자가 해당 컴포넌트에 접근할 경우 로딩이 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;관련 내용은 다음의 글을 확인해주세요.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/23&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Vite 프로젝트 번들링 최적화 (manualChunks)&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;manualChunks를 활용한 브라우저 캐싱 전략&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 본론으로 돌아가서 manualChunks를 활용한 브라우저 캐싱 전략에 대해서 살펴보겠습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;브라우저 캐싱 전략은 사용하는 브라우저 마다 다르게 존재합니다.&lt;br&gt;아래 캐싱 전략은 Chrome 브라우저를 기본으로 작성된 전략임을 참고해주시기 바랍니다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;기본적으로 브라우저는 메모리 캐시와 HTTP를 활용해 캐시 된 리소스를 사용합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;manualChunks를 활용해 배포 환경에서 사용중인 라이브러리 chunk 파일과, 변경되지 않은 컴포넌트 chunk 파일을 캐싱하여 사용자에게 제공하고, 변경된 컴포넌트 chunk 파일만 제공할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제를 통해 살펴보기&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제 환경을 구성하여 살펴보도록 하겠습니다.&lt;br&gt;Vite 프로젝트를 생성하고, AWS S3 버킷에 빌드된 파일을 올려 정적 웹 사이트 호스팅을 통해 배포합니다.&lt;br&gt;개발 &amp;gt; 빌드 &amp;gt; 배포 사이클에서 실제 프로덕션과 유사한 과정으로 살펴보기 위해 Github Actions를 활용해 CI/CD 파이프라인을 간단하게 구축합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 최초로 소스 코드를 수정하여 master 브랜치에 push 합니다.&lt;br&gt;Github Actions에서 커밋을 감지하여 CI/CD Workflow가 작동합니다. 프로젝트를 빌드 후 S3 Bucket에 빌드된 파일을 밀어넣습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsafzh/btsPP91HBdd/eHdqvxut8zhh2K9UlbIf6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsafzh/btsPP91HBdd/eHdqvxut8zhh2K9UlbIf6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsafzh/btsPP91HBdd/eHdqvxut8zhh2K9UlbIf6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdsafzh%2FbtsPP91HBdd%2FeHdqvxut8zhh2K9UlbIf6K%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;1217&quot; height=&quot;801&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;801&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;711&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qDMUK/btsPRwapURw/0iw7opkzJjGSxEWd9W2AAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qDMUK/btsPRwapURw/0iw7opkzJjGSxEWd9W2AAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qDMUK/btsPRwapURw/0iw7opkzJjGSxEWd9W2AAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqDMUK%2FbtsPRwapURw%2F0iw7opkzJjGSxEWd9W2AAk%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;711&quot; height=&quot;495&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;빌드 후 배포된 웹 사이트를 살펴봅니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;vendor-scheduler-[hash].js&lt;/b&gt;&lt;b&gt; chunk&lt;/b&gt; 파일이 디스크에 캐시된 것을 확인할 수 있습니다.&lt;br&gt;시간이 지나 캐싱이 무효화 됐는지 다른 chunk 파일은 캐싱되지 않은 상태로 응답 받았습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAENgG/btsPQCWRIyt/5XKwLSHV9sxsc0mGxjlSQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAENgG/btsPQCWRIyt/5XKwLSHV9sxsc0mGxjlSQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAENgG/btsPQCWRIyt/5XKwLSHV9sxsc0mGxjlSQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAENgG%2FbtsPQCWRIyt%2F5XKwLSHV9sxsc0mGxjlSQk%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;826&quot; height=&quot;452&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두 번째 상황을 확인해보기 위해 소스 코드를 수정 후 master 브랜치에 커밋합니다. 마찬가지로 CI/CD Workflow가 작동합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NdkUN/btsPRqPaKju/gjl7PmB4AX95TBOOwSDIX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NdkUN/btsPRqPaKju/gjl7PmB4AX95TBOOwSDIX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NdkUN/btsPRqPaKju/gjl7PmB4AX95TBOOwSDIX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNdkUN%2FbtsPRqPaKju%2Fgjl7PmB4AX95TBOOwSDIX1%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;998&quot; height=&quot;682&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;682&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;700&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zJWod/btsPPPaZDLM/pVEa0zC7VTnnEgckKPkcJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zJWod/btsPPPaZDLM/pVEa0zC7VTnnEgckKPkcJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zJWod/btsPPPaZDLM/pVEa0zC7VTnnEgckKPkcJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzJWod%2FbtsPPPaZDLM%2FpVEa0zC7VTnnEgckKPkcJk%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;700&quot; height=&quot;509&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;component-one 파일의 내용만 수정했기에 chunk 파일 내용에는 변화가 없습니다. 하지만 chunk 파일의 hash 값을 살펴보면 &lt;b&gt;vendor chunk는 변화가 없고 &lt;/b&gt;index&lt;b&gt;, &lt;/b&gt;component-one&lt;b&gt; chunk 파일의 hash 값만 변화&lt;/b&gt;한 것을 살펴볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dglHoU/btsPRo4L4mW/KuFkxP8lK8UHb7ieKKcPO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dglHoU/btsPRo4L4mW/KuFkxP8lK8UHb7ieKKcPO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dglHoU/btsPRo4L4mW/KuFkxP8lK8UHb7ieKKcPO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdglHoU%2FbtsPRo4L4mW%2FKuFkxP8lK8UHb7ieKKcPO1%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;830&quot; height=&quot;448&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;배포 환경에서 살펴봅니다. 웹 사이트에 접근하니 index, component-one chunk 파일은 새로운 리소스로 응답합니다. 반대로 vendor chunk 파일은 변화가 없기에 HTTP 캐싱 전략을 사용해 응답한 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;마지막으로 Code Splitting까지 확인해보겠습니다.&lt;br&gt;소스 코드에 component-two 파일을 생성한 후 커밋 합니다. CI/CD Workflow가 작동하고 S3 Bucket에 빌드된 결과물이 업로드 되어 배포됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CASkM/btsPP82L4n6/rDNYGqrxk23nGQ19NzduZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CASkM/btsPP82L4n6/rDNYGqrxk23nGQ19NzduZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CASkM/btsPP82L4n6/rDNYGqrxk23nGQ19NzduZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCASkM%2FbtsPP82L4n6%2FrDNYGqrxk23nGQ19NzduZk%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;648&quot; height=&quot;550&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;배포 환경을 살펴봅니다. click open Component2 버튼을 클릭해야 component-two 컴포넌트가 랜더링됩니다.&lt;br&gt;초기에 해당 컴포넌트를 사용하지 않으니 (Lazy Loading) chunk 파일을 요청하지 않습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baYeuy/btsPSO9c4KO/Kw0zfBNdh5vUqWT7Uqvny1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baYeuy/btsPSO9c4KO/Kw0zfBNdh5vUqWT7Uqvny1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baYeuy/btsPSO9c4KO/Kw0zfBNdh5vUqWT7Uqvny1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaYeuy%2FbtsPSO9c4KO%2FKw0zfBNdh5vUqWT7Uqvny1%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;830&quot; height=&quot;446&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;버튼을 클릭한 후 입니다. 컴포넌트를 랜더링 하기 위해 component-two chunk 파일을 요청합니다.&lt;br&gt;이때 chunk 파일은 사용자가 배포된 후 처음 확인하는 컴포넌트임에 캐싱이 적용되지 않았음을 확인할 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3e2Qz/btsPQB4JQkv/JKl7mafeocsQ08CIzizoCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3e2Qz/btsPQB4JQkv/JKl7mafeocsQ08CIzizoCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3e2Qz/btsPQB4JQkv/JKl7mafeocsQ08CIzizoCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3e2Qz%2FbtsPQB4JQkv%2FJKl7mafeocsQ08CIzizoCk%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;865&quot; height=&quot;484&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;chunk 파일 명칭 전략&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 생성한 chunk 파일은 일반적으로 파일 명 뒤 hash를 가지고 있습니다.&lt;br&gt;그 hash 값을 기반으로 다른 chunk 파일이 생성되고, 캐시가 무효화 되고 새로운 리소스를 요청하는데, 만약 hash를 부여하고 싶지 않은 경우 어떻게 해야할까요?&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 Rollup의 &lt;b&gt;chunkFileNames&lt;/b&gt;을 활용하는 것 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://rollupjs.org/configuration-options/#output-chunkfilenames&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;Rollup chunkFileNames&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Configuration Options | Rollup&quot; data-ke-align=&quot;alignCenter&quot; data-og-host=&quot;rollupjs.org&quot; data-og-source-url=&quot;https://rollupjs.org/configuration-options/#output-chunkfilenames&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/MRZaK/hyZvuuemja/AAAAAAAAAAAAAAAAAAAAAA25EbU7GODf3bwhKKQb99ZWcezG4Fw5KzGbSGKfkfkC/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1756652399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Zx6JL0LyA2srTorR%2BQwGmWD%2B%2FKM%3D&quot; data-og-url=&quot;https://rollupjs.org/configuration-options/#output-chunkfilenames&quot;&gt;&lt;a href=&quot;https://rollupjs.org/configuration-options/#output-chunkfilenames&quot; target=&quot;_blank&quot; data-source-url=&quot;https://rollupjs.org/configuration-options/#output-chunkfilenames&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/MRZaK/hyZvuuemja/AAAAAAAAAAAAAAAAAAAAAA25EbU7GODf3bwhKKQb99ZWcezG4Fw5KzGbSGKfkfkC/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1756652399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=Zx6JL0LyA2srTorR%2BQwGmWD%2B%2FKM%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Configuration Options | Rollup&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;rollupjs.org&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react-swc&quot;;

// https://vite.dev/config/
export default defineConfig({
&amp;nbsp;&amp;nbsp;plugins: [react()],
&amp;nbsp;&amp;nbsp;server: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port: 3001,
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;build: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rollupOptions: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;output: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;manualChunks: (id) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (id.includes(&quot;node_modules&quot;)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const module = id?.split(&quot;node_modules/&quot;)?.pop()?.split(&quot;/&quot;)[0];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return `vendor-${module}`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return null;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chunkFileNames: (info) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const isVendor = info.name?.startsWith(&quot;vendor&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const directory = &quot;assets/[name]&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const extensionName = &quot;.js&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const fileName = !isVendor ? &quot;[hash]&quot; : &quot;&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return `${directory}${fileName}${extensionName}`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;},
});&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 vite.config.ts 파일의 예시를 살펴보면, vendor 파일의 경우 [hash]를 사용하지 않고 파일 명을 만들도록 설정을 수정했습니다.&lt;br&gt;빌드 결과 다음과 같이 라이브러리 청크 파일 명칭에 hash가 빠진 것을 확인하실 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tXSJB/btsPSYKB4ju/B8q8SInQjiDKGAvG0R76D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tXSJB/btsPSYKB4ju/B8q8SInQjiDKGAvG0R76D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tXSJB/btsPSYKB4ju/B8q8SInQjiDKGAvG0R76D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtXSJB%2FbtsPSYKB4ju%2FB8q8SInQjiDKGAvG0R76D0%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;484&quot; height=&quot;171&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;정말 변하지 않는 라이브러리일 경우 (버전 업그레이드를 하지 않거나 자체적으로 사용하는 라이브러리) 해당 전략을 사용해 &lt;b&gt;특정 라이브러리 캐시 전략을 사용&lt;/b&gt;할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 주의해야 할 점은, 모든 라이브러리에 위와 같이 hash 값을 제외한 chunk를 만들 경우, 라이브러리 버전 업그레이드에 따른 캐시 무효화를 사용할 수 없다는 점 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 경우 사용자에게 캐싱된 chunk 파일이 프로젝트에서 사용하지 않는 버전이라면, 사용자는 의도하지 않은 버그를 마주칠 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;예를 들어봅시다. 프로젝트에서 react 18 버전을 사용 중에 있습니다. 위와 같이 라이브러리 chunk 파일에 hash를 사용하지 않을 경우 사용자는 vendor-react.js 라는 chunk파일을 내려받아 사용합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;프로젝트를 진행함에 따라 react 버전을 19 버전으로 올렸습니다. 프로젝트에서 사용하는 react의 버전은 올라갔지만 chunk 파일은 hash 값이 존재하지 않아 여전히 vendor-react.js 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;사용자는 이미 웹 사이트에 접근한 적이 있음으로 vendor-react.js 리소스를 브라우저 디스크에 캐싱중에 있습니다. 혹은 웹 서버에서 파일의 변경점이 없다고 판단해 304 Not Modified를 응답합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 사용자가 내려받은 컴포넌트 chunk 파일은 react 19를 요구하지만 현재 사용자가 사용하는 vendor-react.js chunk 파일은 react 18 버전 입니다. 만약 컴포넌트에서 react 19 API (eg. use hook)을 사용할 경우 웹 사이트에서 예외 상황이 발생해 사용자는 웹 사이트를 이용할 수 없게 됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;hash 값은 어디서 오는 것일까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Rollup 공식 문서에 의하면 hash 값은 최종적으로 생성된 chunk 파일을 기반으로 만들어집니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[hash]: A hash based only on the content of the final generated chunk, including transformations in renderChunk and any referenced file hashes. You can also set a specific hash length via e.g. [hash:10]. By default, it will create a base-64 hash. If you need a reduced character sets, see output.hashCharacters&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;manualChunks를 활용해 chunk 파일을 분리하면 다양한 이점을 취할 수 있습니다.&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;현재 Beta 테스트 중인 Rolldown 프로젝트에서는 advancedChunks라는 옵션으로 해당 기능을 제공한다고 합니다. 관련해서는 아래 내용을 참고해주세요.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://rolldown.rs/guide/in-depth/advanced-chunks#advanced-chunks&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;Rolldown advancedChunks&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Rolldown | Rust bundler for JavaScript&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;RolldownFast Rust-based bundler for JavaScript with Rollup-compatible API&quot; data-og-host=&quot;rolldown.rs&quot; data-og-source-url=&quot;https://rolldown.rs/guide/in-depth/advanced-chunks#advanced-chunks&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bMZ5KG/hyZuzJk5uK/AAAAAAAAAAAAAAAAAAAAAE8Q5nYrZgyNUoawLufZO0tn4zEtjdXd5Lty0eGI5Zoo/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1756652399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ZYvCtkkIdXUc0KJJpM18HmWHCi0%3D&quot; data-og-url=&quot;https://rolldown.rs/&quot;&gt;&lt;a href=&quot;https://rolldown.rs/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://rolldown.rs/guide/in-depth/advanced-chunks#advanced-chunks&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bMZ5KG/hyZuzJk5uK/AAAAAAAAAAAAAAAAAAAAAE8Q5nYrZgyNUoawLufZO0tn4zEtjdXd5Lty0eGI5Zoo/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1756652399&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ZYvCtkkIdXUc0KJJpM18HmWHCi0%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Rolldown | Rust bundler for JavaScript&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;RolldownFast Rust-based bundler for JavaScript with Rollup-compatible API&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;rolldown.rs&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 글을 통해 프로젝트의 고유한 캐시 전략을 활용해 사용자에게 더욱 좋은 경험을 제공할 수 있길 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/87</guid>
      <comments>https://kangs-develop.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 13 Aug 2025 15:32:49 +0900</pubDate>
    </item>
    <item>
      <title>Vercel 배포 시 Function Region</title>
      <link>https://kangs-develop.tistory.com/86</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 사이드 프로젝트를 진행하며 Frontend 배포는 Vercel을 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크를 Next.js를 사용했고 Vercel에 배포 시 가장 간단하게 배포할 수 있다는 장점이 있었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 후 당연히 Function Region을 변경했다고 생각을 했었는데, 프로덕션 환경에서의 프론트엔드가 너무 느렸습니다.&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;여러가지 확인해본 결과 프로젝트의 Vercel Function Region이 기본 값인 North America의 Washington, D.C로 설정이 되어있었습니다.&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;그럼 왜 Function Region을 변경해야 하는지,변경하는 법과 변경 전과 후 어떤 차이가 있는지 살펴보도록 하겠습니다.&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;&lt;b&gt;ssalon-de 프로젝트 인프라 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssalon-de 프로젝트의 서버는 AWS EC2에 배포된 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 VM은 AWS의 ap-norteast-2(아시아 태평양 서울) 리전에 위치하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 PaaS 서비스인 Supabase를 사용중에 있는데, 해당 DB 역시 AWS ap-norteast-2(아시아 태평양 서울) 에 위치하고 있습니다.&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;&lt;b&gt;Function Region&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel에 배포된 Next.js 프로젝트는 Serverless Function으로 실행됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;여기서 Serverless란 서버가 존재하지 않다는 것이 아닌 서버를 직접 관리하지 않아도 된다 (신경 쓰지 않는다) 입니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Serverless Function 이란 함수 단위로 제공되는 서버리스 서비스인데 각각의 요청을 함수 단위로 관리할 수 있게 됩니다.&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;Vercel에 배포된 프로젝트는 Vercel Function (Serverless Function)으로 실행이 되는데 Vercel Function이 위치할 리전(Region)은 AWS의 Region과 일치합니다.&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;다만 추가 설정을 하지 않는다면 North America의 Washington, D.C로 설정 되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvIKSu/btsPMmERhdc/KjPPokdunFV7oU5hZP1mz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvIKSu/btsPMmERhdc/KjPPokdunFV7oU5hZP1mz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvIKSu/btsPMmERhdc/KjPPokdunFV7oU5hZP1mz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvIKSu%2FbtsPMmERhdc%2FKjPPokdunFV7oU5hZP1mz1%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;934&quot; height=&quot;590&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Vercel의 Hobby Plan은 Function Region을 하나만 선택할 수 있습니다. Edge Location을 통해 여러 리전에서 사용자에게 빠른 환경을 제공하고 싶다면 (CDN 사용) Pro 플랜을 이용하실 수 있습니다.&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;본론으로 돌아가 봅시다.&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;서버와 DB는 서울 리전에, Vercel Function(서버리스 함수)는 워싱턴 리전에 위치한 상황입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에 위치한 사용자가 주소를 입력해 요청을 보내면 워싱턴에 위치한 서버리스 함수에 요청을 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버리스 함수는 페이지를 만들기 위해 서울에 위치한 서버에 요청을 보내고, 서버에서는 DB에 데이터를 질의합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQsBLX/btsPLenkJg7/hJaZFmN6MXrhrSrjq0xkrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQsBLX/btsPLenkJg7/hJaZFmN6MXrhrSrjq0xkrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQsBLX/btsPLenkJg7/hJaZFmN6MXrhrSrjq0xkrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQsBLX%2FbtsPLenkJg7%2FhJaZFmN6MXrhrSrjq0xkrk%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;798&quot; height=&quot;347&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&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;물리적으로 거리가 있는 서버간의 요청은 당연히 지연(레이턴시: Latency)가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황을 해결하려면 어떤 솔루션을 취해야할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉬운 방법은 프론트엔드 Vercel Function의 Region을 변경하는 것 입니다.&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;&lt;b&gt;Function Region 변경하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경하는 방법은 간단합니다. 먼저 배포된 프로젝트에 접속한 후 상단 메뉴 중 Settings를 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1629&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqMttE/btsPK4SHq3x/0aBapEe6o1XQkyJna3qKN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqMttE/btsPK4SHq3x/0aBapEe6o1XQkyJna3qKN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqMttE/btsPK4SHq3x/0aBapEe6o1XQkyJna3qKN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqMttE%2FbtsPK4SHq3x%2F0aBapEe6o1XQkyJna3qKN0%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;654&quot; height=&quot;363&quot; data-origin-width=&quot;1629&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&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;그리고 우측 메뉴 중 Functions를 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음의 화면을 확인할 수 있습니다. 그 중 Function Region을 변경할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCRv5B/btsPLYRKP0J/GVHRRUSmCrGFaNG5YBdJI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCRv5B/btsPLYRKP0J/GVHRRUSmCrGFaNG5YBdJI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCRv5B/btsPLYRKP0J/GVHRRUSmCrGFaNG5YBdJI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCRv5B%2FbtsPLYRKP0J%2FGVHRRUSmCrGFaNG5YBdJI0%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;1615&quot; height=&quot;889&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;889&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Function Region 중 Asia Pacific을 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Seoul, South Korea (Northeast) - ap-northeast-2 - icn1 을 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 Save 버튼을 클릭합니다.&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;Function Region을 변경하면 우측 하단에 새로운 배포가 필요하든 알림 팝업이 등장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 Redeploy 버튼을 클릭하면 새로운 리전에 배포를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PbLkI/btsPLjWorbo/H52z29KnrSr3OZbOY93ptK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PbLkI/btsPLjWorbo/H52z29KnrSr3OZbOY93ptK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PbLkI/btsPLjWorbo/H52z29KnrSr3OZbOY93ptK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPbLkI%2FbtsPLjWorbo%2FH52z29KnrSr3OZbOY93ptK%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;444&quot; height=&quot;132&quot; data-origin-width=&quot;444&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;결과&lt;/b&gt;&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;변경 전.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NO1xb/btsPJUch6UV/5aKaYRmCUmXCPBbewPckMK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NO1xb/btsPJUch6UV/5aKaYRmCUmXCPBbewPckMK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NO1xb/btsPJUch6UV/5aKaYRmCUmXCPBbewPckMK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/NO1xb/btsPJUch6UV/5aKaYRmCUmXCPBbewPckMK/img.gif&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;309&quot; data-filename=&quot;변경 전.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 요청 시간을 살펴보면 가장 오래 걸리는 요청은 최대 900ms 까지 소요가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 300ms의 요청 시간이 소요되며,실제 UI를 요청을 보낸 페이지가 랜더링이 될 때 까지 수 초의 시간을 기다려야 합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;변경 후.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJN3tM/btsPJBX3Yma/cGI18Oe7XWjXwpcLBLiJi1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJN3tM/btsPJBX3Yma/cGI18Oe7XWjXwpcLBLiJi1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJN3tM/btsPJBX3Yma/cGI18Oe7XWjXwpcLBLiJi1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cJN3tM/btsPJBX3Yma/cGI18Oe7XWjXwpcLBLiJi1/img.gif&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;315&quot; data-filename=&quot;변경 후.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&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;변경 후 상황을 살펴봅시다. 단순히 UI만 살펴봐도 속도가 빨라졌다는 것이 체감될 정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 900ms 소요되던 요청이 300ms 정도로 3배 빨라졌고, 평균 네트워크 요청 속도가 200ms 내외로 레이턴시(Latency)가 줄어들었습니다.&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;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel Function의 Region을 변경한 후 사용자로부터 플랫폼의 속도가 빨라졌다는 것이 체감된다는 피드백을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단하게 배포된 인프라의 리전만 변경해줘도 사용자가 체감할 수 있는 변화를 만들어줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel을 이용해 프로젝트를 배포한다면 꼭 한번 체크해보는 것을 어떨까요?&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>[WEB] 프론트엔드</category>
      <category>cloud</category>
      <category>deploy</category>
      <category>frontend</category>
      <category>Function Region</category>
      <category>vercel</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/86</guid>
      <comments>https://kangs-develop.tistory.com/86#entry86comment</comments>
      <pubDate>Fri, 8 Aug 2025 12:47:02 +0900</pubDate>
    </item>
    <item>
      <title>React, Memoization</title>
      <link>https://kangs-develop.tistory.com/85</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;11&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;React를 사용해 Web Application을 개발한다면 메모이제이션(Memoization) 이라는 단어는 익숙한 단어입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 메모이제이션이란 무엇인지, 메모이제이션을 통해 해결하려는 것은 무엇인지 살펴보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;166&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메모이제이션(Memoization)&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;메모이제이션의 사전적 의미는 컴퓨터 프로그램이 복잡한 함수 호출의 결과값을 함수에 저장해놓고, 같은 입력이 반복될 때 저장한 값을 반환하도록 하여 속도를 높이는 최적화 기술입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 값 비싼 연산으로 인해 도출된 값을 캐싱한 후 같은 Input일 경우 캐싱된 값을 반환한다고 생각할 수 있습니다. 그럼 이 개념이 왜 React에 적용이 되는 것 일까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;347&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;347&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React, Component, 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;React를 사용해 Web Application을 개발한다면 컴포넌트(Component)단위로 UI를 분리하고, 컴포넌트를 조립하여 결과물을 만들고는 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;496&quot; data-ke-size=&quot;size16&quot;&gt;컴포넌트에는 여러 비즈니스 로직이 포함될 수 있는데, 그 중 컴포넌트에 전달되는 props와 컴포넌트에서 소유하는 state는 값이 변경될 때 컴포넌트의 랜더링이 발생합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;496&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;595&quot; data-ke-size=&quot;size16&quot;&gt;props와 state가 변경될 때 마다 해당 값을 참조하고 있는 컴포넌트는 반드시 랜더링이 발생합니다. 이때 발생되는 랜더링은 불필요한 랜더링 일수도 있습니다. 예시를 들어보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;595&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;595&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1750237387798&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type CheckboxProps = React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; &amp;amp; {
  label: string;
};

const Checkbox: React.FC&amp;lt;CheckboxProps&amp;gt; = ({ label, ...props }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input type=&quot;checkbox&quot; {...props} /&amp;gt;
      &amp;lt;label htmlFor={props.id}&amp;gt;{label}&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Checkbox;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1750237394264&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import Checkbox from &quot;./checkbox&quot;;

const Test = () =&amp;gt; {
  const [value, setValue] = useState({ id: &quot;&quot;, checked: false });

  const handleChangeCheckbox = (e: React.SyntheticEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    const id = e.currentTarget.id;
    const checked = e.currentTarget.checked;

    setValue({ id, checked });
  };

  const isChecked = (id: string) =&amp;gt; id === value.id &amp;amp;&amp;amp; value.checked;

  return (
    &amp;lt;div&amp;gt;
      {options.map((option) =&amp;gt; (
        &amp;lt;Checkbox
          id={option.id}
          key={option.id}
          label={option.label}
          checked={isChecked(option.id)}
          onChange={handleChangeCheckbox}
        /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};

export default Test;&lt;/code&gt;&lt;/pre&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-filename=&quot;1.gif&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AtHoc/btsOFULnOps/N5wnCyOFnBqr4lgDnDjlq1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AtHoc/btsOFULnOps/N5wnCyOFnBqr4lgDnDjlq1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AtHoc/btsOFULnOps/N5wnCyOFnBqr4lgDnDjlq1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/AtHoc/btsOFULnOps/N5wnCyOFnBqr4lgDnDjlq1/img.gif&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;206&quot; height=&quot;288&quot; data-filename=&quot;1.gif&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;우리는 Option 4 체크박스를 클릭했고 해당 체크박스만 랜더링이 발생하는 것을 기대하고 있습니다. 하지만 우리의 의도와는 다르게 모든 체크박스 컴포넌트에서 랜더링이 발생합니다. 이는 불필요한 랜더링입니다.&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;2.gif&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SqAAl/btsOFIKLP7z/7WsvGvOFeJG9nf2EeQMQAK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SqAAl/btsOFIKLP7z/7WsvGvOFeJG9nf2EeQMQAK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SqAAl/btsOFIKLP7z/7WsvGvOFeJG9nf2EeQMQAK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/SqAAl/btsOFIKLP7z/7WsvGvOFeJG9nf2EeQMQAK/img.gif&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;688&quot; height=&quot;288&quot; data-filename=&quot;2.gif&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1881&quot; data-ke-size=&quot;size16&quot;&gt;랜더링이 발생하는 원인을 분석해보기 위해 React Profiler을 활용해봅시다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1881&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1929&quot; data-ke-size=&quot;size16&quot;&gt;각 랜더링이 발생한 원인을 살펴보니 Props가 변경됐고, 불필요한 랜더링이 발생한 컴포넌트는 onChange 함수로 인해 변경이 됐다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2022&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2024&quot; data-ke-size=&quot;size16&quot;&gt;props로 전달되는 onChange 함수인 handleChangeCheckbox 함수가 변경됐고, 이로 인해 다른 Checkbox의 리랜더링이 발생합니다. 그럼 handleChangeCheckbox 함수는 왜 변경이 됐을지 생각해봅시다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2024&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2159&quot; data-ke-size=&quot;size16&quot;&gt;handleChangeCheckbox 함수를 살펴보면 setValue를 통해 state 값을 변경하고 있습니다. 이로 인해 Test 컴포넌트가 리랜더링이 되며 리랜더링이 될 때 handleChangeCheckbox 함수가 생성이 됩니다. handleChangeCheckbox 함수가 변경이 됐고 props로 전달되는 값이 변경이 됐기에 Checkbox 컴포넌트가 리랜더링이 발생합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2159&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2376&quot; data-ke-size=&quot;size16&quot;&gt;위 과정(불필요한 리랜더링)을 최적화 해봅시다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2376&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2376&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;memo, useCallback&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2425&quot; data-ke-size=&quot;size16&quot;&gt;React에서 제공하는 메모이제이션 API인 memo 함수와 함수를 메모이제이션 할 수 있는 useCallback 훅을 사용합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2425&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2500&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #1868db;&quot; href=&quot;https://ko.react.dev/reference/react/memo&quot; data-renderer-mark=&quot;true&quot;&gt;memo&lt;/a&gt; 훅은 얕은 비교를 통해 컴포넌트의 리랜더링을 최적화합니다. &lt;a style=&quot;color: #1868db;&quot; href=&quot;https://ko.react.dev/reference/react/useCallback&quot; data-renderer-mark=&quot;true&quot;&gt;useCallback&lt;/a&gt; 훅은 컴포넌트 랜더링 시 생성된 함수를 메모이제이션 하고, 리랜더링 시 Dependency Array의 값이 변경됐는지 체크한 다음 변경되지 않았다면 메모이제이션 된 함수를 재사용합니다.&lt;/p&gt;
&lt;blockquote data-renderer-start-pos=&quot;2500&quot; data-ke-style=&quot;style3&quot;&gt;useCallback 구현체를 살펴보고 싶다면 다음 글을 확인해주세요.&lt;br /&gt;&lt;a href=&quot;https://velog.io/@woohm402/useCallback-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useCallback 알아보기&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;리랜더링 최적화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;우리가 최적화 하고 싶은 컴포넌트는 Checkbox 컴포넌트 입니다. 해당 컴포넌트를 memo로 감싸서 export 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750237483678&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo } from &quot;react&quot;;

type CheckboxProps = React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; &amp;amp; {
  label: string;
};

const Checkbox: React.FC&amp;lt;CheckboxProps&amp;gt; = ({ label, ...props }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input type=&quot;checkbox&quot; {...props} /&amp;gt;
      &amp;lt;label htmlFor={props.id}&amp;gt;{label}&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default memo(Checkbox);&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;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;값이 변경될 때 Test 컴포넌트의 리랜더링이 발생한다고 했습니다. 이때 Checkbox 컴포넌트의 onChange Props로 넘겨주는 handleChangeCheckbox 함수가 다시 생성되지 않도록 (메모이제이션 된 값을 사용하도록) 수정해봅시다!&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750237495914&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import Checkbox from &quot;./checkbox&quot;;

const Test = () =&amp;gt; {
  const [value, setValue] = useState({ id: &quot;&quot;, checked: false });

  const handleChangeCheckbox = useCallback(
    (e: React.SyntheticEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      const id = e.currentTarget.id;
      const checked = e.currentTarget.checked;
      setValue({ id, checked });
    },
    [setValue]
  );

  const isChecked = (id: string) =&amp;gt; id === value.id &amp;amp;&amp;amp; value.checked;

  return (
    &amp;lt;div&amp;gt;
      {options.map((option) =&amp;gt; (
        &amp;lt;Checkbox
          id={option.id}
          key={option.id}
          label={option.label}
          checked={isChecked(option.id)}
          onChange={handleChangeCheckbox}
        /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};

export default Test;&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;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;handleChangeCheckbox 함수를 useCallback 훅의 첫 번째 인자로 넣어줬습니다. 훅의 두 번째 인자로 함수에서 의존하고 있는 외부 값을 넣어줍니다. 해당 함수에서는 setValue를 의존하고 있기 때문에 의존성 배열에 추가했습니다.&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;최적화를 진행했으니 결과를 살펴보겠습니다.&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;3.gif&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U7dLi/btsOGViafJe/midbR3kJYiFNKBBELPUYs1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U7dLi/btsOGViafJe/midbR3kJYiFNKBBELPUYs1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U7dLi/btsOGViafJe/midbR3kJYiFNKBBELPUYs1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/U7dLi/btsOGViafJe/midbR3kJYiFNKBBELPUYs1/img.gif&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;422&quot; height=&quot;322&quot; data-filename=&quot;3.gif&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4337&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 모든 체크박스가 리랜더링 됐지만, 최적화 이후에는 실제 값이 변경된 컴포넌트만 리랜더링이 발생했습니다. 의도한 바가 정확히 적용된 것 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4337&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4423&quot; data-ke-size=&quot;size16&quot;&gt;여기서 궁금한 점이 발생했습니다. Checkbox 컴포넌트에 isChecked 함수를 사용해 값을 넘겨주고 있는데 isChecked 함수에는 메모이제이션을 적용하지 않았습니다. 그럼에도 리랜더링이 발생하지 않았구요. 어떤 차이가 있는지 궁금하지 않나요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4423&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4423&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스칼라 값&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4575&quot; data-ke-size=&quot;size16&quot;&gt;이 현상을 이해하기 위해선 JavaScript 언어에 대해서 이해해야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4575&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4620&quot; data-ke-size=&quot;size16&quot;&gt;JavaScript에서 스칼라 값(원시 값)은 string, number, boolean, symbol 값 입니다. 해당 값들은 값 비교 시 리터럴 그 자체를 사용해 값을 비교합니다. 반면에 스칼라 값이 아닌 값은 JavaScript에서 Object입니다. 우리가 자주 사용하는 배열, 함수, 그 외 객체 모든 리터럴(값)은 Object 형태입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4620&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4817&quot; data-ke-size=&quot;size16&quot;&gt;Object는 값 비교 시 주소 값을 통해 비교됩니다. 예를 들어 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750237545846&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const scala1 = 1;
const scala2 = 1;

const object1 = {
  a: 1,
};

const object2 = {
  a: 1,
};

const object3 = object1;

console.log(scala1 === scala2);
console.log(object1 === object2);
console.log(object1 === object3);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5092&quot; data-ke-size=&quot;size16&quot;&gt;scala1과 scala2는 모두 number 타입의 스칼라 값이며 리터럴은 1 입니다. 값 비교 시 1 === 1 로 true를 반환합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5092&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5173&quot; data-ke-size=&quot;size16&quot;&gt;반면에 object1과 object2는 객체(Object) 타입이며 같은 형태를 가지고 있습니다. 하지만 런타임 시 객체는 메모리에 생성되고 메모리 상에서 object1, object2 변수는 해당 객체의 주소 값을 가지고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5173&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5305&quot; data-ke-size=&quot;size16&quot;&gt;따라서 object1과 object2을 비교했을 때 (15행) 서로 다른 주소 값을 가지고 있기에 false를 반환합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5305&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5375&quot; data-ke-size=&quot;size16&quot;&gt;반대로 object3은 object1의 주소 값을 할당 받았습니다. 따라서 같은 메모리 주소를 바라보고 있고, object1과 object3을 비교했을 때 true를 반환합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5375&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5375&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;우리는 스칼라 값을 비교할 때 값 자체를 비교한다는 것을 확인했습니다. 그럼 isChecked를 왜 메모이제이션 하지 않아도 리랜더링이 발생하지 않는지 살펴봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750237565880&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 생략

  return (
    &amp;lt;div&amp;gt;
      {options.map((option) =&amp;gt; (
        &amp;lt;Checkbox
          id={option.id}
          key={option.id}
          label={option.label}
          checked={isChecked(option.id)}
          onChange={handleChangeCheckbox}
        /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5856&quot; data-ke-size=&quot;size16&quot;&gt;checked props를 살펴봅시다 isChecked 함수에 &lt;a style=&quot;color: #1868db;&quot; href=&quot;http://option.id/&quot; data-renderer-mark=&quot;true&quot;&gt;option.id&lt;/a&gt; 값을 넘겨주어 실행하고 있습니다. isChecked 함수는 실행되어 스칼라 값인 boolean을 리턴합니다. 따라서 checked props에 넘겨지는 값은 boolean 값 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;5856&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6005&quot; data-ke-size=&quot;size16&quot;&gt;메모이제이션 된 컴포넌트는 얕은 비교를 통해 리랜더링 합니다. scala 값인 checked는 실제 값이 변경되지 않았다면 이전과 같은 값이기에 비교 결과 true를 리턴합니다. 그렇기 때문에 isChecked 함수를 메모이제이션 하지 않아도 리랜더링이 발생하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6005&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6005&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어떤 값을 메모이제이션 해야할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6182&quot; data-ke-size=&quot;size16&quot;&gt;메모이제이션을 활용해 최적화 하는 과정을 살펴봤습니다. 이 과정을 통해 우리는 메모이제이션을 하는 목적은 랜더링 최적화를 위해서 라는 것을 알게 됐습니다. 그럼 어떤 값을 메모이제이션 하는 것이 좋을까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6182&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6299&quot; data-ke-size=&quot;size16&quot;&gt;얕은 비교 시 객체는 메모리의 주소 값으로 비교합니다. 컴포넌트가 리랜더링이 된 후 객체가 다시 생성 된다면 메모리의 주소가 달라지기 때문에 얕은 비교에서 항상 리랜더링을 발생 시킵니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6299&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6406&quot; data-ke-size=&quot;size16&quot;&gt;따라서 객체의 값은 메모이제이션을 통해 의존성 배열이 변경되지 않는 경우 객체의 값은 메모이제이션 된 값을 사용하도록 하는 것이 좋습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6406&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6485&quot; data-ke-size=&quot;size16&quot;&gt;반대로 스칼라 값은 얕은 비교 시 값 그 자체로 비교합니다. 따라서 메모이제이션을 하지 않아도 항상 동일한 값을 반환하기 때문에 부모 컴포넌트에서 리랜더링이 발생해도 해당 값(props)으로 인한 리랜더링이 발생하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6485&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6485&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;값 비싼 연산의 최적화&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6629&quot; data-ke-size=&quot;size16&quot;&gt;스칼라 값은 값 그 자체로 비교하기 때문에 메모이제이션을 하지 않아도 된다고 했습니다. 하지만 해당 값을 연산하는 과정으로 인해 UI 블로킹이 발생한다면 어떨까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6629&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6722&quot; data-ke-size=&quot;size16&quot;&gt;예시를 들어보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750237603332&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const veryExpensiveCompute = (count: number) =&amp;gt; {
  // 복잡한 연산을 시뮬레이션
  let sum = 0;
  for (let i = count; i &amp;lt; 1e6; i++) {
    for (let j = 0; j &amp;lt; 1000; j++) {
      // 이중 루프를 통해 복잡한 연산을 시뮬레이션
      sum += i + j;
    }
  }
  return sum;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6722&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;6722&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;우리가 사용할 veryExpensiveCompute 위 함수를 사용해 number 타입의 스칼라 값을 반환 받습니다. 해당 함수는 이중 루프이며 엄청나게 많은 연산을 수행하고 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750237614056&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback, useMemo, useState } from &quot;react&quot;;
import Checkbox from &quot;./checkbox&quot;;

const options = [
  { id: &quot;option1&quot;, label: &quot;option 1&quot; },
  { id: &quot;option2&quot;, label: &quot;option 2&quot; },
  { id: &quot;option3&quot;, label: &quot;option 3&quot; },
  { id: &quot;option4&quot;, label: &quot;option 4&quot; },
];

const Test = () =&amp;gt; {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState({ id: &quot;&quot;, checked: false });

  const handleChangeCheckbox = useCallback(
    (e: React.SyntheticEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      const id = e.currentTarget.id;
      const checked = e.currentTarget.checked;

      setValue({ id, checked });
    },
    [setValue]
  );

  const isChecked = (id: string) =&amp;gt; id === value.id &amp;amp;&amp;amp; value.checked;

  const computedValue = veryExpensiveCompute(count);
  console.log(&quot;Computed Value:&quot;, computedValue);

  return (
    &amp;lt;div&amp;gt;
      {options.map((option) =&amp;gt; (
        &amp;lt;Checkbox
          id={option.id}
          key={option.id}
          label={option.label}
          checked={isChecked(option.id)}
          onChange={handleChangeCheckbox}
        /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};

export default Test;&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;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;지금까지 최적화 한 컴포넌트에서 해당 값을 연산하고 console을 통해 출력해보도록 합니다.&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;4.gif&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oDMPA/btsOHEz1qMO/PClu1UviKkhuaU4CGgBb4k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oDMPA/btsOHEz1qMO/PClu1UviKkhuaU4CGgBb4k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oDMPA/btsOHEz1qMO/PClu1UviKkhuaU4CGgBb4k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/oDMPA/btsOHEz1qMO/PClu1UviKkhuaU4CGgBb4k/img.gif&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;454&quot; height=&quot;476&quot; data-filename=&quot;4.gif&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8263&quot; data-ke-size=&quot;size16&quot;&gt;컴포넌트가 랜더링 될 때 이미 값이 연산 됐습니다. 클릭 이벤트가 발생할 때 Test 컴포넌트의 상태가 변경되어 리랜더링이 발생하고 veryExpensiveCompute 함수 통해 값을 다시 연산합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8263&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8378&quot; data-ke-size=&quot;size16&quot;&gt;이때 연산 과정이 JavaScript의 메인 스레드를 점유하게 되고 이로 인해 사용자의 이벤트로 인한 UI 랜더링이 블로킹되는 현상이 발생합니다. (UI Blocking)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8378&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8475&quot; data-ke-size=&quot;size16&quot;&gt;이를 막기 위해 우리는 메모이제이션을 사용할 수 있습니다. 메모이제이션을 사용해 컴포넌트의 리랜더링 시 이전에 생성된 값을 재사용 하는 것 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8475&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8475&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;useMemo&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;8475&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;useMemo 훅을 사용해 값을 메모이제이션 합시다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750237648813&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback, useMemo, useState } from &quot;react&quot;;
import Checkbox from &quot;./checkbox&quot;;

const options = [
  { id: &quot;option1&quot;, label: &quot;option 1&quot; },
  { id: &quot;option2&quot;, label: &quot;option 2&quot; },
  { id: &quot;option3&quot;, label: &quot;option 3&quot; },
  { id: &quot;option4&quot;, label: &quot;option 4&quot; },
];

const Test = () =&amp;gt; {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState({ id: &quot;&quot;, checked: false });

  const handleChangeCheckbox = useCallback(
    (e: React.SyntheticEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      const id = e.currentTarget.id;
      const checked = e.currentTarget.checked;

      setValue({ id, checked });
    },
    [setValue]
  );

  const isChecked = (id: string) =&amp;gt; id === value.id &amp;amp;&amp;amp; value.checked;

  const computedValue = useMemo(() =&amp;gt; veryExpensiveCompute(count), [count]);
  console.log(&quot;Computed Value:&quot;, computedValue);

  return (
    &amp;lt;div&amp;gt;
      {options.map((option) =&amp;gt; (
        &amp;lt;Checkbox
          id={option.id}
          key={option.id}
          label={option.label}
          checked={isChecked(option.id)}
          onChange={handleChangeCheckbox}
        /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
};

export default Test;&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;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;그리고 결과를 살펴봅니다.&lt;/span&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-filename=&quot;5.gif&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yheQW/btsOFMl2Mq4/tkRsOKK2WKKYrygJxd9Xf0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yheQW/btsOFMl2Mq4/tkRsOKK2WKKYrygJxd9Xf0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yheQW/btsOFMl2Mq4/tkRsOKK2WKKYrygJxd9Xf0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/yheQW/btsOFMl2Mq4/tkRsOKK2WKKYrygJxd9Xf0/img.gif&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;514&quot; height=&quot;584&quot; data-filename=&quot;5.gif&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9764&quot; data-ke-size=&quot;size16&quot;&gt;컴포넌트 최초 랜더링(마운트) 시 연산을 수행하고 해당 값을 메모이제이션 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9764&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9811&quot; data-ke-size=&quot;size16&quot;&gt;그리고 Checkbox가 클릭된 후 메모이제이션 된 값을 재사용 하기 때문에 (연산 과정을 스킵) 이전에 발생한 UI Blocking 현상이 발생하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9811&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9902&quot; data-ke-size=&quot;size16&quot;&gt;물론 최초 랜더링 시 버벅거림 (UI Blocking) 현상은 메모이제이션을 통해 최적화 할 수 없습니다. 반드시 한번은 연산을 실행해야 값을 도출할 수 있으니깐요. 만약 실제 프로젝트에서 이런 문제가 발생한다면 근본적으로 문제를 해결하기 위해서 워커 스레드를 사용하여 문제를 해결하는 것도 좋은 방안일 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9902&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9902&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메모이제이션 시 주의해야 할 점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10102&quot; data-ke-size=&quot;size16&quot;&gt;메모이제이션을 통해 성능 최적화를 할 때 고려해야 할 몇 가지 사항이 있습니다. 먼저 최적화에는 언제나 트레이드 오프가 존재한다는 것 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10102&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10184&quot; data-ke-size=&quot;size16&quot;&gt;메모이제이션은 컴퓨트 된 값을 캐싱하여 재사용하는 방식입니다. 이 방식은 컴퓨트 시간을 줄여주지만 캐싱을 위해 메모리 공간을 점유합니다. 따라서 무분별한 메모이제이션 보다는 랜더링 퍼포먼스를 측정하고 불필요한 리랜더링이 발생하는 변수를 최적화 하는 것이 중요합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10184&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10334&quot; data-ke-size=&quot;size16&quot;&gt;또한 값 자체를 메모이제이션 하는 것 만으로는 랜더링 최적화를 진행할 수 없는 케이스도 존재합니다. 이런 경우에 랜더링을 유발하는 값이 적절하게 설계 되었는지를 다시 한번 검토한 후 로직을 적절하게 분리하여 랜더링 최적화를 진행하는 것이 중요합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10334&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10334&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10482&quot; data-ke-size=&quot;size16&quot;&gt;이번 글을 통해 메모이제이션을 통해 React의 랜더링 최적화를 하는 방식에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10482&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10538&quot; data-ke-size=&quot;size16&quot;&gt;최근 React 19 버전에는 메모이제이션을 자동으로 해주는 React Compiler(Forget)이 등장했습니다. 아직 RC 단계이지만 React Compiler을 통해 메모이제이션에 들어가는 리소스를 최적화 할 수 있다면 React의 DX가 한층 상승하지 않을까 생각하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10538&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;10700&quot; data-ke-size=&quot;size16&quot;&gt;읽어봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>memoization</category>
      <category>react</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/85</guid>
      <comments>https://kangs-develop.tistory.com/85#entry85comment</comments>
      <pubDate>Wed, 18 Jun 2025 18:11:36 +0900</pubDate>
    </item>
    <item>
      <title>[React] 제어 컴포넌트, 비제어 컴포넌트, 그리고 폼 다루기</title>
      <link>https://kangs-develop.tistory.com/83</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 애플리케이션은 컴포넌트 단위로 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 컴포넌트 중에서도 폼(Form) 컴포넌트가 존재합니다. 폼 컴포넌트는 사용자에게 값을 입력 받아 요청을 처리할 수 있는 중요한 역할을 하는 컴포넌트 입니다.&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;React에서 폼 컴포넌트를 다루기 위한 방법은 크게 두 가지가 있습니다. 제어 컴포넌트, 비제어 컴포넌트 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 제어 컴포넌트는 무엇이고, 비제어 컴포넌트는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글에서는 제어 컴포넌트, 비제어 컴포넌트의 차이를 알아보고 react-hook-form 라이브러리를 사용해 비제어 컴포넌트를 살펴보도록 하겠습니다.&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;&lt;b&gt;잠깐! JavaScript 없이 form을 다루는 법을 아시나요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript를 사용하지 않고 폼을 다루는 방법을 아시나요? 최근 면접에서 해당 질문을 받고 당황한 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발자로 근무하며 JavaScript 없는 프론트엔드는 상상할 수 없었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript 없이 단순히 HTML 태그만을 사용해 폼을 다룰 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748774735966&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;test-form&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;form method=&quot;post&quot; action=&quot;/submit&quot;&amp;gt;
      &amp;lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;Name&quot; /&amp;gt;
      &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;Email&quot; /&amp;gt;
      &amp;lt;input name=&quot;password&quot; placeholder=&quot;Password&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;submit&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&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;form 태그의 method, action에 집중해봅시다. method 속성은 어떤 메서드로 서버에 요청을 보낼지,&lt;span style=&quot;background-color: #ffffff; color: #575757; text-align: left;&quot;&gt;&amp;nbsp;action 속성은 폼&amp;nbsp;데이터(form data)를 서버로 보낼 때 해당 데이터가 도착할 URL을 명시합니다.&lt;/span&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;위 HTML 문서에서 각 input에 값을 입력한 후 submit button을 클릭하면 post 요청으로 /submit에 폼 데이터를 전송합니다. 그리고 서버는 application/x-www-form-urlencoded 혹은 multipart/form-data 형식으로 데이터를 받게 됩니다.&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;요즘은 입력된 값을 검증하거나 ajax(fetch를 사용한) 요청을 위해 JavaScript를 사용해 폼 컴포넌트를 만들고 있습니다. 그래도 HTML 태그만을 사용해 폼을 다룰수 있다는 것도 알고 있으면 좋을것 같습니다.&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;&lt;b&gt;제어 컴포넌트&lt;/b&gt;&lt;/h3&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;b&gt;React&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React로 개발을 하다보면 무수히 많은 State와 Props를 사용합니다. 그리고 State, Props는 React 라이브러리에 의해 재조정(리랜더링)을 트리거 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;State, Props를 통해 컴포넌트가 React에 제어된다.&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;그럼 간단하게 State를 사용해 폼을 만들어보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748775479788&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Props extends React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; {
  label: string;
}

const Input: React.FC&amp;lt;Props&amp;gt; = ({ label, ...inputProps }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;label&amp;gt;{label}&amp;lt;/label&amp;gt;
      &amp;lt;input {...inputProps} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Input;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748775368860&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

import Input from &quot;./input&quot;;

const ControlledComponents = () =&amp;gt; {
  const [form, setForm] = useState({
    id: &quot;&quot;,
    pwd: &quot;&quot;,
    name: &quot;&quot;,
  });

  const handleChange = (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    const { id, value } = e.currentTarget;
    setForm((prev) =&amp;gt; ({ ...prev, [id]: value }));
  };

  return (
    &amp;lt;form style={{ display: &quot;flex&quot;, flexDirection: &quot;column&quot;, gap: &quot;1rem&quot; }}&amp;gt;
      &amp;lt;h1&amp;gt;Controlled Components&amp;lt;/h1&amp;gt;
      &amp;lt;Input id=&quot;id&quot; value={form.id} label=&quot;id&quot; onChange={handleChange} /&amp;gt;
      &amp;lt;Input
        id=&quot;pwd&quot;
        value={form.pwd}
        label=&quot;pwd&quot;
        type=&quot;password&quot;
        onChange={handleChange}
      /&amp;gt;
      &amp;lt;Input id=&quot;name&quot; value={form.name} label=&quot;name&quot; onChange={handleChange} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default ControlledComponents;&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;간단하게 id, password, name을 받는 폼 컴포넌트 입니다. state를 사용해 폼을 정의하고 각 값을 input 태그에 주입해 값을 추적, 관리 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-06-017.59.56-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9B4uL/btsOlWodl6q/R4qxj4hfuqcYaErLUFoCU1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9B4uL/btsOlWodl6q/R4qxj4hfuqcYaErLUFoCU1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9B4uL/btsOlWodl6q/R4qxj4hfuqcYaErLUFoCU1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/9B4uL/btsOlWodl6q/R4qxj4hfuqcYaErLUFoCU1/img.gif&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;800&quot; height=&quot;257&quot; data-filename=&quot;2025-06-017.59.56-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&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;만들어진 컴포넌트에서 값을 변경해봅시다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;저는 React DevTools의 Components에서 랜더링 발생시 하이라이트 표시가 되는 옵션을 켜놨습니다.&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;위 캡쳐를 보면 알 수 있듯이 각 input을 변경할 때 마다 모든 컴포넌트에서 리랜더링이 발생하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id라는 input을 변경했는데 state에 의존하고 있는 모든 id, pwd, name 필드에서 리랜더링이 발생하다니, 상당히 비효율적이라고 생각이 됩니다.&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;이런 불필요한 리랜더링이 발생하는 것이 싫어서 각 필드 값별로 State를 분리하고 Input 컴포넌트와 핸들링 함수를 메모이제이션 하기로 결정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748776075953&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;

interface Props extends React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; {
  label: string;
}

const Input: React.FC&amp;lt;Props&amp;gt; = ({ label, ...inputProps }) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;label&amp;gt;{label}&amp;lt;/label&amp;gt;
      &amp;lt;input {...inputProps} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default React.memo(Input);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748776063695&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback, useState } from &quot;react&quot;;

import Input from &quot;./input&quot;;

const ControlledComponents = () =&amp;gt; {
  const [name, setName] = useState(&quot;&quot;);
  const [pwd, setPwd] = useState(&quot;&quot;);
  const [id, setId] = useState(&quot;&quot;);

  const handleChangeName = useCallback(
    (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      setName(e.currentTarget.value);
    },
    []
  );

  const handleChangePwd = useCallback(
    (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      setPwd(e.currentTarget.value);
    },
    []
  );

  const handleChangeId = useCallback(
    (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
      setId(e.currentTarget.value);
    },
    []
  );

  return (
    &amp;lt;form style={{ display: &quot;flex&quot;, flexDirection: &quot;column&quot;, gap: &quot;1rem&quot; }}&amp;gt;
      &amp;lt;h1&amp;gt;Controlled Components&amp;lt;/h1&amp;gt;
      &amp;lt;Input id=&quot;id&quot; value={id} label=&quot;id&quot; onChange={handleChangeId} /&amp;gt;
      &amp;lt;Input
        id=&quot;pwd&quot;
        value={pwd}
        label=&quot;pwd&quot;
        type=&quot;password&quot;
        onChange={handleChangePwd}
      /&amp;gt;
      &amp;lt;Input id=&quot;name&quot; value={name} label=&quot;name&quot; onChange={handleChangeName} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default ControlledComponents;&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;코드가 상당히 복잡해졌습니다. 관리해야 할 State도 늘어났고 리랜더링 최적화를 위한 일까지 더해져 부모 컴포넌트가 이전의 코드보다 더 비대해진 것을 볼 수 있습니다. 실제 성능을 확인해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-06-018.08.08-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSQAtm/btsOmqoN8zM/ybNh4JI0lXNnme5K0NrK3k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSQAtm/btsOmqoN8zM/ybNh4JI0lXNnme5K0NrK3k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSQAtm/btsOmqoN8zM/ybNh4JI0lXNnme5K0NrK3k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bSQAtm/btsOmqoN8zM/ybNh4JI0lXNnme5K0NrK3k/img.gif&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;800&quot; height=&quot;257&quot; data-filename=&quot;2025-06-018.08.08-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;257&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;그렇지만 폼 컴포넌트를 만들고, 리랜더링 최적화까지의 과정을 살펴보면 일을 위한 일을 하는 느낌입니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비제어 컴포넌트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비제어 컴포넌트에 대해서 알아봅시다. 위에서 제어 컴포넌트는 React에 의해 통제되는 컴포넌트라는 가설을 세웠습니다. 그럼 비제어 컴포넌트는 React에 의해 통제받지 않는 컴포넌트라고 가설을 세워봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;react-hook-form&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-hook-form 라이브러리는 React에서 폼을 쉽게 다룰 수 있게 도와주는 대표적인 라이브러리 입니다. react-hook-form은 비제어 컴포넌트 방식으로 폼을 다루고 있습니다. 자세한 내용은 아래 블로그 글과 공식 홈페이지를 참고해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/introduce-react-hook-form/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-hook-form 을 활용해 효과적으로 폼 관리하기&lt;/a&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://react-hook-form.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-hook-form 공식 문서&lt;/a&gt;&lt;/blockquote&gt;
&lt;figure id=&quot;og_1748779371748&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;react-hook-form 을 활용해 효과적으로 폼 관리하기 - 오픈소스컨설팅 테크블로그 %&quot; data-og-description=&quot;오픈소스컨설팅 테크블로그 react-hook-form 을 활용해 효과적으로 폼 관리하기 오픈소스컨설팅에서 프론트엔드 개발을 하고 있는 강동희입니다. react-hook-form 을 도입한 경험을 공유합니다!&quot; data-og-host=&quot;tech.osci.kr&quot; data-og-source-url=&quot;https://tech.osci.kr/introduce-react-hook-form/&quot; data-og-url=&quot;https://tech.osci.kr/introduce-react-hook-form/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/e9L5U/hyY1f4Ufby/0KwoDyzqYOwpTN4d1V0bQK/img.png?width=87&amp;amp;height=83&amp;amp;face=0_0_87_83,https://scrap.kakaocdn.net/dn/XCKbo/hyY1dsvYhm/74oqBOrHJSWoPkaOfl0bRK/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/n7rtT/hyY1g3O6Md/zPFPzK54sbaAVeW3w4EU50/img.png?width=824&amp;amp;height=346&amp;amp;face=0_0_824_346&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/introduce-react-hook-form/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.osci.kr/introduce-react-hook-form/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/e9L5U/hyY1f4Ufby/0KwoDyzqYOwpTN4d1V0bQK/img.png?width=87&amp;amp;height=83&amp;amp;face=0_0_87_83,https://scrap.kakaocdn.net/dn/XCKbo/hyY1dsvYhm/74oqBOrHJSWoPkaOfl0bRK/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/n7rtT/hyY1g3O6Md/zPFPzK54sbaAVeW3w4EU50/img.png?width=824&amp;amp;height=346&amp;amp;face=0_0_824_346');&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;react-hook-form 을 활용해 효과적으로 폼 관리하기 - 오픈소스컨설팅 테크블로그 %&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스컨설팅 테크블로그 react-hook-form 을 활용해 효과적으로 폼 관리하기 오픈소스컨설팅에서 프론트엔드 개발을 하고 있는 강동희입니다. react-hook-form 을 도입한 경험을 공유합니다!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.osci.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-hook-form 라이브러리를 사용해 똑같은 폼 컴포넌트를 만들어봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1748776499329&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Input from &quot;./input&quot;;
import { useForm } from &quot;react-hook-form&quot;;

const defaultValues = {
  id: &quot;&quot;,
  pwd: &quot;&quot;,
  name: &quot;&quot;,
};

type Form = typeof defaultValues;

const UncontrolledComponents = () =&amp;gt; {
  const formMethod = useForm&amp;lt;Form&amp;gt;({
    defaultValues,
  });

  const { register } = formMethod;

  return (
    &amp;lt;form style={{ display: &quot;flex&quot;, flexDirection: &quot;column&quot;, gap: &quot;1rem&quot; }}&amp;gt;
      &amp;lt;h1&amp;gt;Controlled Components&amp;lt;/h1&amp;gt;
      &amp;lt;Input label=&quot;id&quot; {...register(&quot;id&quot;)} /&amp;gt;
      &amp;lt;Input label=&quot;pwd&quot; {...register(&quot;pwd&quot;)} /&amp;gt;
      &amp;lt;Input label=&quot;name&quot; {...register(&quot;name&quot;)} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default UncontrolledComponents;&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;코드를 작성했으니 UI가 잘 동작하는지 확인해봅시다. 그리고 위에서 문제가 있었던 리랜더링에 대해서 살펴봅시다.&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-filename=&quot;ezgif.com-video-to-gif-converter (1).gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deb7u5/btsOlPijxJy/KjjgimKs2KUb7L0wNZ3o8K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deb7u5/btsOlPijxJy/KjjgimKs2KUb7L0wNZ3o8K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deb7u5/btsOlPijxJy/KjjgimKs2KUb7L0wNZ3o8K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/deb7u5/btsOlPijxJy/KjjgimKs2KUb7L0wNZ3o8K/img.gif&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;800&quot; height=&quot;498&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (1).gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&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;react-hook-form은 내부적으로 react의 useRef를 활용해 값을 제어합니다. ref 객체는 JavaScript의 힙 영역에 저장되는 객체이며 랜더링 시 매번 같은 객체를 참조합니다. 메모리에서 참조되는 주소 값이 항상 동일하기에 실제 값이 바뀌어도 리랜더링이 되지 않습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Ref를 사용해 폼 컨트롤 해보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef를 사용한 값은 항상 동일한 참조를 유지하고 react-hook-form은 내부적으로 useRef를 사용해 값을 다룬다고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 react-hook-form은 useRef를 사용해 어떻게 값을 다룰수 있을지 생각해봅시다!&lt;/p&gt;
&lt;pre id=&quot;code_1748778365393&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;

type ReturnType&amp;lt;T&amp;gt; = {
  formRef: React.Ref&amp;lt;HTMLInputElement | null&amp;gt;;
  getValue: () =&amp;gt; T;
};

function useRefForm&amp;lt;T&amp;gt;(): ReturnType&amp;lt;T&amp;gt; {
  const formRef = React.useRef&amp;lt;HTMLInputElement | null&amp;gt;(null);

  const getValue = () =&amp;gt; {
    return formRef.current?.value as T;
  };

  return {
    formRef,
    getValue,
  };
}

export default useRefForm;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 input의 ref에 들어갈 ref를 선언합니다. getValue 메서드를 사용해 특성 시점에 ref의 value를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748778493551&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import Input from &quot;./input&quot;;
import useRefForm from &quot;./use-ref-form&quot;;

const RefComponent = () =&amp;gt; {
  const [formValue, setFormValue] = useState({
    name: &quot;&quot;,
    pwd: &quot;&quot;,
    id: &quot;&quot;,
  });
  const { formRef: name, getValue: getName } = useRefForm&amp;lt;string&amp;gt;();
  const { formRef: pwd, getValue: getPwd } = useRefForm&amp;lt;string&amp;gt;();
  const { formRef: id, getValue: getId } = useRefForm&amp;lt;string&amp;gt;();

  return (
    &amp;lt;form
      style={{ display: &quot;flex&quot;, flexDirection: &quot;column&quot;, gap: &quot;1rem&quot; }}
      onSubmit={(e) =&amp;gt; {
        e.preventDefault();
        const nameValue = getName();
        const idValue = getId();
        const pwdValue = getPwd();

        const data = {
          name: nameValue,
          id: idValue,
          pwd: pwdValue,
        };

        setFormValue(data);
      }}
    &amp;gt;
      &amp;lt;h1&amp;gt;Ref Components&amp;lt;/h1&amp;gt;
      &amp;lt;Input ref={name} label=&quot;id&quot; /&amp;gt;
      &amp;lt;Input ref={pwd} label=&quot;pwd&quot; /&amp;gt;
      &amp;lt;Input ref={id} label=&quot;name&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;submit&amp;lt;/button&amp;gt;
      &amp;lt;p style={{ display: &quot;flex&quot;, gap: &quot;1rem&quot; }}&amp;gt;
        &amp;lt;span&amp;gt;{formValue.name}&amp;lt;/span&amp;gt;
        &amp;lt;span&amp;gt;{formValue.id}&amp;lt;/span&amp;gt;
        &amp;lt;span&amp;gt;{formValue.pwd}&amp;lt;/span&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default RefComponent;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅을 사용하는 폼 컴포넌트 입니다. submit 시점에 form을 만들어 state에 넣어줍니다. 값을 잘 추적하고 있는지 확인하기 UI에서 확인하고 싶어 state를 사용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2025-06-018.49.59-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nuMeJ/btsOlMMJ5u3/KrwWZNiH9fsd6jmb7eVFiK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nuMeJ/btsOlMMJ5u3/KrwWZNiH9fsd6jmb7eVFiK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nuMeJ/btsOlMMJ5u3/KrwWZNiH9fsd6jmb7eVFiK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/nuMeJ/btsOlMMJ5u3/KrwWZNiH9fsd6jmb7eVFiK/img.gif&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;800&quot; height=&quot;603&quot; data-filename=&quot;2025-06-018.49.59-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/figure&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;단순히 ref 객체만 사용해 폼을 다뤄봤습니다. react-hook-form 라이브러리를 사용했을 때 처럼 값을 변경해도 리랜더링이 발생하지 않으며 값을 확인하려고 하는 순간(submit)에만 State가 변경되어 랜더링이 발생하는 것을 확인할 수 있습니다.&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;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼 컴포넌트를 통해 제어 컴포넌트와 비제어 컴포넌트에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼 컴포넌트를 구현하며 React에 의해 값이 제어되는(랜더링이 발생하는) 컴포넌트를 제어 컴포넌트라고 부른다는 것을 확인할 수 있었습니다.&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;이런 경우 제어 컴포넌트를 사용하되 랜더링 퍼포먼스에 유의하는 것이 최선의 선택이며, 퍼포먼스를 고려하여 비제어 컴포넌트를 사용할 수 있고, 폼 같은 경우에는 비제어 컴포넌트를 쉽게 다룰 수 있는 react-hook-form 라이브러리가 있다는 정도만 기억하면 좋을 것 같습니다.&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;PS. 번외로 react-hook-form 라이브러리도 내부적으로 폼의 상태를 관리하기 위해 State를 사용하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748779149834&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// react-hook-form 레포지토리의 useForm 발췌

export function useForm&amp;lt;
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues = TFieldValues,
&amp;gt;(
  props: UseFormProps&amp;lt;TFieldValues, TContext, TTransformedValues&amp;gt; = {},
): UseFormReturn&amp;lt;TFieldValues, TContext, TTransformedValues&amp;gt; {
  const _formControl = React.useRef&amp;lt;
    UseFormReturn&amp;lt;TFieldValues, TContext, TTransformedValues&amp;gt; | undefined
  &amp;gt;(undefined);
  const _values = React.useRef&amp;lt;typeof props.values&amp;gt;(undefined);
  const [formState, updateFormState] = React.useState&amp;lt;FormState&amp;lt;TFieldValues&amp;gt;&amp;gt;({
    isDirty: false,
    isValidating: false,
    isLoading: isFunction(props.defaultValues),
    isSubmitted: false,
    isSubmitting: false,
    isSubmitSuccessful: false,
    isValid: false,
    submitCount: 0,
    dirtyFields: {},
    touchedFields: {},
    validatingFields: {},
    errors: props.errors || {},
    disabled: props.disabled || false,
    isReady: false,
    defaultValues: isFunction(props.defaultValues)
      ? undefined
      : props.defaultValues,
  });
  
  // 생략&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State를 잘 다루는 방법에 대해서 고민이 있으시다면 아래 포스팅을 참고해주시면 감사하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[React] 상태(State)에 대하여&lt;/a&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;읽어봐주셔서 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>react</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/83</guid>
      <comments>https://kangs-develop.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 1 Jun 2025 21:06:52 +0900</pubDate>
    </item>
    <item>
      <title>Vite의 사전 번들링, HMR</title>
      <link>https://kangs-develop.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Vite&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite는 &lt;b&gt;프로젝트 전체를 번들링하지 않고도&lt;/b&gt; 빠른 개발 서버를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite가 등장하기 전 많이 사용된 번들러인 Webpack은 개발 서버를 실행할 때 프로젝트 전체의 의존 그래프를 해석하고 이를 번들링한 결과물을 브라우저에 전달합니다. 반면, Vite는 Native ESM(ECMAScript Modules)을 활용하여 &lt;b&gt;브라우저가 직접 모듈을 해석하도록 위임&lt;/b&gt;하는 방식으로 동작합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어, Webpack은 진입점부터 모든 의존성을 분석해 번들링을 수행한 뒤 이를 메모리에 저장하고 개발 서버를 통해 서빙합니다. 반면 Vite는 &lt;b&gt;콜드 스타트 시점에만 esbuild로 라이브러리를 사전 번들링&lt;/b&gt;하고, 이후에는 소스 코드를 ESM 형태로 그대로 브라우저에 전달합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이로 인해 &lt;b&gt;Vite는 번들링 시간이 거의 없이 즉시 개발 서버를 실행&lt;/b&gt;할 수 있고, 변경된 모듈만 빠르게 반영되어 좋은 개발 경험(DX)을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDJk7o/btsOaF8fOWl/VhHFH6TkGrkkrmjXBnrZmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDJk7o/btsOaF8fOWl/VhHFH6TkGrkkrmjXBnrZmK/img.png&quot; data-alt=&quot;Vite 공식 홈페이지 참고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDJk7o/btsOaF8fOWl/VhHFH6TkGrkkrmjXBnrZmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDJk7o%2FbtsOaF8fOWl%2FVhHFH6TkGrkkrmjXBnrZmK%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;718&quot; height=&quot;812&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Vite 공식 홈페이지 참고&lt;/figcaption&gt;
&lt;/figure&gt;
&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;&lt;b&gt;Vite의 개발 서버는 어떻게 작동하나?&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite는 개발 서버 구동 시 다음과 같은 과정을 거칩니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;콜드 스타트 시 사전 번들링 (Pre-Bundling)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;node_modules에 존재하는 외부 의존성(CJS 또는 ESM)을 &lt;b&gt;esbuild&lt;/b&gt;를 이용해 &lt;b&gt;하나의 ESM으로 번들링&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;주된 목적은 &lt;b&gt;CJS &amp;rarr; ESM 변환&lt;/b&gt;, &lt;b&gt;모듈 수 최소화&lt;/b&gt;, &lt;b&gt;캐싱 최적화&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;번들링 결과는 .vite/deps 디렉토리에 저장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;사전 번들링은 일반적으로 한 번만 수행되며, 이후 개발 서버를 다시 실행해도 그대로 캐시를 사용합니다. --force 플래그를 사용하면 캐싱된 사전 번들링을 무시하고 다시 콜드 스타트를 할 수 있습니다.&lt;/blockquote&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;380&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qa7e/btsOaVDciAB/F2jTOWONo2KMkTeuDFo4gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qa7e/btsOaVDciAB/F2jTOWONo2KMkTeuDFo4gk/img.png&quot; data-alt=&quot;Vite 프로젝트를 생성한 후 의존성을 설치(pnpm install)한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qa7e/btsOaVDciAB/F2jTOWONo2KMkTeuDFo4gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qa7e%2FbtsOaVDciAB%2FF2jTOWONo2KMkTeuDFo4gk%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;380&quot; height=&quot;472&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Vite 프로젝트를 생성한 후 의존성을 설치(pnpm install)한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t9WA9/btsObsUAcO3/oscog3VJ4mnPcmWQQjk3K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t9WA9/btsObsUAcO3/oscog3VJ4mnPcmWQQjk3K1/img.png&quot; data-alt=&quot;개발 서버를 구동한 후 (pnpm dev) .vite/deps에 의존성(라이브러리)들이 사전 번들링이 된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t9WA9/btsObsUAcO3/oscog3VJ4mnPcmWQQjk3K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft9WA9%2FbtsObsUAcO3%2Foscog3VJ4mnPcmWQQjk3K1%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;370&quot; height=&quot;773&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;773&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발 서버를 구동한 후 (pnpm dev) .vite/deps에 의존성(라이브러리)들이 사전 번들링이 된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 esbuild를 사용할까?&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;esbuild는 Go로 작성된 초고속 번들러로, &lt;b&gt;Webpack이나 Rollup보다 10~100배 빠릅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Vite는 개발 서버의 응답 속도와 콜드 스타트 시간을 극대화하기 위해 esbuild를 사용해 의존성 번들링을 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Native ESM 기반 개발 서버&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 Vite 개발 서버(localhost:5173)에 최초 요청 시 다음과 같은 순서로 작동합니다:&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;index.html&lt;/b&gt;을 요청하여 HTML 문서와 &amp;lt;script type=&quot;module&quot;&amp;gt;로 명시된 진입 모듈을 받아옵니다.&lt;/li&gt;
&lt;li&gt;진입 모듈에서 import한 ESM 모듈들을 차례대로 &lt;b&gt;동적으로 요청&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개발 서버 구동&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초로 개발 서버를 구동하면 캐싱된 모듈이 없기에 모든 모듈에 대한 요청이 200OK로 응답 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b48XLy/btsOagBm0Dt/uRzH9oZ1XtZMx4A0jMd1E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b48XLy/btsOagBm0Dt/uRzH9oZ1XtZMx4A0jMd1E0/img.png&quot; data-alt=&quot;네트워크 탭을 살펴보면 t라는 쿼리 파라미터도 존재하지 않습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b48XLy/btsOagBm0Dt/uRzH9oZ1XtZMx4A0jMd1E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb48XLy%2FbtsOagBm0Dt%2FuRzH9oZ1XtZMx4A0jMd1E0%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;692&quot; height=&quot;515&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네트워크 탭을 살펴보면 t라는 쿼리 파라미터도 존재하지 않습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #292a2e;&quot;&gt;개발 서버 구동 시 Vite 개발 서버와 브라우저간에 WebSocket 커넥션이 열리게 되는데, 해당 커넥션은 HMR을 핸들링 하기 위한 커넥션으로 사용됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJG3Z4/btsN9HThs60/zY85rYY9aBhMf8gToKnSfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJG3Z4/btsN9HThs60/zY85rYY9aBhMf8gToKnSfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJG3Z4/btsN9HThs60/zY85rYY9aBhMf8gToKnSfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJG3Z4%2FbtsN9HThs60%2FzY85rYY9aBhMf8gToKnSfk%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;725&quot; height=&quot;401&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;401&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;645&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3z3bG/btsOcbdHy2C/FOcKfRsKRKiE8UIGS9Mbf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3z3bG/btsOcbdHy2C/FOcKfRsKRKiE8UIGS9Mbf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3z3bG/btsOcbdHy2C/FOcKfRsKRKiE8UIGS9Mbf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3z3bG%2FbtsOcbdHy2C%2FFOcKfRsKRKiE8UIGS9Mbf1%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;645&quot; height=&quot;390&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #292a2e;&quot;&gt;작동 여부를 확인하기 위해 코드를 수정하겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

function App() {
&amp;nbsp;&amp;nbsp;const [count, setCount] = useState(0);

&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h1&amp;gt;Vite 개발 서버&amp;lt;/h1&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div className=&quot;card&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={() =&amp;gt; setCount((count) =&amp;gt; count + 1)}&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;count: {count}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/&amp;gt;
&amp;nbsp;&amp;nbsp;);
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;원래의 소스 코드를 수정한 후 파일을 저장합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Vite 개발 서버가 소스 코드의 변경을 감지하여 브라우저에 소스 코드가 변경됐다는 메세지를 보내며 해당 메세지에는 변경된 모듈의 경로를 함께 알려줍니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;커넥션에서 전달 받은 메세지를 통해 코드 변경을 감지하고 변경된 모듈을 요청합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFIR8Z/btsObytGgFU/Cf3ael3h0mWLMVXQT8GrJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFIR8Z/btsObytGgFU/Cf3ael3h0mWLMVXQT8GrJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFIR8Z/btsObytGgFU/Cf3ael3h0mWLMVXQT8GrJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFIR8Z%2FbtsObytGgFU%2FCf3ael3h0mWLMVXQT8GrJ1%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;875&quot; height=&quot;425&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #292a2e;&quot;&gt;이후 동일한 모듈을 요청할 경우 해당 timestamp를 사용해 요청을 보냅니다. Vite 개발 서버는 timestamp를 기반으로 동일한 모듈인지 체크하여 변함이 없다면 304 Not Modified를 응답합니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;?t=timestamp&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 URL을 살펴보면 t라는 key를 가진 쿼리 파라미터를 확인할 수 있는데, 해당 쿼리 파라미터는 브라우저의 캐시 정책을 우회하기 위해 사용하는 쿼리 파라미터입니다.&lt;br /&gt;브라우저는 기본적으로 다음의 캐시 특성을 지니고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 Native ESM은 URL을 기준으로 캐시합니다.&lt;/li&gt;
&lt;li&gt;같은 URL이면 이전에 불러온 모듈은 절대 다시 요청하지 않습니다. (Cache-Control: no-cache 헤더가 존재해도 마찬가지입니다.)&lt;/li&gt;
&lt;li&gt;특히 &amp;lt;script type=&amp;rdquo;module&amp;rdquo;&amp;gt;로 import 된 모듈은 ESM 캐시의 영향을 강하게 받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 특성을 우회하기 위해 Vite 개발 서버는 브라우저에서 개발 서버에 요청을 보낼 시 timestamp를 사용해 캐시 정책을 우회(무효화) 합니다.&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;브라우저는 Vite 개발 서버에 요청을 보낼 때 기본적으로 Cache-Control: no-cache 헤더를 포함합니다. 이는 브라우저가 해당 자원을 캐시하고 있더라도, 반드시 서버에 유효성 검사를 요청하라는 의미입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;?t=timestamp는 어디서 왔을까?&lt;/b&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드가 변경된 후 클라이언트는 Web Socket을 통해 소스 코드의 변경을 알림 받습니다. 그리고 t=timestamp를 쿼리 파라미터로 붙여 변경된 모듈을 다시 요청합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저는 문득 t=timestamp는 어디서 생긴 값인지 궁금했습니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-renderer-start-pos=&quot;2704&quot; data-ke-size=&quot;size16&quot;&gt;해당 내용은 아래 블로그를 참고한 내용입니다. Vite에서 HMR이 어떻게 동작하는지 소스 코드 레벨에서 잘 분석을 해준 포스팅 입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-renderer-start-pos=&quot;2782&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-renderer-start-pos=&quot;2782&quot; data-annotation-mark=&quot;true&quot; data-annotation-inline-node=&quot;true&quot; data-card-url=&quot;https://pozafly.github.io/environment/why-do-you-use-import-meta-in-vite/&quot; data-inline-card=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;a style=&quot;background-color: #ffffff; color: #1868db;&quot; href=&quot;https://pozafly.github.io/environment/why-do-you-use-import-meta-in-vite/&quot; data-testid=&quot;inline-card-resolved-view&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;span data-testid=&quot;icon-position-wrapper&quot;&gt;&lt;span data-testid=&quot;icon-wrapper&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Vite에서 import.meta는 왜 사용하는 걸까? (feat. HMR)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;먼저 Vite 개발 서버는 소스 코드가 변경되면 변경된 모듈에 timestamp를 붙여서 다시 생성합니다.&lt;br /&gt;예를 들면 App.tsx 코드를 수정했다면, Vite 개발 서버는 해당 소스 코드를 App.tsx?t=1747990597069 이런 식으로 다시 생성합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;궁금증을 해결하기 위해 @vite/client의 코드를 간략하게 살펴보겠습니다. 이전에 어떤 과정이 있는지 (import.meta.hot에 HMR Context가 주입되는 과정)는 생략하도록 하겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Web Socket 커넥션이 연결된 후 @vite/client는 커넥션에서 전달 받는 메세지를 handleMessage 함수로 처리합니다. 아래는 handleMessage 함수의 일부입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;async function handleMessage(payload) {
&amp;nbsp;&amp;nbsp;switch (payload.type) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 생략 //
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;case &quot;update&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await hmrClient.notifyListeners(&quot;vite:beforeUpdate&quot;, payload);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await Promise.all(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;payload.updates.map(async (update) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (update.type === &quot;js-update&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return hmrClient.queueUpdate(update);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const { path, timestamp } = update;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const searchUrl = cleanUrl(path);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const el = Array.from(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;document.querySelectorAll(&quot;link&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;).find(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(e) =&amp;gt; !outdatedLinkTags.has(e) &amp;amp;&amp;amp; cleanUrl(e.href).includes(searchUrl)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!el) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes(&quot;?&quot;) ? &quot;&amp;amp;&quot; : &quot;?&quot;}t=${timestamp}`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new Promise((resolve) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const newLinkTag = el.cloneNode();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;newLinkTag.href = new URL(newPath, el.href).href;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const removeOldEl = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;el.remove();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;console.debug(`[vite] css hot updated: ${searchUrl}`);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resolve();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;newLinkTag.addEventListener(&quot;load&quot;, removeOldEl);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;newLinkTag.addEventListener(&quot;error&quot;, removeOldEl);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;outdatedLinkTags.add(el);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;el.after(newLinkTag);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break;
// 생략 //&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;payload로 전달받은 데이터 중 updates 배열을 순회합니다. 이때 update 객체에는 path, timestamp라는 프로퍼티가 존재하는데, 이 프로퍼티는 Vite 개발 서버로부터 전달받은 변경된 파일명과 timestamp 값 입니다. 해당 값들을 활용해 newPath를 만들고 해당 path로 새로운 모듈을 요청합니다.&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;조금 더 자세하게 살펴보겠습니다. JavaScript 코드를 수정할 시 update.type 은 'js-update' 입니다. 이 경우 hmrClient.queueUpdate 메서드에 update 객체를 넘겨주며 핸들링 함수가 종료됩니다. queueUpdate 메서드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748146909331&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  async queueUpdate(payload) {
    this.updateQueue.push(this.fetchUpdate(payload));
    if (!this.pendingUpdateQueue) {
      this.pendingUpdateQueue = true;
      await Promise.resolve();
      this.pendingUpdateQueue = false;
      const loading = [...this.updateQueue];
      this.updateQueue = [];
      (await Promise.all(loading)).forEach((fn) =&amp;gt; fn &amp;amp;&amp;amp; fn());
    }
  }&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;hmrClient 객체의 updateQueue 프로퍼티에 fetchUpdate 메서드를 실행시켜 값을 밀어넣고 있습니다. fetchUpdate 메서드를 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748146987397&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  async fetchUpdate(update) {
    const { path, acceptedPath, firstInvalidatedBy } = update;
    
    // 생략
    if (isSelfUpdate || qualifiedCallbacks.length &amp;gt; 0) {
      const disposer = this.disposeMap.get(acceptedPath);
      if (disposer) await disposer(this.dataMap.get(acceptedPath));
      try {
        // importUpdateModule을 확인
        fetchedModule = await this.importUpdatedModule(update);
      } catch (e) {
        this.warnFailedUpdate(e, acceptedPath);
      }
    }
    
    // 생략
  }&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;파라미터로 전달되는 update는 커넥션에서 전달받은 메세지 객체 입니다. 여기서 fetchedModule이라는 값에 집중할 필요가 있고 해당 변수에 값을 할당하는 메서드를 한번 더 살펴보겠습니다.&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;importUpdatedModule 메서드는 hmrClient 객체가 생성될 때 파라미터로 전달받는 함수 입니다. 해당 함수는 파라미터로 전달받아 객체의 메서드로 등록됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748148047942&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  async function importUpdatedModule({
    acceptedPath,
    timestamp,
    explicitImportRequired,
    isWithinCircularImport
  }) {
    const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`);
    const importPromise = import(
      /* @vite-ignore */
      base + acceptedPathWithoutQuery.slice(1) + `?${explicitImportRequired ? &quot;import&amp;amp;&quot; : &quot;&quot;}t=${timestamp}${query ? `&amp;amp;${query}` : &quot;&quot;}`
    );
    if (isWithinCircularImport) {
      importPromise.catch(() =&amp;gt; {
        console.info(
          `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`
        );
        pageReload();
      });
    }
    return await importPromise;
  }&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;메서드를 살펴보면 importPromise에 url로 t=timestamp 값이 들어가는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 importPromise는 fetchedModule로, fetchedModule 값은 qualifiedCallbacks을 순회하며 각 객체의 deps로 변형이 되어 해당 객체 속 fn의 인자로 전달됩니다.&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;지금은 t=timestamp의 값이 어디서 왔는지에 포커스를 두고 있어 더 자세하게 살펴보지는 않겠습니다. 만약 그 이후의 Vite의 HMR이 어떻게 동작하는지 궁금하시다면 아래 블로그 글을 참고해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://velog.io/@woohm402/vite-react-hmr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Vite 프로젝트에서 리액트 컴포넌트는 어떻게 HMR될까? (소스코드 뜯어보기)&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1748148406524&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;Vite 프로젝트에서 리액트 컴포넌트는 어떻게 HMR될까? (소스코드 뜯어보기)&quot; data-og-description=&quot;vite + react 프로젝트에서 HMR이 수행되는 과정을 쫓아가 봐요&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@woohm402/vite-react-hmr&quot; data-og-url=&quot;https://velog.io/@woohm402/vite-react-hmr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bq4l6c/hyYYtIXVUC/RKDMG3AEMZngCfuqi8irx1/img.png?width=1028&amp;amp;height=695&amp;amp;face=0_0_1028_695,https://scrap.kakaocdn.net/dn/S6ykg/hyYYEDINKW/fkwmvvLhJOrsUNxFkzKWE0/img.png?width=1028&amp;amp;height=695&amp;amp;face=0_0_1028_695,https://scrap.kakaocdn.net/dn/VJtaI/hyY0pyx8M7/Kq816HpkBVbKvLi6P12A60/img.png?width=1170&amp;amp;height=519&amp;amp;face=0_0_1170_519&quot;&gt;&lt;a href=&quot;https://velog.io/@woohm402/vite-react-hmr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@woohm402/vite-react-hmr&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bq4l6c/hyYYtIXVUC/RKDMG3AEMZngCfuqi8irx1/img.png?width=1028&amp;amp;height=695&amp;amp;face=0_0_1028_695,https://scrap.kakaocdn.net/dn/S6ykg/hyYYEDINKW/fkwmvvLhJOrsUNxFkzKWE0/img.png?width=1028&amp;amp;height=695&amp;amp;face=0_0_1028_695,https://scrap.kakaocdn.net/dn/VJtaI/hyY0pyx8M7/Kq816HpkBVbKvLi6P12A60/img.png?width=1170&amp;amp;height=519&amp;amp;face=0_0_1170_519');&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;Vite 프로젝트에서 리액트 컴포넌트는 어떻게 HMR될까? (소스코드 뜯어보기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;vite + react 프로젝트에서 HMR이 수행되는 과정을 쫓아가 봐요&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite 개발 서버가 코드가 수정되면 script 파일을 timestamp를 붙여서 다시 생성한다는 점과 Web Socket을 통해 클라이언트에 timestamp와 변경된 스크립트 파일의 경로를 보내준다는 점, 브라우저의 캐싱 정책을 우회하기 위해 t 쿼리 파라미터를 활용한다는 점을 고려하면 Vite의 HMR이 어떻게 동작하는지 감이 잡힙니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/81</guid>
      <comments>https://kangs-develop.tistory.com/81#entry81comment</comments>
      <pubDate>Fri, 23 May 2025 18:44:05 +0900</pubDate>
    </item>
    <item>
      <title>[React] react-query prefetch</title>
      <link>https://kangs-develop.tistory.com/80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 진행중 매출을 등록할 때 필드가 너무 늦게 뜨거나 어떤 경우 데이터가 안나온다는 피드백을 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서 해당 케이스를 react-query의 prefetch를 통해 해결한 과정을 남겨보겠습니다.&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-filename=&quot;2025-04-2712.35.50-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dELAVv/btsNB7Dx4zZ/U3yWCPFcQKEZQ0mwxv0E11/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dELAVv/btsNB7Dx4zZ/U3yWCPFcQKEZQ0mwxv0E11/img.gif&quot; data-alt=&quot;AS-IS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dELAVv/btsNB7Dx4zZ/U3yWCPFcQKEZQ0mwxv0E11/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dELAVv/btsNB7Dx4zZ/U3yWCPFcQKEZQ0mwxv0E11/img.gif&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;240&quot; height=&quot;388&quot; data-filename=&quot;2025-04-2712.35.50-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AS-IS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 사진에서 확인할 수 있듯이 매출 화면에 접근할 때 마다 매번 필터 데이터를 호출하고 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;데이터의 성격&lt;/b&gt;&lt;/h3&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-filename=&quot;화면 기록 2025-04-27 오후 12.21.10.gif&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;1444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2ROxH/btsNBu63U1H/VcIEi8JGO82JGCLEXgEyYK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2ROxH/btsNBu63U1H/VcIEi8JGO82JGCLEXgEyYK/img.gif&quot; data-alt=&quot;데이터의 성격&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2ROxH/btsNBu63U1H/VcIEi8JGO82JGCLEXgEyYK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/c2ROxH/btsNBu63U1H/VcIEi8JGO82JGCLEXgEyYK/img.gif&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;240&quot; height=&quot;388&quot; data-filename=&quot;화면 기록 2025-04-27 오후 12.21.10.gif&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;1444&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구현에서는 해당 데이터를 필요로 하는 곳에서 react-query 를 통해 서버 상태로 관리하고 있었습니다.&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;b&gt;prefetch를 통해 query를 캐싱한 후 실제 데이터를 사용하는 페이지에서 데이터의 호출 없이 화면에 바로 display 하는 것&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;여기서 동시성 문제로, 만약 사용자가 아닌 로그인 한 제 3자가 데이터를 변경했을 경우엔 사용자가 stale한 상태의 데이터를 바라볼 수 있지 않을까 고민했지만 현재 구현한 웹 애플리케이션은 1:1 방식으로 설계가 되어있어 사용자의 세션이 탈취당하지 않은 이상 해당 데이터를 변경할 수 없다 라는 결론이 도출됐습니다.&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;마지막으로 유저가 실제 데이터를 변경할 경우 invalidateQueries 메서드를 활용해 캐싱된 query를 강제로 stale 하게 만들었습니다.&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;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 같은 종류의 query는 같은 옵션을 공유해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prefetchQuery에 넣어줄 쿼리 옵션 객체는 useVisitTypes의 옵션과 동일한 옵션이여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tanstack/react-query 패키지에서 제공하는 UseQueryOptions 타입을 사용해 옵션 객체의 타입을 만들고, 패키지의 queryOptions메서드에 options 객체를 넣어주며 옵션 객체를 만듭니다.&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;해당 options 객체를 useQuery를 래핑한 커스텀 훅에서 호출해주고, 쿼리의 옵션으로 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745725417982&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type VisitTypesQueryOptions = Omit&amp;lt;UseQueryOptions&amp;lt;VisitType[]&amp;gt;, &quot;queryKey&quot;&amp;gt;;
export const visitTypesQueryOptions = (options?: VisitTypesQueryOptions) =&amp;gt;
  queryOptions({
    ...options,
    staleTime: FILTER_STALE_TIME,
    queryKey: [KEYS.settings.visitTypes],
    queryFn: getVisitTypes,
  });

export const useVisitTypes = (
  options?: Omit&amp;lt;UseQueryOptions&amp;lt;VisitType[]&amp;gt;, &quot;queryKey&quot; | &quot;queryFn&quot;&amp;gt;
) =&amp;gt; {
  const queryOptions = visitTypesQueryOptions(options);
  return useQuery&amp;lt;VisitType[]&amp;gt;({
    ...queryOptions,
  });
};&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;prefetchQuery에서도 위 옵션 객체를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745725635797&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useQueryClient } from &quot;@tanstack/react-query&quot;;
import {
  visitTypesQueryOptions,
} from &quot;@/queries/settings&quot;;
import { useCallback, useEffect, useRef } from &quot;react&quot;;

const usePrefetchFilters = () =&amp;gt; {
  const isMount = useRef(false);
  const queryClient = useQueryClient();

  const visitTypesOptions = visitTypesQueryOptions();

  const prefetchFilters = useCallback(async () =&amp;gt; {
    await queryClient.prefetchQuery({
      ...visitTypesOptions,
    });
  }, [
    queryClient,
    visitTypesOptions,
  ]);

  useEffect(() =&amp;gt; {
    if (isMount.current) return;

    isMount.current = true;
    prefetchFilters();
  }, [prefetchFilters]);
};

export default usePrefetchFilters;&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;prefetch 로직을 담고 있는 커스텀 훅에서 옵션 객체를 불러오고, 해당 옵션을 활용해 query를 prefetch를 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 isMount 라는 ref를 활용해 mount 된 이후에는 prefetchFilter을 하지 않도록 막아줍니다.&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-filename=&quot;2025-04-2712.41.25-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1wH2n/btsNCyUVtsD/9swa1nNwt0MoFZt7XPfF7K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1wH2n/btsNCyUVtsD/9swa1nNwt0MoFZt7XPfF7K/img.gif&quot; data-alt=&quot;TO-BE&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1wH2n/btsNCyUVtsD/9swa1nNwt0MoFZt7XPfF7K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/c1wH2n/btsNCyUVtsD/9swa1nNwt0MoFZt7XPfF7K/img.gif&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;240&quot; height=&quot;388&quot; data-filename=&quot;2025-04-2712.41.25-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1292&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TO-BE&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;변경 이후 매출 페이지에 접근할 때 매번 필터 데이터를 호출하지 않고 캐싱된 데이터를 사용하고 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tanstack/react-query의 강력한 기능 중 하나는 캐싱 전략 입니다. 캐싱을 잘 활용하면 사용자에게 더 좋은 경험을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터의 성격을 잘 고민해보면 더 좋은 UX를 만들수 있지 않을까라는 생각도 듭니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/80</guid>
      <comments>https://kangs-develop.tistory.com/80#entry80comment</comments>
      <pubDate>Sun, 27 Apr 2025 12:55:56 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] this 심화 탐구</title>
      <link>https://kangs-develop.tistory.com/79</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;요약&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;엄격 모드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;엄격&amp;nbsp;모드를&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;globalThis는&amp;nbsp;undefined를&amp;nbsp;가리킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수 선언문&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;함수&amp;nbsp;선언문에서의&amp;nbsp;this는&amp;nbsp;전역&amp;nbsp;객체를&amp;nbsp;가리킨다.&amp;nbsp;단&amp;nbsp;객체의&amp;nbsp;메서드가&amp;nbsp;함수&amp;nbsp;선언문으로&amp;nbsp;작성된&amp;nbsp;경우&amp;nbsp;this는&amp;nbsp;해당&amp;nbsp;객체를&amp;nbsp;가리킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;화살표 함수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;화살표&amp;nbsp;함수는&amp;nbsp;this&amp;nbsp;바인딩이&amp;nbsp;존재하지&amp;nbsp;않는다. &lt;br /&gt;-&amp;nbsp;화살표&amp;nbsp;함수에서&amp;nbsp;사용하는&amp;nbsp;this는&amp;nbsp;화살표&amp;nbsp;함수가&amp;nbsp;정의된(코드의&amp;nbsp;위치)&amp;nbsp;스코프의&amp;nbsp;this를&amp;nbsp;캡처한다. &lt;br /&gt;-&amp;nbsp;이&amp;nbsp;말을&amp;nbsp;다시&amp;nbsp;말하면,&amp;nbsp;화살표&amp;nbsp;함수에서&amp;nbsp;사용하는&amp;nbsp;this는&amp;nbsp;화살표&amp;nbsp;함수의&amp;nbsp;상위&amp;nbsp;스코프의&amp;nbsp;this를&amp;nbsp;가리킨다.&amp;nbsp;&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;-&amp;nbsp;객체의&amp;nbsp;메서드가&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;선언된&amp;nbsp;경우,&amp;nbsp;this는&amp;nbsp;객체가&amp;nbsp;정의된&amp;nbsp;위치의&amp;nbsp;상위&amp;nbsp;스코프를&amp;nbsp;가리킨다.&amp;nbsp;여기서&amp;nbsp;상위&amp;nbsp;스코프는&amp;nbsp;객체&amp;nbsp;자신이&amp;nbsp;아닌&amp;nbsp;객체가&amp;nbsp;정의되어&amp;nbsp;있는&amp;nbsp;스코프를&amp;nbsp;말한다. &lt;br /&gt;-&amp;nbsp;스코프와&amp;nbsp;객체를&amp;nbsp;구분해서&amp;nbsp;이해할&amp;nbsp;필요가&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;중요!&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;객체 리터럴(obj) 자체는 this 바인딩을 만들어내지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;화살표 함수에서의 this 바인딩&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 this 바인딩이 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수 내에서 사용하는 this는 정의되는 시점에 화살표 함수의 상위 스코프 this를 캡처한다.&lt;/p&gt;
&lt;pre id=&quot;code_1744264121387&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
    getThis: () =&amp;gt; {
        return this;
    }
}

console.log(obj.getThis()); // module.exports&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;위&amp;nbsp;obj의&amp;nbsp;getThis&amp;nbsp;메서드는&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;정의되었고&amp;nbsp;this를&amp;nbsp;반환한다. &lt;br /&gt;getThis&amp;nbsp;메서드가&amp;nbsp;정의되는&amp;nbsp;시점은&amp;nbsp;obj&amp;nbsp;객체가&amp;nbsp;생성되는&amp;nbsp;시점이다. &lt;br /&gt;그 시점의 상위 스코프는 전역/모듈 스코프이다. &lt;br /&gt;따라서&amp;nbsp;getThis&amp;nbsp;메서드로&amp;nbsp;반환되는&amp;nbsp;this는&amp;nbsp;전역/모듈&amp;nbsp;스코프인&amp;nbsp;module.exports가&amp;nbsp;반환된다.&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;&lt;b&gt;함수 선언문의 this 바인딩&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 선언문에서의 this는 global 객체를 가리킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1744264344836&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function foo() {
  return this;
}

console.log(foo()); // global&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단 엄격 모드(strict mode)의 함수 선언문 this 바인딩은 undefined 이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1744264373604&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use strcit&quot;;

function foo() {
  return this;
}

console.log(foo()); // undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;함수 선언문 내 화살표 함수의 this 결정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1744264232254&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function foo() {
    const obj = {
        getThis: () =&amp;gt; {
            return this;
        }
    }

    console.log(obj.getThis());
}

foo(); // global 객체&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;위&amp;nbsp;foo&amp;nbsp;함수에&amp;nbsp;정의된&amp;nbsp;obj&amp;nbsp;객체의&amp;nbsp;getThis&amp;nbsp;메서드는&amp;nbsp;this를&amp;nbsp;반환한다. &lt;br /&gt;obj&amp;nbsp;객체의&amp;nbsp;getThis가&amp;nbsp;정의되는&amp;nbsp;시점은&amp;nbsp;foo&amp;nbsp;함수가&amp;nbsp;호출되는&amp;nbsp;시점이다. &lt;br /&gt;foo&amp;nbsp;함수의&amp;nbsp;this&amp;nbsp;바인딩은&amp;nbsp;global&amp;nbsp;객체이다. &lt;br /&gt;obj.getThis에서&amp;nbsp;반환하는&amp;nbsp;this는&amp;nbsp;정의&amp;nbsp;시점&amp;nbsp;상위&amp;nbsp;스코프의(foo)의&amp;nbsp;this를&amp;nbsp;캡처한다. &lt;br /&gt;따라서&amp;nbsp;foo&amp;nbsp;함수의&amp;nbsp;실행&amp;nbsp;결과&amp;nbsp;obj.getThis()에서&amp;nbsp;반환되는&amp;nbsp;this는&amp;nbsp;global&amp;nbsp;객체이다.&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;&lt;b&gt;블록 스코프 내 화살표 함수의 this&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보고 foo 함수 내부의 obj.getThis 메서드가 반환할 this를 예측해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1744264475184&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function foo() {
    const obj = {
        getThis: () =&amp;gt; {
            return this;
        }
    }

    console.log(obj.getThis());
}


{
    foo(); // global 객체
}&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;getThis&amp;nbsp;메서드로&amp;nbsp;반환되는&amp;nbsp;this는&amp;nbsp;global&amp;nbsp;객체이다. &lt;br /&gt;Block&amp;nbsp;스코프는&amp;nbsp;스코프만&amp;nbsp;형성할&amp;nbsp;뿐&amp;nbsp;this&amp;nbsp;바인딩에는&amp;nbsp;영향을&amp;nbsp;주지&amp;nbsp;않는다. &lt;br /&gt;따라서&amp;nbsp;foo함수가&amp;nbsp;실행되는&amp;nbsp;시점의&amp;nbsp;this는&amp;nbsp;global&amp;nbsp;객체이며&amp;nbsp;getThis&amp;nbsp;메서드가&amp;nbsp;생성되는&amp;nbsp;시점의&amp;nbsp;this는&amp;nbsp;상위&amp;nbsp;스코프의&amp;nbsp;this인&amp;nbsp;global이&amp;nbsp;캡처된다.&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;&lt;b&gt;중첩된 화살표 함수에서의 this 바인딩&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 아래 코드를 예측해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1744264496588&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function foo() {
  const boo = () =&amp;gt; {
    const hoo = () =&amp;gt; {
      return this;
    };

    return hoo();
  };

  return boo();
}

console.log(foo()); // global&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;foo&amp;nbsp;함수는&amp;nbsp;함수&amp;nbsp;선언문으로&amp;nbsp;작성이&amp;nbsp;됐고,&amp;nbsp;foo&amp;nbsp;함수&amp;nbsp;내부의&amp;nbsp;boo&amp;nbsp;함수는&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;작성이&amp;nbsp;됐다. &lt;br /&gt;boo&amp;nbsp;함수&amp;nbsp;내부에는&amp;nbsp;hoo&amp;nbsp;함수가&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;작성이&amp;nbsp;되었고,&amp;nbsp;this를&amp;nbsp;반환한다. &lt;br /&gt;boo&amp;nbsp;함수는&amp;nbsp;hoo&amp;nbsp;함수를&amp;nbsp;호출해&amp;nbsp;반환하고,&amp;nbsp;foo&amp;nbsp;함수는&amp;nbsp;boo&amp;nbsp;함수를&amp;nbsp;호출해&amp;nbsp;반환한다.&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;이때&amp;nbsp;hoo&amp;nbsp;함수의&amp;nbsp;this는&amp;nbsp;foo&amp;nbsp;함수의&amp;nbsp;this를&amp;nbsp;캡쳐하고&amp;nbsp;있다. &lt;br /&gt;foo&amp;nbsp;함수의&amp;nbsp;this는&amp;nbsp;global&amp;nbsp;객체이다. &lt;br /&gt;boo&amp;nbsp;함수는&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;상위&amp;nbsp;스코프의&amp;nbsp;this를&amp;nbsp;캡처한다.&amp;nbsp;이때&amp;nbsp;상위&amp;nbsp;스코프인&amp;nbsp;foo&amp;nbsp;함수&amp;nbsp;스코프의&amp;nbsp;this는&amp;nbsp;global이다. &lt;br /&gt;hoo&amp;nbsp;함수는&amp;nbsp;화살표&amp;nbsp;함수로&amp;nbsp;상위&amp;nbsp;스코프의&amp;nbsp;this를&amp;nbsp;캡처한다.&amp;nbsp;이때&amp;nbsp;상위&amp;nbsp;스코프인&amp;nbsp;boo&amp;nbsp;함수의&amp;nbsp;this는&amp;nbsp;foo&amp;nbsp;함수의&amp;nbsp;this를&amp;nbsp;캡처했기에&amp;nbsp;global&amp;nbsp;이다.&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;따라서&amp;nbsp;hoo&amp;nbsp;함수의&amp;nbsp;this는&amp;nbsp;global&amp;nbsp;객체를&amp;nbsp;캡처하고&amp;nbsp;이를&amp;nbsp;반환한다.&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;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript의 this는 정말 다양한 케이스에서 다르게 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 선언문, 화살표 함수, 객체 클래스의 this 결정, 블록 스코프에서의 this, call apply 등 함수의 prototype 메서드에 의한 명시적 this 바인딩 등.. 이해해야 할 범위가 정말 넓다.&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;this 바인딩이 어떻게 결정되는지는 단순히 this에 대해서만 생각해서는 이해할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 컨텍스트, 함수 선언문과 화살표 함수의 차이, JavaScript라는 언어 자체를 이해해야 조금은 이해가 된다.&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;이번 this 바인딩이 조금 어렵게 느껴졌다면 아래 두 글을 읽고 다시 한번 읽어보면 조금은 이해가 될수도..&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://kangs-develop.tistory.com/71&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;[JavaScript] this 바인딩&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/73&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;[JavaScript] 실행 컨텍스트&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #ffffff; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;[JavaScript] this 바인딩&lt;/h2&gt;</description>
      <category>자바스크립트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/79</guid>
      <comments>https://kangs-develop.tistory.com/79#entry79comment</comments>
      <pubDate>Thu, 10 Apr 2025 15:03:04 +0900</pubDate>
    </item>
    <item>
      <title>회고</title>
      <link>https://kangs-develop.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 2025년 3월의 중순이다.&lt;br&gt;2024년을 정신없이 보내고 2025년을 맞이하고,&lt;br&gt;정신없는 1월 2월을 지내보내 3월 중순을 맞이하고,&lt;br&gt;이제는 숨을 고를수 있는 시간이 와서 조금씩 마음에 여유를 되찾아가고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그동안 어떤 일이 있었고 나는 어떤 생각을 했고 어떤 감정을 느꼈는지 돌아본다.&lt;br&gt;&amp;nbsp;&lt;br&gt;작년 상반기 회고 글을 작성하고 결혼에 대한 이야기를 살짝 남겨놨었다.&lt;br&gt;그렇게 나는 2월 23일 공식적인 유부남이 됐다.&lt;br&gt;작년 5월부터 진행한 결혼 준비, 대출, 신혼집 구하기, 대망의 결혼식까지..&lt;br&gt;인생의 가장 큰 숙제라고 하듯 정말 쉽지 않았고 힘들었던 일들을 흘려보냈다.&lt;br&gt;물론 결혼 준비에 있어서는 와이프가 제일 많이 고생을 했지만, 대출을 알아보고 집을 구하고 여행을 준비하는 과정도 어려움이 있었다.&lt;br&gt;결국은 무탈하게 모든 것을 이겨내고 다시 평화를 되찾았다.&lt;br&gt;마음이 평안해지고 일상을 되찾은 기분이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;요즘, 결혼식이 끝나고 신혼 여행에서 복귀해 다시 회사에 복귀했다.&lt;br&gt;그리고 날도 따뜻해지면서 지난 날의 나를 돌아보고 생각하게 됐다.&lt;br&gt;그동안 너무 여유가 없었던 것은 아닌지,&lt;br&gt;결혼 전까지 버는 족족 저축을 하며 대부분의 돈을 모으고 스트레스를 받아했던 내가&lt;br&gt;이제 인생의 가장 큰 숙제에 합격 점을 맞아서 그런지,&lt;br&gt;조금씩 너그럽게 생각해보려고 노력하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;오늘, 아니 어제, 선임 연구원으로 승진했다.&lt;br&gt;이제 4월이면 4년차 프론트엔드 개발자가 된다.&lt;br&gt;작년에 많은 어려움이 있었기에, 지금의 내가 소중하게 느껴진다.&lt;br&gt;앞으로도 어려움이 있겠지만 지나간 날을 생각하며 견뎌낼 수 있지 않을까 라는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;성장.&lt;br&gt;인간은 태어나서 죽을 때까지 성장한다.&lt;br&gt;몇년 전 부터 가장 핫한 키워드가 성장이 아닐까 생각한다.&lt;br&gt;이 성장이라는 키워드로 공포감을 불러서 마켓팅을 하기도 하며 단어 자체는 긍정적이지만, 나 자체는 이 단어를 긍정적으로 바라보고 있지는 않다.&lt;br&gt;어떤 경험에 좋은 성장, 나쁜 성장이 나뉘어져 있으리.&lt;br&gt;똑같은 것을 경험해도 다르게 느끼는 것이 사람이기 마련인데 요즘은 너무 성장이라는 단어에 포커싱을 맞추고 있는 것은 아닐까 라는 생각이다.&lt;br&gt;뒤쳐지는 것, 물경력, 불경력 이 모든 것이 사회라는 피라미드의 가장 높은 꼭대기에 앉아 있는 존재가 만들어낸 허상은 아닐지.&lt;br&gt;물경력이 어디있고 불경력이 어디있을까, 뭐든지 자신이 하는 것에 최선을 다하고 어떤 경험으로 느끼냐가 더욱 중요한 것일텐데.&lt;br&gt;요즘은 트랜디한 것을 경험하고 발을 담궜음에 포커싱이 되어 있는 것 같아서 아쉽다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그래서 난 올해 뭘 할까.&lt;br&gt;사실 구체적인 계획은 없다.&lt;br&gt;뭐, 사이드 프로젝트로 진행하고 있는 프로젝트를 계속 디벨롭 할 계획이다.&lt;br&gt;아직 여러가지 구현해야 할 기능들이 남아있기 때문이다.&lt;br&gt;또 하반기에는 대학원 석사를 진행하기 위해 지원해볼 계획이다.&lt;br&gt;한양대학교, 국민대학교를 생각중에 있는데 꼭 붙어서 계속 공부하고 싶다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지난 3년을 돌이켜보면 주니어 개발자로써 뒤쳐지지 않는 개발자가 되고 싶었다.&lt;br&gt;그래서 회사를 다니며 많은 블로그 글을 작성하고, 공부하고, 자격증을 따고, 대학교를 졸업했다.&lt;br&gt;사이드 프로젝트도 여러번, 회사에서 많은 프로젝트도 진행하고 정말 바쁘게 3년을 보냈던 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;주니어 개발자가 몇년차 까지 일까?&lt;br&gt;아직 주니어 개발자지만 4년차면 이제 조금은 업무에 익숙한 개발자인데 말이다.&lt;br&gt;앞으로 내가 해야할 일들이 많아질 것 같다.&lt;br&gt;솔직한 심정으로는 좋다. 경험해볼 것이 많으니까.&lt;br&gt;&amp;nbsp;&lt;br&gt;앞으로도 쭉 바빴으면 좋겠다.&lt;br&gt;바쁜 일상 속에서도 내 자신을 찾을수 있으면 더욱 좋을것 같기도.&lt;/p&gt;</description>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/78</guid>
      <comments>https://kangs-develop.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 15 Mar 2025 00:29:20 +0900</pubDate>
    </item>
    <item>
      <title>[MFA] Micro App의 관심사 공유</title>
      <link>https://kangs-develop.tistory.com/77</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Micro App 간의 공통 관심사가 있고, 해당 관심사를 서로 공유해 하는 케이스&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Micro Frontend Architecture에서 Micro App은 &lt;b&gt;느슨하게 &lt;/b&gt;결합됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 105226.png&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMrIpX/btsMgQxqFya/ov8RsUw9Gjx14D8pYInDik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMrIpX/btsMgQxqFya/ov8RsUw9Gjx14D8pYInDik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMrIpX/btsMgQxqFya/ov8RsUw9Gjx14D8pYInDik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMrIpX%2FbtsMgQxqFya%2Fov8RsUw9Gjx14D8pYInDik%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;775&quot; height=&quot;520&quot; data-filename=&quot;스크린샷 2025-02-13 105226.png&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;한 개의 Shell(Host) App과 3개의 Remote App이 있습니다. 각각의 Micro App은 특정 App에 의존하지 않고 각각의 포트에서 홀로 구동(Standalone)이 될 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Module Federation으로 통합될 시 구동 중인 Remote App들은 Shell App에 런타임에 통합됩니다. 이때 Remote App은 반드시 Remote App의 역할만 할 수 있는 것은 아닙니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Remote App은 특정 모듈을 expose 하여 다른 Remote App의 Host App이 될 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;위 그림에서 Remote2 App은 Remote App의 Fragment를 Remote로 사용하고 있습니다. 이 경우에 Remote2 App은 모듈을 expose하고 다른 App을 remote로 등록하기도 합니다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;ldquo;공통&amp;rdquo; 관심사&lt;/b&gt;&lt;/h3&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;만약 모든 App에서 공통 관심사(상태)가 있을 경우 어떨까요?&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각 Remote App은 하나의 플랫폼을 이루기 위해 Shell App에 통합됩니다. 개발하는 입장에선 여러 개의 App을 띄우지만, 사용자의 관점에는 자신이 여러 개의 App에 접근하는 것에 관심이 없듯 하나의 플랫폼만 (통합된 Shell App)만 바라봅니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 플랫폼에 로그인하여 사용자 인증을 한 뒤 프론트엔드는 사용자의 정보를 담고 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;모든 App은 사용자 정보에 관심이 있습니다. 이를 &lt;b&gt;공통 관심사(횡단 관점)&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 105946.png&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zx70c/btsMhNfGY0Y/X0Un37w8CVH7uqO2rXpOx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zx70c/btsMhNfGY0Y/X0Un37w8CVH7uqO2rXpOx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zx70c/btsMhNfGY0Y/X0Un37w8CVH7uqO2rXpOx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzx70c%2FbtsMhNfGY0Y%2FX0Un37w8CVH7uqO2rXpOx0%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;819&quot; height=&quot;495&quot; data-filename=&quot;스크린샷 2025-02-13 105946.png&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;614&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;예시를 들어봅니다. Shell App에 유저 정보를 위한 Store가 존재하고 로그인 기능이 붙어 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 로그인을 하면 서버로부터 유저 정보를 받아 Store에 Set 합니다. Shell App은 Store을 expose 하고, Remote App에서 해당 Store을 remote로 등록해 사용합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이런 경우 Host &amp;larr; &amp;rarr; Remote App의 순환 참조가 발생합니다. MFA 아키텍처에서 순환 참조가 발생하면 (Micro App에서 expose, remote) 디버깅이 어려워지고 강 결합이 발생하기 됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;느슨한 결합을 유지하며 의존성을 단 방향으로 유지하며 상태를 한 방향으로 흐르게 설계하는 것이 데이터의 흐름을 예측하고 예외 케이스를 디버깅 하기에 좋습니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;Module Federation의 이론 상 모든 Micro App은 Container가 될 수 있습니다. 이 말은 즉 순환 참조가 가능하다는 것 입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 2 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Remote App에서 상태를 Expose&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;순환 참조가 발생하지 않으며 가장 간단하게 상태를 공유하는 방식은 사용자 인증을 담당하는 Micro App에서 Store을 Expose 하는 방식입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;다른 Micro App에서 해당 Remote App을 remote로 등록한 후 Store을 사용하면 쉽게 상태를 공유할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 해당 방식은 Store을 사용하기 위해 반드시 Remote App을 구동해야 하는 의존성이 발생하고, 이로 인해 강 결합이 발생합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이벤트 방식의 커뮤니케이션&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각 Micro App에 User Store을 소유하고 Store의 정보가 바뀔 시 정의한 Custom Event를 Dispatch, 각 Micro App에서는 Event를 Listening 하며 Micro App의 커뮤니케이션 방식으로 상태를 공유할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 방식은 각 App이 중복으로 Store을 관리해야 한다는 점과, Custom Event를 정의하고 커뮤니케이션 하는 방식을 관리해야 한다는 점이 발생합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-prosemirror-node-inline=&quot;true&quot; data-prosemirror-node-name=&quot;inlineCard&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://dev.to/florianrappl/communication-between-micro-frontends-41fe&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Communication Between Micro Frontends&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739437174913&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;Communication Between Micro Frontends&quot; data-og-description=&quot;Having gained much experience with the implementation of various microfrontend-based solutions &amp;mdash; I&amp;rsquo;l...&quot; data-og-host=&quot;dev.to&quot; data-og-source-url=&quot;https://dev.to/florianrappl/communication-between-micro-frontends-41fe&quot; data-og-url=&quot;https://dev.to/florianrappl/communication-between-micro-frontends-41fe&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dUgVE4/hyYciIxGqo/WXRsi8vWFkF4dI8USgI0b0/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/bO3JB3/hyYajunxMN/IqxLdr2GBCgV7icZnsT5a1/img.jpg?width=1000&amp;amp;height=420&amp;amp;face=0_0_1000_420&quot;&gt;&lt;a href=&quot;https://dev.to/florianrappl/communication-between-micro-frontends-41fe&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.to/florianrappl/communication-between-micro-frontends-41fe&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dUgVE4/hyYciIxGqo/WXRsi8vWFkF4dI8USgI0b0/img.jpg?width=1000&amp;amp;height=500&amp;amp;face=0_0_1000_500,https://scrap.kakaocdn.net/dn/bO3JB3/hyYajunxMN/IqxLdr2GBCgV7icZnsT5a1/img.jpg?width=1000&amp;amp;height=420&amp;amp;face=0_0_1000_420');&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;Communication Between Micro Frontends&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Having gained much experience with the implementation of various microfrontend-based solutions &amp;mdash; I&amp;rsquo;l...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.to&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Local Storage에 저장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Store을 사용하지 않고 Local Storage를 통해 쉽게 상태를 공유할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 상태 공유를 위해 상태 관리 라이브러리를 공통으로 사용하지 않고 각 Micro App마다 독자적인 기술 스택 구성을 할 수 있다는 장점이 생깁니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Local Storage는 브라우저에 지속되는 성격이기 때문에, 만약 민감한 정보를 담고 있는 상태라면 해당 방식이 옳은 방식인지는 다시 한번 생각해봐야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;window 객체에 저장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;브라우저의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;globalThis&lt;/span&gt;는 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;window&lt;/span&gt; 객체입니다. 이를 활용해 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;window&lt;/span&gt; 객체의 프로퍼티로 특정 상태를 바인딩 하고, 이를 Micro App에서 공유할 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;window&lt;/span&gt; 객체는 JavaScript로 조작이 가능하기에 민감한 정보를 담을 수 없으며 많은 데이터를 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;window&lt;/span&gt;에 바인딩 할 시 메모리 누수가 발생할 수 있는 점, 전역 스코프가 오염될 수 있는 점을 고려해야 합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스토어 패키지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 공통 관심사를 패키징 하는 방법 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;MFA을 구현하는 경우 UI, 타입, Provider, 등 공통 관심사를 패키지로 분리합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Micro App 에서 공유하려는 상태 역시 &lt;b&gt;공통&lt;/b&gt; 관심사라는 점에 착안해 패키지로 분리하고 각 App에서 빌드 된 패키지를 사용하는 방식으로 상태를 공유할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;아래의 예시는 Monorepo/MFA 환경에서 구축된 예시입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;모노레포의 패키지 디렉토리 내 store 패키지를 만들고 라이브러리 모드로 빌드 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-13 114221.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7VLt3/btsMifJybWf/kkdZr2kWYkAfstH0ygIR7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7VLt3/btsMifJybWf/kkdZr2kWYkAfstH0ygIR7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7VLt3/btsMifJybWf/kkdZr2kWYkAfstH0ygIR7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7VLt3%2FbtsMifJybWf%2FkkdZr2kWYkAfstH0ygIR7k%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;630&quot; height=&quot;551&quot; data-filename=&quot;스크린샷 2025-02-13 114221.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각 Micro App에서 해당 Store 패키지를 install 합니다.&lt;/p&gt;
&lt;blockquote data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;Monorepo로 구성되어 있기 때문에 packages 디렉토리는 꼭 workspace에 명시되어야 합니다.&lt;br /&gt;패키지를 설치할 때는 꼭 패키지가 빌드 된 상태여야 합니다.&lt;/blockquote&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-filename=&quot;스크린샷 2025-02-13 114307.png&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwg3tH/btsMiwLdRJA/XR8A7jsjTBxlT38qsjKIG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwg3tH/btsMiwLdRJA/XR8A7jsjTBxlT38qsjKIG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwg3tH/btsMiwLdRJA/XR8A7jsjTBxlT38qsjKIG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdwg3tH%2FbtsMiwLdRJA%2FXR8A7jsjTBxlT38qsjKIG1%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;350&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2025-02-13 114307.png&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Module Federation 플러그인에서 패키지를 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;singleton&lt;/span&gt;으로 공유합니다. &lt;b&gt;꼭 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;singleton&lt;/span&gt;으로 공유&lt;/b&gt;해야 같은 chunk를 바라보고 상태가 공유됩니다!&lt;/p&gt;
&lt;pre id=&quot;code_1739437292075&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const mfConfig = {
  name: &quot;host&quot;,
  filename: &quot;remoteEntry.js&quot;,
  shared: {
    // ... 공유될 라이브러리 목록
    &quot;@mfa/store&quot;: {
      singleton: true,
    },
  },
};&lt;/code&gt;&lt;/pre&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-filename=&quot;스크린샷 2025-02-13 114558.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhKIXu/btsMh8jzjeO/2QxjNNkXgj72lBcce0ebD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhKIXu/btsMh8jzjeO/2QxjNNkXgj72lBcce0ebD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhKIXu/btsMh8jzjeO/2QxjNNkXgj72lBcce0ebD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhKIXu%2FbtsMh8jzjeO%2F2QxjNNkXgj72lBcce0ebD1%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;1516&quot; height=&quot;612&quot; data-filename=&quot;스크린샷 2025-02-13 114558.png&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;각각의 Micro App에서 해당 패키지의 Store을 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;import&lt;/span&gt;하고 사용합니다.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;같은 Shell App, Remote App 모두 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;@mfa/store&lt;/span&gt; 패키지를 설치했고, &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;singleton&lt;/span&gt;으로 라이브러리를 공유해 같은 스토어를 바라보고 있습니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Shell App에서 상태를 변경하니 Remote App에도 변경된 상태를 바라보고 반대의 경우에도 마찬가지 입니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;고려해야 할 점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;패키지를 통해 전역 상태를 각 Micro App에서 공유할 수 있게 되었으며, 상태 공유를 위해 런타임에 의존하지 않기 때문에 Micro App 이 느슨하게 결합됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이 방식으로 주의해야 할 점은 만약 상태 공유를 위해 특정 라이브러리를 사용한다면 MFA를 구성하는 App에서 상태 관리를 위해 같은 라이브러리를 사용해야 한다는 점이 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Micro Frontend는 독립된 프론트엔드 App에서 다양한 팀에 각자의 기술 스택을 사용하며 프로젝트를 진행할 수 있는 장점이 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;패키지를 통해 라이브러리를 컨벤션으로 고정하여 팀의 자율성을 해칠 수 있기 때문에 각 팀의 논의 하에 패키징이 되어야 합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/77</guid>
      <comments>https://kangs-develop.tistory.com/77#entry77comment</comments>
      <pubDate>Thu, 13 Feb 2025 18:04:07 +0900</pubDate>
    </item>
    <item>
      <title>회고: AI와 함께 MVP 만들기</title>
      <link>https://kangs-develop.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 게시글에서는 AI 제품을 활용해 MVP를 만들어본 과정을 정리하고 과정을 통해 느꼈던 저의 생각을 정리합니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MVP를 시작하기 전까지의 과정&lt;/b&gt;&lt;/h3&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;프론트엔드 개발자니깐 화면 개발은 쉽게 할 수 있을 것 이라고 생각을 했고, 서버는 Nest.js를 배워서 구현하면 된다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 막연하게 개발을 시작했지만 디자인에 재능이 없는 나에겐 아주 쉽지 않은 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인이 없으니 화면이 볼품이 없었고, AWS와 Nest에 익숙하지 않기도, 시간이 없다는 핑계로 마무리 짓지 못하고 스탑하게 됐다.&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월, v0라는 AI 제품을 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js를 만든 Vercel에서 출시한 제품인데 프롬프트로 제품을 만들어주며 심지어는 Next.js의 프론트엔드 코드도 작성해주는 것 이다.&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-filename=&quot;스크린샷 2025-02-02 오후 1.27.00.png&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8zUwg/btsL23jZanO/bE50OMd4n8y1JaPHbXtGAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8zUwg/btsL23jZanO/bE50OMd4n8y1JaPHbXtGAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8zUwg/btsL23jZanO/bE50OMd4n8y1JaPHbXtGAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8zUwg%2FbtsL23jZanO%2FbE50OMd4n8y1JaPHbXtGAk%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;523&quot; height=&quot;419&quot; data-filename=&quot;스크린샷 2025-02-02 오후 1.27.00.png&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&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;그렇게 AI 디자이너 및 기획자를 구했으니 개발에 착수했다.&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;&lt;b&gt;개발 환경 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경은 나에게 가장 익숙한 환경으로 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프론트엔드는 Next.js를 사용했다. v0가 Next.js, Tailwind css, shadcn-ui를 사용해 프론트엔드 코드를 뽑아주니 (퍼블리셔라고 해야할까) 나는 해당 코드를 바탕으로 비즈니스 로직을 붙이고 코드를 잘 추상화 하기만 하면 되는 것 이었다.&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;백엔드는 Nest.js를 사용했다. 어느정도 경험이 있었기 때문에 시작하기에 어렵지 않을 것 이라는 판단이었다. DB로는 Supabase라는 서비스를 이용했다. Supabase는 Postgres를 서빙하는 PaaS 제품이다. GUI를 통해 쉽게 DB를 관리할 수 있고 500MB까지는 무료로 사용할 수 있다. 만약 나중에 나의 MVP 제품이 고도화되어 여러 미용사 분들께서 이용하고 수익이 붙게 된다면 AWS RDB로 마이그레이션 할 생각에 있다.&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;인프라는 처음에는 AWS를 이용했다. AWS Route53에서 도메인을 구입해 ALB를 통해 트래픽을 받고, EC2로 트래픽을 보낸다. 가장 쉬운 서버 구성이었기 때문에 개발 서버까지 붙이는 것에 성공했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;s&gt;&lt;i&gt;한달에 용돈 30만원 받는 나에게&lt;/i&gt;&lt;/s&gt;&lt;/span&gt; 프리티어가 끝난 EC2 서버와 ALB 요금은 부담이 된다고 생각했다. 알아보니 NCP가 그나마 가장 저렴했고 올 11월까지 micro 서버를 프리티어로 사용할 수 있기 때문에 서버를 마이그레이션 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 지출되는 요금을 신경쓰고 싶지 않아서 도메인을 해지하고 가비아에서 500원짜리 도메인을 구입한 뒤 NCP Global Domain 서비스를 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같이 로드밸런서 &amp;gt; VM으로 트레픽이 유입된다. 공인 IP(Floating IP)를 사용하면 개당 한달에 약 4300원이 부과된다. 서버에 ssh 접근을 위해 공인 IP를 부여한 상황이라 2개의 요금이 발생한다. 로드밸런서가 WAS용 로드밸런서로 생성이 되어 ssh용 리스너를 붙일수 없다는 점이 아쉽긴 하다.&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;&lt;b&gt;AI 어시스턴트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 시중에 정말 많은 AI 제품이 있다. Chat GPT, Claude, Copilot 이 제품들은 내가 가장 많이 사용하고 있는 제품이다.&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;Copilot은 12월부터 개인적으로 요금을 내며 사용하고 있는데 개발 생산성에 아주 도움이 된다. 특히 이번 Nest.js 서버를 구현하는데 있어서 많은 도움이 됐다. Prisma(ORM)을 사용하고 있는데 처음 사용하는 기술이라 익숙하지 않았다.&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;Chat GPT를 사용해 구현하고 싶은 기능을 프롬프트 하고 Copilot의 도움을 받아 내용을 수정했다. 물론 개발자로써 이렇게 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;시대가 많이 바뀌었다. 나는 개발자를 늦게 시작한 편이다. 2020년부터 개발을 시작했다. 불과 2년전인 2022년 까지만 해도 생성형 AI가 이렇게 활성화 됐지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트러블 슈팅을 하거나 궁금한 내용이 있다면 구글을 통해 검색을 하고 StackOverflow나 Github Issue를 통해 정보를 찾아내고는 했다.&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의 수준이 많이 올라왔기에 예전과 같은 바보 대답의 빈도가 줄어들었다. 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;&lt;b&gt;개발자의 역할이 조금씩, 아니면 많이 바뀌게 된 것&lt;/b&gt;이다. 예전에는 하나의 트러블 슛팅에 많은 리소스가 들었다면, 요즘에는 하나의 트러블 슛팅이 AI와 함께라면 어렵지 않게 된 것이다. 물론 여기서 중요한 것은 진실을 파악할 수 있는 능력과 AI에게 어떤 질문을 해야하는지에 대한 지식이 필요하다는 것, 그리고 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;점점 &lt;b&gt;개발자에게 필요한 능력은 코딩 실력은 기본이고, 문제를 해결할 수 있는 능력과 빠르게 제품을 만들어 낼 수 있는지&lt;/b&gt;가 되어 갈 것이다.&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;&lt;b&gt;개발 결과물&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 과정에 대한 회고는 그다지 필요할 것 같지 않다. 프론트, 백엔드 중 하나라도 전문성이 있는 개발자라면 v0, Chat GPT를 사용해 어렵지 않게 MVP를 만들어 나갈 수 있을 것 이다.&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월 14일에 개발을 시작하여 약 2주하고 몇일 지난 시간에 초기 제품을 만들었다. 만약 나에게 팀이 있다던지, 해당 MVP 제작에만 24시간을 몰두할 수 있는 환경이 주어졌다면 일주일이면 제품을 뽑아낼 수 있지 않을까 라는 생각도 들게된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-02 오후 1.47.13.png&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwy1tU/btsL4PLb4Vk/wXGZep4mOKeaXszcASh3S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwy1tU/btsL4PLb4Vk/wXGZep4mOKeaXszcASh3S0/img.png&quot; data-alt=&quot;1월 14일 2개의 첫 커밋&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwy1tU/btsL4PLb4Vk/wXGZep4mOKeaXszcASh3S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwy1tU%2FbtsL4PLb4Vk%2FwXGZep4mOKeaXszcASh3S0%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;212&quot; height=&quot;129&quot; data-filename=&quot;스크린샷 2025-02-02 오후 1.47.13.png&quot; data-origin-width=&quot;212&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1월 14일 2개의 첫 커밋&lt;/figcaption&gt;
&lt;/figure&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;아직 MVP 제품 치고는 부족한 것들이 많다. 그 흔한 비밀번호 찾기, 변경 기능도 구현이 안됐기 때문이다. 제품에 정말 필수라고 생각되는 우선순위만 구현이 됐기 때문에 손봐야 할 부분들이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 찾기, 변경, 회원 탈퇴, 개인정보 처리정책 등.. 2월에 추가로 기능을 구현할 예정이다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;이것이 애자일인가.. ㅋㅋㅋ&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://ssalon.store&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ssalon de&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;회고를 마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 통해 많은 생각이 들었다. &lt;b&gt;개발자로써 AI의 발전을 두려워해야 하는 것인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 말 Chat GPT의 대두와 (정확한 시기는 기억이 안난다.) 함께 계속해서 논란이 되는 의문점이다. 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;&lt;b&gt;&lt;i&gt;과연?&lt;/i&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 머리속에 &lt;i&gt;&lt;b&gt;아니.&lt;/b&gt;&lt;/i&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;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;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;개발자와 디자이너 및 기획자도 마찬가지다.&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;i&gt;v0, Chat GPT가 코드를 뽑아주지만 도메인에 특화된 UI/UX를 뽑아낼 수 있는가?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;과연 AI가 고객의 요구사항을 정확히 이해할 수 있을까?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;과연 고객이 AI가 명확하게 결과물을 뽑아낼 수 있을 정도로 요구사항을 정확히 짚어낼 수 있을까? 사람이라서 가능한 일이지는 않을까?&lt;/i&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;</description>
      <category>회고</category>
      <category>AI</category>
      <category>MVP</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/76</guid>
      <comments>https://kangs-develop.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 2 Feb 2025 14:08:47 +0900</pubDate>
    </item>
    <item>
      <title>Windows ssh를 통한 Git 프로필 등록</title>
      <link>https://kangs-develop.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Git 계정을 사용하고 싶은 경우가 있습니다. 이런 경우 ssh 키를 저장소 (Github, Gitlab, Bitbucket ...)에 등록해 사용할 수 있습니다.&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;Windows에서 ssh를 사용해 여러가지 Git 프로필을 사용하는 법을 살펴보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Linux나 mac OS에서도 마찬가지의 방식으로 ssh 키 생성, 깃 허브에 ssh키 등록, config 파일을 사용해 ssh 연결 과정을 Git 프로필을 사용할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;키 파일 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 키 파일을 생성해주세요. 사용하실 이메일을 입력해 생성하시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290097689&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;&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;Ed25519 알고리즘을 지원하지 않는 레거시 시스템은 아래의 방식으로 생성해주세요. &lt;/p&gt;
&lt;pre id=&quot;code_1738290118653&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -C &quot;your_email@example.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 프롬프트는 Enter로 패스해도 무방합니다. &lt;/p&gt;
&lt;pre id=&quot;code_1738290129825&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; Enter passphrase (empty for no passphrase): [Type a passphrase]
&amp;gt; Enter same passphrase again: [Type passphrase again]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ssh agent에 SSH 키 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자 권한으로 쉘을 실행합니다. 아래 스크립트를 하나씩 입력해 ssh-agent를 시작해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1738290163913&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Get-Service -Name ssh-agent | Set-Service -StartupType Manual
Start-Service ssh-agent&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;ssh agent에 ssh키를 추가합니다. 아래&amp;nbsp;경로&amp;nbsp;중&amp;nbsp;YOUR에는&amp;nbsp;사용자&amp;nbsp;명을&amp;nbsp;넣어주셔야&amp;nbsp;합니다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;pub키가 아닌 비밀키를 등록&lt;/b&gt;&lt;/span&gt;하셔야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290218520&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-add c:/USERS/{YOUR}/.ssh/id_ed25519&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Git에&amp;nbsp;ssh키&amp;nbsp;추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단 설정 &amp;gt; Settings &amp;gt; 좌측 메뉴의 SSH and GPG keys 로 접속 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH Keys의 New SSH Key를 클릭합니다.&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;Title:&amp;nbsp;키&amp;nbsp;명칭 &lt;br /&gt;Key:&amp;nbsp;pub키&amp;nbsp;내용&amp;nbsp;복사&amp;nbsp;후&amp;nbsp;등록&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;쉘 기준으로 아래 명령어로 pub키를 확인할 수 있습니다. id_ed25519는&amp;nbsp;key&amp;nbsp;명칭입니다.&amp;nbsp;생성한&amp;nbsp;키&amp;nbsp;명칭을&amp;nbsp;확인해주세요. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ssh-ed25519&amp;nbsp;~~~~~&amp;nbsp;your_email@example.com&lt;/b&gt;&amp;nbsp;이&amp;nbsp;확인될&amp;nbsp;것&amp;nbsp;입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290297460&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat ~/.ssh/id_ed25519.pub&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;config에&amp;nbsp;ssh&amp;nbsp;등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;./ssh&amp;nbsp;폴더에&amp;nbsp;config&amp;nbsp;파일을&amp;nbsp;수정&amp;nbsp;(없다면&amp;nbsp;생성)합니다. &lt;br /&gt;윈도우의&amp;nbsp;경우&amp;nbsp;확장자&amp;nbsp;없이&amp;nbsp;config&amp;nbsp;파일을&amp;nbsp;생성&amp;nbsp;후&amp;nbsp;메모장으로&amp;nbsp;열어서&amp;nbsp;수정하시면&amp;nbsp;됩니다. &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;b&gt;C:/Users/{YOUR}/.ssh/config&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290333146&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Host {호스트명}
    HostName {호스트 이름}
    User {사용할 유저 명칭}
    IdentityFile {키파일 위치}

# 예시
Host github.com-kangactor123
    HostName github.com
    User kangactor123
    IdentityFile ~/.ssh/id_ed25519&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ssh&amp;nbsp;연결&amp;nbsp;테스트&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1738290356149&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -T git@호스트명칭

# 예시
ssh -T git@github.com-kangactor123&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;아래&amp;nbsp;내용이&amp;nbsp;나오면&amp;nbsp;ssh&amp;nbsp;연결이&amp;nbsp;성공적으로&amp;nbsp;된&amp;nbsp;것&amp;nbsp;입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290374443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hi {사용할 유저 명칭} You've successfully authenticated, but GitHub does not provide shell access.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ssh로&amp;nbsp;clone&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리&amp;nbsp;우측&amp;nbsp;상단의&amp;nbsp;Code&amp;nbsp;클릭 &lt;br /&gt;Local&amp;nbsp;&amp;gt;&amp;nbsp;Clone의&amp;nbsp;SSH&amp;nbsp;클릭&amp;nbsp;후&amp;nbsp;복사&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;i&gt;&lt;b&gt;Git bash 혹은 PowerShell&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738290410425&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone git@호스트명:유저명/저장소명.git

# 예시
git clone git@github.com-kangactor123:kangactor123/ssalon-fe.git&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;사용자의&amp;nbsp;계정을&amp;nbsp;변경하고&amp;nbsp;싶은&amp;nbsp;경우&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌한&amp;nbsp;설정을&amp;nbsp;변경하는&amp;nbsp;것이&amp;nbsp;아닌&amp;nbsp;로컬의&amp;nbsp;설정을&amp;nbsp;변경하는&amp;nbsp;것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738290442171&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git remote set-url origin git@github.com-user1:user1/저장소명.git
git config user.email user1@example.com

# 예시
git remote set-url origin git@github.com-kangactor123:kangactor123/ssalon-fe.git
git config user.email kangactor123@naver.com&lt;/code&gt;&lt;/pre&gt;</description>
      <category>이것저것</category>
      <category>git</category>
      <category>ssh</category>
      <category>프로필</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/75</guid>
      <comments>https://kangs-develop.tistory.com/75#entry75comment</comments>
      <pubDate>Fri, 31 Jan 2025 11:31:17 +0900</pubDate>
    </item>
    <item>
      <title>[React] 합성(Composition) 컴포넌트</title>
      <link>https://kangs-develop.tistory.com/74</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 합성 컴포넌트에 대해서 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합성 컴포넌트는 컴포넌트를 조합하여 UI를 만드는 것으로 컴포넌트의 재사용성을 극대화 시키는 패턴입니다.&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;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;/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;&lt;b&gt;예제&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일반 컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트를 구현해야 하는 상황이 발생 했습니다. 초기에는 단순히 라벨만 노출시키면 되는 단순한 리스트입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735709895271&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Props = {
  items: Item[];
};

const List: React.FC&amp;lt;Props&amp;gt; = ({ items }) =&amp;gt; {
  return (
    &amp;lt;ul&amp;gt;
      {items.map(({ id, label }) =&amp;gt; {
        return (
          &amp;lt;li key={id}&amp;gt;
            {label}
          &amp;lt;/li&amp;gt;
        );
      })}
    &amp;lt;/ul&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;&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;리스트 컴포넌트에 selectItem 이라는 state를 만들었습니다. 그리고 li 태그 안에 title을 가진 h3 태그도 만들어줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710086561&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Props = {
  items: Item[];
};

const List: React.FC&amp;lt;Props&amp;gt; = ({ items }) =&amp;gt; {
  const [selectItem, setSelectItem] = useState(&quot;&quot;);
  return (
    &amp;lt;ul&amp;gt;
      {items.map(({ id, label }) =&amp;gt; {
        const isSelected = selectItem === id;
        const color = isSelected ? &quot;red&quot; : &quot;black&quot;;
        
        return (
          &amp;lt;li key={id} onClick={() =&amp;gt; setSelectItem(id)} style={{ color }}&amp;gt;
            {title &amp;amp;&amp;amp; &amp;lt;h3 style={{ margin: 0, color }}&amp;gt;{title}&amp;lt;/h3&amp;gt;}
            {label}
          &amp;lt;/li&amp;gt;
        );
      })}
    &amp;lt;/ul&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;또 구분선이 필요하다는 요구사항이 들어왔습니다. List 컴포넌트에 hr 태그를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 컴포넌트는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710129409&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from &quot;react&quot;;

export type Item = {
  id: string;
  label: string;
  title?: string;
  split?: boolean;
};

type Props = {
  items: Item[];
};

const List: React.FC&amp;lt;Props&amp;gt; = ({ items }) =&amp;gt; {
  const [selectItem, setSelectItem] = useState(&quot;&quot;);
  return (
    &amp;lt;ul&amp;gt;
      {items.map(({ id, title, label, split }) =&amp;gt; {
        const isSelected = selectItem === id;
        const color = isSelected ? &quot;red&quot; : &quot;black&quot;;
        return (
          &amp;lt;React.Fragment key={id}&amp;gt;
            &amp;lt;li onClick={() =&amp;gt; setSelectItem(id)} style={{ color }}&amp;gt;
              {title &amp;amp;&amp;amp; &amp;lt;h3 style={{ margin: 0, color }}&amp;gt;{title}&amp;lt;/h3&amp;gt;}
              {label}
            &amp;lt;/li&amp;gt;
            {split &amp;amp;&amp;amp; &amp;lt;hr /&amp;gt;}
          &amp;lt;/React.Fragment&amp;gt;
        );
      })}
    &amp;lt;/ul&amp;gt;
  );
};

export default List;&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;위 컴포넌트를 사용하는 예시입니다. 추상화 된 객체를 정의하고 List 컴포넌트에 넘겨주고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710323516&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import List, { type Item as ItemType } from &quot;./list&quot;;

export default function App() {
  const items: ItemType[] = [
    { id: &quot;id-1&quot;, label: &quot;first&quot;, title: &quot;title&quot; },
    { id: &quot;id-2&quot;, label: &quot;second&quot;, split: true },
    { id: &quot;id-3&quot;, label: &quot;third&quot; },
  ];

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;List items={items} /&amp;gt;
    &amp;lt;/div&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;List 컴포넌트에 기능을 구현하기 위해 여러가지 비즈니스 로직이 섞였습니다. 물론 &lt;b&gt;li&lt;/b&gt; 컴포넌트를 분리하여 사용자 정의 컴포넌트로 구현하여 List 컴포넌트를 단순하게 할 수 있습니다.&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;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;즉 특정 기능을 붙이기 위해서 List 컴포넌트를 계속해서 수정해야 하고 비즈니스 로직이 섞이고 List 컴포넌트는 관리 포인트가 늘어나며 유지보수가 어렵게 됩니다. 기능이 붙으면 붙을수록 재사용성도 떨어지게 됩니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;복합 컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 컴포넌트를 구현하기 위해서 List 컴포넌트에서 li 컴포넌트를 분리할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 분리된 각 컴포넌트에서 상태를 공유하기 위해 Context API를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710461103&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { createContext, PropsWithChildren, useState } from &quot;react&quot;;

type ListContextType = {
  selectItem: string;
  setSelectItem: (id: string) =&amp;gt; void;
};

export const ListContext = createContext&amp;lt;ListContextType&amp;gt;({
  selectItem: &quot;&quot;,
  setSelectItem: () =&amp;gt; {},
});

const CompositionList: React.FC&amp;lt;PropsWithChildren&amp;gt; = ({ children }) =&amp;gt; {
  const [selectItem, setSelectItem] = useState(&quot;&quot;);

  return (
    &amp;lt;ListContext.Provider
      value={{ selectItem, setSelectItem: (id) =&amp;gt; setSelectItem(id) }}
    &amp;gt;
      &amp;lt;ul&amp;gt;{children}&amp;lt;/ul&amp;gt;
    &amp;lt;/ListContext.Provider&amp;gt;
  );
};

export default CompositionList;&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;selectItem 상태는 List 컴포넌트에서 소유하고 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 상태는 children에 props로 넘겨주기 보다는 컴포넌트 간에 느슨하게 결합할 수 있도록 Context API를 통해 상태를 공유하도록 합니다.&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;이제 li 컴포넌트를 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710714430&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { PropsWithChildren, useContext, useMemo } from &quot;react&quot;;
import { ListContext } from &quot;./composition-list&quot;;

import &quot;./item.css&quot;;

type Props = PropsWithChildren&amp;lt;{
  id: string;
}&amp;gt;;

const Item: React.FC&amp;lt;Props&amp;gt; = ({ id, children }) =&amp;gt; {
  const { selectItem, setSelectItem } = useContext(ListContext);

  const isSelected = selectItem === id;
  const className = useMemo(
    () =&amp;gt; [&quot;list&quot;, ...(isSelected ? [&quot;selected&quot;] : [])].join(&quot; &quot;),
    [isSelected]
  );

  return (
    &amp;lt;li onClick={() =&amp;gt; setSelectItem(id)} className={className}&amp;gt;
      {children}
    &amp;lt;/li&amp;gt;
  );
};

export default React.memo(Item);&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;Item 컴포넌트에서 Context API를 통해 상태를 공유하고 있습니다.&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;이 두 개의 컴포넌트를 합성해 UI를 만들어봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1735710856493&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import CompositionList from &quot;./composition-list&quot;;
import Item from &quot;./item&quot;;

export default function App() {
  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;CompositionList&amp;gt;
        &amp;lt;Item id=&quot;id-1&quot;&amp;gt;
          &amp;lt;h3&amp;gt;title&amp;lt;/h3&amp;gt;
          first
        &amp;lt;/Item&amp;gt;
        &amp;lt;Item id=&quot;id-2&quot;&amp;gt;second&amp;lt;/Item&amp;gt;
        &amp;lt;hr /&amp;gt;
        &amp;lt;Item id=&quot;id-3&quot;&amp;gt;third&amp;lt;/Item&amp;gt;
      &amp;lt;/CompositionList&amp;gt;
    &amp;lt;/div&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;이전에는 list를 추상화 해서 넘겨줬던 반면에, 합성 컴포넌트를 사용해 더욱 직관적이고 선언적으로 UI를 구현하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 title 기능과 split 기능은 실제 UI를 구성하는 App 컴포넌트에서 관리하고 List와 Item은 모두 각각의 역할에 충실하고 있습니다. &lt;b&gt;합성 컴포넌트 패턴으로 느슨하게 UI를 구현 함으로써 다른 기능을 추가할 때 더욱 직관적이고 간편하게 구현&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;예를 들어서 체크 기능을 통해 특정 Item을 숨김 처리를 하는 기능이 추가된다고 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합성 컴포넌트 패턴을 이용한다면 기능 추가를 위해 List, Item 컴포넌트를 직접 수정하지 않고 유연하게 기능을 추가할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735711426172&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function App() {
  const [checked, setChecked] = useState(false);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;Checkbox
        checked={checked}
        setChecked={(checked) =&amp;gt; setChecked(checked)}
      /&amp;gt;
      &amp;lt;CompositionList&amp;gt;
        &amp;lt;Item id=&quot;id-1&quot;&amp;gt;
          &amp;lt;h3&amp;gt;title&amp;lt;/h3&amp;gt;
          first
        &amp;lt;/Item&amp;gt;
        {checked &amp;amp;&amp;amp; (
          &amp;lt;&amp;gt;
            &amp;lt;Item id=&quot;id-2&quot;&amp;gt;second&amp;lt;/Item&amp;gt;
            &amp;lt;hr /&amp;gt;
          &amp;lt;/&amp;gt;
        )}
        &amp;lt;Item id=&quot;id-3&quot;&amp;gt;third&amp;lt;/Item&amp;gt;
      &amp;lt;/CompositionList&amp;gt;
    &amp;lt;/div&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;만약 List 컴포넌트를 직접 수정하게 된다면 사이드 이펙트 유무를 체크하기 위해 해당 컴포넌트를 사용하는 모든 곳을 테스트 해봐야 합니다. 합성 컴포넌트를 사용한다면 유연하게 대응할 수 있기 때문에 사이드 이펙트의 위험에서 벗어날 수 있습니다.&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;&lt;b&gt;마치며&lt;/b&gt;&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;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;p data-ke-size=&quot;size16&quot;&gt;읽어봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>react</category>
      <category>합성 컴포넌트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/74</guid>
      <comments>https://kangs-develop.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 1 Jan 2025 15:11:59 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 실행 컨텍스트</title>
      <link>https://kangs-develop.tistory.com/73</link>
      <description>&lt;h1 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h1&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 소스코드를 4가지 타입으로 구분한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;전역 코드
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;전역에 존재하는 소스코드, 전역에 정의된 함수, 클래스 등 내부 코드는 포함되지 않는다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;전역 스코프를 생성하며 전역 코드가 평가되면 전역 실행 컨텍스트가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;함수 코드
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;함수 내부에 존재하는 소스코드, 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함하지 않는다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;지역 스코프를 생성하며 지역변수, 매개변수, arguments 객체를 관리한다. 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결한다. 함수 코드가 평가되면 함수 실행 컨텍스트가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;eval 코드: eval 함수에 인수로 전달되어 실행되는 소스코드&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;모듈 코드: 모듈 내부에 존재하는 소스코드, 모듈 내부의 함수, 클래스 등 내부 코드는 포함하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;소스코드의 평가와 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트 엔진은 소스코드를 평가와 실행 단계로 나눠서 처리한다. 코드의 평가 단계에서는 실행 컨텍스트가 생성되고 &lt;b&gt;작성된 변수, 함수, 클래스 등 선언문이 실행이 되어&lt;/b&gt;(호이스팅) &lt;b&gt;실행 컨텍스트의 렉시컬 환경의 환경 레코드에 등록&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드의 평가 단계가 끝나면 실행 단계(런타임)이 시작되는데 이때 소스코드를 읽어나가며 필요한 정보를 실행 컨텍스트에서 관리하는 스코프에서 검색해 얻게된다. 소스코드의 실행 결과는 실행 컨텍스트에서 관리하는 스코프에 등록이 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;아래 코드를 살펴보며 소스코드가 어떻게 실행이 되는지 예상해보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;var a = 1;
var b = 2;
const d = &quot;c&quot;;

function foo() {
  return a + b;
}

var c = foo();
console.log(c);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;자바스크립트 엔진이 소스코드 평가&lt;/b&gt;를 시작한다. 전역 객체가 생성이 된다. (window, global)&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;여기서 주의해야 할 점은 전역 객체가 전역 실행 컨텍스트가 아니라는 점이다. 전역 객체는 전역 실행 컨텍스트가 생성되기 이전에 생성된다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&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-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;선언된 식별자를 &lt;b&gt;전역 환경 레코드에 등록&lt;/b&gt;한다. 여기서 a, b, c 식별자의 선언과 암묵적 초기화가 발생하고 foo 함수 객체가 생성이 되어 메모리에 암묵적으로 foo 라는 식별자가 생성이되어 해당 함수 객체를 바라본다. 이때 선언된 전역 변수와 함수 객체는 전역 실행 컨텍스트가 관리하는 전역 스코프에 등록이 되며 전역 객체의 프로퍼티와 메서드가 된다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;렉시컬 환경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;렉시컬 환경은 실행 컨텍스트 영역에 어떤 코드들이 작성이 되어있는지에 대한 정보를 담고 있는 컴포넌트다. 즉 &lt;b&gt;식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 자료구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;렉시컬 환경은 &lt;b&gt;환경 레코드&lt;/b&gt;([[EnvironmentRecord]])와 &lt;b&gt;외부 렉시컬 환경 참조&lt;/b&gt;(OuterLexicalEnvironmentReference)로 구성된다. 환경 레코드는 해당 렉시컬 환경을 구성하는 식별자와 값을 담고 있으며 외부 렉시컬 환경에 대한 참조 값은 말 그대로 상위 스코프에 대한 참조 값을 가지고 있다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;전역 환경 레코드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 환경 레코드는 객체 환경 레코드(Object Environment Record)와 선언적 환경 레코드(Declarative Environment Record)로 구성된다. 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 전역 객체가 this 바인딩 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;객체 환경 레코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 객체가 관리하던 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의한 전역 함수, 빌트인 전역 프로퍼티와 빌트인 전역 함수, 표준 빌트인 객체를 관리한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 객체 환경 레코드는 전역 객체를 생성할 때 생성된 &lt;b&gt;전역 객체(BindingObject)와 연결&lt;/b&gt;된다. 전역 코드 평가 단계에서 &lt;b&gt;var 키워드의 전역 변수와 함수 선언문으로 정의된 전역 함수는 BindingObject를 통해 전역 객체의 프로퍼티와 메서드&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;var 키워드의 전역 변수와 함수 선언문이 전역 객체의 프로퍼티, 메서드가 되는 이유이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;var 키워드의 전역 변수 코드의 평가 단계에서 선언과 undefined 초기화가 발생하고 전역 객체 환경 레코드에 등록된다.&lt;br /&gt;&lt;br /&gt;함수 선언문으로 작성된 전역 함수는 코드의 평가 단계에서 함수 객체가 생성되고 암묵적으로 함수 명의 식별자가 생성이 되어 그 즉시 함수 객체를 할당하고 전역 객체 환경 레코드에 등록된다.&lt;br /&gt;&lt;br /&gt;그렇기 때문에 var 키워드의 식별자는 선언문을 만나기 이전에 참조를 할 수 있지만 undefined가 바인딩이 되어있고, 함수 선언문으로 작성된 함수는 선언문을 만나기 이전에 호출을 해도 해당 함수를 호출할 수 있는 것이다.&lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;textColor&quot; data-prosemirror-content-type=&quot;mark&quot; data-text-custom-color=&quot;#0747a6&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;선언적 환경 레코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;let, const 키워드로 선언한 전역 변수를 관리한다. 즉 let, const로 선언된 식별자는 선언적 환경 레코드에 등록이 된다. 이 말은 let, const 키워드로 선언된 식별자는 전역 객체의 프로퍼티로 등록되지 않는다는 것이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;const 키워드로 선언된 식별자는 선언 단계와 초기화 단계가 분리된다. 즉 초기화 단계 (런타임에 실행 흐름이 변수 선언문에 도달하기 이전까지) 일시적 사각 지대(TDZ)에 빠지게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩&lt;/b&gt; 된다. 여기서의 this 바인딩은 전역 객체인 window, global 이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;this 바인딩&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 환경 레코드에 전역 객체가 this 바인딩이 된다. 환경 레코드 내부의 객체 환경 레코드와 선언적 환경 레코드에는 this 바인딩이 발생하지 않는다. this 바인딩은 전역 환경 레코드와 함수 환경 레코드에만 발생한다.&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-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;외부 렉시컬 환경에 대한 참조가 결정&lt;/b&gt;된다. 전역 소스코드는 자신을 포함하는 소스코드가 없으므로 &lt;b&gt;전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 null이 바인딩&lt;/b&gt; 된다. 이는 전역 렉시컬 환경이 스코프 체인의 종점에 존재한다는 것을 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 실행 컨텍스트는 실행 컨텍스트 스택에 push&lt;/b&gt; 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EIUsT/btsLjKjxzua/k3VC7kBvF5AuYlVDcrkki1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EIUsT/btsLjKjxzua/k3VC7kBvF5AuYlVDcrkki1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EIUsT/btsLjKjxzua/k3VC7kBvF5AuYlVDcrkki1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEIUsT%2FbtsLjKjxzua%2Fk3VC7kBvF5AuYlVDcrkki1%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;2924&quot; height=&quot;1564&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;/figure&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;위 그림에서 GlobalThisValue가 전역 렉시컬 환경의 컴포넌트로 오해할 수 있다. GlobalThisValue는 전역 렉시컬 환경의 내부 슬롯이다. ([[ ]] 로 표현한다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;평가 단계가 끝나고 전역 코드를 실행&lt;/b&gt;한다. 스코프에서 a, b, c, d 라는 식별자를 검색해 값을 할당한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIG8vP/btsLjEw1h0R/T2CDDYar00PFxxjWdlTHek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIG8vP/btsLjEw1h0R/T2CDDYar00PFxxjWdlTHek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIG8vP/btsLjEw1h0R/T2CDDYar00PFxxjWdlTHek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIG8vP%2FbtsLjEw1h0R%2FT2CDDYar00PFxxjWdlTHek%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;2896&quot; height=&quot;1532&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1532&quot;/&gt;&lt;/span&gt;&lt;/figure&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-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;8행에서 foo 실행문을 만나 foo 함수 객체를 실행한다. 이때 전역 코드의 실행이 멈추고 &lt;b&gt;foo&lt;/b&gt; &lt;b&gt;함수 코드 평가를 시작&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;함수 코드를 평가하면서 함수 실행 컨텍스트가 생성&lt;/b&gt;되고, 매개변수와 지역변수 선언문이 실행이 되어 함수 실행 컨텍스트의 환경 레코드에 등록된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;함수 실행 컨텍스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수 실행 컨텍스트는 &lt;b&gt;렉시컬 환경과 외부 렉시컬 환경 참조 값&lt;/b&gt;을 갖고, 렉시컬 환경에는 &lt;b&gt;함수 환경 레코드가 바인딩&lt;/b&gt; 된다. 함수 환경 레코드에는 함수 내부에 선언된 식별자가 등록이 되며 arguments 객체가 생성이 되어 바인딩 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부 렉시컬 환경 참조 값은 함수를 실행한 실행 컨텍스트의 렉시컬 환경 참조 값이 바인딩&lt;/b&gt; 된다. foo 함수는 전역 실행 컨텍스트에 의해 호출이 되었기 때문에 외부 렉시컬 환경 참조 값으로 전역 렉시컬 환경 참조 값이 바인딩 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;렉시컬 환경의 [[ThisValue]]에&lt;b&gt; this 바인딩이 결정&lt;/b&gt;이 된다. 함수는 어떤 방식으로 호출이 되었는지에 따라 this 바인딩이 다르게 결정이 된다. 이 예문에서는 함수 선언문으로 호출이 되었기 때문에 전역 객체로 this 바인딩이 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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-filename=&quot;함수 실행 컨텍스트.png&quot; data-origin-width=&quot;2904&quot; data-origin-height=&quot;1588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bziMBc/btsLj1egO24/JUgJuz7AtTxHIcF8iLx8l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bziMBc/btsLj1egO24/JUgJuz7AtTxHIcF8iLx8l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bziMBc/btsLj1egO24/JUgJuz7AtTxHIcF8iLx8l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbziMBc%2FbtsLj1egO24%2FJUgJuz7AtTxHIcF8iLx8l0%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;2904&quot; height=&quot;1588&quot; data-filename=&quot;함수 실행 컨텍스트.png&quot; data-origin-width=&quot;2904&quot; data-origin-height=&quot;1588&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 코드 평가가 종료되고 함수가 실행된다. 함수 실행 컨텍스트에서 a, b 식별자를 검색할 수 없기에 외부 렉시컬 환경 참조 값으로 상위 스코프에서 식별자를 검색한다. 전역 스코프에 등록된 a, b 식별자를 찾고 해당 값을 사용해 값을 반환한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수 실행이 종료되고 실행 컨텍스트 스택에서 Pop 된다. c 식별자에 반환 된 값 (6)이 재할당 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;실행 컨텍스트 스택에서 pop 되고 소멸이 됐지만 렉시컬 환경도 무조건 소멸이 되는 것은 아니다.&lt;br /&gt;만약 렉시컬 환경이 다른 렉시컬 환경에 의해 참조가 되고 있다면 해당 렉시컬 환경은 메모리에 남아있게 된다.&lt;br /&gt;위 개념은 다음 스터디 회차 클로저에서 자세하게 살펴보도록 하겠다.&lt;/blockquote&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;함수 실행 컨텍스트 2.png&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjUhuv/btsLj07vTr3/xhgTnnjA2b2BgotKlmatm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjUhuv/btsLj07vTr3/xhgTnnjA2b2BgotKlmatm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjUhuv/btsLj07vTr3/xhgTnnjA2b2BgotKlmatm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjUhuv%2FbtsLj07vTr3%2FxhgTnnjA2b2BgotKlmatm1%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;2896&quot; height=&quot;1532&quot; data-filename=&quot;함수 실행 컨텍스트 2.png&quot; data-origin-width=&quot;2896&quot; data-origin-height=&quot;1532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 소스 코드의 실행이 끝날 때 실행 컨텍스트 스택에서 전역 실행 컨텍스트를 pop 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위의 모든 과정을 관리하는 것이 &lt;b&gt;실행 컨텍스트&lt;/b&gt;이다. 실행 컨텍스트는 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 관리하는 영역이다. 선언된 식별자와 스코프는 렉시컬 환경으로, 코드의 실행 순서는 실행 컨텍스트 스택으로 관리한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실행 컨텍스트와 블록 레벨 스코프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;var 키워드는 함수 레벨 스코프를, let const 키워드는 블록 레벨 스코프를 갖는다고 했다. 함수는 실행 컨텍스트를 가지고 있지만 블록은 별도의 실행 컨텍스트가 존재하지 않는다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;그럼 블록 레벨 스코프는 어떤 방식으로 관리가 되는 것일까?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;var a = 1;
var b = 2;

{
  const a = 1;
  const b = &quot;c&quot;;
  console.log(a + b);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위 코드에는 전역 코드에서 코드 블럭으로 인해 블록 스코프가 생성이 된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;이를 관리하기 위해 &lt;b&gt;선언적 환경 레코드를 갖는 블록 렉시컬 환경을 생성을 하고 전역 렉시컬 환경을 교체&lt;/b&gt;한다. 블록 렉시컬 환경은 외부 렉시컬 환경 참조 값으로 &lt;b&gt;기존의 전역 렉시컬 환경을 가리킨다&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;블록1.png&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcDm8o/btsLjswEFQY/izOAbgXfOVLyMO7EfNsjk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcDm8o/btsLjswEFQY/izOAbgXfOVLyMO7EfNsjk0/img.png&quot; data-alt=&quot;실행 컨텍스트 스택에서는 아무런 일이 발생하지 않는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcDm8o/btsLjswEFQY/izOAbgXfOVLyMO7EfNsjk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcDm8o%2FbtsLjswEFQY%2FizOAbgXfOVLyMO7EfNsjk0%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;2940&quot; height=&quot;1490&quot; data-filename=&quot;블록1.png&quot; data-origin-width=&quot;2940&quot; data-origin-height=&quot;1490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행 컨텍스트 스택에서는 아무런 일이 발생하지 않는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드 블럭의 실행이 종료되면 전역 렉시컬 환경은 다시 기존의 전역 렉시컬 환경을 가리킨다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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-filename=&quot;블록2.png&quot; data-origin-width=&quot;2950&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOIDtz/btsLjuabGqM/NriCXG4whw24G3i5itgPaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOIDtz/btsLjuabGqM/NriCXG4whw24G3i5itgPaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOIDtz/btsLjuabGqM/NriCXG4whw24G3i5itgPaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOIDtz%2FbtsLjuabGqM%2FNriCXG4whw24G3i5itgPaK%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;2950&quot; height=&quot;1480&quot; data-filename=&quot;블록2.png&quot; data-origin-width=&quot;2950&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>실행 컨텍스트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/73</guid>
      <comments>https://kangs-develop.tistory.com/73#entry73comment</comments>
      <pubDate>Sun, 15 Dec 2024 15:28:37 +0900</pubDate>
    </item>
    <item>
      <title>[소프트웨어] 리팩토링을 대하는 자세</title>
      <link>https://kangs-develop.tistory.com/72</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TjTfN/btsLjaXaHu1/ckoFwnY4lD1KaZgFDrO5ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TjTfN/btsLjaXaHu1/ckoFwnY4lD1KaZgFDrO5ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TjTfN/btsLjaXaHu1/ckoFwnY4lD1KaZgFDrO5ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTjTfN%2FbtsLjaXaHu1%2FckoFwnY4lD1KaZgFDrO5ck%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;1060&quot; height=&quot;622&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링은 소프트웨어 엔지니어라면 뗄 수 없는 업무입니다.&lt;br&gt;소프트웨어의 레거시를 하나씩 새로운 코드로 갈아 끼우거나, 쌓여있는&lt;span style=&quot;color: #333333;&quot;&gt; 기술 부채를 청산하는 행위를 리팩토링이라고 합니다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;좋은 소프트웨어를 만들기 위해서는 반드시 리팩토링이 필요하고, 소프트웨어의 수명을 연장하기 위해서도 리팩토링이 필요합니다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 포스팅에서는 리팩토링을 대하는 자세에 대한 생각을 남겨봅니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;요 근래 회사에서 많은 리팩토링을 진행하고 있습니다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;보통 리팩토링을 한다면 작은 범위에서의 리팩토링을 진행하며 예측할 수 있는 범위 내에서 모듈 단위의 리팩토링을 생각하고는 합니다. 하지만&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최근 제가 진행하고 있는 리팩토링은 프로덕트 프론트엔드의 수명을 연장하기 위해 진행하고 있는 리팩토링 입니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;잠깐, 수명을 연장하는 리팩토링이 뭘까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;수명을 연장하는 리팩토링은 또 무엇일까요? 요즘의 프론트엔드는 다양한 라이브러리에 의존하고 있습니다. 자바스크립트 런타임 환경인 Node.js부터 React와 서드파티 라이브러리 등 프로젝트 내에서 다양한 라이브러리를 사용하고 있습니다.&lt;br&gt;가장 중요한 것은 소프트웨어 버전에는 EOL(End of Life)가 존재한다는 것 입니다.&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단편적인 예시로 Vite가 지원하지 않는 Node.js의 버전이 존재합니다. 만약 프로젝트에서 사용하는 소프트웨어의 버전 관리를 제때 하지 않는다면 기술 부채가 쌓이게 되어 청산하기 어려워질 수 있습니다. 이런 경우에 최신 환경으로 프로젝트를 새로 포팅해야 한다는 점에 더욱 많은 비용이 소모할 수 있습니다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;저의 사례를 살펴보겠습니다.&lt;br&gt;기존의 프로덕트 환경은 Node(14v), npm, Webpack4, React(16v), TypeScript(3v)를 사용하고 있었습니다. 물론 해당 버전들이 아주 레거시이다, 생명주기가 끝났다 라고는 볼 수 없습니다. 아직도 사용하고 계신 분들이 많을 것 이니깐요.&lt;br&gt;&amp;nbsp;&lt;br&gt;제품을 개발하다가 문득 불편함을 느꼈습니다. 저의 하드디스크 용량이 256GB인데 레포지토리 용량이 90G가 넘었기 때문입니다. 이를 살펴보니 Webpack4를 사용하면서 HMR되는 모듈을 모두 빌드하고 node_modules에 저장하고 있기 때문이였습니다. 또한 Webpack4의 특성 상 코드를 수정하면 HMR을 위해 재빌드를 하는데 이 과정에서 상당한 시간이 소요된다는 점이 있었습니다.&lt;br&gt;또 npm을 사용하며 팬텀 디팬던시와 디스크 효율성에 대한 불편함, 빌드와 의존성 설치 시간이 상당 시간 소요된다는 점이 눈에 띄게 불편하다는 생각을 했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이러한 불편함과 지금의 버전이 점점 최신 버전과 멀어져가고 있었고 개선 없이 계속 프로젝트를 진행하게 된다면, 앞으로 최신 환경에 익숙한 개발자분들이 합류 한다면 저희 프로젝트에 온보딩 하기 어려울 것이라고 생각했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 &lt;b&gt;프로덕트 프론트엔드의 수명을 늘리기 위해서는 최신의 환경으로 마이그레이션 해야한다는 생각&lt;/b&gt;으로 대규모 리팩토링을 시작하게 됐습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리팩토링을 대하는 자세&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링을 대하는 자세가 따로 있을까요? 레거시라고 생각하는 모듈은 팀원들과 협의하여 그냥 개선하면 되지 않을까요?&lt;br&gt;물론 현재 진행중인 팀원들과 협의하에 리팩토링을 진행하는 것은 아주 중요한 자세라고 생각합니다. 하지만 제일 중요하다고 생각하는 점은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;레거시에 대한 존중&lt;/b&gt;&lt;/span&gt;이라고 생각합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;프로젝트를 진행하다 레거시 코드를 보고 이거 왜 이렇게 만들었지? 라는 생각을 많이 해보셨을 것 이라고 생각합니다. 필자인 저도 레거시 코드를 보면 그런 생각도 들고, 심지어 제가 작성한 코드를 보고도 그런 생각이 들기 때문입니다. 하지만 그렇게 만든 것은 다 이유가 있었기 때문입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;소프트웨어 엔지니어는 항상 그 시점의 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;최선의 선택&lt;/b&gt;&lt;/span&gt;을 합니다. 여기서 중요한 점은 최고의 선택을 한다는 것이 아닌, &lt;b&gt;최선의 선택을 한다는 것&lt;/b&gt; 입니다. 누구나 보기 좋은 클린 코드 조금 더 깊게 고민해서 만드는 코드도 중요합니다. 하지만 조금 더 중요한 것은 기한을 지키며 문제를 해결할 수 있는 코드입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가령 어떤 문제를 해결하기 위해 A, B의 선택지가 존재하며 A의 선택지가 최고의 해결책이라고 판단이 듭니다. 하지만 A를 선택하기 위해서는 러닝 커브가 발생하며 때에 따라서 의존성 문제가 발생할 수 있고 시간의 문제가 발생할 수 있습니다. 이럴때 경험 깊은 개발자는 B 선택지를 선택하고 기술 부채로 쌓아 놓습니다. 물론 청산을 한다는 가정하에 쌓아 놓습니다. 결국 기술 부채는 쌓이게 되고 레거시가 됩니다. 이런 선택에는 그 당시의 이유가 존재 했습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;레거시를 존중하기&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 왜 레거시를 존중해야 한다는 것 일까요?&lt;br&gt;만약 레거시를 무시하고 무작정 개선을 시작한다고 합시다. 그 개발자는 레거시를 무시하기 때문에 과거 히스토리 조차 무시하고 무작정 리팩토링을 시작합니다. 시간이 흐르고 개발자는 왜 그런 선택지를 선택했는지 이해하게 되고 결국 시간을 낭비하고 리팩토링을 진행했던 코드를 롤백하게 됩니다. 이런 방식은 팀의 비용을 소모하고 팀원과의 불화를 만들수도 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;당장 저의 사례를 말씀드려 보겠습니다.&lt;br&gt;이전에 리팩토링을 진행하던 당시 React 프로젝트의 src 디렉토리 하위에 jQuery 라이브러리 파일이 그대로 존재했습니다. 또한 React 프로젝트에서 jQuery를 사용하는 것이 이해가 안됐습니다.&lt;br&gt;저는 해당 아이템을 필히 개선해야하는 ACTION 아이템이라고 생각하고 jQuery를 걷어내기 위해 몰두 했습니다. 하지만 결과론적으로 jQuery 자체를 걷어내는 것에는 실패 했습니다. 프로젝트에서 사용중인 sdk 코드에서 jQuery를 사용하고 있었고 SDK 자체가 jQuery와 강하게 결합하고 있었기 떄문에 대체할 수 없었기 때문입니다.&lt;br&gt;결국 jQuery 자체를 걷어내기 보다는 jQuery 라이브러리 소스를 걷어내는 것이 집중 했습니다. jQuery 파일의 가장 최소 압축 파일을 public 폴더에 넣어놓고 script로 해당 파일을 읽었습니다. 그리고 jQuery 소스를 참조할 수 있게 하기 위해 window 객체에 jQuery를 주입했습니다. window 객체는 전역 객체이기 때문에 전역 스코프가 오염될 수 있다는 점이 있었지만 그 당시 제가 할 수 있는 최선의 선택이였습니다. 만약 제가 레거시를 존중하고 코드를 살펴봤다면 jQuery 자체를 걷어내기 위해 쏟았던 시간을 절약할 수 있었을 겁니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 &lt;b&gt;레거시를 존중하는 행위는 그때 당시의 히스토리를 이해할 수 있는 좋은 방법이고, 결국엔 시간과 비용을 절약할 수 있는 방법&lt;/b&gt;입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;내가 작성하는 코드도 결국엔 레거시가 된다&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;내가 지금 최선의 코드, 최고의 코드라고 생각하고 작성하고 있는 코드도 결국엔 레거시가 됩니다.&lt;br&gt;추후 다른 개발자가 프로젝트 코드를 살펴봤을때 만약 팀에 남아있다면 히스토리를 직접적으로 알려줄 수 있습니다. 하지만 만약 내가 퇴사하게 된다면 레거시 코드와 그 개발자 사이에는 아무런 연결 선이 존재하지 않습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가급적 어떤 선택지를 선택한다면 히스토리를 남기는 것이 좋습니다. 작성한 코드는 되도록 분자 단위의 커밋으로 쪼개어 커밋 메세지를 잘 작성하거나, PR/MR을 올릴 경우 상세하게 어떤 작업을 했는지 기술하는 것이 좋습니다. Jira의 이슈나, Notion, Confluence, 팀의 드라이브 등 어떤 선택지가 될 수 있을것 같습니다. 나의 선택지, 미래의 레거시 코드와 그의 연결선을 남겨 놓는다면 인수인계 받은 개발자가 다시 한번 최선의 선택을 할 수 있지 않을까라는 생각입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또 코드를 작성할 때 나의 코드가 직접적으로 비즈니스 로직을 머리속에 그려줄 수 있다면 가장 좋을 것 같습니다. 아주 추상화 된 영역의 코드라면 주석을 통해 해당 코드의 역할을 설명해주는 것도 좋은 방법일 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 리팩토링에 대한 좋은 번역글을 살펴보고 후기 글을 남겼습니다. 그 글에서 지양해야 할 점으로 큰 단위의 리팩토링을 이야기 했는데, 지금 제가 큰 단위의 리팩토링을 진행하고 있네요. &lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/50&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;[소프트웨어] 리팩토링 글을 읽고&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;좋은 소프트웨어에는 항상 리팩토링이 따르고, 리팩토링을 진행하는 코드는 항상 그 당시의 최선의 선택이였습니다. 과거를 존중하고 미래로 나아가는 것은 좋은 자세인 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;온고지신 이라는 사자성어가 있습니다. 옛 것을 익히고 새 것을 안다는 것 입니다.&lt;br&gt;이번 포스팅에 아주 잘 어울리는 사자성어 인 것 같습니다.&lt;/p&gt;</description>
      <category>이것저것</category>
      <category>리팩토링</category>
      <category>소프트웨어</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/72</guid>
      <comments>https://kangs-develop.tistory.com/72#entry72comment</comments>
      <pubDate>Sat, 14 Dec 2024 14:26:50 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] this 바인딩</title>
      <link>https://kangs-develop.tistory.com/71</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모던 자바스크립트 Deep Dive 스터디 6회차&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript의 this바인딩은 다른 언어의 this 바인딩과 다르게 동작한다. 상황에 따라 다르게 동작하는 this 바인딩 이번 스터디에서 부셔본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;0 0 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;this&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;this는 객체 자기 자신 혹은 전역 객체를 가르킨다. 자바스크립트의 this 바인딩은 상황에 따라서 다르게 바인딩이 된다. &lt;b&gt;렉시컬 스코프는 코드의 평가 시점에 결정되는 반면에 this 바인딩은 함수의 호출 시점(런타임)에 결정이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;this 바인딩&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;바인딩은 식별자에 메모리 주소 값을 매핑 하는 것을 뜻한다. this 바인딩 역시 this 라는 식별자에 가르키는 객체의 주소를 매핑 하는 것 이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일반 함수에서의 this 바인딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;일반 함수에서 this는 globalThis이다. 즉 브라우저에서는 window, Node 환경에서는 global 객체를 가르킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.17.03.png&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qEg3U/btsLc4QIh4a/aWWeVe5WtcJMsr4Qo5khrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qEg3U/btsLc4QIh4a/aWWeVe5WtcJMsr4Qo5khrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qEg3U/btsLc4QIh4a/aWWeVe5WtcJMsr4Qo5khrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqEg3U%2FbtsLc4QIh4a%2FaWWeVe5WtcJMsr4Qo5khrK%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;854&quot; height=&quot;1058&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.17.03.png&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;foo 함수를 함수 선언문으로 선언했고, foo 함수를 실행한다. foo 함수에서 this를 호출하니 global 객체가 호출된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;콜백 함수의 함수 선언문&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;콜백 함수를 일반 함수로 호출한다면 this 바인딩은 global 객체가 된다. 아래 코드에서 foo 함수는 콜백 함수를 받아서 실행한다. 첫 번째 호출에서의 콜백 함수는 함수 선언문으로 작성된 콜백 함수고, 두 번째 호출의 콜백 함수는 화살표 함수로 작성된 콜백 함수이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;const foo = (callback) =&amp;gt; {
  callback();
};

foo(function () {
  console.log(this);
});

foo(() =&amp;gt; {
  console.log(this);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;6행에서의 this는 전역 객체인 global 객체를, 10행에서는 this는 상위 스코프인 foo를 가리킨다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;419&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.28.14.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4FOZH/btsLcrMv039/GtiV4cSgJEql1Tx7vSU1nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4FOZH/btsLcrMv039/GtiV4cSgJEql1Tx7vSU1nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4FOZH/btsLcrMv039/GtiV4cSgJEql1Tx7vSU1nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4FOZH%2FbtsLcrMv039%2FGtiV4cSgJEql1Tx7vSU1nK%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;838&quot; height=&quot;598&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.28.14.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 함수라도 일반 함수 선언문으로 this를 호출하면 globalThis가 바인딩 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;메서드 호출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;객체의 프로퍼티에 함수가 들어가는 경우 메서드라고 이야기한다. 객체의 메서드의 this 바인딩은 함수 선언문으로 작성 됐는가, 화살표 함수로 작성이 됐는가에 따라서 다르게 동작한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;this.value = &quot;global&quot;;

function foo() {
  console.log(this);
}

const boo = () =&amp;gt; {
  console.log(this);
};

const obj = {
  value: &quot;obj&quot;,
  foo,
  boo,
};

foo();
boo();
obj.foo();
obj.boo();&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;prolog&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;gangdonghui@gangdonghuiui-MacBookPro py % node test2.js
&amp;lt;ref *1&amp;gt; Object [global] {
  global: [Circular *1],
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  structuredClone: [Getter/Setter],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [AsyncFunction: fetch]
}
{ value: 'global' }
{ value: 'obj', foo: [Function: foo], boo: [Function: boo] }
{ value: 'global' }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;17행은 함수 선언문으로 작성한 foo 함수를 호출한다. 일반 함수인 foo 함수의 this는 전역 객체로 바인딩이 됐고 전역 객체가 출력된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;18행은 화살표 함수로 작성한 boo 함수를 호출한다. 화살표 함수의 this 바인딩은 상위 스코프를 가리키고 boo 함수의 상위 스코프는 전역 객체이다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;같은 전역 객체이지만 다른 모습으로 출력 되는 것은 Node.js의 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;console.log()&lt;/span&gt;가 &lt;b&gt;전역 객체를 다르게 직렬화 &lt;/b&gt;하기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;19행은 일반 함수 선언문으로 작성된 foo 메서드를 호출한다. foo 메서드는 this를 출력하고 foo 메서드의 this 바인딩은 객체 자신인 obj를 가리킨다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;20행의 boo 메서드는 화살표 함수로 작성된 obj 객체의 메서드이다. 화살표 함수에서의 this 바인딩은 상위 스코프를 가리킨다. obj 객체의 상위 스코프는 전역이기 때문에 boo 메서드에서 출력하는 this는 globalThis가 출력된다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;302&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.43.43.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqKDV4/btsLdhWzzLu/YClOrQLAKew7JWzkKx6TbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqKDV4/btsLdhWzzLu/YClOrQLAKew7JWzkKx6TbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqKDV4/btsLdhWzzLu/YClOrQLAKew7JWzkKx6TbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqKDV4%2FbtsLdhWzzLu%2FYClOrQLAKew7JWzkKx6TbK%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;604&quot; height=&quot;640&quot; data-filename=&quot;스크린샷 2024-12-10 오후 1.43.43.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;메모리의 구조를 살펴보면 다음과 같다. foo, boo 메서드는 obj 객체에 소유된 것이 아닌 각 프로퍼티가 메모리에 선언된 함수 객체의 주소를 가리키고 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;생성자 함수의 this&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;생성자 함수는 객체를 생성할 때 호출되는 특수한 함수이다. 생성자 함수의 this는 생성될 객체를 바인딩 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this);
}

const person = new Person(&quot;Lee&quot;, 20);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;Person 함수는 생성자 함수이다. 7행에서 new 연산자를 사용해 새로운 객체를 생성하고, Person이라는 생성자 함수를 호출한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;생성자 함수에서의 this는 생성할 객체 자신을 가리킨다고 했다. 그렇기 때문에 생성자 함수에서의 this는 person이 호출된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;call, bind, apply&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;call, bind, apply 메서드는 함수의 프로토타입 메서드이다. 해당 메서드들을 활용하면 this에 명시적으로 바인딩을 할 수 있다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 메서드를 통해 arguments를 전달할 수 있습니다. 이번 회차는 this 바인딩만을 살펴봅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;const argsThis = { value: &quot;args this&quot; };

function foo() {
  console.log(this);
}

foo();
foo.apply(argsThis);
foo.call(argsThis);
foo.bind(argsThis)();&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;prolog&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;gangdonghui@gangdonghuiui-MacBookPro py % node test2.js
&amp;lt;ref *1&amp;gt; Object [global] {
  global: [Circular *1],
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  structuredClone: [Getter/Setter],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [AsyncFunction: fetch]
}
{ value: 'args this' }
{ value: 'args this' }
{ value: 'args this' }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;foo 함수를 호출할 경우 global이 출력된다. apply, call, bind를 통해 명시적으로 this로 바인딩할 객체를 전달해 해당 객체가 this로 출력되는 것을 살펴볼 수 있다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>This</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/71</guid>
      <comments>https://kangs-develop.tistory.com/71#entry71comment</comments>
      <pubDate>Tue, 10 Dec 2024 14:15:32 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] eval 메서드에 대하여</title>
      <link>https://kangs-develop.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 제공하고 있는 함수이지만 사용해서는 안되는 함수가 있습니다. 바로 eval 함수입니다.&lt;br&gt;이번 포스팅에서는 왜 eval을 사용하면 안된다고 하는지 살펴보도록 하겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;eval&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;eval 함수는 내장 전역 함수입니다. 즉 사용자가 정의한 함수가 아닌 자바스크립트 언어에서 제공하고 있는 함수입니다.&amp;nbsp;&lt;br&gt;eval 함수는 문자열로 된 코드를 인자로 받습니다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;인자로 전달된 자바스크립트 코드가 식이면 런타임에 실행해 값을 생성하고, 문이라면 런타임에 코드를 실행&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;eval(&quot;1 + 2&quot;); // 3

const value = eval(&quot;1 + 3&quot;); // value에 4 할당

eval(&quot;var x = 1 * 6&quot;);

console.log(x);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;간단한 코드를 살펴봅시다. 코드에서 value에 값을 할당하고 있습니다. eval 함수에 &quot;1+3&quot; 이라는 문자열을 전달하고, 런타임에 해당 코드를 실행해 value에 값을 할당합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;코드의 평가 시점에 x 라는 변수는 존재하지 않습니다. 따라서 x를 참조하려고 할 때 ReferenceError가 발생해야 할 것 같지만 eval 메서드를 통해 전달한 &quot;var x = 1 * 6&quot;이라는 문이 런타임에 실행이 됩니다. 따라서 x의 값은 6이라는 값이 출력이 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;eval 메서드를 통해 전달받은 코드는 스코프를 수정합니다. 다음의 예제 코드를 살펴봅시다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;var x = &quot;Lee&quot;;

function foo() {
&amp;nbsp;&amp;nbsp;eval(&quot;var x = 3&quot;);

&amp;nbsp;&amp;nbsp;x += &quot;Mong&quot;;
&amp;nbsp;&amp;nbsp;console.log(x);
}

foo();&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;foo 함수는 전역 변수인 x 변수에 문자열을 추가로 입력하고 싶어합니다. 출력 결과로 &quot;LeeMong&quot; 을 기대하고 있습니다.&lt;br&gt;하지만 eval로 전달한 코드로 런타임에 x 식별자가 선언이 되고 값이 할당됩니다. 따라서 출력 결과 값은 기대하지 못한 &quot;3Mong&quot; 이라는 값이 출력이 됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTM02V/btsK2dtWwde/2zhm9uwsQH2dswvdo2OEA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTM02V/btsK2dtWwde/2zhm9uwsQH2dswvdo2OEA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTM02V/btsK2dtWwde/2zhm9uwsQH2dswvdo2OEA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTM02V%2FbtsK2dtWwde%2F2zhm9uwsQH2dswvdo2OEA0%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;940&quot; height=&quot;566&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;코드의 평가 시점에 생성된 스코프 체인에는 x라는 식별자가 전역 스코프에만 존재 했습니다. 하지만 코드의 실행 시점에 eval 메서드를 통해 전달 받은 코드가 실행이 됐고, foo 함수 스코프에 x라는 식별자가 스코프에 등록이 됩니다. 위와 같이 eval 메서드를 통해 전달한 코드는 스코프를 수정합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;strict mode&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;strict mode에서는 eval을 통해 실행된 코드는 별도의 eval 스코프를 갖고 있습니다. eval을 통해 전달된 코드가 실행이 되면서 eval 실행 컨텍스트를 생성하기 때문입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위 예제 코드의 foo 함수에 strict mode를 적용합니다. 그 후 코드를 실행하면 기대한 출력 값인 &quot;LeeMong&quot;이 출력되는 것을 확인할 수 있습니다. 실행 컨텍스트 관점에서 생각해본다면 foo 함수 실행 컨텍스트에 x라는 식별자가 등록이 되는 것이 아닌, eval 실행 컨텍스트에 x라는 값이 바인딩 됐기 때문입니다. 따라서 15행에서 x는 상위 스코프인 전역 스코프에서 x 식별자를 검색하고 기대한 출력 값을 확인할 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uIXC9/btsK15pqGqd/gOVOFnBjdToxvFKzMUTDsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uIXC9/btsK15pqGqd/gOVOFnBjdToxvFKzMUTDsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uIXC9/btsK15pqGqd/gOVOFnBjdToxvFKzMUTDsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuIXC9%2FbtsK15pqGqd%2FgOVOFnBjdToxvFKzMUTDsk%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;814&quot; height=&quot;574&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 웹 애플리케이션을 만들려고 자바스크립트를 사용하고 있습니다. 만약 사용자에게 입력 받은 값을 eval 메서드를 활용해 처리한다면 어떤 일이 발생할까요?&lt;br&gt;&amp;nbsp;&lt;br&gt;아래의 예시 코드를 통해서 살펴봅시다!&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;html&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;head&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;/head&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;body&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;input id=&quot;input&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button id=&quot;submit&quot;&amp;gt;submit&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p id=&quot;result&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;/body&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;script&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var inputValue = &quot;&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var resultValue = &quot;&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const input = document.getElementById(&quot;input&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const submitButton = document.getElementById(&quot;submit&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const result = document.getElementById(&quot;result&quot;);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input.addEventListener(&quot;change&quot;, (event) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;inputValue = eval(event.currentTarget.value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resultValue = inputValue * 5;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;submitButton.addEventListener(&quot;click&quot;, (event) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result.innerText = resultValue;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;우리는 간단하게 사용자에게 값을 입력받아서 5를 곱한 값을 화면에 보여주고 싶습니다.&lt;br&gt;이런 코드를 작성할 일은 없겠지만 eval 메서드의 위험성을 모른채 eval 함수를 통해 값을 inputValue에 넣어주고 있다고 가정합시다!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YdDMB/btsK3ins7J9/Ohz9UIivPzSR6S2QcsMZz1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YdDMB/btsK3ins7J9/Ohz9UIivPzSR6S2QcsMZz1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YdDMB/btsK3ins7J9/Ohz9UIivPzSR6S2QcsMZz1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/YdDMB/btsK3ins7J9/Ohz9UIivPzSR6S2QcsMZz1/img.gif&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;1318&quot; height=&quot;1114&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;숫자를 입력하면 자연스럽게 우리가 의도한 값이 표현됩니다. 하지만 갑자기 나쁜 마음을 먹은 사용자가 이 기능을 망치고 싶은 생각이 들었습니다! 개발자 도구를 통해 소스코드를 살펴보니 input의 값을 eval 메서드로 처리하고 있다는 것을 확인합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;924&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc4dw0/btsK3PLWuGn/LWQxCT00b6hWcsnZy8FNhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc4dw0/btsK3PLWuGn/LWQxCT00b6hWcsnZy8FNhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc4dw0/btsK3PLWuGn/LWQxCT00b6hWcsnZy8FNhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc4dw0%2FbtsK3PLWuGn%2FLWQxCT00b6hWcsnZy8FNhK%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;1426&quot; height=&quot;924&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사용자는 &quot;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;var inputValue = &quot;not a number!! haha&quot;; submitButton.innerText = &quot;haha&quot;;&lt;/b&gt;&lt;/span&gt;&quot; 라는 코드를 input에 입력을 합니다. 우리의 애플리케이션은 기존의 기능을 잃고 submit 버튼의 이름도 바뀌게 됩니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fq8JD/btsK2mj1dfz/EqrzsuQmINOJoL9sOQVBT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fq8JD/btsK2mj1dfz/EqrzsuQmINOJoL9sOQVBT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fq8JD/btsK2mj1dfz/EqrzsuQmINOJoL9sOQVBT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFq8JD%2FbtsK2mj1dfz%2FEqrzsuQmINOJoL9sOQVBT1%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;430&quot; height=&quot;166&quot; data-origin-width=&quot;430&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 가벼운 예시를 들었습니다. 하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;b&gt;만약 우리의 웹 애플리케이션이 토큰 관리에 신경을 쓰지 않아 토큰이 탈취당한 상태라면 서버에 탈취한 토큰으로 악의적인 요청을 할 수 있습니다&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;. 이는 정말 치명적인 웹 취약점 중에 하나입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위의 가벼운 케이스로도 eval 메서드의 위험도를 느끼실 수 있을거라고 생각합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2023년도 Deview에 참가했을때 모 기업의 엔지니어 분의 프론트엔드 세션을 들었는데, eval 메서드를 런타임에 애플리케이션을 주입하는 용도로 사용하고 있다는 컨퍼런스 내용을 들었습니다. 위험성이 있는 메서드 이지만 특정 기능을 구현하기 위해 해당 메서드를 사용할수도 있겠구나 싶었지만, 한편으로는 아리송한 생각도 들었습니다. 아키텍처 상 위험도가 없을수도 있겠지만, 절대 100%를 가정하면 안된다고 생각했기 때문입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;정말 어쩔수 없이 eval 메서드를 사용해야만 한다면 반드시 해당 메서드가 왜 위험한지 알고 사용하셨으면 좋겠습니다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>eval</category>
      <category>JavaScript</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/70</guid>
      <comments>https://kangs-develop.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 1 Dec 2024 16:29:23 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] var 그리고 let, const</title>
      <link>https://kangs-develop.tistory.com/69</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 5회차&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/blockquote&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 변수를 선언하는데 사용하는 키워드는 var let const 가 있다.&lt;br /&gt;var 키워드는 코드의 평가 단계에서 식별자의 선언과 초기화(undefined)가 동시에 발생하며 메모리에서 어떤 일들이 발생하는지 2회차 스터디에서 자세하게 살펴봤다. 이번 스터디에서는 let, const 키워드에 대해서 살펴본다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/m/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[JavaScript] 변수와 데이터 타입&lt;/a&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;let 키워드는 재할당이 가능하고 선언과 초기화를 분리할 수 있다, const 키워드는 재할당이 불가능하며 선언과 초기화가 동시에 이뤄져야 한다,&lt;/i&gt;&lt;/span&gt; 이런 정보는 모두가 쉽게 알고 있는 정보이다. 이런 정보 외에 조금은 특별하다고 생각한 정보를 정리해보겠다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;let&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;let 키워드는 블록 레벨 스코프를 가진다. 블록 레벨 스코프란 코드의 블록을 모두 스코프로 평가한다는 것 이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;let a = 1;

{
  console.log(a); //ReferenceError
  let a = 2;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 코드는 책의 예제와 동일하다. let 키워드는 코드의 블록 &lt;b&gt;{} &lt;/b&gt;을 모두 스코프로 인정하기 때문에 4행에서 참조 에러가 발생한다. 즉 4행의 a는 스코프에 등록된 5행의 a를 참조 하려는 것 이기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;여기서 한가지 더 살펴볼 수 있는 사실은 let 키워드 역시 호이스팅이 된다는 것이다. let 키워드는 코드의 평가 단계에서 선언과 초기화가 다른 시점에 진행이 된다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;var 키워드는 코드의 평가 단계에서 식별자의 선언과 암묵적인 초기화가 병행된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;전역 스코프를 생성한 후 지역 스코프가 생성이 된다. 이때 5행의 a 변수는 선언이 되어 지역 스코프에 등록이 되고 (정확히는 실행 컨텍스트의 렉시컬 환경에 바인딩이 된다.) a 변수는 렉시컬 환경 레코드의 선언적 환경 레코드에 등록이 되기 때문에 코드를 만나서 초기화가 되기 전 까지 참조를 할 수 없게 된다. 이를 &lt;b&gt;일시적 사각 지대&lt;/b&gt;(Temporary Dead Zone) 이라고 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;코드의 평가 단계에서 let 키워드의 변수는 선언이 된다.&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;코드의 평가 단계에서 식별자의 선언이 발생하기 때문에 식별자가 스코프에 관리가 된다. 하지만 TDZ에 의해 실제 선언문을 만나기 이전의 구역에서는 참조할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;식별자의 초기화 단계는 실제로 해당 선언문을 만난 시점에 발생한다. 초기화 단계에서 undefined로 초기화가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;const&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;const 키워드 역시 let 키워드 처럼 TDZ를 가진다. 코드의 평가 단계에서 변수의 선언이 발생하기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Q. cosnt 키워드로 선언한 변수는 상수이다?&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;const 키워드로 선언한 변수는 상수일까? 결론부터 말하자면 &lt;b&gt;원시 값이 할당된 const 키워드의 식별자는 상수처럼 동작하는 &amp;ldquo;값(리터럴)&amp;rdquo; 일 뿐&lt;/b&gt;, 상수는 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자바스크립트에서 원시 값은 불변성(Immutable)을 가진다고 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/m/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;[JavaScript] 원시 값과 객체 리터럴&lt;/b&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;즉 값을 변경한다는 것은 식별자가 참조하고 있는 메모리 주소가 가르키는 값을 변경하는 것이 아닌, 메모리에 새로운 공간을 할당하고 식별자가 해당 주소값을 바라본다는 것 이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;&lt;i&gt;불변한다는 것을 메모리의 관점에서 생각해야 한다. 그렇지 않으면 코드상에서는 값이 변하는데 왜 불변한다는 것일까 라는 의문점이 생기게 된다.&lt;/i&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;const 키워드는 선언과 할당을 동시에 진행해야하며 값을 재할당 할 수 없다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 말들을 종합해보면, const 키워드를 사용해 값을 선언하면 값을 재할당 할 수 없고, 변하지 않는 특성을 가진 자바스크립트의 원시 값을 리터럴로 사용한다면 값을 상수로 사용할 수 있다는 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;자바스크립트에는 자바의 final과 같은 키워드가 존재하지 않는다. 하여 &lt;b&gt;const 와 자바스크립트의 원시 값의 특성을 함께 이용해 상수처럼&lt;/b&gt; 사용할 수 있는 것이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;변경 가능한 값, 객체&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트의 객체는 변경이 가능하다. 메모리에 선언된 객체 내부의 프로퍼티를 변경할 수 있다는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; letter-spacing: 0px;&quot;&gt;const 키워드를 사용하면 값을 재할당 할 수 없다고 했다. &lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: justify;&quot;&gt;그럼 const 키워드로 변수를 선언하면 프로퍼티를 변경할 수 있을까? &lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;span style=&quot;text-align: justify;&quot;&gt;객체의 프로퍼티를 변경하는 것은 가능하다.&lt;/span&gt; &lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;const obj = {
  a: 1
};

obj.a = 2;

console.log(obj.a); // 2&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;원시 값은 변경이 불가능하고 const 키워드로 선언 시 재할당이 불가능 하기에 값을 변경할 수 없지만, 객체는 변경이 가능한 값이며, 프로퍼티를 변경하는 것은 변수에 값을 재할당을 하는 것이 아니기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;객체를 활용한 네임스페이스&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;const 키워드를 사용하면 값을 상수처럼 활용할 수 있다고 했다. 그럼 같은 분류의 값들을 상수로 활용하고 싶다면 어떻게 해야할까?&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;TypeScript를 활용하고 있다면 as const 키워드를 활용해 객체를 변경 불가능 하게 만들 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const OBJECT_MUTABLE = {
  A: &quot;A&quot;,
  B: &quot;B&quot;,
};
OBJECT_MUTABLE.A = &quot;B&quot;;

const OBJECT_AS_CONST = {
  A: &quot;A&quot;,
  B: &quot;B&quot;,
} as const;
OBJECT_AS_CONST.A = &quot;B&quot;; // Compile Error&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;OBJECT_MUTABLE&lt;/b&gt; 객체는 일반 객체이며 &lt;b&gt;OBJECT_AS_CONST&lt;/b&gt; 객체는 타입스크립트의 as const 을 활용해 const assertion(상수 단언)을 할 수 있다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;as const를 통해 상수 단언된 객체는 readonly 객체로서 변경이 불가능&lt;/i&gt;&lt;/span&gt;하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제로 위 코드의 11행 에서는 컴파일 에러가 발생하는데, 에러를 확인해보면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzKFv/btsKZOlTiih/tubo4DW7TuRX0m8KCiG3Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzKFv/btsKZOlTiih/tubo4DW7TuRX0m8KCiG3Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzKFv/btsKZOlTiih/tubo4DW7TuRX0m8KCiG3Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzKFv%2FbtsKZOlTiih%2Ftubo4DW7TuRX0m8KCiG3Fk%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;1056&quot; height=&quot;442&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;as const를 활용해 같은 분류의 값들을 객체화 시키고 코드의 흐름 상에서 변경 불가능하게 프로그래밍 할 수 있다. 다만 주의해야 할 점은 TypeScript는 JavaScript의 슈퍼셋 언어이고 자바스크립트로 컴파일이 되기 때문에 컴파일 된 언어는 결국 객체 프로퍼티의 변경이 가능하다는 것이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;런타임 시점에 다이나믹하게 값이 변경이 될 수 있음으로 객체가 변하지 않도록 항상 사이드 이펙트에 주의&lt;/b&gt;하며 프로그래밍 하자.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;var 키워드가 전역에 선언될 경우&lt;/h2&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;blockquote&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;해당 예제는 브라우저에서 동작합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;var 키워드가 전역에 선언될 경우 암묵적으로 전역 객체인 window 객체의 프로퍼티가 된다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  var a = &quot;global a&quot;;
  b = &quot;global b&quot;;

  console.log(window.a, window.b);
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot;&gt;
&lt;div data-width-type=&quot;pixel&quot; data-width=&quot;481&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;44&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x5hM4/btsKXJfEAj7/cBmd5kNQgwKbny0lvCAO01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x5hM4/btsKXJfEAj7/cBmd5kNQgwKbny0lvCAO01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x5hM4/btsKXJfEAj7/cBmd5kNQgwKbny0lvCAO01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx5hM4%2FbtsKXJfEAj7%2FcBmd5kNQgwKbny0lvCAO01%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;962&quot; height=&quot;44&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;44&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;let 키워드로 선언된 변수는 전역 객체의 프로퍼티가 아니다. let 키워드로 선언된 변수는 선언적 환경 레코드에 등록되기 때문이다. var 키워드를 사용해 전역 변수를 선언한다면 window 객체의 프로퍼티가 된다는 것에 주의하도록 하자.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>변수</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/69</guid>
      <comments>https://kangs-develop.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 27 Nov 2024 18:31:41 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 스코프</title>
      <link>https://kangs-develop.tistory.com/68</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 5회차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;스코프&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프란 식별자가 유효한 범위다. 모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;자바스크립트에서는 var, let, const 어떤 키워드를 사용하느냐에 따라 스코프도 다르게 결정된다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;var a = 'a';

function foo(b) {
  var a = &quot;new a&quot;;
  var c = &quot;c&quot;;
  console.log(a, b, c); // new a, b
}

console.log(a, b, c) // Reference Error (b, c)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 예제 코드를 살펴보면 a 식별자는 전역 스코프를 가지며 b와 c 식별자는 함수 스코프를 가진다. 따라서 8행에서 b와 c를 참조 하려면 참조 에러가 발생한다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;blockquote data-panel-content=&quot;true&quot; data-ke-style=&quot;style3&quot;&gt;자바스크립트 엔진은 스코프를 통해 어떤 식별자를 참조해야 할 지 결정한다. 따라서 스코프란 &lt;b&gt;자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙&lt;/b&gt;이라고 할 수 있다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 코드의 문맥을 살펴보자. a 변수는 전역 스코프에서 선언된 식별자이며 foo 함수 내부 변수인 b, a, c는 함수 스코프에서 선언된 식별자이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;전역 스코프의 a변수와 함수 스코프의 a변수는 식별자가 같지만 스코프가 다르다. 그렇기 때문에 6행과 9행에서 모두 a라는 이름의 변수를 참조 했지만 다른 값이 출력되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEZwm6/btsKUfkIKDg/r6K5voxcFUwQLSwfigjHYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEZwm6/btsKUfkIKDg/r6K5voxcFUwQLSwfigjHYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEZwm6/btsKUfkIKDg/r6K5voxcFUwQLSwfigjHYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEZwm6%2FbtsKUfkIKDg%2Fr6K5voxcFUwQLSwfigjHYk%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;1222&quot; height=&quot;660&quot; data-origin-width=&quot;1222&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전역 스코프, 지역 스코프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프는 전역 스코프와 지역 스코프가 존재한다. 지역 스코프는 보통 함수 몸체에 해당하며 전역 스코프는 지역 스코프가 아닌 모든 스코프이다. 전역 스코프에서 선언한 변수는 지역 스코프 어디서든 참조할 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;스코프는 실행 컨텍스트에 바인딩 된다. 실행 컨텍스트에는 자신의 스코프와 상위 레벨 스코프에 대한 정보를 담고 있다. 그래서 실행 컨텍스트가 상위 스코프를 기억하며 상위 스코프의 식별자를 참조할 수 있는 것이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스코프 체인&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;var a = &quot;a&quot;;

function foo() {
  var b = &quot;b&quot;;
  console.log(a, b); // a, b
  
  function boo() {
    var b = &quot;new b&quot;;
    var c = &quot;c&quot;;
    console.log(a, b); // a, new b
    
    function poo() {
      var b = &quot;new new b&quot;;
      var c = &quot;new c&quot;;
      var d = &quot;d&quot;;
      console.log(a, b, c, d) // a, new new b, new c, d
    }
    
    console.log(d); // Reference Error
  }
  
  console.log(a, b); // // a, b
}

console.log(b); // Reference Error

foo();&lt;/code&gt;&lt;/pre&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;1942&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQAkyk/btsKT9dzJxM/023nKknDa1kRpMkN9GwBhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQAkyk/btsKT9dzJxM/023nKknDa1kRpMkN9GwBhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQAkyk/btsKT9dzJxM/023nKknDa1kRpMkN9GwBhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQAkyk%2FbtsKT9dzJxM%2F023nKknDa1kRpMkN9GwBhK%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;1942&quot; height=&quot;496&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 예문은 전역 스코프에 foo 함수가 선언되고, foo 함수 내부에 boo 함수가, boo 함수 내부에 poo 함수가 선언되며 스코프가 계층적 구조로 연결되며 스코프 체인을 이루고 있다.&lt;br /&gt;19행은 boo 스코프를 가진다. 참조하는 d 변수는 boo 스코프에 존재하는 식별자이고 스코프를 벗어난 식별자이기 때문에 참조할 수 없고 참조 에러가 발생한다. &lt;b&gt;정확히는 자바스크립트 엔진이 스코프 내에서 d 식별자를 찾을 수 없기 때문에 에러가 발생하는 것이다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;자바스크립트 엔진은 변수를 참조할 때 &lt;b&gt;스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 상위 스코프 방향으로 이동하며 (스코프를 거슬러 올라가며) 선언된 변수를 검색&lt;/b&gt;한다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;함수 선언문으로 작성된 함수는 코드 평가 단계에서 메모리에 함수 객체가 생성이 되며 식별자가 바인딩 된다. 함수 객체도 식별자에 할당되기 때문에 함수도 스코프를 갖는다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;렉시컬 스코프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렉시컬 스코프는 정적 스코프로도 불리며 코드의 평가 시점에 함수가 어디서 작성 됐는지에 따라서 스코프를 결정한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;var v = &quot;global&quot;;

function foo() {
  var v = &quot;foo local&quot;;
  boo();
}

function boo() {
  console.log(v);
}

foo();
boo();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 코드의 출력 결과를 예상해보자. 자바스크립트는 렉시컬 스코프(정적 스코프)를 가지며, 코드의 평가 시점(함수의 정의가 평가되는 시점)에 상위 스코프를 결정한다고 했다. (코드의 평가 시점에 함수 객체를 메모리에 올려놓는다.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;스코프의 관점에서 살펴보자. 코드의 평가 단계에서 전역 스코프가 생성이되며 v, foo, boo 식별자(변수와 함수)가 평가되며 전역 스코프에 생성이 된다. 이때 foo, boo함수의 상위 스코프는 전역 스코프이다.&lt;br /&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;1034&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhcDw5/btsKVLCA2ZT/Nl4N9DjlYPwutBwG6tKSxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhcDw5/btsKVLCA2ZT/Nl4N9DjlYPwutBwG6tKSxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhcDw5/btsKVLCA2ZT/Nl4N9DjlYPwutBwG6tKSxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhcDw5%2FbtsKVLCA2ZT%2FNl4N9DjlYPwutBwG6tKSxk%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;1034&quot; height=&quot;454&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;13행에서 foo함수를 호출하고 내부에서 boo함수를 호출한다. boo함수를 호출했고 v 식별자를 참조하려고 하는데 지역 스코프에는 존재하지 않아 상위 스코프인 전역 스코프에서 검색을 통해 v 식별자를 참조하게 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;14행에서 boo함수를 호출할 경우 역시 상위 스코프인 전역 스코프에서 v 식별자를 참조한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;함수가 어디에서 호출이 됐는지가 아닌 &lt;b&gt;어디서 정의 됐는지에 따라서 스코프가 결정되기 때문에&lt;/b&gt; 모두 &amp;ldquo;global&amp;rdquo; 이라는 값을 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XXYsc/btsKUHue39Q/v4Tlvqwkc6aAklJh8AvJ70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XXYsc/btsKUHue39Q/v4Tlvqwkc6aAklJh8AvJ70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XXYsc/btsKUHue39Q/v4Tlvqwkc6aAklJh8AvJ70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXXYsc%2FbtsKUHue39Q%2Fv4Tlvqwkc6aAklJh8AvJ70%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;832&quot; height=&quot;752&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;렉시컬 스코프란 조금 어려운 개념이다. 하지만 자바스크립트는 코드의 평가와 실행 단계가 나눠져있고, 평가 단계에 스코프가 결정된다 라는 것을 생각하면 이해에 조금은 도움이 될 것이다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;렉시컬이라는 것은 어휘라는 것에 집중해보자. 앞으로 자바스크립트를 공부하면 많이 나오는 단어이기 때문에 꼭 이해하고 넘어가야 한다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;함수 스코프와 함수의 실행&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;스코프는 코드의 평가 단계에서 결정된다. 코드의 평가 단계에서 함수 객체가 메모리에 할당이 된다. 그럼 함수 내부에 있는 지역 스코프는 언제 생성이 될까?&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드 실행 단계에서 함수를 호출하면 함수 내부의 코드 평가가 시작된다. 이 과정에서 코드 평가 단계에서 발생하는 일들이 발생한다. 식별자의 선언문을 실행해 식별자를 메모리에 할당하고 undefined 값을 할당한다. 함수 내부에 선언된 함수 선언문은 함수 객체가 메모리에 올라가고 식별자가 바인딩 된다. 이 과정에서 함수 스코프가 결정된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수 스코프는 함수가 종료되는 시점에 소멸된다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;foo();

function foo() {
  var a = 1;
  return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드의 평가 단계에서 &lt;b&gt;전역 실행 컨텍스트가 생성&lt;/b&gt;되고 &lt;b&gt;foo 함수 객체가 생성&lt;/b&gt;되어 &lt;b&gt;렉시컬 환경에 바인딩 되고 실행 컨텍스트는 전역 스코프&lt;/b&gt;를 갖는다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드의 실행 단계에서 &lt;b&gt;foo 함수가 실행되고 함수 코드 평가&lt;/b&gt;가 시작된다. &lt;b&gt;함수 실행 컨텍스트가 생성&lt;/b&gt;되고 &lt;b&gt;a 변수가 렉시컬 환경에 바인딩&lt;/b&gt;되며 &lt;b&gt;실행 컨텍스트는 foo 지역 스코프&lt;/b&gt;를 갖는다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;함수가 종료&lt;/b&gt; 되며 &lt;b&gt;함수 실행 컨텍스트가 소멸&lt;/b&gt;된다. &lt;b&gt;실행 컨텍스트가 소멸되며 a 변수 또한 메모리에서 내려오게 된다&lt;/b&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;1704&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z1U5X/btsKUA9CKUl/moLFh6ilD35NqQkSnKrOuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z1U5X/btsKUA9CKUl/moLFh6ilD35NqQkSnKrOuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z1U5X/btsKUA9CKUl/moLFh6ilD35NqQkSnKrOuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ1U5X%2FbtsKUA9CKUl%2FmoLFh6ilD35NqQkSnKrOuk%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;1704&quot; height=&quot;516&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-pm-slice=&quot;2 2 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;다만 a 변수가 메모리에서 해제되는 시점은 알수가 없다. 이는 자바스크립트의 GC가 수행하는 일이기 때문이다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;만약 foo 함수가 종료가 되어도 함수 스코프에 등록된 식별자를 다른 스코프에서 참조하고 있다면 해당 실행 컨텍스트는 소멸되지 않는다. 이는 클로저라는 개념이다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전역 스코프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 스코프는 자바스크립트 엔진이 코드를 평가하는 시점에 생성이 된다. 전역 스코프는 함수 스코프와 다르게 생명주기의 끝이 없다. 다시 말해서 브라우저가 닫히기 전까지 스코프가 소멸되지 않는다는 것 이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 스코프를 사용할 때 주의할 몇 가지 점들이 있다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;암묵적 결합&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 변수는 어느 스코프에서든 참조할 수 있고 값을 변경할 수 있다. 이 말은 즉 어디서 사이드 이펙트가 발생할지 예측할 수 없다는 점이다. 또한 특정 스코프에서 전역 변수를 참조할 경우 해당 변수가 어느 스코프에서 참조가 됐는지 예측하기 어렵다. 그렇기 때문에 가급적 전역 변수는 변하지 않는 값으로 사용해야 한다. &lt;b&gt;가급적 전역 변수는 &lt;/b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;const&lt;/span&gt;&lt;b&gt; 키워드를 사용해 재할당, 재선언을 할 수 없게 만들며 역할이 명확한 값 만을 사용해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;메모리 누수&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역변수의 생명주기는 프로그램이 종료할 때 소멸된다. 그렇기 때문에 많은 전역 변수를 사용하거나 아주 커다란 객체를 전역 변수로 사용한다면 메모리 누수가 발생할 수 있다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스코프의 끝&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 스코프는 가장 상위 계층에 위치한 스코프이다. 그렇기 때문에 스코프 체인에서 식별자를 검색할 때 가장 끝까지 검색해야 하는 점이 존재한다. 이 말은 검색 속도가 느리다는 점이다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;네임스페이스 오염&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;전역 변수는 가장 상위 스코프에 위치하기 때문에, 특정 스코프에서 var 키워드를 활용해 해당 식별자를 사용할 경우나 변수에 재할당을 한다면 예상치 못한 사이드 이펙트가 발생할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>scope</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/68</guid>
      <comments>https://kangs-develop.tistory.com/68#entry68comment</comments>
      <pubDate>Sun, 24 Nov 2024 14:58:59 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] Spread Operator(전개 연산자)로 객체를 복사한다면?</title>
      <link>https://kangs-develop.tistory.com/67</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Spread Operator는 ES6에 나온 연산자 입니다. 객체에 있는 프로퍼티나 배열 내 값을 전개하는데 사용합니다. 스프레드&amp;nbsp;연산자를&amp;nbsp;사용하면&amp;nbsp;객체나&amp;nbsp;배열을&amp;nbsp;복사할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;이때&amp;nbsp;발생하는&amp;nbsp;복사는&amp;nbsp;깊은&amp;nbsp;복사,&amp;nbsp;얕은&amp;nbsp;복사&amp;nbsp;중&amp;nbsp;어떤&amp;nbsp;복사일까요?&lt;br&gt;가볍게&amp;nbsp;생각하자면&amp;nbsp;얕은&amp;nbsp;복사는&amp;nbsp;단순히&amp;nbsp;메모리의&amp;nbsp;참조&amp;nbsp;주소&amp;nbsp;값을&amp;nbsp;전달하는&amp;nbsp;것이고,&amp;nbsp;깊은&amp;nbsp;복사는&amp;nbsp;메모리에&amp;nbsp;같은&amp;nbsp;모양의&amp;nbsp;새로운&amp;nbsp;객체를&amp;nbsp;생성하는&amp;nbsp;것을&amp;nbsp;의미합니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;결론부터 말하자면 스프레드 연산자를 통해 객체, 배열을 복사하는 행위는 얕은 복사입니다. &lt;br&gt;1-depth의 프로퍼티 까지는 깊은 복사로 복사가 됩니다. 하지만 2-depth 부터의 프로퍼티 값은 원본 객체의 참조 값을 유지합니다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프레드&amp;nbsp;연산자를 활용한 객체 복사의 동작&amp;nbsp;방식&lt;/b&gt;&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;1-depth의 프로퍼티를 새로운 객체에 복사합니다. 즉, 객체의 첫 번째 계층(직접 속성)만 복사되어 새로운 객체가 생성됩니다.&lt;/li&gt;&lt;li&gt;중첩된&amp;nbsp;객체나&amp;nbsp;배열&amp;nbsp;등&amp;nbsp;참조형&amp;nbsp;데이터는&amp;nbsp;기존&amp;nbsp;객체의&amp;nbsp;참조를&amp;nbsp;유지합니다.&amp;nbsp;따라서&amp;nbsp;하위&amp;nbsp;계층의&amp;nbsp;데이터는&amp;nbsp;원본과&amp;nbsp;동일한&amp;nbsp;메모리&amp;nbsp;주소를&amp;nbsp;참조하게&amp;nbsp;됩니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드로&amp;nbsp;이해해보기&lt;/b&gt;&lt;/h3&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const b = {
&amp;nbsp;&amp;nbsp;value: 1,
&amp;nbsp;&amp;nbsp;c: {},
};

const obj = {
&amp;nbsp;&amp;nbsp;value: 1,
&amp;nbsp;&amp;nbsp;b: b,
};

const obj2 = { ...obj };
console.log(obj, obj2);

obj2.value = 3;
obj2.b.value = 3;

console.log(obj, obj2);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MhQgK/btsKRd1p6UA/Ox8SzxjULEmHSNr9ZZ9dt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MhQgK/btsKRd1p6UA/Ox8SzxjULEmHSNr9ZZ9dt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MhQgK/btsKRd1p6UA/Ox8SzxjULEmHSNr9ZZ9dt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhQgK%2FbtsKRd1p6UA%2FOx8SzxjULEmHSNr9ZZ9dt0%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;553&quot; height=&quot;127&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;127&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;obj2 객체는 obj2 객체를 spread해서 만들었습니다. &lt;br&gt;그래서&amp;nbsp;처음&amp;nbsp;console을&amp;nbsp;찍으면&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;값이&amp;nbsp;나오는데,&amp;nbsp;그&amp;nbsp;다음&amp;nbsp;obj2객체의&amp;nbsp;value를&amp;nbsp;3으로&amp;nbsp;변경하고,&amp;nbsp;obj2.b&amp;nbsp;객체의&amp;nbsp;value도&amp;nbsp;3으로&amp;nbsp;변경했습니다. &lt;br&gt;&lt;br&gt;그&amp;nbsp;결과&amp;nbsp;값을&amp;nbsp;찍어보면,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;obj2.value는&amp;nbsp;깊은&amp;nbsp;복사를&amp;nbsp;통해&amp;nbsp;메모리&amp;nbsp;상&amp;nbsp;새로운&amp;nbsp;공간에&amp;nbsp;만들어진&amp;nbsp;객체의&amp;nbsp;프로퍼티&lt;/b&gt;&lt;/span&gt;이기 때문에 obj.value에 영향을 미치지 않지만, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;obj2.b&amp;nbsp;객체는&amp;nbsp;참조를&amp;nbsp;그대로&amp;nbsp;유지하기&amp;nbsp;때문&lt;/b&gt;&lt;/span&gt;에&amp;nbsp;obj2.b.value를&amp;nbsp;변경하면&amp;nbsp;obj.b.value&amp;nbsp;의&amp;nbsp;값도&amp;nbsp;3으로&amp;nbsp;변경된&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 메모리 구조로 살펴보면 아래와 같습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eqe1aT/btsKQFqpCYL/XapVSKlKNESFiw2XudgT1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eqe1aT/btsKQFqpCYL/XapVSKlKNESFiw2XudgT1k/img.png&quot; data-alt=&quot;메모리 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eqe1aT/btsKQFqpCYL/XapVSKlKNESFiw2XudgT1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feqe1aT%2FbtsKQFqpCYL%2FXapVSKlKNESFiw2XudgT1k%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;424&quot; height=&quot;586&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메모리 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;obj 변수는 객체가 저장된 주소인 x00020를 저장하고 있습니다. x00020에는 { value: 1, b: x00028 } 값이 저장되어 있습니다. x00028 주소에는 { c: x00032 } 값이 저장되어 있고 x00032 주소에는 {} 값이 저장되어 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;obj2 객체를 스프레드 연산자를 통해 값을 할당합니다. &lt;b&gt;1depth 프로퍼티 까지는 깊은 복사를 통해 메모리 공간을 새로 할당받고 그 공간에 객체가 저장&lt;/b&gt;됩니다. 이 위치가 x00024이고 obj2 변수가 참조하고 있는 x0008 메모리에는 x00024 주소 값이 할당되어 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;x00024 주소에 할당된 객체를 살펴보면 &lt;b&gt;b 프로퍼티에 x00028 주소 값이 할당 되어있는 것을 확인할 수 있고 이 값은 원본 객체가 참조하고 있는 b 주소 값과 일치&lt;/b&gt;합니다.&amp;nbsp;&lt;br&gt;스프레드 연산자를 통해서 객체를 생성한다면 메모리에는 위와 같은 일이 발생하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;아주&amp;nbsp;깊은&amp;nbsp;복사를&amp;nbsp;하고싶다면?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 방법으로는 JSON.parse(JSON.stringify(객체)) 를 사용할 수 있습니다.&lt;br&gt;두 번째 방법으로는 lodash 라이브러리를 사용한다면 cloneDeep 메서드를 사용해 깊은 복사를 할 수 있습니다. &lt;br&gt;마지막으로 직접 반복문을 통해 객체를 탐색해 새로운 객체를 만드는 것도 방법입니다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>복사</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/67</guid>
      <comments>https://kangs-develop.tistory.com/67#entry67comment</comments>
      <pubDate>Thu, 21 Nov 2024 15:38:51 +0900</pubDate>
    </item>
    <item>
      <title>[클린코드] 함수의 클린코드</title>
      <link>https://kangs-develop.tistory.com/66</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 3 []&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h3&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 3 []&quot;&gt;&lt;b&gt;함수의 클린 코드&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;IIFE 사용을 지양한다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가급적이면 즉시 실행 함수(IIFE: Immediately Invoked Function Expression)를 사용하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732079845686&quot; class=&quot;clojure&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;(() =&amp;gt; {
  console.log(this);
})();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 3 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 3 []&quot; data-ke-size=&quot;size16&quot;&gt;요즘의 프론트엔드는 모듈화를 지향합니다. IIFE 함수는 즉시 실행이 되기 때문에 정의한 함수를 재사용 할 수 없습니다. IIFE 함수는 디버깅이 어렵다는 단점이 있고 함수의 선언문이 즉시 실행이 되기 때문에 IIFE 함수가 지속해서 쌓이다 보면 메모리 누수에 유의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;중첩 함수에서의 스코프&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩 함수를 선언할 시 스코프에 주의합니다. 그리고 클로저 함수를 작성할 시 메모리 누수에 주의합니다. 클로저 함수를 사용할 경우 외부 함수 실행 컨텍스트가 메모리에 계속 유지가 됩니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;모던 브라우저는 클로저 함수의 메모리 누수를 방지하기 위해 메모리 최적화가 잘 되어있습니다. 하지만 개발자로써 어떤 케이스에서 메모리 누수가 발생할 수 있을지 잘 알고 코딩해야 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단일 책임의 원칙과 순수 함수&lt;/b&gt;&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;pre id=&quot;code_1732079845686&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const doingThings = async () =&amp;gt; {
  const data = await fetch(url).then((data) =&amp;gt; data.json());
  const parsedData = data.map((res) =&amp;gt; res + &quot;.json&quot;);
  //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;위 두 가지 항목을 예제 코드로 살펴봅시다. 아래 함수는 어떤 프로세스를 진행하기 위한 함수입니다. 해당 함수에서는 데이터를 불러오는 일과 데이터를 파싱하는 일을 하고 있습니다. 이러한 케이스에서 doingThings라는 함수는 어떤 일을 하는지 명확하지 않기 때문에 추후 유지 보수에 어려움을 겪을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732079845687&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const getData = async () =&amp;gt; {
  const data = await fetch(url).then((res) =&amp;gt; res.json());
  return data;
}

const parsedData = (data) =&amp;gt; {
  return data.map((res) =&amp;gt; res + &quot;.json&quot;)
}

const makeData = async () =&amp;gt; {
  const data = await getData();
  const parsedData = parsedData(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;두 가지 원칙을 반영해 코드를 리팩토링 했습니다.&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;원래의 코드보다는 행이 길어졌지만 함수를 단 하나의 일만 수행하도록 분리를 했고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-prosemirror-content-type=&quot;mark&quot; data-prosemirror-mark-name=&quot;code&quot;&gt;parsedData&lt;/span&gt;라는 함수는 매개변수를 사용해 값을 return 하기는 함수이기 때문에 항상 같은 input일 경우 같은 output을 내겠구나 하고 예측이 가능합니다.&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 함수는 하나의 일만 하고 항상 예측이 가능해야 합니다. 그래야 유지 보수에 쉽고 예측 가능한 신뢰성 높은 소프트웨어를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;매개변수&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수의 매개 변수는 가급적 3개 이하로 디자인 해야 합니다. 많은 인자가 필요하다면 객체를 넘기는 것이 이상적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size16&quot;&gt;함수는 가급적 적은 매개변수를 가지고 있어야 합니다. 이 말은 함수에 많은 매개변수를 전달해서는 안된다는 것이 아닌 클린 코드를 위해서는 매개변수의 개수가 많아서는 안된다는 것 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732079845687&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const getData = async (bookId, reviewId, authorName, bookCode, userName) =&amp;gt; {
  const data = await fetch(`http://api.com/api/book/${bookId}/review/${reviewId}?author=${authorName}&amp;amp;bookCode=${bookCode}&amp;amp;userName=${userName}`)
  return data;
}

getData(1,2,'모던자바스크립트','ef1','프론트')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;위 getData 함수는 데이터를 호출하는 함수입니다. 함수에서는 5개의 매개변수를 받고 있고, 호출하는 곳에서 매개변수 5개를 전달하고 있습니다.&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;혹시 getData를 호출할 때 매개변수로 전달하는 리터럴이 어느 값으로 전달되는지 직관적으로 파악할 수 있을까요?&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size16&quot;&gt;5개가 넘어가는 매개변수는 어떤 값으로 전달하는지 파악하기도 어렵고 디버깅 또한 쉽지 않습니다. 이런 케이스는 &lt;b&gt;객체를 통해 값을 넘기도록&lt;/b&gt; 합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1732079845688&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const getData = async (params) =&amp;gt; {
  const { bookId, reviewId, authorName, bookCode, userName } = params
  const data = await fetch(`http://api.com/api/book/${bookId}/review/${reviewId}?author=${authorName}&amp;amp;bookCode=${bookCode}&amp;amp;userName=${userName}`)
  return data;
}

getData({
  bookId: 1,
  reviewId: 2,
  authorName: '모던 자바스크립트',
  bookCode: 'ef1',
  userName: '프론트'
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;매개변수를 객체로 넘겨주니 함수를 호출하는 곳에서 어떤 값으로 넘겨주고 있는지 직관적으로 알 수 있습니다. 많은 매개변수를 사용하는 경우 반드시 객체를 통해서 값을 넘겨주도록 합시다.&lt;/p&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이른 반환(early return)을 주의해서 사용하자.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-ke-size=&quot;size16&quot;&gt;early return은 함수를 중간에 중단할 수 있으니 유용한 문법입니다. 하지만 early return을 잘못 사용하는 경우 소프트웨어가 예측하기 어렵고 디버깅이 어려울 수 있습니다. early return을 사용하지 않고 if 조건을 통해서 특정 프로세스를 넘길 수 있는 경우에는 if 조건문을 신경쓰도록 하고, early return을 사용하는 경우에는 &lt;b&gt;반드시 소프트웨어의 흐름이 예측 가능하도록 만들도록 합시다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1732079845689&quot; class=&quot;arcade&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// bad
useEffect(() =&amp;gt; {
  if (!api) return;
  api.~~~~
}, [])

// good
useEffect(() =&amp;gt; {
  if (api) {
    api.~~~
  }
}, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-pm-slice=&quot;1 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;b&gt;재귀 함수의 무한루프&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;재귀 함수를 선언할 시 무한 루프에 주의합니다. 반드시 루프를 exit 할 수 있는 조건이 존재해야 합니다.&lt;/p&gt;</description>
      <category>클린코드</category>
      <category>JavaScript</category>
      <category>클린코드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/66</guid>
      <comments>https://kangs-develop.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 20 Nov 2024 14:23:39 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 함수</title>
      <link>https://kangs-develop.tistory.com/65</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 4회차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;함수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 함수는 일급 객체이다. 즉 값처럼 변수에 할당하고 프로퍼티의 값이 될 수 있으며 배열의 요소가 될 수 있다.&lt;br /&gt;자바스크립트에서 함수를 사용하는 방식은 크게 함수 선언문, 함수 리터럴 표현식이 있다. &lt;br /&gt;이번 스터디 회차에서는 함수가 메모리에 선언되는 방법과 호이스팅, 함수의 클린 코드를 집중적으로 살펴본다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;일급 객체&lt;/b&gt;&lt;/h3&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;무명의 리터럴로 생성이 가능하다. 런타임에 생성이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const foo = function() {
&amp;nbsp;&amp;nbsp;console.log('foo')
}&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;변수나 자료구조(객체, 배열)에 저장할 수 있다.&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;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;함수의 선언&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에는 렉시컬(lexical) 이라는 개념이 있다. 작성된 코드의 문맥에 따라서 다르게 해석되는 중의적인 표현을 담고 있다. {}은 코드 블럭으로 해석될 수 있고 객체 리터럴로 해석될 수 있다. 이는 자바스크립트의 렉시컬 환경에 의해 해석이 되는 것이다. 이처럼 기명 함수 리터럴도 중의적인 코드이다. 함수 리터럴을 피연산자로 사용하는 문맥(함수 리터럴이 값으로 평가 되어야 하는 문맥)에는 함수 선언문으로 해석하고, 함수 선언문이 값으로 평가되어야 하는 문맥에는 함수 리터럴 표현식으로 해석한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;함수 선언문&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 선언문으로 함수를 선언할 시 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 거기에 함수를 할당한다.&lt;br /&gt;아래는 foo라는 기명 함수를 함수 선언문을 활용해 선언하고 해당 함수를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function foo() {}

foo();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 스크립트를 실행하면 메모리에는 다음과 같은 일들이 벌어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIQ56/btsKQzoN9Jk/aphYbqEYOwkEMLqjGOD7UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIQ56/btsKQzoN9Jk/aphYbqEYOwkEMLqjGOD7UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIQ56/btsKQzoN9Jk/aphYbqEYOwkEMLqjGOD7UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIQ56%2FbtsKQzoN9Jk%2FaphYbqEYOwkEMLqjGOD7UK%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;1140&quot; height=&quot;626&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 선언문을 통해 선언 했기에 식별자가 존재하지 않지만 코드 평가 단계에서 자바스크립트 엔진에 의해 foo라는 식별자가 암묵적으로 생성된다. 그리고 해당 메모리에 함수 객체를 할당하고 foo 식별자는 해당 공간의 주소 값을 가지게 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;foo 함수를 실행하면 먼저 해당 주소 값에 접근해 함수 객체를 가져온다. 그리고 실행문인 ()을 통해 해당 함수 객체를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메모리 위의 함수 객체&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수 선언문으로 선언된 전역 함수는 코드의 평가 시점에 메모리에 함수 객체가 생성된다고 한다. 그럼 메모리에 올라가는 함수 객체는 어떤 모습일까?&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드 평가 단계에서 생성된 함수 객체의 모습&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수 객체는 &lt;b&gt;Callable Object(호출 가능한 객체)&lt;/b&gt;로 생성되며, 다음과 같은 프로퍼티를 가진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;orderedList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[FunctionName]]&lt;/span&gt;: 함수의 이름 (익명 함수일 경우 빈 문자열 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;&quot;&quot;&lt;/span&gt;)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[Environment]]&lt;/span&gt;: 함수를 정의한 환경(스코프 체인의 정보가 저장됨)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[Code]]&lt;/span&gt;: 함수의 실행 코드&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[FormalParameters]]&lt;/span&gt;: 함수의 매개변수 정보&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[ECMAScriptCode]]&lt;/span&gt;: 함수 본문 코드&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;[[Call]]&lt;/span&gt;: 호출 시 실행되는 내부 메서드 (실제 함수 호출 시 동작을 정의)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;prototype&lt;/span&gt;: 함수 객체의 프로토타입 (일반적으로 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;constructor&lt;/span&gt; 프로퍼티를 가짐)&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;length&lt;/span&gt;: 함수의 매개변수의 개수를 나타냄&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;name&lt;/span&gt;: 함수의 이름 (기본적으로 함수 이름을 나타내며 익명 함수의 경우 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;anonymous&lt;/span&gt;로 설정)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;function add(a, b) {
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수 객체&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lua&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;[[FunctionName]]: &quot;add&quot;
[[FormalParameters]]: [&quot;a&quot;, &quot;b&quot;]
[[Code]]: return a + b;
[[Environment]]: 전역 스코프의 참조&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메모리에 올라가는 함수 객체의 모습&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;add&quot;,
  &quot;length&quot;: 2,
  &quot;prototype&quot;: { &quot;constructor&quot;: &quot;add&quot; },
  &quot;Environment&quot;: &quot;GlobalScope&quot;,
  &quot;Code&quot;: &quot;return a + b;&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;함수 표현식&lt;/b&gt;&lt;/h4&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;804&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKJ7x6/btsKPHg3n3y/nywKWrb6nqptOrwEf7Q3Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKJ7x6/btsKPHg3n3y/nywKWrb6nqptOrwEf7Q3Ek/img.png&quot; data-alt=&quot;foo 함수 리터럴을 boo 변수에 할당하고 boo 함수를 실행한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKJ7x6/btsKPHg3n3y/nywKWrb6nqptOrwEf7Q3Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKJ7x6%2FbtsKPHg3n3y%2FnywKWrb6nqptOrwEf7Q3Ek%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;804&quot; height=&quot;440&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;foo 함수 리터럴을 boo 변수에 할당하고 boo 함수를 실행한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k44ua/btsKOUIcYOz/r7cvr1cTXuGmVZbClMwQk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k44ua/btsKOUIcYOz/r7cvr1cTXuGmVZbClMwQk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k44ua/btsKOUIcYOz/r7cvr1cTXuGmVZbClMwQk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk44ua%2FbtsKOUIcYOz%2Fr7cvr1cTXuGmVZbClMwQk1%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;1300&quot; height=&quot;640&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;호이스팅&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 함수는 코드 평가 단계에서 호이스팅 된다. 이때 함수 선언문과 함수 리터럴 표현식은 다르게 동작한다. 함수 선언문의 호이스팅과 함수 표현식의 호이스팅은 다르게 동작한다. 변수에 할당된 함수 표현식의 호이스팅은 변수 호이스팅으로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;foo(); // foo
boo(); // undefined

function foo() {
&amp;nbsp;&amp;nbsp;console.log(&quot;foo&quot;);
}

var boo = function() {
&amp;nbsp;&amp;nbsp;console.log(&quot;boo&quot;);
};

boo(); // boo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 코드에서 foo 함수는 함수 선언문으로, boo 함수는 함수 표현식으로 boo 변수에 할당된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;코드의 평가 단계에서 자바스크립트 엔진은 foo 함수의 선언문을 메모리에 할당하고 암묵적으로 foo라는 식별자를 생성한다. 이때 암묵적으로 생성된 foo 식별자는 메모리에 할당된 foo 함수 객체를 할당 받았기 때문에 (식별자에 foo 함수 객체의 메모리 주소 값이 바인딩 됐다.) 함수의 실행이 가능하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;boo에 할당된 익명 함수는 변수의 할당문 이기 때문에 할당문이 실행 되는 시점(런타임)에 평가(메모리에 할당)된다. 그리고 변수의 선언문은 실행이 됨으로 boo 식별자는 메모리에 공간을 할당 받는다. 코드의 평가 단계에서는 값의 할당이 이뤄지지 않기 때문에 boo 변수는 undefined로 초기화 된다. 그래서 2행에서 boo를 실행할 때 undefined 이기 때문에 실행이 되고 에러가 발생한다. boo 함수에 할당되는 익명 함수는 코드의 실행 단계(런타임)에 함수 표현식이 평가 된다. 이때서야 함수 객체로서 메모리에 공간을 할당 받고 boo 변수에는 해당 익명 함수의 주소 값이 바인딩 된다.&lt;br /&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;1286&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l3Pvr/btsKQyDJ6YZ/LCpFshtxtHrNJxf8UK6Wj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l3Pvr/btsKQyDJ6YZ/LCpFshtxtHrNJxf8UK6Wj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l3Pvr/btsKQyDJ6YZ/LCpFshtxtHrNJxf8UK6Wj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl3Pvr%2FbtsKQyDJ6YZ%2FLCpFshtxtHrNJxf8UK6Wj1%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;1286&quot; height=&quot;926&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-width-type=&quot;pixel&quot; data-width=&quot;643&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;화살표 함수 (26.3절&lt;/b&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 () =&amp;gt; {} 함수를 간단하게 선언할 수 있다는 점으로 자주 사용된다. 문법만 간단한 것이 아닌 내부 동작도 간소화 됐다. 화살표 함수는 &lt;b&gt;this 바인딩이 다르고, prototype 프로퍼티가 존재하지 않으며 arguments 객체를 생성하지 않는다.&lt;/b&gt; 화살표 함수 내부에서 this, arguments, super, new.target을 참조하면 상위 스코프의 this, arguments, super, new.target를 참조한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;화살표 함수에서 this바인딩&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;화살표 함수에서 this바인딩은 상위 스코프를 가리&lt;/b&gt;킨다.&lt;br /&gt;아래 코드를 살펴보자. foo 메서드에서는 인자를 받아 map함수를 통해 배열을 반환한다. map 함수에는 화살표 함수가 들어가는데 여기서 this를 참조하고 있다.&lt;br /&gt;화살표 함수의 this는 클래스 내부 메서드이기에 객체 자신을 가리킨다. 10행에서 a 객체를 생성했고, 11행에서 foo메서드를 실행한다면 화살표 함수의 this는 a 객체를 가리키고 a.property인 2를 값으로 사용하게 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;class Class {
&amp;nbsp;&amp;nbsp;constructor(property) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.property = property;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;foo(arr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return arr.map((a) =&amp;gt; this.property + a);
&amp;nbsp;&amp;nbsp;}
}
const a = new Class(2);
console.log(a.foo([1, 2, 3])); // [3,4,5]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;반대로 일반 함수 선언문에서의 this를 살펴보자. 일반 함수 내부에서 this 바인딩은 전역 객체이다. 이를 node.js 환경에서 실행하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function func() {
&amp;nbsp;&amp;nbsp;return () =&amp;gt; this;
}
console.log(func()());&lt;/code&gt;&lt;/pre&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;808&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M9uaj/btsKQCTnpph/wroalKQBIsBlR7BrYJ1To1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M9uaj/btsKQCTnpph/wroalKQBIsBlR7BrYJ1To1/img.png&quot; data-alt=&quot;node.js 환경에서 전역 객체는 global 객체이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M9uaj/btsKQCTnpph/wroalKQBIsBlR7BrYJ1To1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM9uaj%2FbtsKQCTnpph%2FwroalKQBIsBlR7BrYJ1To1%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;808&quot; height=&quot;832&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;node.js 환경에서 전역 객체는 global 객체이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;메서드에서 this를 참조하고 싶다면 반드시 일반 메서드 선언문으로 메서드를 만들어야 한다. 아래 코드를 살펴보면 getSome 메서드를 화살표 함수로 만들고 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;상위 스코프인 this는 전역 객체이다. 전역 객체에는 ranNum이라는 프로퍼티가 존재하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const obj = {
&amp;nbsp;&amp;nbsp;ranNum: Math.random(),
&amp;nbsp;&amp;nbsp;getSome: () =&amp;gt; console.log(this, this.ranNum),
};
obj.getSome(); // {} undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래 객체를 살펴보자. getSome메서드는 일반 메서드 선언문으로 작성됐다. 해당 메서드의 this 바인딩은 객체 자신이 되며 obj2 객체 자체와 ranNum 프로퍼티가 출력된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const obj2 = {
&amp;nbsp;&amp;nbsp;ranNum: Math.random(),
&amp;nbsp;&amp;nbsp;getSome() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;console.log(this, this.ranNum);
&amp;nbsp;&amp;nbsp;},
};
obj2.getSome(); // { ranNum: 0.8168228885628372, getSome: [Function: getSome] } 0.8168228885628372&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;매개변수 (값 전달, 주소 전달)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원시 값은 변경 불가능하기 때문에 함수의 매개 변수로 전달할 때 역시 값 그 자체만 전달이 된다(call by value). 하지만 객체는 변경 가능하고 메모리에 참조 값을 가지고 있기 때문에 함수의 매개 변수로 전달할 때 역시 참조 주소를 전달한다 (call by address). 이는 함수 내부에서 객체를 변경한다면 원래의 객체 역시 변경이 된다는 뜻이다.&lt;br /&gt;사이드 이펙트를 방지하기 위해 Object.freeze를 사용해 객체를 불변하게 만든다던지, 함수 내부에서 객체를 깊은 복사를 하여 새로운 객체를 생성해 사용하는 방법을 사용해야 한다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>자바스크립트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/65</guid>
      <comments>https://kangs-develop.tistory.com/65#entry65comment</comments>
      <pubDate>Wed, 20 Nov 2024 14:18:12 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 원시 값과 객체 리터럴</title>
      <link>https://kangs-develop.tistory.com/64</link>
      <description>&lt;blockquote data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 3회차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;객체&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트에서는 원시 값을 제외한 모든 값이 객체이다. 배열, 함수, 정규식 등 모든 것들이 객체이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;원시 값은 변경 불가능하다. 메모리에 한번 선언이 되고 원시 값의 값을 변경한다면 해당 주소의 값이 변경 되는 것이 아닌 새로운 공간에 메모리를 할당하고 해당 주소에 값을 매핑한다. 반면에 객체는 메모리에 선언된 후 해당 주소의 값을 변경할 수 있다. 이 말은 새로운 객체를 변수에 할당하는 것이 아닌 해당 객체의 프로퍼티 값을 변경해도 메모리의 주소가 달라지지 않는다는 것이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로퍼티&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;객체는 프로퍼티의 집합이고 프로퍼티는 문자열 또는 심벌 타입의 키와 값으로 구성된다. 값은 원시 값이나 어떠한 객체가 들어갈 수 있다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;프로퍼티의 키는 가능하면 네이밍 컨벤션을 지켜야 한다. 네이밍 컨벤션을 지키지 않아도 키로 선언될 수 있지만 그 경우에는 문자열임을 알리는 따옴표를 사용해야 하고, 해당 프로퍼티에 접근하기 위해서 대괄호 표기법을 사용해야 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;객체에 존재하지 않는 프로퍼티에 접근하면 undefined를 반환한다. &lt;b&gt;Dynamic한 객체를 사용할 땐 해당 프로퍼티가 undefined의 가능성을 내포하고 있다는 것에 주의&lt;/b&gt;해서 프로그래밍 해야한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로퍼티의 제거&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서는 delete 연산자를 사용해 객체의 프로퍼티를 제거할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;delete 연산자를 사용하면 간단하게 프로퍼티를 제거할 수 있지만, 특정 프로퍼티를 제거한 객체가 필요하다면 &lt;b&gt;불변성 유지를 위해 원본 객체를 변경하는 것이 아닌 새로운 객체를 생성&lt;/b&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;객체 불변성을 유지하는 것은 다음과 같은 이점을 누릴 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;원본 객체를 변경하지 않으므로 사이드 이펙트가 없습니다.&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Immutable 패턴은 데이터 변경 추적, 디버깅, 상태 관리에서 유리합니다(예: React, Redux 등에서 자주 사용).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;하지만 기존 객체가 메모리에 유지가 되기 때문에 메모리 누수가 발생할 수 있다는 점도 기억해야 합니다. 전역 변수와 같이 프로세스가 동작할 때 상시 유지되는 변수라면 불변성을 이용한 프로그래밍은 메모리 누수가 발생합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Mutable Programming&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731733142412&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// delete 연산자를 통해 해당 객체의 프로퍼티를 제거합니다. Mutable 합니다.
const obj = {
  a: 1,
  b: 2,
  c: {},
};

delete obj.c;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Immutable Programming&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731733153919&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 불변성을 위해 새로운 객체를 생성해 리턴합니다.
function deleteProperty(obj, propertyKey) {
  const keys = Object.keys(obj);
  return keys.reduce(
    (prev, cur) =&amp;gt; ({
      ...prev,
      ...(cur === propertyKey ? {} : { [cur]: obj[cur] }),
    }),
    {}
  );
}

const obj = {
  a: 1,
  b: 2,
  c: {},
};

console.log(&quot;immutable&quot;, deleteProperty(obj, &quot;c&quot;));&lt;/code&gt;&lt;/pre&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-filename=&quot;스크린샷 2024-11-16 오후 12.50.39.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tlUY0/btsKMdUgHx5/DCyjxt6GPZHsOnKd1LRKEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tlUY0/btsKMdUgHx5/DCyjxt6GPZHsOnKd1LRKEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tlUY0/btsKMdUgHx5/DCyjxt6GPZHsOnKd1LRKEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtlUY0%2FbtsKMdUgHx5%2FDCyjxt6GPZHsOnKd1LRKEK%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;832&quot; height=&quot;110&quot; data-filename=&quot;스크린샷 2024-11-16 오후 12.50.39.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;원시 값과 객체의 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 원시 값은 변경 불가능(Immutable) 합니다. 즉 메모리에 값이 할당된다면 해당 메모리에는 새로운 값을 할당할 수 없습니다. 그리고 원시 값은 메모리에 실제 값이 저장이 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;원시 값에 값을 재할당 할 경우 새로운 메모리 공간을 할당 받습니다.&lt;/a&gt;&lt;/b&gt;&lt;/i&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;반면에 객체는 변경 가능(Mutable)합니다. 또한 메모리에 공간을 확보하면 해당 공간에 값을 직접 저장하는 것이 아닌 참조 값이 저장됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;자바스크립트에서 객체는 변경 가능하다고 했습니다. 이는 메모리의 효율성을 위함 입니다. 만약 커다란 객체가 변경될 때 마다 메모리에 새로운 공간을 할당 받는다면 해당 객체를 복사하고 새로운 메모리에 할당하기까지의 수행 시간과 메모리 공간의 낭비가 정비례로 발생합니다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;메모리에 할당된 객체.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N9eSC/btsKMONafCG/V87FAck7oUYkimIvbuSKa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N9eSC/btsKMONafCG/V87FAck7oUYkimIvbuSKa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N9eSC/btsKMONafCG/V87FAck7oUYkimIvbuSKa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN9eSC%2FbtsKMONafCG%2FV87FAck7oUYkimIvbuSKa0%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;650&quot; height=&quot;728&quot; data-filename=&quot;메모리에 할당된 객체.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성에 의해 원시 값이 할당된 변수를 다른 변수에 할당한다면 값 전달 (call by value)에 의해 값이 복사되어 전달됩니다. 반면에 객체를 다른 변수에 할당한다면 주소 값 전달 (call by address)에 의해 참조 값이 복사 됩니다. (얕은 복사)&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;pre id=&quot;code_1731733470633&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let a = 1;
let b = a;

console.log(a, b); // 1, 1
a = 2;

console.log(a, b); // 2, 1&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;원시 값을 a 변수에 할당하고 b 변수에 a를 전달합니다. 이때 a의 값은 원시 값이기 때문에 call by value 값 전달으로 인해 실제 값이 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;아래 그림을 살펴보면 a 변수와 b 변수는 메모리 상 다른 공간을 바라보고 있습니다. 단순히 값이 전달 된 것 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-17 오후 1.03.29.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFo6X/btsKNfcIws9/hnRxNpon5wxaj7fZ2kJJt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFo6X/btsKNfcIws9/hnRxNpon5wxaj7fZ2kJJt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFo6X/btsKNfcIws9/hnRxNpon5wxaj7fZ2kJJt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFo6X%2FbtsKNfcIws9%2FhnRxNpon5wxaj7fZ2kJJt1%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;660&quot; height=&quot;674&quot; data-filename=&quot;스크린샷 2024-11-17 오후 1.03.29.png&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 변수에 2를 할당하면 메모리에는 다음과 같은 일이 발생합니다. 원시 값은 불변이기 때문에 메모리에 새로운 공간을 할당 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;원시 값 변경.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXl6S9/btsKMHtN85A/IMzwqQAnUkysemPpZcrkTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXl6S9/btsKMHtN85A/IMzwqQAnUkysemPpZcrkTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXl6S9/btsKMHtN85A/IMzwqQAnUkysemPpZcrkTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXl6S9%2FbtsKMHtN85A%2FIMzwqQAnUkysemPpZcrkTK%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;542&quot; height=&quot;640&quot; data-filename=&quot;원시 값 변경.png&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1731733530946&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = { a: 1 };
const obj2 = obj;

console.log(obj, obj2); // { a: 1 } { a: 1 }

obj.a = 2;

console.log(obj, obj2); // { a: 2 } { a: 2 }&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;반면에 객체는 변경 가능합니다. obj에 객체를 할당하고 obj2에 obj 값을 할당합니다. 객체는 메모리에 참조 값을 저장하고 있습니다. 이로 인해서 obj2에는 obj의 참조 값이 저장이 됩니다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;mediaSingle&quot; data-prosemirror-content-type=&quot;node&quot; data-width-type=&quot;pixel&quot; data-width=&quot;323&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;변수에 객체 할당.png&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebBIgh/btsKLkzGzv4/3INJJt41dfgZMSAk1Uz4g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebBIgh/btsKLkzGzv4/3INJJt41dfgZMSAk1Uz4g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebBIgh/btsKLkzGzv4/3INJJt41dfgZMSAk1Uz4g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebBIgh%2FbtsKLkzGzv4%2F3INJJt41dfgZMSAk1Uz4g1%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;646&quot; height=&quot;680&quot; data-filename=&quot;변수에 객체 할당.png&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4행의 콘솔의 결과는 &lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;{ a: 1 } { a: 1 }&lt;/span&gt;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;6행에서 &lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;obj.a&lt;/span&gt; 의 값을 2로 변경합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;call by address 이기 때문에 8행 콘솔의 출력 결과는 &lt;b&gt;&lt;span data-prosemirror-mark-name=&quot;code&quot; data-prosemirror-content-type=&quot;mark&quot;&gt;{ a: 2 } { a: 2 }&lt;/span&gt;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;obj.a의 값 변경.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SohLe/btsKMExexn8/WJkWDeBuUrjwKAAlKKiyrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SohLe/btsKMExexn8/WJkWDeBuUrjwKAAlKKiyrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SohLe/btsKMExexn8/WJkWDeBuUrjwKAAlKKiyrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSohLe%2FbtsKMExexn8%2FWJkWDeBuUrjwKAAlKKiyrk%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;676&quot; height=&quot;588&quot; data-filename=&quot;obj.a의 값 변경.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-16 오후 1.17.49.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bza09P/btsKL4b9V3M/IZVCFreUaB6jopQJu2sfHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bza09P/btsKL4b9V3M/IZVCFreUaB6jopQJu2sfHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bza09P/btsKL4b9V3M/IZVCFreUaB6jopQJu2sfHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbza09P%2FbtsKL4b9V3M%2FIZVCFreUaB6jopQJu2sfHK%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;782&quot; height=&quot;146&quot; data-filename=&quot;스크린샷 2024-11-16 오후 1.17.49.png&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&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;메모리 위에서의 객체는 위와 같은 구조입니다. 같은 객체를 공유하고 있기 때문에 obj2에서 값을 변경할 경우 obj에서도 값이 변경이 됩니다. 메모리에 선언된 객체는 변경 가능하다는 특성으로 인해 obj에서는 &lt;b&gt;원치 않는 사이드 이펙트가&lt;/b&gt; 발생할 수 있습니다. 그렇기 때문에 이를 정확하게 이해하고 코딩해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;얕은 복사와 깊은 복사&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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;얕은 복사 깊은 복사.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qlCEQ/btsKMjGT5vg/cjv1pq8Q0f9nKlvOnNqkY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qlCEQ/btsKMjGT5vg/cjv1pq8Q0f9nKlvOnNqkY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qlCEQ/btsKMjGT5vg/cjv1pq8Q0f9nKlvOnNqkY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqlCEQ%2FbtsKMjGT5vg%2Fcjv1pq8Q0f9nKlvOnNqkY1%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;1500&quot; height=&quot;784&quot; data-filename=&quot;얕은 복사 깊은 복사.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>티스토리챌린지</category>
      <category>프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/64</guid>
      <comments>https://kangs-develop.tistory.com/64#entry64comment</comments>
      <pubDate>Sat, 16 Nov 2024 14:11:31 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 배열 만들기</title>
      <link>https://kangs-develop.tistory.com/63</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹&amp;nbsp;애플리케이션을&amp;nbsp;만들다&amp;nbsp;보면&amp;nbsp;원하는&amp;nbsp;크기&amp;nbsp;만큼의&amp;nbsp;배열을&amp;nbsp;생성하고&amp;nbsp;싶을&amp;nbsp;때가&amp;nbsp;있습니다. &lt;br /&gt;&lt;br /&gt;사실 자바스크립트의 배열은 일반적인 프로그래밍에서 정의하고 배우는 밀집 배열이 아니기 때문에 런타임 중 자유롭게 배열에 요소를 추가하고 삭제하고 할 수 있습니다. 컴파일 시점에 데이터의 갯수가 정해져 있고, 이를 배열로 구현해야 하는 경우에는 어떻게 해야 할까요?&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;&lt;b&gt;for&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1731569073661&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dates = [];

for (let i = 1; i &amp;lt;= days; i++) {
  dates.push(`${i}일`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;for문을 활용해 계산된 days만큼 loop를 돌아서 날짜를 만들고, 생성한 날짜를 배열에 밀어 넣습니다. 가장&amp;nbsp;간단한&amp;nbsp;방법이면서&amp;nbsp;기초&amp;nbsp;문법만으로도&amp;nbsp;쉽게&amp;nbsp;구현할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방법입니다.&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;&lt;b&gt;Array.from&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array.from은&amp;nbsp;정적&amp;nbsp;메서드로&amp;nbsp;순회가&amp;nbsp;가능하거나&amp;nbsp;유사&amp;nbsp;배열&amp;nbsp;객체를&amp;nbsp;얕게&amp;nbsp;복사해&amp;nbsp;새로운&amp;nbsp;Array로&amp;nbsp;만들어주는&amp;nbsp;메서드&amp;nbsp;입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;여기서 순회 가능한 객체란 Map, Set과 같은 객체를 말하고, 유사 배열 객체란 length 속성과 인덱싱된 요소를 가지고 있는 객체를 뜻합니다.&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;Array.from&amp;nbsp;메서드에&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;인자에&amp;nbsp;length를&amp;nbsp;가지고&amp;nbsp;있는&amp;nbsp;객체를&amp;nbsp;넘겨주고,&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;인자로&amp;nbsp;배열의&amp;nbsp;모든&amp;nbsp;요소에&amp;nbsp;대해&amp;nbsp;호출할&amp;nbsp;함수(mapFn)를&amp;nbsp;넘겨줄&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;mapFn의&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;인자로는&amp;nbsp;각&amp;nbsp;인덱스의&amp;nbsp;요소를,&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;인자로는&amp;nbsp;인덱스를&amp;nbsp;받을&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731569126472&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const dates = Array.from({ length: days }, (_, idx) =&amp;gt; `${idx + 1}일`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;패러다임&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하는 방식은 다르지만 요구사항은 같습니다. 단순히 해당 월의 첫 날부터 마지막 날을 담은 배열이 필요한 것 입니다. 그럼 for문을 활용해 쉽게 구현할 수 있는 방법을 제치고, 굳이 Array.from 메서드를 사용해야 하는 경우가 있을까요? &lt;br /&gt;&lt;br /&gt;만약 팀에서 함수형 프로그래밍을 택해서 프로그래밍을 하고 있다면 for문을 활용해 명령형으로 프로그래밍 하는 것이 어려울 것 입니다. &lt;br /&gt;&lt;br /&gt;이때 Array.from 메서드를 활용한다면 함수형 프로그래밍 패러다임을 지키며 요구사항을 손쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>array</category>
      <category>Array.from</category>
      <category>JavaScript</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/63</guid>
      <comments>https://kangs-develop.tistory.com/63#entry63comment</comments>
      <pubDate>Thu, 14 Nov 2024 16:31:23 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 변수와 데이터 타입</title>
      <link>https://kangs-develop.tistory.com/62</link>
      <description>&lt;blockquote data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 2회차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/blockquote&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;변수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름을 말한다. 변수는 프로그래밍 언어에서 값을 저장하고 참조하는 메커니즘으로 값의 위치를 가리키는 상징적인 이름이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;식별자&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;변수의 이름을 식별자라고 한다. 식별자는 특정 값을 구분할 수 있도록 구유해야 한다. 식별자는 값이 저장된 메모리 주소와 1대1 매핑 관계를 맺으며 이 매핑 관계 또한 메모리에 저장되어 있다. 즉 식별자는 메모리에 올라가 있는 값을 가지고 있는 것이 아닌 &lt;b&gt;해당 메모리의 주소 값을 가지고 있는 것&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;변수의 선언&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;var keyword = &quot;keyword&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;변수의 선언은 메모리에 해당 값을 위한 공간을 확보하고 메모리의 주소와 식별자를 매핑해 값을 저장할 준비를 하는 것 이다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;변수가 선언이 되면서 값이 할당되지 않는다면 초기 값으로 undefined라는 값이 암묵적으로 할당된다.&lt;/blockquote&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;변수의 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;변수의 초기화는 선언된 변수에 값을 할당하는 행위이다. 즉 변수에 매핑된 메모리의 주소 공간에 해당 값을 할당하는 것 이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;값의 할당&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;변수의 선언 단계에서 메모리에 공간을 확보한다. 변수의 선언은 코드의 평가 단계에서 진행되기 때문에 암묵적으로 undefined 값이 할당된다. 그 이후 코드의 실행 단계(런타임)에서 값의 할당이 이뤄지는데, 이때 변수의 선언 당시에 확보한 메모리 공간에 값을 할당하는 것이 아닌 &lt;b&gt;새로운 주소 값에 값을 할당한 후 식별자가 해당 메모리 주소를 바라보게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-09 오전 11.56.27.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lWoWi/btsKC0ASqxz/h3RejFrVjNfUs1MO9Usd60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lWoWi/btsKC0ASqxz/h3RejFrVjNfUs1MO9Usd60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lWoWi/btsKC0ASqxz/h3RejFrVjNfUs1MO9Usd60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlWoWi%2FbtsKC0ASqxz%2Fh3RejFrVjNfUs1MO9Usd60%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;1596&quot; height=&quot;884&quot; data-filename=&quot;스크린샷 2024-11-09 오전 11.56.27.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;값의 재할당&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; 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;스크린샷 2024-11-09 오후 12.01.14.png&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZbmh/btsKCni71C6/sfBjFO7uxV84yd3ZgXV1kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZbmh/btsKCni71C6/sfBjFO7uxV84yd3ZgXV1kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZbmh/btsKCni71C6/sfBjFO7uxV84yd3ZgXV1kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZbmh%2FbtsKCni71C6%2FsfBjFO7uxV84yd3ZgXV1kk%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;2114&quot; height=&quot;622&quot; data-filename=&quot;스크린샷 2024-11-09 오후 12.01.14.png&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위 메모리를 살펴보면 최종적으로 12, 24번 주소의 메모리는 사용하고 있지 않다는 것을 확인할 수 있다. 해당 값들은 자바스크립트의 GC (가비지 컬렉터) 에 의해서 회수된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;호이스팅&lt;/b&gt;&lt;/h3&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트의 실행은 코드의 평가 단계와 실행 단계로 나눠진다. 자바스크립트가 실행되면 자바스크립트 엔진은 작성된 코드를 평가한다. 이 단계에서 코드를 위에서 아래로 읽어나가며 선언된 변수, 함수를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;console.log(keyword); //undefined

var keyword = &quot;keyword&quot;;

console.log(keyword); //keyword&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위 예제 코드를 살펴보면 첫 번째 로그로 undefined가 출력된다. 자바스크립트가 실행이 되면서 코드의 평가 단계에서 keyword라는 변수의 선언을 실행하기 때문이다. 평가단계에서 변수가 선언이 되었으니 keyword라는 변수를 참조할 수 있고, 암묵적으로 바인딩 된 undefined를 출력한다. 그 이후에 실행 단계(런타임)에서 keyword의 값이 &amp;ldquo;keyword&amp;rdquo; 라는 리터럴로 바인딩이 되어 5번째 행에서 &amp;ldquo;keyword&amp;rdquo; 를 출력하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-09 오전 11.42.41.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ADWjZ/btsKD4bkkw1/gIM7Gdd2LKK2ofcgSGs8HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ADWjZ/btsKD4bkkw1/gIM7Gdd2LKK2ofcgSGs8HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ADWjZ/btsKD4bkkw1/gIM7Gdd2LKK2ofcgSGs8HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FADWjZ%2FbtsKD4bkkw1%2FgIM7Gdd2LKK2ofcgSGs8HK%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;908&quot; height=&quot;538&quot; data-filename=&quot;스크린샷 2024-11-09 오전 11.42.41.png&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&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;첫 번째 행에서 keyword는 선언된 변수가 아니지만 참조가 가능하다. 코드의 선언문이 작성된 코드의 최 상단으로 끌어 올려진 것 처럼 동작한다. 이를 두고 자바스크립트에서는 호이스팅(끌어올림)이라고 부르고 있다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;var 키워드로 선언된 변수 뿐만 아니라 let, const, fuction, class 등의 키워드로 선언된 모든 식별자는 호이스팅 된다. var 키워드는 코드의 평가 단계에서 식별자의 선언과 암묵적 초기화가 동시에 진행된다. 이때 초기화는 개발자가 작성한 할당문이 실행되는 것이 아닌 undefined가 암묵적으로 바인딩 되는 것이다.&lt;br /&gt;&lt;br /&gt;let 키워드로 선언된 변수 역시 호이스팅이 되지만 var 키워드와 다르게 동작한다. let 변수는 코드의 평가 단계에서 변수 선언만 발생한다. 변수의 초기화 단계는 코드가 실행될 때 초기화 문을 만나면 진행된다.&lt;br /&gt;&lt;br /&gt;let, const 키워드로 선언한 변수는 호이스팅은 발생하지만 선언되기 이전에 참조할 수 없다. 이는 해당 키워드로 선언한 변수는 일시적 사각지대 (TDZ)에 의해 관리되기 때문에 변수가 선언되기 이전에 참조할 수 없다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-pm-slice=&quot;1 2 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데이터의 타입&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트의 데이터 타입은 원시 타입과 객체 타입으로 분류한다. 원시 타입은 불변한 값이며 객체 타입은 변할수 있는 값으로 분류한다. 원시 타입은 &lt;b&gt;숫자, 문자열, 템플릿 리터럴, undefined, null, 심벌 타입&lt;/b&gt;으로 총 6가지이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;데이터의 타입이 필요한 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;값을 &lt;b&gt;저장할 때 메모리 공간의 크기를 결정&lt;/b&gt;하기 위해&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;값을 참조할 때 한 번 &lt;b&gt;읽어 들어야 할 메모리 공간의 크기를 결정&lt;/b&gt;하기 위해&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;메모리에서 읽어 들인 &lt;b&gt;2진수를 어떻게 해석할 지 결정&lt;/b&gt;하기 위해&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 데이터의 타입은 런타임에 결정이 된다. 코드의 작성 시점에 타입을 지정하지 않고 런타임 시 선언된 변수에 값이 할당되는 순간에 타입이 결정(타입 추론)된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;선언된 변수는 언제든지 다른 타입의 값으로 재할당 될 수 있다. 이는 동적으로 타입이 정해진다는 것 이며 이러한 특성으로 인해 자바스크립트를 동적 타입 언어 라고 부른다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;타입이 동적으로 정해지는 언어는 한번 선언한 변수에 다양한 값을 할당할 수 있지만 이로 인해서 타입 안정성이 떨어짐으로 소프트웨어에 대한 신뢰성이 떨어진다. 또한 타입을 단언하기 위해 여러 방어 코드를 작성해야 함으로 정적 타입 언어에 비해 가독성이 떨어질 수 있다. 아래의 예시 코드는 React에서 타입을 단언 (방어) 하기 위한 코드이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;const Component = (props) =&amp;gt; {
  const handleClick = () =&amp;gt; {
    if (props.onClick instanceof Function) {
      props.onClick();
    }
  }
  
  const label = props?.label ?? '버튼';
  const count = typeof props?.count === 'number' ? props.count : 0;
  
  return &amp;lt;button&amp;gt;{label}&amp;lt;/button&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;props로 전달받는 onClick, label, count 값을 런타임 에러를 피하기 위해 다양한 방어 코드를 작성하고 있다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>티스토리챌린지</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/62</guid>
      <comments>https://kangs-develop.tistory.com/62#entry62comment</comments>
      <pubDate>Sat, 9 Nov 2024 13:31:54 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 프로그래밍과 자바스크립트 특징</title>
      <link>https://kangs-develop.tistory.com/61</link>
      <description>&lt;blockquote data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모던 자바스크립트 Deep Dive 스터디 4회차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.&lt;/blockquote&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로그래밍&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프로그래밍이란 컴퓨터에게 실행을 요구하는 일종의 커뮤니케이션이다. 일반적으로 개발자가 사용하는 프로그래밍 언어는 고수준 언어이다. 인간이 컴퓨터에게 명령을 내리기 가장 좋은 형태로 추상화된 언어이며 개발자가 고수준의 언어로 작성한 코드는 컴파일러 혹은 인터프리터에 의해 컴퓨터가 알기 쉬운 언어인 기계어로 번역된다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;컴파일러와 인터프리터의 동작하는 방식은 다르다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;프로그래밍을 하는데 있어서 언어의 문법을 지키는 것은 당연한 일이다. 하지만 문법을 지키는 것 만큼 중요한 일이 있는데, 그것은 반드시 코드에는 의미가 담겨야 한다는 것 이다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;codeBlock&quot; data-prosemirror-content-type=&quot;node&quot;&gt;&lt;code&gt;// 아래 예제는 교재의 예제
const number = 'string';

console.log(number + number);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;위 예제를 실행하면 에러 없이 정상적으로 &amp;lsquo;stringstring' 이 출력된다. 문법상 오류가 없지만 해당 코드에는 의미가 전혀 담겨있지 않다. number 라는 변수는 문자열 타입이고 변수명에는 전혀 어떠한 의미가 담겨있지 않다. 이는 추후 유지보수에 치명적이며 코드의 품질은 당연 말할것 없이 형편 없다고 볼 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;결국 좋은 프로그래밍은 컴퓨터에게 실행을 요구하는 코드를 작성하는 일이기에 컴퓨터가 잘 이해할 수 있도록 문법을 잘 지켜야 하고, 여러 인간의 요구를 담고있는 결과물(코드)에도 인간이 이해할 수 있도록 의미를 잘 담아야 한다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자바스크립트(JavaScript)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 브라우저에서 사용자에게 다이나믹한 경험을 제공하기 위해 개발된 언어이다. 개발 초창기에 Java의 유행에 편승하기 위해 JavaScript라는 이름을 채택했다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style2&quot;&gt;현재 JavaScript의 상표는 Oracle에서 소유하고 있다. 하지만 Oracle이 JavaScript의 업데이트에 관여를 안한지 오래되어 최근엔 오라클이 자바스크립트의 상표권을 오픈해야 한다는 소리도 들리고 있다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 HTML, CSS 와 함께 웹 애플리케이션의 화면단인 프론트엔드를 개발할 때 주로 사용한다. 요즘은 다양한 프론트엔드 프레임워크가 존재한다. 대표적으로 React, Vue, Angular, Svelte, jQuery 등 이 존재한다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;요즘의 자바스크립트는 브라우저외의 환경에서도 동작이 가능하다. 크롬 엔진인 V8을 기반으로 한 자바스크립트 런타임 환경인 Node.js 덕분에 자바스크립트를 활용해 서버도 구현이 가능하다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 특징 네 가지를 간단하게 살펴보면 다음과 같다.&lt;/p&gt;
&lt;h4 data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인터프리터 언어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 인터프리터 언어이다. 인터프리터는 컴파일러와는 다르게 빌드 시간을 거쳐 작성한 코드를 통채로 기계어로 번역해 사용하는 것이 아닌, 런타임에 문장 단위 한 줄씩 목적 코드인 바이트 코드로 변환한 뒤 실행한다. 런타임에 문장을 번역해 실행하기 때문에 실행 파일이 생성되지 않고, 코드의 실행 시 인터프리트 과정이 반복된다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;코드가 문장 단위로 한 줄씩 실행되기 때문에 디버깅에 용이하고 프로그램이 실행될 때 즉시 실행이 되기 때문에 빠르게 피드백을 받을 수 있다는 장점이 존재한다. 하지만 코드를 실행하며 인터프리트 과정을 반복하기 때문에 컴파일 언어에 비해 느릴 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;요즘의 프론트엔드 애플리케이션은 작성한 코드를 번들링하는 과정을 통해 브라우저가 더욱 이해하기 쉽도록 빌드하는 과정을 거친다. 그렇기 때문에 우리가 작성하는 모던 웹 애플리케이션을 그대로 브라우저가 읽는 것이 아닌, 번들링 과정을 거쳐 생성된 빌드 결과물을 브라우저에서 실행한다는 것을 알고 있어야 한다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;프론트엔드 애플리케이션을 빌드하는 과정은 단순히 번들링만을 하는 것은 아니다. 빌드 시 TypeScript를 사용한다면 타입스크립트 컴파일러에 의해 TypeScript로 작성된 파일이 JavaScript로 컴파일 된다. TypeScript를 JavaScript로 컴파일 한 후 번들러에 의해 번들링이 진행된다. 그 후 번들링 된 파일을 조금 더 압축 시키기 위해 맹글러(eg. Terser)에 의해서 압축을 한다면 프론트엔드 빌드 과정이 끝나게 된다.&lt;br /&gt;&lt;br /&gt;프론트엔드에서의 빌드는 번들링의 과정이고 번들링으로 인해 나온 결과물을 빌드 파일을 웹 서버에서 서빙을 해 브라우저에서 해당 에셋들을 다운로드 받아서 실행시킨다. 모던 브라우저의 자바스크립트 엔진은 자바스크립트 파일을 그대로 인터프리트 하는 것이 아닌, &lt;b&gt;JIT(Just In Time) 컴파일을 통해 기계어로 컴파일 한 후 실행&lt;/b&gt;한다.&lt;/blockquote&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;멀티패러다임 프로그래밍 언어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;프로토타입 기반 OOP&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 프로토타입을 통해 객체지향 프로그래밍(OOP)을 지원하고 있다. 객체는 다른 객체를 원형(prototype)으로 삼아 생성되며, 그 원형 객체의 속성과 메서드를 상속한다.&lt;/p&gt;
&lt;blockquote data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;생성된 객체는 프로토타입을 통해 런타임에 유연하게 프로퍼티와 메서드를 확장할 수 있다. JavaScript 객체는 내부에 [[Prototype]]이라는 숨겨진 프로퍼티를 가지며, 이를 통해 상위 프로토타입에 접근한다. 만약 객체에서 원하는 속성이나 메서드를 찾지 못하면, 프로토타입 체인을 따라 상위 프로토타입에서 속성을 찾는 방식으로 동작한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;함수형 프로그래밍&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;함수형 프로그래밍은 불변성을 강조하며, 상태 변경을 최소화하고 순수 함수를 사용하여 코드의 예측 가능성과 재사용성을 높이는 것을 목표로 삼는다.&lt;/p&gt;
&lt;div data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;panel&quot; data-prosemirror-content-type=&quot;node&quot; data-panel-type=&quot;info&quot;&gt;
&lt;div data-panel-content=&quot;true&quot;&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;순수 함수란 동일한 입력 값을 받으면 항상 동일한 결과 값을 뱉는다는 개념이다. 즉 외부에 사이드 이펙트가 없는 함수를 순수 함수라고 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트는 함수를 일급 객체로 여긴다. 그렇기 때문에 함수를 변수에 할당하거나, 함수의 매개 변수로 사용할 수 있고 반환 값으로 사용할 수 있다. 그리고 자바스크립트는 클로저를 지원하며 이 모든 것들을 활용해 함수형 프로그래밍을 구현할 수 있다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Dynamic Language&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트의 타입은 런타임에 정해진다. 이 말은 자바스크립트는 동적 타입의 언어라는 것 이다. 타입이 런타임에 정해지기 때문에 코드를 작성하는 시점에 변수에 값을 유연하게 할당할 수 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;유연하다는 것은 불안정 할 수 있다는 뜻도 된다. 런타임 시 예상하지 못한 타입의 값이 변수에 할당된다면 런타임 에러를 발생할 수 있다. 이로 인해 코드 작성 시점에 디버깅이 어려울 수 있으며 런타임 에러를 완벽하게 핸들링 할 수 없기 때문에 신뢰가 떨어지는 결과물을 작성할 수 있다는 치명적인 단점이 있다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;요즘에는 자바스크립트에 타입을 얹은 TypeScript를 사용해 위 문제를 타입 안정성을 더하며 웹 애플리케이션을 만들고 있는 추세이다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;싱글 스레드&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트 엔진은 메인 스레드만 가지고 있는 싱글 스레드 언어이다. 자바스크립트에는 별도의 스레드를 작성할 수 있는 기능을 지원하지 않는다. 그럼에도 불구하고 자바스크립트는 비동기 통신이 가능하다. 이는 이벤트 루프라는 특수한 메커니즘 덕분에 가능한 일이다.&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <category>JavaScript</category>
      <category>자바스크립트</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/61</guid>
      <comments>https://kangs-develop.tistory.com/61#entry61comment</comments>
      <pubDate>Sun, 3 Nov 2024 14:46:26 +0900</pubDate>
    </item>
    <item>
      <title>&amp;quot;admin&amp;quot; == &amp;quot;admin   &amp;quot;</title>
      <link>https://kangs-develop.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목에&amp;nbsp;이끌려서&amp;nbsp;들어오신&amp;nbsp;여러분들&amp;nbsp;환영합니다. &lt;br /&gt;이번&amp;nbsp;글은&amp;nbsp;&lt;b&gt;문자열&amp;nbsp;&quot;admin&quot;&amp;nbsp;과&amp;nbsp;&quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;이&amp;nbsp;같다&lt;/b&gt;는&amp;nbsp;것을&amp;nbsp;증명하기&amp;nbsp;위한&amp;nbsp;글이&amp;nbsp;될&amp;nbsp;예정입니다.&lt;br /&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;먼저 서두에 밝히도록 하겠습니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;admin&quot; 과 &quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;은 프로그래밍적 관점에서는 절대 같은 문자열이 아닙니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccfUcA/btsKgMcybfK/zDX6WsPQjK9kxkO8OLkv2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccfUcA/btsKgMcybfK/zDX6WsPQjK9kxkO8OLkv2k/img.png&quot; data-alt=&quot;JavaScript으로 코드를 작성해봐도..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccfUcA/btsKgMcybfK/zDX6WsPQjK9kxkO8OLkv2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccfUcA%2FbtsKgMcybfK%2FzDX6WsPQjK9kxkO8OLkv2k%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;384&quot; height=&quot;267&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JavaScript으로 코드를 작성해봐도..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cp12no/btsKhOAc624/ywZQSCqo5KhhrEyvBWrb8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cp12no/btsKhOAc624/ywZQSCqo5KhhrEyvBWrb8K/img.png&quot; data-alt=&quot;Python으로 코드를 작성해봐도..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cp12no/btsKhOAc624/ywZQSCqo5KhhrEyvBWrb8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcp12no%2FbtsKhOAc624%2FywZQSCqo5KhhrEyvBWrb8K%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;411&quot; height=&quot;266&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Python으로 코드를 작성해봐도..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;그럼&amp;nbsp;왜&amp;nbsp;저는&amp;nbsp;&quot;admin&quot;&amp;nbsp;과&amp;nbsp;&quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;이&amp;nbsp;같다고&amp;nbsp;주장하는&amp;nbsp;것&amp;nbsp;일까요?&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;저의&amp;nbsp;발자취를&amp;nbsp;한번&amp;nbsp;따라가보도록&amp;nbsp;하겠습니다!&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;로그인을 해보자&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;저의&amp;nbsp;네이버&amp;nbsp;아이디는&amp;nbsp;&quot;kangactor123&quot;&amp;nbsp;입니다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;문득 호기심이 들었습니다. 아이디에 공백(White Space)를 포함하면 로그인이 당연히 안되겠지?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpho4A/btsKh9D7Zuz/MJV7vxtKKlmeN8kFK2Shr1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpho4A/btsKh9D7Zuz/MJV7vxtKKlmeN8kFK2Shr1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpho4A/btsKh9D7Zuz/MJV7vxtKKlmeN8kFK2Shr1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bpho4A/btsKh9D7Zuz/MJV7vxtKKlmeN8kFK2Shr1/img.gif&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;592&quot; height=&quot;534&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;웬걸, &quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;kangactor123&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot; 으로 로그인이 되는겁니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;나는 가입할 때 &quot;kangactor123&quot; 이라는 문자열로 가입했는데, 로그인 할 때는 &quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;kangactor123&amp;nbsp;&amp;nbsp;&quot;, &quot;kangactor123&amp;nbsp; &amp;nbsp;&quot;이라는 문자열을 입력해도 서버에서는 같은 아이디로 판단해 로그인을 시켜주는 겁니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&quot;kan&amp;nbsp;gactor123&quot;&amp;nbsp;이라는&amp;nbsp;문자열로는&amp;nbsp;로그인이&amp;nbsp;안되는걸&amp;nbsp;보아&amp;nbsp;서버에서&amp;nbsp;ID를&amp;nbsp;받을&amp;nbsp;때&amp;nbsp;trim&amp;nbsp;메서드를&amp;nbsp;활용해&amp;nbsp;좌우&amp;nbsp;공백을&amp;nbsp;제거하는구나&amp;nbsp;라고&amp;nbsp;유추해볼&amp;nbsp;수&amp;nbsp;있었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;그럼&amp;nbsp;과연&amp;nbsp;네이버만&amp;nbsp;그럴까?&amp;nbsp;하며&amp;nbsp;구글과&amp;nbsp;카카오&amp;nbsp;모두&amp;nbsp;테스트를&amp;nbsp;해봤는데요.&amp;nbsp;모두&amp;nbsp;동일하게&amp;nbsp;개행을&amp;nbsp;제거해주고&amp;nbsp;같은&amp;nbsp;유저&amp;nbsp;정보를&amp;nbsp;내려주는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있었습니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;결국&amp;nbsp;여기서&amp;nbsp;저는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&quot;admin&quot;&amp;nbsp;과&amp;nbsp;&quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;이&amp;nbsp;같다는&amp;nbsp;결론&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;을&amp;nbsp;내렸습니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;데이터&amp;nbsp;가공&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;사용자에게&amp;nbsp;입력받은&amp;nbsp;데이터나,&amp;nbsp;수집한&amp;nbsp;비정형&amp;nbsp;데이터를&amp;nbsp;정형&amp;nbsp;데이터로&amp;nbsp;변경하는&amp;nbsp;과정을&amp;nbsp;데이터&amp;nbsp;전처리&amp;nbsp;과정이라고&amp;nbsp;합니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;일반적으로 로그인 시 ID나 이메일 값을 trim 메서드를 활용해 전처리 과정을 거쳐서 유의미한 데이터로 가공해 서버에서 정형 데이터로 활용하고는 합니다. 이 과정에서 여러가지 이점을 누릴 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;사용자에게 좋은 경험을 선사&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;ID를 입력할 때 실수로 공백을 입력하는 경우가 발생할 수 있습니다. 이때 서버에서 전처리 과정으로 공백을 제거하면 사용자는 실수를 했지만 로그인을 할 수 있어 긍정적인 사용자 경험이 발생합니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;공백을&amp;nbsp;활용해&amp;nbsp;다른&amp;nbsp;계정처럼&amp;nbsp;보이게&amp;nbsp;하려는&amp;nbsp;악의적인&amp;nbsp;유저의&amp;nbsp;행동도&amp;nbsp;막을&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;공백을&amp;nbsp;활용한&amp;nbsp;XSS,&amp;nbsp;SQL&amp;nbsp;Injection&amp;nbsp;공격도&amp;nbsp;방어가&amp;nbsp;가능합니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;프로그래밍, 기획&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;서두에서 저는 엄연하게 두 문자열이 프로그래밍적 관점에서 다르다는 것을 말씀드렸습니다. 그럼에도&amp;nbsp;저는&amp;nbsp;유저,&amp;nbsp;로그인이라는&amp;nbsp;특수한&amp;nbsp;상황에서&amp;nbsp;두&amp;nbsp;문자열이&amp;nbsp;같다고&amp;nbsp;주장하고&amp;nbsp;있습니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;클라이언트(웹)에서&amp;nbsp;유저가&amp;nbsp;의도해서&amp;nbsp;입력한&amp;nbsp;값은&amp;nbsp;&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&amp;nbsp;입니다.&amp;nbsp;하지만&amp;nbsp;서버에서는&amp;nbsp;해당&amp;nbsp;값을&amp;nbsp;&quot;admin&quot;으로&amp;nbsp;판단해&amp;nbsp;같은&amp;nbsp;값을&amp;nbsp;내려주고&amp;nbsp;있죠. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;회원가입&amp;nbsp;당시를&amp;nbsp;생각해봅시다! &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;ID는 대부분의 경우 영소문자, 숫자, 특수기호(&quot;-&quot; 와 &quot;_&quot;)만을 허용하고 있습니다. 공백은 당연히 허락하고 있지 않습니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;클라이언트, 서버에서 이런 유효성 검증 로직을 거치고 생성된 유저의 ID는 당연히 공백을 포함하고 있지 않을 것 입니다. 그래서 유저의 ID는 당연히 공백을 포함하고 있지 않으니 로그인을 할 때 앞 뒤 공백을 포함해도 같은 유저의 정보를 내려주자 라고 기획을 했을것 같습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;데이터 처리의 관점에서도 이점을 취할 수 있습니다. 공백을 제거함으로써 악의적인 사용자의 행동을 사전에 방지할 수 있으니까요.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;결국&amp;nbsp;&quot;admin&quot;과&amp;nbsp;&quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;은&amp;nbsp;같은&amp;nbsp;문자열 일수도 다른 문자열 일수도 있습니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;i&gt;&lt;b&gt;프로그래밍적&amp;nbsp;관점에서&amp;nbsp;사고하는&amp;nbsp;것과,&amp;nbsp;서비스를&amp;nbsp;기획하는&amp;nbsp;측에서&amp;nbsp;사고하는&amp;nbsp;것은&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;결과를&amp;nbsp;도출할&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;것&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; 입니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;s&gt;&lt;i&gt;&lt;b&gt;조금 비틀어진 생각&lt;/b&gt;&lt;/i&gt;&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;저는&amp;nbsp;여기서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;s&gt;&lt;i&gt;&lt;b&gt;조금&amp;nbsp;이상한&amp;nbsp;생각&lt;/b&gt;&lt;/i&gt;&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이&amp;nbsp;들었습니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&quot;회원가입&amp;nbsp;당시에&amp;nbsp;공백을&amp;nbsp;입력받지&amp;nbsp;않고&amp;nbsp;로그인&amp;nbsp;할&amp;nbsp;때&amp;nbsp;ID에는&amp;nbsp;공백이&amp;nbsp;포함되어&amp;nbsp;있지&amp;nbsp;않으니&amp;nbsp;앞&amp;nbsp;뒤&amp;nbsp;공백을&amp;nbsp;잘라서&amp;nbsp;같은&amp;nbsp;유저&amp;nbsp;정보를&amp;nbsp;반환한다.&quot;&amp;nbsp;라면,&amp;nbsp;&quot;adm&amp;nbsp;in&quot;도&amp;nbsp;같은&amp;nbsp;유저&amp;nbsp;정보를&amp;nbsp;반환해야&amp;nbsp;하는&amp;nbsp;것이&amp;nbsp;아닐까&amp;nbsp;라는&amp;nbsp;생각이였습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;위와&amp;nbsp;같은&amp;nbsp;논리라면&amp;nbsp;어쨋든&amp;nbsp;공백을&amp;nbsp;포함하지&amp;nbsp;않으니&amp;nbsp;&quot;adm&amp;nbsp;in&quot;도&amp;nbsp;&quot;&amp;nbsp;&amp;nbsp;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;도&amp;nbsp;&quot;admin&quot;도&amp;nbsp;다&amp;nbsp;같은&amp;nbsp;정보를&amp;nbsp;내뱉어야&amp;nbsp;하지&amp;nbsp;않을까&amp;nbsp;하는&amp;nbsp;비틀어진&amp;nbsp;생각이&amp;nbsp;들었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&quot;adm&amp;nbsp;in&quot;이&amp;nbsp;ID&amp;nbsp;규칙&amp;nbsp;상&amp;nbsp;에러로&amp;nbsp;판단을&amp;nbsp;하고&amp;nbsp;있다면,&amp;nbsp;동일한&amp;nbsp;맥락에서&amp;nbsp;&quot;&amp;nbsp;&amp;nbsp;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;도&amp;nbsp;에러로&amp;nbsp;판단해야&amp;nbsp;하지&amp;nbsp;않을까&amp;nbsp;하는&amp;nbsp;생각이였습니다.&amp;nbsp;어쨋든&amp;nbsp;클라이언트(웹)을&amp;nbsp;사용하는&amp;nbsp;유저가&amp;nbsp;서버에&amp;nbsp;보낸&amp;nbsp;요청(의도)는&amp;nbsp;&quot;adm&amp;nbsp;in&quot;도,&amp;nbsp;&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&amp;nbsp;모두&amp;nbsp;유저가&amp;nbsp;직접&amp;nbsp;의도한&amp;nbsp;행위의&amp;nbsp;결과로&amp;nbsp;요청이&amp;nbsp;된&amp;nbsp;것이라고&amp;nbsp;생각했기&amp;nbsp;때문입니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;현재의&amp;nbsp;사회&amp;nbsp;통념&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;(대부분의&amp;nbsp;플랫폼에서&amp;nbsp;이런&amp;nbsp;기획으로&amp;nbsp;구현이&amp;nbsp;됐으니&amp;nbsp;사회&amp;nbsp;통념이라고&amp;nbsp;하겠습니다.)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;에서는 유저의 편의성을 위해서 앞, 뒤 공백을 허용하고 있고, 데이터를 가공하여 악의적인 사용자의 행위도 방지할 수 있다는 생각으로 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;너무 딥하게 생각하고 있구나&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; 라는 결론을 내렸습니다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;서비스 기획은 당연히 치밀해야 하지만, 너무 딥하게 들어간다면 오히려 서비스와 로직의 복잡도가 높아져 유저에게 좋지 못한 경험을 제공할 수 있기 때문입니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;결국은 기획하기 나름&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;결국은 기획하기 나름이지 않을까 생각합니다. 서비스 기획에는 정답이 없습니다. 모두가 생각하는 기획이 좋은 기획일수도, 소수의 누군가의 독특한 아이디어가 좋은 기획일수도 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;여기서&amp;nbsp;중요한&amp;nbsp;포인트는&amp;nbsp;유저에게&amp;nbsp;얼만큼의&amp;nbsp;만족감을&amp;nbsp;줄&amp;nbsp;수&amp;nbsp;있는지,&amp;nbsp;유저가&amp;nbsp;얼마나&amp;nbsp;좋은&amp;nbsp;경험을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있는지&amp;nbsp;입니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;기획하기&amp;nbsp;나름이니&amp;nbsp;&quot;admin&quot;과&amp;nbsp;&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;도&amp;nbsp;로그인&amp;nbsp;상황에서&amp;nbsp;같은&amp;nbsp;문자열이라는&amp;nbsp;결론을&amp;nbsp;내리도록&amp;nbsp;하겠습니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;어쨋든 유저가 실수로 개행을 입력했다면, 이런 기획은 유저에게 좋은 기억과 경험을 선사해줄 수 있으니깐요. (+보안도 챙기고 말이죠.)&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;조금은&amp;nbsp;어그로성&amp;nbsp;제목에&amp;nbsp;이끌려&amp;nbsp;제&amp;nbsp;글을&amp;nbsp;읽어주셨을&amp;nbsp;것&amp;nbsp;이라고&amp;nbsp;생각합니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;서두에서&amp;nbsp;밝혔듯이,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;s&gt;&lt;i&gt;&lt;b&gt;저는&amp;nbsp;&quot;admin&quot;&amp;nbsp;과&amp;nbsp;&quot;admin&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;은&amp;nbsp;전혀&amp;nbsp;같지&amp;nbsp;않다고&amp;nbsp;생각&lt;/b&gt;&lt;/i&gt;&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;합니다! 그리고&amp;nbsp;글의&amp;nbsp;후미에서도,&amp;nbsp;지금도&amp;nbsp;마찬가지입니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;서비스를 개발하는 개발자여도, 항상 기획된 내용에 의문을 품고 긍정적으로 토론하며 유저에게 더 좋은 경험을 주고자 하는 마음가짐은 좋은 마음가짐인 것 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;읽어봐주셔서&amp;nbsp;감사합니다! &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>이것저것</category>
      <category>IT</category>
      <category>기획</category>
      <category>서비스</category>
      <category>프로그래밍</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/60</guid>
      <comments>https://kangs-develop.tistory.com/60#entry60comment</comments>
      <pubDate>Thu, 24 Oct 2024 09:42:05 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] WebWorker 빌드 시 worker 파일이 포함이 안되는 문제</title>
      <link>https://kangs-develop.tistory.com/59</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 WebWorker, SharedWorker에 대해서 살펴보고 WebSocket을 얹어서 서버와 통신하는 예제를 구현해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드로 추가적인 리팩토링 작업을 하던 중 로컬의 개발 서버에서는 정상 동작 하지만 &lt;b&gt;빌드 시 worker파일을 빌드에 포함하지 못하는 문제&lt;/b&gt;를 확인했습니다. 하여 worker 파일을 빌드하지 않았기 때문에 빌드 결과물을 실행했을 때 worker 스크립트를 찾지 못해서 정상적으로 동작하지 못하는 현상이 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-19 오후 3.49.09.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;1666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7JwNW/btsKbBoYoEE/8U9FhPojkejwKziBAMBO61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7JwNW/btsKbBoYoEE/8U9FhPojkejwKziBAMBO61/img.png&quot; data-alt=&quot;빌드 후 빌드 파일을 실행한 결과입니다. shared-worker.ts를 불러오지 못하고 Pending 상태로 확인됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7JwNW/btsKbBoYoEE/8U9FhPojkejwKziBAMBO61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7JwNW%2FbtsKbBoYoEE%2F8U9FhPojkejwKziBAMBO61%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;2952&quot; height=&quot;1666&quot; data-filename=&quot;스크린샷 2024-10-19 오후 3.49.09.png&quot; data-origin-width=&quot;2952&quot; data-origin-height=&quot;1666&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빌드 후 빌드 파일을 실행한 결과입니다. shared-worker.ts를 불러오지 못하고 Pending 상태로 확인됩니다.&lt;/figcaption&gt;
&lt;/figure&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;빌드 디렉토리인 dist를 확인해도 worker 스크립트는 찾아볼 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-19 오후 4.36.18.png&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTk9av/btsKdFbUGaH/8GEcknAWZ4vtV73kav2lv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTk9av/btsKdFbUGaH/8GEcknAWZ4vtV73kav2lv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTk9av/btsKdFbUGaH/8GEcknAWZ4vtV73kav2lv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTk9av%2FbtsKdFbUGaH%2F8GEcknAWZ4vtV73kav2lv1%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;258&quot; height=&quot;195&quot; data-filename=&quot;스크린샷 2024-10-19 오후 4.36.18.png&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;해결 과정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 도출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 번들링 과정에서 왜 제외가 된 것일까 곰곰히 생각을 해봤습니다. Vite는 번들러로 rollUp을 사용합니다. &lt;b&gt;rollUp 번들러는 번들링 중 용량을 최대한 압축하기 때문에 사용하지 않는 파일을 번들링에 포함하지 않습니다.&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;기존에 WebWorker을 사용하던 방식을 살펴봅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729320849743&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function useWebWorker&amp;lt;T&amp;gt;({ url, initialData }: Props&amp;lt;T&amp;gt;): Result&amp;lt;T&amp;gt; {
  const worker = useRef&amp;lt;Worker | null&amp;gt;(null);
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState&amp;lt;T | null&amp;gt;(initialData ?? null);

  const sendMessage = useCallback((message?: string) =&amp;gt; {
    if (worker.current) {
      setLoading(true);
      worker.current.postMessage(message);
    }
  }, []);

  useEffect(() =&amp;gt; {
    worker.current = new Worker(new URL(url, import.meta.url));
    
 ...생략&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;소스 경로 상 WebWorker의 위치(string)를 url 이라는 인자로 받아서 워커 생성자에 들어가는 URL로 넣어주고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 WebWorker 객체를 런타임에 생성하는 방식으로 개발 서버에서는 런타임에 해당 url의 WebWorker 스크립트가 존재하여 참조가 가능하지만, &lt;b&gt;빌드 시점에는 해당 스크립트를 참조하고 있지 않기 때문에 빌드에서 제외&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 다른 방식으로 WebWorker을 사용하는 것으로 수정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방안 도출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드에 스크립트를 사용하기 위해서 스크립트를 직접 import 하기로 결정했습니다. Vite에서는 WebWorker을 import 하면 해당 스크립트의 WebWorker의 생성자를 리턴해줍니다. 일반 WebWorker는 경로 뒤에 ?worker를, SharedWorker은 경로 뒤에 ?sharedworker을 붙여주면 생성자를 import 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729321607461&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Worker from &quot;./socket-worker?worker&quot;;

import SharedWorker from &quot;./socket-worker?sharedworker&quot;;&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;해당 import 참조를 확인해보면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729321977397&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Vite의 client.d.ts 파일입니다.

// web worker
declare module '*?worker' {
  const workerConstructor: {
    new (options?: { name?: string }): Worker
  }
  export default workerConstructor
}

declare module '*?sharedworker' {
  const sharedWorkerConstructor: {
    new (options?: { name?: string }): SharedWorker
  }
  export default sharedWorkerConstructor
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://ko.vite.dev/guide/features.html#web-workers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web Workers&lt;/a&gt;&lt;/b&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;이전에 작성했던 useWebWorker 훅에 대해서 다시 한번 생각했습니다. 해당 훅은 여러 WebWorker 스크립트를 공통으로 사용할 수 있도록 디자인 된 커스텀 훅 입니다.&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;WebWorker&amp;nbsp;스크립트는 실제 비즈니스 로직을 담고있는 스크립트 파일 입니다. &lt;b&gt;각각의 WebWorker마다 사용하는 이유, 비즈니스 로직이 다 다를 뿐더러 과연 WebWorker를 사용하기 위해 추상화 한 공용 커스텀 훅이 어울리는 방식일까 라는 고민&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;결국 WebWorker와 커스텀 훅을 1:1 방식으로 사용하는 것으로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;커스텀 훅 수정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 훅을 수정합니다. 기존에는 WebWorker를 공통으로 사용하기 위해 WebWorker에서 응답받는 메세지의 타입을 결정하기 위해 제네릭 타입을 사용했습니다. 하지만 수정하는 방식에서는 1:1로 WebWorker 스크립트에 대응하기 때문에 코드 작성 시점에 이미 타입을 알고 있습니다. 그렇기 때문에 제네릭 타입을 걷어내고 실제 사용하는 타입인 SocketData를 넣어주도록 합니다.&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;그 다음 WebWorker 스크립트를 사용하기 위해 WebWorker 파일을 import 합니다. 리턴받은 Worker 생성자를 통해 Worker 객체를 생성하고 ref에 바인딩 합니다.&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;pre id=&quot;code_1729321877541&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback, useEffect, useRef, useState } from &quot;react&quot;;
import { SocketData } from &quot;../../types/socket-data&quot;;

import SharedWorker from &quot;../../workers/shared-worker?sharedworker&quot;;

type Props = {
  initialData?: SocketData;
};

type Result = {
  message: SocketData | null;
  loading: boolean;
  sendMessage: (message?: string) =&amp;gt; void;
};

function useSharedWorker({ initialData }: Props): Result {
  const worker = useRef&amp;lt;SharedWorker | null&amp;gt;(null);
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState&amp;lt;SocketData | null&amp;gt;(
    initialData ?? null
  );

  const sendMessage = useCallback((message?: string) =&amp;gt; {
    if (worker.current) {
      setLoading(true);
      worker.current.port.postMessage(message);
    }
  }, []);

  useEffect(() =&amp;gt; {
    worker.current = new SharedWorker();

    worker.current.port.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
      let messageData = null;
      const { data } = event;

      try {
        messageData = JSON.parse(data);
      } catch {
        messageData = data;
      }

      setLoading(false);
      setMessage(messageData);
    };

    if (worker.current) {
      worker.current.port.start();
    }

    return () =&amp;gt; {
      if (worker.current) {
        worker.current.port.close();
      }
    };
  }, []);

  return {
    message,
    loading,
    sendMessage,
  };
}

export default useSharedWorker;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;검증&lt;/b&gt;&lt;/h4&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;스크린샷 2024-10-19 오후 4.24.13.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmbN3t/btsKcdOebkF/W3RviFaBfVPWuPEa3qkJG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmbN3t/btsKcdOebkF/W3RviFaBfVPWuPEa3qkJG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmbN3t/btsKcdOebkF/W3RviFaBfVPWuPEa3qkJG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmbN3t%2FbtsKcdOebkF%2FW3RviFaBfVPWuPEa3qkJG1%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;250&quot; height=&quot;177&quot; data-filename=&quot;스크린샷 2024-10-19 오후 4.24.13.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&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;빌드 결과물에 worker&amp;nbsp;스크립트가 잘 반영된 것을 확인할 수 있습니다! 이제 빌드 결과물을 실행해보도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-19 오후 4.23.02.png&quot; data-origin-width=&quot;1509&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vmSz7/btsKcNWduJG/jcHZpMxvzWf7CwJVNgcQt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vmSz7/btsKcNWduJG/jcHZpMxvzWf7CwJVNgcQt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vmSz7/btsKcNWduJG/jcHZpMxvzWf7CwJVNgcQt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvmSz7%2FbtsKcNWduJG%2FjcHZpMxvzWf7CwJVNgcQt1%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;1509&quot; height=&quot;496&quot; data-filename=&quot;스크린샷 2024-10-19 오후 4.23.02.png&quot; data-origin-width=&quot;1509&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&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;Vite의 빌드 결과물이 실행되는 포트는 4173번 입니다. 위 결과물을 확인해보면 worker 파일을 잘 불러와 실제 서버와 잘 통신하는 것을 확인할 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Build.gif&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vnxnc/btsKbB3BrXI/pAnt9IhkaKXzvMaf8YupcK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vnxnc/btsKbB3BrXI/pAnt9IhkaKXzvMaf8YupcK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vnxnc/btsKbB3BrXI/pAnt9IhkaKXzvMaf8YupcK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/vnxnc/btsKbB3BrXI/pAnt9IhkaKXzvMaf8YupcK/img.gif&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;1914&quot; height=&quot;1040&quot; data-filename=&quot;Build.gif&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 예제를 작성할 때 빌드를 고려하는 것을 깜빡했는데, 금일 빌드 과정에서 문제점이 있다는 사실을 확인하고 식겁했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곰곰히 원인을 생각해보고 문제를 해결하는데 2~3시간을 보냈는데 고민하는 시간이 정말 행복한 시간이였습니다.&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;현재 개발하는 솔루션에서 엑셀 파일을 프론트엔드 단에서 만들어 다운로드 해주고 있는데 해당 파싱 과정에서 WebWorker를 이용해 UX와 다운로드 단계 까지의 속도를 개선할 수 있지 않을까 라는 생각도 하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음번에 기회가 된다면 실제로 프로젝트에 WebWorker를 적용해보고 적용하며 겪은 과정을 남겨보도록 하겠습니다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/kangactor123/web-worker-monorepo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;저장소 바로가기&lt;/a&gt;&lt;/b&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;읽어봐주셔서 감사합니다!&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>Web</category>
      <category>Webworker</category>
      <category>웹워커</category>
      <category>트러블슈팅</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/59</guid>
      <comments>https://kangs-develop.tistory.com/59#entry59comment</comments>
      <pubDate>Sat, 19 Oct 2024 16:40:37 +0900</pubDate>
    </item>
    <item>
      <title>SharedWorker에 WebSocket 얹어보기</title>
      <link>https://kangs-develop.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 글에서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Toss Slash24 세션 중 N개의 탭, 단 하나의 소켓&lt;/b&gt;&lt;/span&gt;이라는 세션을 공유하며 Web Worker에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/57&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web Worker 알아보기&lt;/a&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=SVt1-Opp3Wo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;N개의 탭, 단 하나의 웹소켓: SharedWorker&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이번 글에서는 Slash24 세션에서 소개한 SharedWorker을 살펴보고 세션에서 공유해주신 솔루션(SharedWorker에 WebSocket을 얹는)을 예제와 함께 알아보도록 하겠습니다.&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;&lt;b&gt;Socket Server&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 SharedWorker을 살펴보기에 앞서 서버 코드를 공유합니다.&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;Interval을 통해서 8초마다 한번 씩 클라이언트에 메세지를 전송하고, 클라이언트에서 메세지를 받으면 그대로 클라이언트에 재전송 하고 있는 예제입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729208704783&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const nameList = [
  &quot;테슬라&quot;,
  &quot;알파벳&quot;,
  &quot;삼성전자&quot;,
  &quot;LG화학&quot;,
  &quot;엔비디아&quot;,
  &quot;네이버&quot;,
  &quot;카카오&quot;,
  &quot;토스&quot;,
];

const WebSocket = require(&quot;ws&quot;);
const wss = new WebSocket.Server({ port: 8080 });

wss.on(&quot;connection&quot;, (ws) =&amp;gt; {
  console.log(&quot;Client connected. Connected Clients: &quot;, wss.clients.size);

  const intervalHandler = () =&amp;gt; {
    const randomIndex = Math.floor(Math.random() * 7);
    const alarm = {
      id: Date.now(),
      name: nameList[randomIndex],
      price: Math.floor(Math.random() * 100000),
      updatedAt: new Date().toDateString(),
    };
    console.log(&quot;ws send&quot;, JSON.stringify({ type: &quot;DATA&quot;, data: alarm }));

    ws.send(
      JSON.stringify({
        type: &quot;DATA&quot;,
        data: alarm,
      })
    );
  };
  const intervalId = setInterval(intervalHandler, 8000);

  ws.on(&quot;message&quot;, (message) =&amp;gt; {
    console.log(`Received: ${message}`);
    let serverMessage = &quot;&quot;;

    if (message === &quot;PING&quot;) {
      serverMessage = JSON.stringify({
        type: &quot;MESSAGE&quot;,
        data: &quot;PONG&quot;,
      });
    } else {
      serverMessage = JSON.stringify({
        type: &quot;MESSAGE&quot;,
        data: `Server: Received your message - ${message}`,
      });
    }

    ws.send(serverMessage);
  });

  // 연결이 닫혔을 때
  ws.on(&quot;close&quot;, () =&amp;gt; {
    console.log(&quot;Client disconnected&quot;);
    clearInterval(intervalId);
  });
});

console.log(&quot;WebSocket server running on ws://localhost:8080&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebWorker&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 글에서는 WebWorker에는 공유하지 않는 별도의 백그라운드 스레드를 가지는 DedicatedWorker과 같은 도메인의 worker 스크립트에 한해서 백그라운드 스레드를 공유하는 SharedWorker가 있다고 했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SharedWorker&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedWorker란 Message Port를 통해서 메인 스레드와 통신하는 백그라운드 스레드 입니다. &lt;br /&gt;SharedWorker에서 메세지 포트는 브라우저 여러 탭이 존재할 수 있으므로 배열을 통해서 관리를 해줄 수 있습니다. &lt;br /&gt;탭&amp;nbsp;간&amp;nbsp;스레드를&amp;nbsp;공유하기&amp;nbsp;때문에&amp;nbsp;같은&amp;nbsp;세션인&amp;nbsp;브라우저에서&amp;nbsp;동일한&amp;nbsp;데이터를&amp;nbsp;바라보아야&amp;nbsp;하는&amp;nbsp;상황에서&amp;nbsp;유용하게&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;주의해야&amp;nbsp;할&amp;nbsp;점은,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;SharedWorker을 지원하지 않는 브라우저와 버전이 존재하는 것&lt;/b&gt;&lt;/span&gt;&amp;nbsp;입니다.&amp;nbsp;그렇기&amp;nbsp;때문에&amp;nbsp;기능을&amp;nbsp;구현하기&amp;nbsp;전&amp;nbsp;반드시&amp;nbsp;지원&amp;nbsp;대상을&amp;nbsp;면밀하게&amp;nbsp;검토하시고&amp;nbsp;해당&amp;nbsp;API를&amp;nbsp;사용하시면&amp;nbsp;좋을&amp;nbsp;것&amp;nbsp;같습니다. &lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DedicatedWorker with WebSocket&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SharedWorker을 구현하기 앞서 먼저 DedicatedWorker에 WebSocket을 얹어보도록 하겠습니다. WebWorker을 사용하기 위한 코드는 이전에 공유드린 코드의 내용과 같습니다. 대신 Worker 위에 WebSocket을 얹기 때문에 워커의 스크립트를 새로 작성합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Worker&amp;nbsp;스크립트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp;socket&amp;nbsp;객체를&amp;nbsp;생성합니다.&lt;br /&gt;이전&amp;nbsp;스크립트와&amp;nbsp;동일하게&amp;nbsp;워커가&amp;nbsp;메세지를&amp;nbsp;받았을&amp;nbsp;때&amp;nbsp;이벤트&amp;nbsp;핸들링&amp;nbsp;처리를&amp;nbsp;합니다.&amp;nbsp;message를&amp;nbsp;전달&amp;nbsp;받았을&amp;nbsp;때&amp;nbsp;그대로&amp;nbsp;열린&amp;nbsp;소켓에&amp;nbsp;메세지를&amp;nbsp;전달합니다.&lt;br /&gt;하단에&amp;nbsp;socket에서&amp;nbsp;메세지를&amp;nbsp;전달받았을&amp;nbsp;경우&amp;nbsp;핸들러&amp;nbsp;함수를&amp;nbsp;작성합니다.&amp;nbsp;웹&amp;nbsp;소켓&amp;nbsp;서버에서&amp;nbsp;받은&amp;nbsp;메세지를&amp;nbsp;그대로&amp;nbsp;클라이언트에&amp;nbsp;전달할&amp;nbsp;예정입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const socket: WebSocket = new WebSocket(&quot;ws://localhost:8080&quot;);

self.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const { data: message } = event;

&amp;nbsp;&amp;nbsp;if (message === &quot;CLOSE_SOCKET&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket.close();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (socket.readyState === socket.CONNECTING) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;postMessage(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;JSON.stringify({ type: &quot;MESSAGE&quot;, data: &quot;Socket is connecting...&quot; })
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (socket) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket.send(message);
&amp;nbsp;&amp;nbsp;}
};

socket.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const { data: serverMessage } = event;
&amp;nbsp;&amp;nbsp;console.log(&quot;worker from socket: &quot;, serverMessage);

&amp;nbsp;&amp;nbsp;postMessage(serverMessage);
};

socket.onopen = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;socket.send(&quot;PING&quot;);
};

socket.onerror = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;postMessage(JSON.stringify({ type: &quot;MESSAGE&quot;, data: &quot;소켓 에러입니다.&quot; }));
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;화면단 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면단에서는&amp;nbsp;알림을&amp;nbsp;받아서&amp;nbsp;데이터를&amp;nbsp;화면에&amp;nbsp;노출하고&amp;nbsp;있습니다.&amp;nbsp;주의해야&amp;nbsp;할&amp;nbsp;점은&amp;nbsp;메세지&amp;nbsp;기반으로&amp;nbsp;데이터를&amp;nbsp;주고&amp;nbsp;받기&amp;nbsp;때문에&amp;nbsp;객체&amp;nbsp;형태의&amp;nbsp;문자열은&amp;nbsp;자바스크립트&amp;nbsp;객체로&amp;nbsp;파싱을&amp;nbsp;해야&amp;nbsp;합니다. &lt;br /&gt;알림&amp;nbsp;객체를&amp;nbsp;전달받아서&amp;nbsp;state에&amp;nbsp;배열로&amp;nbsp;담습니다.&amp;nbsp;그리고&amp;nbsp;화면에&amp;nbsp;알림&amp;nbsp;리스트를&amp;nbsp;랜더링&amp;nbsp;합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useCallback, useEffect, useMemo, useState } from &quot;react&quot;;
import useWebWorker from &quot;../hooks/useWebWorker&quot;;

import AlarmComponent from &quot;./Alarm&quot;;

import styles from &quot;./worker.module.css&quot;;

import { Alarm } from &quot;../types/alarm&quot;;
import { SocketData } from &quot;../types/socket-data&quot;;

import { isEmpty } from &quot;../utils/util&quot;;

const socketWorkerPath = &quot;../workers/socket-worker.ts&quot;;

const WorkerExample = () =&amp;gt; {
  const [alarmList, setAlarmList] = useState&amp;lt;Alarm[]&amp;gt;([]);
  const { message, loading, sendMessage } = useWebWorker&amp;lt;SocketData&amp;gt;({
    url: socketWorkerPath,
  });

  const serverMessage: string | null = useMemo(() =&amp;gt; {
    let serverMsg = null;

    if (message?.type === &quot;MESSAGE&quot;) {
      serverMsg = (message?.data as string) ?? null;
    }

    return serverMsg;
  }, [message]);

  const handleClick = useCallback(() =&amp;gt; {
    sendMessage(`알림 요청: ${Date.now()}`);
  }, [sendMessage]);

  useEffect(() =&amp;gt; {
    if (message?.type === &quot;DATA&quot;) {
      const alarm = message.data as Alarm;
      setAlarmList((prev) =&amp;gt; [...prev, alarm]);
    }
  }, [message]);

  return (
    &amp;lt;div className={styles.container}&amp;gt;
      &amp;lt;h1 className={styles.title}&amp;gt;Dedicated Worker&amp;lt;/h1&amp;gt;
      &amp;lt;div className={styles.serverMessageWrapper}&amp;gt;
        &amp;lt;h3&amp;gt;서버메세지&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;{serverMessage ?? &quot;서버에서 보낸 메세지가 존재하지 않습니다.&quot;}&amp;lt;/p&amp;gt;
        &amp;lt;button onClick={handleClick}&amp;gt;알람 보내기&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div className={styles.alarmWrapper}&amp;gt;
        {loading ? (
          &amp;lt;p&amp;gt;로딩중..&amp;lt;/p&amp;gt;
        ) : isEmpty(alarmList) ? (
          &amp;lt;p&amp;gt;알람이 존재하지 않습니다.&amp;lt;/p&amp;gt;
        ) : (
          alarmList.map((alarm) =&amp;gt; &amp;lt;AlarmComponent key={alarm.id} {...alarm} /&amp;gt;)
        )}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default WorkerExample;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;구현한 내용을 도식화하면 다음과 같은 도면이 나옵니다. 탭마다의 WebWorker를 가지고 있고 각 워커마다 소켓에 연결해 여러 커넥션이 맺어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvI1TL/btsJ9bpNSjv/kZvX5p7Ji4HPsxYH9II44k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvI1TL/btsJ9bpNSjv/kZvX5p7Ji4HPsxYH9II44k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvI1TL/btsJ9bpNSjv/kZvX5p7Ji4HPsxYH9II44k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvI1TL%2FbtsJ9bpNSjv%2FkZvX5p7Ji4HPsxYH9II44k%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;875&quot; height=&quot;346&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 커넥션이 다르기 때문에 소켓에서 전달받는 데이터도 다른 데이터를 받게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;해당 예제에서는 세션을 고려하지 않았습니다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;실제 서버에서 맺어진 커넥션의 갯수를 살펴봅시다!&lt;br /&gt;&amp;nbsp;&lt;br /&gt;커넥션이 연결되면 서버에서 연결된 커넥션의 갯수를 콘솔에 로깅하도록 구현했습니다. 브라우저의&amp;nbsp;탭이&amp;nbsp;늘어날수록&amp;nbsp;커넥션의&amp;nbsp;갯수도&amp;nbsp;늘어나는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;dedicated.gif&quot; data-origin-width=&quot;1912&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tnYNc/btsKbS3Bzdc/Kkq1B5rwl6Sucr7dznSdg1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tnYNc/btsKbS3Bzdc/Kkq1B5rwl6Sucr7dznSdg1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tnYNc/btsKbS3Bzdc/Kkq1B5rwl6Sucr7dznSdg1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/tnYNc/btsKbS3Bzdc/Kkq1B5rwl6Sucr7dznSdg1/img.gif&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;1912&quot; height=&quot;1024&quot; data-filename=&quot;dedicated.gif&quot; data-origin-width=&quot;1912&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SharedWorker with WebSocket&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면&amp;nbsp;이번엔&amp;nbsp;SharedWorker을&amp;nbsp;구현하고&amp;nbsp;WebSocket을&amp;nbsp;얹어보도록&amp;nbsp;하겠습니다. &lt;br /&gt;클라이언트에서&amp;nbsp;SharedWorker을&amp;nbsp;사용하기&amp;nbsp;위한&amp;nbsp;로직은&amp;nbsp;Worker을&amp;nbsp;위한&amp;nbsp;로직과&amp;nbsp;동일합니다.&amp;nbsp;하지만&amp;nbsp;SharedWorker이라는&amp;nbsp;클래스를&amp;nbsp;사용해&amp;nbsp;인스턴스를&amp;nbsp;만들고,&amp;nbsp;해당&amp;nbsp;객체의&amp;nbsp;port&amp;nbsp;프로퍼티를&amp;nbsp;통해&amp;nbsp;메서드를&amp;nbsp;사용해야&amp;nbsp;하는&amp;nbsp;점이&amp;nbsp;다릅니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;useSharedWorker&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useCallback, useEffect, useRef, useState } from &quot;react&quot;;

type Props&amp;lt;T&amp;gt; = {
&amp;nbsp;&amp;nbsp;url: string;
&amp;nbsp;&amp;nbsp;initialData?: T;
};

type Result&amp;lt;T&amp;gt; = {
&amp;nbsp;&amp;nbsp;message: T | null;
&amp;nbsp;&amp;nbsp;loading: boolean;
&amp;nbsp;&amp;nbsp;sendMessage: (message?: string) =&amp;gt; void;
};

function useSharedWorker&amp;lt;T&amp;gt;({ url, initialData }: Props&amp;lt;T&amp;gt;): Result&amp;lt;T&amp;gt; {
&amp;nbsp;&amp;nbsp;const worker = useRef&amp;lt;SharedWorker | null&amp;gt;(null);
&amp;nbsp;&amp;nbsp;const [loading, setLoading] = useState(false);
&amp;nbsp;&amp;nbsp;const [message, setMessage] = useState&amp;lt;T | null&amp;gt;(initialData ?? null);

&amp;nbsp;&amp;nbsp;const sendMessage = useCallback((message?: string) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (worker.current) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setLoading(true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.port.postMessage(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}, []);

&amp;nbsp;&amp;nbsp;useEffect(() =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current = new SharedWorker(new URL(url, import.meta.url));

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.port.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let messageData = null;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const { data } = event;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;messageData = JSON.parse(data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (error) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;messageData = data;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setLoading(false);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setMessage(messageData);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (worker.current) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.port.start();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (worker.current) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.port.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}, [url]);

&amp;nbsp;&amp;nbsp;return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;loading,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendMessage,
&amp;nbsp;&amp;nbsp;};
}

export default useSharedWorker;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;만약&amp;nbsp;SharedWorker&amp;nbsp;클래스를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;없다면&amp;nbsp;TypeScript를&amp;nbsp;사용해&amp;nbsp;SharedWorker을&amp;nbsp;사용하기&amp;nbsp;위해서는&amp;nbsp;tsconfig의&amp;nbsp;옵션을&amp;nbsp;수정할&amp;nbsp;필요가&amp;nbsp;있습니다.&amp;nbsp; &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;i&gt;&lt;b&gt;1. tsconfig 파일의 lib에 ESNext를 추가합니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;&quot;lib&quot;: [&quot;ESNext&quot;, ...],&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;i&gt;&lt;b&gt;2. TypeScript를&amp;nbsp;사용해&amp;nbsp;SharedWorker을&amp;nbsp;구현하기&amp;nbsp;위해서는&amp;nbsp;타입&amp;nbsp;라이브러리를&amp;nbsp;추가해야&amp;nbsp;합니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;pnpm add @types/sharedworker -D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 과정을 통해 컴파일 에러가 발생하지 않는다면 SharedWorker을 구현할 준비가 끝났습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; SharedWorker 스크립트 &lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;let connections: WeakRef&amp;lt;MessagePort&amp;gt;[] = [];
const socket: WebSocket = new WebSocket(&quot;ws://localhost:8080&quot;);

self.onconnect = (event) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const port = event.ports[0];
&amp;nbsp;&amp;nbsp;const weakPort = new WeakRef&amp;lt;MessagePort&amp;gt;(port);

&amp;nbsp;&amp;nbsp;connections.push(weakPort);

&amp;nbsp;&amp;nbsp;if (!weakPort.deref()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;port.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const { data: message } = event;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (message === &quot;CLOSE_SOCKET&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket.close();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (socket.readyState === socket.CONNECTING) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;postMessage(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;JSON.stringify({ type: &quot;MESSAGE&quot;, data: &quot;Socket is connecting...&quot; })
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (socket) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket.send(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;};
};

socket.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const { data: serverMessage } = event;
&amp;nbsp;&amp;nbsp;console.log(&quot;worker from socket: &quot;, serverMessage);

&amp;nbsp;&amp;nbsp;connections.forEach((connection) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const port = connection.deref();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (port) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port.postMessage(serverMessage);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;});
};

socket.onopen = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;socket.send(&quot;PING&quot;);
};

socket.onerror = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;postMessage(JSON.stringify({ type: &quot;MESSAGE&quot;, data: &quot;소켓 에러입니다.&quot; }));
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;SharedWorker은&amp;nbsp;메세지&amp;nbsp;포트를&amp;nbsp;통해&amp;nbsp;메인&amp;nbsp;스레드와&amp;nbsp;통신합니다.&amp;nbsp;그렇기&amp;nbsp;때문에&amp;nbsp;connect를&amp;nbsp;맺고나서&amp;nbsp;연결된&amp;nbsp;포트를&amp;nbsp;커넥션&amp;nbsp;배열에&amp;nbsp;추가해야&amp;nbsp;합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;발표된&amp;nbsp;세션에서는&amp;nbsp;연결된&amp;nbsp;커넥션에&amp;nbsp;의한&amp;nbsp;메모리&amp;nbsp;누수를&amp;nbsp;방지하기&amp;nbsp;위해&amp;nbsp;포트&amp;nbsp;객체를&amp;nbsp;WeafRef로&amp;nbsp;감싸&amp;nbsp;커넥션이&amp;nbsp;참조되지&amp;nbsp;않을&amp;nbsp;경우,&amp;nbsp;얕은&amp;nbsp;참조로&amp;nbsp;가비지&amp;nbsp;컬렉트&amp;nbsp;되도록&amp;nbsp;구현했습니다.&amp;nbsp;해당&amp;nbsp;예제는&amp;nbsp;세션의&amp;nbsp;예제를&amp;nbsp;코드로&amp;nbsp;구현해보는&amp;nbsp;과정이기에&amp;nbsp;동일하게&amp;nbsp;WeafRef로&amp;nbsp;포트를&amp;nbsp;감싸&amp;nbsp;메세지&amp;nbsp;포트를&amp;nbsp;관리하도록&amp;nbsp;합니다. &lt;br /&gt;&amp;nbsp;&lt;br /&gt;그&amp;nbsp;외의&amp;nbsp;로직은&amp;nbsp;DedicatedWorker와&amp;nbsp;동일합니다.&amp;nbsp;한&amp;nbsp;가지&amp;nbsp;다른&amp;nbsp;점은&amp;nbsp;&lt;b&gt;socket으로부터&amp;nbsp;메세지를&amp;nbsp;받았을&amp;nbsp;경우&amp;nbsp;연결된&amp;nbsp;포트&amp;nbsp;전체에&amp;nbsp;메세지를&amp;nbsp;전송해야&amp;nbsp;하는&amp;nbsp;점&lt;/b&gt;&amp;nbsp;입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;socket.onmessage = (event: MessageEvent&amp;lt;string&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const { data: serverMessage } = event;
&amp;nbsp;&amp;nbsp;console.log(&quot;worker from socket: &quot;, serverMessage);

&amp;nbsp;&amp;nbsp;connections.forEach((connection) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const port = connection.deref();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (port) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port.postMessage(serverMessage);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;});
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위 과정을 도식화 하면 아래와 같은 도면이 나옵니다. SharedWorker는&amp;nbsp;동일한&amp;nbsp;도메인,&amp;nbsp;워커일&amp;nbsp;경우&amp;nbsp;스레드를&amp;nbsp;공유한다고&amp;nbsp;했습니다.&amp;nbsp;그렇기&amp;nbsp;때문에&amp;nbsp;동일한&amp;nbsp;워커에서&amp;nbsp;하나의&amp;nbsp;커넥션으로&amp;nbsp;웹소켓&amp;nbsp;서버와&amp;nbsp;커넥션을&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;918&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBPK4b/btsJ9r6ZEan/KA8cDEvV1VHj1mKXM92DkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBPK4b/btsJ9r6ZEan/KA8cDEvV1VHj1mKXM92DkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBPK4b/btsJ9r6ZEan/KA8cDEvV1VHj1mKXM92DkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBPK4b%2FbtsJ9r6ZEan%2FKA8cDEvV1VHj1mKXM92DkK%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;918&quot; height=&quot;371&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을&amp;nbsp;한번&amp;nbsp;고려해&amp;nbsp;시뮬레이션&amp;nbsp;해보도록&amp;nbsp;하겠습니다.&amp;nbsp;만약&amp;nbsp;동일한&amp;nbsp;유저가&amp;nbsp;여러&amp;nbsp;탭을&amp;nbsp;띄어놓고&amp;nbsp;서버와&amp;nbsp;커넥션을&amp;nbsp;맺을&amp;nbsp;경우&amp;nbsp;DedicatedWorker을&amp;nbsp;사용한다면&amp;nbsp;여러개의&amp;nbsp;커넥션을&amp;nbsp;맺게됩니다.&amp;nbsp;반면에&amp;nbsp;SharedWorker을&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;하나의&amp;nbsp;커넥션을&amp;nbsp;사용해&amp;nbsp;프론트엔드&amp;nbsp;단에서&amp;nbsp;서버&amp;nbsp;리소스의&amp;nbsp;낭비를&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제로&amp;nbsp;서버&amp;nbsp;로그를&amp;nbsp;살펴보도록&amp;nbsp;합시다.&amp;nbsp;여러&amp;nbsp;브라우저&amp;nbsp;탭이&amp;nbsp;열리지만&amp;nbsp;서버와&amp;nbsp;연결된&amp;nbsp;커넥션은&amp;nbsp;단&amp;nbsp;한&amp;nbsp;개라는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;shared.gif&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lj9vk/btsKbkfx80y/gkksb14VbthYkFT661AKCK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lj9vk/btsKbkfx80y/gkksb14VbthYkFT661AKCK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lj9vk/btsKbkfx80y/gkksb14VbthYkFT661AKCK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lj9vk/btsKbkfx80y/gkksb14VbthYkFT661AKCK/img.gif&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;1918&quot; height=&quot;1022&quot; data-filename=&quot;shared.gif&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;추가 내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제를 작성하고 리팩토링 하던 중 worker 스크립트가 빌드에 포함이 안되는 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제를 발견하고 트러블슈팅 하는 과정은 아래 포스팅을 참고해주시면 감사하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/59&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[트러블슈팅] WebWorker 빌드 시 worker 파일이 포함이 안되는 문제&lt;/a&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리하며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slash24&amp;nbsp;세션의&amp;nbsp;예제를&amp;nbsp;코드로&amp;nbsp;구현해보며&amp;nbsp;SharedWorker에&amp;nbsp;대해서&amp;nbsp;살펴보고&amp;nbsp;WebSocket을&amp;nbsp;얹어보는&amp;nbsp;시간을&amp;nbsp;가졌습니다. &lt;br /&gt;위&amp;nbsp;과정을&amp;nbsp;실제&amp;nbsp;프로젝트에서&amp;nbsp;구현한다면&amp;nbsp;더욱&amp;nbsp;복잡한&amp;nbsp;코드를&amp;nbsp;가지겠지만,&amp;nbsp;간단하게&amp;nbsp;구현해보는&amp;nbsp;과정에서&amp;nbsp;프론트엔드&amp;nbsp;단에서&amp;nbsp;서버&amp;nbsp;리소스&amp;nbsp;낭비를&amp;nbsp;방지할&amp;nbsp;수&amp;nbsp;있구나&amp;nbsp;라는&amp;nbsp;것을&amp;nbsp;느꼈습니다.&lt;br /&gt;&lt;br /&gt;또한&amp;nbsp;JavaScript는&amp;nbsp;가비지&amp;nbsp;컬렉션에&amp;nbsp;의해서&amp;nbsp;메모리&amp;nbsp;누수를&amp;nbsp;자동으로&amp;nbsp;방지한다고&amp;nbsp;생각을&amp;nbsp;했었는데,&amp;nbsp;WeafRef를&amp;nbsp;통해서&amp;nbsp;한번&amp;nbsp;더&amp;nbsp;세심하게&amp;nbsp;애플리케이션의&amp;nbsp;메모리를&amp;nbsp;관리하는&amp;nbsp;방법을&amp;nbsp;보고&amp;nbsp;많은&amp;nbsp;생각이&amp;nbsp;들었습니다.&lt;br /&gt;&lt;br /&gt;앞으로더 IT 세션에 많은 관심을 가지고 추후에 또 좋은 세션의 경우 코드로 직접 구현해보며 발표 내용을 더욱 깊게 이해해보도록 하겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;구현하신 코드가 궁금하시다면 아래의 저장소를 살펴봐주세요!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://github.com/kangactor123/web-worker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;저장소 바로가기&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;읽어봐주셔서&amp;nbsp;감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>sharedworker</category>
      <category>Web</category>
      <category>Webworker</category>
      <category>프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/58</guid>
      <comments>https://kangs-develop.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 17 Oct 2024 17:39:23 +0900</pubDate>
    </item>
    <item>
      <title>Web Worker 알아보기</title>
      <link>https://kangs-develop.tistory.com/57</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 자기 전 유튜브로 Toss Slash24의 &lt;b&gt;N개의 탭, 단 하나의 웹소켓: SharedWorker &lt;/b&gt;세션을 들었습니다. Web Worker API는 이전에 한번 흝어본 기억이 있어서 알고 있었지만 해당 API를 실제로 사용해본 경험도 없었고 어떤 상황에서 사용하면 좋을지에 대해서 깊게 고민해보지는 못했습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;토스 증권에서 WTS를 출시하면서 유저 한명이 여러개의 탭을 띄우고 웹소켓을 통해서 서버와 실시간 데이터 통신을 하는데, 기존의 구조로는 각 탭마다 Web Socket을 가지고 있어서 한 명의 유저가 서버에 여러개의 웹 소켓을 연결해 서버 리소스를 낭비하고 서버에 부하를 줄 수 있는 상황이였습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;토스증권의 프론트엔드 박건영 개발자님은 이 상황을 서버측에서 해결할 수 있지만 프론트엔드에서 해결하기 위해 방법을 강구하던 중 Web Worker에서 탭끼리 Worker를 공유할 수 있는 Shared Worker을 사용해 웹소켓을 여러 탭에서 공유하여 유저당 하나의 웹 소켓을 매칭할 수 있도록 문제를 해결했습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;shared.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/60kcb/btsKgQd4iAr/aTiVxKXtl58ebYCKK0GIJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/60kcb/btsKgQd4iAr/aTiVxKXtl58ebYCKK0GIJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/60kcb/btsKgQd4iAr/aTiVxKXtl58ebYCKK0GIJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F60kcb%2FbtsKgQd4iAr%2FaTiVxKXtl58ebYCKK0GIJk%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;918&quot; height=&quot;371&quot; data-filename=&quot;shared.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;탭끼리 공유가 가능한 Shared Worker위에 Web Socket을 얹어서 문제를 해결하다니 자기 전 머리가 띵한 느낌을 받았습니다. 해당 세션은 꼭 한번 보시길 추천합니다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://youtu.be/SVt1-Opp3Wo?feature=shared&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;토스ㅣSLASH 24 - N개의 탭, 단 하나의 웹소켓: SharedWorker&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;오늘 포스팅에서는 Web Worker에 대해서 간단히 알아보고 React, TypeScript, Vite 애플리케이션에서 Web Worker을 구현하는 예제를 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해당 포스팅은 Shared Worker에 대한 포스팅은 아닙니다. Dedicated Web Worker을 살펴보고 예제 코드 구현을 통해서 웹 워커를 구현하는 방법을 살펴볼 예정입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Web Worker&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 워커란 브라우저에서 제공하는 웹 API입니다. 웹 애플리케이션에서 메인 스레드로 감당하기 무거운 연산의 경우 웹 워커를 활용해 백그라운드 스레드로 연산을 처리해 메인 스레드에 결과를 응답할 수 있습니다.&lt;br /&gt;메인 스레드에서는 연산을 별도의 백그라운드 스레드에 위임하기 때문에 UI Blocking 없이 사용자에게 좋은 경험을 선사할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web Worker은 Dedicated Worker 과 Shared Worker로 나뉘어집니다. Dedicated Worker은 전용 웹 워커로써 백그라운드 스레드에서 연산 처리를 위해서 사용하는 워커 입니다.&lt;br /&gt;반대로 Shared Worker은 공유가 가능한 웹 워커로 특정 연산의 처리의 목적보다는 백그라운드 스레드를 여러 브라우저 탭에서 공유하기 위해서 사용합니다.&lt;br /&gt;Shared Worker을 공유하기 위해서는 두 가지를 충족시켜야 합니다. 같은 Origin, 같은 JavaScript 파일이여야 공유가 가능합니다. 이번 포스팅은 Shared Worker에 대한 포스팅보다는 Web Worker에 대한 내용을 살펴보는 포스팅임으로 다른 포스팅에서 Shared Worker을 자세히 살펴보겠습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Web Worker 주의할 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 워커를 사용하기에 앞서 주의해야 할 점이 몇 가지 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1. 브라우저 지원 범위&lt;/b&gt;&lt;br /&gt;먼저 브라우저 지원 범위를 살펴봐야합니다. 최신 환경의 브라우저에서는 대부분 웹 워커 기능을 제공하고 있지만 구형 버전의 브라우저나 특정 몇몇의 브라우저는 웹 워커를 기능을 제공하고 있지 않습니다.&lt;br /&gt;특히 Shared Worker은 지원하지 않는 특정 브라우저가 있으므로 개발 중인 서비스의 지원 대상을 면밀하게 검토한 후 적용해야 합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 웹 워커에서 사용할 수 없는 API&lt;/b&gt;&lt;br /&gt;웹 워커는 별도의 스레드를 가집니다. 메인 스레드의 window는 GlobalScope 입니다. 반면에 워커 스레드는 별도의 WorkerGlobalScope를 가지고 있습니다. 스코프가 다르기 때문에 메인 스레드에서 작동하는 기능을 워커 스레드에서 사용할 수 없습니다. 예를 들면 메인 스레드의 Window 객체에 접근할 수 없고, DOM을 조작 할 수 없습니다.&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;Fetch, Web Socket, SSE, Canvas API 등의 API는 워커 스레드에서도 사용할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers#web_apis_available_in_workers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;웹 워커에서 사용 가능한 API 목록&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3. 메세지 기반의 통신&lt;/b&gt;&lt;br /&gt;웹 워커는 메세지를 사용해 애플리케이션과 통신합니다. 메세지 기반의 통신은 이벤트 처리에 있어서 번거로울 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실습&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 워커를 실습해보도록 하겠습니다. 개발 환경은 Vite, React, TypeScript 입니다.&lt;br /&gt;우리가 개발할 앱은 간단히 웹 워커에 숫자를 전송하고 랜덤 숫자를 곱해 메세지를 전달받아서 화면에 디스플레이 합니다.&lt;br /&gt;필요한 컴포넌트와 웹 워커 스크립트는 다음과 같습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1. 웹 워커 객체를 생성하고 메세지를 보낼 컴포넌트&lt;/b&gt;&lt;br /&gt;컴포넌트에서는 웹 워커에 메세지를 전송(postMessage)합니다. 그리고 웹 워커로부터 메세지를 수신합니다. (onmessage)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 메세지를 받아서 백그라운드 스레드에서 연산을 처리할 웹 워커 스크립트&lt;/b&gt;&lt;br /&gt;웹 워커 역시 메세지를 컴포넌트로부터 수신합니다. (onmessage) 연산을 수행한 후 데이터를 메세지로 컴포넌트에 전송합니다. (postMessage)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 과정을 도식화 하면 아래의 그림이 나옵니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzA1X/btsJ3InO7p5/mNZEivokEPpVUqYZ1tKzlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzA1X/btsJ3InO7p5/mNZEivokEPpVUqYZ1tKzlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzA1X/btsJ3InO7p5/mNZEivokEPpVUqYZ1tKzlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzA1X%2FbtsJ3InO7p5%2FmNZEivokEPpVUqYZ1tKzlk%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;720&quot; height=&quot;380&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이번 실습에서는 웹 워커를 사용하기 위한 비즈니스 로직을 추상화해 커스텀 훅으로 만들어 컴포넌트에 제공해볼 예정입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실습 디렉토리 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 디렉토리 구조는 다음과 같습니다.&lt;br /&gt;소스 디레고리 하위에 workers 디렉토리가 위치하고 해당 디렉토리에 워커 스크립트 파일을 위치합니다.&lt;br /&gt;hooks 하위에 useWebWorker 라는 커스텀 훅을 통해 웹 워커를 사용하기 위한 비즈니스 로직을 추상화 합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;├── src
│&amp;nbsp;&amp;nbsp; ├── App.tsx
│&amp;nbsp;&amp;nbsp; ├── components
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── WorkerExample.tsx
│&amp;nbsp;&amp;nbsp; ├── hooks
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── useWebWorker.ts
│&amp;nbsp;&amp;nbsp; ├── main.tsx
│&amp;nbsp;&amp;nbsp; ├── vite-env.d.ts
│&amp;nbsp;&amp;nbsp; └── workers
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; └── worker-example.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;웹 워커 스크립트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 웹 워커는 메세지(기반 숫자)를 수신해 2초 후에 랜덤 숫자를 곱 연산해 메세지를 전송합니다.&lt;br /&gt;아래 스크립트의 self는 웹 워커의 컨텍스트 입니다. 웹 워커 컨텍스트의 onmessage 프로퍼티에 이벤트 핸들링 함수를 작성해줍니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;self.onmessage = (event) =&amp;gt; {
&amp;nbsp;&amp;nbsp;let result;
&amp;nbsp;&amp;nbsp;const { data } = event;

&amp;nbsp;&amp;nbsp;setTimeout(() =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (data) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result = data * Math.random();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result = Math.random();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;postMessage(result);
&amp;nbsp;&amp;nbsp;}, 2000);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;useWebWorker&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 워커를 사용하기 위한 비즈니스 로직을 추상화한 커스텀 훅 입니다. 해당 비즈니스 로직은 단순한 연산을 위한 코드며 만약 복잡한 연산을 처리할 웹 워커를 만든다면 다양한 Web Worker에 전달하기 위한 인자가 존재하거나 로직이 복잡해질 수 있습니다. 그럼 코드를 살펴보도록 하겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;먼저 props로 url 객체를 받습니다. 해당 url은 웹 워커 스크립트의 경로입니다.&lt;br /&gt;worker는 useRef를 통해 비제어 변수로 담아줍니다. 컴포넌트가 마운트 된 후 worker 객체를 만들어 worker에 바인딩 합니다.&lt;br /&gt;컴포넌트 역시 워커로부터 메세지를 수신해야 하기 때문에 onmessage 핸들러를 작성합니다. 그리고 메세지를 전송할 postMessage 함수도 작성합니다.&lt;br /&gt;컴포넌트가 언마운트 됐을 경우 메모리 누수를 방지하기 위해 사용중인 웹 워커 자원을 반납 해야합니다. useEffect의 cleanup 함수를 활용해 자원을 terminate 합니다.&lt;br /&gt;마지막으로 loading 상태를 더했습니다. 웹 워커는 별도의 백그라운드 스레드를 활용해 UI를 Non Blocking 합니다. 이때 사용자에게 데이터를 처리하고 있다는 것을 보여주기 위해 loading 상태를 더했습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useCallback, useEffect, useRef, useState } from &quot;react&quot;;

type Props = {
&amp;nbsp;&amp;nbsp;url: string;
};

type Result&amp;lt;T&amp;gt; = {
&amp;nbsp;&amp;nbsp;message: T | null;
&amp;nbsp;&amp;nbsp;loading: boolean;
&amp;nbsp;&amp;nbsp;sendMessage: (message?: T) =&amp;gt; void;
};

function useWebWorker&amp;lt;T&amp;gt;({ url }: Props): Result&amp;lt;T&amp;gt; {
&amp;nbsp;&amp;nbsp;const worker = useRef&amp;lt;Worker | null&amp;gt;(null);
&amp;nbsp;&amp;nbsp;const [loading, setLoading] = useState(false);
&amp;nbsp;&amp;nbsp;const [message, setMessage] = useState&amp;lt;T | null&amp;gt;(null);

&amp;nbsp;&amp;nbsp;const sendMessage = useCallback((message?: T) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (worker.current) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setLoading(true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.postMessage(message);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}, []);

&amp;nbsp;&amp;nbsp;useEffect(() =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current = new Worker(new URL(url, import.meta.url));

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.onmessage = (event: MessageEvent&amp;lt;T&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setLoading(false);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setMessage(event.data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (worker.current) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;worker.current.terminate();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}, [url]);

&amp;nbsp;&amp;nbsp;return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;loading,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendMessage,
&amp;nbsp;&amp;nbsp;};
}

export default useWebWorker;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import useWebWorker from &quot;../hooks/useWebWorker&quot;;

const workerPath = &quot;../workers/worker-example.ts&quot;;

const WorkerExample = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;const { message, loading, sendMessage } = useWebWorker&amp;lt;number&amp;gt;({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;url: workerPath,
&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h1&amp;gt;랜덤 숫자 생성&amp;lt;/h1&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={() =&amp;gt; sendMessage(2)}&amp;gt;생성하기&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{loading ? &amp;lt;p&amp;gt;로딩중..&amp;lt;/p&amp;gt; : &amp;lt;p&amp;gt;생성한 숫자: {message}&amp;lt;/p&amp;gt;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;);
};

export default WorkerExample;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;제작한 useWebWorker 훅을 사용하는 컴포넌트 입니다. 파라미터의 프로피티인 url로 사용할 웹 워커 스크립트의 상대 경로를 제공합니다. 버튼을 클릭하면 로딩중이라는 ui가 나오고, 메세지를 수신한 후 수신한 숫자를 확인할 수 있습니다.&lt;br /&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;1118&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAMyhX/btsJ35vYRax/RO34QA1cBb97lexDd9ZU4K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAMyhX/btsJ35vYRax/RO34QA1cBb97lexDd9ZU4K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAMyhX/btsJ35vYRax/RO34QA1cBb97lexDd9ZU4K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cAMyhX/btsJ35vYRax/RO34QA1cBb97lexDd9ZU4K/img.gif&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;540&quot; height=&quot;313&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;실습 코드를 확인하고 싶으시다면 아래의 레포지토리를 참고해주시면 감사하겠습니다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://github.com/kangactor123/web-worker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;레포지토리 확인하기&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 워커를 잘 활용한다면 사용자에게 더 좋은 경험을 제공할 수 있습니다. 운영중인 서비스에서 무거운 연산으로 인한 UI Blocking이 발생한다면 한번 쯤 고려해보면 좋을 것 같습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또한 토스증권의 케이스처럼 Web Worker와 다양한 API의 조합을 통해 서버 자원의 낭비를 막을 수 있고, 웹 워커를 공유함으로써 서버에 보내는 요청의 횟수도 최적화 할 수 있지 않을까 하는 생각도 들게 됐습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;기회가 된다면 추후 포스팅에서는 Shared Worker에 대해서도 공부하고 살펴보도록 하겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;읽어봐주셔서 감사합니다!&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>Web</category>
      <category>Webworker</category>
      <category>웹워커</category>
      <category>프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/57</guid>
      <comments>https://kangs-develop.tistory.com/57#entry57comment</comments>
      <pubDate>Sun, 13 Oct 2024 14:36:10 +0900</pubDate>
    </item>
    <item>
      <title>회고</title>
      <link>https://kangs-develop.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;10월 둘째 주 회고 글을 작성한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;9월 마지막 주 기술지원본부에서 이슈를 넘겨줬다. 고객사에서 나온 이슈는 아니지만 솔루션을 사용하는데 있어서 일관성이 없어서 불편하다는 이슈였다.&lt;br&gt;8월에 인수인계 받은 우리 팀의 솔루션은 사실 이슈가 너무 많았고, 이슈를 단순히 덮어 놓은 부분들도 많이 발견했다.&lt;br&gt;이번 이슈는 그 이슈의 일환으로 솔루션의 코어 컴포넌트인 테이블에서의 액션과 로우 선택, 체크가 페이지마다 일관성이 없다는 것 이였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;개인적으로 특정 컴포넌트의 특정 기능은 잘 추상화 되어 하나의 비즈니스 로직으로 간단하게 컴포넌트(페이지)에서 사용할 수 있어야 한다고 생각한다.&lt;br&gt;하지만 현재 개발된 테이블과 비즈니스 로직은 기능을 직접 페이지 컴포넌트에서 구현하고 있기 때문에 수 많은 페이지를 다양한 개발자들이 개발했던 탓인지 일관성이 없는 UX가 많았다.&lt;br&gt;하여 주간 회의에서 테이블의 일관성을 유지하기 위해 정책을 수립하자는 의견을 냈고, 테이블 정책을 수립해 페이지 컴포넌트를 수정하기 시작했다. (대략.. 80개가 넘는 컴포넌트를 수정해야 했다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇게 수정을 시작하니 생각보다 이슈가 있는 화면이 더욱 많았고, 수정하다보니 덮어있던 이슈들도 하나씩 들어나고는 했다. 결국엔 왜 이런 이슈를 해결하지 않고 지금까지 유지했던 것일까? 라는 궁금증마저 들기 시작했다.&lt;br&gt;해당 이슈를 리딩하며 두 분의 프론트엔드 개발자분과 함께 이슈를 진행하고 있었다. 현재 스프린트를 진행하는 것도 아니고, 데드라인이 정해진 이슈도 아니여서 이슈에 이슈가 발생하면 해당 이슈까지 픽스를 하려고 했었다. (현재 고치고자 하는 이슈도 아니였음에도 불구하고) 그러다보니 10월 둘째 주까지 이슈를 끝내지 못하며 이슈가 너무 늘어지고 있다는 생각이 들기 시작했다. 이렇게 꼬리를 무는 이슈까지 다 해결하려고 하다가 원래의 이슈를 해결하지 못하겠다는 생각이 들었던 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;결국 결단을 내렸다. 현재 리포팅 된 이슈를 우선적으로만 픽스하고, 추가적으로 발생하는 이슈건들은 기록으로 남겨놓고 현재 이슈를 쳐내고 개선 건으로 진행하기로 결정했다. 이슈가 늘어지면 늘어질수록 목표를 달성하지 못하고 늘어지는 상황에 공수와 힘만 빼고 있던 상황이였기 때문이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;어제, 10월 11일(금) 일자로 현재 리포팅 된 이슈를 수정하는 것을 종료했다. 물론 차주에 해당 이슈건을 테스트를 해야겠지만, 테스트로 인해 발생하는 이슈는 다른 이슈로 진행할 예정이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;왜 이슈가 이렇게 늘어졌을까 생각해봤다. 소프트웨어 개발자라면 진행중인 이슈가 지연되거나 늘어지는 상황을 반드시 피해야 한다고 생각하기 때문에 회고를 통해 두번 다시 이런 상황을 만들지 말아야 겠다는 생각을 했다.&lt;br&gt;1. 이슈의 범위를 제대로 예측하지 못했다.&lt;br&gt;2. 이슈에서 또 다른 이슈가 발생했을 때 함께 처리하려고 하여 이슈의 진행도가 늘어졌다.&lt;br&gt;3. 함께 작업하는 개발자들과 초기에 제대로 합을 맞추지 못했다.&lt;br&gt;4. 이슈의 단위를 쉽게 생각했다. 해당 이슈를 하위 이슈가 아닌 에픽단위로 생각을 했어야 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;원인을 네 가지로 좁혀봤다.&lt;br&gt;첫 번째 원인은 개발중인 솔루션에 익숙하지 않아서라고 생각한다. 현재 팀에서 근무중인 프론트엔드 개발자는 나 포함 모두 8월 중순에 인수인계를 받아서 코드를 확인했다. 그러다보니 솔루션이 익숙하지 못했고 아무도 범위를 제대로 예측할 수 없었다. 앞으로 제품을 계속 개발하다보면 솔루션에 익숙해져 이슈의 범위를 제대로 예측할 수 있지 않을까 싶다. 이 부분은 익숙함의 문제이기에 시간이 흐르면 해결할 수 있을 것 같다.&lt;br&gt;두 번째 원인은 결단력이 부족해서 였다고 생각한다. 이슈 단위로 일을 할 것이면 해당 이슈에서 리포팅 된 이슈에 한해서 작업을 진행해야 했다. 또 다른 이슈가 발생한다면 이슈로 리포팅 한 후 진행해야 이슈가 늘어지지 않을 것이다. 앞으로는 이슈의 커버리지에 한해서 일을 해야겠다는 생각이 들었다.&lt;br&gt;세 번째 원인은 커뮤니케이션의 부족이였다. 컨플루언스 페이지에 정리를 해서 공유를 하면 괜찮을 것 이라고 생각했는데 오판이였던 것 같다. 앞으로 함께 이슈를 진행할 경우에는 반드시 이슈에 관련된 간단한 스탠드 업 미팅이라도 진행해야겠다는 생각이 들었다.&lt;br&gt;네 번째 원인은 판단 미스이다. 현재 지라를 사용하고 있고 백로그에서 이슈를 에픽 &amp;gt; 스토리,작업,버그 &amp;gt; 하위 이슈 로 나눠서 작업하고 있었다. 해당 이슈를 하위 이슈로 진행했는데, 적어도 중간 단계의 작업으로 분류하고 이슈를 더 잘게 쪼개서 진행했어야 했다는 생각이 들었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번 교훈을 바탕으로 다음번에는 더욱 정교하게 일을 진행해야겠다는 생각이 들었다.&lt;br&gt;지금 팀이 팀장님도 없고 아직 혼란스러워서 뭔가 진행이 계획적으로 되고 있지는 않다. 프론트엔드 파트 만이라도 예전에 경험했던 방식대로 에자일하게 일을 해볼까라는 생각도 하고 있다. 주 단위 스프린트를 통해 이슈를 정확히 예측하고 스토리 포인트를 산정해 이슈를 진행하며 데일리 스탠드업 미팅을 진행해볼까도 생각하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에 했던 고민을 바탕으로 나는 더 나은 소프트웨어 개발자가 될 것이다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/56</guid>
      <comments>https://kangs-develop.tistory.com/56#entry56comment</comments>
      <pubDate>Sat, 12 Oct 2024 13:40:44 +0900</pubDate>
    </item>
    <item>
      <title>MFA (Micro Frontend Architecture)</title>
      <link>https://kangs-develop.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스팅은 MFA 강좌를 수강하고 MFA에 대해서 공부한 내용을 기록하기 위한 포스팅 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA가 무엇인지 살펴보고 조직에서 MFA 도입을 고려하면 좋은 시점에 대해서 고민해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로는 대표적으로 MFA를 구현하는 방식에 대해서 남겨보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;MFA에 대해서 궁금하신 분들은 아래 강의를 통해서 접해보시면 좋을것 같습니다. (관계자 아님, 내돈 내산)&lt;br /&gt;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_mfa&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;gt; 강의 바로가기&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MFA&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA는 Micro Frontend Architecutre의 약자로써 기존의 모놀리식 프론트엔드를 앱 단위로 잘개 쪼개어 하나의 애플리케이션으로 통합하는 현대식 프론트엔드 아키텍처 입니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-03 오후 12.15.01.png&quot; data-origin-width=&quot;2466&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM8Kzc/btsJTHbvb0C/hs9kjKq9Ss0AVbQL3Ec3yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM8Kzc/btsJTHbvb0C/hs9kjKq9Ss0AVbQL3Ec3yk/img.png&quot; data-alt=&quot;https://www.daangn.com/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM8Kzc/btsJTHbvb0C/hs9kjKq9Ss0AVbQL3Ec3yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM8Kzc%2FbtsJTHbvb0C%2Fhs9kjKq9Ss0AVbQL3Ec3yk%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;625&quot; height=&quot;330&quot; data-filename=&quot;스크린샷 2024-10-03 오후 12.15.01.png&quot; data-origin-width=&quot;2466&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.daangn.com/&lt;/figcaption&gt;
&lt;/figure&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 당근 플랫폼이 모놀리식 프론트엔드를 운영한다면 다음과 같이 도식화를 할 수 있을 것 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모놀리식 프론트엔드는 다양한 말로 설명할 수 있겠습니다만, 저는 여기서 &lt;b&gt;전통적인 프론트엔드 방식으로 모든 서비스를 하나의 코드베이스로 개발, 운영하는 형태의 프론트엔드&lt;/b&gt; 라고 정의해보도록 하겠습니다.&lt;/blockquote&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-filename=&quot;스크린샷 2024-10-03 오후 12.22.06.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;1068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0ulqL/btsJUYiLgtV/TwtiImTgDbYIOk3zRUjn21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0ulqL/btsJUYiLgtV/TwtiImTgDbYIOk3zRUjn21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0ulqL/btsJUYiLgtV/TwtiImTgDbYIOk3zRUjn21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0ulqL%2FbtsJUYiLgtV%2FTwtiImTgDbYIOk3zRUjn21%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;306&quot; height=&quot;458&quot; data-filename=&quot;스크린샷 2024-10-03 오후 12.22.06.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;1068&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;위 서비스를 MFA 구조로 잘게 잘라봅시다! 당근은 명확하게 서비스 단위를 나눌 수 있기 때문에 MFA 구조로 설계한다면 보다 명확하게 애플리케이션을 나눌 수 있을 것 같습니다. (실제로 운영을 한다면 보다 더 복잡한 설계가 필요할 것 입니다.)&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-filename=&quot;스크린샷 2024-10-03 오후 12.22.24.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEPD1A/btsJUwAbfdc/MMu5NrVHKcfTYvr7t3jkQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEPD1A/btsJUwAbfdc/MMu5NrVHKcfTYvr7t3jkQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEPD1A/btsJUwAbfdc/MMu5NrVHKcfTYvr7t3jkQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEPD1A%2FbtsJUwAbfdc%2FMMu5NrVHKcfTYvr7t3jkQ1%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;726&quot; height=&quot;552&quot; data-filename=&quot;스크린샷 2024-10-03 오후 12.22.24.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;1046&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA는 MSA와 비슷한 면모가 많습니다. 서비스 단위로 서버를 잘게 쪼개듯이 프론트엔드 애플리케이션도 서비스 단위로 잘게 쪼개어 아키텍처를 설계합니다.&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;그럼 MFA를 왜 도입해야 할까, MFA 도입을 고민하면 좋은 시점은 언제일까에 대해서 생각해보도록 하겠습니다.&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;&lt;b&gt;MFA를 왜 도입해야할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처를 변경하는 과정은 단순히 엔지니어링 관점에서 의사결정을 할 수 없습니다. 아키텍처를 변경하는 과정에서 조직의 변경이 필요할 수 있을 뿐더러 개발, 운영 단계에서 상당히 많은 부분이 변경될 수 있고 챙겨야 할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 MFA 도입을 해야하는 이유를 시뮬레이션 해보도록 하겠습니다.&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;저는 10명의 프론트엔드 엔지니어가 속해있는 팀의 프론트엔드 리더 개발자 입니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상상의 나래를 펼쳐가며 상황을 시뮬레이션 해봤습니다. 위와 같은 상황은 어떤 엔지니어든 겪을 수 있는 상황일 것 같습니다. 위 상황속에서 MFA를 도입하기 위해 설득할 수 있는 포인트가 몇 가지 존재합니다.&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. 지속적으로 확장되는 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 코드 베이스의 복잡도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 빌드, 배포 시간의 단축&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. SPOF&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;지속적으로 확장되는 서비스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 현재까지 확장되었고 앞으로도 확장될 여지가 깊다면 이는 MFA 도입을 고려해봐야 할 시점입니다. 서비스가 이미 성숙한 단계에 머물러 앞으로 현재의 유저가 유지되거나 축소될 여지가 있다면 해당 서비스에 MFA를 도입하는건 비용 낭비일 수 있습니다. 하지만 서비스가 아직 도약기에 있어 앞으로 확장될 수 있다면 MFA로 아키텍처를 재설계해 추후 추가될 서비스를 더욱 빠르게 개발, 서빙하고 안정성 있게 프론트엔드 애플리케이션을 확장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드 베이스의 복잡도&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 코드 베이스로 여러 서비스를 개발하다보면 복잡도는 당연히 높아질 수 밖에 없습니다. 게다가 빠르게 확장되는 서비스에서는 코드 중복과 복잡도 (기술 부채)를 어느정도 용인할 수 있기 때문에 공통 컴포넌트 및 로직의 경우 특정 비즈니스 로직이 섞이게 된다면 다른 서비스에서 사용하기 위해 또 다른 컴포넌트를 만들어야 할 수 있고 계속해서 기술 부채가 쌓이다보면 코드 베이스의 복잡도가 높아지고 응집도는 낮아지게 됩니다. 결국 이런 상황은 새로운 개발자가 팀에 합류했을 때 인수인계에 부담감을 줄 수 있고 사이드 이펙트 또한 예측이 어려워 질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA를 도입하게 된다면 코드 베이스의 복잡도를 낮출 수 있습니다. 만약 공통으로 사용하는 UI-KIT이나 컴포넌트, 로직을 별도의 레포지토리로 분리하여 사용한다면 더욱 명확해질 것 입니다. 서비스 간 관계를 명확하게 하고, 통합 애플리케이션에 서비스 애플리케이션을 주입함으로써 서비스 단위의 독립성을 유지합니다. 이로 인해 서비스의 유지 보수에도 이점이 생기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;빌드, 배포 시간의 단축&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 커다란 코드 베이스를 빌드 후 배포하는 과정은 오랜 시간이 소요될 수 있습니다. 만약 MFA를 도입하게 된다면 변경된 서비스만 빌드 후 재배포 할 수 있기 때문에 상대적으로 빌드, 배포 시간이 단축됩니다. 이로 인해 개발자의 생산성을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA와 함께 모노레포를 도입하고 모노레포 관리 도구들에서 지원해주는 빌드 캐싱 기능을 활용한다면 빌드, 배포 시간의 단축에 대한 이점을 더욱 명확하게 누릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SPOF (Single Point of Failure)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA로 구조를 재설계 함으로써 단일 장애 지점이라고 불리우는 장애 전파를 예방할 수 있습니다. 통합 애플리케이션에 각각의 서비스 애플리케이션에 주입해 각각의 서비스 애플리케이션에서 발생하는 장애가 다른 서비스에 영향을 끼치지 않도록 합니다.&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;MFA로 프론트엔드 아키텍처를 재설계 한다면 위와 같은 이점을 누릴 수 있게 됩니다.&amp;nbsp;&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;&lt;b&gt;MFA에 대한 고민이 유효한 시점&lt;/b&gt;&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;대표적으로 MFA를 구현하는 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA를 구현하는 방법은 여러 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 애플리케이션을 빌드 타임에 통합하는 방식, iframe을 통한 통합 방식, 마지막으로 Module Federation 방식이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하는 방식은 재각각 이지만 여러 팀에서 가장 많이 사용하고 있으며 MFA의 장점을 명확하게 누릴 수 있는 구현 방식은 &lt;b&gt;Module Federation 방식&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;해당 글은 MFA를 구현하는 방식에 대한 글이 아니기 때문에 Webpack을 사용해 MFA를 구현하는 방식은 다른 글을 통해서 살펴보도록 하겠습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Module Federation 방식은 Webpack 5버전에 도입된 방식으로 shell, remote를 활용해 런타임에 remote 애플리케이션을 shell 애플리케이션에 주입하는 방식입니다. 런타임을 통해서 애플리케이션을 주입하기 때문에 자연스럽게 코드 스플리팅이 적용됩니다. mf-app 탬플릿을 활용해 쉽게 SPA(Single Page Application) MFA를 구현할 수 있고, NEXT.js를 활용해도 쉽게 MFA를 구현할 수 있습니다.&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;Vite를 활용한 React 프로젝트 역시 Module Federation을 활용해 MFA를 구현할 수 있습니다. 아래 플러그인은 Webpack의 Module Federation에 감명을 받고 커뮤니티에서 만들어 제공하는 플러그인입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://github.com/originjs/vite-plugin-federation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;vite-plugin-fedration&lt;/a&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;하지만 해당 플러그인은 Vite에서 직접 제공하는 것이 아닌, 커뮤니티를 통해 오픈소스로서 제공하고 있습니다. 그렇기 때문에 해당 플러그인을 통해 프로덕션을 운영하기 위해서는 많은 고민이 필요합니다. 아직 Vite 자체에서 Module Federation을 제공하고 있지는 않지만, Evan You의 Vite 마일스톤에 Module Federation이 포함되어 있다고 하니 기대를 걸어볼 수 있겠습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 레포지토리를 통해 Module Federation을 구현한 다양한 예제 코드를 살펴보실 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://github.com/module-federation/module-federation-examples&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레포지토리 살펴보기&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글을 통해서 MFA에 대해서 간단히 살펴보고 MFA 도입을 고민할 시점과 고민할 이유에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MFA는 기술이 아닌 아키텍처 입니다. 아키텍처를 구현할 수 있는 기술(방법)은 다양합니다. 하지만 아키텍처를 도입하기 위해 타당한 이유를 찾고 조직을 설득하는 과정은 명확해야 합니다.&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;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>MFA</category>
      <category>MicroFrontEnd</category>
      <category>Web</category>
      <category>아키텍처</category>
      <category>프론트엔드</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/54</guid>
      <comments>https://kangs-develop.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 3 Oct 2024 13:30:11 +0900</pubDate>
    </item>
    <item>
      <title>MFA 강의를 수강하고</title>
      <link>https://kangs-develop.tistory.com/53</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdhxkU/btsJQHVHSci/9rPqPxxbsI3yxIEXMqHVo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdhxkU/btsJQHVHSci/9rPqPxxbsI3yxIEXMqHVo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdhxkU/btsJQHVHSci/9rPqPxxbsI3yxIEXMqHVo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdhxkU%2FbtsJQHVHSci%2F9rPqPxxbsI3yxIEXMqHVo0%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;286&quot; height=&quot;724&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지난 7월 21일 MFA 강의 수강을 시작하면서 글을 작성했다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/32&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;MFA, Monorepo 강의 수강을 시작하며&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두 달이 지난 시점에서 강의를 완강했다. 부록 한 파트가 남긴 했는데 레거시 환경에서의 점진적 전환에 관한 파트는 넘어가도 괜찮겠다는 생각이 들어서 수강하지 않았다.&lt;br&gt;&amp;nbsp;&lt;br&gt;강의를 듣기 전까지는 MFA에 대해서 자세하게 알지 못했다.&lt;br&gt;MSA는 근무한 팀에서도 택하고 있는 아키텍처이고, 화제성이 많은 아키텍처이기 때문에 잘 알고 있었다.&lt;br&gt;MFA도 MSA처럼 프론트엔드 애플리케이션을 서비스 단위로 쪼개는 것에 대해서는 알고 있었지만, 정확히 어떤 방식으로 구현을 할지에 대해서는 생각해보지 못했던 것이다.&lt;br&gt;회사에서도 MFA에 대한 이야기가 나왔고, 평상시에 MFA와 모노레포에 대한 두려움을 가지고 있었고 요즘 많은 채용 공고의 JD에서 확인할 수 있었기 때문에 해당 기술은 내 커리어에 있어서 반드시 알아야 하는 기술이라고 생각이 들었다.&lt;br&gt;그렇게 해당 강의를 수강하고 다양한 프론트엔드 도구들과 기술들에 대해서 하나씩 알아보고 실습을 통해 토이 프로젝트로 MFA를 구현하는 단계까지 끝냈다.&lt;br&gt;&amp;nbsp;&lt;br&gt;강의를 수강하고 나서 든 생각은 MFA는 특정 기술이 아닌 아키텍처 라는 것 이다.&lt;br&gt;MFA를 특정 기술처럼 생각하고 이야기 하는 내용을 많이 들었지만 강의를 듣고 나서는 MFA는 결국엔 A(Architecture)일 뿐 구현하는 방법(기술)은 다양하다 라는 것 이다.&lt;br&gt;따라서 MFA에 접근을 하려고 한다면 기술적인 관점 보다는 아키텍처 적인 관점과 조직, 비즈니스적인 관점에서 접근을 해야 한다는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;특정 서비스를 운영하다보면 트레픽이 늘어나고, 고객의 다양한 니즈를 충족시키기 위해 다양한 하위 서비스가 운영될 수 있다. 서비스가 확장됨에 따라서 조직의 규모가 확장이 되고, 중복되는 코드베이스, 늘어나는 협업의 비용, 애매해지는 오너십에 대한 문제가 생길 수 있다.&lt;br&gt;이런 문제를 단번에 해결할 수 없지만 아키텍처적인 관점에서 해결할 수 있는 것이 MFA 라고 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;MFA를 도입해 중복되는 코드를 패키지 단위로 분리해 재사용 하고, 서비스를 App 단위로 분리해 개발, 운영을 하며 코드 베이스의 컨플릭을 방지하며 낭비되는 협업의 비용을 줄인다. 그리고 마지막으로 분리된 App을 통해 해당 서비스의 오너쉽을 명확히 하여 소비자가 더욱 신뢰할 수 있는 서비스를 만든다. MFA를 도입할 경우 위 문제를 가장 이상적이고 아름다운 방안으로 해결할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 MFA가 만능은 아니다. MFA를 도입하려면 조직적인 관점에서도 개편이 필요한 법이다. 또한 현재의 개발 문화를 변경해야 할 수 있기 때문에 조직원들의 충분한 공감과 이해가 필요하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지난 7월 강의를 수강할 시점에 팀 내에서 논의했던 MFA에 대한 이야기는 미뤄질 것 같다. 폭풍같던 8월이 흘러갔고, 폭풍의 결과로 다른 팀에 배치되어 프론트엔드를 보고 있기 때문이다. 하지만 결국 다시 원래의 팀과 원래 개발했던 제품으로 돌아갈 것이고 그때서야 MFA에 대한 이야기를 다시 한번 논의해볼 수 있지 않을까 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아직은 익숙하지 않은 MFA를 조금 더 연습하고 싶어서 당근을 모티브로 아키텍처를 나누는 연습(토이 프로젝트)을 해보고 있다. 아직은 시작 단계여서 작은 코드 베이스 이지만, 연습을 통해 MFA에 조금 더 가까워지고 싶다는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;MFA에 대해서 궁금하고 학습을 원한다면 아래 강의를 수강해보는 것도 괜찮을 것 같다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_mfa&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Micro Frontend부터 모노레포까지 대규모 서비스를 위한 프론트엔드 아키텍처&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>frontend</category>
      <category>MFA</category>
      <category>MicroFrontEnd</category>
      <category>react</category>
      <category>Web</category>
      <category>회고</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/53</guid>
      <comments>https://kangs-develop.tistory.com/53#entry53comment</comments>
      <pubDate>Sat, 28 Sep 2024 12:24:45 +0900</pubDate>
    </item>
    <item>
      <title>[React] 상태(State)에 대하여</title>
      <link>https://kangs-develop.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;React는 다른 UI 프레임워크에 비해서 상태 관리가 복잡하고 어렵다는 이야기가 종종 들립니다. 이번 글에서는 상태에 대해 알아보고 상태 관리가 왜 복잡한지에 대해서 고민해보도록 하겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 React의 상태 관리는 어려울까?&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;React의 상태는 랜더링과 직접적인 관계가 있습니다. 상태가 변경이 되면 해당 상태와 연관된 컴포넌트는 랜더링이 발생합니다.&lt;br&gt;그렇기 때문에 상태를 최적화 하는 것이 중요합니다. React의 상태를 잘 관리하는 것이 곧 컴포넌트 랜더링 최적화이기 때문입니다. (물론 랜더링 최적화 수단은 상태 관리 외 여러가지 것들이 있습니다.)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React의 데이터의 흐름&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;React의 데이터는 단방향으로 흐릅니다. 위에서 아래로, 부모 노드에서 자식 노드로 데이터가 흘러갑니다.&lt;br&gt;이는 React의 디자인과 관련이 있는데, 위에서 아래로 데이터를 흐르게 함으로써 컴포넌트를 순수하며 예측 가능하게 설계하고 디버깅이 용이하게 하기 위함입니다.&lt;br&gt;데이터가 위에서 아래로 흐르기 때문에 상태 또한 마찬가지로 위에서 아래로 흐릅니다. 부모 컴포넌트에서 소유하고 있는 상태는 props를 통해 자식 컴포넌트로 흘러갑니다.&lt;br&gt;종종 자식 컴포넌트에서 관리하는 상태를 부모 컴포넌트로 끌어 올리기도(리프팅) 하지만 React의 데이터는 위에서 아래로 흐르는 단방향 바인딩 입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단방향 바인딩의 특징&lt;/b&gt;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;선언적 프로그래밍&lt;/b&gt;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;React는 선언적 프로그래밍 패러다임을 따릅니다. 즉, UI는 주어진 상태에 따라 어떻게 보일지를 설명하는 방식입니다. 단방향 데이터 흐름은 이 패러다임과 매우 잘 맞습니다. 상태(state)가 변경되면 해당 상태에 맞춰 UI가 자동으로 업데이트되므로 개발자는 데이터를 어디서 변경해야 할지 명확하게 이해할 수 있습니다.&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;UI , 데이터 일관성&lt;/b&gt;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;단방향 데이터 흐름은 데이터의 흐름을 한 방향으로 제한하기 때문에, 데이터의 변동과 그에 따른 UI 업데이트가 일관성 있게 유지됩니다.&lt;/li&gt;&lt;li&gt;만약 데이터 흐름이 양방향이라면, 어떤 컴포넌트가 데이터를 변경했는지 파악하기 어려워질 수 있습니다. 이것은 특히 복잡한 애플리케이션에서 상태 불일치(state inconsistency)와 같은 문제를 야기할 수 있습니다.&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;유지 보수성 및 확장성&lt;/b&gt;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;단방향 데이터 흐름은 애플리케이션 구조가 더 깔끔하고 모듈화된 컴포넌트 설계를 가능하게 합니다. 각각의 컴포넌트가 자신이 받은 데이터(props)에만 의존하여 동작하기 때문에, 컴포넌트 간의 의존성을 최소화할 수 있습니다.&lt;/li&gt;&lt;li&gt;또한 단방향 바인딩 디자인 패턴을 통해서 재사용 가능한 컴포넌트를 설계, 구현할 수 있습니다.&lt;/li&gt;&lt;li&gt;이러한 구조는 코드베이스가 커지더라도 각 컴포넌트를 독립적으로 관리할 수 있게 해주며, 유지 보수와 확장이 용이합니다.&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예측 가능성&lt;/b&gt;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;React에서는 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 방식으로 props를 사용합니다. 이는 데이터가 항상 부모에서 자식으로 흐르기 때문에, 데이터가 어떻게 전달되는지 추적하기가 쉽습니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;상태(State)&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;React는 변할 수 있는 값을 상태라고 부릅니다. 사용자의 인터렉션, 시간의 흐름에 따른 값의 변화, 외부에 의한 변화 등 이러한 요인에 의해 변할 수 있는 값을 상태라고 정의하고 우리는 함수 컴포넌트에서 useState라는 특정 API를 사용해 값을 다루고 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;React의 state는 불변성을 유지해야 합니다. 이 말은 state의 Snapshot은 절대로 직접 변경할 수 없으며 state를 변경하고자 할 때는 Modifier 함수를 이용해야 한다는 것 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 state를 Modifier 함수를 이용하지 않고 직접 변경하려고 한다면 애플리케이션의 변화를 예상할 수 없게 됩니다. 또한 직접 값을 변경하려고 해도 컴파일 타임에 에러를 뱉게 됩니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6LmpO/btsJEmyLNty/7y1I7Ow8l86ZIqWvdpiPK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6LmpO/btsJEmyLNty/7y1I7Ow8l86ZIqWvdpiPK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6LmpO/btsJEmyLNty/7y1I7Ow8l86ZIqWvdpiPK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6LmpO%2FbtsJEmyLNty%2F7y1I7Ow8l86ZIqWvdpiPK0%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;622&quot; height=&quot;399&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;useState를 통해 리턴 받는 Modifier 함수의 파라미터로는 변경될 값 또는 익명 함수를 전달할 수 있습니다. 익명 함수의 파라미터로는 변경할 이전 값이 제공이 되며 이를 활용해 순수 함수를 넘겨줄 수 있게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래는 가장 쉽게 확인할 수 있는 counter 예제입니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

const State = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;const [counter, setCounter] = useState(0);

&amp;nbsp;&amp;nbsp;const handleDecrease = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setCounter((prev) =&amp;gt; prev - 1);
&amp;nbsp;&amp;nbsp;};

&amp;nbsp;&amp;nbsp;const handleIncrease = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setCounter((prev) =&amp;gt; prev + 1);
&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={handleDecrease}&amp;gt;-&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p&amp;gt;{counter}&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={handleIncrease}&amp;gt;+&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;);
};

export default State;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사용자 인터렉션 (버튼 클릭)을 통해서 카운터의 값을 변경하고 있습니다. 여기서 카운터는 변할 수 있는 값이기 때문에 상태로 관리하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eE0cnN/btsJEj9UVyz/kf9MyEBbA2oDdJkAu44Xj1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eE0cnN/btsJEj9UVyz/kf9MyEBbA2oDdJkAu44Xj1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eE0cnN/btsJEj9UVyz/kf9MyEBbA2oDdJkAu44Xj1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/eE0cnN/btsJEj9UVyz/kf9MyEBbA2oDdJkAu44Xj1/img.gif&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;462&quot; height=&quot;458&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;상태가 변경됨에 따라서 해당 상태를 구독 중인 컴포넌트(태그)가 변화하고 있습니다. 이는 상태가 변경됨에 따라서 랜더링이 발생하고 있는 것 입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;상태 최적화&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제를 통해 상태를 변경한다면 상태를 구독하고 있는 (상태 값을 사용하고 있는 것을 상태를 구독한다고 표현하겠습니다.) 컴포넌트에서는 랜더링이 발생한다는 것을 확인했습니다. 랜더링을 최적화 하기 위해서는 상태를 최적화 해야 한다는 사실도 알게 됐습니다. 상태를 최적화 하는 여러 방법을 살펴보겠습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;상태를 적절한 컴포넌트에서 소유하기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;특정 상태를 어느 컴포넌트에서 관리해야 한다는 정답은 없습니다. 다만 React의 특성 상 자식 컴포넌트에서 상태를 사용해야 한다면 props로 내려줘야하고, props를 타고 내려간다면 props drilling 현상이 발생함으로 적당한 위치에서 state를 선언해야 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;state를 props로 계속해서 내리다보면 상태가 변경될 때 디버깅이 어려울 뿐더러 어느 컴포넌트에서 변화가 생길 지 예측하기 어렵습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또한 state를 자식 컴포넌트에서 잘못 소유하고 있다면 부모 컴포넌트에서 해당 state를 사용하기 어렵기 때문에 이 문제를 해결하기 위해 전역 상태 관리를 하게 됩니다. 단순히 이 문제만을 해결하기 위해 프로젝트에 전역 상태관리 도구를 도입하기에는 오버헤드가 크기 때문에 컴포넌트 구조를 재설계 하여 상태를 적절한 컴포넌트에서 관리하는 것으로 문제를 해결할 수 있게 됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단순하게 State 만들기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;State는 정답이 존재하지 않습니다. 원시 값을 State로 다룰 수 있고 객체 형태의 값을 State로 다룰 수 있습니다. 만약 객체 형태의 값을 State로 다룬다면 해당 객체가 복잡하게 설계가 되어있는지 확인해봐야 합니다. 복잡한 형태의 State는 값을 변경하기 어렵습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;아래와 같은 타입을 가진 객체를 State로 다룬다고 가정합시다. 해당 객체는 info라는 프로퍼티에 name이라는 객체를 가지고 있고 name이라는 객체의 프로퍼티를 통해 값을 관리하고 있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type FormType = {
&amp;nbsp;&amp;nbsp;id: string;
&amp;nbsp;&amp;nbsp;pwd: string;
&amp;nbsp;&amp;nbsp;phone: string;
&amp;nbsp;&amp;nbsp;addr: string;
&amp;nbsp;&amp;nbsp;info: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;last: string;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;first: string;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;};
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 객체를 State로 관리한다면 last, first 값을 변경하기 위해서는 다음과 같은 로직을 작성해야 합니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const Form = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;const [form, setForm] = useState&amp;lt;FormType&amp;gt;(initValue);

&amp;nbsp;&amp;nbsp;const handleChangeName = (event: SyntheticEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const id = event.currentTarget.id;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const value = event.currentTarget.value;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setForm((prev) =&amp;gt; ({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...prev,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;info: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...prev.info,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...prev.info.name,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(id === &quot;last&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;? {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;last: value,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;first: value,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}));
&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;... 생략
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;React의 State는 불변성을 유지해야 하기 때문에 복잡한 구조의 객체를 다루기 위해서는 복잡한 과정을 거쳐야 합니다.&lt;br&gt;immer라는 도구를 활용해 위 과정을 조금 더 쉽게 해결 할 수 있겠지만, 이 또한 문제를 해결하기 위해 도구를 도입해야 하기 때문에 관리해야 할 의존성이 추가된다는 오버헤드가 발생합니다. 그렇기 때문에 State는 가급적 단순하게 관리해 다루기 편리하게 최적화 하는 것이 좋습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;State 중복 피하기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;같은 역할이지만 다른 명칭을 가진 여러 개의 State가 존재할 수 있습니다. 해당 State들은 추상화를 통해 상태를 일원화 할 수 있기 때문에 제거해주도록 합시다.&lt;br&gt;마찬가지로 여러 개로 작성된 State가 값이 변할 때 함께 변경되는 케이스가 존재할 수 있습니다. 해당 State들은 같은 역할을 하는 State일 수 있으니 통합을 고려해볼 수 있습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이외에 State 구조에 관한 내용은 다음의 공식 문서를 살펴봐 주세요.&lt;br&gt;&lt;br&gt;&lt;a href=&quot;https://ko.react.dev/learn/choosing-the-state-structure&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;공식 문서 살펴보기&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;State 구조 선택하기 – React&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/learn/choosing-the-state-structure&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eTPo2/hyW6CfGfjq/KvV6JD3cPmnn0eXLCVdOkK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/CvBXF/hyW245ZBaN/CU8EwKwKTbj0I2u6COAe91/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot; data-og-url=&quot;https://ko.react.dev/learn/choosing-the-state-structure&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/choosing-the-state-structure&quot; target=&quot;_blank&quot; data-source-url=&quot;https://ko.react.dev/learn/choosing-the-state-structure&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eTPo2/hyW6CfGfjq/KvV6JD3cPmnn0eXLCVdOkK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/CvBXF/hyW245ZBaN/CU8EwKwKTbj0I2u6COAe91/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;State 구조 선택하기 – React&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;The library for web and native user interfaces&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;ko.react.dev&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;다양한 상태 관리 도구&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 다양한 상태 관리 도구에 대해서 간단하게 살펴봅시다.&lt;br&gt;useState를 통해서 컴포넌트에서 직접 State를 소유하고 관리할 수 있습니다. 하지만 props drilling 문제가 발생하거나 여러 컴포넌트에서 하나의 상태를 공유해서 사용해야 한다면 전역 상태 관리 도구의 도입을 고려해볼 수 있습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Redux&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Redux는 가장 많이 사용중인 상태관리 라이브러리 입니다. Flux 패턴으로 상태를 관리하며 이는 위에서 아래로 데이터가 흐르는 React의 디자인과 상당히 잘 어울리는 라이브러리 입니다.&lt;br&gt;비동기 데이터를 관리하기 위해서는 Redux-thunk나 Redux-saga와 같은 미들웨어를 추가로 도입해야 한다는 단점이 있습니다. 그리고 Flux패턴을 활용한 상태관리에 미들웨어까지 더해져 상태 관리를 위해 상당한 보일러 플레이트를 생성, 관리 해야 한다는 점과 가파른 러닝 커브가 존재한다는 점이 존재해 요즘은 Redux를 벗어나고 있는 추세를 보이고 있습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/%EB%B3%B5%EC%9E%A1%ED%95%98%EA%B3%A0-%EC%96%B4%EB%A0%A4%EC%9A%B4-redux-%EC%A0%81%EC%9D%91%EA%B8%B0/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;복잡하고 어려운 Redux 적응기&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;복잡하고 어려운 Redux 적응기 - 오픈소스컨설팅 테크블로그 %&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;안녕하세요 오픈소스컨설팅 Playce Dev 팀 Front-end 개발자 이정현입니다!이번에는 React 프로젝트를 개발하며, 규모가 확장됨에 따라 많아지는 데이터들을 어떻게 효율적으로 관리할지, 상태 관리를&quot; data-og-host=&quot;tech.osci.kr&quot; data-og-source-url=&quot;https://tech.osci.kr/%EB%B3%B5%EC%9E%A1%ED%95%98%EA%B3%A0-%EC%96%B4%EB%A0%A4%EC%9A%B4-redux-%EC%A0%81%EC%9D%91%EA%B8%B0/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yw6mV/hyW2Zjifqb/kzsM1Bqm98WPhb8h91HlNK/img.png?width=568&amp;amp;height=504&amp;amp;face=0_0_568_504,https://scrap.kakaocdn.net/dn/bnRw1p/hyW6J6W8Hr/udw1MKs2fZ9NFCNLdhlkI1/img.png?width=1024&amp;amp;height=553&amp;amp;face=0_0_1024_553,https://scrap.kakaocdn.net/dn/h6T0B/hyW6FQ1PW5/YXihiStGO48YAlb5Aci1KK/img.png?width=1024&amp;amp;height=331&amp;amp;face=0_0_1024_331&quot; data-og-url=&quot;https://tech.osci.kr/복잡하고-어려운-redux-적응기/&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/복잡하고-어려운-redux-적응기/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://tech.osci.kr/%EB%B3%B5%EC%9E%A1%ED%95%98%EA%B3%A0-%EC%96%B4%EB%A0%A4%EC%9A%B4-redux-%EC%A0%81%EC%9D%91%EA%B8%B0/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yw6mV/hyW2Zjifqb/kzsM1Bqm98WPhb8h91HlNK/img.png?width=568&amp;amp;height=504&amp;amp;face=0_0_568_504,https://scrap.kakaocdn.net/dn/bnRw1p/hyW6J6W8Hr/udw1MKs2fZ9NFCNLdhlkI1/img.png?width=1024&amp;amp;height=553&amp;amp;face=0_0_1024_553,https://scrap.kakaocdn.net/dn/h6T0B/hyW6FQ1PW5/YXihiStGO48YAlb5Aci1KK/img.png?width=1024&amp;amp;height=331&amp;amp;face=0_0_1024_331')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;복잡하고 어려운 Redux 적응기 - 오픈소스컨설팅 테크블로그 %&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;안녕하세요 오픈소스컨설팅 Playce Dev 팀 Front-end 개발자 이정현입니다!이번에는 React 프로젝트를 개발하며, 규모가 확장됨에 따라 많아지는 데이터들을 어떻게 효율적으로 관리할지, 상태 관리를&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;tech.osci.kr&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Zustand&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 Redux와 비슷한 중앙 집중식 상태 관리 아키텍처를 택하고 있습니다. 하지만 Redux와 다르게 Provider을 제공하지 않으며 더욱 간편하게 상태를 관리할 수 있다는 장점이 있습니다.&lt;br&gt;immer, persist 와 같은 유용한 미들웨어도 내장하고 있기 때문에 최근 시작된 프로젝트에서 많이 도입하고 있는 추세입니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/37&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;Hello Zustand&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Hello Zustand!&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Overview프론트엔드에서 상태관리는 중요합니다. 핵심적으로 상태를 통해서 변하는 값에 따라서 UI 를 동적으로 다룰수 있기 때문입니다.React 에서는 기본적으로 useState 라는 훅을 사용해 상태를 &quot; data-og-host=&quot;kangs-develop.tistory.com&quot; data-og-source-url=&quot;https://kangs-develop.tistory.com/37&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CF5Vu/hyW6L4LI9g/6k86A8VXBV5xVkx9knL3LK/img.png?width=800&amp;amp;height=389&amp;amp;face=0_0_800_389,https://scrap.kakaocdn.net/dn/b2we9q/hyW2O9Usz8/ZB6KlgJldWzJVVsiSiKnKk/img.png?width=800&amp;amp;height=389&amp;amp;face=0_0_800_389&quot; data-og-url=&quot;https://kangs-develop.tistory.com/37&quot;&gt;&lt;a href=&quot;https://kangs-develop.tistory.com/37&quot; target=&quot;_blank&quot; data-source-url=&quot;https://kangs-develop.tistory.com/37&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CF5Vu/hyW6L4LI9g/6k86A8VXBV5xVkx9knL3LK/img.png?width=800&amp;amp;height=389&amp;amp;face=0_0_800_389,https://scrap.kakaocdn.net/dn/b2we9q/hyW2O9Usz8/ZB6KlgJldWzJVVsiSiKnKk/img.png?width=800&amp;amp;height=389&amp;amp;face=0_0_800_389')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Hello Zustand!&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Overview프론트엔드에서 상태관리는 중요합니다. 핵심적으로 상태를 통해서 변하는 값에 따라서 UI 를 동적으로 다룰수 있기 때문입니다.React 에서는 기본적으로 useState 라는 훅을 사용해 상태를 &lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;kangs-develop.tistory.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Recoil&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Atom을 활용해 바텀업 패턴으로 상태를 관리하는 라이브러리 입니다. Meta에서 출시한 오픈소스 라이브러리 이지만 23년 4월 12일 이후로 유지 보수가 멈춘 상태입니다.&lt;br&gt;라이브러리 생명주기가 멈췄기 때문에 바텀-업 패턴의 상태관리 라이브러리를 찾는다면 Jotai가 좋은 선택일 것 같습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/recoil-state-management-of-react/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;&lt;b&gt;Recoil, 리액트의 상태관리 라이브러리&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Recoil, 리액트의 상태관리 라이브러리 - 오픈소스컨설팅 테크블로그 %&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Recoil 만을 위한 글이지만, 해당 기술을 탐구하기 전에 같은 문제를 해결하기 위해 사용되고 있는 라이브러리와 비교를 하는것은 상당히 중요한 일이라고 생각합니다.Frontend 개발을 하면서 state &quot; data-og-host=&quot;tech.osci.kr&quot; data-og-source-url=&quot;https://tech.osci.kr/recoil-state-management-of-react/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dFCco8/hyW2ZXVe80/3OK2jTkAlPwFg4Dcl86VZK/img.png?width=377&amp;amp;height=134&amp;amp;face=0_0_377_134,https://scrap.kakaocdn.net/dn/bUFGMO/hyW2VVvP7u/Tq5NUO9WBnh3KVmshuuHvK/img.png?width=1024&amp;amp;height=747&amp;amp;face=0_0_1024_747,https://scrap.kakaocdn.net/dn/rN2lg/hyW219hgIR/L31kOhbkHB7UFBK7pl9pfk/img.png?width=1024&amp;amp;height=553&amp;amp;face=0_0_1024_553&quot; data-og-url=&quot;https://tech.osci.kr/recoil-state-management-of-react/&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/recoil-state-management-of-react/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://tech.osci.kr/recoil-state-management-of-react/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dFCco8/hyW2ZXVe80/3OK2jTkAlPwFg4Dcl86VZK/img.png?width=377&amp;amp;height=134&amp;amp;face=0_0_377_134,https://scrap.kakaocdn.net/dn/bUFGMO/hyW2VVvP7u/Tq5NUO9WBnh3KVmshuuHvK/img.png?width=1024&amp;amp;height=747&amp;amp;face=0_0_1024_747,https://scrap.kakaocdn.net/dn/rN2lg/hyW219hgIR/L31kOhbkHB7UFBK7pl9pfk/img.png?width=1024&amp;amp;height=553&amp;amp;face=0_0_1024_553')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Recoil, 리액트의 상태관리 라이브러리 - 오픈소스컨설팅 테크블로그 %&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Recoil 만을 위한 글이지만, 해당 기술을 탐구하기 전에 같은 문제를 해결하기 위해 사용되고 있는 라이브러리와 비교를 하는것은 상당히 중요한 일이라고 생각합니다.Frontend 개발을 하면서 state &lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;tech.osci.kr&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Jotai&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Jotai는 Atom이란 원자적 상태를 활용해 상태를 바텀업 패턴으로 관리할 수 있습니다. 작은 원자단위로 정의한 상태를 컴포넌트에서 구독해 사용하는 방식으로 정말 간단하게 상태 관리를 사용할 수 있습니다.&lt;br&gt;Recoil과 같은 컨셉의 라이브러리지만 Recoil의 유지 보수가 멈췄다는 점으로 인해 수혜를 받고 있는 라이브러리 입니다. (단순히 Recoil의 대안이라는 점으로 급부상 한 것은 아닙니다.)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@tanstack/react-query&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 받아오는 데이터를 관리할 수 있는 라이브러리 입니다. 요즘은 React에서 사용하는 상태를 클라이언트 상태와 서버 상태로 구분하고는 하는데 구분의 시작점이 된 라이브러리가 react-query 라이브러리 입니다. 단순히 서버 패칭만을 쉽게 도와주는 라이브러리의 범주에서 벗어나 서버 상태라는 개념을 통해 네트워크 요청 최적화, 캐싱, 네트워크 로직 추상화 등 다양한 기능을 제공해주고 있습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/react-query/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;b&gt;React-Query 도입을 위한 고민 (feat. Recoil)&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;React-Query 도입을 위한 고민 (feat. Recoil) - 오픈소스컨설팅 테크블로그 - 강동희&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Web Frontend 개발을 할 때 React 를 사용하면서 마주하게 되는 여러 가지 문제점 중 하나는 state, 상태 관리에 관한 부분입니다. 프론트엔드 개발자라면 state 와 뗄 수 없는 인연을 맺고 있습니다.오늘&quot; data-og-host=&quot;tech.osci.kr&quot; data-og-source-url=&quot;https://tech.osci.kr/react-query/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eRaNu/hyW6JlzO7q/yKlkqyAICja5sSxTVKHVT1/img.png?width=1526&amp;amp;height=886&amp;amp;face=0_0_1526_886,https://scrap.kakaocdn.net/dn/rn4xW/hyW6J0bZ96/yilXQYzlpTewKhUiEj7Z5K/img.png?width=1440&amp;amp;height=720&amp;amp;face=632_195_751_325,https://scrap.kakaocdn.net/dn/b7r1Rf/hyW22AlrUD/mbdDxLeRGhQxUUbhMhex7k/img.png?width=888&amp;amp;height=719&amp;amp;face=0_0_888_719&quot; data-og-url=&quot;https://tech.osci.kr/react-query/&quot;&gt;&lt;a href=&quot;https://tech.osci.kr/react-query/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://tech.osci.kr/react-query/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eRaNu/hyW6JlzO7q/yKlkqyAICja5sSxTVKHVT1/img.png?width=1526&amp;amp;height=886&amp;amp;face=0_0_1526_886,https://scrap.kakaocdn.net/dn/rn4xW/hyW6J0bZ96/yilXQYzlpTewKhUiEj7Z5K/img.png?width=1440&amp;amp;height=720&amp;amp;face=632_195_751_325,https://scrap.kakaocdn.net/dn/b7r1Rf/hyW22AlrUD/mbdDxLeRGhQxUUbhMhex7k/img.png?width=888&amp;amp;height=719&amp;amp;face=0_0_888_719')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;React-Query 도입을 위한 고민 (feat. Recoil) - 오픈소스컨설팅 테크블로그 - 강동희&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Web Frontend 개발을 할 때 React 를 사용하면서 마주하게 되는 여러 가지 문제점 중 하나는 state, 상태 관리에 관한 부분입니다. 프론트엔드 개발자라면 state 와 뗄 수 없는 인연을 맺고 있습니다.오늘&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;tech.osci.kr&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;React의 상태 관리가 왜 어려울까 생각해보면 &lt;b&gt;결국은 랜더링과 직접적인 연관 관계가 있기 때문이지 않을까&lt;/b&gt;라고 생각하고 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;프론트엔드에서 성능 최적화를 위해서는 불필요한 랜더링은 반드시 제거해야 합니다. 그렇기 때문에 상태를 보다 더 엄격하게 관리해야 하고, 데이터가 단방향으로 위에서 아래로 흐르는 React의 특성 상 상태 관리를 위해서는 여러가지 기술을 추가로 학습해야 한다는 점이 상태 관리를 더욱 어렵게 하는 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이번 글을 통해서&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &lt;/span&gt;간단하게 상태 관리에 대해서 알아보았고 상태에 대해서 조금 더 깊게 생각해볼 수 있었습니다. 읽어봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>react</category>
      <category>state</category>
      <category>Web</category>
      <category>상태관리</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/52</guid>
      <comments>https://kangs-develop.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 19 Sep 2024 13:43:44 +0900</pubDate>
    </item>
    <item>
      <title>글</title>
      <link>https://kangs-develop.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;지난 5월에 블로그를 이전하고 벌써 4개월이 지났다.&lt;br&gt;이전 블로그인 벨로그에서 약 50개의 글을 작성했었고, 예전 블로그인 네이버 블로그에서는 약 200개의 글을 작성했으니&lt;br&gt;개발 공부를 시작한 이래로 거의 300개에 가까운 글을 작성한 것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;물론 공부를 하고 흔적을 남기기 위해 작성한 글들도 많고 글의 목적이 개발이 아닌 글들도 여러 건들이 되지만 그동안 상당히 많은 글들을 작성했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;오늘 문득 나는 왜 글을 작성하고 있을까 라는 생각이 들었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금 운영중인 블로그는, &lt;b&gt;주니어 프론트엔드 개발자의 등대 &lt;/b&gt;가 되고 싶다는 목적 하에 운영하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;물론 나도 아직은 주니어 개발자이지만, 어려움을 겪고 있는 주니어 개발자나, 신입 개발자 혹은 개발 지망생 분들께 많은 도움이나 인사이트를 주고 싶다는 생각이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;개발자가 되기 이전부터 나는 글을 쓰는 것을 좋아했다. 개발과는 관련 없는 시와 에세이도 여러 건 집필했던 적도 있고, 군생활을 할 적에는 소설도 써봤을 정도로 글을 작성하는 것을 좋아한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;글을 사랑하는 마음이 개발과 접목이 되어 요즘은 개발에 관련된 정보성 글을 작성하고 사람들이 내 글을 읽어보는 것에 즐거움을 느끼고 있는것 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;글을 작성하면서도 항상 걱정되고 두려운 부분은 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;과연 내가 작성하는 정보가 옳은 정보일지에 대한 부분이다. 요즘 세상은 정보화 사회이다. 구글에 원하는 검색어만 입력하면 관련된 정보들을 확인할 수 있고 정보를 검증하는 역할은 소비자의 몫이다. 만약 해당 정보에 지식이 없는 소비자라면 그릇된 정보를 옳은 정보라고 판단하고 그대로 학습하게 된다. 그렇기 때문에 글을 작성하는 사람은 윤리적으로라도 옳은 정보를 제공해야 한다고 생각한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇기 때문에 항상 글을 작성하면서도 두렵다. 내가 옳지 않은 정보를 제공하게 될까봐 한번 더 검색하고 학습한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그럼에도 나의 이런 마음으로 한번 더 찾아보고 옳은 정보를 제공하여 나 또한 성장하고 있고, 나의 글로 인해 도움을 받게 되는 여러 사람들을 보면 항상 행복한 마음이 든다.&lt;br&gt;&amp;nbsp;&lt;br&gt;앞으로도 나는 계속 글을 작성할 것이다. 혹자는 이직 시장 때문에 글을 작성하는것은 아니냐는 생각이 들 수 있지만, 나의 목표는 나의 즐거움과 사람들에게 좋은 정보를 제공하는 것 이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;마지막으로 예전에 작성해본 시 한 편을 남겨본다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;두부&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;안녕&lt;br&gt;뽀아얀 두부야&lt;br&gt;너에게 누구냐고 묻는다면&lt;br&gt;나는 담백한 두부야&lt;br&gt;라고 대답하겠지&lt;br&gt;&lt;br&gt;두부는 탁하지만&lt;br&gt;투명하지는 않지만&lt;br&gt;담백한 맛이 난다&lt;br&gt;&lt;br&gt;근데 왜&lt;br&gt;맑은 물에 비친 나는&lt;br&gt;맑다못해 속까지 보이는가&lt;br&gt;&lt;br&gt;두부가 되자&lt;br&gt;투명하진 않지만&lt;br&gt;그 속에 무엇인가&lt;br&gt;가득하다 못해 탁해진&lt;br&gt;그 속내까지 담백한 두부가 되자&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>이것저것</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/51</guid>
      <comments>https://kangs-develop.tistory.com/51#entry51comment</comments>
      <pubDate>Sat, 14 Sep 2024 13:21:18 +0900</pubDate>
    </item>
    <item>
      <title>[소프트웨어] 리팩토링 글을 읽고</title>
      <link>https://kangs-develop.tistory.com/50</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;금주&amp;nbsp;9일에&amp;nbsp;Korean&amp;nbsp;Article에서&amp;nbsp;리팩토링에&amp;nbsp;관한&amp;nbsp;좋은&amp;nbsp;번역&amp;nbsp;글을&amp;nbsp;발행했습니다. &lt;br /&gt;글을&amp;nbsp;읽으면서&amp;nbsp;공감되는&amp;nbsp;부분들이&amp;nbsp;많이&amp;nbsp;있었고,&amp;nbsp;좋은&amp;nbsp;인사이트를&amp;nbsp;얻은&amp;nbsp;내용도&amp;nbsp;있었습니다. &lt;br /&gt;지난 몇 달 동안 리팩토링에 대한 업무를 주로 진행하면서 개인적으로 리팩토링에 대해 느꼈던 생각을 남겨보도록 하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://ykss.netlify.app/translation/good_refactoring_vs_bad_refactoring/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;(번역) 좋은 리팩터링 vs 나쁜 리팩터링&lt;/b&gt;&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리팩토링&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;소프트웨어&amp;nbsp;공학에서&amp;nbsp;리팩토링은&amp;nbsp;&lt;b&gt;겉보기&amp;nbsp;동작은&amp;nbsp;그대로&amp;nbsp;유지한&amp;nbsp;채,&amp;nbsp;코드를&amp;nbsp;이해하고&amp;nbsp;수정하기&amp;nbsp;쉽도록&amp;nbsp;내부&amp;nbsp;구조를&amp;nbsp;변경하는&amp;nbsp;기법&lt;/b&gt;&amp;nbsp;이라고&amp;nbsp;정의하고&amp;nbsp;있습니다. &lt;br /&gt;리팩토링의&amp;nbsp;핵심은&amp;nbsp;&lt;b&gt;본래의&amp;nbsp;기능은&amp;nbsp;유지하며&amp;nbsp;소스&amp;nbsp;코드를&amp;nbsp;여러&amp;nbsp;관점에&amp;nbsp;의해&amp;nbsp;변경하는&amp;nbsp;것&lt;/b&gt;인데,&amp;nbsp;여기서의&amp;nbsp;여러&amp;nbsp;관점은&amp;nbsp;협업,&amp;nbsp;유지&amp;nbsp;보수,&amp;nbsp;추가&amp;nbsp;기능에&amp;nbsp;의한&amp;nbsp;리팩토링&amp;nbsp;등&amp;nbsp;이&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제로 리팩토링을 하면서 가장 중요한 것은 리팩토링으로 인한 사이드 이펙트를 방지하는 것인데, 잘못된 리팩토링으로 인한 사이드 이펙트가 발생한다면 잘 돌아가는 프로덕트에 이슈를 야기하여 소프트웨어의 신뢰성이 저하될 가능성이 높아 상당히 주의해야 합니다. &lt;br /&gt;잘&amp;nbsp;계획된&amp;nbsp;리팩토링과&amp;nbsp;테스트를&amp;nbsp;통과한&amp;nbsp;리펙토링은&amp;nbsp;유지&amp;nbsp;보수를&amp;nbsp;편리하게&amp;nbsp;하고&amp;nbsp;프로젝트의&amp;nbsp;수명을&amp;nbsp;늘려주기&amp;nbsp;때문에&amp;nbsp;장기적으로&amp;nbsp;유지중인&amp;nbsp;프로젝트라면&amp;nbsp;반드시&amp;nbsp;고려해보아야&amp;nbsp;합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리팩토링에&amp;nbsp;있어서&amp;nbsp;느꼈던&amp;nbsp;주의점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;계획은&amp;nbsp;철저하게&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리팩토링을&amp;nbsp;진행하기에&amp;nbsp;앞서&amp;nbsp;계획을&amp;nbsp;철저하게&amp;nbsp;세워야합니다. &lt;br /&gt;계획&amp;nbsp;없이&amp;nbsp;코드를&amp;nbsp;확인하고&amp;nbsp;수정한다면&amp;nbsp;기한&amp;nbsp;없이&amp;nbsp;계속&amp;nbsp;진행되는&amp;nbsp;리팩토링이&amp;nbsp;될&amp;nbsp;확률이&amp;nbsp;높을&amp;nbsp;뿐더러&amp;nbsp;결국엔&amp;nbsp;다시&amp;nbsp;원래의&amp;nbsp;코드로&amp;nbsp;롤백하는&amp;nbsp;경우가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있습니다. &lt;br /&gt;&lt;b&gt;계획을&amp;nbsp;철저하게&amp;nbsp;세우고,&amp;nbsp;세운&amp;nbsp;계획을&amp;nbsp;바탕으로&amp;nbsp;이슈&amp;nbsp;단위로&amp;nbsp;리팩토링&lt;/b&gt;을&amp;nbsp;진행한다면&amp;nbsp;훨씬&amp;nbsp;생산성&amp;nbsp;있는&amp;nbsp;리팩토링이&amp;nbsp;될&amp;nbsp;것&amp;nbsp;입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사이드&amp;nbsp;이펙트&amp;nbsp;범위&amp;nbsp;예측하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리팩토링을&amp;nbsp;진행하기에&amp;nbsp;앞서&amp;nbsp;&lt;b&gt;변경할&amp;nbsp;코드의&amp;nbsp;단위를&amp;nbsp;계획하고&amp;nbsp;변경할&amp;nbsp;코드가&amp;nbsp;미치는&amp;nbsp;영향&lt;/b&gt;에&amp;nbsp;대해서&amp;nbsp;생각해봐야&amp;nbsp;합니다. &lt;br /&gt;리팩토링은 기능은 유지한 채 코드를 변경하는 과정인데, 사이드 이펙트의 범위를 예측하지 못한채 리팩토링을 진행한다면 엉뚱한 곳에서 이슈가 생길 위험이 있습니다. &lt;br /&gt;예측 커버리지가 100%가 되지 않는다면 진행중인 리팩토링을 멈추는 것이 좋습니다. 내가 진행한 리팩토링으로 인해 이슈가 생겨, 해당 이슈로 인해 고객의 신뢰도를 저하할 수 있기 때문입니다. &lt;br /&gt;항상&amp;nbsp;리팩토링을&amp;nbsp;진행하기에&amp;nbsp;앞서&amp;nbsp;사이드&amp;nbsp;이펙트의&amp;nbsp;범주를&amp;nbsp;예측하고,&amp;nbsp;내가&amp;nbsp;예측하기&amp;nbsp;힘든&amp;nbsp;범주라면&amp;nbsp;기존의&amp;nbsp;팀원들과&amp;nbsp;상의하에&amp;nbsp;코드&amp;nbsp;변경이&amp;nbsp;미칠&amp;nbsp;영향도를&amp;nbsp;체크하시길&amp;nbsp;바랍니다!&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트가&amp;nbsp;가능한&amp;nbsp;범위&amp;nbsp;내에&amp;nbsp;리팩토링&amp;nbsp;하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리팩토링을&amp;nbsp;진행한&amp;nbsp;이후&amp;nbsp;&lt;b&gt;반드시&amp;nbsp;기능&amp;nbsp;테스트를&amp;nbsp;진행&lt;/b&gt;해야 합니다. 만약 테스트가 불가능 하다면 리팩토링을 멈춰야 합니다. 프로덕션에 올린다면 어떤 이슈가 생길지 예측할 수 없어서 위험하기 때문입니다. &lt;br /&gt;가령&amp;nbsp;테스트&amp;nbsp;데이터가&amp;nbsp;없어서&amp;nbsp;테스트가&amp;nbsp;불가하다면,&amp;nbsp;리팩토링을&amp;nbsp;진행한&amp;nbsp;코드를&amp;nbsp;main&amp;nbsp;브랜치에&amp;nbsp;병합하는&amp;nbsp;과정을&amp;nbsp;멈춰주세요.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;코드를 수정하고 기능이 동작하지 않는다면, 이는 이슈로 나올 확률이 농후합니다. 내가 생각했을 때 이 정도는 괜찮겠지? 라는 판단은 절대 금물입니다. 사양서(소프트웨어 기능 명세서)와 일치하지 않는 기능은 QA 후 이슈로 나올 확률이 높습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반드시 테스트가 가능한 범위 내에서 리팩토링을 진행해주세요!! 이슈를 생산하는 개발자가 되지 말자는 경험에서 나왔습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;큰&amp;nbsp;단위의&amp;nbsp;리팩토링을&amp;nbsp;지양하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;공유&amp;nbsp;드린&amp;nbsp;아티클에도&amp;nbsp;나와있지만&amp;nbsp;리팩토링은&amp;nbsp;&lt;b&gt;가급적&amp;nbsp;작은&amp;nbsp;단위로&amp;nbsp;리팩토링&lt;/b&gt;을&amp;nbsp;해야합니다. &lt;br /&gt;리팩토링의&amp;nbsp;단위가&amp;nbsp;커져&amp;nbsp;작은&amp;nbsp;코드&amp;nbsp;집합,&amp;nbsp;프로젝트를&amp;nbsp;구성하는&amp;nbsp;작은&amp;nbsp;모듈&amp;nbsp;단위가&amp;nbsp;아닌&amp;nbsp;프로젝트&amp;nbsp;전역에&amp;nbsp;영향을&amp;nbsp;끼치는&amp;nbsp;리팩토링&amp;nbsp;이라면,&amp;nbsp;이는&amp;nbsp;코드의&amp;nbsp;리팩토링&amp;nbsp;보다는&amp;nbsp;새로운&amp;nbsp;기능&amp;nbsp;단위의&amp;nbsp;개발,&amp;nbsp;고도화&amp;nbsp;혹은&amp;nbsp;개선&amp;nbsp;작업이&amp;nbsp;아닐까&amp;nbsp;라는&amp;nbsp;생각이&amp;nbsp;듭니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레거시를&amp;nbsp;존중하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레거시에는&amp;nbsp;이유가&amp;nbsp;있습니다.&lt;/b&gt;&amp;nbsp;그때&amp;nbsp;당시&amp;nbsp;그렇게&amp;nbsp;해야만&amp;nbsp;했던&amp;nbsp;이유가&amp;nbsp;있을수도&amp;nbsp;있으니&amp;nbsp;히스토리가&amp;nbsp;남겨져&amp;nbsp;있다면&amp;nbsp;반드시&amp;nbsp;히스토리를&amp;nbsp;추적해보도록&amp;nbsp;합시다. &lt;br /&gt;레거시를&amp;nbsp;무시한&amp;nbsp;채&amp;nbsp;리팩토링을&amp;nbsp;진행한다면&amp;nbsp;그&amp;nbsp;리팩토링은&amp;nbsp;프로젝트에&amp;nbsp;어울리지&amp;nbsp;않는&amp;nbsp;리팩토링이&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다. &lt;br /&gt;이는&amp;nbsp;리팩토링을&amp;nbsp;지양하라는&amp;nbsp;말이&amp;nbsp;아닌,&amp;nbsp;히스토리를&amp;nbsp;정확히&amp;nbsp;이해하고&amp;nbsp;리팩토링에&amp;nbsp;들어가야&amp;nbsp;한다는&amp;nbsp;의미입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;일례&lt;/b&gt;로&amp;nbsp;최근&amp;nbsp;프로젝트에서&amp;nbsp;jQuery를&amp;nbsp;걷어내려는&amp;nbsp;작업을&amp;nbsp;했었습니다. &lt;br /&gt;프로젝트에&amp;nbsp;처음&amp;nbsp;투입되고&amp;nbsp;React&amp;nbsp;프로젝트에&amp;nbsp;jQuery,&amp;nbsp;jQuery-ui&amp;nbsp;라이브러리의&amp;nbsp;번들&amp;nbsp;패키지가&amp;nbsp;소스&amp;nbsp;디렉토리&amp;nbsp;하위에&amp;nbsp;위치하고&amp;nbsp;해당&amp;nbsp;라이브러리를&amp;nbsp;직접&amp;nbsp;사용하는&amp;nbsp;것을&amp;nbsp;보고&amp;nbsp;기괴하다는&amp;nbsp;생각을&amp;nbsp;했습니다. &lt;br /&gt;하여&amp;nbsp;해당&amp;nbsp;레거시를&amp;nbsp;걷어내는&amp;nbsp;작업을&amp;nbsp;진행했는데,&amp;nbsp;코드를&amp;nbsp;분석해보니&amp;nbsp;jQuery를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;밖에&amp;nbsp;없는&amp;nbsp;이유가&amp;nbsp;존재했습니다. &lt;br /&gt;해당&amp;nbsp;프로젝트에서는&amp;nbsp;vmware&amp;nbsp;console&amp;nbsp;이라는&amp;nbsp;기능을&amp;nbsp;제공하고&amp;nbsp;있었는데,&amp;nbsp;vsphere의&amp;nbsp;sdk가&amp;nbsp;jQuery,&amp;nbsp;jQuery-ui를&amp;nbsp;사용하고&amp;nbsp;있었기&amp;nbsp;때문에&amp;nbsp;해당&amp;nbsp;라이브러리를&amp;nbsp;소스&amp;nbsp;디렉토리에서&amp;nbsp;사용하고&amp;nbsp;있었던&amp;nbsp;것&amp;nbsp;입니다. &lt;br /&gt;결국&amp;nbsp;의존성이&amp;nbsp;강하게&amp;nbsp;얽매여&amp;nbsp;있었기&amp;nbsp;때문에&amp;nbsp;jQuery&amp;nbsp;자체를&amp;nbsp;걷어낼&amp;nbsp;수&amp;nbsp;없다고&amp;nbsp;판단이&amp;nbsp;들었고,&amp;nbsp;라이브러리&amp;nbsp;자체는&amp;nbsp;걷어내고&amp;nbsp;정적&amp;nbsp;assest&amp;nbsp;폴더&amp;nbsp;하위에&amp;nbsp;미니멈하게&amp;nbsp;패키징&amp;nbsp;된&amp;nbsp;파일&amp;nbsp;하나를&amp;nbsp;남겨두고&amp;nbsp;html&amp;nbsp;파일에서&amp;nbsp;script로&amp;nbsp;로딩&amp;nbsp;후&amp;nbsp;windows&amp;nbsp;객체의&amp;nbsp;프로퍼티로&amp;nbsp;jQuery를&amp;nbsp;바인딩해&amp;nbsp;사용하는&amp;nbsp;방식으로&amp;nbsp;변경했습니다. &lt;br /&gt;소스&amp;nbsp;디렉토리&amp;nbsp;자체에서&amp;nbsp;무거웠던&amp;nbsp;jQuery,&amp;nbsp;jQuery-ui&amp;nbsp;라이브러리를&amp;nbsp;걷어내&amp;nbsp;코드&amp;nbsp;베이스의&amp;nbsp;용량과&amp;nbsp;번들&amp;nbsp;사이즈를&amp;nbsp;줄였고,&amp;nbsp;모듈화를&amp;nbsp;통해&amp;nbsp;코드의&amp;nbsp;가독성을&amp;nbsp;높였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해당&amp;nbsp;리팩토링을&amp;nbsp;진행하며&amp;nbsp;들었던&amp;nbsp;생각은&amp;nbsp;레거시도&amp;nbsp;이유가&amp;nbsp;있다는&amp;nbsp;점&amp;nbsp;이였습니다.&amp;nbsp;그렇게&amp;nbsp;작성해야만&amp;nbsp;했던&amp;nbsp;이유를&amp;nbsp;정확히&amp;nbsp;캐치하고&amp;nbsp;리팩토링을&amp;nbsp;진행해야&amp;nbsp;프로젝트에&amp;nbsp;어울리는&amp;nbsp;리팩토링을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있구나라는&amp;nbsp;생각도&amp;nbsp;들었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컨벤션을&amp;nbsp;존중하기&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;팀의 컨벤션이 있다면 컨벤션을 존중하여 리팩토링을 진행해야 합니다. 이는&amp;nbsp;공유드린&amp;nbsp;아티클의&amp;nbsp;1번&amp;nbsp;항목으로&amp;nbsp;작성해주신&amp;nbsp;맥락과&amp;nbsp;일치합니다. &lt;br /&gt;새로 시작하는 프로젝트가 아닌 팀 단위로 진행중인 프로젝트라면 팀의 컨벤션이 존재할 수 있습니다. 팀의&amp;nbsp;컨벤션과&amp;nbsp;팀에서&amp;nbsp;채택한&amp;nbsp;프로그래밍&amp;nbsp;방법론을&amp;nbsp;존중해야&amp;nbsp;좋은&amp;nbsp;리팩토링을&amp;nbsp;진행할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 리팩토링을 진행하기 전 특별한 팀의 컨벤션이 존재하지 않는다면, 컨벤션을 먼저 정립하는 것도 고민해보면 좋을 것 같습니다. 컨벤션이&amp;nbsp;없는&amp;nbsp;채로&amp;nbsp;리팩토링을&amp;nbsp;한다면&amp;nbsp;해당&amp;nbsp;코드는&amp;nbsp;추후&amp;nbsp;다시&amp;nbsp;리팩토링&amp;nbsp;할&amp;nbsp;확률이&amp;nbsp;높기&amp;nbsp;떄문입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;리팩토링&amp;nbsp;업무는&amp;nbsp;소프트웨어&amp;nbsp;엔지니어라면&amp;nbsp;항상&amp;nbsp;가슴속에&amp;nbsp;얹고가는&amp;nbsp;업무입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;새로운&amp;nbsp;기능을&amp;nbsp;개발하다보면&amp;nbsp;데드라인의&amp;nbsp;임박,&amp;nbsp;현재&amp;nbsp;기술의&amp;nbsp;한계&amp;nbsp;등&amp;nbsp;여러가지&amp;nbsp;이유로&amp;nbsp;기술부채가&amp;nbsp;쌓이기&amp;nbsp;마련이고, &lt;br /&gt;기술부채를&amp;nbsp;언젠가는&amp;nbsp;청산해야&amp;nbsp;하고,&amp;nbsp;리팩토링도&amp;nbsp;기술부채의&amp;nbsp;일환이기&amp;nbsp;때문이죠.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그간 여러건의 리팩토링을 진행하며 이슈를 생산하는 개발자는 되지 말자 라는 생각을 마음속에 각인해놓고 업무에 임하고 있지만,&amp;nbsp; 항상 사이드 이펙트의 범주를 정확히 예측하고 리팩토링을 진행하는 것은 정말 어려운 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;팀&amp;nbsp;동료들에게&amp;nbsp;많은&amp;nbsp;도움을&amp;nbsp;받고&amp;nbsp;있기도&amp;nbsp;하고,&amp;nbsp;좋은&amp;nbsp;팀&amp;nbsp;동료가&amp;nbsp;복지라는&amp;nbsp;것도&amp;nbsp;여기서&amp;nbsp;느껴지는&amp;nbsp;것&amp;nbsp;같습니다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;읽어봐주셔서&amp;nbsp;감사합니다.&lt;/p&gt;</description>
      <category>이것저것</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/50</guid>
      <comments>https://kangs-develop.tistory.com/50#entry50comment</comments>
      <pubDate>Thu, 12 Sep 2024 11:19:28 +0900</pubDate>
    </item>
    <item>
      <title>pnpm 톺아보기</title>
      <link>https://kangs-develop.tistory.com/49</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 7월 프로젝트의 프론트엔드를 개편하는 과정에서 패키지 매니저를 Yarn berry에서 pnpm으로 교체를 진행했습니다.&lt;br /&gt;그 당시 교체를 선택한 이유는 몇 가지가 있었는데, 이번 포스팅에서는 pnpm을 학습했던 내용을 기록하고 교체를 진행했었던 이유를 남겨보려고 합니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;pNpM&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 pNpM에 대해서 살펴봅시다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;왜 pnpm을 pNpM이라고 했는지 간단하게 살펴보자면, 홈페이지에서 새로고침을 할 때 마다 텍스트가 랜덤하게 변하는 것을 확인 했습니다. 딱히 정해져 있지 않은 것인가 해서 어느 순번에 나타난 pNpM을 살펴보려고 합니다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DIUb4/btsJvqNPjDI/sArJAVFETq2Q5nJdIFZtS1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DIUb4/btsJvqNPjDI/sArJAVFETq2Q5nJdIFZtS1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DIUb4/btsJvqNPjDI/sArJAVFETq2Q5nJdIFZtS1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/DIUb4/btsJvqNPjDI/sArJAVFETq2Q5nJdIFZtS1/img.gif&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;543&quot; height=&quot;287&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;pnpm은 perfomant npm 의 약자로써 말 그대로 향상된 npm입니다. 그럼 어떤 장점이 있길래 향상된 npm 이라고 하는 것 일까요?&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;전역 스토어, 가상 스토어, node_modules를 활용한 디스크 효율성, I/O 퍼포먼스 개선&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 pnpm의 원리를 살펴봅시다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;pnpm을 통해 최초로 라이브러리를 설치하게 된다면 해당 라이브러리는 디스크에 단 하나만 존재하는 저장소(Content-addressable store)에 저장됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저장소의 모습을 살펴볼까요? 터미널을 열어서 디스크에 저장된 저장소의 위치를 확인해봅시다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;pnpm store path&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;저는 다음의 위치를 얻었습니다. (/Users/thomas/.pnpm-store/v3)&amp;nbsp; 스토어로 이동해봅시다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;cd /Users/thomas/.pnpm-store/v3&lt;/code&gt;&lt;/pre&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;1068&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yfPvI/btsJt9T7LZI/NorZHdFArkf6xeUWZ1D7Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yfPvI/btsJt9T7LZI/NorZHdFArkf6xeUWZ1D7Uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yfPvI/btsJt9T7LZI/NorZHdFArkf6xeUWZ1D7Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyfPvI%2FbtsJt9T7LZI%2FNorZHdFArkf6xeUWZ1D7Uk%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;671&quot; height=&quot;92&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;files 라는 폴더가 존재합니다. 해당 폴더로 이동을 하면 숫자와 해쉬 값으로 된 폴더들이 존재합니다. 해당 폴더들은 그동안 다운로드 받았던 폴더의 목록입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제 프로젝트의 node_modules 폴더에는 .pnpm이라는 가상 스토어가 존재하고, 해당 가상 스토어에는 설치한 라이브러리 패키지에서 사용하는 의존성들을 저장소에 저장된 패키지들을 버전별로 하드링크해 사용합니다. 아래 스크린샷을 살펴보면 node_modules 폴더 하위의 .pnpm 이라는 가상 저장소에서 스토어에 설치된 패키지를 하드링크한 라이브러리 목록이 보입니다. 해당 라이브러리들은 라이브러리와 버전 별로 하드링크 되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;1446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LQ5us/btsJuenzz4Q/8ozoKhX2O7JhnkKvIQ0Bs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LQ5us/btsJuenzz4Q/8ozoKhX2O7JhnkKvIQ0Bs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LQ5us/btsJuenzz4Q/8ozoKhX2O7JhnkKvIQ0Bs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLQ5us%2FbtsJuenzz4Q%2F8ozoKhX2O7JhnkKvIQ0Bs1%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;391&quot; height=&quot;525&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;1446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그리고 실제 node_modules 폴더에서는 사용하는 의존성(라이브러리)를 평탄하지 않게 사용하고 있습니다. node_modules에서는 가상 저장소에 저장된 라이브러리를 소프트 링크(심볼릭 링크)로 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JBR0m/btsJv9dNjkf/kN9ecZKE74q5fydVKKBYb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JBR0m/btsJv9dNjkf/kN9ecZKE74q5fydVKKBYb1/img.png&quot; data-alt=&quot;vscode에서는 라이브러리의 우측에 링크 표시를 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JBR0m/btsJv9dNjkf/kN9ecZKE74q5fydVKKBYb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJBR0m%2FbtsJv9dNjkf%2FkN9ecZKE74q5fydVKKBYb1%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;462&quot; height=&quot;291&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;vscode에서는 라이브러리의 우측에 링크 표시를 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;정리하자면 pnpm은 저장소, 가상 저장소, node_modules 를 활용해 라이브러리를 관리합니다. 실제 바이너리 파일들은 전역 저장소에 저장이 되며, 가상 저장소(.pnpm)는 저장소에 저장된 &lt;b&gt;라이브러리 파일을 하드 링크를 활용해&lt;/b&gt; 저장합니다.&lt;br /&gt;프로젝트의 node_modules는 가상 저장소에 &lt;b&gt;하드 링크된 라이브러리를 소프트 링크(심볼릭 링크)를 통해&lt;/b&gt; 라이브러리를 사용하게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하드 링크된 라이브러리들은 가상 저장소에서 버전별로 평탄하게 관리가 되고, node_modules에서 해당 의존성들을 심볼릭 링크로 사용해 디스크를 효율적으로 관리하며, 하드 링크와 소프트 링크 (링크 시스템)를 함께 활용해 디스크 I/O 퍼포먼스를 향상 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;하드링크&lt;/b&gt;&lt;br /&gt;사본과 같은 역할을 한다. 원본 파일이 삭제되어도 하드링크에는 해당 파일의 데이터가 포함되어 있어서 데이터에 엑세스가 가능하다. &lt;b&gt;원본 파일과 같은 inode를 가진다.&lt;/b&gt; 그래서 여러 하드링크를 만들더라도 디스크 용량을 차지하지 않는다. 디렉토리에는 하드링크를 허용하지 않는다.&lt;br /&gt;&lt;br /&gt;inode란?&lt;br /&gt;inode는 index node의 줄임말로서 리눅스 파일 시스템에서 파일이나 디렉토리의 메타 데이터를 저장하는데 사용된다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;소프트 링크(심볼링 링크)&lt;/b&gt;&lt;br /&gt;윈도우의 바로가기와 같은 개념 (포인터의 개념)이다. 주소를 가리키고 있기 때문에 원본 노드가 삭제될 경우 동작이 불가능하다. 하드링크와 다르게 &lt;b&gt;원본과 다른 inode&lt;/b&gt;를 가지며 디렉토리에도 소프트링크를 사용할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;엄격한 의존성 관리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 pnpm이 패키지를 관리하는 방법을 살펴봤습니다. 가상 스토에에서 버전 별로 의존성을 평탄하게 관리하며 node_modules 에서는 의존성을 평탄하게 관리하지 않고 사용하는 버전을 명확하게 합니다.&lt;br /&gt;이로 인해서 의존성 관리를 엄격하게 할 수 있으며, npm과 yarn classic에서 발생했던 팬텀 디펜던시 문제를 해결할 수 있게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;i&gt;&lt;b&gt;간단하게 팬텀 디펜던시에 대해서 알아봅시다.&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;npm v3 이하에서는 사용하는 패키지를 node_modules에서 계층적으로 관리했습니다. 만약 a라는 패키지가 b라는 패키지를 사용하고 b 패키지에서는 c패키지를 사용한다면 아래와 같은 경로가 만들어집니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;.node_modules/a/.node_modules/b/.node_modules/c/.node_modules .....&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;실제로 이렇게 깊게 들어가는 계층 구조로 인해 디스크의 저장 효율성과 I/O 퍼포먼스가 심각하게 저해 됐습니다. Windows 파일 시스템에서는 경로의 길이를 260자로 제한하고 있다는 문제점도 있었습니다. 하여 npm v3 이후부터는 프로젝트에서 사용하는 의존성을 node_modules 폴더의 루트에서 평탄하게 관리하기로 했습니다. 이를 의존성 목록을 끌어올리는 듯한 형상으로 호이스팅이라고 부르게 됐습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;호이스팅으로 인해 디스크 효율성과 I/O 문제는 어느정도 해결을 했지만 다른 문제가 발생했습니다. 실제로 설치하지 않은 버전, 설치하지 않는 라이브러리를 사용할 수 있게 됐다는 점 입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;a라는 패키지를 설치했는데, a 패키지는 b 패키지를 의존성으로 사용하고 있습니다. 하여 node_modules 루트에 b 패키지도 설치가 됐습니다. 프로젝트를 진행하는 도중 설치하지 않은 b 패키지를 발견할 수 있습니다. 또한 a 패키지에서 import 해야 하는 것을 이름이 같아서 b 패키지에서 import 하는 케이스도 발생합니다. 프로젝트가 진행되면서 a 패키지를 사용하지 않게 됐고, a 패키지를 삭제합니다. 이때 b 패키지도 함께 삭제가 되면서 b 패키지에서 import 했던 코드가 에러를 유발합니다. 이를 유령 의존성(설치하지 않는 패키지에 디펜던시가 생겨) 팬텀 디펜던시 라고 부릅니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;pnpm에서는 계층적으로 의존성을 관리하고 버전별로 의존성을 엄격하게 관리하기 때문에 위 팬텀 디펜던시 문제를 해결&lt;/b&gt;할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쉽게 적용하는 모노레포&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm, Yarn classic, Yarn berry 모두 모노레포를 위한 workspace 기능을 제공하고 있습니다.&lt;br /&gt;강의를 통해 세 패키지 매니저의 workspace를 살펴봤지만, 가장 간편하다고 느꼈던 패키지 매니저는 pnpm 입니다.&lt;br /&gt;pnpm을 활용해 모노레포를 셋팅하려면 pnpm-workspace.yaml 파일만 프로젝트의 루트에 만들어주면 됩니다. 아래와 같이 yaml 파일에 패키지를 명시해주면 모노레포를 간단하게 셋팅할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;packages:
&amp;nbsp;&amp;nbsp;- &quot;apps/*&quot;
&amp;nbsp;&amp;nbsp;- &quot;packages/*&quot;
&amp;nbsp;&amp;nbsp;- &quot;server&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;pnpm에서는 --filter 옵션을 활용한다면 모노레포를 쉽게 제어할 수 있다는 장점도 존재합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;pnpm --filter @career-up/edu add packages
pnpm --filter @career-up/ui-kit build
pnpm --filter @career-up/shell dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Yarn Berry &amp;gt; Pnpm&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm에 대해 간단하게 살펴봤습니다. 그럼 프로젝트에서 패키지 매니저를 Yarn berry에서 pnpm으로 교체한 이유를 간단하게 살펴보겠습니다. 저희 프로젝트에서는 Yarn Berry pnp와 zero install 기능을 사용하고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOd23s/btsJvlTgSNQ/njHG9NjHbzztOvVAgRtzg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOd23s/btsJvlTgSNQ/njHG9NjHbzztOvVAgRtzg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOd23s/btsJvlTgSNQ/njHG9NjHbzztOvVAgRtzg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOd23s%2FbtsJvlTgSNQ%2FnjHG9NjHbzztOvVAgRtzg1%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;453&quot; height=&quot;242&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로젝트가 진행될수록 깃에 가해지는 부하&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yarn berry PNP를 사용하면서 Zero Install 기능을 사용해 깃을 통해 의존성 관리를 하고 있었습니다.&lt;br /&gt;이는 의존성을 zip파일을 활용해 깃에서 코드와 함께 형상 관리를 하는 기능입니다. 진행중인 프로젝트는 5년이 넘게 유지보수 중인 오랫동안 진행된 프로젝트 입니다. 그동안 다양한 패키지들을 설치하면서 코드 베이스 용량이 커져가고 깃에 저장된 의존성 덩어리가 비대해져 깃을 통해 프로젝트를 형상 관리하는데 어려움을 겪기 시작했습니다.&lt;br /&gt;또한 Zero Install을 지원하지 않는 패키지들도 존재했고 이로 인해서 완벽한 Zero Install이 어렵다는 결과도 확인을 했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;러닝 커브&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yarn Berry의 PNP 동작 방식은 기존 패키지 매니저인&amp;nbsp;&amp;nbsp;npm, yarn classic과는 다릅니다. 패키지 매니저의 동작 방식을 이해하려면 상당 시간을 투자해야 한다는 점이 있었고 이는 프로젝트를 진행하는 인원들에게 부담을 주고 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;pnpm의 편리함&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 대안이 주는 편리함이 교체의 이유가 될 수 있을까요? 패키지 매니저를 교체할때 쯤 pnpm의 매력에 빠져가고 있었습니다. 너무 간단했고 퍼포먼스 또한 훌륭했습니다. 어차피 프로젝트를 전반적으로 개선하는 과정에서 쉽게 교체할 수 있다면 교체하지 않을 이유가 없다고 생각했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;모노레포의 도입&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 프로젝트는 추후 모노레포와 프로젝트 아키텍처의 구조를 MFA로 변경할 목표를 세우고 있었습니다. 모노레포를 쉽고 효율적으로 사용하기 위해 여러 모노레포 툴을 고민하던 중 Turborepo에 대한 고민을 하게 됐고, Turborepo는 패키지 매니저를 pnpm을 사용하는 것을 권장하고 있다는 점을 캐치 했습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결국은&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국은 Yarn Berry를 걷어내고 pnpm을 도입했습니다.&lt;br /&gt;.yarn 폴더를 제거하고 yarn과 관련된 모든 것들을 삭제한 후 pnpm을 통해 패키지를 설치했습니다. 저희 프로젝트에서는 생각보다 패키지 매니저를 교체하는 과정이 단순하고 간단했기에 특별한 트러블슈팅을 하지 않아도 됐습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;패키지 매니저를 변경한다면 &lt;b&gt;고려해야 할 몇 가지 점&lt;/b&gt;들이 있습니다.&lt;br /&gt;먼저 팀원들에게 가이드를 명확하게 해줘야 한다는 점 입니다. 프론트엔드 개발자는 익숙한 과정이기에 별 탈 없이 pnpm을 적용할 수 있습니다. 하지만 백앤드 개발자나 프론트엔드에 익숙하지 않는 개발자들에게는 pnpm을 활용해 프로젝트를 띄우는 방법을 명확하게 가이드 해야 합니다.&lt;br /&gt;그리고 패키지 매니저를 교체 했기 때문에 CI/CD 스크립트나 빌드 스크립트를 변경해주어야 합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 두 가지를 고려하지 않는다면 반쪽뿐인 패키지 교체가 될 수 있기 때문에 교체 시점에 액션 아이템으로 꼭 짚고 넘어가야 합니다!&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 pnpm에 대해서 살펴보고 패키지 매니저를 교체한 이유를 살펴봤습니다.&lt;br /&gt;이번 포스팅은 사실 제 스스로 pnpm에 대해서 공부했던 내용을 정리하려고 작성한 포스팅입니다.&lt;br /&gt;다른 의견이 있으시다면 댓글을 남겨주셔도 좋습니다. 환영입니다!&lt;br /&gt;&amp;nbsp;&lt;br /&gt;pnpm을 공부하고 계신 분들에게도 많은 도움이 됐으면 좋겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;읽어봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>PackageManager</category>
      <category>pnpm</category>
      <category>Web</category>
      <category>Yarn</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/49</guid>
      <comments>https://kangs-develop.tistory.com/49#entry49comment</comments>
      <pubDate>Sun, 8 Sep 2024 12:46:57 +0900</pubDate>
    </item>
    <item>
      <title>[React] useRef 톺아보기</title>
      <link>https://kangs-develop.tistory.com/48</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트를 하다보면 DOM을 직접 제어하거나, DOM의 정보를 필요로 하는 시점이 있습니다.&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;이번 포스팅에서는 위와 같은 케이스를 바탕으로 React의 useRef 훅에 대해서 살펴보겠습니다.&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;&lt;b&gt;useRef&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 랜더링을 발생시키지 않는 값을 필요로 한다면, 즉 React에 의해서 제어받지 않는 값을 필요로 한다면 고민해볼 수 있는 것이 useRef를 활용한 ref 객체 입니다.&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;React 프로젝트를 하다보면 변하는 값, 즉 상태를 state로 정의합니다. state는 랜더링을 유발하고 React 재조정 엔진에 제어를 당합니다. 반대로 useRef를 사용해 리턴 받는 ref 객체는 값이 변화 하더라도 랜더링이 발생하지 않는 비제어 값 입니다.&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;간단하게 useRef를 사용하는 방법을 살펴봅시다!&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;먼저 useRef 훅에 파라미터로 넘겨주는 값은 useRef의 초기값이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref 객체는 current 프로퍼티를 가지고 있는데, 초기 값은 ref 객체의 current 값이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725682521676&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const numRef = useRef(1);&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;위와 같이 초기 값으로 1을 넣어준 후 console로 numRef.current을 찍어본다면 1 의 값이 콘솔에 찍히게 됩니다.&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;&lt;b&gt;useRef 활용하기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DOM을 직접 다뤄야 할 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트를 진행할 때 DOM을 직접 다뤄야 하는 상황이 온다면 여러분들은 어떤 선택을 하실건가요?&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;예를 들면 특정 DOM 노드에 접근했을 때 스크롤 이벤트가 발생한다던지, Element를 클릭 시 특정 DOM으로 스크롤이 발생해야 한다는 이벤트가 있을 수 있습니다.&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;이런 경우 우리는 useRef를 활용해 DOM 노드를 활용할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;React에서는 DOM을 직접 핸들링 하는 것을 금지하고 있습니다. DOM을 React 재조정 엔진에 맡기지 않고 직접 핸들링 할 경우 예상치 못한 결과가 발생할 여지 (순수하지 못한 함수)가 있기 때문입니다.&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;그럼 간단한 예제를 통해 DOM을 다루는 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725682949527&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from &quot;react&quot;;

export default function App() {
  const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  return (
    &amp;lt;div ref={ref} /&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;먼저 useRef hook을 선언해준 후 초기 값으로 null을 넣어줍니다. 그 이후 참조가 필요한 DOM 노드에 ref 객체를 바인딩 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725683078294&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from &quot;react&quot;;

export default function App() {
  const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  
  const handleClick = () =&amp;gt; {
    if (ref.current) {
      console.log(ref);
    }
  }

  return (
    &amp;lt;div ref={ref}&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;event&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 버튼을 두고 ref를 콘솔에 찍어보는 이벤트를 정의해봅시다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 주의해야 할 점은 초기 값을 null로 넣어줬기 때문에 current의 값을 체크해줘야 한다는 점 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ref 객체 전달하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 컴포넌트에서 자식 컴포넌트로 ref를 전달하려면 어떻게 해야할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref는 특수한 객체이기 때문에 일반적인 props를 넘겨주는 방식으로 자식 컴포넌트에게 넘겨줄 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 컴포넌트에서 ref를 props로 받기 위해서는 forwardRef 라는 고차 함수를 사용해야 합니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19 버전 이후로는 forwardRef를 사용하지 않아도 자식 컴포넌트에서 ref를 전달 받을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 컴포넌트를 정의해 ref를 받는 연습을 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725683272891&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { forwardRef } from &quot;react&quot;;

type Props = {
  title: string;
};

const DomRef = forwardRef&amp;lt;HTMLDivElement, Props&amp;gt;((props, ref) =&amp;gt; {
  return &amp;lt;div ref={ref}&amp;gt;{props.title}&amp;lt;/div&amp;gt;;
});

export default DomRef;&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;DomRef라는 컴포넌트는 Props와 ref를 전달 받습니다. 전달 받은 ref는 div에 바인딩 할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript를 사용하기 때문에 제네릭을 활용해 타입을 바인딩 해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭의 첫 번째 인자로는 ref의 타입을 넣어줍니다. 두 번째 인자로는 Props의 타입을 넣어줍니다.&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;forwardRef의 매개변수로는 컴포넌트를 전달합니다. 첫 번째 인자로 props, 두 번째 인자로 ref를 받습니다. 이 때 주의해야 할 점은 ref는 props에 담겨있지 않다는 점 입니다. ref는 특수한 props이기 때문에 항상 두 번째 인자로 전달 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725683426243&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from &quot;react&quot;;

export default function App() {
  const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  return (
    &amp;lt;DomRef title=&quot;title&quot; ref={ref} /&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;사용하는 곳에서는 props와 ref를 컴포넌트에서 정의된 대로 내려줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;자식 컴포넌트에서 정의한 함수 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 컴포넌트에서 자식 컴포넌트에 정의된 함수를 사용해야 할 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 단방향으로(위에서 아래로) 데이터가 흘러가기 때문에 아래에 정의된 상태를 위로 끌어올리고 싶을 땐 특정 패턴을 활용해야 합니다.&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;자식 컴포넌트는 input 노드가 존재하고 해당 노드는 state로 제어됩니다. 부모 컴포넌트에서는 특정 이벤트가 발생할 경우 state값을 필요로 합니다. 우리는 부모 컴포넌트에서 ref 객체를 정의해 소유하고 있고, props로 자식 컴포넌트에 내려 자식 컴포넌트에서 함수를 정의해 자식 컴포넌트의 state를 전달해 부모 컴포넌트에서 사용할 수 있도록 예제를 구현해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;물론 이런 케이스는 부모 컴포넌트에서 state를 매개 변수로 받는 함수를 정의하고 정의한 함수를 props로 내려주는 방식을 사용할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725683884840&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useRef, useState } from &quot;react&quot;;
import CustomRef from &quot;./custom-ref&quot;;

export default function App() {
  const customRef = useRef&amp;lt;{ getTitle: () =&amp;gt; string } | null&amp;gt;(null);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;CustomRef ref={customRef} title=&quot;init value&quot; /&amp;gt;
    &amp;lt;/div&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;먼저 ref를 정의합니다. current 프로퍼티를 가진 ref 객체에는 getTitle이라는 함수를 바인딩 할 계획입니다. 초기 값은 null로 넣어주도록 하겠습니다.&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;CustomRef 컴포넌트를 구현합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1725684013434&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { forwardRef, useImperativeHandle, useState } from &quot;react&quot;;

type Props = {
  title: string;
};

const CustomRef = forwardRef&amp;lt;{ getTitle: () =&amp;gt; string }, Props&amp;gt;(
  (props, ref) =&amp;gt; {
    const [title, setTitle] = useState(props.title);

    useImperativeHandle(ref, () =&amp;gt; {
      return {
        getTitle: () =&amp;gt; title,
      };
    });

    return (
      &amp;lt;div&amp;gt;
        &amp;lt;input
          value={title}
          onChange={(e) =&amp;gt; setTitle(e.currentTarget.value)}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
);

export default CustomRef;&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;title 이라는 state를 가지고 있으며 input 노드를 통해 state 값을 입력받는 컴포넌트 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 컴포넌트에서 ref와 Props를 전달 받았고, ref에 getTitle이라는 함수를 정의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이때 useImperativeHandle 이라는 API를 사용해 ref 객체에 함수를 바인딩 할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/useImperativeHandle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useImperativeHandle 자세히 살펴보기&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 훅의 첫 번째 인자로 핸들링 할 ref 객체를 넣어주세요. 두 번째 인자로는 익명 함수를 넣어주는데, return 되는 객체를 통해 ref를 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 간단히 getTitle이라는 함수를 정의하고, title 상태 값을 리턴하는 함수를 정의하겠습니다.&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;부모 컴포넌트에서 이를 활용해봅시다! 부모 컴포넌트에서 하나의 상태 값을 받고, 이벤트를 통해 title 값을 해당 상태 값으로 전달하겠습니다. 그리고 해당 값을 화면에 노출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725684267943&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef, useState } from &quot;react&quot;;
import CustomRef from &quot;./custom-ref&quot;;

export default function App() {
  const [input, setInput] = useState(&quot;&quot;);
  const customRef = useRef&amp;lt;{ getTitle: () =&amp;gt; string } | null&amp;gt;(null);

  const handleGetValue = () =&amp;gt; {
    if (customRef?.current) {
      const inputValue = customRef.current.getTitle();
      setInput(inputValue);
    }
  };

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;CustomRef ref={customRef} title=&quot;init value&quot; /&amp;gt;
      &amp;lt;button onClick={handleGetValue}&amp;gt;값 동기화&amp;lt;/button&amp;gt;
      &amp;lt;div&amp;gt;입력된 값은 {input} 입니다.&amp;lt;/div&amp;gt;
    &amp;lt;/div&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;handleGetValue 라는 함수를 정의하고 button에 클릭 이벤트로 바인딩 합니다. 해당 함수에서 ref에 바인딩 된 getTitle 함수를 호출합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2024-09-07-오후-1.46.10.gif&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkx80S/btsJt7aYkdy/uucINSMVALXQfrG7oHQEgk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkx80S/btsJt7aYkdy/uucINSMVALXQfrG7oHQEgk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkx80S/btsJt7aYkdy/uucINSMVALXQfrG7oHQEgk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bkx80S/btsJt7aYkdy/uucINSMVALXQfrG7oHQEgk/img.gif&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;628&quot; height=&quot;332&quot; data-filename=&quot;화면-기록-2024-09-07-오후-1.46.10.gif&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 포스팅을 통해 useRef를 활용하는 몇 가지의 케이스를 살펴봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살펴본 케이스들은 가장 대표적인 케이스이지만 ref 객체는 다양한 활용법이 있습니다.&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;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>react</category>
      <category>useRef</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/48</guid>
      <comments>https://kangs-develop.tistory.com/48#entry48comment</comments>
      <pubDate>Sat, 7 Sep 2024 13:52:04 +0900</pubDate>
    </item>
    <item>
      <title>[React] Context.API를 사용해 Provider 패턴 구현하기</title>
      <link>https://kangs-develop.tistory.com/46</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;최근 업무에서 구현된 AlertDialog를 Swal 라이브러리와 비슷한 형태로 사용 가능하게 POC가 가능하냐는 요청이 들어왔습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Swal 라이브러리는 Sweet Alert의 약자로 Alert 컴포넌트를 Promise 단위로 다루어 선언적이고 쉽게 Alert를 다룰 수 있는 라이브러리입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;구현된 Alert Dialog 컴포넌트는 radix-ui/react-alert 라이브러리를 활용해 구현이 됐으며 해당 컴포넌트를 사용하기 위해서는 컴포넌트를 랜더링 시키고 props를 넘겨줘야 합니다.&lt;br&gt;AlertDialog를 사용하는 곳 페이지 혹은 컴포넌트 마다 랜더링 하도록 구현하는 것은 상대적으로 많은 코드 중복이 발생할 뿐더러 개발 경험이 저하됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하여 Swal 라이브러리 처럼 사용처에서 랜더링을 하지 않고 선언적으로 사용할 수 있는 방법이 있을까 고민하던 중 Context API의 Provider 패턴을 활용해 AlertDialog를 싱글턴 패턴과 비슷하게 구현해보자 라는 아이디어를 고안했고 이런 방식으로 구현해 요구사항 POC를 성공했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번 포스팅을 통해 해당 과정을 살펴보도록 하겠습니다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이번 포스팅에서는 Context API에 대해서 자세하게 다루고 있지 않습니다. 만약 Context API가 궁금하시다면 아래 공식 문서를 살펴봐주세요!&lt;br&gt;&lt;br&gt;&lt;a href=&quot;https://ko.react.dev/learn/passing-data-deeply-with-context&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Context API 공식문서&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기술 선정 이유&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Provider 패턴을 활용해 컴포넌트를 싱글턴 패턴으로 랜더링 하려면 전역 상태를 사용해야 합니다.&lt;br&gt;요즘 사용하는 전역 상태 관리 라이브러리는 굉장히 다양합니다. Redux, Zustand, Jotai, Recoil 등.. 여러가지 라이브러리가 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 저는 특정 라이브러리에 의존성을 부여하고 싶지 않았고 내장된 API인 Context API를 사용해 간단하게 구현할 수 있을 것이라고 판단했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;특히 특정 라이브러리를 설치한다면 해당 컴포넌트를 사용하기 위해 사용하는 프로젝트에서 해당 상태관리 라이브러리를 설치해야 하기 때문에 좋지 못한 의존성 관계를 갖게 됩니다. 이런 상황들을 예방하기 위해 Context API 를 사용하게 됐습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설계&lt;/b&gt;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Context Type&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 Context의 타입은 아래와 같습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type AlertDialogContextProps = {
&amp;nbsp;&amp;nbsp; alertDialogProps: AlertDialogProps;
&amp;nbsp;&amp;nbsp; open: (value: AlertDialogProps) =&amp;gt; void;
&amp;nbsp;&amp;nbsp; close: () =&amp;gt; void;
};&lt;/code&gt;&lt;/pre&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.6047%;&quot;&gt;Property&lt;/td&gt;&lt;td style=&quot;width: 71.3953%;&quot;&gt;Description&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.6047%;&quot;&gt;alertDialogProps&lt;/td&gt;&lt;td style=&quot;width: 71.3953%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;AlertDialog에 넘겨줄 Props (AlertDialog를 사용하는 컴포넌트에서 기존의 props를 필요로 할 소지가 있어서 추가했습니다. 추후 필요 없다고 판단 시 제거해도 괜찮습니다.)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.6047%;&quot;&gt;open&lt;/td&gt;&lt;td style=&quot;width: 71.3953%;&quot;&gt;AlertDialogProps&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt; 를 전달받아 Context에 넘겨주는 함수입니다. AlertDialog를 열 때 isOpen 값은 항상 true이기에 해당 값은 props로 전달받지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 28.6047%;&quot;&gt;close&lt;/td&gt;&lt;td style=&quot;width: 71.3953%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;파라미터와 리턴 값이 없는 함수입니다. AlertDialog를 닫을 때 사용합니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Context&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const init = () =&amp;gt; {};
const initialProps: AlertDialogProps = {
&amp;nbsp;&amp;nbsp; isOpen: false,
&amp;nbsp;&amp;nbsp; children: undefined,
&amp;nbsp;&amp;nbsp; onClickClose: () =&amp;gt; {},
};
const AlertDialogContext = createContext&amp;lt;AlertDialogContextProps&amp;gt;({
&amp;nbsp;&amp;nbsp; open: init,
&amp;nbsp;&amp;nbsp; close: init,
&amp;nbsp;&amp;nbsp; alertDialogProps: initialProps,
});&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;초기 값을 null과 undefined을 지정해주지 않고 initValue를 넣어줍니다. null | undefined를 사용할 경우 해당 Context를 사용하는 곳에서 값의 체크가 필요해 사용처의 코드가 복잡해집니다. Provider 컴포넌트에서 close, open을 재정의 해서 값을 넣어주기 때문에 사용하는 측에서 함수 재정의가 필요하지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;&lt;b&gt;Provider&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const AlertDialogProvider = (props: PropsWithChildren) =&amp;gt; {
&amp;nbsp;&amp;nbsp; const [alertDialogProps, setAlertDialogProps] = useState&amp;lt;AlertDialogProps&amp;gt;(initialProps);
&amp;nbsp;&amp;nbsp; const open = useCallback(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(props: AlertDialogProps) =&amp;gt; setAlertDialogProps({ ...props, isOpen: true }),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[]
&amp;nbsp;&amp;nbsp; );
&amp;nbsp;&amp;nbsp; const close = useCallback(() =&amp;gt; setAlertDialogProps((prev) =&amp;gt; ({ ...prev, isOpen: false })), []);
&amp;nbsp;&amp;nbsp; return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;AlertDialogContext.Provider value={{ open, close, alertDialogProps }}&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {props.children}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;AlertDialog {...alertDialogProps} /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/AlertDialogContext.Provider&amp;gt;
&amp;nbsp;&amp;nbsp; );
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;Provider에서는 open, close 함수를 재정의 해서 Context.Provider에 value로 넘겨줍니다. 해당 Provider에서 state로 alertDialogProps의 상태를 관리하고 AlertDialog를 랜더링 하여 Props를 넘겨주고 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;위 Provider 컴포넌트에서 AlertDialog를 랜더링하고 Context API를 활용해 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;alertDialogProps를&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt; 전역 상태로 사용하고 있어 사용하는 각 페이지 마다 컴포넌트를 랜더링 하지 않아도 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;&lt;b&gt;useAlertDialog Hook&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;AlertDialog를 사용하기 위한 비즈니스 로직을 모아놓은 훅 입니다. &lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;Context API를 사용해 alertContext를 받아서 Dialog를 사용하는 비즈니스 로직을 구현합니다. 커스텀 훅으로 한번 더 래핑한 이유는 AlertDialog를 사용하기 위한 비즈니스 로직을 한번 더 추상화 해 코드 중복을 줄이기 위함입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type OpenAlertDialogProps = Omit&amp;lt;AlertDialogProps, 'isOpen' | 'onClickClose'&amp;gt; &amp;amp; {
&amp;nbsp;&amp;nbsp; onClickClose?: (e?: React.MouseEvent&amp;lt;HTMLButtonElement | HTMLDivElement&amp;gt;) =&amp;gt; void;
};
type UseAlertDialogResult = {
&amp;nbsp;&amp;nbsp; open: (props: OpenAlertDialogProps) =&amp;gt; void;
&amp;nbsp;&amp;nbsp; close: () =&amp;gt; void;
};

const useAlertDialog = (): UseAlertDialogResult =&amp;gt; {
&amp;nbsp;&amp;nbsp; const alertContext = useContext&amp;lt;AlertDialogContextProps&amp;gt;(AlertDialogContext);

&amp;nbsp;&amp;nbsp; const closeDialog = useCallback(() =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alertContext.close();
&amp;nbsp;&amp;nbsp; }, [alertContext]);

&amp;nbsp;&amp;nbsp; const openDialog = useCallback(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(props: OpenAlertDialogProps) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; const onClickClose = (e?: React.MouseEvent&amp;lt;HTMLButtonElement | HTMLDivElement&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (props?.onClickClose instanceof Function) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; props.onClickClose(e);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;closeDialog();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; };
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; alertContext.open({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...props,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onClickClose,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;isOpen: true,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; });
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[alertContext, closeDialog]
&amp;nbsp;&amp;nbsp; );

&amp;nbsp;&amp;nbsp; return {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;open: openDialog,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;close: closeDialog,
&amp;nbsp;&amp;nbsp; };
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Use Case&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export const Test: Story = {
&amp;nbsp;&amp;nbsp; ...Template,
&amp;nbsp;&amp;nbsp; render: () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;AlertDialogProvider&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;Sample /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;Sample2 /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/AlertDialogProvider&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp; },
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;Provider로 App을 래핑합니다. 실제 프로젝트라면 index.tsx에서 Root를 래핑합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const Sample = () =&amp;gt; {
&amp;nbsp;&amp;nbsp; const { open } = useAlertDialog();

&amp;nbsp;&amp;nbsp; const onClickConfirm = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alert('ok');
&amp;nbsp;&amp;nbsp; };

&amp;nbsp;&amp;nbsp; const onClick = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;open({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; onClickConfirm,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; title: 'Default Alert',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; children: &amp;lt;&amp;gt;this is child&amp;lt;/&amp;gt;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cancelButtonText: '취소',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp; };
&amp;nbsp;&amp;nbsp; return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;Button onClick={onClick}&amp;gt;Use Alert Dialog1&amp;lt;/Button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp; );
};

const Sample2 = () =&amp;gt; {
&amp;nbsp;&amp;nbsp; const { open } = useAlertDialog();

&amp;nbsp;&amp;nbsp; const onClickClose = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alert('Close Callback');
&amp;nbsp;&amp;nbsp; };

&amp;nbsp;&amp;nbsp; const onClickConfirm = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alert('ok');
&amp;nbsp;&amp;nbsp; };

&amp;nbsp;&amp;nbsp; const onClick = () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;open({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; onClickClose,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; onClickConfirm,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; title: 'Default Sample2',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; children: &amp;lt;&amp;gt;this is sampe2&amp;lt;/&amp;gt;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cancelButtonText: '취소',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp; };
&amp;nbsp;&amp;nbsp; return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;button onClick={onClick}&amp;gt;Sample2 Alert&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp; );
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;실제 AlertDialog를 사용하는 컴포넌트 입니다. 2개의 다른 컴포넌트에서 Alert를 사용합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;&lt;b&gt;POC 이후 요구사항 반영&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;POC를 팀원들에게 한 이후 몇 가지 요구사항을 받았습니다.&lt;/span&gt;&lt;/span&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;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;confirmButtonText props의 기본 값 “확인”을 초기 값으로 넣어달라는 요청사항이 있어 initialProps에 값을 추가했습니다. open 할 때 기존의 값을 유지시킬 수 있도록 setState 로직을 변경했습니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const initialProps: AlertDialogProps = {
&amp;nbsp;&amp;nbsp; isOpen: false,
&amp;nbsp;&amp;nbsp; children: undefined,
&amp;nbsp;&amp;nbsp; onClickClose: init,
&amp;nbsp;&amp;nbsp; confirmButtonText: '확인',
};

const open = useCallback((props: AlertDialogProps) =&amp;gt; {
&amp;nbsp;&amp;nbsp; setAlertDialogProps((prev) =&amp;gt; ({ ...prev, ...props, isOpen: true }));
}, []);&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;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;onClickClose 함수의 기본 동작은 AlertDialog를 닫아주는 역할입니다. 하여 컴포넌트에서 해당 Callback을 정의해 넘기지 않아도 컴포넌트를 &lt;/span&gt;&lt;/span&gt;&lt;b&gt;닫는 기본 동작이 가능 하도록 수정&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #172b4d;&quot;&gt;했습니다. 만약 Close 시점에 특정 Callback이 실행되는 것이 필요하다면 onClickClose을 정의해 넘기고 컴포넌트가 닫히기 전 해당 함수가 실행되도록 수정했습니다. (onClickClose 콜백 함수에 close를 구현하지 않아도 괜찮습니다. 추가적인 로직만 구현해서 넘겨주세요.)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const openDialog = useCallback(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(props: OpenAlertDialogProps) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const onClickClose = (e?: React.MouseEvent&amp;lt;HTMLButtonElement | HTMLDivElement&amp;gt;) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (props?.onClickClose instanceof Function) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;props.onClickClose(e);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; closeDialog();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alertContext.open({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...props,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; onClickClose,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; isOpen: true,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp; },
&amp;nbsp;&amp;nbsp; [alertContext, closeDialog]
);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AewmW/btsJt1OUevg/Mgo3sRHLqFx32M34Fb3jhk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AewmW/btsJt1OUevg/Mgo3sRHLqFx32M34Fb3jhk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AewmW/btsJt1OUevg/Mgo3sRHLqFx32M34Fb3jhk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/AewmW/btsJt1OUevg/Mgo3sRHLqFx32M34Fb3jhk/img.gif&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;705&quot; height=&quot;282&quot; data-origin-width=&quot;1594&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번&amp;nbsp;포스팅을&amp;nbsp;통해&amp;nbsp;Context&amp;nbsp;API를&amp;nbsp;사용해&amp;nbsp;Provider&amp;nbsp;패턴을&amp;nbsp;구현해봤습니다. &lt;br&gt;React를&amp;nbsp;사용하며&amp;nbsp;디자인&amp;nbsp;패턴을&amp;nbsp;구현해볼&amp;nbsp;기회가&amp;nbsp;있었고&amp;nbsp;특정&amp;nbsp;패턴들을&amp;nbsp;활용해&amp;nbsp;문제를&amp;nbsp;쉽게&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있었습니다. &lt;br&gt;저와&amp;nbsp;같은&amp;nbsp;문제를&amp;nbsp;해결하고자&amp;nbsp;하시는&amp;nbsp;분들에게&amp;nbsp;좋은&amp;nbsp;공유가&amp;nbsp;되었으면&amp;nbsp;좋겠습니다. &lt;br&gt;읽어봐주셔서&amp;nbsp;감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>context.api</category>
      <category>frontend</category>
      <category>provider 패턴</category>
      <category>react</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/46</guid>
      <comments>https://kangs-develop.tistory.com/46#entry46comment</comments>
      <pubDate>Fri, 6 Sep 2024 17:30:07 +0900</pubDate>
    </item>
    <item>
      <title>[React] react-table을 활용해 기본 테이블 만들기</title>
      <link>https://kangs-develop.tistory.com/44</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Overview&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HTML table 태그, div 태그를 활용해 테이블 기능 직접 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. MUI, antd, chakra-ui 와 같은 UI 라이브러리에서 제공해주는 Table 컴포넌트 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Headless UI 라이브러리를 활용해 Table 컴포넌트 구현&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;요즘에는 첫 번째 선택지의 table 태그로 직접 테이블을 구현하지 않습니다. 태그의 자유도가 떨어져 div 태그를 활용해 구현하는 편 입니다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;2번의 UI 라이브러리를 사용한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;테이블 컴포넌트 뿐만 아닌 다양한 컴포넌트를 사용할 수 있어서 장점이 뚜렷합니다.&lt;br /&gt;하지만&amp;nbsp;테이블&amp;nbsp;컴포넌트만&amp;nbsp;사용하기엔&amp;nbsp;라이브러리의&amp;nbsp;번들&amp;nbsp;사이즈가&amp;nbsp;상당히&amp;nbsp;크고,&amp;nbsp;디자인을&amp;nbsp;커스텀하기&amp;nbsp;어렵다는&amp;nbsp;단점이&amp;nbsp;있습니다. &lt;br /&gt;그리고&amp;nbsp;여러&amp;nbsp;프로젝트에서&amp;nbsp;해당&amp;nbsp;라이브러리들을&amp;nbsp;사용해본&amp;nbsp;경험으로는&amp;nbsp;프로젝트가&amp;nbsp;지속될수록&amp;nbsp;라이브러리의&amp;nbsp;버전&amp;nbsp;관리를&amp;nbsp;잘&amp;nbsp;해줘야&amp;nbsp;하는데,&amp;nbsp;메인&amp;nbsp;라이브러리&amp;nbsp;(예를&amp;nbsp;들면&amp;nbsp;react)와의&amp;nbsp;결합도가&amp;nbsp;강하기&amp;nbsp;때문에&amp;nbsp;버전관리가&amp;nbsp;복잡하다는&amp;nbsp;단점이&amp;nbsp;있었습니다. &lt;br /&gt;&lt;br /&gt;오늘 포스팅에서는 3번의 Headless UI 라이브러리를 사용해 테이블의 기능을 붙이고 div 태그를 사용해 데이터를 표현하는 기초 테이블을 구현해보겠습니다.&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;&lt;b&gt;기술 스택&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- React&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- TypeScript&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @tanstack/react-table&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- CSS&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;스타일링은 간단하게 CSS를 사용합니다. CSS는 웹 표준이며 어느 상황에서도 쉽게 사용할 수 있습니다. CSS를 모듈방식으로 사용해 테이블 컴포넌트를 구현해봅니다.&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;&lt;b&gt;설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1724809488049&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm add @tanstack/react-table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;props&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 테이블 컴포넌트를 설계해봅시다. 컴포넌트는 데이터와 컬럼 리스트를 받아서 화면에 표 형식으로 데이터를 그려줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 props로 data와 column을 받아줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 타입은 컴포넌트에서 추론할 수 없습니다. 사용하는 곳 마다의 데이터 타입이 다르기 때문입니다. 그렇기 때문에 타입 매개변수인 제네릭을 활용해 데이터와 컬럼에 타입을 전달해주기로 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724743536402&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { ColumnDef } from &quot;@tanstack/react-table&quot;;

type Props&amp;lt;T&amp;gt; = {
  data: T[];
  columns: ColumnDef&amp;lt;T&amp;gt;[];
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;컴포넌트 구현&lt;/b&gt;&lt;/i&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트는 함수 컴포넌트로 구현할 예정입니다. props로는 위에 설계한 타입의 props를 전달 받을 예정이고 타입 매개변수를 전달 받아야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724743699890&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function BasicTable&amp;lt;T&amp;gt;(props:Props&amp;lt;T&amp;gt;) {
  return &amp;lt;div&amp;gt;&amp;lt;/div&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;react-table 라이브러리에서 useReactTable 훅을 import 합니다. 훅의 매개변수로 설정을 위핸 객체를 넘겨주는데, 해당 객체에 넘겨주는 값들로 react-table의 기능을 구현할 수 있습니다.&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;props로 전달받은 data, columns를 함께 넘겨줍시다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getCoreRowModel 이라는 함수를 라이브러리에서 import해와 함수를 getCoreRowModel의 값으로 함수 호출한 값을 넘겨줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724743859323&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {
  ColumnDef,
  getCoreRowModel,
  useReactTable,
} from &quot;@tanstack/react-table&quot;;

function BasicTable&amp;lt;T&amp;gt;({ data, columns }:Props&amp;lt;T&amp;gt;) {
  const table = useReactTable&amp;lt;T&amp;gt;({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  
  return &amp;lt;div&amp;gt;&amp;lt;/div&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;이제 return 될 테이블 컴포넌트를 마크업 해봅시다! 아래는 설명에 앞서 구현할 코드의 전문입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724809013668&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function BasicTable&amp;lt;T&amp;gt;({ data, columns }: Props&amp;lt;T&amp;gt;) {
  const table = useReactTable&amp;lt;T&amp;gt;({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  const { getHeaderGroups, getRowModel } = table;
  return (
    &amp;lt;div className={style.tableContainer}&amp;gt;
      &amp;lt;div className={style.tableHeaderContainer}&amp;gt;
        {getHeaderGroups().map((headerGroup) =&amp;gt; (
          &amp;lt;div key={headerGroup.id} className={style.tableHeaderRow}&amp;gt;
            {headerGroup.headers.map((header) =&amp;gt; (
              &amp;lt;div
                key={header.id}
                className={style.cell}
                style={{ width: header.getSize() }}
              &amp;gt;
                {header.isPlaceholder
                  ? null
                  : flexRender(
                      header.column.columnDef.header,
                      header.getContext()
                    )}
              &amp;lt;/div&amp;gt;
            ))}
          &amp;lt;/div&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
      &amp;lt;div className={style.tableBodyContainer}&amp;gt;
        {getRowModel().rows.map((row) =&amp;gt; (
          &amp;lt;div key={row.id} className={style.row}&amp;gt;
            {row.getVisibleCells().map((cell) =&amp;gt; (
              &amp;lt;div
                key={cell.id}
                className={style.cell}
                style={{ width: cell.column.getSize() }}
              &amp;gt;
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              &amp;lt;/div&amp;gt;
            ))}
          &amp;lt;/div&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&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;먼저 테이블 header, body를 감싸줄 컨테이너 div를 준비합니다.&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;i&gt;&lt;b&gt;header&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;header에도 여러 row가 존재할 수 있으니 header을 감싸줄 컨테이너 div도 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useReactTable 훅으로부터 리턴받은 table 인스턴스에서 getHeaderGroups 메서드를 꺼내와 header을 그려주겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;headerGroup은 각각의 헤더 로우로써 컬럼들(headers)을 담고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;headerRow 인 div를 사용하고 headerGroup.headers을 통해 cell을 만들어줍시다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 주의해야 할 점은 header.getSize를 통해 컬럼의 너비를 지정해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;div태그를 활용해 flexible하게 테이블 컴포넌트를 마크업 하기 떄문에 반드시 테이블 인스턴스에 저장된 셀의 너비를 사용해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀의 내용은 flexRender이라는 메서드를 라이브러리에서 import 해와서 랜더링 시킵니다. 컬럼에 정의된 내용과 셀을 매핑시켜 flexible 하게 셀을 렌더링합니다.&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;i&gt;&lt;b&gt;body&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;body도 마찬가지로 여러 row모델이 존재함으로 컨테이너 div를 준비합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useReactTable 훅으로부터 리턴받은 table 인스턴스에서 getRowModel 메서드를 꺼내와 body를 그려주겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메서드를 순회하며 각각의 row를 화면에 그려주고 화면에 보여지는 셀만 그리기 위해 getVisibleCells 메서드를 사용해 셀 리스트를 받아옵니다. 그리고 header와 똑같은 방식으로 cell을 순회하고 flexRender 메서드를 사용해 플렉서블하게 셀을 렌더링합니다.&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;i&gt;&lt;b&gt;스타일링&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 스타일링한 CSS 코드도 첨부하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1724809114161&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.tableContainer {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid black;
}

.tableHeaderContainer {
  width: 100%;
  height: 40px;
  border: 1px solid #f4f6f8;
}

.tableHeaderRow {
  width: 100%;
  height: 100%;
  display: flex;
  font-weight: 700;
}

.tableBodyContainer {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow-y: scroll;
}

.row {
  width: 100%;
  height: 100%;
  display: flex;
  border: 1px solid #f4f6f8;
}

.row:hover {
  background-color: #e9f3ff !important;
}
.row:nth-child(odd) {
  background-color: #f9fafc;
}

.cell {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  font-size: 12px;
  border-right: 1px solid #eff1f3;
  border-color: #f4f6f8;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현된 테이블&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 완성한 테이블의 모습입니다. 간단하게 데이터만 display하는 테이블의 형태입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;table-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TLvC3/btsJg7BZ9k4/A3BDfyEhOO9i3b35xWVLTk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TLvC3/btsJg7BZ9k4/A3BDfyEhOO9i3b35xWVLTk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TLvC3/btsJg7BZ9k4/A3BDfyEhOO9i3b35xWVLTk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/TLvC3/btsJg7BZ9k4/A3BDfyEhOO9i3b35xWVLTk/img.gif&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;399&quot; data-filename=&quot;table-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;b&gt;마치며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@tanstack/react-table을 활용해 아주 간단하게 기초 테이블 컴포넌트를 구현해봤습니다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://velog.io/@kangactor123/react-table-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-table 활용해 테이블 만들기 - 1&lt;/a&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;더 자세하게 라이브러리를 살펴보고 싶으시다면 공식 문서를 살펴봐주세요!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://tanstack.com/table/latest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&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;여러 기능이 구현된 테이블 컴포넌트가 궁금하시다면 아래 레포지토리를 참고해주시면 감사하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://github.com/kangactor123/radix-storybook/tree/main/src/components/table&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;radix-storybook 레포지토리&lt;/a&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;읽어봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>[WEB] 프론트엔드</category>
      <category>frontend</category>
      <category>react</category>
      <category>react-table</category>
      <category>Web</category>
      <author>[FE] Lighthouse</author>
      <guid isPermaLink="true">https://kangs-develop.tistory.com/44</guid>
      <comments>https://kangs-develop.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 28 Aug 2024 10:48:11 +0900</pubDate>
    </item>
  </channel>
</rss>