<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mini.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 12 Aug 2024 12:09:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. mini.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mini-woong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[240812 TIL]]></title>
            <link>https://velog.io/@mini-woong/cpls55g4</link>
            <guid>https://velog.io/@mini-woong/cpls55g4</guid>
            <pubDate>Mon, 12 Aug 2024 12:09:18 GMT</pubDate>
            <description><![CDATA[<h3 id="join으로-데이터-받아오기">join으로 데이터 받아오기</h3>
<p>기존의 channel_id와 channel 정보를 따로 받아오는 방식에서 join으로 변경해주었다.</p>
<pre><code>    const { data, error } = await supabase
      .from(&#39;chat_subscribe&#39;)
      .select(`
        created_at,
        channel_id,
        chat_channels(
          *
        )
        `)
      .eq(&#39;user_id&#39;, user_id)</code></pre><hr>

<h3 id="채팅목록-실시간으로-업데이트">채팅목록 실시간으로 업데이트</h3>
<p>채팅의 last message도 실시간으로 업데이트 되게 하였다.</p>
<pre><code>  const getlastMessage = async (channel_id: number, owner_id: string): Promise&lt;{ time: string, message: string } | null&gt; =&gt; {
    //유저의 마지막 대화 불러오기
    const user_id = await userdata.id;
    if (owner_id === user_id) {
      //채널주
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .eq(&quot;channel_id&quot;, channel_id)
        .order(&#39;created_at&#39;, { ascending: false })
        .limit(1)
        .maybeSingle();
      if (!data) {
        return { time: &#39;&#39;, message: &#39;대화를 시작해보세요&#39; }
      } else {
        const message = JSON.parse(JSON.stringify(data.content)).message as string;
        const time = data.created_at.slice(11, 16) as string;
        return { time, message }
      }
    } else {
      //팬
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .eq(&quot;channel_id&quot;, channel_id)
        .in(&quot;user_id&quot;, [user_id, owner_id])
        .order(&#39;created_at&#39;, { ascending: false })
        .limit(1)
        .maybeSingle();
      if (!data) {
        return { time: &#39;&#39;, message: &#39;대화를 시작해보세요&#39; }
      } else {
        const message = JSON.parse(JSON.stringify(data.content)).message as string;
        const time = data.created_at.slice(11, 16) as string;
        return { time, message }
      }
    }
  }

  //실시간
  const receiveChatMessage = async () =&gt; {
    const user_id = await userdata.id;
    const channels = supabase
      .channel(&quot;changes&quot;)
      .on(
        &quot;postgres_changes&quot;,
        {
          event: &quot;INSERT&quot;,
          schema: &quot;public&quot;,
          table: &quot;chat_messages&quot;,
          filter: `channel_id=in.(${channelIdList})`
        },
        (payload) =&gt; {
          const newMessage = payload.new;
          const message = JSON.parse(JSON.stringify(newMessage.content)).message as string;
          const time = newMessage.created_at.slice(11, 16) as string;
          if (myChannel &amp;&amp; newMessage.channel_id === myChannel.channel_id) {
            setMyChannel({
              ...myChannel,
              created_at: time,
              message: message
            })
          } else {
            if (chatListData) {
              setChatListData(chatListData?.map((pre) =&gt; {
                if (pre) {
                  if (pre?.channel_id === newMessage.channel_id &amp;&amp; (newMessage.user_id === user_id || newMessage.user_id === pre?.owner_id)) {
                    return {
                      ...pre,
                      created_at: time,
                      message: message
                    }
                  } else {
                    return pre;
                  }
                }
              }))
            }
          }
        }
      )
      .subscribe();
  };</code></pre><hr>

<h3 id="created_at-기준으로-필터링">created_at 기준으로 필터링</h3>
<p>내가 채팅을 구독한 시점을 받아와 메세지 필터링에 적용시켜주었다.</p>
<pre><code>  const checkMySubCreatedAtTime = async (): Promise&lt;String | null&gt; =&gt; {
    const user_id = await userdata.id;
    const { data, error } = await supabase
      .from(&quot;chat_subscribe&quot;)
      .select(&quot;created_at&quot;)
      .eq(&quot;channel_id&quot;, channel_id)
      .eq(&quot;user_id&quot;, user_id)
      .single();
    if (error) {
      console.log(error);
      return null;
    } else {
      return data.created_at;
    }
  };</code></pre><p>생성 시점을 반환해주고</p>
<pre><code>      const created_at_time = await checkMySubCreatedAtTime();
      const { data, error } = await supabase
        .from(&quot;chat_messages&quot;)
        .select(&quot;*&quot;)
        .in(&quot;user_id&quot;, [user_id, influ_id])
        .eq(&quot;channel_id&quot;, channel_id)
        .order(&#39;created_at&#39;, { ascending: true })
        .gt(&#39;created_at&#39;, created_at_time);</code></pre><p>필터링에 적용시켜준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240808 TIL]]></title>
            <link>https://velog.io/@mini-woong/mdchzd5d</link>
            <guid>https://velog.io/@mini-woong/mdchzd5d</guid>
            <pubDate>Fri, 09 Aug 2024 02:00:21 GMT</pubDate>
            <description><![CDATA[<p>채팅 구독 기능
처음 대화를 시작할 수 있는 버튼을 우선 리스트에 임시로 만들었다.
<img src="https://velog.velcdn.com/images/mini-woong/post/224af7d5-b7b7-492c-a0da-c3114c564999/image.png" alt=""></p>
<pre><code>  const checkOwnerChannel = async (): Promise&lt;number | null&gt; =&gt; {
    const { data, error } = await supabase
      .from(&quot;chat_channels&quot;)
      .select(&quot;channel_id&quot;)
      .eq(&quot;owner_id&quot;, owner_id)
      .maybeSingle();
    if (error) {
      console.log(error);
      return null;
    }
    if (!data) {
      console.log(&#39;채널이 존재하지 않습니다.&#39;)
      return null;
    } else {
      return data.channel_id;
    }
  };

  const startChat = async () =&gt; {
    //유저의 대화구독목록 불러오기
    const user_id = userdata.id
    const channel_id = await checkOwnerChannel();
    if (channel_id &amp;&amp; user_id != undefined) {
      const { data, error } = await supabase
        .from(&#39;chat_subscribe&#39;)
        .select(&#39;chat_subscribe_id&#39;)
        .eq(&#39;channel_id&#39;, channel_id)
        .eq(&#39;user_id&#39;, user_id)
        .maybeSingle();
      if (!data) {
        createNewSubscribe(channel_id, user_id)
      }
    }
    router.push(`/chatlist/[${channel_id}]`)
  }

  const createNewSubscribe = async (channel_id: number, user_id: string) =&gt; {
    const { data, error } = await supabase
      .from(&#39;chat_subscribe&#39;)
      .insert({
        channel_id: channel_id,
        user_id: user_id
      })
    if (error)
      console.log(error)
  }</code></pre><p>채팅 구독이 되어있지 않을 시 구독 관계를 형성해주고, 채팅창으로 이동한다.</p>
<p>줄바꿈 문제 해결
채팅방에 엔터 입력 시 적용이 되지 않았다. whitespace-pre-wrap으로 해결.
영어의 경우 띄어쓰기 없이 길게 쓸 경우 줄바꿈이 되지 않았다. break-all로 해결.
문장분호도 역시 줄바꿈 적용이 되지 않았는데 이는 tailwind로 해결할 수 없어서... global css로 셋을 한번에 설정해주었다. overflow-wrap anywhere로 해결.</p>
<pre><code>.chat {
  overflow-wrap: anywhere;
  word-break: break-all;
  white-space: pre-wrap;
}</code></pre><p><img src="https://velog.velcdn.com/images/mini-woong/post/745485e3-c949-4664-b4d8-c3e558968914/image.png" alt=""><del>(고민의 흔적들... ㅋㅋㅋㅋ)</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240807 TIL]]></title>
            <link>https://velog.io/@mini-woong/w6yhusk6</link>
            <guid>https://velog.io/@mini-woong/w6yhusk6</guid>
            <pubDate>Fri, 09 Aug 2024 00:53:19 GMT</pubDate>
            <description><![CDATA[<h3 id="채팅-목록-구성">채팅 목록 구성</h3>
<h4 id="1-마지막-메세지-불러오기">1. 마지막 메세지 불러오기</h4>
<p>각 채팅방 마다 지난 채팅 중 가장 마지막 메세지를 불러와 화면에 띄워주었다.
문제 1 : map 내부에서 async-await 호출 시 Promise array 해결
Promise 배열을 어떻게 처리할 수 있는 걸까, 하다 Promise.all 이라는 것을 발견했다. 순회 가능한 객체에 주어진 모든 프로미스를 이행하고, 이행한 프로미스를 반환해주어 허무하게도(...) 쉽게 배열 처리가 되었다.</p>
<pre><code>
  const getChatList = async () =&gt; {
    //유저의 대화구독목록 불러오기
    const user_id = userdata.id
    const { data, error } = await supabase
      .from(&#39;chat_subscribe&#39;)
      .select(&#39;*&#39;)
      .eq(&#39;user_id&#39;, user_id)
    if (data) {
      const channelListData = data.map((channel) =&gt; {
        return channel.channel_id
      })
      setChannelList(channelListData)
    }
  }

  const getChatListData = async (chatlist: number[] | null) =&gt; {
    //구독리스트의 정보 불러오기
    if (channelList) {
      const { data, error } = await supabase
        .from(&#39;chat_channels&#39;)
        .select(&#39;*&#39;)
        .in(&#39;channel_id&#39;, [...channelList])
      if (data) {
        const channelListDatas = await Promise.all(data.map(async (channel) =&gt; {
          const response = await getlastMessage(channel.channel_id, channel.owner_id);
          if (response?.time != undefined &amp;&amp; response?.message != undefined) {
            return {
              channel_id: channel.channel_id,
              channel_name: channel.channel_name,
              owner_id: channel.owner_id,
              owner_profile_url: channel.owner_profile_url,
              created_at: response?.time,
              message: response?.message
            }
          }
        }))
        setChatListData(channelListDatas);
      }
    }
  }</code></pre><p>문제 2 : 대화 기록 없을 수 get 에러
메세지가 없을 시에는 대화를 시작하세요 라는 메세지를 띄워주고자 했다. 그러나, 계속 get 400 오류가 콘솔에 찍히는 것이 확인되었다. 대화 기록이 없는 경우는 언제든 발생할 수 있기 때문에 콘솔이 찍히지 않도록 하고 싶었다.
문제가 되는 것은 single() 이었다. 반드시 하나를 받아와야 했던 것. maybeSingle로 해결할 수 있었다.</p>
<pre><code>
  const getlastMessage = async (channel_id: number, owner_id: string): Promise&lt;{ time: string, message: string } | null&gt; =&gt; {
    //유저의 마지막 대화 불러오기
    //실시간
    const user_id = await userdata.id;
    const approve = await userdata.approve;
    if (owner_id === user_id) {
      //채널주
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .eq(&quot;channel_id&quot;, channel_id)
        .order(&#39;created_at&#39;, { ascending: false })
        .limit(1)
        .maybeSingle();
      if (!data) {
        return { time: &#39;&#39;, message: &#39;대화를 시작해보세요&#39; }
      } else {
        const message = JSON.parse(JSON.stringify(data.content)).message as string;
        const time = data.created_at.slice(11, 16) as string;
        return { time, message }
      }
    } else {
      //팬
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .eq(&quot;channel_id&quot;, channel_id)
        .in(&quot;user_id&quot;, [user_id, owner_id])
        .order(&#39;created_at&#39;, { ascending: false })
        .limit(1)
        .maybeSingle();
      if (!data) {
        return { time: &#39;&#39;, message: &#39;대화를 시작해보세요&#39; }
      } else {
        const message = JSON.parse(JSON.stringify(data.content)).message as string;
        const time = data.created_at.slice(11, 16) as string;
        return { time, message }
      }</code></pre><h4 id="2-devide-y-tailwindcss">2. devide-y (tailwindcss)</h4>
<p>채팅방을 구분해주는 선을 devide-y를 통해 간편하게 구성할 수 있었다.
<img src="https://velog.velcdn.com/images/mini-woong/post/9a7ed25b-0e01-4766-915e-b6afac4dccf2/image.png" alt=""></p>
<h4 id="3-ui">3. UI</h4>
<p>요구된 디자인 명세에 맞춰 채팅방 UI를 구성했다.
<img src="https://velog.velcdn.com/images/mini-woong/post/7f19c549-18c4-4afe-b46b-6bff927e7b5a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240806 TIL]]></title>
            <link>https://velog.io/@mini-woong/39q3q7y4</link>
            <guid>https://velog.io/@mini-woong/39q3q7y4</guid>
            <pubDate>Fri, 09 Aug 2024 00:33:03 GMT</pubDate>
            <description><![CDATA[<p>채팅 내부 UI 설정
채팅 디자인의 꼬리 부분 (직각 삼각형)</p>
<ol>
<li>tailwindcss의 border를 이용한 삼각형
border를 사용하여 삼각형을 만들 수 있었다. 하지만 border 자체로 만드는 것이기 때문에 내외부 색상을 다르게 할 수 없었다.</li>
<li>rotate
사각형을 만들고 45도를 돌리는 방법도 있었지만 원하는 디자인과 맞지 않았다.</li>
<li>Image
그 부분만 사진으로 받아와 붙여주는 방법으로 결정하였다.
<img src="https://velog.velcdn.com/images/mini-woong/post/bd8c9ee2-8c26-4f2b-b54b-922eec7cf526/image.png" alt="">시도 1 : 직각 삼각형의 테두리가 모두 있는 사진을 받아와 사용했다. 말풍선과의 연결부분 경계선이 지워지지 않는 문제점이 발생했다.
<img src="https://velog.velcdn.com/images/mini-woong/post/9f38b2af-b2ab-4e0f-86f1-94884a8dc74a/image.png" alt="">시도 2 : 한쪽을 없애고 z 인덱스를 높게 주어서 겹쳐 주었다.
<img src="https://velog.velcdn.com/images/mini-woong/post/cd521a52-0d32-412b-bfb6-90af348e96e0/image.png" alt=""></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[240805 TIL]]></title>
            <link>https://velog.io/@mini-woong/xr2jdg1n</link>
            <guid>https://velog.io/@mini-woong/xr2jdg1n</guid>
            <pubDate>Mon, 05 Aug 2024 12:06:29 GMT</pubDate>
            <description><![CDATA[<p>중간 발표
이번에도 발표를 담당하여 중간 보고를 마쳤다.
발표는 매번 할 때마다 하고 나면 진이 빠진다... (ㅋㅋㅋㅋ)</p>
<p>MVP 스펙으로는 상품 등록, 판매 그리고 채팅 기능을 설명했다. 팀원분들 모두 열심히 해주셔서 뿌듯함과 함께 발표를 진행했던 것 같다.
조금 아쉽게도... 노트북이 좀 <del>구려서</del> (ㅋㅋㅋ) 여러 응용 프로그램을 동시에 켜두니 렉이... ㅜㅜ 시연 때 <del>많이</del>조금 당황했지만, 그래도 무사히 발표를 마칠 수 있었다.
남은 프로젝트 기간도 파이팅!</p>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/669374dd-a2b6-4bcd-ad58-f2ba6bc754af/image.png" alt=""><img src="https://velog.velcdn.com/images/mini-woong/post/a8a706b2-325e-43f0-b8c5-8ea36af50d34/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240802 TIL]]></title>
            <link>https://velog.io/@mini-woong/3fna18rd</link>
            <guid>https://velog.io/@mini-woong/3fna18rd</guid>
            <pubDate>Fri, 02 Aug 2024 13:13:58 GMT</pubDate>
            <description><![CDATA[<h3 id="채팅-목록-불러오기---인플루언서의-내-채팅방-가져오기">채팅 목록 불러오기 - 인플루언서의 내 채팅방 가져오기</h3>
<pre><code>  const getMyChannel = async () =&gt; {
    //나의 채팅 채널 불러오기
    const user_id = userdata.id
    const { data, error } = await supabase
      .from(&#39;chat_channels&#39;)
      .select(&#39;*&#39;)
      .eq(&#39;owner_id&#39;, user_id)
      .single()
    if (data) {
      const channel_data = {
        channel_id: data.channel_id,
        channel_name: data.channel_name,
        created_at: data.created_at,
        owner_id: data.owner_id
      }
      setMyChannel(channel_data)
    }
  }</code></pre><p><img src="https://velog.velcdn.com/images/mini-woong/post/d9fee8c0-81e3-4e72-acc0-9b9598ad12dd/image.png" alt=""></p>
<h3 id="스크롤-조정">스크롤 조정</h3>
<pre><code>  const [firstLoading, setFirstLoading] = useState&lt;boolean&gt;(false)
  const scrollToBottom = () =&gt; {
    if (scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
      if (!firstLoading) {
        setFirstLoading(true)
      } else {
        scrollRef.current.style.scrollBehavior = &#39;smooth&#39;;
      }
    }
  }

  useEffect(() =&gt; {
    scrollToBottom();
  }, [preMessages])</code></pre><p>처음 입장 시, 스크롤이 아래에 고정되게 한다.</p>
<h3 id="메세지-구분하여-좌우-배치">메세지 구분하여 좌우 배치</h3>
<pre><code>          (preMessage.isMine) ?
            &lt;div className=&quot;text-right&quot;&gt;
              &lt;label className=&quot;text-xs text-inherit&quot;&gt;{preMessage.time.slice(11, 16)}&lt;/label&gt;
              &lt;label className=&quot;&quot;&gt;{preMessage.content.message}&lt;/label&gt;
            &lt;/div&gt;
            :
            &lt;div className=&quot;text-left&quot;&gt;
              &lt;label className=&quot;&quot;&gt;{preMessage.content.message}&lt;/label&gt;
              &lt;label className=&quot;text-xs text-inherit&quot;&gt;{preMessage.time.slice(11, 16)}&lt;/label&gt;
            &lt;/div&gt;</code></pre><p>preMessage에 Boolean 값인 isMine을 추가하여 구분해주었다.</p>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/6a09b3e9-e7b1-4a19-b268-ed8efe035c84/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240731 TIL]]></title>
            <link>https://velog.io/@mini-woong/240731-TIL</link>
            <guid>https://velog.io/@mini-woong/240731-TIL</guid>
            <pubDate>Wed, 31 Jul 2024 11:55:26 GMT</pubDate>
            <description><![CDATA[<h3 id="supabase-realtime-실시간-메세지-받기">supabase realtime 실시간 메세지 받기</h3>
<pre><code>  const checkChannelOwner = async (channel_id: number): Promise&lt;String | null&gt; =&gt; {
    const { data, error } = await supabase
      .from(&#39;chat_channels&#39;)
      .select(&#39;owner_id&#39;)
      .eq(&#39;channel_id&#39;, channel_id)
      .single()
    if (error) {
      console.log(error);
      return null;
    }
    else {
      console.log(data.owner_id);
      return data.owner_id;
    }
  }</code></pre><p>채널 정보를 통해 채널의 소유주가 누구인지 반환해준다</p>
<pre><code>  const receiveChatMessage = async (channel_id: number) =&gt; {
    const user_id = await userdata.id
    const approve = await userdata.approve
    const owner_id = await checkChannelOwner(channel_id);
    if (approve &amp;&amp; owner_id == user_id) {
      console.log(&#39;인플루언서 본인의 채팅방입니다.&#39;)
      const channelInflu = supabase
      .channel(&#39;changes&#39;)
      .on(
        &#39;postgres_changes&#39;,
        {
          event: &#39;INSERT&#39;,
          schema: &#39;public&#39;,
          table: &#39;chat_messages&#39;,
          filter: `channel_id=eq.${channel_id}`,
        },
        (payload) =&gt; console.log(payload)
      )
      .subscribe()
    } else {
      console.log(&#39;팬 채팅방입니다.&#39;)
      const channelFan = supabase
      .channel(&#39;changes&#39;)
      .on(
        &#39;postgres_changes&#39;,
        {
          event: &#39;INSERT&#39;,
          schema: &#39;public&#39;,
          table: &#39;chat_messages&#39;,
          filter: `channel_id=eq.${channel_id}`,
        },
        (payload) =&gt; console.log(payload)
      )
      .subscribe()
    }

  }
</code></pre><p>본인의 채팅방인지 확인해주고 실시간으로 받는 메세지를 구분해준다.
문제점 : 다중 열 필터 기능이 아직 개발되지 않은 문제로 실시간 값을 받은 후 이차 필터링을 해줄 것
추후 보완수정 작업을 거칠 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240730 TIL]]></title>
            <link>https://velog.io/@mini-woong/s11oorii</link>
            <guid>https://velog.io/@mini-woong/s11oorii</guid>
            <pubDate>Tue, 30 Jul 2024 13:21:28 GMT</pubDate>
            <description><![CDATA[<h3 id="메세지-보내기">메세지 보내기</h3>
<pre><code>  const sendChatMessage = async () =&gt; {
    const user_id = userdata.id
    const channel_id = 1;
    //유저가 해당 채팅방을 구독하고 있는지 확인하는 함수 필요
    console.log(message)
    const content = JSON.stringify({
      message : message
    });

    const { data, error } = await supabase
      .from(&#39;chat_messages&#39;)
      .insert({
        channel_id: channel_id,
        content:  JSON.parse(content),
        user_id: user_id
      })
      if (error) {
        console.log(&#39;채팅 보내기 실패&#39;)
        console.log(error)
      }
  }</code></pre><p><img src="https://velog.velcdn.com/images/mini-woong/post/3f175be3-3195-4d1d-83b4-7409f5f6fb78/image.png" alt="">메세지를 입력하고 채팅 보내기를 누르면
<img src="https://velog.velcdn.com/images/mini-woong/post/7170b015-a3e8-4d68-8094-486c603cfeeb/image.png" alt="">콘솔에 입력된 메세지가 잘 찍히는 것을 확인할 수 있다.
supabase로도 데이터가 잘 들어간다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240729 TIL]]></title>
            <link>https://velog.io/@mini-woong/240729-TIL</link>
            <guid>https://velog.io/@mini-woong/240729-TIL</guid>
            <pubDate>Mon, 29 Jul 2024 14:12:42 GMT</pubDate>
            <description><![CDATA[<h3 id="채팅-목록-불러오기">채팅 목록 불러오기</h3>
<pre><code>  const getChatList = async () =&gt; {
    //유저의 대화구독목록 불러오기
  const user_id = userdata.id
  console.log(user_id)
    const { data, error } = await supabase
      .from(&#39;chat_subscribe&#39;)
      .select(&#39;*&#39;)
      .eq(&#39;user_id&#39;, user_id)
    if (data) {
      const channelListData = data.map((channel) =&gt; {
        return channel.channel_id
      })
      setChannelList(channelListData)
    }
  }

  const getChatListData = async (chatlist: number[] | null) =&gt; {
    //구독리스트의 정보 불러오기
    // const user_id = userdata.id
    // console.log(user_id)
    if (channelList) {
      const { data, error } = await supabase
        .from(&#39;chat_channels&#39;)
        .select(&#39;*&#39;)
        .in(&#39;channel_id&#39;, [...channelList])
      if (data) {
        console.log(data)
        const channelListDatas = data.map((channel) =&gt; {
          return {
            channel_id: channel.channel_id,
            channel_name: channel.channel_name,
            created_at: channel.created_at,
            owner_id: channel.owner_id
          }
        })
        setChatListData(channelListDatas)
      }
    }
  }

  useEffect(() =&gt; {
    if (userdata != undefined) getChatList();
  }, [userdata])
  useEffect(()=&gt;{
    getChatListData(channelList);
  },[channelList])


  return (
    &lt;&gt;
      {chatListData?.map((channel) =&gt; (
        &lt;div key={channel.channel_id} className=&quot;category-item text-center p-1  min-w-[100px]&quot;&gt;
          &lt;p className=&quot;text-sm font-normal&quot;&gt;{channel.channel_name}&lt;/p&gt;
          &lt;button&gt;{channel.channel_name}의 채팅방 입장하기&lt;/button&gt;
        &lt;/div&gt;
      ))}
    &lt;/&gt;
  )</code></pre><p>유저가 구독하고 있는 채팅방을 불러와서 리스트를 화면에 출력하였다.
<img src="https://velog.velcdn.com/images/mini-woong/post/95bba0ef-3905-4096-87af-90dfbbeed2c4/image.png" alt=""></p>
<h3 id="채팅-메세지-기록-불러오기">채팅 메세지 기록 불러오기</h3>
<pre><code>  const getChatMessages = async () =&gt; {

    const user_id = userdata.id
    const approve = userdata.approve
    if (approve) {
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .eq(&#39;channel_id&#39;, 1)
      console.log(data)
    } else {
      const influ_id = &#39;e717cc1d-16de-43c6-a90b-a567022b48e0&#39;
      const { data, error } = await supabase
        .from(&#39;chat_messages&#39;)
        .select(&#39;*&#39;)
        .in(&#39;user_id&#39;, [user_id, influ_id])
        .eq(&#39;channel_id&#39;, 1)
      console.log(data)
    }
  }</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[240725 TIL]]></title>
            <link>https://velog.io/@mini-woong/vbf8lfin</link>
            <guid>https://velog.io/@mini-woong/vbf8lfin</guid>
            <pubDate>Thu, 25 Jul 2024 13:20:32 GMT</pubDate>
            <description><![CDATA[<h3 id="타입-자동-생성">타입 자동 생성</h3>
<pre><code>supabase gen types typescript --linked &gt; src/types/supabase.ts</code></pre><p>위 명령어를 통해 자동으로 데이터베이스의 타입을 생성할 수 있었다. 번거로움을 덜기 위해 package.json 파일에 등록해두고 사용하였다.</p>
<h4 id="라우터-설정">라우터 설정</h4>
<p>supabase에 저장된 지난 채팅 데이터를 가져오기 위해 라우터 설정을 진행하였...으나
계속 오류가 발생하고 있는 문제가 😰😰
다시 처음부처 코드를 짚어봐야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240724 TIL]]></title>
            <link>https://velog.io/@mini-woong/h5vti4dz</link>
            <guid>https://velog.io/@mini-woong/h5vti4dz</guid>
            <pubDate>Wed, 24 Jul 2024 11:47:27 GMT</pubDate>
            <description><![CDATA[<h3 id="supabase-table-구성">supabase table 구성</h3>
<p>일대다 소통 방식의 채팅이다 보니 테이블 설정을 어떤 방식으로 해야할까 고민이 있었다. 인플루언서의 입장에서는 자신의 채팅을 구독하고 있는 모든 유저의 메세지가 보여야하고, 팬 유저의 입장에서는 자신과 자신이 구독한 인플루언서의 메세지만 보여야하기 때문에 이를 필터해서 받아올 때를 고려해야 했다.</p>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/284869b3-fe51-49cb-9e8c-4ee3dda557ce/image.png" alt=""></p>
<p>결론적으로 위와 같은 테이블 구조로 설정하였다.
인플루언서의 입장에서, channels 테이블에서 하나의 channel_id를 가진다. messages 테이블에서 자신의 channel_id를 가진 row를 불러와 팬 유저의 모든 메세지와, 자신의 지난 메세지를 불러온다.
팬 유저의 입장에서 채팅을 시작하면 subcribe 테이블에 해당 channel의 id와 자신의 id가 저장된다. messages 테이블에서 해당 channel_id 중, channel의 owner_id와 자신의 user_id를 필터링하여 지난 메세지를 불러온다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240722 TIL]]></title>
            <link>https://velog.io/@mini-woong/1r89zob2</link>
            <guid>https://velog.io/@mini-woong/1r89zob2</guid>
            <pubDate>Mon, 22 Jul 2024 12:04:04 GMT</pubDate>
            <description><![CDATA[<p>supabase realtime은 broadcast를 지원하여 단체 채팅 기능 구현을 가능하게 한다. 이를 이용하여 일대다 채팅방을 구현할 수 있을 것 같다.</p>
<h4 id="문제점-분석">문제점 분석</h4>
<ul>
<li>일대다 채팅방으로 &#39;일&#39;에 해당하는 크리에이터는 단체 채팅방으로, &#39;다수&#39;에 해당하는 구독 유저에게는 일대일 채팅방으로 보여져야 한다.</li>
<li>새로운 팬 유저가 &#39;---와 채팅 시작하기&#39;를 클릭 시 새로운 채팅방이 열리는 것이 아닌 크리에이터가 포함된 단채 채팅방에 입장하는 것?</li>
</ul>
<p>위의 두 가지 문제를 고려하여 기능을 구현해야 할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240719 TIL]]></title>
            <link>https://velog.io/@mini-woong/w8ns3tqh</link>
            <guid>https://velog.io/@mini-woong/w8ns3tqh</guid>
            <pubDate>Fri, 19 Jul 2024 11:34:28 GMT</pubDate>
            <description><![CDATA[<p>첫주를 마무리하며 마지막 결정사항들에 대한 회의를 진행했다.</p>
<ol>
<li>배송지 정보 저장 여부
배송지를 미리 저장해두고 이를 불러와 수정하여 사용할 수 있게끔 하기로 하였다.</li>
<li>인플루언서 계정 분리 문제
회원가입 할 때 체크박스로 구분하고, 인플루언서 체크박스에 체크를 하면 승인 전까지는 일반사용자로 분류된다. 승인하면 인플루언서 계정으로 전환된다.</li>
</ol>
<p>외에도 다른 결정사항들이 많았지만 가장 큰 문제 요소들은 위 두가지이다.
일주일은 거의 회의의 연속... 다음 주부터 본격적인 학습과 공부가 시작될 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240718 TIL]]></title>
            <link>https://velog.io/@mini-woong/48weckbg</link>
            <guid>https://velog.io/@mini-woong/48weckbg</guid>
            <pubDate>Thu, 18 Jul 2024 12:24:02 GMT</pubDate>
            <description><![CDATA[<h3 id="1-디자인과의-협업-시-지켜야-할-점">1. 디자인과의 협업 시 지켜야 할 점</h3>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/21613c78-eccd-4f68-81b7-93e273040e5e/image.png" alt=""></p>
<ul>
<li>기술 적 제액 파악 및 대체 솔루션 탐색
왜 구현이 어려운지, 어떤 방법으로 해당 부분을 대체할 수 있는지 친절하게 명시한다.</li>
<li>개발 편의를 위해 디자인을 임의로 바꾸지 말 것
디자이너의 컨셉과 흐름을 존중하며 임의로 변경하지 않고 요청해야 한다.</li>
</ul>
<h3 id="2-회의---우선-순위-정하기">2. 회의 - 우선 순위 정하기</h3>
<p>페이지와 구현해야 할 기능이 많기 때문에 회의를 통해 우선 순위를 지정했다. 나 같은 경우, 텍스트 기반 채팅 기능을 우선으로 구현하고 추가로 미디어 전송을 구현한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240717 최종프로젝트2]]></title>
            <link>https://velog.io/@mini-woong/%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B82</link>
            <guid>https://velog.io/@mini-woong/%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B82</guid>
            <pubDate>Wed, 17 Jul 2024 10:15:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘도 회의의 연속...!</p>
</blockquote>
<ol>
<li>페이지 축소화
기간을 맞추기 위해 페이지 개수를 제한하기로 했다. (기존의 페이지가 너무 많았던...)</li>
<li>페이지 구조 구체화
헤더에 들어갈 기능을 장바구니, 마이페이지, 채팅으로 구체화 하였다. (+ 스토어)</li>
<li>추가 기능
캘린더, 찜한 상품, 취향으로 추천 등은 추후 선택사항으로 구현하기로 했다.</li>
<li>채팅 기능
헤더의 채팅 버튼으로 구독한 인플루언서 대화 목록을 슬라이드로 나열하고 대화 목록 클릭 시 채팅창을 팝업으로 띄운다.</li>
<li>상품 등록 페이지
여러 개의 이미지를 첨부하여 등록 시 나열할 수 있도록 구현한다. (블로그 기능)</li>
</ol>
<p>필요한 기능들을 모두 정리하여 분업한 결과, 나는 <strong>채팅(!!!)</strong> 기능을 맡게 되었다. 처음 시도해보는 기능이라 걱정 반 떨림 반이지만 열심히...! 도전!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240716 TIL 최종프로젝트]]></title>
            <link>https://velog.io/@mini-woong/finalproject1</link>
            <guid>https://velog.io/@mini-woong/finalproject1</guid>
            <pubDate>Tue, 16 Jul 2024 09:01:33 GMT</pubDate>
            <description><![CDATA[<h2 id="아이디어-회의">아이디어 회의</h2>
<h3 id="최종-프로젝트-첫날-본격적인-아이디어-회의-끝에-주제가-정해졌다">*<em>최종 프로젝트 첫날, *</em>본격적인 아이디어 회의 끝에 주제가 정해졌다.</h3>
<p>바로 인플루언서, 유튜버 맞춤 스토어 + 소통 서비스이다.
연예인들이 주로 사용하는 소통 어플의 기능과 인플루언서들이 주로 사용하는 스토어의 기능을 한데 모아 공동구매나 자체 제작 아이템들을 판매하는 것을 좀 더 용이하게 하고, 소비자의 입장에서도 그것들을 한 번에 모아볼 수 있다는 편리함을 기대할 수 있다.
또한, 실시간 채팅 기능까지 포함하여 팬들과의 소통까지 하나의 서비스에서 할 수 있는 것이 우리 프로젝트의 목표이다.
이 아이디어는 내가 낸 주제인데 프로젝트 연달아 3번이나, 내가 낸 아이디어가 채택되어서 매우 뿌듯하다. ㅎㅎ...
이번 최종 프로젝트는 디자이너님이 계시기 때문에!! 피그마 작성을 따로 하지는 않았지만 각자 생각한 레이아웃을 간단하게 그려 공유해보았다.
<img src="https://velog.velcdn.com/images/mini-woong/post/7b731d8e-fc0a-4754-8a22-2c4f4784745b/image.png" alt="">
<img src="https://velog.velcdn.com/images/mini-woong/post/87a0f600-7f74-476f-8f4f-c0709a8be9aa/image.png" alt=""></p>
<p>앞으로 4주간의 프로젝트가 기대된다. 끝까지 포기하지 말고 열심히! 해볼 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240619 TIL]]></title>
            <link>https://velog.io/@mini-woong/240619-TIL</link>
            <guid>https://velog.io/@mini-woong/240619-TIL</guid>
            <pubDate>Wed, 19 Jun 2024 11:37:52 GMT</pubDate>
            <description><![CDATA[<p>카카오맵 마커, 윈도우인포
테일윈드 css
프로젝트 약국 어디가멘?(2)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240613 TIL]]></title>
            <link>https://velog.io/@mini-woong/240613-TIL</link>
            <guid>https://velog.io/@mini-woong/240613-TIL</guid>
            <pubDate>Thu, 13 Jun 2024 18:48:22 GMT</pubDate>
            <description><![CDATA[<h2 id="인증authentication--인가authorization">인증(authentication) / 인가(authorization)</h2>
<h3 id="인증authentication">인증(Authentication)</h3>
<p>서비스를 이용하려는 유저가 등록된 회원인지 확인하는 절차 (ex. 로그인)</p>
<h3 id="인가authorization">인가(Authorization)</h3>
<p>인증을 받은 유저가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 절차 (ex. 마이페이지에서 개인정보를 확인)</p>
<h2 id="http-프로토콜의-특징-2가지">HTTP 프로토콜의 특징 2가지</h2>
<p>인증과 인가가 필요한 이유는 HTTP 프로토콜의 이 2가지 특성 때문이라고 볼 수 있다.</p>
<h3 id="1-무상태stateless">(1) 무상태(stateless)</h3>
<p>각 요청이 독립적이며, 서버가 이전 요청에 대한 정보를 기억하지 않는다. 따라서 각 요청마다 서버에서 요구하는 모든 상태 정보를 담아서 요청해야 한다. 서버는 클라이언트의 상태를 별도로 기억할 필요없이 주문받은 대로 응답한다. 무상태라는 특성 덕분에 동일한 서버를 여러개로 확장시킬 수 있다. (Scale-Out)</p>
<h3 id="2-비연결성connectionless">(2) 비연결성(connectionless)</h3>
<p>서버와 클라이언트는 연결되어 있지 않다. 비연결성으로 인해 최소한의 서버 자원으로 서버를 유지할 수 있다. 아래의 그림에서 왼쪽의 경우가 비연결성을 표현한 것이라고 볼 수 있다.
<img src="https://velog.velcdn.com/images/mini-woong/post/84fd7e7f-7fca-4c1a-83a4-9ce0ec3fb9bf/image.png" alt="">
<em>출처: <a href="https://hanamon.kr/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-http-http%EB%9E%80-%ED%8A%B9%EC%A7%95-%EB%AC%B4%EC%83%81%ED%83%9C-%EB%B9%84%EC%97%B0%EA%B2%B0%EC%84%B1/">https://hanamon.kr/네트워크-http-http란-특징-무상태-비연결성/</a></em></p>
<blockquote>
<p>클리이언트와 서버 간의 지속적인 요청(예로 로그인 상태가 유지되고, 로그인한 유저만 누릴 수 있는 서비스를 제공하는 것)을 위해 세션 기반의 인증/인가 방식 토큰 기반의 인증/인가 방식을 사용한다.</p>
</blockquote>
<h2 id="쿠키">쿠키</h2>
<p>브라우저에 저장되는 작은 데이터 조각. <code>key-value</code> 형태로 저장된다. 마치 서버가 클라이언트의 <code>인증 상태를 기억하는 것처럼 구현</code>할 수 있다. 쿠키는 별도로 삭제 처리하거나 유효기간이 만료되지 않는 한, 서버와 통신할 때 자동으로 주고받는다. 서버에 특정 API 요청을 했을 때(ex. 로그인 요청) 서버가 응답 시 헤더에 <code>Set-Cookie</code> 속성으로 쿠키 정보를 담아주면, 응답을 받은 브라우저는 쿠키를 브라우저에 자동으로 저장한다. 서버에 http 요청 할 때 마다 브라우저에 저장되어 있는 쿠키는 <code>자동으로</code> 서버에 보내진다. 쿠키는 <code>클라이언트에서 직접 추가/수정/삭제할 수 있다</code>.</p>
<h2 id="세션을-이용한-인증-방식">세션을 이용한 인증 방식</h2>
<h3 id="세션">세션</h3>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/d77c026f-6e76-457b-8120-da767f9b8a20/image.png" alt="">
사용자와 서버 간의 연결이 활성화된 상태 또는 인증이 유지되고 있는 상태
로그인 성공 → 서버에서 세션 생성 및 저장(key-value 형식) → key(sessionId)를 브라우저에 응답(by 쿠키)</p>
<h3 id="쿠키-세션-인증-방식">쿠키-세션 인증 방식</h3>
<h4 id="1-로그인회원가입-시-세션-인증">(1) 로그인/회원가입 시 세션 인증</h4>
<p><img src="https://velog.velcdn.com/images/mini-woong/post/a66b1056-2c5c-41c7-84a7-01dbd9a10005/image.png" alt=""></p>
<h4 id="2-로그인회원가입-성공-시-서버에서-쿠키에-sessionid를-포함">(2) 로그인/회원가입 성공 시 서버에서 쿠키에 sessionId를 포함</h4>
<p>서버에서 sessionId를 설정하고 브라우저로 보내면, 브라우저는 이를 쿠키에 저장한다.</p>
<ul>
<li>세션 유지 상태
서버에서 관리하는 세션 저장소에 회원 데이터가 있다.</li>
<li>세션 만료 상태
서버에서 관리하는 세션 저장소에 회원 데이터가 없다.<h4 id="3-인가authorization-필요한-api-요청응답">(3) 인가(Authorization) 필요한 API 요청/응답</h4>
서버는 인가가 필요한 API 요청을 받으면 클라이언트 쿠키에 들어 있는 sessionId를 세션 저장소에 조회하고 존재한다면 DB에 데이터를 조회하여 응답한다.
<img src="https://velog.velcdn.com/images/mini-woong/post/668e03cc-b349-4639-b195-9dac24ffd757/image.png" alt=""><h3 id="세션-인증-방식의-한계">세션 인증 방식의 한계</h3>
세션 인증 방식에는 확장성 문제, 메모리 사용량 증가, 상태 유지의 복잡성, 보안 문제와 같은 한계점이 존재한다. 세션 인증 방식의 이러한 한계를 극복하기 위해 등장한 것이 바로 <code>JWT(JSON Web Token)</code>이다. JWT는 서버 확장성, 무상태성 유지, 보안성 등을 개선한 <code>토큰 기반 인증 방식</code>이다.<h2 id="토큰을-이용한-인증-방식">토큰을 이용한 인증 방식</h2>
<h3 id="토큰">토큰</h3>
클라이언트에서 보관하는 암호화 또는 인코딩된 인증 정보. 서버의 상태를 유지하지 않고도 클라이언트의 인증 상태를 확인할 수 있게 한다. 세션처럼 서버에서 사용자의 인증 정보를 보관할 필요가 없다. 웹에서 인증 수단으로 사용되는 토큰은 주로 JWT (Json Web Token)이다.<h3 id="jwt">JWT</h3>
JWT는 세 가지 주요 부분으로 이루어진다.<h4 id="1-헤더header">(1) 헤더(Header)</h4>
어떤 종류의 토큰인지와 어떤 알고리즘으로 서명되었는지에 대한 정보.<h4 id="2-본문payload">(2) 본문(Payload)</h4>
데이터가 들어있는 부분. 사용자 ID, 토큰의 만료 시간 등.<h4 id="3-서명signature">(3) 서명(Signature)</h4>
토큰이 위조되지 않았는지 확인하는 역할. 서버만이 알 수 있는 비밀 키로 서명되어 있다.<h3 id="jwt의-특징">JWT의 특징</h3>
</li>
<li>국제 인터넷 표준 인증 규격 중 하나이다.</li>
<li>인코딩된 토큰을 누구나 복호화하여 payload를 볼 수 있다. (토큰의 용도는 인증정보(payload)에 대한 보호가 아니라 위조 방지)</li>
<li>정보(payload)를 토큰화할 때 signature에 secret key가 필요하고, secret key는 복호화가 아니라 토큰이 유효한 지를 검증하는 데 사용된다. =&gt; 무결성 보장<h3 id="jwt-토큰-인증-방식">JWT 토큰 인증 방식</h3>
<h4 id="1-로그인회원가입-시-토큰-인증">(1) 로그인/회원가입 시 토큰 인증</h4>
<img src="https://velog.velcdn.com/images/mini-woong/post/e4d04ea4-4d36-421b-bf01-f5ba8002a975/image.png" alt=""><h4 id="2-인가authorization-필요한-api-요청응답">(2) 인가(Authorization) 필요한 API 요청/응답</h4>
<img src="https://velog.velcdn.com/images/mini-woong/post/c57980b5-ffca-4ba8-98cd-e778f390c918/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[240612 TIL]]></title>
            <link>https://velog.io/@mini-woong/240612-TIL</link>
            <guid>https://velog.io/@mini-woong/240612-TIL</guid>
            <pubDate>Thu, 13 Jun 2024 17:57:26 GMT</pubDate>
            <description><![CDATA[<h2 id="json-server">json-server</h2>
<p> &ensp; 아주 간단한 DB와 API 서버를 생성해주는 패키지이다. json-server는 주로 나와 같이 front-end를 공부하거나 백엔드에서 실제 DB와 서버가 완성되기 전 개발에 임시적으로 사용할 mock data를 생성하기 위해 쓰인다.</p>
<blockquote>
<p>앞서 공부했던 Supabase와 Firebase는 백엔드가 없는 환경에서 실제 서비스 구축에 유용하지만 학습이 목적이거나, 실무와의 연관성을 고려하였을 때 json-server가 더 적합하다고 볼 수 있다.</p>
</blockquote>
<h3 id="설치">설치</h3>
<pre><code>yarn add json-server
yarn add json-server -D # 개발 환경인 경우, -D 옵션을 함께 입력합니다.</code></pre><h3 id="생성">생성</h3>
<p> &ensp; 설치가 완료되면 db.json 파일을 생성 후 사용할 data를 넣어준다.</p>
<pre><code>{
  &quot;todos&quot;: [
    {
      &quot;id&quot;: &quot;1&quot;,
      &quot;title&quot;: &quot;json-server&quot;
    }
  ]
}</code></pre><h3 id="실행">실행</h3>
<p> &ensp; 그 후 서버를 실행해주면 간단하게 json-server를 사용할 수 있다. 대략적으로 db.json 파일을 db로 정하고, 4000 포트에서 서버를 시작하겠다는 의미이다.</p>
<pre><code>yarn json-server db.json --port 4000</code></pre><h2 id="axios">axios</h2>
<p> &ensp; node.js와 브라우저를 위한 Promise 기반 http 클라이언트 = http를 이용해서 서버와 통신하기 위해 사용하는 패키지. fetch의 기능과 유사하다.</p>
<pre><code>//axios 설치
yarn add axios</code></pre><h3 id="0-env">(0) .env</h3>
<p> &ensp; 보통 API_KEY 또는 SERVER_URL과 같은 민감한 정보는 .env 파일에 숨긴다. Vite로 만든 프로젝트의 경우 아래처럼 관리할 수 있다.</p>
<pre><code># .env 파일
VITE_이름 = 123
VITE_EXAMPLE_SERVER_URL = http://localhost:4000
VITE_API_KEY = test1234
VITE_SECRET_KEY = abcdefg

// 사용 예시: src/main.js 또는 src/App.jsx 등
const apiKey = import.meta.env.VITE_API_KEY;
console.log(&#39;API Key:&#39;, apiKey);</code></pre><p> &ensp; 프로젝트를 깃으로 공유할 경우, 민감한 정보가 repository에 포함되지 않도록 아래처럼 .gitignore 파일을 작성해주어야 한다.</p>
<pre><code># .gitignore 파일
# Node.js 관련 파일
node_modules/

# 환경 설정 파일
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# 빌드 출력 파일
build/
dist/

# 기타
.DS_Store</code></pre><h3 id="1-get">(1) Get</h3>
<p> &ensp; axios.get은 서버의 데이터를 조회할 때 사용한다.</p>
<pre><code>const [todos, setTodos] = useState(null);
useEffect(() =&gt; {
    const fetchPost = async () =&gt; {
      try {
        const { data } = await axios.get(
          &quot;http://localhost:4000/todos&quot;
        );
        setTodos(data);
      } catch (error) {
        console.error(&quot;Error =&gt; &quot;, error);
      }
    };

    fetchPost();
  }, []);</code></pre><p> &ensp; 아래는 공식문서에 나와 있는 전체 정보 혹은 상세 정보 url을 작성하는 예시이다.
<img src="https://velog.velcdn.com/images/mini-woong/post/44ef1d21-6975-4df2-bfc3-78266573a4c8/image.png" alt=""><img src="https://velog.velcdn.com/images/mini-woong/post/94be97b0-f711-4b52-b3c4-398c017d12ee/image.png" alt=""></p>
<h3 id="2-post">(2) Post</h3>
<p> &ensp; 위의 예시에서 아래 코드를 추가한다. axios.post는 서버에 데이터를 추가할 때 사용한다. 사용 url과 추가하는 값을 인자로 넣어준다.</p>
<pre><code>  const [todo, setTodo] = useState({
    title: &quot;&quot;,
  });

  const onSubmitHandler = async (todo) =&gt; {
    const {data} = await axios.post(&quot;http://localhost:4000/todos&quot;, todo);
    setTodos([...todos, data]);
  }

  return (
    &lt;div&gt;

      &lt;h3&gt;axios 연습&lt;/h3&gt;
      &lt;form onSubmit={(e) =&gt; {
        //alert(&quot;submit&quot;);
        e.preventDefault();
        onSubmitHandler(todo);
      }}&gt;
        &lt;input
          type=&#39;text&#39;
          onChange={(e) =&gt; {
            setTodo({ ...todo, title: e.target.value });
          }}
        /&gt;
        &lt;button&gt;추가하기&lt;/button&gt;
      &lt;/form&gt;

    &lt;/div&gt;
  );</code></pre><p> &ensp; json-server에서는 아이디가 string 타입으로 랜덤 생성되어 삽입된다. 따라서 위 예시처럼 title의 값을 넣어주면 아이디는 자동 생성되며, 이를 오류없이 화면에 출력하기 위해 response에 먼저 할당 후 setTodos를 해주어야 한다.</p>
<h3 id="3-delete">(3) Delete</h3>
<p> &ensp; axios.delete는 서버의 데이터를 삭제할 때 사용한다. 아래 예시처럼 삭제할 값의 주소를 인자로 받는다.</p>
<pre><code>  const onDeleteHandler = async (id) =&gt; {
    await axios.delete(&quot;http://localhost:4000/todos/&quot; + id);
    setTodos(todos.filter((todo) =&gt; todo.id !== id));
  };

  return (
    &lt;div&gt;

      &lt;h3&gt;axios 연습&lt;/h3&gt;
      &lt;form onSubmit={(e) =&gt; {
        //alert(&quot;submit&quot;);
        e.preventDefault();
        onSubmitHandler(todo);
      }}&gt;
        &lt;input
          type=&#39;text&#39;
          onChange={(e) =&gt; {
            setTodo({ ...todo, title: e.target.value });
          }}
        /&gt;
        &lt;button&gt;추가하기&lt;/button&gt;
      &lt;/form&gt;
      {
        todos?.map((todo) =&gt; {
          return (
            &lt;div key={todo.id}&gt;
              &lt;span&gt;{todo.title}&lt;/span&gt; &lt;button onClick={(e) =&gt; onDeleteHandler(todo.id)}&gt;삭제&lt;/button&gt;
            &lt;/div&gt;
          );
        })
      }
    &lt;/div&gt;
  );</code></pre><p>(4) Patch
 &ensp; axios.patch는 서버의 데이터를 일부 수정할 때 주로 사용한다. (put은 전체를 수정한다.) 아래 예시처럼 수정할 값의 주소와 함께, 수정할 내용을 인자로 받는다.</p>
<pre><code>  const [targetId, setTargetId] = useState(&quot;&quot;);
  const [editTodo, setEditTodo] = useState({
    title: &quot;&quot;,
  });

  const onEditHandler = async (targetId, editTodo) =&gt; {
    await axios.patch(&quot;http://localhost:4000/todos/&quot; + targetId, editTodo);
    const newTodos = todos.map(todo =&gt; {
      if (todo.id === targetId) {
        return {
          ...todo,
          title: editTodo.title
        }
      }
      return todo
    });
    setTodos(newTodos)
  }

  return (
    &lt;div&gt;

      &lt;h3&gt;axios 연습&lt;/h3&gt;
      &lt;form onSubmit={(e) =&gt; {
        //alert(&quot;submit&quot;);
        e.preventDefault();
        onSubmitHandler(todo);
      }}&gt;
        &lt;div&gt;
          &lt;input
            type=&#39;text&#39;
            placeholder=&#39;수정할 id&#39;
            onChange={(e) =&gt; {
              setTargetId(e.target.value);
            }}
          /&gt;
          &lt;input
            type=&#39;text&#39;
            placeholder=&#39;수정할 내용&#39;
            onChange={(e) =&gt; {
              setEditTodo({ ...editTodo, title: e.target.value });
            }}
          /&gt;
          &lt;button
            type=&#39;button&#39;
            onClick={() =&gt; onEditHandler(targetId, editTodo)}&gt;
            수정하기
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;input
          type=&#39;text&#39;
          onChange={(e) =&gt; {
            setTodo({ ...todo, title: e.target.value });
          }}
        /&gt;
        &lt;button&gt;추가하기&lt;/button&gt;
      &lt;/form&gt;
      {
        todos?.map((todo) =&gt; {
          return (
            &lt;div key={todo.id}&gt;
              &lt;span&gt;{todo.title}&lt;/span&gt; &lt;button onClick={(e) =&gt; onDeleteHandler(todo.id)}&gt;삭제&lt;/button&gt;
            &lt;/div&gt;
          );
        })
      }
    &lt;/div&gt;
  );
}</code></pre><h2 id="axios-vs-fetch">axios vs fetch</h2>
<p> &ensp; 둘은 모두 HTTP 요청(GET, POST …)을 처리하기 위한 JavaScript 라이브러리이다. 다만 axios는 아래와 같은 장점들을 가지고 있어 react에서는 axios를 더 선호하는 경우가 종종 있다.</p>
<h3 id="1-기본-설정-및-인터셉터-지원">(1) 기본 설정 및 인터셉터 지원</h3>
<p> &ensp; axios는 기본 설정을 정의하고, 이를 통해 모든 요청에 공통 설정을 적용할 수 있다. 또한, 요청 또는 응답을 가로채서 전처리 또는 후처리할 수 있다. 이와 관련된 내용은 후에 추가적으로 공부해보자.</p>
<h3 id="2-더-나은-오류-처리">(2) 더 나은 오류 처리</h3>
<p> &ensp; axios는 HTTP 상태 코드를 기준으로 한 일관된 오류 처리를 제공한다. fetch는 기본적으로 네트워크 오류만 catch 블록으로 전달되고, 4xx, 5xx 오류는 then 블록에서 처리한다.</p>
<h3 id="3-구형-브라우저-지원">(3) 구형 브라우저 지원</h3>
<p> &ensp; axios는 구형 브라우저와의 호환성이 좋다.</p>
<h3 id="4-간결한-문법">(4) 간결한 문법</h3>
<p> &ensp; axios는 간결하고 직관적인 문법을 제공하여 사용하기 쉽다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[240611 TIL]]></title>
            <link>https://velog.io/@mini-woong/240611-TIL</link>
            <guid>https://velog.io/@mini-woong/240611-TIL</guid>
            <pubDate>Tue, 11 Jun 2024 18:45:27 GMT</pubDate>
            <description><![CDATA[<h2 id="http">HTTP</h2>
<p>프로토콜 : 데이터 통신을 월활하게 하기 위해 필요한 통신 규약(신호 송신의 순서, 데이터의 표현법, 오류 검출법 등)
<img src="https://velog.velcdn.com/images/mini-woong/post/bb187ecd-038f-497b-b173-61913f4f471f/image.png" alt=""></p>
<p> &ensp; 클라이언트-서버 모델을 기반으로 동작한다. 요청과 응답은 텍스트 기반의 메시지로 이루어져 있다.</p>
<h3 id="특징">특징</h3>
<p>무상태성 : 모든 요청은 독립적, 이전 요청의 정보를 기억하지 않는다.
확장성 : 확장 헤더를 추가하여 기능을 확장할 수 있다.
유연성 : 다양한 데이터 형식을 전송할 수 있다.</p>
<h3 id="메시지-구조">메시지 구조</h3>
<h4 id="1-요청-메시지">(1) 요청 메시지</h4>
<ul>
<li><p><strong>요청 라인</strong>: 메서드(GET, POST, 등), URL, HTTP 버전</p>
</li>
<li><p><strong>헤더</strong>: 요청의 추가 정보(메타데이터). 브라우저 정보, 인증 정보 등</p>
<blockquote>
<p><strong>왜 요청 헤더에 인증 정보가 들어갈까?</strong></p>
</blockquote>
<p>&ensp; 웹 브라우저가 서버에 요청을 보낼 때, 서버는 요청을 보낸 사람이 누구인지 알아야 한다. 바로 <strong>`인증</strong>(Authentication)`</p>
<p>&ensp; <strong>요청헤더 예시</strong></p>
<pre><code>GET /protected-resource HTTP/1.1
Host: example.com
Authorization: Bearer &lt;Access-Token&gt;</code></pre><ul>
<li><strong>본문</strong>: 선택적, 주로 POST 메서드에서 사용<pre><code>GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html</code></pre></li>
</ul>
</li>
</ul>
<h4 id="2-응답-메시지">(2) 응답 메시지</h4>
<p> &ensp; 서버가 클라이언트의 요청에 대한 응답을 보낼 때 사용</p>
<ul>
<li><p><strong>상태 라인</strong>: HTTP 버전, <strong>상태코드(200, 404, 등)</strong>, 상태 메시지</p>
</li>
<li><p><strong>헤더</strong>: 응답의 추가 정보(메타데이터)</p>
</li>
<li><p><strong>본문</strong>: 선택적, 주로 응답 데이터
&ensp; </p>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1354

&lt;!DOCTYPE html&gt;
&lt;html&gt;
   &lt;head&gt;
       &lt;title&gt;Example&lt;/title&gt;
   &lt;/head&gt;
   &lt;body&gt;
       &lt;h1&gt;Hello, World!&lt;/h1&gt;
   &lt;/body&gt;
&lt;/html&gt;</code></pre><br>
<br>

</li>
</ul>
<h3 id="http-상태코드">HTTP 상태코드</h3>
<p> &ensp; 서버가 클라이언트의 요청을 처리한 결과. 세 자리 숫자로 구성. 첫 번째 자리에 따라 의미가 디드다.
<img src="https://velog.velcdn.com/images/mini-woong/post/34ac3634-930e-47e7-9ecd-d0043649fa14/image.png" alt=""></p>
<ul>
<li><p>1xx: 정보</p>
<ul>
<li><code>100</code> Continue : 요청의 일부를 서버가 받았으며, 나머지를 계속 보내라는 의미.</li>
</ul>
</li>
<li><p><strong>2xx: 성공</strong></p>
<ul>
<li><p><strong><code>200</code> OK</strong> : 요청이 성공적으로 처리됨.</p>
</li>
<li><p><strong><code>201</code> Created</strong> : 요청이 성공적이었으며, 새로운 자원이 생성됨.</p>
</li>
</ul>
</li>
<li><p>3xx: 리다이렉션</p>
<ul>
<li><p><code>301</code> Moved Permanently : 요청한 리소스가 영구적으로 새로운 URL로 이동함.</p>
</li>
<li><p><code>302</code> Found : 요청한 리소스가 임시로 다른 URL로 이동함.</p>
</li>
</ul>
</li>
<li><p><strong>4xx: 클라이언트 오류</strong></p>
<ul>
<li><p><strong><code>400</code> Bad Request</strong> : 잘못된 요청.</p>
</li>
<li><p><code>401</code> Unauthorized : 인증이 필요함.</p>
</li>
<li><p><strong><code>404</code> Not Found</strong> : 요청한 리소스를 찾을 수 없음.</p>
</li>
</ul>
</li>
<li><p><strong>5xx: 서버 오류</strong></p>
<ul>
<li><p><code>500</code> Internal Server Error : 서버가 요청을 처리하는 동안 오류 발생.</p>
</li>
<li><p><strong><code>502</code> Bad Gateway</strong> : 서버가 게이트웨이 또는 프록시 역할을 하는 서버로부터 유효하지 않은 응답을 받음.</p>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="http-메서드-및-rest-api">HTTP 메서드 및 Rest API</h3>
<p> &ensp; HTTP 메서드는 클라이언트가 서버에게 요청의 성격을 알리는 데 사용한다. REST API는 이러한 HTTP 메서드를 사용하여 CRUD 작업을 수행한다.</p>
<h4 id="1-get">(1) GET</h4>
<p> &ensp; 서버로부터 데이터를 요청한다. <code>요청 데이터가 URL에 포함</code>되어 전송되며, 주로 <code>데이터를 조회</code></p>
<h4 id="2-post">(2) POST</h4>
<p> &ensp; 서버에 <code>데이터를 제출</code>한다. <code>요청 데이터가 요청 본문에 포함</code>되어 전송되며, 주로 데이터를 생성하거나 제출할 때 사용한다.</p>
<h4 id="3-put-patch">(3) PUT, PATCH</h4>
<p> &ensp; 서버의 <code>데이터를 업데이트</code>한다. 요청 데이터가 요청 본문에 포함되어 전송된다.</p>
<h4 id="4-delete">(4) DELETE</h4>
<p> &ensp; 서버의 <code>데이터를 삭제</code>한다.</p>
<blockquote>
<p>RESTful 원칙이란?
 &ensp; method와 url의 조합만으로 어떤 종류의 요청인지 추측가능할수록 RESTful하다고 한다.
 Ex)
method가 POST이고, url이 /posts이면 포스트를 추가한다는 요청.
method가 GET이고, url이 /posts이면 포스트를 조회한다는 요청. </p>
</blockquote>
<br>


<h4 id="예시코드">예시코드</h4>
<p> &ensp; 다음은 fetch API를 사용하는 예시이다.</p>
<h4 id="1-get-1">&ensp; (1) GET</h4>
<pre><code>import React, { useState, useEffect } from &quot;react&quot;;

function App() {
  const [data, setData] = useState(null);

  useEffect(() =&gt; {
    const fetchData = async () =&gt; {
      try {
        // get 요청 시, fetch는 method를 명시하지 않아도 된다.
        const response = await fetch(
          &quot;https://jsonplaceholder.typicode.com/posts/1&quot;
        );
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error(&quot;Error fetching data:&quot;, error);
      }
    };

    fetchData();
  }, []);

  return &lt;div&gt;{data ? &lt;div&gt;{data.title}&lt;/div&gt; : &lt;div&gt;Loading...&lt;/div&gt;}&lt;/div&gt;;
}

export default App;
</code></pre><h4 id="2-post-1">&ensp; (2) POST</h4>
<pre><code>import React, { useState } from &quot;react&quot;;

function App() {
  const [title, setTitle] = useState(&quot;&quot;);
  const [body, setBody] = useState(&quot;&quot;);
  const [response, setResponse] = useState(null);

  const handleSubmit = async (event) =&gt; {
    event.preventDefault();
    try {
      const res = await fetch(&quot;https://jsonplaceholder.typicode.com/posts&quot;, {
        method: &quot;POST&quot;,
        headers: {
          &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
        body: JSON.stringify({
          title: title,
          body: body,
          userId: 1,
        }),
      });
      const result = await res.json();
      setResponse(result);
    } catch (error) {
      console.error(&quot;Error creating data:&quot;, error);
    }
  };

  return (
    &lt;div&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input
          type=&quot;text&quot;
          value={title}
          onChange={(e) =&gt; setTitle(e.target.value)}
          placeholder=&quot;Title&quot;
        /&gt;
        &lt;textarea
          value={body}
          onChange={(e) =&gt; setBody(e.target.value)}
          placeholder=&quot;Body&quot;
        /&gt;
        &lt;button type=&quot;submit&quot;&gt;Create Post&lt;/button&gt;
      &lt;/form&gt;
      {response &amp;&amp; &lt;div&gt;Created Post ID: {response.id}&lt;/div&gt;}
    &lt;/div&gt;
  );
}

export default App;
</code></pre>]]></description>
        </item>
    </channel>
</rss>