<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩연습장</title>
    <link>https://codingpracticenote.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 23:33:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>공대탈출</managingEditor>
    <item>
      <title>SnapRoad에서 SignedUrl을 사용한 이유</title>
      <link>https://codingpracticenote.tistory.com/351</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;SnapRoad 서비스는 어떤 서비스인가?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SnapRoad는 &lt;b&gt;private 그룹&lt;/b&gt;을 통해 개인, 그룹의 여행 기록과 사진, 텍스트를 그룹원들만 볼 수 있도록 &lt;b&gt;안전하게 공유&lt;/b&gt;하는 서비스입니다. supabase의 private storage와 signed url은 snaproad의 특징인 프라이빗 그룹을 위한 &lt;b&gt;개인정보 보호와 보안성을 구현&lt;/b&gt;하는데 핵심 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;왜 Signed Url을 사용했나?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Supabase의 storage는 기본적으로 public, private 버킷을 제공하며, private 버킷은 인증된 요청만 접근할 수 있다. 하지만 하기 이유로 signed url을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프라이빗 스토리지의 안전성 유지&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;버킷의 파일을 노출시키지 않고, 지정된 &lt;b&gt;유효시간 동안 특정 url으로만 접근을 허용&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자 인증 후 접근 권한을 제한하여 파일을 제공하기 때문에 &lt;b&gt;데이터를 안전하게 보호&lt;/b&gt;할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일회성 링크로 효율적인 보안 관리&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지정된 유효시간동안 url이 유효하고, &lt;b&gt;만료 시 사용이 불가&lt;/b&gt;하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비인가 사용자가 url을 비정상적인 경로로 획득하더라도 만료 시간이 지나면 접근이 불가하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API 호출 간소화&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;클라이언트에서 &lt;b&gt;복잡한 인증 절차 없이&lt;/b&gt; 링크를 통해 파일을 바로 불러올 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이를 통해 &lt;b&gt;UX를 개선&lt;/b&gt;하고, &lt;b&gt;보안을 유지&lt;/b&gt;할 수 있다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Supabase SignedUrl 사용법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 1개의 파일에 대한 url을 받아와야할 때는 &lt;b&gt;createSignedUrl&lt;/b&gt;을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;supabase에서 제공받은 토큰값은 쿠키로 관리하여 요청시 자동으로 요청헤더에 들어간다. 그 값을 통해 자동으로 url에 넣어 요청url을 만들어 준다. 인자로 &lt;b&gt;path와 파일명을 묶은 문자열&lt;/b&gt;, &lt;b&gt;url의 유효기간&lt;/b&gt;으로 설정할 값을 넣어서 요청을 보내 사용한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731855830049&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { data, error } = await supabase
  .storage
  .from('avatars')
  .createSignedUrl('folder/avatar1.png', 60)
  
// response
// https://example.supabase.co/storage/v1/object/sign/avatars/folder/avatar1.png?token=&amp;lt;TOKEN&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두번째로 여러 signedurl생성을 한 요청으로 보내기 위한 &lt;b&gt;createSignedUrls&lt;/b&gt;이다. &lt;b&gt;Promise.all&lt;/b&gt;이나 &lt;b&gt;allsetteled&lt;/b&gt;를 사용하여 여러 createSignedUrl 요청을 하나로 묶어 보내도 되지만, HTTP1에서는 한번의 요청이 6개로 제한되기 때문에 첫 로딩에 사용하게 되면 &lt;b&gt;성능이슈&lt;/b&gt;가 생길 수 있다. 따라서 여러 signedUrl을 받아야한다면 &lt;b&gt;createSignedUrls가 적절&lt;/b&gt;하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;signedUrl을 사용했던 것과 비슷하게 여러 문자열을 배열로 묶어 유효기간과 함께 보내면 배열 형태로 url묶음을 응답으로 보내준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731859217645&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { data, error } = await supabase
  .storage
  .from('avatars')
  .createSignedUrls(['folder/avatar1.png', 'folder/avatar2.png'], 60)

