<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ella-front-dev.log</title>
        <link>https://velog.io/</link>
        <description> Frontend Developer</description>
        <lastBuildDate>Fri, 28 Jul 2023 08:12:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ella-front-dev.log</title>
            <url>https://images.velog.io/images/ella-front-dev/profile/a9bc77dd-dfbc-45e0-9c54-3810675cfa2b/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ella-front-dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ella-front-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[JS] Lodash]]></title>
            <link>https://velog.io/@ella-front-dev/JS-Lodash</link>
            <guid>https://velog.io/@ella-front-dev/JS-Lodash</guid>
            <pubDate>Fri, 28 Jul 2023 08:12:26 GMT</pubDate>
            <description><![CDATA[<p><a href="https://lodash.com/docs/4.17.15">Lodash Documentation</a></p>
<ul>
<li><p><strong><code>_.isNil(value)</code> :</strong> Checks if <code>value</code> is <code>null</code> or <code>undefined</code>.</p>
</li>
<li><p><code>_.omitBy(targetObj, predicate)</code></p>
<p>  <a href="https://webisfree.com/2022-08-31/%5Blodash%5D-omitBy()-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">[lodash] omitBy() 알아보기</a></p>
<pre><code class="language-jsx">  extSearch(xpos, ypos, keyword, sort) {
    const EXT_SEARCH_URL = &#39;/external/extsearch?&#39;

    const params: any = _.omitBy(
      {
        xpos: xpos,
        ypos: ypos,
        keyword: keyword,
        sort: sort,
      },
      _.isNil.bind(_),
    )

    console.log(&#39;extSearch / params&#39;, params)

    return axios.get(EXT_SEARCH_URL, { params: params }).then((response) =&gt; {
      return Promise.resolve(response.data)
    })
  }</code></pre>
</li>
<li><p><code>_.find()</code> :</p>
<p>  <a href="https://webisfree.com/2018-07-28/lodash-find()-%EA%B0%9D%EC%B2%B4-%EA%B0%92-%EC%B0%BE%EA%B8%B0">lodash find() 객체 값 찾기</a></p>
<pre><code class="language-jsx">  const pickup: any = _.find(this.freightDataStructure.destinations, {
    type: &#39;PICKUP&#39;,
  })</code></pre>
</li>
<li><p><code>_.isEmpty()</code> :</p>
<p>  <a href="https://webisfree.com/2018-07-27/lodash-isempty()-%EC%A0%95%EB%B3%B4-%EB%B0%8F-%EC%98%88%EC%A0%9C">lodash isEmpty() 정보 및 예제</a></p>
</li>
<li><p><code>_.isEqual()</code> :</p>
<pre><code class="language-jsx">  selectItem(item) {
    if (_.isEqual(this.addressType, &#39;pickUp&#39;))
      this.$emit(&#39;pickUpAddressItem&#39;, item)
    else if (_.isEqual(this.addressType, &#39;dropOff1&#39;))
      this.$emit(&#39;dropOffAddressItem1&#39;, item)
    else if (_.isEqual(this.addressType, &#39;dropOff2&#39;))
      this.$emit(&#39;dropOffAddressItem2&#39;, item)
    else if (_.isEqual(this.addressType, &#39;route&#39;))
      this.$emit(&#39;routeAddressItem&#39;, item)

    this.selectedItem = item.parceladdress
    this.menuOpen = false
  }</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230711]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230711</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230711</guid>
            <pubDate>Tue, 11 Jul 2023 09:04:36 GMT</pubDate>
            <description><![CDATA[<h2 id="loadsh">Loadsh</h2>
<p><a href="https://lodash.com/docs/4.17.15">Lodash Documentation</a></p>
<ul>
<li><p><strong>**<code>_.isNil(value)</code> :</strong> Checks if <code>value</code> is <code>null</code> or <code>undefined</code>.</p>
</li>
<li><p><code>_.omitBy(targetObj, predicate)</code></p>
<p>  <a href="https://webisfree.com/2022-08-31/%5Blodash%5D-omitBy()-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">[lodash] omitBy() 알아보기</a></p>
<pre><code class="language-jsx">  extSearch(xpos, ypos, keyword, sort) {
    const EXT_SEARCH_URL = &#39;/external/extsearch?&#39;

    const params: any = _.omitBy(
      {
        xpos: xpos,
        ypos: ypos,
        keyword: keyword,
        sort: sort,
      },
      _.isNil.bind(_),
    )

    console.log(&#39;extSearch / params&#39;, params)

    return axios.get(EXT_SEARCH_URL, { params: params }).then((response) =&gt; {
      return Promise.resolve(response.data)
    })
  }</code></pre>
</li>
<li><p><code>_.find()</code> :</p>
<p>  <a href="https://webisfree.com/2018-07-28/lodash-find()-%EA%B0%9D%EC%B2%B4-%EA%B0%92-%EC%B0%BE%EA%B8%B0">lodash find() 객체 값 찾기</a></p>
<pre><code class="language-jsx">  const pickup: any = _.find(this.freightDataStructure.destinations, {
    type: &#39;PICKUP&#39;,
  })</code></pre>
</li>
<li><p><code>_.isEmpty()</code> :</p>
<p>  <a href="https://webisfree.com/2018-07-27/lodash-isempty()-%EC%A0%95%EB%B3%B4-%EB%B0%8F-%EC%98%88%EC%A0%9C">lodash isEmpty() 정보 및 예제</a></p>
</li>
<li><p><code>_.isEqual()</code> :</p>
<pre><code class="language-jsx">  selectItem(item) {
    if (_.isEqual(this.addressType, &#39;pickUp&#39;))
      this.$emit(&#39;pickUpAddressItem&#39;, item)
  }</code></pre>
</li>
</ul>
<h3 id="auth20-와--jwt의-차이">Auth2.0 와  JWT의 차이</h3>
<p><a href="https://gilssang97.tistory.com/55">OAuth 2.0, JWT</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230705]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230705</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230705</guid>
            <pubDate>Wed, 05 Jul 2023 11:48:10 GMT</pubDate>
            <description><![CDATA[<h2 id="git">Git</h2>
<h3 id="git-pull-rebase">git pull —rebase</h3>
<p><a href="https://jasonspace.tistory.com/11">&quot;git pull&quot;  vs  &quot;git pull --rebase&quot;</a></p>
<ul>
<li><p>문제 : pull 할때 merge가 되면서 히스토리가 남게 된다. git tree를 깔끔하게 관리하기위해서 현재 프로젝트에서는 남기지 않는다고 한다.</p>
</li>
<li><p>방법 : 발생하는 이유는 내가 해당 브런치 원격에 push하지 않은 commit이 있을 때 pull을 받게되면 생성된다.</p>
<ul>
<li>commit이 있을때는 <code>git pull —rebase</code>을 사용해서 pull을 받으면 된다.</li>
</ul>
</li>
<li><p>해결: <code>git pull —rebase</code> 이용</p>
</li>
</ul>
<h3 id="원격에-올라간--commit-되돌리기">원격에 올라간  Commit 되돌리기</h3>
<ul>
<li><p>문제 : 잘못된 Merge Commit이 원격까지 올라가서 이를 되돌려야했다.</p>
</li>
<li><p>방법 : 2가지 방법을 찾았다.</p>
<ul>
<li><p>방법1 &gt; 히스토리 없이 로컬에서 커밋 되돌린 후 강제 푸시</p>
<p>  <code>git reset —hard HEAD~1 / git reset —hard ‘커밋id’</code> : 커밋 되돌리기
  <code>git push -f origin [원격 브런치]</code> : 강제  푸시</p>
<p>  <strong>*주의 사항</strong> : 협업할때 해당 원격에 올린 커밋을 다른 팀원이 받아가게되면 이전 소스가 그대로 원복 될 수 있다.</p>
</li>
<li><p>방법 2&gt; 히스토리 남기고 되돌리기
  <code>git revert —hard ‘커밋id&#39;</code>: 하나의 커밋만 revert
  <code>git revert --no-commit HEAD~3</code> : 다수의  커밋을 revert
  <code>git commit -m &#39;Revert: &quot;Commit 문구&quot;</code>: Revert 했다는 커밋메시지 작성 후 PUSH</p>
</li>
</ul>
</li>
</ul>
<pre><code>[원격 저장소에 올라간 커밋 되돌리기](https://jupiny.com/2019/03/19/revert-commits-in-remote-repository/)</code></pre><ul>
<li>해결 : 현재 메시지가 깨끗하게 유지되어야해서 히스토리가 남으면 안되었다.  그래서 모든 팀원에게 얘기하고 그들이 풀받기 전에 방법 1로  적용했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230629]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230629</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230629</guid>
            <pubDate>Thu, 29 Jun 2023 11:58:16 GMT</pubDate>
            <description><![CDATA[<h2 id="vue">Vue</h2>
<ul>
<li>storeToRefs :  <code>&lt;script setup&gt;</code> 환경에서 store의 state나 Action을 가져올때 반응형으로 가져올 수 있도록 해주는 pinia  메서드</li>
</ul>
<p><a href="https://mine-it-record.tistory.com/641">[VueJS] Pinia Store 기본 사용법 (ft. storeToRefs)</a>
<a href="https://pinia.vuejs.org/core-concepts/">Pinia 🍍</a></p>
<hr>
<h2 id="js">JS</h2>
<h3 id="async-await-여러개-실행-이슈">Async Await 여러개 실행 이슈</h3>
<pre><code class="language-jsx">/** 
    해당 로직은 async await를 then...catch를 사용해서 에러처리한 코드이다.
    각각의 단계에 따라서 다른 에라가 출력 되어야하는데 잘 되지 않아서 이슈가 발생했다.
**/
const onSignup = async () =&gt; {
  await signup(id, password)
    .then(async () =&gt; {
      await signIn(id, password)
        .then(async (response: any) =&gt; {
          await useUserInfo(response.data)
            .then(async () =&gt; {
              await accessToken(id, password)
                .then(() =&gt; onModify())
                .catch((error: any) =&gt; {
                  const errRespData: any = error.response.data;
                  console.log(&quot;Error : 접근키 오류 : &quot;, errRespData)})
            .catch(() =&gt; {
                const errRespData: any = error.response.data;
                console.log(&quot;Error : 회원정보 저장 오류 : &quot;, errRespData)})
            })
        .catch((error: any) =&gt; {
            const errRespData: any = error.response.data;
            console.log(&quot;Error : 로그인 오류 : &quot;, errRespData)})
        })
    .catch((error: any) =&gt; {
        const errRespData: any = error.response.data;
        console.log(&quot;Error : 계정 생성 오류 : &quot;, errRespData)})
    });
};</code></pre>
<ul>
<li>위이슈에 대해서 async await와 then...catch를 여러개 함께쓰면 혹시 문제가 생기는지 <a href="https://codepen.io/eunjoo-yoon/pen/jOQBobE?editors=1011">예제</a>를 만들어서 확인해봤다</li>
</ul>
<ul>
<li>map
  <a href="https://goddino.tistory.com/328">[js] 배열 map으로 object에 key 추가하기, value 추가</a></li>
</ul>
<hr>
<h2 id="coding-test">Coding Test</h2>
<h3 id="pagination-구현">Pagination 구현</h3>
<p><a href="https://velog.io/@eunoia/JS%EB%A1%9C-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">JS로 Pagination 구현하기</a></p>
<hr>
<h2 id="git">Git</h2>
<h3 id="gitlab">gitlab</h3>
<ul>
<li><p>gitlab  permission deny</p>
<pre><code class="language-bash">  /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell: 
  Unable to configure logging: 
  open /var/log/gitlab/gitlab-shell/gitlab-shell.log: 
  permission denied, Unix syslog delivery error</code></pre>
<p>  ⇒ 딱히 방법없어서 ssh 접속 말고 그냥 url 접속으로  해결(git remote set-url로 기존 url 변경)</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230524]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230524</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230524</guid>
            <pubDate>Tue, 23 May 2023 15:01:03 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[[Env] Git Multi Account 설정]]></title>
            <link>https://velog.io/@ella-front-dev/Env-Git-Multi-Account-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@ella-front-dev/Env-Git-Multi-Account-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 14 Apr 2023 04:48:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-ssh-설정">1. ssh 설정</h2>
<h3 id="1-1-ssh-keygen-생성">1-1. ssh-keygen 생성</h3>
<pre><code class="language-shell">    # .ssh로 이동
    cd ~/.ssh

    # ssh-keygen 생성
    # 1&gt;
    ssh-keygen -t rsa -b 4096 -C &quot;userA@gmail.com&quot;
    Generating public/private rsa key pair.

    # 2&gt;
    ssh-keygen -t rsa -C &quot;userA@my_email.com&quot; -f &quot;id_rsa_userA&quot;

    # key 이름 설정
    Enter file in which to save the key (/Users/ella/.ssh/id_rsa): id_rsa_userA

    # 보안을 위해서 설정해야하지만...하지 않음
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in id_rsa_store
    Your public key has been saved in id_rsa_store.pub
    The key fingerprint is:
    SHA256:00000000000000~~~~~~~userA@gmail.com
    The key&#39;s randomart image is:
    +---[RSA 4096]----+
    |. o     .        |
    | = ..  o .       |
    |  BE+ = .        |
    | *.* O + .       |
    |ooO B * S        |
    |+*o@ = . .       |
    |=o*.o +          |
    |oo  .. .         |
    |  .. ..          |
    +----[SHA256]-----+

    # 확인
    &gt; .ssh  ls   
      id_rsa_userA     
      id_rsa_userA.pub 
      id_rsa_userB       
      id_rsa_userB.pub
</code></pre>
<h3 id="1-2-ssh-키-등록">1-2. ssh 키 등록</h3>
<pre><code class="language-shell"># ssh키 등록
  ssh-add id_rsa_userA
</code></pre>
<br>

<h3 id="1-3-ssh-key-daemon-추가-및-권한-확인">1-3. ssh key daemon 추가 및 권한 확인</h3>
<pre><code class="language-shell"># ssh key daemon 추가
  $ eval &quot;$(ssh-agent -s)&quot; &amp;&amp;\
   ssh-add -K id_rsa_userA 

# 권한 확인
  ssh-add -l
</code></pre>
<br>

<h3 id="1-3-ssh-config-작성하기">1-3. ssh config 작성하기</h3>
<pre><code class="language-shell">
# 개인용 계정
  Host userA
     HostName github.com
     User git
     IdentityFile ~/.ssh/id_rsa_userA

# 회사용 계정
  Host userB
     HostName github.com
     User git
     IdentityFile ~/.ssh/id_rsa_userB</code></pre>
<br>

<hr>
<h2 id="2-git-설정">2. git 설정</h2>
<h3 id="2-1-gitconfig-global-설정">2-1. .gitconfig global 설정</h3>
<pre><code class="language-shell"># mac에서 git global 설정 수정
  git config --global --edit

# git global setting
  [includeIf &quot;gitdir:~/DEV/GIT-Store&quot;]
   path = DEV/GIT-Store.gitconfig
  [includeIf &quot;gitdir:~/DEV/GIT-Project&quot;]
   path = DEV/GIT-Project/.gitconfig

# GIT-Store폴더로 .gitconfig 파일 생성
  cd ~/DEV/GIT-Store
  vi .gitconfig

# git local setting
  [user]
        name = &quot;[git name / ex&gt; EllaDev]&quot;
        email = &quot;[git email]&quot;
  [github]
        user = &quot;[git nickname / ex&gt; ella-front-dev]&quot;
</code></pre>
<br>

<h3 id="2-2-원격-git-ssh-설정">2-2. 원격 git ssh 설정</h3>
<ol>
<li>ssh public key 복사<pre><code class="language-shell"># 복사 방법 1&gt;
cat id_rsa_userA.pub | pbcopy
</code></pre>
</li>
</ol>
<h1 id="복사-방법-2">복사 방법 2&gt;</h1>
<p>  pbcoby &lt; id_rsa_userA.pub</p>
<pre><code>2. github에 접속
3. 접속 후 settings &gt; SSH and GPG keys로 이동
4. New SSH Key 설정

&lt;br&gt;

----

## 3. 테스트
### ssh daemon 테스트
``` shell
# ssh -T git@{Host}로 테스트
&gt; ssh -T git@userA</code></pre><h3 id="각-계정의-git-clone-가능한지-확인">각 계정의 git clone 가능한지 확인</h3>
<pre><code class="language-shell"># userA 계정 저장소 clone
&gt; git@userA:userA/repository.git

# userB 계정 저장소 clone
&gt; git@userB:userB/repository.git</code></pre>
<p>** Ref&gt; **</p>
<p><a href="https://velog.io/@jay/multiplegithubaccounts">한 컴퓨터에서 여러 개의 깃허브 계정 사용하기</a>
<a href="https://yangeok.github.io/git/2020/03/08/ssh-multiple-account.html">머신 한 대에서 GIT 계정 여러개 사용하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230320]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230320</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230320</guid>
            <pubDate>Mon, 20 Mar 2023 09:39:17 GMT</pubDate>
            <description><![CDATA[<h1 id="to-do-list">To do list…</h1>
<h3 id="운영인증-signup에-커뮤니티쪽-추천인-넣기--나원오"><del>[운영/인증] Signup에 커뮤니티쪽 추천인 넣기 / 나원오</del></h3>
<ul>
<li>화요일에 오픈</li>
<li>따로 브랜치 따고 금요일에 개발에 올려서 테스트해보기</li>
<li>[중요]QA에 올릴 때 해당 소스 삭제해서 올리기</li>
</ul>
<h3 id="개발지갑--sendswap-수수료-관련-ui-수정--허준녕">[개발/지갑 ] Send/Swap 수수료 관련 UI 수정 / 허준녕</h3>
<pre><code> 1. LMC
   - 토큰 전송 :  보유잔액 &lt; 금액 입력 = &quot;보유잔액이 부족합니다.&quot;
   - 스왑 : 보유잔액 &lt; (금액 입력 + 수수료) = &quot;보유잔액이 부족합니다.&quot;
 2. ETH
   - ERC20 전송 : 토큰 보유잔액 &lt; 토큰 금액 입력 = &quot;보유잔액이 부족합니다.&quot; / ETH 보유잔액 &lt; 수수료 = &quot;보유잔액이 부족합니다.&quot;
   - ETH 전송 : 보유잔액 &lt; (금액 입력 + 수수료) = &quot;보유잔액이 부족합니다.&quot;
   - 스왑 : LM 보유잔액 &lt; LM 금액 입력 = &quot;보유잔액이 부족합니다.&quot; / ETH 보유잔액 &lt; 수수료 = &quot;보유잔액이 부족합니다.&quot;</code></pre><hr>
<h1 id="today-add-list">Today add list…</h1>
<h3 id="운영지갑-이더-네트워크-수수료-fix-값도-sendswap-할때-같이-보내주기-허준녕"><del>[운영/지갑] 이더 네트워크 수수료 fix 값도 send/swap 할때 같이 보내주기/ 허준녕</del></h3>
<ul>
<li>이유? 보내는 동안 수수료가 변해서 출금이나 이체가 안되는 경우가 있기 때문에</li>
</ul>
<h3 id="운영인증-회원가입-이름-띄어-쓰기-가능하도록-정규식-수정--나원오"><del>[운영/인증] 회원가입 이름 띄어 쓰기 가능하도록 정규식 수정 / 나원오</del></h3>
<ul>
<li>이유 : 다국어 서비스로 외국인들도 가능하도록하기 위해서</li>
</ul>
<pre><code class="language-tsx">//before
const regExp = /^[ㄱ-ㅎ|가-힣|a-z|A-Z]+$/

// after
const regExp = /^[ㄱ-ㅎ|가-힣|a-z|A-Z|\s]+$/</code></pre>
<p>⇒  validate 파일 수정 후, 회원가입 폼에서 replace 시키는 부분 삭제 및 수정</p>
<h3 id="운영인증-이메일-정규식-수정signin--나원오"><del>[운영/인증] 이메일 정규식 수정(signin) / 나원오</del></h3>
<ul>
<li>이유 : 플레이놈 정규식과 같게 해야 해당 서비스 회원들도 가입이 가능하므로</li>
</ul>
<pre><code class="language-tsx">// Before
const regExp =
      /^[0-9a-zA-Z_,.]+[0-9a-zA-Z]*@[0-9a-zA-Z]{1,255}\.[a-zA-Z]{1,3}/i

// After
const regExp = /[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9.]+/g</code></pre>
<h3 id="운영지갑-swapsend에-고정된-수수료를-보내기"><del>[운영/지갑] Swap/Send에 고정된 수수료를 보내기</del></h3>
<ul>
<li>이유 : API가 처리되는 동안 수수료가 변해서 제대로 기능 실행이 안된다.</li>
</ul>
<h3 id="운영지갑-swap-시킬때-이중으로-api-쏘는-것-막기"><del>[운영/지갑] Swap 시킬때 이중으로 API 쏘는 것 막기</del></h3>
<ul>
<li>이유 : 현재 swap 할때 시간이 많이 소요되므로 사용자가 이중으로 클릭 할 수 있다.]</li>
<li>해결방법 : send와 같이 진행중이라는 팝업을 띄우고 해당 홈을 이동하도록 한다.</li>
</ul>
<h3 id="운영지갑-공통-alert--confirm-popup-다국어-문제"><del>[운영/지갑] 공통 Alert &amp; Confirm Popup 다국어 문제</del></h3>
<ul>
<li>이슈 : 언어를 다르게 설정하는 것에 따라 다국어 설정이 제대로 되지 않는다</li>
<li>원인 : store에 있는 default popup 값이 정적으로 설정되어있어서 나타는 현상</li>
<li>해결 : 각각의 alert / confirm 팝업에 각각의 default popup 값을  넣는다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 230307]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-230307</link>
            <guid>https://velog.io/@ella-front-dev/TIL-230307</guid>
            <pubDate>Wed, 08 Mar 2023 01:50:10 GMT</pubDate>
            <description><![CDATA[<h2 id="업무">업무</h2>
<h3 id="이슈-빌드-후pm2환경에-로그인하면-에러돔페이지를-리턴">[이슈] 빌드 후(pm2환경)에 로그인하면 에러돔페이지를 리턴</h3>
<h4 id="원인예상1">원인예상1</h4>
<ul>
<li>아래와 같이 중복로그인 체크 API에서 오류가 난다.<pre><code>  duplicateResult = await AuthService.checkDuplicateLogin({
          userSn: params.userSn,
          loginUuid,
        })</code></pre></li>
<li>해당 API를 체크했지만 따로</li>
</ul>
<h4 id="원인예상2">원인예상2</h4>
<ul>
<li>CORS 오류 ⇒ 이유? 따로 에러를 주지 않아서 예상해봄</li>
</ul>
<h4 id="원인예상3">원인예상3</h4>
<ul>
<li>히스토리 체크 후,devProxy를 설정했는데 위 API에서 나는 에러를 해당프록시서버 api주소로 &#39;401&#39;에러를 뱉어낸다. 근데 이를 로컬서버주소로 바뀌지 않아서 만들어진 이슈이다.</li>
</ul>
<h4 id="결론">결론</h4>
<ul>
<li>해당 이슈는 배포 관련 히스토리를 몰라서 만들어진 이슈였고 서비스에 따라서 배포과정이나 배포 설정등이 다를 수 있다는 걸 알게 되었다. </li>
</ul>
<h2 id="스터디">스터디</h2>
<h3 id="tech">Tech</h3>
<h4 id="startwidth">startWidth</h4>
<p><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith">String.prototype.startsWith() - JavaScript | MDN</a></p>
<h4 id="useasyncstate">useAsyncState</h4>
<p><a href="https://vueuse.org/core/useAsyncState/">VueUse</a></p>
<h4 id="dayjs">dayjs</h4>
<p><a href="https://day.js.org/">dayjs</a></p>
<hr>
<h3 id="env">Env</h3>
<p>배포 관련해서 환경변수별 환경설정</p>
<ul>
<li>현재 지갑서비스는</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Work] 커뮤니티 서비스 개발 회고(1)]]></title>
            <link>https://velog.io/@ella-front-dev/Work-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@ella-front-dev/Work-%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 24 Feb 2023 02:33:00 GMT</pubDate>
            <description><![CDATA[<h1 id="첫-프로젝트를-끝내고">첫 프로젝트를 끝내고...</h1>
<p>작년부터 프리랜서로 일하면서 블록체인 관련 프로젝트를 계속 해보고 싶었는데 좋은 기회에 맡게 되어서 설레는 마음으로 프로젝트를 시작하게 되었다. 3개월이라는 짧은 데드라인을 알고 시작하는만큼 힘든 과정이 될꺼라고 짐작했지만 막바지에는 정신적인 압박이 상당했다. 다행히 함께 일하게 된 시니어 개발자님과 함께여서 서로 으싸으싸하면서 험난한 고난들을 이겨낼 수 있었다. </p>
<p>함께 일하는 분들 모두 도와주셔서 좋은 분위기에서 일 할 수 있었으나 외주 기획과 외주 디자이너와의 커뮤니케이션 미스로 인한 일정 딜레이 및 미비한 기획으로 인한 추후 수정 및 개선사항 상당히 많았다. </p>
<p>특히 이번 프로젝트에서 퍼블리싱까지 함께 맡게 되면서 반응형을 하게되었는데.. 기획이며 디자인에 반응형이 제대로 적용되어있지 않아서 굉장히 곤란했다. 계속 커뮤니케이션이 가능한 상주인원이라면 괜찮았겠지만 외주이다보니 그것 또한 여의치 않았다.
다행인건 이런 상황에서 모든 경우의 수를 생각하신 경험많고 유능한 팀장님 덕분에 제시간에 서비스 테스트까지 모두 진행 할 수 있었다는 것이다.
그래서 힘든 과정이었음에도 커뮤니케이션과 일정 관련 부분에 있어서도 많은 걸 얻어 갈 수 있는 프로젝트였다.</p>
<p>물론 기술적인 부분에서 가장 성장 할 수 있게 해준 프로젝트이기도 했다. 그 부분은 아래서 더 자세한 설명 할 것이다.</p>
<br>

<br>

<hr>
<h1 id="커뮤니티-서비스에-대하여">커뮤니티 서비스에 대하여...</h1>
<blockquote>
<h3 id="프론트엔드-기본-스펙">프론트엔드 기본 스펙</h3>
</blockquote>
<ul>
<li>Nuxt3 SSR</li>
<li>Typescript</li>
<li>Pinia</li>
<li>Vite</li>
<li>Eslint/Prettier</li>
</ul>
<p>커뮤니티 서비스는 처음이였고 SSR과 Typescript는 토이프로젝트로 만들어보았지 직접 서비스를 만드는 건 처음이라 정말 막막했다. 거기다가... nuxt3을 사용하면서 처음 사용해보는 Composition API는 나를 더 혼란스럽게 만들었다. </p>
<p>물론 React로도 작업을 해봤기 때문에 금방 적응하긴 했지만...이전 Vue2에 익숙해져있던 생각회로가 가끔 오작동으로 엉키는 건 어쩔 수 없었다.
위의 시행 착오와 개발 과정에 대해서는 회고록 Version2에서 자세히 다루도록 하겠다. </p>
<br>

<h2 id="커뮤니티에는-어떤-기능들이">커뮤니티에는 어떤 기능들이?</h2>
<h3 id="basic-crud">Basic CRUD</h3>
<p>기본적인 CRUD기능을 가지고 Rest API로 API통신을 하는 게시판 형태의 서비스이다. 
Api 통신에 있어서 백엔드와 협의한 내용은 Paginaion 정보를 header로 받고 Wrapper가 있는 response가 갈 수 있다는 것이었다. 기획이 완료되지 않은 시점이라서 유연하게 만들어야해서 간략하게 Wrapper로 response를 보낼때 형식을 협의했다. Api interceptor를 이용해서 협의한 형식의 에러코드를 읽어 실제 에러로 변환해서 내려줄 수 있도록 셋팅했다. 이유는 디버깅이 용의하도록 하기 위해서 이와 같이 셋팅하였다. </p>
<h3 id="editor">Editor</h3>
<p>에디터 기능 구현을 위해 &quot;Tiptap&quot;라이브러리를 커스터마이징하여 제작되었다. 예를들어 기본적으로 사진이나 동영상이 구분되어 올라가야 했고 기존 라이브러리의 파일을 올리는 방법이 다소 생소해서 익숙한 방법으로 수정했다. 또한 백엔드 개발자분들과 기능 협의 또한 필요했다. 
이미지 업로드에 대해서 한번에 보내는것은 시간이 많이 걸려서 회의 후에 사진을 올릴때마다 API를 날려 디비에 임시로 저장하고 S3 이미지 주소를 가지고 에디터에 삽입하는 방법을 택했다. 이와 같이 기획에 맞춰 기능 수정이 들어갔다.</p>
<h3 id="recycler-view">Recycler view</h3>
<p>다양하고 많은 양의 포스팅(데이터)이 리스트업 되어야하므로 성능이슈를 최소화하기 위해서 Recycler view 구현하게 되었다.</p>
<h3 id="share--open-graph">Share / Open Graph</h3>
<p>Share를 위해서도 Meta태그에 OG를 삽입하여 각 소셜에서 공유 가능하도록 구현했다.
또한 우리 포스팅 안에도 Open Graph기능을 구현하여 넣었다. 이 과정에서 몇가지 시행착오가 있었다. </p>
<h3 id="seo">SEO</h3>
<p>각 포스팅마다 상세페이지를 따로 만들어서 SEO가 가능하도록 만들었다.</p>
<h3 id="gana">GA/NA</h3>
<p>1차 제작으로 간단한 GA와 NA만 구현하고 추후에 늘려가는 방향으로 정해졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221230]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221230</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221230</guid>
            <pubDate>Fri, 30 Dec 2022 14:00:15 GMT</pubDate>
            <description><![CDATA[<h3 id="이슈--공통-컴포넌트화">이슈 : 공통 컴포넌트화</h3>
<ul>
<li><p>문제1 : Modal Common에서 버튼의 액션을 부모가 제어할 수 없다. 그럼 부모에서 관련 데이터값을 모두 가지고 있어야한다. 그게… 복잡한 구현이 될 것이므로 다르게  좀더 깔끔하게 가져다 쓸 수있으려면….은?</p>
</li>
<li><p>해결방안1 : 데이터만 갖는 컨테이너 파일를 만들어서 거기서 모달을 호출하는 방식으로 해결한다.</p>
<pre><code class="language-html">  &lt;!-- @/pages/index.vue **/ --&gt;
  &lt;template&gt;
    &lt;div&gt;
      &lt;BaseButtonsText
        v-if=&quot;userStore.user&quot;
        :label=&quot;&#39;Create Post&#39;&quot;
        :theme=&quot;&#39;primary-yellow&#39;&quot;
        @click=&quot;createModal = !createModal&quot;
      /&gt;

      &lt;PostCreate
        :is-open=&quot;createModal&quot;
      /&gt;
    &lt;/div&gt;
  &lt;/template&gt;
  &lt;script&gt;
  export default defineComponent({
    components: {
      BaseButtonsText,
      PostCreate
      // BoxModalsModalCreatePost
    },
    setup() {
      const handleOnOpenModalPostCreate = () =&gt; {
        $vfm.toggle(&#39;modal-create-post-test&#39;, true).then(hook =&gt; {
          console.log(hook)
        })
        console.log($vfm.get(&#39;modal-create-post-test&#39;))
      }
      return { createModal, handleOnOpenModalPostCreate }
    }
  })
  &lt;/script&gt;

  &lt;!-- @/component/box/modal/PostCreate/index.vue --&gt;
  &lt;template&gt;
    &lt;div&gt;&lt;/div&gt;
  &lt;/template&gt;

  &lt;script lang=&#39;ts&#39;&gt;

  export default defineComponent({
    name: &#39;PostCreate&#39;,
    props: {
      isOpen: {
        required: true,
        type: Boolean
      }
    },
    setup(props) {
      const isOpen = toRefs(props).isOpen

      watch(isOpen,(current)=&gt; {
        if (current) handleOnCreatePostModal()
      })

      const handleOnCreatePostModal = () =&gt; {
        $vfm.show({
          component: ModalCommon,
          bind: {
            name: &#39;modalCommon&#39;,
            showCloseBtn: true
          },
          on: {...},
          slots: {
            title: &#39;포스트작성&#39;,
            contents: { component: PostCreateContent },
            actions: {
              component: BaseButtonsText
            }
          }
        })
      }
      return{}
    }
  })
  &lt;/script&gt;

  &lt;!-- @/component/base/modal/ModalCommon.vue **/ --&gt;
  &lt;template&gt;
    &lt;vue-final-modal
      v-slot=&quot;{ params, close }&quot;
      v-bind=&quot;$attrs&quot;
      classes=&quot;modal-container&quot;
      content-class=&quot;modal-content&quot;
    &gt;
      &lt;div v-if=&quot;slots.title || $attrs.showCloseBtn&quot; class=&quot;modal-header&quot;&gt;
        &lt;h4 v-if=&quot;slots.title&quot; class=&quot;title&quot;&gt;
          &lt;slot name=&quot;title&quot;&gt;&lt;/slot&gt;
        &lt;/h4&gt;

        &lt;div v-if=&quot;$attrs.showCloseBtn&quot; class=&quot;close-wrap&quot; @click=&quot;close&quot;&gt;
          &lt;BaseButtonsIcon
            :icon=&quot;{ type: &#39;outline&#39;, name: &#39;close-extend&#39; }&quot;
            class=&quot;btn-close&quot;
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;ScrollContainer
        v-if=&quot;slots.contents&quot;
        class=&quot;modal-contents modal-scroll-container&quot;
      &gt;
        &lt;slot name=&quot;contents&quot; :params=&quot;params&quot;&gt;&lt;/slot&gt;
      &lt;/ScrollContainer&gt;

      &lt;div v-if=&quot;slots.btnCancel || slots.btnConfirm || slots.btnOptional&quot; class=&quot;modal-actions&quot;&gt;
        &lt;slot name=&quot;btnCancel&quot; &gt;&lt;/slot&gt;
        &lt;slot name=&quot;btnConfirm&quot; &gt;&lt;/slot&gt;
        &lt;slot name=&quot;btnOptional&quot; &gt;&lt;/slot&gt;
      &lt;/div&gt;
    &lt;/vue-final-modal&gt;
  &lt;/template&gt;
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent , useSlots } from &#39;vue&#39;

  export default defineComponent({
    name: &#39;ModalCommon&#39;,
    inheritAttrs: false,
    emits: {
      confirm: (_evt: any) =&gt; true,
      cancel: (_evt: any) =&gt; true
    },
    setup() {
      const slots = useSlots()

      return { slots }
    }
  })
  &lt;/script&gt;</code></pre>
</li>
</ul>
<ul>
<li><p>해결방안2: CustomModal 형태로 여기서 제공하는 것들을 커스터마이징해서 만들어서 사용한다.</p>
<p>  <a href="https://vue-final-modal.org/examples/recommend">Recommended usage</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL]221228]]></title>
            <link>https://velog.io/@ella-front-dev/TIL221228</link>
            <guid>https://velog.io/@ella-front-dev/TIL221228</guid>
            <pubDate>Wed, 28 Dec 2022 23:26:14 GMT</pubDate>
            <description><![CDATA[<h2 id="이슈">이슈</h2>
<h3 id="해시태그">해시태그</h3>
<ul>
<li><p>문제 : dynamic ref를 설정해서 getBoundingClientRect() 값을 가져오지 못한다.</p>
</li>
<li><p>원인 : Element tag가 아니라 Component를 loop 돌리기 때문이다.</p>
</li>
<li><p>해결 :</p>
<pre><code class="language-html">  //@/components/base/create-hashtag/TagInput.vue
  &lt;template&gt;
  ...
      &lt;HashtagMenu
        v-for=&quot;(item, index) in filterTags&quot;
        :ref=&quot;el=&gt; {menuItemRef[index]= el}&quot;
        :key=&quot;index&quot;
        :index=&quot;index&quot;
        :item=&quot;item&quot;
        :active-index=&quot;choiceTagsIndex&quot;
        @click=&quot;clickOnMenu(item)&quot;
      /&gt;
  ...
  &lt;/template&gt;
  &lt;script&gt;
  export default defineComponent({
      setup(props, { emit }) {
          const menuItemRef = ref&lt;typeof HashtagMenu[]&gt;([])
          const handleOnChoiceTag = () =&gt;{
              //변경 전
               const scrollPosition = menuItemRef.value[choiceTagsIndex.value].getBoundingClientRect()
              //변경 후
             const scrollPosition = menuItemRef.value[choiceTagsIndex.value].$el.getBoundingClientRect()
      }
      }
  })
  &lt;/script&gt;</code></pre>
</li>
<li><p>문제2: 해당 아이템이 window scroll 기준 포지션을 나타낸다. 내부 컨테이너 안의 아이템을 ScrollTop을 시켜주는 방법은?</p>
</li>
<li><p>해결:</p>
<ul>
<li><p>방법) scrollIntoView() 사용</p>
<p>  <a href="https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView">element.scrollIntoView - Web API | MDN</a></p>
</li>
</ul>
</li>
</ul>
<pre><code>```jsx
export default defineComponent({
    setup(props, { emit }) {
        const menuItemRef = ref&lt;typeof HashtagMenu[]&gt;([])
        const handleOnChoiceTag = () =&gt;{
             menuItemRef.value[choiceTagsIndex.value].scrollIntoView()
    }
    }
})
```</code></pre><ul>
<li><p>문제 3: 해당 아이템을 하나 내릴때마다 내부 스크롤이 아이템 하나씩 내려간다.</p>
</li>
<li><p>해결 방안 : 해당 아이템이 화면에 나타나지 않은 것을 감지해서 그때 ScrollTop으로 내려갈 수 있도록 한다.</p>
<ul>
<li><p>아래 함수를 이용해서 해당 아이템이 현재 화면에 있는지 체크 할 수 있다.</p>
<pre><code class="language-jsx">export const detectElementInViewport = (el: Element, scrollContainer: HTMLElement | null) =&gt; {
const rect = el.getBoundingClientRect()
const scrollRect = scrollContainer?.getBoundingClientRect()
return (
  rect.top &gt;= 0 &amp;&amp;
  rect.left &gt;= 0 &amp;&amp;
  rect.bottom &lt;= scrollContainer!.clientHeight + scrollRect!.top &amp;&amp;
  rect.right &lt;= scrollContainer!.clientWidth + scrollRect!.right
)
}</code></pre>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="modal-component">Modal Component</h3>
<ul>
<li>vue-final-modal</li>
</ul>
<p><a href="https://github.com/vue-final/vue-final-modal/issues/10">https://github.com/vue-final/vue-final-modal/issues/10</a></p>
<br>

<hr>
<h2 id="node와-element">Node와 Element</h2>
<h3 id="node-list-vs-element-collection">Node List vs Element Collection</h3>
<p><a href="https://medium.com/@layne_celeste/htmlcollection-vs-nodelist-4b83e3a4fb4b">HTMLCollection vs NodeList</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221222]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221222-r52fkyuw</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221222-r52fkyuw</guid>
            <pubDate>Fri, 23 Dec 2022 00:02:04 GMT</pubDate>
            <description><![CDATA[<h2 id="hashtag-component">Hashtag Component</h2>
<h3 id="분석">분석</h3>
<ul>
<li>제목 / 에디터 / 해시태그 ⇒ 글자 제한 Byte 기준으로 계산해주는 라이브러리 사용</li>
<li>에디터<ul>
<li>제목 완료 후, 키보드로 에디터로 이동 가능</li>
<li>한글 5000자(영문 10,000byte) 까지 입력가능</li>
<li>10장까지 등록 가능하고 10장 이상되면 비활성화</li>
<li>글자 최소 10자 이상이어야  저장가능</li>
</ul>
</li>
<li>해시태그<ul>
<li>prefix # 사용</li>
<li>해시태그 등록 : ‘,’ 사용<ul>
<li>최대 30자(60byte) 입력</li>
<li>단어 10개 정도 표시 , 이상은 스크롤</li>
<li>5개 까지 등록가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="기능구현">기능구현</h3>
<ul>
<li><input checked="" disabled="" type="checkbox"> Input창에서 HashtagMenu로 방향키로 이동</li>
<li><input checked="" disabled="" type="checkbox"> ActiveFocus가 방향키로 움직인다.</li>
<li><input checked="" disabled="" type="checkbox"> Enter / Click해서 등록</li>
<li><input checked="" disabled="" type="checkbox"> validation 함수 생성 및 적용</li>
</ul>
<h3 id="sideeffect">SideEffect</h3>
<ul>
<li>이슈 1<ul>
<li>문제 : backspace로 지웠을때 menu창이 안없어진다. 한번 더 backspace를 눌렀을때 없어진다.</li>
<li>원인 : 구조상에 초기화하는 부분이 빠져있었다.</li>
<li>해결 : 전체 조건에 맞게 함수 구조 수정</li>
</ul>
</li>
<li>이슈 2<ul>
<li>문제 : enter를 했을때 menu창이 안없어진다.</li>
<li>원인 : 조상에 초기화하는 부분이 빠져있었다.</li>
<li>해결 : 전체 조건에 맞게 함수 구조 수정</li>
</ul>
</li>
</ul>
<hr>
<h2 id="로딩바">로딩바</h2>
<ul>
<li>전역에서 사용하는 로딩바</li>
<li>각각의 섹션에서 사용히는 로딩바</li>
</ul>
<p>ex) 유투브는 전역 로딩바 없음!! 섹션을 더 중하게 여김</p>
<h3 id="구현방법">구현방법</h3>
<ul>
<li>데이터단(store)에서 로딩바를 구현해서 에러처리까지 하는 방식<ul>
<li>대부분 자동화 되어있고 페이지단에서 따로 손볼 필요가 없다.</li>
<li>구현하는 로직 자체가 복잡해서 안정화 되어있는 프로젝트에서 시간을 들여 만들 수 있을듯하다.</li>
<li>한번 구현하면 편하게 쓸 수 있다.</li>
</ul>
</li>
<li>페이지나 컴포넌트단에서 이벤트를 발생시켜 처리하는 방법</li>
</ul>
<h3 id="현재-프로젝트에서-구현한-방식">현재 프로젝트에서 구현한 방식</h3>
<ul>
<li>Global</li>
</ul>
<pre><code class="language-jsx">// store/layout/index.ts
/* action */
updateLoadingIndicatorGlobal(payload: LayoutType.LoadingIndicator) {
  this.loadingIndicatorGlobal = payload
}

// component/bars/header/Tools.ts
const handleOnLogin = async () =&gt; {
  try {
    layoutStore.updateLoadingIndicatorGlobal(true)

    await Promise.all([
      await userStore.fetchAuthToken(),
      await userStore.fetchUser()
    ])
  } catch (err) {
    console.log(err)
  } finally {
    layoutStore.updateLoadingIndicatorGlobal(false)
  }
}</code></pre>
<ul>
<li>Section</li>
</ul>
<pre><code class="language-jsx">// component/base/ImageContainer.vue

const isTimeout = ref(false)
const isLoadFail = ref(false)
const isLoadDone = ref(false)

const handleOnLoadFail = (evt: Event) =&gt; {
  isLoadDone.value = true
  isLoadFail.value = true
  ;(evt.target as HTMLImageElement).remove()
}
const handleOnLoadDone = () =&gt; {
  isLoadDone.value = true
  isLoadFail.value = false
}

onMounted(() =&gt; {
  setTimeout(() =&gt; {
    if (!isLoadDone.value) {
      isTimeout.value = true
      isLoadDone.value = true
      isLoadFail.value = true
    }
  }, 8000)
})</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221221]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221222</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221222</guid>
            <pubDate>Wed, 21 Dec 2022 23:19:19 GMT</pubDate>
            <description><![CDATA[<h2 id="hashtag-component라이브러리">Hashtag Component(라이브러리)</h2>
<h3 id="타입스크립트">타입스크립트</h3>
<ul>
<li><p>모듈 vs 네임스페이스</p>
<p>  <a href="https://velog.io/@kmp1007s/Typescript-%EB%AA%A8%EB%93%88%EA%B3%BC-%EA%B4%80%EB%A0%A8%EB%90%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0">Typescript의 모듈 시스템</a></p>
</li>
</ul>
<pre><code class="language-jsx">declare module &#39;@johmun/vue-tags-input&#39; {
  type Plugin_2&lt;Option = any&gt; = PluginInstallFunction&lt;Option&gt; &amp; {
    install?: PluginInstallFunction&lt;Option&gt;;
  } | {
    install: PluginInstallFunction&lt;Option&gt;;
  };
  export {Plugin_2 as Plugin}

  type PluginInstallFunction&lt;Option&gt; = (app: App, ...options: Option[]) =&gt; any;

  interface App {
    //use(plugin: Plugin_2, ...options: any[]): this; // this line overrides below (because of any)
    use&lt;Option&gt;(plugin: Plugin_2&lt;Option&gt;, ...options: Option[]): this;
  }

  export const install: PluginInstallFunction&lt;any&gt;;
}.  </code></pre>
<p>⇒ 라이프사이클 문제로 적용 안됨</p>
<br>

<h2 id="hashtag-component">Hashtag Component</h2>
<h3 id="이슈사항">이슈사항</h3>
<ul>
<li><p>input value값이  ‘’ 일때 backspace로 지울때 Tag를 지우는것인 아닌지 값이 ‘’ 인지 아닌지로 체크하는데 마지막 하나 남은걸 지울때 앞의 태그와 함께 지워진다</p>
<ul>
<li>원인 : 이벤트가 일어난 이후에 value 값을 체크 하므로 현재 마지막 값이 지워지는 순간 값을 ‘’으로 인식해서 발생하는 이슈이다.</li>
<li>방법1 : ‘’값을 가지는 순간 Count라는 변수를 줘서 두번째부터 태그가 지워지도록 설계<ul>
<li>문제 발생 : Count값이… 꼬인다ㅠㅠ</li>
</ul>
</li>
<li>방법2 : KeyDown 이벤트를 줘서  preValue값을 가지고  두개를 구분해서 사용한다.</li>
</ul>
</li>
<li><p>기존에 가지고 있는 태그와  새로 사용자 정의된 태그는 TagSet 배열에 추가할때 어떻게 구분해서 Push 하는가?</p>
<ul>
<li>방법 : 추가하는 함수를 2가지로 만들어서 사용한다. input.value로 추가할때는 string 값을 받아서 기존에 가지고 있는 해시태그를 검사해서 있으면 그 Item을 배열에 추가하고 아닌경우는 key값을 맞춰서 새로운 Item을 추가한다.</li>
</ul>
</li>
<li><p>input 창에 있는 Focus를 어떻게 HashtagMenu로 옮겨서 할 수 있을까?</p>
<ul>
<li><p>방법 : Blur이벤트를 이용해서 input에 Focus를 잃게하고 Parents Node로 포커싱을 주고 KeyEvent를 이용해서 방향키를 누르면 Focus가 움직이도록 설정한다.</p>
<p>REF&gt;</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur">HTMLElement.blur() - Web APIs | MDN</a></p>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL]221214]]></title>
            <link>https://velog.io/@ella-front-dev/TIL221214</link>
            <guid>https://velog.io/@ella-front-dev/TIL221214</guid>
            <pubDate>Thu, 15 Dec 2022 00:05:42 GMT</pubDate>
            <description><![CDATA[<h2 id="checkbox--radio-구현">checkbox / radio 구현</h2>
<p>웹표준만 적용하지 않는다면 vue/react에서는 checkbox / radio 기능이 구현이 가능하므로 따로 CSS으로 숨기거나 하는 방법이 아니라 커스텀으로 만들 수 있다.</p>
<h3 id="mutationobserver-method">mutationObserver method</h3>
<p>React / Vue의 로우레벨에서는 위의 패턴과 방법으로 실시간 감지 기능이 쓰여지고 있다.</p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/MutationObserver">MutationObserver - Web API | MDN</a></p>
<h3 id="observer-pattern">observer pattern</h3>
<p><a href="https://velog.io/@longroadhome/%EB%AA%A8%EB%8D%98JS-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-Observers">[모던JS: 브라우저] Observers</a></p>
<h2 id="github">Github</h2>
<ul>
<li><p>문제: remote에 기존에 있던 브랜치들이 정리되어 같은 이름의 새로운 브런치가 생성되었는데…  로컬에서 계속 branch를 인식하지 못하고 에러를 낸다.</p>
</li>
<li><p>해결 : <code>git remote prune origin</code>  를 사용해서 branch를 잘라내고 remote와 맞춰서 동기화 시킬 수 있도록한다.</p>
<p>  <a href="https://devconnected.com/how-to-clean-up-git-branches/"></a></p>
</li>
</ul>
<h2 id="popup">Popup</h2>
<p><a href="https://vue-final-modal.org/">Introduction</a></p>
<h3 id="이슈">이슈</h3>
<ul>
<li><p>문제 : 다이나믹 모달이 잘 적용 되지 않는다.</p>
</li>
<li><p>임시 해결 : 설정하는 부분을 String이 아니라 Component로 넣었더니 되었다.</p>
<pre><code class="language-jsx">  // pages/index.vue
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent, reactive, inject, ref } from &#39;vue&#39;
  // import { $vfm } from &#39;vue-final-modal&#39;
  import ModalDialog from &#39;@/components/base/modal/ModalDialog.vue&#39;

  export default defineComponent({

    setup(){
      // const state = reactive({
      //   $vfm: inject(&#39;$vfm&#39;)  })

      const state = reactive({
        isBaseModal: false,
        isDialogModal: false,
        $vfm: inject(&#39;$vfm&#39;) 

      })

      const showDialogModal = () =&gt; {
        state.$vfm.show(&#39;ModalDialog&#39;)
      }
  &lt;/script&gt;</code></pre>
</li>
</ul>
<h2 id="vue-vscode단축어">vue vscode단축어</h2>
<pre><code class="language-jsx">//vueinit ⇒ vue template 설정 
&lt;template lang=&quot;&quot;&gt;
  &lt;div&gt;

  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
export default {

}
&lt;/script&gt;
&lt;style lang=&quot;&quot;&gt;

&lt;/style&gt;

//scriptcomposition ⇒  vue composition설정
&lt;script&gt;
import { defineComponent } from &#39;@vue/composition-api&#39;

export default defineComponent({
  setup() {

  },
})
&lt;/script&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221212 ]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221212</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221212</guid>
            <pubDate>Mon, 12 Dec 2022 23:50:45 GMT</pubDate>
            <description><![CDATA[<h2 id="tiptap-이슈사항">Tiptap 이슈사항</h2>
<h3 id="외부-클릭시-액션-설정">외부 클릭시 액션 설정</h3>
<ul>
<li><p>문제: 팁탭에서 툴팁밖을 클릭시 툴팁이 없어지는 기능 필요</p>
</li>
<li><p>해결: @vueuse를 import 해서 onClickOutside 기능 이용!</p>
<pre><code class="language-jsx">  &lt;template&gt;
    &lt;div ref=&quot;target&quot; class=&quot;button-set&quot;&gt;&lt;/div&gt;
  &lt;/template&gt;
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent, ref } from &#39;vue&#39;
  import { Editor } from &#39;@tiptap/core&#39;
  import { onClickOutside } from &#39;@vueuse/core&#39;

  export default defineComponent({
    name: &#39;ColorPicker&#39;,
    setup(props, { emit }){
      const target = ref(null)

      onClickOutside(target, () =&gt; emit(&#39;handleOnCloseBox&#39;, props.dataSet.sort))

      return { target }

    }
  })

  &lt;/script&gt;</code></pre>
</li>
</ul>
<h3 id="동영상-링크와-일반-링크를-구분">동영상 링크와 일반 링크를 구분</h3>
<ul>
<li><p>문제: 하나의 기능을 2개로 구분해야함</p>
</li>
<li><p>해결: class로 나눠서 사용</p>
<pre><code class="language-jsx">  const handleOnSetLink = () =&gt; {
    // //cancelled &amp; empty &amp; undefined
    if (state.value === null || state.value === &#39;&#39; || state.value === undefined) {
      props.editor.chain().focus().extendMarkRange(&#39;link&#39;).unsetLink().run()
      state.value = null
      return  emit(&#39;handleOnCloseBox&#39;, props.sort)
    }

    // update link
    if(props.sort === &#39;link&#39;){
      props.editor.chain().focus().extendMarkRange(&#39;link&#39;).setLink({ href: state.value, class: &#39;link&#39; }).run()
      console.log(&#39;editor &#39;,props.editor.getAttributes(&#39;link&#39;))
    }else {
      props.editor.chain().focus().extendMarkRange(&#39;link&#39;).setLink({ href: state.value, class: &#39;video&#39; }).run()
    }
    state.value = null
  }</code></pre>
<h3 id="에디터-영역에-접근">에디터 영역에 접근</h3>
</li>
</ul>
<p>window를 이용해서 직접적으로 에디터 영역 접근</p>
<ul>
<li><p>문제: 링크안에 링크가 들어가게되면 안되기 때문에 이를 막아야한다. 그리고 동영상 링크랑 일반 링크에 따라서 다르게 알럿이 떠야한다.</p>
</li>
<li><p>해결: 블럭포커스가 되었는지 확인하는거랑 현재 포커싱되어있는 블럭이 링크태그인지 확인해서 분류</p>
<pre><code class="language-jsx">  const handleOnLinkbox = (sort:string) =&gt; {
    const selectBlock = window.getSelection() as Selection | null
    let isBlock = false
    selectBlock?.isCollapsed ? &#39;&#39; : isBlock = true

    if(isBlock){
      if(selectBlock?.focusNode?.parentElement?.classList[0] === sort || selectBlock?.focusNode?.parentElement?.nodeName !== &#39;A&#39;){
         sort === &#39;link&#39; ? state.isLink = true : state.isVideo = true 
      }else{
        alert(&#39;이미 링크지정이 되었습니다.&#39;)
      }
    } else {
       alert(&#39;링크할 텍스트를 선택해주세요.&#39;)
    }
  }</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221208]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221208</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221208</guid>
            <pubDate>Thu, 08 Dec 2022 00:56:47 GMT</pubDate>
            <description><![CDATA[<h2 id="demo-api">Demo API</h2>
<h3 id="uselazyasyncdata">useLazyAsyncData</h3>
<ul>
<li><p>페이지 렌더링 전에 초기 데이터를 가져오기 위해서 사용</p>
<pre><code class="language-jsx">  &lt;template&gt;
    &lt;div&gt;
      {{ pending ? &#39;Loading&#39; : count }}
    &lt;/div&gt;
  &lt;/template&gt;
  &lt;script setup&gt;
  //Navigation will occur before fetching is complete. Handle pending and error states directly within your component&#39;s template

  const { pending, data: count } = useLazyAsyncData(&#39;count&#39;, () =&gt; $fetch(&#39;/api/count&#39;))
  watch(count, (newCount) =&gt; {
      // Because count starts out null, you won&#39;t have access
    // to its contents immediately, but you can watch it.
  })
  &lt;/script&gt;</code></pre>
<pre><code class="language-jsx">
  // Using Async Setup
  &lt;script&gt;
  export default defineComponent({
    async setup() {
      const [{ data: organization }, { data: repos }] = await Promise.all([
        useFetch(`https://api.github.com/orgs/nuxt`),
        useFetch(`https://api.github.com/orgs/nuxt/repos`)
      ])
      return {
        organization,
        repos
      }
    }
  })
  &lt;/script&gt;
  &lt;template&gt;
    &lt;header&gt;
      &lt;h1&gt;{{ organization.login }}&lt;/h1&gt;
      &lt;p&gt;{{ organization.description }}&lt;/p&gt;
    &lt;/header&gt;
  &lt;/template&gt;</code></pre>
</li>
<li><p>useLazyAsyncData와 useAsyncData의 차이는??</p>
</li>
</ul>
<p>REF&gt;</p>
<p><a href="https://nuxt.com/docs/api/composables/use-lazy-async-data#uselazyasyncdata">useLazyAsyncData</a></p>
<p><a href="https://nuxt.com/docs/getting-started/data-fetching#uselazyasyncdata">Data Fetching</a></p>
<h2 id="배포">배포</h2>
<ul>
<li>SSR 배포 및  SSG 배포의 차이??</li>
<li>클라이언트 배포는 어떻게??</li>
</ul>
<h2 id="tab">Tab</h2>
<h3 id="이슈">이슈</h3>
<p>문제 : API 를 넣는 와중에 tab 기능이 안됨</p>
<p>원인 : ref로 만들었던 변수를 바꾸려고 할때 변수는 바뀌는데 이게 reactive가 잘 안되었다. </p>
<p>해결 : reactive()를 사용해서 바꾸니 기능은 작동한다.</p>
<p>⇒ ref 와 reactive에 대해서 정확하게 다시 정리해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 221013]]></title>
            <link>https://velog.io/@ella-front-dev/TIL-221013</link>
            <guid>https://velog.io/@ella-front-dev/TIL-221013</guid>
            <pubDate>Thu, 13 Oct 2022 08:24:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p> <strong>Today I Learn</strong></p>
</blockquote>
<ul>
<li>Array.sort()</li>
<li>String.spilt()</li>
<li>Array.join()</li>
</ul>
<h1 id="1javascript">1.Javascript</h1>
<h2 id="11-method">1.1. Method</h2>
<h3 id="111-arraysort">1.1.1. Array.sort()</h3>
<p><strong>매개변수 / 반환값</strong></p>
<pre><code class="language-javascript">    arr.sort([&#39;compareFunction&#39;])
/*
    =&gt; Q1.그럼 숫자나 객체를 문자로 바꾸어서 유니코드에 따라 정렬한다는데... 그렇다면 우선순위는 어떻게 되는가?
     */
     // Answer1
    let arr1 = [1,10,2,31234,&#39;안녕&#39;, {a:1, b:&#39;a&#39;}]
    arr1.sort() // [1, 10, 2, 31234, {a:1, b:&#39;a&#39;}, &#39;안녕&#39;]
</code></pre>
<br>

<ol>
<li><p>매개변수(compareFunction) : </p>
<ul>
<li>매개변수를 생략하면 각 요소를 문자열 변환하여 <em><strong>유니코드 코드 포인트</strong></em> 순서로 문자열을 비교하여 정렬한다.
Ex1) 문자 : [&#39;체리&#39;, &#39;바나나&#39;] -&gt; [&#39;바나나&#39;, &#39;체리&#39;] 
Ex2) 숫자 : 숫자는 문자로 변환되어 정렬되므로 [9,80] -&gt; [80,9]</li>
</ul>
</li>
<li><p>반환값(return) : </p>
<ul>
<li>기존 배열이 정렬되는 것이므로 불변성이 지켜지지 않는다. 복사본 없다.</li>
</ul>
</li>
</ol>
<p><strong>설명</strong></p>
<pre><code class="language-javascript">// Compare 함수의 동작
  function compare(a, b) {
    if (a &lt; b) {
      return -1;
    }
    if (a &gt; b) {
      return 1;
    }
    /*
    a와 b가 같을 때 =&gt; a와 b를 서로에 대해 변경하지 않고 모든 다른 요소에 대해 정렬(ECMAscript 표준은 이러한 동작을 보장하지 않으므로 모든 브라우저(예 : Mozilla 버전은 적어도 2003 년 이후 버전 임)가 이를 존중하지는 않는다.)
    */
    return 0;
  }</code></pre>
<ol>
<li>문자열 대신 숫자를 비교<pre><code class="language-JS">// 오름차순 정렬
function compareNumbers(a, b) {
return a - b;
}
</code></pre>
</li>
</ol>
<p>let numbers = [4, 2, 5, 1, 3];
numbers.sort(function(a, b) {
  return a - b;
});
console.log(numbers);</p>
<p>// [1, 2, 3, 4, 5]</p>
<pre><code>
2. 객체 : 해당 속성 중 하나의 값을 기준으로 정렬
```js
let items = [
  { name: &#39;Edward&#39;, value: 21 },
  { name: &#39;Sharpe&#39;, value: 37 },
  { name: &#39;And&#39;, value: 45 },
  { name: &#39;The&#39;, value: -12 },
  { name: &#39;Magnetic&#39;, value: 13 },
  { name: &#39;Zeros&#39;, value: 37 }
];

// value 기준으로 정렬
items.sort(function (a, b) {
  if (a.value &gt; b.value) {
    return 1;
  }
  if (a.value &lt; b.value) {
    return -1;
  }
  return 0;
});

// name 기준으로 정렬
items.sort(function(a, b) {
  let nameA = a.name.toUpperCase(); // ignore upper and lowercase
  let nameB = b.name.toUpperCase(); // ignore upper and lowercase
  if (nameA &lt; nameB) {
    return -1;
  }
  if (nameA &gt; nameB) {
    return 1;
  }
  // 이름이 같을 경우
  return 0;
});</code></pre><br>

<p><strong>예제</strong></p>
<ol>
<li>배열만들기 / 정렬<pre><code class="language-js">function compareNumbers(a, b) {
return a - b;
}
</code></pre>
</li>
</ol>
<p>/* 문자열 배열 정렬*/
let stringArray = [&#39;Blue&#39;, &#39;Humpback&#39;, &#39;Beluga&#39;];</p>
<p>console.log(&#39;stringArray:&#39;, stringArray.join());
console.log(&#39;Sorted:&#39;, stringArray.sort());</p>
<p>//stringArray: Blue,Humpback,Beluga
//Sorted: Beluga,Blue,Humpback</p>
<p>/* 숫자 배열 정렬 */
// 1. 문자열로 되어있는 숫자를 정렬
let numericStringArray = [&#39;80&#39;, &#39;9&#39;, &#39;700&#39;]</p>
<p>console.log(&#39;numberArray:&#39;, numberArray.join());
console.log(&#39;Sorted without a compare function:&#39;, numberArray.sort());
console.log(&#39;Sorted with compareNumbers:&#39;, numberArray.sort(compareNumbers));</p>
<p>// numberArray: 40,1,5,200
// Sorted without a compare function: 1,200,40,5 =&gt; 제대로 안됨
// Sorted with compareNumbers: 1,5,40,200 =&gt; 오름차순</p>
<p>//2. 숫자로만 되어있는 정렬
let numberArray = [40, 1, 5, 200];</p>
<p>console.log(&#39;numericStringArray:&#39;, numericStringArray.join());
console.log(&#39;Sorted without a compare function:&#39;, numericStringArray.sort());
console.log(&#39;Sorted with compareNumbers:&#39;, numericStringArray.sort(compareNumbers));</p>
<p>// numericStringArray: 80,9,700
// Sorted without a compare function: 700,80,9 =&gt; 내림 차순
// Sorted with compareNumbers: 9,80,700 =&gt; 오름 차순</p>
<p>/* 숫자와 문자열이 혼합되어있는 배열 */
let mixedNumericArray = [&#39;80&#39;, &#39;9&#39;, &#39;700&#39;, 40, 1, 5, 200];</p>
<p>console.log(&#39;mixedNumericArray:&#39;, mixedNumericArray.join());
console.log(&#39;Sorted without a compare function:&#39;, mixedNumericArray.sort());
console.log(&#39;Sorted with compareNumbers:&#39;, mixedNumericArray.sort(compareNumbers));</p>
<p>// mixedNumericArray: 80,9,700,40,1,5,200
// Sorted without a compare function: 1,200,40,5,700,80,9
// Sorted with compareNumbers: 1,5,9,40,80,200,700</p>
<p>```</p>
<ol start="2">
<li>map을 사용한 정렬 =&gt; 좀더 이해가 필요!!
compareFunction은 배열 내의 요소마다 여러 번 호출되므로 성능적인 면이 저하될 수 있다. 그러므로 compareFunction이 복잡해지고, 정렬할 요소가 많아질 경우, map을 사용한 정렬하면 더 좋다. 이 방법은 임시 배열을 하나 만들어서 여기에 실제 정렬에 사용할 값만을 뽑아서 넣어서 이를 정렬하고, 그 결과를 이용해서 실제 정렬을 하는 것이다.</li>
</ol>
<br>

<p><strong>Ref</strong>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">MDN Document</a></p>
<hr>
<h3 id="112-stringspilt">1.1.2. String.spilt()</h3>
<p><strong>Ref</strong>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/split">MDN Document</a></p>
<hr>
<h3 id="113-arrayjoin">1.1.3. Array.join()</h3>
<hr>
<p><strong>Ref</strong>
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/join">MDN Document</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[] Heap 구현]]></title>
            <link>https://velog.io/@ella-front-dev/Heap-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ella-front-dev/Heap-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 17 May 2022 14:25:33 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[[Project] YoonSea 개발 과정기(1)]]></title>
            <link>https://velog.io/@ella-front-dev/Project-YoonSea-%EA%B0%9C%EB%B0%9C-%EA%B3%BC%EC%A0%95%EA%B8%B01</link>
            <guid>https://velog.io/@ella-front-dev/Project-YoonSea-%EA%B0%9C%EB%B0%9C-%EA%B3%BC%EC%A0%95%EA%B8%B01</guid>
            <pubDate>Mon, 11 Apr 2022 17:51:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 프로젝트는 &quot;OpenSea Clone Project&quot;로 간단한 기능구현과 스타일일링 되었습니다. </p>
</blockquote>
<h1 id="기획">기획</h1>
<h2 id="software-reqirement">Software Reqirement</h2>
<h3 id="기술스택">기술스택</h3>
<p><strong>Client</strong></p>
<ul>
<li>React: Create React App으로 기본 셋팅 및 컴포넌트 구현</li>
<li>Tailwind Css : 스타일링 구현</li>
<li>Eslint &amp; Prettier</li>
</ul>
<p><strong>Server</strong></p>
<ul>
<li>solidity : 스타트 컨트랙트 구현</li>
</ul>
<p><strong>Database</strong></p>
<ul>
<li>IPFS</li>
</ul>
<p><strong>Deploy</strong></p>
<ul>
<li>정적 배포를 생각하고 있으며 아직 정해지지 않았다. </li>
</ul>
</br>

<h3 id="구현해야하는-기능">구현해야하는 기능</h3>
<ul>
<li>메타마스크 지갑 연결</li>
<li>IPFS 연결 및 호출 기능</li>
<li>NFT 생성 기능</li>
<li>NFT 구매 기능 구현</li>
<li>NFT 판매 기능 구현</li>
</ul>
<br/>

<hr>
<h1 id="환경-설정">환경 설정</h1>
<h2 id="패키지-정보">패키지 정보</h2>
<pre><code class="language-json">  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^17.0.2&quot;,
    &quot;react-dom&quot;: &quot;^17.0.2&quot;,
    &quot;react-router-dom&quot;: &quot;^6.3.0&quot;,
    &quot;react-scripts&quot;: &quot;4.0.3&quot;,
    &quot;web3&quot;: &quot;^1.7.3&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;eslint-config-prettier&quot;: &quot;^8.5.0&quot;,
    &quot;eslint-plugin-prettier&quot;: &quot;^4.0.0&quot;,
    &quot;prettier&quot;: &quot;^2.6.2&quot;
  }</code></pre>
<br/>

<h2 id="component-구조">Component 구조</h2>
<p><img src="https://velog.velcdn.com/images/ella-front-dev/post/07f69acf-a6ed-43e7-83ac-f91f4e119627/image.png" alt=""></p>
<br/>

<h2 id="eslint--prettier-설정">Eslint &amp; Prettier 설정</h2>
<p>프로젝트를 함께 개발하다보면 각기 다른 환경과 코딩 습관을 가지고 있기 때문에 협업을 위해서 eslint와 prettier를 사용하였다.
esLint는 주로 에러가 나는 코드를 찾아주고 규칙에 따라 코드를 일괄되게 사용할 수 있도록 해준다. prettier은 코드가 가독성 좋게 보여질 수 있도록 강제적으로 코드를 변경한다.</p>
<h3 id="설치">설치</h3>
<p>이 프로젝트에서는 Create-react-app을 사용했기 때문에 이미 eslint가 설치되어있다.</p>
<pre><code class="language-shell">$ npm i -D prettier
$ npm i -D eslint-config-prettier eslint-plugin-prettier </code></pre>
<ul>
<li><strong>eslint-config-prettier</strong> : ESLint의 formatting 관련 설정 중 Prettier와 충돌하는 부분을 비활성화 </li>
<li><strong>eslint-plugin-prettier</strong> : Prettier를 ESLint 플러그인으로 추가한다. 즉, Prettier에서 인식하는 코드상의 포맷 오류를 ESLint 오류로 출력해준다.</li>
</ul>
<br/>

<h3 id="파일-생성-및-셋팅">파일 생성 및 셋팅</h3>
<p><strong>.eslintrc.js</strong></p>
<pre><code class="language-js">module.exports = {
  env: {
    browser: true,
    node: true,
  },
  plugins: [&#39;prettier&#39;],
  extends: [&#39;react-app&#39;, &#39;eslint:recommended&#39;, &#39;plugin:prettier/recommended&#39;],
  rules: {
    &#39;prettier/prettier&#39;: &#39;error&#39;,
  },
};</code></pre>
<ul>
<li>env : 적용되는 환경 설정</li>
</ul>
<p><strong>.prettierrc</strong></p>
<pre><code class="language-js">{
  &quot;trailingComma&quot;: &quot;es5&quot;,
  &quot;tabWidth&quot;: 2,
  &quot;semi&quot;: true, 
  &quot;singleQuote&quot;: true 
}</code></pre>
<ul>
<li>trailingComma : 여러 줄이 있는 구문에서 후행 쉼표 여부</li>
<li>tabWidth: 탭너비</li>
<li>semi: 끝에 세미클론 여부</li>
<li>singleQuote : 작은 따움표 사용 여부</li>
</ul>
<br/>

<h3 id="vscode-설정">VScode 설정</h3>
<p>VSCode 설정(윈도우, 리눅스에서는 Ctrl + , 맥에서는 Cmd + ,)으로 들어간다.
User와 Workspace 항목있는데 User는 VSCode 자체 설정으로 모든 프로젝트에 적용이 되고, Workspace는 현재 프로젝트에서만 설정이 적용되며, <code>.vscode/settings.json</code>에 설정 항목이 저장된다.</p>
<pre><code class="language-json">{
  // Set the default
  &quot;editor.formatOnSave&quot;: false, // true : 저장시 포맷이 자동으로 변경
  // Enable per-language
  &quot;[javascript]&quot;: {
    &quot;editor.formatOnSave&quot;: true
  },
  &quot;editor.codeActionsOnSave&quot;: {
    // For ESLint
    &quot;source.fixAll.eslint&quot;: true
  }
}</code></pre>
<p><strong>Ref</strong>
<a href="https://eslint.org/docs/rules/">ESLint 규칙</a>
<a href="https://prettier.io/docs/en/options.html">Prettier 옵션</a>
<a href="https://velog.io/@recordboy/ESLint-Prettier-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">ESLint, Prettier 적용하기</a></p>
<br/>


<h2 id="이슈사항">이슈사항</h2>
<h3 id="webpack5">Webpack5</h3>
<p><code>create-react-app</code>으로 기본 환경을 셋팅했다.
이 과정에서 <code>create-react-app</code>이 React 버전이 18로 업그레이드 되면서 webpack5를 적용하게 되어서 다른 패키지들과 호환이 안되는 이슈가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/ella-front-dev/post/c8b50847-c4f5-4c40-bdc3-317f99902cc1/image.gif" alt=""></p>
<p>이를 해결하는 방안으로 2가지를 생각했다.</p>
<p><strong>방법 1</strong>
Webpack5를 그대로 유지하면서 호환이 안되는 것들을 호환이 가능하도록 해주는 패키지들(polyfill)을 설치해서 셋팅한다.</p>
<pre><code class="language-json">//polyfill
&quot;assert&quot;: &quot;^2.0.0&quot;,
&quot;crypto&quot;: &quot;^1.0.1&quot;,
&quot;crypto-browserify&quot;: &quot;^3.12.0&quot;,
&quot;http&quot;: &quot;0.0.1-security&quot;,
&quot;https&quot;: &quot;^1.0.0&quot;,
&quot;https-browserify&quot;: &quot;^1.0.0&quot;,
&quot;os-browserify&quot;: &quot;^0.3.0&quot;,
&quot;stream&quot;: &quot;0.0.2&quot;,
&quot;stream-http&quot;: &quot;^3.2.0&quot;,
&quot;url&quot;: &quot;^0.11.0&quot;</code></pre>
<p><strong>방법 2</strong>
react-scripts 버젼을 5.0.0에서 4.0.3으로 하고 react(v17.0.2), react-dom(17.0.2) 버젼을 낮추어 셋팅한다.</p>
<p><strong>선택된 방법</strong>
안정성을 생각해서 &quot;방법2&quot;를 선택했다. webpack5 호환이 안되는 패키지가 많으면 그만큼 많은 폴리필 패키지를 설치해야하기 때문에 기능도 무거워지고 사용하지 못하는것들도 있지 않으까 하는 생각에 이전에 사용하던 버젼으로 선택하게 되었다.
사실 webpack.config.js를 사용해서 구현도 가능하지만 제한된 시간에 구현이 어려울꺼 같아서 선택하지 않았지만 추후에 구성해봐야겠다.</p>
<p><strong>셋팅 방법</strong>
node-modules와 package-lock.json 파일 삭제 후 아래 처럼 파일 코드를 변경한다.</p>
<ul>
<li>package.json<pre><code class="language-json">// 변경 전 
[
&quot;dependencies&quot;: {
  &quot;@testing-library/jest-dom&quot;: &quot;^5.16.4&quot;,
  &quot;@testing-library/react&quot;: &quot;^12.1.4&quot;,
  &quot;@testing-library/user-event&quot;: &quot;^13.5.0&quot;,
  &quot;react&quot;: &quot;^18.0.0&quot;,
  &quot;react-dom&quot;: &quot;^18.0.0&quot;,
  &quot;react-router-dom&quot;: &quot;^6.3.0&quot;,
  &quot;react-scripts&quot;: &quot;5.0.0&quot;,
  &quot;web-vitals&quot;: &quot;^2.1.4&quot;,
  &quot;web3&quot;: &quot;^1.7.3&quot;
},
</code></pre>
</li>
</ul>
<p>// 변경 후 
  &quot;dependencies&quot;: {
    &quot;react&quot;: &quot;^17.0.2&quot;,
    &quot;react-dom&quot;: &quot;^17.0.2&quot;,
    &quot;react-router-dom&quot;: &quot;^6.3.0&quot;,
    &quot;react-scripts&quot;: &quot;4.0.3&quot;,
    &quot;web-vitals&quot;: &quot;^2.1.4&quot;,
    &quot;web3&quot;: &quot;^1.7.3&quot;
  },</p>
<pre><code>
- index.js
``` js
// 변경 전
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import &#39;./index.css&#39;;
import App from &#39;./App&#39;;

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;));

root.render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
);


// 변경 후
import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom&#39;;
import { BrowserRouter } from &#39;react-router-dom&#39;;
import &#39;./index.css&#39;;
import App from &#39;./App&#39;;
import * as serviceWorker from &#39;./serviceWorker&#39;;

ReactDOM.render(
  &lt;BrowserRouter&gt;
    &lt;App /&gt;
  &lt;/BrowserRouter&gt;,
  document.getElementById(&#39;root&#39;)
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Basic  - 앱 프론트엔드 & Web3.js]]></title>
            <link>https://velog.io/@ella-front-dev/CrytoZombie-basic-%EC%95%B1-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-Web3.js</link>
            <guid>https://velog.io/@ella-front-dev/CrytoZombie-basic-%EC%95%B1-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-Web3.js</guid>
            <pubDate>Thu, 07 Apr 2022 17:51:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 글은 CrytoZombie를 공부하면서 정리한 내용입니다.</p>
</blockquote>
<p><a href="https://cryptozombies.io/ko/course">CrytoZombie</a></p>
<h1 id="ch1-web3js">Ch1. Web3.js</h1>
<p>사용자들이 상호작용 할 수 있는 기본적인 웹 페이지를 만들것이다.
이를 만들기 위해, 우리는 이더리움 재단에서 만든 자바스크립트 라이브러리인 Web3.js를 사용한다.</p>
<h2 id="web3js-란">Web3.js 란?</h2>
<p>이더리움 네트워크는 노드로 구성되어 있고, 각 노드는 블록체인의 복사본을 가지고 있다. 스마트 컨트랙트의 함수를 실행하고자 한다면, 이 노드들 중 하나에 질의를 보내 아래 내용을 전달해야 한다.</p>
<ol>
<li>스마트 컨트랙트의 주소</li>
<li>실행하고자 하는 함수</li>
<li>그 함수에 전달하고자 하는 변수들
이더리움 노드들은 JSON-RPC라고 불리는 언어로만 소통할 수 있고, 이는 사람이 읽기는 불편하다. 컨트랙트의 함수를 실행하고 싶다고 질의를 보내는 것은 이와 같다.<pre><code class="language-json">{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;eth_sendTransaction&quot;,&quot;params&quot;:[{&quot;from&quot;:&quot;0xb60e8dd61c5d32be8058bb8eb970870f07233155&quot;,&quot;to&quot;:&quot;0xd46e8dd67c5d32be8058bb8eb970870f07244567&quot;,&quot;gas&quot;:&quot;0x76c0&quot;,&quot;gasPrice&quot;:&quot;0x9184e72a000&quot;,&quot;value&quot;:&quot;0x9184e72a&quot;,&quot;data&quot;:&quot;0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675&quot;}],&quot;id&quot;:1}</code></pre>
다행히도, Web3.js는 이런 골치 아픈 질의를 아닌 편리하고 쉽게 읽을 수 있는 자바스크립트 인터페이스로 상호작용을 할 수 있게 한다.</li>
</ol>
<p>위의 질의문을 작성할 필요 없이, 자네의 코드에서 함수를 아래와 같이 호출하는 것이다.</p>
<pre><code class="language-js">CryptoZombies.methods.createRandomZombie(&quot;Vitalik Nakamoto 🤔&quot;)
  .send({ from: &quot;0xb60e8dd61c5d32be8058bb8eb970870f07233155&quot;, gas: &quot;3000000&quot; })</code></pre>
<p>먼저 프로젝트에 Web3.js를 설정하도록 해야한다.</p>
<p>= 프로젝트에서 가장 많이 사용하는 패키지 도구를 써서 Web3.js를 추가</p>
<pre><code class="language-shell">// NPM을 사용할 때
npm install web3

// Yarn을 사용할 때
yarn add web3

// Bower를 사용할 때
bower install web3</code></pre>
<p>= 단순히 <a href="https://github.com/ChainSafe/web3.js/tree/1.x/dist">github</a>에서 간략화된 .js 파일을 다운로드하거나 CDN 코드를 사용한다.</p>
<pre><code class="language-html">&lt;!-- web3.min.js 파일을 프로젝트에 추가 --&gt;
&lt;script language=&quot;javascript&quot; type=&quot;text/javascript&quot; src=&quot;web3.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- CDN을 프로젝트에 추가 | --&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- CDN을 프로젝트에 추가 | unpkg --&gt;
&lt;script src=&quot;https://unpkg.com/web3@latest/dist/web3.min.js&quot;&gt;&lt;/script&gt;</code></pre>
<hr>
<h1 id="ch2-web3-provider">Ch2. Web3 Provider</h1>
<p>이 단계에서는 Web3 프로바이더로 Web3.js를 초기화하는 단계이다.
Web3.js에서 Web3 프로바이더를 설정하는 것은 우리 코드에 읽기와 쓰기를 처리하려면 어떤 노드와 통신을 해야 하는지 설정하는 것이다. 이는 전통적인 웹 앱에서 API 호출을 위해 원격 웹 서버의 URL을 설정하는 것과 같다.</p>
<p>나만의 이더리움 노드를 프로바이더로 운영할 수도 있다. 하지만 편리하게 쓸 수 있는 제3자 서비스가 있다. DApp의 사용자들을 위해 이더리움 노드를 운영할 필요가 없이 사용할 수 있는 서비스이다.(Infura)</p>
<h2 id="infura">Infura</h2>
<p><a href="https://infura.io/">Infura</a>는 빠른 읽기를 위한 캐시 계층을 포함하는 다수의 이더리움 노드를 운영하는 서비스이다. 접근을 위한 API를 무료로 사용할 수 있다. Infura를 프로바이더로 사용하면, 나만의 이더리움을 설치하고 계속 유지할 필요 없이 이더리움 블록체인과 메세지를 확실히 주고받을 수 있다.</p>
<p>다음과 같이 Web3에 Web3 프로바이더로 Infura를 쓰도록 설정할 수 있다:</p>
<pre><code class="language-js">var web3 = new Web3(new Web3.providers.WebsocketProvider(&quot;wss://mainnet.infura.io/ws&quot;));</code></pre>
<p>하지만, 많은 사용자들이 우리의 DApp을 사용할 것이기에(이 사용자들은 단순히 읽기만 하는 게 아니라 블록체인에 뭔가 쓰기도 할 것이기에) 우리는 이 사용자들이 그들의 개인 키로 트랜잭션에 서명을 할 수 있도록 해야 할 것이다.</p>
<p>*참고: 이더리움(그리고 일반적으로 블록체인)은 트랜잭션에 전자 서명을 하기 위해 공개/개인 키 쌍을 사용한다. 말하자면 전자 서명을 위해 엄청나게 안전한 비밀번호 같은 것이다. 이런 방식으로 내가 만약 블록체인에서 어떤 데이터를 변경하면, 나의 공개 키를 통해 내가 거기 서명을 한 사람이라고 증명할 수 있다. 하지만 아무도 내 개인 키를 모르기 때문에, 내 트랜잭션을 누구도 위조할 수 없다.</p>
<p>앱 프론트엔드에서 사용자들의 개인 키를 관리하려 하는 것은 아마 좋은 생각이 아니다. 이를 대신 처리해주는 서비스가 많은데 이중 가장 유명한 것은 메타마스크(Metamask)이다.</p>
<h2 id="메타마스크">메타마스크</h2>
<ul>
<li><a href="https://metamask.io/">메타마스크</a>: 사용자들이 이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는 크롬과 파이어폭스의 브라우저 확장 프로그램</li>
<li>해당 계정들을 써서 Web3.js를 사용하는 웹사이트들과 상호작용을 할 수 있도록 한다.</li>
</ul>
<p>*참고: 메타마스크는 내부적으로 Infura의 서버를 Web3 프로바이더로 사용한다. 하지만 사용자들에게 그들만의 Web3 프로바이더를 선택할 수 있는 옵션을 주기도 한다. 즉 메타마스크의 Web3 프로바이더를 사용하면, 사용자에게 선택권을 주는 것이기도 하면서 앱에서 걱정할 거리를 하나 줄일 수 있다.</p>
<h3 id="메타마스크의-web3-프로바이더-사용">메타마스크의 Web3 프로바이더 사용</h3>
<p>메타마스크는 web3라는 전역 자바스크립트 객체를 통해 브라우저에 Web3 프로바이더를 주입한다.
앱에서는 web3가 존재하는지 확인하고, 만약 존재한다면 web3.currentProvider를 프로바이더로서 사용하면 되다.</p>
<p>= 템플릿 코드</p>
<pre><code class="language-js">// 사용자가 메타마스크를 설치했는지 확인하고 설치가 안 된 경우 우리 앱을 사용하려면 메타마스크를 설치해야 한다고 알려주는 것

window.addEventListener(&#39;load&#39;, function() {
  // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
  if (typeof web3 !== &#39;undefined&#39;) {
    // Mist/MetaMask의 프로바이더 사용
    web3js = new Web3(web3.currentProvider);
  } else {
    // 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
    // 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
  }

  // 이제 자네 앱을 시작하고 web3에 자유롭게 접근할 수 있네:
  startApp()

})</code></pre>
<p>*참고: 메타마스크 말고도 사용자들이 쓸 수 있는 다른 개인 키 관리 프로그램도 있다.(ex - 미스트(Mist) 하지만, 그것들도 모두 web3 변수를 주입하는 동일한 형태를 사용한다.</p>
<hr>
<h1 id="ch3-컨트랙트와-대화">Ch3. 컨트랙트와 대화</h1>
<p>이 단계는 스마트 컨트랙트와 통신하는 단계이다.
Web3.js는 스마트 컨트랙트와 통신을 위해 2가지를 필요로 하다(컨트랙트의 주소와 ABI)</p>
<h2 id="컨트랙트-주소">컨트랙트 주소</h2>
<p>스마트 컨트랙트를 모두 작성한 후에는 그걸 컴파일한 후 이더리움에 배포할 것이다. 
컨트랙트를 배포한 후, 해당 컨트랙트는 영원히 존재하는, 이더리움 상에서 고정된 주소를 얻는다. 레슨2를 상기해보면, 이더리움 메인넷에서 크립토키티의 주소는 &quot;0x06012c8cf97BEaD5deAe237070F9587f8E7A266d&quot;였다.
우리의 스마트 컨트랙트와 통신을 하기 위해 배포 후 이 주소를 복사해야 할 것이다.</p>
<h2 id="컨트랙트-abi">컨트랙트 ABI</h2>
<p>ABI는 Application Binary Interface의 약자로 기본적으로 JSON 형태로 컨트랙트의 메소드를 표현하는 것이다. 우리 컨트랙트가 이해할 수 있도록 하려면 Web3.js가 어떤 형태로 함수 호출을 해야 하는지 알려주는 것이다.</p>
<p>이더리움에 배포하기 위해 컨트랙트를 컴파일할 때, 솔리디티 컴파일러가 자네에게 ABI를 줄 것이다. 그러니 컨트랙트 주소와 함께 이를 복사하여 저장해야 한다.</p>
<h2 id="web3js-컨트랙트-인스턴스화">Web3.js 컨트랙트 인스턴스화</h2>
<p>컨트랙트의 주소와 ABI를 얻고 나면, Web3에서 인스턴스화 할 수 있다.</p>
<pre><code class="language-js">// myContract 인스턴스화
var myContract = new web3js.eth.Contract(myABI, myContractAddress);</code></pre>
<hr>
<h1 id="ch4-컨트랙트-함수-호출">Ch4. 컨트랙트 함수 호출</h1>
<p>Web3.js는 컨트랙트의 함수를 호출하기 위해 우리가 사용할 두 개의 메소드를 가진다.(call과 send)</p>
<h2 id="call">Call</h2>
<p><code>call</code>은 view와 pure 함수를 위해 사용한다. 로컬 노드에서만 실행하고, 블록체인에 트랜잭션을 만들지 않는다.</p>
<p>*복습: view와 pure 함수는 읽기 전용이고 블록체인에서 상태를 변경하지 않는다. 가스를 전혀 소모하지 않고, 메타마스크에서 트랜잭션에 서명하라고 사용자에게 창을 띄우지도 않는다.</p>
<pre><code class="language-js">// 123을 매개 변수로 myMethod라는 이름의 함수를 call한다.
myContract.methods.myMethod(123).call()</code></pre>
<h2 id="send">Send</h2>
<p><code>send</code>는 트랜잭션을 만들고 블록체인 상의 데이터를 변경한다. view와 pure가 아닌 모든 함수에 대해 send를 사용해야 한다.</p>
<p>*참고: 트랜잭션을 send하는 것은 사용자에게 가스를 지불하도록 하고, 메타마스크에서 트랜잭션에 서명하라고 창을 띄울 것이다. Web3 프로바이더로 메타마스크를 사용할 때, send()를 호출하면 자동으로 이 모든 것이 이루어지고, 우리의 코드에 어떤 특별한 것도 추가할 필요가 없다.</p>
<pre><code class="language-js">// 123을 매개 변수로 myMethod라는 이름의 함수를 호출하는 트랜잭션을 send할 수 있다
myContract.methods.myMethod(123).send()</code></pre>
<h2 id="예제">예제</h2>
<p>이제 컨트랙트에서 데이터에 접근하기 위해 call을 사용하는 실제 예제를 보자.</p>
<pre><code class="language-js">// 좀비 배열을 public으로 만듬
Zombiep[] public zombies;</code></pre>
<p>솔리디티에서, public으로 변수를 선언하면 자동으로 같은 이름의 퍼블릭 &quot;getter&quot; 함수를 만들어 진다.
ID 15인 좀비를 찾는다면, <code>zombies(15)</code>와 같이 변수를 함수인 것처럼 호출할 수 있다. </p>
<p>아래 코드는 프론트엔드에서 좀비 ID를 받아 해당 좀비에 대해 컨트랙트에 질의를 보내고, 결과를 반환하는 자바스크립트 함수를 작성하는 방법이다.</p>
<p>*참고: 아래 사용하는 모든 코드 예제들은 콜백 대신 Promise를 사용하는 Web3.js 1.0 버전을 사용하고 있다. 온라인에서 찾을 수 있는 다른 많은 코드들은 Web3.js의 이전 버전을 사용하고 있다. 1.0 버전에서 문법이 많이 바뀌었으니 코드가 다르다면 버전을 참고 해야한다.</p>
<pre><code class="language-js">//Web3 프로바이더와 통신하여 컨트랙트의 Zombie[] public zombies에서 인덱스가 id인 좀비를 반환하도록 한다.
function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// 함수를 호출하고 결과를 가지고 무언가를 처리:
getZombieDetails(15)
.then(function(result) {
  console.log(&quot;Zombie 15: &quot; + JSON.stringify(result));
});

// console result 값 
/*
{
  &quot;name&quot;: &quot;H4XF13LD MORRIS&#39;S COOLER OLDER BROTHER&quot;,
  &quot;dna&quot;: &quot;1337133713371337&quot;,
  &quot;level&quot;: &quot;9999&quot;,
  &quot;readyTime&quot;: &quot;1522498671&quot;,
  &quot;winCount&quot;: &quot;999999999&quot;,
  &quot;lossCount&quot;: &quot;0&quot; // Obviously.
}
*/</code></pre>
<ul>
<li>컨트랙트 통신은 비동기적으로 일어나고 Web3는 Promise를 반환한다.</li>
</ul>
<hr>
<h1 id="ch5-메타마스크--계정">Ch5. 메타마스크 &amp; 계정</h1>
<h2 id="메타마스크에서-사용자-계정-가져오기">메타마스크에서 사용자 계정 가져오기</h2>
<p>주입되어 있는 web3 변수에 현재 활성화된 계정이 무엇인지 아래와 같이 확인할 수 있다.</p>
<pre><code class="language-js">var userAccount = web3.eth.accounts[0]

var accountInterval = setInterval(function() {
  // 계정이 바뀌었는지 확인
  if (web3.eth.accounts[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
    updateInterface();
  }
}, 100);</code></pre>
<p>사용자가 언제든지 메타마스크에서 활성화된 계정을 바꿀 수 있기 때문에, 앱은 이 변수의 값이 바뀌었는지 확인하기 위해 계속 감시를 하고 값이 바뀌면 그에 따라 UI를 업데이트해야 할 것한다.
위 코드는 userAccount가 여전히 web3.eth.accounts[0]과 같은지 100밀리초마다 확인하고 있다.
그렇지 않다면, userAccount에 현재 활성화된 계정을 다시 할당하고, 화면을 업데이트하기 위한 함수를 호출한다.</p>
<hr>
<h1 id="ch6-데이터-보여주기">Ch6. 데이터 보여주기</h1>
<p>startApp() 내에서 getZombiesByOwner의 호출 결과를 써서 호출한 displayZombies의 결과로 좀비 ID 배열([0, 13, 47])을 전달받을 수 있다.</p>
<p>= displayZombies 함수 다음 기능을 한다,</p>
<ol>
<li>먼저 이미 무언가가 #zombies div의 안에 들어 있다면 이 div의 내용을 비운다. 이것은 사용자가 그들의 활성화된 MetaMask 계정을 변경하면 새로운 좀비 군대를 로딩하기 전에 기존의 것을 삭제하는 것과 같다.</li>
<li>반복을 통해 각 id마다 getZombieDetails(id)를 호출해서 우리의 스마트 컨트랙트에서 좀비에 대한 모든 정보를 찾는다.</li>
<li>화면에 표시하기 위해 HTML 템플릿에 좀비에 대한 정보를 집어넣고, 해당 템플릿을 #zombies div에 붙여넣는다.</li>
</ol>
<pre><code class="language-js">// 우리 컨트랙트에서 좀비 상세 정보를 찾아, `zombie` 객체 반환
getZombieDetails(id)
.then(function(zombie) {
  // HTML에 변수를 넣기 위해 ES6의 &quot;template literal&quot; 사용
  // 각각을 #zombies div에 붙여넣기
  $(&quot;#zombies&quot;).append(`&lt;div class=&quot;zombie&quot;&gt;
    &lt;ul&gt;
      &lt;li&gt;Name: ${zombie.name}&lt;/li&gt;
      &lt;li&gt;DNA: ${zombie.dna}&lt;/li&gt;
      &lt;li&gt;Level: ${zombie.level}&lt;/li&gt;
      &lt;li&gt;Wins: ${zombie.winCount}&lt;/li&gt;
      &lt;li&gt;Losses: ${zombie.lossCount}&lt;/li&gt;
      &lt;li&gt;Ready Time: ${zombie.readyTime}&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;`);
});</code></pre>
<hr>
<h1 id="ch7-트랜젝션">Ch7. 트랜젝션</h1>
<p><strong>send 함수를 이용해 스마트 컨트랙트의 데이터를 변경하는 방법</strong></p>
<ol>
<li>트랜잭션을 전송(send)하려면 함수를 호출한 사람(from) 주소가 필요하다.(솔리디티 코드에서는 msg.sender와 같음). 이는 DApp의 사용자가 되어야 하고 메타마스크가 나타나 그들에게 서명을 하도록 한다.</li>
<li>트랜잭션 전송(send)은 가스를 소모한다.</li>
<li>사용자가 트랜잭션 전송을 하고 난 후 실제로 블록체인에 적용될 때까지는 상당한 지연이 발생할 것이다. 트랜잭션이 블록에 포함될 때까지 기다려야 하는데, 이더리움의 평균 블록 시간이 15초이기 때문이지. 만약 이더리움에 보류 중인 거래가 많거나 사용자가 가스 가격을 지나치게 낮게 보낼 경우, 우리 트랜잭션이 블록에 포함되길 기다려야 하고, 이는 몇 분씩 걸릴 수 있다.</li>
</ol>
<p>그러니 이 코드의 비동기적 특성을 다루기 위한 로직이 필요하다.</p>
<h2 id="좀비-만들기">좀비 만들기</h2>
<p>=  솔리디티 코드</p>
<pre><code class="language-js">function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}</code></pre>
<p>= web3에서 위의 함수를 호출하는 코드</p>
<pre><code class="language-js">function createRandomZombie(name) {
  // 시간이 꽤 걸릴 수 있으니, 트랜잭션이 보내졌다는 것을
  // 유저가 알 수 있도록 UI를 업데이트해야 함
  $(&quot;#txStatus&quot;).text(&quot;Creating new zombie on the blockchain. This may take a while...&quot;);
  // 우리 컨트랙트에 전송하기:
  return CryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on(&quot;receipt&quot;, function(receipt) {
    $(&quot;#txStatus&quot;).text(&quot;Successfully created &quot; + name + &quot;!&quot;);
    // 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on(&quot;error&quot;, function(error) {
    // 사용자들에게 트랜잭션이 실패했음을 알려주기 위한 처리
    $(&quot;#txStatus&quot;).text(error);
  });
}</code></pre>
<p>= Web3 프로바이더에게 트랜잭션을 전송(send) 후, 이벤트 리스너 연결</p>
<ul>
<li><code>receipt</code> : 트랜잭션이 이더리움의 블록에 포함될 때, 즉 좀비가 생성되고 우리의 컨트랙트에 저장되었을 때 발생한다. </li>
<li><code>error</code> : 트랜잭션이 블럭에 포함되지 못했을 때, 예를 들어 사용자가 충분한 가스를 전송하지 않았을 때 발생하게 된다. 우리는 우리의 UI를 통해 사용자에게 트랜잭션이 전송되지 않았음을 알리고, 다시 시도할 수 있도록 해야한다.</li>
</ul>
<p>*참고: 자네가 send를 호출할 때 gas와 gasPrice를 선택적으로 지정할 수 있다. (<code>.send({ from: userAccount, gas: 3000000 })</code>). 만약 지정하지 않는다면, 메타마스크는 사용자가 이 값들을 선택할 수 있도록 한다.</p>
<hr>
<h1 id="ch8-payable-함수-호출">Ch8. Payable 함수 호출</h1>
<p>= 솔리디티 코드(ZombieHelper.sol)</p>
<pre><code class="language-js">function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}</code></pre>
<ul>
<li>이더가 아니라 wei로 얼마를 보낼지 정해야 한다.</li>
</ul>
<h2 id="wei란">Wei란?</h2>
<ul>
<li>wei : 이더의 가장 작은 하위 단위이다.(1 eth = 10^18 wei)</li>
</ul>
<pre><code class="language-js">// web3 변환 유틸리티
web3js.utils.toWei(&quot;1&quot;); // 1 ETH를 Wei로 변경

// levelUp함수 호출
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei(&quot;0.001&quot;) })  // levelUpFee = 0.001 ether로 설정</code></pre>
<hr>
<h1 id="ch9-event-구독">Ch9. Event 구독</h1>
<h2 id="새로운-좀비-수선">새로운 좀비 수선</h2>
<p>zombiefactory.sol에서 새로운 좀비가 생성될 때마다 매번 호출되던 NewZombie라는 이벤트가 있다.
<code>event NewZombie(uint zombieId, string name, uint dna);</code></p>
<p>Web3.js에서 이벤트를 구독하여 해당 이벤트가 발생할 때마다 Web3 프로바이더가 해당 코드 내의 어떠한 로직을 실행시키도록 할 수 있다.</p>
<pre><code class="language-js">cryptoZombies.events.NewZombie()
.on(&quot;data&quot;, function(event) {
  let zombie = event.returnValues;
  // `event.returnValue` 객체에서 이 이벤트의 세 가지 반환 값에 접근할 수 있네:
  console.log(&quot;새로운 좀비가 태어났습니다!&quot;, zombie.zombieId, zombie.name, zombie.dna);
}).on(&quot;error&quot;, console.error);</code></pre>
<p>DApp에서 현재 사용자의 좀비만이 아니라 어떤 좀비가 생성되든지 항상 알림을 보낼 것이다.
현재 사용자가 만든 것에 대해서만 알림을 보내고 싶다면 어떻게 해야 할까?</p>
<h2 id="indexed">Indexed</h2>
<p>이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해, ERC721을 구현할 때 Transfer 이벤트에서 했던 것처럼 솔리디티 컨트랙트에 indexed 키워드를 사용해야 한다.</p>
<pre><code class="language-js">// 솔리디티 소스
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
// _from과 _to가 indexed 되어 있기 때문에, 프론트엔드의 이벤트 리스너에서 이들을 필터링할 수 있다.

//  프론트
// `filter`를 사용해 `_to`가 `userAccount`와 같을 때만 코드를 실행
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on(&quot;data&quot;, function(event) {
  let data = event.returnValues;
  // 현재 사용자가 방금 좀비를 받았네!
}).on(&quot;error&quot;, console.error);
</code></pre>
<p>event와 indexed 영역을 사용하는 것은 컨트랙트에서 변화를 감지하고 프론트엔드에 반영할 수 있게 하는 유용한 방법이다.</p>
<h2 id="지난-이벤트에-대해-질의">지난 이벤트에 대해 질의</h2>
<p><code>getPastEvents</code>를 이용해 지난 이벤트들에 대해 질의를 하고, <code>fromBlock</code>과 <code>toBlock</code> 필터들을 이용해 이벤트 로그에 대한 시간 범위를 솔리디티에 전달할 수 있다.(여기서 &quot;block&quot;은 이더리움 블록 번호를 나타낸다).</p>
<pre><code class="language-js">cryptoZombies.getPastEvents(&quot;NewZombie&quot;, { fromBlock: 0, toBlock: &quot;latest&quot; })
.then(function(events) {
  // `events`는 우리가 위에서 했던 것처럼 반복 접근할 `event` 객체들의 배열이네.
  // 이 코드는 생성된 모든 좀비의 목록을 우리가 받을 수 있게 할 것이네.
});</code></pre>
<p>위 메소드를 사용해서 시작 시간부터의 이벤트 로그들에 대해 질의를 할 수 있기 때문에, 이를 통해 이벤트를 저렴한 형태의 storage로 사용하는 것 예시를 만들 수 있다.</p>
<p>다시 생각해보면, 데이터를 블록체인에 기록하는 것은 솔리디티에서 가장 비싼 비용을 지불하는 작업 중 하나이다. 하지만 이벤트를 이용하는 것은 가스 측면에서 훨씬 더 저렴하다.</p>
<p>여기서 단점이 되는 부분은 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다는 것이다. 하지만 히스토리로 블록체인에 기록하여 앱의 프론트엔드에서 읽기를 원하는 데이터가 있다면, 이는 새겨놓아야 할 중요한 사용 예시이다.</p>
<p>예를 들어, 우린 이것을 좀비 전투의 히스토리 기록용으로 사용할 수 있다. 좀비가 다른 좀비를 공격할 때마다, 그리고 누군가 이길 때마다 우린 이벤트를 생성할 수 있다. 스마트 컨트랙트는 추후 결과를 계산할 때 이 데이터가 필요하지 않지만, 사용자들이 앱의 프론트엔드에서 찾아볼 수 있는 유용한 데이터이다.</p>
<hr>
<p><strong>전체 소스</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;CryptoZombies front-end&lt;/title&gt;
    &lt;script language=&quot;javascript&quot; type=&quot;text/javascript&quot; src=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js&quot;&gt;&lt;/script&gt;
    &lt;script language=&quot;javascript&quot; type=&quot;text/javascript&quot; src=&quot;web3.min.js&quot;&gt;&lt;/script&gt;
    &lt;script language=&quot;javascript&quot; type=&quot;text/javascript&quot; src=&quot;cryptozombies_abi.js&quot;&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;txStatus&quot;&gt;&lt;/div&gt;
    &lt;div id=&quot;zombies&quot;&gt;&lt;/div&gt;

    &lt;script&gt;
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = &quot;YOUR_CONTRACT_ADDRESS&quot;;
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

        var accountInterval = setInterval(function() {

          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];

            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);


        cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
          .on(&quot;data&quot;, function(event) {
            let data = event.returnValues;


            getZombiesByOwner(userAccount).then(displayZombies);
          }).on(&quot;error&quot;, console.error);
      }

      function displayZombies(ids) {
        $(&quot;#zombies&quot;).empty();
        for (id of ids) {

          getZombieDetails(id)
          .then(function(zombie) {


            $(&quot;#zombies&quot;).append(`&lt;div class=&quot;zombie&quot;&gt;
              &lt;ul&gt;
                &lt;li&gt;Name: ${zombie.name}&lt;/li&gt;
                &lt;li&gt;DNA: ${zombie.dna}&lt;/li&gt;
                &lt;li&gt;Level: ${zombie.level}&lt;/li&gt;
                &lt;li&gt;Wins: ${zombie.winCount}&lt;/li&gt;
                &lt;li&gt;Losses: ${zombie.lossCount}&lt;/li&gt;
                &lt;li&gt;Ready Time: ${zombie.readyTime}&lt;/li&gt;
              &lt;/ul&gt;
            &lt;/div&gt;`);
          });
        }
      }

      function createRandomZombie(name) {


        $(&quot;#txStatus&quot;).text(&quot;Creating new zombie on the blockchain. This may take a while...&quot;);

        return CryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on(&quot;receipt&quot;, function(receipt) {
          $(&quot;#txStatus&quot;).text(&quot;Successfully created &quot; + name + &quot;!&quot;);

          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on(&quot;error&quot;, function(error) {

          $(&quot;#txStatus&quot;).text(error);
        });
      }

      function feedOnKitty(zombieId, kittyId) {
        $(&quot;#txStatus&quot;).text(&quot;Eating a kitty. This may take a while...&quot;);
        return CryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on(&quot;receipt&quot;, function(receipt) {
          $(&quot;#txStatus&quot;).text(&quot;Ate a kitty and spawned a new Zombie!&quot;);
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on(&quot;error&quot;, function(error) {
          $(&quot;#txStatus&quot;).text(error);
        });
      }

      function levelUp(zombieId) {
        $(&quot;#txStatus&quot;).text(&quot;좀비를 레벨업하는 중...&quot;);
        return CryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3.utils.toWei(&quot;0.001&quot;) })
        .on(&quot;receipt&quot;, function(receipt) {
          $(&quot;#txStatus&quot;).text(&quot;압도적인 힘! 좀비가 성공적으로 레벨업했습니다.&quot;);
        })
        .on(&quot;error&quot;, function(error) {
          $(&quot;#txStatus&quot;).text(error);
        });
      }

      function getZombieDetails(id) {
        return cryptoZombies.methods.zombies(id).call()
      }

      function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }

      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

      window.addEventListener(&#39;load&#39;, function() {


        if (typeof web3 !== &#39;undefined&#39;) {

          web3js = new Web3(web3.currentProvider);
        } else {


        }


        startApp()

      })
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><strong>좀더 생각해볼 것들</strong></p>
<ol>
<li><p>attack, changeName, changeDna, 그리고 ERC721 함수인 transfer, ownerOf, balanceOf 함수를 구현한다. 이런 함수들의 구현은 우리가 다룬 모든 다른 send 트랜잭션과 동일하다.</p>
</li>
<li><p>setKittyContractAddress, setLevelUpFee, 그리고 withdraw를 실행할 수 있는 &quot;관리 페이지&quot;를 구현한다. 이러한 구현들은 우리가 이미 다룬 함수들과 동일할 것이다. 그저 해당 컨트랙트를 배포했던 이더리움 주소에서 이 함수들을 호출했는지 확인하면 된다. 이 함수들은 onlyOwner 제어자를 가지고 있다.</p>
</li>
<li><p>이 앱에서 구현하고 싶은 다른 몇 가지 화면이 있다.</p>
</li>
</ol>
<ul>
<li>개별 좀비 페이지: 특정 좀비에 대한 영구적인 링크를 통해 그 좀비의 정보를 볼 수 있는 곳이다. 이 페이지에서는 좀비의 외관과 이름, 주인(사용자 프로필 페이지에 대한 링크와 함께), 승리/패배 횟수, 전투 기록, 기타 등등을 보여줄 것이다.</li>
<li>사용자 페이지: 영구적인 링크를 통해 사용자의 좀비 군대를 볼 수 있는 곳이다. 개별 좀비를 클릭하여 해당 페이지를 볼 수 있을 것이고, 자네가 메타마스크에 로그인되어 있고, 군대를 가지고 있다면 좀비를 클릭해 공격할 수도 있을 것이다.</li>
<li>홈페이지: 현재 사용자의 좀비 군대를 볼 수 있는, 사용자 페이지의 한 종류이다(우리가 index.html에서 구현학 시작했던 곳이다).</li>
</ul>
<ol start="4">
<li><p>UI 상에서 사용자가 크립토키티를 먹이로 줄 수 있는 방법이 있어야 한다. 홈페이지에서 각 좀비 옆에 &quot;먹이 주기&quot; 같은 버튼을 만들고, 사용자가 고양이의 ID를 입력하게 하는 텍스트 박스를 만들 수 있다.(또는 그 고양이의 URL, 예를 들면: <a href="https://www.cryptokitties.co/kitty/578397">https://www.cryptokitties.co/kitty/578397</a>). 이 버튼은 feedOnKitty 함수를 호출할 것이네.</p>
</li>
<li><p>UI 상에서 한 사용자가 다른 사용자의 좀비를 공격할 수 있는 방법이 있어야 한다.
이를 구현하는 하나의 방법은 한 사용자가 다른 사용자의 페이지로 들어가면, &quot;이 좀비 공격하기&quot; 버튼을 보여주는 것이다. 사용자가 그 버튼을 클릭하면, 현재 사용자의 좀비 군대를 포함하는 모달 창을 띄우고 &quot;어떤 좀비로 공격하시겠습니까?&quot; 메세지를 보여주면 된다.
또 사용자의 홈페이지에서 각 좀비 옆에 &quot;좀비 공격하기&quot; 버튼을 둘 수도 있다. 사용자가 그걸 클릭하면, 사용자가 좀비의 ID를 입력하여 찾을 수 있는 찾기 영역을 가지는 모달 창을 띄울 수 있다. 또는 &quot;아무 좀비나 공격하기&quot; 같은 옵션을 줘서 임의로 찾을 수도 있을 것이다.
그리고 쿨다운 기간이 아직 다 지나지 않은 사용자의 좀비는 회색 처리를 할 수도 있다. UI 상에서 사용자에게 해당 좀비로는 아직 공격할 수 없고 얼마나 더 기다려야 하는지 보여줘야한다.</p>
</li>
<li><p>사용자의 홈페이지에는 각 좀비의 이름 또는 DNA를 바꾸고, 일정 비용을 내고 레벨업을 할 수 있는 옵션이 있을 수 있다. 사용자의 레벨이 충분하지 않으면 어떤 옵션들을 회색 처리를 할 수 있다.</p>
</li>
<li><p>새로운 사용자들을 위해, createRandomZombie()를 호출해 군대의 첫 번째 좀비를 만들 수 있는 입력 창과 함께 환영 메세지를 보여줄 수 있다.</p>
</li>
<li><p>마지막 챕터에서 논의한 것처럼, 우리 스마트 컨트랙트에 indexed 프로퍼티로 사용자의 address를 가지는 Attack 이벤트를 추가하고 싶을 수 있다. 이를 통해 실시간 알림을 만들 수 있을 것이다 - 사용자에게 그의 좀비가 공격당하면 알림 창을 띄워 알려주어, 그를 공격한 사용자/좀비를 보여주고 복수할 수 있게 하는 것이다.</p>
</li>
<li><p>또한 일종의 프론트엔드 캐시 계층을 구현하여 똑같은 데이터를 위해 Infura에 계속 접근하지는 않도록 하고 싶을 수 있다(우리의 현재 displayZombies 구현은 인터페이스를 새로고침할 때마다 각 좀비에 대해 getZombieDetails를 호출하지 - 하지만 현실적으로 우리의 군대에 추가된 새 좀비에 대해서만 이 함수를 호출하면 된다).</p>
</li>
</ol>
<p>10.실시간 채팅방을 만들기.</p>
<hr>
<p>** 내가 받은 좀비들 **
<img src="https://media.vlpt.us/images/ella-front-dev/post/bc096497-5be5-4f26-a9cc-1c1fd9b39140/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-04-08%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%202.44.26.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>