<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>학습 및 삽질 기록</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 11 Jul 2024 08:04:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 학습 및 삽질 기록. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hgoguma_124" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Flutter Riverpod과 클린 아키텍처 정리]]></title>
            <link>https://velog.io/@hgoguma_124/Flutter-Riverpod%EA%B3%BC-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@hgoguma_124/Flutter-Riverpod%EA%B3%BC-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 11 Jul 2024 08:04:51 GMT</pubDate>
            <description><![CDATA[<p>Riverpod 라이브러리를 처음 써봐서 그런지 이것저것 이해가 안 가는 게 투성이다.
그래도 공식 사이트에 설명도 잘 되어 있고, 다양한 예제들이 있어서 다행이다. 1버전 2버전 중구난방인게 좀 아쉽지만...</p>
<p>Riverpod 공식 사이트에 있는 <a href="https://github.com/Uuttssaavv/flutter-clean-architecture-riverpod">클린 아키텍쳐 코드</a>를 보면서 이래저래 정리해봤다.</p>
<h3 id="feature별-폴더-구조">Feature별 폴더 구조</h3>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/f36bf8ee-685f-4193-be1e-109025e66a09/image.png" alt=""></p>
<p>최근에 &lt;클린 아키텍처&gt; 책을 읽고 있는데, 책에서 나온 예시들과 비슷한 구석들이 꽤 있는 것 같다. </p>
<p>위와 같은 구조에서는 <strong>데이터</strong>, <strong>비지니스 로직</strong>, <strong>UI</strong>로 레이어를 각각 나누는 게 핵심이다. 세가지 레이어에서 관심사를 분리해 서로 간에 결합도가 낮아지게 만들어야 한다.</p>
<p>인상적인 지점은 Domain 레이어에서 비지니스 로직에서 필요한 함수들을 추상 클래스로 만들고, 실제 구현부는 Data 레이어의 Repository에서 한다는 점이다. 생각해보면, Data 레이어의 Repository여야 Datasource를 통해 서버나 로컬DB에 접근할 수 있기 때문에 맞는 것 같다. </p>
<p>이 부분은 &lt;클린 아키텍처&gt;에서 강조했던, 좋은 아키텍처는 프레임워크에 종속되지 않는다는 개념과 맞닿는 것 같다. 책에서는 데이터베이스가 바뀌거나 UI 플랫폼이 바뀔 수 있으므로, 이런 프레임워크 등과 관련된 로직은 비지니스 로직과 분리되어 작성하도록 하는데, Riverpod에서 이렇게 분리한 것도 이런 맥락이 아닐까 하는 생각이 든다.</p>
<p>여기서 빠진건 데이터의 구조를 정의하는 Model 부분인데, 이건 피쳐별로 폴더를 만들어서 넣어도 되고, 아니면 위 예시처럼 아예 한 뎁스 바깥에 넣어도 되는 것 같다. Riverpod의 다른 예시들을 보면, 피쳐별로 Model을 정의한 케이스들도 있다. 
개인적으로는 피쳐별로 정의하는 게 깔끔할 것 같다.</p>
<p>사실 &lt;클린 아키텍처&gt;에 나온 예시들이 Java 소스 기반이고 서버쪽 중심이라서 뭔가 와닿지 않았는데, Flutter 와 Riverpod의 예시들을 보다 보니 좀 더 명확하게 이해되는 부분들이 있는 것 같아 신기하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Inputmask 오픈소스에 PR 올리기 (ShadowDOM에서 정상 동작하지 않는 이슈)]]></title>
            <link>https://velog.io/@hgoguma_124/Inputmask-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4%EC%97%90-PR-%EC%98%AC%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Inputmask-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4%EC%97%90-PR-%EC%98%AC%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Tue, 28 Nov 2023 06:08:22 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>현재 진행하고 있는 프로젝트에서 <a href="https://robinherbots.github.io/Inputmask/">Inputmask</a> 오픈소스 라이브러리를 사용해서 마스킹 처리 하는 기능을 개발하게 되었다.
그런데 개발 환경에서는 동작이 잘 되었는데 실제 빌드된 환경에서는 제대로 동작하지 않는 상황이 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/05fac98e-c86f-4277-a899-763de7e2bd51/image.gif" alt=""></p>
<p>위 gif 처럼 일반적인 DOM에서는 휴대폰 번호 마스킹 처리가 정상적인데, Shadow DOM에서는 input에 포커싱 하자마자 포커싱이 이상하게 잡히고 아예 입력이 안됐다.</p>
<br />

<h2 id="원인">원인</h2>
<p>이 프로젝트는 먼저 Vue 기반으로 개발한 후에 빌드된 결과물이 WebComponent로 특정 DOM의 하위에 부착되는 형태로 구성되었다. 
개발 환경에서는 일반 DOM에서 개발했기 때문에 별 이상이 없었지만, 신기하게 빌드 되어서 ShadowDOM 하위에 위치하면 제대로 동작하지 않았다. 
결국 Inputmask 라이브러리 내부에서 ShadowDOM 하위에 있는 특정 타겟을 찾지 못하는 게 아닐까 하는 의심이 들어서 Inputmask 소스를 하나하나 까보기 시작했다.</p>
<p>하나하나 디버깅해본 결과 역시나 내부적으로 마스킹 처리하는 타겟을 찾는 부분이 문제였다.</p>
<pre><code class="language-javascript">
if (input === (input.inputmask.shadowRoot || input.ownerDocument).activeElement) ...
</code></pre>
<p>위 코드 처럼 현재 마스킹 처리하고자 하는 Inputmask 인스턴스와 실제 DOM 타겟이 맞는지 확인하는 코드들이 있었다.
여기서 문제가 된 부분은 input.ownerDocument였다. 내 추측으로는 위 코드는 결국 input 엘리먼트의 최상단 Document안에서 실제 포커싱 된 객체를 찾고자 하는 코드 같은데, input 엘리먼트가 속한 최상단 객체를 찾고자 ownerDocument로 접근 하는 게 문제였다. </p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/ownerDocument">MDN</a>에 따르면 <span style="color:red">ownerDocument</span>는 가장 탑 레벨의 Node 객체를 반환한다. </p>
<p>이 경우 항상 DOM의 가장 최상단인 일반 Document 객체만을 반환하게 되는데, 문제는 ShadowDOM 하위에 있는 엘리먼트에서 ownerDocument로 접근하는 경우에도 실제 해당 엘리먼트가 속한 최상단 DOM(Shadow Root)가 아니라 일반 Document를 반환한다는 점이다. 
ShadowDOM은 일반적인 DOM과 별개의 DOM으로 캡슐화되어 있다. 따라서 최상단 DOM 내부에서는 캡슐화 된 ShadowDOM의 특정 엘리먼트를 찾을 수가 없다. 
따라서 저 코드로 인해 결국 실제 포커스 된 input 엘리먼트를 찾지 못하게 되는 이슈가 발생했고, 그래서 정상적으로 동작하지 않았다.</p>
<p>결국 input.ownerDocument로 특정 input 엘리먼트가 속한 최상단 DOM을 찾고자할 때, input이 일반적인 DOM 하위에 위치한 것 뿐만 아니라 ShadowDOM 하위에 위치할 수 있다는 점까지 신경써서, ShadowDOM에 위치한다면, Shadow Root를 반환할 수 있도록 변경되어야 했다.</p>
<br />

