<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ji-frontdev</title>
    <link>https://ji-frontdev.tistory.com/</link>
    <description>프론트엔드와 관련한 모든 영역을 안내</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 17:52:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ji-frontdev</managingEditor>
    <image>
      <title>ji-frontdev</title>
      <url>https://tistory1.daumcdn.net/tistory/7407756/attach/bd0607911d96431fa833ed6375d1a425</url>
      <link>https://ji-frontdev.tistory.com</link>
    </image>
    <item>
      <title>2024년 vs 2025년 AI 트렌드, 뭐가 달라졌을까?  </title>
      <link>https://ji-frontdev.tistory.com/entry/2024%EB%85%84-vs-2025%EB%85%84-AI-%ED%8A%B8%EB%A0%8C%EB%93%9C-%EB%AD%90%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EC%9D%84%EA%B9%8C-%F0%9F%A4%96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;AI는 매년 상상 이상의 속도로 발전하고 있습니다.&lt;br /&gt;2024년이 &amp;lsquo;&lt;b&gt;AI 대중화의 시작&lt;/b&gt;&amp;rsquo;을 알린 해였다면, 2025년은 &amp;lsquo;&lt;b&gt;초개인화 &amp;amp; 실질적 가치 창출&lt;/b&gt;&amp;rsquo;의 시대로 자리 잡고 있습니다.&lt;br /&gt;이번 포스팅에서는 &lt;b&gt;2024년과 2025년 AI 트렌드를 비교&lt;/b&gt;하고, 각 산업별 활용 현황을 정리해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqvBJs/btsP1jB7Qs0/01f61X3BVjZEDn0fYRRs60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqvBJs/btsP1jB7Qs0/01f61X3BVjZEDn0fYRRs60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqvBJs/btsP1jB7Qs0/01f61X3BVjZEDn0fYRRs60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqvBJs%2FbtsP1jB7Qs0%2F01f61X3BVjZEDn0fYRRs60%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-end=&quot;327&quot; data-start=&quot;324&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;356&quot; data-start=&quot;329&quot; data-ke-size=&quot;size26&quot;&gt;  2024년 AI 트렌드: 대중화의 시작&lt;/h2&gt;
&lt;h3 data-end=&quot;374&quot; data-start=&quot;358&quot; data-ke-size=&quot;size23&quot;&gt;1. AI의 대중화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;473&quot; data-start=&quot;375&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;420&quot; data-start=&quot;375&quot;&gt;기업들이 AI를 속속 도입하며 &lt;b&gt;업무 효율 &amp;amp; 고객 경험 개선&lt;/b&gt;에 집중&lt;/li&gt;
&lt;li data-end=&quot;473&quot; data-start=&quot;421&quot;&gt;특히 &lt;b&gt;Generative AI&lt;/b&gt;가 디자인, 마케팅, 콘텐츠 제작에서 혁신 주도 ✨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;500&quot; data-start=&quot;475&quot; data-ke-size=&quot;size23&quot;&gt;2. Generative AI 확산&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;565&quot; data-start=&quot;501&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;501&quot;&gt;텍스트, 이미지, 음악 생성까지 가능한 &lt;b&gt;창조형 AI&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;565&quot; data-start=&quot;538&quot;&gt;맞춤형 콘텐츠 제작 &amp;amp; 마케팅 강화에 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;588&quot; data-start=&quot;567&quot; data-ke-size=&quot;size23&quot;&gt;3. 산업별 AI 활용 확대&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;675&quot; data-start=&quot;589&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;616&quot; data-start=&quot;589&quot;&gt;&lt;b&gt;금융&lt;/b&gt; : 사기 탐지 &amp;amp; 리스크 평가&lt;/li&gt;
&lt;li data-end=&quot;646&quot; data-start=&quot;617&quot;&gt;&lt;b&gt;의료&lt;/b&gt; : 영상 분석 &amp;amp; 신약 개발 지원&lt;/li&gt;
&lt;li data-end=&quot;675&quot; data-start=&quot;647&quot;&gt;&lt;b&gt;제조업&lt;/b&gt; : 스마트 팩토리, 품질 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;695&quot; data-start=&quot;677&quot; data-ke-size=&quot;size23&quot;&gt;4. AI 플랫폼 성장&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;751&quot; data-start=&quot;696&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;720&quot; data-start=&quot;696&quot;&gt;기업들이 쉽게 AI를 도입하도록 지원&lt;/li&gt;
&lt;li data-end=&quot;751&quot; data-start=&quot;721&quot;&gt;&lt;b&gt;기술적 장벽 &amp;rarr; 낮추고, 접근성 &amp;rarr; 높임&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;771&quot; data-start=&quot;753&quot; data-ke-size=&quot;size23&quot;&gt;5. 윤리적 AI 논의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;835&quot; data-start=&quot;772&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;810&quot; data-start=&quot;772&quot;&gt;&quot;AI가 어떻게 결정했는가?&quot;에 대한 &lt;b&gt;투명성 요구 증가&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;835&quot; data-start=&quot;811&quot;&gt;기업마다 &lt;b&gt;윤리 가이드라인 마련&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;840&quot; data-start=&quot;837&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;875&quot; data-start=&quot;842&quot; data-ke-size=&quot;size26&quot;&gt;  2025년 AI 트렌드: 초개인화 &amp;amp; 실질적 가치&lt;/h2&gt;
&lt;h3 data-end=&quot;914&quot; data-start=&quot;877&quot; data-ke-size=&quot;size23&quot;&gt;1. 초개인화 (Hyper-Personalization)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;985&quot; data-start=&quot;915&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;945&quot; data-start=&quot;915&quot;&gt;고객 &lt;b&gt;행동&amp;middot;취향&amp;middot;맥락 기반 맞춤형 서비스&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;985&quot; data-start=&quot;946&quot;&gt;마케팅, 금융, 의료 등에서 &lt;b&gt;개인별 AI 경험&lt;/b&gt; 제공  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1005&quot; data-start=&quot;987&quot; data-ke-size=&quot;size23&quot;&gt;2. 실질적 가치 창출&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1065&quot; data-start=&quot;1006&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1037&quot; data-start=&quot;1006&quot;&gt;단순 효율화 &amp;rarr; &lt;b&gt;새로운 비즈니스 모델 창출&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1065&quot; data-start=&quot;1038&quot;&gt;AI가 이제는 &lt;b&gt;전략적 자산&lt;/b&gt;으로 격상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1085&quot; data-start=&quot;1067&quot; data-ke-size=&quot;size23&quot;&gt;3. 산업 맞춤형 AI&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1177&quot; data-start=&quot;1086&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1114&quot; data-start=&quot;1086&quot;&gt;&lt;b&gt;의료&lt;/b&gt; : 환자 데이터 기반 맞춤 치료&lt;/li&gt;
&lt;li data-end=&quot;1146&quot; data-start=&quot;1115&quot;&gt;&lt;b&gt;농업&lt;/b&gt; : 작물 성장 예측 &amp;amp; 스마트 농기계&lt;/li&gt;
&lt;li data-end=&quot;1177&quot; data-start=&quot;1147&quot;&gt;&lt;b&gt;관광&lt;/b&gt; : 개인 여행 추천 &amp;amp; 자동화 예약&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1199&quot; data-start=&quot;1179&quot; data-ke-size=&quot;size23&quot;&gt;4. AI 에이전트의 성장&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1267&quot; data-start=&quot;1200&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1237&quot; data-start=&quot;1200&quot;&gt;고객 상담, 정보 제공 등 &lt;b&gt;상호작용형 에이전트&lt;/b&gt; 대중화&lt;/li&gt;
&lt;li data-end=&quot;1267&quot; data-start=&quot;1238&quot;&gt;  사람과 대화하듯 AI와 상호작용하는 시대&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1304&quot; data-start=&quot;1269&quot; data-ke-size=&quot;size23&quot;&gt;5. 설명 가능한 AI (Explainable AI)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1356&quot; data-start=&quot;1305&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1339&quot; data-start=&quot;1305&quot;&gt;&quot;AI가 왜 그렇게 판단했는지&quot;를 &lt;b&gt;투명하게 설명&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1356&quot; data-start=&quot;1340&quot;&gt;신뢰도 &amp;amp; 책임성 강화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1374&quot; data-start=&quot;1358&quot; data-ke-size=&quot;size23&quot;&gt;6. 멀티모달 AI&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1429&quot; data-start=&quot;1375&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1408&quot; data-start=&quot;1375&quot;&gt;텍스트, 이미지, 음성까지 &lt;b&gt;동시 이해 &amp;amp; 처리&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1429&quot; data-start=&quot;1409&quot;&gt;UX를 한층 더 풍부하게  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1450&quot; data-start=&quot;1431&quot; data-ke-size=&quot;size23&quot;&gt;7. AI M&amp;amp;A 활발화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1510&quot; data-start=&quot;1451&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1486&quot; data-start=&quot;1451&quot;&gt;AI 스타트업 &amp;harr; 빅테크 기업 간 &lt;b&gt;기술 확보 경쟁&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1510&quot; data-start=&quot;1487&quot;&gt;M&amp;amp;A를 통한 &lt;b&gt;생태계 가속화&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1515&quot; data-start=&quot;1512&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1535&quot; data-start=&quot;1517&quot; data-ke-size=&quot;size26&quot;&gt;  산업별 AI 활용 현황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1765&quot; data-start=&quot;1537&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1566&quot; data-start=&quot;1537&quot;&gt;  &lt;b&gt;금융&lt;/b&gt; : 사기 탐지, 리스크 평가&lt;/li&gt;
&lt;li data-end=&quot;1603&quot; data-start=&quot;1567&quot;&gt;  &lt;b&gt;의료&lt;/b&gt; : 의료 이미지 분석, 개인 맞춤형 치료&lt;/li&gt;
&lt;li data-end=&quot;1634&quot; data-start=&quot;1604&quot;&gt;  &lt;b&gt;제조업&lt;/b&gt; : 생산 최적화, 품질 관리&lt;/li&gt;
&lt;li data-end=&quot;1668&quot; data-start=&quot;1635&quot;&gt;  &lt;b&gt;농업&lt;/b&gt; : 스마트 농기계, 작물 성장 예측&lt;/li&gt;
&lt;li data-end=&quot;1702&quot; data-start=&quot;1669&quot;&gt;✈️ &lt;b&gt;관광&lt;/b&gt; : 맞춤형 여행 추천, 예약 자동화&lt;/li&gt;
&lt;li data-end=&quot;1736&quot; data-start=&quot;1703&quot;&gt;  &lt;b&gt;미디어&lt;/b&gt; : 콘텐츠 추천, 자동 뉴스 생성&lt;/li&gt;
&lt;li data-end=&quot;1765&quot; data-start=&quot;1737&quot;&gt;⚖️ &lt;b&gt;법률&lt;/b&gt; : 계약 분석, 법률 상담&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;1827&quot; data-start=&quot;1767&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1827&quot; data-start=&quot;1769&quot; data-ke-size=&quot;size16&quot;&gt;산업마다 AI가 적용되는 방식은 다르지만, 공통적으로 &lt;b&gt;생산성과 효율성 &amp;rarr; 극대화&lt;/b&gt;하고 있습니다.&lt;/p&gt;
&lt;/blockquote&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;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGZy1I/btsP2FxDGsd/x22OQxgh1G7E7C17SmjG01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGZy1I/btsP2FxDGsd/x22OQxgh1G7E7C17SmjG01/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGZy1I/btsP2FxDGsd/x22OQxgh1G7E7C17SmjG01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGZy1I%2FbtsP2FxDGsd%2Fx22OQxgh1G7E7C17SmjG01%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;1080&quot; height=&quot;837&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1971&quot; data-start=&quot;1829&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://gscaltexmediahub.com/future/2025aitrend/&quot; data-end=&quot;1969&quot; data-start=&quot;1911&quot;&gt;이미지 출처&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr data-end=&quot;1976&quot; data-start=&quot;1973&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1993&quot; data-start=&quot;1978&quot; data-ke-size=&quot;size26&quot;&gt;  AI의 미래 전망&lt;/h2&gt;
&lt;p data-end=&quot;2036&quot; data-start=&quot;1995&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 AI는 &lt;b&gt;더 넓고, 더 깊게&lt;/b&gt; 우리의 삶에 스며들 것입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2160&quot; data-start=&quot;2038&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2073&quot; data-start=&quot;2038&quot;&gt;&lt;b&gt;AI의 보편화&lt;/b&gt; : 모든 산업 &amp;amp; 일상 속으로 확산&lt;/li&gt;
&lt;li data-end=&quot;2123&quot; data-start=&quot;2074&quot;&gt;&lt;b&gt;AI와 인간 협업&lt;/b&gt; : 인간의 창의성 + AI의 연산 능력 &amp;rarr; 시너지 극대화&lt;/li&gt;
&lt;li data-end=&quot;2160&quot; data-start=&quot;2124&quot;&gt;&lt;b&gt;정교화 &amp;amp; 고도화&lt;/b&gt; : 점점 더 스마트한 AI로 진화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2256&quot; data-start=&quot;2162&quot; data-ke-size=&quot;size16&quot;&gt;  결론적으로, 2024년의 AI가 &lt;b&gt;&amp;ldquo;대중화의 문을 열었다면&amp;rdquo;&lt;/b&gt;,&lt;br /&gt;2025년의 AI는 **&amp;ldquo;가치를 창출하며 삶을 바꾸는 동반자&amp;rdquo;**로 자리 잡고 있습니다.&lt;/p&gt;
&lt;hr data-end=&quot;2261&quot; data-start=&quot;2258&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;2346&quot; data-start=&quot;2263&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;마무리 한 줄&lt;/b&gt;&lt;br /&gt;AI는 이제 기술 그 이상입니다.&lt;br /&gt;우리가 어떻게 활용하느냐에 따라 &lt;b&gt;미래의 경쟁력이 달라질 것&lt;/b&gt;입니다.  &lt;/p&gt;
&lt;p data-end=&quot;2346&quot; data-start=&quot;2263&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2346&quot; data-start=&quot;2263&quot; data-ke-size=&quot;size16&quot;&gt;#AI#AI트렌드 #2024AI #2025AI #GenerativeAI #산업별AI #초개인화 #AI에이전트 #AI윤리 #AI미래&lt;/p&gt;</description>
      <category>2024AI</category>
      <category>2025ai</category>
      <category>Ai</category>
      <category>Ai미래</category>
      <category>AI에이전트</category>
      <category>AI윤리</category>
      <category>AI트렌드</category>
      <category>generativeAI</category>
      <category>산업별ai</category>
      <category>초개인화</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/112</guid>
      <comments>https://ji-frontdev.tistory.com/entry/2024%EB%85%84-vs-2025%EB%85%84-AI-%ED%8A%B8%EB%A0%8C%EB%93%9C-%EB%AD%90%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EC%9D%84%EA%B9%8C-%F0%9F%A4%96#entry112comment</comments>
      <pubDate>Fri, 22 Aug 2025 07:00:39 +0900</pubDate>
    </item>
    <item>
      <title>정육각, 초록마을 인수 후.. 유니콘 후보에서 회생 절차까지</title>
      <link>https://ji-frontdev.tistory.com/entry/%EC%A0%95%EC%9C%A1%EA%B0%81-%EC%B4%88%EB%A1%9D%EB%A7%88%EC%9D%84-%EC%9D%B8%EC%88%98-%ED%9B%84-%EC%9C%A0%EB%8B%88%EC%BD%98-%ED%9B%84%EB%B3%B4%EC%97%90%EC%84%9C-%ED%9A%8C%EC%83%9D-%EC%A0%88%EC%B0%A8%EA%B9%8C%EC%A7%80</link>
      <description>&lt;h1&gt;정육각, 초록마을 인수 후 자본잠식&amp;hellip; 무엇이 잘못되었나?&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciHsCx/btsPS0KqVX8/2NPWKyKsnbv9OQ21KBJhg0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciHsCx/btsPS0KqVX8/2NPWKyKsnbv9OQ21KBJhg0/img.jpg&quot; data-alt=&quot;초록마을&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciHsCx/btsPS0KqVX8/2NPWKyKsnbv9OQ21KBJhg0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciHsCx%2FbtsPS0KqVX8%2F2NPWKyKsnbv9OQ21KBJhg0%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;640&quot; height=&quot;343&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초록마을&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmFNwr/btsPUVOoXtM/O05X5IA0fB7Vcg9rBXEKkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmFNwr/btsPUVOoXtM/O05X5IA0fB7Vcg9rBXEKkk/img.png&quot; data-alt=&quot;정육각&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmFNwr/btsPUVOoXtM/O05X5IA0fB7Vcg9rBXEKkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmFNwr%2FbtsPUVOoXtM%2FO05X5IA0fB7Vcg9rBXEKkk%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;1193&quot; height=&quot;1137&quot; data-origin-width=&quot;1193&quot; data-origin-height=&quot;1137&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정육각&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 관점에서 본 기업 분석&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  유니콘 후보에서 회생 절차까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2016년 설립된 정육각은 &quot;도축 후 4일 이내 배송&quot;을 핵심 가치로 내세운 푸드테크 스타트업입니다. &lt;br /&gt;2019~2021년 동안 누적 투자금 약 1,200억 원을 유치했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년에는 기업가치 4,000억 원을 인정받아 유니콘 후보로 주목받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 해, 정육각은 유기농&amp;middot;친환경 식품 유통업체 초록마을을 약 900억 원에 인수했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 3년도 채 되지 않아 양사 모두 자본잠식과 회생 절차에 들어갔습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;[내 분석]&lt;/b&gt; &quot;온라인 신선식품 기술 + 전국 오프라인 유통망&quot;의 시너지를 노린 전략으로 보이지만, 결과적으로는 기술과 사업모델의 부조화가 주요 실패 원인이었다고 판단됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 정육각의 기술적 강점 (공개 정보 기반)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정육각은 다음과 같은 IT 기반 신선식품 공급망(SCM) 기술을 보유했다고 공개했습니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 기반 수요 예측&lt;/h3&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;AI 기반 수요 예측 시스템 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜드체인 물류 자동화&lt;/h3&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도축-배송 시간 단축&lt;/h3&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀필먼트 통합 플랫폼&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;B2C 및 B2B 납품 물량 통합 관리 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  초록마을 인수: 기대와 현실&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;초록마을의 300여 개 매장을 통한 오프라인 확장&lt;/li&gt;
