<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>doyI-T</title>
    <link>https://2-doooo-2.tistory.com/</link>
    <description>피어나는 프론트엔드 개발자 이도이입니다. 방문해주셔서 감사합니다 </description>
    <language>ko</language>
    <pubDate>Thu, 9 Jul 2026 13:21:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>2-doooo-2</managingEditor>
    <image>
      <title>doyI-T</title>
      <url>https://tistory1.daumcdn.net/tistory/5946626/attach/856230a2433c4e97a7117dad97f4c975</url>
      <link>https://2-doooo-2.tistory.com</link>
    </image>
    <item>
      <title>[구름톤] - 구름톤 in Jeju 13기 후기(우수상 )</title>
      <link>https://2-doooo-2.tistory.com/191</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rSrEw/btsMxmodWic/W3LTjzBpdvKtUixeBh2RJk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rSrEw/btsMxmodWic/W3LTjzBpdvKtUixeBh2RJk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rSrEw/btsMxmodWic/W3LTjzBpdvKtUixeBh2RJk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrSrEw%2FbtsMxmodWic%2FW3LTjzBpdvKtUixeBh2RJk%2Fimg.webp&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;700&quot; height=&quot;365&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1068&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;☁️구름톤 지원하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;73&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;작년에 부트캠프에서 같이 교육 듣던 친구가 추천해준 해커톤! 특별한 계기보다는 재미있어 보였다! 13기 모집 알림을 신청해두었고, 드디어 문자로 알림이 왔다.&lt;/p&gt;
&lt;p data-end=&quot;73&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;108&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&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;b&gt;(선택) 시중에 나와 있는 디자인 시스템(Material Design, Radix UI, Chakra UI)을 사용해 본 적이 있나요? 있다면 어떤 디자인 시스템을 사용했고, 어떻게 활용했는지 구체적으로 설명해주세요.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(선택) 다른 디자이너나 개발자와 협업하여 디자인시스템 라이브러리를 배포하거나 개선한 경험이 있나요? 있다면 구체적인 작업 내용을 설명해주세요.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구름톤 참여 동기를 작성해주세요.(공란 포함 최대 300자)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지원자께서 생각하시는 최근 제주도가 직면한 사회 문제는 무엇이라고 생각하나요? (공란 포함 최대 300자)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구름톤에서 2번의 사회문제를 해결하기 위해 어떤 서비스를 만들고 싶은지 설명해주세요.(공란 포함 최대 500자)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구름톤에서는 '팀원과의 협업'을 중요하게 생각합니다. 유연한 협업을 이끌어 나가기 위해 가지고 있는 나만의 장점을 설명해주세요.(공란 포함 최대 300자)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구름톤을 통해 어떤 부분의 '성장'을 기대하고 있는지, 있다면 작성해주세요.(공란 포함 최대 300자)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;진행하신 프로젝트 중 한 가지를 선택하여 설명해주세요.(최대 500자)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이전에 진행한 프로젝트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Radix UI&lt;/b&gt;를 사용한 경험이 있어서 선택 항목에 모두 작성했다. 취준생이라 미리 정리해둔 내용들이 있어 큰 도움이 됐다. 1월 28일에 지원서 제출하고, 2월 11일에 발표가 났다. 야호~~~ &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blCjqu/btsMvzPLM9i/hmDBlIgLeE9rxAOFoLGZu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blCjqu/btsMvzPLM9i/hmDBlIgLeE9rxAOFoLGZu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blCjqu/btsMvzPLM9i/hmDBlIgLeE9rxAOFoLGZu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblCjqu%2FbtsMvzPLM9i%2FhmDBlIgLeE9rxAOFoLGZu1%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;648&quot; height=&quot;115&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;115&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250225_223128261_01.jpg&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;779&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lHYQT/btsMuT83Wnl/Aok0YNpKY4pJoUVnn9fdzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lHYQT/btsMuT83Wnl/Aok0YNpKY4pJoUVnn9fdzK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lHYQT/btsMuT83Wnl/Aok0YNpKY4pJoUVnn9fdzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlHYQT%2FbtsMuT83Wnl%2FAok0YNpKY4pJoUVnn9fdzK%2Fimg.jpg&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;500&quot; height=&quot;345&quot; data-filename=&quot;KakaoTalk_20250225_223128261_01.jpg&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;779&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;759&quot; data-start=&quot;683&quot; data-ke-size=&quot;size16&quot;&gt;문자만 보면 떨어진 줄 알았을 듯!!&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;759&quot; data-start=&quot;683&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;해커톤은 처음이었고, 당시 여러 합불 연락을 받고 있던 터라 구름톤에 대해 크게 생각하지 않고 있었다.그런데 막상 합격 소식을 들으니 기뻤댜! 사실 기대했을지도,, ㅎㅎ 바로 비행기 예약하고 숙소도 잡았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvPxai/btsMw2QXPjW/v55itkMgJMNU3XVLSzzr1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvPxai/btsMw2QXPjW/v55itkMgJMNU3XVLSzzr1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvPxai/btsMw2QXPjW/v55itkMgJMNU3XVLSzzr1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvPxai%2FbtsMw2QXPjW%2Fv55itkMgJMNU3XVLSzzr1k%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;477&quot; height=&quot;220&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;숙소 추천해주시는데 난 블로그 글 보고 이미 라마다 시티 호텔로 예약해놨다 ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;그리고 그 전날에는 진짜 부담X100000, 자기 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;PR&lt;/span&gt;올라오신 분들과 쭉 읽어봤는데 현직자 분들이 너무 많았고 그리고 대표님도 계셨댜,,&amp;nbsp; 이 사람들 사이에서 나를 어떻게 어필하면 좋지?&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;오랜만에 제주도 가는데 전날에는 즐거움,, 5% 부담 95%%%% 프로처럼 즐기지 못하겠니??&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUPxn6/btsMwYVufll/xFOhIk8GipFKH2kyEyu56K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUPxn6/btsMwYVufll/xFOhIk8GipFKH2kyEyu56K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUPxn6/btsMwYVufll/xFOhIk8GipFKH2kyEyu56K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUPxn6%2FbtsMwYVufll%2FxFOhIk8GipFKH2kyEyu56K%2Fimg.jpg&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;680&quot; height=&quot;383&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;그래 진심이라 그렇다~~~~~~&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;889&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 제주도 일정 시작하기&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_20250225_233159503.jpg&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AGDg9/btsMvP5Y04F/Q5zzYK5Fu6eoQKPFyhQJ0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AGDg9/btsMvP5Y04F/Q5zzYK5Fu6eoQKPFyhQJ0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AGDg9/btsMvP5Y04F/Q5zzYK5Fu6eoQKPFyhQJ0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAGDg9%2FbtsMvP5Y04F%2FQ5zzYK5Fu6eoQKPFyhQJ0k%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;400&quot; height=&quot;610&quot; data-filename=&quot;edited_KakaoTalk_20250225_233159503.jpg&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1785&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이도이가 이도이동으로 떠나기ㅎㅎ&amp;nbsp; 일정 시작하기 하루전에 라마다 호텔로 갔고 다른 곳 둘러볼까 하다가 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;PR&lt;/span&gt;을 아직 다 작성하지 못해서 고냥 호텔에서 맥주마시면서 마저 작성했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddElP7/btsMv6TUAAP/IcaywpzloJmjEHypbdTgC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddElP7/btsMv6TUAAP/IcaywpzloJmjEHypbdTgC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddElP7/btsMv6TUAAP/IcaywpzloJmjEHypbdTgC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddElP7%2FbtsMv6TUAAP%2FIcaywpzloJmjEHypbdTgC0%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;500&quot; height=&quot;500&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 이름으로 확실히 각인될 수 있다고 생각해서 gpt가 추천해 준 멘트를 활용해봤다 ㅋㅋㅋ 너무 나대나 생각도 했지만 다들 자기 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;PR&lt;/span&gt; 생각하느라 나에겐 큰 관심이 없을 거야라고 위안을 삼고 그냥 했다. 그리고 ui/ux 화면단에서 많이 어필하고 싶어 gif도 첨부했다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1️⃣1일차 - 2월 18일 : 두근두근 설레는 첫만남~~ &lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-1. 아이스브레이킹&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTcSvZ/btsMvNAqAGY/qTa9QzMKlikL3PqjoJ5JS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTcSvZ/btsMvNAqAGY/qTa9QzMKlikL3PqjoJ5JS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTcSvZ/btsMvNAqAGY/qTa9QzMKlikL3PqjoJ5JS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTcSvZ%2FbtsMvNAqAGY%2FqTa9QzMKlikL3PqjoJ5JS1%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;600&quot; height=&quot;664&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-2일차는 구름스퀘어에서 진행됐다! &lt;b&gt;10시 이후에 입실이 가능하대서 천천히 나왔는데 55분에 어디시냐고 전화가 와서 당황했다&lt;/b&gt;ㅎㅎ 도착해 보니 미리 정해진 조에 앉아야 했고, 우리 조에는 남자분 한 분만 계셨다. 조원들이 모두 모인 후, 가볍게 인사를 나눴다!&amp;nbsp; 항상 시작하기 전에 가장 긴장하는 편이라 오히려 &lt;b&gt;현장에서 사람들이랑 대화를 좀 나누니깐 좀 긴장이 풀렸다&lt;/b&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_20250225_235947949_05.jpg&quot; data-origin-width=&quot;1687&quot; data-origin-height=&quot;2473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GGhxb/btsMvQKv6Yo/hfhFAFQKNlzSBDyeUauFA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GGhxb/btsMvQKv6Yo/hfhFAFQKNlzSBDyeUauFA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GGhxb/btsMvQKv6Yo/hfhFAFQKNlzSBDyeUauFA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGGhxb%2FbtsMvQKv6Yo%2FhfhFAFQKNlzSBDyeUauFA1%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;500&quot; height=&quot;733&quot; data-filename=&quot;edited_KakaoTalk_20250225_235947949_05.jpg&quot; data-origin-width=&quot;1687&quot; data-origin-height=&quot;2473&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 도착하자 받은 굿즈!! 텀블러 너무 마음에 든다. 처음에는 간단한 게임을 진행했다. TTS노래 맞추기랑 손병호 게임등등 진행했는데 이긴 팀은 커피 쿠폰 주시고 도시락 빨리 먹기였는데 도시락 빨리 먹어야 뭐하나~ 커피도 사먹으면 돼지 ㅎㅎ&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-2. 해커톤 강의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 &lt;b&gt;카카오 헌터(조훈)&lt;/b&gt;님께서 해커톤 강의를 해주셨는데 간략하지만 핵심적인 내용들이 많았다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;거인에 등에 올라타라&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;완벽함보다 속도에 집중해라&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;이미 만들어진 바퀴를 만들지 마라!&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 출시된 서비스 중에서 불편한 점을 찾고, 우리만의 차별화된 강점을 1~2가지를 넣자&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-3. 3-mins, SELF-PR&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 자기 PR이 시작됐는데 예상보다 고요했다! 경력 있는 분들은 더 많았고 같이 작업하면서 배우고 싶다고 생각했다. 제한시간이 3분이었는데 다른 분들이 예상보다 발표를 빨리해서 시간을 남기신 분들도 꽤 있었다. 드디어 내 차례가 돼서 발표를 했는데 하다 보니 하고 싶은 말이 많아져서 정신없이 하다가 중간에 스톱워치 보니깐 한 10초 남아 있어서 급하게 인사를 하고 자리에 돌아왔다.&amp;nbsp;&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;그리고 나머지 키워드도 공개!!!&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_KakaoTalk_20250225_235947949_03.jpg&quot; data-origin-width=&quot;1985&quot; data-origin-height=&quot;1482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbJeJL/btsMyuMHBRu/OYVQge13CC93SkArZ3Wr8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbJeJL/btsMyuMHBRu/OYVQge13CC93SkArZ3Wr8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbJeJL/btsMyuMHBRu/OYVQge13CC93SkArZ3Wr8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbJeJL%2FbtsMyuMHBRu%2FOYVQge13CC93SkArZ3Wr8k%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;400&quot; height=&quot;299&quot; data-filename=&quot;edited_edited_KakaoTalk_20250225_235947949_03.jpg&quot; data-origin-width=&quot;1985&quot; data-origin-height=&quot;1482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 공유 경제가 다른 기수에서 나왔던 생성형 AI, &lt;span style=&quot;background-color: #ffffff; color: #334151; text-align: left;&quot;&gt;K-SDGS보다 수월하다고 생각했다!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1-4. GDS 강의 및 실습&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로 강의 및 실습시간이 이어졌다. 두 개의 강의 중 선택할 수 있는 줄 알았는데, &lt;b&gt;프론트엔드 개발자와 디자이너는 GDS를, 백엔드 개발자는 크램폴린 IDE 실습 강의를 나눠서 들었다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구름의 디디님이 피그마로 GDS를 적용하는 실습을 진행하셨고 구름의 제로님이 React+Vite+Vapor를 이용한 강의를 진행하셨다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;디자인 시스템이 무엇인지, 왜 디자인 시스템을 도입해야 하는지 등 강의 내용은 정말 유용했고, 체계적으로 구축된 시스템이 개발 속도를 높이는 데 큰 역할을 한다는 걸 실감했다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;249&quot; data-start=&quot;182&quot; data-ke-size=&quot;size16&quot;&gt;다만, 실습 시간이 너무 짧아서 아쉬웠다. 끝까지 적용해보고 싶었는데, 실습 시간이 20분도 채 안 됐던 것 같다.  &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;1일 차 마치고 조원들이랑 멘토님한테 추천받은 돈사무소라는 고깃집으로 고기를 먹으러 갔다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250225_235947949_09.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;2561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9rNMo/btsMxlQxtRW/KXwYfmFzrkdXFvr90wa4PK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9rNMo/btsMxlQxtRW/KXwYfmFzrkdXFvr90wa4PK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9rNMo/btsMxlQxtRW/KXwYfmFzrkdXFvr90wa4PK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9rNMo%2FbtsMxlQxtRW%2FKXwYfmFzrkdXFvr90wa4PK%2Fimg.jpg&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;400&quot; height=&quot;339&quot; data-filename=&quot;KakaoTalk_20250225_235947949_09.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;2561&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 data-ke-size=&quot;size16&quot;&gt;이날은 다들 ideation을 마쳐야해서 일찍 돌아갔다. 나도 일찍 돌아가서 다시 한번 아이디어 고민을 했다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매력적인 핵심 키워드 하나 생각해 보기&amp;nbsp;&lt;/li&gt;
&lt;li&gt;우승작 쭉 살펴보니 낭만 한 스푼 들어가 있기&amp;nbsp;&lt;/li&gt;
&lt;li&gt;오픈 소스 적절히 활용하기&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 모앙버스라는 주제로 최종 ideation을 2일 차 오전 8시에 제출했다ㅎㅎ 중간에 잠이 쏟아져서 아침 6시에 일어나서 다시 만들었다.&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;1449&quot; data-origin-height=&quot;809&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t7vVw/btsMxlXlW00/mZKHmk8ydcjUejm7ChwWRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t7vVw/btsMxlXlW00/mZKHmk8ydcjUejm7ChwWRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t7vVw/btsMxlXlW00/mZKHmk8ydcjUejm7ChwWRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft7vVw%2FbtsMxlXlW00%2FmZKHmk8ydcjUejm7ChwWRK%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;800&quot; height=&quot;447&quot; data-origin-width=&quot;1449&quot; data-origin-height=&quot;809&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2️⃣2일차 - 2월 19일 : 대망의 팀빌딩과 비어파티&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2-1. 아이데이션 발표 및 팀 빌딩&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;85&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이때 다른 분들의 아이디어를 들으면서 원하는 주제를 선택해야 하고, 동시에 내 발표 준비까지 해야 해서 내적으로 혼돈의 카오스였다.  &amp;zwj; &lt;/p&gt;
&lt;p data-end=&quot;146&quot; data-start=&quot;87&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오전 9시까지 제출해야 했는데, 나중에 제출한 사람들부터 발표가 시작됐다. 곧 내 차례라는 뜻...ㅎㅎ&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;내 아이디어를 좀 더 구체적으로 정리해 볼 걸 하는 아쉬움이 남았다. 아직 머릿속에서 정리가 덜 된 상태라 발표 때 살짝 버벅거렸던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;후기 보면 팀 빌딩 끝나고 도떼기 시장터가 된다고 했지만 뭔가 그 정도는 아니었다. 그리고 어떻게 하지 고민하고 있을 때 &lt;b&gt;백엔드 개발자 한분이 나에게 먼저 와서 유심히 보고 있었다고 말해줬다~!! 정말 한줄기의 빛과 같았다.✨&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;나도 용기를 얻고 내가 같이 하고 싶었던 프론트엔드 개발자분에게 가서 혹시 아이디어 본인이 구현한 걸로 기획하고 싶으시냐고 여쭤봤고 그리고 어찌저찌 다른 분들과도 이야기하면서 팀이 완성됐다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팀이 마지막쯤에 다 정해졌는데 오히려 좋았다,,,  내 경험상, 각자 아이디어와 방향을 조금 더 고민한 후에 팀을 꾸리는 것이 더 좋은 결과를 낳을 때가 많았던 것 같다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;그리고 팀원들끼리 같이 도시락 먹고 성산플레이스캠프로 이동했다. 가는 길이 내가 진짜 제주도 왔구나 싶었던,,&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;228&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;또 우리는 팀명을 가는 길에 정했다 ㅎㅎ 다들 뭔가 제대로 된 걸 하고 싶어서 오래 고민한 것 같다. 결국 우리는 그렇게 &lt;b&gt;클라우디&lt;/b&gt;&amp;nbsp;팀이 되었다.&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/o1ovK/btsMw0eU2K1/fNUhaCPsG37wjtssVKFktk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1ovK/btsMw0eU2K1/fNUhaCPsG37wjtssVKFktk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;2017&quot; data-filename=&quot;edited_KakaoTalk_20250226_121810747.jpg&quot; width=&quot;400&quot; height=&quot;461&quot; data-widthpercent=&quot;34.52&quot; style=&quot;width: 33.7173%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1ovK/btsMw0eU2K1/fNUhaCPsG37wjtssVKFktk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1ovK%2FbtsMw0eU2K1%2FfNUhaCPsG37wjtssVKFktk%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;2268&quot; height=&quot;2017&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv5mEx/btsMu9KNpZg/YBcZYhq4bkZJD0KcoW47zk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv5mEx/btsMu9KNpZg/YBcZYhq4bkZJD0KcoW47zk/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2537&quot; data-origin-height=&quot;2268&quot; data-filename=&quot;KakaoTalk_20250225_235947949_10.jpg&quot; style=&quot;width: 33.5423%; margin-right: 10px;&quot; data-widthpercent=&quot;34.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv5mEx/btsMu9KNpZg/YBcZYhq4bkZJD0KcoW47zk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv5mEx%2FbtsMu9KNpZg%2FYBcZYhq4bkZJD0KcoW47zk%2Fimg.jpg&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;2537&quot; height=&quot;2268&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE0BHV/btsMv7eAtDv/pZZEh50U6TuCx1yVcOMh91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE0BHV/btsMv7eAtDv/pZZEh50U6TuCx1yVcOMh91/img.png&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;2236&quot; data-is-animation=&quot;false&quot; data-filename=&quot;edited_KakaoTalk_20250226_130245024.jpg&quot; data-widthpercent=&quot;31.14&quot; style=&quot;width: 30.4149%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE0BHV/btsMv7eAtDv/pZZEh50U6TuCx1yVcOMh91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE0BHV%2FbtsMv7eAtDv%2FpZZEh50U6TuCx1yVcOMh91%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;2268&quot; height=&quot;2236&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;저 멀리 숙소에서 바다가 보이지만 결국 제주도에서 올라올 때까지 바다한 번 못보고 올라왔다는 슬픈 이야기,,&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;2-2. 카카오 현직자 특강&amp;nbsp;&lt;/b&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_20250226_122452015_02.jpg&quot; data-origin-width=&quot;1993&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMrtp1/btsMvwy8K3d/MOyn22sjTaUKS3q0C8JM9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMrtp1/btsMvwy8K3d/MOyn22sjTaUKS3q0C8JM9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMrtp1/btsMvwy8K3d/MOyn22sjTaUKS3q0C8JM9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMrtp1%2FbtsMvwy8K3d%2FMOyn22sjTaUKS3q0C8JM9K%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;700&quot; height=&quot;451&quot; data-filename=&quot;edited_KakaoTalk_20250226_122452015_02.jpg&quot; data-origin-width=&quot;1993&quot; data-origin-height=&quot;1283&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기획서 작성&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;어려운 이야기는 개념정리부터 하고 시작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제점인지 후 해결방안으로 바로 연결하기 - 시작 후 중요한 것 부터 말하세요 &lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰도를 향상시킬 수 있는 말을 인용하기 - 전문가 말 인용하기&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;발표하기 Tip&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발표시작 때 관심을 끌 수 있는 어그로로 시작하기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;발표에 의미를 부여하기&lt;/li&gt;
&lt;li&gt;과거의 성과나 숫자를 언급&lt;/li&gt;
&lt;li&gt;유머러스한 이야기&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표를 하게 되는 순간은 인생에서 또 언제 어떤 순간에 있을지 모르니 여러모로 유익한 강의였다!!&amp;nbsp;&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일 차까지 확정된 기획이 없어서 빨리 기획 나오고 개발 초기세팅이라도 시작하고 내일을 맞이하고 싶었다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;2-3. 비어파티&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h3&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/979r1/btsMyEIJ05r/kmfB7KiDiABi8kXLthZFSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/979r1/btsMyEIJ05r/kmfB7KiDiABi8kXLthZFSk/img.png&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;2424&quot; data-is-animation=&quot;false&quot; data-filename=&quot;edited_KakaoTalk_20250225_235947949_12.jpg&quot; data-widthpercent=&quot;52.61&quot; style=&quot;width: 52.0003%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/979r1/btsMyEIJ05r/kmfB7KiDiABi8kXLthZFSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F979r1%2FbtsMyEIJ05r%2FkmfB7KiDiABi8kXLthZFSk%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;2268&quot; height=&quot;2424&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb99sU/btsMwZAC0Tm/Yf3UWa12Uq4UIaGywkZldk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb99sU/btsMwZAC0Tm/Yf3UWa12Uq4UIaGywkZldk/img.png&quot; data-origin-width=&quot;2031&quot; data-origin-height=&quot;2410&quot; data-is-animation=&quot;false&quot; data-filename=&quot;edited_KakaoTalk_20250225_235947949_13.jpg&quot; data-widthpercent=&quot;47.39&quot; style=&quot;width: 46.8369%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb99sU/btsMwZAC0Tm/Yf3UWa12Uq4UIaGywkZldk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb99sU%2FbtsMwZAC0Tm%2FYf3UWa12Uq4UIaGywkZldk%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;2031&quot; height=&quot;2410&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;유명한 구름톤의 하이라이트 &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;처음에는 랜덤으로 앉아서 먹었고&amp;nbsp;&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;&amp;nbsp;&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;그리고 확실히 현직자 분들이랑 이야기 나누면서 얻어가는 것도 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발을 진행하다 보면 어느 시점에 여쭤봐야 하지? 얼마큼 나 혼자 할 수 있을 때까지 해야 하지? 라는 고민이 참 많이 드는데 결론은 빨리 질문하는 게 좋은 것 같다!&lt;/b&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;b&gt;비어파티는 본격적인 해커톤을 알리는 시작&lt;/b&gt;인 것 같다 ㅎㅎ&amp;nbsp;&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;다들 맥주 조금만 마시고 들어가서 코딩 시작 ㅎㅎ&amp;nbsp;&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;그리고 우리 팀의 막내 디자이너는 이때부터 밤새기를 시작했다. 젊어서 그런가 열정에 박수를 보낸다!!!!!!!!!!&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;3️⃣3일차 - 2월 20일 : 39시간 무수면 해커톤 시작 &lt;/b&gt;&lt;/b&gt;&lt;/h2&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/buztCX/btsMvv1ju4N/eSqxwsUA0vbHFyWzlz1BZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buztCX/btsMvv1ju4N/eSqxwsUA0vbHFyWzlz1BZ1/img.png&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;447&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.7972%; margin-right: 10px;&quot; data-widthpercent=&quot;49.37&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buztCX/btsMvv1ju4N/eSqxwsUA0vbHFyWzlz1BZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuztCX%2FbtsMvv1ju4N%2FeSqxwsUA0vbHFyWzlz1BZ1%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;791&quot; height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2PAsd/btsMvMWevOt/y2sCaHcDKvUMZecRUzBV70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2PAsd/btsMvMWevOt/y2sCaHcDKvUMZecRUzBV70/img.png&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;437&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.04%;&quot; data-widthpercent=&quot;50.63&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2PAsd/btsMvMWevOt/y2sCaHcDKvUMZecRUzBV70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2PAsd%2FbtsMvMWevOt%2Fy2sCaHcDKvUMZecRUzBV70%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;793&quot; height=&quot;437&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;3일 차는 본격적인 해커톤이 시작되었다! 우리 팀이 최종적으로 결정한 기획은 &lt;b&gt;혼잡도를 기반으로 숨겨진 장소를 추천해 주는 서비스&lt;/b&gt;였다. 사용자가 인기 관광지의 혼잡도를 실시간으로 확인하고, 만약 혼잡하면 현지인들이 추천하는 한적한 장소로 다른 관광지를 안내해주는 시스템이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;우리 팀의 브레인, 백엔드 개발자님이 제주 관광 빅데이터 서비스 플랫폼에서 가져온 데이터를 활용해, 좌표(위도, 경도)를 기반으로 반경 150미터 이내에 있는 관광객과 도민의 숫자를 실시간으로 확인할 수 있게 해주셨다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 이 정보를 화면단에 잘 띄우면 됐다. 아무래도 첫 화면이 할 일이 많을 것 같아 메인 화면/나머지 화면으로 프론트 파트를 나누고 각자 맡은 일을 시작했다. 몇 시까지 마치겠다고 시간을 정해두고 일정을 공유했지만, 생각보다 쉽지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 쉬지 않고 화면을 완성해 나갔다. 없는 시간에 공통으로 사용할 수 있는 컴포넌트들을 계속해서 추출하려고 노력했고, 디자인도 더 나은 방법이 있으면 함께 고민하려 했다. GDS는 적용해보고 싶었지만, 시간이 많이 소요될 것 같아서 결국 포기했다.&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;중간중간에 디자인과 기획은 계속 바뀌어서 회의를 꾸준히 해줘야 했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250226_142300058_02.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfo4pt/btsMys2HGHX/TSPk3qElCLXuO9U8qihCkk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfo4pt/btsMys2HGHX/TSPk3qElCLXuO9U8qihCkk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfo4pt/btsMys2HGHX/TSPk3qElCLXuO9U8qihCkk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfo4pt%2FbtsMys2HGHX%2FTSPk3qElCLXuO9U8qihCkk%2Fimg.jpg&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;500&quot; height=&quot;667&quot; data-filename=&quot;KakaoTalk_20250226_142300058_02.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;전날 밤샌 막내 디자이너 깨웠더니 &quot;&lt;/span&gt;&lt;b&gt;여기가 어디야&quot;&lt;/b&gt; 시전,,, 귀여운 모숩 동영상으로 남기지 못한 게 한이다 ㅠㅠ&amp;nbsp;&lt;/span&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;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밤새고 오전 8시30분에 마주한 에러 ㅎㅎ&amp;nbsp; 깔깔&amp;nbsp;&lt;b&gt;&lt;b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;4️⃣4일차 - 2월 21일 : 해커톤 결과 발표 및 시상&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;시연 영상을 찍기 10분 전까지 코드를 수정했다. 마지막 제출은 조금 늦어져서 감점을 받았지만, 그래도 넣고 싶은 자료들은 다 반영할 수 있었다. 발표가 시작되고 다른 팀들 발표를 쭉 들으면서, 상 받을 것 같은 팀들은 눈에 띄었다. 정말 짧은 시간 안에 탄탄한 스토리로 PPT를 완성하셨다. 물론, 우리 팀 발표가 내 맘속에선 일등~~~&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;발표 끝나고 받은 심사평들, 다른 팀들 할 때도 느낀 거지만 마냥 칭찬을 해주시기 보다 날카로운 피드백을 해주셨다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;여행을 갈 때 핫플을 가려고 하는 이유가 있을 텐데 어떻게 생각하는지?&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여행이라는 키워드는 F스러운 서비스지만 T스럽게 풀어낸 것 같다. F의 요소가 조금 담겨있었으면 좋았을 것&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI/UX 완성도가 좋아서 URL나중에 받아보고 싶다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI(o3모델)를 사용한 이유&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 사람들이 핫플을 가려는 이유가 SNS 발달로 포토존의 쏠림 현상을 가장 큰 원인으로 집어냈다. 이런 원인이라면&amp;nbsp; 서비스에서 유사하지만 한적한 장소를 추천해줄 때, 굳이 1,2시간 웨이팅을 하면서 인기 있는 장소에 갈 필요는 없지 않을까?라고 생각했다!&amp;nbsp;&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;나머지 질문들도 어려웠을 수 있는 답변을 기획자랑 백엔드 개발자분이 잘 받아쳐줬다!!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 발표가 끝나고 대망의 시상식이 시작됐다. 우리팀이 가산점 없이 감점만 있어서 3등 아니면 가망이 없다고 생각했는데 처음으로 이름이 불렸다!!! 우와아~~~~~~ 우수상!!!!&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250226_151727993.jpg&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;2803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xaxMu/btsMvLQF38q/RMlsNl39mr9SIjqtJizwik/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xaxMu/btsMvLQF38q/RMlsNl39mr9SIjqtJizwik/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xaxMu/btsMvLQF38q/RMlsNl39mr9SIjqtJizwik/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxaxMu%2FbtsMvLQF38q%2FRMlsNl39mr9SIjqtJizwik%2Fimg.jpg&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;500&quot; height=&quot;618&quot; data-filename=&quot;KakaoTalk_20250226_151727993.jpg&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;2803&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;구름톤을 통해 느낀 점&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;짧은 시간 안에 속도전을 해야 하다 보니 기획과 디자인이 정말 중요하다는 걸 실감했다. 그러기 위해서 서로를 납득시킬 수 있는 이야기를 계속 나누려 노력했다. 팀원들 모두 피드백을 잘 받아들이고 더 나은 방향으로 고민하는 게 느껴져서 좋았다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해커톤 시작하기 전에는 부담 왕창 뿐이었는데 왜 재밌다고 하는지 알겠다. 항상 열정적인 사람들은 나에게 큰 영감을 준다. 나도 더 열심히 살아야지 ㅎㅎ 이렇게 또 다르게 살아가고 있는 사람들한테 에너지 왕창 받고 왔다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 시간이 된다면 챗봇서비스에 AI이 제대로 적용하고 디자인 나온 대로 프론트단에서 조금 더 수정하고 싶다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;수고했다 짝짝짝  &amp;nbsp;&lt;/span&gt; &lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250226_142300058_01.jpg&quot; data-origin-width=&quot;3464&quot; data-origin-height=&quot;3464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpDA7m/btsMyDbWVPA/leRbENG2NnA5cJh7cyAxDK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpDA7m/btsMyDbWVPA/leRbENG2NnA5cJh7cyAxDK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpDA7m/btsMyDbWVPA/leRbENG2NnA5cJh7cyAxDK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpDA7m%2FbtsMyDbWVPA%2FleRbENG2NnA5cJh7cyAxDK%2Fimg.jpg&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;600&quot; height=&quot;600&quot; data-filename=&quot;KakaoTalk_20250226_142300058_01.jpg&quot; data-origin-width=&quot;3464&quot; data-origin-height=&quot;3464&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;&amp;nbsp;&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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동</category>
      <category>goormthon</category>
      <category>구름</category>
      <category>구름톤</category>
      <category>구름톤인제주</category>
      <category>제주</category>
      <category>해커톤</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/191</guid>
      <comments>https://2-doooo-2.tistory.com/191#entry191comment</comments>
      <pubDate>Wed, 26 Feb 2025 15:32:48 +0900</pubDate>
    </item>
    <item>
      <title>Next.js(14버전)- SSE 실시간 알림 구현</title>
      <link>https://2-doooo-2.tistory.com/190</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v5wzv/btsL2zJLd74/6df2CGnXgpV9HrkZcmLRy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v5wzv/btsL2zJLd74/6df2CGnXgpV9HrkZcmLRy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v5wzv/btsL2zJLd74/6df2CGnXgpV9HrkZcmLRy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv5wzv%2FbtsL2zJLd74%2F6df2CGnXgpV9HrkZcmLRy1%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;681&quot; height=&quot;492&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; ️들어가며&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;해당 글은 &quot;next&quot;: &quot;14.2.6&quot; 버전을 바탕으로 한 프로젝트입니다.&amp;nbsp;&lt;br /&gt;SSE는 HTTP 표준 기술 중 하나이므로 특정 버전이 존재하지 않습니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;❓SSE를 선택한 이유는?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리FIS아카데미에서 진행한 최총 프로젝트에서 우리의 핵심 기능은 '정산'이었다. 이때 &lt;span style=&quot;color: #000000;&quot;&gt;신속한 정산을 위해 &amp;lsquo;정산 요청&amp;rsquo;, &amp;lsquo;정산 시작&amp;rsquo;, &amp;lsquo;정산 완료&amp;rsquo; 등의 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정보를 여행원 간 실시간으로 공유할 필요성&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 느꼈고 우리는 실시간으로 서비스를 구현할 기술을 찾고 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; SSE(Server-Sent Events) &lt;/b&gt;VS &lt;b&gt;Polling &lt;/b&gt;VS &lt;b&gt;WebSocket&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 197px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 연결 &amp;nbsp;방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 데이터 흐름 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 실시간성 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 서버 부하 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 브라우저 지원 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 사용 사례 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;&lt;b&gt;SSE&lt;/b&gt; (Server-Sent Events)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;단방향 (서버 &amp;rarr; 클라이언트)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;서버에서 클라이언트로만 데이터 전송&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;중간 (연결 유지)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;대부분의 최신 브라우저 지원 (IE 미지원)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;주가 정보, 알림 시스템, 대시보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;&lt;b&gt;Polling&lt;/b&gt; (Short/Long)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;클라이언트가 주기적으로 요청&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;클라이언트 &amp;rarr; 서버 요청, 서버 응답&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;낮음 (지연 발생)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;높음 (반복적인 요청)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;모든 브라우저에서 가능&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;채팅, 데이터 새로고침&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;양방향 (서버 ⇄ 클라이언트)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;서버와 클라이언트가 자유롭게 데이터 교환&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;높음 (실시간)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;낮음 (연결 유지, 효율적)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;대부분의 최신 브라우저 지원&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot;&gt;실시간 게임, 채팅, 주식 거래&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;color: #000000;&quot;&gt;Polling은 주기적으로 서버에 요청을 보내는 방식, 리소스 낭비가 심하다고 판단, 또한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;웹소켓역시 양방향 통신으로 우리가 구현할 실시간 알림에 쓰기에는 오버 스펙이라고 생각했다. 그래서 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버에서 클라이언트로 단방향 통신인 SSE를 선택해서 구현하게 되었다.&amp;nbsp;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;++ SSE와 WebSocket의 차이&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 웹 기술이기 때문에 HTTP 프로토콜 위에서 동작한다.또한, HTTP 연결을 유지한 상태에서 재연결이나 추가 설정 없이 서버로부터 지속적인 데이터 스트림을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, WebSocket은 독립적인 프로토콜을 사용하고, HTTP와는 별도의 연결을 만들어 데이터를 주고받는다.&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;SSE는 CORS(Cross-Origin Resource Sharing)을 통해 다른 도메인에서도 데이터를 수신할 수 있다. WebSocket도 동일한 도메인 간 통신을 제공하지만, 보안상의 이유로 추가 구성이 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;   Next.js에 SSE 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서는 서버 사이드 렌더링 지원해서 SSE API Route도 구현할 수 있다. 하지만 우리 프로젝트에서느 Spring으로 서버를 구현했기 때문에 나는 데이터를 받아오는 로직을 구현하면 됐다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 서비스에서는 실시간 알림 서비스를 로그인된 회원만 이용할 수 있도록 설계했다. 그래서 서버랑 SSE 연결 시 Authorization 헤더에 토큰값을 포함하여 인증된 요청만 처리되도록 구현해야했고 EventSource API는 요청을 보낼 때 커스텀 헤더를 추가할 수 없어서 event-source-polyfill&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;라이브러리를 사용하였다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;문제 코드&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738325007472&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { EventSourcePolyfill } from 'event-source-polyfill';
