<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DYO 공부하는 블로그</title>
    <link>https://yun-engene.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 10 Jun 2026 01:50:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DYODa</managingEditor>
    <image>
      <title>DYO 공부하는 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5212955/attach/acadbee9bf594c2f8bca5828b1649178</url>
      <link>https://yun-engene.tistory.com</link>
    </image>
    <item>
      <title>시니어 개발자의 무기는 무엇일까?</title>
      <link>https://yun-engene.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;758ba8b5-6110-47ec-9652-5f335128adb1.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duhzUc/dJMcadhJoDW/bCd03c1LDcwC2TkZTz6F2k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duhzUc/dJMcadhJoDW/bCd03c1LDcwC2TkZTz6F2k/img.jpg&quot; data-alt=&quot;시니어 개발자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duhzUc/dJMcadhJoDW/bCd03c1LDcwC2TkZTz6F2k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduhzUc%2FdJMcadhJoDW%2FbCd03c1LDcwC2TkZTz6F2k%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;1024&quot; height=&quot;559&quot; data-filename=&quot;758ba8b5-6110-47ec-9652-5f335128adb1.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시니어 개발자&lt;/figcaption&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;나는 이제 갓 5개월 된 신입 개발자다. 어려운 시기에 취직한 게 다행이라고 해야 할지, 공백기가 조금 있었음에도 운이 좋게 어느 정도 규모가 있는 회사에 들어갈 수 있었다.&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;그중에서도 내가 일하면서 가장 좋아하는 지점은 다른 개발자들이 어떻게 일하는지 보고 배울 수 있다는 점이다. 어떻게 요구를 관리하고, 기획서를 어떻게 해석하며, 기능 적용에 대해서 어떻게 구현할지를 생각하는 선배 개발자들을 보면서 배우고 있다.&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;미들급 개발자들에게 구현방법이나 회사 코드에 대해서 질문하고 답변을 받으면 '내가 모르는 부분'에 대해서 정말 잘 알고 빠르다는 인상을 받는다.&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;투자와 서비스 모니터링을 다루는 회사 도메인 특성상 하나의 기능에도 여러 팀이 엮인다.&lt;br /&gt;서비스 신청, 모니터링, 계약, 청구, 정산, 계좌연동, 보안까지 연결되다 보니 &amp;lsquo;이걸 도대체 어떻게 접근해야 하지?&amp;rsquo; 막막할 때가 많았다.&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;최근에 마케팅팀, 서비스팀, PO팀, 실무팀이 모두 엮인 일에 대해서 서비스 페이지 기능과 백오피스 기능을 만들어야 하는 경우가 있었다.&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;size18&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&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;&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;&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;/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-end=&quot;540&quot; data-start=&quot;430&quot; data-ke-size=&quot;size16&quot;&gt;신입 개발자는 보통 모든 예외 상황과 프로세스를 머릿속에 넣고 구현하려고 한다. 나 역시 &amp;ldquo;이 경우는?&amp;rdquo;, &amp;ldquo;저 팀은?&amp;rdquo;, &amp;ldquo;기존 흐름은?&amp;rdquo;을 계속 고민하면서 점점 복잡하게 생각하고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;540&quot; data-start=&quot;430&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;그런데 시니어는 오히려 책임의 경계를 먼저 나눈다고 느꼈다. 누가 결정하고, 누가 검증하고, 어디까지를 시스템이 책임질지를 정한 뒤 기능 자체를 단순하게 만든다.&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;695&quot; data-start=&quot;639&quot; data-ke-size=&quot;size16&quot;&gt;지금 와서 생각해보면, 기술 자체보다도 복잡함을 관리하는 방식이 더 큰 차이를 만든다는 생각이 든다.&lt;/p&gt;</description>
      <category>일</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/112</guid>
      <comments>https://yun-engene.tistory.com/112#entry112comment</comments>
      <pubDate>Thu, 14 May 2026 20:59:18 +0900</pubDate>
    </item>
    <item>
      <title>데브코스 5기 수료 후기</title>
      <link>https://yun-engene.tistory.com/111</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mj4VX/dJMcagxlpW3/0qHnkqzMHOlw0EhwDOzpgk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mj4VX/dJMcagxlpW3/0qHnkqzMHOlw0EhwDOzpgk/img.jpg&quot; data-alt=&quot;데브코스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mj4VX/dJMcagxlpW3/0qHnkqzMHOlw0EhwDOzpgk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMj4VX%2FdJMcagxlpW3%2F0qHnkqzMHOlw0EhwDOzpgk%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;440&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데브코스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 시작하게 되었나&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byemB7/dJMcaaDTQGF/bPpM1zLlmAtrWsolKU6kqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byemB7/dJMcaaDTQGF/bPpM1zLlmAtrWsolKU6kqk/img.png&quot; data-alt=&quot;데브코스 들어가기 전에 일하던 매장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byemB7/dJMcaaDTQGF/bPpM1zLlmAtrWsolKU6kqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyemB7%2FdJMcaaDTQGF%2FbPpM1zLlmAtrWsolKU6kqk%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;709&quot; height=&quot;533&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데브코스 들어가기 전에 일하던 매장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4년제 컴공과를 졸업하고, 취직이 어려워 비개발 직무로 일을 하고 있었는데요. (자전거 스포츠웨어 매장 점장) 일을 하던 초반에는 매장관리, 판매업이 재미있었어서 개발이 내 업이 아닌가 고민하고 있었어요. 임금에도 나름 만족하고 있었구요.&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;그런데 일하면서도 취미 생활로 간단하게 프로젝트하고, 일하는 중에 코테공부, 언어공부 만지작거리고 있는 자신을 발견하고 개발자로 재시작을 해보자. 싶어서 부트캠프를 찾아보다가 데브코스에 들어가게 되었습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;입과와 수료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정은 5월 중순에 들어가서 10월 중순에 끝났습니다. 수료하고 직후에 바로 후기를 쓰지 않은 건 그 사이에 아주 바빴거든요. 이력서 쓰고, 포트폴리오 웹사이트-PPT 작성하고, 면접 스터디 진행하고, 짬내서 프로젝트 팀에 합류하고.&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;12월 중순인 지금은 취직을 확정짓고 출근하기까지 대기중이에요. 취직까지 과정 끝나고 두달정도 걸렸네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;교육과정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;LetHimCookMagicGIF.gif&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYqD3N/dJMcagREh1V/FNx2Z6ZjE1oGAEGpjC1Tvk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYqD3N/dJMcagREh1V/FNx2Z6ZjE1oGAEGpjC1Tvk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYqD3N/dJMcagREh1V/FNx2Z6ZjE1oGAEGpjC1Tvk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bYqD3N/dJMcagREh1V/FNx2Z6ZjE1oGAEGpjC1Tvk/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;319&quot; height=&quot;171&quot; data-filename=&quot;LetHimCookMagicGIF.gif&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- 학습방법&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;평균적으로 프로젝트 계획-완성 기간이 3주, 최종 프로젝트는 4.5주 정도인 것 같았습니다.&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;비대면으로 점심시간 제외 하루 8시간이 정규 시간이고, 이외에는 자유입니다. 프로젝트 코어로 진행할 때는 팀원들과, 보통은 강의를 들었습니다. 저는 보통 정규시간 끝나고 도서관 가서 9시까지 공부하고 집에 와서 프로젝트 PR확인하고 쉬거나, 프로젝트 코드 짰었어요.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 기술들은 JS, TS, React, Next 위주이고 관련 라이브러리에 대한 학습을 진행했습니다. Zustand, Tanstack 등 현재 현업에서 많이 사용하는 최신 기술 스택 위주로 학습 과정이 짜여 있어 좋았어요.&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;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;- 멘토링&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;- 비전공자라면&lt;/h4&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;동기 중에 학습하기 어려워하시는 비전공자분들이 꽤 있었고, 중간에 나가시는 분들도 좀 있었습니다. &quot;나는 개발에 완전 문외한이다!&quot; 하시는 분은 조금 비추천이고, 사전에 JS에 대한 학습이나 React에 대한 학습을 하고 오시는 걸 추천합니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 프론트끼리 3회, 최종은 백-프론트 협업 기반으로 1회 진행했어요.&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회차&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1회차 때는 온보딩이라 그런지 주제 자체가 그렇게 어렵지는 않았어요. 1회차는 과제가 여러개였습니다.&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;- TodoList&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 바닐라로 TotoList 만들기. 각자 구현하는 방법으로 진행해서 합쳤습니다. 이때는 정돈이 정말 안 됐어서 최종 결과물을 각자 진행한 프로젝트의 모듈 몇개를 가져와 붙여 구현했어요.&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;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckjYI1/dJMcabiv4eb/e9PkYTpvvlAk0wUjjooNXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckjYI1/dJMcabiv4eb/e9PkYTpvvlAk0wUjjooNXk/img.png&quot; data-alt=&quot;TODOWEEDS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckjYI1/dJMcabiv4eb/e9PkYTpvvlAk0wUjjooNXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckjYI1%2FdJMcabiv4eb%2Fe9PkYTpvvlAk0wUjjooNXk%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;740&quot; height=&quot;352&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TODOWEEDS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766648987300&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[MINI] TODOLIST 프로젝트 후기&quot; data-og-description=&quot;-  프로젝트 개요프로젝트명: GrowWeed ToDo ListGitHub: https://github.com/yoon5450/Team_GrowWeed_To-Do-List목표: DOM과 localStorage를 이용한 기본 기능 구현 + 구조적 코드 분리 학습기능 요약:할 일 추가/삭제localSt&quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/49&quot; data-og-url=&quot;https://yun-engene.tistory.com/49&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/uoh5M/hyZPVyktBx/sy8fn8QmmCY6CCHKUWqu30/img.png?width=800&amp;amp;height=380&amp;amp;face=0_0_800_380,https://scrap.kakaocdn.net/dn/bnRnhc/hyZPOZwjdy/bKBNQlwqPXZQwvP7Arfw6k/img.png?width=800&amp;amp;height=380&amp;amp;face=0_0_800_380,https://scrap.kakaocdn.net/dn/nUlTA/hyZQsH5CWn/f0mZcfodlifRRuGwv9oNf1/img.png?width=1900&amp;amp;height=903&amp;amp;face=0_0_1900_903&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/49&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/uoh5M/hyZPVyktBx/sy8fn8QmmCY6CCHKUWqu30/img.png?width=800&amp;amp;height=380&amp;amp;face=0_0_800_380,https://scrap.kakaocdn.net/dn/bnRnhc/hyZPOZwjdy/bKBNQlwqPXZQwvP7Arfw6k/img.png?width=800&amp;amp;height=380&amp;amp;face=0_0_800_380,https://scrap.kakaocdn.net/dn/nUlTA/hyZQsH5CWn/f0mZcfodlifRRuGwv9oNf1/img.png?width=1900&amp;amp;height=903&amp;amp;face=0_0_1900_903');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[MINI] TODOLIST 프로젝트 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;-  프로젝트 개요프로젝트명: GrowWeed ToDo ListGitHub: https://github.com/yoon5450/Team_GrowWeed_To-Do-List목표: DOM과 localStorage를 이용한 기본 기능 구현 + 구조적 코드 분리 학습기능 요약:할 일 추가/삭제localSt&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzuY7O/dJMb99LKN5o/XgqrGUj6AjVgw32AI2oqfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzuY7O/dJMb99LKN5o/XgqrGUj6AjVgw32AI2oqfk/img.png&quot; data-alt=&quot;JS 실행, 코테 채점기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzuY7O/dJMb99LKN5o/XgqrGUj6AjVgw32AI2oqfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzuY7O%2FdJMb99LKN5o%2FXgqrGUj6AjVgw32AI2oqfk%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;722&quot; height=&quot;728&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JS 실행, 코테 채점기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/38&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766649268091&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[MINI] JS 코드 실행기 프로젝트 (1) - 후기 및 기술 요약&quot; data-og-description=&quot;- 설계 이유알고리즘 문제 만들기 과제 제출 폼에 JS, CSS 파일이 포함되어 있어서 코드 실행기 만들면 재밌겠다 싶어 시작하게 되었다. 아이디어 시작할 때는 9 to 6 정규 수업 시간 외에 4일 정도(&quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/38&quot; data-og-url=&quot;https://yun-engene.tistory.com/38&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nVUJ1/hyZQGzyLun/eFiRZHTJnDGZHkkLrJAZxK/img.png?width=722&amp;amp;height=728&amp;amp;face=0_0_722_728,https://scrap.kakaocdn.net/dn/bXLeyn/hyZQuMEWMT/SM8ILWbt98xSXrTKPdotmK/img.png?width=722&amp;amp;height=728&amp;amp;face=0_0_722_728,https://scrap.kakaocdn.net/dn/MFOgF/hyZPV6b9Sg/8dmVnccObmwykM7SJpBZmK/img.jpg?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/38&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nVUJ1/hyZQGzyLun/eFiRZHTJnDGZHkkLrJAZxK/img.png?width=722&amp;amp;height=728&amp;amp;face=0_0_722_728,https://scrap.kakaocdn.net/dn/bXLeyn/hyZQuMEWMT/SM8ILWbt98xSXrTKPdotmK/img.png?width=722&amp;amp;height=728&amp;amp;face=0_0_722_728,https://scrap.kakaocdn.net/dn/MFOgF/hyZPV6b9Sg/8dmVnccObmwykM7SJpBZmK/img.jpg?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[MINI] JS 코드 실행기 프로젝트 (1) - 후기 및 기술 요약&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;- 설계 이유알고리즘 문제 만들기 과제 제출 폼에 JS, CSS 파일이 포함되어 있어서 코드 실행기 만들면 재밌겠다 싶어 시작하게 되었다. 아이디어 시작할 때는 9 to 6 정규 수업 시간 외에 4일 정도(&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;제공되는 API를 이용해 노션 웹페이지와 자유주제로 구현하면 되는 프로젝트. 지금 생각해도 아키텍쳐가 정말 나빴어서 코드 탐색이 정말 어려웠던 프로젝트였어요. 제공되는 API로만 이용하기에 좀 부족한 것 같아서 AWS EC2와 Flask를 이용해 API 서버를 만들고, 그쪽으로 데이터를 요청하도록 구현했습니다.&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;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buPPLl/dJMcagxlro7/sLN8NiKIKgM6HyzPtarI60/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buPPLl/dJMcagxlro7/sLN8NiKIKgM6HyzPtarI60/img.jpg&quot; data-alt=&quot;구인구직 웹사이트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buPPLl/dJMcagxlro7/sLN8NiKIKgM6HyzPtarI60/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuPPLl%2FdJMcagxlro7%2FsLN8NiKIKgM6HyzPtarI60%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;1280&quot; height=&quot;597&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구인구직 웹사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/72&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/72&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766649249923&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;D-WORK 팀 프로젝트(2) - 개인적인 후기&quot; data-og-description=&quot;이번 포스팅에서는 실제 구현할 때 팀장으로서 프로젝트 진행 계획을 어떻게 계획했는지와 어떤 부분을 담당했는지, 실제 구현하면서 트러블슈팅 했던 경험을 다뤄보려고 한다. D-WORK 프로젝트 &quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/72&quot; data-og-url=&quot;https://yun-engene.tistory.com/72&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/l12kq/hyZPDDFvQd/XUUkUcFgBykb4QHakS8PKk/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/buWrfF/hyZPGAqlFj/YC0OAcRJZz8oKZFNYCGoZ1/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ZtDmH/hyZPVd2Y6F/QSVk9g7P1td15GahorR5gK/img.jpg?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/72&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/72&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/l12kq/hyZPDDFvQd/XUUkUcFgBykb4QHakS8PKk/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/buWrfF/hyZPGAqlFj/YC0OAcRJZz8oKZFNYCGoZ1/img.jpg?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/ZtDmH/hyZPVd2Y6F/QSVk9g7P1td15GahorR5gK/img.jpg?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;D-WORK 팀 프로젝트(2) - 개인적인 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 실제 구현할 때 팀장으로서 프로젝트 진행 계획을 어떻게 계획했는지와 어떤 부분을 담당했는지, 실제 구현하면서 트러블슈팅 했던 경험을 다뤄보려고 한다. D-WORK 프로젝트&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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회차&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2회차는 React로 구현하는 프로젝트였습니다. 실시간 채팅을 Supabase를 이용해서 구현했어요.&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;- MusicMate&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 채팅, 음성 공유, 이미지 공유, 유튜브-스포티파이-애플뮤직 링크 임베딩 기능을 구현한 음악 취향 공유 커뮤니티&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mSJ4v/dJMcacPfHS1/UVTzihOjMTcdakXeOKB4N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mSJ4v/dJMcacPfHS1/UVTzihOjMTcdakXeOKB4N0/img.png&quot; data-alt=&quot;MusicMate 프로젝트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mSJ4v/dJMcacPfHS1/UVTzihOjMTcdakXeOKB4N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmSJ4v%2FdJMcacPfHS1%2FUVTzihOjMTcdakXeOKB4N0%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;1898&quot; height=&quot;902&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MusicMate 프로젝트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/86&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/86&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766649597545&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[MusicMate] 팀 프로젝트 - 프로젝트 시작&quot; data-og-description=&quot;이번 프로젝트는 음악 클립을 공유하고 음악 관련 소통을 하는 SNS 플랫폼이다.이번에도 팀장으로 전체 프로젝트 일정 관리, 스크럼 마스터로 작업하게 되었다. 이번 글에서는 프로젝트 준비 과&quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/86&quot; data-og-url=&quot;https://yun-engene.tistory.com/86&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1ZPfJ/hyZPQQy5Qd/FqJFezRMHyBTM99QbXFv3K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b6OeDr/hyZPP5cdyv/TtXdeKtHBnZuygigMPneOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/zvVMC/hyZPJ4YD0o/F6jUgQETR1JoIKUGpsyy20/img.png?width=1610&amp;amp;height=568&amp;amp;face=0_0_1610_568&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/86&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/86&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1ZPfJ/hyZPQQy5Qd/FqJFezRMHyBTM99QbXFv3K/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b6OeDr/hyZPP5cdyv/TtXdeKtHBnZuygigMPneOK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/zvVMC/hyZPJ4YD0o/F6jUgQETR1JoIKUGpsyy20/img.png?width=1610&amp;amp;height=568&amp;amp;face=0_0_1610_568');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[MusicMate] 팀 프로젝트 - 프로젝트 시작&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 음악 클립을 공유하고 음악 관련 소통을 하는 SNS 플랫폼이다.이번에도 팀장으로 전체 프로젝트 일정 관리, 스크럼 마스터로 작업하게 되었다. 이번 글에서는 프로젝트 준비 과&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://music-mate-kappa.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://music-mate-kappa.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766649719112&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MusicMate&quot; data-og-description=&quot;음악 SNS 커뮤니티, MusicMate&quot; data-og-host=&quot;music-mate-kappa.vercel.app&quot; data-og-source-url=&quot;https://music-mate-kappa.vercel.app/&quot; data-og-url=&quot;https://music-mate-kappa.vercel.app/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/g2lne/hyZQrWH8jo/rTc2h5YkNxWqYLRG1NKOM1/img.jpg?width=312&amp;amp;height=312&amp;amp;face=0_0_312_312&quot;&gt;&lt;a href=&quot;https://music-mate-kappa.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://music-mate-kappa.vercel.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/g2lne/hyZQrWH8jo/rTc2h5YkNxWqYLRG1NKOM1/img.jpg?width=312&amp;amp;height=312&amp;amp;face=0_0_312_312');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MusicMate&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;음악 SNS 커뮤니티, MusicMate&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;music-mate-kappa.vercel.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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회차&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3회차도 React로 구현하는 프로젝트였습니다. 이때는 Tanstack 사용하면서 데이터 흐름에 신경쓰고 구현했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 프로젝트 기간도 2주로 짧아 열심히 달려서 구현했는데, 조금 지쳤었습니다.&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;- PickItBook&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서관 공공 API 를 이용한 목록 조회, 실시간 미션 피드백, 유저 책 리뷰, 통계를 집계하는 커뮤니티&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYIEoW/dJMcachpxLk/3DMl05JG6yGU2ofcdJcXF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYIEoW/dJMcachpxLk/3DMl05JG6yGU2ofcdJcXF1/img.png&quot; data-alt=&quot;PickItBook&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYIEoW/dJMcachpxLk/3DMl05JG6yGU2ofcdJcXF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYIEoW%2FdJMcachpxLk%2F3DMl05JG6yGU2ofcdJcXF1%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;1895&quot; height=&quot;898&quot; data-origin-width=&quot;1895&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PickItBook&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/103&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766649793833&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[데브코스 프론트] PickItBook 프로젝트 회고&quot; data-og-description=&quot;배포 주소 https://pick-it-book.vercel.app/ PickitBook pick-it-book.vercel.app 프로젝트 레포지토리 https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6 GitHub - prgrms-fe-devcourse/FES-5-Project-TEAM-6: 룰렛 기반 책 선택과 도전&quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/103&quot; data-og-url=&quot;https://yun-engene.tistory.com/103&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/B5lvS/hyZPGf55aa/LkjLInuGgHy6iDuZhl3Lj1/img.png?width=800&amp;amp;height=505&amp;amp;face=0_0_800_505,https://scrap.kakaocdn.net/dn/bJSCdN/hyZPRaSPB1/EnK99h1INttpbKHXmqZK7k/img.png?width=800&amp;amp;height=505&amp;amp;face=0_0_800_505,https://scrap.kakaocdn.net/dn/b4WdL6/hyZPJKF2Vp/GKlkCmdn71DcK1UwWWhi3K/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/103&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/B5lvS/hyZPGf55aa/LkjLInuGgHy6iDuZhl3Lj1/img.png?width=800&amp;amp;height=505&amp;amp;face=0_0_800_505,https://scrap.kakaocdn.net/dn/bJSCdN/hyZPRaSPB1/EnK99h1INttpbKHXmqZK7k/img.png?width=800&amp;amp;height=505&amp;amp;face=0_0_800_505,https://scrap.kakaocdn.net/dn/b4WdL6/hyZPJKF2Vp/GKlkCmdn71DcK1UwWWhi3K/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[데브코스 프론트] PickItBook 프로젝트 회고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;배포 주소 https://pick-it-book.vercel.app/ PickitBook pick-it-book.vercel.app 프로젝트 레포지토리 https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6 GitHub - prgrms-fe-devcourse/FES-5-Project-TEAM-6: 룰렛 기반 책 선택과 도전&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pick-it-book.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pick-it-book.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766650126619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PickitBook&quot; data-og-description=&quot;&quot; data-og-host=&quot;pick-it-book.vercel.app&quot; data-og-source-url=&quot;https://pick-it-book.vercel.app/&quot; data-og-url=&quot;https://pick-it-book.vercel.app/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://pick-it-book.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pick-it-book.vercel.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PickitBook&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pick-it-book.vercel.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- 4회차 (최종)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드와 협업한 프로젝트를 진행했고 Next로 구현하는 프로젝트였습니다. Next 학습 기간이 짧았는데 적용해야 했어서, 정말 어렵게 진행했어요.&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;- Re:Life&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 이용한 평행세계 시나리오 테스트, 시나리오 이미지 생성, 커뮤니티, 통계를 보여주는 서비스&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9ahGi/dJMcahJNXsM/hGJf2g14COMN17g1PK3kV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9ahGi/dJMcahJNXsM/hGJf2g14COMN17g1PK3kV0/img.png&quot; data-alt=&quot;Re:Life 프로젝트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9ahGi/dJMcahJNXsM/hGJf2g14COMN17g1PK3kV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9ahGi%2FdJMcahJNXsM%2FhGJf2g14COMN17g1PK3kV0%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;1109&quot; height=&quot;566&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Re:Life 프로젝트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yun-engene.tistory.com/106&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766650930397&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Re:Life] 프로젝트 회고&quot; data-og-description=&quot;1. 프로젝트 개요  Re:Life&amp;quot;만약 그때 다른 선택을 했다면?&amp;quot;AI가 시뮬레이션하는 평행우주적 인생 시나리오 서비스  프로젝트 소개Re:Life는 사용자의 과거 인생 선택을 기반으로, AI가 &amp;quot;만약 그때&quot; data-og-host=&quot;yun-engene.tistory.com&quot; data-og-source-url=&quot;https://yun-engene.tistory.com/106&quot; data-og-url=&quot;https://yun-engene.tistory.com/106&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhV2j2/hyZP5HJIMQ/WYKwm3f8YQkSybKMCDY1rk/img.png?width=800&amp;amp;height=408&amp;amp;face=0_0_800_408,https://scrap.kakaocdn.net/dn/rrwTw/hyZPWjHg1K/P0jNJkycNyhyGfgxiiSaj0/img.png?width=800&amp;amp;height=408&amp;amp;face=0_0_800_408,https://scrap.kakaocdn.net/dn/6zKuX/hyZP58OX4u/6cBCM2VdUqapdTZKc4Rkp0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://yun-engene.tistory.com/106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yun-engene.tistory.com/106&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhV2j2/hyZP5HJIMQ/WYKwm3f8YQkSybKMCDY1rk/img.png?width=800&amp;amp;height=408&amp;amp;face=0_0_800_408,https://scrap.kakaocdn.net/dn/rrwTw/hyZPWjHg1K/P0jNJkycNyhyGfgxiiSaj0/img.png?width=800&amp;amp;height=408&amp;amp;face=0_0_800_408,https://scrap.kakaocdn.net/dn/6zKuX/hyZP58OX4u/6cBCM2VdUqapdTZKc4Rkp0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Re:Life] 프로젝트 회고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 프로젝트 개요  Re:Life&quot;만약 그때 다른 선택을 했다면?&quot;AI가 시뮬레이션하는 평행우주적 인생 시나리오 서비스  프로젝트 소개Re:Life는 사용자의 과거 인생 선택을 기반으로, AI가 &quot;만약 그때&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yun-engene.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시연영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=9T7L8-4rH9M&amp;amp;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=9T7L8-4rH9M&amp;amp;feature=youtu.be&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=9T7L8-4rH9M&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/fxbMm/hyZPRBWcEs/ZriTE6gvJ5207rjyHR7hnk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;백엔드 8회차, 프론트엔드 6회차 1팀 Re:Life 최종발표 영상&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/9T7L8-4rH9M&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 정말 많이 배웠고, 가치있는 경험도 많이 한 과정이었던 것 같습니다. 프로젝트를 많이 하고, 스터디를 진행하기를 원해 이런 조건이 맞아떨어지는 데브코스에서 학습했는데, 결과적으로는 열심히 할 수만 있다면 아주 추천입니다.&lt;/p&gt;</description>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/111</guid>
      <comments>https://yun-engene.tistory.com/111#entry111comment</comments>
      <pubDate>Thu, 25 Dec 2025 17:25:19 +0900</pubDate>
    </item>
    <item>
      <title>왜 Tanstack Query는 staleTime과 gcTime을 분리해 관리할까?</title>
      <link>https://yun-engene.tistory.com/110</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc0T2L/dJMcadAvLZ7/2UAWJXhWkDPuTqfGkMEwR1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc0T2L/dJMcadAvLZ7/2UAWJXhWkDPuTqfGkMEwR1/img.jpg&quot; data-alt=&quot;상한 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc0T2L/dJMcadAvLZ7/2UAWJXhWkDPuTqfGkMEwR1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc0T2L%2FdJMcadAvLZ7%2F2UAWJXhWkDPuTqfGkMEwR1%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;568&quot; height=&quot;568&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상한 데이터&lt;/figcaption&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;왜&amp;nbsp;Tanstack&amp;nbsp;Query는&amp;nbsp;staleTime과&amp;nbsp;gcTime을&amp;nbsp;분리해&amp;nbsp;관리할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tanstack Query는 데이터 재요청 시간인 staleTime의 기본값은 0분이고, gcTime의 기본값은 5분이다. 왜 이 둘을 분리해 관리할까?&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;Tanstack Query와 유사한 기능을 하는 라이브러리인 SWR은 라이브러리 이름이 정답을 그대로 서술하고 있다. SWR의 풀네임은 &lt;span style=&quot;color: #3a4954; text-align: start;&quot;&gt;Stale-While-Revalidate이다. 이 말인 즉, 검증하기 전에 메모리에 남아 있는 캐싱된 데이터를 보여 주고, 재검증이 완료된 데이터를 새로 보여준다는 의미이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #3a4954; text-align: start;&quot;&gt;어떻게 동작하는지&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: #3a4954;&quot;&gt;staleTime이 지났을 경우 Tanstack Query는 서버에 재요청하고 캐시를 갱신한다. 이때, gcTime이 남아있다면 메모리에 남아 있는 데이터를 빠르게 불러오고, 백그라운드에서 데이터를 검증한 뒤 UI를 갱신한다.&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 2.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdYVw/dJMcaajxc5i/Ro0msfKqWaxKPbeYSDkmXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdYVw/dJMcaajxc5i/Ro0msfKqWaxKPbeYSDkmXK/img.png&quot; data-alt=&quot;동작 설명 도식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdYVw/dJMcaajxc5i/Ro0msfKqWaxKPbeYSDkmXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdYVw%2FdJMcaajxc5i%2FRo0msfKqWaxKPbeYSDkmXK%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;1281&quot; height=&quot;557&quot; data-filename=&quot;Frame 2.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;동작 설명 도식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 방법으로 유저는 데이터 요청 중에도 캐싱된 이전 데이터를 빠르게 불러와 보고 있으므로, 마치 초고속 인터넷을 사용하는 것처럼 느끼도록 할 수 있다.&lt;/p&gt;</description>
      <category>React/Tanstack Query</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/110</guid>
      <comments>https://yun-engene.tistory.com/110#entry110comment</comments>
      <pubDate>Mon, 15 Dec 2025 18:37:18 +0900</pubDate>
    </item>
    <item>
      <title>[Frobot] CS 평가 면접 봇 웹페이지 제작기 ( 1 )</title>
      <link>https://yun-engene.tistory.com/109</link>
      <description>&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;그래서 GPT를 이용해 기술 면접 준비를 하고 있었는데, 평가는 좋지만 가장 불편한 지점은 내가 어느 정도 했는지 통계를 내거나, 어떤 질문이 잘못되었는지 잘 알기 어렵다는 점이다. ( 세션이 길어지면 오류 확률이 올라가니까. )&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;프로젝트를 시작할 때는 자신의 불편한 점부터 해소하는 것으로 시작하라는 이야기를 들은 적이 있다. 그래서 CS평가 면접 봇 웹페이지 프로젝트를 시작하기로 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기획서&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  FroBot [가제]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 면접 질문을 AI에게 제출해 채점받고, 채점 기록은 DB에 기록됩니다. 유저는 채점 결과와 제출한 답안, 점수 기록을 시각적으로 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&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;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;설정 메뉴&lt;/li&gt;
