<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>whale_dream.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Fri, 26 Dec 2025 09:03:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>whale_dream.log</title>
            <url>https://velog.velcdn.com/images/whale_dream/profile/c5b6e5e1-325e-4bb1-a918-89a80f1a0178/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. whale_dream.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/whale_dream" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[단일 Protected Trunk-based 커스텀 브랜치 전략]]></title>
            <link>https://velog.io/@whale_dream/%EB%8B%A8%EC%9D%BC-Protected-Trunk-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@whale_dream/%EB%8B%A8%EC%9D%BC-Protected-Trunk-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Fri, 26 Dec 2025 09:03:03 GMT</pubDate>
            <description><![CDATA[<p>오늘은 저희 팀과 본부에서 사용 중인 커스텀 브랜치 전략에 대해 소개하려고 합니다.</p>
<p>약 1년 9개월 전, 기존에 사용하던 브랜치 전략에 한계를 느끼면서
당시 같은 신입 팀원분들과 함께 문제를 정리하고, 시니어 개발자분들의 조언을 받아 설계한 전략입니다.</p>
<p>개인적으로는 지금까지도 상당히 만족하며 사용 중인 전략이어서,
비슷한 고민을 하고 계신 분들께 참고가 되었으면 하는 마음으로 정리해봅니다.</p>
<h2 id="1-기존-브랜치-전략">1. 기존 브랜치 전략</h2>
<h3 id="11-과거-브랜치-전략">1.1. 과거 브랜치 전략</h3>
<p>과거에 사용하던 브랜치 전략은 dev 브랜치가 모든 역할을 떠안는 구조였습니다.</p>
<ul>
<li>모든 feature / bugfix가 dev 브랜치로 바로 통합되었고</li>
<li>dev 브랜치가 개발·통합·배포 역할을 동시에 수행했으며</li>
<li>릴리즈 일정도 명확하지 않아, 요청이 들어올 때마다 dev에서 바로 분기되었습니다</li>
</ul>
<p><del>이것도 전략이라고 부를 수 있을까요</del></p>
<h3 id="12-어떤-문제가-있었냐면요">1.2. 어떤 문제가 있었냐면요</h3>
<p> *<em>사업 일정에 따라 dev 브랜치는 완성도와 무관하게 stg로 강제 배포되거나 패키징되어야 했고,
그 과정에서 미완성 코드가 함께 배포되는 문제가 반복적으로 발생했습니다. *</em></p>
<p>또한 릴리즈 시점마다 개발 중인 코드를 수동으로 주석 처리해야 했는데, 제가 개발한 부분이 아니면 어떤 부분을 주석처리 해야하는지도 명확하지 않았습니다. </p>
<h2 id="2-현재의-커스텀-브랜치-전략">2. 현재의 커스텀 브랜치 전략</h2>
<h3 id="21-tl-dr">2.1. TL; DR</h3>
<p>문제를 해결하기 위해 새로운 전략을 고안했습니다.
이름을 붙여보자면 이렇습니다.</p>
<blockquote>
<p>단일 Protected Trunk-based 브랜치 전략</p>
</blockquote>
<p>신규 인력이 팀(또는 본부)에 합류하면, 저는 보통 이렇게 설명합니다.</p>
<ol>
<li><p>이 전략의 주인공인 base 브랜치는 안전한 단일 trunk이며, <strong>언제든지 배포·패키징이 되더라도 이상없는 안전한 코드들만 반영</strong>되어야 합니다. 반면 dev 브랜치는 배포된 개발환경과 연결된 브랜치이며, playground로 사용하고 있습니다.</p>
</li>
<li><p>모든 feature, bugfix, refactor 등은 base에서 따고, 해당 작업에 대해 개발한 당사자의 1차 검증이 완료되었을 때 PR을 통해 코드리뷰를 받고 base로 머지합니다.</p>
<ul>
<li>여기서 말하는 개발자의 1차 검증은 dev 브랜치로의 머지 및 dev 브랜치와 연결된 개발환경에서의 실제 동작 확인입니다. dev 브랜치로의 머지는 코드리뷰 없이 자유롭게 가능하고, <strong>dev -&gt; base 로 소스가 흘러들어오지만 않도록</strong> 주의하면 됩니다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li>그래도 어렵다면 두가지만 지켜주시면 됩니다.<ul>
<li>작업 브랜치에서 dev 브랜치를 pull 받는 건 
<span style="color:red"> 절대 금지</span> 입니다. dev를 pull 받는 일은 거의 없습니다.</li>
<li>개발자는 작업을 완료하면 머지를 두번 해야합니다. (dev로 먼저, 1차 검증 후 base로)</li>
</ul>
</li>
</ol>
<h3 id="22-자세히-설명하자면">2.2. 자세히 설명하자면</h3>
<h4 id="221-그림으로-이해하기">2.2.1. 그림으로 이해하기</h4>
<p><img src="https://velog.velcdn.com/images/whale_dream/post/4c09a50e-7a7e-4653-90ea-1af1518660de/image.png" alt=""></p>
<p>base는 항상 Single Source of Truth입니다.
보시다시피 릴리즈의 hotfix를 제외한 모든 변경 사항은 base에서 시작하며,
dev의 소스가 base로 직접 유입되는 일은 없습니다.</p>
<h4 id="222-의견-도출-과정">2.2.2. 의견 도출 과정</h4>
<p>새 브랜치 전략을 설계하며 세운 조건은 명확했습니다.</p>
<blockquote>
</blockquote>
<p>⭐️ 1. 언제든지 패키징 될 수 있을 정도로 안정되면서, 기능 하나하나가 개발될 때마다 바로 반영할 수 있어야 하고,
2. PR을 자연스럽게 활성화 시킬 수 있는 방향이면 좋겠다.</p>
<p>초기에는 기존 전략을 유지한 채 PR만 규칙으로 강제하는 안도 검토했습니다.
이 방식은 새로운 전략 도입에 대한 거부감이 적고, 빠르게 적용할 수 있다는 장점이 있었습니다.</p>
<p>하지만 고민 끝에, 이 방식은 문제의 본질을 해결하지 못한다고 판단했습니다.
특히 1번 조건을 개발자 개인의 양심과 자율성에만 의존하는 구조는
지속 가능성 측면에서 불안 요소가 크다고 느꼈습니다.</p>
<p>그 결과,
<strong>base를 protected trunk로 명확히 분리하자는 결론</strong>에 도달했습니다.</p>
<p>이 전략은 다음과 같은 방식으로 두 조건을 모두 만족시킵니다.</p>
<ol>
<li><p>base에는 함부로 머지할 수 없기 때문에,
안정적인 상태를 유지한 채 새로운 기능을 dev 환경에서 먼저 검증할 수 있습니다.</p>
</li>
<li><p>feature → base PR은 개발 초기부터 열어둘 수 있어서
충분한 코드 리뷰 시간을 확보할 수 있고
base 머지는 1명 이상의 approve를 필수로 하여 최소한의 리뷰가 구조적으로 보장됩니다.</p>
</li>
</ol>
<h4 id="223-자주-받는-질문들">2.2.3. 자주 받는 질문들</h4>
<blockquote>
<p>Q1. 왜 base 와 배포된 브랜치 dev 굳이 분리했나요?</p>
</blockquote>
<p>A1. 
“로컬에서 충분히 검증된 코드를 dev에 올리면 되는 것 아닌가?”라는 질문을 자주 받습니다.
 저희는 Micro Frontend Architecture 중 하나인 Webpack 5의 module federation 구조로 구성되어 있는데요, 하나의 host와 총 약 8-9개의 remote 모듈이 있습니다. 
 로컬 개발 시에는 모든 remote 서버를 동시에 띄우지 않기 때문에,
로컬에서는 정상 동작하던 코드가 실제 개발 환경에서는
remote 간 충돌로 인해 예상치 못한 이슈를 일으킨 경험이 여러 차례 있었습니다.</p>
<p>즉, local에서 동작한다고 해서 배포된 환경에서도 안전하다고 확신할 수 없는 구조였기 때문에
dev 환경에서의 실검증 단계를 분리할 필요가 있었습니다.</p>
<blockquote>
<p>Q2. base에 배포된 환경이 없어서 불안하지는 않나요? 시간이 흐를수록 dev와 base의 소스가 점점 더 멀어질텐데요.</p>
</blockquote>
<p>A2. 이 부분은 이번 전략을 고민하면서 가장 염려했던 지점입니다.</p>
<ul>
<li>dev의 소스가 실수로 base에 병합될 가능성</li>
<li>실험용 코드가 dev에만 누적되어 두 브랜치의 형상이 멀어질 가능성</li>
</ul>
<p>이런 리스크를 고려해 base 전용 배포 환경도 검토했지만,
리소스 제약 이슈도 있고, base → tst 검증 브랜치에 이미 전용 검증 환경이 존재한다는 점을 고려해
필수 구성 요소는 아니라고 판단했습니다.</p>
<p>그럼에도 불구하고 안전장치를 하나 더 두기 위해,
dev와 base의 괴리가 일정 수준 이상 벌어졌다고 판단되면
기존 dev를 삭제하고 base 기준으로 dev를 재분기하는 운영 원칙을 두었습니다.</p>
<p>이를 통해 브랜치 간 괴리가 장기적으로 누적되는 상황을 방지하고자 했습니다.</p>
<blockquote>
<p>Q3. 충돌이 빈번할 것 같은데 충돌은 어떻게 해결하나요?</p>
</blockquote>
<p>A3. 하나의 기능에 대한 merge가 최소 두번(dev 로 n번, base로 1번) 이루어져야 하기 때문에 충돌이 상대적으로 꽤 빈번하게 발생합니다.</p>
<ul>
<li>base 머지 시 충돌
  -&gt; origin/base를 pull 받아 로컬에서 해결</li>
<li>dev 머지 시 충돌
  -&gt; feature에서 dev를 pull 받는 것은 금지
  -&gt; dev 기준의 충돌 해결용 브랜치를 따서 feature를 병합하거나 cherry-pick</li>
</ul>
<h3 id="23-얻은-효과는요">2.3. 얻은 효과는요!</h3>
<p>결국 궁극적 목표였던 <span style='color:red'>안정성</span>을 얻었습니다. 
개발중에 갑자기 실제 사업 환경 배포 요청이 들어와도 더이상 불안하지 않습니다.
물론 base에 머지된 소스들이 버그가 없다! 는 전혀 아니지만, 크리티컬한 이슈가 없음은 확실하게 보장해줍니다.</p>
<p>또한 PR이 기능 개발 초기부터 열려 있기 때문에
완벽하지 않더라도 동료 개발자의 코드를 미리 보고 이야기하는 기회가 조금 더 많아졌습니다.</p>
<h3 id="24-그럼에도-한계점은">2.4. 그럼에도 한계점은!</h3>
<p>장점 만큼이나 한계점도 명확합니다.</p>
<ol>
<li><p>충돌 해결이 매우 귀찮습니다.</p>
<ul>
<li>그러나 기능 단위 아키텍쳐인 FSD(Feature Sliced Design)으로 프론트엔드 아키텍쳐를 개선한 이후로는, 각자 자신의 기능별로 slice를 나누어 개발하기 때문에 충돌 빈도가 확실히 줄었습니다.</li>
</ul>
</li>
<li><p>두번 merge 하는 것이 귀찮습니다.</p>
<ul>
<li>귀찮음에서만 끝나면 다행인데, 가끔 까먹고 dev, base 둘 중 하나의 브랜치에만 merge 하는 경우도 종종 발생합니다. 이는 결국 개발자의 주의가 필요한 부분입니다.</li>
</ul>
</li>
</ol>
<h2 id="3-회고">3. 회고</h2>
<h3 id="31-이-새로운-브랜치-전략에-만족하냐구요">3.1. 이 새로운 브랜치 전략에 만족하냐구요?</h3>
<p>개인적으로는 정말 만족합니다.
분명 trade-off가 있지만... <span style="color:red"> 안정성 </span>에서 얻는 이점이 그 모든 불편함을 충분히 상쇄한다고 생각합니다.</p>
<p>약 15~20명의 프론트엔드 개발자가 하나의 모노레포에서 작업하는 현재의 환경에서 base 브랜치는 심리적인 안전장치로도 큰 역할을 하고 있습니다.</p>
<h3 id="32-어려웠던-점은">3.2. 어려웠던 점은</h3>
<p>브랜치 전략을 수립할 당시는 지금보다 훨씬 신입이었습니다.</p>
<ul>
<li>브랜치 전략 변경에 대한 공감대를 형성하고</li>
<li>“왜 굳이 이렇게까지 해야 하느냐”는 질문에 합리적인 이유를 들어 답하는 것이
조금 어려웠지만, 충분한 논의 끝에 대부분의 구성원이 전략의 필요성에 공감하게 되었습니다.</li>
</ul>
<h3 id="33-배운-점">3.3. 배운 점</h3>
<p>문제가 발생했을 때,
문제를 반복적으로 해결하는 방식이 아니라 문제 발생 자체를 구조적으로 예방하는 접근이 얼마나 중요한지 배울 수 있었습니다. </p>
<p>또, 동료 신입 개발자분들과의 지속적인 논의를 통해 더 나은 해결책을 만들어가는 경험이 즐거웠습니다.</p>
<h2 id="4-끝으로">4. 끝으로</h2>
<p>이 브랜치 전략은 모든 팀에 적합한 만능 해법은 아닙니다.</p>
<ul>
<li>다인원이 하나의 레포에서 작업하고</li>
<li>배포 안정성이 중요한 팀
이라면 한 번쯤 참고해볼 만한 전략이라고 생각합니다.</li>
</ul>
<p>더 나은 개선점이나 의견이 있다면 언제든지 이야기 나눠보고 싶습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIL] 항해 플러스 프론트엔드 6기 1,2,3주차 회고]]></title>
            <link>https://velog.io/@whale_dream/WIL-%ED%95%AD%ED%95%B4-%ED%94%8C%EB%9F%AC%EC%8A%A4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-6%EA%B8%B0-123%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@whale_dream/WIL-%ED%95%AD%ED%95%B4-%ED%94%8C%EB%9F%AC%EC%8A%A4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-6%EA%B8%B0-123%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 28 Jul 2025 04:06:54 GMT</pubDate>
            <description><![CDATA[<p> 항해 플러스 프론트엔드 6기 코스를 시작한지 어느새 3주가 지나 첫번째 챕터 JS &amp; React 딥다이브 챕터가 마무리되었습니다. 3주간을 되돌아보면 제 일상의 대부분이 항해 플러스였습니다. 매주 난도 높은 과제를 수행하는 것이 저에겐 쉽진 않았지만 그만큼 많은 것을 얻고 느끼고 배운 3주였습니다. 사실 매주 회고를 작성하려고 했지만… 헤헤 실패! 그래도 첫번째 챕터가 끝난 기념으로 첫번째 회고를 작성해보겠습니다.</p>
<hr>
<h2 id="항해-플러스-신청-계기">항해 플러스 신청 계기</h2>
<p>내가 항해 플러스를 신청한데에는 사실 아주 많은 이유가 있다. 
아마 한 20가지 정도는 작성할 수 있을것 같은데ㅋㅋㅋ</p>
<p>몇가지만 적어보자면…</p>
<ol>
<li>작년 한해동안 회사에서 잘하는 동료분에게 많은 도움을 받으면서 나도 실력을 향상시켜서 동료분들에게, 회사에 그렇게 도움이 되는 사람이 되고 싶었고</li>
<li>한번 더 크게 성장할 원동력이 필요했고</li>
<li>3년차가 되면서 회사 업무와 코드에 익숙해졌는데, 다른 회사의 개발자분들은 어떻게 개발을 하고 있는지 궁금해서 회사 밖의 많은 프론트엔드 개발자분들을 만나고 싶었고</li>
<li>기본기를 다져서 내 실력에 스스로 자신감을 얻고 싶었다.</li>
</ol>
<p>그 밖의 수십가지 이유로 항해플러스에 오게 됐는데,, 3주차가 지난 지금 항해 플러스 하길 정말 잘했다는 생각이 든다. 이미 이 중 상당 부분을 달성하고 있기 때문이다!</p>
<hr>
<h1 id="kpt-회고">KPT 회고</h1>
<h2 id="🐳-keep">🐳 Keep</h2>
<p><strong>현실과 타협을 잘 했다!</strong></p>
<p>3주를 지나고 보니 가장 잘했다고 생각하는 점이다. </p>
<p>처음에는 과제와 관련된 모든 개념들을 완벽하게 이해하고 차근차근 모든걸 다 이해하면서 AI 도움을 거의 받지 않고 과제를 수행하고 싶었는데…..  내 실력에는 불가능한 일이었다는걸 1주차에 바로 깨달았다.</p>
<p>어떻게 해서라도, 내 코드가 내 마음에 전혀 들지 않더라도 과제를 일단 통과하기 vs 최대한 스스로 학습하고 완벽하게 구현하되 과제의 통과 유무는 신경쓰지 말기</p>
<p>이 두가지 중에서 많은 고민을 하다 결국 그래도 과제 기간은 어떻게든 맞추고 이후에 시간을 내서 다시 한번 과제를 수행하자는 마음으로 첫번째를 택했고, 결과적으로 돌아보니 이렇게라도 과제 전체를 한 것이 항해 플러스의 진도를 따라가며 코치님께서 전달하고자 하셨던 내용들을 흡수하는데 더 도움이 되었던 것 같다.
<br /></p>
<p><strong>한주 한주 지날수록 나아졌다!</strong></p>
<p>1주차는 좌절과 절망의 연속이었다. 그냥 매일매일이 형편없는 나 견디기 챌린지..🥹
2주차는 그래도 1주차보단 SPA에 대한 이해나, AI 사용 등등 모든 면에서 더 나은 과제 수행을 했다.
3주차는 각각의 개념을 확실하게 이해하고자 노력했고, 나름 만족스럽게 과제 수행을 했다.</p>
<p>사실 이게 난이도 차이도 있겠지만, 1주차의 고생들이 2,3,주차를 과제를 조금 더 수월하게 수행할 수 있게 만들었다고 생각한다.
아직도 난 많이 부족하지만 어제보다 나아지고 있음을 생각하며 더 열심히 해야겠다.
<br /></p>
<h2 id="🦐-problem">🦐 Problem</h2>
<p><strong>부족했던 부분들을 못 메꿨다!</strong></p>
<p>일단 시간내에 과제를 수행하고 과제를 복습하면서 부족한 부분을 메꾸자 ← 이게 내 다짐이었는데..
그냥 과제를 시간내에 수행하기만 한 사람이 된 것 같기도 하다.
과제 끝나면 찾아보기로 한 궁금한 점들이 많았는데 그중 한 절반정도만 찾아보고 나머지는 귀찮음에 날려버렸다.다음주부턴 귀찮음을 이기고 꼭 궁금했던 부분 다 해결하기 도전..
<br /></p>
<p><strong>다양한 의견 교류를 하지 못했다!</strong></p>
<p>늘 과제 진도가 다른분들에 비해 한박자 느려서.. 아는 게 많지 않아 말할만한 의견이 별로 없었다..
그래도 클린코드 주에는 평소에 생각했던 내가 선호하는 코드 작성 방향들이 있으니 이야기 할 만한 부분이 있을 것 같다..! (말할 용기만 생긴다면…)
<br /></p>
<h2 id="🔥-try">🔥 Try</h2>
<p><strong>조금 더 적극적으로 코드리뷰 참여하기!</strong></p>
<p>예전에 한번 코드리뷰를 받은적은 있지만, 코드리뷰를 내가 한 건 항해가 처음이었다. 
나는 아는게 없는데 내가 코드리뷰를…? 이라는 마음에 처음엔 조금 어색하고 두려웠는데, 팀원분들과 가벼운 내용부터 코드리뷰를 해 보니 생각보다 어려운 일이 아니었다..! 
그리고 무엇보다 다른 분들의 코드나 다른분들께서 해주신 코드리뷰를 보고 “아 나도 이런식으로 해봐야겠다~ 이런걸 신경써야 겠다~” 를 느낄 수 있어서 좋았다.
4주차 부터는 조금 더 적극적으로 의견 공유를 하고 코드 리뷰에 참여해야겠다.
<br /></p>
<p><strong>깊게 공부하기!</strong></p>
<p>1,2,3 주차를 하면서 느낀건 여태껏 내가 정말 겉핥기식 공부만 하고 있었다는 것이다. 
3주간 프레임워크 없이 SPA 만들기를 진행하면서 SPA 프레임워크의 동작 원리를 어느정도 알고있다고 생각했는데 알고있기는 커녕 나는 여태껏 궁금해 한 적 조차 없었다는 사실을 깨달았다.
SPA 프레임워크, 가상돔, 리액트 훅들과 전역 상태 관리를 직접 구현해보니, 
과거의 내가 위의 개념들이 무엇인지 이해하기 위해 했던 공부와 이런 것들을 직접 구현하기 위해 무엇이 필요한지 고민했던 3주간의 공부의 깊이가 얼마나 차이나는지 느낄 수 있었다.</p>
<p>어떻게 해야 깊게 공부할 수 있는지 조금은 느낄 수 있었고, 나도 앞으로 이렇게 깊게 파고드는 공부를 하기 위해 노력해야 겠다는 다짐을 했다.</p>
<hr>
<h1 id="회고를-마치며">회고를 마치며</h1>
<p>체력적으로나 정신적으로 꽤 힘들긴 했지만, 많은 걸 배우고 느낀 후회 없는 3주였습니다.
어느정도냐면.. 이제 겨우 3주 끝냈으면서 벌써부터 항해 끝나면 일상이 정말 허전하겠다…라는 생각이 종종 든답니다..
3주간 SPA부터 가상돔, 리액트 훅들과 전역상태관리를 직접 구현해보니 각각 어떤 문제를 해결하고자 했는지를 확실히 이해할 수 있었습니다.</p>
<p>그리고 이를 바탕으로 실무에서 특정 이슈에 대해 다른 분이 제시한 해결 방안이 SPA가 추구하는 방향과 맞지 않다는 주장도 할 수 있게 되었다는 점이 개인적으로는 꽤 뿌듯합니다.(나도 이제 자신있게 주장할 수 있다아아아<del>~</del>!!!!) </p>
<p>이렇게 성장할 수 있게 도와주신 준일 코치님께 진심으로 감사드립니다.</p>
<p><strong>그리고 무엇보다 회고에서 가장 하고싶었던 말은…</strong></p>
<p>이렇게 3주를 버틸 수 있었던 건 우리 4팀분들 덕분입니다아🫶
최고의 학메님과 최고의 팀장님과 최고의 팀원들 뿐인 4팀에 속해서 행복합니다🐳
북끄러워서 표현은 잘 못하지만 4팀 모두들 내가 진짜 많이 조아해에에~~!!</p>
<p>그리고 앞으로 함께할 7팀분들도 잘 부탁드립니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useTitle 적용 가이드]]></title>
            <link>https://velog.io/@whale_dream/useTitle</link>
            <guid>https://velog.io/@whale_dream/useTitle</guid>
            <pubDate>Wed, 25 Jun 2025 04:38:09 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<h2 id="요약">요약</h2>