&lt;li&gt;유기농 야채, 가공식품 등 제품 포트폴리오 다각화&lt;/li&gt;
&lt;li&gt;프리미엄&amp;middot;신선 브랜드 이미지 상호 강화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;인수 자금 900억 원 중 &lt;b&gt;370억 원을 단기 차입&lt;/b&gt;으로 조달(*단기 차입: &lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;다른 외부로부터 빌린 돈)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;코로나19 이후 오프라인 매장 방문객 감소&lt;/li&gt;
&lt;li&gt;2023-2024년 투자 시장 위축으로 추가 자금 유치 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&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;&lt;b&gt;기술-제품 부조화 문제&lt;/b&gt;: 정육각의 초신선 공급망 기술은 빠른 회전율을 가진 신선 정육에 최적화되어 있었습니다. 반면 초록마을의 주력 상품인 가공&amp;middot;포장 유기농식품은 상대적으로 긴 유통기한을 가지며 다른 공급망 구조를 필요로 합니다. 이는 마치 F1 레이싱카로 일반 도로를 달리는 것과 같은 기술-사업 불일치였다고 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타이밍의 불운&lt;/b&gt;: 코로나19라는 예측 불가능한 변수가 오프라인 중심의 초록마을 사업에 치명타를 가했습니다. 하지만 이런 리스크에 대한 사전 대비가 부족했던 것으로 판단됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 인수 직후 (2022년)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;900억 원 인수 자금 중 370억 원 차입금 발생&lt;/li&gt;
&lt;li&gt;재무 레버리지 급상승&lt;/li&gt;
&lt;li&gt;통합 비용 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 외부 환경 악화 (2023년)&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 현금흐름 악화 (2024년)&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 자본잠식 (2024년 말)&lt;/h3&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;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;재무적으로 보면 정육각은 매출은 늘었지만, 계속해서 큰 영업손실&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2019년: 매출 41억, 영업손실 26억&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2020년: 매출 162억, 영업손실 79억&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2021년: 매출 401억, 영업손실 249억&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2023년: 매출 282.5억, 영업손실 67.4억, 당기순손실 314.8억&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;2022년: 영업손실 82억&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2023년: 영업손실 67억&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 단계별로 보면 재무 위험 관리의 부재가 가장 큰 문제였다고 생각합니다. 특히 대규모 인수 시 최악의 시나리오에 대한 대비책이 부족했던 것으로 보입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  기술 관점에서의 핵심 문제 분석&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 시스템 아키텍처의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정육각의 SCM 시스템은 고속회전 상품군에 특화된 아키텍처로 설계되었을 것입니다. 실시간 재고 관리, 빠른 의사결정 프로세스, 짧은 리드타임 최적화 등이 핵심이었겠죠. 하지만 이런 시스템을 저속회전 상품군에 그대로 적용하면 오히려 오버엔지니어링이 되어 비효율을 초래합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 품질과 예측 정확도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 거래 데이터는 실시간성과 정확도가 높지만, 오프라인 매장 데이터는 그렇지 않습니다. 결국 AI 모델의 입력 데이터 품질이 떨어지면서 예측 정확도도 함께 하락했을 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기술 투자 ROI의 급속한 악화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 확장과 유지보수 비용은 기하급수적으로 증가하는 반면, 매출 증가는 선형적이었을 가능성이 높습니다. 특히 서로 다른 사업 모델을 하나의 플랫폼에서 운영하려다 보니 복잡성만 늘어났을 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  사례에서 배우는 교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정육각-초록마을 사례는 높은 기업가치를 인정받은 푸드테크 스타트업도 M&amp;amp;A 후 3년 내에 회생 절차에 들어갈 수 있음을 보여준 실제 사례입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;[내가 생각하는 핵심 교훈들]&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기술의 도메인 특화성을 간과하지 말자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 뛰어난 기술이라도 특정 도메인에 최적화된 경우, 다른 영역으로의 확장은 신중해야 합니다. 기존 기술을 재활용하려는 욕심보다는, 새로운 도메인에 맞는 별도의 기술 스택을 고려하는 것이 더 현명할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. M&amp;amp;A는 기술 통합이 아닌 사업 통합이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 기술 기업들이 M&amp;amp;A를 통해 기술적 시너지를 기대하지만, 실제로는 사업 모델, 조직 문화, 고객층의 통합이 더 중요합니다. 기술은 그 다음 문제입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 현금흐름이 기술보다 우선이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 혁신적인 기술을 보유해도 현금이 떨어지면 게임 오버입니다. 특히 대규모 인수 시에는 보수적인 재무 계획이 필수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 시장 환경 변화에 대한 민감도를 높여야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코로나19 같은 외부 충격은 예측하기 어렵지만, 그런 상황에서도 생존할 수 있는 사업 구조를 미리 고민해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;정육각은 2016년 설립부터 2024년 회생 절차까지 8년간의 여정, 초록마을과의 합병은 2022년부터 시작되어 약 2년 만에 실패로 끝났습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&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;정육각의 기술은 분명 인상적이었습니다. 하지만 그 기술을 잘못된 방향으로 확장하려다가 결국 본업의 경쟁력마저 잃게 된 것 같아 아쉽습니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>프론트 개발자의 이직 여정</category>
      <category>기업분석</category>
      <category>스타트업분석</category>
      <category>유니콘기업</category>
      <category>정육각</category>
      <category>초록마을</category>
      <category>푸드테크</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/111</guid>
      <comments>https://ji-frontdev.tistory.com/entry/%EC%A0%95%EC%9C%A1%EA%B0%81-%EC%B4%88%EB%A1%9D%EB%A7%88%EC%9D%84-%EC%9D%B8%EC%88%98-%ED%9B%84-%EC%9C%A0%EB%8B%88%EC%BD%98-%ED%9B%84%EB%B3%B4%EC%97%90%EC%84%9C-%ED%9A%8C%EC%83%9D-%EC%A0%88%EC%B0%A8%EA%B9%8C%EC%A7%80#entry111comment</comments>
      <pubDate>Fri, 15 Aug 2025 11:03:19 +0900</pubDate>
    </item>
    <item>
      <title>[react/ts] 한글 Base64 디코딩 삽질기: atob만으로는 안 되는 이유와 해결법(atob,escape,decodeURIComponent,TextDecoder)</title>
      <link>https://ji-frontdev.tistory.com/entry/reactts-%ED%95%9C%EA%B8%80-Base64-%EB%94%94%EC%BD%94%EB%94%A9-%EC%82%BD%EC%A7%88%EA%B8%B0-atob%EB%A7%8C%EC%9C%BC%EB%A1%9C%EB%8A%94-%EC%95%88-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%ED%95%B4%EA%B2%B0%EB%B2%95atobescapedecodeURIComponentTextDecoder</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;삽질의 시작  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 프로젝트를 진행하던 중, 서버에서 Base64로 인코딩된 한글 데이터를 받아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 디코딩해야 하는 상황이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Base64 디코딩이야 간단하지!&quot; 하며 당연히 atob()를 사용했는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 서버에서 받은 Base64 인코딩된 한글
const base64Data = &quot;7JWI64WV7ZWY7IS47JqU&quot;; // &quot;안녕하세요&quot;를 base64로 인코딩한 값

// 단순한 접근
const decoded = atob(base64Data);
console.log(decoded); // &amp;igrave;???&amp;iacute;&amp;igrave;&amp;cedil;&amp;igrave;! (깨진 한글...)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;&lt;i&gt;&lt;b&gt;결과는 참담한 한글 깨짐 현상&lt;/b&gt;이었습니다. 분명 서버에서는 제대로 인코딩했는데 왜 깨질까요?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 문제를 해결하기 위해 escape와 decodeURIComponent를 사용해야 했고, &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;그 과정에서 인코딩/디코딩의 원리를 제대로 이해하게 되었습니다.&lt;/i&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;문제의 원인: atob()의 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;atob()는 Latin-1만 지원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 atob() 함수는 &lt;b&gt;Latin-1(ISO-8859-1) 문자 집합&lt;/b&gt;을 기준으로 동작합니다. 이는 1바이트로 표현되는 서구권 문자만 올바르게 처리할 수 있다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// ASCII/Latin-1 문자는 정상 동작
const englishBase64 = btoa(&quot;Hello World&quot;);
console.log(englishBase64); // &quot;SGVsbG8gV29ybGQ=&quot;
console.log(atob(englishBase64)); // &quot;Hello World&quot; ✅

// 하지만 한글은...
const koreanText = &quot;안녕하세요&quot;;
// btoa(koreanText); // ❌ DOMException 발생!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UTF-8과 Latin-1의 차이점&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 문자별 바이트 분석
const analyzeText = (text: string) =&amp;gt; {
  const utf8Bytes = new TextEncoder().encode(text);
  console.log(`&quot;${text}&quot;`);
  console.log(`UTF-8 bytes: [${Array.from(utf8Bytes).join(', ')}]`);
  console.log(`Byte length: ${utf8Bytes.length}`);
};

analyzeText(&quot;A&quot;);      // UTF-8: [65], Latin-1과 동일
analyzeText(&quot;안&quot;);     // UTF-8: [236, 149, 136], 3바이트!
analyzeText(&quot; &quot;);     // UTF-8: [240, 159, 140, 159], 4바이트!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;올바른 해결법: 단계별 접근&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 전통적인 방법 (Deprecated)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// ⚠️ Deprecated 방법이지만 여전히 작동
const decodeBase64Korean = (base64String: string): string =&amp;gt; {
  try {
    // 1. Base64 디코딩
    const decoded = atob(base64String);
    
    // 2. escape로 URL 인코딩 형태로 변환
    const escaped = escape(decoded);
    
    // 3. decodeURIComponent로 UTF-8 디코딩
    const result = decodeURIComponent(escaped);
    
    return result;
  } catch (error) {
    console.error('Base64 디코딩 실패:', error);
    return '';
  }
};

