<?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>Mon, 28 Jul 2025 04:06:54 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[[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>