<p>각 목록 페이지의 title과 description을 백엔드 api를 통해 받은 메뉴 데이터 값으로 출력하도록 하는 useTitle 컴포저블을 작성하여 host 모듈에서 expose 해두었습니다.</p>
<h2 id="기존-문제점">기존 문제점</h2>
<p>*** 에서는 메뉴 관리를 통해 메뉴명을 바꿀 수 있으며, 매 버전마다 메뉴명이 조금씩 달라집니다. 
기존에는 각 목록 페이지의 title과 description 출력에 프론트의 정적인 값을 사용했기 때문에, 메뉴 관리로 LNB의 메뉴명을 변경했을 때 프론트엔드 소스를 수정하여 재배포하지 않으면 목록의 title과 LNB 메뉴명이 일치하지 않는 이슈가 있었습니다. 
이를 프론트 소스 변경 없이 해결하기 위해 [메뉴 관리]에서 설정한 메뉴 데이터의 title과 description을 반환하는 훅을 작성했습니다.</p>
<h1 id="구현-방식">구현 방식</h1>
<p>저희 솔루션에서는 route의 path가 달라질 때마다 현재 path(url)가 속한 LNB를 백엔드에서 받은 메뉴 데이터에서 찾고, 찾은 정보를 host의 useMenuStore에 저장해둡니다. useTitle 훅에서는 이 정보들 중 title(현재 path가 속한 LNB 이름)과 description을 반환합니다.</p>
<p>useTitle.ts</p>
<pre><code>// 목록 페이지 title, description을 newMenuStore의 현재 선택된 메뉴 데이터에서 가져오는 훅
import { computed } from &#39;vue&#39;;
import { useMenuStore } from &#39;@/stores/menu/useMenuStore&#39;;
export const useTitle = () =&gt; {
  const store = useMenuStore();
  const pageTitle = computed(() =&gt; store.currentMenuInfo?.lnbMenuName ?? &#39;&#39;);
  const pageDescription = computed(
    () =&gt; store.currentMenuInfo?.description ?? &#39;&#39;
  );
  return {
    pageTitle,
    pageDescription,
  };
};
</code></pre><h1 id="적용-가이드">적용 가이드</h1>
<ol>
<li><p>remote 레포지토리 shims-vue.d.ts 파일에 host useTitle 컴포저블 타입 정의를 추가합니다.</p>
<pre><code>// shims-vue.d.ts파일
declare module &#39;host*****/hooks/useTitle&#39; {
import { Ref } from &#39;vue&#39;;
export const useTitle: () =&gt; {
 pageTitle: Ref&lt;string&gt;;
 pageDescription: Ref&lt;string&gt;;
};
}</code></pre></li>
<li><p>Title 및 description 적용이 필요한 곳에서 useTitle 컴포저블을 import 하여 사용합니다.</p>
<pre><code>&lt;template&gt;
&lt;a-flex class=&quot;page-title&quot;&gt;
 &lt;a-typography-title :level=&quot;1&quot;&gt;
   {{ pageTitle }}
 &lt;/a-typography-title&gt;
 &lt;p class=&quot;typography-description&quot;&gt;
   {{ pageDescription }}
 &lt;/p&gt;