// response
// [
//  {
//    &quot;error&quot;: null,
//    &quot;path&quot;: &quot;folder/avatar1.png&quot;,
//    &quot;signedURL&quot;: &quot;/object/sign/avatars/folder/avatar1.png?token=&amp;lt;TOKEN&amp;gt;&quot;,
//    &quot;signedUrl&quot;: &quot;https://example.supabase.co/storage/v1/object/sign/avatars/folder/avatar1.png?token=&amp;lt;TOKEN&amp;gt;&quot;
//  },
//  {
//    &quot;error&quot;: null,
//    &quot;path&quot;: &quot;folder/avatar2.png&quot;,
//    &quot;signedURL&quot;: &quot;/object/sign/avatars/folder/avatar2.png?token=&amp;lt;TOKEN&amp;gt;&quot;,
//    &quot;signedUrl&quot;: &quot;https://example.supabase.co/storage/v1/object/sign/avatars/folder/avatar2.png?token=&amp;lt;TOKEN&amp;gt;&quot;
//  }
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 js에서 createSignedUrls는 큰 단점이 하나 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단일 url생성인 createSignedUrl은 transform옵션을 사용하여 이미지 파일 형식(jpg, png, webp, avif, ...)으로 변경하거나, width, height, quality, resize를 사용하여 이미지의 크기를 변경할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기본적인 createSignedUrl은 이미지 파일의 &lt;b&gt;퀄리티를 80%로 낮춰&lt;/b&gt; 사용자에게 보내주어 어느정도 &lt;b&gt;최적화&lt;/b&gt;가 된 상태이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;현재 SnapRoad의 signedUrl 요청 흐름도 / 최적화 예정 내역&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW4oEV/btsKLncp1Nh/huxjgNKJhIk87zvqeYPTe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW4oEV/btsKLncp1Nh/huxjgNKJhIk87zvqeYPTe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW4oEV/btsKLncp1Nh/huxjgNKJhIk87zvqeYPTe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW4oEV%2FbtsKLncp1Nh%2FhuxjgNKJhIk87zvqeYPTe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;440&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 SnapRoad에서 signedUrl을 생성하는 흐름도는 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 데이터를 받아와 테이블에 저장된 이미지 파일명을 받아오고, 그 파일명을 배열로 묶어 createSignedUrls를 통해 이미지 요청 경로를 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Baas로 Supabase를 사용하다보니 이런 백엔드 서비스로직을 프론트에서 구현해야하여 요청이 나눠진 상태인데, 이를 sql function을 만들어 아예 처음부터 이미지 파일명이 아닌 signedUrl으로 만들어 클라이언트서버에 보내주는 것으로 최적화를 진행할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 createSignedUrls는 이미지 확장자나 퀄리티 지정과 같은 최적화를 사용할 수 없어 해당 부분도 같이 최적화를 진행할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화 후 예상 요청 흐름도는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NemkB/btsKMaw7bbp/vrJLziS9BnHyNAk09KzarK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NemkB/btsKMaw7bbp/vrJLziS9BnHyNAk09KzarK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NemkB/btsKMaw7bbp/vrJLziS9BnHyNAk09KzarK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNemkB%2FbtsKMaw7bbp%2FvrJLziS9BnHyNAk09KzarK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;312&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 요청흐름을 제거하면 큰 성능향상이 이뤄질 것으로 예상되며, 각 이미지의 최적화 로직또한 supabase에서 제공해주기 때문에 이미지 src요청에서도 성능향상과 최적화가 이뤄질 것이라고 생각한다.&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/351</guid>
      <comments>https://codingpracticenote.tistory.com/351#entry351comment</comments>
      <pubDate>Mon, 18 Nov 2024 01:24:37 +0900</pubDate>
    </item>
    <item>
      <title>말이안되는 요청코드 rpc로 쉽게? 해결하기</title>
      <link>https://codingpracticenote.tistory.com/350</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241025163414.png&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OTWZg/btsKl1eQgh5/2XOTiQQDZPkvcH7NFukOFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OTWZg/btsKl1eQgh5/2XOTiQQDZPkvcH7NFukOFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OTWZg/btsKl1eQgh5/2XOTiQQDZPkvcH7NFukOFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOTWZg%2FbtsKl1eQgh5%2F2XOTiQQDZPkvcH7NFukOFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;476&quot; height=&quot;289&quot; data-filename=&quot;Pasted image 20241025163414.png&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;최종&amp;nbsp;프로젝트&amp;nbsp;중&amp;nbsp;그룹&amp;nbsp;리스트페이지에서&amp;nbsp;랜덤한&amp;nbsp;이미지를&amp;nbsp;보여줘야할&amp;nbsp;필요가&amp;nbsp;있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241025163559.png&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzhXL/btsKlXXPI5v/lEuhvtGpQATsh8Ktfo7FiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzhXL/btsKlXXPI5v/lEuhvtGpQATsh8Ktfo7FiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzhXL/btsKlXXPI5v/lEuhvtGpQATsh8Ktfo7FiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzhXL%2FbtsKlXXPI5v%2FlEuhvtGpQATsh8Ktfo7FiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1313&quot; height=&quot;757&quot; data-filename=&quot;Pasted image 20241025163559.png&quot; data-origin-width=&quot;1313&quot; data-origin-height=&quot;757&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트의 erd였는데&lt;br /&gt;내가&amp;nbsp;원하는&amp;nbsp;기능은&amp;nbsp;이것이었다.&lt;br /&gt;1.&amp;nbsp;로그인한&amp;nbsp;유저의&amp;nbsp;id로&amp;nbsp;user_group에서&amp;nbsp;유저가&amp;nbsp;속한&amp;nbsp;그룹&amp;nbsp;랜덤하게&amp;nbsp;가져오기&lt;br /&gt;2.&amp;nbsp;가져온&amp;nbsp;그룹&amp;nbsp;id를&amp;nbsp;통해&amp;nbsp;post테이블에서&amp;nbsp;해당&amp;nbsp;그룹에서&amp;nbsp;생성된&amp;nbsp;게시글의&amp;nbsp;대표&amp;nbsp;이미지&amp;nbsp;파일명&amp;nbsp;가져오기&lt;br /&gt;3.&amp;nbsp;가져온&amp;nbsp;대표&amp;nbsp;이미지&amp;nbsp;파일명을&amp;nbsp;createSignedUrl을&amp;nbsp;통해&amp;nbsp;프라이빗한&amp;nbsp;이미지&amp;nbsp;url생성하기&lt;br /&gt;4.&amp;nbsp;그걸&amp;nbsp;특정&amp;nbsp;시간마다&amp;nbsp;재실행하여&amp;nbsp;이미지를&amp;nbsp;계속&amp;nbsp;변경시키기&lt;br /&gt;근데&amp;nbsp;supabase-js에는&amp;nbsp;랜덤하게&amp;nbsp;하나만&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;기능은&amp;nbsp;제공되지&amp;nbsp;않고&amp;nbsp;있었다....&lt;br /&gt;그래서&amp;nbsp;일단&amp;nbsp;차선책으로&amp;nbsp;아래와&amp;nbsp;같이&amp;nbsp;구현해보았다.&lt;br /&gt;1.&amp;nbsp;로그인한&amp;nbsp;유저의&amp;nbsp;id로&amp;nbsp;user_group에서&amp;nbsp;유저가&amp;nbsp;속한&amp;nbsp;그룹&amp;nbsp;10개&amp;nbsp;가져오기&lt;br /&gt;2.&amp;nbsp;그룹&amp;nbsp;10개를&amp;nbsp;랜덤하게&amp;nbsp;sort하기&lt;br /&gt;3.&amp;nbsp;sort된&amp;nbsp;배열을&amp;nbsp;promise.allsettled를&amp;nbsp;사용하여&amp;nbsp;post_image_name&amp;nbsp;가져오기&lt;br /&gt;4.&amp;nbsp;완료된&amp;nbsp;promise배열중&amp;nbsp;status가&amp;nbsp;fulfilled인&amp;nbsp;값만&amp;nbsp;필터링하기&lt;br /&gt;5.&amp;nbsp;필터링된&amp;nbsp;배열에&amp;nbsp;signedurl생성하는&amp;nbsp;함수를&amp;nbsp;각각&amp;nbsp;promise.all사용&lt;br /&gt;6.&amp;nbsp;url배열&amp;nbsp;완성!&lt;br /&gt;끔찍한&amp;nbsp;로직&lt;br /&gt;```javascript&lt;br /&gt;const&amp;nbsp;groupLists&amp;nbsp;=&amp;nbsp;await&amp;nbsp;getGroupPostLists(userId);&lt;br /&gt;if&amp;nbsp;(groupLists.length&amp;nbsp;===&amp;nbsp;0)&amp;nbsp;return&amp;nbsp;null;&lt;br /&gt;//랜덤순으로&amp;nbsp;그룹리스트&amp;nbsp;정렬&lt;br /&gt;const&amp;nbsp;randomGroupIds&amp;nbsp;=&amp;nbsp;groupLists.sort(()&amp;nbsp;=&amp;gt;&amp;nbsp;Math.random()&amp;nbsp;-&amp;nbsp;0.5);&lt;br /&gt;const&amp;nbsp;PostsList&amp;nbsp;=&amp;nbsp;await&amp;nbsp;Promise.allSettled(&lt;br /&gt;&amp;nbsp;&amp;nbsp;randomGroupIds.map(async&amp;nbsp;({&amp;nbsp;group_id&amp;nbsp;})&amp;nbsp;=&amp;gt;&amp;nbsp;await&amp;nbsp;getPostListsByGroupId(group_id)),&lt;br /&gt;);&lt;br /&gt;const&amp;nbsp;fulfilledLists&amp;nbsp;=&amp;nbsp;PostsList.filter((el)&amp;nbsp;=&amp;gt;&amp;nbsp;el.status&amp;nbsp;===&amp;nbsp;'fulfilled');&lt;br /&gt;const&amp;nbsp;ImageLists&amp;nbsp;=&amp;nbsp;await&amp;nbsp;Promise.all(&lt;br /&gt;&amp;nbsp;&amp;nbsp;fulfilledLists.map((fulfilled)&amp;nbsp;=&amp;gt;&lt;br /&gt;getSignedImgUrl('tour_images',&amp;nbsp;60&amp;nbsp;*&amp;nbsp;10,&amp;nbsp;`group_name/${fulfilled.value.post_thumbnail_image}`),&lt;br /&gt;&amp;nbsp;&amp;nbsp;),&lt;br /&gt;);&lt;br /&gt;```&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241025165602.png&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5kePo/btsKj5jdVdw/8Kk6mD0uFMNBYGDftErTd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5kePo/btsKj5jdVdw/8Kk6mD0uFMNBYGDftErTd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5kePo/btsKj5jdVdw/8Kk6mD0uFMNBYGDftErTd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5kePo%2FbtsKj5jdVdw%2F8Kk6mD0uFMNBYGDftErTd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;273&quot; data-filename=&quot;Pasted image 20241025165602.png&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;페이지&amp;nbsp;이동&amp;nbsp;후&amp;nbsp;첫&amp;nbsp;로직이&amp;nbsp;3초나&amp;nbsp;걸린다...&lt;br /&gt;이건&amp;nbsp;sql을&amp;nbsp;무조건&amp;nbsp;써야할거같다는&amp;nbsp;생각이&amp;nbsp;듦&lt;br /&gt;1.&amp;nbsp;rpc로&amp;nbsp;요청을&amp;nbsp;보내서&amp;nbsp;post가&amp;nbsp;있는&amp;nbsp;group_id를&amp;nbsp;랜덤하게&amp;nbsp;하나만&amp;nbsp;가져온다.&lt;br /&gt;2.&amp;nbsp;post테이블에&amp;nbsp;group_id로&amp;nbsp;작성된&amp;nbsp;post를&amp;nbsp;랜덤하게&amp;nbsp;하나만&amp;nbsp;가져온다.&lt;br /&gt;3.&amp;nbsp;가져온&amp;nbsp;이미지파일&amp;nbsp;하나를&amp;nbsp;url로&amp;nbsp;변환한다.&lt;br /&gt;sql문법을&amp;nbsp;두번만&amp;nbsp;작성하면&amp;nbsp;로직이&amp;nbsp;매우&amp;nbsp;간단해지고&amp;nbsp;성능도&amp;nbsp;오르지&amp;nbsp;않을까&amp;nbsp;라는&amp;nbsp;생각에&amp;nbsp;해보았다.&lt;br /&gt;근데&amp;nbsp;내가&amp;nbsp;어떻게&amp;nbsp;sql&amp;nbsp;문법을&amp;nbsp;작성하나&lt;br /&gt;구글링을&amp;nbsp;해보고&amp;nbsp;스택오버플로우에&amp;nbsp;비슷한&amp;nbsp;사례가&amp;nbsp;있는지&amp;nbsp;검색해보고&lt;br /&gt;gpt한테&amp;nbsp;물어도&amp;nbsp;보고&amp;nbsp;파파고를&amp;nbsp;사용해&amp;nbsp;supabase&amp;nbsp;ai와도&amp;nbsp;대화를&amp;nbsp;해봤지만&lt;br /&gt;gpt는&amp;nbsp;테이블&amp;nbsp;구조를&amp;nbsp;이해못하고&amp;nbsp;supabase&amp;nbsp;ai는&amp;nbsp;아직&amp;nbsp;좀&amp;nbsp;부족해서&amp;nbsp;해결을&amp;nbsp;못해주었다.&lt;br /&gt;&lt;br /&gt;이걸&amp;nbsp;튜터님한테&amp;nbsp;들고가면&amp;nbsp;도대체&amp;nbsp;뭔&amp;nbsp;소리를&amp;nbsp;들을까&amp;nbsp;싶다가도&amp;nbsp;아무리생각해도&amp;nbsp;첫로딩에&amp;nbsp;3초가&amp;nbsp;걸리는건&amp;nbsp;좀&amp;nbsp;아니다&amp;nbsp;싶어&amp;nbsp;들고갔다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241025175324.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HCyEH/btsKl4ikZyK/ZzfrifHtbt4paHvU8RcJdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HCyEH/btsKl4ikZyK/ZzfrifHtbt4paHvU8RcJdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HCyEH/btsKl4ikZyK/ZzfrifHtbt4paHvU8RcJdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHCyEH%2FbtsKl4ikZyK%2FZzfrifHtbt4paHvU8RcJdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;703&quot; data-filename=&quot;Pasted image 20241025175324.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;703&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;3160ms&amp;nbsp;-&amp;gt;&amp;nbsp;89ms&lt;br /&gt;jesus&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241025175255.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bovHgt/btsKlVZ1r7m/j4SWOt4mGnDyFsZyQajY6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bovHgt/btsKlVZ1r7m/j4SWOt4mGnDyFsZyQajY6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bovHgt/btsKlVZ1r7m/j4SWOt4mGnDyFsZyQajY6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbovHgt%2FbtsKlVZ1r7m%2Fj4SWOt4mGnDyFsZyQajY6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;381&quot; height=&quot;75&quot; data-filename=&quot;Pasted image 20241025175255.png&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;```sql&lt;br /&gt;begin&lt;br /&gt;&amp;nbsp;&amp;nbsp;return&amp;nbsp;query&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;select&amp;nbsp;p.post_thumbnail_image&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from&amp;nbsp;public.post&amp;nbsp;p&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;,&amp;nbsp;(&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;select&amp;nbsp;p.post_id,&amp;nbsp;count(1)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from&amp;nbsp;public.post&amp;nbsp;p&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;where&amp;nbsp;p.user_id&amp;nbsp;=&amp;nbsp;input_user_id&amp;nbsp;&amp;nbsp;--&amp;nbsp;동적&amp;nbsp;user_id&amp;nbsp;파라미터&amp;nbsp;적용&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;group&amp;nbsp;by&amp;nbsp;p.post_id&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;having&amp;nbsp;count(1)&amp;nbsp;&amp;gt;&amp;nbsp;0&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;cal_p&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;where&amp;nbsp;p.post_id&amp;nbsp;=&amp;nbsp;cal_p.post_id&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limit&amp;nbsp;5;&lt;br /&gt;end;&lt;br /&gt;```&lt;br /&gt;뭔가&amp;nbsp;같은&amp;nbsp;데이터만&amp;nbsp;들어오길래&amp;nbsp;봤더니&amp;nbsp;내&amp;nbsp;게시물의&amp;nbsp;이미지만&amp;nbsp;받아오는&amp;nbsp;거였음&lt;br /&gt;그래서&amp;nbsp;두번의&amp;nbsp;rpc를&amp;nbsp;사용하기로&amp;nbsp;함&lt;br /&gt;```sql&lt;br /&gt;SELECT&amp;nbsp;user_group.group_id&lt;br /&gt;FROM&amp;nbsp;user_group&lt;br /&gt;JOIN&amp;nbsp;profiles&amp;nbsp;ON&amp;nbsp;profiles.user_id&amp;nbsp;=&amp;nbsp;user_group.user_id&lt;br /&gt;JOIN&amp;nbsp;post&amp;nbsp;on&amp;nbsp;post.group_id&amp;nbsp;=&amp;nbsp;user_group.group_id&lt;br /&gt;WHERE&amp;nbsp;profiles.user_id&amp;nbsp;=&amp;nbsp;insert_user_id&lt;br /&gt;ORDER&amp;nbsp;BY&amp;nbsp;RANDOM()&lt;br /&gt;LIMIT&amp;nbsp;1;&lt;br /&gt;```&lt;br /&gt;```sql&lt;br /&gt;begin&lt;br /&gt;&amp;nbsp;&amp;nbsp;return&amp;nbsp;(&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;select&amp;nbsp;p.post_thumbnail_image&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from&amp;nbsp;public.post&amp;nbsp;p&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;where&amp;nbsp;p.group_id&amp;nbsp;=&amp;nbsp;input_group_id&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;order&amp;nbsp;by&amp;nbsp;random()&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limit&amp;nbsp;1&lt;br /&gt;&amp;nbsp;&amp;nbsp;);&lt;br /&gt;end;&lt;br /&gt;```&lt;br /&gt;처음&amp;nbsp;user_id를&amp;nbsp;넣어서&amp;nbsp;속한&amp;nbsp;그룹중&amp;nbsp;post가&amp;nbsp;있는&amp;nbsp;group_id를&amp;nbsp;받아오고&lt;br /&gt;받아온&amp;nbsp;group_id로&amp;nbsp;post&amp;nbsp;중&amp;nbsp;랜덤한&amp;nbsp;하나의&amp;nbsp;썸네일&amp;nbsp;이미지를&amp;nbsp;받아온다.&lt;br /&gt;```jsx&lt;br /&gt;&amp;nbsp;&amp;nbsp;useEffect(()&amp;nbsp;=&amp;gt;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const&amp;nbsp;refetchInterval&amp;nbsp;=&amp;nbsp;setInterval(()&amp;nbsp;=&amp;gt;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;refetch();&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&amp;nbsp;1000&amp;nbsp;*&amp;nbsp;10);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;refetchInterval;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return&amp;nbsp;()&amp;nbsp;=&amp;gt;&amp;nbsp;clearInterval(refetchInterval);&lt;br /&gt;&amp;nbsp;&amp;nbsp;});&lt;br /&gt;```&lt;br /&gt;그렇게&amp;nbsp;받아온&amp;nbsp;데이터를&amp;nbsp;10초에&amp;nbsp;한번씩&amp;nbsp;다시&amp;nbsp;refetch해준다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20241026010113.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GVnwA/btsKk62zgRL/kSTpuecvLIQ2hYtCsLzOt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GVnwA/btsKk62zgRL/kSTpuecvLIQ2hYtCsLzOt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GVnwA/btsKk62zgRL/kSTpuecvLIQ2hYtCsLzOt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGVnwA%2FbtsKk62zgRL%2FkSTpuecvLIQ2hYtCsLzOt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;152&quot; data-filename=&quot;Pasted image 20241026010113.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;refetch.gif&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duuIu6/btsKlajeGxd/49kGfo5L1PbfMZHD0LRX90/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duuIu6/btsKlajeGxd/49kGfo5L1PbfMZHD0LRX90/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duuIu6/btsKlajeGxd/49kGfo5L1PbfMZHD0LRX90/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/duuIu6/btsKlajeGxd/49kGfo5L1PbfMZHD0LRX90/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;374&quot; height=&quot;261&quot; data-filename=&quot;refetch.gif&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;정상작동함...ㅠㅠ&lt;br /&gt;근데&amp;nbsp;infinitequery가&amp;nbsp;6개씩받아오고,&amp;nbsp;이미지가&amp;nbsp;1개씩&amp;nbsp;받아와야해서&amp;nbsp;6개의&amp;nbsp;HTTP요청에&amp;nbsp;제한이&amp;nbsp;걸려&amp;nbsp;해당부분을&amp;nbsp;해결해야할것도&amp;nbsp;같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;infiniteQuery부분의 promise.all을 사용한 createSignedUrl도 합칠수있으면 합치면 좋을 것 같고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변하는 이미지 부분도 한 rpc에서 createSignedUrl로 변경할 수 있으면 매우 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 상황이 그냥 한글을 보고 읽을수있는데 뜻을 모르는 상황같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 FE개발트랙에서 배워야할 이유는 없는 것이지만 알아둬서 나쁠 이유는 없다고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;baas사용 프로젝트 rpc를 사용해서 성능 최적화를 이끌어냈다는게 나쁜건 아닐거라고 생각한다.&amp;nbsp;&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/350</guid>
      <comments>https://codingpracticenote.tistory.com/350#entry350comment</comments>
      <pubDate>Sat, 26 Oct 2024 01:38:44 +0900</pubDate>
    </item>
    <item>
      <title>24.10.22 supabase OAuth 관련 에러 수정</title>
      <link>https://codingpracticenote.tistory.com/349</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 문제발생&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth관련 설정 후 소셜로그인 하여도 에러가 발생하는 문제&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 원인추론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;깃허브와 supabase관련 데이터 입력이 잘못되었나?&lt;/li&gt;