&lt;li&gt;통계 메뉴&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;유저가 제출한 정답과 평가 정보&lt;/li&gt;
&lt;li&gt;유저 이용 통계 ( 깃허브 잔디 형식 사용 streak 시스템, 분야별 채점 통계 등 )&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;문제 목록 공유&lt;/li&gt;
&lt;li&gt;문제 가져오기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜덤으로 문제를 AI가 선택하여 유저에게 표출&lt;/li&gt;
&lt;li&gt;유저가 문제를 선택하여 정답을 제출&lt;/li&gt;
&lt;li&gt;유저의 답변에 대해 꼬리질문&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;사전 정의된 프롬프트를 기준으로 답변을 채점&lt;/li&gt;
&lt;li&gt;유저의 답변에 대해 피드백과 채점 점수를 제시&lt;/li&gt;
&lt;li&gt;유저에게 점수 그래프를 제시&lt;/li&gt;
&lt;li&gt;유저의 답변과 AI 피드백을 서버에 저장&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;유저가 제출한 정답과 평가 정보 표출&lt;/li&gt;
&lt;li&gt;그래프를 통한 유저 점수 통계 시각화&lt;/li&gt;
&lt;li&gt;깃허브 잔디 형식 사용 streak 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  문제 정의&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI봇은 있지만 사용자가 부족하거나 잘 알고 있는 부분을 정돈된 형태로 파악하기 어려움&lt;/li&gt;
&lt;li&gt;기존 만들어진 AI 면접 봇 페이지는 서비스가 중단되거나 정보 저장을 지원하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt; &amp;zwj;♂️ 목표 사용자&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;CS지식 점검을 하고 싶은 개발자&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;사용자 질문 범위에서 무작위로 질문 1개를 출력한다.&lt;/li&gt;
&lt;li&gt;사용자가 질문을 보고, 답변을 입력해 제출한다.&lt;/li&gt;
&lt;li&gt;AI가 해당 답변을 받고, 질문의 목적에 적절한지 평가한다.&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;h4 data-ke-size=&quot;size20&quot;&gt; 유사 서비스 분석&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPT 면접왕&lt;/li&gt;
&lt;li&gt;Frontend Handbook&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✨ 차별점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 정돈된 상태로 저장되지 않는 ai봇과 달리, 답변 목록이 정돈된 상태로 제공된다.&lt;/li&gt;
&lt;li&gt;연속 달성, 잔디 시스템 등과 같이 사용자의 사용 통계를 시각적으로 제시할 수 있다.&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 data-ke-size=&quot;size26&quot;&gt;디자인 문서 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 디자이너도 아니고, 그저 figma 작업에 익숙할 뿐이니 최대한 깔끔하게 구성해보려고 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;page2.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kNVqV/dJMcadgbnUF/DPNYhwIInzDdLlup8TEJKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kNVqV/dJMcadgbnUF/DPNYhwIInzDdLlup8TEJKk/img.png&quot; data-alt=&quot;메인 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kNVqV/dJMcadgbnUF/DPNYhwIInzDdLlup8TEJKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkNVqV%2FdJMcadgbnUF%2FDPNYhwIInzDdLlup8TEJKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1024&quot; data-filename=&quot;page2.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메인 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;page.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbaLkq/dJMcaioiINs/KdQXx9RIWHPLI9KH1EJao0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbaLkq/dJMcaioiINs/KdQXx9RIWHPLI9KH1EJao0/img.png&quot; data-alt=&quot;설정메뉴 토글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbaLkq/dJMcaioiINs/KdQXx9RIWHPLI9KH1EJao0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbaLkq%2FdJMcaioiINs%2FKdQXx9RIWHPLI9KH1EJao0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1024&quot; data-filename=&quot;page.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설정메뉴 토글&lt;/figcaption&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;대시보드는 차트가 많아 직접 그릴 엄두가 안 나서 제미나이한테 기능을 설명하고 UI를 그려달라고 요청했는데, 이런 느낌이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 구현할 때는 왼쪽 사이드바를 빼고 다른 메뉴를 넣어야 할 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_43mdk643mdk643md.png&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;1504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr4Tu1/dJMcacn5SCf/l9SJ6QhrFFz6Ku87cE9R20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr4Tu1/dJMcacn5SCf/l9SJ6QhrFFz6Ku87cE9R20/img.png&quot; data-alt=&quot;Gemini에게 요청한 대시보드 ui 디자인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr4Tu1/dJMcacn5SCf/l9SJ6QhrFFz6Ku87cE9R20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr4Tu1%2FdJMcacn5SCf%2Fl9SJ6QhrFFz6Ku87cE9R20%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;2848&quot; height=&quot;1504&quot; data-filename=&quot;Gemini_Generated_Image_43mdk643mdk643md.png&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;1504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Gemini에게 요청한 대시보드 ui 디자인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자서 진행하다 보면 길을 자주 잃을 것 같아, 블로그에 기록할 겸 요구사항 정의, 디자인 문서, MVP 정의까지 기록하면서 진행해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함 가보자고.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;LetsGoComeOnGIF.gif&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxRoR/dJMcahppAMd/a3ZtQpaXRq3pxSKpVsVDP1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxRoR/dJMcahppAMd/a3ZtQpaXRq3pxSKpVsVDP1/img.gif&quot; data-alt=&quot;레츠고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxRoR/dJMcahppAMd/a3ZtQpaXRq3pxSKpVsVDP1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/CxRoR/dJMcahppAMd/a3ZtQpaXRq3pxSKpVsVDP1/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;200&quot; height=&quot;110&quot; data-filename=&quot;LetsGoComeOnGIF.gif&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레츠고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현 상태....&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;StopNoGIF.gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/etyIdG/dJMcahQCqnG/3phh2SqPkWKbGlFNXuUYJK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/etyIdG/dJMcahQCqnG/3phh2SqPkWKbGlFNXuUYJK/img.gif&quot; data-alt=&quot;무기한 중단&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/etyIdG/dJMcahQCqnG/3phh2SqPkWKbGlFNXuUYJK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/etyIdG/dJMcahQCqnG/3phh2SqPkWKbGlFNXuUYJK/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;426&quot; height=&quot;498&quot; data-filename=&quot;StopNoGIF.gif&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;무기한 중단&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취직한 곳이 Vue 기반이라, 기존에 작성하던 코드를 사용할 수 없게 되었습니다. 다른 프로젝트와 공부 진행중이에요&lt;/p&gt;</description>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/109</guid>
      <comments>https://yun-engene.tistory.com/109#entry109comment</comments>
      <pubDate>Wed, 10 Dec 2025 19:57:33 +0900</pubDate>
    </item>
    <item>
      <title>Pinterest같은 Masonry 레이아웃 만들기</title>
      <link>https://yun-engene.tistory.com/108</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBPFyq/dJMcacBAnF8/hkVg2stZYbhbGoKDuV2iB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBPFyq/dJMcacBAnF8/hkVg2stZYbhbGoKDuV2iB0/img.png&quot; data-alt=&quot;이 레이아웃을 적용한 것으로 가장 유명한 pinterest&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBPFyq/dJMcacBAnF8/hkVg2stZYbhbGoKDuV2iB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBPFyq%2FdJMcacBAnF8%2FhkVg2stZYbhbGoKDuV2iB0%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;1898&quot; height=&quot;908&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이 레이아웃을 적용한 것으로 가장 유명한 pinterest&lt;/figcaption&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;Masonry란?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kKTAk/dJMcabWX848/oRYqOTm7LIk0NmFdIxP0T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kKTAk/dJMcabWX848/oRYqOTm7LIk0NmFdIxP0T0/img.png&quot; data-alt=&quot;네이버 영어사전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kKTAk/dJMcabWX848/oRYqOTm7LIk0NmFdIxP0T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkKTAk%2FdJMcabWX848%2FoRYqOTm7LIk0NmFdIxP0T0%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;652&quot; height=&quot;224&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네이버 영어사전&lt;/figcaption&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;Masonry는 사전적으로는 벽돌 쌓기입니다. 아이템들을 높이가 다른 벽돌을 쌓는 것처럼 쌓아주는 레이아웃을 의미합니다.&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;flex, grid레이아웃은 줄 단위로 정렬하기 때문에 높낮이가 다른 아이템을 정렬할 때, 아래 공간이 남는 문제가 발생하게 됩니다. 이러한 문제를 해결하기 위해 Masonry 레이아웃이 등장했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b04jLZ/dJMcaiV211t/NgqA2ghUlNKD65D8GLQ5A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b04jLZ/dJMcaiV211t/NgqA2ghUlNKD65D8GLQ5A0/img.png&quot; data-alt=&quot;grid기반 높낮이가 다른 아이템&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b04jLZ/dJMcaiV211t/NgqA2ghUlNKD65D8GLQ5A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb04jLZ%2FdJMcaiV211t%2FNgqA2ghUlNKD65D8GLQ5A0%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;1024&quot; height=&quot;658&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;grid기반 높낮이가 다른 아이템&lt;/figcaption&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;비율을 계산해 이미지를 잘리지 않도록 고정 width에 고정하는 것은 어렵지 않지만, 결국 문제는 &quot;배치를 어떻게 할 것인가.&quot; 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1764642026239&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ...

  const aspectRatio = imageWidth / imageHeight;
  const height = width / aspectRatio;
  
  return (
    &amp;lt;div
      onClick={onClick}
      style={{ width: width, height: height }}
      className={tw(
        `bg-gray-100 rounded-md relative cursor-pointer group`,
        className
      )}
    &amp;gt;
    
    )
// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 아이디어와 함께 배치 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;구현 아이디어&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- flex 레이아웃에 flex-cols를 적용해 각 줄별 배치를 자연스럽게 구현하는 방법&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crGyRk/dJMcaiuZczA/hYKxJISsYC57oenQ7uIRbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crGyRk/dJMcaiuZczA/hYKxJISsYC57oenQ7uIRbk/img.png&quot; data-alt=&quot;출처: WIT 블로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crGyRk/dJMcaiuZczA/hYKxJISsYC57oenQ7uIRbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrGyRk%2FdJMcaiuZczA%2FhYKxJISsYC57oenQ7uIRbk%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;759&quot; height=&quot;525&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: WIT 블로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 각각의 줄이 독립된 상태로, 배치 방향에 따라 자연스럽게 직전 요소에 다음 아이템이 달라붙는다.가장 단순하게 구현할 수 있고, 순서를 보장하는 것도 어렵지 않으나, row 한 쪽에 아이템 높이가 높은 것들만 배치된다면 어색한 구조로 배치될 수 있다는 문제가 있습니다.&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;867&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csT1BU/dJMcagjFT9Q/qsiFfoZKm05Pm42WR0DdXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csT1BU/dJMcagjFT9Q/qsiFfoZKm05Pm42WR0DdXK/img.png&quot; data-alt=&quot;출처: WIT 블로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csT1BU/dJMcagjFT9Q/qsiFfoZKm05Pm42WR0DdXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsT1BU%2FdJMcagjFT9Q%2FqsiFfoZKm05Pm42WR0DdXK%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;742&quot; height=&quot;876&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: WIT 블로그&lt;/figcaption&gt;
&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 어떻게 구현할까&lt;/p&gt;
&lt;pre id=&quot;code_1764645141519&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function arrangeMasonry(items, columnCount, gap = 16) {
  const columnHeights = new Array(columnCount).fill(0);
  const columns = new Array(columnCount).fill(null).map(() =&amp;gt; []);
  
  items.forEach((item) =&amp;gt; {
    // 가장 낮은 컬럼 찾기
    const shortestColumnIndex = columnHeights.indexOf(
      Math.min(...columnHeights)
    );
    
    // 해당 컬럼에 아이템 추가
    columns[shortestColumnIndex].push(item);
    
    // 컬럼 높이 업데이트
    columnHeights[shortestColumnIndex] += item.height + gap;
  });
  
  return columns;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 배치를 연산할 때, 아이템 개수 기준이 아닌 높이 기준으로 연산합니다. 가장 높이가 낮은 열에 아이템을 배치함으로써 위와 같은 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- grid의 masonry속성 적용해 구현하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도 CSS grid 속성에 masonry 속성이 존재합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764644316740&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.grid {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  grid-template-rows: masonry;
}&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btsATn/dJMcagRvBvH/z7HBcpl6eC17vrjm9hxjC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btsATn/dJMcagRvBvH/z7HBcpl6eC17vrjm9hxjC1/img.png&quot; data-alt=&quot;크롬 환경, 정상적으로 동작하지 않는 것처럼 보인다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btsATn/dJMcagRvBvH/z7HBcpl6eC17vrjm9hxjC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtsATn%2FdJMcagRvBvH%2Fz7HBcpl6eC17vrjm9hxjC1%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;786&quot; height=&quot;708&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;크롬 환경, 정상적으로 동작하지 않는 것처럼 보인다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDN 문서에서도 예시 코드가 정상적으로 작동하지 않는 모습을 확인할 수 있습니다.&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;796&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwVLLN/dJMcabbBqII/tSjOKjUb0faRWorDhzuvMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwVLLN/dJMcabbBqII/tSjOKjUb0faRWorDhzuvMk/img.png&quot; data-alt=&quot;호환성 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwVLLN/dJMcabbBqII/tSjOKjUb0faRWorDhzuvMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwVLLN%2FdJMcabbBqII%2FtSjOKjUb0faRWorDhzuvMk%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;796&quot; height=&quot;575&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호환성 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 예상대로, 호환성이 나쁜 실험적 기능이라는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- absolute를 이용해 각각의 아이템의 배치를 지정하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 아이템을 absolute로 지정해 realative 컨테이너에 배치하는 방법도 있습니다. 이 경우는 각각의 아이템의 top, left 속성을 지정해 배치를 결정합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Grou32p 1.png&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhp9fX/dJMcahJFoEV/kCfKkGZvZs3Hva9P6z4HZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhp9fX/dJMcahJFoEV/kCfKkGZvZs3Hva9P6z4HZk/img.png&quot; data-alt=&quot;컨테이너 배치 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhp9fX/dJMcahJFoEV/kCfKkGZvZs3Hva9P6z4HZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhp9fX%2FdJMcahJFoEV%2FkCfKkGZvZs3Hva9P6z4HZk%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;1447&quot; height=&quot;797&quot; data-filename=&quot;Grou32p 1.png&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;797&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨테이너 배치 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 전체 컨테이너에서 row 컨테이너를 관리하지 않고, 각각의 아이템에 대해 배치해주는 방법입니다.&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;열로 나누어진 컨테이너를 관리하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transform X, Y로 리플로우를 유발하지 않고 배치할 수 있다&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;특히 장점이라고 할 수 있는 것은 transform을 이용한 방법으로 구현할 수 있기 때문에 반응형 작업으로 아이템의 위치가 변경되는 작업을 하더라도 &lt;b&gt;리플로우를 방지&lt;/b&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;Pinterest는 어떤 방법을 사용하고 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 처음으로 돌아가서, Pinterest와 같은 레이아웃을 구성하기로 했을 때, Pinterest가 어떻게 구현했는지 살펴보는 게 좋을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFfdbt/dJMcaaX4eBa/XcEQ2qbKVa8KUAb2x4keI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFfdbt/dJMcaaX4eBa/XcEQ2qbKVa8KUAb2x4keI1/img.png&quot; data-alt=&quot;Pinterest의 Container&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFfdbt/dJMcaaX4eBa/XcEQ2qbKVa8KUAb2x4keI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFfdbt%2FdJMcaaX4eBa%2FXcEQ2qbKVa8KUAb2x4keI1%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;1826&quot; height=&quot;900&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pinterest의 Container&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinterest의 아이템 컨테이너는 relative로 구성되어 있고, 하위의 아이템들은 flat하게 absolute 아이템들로 이루어진 것을 확인할 수 있습니다.&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;459&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oc6rP/dJMcadtF6jn/e8ATvts8MABKhwuIWm8yC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oc6rP/dJMcadtF6jn/e8ATvts8MABKhwuIWm8yC0/img.png&quot; data-alt=&quot;Item의 속성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oc6rP/dJMcadtF6jn/e8ATvts8MABKhwuIWm8yC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foc6rP%2FdJMcadtF6jn%2Fe8ATvts8MABKhwuIWm8yC0%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;459&quot; height=&quot;69&quot; data-origin-width=&quot;459&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Item의 속성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 Item의 스타일 속성을 봤을 때, left, top을 0으로 고정하고 기준점으로 잡은 뒤, translate X, Y 속성을 통해 리플로우를 방지하는 방식으로 구현되어 있는 것을 확인할 수 있습니다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Pinterest는 무한 스크롤 로드와 가상 스크롤을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 아이템 컨테이너는 relative로 구성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 하위 아이템은 flat하게 absolute 아이템으로 구성되어 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 아이템의 속성은 left, top 0으로 고정, translate X, Y 속성 조정을 통해 리플로우를 방지하고 있다.&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;/h3&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;- react-masonry-css&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3c4858; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;과거부터 많이 사용되던 라이브러리이고, 지속적으로 개선되고 있는 라이브러리인 react-masonry-css는 CSS기반으로 동작하고 번들 사이즈도 가벼워 성능 부하가 가장 적은 선택이고, CSS기반이므로 TypeScript 프로젝트에도 적용하는 데 문제가 없습니다. 그런데 배치를 Pinterest 형태로 원한다면 무한스크롤을 위해 열 높이를 맞춰 주는 것이 필요한데, &lt;span style=&quot;background-color: #ffffff; color: #3c4858; text-align: start;&quot;&gt;react-masonry-css는 이를 지원하지 않는 것을 고려해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0hqtW/dJMcagxc3i2/IzDGjAzBmKcVzLXhm2F2N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0hqtW/dJMcagxc3i2/IzDGjAzBmKcVzLXhm2F2N0/img.png&quot; data-alt=&quot;출처: WIT 블로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0hqtW/dJMcagxc3i2/IzDGjAzBmKcVzLXhm2F2N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0hqtW%2FdJMcagxc3i2%2FIzDGjAzBmKcVzLXhm2F2N0%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;780&quot; height=&quot;921&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: WIT 블로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1I8PT/dJMcagKKpF7/Muk8iMeqHi2iO61PuWEq2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1I8PT/dJMcagKKpF7/Muk8iMeqHi2iO61PuWEq2k/img.png&quot; data-alt=&quot;NPM 통계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1I8PT/dJMcagKKpF7/Muk8iMeqHi2iO61PuWEq2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1I8PT%2FdJMcagKKpF7%2FMuk8iMeqHi2iO61PuWEq2k%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;379&quot; height=&quot;257&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NPM 통계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #3c4858; text-align: start;&quot;&gt;하지만 여전히 인기 많은 라이브러리이다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- react-responsive-masonry&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;masonry 배치를 지원하면서 flex 기반으로 동작하며 반응형도 지원하는 라이브러리입니다. 비교적 최근까지 업데이트되는 것으로 보이며, 번들 사이즈는 70kb정도로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;masonry&lt;span&gt; 배치 라이브러리중에서는 무거운 편입니다. 반응형도 정말 잘 되는 편이고, 궁금하다면 데모 페이지가 있으니 직접 들어가서 확인해보면 좋을 것 같습니다. npm 데모 이미지에서는 반응형으로 아이템 개수를 선택할 수 있는 것으로 보이는데, 실제 데모에서는 크기만 줄어듭니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cedricdelpoux.github.io/react-responsive-masonry/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;데모 페이지&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764658779192&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;react-responsive-masonry 2.3.0 Demo&quot; data-og-description=&quot;&quot; data-og-host=&quot;cedricdelpoux.github.io&quot; data-og-source-url=&quot;https://cedricdelpoux.github.io/react-responsive-masonry/&quot; data-og-url=&quot;https://cedricdelpoux.github.io/react-responsive-masonry/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://cedricdelpoux.github.io/react-responsive-masonry/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cedricdelpoux.github.io/react-responsive-masonry/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;react-responsive-masonry 2.3.0 Demo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cedricdelpoux.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript 지원도 비교적 최근인 8개월 전 업데이트로 추가된 것으로 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBwUnZ/dJMb99LCz2N/F8DqMKiyW5o6IB4hrhu6n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBwUnZ/dJMb99LCz2N/F8DqMKiyW5o6IB4hrhu6n1/img.png&quot; data-alt=&quot;타입스크립트도 지원하는 것으로 보인다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBwUnZ/dJMb99LCz2N/F8DqMKiyW5o6IB4hrhu6n1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBwUnZ%2FdJMb99LCz2N%2FF8DqMKiyW5o6IB4hrhu6n1%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;777&quot; height=&quot;624&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;타입스크립트도 지원하는 것으로 보인다.&lt;/figcaption&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;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[MDN] &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764643949867&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Masonry layout - CSS | MDN&quot; data-og-description=&quot;To create the most common masonry layout, your columns will be the grid axis and the rows the masonry axis, defined with grid-template-columns and grid-template-rows. The child elements of this container will now lay out item by item along the rows, as the&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Masonry_layout&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Masonry layout - CSS | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;To create the most common masonry layout, your columns will be the grid axis and the rows the masonry axis, defined with grid-template-columns and grid-template-rows. The child elements of this container will now lay out item by item along the rows, as the&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[WIT 블로그] &lt;a href=&quot;https://wit.nts-corp.com/2022/10/26/6595&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wit.nts-corp.com/2022/10/26/6595&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764643968663&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Masonry 레이아웃 구현하기 | WIT블로그&quot; data-og-description=&quot;Masonry layout is a layout method where one axis uses a typical strict grid layout, most often columns, and the other a masonry layout.On the masonry axis, rather than sticking to a strict grid with gaps being left after shorter items, the items in the fol&quot; data-og-host=&quot;wit.nts-corp.com&quot; data-og-source-url=&quot;https://wit.nts-corp.com/2022/10/26/6595&quot; data-og-url=&quot;https://wit.nts-corp.com/2022/10/26/6595&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wrV0F/hyZO2Xj1vd/dnPPcihpkfWftw0BEKi8a0/img.png?width=1024&amp;amp;height=708&amp;amp;face=0_0_1024_708,https://scrap.kakaocdn.net/dn/cHb0b8/hyZNC6WKjq/kKo85ACDVPy8ycbcbZeo8K/img.png?width=1024&amp;amp;height=704&amp;amp;face=0_0_1024_704,https://scrap.kakaocdn.net/dn/dx3xlx/hyZO3WfdbR/nJWvmdJMlCtREEVzxSEpj1/img.png?width=1024&amp;amp;height=703&amp;amp;face=0_0_1024_703&quot;&gt;&lt;a href=&quot;https://wit.nts-corp.com/2022/10/26/6595&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wit.nts-corp.com/2022/10/26/6595&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wrV0F/hyZO2Xj1vd/dnPPcihpkfWftw0BEKi8a0/img.png?width=1024&amp;amp;height=708&amp;amp;face=0_0_1024_708,https://scrap.kakaocdn.net/dn/cHb0b8/hyZNC6WKjq/kKo85ACDVPy8ycbcbZeo8K/img.png?width=1024&amp;amp;height=704&amp;amp;face=0_0_1024_704,https://scrap.kakaocdn.net/dn/dx3xlx/hyZO3WfdbR/nJWvmdJMlCtREEVzxSEpj1/img.png?width=1024&amp;amp;height=703&amp;amp;face=0_0_1024_703');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Masonry 레이아웃 구현하기 | WIT블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Masonry layout is a layout method where one axis uses a typical strict grid layout, most often columns, and the other a masonry layout.On the masonry axis, rather than sticking to a strict grid with gaps being left after shorter items, the items in the fol&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wit.nts-corp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/108</guid>
      <comments>https://yun-engene.tistory.com/108#entry108comment</comments>
      <pubDate>Tue, 2 Dec 2025 16:06:23 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 개발자 포트폴리오 사이트 만들기</title>
      <link>https://yun-engene.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;예전부터 개인 포트폴리오 사이트를 웹페이지로 만들고 싶은 욕심이 있었다. 그래서 세달 전에 레포 파고, 배포 하고, 도메인까지 연결해뒀었던 것이 있어 그걸 개선해 사용해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 달 전에 처음 레포를 파고, 배포할 때는 두근거리면서 시작했지만 프로젝트 너무 일정이 바빴고, 포트폴리오 결과물이 마음에 안 들었다. 때문에 추후에 시간이 남을 때 리뉴얼을 해보기로 계획했고, 지금에서야 리뉴얼 작업을 하기로 결정했다.&lt;/p&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2850&quot; data-origin-height=&quot;1442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5IcPM/dJMcaiaDmfO/Pxd2D2GURoKgucRSPB5XYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5IcPM/dJMcaiaDmfO/Pxd2D2GURoKgucRSPB5XYK/img.png&quot; data-alt=&quot;히어로 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5IcPM/dJMcaiaDmfO/Pxd2D2GURoKgucRSPB5XYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5IcPM%2FdJMcaiaDmfO%2FPxd2D2GURoKgucRSPB5XYK%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;2850&quot; height=&quot;1442&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2850&quot; data-origin-height=&quot;1442&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;히어로 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;1453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwRTxw/dJMcabo2gih/MYzDRCoevyfvTYmwrWy4G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwRTxw/dJMcabo2gih/MYzDRCoevyfvTYmwrWy4G0/img.png&quot; data-alt=&quot;스킬, 프로젝트 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwRTxw/dJMcabo2gih/MYzDRCoevyfvTYmwrWy4G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwRTxw%2FdJMcabo2gih%2FMYzDRCoevyfvTYmwrWy4G0%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;2344&quot; height=&quot;1453&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;1453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스킬, 프로젝트 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로 개선해야겠다고 느꼈던 부분&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;HodaverseGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/debgOD/dJMcafrrqm7/kStnyePS6Qnk0FIJPju5X1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/debgOD/dJMcafrrqm7/kStnyePS6Qnk0FIJPju5X1/img.gif&quot; data-alt=&quot;뭘 해야 할지 먼저 정했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/debgOD/dJMcafrrqm7/kStnyePS6Qnk0FIJPju5X1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/debgOD/dJMcafrrqm7/kStnyePS6Qnk0FIJPju5X1/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;419&quot; height=&quot;311&quot; data-filename=&quot;HodaverseGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뭘 해야 할지 먼저 정했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 히어로 섹션의 높이가 애매하다. 반응형으로 100vh로 화면을 꽉 채우는 설계로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. About 섹션에 내 소개가 부족한 것 같아 좌우명이나 개발철학과 같은 것들 짧게 써보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 인터렉티브한 요소가 너무 적어 사이트가 심심함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 오렌지 + 화이트가 당시에는 예쁘다고 생각해 골랐는데 색상 대비가 나쁜 것 같아 수정해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 아무리 썸네일이더라도 프로젝트를 어느 정도 설명해야 하는데 설명 섹션이 너무 작은 것 같음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 프로젝트 디테일 페이지가 없는데, 프로젝트 디테일을 추가해야 함.&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;/h3&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;735&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qbePD/dJMcaiho3AZ/rpyt1vVCawz2arMjlUjIR1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qbePD/dJMcaiho3AZ/rpyt1vVCawz2arMjlUjIR1/img.jpg&quot; data-alt=&quot;음 그래.. 쓸 수 있는 건 쓰고 엎자.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qbePD/dJMcaiho3AZ/rpyt1vVCawz2arMjlUjIR1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqbePD%2FdJMcaiho3AZ%2Frpyt1vVCawz2arMjlUjIR1%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;601&quot; height=&quot;481&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;음 그래.. 쓸 수 있는 건 쓰고 엎자.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트 아키텍쳐와 디자인을 엎고 새로 구현하기로 결심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인부터 새로 짜자&lt;/h3&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;애초에 디자인 문서를 안 짜고 작업해서 이런 상황이 발생한 것 같다. 디자인을 잘 하지는 않지만 Figma를 이용해 디자인 문서를 작성해줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BQwSy/dJMcacuLT9O/ljbaK9BjFOJLh3lXhRIH7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BQwSy/dJMcacuLT9O/ljbaK9BjFOJLh3lXhRIH7k/img.png&quot; data-alt=&quot;검은색 단색 + 흰색 조합으로 구성했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BQwSy/dJMcacuLT9O/ljbaK9BjFOJLh3lXhRIH7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBQwSy%2FdJMcacuLT9O%2FljbaK9BjFOJLh3lXhRIH7k%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;683&quot; height=&quot;756&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검은색 단색 + 흰색 조합으로 구성했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(전체를 검은 색으로 구성하니 너무 단순해 보이는 면이 있어 구현하면서 흰색으로 개선한 섹션도 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 구조도 개선하자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아예 번들링도 TurboPack으로 변경하고 Next 기반 프로젝트로 변경할지도 고민했으나 이미 프로젝트에 배포 설정, 도메인 설정 다 되어 있는 상태인데다가, 프로젝트 크기도 별로 크지 않아 Next로 마이그레이션하기는 품이 너무 커질 것 같아 폴더 구조만 개선했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ek5DIz/dJMcahpjMcX/gXvMfAPV0Gn4Ti9YKt5vL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ek5DIz/dJMcahpjMcX/gXvMfAPV0Gn4Ti9YKt5vL0/img.png&quot; data-alt=&quot;domain 기반으로 변경했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ek5DIz/dJMcahpjMcX/gXvMfAPV0Gn4Ti9YKt5vL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fek5DIz%2FdJMcahpjMcX%2FgXvMfAPV0Gn4Ti9YKt5vL0%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;254&quot; height=&quot;723&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;domain 기반으로 변경했다.&lt;/figcaption&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React - 익숙하고 편하니 선택. constants 기반 정적 데이터 관리하는 데 선언적으로 아이템 관리하기도 쉽고, 에코시스템도 잘 되어 있어 js 바닐라로 하는 것보다 훨씬 나았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TailwindCSS - 모듈 CSS로 번들링해 관리하는 것보다 React 코드 내에서 관리하는 것이 명확하고 가독성도 좋아 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Framer Motion - Framer를 계속 써보고 싶었는데, 이상하게 연이 없었다. GSAP보다 선언적인 방식으로 애니메이션 구현을 위해 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-router-dom&amp;nbsp;- 라우터를 직접 구현하기보다 간편하게 관리할 수 있는 react-router-dom을 이용해 라우팅 관리&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 작성했던 부분에 공통으로 이용할 수 있는 것 같은 부분은 그대로 가져다 쓰고, 디자인 자체를 변경해야 했다. 기존 삽질하던게 2일 정도, 코드 작성하고 디테일 페이지 작업까지 마치는 데는 5일 정도 걸렸다. 미리 이력서를 작성해둬서, 소스는 그쪽에서 가져오면 돼서 얼마 안 걸렸다.&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;제일 어려웠던 부분은 이벤트가 없고 백엔드 서버가 없는 정적인 코드로 작성하려고 constants에 모든 프로젝트 정보를 담아 아이템으로 뿌려줬었는데, 이 데이터 관리하는 게 좀 피곤했다.&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;constant.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764048615169&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const PROJECTS: ProjectDetail[] = [
  {
    thumbnail: relifeThumb,
    title: &quot;Re:Life&quot;,
    desc: &quot;AI 평행우주 시나리오 조회 플랫폼&quot;,
    period: &quot;2025-09-15 ~ 2025-10-16 (4주)&quot;,
    teammate: &quot;프론트엔드팀 3명-백엔드팀 5명 (팀장)&quot;,
    background:
      '인생의 중요한 선택에서 &quot;만약 그때 다른 선택을 했다면?&quot; 이라는 궁금증에서 출발한 프로젝트입니다. 사용자의 성향과 그 시점의 선택에 따라 평행우주 인생 시나리오를 시뮬레이션하는 서비스입니다.',
    architecture: relifeArchitecture,
    teamFlag: [
      &quot;백엔드팀과 원팀으로 협업해 서로 윈윈하기&quot;,
      &quot;결정은 단순히 이야기로 끝나지 않고, 문서를 남겨 명확히 정의하기&quot;,
      &quot;예의는 지키되, 의견 제시를 망설이지 않기&quot;,
    ],
    skills: [
      &quot;Typescript&quot;,
      &quot;React&quot;,
      &quot;Next.js&quot;,
      &quot;TailwindCSS&quot;,
      &quot;GSAP&quot;,
      &quot;react-hook-form&quot;,
      &quot;zod&quot;,
      &quot;axios&quot;,
      &quot;Tanstack Query&quot;,
    ],
    skillReason: [
      {
        skill: &quot;Typescript&quot;,
        reason:
          &quot;코드 작성 단계에서 타입을 걸러줘 안정적인 코드 작성이 가능한 장점이 있고, 사용하는 라이브러리들도 모두 Type에 강점이 있는 라이브러리였기 때문에 연계성이 좋다고 판단했다.&quot;,
      },
      {
        skill: &quot;React&quot;,
        reason:
          &quot;팀원 모두 팀 활동을 거치면서 컴포넌트 기반의 작업의 재사용성과 작업 분리에 대해서 잘 이해하고 있었기 때문에 좋은 선택지라고 생각했다.&quot;,
      },
      {
        skill: &quot;Next.js&quot;,
        reason:
          &quot;백엔드에서 많은 데이터를 받아오고 많은 페이지를 상정했기 때문에 서버 컴포넌트, 최적화 기능, 라우팅 구조, SEO에서 큰 장점이 있다고 생각했다. 때문에 Next를 사용하기로 결정하게 되었다.&quot;,
      },
      {
        skill: &quot;TailwindCSS&quot;,
        reason:
          &quot;CSS 프레임워크로 빠른 스타일링과 일관된 디자인 시스템 구축을 위해 선택했다.&quot;,
      },
      {
        skill: &quot;GSAP&quot;,
        reason:
          &quot;팀원들이 익숙한 라이브러리라 온보딩이 쉽다고 판단했고, 라이브러리의 다양한 기능이 무료로 풀렸기 때문에 강력한 기능들을 사용해보고 싶은 욕심이 있었다. 스크롤 제어에서의 정밀함이 장점이라고 생각해 선택했다.&quot;,
      },
      {
        skill: &quot;react-hook-form&quot;,
        reason:
          &quot;유저 입력이 많은 서비스였기 때문에 유효성 검증에 react-hook-form을 이용하면 유저 입력 검증을 쉽게 구현할 수 있다고 생각해 선택했다.&quot;,
      },
      {
        skill: &quot;Zod&quot;,
        reason:
          &quot;백엔드 협업에서 데이터 구조는 빈번하게 있을 수 있다고 판단해 런타임 타입 검증이 필요하다고 생각해 Zod를 이용하려고 했다.&quot;,
      },
      {
        skill: &quot;axios&quot;,
        reason:
          &quot;간단하게 사용할 수 있는 fetch, 공용 인스턴스 구현 기능이 백엔드 협업에서 헤더 설정과 인터셉터를 이용한 전처리에 큰 장점이 있다고 느껴 선택했다.&quot;,
      },
      {
        skill: &quot;Tanstack Query&quot;,
        reason:
          &quot;클라이언트에서의 캐시 관리에 큰 장점이 있는 데 더해 RSC에서도 dehydrate 패턴으로 데이터를 받아오는 데 큰 장점이 있다고 느껴 선택했다.&quot;,
      },
    ],
    charged: [
      {
        title: &quot;프론트엔드 아키텍처 설계 및 팀 리드&quot;,
        desc: &quot;Feature-based 디렉터리 구조로 코드 응집도 향상, 팀원 온보딩을 위한 기술 문서 작성으로 코드 작성 일관성 개선&quot;,
      },
      {
        title: &quot;Next.js App Router + Server Action 기반 인증 시스템 구현&quot;,
        desc: &quot;로그인 / 회원가입 로직을 숨기기 위해 Server Action으로 처리, Next.js Middleware를 이용한 로그인 상태별 라우팅 그룹 접근 제어&quot;,
      },
      {
        title: &quot;CSRF 처리를 위한 서버, 클라이언트 공용 fetcher 구현&quot;,
        desc: &quot;헤더 설정과 CSRF 인증 실패 재인증 로직을 거치는 공용 fetcher 구현으로 팀 로직 일관 처리&quot;,
      },
      {
        title: &quot;Zod + React-Hook-Form 타입 안전 폼 시스템&quot;,
        desc: &quot;스키마 기반 런타임 유효성 검증 + ts 타입 추론, setValue / control를 이용한 중첩 폼 상태 관리&quot;,
      },
      {
        title: &quot;SSR + Tanstack Query 캐시&quot;,
        desc: &quot;dehydrate 패턴으로 서버/클라이언트 캐시 일관성 유지&quot;,
      },
      {
        title: &quot;커뮤니티 페이지 구현&quot;,
        desc: &quot;게시글 CRUD, 좋아요 구현, 댓글 CRUD, 좋아요 구현, 디테일 페이지 동적 메타데이터 SEO 설정, SSR-CSR 구분 처리&quot;,
      },
    ],
    achievements: [
      &quot;DDD 개념에서 파생된 Feature-based 디렉터리 구조로 코드 탐색 시간 20% 단축&quot;,
      &quot;팀원 온보딩 지원으로 프로젝트의 모든 클라이언트 요청을 Tanstack Query로 일관성있게 처리&quot;,
      &quot;헤더 설정과 CSRF 인증 실패 재인증 로직을 거치는 공용 fetcher 구현으로 팀 코드 구현 복잡도 감소&quot;,
      &quot;커뮤니티 페이지 메타데이터 설정으로 SEO 평가 점수 60점에서 100점으로 상승&quot;,
      &quot;커뮤니티 페이지 캐싱 적용으로 요청수 40%이상 감소&quot;,
    ],
    deployStatus: &quot;백엔드 배포 중단&quot;,
    deployUrl: &quot;https://relife.kr&quot;,
    youtubeUrl: &quot;https://youtu.be/9T7L8-4rH9M&quot;,
    tistoryUrl: &quot;https://yun-engene.tistory.com/106&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;이런 형식으로 모든 정적 데이터를 관리했다.&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;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;히어로 섹션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;히어로.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwtoll/dJMcadUHi2t/CNjbQFRtikqfljNGnLJ8F1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwtoll/dJMcadUHi2t/CNjbQFRtikqfljNGnLJ8F1/img.gif&quot; data-alt=&quot;히어로 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwtoll/dJMcadUHi2t/CNjbQFRtikqfljNGnLJ8F1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cwtoll/dJMcadUHi2t/CNjbQFRtikqfljNGnLJ8F1/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;1000&quot; height=&quot;563&quot; data-filename=&quot;히어로.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;히어로 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 검은색 단색이었다가, 그라데이션으로 개선하고도 페이지가 좀 심심한가 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 포트폴리오 사이트의 히어로 페이지는 대부분 애니메이션이 있었던 생각이 나서 기존 작업한 내용을 편집해 그라데이션 scale, translate로 전환되는 애니메이션과 영상 재생을 넣어줬다. 영상 재생 때문에 페이지가 조금 무거워지긴 했는데 작업 내용을 바로 보여줄 수 있는 게 마음에 들었다.&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;최적화를 위해 영상은 480p로 일괄 처리하고, 배속을 걸고 압축해 영상 시간을 최대한 줄인 뒤에 lazy loading으로 최적화하도록 했다. 백그라운드 위에 상위에 블러를 배치하고 나니 화질 낮은 게 티가 덜 나고, 영상의 동적인 느낌은 그대로 살아 블러 처리했다.&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;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b42kIE/dJMb99Lz2C2/lC7x1L73GyyfVMIqM1sR70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b42kIE/dJMb99Lz2C2/lC7x1L73GyyfVMIqM1sR70/img.png&quot; data-alt=&quot;자기소개 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b42kIE/dJMb99Lz2C2/lC7x1L73GyyfVMIqM1sR70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb42kIE%2FdJMb99Lz2C2%2FlC7x1L73GyyfVMIqM1sR70%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;1900&quot; height=&quot;723&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1900&quot; data-origin-height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자기소개 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기소개 섹션은 소개 단 3개가 애니메이션으로 들어오도록 구현했다. 빈공간이 있긴 한데, 나쁘지 않은 것 같아서 남겨 뒀다.&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;지금 다시 보니까 반응형 작업한다고 grid를 조금 만졌더니 좌우 여백이 삭제된 것 같은데, 수정해야겠다... 자식 요소인데 왜 padding이 빠진 거지?&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;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;피어리뷰.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uonfU/dJMcag40HaK/7dH37Duje6NptmGbtd3KD1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uonfU/dJMcag40HaK/7dH37Duje6NptmGbtd3KD1/img.gif&quot; data-alt=&quot;피어리뷰 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uonfU/dJMcag40HaK/7dH37Duje6NptmGbtd3KD1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/uonfU/dJMcag40HaK/7dH37Duje6NptmGbtd3KD1/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;1000&quot; height=&quot;563&quot; data-filename=&quot;피어리뷰.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;피어리뷰 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트캠프 진행하며 받았던 피어리뷰를 아래로 천천히 넘어가는 영화 크레딧처럼 구현했다. 마우스가 올라가면 멈추고, 드래그해서 이전과 이후 데이터를 볼 수 있도록 하고 반복되도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;포트폴리오에 리더십 관련한 내용이 들어갔으니 설득력 있는 내용을 넣기 위해 피어리뷰 원본 내용을 넣고 Modal로 열어서 볼 수 있도록 구현했다.&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;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스킬.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DbuUI/dJMcagcSi3m/Z2xUYY9pEGKesiwi281bnK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DbuUI/dJMcagcSi3m/Z2xUYY9pEGKesiwi281bnK/img.gif&quot; data-alt=&quot;스킬 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DbuUI/dJMcagcSi3m/Z2xUYY9pEGKesiwi281bnK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/DbuUI/dJMcagcSi3m/Z2xUYY9pEGKesiwi281bnK/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;1000&quot; height=&quot;563&quot; data-filename=&quot;스킬.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스킬 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 포트폴리오의 스킬 섹션을 가져왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람들에게 리뷰 받았을 때, 필요 없는 것이 너무 많아 보여서 어느정도 쓸 줄 아는지 보였으면 좋겠다고 이야기를 들었다. 그런데 숙련도를 %로 표시하기에는 고민이 너무 많아서. '렉시컬 환경, 실행 컨텍스트 환경을 이해했다'라고 하더라도 JS를 80%로 적는 게 맞는건가? 싶어서 어느 정도 개념을 알고 있는지 텍스트로 덧붙였다.&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;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;프로젝트.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rk8nD/dJMcahCQlOG/OkjIIksZBBURQIoodKsxc0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rk8nD/dJMcahCQlOG/OkjIIksZBBURQIoodKsxc0/img.gif&quot; data-alt=&quot;프로젝트 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rk8nD/dJMcahCQlOG/OkjIIksZBBURQIoodKsxc0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rk8nD/dJMcahCQlOG/OkjIIksZBBURQIoodKsxc0/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;1000&quot; height=&quot;563&quot; data-filename=&quot;프로젝트.gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 섹션은 기본적인 정보에서 hover했을 때 프로젝트 개요와 기술 스택, 내가 구현한 핵심 내용을 볼 수 있도록 애니메이션으로 구현했고, 클릭하면 Detail 페이지로 갈 수 있도록 라우팅 설정을 해 뒀다.&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;h4 data-ke-size=&quot;size20&quot;&gt;컨텍트 섹션&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQVzwr/dJMcaaKucVJ/L2iZAH3h1FJaCICJh43lFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQVzwr/dJMcaaKucVJ/L2iZAH3h1FJaCICJh43lFk/img.png&quot; data-alt=&quot;컨텍트 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQVzwr/dJMcaaKucVJ/L2iZAH3h1FJaCICJh43lFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQVzwr%2FdJMcaaKucVJ%2FL2iZAH3h1FJaCICJh43lFk%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;1904&quot; height=&quot;402&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨텍트 섹션&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리로 컨텍트 섹션에 간단한 메세지와 버튼으로 깃허브, 티스토리, 이메일로 연결될 수 있도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;반응형 작업&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3j3hU/dJMcac9kDAU/HGK4eWRACxzP7vjDEpZRBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3j3hU/dJMcac9kDAU/HGK4eWRACxzP7vjDEpZRBk/img.png&quot; data-alt=&quot;반응형 화면 1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3j3hU/dJMcac9kDAU/HGK4eWRACxzP7vjDEpZRBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3j3hU%2FdJMcac9kDAU%2FHGK4eWRACxzP7vjDEpZRBk%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;604&quot; height=&quot;904&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;반응형 화면 1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJrz8W/dJMcafE1WnC/BRDA53KgFyQJ84cjJEdRw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJrz8W/dJMcafE1WnC/BRDA53KgFyQJ84cjJEdRw1/img.png&quot; data-alt=&quot;반응형 화면 2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJrz8W/dJMcafE1WnC/BRDA53KgFyQJ84cjJEdRw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJrz8W%2FdJMcafE1WnC%2FBRDA53KgFyQJ84cjJEdRw1%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;605&quot; height=&quot;904&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;반응형 화면 2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반응형 화면도 당연히 구현했다. 짧은 기간 목표를 잡긴 했어도, 할 줄 아는 것들은 담아야 한다고 생각해 반응형 작업도 완료해뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽질하고 설계부터 구현까지 9일정도 걸렸던 것 같다. 프로젝트 상세정보에 트러블슈팅도 담으면 참 좋을 것 같은데. 트러블슈팅을 넣으면 분량이 너무 길어져서 안 넣고 있는데, 블로그 글 작성하면서 다시 열어본김에. 하루이틀 작업 해서 작업해서 넣어야겠다.&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;포트폴리오를 웹페이지로 만들고 PPT는 따로 안 만드려고 했는데, 둘다 있는 게 좋다고 해서 PPT도 만들었다. (한번 할 거 두번했다.) PPT도 기회가 되면 포스팅해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포폴 페이지를 공유할까 생각했는데, 내 얼굴이 있어서... 링크를 올리지는 못하겠다. 부끄럽다.&lt;/p&gt;</description>
      <category>Project/개발자 포트폴리오 웹사이트</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/107</guid>
      <comments>https://yun-engene.tistory.com/107#entry107comment</comments>
      <pubDate>Tue, 25 Nov 2025 15:46:22 +0900</pubDate>
    </item>
    <item>
      <title>[Next] Next에서 Tanstack Query 사용하기</title>
      <link>https://yun-engene.tistory.com/105</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 왜 Tanstack을 많이 쓰나요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tanstack Query&lt;/code&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;code&gt;staleTime&lt;/code&gt;과 &lt;code&gt;gcTime&lt;/code&gt;을 이용한 &lt;b&gt;캐싱, 최신화 관리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refetchOnWindowFocus&lt;/code&gt;와 같은 &lt;b&gt;브라우저 레벨의 유저 동작에서의 제어 옵션&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt; 등을 이용한 &lt;b&gt;return 데이터 가공&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;isPending&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt; 등과 같은 &lt;b&gt;다양한 상황에서 제공되는 상태&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultSetting&lt;/code&gt;으로 &lt;b&gt;전역적인 쿼리 클라이언트 동작 제어&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onMutate&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt; Callback 등으로 이벤트 발생 타이밍마다 작동하는 &lt;b&gt;콜백 제어로 낙관적 업데이트 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queryClient&lt;/code&gt; 직접 제어를 통한 쿼리 무효화, &lt;code&gt;refetching&lt;/code&gt; 등 &lt;b&gt;직접 제어 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;무한 스크롤 제어에 특히 강력한 &lt;code&gt;InfinityQuery&lt;/code&gt;과 같은 &lt;b&gt;특정 쿼리 동작에 특화된 함수&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 정리한 기능만 정리한 내용이고, 이 외에도 수많은 옵션과 기능으로 비동기 요청을 처리할 수 있는 것은 물론 &lt;code&gt;React&lt;/code&gt;, &lt;code&gt;Vue&lt;/code&gt;, &lt;code&gt;Solid&lt;/code&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tanstack의 기능 중에 가장 강력한 부분이 캐시(cache)이기 때문에 이 개념은 가져가시는 게 좋습니다. &lt;code&gt;staleTime&lt;/code&gt;과 &lt;code&gt;gcTime&lt;/code&gt; 으로 신선도를 유지하는 시간을 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TanStack Query는 캐시한 데이터를 신선(Fresh)하거나 상한(Stale) 상태로 구분해 관리합니다.&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;일종의 데이터 유통기한 정도로 생각하면 이해하기 쉽습니다.&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;코드 예시로 보는 Tanstack의 선언적 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 비동기 처리와 같은 사이드이펙트 처리를 할 때는 &lt;code&gt;useEffect&lt;/code&gt;를 사용해 처리해야 하는데, &lt;code&gt;Tanstack&lt;/code&gt;을 이용하면 선언적으로 관리하기 편리해집니다. &lt;code&gt;async/await&lt;/code&gt; 구문을 사용하는 것처럼요. 실제 코드 구문으로 예시를 보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tanstack을 사용하지 않는 비동기 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const Someting(id) {
    const [data, setData] = useState&amp;lt;SomeType[]&amp;gt;([]);

    useEffect(() =&amp;gt; {
        async function getData() {
            try{
                const res = await fetch(`/api/library?isbn13=${isbn13}`);
                const data = await res.json();
                setData(data)
            }catch{
                console.log(&quot;Someting: getData 실행 오류&quot;);
            }
        }
    }, [])

    if(data.length &amp;lt;= 0)
        return (&amp;lt;div&amp;gt;데이터가 없습니다.&amp;lt;/div&amp;gt;)

    return (
        &amp;lt;ul&amp;gt;
            {data.map(() =&amp;gt; (...))}
        &amp;lt;/ul&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state 기반으로 처리하는 익숙한 형태죠? 비동기 처리는 사이드이펙트로 처리하기 때문에 useEffect 내부에 async funtion을 선언해 처리하는 것이 일반적입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tanstack을 사용하는 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const Someting(id) {
    const {data, isLoading, isError} = useGetData()

    if(isLoading) return (&amp;lt;div&amp;gt;로딩 중입니다.&amp;lt;/div&amp;gt;)

    if(isError) return (&amp;lt;div&amp;gt;오류가 발생했습니다.&amp;lt;/div&amp;gt;)

    return (
        &amp;lt;ul&amp;gt;
            {data.map(() =&amp;gt; (...))}
        &amp;lt;/ul&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tanstack&lt;/code&gt;은 모든 Query를 &lt;code&gt;hook&lt;/code&gt;으로 분리해 관리합니다. 때문에 사용하지 않았을 때와 비교했을 때 컴포넌트를 훨씬 선언적이고 절차 기반인 것처럼 관리할 수 있고, 상태에 따른 분기를 간단하게 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 &lt;code&gt;fetch&lt;/code&gt;문을 &lt;code&gt;queryFn&lt;/code&gt;에 작성해야 하긴 하지만, 로직을 분리함으로써 재사용성을 높이고 비즈니스 로직이 하나의 책임만을 가지도록 설계하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 언제 Next의 &lt;code&gt;fetch&lt;/code&gt; 를 쓰고 언제 &lt;code&gt;TanStack Query&lt;/code&gt;를 써야 되는가.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSC(React Server Components)로 작성해야 할 때와 RCC(React Client Components)로 작성해야 할 때는 구분해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적, 갱신이 자주 일어나지 않을 데이터(RSC) : &lt;code&gt;fetch()&lt;/code&gt; + Next 캐시 / ISR 활용 필요 시 클라이언트에서 &lt;code&gt;useQuery&lt;/code&gt;는 disabled + initialData만 사용.&lt;/li&gt;
&lt;li&gt;상호작용 필요(event) / 자주 갱신(RCC)-채팅창 : 클라이언트의 &lt;code&gt;useQuery&lt;/code&gt; 또는 &lt;code&gt;useSuspenseQuery&lt;/code&gt; 중심으로 낙관적 업데이트, 무효화, &lt;code&gt;Infinite Query&lt;/code&gt;등 &lt;code&gt;TQ&lt;/code&gt;의 장점을 활용&lt;/li&gt;
&lt;li&gt;혼합(Hybrid) : 서버에서 1차 패치 &amp;rarr; 클라이언트에서 &lt;code&gt;HydrationBoundary&lt;/code&gt;로 이어받아 최신화&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부연 설명&lt;/b&gt; : 엄밀히 따지자면 혼합(Hybrid) 방식은 &lt;b&gt;클라이언트 컴포넌트&lt;/b&gt;입니다. 서버 레벨에서 사전 로드된 데이터를 넘겨줄 뿐이지 번들에 포함됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기본 설정&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Query Provider 선언&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트 옵션이 정의된 Query Provider로부터 쿼리 클라이언트를 받아 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// TanstackProvider.tsx
import { QueryClient, QueryClientProvider } from &quot;@tanstack/react-query&quot;;
import { useState } from &quot;react&quot;;
import { ReactQueryDevtools } from &quot;@tanstack/react-query-devtools&quot;;

function TanstackProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =&amp;gt;
      new QueryClient({
        defaultOptions: {
          queries: {
            refetchOnWindowFocus: false,
            staleTime: 1000 * 60 * 5,
            gcTime: 1000 * 60 * 5,
            retry: 2,
            refetchIntervalInBackground: false,
            retryDelay: 1000,
          },
          mutations: {
            retry: 2,
            retryDelay: 1000,
          }
        },
      })
  );

  return (
    &amp;lt;&amp;gt;
      &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
        {children}
        &amp;lt;ReactQueryDevtools client={queryClient} /&amp;gt;
      &amp;lt;/QueryClientProvider&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

export default TanstackProvider;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Root Layout 컨텐츠 요소의 부모 요소로 작성&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Root 레이아웃에서 정의하면 &lt;b&gt;외부 파일 @/feature/api/useDataFetching.ts 에서 참조하더라도&lt;/b&gt; &lt;code&gt;Query Client&lt;/code&gt; 에서 정의된 기본 설정을 따릅니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// app/layout.tsx

// ... 생략

export default function RootLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;html lang=&quot;ko&quot;&amp;gt;
        &amp;lt;body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        &amp;gt;
         &amp;lt;TanstackProvider&amp;gt;
          {children}
             &amp;lt;/TanstackProvider&amp;gt;
        &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 키 팩토리 ( 메서드명은 규칙에 따라 통합 )&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// queryKeyFactory.ts
export const queryKeys = {
    missions: {
        all: () =&amp;gt; ['missions'] as const,
        byId: (id: number) =&amp;gt; ['missions', id] as const,
        search: (q: string) =&amp;gt; ['missions', 'search', q] as const,
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 팩토리가 필요한 이유는 &lt;code&gt;invalidateQuerys&lt;/code&gt;(관련 쿼리키로 조회한 내용의 stale을 초기화)등의 명령어를 사용할 때 해당 쿼리키의 구조를 정확하게 알지 않아도 사용 가능하도록 하기 위해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const qc = useQueryClient(); // 쿼리 클라이언트를 가져옴
const uid = useAuthStore((s) =&amp;gt; s.user?.id) ? &quot;&quot; // 스토어로부터 유저 정보를 가져옴

const {mutate: reviewMutate} = useSetReview({onMutate:() =&amp;gt; {
    qc.invalidQuerys(queryKeys.missions.byId(userId))
}}) &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드 스니펫 예시에서는 리뷰를 작성했을 때 리뷰를 갱신하는 게 아닌 미션 목록을 갱신하고 있다. 하지만 mission 목록의 &lt;code&gt;queryKey&lt;/code&gt;를 어떻게 작성했는지 알 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 쿼리 키 팩토리와 연결해 useQuery를 사용하는 실사용 예시&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 키 팩토리&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;//queryKeys.ts

export const queryKeys = {
  ask: {
    all: ['ask'] as const,
    detail: (id: string) =&amp;gt; [...queryKeys.ask.all, id] as const,
  },
  book: {
    all: ['book'] as const,
    detail: (id: string) =&amp;gt; [...queryKeys.book.all, id] as const,
  },
  bookmark: {
    all: ['bookmark'] as const,
    detail: (id: string) =&amp;gt; [...queryKeys.bookmark.all, id] as const,
    byUser: (userId: string) =&amp;gt; [...queryKeys.bookmark.all, userId] as const,
  },
  comment: {
    all: ['comment'] as const,
    detail: (id: string) =&amp;gt; [...queryKeys.comment.all, id] as const,
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mutation&lt;/code&gt; 상호작용에 따라 캐시해두었던 데이터를 queryKey 기반으로 &lt;code&gt;invalidate&lt;/code&gt; (query를 무효화하고 재갱신)하기 위해 파일을 분리해 관리합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;요청 repo (DB에 따라, 기능에 따라 분리) - Optional&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// book.repo.ts

// 여기서의 fetch는 next의 fetch가 아닌 window 빌트인 fetch
// axios를 사용하기 원한다면 변경도 가능함.
export const bookRepo = {
  getBook: async (isbn13: string) =&amp;gt; {
    const res = await fetch(`/api/library?isbn13=${isbn13}`);
    if (!res.ok) throw new Error(&quot;failed&quot;);
    return res.json();
  },
}

export default bookRepo&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;useQuery 훅 정의&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// hook/useBookFetching.ts

import { useQuery } from &quot;@tanstack/react-query&quot;;
import { queryKeys } from &quot;@/queryKeys&quot;;
import bookRepo from &quot;@/repo/book.repo&quot;;

export const useGetBookDetail = (isbn13: string) =&amp;gt; {
  return useQuery({
    queryKey: queryKeys.book.detail(isbn13),
    queryFn: () =&amp;gt; bookRepo.getBook(isbn13),
    staleTime: 1000 * 60, // Optional
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;컴포넌트에서 사용&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// test_query/page.tsx

&quot;use client&quot;; // tanstack query는 RCC에서만 실행되어야 합니다.

import { useGetBookDetail } from &quot;@/hook/useBookFetching&quot;;

function Page() {
  // 중요! 조건문으로 처리하거나 useEffect 내부에 넣을 수 없고 최상단에 정의해야 함.
  const { data, isLoading, error } = useGetBookDetail(&quot;9788936433598&quot;);

  console.log(data);
  return &amp;lt;div&amp;gt;Pa&amp;lt;/div&amp;gt;;
}

export default Page;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Dehydrate / HydrationBoundary 패턴 ( Hybrid )&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 SSR로 데이터와 함께 초기 로딩해주는 것은 빠르고, SEO 최적화에 도움이 된다. 그런데 댓글 목록과 같이 SEO에 포함되기를 원하면서, 유저가 댓글을 작성했을 때 목록에 즉시 반영되도록 하려면 어떻게 해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR - 로딩이 빠름, SEO 최적화에 도움이 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR - 유저 상호작용에 대해 즉시 반영되어야 하는 경우에 효과적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 CSR로 댓글 목록 컴포넌트를 작성할 수도 있지만, SSR 방식으로 초기 로드하는 방법이 있다. Dehydrate / HydrationBoundary 패턴이다.&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;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 사이드에서 SSR 방식으로 queryClient에 prefetch로 데이터를 주입한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;async function Page({ params }: { params: Promise&amp;lt;{ id: string }&amp;gt; }) {
  const { id } = await params;

  const queryClient = new QueryClient();

  try {
    await Promise.all([
      queryClient.prefetchQuery({
        queryKey: queryKeys.comment.get(id),
        queryFn: () =&amp;gt; getComments({ id }),
      }),
      queryClient.prefetchQuery({
        queryKey: queryKeys.post.id(id),
        queryFn: () =&amp;gt; getPost(id),
      }),
    ]);
  } catch (error) {
    console.error(&quot;Prefetch error:&quot;, error);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HydrationBoundary에 queryClient를 넘겨 준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;  return (
    &amp;lt;div className=&quot;w-full flex flex-col items-center min-h-[calc(100vh-140px)] pt-4&quot;&amp;gt;
      &amp;lt;div className=&quot;w-[80%] md:w-[60%] flex flex-col min-h-[calc(100vh-140px)] py-15 gap-4&quot;&amp;gt;
        {/* 게시글 영역 */}
        &amp;lt;HydrationBoundary state={dehydrate(queryClient)}&amp;gt;
          &amp;lt;PostContent id={id} /&amp;gt;
          {/* 댓글 작성 영역 */}
          &amp;lt;CommentWrite id={id} /&amp;gt;
          {/* 댓글 영역 */}
          &amp;lt;PostCommnet id={id} /&amp;gt;
        &amp;lt;/HydrationBoundary&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하위 요소에서 서버의 &lt;code&gt;queryClient&lt;/code&gt; 데이터를 넘겨 받아 캐시 데이터에 추가한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 경우에는 반드시 서버의 &lt;code&gt;queryClient&lt;/code&gt;의 &lt;code&gt;queryKey&lt;/code&gt;와 클라이언트의 &lt;code&gt;queryKey&lt;/code&gt;가 일치해야 한다. (queryKey 기반한 캐시를 받아오기 때문)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;  const { data: comments, isLoading } = useGetComments({ id, page: 1, size: 30 });

    // 아래에서 data를 자유롭게 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 요약&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 컴포넌트에서 새로운 &lt;code&gt;queryClient&lt;/code&gt;를 생성&lt;/li&gt;
&lt;li&gt;해당 &lt;code&gt;queryClient&lt;/code&gt;에 서버에서 조회한 데이터를 넘긴다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hydrateBoundary&lt;/code&gt;에 해당 &lt;code&gt;queryClient&lt;/code&gt;를 넘긴다.&lt;/li&gt;
&lt;li&gt;전역으로 선언된 &lt;code&gt;queryClient&lt;/code&gt;는 해당 쿼리키와 데이터를 받아 오고, 하위의 클라이언트 컴포넌트에서 같은 쿼리키를 사용하면 서버에서 넘겨 준 데이터를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutate&lt;/code&gt; 발생 시에 &lt;code&gt;Tanstack Query&lt;/code&gt;로 일관성 있게 관리하면 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 처리의 queryKey 문제도 개선됩니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 관리하는 데 정말 도움이 됩니다. &lt;code&gt;Next&lt;/code&gt;에서 &lt;code&gt;fetch&lt;/code&gt;는 두 종류로 나누어 사용하죠. 서버단 &lt;code&gt;Next fetch&lt;/code&gt;와 클라이언트단 &lt;code&gt;window fetch&lt;/code&gt; 양쪽으로 나뉠 경우에 캐시 관리가 정말 곤란해집니다. 클라이언트의 &lt;code&gt;fetch&lt;/code&gt;를 &lt;code&gt;Tantack&lt;/code&gt;을 이용하고, 서버단 &lt;code&gt;fetch&lt;/code&gt;를 &lt;code&gt;Next Fetch&lt;/code&gt;를 이용했다고 가정해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tanstack&lt;/code&gt;은 &lt;code&gt;queryKey&lt;/code&gt;를 이용해 캐시를 관리할 수 있고, &lt;code&gt;Next Fetch&lt;/code&gt;는 &lt;code&gt;tag&lt;/code&gt; 와 &lt;code&gt;path&lt;/code&gt; 를 이용해 캐시를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽을 &lt;code&gt;revalidate&lt;/code&gt; 하는 예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// 클라이언트
queryClient.invalidateQueries(['post', id]) // ['post', id] 구조를 가진 모든 요청을 invalidate

//서버
revalidatePath('/blog/post-1') // 해당 path의 모든 요청이 revalidate
revalidateTag('post-1') // 단일 string&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;post 하나에 대해 양쪽에서 관리해야 한다면 이중으로 요청해야 되는 문제가 있습니다. 이때 유틸 함수를 통해 관리하면 되는 것 아닌가요? 라고 생각할 수 있는데, 이 경우도 완벽하게 호환되지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;invalidateQueries&lt;/code&gt; 는 배열형 데이터를 받고 [&amp;rsquo;post&amp;rsquo;, id]를 인수로 전달할 경우 [&amp;rsquo;post&amp;rsquo;, id]가 &lt;code&gt;queryKey&lt;/code&gt; 인 모든 요청을 초기화합니다. 그런데 [&amp;rsquo;post&amp;rsquo;]라고 전달하는 경우 [&amp;rsquo;post&amp;rsquo;, &amp;hellip;]로 시작하는 모든 query 요청을 초기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;revalidateTag&lt;/code&gt; 는 tag 단일 string만 초기화할 수 있으므로, 완벽하게 대체하기에는 적절하지 않습니다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dehydrate / HydrationBoundary(Hybrid) 패턴이 모든 문제를 해결할 수 있는 마법처럼 느껴지지만 실제로 적용할 때는 고려해봐야 합니다. 이 패턴을 적용할 경우 코드량이 늘어나고 코드의 복잡도가 증가할 수 있습니다.&lt;/p&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;dehydrate 패턴을 상위 요소에서 수행해야 함.&lt;/li&gt;
&lt;li&gt;하위 요소의 queryKey와 반드시 일치시켜주어야 정상적으로 작동함.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 이유로 복잡도가 증가할 수 있는 문제도 있습니다.&lt;/p&gt;</description>
      <category>React/Next.js</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/105</guid>
      <comments>https://yun-engene.tistory.com/105#entry105comment</comments>
      <pubDate>Thu, 20 Nov 2025 22:10:54 +0900</pubDate>
    </item>
    <item>
      <title>[Re:Life] 프로젝트 회고</title>
      <link>https://yun-engene.tistory.com/106</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 개요&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Re:Life&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;만약 그때 다른 선택을 했다면?&quot;&lt;/b&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 시뮬레이션하는 평행우주적 인생 시나리오 서비스&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  프로젝트 소개&lt;/h3&gt;
&lt;a id=&quot;user-content--프로젝트-소개&quot; style=&quot;background-color: #000000; color: #0969da;&quot; href=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE#-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%86%8C%EA%B0%9C&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Re:Life&lt;/b&gt;는 사용자의 과거 인생 선택을 기반으로, AI가 &quot;만약 그때 다른 선택을 했다면?&quot;이라는 평행우주적 인생 시나리오를 시뮬레이션하여 제공하는 웹 서비스입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순한 재미를 넘어, 실제 사회 통계와 개인 성향 데이터를 반영하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;현실적이고 구체적인 대안적 삶&lt;/b&gt;을 탐색할 수 있도록 설계되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;깃허브 레포지토리&lt;/h3&gt;
&lt;figure id=&quot;og_1761643909220&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE: 프로그래머스 웹개발 프론트엔드 6기 8회차 1팀 최&quot; data-og-description=&quot;프로그래머스 웹개발 프론트엔드 6기 8회차 1팀 최종 프로젝트 프론트엔드 레포지토리입니다. Contribute to prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE&quot; data-og-url=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eTLGG/hyZMMTZtQe/1zZRkTxbuj0doEmIZMpqE0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfCkAf/hyZK8YOxsM/JZwrirzg9p9mWcE0qXMSHK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eTLGG/hyZMMTZtQe/1zZRkTxbuj0doEmIZMpqE0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfCkAf/hyZK8YOxsM/JZwrirzg9p9mWcE0qXMSHK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE: 프로그래머스 웹개발 프론트엔드 6기 8회차 1팀 최&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스 웹개발 프론트엔드 6기 8회차 1팀 최종 프로젝트 프론트엔드 레포지토리입니다. Contribute to prgrms-web-devcourse-final-project/WEB5_6_OneTop_FE development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;배포 주소 ( 백엔드 서버 배포 중단 )&lt;/h3&gt;
&lt;figure id=&quot;og_1761643947584&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Re:Life&quot; data-og-description=&quot;Re:Life는 당신의 인생 선택을 기록하고, 다른 선택을 했다면 어떤 평행우주가 펼쳐질지 보여줍니다. 지금 바로 시작해보세요! 시작하기&quot; data-og-host=&quot;www.relife.kr&quot; data-og-source-url=&quot;https://www.relife.kr/&quot; data-og-url=&quot;https://www.relife.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.relife.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.relife.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Re:Life&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Re:Life는 당신의 인생 선택을 기록하고, 다른 선택을 했다면 어떤 평행우주가 펼쳐질지 보여줍니다. 지금 바로 시작해보세요! 시작하기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.relife.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시연 영상&lt;/h3&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=9T7L8-4rH9M&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ZWnpa/hyZMd6VLAS/bvXTbaIpqAO9efFYv2dyl0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;백엔드 8회차, 프론트엔드 6회차 1팀 Re:Life 최종발표 영상&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/9T7L8-4rH9M&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 시작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- 팀 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 선정부터 쉽지 않은 과정이긴 했다. 익숙한 프론트엔드 팀원들끼리의 프로젝트가 아니라 처음 보는 백엔드 팀원들을 선정하고 그 사람들과 컨텍해 팀을 이루어야 했다. 때문에 팀을 프론트팀끼리 구성하고, 팀 PR을 구성해야 했다. 매우 감사하게도, 프론트 팀에서는 원하는 팀원들에게 요청을 보냈을 때 바로 구성할 수 있었다. 팀 PR 문서는 다른 프론트팀이 너무 재미있게 짜서 그렇게 짜야 하나 싶었지만 우리 팀 스타일이 아닌 것 같아 최대한 해왔던 것을 잘 보여줄 수 있는 형태로 구성했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;NopeNotMyStyleDonnaGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJK58/dJMcajgcgga/2Sx3iW4wHKCJdRHJkW0vBK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJK58/dJMcajgcgga/2Sx3iW4wHKCJdRHJkW0vBK/img.gif&quot; data-alt=&quot;하지만 그건 내 스타일 아님.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJK58/dJMcajgcgga/2Sx3iW4wHKCJdRHJkW0vBK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/AJK58/dJMcajgcgga/2Sx3iW4wHKCJdRHJkW0vBK/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;358&quot; height=&quot;358&quot; data-filename=&quot;NopeNotMyStyleDonnaGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하지만 그건 내 스타일 아님.&lt;/figcaption&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 자체는 내 아이디어는 아니었고, 팀원의 아이디어로 결정된 아이디어였다. 사실 나는 처음엔 반대 의견이 더 컸었다. 프로젝트 기획 자체는 신선하고 재미있어 보였다. 그런데 문제라고 생각했던 점은&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트 확장이 어려워 보임&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기획은 신선하고 재미있지만 '유저 입력 선택지' 자체가 가진 유저 입력복잡도 증가 (어떻게 유저에게 UX로 납득되게 보여줄 것인지)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 사용이 고정되지 않음으로서 결과값 불안정 위험성&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 이유로 프로젝트 아이디어 선정에서 반대 의견을 이야기했었으나, 백엔드 팀원들과 논의 과정에서 최종 결정된 아이디어가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 내가 옳을 수는 없다. 백엔드 팀에서도 복잡도 제어에 자신감을 내비쳤었고, 이미 선택된 기획에서는 내가 납득해야 '원 팀'이 될 수 있다고 생각했었다. 내가 납득할 수 있었던 이유는 두 가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;/ol&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;h3 data-ke-size=&quot;size23&quot;&gt;- 기술 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TypeScript&lt;/b&gt; - 코드 작성 단계에서 타입을 걸러주는 강력함은 말할 것 없고, 사용하는 라이브러리들도 모두 Type에 강점이 있는 라이브러리였기 때문에 연계성이 좋다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;React&lt;/b&gt; - 팀원 모두 팀 활동을 거치면서 컴포넌트 기반의 작업의 재사용성과 작업 분리에 대해서 잘 이해하고 있었기 때문에 좋은 선택지라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js&lt;/b&gt; - 백엔드에서 많은 데이터를 받아오고 많은 페이지를 상정했기 때문에 서버 컴포넌트, 최적화 기능, 라우팅 구조, SEO에서 큰 장점이 있다고 생각했다. 때문에 Next를 사용하기로 결정하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tanstack Query&lt;/b&gt; - 클라이언트에서의 캐시 관리에 큰 장점이 있는 데 더해 RSC에서도 dehydrate 패턴으로 데이터를 받아오는 데 큰 장점이 있다고 느껴&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Axios&lt;/b&gt; - 간단하게 사용할 수 있는 fetch, 공용 인스턴스 구현 기능이 백엔드 협업에서 헤더 설정과 인터셉터를 이용한 전처리에 큰 장점이 있다고 느껴 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Zod&lt;/b&gt; - 백엔드 협업에서 데이터 구조는 빈번하게 있을 수 있다고 판단해 런타임 타입 검증이 필요하다고 생각해 Zod를 이용하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;react-hook-form&lt;/b&gt; - 유저 입력이 많은 서비스였기 때문에 유효성 검증에 react-hook-form을 이용하면 유저 입력 검증을쉽게 구현할 수 있다고 생각해 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GSAP &lt;/b&gt;- 이미 GSAP 애니메이션에 익숙한 팀원들이 많기도 했고(나 포함), 라이브러리의 다양한 기능이 무료로 풀렸기 때문에 강력한 기능들을 사용해보고 싶은 욕심이 있었다. 스크롤 제어에서의 강력함은 말 할 것도 없고.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'평행우주'라는 컨셉에 맞게 'multiverse'하면 우주의 이미지가 떠오르듯이, 우주의 모습을 모티브로 작성했다. 먼저 레퍼런스를 가져와 초기 컨셉을 정하고, 와이어프레임 구성 후에 디테일 작업을 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgKQlQ/dJMcai2FdBe/vksJ1hXO6Ka2tjlkCPJ4b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgKQlQ/dJMcai2FdBe/vksJ1hXO6Ka2tjlkCPJ4b1/img.png&quot; data-alt=&quot;메인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgKQlQ/dJMcai2FdBe/vksJ1hXO6Ka2tjlkCPJ4b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgKQlQ%2FdJMcai2FdBe%2FvksJ1hXO6Ka2tjlkCPJ4b1%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;1109&quot; height=&quot;566&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;평행 우주 리스트 (완료) (4).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XLT0u/dJMcahJr9Xg/ieoDAu4nZwMmp6qUbxJf0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XLT0u/dJMcahJr9Xg/ieoDAu4nZwMmp6qUbxJf0k/img.png&quot; data-alt=&quot;평행우주 목록&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XLT0u/dJMcahJr9Xg/ieoDAu4nZwMmp6qUbxJf0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXLT0u%2FdJMcahJr9Xg%2FieoDAu4nZwMmp6qUbxJf0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;평행 우주 리스트 (완료) (4).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;평행우주 목록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 아키텍쳐&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- 프로젝트 아키텍쳐&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;relifeArchitecture.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qOIJH/dJMcagqe1ev/HjAiI6VJlQxpfRKx48y7w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qOIJH/dJMcagqe1ev/HjAiI6VJlQxpfRKx48y7w0/img.png&quot; data-alt=&quot;전체 아키텍쳐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qOIJH/dJMcagqe1ev/HjAiI6VJlQxpfRKx48y7w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqOIJH%2FdJMcagqe1ev%2FHjAiI6VJlQxpfRKx48y7w0%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;626&quot; height=&quot;495&quot; data-filename=&quot;relifeArchitecture.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 아키텍쳐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 아키텍쳐는 다음과 같다. 위 아키텍쳐는 조금 백엔드 중심으로 작성되어 있으니, 프론트 중심이면서 단순화되어 있는 아키텍쳐는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Slide 16_9 -23 3.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lrLx9/dJMcaezbZqj/Anzi7zQGhe5iAKpp4pKibK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lrLx9/dJMcaezbZqj/Anzi7zQGhe5iAKpp4pKibK/img.png&quot; data-alt=&quot;프론트 중심의 단순화된 아키텍쳐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lrLx9/dJMcaezbZqj/Anzi7zQGhe5iAKpp4pKibK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlrLx9%2FdJMcaezbZqj%2FAnzi7zQGhe5iAKpp4pKibK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;Slide 16_9 -23 3.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프론트 중심의 단순화된 아키텍쳐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next를 사용하므로, 배포 서버에서 RSC 렌더링과 server action의 책임을 가지고 있고, 비즈니스 로직들은 Nginx 프록시를 거쳐 메인 서버에서 처리한다. OAuth는 Github와 Google을 이용했다. 클라이언트에서 url을 연결해주고, 로그인 성공 시 OAuth 서버는 리다이렉트를 거쳐 메인 서버에서 클라이언트로 cookie를 내려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 내가 구현한 파트, 트러블슈팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- 로그인, 회원가입&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;⭐ 구현 내용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;회원가입-1(1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEBvZI/dJMb99Lp43W/PXYptVvea4HQKZaKtDeDJ0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEBvZI/dJMb99Lp43W/PXYptVvea4HQKZaKtDeDJ0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEBvZI/dJMb99Lp43W/PXYptVvea4HQKZaKtDeDJ0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cEBvZI/dJMb99Lp43W/PXYptVvea4HQKZaKtDeDJ0/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;1000&quot; height=&quot;563&quot; data-filename=&quot;회원가입-1(1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&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;로그인 / 회원가입의 form은 react-hook-form과 zod를 이용해 유효성을 관리해 유저에게 즉시 피드백을 줄 수 있도록 했다.&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;로그인 / 회원가입을 구현하면서 신경썼던 점은 로직을 next의 server action으로 분리하는 것이었다. server action을 이용하면 서버 레벨에서 요청하게 된다. 그 때문에 로그인 / 회원가입 로직을 노출하지 않고 숨길 수 있다.&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;백엔드에서 JWT를 사용한 인증을 사용할 것이라고 예상하고 프로젝트 시작 전에 미리 준비해뒀었는데, Session + CSRF 토큰 기반으로 관리하기로 결정돼 Session ID를 이용해 구현했다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- next를 학습하기는 했지만 next에 익숙하지 않았기 때문에 서버 레벨의 요청, cookie를 cookieStore로 수동 관리하는 것에 어려움이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 또한 모든 DB 요청이 인증 권한이 없으면 요청이 불가능했고, 백엔드에서 CSRF 구현 관련으로 이슈가 많아 로그인 구현이 지연되었기 때문에 최대한 빠르게 작업을 마무리 해 줄 필요가 있었다.&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;익숙하지 않은 개념(next server action 요청, CSRF 토큰)에 시간적 압박까지 있어 구현하면서 정말 스트레스를 많이 받았었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;TimePassGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4yj6W/dJMcain3F6G/3aOf90KLOd7PQo9GAJuFi0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4yj6W/dJMcain3F6G/3aOf90KLOd7PQo9GAJuFi0/img.gif&quot; data-alt=&quot;시간이 너무 촉박했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4yj6W/dJMcain3F6G/3aOf90KLOd7PQo9GAJuFi0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/4yj6W/dJMcain3F6G/3aOf90KLOd7PQo9GAJuFi0/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;427&quot; height=&quot;324&quot; data-filename=&quot;TimePassGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시간이 너무 촉박했다.&lt;/figcaption&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 구현 전에는 요청 권한을 임시적으로 모두 낮춰주는 것을 백엔드에 요청했어야 했는데, 그 부분이 아쉽다. 그렇게 했다면 처음 구현에서 훨씬 완성도 있는 코드를 만들 수 있었을 것 같고, mock 데이터 관리 소요도 훨씬 줄었을 것 같다.&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;- 공용 fetch 인스턴스 유틸&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;⭐ 구현 내용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;StrangeBrewStrangeBrewBottlesGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdMRAB/dJMcaiawwgV/O7XflCkEIMZHVVimdl7Vb1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdMRAB/dJMcaiawwgV/O7XflCkEIMZHVVimdl7Vb1/img.gif&quot; data-alt=&quot;공장처럼 모든 요청을 일관화할 필요가 있어 보였다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdMRAB/dJMcaiawwgV/O7XflCkEIMZHVVimdl7Vb1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bdMRAB/dJMcaiawwgV/O7XflCkEIMZHVVimdl7Vb1/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;498&quot; height=&quot;280&quot; data-filename=&quot;StrangeBrewStrangeBrewBottlesGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공장처럼 모든 요청을 일관화할 필요가 있어 보였다.&lt;/figcaption&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;CSRF 토큰 인증을 요구했기 때문에 서버에서의 요청(Next의 fetch)과 클라이언트 요청(Axios) 모두 전처리가 필요할 것 같았다. 로그인을 구현하기에 앞서 먼저 구현해야 할 필요성을 느끼고 구현했다.&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;p data-ke-size=&quot;size16&quot;&gt;1. 두 개의 인스턴스는 이름만 다를 뿐 같은 파라미터 구조와 요청 방식을 가져야 한다. (단순화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. CSRF 인증 실패 시(403 에러)에 CSRF 토큰을 재수령하고 재요청하도록 설정한다. (안정성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 에러에 대해서는 중계만 하고, 에러 처리 핵심 로직은 사용되는 위치에서 구현한다 (다양성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 모든 요청에 대해 이 인스턴스를 사용하도록 한다 (일관성)&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;위 가치들을 기본으로 두고, 공용으로 사용할 nextFetcher.ts와 api.ts를 구현했다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSRF 토큰을 이용하는 건 크게 어렵지 않다고 생각했었다. interceptor를 붙여 헤더에 토큰을 삽입하고, 403에러가 뜰 경우 retry하는 로직을 짜면 쉽게 구현할 수 있을 것이라고 예상했는데, 늘 코드 짜는 게 그렇듯 계획대로 되지는 않는다.&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;정상적으로 구현했다고 생각했는데, 지속적으로 403에러가 발생했다. 원인은 나는 백엔드에서 내려주는 토큰 이름이 'CSRF-TOKEN'이라고 인식하고 있었다. 그런데 실제로 내려오는 토큰 이름은 'XSRF-TOKEN'이었다. 백엔드에서는 쿠키 이름을 직접 지정해 관리하는 cookieStore에 대한 인식이 없어 자동지정 명령으로 저장될 줄 알아서 공유하지 않았다고 하고, 나는 너무 개인적으로 쿠키 이름을 생각했던 것 같다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✅ 아쉬운 점&lt;/h4&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;1. 정상적으로 구현 된 것 같은데 안 되네?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한참 이리저리 바꿔보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 혹시 모르니 Axios 요청으로 요청해보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 정상적으로 받는 것 같은데. Application에 찍히는 토큰 이름이 다르네?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 소통 시작, 해결&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;h3 data-ke-size=&quot;size23&quot;&gt;-&amp;nbsp; Google Tag Manager(GTM) + Clarity 연결&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;⭐ 구현 내용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sFVri/dJMcajN2mcI/DCnharjqJyzEbYoWUELCsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sFVri/dJMcajN2mcI/DCnharjqJyzEbYoWUELCsK/img.png&quot; data-alt=&quot;Clarity 대시보드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sFVri/dJMcajN2mcI/DCnharjqJyzEbYoWUELCsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsFVri%2FdJMcajN2mcI%2FDCnharjqJyzEbYoWUELCsK%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;1906&quot; height=&quot;910&quot; data-origin-width=&quot;1906&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Clarity 대시보드&lt;/figcaption&gt;
&lt;/figure&gt;
&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; text-align: start;&quot;&gt;프로젝트 시작 전부터 '나는 이번 프로젝트에서 유저 데이터 분석을 위해 로깅을 써봐야겠다.'라고 결심했었는데, 사실 로깅도 백엔드에서 받아줘야 하다 보니. 우선순위가 뒤로 밀려서 내가 큰 작업 할 필요 없이 프론트 레벨에서 붙일 수 있는 도구인 Clarity를 사용하기로 결정했다.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;그냥 Clarity만 붙여도 작동하지만, Google Tag Manager(GTM)을 이용했을 때 여러 분석 도구로 확장해 이용할 수 있도록 설정했다. Clarity를 적용해 유저의 활동을 추적해 유저의 활동을 분석할 수 있게 됐고, 페이지별 분석 점수 또한 얻을 수 있게 됐다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;  좋았던 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 붙이는 것 자체도 어렵지 않게 구현할 수 있었고, 편리한 대시보드 UI로 유저 실제 피드백과 LCP, FCP 등과 같은 분석 데이터를 얻을 수 있어 프로젝트를 개선할 수 있는 방향성을 잡을 수 있었다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스를 돌려 보고, 실제 유저에게 서비스해봤으면 개선 방향을 더 넓게 잡을 수 있었을 텐데, 40명 미만의 제한된 데이터만 받을 수 있는 게 아쉬웠다.&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;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;⭐ 구현 내용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;온보딩-1 (1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A4r7I/dJMb995ITmY/i50g39UR5gnvPd5pBJy1o1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A4r7I/dJMb995ITmY/i50g39UR5gnvPd5pBJy1o1/img.gif&quot; data-alt=&quot;유저 온보딩 페이지(3배속)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A4r7I/dJMb995ITmY/i50g39UR5gnvPd5pBJy1o1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/A4r7I/dJMb995ITmY/i50g39UR5gnvPd5pBJy1o1/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;1000&quot; height=&quot;563&quot; data-filename=&quot;온보딩-1 (1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;유저 온보딩 페이지(3배속)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;next의 middleware를 이용해 회원 로그인이 필요한 라우트 그룹 (protected)에 접근할 경우 redirect를 지정하고 온보딩 페이지로 이동시킨다.&lt;/span&gt;&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 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 정확도를 높이기 위한 유저 데이터를 받고 해당 페이지에 이동시키도록 온보딩 페이지를 구성했다.&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;유저 프로필 설정 페이지는 GSAP을 이용해 슬라이드 애니메이션으로 구성했다. 각각의 페이지 아이템들을 form 요소의 자식 요소로 삽입해 render 패턴으로 관리했다.&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;하위 요소의 props를 고정시켜 &lt;/span&gt;zod의 검증과 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;react-hook-form의 controll, setValue 등을 이용해 관리했다.&lt;/span&gt;&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;  구현에서의 어려움&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;- (protected) 라우팅 그룹 리다이렉트 관리&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 리다이렉트를 구현할 때는 (protected) 라우팅 그룹의 레이아웃에 searchParams를 붙여 리다이렉트를 처리했다. 그런데 잘 작동하는 줄 알았던 리다이렉트가 다른 기능 구현한 시점부터 꼬이기 시작하더니, 작동을 멈췄다.&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: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'이 레이아웃은 공통으로 동작하니까 여기서 처리하면 모든 페이지 관리가 가능하겠지?'&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;라는 생각으로 구현했었는데, 알고 보니 layout에서는 searchParams를 받아올 수 없는 문제가 있었다. 근본적으로 뭔가 잘못 설계됐던 것이다. 물론 protected의 layout을 &quot;use client&quot;로 관리하면 useParma()을 이용할 수 있어 안 되는 건 아닌데... next를 사용하는 장점이 아예 사라질 것 같아 그건 옵션이 못 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;OhNoLiamScottEdwardsGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVtRm7/dJMcacVGfMQ/5u3DgiZ1e6HfCbZFs7Fejk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVtRm7/dJMcacVGfMQ/5u3DgiZ1e6HfCbZFs7Fejk/img.gif&quot; data-alt=&quot;기존에 잘 설계해서 잘 작동하는 줄 알았는데... 근본적으로 잘못됐다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVtRm7/dJMcacVGfMQ/5u3DgiZ1e6HfCbZFs7Fejk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cVtRm7/dJMcacVGfMQ/5u3DgiZ1e6HfCbZFs7Fejk/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;394&quot; height=&quot;394&quot; data-filename=&quot;OhNoLiamScottEdwardsGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존에 잘 설계해서 잘 작동하는 줄 알았는데... 근본적으로 잘못됐다.&lt;/figcaption&gt;
&lt;/figure&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;기존 구현 방법 자체가 잘못 됐다면 아예 다른 방법을 적용해야겠다고 생각했고, 기존 대체재로 고려했으나 사용 경험이 없어 사용하지 않았던 middleware를 이용해 로그인 상태에 따라 (protected) 내부 접근을 관리하도록 개선했다.&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;size18&quot;&gt;- 프로필 설정 페이지 렌더링 관리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;슬라이드 형태의 form을 사용할 때 고민했던 점은 다양한 컴포넌트를 슬라이드 페이지 내부에 렌더링하면서 추가, 제거가 용이하도록 작성하는 거였다. 그래서 최초 설계에는 input 컴포넌트들을 슬라이더 내부의 배열로 관리하려고 했지만 constant처럼 step을 미리 정의해 파일을 분리해 관리했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761645089553&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const steps: StepDefinition[] = [
  {
    key: &quot;name&quot;,
    label: &quot;당신의 이름은?&quot;,
    placeholder: &quot;이름을 입력해주세요&quot;,
    component: InputText,
  },
  
  // 추가 컴포넌트들
  
  ]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;요소 렌더링과 애니메이션, 제출 동작은 slider에서 담당하고 추가적인 아이템이 필요하면 단순하게 steps만 관리하면 되는 구조로 개선했다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✅ 아쉬운 점&lt;/h4&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;h3 data-ke-size=&quot;size23&quot;&gt;- 커뮤니티 페이지 전체 구현&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;⭐ 구현 내용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;커뮤니티 기능 (1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R6j4H/dJMcafSpmvF/EyUAXpxEyksns4uVNtbimK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R6j4H/dJMcafSpmvF/EyUAXpxEyksns4uVNtbimK/img.gif&quot; data-alt=&quot;커뮤니티 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R6j4H/dJMcafSpmvF/EyUAXpxEyksns4uVNtbimK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/R6j4H/dJMcafSpmvF/EyUAXpxEyksns4uVNtbimK/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;823&quot; height=&quot;463&quot; data-filename=&quot;커뮤니티 기능 (1).gif&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커뮤니티 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티 전체 기능을 구현했다. 혼자 작업했는데, CRUD 위주 작업이었지만 볼륨이 꽤 큰 작업이었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SoBusyWorkingGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEjJ7h/dJMcagcIaAL/GIEXZEEzHPLPLXWkdKKFO1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEjJ7h/dJMcagcIaAL/GIEXZEEzHPLPLXWkdKKFO1/img.gif&quot; data-alt=&quot;진짜 할 게 너무 많았다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEjJ7h/dJMcagcIaAL/GIEXZEEzHPLPLXWkdKKFO1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cEjJ7h/dJMcagcIaAL/GIEXZEEzHPLPLXWkdKKFO1/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;375&quot; height=&quot;375&quot; data-filename=&quot;SoBusyWorkingGIF.gif&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;진짜 할 게 너무 많았다.&lt;/figcaption&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;size18&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;895&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tq7k0/dJMcajmYd62/F1jSKubkdFWySeQIKCCPZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tq7k0/dJMcajmYd62/F1jSKubkdFWySeQIKCCPZ0/img.png&quot; data-alt=&quot;투표 게시글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tq7k0/dJMcajmYd62/F1jSKubkdFWySeQIKCCPZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTq7k0%2FdJMcajmYd62%2FF1jSKubkdFWySeQIKCCPZ0%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;895&quot; height=&quot;104&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 게시글&lt;/figcaption&gt;
&lt;/figure&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;은 유저가 지정한 선택지로 투표 이벤트가 포함된 형태이다. 낙관적 업데이트를 적용해 클릭 시 투표가 즉시 반영되고, fallback시 이전 상태로 복귀하도록 구현해 UX를 개선했다.&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;size18&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;242&quot; data-origin-height=&quot;50&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Xtij/dJMcac9dDmP/2kksvPUdKYrjHKvYAHwqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Xtij/dJMcac9dDmP/2kksvPUdKYrjHKvYAHwqk0/img.png&quot; data-alt=&quot;커뮤니티 탭 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Xtij/dJMcac9dDmP/2kksvPUdKYrjHKvYAHwqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Xtij%2FdJMcac9dDmP%2F2kksvPUdKYrjHKvYAHwqk0%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;242&quot; height=&quot;50&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커뮤니티 탭 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭 전환은 게시글 카테고리를 선택해 해당 카테고리만 존재하는 게시글 목록을 확인할 수 있으며, searchParams를 이용한 전환으로 새로고침 없이 유저 클릭에 즉시 반영되는 부드러운 전환이 가능하다.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IymBX/dJMcafkzqbG/mMA7Zr1jvKBJLmR6LV3jrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IymBX/dJMcafkzqbG/mMA7Zr1jvKBJLmR6LV3jrk/img.png&quot; data-alt=&quot;검색 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IymBX/dJMcafkzqbG/mMA7Zr1jvKBJLmR6LV3jrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIymBX%2FdJMcafkzqbG%2FmMA7Zr1jvKBJLmR6LV3jrk%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;932&quot; height=&quot;279&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검색 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 기능은 유저가 지금 보고 있는 탭 기준으로 반영해 선택된 카테고리 목록에서 검색하도록 구현됐다. 유저는 '제목, 제목+내용, 작성자'로 전환해 검색 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;926&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBeasE/dJMcadmKN4w/mpLckii3YgeNvmU1Htse41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBeasE/dJMcadmKN4w/mpLckii3YgeNvmU1Htse41/img.png&quot; data-alt=&quot;커뮤니티 상세&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBeasE/dJMcadmKN4w/mpLckii3YgeNvmU1Htse41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBeasE%2FdJMcadmKN4w%2FmpLckii3YgeNvmU1Htse41%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;926&quot; height=&quot;679&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;679&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커뮤니티 상세&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 상세에서 작성, 수정, 삭제, 좋아요가 가능하고, 게시글의 댓글에 작성, 수정, 삭제, 좋아요가 가능하다. 유저가 원한다면 익명으로 댓글이나 게시글을 작성해 활동할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 상세의 데이터들은 generateMetadata를 이용해 게시글별 메타데이터를 따로 설정해 SEO와 링크 공유 시 볼 수 있는 데이터를 확보할 수 있는 형태로 설계했다.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;88&quot; data-origin-height=&quot;50&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nPmSm/dJMcaf5WAtb/s3o0kKR6ARtZkpTzkx5Ot1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nPmSm/dJMcaf5WAtb/s3o0kKR6ARtZkpTzkx5Ot1/img.png&quot; data-alt=&quot;황금비 프로필&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nPmSm/dJMcaf5WAtb/s3o0kKR6ARtZkpTzkx5Ot1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnPmSm%2FdJMcaf5WAtb%2Fs3o0kKR6ARtZkpTzkx5Ot1%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;88&quot; height=&quot;50&quot; data-origin-width=&quot;88&quot; data-origin-height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;황금비 프로필&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 닉네임에 따라 해시값으로 황금비 기준으로 그라데이션을 만들어 프로필 이미지로 보여준다. 초기 기획에는 유저의 프로필 이미지를 보여주는 형태를 예상했었다. 그런데 이미지 업로드가 작업 시간상 어려울 것 같아 프로필 이미지 자리를 빼기로 결정했었는데, 빼니까 너무 허전해 구글의 기본 프로필 이미지에 착안해서 자동 생성해서 보여 주도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;1901&quot; data-origin-height=&quot;895&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xTGd4/dJMcaaDy3Ne/Ci7olO3YNgG24sCqWVhHbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xTGd4/dJMcaaDy3Ne/Ci7olO3YNgG24sCqWVhHbk/img.png&quot; data-alt=&quot;메인 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xTGd4/dJMcaaDy3Ne/Ci7olO3YNgG24sCqWVhHbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxTGd4%2FdJMcaaDy3Ne%2FCi7olO3YNgG24sCqWVhHbk%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;1901&quot; height=&quot;895&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;895&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메인 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지에서는 카테고리별 좋아요 기준의 인기 게시글과 최신 업로드된 게시글, 투표와 시나리오 카드를 확인할 수 있다. 투표는 Swiper로 처리해 10개의 아이템을 넘겨서 볼 수 있도록 구현했고, 인기 게시글 목록은 일반적인 커뮤니티 페이지의 메인 페이지 레퍼런스를 따와 목록 형식으로 길게 렌더링해줬다. 시나리오 카드는 점수별 애니메이션 구분이 매력 부분이라 생각해 카드 위주로 바로 보여주도록 설정했다.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zJsiu/dJMcadtwpbT/KrhDTIz9NOyjKmkVS16EWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zJsiu/dJMcadtwpbT/KrhDTIz9NOyjKmkVS16EWk/img.png&quot; data-alt=&quot;시나리오 공유&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zJsiu/dJMcadtwpbT/KrhDTIz9NOyjKmkVS16EWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzJsiu%2FdJMcadtwpbT%2FKrhDTIz9NOyjKmkVS16EWk%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;773&quot; height=&quot;561&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시나리오 공유&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;size18&quot;&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;  구현에서의 어려움&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;- 캐싱 관리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;캐싱을 관리할 때 가장 어렵게 만들었던 부분은 서버 query, 클라이언트 query 이중화였다. 서버에서의 요청은 Next Fetch를 이용하고, 클라이언트에서의 요청은 Tanstack + Axios를 이용했다.&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;문제는 여기서 발생했다. Tanstack의 쿼리 키 관리는 queryClient.invalidateQueries(Array)로, Next의 fetch는 revalidatePath(string)으로 관리해야 했다. 처리 일관화가 어렵고, 투표와 같은 특정 기능 수행 시점에 관련된 모든 query들을 무효화해줘야 하는데, 이중 처리와 키 일관화 저하로 캐시 관리의 복잡도가 증가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Slide 16_932 - 4.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBSYDF/dJMcae62hRO/RGgmlzOAe29JYTFxmjoJO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBSYDF/dJMcae62hRO/RGgmlzOAe29JYTFxmjoJO1/img.png&quot; data-alt=&quot;이중 관리 문제 도식화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBSYDF/dJMcae62hRO/RGgmlzOAe29JYTFxmjoJO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBSYDF%2FdJMcae62hRO%2FRGgmlzOAe29JYTFxmjoJO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;Slide 16_932 - 4.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이중 관리 문제 도식화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조에서는 클라이언트 컴포넌트에서의 mutate에 대해 서버 컴포넌트의 query를 관리해줘야 했기 때문에 쿼리 키와 쿼리 무효화 관리 복잡도 관리를 위해서는 처리를 일관화 할 필요가 있었다.&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;Tanstack Query를 이용해 RCC와 RSC 사이의 캐시 관리를 일관화해주면서, SEO까지 관리할 수 있는 방법이 있다. dehydrate 패턴을 이용하는 것이다.&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;dehydrate패턴을 적용할 경우 코드 복잡도가 상승하지만, 문제 발생 시점에서 캐싱 처리 개선이라는 목적을 위해서는 코드 복잡도보다 캐시 관리 일관성이 더 중요하다고 생각했다.&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;&lt;b&gt;dehydrate 패턴 사용의 순서는 다음과 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 서버 컴포넌트에서 새로운 queryClient를 생성&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 해당 queryClient에 서버에서 조회한 데이터를 넘긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. hydrateBoundary에 해당 queryClient를 넘긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 전역으로 선언된 queryClient는 해당 쿼리키와 데이터를 받아 오고, 하위의 클라이언트 컴포넌트에서 같은 쿼리키를 사용하면 서버에서 넘겨 준 데이터를 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5. mutate 발생 시에 Tanstack Query로 일관성 있게 관리하면 된다.&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;조회 자체는 서버에서 넘겨준 데이터를 그대로 사용할 수 있기 때문에 SEO개선, 로드 속도에 큰 장점이 있는 패턴이다. 캐시 복잡도 문제를 dehydrate 패턴으로 해결할 수 있었다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✅ 아쉬운 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멘토링 과정에서 이런 게시판에 대해 캐싱을 관리하기보다 항상 fresh한 데이터를 보여주는 것도 좋은 방법이라는 이야기를 들었다.&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;또한 캐싱 관리에 의해서 생산성이 조금 떨어진 것도 문제여서, 기술을 적용할 때는 기술 부채도 고려해야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 개인적인 성장&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- DDD 기반 디렉터리 구조 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 DDD에서 영향을 받은 Feature-based 디렉터리 구조로 초기 구조를 짜면서 이전과 다르게 정돈된 형태로 파일 구조를 관리했고, 실제로 사용하면서도 장점을 체감했다. 팀원들과 문서를 공유하며 초기 디렉터리 구조를 example로 제공해 팀원 온보딩을 도왔던 경험도 좋은 경험이 된 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1761720752506&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/
 ├─ app/                # Next.js 라우팅 (app router)
 │   ├─ (public)/       # 공개 페이지 (예: 로그인, 홈)
 │   ├─ (protected)/    # 인증 필요한 페이지 (예: 마이페이지, 대시보드)
 │   └─ api/            # 서버 액션/route handler
 │
 ├─ domains/            # ⭐ 도메인별 코드 모음
 │   ├─ book/
 │   │   ├─ components/ # 책 관련 UI 컴포넌트
 │   │   ├─ hooks/      # useBookQuery, useBookMutation
 │   │   ├─ services/   # axios/fetch 로직 (bookRepo)
 │   │   ├─ types.ts    # Book 관련 타입 정의
 │   │   └─ utils.ts    # 책 도메인에서만 쓰는 유틸
 │   ├─ user/
 │   │   ├─ components/
 │   │   ├─ hooks/
 │   │   ├─ services/
 │   │   └─ types.ts
 │   └─ ...
 │
 ├─ shared/             # 공용 유틸리티 &amp;amp; 디자인 시스템
 │   ├─ components/     # Button, Modal, Input 등 공용 UI
 │   ├─ hooks/          # useIntersectionObserver, useDebounce 등
 │   ├─ lib/            # axiosInstance, queryClient, config 등
 │   ├─ utils/          # formatDate, clsx, constants
 │   └─ types/          # 전역 타입
 │   └─ styles/         # 전역 스타일, tailwind config 확장&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;- Next App Router 구조 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router에서 라우팅 구조 설계를 맡아서 작업했는데, Next의 가상 라우팅, 라우팅 그룹, loader 등을 이해하며 작성했던 경험으로 Next의 라우팅 구조에 익숙해 질 수 있었던 것 같다. 특히 기억에 남는 부분은 middleware로 라우팅을 제어해 onboading 페이지로 유저를 이동시키고, 유저가 이동을 요청했던 주소로 redirect하는 기능을 구현한 것이 기억에 남는다.&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;- Next Server Level에서의 쿠키 관리와 CSRF 토큰 인증 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR에서 쿠키 관리를 어떻게 해야 하는지에 대해 이해할 수 있었고, CSRF 토큰을 이용한 인증 절차를 재시도 로직을 포함한 안정성 있는 방법으로 구현해 쿠키 관리와 보안에 대해서 이해할 수 있었다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 프로젝트에서는 팀 단위의 컴포넌트 재사용에 집중한 부분이 훨씬 컸었는데, 이번에 커뮤니티를 작업하면서 도메인이 팀원들과 많이 달라 공유하는 부분이 적었다. 컴포넌트만 공유하는 게 아니라 구조적으로 팀의 개발 경험을 향상시킬 수 있는 구조에 대해 더 고민하게 되었던 시간이었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;팀 전체의 fetch를 담당하는 인스턴스 유틸 함수 nextFetcher.ts, api.ts&lt;/li&gt;
&lt;li&gt;Provider 초기 설정&lt;/li&gt;
&lt;li&gt;백엔드 에러 코드를 한글로 바인딩한 serverErrorMessages.ts&lt;/li&gt;
&lt;li&gt;쿼리키를 한번에 관리하는 스토어 queryKeys.ts&lt;/li&gt;
&lt;/ol&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;h3 data-ke-size=&quot;size23&quot;&gt;- 캐시 관리와 Tanstack의 dehydrate 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 캐시를 관리하며 시행착오를 많이 겪었었다. 서버단, 클라이언트단 양쪽을 관리하는 게 너무 복잡했었는데, dehydrate 패턴을 적용하면서 데이터를 어떻게 관리해야 할지 어느 정도 이해하게 되었다.&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;- SEO 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next가 등장하게 된 배경이라고 할 수 있는 SEO Metadata 관리를 static으로도, dynamic으로도 관리할 수 있는 방법을 배웠다. 페이지 자체가 client component일 때 layout으로 책임을 넘겨 관리하는 방법, dynamic에서 query를 이용해 데이터를 씌우는 방법 등과 같이 유연하게 이용할 수 있는 방법을 배웠다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;- 로딩 UX 고려&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 사용하는 프로젝트였고, 자연스럽게 AI의 결과를 받는 로딩 시간에 팀 전체가 집중 할 수 밖에 없었다. 유저가 얼마나 기다려 줄 수 있는지, 로딩 시간은 유저에게 어떻게 피드백해야 할 지에 대해 고민할 수 있는 시간이었다.&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 data-ke-size=&quot;size26&quot;&gt;6. 마무리 소감&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;/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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 원팀으로 소통이 잘 됐던 것 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DDD 기반 아키텍쳐를 성공적으로 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 관심사 분리가 잘 된 코드들이 많았던 것 같음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 어려운 도메인을 성공적으로 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  아쉬운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AI 결과 데이터의 점수를 얻는 것이 너무 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 어려운 도메인에서 나왔던 관리의 어려움과 혼란&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 초기 개발 지연으로 인한 혼란&lt;/p&gt;</description>
      <category>Project/Re:Life</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/106</guid>
      <comments>https://yun-engene.tistory.com/106#entry106comment</comments>
      <pubDate>Wed, 29 Oct 2025 16:21:07 +0900</pubDate>
    </item>
    <item>
      <title>[Re:Life] next 사용하면서 학습한 내용 정리</title>
      <link>https://yun-engene.tistory.com/104</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;동적 라우팅은 page에서 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 라우팅을 사용할 때는 layout 레벨에서 사용하면 필요한 parameter를 받아오는 것이 불가능하기 때문에 page에서 관리해야 한다. 정적 라우팅은 상관 없지만, &amp;lsquo;일관성&amp;rsquo;을 갖추는 게 코드 가독성에 많은 영향을 미치기 때문에 하나로 통일해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;server action으로 로그인 시에는 Cookie를 받을 때 직접 관리해줘야 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 해석을 마치기 때문에 쿠키를 클라이언트로 직접 내려줘야 한다. RCC에서 fetch할 경우 set-cookie가 자동으로 쿠키를 처리해주지만, server action또는 RSC에서 쿠키를 수령해야 하는 경우에는 위와 같이 cookieStore에 직접 쿠키를 삽입해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;  // Set-Cookie 헤더로 자동으로 JSESSIONID가 설정됨
  const setCookieHeaders = res.headers.get(&quot;set-cookie&quot;);

  if (setCookieHeaders) {
    const jsessionid = setCookieHeaders.match(/JSESSIONID=([^;]+)/)?.[1];
    const cookieStore = await cookies();
    cookieStore.set(&quot;JSESSIONID&quot;, jsessionid || &quot;&quot;, {
      path: &quot;/&quot;,
      httpOnly: true,
      secure: process.env.NODE_ENV === &quot;production&quot;,
      sameSite: &quot;lax&quot;,
      maxAge: 60 * 60 * 24 * 30,
    });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정적 사이트를 생성했을 경우 cookie를 조회할 수 없다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cookie를 조회하고 사용하기 위해서는 반드시 SSG(정적 사이트 생성) 대신 SSR(서버 사이드 렌더링)로 처리해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const dynamic = &quot;force-static&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;force-static으로 강제로 정적 사이트를 생성했다면 별다른 오류 메세지 없이 cookie 조회에 undefind를 리턴하므로 주의할 것.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 라우팅 &lt;code&gt;()&lt;/code&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;code&gt;use&lt;/code&gt; hook을 통해 필요한 값을 추출할 수 있다.&lt;/h3&gt;
&lt;pre id=&quot;code_1759142302097&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'
import { use, useState, useEffect } from 'react'

interface Movie {
  Title: string
  Plot: string
}

export default function MovieDetails({
  params, // 동적 세그먼트
  searchParams // 쿼리스트링
}: {
  params: Promise&amp;lt;{ movieId: string }&amp;gt;
  searchParams: Promise&amp;lt;{ plot?: 'short' | 'full' }&amp;gt;
}) {
  const { movieId } = use(params)
  const { plot } = use(searchParams)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 컴포넌트에서 프리로드 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 컴포넌트에서 다음 페이지 로딩을 미리 하도록 처리할 수 있다. &lt;code&gt;router&lt;/code&gt;의 &lt;code&gt;prefetch&lt;/code&gt;를 이용해 라우팅 경로를 지정하면, 다음 페이지는 바로 로드될 수 있는 상태로 대기한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759142336460&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
import { useRouter } from 'next/navigation'
import Link from 'next/link'

// 생략..

export default function Header() {
  const pathname = usePathname()
  const router = useRouter()

  useEffect(() =&amp;gt; {
    router.prefetch('/movies')
  }, [router])

  return (
    &amp;lt;header className=&quot;flex items-center&quot;&amp;gt;
      {/* 생략.. */}
      &amp;lt;button
        className=&quot;rounded bg-gray-800 px-2 py-1 text-sm text-white transition-colors hover:bg-gray-700&quot;
        onClick={() =&amp;gt; router.push('/movies')}&amp;gt;
        Movies(Push)
      &amp;lt;/button&amp;gt;
    &amp;lt;/header&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 컴포넌트에 너무 집착하지 말자&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터렉션 요소, 애니메이션 요소가 늘어나면 클라이언트 컴포넌트가 늘어나는 건 당연하고, input 요소의 label과 같은 요소를 분리한다고 해서 번들 사이즈가 체감 될 정도로 줄어들지는 않는다. 필요에 따라 정적인 요소와 동적인 요소를 구분하고 RSC를 사용할지 RCC를 사용할지 구분하면 된다. 최적화에 정말 집착하고 싶다면 &lt;code&gt;Atomic Design&lt;/code&gt; 을 따를 것.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Project/Re:Life</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/104</guid>
      <comments>https://yun-engene.tistory.com/104#entry104comment</comments>
      <pubDate>Mon, 29 Sep 2025 19:33:38 +0900</pubDate>
    </item>
    <item>
      <title>[데브코스 프론트] PickItBook 프로젝트 회고</title>
      <link>https://yun-engene.tistory.com/103</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2849&quot; data-origin-height=&quot;1799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lcxlj/btsQBuhDbq3/rLqibsTzMBgOzPUB1fqKQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lcxlj/btsQBuhDbq3/rLqibsTzMBgOzPUB1fqKQk/img.png&quot; data-alt=&quot;프로젝트 메인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lcxlj/btsQBuhDbq3/rLqibsTzMBgOzPUB1fqKQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLcxlj%2FbtsQBuhDbq3%2FrLqibsTzMBgOzPUB1fqKQk%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;705&quot; height=&quot;445&quot; data-origin-width=&quot;2849&quot; data-origin-height=&quot;1799&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 메인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 주소 &lt;a href=&quot;https://pick-it-book.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pick-it-book.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758587975921&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PickitBook&quot; data-og-description=&quot;&quot; data-og-host=&quot;pick-it-book.vercel.app&quot; data-og-source-url=&quot;https://pick-it-book.vercel.app/&quot; data-og-url=&quot;https://pick-it-book.vercel.app/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://pick-it-book.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pick-it-book.vercel.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PickitBook&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pick-it-book.vercel.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;a href=&quot;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot;&gt;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757945539573&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - prgrms-fe-devcourse/FES-5-Project-TEAM-6: 룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한&quot; data-og-description=&quot;룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한 관심을 높이는 PickItBook 프로젝트입니다. - prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot; data-og-url=&quot;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQv8BP/hyZIY9Vmvy/cpIuv0gbslpGqiQgcCpTu0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bzesNo/hyZI00V0Hu/gZGXo2pvaEF6k14WyEqac0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQv8BP/hyZIY9Vmvy/cpIuv0gbslpGqiQgcCpTu0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bzesNo/hyZI00V0Hu/gZGXo2pvaEF6k14WyEqac0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - prgrms-fe-devcourse/FES-5-Project-TEAM-6: 룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한 관심을 높이는 PickItBook 프로젝트입니다. - prgrms-fe-devcourse/FES-5-Project-TEAM-6&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 프로젝트명&lt;/b&gt;: PickItBook&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 기간&lt;/b&gt;: 2025.08.22 ~ 2025.09.07&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;/li&gt;
&lt;li&gt;독서 습관 형성을 위한 게임화 요소 도입&lt;/li&gt;
&lt;li&gt;다양한 장르와 미션을 통한 새로운 독서 경험 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;949&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhLYjj/btsQAQFwcXC/JPCz3D1LwM2hddy6CGeRVk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhLYjj/btsQAQFwcXC/JPCz3D1LwM2hddy6CGeRVk/img.jpg&quot; data-alt=&quot;프로젝트 와이어프레임&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhLYjj/btsQAQFwcXC/JPCz3D1LwM2hddy6CGeRVk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhLYjj%2FbtsQAQFwcXC%2FJPCz3D1LwM2hddy6CGeRVk%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;1280&quot; height=&quot;949&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;949&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 와이어프레임&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;12 (1).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pQliU/btsQAYKqrSO/FTID1uEqkttQEE4kU5kg00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pQliU/btsQAYKqrSO/FTID1uEqkttQEE4kU5kg00/img.png&quot; data-alt=&quot;프로젝트 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pQliU/btsQAYKqrSO/FTID1uEqkttQEE4kU5kg00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpQliU%2FbtsQAYKqrSO%2FFTID1uEqkttQEE4kU5kg00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;12 (1).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&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;룰렛 UI&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;책 표지가 슬롯처럼 돌아가는 애니메이션(Framer Motion, Lottie 등 활용)&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;책 데이터 소스 및 API&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Books API로 표지・메타데이터 자동 불러오기&lt;/li&gt;
&lt;li&gt;국내 도서관 소장 여부: 국립중앙도서관 OpenAPI, 도서관 정보나루 API&lt;/li&gt;
&lt;li&gt;ISBN 기반 데이터 통합&lt;/li&gt;
&lt;li&gt;전자책 보유여부: 각 도서관/사이버도서관 API 확인&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;장르, 연령대별 선호도, 성별&lt;/li&gt;
&lt;li&gt;키워드 기반 검색, 제목 검색, 작가명 검색&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;룰렛 결과와 함께 난이도별 미션(예: 리뷰 작성하기, 3줄 요약하기)을 랜덤으로 제시&lt;/li&gt;
&lt;li&gt;미션 완료/진행 상태 트래킹, 뱃지 획득&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인 서재 &amp;amp; 업적&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;대시보드에 업적/진행상황 보여줌&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;월간 독서량/미션 추이/장르 편중(Rechart, Chart.js 등)&lt;/li&gt;
&lt;li&gt;독서 성향, 인기 도서 통계 시각화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리뷰/3줄요약&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리뷰/별점/3줄 요약 등록(이미지, 텍스트) &amp;rarr; 책 정보 페이지에 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 팀 플래그&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 새로운 기술 적용해보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 바닐라로 기능을 구현해보는 데 집중했다면, 자주 사용되는 라이브러리를 공부하고 적용해보기로 결정했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 라이브러리 사용에는 이유가 있어야 한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Zustand&lt;/code&gt; 많이 쓰니까. &lt;code&gt;Tanstack Query&lt;/code&gt; 많이 쓰니까. 같은 이유로 기술을 선정하는 게 아니라 &lt;code&gt;Zustand&lt;/code&gt;의 상태 관리, 트리거 로직파악하고 적용했을 때 무슨 장점이 있는지, 비슷한 라이브러리 사이에서 왜 이것을 선택해야 하는지를 팀 단위 회의로 적용하기로 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 컴포넌트 재사용성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재사용될 가능성이 있는 컴포넌트를 주기적으로 공유하고, 반복적인 코드 작성을 줄이며 생산성을 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 팀 플래그를 팀 단위로 정했다.&lt;/p&gt;
&lt;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로젝트 사전준비&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 기술선정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tanstack Query&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 사용하기 때문에 &lt;code&gt;staleTime&lt;/code&gt;을 이용한 요청량 제어가 유효하다고 판단했다. 옵션을 사용해 쿼리 제어가 간편하고 요청 상태에 따른 화면 제어, 성공, 실패 동작을 구분해 폴백과 데이터 갱신에 강점이 있어 Tanstack Query를 선정하게 되었다.&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;GSAP vs Framer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션을 관리하는 데 있어 &lt;code&gt;Framer&lt;/code&gt;와 &lt;code&gt;Gsap&lt;/code&gt; 사이에서 어떤 것을 사용할지 고민했는데, &lt;code&gt;framer&lt;/code&gt;는 기본적인 애니메이션 구현시 쉽게 사용할 수 있고, 리액트 친화적이라 선언형으로 다룰 수 있는 장점이 있고, Gsap은 명령형이었기 때문에 사용해봤던 라이브러리인 &lt;code&gt;Gsap&lt;/code&gt;을 사용할지, &lt;code&gt;Framer&lt;/code&gt;를 적용할지 고민했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 내부에서 회의한 결과, 스크롤 제어 애니메이션에 &lt;code&gt;Gsap&lt;/code&gt;이 장점이 있고, 상대적으로 익숙했기 때문에 &lt;code&gt;Gsap&lt;/code&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;Three.js&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;북마크된 책을 쌓아서 내 서재에 책을 얼마나 담았는지 확인하는 기능을 구상했는데, 북마크된 책이 많아짐에 따라 2d에서는 보여줄 수 있는 한계가 존재했다. 그래서 자유로운 시점 변경으로 쌓인 책을 확인할 수 있도록 하기 위해 &lt;code&gt;Three.js&lt;/code&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;husky&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;&lt;b&gt;zustand&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 상태 관리를 위해 선택하게 되었다. 그러나 전역 상태의 위험성이 있어 유저 정보, 루트 메뉴 제어와 같은 공통적인 부분만 스토어로 관리하고 일반적인 &lt;code&gt;fetching cach&lt;/code&gt;e는 &lt;code&gt;Tanstack Query&lt;/code&gt;를 이용해 관리하기로 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 기능 명세화 / 요구사항 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획서에 따라 프로젝트의 기능을 명세화하고 요구사항을 정의했다. MVP로 가장 먼저 구현해야 할 기능들을 정의하고 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0&quot;&gt;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757945331596&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;PickItBook 요구사항&quot; data-og-description=&quot;ABCDEFGHIJKLMNOPQRSTUVWXY카테고리요구사항설명릴리즈룰렛필터 적용 후보 도서 추출장르 / 분량 / 인기 / 연령 / 성별 / 읽음제외 적용MUSTMVP룰렛스핀 애니메이션2&amp;ndash;4초 가속-감속, 마지막 300ms 하이라이&quot; data-og-host=&quot;docs.google.com&quot; data-og-source-url=&quot;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0&quot; data-og-url=&quot;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0&amp;amp;usp=embed_facebook&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AC3Ry/hyZJuMTxDk/5CgZkQxrX750ZhZianeKV0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AC3Ry/hyZJuMTxDk/5CgZkQxrX750ZhZianeKV0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PickItBook 요구사항&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ABCDEFGHIJKLMNOPQRSTUVWXY카테고리요구사항설명릴리즈룰렛필터 적용 후보 도서 추출장르 / 분량 / 인기 / 연령 / 성별 / 읽음제외 적용MUSTMVP룰렛스핀 애니메이션2&amp;ndash;4초 가속-감속, 마지막 300ms 하이라이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터베이스 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 구조는 &lt;code&gt;3NF&lt;/code&gt; 기반으로 설계했는데, 프로젝트를 진행하면서 조금 불안정해진 면이 있다. ( 잘 관리했어야 했는데 아쉬운 부분이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 supabase로 관리했고, 조회는 View 또는 RPC 삽입은 테이블 직접 삽입이라는 규칙을 정해 데이터베이스 규칙을 관리했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 내가 구현한 파트&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 시연 영상, 기여 목록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 오래 작업한 파트 시연 영상만 가져와봤다.&lt;/p&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&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;code&gt;Swiper&lt;/code&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;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;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 진행 관리&lt;/li&gt;
&lt;li&gt;프로젝트 초기 설정&lt;/li&gt;
&lt;li&gt;초기 데이터베이스 생성&lt;/li&gt;
&lt;li&gt;Root 컴포넌트
&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;ScrollTopButton 초기 디자인, 기능 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Search Page
&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;SearchList 관련 컴포넌트 작성&lt;/li&gt;
&lt;li&gt;검색 기능 추가&lt;/li&gt;
&lt;li&gt;검색 결과 리스트 &amp;lsquo;Grid&amp;rsquo;, &amp;lsquo;Line&amp;rsquo; mode 변경 구현&lt;/li&gt;
&lt;li&gt;Tanstack Query를 이용한 useBookFetching 작성&lt;/li&gt;
&lt;li&gt;페이지 네비게이션 컴포넌트 작성&lt;/li&gt;
&lt;li&gt;Filter 컴포넌트 구현, Gsap 애니메이션 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Detail Page
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MissionPartition 컴포넌트 작성 - 미션 목록&lt;/li&gt;
&lt;li&gt;BookDataPratition 컴포넌트 작성 - 책 상세정보&lt;/li&gt;
&lt;li&gt;UserScorePartition 컴포넌트 작성 - 리뷰 점수 종합&lt;/li&gt;
&lt;li&gt;ReviewWritePartition 컴포넌트 작성 - 리뷰 작성&lt;/li&gt;
&lt;li&gt;ReviewListPartition 컴포넌트 작성 - 리뷰 목록
&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fetching, Pending 상태를 이용한 Load 애니메이션으로 UX 개선&lt;/li&gt;
&lt;li&gt;모든 데이터 QueryKey 무효화를 이용해 Stale Time 초기화해 서버의 변경사항 즉시 반영되도록 작성&lt;/li&gt;
&lt;li&gt;좋아요, 미션 수령에 대해 낙관적 업데이트 적용 ( 실패 시 Fallback )&lt;/li&gt;
&lt;li&gt;재사용성 고려, Partition으로 각 섹션을 분리, 재사용될 Bookmark, RatingStar 등의 컴포넌트 유연하게 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mission 트리거, 이벤트 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미션 관리 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;task_templates(미션 데이터)&lt;/li&gt;
&lt;li&gt;task_bundle(미션 묶음)&lt;/li&gt;
&lt;li&gt;task_bundle_items(묶음 아이템)&lt;/li&gt;
&lt;li&gt;user_tasks(유저가 수령한 미션)&lt;/li&gt;
&lt;li&gt;user_task_event_log(유저 활동 이벤트 로그&lt;/li&gt;
&lt;li&gt;user_book_task_assignment(유저가 수령한 번들)&lt;/li&gt;
&lt;li&gt;task_reward(수령한 보상 목록) 작성&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;api_assign_book_tasks- 결정적 난수로 미션 목록을 뽑아 유저에게 부여합니다. (task, bundle 등 처리)&lt;/li&gt;
&lt;li&gt;api_process_event - 유저의 행동에 대한 이벤트 로그를 부여하고 트리거 로직에 따라 보상 여부를 판별합니다. ( 리뷰 작성, 북마크 추가 등 )&lt;/li&gt;
&lt;li&gt;getBundleIdByISBN - isbn에 대한 미션 번들 번호를 받아옵니다. 각각의 이벤트 ( 리뷰 작성, 북마크 추가 ) 등에 대해 이벤트 로직 추가 또는 트리거 부여&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;realtime 구독 추가 task_reward에 대해 Realtime 로직을 추가하고 로그인한 유저가 미션을 완료했을 경우 콜백을 실행하도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AWS EC2 이용한 Express + Nginx 프록시 서버 구현
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;White List 관리를 위해 Vercel로 중계하는 프록시 서버 구현&lt;/li&gt;
&lt;li&gt;로컬 호출도 Proxy 호출하도록 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전부터 DB 관련 작업, 백엔드 관련 작업을 많이 작업했어서 프론트엔드 학습하는 과정에서 좀 벗어나지 않나... 하는 고민을 했는데, 프로젝트 자체가 불안정한 것이 너무 마음에 안 들어서 이번에도 백엔드쪽 작업을 섞어서 작업하게 되었다.&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;2. 검색 기능 구현&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1905&quot; data-origin-height=&quot;905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAwkQT/btsQBoaOGkC/4qFvRCxUzxTCP2aCunaXA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAwkQT/btsQBoaOGkC/4qFvRCxUzxTCP2aCunaXA0/img.png&quot; data-alt=&quot;검색 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAwkQT/btsQBoaOGkC/4qFvRCxUzxTCP2aCunaXA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAwkQT%2FbtsQBoaOGkC%2F4qFvRCxUzxTCP2aCunaXA0%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;1905&quot; height=&quot;905&quot; data-origin-width=&quot;1905&quot; data-origin-height=&quot;905&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검색 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장서 데이터는 외부 API를 이용해 구현했다. 단순하게 외부 API에 요청해 데이터를 화면에 렌더링하기만 하면 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 response로 들어오는 데이터도 생소해 타입 정의하기도 어려웠고, &lt;code&gt;Tanstack Query&lt;/code&gt;를 처음으로 적용한 기능이라 조금 헤맸다. 결과적으로 데이터도 잘 받아왔고, &lt;code&gt;Tanstack Query&lt;/code&gt;의 &lt;code&gt;useQuery&lt;/code&gt;도 성공적으로 적용해 &lt;code&gt;staleTime&lt;/code&gt;과 &lt;code&gt;refetchOnWindowFocus&lt;/code&gt;와 같은 옵션들을 이해하고 사용할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 페이지를 구현하면서 제일 신경 쓴 부분은 아주 단순하게 펼쳐지는 필터 드롭다운이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;132&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iypu6/btsQBvHFx9u/8Vh9rr2Cr3KzP3xq2m0xrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iypu6/btsQBvHFx9u/8Vh9rr2Cr3KzP3xq2m0xrK/img.png&quot; data-alt=&quot;아주 간단한 기능이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iypu6/btsQBvHFx9u/8Vh9rr2Cr3KzP3xq2m0xrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIypu6%2FbtsQBvHFx9u%2F8Vh9rr2Cr3KzP3xq2m0xrK%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;132&quot; height=&quot;211&quot; data-origin-width=&quot;132&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아주 간단한 기능이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 컴포넌트가 재사용될 수 있으면서, 서브메뉴가 있든 없든 작동하고, 조금 더 인터렉티브하게 만들고 싶었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;PickitBook - Chrome 2025-09-15 21-31-25.gif&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9LXc6/btsQBo9IJYs/nuewNsqaQPDKtmrbEoD2P0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9LXc6/btsQBo9IJYs/nuewNsqaQPDKtmrbEoD2P0/img.gif&quot; data-alt=&quot;팀원 페이지에 있는 필터 컴포넌트다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9LXc6/btsQBo9IJYs/nuewNsqaQPDKtmrbEoD2P0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b9LXc6/btsQBo9IJYs/nuewNsqaQPDKtmrbEoD2P0/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;639&quot; height=&quot;343&quot; data-filename=&quot;PickitBook - Chrome 2025-09-15 21-31-25.gif&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팀원 페이지에 있는 필터 컴포넌트다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 gif에서 나오는 필터 컴포넌트와 같은 컴포넌트이다. 컴포넌트의 아이템은 대분류와 소분류 아이템으로 나뉘는데, 0부터 10 단위로 0, 10, 20 ...은 대분류이고, 1, 11, 12와 같은 아이템은 10단위로 끊긴 대분류의 내부 아이템들이다. 대분류만 필요할 경우 10단위로 끊긴 아이템들만 사용하면 서브 아이템은 표시되지 않고, 서브 아이템에 대해서 &lt;code&gt;gsap&lt;/code&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;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// 상위 컴포넌트에서 받을 수 있는 데이터
{top:{code:'20', value:'사회과학'}, bottom:{code:'21', value:'통계학'}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 성공적으로 다른 팀원의 페이지에 정착한 공유 컴포넌트를 만들 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 상세 페이지&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q2ZE9/btsQzFYNIFF/LWvMs429sig1SNntgqufk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q2ZE9/btsQzFYNIFF/LWvMs429sig1SNntgqufk1/img.png&quot; data-alt=&quot;상세 페이지 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q2ZE9/btsQzFYNIFF/LWvMs429sig1SNntgqufk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ2ZE9%2FbtsQzFYNIFF%2FLWvMs429sig1SNntgqufk1%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;1904&quot; height=&quot;905&quot; data-origin-width=&quot;1904&quot; data-origin-height=&quot;905&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상세 페이지 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 페이지는 책 정보들을 외부 API로부터 불러와 책 정보를 보여주도록 했다. 관련 미션, 북마크, 평점, 리뷰와 같은 유저의 활동에 의한 데이터는 &lt;code&gt;supabase&lt;/code&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;&lt;b&gt;첫째로&lt;/b&gt;, 상위 컴포넌트를 &lt;code&gt;dumb&lt;/code&gt;하게 유지하고 하위 컴포넌트에서 &lt;code&gt;mutation&lt;/code&gt;과 조건 처리와 같은 로직을 수행하도록 하는 것이다. Container/Presentational Pattern을 이용해보고 싶었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9MhHP/btsQBn33dF7/7JJ1Ao1lrGGzKyn6mMpwg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9MhHP/btsQBn33dF7/7JJ1Ao1lrGGzKyn6mMpwg0/img.png&quot; data-alt=&quot;Tanstack Query를 사용하니 하위 요소에서 fetching 시켰으면 어땠을까 하는 아쉬움이 남는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9MhHP/btsQBn33dF7/7JJ1Ao1lrGGzKyn6mMpwg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9MhHP%2FbtsQBn33dF7%2F7JJ1Ao1lrGGzKyn6mMpwg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;417&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Tanstack Query를 사용하니 하위 요소에서 fetching 시켰으면 어땠을까 하는 아쉬움이 남는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상단 Page 컴포넌트는 단순히 Query만 수행하고 하위 요소에서 &lt;code&gt;Mutation&lt;/code&gt;, &lt;code&gt;조건부 처리&lt;/code&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;, 각각의 동작에 대해 데이터가 연동성을 가질 수 있게 하는 것이었다. 리뷰를 작성했다면 그 페이지에서 즉시 리뷰가 갱신되어야 하고, 미션을 완료했다면 그 즉시 미션 목록에서 미션 완료로 갱신되어야 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;PickitBook - Chrome 2025-09-09 16-14-44.gif&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDtvdo/btsQydvjiBM/UO3VEb6SsgidD0IQKTtJGK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDtvdo/btsQydvjiBM/UO3VEb6SsgidD0IQKTtJGK/img.gif&quot; data-alt=&quot;즉시 연동되는 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDtvdo/btsQydvjiBM/UO3VEb6SsgidD0IQKTtJGK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bDtvdo/btsQydvjiBM/UO3VEb6SsgidD0IQKTtJGK/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;527&quot; height=&quot;283&quot; data-filename=&quot;PickitBook - Chrome 2025-09-09 16-14-44.gif&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;즉시 연동되는 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tanstack Query&lt;/code&gt;의 &lt;code&gt;useQueryClient&lt;/code&gt;를 이용해 리뷰를 작성할 경우 목록에 즉시 추가되고, 리뷰 관련 query들의 &lt;code&gt;staleTime&lt;/code&gt;을 즉시 무효화시키도록 &lt;code&gt;invalidQuerys&lt;/code&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 각 섹션에 대해 &lt;code&gt;Partition&lt;/code&gt;으로 네이밍한 것이 마음에 안 든다. 대체로 &lt;code&gt;Section&lt;/code&gt;이라고 네이밍하더라... 코드 자체를 스타일이라고 할 수는 있어도 일반적으로 통용되는 네이밍 방식을 쓰는 게 더 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 상위 요소에서 Query를 수행할 필요 없이 &lt;code&gt;Tanstack Query&lt;/code&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;4. 트리거 이벤트 관리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 초기에 러프하게 기능 구현을 생각해두고 있을 때는 &lt;code&gt;Zustand Store&lt;/code&gt;에서 트리거 이벤트를 관리하면 되지 않을까? 하는 생각을 하고 있었다. 그런데 실제 구현 계획을 세워보니 &lt;code&gt;Store&lt;/code&gt;로 이벤트를 관리하는 데에 문제가 있어 보였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (29) 1 (1).png&quot; data-origin-width=&quot;3450&quot; data-origin-height=&quot;1434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRD28W/btsQzbjkbE4/nRrKkoTKsLr6R6jylLCtEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRD28W/btsQzbjkbE4/nRrKkoTKsLr6R6jylLCtEK/img.png&quot; data-alt=&quot;팬아웃이 커질 것 같음.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRD28W/btsQzbjkbE4/nRrKkoTKsLr6R6jylLCtEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRD28W%2FbtsQzbjkbE4%2FnRrKkoTKsLr6R6jylLCtEK%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;3450&quot; height=&quot;1434&quot; data-filename=&quot;image (29) 1 (1).png&quot; data-origin-width=&quot;3450&quot; data-origin-height=&quot;1434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팬아웃이 커질 것 같음.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차적인 문제는 클라이언트 각각의 컴포넌트의 동작에 대해 이벤트 처리 로직을 추가해줘야 하기 때문에 로직이 흩어지는 문제가 있다.&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;그러니 서버 쪽에서 관리하던가, 미션 로직을 단순화할 필요가 있었다. 가장 먼저 생각했던 건 서버쪽으로 미션 발급과 미션 트리거 관리를 전부 넘겨버리는 거였다. 이번 프로젝트에서는 서버쪽 작업을 너무 하기 싫었어서 클라이언트쪽에서 뾰족한 방법이 없는지 강사님과 멘토님께 자문을 구했다.&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;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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미션, 도전과제 트리거를 어떻게 관리해야 할까요?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로그램적 문제보다는 구현 방향성에 대한 질문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희는 유저가 뽑은 책에 대해 조건에 따라 달성되는 미션(읽은 책에 대해서 리뷰 남기기, 책 요약 남기기, 인상깊은 구문 남기기 등)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 유저의 활동에 대한 조건에 따라 달성되는 도전과제(책 50권 서재에 넣기, 미션 100개 달성하기, 리뷰 50개 남기기)가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 클라이언트단에서 조건을 관리하려고 하니 트리거 동작을 위해 유저 정보 스토어에 담아야 할 정보가 점점 늘어나야 하고, 모든 요청에 따라서 유저 정보 스토어를 항상 관리해야 하는 문제와 각 컴포넌트에 대해서 트리거 로직이 흩어져야 하는 문제가 있습니다. ( 팬아웃이 너무 커질 것 같다는 예상)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[ 팬아웃 사진 ]&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 지금 고민하고 있는 부분은 미션이나 도전과제의 종류를 줄이고 트리거 로직을 최소화할지, 유저가 모든 미션에 대해서 완료 처리를 직접 하도록 할지 고민입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트리거 로직을 최소화할 경우에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드에서 구현해야 할지(supabase에서)&lt;/li&gt;
&lt;li&gt;클라이언트단에서 구현해야 할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에 대한 고민이 있고&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;유저가 미션에 대해서 완료 처리를 직접 해야할 경우에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저가 즉각적인 피드백을 못 느낌&lt;/li&gt;
&lt;li&gt;컨텐츠의 클라이언트 의존성이 강해짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에 대한 고민이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트 트리거로 작동한다면 유저에게 즉각적으로 피드백(애니메이션, 모달 등)을 보여주는 것을 기대했는데, 관리가 너무 어려울 것 같아서 질문드립니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB 구조는 이렇습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[DB 구조 사진]&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;관련 컴포넌트 구조는 이렇습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;21 (1).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOTaoh/dJMcafLEGFj/XHCtSHHxIT1QMlsypNlxK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOTaoh/dJMcafLEGFj/XHCtSHHxIT1QMlsypNlxK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOTaoh/dJMcafLEGFj/XHCtSHHxIT1QMlsypNlxK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOTaoh%2FdJMcafLEGFj%2FXHCtSHHxIT1QMlsypNlxK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;21 (1).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure data-ke-type=&quot;image&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서 나온 대안을 요약하면 다음과 같았다. 1, 2는 구현 부하를 줄이는 방법이었고, 3은 원인을 제거하는 방법이었다. 강사님과 멘토님 모두 서버에서 구현하는 방법을 제안해주셨고, 원인 제거를 위해 서버에서 구현하기로 결정했다.&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;supabase는 Postgre기반이어서 RPC를 생성해 관리하도록 했다. 필요한 함수는 세 가지였다.&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;1. api_list_book_missions&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지에 들어갈 때마다, 새로고침할 때마다 미션 목록을 랜덤으로 가져오는 것은 어색하다. 책에는 &lt;b&gt;isbn13&lt;/b&gt;이라는 고유한 키값이 있다. 이 키에 해당하는 미션을 확정적으로 가져오기 위해 미션 번들의 id를 &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;2. api_assign_book_tasks&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 &lt;b&gt;isbn13&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;3. api_process_event&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;이벤트 분류별 관리를 위해 분리한 함수 api_rule_count_event, api_rule_checklist, api_rule_streak도 있지만, 이 셋은 3번 함수 api_process_event에 의해 호출된다.&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;1. 미션 목록, 미션 번들, 번들 목록, 이벤트 로그, 유저 미션, 보상 목록 테이블로 미션을 관리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JndEL/btsQzAcs5PJ/X3tmQcUoFwnXeE5Vk9gokK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JndEL/btsQzAcs5PJ/X3tmQcUoFwnXeE5Vk9gokK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JndEL/btsQzAcs5PJ/X3tmQcUoFwnXeE5Vk9gokK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJndEL%2FbtsQzAcs5PJ%2FX3tmQcUoFwnXeE5Vk9gokK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;17.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&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;2. 이벤트 로그에 들어온 정보를 판별해 유저 미션 완료 처리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;18.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Lyz1/btsQArTun7a/0Ck7KZ9ejTBsF3VqemKgZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Lyz1/btsQArTun7a/0Ck7KZ9ejTBsF3VqemKgZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Lyz1/btsQArTun7a/0Ck7KZ9ejTBsF3VqemKgZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Lyz1%2FbtsQArTun7a%2F0Ck7KZ9ejTBsF3VqemKgZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;18.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&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;3. 클라이언트의 realtime event - 소켓 이벤트를 이용해 변경을 파악한 후 렌더링&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;19.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo1iR5/btsQxIvsnNj/t0sOZniEChCWkDKvzAQke1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo1iR5/btsQxIvsnNj/t0sOZniEChCWkDKvzAQke1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo1iR5/btsQxIvsnNj/t0sOZniEChCWkDKvzAQke1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo1iR5%2FbtsQxIvsnNj%2Ft0sOZniEChCWkDKvzAQke1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;19.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Nginx, Express를 이용한 Proxy 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API를 사용하면서 Vercel 배포를 사용할 때의 가장 큰 문제는 외부 API 주소에 화이트리스트를 등록하는 게 불가능하다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ShockedSurprisedGIF.gif&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zLnzY/btsQAU2q68b/PGTGTDQTRoXgwZjoxek80K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zLnzY/btsQAU2q68b/PGTGTDQTRoXgwZjoxek80K/img.gif&quot; data-alt=&quot;이걸 배포하는 날에 발견했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zLnzY/btsQAU2q68b/PGTGTDQTRoXgwZjoxek80K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/zLnzY/btsQAU2q68b/PGTGTDQTRoXgwZjoxek80K/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;226&quot; height=&quot;399&quot; data-filename=&quot;ShockedSurprisedGIF.gif&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이걸 배포하는 날에 발견했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 동적 IP 주소를 사용하기 때문에 요청하는 IP가 고정적이지 않다. 따라서 요청하는 IP주소가 지속적으로 변경되는데, 이런 경우 문제는 프로젝트가 언제든 멈출 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (8) 1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LSe3R/btsQAHWvwQ1/8KkBZirpMXK3x0Eu1EAJYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LSe3R/btsQAHWvwQ1/8KkBZirpMXK3x0Eu1EAJYk/img.png&quot; data-alt=&quot;WhiteList 불가능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LSe3R/btsQAHWvwQ1/8KkBZirpMXK3x0Eu1EAJYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLSe3R%2FbtsQAHWvwQ1%2F8KkBZirpMXK3x0Eu1EAJYk%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;1280&quot; height=&quot;203&quot; data-filename=&quot;image (8) 1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;WhiteList 불가능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 그래도 기본적으로 AWS는 쓸 줄 알았고, 아직 무료 플랜도 남아있는 상태여서 서버에 Proxy를 구현해 사용할 수 있었다. 프로젝트 마감 시점에 가까웠었기 때문에 단시간에 처리해야 했었다. 그래서 급하게 처리할 수 밖에 없어 Nginx를 붙여 두기는 했지만 https요청으로 받는 것도 아니었고, certbot에서 인증서도 안 받고 급하게 구현해 어떻게든 문제를 해결할 수 있었다. ( 많이 아쉽다 )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmMI98/btsQyENa85W/7MTZlNuakURCwEeF20ikN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmMI98/btsQyENa85W/7MTZlNuakURCwEeF20ikN1/img.png&quot; data-alt=&quot;요청 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmMI98/btsQyENa85W/7MTZlNuakURCwEeF20ikN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmMI98%2FbtsQyENa85W%2F7MTZlNuakURCwEeF20ikN1%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;1161&quot; height=&quot;267&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요청 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy 요청을 보내는 김에 supabase 요청 처리도 Proxy 서버를 통해 요청하고 싶긴 했지만 마감 직전에 급하게 구현해서 빠르게 필요한 것만 처리할 수밖에 없었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 개인적인 성장&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Tanstack Query 학습 및 성공적인 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션들에 대해 학습한 것은 물론, &lt;code&gt;Tanstack Query&lt;/code&gt;의 &lt;code&gt;caching&lt;/code&gt; 전략과 쿼리 무효화에 대해서 학습할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 직접 부딪혀보면서 어떤 패턴이 더 좋은가에 대해서 생각하게 되었다. 아래는 내가 프로젝트 진행하면서 &lt;code&gt;Tanstack Query&lt;/code&gt;에 대해서 정리한 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tanstack Query의 강력한 점을 꼽자면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;staleTime과 gcTime을 이용한 캐싱, 최신화 관리&lt;/li&gt;
&lt;li&gt;refetchOnWindowFocus와 같은 브라우저 레벨의 유저 동작에서의 제어 옵션&lt;/li&gt;
&lt;li&gt;select 등을 이용한 return 데이터 가공&lt;/li&gt;
&lt;li&gt;isLoading, isPending, isError 등과 같은 다양한 위치에서 제공되는 상태&lt;/li&gt;
&lt;li&gt;defaultSetting으로 전역적인 쿼리 클라이언트 동작 제어&lt;/li&gt;
&lt;li&gt;onMutate, isError Callback 등으로 이벤트 발생 타이밍마다 작동하는 콜백 제어로 낙관적 업데이트 가능&lt;/li&gt;
&lt;li&gt;queryClient 직접 제어를 통한 쿼리 무효화, refetching 등 직접 제어 가능&lt;/li&gt;
&lt;li&gt;Infinity 제어에 특히 강력한 InfinityQuery과 같은 특정 쿼리 동작에 특화된 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 써본 동작들만 해도 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 박치기 해보면서 작성한 코드와 최종 정착하게 된 코드를 정리해보자&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Mutation&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완전 초기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// useBookmarkFetching.ts
// 북마크 토글을 처리합니다.
export const useToggleBookmark = (isbn13: string | undefined, uid?: string) =&amp;gt; {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: () =&amp;gt; {
      if (!isbn13) throw new Error(&quot;isbn13 is required&quot;);
      return bookmarkRepo.toggleBookmark(isbn13);
    },
    onSuccess: () =&amp;gt; {
      queryClient.invalidateQueries({ queryKey: [&quot;bookmark&quot;, isbn13] });
      if (uid) queryClient.invalidateQueries({ queryKey: [&quot;bookmarks&quot;, uid] });
    },
  });
};&lt;/code&gt;&lt;/pre&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;Mutate Function 호출 시점에 파라미터를 받고 있음&lt;/li&gt;
&lt;li&gt;이 상태면 mutation 함수 하나에 대해서 항상 id값을 호출해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;  // BookDetailPartition.tsx (파일명도 Section으로 변경하는 게 자연스러워 보인다.)
  ...
    // 호출 시점
  const { mutate: toggleBookmark, isPending: togglePending } =
    useToggleBookmark(id);
 ...

    return( ...
       &amp;lt;button onClick={toggleBookmark}&amp;gt;북마크 버튼&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상태와 같이 각각의 아이템에 대한 mutate 함수를 호출할 때마다 새로 받아야 하므로 좋지 않음. 리스트 아이템이라면 매 리스트 아이템마다 &lt;code&gt;useToggleBookmark&lt;/code&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제를 인식하고 &lt;code&gt;mutationFn&lt;/code&gt; 콜백에 파라미터를 부여해 하나의 &lt;code&gt;mutate&lt;/code&gt; 함수를 이용해 여러 아이템을 통제하도록 구현&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;//useReviewFetching.ts
// 파일과 함께 리뷰를 게시합니다 ( 파일 없어도 상관 없음 )
export const useSetReviewWithFiles = () =&amp;gt; {
  const qc = useQueryClient();

  return useMutation({
    mutationKey: [&quot;review&quot;, &quot;create&quot;],
    mutationFn: (vars: SetReviewType) =&amp;gt; reviewRepo.setReviewWithFile(vars),
    retry: 0,
    onSuccess: (_newReview, vars) =&amp;gt; {
      logicRpcRepo.setProcessEvent(&quot;REVIEW_CREATED&quot;, {
        book_id: vars.isbn13,
        review_id: _newReview.id,
      });
      qc.invalidateQueries({ queryKey: [&quot;review&quot;, &quot;byIsbn&quot;, vars.isbn13] });
      qc.invalidateQueries({ queryKey: [&quot;review&quot;, &quot;byUser&quot;, vars.uid] });
    },
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 시점마다 &lt;code&gt;vars&lt;/code&gt;를 부여하여 리스트 아이템에 대해 매번 새로 생성해야 하는 문제 해결&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;// reviewWritePartition.tsx
const handleSubmit = (e: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    ...
    const { isbn13, bookname: title } = data.book;
    mutate({
      isbn13,
      title,
      content,
      score: rating,
      uid: id,
      image_file: image,
    });

        ...
  };&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;/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;&lt;b&gt;최종 정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 동작만 취하면서 기본 옵션은 &lt;code&gt;default&lt;/code&gt;에 종속적으로 변경해 반복적인 &lt;code&gt;options&lt;/code&gt; 소요를 줄이고 &lt;code&gt;onMutate&lt;/code&gt;와 같은 콜백 동작, 옵션들을 쿼리를 사용하는 컴포넌트에서 주입하도록 변경해 유연성을 개선했다.&lt;/p&gt;
&lt;p 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;state와 같은 상태 변경이나 컴포넌트 요소 통제 동작을 주입 할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 컴포넌트에서 이 동작을 사용한다고 했을 때 컴포넌트에 따른 유연한 동작을 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// useSummaryFetching.ts
export const useSetSummary = (
  options?: UseMutationOptions&amp;lt;unknown, Error, SetSummaryType&amp;gt;
) =&amp;gt; {
  return useMutation({
    mutationKey: [&quot;setSummary&quot;],
    mutationFn: async ({ summary, isbn13 }: SetSummaryType) =&amp;gt; {
      const row = await summaryRepo.setSummary(summary, isbn13);
      logicRpcRepo.setProcessEvent(&quot;SUMMARY_CREATED&quot;, {
        book_id: isbn13,
        summary_id: String(row.id),
      });

      return row;
    },
    ...options,
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출해 사용할 때는 이런 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; // SummaryPartition.tsx
  const { mutate } = useSetSummary({
    onSuccess: () =&amp;gt; {
        // 사이드 이펙트 동작을 직접 부여
      qc.invalidateQueries({ queryKey: [&quot;getSummary&quot;, isbn13] });
    },
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점만 있는 것은 아니고, 일장일단이 있다. 한번만 호출될 단순한 동작이거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 컴포넌트에서 호출하더라도 동작이 같아야 할 경우 로직의 응집성이 흩어지거나 중복 작성할 수 있는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 일관성 있는 동작을 부여하기 위해 대부분의 useMutate에서 이 동작을 사용할 것 같다. options를 optional로 부여했으니 단순 조회 같은 추가 로직 부여가 필요 없는 경우 옵션을 추가적으로 부여할 필요는 없을 것 같다.&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;2. 팀 리더로서 소통 주도&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;3. 재사용성 있는 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 서술한 filter 컴포넌트 뿐만 아니라 목록, 리뷰, 내 서재, 책 정보 페이지에서 재사용 될 수 있는 평점 정보 RatingStar 컴포넌트, Bookmark 컴포넌트 등을 작성했다. 이 컴포넌트들을 작성하면서 재사용성 있는 컴포넌트를 작성하는 방법에 대해 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 작성 초기에 &lt;code&gt;Props&lt;/code&gt;를 모든 상황에 대비해 작성하기보다는 팀원에게 필요한 상황에 대해서 물어보고 그 상황에 맞는 컴포넌트를 만들고, 추가적으로 필요한 상황이 올 경우 지속적으로 관리하며 더 넓은 상황에 대처할 수 있는 컴포넌트가 되어가는 것이 중요한 것 같다고 느꼈다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 좋았던 점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 생산성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 공유, 필요한 라이브러리 적극적으로 도입으로 빠르게 기능을 만들어낼 수 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 소통 / 협업&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원활한 커뮤니케이션이 가능한 팀원들과, 자유로운 피드백이 가능한 분위기를 조성하는 게 프로젝트 진행에 얼마나 도움이 되는지 알게 되었던 계기가 된 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 라이브러리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 도구와 라이브러리를 적극 도입해 프로젝트에 녹여낸 것이 좋았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 아쉬웠던 점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 학습&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 플래그부터 생산성에 직결된 것을 많이 넣었다. 그것에 좀 매몰된 것이 아쉽다. 직접 부딪혀보면서 학습하는 것도 큰 도움이 되었다고 생각하지만, 처음부터 어떤 방법이 좋은 방법인지 잘 알고 있었다면 코드 퀄리티에 더 신경을 쓸 수 있었을 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 코드 품질&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번과 거의 연결되는 이야기이다. 급하게 기능을 작성한 느낌이 없지않아 있어 응집도 / 결합도 관리가 조금 부족했던 것 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 디테일 부족&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근성(aria-label) 관리가 부족했고, 디테일 페이지에 대해서는 반응형 작업을 하지 못했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 마무리&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;code&gt;Tanstack Query&lt;/code&gt;를 적용할 계획인 만큼, 팀원들과 공유할 수 있는 문서를 만들 계획입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습에 대한 아쉬움은 팀원들과 함께 공유하는 notion에 주기적으로 next나 관련 토픽들을 공유하면서 학습을 진행하면 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적극적으로 내 일처럼 도와주신 이성헌 멘토님, 우리 범쌤 덕분에 프로젝트를 열심히 하게 되는 계기가 되었던 것 같습니다 감사합니다!&lt;/p&gt;</description>
      <category>Project/PickItBook</category>
      <author>DYODa</author>
      <guid isPermaLink="true">https://yun-engene.tistory.com/103</guid>
      <comments>https://yun-engene.tistory.com/103#entry103comment</comments>
      <pubDate>Mon, 15 Sep 2025 23:03:19 +0900</pubDate>
    </item>
  </channel>
</rss>