&lt;/a-flex&gt;
&lt;/template&gt;
</code></pre></li>
</ol>
<script setup lang="ts">
import { useTitle } from 'host*****/hooks/useTitle';

const { pageTitle, pageDescription } = useTitle();
</script>
<pre><code>
# remote 모듈 적용 예시
remote에서는 목록 페이지에 공통으로 ListLayout 컴포넌트를 적용하고 있습니다.  </code></pre><template>
  <TheLayout>
    <div class="info-container-pull">
      <a-flex class="page-title">
        <a-typography-title :level="1">
          <slot name="title"></slot>
          {{ showTitle }}
        </a-typography-title>
        <p class="typography-description">
          {{ showDescription }}
        </p>
      </a-flex>
      <slot />
      <!--@deprecated -->
      <slot name="table" />
      <slot name="content" />
    </div>
  </TheLayout>
</template>

<script setup lang="ts">
import TheLayout from '@/layouts/TheLayout.vue';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useT } from '@/hooks/i18n/useT';
import { useTitle } from 'host******/hooks/useTitle';

const { t } = useT();
const route = useRoute();

const props = defineProps<{
  title?: string;
  description?: string;
  /**
   * 백엔드 메뉴 데이터에서 받은 title/description과는 별개의 값으로 출력하고 싶을 때 isStatic 값을 true로 넘겨주시면 됩니다.
   */
  isStatic?: boolean;
}>();