// 사용 예시
const base64Data = &quot;7JWI64WV7ZWY7IS47JqU&quot;;
const decoded = decodeBase64Korean(base64Data);
console.log(decoded); // &quot;안녕하세요&quot; ✅&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 모던 방법: TextDecoder 활용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;const decodeBase64KoreanModern = (base64String: string): string =&amp;gt; {
  try {
    // 1. Base64를 Uint8Array로 변환
    const binaryString = atob(base64String);
    const bytes = new Uint8Array(binaryString.length);
    
    for (let i = 0; i &amp;lt; binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    
    // 2. TextDecoder로 UTF-8 디코딩
    const decoder = new TextDecoder('utf-8');
    return decoder.decode(bytes);
  } catch (error) {
    console.error('Base64 디코딩 실패:', error);
    return '';
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 가장 모던한 방법: Web API 활용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 최신 브라우저 지원 (IE 제외)
const decodeBase64KoreanWebAPI = async (base64String: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
  try {
    // Base64 문자열을 Blob으로 변환
    const response = await fetch(`data:text/plain;base64,${base64String}`);
    const text = await response.text();
    return text;
  } catch (error) {
    console.error('Base64 디코딩 실패:', error);
    return '';
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React에서 실전 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 훅으로 만들기&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import { useState, useCallback } from 'react';

interface UseBase64DecoderReturn {
  decode: (base64String: string) =&amp;gt; string;
  isLoading: boolean;
  error: string | null;
}

export const useBase64Decoder = (): UseBase64DecoderReturn =&amp;gt; {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState&amp;lt;string | null&amp;gt;(null);

  const decode = useCallback((base64String: string): string =&amp;gt; {
    setIsLoading(true);
    setError(null);

    try {
      // 입력값 검증
      if (!base64String || typeof base64String !== 'string') {
        throw new Error('유효하지 않은 Base64 문자열입니다.');
      }

      // Base64 형식 검증
      const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
      if (!base64Regex.test(base64String)) {
        throw new Error('Base64 형식이 올바르지 않습니다.');
      }

      // 디코딩 실행
      const binaryString = atob(base64String);
      const bytes = new Uint8Array(binaryString.length);
      
      for (let i = 0; i &amp;lt; binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
      }
      
      const decoder = new TextDecoder('utf-8');
      const result = decoder.decode(bytes);
      
      setIsLoading(false);
      return result;
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : '디코딩 중 오류가 발생했습니다.';
      setError(errorMessage);
      setIsLoading(false);
      return '';
    }
  }, []);

  return { decode, isLoading, error };
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React 컴포넌트에서 사용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import React, { useState } from 'react';
import { useBase64Decoder } from './hooks/useBase64Decoder';

const Base64DecoderComponent: React.FC = () =&amp;gt; {
  const [input, setInput] = useState('');
  const [result, setResult] = useState('');
  const { decode, isLoading, error } = useBase64Decoder();

  const handleDecode = () =&amp;gt; {
    const decoded = decode(input);
    setResult(decoded);
  };

  return (
    &amp;lt;div className=&quot;p-4 max-w-md mx-auto&quot;&amp;gt;
      &amp;lt;h2 className=&quot;text-xl font-bold mb-4&quot;&amp;gt;Base64 한글 디코더&amp;lt;/h2&amp;gt;
      
      &amp;lt;div className=&quot;mb-4&quot;&amp;gt;
        &amp;lt;label className=&quot;block text-sm font-medium mb-2&quot;&amp;gt;
          Base64 문자열:
        &amp;lt;/label&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          value={input}
          onChange={(e) =&amp;gt; setInput(e.target.value)}
          className=&quot;w-full p-2 border rounded&quot;
          placeholder=&quot;7JWI64WV7ZWY7IS47JqU&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;button
        onClick={handleDecode}
        disabled={isLoading || !input}
        className=&quot;w-full bg-blue-500 text-white p-2 rounded disabled:opacity-50&quot;
      &amp;gt;
        {isLoading ? '디코딩 중...' : '디코딩'}
      &amp;lt;/button&amp;gt;

      {error &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;mt-4 p-2 bg-red-100 text-red-700 rounded&quot;&amp;gt;
          {error}
        &amp;lt;/div&amp;gt;
      )}

      {result &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;mt-4 p-2 bg-green-100 rounded&quot;&amp;gt;
          &amp;lt;strong&amp;gt;결과:&amp;lt;/strong&amp;gt; {result}
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
};

export default Base64DecoderComponent;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버와의 협업 시 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서버 측 인코딩 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;processing&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Node.js 서버에서 올바른 인코딩
const text = &quot;안녕하세요&quot;;
const base64 = Buffer.from(text, 'utf8').toString('base64');
console.log(base64); // &quot;7JWI64WV7ZWY7IS47JqU&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. API 응답 헤더 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dart&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Fetch 시 응답 헤더 확인
const fetchData = async () =&amp;gt; {
  const response = await fetch('/api/data');
  const contentType = response.headers.get('content-type');
  console.log('Content-Type:', contentType); // application/json; charset=utf-8
  
  const data = await response.json();
  return data;
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. TypeScript 타입 정의&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;interface ApiResponse {
  encodedMessage: string; // Base64 인코딩된 메시지
  timestamp: number;
  userId: string;
}

interface DecodedMessage {
  originalMessage: string; // 디코딩된 원본 메시지
  decodedAt: Date;
}

const processApiResponse = (response: ApiResponse): DecodedMessage =&amp;gt; {
  const { decode } = useBase64Decoder();
  
  return {
    originalMessage: decode(response.encodedMessage),
    decodedAt: new Date()
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인코딩/디코딩의 핵심 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ASCII의 역할과 한계&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;1c&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// ASCII는 0-127 범위의 문자만 표현
console.log('A'.charCodeAt(0)); // 65 (ASCII)
console.log('안'.charCodeAt(0)); // 50504 (Unicode, ASCII 범위 초과!)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유니코드와 UTF-8&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;const explainUnicode = (char: string) =&amp;gt; {
  const codePoint = char.codePointAt(0)!;
  const utf8Bytes = new TextEncoder().encode(char);
  
  console.log(`문자: ${char}`);
  console.log(`유니코드 코드 포인트: U+${codePoint.toString(16).toUpperCase()}`);
  console.log(`UTF-8 바이트: [${Array.from(utf8Bytes).map(b =&amp;gt; `0x${b.toString(16)}`).join(', ')}]`);
};

explainUnicode('A');    // 1바이트
explainUnicode('안');   // 3바이트
explainUnicode(' ');   // 4바이트&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 최적화 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 메모이제이션 활용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import { useMemo } from 'react';

const useMemoizedDecoder = (base64String: string) =&amp;gt; {
  return useMemo(() =&amp;gt; {
    if (!base64String) return '';
    
    try {
      const binaryString = atob(base64String);
      const bytes = new Uint8Array(binaryString.length);
      
      for (let i = 0; i &amp;lt; binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
      }
      
      return new TextDecoder('utf-8').decode(bytes);
    } catch {
      return '';
    }
  }, [base64String]);
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 워커 활용 (대용량 데이터)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// worker.ts
self.onmessage = function(e) {
  const { base64Data } = e.data;
  
  try {
    const binaryString = atob(base64Data);
    const bytes = new Uint8Array(binaryString.length);
    
    for (let i = 0; i &amp;lt; binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    
    const result = new TextDecoder('utf-8').decode(bytes);
    self.postMessage({ success: true, result });
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};

// 메인 스레드에서 사용
const decodeWithWorker = (base64String: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    const worker = new Worker('/decoder-worker.js');
    
    worker.postMessage({ base64Data: base64String });
    
    worker.onmessage = (e) =&amp;gt; {
      const { success, result, error } = e.data;
      worker.terminate();
      
      if (success) {
        resolve(result);
      } else {
        reject(new Error(error));
      }
    };
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디버깅과 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단위 테스트&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// base64Decoder.test.ts
import { describe, it, expect } from 'vitest';

describe('Base64 한글 디코더', () =&amp;gt; {
  const testCases = [
    { input: '7JWI64WV7ZWY7IS47JqU', expected: '안녕하세요' },
    { input: 'SGVsbG8=', expected: 'Hello' },
    { input: '7ZWc6riAIOuniOS9nOycvA==', expected: '한글 테스트' }
  ];

  testCases.forEach(({ input, expected }) =&amp;gt; {
    it(`should decode &quot;${input}&quot; to &quot;${expected}&quot;`, () =&amp;gt; {
      const result = decodeBase64Korean(input);
      expect(result).toBe(expected);
    });
  });

  it('should handle invalid input gracefully', () =&amp;gt; {
    expect(decodeBase64Korean('')).toBe('');
    expect(decodeBase64Korean('invalid!')).toBe('');
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 브라우저 호환성 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;const checkBrowserSupport = () =&amp;gt; {
  const support = {
    textDecoder: typeof TextDecoder !== 'undefined',
    textEncoder: typeof TextEncoder !== 'undefined',
    atob: typeof atob !== 'undefined',
    fetch: typeof fetch !== 'undefined'
  };

  console.log('브라우저 지원 현황:', support);
  return support;
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 프로덕션 환경에서의 예외 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 에러 바운더리&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class Base64DecoderErrorBoundary extends Component&amp;lt;Props, State&amp;gt; {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Base64 디코딩 에러:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        &amp;lt;div className=&quot;p-4 bg-red-50 border border-red-200 rounded&quot;&amp;gt;
          &amp;lt;h3 className=&quot;text-red-800 font-semibold&quot;&amp;gt;디코딩 오류&amp;lt;/h3&amp;gt;
          &amp;lt;p className=&quot;text-red-600&quot;&amp;gt;
            Base64 디코딩 중 오류가 발생했습니다. 데이터를 확인해주세요.
          &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      );
    }

    return this.props.children;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로깅과 모니터링&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;const logDecodingAttempt = (base64String: string, success: boolean, error?: string) =&amp;gt; {
  const logData = {
    timestamp: new Date().toISOString(),
    inputLength: base64String.length,
    success,
    error,
    userAgent: navigator.userAgent
  };

  // 프로덕션에서는 실제 로깅 서비스로 전송
  console.log('Base64 디코딩 로그:', logData);
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리: 삽질에서 얻은 교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 atob()만 사용하면 될 줄 알았던 Base64 한글 디코딩 문제가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인코딩/디코딩의 깊은 원리를 이해하는 계기가 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 포인트&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;atob()는 Latin-1 기준&lt;/b&gt;이므로 한글 처리에 한계가 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UTF-8과 Latin-1의 차이&lt;/b&gt;를 이해해야 올바른 해결책을 찾을 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TextDecoder API&lt;/b&gt;를 활용하면 모던하고 안전한 처리가 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입스크립트와 React&lt;/b&gt;를 활용해 재사용 가능한 컴포넌트 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&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;/li&gt;
&lt;li&gt;&lt;b&gt;브라우저 호환성과 성능&lt;/b&gt;을 함께 고려해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리와 사용자 경험&lt;/b&gt;도 빼먹지 말아야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>atob</category>
      <category>atob 한글 깨짐</category>
      <category>Base64 한글 디코딩</category>
      <category>decodeURIComponent</category>
      <category>Escape</category>
      <category>React Base64</category>
      <category>TextDecoder</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/110</guid>
      <comments>https://ji-frontdev.tistory.com/entry/reactts-%ED%95%9C%EA%B8%80-Base64-%EB%94%94%EC%BD%94%EB%94%A9-%EC%82%BD%EC%A7%88%EA%B8%B0-atob%EB%A7%8C%EC%9C%BC%EB%A1%9C%EB%8A%94-%EC%95%88-%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%ED%95%B4%EA%B2%B0%EB%B2%95atobescapedecodeURIComponentTextDecoder#entry110comment</comments>
      <pubDate>Wed, 6 Aug 2025 05:45:46 +0900</pubDate>
    </item>
    <item>
      <title>ECMAScript 2025(ES2025)의 핵심 변경 사항</title>
      <link>https://ji-frontdev.tistory.com/entry/ECMAScript-2025ES2025%EC%9D%98-%ED%95%B5%EC%8B%AC-%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD</link>
      <description>&lt;h1 data-end=&quot;148&quot; data-start=&quot;109&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2025년 6월 25일&lt;/b&gt;, 제129회 Ecma 총회에서 &lt;b&gt;ECMAScript 2025&lt;/b&gt; 언어 사양이 공식 승인되었습니다.&lt;br /&gt;이번 업데이트는 JavaScript의 실용성과 생산성을 높이는 핵심 기능들이 대거 포함되어 있어, 개발자 경험을 한 단계 끌어올리는 중요한 전환점이 될 것으로 기대됩니다.&lt;/p&gt;
&lt;hr data-end=&quot;396&quot; data-start=&quot;393&quot; data-ke-style=&quot;style1&quot; /&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;Iterator Helpers&lt;/b&gt;: 함수형 프로그래밍 패턴을 반복자에 적용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Set Methods&lt;/b&gt;: 교집합, 합집합 등 집합 연산의 표준화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;JSON Modules&lt;/b&gt;: JSON 파일을 import로 직접 불러오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Promise.try()&lt;/b&gt;: 동기/비동기 오류 처리의 간결화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Float16Array&lt;/b&gt;: 메모리 효율적인 16비트 부동소수점 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;정규표현식 개선&lt;/b&gt;: 유니코드, 이모지, 캡처 그룹 개선 등&lt;/p&gt;
&lt;hr data-end=&quot;691&quot; data-start=&quot;688&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.   Iterator Helpers - 반복자의 함수형 혁신&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4t1s/btsPql8Bug3/Ggd1TEWOASc1jQwdhlJupk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4t1s/btsPql8Bug3/Ggd1TEWOASc1jQwdhlJupk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4t1s/btsPql8Bug3/Ggd1TEWOASc1jQwdhlJupk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4t1s%2FbtsPql8Bug3%2FGgd1TEWOASc1jQwdhlJupk%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;659&quot; height=&quot;313&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;313&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;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 배열로 변환 후 .filter(), .map() 체인을 사용했지만&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ES2025부터는 반복자 자체에 지연 평가(lazy evaluation)&lt;/b&gt; 기반 연산이 가능해졌습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;대용량 데이터 처리에 특히 적합합니다.&lt;/p&gt;
&lt;hr data-end=&quot;1063&quot; data-start=&quot;1060&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.   Set Methods - 집합 연산 표준화&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be7h2a/btsPqxAVeAo/5DIPwnKzSCeIPBil6S2dZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be7h2a/btsPqxAVeAo/5DIPwnKzSCeIPBil6S2dZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be7h2a/btsPqxAVeAo/5DIPwnKzSCeIPBil6S2dZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe7h2a%2FbtsPqxAVeAo%2F5DIPwnKzSCeIPBil6S2dZ1%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;269&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;lodash 등의 유틸리티 없이 &lt;b&gt;직접 집합 연산이 가능&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;데이터 분석, 중복 제거, 교차 데이터 집계 등에 유용&lt;/p&gt;
&lt;hr data-end=&quot;1516&quot; data-start=&quot;1513&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.   JSON Modules - JSON 파일 직접 import&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B4gtJ/btsPrO2LSJx/R51cYghQeFOHGBfsKmcC20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B4gtJ/btsPrO2LSJx/R51cYghQeFOHGBfsKmcC20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B4gtJ/btsPrO2LSJx/R51cYghQeFOHGBfsKmcC20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB4gtJ%2FbtsPrO2LSJx%2FR51cYghQeFOHGBfsKmcC20%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;673&quot; height=&quot;314&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;JSON 파일을 fetch나 번들링 없이 &lt;b&gt;모듈처럼 import 가능&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;정적 설정, 환경 구성 관리가 쉬워짐&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Node.js 22+, Chrome 126+, TypeScript 5.5+ 대응 필요&lt;/p&gt;
&lt;hr data-end=&quot;1905&quot; data-start=&quot;1902&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.   Promise.try() - 에러 처리 통합&lt;/p&gt;
&lt;pre id=&quot;code_1753016041564&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function processData(data) {
  return Promise.try(() =&amp;gt; {
    const result = syncProcess(data);
    return asyncProcess(result);
  });
}

Promise.try(() =&amp;gt; {
  if (Math.random() &amp;lt; 0.5) throw new Error(&quot;실패!&quot;);
  return &quot;성공!&quot;;
})
.then(console.log)
.catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 try/catch + Promise 조합 필요&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이제는 &lt;b&gt;동기/비동기 코드를 하나의 흐름으로 처리&lt;/b&gt;&lt;/p&gt;
&lt;hr data-end=&quot;2293&quot; data-start=&quot;2290&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5.   Float16Array - 고성능 16비트 부동소수점&lt;/p&gt;
&lt;pre id=&quot;code_1753016049596&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const f16 = new Float16Array([1.5, 2.7, 3.14]);
console.log(f16.length); // 3

const rounded = Math.f16round(3.14159265359);
console.log(rounded); // 3.140625&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;Float32Array 대비 &lt;b&gt;메모리 절반&lt;/b&gt; 사용&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;WebGL, 머신러닝, 고성능 수치 연산에서 필수&lt;/p&gt;
&lt;hr data-end=&quot;2573&quot; data-start=&quot;2570&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.   정규표현식 개선&lt;/p&gt;
&lt;pre id=&quot;code_1753016062862&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 유니코드 스크립트 매칭
const hangulRegex = /\p{Script=Hangul}+/u;
console.log(&quot;안녕하세요 Hello&quot;.match(hangulRegex)); // [&quot;안녕하세요&quot;]

// 이모지 필터링
const emojiRegex = /\p{Emoji}/u;
console.log(&quot; &quot;.match(emojiRegex)); // [&quot; &quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;향상된 유니코드 처리&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;정규식 inline flag (?i:HELLO) 사용 가능&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;중복된 named group을 /v 플래그로 처리 가능&lt;/p&gt;
&lt;hr data-end=&quot;2908&quot; data-start=&quot;2905&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  브라우저 및 플랫폼 지원&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-end=&quot;4319&quot; data-start=&quot;4263&quot; data-ke-size=&quot;size16&quot;&gt;TypeScript 5.5 이상 + lib: [&quot;esnext&quot;] 설정으로 대부분의 기능 사용 가능&lt;/p&gt;
&lt;hr data-end=&quot;4324&quot; data-start=&quot;4321&quot; data-ke-style=&quot;style1&quot; /&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;: Float16Array, lazy iterator&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생산성 향상&lt;/b&gt;: 표준화된 Set 연산, JSON 모듈 import&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 품질 개선&lt;/b&gt;: Promise.try(), 정규표현식 유틸리티&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이브러리 의존도 감소&lt;/b&gt;: Lodash, Ramda 등의 유틸 함수 대체 가능&lt;/p&gt;
&lt;hr data-end=&quot;4531&quot; data-start=&quot;4528&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  마이그레이션 가이드&lt;/p&gt;
&lt;div&gt;
&lt;p data-end=&quot;4598&quot; data-start=&quot;4551&quot; data-ke-size=&quot;size16&quot;&gt;기존 lodash, 커스텀 헬퍼 함수 등을 ES2025 기능으로 대체할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1753016124500&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 lodash
_.intersection(arr1, arr2);

// ES2025
new Set(arr1).intersection(new Set(arr2));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4808&quot; data-start=&quot;4706&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4747&quot; data-start=&quot;4706&quot;&gt;filter().map().toArray()를 반복자 체인으로 전환&lt;/li&gt;
&lt;li data-end=&quot;4778&quot; data-start=&quot;4748&quot;&gt;설정 관련 JSON 파일은 import로 직접 로딩&lt;/li&gt;
&lt;li data-end=&quot;4808&quot; data-start=&quot;4779&quot;&gt;에러 핸들링에는 Promise.try() 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;4813&quot; data-start=&quot;4810&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;4822&quot; data-start=&quot;4815&quot; data-ke-size=&quot;size26&quot;&gt;✅ 결론&lt;/h2&gt;
&lt;p data-end=&quot;4911&quot; data-start=&quot;4824&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ECMAScript 2025&lt;/b&gt;는 단순한 문법 개선이 아닌, 실질적으로 개발자들의 업무 효율성과 코드 품질을 높여주는 기능이 대거 포함된 업데이트입니다.&lt;/p&gt;
&lt;p data-end=&quot;4940&quot; data-start=&quot;4913&quot; data-ke-size=&quot;size16&quot;&gt;특히 다음과 같은 점에서 강력한 가치가 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5032&quot; data-start=&quot;4942&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4965&quot; data-start=&quot;4942&quot;&gt;함수형 프로그래밍 스타일의 도입과 확장&lt;/li&gt;
&lt;li data-end=&quot;4983&quot; data-start=&quot;4966&quot;&gt;메모리 효율성과 성능 최적화&lt;/li&gt;
&lt;li data-end=&quot;5010&quot; data-start=&quot;4984&quot;&gt;명확한 표준 API로 라이브러리 의존도 감소&lt;/li&gt;
&lt;li data-end=&quot;5032&quot; data-start=&quot;5011&quot;&gt;실제 서비스 코드에 즉시 적용 가능&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend</category>
      <category>ecmascript 2025</category>
      <category>es2025 신기능</category>
      <category>groupby</category>
      <category>js 신기능</category>
      <category>promise.withresolvers</category>
      <category>record tuple</category>
      <category>불변 데이터 구조</category>
      <category>자바스크립트 개발 트렌드</category>
      <category>자바스크립트 최신 문법</category>
      <category>자바스크립트 표준</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/109</guid>
      <comments>https://ji-frontdev.tistory.com/entry/ECMAScript-2025ES2025%EC%9D%98-%ED%95%B5%EC%8B%AC-%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD#entry109comment</comments>
      <pubDate>Mon, 21 Jul 2025 09:00:33 +0900</pubDate>
    </item>
    <item>
      <title>앱 스킴(URL Scheme)&amp;rdquo;의 개념과 Android/iOS 차이, 앱 선택 처리 방식</title>
      <link>https://ji-frontdev.tistory.com/entry/%EC%95%B1-%EC%8A%A4%ED%82%B4URL-Scheme%E2%80%9D%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-AndroidiOS-%EC%B0%A8%EC%9D%B4-%EC%95%B1-%EC%84%A0%ED%83%9D-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EC%8B%9D</link>
      <description>&lt;h1 data-end=&quot;133&quot; data-start=&quot;107&quot;&gt;  앱 스킴(URL Scheme)이란?&lt;/h1&gt;
&lt;h3 data-end=&quot;173&quot; data-start=&quot;134&quot; data-ke-size=&quot;size23&quot;&gt;&amp;ndash; Android와 iOS의 앱 연결 방식과 차이를 이해하기&lt;/h3&gt;
&lt;hr data-end=&quot;178&quot; data-start=&quot;175&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;189&quot; data-start=&quot;180&quot; data-ke-size=&quot;size26&quot;&gt;✨ 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;354&quot; data-start=&quot;190&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;207&quot; data-start=&quot;190&quot;&gt;앱 스킴이란 무엇인가?&lt;/li&gt;
&lt;li data-end=&quot;232&quot; data-start=&quot;208&quot;&gt;앱 스킴과 딥링크는 어떤 관계인가?&lt;/li&gt;
&lt;li data-end=&quot;262&quot; data-start=&quot;233&quot;&gt;Android와 iOS의 앱 스킴 처리 방식&lt;/li&gt;
&lt;li data-end=&quot;287&quot; data-start=&quot;263&quot;&gt;앱 스킴 충돌 시 어떻게 동작할까?&lt;/li&gt;
&lt;li data-end=&quot;306&quot; data-start=&quot;288&quot;&gt;앱 미설치 시 처리 방식&lt;/li&gt;
&lt;li data-end=&quot;325&quot; data-start=&quot;307&quot;&gt;앱 선택 UI 제공 여부&lt;/li&gt;
&lt;li data-end=&quot;344&quot; data-start=&quot;326&quot;&gt;실제 서비스 적용 시 팁&lt;/li&gt;
&lt;li data-end=&quot;354&quot; data-start=&quot;345&quot;&gt;마무리 요약&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;359&quot; data-start=&quot;356&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;381&quot; data-start=&quot;361&quot; data-ke-size=&quot;size26&quot;&gt;1. ✅ 앱 스킴이란 무엇인가?&lt;/h2&gt;
&lt;p data-end=&quot;454&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;**앱 스킴(URL Scheme)**은 웹 브라우저나 다른 앱에서 &lt;b&gt;특정 앱을 직접 실행&lt;/b&gt;하기 위한 일종의 주소 규칙입니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;545&quot; data-start=&quot;456&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;458&quot; data-ke-size=&quot;size16&quot;&gt;예:&lt;br /&gt;kakaotalk://inappbrowser &amp;rarr; 카카오톡 실행&lt;br /&gt;mobileid://requestVP &amp;rarr; 모바일 신분증 앱 실행&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;547&quot; data-ke-size=&quot;size16&quot;&gt;앱이 설치되어 있다면 해당 스킴을 통해 앱을 직접 호출할 수 있고,&lt;br /&gt;파라미터를 함께 전달해 &lt;b&gt;앱 내 특정 기능까지 바로 진입&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;644&quot; data-start=&quot;632&quot; data-ke-size=&quot;size23&quot;&gt;  용어 설명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;763&quot; data-start=&quot;645&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;703&quot; data-start=&quot;645&quot;&gt;&lt;b&gt;URL Scheme&lt;/b&gt;: 앱이 인식할 수 있는 특별한 URL 포맷. 보통 앱이름://경로 형식&lt;/li&gt;
&lt;li data-end=&quot;763&quot; data-start=&quot;704&quot;&gt;&lt;b&gt;딥링크(Deep Link)&lt;/b&gt;: 앱의 내부 특정 화면까지 직접 진입할 수 있도록 도와주는 링크 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;768&quot; data-start=&quot;765&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;797&quot; data-start=&quot;770&quot; data-ke-size=&quot;size26&quot;&gt;2. ✅ 앱 스킴과 딥링크는 어떤 관계인가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;958&quot; data-start=&quot;799&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;826&quot; data-start=&quot;799&quot;&gt;&lt;b&gt;앱 스킴은 딥링크의 한 종류&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-end=&quot;864&quot; data-start=&quot;827&quot;&gt;웹 브라우저에서 앱을 여는 가장 기본적인 방식이 앱 스킴입니다.&lt;/li&gt;
&lt;li data-end=&quot;958&quot; data-start=&quot;865&quot;&gt;최신 방식은 &lt;b&gt;Universal Link(iOS)&lt;/b&gt; 또는 **App Link(Android)**도 존재하지만,&lt;br /&gt;&lt;b&gt;앱 스킴은 여전히 많이 사용됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1145&quot; data-start=&quot;960&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1016&quot; data-start=&quot;988&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;995&quot; data-start=&quot;988&quot;&gt;앱 스킴&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;1016&quot; data-start=&quot;995&quot;&gt;예: myapp://home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1089&quot; data-start=&quot;1017&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1034&quot; data-start=&quot;1017&quot;&gt;Universal Link&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;1089&quot; data-start=&quot;1034&quot;&gt;예: &lt;a href=&quot;https://example.com/path&quot;&gt;https://example.com/path&lt;/a&gt; &amp;ndash; 앱 설치 시 앱으로, 아니면 웹으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1145&quot; data-start=&quot;1090&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1111&quot; data-start=&quot;1090&quot;&gt;App Link (Android)&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;1145&quot; data-start=&quot;1111&quot;&gt;Universal Link와 유사 &amp;ndash; 안전하게 앱 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1150&quot; data-start=&quot;1147&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1184&quot; data-start=&quot;1152&quot; data-ke-size=&quot;size26&quot;&gt;3. ✅ Android와 iOS의 앱 스킴 처리 방식&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 96px;&quot; border=&quot;1&quot; data-end=&quot;1362&quot; data-start=&quot;1186&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;Android&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;iOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1261&quot; data-start=&quot;1234&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1245&quot; data-start=&quot;1234&quot;&gt;스킴 중복 등록&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1252&quot; data-start=&quot;1245&quot;&gt;✅ 가능&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1261&quot; data-start=&quot;1252&quot;&gt;❌ 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1316&quot; data-start=&quot;1262&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1280&quot; data-start=&quot;1262&quot;&gt;스킴 호출 시 앱 선택 UI&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1295&quot; data-start=&quot;1280&quot;&gt;✅ 시스템이 자동 제공&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1316&quot; data-start=&quot;1295&quot;&gt;❌ 없음 (개발자가 직접 구성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1362&quot; data-start=&quot;1317&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1326&quot; data-start=&quot;1317&quot;&gt;앱 우선순위&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1346&quot; data-start=&quot;1326&quot;&gt;사용자 선택 or 기본 앱 설정&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1362&quot; data-start=&quot;1346&quot;&gt;먼저 등록된 앱만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;1376&quot; data-start=&quot;1364&quot; data-ke-size=&quot;size23&quot;&gt;  예시 상황&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1476&quot; data-start=&quot;1377&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1476&quot; data-start=&quot;1377&quot;&gt;mobileid://이라는 스킴을 여러 앱(A, B, C)이 등록한 경우,
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1476&quot; data-start=&quot;1425&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1451&quot; data-start=&quot;1425&quot;&gt;Android: &quot;앱 선택 팝업&quot; 자동 표시&lt;/li&gt;
&lt;li data-end=&quot;1476&quot; data-start=&quot;1454&quot;&gt;iOS: 가장 먼저 등록한 앱만 열림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1481&quot; data-start=&quot;1478&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1510&quot; data-start=&quot;1483&quot; data-ke-size=&quot;size26&quot;&gt;4. ✅ 앱 스킴 충돌 시 어떻게 동작할까?&lt;/h2&gt;
&lt;h3 data-end=&quot;1526&quot; data-start=&quot;1512&quot; data-ke-size=&quot;size23&quot;&gt;  Android&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1607&quot; data-start=&quot;1527&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1607&quot; data-start=&quot;1527&quot;&gt;동일 스킴을 가진 앱이 여러 개 설치된 경우,&lt;br /&gt;시스템이 아래와 같은 **앱 선택 창(Chooser Dialog)**을 자동으로 띄움:&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;1677&quot; data-start=&quot;1609&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1677&quot; data-start=&quot;1611&quot; data-ke-size=&quot;size16&quot;&gt;&quot;다음 앱으로 열기&quot;&lt;br /&gt;  지갑24&lt;br /&gt;  PASS&lt;br /&gt;  삼성월렛&lt;br /&gt;[이번 한 번만] [항상]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1716&quot; data-start=&quot;1679&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 사용자는 원하는 앱을 선택하고, 항상 이 앱으로 열 수도 있음.&lt;/p&gt;
&lt;h3 data-end=&quot;1728&quot; data-start=&quot;1718&quot; data-ke-size=&quot;size23&quot;&gt;  iOS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1834&quot; data-start=&quot;1729&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1753&quot; data-start=&quot;1729&quot;&gt;동일 스킴을 두 개 이상 등록 불가.&lt;/li&gt;
&lt;li data-end=&quot;1795&quot; data-start=&quot;1754&quot;&gt;&lt;b&gt;한 앱만 해당 스킴을 가질 수 있고&lt;/b&gt;, 충돌 시 나머지는 무시됨.&lt;/li&gt;
&lt;li data-end=&quot;1834&quot; data-start=&quot;1796&quot;&gt;&lt;b&gt;앱 선택 UI가 없음. &amp;rarr; 개발자가 직접 리스트 구성 필요&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1839&quot; data-start=&quot;1836&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1862&quot; data-start=&quot;1841&quot; data-ke-size=&quot;size26&quot;&gt;5. ✅ 앱 미설치 시 처리 방식&lt;/h2&gt;
&lt;h3 data-end=&quot;1886&quot; data-start=&quot;1864&quot; data-ke-size=&quot;size23&quot;&gt;단순 스킴 (myapp://)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1984&quot; data-start=&quot;1887&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1984&quot; data-start=&quot;1887&quot;&gt;앱이 설치되지 않았을 경우:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1984&quot; data-start=&quot;1907&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1950&quot; data-start=&quot;1907&quot;&gt;&lt;b&gt;Android&lt;/b&gt;: 아무 반응이 없거나, Toast로 &amp;ldquo;앱 없음&amp;rdquo; 안내&lt;/li&gt;
&lt;li data-end=&quot;1984&quot; data-start=&quot;1953&quot;&gt;&lt;b&gt;iOS&lt;/b&gt;: 아무 반응 없음 (에러 표시도 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2025&quot; data-start=&quot;1986&quot; data-ke-size=&quot;size23&quot;&gt;해결책: intent:// 스킴 사용 (Android 전용)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752017579736&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;intent://somepath#Intent;scheme=myapp;package=com.example.myapp;end&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2164&quot; data-start=&quot;2107&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2123&quot; data-start=&quot;2107&quot;&gt;앱이 설치되어 있으면 실행&lt;/li&gt;
&lt;li data-end=&quot;2164&quot; data-start=&quot;2124&quot;&gt;설치 안 돼 있으면 &amp;rarr; &lt;b&gt;Google Play Store&lt;/b&gt;로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2169&quot; data-start=&quot;2166&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2192&quot; data-start=&quot;2171&quot; data-ke-size=&quot;size26&quot;&gt;6. ✅ 앱 선택 UI 제공 여부&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2390&quot; data-start=&quot;2194&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2287&quot; data-start=&quot;2242&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2259&quot; data-start=&quot;2242&quot;&gt;여러 앱이 동일 스킴 등록&lt;/td&gt;
&lt;td data-end=&quot;2275&quot; data-start=&quot;2259&quot; data-col-size=&quot;sm&quot;&gt;앱 선택 화면 자동 제공&lt;/td&gt;
&lt;td data-end=&quot;2287&quot; data-start=&quot;2275&quot; data-col-size=&quot;sm&quot;&gt;❌ 자동 미제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2330&quot; data-start=&quot;2288&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2303&quot; data-start=&quot;2288&quot;&gt;앱을 고정 선택 가능?&lt;/td&gt;
&lt;td data-end=&quot;2321&quot; data-start=&quot;2303&quot; data-col-size=&quot;sm&quot;&gt;✅ &amp;ldquo;항상 이 앱으로 열기&amp;rdquo;&lt;/td&gt;
&lt;td data-end=&quot;2330&quot; data-start=&quot;2321&quot; data-col-size=&quot;sm&quot;&gt;❌ 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2390&quot; data-start=&quot;2331&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2349&quot; data-start=&quot;2331&quot;&gt;앱 리스트 수동 구성 가능?&lt;/td&gt;
&lt;td data-end=&quot;2367&quot; data-start=&quot;2349&quot; data-col-size=&quot;sm&quot;&gt;✅ 가능 (UX 제어 목적)&lt;/td&gt;
&lt;td data-end=&quot;2390&quot; data-start=&quot;2367&quot; data-col-size=&quot;sm&quot;&gt;✅ 필수 (UI 직접 구현해야 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;2395&quot; data-start=&quot;2392&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2418&quot; data-start=&quot;2397&quot; data-ke-size=&quot;size26&quot;&gt;7. ✅ 실제 서비스 적용 시 팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2568&quot; data-start=&quot;2420&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2463&quot; data-start=&quot;2420&quot;&gt;Android에서는 intent:// 스킴을 사용하는 것이 가장 안전함&lt;/li&gt;
&lt;li data-end=&quot;2515&quot; data-start=&quot;2464&quot;&gt;iOS에서는 앱 리스트 UI를 직접 구성하고, 각 앱의 고유 스킴을 수동으로 실행해야 함&lt;/li&gt;
&lt;li data-end=&quot;2568&quot; data-start=&quot;2516&quot;&gt;스킴 호출 시 JS fallback 로직 (앱 미설치 시 스토어 이동)을 반드시 구현할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;ts&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;복사&lt;span data-state=&quot;closed&quot;&gt;편집&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;// iOS 앱 스킴 호출 후 fallback&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;window&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;location&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;href&lt;/span&gt;&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&lt;span&gt;&quot;mobileid://requestVP&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;&lt;span&gt;setTimeout&lt;/span&gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;() =&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;&lt;span&gt;window&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;location&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;&lt;span&gt;href&lt;/span&gt;&lt;/span&gt;&lt;span&gt; = &lt;/span&gt;&lt;span&gt;&lt;span&gt;&quot;&lt;a href=&quot;https://apps.apple.com/app/모바일신분증&quot;&gt;https://apps.apple.com/app/모바일신분증&lt;/a&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;; }, &lt;/span&gt;&lt;span&gt;&lt;span&gt;1500&lt;/span&gt;&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;2748&quot; data-start=&quot;2745&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2765&quot; data-start=&quot;2750&quot; data-ke-size=&quot;size26&quot;&gt;8.   마무리 요약&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2988&quot; data-start=&quot;2767&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2842&quot; data-start=&quot;2815&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2826&quot; data-start=&quot;2815&quot;&gt;스킴 중복 허용&lt;/td&gt;
&lt;td data-end=&quot;2833&quot; data-start=&quot;2826&quot; data-col-size=&quot;sm&quot;&gt;✅ 가능&lt;/td&gt;
&lt;td data-end=&quot;2842&quot; data-start=&quot;2833&quot; data-col-size=&quot;sm&quot;&gt;❌ 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2877&quot; data-start=&quot;2843&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2856&quot; data-start=&quot;2843&quot;&gt;앱 선택 화면 제공&lt;/td&gt;
&lt;td data-end=&quot;2863&quot; data-start=&quot;2856&quot; data-col-size=&quot;sm&quot;&gt;✅ 자동&lt;/td&gt;
&lt;td data-end=&quot;2877&quot; data-start=&quot;2863&quot; data-col-size=&quot;sm&quot;&gt;❌ 수동 구성 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2951&quot; data-start=&quot;2878&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2889&quot; data-start=&quot;2878&quot;&gt;앱 미설치 대응&lt;/td&gt;
&lt;td data-end=&quot;2918&quot; data-start=&quot;2889&quot; data-col-size=&quot;sm&quot;&gt;intent:// 또는 JS fallback&lt;/td&gt;
&lt;td data-end=&quot;2951&quot; data-start=&quot;2918&quot; data-col-size=&quot;sm&quot;&gt;Universal Link 또는 JS fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2988&quot; data-start=&quot;2952&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2964&quot; data-start=&quot;2952&quot;&gt;실무 적용 난이도&lt;/td&gt;
&lt;td data-end=&quot;2969&quot; data-start=&quot;2964&quot; data-col-size=&quot;sm&quot;&gt;낮음&lt;/td&gt;
&lt;td data-end=&quot;2988&quot; data-start=&quot;2969&quot; data-col-size=&quot;sm&quot;&gt;상대적으로 높음 (제약 多)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;2993&quot; data-start=&quot;2990&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3008&quot; data-start=&quot;2995&quot; data-ke-size=&quot;size26&quot;&gt;  마무리 코멘트&lt;/h2&gt;
&lt;p data-end=&quot;3129&quot; data-start=&quot;3010&quot; data-ke-size=&quot;size16&quot;&gt;앱 스킴은 &lt;b&gt;웹과 앱, 앱과 앱 간의 연결을 만드는 매우 중요한 기술&lt;/b&gt;입니다.&lt;br /&gt;Android와 iOS는 이를 처리하는 방식이 다르기 때문에,&lt;br /&gt;UX/보안/fallback 처리까지 고려한 구현이 필요합니다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/108</guid>
      <comments>https://ji-frontdev.tistory.com/entry/%EC%95%B1-%EC%8A%A4%ED%82%B4URL-Scheme%E2%80%9D%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-AndroidiOS-%EC%B0%A8%EC%9D%B4-%EC%95%B1-%EC%84%A0%ED%83%9D-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EC%8B%9D#entry108comment</comments>
      <pubDate>Wed, 9 Jul 2025 08:33:47 +0900</pubDate>
    </item>
    <item>
      <title>[Higher-Order Component(HOC)] 등장배경/Custom Hooks 비교 / 예제 코드</title>
      <link>https://ji-frontdev.tistory.com/entry/React%EC%9D%98-%EA%B3%A0%EC%A0%84%EC%A0%81%EC%9D%B8-%EC%9E%AC%EC%82%AC%EC%9A%A9-%ED%8C%A8%ED%84%B4-Higher-Order-ComponentHOC</link>
      <description>&lt;h2 data-end=&quot;259&quot; data-start=&quot;243&quot; data-ke-size=&quot;size26&quot;&gt;1. HOC란 무엇인가?&lt;/h2&gt;
&lt;blockquote data-end=&quot;333&quot; data-start=&quot;296&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;333&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;**Higher-Order Component(HOC)**는 &lt;br /&gt;&amp;ldquo;컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 함수&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;380&quot; data-start=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-end=&quot;380&quot; data-start=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;즉, 하나의 컴포넌트를 감싸 공통된 &lt;b&gt;로직이나 기능을 주입&lt;/b&gt;하는 패턴이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745066067238&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function withFeature&amp;lt;P&amp;gt;(WrappedComponent: React.ComponentType&amp;lt;P&amp;gt;) {
  return function EnhancedComponent(props: P) {
    // 공통 로직
    return &amp;lt;WrappedComponent {...props} /&amp;gt;;
  };
}​&lt;/code&gt;&lt;/pre&gt;
&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;577&quot; data-start=&quot;574&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;602&quot; data-start=&quot;579&quot; data-ke-size=&quot;size26&quot;&gt;2. HOC 패턴의 역사와 등장 배경&lt;/h2&gt;
&lt;h3 data-end=&quot;620&quot; data-start=&quot;604&quot; data-ke-size=&quot;size23&quot;&gt; ️ 언제 등장했나?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;707&quot; data-start=&quot;621&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;681&quot; data-start=&quot;621&quot;&gt;**React Hooks가 등장하기 전 (React 16.8 이전)**에 유일한 로직 재사용 방식이었다.&lt;/li&gt;
&lt;li data-end=&quot;707&quot; data-start=&quot;682&quot;&gt;React 공식 문서에서도 권장되던 방식.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;723&quot; data-start=&quot;709&quot; data-ke-size=&quot;size23&quot;&gt;  왜 등장했나?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;812&quot; data-start=&quot;724&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;776&quot; data-start=&quot;724&quot;&gt;공통 기능 (로딩, 인증, 로깅, 테마 등)을 여러 컴포넌트에서 반복 작성하지 않기 위해.&lt;/li&gt;
&lt;li data-end=&quot;812&quot; data-start=&quot;777&quot;&gt;코드 중복 제거 및 추상화를 통해 유지보수성을 높이기 위해.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;817&quot; data-start=&quot;814&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;838&quot; data-start=&quot;819&quot; data-ke-size=&quot;size26&quot;&gt;3. HOC vs 다른 패턴들&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1317&quot; data-start=&quot;840&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;패턴&lt;/td&gt;
&lt;td&gt;등장 시기&lt;/td&gt;
&lt;td&gt;강점&lt;/td&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1139&quot; data-start=&quot;1046&quot;&gt;
&lt;td data-end=&quot;1063&quot; data-start=&quot;1046&quot;&gt;HOC&lt;/td&gt;
&lt;td data-end=&quot;1082&quot; data-start=&quot;1063&quot;&gt;pre-Hooks&lt;/td&gt;
&lt;td data-end=&quot;1112&quot; data-start=&quot;1082&quot;&gt;UI 래핑, 공통 로직 삽입&lt;/td&gt;
&lt;td data-end=&quot;1139&quot; data-start=&quot;1112&quot;&gt;Wrapper Hell, 타입 추론 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1235&quot; data-start=&quot;1140&quot;&gt;
&lt;td data-end=&quot;1157&quot; data-start=&quot;1140&quot;&gt;Render Props&lt;/td&gt;
&lt;td data-end=&quot;1176&quot; data-start=&quot;1157&quot;&gt;pre-Hooks&lt;/td&gt;
&lt;td data-end=&quot;1209&quot; data-start=&quot;1176&quot;&gt;동적인 props 전달&lt;/td&gt;
&lt;td data-end=&quot;1235&quot; data-start=&quot;1209&quot;&gt;가독성 떨어짐 (중첩 JSX)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1317&quot; data-start=&quot;1236&quot;&gt;
&lt;td data-end=&quot;1253&quot; data-start=&quot;1236&quot;&gt;Custom Hooks&lt;/td&gt;
&lt;td data-end=&quot;1269&quot; data-start=&quot;1253&quot;&gt;React 16.8 이후&lt;/td&gt;
&lt;td data-end=&quot;1292&quot; data-start=&quot;1269&quot;&gt;로직 분리 및 재사용, 명확한 추상화&lt;/td&gt;
&lt;td data-end=&quot;1317&quot; data-start=&quot;1292&quot;&gt;렌더링 제어는 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1322&quot; data-start=&quot;1319&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1350&quot; data-start=&quot;1324&quot; data-ke-size=&quot;size26&quot;&gt;4. TypeScript 기반 HOC 예제&lt;/h2&gt;
&lt;h3 data-end=&quot;1365&quot; data-start=&quot;1352&quot; data-ke-size=&quot;size23&quot;&gt;  로깅 HOC&lt;/h3&gt;
&lt;pre id=&quot;code_1745066104778&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react';

function withLogger&amp;lt;P&amp;gt;(WrappedComponent: React.ComponentType&amp;lt;P&amp;gt;) {
  const ComponentWithLogger = (props: P) =&amp;gt; {
    console.log(`[Logger] ${WrappedComponent.name} rendered`);
    return &amp;lt;WrappedComponent {...props} /&amp;gt;;
  };

  ComponentWithLogger.displayName = `WithLogger(${WrappedComponent.displayName || WrappedComponent.name})`;
  return ComponentWithLogger;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;1779&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size23&quot;&gt;사용&lt;/h3&gt;
&lt;pre id=&quot;code_1745066116354&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type MyComponentProps = { message: string };

const MyComponent = ({ message }: MyComponentProps) =&amp;gt; &amp;lt;div&amp;gt;{message}&amp;lt;/div&amp;gt;;

export default withLogger(MyComponent);&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr data-end=&quot;1960&quot; data-start=&quot;1957&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1993&quot; data-start=&quot;1962&quot; data-ke-size=&quot;size26&quot;&gt;5. 같은 기능, Custom Hook으로 바꾸면?&lt;/h2&gt;
&lt;h3 data-end=&quot;2016&quot; data-start=&quot;1995&quot; data-ke-size=&quot;size23&quot;&gt;  Custom Hook 버전&lt;/h3&gt;
&lt;pre id=&quot;code_1745066126900&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function useLogger(componentName: string) {
  useEffect(() =&amp;gt; {
    console.log(`[Logger] ${componentName} rendered`);
  }, []);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;2169&quot; data-start=&quot;2161&quot; data-ke-size=&quot;size23&quot;&gt;사용 예&lt;/h3&gt;
&lt;pre id=&quot;code_1745066138078&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const MyComponent = () =&amp;gt; {
  useLogger('MyComponent');
  return &amp;lt;div&amp;gt;Hello&amp;lt;/div&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; font-size: 1.44em; letter-spacing: -1px;&quot;&gt;✅ 비교 요약&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;
&lt;div&gt;&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2864&quot; data-start=&quot;2282&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;HOC&lt;/td&gt;
&lt;td&gt;Custom Hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2544&quot; data-start=&quot;2468&quot;&gt;
&lt;td data-end=&quot;2483&quot; data-start=&quot;2468&quot;&gt;사용 방식&lt;/td&gt;
&lt;td data-end=&quot;2512&quot; data-start=&quot;2483&quot;&gt;컴포넌트 감싸기&lt;/td&gt;
&lt;td data-end=&quot;2544&quot; data-start=&quot;2512&quot;&gt;내부에서 훅 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2632&quot; data-start=&quot;2545&quot;&gt;
&lt;td data-end=&quot;2559&quot; data-start=&quot;2545&quot;&gt;로직 재사용&lt;/td&gt;
&lt;td data-end=&quot;2594&quot; data-start=&quot;2559&quot;&gt;✅&lt;/td&gt;
&lt;td data-end=&quot;2632&quot; data-start=&quot;2594&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2706&quot; data-start=&quot;2633&quot;&gt;
&lt;td data-end=&quot;2647&quot; data-start=&quot;2633&quot;&gt;렌더링 제어&lt;/td&gt;
&lt;td data-end=&quot;2676&quot; data-start=&quot;2647&quot;&gt;✅ (조건부 렌더링 가능)&lt;/td&gt;
&lt;td data-end=&quot;2706&quot; data-start=&quot;2676&quot;&gt;❌ (렌더링 자체는 못 막음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2784&quot; data-start=&quot;2707&quot;&gt;
&lt;td data-end=&quot;2722&quot; data-start=&quot;2707&quot;&gt;UI 구조 영향&lt;/td&gt;
&lt;td data-end=&quot;2756&quot; data-start=&quot;2722&quot;&gt;❌ Wrapper 증가&lt;/td&gt;
&lt;td data-end=&quot;2784&quot; data-start=&quot;2756&quot;&gt;✅ 컴포넌트 구조 그대로 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2864&quot; data-start=&quot;2785&quot;&gt;
&lt;td data-end=&quot;2802&quot; data-start=&quot;2785&quot;&gt;TypeScript 추론&lt;/td&gt;
&lt;td data-end=&quot;2832&quot; data-start=&quot;2802&quot;&gt;❌ 복잡할 수 있음&lt;/td&gt;
&lt;td data-end=&quot;2864&quot; data-start=&quot;2832&quot;&gt;✅ 타입 추론 명확&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;2869&quot; data-start=&quot;2866&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2921&quot; data-start=&quot;2871&quot; data-ke-size=&quot;size26&quot;&gt;6. Next.js + React Query + Zustand 환경에서의 HOC 예시&lt;/h2&gt;
&lt;h3 data-end=&quot;2946&quot; data-start=&quot;2923&quot; data-ke-size=&quot;size23&quot;&gt;예: 인증 여부를 검사하고 리디렉션&lt;/h3&gt;
&lt;pre id=&quot;code_1745066161968&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// withAuth.tsx
import { useRouter } from 'next/router';
import { useUserStore } from '@/stores/userStore';

export function withAuth&amp;lt;P&amp;gt;(Component: React.ComponentType&amp;lt;P&amp;gt;) {
  const Wrapper = (props: P) =&amp;gt; {
    const router = useRouter();
    const isAuthenticated = useUserStore((state) =&amp;gt; state.isLoggedIn);

    useEffect(() =&amp;gt; {
      if (!isAuthenticated) router.push('/login');
    }, [isAuthenticated]);

    if (!isAuthenticated) return null;
    return &amp;lt;Component {...props} /&amp;gt;;
  };

  return Wrapper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745066172385&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// pages/profile.tsx
const ProfilePage = () =&amp;gt; &amp;lt;div&amp;gt;프로필입니다&amp;lt;/div&amp;gt;;
export default withAuth(ProfilePage);​&lt;/code&gt;&lt;/pre&gt;
&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;3594&quot; data-start=&quot;3591&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3619&quot; data-start=&quot;3596&quot; data-ke-size=&quot;size26&quot;&gt;7. 실무에서 HOC를 선택하는 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3753&quot; data-start=&quot;3621&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3663&quot; data-start=&quot;3621&quot;&gt;여러 페이지에 동일한 &lt;b&gt;UI 제어 또는 조건 분기 로직이 필요할 때&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;3722&quot; data-start=&quot;3664&quot;&gt;&lt;b&gt;SSR&lt;/b&gt;, &lt;b&gt;라우팅 전 체크&lt;/b&gt;, &lt;b&gt;로딩/에러 핸들링&lt;/b&gt; 등 &lt;b&gt;렌더링 제어&lt;/b&gt;가 필요할 때&lt;/li&gt;
&lt;li data-end=&quot;3753&quot; data-start=&quot;3723&quot;&gt;특정 &lt;b&gt;스타일/레이아웃/테마 래핑&lt;/b&gt;이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;3758&quot; data-start=&quot;3755&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3789&quot; data-start=&quot;3760&quot; data-ke-size=&quot;size26&quot;&gt;8. 정리: 지금 시점에서 HOC를 쓴다는 건?&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3985&quot; data-start=&quot;3791&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅ 사용할 때&lt;/td&gt;
&lt;td&gt;❌ 피해야 할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3901&quot; data-start=&quot;3848&quot;&gt;
&lt;td data-end=&quot;3867&quot; data-start=&quot;3848&quot;&gt;공통 렌더링 제어가 필요할 때&lt;/td&gt;
&lt;td data-end=&quot;3901&quot; data-start=&quot;3867&quot;&gt;단순 로직 재사용만 필요한 경우 (useXXX로 충분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3945&quot; data-start=&quot;3902&quot;&gt;
&lt;td data-end=&quot;3921&quot; data-start=&quot;3902&quot;&gt;UI 컨텍스트나 테마 적용 시&lt;/td&gt;
&lt;td data-end=&quot;3945&quot; data-start=&quot;3921&quot;&gt;너무 많은 HOC 중첩이 발생할 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3985&quot; data-start=&quot;3946&quot;&gt;
&lt;td data-end=&quot;3961&quot; data-start=&quot;3946&quot;&gt;페이지 단위 조건 분기&lt;/td&gt;
&lt;td data-end=&quot;3985&quot; data-start=&quot;3961&quot;&gt;로직 복잡도 높은 컴포넌트에 적용 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;3990&quot; data-start=&quot;3987&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;4003&quot; data-start=&quot;3992&quot; data-ke-size=&quot;size26&quot;&gt;✍️ 마무리하며&lt;/h2&gt;
&lt;p data-end=&quot;4095&quot; data-start=&quot;4005&quot; data-ke-size=&quot;size16&quot;&gt;HOC는 &lt;b&gt;오래된 개념이지만 여전히 유효한 재사용 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;4095&quot; data-start=&quot;4005&quot; data-ke-size=&quot;size16&quot;&gt;다만, React의 변화와 함께 &lt;b&gt;사용 목적이 명확한 상황&lt;/b&gt;에서만 선택적으로 활용해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;4194&quot; data-start=&quot;4097&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;다음 글에서는 Render Props 또는 Compound Components 패턴&lt;/b&gt;도 함께 정리해볼 예정&lt;/p&gt;</description>
      <category>Frontend/React</category>
      <category>frontendengineering</category>
      <category>higherordercomponent</category>
      <category>hoc</category>
      <category>React</category>
      <category>reactpatterns</category>
      <category>TypeScript</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/107</guid>
      <comments>https://ji-frontdev.tistory.com/entry/React%EC%9D%98-%EA%B3%A0%EC%A0%84%EC%A0%81%EC%9D%B8-%EC%9E%AC%EC%82%AC%EC%9A%A9-%ED%8C%A8%ED%84%B4-Higher-Order-ComponentHOC#entry107comment</comments>
      <pubDate>Sun, 20 Apr 2025 10:00:48 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 개발자가 구독하거나 즐겨찾기 해두면 좋은 뉴스레터, 사이트</title>
      <link>https://ji-frontdev.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EA%B5%AC%EB%8F%85%ED%95%98%EA%B1%B0%EB%82%98-%EC%A6%90%EA%B2%A8%EC%B0%BE%EA%B8%B0-%ED%95%B4%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%80-%EB%89%B4%EC%8A%A4%EB%A0%88%ED%84%B0-%EC%82%AC%EC%9D%B4%ED%8A%B8</link>
      <description>&lt;blockquote data-end=&quot;157&quot; data-start=&quot;128&quot; data-ke-style=&quot;style3&quot;&gt;프론트엔드 개발자로서 트렌드, 기술, 도구 변화가 빠르기 때문에 꾸준히 정보 얻는 게 중요해요. &lt;br /&gt;아래는 프론트엔드 개발자가 구독하거나 즐겨찾기 해두면 좋은 뉴스레터, 사이트 등을 정리해봤어요.&lt;/blockquote&gt;
&lt;h3 data-end=&quot;157&quot; data-start=&quot;128&quot; data-ke-size=&quot;size23&quot;&gt;  뉴스레터 (이메일로 받아보기 좋은 소스)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;772&quot; data-start=&quot;158&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;281&quot; data-start=&quot;158&quot;&gt;&lt;b&gt;Frontend Focus&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;281&quot; data-start=&quot;185&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;237&quot; data-start=&quot;185&quot;&gt;&lt;a href=&quot;https://frontendfoc.us&quot;&gt;https://frontendfoc.us&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;281&quot; data-start=&quot;241&quot;&gt;매주 발행되는 프론트엔드 뉴스 요약 (HTML, CSS, JS 중심)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;418&quot; data-start=&quot;283&quot;&gt;&lt;b&gt;JavaScript Weekly&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;418&quot; data-start=&quot;313&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;313&quot;&gt;&lt;a href=&quot;https://javascriptweekly.com&quot;&gt;https://javascriptweekly.com&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;418&quot; data-start=&quot;381&quot;&gt;JS 중심으로 유용한 라이브러리, 아티클, 도구 등을 매주 소개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;553&quot; data-start=&quot;420&quot;&gt;&lt;b&gt;React Status&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;553&quot; data-start=&quot;445&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;445&quot;&gt;&lt;a href=&quot;https://react.statuscode.com&quot;&gt;https://react.statuscode.com&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;553&quot; data-start=&quot;513&quot;&gt;React 생태계 관련 소식 요약 (라이브러리, 아티클, 릴리즈 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;667&quot; data-start=&quot;555&quot;&gt;&lt;b&gt;CSS Weekly&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;667&quot; data-start=&quot;578&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;630&quot; data-start=&quot;578&quot;&gt;&lt;a href=&quot;https://css-weekly.com&quot;&gt;https://css-weekly.com&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;667&quot; data-start=&quot;634&quot;&gt;최신 CSS 기법과 인사이트를 간단히 받아볼 수 있어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;669&quot;&gt;&lt;b&gt;Bytes.dev&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;772&quot; data-start=&quot;691&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;733&quot; data-start=&quot;691&quot;&gt;&lt;a href=&quot;https://bytes.dev&quot;&gt;https://bytes.dev&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;737&quot;&gt;유머 섞인 톤으로 JS/웹 개발 뉴스 전해주는 인기 뉴스레터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;777&quot; data-start=&quot;774&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;795&quot; data-start=&quot;779&quot; data-ke-size=&quot;size23&quot;&gt;  사이트 &amp;amp; 블로그&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1527&quot; data-start=&quot;796&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;943&quot; data-start=&quot;796&quot;&gt;&lt;b&gt;Smashing Magazine&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;943&quot; data-start=&quot;826&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;898&quot; data-start=&quot;826&quot;&gt;&lt;a href=&quot;https://www.smashingmagazine.com&quot;&gt;https://www.smashingmagazine.com&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;943&quot; data-start=&quot;902&quot;&gt;디자인, UX, HTML/CSS, JS 등 다양한 프론트엔드 주제 다룸&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1106&quot; data-start=&quot;945&quot;&gt;&lt;b&gt;CSS-Tricks (지금은 DigitalOcean 블로그로 통합)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1106&quot; data-start=&quot;995&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1069&quot; data-start=&quot;995&quot;&gt;&lt;a href=&quot;https://www.digitalocean.com/blog&quot;&gt;https://www.digitalocean.com/blog&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1106&quot; data-start=&quot;1073&quot;&gt;CSS, JS 실용적인 팁, 문제 해결 중심 아티클 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1198&quot; data-start=&quot;1108&quot;&gt;&lt;b&gt;Dev.to&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1198&quot; data-start=&quot;1127&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1163&quot; data-start=&quot;1127&quot;&gt;&lt;a href=&quot;https://dev.to&quot;&gt;https://dev.to&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1198&quot; data-start=&quot;1167&quot;&gt;개발자 커뮤니티 기반, 다양한 주제로 아티클이 올라옴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1405&quot; data-start=&quot;1200&quot;&gt;&lt;b&gt;Medium &amp;ndash; Better Programming / JavaScript in Plain English&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1405&quot; data-start=&quot;1270&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1352&quot; data-start=&quot;1270&quot;&gt;&lt;a href=&quot;https://medium.com/better-programming&quot;&gt;https://medium.com/better-programming&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1405&quot; data-start=&quot;1356&quot;&gt;초중급 개발자에 좋은 튜토리얼과 모던 JS/React/Next.js 등 실습 글 다수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1527&quot; data-start=&quot;1407&quot;&gt;&lt;b&gt;MDN Web Docs&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1527&quot; data-start=&quot;1432&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1498&quot; data-start=&quot;1432&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org&quot;&gt;https://developer.mozilla.org&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1527&quot; data-start=&quot;1502&quot;&gt;기본기와 참조용으로는 최고의 웹 표준 문서&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1532&quot; data-start=&quot;1529&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1793&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size23&quot;&gt;  추가 추천&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2081&quot; data-start=&quot;1794&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1794&quot;&gt;&lt;b&gt;GitHub Trending (JavaScript / TypeScript)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1954&quot; data-start=&quot;1846&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1930&quot; data-start=&quot;1846&quot;&gt;&lt;a href=&quot;https://github.com/trending/javascript&quot;&gt;https://github.com/trending/javascript&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1954&quot; data-start=&quot;1933&quot;&gt;요즘 뜨는 오픈소스 트렌드 살펴보기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2081&quot; data-start=&quot;1956&quot;&gt;&lt;b&gt;HN (Hacker News)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2081&quot; data-start=&quot;1983&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2047&quot; data-start=&quot;1983&quot;&gt;&lt;a href=&quot;https://news.ycombinator.com&quot;&gt;https://news.ycombinator.com&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;2081&quot; data-start=&quot;2050&quot;&gt;실리콘밸리 개발자들이 보는 뉴스, 흥미로운 토론 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend</category>
      <category>bytes.dev</category>
      <category>cssweekly</category>
      <category>frontendfocus</category>
      <category>javascriptweekly</category>
      <category>reactstatus</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/106</guid>
      <comments>https://ji-frontdev.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EA%B5%AC%EB%8F%85%ED%95%98%EA%B1%B0%EB%82%98-%EC%A6%90%EA%B2%A8%EC%B0%BE%EA%B8%B0-%ED%95%B4%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%80-%EB%89%B4%EC%8A%A4%EB%A0%88%ED%84%B0-%EC%82%AC%EC%9D%B4%ED%8A%B8#entry106comment</comments>
      <pubDate>Sat, 19 Apr 2025 10:00:04 +0900</pubDate>
    </item>
    <item>
      <title>Next.js App Router에서 CSR 탭에서 SSR 페이지 전환으로 전환하기까지</title>
      <link>https://ji-frontdev.tistory.com/entry/Nextjs-App-Router%EC%97%90%EC%84%9C-CSR-%ED%83%AD%EC%97%90%EC%84%9C-SSR-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%84%ED%99%98%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80</link>
      <description>&lt;p data-end=&quot;394&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;Next.js의 App Router 기반 프로젝트에서 탭 UI를 구성하고, 각 탭에 컴포넌트를 연결하는 작업까지는 무리 없이 진행되었다.&lt;br /&gt;버튼을 클릭하면 탭이 바뀌고, 화면도 전환되는 &lt;b&gt;인터랙션은 제대로 작동&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-end=&quot;417&quot; data-start=&quot;396&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 치명적인 문제가 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-end=&quot;453&quot; data-start=&quot;419&quot; data-ke-size=&quot;size23&quot;&gt;  화면은 떴지만, 정작 중요한 데이터는 비어 있었다&lt;/h3&gt;
&lt;p data-end=&quot;546&quot; data-start=&quot;455&quot; data-ke-size=&quot;size16&quot;&gt;탭을 눌러도 아무런 정보가 표시되지 않았다.&lt;br /&gt;버튼은 잘 보였고, 레이아웃도 정상이라 &amp;ldquo;정상 동작&amp;rdquo;처럼 보였지만, &lt;b&gt;실제로는 데이터가 끝내 표시되지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;706&quot; data-start=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;원인은 클라이언트 사이드에서 데이터를 요청하는 구조 때문이었다.&lt;br /&gt;useQuery를 사용해 데이터를 가져오도록 했지만, 이 방식은 브라우저가 로드된 뒤 실행된다.&lt;br /&gt;그래서 브라우저에서 API 요청이 이루어지고, 거기서 데이터를 받아야만 실제 콘텐츠가 보여지는 구조였던 것이다.&lt;/p&gt;
&lt;p data-end=&quot;864&quot; data-start=&quot;708&quot; data-ke-size=&quot;size16&quot;&gt;그런데 탭이 &lt;b&gt;기본적으로 서버 사이드에서 렌더링되는 구조&lt;/b&gt;가 아니고, use client로 클라이언트 컴포넌트로만 구성되어 있다 보니, 데이터는 서버에서 전혀 전달되지 않았고, 브라우저에서도 &lt;b&gt;API 요청조차 보내지 못한 상태&lt;/b&gt;였다.&lt;br /&gt;(게다가 에러도 안 났다...)&lt;/p&gt;
&lt;hr data-end=&quot;869&quot; data-start=&quot;866&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;892&quot; data-start=&quot;871&quot; data-ke-size=&quot;size23&quot;&gt;  SEO도 완전히 무의미했다&lt;/h3&gt;
&lt;p data-end=&quot;1027&quot; data-start=&quot;894&quot; data-ke-size=&quot;size16&quot;&gt;이 구조에서는 HTML의 초기 렌더링 결과에 &lt;b&gt;실제 콘텐츠 데이터가 아무것도 포함되어 있지 않았다.&lt;/b&gt;&lt;br /&gt;검색 엔진, OG 태그, SNS 미리보기 등에서도 &lt;b&gt;빈 껍데기만&lt;/b&gt; 확인할 수 있었고, 이는 SEO 측면에서 심각한 결함이었다.&lt;/p&gt;
&lt;hr data-end=&quot;1032&quot; data-start=&quot;1029&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1067&quot; data-start=&quot;1034&quot; data-ke-size=&quot;size23&quot;&gt;✅ 구조 개선: SSR 페이지 전환 방식으로 리팩토링&lt;/h3&gt;
&lt;p data-end=&quot;1180&quot; data-start=&quot;1069&quot; data-ke-size=&quot;size16&quot;&gt;결국 탭 UI를 &lt;b&gt;단순한 라우팅 전환 트리거로 변경&lt;/b&gt;하고,&lt;br /&gt;각 탭은 /guide/a, /guide/b처럼 &lt;b&gt;SSR 페이지로 구성&lt;/b&gt;해 데이터를 미리 불러와 렌더링하도록 구조를 바꿨다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1315&quot; data-start=&quot;1182&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1213&quot; data-start=&quot;1182&quot;&gt;탭 클릭 &amp;rarr; 페이지 이동 (router.push)&lt;/li&gt;
&lt;li data-end=&quot;1269&quot; data-start=&quot;1214&quot;&gt;각 페이지에서 서버에서 데이터를 미리 불러오기 (async 서버 컴포넌트 + fetch)&lt;/li&gt;
&lt;li data-end=&quot;1315&quot; data-start=&quot;1270&quot;&gt;초기 HTML에 데이터 포함 &amp;rarr; &lt;b&gt;SEO 반영 가능 + 즉시 콘텐츠 표시&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1320&quot; data-start=&quot;1317&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1356&quot; data-start=&quot;1322&quot; data-ke-size=&quot;size23&quot;&gt;  정리: 인터랙션이 된다고 &amp;ldquo;잘 된 UI&amp;rdquo;는 아니다&lt;/h3&gt;
&lt;p data-end=&quot;1385&quot; data-start=&quot;1358&quot; data-ke-size=&quot;size16&quot;&gt;이번 시행착오를 통해 다음과 같은 교훈을 얻었다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1592&quot; data-start=&quot;1387&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1420&quot; data-start=&quot;1387&quot;&gt;&lt;b&gt;겉으로 보이는 UI와 실제 데이터 상태는 다르다.&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1460&quot; data-start=&quot;1421&quot;&gt;클라이언트 컴포넌트만 사용하면, 서버는 아무것도 렌더링하지 않는다.&lt;/li&gt;
&lt;li data-end=&quot;1516&quot; data-start=&quot;1461&quot;&gt;Next.js App Router에서는 getServerSideProps를 사용할 수 없다.&lt;/li&gt;
&lt;li data-end=&quot;1563&quot; data-start=&quot;1517&quot;&gt;서버에서 데이터를 가져오는 구조로 렌더링하려면 SSR 컴포넌트를 사용해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;1592&quot; data-start=&quot;1564&quot;&gt;SEO 성능 확보는 구조적인 설계에서 시작된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/NextJs</category>
      <category>approuter</category>
      <category>nextjs</category>
      <category>reactquery</category>
      <category>SSR</category>
      <category>개발공부기록</category>
      <category>서버사이드렌더링</category>
      <category>클라이언트렌더링</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/105</guid>
      <comments>https://ji-frontdev.tistory.com/entry/Nextjs-App-Router%EC%97%90%EC%84%9C-CSR-%ED%83%AD%EC%97%90%EC%84%9C-SSR-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%84%ED%99%98%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80#entry105comment</comments>
      <pubDate>Fri, 18 Apr 2025 10:00:32 +0900</pubDate>
    </item>
    <item>
      <title>[Vite defineConfig 설정 기본편] root, base, plugins, resolve, build, 상황별 예시</title>
      <link>https://ji-frontdev.tistory.com/entry/Vite-defineConfig-%EC%84%A4%EC%A0%95-%EA%B8%B0%EB%B3%B8%ED%8E%B8-root-base-plugins-resolve-build-%EC%83%81%ED%99%A9%EB%B3%84-%EC%98%88%EC%8B%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;364&quot; data-start=&quot;329&quot; data-ke-size=&quot;size26&quot;&gt;1.   defineConfig()와 설정 구조 개요&lt;/h2&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;Vite는 ESM 기반의 번들러로, vite.config.ts 또는 .js 파일을 통해 설정을 제공합니다.&lt;/p&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;이 설정 파일에서 defineConfig()는 타입 지원과 자동 완성을 위한 래퍼입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744767618846&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본 구조
import { defineConfig } from 'vite';

export default defineConfig({
  root: '.',           // 프로젝트 루트 (index.html 기준)
  plugins: [],         // 플러그인 배열
  resolve: {},         // 경로 해석 관련 설정
  build: {},           // 빌드 설정 (rollup 옵션 포함)
});&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr data-end=&quot;747&quot; data-start=&quot;744&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;774&quot; data-start=&quot;749&quot; data-ke-size=&quot;size26&quot;&gt;2.   주요 설정 속성 복기 및 해설&lt;/h2&gt;
&lt;h3 data-end=&quot;788&quot; data-start=&quot;776&quot; data-ke-size=&quot;size23&quot;&gt;✅ root&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;944&quot; data-start=&quot;789&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;811&quot; data-start=&quot;789&quot;&gt;기본값: process.cwd()&lt;/li&gt;
&lt;li data-end=&quot;875&quot; data-start=&quot;812&quot;&gt;설명: 프로젝트의 루트 디렉토리 설정. 이 디렉토리 기준으로 index.html, src 등이 해석됨.&lt;/li&gt;
&lt;li data-end=&quot;944&quot; data-start=&quot;876&quot;&gt;예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744767651903&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root: './demo', // ./demo/index.html이 엔트리로 사용됨&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;949&quot; data-start=&quot;946&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;963&quot; data-start=&quot;951&quot; data-ke-size=&quot;size23&quot;&gt;✅ base&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1139&quot; data-start=&quot;964&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;976&quot; data-start=&quot;964&quot;&gt;기본값: '/'&lt;/li&gt;
&lt;li data-end=&quot;1033&quot; data-start=&quot;977&quot;&gt;설명: 빌드시 생성되는 자산의 URL prefix. CDN 경로 지정 등 외부 환경 대응에 사용.&lt;/li&gt;
&lt;li data-end=&quot;1139&quot; data-start=&quot;1034&quot;&gt;예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744767664416&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;base: './', // 상대 경로로 자산 import (file:// 테스트용)
base: '/static/', // Nginx 서브경로 대응&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1144&quot; data-start=&quot;1141&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1161&quot; data-start=&quot;1146&quot; data-ke-size=&quot;size23&quot;&gt;✅ plugins&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1413&quot; data-start=&quot;1162&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1203&quot; data-start=&quot;1162&quot;&gt;설명: Vite의 플러그인 생태계 또는 Rollup 플러그인 포함 가능&lt;/li&gt;
&lt;li data-end=&quot;1413&quot; data-start=&quot;1204&quot;&gt;주요 예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744767678545&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins: [
  react(),                   // @vitejs/plugin-react-swc
  tsconfigPaths(),           // tsconfig.json 기반 경로 alias
  removeCrossoriginPlugin()  // 커스텀 HTML 수정 플러그인
]&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1418&quot; data-start=&quot;1415&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1435&quot; data-start=&quot;1420&quot; data-ke-size=&quot;size23&quot;&gt;✅ resolve&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1588&quot; data-start=&quot;1436&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1476&quot; data-start=&quot;1436&quot;&gt;설명: 모듈 import 경로를 해석하는 방식 지정 (alias 등)&lt;/li&gt;
&lt;li data-end=&quot;1588&quot; data-start=&quot;1477&quot;&gt;예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744767688650&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resolve: {
  alias: {
    '@editor': path.resolve(__dirname, 'src/editor'),
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1593&quot; data-start=&quot;1590&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1608&quot; data-start=&quot;1595&quot; data-ke-size=&quot;size23&quot;&gt;✅ build&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2257&quot; data-start=&quot;1609&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1637&quot; data-start=&quot;1609&quot;&gt;설명: Vite &amp;rarr; Rollup 기반 빌드 설정&lt;/li&gt;
&lt;li data-end=&quot;2257&quot; data-start=&quot;1638&quot;&gt;주요 속성:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1744767705180&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;build: {
  outDir: 'dist',                 // 출력 디렉토리
  assetsDir: 'assets',           // 정적 자산 폴더
  sourcemap: true,               // sourcemap 생성 여부
  target: 'es2020',              // 트랜스파일 타겟
  minify: 'esbuild',             // 'terser' or 'esbuild'
  rollupOptions: {
    input: './index.html',       // HTML 엔트리 명시 (멀티페이지 대응 시 유용)
    output: {
      entryFileNames: `assets/[name]-[hash].js`,
      chunkFileNames: `assets/[name]-[hash].js`,
      assetFileNames: `assets/[name]-[hash].[ext]`,
    },
  },
  emptyOutDir: true,             // 기존 dist 제거 여부
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;2262&quot; data-start=&quot;2259&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2284&quot; data-start=&quot;2264&quot; data-ke-size=&quot;size26&quot;&gt;3.   설정 상황 예시&lt;/h2&gt;
&lt;h3 data-end=&quot;2328&quot; data-start=&quot;2286&quot; data-ke-size=&quot;size23&quot;&gt;  A. 파일 시스템 기반 로딩 테스트 (file:// 환경 대응)&lt;/h3&gt;
&lt;pre id=&quot;code_1744767724422&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default defineConfig({
  base: './', // 상대 경로로 경로 지정
  plugins: [
    {
      name: 'remove-crossorigin',
      transformIndexHtml(html) {
        return html.replace(/&amp;lt;script type=&quot;module&quot; crossorigin/g, '&amp;lt;script type=&quot;module&quot;');
      }
    }
  ]
});&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2622&quot; data-start=&quot;2600&quot; data-ke-size=&quot;size23&quot;&gt;  B. 외부 CDN 자산 사용&lt;/h3&gt;
&lt;pre id=&quot;code_1744767734703&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default defineConfig({
  base: 'https://cdn.mycdn.com/myapp/',
});&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-end=&quot;2742&quot; data-start=&quot;2708&quot; data-ke-size=&quot;size23&quot;&gt;  C. 라이브러리 번들링 용도 (build.lib)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1744767747168&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      fileName: format =&amp;gt; `my-lib.${format}.js`
    },
    rollupOptions: {
      external: ['react'],
      output: {
        globals: {
          react: 'React'
        }
      }
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;3079&quot; data-start=&quot;3076&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3092&quot; data-start=&quot;3081&quot; data-ke-size=&quot;size26&quot;&gt;  마무리 팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3331&quot; data-start=&quot;3094&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3135&quot; data-start=&quot;3094&quot;&gt;&lt;b&gt;vite build는 Rollup을 래핑한 고수준 추상화&lt;/b&gt;임.&lt;/li&gt;
&lt;li data-end=&quot;3210&quot; data-start=&quot;3136&quot;&gt;&lt;b&gt;파일 경로, publicPath(base), asset 위치 정책이 명확하지 않으면&lt;/b&gt; 정적 배포에서 이슈가 빈번하게 발생함.&lt;/li&gt;
&lt;li data-end=&quot;3276&quot; data-start=&quot;3211&quot;&gt;로컬에서 file://로 열 경우엔 crossorigin, base, 상대경로의 정합성이 매우 중요함.&lt;/li&gt;
&lt;li data-end=&quot;3331&quot; data-start=&quot;3277&quot;&gt;다중 HTML 엔트리를 사용하는 경우 rollupOptions.input을 꼭 명시할 것.&lt;/li&gt;
&lt;/ul&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;568&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75dzW/btsNm2PV9iU/EFtfaVKGFLyli4yppNw0h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75dzW/btsNm2PV9iU/EFtfaVKGFLyli4yppNw0h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75dzW/btsNm2PV9iU/EFtfaVKGFLyli4yppNw0h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75dzW%2FbtsNm2PV9iU%2FEFtfaVKGFLyli4yppNw0h0%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;568&quot; height=&quot;424&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>frontendbuild</category>
      <category>rolluptips</category>
      <category>typescriptdev</category>
      <category>viteconfig</category>
      <category>webperformance</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/104</guid>
      <comments>https://ji-frontdev.tistory.com/entry/Vite-defineConfig-%EC%84%A4%EC%A0%95-%EA%B8%B0%EB%B3%B8%ED%8E%B8-root-base-plugins-resolve-build-%EC%83%81%ED%99%A9%EB%B3%84-%EC%98%88%EC%8B%9C#entry104comment</comments>
      <pubDate>Wed, 16 Apr 2025 10:46:16 +0900</pubDate>
    </item>
    <item>
      <title>tsconfig.json 설정 오류 해결: allowImportingTsExtensions는 왜 에러가 날까?</title>
      <link>https://ji-frontdev.tistory.com/entry/tsconfigjson-%EC%84%A4%EC%A0%95-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-allowImportingTsExtensions%EB%8A%94-%EC%99%9C-%EC%97%90%EB%9F%AC%EA%B0%80-%EB%82%A0%EA%B9%8C</link>
      <description>&lt;h3 data-end=&quot;275&quot; data-start=&quot;262&quot; data-ke-size=&quot;size23&quot;&gt;  에러 원인:&lt;/h3&gt;
&lt;p data-end=&quot;371&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;allowImportingTsExtensions 옵션은 TypeScript가 .ts 또는 .tsx 확장자를 명시적으로 import하는 것을 허용하는 기능입니다.&lt;/p&gt;
&lt;p data-end=&quot;371&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744719397982&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { something } from './utils.ts';&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;531&quot; data-start=&quot;428&quot; data-ke-size=&quot;size16&quot;&gt;이런 식의 import를 허용하려면 TypeScript가 &lt;b&gt;실제로 JavaScript 파일을 emit하지 않는&lt;/b&gt; 설정이어야 합니다. 그렇지 않으면 문제가 발생할 수 있기 때문이에요.&lt;/p&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 옵션을 사용하려면 아래 중 하나를 설정해야 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;683&quot; data-start=&quot;569&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;623&quot; data-start=&quot;569&quot;&gt;&lt;b&gt;&quot;noEmit&quot;: true&lt;/b&gt;&lt;br /&gt;  TypeScript가 아무 파일도 출력하지 않음&lt;/li&gt;
&lt;li data-end=&quot;683&quot; data-start=&quot;624&quot;&gt;&lt;b&gt;&quot;emitDeclarationOnly&quot;: true&lt;/b&gt;&lt;br /&gt;  타입 정의 파일(.d.ts)만 출력함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;688&quot; data-start=&quot;685&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;702&quot; data-start=&quot;690&quot; data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법:&lt;/h3&gt;
&lt;h4 data-end=&quot;740&quot; data-start=&quot;704&quot; data-ke-size=&quot;size20&quot;&gt;방법 1. noEmit 설정 추가하기 (가장 일반적)&lt;/h4&gt;
&lt;pre id=&quot;code_1744719426488&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;allowImportingTsExtensions&quot;: true,
    &quot;noEmit&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h4 data-end=&quot;882&quot; data-start=&quot;844&quot; data-ke-size=&quot;size20&quot;&gt;방법 2. emitDeclarationOnly 설정 추가&lt;/h4&gt;
&lt;pre id=&quot;code_1744719437097&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;allowImportingTsExtensions&quot;: true,
    &quot;emitDeclarationOnly&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1002&quot; data-start=&quot;999&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1021&quot; data-start=&quot;1004&quot; data-ke-size=&quot;size23&quot;&gt;❓ 왜 이렇게 제한할까?&lt;/h3&gt;
&lt;p data-end=&quot;1157&quot; data-start=&quot;1022&quot; data-ke-size=&quot;size16&quot;&gt;.ts 확장자를 포함한 import는 Node.js ESM 환경에서 주로 사용되며, TypeScript가 이를 처리할 때 .js로 변환하는 과정에서 충돌이 발생할 수 있기 때문입니다. 그래서 &lt;b&gt;출력을 하지 않도록 제한&lt;/b&gt;한 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;1157&quot; data-start=&quot;1022&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>allowimportingtsextensions</category>
      <category>frontenddev</category>
      <category>JavaScript</category>
      <category>nodeesm</category>
      <category>tsconfig</category>
      <category>TypeScript</category>
      <category>개발자블로그</category>
      <category>개발팁</category>
      <category>에러해결</category>
      <category>프론트엔드</category>
      <author>ji-frontdev</author>
      <guid isPermaLink="true">https://ji-frontdev.tistory.com/103</guid>
      <comments>https://ji-frontdev.tistory.com/entry/tsconfigjson-%EC%84%A4%EC%A0%95-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-allowImportingTsExtensions%EB%8A%94-%EC%99%9C-%EC%97%90%EB%9F%AC%EA%B0%80-%EB%82%A0%EA%B9%8C#entry103comment</comments>
      <pubDate>Tue, 15 Apr 2025 21:19:49 +0900</pubDate>
    </item>
  </channel>
</rss>