<h2 id="해결">해결</h2>
<p>구글링 결과, <a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode">getRootNode()</a> 라는 API를 찾았다. ownerDocument와 비슷하게 해당 Node의 최상단 DOM을 반환하는 API인데, ShadowDOM인 경우 ShadowDOM의 Root를 반환해준다.</p>
<p>문제 됐던 위 코드를 아래와 같이 바꾸니 빌드된 환경, 즉 Shadow DOM 하위에서도 정상 동작했다.</p>
<pre><code class="language-javascript">
if (input === (input.inputmask.shadowRoot || input.getRootNode()).activeElement) ...
</code></pre>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/d726734a-0727-4903-b5d7-6755d22a5b2f/image.gif" alt=""></p>
<p><a href="https://github.com/RobinHerbots/Inputmask">Inputmask github</a>에서 최신 버전으로 git fork후 코드 수정을 한 다음 빌드 및 테스트까지 한 다음 문제가 없는 것 같아, <a href="https://github.com/RobinHerbots/Inputmask/pull/2753">PR</a>을 올렸다.</p>
<p>PR 올린 후에 2주 정도 기다렸는데, Merge 되지는 않아서 우선 내부 프로젝트에만 변경된 소스가 적용되도록 수정했다. 언젠가 Merge 되길 바라며...</p>
<h2 id="결론">결론</h2>
<p>사실 이렇게 정리하고 보니 별 거 아닌 것 같아 보이지만, 실제로 디버깅해서 원인 파악하는 데까지 이틀 정도 걸렸다. 
그리고 그 과정에서 기타 시행 착오들도 있었다.</p>
<ul>
<li>node_modules에 설치된 패키지를 바라보는 게 아니라 fork 떠서 수정 중인 라이브러리를 바라보게 하려고 pnpm link를 처음 사용해봤다. </li>
<li>빌드된 dist 파일 대신 실제 코드를 바라보게 하려고 package.json을 까보면서 exports 구문이 뭘 의미하는지 새삼 알게 되었다.</li>
<li>마지막에 라이브러리 수정 후에 빌드된 결과물이 제대로 동작하지 않아서 webapck 설정도 까보고 프로젝트 빌드나 테스트 등을 관리하는 툴인 <a href="https://gruntjs.com/">grunt</a>에 대해서도 알게 되었다.</li>
</ul>
<p>이래저래 시행착오를 거치면서 PR 까지 올리니까 뿌듯하다. 별 거 아니지만 새삼 다른 사람이 만들어놓은 오픈 소스 라이브러리를 까보는 게 또 흔한 기회는 아닌 것 같아서 재밌었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vitest로 Vue Component 테스트 코드 작성하기]]></title>
            <link>https://velog.io/@hgoguma_124/Vitest%EB%A1%9C-Vue-Component-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Vitest%EB%A1%9C-Vue-Component-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 12 Jun 2023 01:58:03 GMT</pubDate>
            <description><![CDATA[<h2 id="환경-설정">환경 설정</h2>
<h4 id="1-설치">1. 설치</h4>
<ul>
<li><p>Vite로 Vue + Typescript 기반으로 Vue 환경 셋팅
  <code>pnpm create vue-component-test --template vue-ts</code></p>
</li>
<li><p>Unit Test Framework인 Vitest(<a href="https://vitest.dev/">https://vitest.dev/</a>) 
  <code>pnpm add -D vitest @vitest/ui</code>
  Vitest외에 Jest, Mocha 등의 라이브러리를 사용할 수 있지만 Vite를 기반으로 생성했다보니 Vite와 가장 궁합이 좋을 것 같아서 Vitest를 선택했다.
  Vitest의 ui 라이브러리는 옵셔널이다. 기본적으로 Vitest의 테스트 결과는 터미널로 확인할 수 있는데 ui 옵션을 사용하면 더 편리하게 확인할 수 있다. </p>
</li>
<li><p>Vue Test Utils(<a href="https://test-utils.vuejs.org/">https://test-utils.vuejs.org/</a>)
  Vue Component를 테스트하기 위해 사용되는 라이브러리다.
  <code>pnpm add -D @vue/test-utils</code></p>
</li>
</ul>
<h4 id="2-셋팅">2. 셋팅</h4>
<pre><code class="language-typescript">// vitest.config.ts
import vue from &#39;@vitejs/plugin-vue&#39;
import { fileURLToPath } from &#39;node:url&#39;
import path from &#39;path&#39;
import { defineConfig } from &#39;vitest/config&#39;

export default defineConfig({
  plugins: [vue()],
  // test 코드 내에서 @ alias 사용을 위해 셋팅
  resolve: {
    alias: {
      &#39;@&#39;: path.resolve(__dirname, &#39;./src&#39;),
    },
  },
  test: {
    // 브라우저 환경에서 테스트하는 것을 명시
    environment: &#39;jsdom&#39;,
    root: fileURLToPath(new URL(&#39;./&#39;, import.meta.url)),
  },
})</code></pre>
<p>package.json에 test용 script command 추가</p>
<pre><code>{
  ...
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vue-tsc &amp;&amp; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;test:unit&quot;: &quot;vitest --ui&quot;
  },
  ...
}
</code></pre><h2 id="테스트-코드-작성">테스트 코드 작성</h2>
<h4 id="1-테스트-대상">1. 테스트 대상</h4>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/c71fde2e-0a33-47cb-ac05-4810956e262a/image.png" alt=""></p>
<p>위와 같이 palette 형태의 색상을 선택할 수 있는 Palette 컴포넌트를 만들었다.</p>
<p>Palette 컴포넌트는 다음과 같은 기능을 가진다.</p>
<ol>
<li>hex코드로 작성된 string을 배열을 받아 팔레트 영역을 그린다.</li>
<li>width props라 존재하면 해당 width에 맞춰 팔레트 영역의 너비를 그린다.</li>
<li>v-model로 양방향 바인딩을 제공한다. </li>
<li>Palette에서 특정 컬러 선택시 혹은 v-model로 이미 양방향으로 바인딩된 데이터가 있을 시 선택되었음을 의미하는 dot 형태의 UI가 표출 된다.</li>
</ol>
<p>Palette 컴포넌트의 Vue 코드는 아래와 같다.</p>
<pre><code>// src/components/color-palette/ColorPalette.vue
&lt;script lang=&quot;ts&quot; setup&gt;
import { computed } from &#39;vue&#39;

import type { ColorPaletteProps } from &#39;./types&#39;

const props = withDefaults(defineProps&lt;ColorPaletteProps&gt;(), {
  modelValue: &#39;&#39;,
  palette: undefined,
  width: &#39;160&#39;,
})

const emit = defineEmits([&#39;update:modelValue&#39;])

const containerW = computed(() =&gt; {
  return !props.width.includes(&#39;px&#39;) ? `${props.width}px` : `${props.width}`
})

const paletteUpperCase = (palette: string[]) =&gt; {
  return palette?.map((c: string) =&gt; c.toUpperCase())
}

const onClick = (color: string) =&gt; {
  emit(&#39;update:modelValue&#39;, color)
}
&lt;/script&gt;

&lt;template&gt;
  &lt;div class=&quot;palette&quot; :style=&quot;{ width: containerW }&quot;&gt;
    &lt;div
      v-for=&quot;c in paletteUpperCase(props.palette)&quot;
      class=&quot;palette-item&quot;
      :style=&quot;{ backgroundColor: c }&quot;
      :key=&quot;c&quot;
      @click=&quot;onClick(c)&quot;
    &gt;
      &lt;div
        class=&quot;palette-item__picked&quot;
        v-if=&quot;c === props.modelValue&quot;
        :style=&quot;{ backgroundColor: props.modelValue === &#39;#FFFFFF&#39; ? &#39;#000&#39; : &#39;&#39; }&quot;
      &gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre><pre><code>// src/App.vue
&lt;script setup lang=&quot;ts&quot;&gt;
import { ref } from &#39;vue&#39;
const colors = ref(&#39;&#39;)

const palette = ref([
  &#39;#FFFFFF&#39;,
  &#39;#F6F6F6&#39;,
  &#39;#F1F1F1&#39;
])

&lt;/script&gt;

&lt;template&gt;
 &lt;ColorPalette v-model=&quot;colors&quot; :palette=&quot;palette&quot; width=&quot;200&quot; /&gt;
&lt;/template&gt;
</code></pre><h4 id="2-테스트-시나리오-및-코드">2. 테스트 시나리오 및 코드</h4>
<pre><code class="language-typescript">// src/components/color-palette/ColorPalette.test.ts
import { mount, shallowMount } from &#39;@vue/test-utils&#39;
import tinycolor from &#39;tinycolor2&#39;
import { describe, expect, it } from &#39;vitest&#39;

import ColorPalette from &#39;./ColorPalette.vue&#39;

describe(&#39;ColorPalette.vue&#39;, () =&gt; {
  it(&#39;mounted&#39;, () =&gt; {
    const wrapper = mount(ColorPalette, {
      props: {
        palette: [],
        modelValue: &#39;&#39;,
      },
    })
    expect(wrapper.classes(&#39;palette&#39;)).toBe(true)
  })

  it(&#39;width props에 따라 palette 컨테이너의 width가 결정된다.&#39;, () =&gt; {
    const width = &#39;200&#39;
    const wrapper = mount(ColorPalette, {
      props: {
        modelValue: &#39;&#39;,
        palette: [],
        width,
      },
    })
    const style = wrapper.find(&#39;.palette&#39;).attributes(&#39;style&#39;)
    expect(style?.includes(&#39;width&#39;)).toBe(true)
    expect(style).toContain(`width: ${width}px`)
  })
  const palettes = [&#39;#F5EFE7&#39;, &#39;#D8C4B6&#39;, &#39;#4F709C&#39;]

  it(&#39;palettes props에 따라 palette item이 설정된다.&#39;, () =&gt; {
    const wrapper = mount(ColorPalette, {
      props: {
        palette: palettes,
        modelValue: &#39;&#39;,
      },
    })
    expect(wrapper.findAll(&#39;.palette-item&#39;)).toHaveLength(palettes.length)
    expect(wrapper.findAll(&#39;.palette-item&#39;)[1].attributes(&#39;style&#39;)).toContain(
      `background-color: ${tinycolor(palettes[1]).toRgbString()}`,
    )
  })

  it(&#39;팔레트 선택시 update:modelValue emit이벤트가 발생된다.&#39;, async () =&gt; {
    const wrapper = mount(ColorPalette, {
      props: {
        palette: palettes,
        modelValue: &#39;&#39;,
      },
    })
    await wrapper.find(&#39;.palette-item&#39;).trigger(&#39;click&#39;)
    const emitEvt = wrapper.emitted(&#39;update:modelValue&#39;)
    expect(emitEvt).toHaveLength(1)
    expect(emitEvt[0][0]).toEqual(palettes[0])
  })

  it(&#39;팔레트 modelValue가 업데이트 되었을 시 선택되었음을 의미하는 UI가 활성화 된다.&#39;, async () =&gt; {
    const wrapper = shallowMount(ColorPalette, {
      props: {
        palette: palettes,
        modelValue: &#39;&#39;,
      },
    })
    expect(wrapper.find(&#39;.palette-item__picked&#39;).exists()).toBe(false)
    await wrapper.setProps({ modelValue: palettes[0] })
    expect(wrapper.find(&#39;.palette-item__picked&#39;).exists()).toBe(true)
  })
})

</code></pre>
<h4 id="3-테스트-결과">3. 테스트 결과</h4>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/1e8433a2-9d80-4be7-9e4b-ddf687678336/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/31fbb46f-0a68-477a-8f8b-cf06fe1a1953/image.png" alt=""></p>
<p>테스트를 진행하면 위와 같이 테스트 결과를 얻을 수 있다. ui 옵션을 통해 브라우저에서 전체 테스트 결과와 테스트 케이스에 대한 결과 등을 상세하게 확인할 수 있다. </p>
<h2 id="회고">회고</h2>
<ul>
<li><p>프로젝트 진행하면서 잠깐 시간이 남아서 유닛 테스트를 적용해봤다. 
결과적으로 보면 그냥 테스트 관련 라이브러리 설치하고 적용하고 테스트 코드 작성하고 테스트 진행하면 끝! 으로 아주 심플해보이는데 중간 중간에 잘 몰라서 삽질을 꽤 했다. 
describe, it 과 같은 함수를 이용해서 테스트 코드를 작성하는 법을 잘 몰라서 제대로 된 테스트 코드가 아니라는 에러가 발생했는데 사용한 테스트 라이브러리 이슈인 줄 알고 구글링하는 등.. 이상한 데서 불필요하게 삽질을 했다. 
더불어서 Vue Test Utils을 통해 Vue 컴포넌트의 props를 설정하거나 해당 DOM element의 attribute를 가져오거나 하는 등의 방법도 익숙치 않아서 헤맸던 부분이 있다. </p>
</li>
<li><p>테스트 코드를 어떻게 잘 작성할 수 있을지는 앞으로 더 고민해야 하는 부분이다. 
이번에는 만든 컴포넌트의 UI와 기능을 테스트해봤는데, 테스트 케이스를 정리하는 것부터 고민이었다. Vue 컴포넌트가 DOM에 잘 mount 되는 것만 테스트 하면 되는 건지, props가 잘 전달 되어서 원하는 대로 셋팅 되는 걸 테스트해야 하는 건지, 클릭 등의 이벤트 발생 로직까지 테스트해야 하는 건지 고민이었다. 
그리고 각각의 테스트 코드를 작성하면서 무엇을 테스트가 성공한 것으로 볼 것인지에 대해서도 고민이었다. 작성하면서 UI 오픈소스 라이브러리인 element plus 의 테스트 케이스 등을 참고해봤다. 다음에는 테스트 방법론이나 테스트 케이스, 테스트 코드 작성법 등에 대해서 좀 더 학습해보고 적용해봐야겠다.</p>
</li>
</ul>
<h2 id="코드-확인">코드 확인</h2>
<p>작성한 코드는 아래 git 저장소에서 확인할 수 있다.</p>
<p><a href="https://github.com/hyeleex2/vue-component-test">https://github.com/hyeleex2/vue-component-test</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그래프 자료 구조와 DFS, BFS]]></title>
            <link>https://velog.io/@hgoguma_124/%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0%EC%99%80-DFS-BFS</link>
            <guid>https://velog.io/@hgoguma_124/%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0%EC%99%80-DFS-BFS</guid>
            <pubDate>Sun, 04 Jun 2023 05:55:36 GMT</pubDate>
            <description><![CDATA[<h3 id="그래프란">그래프란?</h3>
<ul>
<li>연결되어 있는 개체 간의 관계를 표현할 수 있는 자료 구조<ul>
<li>어떤 자료나 개념을 표현하는 정점(Vertex, Node)들의 집합과  이들을 연결하는 간선(Edge)들의 집합으로 이루어짐.</li>
</ul>
</li>
</ul>
<h3 id="그래프의-특성과-활용">그래프의 특성과 활용</h3>
<ul>
<li>현실 세계의 사물이나 추상적인 개념 간의 연결 관계 표현</li>
<li>도로망, SNS 지인 관계, 웹 사이트 링크, 네트워크 등에서 활용</li>
<li>부모 자식 관계와 같은 제약 사항이 없기 때문에 훨씬 다양한 구조 표현 가능</li>
</ul>
<h3 id="기타-자료-구조와의-비교">기타 자료 구조와의 비교</h3>
<ul>
<li>관계를 표현 → 연결 리스트</li>
<li>순서를 표현 → 큐, 스택</li>
<li>계층 구조를 표현 → 트리</li>
<li>관계의 방향, 정도(가중치), n:n 등 복잡한 관계 표현은 그래프로 가능</li>
</ul>
<h3 id="관련-용어">관련 용어</h3>
<ul>
<li>undirected graph : 엣지가 방향성을 갖지 않는 그래프</li>
<li>directed graph : 엣지가 방향성을 갖는 그래프</li>
<li>weighted graph : 가중치를 갖는 그래프</li>
<li>degree : edge의 개수<ul>
<li>들어오는 화살표 : in-degree<ul>
<li>나가는 화살표 : out-degree</li>
</ul>
</li>
</ul>
</li>
<li>cycle : 출발과 도착이 같은 경로들의 집합</li>
<li>complete graph : 모든 vertex가 다른 vertex와 adjacent 형태로 연결되어 있는 형태. </li>
</ul>
<h3 id="그래프의-표현">그래프의 표현</h3>
<ol>
<li>연결 리스트
 -연결된 엣지 리스트를 표현
 -엣지의 개수가 많이질 경우 메모리 소요가 큼</li>
<li>행렬
 -서로 간에 연결되어 있는지 여부를 2차원 배열로 구현
 -불필요하게 메모리를 낭비하게 되는 단점이 있음
<img src="https://velog.velcdn.com/images/hgoguma_124/post/154563d4-02a1-4b98-a946-b0f6ba2bb661/image.png" alt=""></li>
</ol>
<ul>
<li>dense graph(엣지의 개수가 많음)인 경우 2차원 배열 활용, sparse graph(엣지의 개수가 적음)인 경우 연결 리스트를 활용</li>
</ul>
<h3 id="그래프의-탐색-알고리즘">그래프의 탐색 알고리즘</h3>
<h4 id="탐색-알고리즘">탐색 알고리즘</h4>
<ul>
<li>하나의 정점에서 시작하여 그래프의 모든 정점을 방문하는 알고리즘</li>
<li>트리의 순회와 비슷 → 트리보다 구조 복잡(트리는 계층 구조가 있기 때문에 더 간단)</li>
</ul>
<h4 id="dfsdepth-first-search-깊이-우선-탐색">DFS(Depth-First Search, 깊이 우선 탐색)</h4>
<ul>
<li><p>미로 찾기와 비슷. 시작점에서 가장 깊이 들어갈 수 있는 곳 까지 탐색한 후 길이 막히면 갈림길이 있었던 곳으로 다시 돌아와서 시작하는 형태</p>
</li>
<li><p><strong>스택</strong> 또는 <strong>재귀 함수</strong>를 활용하여 구현</p>
</li>
<li><p>구현 방법(스택)</p>
<ol>
<li>방문한 노드를 관리하는 배열과 스택 배열로 관리. 스택은 막다른 길에 도달했을 때 다시 되돌아오기 위해 사용.</li>
<li>먼저 시작점을 방문했다고 배열에 저장 후 해당 노드를 스택에 저장. 그 다음으로 이동한 노드에서 방문할 수 있는 노드가 있는지 확인 후 방문 가능하면 방문한 노드 배열에 저장하고 해당 노드를 스택 배열에 저장. 만약 방문할 수 있는 노드가 없는 경우 스택에서 해당 노드를 pop 하고, 이전 노드로 백트래킹함.</li>
<li>스택이 빌 때까지 2 과정을 반복한 후 스택이 비어 있으면 종료</li>
</ol>
</li>
</ul>
<h4 id="bfsbreath-first-search-너비-우선-탐색">BFS(Breath-First Search, 너비 우선 탐색)</h4>
<ul>
<li><p>동심원 모양으로 탐색함.</p>
</li>
<li><p><strong>큐</strong>를 사용하여 구현</p>
</li>
<li><p>구현 방법</p>
<ol>
<li>시작점을 큐에 넣고 시작.</li>
<li>시작점에서 인접한 노드들을 큐에 넣고 시작점은 deque 하고 방문 처리 함. 큐의 순서대로 하나씩 방문 처리 하며 deque함. 방문시에 아직 방문하지 않은 인접한 노드가 있으면 새로 큐에 넣음. </li>
<li>큐가 빌 때까지 2를 반복하고 큐가 비어 있으면 종료</li>
</ol>
</li>
<li><p>활용 영역</p>
<pre><code>  1. 검색 엔진의 크롤링
  2. 최단 거리 구하기
  3. 웹 사이트의 소셜 네트워크
  4. 추천 시스템
  5. GPS 네비게이션 시스템</code></pre></li>
</ul>
<h2 id="dfs-vs-bfs">DFS vs BFS</h2>
<ul>
<li>메모리 사용량 : DFS &lt; BFS<ul>
<li>BFS의 경우 인접한 노드들을 모두 큐에 담기 때문에 불필요한 메모리를 잡아 먹게 됨. 인접한 노드들을 모두 큐에 넣는 과정에서 오버플로우가 발생할 수 있음. </li>
</ul>
</li>
<li>속도 : DFS &lt; BFS</li>
<li>언제 무엇을 사용 해야 하는가?<ul>
<li>DFS, BFS의 시간 복잡도는 같기 때문에 중 뭐가 더 낫다고 볼 순 없고 상황에 맞게 사용하면 됨. <ul>
<li>그래프의 모든 정점을 방문해야 한다면 BFS, DFS 둘 다 상관 없음</li>
</ul>
</li>
<li>그래프가 정말 크다면 DFS가 낫고 최단 거리 계신 시에는 BFS가 나음.</li>
<li>경로의 특징을 저장하는 등의 로직이 더 필요한 경우 DFS 사용</li>
</ul>
</li>
</ul>
<h4 id="출처">출처</h4>
<ul>
<li>알고리즘과 문제해결 / 고려사이버대학교, 임철홍</li>
<li>알고리즘 / 인천대학교, 전경구</li>
<li><a href="https://www.geeksforgeeks.org/applications-of-breadth-first-traversal/">https://www.geeksforgeeks.org/applications-of-breadth-first-traversal/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue.js의 새로운 상태 관리 라이브러리 Pinia]]></title>
            <link>https://velog.io/@hgoguma_124/Vue.js%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Pinia</link>
            <guid>https://velog.io/@hgoguma_124/Vue.js%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Pinia</guid>
            <pubDate>Wed, 19 Oct 2022 08:39:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>현재 Vue2를 기반으로 만들어진 기존 레거시 프로젝트를 Vue3로 마이그레이션 하는 작업을 진행중이다. 이 과정에서 자연스럽게 기존에 사용하던 상태 관리 라이브러리도 Vuex에서 Pinia로 변경하는 작업을 진행중이다. 그 과정에서 Pinia의 핵심을 정리해보고 Vuex와 간단하게 비교해보고자 한다.</p>
</blockquote>
<br />

<h2 id="pinia의-등장">Pinia의 등장</h2>
<p>Pinia는 Vue3의 핵심인 Composition API에 적합한 형태로 만들어진 Vue의 상태 관리 라이브러리이다. 
기존에 Vue.js의 상태 관리 라이브러리 하면 자연스레 Vuex였는데, 이제 공식적으로 추천하는 상태 관리 라이브러리는 Pinia로 변경됐다. 
이제는 Vue3가 디폴트가 된 것 처럼 비슷한 컨셉을 공유하는 Pinia가 공식적인 상태 관리 라이브러리가 된 것은 어쩌면 당연한 흐름인 것 같다.
Vuex와 Pinia 중 어떤 걸 사용해야 할지 고민이 들텐데, 여기저기 공식 문서를 본 결과 <strong>기존 Vue2 사용자라면 Vuex를 사용하고, Vue3 사용자라면 Pinia를 사용하는 게 궁합이 좋을 것 같다.</strong>
물론 Pinia가 Vue3만 서포트하는 건 아니고 당연히 Vue2와도 함께 사용할 수 있다. 하지만 전반적인 컨셉을 유지하고 코드를 통일성 있게 작성하기 위해서는 궁합이 잘 맞는 걸 선택하는 게 베스트일 것 같다.</p>
<br />


<h2 id="pinia의-강점">Pinia의 강점</h2>
<p>공식 문서를 보고 느낀 Pinia의 강점은 크게 다음과 같다.</p>
<p><strong>1. Intuitive</strong>
     Vue3 기반의 <em>setup()</em> 함수를 사용하는데 친숙하다면 코드 작성이 보다 직관적으로 느껴진다. </p>
<p><strong>2. Type Safe</strong>
    Typescript 기반으로 Type에 대한 안정성이 보장된다.</p>
<p><strong>3. Extensibility</strong>
    localstorage 등을 사용해 확장할 수 있다.</p>
<p>이외에도 Devtool 지원, 적은 용량, 모듈화 등의 장점이 돋보인다.</p>
<br />

<h2 id="핵심-컨셉">핵심 컨셉</h2>
<h3 id="store">Store</h3>
<p>Store라는 단위 안에서 상태를 관리한다는 핵심 컨셉은 기존 Vuex와 동일하다.
<em>defineStore()</em> 메소드를 통해 해당 store의 유니크한 id와 옵션(state, getters, actions)를 정의한다. 이 메소드로 만들어진 Store 객체는 use 키워드 + id + Store 키워드의 조합으로 이름을 붙여 export 하는 방식이 권장된다. 
Pinia는 기본적으로 Vue2(Options API)와 Vue3(Setup API) 둘 다 지원하기 때문에 각 API의 문법 대로 Store를 만들 수 있다.</p>
<h4 id="options-api">Options API</h4>
<pre><code>&lt;script&gt;
import { defineStore } from &#39;pinia&#39;

export const useCounterStore = defineStore(&#39;counter&#39;, {
  state: () =&gt; ({ count: 0, name: &#39;Eduardo&#39; }),
  getters: {
    doubleCount: (state) =&gt; state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
&lt;/script&gt;</code></pre><p>코드에서 알 수 있듯 Options API로 Store를 정의하는 방식은 기존 Vuex와 동일하다. 
다만 Vuex와 비교했을 때 state를 실제 변경하는 mutations가 따로 없다. actions에서 mutations, 즉 state 변경까지 함께 작성된다.</p>
<h4 id="setup-api">Setup API</h4>
<pre><code>&lt;script&gt;
export const useCounterStore = defineStore(&#39;counter&#39;, () =&gt; {
  const count = ref(0)
  const name = ref(&#39;Eduardo&#39;)
  const doubleCount = computed(() =&gt; count.value * 2)
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})
&lt;/script&gt;</code></pre><p>Setup API는 Vue3의 Setup API와 비슷하게 생겼다. 
Options API 와 비교했을 때 다음과 같은 형식으로 state, getters, actions를 정의할 수 있다. </p>
<pre><code>1. _ref()_ =&gt; state 정의
2. _computed()_ =&gt; getters 정의
3. _function()_ =&gt; actions 정의</code></pre><p>그리고 정의한 state, getters, actions 함수를 마지막에 return 해주면 된다.</p>
<p>이렇게 만들어진 Store는 아래 코드 처럼 Vue 컴포넌트의 setup 함수 등에서 사용할 수 있다.</p>
<pre><code>&lt;script&gt;
import { useCounterStore } from &#39;~/store/counter&#39;

export default defineComponent({
  setup() {
      // counter store 가져오기
    const store = useCounterStore()
    const { name, doubleCount } = store

    return {
      name,
      doubleCount,
    }
  },
})
&lt;/script&gt;</code></pre><h4 id="options-api-vs-setup-api">Options API vs Setup API</h4>
<p>공식 문서에 따르면, 두 API 문법 중 친숙한 형태의 것을 사용하면 된다고 나와있다. 아직 Vue2에 익숙하다면 Options API 를 사용하는 게 좋아보이고 Vue3에 이미 친숙하다면 Setup API를 사용하면 좋을 것 같다.
개인적으로 두 가지 모두 작성해보고 느낀 바로는 Setup API를 사용하는 것이 더 편했다. 그 이유는 다음과 같다.</p>
<ul>
<li><p>getters, actions 등에서 state 접근시 state 키워드를 사용하지 않아도 된다. 
Options API의 경우 this.state 등으로 접근해야 하는데, Setup API의 경우 그냥 state 그 자체에 바로 접근이 가능하다.</p>
</li>
<li><p>다른 Store 사용이 용이하다. 
A 라는 Store에서 B라는 Store의 action을 실행해야 하는 경우를 가정해보자. Options API의 경우 다음과 같이 코드를 작성해야 한다.</p>
</li>
</ul>
<pre><code>&lt;script&gt;
import { useAStore } from &#39;~/store/a&#39;
    actions: {
        actionA () {
            const aStore = useAStore()
            store.actionA()
          },
          actionB () {
            const aStore = useAStore()
            store.actionB()
          },
    }
&lt;/script&gt;        </code></pre><p>useAStore()라는 코드를 두번 작성해야 해서 귀찮다...</p>
<pre><code>&lt;script&gt;
import { useAStore } from &#39;~/store/a&#39;
    const aStore = useAStore()

    const actionA = () =&gt; {
        aStore.actionA()
    }

    const actionB = () =&gt; {
        aStore.actionB()
    }
&lt;/script&gt;</code></pre><p>Setup API를 사용하면 깔끔하게 한번만 작성하면 된다.</p>
<br />

<h3 id="state">State</h3>
<h4 id="state-셋팅">State 셋팅</h4>
<p>State의 경우 Typescript를 적용해 State의 Type을 설정할 수 있다.</p>
<pre><code>&lt;script&gt;
interface UserInfo {
  name: string
  age: number
}
// Options API
export const useUserStore = defineStore(&#39;user&#39;, {
  state: () =&gt; {
    return {
        userList: [] as UserInfo[],
          user: null as UserInfo | null,
    }
  },
})

// Setup API
export const useUserStore = defineStore(&#39;user&#39;, () =&gt; {
  const userList = ref&lt;UserInfo[]&gt;([])
  const user = ref&lt;UserInfo || null &gt;(null)
})
&lt;/script&gt;</code></pre><h4 id="state-mutation">State Mutation</h4>
<p>State의 값을 바꾸는 Mutation의 방식은 두가지로 나뉜다.</p>
<pre><code>1. 직접적인 변경
2. $patch() 메소드를 이용한 변경
   $patch()를 사용하면 여러 State를 한번에 변경할 수 있고 array와 같은 컬렉션의 데이터를 변경할 때 용이</code></pre><pre><code>&lt;script&gt;
// 1. 직접적인 변경
store.count++

// 2. $patch 메소드를 사용한 mutation
// 2-1. 여러 state를 한번에 muation 하기
// $patch 메소드의 파라미터로 변경할 state 객체를 받아 mutation 할 수 있음
store.$patch({
  count: store.count + 1,
  age: 120,
  name: &#39;DIO&#39;,
})

// 2-2. array 형태의 State mutation 
// function을 파라미터로 넘겨서 mutation 할 수 있음
cartStore.$patch((state) =&gt; {
  state.items.push({ name: &#39;shoes&#39;, quantity: 1 })
  state.hasChanged = true
})
&lt;/script&gt;</code></pre><br />

<h3 id="getters">Getters</h3>
<p>getters는 기존 Vuex와 마찬가지로 주로 state의 계산된 값을 반환하는데 사용된다.
이 외에도 다른 getters의 값을 가져와 재가공하는 경우도 있는데, 이 경우에는 this를 통해 store의 전체 instance의 getters 값을 가져올 수 있다. 만약 이렇게 다른 getters의 값을 가져오는 getters를 만드는 경우, 해당 getters의 return type을 명시해줘야 한다.  </p>
<pre><code>&lt;script&gt;

// Options API
export const useCounterStore = defineStore(&#39;counter&#39;, {
  state: () =&gt; ({
    count: 0,
  }),
  getters: {
      // state의 계산된 값 반환
    doubleCount: (state) =&gt; state.count * 2,

    // getters를 가공해 반환하는 getters 정의
    doublePlusOne(): number {
      // getters의 반환 값 typing 필요
      return this.doubleCount + 1
    },
  },
})

// Setup API

export const useCounterStore = defineStore(&#39;counter&#39;, () =&gt; {
    // state
    const count = ref&lt;number&gt;(0);
    // getters 
    const doubleCount = computed(() =&gt; count.value * 2);
    const doublePlusOne = computed(() =&gt; doubleCount.value + 1);
})

&lt;/script&gt;</code></pre><p>Vuex와 마찬가지로 getters에서 인자를 받을 수도 있다. 이 경우 함수를 다시 return 하는 형태로 만들면 된다.</p>
<pre><code>&lt;script&gt;
export const useStore = defineStore(&#39;user&#39;, {
  getters: {
      // getters 호출시 userId를 파라미터로 넘기며 호출 할 수 있다.
    getUserById: (state) =&gt; {
      return (userId) =&gt; state.users.find((user) =&gt; user.id === userId)
    },
  },
})

&lt;/script&gt;</code></pre><br />

<h3 id="actions">Actions</h3>
<p>actions에서는 state와 관련된 비지니스 로직을 구현하고 state를 변경한다.
기존 Vuex에서는 actions에서 비지니스 로직만을 담당하고 실제 state를 변경하는 것은 commit을 통해 mutations에 위임했다. 이와 달리 Pinia는 mutations를 굳이 거치지 않고 actions에서 바로 state를 변경할 수 있다</p>
<p>Vuex와 마찬가지로 actions는 await/async를 통해 비동기 처리도 가능하다.</p>
<pre><code>&lt;script&gt;

// Options API
export const useUsers = defineStore(&#39;users&#39;, {
  state: () =&gt; ({
    userData: null,
  }),

  actions: {
    async loginUser(id, password) {
      try {
        this.userData = await api.post({ id, password })
        alert(`Welcome back ${this.userData.name}!`)
      } catch (error) {
        alert(error)
        return error
      }
    },
  },
})
&lt;/script&gt;</code></pre><p>위와 같이 로그인 API를 호출해 서버에서 받아온 데이터로 userData의 state를 변경하고 성공/실패 여부에 따라 alert을 띄우는 비지니스 로직을 설정할 수 있다.</p>
<br/>

<h2 id="nuxt3과-함께-사용하기">Nuxt3과 함께 사용하기</h2>
<p>Nuxt.js 프레임워크에서 Pinia를 사용하기 위해서는 <a href="https://www.npmjs.com/package/@pinia/nuxt">@pinia/nuxt</a> 모듈을 설치하면 된다.</p>
<p>모듈 설치 후 nuxt.config.ts 파일에 모듈을 등록하고 옵션을 설정하면 된다.</p>
<pre><code>&lt;script&gt;
export default defineNuxtConfig({
  modules: [
    &#39;@pinia/nuxt&#39;,
    {
        autoImports: [
          // `defineStore` 를 자동으로 import 해줌
          // import { defineStore } from &#39;pinia&#39; 를 하지 않아도 알아서 import 해줌
          &#39;defineStore&#39;,
        ],
    },
  ],
})
&lt;/&lt;script&gt;</code></pre><p>만약 Nuxt3에서 Pinia 인스턴스에 접근하고 싶다면 다음과 같이 하면 된다.</p>
<pre><code>1. usePinia()
    @pinia/nuxt 모듈을 사용하면 usePinia() 메소드가 자동으로 import 되어 pinia 인스턴스에 접근할 수 있다.
2. useNuxtApp()
    useNuxtApp()에서 $pinia를 통해 pinia 인스턴스에 접근할 수 있다.</code></pre><pre><code>&lt;script&gt;

export default defineComponent({
    setup () {
        const pinia = usePinia()
        const { $pinia } = useNuxtApp()
    }
    return {
        pinia,
        $pinia
    }
})

&lt;/script&gt;</code></pre><br/>

<h2 id="module-대신-store">Module 대신 Store</h2>
<p>기존에 Vuex에서는 하나의 Store를 Module 단위로 쪼개 관리하는 개념이 존재했다.</p>
<pre><code>&lt;script&gt;
const moduleA = {
  state: () =&gt; ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () =&gt; ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

&lt;/script&gt;
</code></pre><p>위와 같이 A, B라는 모듈을 만들고 두개의 모듈을 하나의 Store로 묶어서 만들 수 있다. 또한 _registerModule()_을 통해 다이나믹하게 모듈을 만들 수 있었다.</p>
<p>Pinia에서는 Module이라는 개념이 사라지고 Store만 남게 된다. 즉, Module을 통해 Store를 만드는 것이 아닌 Store 그 자체를 만드는 것이다. Pinia에서는 id라는 유니크 값을 통해 Store를 생성하기 때문에 Vuex에서 namespaced 된 모듈을 생성했던 것 처럼 namespaced 된, 즉 독립된 Store를 생성할 수 있다고 설명한다.</p>
<p>따라서 Pinia 공식 문서에 따르면 Vuex와 비교했을 때 아래와 같은 디렉토리 구조를 갖게 된다.</p>
<pre><code># Vuex example (namespaced 모듈 사용)
src
└── store
    ├── index.js           # Vuex 초기화 및 modules import
    └── modules
        ├── module1.js     # &#39;module1&#39; namespace
        └── nested
            ├── index.js   # &#39;nested&#39; namespace, imports module2 &amp; module3
            ├── module2.js # &#39;nested/module2&#39; namespace
            └── module3.js # &#39;nested/module3&#39; namespace

# Pinia equivalent, note ids match previous namespaces
src
└── stores
    ├── index.js          # (선택) Pinia 초기화. store가 자동 import되어 따로 import 할 필요 없음
    ├── module1.js        # &#39;module1&#39; id
    ├── nested-module2.js # &#39;nestedModule2&#39; id
    ├── nested-module3.js # &#39;nestedModule3&#39; id
    └── nested.js         # &#39;nested&#39; id</code></pre><p>Vuex와 비교했을 때 Pinia는 간결한 구조를 가지지만 namespaced 되어 독립된 store 객체를 가진다는 컨셉은 유효하다. </p>
<p>이 부분의 경우 학습하면서 가장 헷갈렸다. 기존 레거시 프로젝트에서는 <em>registerModule()</em> 을 통해 동적으로 module을 생성하는 로직이 있어서 Pinia에서도 비슷한 방식으로 풀려고 했는데, 애초에 Module이라는 개념 자체가 부재했다. Pinia의 공식 문서를 보고 구글링해봐도 Store를 동적으로 생성하고 이를 다루는 예시가 딱히 없어서 어떻게 풀어내야 할지 난제다. 이 부분은 좀 더 조사가 필요할 듯 하다.</p>
<br />

<blockquote>
<p>2022-10-24 <strong>Dynamic Store 생성</strong> 추가</p>
</blockquote>
<h2 id="동적-store-생성-및-삭제">동적 Store 생성 및 삭제</h2>
<p>동적으로 Store를 생성하는 로직을 만들어봤다.
Nuxt3 구조에서 composables 디렉토리 하위에 동적으로 Store를 생성하는 로직을 아래와 같이 작성했다. </p>
<pre><code>&lt;script&gt;
// composables/useDynamicStore.ts
export default function (id: string) {
  return defineStore(id, () =&gt; {
    return {
        sates,
        getters,
        actions,
    }
  })
}

&lt;/scirpt&gt;</code></pre><p>useDynamicStore는 id를 파라미터로 받아서 동적으로 store를 생성 후 반환한다.</p>
<pre><code>&lt;script&gt;
// middleware/store.ts
useDynamicStores(id)()
&lt;/script&gt;</code></pre><p>위 코드 처럼 Store를 동적으로 생성하고자 할 때 useDynamicStore를 호출하기만 하면 된다.</p>
<p>동적으로 만들어진 Store를 삭제하는 로직은 다음과 같다.</p>
<pre><code>&lt;script&gt;
// composables/useDisposeStore.ts

import { Store, Pinia, getActivePinia } from &#39;pinia&#39;

interface ExtendedPinia extends Pinia {
  _s: Map&lt;string, Store&gt;
}

export default function (id: string) {
  if (!id) return

  // Pinia에서 제공하는 getActivePinia() 메소드로 pinia 인스턴스 가져오기
  const activePinia = getActivePinia() as ExtendedPinia
  if (!activePinia) {
    throw new Error(&#39;There is no active Pinia instance&#39;)
  }

  // id 값이 일치하는 Store 가져오기
  const targetStore = activePinia._s?.get(id)
  if (targetStore) targetStore.$dispose()
}

&lt;/scirpt&gt;</code></pre><p>특정 id를 갖는 Store를 찾아 $dispose() 메소드를 통해 삭제하면 된다.
여기서 포인트는 getActivePinia() 메소드로 반환되는 Pinia 객체(proxy)의 _s 키 값에 Store들이 Map 객체 형태로 들어있다는 점이다. 이 객체에서 id 값이 일치하는 Store를 찾아 $dispose 하면 된다.</p>
<p>참고
<a href="https://lobotuerto.com/notes/til-how-to-reset-all-stores-in-pinia">https://lobotuerto.com/notes/til-how-to-reset-all-stores-in-pinia</a></p>
<br />

<h2 id="결론">결론</h2>
<p>Vue3로 마이그레이션 하는 과정에서 느낀바로는 Pinia가 확실히 Vue3와 궁합이 좋은 듯 하다. 
일단 형식도 setup 형태로 비슷하고 Vuex에 비해 코드도 훨씬 간결해지며 익숙해지면 가독성도 높다. 어쨌든 이제 Vuex의 버전업은 딱히 없을 것 같고 공식적으로도 Pinia를 권장하고 있으니 익숙해지는 과정을 거치면 될 것 같다.</p>
<p>번외로 Vue3, Nuxt3로 마이그레이션 하는 과정에서 참고할 수 있는 것들이 공식 문서 외에는 딱히 없어서 좀 외롭다... Vue 진영 자체가 React 보다 훨씬 작아서(특히 한국에서) 원래부터 외로웠지만 Vue3는 정말 더 외로운 것 같다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue 3.0과 Composition API]]></title>
            <link>https://velog.io/@hgoguma_124/Vue-3.0%EA%B3%BC-Composition-API</link>
            <guid>https://velog.io/@hgoguma_124/Vue-3.0%EA%B3%BC-Composition-API</guid>
            <pubDate>Tue, 19 Jul 2022 08:21:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지금까지 회사에서 주로 사용해본 Vue.js 버전은 2.0 기준이었다.
2020년 9월 Vue 3.0이 등장했고 드디어 2022년 2월 Vue.js의 기본 버전이 3버전으로 설정되었다.
이번에 회사에서 Vue 3.0를 주제로 발표 준비를 하면서 짧게 학습한 Vue 3.0의 컨셉과 API 등을 정리해보려 한다.</p>
</blockquote>
<br>

<h3 id="vue-30의-특징">Vue 3.0의 특징</h3>
<ol>
<li>가상 DOM 최적화</li>
<li>트리 쉐이킹 강화</li>
<li>Composition API의 공식화</li>
<li>Typescript 지원 강화</li>
<li>Teleport</li>
<li>Suspense</li>
<li>그 외 템플릿 표현식 수정 및 추가 제공, class 및 style 바인딩 변경 등등</li>
</ol>
<p>Vue 공식 블로그에 따르면 이런 특징들로 인해 Vue 2.0과 비교했을 때 번들 사이즈가 41% 줄어들었고 렌더링이 55%가 빨라졌고 메모리 사용이 54% 줄어 들었다고 한다.</p>
<p>결론적으로 이전보다 업그레이드 되어서 퍼포먼스가 향상되었다는 것인데, 사실상 핵심은 React.js의 변화에 발맞춰 변경된 것이라고 볼 수 있다.</p>
<h3 id="react의-변화">React의 변화</h3>
<p>그렇다면 React.js가 어떻게 변화했길래 Vue.js가 이에 발맞춰서 버전까지 바꾼걸까?
React.js 는 이미 2019년 6월에 공식적으로 Hook을 도입했고 함수형 컴포넌트로 개발하기를 권장하고 있다. 
이러한 흐름은 결국 기존의 클래스 컴포넌트가 가진 단점(재사용성이 좋지 않고 가독성이 좋지 않으며 코드 길이가 길다 등등)을 극복하고 Hook을 통해 로직의 재사용성을 높이고 유지 보수가 용이하게 만들기 위한 것이다. 
이런 맥락에서 Vue 3.0은 기존의 React Hook의 장점은 가져가되 단점은 보완하는 방식으로 만들어진 것 같다. 
결국 Composition API를 기반으로 한 함수형 컴포넌트가 Vue 3.0의 핵심인 것 같다.</p>
<h3 id="composition-api">Composition API</h3>
<p>React의 Hook의 역할을 대신하는 API로, Reactivity(반응성)과 컴포넌트의 라이프 사이클을 관리하고 의존성을 주입하는 등의 역할을 한다.
Composition API를 통해 기존의 Options API에서 관리하던 data, props, computed, watch, methods, 라이프 사이클 등을 setup 함수 내에서 한번에 관리할 수 있다. 또한 관심사와 로직을 분리해서 작성할 수 있기 때문에 공통되는 로직의 경우 하나의 파일에서 import 해서 재사용할 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/28ade691-947a-4008-a0c8-432617b6eb35/image.png" alt=""></p>
<p>공식 문서에 따르면 위의 코드 화면 처럼 Composition API를 사용하면 Options API 보다 코드 양도 줄고 가독성도 높다. </p>
<h4 id="composition-api의-이점">Composition API의 이점</h4>
<ol>
<li>데이터, 로직 목적별로 관심사를 분리할 수 있음</li>
<li>불필요한 코드양을 줄여 가독성을 높임</li>
<li>반복되는 코드들을 한 곳에서 관리해 재사용성이 높아지고 유지 보수 용이</li>
<li>Typescript와 함께 사용하면 이점 극대화</li>
<li>React Hook의 단점 보완
 (컴포넌트가 변경될 때 마다 함수를 재실행하는 React와 달리 Composition API는 setup 함수가 실행될 때, 즉 컴포넌트가 생성될 때 최초 한 번 실행된다. 따라서 불필요한 렌더링을 줄일 수 있다.)</li>
</ol>
<h4 id="composition-api의-setup">Composition API의 setup()</h4>
<p>setup() 함수는 Composition API의 시작점으로 state로 관리할 반응성, 라이프 사이클 훅, props 등을 반환한다.</p>
<h4 id="composition-api의-state-관리">Composition API의 State 관리</h4>
<p>기존의 Options API에서는 아래와 같이 state를 data 객체로 묶어 반환했다.</p>
<pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;span&gt;{{ name }}&lt;/span&gt;
    &lt;span&gt;{{ count }}&lt;/span&gt;
    &lt;button @click=&quot;count++&quot;&gt;카운트 증가&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
    data () {
        return {
            count: 0,
            name: &#39;이름&#39;
        }
    }
}
&lt;/script&gt;</code></pre><p>Composition API에서는 state 두가지 방식으로 관리한다. </p>
<ol>
<li>ref()</li>
</ol>
<ul>
<li>Options API의 data와 비슷하다고 보면 된다. state와 변경 가능한 객체를 반환해 실제 값을 변경하고자 한다면 .value라는 속성으로 접근해 변경할 수 있다.</li>
</ul>
<ol start="2">
<li>reactive()</li>
</ol>
<ul>
<li>원시형 데이터 타입(String, number)에는 반응성을 가지지 않고 주로 객체, 배열에 사용</li>
</ul>
<p>주로 ref()를 사용하는 걸 추천하는 듯 한데 두 API의 차이점에 대해서는 나중에 한번 더 학습해봐야겠다.</p>
<p>아래는 ref와 reactive를 사용해 state를 관리한 예시이다.</p>
<pre><code>&lt;script&gt;
import { ref, reactive } from &#39;vue&#39;
export default {
  setup() {
      const count = ref(0);
    const obj = reactive({ name: &#39;&#39; });
    obj.name = &#39;고구마&#39;;
    return {
        count,
          ...obj
    }
  }
}
&lt;/script&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;span&gt;{{ name }}&lt;/span&gt;
    &lt;span&gt;{{ count }}&lt;/span&gt;
    &lt;button @click=&quot;count++&quot;&gt;카운트 증가&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><h4 id="composition-api의-라이프-사이클">Composition API의 라이프 사이클</h4>
<p>라이프 사이클은 Options API와 큰 틀에서 보면 크게 다르지 않다.
라이프 사이클은 크게 4가지로 나뉜다.</p>
<pre><code>1. 컴포넌트 생성 단계
2. 실제 DOM에 마운트 되는 단계
3. state가 변경되는 단계 
4. 컴포넌트가 DOM에서 사라지는 단계</code></pre><p>Options API와 Composition API의 라이프 사이클 훅은 다음과 같다.
<img src="https://velog.velcdn.com/images/hgoguma_124/post/978ae2ce-69ce-48a6-a2e2-c3e82d2dfe67/image.png" alt=""></p>
<p>만약 Vue2를 사용한다면 컴포넌트가 DOM에서 사라지기 전 호출되는 beforeDestroy() 와 DOM에서 사라지고 나서 호출되는 destroyed() 훅이 beforeUnmount(), unmounted()로 변경된 것을 볼 수 있다.</p>
<p>약간 헷갈릴 수 있는 점은 setup()의 경우 컴포넌트 생성 단계에 해당하는 beforeCreate(), created() 라이프 사이클 훅이 없다는 점이다.
공식 문서에 따르면 setup() 함수가 beforeCreate와 created 라이프 사이클 훅 사이에 실행되므로 해당 라이프 사이클에서 작성할 로직들을 setup() 함수 안에 정의하면 된다.</p>
<h4 id="script-setup으로-composition-api-쓰기"><code>&lt;script setup&gt;</code>으로 Composition API 쓰기</h4>
<p>일단 여기까지 보면 기존의 Vue2 보다 코드양이 훨씬 적어진 걸 볼 수 있다. 기존의 Vue2는 class 컴포넌트 기반인데다가 data, props, computed, methods를 각각 객체로 한땀한땀 정의해야만 해서 보일러 플레이트성 코드가 길다는 단점이 있었다.
그런데 Vue 3.0은 여기서 더 나아가 더 짧고 간단하게 Composition API를 사용할 수 있는 방법을 소개한다.
기존에 사용하던 <code>&lt;script&gt;</code> 태그 대신 <code>&lt;script setup&gt;</code> 을 사용하면 굳이 setup 함수를 선언하지 않아도 알아서 setup 함수 형태로 반환해준다. 누구나 쉽고 간단하게 개발할 수 있게 만든다는 Vue의 철학이 엿보인다.</p>
<p>아래는 부모 컴포넌트로부터 props를 받고 부모 컴포넌트에 emit 하는 형태의 자식 컴포넌트 코드이다.</p>
<pre><code>&lt;script setup lang=&quot;ts&quot;&gt;
import { ref, computed, defineProps, defineEmits } from &#39;vue&#39;
interface Props {
  name: string
  birth: string
}
interface EmitProps {
  (e: &#39;edit&#39;, name: string): void
}
const props = defineProps&lt;Props&gt;()
const emit = defineEmits&lt;EmitProps&gt;()

const newName = ref(&#39;&#39;)
const age = computed(() =&gt; {
  return props.birth ? 2022 - Number(props.birth) : &#39;&#39;
})
const edit = () =&gt; {
  emit(&#39;edit&#39;, newName.value)
}
&lt;/script&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;div&gt;
      &lt;p&gt; 이름 : {{ name }}&lt;/p&gt;
      &lt;p&gt; 연도 : {{ birth }}&lt;/p&gt;
      &lt;p&gt; 나이 : {{ age }}&lt;/p&gt;
    &lt;/div&gt;
    &lt;input v-model=&quot;newName&quot; label=&quot;새로운 이름&quot; /&gt;
    &lt;btn @click=&quot;edit&quot;&gt;수정&lt;/btn&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>setup() 함수를 따로 정의하고 사용하는 값들을 return 할 필요가 없어서 코드가 보다 간결하다.
또한 props 와 emit 도 typescrip로 정의한 후 defineProps(), defineEmit() 등의 API로 접근 가능해서 가독성도 좋다.</p>
<h3 id="결론">결론</h3>
<p>미루던 Vue 3.0 학습을 사내 발표를 계기로 공부하게 되었는데 생각보다 실무에 적용하면 좋은 장점들이 많이 보였다. 불필요한 코드양도 줄일 수 있고 가독성도 높아서 복잡한 로직을 이해하고 구현하는 데 효과적일 것 같다.
특히 Typescript와 함께 쓰면 그 이점이 더 배가 될 것 같다.
결국 형태는 다르지만 Vue 3.0의 본질은 React Hook인 듯 하다. 공식 문서에서도 React Hook에 영감을 받아 Composition API를 만들었다고도 하니 뭐..
과연 Vue에서 주장하는대로 React 보다 퍼포먼스가 더 좋을지는 실제로 프로젝트를 진행해봐야 판단할 수 있을 것 같다.</p>
<h3 id="참고">참고</h3>
<p><a href="https://vuejs.org/guide/extras/composition-api-faq.html#relationship-with-class-api">https://vuejs.org/guide/extras/composition-api-faq.html#relationship-with-class-api</a>
<a href="https://joshua1988.github.io/web-development/vuejs/vue3-as-default/">https://joshua1988.github.io/web-development/vuejs/vue3-as-default/</a>
<a href="https://www.youtube.com/watch?v=Hr_Rx9N1hzw">https://www.youtube.com/watch?v=Hr_Rx9N1hzw</a>
<a href="https://blog.vuejs.org/">https://blog.vuejs.org/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[사이드 프로젝트] 심플리 정산 프로젝트 제작기]]></title>
            <link>https://velog.io/@hgoguma_124/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%AC%ED%94%8C%EB%A6%AC-%EC%A0%95%EC%82%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%AC%ED%94%8C%EB%A6%AC-%EC%A0%95%EC%82%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A0%9C%EC%9E%91%EA%B8%B0</guid>
            <pubDate>Tue, 21 Jun 2022 14:23:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/fe282a5f-2a19-4903-9550-9455d71dbaca/image.png" alt=""></p>
<p><a href="https://simply-jungsan.vercel.app/" target="_blank">심플리정산 페이지로 이동</a></p>
<br>

<h2 id="개요">개요</h2>
<blockquote>
</blockquote>
<p>시작은 친구 나랑의 아이디어에서 시작했다. 모임에서 각자 정산하는 과정이 생각보다 까다로웠다는 나랑의 문제의식에서 시작해 간단하게 정산해주는 웹사이트를 제작하기로 했다.
사실 작년 연말에 시작해서 길어야 한 두달이면 끝나겠거니... 했지만... 나의 귀차니즘을 과소 평가해버렸다!^^ 이래저래 미루고 미뤄서 결국 6개월 정도가 걸려버렸다!</p>
<br>

<h2 id="프로젝트-기간">프로젝트 기간</h2>
<p>2021.12 ~ 2022.06</p>
<p>퇴근하고 나서 뭐 하기... 쉽지 않잖아.... 나만 그런 거 아니 잖아....
<br></p>
<h2 id="프로젝트-참여-구성원">프로젝트 참여 구성원</h2>
<h3 id="📋-나랑-기획-디자인-마케팅">📋 나랑 (기획, 디자인, 마케팅)</h3>
<p>아이디어 제시부터 시작해서 기획, 디자인, 마케팅까지 올라운더로 열심히 일한 나랑! 
내 친구지만,,, 너무 멋진 친구다,,, 말모말모
중간중간 프로젝트가 갈 길을 잃고 동력을 잃기도 했는데 그 과정에서 나랑이 너무나도 열심히 해줬기에 나도 힘낼 수 있었던 것 같다.</p>
<h3 id="⛏-호박고구마-프론트엔드">⛏ 호박고구마 (프론트엔드)</h3>
<p>사실 그냥 간단한 계산기여서 오래 걸릴 거라고 생각하진 않았는데, Next.js나 Recoil 같이 새로 쓰는 프레임워크와 라이브러리가 많아서 생각보다 애 먹었다. 배포하는 것도 허둥지둥... 쉬울 줄 알았는데 쉬운 게 하나도 없었다.</p>
<br>

<h2 id="주요-기능">주요 기능</h2>
<h3 id="1-지출-내역-crud">1. 지출 내역 CRUD</h3>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/d4897232-8897-4122-bf10-72836b4aefb8/image.png" alt=""><img src="https://velog.velcdn.com/images/hgoguma_124/post/e6a9cf20-7df4-49ca-8169-fd35d6936045/image.png" alt=""><img src="https://velog.velcdn.com/images/hgoguma_124/post/3abcf62e-f75d-41ab-8b66-e97c1bb26d82/image.png" alt=""><img src="https://velog.velcdn.com/images/hgoguma_124/post/15626618-82c2-4272-834d-0c751a537ecc/image.png" alt=""><img src="https://velog.velcdn.com/images/hgoguma_124/post/89c4c546-e688-432b-868c-63f0dd19d065/image.png" alt=""></p>
<ul>
<li><p>계산 로직
세자리수 넘어가는 숫자만 보면 머리 아픈 병에 걸린 사람이라 계산 로직을 어떻게 해야 하는지 이리저리 헤매다가 구글링을 통해 힌트를 접했다. 나랑과 함께 엑셀 시트로 여러 케이스를 가정하고 계산했던 게 나중에 테스트용 JSON을 만들 때 큰 도움이 됐다.</p>
</li>
<li><p>내역 조회/작성/수정/삭제
사실 CRUD 자체는 기본이라 구현하는 데 어렵진 않았는데, 컴포넌트 단위로 분리해서 작업하다 보니 props 같은 걸 주고 받는 과정에서 시간이 걸렸던 것 같다. 초반부터 뭔가 깔끔하게 디렉토리 구조 잡고 컴포넌트로 분리해서 작업하고 싶었던 욕심에 과하게 분리해서 오히려 기능 구현에 시간을 잡아 먹었던 케이스다. 
결과물이 그렇다고 깔끔한 것도 아닌 것 같고 기능 구현이 미처 되지 않은 상태에서 뇌피셜로 컴포넌트를 나누다보니 기준 없이 나뉜 느낌도 든다.</p>
</li>
</ul>
<h3 id="2-카카오-api-연동">2. 카카오 API 연동</h3>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/842b20aa-990a-49da-9f6a-ddfea91db3a6/image.png" alt=""></p>
<ul>
<li>카카오톡 공유 API
정산 결과를 카카오톡으로 공유하는 기능을 위해 카카오톡 공유 API를 사용해봤다. 
<a href="https://developers.kakao.com/" target="_blank">Kakao Developers</a> 에서 어플리케이션 등록 후 가이드 대로 따라만 하면 쉽게 API를 연결할 수 있었다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/hgoguma_124/post/7d3db00a-a7d4-45ad-874e-f17231e75623/image.png" alt=""></p>
<p>그리고 정말 편리하게도 위 화면처럼 카카오에서 발송할 메시지 템플릿을 구성할 수 있는 페이지를 제공한다. 그래서 사용자에게 발송될 메시지의 이미지, 텍스트, 레이아웃 등을 구성할 수 있고 특정 이벤트도 등록할 수 있다. 가이드도 친절하고 사용법도 어렵지 않아서 뚝딱 연결할 수 있었다.
그런데 초반에 웹에서 테스트할 때 &#39;카카오톡으로 공유하기&#39;버튼 클릭 후 카카오톡이 열려야 하는데 열리지 않아서 애를 먹었는데, 찾아보니 Javascript의 경우 웹에서 테스트하긴 어렵다는 공식 답변을 발견했다. 그래서 일일이 배포하고 모바일로 테스트해야 해서 좀 많이 불편했다. 근데 어느 순간부터 카카오 API가 수정된건지 뭔지 웹에서도 잘 작동해서 쉽게 테스트할 수 있었다.
이번엔 카카오톡으로 웹 사이트를 공유하는 단순한 API만 사용해봤지만 다음엔 카카오톡 로그인, 회원가입 같은 인증이나 지도 같은 좀 더 다양하게 커스터마이징 할 수 있는 API를 사용해보면 재밌을 것 같다.</p>
<br>

<h2 id="사용-기술">사용 기술</h2>
<h3 id="1-nextjs">1. Next.js</h3>
<p>페이지 단위로 서버사이드 렌더링은 물론 클라이언트 렌더링까지 제공하는 멋진 React 프레임워크인 Next.js를 사용했다.
Next.js 자체는 이미 인강으로 접해보고 몇번 토이 프로젝트(그러나 완성하지 못한..)용으로 사용해봤기 때문에 많이 안다고 생각했었는데 웬걸... 아니었다.
이것저것 시도해보면서 공식 사이트를 몇 번이나 정독하고 구글링하면서 전체적인 흐름을 이번에야 좀 이해한 것 같다. 물론 흐름만 이해하게 된거지, Next.js 서버가 어떻게 돌아가는지, 렌더링 과정은 구체적으로 어떤건지 더 깊이 공부할 게 산더미다.</p>
<ul>
<li><p>Data Fetching
이번 프로젝트의 경우 간단한 계산기 프로그램이어서 Next.js의 핵심 기능인 서버사이드 렌더링을 맛볼 기회는 아쉽게도 없었다. 그런데 Firebase에서 데이터를 가져오는 과정에서 원하는 흐름으로 데이터가 불러와지지 않았고, 에러 뿜뿜에 여러 가지 시행착오를 겪어야만 했다. 그 과정에서 <a href="https://nextjs.org/docs/basic-features/data-fetching/overview" target="_blank">Data Fetching</a> 에 대한 공식 문서를 진짜 수십번은 본 거 같다ㅋㅋㅋㅋㅋㅋ 그러면서 공식 문서와 친해졌고 Next.js의 주요 컨셉과 getServerSideProps, getStaticPaths, getInitialProps 같은 것들이 어떤 차이를 갖고 어떤 기능을 하는지 이해하게 됐다. 실제로 getServerSideProps를 써보면서 Firebase 서버에서 데이터를 가져오기도 하고 파라미터에 뭐가 들어있는지 찾아보기도 했다. Next.js에 대해서는 아예 포스팅을 따로 하면서 좀 더 깊게 공부해야겠다.</p>
</li>
<li><p>Dynamic Routes
더불어 정산 결과 페이지의 경우 고유한 Uid를 생성후 Firebase에 저장하고 해당 Uid를 Url로 활용하기도 했는데, 이 때 Next.js에서 제공하는 
<a href="https://nextjs.org/docs/routing/dynamic-routes" target="_blank">Dynamic Route</a> 기능이 너무너무 편했다.
기본적으로 Next.js는 pages 디렉토리에 따라 라우팅 되기 때문에 라우팅을 따로 고민하지 않아도 돼서 편했는데, Dynamic Route를 설정하는 것도 쉬워서 너무 좋았다. 
물론 쉽다고 해서 바로 된 건 아니지만^_^; 
사실 공식 문서를 꼼꼼히 독해만 해도 답이 있는데 대충 읽음 -&gt; 해보니까 안됨 -&gt; 그냥 구글링 때림 -&gt; 안됨 -&gt; 다시 공식 문서로 돌아와서 정독 
이렇게 한바퀴 도니까 금방 끝날 게 오래 걸렸다ㅋㅋㅋㅋㅋ
진짜 공식 문서만 읽으면 거의 80%는 답이 있는데 왤케 안 읽는 걸까... 공식 문서 안 읽고 구글링 하는 병에 걸린 것이예요,,, 
매번 열심히 읽자고 다짐하는데 안되는 사람... 영어와 좀 더 많이 친해져야겠다ㅎㅎ</p>
</li>
</ul>
<p>아무튼 다시 돌아와서 Next.js의 Dynamic Route 기능이 편한건 정말 확실하다. 오죽하면 회사에서도 그냥 React.js 로 토이 프로젝트 하다가 Dynamic Route 구현이 골치 아파서 Next.js를 사용하기로 했을까. 
역시 세상에 천재들은 많고 많다... 멋지고.. 고맙습니다... 
<br></p>
<h3 id="2-firebase">2. Firebase</h3>
<p><span style="color:#8f4032">Firebase는 들으시오. 가이드 문서를 좀 더 구체적으로, 친절하게 적으시오.</span></p>
<p>아니 이건 예에에에전에 GA 애널리틱스 강의 보고 가이드 문서 읽으면서도 느꼈던 건데 전반적으로 가이드가 불친절한 것 같다. 딱 정말정말 기본적인 것만 띡띡 알려주고 끝ㅋㅋㅋㅋㅋㅋ
그냥 이건 구글, AWS 같은 미국 IT 기업의 특징인가;
그리고 뭔가 특유의 영문 -&gt; 한국어 번역 느낌이 이질적이어서 더 가이드 문서가 안 읽힌다. 그냥 원문 보는 게 편했다.
그리고 뭔가 앱 개발에서 수요가 더 커서 그런건지 뭔지 웹용 Javascript 문서는 더 불친절...</p>
<p>어쨌든 그래도 처음엔 학습겸 되도록이면 Firebase의 많은 기능을 사용해보고 싶어서 이것저것 많이 건드려봤다.</p>
<ul>
<li>Firestore Database
이번 프로젝트에서 실질적으로 사용한 기능이다. 콘솔 페이지에서 컬렉션과 문서 만들고 클라이언트에서 JSON을 보내면 뚝딱 DB에 저장해준다. 
근데 문제는 Firestore에서 데이터를 관리하는 컨셉(컬렉션과 문서)에 대한 깊은 이해가 없다보니 데이터를 가공하는데 애를 먹었다.
가이드 문서에서 하라는 대로 요청했는데 응답값이 예상에서 벗어난 JSON 구조 형태로 떨어져서 어떻게 가공해야 하는지 엄청 구글링했다ㅎㅎ
도대체 왜 가이드 문서랑 다르게 나오는지 이해가 안됨; 
이번에야 정말 간단한 CRUD만 있어서 그렇지 데이터 구조가 복잡하거나 컬렉션간에 Join이 필요하다거나 했으면 머리 쥐어 뜯어서 탈모왔을듯ㅎㅎ
앞으로의 학습 지점으로 남겨놔야겠다.</li>
</ul>
<ul>
<li><p>Functions
따로 서버를 구축할 필요 없이 뚝딱 API를 만들 수 있는 멋진 기능이다. 이번 프로젝트에서는 사용하지 않았지만, 한번 학습겸 사용해봤다. 
Functions 기능을 사용하려면 무료에서 유료 요금제로 바꿔야 해서 조금 떨렸다. 아니 사실 많이...^_^ 매일매일 콘솔 대시보드에서 사용량 확인한 사람...
왜냐면 약 2년전에 AWS 한번 써본다고 무료 계정 만들고 DB 만들고 방치했다가 갑자기 15만원인가 과금되서 부랴부랴 AWS에 연락해서 &quot;나는 이거 아직까지 사용되는 지 몰랐다, 무료라서 다 무료인 줄 알았다, 나 그냥 학습용으로 만든 거고 나 돈 없다&quot;고 징징댔던 게 약간 트라우마 처럼 남아서 계속 대시보드 쳐다봄ㅋㅋㅋㅋ
다행히 이번엔 저번 같은 일은 발생하지 않았다.
다시 돌아와서 Functions로 Firestore에서 데이터를 가져와서 클라이언트에 보내는 간단한 API를 만드는데도 역시나 오래걸렸다. 
사용법이 익숙하지 않고 헤매느라 개발 서버에서 한줄한줄 디버깅 하면서 어디가 잘못된 건지 알고 싶었는데 할 수가 없었다. 공식 문서 보고 디버깅이랑 로그도 찍어봤는데 안되던데요...ㅎ...ㅠㅠ 어쨌든 제대로 된 디버깅을 할 수 없다는 점이 답답했지만 우여곡절 끝에 테스트가 성공하긴 했는데, 데이터를 가져오는데 시간이 생각보다 많이 소요 돼서 결국은 Functions를 사용하지 않기로 결정했다.
가뜩이나 성격 급한데 이렇게 느리면... 개발 못해...
찾아보니까 나처럼 응답 속도에 대해서 좀 불만족스러운 말들이 있던데 뭐 어쨌든 무료로 사용하는 거니까 너무 큰 걸 바라진 않아야지 싶긴 하다.
그럼에도 불구하고 별도의 서버 없이 간단하게 API를 만들 수 있기 때문에 멋진 기능이긴 하다. 규모가 크진 않지만 이번 프로젝트 보단 큰 프로젝트에서 사용해보면 좋을 것 같다.</p>
</li>
<li><p>Hosting
호스팅의 경우 Firebase 기반의 기능 중 가장 만족스러웠던 기능이다. 사실 뭐 기능이라고 할 것도 없는게 그냥 커맨드 몇 줄 치면 뚝딱 알아서 호스팅 해줘서 아주 편했다. 그러나 중간에 Vercel로 호스팅 하기로 하면서 아쉽게도 이번 프로젝트에선 사용하지 못했다. </p>
<br>

</li>
</ul>
<h3 id="3-vercel">3. Vercel</h3>
<p>Next.js 개발팀에서 만든 호스팅 서비스인데, 이번에 처음 사용해봤다. 간단하게 프로젝트를 만들고 git 저장소랑 연결하면 push 될 때 마다 자동으로 배포를 해준다. 어플리케이션에서 사용하는 환경 변수(API 키 값 등) 같은 것도 쉽게 넣을 수 있다. 
그냥 commit 하고 push 만 하면 알아서 자동으로 배포 해주고 도메인도 제공해주니 짱짱맨!
이렇게 비상업적인 개인의 프로젝트를 배포하는데에 참 편리한 친구였다. 이 외에도 비슷하게 Netlify도 많이 사용하던데 다음에 한번 써보고 같이 비교해보면 재밌을 것 같다.</p>
<br>


<h2 id="마치며">마치며</h2>
<p>사실 나는 개발자가 되면 매일 퇴근하고 공부하고 사이드 프로젝트도 막 여러개 하고 그럴 줄 알았다. 아니, 내가 그럴 수 있을 거라고 생각했다. 그런데 막상 회사에 다녀보니 쉽지 않았다. 
일단 퇴근 하면 진이 다 빠지고, 좀 눕기도 하고 운동도 해야 하고 넷플릭스도 봐야 하고 유튜브도 봐야하고 밀린 빨래도 해야 하고 설거지도 해야 하고 청소도 좀 해야 하고... 해야 하는 게 생각보다 많았다.
이런 상황에서 열심히 여러 기술들을 공부하고 그걸 토이 프로젝트에 사용해보고 그런 과정을 블로그로 기록하고 이런 게 정말 쉽지 않다는 걸 느낀다.
열심히 공부한 걸 이렇게 블로그에 포스팅하는 개발자분들... 정말 존경스럽다.
이 프로젝트 자체도 마찬가지지만 이 글 자체도 2주 전에 써놓고 임시저장 한 걸 꾸역꾸역 쓰고 있다. </p>
<p>그래서, 이런 나이기에 이 프로젝트는 큰 의미를 갖는다. 특별한 기능 없는, 그냥 계산기 정도에 불과한 웹 사이트지만 온전히 나 혼자서 무언가를 만들어 낸 건 처음이다. 학원에서 프로젝트할 때엔 그래도 같이 개발하는 사람들이 있었고, 각자 맡은 바가 나눠져 있었다. 그런데 이 프로젝트는 정말 나 혼자서 0부터 1까지 했기 때문에 뜻깊다. 
사이드 프로젝트 해보겠다고 git에 저장소 만들고 지운게 한 수십번은 될 거 같은데, 이렇게 꾸준히 무언가를 끝까지 하다니!
개발자라는 정체성을 떠나서 나라는 사람에게 이런 &#39;완주&#39;가 소중하다는 걸 새삼 느꼈다.</p>
<p>갑자기 내가 개발이 마치 취미이자 일상 생활이자 행복의 원천인 것 처럼 미친듯이 무언갈 계속 공부하고 프로그램을 만들 거라고 스스로에게 거짓말은 못하겠다. 여전히 퇴근 하고 나서 침대에 눕고 뒹굴거리고.. 그렇게 하루를 보내다가 또 어떤 날은 노트북을 켜고 공부를 하고 인강을 듣고 갑자기 블로그를 쓰고 그러겠지.. 
그러나 이번 프로젝트가 이틀 놀 거 하루만 놀고 정신 차리고 새로운 기술을 공부하고 도전하는 좋은 계기가 될 것 같다.</p>
<p>사실은 남들처럼 멋지게 이것저것 기술적으로 설명하고 싶었는데 그냥 이래저래 내 생각과 경험을 정리한 일종의 회고록이 되어 버렸다.
어쨌든 수고한 나 자신을 토닥여주고 싶고, 무엇보다도 기획과 디자인이라는 전문 분야가 아닌 영역에 도전하고 마케팅도 놓치지 않은 나랑에게 너무 멋지고 고맙다는 말을 하고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API 호출 로직에 Typescript 입히기]]></title>
            <link>https://velog.io/@hgoguma_124/API-%ED%98%B8%EC%B6%9C-%EB%A1%9C%EC%A7%81%EC%97%90-Typescript-%EC%9E%85%ED%9E%88%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/API-%ED%98%B8%EC%B6%9C-%EB%A1%9C%EC%A7%81%EC%97%90-Typescript-%EC%9E%85%ED%9E%88%EA%B8%B0</guid>
            <pubDate>Wed, 16 Feb 2022 00:21:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기존의 javascript로 구현된 레거시 프로젝트에 typescript를 입히는 과정에서 겪은 상황과 해결방법을 정리한 포스팅입니다. </p>
</blockquote>
<h3 id="1-request-파라미터-타입-정의">1. Request 파라미터 타입 정의</h3>
<p>서버에 데이터 요청시 공통으로 사용하는 파라미터의 타입들을 정해준다.
주로 리스트의 데이터를 가져오는 API 호출을 많이 사용해서 Request에 paging 관련 정보를 넣는다.</p>
<pre><code class="language-javascript">export interface CommonRequest {
  pagingNo: number;
  pagingSize: number;
  pagingSort: string;
}</code></pre>
<br />

<h3 id="2-response-데이터-타입-정의">2. Response 데이터 타입 정의</h3>
<p>서버에서 공통으로 응답되는 것과 상황별 응답 데이터를 구분한다.</p>
<h3 id="공통-데이터-타입-정의">공통 데이터 타입 정의</h3>
<p>서버에서 응답값을 줄 때 공통적으로 주는 부분을 정의한다.</p>
<pre><code class="language-javascript">export interface CommonResponse {
  resultCd: string;
  errCd: string;
  errMsg: string;
  // 아래 데이터는 필수값이 아니므로 ? 사용
  pagingLastPageNo?: number;
  pagingNo?: number;
  pagingRowCnt?: number;
  pagingTotCnt?: number;
}
</code></pre>
<h3 id="상황별-정리">상황별 정리</h3>
<p>실제 API를 호출하는 상황에서 응답 받을 데이터를 정의한다.
예를 들어 장바구니 아이템 리스트를 가져오고 싶다면, 장바구니 아이템 자체에 대해 먼저 타입을 정의한다.</p>
<h4 id="장바구니-아이템-타입-정의">장바구니 아이템 타입 정의</h4>
<pre><code class="language-javascript">export interface Item {
  name: string;
  price: number;
  type: string;
  createDate: string;
}</code></pre>
<p>실제 데이터는 장바구니 아이템이 여러개 담긴 배열 형태로 떨어진다.</p>
<h4 id="장바구니-아이템-리스트-조회-request-타입-정의">장바구니 아이템 리스트 조회 Request 타입 정의</h4>
<pre><code class="language-javascript">export interface ItemListResponse extends CommonResponse {
  list: Item[]
}</code></pre>
<p>extends를 활용해 공통으로 사용하는 응답 타입들을 사용하고, list라는 변수의 타입을 먼저 정의한 Item 객체 배열로 정의한다.</p>
<br />

<h3 id="3-axios-및-api-호출-로직-정의">3. Axios 및 API 호출 로직 정의</h3>
<p>axios 라이브러리를 통해 공통으로 API 호출을 정의하는 로직을 정의한다.</p>
<pre><code class="language-javascript">import axios, { AxiosInstance, AxiosResponse } from &#39;axios&#39;;
import { CommonRequest } from &#39;@/modules/types/api/common&#39;;

// axios instance 생성
const customAxios: AxiosInstance = axios.create({
  headers: {
    access_token: `${accessToken}`
  }
});

// 실제 API 통신
// path : API url
// params : request parameter
export const doAxios = async &lt;T&gt;(path: string, params: CommonRequest): Promise&lt;T | null&gt; =&gt; {
  try {
    const { status, data }: AxiosResponse&lt;T&gt; = await customAxios.post(path, params);
    return status &lt; 500 ? data : null
  } catch (err) {
    console.log(err)
  }
};
</code></pre>
<p>제네릭을 이용해서 위 메소드를 호출하는 시점에 타입을 넘겨줄 수 있도록 한다. 이렇게 하면 doAxios 로 반환되는 값은 특정 타입을 갖게 된다.</p>
<h3 id="4-api-호출하기">4. API 호출하기</h3>
<pre><code class="language-javascript">
import { useCallback } from &#39;react&#39;;
import { doAxios } from &#39;@/modules/common/api&#39;;
import { CommonRequest } from &#39;@/modules/types/api/common&#39;;
import { ModuleResponse } from &#39;@/modules/types/api/product&#39;;

const loadData = useCallback(async () =&gt; {
    const path = `${path}`
    const params: CommonRequest = {
      pagingNo: 1,
      pagingSize: 10,
      pagingSort: `${pagingSort}`
    }
    const data = await doAxios&lt;ModuleResponse&gt;(path, params)
  }, [])</code></pre>
<p>이렇게 하면 data.resultCd, data.errMsg, data.pagingNo 등 CommonResponse interface에 정의한 값들에 바로 접근 가능하다.
동시에 ItemListResponse interface에 정의한 data.list에 접근 가능하다.
data.list에서 forEach, map 등의 메소드를 통해 각 배열의 인자의 키 값에도 접근 가능하다.</p>
<br />

<p>typescript가 없었다면 응답 값으로 무엇이 있는지 알 수 없고, 객체 안에 어떤 데이터가 있는지 알 수 없었을 텐데 type을 정의하니 자동으로 타입과 객체 추론이 가능하다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nuxt.js 에서 에러 페이지 처리하기]]></title>
            <link>https://velog.io/@hgoguma_124/Nuxt.js-%EC%97%90%EC%84%9C-%EC%97%90%EB%9F%AC-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Nuxt.js-%EC%97%90%EC%84%9C-%EC%97%90%EB%9F%AC-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 28 Dec 2021 11:44:45 GMT</pubDate>
            <description><![CDATA[<p>Nuxt.js 에서는 예기치 않은 에러 발생시, layout 폴더 하위에 error.vue 파일을 이용하면 아주 쉽게 에러 페이지를 커스터마이징 할 수 있다.
(이걸 모르고 에러 페이지를 따로 만들어서 라우터를 태우는 뻘짓을 한 사람,,)
<br><br></p>
<h4 id="1-layouterrorvue-생성">1. layout/error.vue 생성</h4>
<p>먼저 layout 디렉토리에 error.vue 파일을 생성한다.</p>
<pre><code>&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;h1 v-if=&quot;error.statusCode === 404&quot;&gt;Page not found&lt;/h1&gt;
    &lt;h1 v-else&gt;An error occurred&lt;/h1&gt;
    &lt;NuxtLink to=&quot;/&quot;&gt;Home page&lt;/NuxtLink&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  props: [&#39;error&#39;],
  layout: &#39;errorLayout&#39;
}
&lt;/script&gt;</code></pre><p>layout에 &#39;errorLayout&#39;이라는 커스터마이징한 레이아웃을 설정할 수 있고, props에 &#39;error&#39;를 넣으면 nuxt.js에서 관리하는 error 객체를 사용할 수 있다.
<br><br></p>
<h4 id="2-error-객체-다루기">2. error 객체 다루기</h4>
<p>그렇다면 error.vue에서 props로 전달되는 error 객체를 어떻게 다룰 수 있을까? 
쉽다. 에러 페이지로 전달하고 싶을 때 nuxt.js에서 제공하는 에러 객체에 에러 코드나 에러 메시지 등을 전달하면 된다.</p>
<p>현재 진행하고 있는 프로젝트에서는 nuxt.js에서 제공하는 미들웨어 기능을 사용해 라우팅 처리를 진행중인데, 이 때 사용자가 존재하지 않거나 접근 권한이 없는 페이지로 진입하려고 했을 때 에러 페이지로 라우터를 변경할 수 있다.</p>
<pre><code>// 사용자 권한에 따라 사용할 수 있는 view가 아닌 경우 route error 처리
if (!view) {
    return ctx.error({ statusCode: 404, message: &#39;Route Error&#39; })
}</code></pre><p>위와 같이 미들웨어에서 접근 가능한 ctx에서 error 핸들러를 통해 에러 코드와 에러 메시지를 props로 전달할 수 있다.
이러면 error.vue 레이아웃에서 전달된 에러 코드와 에러 메시지가 나타나게 된다.  </p>
<p>만약 미들웨어가 아니라 컴포넌트나 페이지 단위에서 에러 핸들러를 사용할 경우에는 아래와 같이 활용할 수 있다.</p>
<pre><code>this.$nuxt.error({ statusCode: 404, message: &#39;Route Error&#39; })</code></pre><p><br><br></p>
<h4 id="3-error-페이지-진입-전-로직-실행하기">3. error 페이지 진입 전 로직 실행하기</h4>
<p>여기서 또 하나의 난관에 부딪혔는데, 팝업이 열려 있는 상태에서 에러가 발생해 에러 페이지로 이동한 경우 팝업이 닫히지 않는 문제가 있었다.
error.vue 컴포넌트가 생성되기 전, 즉 에러 페이지 진입 바로 직전에 열려 있는 팝업들을 닫아야 했다.
미들웨어나 컴포넌트 라이프 사이클 훅을 살펴봤지만 원하는 대로 작동하지 않아 찾아보니, nuxt.config.js에서 제어가 가능했다.</p>
<pre><code>export default {
 vue: {
    config: {
      errorHandler: (error, vm, info) =&gt; {
        // erorr 이동전 아래 로직 수행 ...
      }
    }
  },
  ...
}</code></pre><p>위와 같이 정의하면 에러 페이지 진입 전에 위의 errorHandler가 먼저 실행된다. 
<img src="https://images.velog.io/images/hgoguma_124/post/8bd8dcd7-4cd4-44cc-8a67-3641377f41ea/1.png" alt="">
<br>
이렇게 error.vue 레이아웃을 이용하면 아주 쉽게 에러 페이지를 컨트롤할 수 있다. 
<br><br></p>
<p><strong>출처</strong>
<a href="https://nuxtjs.org/docs/directory-structure/layouts/">https://nuxtjs.org/docs/directory-structure/layouts/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Click 이벤트 시 Blur 이벤트 막기]]></title>
            <link>https://velog.io/@hgoguma_124/Click-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%8B%9C-Blur-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A7%89%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Click-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%8B%9C-Blur-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A7%89%EA%B8%B0</guid>
            <pubDate>Thu, 25 Nov 2021 04:30:00 GMT</pubDate>
            <description><![CDATA[<p>아래와 같은 Pagination UI 컴포넌트 개발시 이상한 이슈를 겪었다.
<img src="https://images.velog.io/images/hgoguma_124/post/61bdf473-a823-4e26-a12f-3a2b3bee1ca5/image.png" alt="">
<br></p>
<h3 id="문제-상황">문제 상황</h3>
<p>페이지를 입력하는 input 창에 Blur 이벤트가 걸려 있고, 양쪽의 Prev와 Next 버튼에 Click 이벤트가 걸려 있는 상황이었다. </p>
<p>그런데 양쪽 버튼 클릭시 이벤트가 발생하지 않았다. 
확인해보니 사용자가 input에 데이터를 입력 후 버튼을 클릭 했을 때 Click 이벤트 보다 Blur 이벤트가 먼저 발생해서 생기는 이슈였다. </p>
<br>
<br>


<h3 id="해결-방법">해결 방법</h3>
<blockquote>
<p>버튼에 Mousedown 이벤트를 걸고 Mousedown 이벤트 발생시 preventDefault를 실행해 Blur 이벤트가 발생하지 않도록 처리하면 된다.</p>
</blockquote>
<p>각각의 이벤트가 어떻게 발생하는지 확인하고 싶어서 Code Pen으로 간단하게 코드를 작성해 봤다.</p>
<p><a href="https://codepen.io/hgoguma/pen/WNEqNJR">Blur, Mousedown, Click 이벤트 확인 Code Pen</a></p>
<br>]]></description>
        </item>
        <item>
            <title><![CDATA[[Vue.js] 라이브러리 없이 Drag & Drop 가능한 파일 업로드 만들기]]></title>
            <link>https://velog.io/@hgoguma_124/Vue.js-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-Drag-Drop-%EA%B0%80%EB%8A%A5%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Vue.js-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%97%86%EC%9D%B4-Drag-Drop-%EA%B0%80%EB%8A%A5%ED%95%9C-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 08 Oct 2021 07:33:45 GMT</pubDate>
            <description><![CDATA[<p>보통 Drag &amp; Drop이 가능한 파일 업로드 기능을 구현할 때는 기존에 있던 라이브러리를 쓰면 간단하고 빠르게 해결할 수 있다. 
지금까지 제일 유명한 Dropzone.js나 filepond.js 등을 사용해봤는데, 이번에는 라이브러리 없이 간단하게 파일 업로드를 구현해봤다.
어려울 줄 알았는데 생각보다 쉬워서 놀랐다.
<br></p>
<hr>
<br>

<h3 id="마크업">마크업</h3>
<pre><code>&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;file-upload-container&quot; 
      @dragenter=&quot;onDragenter&quot;
      @dragover=&quot;onDragover&quot;
      @dragleave=&quot;onDragleave&quot;
      @drop=&quot;onDrop&quot;
      @click=&quot;onClick&quot;
    &gt;
      &lt;div class=&quot;file-upload&quot; :class=&quot;isDragged ? &#39;dragged&#39; : &#39;&#39;&quot;&gt;
        Drag &amp; Drop Files
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;!-- 파일 업로드 --&gt;
    &lt;input type=&quot;file&quot; ref=&quot;fileInput&quot; class=&quot;file-upload-input&quot; @change=&quot;onFileChange&quot; multiple&gt;
    &lt;!-- 업로드된 리스트 --&gt;
    &lt;div class=&quot;file-upload-list&quot;&gt;
      &lt;div class=&quot;file-upload-list__item&quot; v-for=&quot;(file, index) in fileList&quot; :key=&quot;index&quot;&gt;
        &lt;div class=&quot;file-upload-list__item__data&quot;&gt;
          &lt;img class=&quot;file-upload-list__item__data-thumbnail&quot; :src=&quot;file.src&quot;&gt;
          &lt;div class=&quot;file-upload-list__item__data-name&quot;&gt;
            {{ file.name }}
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;file-upload-list__item__btn-remove&quot; @click=&quot;handleRemove(index)&quot;&gt;
          삭제
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><ol>
<li>Drag &amp; Drop 할 영역에 Drag &amp; Drop과 Click 이벤트를 걸어준다.</li>
<li>input type=file 태그는 화면에 보이지 않게 css 로 숨기고 Change 이벤트를 걸어준다.</li>
<li>업로드 된 파일 리스트를 보여줄 리스트를 만든다.</li>
</ol>
<h3 id="css">CSS</h3>
<pre><code>&lt;style lang=&quot;scss&quot;&gt;
.container {
  min-height: 300px;
  width: 500px;
  margin: 0 auto;
}
.file-upload {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  border: transparent;
  border-radius: 20px;
  cursor: pointer;
  &amp;.dragged {
    border: 1px dashed powderblue;
    opacity: .6;
  }
  &amp;-container {
    height: 300px;
    padding: 20px;
    margin: 0 auto;
    box-shadow: 0 0.625rem 1.25rem #0000001a;
    border-radius: 20px;
  }
  &amp;-input {
    display: none;
  }
  &amp;-list {
    margin-top: 10px;
    width: 100%;
    &amp;__item {
      padding: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      &amp;__data {
        display: flex;
        align-items: center;
        &amp;-thumbnail {
          margin-right: 10px;
          border-radius: 20px;
          width: 120px;
          height: 120px;
        }
      }
      &amp;__btn-remove {
        cursor: pointer;
        border: 1px solid powderblue;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 5px;
        border-radius: 6px;
      }
    }
  }
}
&lt;/style&gt;</code></pre><p>CSS는 특별할 게 없다.
Drag &amp; Drop 영역에 파일이 드래그 될 때 해당 영역에 보더와 opactity 효과를 줬다.</p>
<br>

<h3 id="구현된-화면">구현된 화면</h3>
<p><img src="https://images.velog.io/images/hgoguma_124/post/5ce594f8-35ae-4da0-b7fe-011189b4a499/image.png" alt=""></p>
<br>

<h3 id="이벤트-걸기">이벤트 걸기</h3>
<pre><code>&lt;script&gt;
      onClick () {
        this.$refs.fileInput.click()
      },
      onDragenter (event) {
        // class 넣기
        this.isDragged = true
      },
      onDragleave (event) {
        // class 삭제
        this.isDragged = false
      },
      onDragover (event) {
        // 드롭을 허용하도록 prevetDefault() 호출
        event.preventDefault()
      },
      onDrop (event) {
        // 기본 액션을 막음 (링크 열기같은 것들)
        event.preventDefault()
        this.isDragged = false
        const files = event.dataTransfer.files
        this.addFiles(files)
      },
      onFileChange (event) {
        const files = event.target.files
        this.addFiles(files)
      }
    &lt;/script&gt;</code></pre><ol>
<li><p>우선 Drag &amp; Drop 영역을 클릭 했을 때, input type=file을 클릭한 것과 같은 효과를 낼 수 있는 이벤트를 추가한다.</p>
</li>
<li><p>dragenter, 즉 Drag &amp; Drop 영역 안으로 파일이 드래그 되는 경우 isDragged 변수의 상태 값을 변경해 class 가 추가되도록 한다.</p>
</li>
<li><p>dragleave, 즉 Drag &amp; Drop 영역 바깥으로 파일이 나가는 경우 isDragged 변수의 상태값을 다시 변경해 추가되었던 class를 삭제해준다. </p>
</li>
<li><p>dragover 이벤트에서 preventDefault()를 통해 드롭이 가능하도록 허용한다.</p>
</li>
<li><p>drop 이벤트에서 기본 액션을 막기 위해 preventDefault()를 실행하고, event.dataTransfer.files 를 통해 전달된 파일 객체를 addFiles 메소드에 넘긴다.</p>
</li>
<li><p>onFileChange는 input type=file 의 change 이벤트에 걸리는 메소드인데, 파라미터로 넘어온 파일 객체를 addFiles 메소드로 넘긴다.</p>
<br>


</li>
</ol>
<h3 id="파일-커스텀-메소드">파일 커스텀 메소드</h3>
<pre><code>&lt;script&gt;
   async addFiles (files) {
      for(let i = 0; i &lt; files.length; i++) {
        const src = await this.readFiles(files[i])
        files[i].src = src
        this.fileList.push(files[i])
      }
    },
    // FileReader를 통해 파일을 읽어 thumbnail 영역의 src 값으로 셋팅
    async readFiles (files) {
      return new Promise((resolve, reject) =&gt; {
        const reader = new FileReader()
        reader.onload = async (e) =&gt; {
          resolve(e.target.result) 
        }
        reader.readAsDataURL(files)
      })
    },
    handleRemove (index) {
      this.fileList.splice(index, 1)
    }
&lt;/script&gt;</code></pre><ol>
<li>addFiles 메소드에서는 파라미터로 넘어온 파일 객체를 읽고 fileList 배열에 추가한다.</li>
<li>readFiles 메소드는 업로드한 파일을 읽어 썸네일을 생성하기 위해 만든 메소드다.
주의할 점은 FileReader 객체는 비동기로 파일을 읽는다는 점이다. 때문에 실제 화면에 보여줄 fileList 배열에 파일 객체를 추가하기 전에 async, await 을 통해 먼저 파일을 읽는다.
readAsDataURL 메소드를 통해 읽은 파일의 URL 값을 얻을 수 있다. 해당 URL 값을 화면에 보여줄 파일의 src 값으로 셋팅해주면, 사진 파일의 경우 해당 src 값을 썸네일 처럼 활용할 수 있다.<ol start="3">
<li>hadleRemove의 경우 파일 리스트에서 삭제 버튼을 클릭시 splice 메소드를 통해 해당 배열에서 index에 해당하는 인자를 삭제한다.</li>
</ol>
</li>
</ol>
<hr>
<br>
Drag & Drop 기능은 막연하게 어려울 거라고 생각했는데 아니었다.<br>
생각보다 쉬워서 당황.. <br>
여기에 실제 프로젝트에서는 파일 유효성 검사(파일 개수, 용량, 확장자, mime 타입 등) 로직과 Sheet.js 라이브러리를 통해 업로드한 엑셀 파일을 json으로 가공하는 로직을 따로 추가했다. 

<p>정말 시간이 없고 여러 기능이 필요하다 싶을 땐 라이브러리를 사용하는 게 좋을 것 같고, 딱히 큰 기능이 필요 없는 경우엔 이번에 만든 걸 커스텀해서 사용하면 좋을 것 같다. 
<br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nuxt.js로 github pages에 정적 사이트 배포하기 ]]></title>
            <link>https://velog.io/@hgoguma_124/Nuxt.js%EB%A1%9C-github-pages%EC%97%90-%EC%A0%95%EC%A0%81-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hgoguma_124/Nuxt.js%EB%A1%9C-github-pages%EC%97%90-%EC%A0%95%EC%A0%81-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 12 Sep 2021 13:35:33 GMT</pubDate>
            <description><![CDATA[<p>최근에 회사에서 하며 Nuxt.js로 만든 프로젝트를 배포해야 했는데, 배포하며 겪었던 시행착오를 정리해보려 한다.</p>
<p>결론만 말하자면, Nuxt.js 공식 사이트에 친절히 다 적혀있으니 그대로만 하면 된다^^ </p>
<p>항상 늘 그렇듯이 공식 사이트에 모든 설명과 힌트가 있는데 혼자 고민하거나 애꿎은 구글링을 계속해서 뭔가를 해결하려고 하는 게 문제인 것 같다. 독해하는 게 귀찮더라도 공식 사이트를 꼼꼼히 읽으면 시간이 절약된다는 것을 다시금 느꼈다.</p>
<p>참고한 공식 사이트 링크
<a href="https://ko.nuxtjs.org/docs/2.x/deployment/github-pages">https://ko.nuxtjs.org/docs/2.x/deployment/github-pages</a></p>
<p>공식 사이트에서 기본적으로 소개하는 방법은 크게 </p>
<p>1) push-dir 2) github actions 3) Travis CI 4) Appveyor 인데 이중에서 실제 사용해본 방법은 push-dir 과 Trais CI 였다.</p>
<p>일단 위 방법들을 사용하기 전에 해야 하는 작업들이 있다.</p>
<h2 id="nuxtconfigjs-변경">nuxt.config.js 변경</h2>
<p>nuxt.config.js 파일에 아래 코드를 추가한다.</p>
<pre><code>export default {
  target: &#39;static&#39;, // 정적 웹사이트임을 명시
  router: {
    base: &#39;/&lt;repository-name&gt;/&#39; // github repository 이름 넣기
  }
}</code></pre><p>예를 들어 내 github repository가 &#39;nuxt-gh-pages&#39;이면, 아래와 같이 작성하면 된다.</p>
<pre><code>export default {
  target: &#39;static&#39;,
  router: {
    base: &#39;/nuxt-gh-pages/&#39;
  }
}</code></pre><p>이러면 사실상 끝이다.
코드 세줄만 추가하면 끝인데 뭘 그렇게 삽질한건지 모르겠다ㅎㅎ
참고로 여기서 전제조건은 github repository가 존재한다는 것과 nuxt.config.js의 ssr 값이 false, 즉 서버사이드 렌더링이 아니어야 한다는 점이다.</p>
<p>여기까지 했으면 공식 사이트에서 소개하는 방법 대로 하면 된다.</p>
<h2 id="push-dir-사용">push-dir 사용</h2>
<p>push-dir방법은 아주 간단해서 지금도 사용중이다.
공식 사이트에서 하라는 대로만 하면 아주 간단^^</p>
<h4 id="1-push-dir-패키지-깔기">1. push-dir 패키지 깔기</h4>
<pre><code>install push-dir --save-dev</code></pre><h4 id="2-packagejson에-deploy-script코드-추가">2. package.json에 deploy script코드 추가</h4>
<pre><code>&quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nuxt&quot;,
    &quot;build&quot;: &quot;nuxt build&quot;,
    &quot;start&quot;: &quot;nuxt start&quot;,
    &quot;generate&quot;: &quot;nuxt generate&quot;,
    &quot;deploy&quot;: &quot;push-dir --dir=dist --branch=gh-pages --cleanup&quot;
  },</code></pre><p>공식 사이트에 적혀있는대로 deploy 커맨드 코드만 추가하면 준비 끝이다.
** 여기서 build와 generate는 둘다 배포용 파일을 생성하는 커맨드 코드인데, generate의 경우 정적 사이트용 커맨드다.</p>
<h4 id="3-배포할-파일-만들기">3. 배포할 파일 만들기</h4>
<pre><code>npm run generate</code></pre><p>위 코드만 치면 알아서 dist 폴더가 만들어지면서 하위에 배포할 파일이 생성된다. </p>
<p><img src="https://images.velog.io/images/hgoguma_124/post/bbbd8847-685c-4dfc-87c9-172ff819ff9c/image.png" alt="">
이래저래 단계를 거쳐 파일이 생성된다.</p>
<p><img src="https://images.velog.io/images/hgoguma_124/post/d32abc0c-4cb6-441c-86bc-d313bf8ec6b7/folder.png" alt=""></p>
<p>위와 같이 dist 폴더 하위에 있는 파일들이 실제 배포될 파일이다.</p>
<p><img src="https://images.velog.io/images/hgoguma_124/post/e4930c1b-7dcb-475e-bee2-d3528a18d2c8/image.png" alt="">
index.html 파일을 살펴보면, 루트 디렉토리 url에 nuxt.config.js에서 추가한 저장소 이름이 추가되어 있음을 확인할 수 있다. 저장소 이름이 정상적으로 추가되어 있으면 성공이다!</p>
<h4 id="4-gh-pages-브랜치에-올리기">4. gh-pages 브랜치에 올리기</h4>
<pre><code>npm run deploy</code></pre><p>앞서 추가했던 스크립트 코드를 실행하면 github 저장소에 gh-pages 브랜치가 생성되고 해당 브랜치에 dist 하위의 파일들을 저장소에 올려준다.</p>
<p><img src="https://images.velog.io/images/hgoguma_124/post/57277897-0265-4317-a9ad-25dd356c2688/image.png" alt="">
위와 같이 gh-pages 브랜치에 잘 올라가면 성공</p>
<p>간혹 여기서 .nojekyll 파일이나 index.html 파일이 안생기기도 하는데, 수동으로 만들면 된다^^
.nojekyll 파일은 내용 없이 파일 껍데기만 있어도 되고 index.html은 200.html 을 복붙하면 된다.</p>
<p><img src="https://images.velog.io/images/hgoguma_124/post/72964c24-d2d7-4687-adc3-7dc0444e91b1/image.png" alt="">
github 저장소에서 Settings - Pages 메뉴에 들어가면 위와 같이 github pages에 배포되어 있음을 확인할 수 있다. 참고로 이 메뉴에서 Source 영역에서 바라보고 있는 브랜치가 gh-pages여아만 github pages 도메인으로 올라간다. 만약 다른 브랜치라면 변경해주면 된다.</p>
<p>정적 사이트에 배포하기... 너무 쉽다. 그냥 공식 사이트에서 하라는 대로만 하면 된다. 
Travis CI 방법도 generate를 통해 만들어진 정적 파일을 gh-pages 브랜치에 올리는 것으로 기본적인 순서는 같은데, 일일이 커맨드를 치지 않아도 자동으로 배포해준다는 점에서 아주 편리하다. Travis CI 가입하고 공식 사이트에서 하라는 대로 yml 파일만 넣어주면 클릭 한방에 배포까지 자동으로 해준다.</p>
<p>이렇게도 답이 뻔한데 삽질했던 이유는 일단 정적 사이트에 대한 개념 이해가 부족한 상태에서 원하는 결과가 안나오자 냅다 구글링만 했기 때문이다. 그냥 구글링할 시간에 공식 사이트를 꼼꼼히 읽었으면 삽질할 시간이 줄었다. </p>
<p>어쨌든 한번 삽질하며 이해했으니 다음번엔 후다닥 배포할 수 있을 것 같다^^</p>
<h4 id="오늘의-교훈--공식-사이트를-열심히-읽자">오늘의 교훈 : 공식 사이트를 열심히 읽자</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue.js .sync modifier]]></title>
            <link>https://velog.io/@hgoguma_124/Vue.js-.sync-modifier</link>
            <guid>https://velog.io/@hgoguma_124/Vue.js-.sync-modifier</guid>
            <pubDate>Sun, 28 Feb 2021 13:55:54 GMT</pubDate>
            <description><![CDATA[<p>Vue.js에서 양방향 바인딩을 한 경우 자식 컴포넌트에서 부모 컴포넌트로 이벤트 emit을 통해 값 변경시 &#39;update:myPropName&#39; 패턴이 권장됨.</p>
<h3 id="간단-예시">간단 예시</h3>
<h4 id="부모-컴포넌트에서-props-전달">부모 컴포넌트에서 props 전달</h4>
<pre><code>&lt;parent-component
 :title.sync=&quot;doc.title&quot;
 /&gt;</code></pre><h4 id="자식-컴포넌트에서-이벤트-emit-시">자식 컴포넌트에서 이벤트 emit 시</h4>
<pre><code>this.$emit(&#39;update.title&#39;, newTitle)</code></pre><h3 id="주의">주의</h3>
<p>v-bind &amp; .sync 수식어는 표현식과 함께 동작하지 않음</p>
<pre><code>:visible.sync=&quot;isOneFlag &amp;&amp; isTwoFlag&quot; (X)
:visible.sync=&quot;isFlag&quot; (O)</code></pre><p>출처
vue.js 공식 사이트 
<a href="https://kr.vuejs.org/v2/guide/components-custom-events.html">https://kr.vuejs.org/v2/guide/components-custom-events.html</a></p>
]]></description>
        </item>
    </channel>
</rss>