import { auth } from '@withbee/auth-config';

export const connectSSE = async (url: string, onMessage: (data: any) =&amp;gt; void, onConnect?: () =&amp;gt; void, onError?: (error: any) =&amp;gt; void, token?: string) =&amp;gt; {
  // 인증 정보 얻기
  const session = await auth();
  const accessToken = session?.user!.accessToken;
  if (!accessToken) throw new Error('Authentication required');

  // SSE 연결 설정
  const eventSource = new EventSourcePolyfill(url, {
    headers: { Authorization: `Bearer ${accessToken}` },
    heartbeatTimeout: 30000,  // 30초마다 heartbeat 체크
  });

  // 연결 성공 이벤트
  eventSource.addEventListener('connect', (event: any) =&amp;gt; {
    if (event.data === 'SSE 연결이 완료되었습니다.') {
      onConnect?.(); // 연결 완료 콜백 호출
    }
  });

  // 메시지 수신 이벤트
  eventSource.addEventListener('message', (event: MessageEvent) =&amp;gt; {
    onMessage(event.data); // 메시지 처리
  });

  // 에러 처리
  eventSource.onerror = (error) =&amp;gt; {
    console.error('SSE Error:', error);
    onError?.(error); // 에러 콜백 호출
    eventSource.close(); // 연결 종료
  };

  return eventSource; // 이벤트 소스 반환
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&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;image.png&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qb27E/btsL4HzC3Ks/KJp6HJ1ntNATJJqlpdIM6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qb27E/btsL4HzC3Ks/KJp6HJ1ntNATJJqlpdIM6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qb27E/btsL4HzC3Ks/KJp6HJ1ntNATJJqlpdIM6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQb27E%2FbtsL4HzC3Ks%2FKJp6HJ1ntNATJJqlpdIM6K%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;622&quot; height=&quot;23&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;23&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 data-ke-size=&quot;size16&quot;&gt;headers 객체가 서버 요청 스코프 밖에서 호출됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 무슨 소리인가 했는데&amp;nbsp; &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;서버 컴포넌트에서는 브라우저 전용 API인 EventSource 를 사용할 수 없어서 난 에러였다!.&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;b&gt;그렇다면 요청 스코프 밖에서 호출됐다는 게 무슨 말일까? &lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;connectSSE 함수가 'use client' 지시문 없이 작성됨&lt;/li&gt;
&lt;li&gt;Next.js는 이 코드를 서버 컴포넌트로 인식&lt;/li&gt;
&lt;li&gt;빌드/서버 시작 시점에 이 코드가 평가됨&lt;/li&gt;
&lt;li&gt;이 시점에는 실제 HTTP 요청이 없음 (= 요청 스코프 밖)&lt;/li&gt;
&lt;li&gt;headers 객체를 생성하려고 시도하지만 실제 요청 컨텍스트가 없어서 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3d3929; text-align: start;&quot;&gt;그러니깐 대강 요약해보면 브라우저 전용 api안에 header가 있는데 서버컴포넌트에서 이를 부를려고 하니깐 요청 스코프 밖에서 header객체가 호출됐다는 것이다.&amp;nbsp;&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;b&gt;&lt;span style=&quot;color: #3d3929; text-align: start;&quot;&gt;꼬꼬질(꼬리의 꼬리를 무는 질문 ㅎㅎ 내가 만든 용어)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #3d3929; text-align: start;&quot;&gt;그렇다면 &lt;b&gt;요청 스코프&lt;/b&gt;는 정확히 언제 표현되는 단어일까?&amp;nbsp;&lt;/span&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;요청 스코프는 하나의 HTTP 요청이 시작되어 응답이 완료될 때까지의 생명주기를 가지는 실행 컨텍스트를 의미한다.&amp;nbsp; Next.js에서는 크게 세 가지 컨텍스트가 있다.&amp;nbsp;&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;서버 사이드 렌더링(SSR) 컨텍스트&lt;/p&gt;
&lt;pre id=&quot;code_1738327461313&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버에서 실행되는 코드
// headers(), cookies() 등의 API 사용 가능
export default function Page() {
  // 여기는 요청 스코프 내부
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 사이드 컨텍스트&lt;/p&gt;
&lt;pre id=&quot;code_1738327550317&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'
// 브라우저에서 실행되는 코드
// window, document 등 브라우저 API 사용 가능&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;/p&gt;
&lt;pre id=&quot;code_1738327565016&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 빌드할 때 실행되는 코드
// 요청 스코프 밖
const config = {
  // 여기서 headers 접근 불가
}&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;해결책은 간단했다. EventSourcePolyfill이 필요로 하는 브라우저 컨텍스트가 없었고 headers 객체를 생성하려고 하지만 실제 HTTP 요청 컨텍스트가 없어 에러가 난 거기 때문에 저 컴포넌트를 클라이언트 컴포넌트로 바꿔주면 된다. 그러니깐 'use client'만 붙여주면 된다.&amp;nbsp;&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;문제 해결 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1738327869947&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
// import 생략

export default function RealTimeMsg() {
  const { data: session, status } = useSession();
  const [notifications, setNotifications] = useState&amp;lt;Notification[]&amp;gt;([]);
  const [accessToken, setAccessToken] = useState&amp;lt;string | null&amp;gt;(null);
  const [isVisible, setIsVisible] = useState(false);
  const [isAnimatingOut, setIsAnimatingOut] = useState(false);

  useEffect(() =&amp;gt; {
    if (
      status === 'authenticated' &amp;amp;&amp;amp;
      session?.user?.accessToken &amp;amp;&amp;amp;
      !accessToken
    ) {
      setAccessToken(session.user.accessToken);
    }
  }, [session, status, accessToken]);

 useEffect(() =&amp;gt; {
  let eventSource: EventSourcePolyfill;
  if (accessToken) {
    (async () =&amp;gt; {
      eventSource = await connectSSE(
        `${BASE_URL}/api/notifications/stream`,
        (rawData) =&amp;gt; {
          const parsedNotification: Notification = JSON.parse(rawData);
          setNotifications((prev) =&amp;gt; [...prev, parsedNotification]);
          // 알림 표시 및 자동 제거 타이머 설정
        }
      );
    })();
  }
  // 클린업
  return () =&amp;gt; {
    eventSource?.close();
  };
}, [accessToken]);

  const handleClose = () =&amp;gt; {
    setIsAnimatingOut(true);
    setTimeout(() =&amp;gt; {
      setIsVisible(false);
      setNotifications((prev) =&amp;gt; prev.slice(1));
    }, 1000); // 애니메이션 시간과 맞춤
  };

  if (!isVisible || notifications.length === 0) {
    return null;
  }

  const notification = notifications[0];
  if (!notification) {
    return null;
  }

  return (
    &amp;lt;div className={styles.background}&amp;gt;
      // 생략
    &amp;lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #3d3929; text-align: start;&quot;&gt;최종 화면&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #3d3929; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;알림배지.gif&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tpQLc/btsL4oUDZX3/nSgKddpbkyKpmNUzerOKtk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tpQLc/btsL4oUDZX3/nSgKddpbkyKpmNUzerOKtk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tpQLc/btsL4oUDZX3/nSgKddpbkyKpmNUzerOKtk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/tpQLc/btsL4oUDZX3/nSgKddpbkyKpmNUzerOKtk/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;360&quot; height=&quot;720&quot; data-filename=&quot;알림배지.gif&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;720&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 마치며&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 서버 컴포넌트를 클라이언트 컴포넌트로 바꾸면 해결되는 문제였지만 에러 로그에서 Next.js의 SSR과 CSR의 충분한 이해가 없으면 헷갈리는 문제였기에 충분한 시간을 들여서 고민해보았다,, 그리고 그 뒤로 배포하는 과정에서 로컬에서 개발할 때는 SSE 연결이 에러 없이 잘 작동했는데 배포 후 SSE연결하는 페이지에서 502 Bad Gateway에러가 발생했었다. SSE통신을 지속적으로 연결하려면 nginx쪽에서도 따로 또 설정을 해줘야 했다. 다행히 팀원들의 도움으로 마무리를 깔끔하게 할 수 있었던 것 같다. 새로운 기술 학습해서 짜릿하다~~ &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;&amp;nbsp;&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;&amp;nbsp;&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;b&gt;참고자료&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&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://iii.ad/7a8a43&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://iii.ad/7a8a43&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/Next.js</category>
      <category>Next.js</category>
      <category>SSE</category>
      <category>실시간 알림</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/190</guid>
      <comments>https://2-doooo-2.tistory.com/190#entry190comment</comments>
      <pubDate>Fri, 31 Jan 2025 22:04:30 +0900</pubDate>
    </item>
    <item>
      <title>Next.js + Storybook이용해서 컴포넌트 개발하기 (pnpm 사용)</title>
      <link>https://2-doooo-2.tistory.com/189</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm7uqe/btsKGOfIKyG/rqY8YR0biMaaqb5UGYHUAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm7uqe/btsKGOfIKyG/rqY8YR0biMaaqb5UGYHUAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm7uqe/btsKGOfIKyG/rqY8YR0biMaaqb5UGYHUAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm7uqe%2FbtsKGOfIKyG%2FrqY8YR0biMaaqb5UGYHUAK%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;700&quot; height=&quot;294&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; ️들어가며&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해당 글은 &quot;next&quot;: &quot;14.2.6&quot;, &quot;@storybook/nextjs&quot;: &quot;8.4.1&quot; 버전을 바탕으로 한 프로젝트입니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Storybook 이란?&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 찾아보면 &quot;&lt;b&gt;스토리북은 UI 구성 요소와 페이지를 독립적으로 제작하는 프론트엔드 작업 환경입니다. 수천 개의 팀이 UI 개발, 테스트, 문서화를 위해 Storybook을 사용하고 있으며, 오픈 소스로 무료로 제공됩니다&lt;/b&gt; &quot; 라고 나와있다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;❓Storybook 언제,왜 사용하는가?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그렇다! 스토리북은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;분리 환경에서 UI 컴포넌트를 개발할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;UI 컴포넌트 라이브러리를 문서화 할 수도 있고, 디자인 시스템을 개발하기 위해 사용할 수도 있다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 Storybook은 UI 컴포넌트를 빠르고 독립적으로 개발할 때 매우 유용하게 사용된다. 또한 테스트 자동화 및 문서화에도 적합하여, 특히 팀 단위 협업 시 개발 효율성과 유지보수성을 크게 향상시킬 수 있다.&amp;nbsp;&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;그래서 이번 파이널 프로젝트에서 공통으로 들어갈 모달이나 버튼이 많아 스토리북을 사용하여 프로젝트를 진행하기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅Next.js에 Storybook 사용하기&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;storybook 셋팅하기&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트에서는 pnpm사용해서 명령어가 다르면 해당 사이트를 들어가서 자신의 프로젝트에 맞는 패키지 명령어를 사용하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://storybook.js.org/docs/get-started/frameworks/nextjs?renderer=react&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://storybook.js.org/docs/get-started/frameworks/nextjs?renderer=react&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731419613188&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm dlx storybook@latest init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어는 run까지 한꺼번에 실행되고, 설치하고 나서는 실행 명령어만 사용하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731419582793&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm storybook&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;1887&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZyErH/btsKFD0PLUh/bJ5kosKnHPtlJYcXWWB4s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZyErH/btsKFD0PLUh/bJ5kosKnHPtlJYcXWWB4s0/img.png&quot; data-alt=&quot;위의 이미지는 스토리북의 기본 셋팅이 아닌 현재 진행중인 프로젝트에서 활용한 스토리북이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZyErH/btsKFD0PLUh/bJ5kosKnHPtlJYcXWWB4s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZyErH%2FbtsKFD0PLUh%2FbJ5kosKnHPtlJYcXWWB4s0%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;700&quot; height=&quot;320&quot; data-origin-width=&quot;1887&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위의 이미지는 스토리북의 기본 셋팅이 아닌 현재 진행중인 프로젝트에서 활용한 스토리북이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;storybook UI구조&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjcHXZ/btsKGOGNSP3/SW5STcMLa7F45qdIwhgAl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjcHXZ/btsKGOGNSP3/SW5STcMLa7F45qdIwhgAl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjcHXZ/btsKGOGNSP3/SW5STcMLa7F45qdIwhgAl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjcHXZ%2FbtsKGOGNSP3%2FSW5STcMLa7F45qdIwhgAl0%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;232&quot; height=&quot;143&quot; data-origin-width=&quot;232&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6AWqW/btsKFovPBTO/iIopmuQrGXDdpekVE6m8dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6AWqW/btsKFovPBTO/iIopmuQrGXDdpekVE6m8dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6AWqW/btsKFovPBTO/iIopmuQrGXDdpekVE6m8dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6AWqW%2FbtsKFovPBTO%2FiIopmuQrGXDdpekVE6m8dK%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;232&quot; height=&quot;375&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 명령어 후에는 .storybook폴더와 stories폴더가 생긴 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;main.ts &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731415315768&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import type { StorybookConfig } from &quot;@storybook/nextjs&quot;;

import { join, dirname } from &quot;path&quot;;

/**
 * This function is used to resolve the absolute path of a package.
 * It is needed in projects that use Yarn PnP or are set up within a monorepo.
 */
function getAbsolutePath(value: string): any {
  return dirname(require.resolve(join(value, &quot;package.json&quot;)));
}
const config: StorybookConfig = {
  stories: [
    &quot;../stories/**/*.mdx&quot;,
    &quot;../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)&quot;,
  ],
  addons: [
    getAbsolutePath(&quot;@storybook/addon-onboarding&quot;),
    getAbsolutePath(&quot;@storybook/addon-essentials&quot;),
    getAbsolutePath(&quot;@chromatic-com/storybook&quot;),
    getAbsolutePath(&quot;@storybook/addon-interactions&quot;),
  ],
  framework: {
    name: getAbsolutePath(&quot;@storybook/nextjs&quot;),
    options: {},
  },
  staticDirs: [&quot;../public&quot;],
};
export default config;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.ts &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;에는 storybook을 위한 config 설정들이 담겨있다.&lt;span&gt; pnpm dlx storybook@latest init&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;을 통해서 기본으로 설정되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;stories&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;addons&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세팅이 되어있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #212529;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;주요 함수, 속성들&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;getAbsolutePath 함수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키지의 package.json 파일을 기준으로 절대 경로를 반환하는 역할&lt;/li&gt;
&lt;li&gt;모노레포로 구성되어 있는 프로젝트의 패키지 위치를 명확하게 찾기 위해 절대 경로가 필요&lt;/li&gt;
&lt;li&gt;require.resolve로 패키지를 찾고, dirname으로 해당 패키지의 상위 디렉터리 경로를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;stories 속성&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Storybook에서 사용할 스토리 파일의 경로를 지정&lt;/li&gt;
&lt;li&gt;../stories 폴더 아래의 모든 mdx 파일과 다양한 확장자(js, jsx, mjs, ts, tsx)를 가진 스토리 파일을 포함하도록 설정&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;addons &lt;b&gt;속성&amp;nbsp;&lt;/b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storybook 기능을 확장하는 애드온들이 포함되어 있다.&amp;nbsp;&lt;br /&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;b&gt;@storybook/addon-onboarding&lt;/b&gt;: Storybook 시작 가이드 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@storybook/addon-essentials&lt;/b&gt;: 기본적인 스토리북 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@storybook/addon-interactions&lt;/b&gt;: 상호작용 테스트 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;framework &lt;b&gt;&lt;b&gt;속성&amp;nbsp;&lt;/b&gt;&lt;/b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Storybook이 사용해야 하는 프레임워크를 지정&lt;/li&gt;
&lt;li&gt;Next.js용 Storybook을 설정하기 위해 @storybook/nextjs 프레임워크를 절대 경로로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;preview.ts&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731415380256&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import type { Preview } from &quot;@storybook/react&quot;;

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;preview.ts는 Storybook에서 미리보기 환경을 설정하는 데 사용되며, 각 스토리의 기본 매개변수와 속성을 정의한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;controls &lt;/span&gt;&lt;b&gt;&lt;b&gt;속성&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/b&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;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt; 컴포넌트의 props를 Storybook에서 조작 &lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;color: 배경색이나 텍스트 색상처럼 컬러 관련 props를 자동 감지하여 색상 선택기를 제공합니다.&lt;/li&gt;
&lt;li&gt;date: Date로 끝나는 props를 감지해 날짜 선택기를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt; matchers &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;b&gt;속성&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/b&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;b&gt;color&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/(background|color)$/i 정규 표현식을 사용해 prop 이름이 background나 color로 끝나는 경우를 자동으로 감지&lt;/li&gt;
&lt;li&gt;해당 props는 &lt;b&gt;컬러 선택 컨트롤&lt;/b&gt;이 적용되어 Storybook에서 색상을 선택할 수 있음&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;date&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/Date$/i 정규 표현식을 통해 prop 이름이 Date로 끝나는 경우를 자동으로 감지&lt;/li&gt;
&lt;li&gt;감지된 props는 &lt;b&gt;날짜 선택 컨트롤&lt;/b&gt;이 적용되어 Storybook에서 날짜를 선택할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;b&gt;storybook 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;Button.stories.tsx&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731417482047&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Button } from './Button';

const meta: Meta&amp;lt;typeof Button&amp;gt; = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
  },
  args: { onClick: action('clicked') },
};

