<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_soul_.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 12 Feb 2024 10:59:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. _soul_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_soul_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[👏내일배움캠프 React 3기 수료 후기👏]]></title>
            <link>https://velog.io/@_soul_/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-React-3%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-React-3%EA%B8%B0-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 12 Feb 2024 10:59:43 GMT</pubDate>
            <description><![CDATA[<p>내일배움캠프 React 3기 수료 후기...</p>
<p>4개월 간의 내일배움캠프를 마치고 꿀같은 설 연휴 휴식을 만끽하며 수료후기를 작성합니다.
이렇게 보니 연휴 전에 일정이 끝나서 참 다행이라는 생각이 드네요. 연휴 내내 프로젝트 마무리하고 있을 생각하니,, <del><em>(노드 트랙 아직 안끝나신 것 같은데,, 화이팅임다..!!)</em></del></p>
<p>수많은 부트캠프 중 왜 내배캠을 선택했는지, 내배캠에서 공부하면서 어떤 점이 좋았는지/아쉬웠는지, 그래서 내배캠을 추천하는지 주절주절 적어보겠습니닷</p>
<p>_<strong>아주 개인적인 의견</strong>_임을 먼저 말씀드리며..!!</p>
<blockquote>
<h3 id="내배캠을-선택한-이유">내배캠을 선택한 이유</h3>
</blockquote>
<h4 id="짧은-교육-기간">짧은 교육 기간</h4>
<p>제가 내배캠을 선택한 가장 큰 이유는 교육기간이 다른 국비지원 부트캠프에 비해 짧다는 것이었습니다. 보통 국비지원 부트캠프는 최소 6개월 코스인 것에 비해 내배캠을 4개월+α로 짧았고, 교육 일정이 월~금으로 주말이 보장된다는 것이 굉장히 큰 장점이었습니다. (물론, 팀 프로젝트 기간에는 주말에도 종종 작업을 하기도 했습니다.) 짧은 기간동안 밀도 있게 공부해서 빨리 취업준비를 하고 싶다는 생각이 크기도 했고, 캠프 기간이 길어지게 되면 &#39;9 to 6 매일매일 공부하는 나 자신&#39;에만 만족해버릴까봐 걱정도 됐기 때문에 기간이 짧다는게 가장 큰 장점이었던 것 같습니다.</p>
<h4 id="캠프-시작-날짜">캠프 시작 날짜</h4>
<p>부트캠프를 알아보던 당시에 다른 공부를 위해 학원을 다니고 있을 때라 그 기간 이후로 곧바로 시작할 수 있는 일정이라서 좋았습니다. (사전캠프는 참가하지 않았습니다.)</p>
<h4 id="간편한-지원-방식">간편한 지원 방식</h4>
<p>내일배움캠프 지원을 하면서 같이 봤던 다른 국비지원 부트캠프에서는 3문항 정도의 자기소개서를 요구했는데 그게 생각보다 부담스럽더군요;; 그에 반해 내배캠은 자기주도학습 경험과 관련된 간단한 짧은 자기PR? 영상만 제출하면 되는 거라서 간단했습니다. <del>(모집기간 임박/넘어서 지원하신 분들은 이런 영상 제출 없이도 지원합격 하셨다고 하시기도 했습니다.)</del></p>
<br>
<br>

<blockquote>
<h3 id="내배캠을-하면서-아쉬웠던-점">내배캠을 하면서 아쉬웠던 점</h3>
</blockquote>
<h4 id="내-것으로-만들기에는-물리적인-시간이-적다">&#39;내 것&#39;으로 만들기에는 물리적인 시간이 적다</h4>
<p>기간이 짧은 부트캠프의 양면성이겠지만 주중에는 캠프 공부와 프로젝트로 9to9 + α를 보내기 때문에 강의를 듣고 프로젝트를 진행하는 것 이외에 공부한 것을 나의 것으로 정리하고 복습하려면 주말 시간을 이용해야 합니다. 그런데 주중을 9to9 + α으로 보내다 보면,, 주말에는 정말 쉬고 싶다는 생각이 강하게 듭니다,, 그래서 캠프를 마치면서 팀원들과 날잡고 포트폴리오 정리나 블로그 정리할 시간을 가져야겠다 라는 이야기를 하기도 했습니다.</p>
<p>캠프를 진행하면서 정리하기에는 쉽지 않다는 의미!!</p>
<br>

<h4 id="코딩을-처음-해보는-거라면">코딩을 처음 해보는 거라면,,</h4>
<p>우선 저는 대학교에서 컴퓨터를 전공했고, 내배캠 합류 전에 개인적으로 기본적인 html + css + 바닐라 자바스크립트 정도를 개인적으로 공부했던 경험이 있습니다. (학부에서 웹프로그래밍 강의를 들은 적은 없습니다) 웹프로그래밍 경험이 없는 건 다른 수강생들과 다르지 않지만 기본적으로 &#39;코딩 기초체력&#39;은 아무래도 제가 더 좋다,,고,,해도,, 되겠죠,,?? <del>(학부때 공부를 그렇게 열심히 한건 아니지만 그래도,, 8학기 짬바,, 물론 캠프 하시는 분들 중에도 전공생분들이 여럿 있으시고 백엔드에서 개발하다가 오신 분들도 있고, 웹 개발 공부 한참 하시다 오신 분들도 있고 실력자이신 분들도 많은 걸로 알고 있습니다!)</del></p>
<p>무튼 캠프하면서 대부분의 팀 프로젝트에서 저는 실력이 좋은 편에 속했었는데 강의를 듣거나 과제를 하거나 팀 프로젝트를 하면서 <code>와 코딩 처음 해보는 사람은 따라가기 쉽지 않겠다</code>라는 생각을 여러 번 했던 것 같습니다. 아무래도 짧은 기간 내에 기초부터 심화까지 공부하는 커리큘럼이다 보니 일정이 빠르게 진행되는데 처음이라면 용어나 개념 자체가 생소할테니까요. 당연히 쉽지 않다는 의미이지 *<em>불가능하다는 의미가 절대 아닙니다!! *</em> 최종 프로젝트 때에는 <code>잘하는 사람 정말 많다</code>라는 생각이 계속 들 정도로 다들 멋지게 프로젝트 완성하시더라구요. 정말 대단하신 것 같아요.</p>
<p><br><br><br></p>
<blockquote>
<h3 id="내배캠을-하면서-좋았던-점">내배캠을 하면서 좋았던 점</h3>
<p><em>아쉬운 점을 먼저 적은 건 좋았던 점을 적기위한 빌드업,, 이라고 하겠습니다!</em>
다른 부트캠프를 경험해보지 않았기 때문에 <code>순수하게 내배캠 이런점이 좋았어요!</code>로 적어보겠습니닷</p>
</blockquote>
<h4 id="튜터-상주">튜터 상주</h4>
<p>즉각적으로 질의응답이 가능한 튜터님이 9to9 상주해 계셔서 좋았습니다. 저는 튜터님께 개인적으로 질문을 많이 하진 않았지만(스스로,, 해결하고 싶었습니다,,) 팀프로젝트를 할 때 기술적으로 문제가 있거나 의사결정 과정에서 조언이 필요할 때 바로바로 튜터님을 찾아가 의견을 들을 수 있어서 든든하고 좋았습니다.</p>
<h4 id="담임-매니저-관리와-전반적인-캠프-분위기">담임 매니저 관리와 전반적인 캠프 분위기</h4>
<p>담임 매니저님이 돌아다니시면서 팀원들 간 어색함도 풀어주시고 강의 수강 진도율 같은 것도 체크해주셨던 점이 좋았습니다. 아무래도 제공된 동영상 강의를 자기주도학습 하는 것이다보니 느슨해지고 게을러질 수 있는데 거의 매일 순회하시면서 강의/프로젝트 진행상황을 체크해주셔서 좋았고 항상 <code>!으쌰으쌰 할 수 있다 화이팅!</code> 해주셨던게 정말 큰 힘이 되었던 것 같습니다.</p>
<p>그런 담임 매니저님 덕분인지 전반적인 캠프 분위기도 <code>!으쌰으쌰 할 수 있다 화이팅!</code> + <code>항상 예쁜말로 소통하기</code>였어서 마지막까지 살짝만 지치고 <del>(지치지 않았다고 하면 그건 거짓말입니다)</del> 기분 좋게 마무리할 수 있었습니다. 물론 많은 사람이 모이는 것이기 때문에 빌런이 없다고는 말할 수 없겠지만 저는 그런 빌런을 거의 만나지 않고 좋은 사람들과 수료했다는 느낌을 받았습니다!</p>
<h4 id="팀-프로젝트와-성취의-경험">팀 프로젝트와 성취의 경험</h4>
<p>팀 프로젝트 있는 강의를 선호하지 않기도 했고 학부생활의 절반이 코로나가 심했던 기간이었다보니 팀플 경험이 많지 않았던 것이 대학생활에서 후회하는 것들 중 하나였는데 내배캠하면서 팀플할 기회가 많아서 좋았습니다. 회사에 들어가게 되면 팀플에 세계로 들어가게 되는 것과 마찬가지일 테니까요.</p>
<p>저는 개인적으로 성취의 경험을 굉장히 중요하게 생각하는 편인데 팀 프로젝트를 완성할 때마다 작은 일이지만 또 하나 해냈다! 라는 보람과 성취의 경험을 느낄 수 있어서 좋았습니다.</p>
<h4 id="최종-프로젝트-과정에서의-서포트">최종 프로젝트 과정에서의 서포트</h4>
<p>약 5주간 진행되는 최종 프로젝트에서는 처음으로 디자이너님 + 전담 튜터의 멘토링이 있었던 것이 좋았습니다. 아무래도 디자이너가 있으니 프로젝트의 완성도가 확 높아지는 느낌이 있었고 (좋은 기획 / 기능이 있어도 UI가 별로면 그런게 잘 안보이니까요) 매주 진행되는 기술 멘토링도 프로젝트 뿐 아니라 개인적인 성장에도 많은 도움이 되었습니다. 그리고 최종 프로젝트의 경우에는 팀을 거의 자율적으로 구성했는데 저 같은 경우에는 실력 좋고 인성 좋고 공부 많이 하시는 분들과 함께해서 팀원들에게 배운 것들도 정말 많았습니다. 그리고 발표 이후 이전 수료생 분들이 오셔서 저희에게 질문도 주시고 질문도 받아주신 것들도 앞으로 취업 준비하면서 도움이 많이 될 것 같습니다!</p>
<br>
<br>

<blockquote>
<h3 id="내일배움캠프를-추천합니다">내일배움캠프를 추천합니다?!</h3>
<p><del>누구에게나 추천할만한 부트캠프! ❌</del>
<strong>국비지원 프론트엔드 부트캠프를 들어야겠다고 결심한 분에게 추천할 만한 부트캠프! ⭕</strong></p>
</blockquote>
<p>그냥 프론트엔드 공부 한번 해볼까? 하는 분들에게는 <strong><em>절대절대절대</em></strong> 추천하지 않습니다.
<strong><code>부트캠프를 수료하고 바로 취업 준비해서 취뽀하겠다!</code> 라고 마음먹으신 분들이라면 충분히 추천할 만한 부트캠프라고 생각해요.</strong> 제가 다른 부트캠프는 경험이 없어서 비교해서 이런점이 좋습니다~라고 이야기할 순 없습니다. 그리고 내배캠 하나로 취업 준비 끝입니다! 라고 말하기에는 아쉬운 점이 조금 있긴 합니다만 그럼에도 불구하고 내배캠이 추천할 만하다라고 이야기하는 이유는 <strong><code>짧은 기간에 임팩트있게</code></strong> 공부할 수 있다는 장점이 있는 아주 크다고 생각하기 때문입니다.
(물론 아직 취업 지원 주차가 시작하지 않은 시점이기도 하고 그 이후에 취업준비는 아직 경험해보지 못한 부분이라 어떻다라고 말하기 어렵겠네요.)</p>
<p><Br><br></p>
<blockquote>
<p>++ 이미 내일배움캠프에 지원하기로 마음 먹었고, 지원하신 분이라면 캠프 시작 전에 간단하게라도 코딩공부나 웹프로그래밍 공부를 해보시면 좋을 듯 합니다. 위에 적었듯이 캠프가 빠르고 밀도있게 진행되다보니 조금이라도 선행되어있는 것이 있으면 따라가기 한결 수월하실 거예요!!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[전체 동의 버튼 만들기 (그런데 이제 react-hook-form을 곁들인)]]></title>
            <link>https://velog.io/@_soul_/%EC%A0%84%EC%B2%B4-%EB%8F%99%EC%9D%98-%EB%B2%84%ED%8A%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/%EC%A0%84%EC%B2%B4-%EB%8F%99%EC%9D%98-%EB%B2%84%ED%8A%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 29 Jan 2024 13:16:31 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/_soul_/post/3ccc50da-30a2-44e8-8888-de370defe5a6/image.png" alt=""></p>
<p>맨 위에 체크박스를 체크하면 밑에 두 개도 체크되게 하고
맨 위에 체크박스 체크 해제하면 밑에 두 개도 체크 해제되게 하고
다 체크되어있다가 밑에 두 개 중 하나 체크 해제하면 전체 동의 버튼도 해제하게 하는 그런 체크박스를 만들었다.</p>
<pre><code class="language-typescript">...
//전체 동의 클릭 시
  const allAgree = (e: React.FormEvent&lt;HTMLInputElement&gt;) =&gt; {
    if (e.currentTarget.checked) {
      setValue(`fullTerms`, true);
      setValue(`protection`, true);
      setValue(`useTerms`, true);
    } else {
      setValue(`fullTerms`, false);
      setValue(`protection`, false);
      setValue(`useTerms`, false);
    }
  };
  //약관 동의
  const agree = (e: React.FormEvent&lt;HTMLInputElement&gt;, target: string) =&gt; {
    if (e.currentTarget.checked) {
      setValue(target, true);
    } else {
      setValue(`fullTerms`, false);
      setValue(target, false);
    }
  };

...
const {
    register,
    formState: { isValid },
    setValue
  } = useForm({ mode: &quot;onBlur&quot; });
...
&lt;form action=&quot;&quot;&gt;
     &lt;div className=&quot;mobile:text-[14px]&quot;&gt;
          &lt;div className=&quot;border-black border-b&quot;&gt;
                 &lt;h2 className=&quot;mb-4 font-bold text-[20px] mobile:text-[18px]&quot;&gt;렌탈 약관동의&lt;/h2&gt;
          &lt;/div&gt;

          &lt;div className=&quot;p-7 border-b text-tc-middle mobile:p-4&quot;&gt;
                &lt;input
                    id=&quot;fullTerms&quot;
                    type=&quot;checkbox&quot;
                    {...register(&quot;fullTerms&quot;)}
                    required
                    className=&quot;w-4 h-4 mr-[20px] mobile:mr-[10px] mobile:w-3&quot;
                    onClick={allAgree}
                 /&gt;
                 &lt;label htmlFor=&quot;fullTerms&quot;&gt;전체 약관 동의&lt;/label&gt;
          &lt;/div&gt;
          &lt;div className=&quot;p-7 border-b text-tc-middle flex justify-between mobile:p-4&quot;&gt;
                 &lt;div&gt;
                      &lt;input
                        id=&quot;protection&quot;
                        type=&quot;checkbox&quot;
                        {...register(&quot;protection&quot;, { required: &quot;필수체크 사항입니다.&quot; })}
                        className=&quot;w-4 h-4 mr-[20px] mobile:mr-[10px] mobile:w-3&quot;
                        onClick={(e) =&gt; agree(e, `protection`)}
                      /&gt;
                      &lt;label htmlFor=&quot;protection&quot;&gt;개인 정보 보호를 위한 이용자 동의 (필수)&lt;/label&gt;
                    &lt;/div&gt;
                    &lt;p className=&quot;text-[14px] font-medium text-tc-light text underline cursor-pointer mobile:text-[10px] mobile:py-1.5&quot;&gt;
                      내역보기
                    &lt;/p&gt;
                  &lt;/div&gt;
                  &lt;div className=&quot;p-7 border-b text-tc-middle flex justify-between mobile:p-4&quot;&gt;
                    &lt;div&gt;
                      &lt;input
                        id=&quot;useTerms&quot;
                        type=&quot;checkbox&quot;
                        {...register(&quot;useTerms&quot;, { required: &quot;필수체크 사항입니다.&quot; })}
                        required
                        className=&quot;w-4 h-4 mr-[20px] mobile:mr-[10px] mobile:w-3&quot;
                        onClick={(e) =&gt; agree(e, `useTerms`)}
                      /&gt;
                      &lt;label htmlFor=&quot;useTerms&quot;&gt;렌트 상품 이용약관 동의 (필수)&lt;/label&gt;
                    &lt;/div&gt;
                    &lt;p className=&quot;text-[14px] font-medium text-tc-light text underline cursor-pointer mobile:text-[10px] mobile:py-1.5&quot;&gt;
                      내역보기
                    &lt;/p&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
                ...
</code></pre>
<p> react-hook-form setValue를 이용하면 뚝딱 해결할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[tailwindCSS로 스켈레톤 적용하기]]></title>
            <link>https://velog.io/@_soul_/tailwindCSS%EB%A1%9C-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/tailwindCSS%EB%A1%9C-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 25 Jan 2024 14:10:33 GMT</pubDate>
            <description><![CDATA[<p>서버에서 데이터를 불러오는 동안 유저가 심심하지 않게 애니메이션이 들어가 있는 스켈레톤을 적용했다.</p>
<pre><code class="language-typescript">//ImgPulse.tsx
import React from &quot;react&quot;;
import { FaRegImage } from &quot;react-icons/fa6&quot;;

const ImgPulse = () =&gt; {
  return (
    &lt;div className=&quot;flex items-center justify-center  absolute inset-0 bg-gray-200 rounded-lg animate-pulse&quot;&gt;
      &lt;FaRegImage className=&quot;text-tc-light&quot; size={40} /&gt;
    &lt;/div&gt;
  );
};

export default ImgPulse;

--------------------------------------------------------------------

//CardPulse.tsx
import React from &quot;react&quot;;
import ImgPulse from &quot;./ImgPulse&quot;;

const CardPulse = () =&gt; {
  return (
    &lt;div className=&quot;w-[246px]&quot;&gt;
      &lt;div className=&quot;relative h-[246px] w-[246px] mb-[20px]&quot;&gt;
        &lt;ImgPulse /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;flex flex-col gap-[10px] animate-pulse&quot;&gt;
        &lt;div className=&quot; h-[12px] w-[50px] bg-gray-200 rounded-full &quot;&gt;&lt;/div&gt;
        &lt;div className=&quot;text-[14px] font-[500] bg-gray-200 rounded-full h-[14px] w-[246px] &quot;&gt;&lt;/div&gt;
        &lt;div className=&quot;flex justify-between items-center&quot;&gt;
          &lt;div className=&quot;flex gap-[8px] items-center&quot;&gt;
            &lt;div className=&quot;text-[16px] h-[16px] w-[150px] bg-gray-200 rounded-full &quot;&gt;&lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CardPulse;</code></pre>
<p>나이쓰한 테일윈드 덕분에 간단하게 스켈레톤을 구현했다. <em>(className에 <code>animate-pulse</code>넣어주면 되더라~)</em></p>
<p>완성한 컴포넌트를 로딩되는 상황에 넣어주면 된다.</p>
<pre><code class="language-typescript">&quot;use client&quot;;
import React, { useState } from &quot;react&quot;;
...
import CartPulse from &quot;@/components/pulse/CartPulse&quot;;

const PaymentPage = ({ params }: { params: { userId: string } }) =&gt; {
  const userId = params.userId;
  const { data: cart, isLoading } = useQuery({
    queryKey: [&quot;cart&quot;],
    queryFn: async () =&gt; await getAllCart({ userId })
  });
 ...

  return (
    &lt;&gt;
      &lt;PageBreadCrumb linkList={linkList} /&gt;
      &lt;Section title={&quot;주문서&quot;} isCenter={true}&gt;
        {!isLoading &amp;&amp; cart !== undefined ? (
          ...
        ) : (
          &lt;CartPulse /&gt;        //이렇게
        )}
      &lt;/Section&gt;
    &lt;/&gt;
  );
};

export default PaymentPage;
</code></pre>
<p>적용한 모습
<img src="https://velog.velcdn.com/images/_soul_/post/b6e37443-5299-4516-97d7-a12e66b3b430/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[supabase] trigger/function 조건부로 작성하기]]></title>
            <link>https://velog.io/@_soul_/supabase-triggerfunction-%EC%A1%B0%EA%B1%B4%EB%B6%80%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/supabase-triggerfunction-%EC%A1%B0%EA%B1%B4%EB%B6%80%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 22 Jan 2024 13:13:52 GMT</pubDate>
            <description><![CDATA[<p>supabase auth를 이용해 회원가입/로그인을 구현하면서 유저 정보(이메일, 이름, 프로필 사진, 전화번호)를 직접 auth에 접근하지 않고 userinfo 테이블을 생성해 관리했다.
<a href="https://supabase.com/docs/guides/auth/managing-user-data">(supabase에서도 유저 정보 테이블을 따로 생성해 관리하는 것을 추천한다.)</a></p>
<p>auth에 새로운 데이터가 insert된 후에 작동하는 trigger와 함수를 작성해 자동으로 새로운 유저가 가입하면 userinfo 테이블에 정보가 삽입되게 하였다.</p>
<p>처음에 작성한 트리거와 함수는 다음과 같다.</p>
<pre><code class="language-sql">create function public.handle_new_user()
returns trigger as $$
begin
insert into public.userInfo(id, email, username, avatar_url)
values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;);
return new;
end;

$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users        
  for each row execute procedure public.handle_new_user();</code></pre>
<p>새로운 회원이 가입해 auth.users에 insert가 발생하면 그 이후에 <code>on_auth_user_created</code>라는 트리거가 활성화된다.</p>
<p><code>on_auth_user_created</code>는 <code>handle_new_user()</code>라는 함수를 실행한다.
<code>handle_new_user()</code>는 회원 가입 시 생성된 <code>raw_user_meta_data</code> <span style="font-size: 15px; color: darkgray">(auth.users 테이블에서 확인 가능)</span>에 있는 id (auth.users에 자동 생성되는 primary key), email, username, avatar_url을 <code>userInfo</code> 테이블의 각 column에 맞게 넣어준다. <span style="font-size: 15px; color: darkgray">(values 앞뒤로 순서 맞춰서 적어야 함)</span></p>
<p>이메일 회원가입 시 입력받은 이름을 <code>raw_user_meta_data</code>에 <code>name</code>이라는 키로 값을 저장해줬고, <code>avatar_url</code>을 <code>null</code>로 저장해줬으므로 회원가입 시 문제가 없었고,</p>
<pre><code class="language-typescript">const { data, error } = await supabase.auth.signUp({
      email: id,
      password: pw,
      options: {
        data: {
          //options안 data 객체 안에 key:value형식으로 넣어주면 
          //raw_user_meta_data에 객체 형태로 저장된다.
          name: name,
          avatar_url: null,        
        }
      }
    });</code></pre>
<p>구글과 카카오 소셜 로그인 시에도 <code>raw_user_meta_data</code>에 해당 이름에 맞는 key가 존재하므로 문제가 없었다.</p>
<br>

<h2 id="💢문제-발생💢">💢문제 발생💢</h2>
<p>회원가입 시 전화번호를 입력받고 본인인증을 하면서 문제가 발생했다.
입력받은 전화번호를 아이디/비밀번호 찾기, 결제페이지에서 사용해야하므로 다른 정보와 동일하게 <code>raw_user_meta_data</code>에 저장하고 <code>handle_new_user()</code>를 수정해줬다.
<span style="font-size: 15px; color: darkgray">(물론 userinfo 테이블에 phone column도 추가했다)</span>
이렇게</p>
<pre><code class="language-typescript">const { data, error } = await supabase.auth.signUp({
      email: id,
      password: pw,
      options: {
        data: {
          name: name,
          avatar_url: null,
          phone: phone,
        }
      }
    });</code></pre>
<pre><code class="language-sql">--handle_new_user() 함수
begin
insert into public.userInfo(id, email, username, avatar_url, phone)
values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;);
return new;
end;</code></pre>
<p>수정한 후 일반 회원가입 시 잘 작동하는 것을 확인하고, 문제가 없는 줄 알았으나,,</p>
<h3 id="카카오-로그인--구글-로그인-불가">카카오 로그인 &amp; 구글 로그인 불가</h3>
<p>카카오 신규 로그인과 구글 신규 로그인 시 에러가 발생하는 것을 발견했다.
배포 이후 발견한 에러라 배포와 관련된 설정이 잘못되어 발생하는 오류인지 알고 이리저리 supabase나 kakao developers, google cloud console에서 url을 수정해보았지만 고쳐지지 않았다.
그러다가 trigger가 정상 작동하지 않으면 auth에 새로 등록이 안된다는 것이 생각났다.
<span style="font-size: 15px; color: darkgray">(지난 프로젝트에서 trigger function 작성할 때 column이름에 오타가 있어서 회원가입이 정상 작동하지 않았었다.)</span>
구글과 카카오로 로그인했을 때 <code>raw_user_meta_data</code>에 전화번호 정보가 없어서 (정확히는 key가 &#39;phone&#39;인 값이 없다. 물론 가져올 수 있는 전화번호도 없다.) 에러가 발생하는 것이었다.</p>
<p>그래서 생각한 해결책은
<strong>1. 조건부로 trigger를 실행한다</strong>
<strong>2. function 내에서 분기처리한다.</strong>
였는데 일단 2번으로 해보기로 했다.</p>
<br>

<h2 id="function-조건문-처리하기">function 조건문 처리하기</h2>
<pre><code class="language-sql">begin
  if new.raw_app_meta_data-&gt;&gt;&#39;provider&#39; = &#39;email&#39; then
    insert into public.userInfo(id, email, username, avatar_url, phone)
    values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;, new.raw_user_meta_data-&gt;&gt;&#39;phone&#39;);

  elsif new.raw_app_meta_data-&gt;&gt;&#39;provider&#39; = &#39;kakao&#39; then
    insert into public.userInfo(id, email, username, avatar_url, phone)
    values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;, &#39;kakao 유저&#39;);

  elsif new.raw_app_meta_data-&gt;&gt;&#39;provider&#39; = &#39;google&#39; then
    insert into public.userInfo(id, email, username, avatar_url, phone)
    values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;, &#39;google 유저&#39;);
  end if;

  return new;</code></pre>
<p>회원 가입 유형에 따라<code>raw_app_meta_data</code>에 <code>provider</code>값이 있다는 것을 이용해 조건문 처리를 하였다.
기본 이메일 회원가입시에는 기존과 동일하게 처리하고 카카오와 구글 로그인 시에는 <code>phone</code> column에 각각 &#39;kakao 유저&#39;, &#39;google 유저&#39;라고 string을 넣어주었다. (phone은 text type으로 되어있다.)</p>
<p><code>elsif</code>라는 키워드는 생각도 못했다. <code>elif</code>일거라고 생각했는데 ㄷㄷ</p>
<p>무튼 잘 해결해서 다행이다!@!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아이디 찾기 구현하기]]></title>
            <link>https://velog.io/@_soul_/%EC%95%84%EC%9D%B4%EB%94%94-%EC%B0%BE%EA%B8%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/%EC%95%84%EC%9D%B4%EB%94%94-%EC%B0%BE%EA%B8%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jan 2024 15:03:06 GMT</pubDate>
            <description><![CDATA[<p>supabase로 구현한 회원가입/로그인 기능에 아이디 찾기를 추가해보았다.
<img src="https://velog.velcdn.com/images/_soul_/post/4627d784-f2f7-43f2-aef4-ae85a227e0c9/image.png" alt="">
<img src="https://velog.velcdn.com/images/_soul_/post/badc43ac-cf77-4325-bba4-42642912d04d/image.png" alt="">
<img src="https://velog.velcdn.com/images/_soul_/post/7256a5f7-049e-41ec-8159-004b92250a64/image.png" alt=""></p>
<p>로그인 페이지와 별도로 페이지를 만들었다</p>
<pre><code class="language-typescript">//src&gt;app&gt;auth&gt;find
&quot;use client&quot;;
import React, { useState } from &quot;react&quot;;
import FindID from &quot;./FindID&quot;;
import FindPW from &quot;./FindPW&quot;;

const AuthFindPage = () =&gt; {
  const [mode, setMode] = useState&lt;boolean&gt;(true);
  return &lt;&gt;{mode ? &lt;FindID setMode={setMode} /&gt; : &lt;FindPW setMode={setMode} /&gt;}&lt;/&gt;;
};

export default AuthFindPage;</code></pre>
<p>FindId 컴포넌트가 아이디를 찾는 부분이다. 비밀번호 재설정도 추가할 예정.</p>
<pre><code class="language-typescript">//&lt;FindId&gt;
import React, { useState } from &quot;react&quot;;
import PageBreadCrumb from &quot;@/components/layout/PageBreadCrumb&quot;;
import { useForm, SubmitHandler } from &quot;react-hook-form&quot;;
import { supabase } from &quot;@/service/supabase&quot;;
import ResultID from &quot;./ResultID&quot;;

interface Props {
  setMode: React.Dispatch&lt;React.SetStateAction&lt;boolean&gt;&gt;;
}

interface FormValue {
  name: string;
  phone: string;
}

const linkList = [
...
];

const FindID = ({ setMode }: Props) =&gt; {
  //찾기
  const [find, setFind] = useState(false);
  //찾기 결과
  const [result, setResult] = useState&lt;boolean | string&gt;(false);

  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm&lt;FormValue&gt;({ mode: &quot;onBlur&quot; });

  const onSubmit: SubmitHandler&lt;FormValue&gt; = async (inputData) =&gt; {
    //아이디 찾기 버튼 눌렀을 때
    setFind(true);
    const { data, error } = await supabase
      .from(&quot;userinfo&quot;)
      .select(&quot;email&quot;)
      .match({ username: inputData.name, phone: inputData.phone });
    if (error) {
      window.confirm(&quot;아이디 찾기 실패&quot;);
    } else {
      setResult(data[0]?.email || false);
    }
  };
  return (
    &lt;&gt;
      &lt;PageBreadCrumb linkList={linkList} /&gt;
      &lt;div className=&quot;flex justify-center items-center space-x-[40px] text-center text-[25px] font-semibold mb-[80px] &quot;&gt;
        &lt;h1&gt;아이디 찾기&lt;/h1&gt;
        &lt;p onClick={() =&gt; setMode(false)} className=&quot;text-tc-light cursor-pointer&quot;&gt;
          비밀번호 찾기
        &lt;/p&gt;
      &lt;/div&gt;

      &lt;div className=&quot;flex flex-col justify-center items-center &quot;&gt;
        &lt;div className=&quot;w-[345px]&quot;&gt;
          {!find ? (
            &lt;form onSubmit={handleSubmit(onSubmit)}&gt;
              &lt;div className=&quot;mb-[15px]&quot;&gt;
                &lt;label htmlFor=&quot;email&quot; className=&quot;block mb-2&quot;&gt;
                  Username
                  &lt;span className=&quot;inline-block ml-2 text-[12px] text-red-600&quot;&gt;{errors?.name?.message}&lt;/span&gt;
                &lt;/label&gt;
                &lt;input
                  id=&quot;name&quot;
                  type=&quot;text&quot;
                  {...register(&quot;name&quot;, {
                    required: &quot;이름을 입력하세요.&quot;
                  })}
                  placeholder=&quot;이름&quot;
                  className=&quot;block w-full h-[50px] border p-[15px]&quot;
                /&gt;
              &lt;/div&gt;

              &lt;div className=&quot;mb-[20px]&quot;&gt;
                &lt;label htmlFor=&quot;phone&quot; className=&quot;block mb-2&quot;&gt;
                  Phone Number
                  &lt;span className=&quot;inline-block ml-2 text-[12px] text-red-600&quot;&gt;{errors?.phone?.message}&lt;/span&gt;
                &lt;/label&gt;
                &lt;div className=&quot;grid grid-cols-5 w-full  border&quot;&gt;
                  &lt;input
                    id=&quot;phone&quot;
                    type=&quot;phone&quot;
                    placeholder=&quot;휴대폰번호&quot;
                    {...register(&quot;phone&quot;, {
                      required: &quot;휴대폰번호를 입력하세요&quot;
                    })}
                    className=&quot;block col-span-4 h-[50px] p-[15px]&quot;
                  /&gt;
                  &lt;button className=&quot;border-l h-[50px] text-tc-middle font-normal&quot;&gt;인증하기&lt;/button&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;button type=&quot;submit&quot; className=&quot;h-[50px] rounded-[5px] w-[100%] bg-point text-white&quot;&gt;
                아이디 찾기
              &lt;/button&gt;
            &lt;/form&gt;
          ) : (
            &lt;ResultID result={result} setFind={setFind} setMode={setMode} /&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default FindID;</code></pre>
<p>이름과 휴대폰번호를 입력받고 supabase userinfo 테이블에서 두 값을 같은 행의 id를 찾는다.</p>
<blockquote>
<pre><code class="language-typescript"> //아이디 찾기 버튼 눌렀을 때
    setFind(true);
    const { data, error } = await supabase
      .from(&quot;userinfo&quot;)
      .select(&quot;email&quot;)
      .match({ username: inputData.name, phone: inputData.phone });
    if (error) {
      window.confirm(&quot;아이디 찾기 실패&quot;);
    } else {
      setResult(data[0]?.email || false);
    }</code></pre>
</blockquote>
<pre><code>
검색 결과에 맞춰 화면에 출력해준다.

```typescript
//&lt;ResultID&gt;
import React from &quot;react&quot;;
import { useRouter } from &quot;next/navigation&quot;;

interface Props {
  result: string | boolean;
  setFind: React.Dispatch&lt;React.SetStateAction&lt;boolean&gt;&gt;;
  setMode: React.Dispatch&lt;React.SetStateAction&lt;boolean&gt;&gt;;
}

const ResultID = ({ result, setFind, setMode }: Props) =&gt; {
  const router = useRouter();

  return (
    &lt;&gt;
      {result ? (
        &lt;div&gt;
          &lt;p className=&quot;mb-[40px] text-center text-[18px] font-semibold&quot;&gt;아이디 찾기가 완료되었습니다.&lt;/p&gt;

          &lt;p className=&quot;px-[15px] py-[25px] mb-[20px] text-[16px] font-normal border&quot;&gt;{result}&lt;/p&gt;

          &lt;div className=&quot;flex justify-center space-x-[9px]&quot;&gt;
            &lt;button
              onClick={() =&gt; router.push(&quot;/auth&quot;)}
              className=&quot;w-[168px] h-[50px] rounded-[5px] bg-point text-white&quot;
            &gt;
              로그인
            &lt;/button&gt;
            &lt;button
              onClick={() =&gt; setMode(false)}
              className=&quot;w-[168px] h-[50px] rounded-[5px] text-point border border-point&quot;
            &gt;
              비밀번호 찾기
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      ) : (
        &lt;div&gt;
          &lt;p className=&quot;mb-[40px] text-center text-[18px] font-semibold&quot;&gt;고객님 명의로 찾은 아이디가 없습니다.&lt;/p&gt;
          &lt;div className=&quot;flex justify-center space-x-[9px]&quot;&gt;
            &lt;button onClick={() =&gt; setFind(false)} className=&quot;w-[168px] h-[50px] rounded-[5px] bg-point text-white&quot;&gt;
              다시 찾기
            &lt;/button&gt;
            &lt;button
              onClick={() =&gt; router.push(&quot;/&quot;)}
              className=&quot;w-[168px] h-[50px] rounded-[5px] text-point border border-point&quot;
            &gt;
              홈으로 이동
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/&gt;
  );
};

export default ResultID;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 데이터 생성 및 결제페이지 css]]></title>
            <link>https://velog.io/@_soul_/TIL-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EA%B2%B0%EC%A0%9C%ED%8E%98%EC%9D%B4%EC%A7%80-css</link>
            <guid>https://velog.io/@_soul_/TIL-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EA%B2%B0%EC%A0%9C%ED%8E%98%EC%9D%B4%EC%A7%80-css</guid>
            <pubDate>Wed, 17 Jan 2024 14:27:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1-지점-데이터-및-상품-데이터-생성">1. 지점 데이터 및 상품 데이터 생성</h2>
<p>admin 페이지에서 직접 데이터를 입력해 supabase의 store 테이블과 product 테이블에 넣어주었다.</p>
<p><img src="https://velog.velcdn.com/images/_soul_/post/6b007761-594f-4698-b8d5-16f974b5d1ce/image.png" alt="">
<img src="https://velog.velcdn.com/images/_soul_/post/35c0d52b-c848-4c72-99db-bb01d990222c/image.png" alt="">
<img src="https://velog.velcdn.com/images/_soul_/post/4c380864-259c-428d-8be3-fbf624fbf743/image.png" alt="">
<img src="https://velog.velcdn.com/images/_soul_/post/86153d66-5e65-463a-9c1e-1c7b2239a3e5/image.png" alt="">
tada~!🧞‍♀️</p>
<p><br><br></p>
<h2 id="2-cart-컴포넌트-수정">2. cart 컴포넌트 수정</h2>
<p>장바구니 페이지와 결제 페이지에서 사용되는 컴포넌트를 수정했다. 할인가격을 적용했다.</p>
<p><br><br></p>
<h2 id="3-결제-페이지-만들기">3. 결제 페이지 만들기</h2>
<p>장바구니에 있는 데이터와 유저 데이터를 불러오고 페이지 css를 진행했다.
내일은 결제하기 버튼을 눌렀을 때 과정을 구현해야한다.
<img src="https://velog.velcdn.com/images/_soul_/post/cdc54454-179c-46ae-8977-eb36c2828bbf/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 장바구니 구현하기 - 총 결제금액]]></title>
            <link>https://velog.io/@_soul_/TIL-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%B4%9D-%EA%B2%B0%EC%A0%9C%EA%B8%88%EC%95%A1</link>
            <guid>https://velog.io/@_soul_/TIL-%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%B4%9D-%EA%B2%B0%EC%A0%9C%EA%B8%88%EC%95%A1</guid>
            <pubDate>Tue, 16 Jan 2024 14:32:02 GMT</pubDate>
            <description><![CDATA[<p><del>어제 하루종일 삽질한 문제 해결</del></p>
<p><img src="https://velog.velcdn.com/images/_soul_/post/bea7ee2e-fdd9-4a8c-b312-9053bbf31552/image.png" alt="">
장바구니에서 총 결제 금액이 문제였다,, 카트 컴포넌트에서 상품 수량이 변경되면 거기에 맞게 바로바로 업데이트 됐으면 좋겠는데!!</p>
<h3 id="문제점">문제점</h3>
<p>처음에 생각한 로직은 페이지 처음 로드할 때 db에서 불러온 초기값을 가지고 총 결제금액을 계산한 후, 각 컴포넌트에서 수량이 변경되었을 때에 맞춰 더하고 빼주려고 했다.
react-hook-form을 이용해 만든 number input 컴포넌트에서 변경 이전 값을 가져올 방법이 없다..!!</p>
<p><br><br></p>
<h3 id="해결방법">해결방법</h3>
<p>페이지에서 아이템마다 총 금액을 담는 배열을 만들어서 각 컴포넌트에서 업데이트하고, 페이지에서는 배열에 있는 금액의 총합을 이용하도록 했다.</p>
<pre><code class="language-typescript">&quot;use client&quot;;
import React, { useEffect, useState } from &quot;react&quot;;
import Section from &quot;@/components/layout/Section&quot;;
import CartItem from &quot;./CartItem&quot;;
import { useCart } from &quot;@/hooks&quot;;
import Link from &quot;next/link&quot;;
import PageBreadCrumb from &quot;@/components/layout/PageBreadCrumb&quot;;
    ...
const Page = () =&gt; {
    ...

  const { cart, isLoading } = useCart({ userId: auth, cartId: &quot;&quot; });

  const [cartPrice, setCartPrice] = useState&lt;number[]&gt;([]);
    //카트 아이템당 총 금액을 담는 배열
  return (
    &lt;&gt;
      &lt;PageBreadCrumb linkList={linkList} /&gt;
      &lt;Section title={&quot;장바구니&quot;} isCenter={true}&gt;
        {logedIn ? (
                     ...

                  {(cart as CartBox[]).map((cartItem, idx) =&gt; {
                    return (
                      cartItem &amp;&amp; (
                        &lt;CartItem
                          cart={cartItem}
                          cartPrice={cartPrice}
                          setCartPrice={setCartPrice}
                          idx={idx}
                          key={cartItem.id}
                        /&gt;
                      )
                    );
                  })}

                  ...

                    &lt;div className=&quot;text-right text-[16px] font-bold mr-1&quot;&gt;
                      총 결제금액
                      &lt;p className=&quot;text-[24px] font-bold inline-block ml-1&quot;&gt;
                        {&quot; &quot;}
                        {cartPrice.reduce((acc, num) =&gt; acc + num, 0)}원
                           //총 결제금액은 배열의 총합으로 구하기
                        ...</code></pre>
<pre><code class="language-typescript">import React, { useEffect, useState } from &quot;react&quot;;
import Image from &quot;next/image&quot;;
import { CartBox } from &quot;./page&quot;;
import { VscChromeClose } from &quot;react-icons/vsc&quot;;
import { useCart } from &quot;@/hooks&quot;;
import { useForm } from &quot;react-hook-form&quot;;
import NumberInput from &quot;@/components/NumberInput&quot;;

...

const CartItem = (cart: Props) =&gt; {
  const { count, id, product_id, store_id, user_id, rent_date, store, product } = cart.cart;
  const { name, thumbnail, price, percentage_off, category } = product;
  const { cartPrice, setCartPrice, idx } = cart;

  const [isVisible, setIsVisible] = useState(true);

  const { updateCountMutation, deleteCart } = useCart({ userId: user_id, cartId: id });

  const {
    register,
    setValue,
    getValues,
    watch,
    formState: { errors }
  } = useForm({ mode: &quot;onChange&quot; });
  const watchCount = watch();

  useEffect(() =&gt; {
    const updateCount = async () =&gt; {
      if (isVisible) {
        //cartPrice 배열에 총액
        cartPrice[idx] = getValues(&quot;count&quot;) * price;
        setCartPrice([...cartPrice, 0]);
        updateCountMutation.mutate(watchCount.count);
      } else {
        //삭제했을 때
        cartPrice[idx] = 0;
        setCartPrice([...cartPrice, 0]);
      }
    };
    updateCount();
  }, [watchCount.count, isVisible]);

  const handleCartDelete = () =&gt; {
    setIsVisible(false);
    deleteCart(id);
  };

  return (
    &lt;&gt;
     ...
    &lt;/&gt;
  );
};

export default CartItem;
</code></pre>
<p><img src="https://velog.velcdn.com/images/_soul_/post/247edf84-f358-4091-a514-014d5108f955/image.png" alt=""></p>
<p>짜잔
수량이 바뀌어도, 삭제해도 잘 작동한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA['intrinsicAtrributes&...형식에서 선언된..'에러 해결하기]]></title>
            <link>https://velog.io/@_soul_/intrinsicAtrributes...%ED%98%95%EC%8B%9D%EC%97%90%EC%84%9C-%EC%84%A0%EC%96%B8%EB%90%9C..%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/intrinsicAtrributes...%ED%98%95%EC%8B%9D%EC%97%90%EC%84%9C-%EC%84%A0%EC%96%B8%EB%90%9C..%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 14 Jan 2024 23:46:22 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@_soul_/%EC%97%90%EB%9F%AC%EA%B0%80-%EB%82%98%EB%8A%94%EB%8D%B0-%EC%8B%A4%ED%96%89%EC%9D%B4-%EB%90%9C%EB%8B%A4">여기서 계속</a>
<del>생각보다 간단히 해결되는 문제였다..</del></p>
<p>cartItem 상단에서 props로 받아온 cart값을 콘솔에 찍어봤을 때 값이 <code>{cart: {...}}</code>형태로 받아오는 것을 확인했다.</p>
<p>그래서</p>
<pre><code class="language-typescript">import React, { useEffect, useState } from &quot;react&quot;;
import { supabase } from &quot;@/service/supabase&quot;;
import Image from &quot;next/image&quot;;
import { CartBox } from &quot;./page&quot;;
import { VscChromeClose } from &quot;react-icons/vsc&quot;;
import { useCart } from &quot;@/hooks&quot;;

interface Props {
  cart: CartBox;
}

interface Input {
  count: number;
}

const CartItem = (cart: Props) =&gt; {
  const { count, id, product_id, store_id, user_id, rent_date, store, product } = cart.cart;
  const { name, thumbnail, price, percentage_off, category } = product;
  // console.log(product);

  const [isVisible, setIsVisible] = useState(true);

  const { updateCountMutation, deleteCart } = useCart({ userId: user_id, cartId: id });
  ...</code></pre>
<p>CartItem이 받아오는 props 타입을 <code>cart:{...}</code>에 맞게 <code>cart: CartBox</code>로 고쳐줬다.</p>
<p><br><br></p>
<p>에러 해결 끝<del>!</del>!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🥵에러가 나는데 실행이 된다?!🥵]]></title>
            <link>https://velog.io/@_soul_/%EC%97%90%EB%9F%AC%EA%B0%80-%EB%82%98%EB%8A%94%EB%8D%B0-%EC%8B%A4%ED%96%89%EC%9D%B4-%EB%90%9C%EB%8B%A4</link>
            <guid>https://velog.io/@_soul_/%EC%97%90%EB%9F%AC%EA%B0%80-%EB%82%98%EB%8A%94%EB%8D%B0-%EC%8B%A4%ED%96%89%EC%9D%B4-%EB%90%9C%EB%8B%A4</guid>
            <pubDate>Thu, 11 Jan 2024 14:43:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>_<span style="font-size: 12px">나를 환장하게 하는 <span style="color:red">빨간줄</span></span>_</p>
</blockquote>
<p>오늘 뭔가 영차영차해서 장바구니 컴포넌트를 만들려고 했다. <del>(오늘 완성하고 싶었는데)</del></p>
<p><em>와 TIL쓰려고 코드 복사하면서 잉?이거 왜 이렇게 생겼냐? 싶은걸 고쳤더니 에러가 6개에서 2개로 줄었다..!!</em></p>
<p>그래서 오늘 무엇을 했는가</p>
<h2 id="1-장바구니-api-route-작성">1. 장바구니 api route 작성</h2>
<pre><code class="language-typescript">import { supabase } from &quot;@/service/supabase&quot;;
import { NextResponse, NextRequest } from &quot;next/server&quot;;

export const GET = async (res: NextResponse, context: { params: { userId: string } }) =&gt; {
  const {
    params: { userId }
  } = context;
  let { data: cart, error } = await supabase
    .from(&quot;cart&quot;)
    .select(`*, store(name), product(name, thumbnail, category(category_name), price, percentage_off)`)
    .eq(&quot;user_id&quot;, userId);
  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
  return NextResponse.json(cart);
};</code></pre>
<p><a href="http://localhost:3000/api/cart/%60userId%60%EB%A5%BC">http://localhost:3000/api/cart/`userId`를</a> 입력하면 해당 유저가 장바구니에 담은 아이템 데이터를 잘 가져오는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/_soul_/post/57ab9f90-8a5a-4599-a764-6992a18efbad/image.png" alt=""></p>
<h2 id="2-장바구니-데이터를-fetch-해올-usecartts-작성">2. 장바구니 데이터를 fetch 해올 useCart.ts 작성</h2>
<pre><code class="language-typescript">import { useQuery } from &quot;@tanstack/react-query&quot;;

interface CartBox {
  cart: {
    count: number | null;
    id: string;
    product_id: string | null;
    store_id: string | null;
    user_id: string;
    product: {
      name: string;
      thumbnail: string;
      price: number;
      percentage_off: number;
      category: {
        category_name: string;
      };
    };
    store: {
      name: string;
    };
  };
}

const useCart = (id: string) =&gt; {
  const {
    data: cart,
    isLoading,
    isError
  } = useQuery&lt;CartBox[]&gt;({
    queryFn: async (): Promise&lt;CartBox[]&gt; =&gt; {
      const response = await fetch(`/api/cart/${id}`, { method: &quot;GET&quot; });
      const data = await response.json();
      return data;
    },
    queryKey: [&quot;cart&quot;]
  });
  // console.log(cart);
  return { cart, isLoading };
};

export default useCart;</code></pre>
<h2 id="3-cart-페이지-만들기">3. Cart 페이지 만들기</h2>
<p><img src="https://velog.velcdn.com/images/_soul_/post/343f45a9-56fb-45bd-83c4-85cc2debe207/image.png" alt="">
요 녀석이 결제 페이지에서 동일하게 들어가서 컴포넌트로 분리하기로 했는데 일단,, 일단 컴포넌트로 분리하지 않고,,</p>
<pre><code class="language-typescript">&quot;use client&quot;;
import React, { useState } from &quot;react&quot;;
import Image from &quot;next/image&quot;;
// import CartItem from &quot;./CartItem&quot;;  &lt;- 컴포넌트로 만드려고 했던 흔적...

import useCart from &quot;@/hooks/useCart&quot;;

interface CartBox {
  cart: {
    count: number | null;
    id: string;
    product_id: string | null;
    store_id: string | null;
    user_id: string;
    product: {
      name: string;
      thumbnail: string;
      price: number;
      percentage_off: number;
      category: {
        category_name: string;
      };
    };
    store: {
      name: string;
    };
  };
}

// const CartItem = ({ cart, product, store }: Props) =&gt; {
const CartItem = (cart: CartBox) =&gt; {
  const { count, id, product_id, store_id, user_id, store, product } = cart.cart;
  const { name: product_name, thumbnail, price, percentage_off, category } = product;
  console.log(price);

  //수정필
  const [cnt, setCnt] = useState(count || 0);
  return (
    &lt;&gt;
      &lt;div className=&quot;border border-gray w-[60%] p-2&quot;&gt;
        &lt;div className=&quot;flex flex-row my-2&quot;&gt;
          &lt;div className=&quot;mx-5 &quot;&gt;
            &lt;Image src={thumbnail} width={160} height={120} alt=&quot;상품대표이미지&quot; /&gt;
          &lt;/div&gt;
          &lt;div className=&quot;mx-3&quot;&gt;
            &lt;p&gt;{category.category_name}&lt;/p&gt;
            &lt;p&gt;{store.name}&lt;/p&gt;
            &lt;p&gt;날짜&lt;/p&gt;
            &lt;p&gt;{price}원&lt;/p&gt;
            &lt;div className=&quot;flex flex-row&quot;&gt;
              &lt;button onClick={() =&gt; setCnt(cnt - 1)} className=&quot;bg-gray-300  w-[25px] h-[25px]&quot;&gt;
                -
              &lt;/button&gt;

              &lt;p className=&quot;w-[25px] h-[25px] text-center border border-gray-300&quot;&gt;{cnt}&lt;/p&gt;
              &lt;button onClick={() =&gt; setCnt(cnt + 1)} className=&quot;bg-gray-300  w-[25px] h-[25px]&quot;&gt;
                +
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className=&quot;flex flex-row border-t&quot;&gt;
          &lt;p&gt;상품금액{price}원&lt;/p&gt;
          &lt;p&gt;수량 {cnt}개&lt;/p&gt;
          &lt;p&gt;총금액{cnt * price}원&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

const page = () =&gt; {
  //useCart에 사용자 id
  const { cart, isLoading } = useCart(&quot;aba26c49-82c0-42b2-913c-c7676527b553&quot;);
  // console.log(cart);
  return (
    &lt;&gt;
      {!isLoading ? (
        &lt;div&gt;
          {(cart as CartBox[]).map((cartItem) =&gt; {
            console.log(&quot;cartItem:&quot;, cartItem);
            return cartItem &amp;&amp; &lt;CartItem cart={cartItem} key={cartItem.id} /&gt;;        
            //↑여기서 에러가 2개난다. 나증말 환장해
          })}
        &lt;/div&gt;
      ) : (
        &lt;div&gt;Loading...&lt;/div&gt;
      )}
    &lt;/&gt;
  );
};

export default page;
</code></pre>
<p>어떤 에러 2개가 나냐~~
<img src="https://velog.velcdn.com/images/_soul_/post/beba1e07-1ff3-4c10-9dc4-50be68bfd507/image.png" alt="">
첫번째 에러는 잘 모르겠지만 두번째 에러는 정말 모르겠다. 콘솔에 key값 없다고 warning도 안뜨고 cartItem을 콘솔에 찍어봤을 때 분명히 id값이 있는데 왜,, 왜 없다고 하니,,
<img src="https://velog.velcdn.com/images/_soul_/post/b668a7a2-b1e9-4e5d-a206-9356acfdfb79/image.png" alt=""></p>
<p>실행은 멀쩡하게 잘 된다. (불길해보이는 warning은 사진 사이즈 때문이다. 나중에 수정해야디)
기계친구한테 물어봐도 시원한 해결책을 얻지 못했다.</p>
<p>내일 점심 먹기 전에 꼭 해결해야지. 그래서 저녁 먹기전에 컴포넌트 분리하고 cart 페이지 틀까지 잡는게 목표!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] DB에서 읽어온 정보가 들어있는 헤더 만들기]]></title>
            <link>https://velog.io/@_soul_/TIL-DB%EC%97%90%EC%84%9C-%EC%9D%BD%EC%96%B4%EC%98%A8-%EC%A0%95%EB%B3%B4%EA%B0%80-%EB%93%A4%EC%96%B4%EC%9E%88%EB%8A%94-%ED%97%A4%EB%8D%94-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/TIL-DB%EC%97%90%EC%84%9C-%EC%9D%BD%EC%96%B4%EC%98%A8-%EC%A0%95%EB%B3%B4%EA%B0%80-%EB%93%A4%EC%96%B4%EC%9E%88%EB%8A%94-%ED%97%A4%EB%8D%94-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 10 Jan 2024 15:09:43 GMT</pubDate>
            <description><![CDATA[<p><span style="font-size: 12px">오늘을 모든 페이지에 공동으로 들어가는 헤더를 만들었습니다. 작업 속도가 거북이보다 느렸던게 아주 큰 흠이지만, 이전 프로젝트에서 정확히 이해하지 못하고 넘어갔던 부분을 조금씩 알아가는 느낌이 들어 다행이라는 생각이 듭니다. 오늘 공부한 부분을 내일 개발할 부분에 적용할 수 있겠죠?! <br/>
이번 프로젝트는 지금까지와는 다르게 개발기간도 길고, 디자이너님도 함께합니다. 그래서 우리 팀이 아주아주 정성으로 작성한 와이어프레임을 가지고 멋지게 작업해주고 계십니다. 어제 메인페이지 와이어프레임 디자이너.ver을 받아볼 수 있었는데요. 고것을 가지고 헤더를 만들어보았습니다. </span></p>
<br>

<h2 id="🎨디자이너님의-믓찐-와이어프레임">🎨디자이너님의 믓찐 와이어프레임</h2>
<p>중 일부입니다.
<img src="https://velog.velcdn.com/images/_soul_/post/c86d44c6-17fa-41a8-9b7a-a86a18fc40f1/image.png" alt="">
깔끔하고 멋지죠?!</p>
<h2 id="💻구현한-헤더-프로토타입">💻구현한 헤더 (프로토타입)</h2>
<p><img src="https://velog.velcdn.com/images/_soul_/post/cf0610f8-f174-4e19-891f-f5ce5cd630ce/image.png" alt="">
<span style="font-size: 12px">footer 부분은 팀원분이 만드셨습니닷</span></p>
<p>좌상단에서부터 보면</p>
<h3 id="✔-로고">✔ 로고</h3>
<p>사이트 로고가 들어갈 부분은 일단 text로 처리했습니다. 클릭하면 메인페이지로 링크됩니다.</p>
<h3 id="✔-카테고리">✔ 카테고리</h3>
<p>아직 색상팔레트가 정확히 정해지지 않은 것 같아 검은색으로 남겨두었습니다.
카테고리를 클릭하면 상품 카테고리 드롭다운이 나옵니다.
<img src="https://velog.velcdn.com/images/_soul_/post/e81c3a34-e773-4c63-9a01-c123971ee8ad/image.png" alt="">
이렇게. <span style="font-size: 12px">(css는 일단 흐린눈을,,👀)</span>
그리고 해당 카테고리를 클릭하면 카테고리 페이지로 링크되도록 했습니다.</p>
<h3 id="✔-검색창">✔ 검색창</h3>
<p>검색창은 input만 있습니다. 나중에 검색기능을 완성되면 input 처리와 검색 결과 처리를 추가할 계획입니다.</p>
<h3 id="✔-사람-아이콘">✔ 사람 아이콘</h3>
<p>이 부분은 로그인 여부에 따라 달라지는데 로그인하지 않았다면 클릭했을 때 로그인/회원 가입 페이지로 연결됩니다.
<img src="https://velog.velcdn.com/images/_soul_/post/2cba505a-c33a-42ce-ae45-57b836947a00/image.png" alt=""></p>
<p>만약 로그인 한 상태라면
<img src="https://velog.velcdn.com/images/_soul_/post/fe7ee873-f5b2-47e2-86bc-5ae3be53a4fe/image.png" alt="">
클릭했을 때 드롭다운이 나옵니다. 마이페이지를 클릭하면 user 페이지로 이동하고 로그아웃 버튼을 누르면 로그아웃하며 홈으로 이동합니다.</p>
<p>로그인 상태는 zustand를 이용해 전역상태 관리합니다.</p>
<h3 id="✔-장바구니-아이콘">✔ 장바구니 아이콘</h3>
<p>장바구니 아이콘을 클릭하면 장바구니 페이지로 링크됩니다.
로그인 하지 않았을 때는 보이지 않도록 해야하는데 방금 생각났습니다. 수정해야겠네요.</p>
<p>수정하려다가 다시 생각해보니 로그인 상태는 zustand를 이용해 관리하는데 header에는 use client가 없습니다. header를 그냥 csr으로 관리할 지 팀원들과 상의해봐야겠네요.</p>
<br>

<p>++ 모든 드롭다운은 영역밖을 클릭하면 닫히도록 만들었습니다.</p>
<p><br><br></p>
<h2 id="📂-db에서-카테고리-목록-가져오기">📂 DB에서 카테고리 목록 가져오기</h2>
<p>카테고리 목록은 supabase public schema에 category라는 이름의 table로 저장되어있습니다. 물론 헤더의 카테고리 목록 부분을 하드코딩해도 되지만 휴먼 에러가 발생할 수 있고, 추후 카테고리의 확장성을 고려했을 때 db에서 불러오기로 했습니다.
<img src="https://velog.velcdn.com/images/_soul_/post/612cd74d-b6a1-4142-b769-89a9d58af0c4/image.png" alt=""></p>
<h4 id="1-srcappapi-폴더에-category폴더-만들고-routets-만들기">1. src&gt;app&gt;api 폴더에 category폴더 만들고 route.ts 만들기</h4>
<pre><code class="language-typescript">import { supabase } from &quot;@/service/supabase&quot;;
import { NextRequest, NextResponse } from &quot;next/server&quot;;

export const GET = async () =&gt; {
  let { data: category, error } = await supabase.from(&quot;category&quot;).select(&quot;*&quot;);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
  return NextResponse.json(category);
};</code></pre>
<h4 id="2-srchooks에-usecategorytsx-만들기">2. src&gt;hooks에 useCategory.tsx 만들기</h4>
<pre><code class="language-typescript">import { CategoryTable } from &quot;@/types/db&quot;;
import { useQuery } from &quot;@tanstack/react-query&quot;;

const useCategory = () =&gt; {
  const {
    data: category,
    isLoading,
    isError
  } = useQuery&lt;CategoryTable[] | CategoryTable&gt;({
    queryFn: async (): Promise&lt;CategoryTable[] | CategoryTable&gt; =&gt; {
      const response = await fetch(`/api/category`, { method: &quot;GET&quot; });
      const data = await response.json();
      return data;
    },

    queryKey: [&quot;category&quot;]
  });

  return { category, isLoading };
};

export default useCategory;
</code></pre>
<h4 id="3-카테고리-컴포넌트-만들기">3. 카테고리 컴포넌트 만들기</h4>
<pre><code class="language-typescript">&quot;use client&quot;;
import React, { useState, useEffect } from &quot;react&quot;;
import Link from &quot;next/link&quot;;

import { RxHamburgerMenu } from &quot;react-icons/rx&quot;;
import useCategory from &quot;@/hooks/useCategory&quot;;
import { CategoryTable } from &quot;@/types/db&quot;;

interface Props {
  category: CategoryTable;
}

const CategoryName = ({ category: { id, category_name } }: Props) =&gt; {
  return (
    &lt;Link href={`/category/${id}`}&gt;
      &lt;li&gt;{category_name}&lt;/li&gt;
    &lt;/Link&gt;
  );
};

const HeadCategory = () =&gt; {
  //카테고리 메뉴 열기
  const [open, setOpen] = useState(false);
  const { category, isLoading } = useCategory();
  //   console.log(category);

  ...

  return (
    &lt;&gt;
      &lt;div className=&quot;flex flex-row space-x-4 cursor-pointer relative&quot; onClick={() =&gt; setOpen(!open)}&gt;
        &lt;RxHamburgerMenu size=&quot;24&quot; /&gt;
        &lt;p&gt;카테고리&lt;/p&gt;
        {open &amp;&amp; !isLoading &amp;&amp; (
          &lt;ul className=&quot;absolute p-1 mt-7 ml-7 text-right w-32 z-50 right-0 bg-white shadow-md cursor-pointer&quot;&gt;
            {(category as CategoryTable[]).map((category) =&gt; (
              &lt;CategoryName category={category} key={category.id} /&gt;
            ))}
          &lt;/ul&gt;
        )}
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default HeadCategory;
</code></pre>
<p>useCategory에서 가져온 데이터에 map함수를 적용하는게 쉽지 않았습니다.
코드가 크게 맘에 들지는 않습니다. 너무 지저분해요.
프로젝트를 진행하면서 시간 여유가 있다면 깔끔하게 정리하고 싶습니다.</p>
<h4 id="4-카테고리-컴포넌트-헤더에-삽입">4. 카테고리 컴포넌트 헤더에 삽입</h4>
<pre><code class="language-typescript">...
const Header = () =&gt; {
  return (
    &lt;header className=&quot;py-2&quot;&gt;
      &lt;div className=&quot;container m-auto flex flex-col max-w-[1200px] min-h-[128px] w-[90%] space-y-4&quot;&gt;
        &lt;div className=&quot;py-4&quot;&gt;
          &lt;Link href={&quot;/&quot;} className=&quot;font-black text-3xl&quot;&gt;
            YOLOCEAN
          &lt;/Link&gt;
        &lt;/div&gt;

        &lt;div className=&quot;flex flex-row justify-between pb-4&quot;&gt;
          &lt;div&gt;
            &lt;HeadCategory /&gt;        //이렇게 삽입해줬습니다.
          &lt;/div&gt;
          &lt;div className=&quot;flex flex-row space-x-6&quot;&gt;
            &lt;div className=&quot;w-72 border border-black rounded-xl&quot;&gt;
              &lt;input type=&quot;text&quot; className=&quot;mx-2 w-56 focus:outline-none&quot; /&gt;
              &lt;AiOutlineSearch className=&quot;inline mr-3 ml-2&quot; size=&quot;20&quot; /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;AuthBtn /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;Link href={&quot;/cart&quot;}&gt;
                &lt;AiOutlineShopping size=&quot;22&quot; /&gt;
              &lt;/Link&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  );
};</code></pre>
<blockquote>
<p><span style="font-size:10px">db에서 데이터를 잘 가져왔던 경험을 가지고 앞으로 프로젝트에 잘 써먹어 보도록 하겠습니다. 오늘도 수고했다!!</span></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Hook Form으로 회원가입/로그인 구현하기]]></title>
            <link>https://velog.io/@_soul_/React-Hook-Form%EC%9C%BC%EB%A1%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/React-Hook-Form%EC%9C%BC%EB%A1%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 09 Jan 2024 15:18:52 GMT</pubDate>
            <description><![CDATA[<h1 id="❓-react-hook-form을-적용한-이유">❓ React-Hook-Form을 적용한 이유</h1>
<span style="line-height: 2">
지금까지는 input 이벤트를 관리할 때 useState & onChange를 이용했다.
이렇게 했을 때 단점은 input 창에 변화가 있을 때마다 리렌더링이 발생한다는 것이다.
입력의 개수가 적을 때는 크게 문제가 되지 않겠지만, 관리해야 하는 state가 많아지고, 입력할 때마다 컴포넌트가 리렌더링하게 되면 불필요한 연산이 발생하고, 유효성 검사 같은 기능이 필요할 경우 코드 양이 많아지고, 유지보수에 어려움이 생길 수 있다.</span>

<h2 id="🌷-react-hook-form의-장점-🌷">🌷 React-Hook-Form의 장점 🌷</h2>
<blockquote>
<p><strong>Performance of React Hook Form</strong>
Performance is one of the primary reasons why this library was created. <br>
<em><strong>&quot;성능이 곧 react-hook-form을 만든 이유&quot;</strong></em></p>
</blockquote>
<h3 id="1-성능-최적화">1. 성능 최적화</h3>
<p>내부적으로 성능 최적화를 고려해 설계되었다. 리렌더링을 최소화 해 불필요한 작업을 방지하고, 빠른 사용자 경험을 제공한다.
<span style="font-size: 24px; font-weight: bold; font-style: italic; color: orange">📌 대규모 폼이나 복잡한 유효성 검사 로직을 가진 폼에서 특히 유용!!</span></p>
<h3 id="2-간편한-api">2. 간편한 API</h3>
<p>사용하기 쉽고 직관적인 API를 제공한다.</p>
<h3 id="3-유효성-검사-용이">3. 유효성 검사 용이</h3>
<p>다양한 유효성 검사 규칙과 커스텀 유효성 검사 규칙을 사용해 폼의 요구사항을 쉽게 처리할 수 있다.</p>
<p><br><br></p>
<h1 id="🎯-react-hook-form-사용하기">🎯 React-Hook-Form 사용하기</h1>
<h3 id="🌊-react-hook-form-없이-구현한-회원가입">🌊 react-hook-form 없이 구현한 회원가입</h3>
<pre><code class="language-typescript">const SignUp = ({ login, setLogin, setOpen }: Props) =&gt; {
  //입력받은 이메일, 패스워드, 닉네임
  const [id, setId] = useState&lt;string&gt;(&quot;&quot;);
  const [pw, setPw] = useState&lt;string&gt;(&quot;&quot;);
  const [pwCheck, setPwCheck] = useState&lt;string&gt;(&quot;&quot;);
  const [name, setName] = useState&lt;string&gt;(&quot;&quot;);

  //유효성 검사에 사용
  const pwValid = pwCheck !== &quot;&quot; &amp;&amp; pw === pwCheck ? true : false;
  const isValid = pwValid &amp;&amp; emailValidChk(id) &amp;&amp; pw.length &gt; 5 &amp;&amp; name !== &quot;&quot; ? false : true;

  ...

  return (
    ...
   &lt;form className=&quot;space-y-6&quot; onSubmit={signUpNewUser}&gt;
            &lt;div&gt;
              &lt;label htmlFor=&quot;email&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                Email address{&quot; &quot;}
                {emailValidChk(id) ? (
                  &lt;span&gt;✅&lt;/span&gt;
                ) : (
                  &lt;span className=&quot;text-xs text-red-600&quot;&gt; 이메일 형식이 유효하지 않습니다.&lt;/span&gt;
                )}
              &lt;/label&gt;
              &lt;div className=&quot;mt-2&quot;&gt;
                &lt;input
                  id=&quot;email&quot;
                  name=&quot;email&quot;
                  type=&quot;email&quot;
                  required
                  className=&quot;block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6&quot;
                  onChange={(e) =&gt; setId(e.target.value)}
                  placeholder=&quot;  example@where2go.com&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            //비밀번호 input 생략

            &lt;div&gt;
              &lt;div className=&quot;flex items-center justify-between&quot;&gt;
                &lt;label htmlFor=&quot;pwConfirm&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                  Password 확인{&quot; &quot;}
                  {!pwValid ? (
                    &lt;span className=&quot;text-xs text-red-600&quot;&gt;비밀번호가 일치하지 않습니다.&lt;/span&gt;
                  ) : (
                    &lt;span&gt;✅&lt;/span&gt;
                  )}
                &lt;/label&gt;
              &lt;/div&gt;
              &lt;div className=&quot;mt-2&quot;&gt;
                &lt;input
                  id=&quot;pwConfirm&quot;
                  name=&quot;pwConfirm&quot;
                  type=&quot;password&quot;
                  required
                  className=&quot;block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6&quot;
                  onChange={(e) =&gt; setPwCheck(e.target.value)}
                  placeholder=&quot;  비밀번호 재입력&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            // username 입력 생략

            &lt;div&gt;
              &lt;Button type=&quot;submit&quot; size=&quot;md&quot; disabled={isValid} className=&quot;sm:mx-auto sm:w-full sm:max-w-sm mb-8 mt-2&quot;&gt;
                SignUp &amp; Login
              &lt;/Button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
            ...
  );
</code></pre>
<p><span style="line-height: 2">  react-hook-form 없이 만든 회원가입은 input마다 useState로 처리하고, 유효성 검사도 로직을 각각 만들어 에러 메세지 처리를 해야한다. onChange로 state를 변경할 때마다 렌더링 되므로 불필요한 렌더링이 너무 많고, 유효성 검사가 복잡하다는 단점이 한눈에 보인다. </span></p>
<h2 id="✨-react-hook-form으로-리팩토링">✨ React-hook-form으로 리팩토링</h2>
<h3 id="⚙-react-hook-form-install">⚙ React-hook-form install</h3>
<p><code>npm install react-hook-form</code></p>
<pre><code class="language-typescript">...
import { useForm, SubmitHandler } from &quot;react-hook-form&quot;;
...

interface FormValue {
  id: string;
  pw: string;
  pwCheck: string;
  name: string;
}

const SignUp = ({ mode, setMode }: Props) =&gt; {
  //회원가입 함수 signUpNewUser() 생략

  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
    setError
  } = useForm&lt;FormValue&gt;({ mode: &quot;onBlur&quot; });

  //비밀번호 유효성 검사를 위해 pw 입력값 확인
  const passwordRef = useRef&lt;string | null&gt;(null);
  passwordRef.current = watch(&quot;pw&quot;);

  const onSubmit: SubmitHandler&lt;FormValue&gt; = (inputData) =&gt; {
    console.log(inputData);
    signUpNewUser(inputData.id, inputData.pw, inputData.name);
  };
</code></pre>
<pre><code class="language-typescript">
  return (
    &lt;&gt;
             ...
          &lt;form onSubmit={handleSubmit(onSubmit)} className=&quot;space-y-6&quot;&gt;
            &lt;div&gt;
              &lt;label htmlFor=&quot;email&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                Email address
                &lt;span className=&quot;text-xs text-red-600&quot;&gt;{errors?.id?.message}&lt;/span&gt;
              &lt;/label&gt;
              &lt;div&gt;
                &lt;input
                  id=&quot;email&quot;
                  type=&quot;email&quot;
                  {...register(&quot;id&quot;, {
                    required: &quot;   이메일을 입력하세요.&quot;,
                    pattern: {
                      value: /^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-za-z0-9\-]+/,
                      message: &quot;   이메일 형식이 유효하지 않습니다.&quot;
                    }
                  })}
                  placeholder=&quot;example@yolocean.com&quot;
                  className=&quot;block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-300 sm:text-sm sm:leading-6&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;div className=&quot;flex items-center justify-between&quot;&gt;
                &lt;label htmlFor=&quot;password&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                  Password
                  &lt;span className=&quot;text-xs text-red-600&quot;&gt;{errors?.pw?.message}&lt;/span&gt;
                &lt;/label&gt;
              &lt;/div&gt;
              &lt;div&gt;
                &lt;input
                  id=&quot;password&quot;
                  type=&quot;password&quot;
                  {...register(&quot;pw&quot;, {
                    required: &quot;   비밀번호를 입력하세요.&quot;,
                    minLength: {
                      value: 6,
                      message: &quot;   6자리 이상 입력하세요.&quot;
                    }
                  })}
                  placeholder=&quot;비밀번호를 입력하세요.&quot;
                  className=&quot;block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-300 sm:text-sm sm:leading-6&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;div&gt;
                &lt;label htmlFor=&quot;pwConfirm&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                  Password 확인 &lt;span className=&quot;text-xs text-red-600&quot;&gt;{errors?.pwCheck?.message}&lt;/span&gt;
                &lt;/label&gt;
              &lt;/div&gt;
              &lt;div&gt;
                &lt;input
                  id=&quot;pwConfirm&quot;
                  type=&quot;password&quot;
                  {...register(&quot;pwCheck&quot;, {
                    required: true,
                    validate: (value) =&gt; (value === passwordRef.current ? true : &quot;   비밀번호가 일치하지 않습니다.&quot;)
                  })}
                  placeholder=&quot;비밀번호 확인&quot;
                  className=&quot;block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-300 sm:text-sm sm:leading-6&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;div&gt;
                &lt;label htmlFor=&quot;name&quot; className=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;
                  Username
                  &lt;span className=&quot;text-xs text-red-600&quot;&gt;{errors?.name?.message}&lt;/span&gt;
                &lt;/label&gt;
              &lt;/div&gt;
              &lt;div&gt;
                &lt;input
                  id=&quot;name&quot;
                  type=&quot;name&quot;
                  placeholder=&quot;yolocean에서 사용할 이름을 입력하세요&quot;
                  {...register(&quot;name&quot;, {
                    required: &quot;   이름을 입력하세요&quot;
                  })}
                  className=&quot;block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-300 sm:text-sm sm:leading-6&quot;
                /&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                type=&quot;submit&quot;
                // disabled={isValid}
                className=&quot;flex w-full justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 cursor-pointer&quot;
              &gt;
                SignUp &amp; Login
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;

          &lt;div&gt;
            &lt;p
              onClick={() =&gt; setMode(!mode)}
              className=&quot;mt-10 text-center text-sm text-blue-700 hover:text-blue-500 cursor-pointer&quot;
            &gt;
              로그인 하기
            &lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

...
</code></pre>
<h3 id="register">register</h3>
<h4 id="이메일-input-부분을-살펴보면">이메일 input 부분을 살펴보면</h4>
<blockquote>
</blockquote>
<pre><code class="language-typescript"> &lt;input
     id=&quot;email&quot;
     type=&quot;email&quot;
     {...register(&quot;id&quot;, {
           required: &quot;   이메일을 입력하세요.&quot;,
           pattern: {
                   value: /^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-za-z0-9\-]+/,
                  message: &quot;   이메일 형식이 유효하지 않습니다.&quot;
           }
      })}
      placeholder=&quot;example@yolocean.com&quot;
      className=&quot;...&quot;
 /&gt;</code></pre>
<ul>
<li>첫번째 매개변수 name : 해당 필드를 다룰 key값으로 반드시 들어가야 하는 값이다.</li>
<li>두번째 매개변수 options 객체: 유효성 검사를 위한 프로퍼티들이 들어갈 수 있다.<blockquote>
<ul>
<li>required, min, max, minLength, maxLength, pattern 등...</li>
<li>value와 message로 구성된 객체를 주면 해당 에러에 대한 구체적인 메세지를 제공할 수도 있다.</li>
</ul>
</blockquote>
</li>
</ul>
<br>

<h3 id="watch">watch</h3>
<h4 id="코드-상단-비밀번호-유효성-검사-부분에서">코드 상단 비밀번호 유효성 검사 부분에서</h4>
<pre><code class="language-typescript">//비밀번호 유효성 검사를 위해 pw 입력값 확인
  const passwordRef = useRef&lt;string | null&gt;(null);
  passwordRef.current = watch(&quot;pw&quot;);</code></pre>
<ul>
<li>watch를 폼에 입력된 값을 구독해 실시간으로 체크할 수 있게 한다,</li>
<li>매개변수를 주지 않으면 전체 값을 관찰할 수 있고, 매개변수를 주면 해당 값만 관찰할 수 있다.</li>
</ul>
<br>

<h3 id="handlesubmit">handleSubmit</h3>
<p>폼에서 데이터를 입력하고 사용자가 등록(회원가입) 버튼을 누르면 submit 이벤트가 발생한다. 이때, 사용자가 해당 데이터를 올바른 형식으로 입력했는지 검증을 한다. form 태그의 onSubmit 에 handleSubmit 이라는 함수를 넣어주고 매개변수로 우리가 정의한 onSubmit 함수를 넣어주면 된다. onSubmit 함수를 정의 할 때 매개변수로 data 라는 값을 받을 수 있는데, 해당 값은 사용자가 제출 버튼을 클릭 한 후 내려오는 사용자 입장에서 최종으로 제출하는 데이터가 된다. 이 값을 이용해 서버에 값을 넘겨주면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 최종 프로젝트 db구성 및 와이어 프레임 수정]]></title>
            <link>https://velog.io/@_soul_/TIL-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-db%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EC%99%80%EC%9D%B4%EC%96%B4-%ED%94%84%EB%A0%88%EC%9E%84-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@_soul_/TIL-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-db%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EC%99%80%EC%9D%B4%EC%96%B4-%ED%94%84%EB%A0%88%EC%9E%84-%EC%88%98%EC%A0%95</guid>
            <pubDate>Mon, 08 Jan 2024 14:06:43 GMT</pubDate>
            <description><![CDATA[<p><span style="font-size: 12px">오늘은 종일 와이어프레임을 수정했습니다..!! 기초가 탄탄해야 튼튼한 건물을 세우는 것 처럼 자세하고 정성들여 만든 와이어프레임이 멋진 웹페이지를 완성하는데 큰 도움이 될 것이라고 믿으며..!! </span></p>
<h1 id="📁db-설계하기">📁DB 설계하기</h1>
<p>우선 어떤 테이블이 필요한지, 테이블마다 어떤 필드가 필요한지 스프레드시트에 정리해보았다.
<img src="https://velog.velcdn.com/images/_soul_/post/f3fc5c03-49e1-485b-a9c0-4d394b422ba5/image.png" alt="">
이렇게.</p>
<h2 id="📂database-schema">📂Database Schema</h2>
<p>위에서 작성한 스프레드시트를 기반으로 supabase에 table을 만들었다.
<img src="https://velog.velcdn.com/images/_soul_/post/fcd7bfb5-7bd9-4b21-b5ad-dafbaac9b417/image.png" alt=""></p>
<p>(멋져요,,🙌)</p>
<p>store나 상품관련 dummy data가 많이 필요할텐데 supabase는 csv파일을 바로 db에 업로드할 수 있다고 하니 다행이다.</p>
<p><br><br></p>
<h1 id="🙃와이어프레임-수정">🙃와이어프레임 수정</h1>
<p><span style="font-size: 12px">이라고 쓰고 재창조..라고 읽는</span>
프로젝트 기획이 한번 변경된 후 간단하게 작성한 와이어프레임으로는 좀 부족할 듯 싶어 와이어프레임을 수정해보았다.</p>
<h3 id="구와이어프레임">구)와이어프레임</h3>
<p><img src="https://velog.velcdn.com/images/_soul_/post/dfea860f-322f-4234-a1d7-100181801acc/image.png" alt=""></p>
<p>분명히 만들때는 이정도면 충분하지라고 생각했는데 지금 생각해보니 어림도 없는 생각이었다.
디자이너님도 이 와이어프레임으로 작업해달라고 하면 힘드셨을듯.</p>
<h2 id="⭐새로-만든-와이어프레임⭐">⭐새로 만든 와이어프레임⭐</h2>
<p><img src="https://velog.velcdn.com/images/_soul_/post/8f75b8ac-5b6a-4b13-9a13-a66ca08f644d/image.png" alt="">
한눈에 봐도 볼륨차이가 많이 난다.
아무래도 이렇게 페이지가 많은 프로젝트는 진행해본 적이 없어서 와이어프레임 짜는 것도 쉽지 않았다.
와이어프레임을 보니 프로젝트가 끝나지도 않았는데 완성한 것처럼 뿌듯해서;;
내일부터는 진짜로 개발에 집중해서 내가 맡은 부분 후다다다닥 끝내야지.</p>
<br>

<blockquote>
<p>이번 로그인/회원가입 기능에는 react-hook-form을 사용해보려고 한다.
아주 나이쓰하다던데,, 잘 활용해봐야겠다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Supabase] 이메일로 회원가입/로그인 구현하기]]></title>
            <link>https://velog.io/@_soul_/Supabase-%EC%9D%B4%EB%A9%94%EC%9D%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_soul_/Supabase-%EC%9D%B4%EB%A9%94%EC%9D%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 07 Jan 2024 07:35:42 GMT</pubDate>
            <description><![CDATA[<p>어디가지🍆 프로젝트를 진행하면서 처음으로 supabase를 사용해보았다.
supabase 공식 docs가 꽤나 친절하게 설명되어있는 편이지만 영어로 되어있고
구글링했을 때 한국어로 된 정보는 거의 없고 youtube이 대부분이었어서
<img src="https://velog.velcdn.com/images/_soul_/post/b13bec18-9eac-4a36-8b15-3796a44ccbb9/image.jpg" alt="명수형내가한번해보겠다짤">
정리를 해보려고 한다^^</p>
<h1 id="🤜🤛supabase-vs-firebase"><a href="https://supabase.com/alternatives/supabase-vs-firebase">🤜🤛Supabase vs Firebase</a></h1>
<p>supabase는 RDBMS이라는 것이 firebase와 가장 큰 차이점이다.
firebase는 <em>&#39;document-based data storage&#39;_를 기반으로 구축하지만 supabase는 _&#39;postgreSQL&#39;</em> 이라는 관계형 데이터베이스 시스템을 제공한다. 때문에 SQL 쿼리를 사용할 수 있다.</p>
<blockquote>
<ul>
<li>SQL 쿼리 사용 가능</li>
</ul>
</blockquote>
<ul>
<li>개발과정에서 가격 부담이 적음 
( firebase는 CRUD에 대해 가격을 청구하고, supabase는 저장된 데이터 양에 따라 요금 청구 → 개발 과정에서 가격 부담이 적다)</li>
</ul>
<br>

<h1 id="🔐supabase-auth">🔐Supabase Auth</h1>
<p>이번 프로젝트에서는 이메일 로그인과 카카오 로그인을 구현했는데, Docs가 아주 친절하게 step by step으로 설명되어있어서 따라서 하면 어렵지 않았다. 특히 카카오 로그인에 대한 설명도 상세히 적혀있어서 꽤 감동이었다. (물론 자잘한 문제가 있긴 했지만,,)</p>
<h2 id="🔨회원-정보-관리-방법">🔨회원 정보 관리 방법</h2>
<p>supabase docs에서는 Authentication 자체의 meta_data를 활용하기 보다는 <a href="https://supabase.com/docs/guides/auth/managing-user-data">별개의 table을 만들어 관리하기를 추천</a>하고 있다. (보안문제)</p>
<p>그래서 유저 정보를 갖는 table인 userinfo Auth에 새로운 정보가 생기면 userinfo 테이블에 자동으로 update되게 만드는 것이었다. (트리거를 만들어야되겠군..!!)
외국인 선생님의 youtube영상과 희미한 SQL 문법 기억을 이용해 함수와 트리거를 작성했다. </p>
<h3 id="📌auth-생성-시-자동으로-update-되는-userinfo-테이블-만들기">📌Auth 생성 시 자동으로 update 되는 userinfo 테이블 만들기</h3>
<h4 id="✔-userinfo-테이블-만들기">✔ userinfo 테이블 만들기</h4>
<ul>
<li>SQL editor로 만들기
```sql</li>
<li><ul>
<li>create a table for public profiles</li>
</ul>
</li>
</ul>
<p>-- userInfo라는 이름의 테이블을 생성
-- userInfo에서 관리하고 싶은 column들의 이름과 타입을 적어준다.
-- 생성은 꼭 SQL Editor에서 하지 않아도 된다. table Editor에서도 가능
create table userInfo(
  id uuid references auth.users on delete cascade not null primary key,
  email text,
  avatar_url text,
  username text
  admin bool,
);</p>
<pre><code>* Table editor로 만들기
![](https://velog.velcdn.com/images/_soul_/post/c7bbb81b-f738-4241-bb95-8a47858060ac/image.png)
id 사슬 클릭하고
![](https://velog.velcdn.com/images/_soul_/post/28e79613-17c4-4df7-9ab2-9862d59d86de/image.png)

&lt;br&gt;

#### ✔ Function과 Trigger 작성하기
&gt; auth.users에 새로운 값 insert → auth.users에 새로 입력된 값으로 userinfo에 회원 정보 저장

```sql

create policy &quot;public profiles are viewed by everyone.&quot; on userInfo
for select using(true);

create policy &quot;Users can insert their own profile.&quot; on userInfo
for insert with check(auth.uid()=id);

-- 회원가입 시 auth 테이블에 새로 생기는 row의 id, email, raw_user_meta_data 칼럼에서 값을 가져와서 자동으로 userinfo 테이블에 업데이트 해줌
-- auth의 id == userinfo의 id
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.userInfo(id, email, username, avatar_url)
values (new.id, new.email, new.raw_user_meta_data-&gt;&gt;&#39;name&#39;, new.raw_user_meta_data-&gt;&gt;&#39;avatar_url&#39;);
return new;
end;

$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();</code></pre><ul>
<li><p>auth 스키마의 users 테이블에서 값을 가져오는 것
(회원가입을 하면 users 테이블이 업데이트 된다.)
<img src="https://velog.velcdn.com/images/_soul_/post/fb678ce0-de54-458c-b000-853c54b39846/image.png" alt=""></p>
</li>
<li><p>새로 회원 가입을 할 때 가져올 column의 값이 비어있으면 (가져올 수 있는 값이 없으면) 에러가 발생하므로 주의</p>
</li>
</ul>
<h4 id="✔-확인하기">✔ 확인하기</h4>
<ul>
<li>데이터 베이스 schema visualizer에서 auth.users와 userinfo가 연결되어있는 것을 확인 할 수 있다.
<img src="https://velog.velcdn.com/images/_soul_/post/e174297a-01d2-4f9d-98d0-1cc20e199556/image.png" alt=""></li>
<li>데이터 베이스 functions에서 작성한 함수를 확인/수정할 수 있다.
<img src="https://velog.velcdn.com/images/_soul_/post/83cefb52-fc51-4012-8989-10c64c539392/image.png" alt=""></li>
<li>데이터 베이스 triggers에서 작성한 트리거를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/_soul_/post/968b41bf-824f-4c02-b966-fdc06cd259b1/image.png" alt=""><ul>
<li>auth trigger는 SQL editor로만 수정 가능</li>
</ul>
</li>
</ul>
<br>
<br>

<h2 id="📝이메일-회원가입-구현하기">📝이메일 회원가입 구현하기</h2>
<p>위에서 작성한 트리거에 auth.users의 raw_user_meta_data_ column의 값을 가져왔으므로 회원가입 시에 raw_user_meta_data_ 값을 함께 등록하는 것이 매우 중요하다.</p>
<pre><code class="language-typescript">.
.
.
    const { data, error } = await supabase.auth.signUp({
      email: id,
      password: pw,
      options: {
        data: {
          user_name: name,
          avatar_url: null
        }
      }
    });
.
.
.</code></pre>
<p>위의 코드에서 option의 data 부분이 raw_user_meta_data_ column에 저장되는 값이다.</p>
<blockquote>
<p><strong>회원가입을 진행하고나서 웹페이지 localstorage에 user 정보가 저장되는 것을 보아 정상적으로 회원가입이 진행되고 나면 자동으로 로그인 되는 듯 하다.</strong>
<br>
++ 기본설정이 이렇게↑  되어있고 회원가입과 로그인을 분리하고 싶으면
Settings → Authentication → Auth Settings → User Signups 에서 Allow new users to sign up을 끄면 된다.</p>
</blockquote>
<br>

<h3 id="❗❗error-code--429-too-many-requests">❗❗Error Code : 429 too many requests</h3>
<p>supabase auth 설정을 아무것도 건들이지 않은 상태에서 이메일 회원가입을 하게 되면 입력한 이메일 주소로 인증메일이 발송된다.
↓ <span style="color: yellow">waiting for verification,,</span>
<img src="https://velog.velcdn.com/images/_soul_/post/a4fcf33b-389a-4edd-b164-f5bdad609e59/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/_soul_/post/b1602260-d28b-43ab-8c87-2f7becf8d6d1/image.png" alt=""></p>
<p>오 이런 기능이?! 하고 나서 회원가입과 로그인을 계속해서 테스트 하다보면
갑자기 회원가입도 로그인도 안되는 현상이 발생한다. 콘솔을 열어보면 429에러가 발생한 것을 볼 수 있는데 supabase에서 인증메일을 보내는 기본 횟수가 아주 적기 때문이다. (정확한 횟수는 모르겠다.)</p>
<h4 id="해결방법1-이메일-인증기능-끄기">해결방법1) 이메일 인증기능 끄기</h4>
<ul>
<li>Authentication → Providers → Auth Providers → Email → Confirm email 끄기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_soul_/post/80953ad4-8fd0-40bd-8a0c-1a3fa8f389fc/image.png" alt=""></p>
<h4 id="해결방법2-smtp-서버-이용하기">해결방법2) SMTP 서버 이용하기</h4>
<ul>
<li>이메일 인증기능이 꼭 필요하다면 자체 SMTP 서버를 사용하는 방법이 있다.
처음에 이메일 인증기능을 끄는 기능이 있는지 모르고 Gmail을 이용해 SMTP setting을 했는데 특별히 어렵지 않았고 정상적으로 인증이메일이 발송되는 것도 확인했다.</li>
</ul>
<br>
<br>

<h2 id="🔑이메일로-로그인-구현하기">🔑이메일로 로그인 구현하기</h2>
<pre><code class="language-typescript">    const { data, error } = await supabase.auth.signInWithPassword({
      email: id,
      password: pw
    });</code></pre>
<p>로그인을 하면 localStorage에 동일한 이름의 key값으로 로그인 정보가 저장되는 것을 알 수 있다</p>
<h2 id="🔒로그아웃">🔒로그아웃</h2>
<pre><code class="language-typescript">  const { error } = await supabase.auth.signOut();</code></pre>
<p><br><br></p>
<blockquote>
<p><a href="https://supabase.com/docs/guides/auth/auth-email">공식 docs 링크</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 팀 프로젝트 S.A.]]></title>
            <link>https://velog.io/@_soul_/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S.A</link>
            <guid>https://velog.io/@_soul_/%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S.A</guid>
            <pubDate>Fri, 05 Jan 2024 08:00:01 GMT</pubDate>
            <description><![CDATA[<p><strong>아이디어</strong></p>
  <span style="font-size: 10px">
- 이력서 관리
- 여행 플래너 (날씨 + 캘린더 + 지도)
- 개발 블로그
    - 블로그 포스팅 기능
    - 다른 사람 포스팅 기능
    - 팔로우
    - 좋아요
    - 회원가입
    - notion 연동
- 유기견 사이트
- 분위기 공유 사이트 (음악 재생)
- 렌탈 어플리케이션 플랫폼
    - 쿠팡처럼 신청을하고 렌탈을 할 수 있는 어플리케이션
    - 유아용품 (아이들의 성장이 빠르기 때문에 )
    - 위치기반 (주변 기반에서 보내줘야하기 때문에 + 계약서)
    - 렌탈을 하는 업체가 없기 떄문에 테스트 시에는 임의로 넣어줘야한다.


<h4 id="고려하고-있는-기능들">고려하고 있는 기능들</h4>
<ul>
<li>실시간 채팅</li>
<li>차트</li>
<li>커머스 기능</li>
<li>캘린더</li>
<li>지도</li>
<li>뮤직 플레이어</li>
</ul>
<h4 id="고려하고-있는-라이브러리">고려하고 있는 라이브러리</h4>
<ul>
<li>matter-js (물리 엔진 기능)</li>
<li>clayful (커머스 회원, 결제 기능)</li>
<li>react-player (음악 플레이어 기능)</li>
<li>toast ui editor(마크다운 에디터 기능) ⇒ @uiw/react-md-editor</li>
</ul>
</span>

<h1 id="프로젝트">프로젝트</h1>
<h2 id="해양-레저-렌탈전문-웹앱">해양 레저 렌탈전문 웹/앱</h2>
<h3 id="mvp-스펙">MVP 스펙</h3>
<p><strong>일반 사용자 관련</strong></p>
<ul>
<li>회원가입 , 로그인 </li>
<li>회원정보 수정</li>
<li>문의에 대한 답변 알림</li>
</ul>
<p><strong>관리자 관련</strong></p>
<ul>
<li>제품 등록 (제품 대여소에대한 지도정보 등록)</li>
<li>제품에 대한 문의 답변작성</li>
<li>등록된 제품의 상태관리</li>
</ul>
<p>*<em>제품 관련 *</em></p>
<ul>
<li>제품 조회</li>
<li>렌탈 후기작성(댓글 및 별점 + 사용자가 렌탈한 이력이 있을경우 작성가능)</li>
<li>제품에 대한 문의 </li>
<li>장바구니 담기<br>

</li>
</ul>
<h3 id="개발환경">개발환경</h3>
<ul>
<li>Next.js - App Router</li>
<li>supabase</li>
<li>react kakao map</li>
<li>zustand</li>
<li>tailwindCSS</li>
</ul>
<br>

<h3 id="와이어-프레임"><a href="https://www.figma.com/file/F5dBmowb0QXH8qpqY7yG0V/CLIONE?type=whiteboard&amp;node-id=0%3A1&amp;t=jwV6123vMhHjkhbH-1">와이어 프레임</a></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[Where2Go (어디가지🍆) 프로젝트 KPT 회고]]></title>
            <link>https://velog.io/@_soul_/Where2Go-%EC%96%B4%EB%94%94%EA%B0%80%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@_soul_/Where2Go-%EC%96%B4%EB%94%94%EA%B0%80%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 03 Jan 2024 07:54:28 GMT</pubDate>
            <description><![CDATA[<h1 id="kpt-회고">KPT 회고</h1>
<h3 id="keep---현재-만족하고-있는-부분"><strong>Keep - 현재 만족하고 있는 부분</strong></h3>
<ul>
<li><p>도⭐⭐</p>
<ul>
<li>팀 분위가 너무 좋았다</li>
<li>모르는 것이 있을 때 같이 고민해주고 답을 찾는 과정이 좋았다</li>
<li>깃헙 룰이 잘 지켜졌다고 생각한다</li>
</ul>
</li>
<li><p>마⭐⭐</p>
<ul>
<li>초기 프로젝트 세팅 : 프로젝트 초기 설정을 탄탄하게 해서 편했다</li>
<li>팀원간의 활발한 의사소통 : 의견 공유와 피드백을 통한 프로젝트 퀄리티 향상</li>
<li>git 커밋 컨벤션 : 커밋 컨벤션에 따라 git을 관리한 부분</li>
</ul>
</li>
<li><p>한⭐⭐</p>
<ul>
<li>각자 맡은 역할을 모두 성실하게 수행했다</li>
<li>tailwind, nextjs, ts, supabase 등 새로운 기술에 도전하여 포기하지 않고 끝까지 완성해냈다!</li>
<li>nextjs의 장점을 십분 활용하여 SSR로 구현하려고 시도한 점이 좋았다.</li>
</ul>
</li>
<li><p>민⭐⭐</p>
<ul>
<li>github issue로 진행상황 관리 : 구두로 진행상황 공유하는 것 뿐만 아니라 문서화 되어있어서 좋음</li>
<li>커밋 컨벤션</li>
</ul>
</li>
</ul>
<h3 id="problem---불편하게-느끼는-부분"><strong>Problem - 불편하게 느끼는 부분</strong></h3>
<ul>
<li><p>도⭐⭐</p>
<ul>
<li>Next.js에 적응하는데 시간이 걸려 프로젝트를 진행하는데 힘든 점이 많았다.</li>
<li>Next.js를 사용하는데 page.tsx에 몰아서 작성하다보니 가독성이 떨어졌다.</li>
</ul>
</li>
<li><p>마⭐⭐</p>
<ul>
<li>초기 프로젝트 기획 : 초기에 프로젝트를 너무 장황하게 잡은 것 같다.</li>
<li>시간 : 프로젝트 진행 기간이 좀 짧았던 것 같다.</li>
</ul>
</li>
<li><p>한⭐⭐</p>
<ul>
<li>리액트 쿼리를 사용하면서 여러개의 쿼리키를 효율적으로 관리하지 못함</li>
<li>타입스크립트가 미숙하여 코드 이해보다 돌아가게 하는 데 중점을 둔 것이 아쉬움</li>
</ul>
</li>
<li><p>민⭐⭐</p>
<ul>
<li>pull request 리뷰 🌟</li>
<li>merge끝난 브랜치 삭제하기</li>
<li>처음 쓰는 기술스택,, 쉽지 않아여,,(익숙해지자!!)</li>
</ul>
</li>
</ul>
<h3 id="try---problem에-대한-해결책-당장-실행-가능한-것"><strong>Try - Problem에 대한 해결책, 당장 실행 가능한 것</strong></h3>
<ul>
<li>시간여행을 두려워하지 말고 병합한 브랜치는 가차없이 삭제하자</li>
<li>서버와 통신할 대 모든 로딩에 대해 spinner, 스켈레톤 등의 적용을 고려하자 loading.ts</li>
<li>next js의 SSR, SSG개념 이해하고 프로젝트에 잘 활용하기</li>
<li>코드가 한 파일에 올리지 않도록 구조화를 잘짜서 깔끔하게 만들기</li>
<li>리액트 쿼리의 다양한 기능 사용해보기 (인피니트 쿼리, 페이지네이션…)</li>
<li>PR 리뷰 한번씩 남겨보기 (KPT 회고 끝난 다음 바로!!)</li>
<li>PR 리뷰를 잘 남기자!: 사소한 리뷰라도 서로 공유하기 LGTM :)</li>
<li>app router와 pages router를 이용한 todo app을 만들며 next.js 와 SSR에 친숙해지기</li>
<li>리액트 쿼리 사용 시 쿼리 키를 하드코딩으로 작성하기보다 객체형태로 key를 관리해 휴먼 에러를 방지하자</li>
<li>커밋 컨벤션처럼 PR review도 규칙을 만들어보자</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_soul_/post/edee2467-716e-4afb-b0df-20c220ae75e3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 주차 요금 계산]]></title>
            <link>https://velog.io/@_soul_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A3%BC%EC%B0%A8-%EC%9A%94%EA%B8%88-%EA%B3%84%EC%82%B0</link>
            <guid>https://velog.io/@_soul_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%A3%BC%EC%B0%A8-%EC%9A%94%EA%B8%88-%EA%B3%84%EC%82%B0</guid>
            <pubDate>Thu, 28 Dec 2023 00:09:52 GMT</pubDate>
            <description><![CDATA[<h2 id="주차-요금-계산"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/92341">주차 요금 계산</a></h2>
<blockquote>
<h4 id="문제-설명">문제 설명</h4>
<p>주차장의 요금표와 차량이 들어오고(입차) 나간(출차) 기록이 주어졌을 때, 차량별로 주차 요금을 계산하려고 합니다. 아래는 하나의 예시를 나타냅니다.<br></p>
</blockquote>
<ul>
<li>요금표<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>기본 시간(분)</th>
<th>기본 요금(원)</th>
<th>단위 시간(분)</th>
<th>단위 요금(원)</th>
</tr>
</thead>
<tbody><tr>
<td>180</td>
<td>5000</td>
<td>10</td>
<td>600</td>
</tr>
</tbody></table>
</li>
<li>입/출차 기록<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>시각(시:분)</th>
<th>차량 번호</th>
<th>내역</th>
</tr>
</thead>
<tbody><tr>
<td>05:34</td>
<td>5961</td>
<td>입차</td>
</tr>
<tr>
<td>06:00</td>
<td>0000</td>
<td>입차</td>
</tr>
<tr>
<td>06:34</td>
<td>0000</td>
<td>출차</td>
</tr>
<tr>
<td>07:59</td>
<td>5961</td>
<td>출차</td>
</tr>
<tr>
<td>07:59</td>
<td>0148</td>
<td>입차</td>
</tr>
<tr>
<td>18:59</td>
<td>0000</td>
<td>입차</td>
</tr>
<tr>
<td>19:09</td>
<td>0148</td>
<td>출차</td>
</tr>
<tr>
<td>22:59</td>
<td>5961</td>
<td>입차</td>
</tr>
<tr>
<td>23:00</td>
<td>5961</td>
<td>출차</td>
</tr>
</tbody></table>
</li>
<li>자동차별 주차 요금<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th>차량 번호</th>
<th>누적 주차 시간(분)</th>
<th>주차 요금(원)</th>
</tr>
</thead>
<tbody><tr>
<td>0000</td>
<td>34 + 300 = 334</td>
<td>5000 + ⌈(334 - 180) / 10⌉ x 600 = 14600</td>
</tr>
<tr>
<td>0148</td>
<td>670</td>
<td>5000 +⌈(670 - 180) / 10⌉x 600 = 34400</td>
</tr>
<tr>
<td>5961</td>
<td>145 + 1 = 146</td>
<td>5000</td>
</tr>
</tbody></table>
<blockquote>
<ul>
<li>어떤 차량이 입차된 후에 출차된 내역이 없다면, 23:59에 출차된 것으로 간주합니다.</li>
</ul>
</blockquote>
<ul>
<li>0000번 차량은 18:59에 입차된 이후, 출차된 내역이 없습니다. 따라서, 23:59에 출차된 것으로 간주합니다.</li>
</ul>
</li>
<li>00:00부터 23:59까지의 입/출차 내역을 바탕으로 차량별 누적 주차 시간을 계산하여 요금을 일괄로 정산합니다.</li>
<li>누적 주차 시간이 기본 시간이하라면, 기본 요금을 청구합니다.</li>
<li>누적 주차 시간이 기본 시간을 초과하면, 기본 요금에 더해서, 초과한 시간에 대해서 단위 시간 마다 단위 요금을 청구합니다.<ul>
<li>초과한 시간이 단위 시간으로 나누어 떨어지지 않으면, 올림합니다.</li>
<li>⌈a⌉ : a보다 작지 않은 최소의 정수를 의미합니다. 즉, 올림을 의미합니다.<br>
주차 요금을 나타내는 정수 배열 fees, 자동차의 입/출차 내역을 나타내는 문자열 배열 records가 매개변수로 주어집니다. 차량 번호가 작은 자동차부터 청구할 주차 요금을 차례대로 정수 배열에 담아서 return 하도록 solution 함수를 완성해주세요.
#### 제한사항
링크 참조
<br>
#### 입출력 예시
링크 참조



</li>
</ul>
</li>
</ul>
<h3 id="solution-code">solution code</h3>
<pre><code class="language-javascript">function solution(fees, records) {
    let answer = [];

    const getNumberTime = (time) =&gt; {
        const arr = time.split(&quot;:&quot;);
        return Number.parseInt(arr[0]) * 60 + Number.parseInt(arr[1]);
    }

    const getParkTime = (start, end) =&gt; { 
        return getNumberTime(end) - getNumberTime(start); 
    }

    const getTotalFee = (time) =&gt; {

        // 기본 시간
        let cost = fees[1];
        time -= fees[0];

        // 추가 단위 시간
        if(time &gt; 0) {
            cost += Math.ceil(time/fees[2]) * fees[3];
        }

        return cost;
    }

    const park = records.map((item) =&gt; item.split(&quot; &quot;)).sort((a, b) =&gt; a[1] === b[1] ? a[0] - b[0] : a[1] - b[1]);

    let time = 0;
    for(let i=park.length-1; i&gt;=0;i--) {

        if(park[i][2] === &#39;IN&#39;) {
            // 나간 기록이 없는 차량
            time += getParkTime(park[i][0], &quot;23:59&quot;);
        } else {
            // 나간 기록이 있는 차량
            time += getParkTime(park[i-1][0], park[i][0]);
            i--;
        }

        // 차량 종류가 바뀌면 answer 맨 앞에 주차 요금 추가
        if(i === 0 || (park[i][1] !== park[i-1][1])) {
            answer.unshift(getTotalFee(time));
              // 차량 번호의 오름차순 정렬된 park 배열을 반대로 탐색하고 있기 때문에 unshift
            time = 0;
        }
    }
    return answer;
}</code></pre>
<p><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Where2Go (어디가지🍆) 프로젝트 S.A. (미완성)]]></title>
            <link>https://velog.io/@_soul_/Where2Go-%EC%96%B4%EB%94%94%EA%B0%80%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S.A.-%EB%AF%B8%EC%99%84%EC%84%B1</link>
            <guid>https://velog.io/@_soul_/Where2Go-%EC%96%B4%EB%94%94%EA%B0%80%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-S.A.-%EB%AF%B8%EC%99%84%EC%84%B1</guid>
            <pubDate>Wed, 27 Dec 2023 00:05:30 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>프로젝트 명 : Where2Go (어디가지🍆)</p>
</li>
<li><p>소개</p>
<ul>
<li>한 줄 정리 : 자랑하고 싶은 숨은 명소들, 함께 즐기고 싶은 특별한 장소들을 공유하는 지도 기반의 소셜 플랫폼 입니다</li>
<li>내용 :  ‘어디가지?’ 는 지도 기반의 소셜 플랫폼으로, 여러 사용자들이 자신만의 특별한 스팟을 공유하고 추천할 수 있는 서비스입니다.
해당 서비스를 통해 자신이 지나쳤던 장소를 발견하고, 사용자들과 특별한 장소를 공유할 수 있습니다.</li>
</ul>
<br>

<ul>
<li><p>필수 구현 사항</p>
<ul>
<li><p>게시물</p>
<ul>
<li><p>게시물 작성:</p>
<ul>
<li><p>사진 업로드 (최소 1장, 최대 5장)</p>
</li>
<li><p>공간에 대한 소개글</p>
</li>
<li><p>장소의 주소 (상세하게)</p>
</li>
<li><p>방문한 날짜</p>
</li>
<li><p>대표 카테고리 (1개만)</p>
</li>
</ul>
</li>
<li><p>게시물 삭제 / 수정</p>
</li>
</ul>
</li>
<li><p>타 유저 게시물/개인 페이지를 통해 팔로우/취소 가능</p>
</li>
<li><p>사용자</p>
<ul>
<li><p>프로필</p>
<p>  프로필 수정</p>
</li>
<li><p>마이페이지에서 사용자가 작성한 게시물 전체 확인</p>
</li>
</ul>
</li>
<li></li>
<li><p>지도API (카카오맵)</p>
<ul>
<li>키워드로 장소 검색</li>
<li>등록된 스팟에 핀 찍기 (+오버레이)</li>
<li>현재 유저 위치 기반 장소 찾기</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
 <br>

<ul>
<li>추가 구현 사항<ul>
<li>댓글 관련 기능 (작성/수정/삭제)</li>
<li>회원 탈퇴 기능</li>
<li>지도 오버레이 상세화</li>
</ul>
</li>
</ul>
 <br>


<ul>
<li><p><strong>개발환경</strong></p>
<ul>
<li>전역 상태 관리 라이브러리: zustand</li>
<li>백엔드 서버: supabase</li>
<li>프레임워크: Next.js (pages-router)</li>
<li>TypeScript</li>
</ul>
<br>



</li>
</ul>
<ul>
<li><strong>와이어프레임</strong>
<img src="https://velog.velcdn.com/images/_soul_/post/aa33cb3a-7a5b-41dc-b8ac-bd3cd0be4769/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] React Query의 동작원리]]></title>
            <link>https://velog.io/@_soul_/React-React-Query%EC%9D%98-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@_soul_/React-React-Query%EC%9D%98-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Mon, 25 Dec 2023 06:28:43 GMT</pubDate>
            <description><![CDATA[<h1 id="🎯-일단-사용해보기">🎯 일단 사용해보기</h1>
<h3 id="0-설치">0. 설치</h3>
<pre><code class="language-bash">yarn add react-query</code></pre>
<h4 id="-json-server-설치-및-실행">+ json-server 설치 및 실행</h4>
<pre><code class="language-bash">yarn add json-server
yarn json-server --watch db.json --port 3001
//db.json 파일을 db로 사용하고 3001번 포트에서 서버를 시작하겠다</code></pre>
<h4 id="-axios-설치-및-실행">+ axios 설치 및 실행</h4>
<pre><code class="language-bash">yarn add axios</code></pre>
<h3 id="1-queryclientprovider">1. QueryClientProvider</h3>
<p>: 데이터를 읽어오는 기능(QueryClient)를 애플리케이션 전체에 주입하는 API</p>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import Router from &quot;./shared/router&quot;;
import { QueryClient, QueryClientProvider } from &quot;react-query&quot;;

const App = () =&gt; {
  &lt;QueryClientProvider client={QueryClient}&gt;
    return &lt;Router /&gt;;
  &lt;/QueryClientProvider&gt;;
};

export default App;</code></pre>
<h3 id="2-usequery">2. useQuery</h3>
<pre><code class="language-javascript">import { useQuery } from &#39;react-query&#39;;
import { fetchTodoList } from &#39;../api/fetchTodoList&#39;;

function App() {
    const info = useQuery(&#39;todos&#39;, fetchTodoList);
      //const { isLoading, isError, data } = useQuery(&quot;todos&quot;, fetchTodoList);
      ...
}</code></pre>
<p><code>useQuery(&#39;todos&#39;, fetchTodoList)</code></p>
<h4 id="1-usequery의-첫번째-인자">(1) useQuery의 첫번째 인자</h4>
<ul>
<li><p><code>Query Keys</code>
: refetching / caching 처리 / 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 사용된다. (여러 컴포넌트에 존재해도 같은 key면 같은 쿼리 및 데이터를 보장함)</p>
</li>
<li><p><code>Query Keys</code>는 한 단어일수도, 배열일 수도 nested객체 일 수도 있다.</p>
<pre><code class="language-javascript">const query1 = useQuery(&#39;qk&#39;, api); // unique
const query2 = useQuery(&#39;qk2&#39;, api); // not unique
const query3 = useQuery(&#39;qk2&#39;, api); // not unique</code></pre>
<p>&amp;nbsp : key 이므로 고유한 값을 가져야 한다.</p>
<br>
</li>
<li><p>한 단어로 이뤄진 <code>Query Keys</code></p>
<pre><code class="language-javascript">useQuery(&#39;todos&#39;, ...)</code></pre>
<p>위와 같은 코드가 있다면 내부적으로는 아래처럼 해석된다.</p>
<pre><code class="language-javascript">queryKey === [&#39;todos&#39;]     // 배열 형태로 갖고 있다</code></pre>
<br>
</li>
<li><p>배열 형태로 이뤄진 <code>Query Keys</code>
: 문자, 숫자, object 등등 여러가지를 조합한 배열 형태의 key도 사용 가능</p>
</li>
</ul>
<pre><code class="language-javascript">// ID가 5인 todo 아이템 1개
useQuery([&#39;todo&#39;, 5], ...)
// queryKey === [&#39;todo&#39;, 5]

// ID가 5인 todo 아이템 1개인데, preview 속성은 true야
useQuery([&#39;todo&#39;, 5, { preview: true }], ...)
// queryKey === [&#39;todo&#39;, 5, { preview: true }]

// todolist 전체인데, type은 done이야
useQuery([&#39;todos&#39;, { type: &#39;done&#39; }], ...)
// queryKey === [&#39;todos&#39;, { type: &#39;done&#39; }]</code></pre>
<h4 id="2-두-번째-인자">(2) 두 번째 인자</h4>
<ul>
<li><code>Query Functions</code>
: promise 객체 리턴
: promise 객체는 반드시 data resolve하거나 에러를 내야한다<blockquote>
<ul>
<li>resolve : 정상적으로 통신되었음을 의미</li>
<li>오류가 발생한 경우에는 그에 맞는 적절한 오류 처리 관련 로직을 삽입해 처리해줘야한다. (axios, fetch, graphgl...)</li>
</ul>
</blockquote>
</li>
</ul>
<h4 id="3-usequery의-결과물">(3) useQuery의 결과물</h4>
<p>: useQuery를 통해 얻은 결과물은 객체이다.
: 객체 안에는 &#39;조회&#39;를 요청한 결과에 대한 거의 모든 정보가 들어있고, 그 과정에 대한 정보도 들어있다.</p>
<blockquote>
<ul>
<li>조회를 시작하면 <code>isLoading</code>이 <code>true</code>가 되고</li>
<li>조회 결과 오류가 나면 <code>isError</code>가 <code>true</code>가, <code>isLoading</code>은 <code>false</code>가 된다. <code>error 객체</code>를 통해 더 상세한 오류 내용을 확인 할 수 있다. </li>
<li>조회 결과 정상이 되면 <code>isSuccess</code>가 <code>true</code>가 , <code>isLoading</code>은 <code>false</code>가 된다. <code>data 객체</code>를 통해 좀 더 상세한 조회 결과를 확인할 수 있다.</li>
</ul>
</blockquote>
<p><br><br></p>
<h2 id="2-usemutation">2. useMutation</h2>
<p>: query와 다르게 mutation은 CUD에서 사용됨</p>
<ul>
<li>예제<pre><code class="language-javascript">// [출처] : 공식문서
</code></pre>
</li>
</ul>
<p>function App() {
   const mutation = useMutation(newTodo =&gt; {
     return axios.post(&#39;/todos&#39;, newTodo)
   })</p>
<p>   return (
     <div>
       {mutation.isLoading ? (
         &#39;Adding todo...&#39;
       ) : (
         &lt;&gt;
           {mutation.isError ? (
             <div>An error occurred: {mutation.error.message}</div>
           ) : null}</p>
<pre><code>       {mutation.isSuccess ? &lt;div&gt;Todo added!&lt;/div&gt; : null}

       &lt;button
         onClick={() =&gt; {
           mutation.mutate({ id: new Date(), title: &#39;Do Laundry&#39; })
         }}
       &gt;
         Create Todo
       &lt;/button&gt;
     &lt;/&gt;
   )}
 &lt;/div&gt;</code></pre><p>   )
 }</p>
<pre><code>
 * `mutation.mutate(인자)`
 : **인자**는 반드시 한 개의 변수 또는 객체여야 한다.
 : 결과는 객체의 형태로 되어있고
 : 그 객체는 항상 (isIdle | isLoading | isError| isSuccess) 중 하나의 상태이다.

 &lt;br&gt;&lt;br&gt;

# 🎯 React Query란?
: 서버 상태 관리를 쉽게 할 수 있게 도와주는 라이브러리 이다.
&gt; 서버 상태 관리: 서버에 요청하고 응답받는 모든 과정과 연관된 데이터들
* `fetching`: 서버에서 데이터 받아오기
* `caching`: 서버에서 받아온 데이터를 따로 보관해서 동일한 데이터가 단 시간 내에 다시 필요할 시 서버 요청없이 보관된 데이터에서 꺼내쓰기
* `synchronizing`: 서버 상의 데이터와 보관 중인 캐시 데이터(서버 상태)를 동일하게 만들기(동기화)
* `updating`: 서버 데이터 변경 용이 (mutation &amp; invalidateQueries)


#### 기존 미들웨어의 한계
: 서버와의 API통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-Saga등의 미들웨어를 사용할 수 있다. 하지만 문제가 있는데,,
* 보일러 플레이트 코드의 양이 너무 많다.
* Redux는 비동기 데이터 관리를 위한 전문 라이브러리가 아니다. (규격화 문제)

#### 리액트 쿼리의 장점
* 보일러 플레이트를 만들다가 오류날 일이 없다
* 사용방법이 thunk에 비해 쉽고 직관적이다.

#### 기존 미들웨어와의 차이
```javascript
// React Query 미사용 시
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);

const getTodos = async () =&gt; {
  setIsLoading(true);
    const data = await axios.get(`${API_URL}/todos`).then(res =&gt; res.data);
    setTodos(data);
  setIsLoading(false);
}
useEffect(() =&gt; {
    getTodos();
}, []);


// React Query 사용 시
const getTodos = () =&gt; axios.get(`${API_URL}/todos`).then(res =&gt; res.data);

const { data: todos, isLoading } = useQuery([&quot;todos&quot;], getTodos);</code></pre><p><strong>+읽어볼 만한 글) <a href="https://tech.kakaopay.com/post/react-query-1/#-%EB%84%88%EB%AC%B4-%EC%9E%A5%ED%99%A9%ED%95%9C-boilerplate-%EC%BD%94%EB%93%9C">카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유</a></strong></p>
<br>

<h3 id="✔-주요-키워드">✔ 주요 키워드</h3>
<ul>
<li><p>Query
: 어떤 데이터에 대한 요청
: axios의 get요청과 유사</p>
</li>
<li><p>Mutation
: 데이터 그룹 자체를 변경하는 것 (Create, Update, Delete)
: axios의 post, put, patch, delete 요청과 유사 </p>
</li>
<li><p>Query Invalidation
: 쿼리 무효화
: 기존에 가져온 Query는 서버 데이터이기 때문에 언제든지 변경이 있을 수 있다. <strong>최신 상태가 아닐 수</strong> 있으므로 기존의 쿼리를 무효화한 후 최신화 해야 하는데 React Query는 이런 과정을 알아서 해준다.</p>
</li>
</ul>
<br>

<h3 id="✔-swr-전략">✔ SWR 전략</h3>
<blockquote>
<p>stale-while-revalidate : 신규 데이터가 도착하는 동안 일단 기존 캐싱된 데이터를 사용하도록 하는 전략</p>
<blockquote>
<p>서버의 헤더 응답 설정 Cache-control에서 아이디어 기원
<img src="https://velog.velcdn.com/images/_soul_/post/2fc37d00-eac3-41e9-a38b-3c4c7909f763/image.png" alt="">
⬆️ 클라이언트가 0~1초 사이에 다시 데이터를 요청하면, 서버 호출없이 캐시 데이터를 바로 사용
<img src="https://velog.velcdn.com/images/_soul_/post/00bf00f1-e537-4a33-91fa-e9beb727b465/image.png" alt="">
⬆️ 클라이언트가 1 ~ 60s 사이에 다시 데이터 요청하면, 일단 캐시 데이터를 사용하고 서버에서 신규데이터를 주면 그것으로 교체
<br></p>
</blockquote>
</blockquote>
<h3 id="✔-캐시-데이터-저장">✔ 캐시 데이터 저장</h3>
<ul>
<li>QueryClientProvider는 React Context API를 내부적으로 사용한다.</li>
<li>QueryCLientProvider의 자식으로 있는 모든 컴포넌트들은 캐시 데이터에 접근할 수 있다.</li>
<li>페이지 컴포넌트 외부에 상태가 존재한다는 점에서 캐시 데이터는 전역 상태로 볼 수 있다.<pre><code class="language-javascript">//App.jsx
const queryClient = new QueryClient();
</code></pre>
</li>
</ul>
<p>const App = () =&gt; {
      <QueryClientProvider client = {queryClient}>
          <Router/>
      </QueryClientProvider>
    };
}</p>
<pre><code>
### ✔ useQuery에서 자주 사용하는 옵션들
#### ① enabled
```javascript
useQuery([&quot;todos&quot;], getTodos, {enable: true})</code></pre><p>: boolean 타입 (true / false)
: true일 경우에만 queryFn 실행
: default 값은 true, useQuery 자동 실행됨</p>
<h5 id="예제1-disabling--pausing-queries-이벤트-발생-시에만-수동-실행하고-싶을-때">예제1 (Disabling / Pausing Queries): 이벤트 발생 시에만 수동 실행하고 싶을 때</h5>
<pre><code class="language-javascript">const {data, refetch} = useQuery([&quot;todos&quot;], getTodos, {enabled: false});

return (
    &lt;div&gt;
          &lt;button onClick = {() =&gt; refetch()}&gt;
              데이터 불러오기
        &lt;/button&gt;
    &lt;/div&gt;
);</code></pre>
<h5 id="예제2-dependent-queries-usequery가-2개-이상이고-실행순서-설정이-필요할-때">예제2 (Dependent Queries): useQuery가 2개 이상이고 실행순서 설정이 필요할 때</h5>
<pre><code class="language-javascript">//Get user
const {data: user} = useQuery({queryKey: [&#39;user&#39;, email], queryFn: getUserByEmail});

const userId = user?.id;

//Get user&#39;s projects
const { status, fetchStatus, data: projects }
    = useQuery({ queryKey: [&#39;projects: userId&#39;], queryFn: getProjectsByUser, enabled: !!userId});
// userId가 존재하기 전까지 이 쿼리는 실행되지 않음
// !!userId === Boolean(userId)</code></pre>
<h4 id="②-select">② select</h4>
<p>queryFn에 의해 리턴된 값을 변형시킨 후에 useQuery의 리턴 data로 넘겨줌
(단, cache data는 queryFn에서 리턴 받은 값 그대로 이다.)</p>
<pre><code class="language-javascript">import {useQuery} from &quot;react-query&quot;

function User(){
  const { data } = useQuery( [&#39;user&#39;], fetchUser, { select: (user) =&gt; user.username, });
  return &lt;div&gt;Username: {data}&lt;/div&gt;;
}</code></pre>
<br> 

<h3 id="✔-react-query의-데이터-흐름">✔ react Query의 데이터 흐름</h3>
<blockquote>
<p>&quot;오래된 것 먼저, 리렌더링 되면서 새 것으로 교체&quot;</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/_soul_/post/f78722e5-89d9-4f53-8f5c-7e235712884f/image.png" alt=""></p>
<p><br><br></p>
<h2 id="📌tanstack-query-">📌Tanstack Query ?</h2>
<p>: React Query v4 부터 라이브러리 이름이 <a href="https://tanstack.com/query/latest">Tanstack Query</a>로 변경되었다. React 뿐 아니라 Vue 같은 다른 SPA 프레임워크에도 적용!!</p>
<pre><code class="language-bash">// &quot;react-query&quot;: &quot;^3.39.3&quot;
yarn add react-query

// &quot;@tanstack/react-query&quot;: &quot;^4.29.19&quot;
yarn add @tanstack/react-query</code></pre>
<p>+&amp;nbspv4부터는 query key를 반드시 배열 형태로 써야 함
<br><br></p>
<h2 id="📌react-query-lifecycle">📌React Query LifeCycle</h2>
<p><img src="https://velog.velcdn.com/images/_soul_/post/2c314eb9-f8f0-4820-bae4-4b48a9ef8625/image.png" alt=""></p>
<ul>
<li>fresh: 새거가 필요하지 않은 상태</li>
<li>state: 새거가 필요한 상태</li>
<li>기본 설정 (default config)</li>
</ul>
<table>
<thead>
<tr>
<th>기본설정</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>staleTime: 0</td>
<td>useQuery 또는 useInfiniteQuery에 등록된 QueryFn을 통해 fetch해 온 데이터는 항상 stale data 취급</td>
</tr>
<tr>
<td>refetchOnMount: true</td>
<td>useQuery 또는 useInfiniteQuery가 있는 컴포넌트가 마운트 시 stale data를 refetch 자동 실행</td>
</tr>
<tr>
<td>refetchOnWindowFocus: true</td>
<td>실행중인 브라우저 화면은 focus할 때마다 stale data를 refetch 자동 실행</td>
</tr>
<tr>
<td>refetchOnReconnect : true</td>
<td>Network가 끊겼다가 재연결되었을 때 stale data를 refetch 자동 실행</td>
</tr>
<tr>
<td>cacheTime: 5분 (1000 * 60 * 5ms)</td>
<td>useQuery 또는 useInfiniteQuery가 있는 컴포넌트가 언마운트 되었ㅇ르 때 inactive query라고 부르며, inactive 상태가 5분 경과 후 GC(Garbage Collector)에 의해 cache data 삭제 처리</td>
</tr>
<tr>
<td>retry: 3</td>
<td>useQuery 또는 useInfiniteQuery에 등록된 queryFn이 API 서버에 요청을 보내서 실패하더라도 바로 에러를 띄우지 않고 총 3번까지 재요청을 자동으로 사용</td>
</tr>
</tbody></table>
<blockquote>
<ul>
<li>staleTime: 얼마의 시간이 흐른 뒤에 stale취급할 것인가 (default: 0)<blockquote>
<p>staleTime &gt; 0이면, fresh data
staleTime = 0이면, stale data</p>
</blockquote>
</li>
<li>cacheTime: inactive된 이후로 메모리에 얼마만틈 있을 것인가 (default: 5분, cacheTime이 0이 되면 삭제처리됨)</li>
</ul>
</blockquote>
<p><br><br></p>
<h2 id="📌isloading-vs-isfetching">📌isLoading vs isFetching</h2>
<blockquote>
<ul>
<li><code>isLoading</code>: 새로운 캐시 데이터를 서버에서 받고 있는가</li>
<li><code>isFetching</code>: 서버에서 데이터를 받고 있는가</li>
</ul>
</blockquote>
<p>: 캐시 데이터가 있는 경우 <code>isLoading</code>은 <code>false</code>, <code>isFetching</code>은 <code>true</code>이다.</p>
<p><br><br></p>
<h2 id="📌usequery-실행-시cachetime--0인-것과-queryfn">📌useQuery 실행 시cacheTime &gt; 0인 것과 queryFn</h2>
<ul>
<li>useQuery 실행 시 cacheTime &gt; 0인 것과 queryFn 실행은 관계 없다. (stateTime과 관련 있음)</li>
<li>cacheTime &gt; 0이면 캐시 데이터가 존재하고, 이 경우 useQuery를 실행하면 stale data를 우선 받고 cacheTime이 0이면 queryFn을 실행한 리턴 값으로 리렌더링하면서 바꿔준다.</li>
<li>cacheTime이 0이 되면 캐시 데이터가 삭제되기 때문에 useQuery로 data 호출 시 undefined 값을 우선 받고 queryFn을 실행한 리턴 값으로 리렌더링 하면서 바꿔준다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 바탕화면 정리]]></title>
            <link>https://velog.io/@_soul_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B0%94%ED%83%95%ED%99%94%EB%A9%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@_soul_/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%B0%94%ED%83%95%ED%99%94%EB%A9%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 21 Dec 2023 12:04:49 GMT</pubDate>
            <description><![CDATA[<h2 id="바탕화면-정리"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/161990">바탕화면 정리</a></h2>
<blockquote>
<h4 id="문제-설명">문제 설명</h4>
<p>코딩테스트를 준비하는 머쓱이는 프로그래머스에서 문제를 풀고 나중에 다시 코드를 보면서 공부하려고 작성한 코드를 컴퓨터 바탕화면에 아무 위치에나 저장해 둡니다. 저장한 코드가 많아지면서 머쓱이는 본인의 컴퓨터 바탕화면이 너무 지저분하다고 생각했습니다. 프로그래머스에서 작성했던 코드는 그 문제에 가서 다시 볼 수 있기 때문에 저장해 둔 파일들을 전부 삭제하기로 했습니다.<br>
컴퓨터 바탕화면은 각 칸이 정사각형인 격자판입니다. 이때 컴퓨터 바탕화면의 상태를 나타낸 문자열 배열 <code>wallpaper</code>가 주어집니다. 파일들은 바탕화면의 격자칸에 위치하고 바탕화면의 격자점들은 바탕화면의 가장 왼쪽 위를 (0, 0)으로 시작해 (세로 좌표, 가로 좌표)로 표현합니다. 빈칸은 &quot;.&quot;, 파일이 있는 칸은 &quot;#&quot;의 값을 가집니다. 드래그를 하면 파일들을 선택할 수 있고, 선택된 파일들을 삭제할 수 있습니다. 머쓱이는 최소한의 이동거리를 갖는 한 번의 드래그로 모든 파일을 선택해서 한 번에 지우려고 하며 드래그로 파일들을 선택하는 방법은 다음과 같습니다.
<br></p>
</blockquote>
<ul>
<li>드래그는 바탕화면의 격자점 <code>S(lux, luy)</code>를 마우스 왼쪽 버튼으로 클릭한 상태로 격자점 <code>E(rdx, rdy)</code>로 이동한 뒤 마우스 왼쪽 버튼을 떼는 행동입니다. 이때, <strong>&quot;점 S에서 점 E로 드래그한다&quot;</strong>고 표현하고 점 S와 점 E를 각각 드래그의 시작점, 끝점이라고 표현합니다.</li>
<li>점 <code>S(lux, luy)</code>에서 점 <code>E(rdx, rdy)</code>로 드래그를 할 때, <strong>&quot;드래그 한 거리&quot;</strong>는 <code>|rdx - lux| + |rdy - luy|</code>로 정의합니다.</li>
<li>점 S에서 점 E로 드래그를 하면 바탕화면에서 두 격자점을 각각 왼쪽 위, 오른쪽 아래로 하는 직사각형 내부에 있는 모든 파일이 선택됩니다.<br>
예를 들어 `wallpaper` = [".#...", "..#..", "...#."]인 바탕화면을 그림으로 나타내면 다음과 같습니다.
![](https://velog.velcdn.com/images/_soul_/post/b61b3e76-596e-4b43-863a-e5e33a379818/image.png)
이러한 바탕화면에서 다음 그림과 같이 S(0, 1)에서 E(3, 4)로 드래그하면 세 개의 파일이 모두 선택되므로 드래그 한 거리 (3 - 0) + (4 - 1) = 6을 최솟값으로 모든 파일을 선택 가능합니다.
![](https://velog.velcdn.com/images/_soul_/post/ef0d5a4c-59b2-4544-af9b-fccc250e1f63/image.png)
(0, 0)에서 (3, 5)로 드래그해도 모든 파일을 선택할 수 있지만 이때 드래그 한 거리는 (3 - 0) + (5 - 0) = 8이고 이전의 방법보다 거리가 늘어납니다.<br>
머쓱이의 컴퓨터 바탕화면의 상태를 나타내는 문자열 배열 `wallpaper`가 매개변수로 주어질 때 바탕화면의 파일들을 한 번에 삭제하기 위해 최소한의 이동거리를 갖는 드래그의 시작점과 끝점을 담은 정수 배열을 return하는 `solution` 함수를 작성해 주세요. 드래그의 시작점이 `(lux, luy)`, 끝점이 `(rdx, rdy)`라면 정수 배열 `[lux, luy, rdx, rdy]`를 return하면 됩니다.
#### 제한사항</li>
<li>1 ≤ <code>wallpaper</code>의 길이 ≤ 50</li>
<li>1 ≤ <code>wallpaper[i]</code>의 길이 ≤ 50<ul>
<li><code>wallpaper</code>의 모든 원소의 길이는 동일합니다.</li>
</ul>
</li>
<li><code>wallpaper[i][j]</code>는 바탕화면에서 <code>i + 1</code>행 <code>j + 1</code>열에 해당하는 칸의 상태를 나타냅니다.</li>
<li><code>wallpaper[i][j]</code>는 &quot;#&quot; 또는 &quot;.&quot;의 값만 가집니다.</li>
<li>바탕화면에는 적어도 하나의 파일이 있습니다.</li>
<li>드래그 시작점 <code>(lux, luy)</code>와 끝점 <code>(rdx, rdy)</code>는 <code>lux &lt; rdx</code>, <code>luy &lt; rdy</code>를 만족해야 합니다.<br>
#### 입출력 예시
|wallpaper|result|
|-|-|
|[".#...", "..#..", "...#."]|[0, 1, 3, 4]|
|["..........", ".....#....", "......##..", "...##.....", "....#....."]    |[1, 3, 5, 8]|
|[".##...##.", "#..#.#..#", "#...#...#", ".#.....#.", "..#...#..", "...#.#...", "....#...."]|[0, 0, 7, 9]|
|["..", "#."]|[1, 0, 2, 1]|



</li>
</ul>
<h3 id="solution-code">solution code</h3>
<pre><code class="language-javascript">function solution(wallpaper) {
  let x = [];
  let y = [];

  wallpaper.forEach((sub, i) =&gt; {
    sub.split(&quot;&quot;).forEach((e, j) =&gt; {
      if (e === &quot;#&quot;) {
        x.push(i);
        y.push(j);
      }
    });
  });

  x.sort((a, b) =&gt; a - b);
  y.sort((a, b) =&gt; a - b);

  return [x[0], y[0], x.at(-1) + 1, y.at(-1) + 1];
}</code></pre>
<p><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] react-router-dom]]></title>
            <link>https://velog.io/@_soul_/React-react-router-dom</link>
            <guid>https://velog.io/@_soul_/React-react-router-dom</guid>
            <pubDate>Wed, 20 Dec 2023 07:53:12 GMT</pubDate>
            <description><![CDATA[<h2 id="📌react-router-dom-이란">📌react-router-dom 이란?</h2>
<span style="line-height: 2">
: 페이지를 구현할 수 있게 해주는 패키지 <br>
→ SPA (single Page Application)기반인 <span style="font-weight: bold; background-color: #ebd234">React 프로젝트 안에서 여러 개의 페이지를 구현</span>할 수 있다.</span>

<br>

<h2 id="📌react-router-dom-설치하기">📌react-router-dom 설치하기</h2>
<p>: 패키지 설치
<code>yarn add react-router-dom</code>
<img src = "https://velog.velcdn.com/images/_soul_/post/0869fcb8-d2ff-49ab-97af-102102619d1b/image.png" style = "width: 450px" alt = "react-router-dom 설치"/></p>
<br>

<h2 id="📌react-router-dom-사용하기">📌react-router-dom 사용하기</h2>
<h3 id="0-순서">0. 순서</h3>
<ul>
<li>페이지 컴포넌트 생성</li>
<li><code>Router.js</code> 생성 및 router 설정 코드 작성</li>
<li><code>App.js</code>에 import 및 적용</li>
</ul>
<h3 id="1-페이지-컴포넌트-생성-예시">1. 페이지 컴포넌트 생성 (예시)</h3>
<p><code>src</code>폴더에 <code>pages</code> 폴더를 만들고 <code>Home</code> <code>About</code> <code>Contact</code> <code>Works</code> 4개의 컴포넌트 생성
<img src="https://velog.velcdn.com/images/_soul_/post/5b9cdf30-dc0f-413d-bef7-6047336e6aa9/image.png" alt=""></p>
<br>

<h3 id="2-routerjs-생성-및-router-설정-코드-작성">2. <code>Router.js</code> 생성 및 router 설정 코드 작성</h3>
<p>: <span style="color: #e03a31; font-style: italic">브라우저에 url을 입력하고 이동했을 때 우리가 원하는 페이지 컴포넌트로 이동하게 만드는 부분</span>
: url 하나당 1개의 페이지 컴포넌트 매칭 → URL = Route
: 일반적으로 Route 설정 코드는 <code>Router.js</code> 라는 파일을 별도로 분리해서 작성한다.</p>
<h4 id="1-src-폴더-안에-shared-폴더를-생성하고-그-안에-routerjs-파일-생성">(1) <code>src</code> 폴더 안에 <code>shared</code> 폴더를 생성하고 그 안에 <code>Router.js</code> 파일 생성</h4>
<pre><code class="language-javascript">import React from &quot;react&quot;;
//react-router-dom을 사용하기 위해 API import
import { BrowserRouter, Route, Routes } from &quot;react-router-dom&quot;;

const Router = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Routes&gt;&lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default Router;</code></pre>
<blockquote>
<p><span style="font-weight: bold; color: #3154e0"> <span style="color:white; font-weight: normal"><code>&lt;BrowserRouter&gt;</code></span>를 <span style="color:white; font-weight: normal"><code>&lt;Router&gt;</code></span>로 감싸는 이유는 브라우저가 깜빡이지 않고 다른 페이지로 이동할 수 있게 만들어주기 위함!! (SPA의 장점)</span></p>
</blockquote>
<p>Route를 설정할 뼈대 완성!!</p>
<br>

<h4 id="2-페이지-컴포넌트-마다-route-설정하기">(2) 페이지 컴포넌트 마다 Route 설정하기</h4>
<p><code>Router.js</code></p>
<pre><code class="language-javascript">import React from &quot;react&quot;;
//react-router-dom을 사용하기 위해 API import
import { BrowserRouter, Route, Routes } from &quot;react-router-dom&quot;;
import Home from &quot;../pages/Home&quot;;
import About from &quot;../pages/About&quot;;
import Contact from &quot;../pages/Contact&quot;;
import Works from &quot;../pages/Works&quot;;

const Router = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Routes&gt;
        &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
        &lt;Route path=&quot;about&quot; element={&lt;About /&gt;} /&gt;
        &lt;Route path=&quot;contact&quot; element={&lt;Contact /&gt;} /&gt;
        &lt;Route path=&quot;works&quot; element={&lt;Works /&gt;} /&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default Router;</code></pre>
<blockquote>
<p><code>&lt;Route/&gt;</code>에는 react-router-dom에서 지원하는 props가 있어서
  <code>path</code>는 우리가 사용하고 싶은 주소를 넣어주고
  <code>element</code>는 해당 주소로 이동했을 때 보여주고자 하는 컴포넌트를 넣어주면 된다.</p>
</blockquote>
  <br>

<h3 id="3-appjs에-routerjs-import-하기">3. App.js에 Router.js import 하기</h3>
<p><code>App.js</code></p>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import Router from &quot;./shared/Router&quot;;

function App() {
  return &lt;Router /&gt;;
}

export default App;</code></pre>
<blockquote>
<p><code>Router.js</code>를 App 컴포넌트에 넣는 이유는 _<strong>가장 최상위 컴포넌트가 <code>App.js</code></strong>_이기 때문이다.<br>
  어떤 컴포넌트를 화면에 띄우던지 항상 <code>App.js</code>를 거쳐야하므로 path별로 분기되는 <code>Router.js</code>를 <code>App.js</code>에 위치시켜 서비스를 이용하는 모든 사용자가 항상 <code>App.js → Router.js</code>를 거치도록 코드를 구현하는 것이다.</p>
</blockquote>
<p>  <br><br></p>
<h2 id="📌react-router-dom-hooks">📌react-router-dom Hooks</h2>
<p>: react-router-dom에서 제공하는 다양한 hook들 <a href="https://reactrouter.com/en/main/hooks/use-action-data">(공식문서)</a></p>
<h3 id="1-usenavigate">(1) useNavigate</h3>
<p>: <strong>어떤 버튼이나 컴포넌트를 눌렀을 때 페이지를 이동하도록</strong> 하는 훅</p>
<pre><code class="language-javascript">  // src/pages/home.js
import { useNavigate } from &quot;react-router-dom&quot;;

const Home = () =&gt; {
  const navigate = useNavigate();

  return (
    &lt;button
      onClick={() =&gt; {
        navigate(`/works`);
      }}
    &gt;
      works로 이동
    &lt;/button&gt;
  );
};

export default Home;</code></pre>
<blockquote>
<p><code>navigate</code>를 생성하고 <code>navigate(이동하려는 url문자열)</code>을 통해 페이지 이동</p>
</blockquote>
<h3 id="2-uselocation">(2) useLocation</h3>
<p><code>react-router-dom</code>을 사용하면 현재 페이지의 정보들을 추가적으로 얻을 수 있다.</p>
<pre><code class="language-javascript">// src/pages/works.js
import { useLocation } from &quot;react-router-dom&quot;;

const Works = () =&gt; {
  const location = useLocation();
  console.log(&quot;location :&gt;&gt; &quot;, location);
  return (
    &lt;div&gt;
      &lt;div&gt;{`현재 페이지 : ${location.pathname.slice(1)}`}&lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Works;</code></pre>
<img src="https://velog.velcdn.com/images/_soul_/post/14e1da5a-2360-4179-b35d-304ccbd222e4/image.png" style="width: 500px"/>

<p>  <br><br></p>
<h2 id="📌link">📌Link</h2>
<p>: <code>Link</code>는 html 태그 중 <code>a</code>태그의 기능을 대체하는 API이다.</p>
<blockquote>
<p>JSX에서 <code>a</code>태그를 사용하면 페이지를 이동하면서 브라우저가 <strong>새로고침(refresh)</strong>되고, 브라우저가 새로고침되면 <span style="color: #ab290c; font-weight: bold">모든 컴포넌트가 다시 렌더링되어야 하고 <code>redux</code>나 <code>useState</code>를 통해 메모리상에 구축해놓은 모든 상태값이 초기화</span> 된다. 
  → 성능에 악영향줄 수 있는 불필요한 움직임이다</p>
</blockquote>
<pre><code class="language-javascript">import { Link, useLocation } from &#39;react-router-dom&#39;;

const Works = () =&gt; {
  const location = useLocation();
  console.log(&#39;location :&gt;&gt; &#39;, location);
  return (
    &lt;div&gt;
      &lt;div&gt;{`현재 페이지 : ${location.pathname.slice(1)}`}&lt;/div&gt;
      &lt;Link to=&quot;/contact&quot;&gt;contact 페이지로 이동하기&lt;/Link&gt;
    &lt;/div&gt;
  );
};

export default Works;</code></pre>
<p><br><br></p>
<h2 id="📌children-용도">📌children 용도</h2>
<p>공식문서에서 설명하는 props.children</p>
<blockquote>
<p>💡 어떤 컴포넌트들은 어떤 자식 엘리먼트가 들어올지 미리 예상할 수 없는 경우가 있습니다. <strong>범용적인 ‘박스’ 역할</strong>을 하는 Sidebar 혹은 Dialog와 같은 컴포넌트에서 특히 자주 볼 수 있습니다</p>
</blockquote>
<img src = "https://velog.velcdn.com/images/_soul_/post/a0ee164b-537e-4a53-be6b-7b3d0e219961/image.png" style="width: 350px"/>


<p><strong>범용적인 ‘박스’ 역할</strong>을 하는 컴포넌트는 크게 봤을 때 Layout 역할을 하는 컴포넌트라고 생각할 수 있다.</p>
<blockquote>
<p>children props를 이용해 페이지 레이아웃을 만들고 개별적으로 존재하는 Header, Footer, Page를 합성해 개발자가 의도하는 UI를 만들어주는 Layout 컴포넌트를 만들어보자.</p>
</blockquote>
<p><code>src/shared</code>폴더에 <code>Layout.js</code>파일 생성</p>
<pre><code class="language-javascript">  // src/shared/Layout.js

import React from &#39;react&#39;;

const HeaderStyles = {
  width: &#39;100%&#39;,
  background: &#39;black&#39;,
  height: &#39;50px&#39;,
  display: &#39;flex&#39;,
  alignItems: &#39;center&#39;,
  paddingLeft: &#39;20px&#39;,
  color: &#39;white&#39;,
  fontWeight: &#39;600&#39;,
};
const FooterStyles = {
  width: &#39;100%&#39;,
  height: &#39;50px&#39;,
  display: &#39;flex&#39;,
  background: &#39;black&#39;,
  color: &#39;white&#39;,
  alignItems: &#39;center&#39;,
  justifyContent: &#39;center&#39;,
  fontSize: &#39;12px&#39;,
};

const layoutStyles = {
  display: &#39;flex&#39;,
    flexDirection: &#39;column&#39;,
  justifyContent: &#39;center&#39;,
  alignItems: &#39;center&#39;,
  minHeight: &#39;90vh&#39;,
}

function Header() {
  return (
    &lt;div style={{ ...HeaderStyles }}&gt;
      &lt;span&gt;Let&#39;s learn React&lt;/span&gt;
    &lt;/div&gt;
  );
}

function Footer() {
  return (
    &lt;div style={{ ...FooterStyles }}&gt;
      &lt;span&gt;Have a Nice Day!&lt;/span&gt;
    &lt;/div&gt;
  );
}


function Layout({ children }) {
  return (
    &lt;div&gt;
      &lt;Header /&gt;
      &lt;div style={{...layoutStyles}}&gt;
        {children}
      &lt;/div&gt;
      &lt;Footer /&gt;
    &lt;/div&gt;
  );
}

export default Layout;</code></pre>
<p><code>Router.js</code> 수정 (<code>&lt;Routes&gt;</code> 컴포넌트를 <code>&lt;Layout&gt;</code> 컴포넌트로 감싸준다)</p>
<pre><code class="language-javascript">import React from &#39;react&#39;;
import { BrowserRouter, Route, Routes } from &#39;react-router-dom&#39;;
import Home from &#39;../pages/Home&#39;;
import About from &#39;../pages/About&#39;;
import Contact from &#39;../pages/Contact&#39;;
import Works from &#39;../pages/Works&#39;;
import Layout from &#39;./Layout&#39;;

const Router = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Layout&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
          &lt;Route path=&quot;about&quot; element={&lt;About /&gt;} /&gt;
          &lt;Route path=&quot;contact&quot; element={&lt;Contact /&gt;} /&gt;
          &lt;Route path=&quot;works&quot; element={&lt;Works /&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/Layout&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default Router;</code></pre>
<img src="https://velog.velcdn.com/images/_soul_/post/0047ac58-09f8-40f8-8098-0159c61952f2/image.png" style="width: 500px"/>


<p>  <br><br></p>
<h2 id="📌dynamic-routes">📌Dynamic Routes</h2>
<p>: 동적 라우팅. path에 유동적인 값을 넣어서 특정 페이지로 이동하게끔 하는 방법을 말한다.</p>
<blockquote>
<p>예를 들어 works 페이지에 여러 개의 work가 보이고 work마다 독립적인 페이지를 갖도록 구현한다면?!</p>
</blockquote>
<p><code>Router.js</code>에서 Dynamic Routes를 설정한다.</p>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import { BrowserRouter, Route, Routes } from &quot;react-router-dom&quot;;
import Home from &quot;../pages/Home&quot;;
import About from &quot;../pages/About&quot;;
import Contact from &quot;../pages/Contact&quot;;
import Works from &quot;../pages/Works&quot;;
import Layout from &quot;./Layout&quot;;

const Router = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Layout&gt;
        &lt;Routes&gt;
          &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
          &lt;Route path=&quot;about&quot; element={&lt;About /&gt;} /&gt;
          &lt;Route path=&quot;contact&quot; element={&lt;Contact /&gt;} /&gt;
          &lt;Route path=&quot;works&quot; element={&lt;Works /&gt;} /&gt;
          &lt;Route path=&quot;works/:id&quot; element={&lt;Works /&gt;} /&gt;    //Dynamic Route
        &lt;/Routes&gt;
      &lt;/Layout&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default Router;</code></pre>
<p>이전과는 다르게 <code>works/:id</code> 라고 path가 들어갑니다. <code>:id</code>는 동적인 값을 받겠다라는 의미이고 works/1 로 이동해도 <Work /> 로 이동하고, works/2, works/3 …. works/100 모두 <Work /> 로 이동하게 해준다.</p>
<blockquote>
<p><code>:id</code>는 <strong>useParams 훅에서 조회할 수 있는 값</strong>이 된다.</p>
</blockquote>
<img src ="https://velog.velcdn.com/images/_soul_/post/e2e512ac-3504-42a0-a7f5-fd426dc7c27c/image.png" style="width: 500px"/>




<p>  <br><br></p>
<h2 id="📌useparamㄴ">📌useParamㄴ</h2>
<p>: Dynamic Routes를 사용하면 element에 설정된 같은 컴포넌트를 렌더링 하게 된다.</p>
<pre><code class="language-javascript">&lt;Route path=&quot;works/:id&quot; element={&lt;Work /&gt;} /&gt;</code></pre>
<p><code>useParams</code>을 이용하면 같은 컴포넌트를 렌더링 하더라도 각각의 고유한 <code>id</code>값을 조회할 수 있다.</p>
<ul>
<li><code>Works.js</code> 수정<pre><code class="language-javascript">// src/pages/Works.js
</code></pre>
</li>
</ul>
<p>import React from &#39;react&#39;;
import { Link } from &#39;react-router-dom&#39;;</p>
<p>const data = [
  { id: 1, todo: &#39;리액트 배우기&#39; },
  { id: 2, todo: &#39;노드 배우기&#39; },
  { id: 3, todo: &#39;자바스크립트 배우기&#39; },
  { id: 4, todo: &#39;파이어 베이스 배우기&#39; },
  { id: 5, todo: &#39;넥스트 배우기&#39; },
  { id: 6, todo: &#39;HTTP 프로토콜 배우기&#39; },
];</p>
<p>function Works() {
  return (
    <div>
      {data.map((work) =&gt; {
        return (
          <div key={work.id}>
            <div>할일: {work.id}</div>
            &lt;Link to={<code>/works/${work.id}</code>}&gt;
              &lt;span style={{ cursor: &#39;pointer&#39; }}&gt;➡️ Go to: {work.todo}</span>
            </Link>
          </div>
        );
      })}
    </div>
  );
}</p>
<p>export default Works;</p>
<pre><code>* `&lt;Work&gt;` 컴포넌트 생성
```javascript
// src/pages/Work.js

import React from &#39;react&#39;;
import { useParams } from &#39;react-router-dom&#39;;

const data = [
  { id: 1, todo: &#39;리액트 배우기&#39; },
  { id: 2, todo: &#39;노드 배우기&#39; },
  { id: 3, todo: &#39;자바스크립트 배우기&#39; },
  { id: 4, todo: &#39;파이어 베이스 배우기&#39; },
  { id: 5, todo: &#39;넥스트 배우기&#39; },
  { id: 6, todo: &#39;HTTP 프로토콜 배우기&#39; },
];

function Work() {
  const param = useParams();

  const work = data.find((work) =&gt; work.id === parseInt(param.id));

  return &lt;div&gt;{work.todo}&lt;/div&gt;;
}

export default Work;</code></pre><ul>
<li><code>Router.js</code> 수정<pre><code class="language-javascript">import React from &quot;react&quot;;
import { BrowserRouter, Route, Routes } from &quot;react-router-dom&quot;;
import Home from &quot;../pages/Home&quot;;
import About from &quot;../pages/About&quot;;
import Contact from &quot;../pages/Contact&quot;;
import Works from &quot;../pages/Works&quot;;
import Work from &quot;../pages/Work&quot;;
import Layout from &quot;./Layout&quot;;
</code></pre>
</li>
</ul>
<p>const Router = () =&gt; {
  return (
    <BrowserRouter>
      <Layout>
        <Routes>
          &lt;Route path=&quot;/&quot; element={<Home />} /&gt;
          &lt;Route path=&quot;about&quot; element={<About />} /&gt;
          &lt;Route path=&quot;contact&quot; element={<Contact />} /&gt;
          &lt;Route path=&quot;works&quot; element={<Works />} /&gt;
          &lt;Route path=&quot;works/:id&quot; element={<Work />} /&gt;
        </Routes>
      </Layout>
    </BrowserRouter>
  );
};</p>
<p>export default Router;</p>
<pre><code></code></pre>]]></description>
        </item>
    </channel>
</rss>