<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>waffle_bear.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 07 Mar 2024 08:36:26 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>waffle_bear.log</title>
            <url>https://velog.velcdn.com/images/waffle_bear/profile/4c7b3bdf-68e9-4d02-acaa-b5b69e263af6/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. waffle_bear.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/waffle_bear" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[취업 성공] 비전공자 개발자 취업 성공🥲]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5</link>
            <guid>https://velog.io/@waffle_bear/%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%B7%A8%EC%97%85-%EC%84%B1%EA%B3%B5</guid>
            <pubDate>Thu, 07 Mar 2024 08:36:26 GMT</pubDate>
            <description><![CDATA[<h3 id="소감">소감</h3>
<p>어.. 내가 이런 글을 쓰게 될 줄은 몰랐다.
요즘 같은 빙하기에 취업 성공이라는 단어를 쓸 수 있음에 감사한다.</p>
<p>아래에 적는 내용은 <strong>늦었다 싶어 고민하는 개발자 취준생,
비전공자 개발자 취준생에게 도움이 되었으면 한다</strong>.</p>
<h2 id="캠프-이전의-삶">캠프 이전의 삶</h2>
<h3 id="1-터벅터벅-나의-일생">1. 터벅터벅 나의 일생</h3>
<p>이과생으로 고등학교 졸업 후 전공은 신학,
직업은 영상 제작자로 아주 다이나믹한 삶이었다.</p>
<p>20대 후반,
어느 정도 경험할 것을 해보고 자리를 잡을 나이에
나는 번아웃이 찾아왔고</p>
<p>&#39;영상을 평생할 수 있을까...&#39; 싶던 나에게
<strong>개발자 붐</strong> 소식이 들렸다.</p>
<p>전에 회사에서 일할 때 봤던 사내 개발자분들..🤔
생각해보니 멋있던 거 같기도 하고..🤔
<del>(팀 섭외, 장소 섭외, 차량 및 장비 대여, 밤샘 촬영, 밤샘 편집이 없네...?🤩)</del></p>
<p>개발자로 전직하는 결심에는 감정적인 부분이 분명히 있긴 하다.</p>
<p>하지만 개발에서 매력을 느껴서 전직을 결심했고
개발에서 <strong>제일 매력적인 부분</strong>은 <strong>코드 하나로 만드는 엄청난 것들</strong>이었다.</p>
<h3 id="아-근데-이때-난-입대를-하게-됐다"><strong>아 근데 이때 난 입대를 하게 됐다.</strong></h3>
<h3 id="개발자-붐-시기에"><strong>개발자 붐 시기에..</strong></h3>
<h3 id="27세의-나이에🥲"><strong>27세의 나이에..🥲</strong></h3>
<hr>
<h3 id="2-혼자서도-잘-해낼-줄">2. 혼자서도 잘 해낼 줄..</h3>
<p><strong>[ 제대 후 ]</strong>
난 복무 기간에도 코딩 공부를 했다.
파이썬 겉핥기, C 겉핥기.. 등
시간 낭비가 맞는 말인가...ㅎ</p>
<p>암튼 <strong>나이 29</strong>.
본격적으로 강의를 듣기 시작했다.
그렇게 5개월 정도 공부하고 만든 포폴로
(아주 자쒼 있게) 이력서를 제출했다.</p>
<p><strong>MERN 스택으로다가 아주 기가 막힌!</strong>(지극히 주관적인 생각이었다.)</p>
<h3 id="나의-포폴">(나의 포폴)</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/c579a6e3-0bbe-4cf4-9aae-37ac040d716c/image.png" alt=""></p>
<h2 id="어-근데-개발자-붐이-끝났다">어. 근데 개발자 붐이 끝났다.</h2>
<p>이력서를 안 읽는다.
군복무 2년동안 뽑을 신입은 다 뽑은 것이다.</p>
<blockquote>
<p>24-03-07 글을 작성하는 오늘.
예전 생각이 나서 입사지원 현황 보니까
아직도 내 이력서 안 읽은 곳이 있네 (8개월째 안읽씹 중)</p>
<h4 id="2023-07-20-입사지원png">2023-07-20 입사지원.png</h4>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/2a802867-6c3a-43a5-a0d1-4e5f74b28220/image.png" alt=""></p>
</blockquote>
<h4 id="암튼-이렇게-혼자서도-잘해요는-끝나고">암튼 이렇게 혼자서도 잘해요는 끝나고..</h4>
<h4 id="취업-연계를-받자-하는-마음으로">취업 연계를 받자!! 하는 마음으로</h4>
<h4 id="스파르타-코딩-클럽-내배캠을-시작했다">스파르타 코딩 클럽 내배캠을 시작했다.</h4>
<hr>
<h2 id="캠프-시작-전-내-솔직한-생각">캠프 시작 전 내 솔직한 생각</h2>
<p>캠프 시작 전 나의 인식은 사실 부정적이었다.
저런 곳은 그냥 양산형 개발자 만들고</p>
<p>코딩 노예로 보내도 취업은 취업이니까
취업률이 높은 거 아닌가... 하는 정도였다.</p>
<p>캠프 수료 후
내가 아래의 짤처럼 취업하게 되는 것이 아닐까.. 싶었다.</p>
<p>(마치 일용직을 봉고에 데려가는 느낌으로 개발자를 데려가는 유명 짤..)
<img src="https://velog.velcdn.com/images/waffle_bear/post/be351641-4d23-450c-867b-6329f86dea1f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/fd411dcf-0db1-4745-a873-c4e45fadef76/image.png" alt="">
-출처: Youtube &lt;조코딩 Jocoding&gt; 채널의 &#39;좋코딩&#39; 중-</p>
<br>

<h3 id="근데-캠프에서-생애-첫-롤모델이-생겼다"><strong>근데 캠프에서 생애 첫 롤모델이 생겼다.</strong></h3>
<br>
<br>

<h2 id="캠프-회고">캠프 회고</h2>
<h3 id="1-터벅터벅-캠프-일상">1. 터벅터벅 캠프 일상</h3>
<p>캠프 시작.
난 <strong>웹 트랙_프론트엔드 코스(React 코스)를 선택</strong>했다.</p>
<p>현재 주변을 둘러보면 웹이 없는 곳이 없고,
JS가 범용성이 높아지면서 모바일 앱까지 만들 수 있으니까 선택했다.</p>
<h4 id="-일정--은-크게-아래와-같았다">[ 일정 ] 은 크게 아래와 같았다</h4>
<ul>
<li>웹 기초 - 브라우저 / HTML / CSS / JS</li>
<li>주특기 입문, 숙련, 심화 - React 기초 / 상태 관리 / 최적화 등</li>
<li>주특기 플러스 (Next js)</li>
<li>최종 프로젝트 (이전 협업의 2배의 기간동안 만드는 결정체)</li>
<li>수료 후</li>
</ul>
<p>국비교육은 양산형 개발자 생산이라는
부정적 인식이 있던 나에게는 <strong>정말 의외</strong>였다.</p>
<p>이외에도 <strong>매니저님들의 관리</strong>나
<strong>강의 자료와 영상</strong>이 엄청 <strong>탄탄</strong>했다.</p>
<h4 id="-장점-">[ 장점 ]</h4>
<p>사람에 따라 다를 수 있겠지만
나는 <strong>수강생 관리, 강사진, 강의 자료</strong>
그리고 <strong>강력 추천</strong>하는 부분은 <strong>협업 경험</strong>이 있겠다.</p>
<ul>
<li><p><strong>협업 경험</strong>
개발자들이 왜 자꾸 협업을 중요하게 생각하는지
이해 안 되는 사람이 있을 수 있겠지.. 나도 그랬다.</p>
<p>개발은 혼자서도 가능은 하지만</p>
</li>
<li><p><em>협업을 할 때 폭발적인 개발이 가능하기 때문*</em>인데.</p>
<p>그 폭발이 <strong>프로젝트가 폭발하는가</strong>
<strong>시너지가 폭발하는가</strong> 둘 중 하나라서
협업이 중요한 것이다.</p>
<p>캠프에서 경험하는 팀 프로젝트는
<strong>협업의 중요성, 좋은 팀원에 대한 가치관</strong>을 알 수 있게 하고</p>
<p><strong>기술 스택 선정, 프로젝트 개요, 요구 사항 분석</strong> 등
<strong>현업을 간접적으로 경험</strong>할 수 있는 것의 좋은 체험 학습이었다.</p>
</li>
</ul>
<ul>
<li><p><strong>엄청난 강사진</strong>
캠프 중 <strong>양질의 강의 자료와 설명</strong>들
정말 정말 <strong>강력하게 추천</strong>한다.</p>
</li>
<li><p><em>일정이 끝난 오후 9시 이후*</em>에도
현업 꿀팁, 다양한 라이브러리 설명, 보충 설명 등</p>
</li>
<li><p><em>끊임없이 수강생들에게 지식을 제공하는 강사진*</em>..⭐</p>
<p>온라인 수료식 때 카메라 속 <strong>모든 튜터님들이 전부 다 멋있었다</strong>.
태어나서 그런 웅장함은 처음 느껴봤다.</p>
<p>모든 튜터님들을 존경하지만,
그 중 잊지 못 할 모두의 잉끼 튜터.. 최원장 튜터님⭐</p>
<p>연예인한테도 관심이 없는 내가
튜터님을 롤모델로 정하게 됐다.</p>
</li>
</ul>
<hr>
<h2 id="캠프-수료-후">캠프 수료 후</h2>
<h3 id="1-망망대해-같은-이력서-작성-중-나침반">1. 망망대해 같은 이력서 작성 중 나침반</h3>
<p> 이 소제목은 내가 커리어 매니저님께 <strong>실제로 했던 말</strong>이다.
 솔직히 이력서, 자소서 얼마나 막막한가..</p>
<p> 이제 신입이
 트러블 슈팅이나 기술 스택 선정에 얼마나 심도가 있겠으며</p>
<p> 특히 유관경력 없고 비전공자인 나에게
 개발자로서의 핵심 역량이 어딨겠냐는 것이다.</p>
<h3 id="하지만-아주-빠르게-이력서를-통과했다">하지만 아주 빠르게 이력서를 통과했다!</h3>
<p> 수료 후 취업지원 주차에 들은 특강을 듣고
 술술 써내려 갔던 이력서가 통과됐다.</p>
 <br>

<h2 id="취업지원-주차의-장점">취업지원 주차의 장점</h2>
<h3 id="1-이력서를-보기-좋게-작성하는-방법과-탬플릿을-제공">1. 이력서를 보기 좋게 작성하는 방법과 탬플릿을 제공</h3>
<p> <strong>이력서를 한 눈에 볼 수 있도록 제작 된 템플릿</strong>을 받았다.
 탬플릿은 <strong>섹션 별로 딱딱 배치</strong>가 되어있어 작성하는 내가 봐도 <strong>깔끔했다</strong>.</p>
<h3 id="2-섹션-별-적절한-예시와-설명">2. 섹션 별 적절한 예시와 설명</h3>
<p> 깔끔한 탬플릿을 받았어도
 그 <strong>섹션을 채워나가는 건 본인</strong>이지 않은가..?
 분명 막막할 수 있다.</p>
<p> 근데 커리어 매니저님이 아주 <strong>좋은 예시들을 보여주며</strong>
 특강을 진행하시는데 그 특강을 듣는다면
 망망대해에서 발견한 나침반 같은 느낌이 들 것이다.</p>
<h2 id="마무리">마무리</h2>
<p> 이 회고의 시작에서도 말했지만,
 나이 때문에, 학력 때문에 고민한다면
 아직 많이 남은 날 중에 딱 4개월만
 스스로에게 기회를 줘보면 좋겠다.</p>
<p> 나 같은 경우에는
 <strong>그 기회를 스파르타 코딩 클럽에서 잡았고</strong>
 <strong>만족스러운 결과</strong>를 얻었기 때문에 이렇게 추천을 해본다.</p>
<p> 고민은 시간만 늦출 뿐이란 것을 알 것이다.
 비전공자에 유관 경력이 없는 나도 됐다.</p>
<p> 고민은 조금만 하고
 시작해보면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Thu, 25 Jan 2024 12:44:41 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>MVP에서 조금씩 기능을 추가하고 디버깅..</li>
<li>모바일 대응</li>
<li>뒤로가기 시 페이지 전환 없이 UI 제어</li>
</ul>
<h2 id="작은-회고">작은 회고</h2>
<ul>
<li>많은 일들이 있었지만 MVP 일정에 쫓겨 TIL을 작성하지 못 했다..
정말 많은 디버깅과 리팩터링 추가 기능 구현 등...... 많은 것을 했지만,
힘든 날에 TIL을 쓰지 못 했던 것을 반성한다..</li>
</ul>
<h2 id="뒤로가기-시-페이지-전환-없이-ui-제어">뒤로가기 시 페이지 전환 없이 UI 제어</h2>
<h4 id="개발-1010시간-중에-제일-힘들었다"><strong>개발 1010시간 중에 제일 힘들었다</strong></h4>
<p>navigate, history 등 여러 정보를 찾아보고
뒤로가기 이벤트를 감지할 수 있는 window의 이벤트 종류까지😫</p>
<h4 id="아니-근데-고민했던-것보다-너무-간단한-거-아니오">(아니 근데 고민했던 것보다... 너무 간단한 거 아니오....)</h4>
<h3 id="1-문제의-발단">1. 문제의 발단</h3>
<ul>
<li><p>채팅 페이지는 처음 채팅방 목록과 채팅메세지가 뜨는 컴포넌트로 분리해놨었다.
(반응형 디자인을 보고 이것이 신의 한 수였다는 것을 깨달았다..)</p>
</li>
<li><p>분리가 잘 되어있던 컴포넌트의 스타일을 변경하여
768px 이하에서는 <strong>메인에 채팅방 목록</strong>이, <strong>채팅방을 클릭하면 채팅창이 열리는 구조</strong>인데</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/7df07b48-2d90-4190-9df1-d1620d7aa215/image.gif" alt=""></p>
<h4 id="😫-ui에-만들어놓은-뒤로가기를-누르지-않으면-완전히-이전-페이지로-간다😫">😫 UI에 만들어놓은 뒤로가기를 누르지 않으면, 완전히 이전 페이지로 간다..😫</h4>
<h2 id="2-문제-접근">2. 문제 접근</h2>
<blockquote>
<p>&quot;클릭 이벤트처럼 이벤트를 감지하고 막는 뭔가가 있겠지...&quot;</p>
</blockquote>
<p>근데 그런 건 없었다 엉-엉-🥺</p>
<p>하지만 window 이벤트 리스너의 <code>popstate</code>를 찾게 되었는데
<code>window.history.pushState()</code>를 통해 쌓은 스택을
뒤로가기 버튼 클릭 시 popState 하는 것이다.
배열의 push, pop과 같았다. 여기서 콧구멍이 조금 드릉드릉 했던 거 같다.</p>
<p>간단하게 <code>pushState</code>,  <code>popState</code>를 설명해보자면
어떤 요소를 클릭하면 pushState를 하여 스택을 쌓도록 해놓은
페이지에 방문을 했다 가정을 해보자</p>
<pre><code>[클라이언트의 활동]           [스택]

   페이지 방문       =&gt;        0
  pushState작동     =&gt;        1
  또 pushState      =&gt;        2
뒤로가기 (popstate)  =&gt;        1
뒤로가기 (popstate)  =&gt;        0
뒤로가기 (popstate)  =&gt;  이전 페이지로 전환</code></pre><p>이런 느낌이다. 
원래는 뒤로가기를 누르면 해당 페이지에 어떤 것이 떠있든
스택이 0일 때는 뒤로가기는 기본 작동을 막을 수가 없다.
다만, <strong>스택이 하나라도 있다면 뒤로가기를
내가 제어할 수 있다는 것!</strong></p>
<p>하지만 위의 그림(?)처럼 어떤 요소를 클릭해서 스택을 쌓았다면
뒤로가기 시 페이지 전환은 없이 스택이 하나 제거되고 끝이나는 것이다.</p>
<h2 id="3-해결">3. 해결</h2>
<ul>
<li>이제 내가 뭘 해야 되는지 확실히 알았다🥹<h3 id="1-스택-쌓기">1. 스택 쌓기</h3>
먼저 채팅방을 눌렀을 때 채팅창 컴포넌트가 등장하도록 하는
함수에 <code>pushState(null, &#39;&#39;, &#39;&#39;)</code> 이렇게 빈 스택을 쌓도록 하였다.
이제 채팅방을 하나 누를 때마다 빈 스택이 하나씩 쌓인다.<pre><code class="language-ts">// 채팅방을 누르면 채팅창 styled-components를 조작해
// 우측에서 채팅창이 쇽- 등장하게 하는 함수
const handleBoardPosition = () =&gt; {
 // 채팅방을 하나씩 들어갈 때마다 스택하나 추가
 window.history.pushState(null, &#39;&#39;, &#39;&#39;);
 setboardPosition(0);
};</code></pre>
</li>
</ul>
<h3 id="2-뒤로가기-시-스택-제거-채팅창-원위치">2. 뒤로가기 시 스택 제거, 채팅창 원위치</h3>
<pre><code class="language-ts">useEffect(() =&gt; {
  const handlePopState = () =&gt; {
    // 뒤로가기 누르면 스택이 하나 빠지고
    if (boardPosition === 0) {
      // 만약 채팅창이 나와있으면 원위치로 돌려주고
      handleHideBoardPosition();
    } else {
      // 스택이 없으면 바로 이전 페이지로
      navi(-1);
    }
  };

  // 뒤로가기 이벤트를 감지하기 위한 리스너
  window.addEventListener(&#39;popstate&#39;, handleClickBrowserBackBtn);

  // 언마운트 시 리스너 제거
  return () =&gt; {
    window.removeEventListener(&#39;popstate&#39;, handleClickBrowserBackBtn);
  };
}, [boardPosition]);</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/51bed830-ec03-4ee0-84f9-b64a21c5c0fb/image.gif" alt=""></p>
<h3 id="느낀점">느낀점</h3>
<p>해결하면서 정말 힘들었고
어떻게 하면 될 거 같다! 라는 생각이 막혀서 더 힘들었다.
정말 유용하고 좋은 것을 배웠고
해결한 후에는 감동적이었다.. 광광 우럭따...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트 - 찜하기]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Jan 2024 09:12:08 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>찜하기 기능 구현</li>
<li>채팅방 목록의 각 채팅방의 마지막 메세지 관련 정보 표기(마지막 메세지, 경과 시간)</li>
</ul>
<h2 id="1-채팅방-목록에-정보-표기">1. 채팅방 목록에 정보 표기</h2>
<h3 id="접근-방법">접근 방법</h3>
<ul>
<li>먼저 채팅방 목록 컴포넌트를 분리했다.
그리고 처음에는 이거를 방이 생성되는 map에 돌려서 id값 비교 후
표기가 되도록 해야되나.. 했지만
방 개수에 비해 메세지가 심하게 많아서 index값이 닿지 않아서
포기.. 그러다 생각난 함수 하나!!</li>
</ul>
<h3 id="1-1-메세지들-가져와-state에-담기">1-1. 메세지들 가져와 state에 담기</h3>
<ul>
<li><p>DB에서 메세지들을 가져와 state에 담고
각 채팅방 id를 인자로 받아 해당 채팅방에 속한 메세지만
filter 후 시간순으로 정렬 하고
index 제일 마지막 부분만 뙇 반환하는 함수🥹🥹🥹</p>
<pre><code class="language-ts">const findMatchMessage = (room: string): any =&gt; {
  if (allMessage !== null) {
    const Matched = allMessage
      .map((msg) =&gt; {
        return msg.chat_room_id === room &amp;&amp; msg;
      })
      .sort((a: any, b: any) =&gt; a.created_at - b.created_at)
      .filter((msg) =&gt; msg !== false);

    return Matched[Matched.length - 1];
  }
};</code></pre>
</li>
</ul>
<pre><code class="language-ts">&lt;UI 부분&gt;
const ChatRoomList = () =&gt; {
  return (
// 생략... //

// 그래서 아래와 같이 &lt;p&gt; 태그 안에서 마지막 메세지를 반환 해줄 수 있게 되었다.
&lt;St.StListLower&gt;
  &lt;p&gt;{findMatchMessage(room.id).content}&lt;/p&gt;
&lt;span&gt;{parseDate(findMatchMessage(room.id).created_at)}&lt;/span&gt;
&lt;/St.StListLower&gt;
 );
}</code></pre>
<h3 id="1-1에서-남은-찝찝함">1-1에서 남은 찝찝함</h3>
<ul>
<li><p>지금 DB의 메세지는 많아봐야 테스트 메세지 10개쯤..
메세지의 용량도 몇 KB 안 하겠지만,
혹시 나중에 메세지 데이터가 쌓여서 <strong>10,000개가 되도</strong>
과연 DB의 메세지를 가져와서 state에 저장 후 작동까지</p>
</li>
<li><p><em>지금과 같은 속도가 날까..?*</em> 싶은 걱정이 남았고</p>
</li>
<li><p>DB에서 가져올 때 애초에 유저가 속한 채팅방에 대한 메세지만 쇽쇽-
가져올 방법을 생각했지만 각 채팅방의 ID를 모두 비교하여 가져오는
방법을 찾지 못 했다.
supabase 클라이언트에서 제공하는 <code>eq</code> 라는 것과 배열 메서드 <code>forEach</code>를 사용해서
채팅방의 아이디가 같은 것만 쇽쇽 가져오려는 시도도 해봤지만 실패했다.</p>
</li>
</ul>
<h3 id="어쨌든-채팅방-결과물">어쨌든.. 채팅방 결과물</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/d466cd20-bb68-4a13-a194-55f2056f275a/image.png" alt=""></p>
<h2 id="2-찜하기-기능">2. 찜하기 기능</h2>
<h3 id="접근-방법-1">접근 방법</h3>
<ul>
<li><p>찜 버튼을 누르면
DB에서 해당 게시물의 id를 가진 row를 찾아서!
컬럼 중 liked_user라는 배열에 좋아요를 누른 유저의 아이디를 업데이트 해주고!
컬럼 중 likes라는 int4 형태의 값을 1 증가 시켜준다!</p>
</li>
<li><p>유저 테이블의 row에 가서도
like_post 배열에 게시물의 정보를 업데이트 해준다!</p>
</li>
</ul>
<h2 id="문제">문제</h2>
<p><strong>내 머리는 뭘 해야하는지 정확하게 알고 있었다!</strong>
supabase 클라이언트에 update는 이상하다는걸 알기 전까지는..</p>
<p>내가 생각한 update는
예를 들어 {바꾸려는 칼럼: 업데이트 값} 이게 맞긴한데...
난 supabase가 바꾸려는 칼럼에 없는 값을 인지하고
추가를 해주는 그런 최첨단 차세대 DB로 알고 있던 거..</p>
<h2 id="해결">해결</h2>
<ul>
<li><p>이게 맞나? 싶긴하지만
DB에서 기존 값을 가져와 새 값을 추가하여 update 해주는 것으로 진행하였다.</p>
<pre><code class="language-ts">const handleLike = async () =&gt; {
  // 해당 게시물의 현재 좋아요 수 가져오기
  const { data: likesField, error: likes } = await supabase
    .from(&#39;products&#39;)
    .select(&#39;likes&#39;)
    .eq(&#39;id&#39;, id);
  // 해당 게시물의 좋아요 누른 사람들 현재 값 가져오기
  const { data: existingData, error: user_no_exists } = await supabase
    .from(&#39;products&#39;)
    .select(&#39;like_user&#39;)
    .eq(&#39;id&#39;, id);

  //==============================
  // 이거슨 product 테이블 관련 처리
  //==============================

  if ( // 가져온 값이 존재하고, length &gt; 0 이라는 조건 //) {

    // int 4 타입의 좋아요 값 증가는 생략 //

    // 배열인지, 배열이면 길이가 0이상인지 확인
    const like_userList =
      // 가져 온 값이 null이 아니고 배열이고 length가 0 보다 큰가? 라는 조건에
      existingData &amp;&amp; Array.isArray(existingData) &amp;&amp; existingData.length &gt; 0
          // 참이면 existingData[0].like_user 가 배열이 아니면 빈 배열로 초기화
        ? Array.from(existingData[0].like_user || [])
        // 거짓이면 그냥 빈 배열로 초기화
        : [];

    // 새로 추가할 데이터
    const newLikeUser = {
      userNickname: curUser?.nickname,
      user_uid: curUser?.uid
    };

    // 기존 값과 추가할 객체 데이터를 합체
    const updatedLikeUserList = [...like_userList, newLikeUser];

    // 좋아요한 사용자 업데이트
    const { status: likeUser, error: likeUserFail } = await supabase
      .from(&#39;products&#39;)
      .update({
        like_user: updatedLikeUserList // 합쳐진 값으로 업데이트!
      })
      .eq(&#39;id&#39;, id);

  /// 에러 처리 생략///

    // 찜한 게시물인지 아닌지 판단하여 state를 변경하는 함수
    // 이게 작동하면 state에 따라 찜 버튼이 달라집니다.
  isLikedProduct();
};</code></pre>
</li>
</ul>
<h2 id="결과물">결과물</h2>
<ul>
<li>새로고침 해도 잘 유지가 된다...⭐
<img src="https://velog.velcdn.com/images/waffle_bear/post/ce7b19a9-3541-4de5-a5c9-28d85cdcc0b2/image.gif" alt=""></li>
</ul>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>DB를 설계할 땐 기능에 맞게!
DB테이블을 아주아주 잘 구상하자..!!😫😫
기능을 구현하려다보니 빠진 컬럼이나 추가되는 컬럼이 생기고
설계를 잘 해놓으면 위기의 순간에 방법이 보일 수도..!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트 - 지도, 주소 복사]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A7%80%EB%8F%84-%EC%A3%BC%EC%86%8C-%EB%B3%B5%EC%82%AC</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A7%80%EB%8F%84-%EC%A3%BC%EC%86%8C-%EB%B3%B5%EC%82%AC</guid>
            <pubDate>Thu, 18 Jan 2024 08:07:55 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>중고 상세페이지의 거래 장소의 주소를 가져와
지도를 띄워주고 주소 복사 + 길 찾기로 연결</li>
</ul>
<br/>

<h2 id="주소의-위치를-지도에-띄워주기">주소의 위치를 지도에 띄워주기</h2>
<blockquote>
<h3 id="카카오-api-사용-설정">카카오 API 사용 설정</h3>
<p>이전에 작성한 글에 기본적으로 <strong>카카오 API 설정을 어떻게 하는지</strong>
<strong>API KEY를 어떻게 발급 받는지</strong>는 아래의 글에 포스팅 해두었습니다.
<a href="https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EC%97%B0%EA%B2%B0">카카오 API 연결</a></p>
</blockquote>
<h3 id="1-indexhtml에-cdn-기입">1. index.html에 CDN 기입</h3>
<ul>
<li>카카오 디벨로퍼 사이트에서 기본적으로 KEY 발급과 플랫폼 설정 뒤
아래와 같이 public/index.html 에 <code>&lt;script&gt;</code> 태그 안에 CDN을 기입하고
중간에 <code>appkey=</code> 이라는 쿼리 안에 발급 받은 KEY 중 Javascript KEY를 기입한다.
연결은 이것으로 끝!
<img src="https://velog.velcdn.com/images/waffle_bear/post/18963101-86f0-4d36-a341-dbd22df68b99/image.png" alt=""></li>
</ul>
<br/>


<h3 id="2-kakao-maps-sdk-설치">2. kakao maps SDK 설치</h3>
<h4 id="yarn">yarn</h4>
<blockquote>
<p>yarn add react-kakao-maps-sdk    </p>
</blockquote>
<h4 id="npm">npm</h4>
<blockquote>
<p>npm install react-kakao-maps-sdk </p>
</blockquote>
<h3 id="3-geocoder를-사용해서-좌표-받아오기">3. Geocoder를 사용해서 좌표 받아오기</h3>
<pre><code class="language-ts">&lt;Maps.tsx&gt;

// 좌표 값을 담담하는 state
const [coord, setCoord] = useState&lt;Coord&gt;({ lat: 0, lng: 0 });
// 주소를 기반으로 좌표를 주는 API
const geocoder = new kakao.maps.services.Geocoder();

const readUserLocation = () =&gt; {
  geocoder.addressSearch(
    searchAddress,
    (result: CoderResult[], status: kakao.maps.services.Status) =&gt; {
      // result가 빈 배열이거나나 status가 ZERO_RESULT로 나올 때가 있어
      // 조건을 걸어두었다.
      if (result.length &gt; 0 &amp;&amp; status === &#39;OK&#39;) {
        // x = 경도, y = 위도를 state에 업데이트
        const { x, y } = result[0];
        setCoord({
          lat: Number(y),
          lng: Number(x)
        });
      }
    }
  );
};

// mount 시 받아 온 주소의 위도, 경도를 업데이트 
useEffect(() =&gt; {
  readUserLocation();
}, []);</code></pre>
<h3 id="4-지도-ui-그려주기">4. 지도 UI 그려주기</h3>
<ul>
<li>SDK 설치시 MAP, MapMarker 등 제공해주는 컴포넌트가 있는데
그것을 사용하여 지도를 그려주는 것</li>
<li>가장 기본적으로 <code>&lt;Map&gt;</code> 컴포넌트는 필수 props로 center가 들어가는데
받아 온 좌표를 center props에 넘겨주면 그 주소를 지도의 가운데에 맞춰 띄운다.<pre><code class="language-ts">&lt;&gt;
&lt;StModalContainer&gt;
 &lt;Map center={coord} style={{ width: &#39;100%&#39;, height: &#39;100%&#39; }} draggable&gt;
   &lt;StInfoBox position={coord}&gt;
     &lt;StOverayBox&gt;{searchAddress}&lt;/StOverayBox&gt;
   &lt;/StInfoBox&gt;
 &lt;/Map&gt;
 &lt;StButtonBox&gt;
  &lt;a target=&quot;_blank&quot;
     href={`https://map.kakao.com/link/to/집,${coord.lat},${coord.lng}`} rel=&quot;noreferrer&quot;&gt;
    &lt;Buttons&gt;길 찾기&lt;/Buttons&gt;
  &lt;/a&gt;
   &lt;Buttons onClick={copyAddress} id={searchAddress}&gt;
     주소 복사
   &lt;/Buttons&gt;
 &lt;/StButtonBox&gt;
&lt;/StModalContainer&gt;
&lt;/&gt;</code></pre>
<br/>

</li>
</ul>
<h2 id="채팅방-스크롤을-최하단에-두기">채팅방 스크롤을 최하단에 두기</h2>
<ul>
<li>채팅방이 mount 시 스크롤을 최하단에 위치하도록 하여
가장 최근의 메세지가 보이도록 하는 것</li>
</ul>
<h3 id="1-useref를-사용하여-채팅방-div와-연결">1. useRef를 사용하여 채팅방 div와 연결</h3>
<pre><code class="language-ts">// useRef
const scrollRef = useRef&lt;HTMLDivElement&gt;(null);

// 메세지 state가 변경 될 때마다 ref의 current값이 있다면
// ref로 설정 된 요소의 scroll 바 위치를 해당 요소의 총 높이 값으로 설정
useEffect(() =&gt; {
  if (scrollRef.current) {
    const scrollContainer = scrollRef.current;
    scrollContainer.scrollTop = scrollContainer.scrollHeight;
  }
}, [messages]);


// UI 부분
// 채팅 메세지 div를 ref에 연결해준다.
&lt;St.StChatGround ref={scrollRef}&gt;
  &lt;ChatMessages messages={messages} curUser={curUser} /&gt;
&lt;/St.StChatGround&gt;</code></pre>
<br/>

<h2 id="주소-복사-기능">주소 복사 기능</h2>
<ul>
<li><p>지도 모달 내 주소 복사 버튼의 기능</p>
</li>
<li><p>대표적인 라이브러리를 사용하려고 했으나, 마지막 업데이트 기간도 오래 전이고,
웹에서 제공해주는 좋은 API가 있어 사용했다.</p>
</li>
<li><p>이 Navigator API는 시스템 클립보드에 비동기적으로 접근하여 실행할 수 있기 때문에
텍스트가 잘 복사되면 간단하게 alert으로 알려주었다.</p>
<pre><code class="language-ts">const copyAddress = async (e: MouseEvent&lt;HTMLButtonElement&gt;) =&gt; {
const address = e.currentTarget.id; // 주소 텍스트

try {
  await window.navigator.clipboard.writeText(address);
  alert(&#39;주소 복사 완료!&#39;);
} catch (error) {
  console.error(&#39;주소 복사 실패:&#39;, error);
}
};</code></pre>
</li>
</ul>
<h3 id="느낀점">느낀점</h3>
<ul>
<li>분명 이전 팀 프로젝트 때 카카오 맵 API를 다뤘을 때는
코드 흐름도 잘 안 보이고 어려웠는데.. 오늘은 공식문서에서 보여주는 흐름도 잘 보였고
그래서 내가 뭘 해야 하는지도 명확히 알았다.
이전보다는 조금 더 성장했나보다 싶어서 기쁘다⭐</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 - 실시간 채팅 완료..]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%99%84%EB%A3%8C</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%99%84%EB%A3%8C</guid>
            <pubDate>Wed, 17 Jan 2024 12:00:53 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>길고 길었던 실시간 채팅 디버깅 + 새 기능 추가</li>
</ul>
<h2 id="1-디버깅">1. 디버깅</h2>
<h3 id="1-1-유저가-채팅방-생성-시-기존-데이터가-사라지고-업데이트-되는-것">1-1. 유저가 채팅방 생성 시 기존 데이터가 사라지고 업데이트 되는 것.</h3>
<ul>
<li><strong>원인</strong>
휴먼 에러</li>
<li><strong>해결</strong>
supabase에서 제공하는 API를 사용하면 자동으로 없는 값만 추가가 되는 update가 되는 줄 알았다.. 정신을 차리고 코드를 수정했다.</li>
<li><strong>디버깅 중 또 다른 오류</strong>
supabase의 chat_rooms 컬럼이 <code>null 가능</code> 으로 되어있어서
타입 오류가 오랜 시간동안 났다...부끄러웠다</li>
</ul>
<pre><code class="language-ts">const room_id = room[0]?.id;
  // 이 부분에서 [room_id] 이렇게만 써버렸다.
  const updatedData = [...userInfo.chat_rooms, room_id];
  const { data, error } = await supabase
  .from(&#39;user&#39;)
  .update({ chat_rooms: updatedData })
  .eq(&#39;uid&#39;, userInfo[0].uid)
  .select();</code></pre>
<h3 id="1-2-채팅방-중복-생성-방지">1-2. 채팅방 중복 생성 방지</h3>
<ul>
<li>채팅방은 게시물의 id가 심어진 버튼을 누르면 생성이 되는데
그 id는 채팅방의 about이라는 필드 값에 등록된다.
그렇기 때문에 DB 채팅방 테이블의 <strong>about 컬럼을 유니크 하게 설정</strong>하여
채팅방 생성 시 <strong>같은 게시물에 대한 채팅방 중복 생성을 방지</strong>하였다.</li>
<li>UI에서도 표현하고 싶었기 때문에
해당 게시물에 대한 채팅방의 존재 여부에 따라
버튼의 내용을 아래와 같이 바뀌도록 설정했다.<blockquote>
<p>채팅방 존재 O =&gt; &#39;이어서 채팅하기&#39;
채팅방 존재 X =&gt; &#39;채팅 후 구매하기&#39;</p>
</blockquote>
</li>
</ul>
<pre><code class="language-ts">const [isExist, setIsExist] = useState&lt;boolean&gt;(false);

// 채팅방 테이블을 조회하여 채팅방의 중복을 판단하는 함수
const isExistsRoom = async () =&gt; {
  const { data: chat_rooms, error } = await supabase
  .from(&#39;chat_room&#39;)
  .select(&#39;about&#39;)
  .eq(&#39;about&#39;, id);

  if (chat_rooms &amp;&amp; chat_rooms.length &gt; 0) {
    setIsExist(true);
    console.log(&#39;exists!&#39;);
  }
};

useEffect(() =&gt; {
  isExistsRoom();
}, []);</code></pre>
<h3 id="1-3-멈춰버린-실시간-기능-복구">1-3. 멈춰버린 실시간 기능 복구</h3>
<ul>
<li><strong>원인</strong>
정리되지 않은 코드 (휴먼 에러)</li>
<li><strong>해결</strong>
난장인 함수들을 하나하나 살펴보며
설계의 중요성을 깨달았고, 적합한 곳에서 실시간 기능이 동작하도록 수정했다.
분명 함수가 톱니바퀴처럼 착착 같이 돌아가게 설계했던 거 같은데
그냥 바퀴였다.</li>
</ul>
<h3 id="1-4-채팅방-하나-누를-때마다-같은-데이터-계속-가져오는-버그">1-4. 채팅방 하나 누를 때마다 같은 데이터 계속 가져오는 버그</h3>
<p>원래 정상적인 작동하면 채팅방을 한번 누르면 그 채팅방의 메세지를 가져오고 끝나야 되는데
그 채팅방을 누를 때마다 똑같은 데이터를 가져오는 버그였다.</p>
<ul>
<li><p><strong>원인</strong>
처음에는 실시간으로 오는 데이터를 기존
채팅방 state에 업데이트 해주는 방식으로 했는데</p>
</li>
<li><p><strong>해결</strong>
실시간 업데이트가 발생하면 채팅방과 연결 된 메세지들을
통으로 다시 state에 업데이트 해주는 것으로 해결되었다.</p>
</li>
</ul>
<h3 id="1-5-storage-이미지-업로드-실패">1-5. storage 이미지 업로드 실패</h3>
<ul>
<li><p><strong>원인</strong>
supabase storage는 한글 이름의 파일을 유효하지 않은 이름의 파일로 인식하기 때문에
아래와 같이 <code>Invalid Input 에러</code>가 뜬다
<img src="https://velog.velcdn.com/images/waffle_bear/post/c515a5e5-590d-4592-b577-7dfd5daa00e4/image.png" alt=""></p>
</li>
<li><p><strong>해결</strong>
업로드 전 file의 이름을 영문으로 바꿔서 업로드 했다.</p>
<pre><code class="language-ts">const handleImageUpload = async (file: File) =&gt; {
const { data, error } = await supabase.storage
.from(&#39;images&#39;)
// 받은 file을 &#39;messages폴더에 고유id 값(숫자 + 영문)&#39;으로 저장 
.upload(`messages/${uuid()}`, file, {
  contentType: file.type
});

if (error) {
  console.error(&#39;파일 업로드 실패:&#39;, error);
  return;
}
</code></pre>
</li>
</ul>
<p>//... 생략//</p>
<p>};</p>
<p>```</p>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/cdb91314-d03c-4f5b-80a6-d2ef74251afc/image.gif" alt=""></p>
<h2 id="해야할-것">해야할 것</h2>
<ul>
<li>상세페이지 찜하기 기능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 - 2차 병합]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%B0%A8-%EB%B3%91%ED%95%A9</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%B0%A8-%EB%B3%91%ED%95%A9</guid>
            <pubDate>Tue, 16 Jan 2024 12:06:06 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>담당 기능 리팩터링</li>
<li>2차 병합</li>
</ul>
<h2 id="리팩터링">리팩터링</h2>
<h3 id="1-타입-지정">1. 타입 지정</h3>
<ul>
<li>타입이 정해지지 않아 any로 지정했던 부분들에</li>
</ul>
<p>타입을 명확하게 해주었다.</p>
<p>처음에는 타입 명시가 귀찮은 느낌이 없지 않았지만,
막상 하나씩 타입을 지정하는 과정에서 약간 재미를 느끼게 됐다.</p>
<p>명확한 타입으로 리팩터링 해보니
구조적으로 뭔가 단단해진 느낌이 든다.</p>
<h3 id="2-컴포넌트-분리">2. 컴포넌트 분리</h3>
<ul>
<li>리액트의 컴포넌트를 볼 때
스크롤을 엄청 많이 해야한다면,
무언가 잘못된 것이라는 말을 어디선가 본적이 있다.
그 말을 되새기며 작성한 코드를 보는데 내 코드는 뭔가 잘못됐다.
스크롤양이 어마어마했다.</li>
</ul>
<h3 id="2-1-일차적으로-스타일-컴포넌트를-분리">2-1. <strong>일차적으로 스타일 컴포넌트를 분리</strong></h3>
<p>styled-components로 작업했기 때문에
각 컴포넌트와 동일한 위치에 style.ts를 만들어
스타일을 모두 옮겼다.</p>
<h3 id="2-2-이차적으로-컴포넌트-분리">2-2. <strong>이차적으로 컴포넌트 분리</strong></h3>
<p>map으로 그려지던 UI를 모두 분리했다.
props로 내려주어도 딱 한 번까지만 내려주는 직계 자식(?)까지만
컴포넌트를 분리했고</p>
<h3 id="2-3-함수-정리">2-3. <strong>함수 정리</strong></h3>
<p>제일 어려웠던 부분..
코드를 작성하는 과정에서 단일의 기능만 하도록 하고
함수가 순차적으로 톱니바퀴처럼 작동하도록 생각하고 짰는데..?
막상 정리하며 보니까 중복 코드가 엄청 많았다.
오늘은 내가 아는 개념 내에서만 정리를 했는데
supabase와 통신할 때의 코드 같은 것은
중복적으로 사용이 되는 것을 봐서는 어떻게 분리를 해서
공통으로 사용하는 함수로 만들 수 있을 거 같다는 느낌이 든다.
리팩터링 개념에 대해 공부를 좀 해야겠다.</p>
<h2 id="문제">문제</h2>
<ul>
<li><p>실시간 채팅의 실시간 메세지 기능이 작동하지 않는다.
이건 마치 홍철 없는 홍철팀...
오류 메세지도 안 뜨고, 그냥 지금 어디선가
재렌더가 일어나야 되는데 일어나지 않고 있는 거 같다🥺
메세지 전송을 해도 DB 테이블 업데이트에 대한 payload가 오지 않는다.
<img src="blob:https://velog.io/3a948663-2264-4527-ae2e-80b43c125f6a" alt="업로드중.."></p>
</li>
<li><p>내일은 리팩터링 개념 학습 후 적용, 실시간 문제 해결, 디자인 적용을 해야겠다. </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 - 기능 연결]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%8A%A5-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Mon, 15 Jan 2024 13:11:41 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>로그인 - 게시물 상세 - 채팅 연결</li>
</ul>
<h2 id="1-supabase-카톡-소셜-로그인-연결하기">1. supabase 카톡 소셜 로그인 연결하기</h2>
<h4 id="1-카카오-디벨로퍼-사이트에-가입을-한다">1. <a href="https://developers.kakao.com/">카카오 디벨로퍼 사이트</a>에 가입을 한다.</h4>
<h4 id="2-내-어플리케이션을-등록한다">2. 내 어플리케이션을 등록한다.</h4>
<ul>
<li><p>아래 사진의 애플리케이션 추가하기를 누른 후
<img src="https://velog.velcdn.com/images/waffle_bear/post/cec5612b-a907-4dbe-9239-f98fd6eb1b2f/image.png" alt=""></p>
</li>
<li><p>입력란을 채워준다. (나는 앱 이름과 사업자명을 같게 했다.)
<img src="https://velog.velcdn.com/images/waffle_bear/post/05135b15-82e8-43eb-a2f3-26058563b6ec/image.png" alt=""></p>
</li>
</ul>
<h4 id="3-카카오-로그인-활성화">3. 카카오 로그인 활성화</h4>
<ul>
<li>내 애플리케이션 - 제품설정 - 카카오 로그인 탭에 들어가
활성화 설정을 ON으로 설정한다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/dbec90f2-6270-4c37-a07a-73cdbf6aca6a/image.png" alt=""></li>
</ul>
<h4 id="4-동의-항목-설정">4. 동의 항목 설정</h4>
<ul>
<li>다음은 제품설정의 동의 항목으로 가서</li>
<li>아래 항목들에 설정을 하자
<img src="https://velog.velcdn.com/images/waffle_bear/post/da774019-88fb-42d9-af4c-f2dc3b92a65a/image.png" alt=""></li>
</ul>
<h4 id="4-1-동의항목-설정-이메일-권한-얻기">4-1. 동의항목 설정 이메일 권한 얻기</h4>
<ul>
<li>프로젝트 사이트에서는 이메일을 필수로 받아야 하는데
처음 동의항목 설정에 들어가면 카카오 계정(이메일)이 권한 없음으로 되어있는데
이것의 권한을 얻기 위해서는 <strong>비즈 앱 신청</strong>을 해야한다.</li>
<li>위의 <strong>개인정보 동의항목 심사 신청</strong> 버튼을 누른 뒤
비즈 앱으로 전환하도록 하자!</li>
</ul>
<h4 id="5-플랫폼-설정하기">5. 플랫폼 설정하기</h4>
<ul>
<li>거의 다 왔다. 플랫폼 설정에서
아직 배포하지 않았다면 <code>npm start</code> 시 뜨는 주소! (<a href="http://localhost:3000">http://localhost:3000</a>)
배포했다면 배포한 사이트의 주소를 적자!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/a1c47b33-9a72-4a0a-aa14-bf646a6cab8c/image.png" alt=""></p>
<h4 id="6-앱-키-입력하기">6. 앱 키 입력하기</h4>
<ul>
<li>아래의 키를 보면 다양하게 있는데</li>
<li>supabase의 카카오톡 Provider에는 REST API 키, Javascript 키를 사용한다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/d6cfa98b-4d84-43df-92b8-e350ba5f9a56/image.png" alt=""></li>
</ul>
<h4 id="7-supabase-provider-설정">7. supabase Provider 설정</h4>
<ul>
<li>아래와 같이 내 프로젝트 auth탭의 Providers에 들어가서
카카오톡을 Enabled로 해놓고
REST API Key에는 6번의 REST API 키를
Cilent Secret Code 에는 6번의 Javascript 키(웹 기준)를 입력하면 끝..!
<img src="https://velog.velcdn.com/images/waffle_bear/post/dac78d57-c4d4-4f38-9b53-023c1a753569/image.png" alt=""></li>
</ul>
<h2 id="2-상세-페이지와-채팅-연결하기">2. 상세 페이지와 채팅 연결하기</h2>
<h3 id="큰-흐름">큰 흐름</h3>
<ol>
<li><p>상세페이지에 접속하여 채팅하기 버튼 클릭 시
채팅방 테이블에 현재 로그인 유저와 게시물 작성 유저의 uid가 담긴
row를 하나 insert 한다.</p>
</li>
<li><p>채팅방 테이블에 채팅방이 생성되면
첫 메세지로 내가 클릭한 상품의 { 게시물의 제목, 가격, 관심 있어요! }
라는 세 개의 메세지를 상대방에게 전송한다.</p>
</li>
</ol>
<h2 id="코드">코드</h2>
<h3 id="1-상세-페이지에서-채팅방-생성">1. 상세 페이지에서 채팅방 생성</h3>
<ul>
<li><p>채팅하기 버튼에는 게시물 작성자의 uid가 심어져 있다.</p>
</li>
<li><p>현재 로그인 유저는 mount 시 state에 저장된다.</p>
</li>
<li><p>게시물 데이터는 mount 시 state에 저장된다</p>
<pre><code class="language-ts">// 채팅하기 버튼 클릭 시
const makeChatRoom = async (e: MouseEvent) =&gt; {
// 게시물 작성자의 uid
const targetId = e.currentTarget.id;

// user 테이블에서 채팅 상대 정보 가져오기
// user 테이블의 uid 컬럼 값 === 게시물 작성자의 uid인 것을 가져온다
const { data: targetUser, error: noUser } = await supabase
.from(&#39;user&#39;)
.select(&#39;*&#39;)
.eq(&#39;uid&#39;, targetId);

// 게시물 작성자의 정보가 존재하면
// state에 저장
if (targetUser &amp;&amp; targetUser.length &gt; 0) {
  setTarget(targetUser[0]);
}

// error 처리 (추후 UI적으로 표현 예정)
if (noUser) console.log(&#39;user is not exists&#39;, noUser);
};</code></pre>
</li>
</ul>
<h3 id="1-1-유저들의-정보를-채팅방-테이블에-담아-insert-하기">1-1. 유저들의 정보를 채팅방 테이블에 담아 insert 하기</h3>
<ul>
<li><p>미리 준비되어야 하는 정보들과 그 흐름을 설명하기 위해
최종 목표에 도달하기까지의 함수들을 설명하겠습니당.</p>
<pre><code class="language-ts">// 생성된 채팅방 row에 로그인 유저와 채팅 상대 정보 insert 하는 함수
const insertUserIntoChatRoom = async (
  curUser: CustomUser,
  target: CustomUser
) =&gt; {
  // participants는 채팅방 테이블의 row가 갖는 채팅 참여자 필드이다.
  const participants = [
    {
      participants: [
        { user_id: target.uid, user_name: target.username },
        {
          user_id: curUser.uid,
          user2_name: curUser.username
        }
      ]
    }
  ];

  // 채팅방 테이블의 participants 필드에 참여자 정보 insert
  const { data: chatRoom, error } = await supabase
    .from(&#39;chat_room&#39;)
    .insert(participants);

  // 유저가 속한 채팅방을 반환하는 함수
  // 이것은 유저가 속한 채팅방의 id를 얻기 위해 사용합니다
  await findRoom();

  if (error) console.log(&#39;생성 실패&#39;);
};</code></pre>
</li>
</ul>
<h3 id="1-2-각-유저에게-본인이-속한-채팅방-id-업데이트">1-2. 각 유저에게 본인이 속한 채팅방 id 업데이트</h3>
<pre><code class="language-ts">// 유저가 속한 채팅방을 반환해주는 함수
const findRoom = async () =&gt; {
  const { data: foundRoom, error } = await supabase
  .from(&#39;chat_room&#39;)
  .select(&#39;*&#39;);

  if (error) {
    console.error(&#39;일치하는 방 없음&#39;, error);
    return;
  }

  // 채팅 테이블의 row를 읽어와서 (curUser는 현재 로그인 된 유저 정보 state)
  if (foundRoom &amp;&amp; curUser) {
    // 유저가 속한 채팅방을 filter 후
    const filtered = foundRoom.filter((room: any) =&gt; {
      return room.participants.some(
        (participant: any) =&gt; participant.user_id === curUser.uid
      );
    });

    // 유저가 속한 채팅방 반환
    return filtered;
  }
};</code></pre>
<h3 id="1-3-유저-테이블에도-소속-된-채팅방-id-insert">1-3. 유저 테이블에도 소속 된 채팅방 id insert</h3>
<pre><code class="language-ts">// 유저 테이블의 chat_rooms 필드값에 방을 업뎃해주자
const insertRoomintoUser = async (userInfo: CustomUser[]) =&gt; {
  // 헬퍼함수로 유저가 속한 채팅 테이블 할당
  const room = (await findRoom()) as any;

  // 만약 유저가 속한 채팅방 데이터와 현재 로그인 한 유저 정보가 있다면
  if (room &amp;&amp; userInfo) {
    // 채팅방 id 추출
    const room_id = room[0].id;

    // 인자로 받은 user 정보의 uid와 같은 필드의 chat_rooms에
    // 채팅방 id 추가!
    const { data, error } = await supabase
    .from(&#39;user&#39;)
    .update({ chat_rooms: [room_id] })
    .eq(&#39;uid&#39;, userInfo[0].uid)
    .select();

    if (error) {
      console.error(&#39;채팅방 추가 실패&#39;, error.message);
      return false;
    }
  }
};</code></pre>
<h3 id="1-4-최종적으로-유저-각자에게도-채팅방-id-추가">1-4. 최종적으로 유저 각자에게도 채팅방 id 추가</h3>
<pre><code class="language-ts">// 이제 각 유저에게 채팅방을 추가하자
// 현재 로그인 유저, 채팅 상대 유저 정보를 인자로 보내어
// 유저의 chat_room_id에 채팅방 id를 update 해준다 (insertRoomIntoUser 함수)
const findUser = async (User: CustomUser) =&gt; {
  const { data: userInfo, error } = await supabase
  .from(&#39;user&#39;)
  .select(&#39;*&#39;)
  .eq(&#39;uid&#39;, User?.uid);

      if (userInfo) {
    await insertRoomIntoUser(userInfo as any);
  }
};</code></pre>
<br/>

<h3 id="2-채팅방-생성--유저와-채팅방-연결-후-시작-메세지-전송">2. 채팅방 생성 / 유저와 채팅방 연결 후 시작 메세지 전송</h3>
<pre><code class="language-ts">// 게시물 관련 데이터를 첫 메세지로 보낸다.
const sendFirstMessage = async () =&gt; {
  if (product &amp;&amp; curUser) {
    // 유저가 속한 채팅방을 반환하여 room에 할당
    const room = await findRoom();

    if (room) {
      // 채팅방 생성 시 자동으로 전송되는 메세지
      const InitMessage = [
        {
          sender_id: curUser.uid,
          content: `제목: ${product[0].title}`,
          chat_room_id: room[0]?.id
        },
          {
          sender_id: curUser.uid,
          content: `${product[0].price}원`,
          chat_room_id: room[0]?.id
        },
          {
        sender_id: curUser.uid,
        content: &#39;이 상품에 관심 있어요!&#39;,
        chat_room_id: room[0]?.id
        }
      ];

    // 메세지 테이블에 시작 메세지 insert
    const { data, error } = await supabase
    .from(&#39;chat_messages&#39;)
    .insert(InitMessage);

    if (error) console.log(&#39;메세지 전송 실패..&#39;, error);
    }
  } else console.log(&#39;데이터 없음&#39;);
};</code></pre>
<h2 id="결과물">결과물</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/65b84a95-0377-45f9-b2ef-d835779ed635/image.gif" alt=""></p>
<h3 id="느낀점">느낀점</h3>
<ul>
<li><p>DB 데이터를 다루는 것에 있어서 뭔가
엄청나게 많은 함수를 선언하고 사용하는데 이게 맞나..? 싶다🥺
좀 더 효율적으로 바꿀 수 있겠다는 생각이 든다.</p>
</li>
<li><p>typescript에서 any를 많이 사용했는데
type은 어떻게 지정해야 되는 것인지 모를 때가 많다..⭐
리팩토링 단계에서는 명시하는 것으로....!!!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트-실시간 채팅 이미지 전송]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%84%EC%86%A1</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%84%EC%86%A1</guid>
            <pubDate>Sun, 14 Jan 2024 14:05:52 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>실시간 채팅 이미지 전송 기능</li>
</ul>
<h2 id="접근-방식">접근 방식</h2>
<ul>
<li>Supabase의 Storage와 연계하여 작업하는 거라서
처음에는 감이 오지 않았지만,
데이터를 다루는 흐름을 파악한 후 그 흐름을 코드로 풀어냈다.
흐름은 아래와 같다.</li>
</ul>
<ol>
<li>Storage에 파일 업로드</li>
<li>업로드 한 이미지의 Public_URL 반환</li>
<li>반환 된 이미지 URL을 메세지 전송 시
전송하는 메세지의 필드 값에 저장하여 insert</li>
<li>메세지 데이터를 가져왔을 때 image_url의 필드값 유무에 따라 렌더</li>
</ol>
<h2 id="코드">코드</h2>
<ol>
<li><p>type이 file인 input을 만들어 Storage에 파일 업로드</p>
<pre><code class="language-ts">const handleImage = async (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
 const file = e.target.files;
 if (file) {
   // handleImageUpload로 넘겨줍니다
   handleImageUpload(file[0]);
 } else {
   return;
 }
};</code></pre>
</li>
<li><p>이미지를 받아 Storage에 업로드 후 public_url을 state에 저장하기</p>
<pre><code class="language-ts">const handleImageUpload = async (file: File) =&gt; {
 const { data, error } = await supabase.storage
   .from(&#39;images&#39;)
   .upload(`messages/${file.name}`, file, {
     contentType: file.type
   });

 if (error) {
   console.error(&#39;파일 업로드 실패:&#39;, error);
   return;
 }
 // 에러가 아니라면 스토리지에서 방금 올린 이미지의 publicURL을 받아와서
 const res = supabase.storage.from(&#39;images&#39;).getPublicUrl(data.path);
 // image 경로를 저장하는 state에 set 해주고
 setImages(res.data.publicUrl);
};</code></pre>
</li>
</ol>
<ol start="3">
<li><p>메세지를 전송하면 해당 메세지 Row 필드값에 추가하여 insert 하기</p>
<pre><code class="language-ts">const sendMessage = async (e: FormEvent) =&gt; {
e.preventDefault();
if (curUser) {
 const { data, error } = await supabase.from(&#39;chat_messages&#39;).insert([
   // 메세지 테이블에 insert 하는 row의 형태
   {
     id: uuid(),
     sender_id: curUser?.id,
     chat_room_id: clicked,
     content: chatInput,
     // 이 필드값에 public url이 담긴 state를 value로 줌.
     image_url: images
   }
 ]);
 setChatInput(&#39;&#39;);
 setImages(&#39;&#39;);

 if (error) {
   console.log(&#39;전송 실패&#39;, error);
 }
} 
};</code></pre>
</li>
</ol>
<h2 id="결과물">결과물</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/16f99ba8-bfe0-4389-bd10-45c5232a6772/image.gif" alt=""></p>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>흐름을 파악하고 설계한 뒤
설계대로 코드를 작성할 수 있게 되어 기쁘다🥹</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트 - 읽지 않은 메세지 표시]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%BD%EC%A7%80-%EC%95%8A%EC%9D%80-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%91%9C%EC%8B%9C</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%BD%EC%A7%80-%EC%95%8A%EC%9D%80-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%91%9C%EC%8B%9C</guid>
            <pubDate>Thu, 11 Jan 2024 12:05:31 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>채팅방 목록에서 읽지 않은 메세지 표시하기..</li>
</ul>
<h2 id="접근방식">접근방식</h2>
<ul>
<li>채팅방 목록을 생성할 때
채팅방과 연결된 메세지 테이블에 isRead가 false인 데이터를 가져와
그 length를 취합하여 각각의 채팅방에 표기..?
채팅방을 누르면 각각의 채팅방에 isRead를 true로 전부 update</li>
</ul>
<h2 id="실행">실행</h2>
<h3 id="1-채팅방과-메세지를-가져올-때">1. 채팅방과 메세지를 가져올 때</h3>
<ul>
<li><p>채팅방 id를 인자로 보내면
해당 id와 연결된 메세지들을 가져오는데 isNew가 false인 것들을 가져온다.</p>
<pre><code class="language-ts">const unreadCount = async (room_id: string) =&gt; {
  let { data: chat_messages, error } = await supabase
    .from(&#39;chat_messages&#39;)
    .select()
    .eq(&#39;chat_room_id&#39;, room_id)
    .eq(&#39;isNew&#39;, false);

    return chat_messages?.length
};</code></pre>
</li>
</ul>
<h3 id="문제">문제</h3>
<ul>
<li>이것을 useEffect와 어떻게 연결할 것인가...
그리고 저 함수를 JSX 내에서 사용하면 Promise(Pending) 상태로 온다..🥲</li>
</ul>
<h3 id="해결">해결</h3>
<ul>
<li><p>결국 state를 하나 만들어 채팅방 리스트 UI를 map으로 그릴 때
index에 따라서 착착 들어가도록 했다.</p>
</li>
<li><p>mount 시 채팅방 목록 state가 생성되면,
그 채팅방 데이터들을 순회하며 읽지 않은 메세지를 unread state에 저장해둔다!</p>
</li>
</ul>
<pre><code class="language-ts">useEffect(() =&gt; {
  // 각 채팅방 목록이 업데이트될 때마다 안 읽은 메세지 수를 가져오고 상태에 저장
  if (rooms) {
    Promise.all(rooms.map((room) =&gt; unreadCount(room.id))).then((counts) =&gt; {
      setUnread(counts);
    });
  }
  }, [rooms]);</code></pre>
<pre><code class="language-ts">// 채팅방 목록 UI
&lt;ChatList&gt;
// 채팅방 state를 map으로 순회하며 UI를 그린다.
{rooms?.map((room, i) =&gt; {
  // ... 생략 /// 
  return (
   // 채팅방 UI
   &lt;ListRoom&gt;
     // 채팅방 목록을 따라 각 방마다 안 읽은 메세지를 정리해 놓은
     // unread[index]로 배치한다.
     // 그럼 각 방에 대한 읽지 않은 메세지 수가 차례대로 ![](https://velog.velcdn.com/images/waffle_bear/post/febaf566-2e43-40ed-9dfb-b8b68b4213f6/image.gif)
나온다
     &lt;p&gt; 새 메세지: &amp;nbsp; {unread &amp;&amp; unread[i]} &lt;/p&gt;
   &lt;/ListRoom&gt;
  );
})}
&lt;/ChatList&gt;</code></pre>
<h2 id="결과">결과</h2>
<h3 id="유저-2-----유저-3에게-테스트-메세지-전송">유저 2 ---&gt; 유저 3에게 테스트 메세지 전송</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/de6ce854-0f0a-4e04-b020-19730bf91b29/image.gif" alt=""></p>
<h3 id="메세지-받은-유저-3의-채팅방-모습">메세지 받은 유저 3의 채팅방 모습</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/128ad105-8c9b-44e2-b47c-c174b7652ad7/image.gif" alt=""></p>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>점점 useEffect 사용도 많아지고
늘 사용하던 개념에 대한 얕았던 지식의 바닥이 드러나고 채우고
또 드러나고 채우고의 반복이다🥲</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트_실시간 채팅, 알림 완료]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%95%8C%EB%A6%BC-%EC%99%84%EB%A3%8C</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%95%8C%EB%A6%BC-%EC%99%84%EB%A3%8C</guid>
            <pubDate>Wed, 10 Jan 2024 10:51:07 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>supabase 리얼타임을 사용하여 실시간 채팅, 알림 구현</li>
</ul>
<h2 id="어제의-문제-리뷰">어제의 문제 리뷰</h2>
<ul>
<li><p>어제 있었던 invalid input에 대해 알아봤지만
데이터를 직렬화 하여 insert 하지 않아도
다른 DB에는 잘 insert 되는 것을 확인하였고
결론은 테이블 셋팅 단계에서 문제가 있던 거 같다.</p>
</li>
<li><p>메세지 테이블과, 채팅방 테이블 설정을 다시 하는 것으로
문제를 해결했다!</p>
</li>
</ul>
<h2 id="오늘의-구현-과정">오늘의 구현 과정</h2>
<h3 id="1-테이블-설정">1. 테이블 설정</h3>
<ul>
<li>보편적인 실시간 채팅 설정에 따라
메세지 테이블, 채팅방 테이블을 설정했다.</li>
<li>이때 메세지 테이블은 채팅방의 room_id 컬럼을 foreign key로 설정하고
채팅방의 participants 필드에는 참여자 정보를 담아
유저가 채팅방을 read 하면 채팅방에 따라 메세지를 read 하도록 연결했다.</li>
</ul>
<h3 id="2-로그인회원가입">2. 로그인/회원가입</h3>
<ul>
<li>간단하게 Email Provider만을 사용하여 로그인, 회원가입 구현.</li>
<li>회원가입 시 user 테이블에 회원가입한 사용자의 정보를 저장.</li>
</ul>
<h3 id="3-채팅방-생성하기--채팅방-리스트-ui-그리기">3. 채팅방 생성하기 &amp; 채팅방 리스트 UI 그리기</h3>
<ul>
<li><p>2번 테스트 유저로 로그인 하여
1번 테스트 유저에게 채팅하기 버튼을 클릭 시
아래 함수가 작동하며 채팅방 목록이 생긴다.</p>
<pre><code class="language-typescript">const makeChatRoom = async (e: MouseEvent&lt;HTMLButtonElement&gt;) =&gt; {
  // 클릭하는 버튼에 있는 상대방 유저 id
  const id = e.currentTarget.id;

  try {
    // 현재 로그인 된 유저가 있고, 로그인 유저의 identities가 undefined가 아닐 때
    if (curUser &amp;&amp; curUser.identities !== undefined) {
      // chat_room 테이블의 participants 필드에 row를 insert한다
      // row에 들어가는 속성은 테스트를 위해 간소하게
      // 로그인 된 유저의 id와 관련 정보, 상대방 유저의 id와 관련 정보를 담았다..
      const { data, error } = await supabase.from(&quot;chat_room&quot;).insert([
        {
          participants: [
            { user_id: id, user_name: &quot;test1&quot; },
            {
              user_id: curUser.id,
              user2_name: &quot;test2&quot;,
            },
          ],
        },
      ]);
    }
  } catch (err) {
    console.log(&quot;failed&quot;, err);
  }
};</code></pre>
<h3 id="생성과-동시에-채팅방을-가져와-필터하여-ui를-그린다">생성과 동시에 채팅방을 가져와 필터하여 UI를 그린다.</h3>
<pre><code class="language-typescript">// 현재 로그인 된 유저의 채팅방을 가져오는 함수
const getRoomsforUser = async () =&gt; {
  // 일단 모든 채팅방을 가져온 후
  try {
    const { data: chat_room, error } = await supabase
      .from(&quot;chat_room&quot;)
      .select(&quot;*&quot;);

    if (error) {
      console.error(&quot;채팅방 가져오기 실패&quot;, error);
      return;
    }

    // chat_room과 현재 로그인 유저 정보가 있다면
    if (chat_room &amp;&amp; curUser) {
      // 가져온 채팅방 목록의 participants를 순회하며
      const filtered = chat_room.filter((room: any) =&gt; {
        // 현재 로그인 된 유저가 속한 채팅방만 filter한다.
        return room.participants.some(
          (participant: any) =&gt; participant.user_id === curUser.id
        );
      });
      setRooms(filtered);
    }
  } catch (error) {
    console.error(&quot;소속 된 채팅방이 없습니다&quot;, error);
  }
};</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/43fcc37e-ff80-4869-92aa-13e631f1ea49/image.gif" alt=""></p>
<h3 id="4-채팅하기">4. 채팅하기</h3>
<ul>
<li>메세지 전송 시 chat_room_id를 현재 열린 채팅방의 id로 하여
메세지 테이블에 insert 한다!<pre><code class="language-ts">const sendMessage = async (e: FormEvent) =&gt; {
  e.preventDefault();
  if (curUser) {
    const { data, error } = await supabase.from(&quot;chat_messages&quot;).insert([
      {
        sender_id: curUser?.id,
        // 채팅방을 클릭 시, 클릭 된 채팅방 id를 저장하는 state가 clicked이다.
        chat_room_id: clicked,
        content: chatInput,
      },
    ]);
  }
};</code></pre>
</li>
</ul>
<h3 id="5-실시간으로-업데이트-하기">5. 실시간으로 업데이트 하기</h3>
<ul>
<li>useEffect를 통해 열려있는 채팅방의 id와 같은 chat_room_id를 가진
메세지 테이블을 구독하여 실시간으로 업데이트 한다.</li>
</ul>
<h3 id="6-실시간-알림-확인">6. 실시간 알림 확인</h3>
<ul>
<li>채팅방을 끈 뒤 다른 페이지로 이동한 후
useEffect를 사용하여 구독해둔 채팅방에 대한 실시간 알림을 받는다
<img src="https://velog.velcdn.com/images/waffle_bear/post/8587a844-1dca-4063-9520-402cb6f36786/image.gif" alt=""></li>
</ul>
<h2 id="결과물">결과물</h2>
<h3 id="로그인">로그인</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/39ca6811-483d-465e-9b6d-843b6ce140e6/image.gif" alt=""></p>
<h3 id="채팅방-생성">채팅방 생성</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/481f4437-ee32-4497-97f7-a5d775dfa2f3/image.gif" alt=""></p>
<h3 id="채팅해보기">채팅해보기</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/ec6079f7-7459-484f-95e8-7342f3bdd5f8/image.gif" alt=""></p>
<h2 id="어려웠던-부분">어려웠던 부분</h2>
<h3 id="1-데이터간의-연결이-어려웠다">1. 데이터간의 연결이 어려웠다</h3>
<ul>
<li>어떤 데이터가 어떤 데이트를 참조하여 수월하게 데이터를 다룰지 고민을 많이 했다.</li>
</ul>
<h3 id="2-새로고침-시-뜨지-않는-채팅방">2. 새로고침 시 뜨지 않는 채팅방</h3>
<ul>
<li><p>채팅방 생성 후 새로고침을 하면 useEffect 속 채팅방 가져오는
함수가 작동하지 않았는데.
useEffcet를 하나만 사용해야 한다는 이상한 개념이 있었던 탓이다.
1번 useEffect를 통해 유저 정보를 가져오고
2번 useEffect에서 user정보를 의존성 배열에 두고
현재 로그인 된 사용자의 채팅방 목록을 가져오도록 하였다.</p>
<pre><code class="language-ts">useEffect(() =&gt; {
// mount 시 유저 정보를 가져오는 함수
const getUserData = async () =&gt; {
  const { data, error } = await supabase.auth.getUser();
  if (data) {
    setCurUser(data.user);
    } else {
      console.log(&quot;user data is empty&quot;);
    }
  }
};

  getUserData();
}, []);
</code></pre>
</li>
</ul>
<p>// 로그인 된 유저 정보가 변하고, 그 정보가 있을 경우 채팅방 fetch
useEffect(() =&gt; {
  if (curUser) {
    getRoomsforUser();
  }
}, [curUser]);</p>
<p>```</p>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>새로운 기술 supabase 강력하다.
뉴비인 나에게는 장점만 보인다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트 - supabase 실시간 챝]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-supabase-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%9D</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-supabase-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%9D</guid>
            <pubDate>Tue, 09 Jan 2024 12:38:14 GMT</pubDate>
            <description><![CDATA[<h3 id="오늘-했던-것">오늘 했던 것</h3>
<ul>
<li>기능에 필요한 테이블 설정</li>
<li>실시간 채팅 구현</li>
</ul>
<h2 id="테이블-설정">테이블 설정</h2>
<ul>
<li>구글에 검색을 해본 뒤 일반적으로 실시간 채팅에 사용하는
테이블 구성을 참고하여 작성했다.
유저 테이블, 메세지 테이블, 채팅방 테이블</li>
</ul>
<h3 id="메세지---채팅방---유저-테이블을-어떻게-연결하고-참조하지">메세지 - 채팅방 - 유저 테이블을 어떻게 연결하고 참조하지..?</h3>
<ul>
<li>답은 foreign key였다.
채팅방의 room_id를 foreign key로 참조하여 메세지를 가져와
이 메세지 데이터가 어느 채팅방에 소속 된 데이터인지 알게 되는 것.
유저도 어느 채팅방에 소속되었는지 foreign key로 연결할 수 있었다.</li>
</ul>
<h3 id="1차-테스트---혼자-쓰는-채팅-성공">1차 테스트 - 혼자 쓰는 채팅 (성공)</h3>
<ul>
<li>연결된 테이블을 실시간으로 구독하는 것을 이해하기 위해
로그인 된 유저가 혼자 쓰는 채팅방을 만들어봤다.</li>
<li>채팅 시작 버튼을 누르면 채팅방(input만 달랑 있는 방)으로 들어가
인풋에 메세지를 작성해 보내면 실시간으로 추가되는 데이터가 오고
state에 업데이트 해서 실시간으로 나오도록 하였다.</li>
</ul>
<h4 id="로직은-간단했다">로직은 간단했다</h4>
<pre><code class="language-js">
 // 컴포넌트 마운트 시 메세지 가져오기
 const getMsg = async () =&gt; {
    try {
      let { data: chat_messages, error } = await supabase
        .from(&quot;chat_messages&quot;)
        .select(&quot;*&quot;);
      // 메세지 가져온 것을 state에 업데이트 하기
      setTestOne(chat_messages);
    } catch (err) {
      console.log(err);
    }
  };

  // 채팅메세지 테이블을 구독하여 모든 이벤트에 대한 알림을 받도록 하는 함수
  useEffect(() =&gt; {
    const updatedMessages = supabase
      .channel(&quot;custom-all-channel&quot;)
      .on(
        &quot;postgres_changes&quot;,
        { event: &quot;*&quot;, schema: &quot;public&quot;, table: &quot;chat_messages&quot; },
        (payload) =&gt; {
          setTestOne((prev) =&gt; [...prev, payload.new]);
        }
      )
      .subscribe();

    // 마운트 시 메세지 가져오기
    getMsg();

    // 언마운트 시 구독 해제
    return () =&gt; {
      updatedMessages.unsubscribe();
    };
  }, []);
</code></pre>
<h3 id="해결하지-못한-문제">해결하지 못한 문제</h3>
<ol>
<li>테스트 유저 계정 2개를 만들었다.</li>
<li>회원가입 시 유저 테이블에 정해둔 scheme 형태의 유저 데이터가
user 테이블에 업데이트 된다.</li>
<li>한 유저로 로그인 하여 다른 유저에게 채팅하기 버튼을 클릭 시
클릭 된 유저의 uid와 클릭한 유저의 uid를 가진 채팅방 테이블이 생성된다.</li>
<li>채팅방으로 이동하면 현재 로그인 된 유저가 소속된 채팅방을 모두 가져온다.</li>
</ol>
<h3 id="현재-이-4번에-막혀있다">현재 이 4번에 막혀있다.</h3>
<pre><code class="language-js">const { data: chatRooms, error } = await supabase
        .from(&quot;chat_room&quot;)
        .select()
        .contains(&quot;participants&quot;, [{ user_id: userData.id }]);</code></pre>
<p>이렇게 participants 필드에서 user_id가 현재 로그인 유저 id와 같은 것을 
가져오도록 쿼리를 작성했는데
JSON 타입에 유효하지 않은 input 문법 오류(?)가 나온다..
아무래도 테이블 설정을 잘못한 것이거나 쿼리문의 오류겠지.. 하는 막연한 생각이 든다.</p>
<p>오늘 강의를 한번 더 듣고 놓친 부분을 확인해봐야겠다..
<img src="https://velog.velcdn.com/images/waffle_bear/post/dc3593b1-cc30-402f-a9a7-75ba114ca749/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 2 _ 기획 회의]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2-%EA%B8%B0%ED%9A%8D-%ED%9A%8C%EC%9D%98</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2-%EA%B8%B0%ED%9A%8D-%ED%9A%8C%EC%9D%98</guid>
            <pubDate>Mon, 08 Jan 2024 13:26:46 GMT</pubDate>
            <description><![CDATA[<h3 id="오늘-했던-것">오늘 했던 것</h3>
<ul>
<li>첫날 진행했던 기획 회의에 따른
와이어 프레임이 나온 후 디자인 수정</li>
<li>UX를 개선하기 위한 UI 수정</li>
<li>차별성을 위한 회의 및 기능 추가</li>
<li>사용자 유입을 위한 방안 협의</li>
<li>기능 구현에 대한 기술 이슈</li>
</ul>
<h3 id="디자인-관련-회의">디자인 관련 회의</h3>
<ul>
<li><p>1/4
디자이너분과 첫 회의를 가진 후
3일 뒤인 일요일에 첫 와이어 프레임이 나왔다.
분명 MVP 개발을 위해 핵심 기능을 위한 페이지만 디자인 해서
빠르게 개발에 돌입할 수 있을 거 같았는데</p>
</li>
<li><p>1/8 오늘.
와이어 프레임을 확인하며 뭐에 홀린듯 나를 포함한 팀원들이
다양한 관점에서 생각을 하게 되면서 디자인의 작은 부분들이 수정에 들어가게 되었다.
사유는 UX 관련한 것이 많았고, 아직 컬러도 안 정해져서
개발팀이나 디자이너분이나 초조한 느낌이 있는데
이렇게 기획이 쇽쇽 바뀔 줄 몰랐다🥲</p>
</li>
</ul>
<h3 id="db-테이블-셋팅">DB 테이블 셋팅</h3>
<ul>
<li>회원가입부터 시작해서 우리가 기능에 사용 할 데이터나
유저들을 관리하기 위한 데이터를 셋팅하는 것을 했다.
막상 적다보니 정말 대량의 데이터를 다루게 됐는데
그래도 기능과 사용할 데이터 scheme을 팀원 모두가 같이 짜다보니
우리가 만드는 서비스 속 기능과 데이터 간의 관계 등 전반적인 흐름을 알게 되는
좋은 시간이었다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/12d2ca00-a9f7-4c83-ac63-2aabeb0eeea4/image.png" alt=""></li>
</ul>
<h3 id="기술적인-이슈">기술적인 이슈</h3>
<ul>
<li>supabase realTime을 적용하여
실시간 채팅을 구현해보았다.
공식문서가 워낙 잘 나와있어서 학습하고 구현까지는 어렵지 않았지만,
실제 유저 간의 실시간 채팅이 생성될 때부터
유저들이 참여한 채팅방을 구독하는 것에 대해
DB를 어떻게 구성해야할지 의문이 들었다.
오늘 찾아보고 학습한 결과로는</li>
</ul>
<p><strong>채팅방 생성 시</strong></p>
<blockquote>
</blockquote>
<ol>
<li>유저의 데이터 속에 참여한 채팅방의 id를 가지도록 한다</li>
<li>생성 된 채팅방 테이블에 해당 채팅에 참여한 유저들의 정보와 채팅방 고유의 정보를 넣는다.</li>
<li>유저가 가진 채팅방의 id로 해당 id를 가진 채팅방에 업데이트가 있을 시 알림을 받도록 구독한다.</li>
<li>메세지 테이블은 별도로 만들어 <strong>채팅방 테이블과 연결해놓는다</strong>.</li>
</ol>
<ul>
<li>위의 방법이 일반적으로 사용하는 채팅방의 구조인데
4번의 채팅방 테이블과 채팅방 메세지를 연결해놓는다는 게 이해가 잘 안 된다.
<code>foreign key</code>라는 것을 사용하여 데이터간의 관계를 형성하는 것이라고 하는데
아직 그것에 대한 공부는 하지 못 했다🥲
내일까지는 학습을 마쳐야 디자인이 나오는 즉시 개발하고 테스트가 가능할 거 같다.
될 수 있으면 쿼리문도..</li>
</ul>
<h3 id="느낀점">느낀점</h3>
<ul>
<li>회의가 끝이 없다
물론 회의가 끝나면 산으로 가는 느낌이 아닌
확실히 더 나아지고 있다는 느낌이 들고
팀원이 모두 자신의 의견만으로 어떻게 해보려는 것이 아닌
우리 서비스에 진심어린 애정을 가지고 있는 것이 느껴져서 정말 좋다.
기술적인 이슈는 학습하면 그만이다.
최종 프로젝트는 소통도 성장도 모두 가져갈 수 있는 기회인 거 같다.</li>
</ul>
<p><strong>소통이 활발한 우리 팀</strong></p>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/241027e9-735c-48e9-8d79-c17a9a3c2a47/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/5e2cb2fa-c4dc-4601-9516-d5073061bb9a/image.png" alt=""></p>
<h4 id="디자이너님-도망가시면-안-ㄷ🥲">디자이너님 도망가시면 안 ㄷ......🥲</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 - 준비]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A4%80%EB%B9%84</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A4%80%EB%B9%84</guid>
            <pubDate>Fri, 05 Jan 2024 11:40:29 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-배운-것">오늘 배운 것</h2>
<ul>
<li><p>supabase의 회원가입, 로그인</p>
<h5 id="supabase의-공식문서가-너무-잘-되어있어서-이해하기-쉬웠다">supabase의 공식문서가 너무 잘 되어있어서 이해하기 쉬웠다!</h5>
<p><a href="https://supabase.com/docs/guides/auth/quickstarts/react">supabase Auth</a></p>
</li>
</ul>
<h2 id="supabase-회원가입--로그인-구현하기🤩">supabase 회원가입 / 로그인 구현하기🤩</h2>
<h3 id="1-supabase-organization-만들기">1. supabase organization 만들기</h3>
<ul>
<li><a href="https://supabase.com">supabase 공식 홈페이지</a> 에 방문하여
로그인 후 organization을 만든다.</li>
<li>organization은 로그인 후 dashboard에 들어가면 생성할 수 있다.</li>
</ul>
<h4 id="dashboard의-org-생성-창">dashboard의 org 생성 창</h4>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/99a11eec-e181-4c18-9bcd-73cfb884e1d7/image.png" alt=""></p>
<ul>
<li>organiztion에 조직 이름, 조직 타입, 사용 할 플랜을 선택하는데</li>
<li><em>중요한 것은 역시 플랜*</em>!
무료 플랜은 프로젝트를 두 개까지 생성 가능하고
일주일 동안 활동이 없으면 일시정지가 되는 것이 특징이다.
이 외에도 특징이 있는데 (스토리지 1GB, 데이터베이스 500MB 등)
자세한 것은 <a href="https://supabase.com/pricing">supabase 가격표</a>를 참고하길.</li>
</ul>
<h3 id="2-project-생성">2. project 생성</h3>
<ul>
<li><p>project는 org와 마찬가지로 <strong>dashboard에서 생성</strong> 할 수 있다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/afb31692-bc89-427a-b762-060856ea5477/image.png" alt=""></p>
</li>
<li><p>크게 건드릴 부분은 없고</p>
</li>
<li><p><em>org 선택, 지역 선택만 잘하면 된다*</em>.</p>
</li>
<li><p><em>Database password는 나중에 변경도 가능*</em>하다.</p>
</li>
</ul>
<h3 id="3-project-url-db-key-발급">3. Project URL, DB KEY 발급</h3>
<ul>
<li>프로젝트를 생성 후 본격적으로 Auth를 사용하기 전에
생성한 <strong>프로젝트 - settings - API 로 이동</strong>하여
<code>Project URL</code>과 <code>Project API KEY</code>를 복사해두자.</li>
</ul>
<h3 id="4-본격-api-사용">4. 본격 API 사용</h3>
<ul>
<li><p>위의 3단계를 끝냈다면
코드 에디터를 켜서 프로젝트를 만들자
나는 리액트로 진행했다.</p>
</li>
<li><p>프로젝트를 생성했다면
터미널에 아래의 코드를 입력하여 <code>supabase-cli</code>를 설치하자
난 결국 기본적인 form을 만들어서 진행했지만,</p>
</li>
<li><p><em>supabase에서 제공해주는 편리한 템플릿*</em>도 궁금해서 <code>auth-ui</code>도 같이 받았다.
템플릿을 사용하지 않는다면 <code>@supabase/supabase-js</code> 만 받아도 된다.</p>
</li>
</ul>
<p>npm</p>
<blockquote>
<p>npm install @supabase/supabase-js </p>
</blockquote>
<p>템플릿을 사용하고 싶다면 추가로 설치</p>
<blockquote>
<p>npm install @supabase/auth-ui-react @supabase/auth-ui-shared</p>
</blockquote>
<h4 id="supabase에서-지원하는-템플릿">supabase에서 지원하는 템플릿</h4>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/0de8fd07-4f45-4563-a37c-a5a12ed5b73b/image.png" alt=""></p>
<h3 id="6-supabase-client-생성">6. supabase Client 생성</h3>
<ul>
<li><code>supabase-cli</code>에서 제공하는 <code>createClient()</code>를 사용하여
프로젝트 내에 생성해준다.</li>
<li><code>createClient()</code> 에 전달하는 인자는 <strong>위의 3번 단계에서 복사해둔</strong>
<code>Project URL</code>과 <code>Project API KEY</code>다.<pre><code class="language-js">const supabase = createClient(
  &quot;3번에서 복사해둔 Project URL&quot;,
  3번에서 복사해둔 Project API KEY
);</code></pre>
</li>
</ul>
<h3 id="6-provider-설정">6. Provider 설정</h3>
<ul>
<li>supabase 페이지의 <strong>프로젝트 - settings</strong>에 가서 <strong>Providers를 확인</strong>해보자
supabase의 회원가입 방식에는 아래와 같은 목록이 있는데
<img src="https://velog.velcdn.com/images/waffle_bear/post/2ee51b4d-fe75-4a35-ad14-5ce61e588045/image.png" alt="">
사용하고자 하는 방식을 활성하면 된다.
난 email 방식만 사용하여 테스트 했고,</li>
<li><em>email 가입 방식*</em>은 기본적으로 <strong>invite 이메일을 확인해야 회원가입이 완료되는 형식</strong>이라
테스트 하기가 번거로워 회원가입 시 <strong>confirm email 설정을 비활성</strong> 했다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/1e2cefdd-8b5f-49da-ba04-f886ba70370c/image.png" alt=""></li>
</ul>
<h4 id="confirm-email을-켜두면-회원가입-시-아래와-같이-보여진다">Confirm email을 켜두면 회원가입 시 아래와 같이 보여진다</h4>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/684859cc-66fb-4ff9-8427-96d27a485fb8/image.png" alt=""></p>
<h3 id="7-로그인-회원가입">7. 로그인, 회원가입</h3>
<ul>
<li><strong>supabase-cli</strong>에 <code>signup()</code>, <code>signout()</code>, <code>signin()</code>이 모두 있기 때문에
간편했다.
<a href="https://supabase.com/docs/guides/auth/auth-email">Auth API 문서</a></li>
</ul>
<pre><code class="language-js">// signup 예시
async function signUpNewUser() {
  const { data, error } = await supabase.auth.signUp({
    email: &#39;example@email.com&#39;,
    password: &#39;example-password&#39;,
    // 이 옵션이 있으면 confirm email을 받은 유저가
    // 이메일의 링크를 클릭 시 이동하게 되는 사이트 같다. (안 써봐서 확실치 않다.)
    options: {
      emailRedirectTo: &#39;https//example.com/welcome&#39;
    }
  })
}

// signin 예시
async function signInWithEmail() {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: &#39;example@email.com&#39;,
    password: &#39;example-password&#39;
  })
}</code></pre>
<h4 id="로그인-응답으로-오는-data를-출력하면-아래와-같은-정보가-있다">로그인 응답으로 오는 data를 출력하면 아래와 같은 정보가 있다.</h4>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/ecf8a523-681c-41a4-b04a-86653e40d624/image.png" alt=""></p>
<h3 id="참고">참고</h3>
<ul>
<li>암호의 최소 길이, 토큰 만료시간 등을 설정할 수 있는 곳은
supabase의 내 프로젝트 - settings - Authentication에 들어가보면 변경 가능하다.</li>
</ul>
<h2 id="느낀점">느낀점</h2>
<ul>
<li><strong>supabase realTime</strong> 기능을 학습하고 싶어서 시작했다가
기본적인 기능을 모두 공부하는 중인데 맘에 든다 <strong>supabase</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 기획]]></title>
            <link>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@waffle_bear/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Thu, 04 Jan 2024 14:21:42 GMT</pubDate>
            <description><![CDATA[<h3 id="프로젝트명">프로젝트명</h3>
<ul>
<li>palette market</li>
</ul>
<h3 id="프로젝트-소개">프로젝트 소개</h3>
<ul>
<li>비싼 미술용품을 다 쓰지도 못한 상태로 학기가 끝나서
아깝지만 버리거나, 처분하지 못해 곤란한 미대생들을 위한
중고 거래, 공동 구매 서비스를 제공하는 프로젝트</li>
</ul>
<h3 id="사용-기술">사용 기술</h3>
<ul>
<li>React</li>
<li>styled-components</li>
<li>react-query</li>
<li>supabase</li>
</ul>
<h3 id="와이어-프레임">와이어 프레임</h3>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/af8ac758-528e-40f2-8bc7-41786668b11a/image.png" alt=""></p>
<h3 id="느낀점">느낀점</h3>
<ul>
<li><p>최종 프로젝트는 기간이 이전 과제보다는 여유가 있어
기획 회의를 길게 가졌다.
역시 소통은 협업의 꽃이다⭐</p>
</li>
<li><p>이번 프로젝트는 리얼타임 관련 기능이 핵심 기능으로 들어가는데
너무 재밌을 거 같고. 모두 성장하는 프로젝트가 될 거 같다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 끝 - KPT 회고]]></title>
            <link>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%81%9D-KPT-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%81%9D-KPT-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 03 Jan 2024 08:40:55 GMT</pubDate>
            <description><![CDATA[<h3 id="팀-프로젝트-개요">팀 프로젝트 개요</h3>
<ul>
<li>프로젝트 종류: 이미지 제공 웹</li>
<li>사용 기술: <code>next js</code>, <code>firebase</code>, <code>next-auth</code>, <code>useQuery</code></li>
<li>API: <code>unsplash</code>, <code>pixabay</code></li>
</ul>
<h2 id="kpt-회고">KPT 회고</h2>
<h3 id="keep">Keep</h3>
<ul>
<li>새로운 기술에 도전하는 것</li>
</ul>
<h3 id="problem">Problem</h3>
<ul>
<li>코드 리뷰가 없음</li>
<li>새로운 도전은 좋지만
어떤 기술에 대해서 왜 사용했는지
어디에 어떻게 적용하여 의미가 있었는지 말하지 못 함.
모던 웹 기술에 포함된다고 그냥 사용하면 아무런 의미가 없다.</li>
<li>역할 분담 실수</li>
</ul>
<h3 id="try">Try</h3>
<ul>
<li>일정이 촉박해져도 코드 리뷰를 진행하도록.</li>
<li>새로 학습하는 기술이 왜 나왔는지
어떤 문제 해결에 좋은지 파악하고 진행하도록 (공부⭐ 공부⭐).</li>
<li>역할 분담을 적절하게 할 수 있는 방법을 찾도록.</li>
</ul>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li><p>이번 프로젝트에서는 경험치가 좀 쌓였다.
유저 데이터 가공 및 관리, 대량의 데이터 가공 및 관리, 인공지능 API 사용,
데이터 검수를 위한 로직 작성 등</p>
<p>여러 곳에서 새롭게 마주하는 문제들이 나왔고
그런 문제에 접근하고, 해결하는 그 과정이 재밌었는데
처음에는 막막하고 불가능할 거 같았던 문제를 직접
해결하는 것이 너무 재밌었다.</p>
</li>
<li><p>중간에 팀원들과 라이브 셰어를 켜놓고
한 사람한테 우르르 붙어서 코드를 작성했는데
신선하고 재밌었다. 오랜만에 협업 느낌이 들었다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 - 불량데이터 검사]]></title>
            <link>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B6%88%EB%9F%89%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%80%EC%82%AC</link>
            <guid>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B6%88%EB%9F%89%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%80%EC%82%AC</guid>
            <pubDate>Tue, 02 Jan 2024 00:59:23 GMT</pubDate>
            <description><![CDATA[<h3 id="오늘의-문제">오늘의 문제</h3>
<ul>
<li>DB에 저장한 데이터 중 pixabay에서 온 이미지 url이
비어있는 문제</li>
</ul>
<h3 id="문제-해결">문제 해결</h3>
<ul>
<li>DB에서 데이터를 가져올 때
클라이언트단에서 데이터를 한번 검사하는 함수 작성</li>
<li>만약 해당 이미지로 GET 요청 시 response가 200이 아니라면
제외 후 UI 생성</li>
</ul>
<h3 id="접근-방법">접근 방법</h3>
<ul>
<li>이미지 url이 비어있을 때.
어떻게 사용하기 전 미리 알 수 있을까 고민했다.</li>
<li>우리가 받아 온 image url로 GET 요청을 했을 때
응답이 200이 아닌 다른 것으로 올까? 하여 시도해봄.</li>
<li>실험해본 결과 불량 데이터는 페이지가 뜨는데 <code>400 error</code>가 떳다!</li>
</ul>
<h3 id="해결-과정">해결 과정</h3>
<ol>
<li><p>이미지 존재 여부를 파악하는 함수 작성</p>
<pre><code class="language-js">const checkImageExists = async (url: string) =&gt; {
 try {
   // 이미지 url에 get 요청 후 그것에 대한 응답을 변수에 담는다.
   const response = await fetch(url);
   return response.status; // 응답 상태코드를 반환 (200,400 등..)
 } catch (error) {
   console.error(&#39;Error checking image existence:&#39;, error);
   return false; // 에러가 발생하면 이미지가 없다고 가정
 }
};</code></pre>
</li>
<li><p>DB에서 받아온 이미지 배열을 검사하기 위한 함수 작성</p>
<pre><code class="language-js">// DB 데이터를 받아 온 배열을 인자로 넣음
const isExist = async (fbData: fetchedItem[]) =&gt; {
 // 데이터를 순회하며 imgPath에 GET 요청을 시도하며
 // GET 요청의 결과가 200 이라면 해당 아이템을 반환
 // GET 요청의 결과가 200이 아니라면 null을 반환
 // 위의 결과값들로 proceed라는 변수에 새 배열을 생성
 const proceed = await Promise.all(
   fbData.map(async (item: any) =&gt; {
     const result = await checkImageExists(item.imgPath);
     if (result === 200) return item;
     else return null;
   })
 );

 // 최종 반환 값은 검사가 끝난 배열에서 null 값을 제외한 배열을 반환
 return proceed.filter((el) =&gt; el !== null);
};</code></pre>
</li>
</ol>
<h2 id="찝찝한-부분">찝찝한 부분</h2>
<ul>
<li>데이터를 저장할 때 이 과정을 실행했다면
이후 데이터를 사용할 때 로딩시간이 조금은 단축되지 않았을까 싶다🥲</li>
</ul>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>이번 프로젝트는 트러블 슈팅 할 것들이 많다.
새삼 재밌다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트_이미지 태그 자동생성 AI]]></title>
            <link>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B7%B8-%EC%9E%90%EB%8F%99%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B7%B8-%EC%9E%90%EB%8F%99%EC%83%9D%EC%84%B1</guid>
            <pubDate>Tue, 02 Jan 2024 00:45:26 GMT</pubDate>
            <description><![CDATA[<h3 id="문제였던-상황">문제였던 상황</h3>
<ul>
<li>이미지 API를 두 곳에서 받아와
우리의 DB에 넣기 전 가공하는 과정이 필요했다.</li>
<li>우리가 사용할 값들로 재구성 된 값들 중
tag 속성은 나중에 이미지를 검색할 때 필요한
아주 중요한 값이었다.</li>
<li>하지만 unsplash API는 검색 API가 아니라면
tag 속성을 제공해주지 않았던 것.............⭐</li>
</ul>
<h3 id="접근-방식">접근 방식</h3>
<ul>
<li>사실 떠오른 방식이 없었다</li>
</ul>
<h4 id="이미지-인식-ai를-알기-전까지는🤩">이미지 인식 AI를 알기 전까지는..🤩</h4>
<h3 id="해결-과정">해결 과정</h3>
<ol>
<li><p><strong>clarifi</strong> 라는 다양한 인공지능 API를 지원하는 사이트를 알았다.
사용한 모델은 image-recognition.</p>
</li>
<li><p>먼저 DB에서 가져온 이미지를 배열에 담은 후
map을 사용하여 이미지 url만 따로 배열에 담는다.
<img src="https://velog.velcdn.com/images/waffle_bear/post/663a93d1-8437-4c06-8c83-3dc40a58bc9f/image.png" alt=""></p>
</li>
</ol>
<ol start="3">
<li>AI의 API에 분석하고자 하는 inputs를 담아 자동 태그 부여 시작!
<img src="https://velog.velcdn.com/images/waffle_bear/post/3997df84-8a34-40d7-9cf3-2bafab5902bf/image.png" alt=""></li>
</ol>
<h2 id="결과">결과</h2>
<ul>
<li>정상적으로 태그가 부여되어 firebase에 담겼다.🥹
<img src="https://velog.velcdn.com/images/waffle_bear/post/9703e9b2-51af-4baf-bae3-caa03869f2d3/image.png" alt=""></li>
</ul>
<h2 id="느낀점">느낀점</h2>
<ul>
<li><p>AI가 엄청나게 매력적이라는 생각이 가장 크게 들었다.
다만 검색을 시연했을 때 부여 된 태그의 정확성이 조금은 떨어져서 아쉽다.🥹</p>
</li>
<li><p>대량의 데이터를 다루고, 여러 개의 API를 사용하는 경험은 좋은 경험이었다.
이미지 url만 추려내거나, 태그가 부여 된 url을 다시 기존의 데이터와 합치고
firebase에 담는 과정이 제일 힘들었다🥲</p>
<p>그래도 <strong>오늘은 경험치 + 1 달성⭐</strong></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀 프로젝트 (semi-final) _ 기획]]></title>
            <link>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-semi-final-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@waffle_bear/%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-semi-final-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Wed, 27 Dec 2023 12:36:43 GMT</pubDate>
            <description><![CDATA[<h3 id="프로젝트명">프로젝트명</h3>
<ul>
<li>findPic</li>
</ul>
<h3 id="사용-기술">사용 기술</h3>
<ul>
<li>next js (Pages Router)</li>
<li>styled-components</li>
<li>axios</li>
<li>react-query</li>
<li>firebase</li>
</ul>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li>이미지를 제공하는 사이트들의 API를 사용하여
하나의 페이지에서 다양한 사이트의 이미지를 제공해주는 것</li>
<li>이미지 검색</li>
<li>태그 기반 관련 이미지 추천</li>
<li>다운로드 기능</li>
<li>좋아요 기능</li>
</ul>
<h3 id="맡은-문제">맡은 문제</h3>
<ol>
<li><p>API를 통해 가져오는 이미지를 가공하여
우리의 DB에 가공하여 넣는다.
가공하여 추가되는 이미지 데이터 중 중복되는 데이터는
DB에 추가되지 않고
중복되지 않는 이미지만 선별하여 DB에 추가.</p>
</li>
<li><p>날마다 DB의 하루 할당량, API 호출 할당량을 고려하여
DB를 업데이트 하는 것.</p>
</li>
<li><p>DB의 데이터가 쌓일수록 중복을 검수하는 시간이 엄청나게 늘어날텐데
검수를 빠르게 할 수 있는 코드를 생각하여 작성해야 할 거 같음.
DB의 업데이트 속도가 느려진다면
<code>cron expression</code>을 사용하여 새벽 점검 같은 느낌의 DB 업데이트를 사용 할 예정..</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[개인 프로젝트 - 2일 차]]></title>
            <link>https://velog.io/@waffle_bear/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%9D%BC-%EC%B0%A8</link>
            <guid>https://velog.io/@waffle_bear/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%9D%BC-%EC%B0%A8</guid>
            <pubDate>Fri, 22 Dec 2023 07:17:31 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>메인페이지 완성</li>
<li>어제의 문제 해결</li>
</ul>
<h2 id="오늘의-결과물">오늘의 결과물</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/38cb3d73-a79d-487d-a054-c21d3bbee21c/image.gif" alt=""></p>
<h2 id="어제-문제였던-것">어제 문제였던 것</h2>
<ul>
<li>Fontawesome 모듈을 동적으로 import 하는 것<ul>
<li>FontAwesome은 동적 import가 불가능 했다..
그래서 따로 공통 컴포넌트로 빼서 Icon을 담당하도록 하였다.</li>
</ul>
</li>
</ul>
<h4 id="해결✅">해결✅</h4>
<pre><code class="language-js">&lt;util/iconGenerator.js&gt;

import * as icon from &quot;@fortawesome/free-solid-svg-icons&quot;;
import { FontAwesomeIcon } from &quot;@fortawesome/react-fontawesome&quot;;

// 아이콘 컴포넌트를 반환
export const retunIcon = (iconName, size = &#39;1x&#39;, color = &#39;white&#39;) =&gt; {
  return &lt;FontAwesomeIcon style={{ color }} icon={icon[iconName]} size={size} color=&quot;white&quot; /&gt;;
};
</code></pre>
<h2 id="오늘의-문제">오늘의 문제</h2>
<ul>
<li>하나의 <code>useRef</code>로 <strong>여러 개의 값을 관리</strong>하는 것.</li>
</ul>
<h4 id="react에서-intersectionobserver-api-사용-과정">React에서 IntersectionObserver API 사용 과정</h4>
<ul>
<li><strong>Intersection Observer</strong>를 사용할 때
<code>new IntersectionObserver()</code>의 <code>observe()</code> 메서드는</li>
<li><em>인자로 관찰 할 target을 전달*</em>해줘야 하는데
이 <strong>target은 DOM Element</strong>다.
그래서 리액트는 <strong>useRef를 사용하여 타겟을 전달</strong>해주는데
방법은 아래와 같다.</li>
</ul>
<ol>
<li>useRef Hook으로 변수 생성</li>
<li>관찰하고자 하는 요소에 <strong>ref속성</strong>을 주고
그 요소를 <code>useRef()</code>를 할당한 변수의 <strong>current 값</strong>으로 할당한다.</li>
<li>컴포넌트 마운트 시 관찰 대상을 <strong>observe() 메서드에 등록</strong>한다.<pre><code class="language-js">const observeSection = () =&gt; {
 // observe() 메서드에 ref 속성을 가진 요소를 전달
 observer.observe(observeTarget.current)
};</code></pre>
</li>
<li>IntersectionObserver의 callback 함수에
관찰 대상이 나타났을 때(나타났을 때 외에도 여러가지 옵션이 있다)
적용하고자 할 코드를 작성한다.</li>
</ol>
<pre><code class="language-js"> // useRef를 하나 생성
 const observeTarget = useRef();

  // observe 할 대상이 화면에 나타나면 적용되는 코드
  const observer = new IntersectionObserver((e) =&gt; {
    e.forEach((el) =&gt; {
      if (el.isIntersecting) el.target.style.animationPlayState = &quot;running&quot;;
    });
  });

  // 관찰 할 대상을 등록하는 함수
  const observeSection = () =&gt; {
    observer.observe(observeTarget.current)
  };

  // 컴포넌트가 mount 시 observer에 타겟을 전달한다
  useEffect(() =&gt; {
    observeSection();
  }, []);

 // UI
 &lt;HireTextUl ref={observeTarget}&gt;
   {listUI}
 &lt;/HireTextUl&gt;</code></pre>
<h3 id="문제의-시작">문제의 시작</h3>
<ul>
<li><p>위와 같이 하다보니 문제가 생겼다
애니메이션을 주고 싶은 대상이 많을 때
useRef가 5-6개가 되어버린 것이다..</p>
</li>
<li><p>useRef를 하나만 사용할 때는
자세한 작동원리를 모르고 사용했는데
오늘 useRef의 생김새를 뜯어보게 됐다.</p>
</li>
</ul>
<h3 id="해결-과정">해결 과정</h3>
<h4 id="1-useref-뜯어보기">1. useRef 뜯어보기</h4>
<ul>
<li>useRef에 초기 값을 배열로 주게 되면 어떻게 되나 확인했다.<pre><code class="language-js">const test = useRef([]);
console.log(test); // {current: Array(0)}</code></pre>
이렇게 되면 <code>test.current</code> 는 배열이란 거구나~ 하고 깨달았다.</li>
</ul>
<h4 id="2-refcurrent-배열에-dom-요소-추가하기">2. ref.current 배열에 DOM 요소 추가하기</h4>
<ul>
<li>아래의 코드를 보면 ref 속성 안에
<code>(el) =&gt; (observeTarget.current[1] = el)</code> 요런 callback 함수가 있는데
저 함수의 인자는 해당 DOM 요소를 뜻한다.
따라서 callback 함수는 <strong>ref.current[1]에 해당 DOM 요소를 담겠다</strong> 라는 뜻인 것.</li>
</ul>
<pre><code class="language-js"> &lt;HireTextUl ref={(el) =&gt; (observeTarget.current[1] = el)}&gt;
   {listUI}
 &lt;/HireTextUl&gt;
 &lt;ButtonBox ref={(el) =&gt; (observeTarget.current[2] = el)}&gt;&lt;/ButtonBox&gt;
 // 생략</code></pre>
<h4 id="3-이제-refcurrent에-여러-dom-요소를-담도록-하자">3. 이제 ref.current에 여러 DOM 요소를 담도록 하자</h4>
<pre><code class="language-js">&lt;HireTextUl ref={(el) =&gt; (observeTarget.current[1] = el)}&gt;
   {listUI}
&lt;/HireTextUl&gt;

&lt;ButtonBox ref={(el) =&gt; (observeTarget.current[2] = el)}&gt;
      &lt;Button
      width={160}
      height={60}
      isBackground={true}
      text={&quot;Hire a trainer&quot;}
    /&gt;
    &lt;Button width={160} height={60} text={&quot;Talk in person&quot;} /&gt;
&lt;/ButtonBox&gt;

&lt;ImageBox ref={(el) =&gt; (observeTarget.current[3] = el)}&gt;
  &lt;ImageEl src={&quot;trainer&quot;} /&gt;
&lt;/ImageBox&gt;</code></pre>
<h4 id="4-마지막으로-observe-메서드에-dom-요소들을-등록해준다">4. 마지막으로 observe 메서드에 DOM 요소들을 등록해준다.</h4>
<pre><code class="language-js">const observeElement = () =&gt; {
  observeTarget.current.forEach((el) =&gt; {
    observer.observe(el);
  });
};</code></pre>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>늘 쓰던 속성(ref)이라도 저런 콜백함수를 사용할 수 있는지 몰랐다.
저런 방식으로 ref의 current 값에 DOM 요소를 할당 할 수 있다니
신세계다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[개인 프로젝트 시작]]></title>
            <link>https://velog.io/@waffle_bear/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@waffle_bear/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Thu, 21 Dec 2023 10:46:41 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-했던-것">오늘 했던 것</h2>
<ul>
<li>개인 프로젝트 기획</li>
<li>메인 페이지 작성</li>
</ul>
<h2 id="프로젝트-설명">프로젝트 설명</h2>
<h3 id="이름">이름</h3>
<ul>
<li>About Exercise</li>
</ul>
<h3 id="개요">개요</h3>
<ul>
<li>건강한 라이프 스타일을 위한 웹앱</li>
</ul>
<h3 id="사용하는-것">사용하는 것</h3>
<ul>
<li>React</li>
<li>react-query</li>
<li>react-router</li>
<li>styled-components</li>
<li>axios</li>
<li>json-server</li>
<li>firebase Auth</li>
<li>firebase Storage</li>
</ul>
<h3 id="구현-할-기능">(구현 할) 기능</h3>
<ul>
<li>헬스장 검색 기능<ul>
<li>검색 된 헬스장의 평점, 리뷰 등을 보여줍니다  </li>
</ul>
</li>
</ul>
<ul>
<li>운동 튜토리얼 제공<ul>
<li>Youtube API를 활용한 영상 제공</li>
<li>원하는 운동을 검색하여 영상 시청</li>
</ul>
</li>
<li>다양한 카테고리의 게시판 기능<ul>
<li>헬스뿐만 아니라 다양한 라이프 스타일 관련 커뮤니티 기능</li>
</ul>
</li>
</ul>
<h2 id="오늘의-결과물">오늘의 결과물</h2>
<p><img src="https://velog.velcdn.com/images/waffle_bear/post/04acc9a8-3bc0-4dd3-9c74-0372af682a4b/image.gif" alt=""></p>
<h2 id="문제점-해결-x">문제점 (해결 X)</h2>
<ol>
<li>서버에서 제공하는 서비스 데이터를 가져오는데
데이터에 존재하는 아이콘 모듈을 동적으로 import해야 한다.</li>
</ol>
<ul>
<li>일단 비동기 import까지는 했지만 아직 적용을 못 해서  </li>
<li><em>UTF-8 Emoji를 parsing 하여 대체*</em> 해놨다🥺</li>
</ul>
<h2 id="느낀점">느낀점</h2>
<ul>
<li><p>쉽지만은 않은 난이도의 프로젝트를 계획했지만..!
초반부터 문제점이라니.</p>
<p>하지만 의미있는 문제를 마주한 거 같아서 기쁘다.
비동기 관련 된 문제라니..!
내일은 해결할 수 있길...🙏</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>