const { pageTitle, pageDescription } = useTitle();

const showTitle = computed(() => {
  const fallbackTitle = t(props.title ?? (route.meta.title || ''));
  return props.isStatic ? fallbackTitle : pageTitle.value || fallbackTitle;
});

const showDescription = computed(() => {
  const fallbackDescription = t(
    props.description ?? (route.meta.subTitle || '')
  );
  return props.isStatic
    ? fallbackDescription
    : pageDescription.value || fallbackDescription;
});
</script>
<style scoped></style>
<pre><code>
ListLayout 에서는 기본적으로 useTitle의 pageTitle과 pageDescription을 출력합니다.

## Edge case
1. LNB 데이터에 존재하지 않는 목록 페이지 (ex. 마이페이지)

마이페이지처럼 LNB 데이터에 존재하지 않는 목록 페이지는 menuStore에 현재 메뉴에 대한 정보가 없습니다. 
따라서 useTitle의 pageTitle, pageDescription은 빈문자열(&#39;&#39;)을 반환하며, falsy 값으로 평가되어 fallbackTitle과 fallbackDescription이 출력됩니다.



2. 메뉴 데이터의 LNB명이 아닌 프론트에서 별도로 정의해둔 title/description을 출력해야 하는 경우

프론트에서 별도로 정의해둔 title/description을 출력해야 하는 경우에는 해당 목록 컴포넌트에서 ListLayout을 호출 할 때 isStatic props를 true로 전달해주시면 됩니다. isStatic이 true일 경우, props로 함께 받은 title, description, 혹은 route.meta에 정의된 값이 출력됩니다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[모듈페더레이션 환경에서 Vue Error Boundary 구현하기]]></title>
            <link>https://velog.io/@whale_dream/%EB%AA%A8%EB%93%88%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Vue-Error-Boundary-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@whale_dream/%EB%AA%A8%EB%93%88%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Vue-Error-Boundary-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 19 Mar 2025 16:33:05 GMT</pubDate>
            <description><![CDATA[<h1 id="0-배경">0. 배경</h1>
<p> 현재 개발하고 있는 솔루션은 화면에서 Error가 발생하면, 해당 Error 발생 시점부터 사용자가 새로고침을 하기 전까지 애플리케이션 전체가 정상적으로 동작하지 않는 이슈가 있습니다. 이로 인해 사용자 입력이 반영되지 않거나 화면이 다시 렌더링 되지 않는 등의 현상이 나타납니다. 이를 해결하기 위한 방법인 Error Boundary에 대해 리서치 하고 테스트 한 결과를 정리했습니다.
 <br /></p>
<h1 id="1-error-boundary란">1. Error Boundary란?</h1>
<h2 id="error-boundary의-역할">Error Boundary의 역할</h2>
<p> UI의 일부분에 존재하는 자바스크립트 에러가 전체 애플리케이션을 중단시키면 안 됩니다. Error boundary 컴포넌트는 하위 컴포넌트에서 발생한 에러가 상위 컴포넌트로 전파되는 것을 막고, 깨진 컴포넌트 트리 대신 Fallback UI를 보여줍니다. 즉, 상위 컴포넌트는 하위 컴포넌트의 에러를 모른 채 정상적으로 계속 작동 할 수 있습니다.</p>
<p>또한, 에러 종류에 따라 적절한 화면을 정의하여 보여줌으로써, 애플리케이션 전반에서 에러와 관련된 일관성 있는 사용자 경험을 제공할 수 있습니다.</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;SiblingComponent /&gt;
    &lt;ErrorBoundary&gt;
      &lt;ChildComponent /&gt;
    &lt;/ErrorBoundary&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script setup lang=&quot;ts&quot;&gt;
import ErrorBoundary from &#39;./ErrorBoundary.vue&#39;;
import ChildComponent from &#39;./ChildComponent.vue&#39;;
import SiblingComponent from &#39;./SiblingComponent.vue&#39;;</code></pre><p>위의 코드에서, ErrorBoundary 컴포넌트로 감싸진 ChildComponent에서 발생한 에러는 상위 컴포넌트와 형제 컴포넌트인 SiblingComponent에 영향을 주지 않습니다.</p>
<h2 id="react-error-boundary참고">React Error Boundary(참고)</h2>
<p>React는 v16 부터 공식적으로 Error Boundary 컴포넌트를 제공합니다. React의 Error Boundary는 렌더링 도중 생명주기 메서드 및 그 아래에 있는 전체 트리에서 에러를 잡아냅니다.
<a href="https://ko.legacy.reactjs.org/docs/error-boundaries.html">에러 경계(Error Boundarise) - React 공식문서</a></p>
<p><img src="https://velog.velcdn.com/images/whale_dream/post/96936e26-bc10-4540-af38-c63f029d0a26/image.png" alt="토스 10to100 웹페이지 에러 바운더리 Fallback UI"></p>
<h1 id="2-vue-error-boundary-구현하기">2. Vue Error Boundary 구현하기</h1>
<p>Vue는 Error Boundary 컴포넌트를 공식적으로 제공하지는 않지만, 자식 컴포넌트에서 전파된 에러가 캡쳐되었을 때 호출될 콜백을 등록할 수 있는 onErrorCaptured lifecycle hook을 제공합니다. 해당 훅을 사용하여 Error Boundary를 구현하였습니다.
<a href="https://ko.vuejs.org/api/composition-api-lifecycle#onerrorcaptured">Life Cycle Hook #onErrorCaptured() - Vue 공식문서</a></p>
<h2 id="errorboundaryvue">ErrorBoundary.vue</h2>
<p>완성본이 아닌 임시로 구현한 코드입니다.</p>
<pre><code>&lt;template&gt;
  &lt;div v-if=&quot;hasError&quot; class=&quot;error-boundary-overlay&quot;&gt;
    &lt;div class=&quot;error-container&quot;&gt;
      &lt;div v-if=&quot;errorStatus === 403&quot; class=&quot;error-boundary-content&quot;&gt;
        &lt;span
          class=&quot;material-symbols-outlined material-symbols-filled material-symbols-14 material-symbols-default error-icon&quot;
          &gt;lock&lt;/span
        &gt;
        &lt;p class=&quot;error-message&quot;&gt;
          작업을 수행할 권한이 없습니다. 관리자에게 문의하세요.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;div v-else-if=&quot;errorStatus === 500&quot; class=&quot;error-boundary-content&quot;&gt;
        &lt;span
          class=&quot;material-symbols-outlined material-symbols-filled material-symbols-14 material-symbols-default error-icon&quot;
          &gt;error_outline&lt;/span
        &gt;
        &lt;p class=&quot;error-message&quot;&gt;API에 문제가 발생했습니다.&lt;/p&gt;
      &lt;/div&gt;
      &lt;div v-else-if=&quot;errorStatus === 503&quot; class=&quot;error-boundary-content&quot;&gt;
        &lt;span
          class=&quot;material-symbols-outlined material-symbols-filled material-symbols-14 material-symbols-default error-icon&quot;
          &gt;network_check&lt;/span
        &gt;
        &lt;p class=&quot;error-message&quot;&gt;
          시스템에 문제가 발생했습니다. 잠시후 다시 시도해주세요.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;div v-else class=&quot;error-boundary-content&quot;&gt;
        &lt;span
          class=&quot;material-symbols-outlined material-symbols-filled material-symbols-14 material-symbols-default error-icon&quot;
          &gt;warning&lt;/span
        &gt;
        &lt;p class=&quot;error-message&quot;&gt;예상치 못한 에러가 발생했습니다.&lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;slot v-else&gt;&lt;/slot&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot; setup&gt;
import {
  ref,
  onErrorCaptured,
  watch,
  toRaw,
  ComponentPublicInstance,
} from &#39;vue&#39;;
import { useRoute } from &#39;vue-router&#39;;
import { AxiosHeaders } from &#39;axios&#39;;
import { DefaultResType } from &#39;@/types/common/DefaultResType&#39;;

const hasError = ref(false);
const errorStatus = ref();
const route = useRoute();

interface FetchError {
  config: any;
  data: DefaultResType;
  headers: AxiosHeaders;
  request: any;
  status: number;
  statusText: string;
}

// 함수가 true를 반환하면 함수가 호출된 블록 스코프에서 타입이 FetchError로 추론
function isFetchError(error: any): error is FetchError {
  return (
    typeof error === &#39;object&#39; &amp;&amp;
    error !== null &amp;&amp;
    error.headers &amp;&amp;
    error.headers.constructor?.name === &#39;AxiosHeaders&#39;
  );
}

onErrorCaptured(
  (err: unknown, instance: ComponentPublicInstance | null, info: string) =&gt; {
    hasError.value = true;
    if (isFetchError(err)) {
      errorStatus.value = err.status;
    } else {
      errorStatus.value = 0;
      console.error(
        &#39;Error captured in ErrorBoundary Component: \n&#39;,
        err,
        &#39;\n in Component: &#39;,
        instance?.$?.type
      );
    }
    // 에러가 상위 컴포넌트로 전파되지 않도록 막음
    return false;
  }
);

//  라우트 변경 시 에러 상태 초기화
watch(
  () =&gt; route.path,
  () =&gt; {
    hasError.value = false;
    errorStatus.value = 0;
  }
);
&lt;/script&gt;

&lt;style scoped lang=&quot;scss&quot;&gt;
&lt;/style&gt;</code></pre><h3 id="핵심-동작-원리">핵심 동작 원리</h3>
<blockquote>
<p>ErrorBoundary 컴포넌트의 onErrorCaptured() 훅에서 false를 반환하면 에러가 더이상 상위 컴포넌트로 전파되지 않습니다. hasError 속성이 true이면 Fallback UI를 렌더링합니다.</p>
<ul>
<li>hasError 속성이 true 일 때 errorStatus 값에 따라 서로 다른 UI를 렌더링합니다.</li>
</ul>
</blockquote>
<h3 id="axios-error와-화면-자바스크립트-error-처리-분리">Axios Error와 화면 자바스크립트 Error 처리 분리</h3>
<p>Vue의 onErrorCaptured() 훅은 에러, 에러를 트리거한 컴포넌트 인스턴스, 에러 소스 유형을 지정하는 정보 문자열, 총 세 개의 인자를 받습니다. </p>
<pre><code>onErrorCaptured(
  (err: unknown, instance: ComponentPublicInstance | null, info: string) =&gt; {
})</code></pre><p>첫번째 인자인 err에는 모든 타입의 Error가 들어올 수 있기 때문에 기본적으로 unknown 타입을 갖습니다.</p>
<p>따라서 이 err를 에러의 종류(Axios Error / 그 밖의 Error)에 따라 분기하여 처리하려면 각 err의 속성에 안전하게 접근하기 위한 타입 가드가 필요합니다.</p>
<p>타입스크립트 instanceof 연산자를 이용해 타입가드를 설정하려고 했으나, 403, 500과 같은 Error response가 AxiosError 타입으로 판단되지 않는 이슈가 있습니다. 원인을 아직 명확히 알 수 없으나, tanstack-query로 response가 한번 더 감싸지면서 AxiosError 타입으로 추론되지 않는 것으로 짐작하고 있습니다. (추후 명확한 원인 파악 후 개선되어야 할 부분입니다.)</p>
<p> 우선은 FetchError 타입인지 판단할 수 있는 사용자 정의 타입 가드 함수를 정의하여 캡쳐한 err가 Axios Error인 경우 에러 코드를 errorStatus로 설정하는 로직을 구현했습니다. 그 외의 JS, 혹은 렌더링 및 라이프사이클 오류의 경우 errorStatus를 0으로 설정했습니다. </p>
<pre><code>// 함수가 true를 반환하면 함수가 호출된 블록 스코프에서 타입이 FetchError로 추론
function isFetchError(error: any): error is FetchError {
  return (
    typeof error === &#39;object&#39; &amp;&amp;
    error !== null &amp;&amp;
    error.headers &amp;&amp;
    error.headers.constructor?.name === &#39;AxiosHeaders&#39;
  );
}
onErrorCaptured(
  (err: unknown, instance: ComponentPublicInstance | null, info: string) =&gt; {
    hasError.value = true;
    if (isFetchError(err)) {
      errorStatus.value = err.status;
    } else {
      errorStatus.value = 0;
      console.error(
        &#39;Error captured in ErrorBoundary Component: \n&#39;,
        err,
        &#39;\n in Component: &#39;,
        instance?.$?.type
      );
    }
    // 에러가 상위 컴포넌트로 전파되지 않도록 막음
    return false;
  }
);</code></pre><h3 id="haserror-속성-초기화">hasError 속성 초기화</h3>
<p>Fallback UI를 보여줄것인지를 판단하는 hasError 속성은 기본적으로 애플리케이션이 처음 mount 되었을 때, ErrorBoundary 컴포넌트가 새로고침 등의 이유로 unMount되었다가 다시 mount 되었을 때, route.path가 이동했을 때 초기화 되도록 구현하였습니다.</p>
<h2 id="구현-과정에서의-고민과-결론">구현 과정에서의 고민과 결론</h2>
<h3 id="1-에러-바운더리를-host-remote-중-어디에-둘-것인지">#1 에러 바운더리를 host, remote 중 어디에 둘 것인지</h3>
<p> Error Boundary 컴포넌트는 솔루션 전체에서 일관성 있게 적용 되어야 하기 때문에, host에 두고 관리하는 것이 좋을 것 같습니다. host에서 Error Boundary 컴포넌트를 작성해서 expose 하고, 해당 컴포넌트를 remote에서 import해서 사용하는 방식으로 구현했고, 테스트 완료 했습니다.</p>
<p>host-app vue.config 파일</p>
<pre><code>exposes: {
     ...
    &#39;./components/ErrorBoundary&#39;: &#39;./src/views/error/ErrorBoundary&#39;,
     ...
 },
</code></pre><p>각 remote-app의 App.vue</p>
<pre><code>&lt;template&gt;
  &lt;TheLNB /&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;ErrorBoundary&gt;
      &lt;Content /&gt;
    &lt;/ErrorBoundary&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script lang=&quot;ts&quot; setup&gt;
import TheLNB from &#39;hostMaestro/components/TheLNB&#39;;
import ErrorBoundary from &#39;hostMaestro/components/ErrorBoundary&#39;;
&lt;script/&gt;</code></pre><h3 id="2-에러-바운더리를-어떤-레이어에-적용할지">#2 에러 바운더리를 어떤 레이어에 적용할지</h3>
<p>일반적으로 에러 바운더리를 두는 위치는 다음과 같습니다.
<img src="https://velog.velcdn.com/images/whale_dream/post/cb5c4dc3-64a5-4faa-8828-5d8894fdade6/image.png" alt=""></p>
<p>현재는 remote에서 LNB를 제외한 Content 영역에 ErrorBoundary 컴포넌트를 두었습니다.</p>
<p>일반적으로 에러는 특정 페이지에서 발생합니다. 특정 페이지에서 에러가 발생해도 사용자는 LNB 클릭을 통해 다른 페이지로 이동하여 정상적인 이용을 지속할 수 있어야 한다고 생각했기 때문에 LNB 컴포넌트를 제외하고 Content 영역에만 두었으나, 의견 주시면 감사하겠습니다.</p>
<p>ErrorBoundary 컴포넌트를 더 하위 레벨에서 사용할 수도 있습니다. </p>
<p>이 경우, ErrorBoundary로 감싸진 컴포넌트에 해당하는 영역만 Fallback UI가 노출됩니다. 차트 등, 일시적인 이슈가 생길 가능성이 조금 더 높은 컴포넌트를 감싸서 사용하면 UX 향상에 도움됩니다.</p>
<h3 id="3-에러-바운더리-상태를-언제-reset-할것인지">#3 에러 바운더리 상태를 언제 reset 할것인지</h3>
<p>하위 컴포넌트에서 발생한 Error가 캡쳐되면 onErrorCaptured() 훅에서 false를 반환하고 Fallback UI가 렌더링됩니다. 이 때 사용자가 다른 특정 행동을 하면 Fallback UI에서 벗어나서 사용자의 행동대로 계속 작동해야합니다. </p>
<p>대표적으로, 사용자가 LNB 등을 클릭하여 다른 페이지로 이동해서 라우트가 변경되었을 때, Fallback UI가 아닌 해당 페이지의 UI를 렌더링해야 합니다. 이를 위해 ErrorBoundary 컴포넌트 내에서 route.path를 감시(watch)해서 route.path에 변화가 생기면 hasError 를 false로 초기화하도록 구현하였습니다.</p>
<p> 기획 및 정책에 따라 Fallback UI에 ‘다시 시도하기&#39; 버튼을 두고, 버튼 클릭 시 에러 바운더리 상태를 reset하는 경우도 있습니다. GET 요청 실패 시 해당 버튼을 적용한다면 사용자는 직접 새로고침을 하지 않고도 다시 데이터를 fetching 할 수 있을 것입니다.</p>
<h3 id="4-에러-바운더리-적용-후에도-컴포넌트-에러-추적-유지하기">#4 에러 바운더리 적용 후에도 컴포넌트 에러 추적 유지하기</h3>
<p>개발자 경험(DX)를 위해서는 화면에서 자바스크립트 에러가 발생했을 때 콘솔 출력을 통한 에러 추적이 ErrorBoundary 컴포넌트 적용 전/후가 비슷한 수준으로 가능해야 합니다. 
<img src="https://velog.velcdn.com/images/whale_dream/post/4b64372d-7a9f-4603-beb7-ab2e9171bf68/image.png" alt="에러 바운더리 적용 후 콘솔 에러 출력"></p>
<p>초록색 박스 </p>
<p>기존에는 해당 초록색 박스 부분을 클릭하여 에러가 발생한 컴포넌트의 소스를 확인 할 수 있었습니다. 하지만 에러 바운더리 적용 이후에는 에러가 최종 캡쳐된 에러 바운더리 컴포넌트로 연결되어 디버깅에 도움되진 않습니다.</p>
<p>빨간색 박스</p>
<p>대신, 기존에 확인 가능했던 수준의 에러 추적은 보통 빨간색 박스 부분을 클릭하여 확인 할 수 있습니다. 해당 링크 클릭 시, 에러가 발생한 컴포넌트가 js로 변환된 파일의 소스를 볼 수 있어 대략적인 에러 추적이 가능합니다.</p>
<p>파란색 박스</p>
<p>조금 더 편리한 에러 추적을 위해 에러 캡쳐 시, 콘솔에 에러와 함께 에러가 발생한 인스턴스에 관한 정보를 출력하도록 하였습니다. 에러가 발생한 컴포넌트의 이름, 파일 경로 등을 확인 할 수 있습니다.</p>
<p>결론적으로, ts 파일, vue 파일 내의 template태그, script태그 각각에 에러를 발생시켜 테스트 해 본 결과, 에러 바운더리 적용 전후에서 유사한 수준으로 에러추적이 가능합니다.</p>
<h1 id="3-error-boundary-적용-결과">3. Error Boundary 적용 결과</h1>
<p>Error Boundary가 제대로 작동하는지 확인하기 위해 테스트 용으로 만든 임시 UI입니다.</p>
<blockquote>
<p>테스트를 위해 운영자 메뉴관리 페이지에 의도적으로 에러를 발생시킨 상황입니다.</p>
</blockquote>
<h2 id="error-boundary-적용-전">Error Boundary 적용 전</h2>
<blockquote>
<p>사내 컨플루언스 문서에는 영상을 첨부하였으나, 회사 제품이므로 이 포스트에서는 영상을 생략하겠습니다.</p>
</blockquote>
<p>에러 바운더리 컴포넌트를 적용하지 않았을 때 사용자가 애플리케이션을 사용하던 중 에러가 있는 페이지(운영자 메뉴관리 페이지)에 접속하면, 그 이후로 새로고침 하기 전까지는 애플리케이션 전체가 어떠한 작동도 하지 않습니다.</p>
<h2 id="error-boundary-적용-후">Error Boundary 적용 후</h2>
<p>에러 바운더리 컴포넌트를 적용했을 때, 사용자가 에러가 있는 페이지(운영자 메뉴관리 페이지)에 접속하면 해당 페이지에서 발생한 error가 Error Boundary 컴포넌트에서 캡쳐되어 더이상 상위로 전파되지 않으며, Fallback UI가 노출됩니다. 애플리케이션이 중단되지 않기 때문에 사용자는 새로고침 없이 다른 행동을 이어갈 수 있습니다.</p>
<h3 id="에러-타입-별-fallback-ui-적용-예시">에러 타입 별 Fallback UI 적용 예시</h3>
<p>( 사진 생략 )
403(권한없음) / 503(Service Unavailable) / ... / 그 외의 Error Fallback UI를 다르게 구현하였습니다.</p>
<h3 id="특정-컴포넌트에-에러-바운더리-적용">특정 컴포넌트에 에러 바운더리 적용</h3>
<p>( 사진 생략 )
에러 바운더리로 감싼 컴포넌트의 영역에만 Fallback UI가 노출되도록 구현하였습니다.
<br /></p>
<h1 id="4-추가적인-논의가-필요한-부분">4. 추가적인 논의가 필요한 부분</h1>
<h2 id="에러가-발생-했을때-어떤-ui를-보여줄-것인지-fallback-ui">에러가 발생 했을때 어떤 UI를 보여줄 것인지 (Fallback UI)</h2>
<p>에러 타입(ex. 자바스크립트 에러 / Axios 403, 500 에러 등)별로 각각 어떤 UI를 보여줄 것인지 기획/디자인/퍼블리싱이 필요합니다.</p>
<p>API 403(권한없음) response를 받았을 경우 Fallback UI에서 문의하기 이동할 수 있는 버튼 또는 링크를 제공해도 좋을 것 같습니다.</p>
<p>API 401(Unauthorized) response를 받았을 경우 로그인 페이지로 리다이렉트 시키는것이 가능한지 확인이 필요합니다.</p>
<h2 id="에러-바운더리를-어떤-레이어에-어떤-단위로-설정할지">에러 바운더리를 어떤 레이어에 어떤 단위로 설정할지</h2>
<p>적절한 수준에서 Error Boundary를 배치하면 오류가 발생해도 사용자 경험을 최대한 보호할 수 있습니다.</p>
<p>기본적으로 Content를 감싸도록 Error Boundary를 설정해 두었고, 
데이터 위젯, 차트, 리스트 등 특정 컴포넌트 단위로 에러 바운더리를 추가로 설정하여 개별 UI가 깨져도 나머지 UI는 정상 작동하도록 설정할 수 있습니다. 이와 관련한 정책이 필요합니다.</p>
<h2 id="에러-바운더리-상태를-언제-리셋할지">에러 바운더리 상태를 언제 리셋할지</h2>
<p>현재는 route.path의 변화가 있을 때만 에러바운더리 상태를 리셋하여 Fallback UI가 아닌 기존 화면을 재렌더링하도록 구현하였습니다.
그 밖의 어떤 상황에서 에러 바운더리 상태를 리셋할 것인지에 대한 정책이 필요합니다.</p>
<h1 id="5-개선사항">5. 개선사항</h1>
<p>403, 500, JS 에러 외에도 다양한 Error Status로 분기처리 할 수 있는 확장성을 고려하여 hasError 상태를 변경하거나, 특정 커스텀 동작을 수행할 수 있도록 Composable 함수(custom hook)로 구현하는 것을 고려하고 있습니다.</p>
<p>onErrorCaptured 훅에서 error를 사용자 정의 타입 가드 함수가 아닌, instanceof 연산자를 사용해 공식적인 AxiosError 타입으로 타입을 좁힐 수 있도록 수정이 필요합니다. 문제 원인을 찾는중입니다.</p>
<p>구현 가능성 정도만 테스트 했습니다. CMP에 실제로 적용을 위해서는 컴포넌트 고도화와 더 많은 논의가 필요합니다.</p>
]]></description>
        </item>
    </channel>
</rss>