&lt;li&gt;Auth를 팔 때마다 깃허브 OAuth Apps의 키를 새로 발급받아야 하나?&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 해결방안&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 추론 모두 해결되지 않아서 네트워크 탭의 에러코드와 메시지를 파악함&lt;/li&gt;
&lt;li&gt;확인해보니 에러코드가 500번&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VioX0/btsKfbouWRu/4Vbyy3M7Hsy2BF0Dc3rN6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VioX0/btsKfbouWRu/4Vbyy3M7Hsy2BF0Dc3rN6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VioX0/btsKfbouWRu/4Vbyy3M7Hsy2BF0Dc3rN6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVioX0%2FbtsKfbouWRu%2F4Vbyy3M7Hsy2BF0Dc3rN6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;62&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 에러는 데이터베이스 로그를 식별해보라하여 DB의 Log를 확인해봄&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1729528888033&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;component&quot;:&quot;api&quot;,&quot;error&quot;:&quot;failed to close prepared statement: 
ERROR: current transaction is aborted, commands ignored until end of transaction block 
(SQLSTATE 25P02): 
ERROR: null value in column \&quot;avatar_url\&quot; of relation \&quot;users\&quot; violates not-null constraint (SQLSTATE 23502)&quot;,
&quot;level&quot;:&quot;error&quot;,&quot;method&quot;:&quot;GET&quot;,&quot;msg&quot;:&quot;500: Database error saving new user&quot;,
&quot;path&quot;:&quot;/callback&quot;,&quot;referer&quot;:&quot;http://localhost:3000/&quot;,&quot;remote_addr&quot;:&quot;218.148.199.243&quot;,
&quot;request_id&quot;:&quot;8d62a1a0d7c3d1e3-ICN&quot;,&quot;time&quot;:&quot;2024-10-21T16:30:18Z&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1729528955233&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;component&quot;:&quot;api&quot;,&quot;error&quot;:&quot;failed to close prepared statement: 
ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02): 
ERROR: column \&quot;image_url\&quot; of relation \&quot;users\&quot; does not exist (SQLSTATE 42703)&quot;,
&quot;level&quot;:&quot;error&quot;,&quot;method&quot;:&quot;POST&quot;,&quot;msg&quot;:&quot;500: Database error saving new user&quot;,
&quot;path&quot;:&quot;/signup&quot;,&quot;referer&quot;:&quot;http://localhost:3000&quot;,&quot;remote_addr&quot;:&quot;1.227.78.56&quot;,
&quot;request_id&quot;:&quot;8d629c9e15a0d1f3-ICN&quot;,&quot;time&quot;:&quot;2024-10-21T16:26:52Z&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해 보니 users테이블의 image_url column이 없는 것과 avatar이 Nullable하지 않은 것이 문제로 파악되어 해당 부분 추가 및 수정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE80gu/btsKebJRluu/H70MplUtxvr9LZmM7CtIT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE80gu/btsKebJRluu/H70MplUtxvr9LZmM7CtIT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE80gu/btsKebJRluu/H70MplUtxvr9LZmM7CtIT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcE80gu%2FbtsKebJRluu%2FH70MplUtxvr9LZmM7CtIT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;662&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 결과&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상로그인 성공함&lt;/p&gt;</description>
      <category>2차 공부/최종프로젝트</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/349</guid>
      <comments>https://codingpracticenote.tistory.com/349#entry349comment</comments>
      <pubDate>Tue, 22 Oct 2024 01:43:42 +0900</pubDate>
    </item>
    <item>
      <title>24.10.20 NextJS react-hook-form Zod</title>
      <link>https://codingpracticenote.tistory.com/348</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Zod라이브러리는 간단한 구조부터 복잡한 구조까지 타입검증을 도와주는 라이브러리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zod.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://zod.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729350720741&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference&quot; data-og-description=&quot;TypeScript-first schema validation with static type inference - colinhacks/zod&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://zod.dev/&quot; data-og-url=&quot;https://github.com/colinhacks/zod&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OXXpI/hyXhSjXjHg/j5ldKdywanFoD0csyR0yq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_155_1044_215,https://scrap.kakaocdn.net/dn/cq0S2z/hyXhSK2eh9/eBmrSsADkCBO5gUI322o20/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_155_1044_215&quot;&gt;&lt;a href=&quot;https://zod.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://zod.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OXXpI/hyXhSjXjHg/j5ldKdywanFoD0csyR0yq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_155_1044_215,https://scrap.kakaocdn.net/dn/cq0S2z/hyXhSK2eh9/eBmrSsADkCBO5gUI322o20/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_155_1044_215');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TypeScript-first schema validation with static type inference - colinhacks/zod&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729350707657&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i zod