export default meta;

type Story = StoryObj&amp;lt;typeof Button&amp;gt;;

export const Primary: Story = {
  args: {
    primary: true,
    label: '선택 완료',
  },
};

export const LargePrimary: Story = {
  args: {
    primary: true,
    size: 'large',
    label: '그룹 선택 완료',
  },
};

export const SmallPrimary: Story = {
  args: {
    primary: true,
    size: 'small',
    label: '불러오기',
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;import type { Meta, StoryObj } from '@storybook/react'&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Meta: 스토리의 메타데이터를 설정. 주로 스토리의 제목, 컴포넌트, 기타 설정들이 포함&lt;/li&gt;
&lt;li&gt;StoryObj: 개별 스토리의 속성 타입을 정의. 각 스토리에서 컴포넌트를 어떻게 렌더링할지를 설정할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;import { action } from '@storybook/addon-actions';&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@storybook/addon-actions에서 action 함수를 가져옴. 이 함수는 사용자가 버튼을 클릭했을 때 로그를 기록하거나 이벤트를 추적하는 데 사용&lt;/li&gt;
&lt;li&gt;버튼 클릭과 같은 이벤트를 추적하는 데 유용. 여기서는 onClick 이벤트를 추적하여 Storybook의 &quot;Actions&quot; 패널에 출력&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;1542&quot; data-origin-height=&quot;861&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l4E5B/btsKHvNzjqP/vFlafZ4V9YS7fcPpybMSH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l4E5B/btsKHvNzjqP/vFlafZ4V9YS7fcPpybMSH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l4E5B/btsKHvNzjqP/vFlafZ4V9YS7fcPpybMSH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl4E5B%2FbtsKHvNzjqP%2FvFlafZ4V9YS7fcPpybMSH1%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;600&quot; height=&quot;335&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;861&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 style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;b&gt;const meta: Meta&amp;lt;typeof Button&amp;gt; = { ... };&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;meta 객체, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;스토리에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;컴포넌트의 메타데이터&lt;/b&gt;를 정의 - 주요 항목
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;parameters&lt;/b&gt;: Storybook에서 설정할 수 있는 다양한 추가 옵션. layout: 'centered'로 설정하면 버튼이 화면 중앙에 위치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tags&lt;/b&gt;: 자동 문서화 기능인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;autodocs&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;태그가 추가되어, Storybook의 자동 문서화 시스템에 의해 이 컴포넌트에 대한 설명이 자동으로 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;argTypes&lt;/b&gt;: 각 스토리에서 사용될&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;argType&lt;/b&gt;을 정의하는데 사용. argTypes는 스토리의 제어를 위한 타입을 정의하는 데 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;args&lt;/b&gt;: Button 컴포넌트의 기본 인자 값을 설정. 여기서는 onClick 이벤트가 발생했을 때 action('clicked')가 호출되도록 설정하여 버튼 클릭을 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;b&gt;여러 스토리들(Primary,Secondary, LargePrimary등);&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/span&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;span style=&quot;color: #333333; text-align: left;&quot;&gt;각 스토리는 Button 컴포넌트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;다양한 변형을 보여주는 예시&lt;/li&gt;
&lt;li&gt;args를 사용하여 버튼의 속성을 설정, 스타일과 텍스트 등을 바꿔서 시연&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;b&gt;Button.tsx 컴포넌트&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1731418438087&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 React from &quot;react&quot;;
import { motion } from &quot;framer-motion&quot;;
import styles from &quot;./button.module.css&quot;;

export interface ButtonProps {
  primary?: boolean;
  size?: &quot;small&quot; | &quot;medium&quot; | &quot;large&quot; | &quot;xlarge&quot;;
  label: string;
  onClick?: () =&amp;gt; void;
  className?: string;
}

export const Button = ({
  primary = false,
  size = &quot;medium&quot;,
  label,
  ...props
}: ButtonProps) =&amp;gt; {
  const mode = primary ? styles.primary : styles.secondary;

  return (
    &amp;lt;motion.button
      type=&quot;button&quot;
      className={[styles[size], mode, styles.button].join(&quot; &quot;)}
      transition={{
        duration: 1,
        ease: &quot;easeInOut&quot;,
        repeat: Infinity,
        repeatType: &quot;mirror&quot;,
      }}
      {...props}
    &amp;gt;
      {label}
    &amp;lt;/motion.button&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;b&gt;export interface ButtonProps { ... }&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/span&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;b&gt;ButtonProps&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;primary: 버튼 스타일 (기본값은 false)&lt;/li&gt;
&lt;li&gt;size: 버튼 크기 (small, medium, large, xlarge 중 선택 가능, 기본값은 medium)&lt;/li&gt;
&lt;li&gt;label: 버튼에 표시할 텍스트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;b&gt;export const Button = ({ ... }: ButtonProps) =&amp;gt; { ... }&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 컴포넌트는 ButtonProps 타입을 받으며, 기본값을 설정하여 props를 구조 분해 할당&lt;/li&gt;
&lt;li&gt;primary, size, label을 기본값을 지정하고, props를 나머지로 받아서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; return ( ... )&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;className: 버튼에 적용할 CSS 클래스를 설정. styles[size]로 버튼 크기를, mode로 버튼 스타일을 설정하고, styles.button을 항상 추가&lt;/li&gt;
&lt;li&gt;...props: 나머지 props를 버튼 엘리먼트에 전달. 이로 인해 onClick과 같은 다른 props가 전달될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;label: 버튼의 텍스트를 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; 마치며&lt;/h2&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;b&gt;참고자료&amp;nbsp;&lt;/b&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://velog.io/@juno7803/Storybook-Storybook-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@juno7803/Storybook-Storybook-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/&quot;&gt;https://storybook.js.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로젝트/WITHBEE_TREVEL</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/189</guid>
      <comments>https://2-doooo-2.tistory.com/189#entry189comment</comments>
      <pubDate>Tue, 12 Nov 2024 22:47:35 +0900</pubDate>
    </item>
    <item>
      <title>Next.js - Infinite Scroll활용해서 데이터 처리하기</title>
      <link>https://2-doooo-2.tistory.com/188</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vJ0ln/btsKvAO9qzf/BkIJsxTGYywKbk05wE4edk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vJ0ln/btsKvAO9qzf/BkIJsxTGYywKbk05wE4edk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vJ0ln/btsKvAO9qzf/BkIJsxTGYywKbk05wE4edk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvJ0ln%2FbtsKvAO9qzf%2FBkIJsxTGYywKbk05wE4edk%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;700&quot; height=&quot;367&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리fis아카데미에서 사이드 프로젝트를 진행하면서 랭킹기능을 구현하게 되었다. 페이지네이션이랑 무한스크롤 중에 고민하다가 일일이 클릭하며 페이지를 이동하는게 번거롭다고 느꼈고 InfiniteScroll을 구현해보고 싶었다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 InfiniteScroll을 사용했는가?&lt;/b&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;b&gt;사용자 경험 향상&lt;/b&gt;: 사용자가 스크롤을 내릴 때 자동으로 추가 데이터를 불러옴으로써, 페이지를 수동으로 전환할 필요 없이 매끄럽고 연속적인 데이터 로딩 경험을 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt;: 초기 데이터 로딩 시 모든 데이터를 한 번에 가져오는 것이 아니라, 필요한 만큼의 데이터만 순차적으로 불러와서 성능을 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;편리한 데이터 처리&lt;/b&gt;: 다음 페이지 데이터를 가져오는 로직이 명확하게 분리되어 있어, 유지보수와 확장성이 용이&lt;/li&gt;
&lt;/ul&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;b&gt;1. react-infinite-scroller 설치하기&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1730611358898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install react-infinite-scroller&lt;/code&gt;&lt;/pre&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;b&gt;2.import하기&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1730611877095&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import InfiniteScroll from 'react-infinite-scroller';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.적용하기&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-1. 백엔드에서 반환되는 데이터 구조 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1730614940744&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;member&quot;: {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;홍길동&quot;,
    &quot;course&quot;: &quot;클라우드 서비스&quot;,
    &quot;rank&quot;: 2,
    &quot;image&quot;: &quot;이미지 url 주소&quot;,
    &quot;studyTime&quot;: 1000,
    &quot;totalTime&quot;: 6000
  },
  &quot;ranking&quot;: {
    &quot;hasNext&quot;: true,
    &quot;ranks&quot;: [
      {
        &quot;id&quot;: 1,
        &quot;name&quot;: &quot;홍길동&quot;,
        &quot;course&quot;: &quot;클라우드 서비스&quot;,
        &quot;rank&quot;: 2,
        &quot;image&quot;: &quot;이미지 url 주소&quot;,
        &quot;studyTime&quot;: 1000,
        &quot;totalTime&quot;: 6000
      }
      // 추가 학생 데이터...
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hasNext는 값을 확인하여 더 이상 가져올 데이터가 있는지 여부를 확인하는 변수다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-2 구조 분해 할당 구현 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출을 통해 데이터를 받아오고, 필요한 정보를 구조 분해 할당을 통해 상태 변수에 저장. &amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1730615068524&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { member, ranking: { ranks: newRankings, hasNext } }: ApiResponse = await getMemberRanking({
  tab: activeTab,
  pageNumber,
  size,
});&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-3. 상태관리&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 useState 훅을 사용하여 상태 변수 설정:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;activeTab: 현재 활성화된 탭 (일간, 주간, 월간)&lt;/li&gt;
&lt;li&gt;rankings: 학생들의 랭킹 리스트&lt;/li&gt;
&lt;li&gt;hasMore: 추가 데이터 유무&lt;/li&gt;
&lt;li&gt;currentUser: 현재 사용자의 정보&lt;/li&gt;
&lt;li&gt;loading: 데이터 로딩 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730615245825&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [activeTab, setActiveTab] = useState&amp;lt;'DAILY' | 'WEEKLY' | 'MONTHLY'&amp;gt;('DAILY');
  const [rankings, setRankings] = useState&amp;lt;Student[]&amp;gt;([]);
  const [hasMore, setHasMore] = useState(true);
  const [page, setPage] = useState(0);
  const [currentUser, setCurrentUser] = useState&amp;lt;Student | null&amp;gt;(null);
  const [loading, setLoading] = useState(false);
  const size = 10;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-4. 데이터 페칭 함수 작성&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출을 처리하는 fetchData 함수 구현&lt;/p&gt;
&lt;pre id=&quot;code_1730616489736&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fetchData = async (pageNumber: number) =&amp;gt; {
  try {
    setLoading(true);
    const response = await getMemberRanking({ tab: activeTab, pageNumber, size });
    // 데이터 처리 로직...
  } catch (error) {
    console.error('데이터 로드 에러:', error);
  } finally {
    setLoading(false);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetchData 함수는 페이지 번호를 인자로 받아 API를 통해 랭킹 데이터를 요청, 요청은 getMemberRanking 함수를 통해 이루어지며, 현재 활성화된 탭(activeTab)과 페이지 번호(pageNumber), 가져올 데이터의 크기(size)를 매개변수로 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 전환이나 데이터 갱신 시 사용자에게 최신 정보를 제공하는 역할 수행&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-5. 무한 스크롤 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 스크롤을 내리면 추가 데이터를 불러오는 loadMore 함수 작성:&lt;/p&gt;
&lt;pre id=&quot;code_1730616598173&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const loadMore = () =&amp;gt; {
  if (hasMore &amp;amp;&amp;amp; !loading) {
    fetchData(page + 1);
  }
};&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-6. 테이블 렌더링&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 스크롤할 때 loadMore 함수를 호출하고, hasMore 값에 따라 더 로드할 수 있는지 확인. 데이터가 로드되는 동안 로딩 메시지가 표시.&lt;/p&gt;
&lt;pre id=&quot;code_1730616780205&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;InfiniteScroll
  pageStart={0}
  loadMore={loadMore}
  hasMore={hasMore}
  loader={
    &amp;lt;div className={styles.loader} key={0}&amp;gt;
      Loading...
    &amp;lt;/div&amp;gt;
  }
  useWindow={false}
  getScrollParent={() =&amp;gt; containerRef.current}
&amp;gt;
  &amp;lt;table&amp;gt;
    ...
  &amp;lt;/table&amp;gt;
&amp;lt;/InfiniteScroll&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 흐름:&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;loadMore 함수&lt;/b&gt;: 스크롤이 끝에 도달할 때마다 호출되며, 다음 페이지의 데이터를 요청. 이 함수는 부모 컴포넌트에서 fetchData를 호출하여 새로운 데이터를 가져오는 역할을 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;hasMore prop&lt;/b&gt;: 현재 더 불러올 데이터가 있는지를 판단. fetchData에서 새로운 데이터를 성공적으로 불러온 후, hasNext의 값에 따라 업데이트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;loader&lt;/b&gt;: 데이터 로딩 중에 보여줄 로딩 UI를 정의. 새로운 데이터가 로드되는 동안 사용자에게 로딩 중임을 알려줌.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;getScrollParent&lt;/b&gt;: 스크롤 이벤트를 감지할 부모 요소를 지정. containerRef를 사용하여 InfiniteScroll 컴포넌트의 스크롤 부모를 지정.&lt;/li&gt;
&lt;/ol&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 data-token-index=&quot;0&quot;&gt;이렇게 Infinite Scroll&lt;/span&gt;을 활용해 서버에서 필요한 데이터만 요청하여 네트워크 트래픽을 줄이고 성능 최적화할 수 있게 되었다!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.에러 회고 및 리팩토링 진행&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음 랭킹 페이지에 진입했을 때는 정상적인 데이터 반환했으나 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;주, 월랭킹으로 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;페이지를 전환했을 때&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스크롤 통해 추가 데이터를 로드 했을 때&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 동일한 구간의 랭킹이 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중복으로 불러오는 문제&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가 발생&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;에러 수정 전 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1730617704090&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Ranking() {
// 상태 정의 생략 

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
        setRankings([]);
        setPage(0);
        setHasMore(true);

        const {
          member,
          ranking: { ranks: initialRankings, hasNext },
        }: ApiResponse = await getMemberRanking({
          tab: activeTab,
          pageNumber: 0,
          size,
        });

        const currentUserData: Student = {
          id: member.id,
          name: member.name,
          studyTime: member.studyTime,
          totalTime: member.totalTime,
          course: member.course,
          rank: member.rank,
          image: member.image || rankingImg,
        };

        setRankings(initialRankings);
        setHasMore(hasNext);
        setCurrentUser(currentUserData);
    };

    fetchData();
  }, [activeTab]);

  const loadMore = async () =&amp;gt; {
    if (hasMore) { // 로딩 상태 체크 없이 요청 발생
      const nextPage = page + 1;
      try {
        const response: ApiResponse = await getMemberRanking({
          tab: activeTab,
          pageNumber: nextPage,
          size,
        });

        const newRankings = response.ranking.ranks;
        // 기존 상태값을 초기화하지 않고 데이터를 추가
        setRankings((prevRankings) =&amp;gt; [...prevRankings, ...newRankings]);
        setHasMore(response.ranking.hasNext);
        setPage(nextPage);
      } catch (error) {
        console.error('데이터 로드 에러:', error);
      }
    }
  };

  return (
    &amp;lt;section className={styles.container}&amp;gt;
      // 생략
    &amp;lt;/section&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;에러 수정 및 리팩토링&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1730618041368&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Ranking() {
 // 기타 상태 정의 생략
  const [loading, setLoading] = useState(false);

  const fetchData = async (pageNumber: number) =&amp;gt; {
    try {
      setLoading(true);
      const {
        member,
        ranking: { ranks: newRankings, hasNext },
      }: ApiResponse = await getMemberRanking({
        tab: activeTab,
        pageNumber,
        size,
      });

      if (pageNumber === 0) {
       // 첫 로드 시 기존 데이터를 덮어씀 
        setRankings(newRankings);
        setCurrentUser({
          id: member.id,
          name: member.name,
          studyTime: member.studyTime,
          totalTime: member.totalTime,
          course: member.course,
          rank: member.rank,
          image: member.image || rankingImg,
        });
      } else {
        setRankings((prevRankings) =&amp;gt; [...prevRankings, ...newRankings]);
      }

      setHasMore(hasNext);
      setPage(pageNumber);
    } catch (error) {
      console.error('데이터 로드 에러:', error);
    } finally {
      setLoading(false); // 로딩 상태 초기화
    }
  };

  useEffect(() =&amp;gt; {
    fetchData(0); // 새 탭으로 변경 시 첫 페이지부터 데이터 로드 
  }, [activeTab]);

  const loadMore = () =&amp;gt; {
  // 로딩 중이 아닐 때만 데이터 요청
    if (hasMore &amp;amp;&amp;amp; !loading) {
      fetchData(page + 1);
    }
  };

  return (
    &amp;lt;section className={styles.container}&amp;gt;
      // 생략
    &amp;lt;/section&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 해결 핵심 이유&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. &lt;b&gt;로딩 상태 추가&lt;/b&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;수정 후 아래 코드를 넣음으로써 loading 상태를 추가하여 데이터 로딩 중에는 추가 요청이 발생하지 않도록 함. 이로 인해 여러 요청이 동시에 이루어지는 문제 방지&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [loading, setLoading] = useState(false); // 새로운 로딩 상태
&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;fetchData 함수 수정&lt;/b&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 코드&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const fetchData = async () =&amp;gt; {
  // API 호출 및 데이터 처리
}
&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;수정 후 코드&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const fetchData = async (pageNumber: number) =&amp;gt; {
  try {
    setLoading(true); // 로딩 시작
    const {
      member,
      ranking: { ranks: newRankings, hasNext },
    }: ApiResponse = await getMemberRanking({
      tab: activeTab,
      pageNumber,
      size,
    });

    if (pageNumber === 0) {
      setRankings(newRankings); // 초기 데이터 설정
      setCurrentUser({
        // 생략
      });
    } else {
      setRankings((prevRankings) =&amp;gt; [...prevRankings, ...newRankings]); // 추가 데이터 병합
    }

    setHasMore(hasNext);
    setPage(pageNumber);
  } catch (error) {
    console.error('Error fetching data:', error);
  } finally {
    setLoading(false); // 로딩 종료
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pageNumber가 0일 때 초기 데이터를 설정하고, 그 외의 경우에는 기존 데이터와 병합하는 방식으로 수정, setLoading(true)로 로딩 시작을 표시하고, 데이터 요청 후 setLoading(false)로 로딩 종료를 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. loadMore 함수 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심으로 문제가 됐던 지점, 로딩 중에도 여러번 호출될 수 있어서 중복 데이터 요청이 발생할 수 있음.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;React의 상태 업데이트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비동기적으로 처리됨. s&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;etLoading(true)가 실행되어도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉시 상태가 반영되지 않음. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상태 반영 이전에 사용자가 다시 스크롤하거나, 탭을 변경하면 fetchData 또는 loadMore가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가 호출될 가능성&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 존재, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 loading 상태가 아직 업데이트 되지 않아 중복 요청을 막지 못한 결과임.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 코드&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const loadMore = async () =&amp;gt; {
  if (hasMore) {
    const nextPage = page + 1;
    try {
      const response: ApiResponse = await getMemberRanking({
        tab: activeTab,
        pageNumber: nextPage,
        size,
      });
      const newRankings = response.ranking.ranks;
      setRankings((prevRankings) =&amp;gt; [...prevRankings, ...newRankings]);
      setHasMore(response.ranking.hasNext);
      setPage(nextPage);
    } catch (error) {
      console.error('데이터 로드 에러:', error);
    }
  }
}
&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;수정 후 코드&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const loadMore = () =&amp;gt; {
  if (hasMore &amp;amp;&amp;amp; !loading) { // 로딩 상태를 확인하여 중복 요청 방지
    fetchData(page + 1);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;react, next.js를 사용하다보면 관리해야할 상태가 많아지고 동작 순서가 헷갈리는 경우가 종종 생긴다. 어떤 코드가 다른 코드에 영향을 주고 또 그로 인해 사이드 이펙트가 생긴다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비동기 상태 업데이트가 이루어지는 React의 메커니즘은 그 자체로 주의가 필요가 한 것 같다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 이외의 무한스크롤 구현하는 방법&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 프로젝트를 진행하면서 똑같은 기능을 다른 방식으로 구현한 팀원이 있어서 이 방법도 살짝 정리하고 넘어가야겠다.&amp;nbsp;&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;b&gt; react-intersection-observer 라이브러리 활용한 무한스크롤&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737820316666&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from &quot;react&quot;;
import { useInView } from &quot;react-intersection-observer&quot;;

const InfiniteScrollWithLibrary = () =&amp;gt; {
  const [items, setItems] = useState&amp;lt;number[]&amp;gt;([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);

  const { ref, inView } = useInView({
    threshold: 1.0, // 100% 보일 때 트리거
    rootMargin: &quot;100px&quot;, // 트리거를 앞당김
  });

  // 데이터를 가져오는 함수
  const fetchData = async () =&amp;gt; {
    if (loading) return;
    setLoading(true);
    const newItems = Array.from({ length: 10 }, (_, i) =&amp;gt; i + 1 + (page - 1) * 10);
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 1000));
    setItems((prev) =&amp;gt; [...prev, ...newItems]);
    setPage((prev) =&amp;gt; prev + 1);
    setLoading(false);
  };

  // inView가 true일 때 데이터 로드
  React.useEffect(() =&amp;gt; {
    if (inView) fetchData();
  }, [inView]);

  return (
    &amp;lt;div&amp;gt;
      {items.map((item, index) =&amp;gt; (
        &amp;lt;div key={index} style={{ padding: &quot;20px&quot;, border: &quot;1px solid black&quot; }}&amp;gt;
          Item {item}
        &amp;lt;/div&amp;gt;
      ))}
      &amp;lt;div ref={ref} style={{ height: &quot;50px&quot;, backgroundColor: &quot;lightgray&quot; }}&amp;gt;
        {loading ? &quot;Loading...&quot; : &quot;Load More&quot;}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default InfiniteScrollWithLibrary;&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;react-intersection-observer vs react-infinite-scroller&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 500px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; 기능 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 20px;&quot;&gt;&lt;b&gt; react-intersection-observer &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 20px;&quot;&gt;&lt;b&gt; react-infinite-scroller &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;사용 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;useInView 훅 또는 InView 컴포넌트를 사용해 특정 DOM 요소가 &lt;b&gt;뷰포트에 진입&lt;/b&gt;했는지 감지&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;기존 스크롤 이벤트를 감지하여 데이터 로드 (&lt;b&gt;스크롤 위치&lt;/b&gt;에 의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;기반 기술&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;브라우저의 Intersection Observer API를 사용&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;스크롤 이벤트(onScroll)에 의존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;성능 최적화&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;요소가 화면에 진입했을 때만 트리거, 성능에 매우 효율적&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;스크롤 이벤트가 과도하게 발생하면 성능 저하 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;사용 용도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;특정 요소가 화면에 보일 때(진입/퇴장 감지)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;무한스크롤 구현에 초점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;데이터 로드 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;관찰 대상이 화면에 보이면 새로운 데이터를 로드&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;스크롤 위치가 하단에 가까워지면 새로운 데이터를 로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;의존성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;브라우저 기본 API 기반으로 가벼움&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;추가적인 로직 및 상태 관리를 포함, 상대적으로 무겁게 느껴질 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;구현 난이도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;단순하고 간결 (라이브러리와 훅으로 구현 가능)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;초기 설정이 간단하지만, 세부적인 커스터마이징이 필요한 경우 복잡할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;뷰포트 진입 감지 외에도 다양한 threshold, rootMargin 등을 설정 가능&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;데이터 로드 외에 커스터마이징된 스크롤 이벤트 처리 기능 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;최신 API 기반으로 성능 우수, 요소별로 세밀한 관찰 가능&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;사용이 간단하고, 기존 스크롤 이벤트와 호환성이 뛰어남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;최신 브라우저에서만 완벽 지원, 오래된 브라우저(IE 등)에서는 폴리필 필요&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;스크롤 이벤트 과다 호출로 성능 저하 가능, 직접적인 Intersection Observer 기능 지원 부족&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;설치 및 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;npm install react-intersection-observer (경량 라이브러리, 약 2KB)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;npm install react-infinite-scroller (약간 무거움, 약 8KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;유지보수 상태&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;활발하게 유지보수 중&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;유지보수는 되고 있지만 상대적으로 업데이트 빈도가 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;예제&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;요소가 화면에 보일 때 추가 데이터 로드 (e.g., 이미지 Lazy Load, 무한스크롤)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;페이지 하단 근처에서 데이터 로드 (무한스크롤에 초점)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;react-intersection-observer 를 알았다면&amp;nbsp; 이 라이브러리를 사용했을 것 같지만 직접 사용해봐야 좋은 라이브러리의 장점을 직접 체감할 수 있는 것이니,,  특히 내가 맡은 랭킹 페이지 ui에서는 탭간 전환이 많아서 뷰포트에 들어오거나 나가는 순간을 정확히 감지 할 수 있는 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;react-intersection-observer가 훨씬 적합했을 것 같다. 이외에도 더 다양한 동작을 처리할 수 있다고 하니,, 이미 프로젝트는 끝났지만 다른 팀원 덕분에 좋은 라이브러리를 알게된 게 럭키비키다~~~  &lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고자료&amp;nbsp;&lt;/b&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://www.npmjs.com/package/react-intersection-observer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/react-intersection-observer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-infinite-scroller&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/react-infinite-scroller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/Next.js</category>
      <category>infinite-scroller</category>
      <category>React</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/188</guid>
      <comments>https://2-doooo-2.tistory.com/188#entry188comment</comments>
      <pubDate>Sun, 3 Nov 2024 16:20:38 +0900</pubDate>
    </item>
    <item>
      <title>React - React Testing Library,Jest 이용해서 테스트하기</title>
      <link>https://2-doooo-2.tistory.com/187</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyBUM/btsJ4J6J1fC/IGV5E87Q0Yv0fRoUP9ceyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyBUM/btsJ4J6J1fC/IGV5E87Q0Yv0fRoUP9ceyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyBUM/btsJ4J6J1fC/IGV5E87Q0Yv0fRoUP9ceyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyBUM%2FbtsJ4J6J1fC%2FIGV5E87Q0Yv0fRoUP9ceyk%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;700&quot; height=&quot;329&quot; data-origin-width=&quot;936&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅React Testing Libraries란??&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;React Testing Library는 React 애플리케이션의 컴포넌트를 테스트하기 위한 라이브러리이다. 주로 사용자 관점에서의 컴포넌트 동작을 테스트하도록 설계되어 있으며, 컴포넌트가 실제로 어떻게 동작하는지를 검증하는 데 중점을 둔다.&amp;nbsp;&lt;/span&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;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;※ 프론트엔드 테스트 종류&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;table id=&quot;719f23b6-5ed7-4d85-a0c4-7668031e0637&quot; style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;41833d96-e9c4-4c83-bb75-41798d6838e4&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;&lt;b&gt;React Testing Library&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;&lt;b&gt;Vitest&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;&lt;b&gt;Cypress&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;8510c02d-b675-47cb-9f99-aa8c5e4ed764&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;단위 테스트, 통합 테스트&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;단위 테스트, 통합 테스트&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;엔드 투 엔드(E2E) 테스트, 통합 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;70024669-b9b8-4a43-b622-cc111d834cee&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;실행 환경&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;노드 환경에서 가상의 DOM 사용&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;노드 환경에서 Vite와 통합해 빠르게 실행&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;실제 브라우저 환경에서 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;34863c60-540b-4ba5-b64f-c89ecbed893b&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;사용 용도&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;컴포넌트의 단위 테스트&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;컴포넌트/함수의 단위 및 통합 테스트&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;전체 애플리케이션 흐름, UI 상호작용 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;e57b769d-281f-49be-ac37-71e7b3cbe047&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;비동기 작업&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;간단한 비동기 로직 테스트 가능&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;비동기 로직 테스트 가능&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;네트워크 요청, 브라우저 렌더링 테스트 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;07d26f61-09f8-4502-9f7c-3ee67b40d09f&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;속도&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;매우 빠름&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;매우 빠름 (특히 Vite와 통합 시)&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;상대적으로 느릴 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2df8a5c9-a21f-4335-b07d-edc779692684&quot;&gt;
&lt;td id=&quot;l{jy&quot;&gt;&lt;b&gt;시각적 피드백&lt;/b&gt;&lt;/td&gt;
&lt;td id=&quot;k=[s&quot;&gt;콘솔 출력&lt;/td&gt;
&lt;td id=&quot;jb{g&quot;&gt;콘솔 출력&lt;/td&gt;
&lt;td id=&quot;:Hnb&quot;&gt;스크린샷 및 비디오 캡처 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사용자 관점에서의 컴포넌트 동작을 테스트하도록 설계되어있다는게 무슨 말일까?? &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; Vitest 단위 테스트와 비교해서 알아보자!&amp;nbsp;&lt;/span&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;b&gt;React Testing Library&lt;/b&gt;를 사용할 때:&lt;/p&gt;
&lt;pre id=&quot;code_1728742068065&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test('버튼 클릭 시 텍스트 변경', () =&amp;gt; {
  render(&amp;lt;MyComponent /&amp;gt;);
  const button = screen.getByRole('button', { name: /클릭/i });
  fireEvent.click(button);
  expect(screen.getByText(/변경된 텍스트/i)).toBeInTheDocument();
});&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;b&gt;Vitest&lt;/b&gt;로 단위 테스트를 작성할 때:&lt;/p&gt;
&lt;pre id=&quot;code_1728742082524&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test('함수 호출 테스트', () =&amp;gt; {
  const mockFunction = vi.fn();
  myFunction(mockFunction);
  expect(mockFunction).toHaveBeenCalled();
});&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;b&gt;React Testing Library&lt;/b&gt;는 UI의 동작과 사용자 상호작용을 중심으로 작성된 반면, &lt;b&gt;Vitest&lt;/b&gt;의 예시는 함수 호출 여부를 검증하는 단위 테스트이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;React Testing Library: 사용자 중심의 UI 테스트로, 실제 사용자처럼 컴포넌트와 상호작용하며 결과를 검증한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Vitest: 테스트 러너로, 내부 구현 세부 사항을 검증하는 단위 테스트를 작성할 수 있지만, UI 상호작용 중심의 테스트를 원한다면 React Testing Library와 함께 사용하는 것이 좋다.&amp;nbsp;&lt;/blockquote&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 Testing Library로 돌아와서 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Jest 와 React Testing Library의 역할을 살펴보면서 이해해보자!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Jest란 무엇일까?&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Testing Library는 DOM 상호작용을 중심으로 컴포넌트를 테스트하는 도구이지만, 테스트 환경을 구성하고 실행하는 역할은 수행하지 않는다. &lt;b&gt;Jest&lt;/b&gt;는 React Testing Library와 함께 사용되는 &lt;b&gt;테스트 러너&lt;/b&gt;로, 테스트를 실행하고 결과를 평가하는 중요한 역할을 한다.&amp;nbsp;&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;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;※&lt;span&gt; 테스트 러너란?&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️테스트 코드를 실행하고 결과를 보고하는 역할&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️&lt;/b&gt;테스트 코드의 실행 환경을 설정하고, 각 테스트 케이스를 실행하여 그 결과를 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️&lt;/b&gt;테스트 러너는 테스트 스위트를 관리하고, 테스트 결과를 출력하며, 테스트 환경의 초기화와 정리를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️&lt;/b&gt;대표적인 테스트 러너로는 Jest, Mocha, Jasmine, Karma 등이 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Jest란?&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️&lt;/b&gt; 자바스크립트 테스트 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️&lt;/b&gt; 사용자는 Jest 를 이용하여 테스트 코드를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️ &lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;하지만 Node.js 환경에서 실행되기 때문에 DOM이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✔️ &lt;/b&gt;DOM없이는 React 컴포넌트 테스트가 불가능하다. 그래서 React Testing Library에서 제공하는 가상의 DOM을 이용한다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Jest에서 제공하는 함수&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;describe() : 테스트 그룹을 정의할 때 사용&lt;/li&gt;
&lt;li&gt;test() : 개별 테스트 케이스를 정의&lt;/li&gt;
&lt;li&gt;beforeEach() : 각 테스트가 실행되기 전에 실행&lt;/li&gt;
&lt;li&gt;afterEach() : 각 테스트가 실행된 후에 실행&lt;/li&gt;
&lt;li&gt;beforeAll() : 모든 테스트가 실행되기 전에 한 번 실행&lt;/li&gt;
&lt;li&gt;afterAll() : 모든 테스트가 실행된 후에 한 번 실행&lt;/li&gt;
&lt;li&gt;expect() : 테스트에서 값이나 상태를 검증할 때 사용&lt;/li&gt;
&lt;li&gt;주요 Matcher들
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;toBe() : 엄격한 일치 여부를 확인, ===와 유사한 비교&lt;/li&gt;
&lt;li&gt;&lt;span&gt;toBeInTheDocument&lt;/span&gt;() : 해당 요소가 문서에 존재하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mock관련 함수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jest.fn() : Jest에서 함수의 동작을 모킹하기 위해 사용되는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://jestjs.io/docs/expect&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jestjs.io/docs/expect&lt;/a&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅간단한 실습&amp;nbsp;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. testing-library와 jest 설치하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728742745829&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install @testing-library/react @testing-library/jest-dom&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;설치하고 나면 pacage.json에 관련 코드가 생성된 걸 볼 수있다.&lt;/p&gt;
&lt;pre id=&quot;code_1728743969548&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;scripts&quot;: { 
    &quot;test&quot;:&quot;jest&quot;
},
&quot;devDependencies&quot;: {
     &quot;jest&quot;: &quot;^29.7.0&quot;,
     &quot;jest-environment-jsdom&quot;: &quot;^29.7.0&quot;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. 필요한 모듈 .babelrc 파일에 설정하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728743915097&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;presets&quot;: [
    [
      &quot;@babel/preset-env&quot;, //최신 JavaScript 문법(ES6 이상)을 구버전 JavaScript로 변환
      {
        &quot;modules&quot;: false, // Babel이 ES6 모듈 문법(import/export)을 CommonJS로 변환하지 않게
        &quot;targets&quot;: {
          &quot;node&quot;: &quot;16&quot;
        }
      }
    ],
    &quot;@babel/preset-react&quot; // React JSX 문법을 JavaScript로 변환해 주는 preset
  ],
  &quot;env&quot;: {
    &quot;test&quot;: {
      &quot;plugins&quot;: [
        &quot;@babel/plugin-transform-modules-commonjs&quot;, // 테스트 환경에서는 CommonJS로 변환이 필요하기 때문에
        &quot;@babel/plugin-transform-runtime&quot; 
        // Babel이 코드 내에서 반복적으로 사용하는 헬퍼 함수(예: async/await, spread 문법 등)를 전역 공간에 중복 없이 공유
        // 비동기 코드나 async/await 문법이 포함된 코드를 테스트할 때 중요하며, 최신 문법을 지원하지 않는 환경에서 동작하도록 폴리필을 추가
      ]
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. &lt;/b&gt;&lt;b&gt;Jest가 브라우저 환경을 모방할 수 있도록 jest.config.cjs 파일 설정하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728745223107&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;module.exports = {
  testEnvironment: &quot;jsdom&quot;,
};&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;나와 같은 경우 그냥 .js파일로 돌리면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;41&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZoCoV/btsJ32lGmjL/tSQotvmHYZJ7tyiiZY5Wck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZoCoV/btsJ32lGmjL/tSQotvmHYZJ7tyiiZY5Wck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZoCoV/btsJ32lGmjL/tSQotvmHYZJ7tyiiZY5Wck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZoCoV%2FbtsJ32lGmjL%2FtSQotvmHYZJ7tyiiZY5Wck%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;567&quot; height=&quot;41&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;41&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 에러가 발생해서 cjs로 변경해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; cjs &lt;/b&gt;vs&lt;b&gt; js &lt;/b&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;b&gt;.cjs&lt;/b&gt;: &lt;b&gt;CommonJS&lt;/b&gt; 모듈 시스템을 사용하는 파일 확장자. CommonJS는 require()와 module.exports 문법을 사용하며, &lt;b&gt;Node.js&lt;/b&gt;에서 오랫동안 사용되어온 모듈 시스템.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.js&lt;/b&gt;: 기본적으로 &lt;b&gt;ES Modules (ESM)&lt;/b&gt; 형식을 사용하는 파일 확장자. ESM은 import/export 문법을 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; 왜 cjs 확장자를 사용해야 오류가 안 나는가? &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 ES Modules (ESM)과 CommonJS (CJS)를 모두 지원하지만, &lt;b&gt;Jest&lt;/b&gt;는 기본적으로 &lt;b&gt;CommonJS&lt;/b&gt;를 사용한다. jest.config.js 파일에서 module.exports를 사용하면 CommonJS 문법을 사용하고 있다는 의미인데, Node.js는 .js 파일을 &lt;b&gt;ESM&lt;/b&gt;으로 처리하려고 시도할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 테스트 적용시킬 CreateSchedule 컴포넌트 준비&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728744163849&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from &quot;react&quot;;

const CreateSchedule = ({ addResult }) =&amp;gt; {
  const [company_name, setCompanyName] = useState(&quot;&quot;);
  const [deadline, setDeadline] = useState(&quot;&quot;);
  const [result_date, setResultDate] = useState(&quot;&quot;);
  const [result, setResult] = useState(3);

  const handleSubmit = (e) =&amp;gt; {
    e.preventDefault();
    const newResult = {
      company_name,
      deadline,
      result_date,
      result,
    };
    addResult(newResult);
    clearForm();
  };

  const clearForm = () =&amp;gt; {
    setCompanyName(&quot;&quot;);
    setDeadline(&quot;&quot;);
    setResultDate(&quot;&quot;);
    setResult(3);
  };

  const handleResultChange = (e) =&amp;gt; {
    const value = e.target.value;
    if (value === &quot;합격&quot;) {
      setResult(1);
    } else if (value === &quot;불합격&quot;) {
      setResult(2);
    } else {
      setResult(3);
    }
  };

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;h2&amp;gt;스케줄 추가&amp;lt;/h2&amp;gt;
      &amp;lt;div className=&quot;form_wrap&quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label htmlFor=&quot;company_name&quot;&amp;gt;회사명&amp;lt;/label&amp;gt;
          &amp;lt;input
            id=&quot;company_name&quot;
            type=&quot;text&quot;
            placeholder=&quot;회사명&quot;
            value={company_name}
            onChange={(e) =&amp;gt; setCompanyName(e.target.value)}
            required
            data-testid=&quot;company-name-input&quot;
          /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label htmlFor=&quot;deadline&quot;&amp;gt;서류지원 마감&amp;lt;/label&amp;gt;
          &amp;lt;input
            id=&quot;deadline&quot;
            type=&quot;datetime-local&quot;
            value={deadline}
            onChange={(e) =&amp;gt; setDeadline(e.target.value)}
            required
            data-testid=&quot;deadline-input&quot; 
          /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label htmlFor=&quot;result_date&quot;&amp;gt;결과 발표&amp;lt;/label&amp;gt;
          &amp;lt;input
            id=&quot;result_date&quot;
            type=&quot;datetime-local&quot;
            value={result_date}
            onChange={(e) =&amp;gt; setResultDate(e.target.value)}
            required
            data-testid=&quot;result-date-input&quot; 
          /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;label htmlFor=&quot;result&quot;&amp;gt;결과&amp;lt;/label&amp;gt;
          &amp;lt;select
            id=&quot;result&quot;
            value={result === 1 ? &quot;합격&quot; : result === 2 ? &quot;불합격&quot; : &quot;진행 중&quot;}
            onChange={handleResultChange}
            data-testid=&quot;result-select&quot; 
          &amp;gt;
            &amp;lt;option value=&quot;진행 중&quot;&amp;gt;진행 중&amp;lt;/option&amp;gt;
            &amp;lt;option value=&quot;합격&quot;&amp;gt;합격&amp;lt;/option&amp;gt;
            &amp;lt;option value=&quot;불합격&quot;&amp;gt;불합격&amp;lt;/option&amp;gt;
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;button_wrap&quot;&amp;gt;
          &amp;lt;button type=&quot;submit&quot; data-testid=&quot;submit-button&quot;&amp;gt;
            추가
          &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};

export default CreateSchedule;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;data-testid&lt;/b&gt;: 테스트 중에 요소를 식별하기 위한 고유한 식별자로 사용. React Testing Library에서 screen.getByTestId()와 같은 함수를 통해 이 요소를 쉽게 찾아서 테스트할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. &lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;CreateSchedule.test.js작성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728744198056&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import &quot;@testing-library/jest-dom&quot;;
import { render, screen, fireEvent } from &quot;@testing-library/react&quot;;
import CreateSchedule from &quot;./CreateSchedule&quot;;

describe(&quot;CreateSchedule 컴포넌트 테스트&quot;, () =&amp;gt; {
  test(&quot;폼이 올바르게 렌더링된다&quot;, () =&amp;gt; {
    render(&amp;lt;CreateSchedule addResult={jest.fn()} /&amp;gt;);

    expect(screen.getByTestId(&quot;company-name-input&quot;)).toBeInTheDocument();
    expect(screen.getByTestId(&quot;deadline-input&quot;)).toBeInTheDocument();
    expect(screen.getByTestId(&quot;result-date-input&quot;)).toBeInTheDocument();
    expect(screen.getByTestId(&quot;result-select&quot;)).toBeInTheDocument();
    expect(screen.getByRole(&quot;button&quot;, { name: /추가/i })).toBeInTheDocument();
  });

  test(&quot;입력 필드에 값을 입력하고 제출하면 addResult가 호출된다&quot;, () =&amp;gt; {
    const addResultMock = jest.fn();
    render(&amp;lt;CreateSchedule addResult={addResultMock} /&amp;gt;);

    // 입력값 설정
    fireEvent.change(screen.getByTestId(&quot;company-name-input&quot;), {
      target: { value: &quot;회사 A&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;deadline-input&quot;), {
      target: { value: &quot;2024-10-12T12:00&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;result-date-input&quot;), {
      target: { value: &quot;2024-10-15T12:00&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;result-select&quot;), {
      target: { value: &quot;합격&quot; },
    });

    // 제출 버튼 클릭
    fireEvent.click(screen.getByRole(&quot;button&quot;, { name: /추가/i }));

    // addResult가 호출되었는지 확인
    expect(addResultMock).toHaveBeenCalledWith({
      company_name: &quot;회사 A&quot;,
      deadline: &quot;2024-10-12T12:00&quot;,
      result_date: &quot;2024-10-15T12:00&quot;,
      result: 1, // &quot;합격&quot; 선택 시 result는 1
    });
  });

  test(&quot;폼 제출 후 입력값이 초기화된다&quot;, () =&amp;gt; {
    const addResultMock = jest.fn();
    render(&amp;lt;CreateSchedule addResult={addResultMock} /&amp;gt;);

    // 입력값 설정
    fireEvent.change(screen.getByTestId(&quot;company-name-input&quot;), {
      target: { value: &quot;회사 A&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;deadline-input&quot;), {
      target: { value: &quot;2024-10-12T12:00&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;result-date-input&quot;), {
      target: { value: &quot;2024-10-15T12:00&quot; },
    });
    fireEvent.change(screen.getByTestId(&quot;result-select&quot;), {
      target: { value: &quot;합격&quot; },
    });

    // 제출 버튼 클릭
    fireEvent.click(screen.getByRole(&quot;button&quot;, { name: /추가/i }));

    // 입력 필드가 초기화되었는지 확인
    expect(screen.getByTestId(&quot;company-name-input&quot;).value).toBe(&quot;&quot;);
    expect(screen.getByTestId(&quot;deadline-input&quot;).value).toBe(&quot;&quot;);
    expect(screen.getByTestId(&quot;result-date-input&quot;).value).toBe(&quot;&quot;);
    expect(screen.getByTestId(&quot;result-select&quot;).value).toBe(&quot;진행 중&quot;);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;설정 및 준비&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@testing-library/jest-dom: 추가적인 Jest 매처를 사용하기 위해 임포트. 이를 통해 toBeInTheDocument()와 같은 메소드를 사용&lt;/li&gt;
&lt;li&gt;render, screen, fireEvent: React Testing Library의 유틸리티 함수들을 임포트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;render: 컴포넌트를 실제 DOM에 렌더링&lt;/li&gt;
&lt;li&gt;screen: 렌더링된 컴포넌트에서 요소를 찾기 위한 API를 제공&lt;/li&gt;
&lt;li&gt;fireEvent: 사용자 이벤트를 시뮬레이션하는 데 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;테스트 스위트 설명&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;describe(&quot;CreateSchedule 컴포넌트 테스트&quot;, () =&amp;gt; {...}): 이 블록 안에는 CreateSchedule 컴포넌트에 대한 여러 테스트가 포함되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;테스트 케이스 분석&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 1: &quot;폼이 올바르게 렌더링된다&quot;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: CreateSchedule 컴포넌트가 올바르게 렌더링되는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;render 함수를 사용하여 컴포넌트를 렌더링&lt;/li&gt;
&lt;li&gt;getByTestId 및 getByRole를 사용하여 입력 필드와 버튼이 DOM에 존재하는지 확인&lt;/li&gt;
&lt;li&gt;모든 요소가 존재해야 하므로 각각의 expect 문이 true이면 테스트가 통과&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 2: &quot;입력 필드에 값을 입력하고 제출하면 addResult가 호출된다&quot;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: 폼에 값을 입력하고 제출할 때 addResult가 올바른 인자로 호출되는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jest.fn()을 사용하여 addResult의 모킹 함수를 생성&lt;/li&gt;
&lt;li&gt;각 입력 필드에 fireEvent.change로 값을 설정&lt;/li&gt;
&lt;li&gt;제출 버튼을 클릭하여 폼을 제출&lt;/li&gt;
&lt;li&gt;expect 문으로 addResultMock이 호출되었는지 확인하고, 올바른 인자가 전달되었는지 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 3: &quot;폼 제출 후 입력값이 초기화된다&quot;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: 폼 제출 후 입력 필드가 초기화되는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름&lt;/b&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;입력 필드의 값을 검사하여 각 필드가 초기화되었는지 확인. 이때, company-name-input은 비어 있어야 하고, result-select는 기본값인 &quot;진행 중&quot;이 되어야 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 테스트 실행&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728745028483&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm test&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;실행하고 나서&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzhb7/btsJ3rsBG8X/dNhFP9gw2YnDwfRLv5AkC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzhb7/btsJ3rsBG8X/dNhFP9gw2YnDwfRLv5AkC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzhb7/btsJ3rsBG8X/dNhFP9gw2YnDwfRLv5AkC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flzhb7%2FbtsJ3rsBG8X%2FdNhFP9gw2YnDwfRLv5AkC1%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;423&quot; height=&quot;27&quot; data-origin-width=&quot;423&quot; data-origin-height=&quot;27&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러 타입에러를 마주했다. 그 이유는&amp;nbsp; Jest 매처가 확장시켜주는 라이브러리를 추가해줘야 한다. 즉, Jest에 기본적으로 포함되지 않고, 추가적인 라이브러리에서 제공되는 함수이다.&amp;nbsp;&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;그래서CreateSchedule.test.js에 해당 라이브러리 import 해주고&lt;/p&gt;
&lt;pre id=&quot;code_1728745749770&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import &quot;@testing-library/jest-dom&quot;;&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;jest.config.cjs파일에&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728745800790&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;setupFilesAfterEnv: [&quot;&amp;lt;rootDir&amp;gt;/jest.config.cjs&quot;]&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;이 코드를 추가해줬다. setupFilesAfterEnv속성이 Jest가 각 테스트 파일을 실행하기 전에 필요한 초기 설정을 수행할 수 있게 해준다고 한다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k76nm/btsJ4hiu2zu/jV3ZFMyZd4kmzyaa1bBByk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k76nm/btsJ4hiu2zu/jV3ZFMyZd4kmzyaa1bBByk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k76nm/btsJ4hiu2zu/jV3ZFMyZd4kmzyaa1bBByk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk76nm%2FbtsJ4hiu2zu%2FjV3ZFMyZd4kmzyaa1bBByk%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;361&quot; height=&quot;201&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;201&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;&amp;nbsp;&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;b&gt;참고자료&lt;/b&gt;&amp;nbsp;&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://www.npmjs.com/package/@testing-library/jest-dom&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/@testing-library/jest-dom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@wlwl99/Testing-Library-Jest&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@wlwl99/Testing-Library-Jest&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/React</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/187</guid>
      <comments>https://2-doooo-2.tistory.com/187#entry187comment</comments>
      <pubDate>Sun, 13 Oct 2024 00:17:44 +0900</pubDate>
    </item>
    <item>
      <title>Spring - LocalDateTime으로 원하는 데이터 주고 받기</title>
      <link>https://2-doooo-2.tistory.com/186</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y2WKB/btsJ2JVjpPn/auKwoGOT8KscJs8osdIaE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y2WKB/btsJ2JVjpPn/auKwoGOT8KscJs8osdIaE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y2WKB/btsJ2JVjpPn/auKwoGOT8KscJs8osdIaE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy2WKB%2FbtsJ2JVjpPn%2FauKwoGOT8KscJs8osdIaE0%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;619&quot; height=&quot;313&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅문제 상황&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 날짜와 시간 데이터를 받아와야하는데 타입이 LocalDateTime이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서는 input에 &amp;lt;input type=&quot;date&quot;&amp;gt; 으로 날짜만 보내고 있었던 상황&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다음과 같은 에러가 떴다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728704012552&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Resolved [org.springframework.http.converter.HttpMessageNotReadableException: 
JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime` 
from String &quot;2024-09-22 00:00&quot;: Failed to deserialize java.time.LocalDateTime: 
(java.time.format.DateTimeParseException) 
Text '2024-09-22 00:00' could not be parsed at index 10]&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;JSON 파싱 오류: 문자열 &quot;2024-09-22 00:00&quot;에서 `java.time.LocalDateTime` 유형의 값을 역직렬화할 수 없다고 한다!!&amp;nbsp;&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;※역직렬화(Deserialization)란? = &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;@RequestBody&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5962; text-align: start;&quot;&gt;직렬화된 데이터를 다시 객체의 형태로 만드는 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;span style=&quot;color: #5c5962;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;ex)&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1728704491536&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person{
	private String name;
    
    public Sample(String name) {
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #5c5962; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #5c5962; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 클래스가 있다고 할 때, Json 데이터 형식을 예로 들면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #5c5962; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Person person = new Person(&quot;김철수&quot;);&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;{ &quot;name&quot; : &quot;김철수&quot;}&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 같은 방식으로 변경하는 것을 직렬화,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #5c5962; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;{ &quot;name&quot; : &quot;김철수&quot;}&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터를 받아서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person이라는 객체의&lt;span&gt;&amp;nbsp;&lt;/span&gt;name&lt;span&gt;&amp;nbsp;&lt;/span&gt;필드에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;김철수&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 할당하고 객체를 생성하는 것을 역직렬화라고 할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅실패한 방법&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 프론트에서 시간까지 같이 보낼 수 있는&amp;nbsp; type으로 수정&lt;/p&gt;
&lt;pre id=&quot;code_1728704633941&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
   &amp;lt;label htmlFor=&quot;deadline&quot;&amp;gt;서류지원 마감&amp;lt;/label&amp;gt;
      &amp;lt;input
        id=&quot;deadline&quot;
        type=&quot;datetime-local&quot;
        value={deadline}
        onChange={(e) =&amp;gt; setDeadline(e.target.value)}
        required
       /&amp;gt;
  &amp;lt;/div&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;※날짜를 지정할 수 있는 HTML의 input 요소에서 사용 가능한 type 속성&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;date&lt;/b&gt;: 날짜를 선택할 수 있는 입력 필드 (년, 월, 일)&lt;/p&gt;
&lt;pre id=&quot;code_1728705579010&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;date&quot;&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;b&gt;datetime-local&lt;/b&gt;: 날짜와 시간을 선택할 수 있는 입력 필드 (년, 월, 일, 시간, 분)&lt;/p&gt;
&lt;pre id=&quot;code_1728705616124&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;datetime-local&quot;&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;b&gt;month&lt;/b&gt;: 연도와 월만 선택할 수 있는 입력 필드&lt;/p&gt;
&lt;pre id=&quot;code_1728705630597&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;month&quot;&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;b&gt;week&lt;/b&gt;: 연도와 주를 선택할 수 있는 입력 필드&lt;/p&gt;
&lt;pre id=&quot;code_1728705642785&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;week&quot;&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;b&gt;time&lt;/b&gt;: 시간만 선택할 수 있는 입력 필드 (시간, 분)&lt;/p&gt;
&lt;pre id=&quot;code_1728705655265&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;time&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 RequestDto에서는&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728704770776&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm&quot;)
LocalDateTime deadline,&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;@DateTimeFormat 어노테이션을 추가해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 클라이언트에서 &quot;yyyy-MM-dd HH:mm&quot; 형식의 문자열로 날짜와 시간을 입력받아 LocalDateTime 필드로 변환해 처리할 수 있다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅성공한 방법&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react에서 날짜 보내는 데이터 포맷팅해서 보내기&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728708321984&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   const formatDate = (date) =&amp;gt; {
      const d = new Date(date);
      const year = d.getFullYear();
      const month = String(d.getMonth() + 1).padStart(2, '0');
      const day = String(d.getDate()).padStart(2, '0');
      const hours = String(d.getHours()).padStart(2, '0');
      const minutes = String(d.getMinutes()).padStart(2, '0');
      return `${year}-${month}-${day} ${hours}:${minutes}`;
    };

    const formattedDeadline = formatDate(updatedResult.deadline);
    const formattedResultDate = formatDate(updatedResult.result_date);&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;이 값을 body에 담아서 보내주는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728708357187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;body: JSON.stringify({
   company_name: updatedResult.company_name,
   deadline: formattedDeadline,
   result_date: formattedResultDate,
   result: updatedResult.result, 
}),&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;이렇게 포맷팅을 해서 보내야하는 이유는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML5의 datetime-local 입력 형식과 서버의 LocalDateTime 차이 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 값은 &quot;yyyy-MM-ddTHH:mm&quot; 형식, 서버에서 LocalDateTime으로 데이터를 받기 위해서는 yyyy-MM-dd HH:mm으로 변환해주는 작업이 필요!!&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;스프링에서 @DateTimeFormat 대신 @JsonFormat 사용하기&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728705768508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm&quot;, timezone = &quot;Asia/Seoul&quot;)
LocalDateTime deadline,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@DateTimeFormat 대신 @JsonFormat을 사용했더니 에러가 해결됐다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@DateTimeFormat 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@JsonFormat 했더니 에러가 해결된걸까?&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅ &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@DateTimeFormat&lt;span&gt;&amp;nbsp; vs &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@JsonFormat&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@DateTimeFormat&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728706052370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.format.annotation;

...

public @interface DateTimeFormat {
     ...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;패키지에서 볼 수 있듯이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;@DateTimeFormat&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;은 Spring의 어노테이션이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@JsonFormat&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728706131522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.fasterxml.jackson.annotation;

...

public @interface JsonFormat {
	...
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;패키지를 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@JsonFormat&lt;/b&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@RequestBody, @ResponseBody를 사용한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;직렬화/역직렬화를 담당하는&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Jackson 라이브러리의 어노테이션이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Jackson 라이브러리에서는 내부적으로 pattern으로 지정한 형식을 LocalDateTime으로 변환해준다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서, Jackson 라이브러리의 @JsonFormat은 날짜 형식이라면 자유롭게 사용이 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@RequestBody, @ResponseBody를 사용한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;역직렬화/직렬화 시&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;@DateTimeFormat을 사용하려면 Jackson 라이브러리에 있는 어노테이션이 아니기 때문에&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;LocalDateTime의 기본 형식인 'yyyy-MM-dd'T'HH:mm:ss'으로 요청이 들어와야 바인딩된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(이때, @DateTimeFormat을 사용하지 않아도 바인딩이 된다)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로 @RequestBody, @ResponseBody를 사용한 역직렬화/직렬화 시&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;날짜 형식을 자유롭게 사용하기 위해서는&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@DateTimeFormat&lt;/b&gt;이 아닌,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;@JsonFormat&lt;/b&gt;을 사용해야한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(물론, Request Body 날짜 형식이 LocalDateTime의 기본 형식인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;'yyyy-MM-dd'T'HH:mm:ss'이라면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;@DateTimeFormat, @JsonFormat 둘다 사용하지 않아도 그대로 바인딩이 가능하다.)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅하지만!!! 진짜 성공한 방법&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;LocalDateTime의 기본 형식인 'yyyy-MM-dd'T'HH:mm:ss'이 아닐때를 전제&lt;br /&gt;&lt;br /&gt;1. @RequestBody, @ResponseBody : @JsonFormat 사용&lt;br /&gt;2. @RequestParam, @ModelAttribute : @DateTimeFormat 사용&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;블로그에서 이 글을 보고 오잉?? 싶었다. 내가 보내는 데이터는 &amp;lt;input type=&quot;datetime-local&quot;&amp;gt; 이 형식을 사용했다면 기본형식이 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;'yyyy-MM-dd'T'HH:mm:ss' 이게 맞을 텐데,,, 왠걸!! 포맷팅도 안해주고 어노테이션을 붙이지 않아도 제대로 보내진다.. 이게 무슨일이지??? 고치는 과정에서 어노테이션을 추가하고 테스트를 해봐서 그런가? 흑흑&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;꼭 저 전제를 잊으면 안될 것 같다!! &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&amp;lt;input type=&quot;datetime-local&quot;&amp;gt;&lt;span&gt;&amp;nbsp; 을 사용했다면 &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;@JsonFormat&lt;span&gt; , &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;@DateTimeFormat&lt;span&gt;&amp;nbsp; 이 어노테이션을 붙이지 않아도 LocalDateTime 타입에 맞춰서 보내진다!&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;최종 코드&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728710329656&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;input type=&quot;datetime-local&quot;/&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;/p&gt;
&lt;pre id=&quot;code_1728710403991&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LocalDateTime deadline&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;/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;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고자료&lt;/b&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://ksh-coding.tistory.com/107&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ksh-coding.tistory.com/107&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://inkyu-yoon.github.io/docs/Language/Java/Serialization&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://inkyu-yoon.github.io/docs/Language/Java/Serialization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Backend/Java</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/186</guid>
      <comments>https://2-doooo-2.tistory.com/186#entry186comment</comments>
      <pubDate>Sat, 12 Oct 2024 13:16:58 +0900</pubDate>
    </item>
    <item>
      <title>Next.js - Auth.js( NextAuth.js) 를 이용한 Keycloak과의 인증 처리 과정</title>
      <link>https://2-doooo-2.tistory.com/185</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DZOyI/btsJJK71MOc/KuxVHfjOJkH0zdb9KiAze0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DZOyI/btsJJK71MOc/KuxVHfjOJkH0zdb9KiAze0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DZOyI/btsJJK71MOc/KuxVHfjOJkH0zdb9KiAze0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDZOyI%2FbtsJJK71MOc%2FKuxVHfjOJkH0zdb9KiAze0%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;700&quot; height=&quot;230&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅next-auth 란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auth.js(NextAuth.js)는 Next.js 프로젝트의 사용자 인증 및 세션 관리를 위한 라이브러리이다. Google, GitHub 등의 다양한 인증 공급자를 지원하며, (그중에서도 내가 구현한거는 Keycloak과의 인증이다 )Next.js의 서버와 클라이언트 측 모두에서 인증 및 세션 관리를 손쉽게 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅NextAuth.js를&amp;nbsp;이용한&amp;nbsp;Keycloak과의&amp;nbsp;인증&amp;nbsp;처리&amp;nbsp;과정&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 274px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 19.4186%;&quot;&gt;단계&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 18.6047%;&quot;&gt;파일명&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 61.8605%;&quot;&gt;동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;로그인 버튼 렌더링&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;AuthStatus.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;사용자가 로그인 상태인지 확인하고, 로그인 버튼을 클릭하면 signIn 함수를 호출하여 Keycloak으로 로그인 요청 전송.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;Keycloak 로그인 처리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;auth.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;Keycloak 제공자와의 통신을 설정하고, JWT 토큰을 사용한 인증 및 세션 관리를 처리.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;세션 및 토큰 관리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;auth.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;JWT 콜백에서 액세스 토큰과 리프레시 토큰을 처리하고, 세션에 필요한 정보를 저장하여 클라이언트와 서버에서 사용.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;로그아웃 버튼 렌더링&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;AuthStatus.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;로그아웃 버튼 클릭 시 keycloakSessionLogout 함수 호출, api/auth/logout로 서버에서 로그아웃 처리 요청 전송.(클라이언트 컴포넌트 -&amp;gt; 서버 컴포넌트로 : id_token을&amp;nbsp;클라이언트에&amp;nbsp;노출시키지&amp;nbsp;않기&amp;nbsp;위해)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;Keycloak 로그아웃 처리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;api/auth/logout/route.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;Keycloak 로그아웃 URL로 요청을 보내고, 세션에 저장된 id_token을 사용하여 로그아웃을 완료.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 19.4186%;&quot;&gt;&lt;b&gt;스프링 서버로 API 요청&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 18.6047%;&quot;&gt;RootLayout.js&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 61.8605%;&quot;&gt;로그인 상태일 때 스프링 서버로 access_token을 포함한 요청을 전송하여 데이터를 가져옴.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅auth.js(루트) - NextAuth.js와 Keycloak Provider 설정&lt;/h2&gt;
&lt;pre id=&quot;code_1727167419851&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import NextAuth from &quot;next-auth&quot; 
import Keycloak from &quot;next-auth/providers/keycloak&quot;


// 액세스 토큰이 만료되었을 경우 새로운 액세스 토큰을 발급받기 위해 리프레시 토큰 활용
async function refreshAccessToken(token) {
    const response = 
        await fetch(`${process.env.REFRESH_TOKEN_URL}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                client_id: process.env.AUTH_KEYCLOAK_ID,
                client_secret: process.env.AUTH_KEYCLOAK_SECRET,
                grant_type: 'refresh_token',
                refresh_token: token.refresh_token,
            })
        });

    const data = await response.json();
    return {
        ...token,
        access_token: data.access_token,
        id_token: data.id_token,
        expires_at: Math.floor(Date.now() / 1000) + data.expires_in,
        refresh_token: data.refresh_token,
    }
}


const config = {
    providers: [
        Keycloak({
            clientId: `${process.env.AUTH_KEYCLOAK_ID}`,
            clientSecret: `${process.env.AUTH_KEYCLOAK_SECRET}`,
            issuer: `${process.env.AUTH_KEYCLOAK_ISSUER}`
        })
    ],
    // TODO: 인증과 관련된 다른 처리 로직
    callbacks: { // 인증 처리 과정에서 자동으로 호출되는 콜백들의 목록
        session: {
            strategy: 'jwt', // 세션 관리 방식을 jwt 전략으로 활용
            maxAge: 60 * 60 * 24 // 세션 만료시간(초)
        },

        async jwt({ token, account }) {                        
            const currentTime = Math.floor(Date.now() / 1000);

            if (account) {
                // token 객체에 개별 프로퍼티들로 할당
                token.access_token = account.access_token;
                token.id_token = account.id_token;
                token.expires_at = account.expires_at;
                token.refresh_token = account.refresh_token;
                return token;
            } else if (currentTime &amp;lt; token.expires_at) {
                console.log('토큰이 아직 만료되지 않음');
                return token; // 만료되지 않았기 때문에 기존 토큰 반환
            } else { // 토큰이 만료되었을 경우,
                // keycloak의 토큰 엔드포인트(REFRESH_TOKEN_URL)로 요청 전송
                console.log('액세스토큰이 만료됨');
                
                try {
                    const refreshedToken = await refreshAccessToken(token);
                    console.log('토큰이 갱신되었음');
                    return refreshedToken;
                } catch (error) {
                    console.error('토큰 리프레싱 에러', error);
                    return { ...token, error: 'RefreshToken Error' }
                }
            }

            return token;                
        },

        // jwt 콜백이 반환하는 token을 전달받고, 
        // session을 확인하는 코드(ex. auth())가 호출될 때마다 session 콜백이 호출됨
        async session({ session, token }) {
            // keycloak 로그아웃 엔드포인트에 활용할 id_token을 세션에 저장
            session.id_token = token.id_token;
            // in Java Servlet - session.setAttribute(&quot;id_token&quot;, id_token);

            // Spring 리소스 서버에 전송할 때 필요한 access_token을 세션에 저장
            session.access_token = token.access_token;

            return session;
        }
    }
}
 
export const { handlers, signIn, signOut, auth } = NextAuth(config)
/*
    next-auth에서 제공하는 내장 함수
        handlers - 인증과 관련된 API 라우트
        signIn: 사용자 로그인 함수
        signOut: 로그아웃 함수
        auth : Next.js 서버가 가진 세션 정보를 반환하는 함수
    
*/&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;auth.js에서 NextAuth의 Keycloak provider를 설정&lt;/li&gt;
&lt;li&gt;Keycloak() 함수에 clientId, clientSecret, issuer 등의 값을 지정하여 Keycloak과의 인증을 처리&lt;/li&gt;
&lt;li&gt;jwt() 콜백에서 토큰을 갱신하는 로직과 만료된 토큰을 새로 발급받는 과정이 포함되어 있다. 만료된 액세스 토큰은 refresh_token을 이용하여 갱신된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;호출옵션&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;providers: Credentials, Google, GitHub 등의 인증 공급자를 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;session: 세션 관리 방식을 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages: 사용자 정의 페이지 경로를 지정하며, 로그인 페이지의 기본값은 /auth/signin&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callbacks: 인증 및 세션 관리 중 호출되는 각 핸들러를 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callbacks.signIn: 사용자 로그인을 시도했을 때 호출되며, true를 반환하면 로그인 성공, false를 반환하면 로그인 실패로 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callbacks.redirect: 페이지 이동 시 호출되며, 반환하는 값은 리다이렉션될 URL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callbacks.jwt: JWT가 생성되거나 업데이트될 때 호출되며, 반환하는 값은 암호화되어 쿠키에 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;callbacks.session: jwt 콜백이 반환하는 token을 받아, 세션이 확인될 때마다 호출되며, 반환하는 값은 클라이언트에서 확인할 수 있다. (2번 이상 호출될 수 있음)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅components/AuthStatus.js - useSession() 훅을 사용하여 사용자의 세션 상태를 확인&lt;/h2&gt;
&lt;pre id=&quot;code_1727184200182&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client' 
import React from 'react'
import { signIn, signOut, useSession } from 'next-auth/react';

// keycloak의 세션을 날리는 요청
async function keycloakSessionLogout() {
    try {
        // api/auth/logout/route.js의 GET() 호출
        await fetch('/api/auth/logout', { method: 'GET' });
    } catch (error) {
        console.error(error);
    }
}

const AuthStatus = () =&amp;gt; {
    // useSession() - 클라이언트 컴포넌트(use client)에서 세션 값을 가져오기 위한 훅
    const { data: session, status } = useSession();

    if (status === 'loading' &amp;amp;&amp;amp; !session) {
        return &amp;lt;div&amp;gt;Loading..&amp;lt;/div&amp;gt;
    } else if (session) { // 로그인 되었을 경우
        return (
            &amp;lt;div&amp;gt;
                &amp;lt;div&amp;gt;
                    {`${session.user.email} 하이~`}
                &amp;lt;/div&amp;gt;
                &amp;lt;button className='className=&quot;mt-2 px-2 py-1 font-bold text-white border rounded border-gray-50&quot;'
                onClick={() =&amp;gt; keycloakSessionLogout().then(() =&amp;gt; signOut({ callbackUrl: '/' }))}&amp;gt;
                    로그아웃
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        )
    }
    
    return (
        &amp;lt;div&amp;gt;
      &amp;lt;button
        className=&quot;mt-2 px-2 py-1 font-bold text-white border rounded border-gray-50&quot;
        onClick={() =&amp;gt; signIn(&quot;keycloak&quot;)}&amp;gt;
        로그인
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
    )
}

export default AuthStatus&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 컴포넌트에서는 useSession훅을 사용해 세션값을 가져오고 서버컴포넌트에서는 auth()함수를 사용해 세션값을 가져온다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;signIn()과 signOut()을 사용하여 Keycloak에 대한 로그인 및 로그아웃을 처리하며, 로그아웃 시 Keycloak의 세션도 함께 제거하기 위한 keycloakSessionLogout()을 사용한다.&lt;/li&gt;
&lt;li&gt;로그인 처리&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;auth.js에서 NextAuth()를 통해 export해주는 signIn함수를 keycloak 프로바이더를 통해 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로그아웃 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;signOut() -&amp;gt; Next-auth에서 제공해주는 내장 함수, 클라이언트 애플리케이션용 로그아웃 처리&lt;/li&gt;
&lt;li&gt;Keycloak 서버 자체의 세션 정보도 지우는 처리 - 커스텀 함수, keycloakSessionLogout() - 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;next.js는 클라이어언트 애플리케이션이면서 서버 로직도 작성 가능하다. AuthStatus라는 브라우저에서 Next.js의 서버 로직인 GET:api/auth/logout로 요청을 전송할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그러면 GET:api/auth/logout는 요청을 받고, 응답 객체로 반환해주는 처리를 해야한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅app/api/auth/[...nextauth]/route.js&lt;/h2&gt;
&lt;pre id=&quot;code_1727186016877&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { handlers } from &quot;@/auth&quot; // 방금 생성한 auth.js에서 handlers를 불러옴
export const { GET, POST } = handlers

/*
    Auth.js는 api/auth에 해당하는 경로를 통해 인증 처리를 수행
*/&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅app/api/auth/logout/route.js&lt;/h2&gt;
&lt;pre id=&quot;code_1727186130266&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 라우트 핸들러, 서버 프로세스에서 동작

import { auth } from &quot;@/auth&quot;;


export async function GET() {
    // keycloak 엔드포인트로 로그아웃하기 위해서는 id_token이 필요

    // id_token을 세션에서 꺼내야 함
    const session = await auth();
    if (session) {
        const idToken = session.id_token;
        
        // keycloak의 엔드포인트로 호출
        const KEYCLOAK_URL = `${process.env.END_SESSION_URL}?id_token_hint=${idToken}&amp;amp;post_logout_redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`; // 로그아웃 처리 후 리다이렉트할 경로
        
        // 요청 전송
        try {
            const response = await fetch(KEYCLOAK_URL, { method: 'GET' });
            console.log('resp', response);
            
        } catch (error) {
            console.error(error);
            return new Response({ status: 500 });
        }
    }
    
    return new Response({ status: 200 });
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Keycloak의 로그아웃 엔드포인트로 요청을 전송하여 Keycloak 서버에 세션을 삭제&lt;/li&gt;
&lt;li&gt;id_token을 클라이언트에 노출시키지 않기 위해 클라이언트 컴포넌트에서 서버컴포넌트로 넘겨받는 과정이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;✅layout.js(루트) - 백엔드 API와의 연동&lt;/h2&gt;
&lt;pre id=&quot;code_1727166979553&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { auth } from &quot;@/auth&quot;;
import &quot;./globals.css&quot;;
import AuthStatus from &quot;@/components/AuthStatus&quot;;
import { SessionProvider } from &quot;next-auth/react&quot;;

export const metadata = {
  title: &quot;Create Next App&quot;,
  description: &quot;Generated by create next app&quot;,
};

export default async function RootLayout({ children }) {

  // 스프링 리소스 서버로 요청 전송
  const session = await auth();

  // 스프링 서버에게 요청을 전송할 때 필요한 값 - Access token
  // 세션에 저장된 액세스 토큰을 꺼내서 Authorizaiton 헤더에 담아서 요청을 전송해서 담아야함 
  const response = await fetch(`${process.env.BACKEND_URL}/api/data`, {
    method:&quot;GET&quot;,
    headers:{
      &quot;Authorization&quot;:'Bearer '+ session?.access_token,
      &quot;Content-type&quot;:&quot;application/json&quot;,
    }
  });

  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
          &amp;lt;div className=&quot;w-4/5 h-screen p-3 bg-white&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
            &amp;lt;h2 className=&quot;text-3xl&quot;&amp;gt;Next Client App&amp;lt;/h2&amp;gt;
              {/* 로그인 상태를 확인하는 용도의 컴포넌트 */}
              &amp;lt;SessionProvider&amp;gt;
             	 &amp;lt;AuthStatus/&amp;gt;
              &amp;lt;/SessionProvider&amp;gt;         
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;auth()를 통해 auth.js에서 설정된 callback 함수 안에 있는 session 정보 를 얻고, 토큰을 활용해 API 요청을 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고사이트&amp;nbsp;&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://www.keycloak.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.keycloak.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://authjs.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://authjs.dev/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://adoptium.net/temurin/releases/?arch=x64&amp;amp;version=17&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://adoptium.net/temurin/releases/?arch=x64&amp;amp;version=17&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※자바17로 되어 있어야 keycloak사용가능&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/Next.js</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/185</guid>
      <comments>https://2-doooo-2.tistory.com/185#entry185comment</comments>
      <pubDate>Tue, 24 Sep 2024 23:35:25 +0900</pubDate>
    </item>
    <item>
      <title>Next.js - next.js에서 session 활용하기</title>
      <link>https://2-doooo-2.tistory.com/184</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; session 이란 무엇인가?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;클라이언트와 서버 간의 연결 상태&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 의미, 즉 &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;클라이언트가 브라우저에 접속하여&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;서버와 접속이 종료하기 전의 상태&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;를 의미한다.&lt;/span&gt; &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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; session 동작 순서&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI93ys/btsJGA5t0Bq/hehqGHCIs3dLkaek1irOt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI93ys/btsJGA5t0Bq/hehqGHCIs3dLkaek1irOt1/img.png&quot; data-alt=&quot;출처: https://dongsik93.github.io/til/2020/01/08/til-authorization(1)/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI93ys/btsJGA5t0Bq/hehqGHCIs3dLkaek1irOt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI93ys%2FbtsJGA5t0Bq%2FhehqGHCIs3dLkaek1irOt1%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;700&quot; height=&quot;356&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://dongsik93.github.io/til/2020/01/08/til-authorization(1)/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #404040; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 로그인을 한다&lt;/li&gt;
&lt;li&gt;서버측에서는 로그인 정보를 통해 사용자를 확인한다.&lt;/li&gt;
&lt;li&gt;서버측에서 세션 저장소에 사용자 세션 생성을 요청한다.&lt;/li&gt;
&lt;li&gt;사용자의 고유한 ID값을 부여하여 세션 저장소에 저장하고, 이와 연결되는 세션 ID를 발행한다.&lt;/li&gt;
&lt;li&gt;사용자는 서버에서 해당 세션ID를 받아 쿠키에 저장한다&lt;/li&gt;
&lt;li&gt;인증이 필요한 요청을 할 때 헤더에 쿠키를 실어 요청한다&lt;/li&gt;
&lt;li&gt;서버에서는 쿠키를 받아 세션 저장소에 대조를 한다&lt;/li&gt;
&lt;li&gt;인증 후 요청된 정보를 가져온다&lt;/li&gt;
&lt;li&gt;인증이 완료되고 서버는 사용자에게 맞는 데이터를 보내준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; session 과 cookie&lt;/h2&gt;
&lt;table style=&quot;background-color: #ffffff; color: #212529; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;저장위치&lt;/td&gt;
&lt;td&gt;클라이언트&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이프사이클(만료시점)&lt;/td&gt;
&lt;td&gt;쿠키 저장시 설정&lt;/td&gt;
&lt;td&gt;브라우저 종료 시 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;비교적 취약&lt;/td&gt;
&lt;td&gt;안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;비교적 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; Next.js에서 session과 cookie이용하기&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;server 컴포넌트와 client 컴포넌트 관점에서 프로젝트에서 사용한 로직을 정리해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버 컴포넌트 - instance.ts, authApi.ts&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1727096681824&quot; class=&quot;javascript&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;'use server';

import { cookies } from 'next/headers'; // 서버 사이드에서 쿠키를 처리하기 위한 내장 모듈
import { BASE_URL } from '@/constants/url';
import { auth } from '@/auth';
import { redirect } from 'next/dist/server/api-utils';

interface RequestOptions {
  headers?: Record&amp;lt;string, string&amp;gt;;
  [key: string]: string | Record&amp;lt;string, string&amp;gt; | undefined;
}

const fetchInstance = async (url: string, options: RequestOptions = {}) =&amp;gt; {
  const session = await auth();
  const accessToken = session?.user?.accessToken;

  const headers: RequestOptions['headers'] = {
    'Content-Type': 'application/json',
    ...options.headers,
  };

  if (accessToken) {
    headers.Authorization = `Bearer ${accessToken}`;
  }

  try {
    const response = await fetch(`${BASE_URL}${url}`, {
      ...options,
      headers,
    });

   //response 로직 생략 
   
  } catch (error) {
   //에러 처리 로직 생략
  }
};

export const instance = fetchInstance;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727098111977&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use server';

import { BASE_URL } from '@/constants/url';
import { instance } from './instance';

export const signIn = async ({ email, password }: { email: string; password: string }) =&amp;gt; {
  const response = await instance('sign-in', {
    body: JSON.stringify({ email, password }),
    credentials: 'include',
    method: 'POST',
  });

  return response;
};

export const reissueToken = async (refresh_token: string) =&amp;gt; {
  const response = await fetch(`${BASE_URL}reissue`, {
    headers: {
      Cookie: `refresh_token=${refresh_token}`,
    },
    method: 'POST',
  });

  // reponse에러 로직 생략

  const cookie = response.headers.get('Set-Cookie');
  const { accessToken } = await response.json();
  const refreshToken = cookie?.split(';')[0].split('=')[1];

  return { accessToken, refreshToken };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세션(Session) &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 사이드에서는&lt;b&gt; auth() 함수&lt;/b&gt;를 통해 세션 정보를 가져온다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에는 보통 사용자 액세스 토큰과 같은 인증 관련 정보가 포함되며, 이를 이용해 API 요청 시 인증을 처리한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;fetchInstance 함수에서는 세션 정보를 가져와서 액세스 토큰(session?.user?.accessToken)을 추출하고, 이 토큰을 Authorization 헤더에 넣어 API 요청 시 사용한다.&amp;nbsp; (Bearer ${accessToken})&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt; 액세스 토큰(Access Token)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액세스 토큰은 서버에서 외부 서비스로의 API 요청을 인증하기 위해 사용된다. 이 토큰이 있어야만 권한이 필요한 작업들을 수행할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;코드에서는 /sign-in이나 토큰 재발급(reissue) 같은 API 요청을 보낼 때, 헤더 또는 본문에 액세스 토큰을 포함시켜 요청을 인증한다. 만약 액세스 토큰이 만료되거나 없을 경우, 새로운 토큰을 발급받거나 에러 처리 로직이 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 쿠키(Cookies)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키는 주로 서버 사이드에서 세션을 유지하는 데 사용된다. reissueToken 함수에서는 refresh_token이 쿠키에 저장되어, 이를 이용해 새로운 액세스 토큰을 발급받는다.&lt;/li&gt;
&lt;li&gt;또한 서버는 Set-Cookie 헤더를 통해 새로운 토큰을 클라이언트에 저장할 수 있다. (response.headers.get('Set-Cookie'))&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클라이언트 컴포넌트 - Header.tsx&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1727098353362&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';

//style관련 import 생략

import useUserInfo from '@/hook/useUserInfo';
import { useUserInfoStore } from '@/store/memberStore';
import { useSession } from 'next-auth/react';
import { useEffect } from 'react';
import { getUserInfo } from '@/api/memberApi';

export default function Header() {
  const userInfo = useUserInfo();
  const { setUserInfo } = useUserInfoStore();

  const { data: session, update } = useSession();

  const accessToken = session?.user?.accessToken;
  const refreshToken = session?.user?.refreshToken;

  // update();

  useEffect(() =&amp;gt; {
    const storedUserInfo = localStorage.getItem('userInfo');
    if (storedUserInfo &amp;amp;&amp;amp; !accessToken &amp;amp;&amp;amp; !refreshToken) {
      localStorage.removeItem('userInfo');
      setUserInfo(null);
    }
  }, [accessToken, refreshToken, setUserInfo]);

  // 유저정보조회
  useEffect(() =&amp;gt; {
    (async () =&amp;gt; {
      if (!localStorage.getItem('userInfo') &amp;amp;&amp;amp; accessToken &amp;amp;&amp;amp; refreshToken) {
        const result = await getUserInfo();
        const storedUserInfo = { name: result.name, course: result.course, email: result.email };
        if (result) {
          localStorage.setItem('userInfo', JSON.stringify(storedUserInfo));
          setUserInfo(result);
        }
      }
    })();
  }, [accessToken, refreshToken, setUserInfo]);

  return (
     // html 로직 생략
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세션(Session)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 사이드에서는 &lt;b&gt;next-auth의 useSession&lt;/b&gt;을 사용해 사용자 세션 상태를 관리한다. 로그인 시 액세스 토큰과 리프레시 토큰을 세션에 저장한 후, 이를 이용해 인증된 API 요청을 보낼 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;토큰을 가져오고(session?.user?.accessToken, session?.user?.refreshToken), 이 토큰들을 이용해 사용자 정보를 서버에서 받아와 전역 상태에 저장하거나 localStorage에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;액세스 토큰(Access Token)&lt;/span&gt;&lt;/b&gt;&lt;/h4&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;/ul&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쿠키(Cookies)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 사이드에서는 쿠키를 직접적으로 다루진 않지만, credentials: 'include' 옵션을 통해 쿠키를 자동으로 요청에 포함시킬 수 있다. 이를 통해 브라우저가 자동으로 쿠키(예: 리프레시 토큰)를 서버에 전송하여 세션이 유진된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전반적으로 핵심은 next.js는 서버 컴포넌트에서 쿠키를 읽고 처리한 뒤 클라이언트로 필요한 데이터만 보내주는 방식이기 때문에 쿠키 값을 클라이언트에서 직접 노출시키지 않고도 사용할 수 있다!!&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 우리 프로젝트에서는 세션과 쿠키, 토큰을 모두 사용했다. 보통 세션, 쿠키 vs jwt(토큰) 이렇게 나눠서 로그인을 구현한다. 하지만 Next.js에서 제공하는 useSession훅을 사용해 세션 관리 기능을 활용하면서도, 클라이언트에서 토큰을 통해 API 요청을 처리하는 방식으로 혼합된 구조를 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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://nextjs.org/docs/app/building-your-application/authentication#session-management&quot;&gt;https://nextjs.org/docs/app/building-your-application/authentication#session-management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dongsik93.github.io/til/2020/01/08/til-authorization(1)/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dongsik93.github.io/til/2020/01/08/til-authorization(1)/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@beneficial/Session%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@beneficial/Session%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/Next.js</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/184</guid>
      <comments>https://2-doooo-2.tistory.com/184#entry184comment</comments>
      <pubDate>Mon, 23 Sep 2024 00:03:55 +0900</pubDate>
    </item>
    <item>
      <title>SVG 활용하기</title>
      <link>https://2-doooo-2.tistory.com/183</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1jJUT/btsLAiBwMZ7/cNqaIHsExrQ6cb9fTGeHL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1jJUT/btsLAiBwMZ7/cNqaIHsExrQ6cb9fTGeHL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1jJUT/btsLAiBwMZ7/cNqaIHsExrQ6cb9fTGeHL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1jJUT%2FbtsLAiBwMZ7%2FcNqaIHsExrQ6cb9fTGeHL0%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;700&quot; height=&quot;366&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; SVG란?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SVG는 'Scalable Vector Graphics' 의 약어로&lt;br /&gt;각 위치 값을 표시하는 벡터와 같은 방식의 2차원 그래픽용 XML 기반의 형식을 이야기한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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-origin-width=&quot;1280&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duvO1o/btsLzq0VtnI/uhuDzjQPCjO1QQ9l49hvkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duvO1o/btsLzq0VtnI/uhuDzjQPCjO1QQ9l49hvkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duvO1o/btsLzq0VtnI/uhuDzjQPCjO1QQ9l49hvkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduvO1o%2FbtsLzq0VtnI%2FuhuDzjQPCjO1QQ9l49hvkK%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;600&quot; height=&quot;279&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;595&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅SVG를 사용하는 이유는? &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PNG나 JPG는 비트맵 기반의 이미지로 각 항목에 하나 이상의 정보 비트를 가지고 있는 표현이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 SVG는 벡터 기반으로 각 좌표에 점을 이어서 만들어지는 개념이다. 벡터 기반의 아이콘은 확대나 축소를 해도 깨지지 않는 것과 같은 원리이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;프로젝트에서의 SVG 활용 사례&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 참여한 프로젝트에서는 &lt;b&gt;MobileFooter&lt;/b&gt; 컴포넌트에 SVG 아이콘을 활용하여 사용자 경험을 개선했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단 네비게이션 메뉴에서 각 아이콘을 클릭하면 fill이 채워지도록 하졌고 css의 scale을 이용해 크기를 자연스럽게 커지도록 하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735537499450&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function MobileFooter() {
   const menuItems = [
    {
      href: '/',
      label: '카드',
      icon: (
        &amp;lt;svg
          className={styles.svg}
          width=&quot;51&quot;
          height=&quot;38&quot;
          viewBox=&quot;0 0 51 38&quot;
          fill=&quot;none&quot;
          xmlns=&quot;http://www.w3.org/2000/svg&quot;
        &amp;gt;
          &amp;lt;path
            d=&quot;M1 19.8676C1 11.2095 1 6.88046 3.83129 4.19071C6.66261 1.50098 11.2195 1.50098 20.3333 1.50098H30C39.1137 1.50098 43.6708 1.50098 46.502 4.19071C49.3333 6.88046 49.3333 11.2095 49.3333 19.8676C49.3333 28.5257 49.3333 32.8549 46.502 35.5445C43.6708 38.2343 39.1137 38.2343 30 38.2343H20.3333C11.2195 38.2343 6.66261 38.2343 3.83129 35.5445C1 32.8549 1 28.5257 1 19.8676Z&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;3&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M20.3332 30.1982H10.6665&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;3&quot;
            strokeLinecap=&quot;round&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M15.4998 23.3105H10.6665&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;3&quot;
            strokeLinecap=&quot;round&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M1 15.2764H49.3333&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;3&quot;
            strokeLinecap=&quot;round&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M30 26.7548C30 24.5902 30 23.508 30.7078 22.8355C31.4157 22.1631 32.5549 22.1631 34.8333 22.1631C37.1118 22.1631 38.251 22.1631 38.9588 22.8355C39.6667 23.508 39.6667 24.5902 39.6667 26.7548C39.6667 28.9193 39.6667 30.0015 38.9588 30.674C38.251 31.3464 37.1118 31.3464 34.8333 31.3464C32.5549 31.3464 31.4157 31.3464 30.7078 30.674C30 30.0015 30 28.9193 30 26.7548Z&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;3&quot;
          /&amp;gt;
        &amp;lt;/svg&amp;gt;
      ),
    },
    {
      href: '/travel',
      label: '여행',
      icon: (
        &amp;lt;svg
          className={styles.svg}
          width=&quot;30&quot;
          height=&quot;32&quot;
          viewBox=&quot;0 0 30 33&quot;
          fill=&quot;none&quot;
          xmlns=&quot;http://www.w3.org/2000/svg&quot;
        &amp;gt;
          &amp;lt;path
            d=&quot;M10.3851 31.288L14.0056 28.2425C14.5494 27.7763 15.4506 27.7763 15.9945 28.2425L19.6149 31.288C20.4539 31.7075 21.4795 31.288 21.7902 30.3868L22.4739 28.3202C22.6448 27.8229 22.4739 27.0926 22.101 26.7197L18.5738 23.177C18.3097 22.9284 18.1077 22.4312 18.1077 22.0738V17.6454C18.1077 16.9928 18.5894 16.682 19.1953 16.9306L26.8246 20.2247C28.0211 20.7375 29 20.1004 29 18.7952V16.7908C29 15.7497 28.2231 14.5533 27.2597 14.1493L18.5738 10.4045C18.3252 10.2958 18.1077 9.96947 18.1077 9.68978V5.0283C18.1077 3.5677 17.0355 1.84295 15.7303 1.17481C15.2642 0.941731 14.7203 0.941731 14.2542 1.17481C12.949 1.84295 11.8768 3.58324 11.8768 5.04384V9.70532C11.8768 9.98501 11.6593 10.3113 11.4106 10.4201L2.74029 14.1648C1.77691 14.5533 1 15.7497 1 16.7908V18.7952C1 20.1004 1.97891 20.7375 3.17536 20.2247L10.8047 16.9306C11.3951 16.6665 11.8923 16.9928 11.8923 17.6454V22.0738C11.8923 22.4312 11.6903 22.9284 11.4417 23.177L7.91454 26.7197C7.54162 27.0926 7.3707 27.8074 7.54162 28.3202L8.2253 30.3868C8.50499 31.288 9.53052 31.7231 10.3851 31.288Z&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;1.5&quot;
            strokeLinecap=&quot;round&quot;
            strokeLinejoin=&quot;round&quot;
          /&amp;gt;
        &amp;lt;/svg&amp;gt;
      ),
    },
    {
      href: '/banking',
      label: '뱅킹',
      icon: (
        &amp;lt;svg
          className={styles.svg}
          width=&quot;36&quot;
          height=&quot;36&quot;
          viewBox=&quot;0 0 36 36&quot;
          fill=&quot;none&quot;
          xmlns=&quot;http://www.w3.org/2000/svg&quot;
        &amp;gt;
          &amp;lt;path
            d=&quot;M1.3335 21.3333C1.3335 15.048 1.3335 11.9052 3.28611 9.95262C5.23875 8 8.38143 8 14.6668 8H21.3335C27.6188 8 30.7617 8 32.7142 9.95262C34.6668 11.9052 34.6668 15.048 34.6668 21.3333C34.6668 27.6187 34.6668 30.7615 32.7142 32.714C30.7617 34.6667 27.6188 34.6667 21.3335 34.6667H14.6668C8.38143 34.6667 5.23875 34.6667 3.28611 32.714C1.3335 30.7615 1.3335 27.6187 1.3335 21.3333Z&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;2&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M24.6668 7.99967C24.6668 4.85697 24.6668 3.28562 23.6905 2.30932C22.7142 1.33301 21.1428 1.33301 18.0002 1.33301C14.8575 1.33301 13.2861 1.33301 12.3098 2.30932C11.3335 3.28562 11.3335 4.85697 11.3335 7.99967&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;2&quot;
          /&amp;gt;
          &amp;lt;path
            d=&quot;M18.0003 26.8882C19.8413 26.8882 21.3337 25.6445 21.3337 24.1105C21.3337 22.5763 19.8413 21.3327 18.0003 21.3327C16.1593 21.3327 14.667 20.089 14.667 18.5548C14.667 17.0208 16.1593 15.7772 18.0003 15.7772M18.0003 26.8882C16.1593 26.8882 14.667 25.6445 14.667 24.1105M18.0003 26.8882V27.9993M18.0003 15.7772V14.666M18.0003 15.7772C19.8413 15.7772 21.3337 17.0208 21.3337 18.5548&quot;
            stroke=&quot;#9ca3af&quot;
            strokeWidth=&quot;2&quot;
            strokeLinecap=&quot;round&quot;
          /&amp;gt;
        &amp;lt;/svg&amp;gt;
      ),
    },
  ];
  
  return (
    &amp;lt;div className={styles.mobile}&amp;gt;
      &amp;lt;footer className={styles.footer}&amp;gt;
        &amp;lt;nav className={styles.nav}&amp;gt;
          &amp;lt;ul className={styles.menuList}&amp;gt;
            {menuItems.map((item) =&amp;gt; {
              const isActive =
                item.href === '/'
                  ? pathname === item.href
                  : pathname.includes(item.href!);

              return (
                &amp;lt;li key={item.href} className={styles.menuItem}&amp;gt;
                  &amp;lt;Link href={item.href || '/'} className={styles.menuLink}&amp;gt;
                    &amp;lt;span
                      className={`${styles.iconWrapper}  ${isActive ? styles.active : ''}`}
                    &amp;gt;
                      {item.icon}
                    &amp;lt;/span&amp;gt;
                    &amp;lt;span
                      className={`${styles.label} ${isActive ? styles.active : ''}`}
                    &amp;gt;
                      {item.label}
                    &amp;lt;/span&amp;gt;
                  &amp;lt;/Link&amp;gt;
                &amp;lt;/li&amp;gt;
              );
            })}
          &amp;lt;/ul&amp;gt;
        &amp;lt;/nav&amp;gt;
      &amp;lt;/footer&amp;gt;
    &amp;lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;SVG 임포트 방식 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React와 Next.js에서는 여러 방법으로 SVG를 사용할 수 있다. 각각의 장단점을 살펴보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Inline SVG&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735537657012&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const MyIcon = () =&amp;gt; ( 
	&amp;lt;svg width=&quot;24&quot; height=&quot;24&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt; 
		&amp;lt;path d=&quot;M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z&quot; /&amp;gt; 
	&amp;lt;/svg&amp;gt; 
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 JSX에 작성하므로 외부 파일 의존성이 없다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;동적으로 속성을 조정하기 용이하다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 가독성이 떨어질 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;복잡한 SVG의 경우 JSX 코드가 길어질 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 파일 임포트&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538066307&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import MyIcon from './icon.svg'; 

const App = () =&amp;gt; &amp;lt;MyIcon /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SVG 파일을 별도로 관리할 수 있어 코드가 간결해진다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;SVGR을 설정하면 SVG를 React 컴포넌트처럼 사용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 도구(Webpack, Vite 등)에 추가 설정이 필요하다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. CSS 백그라운드 이미지&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538128681&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.icon { 
   background-image: url('./icon.svg');
   width: 24px; 
   height: 24px; 
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스타일 파일에서 SVG를 관리하므로 레이아웃과 스타일을 분리할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SVG의 색상이나 크기를 동적으로 변경하기 어렵다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;React에서 SVG 스타일링&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 속성을 활용한 동적 스타일링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 width, height, fill, stroke 같은 SVG 속성을 props로 전달하여 동적으로 조정할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538215851&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const DynamicIcon = ({ size = 24, color = 'black' }) =&amp;gt; ( 
 &amp;lt;svg width={size} height={size} fill={color} xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt; 
 	&amp;lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;10&quot; stroke=&quot;black&quot; strokeWidth=&quot;2&quot; /&amp;gt; 
 &amp;lt;/svg&amp;gt; 
 );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. CSS를 통한 스타일링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 모듈 또는 Tailwind CSS를 사용하면 SVG에 스타일을 쉽게 적용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538312042&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import styles from './Icon.module.css'; 

const StyledIcon = () =&amp;gt; ( 
    &amp;lt;svg className={styles.icon} xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt; 
    	&amp;lt;path d=&quot;M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z&quot; /&amp;gt; 
    &amp;lt;/svg&amp;gt; 
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538325549&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* Icon.module.css */ 
.icon { fill: blue; transition: fill 0.3s; } 
.icon:hover { fill: red; }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;SVG 최적화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. SVGO를 활용한 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SVG 파일을 최적화하면 불필요한 속성과 코드가 제거되어 성능이 향상된다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도구&lt;/b&gt;: &lt;a href=&quot;https://github.com/svg/svgo&quot;&gt;&lt;span&gt;SVGO&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최적화 방법&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735538347185&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx svgo ./icon.svg --output ./icon.optimized.svg&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;size18&quot;&gt;&lt;b&gt;2. 컴포넌트화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SVG를 React 컴포넌트로 변환하면 재사용성과 코드 관리가 편리해진다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735538406537&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react'; 

const Icon = ({ color = 'black' }) =&amp;gt; ( 
    &amp;lt;svg fill={color} xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt; 
        &amp;lt;path d=&quot;M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z&quot; /&amp;gt; 
    &amp;lt;/svg&amp;gt; 
); 

export default Icon;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;Next.js 환경에서의 SVG 사용 팁&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 서버 사이드 렌더링 (SSR)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서 SVG 컴포넌트를 SSR로 렌더링하려면 SVGR 설정을 추가해야 한다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;next.config.js&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1735538527176&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;module.exports = { 
	webpack(config) { 
        config.module.rules.push({
            test: /\.svg$/, 
            use: ['@svgr/webpack'], 
        }); 
	return config; 
   }, 
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;b&gt;2. Lazy Load와 Dynamic Import&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SVG 아이콘을 동적으로 로드하면 초기 로딩 속도를 개선할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735538605668&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import dynamic from 'next/dynamic'; 

const DynamicIcon = dynamic(() =&amp;gt; import('./MyIcon'), { ssr: false }); 

const App = () =&amp;gt; &amp;lt;DynamicIcon /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;실제 사용 사례와 확장&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션 추가 - SVG에 간단한 애니메이션을 추가하면 더욱 생동감 있는 UI를 만들 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735538683190&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const AnimatedIcon = () =&amp;gt; ( 
    &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;
        &amp;lt;circle cx=&quot;50&quot; cy=&quot;50&quot; r=&quot;40&quot; stroke=&quot;black&quot; strokeWidth=&quot;3&quot; fill=&quot;red&quot;&amp;gt;
            &amp;lt;animate attributeName=&quot;r&quot; from=&quot;40&quot; to=&quot;20&quot; dur=&quot;1s&quot; repeatCount=&quot;indefinite&quot; /&amp;gt; 
        &amp;lt;/circle&amp;gt;
    &amp;lt;/svg&amp;gt; 
);&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;참고자료&amp;nbsp;&lt;/span&gt;&lt;/b&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://brunch.co.kr/@ggk234/11&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brunch.co.kr/@ggk234/11&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/기타</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/183</guid>
      <comments>https://2-doooo-2.tistory.com/183#entry183comment</comments>
      <pubDate>Tue, 17 Sep 2024 23:47:00 +0900</pubDate>
    </item>
    <item>
      <title>Electron이란?</title>
      <link>https://2-doooo-2.tistory.com/182</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lI8zt/btsLATam3gW/4X91OJ1Dso5BQoN6oouLEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lI8zt/btsLATam3gW/4X91OJ1Dso5BQoN6oouLEK/img.png&quot; data-alt=&quot;electron&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lI8zt/btsLATam3gW/4X91OJ1Dso5BQoN6oouLEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlI8zt%2FbtsLATam3gW%2F4X91OJ1Dso5BQoN6oouLEK%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;700&quot; height=&quot;368&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;electron&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Electron이란?&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일렉트론이란 데스크톱 애플리케이션을 구축하는 도구&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Electron은 데스크톱 애플리케이션을 개발하기 위한 &lt;b&gt;오픈소스 프레임워크&lt;/b&gt;이다. HTML, CSS, JavaScript와 같은 웹 기술을 사용하여 Windows, macOS, Linux에서 실행 가능한 크로스플랫폼 데스크톱 애플리케이션을 만들 수 있다. 물론 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Angular, React, Vue도 사용가능하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가로 매년마다 갱신되는 프론트엔드 로드맵 마지막 쪽에서 electron을 찾아볼 수 있다. 그만큼 프론트엔드에서도 나름 중요한 역할을 차지하고 있다는 거 아닐까?&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;1337&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN7oCS/btsLBW4UJVo/tnfTVqli7PhwiGNKP8FKnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN7oCS/btsLBW4UJVo/tnfTVqli7PhwiGNKP8FKnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN7oCS/btsLBW4UJVo/tnfTVqli7PhwiGNKP8FKnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN7oCS%2FbtsLBW4UJVo%2FtnfTVqli7PhwiGNKP8FKnk%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;700&quot; height=&quot;204&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅Electron을 활용한 대표적인 애플리케이션&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;937&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n9CAo/btsLADk2LdG/wZck3qnKsOXlhUg3boxlP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n9CAo/btsLADk2LdG/wZck3qnKsOXlhUg3boxlP0/img.png&quot; data-alt=&quot;출저:Dev.DY&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n9CAo/btsLADk2LdG/wZck3qnKsOXlhUg3boxlP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn9CAo%2FbtsLADk2LdG%2FwZck3qnKsOXlhUg3boxlP0%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;700&quot; height=&quot;260&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출저:Dev.DY&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리가 알고 있는 많은 앱들이 이미 electron을 이용하고 있고 추가로 notion,postman도 electron을 활용한 데스크탑 앱이다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅&lt;/b&gt;&lt;b&gt;Electron의 장점과 단점&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&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;웹 기술 기반으로 기존 프론트엔드 개발자에게 친숙&lt;/li&gt;
&lt;li&gt;Node.js 및 Electron API로 강력한 기능 제공&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&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;애플리케이션 크기 증가 (Chromium 포함)&lt;/li&gt;
&lt;li&gt;성능 이슈 (특히 대규모 애플리케이션)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅Electron 설치 및 실행&lt;/b&gt;&lt;/h2&gt;
&lt;h4 id=&quot;설치&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735542850513&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install --save-dev electron&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;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;그리고 나서 package.json을 수정해야 한다. main에 main.js를 설정하지 않으면 Electron은 index.js를 로드하려고 할 것이다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1735542984569&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json
{
  &quot;name&quot;: &quot;your-app&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;main&quot;: &quot;main.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;electron .&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;mainjs&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;main.js&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcBvy/btsLBeL5wIR/N2UWYkwrGPMffkMKScU121/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcBvy/btsLBeL5wIR/N2UWYkwrGPMffkMKScU121/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcBvy/btsLBeL5wIR/N2UWYkwrGPMffkMKScU121/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcBvy%2FbtsLBeL5wIR%2FN2UWYkwrGPMffkMKScU121%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;641&quot; height=&quot;364&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Electron에서 메인 프로세스 파일은 애플리케이션의 진입점이며,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Electron API를 사용하여 애플리케이션의 라이프사이클을 관리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1735543158891&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { app, BrowserWindow } = require('electron')
const path = require('path')
const isDev = require('electron-is-dev')

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  })

  // 개발 모드에서는 localhost:3000, 프로덕션에서는 build된 파일을 로드
  win.loadURL(
    isDev 
      ? 'http://localhost:3000'
      : `file://${path.join(__dirname, '../build/index.html')}`
  )

  // 개발자 도구 열기
  if (isDev) {
    win.webContents.openDevTools()
  }
}

app.whenReady().then(createWindow)

app.on('window-all-closed', () =&amp;gt; {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () =&amp;gt; {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app: 애플리케이션의 생명 주기를 관리하는 Electron 모듈. 애플리케이션이 시작되고 종료될 때 발생하는 이벤트를 처리&lt;/li&gt;
&lt;li&gt;BrowserWindow: 새로운 브라우저 창을 생성하는 데 사용되는 모듈. 이 창은 HTML, CSS, JavaScript를 렌더링하여 데스크탑 애플리케이션 UI를 제공.&lt;/li&gt;
&lt;li&gt;path: 파일 경로를 다룰 수 있는 Node.js 내장 모듈로, 경로를 안전하게 처리하는 데 사용.&lt;/li&gt;
&lt;li&gt;isDev: 현재 Electron 애플리케이션이 개발 모드인지 확인하는 라이브러리. 개발 모드에서는 http://localhost:3000에서 앱을 실행하고, 프로덕션 모드에서는 빌드된 파일을 로드.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;index.html&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735543448450&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --&amp;gt;
    &amp;lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src 'self'; script-src 'self'&quot;&amp;gt;
    &amp;lt;title&amp;gt;Hello World!&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Hello World!&amp;lt;/h1&amp;gt;
    We are using Node.js &amp;lt;span id=&quot;node-version&quot;&amp;gt;&amp;lt;/span&amp;gt;,
    Chromium &amp;lt;span id=&quot;chrome-version&quot;&amp;gt;&amp;lt;/span&amp;gt;,
    and Electron &amp;lt;span id=&quot;electron-version&quot;&amp;gt;&amp;lt;/span&amp;gt;.
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 애플리케이션 실행 &lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735543316257&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm start&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;912&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ovFHp/btsLCHMZr3N/gTjMguDuatrkP1P9tkpLHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ovFHp/btsLCHMZr3N/gTjMguDuatrkP1P9tkpLHk/img.png&quot; data-alt=&quot;출처:www.electronjs.org&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ovFHp/btsLCHMZr3N/gTjMguDuatrkP1P9tkpLHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FovFHp%2FbtsLCHMZr3N%2FgTjMguDuatrkP1P9tkpLHk%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;700&quot; height=&quot;546&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:www.electronjs.org&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;마치며.... 이 글을 쓰게 된 계기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2달여동안 프로젝트를 진행하며 사용자를 늘리기 위해 이것저것 시도해봤지만 일회성 사용자만 잠시 늘어날 뿐 정기 사용자를 유지한다는 것이 얼마나 어려운일지 알게 되었다. 그래서 electron을 이용해서 데스크톱 앱을 만들었으면 조금이라도 정기 사용자가 늘지 않았을까라는 아쉬움에 electorn에 대해 공부하고 코드에서 어떻게 활용하는지 알아보고자 이 글을 작성하게 되었다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고자료&amp;nbsp;&lt;/b&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://www.electronjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.electronjs.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@ckstn0777/Electron%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@ckstn0777/Electron%EC%9D%BC%EB%A0%89%ED%8A%B8%EB%A1%A0-%EC%9D%B4%EB%9E%80&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend/기타</category>
      <author>2-doooo-2</author>
      <guid isPermaLink="true">https://2-doooo-2.tistory.com/182</guid>
      <comments>https://2-doooo-2.tistory.com/182#entry182comment</comments>
      <pubDate>Tue, 17 Sep 2024 21:00:37 +0900</pubDate>
    </item>
  </channel>
</rss>