yarn/pnpm add zod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729351087360&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { z } from &quot;zod&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;z를 가지고 schema를 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1729351356163&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const signInSchema = z.object({
    email: z.string().email({ message: &quot;invalid email&quot; }),
    password: z.string(),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 이메일이 string, email형식임을 미리 선언하고 비밀번호가 string임을 미리 선언해서 스키마를 만들어둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1729351557881&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const onSubmit = (value: FieldValues) =&amp;gt; {
    signInSchema.parse(value);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 눌렀을 때 기존에 유효성 검사를 하던 것을 어느정도는 대신해주는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 파싱 작업을 콘솔로 찍었을 때 만약 파싱을 통과한다면 value객체 자체를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s9diU/btsKcYDhRum/ArP4TdhbT7NT59XlNg5BuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s9diU/btsKcYDhRum/ArP4TdhbT7NT59XlNg5BuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s9diU/btsKcYDhRum/ArP4TdhbT7NT59XlNg5BuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs9diU%2FbtsKcYDhRum%2FArP4TdhbT7NT59XlNg5BuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;344&quot; height=&quot;279&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패한다면 ZodError를 반환해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pTiGS/btsKckNsZ9k/9rxKiQzKKhCktuSqws67RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pTiGS/btsKckNsZ9k/9rxKiQzKKhCktuSqws67RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pTiGS/btsKckNsZ9k/9rxKiQzKKhCktuSqws67RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpTiGS%2FbtsKckNsZ9k%2F9rxKiQzKKhCktuSqws67RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;552&quot; height=&quot;102&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이곳에서 지정해준 메시지를 에러속에 담아서 리턴해주는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript는 컴파일타임에 모두 js로 바꿔주는데 zod는 런타임에도 타입에 관련된 에러를 나타내준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리가 유저이벤트같은 예기치 못한, 처리하지 못하는 런타임중의 타입검증을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 react-hook-form의 패턴매칭, 필수값 등의 옵션이나 zod의 런타임 타입검증을 합치기 위해서는 resolver가 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1729353231746&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm add @hookform/resolvers&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1729353270712&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { zodResolver } from &quot;@hookform/resolvers/zod&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리로부터 zodResolver를 가져와주고&lt;/p&gt;
&lt;pre id=&quot;code_1729353389813&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { register, handleSubmit, formState } = useForm({
    mode: &quot;onChange&quot;,
    defaultValues: {
        email: &quot;test@test.com&quot;,
        password: &quot;&quot;,
    },
    resolver: zodResolver(signInSchema),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useForm 안에 resolver속성에 zodResolver를 넣고, 그 안에 미리 만들어둔 스키마를 집어넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정해두면 react-hook-form과 연결되어 처리가 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1729354895274&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const signInSchema = z.object({
    email: z
        .string({
            message: &quot;email required&quot;,
        })
        .min(1, { message: &quot;minimum length is 1&quot; })
        .max(10, { message: &quot;maximum length is 10&quot; })
        .email({ message: &quot;invalid email&quot; }),
    password: z.string(),
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 처리해두면 길이나 타입이나 형식같은 유효성검사에 따른 다양한 메시지를 처리해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 사용성은 좋은 것 같다.&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/348</guid>
      <comments>https://codingpracticenote.tistory.com/348#entry348comment</comments>
      <pubDate>Sun, 20 Oct 2024 01:29:08 +0900</pubDate>
    </item>
    <item>
      <title>24.10.19 NextJS react-hook-form</title>
      <link>https://codingpracticenote.tistory.com/347</link>
      <description>&lt;pre id=&quot;code_1729342344081&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useState } from &quot;react&quot;;

const SignInForm = () =&amp;gt; {
  const [email, setEmail] = useState(&quot;&quot;);
  const [password, setPassword] = useState(&quot;&quot;);

  return (
    &amp;lt;form
      onSubmit={(e) =&amp;gt; {
        e.preventDefault();
      }}
      className=&quot;flex flex-col gap-4 p-5 items-center w-full m-auto&quot;
    &amp;gt;
      &amp;lt;div className=&quot;flex flex-col gap-2&quot;&amp;gt;
        &amp;lt;label htmlFor=&quot;email&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
        &amp;lt;input
          id=&quot;email&quot;
          type=&quot;email&quot;
          value={email}
          onChange={(e) =&amp;gt; setEmail(e.target.value)}
          placeholder=&quot;Email&quot;
          className=&quot;text-black&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;flex flex-col gap-2&quot;&amp;gt;
        &amp;lt;label htmlFor=&quot;password&quot;&amp;gt;Password&amp;lt;/label&amp;gt;
        &amp;lt;input
          type=&quot;password&quot;
          value={password}
          onChange={(e) =&amp;gt; setPassword(e.target.value)}
          placeholder=&quot;Password&quot;
          className=&quot;text-black&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;button
        className=&quot;bg-gray-800 text-white px-4 py-2 rounded-md&quot;
        type=&quot;submit&quot;
      &amp;gt;
        Sign In
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default SignInForm;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전형적인 리액트의 로그인 컴포넌트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState로 아이디 비밀번호 state를 만들어주고, 제어 컴포넌트로 값의 변화마다 리렌더링 시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 로그인 페이지면 어느정도 볼만하겠지만, 회원가입이나 혹은 input이 굉장히 많은 페이지에선 절대적인 코드의 양이 어마무시하게 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state생성, state핸들링, submit함수, submit전 데이터 전처리, 각 state에 대한 유효성 검사 등...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다도 한 state가 변화할때마다 해당 state와 연관된 모든 컴포넌트, 그 자식 컴포넌트가 모두 리렌더링 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 그러한 코드의 양을 줄일 수 있고, 리렌더링관리를 쉽게 할 수 있는 react-hook-form을 사용해보고자 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1729342610285&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i react-hook-form
yarn/pnpm add react-hook-form&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. useForm, handleSubmit&lt;/p&gt;
&lt;pre id=&quot;code_1729342634175&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useForm } from &quot;react-hook-form&quot;;

const SignInForm = () =&amp;gt; {
    const { register, handleSubmit, formState } = useForm();

    const onSubmit = () =&amp;gt; {};

    return (
        &amp;lt;form onSubmit={handleSubmit(onSubmit)} className=&quot;flex flex-col gap-4 p-5 items-center w-full m-auto&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리를 설치하고 useForm을 가져와서 그 안의 register, handleSubmit, formState를 가져와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 submit을 위한 onSubmit함수를 생성해주고, form태그의 onSubmit에 handleSubmit으로 감싸서 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729342727009&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { FieldValues, useForm } from &quot;react-hook-form&quot;

const onSubmit = (value: FieldValues) =&amp;gt; {
  console.log(value)
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onSubmit에서는 value를 인자로 받아주고 타입은 react-hook-form의 FieldValues를 지정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. register&lt;/p&gt;
&lt;pre id=&quot;code_1729342867811&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input
    id=&quot;email&quot;
    type=&quot;email&quot;
    value={email}
    onChange={(e) =&amp;gt; setEmail(e.target.value)}
    placeholder=&quot;Email&quot;
    className=&quot;text-black&quot;
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 우리는 다양한 input에 value, onChange를 사용해 state를 관리해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 useForm이 제공해주는 register을 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729343006386&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input
    {...register(&quot;email&quot;, {
        required: true,
        pattern: {
            value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
            message: &quot;Invalid email address&quot;,
        },
    })}
    id=&quot;email&quot;
    type=&quot;email&quot;
    placeholder=&quot;Email&quot;
    className=&quot;text-black&quot;
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 value, onChange를 제거하고 register을 넣어준다. 그리고 첫 인자로는 해당 값의 id를 넣어준다 해당 필드는 이메일을 관리하는 필드이기때문에 email을 넣어주고, 두번쨰 인자로는 option을 객체 형식으로 지정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 option은 docs에서 참고하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biI1bU/btsKdey35gM/s8qZhQJfmdZfehtAz22AkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biI1bU/btsKdey35gM/s8qZhQJfmdZfehtAz22AkK/img.png&quot; data-alt=&quot;https://react-hook-form.com/docs/useform/register&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biI1bU/btsKdey35gM/s8qZhQJfmdZfehtAz22AkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiI1bU%2FbtsKdey35gM%2Fs8qZhQJfmdZfehtAz22AkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;823&quot; height=&quot;369&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://react-hook-form.com/docs/useform/register&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yFHnl/btsKb2TWs1e/JzYbt6SXJudK9gbtzCCe51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yFHnl/btsKb2TWs1e/JzYbt6SXJudK9gbtzCCe51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yFHnl/btsKb2TWs1e/JzYbt6SXJudK9gbtzCCe51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyFHnl%2FbtsKb2TWs1e%2FJzYbt6SXJudK9gbtzCCe51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1007&quot; height=&quot;287&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onSubmit의 인자로 받은 value가 저렇게 이쁜 모습으로 알아서 들어오는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. formState&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;formState는 어떤 값을 가지고 있을지 콘솔을 찍어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI540q/btsKdGhEXLz/7Uz7KMPKhjtLFKSKRpcIXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI540q/btsKdGhEXLz/7Uz7KMPKhjtLFKSKRpcIXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI540q/btsKdGhEXLz/7Uz7KMPKhjtLFKSKRpcIXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI540q%2FbtsKdGhEXLz%2F7Uz7KMPKhjtLFKSKRpcIXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;263&quot; height=&quot;247&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defaultValues, dirtyFields, errors,isValid&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 defaultValues가 undefined로 표시되고 있다. 우리가 useForm에서 설정해주지 않았기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1729343537204&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { register, handleSubmit, formState } = useForm({
    defaultValues: {
        email: &quot;123&quot;,
    },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 defaultValues를 지정해주면 렌더링시 해당 기본값이 필드에 들어가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P1VTN/btsKcVmd9sT/14fTVQh0BkGBgY59PZ3OdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P1VTN/btsKcVmd9sT/14fTVQh0BkGBgY59PZ3OdK/img.png&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;276&quot; data-is-animation=&quot;false&quot; style=&quot;width: 24.4066%; margin-right: 10px;&quot; data-widthpercent=&quot;24.69&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P1VTN/btsKcVmd9sT/14fTVQh0BkGBgY59PZ3OdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP1VTN%2FbtsKcVmd9sT%2F14fTVQh0BkGBgY59PZ3OdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmiANh/btsKbSjGuFk/IpIV9g5z4zjsrRBwhwbVI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmiANh/btsKbSjGuFk/IpIV9g5z4zjsrRBwhwbVI0/img.png&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;93&quot; data-is-animation=&quot;false&quot; style=&quot;width: 74.4306%;&quot; data-widthpercent=&quot;75.31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmiANh/btsKbSjGuFk/IpIV9g5z4zjsrRBwhwbVI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmiANh%2FbtsKbSjGuFk%2FIpIV9g5z4zjsrRBwhwbVI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;298&quot; height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;formState는 에러도 보유하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSYFis/btsKcNB0cBc/CeCDI1njuGkNKw8rL5a0k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSYFis/btsKcNB0cBc/CeCDI1njuGkNKw8rL5a0k0/img.png&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;258&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.34%; margin-right: 10px;&quot; data-widthpercent=&quot;52.96&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSYFis/btsKcNB0cBc/CeCDI1njuGkNKw8rL5a0k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSYFis%2FbtsKcNB0cBc%2FCeCDI1njuGkNKw8rL5a0k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQKf7J/btsKdgp7xSb/C3khk623l3VStVBw5dxb2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQKf7J/btsKdgp7xSb/C3khk623l3VStVBw5dxb2K/img.png&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;165&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;47.04&quot; style=&quot;width: 46.4972%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQKf7J/btsKdgp7xSb/C3khk623l3VStVBw5dxb2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQKf7J%2FbtsKdgp7xSb%2FC3khk623l3VStVBw5dxb2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 required로 설정한 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특정 패턴을 지키지 않았거나, 데이터가 없을 때 미리 지정해둔 메시지를 보내주는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebsFy6/btsKb9kYBLm/9wJWxnuu9SztWHc4V1tqd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebsFy6/btsKb9kYBLm/9wJWxnuu9SztWHc4V1tqd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebsFy6/btsKb9kYBLm/9wJWxnuu9SztWHc4V1tqd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebsFy6%2FbtsKb9kYBLm%2F9wJWxnuu9SztWHc4V1tqd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;508&quot; height=&quot;85&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1729345379899&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;flex flex-col gap-2&quot;&amp;gt;
    &amp;lt;label htmlFor=&quot;email&quot;&amp;gt;Email&amp;lt;/label&amp;gt;
    &amp;lt;input
        {...register(&quot;email&quot;, {
            required: true,
            pattern: {
                value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                message: &quot;Invalid email address&quot;,
            },
        })}
        id=&quot;email&quot;
        type=&quot;email&quot;
        placeholder=&quot;Email&quot;
        className=&quot;text-black&quot;
    /&amp;gt;
    {formState.errors?.email?.message}
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리가 이렇게 에러메시지를 넣어주면 화면에 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpyLxv/btsKbDUFUxH/j1G4EifDQovbPtDkxeQJt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpyLxv/btsKbDUFUxH/j1G4EifDQovbPtDkxeQJt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpyLxv/btsKbDUFUxH/j1G4EifDQovbPtDkxeQJt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpyLxv%2FbtsKbDUFUxH%2Fj1G4EifDQovbPtDkxeQJt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;118&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 formState의 isValid를 사용하면 모든 formData값이 유효한지 검사해주어 bool값에따라 원하는 기능을 제어할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1729345662679&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-gray-800 text-white px-4 py-2 rounded-md&quot; type=&quot;submit&quot; disabled={!formState.isValid}&amp;gt;
    Sign In
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 식으로 formState가 false일때는 버튼을 disabled시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729349174199&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { FieldValues, useForm } from &quot;react-hook-form&quot;;

const LoginTest = () =&amp;gt; {
    const { register, handleSubmit, formState } = useForm({
        mode: &quot;onChange&quot;,
        defaultValues: {
            email: &quot;test@test.com&quot;,
            password: &quot;&quot;,
        },
    });

    const onSubmit = (value: FieldValues) =&amp;gt; {};

    return (
        &amp;lt;form onSubmit={handleSubmit(onSubmit)}&amp;gt;
            &amp;lt;div className=&quot;flex flex-col justify-center items-center&quot;&amp;gt;
                &amp;lt;div className=&quot;border border-solid border-black&quot;&amp;gt;
                    &amp;lt;label htmlFor=&quot;email&quot;&amp;gt;이메일&amp;lt;/label&amp;gt;
                    &amp;lt;input
                        id=&quot;email&quot;
                        type=&quot;text&quot;
                        {...register(&quot;email&quot;, {
                            required: true,
                            pattern: {
                                value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                                message: &quot;Invalid email address&quot;,
                            },
                        })}
                    /&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div className=&quot;border border-solid border-black&quot;&amp;gt;
                    &amp;lt;label htmlFor=&quot;password&quot;&amp;gt;비밀번호&amp;lt;/label&amp;gt;
                    &amp;lt;input
                        id=&quot;password&quot;
                        type=&quot;password&quot;
                        {...register(&quot;password&quot;, {
                            required: true,
                        })}
                    /&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;button type=&quot;submit&quot; className=&quot;border border-solid border-black&quot;&amp;gt;
                    로그인
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/form&amp;gt;
    );
};

export default LoginTest;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/347</guid>
      <comments>https://codingpracticenote.tistory.com/347#entry347comment</comments>
      <pubDate>Sat, 19 Oct 2024 22:49:05 +0900</pubDate>
    </item>
    <item>
      <title>24.10.15 tailwind에서 제공하지 않는 style속성 사용하기</title>
      <link>https://codingpracticenote.tistory.com/346</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;98&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zt7EF/btsJ7NO7lbF/5Ko0h9YUTl0rMWd0fRZXhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zt7EF/btsJ7NO7lbF/5Ko0h9YUTl0rMWd0fRZXhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zt7EF/btsJ7NO7lbF/5Ko0h9YUTl0rMWd0fRZXhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZt7EF%2FbtsJ7NO7lbF%2F5Ko0h9YUTl0rMWd0fRZXhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;98&quot; height=&quot;383&quot; data-origin-width=&quot;98&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀프로젝트에서 전체 페이지에 적용되는 사이드바에 이런식으로 텍스트를 새로로 작성하고 싶었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6i4Qk/btsJ7a49J4j/tWPeJOkkHGyviG5np89kf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6i4Qk/btsJ7a49J4j/tWPeJOkkHGyviG5np89kf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6i4Qk/btsJ7a49J4j/tWPeJOkkHGyviG5np89kf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6i4Qk%2FbtsJ7a49J4j%2FtWPeJOkkHGyviG5np89kf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;545&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모르니까 바로 검색을 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두들 writing-mode: &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;vertical-lr&lt;span&gt; 을 사용하면 텍스트를 글자의 왼쪽에서 오른쪽 순서로 방향은 위에서 아래로 세로로 표현해준다는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkMEuw/btsJ7bJKkOg/akDDeOQ9gFfZJQim67jaA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkMEuw/btsJ7bJKkOg/akDDeOQ9gFfZJQim67jaA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkMEuw/btsJ7bJKkOg/akDDeOQ9gFfZJQim67jaA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkMEuw%2FbtsJ7bJKkOg%2FakDDeOQ9gFfZJQim67jaA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1172&quot; height=&quot;773&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;773&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 우리의 엄청난 tailwind는 해당 속성을 지원해주지 않고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 텍스트 태그의 width를 폰트크기보다 아주 조금 크게하고, width를 넘어가는 텍스트를 아래로 보낼까 했지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 맞나? 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 tailwind에서 제공해주지 않는 속성을 사용할 방법을 찾아보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbaH5o/btsJ5xUJnNM/QpUaM1kgAkhfRNYtyd3cNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbaH5o/btsJ5xUJnNM/QpUaM1kgAkhfRNYtyd3cNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbaH5o/btsJ5xUJnNM/QpUaM1kgAkhfRNYtyd3cNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbaH5o%2FbtsJ5xUJnNM%2FQpUaM1kgAkhfRNYtyd3cNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;730&quot; height=&quot;677&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 어쩌구 저쩌구 뭐라뭐라 썼는데 결국 tailwind.config.js파일의 theme안에 추가를 하면 되는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1729004020423&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  theme: {
    extend: {
      writingMode: {
        'vertical-lr': 'vertical-lr',
        'vertical-rl': 'vertical-rl',
        'horizontal-tb': 'horizontal-tb'
      },
      colors: {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 바로 해당 속성을 추가해봤다. 근데 적용이 안됐다.&lt;/p&gt;
&lt;pre id=&quot;code_1729004097565&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
  .writingMode-vertical-lr {
    writing-mode: vertical-lr;
  }
  .writingMode-vertical-rl {
    writing-mode: vertical-rl;
  }
  .writingMode-horizontal-tb {
    writing-mode: horizontal-tb;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 저 layer의 utilities를 추가하지 않는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tailwind가 어떻게 동작하는지 생각해본적이 없었는데, tailwind의 세팅코드들이 아마 tailwind에서 제공해주는 모든 속성을 저런식으로 만들어놓고 적용시켜준 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1729004208113&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Link className=&quot;writingMode-vertical-lr text-white&quot; href={'/'}&amp;gt;
  홈
&amp;lt;/Link&amp;gt;
&amp;lt;Link className=&quot;writingMode-vertical-lr text-white&quot; href={'/tech_interview'}&amp;gt;
  기술면접
&amp;lt;/Link&amp;gt;
&amp;lt;Link className=&quot;writingMode-vertical-lr text-white&quot; href={'/resume'}&amp;gt;
  이력서
&amp;lt;/Link&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들어둔 class를 저렇게 className에 넣으면 당연하게도 적용이 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 의미에서 tailwind가 next와 잘 맞지 않나 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저런 커스텀 스타일을 만들어두고, Root에 위치한 globals.css에 추가를 하면 next의 모든 페이지는 레이어드되어있기 때문에 해당 클래스명을 사용하면 css가 적용되는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfi4t2/btsJ6Uhbxtb/NrmwHkaTdkVZnu8lGYoxAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfi4t2/btsJ6Uhbxtb/NrmwHkaTdkVZnu8lGYoxAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfi4t2/btsJ6Uhbxtb/NrmwHkaTdkVZnu8lGYoxAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfi4t2%2FbtsJ6Uhbxtb%2FNrmwHkaTdkVZnu8lGYoxAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;919&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 어쨌든 처음엔 왜 지원을 안하는건지 싶었다. 이슈도 이미 생성되어있었고 원하는사람도 많았는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가하는 법이 어렵진 않아 쉽게 적용할 수 있었다.&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/346</guid>
      <comments>https://codingpracticenote.tistory.com/346#entry346comment</comments>
      <pubDate>Tue, 15 Oct 2024 23:59:31 +0900</pubDate>
    </item>
    <item>
      <title>24.10.14 supabase.insert, promise.all과 transaction</title>
      <link>https://codingpracticenote.tistory.com/345</link>
      <description>&lt;pre id=&quot;code_1728917913767&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    !!eduArray.length &amp;amp;&amp;amp;
      eduArray.forEach(async (edu) =&amp;gt; {
        const eduFormData = {
          post_id: postId,
          user_uuid: userId,
          ...edu
        };
        const { data: eduPostDtat, error: eduPostError } = await browserClient
          .from('post_detail_education')
          .insert([eduFormData]);
      });

    !!expArray.length &amp;amp;&amp;amp;
      expArray.forEach(async (exp) =&amp;gt; {
        const expFormData = {
          post_id: postId,
          user_uuid: userId,
          ...exp
        };
        const { data: expPostDtat, error: expPostError } = await browserClient
          .from('post_detail_experience')
          .insert([expFormData]);
      });

    !!licArray.length &amp;amp;&amp;amp;
      licArray.forEach(async (lic) =&amp;gt; {
        const licFormData = {
          post_id: postId,
          user_uuid: userId,
          ...lic
        };
        const { data: licPostDtat, error: licPostError } = await browserClient
          .from('post_detail_license')
          .insert([licFormData]);
      });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 팀프로젝트에서 이력서에 들어가는 학력, 경력, 자격증 관련 데이터뭉치들을 supabase에 올려주는 작업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 세개를 한번에 하나씩 실행하다보니 좀 비효율적이라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러 promise요청을 병렬적으로 실행시켜주는 promise.all을 사용해보고자 공식 문서를 읽어보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XXsAi/btsJ5m5FoT2/Fdqw5KU2Mfbj3DcngHksXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XXsAi/btsJ5m5FoT2/Fdqw5KU2Mfbj3DcngHksXK/img.png&quot; data-alt=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XXsAi/btsJ5m5FoT2/Fdqw5KU2Mfbj3DcngHksXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXXsAi%2FbtsJ5m5FoT2%2FFdqw5KU2Mfbj3DcngHksXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;296&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise.all()의 괄호안에 배열속에 어떤 요청들을 넣어주면 병렬적으로 그 요청이 실행되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 의문이 들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1728918118168&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = new Promise(resolve=&amp;gt;resolve(3초가 걸리는 작업))
const b = new Promise(resolve=&amp;gt;resolve(20초가 걸리는 작업))
const c = new Promise(resolve=&amp;gt;resolve(3초가 걸리는 작업))

Promise.all([a,b,c]).then(...).catch(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 여러 작업을 한번에 진행할 때 a,c는 b보다 일찍 fulfilled될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다는 의미는 셋 다 post요청일 때 이미 a,c요청에 대한 데이터가 DB에 저장된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 b에서 오류가 발생해버린다면? 그런데 세 데이터가 모두 한 데이터와 연관이 있어서 하나라도 실패하면 무의미해진다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼이제 귀찮아지는거다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨋든 한번 화면으로 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1728920301080&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
        resolve(console.log(&quot;a끝!&quot;));
    }, 3000);
});

const b = new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
        reject(console.log(&quot;b는 끝내지 못했어!&quot;));
        // resolve(console.log(&quot;b끝!&quot;));
    }, 6000);
});

const c = new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
        resolve(console.log(&quot;c끝!&quot;));
    }, 4000);
});

Promise.all([a, b, c])
    .then(() =&amp;gt; console.log(&quot;then&quot;))
    .catch(() =&amp;gt; console.log(&quot;catch&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드가 있다고 가정해보자. 실패할 예정인 b로직은 6초후에 실패하고 성공할 예정인 a,c로직은 3,4초뒤에 성공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mdn에 애매하게 설명해둔 설명대로라면 모두 실행되지않고 b가 실패해 b는 끝내지 못했어! 만 출력될 것 같지만&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8VRIo/btsJ4eATY1t/HaG3OAuaiExoo9TwZpTvX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8VRIo/btsJ4eATY1t/HaG3OAuaiExoo9TwZpTvX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8VRIo/btsJ4eATY1t/HaG3OAuaiExoo9TwZpTvX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8VRIo%2FbtsJ4eATY1t%2FHaG3OAuaiExoo9TwZpTvX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;273&quot; height=&quot;80&quot; data-origin-width=&quot;273&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실상은 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Promise.all은 도대체 어떤 녀석일까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 모든 요청을 한번에 .then이나 .catch로 보내는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에서는 따로따로 요청이 처리되고 후 로직을 기다리고 있는 것&lt;/p&gt;
&lt;pre id=&quot;code_1728920462962&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    const a =
      !!eduArray.length &amp;amp;&amp;amp;
      eduArray.forEach(async (edu) =&amp;gt; {
        //...
        const { data: eduPostDtat, error: eduPostError } = await browserClient
          .from('post_detail_education')
          .insert([eduFormData]);
      });

    const b =
      !!expArray.length &amp;amp;&amp;amp;
      expArray.forEach(async (exp) =&amp;gt; {
        //...
        const { data: expPostDtat, error: expPostError } = await browserClient
          .from('post_detail_experience')
          .insert([expFormData]);
      });

    const c =
      !!licArray.length &amp;amp;&amp;amp;
      licArray.forEach(async (lic) =&amp;gt; {
        //...
        const { data: licPostDtat, error: licPostError } = await browserClient
          .from('post_detail_license')
          .insert([licFormData]);
      });
    const d = new Promise((reject) =&amp;gt; {
      setTimeout(() =&amp;gt; reject('실패!'), 5000);
    });
    Promise.all([a, b, c, d])
      .then((res) =&amp;gt; console.log('res :&amp;gt;&amp;gt; ', res))
      .catch((err) =&amp;gt; console.log('err :&amp;gt;&amp;gt; ', err));
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 supabase에 insert하는 요청을 Promise.all로 처리한다면 insert인 post요청이 각각 일어나고, 결국엔 catch로 에러처리가 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCt66q/btsJ4QlTJ4O/PcdApApDYtitFDckDHJ8aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCt66q/btsJ4QlTJ4O/PcdApApDYtitFDckDHJ8aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCt66q/btsJ4QlTJ4O/PcdApApDYtitFDckDHJ8aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCt66q%2FbtsJ4QlTJ4O%2FPcdApApDYtitFDckDHJ8aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;308&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터를 각각의 table에 insert하는 요청은 일어나고, d라는 Promise가 reject되므로 catch로 이어지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;31&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZbVCh/btsJ4vvweOt/b5o4BlYRQ1FAd8dIpsMhIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZbVCh/btsJ4vvweOt/b5o4BlYRQ1FAd8dIpsMhIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZbVCh/btsJ4vvweOt/b5o4BlYRQ1FAd8dIpsMhIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZbVCh%2FbtsJ4vvweOt%2Fb5o4BlYRQ1FAd8dIpsMhIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;975&quot; height=&quot;31&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;31&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caS7Qv/btsJ59ZfpUN/vuWiKCkINeGkdWCGiPaoVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caS7Qv/btsJ59ZfpUN/vuWiKCkINeGkdWCGiPaoVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caS7Qv/btsJ59ZfpUN/vuWiKCkINeGkdWCGiPaoVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaS7Qv%2FbtsJ59ZfpUN%2FvuWiKCkINeGkdWCGiPaoVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;33&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 데이터는 각 테이블에 들어갔지만, catch처리된 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 저렇게 의미없어진(연결이 끊긴)데이터를 어떻게 처리해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저런 요청단위를 DB에선 Transaction이라고 부른다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCGeo/btsJ4RdY7Bq/AbTTr2j8Qnkj5WgKxaby3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCGeo/btsJ4RdY7Bq/AbTTr2j8Qnkj5WgKxaby3K/img.png&quot; data-alt=&quot;https://smin1620.tistory.com/325&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCGeo/btsJ4RdY7Bq/AbTTr2j8Qnkj5WgKxaby3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCGeo%2FbtsJ4RdY7Bq%2FAbTTr2j8Qnkj5WgKxaby3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;332&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;665&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://smin1620.tistory.com/325&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 상태를 변화시키기 위해 수행하는 작업 단위라고 하는데 뭐 어쨌든 깊이 들어가지않겠다 BE의 영역이기 때문에&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BiK11/btsJ6scY1OZ/yUfWQL9kAQMHRJe0mQRGn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BiK11/btsJ6scY1OZ/yUfWQL9kAQMHRJe0mQRGn1/img.png&quot; data-alt=&quot;https://smin1620.tistory.com/325&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BiK11/btsJ6scY1OZ/yUfWQL9kAQMHRJe0mQRGn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBiK11%2FbtsJ6scY1OZ%2FyUfWQL9kAQMHRJe0mQRGn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;633&quot; height=&quot;195&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://smin1620.tistory.com/325&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 난 위 사진과 같은 rollback의 기능이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1728921200246&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const insertEducation = async (eduArray, postId, userId) =&amp;gt; {
  try {
    await Promise.all(
      eduArray.map(async (edu) =&amp;gt; {
        const eduFormData = {
          post_id: postId,
          user_uuid: userId,
          ...edu,
        };
        const { error: eduPostError } = await browserClient
          .from(&quot;post_detail_education&quot;)
          .insert([eduFormData]);

        if (eduPostError) throw new Error(&quot;Education insert failed&quot;);
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: &quot;education&quot;, message: error.message };
  }
};

const insertExperience = async (expArray, postId, userId) =&amp;gt; {
  try {
    await Promise.all(
      expArray.map(async (exp) =&amp;gt; {
        const expFormData = {
          post_id: postId,
          user_uuid: userId,
          ...exp,
        };
        const { error: expPostError } = await browserClient
          .from(&quot;post_detail_experience&quot;)
          .insert([expFormData]);

        if (expPostError) throw new Error(&quot;Experience insert failed&quot;);
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: &quot;experience&quot;, message: error.message };
  }
};

const insertLicense = async (licArray, postId, userId) =&amp;gt; {
  try {
    await Promise.all(
      licArray.map(async (lic) =&amp;gt; {
        const licFormData = {
          post_id: postId,
          user_uuid: userId,
          ...lic,
        };
        const { error: licPostError } = await browserClient
          .from(&quot;post_detail_license&quot;)
          .insert([licFormData]);

        if (licPostError) throw new Error(&quot;License insert failed&quot;);
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: &quot;license&quot;, message: error.message };
  }
};

const handleRollback = async (postId) =&amp;gt; {
  // 삽입 실패 시 rollback 처리
  console.log(&quot;Rolling back changes...&quot;);
  await browserClient.from(&quot;post_detail_education&quot;).delete().eq(&quot;post_id&quot;, postId);
  await browserClient.from(&quot;post_detail_experience&quot;).delete().eq(&quot;post_id&quot;, postId);
  await browserClient.from(&quot;post_detail_license&quot;).delete().eq(&quot;post_id&quot;, postId);
  console.log(&quot;Rollback completed&quot;);
};

const runInserts = async () =&amp;gt; {
  const postId = &quot;post123&quot;; // 예시 postId
  const userId = &quot;user123&quot;; // 예시 userId

  const eduArray = [{ title: &quot;edu1&quot; }, { title: &quot;edu2&quot; }];
  const expArray = [{ title: &quot;exp1&quot; }];
  const licArray = [{ title: &quot;lic1&quot; }];

  try {
    // Promise.all로 모든 삽입 작업 실행
    const results = await Promise.all([
      insertEducation(eduArray, postId, userId),
      insertExperience(expArray, postId, userId),
      insertLicense(licArray, postId, userId),
    ]);

    // 실패한 작업이 있는지 확인
    const failed = results.find((result) =&amp;gt; !result.success);
    if (failed) {
      throw new Error(`Failed on ${failed.error}: ${failed.message}`);
    }

    console.log(&quot;All inserts successful&quot;);
  } catch (err) {
    console.log(&quot;Error occurred:&quot;, err.message);
    await handleRollback(postId); // 실패 시 롤백
  }
};

runInserts();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 이렇게 처리해야한다고 한다. 솔직히말하면 감당이 안된다. 조금 더 Promise.all을 사용해야하는지 고민을 해봐야할것같다 ㅋㅋ&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/345</guid>
      <comments>https://codingpracticenote.tistory.com/345#entry345comment</comments>
      <pubDate>Tue, 15 Oct 2024 00:56:38 +0900</pubDate>
    </item>
    <item>
      <title>24.10.11 react에서 리스트 매핑중 key를 사용해야하는 이유</title>
      <link>https://codingpracticenote.tistory.com/344</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제상황&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cekq1X/btsJ3RK49Qg/SgduJi3Lwd9RbmlLj1fccK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cekq1X/btsJ3RK49Qg/SgduJi3Lwd9RbmlLj1fccK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cekq1X/btsJ3RK49Qg/SgduJi3Lwd9RbmlLj1fccK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcekq1X%2FbtsJ3RK49Qg%2FSgduJi3Lwd9RbmlLj1fccK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;252&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 팀 프로젝트의 mockview와 state들을 만들면서 이러한 오류가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학력의 오른쪽 + 버튼을 누르면 아래 3가지 입력창이 계속 생기는 기능을 만들고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1728647281024&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{eduArray.map((edu, idx) =&amp;gt; {
    return (
      &amp;lt;div key={crypto.randomUUID()}&amp;gt;
        &amp;lt;Input
          isLabeled={true}
          labelText=&quot;졸업년월&quot;
          value={edu.graduated_at}
          onChange={(e) =&amp;gt; handleEduArray(idx, 'graduated_at', e)}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    );
})}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 key에 crypto.randomUUID()를 넣어주어 유니크한 키 값을 가지도록 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;문제1.gif&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n2ucr/btsJ2HJOxxH/q2pz6TwTo2b6x6dgc7Udd1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n2ucr/btsJ2HJOxxH/q2pz6TwTo2b6x6dgc7Udd1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n2ucr/btsJ2HJOxxH/q2pz6TwTo2b6x6dgc7Udd1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/n2ucr/btsJ2HJOxxH/q2pz6TwTo2b6x6dgc7Udd1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;701&quot; height=&quot;459&quot; data-filename=&quot;문제1.gif&quot; data-origin-width=&quot;701&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인풋값을 입력할때마다 focus가 풀렸다. 이유가 뭘까??&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;발생 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZFByq/btsJ4o2vSNI/SpM5i9scLj9Jlp1vKVKqWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZFByq/btsJ4o2vSNI/SpM5i9scLj9Jlp1vKVKqWK/img.png&quot; data-alt=&quot;https://ko.react.dev/learn/rendering-lists#why-does-react-need-keys&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZFByq/btsJ4o2vSNI/SpM5i9scLj9Jlp1vKVKqWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZFByq%2FbtsJ4o2vSNI%2FSpM5i9scLj9Jlp1vKVKqWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;915&quot; height=&quot;280&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://ko.react.dev/learn/rendering-lists#why-does-react-need-keys&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 내부 리스트 매핑에서의 key는 결국 리액트가 해당 컴포넌트를 식별하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 또한 배열로 관리하여 key값으로 비교하게 되는데, 내가 작성한 코드는 리렌더링시마다 crypto.randomUUID()를 재실행하여 새로운 키값을 부여하기 때문에 의도치않은 성능감소와 에러를 발생시켰던 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 시도&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1728658170766&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const addForm = (objName: string) =&amp;gt; {
    switch (objName) {
      case 'education':
        setEduArray((prev) =&amp;gt; [...prev, { ...emptyEduObj, edu_id: crypto.randomUUID() }]);
        break;
      case 'experience':
        setExpArray((prev) =&amp;gt; [...prev, { ...emptyExpObj, exp_id: crypto.randomUUID() }]);
        break;
      case 'license':
        setLicArray((prev) =&amp;gt; [...prev, { ...emptyLicObj, lic_id: crypto.randomUUID() }]);
        break;
    }
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열에 객체를 추가할 때 랜덤값을 생성하여 각각의 아이디로 주입하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 생성당시에 값이 추가되어 리렌더링시에도 키값이 변경될 일이 없기 때문에 리액트는 동일한 컴포넌트로 인식하여 focus를 유지하는 것으로 생각했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;해결.gif&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJTRo4/btsJ3SDg85Y/xxKaanlxfTTyr4ouENt8Gk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJTRo4/btsJ3SDg85Y/xxKaanlxfTTyr4ouENt8Gk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJTRo4/btsJ3SDg85Y/xxKaanlxfTTyr4ouENt8Gk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bJTRo4/btsJ3SDg85Y/xxKaanlxfTTyr4ouENt8Gk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;309&quot; data-filename=&quot;해결.gif&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트가 같은 컴포넌트로 인식하여 리렌더링과 input focus의 버그를 발생시키지 않는 모습이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnn7j/btsJ26bbTwn/oOxMwbVIq4gHYTktPDa0T0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnn7j/btsJ26bbTwn/oOxMwbVIq4gHYTktPDa0T0/img.gif&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-is-animation=&quot;true&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnn7j/btsJ26bbTwn/oOxMwbVIq4gHYTktPDa0T0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flnn7j%2FbtsJ26bbTwn%2FoOxMwbVIq4gHYTktPDa0T0%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beV3Ac/btsJ4hP18v4/BzB2MIqFvWdua8DOrY20k0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beV3Ac/btsJ4hP18v4/BzB2MIqFvWdua8DOrY20k0/img.gif&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot; data-is-animation=&quot;true&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beV3Ac/btsJ4hP18v4/BzB2MIqFvWdua8DOrY20k0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeV3Ac%2FbtsJ4hP18v4%2FBzB2MIqFvWdua8DOrY20k0%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;https://tecoble.techcourse.co.kr/post/2021-04-25-react-key/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 느낌인지 비교하기 쉬운 사진을 가져와 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/344</guid>
      <comments>https://codingpracticenote.tistory.com/344#entry344comment</comments>
      <pubDate>Fri, 11 Oct 2024 23:53:37 +0900</pubDate>
    </item>
    <item>
      <title>24.10.08 개인프로젝트 마무리</title>
      <link>https://codingpracticenote.tistory.com/342</link>
      <description>&lt;div align=&quot;center&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://next-task-lol.vercel.app/&quot;&gt;리그오브레전드 정보 페이지 바로가기&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;LIP는 &lt;/span&gt;&lt;b&gt;무료&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;로 &lt;/span&gt;&lt;b&gt;로그인 없이&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 사용할 수 있는 리그오브레전드 정보 제공 서비스입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next&lt;/b&gt;와 &lt;b&gt;React&lt;/b&gt;, &lt;b&gt;TypeScript&lt;/b&gt;를 사용하여 개발하였으며&lt;br /&gt;원하는 챔피언이나 아이템의 정보를 확인하고&lt;br /&gt;이상형월드컵을 통해 인기 챔피언을 알아볼 수 있습니다!&lt;/p&gt;
&lt;br /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/e0964021-94e1-475d-af90-026242382d02&quot; alt=&quot;image&quot; width=&quot;747&quot; height=&quot;358&quot; /&gt;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A6%AC%EA%B7%B8%EC%98%A4%EB%B8%8C%EB%A0%88%EC%A0%84%EB%93%9C-%EC%A0%95%EB%B3%B4-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B0%94%EB%A1%9C%EA%B0%80%EA%B8%B0&quot;&gt;리그오브레전드 정보 페이지 바로가기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lip-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%8A%94-%EC%9D%B4%EB%9F%B0%EA%B1%B8-%ED%95%A0-%EC%88%98-%EC%9E%88%EC%96%B4%EC%9A%94&quot;&gt;LIP 서비스는 이런걸 할 수 있어요!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9D%B8%EA%B3%BC%EC%A0%9C%EC%97%90%EC%84%9C-%EC%9D%B4%EB%9F%B0%EA%B1%B8-%ED%96%88%EC%96%B4%EC%9A%94&quot;&gt;개인과제에서 이런걸 했어요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lip%EC%97%90%EC%84%A0-%EC%9D%B4%EB%9F%B0-%EA%B8%B0%EC%88%A0%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%96%88%EC%96%B4%EC%9A%94&quot;&gt;LIP에선 이런 기술을 사용했어요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lip%EC%9D%98-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84%EB%8F%84&quot;&gt;LIP의 서비스 요청 흐름도&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%ED%94%BC%EB%93%9C%EB%B0%B1&quot;&gt;피드백&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LIP 서비스는 이런걸 할 수 있어요!&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/8c348263-1c85-4cb4-aea3-6dba56902481&quot; alt=&quot;01챔피언목록&quot; width=&quot;604&quot; height=&quot;324&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/5739a5cc-f34e-45bb-bc21-cde59a70210b&quot; alt=&quot;02챔피언상세&quot; width=&quot;603&quot; height=&quot;323&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/34abebd1-a325-4012-ad33-bdfbed6f0e04&quot; alt=&quot;03아이템목록&quot; width=&quot;604&quot; height=&quot;324&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/4af5caf7-8b61-4b99-90c3-9a5a62972fb7&quot; alt=&quot;04아이템상세&quot; width=&quot;603&quot; height=&quot;323&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/c44de086-12ef-4e9d-9cdd-859103635a2a&quot; alt=&quot;05로테이션&quot; width=&quot;601&quot; height=&quot;322&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/62065653-e12c-4582-a151-bec550235aae&quot; alt=&quot;06이상형월드컵&quot; width=&quot;601&quot; height=&quot;322&quot; /&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/c6e0b7f0-970b-4409-9c7e-7d298bb20245&quot; alt=&quot;07이상형월드컵결과&quot; width=&quot;601&quot; height=&quot;322&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개인과제에서 이런걸 했어요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NextJS를 사용하며 발생한 여러 문제들&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 분리 후 스타일이 적용되지 않는 문제 - &lt;a href=&quot;#%EC%A0%84%EC%83%81%EA%B5%AD&quot;&gt;전상국&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;useSuspenseQuery와 hydration 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LIP에선 이런 기술을 사용했어요&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/32a4a41c-1425-41e1-8990-2717533dc726&quot; alt=&quot;기술의사결정&quot; width=&quot;767&quot; height=&quot;453&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LIP의 서비스 요청 흐름도&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/ccf9b24f-6bf2-4259-8080-634ac1bedd30&quot; alt=&quot;FE Architecture&quot; width=&quot;756&quot; height=&quot;663&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전상국&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/escape-engineering&quot;&gt;&lt;img src=&quot;https://avatars.githubusercontent.com/u/117638805?s=400&amp;amp;v=4&quot; width=&quot;150&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://codingpracticenote.tistory.com/340&quot;&gt;분리된 컴포넌트의 스타일이 왜 적용되지 않는걸까 - 전상국&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;useSuspenseQuery + CSR ==&amp;gt; hydration Error&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/342</guid>
      <comments>https://codingpracticenote.tistory.com/342#entry342comment</comments>
      <pubDate>Tue, 8 Oct 2024 21:28:04 +0900</pubDate>
    </item>
    <item>
      <title>24.10.07 nextjs 에러핸들링</title>
      <link>https://codingpracticenote.tistory.com/341</link>
      <description>&lt;pre id=&quot;code_1728388603944&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;todoList&quot;: [
        {
            &quot;id&quot;: &quot;adsf&quot;,
            &quot;title&quot;: &quot;titleTest&quot;,
            &quot;desc&quot;: &quot;DescTest&quot;,
            &quot;isCompleted&quot;: false,
            &quot;createdAt&quot;: 123
        },
        {
            &quot;id&quot;: &quot;adsd&quot;,
            &quot;title&quot;: &quot;titleTest&quot;,
            &quot;desc&quot;: &quot;DescTest&quot;,
            &quot;isCompleted&quot;: false,
            &quot;createdAt&quot;: 123
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json-server를 이용하여 간단한 투두리스트를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 개인과제를 진행하며 제일 궁금했던건 CSR페이지에서 route.ts로 fetch를 했을 때 에러핸들링을 어떻게 할 것이냐는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1728388822823&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Todo } from &quot;@/types/Todo&quot;;
import { NextResponse } from &quot;next/server&quot;;

export const BASEURL = &quot;http://localhost:4000&quot;;

export async function GET() {
    try {
        const res = await fetch(`${BASEURL}/todoList`, { cache: &quot;no-store&quot; });
        const data: Todo[] = await res.json();
        return NextResponse.json({ data });
    } catch (error) {
        console.log(&quot;error :&amp;gt;&amp;gt; &quot;, error);
        throw { message: &quot;에러발생&quot; };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;route handler는 이렇게 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try catch를 사용하여 에러를 잡아내 에러를 던져준다.&lt;/p&gt;
&lt;pre id=&quot;code_1728390319117&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { Todo } from &quot;@/types/Todo&quot;;
import { useEffect, useState } from &quot;react&quot;;

const BASEURL = &quot;http://localhost:3000&quot;;
const HomePage = () =&amp;gt; {
    const [todos, setTodos] = useState&amp;lt;Todo[]&amp;gt;();
    const [isError, setIsError] = useState(false);
    const fetchTodos = async () =&amp;gt; {
        try {
            const todoRes = await fetch(`${BASEURL}/api/todos`);
            console.log(&quot;todoRes :&amp;gt;&amp;gt; &quot;, todoRes);
            const { data: todoData }: { data: Todo[] } = await todoRes.json();
            setTodos(todoData);
        } catch {
            console.log(&quot;1 :&amp;gt;&amp;gt; &quot;, 1);
            setIsError(true);
        }
    };
    useEffect(() =&amp;gt; {
        fetchTodos();
    }, []);
    if (isError) throw new Error(&quot;1`&quot;);
    return (
        &amp;lt;div className=&quot;flex flex-col justify-center items-center py-[200px]&quot;&amp;gt;
            &amp;lt;h1&amp;gt;투두리스트&amp;lt;/h1&amp;gt;
            &amp;lt;ul&amp;gt;
            	//...
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    );
};

export default HomePage;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음에는 try catch를 여기서도 사용하여 throw new Error를 통해 에러페이지로 이동하게끔 했는데, useEffect 안에서 throw되어서 그런지 에러를 잡아내지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 에러상태를 state로 만들어주고, catch구문으로 들어갈 시 setIsError를 true로 만들어 에러페이지를 화면에 보여주게끔 설정하였다.&lt;/p&gt;</description>
      <category>2차 공부/TIL</category>
      <author>공대탈출</author>
      <guid isPermaLink="true">https://codingpracticenote.tistory.com/341</guid>
      <comments>https://codingpracticenote.tistory.com/341#entry341comment</comments>
      <pubDate>Tue, 8 Oct 2024 21:26:47 +0900</pubDate>
    </item>
  </channel>
</rss>