<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Leirbag</title>
    <link>https://leirbag.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 13:35:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>엘리브가</managingEditor>
    <item>
      <title>msw 2.0를 활용한 api mocking (설치 및 기본 세팅)</title>
      <link>https://leirbag.tistory.com/167</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;msw.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mHoGU/btsz10DO3Dc/1Va3t6r6HBX9bG86cCwLoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mHoGU/btsz10DO3Dc/1Va3t6r6HBX9bG86cCwLoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mHoGU/btsz10DO3Dc/1Va3t6r6HBX9bG86cCwLoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmHoGU%2Fbtsz10DO3Dc%2F1Va3t6r6HBX9bG86cCwLoK%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;225&quot; height=&quot;225&quot; data-filename=&quot;msw.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발을 하기 위해서는 서버와의 통신이 필연적입니다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발을 하다보면 백엔드에서 api의 제작이 늦어질 수 있습니다.&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;894&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dm0CtP/btsz1IQYelu/Ugk4JQKrwLixZFgtqIYFi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dm0CtP/btsz1IQYelu/Ugk4JQKrwLixZFgtqIYFi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dm0CtP/btsz1IQYelu/Ugk4JQKrwLixZFgtqIYFi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdm0CtP%2Fbtsz1IQYelu%2FUgk4JQKrwLixZFgtqIYFi1%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;511&quot; height=&quot;88&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 서버가 완성이 되더라도, 실패에 대한 테스트는 어떻게 진행할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;msw가 이러한 문제들을 해결해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qIHzC/btszZmgIHis/8q645vOD1CRQk9QeKg85U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qIHzC/btszZmgIHis/8q645vOD1CRQk9QeKg85U0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qIHzC/btszZmgIHis/8q645vOD1CRQk9QeKg85U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqIHzC%2FbtszZmgIHis%2F8q645vOD1CRQk9QeKg85U0%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;451&quot; height=&quot;183&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 개발 환경에 포함되어있는 msw가 서버의 역할을 대신하면서 요청/응답을 mocking 하게 되며, 각종 통신 상황에 대해서 테스트를 진행합니다.&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;msw의 핵심 개념은 다음과 같습니다.&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. Intercepting requests&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;사용자가 서버로 보내려던 요청이 msw로 흘러갑니다.&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. mocking responses&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;msw가 대신 응답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 원하는 형태의 응답을 회신할 수 있습니다. (&lt;s&gt;답정너&lt;/s&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;기존에 msw를 가지고 잘 개발해왔고, 사용법에 대한 정보가 많아 글을 따로 작성하지 않았지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 msw 2.0 버전이 등장하게 되며 기존과의 사용 방법이 일부 달라졌습니다.&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;MSW 2.0은 중요한 업데이트로, Fetch API를 완벽하게 지원하며 여러 개선 사항과 버그 수정이 이뤄졌다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 JavaScript Fetch API를 사용하도록 변경되어 브라우저 및 Node.js에서 더 효과적인 네트워크 요청 및 응답을 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 응답이 제대로 동작하지 않거나, 잘못된 에러가 출력되는 등 여러 가지 문제들이 있었는데, 2.0 버전은 어떤지 좀 더 지켜봐야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;Fetch API 지원&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch API를 표준으로 사용하여 네트워크 요청 및 응답을 할 수 있습니다.&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;&lt;br /&gt;&lt;b&gt;이전 버전과의 호환성 문제 해결&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전에서 발생한 호환성 문제와 복잡성을 제거하고 표준 Fetch API를 사용합니다.&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;더 이상 다른 패키지의 폴리필에 의존하지 않으며, 플랫폼의 기능을 활용합니다. ESM과의 완벽한 호환성을 보인다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;b&gt;참고로 node.js 18버전 이상에서만 동작합니다.&lt;/b&gt;&lt;/i&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;span style=&quot;color: #0070d1; text-align: start;&quot;&gt;&lt;a href=&quot;https://mswjs.io/blog/introducing-msw-2.0&quot;&gt;https://mswjs.io/blog/introducing-msw-2.0&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699431668442&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;Introducing MSW 2.0&quot; data-og-description=&quot;The biggest library release is finally here! Learn more about the changes, motivation behind them, and how to upgrade to the next version today.&quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/blog/introducing-msw-2.0&quot; data-og-url=&quot;https://mswjs.io/blog/introducing-msw-2.0/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmIkh0/hyUu4eqRtc/vWdCngafz8dsiuRjFBXle1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cOlmFk/hyUrBEYpiE/YThzqEmswKKUBlMqNjAcf0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bVVcYr/hyUu19Rcg9/6mdcBUXzc3NWWD1HKKYzV1/img.png?width=2000&amp;amp;height=2000&amp;amp;face=0_0_2000_2000&quot;&gt;&lt;a href=&quot;https://mswjs.io/blog/introducing-msw-2.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/blog/introducing-msw-2.0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmIkh0/hyUu4eqRtc/vWdCngafz8dsiuRjFBXle1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cOlmFk/hyUrBEYpiE/YThzqEmswKKUBlMqNjAcf0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bVVcYr/hyUu19Rcg9/6mdcBUXzc3NWWD1HKKYzV1/img.png?width=2000&amp;amp;height=2000&amp;amp;face=0_0_2000_2000');&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;Introducing MSW 2.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The biggest library release is finally here! Learn more about the changes, motivation behind them, and how to upgrade to the next version today.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mswjs.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/migrations/1.x-to-2.x&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mswjs.io/docs/migrations/1.x-to-2.x&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699431778209&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;1.x &amp;rarr; 2.x&quot; data-og-description=&quot;Migration guidelines for version 2.0.&quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/docs/migrations/1.x-to-2.x&quot; data-og-url=&quot;https://mswjs.io/docs/migrations/1.x-to-2.x/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/J7WpU/hyUrATBPXj/cZvksPOnBEHkea8wXdV0L1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/oZNkT/hyUrxWSl0J/KxKWvEJlO0ASOT6KSD7bQ0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/migrations/1.x-to-2.x&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/docs/migrations/1.x-to-2.x&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/J7WpU/hyUrATBPXj/cZvksPOnBEHkea8wXdV0L1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/oZNkT/hyUrxWSl0J/KxKWvEJlO0ASOT6KSD7bQ0/img.jpg?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;1.x &amp;rarr; 2.x&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Migration guidelines for version 2.0.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mswjs.io&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;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vite react 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699424489217&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn create vite . --template react-ts&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1699425615383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i --save-dev @types/node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;msw 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699424843686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install msw --save-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mockServiceWorker.js 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699425303455&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx msw init ./public&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sSXMl/btsz1rVYjb5/3WQyQrWuJMlYUqRlDvJR2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sSXMl/btsz1rVYjb5/3WQyQrWuJMlYUqRlDvJR2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sSXMl/btsz1rVYjb5/3WQyQrWuJMlYUqRlDvJR2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsSXMl%2Fbtsz1rVYjb5%2F3WQyQrWuJMlYUqRlDvJR2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;484&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccZzBO/btszX2JDy0P/nZUMOclQz3YOuTaxBBvk60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccZzBO/btszX2JDy0P/nZUMOclQz3YOuTaxBBvk60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccZzBO/btszX2JDy0P/nZUMOclQz3YOuTaxBBvk60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccZzBO%2FbtszX2JDy0P%2FnZUMOclQz3YOuTaxBBvk60%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;585&quot; height=&quot;230&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;364&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;&lt;b&gt;src/mocks/browser.ts&amp;nbsp;생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699429297817&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/mocks/handlers.ts &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;생성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699429315198&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/handlers.ts
import {http, HttpResponse} from 'msw'

export const handlers = [
  http.get('/hello', () =&amp;gt; {
    console.log('msw:get :: /hello')
    return HttpResponse.json({
      data: &quot;Captured a \&quot;GET /hello\&quot; request&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;b&gt;main.tsx (혹은 index.tsx) 변경&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699429734059&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

async function deferRender() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }

  const { worker } = await import('./mocks/browser')

  return worker.start()
}

deferRender().then(() =&amp;gt; {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    &amp;lt;React.StrictMode&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;,
  )
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.tsx 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699429754166&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import './App.css'

function App() {

  const handlePosts = async () =&amp;gt; {
    const response = await fetch('/posts')
    const posts = await response.json()
    console.log(posts)
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={handlePosts}&amp;gt;Get Posts&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default App&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;api mocking에 성공하게 되면 외부 api로의 요청을 가로채서 msw가 대신 응답을 하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-08-2023 16-57-21.gif&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rclRE/btsz2l8VgUs/aasVq4kWHRklMOsUnaavkK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rclRE/btsz2l8VgUs/aasVq4kWHRklMOsUnaavkK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rclRE/btsz2l8VgUs/aasVq4kWHRklMOsUnaavkK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rclRE/btsz2l8VgUs/aasVq4kWHRklMOsUnaavkK/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;776&quot; height=&quot;554&quot; data-filename=&quot;Nov-08-2023 16-57-21.gif&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;554&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;세팅이 끝났으니 자세한 사용 방법은 아래 문서에서 확인하면 됩니다.&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;기존에는 rest객체와 res 함수를 사용하여 응답을 모킹할 수 있었으나, 2.0부터는 http와 HttpResponse가 이를 대신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 개념은 기존과 거의 유사하기 때문에 추가로 사용법에 대해서 서술하지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/http&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mswjs.io/docs/api/http&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699432290981&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;http&quot; data-og-description=&quot;Intercept HTTP requests.&quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/docs/api/http&quot; data-og-url=&quot;https://mswjs.io/docs/api/http/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oK0RQ/hyUrrvAWTy/VMLR0JHd55RhACb6ogLO3k/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7WhZZ/hyUuSLP8th/xMizxGbCdrEBnRTCkl4NqK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/http&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/docs/api/http&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oK0RQ/hyUrrvAWTy/VMLR0JHd55RhACb6ogLO3k/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7WhZZ/hyUuSLP8th/xMizxGbCdrEBnRTCkl4NqK/img.jpg?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;http&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Intercept HTTP requests.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mswjs.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/delay/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mswjs.io/docs/api/delay/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699432318115&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;delay&quot; data-og-description=&quot;Control response timing.&quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/docs/api/delay/&quot; data-og-url=&quot;https://mswjs.io/docs/api/delay/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kSCkm/hyUrAe0lZS/rB7N4qfn7bxEDo4HcCzvTk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/iaFWk/hyUuT43ldq/esoOQZXFnlHLybReNF7TIK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/delay/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/docs/api/delay/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kSCkm/hyUrAe0lZS/rB7N4qfn7bxEDo4HcCzvTk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/iaFWk/hyUuT43ldq/esoOQZXFnlHLybReNF7TIK/img.jpg?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;delay&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Control response timing.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mswjs.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/http-response&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mswjs.io/docs/api/http-response&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699432330243&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;HttpResponse&quot; data-og-description=&quot; &quot; data-og-host=&quot;mswjs.io&quot; data-og-source-url=&quot;https://mswjs.io/docs/api/http-response&quot; data-og-url=&quot;https://mswjs.io/docs/api/http-response/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pxuaV/hyUrEBGv9m/TnPvfX0vxdz6jJssKXDCE1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b5ISBT/hyUu5j6Shs/ttyG9bg37gU2YcJxTTAsz1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://mswjs.io/docs/api/http-response&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mswjs.io/docs/api/http-response&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pxuaV/hyUrEBGv9m/TnPvfX0vxdz6jJssKXDCE1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b5ISBT/hyUu5j6Shs/ttyG9bg37gU2YcJxTTAsz1/img.jpg?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;HttpResponse&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;mswjs.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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존과 동일하게 msw로 거의 모든 상황에서 모의 요청/응답을 실행해볼 수 있으며, 필요한 경우 핸들러 내부에 Storage API나 IndexedDB를 활용하여 서버의 기능을 흉내내는 것도 가능합니다.&lt;/p&gt;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>MSW</category>
      <category>msw 2.0</category>
      <category>msw 2.0 설정</category>
      <category>msw 2.0 세팅</category>
      <category>msw react</category>
      <category>msw2.0</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/167</guid>
      <comments>https://leirbag.tistory.com/167#entry167comment</comments>
      <pubDate>Wed, 1 Nov 2023 15:38:49 +0900</pubDate>
    </item>
    <item>
      <title>React에서 Google Maps API, Tanstack Query로 대용량 마커 데이터를 관리하는 방법</title>
      <link>https://leirbag.tistory.com/160</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;google_maps.png&quot; data-origin-width=&quot;2275&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rNj2d/btszXLoaDXV/JFb1kgqHleBSPFjuG7HE30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rNj2d/btszXLoaDXV/JFb1kgqHleBSPFjuG7HE30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rNj2d/btszXLoaDXV/JFb1kgqHleBSPFjuG7HE30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrNj2d%2FbtszXLoaDXV%2FJFb1kgqHleBSPFjuG7HE30%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;159&quot; height=&quot;143&quot; data-filename=&quot;google_maps.png&quot; data-origin-width=&quot;2275&quot; data-origin-height=&quot;2048&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;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/woowacourse-teams/2023-car-ffeine&quot;&gt;최근 진행한 프로젝트&lt;/a&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. 대량의 데이터(전국 약 6만여 건)를 사용자의 화면에 해당하는 영역만 적당하게 마커로 렌더링 해줄 것&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. 한번 렌더링 한 마커는 유지하고, 화면에서 벗어난 마커는 해제할 것&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.&amp;nbsp;지도는 지도대로 동작하고, React는 React대로 동작하여 독립적인 환경을 보장할 것&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cf-zoom.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOdnRW/btszX0Fy3PS/4wwZtWoHrbKmkyXK3WVfok/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOdnRW/btszX0Fy3PS/4wwZtWoHrbKmkyXK3WVfok/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOdnRW/btszX0Fy3PS/4wwZtWoHrbKmkyXK3WVfok/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cOdnRW/btszX0Fy3PS/4wwZtWoHrbKmkyXK3WVfok/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;640&quot; height=&quot;456&quot; data-filename=&quot;cf-zoom.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cf-move.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNh1n4/btszXGHlVE6/hOAqiQVjkWXkg64ercXsyk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNh1n4/btszXGHlVE6/hOAqiQVjkWXkg64ercXsyk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNh1n4/btszXGHlVE6/hOAqiQVjkWXkg64ercXsyk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bNh1n4/btszXGHlVE6/hOAqiQVjkWXkg64ercXsyk/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;640&quot; height=&quot;456&quot; data-filename=&quot;cf-move.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는&amp;nbsp;Google Maps API, React, 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;/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;/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://leirbag.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leirbag.tistory.com/154&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699455248955&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;고밀도 지도 데이터 관리를 위한 효과적인 전략: 지도 확대 및 마커 제어&quot; data-og-description=&quot;지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제&quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/154&quot; data-og-url=&quot;https://leirbag.tistory.com/154&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNPPX0/hyUrzULHMK/l9IMIBwoZKZCMASKPeMt20/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/tuHfU/hyUuQ1DN5e/S699DTOvmuogkYrXkOqh2K/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/eelhir/hyUruePBjN/frEkev70uRT3kkJq41cqy1/img.png?width=3068&amp;amp;height=1866&amp;amp;face=0_0_3068_1866&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/154&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/154&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNPPX0/hyUrzULHMK/l9IMIBwoZKZCMASKPeMt20/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/tuHfU/hyUuQ1DN5e/S699DTOvmuogkYrXkOqh2K/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/eelhir/hyUruePBjN/frEkev70uRT3kkJq41cqy1/img.png?width=3068&amp;amp;height=1866&amp;amp;face=0_0_3068_1866');&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;고밀도 지도 데이터 관리를 위한 효과적인 전략: 지도 확대 및 마커 제어&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 글에서 나온 개념들은 이 글에서 자주 등장할 개념입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 바라보는 화면을 기준으로 마커 데이터를 요청하는 것은 어떤 지도 라이브러리에서도 가능할 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사실 백엔드 단에서 데이터를 어떻게 조회하던 간에, 프론트에는 마커의 좌표 배열이 도착할 것입니다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 도착한 마커 배열 데이터를 어떻게 동적으로 관리할 수 있는지에 대한 아이디어를 소개합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실습을 위한 기본 설치&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/146&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leirbag.tistory.com/146&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699455551354&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;왕빠른 Vite React를 사용하여 GitHub Pages 및 Storybook 자동 배포 설정하기&quot; data-og-description=&quot;Create React App에서 리액트 프로젝트와 Storybook 배포를 자동화 하는 것은 자료가 많고 매우 쉽습니다. 하지만 Vite라면 어떨까요? 아직까지 Vite는 다소 귀찮은 작업들이 존재하는데요, 자료가 거의 &quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/146&quot; data-og-url=&quot;https://leirbag.tistory.com/146&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/QCwNq/hyUu5EuPvT/KAKR8cT8BHTi6dKUKVBtH1/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/t7ARI/hyUuVBSTAS/HgLikCrjaESic8YRmwQCtk/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/KqhQ6/hyUu1oyP0W/AmXYeLbS8KdnreQprXzY7k/img.png?width=1045&amp;amp;height=411&amp;amp;face=0_0_1045_411&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/146&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/146&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/QCwNq/hyUu5EuPvT/KAKR8cT8BHTi6dKUKVBtH1/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/t7ARI/hyUuVBSTAS/HgLikCrjaESic8YRmwQCtk/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640,https://scrap.kakaocdn.net/dn/KqhQ6/hyUu1oyP0W/AmXYeLbS8KdnreQprXzY7k/img.png?width=1045&amp;amp;height=411&amp;amp;face=0_0_1045_411');&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;왕빠른 Vite React를 사용하여 GitHub Pages 및 Storybook 자동 배포 설정하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Create React App에서 리액트 프로젝트와 Storybook 배포를 자동화 하는 것은 자료가 많고 매우 쉽습니다. 하지만 Vite라면 어떨까요? 아직까지 Vite는 다소 귀찮은 작업들이 존재하는데요, 자료가 거의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot;&gt;위 글에 따라 vite의 기본 설치를 진행해주세요. 자동 배포 설정이나 Storybook은 설치하지 않아도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@googlemaps/react-wrapper 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leirbag.tistory.com/158&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699455415644&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;React에서 Google Maps API를 자유롭게 사용하는 방법 (@googlemaps/react-wrapper)&quot; data-og-description=&quot;React에서 Google Maps API를 사용하기 위해 사용하는 @react-google-maps/api나 react-google-maps 같은 라이브러리들을 사용하는 방법이 있을 것입니다. 이 라이브러리들을 사용하면 Google Maps API에 있는 주요 기&quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/158&quot; data-og-url=&quot;https://leirbag.tistory.com/158&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/yNpX4/hyUru0fjCm/AanbnYvpOLjX3pv6BKbOp0/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/bchxRA/hyUu6ckvUw/J6YtdAynvDbpU0b0ZZWTN1/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/V6hG1/hyUrEaHl5p/GtCyw3phB2vGVwkDUP5o20/img.png?width=2900&amp;amp;height=1988&amp;amp;face=0_0_2900_1988&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/158&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/158&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/yNpX4/hyUru0fjCm/AanbnYvpOLjX3pv6BKbOp0/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/bchxRA/hyUu6ckvUw/J6YtdAynvDbpU0b0ZZWTN1/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/V6hG1/hyUrEaHl5p/GtCyw3phB2vGVwkDUP5o20/img.png?width=2900&amp;amp;height=1988&amp;amp;face=0_0_2900_1988');&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에서 Google Maps API를 자유롭게 사용하는 방법 (@googlemaps/react-wrapper)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React에서 Google Maps API를 사용하기 위해 사용하는 @react-google-maps/api나 react-google-maps 같은 라이브러리들을 사용하는 방법이 있을 것입니다. 이 라이브러리들을 사용하면 Google Maps API에 있는 주요 기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 글에 따라 @googlemaps/react-wrapper를 설치해주시고 기본적인 세팅(지도를 화면에 띄우는 것)을 진행해주세요.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 벡터 지도 설정도 해주셔야 뒤에서 실습 할 AdvancedMarkerElement 객체를 사용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;msw 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leirbag.tistory.com/167&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699455483143&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;msw 2.0를 활용한 api mocking (설치 및 기본 세팅)&quot; data-og-description=&quot;프론트엔드 개발을 하기 위해서는 서버와의 통신이 필연적입니다, 하지만 개발을 하다보면 백엔드에서 api의 제작이 늦어질 수 있습니다. 이런 경우에는 어떻게 해야할까요? 백엔드에서 api를 다&quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/167&quot; data-og-url=&quot;https://leirbag.tistory.com/167&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cVIin7/hyUuW1QQs3/sEH6u0yhNVfQ6OEa4gLcbK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/G0bkR/hyUruzabzh/KjDFFifdk9WR4niEVIpPR1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/hpssc/hyUrDiAhfQ/O5R7zsSkzR0UxKIJkkgc8K/img.png?width=622&amp;amp;height=484&amp;amp;face=0_0_622_484&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/167&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cVIin7/hyUuW1QQs3/sEH6u0yhNVfQ6OEa4gLcbK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/G0bkR/hyUruzabzh/KjDFFifdk9WR4niEVIpPR1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/hpssc/hyUrDiAhfQ/O5R7zsSkzR0UxKIJkkgc8K/img.png?width=622&amp;amp;height=484&amp;amp;face=0_0_622_484');&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;msw 2.0를 활용한 api mocking (설치 및 기본 세팅)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발을 하기 위해서는 서버와의 통신이 필연적입니다, 하지만 개발을 하다보면 백엔드에서 api의 제작이 늦어질 수 있습니다. 이런 경우에는 어떻게 해야할까요? 백엔드에서 api를 다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;msw 2.0이 출시되었으므로 새로운 버전에 맞춰 코드를 작성할 예정입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;msw도 설치해주시고 기본 세팅을 진행해주세요.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TanStack Query 설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1699455929155&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add @tanstack/react-query&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1699456200643&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add -D @tanstack/eslint-plugin-query&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1699456267792&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add @tanstack/react-query-devtools&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;src/main.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699456387414&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import {QueryClient, QueryClientProvider} from &quot;@tanstack/react-query&quot;;
import {ReactQueryDevtools} from &quot;@tanstack/react-query-devtools&quot;;

async function deferRender() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }

  const { worker } = await import('./mocks/browser')

  return worker.start()
}

const queryClient = new QueryClient()

deferRender().then(() =&amp;gt; {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    &amp;lt;React.StrictMode&amp;gt;
      &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
        &amp;lt;App /&amp;gt;
        &amp;lt;ReactQueryDevtools initialIsOpen={false} /&amp;gt;
      &amp;lt;/QueryClientProvider&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;,
  )
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 기본적인 쿼리 세팅과 함께 msw까지 적용해주시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;external-state 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/151&quot;&gt;https://leirbag.tistory.com/151&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699456750210&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;useSyncExternalStore로 만들어보는 전역상태관리 라이브러리&quot; data-og-description=&quot;몇 달 전부터 전역상태관리 라이브러리를 직접 만들어보고 싶었는데, 우테코 방학 기간에 심심해서 만들어 봤습니다. 혹시 useSyncExternalStore가 무엇인지 처음 들어보신다면 이 문서를 확인해보시&quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/151&quot; data-og-url=&quot;https://leirbag.tistory.com/151&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bUpj3M/hyUrpSab9o/I5kdIcotWgidT5vQq63ro1/img.png?width=441&amp;amp;height=441&amp;amp;face=0_0_441_441,https://scrap.kakaocdn.net/dn/U1F6b/hyUrwjrkFu/mO5dvfpwNvVbvz4khfMYM1/img.png?width=441&amp;amp;height=441&amp;amp;face=0_0_441_441,https://scrap.kakaocdn.net/dn/cphWRB/hyUuQURznn/OnpTKX5QfHRLEJlATABRTK/img.png?width=1770&amp;amp;height=1008&amp;amp;face=0_0_1770_1008&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/151&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/151&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bUpj3M/hyUrpSab9o/I5kdIcotWgidT5vQq63ro1/img.png?width=441&amp;amp;height=441&amp;amp;face=0_0_441_441,https://scrap.kakaocdn.net/dn/U1F6b/hyUrwjrkFu/mO5dvfpwNvVbvz4khfMYM1/img.png?width=441&amp;amp;height=441&amp;amp;face=0_0_441_441,https://scrap.kakaocdn.net/dn/cphWRB/hyUuQURznn/OnpTKX5QfHRLEJlATABRTK/img.png?width=1770&amp;amp;height=1008&amp;amp;face=0_0_1770_1008');&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;useSyncExternalStore로 만들어보는 전역상태관리 라이브러리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;몇 달 전부터 전역상태관리 라이브러리를 직접 만들어보고 싶었는데, 우테코 방학 기간에 심심해서 만들어 봤습니다. 혹시 useSyncExternalStore가 무엇인지 처음 들어보신다면 이 문서를 확인해보시&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 글은 useSyncExternalStore를 활용한 상태관리 라이브러리를 만드는 글 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부시스템인 지도를 다루는 것은 물론이고, 클라이언트 전역상태를 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(기본적인 사용법이 recoil과 상당히 유사하므로 금방 익히실 것입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;패키지를 설치해서 사용하는 것도 가능하니, 필요하시다면 설치해서 사용하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 글에서는 해당 도구를 이용하여 전역 상태에 접근하는 것을 예제로 보여드릴 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699456730949&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add external-state&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;라이브러리 사용법은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/gabrielyoon7/external-state/blob/main/docs/readme-kr.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/gabrielyoon7/external-state/blob/main/docs/readme-kr.md&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만일, 이를 원치 않는다면 편하신 방법으로 전역 상태 관리 라이브러리를 선택하셔도 좋습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;지도 생성 및 레이아웃 나누기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지도 생성과 함께 데이터 확인용 레이아웃을 만들어보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/googleMapStore.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699459613837&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/googleMapStore.ts

import {store} from &quot;external-state&quot;;

export const INITIAL_CENTER = {
  lat: 37.5,
  lng: 127.0,
}
export const INITIAL_ZOOM_LEVEL = 16;

export const getGoogleMapStore = (() =&amp;gt; {
  let googleMap: google.maps.Map;

  const container = document.createElement('div');

  container.id = 'map';
  container.style.minHeight = '100vh';

  document.body.appendChild(container);

  return () =&amp;gt; {
    if (!googleMap) {

      googleMap = new window.google.maps.Map(container, {
        center: INITIAL_CENTER,
        zoom: INITIAL_ZOOM_LEVEL,
        disableDefaultUI: true,
        mapId: '92cb7201b7d43b21',
      });
    }

    return store&amp;lt;google.maps.Map&amp;gt;(googleMap);
  };
})();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;클로저를 활용하여 구글 지도 객체를 단 한번만 생성할 수 있도록 합니다. 단, 생성된 객체는 store 객체에 담깁니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/Dashboard.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699459533856&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/Dashboard.tsx

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

const dashboardStyle: CSSProperties = {
  position: 'absolute',
  width: '300px',
  height: '500px',
  backgroundColor: 'white',
  top: '0',
  left: '0',
  zIndex: 100,
};

function Dashboard() {

  return (
    &amp;lt;div style={dashboardStyle}&amp;gt;
      hi
    &amp;lt;/div&amp;gt;
  );
}

export default Dashboard;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아직은 비워져있지만, 앞으로 충전소 상태를 보여줄 React UI 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/GoogleMap.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699459800955&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/GoogleMap.tsx

import {useExternalValue} from &quot;external-state&quot;;
import {getGoogleMapStore} from &quot;./googleMapStore.ts&quot;;

function GoogleMap() {

  const googleMap = useExternalValue(getGoogleMapStore());

  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
}

export default GoogleMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;구글 지도 객체가 store에 담겨 전역적으로 접근할 수 있으므로 , useExternalValue훅으로 호출합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699459512389&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/App.tsx

import {Status, Wrapper} from &quot;@googlemaps/react-wrapper&quot;;
import GoogleMap from &quot;./GoogleMap.tsx&quot;;
import Dashboard from &quot;./Dashboard.tsx&quot;;

const render = (status: Status) =&amp;gt; {
  switch (status) {
    case Status.LOADING:
      return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
    case Status.FAILURE:
      return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
    case Status.SUCCESS:
      return (
        &amp;lt;&amp;gt;
          &amp;lt;GoogleMap/&amp;gt;
          &amp;lt;Dashboard/&amp;gt;
        &amp;lt;/&amp;gt;
      );
  }
};

function App() {

  return (
    &amp;lt;Wrapper apiKey=&quot;&quot; render={render} libraries={['marker']}/&amp;gt;
  )
}

export default App&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; 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;2138&quot; data-origin-height=&quot;1886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpLiYE/btszX2XFM2e/EIURbItS7dxwxvFG6GKVqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpLiYE/btszX2XFM2e/EIURbItS7dxwxvFG6GKVqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpLiYE/btszX2XFM2e/EIURbItS7dxwxvFG6GKVqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpLiYE%2FbtszX2XFM2e%2FEIURbItS7dxwxvFG6GKVqk%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;2138&quot; height=&quot;1886&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;1886&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 지도 객체가 전역적으로 접근이 가능해졌고, 어떤 상태를 띄울 React 컴포넌트(Dashboard) 또한 렌더링하게 되었습니다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; 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;1350&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yGKcB/btsz1RgfoL4/FJ70ZzECFSTFg52w3O8Zn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yGKcB/btsz1RgfoL4/FJ70ZzECFSTFg52w3O8Zn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yGKcB/btsz1RgfoL4/FJ70ZzECFSTFg52w3O8Zn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyGKcB%2Fbtsz1RgfoL4%2FFJ70ZzECFSTFg52w3O8Zn0%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;1350&quot; height=&quot;786&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;현 위치와 디스플레이 크기 알아내기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일단 현위치와 디스플레이 크기를 알아내어야 서버에 적절한 범위로 데이터를 요청할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 사용하는 식에 대한 내용은 맨 위에 첨부한 게시글의 위도델타, 경도델타에 대한 규칙(?) 따라 만들어진 정보입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/getDisplayPosition.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699461592953&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/getDisplayPosition.ts

import {DisplayPosition} from &quot;./types.ts&quot;;

export const getDisplayPosition = (map: google.maps.Map): DisplayPosition =&amp;gt; {
  const center = map.getCenter();
  const bounds = map.getBounds();

  const longitudeDelta = bounds
    ? (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2
    : 0;
  const latitudeDelta = bounds
    ? (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2
    : 0;
  const longitude = center ? center.lng() : 0;
  const latitude = center ? center.lat() : 0;
  const zoom = map.getZoom() || 0;

  return {
    longitude,
    latitude,
    longitudeDelta: longitudeDelta,
    latitudeDelta: latitudeDelta,
    zoom,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;구글 맵 객체에서 현위치의 좌표와, 디스플레이 영역 사이즈를 고려한 델타 값 그리고 줌 레벨을 알아내는 과정입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;옵셔널 체이닝을 사용한 이유는 &lt;b&gt;지도 객체의 로드 상황에 따라 참조 가능 여부가 달라지기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/types.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699460983511&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/types.ts

export interface DisplayPosition {
  longitude: number;
  latitude: number;
  longitudeDelta: number;
  latitudeDelta: number;
  zoom: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;스크린 크기를 델타로 관리하면 나중에 범위를 배율로 커스터마이징 할 때 편해지고, 약속된 단위로 제어를 할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;충전소 데이터 수신 훅 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;마커를 생성하기 전에 Dashboard에 충전소 데이터를 수신해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/useStations.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699462527867&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/useStations.ts
import {useQuery} from &quot;@tanstack/react-query&quot;;
import {getGoogleMapStore} from &quot;./googleMapStore.ts&quot;;
import {getDisplayPosition} from &quot;./getDisplayPosition.ts&quot;;
import {Station} from &quot;./types.ts&quot;;

export const fetchStations = async () =&amp;gt; {
  const googleMap = getGoogleMapStore().getState();
  const {latitudeDelta, longitudeDelta, longitude, latitude} = getDisplayPosition(googleMap);

  const stations = await fetch(`/stations?latitude=${latitude}&amp;amp;longitude=${longitude}&amp;amp;latitudeDelta=${latitudeDelta}&amp;amp;longitudeDelta=${longitudeDelta}`).then&amp;lt;Station[]&amp;gt;(async (response) =&amp;gt; {
    const data = await response.json();
    return data;
  });

  return stations;
}

export const useStations = () =&amp;gt; {
  return useQuery({
    queryKey: ['stations'],
    queryFn: fetchStations,
    refetchOnWindowFocus: false,
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;충전소 정보를 수신하는 페치 훅을 만듭니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/Dashboard.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699462543365&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/Dashboard.tsx

import {CSSProperties} from &quot;react&quot;;
import {useStations} from &quot;./useStations.ts&quot;;

const dashboardStyle: CSSProperties = {
  position: 'absolute',
  width: '300px',
  height: '500px',
  backgroundColor: 'white',
  top: '0',
  left: '0',
  zIndex: 100,
};

function Dashboard() {

  const {data: stations, isLoading, isError} = useStations();

  if (isLoading) {
    return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
  }

  if (isError) {
    return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
  }

  return (
    &amp;lt;div style={dashboardStyle}&amp;gt;
      {stations?.map(station =&amp;gt; (
        &amp;lt;div key={station.stationId}&amp;gt;
          {station.stationName}
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}

export default Dashboard;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드에 연동해서 서버 상태를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3mJjC/btszXIdViZL/R6iX6OkUMMoGS1KbK2e0bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3mJjC/btszXIdViZL/R6iX6OkUMMoGS1KbK2e0bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3mJjC/btszXIdViZL/R6iX6OkUMMoGS1KbK2e0bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3mJjC%2FbtszXIdViZL%2FR6iX6OkUMMoGS1KbK2e0bk%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;1236&quot; height=&quot;710&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;당연하지만 서버가 없으므로 에러가 발생합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;요청 모킹하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/mocks/handlers.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699463704188&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/handlers.ts
import {http, HttpResponse} from 'msw'

export const handlers = [
  http.get(`/stations`, async ({request}) =&amp;gt; {
    const url = new URL(request.url)

    const latitude = url.searchParams.get('latitude');
    const longitude = url.searchParams.get('longitude');
    const latitudeDelta = url.searchParams.get('latitudeDelta');
    const longitudeDelta = url.searchParams.get('longitudeDelta');

    const northEastBoundary = {
      latitude: Number(latitude) + Number(latitudeDelta),
      longitude: Number(longitude) + Number(longitudeDelta),
    };

    const southWestBoundary = {
      latitude: Number(latitude) - Number(latitudeDelta),
      longitude: Number(longitude) - Number(longitudeDelta),
    };

    console.log(latitude, longitude, latitudeDelta, longitudeDelta, northEastBoundary, southWestBoundary)

    return HttpResponse.json([
      {
        stationId: 'test_station0', stationName: 'test_station0', latitude: 0, longitude: 0
      },
      {
        stationId: 'test_station1', stationName: 'test_station1', latitude: 1, longitude: 1
      },
    ]);
  }),
]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;바깥으로 나가려던 요청을 msw로 모킹하여 확인해보고, 오류가 나지 않도록 임시 데이터를 반환해주는 것을 테스트 해봅니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; 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;2140&quot; data-origin-height=&quot;1880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7FRIa/btszYnm6uBZ/QfcuSSLQbcgycZznnjapYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7FRIa/btszYnm6uBZ/QfcuSSLQbcgycZznnjapYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7FRIa/btszYnm6uBZ/QfcuSSLQbcgycZznnjapYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7FRIa%2FbtszYnm6uBZ%2FQfcuSSLQbcgycZznnjapYk%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;2140&quot; height=&quot;1880&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;1880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;msw가 요청을 잘 가로채고, 테스트 데이터를 반환했음을 확인했습닌다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다만, 델타 값이 0으로 날라가는 현상이 감지됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이는 뒤에서 해결하고, 가상의 마커 데이터를 생성해서 반환해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;가상의 충전소 데이터 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;우선 데이터가 없으므로 랜덤한 마커 데이터를 생성해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mocks/data.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699458255029&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/data.ts

import {Station} from &quot;../types.ts&quot;;

export const generateRandomStationId = () =&amp;gt; {
  const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  const numbers = '0123456789';

  const randomChar = (source: string) =&amp;gt; source[Math.floor(Math.random() * source.length)];

  const randomLetter1 = randomChar(letters);
  const randomLetter2 = randomChar(letters);
  const randomNumber = Array.from({length: 6}, () =&amp;gt; randomChar(numbers)).join('');

  return `${randomLetter1}${randomLetter2}${randomNumber}`;
};

export const mockStations = Array.from({length: 60000}, () =&amp;gt; {
  const randomStationId = generateRandomStationId();

  const newStation: Station = {
    stationId: randomStationId,
    stationName: `충전소 ${randomStationId}`,
    latitude: 37 + 0.25 + 9999 * Math.random() * 0.00005,
    longitude: 127 - 0.25 + 9999 * Math.random() * 0.00005,
  };

  return newStation;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/types.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699458280241&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/types.ts

export interface Station {
  stationId: string;
  stationName: string;
  latitude: number;
  longitude: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드에 따라 랜덤한 마커 데이터가 6만여 개 수도권에 생성될 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;msw가 가상의 충전소 데이터를 반환하게 하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/mocks/handlers.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699464062394&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/handlers.ts
import {http, HttpResponse} from 'msw'
import {mockStations} from &quot;./data.ts&quot;;

export const handlers = [
  http.get(`/stations`, async ({request}) =&amp;gt; {
    const url = new URL(request.url)

    const latitude = url.searchParams.get('latitude');
    const longitude = url.searchParams.get('longitude');
    const latitudeDelta = url.searchParams.get('latitudeDelta');
    const longitudeDelta = url.searchParams.get('longitudeDelta');

    const northEastBoundary = {
      latitude: Number(latitude) + Number(latitudeDelta),
      longitude: Number(longitude) + Number(longitudeDelta),
    };

    const southWestBoundary = {
      latitude: Number(latitude) - Number(latitudeDelta),
      longitude: Number(longitude) - Number(longitudeDelta),
    };

    console.log(latitude, longitude, latitudeDelta, longitudeDelta, northEastBoundary, southWestBoundary)


    return HttpResponse.json(mockStations);
  }),
]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리곤, 위와 같이 msw 반환 함수에서 mockStations를 반환해보면 어떻게 나올까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; 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;2138&quot; data-origin-height=&quot;1880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUr2oa/btsz3eovAmk/uDGfMiU0wucKTJKMSZVkY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUr2oa/btsz3eovAmk/uDGfMiU0wucKTJKMSZVkY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUr2oa/btsz3eovAmk/uDGfMiU0wucKTJKMSZVkY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUr2oa%2Fbtsz3eovAmk%2FuDGfMiU0wucKTJKMSZVkY0%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;2138&quot; height=&quot;1880&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;1880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;세어보지는 않았지만 6만여개의 데이터를 수신했을 것입니다(?)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2016&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ob6wn/btsz2pjwYqX/nggKypjAoVwykwU4v8EyoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ob6wn/btsz2pjwYqX/nggKypjAoVwykwU4v8EyoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ob6wn/btsz2pjwYqX/nggKypjAoVwykwU4v8EyoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fob6wn%2Fbtsz2pjwYqX%2FnggKypjAoVwykwU4v8EyoK%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;2016&quot; height=&quot;943&quot; data-origin-width=&quot;2016&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;idle 상태에 이벤트를 걸어 쿼리 무효화를 날리기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 델타 값이 0으로 측정되는 이유는 google maps api의 bounds 메서드의 활성화 시점과도 관련이 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일단, &lt;b&gt;구글 지도가 bounds를 측정하는 시점은 DOM에 안착된 이후&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt; 처음 개발할 때에는 이 이유를 몰라 굉장히 고생하였는데, 바닐라 환경에서 개발을 유지하려다보니 디버깅 하기는 쉬웠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DOM에 달라붙은 이후, 현재 디바이스 스크린의 영역을 찾아내어 해당 위치의 좌표를 할당하게 되는 google maps api의 특성에 따라, &lt;b&gt;bounds 메서드가 동작하는 그 순간을 이벤트로 검출&lt;/b&gt;해야합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/GoogleMap.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699465404592&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/GoogleMap.tsx

import {useExternalValue} from &quot;external-state&quot;;
import {getGoogleMapStore} from &quot;./googleMapStore.ts&quot;;
import {useEffect} from &quot;react&quot;;
import {useQueryClient} from &quot;@tanstack/react-query&quot;;

function GoogleMap() {

  const googleMap = useExternalValue(getGoogleMapStore());
  const queryClient = useQueryClient();

  useEffect(() =&amp;gt; {
    googleMap.addListener('idle', () =&amp;gt; {
      queryClient.invalidateQueries({queryKey: ['stations']});
    });

  }, [googleMap]);

  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
}

export default GoogleMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그 순간을 idle 이벤트로 정의해버립니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 이벤트는 Google Maps API에서 제공하며, &lt;b&gt;공식문서에 있는 이벤트를 그대로 사용&lt;/b&gt;할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;화면의 양 끝점을 검출할 수 있게 된 순간, &lt;b&gt;idle 이벤트가 발동하여 쿼리무효화를 진행&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 쿼리 무효화로 인해 useQuery가 충전소 데이터를 새롭게 수신하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/mocks/handlers.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699465489011&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/mocks/handlers.ts
import {http, HttpResponse} from 'msw'
import {mockStations} from &quot;./data.ts&quot;;
import {Station} from &quot;../types.ts&quot;;

export const handlers = [
  http.get(`/stations`, async ({request}) =&amp;gt; {
    const url = new URL(request.url)

    const latitude = url.searchParams.get('latitude');
    const longitude = url.searchParams.get('longitude');
    const latitudeDelta = url.searchParams.get('latitudeDelta');
    const longitudeDelta = url.searchParams.get('longitudeDelta');

    const northEastBoundary = {
      latitude: Number(latitude) + Number(latitudeDelta),
      longitude: Number(longitude) + Number(longitudeDelta),
    };

    const southWestBoundary = {
      latitude: Number(latitude) - Number(latitudeDelta),
      longitude: Number(longitude) - Number(longitudeDelta),
    };

    console.log(latitude, longitude, latitudeDelta, longitudeDelta, northEastBoundary, southWestBoundary);

    const isStationLatitudeWithinBounds = (station: Station) =&amp;gt; {
      return (
        station.latitude &amp;gt; southWestBoundary.latitude &amp;amp;&amp;amp;
        station.latitude &amp;lt; northEastBoundary.latitude
      );
    };

    const isStationLongitudeWithinBounds = (station: Station) =&amp;gt; {
      return (
        station.longitude &amp;gt; southWestBoundary.longitude &amp;amp;&amp;amp;
        station.longitude &amp;lt; northEastBoundary.longitude
      );
    };

    const foundStations = mockStations.filter(
      (station) =&amp;gt;
        isStationLatitudeWithinBounds(station) &amp;amp;&amp;amp; isStationLongitudeWithinBounds(station)
    )
    
    return HttpResponse.json(foundStations);
  }),
]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 서버에서 어떻게 연산할지는 프로젝트마다 다르겠지만, 우선은 요청에 들어온 영역 만큼을 잘라내어 보내는 것으로 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 idle 이벤트로 인해 델타 값이 더이상 0이 아닌, 실제 값으로 검출이 될 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; 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;Nov-09-2023 02-49-44.gif&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGDjZE/btsz2W2AtYz/SJHbuv12Gw0fY38jGuiJlk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGDjZE/btsz2W2AtYz/SJHbuv12Gw0fY38jGuiJlk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGDjZE/btsz2W2AtYz/SJHbuv12Gw0fY38jGuiJlk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cGDjZE/btsz2W2AtYz/SJHbuv12Gw0fY38jGuiJlk/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;724&quot; height=&quot;628&quot; data-filename=&quot;Nov-09-2023 02-49-44.gif&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;지도 객체가 idle 상태에 빠질 때 마다 쿼리 무효화를 진행하고, 그 시점의 화면 영역을 읽어와서 새로운 데이터를 수신하여 서버 상태로 관리하는 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 서버 상태(충전소)를 마커로 렌더링 하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 사용자가 지도를 동작할 때 마다, 스크린 영역을 검출하여 서버로부터 상태를 업데이트합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 수신받을 때 마다 충전소 데이터를 마커로 보여줄 방법은 없을까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;놀랍게도 충전소 마커를 React Component로 관리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;분명히 지도 객체는 Vanilla JS의 영역임에도 불구하고 이러한 결합이 가능합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #607080; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/StationMarker.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699466809244&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/StationMarker.tsx

import {Station} from &quot;./types.ts&quot;;
import {useEffect} from &quot;react&quot;;
import {useExternalValue} from &quot;external-state&quot;;
import {getGoogleMapStore} from &quot;./googleMapStore.ts&quot;;
import {createRoot} from &quot;react-dom/client&quot;;

function StationMarker({station}: { station: Station }) {
  const googleMap = useExternalValue(getGoogleMapStore());

  useEffect(() =&amp;gt; {
    const {latitude, longitude, stationName} = station;

    const container = document.createElement('div');

    const markerInstance = new google.maps.marker.AdvancedMarkerElement({
      position: {lat: latitude, lng: longitude},
      map: googleMap,
      title: stationName,
      content: container,
    });

    createRoot(container).render(
      &amp;lt;div
        style={{backgroundColor: 'red', width: '10px', height: '10px', borderRadius: '50%'}}
      /&amp;gt;
    );

    markerInstance.addListener('click', () =&amp;gt; {
      googleMap.panTo({lat: latitude, lng: longitude});
    });

    return () =&amp;gt; {
      markerInstance.map = null;
    };
  }, []);

  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
}

export default StationMarker;&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;이 컴포넌트는 DOM에 직접적으로 무언가를 부착하지 않습니다. 하지만, 간접적으로 지도 객체 위에 리액트 컴포넌트를 부착할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createElement로 엘리먼트를 생성하고, 해당 엘리먼트에 React Component를 렌더링 한 다음, 지도 위의 마커에 부착할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, 이 컴포넌트가 React 생명주기에 의해 사라지게 된다면 clean up 함수로 지도에서 마커가 탈락하도록 했습니다.&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;src/MarkerContainer.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699466903314&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/MarkerContainer.tsx
import {useStations} from &quot;./useStations.ts&quot;;
import StationMarker from &quot;./StationMarker.tsx&quot;;

function MarkerContainer() {
  const {data: stations} = useStations();

  if (stations === undefined) {
    return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;;
  }

  return stations.map(station =&amp;gt; (
    &amp;lt;StationMarker key={station.stationId} station={station}/&amp;gt;
  ));
}

export default MarkerContainer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컴포넌트는 앞서 만든 StationMarker 컴포넌트를 부착하는 컨테이너 컴포넌트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useStations라는 쿼리 훅을 사용하여 연동이 가능하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;src/ App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699467033680&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const render = (status: Status) =&amp;gt; {
  switch (status) {
    case Status.LOADING:
      return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
    case Status.FAILURE:
      return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
    case Status.SUCCESS:
      return (
        &amp;lt;&amp;gt;
          &amp;lt;GoogleMap/&amp;gt;
          &amp;lt;Dashboard/&amp;gt;
          &amp;lt;MarkerContainer/&amp;gt;
        &amp;lt;/&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-09-2023 03-12-44.gif&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pLTlR/btszZntsv45/a01lWQF7wI1FTccXuGOTB0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pLTlR/btszZntsv45/a01lWQF7wI1FTccXuGOTB0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pLTlR/btszZntsv45/a01lWQF7wI1FTccXuGOTB0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/pLTlR/btszZntsv45/a01lWQF7wI1FTccXuGOTB0/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;724&quot; height=&quot;628&quot; data-filename=&quot;Nov-09-2023 03-12-44.gif&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&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;2. 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;참고로 이 마커 컴포넌트에 애니메이션을 적용하면 어떤 컴포넌트가 실제로 렌더링 되는지 눈으로 확인이 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-19-2023 00-50-09.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5o8b9/btsAzdXid2f/gjYfrtu1qelIhVTXhskPe1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5o8b9/btsAzdXid2f/gjYfrtu1qelIhVTXhskPe1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5o8b9/btsAzdXid2f/gjYfrtu1qelIhVTXhskPe1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/5o8b9/btsAzdXid2f/gjYfrtu1qelIhVTXhskPe1/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;640&quot; height=&quot;404&quot; data-filename=&quot;Nov-19-2023 00-50-09.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 마커 렌더링에 동적인 애니메이션을 적용해보면 위와 같습니다. (새로 렌더링 된 컴포넌트만 애니메이션이 붙습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 위 구조를 고수하게 되면 gif의 마지막 부분처럼 마커 렌더링이 산발적으로 업데이트 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마커의 개수가 아주 많아지면 성능이 많이 떨어진다는 뜻입니다. (하지만 몇 개 없는 상황에서는 최적의 선택입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하기 위해 useLayoutEffect를 사용한다거나, 마커 렌더링을 관리하는 전용 훅을 제작하여 DOM 접근을 최소화 하는 방안도 있습니다. (아니면 React 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;실제 프로젝트에서는 트러블 슈팅을 위해 렌더링 관련하여 최적화 과정을 촘촘하게 거쳤으며, 영역을 크게 잡고 재요청을 막는 캐싱 처리를 한다던지, 한 번 알게된 정보는 덜 요청하는 기능을 넣는다던지, 여러 작업들을 거쳐 개선을 거듭하였습니다.&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;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이 글에서 제안하는 방식은 최선이 아닙니다.&lt;/span&gt;&lt;/b&gt;&lt;/i&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;span style=&quot;color: #ee2323;&quot;&gt;React, Tanstack Query, Google Maps API를 유기적으로 동작하게 하는 단단한 기반&lt;/span&gt;이 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 최적화를 마저 언급하지 않는 이유는 이미 글이 너무 길어졌기 때문입니다.  &lt;/p&gt;
&lt;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;1972&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dS4gCN/btszX1j7ZaY/PAmqnz5uz4ITGlgFoL6Ze0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dS4gCN/btszX1j7ZaY/PAmqnz5uz4ITGlgFoL6Ze0/img.png&quot; data-alt=&quot;마지막으로 구조를 정리하다보니 순서가 일부 바뀌었다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dS4gCN/btszX1j7ZaY/PAmqnz5uz4ITGlgFoL6Ze0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdS4gCN%2FbtszX1j7ZaY%2FPAmqnz5uz4ITGlgFoL6Ze0%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;1972&quot; height=&quot;1166&quot; data-origin-width=&quot;1972&quot; data-origin-height=&quot;1166&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;글을 쓰면서 레포를 새로 열고 코드를 새로 작성해서 그런 것이니 필요한 구조로 배치하면 됩니다!&lt;/p&gt;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>Google Maps API</category>
      <category>reaact</category>
      <category>react wrapper</category>
      <category>Tanstack Query</category>
      <category>구글 지도</category>
      <category>리액트</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/160</guid>
      <comments>https://leirbag.tistory.com/160#entry160comment</comments>
      <pubDate>Thu, 26 Oct 2023 09:54:08 +0900</pubDate>
    </item>
    <item>
      <title>카페인 서비스와 함께하는 전기차 여행 - 2</title>
      <link>https://leirbag.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은 &lt;a href=&quot;https://car-ffeine.github.io/40&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카페인 서비스 팀 블로그&lt;/a&gt; 에도 작성되어있는 글 입니다.&lt;br /&gt;이 글은 센트와 함께 작성한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요? 센트와 가브리엘 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희 카페인 팀에서는 지난번&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39&quot;&gt;카페인 서비스 1차 체험&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;진행 이후 일부 기능 개선이 있었습니다. 기능 개선의 유용성을 판별하고자 카페인 서비스 2차 체험을 다녀왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희 팀에서 1차 체험 이후 개선한 사항은 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-지역검색&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 지역검색&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#1-%EC%A7%80%EC%97%AD%EA%B2%80%EC%83%89&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NGZhy/btszb8J2j9a/pwM8kE1iW7kHkSJ1NwiTOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NGZhy/btszb8J2j9a/pwM8kE1iW7kHkSJ1NwiTOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NGZhy/btszb8J2j9a/pwM8kE1iW7kHkSJ1NwiTOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNGZhy%2Fbtszb8J2j9a%2FpwM8kE1iW7kHkSJ1NwiTOk%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;551&quot; height=&quot;396&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제는 검색어를 입력하는 경우, 전국 도시의 주소가 같이 제공됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-충전소-마커를-확인할-수-있는-지도-영역-확장&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 충전소 마커를 확인할 수 있는 지도 영역 확장&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#2-%EC%B6%A9%EC%A0%84%EC%86%8C-%EB%A7%88%EC%BB%A4%EB%A5%BC-%ED%99%95%EC%9D%B8%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%A7%80%EB%8F%84-%EC%98%81%EC%97%AD-%ED%99%95%EC%9E%A5&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;907&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bundEA/btsy8q6boOw/IPs4zzd90UiMVRB1jR1vl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bundEA/btsy8q6boOw/IPs4zzd90UiMVRB1jR1vl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bundEA/btsy8q6boOw/IPs4zzd90UiMVRB1jR1vl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbundEA%2Fbtsy8q6boOw%2FIPs4zzd90UiMVRB1jR1vl0%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;341&quot; height=&quot;907&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;907&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(기존에는 위 사진보다 좁은 영역만을 호출하는 것이 허용되었다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; 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;/ul&gt;
&lt;h2 id=&quot;체험-규칙-설정&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;체험 규칙 설정&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EC%B2%B4%ED%97%98-%EA%B7%9C%EC%B9%99-%EC%84%A4%EC%A0%95&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개선한 기능이 실제로 유용한지 확인해보기 위해 저희는 카페인 서비스 2차 체험의 규칙을 다음과 같이 설정했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희는 좀 더 의미있는 경험을 하기위해 1차 체험 때 정했던 규칙에 더해서 다음과 같은 추가 규칙을 설정하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;중간에-목표-지점이-많이-변경된다&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;중간에 목표 지점이 많이 변경된다&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EC%A4%91%EA%B0%84%EC%97%90-%EB%AA%A9%ED%91%9C-%EC%A7%80%EC%A0%90%EC%9D%B4-%EB%A7%8E%EC%9D%B4-%EB%B3%80%EA%B2%BD%EB%90%9C%EB%8B%A4&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 카페인 서비스 1차 체험에서는 지역 검색이 없어 목표 지점을 찾는 것이 불편했습니다. 1차 체험 이후 지역 검색이 추가 되었으므로 이 기능이 얼마나 유용한지 경험해보고자 이 규칙을 설정했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가로 목표 지점 주변의 충전소를 확인할 때 새로 추가된 지도 영역 확장이 얼마나 유용한지도 경험해보고자 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;체험-개요&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;체험 개요&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EC%B2%B4%ED%97%98-%EA%B0%9C%EC%9A%94&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFVoJL/btszcHk41Zx/OhtuIBRehwxUz6d9GTYOd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFVoJL/btszcHk41Zx/OhtuIBRehwxUz6d9GTYOd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFVoJL/btszcHk41Zx/OhtuIBRehwxUz6d9GTYOd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFVoJL%2FbtszcHk41Zx%2FOhtuIBRehwxUz6d9GTYOd1%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;494&quot; height=&quot;720&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잠실역 출발&lt;/li&gt;
&lt;li&gt;하남 만두집&lt;/li&gt;
&lt;li&gt;다음 목적지 설정&lt;/li&gt;
&lt;li&gt;판교&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;체험-후기&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;체험 후기&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EC%B2%B4%ED%97%98-%ED%9B%84%EA%B8%B0&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;잠실역-출발&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;잠실역 출발&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EC%9E%A0%EC%8B%A4%EC%97%AD-%EC%B6%9C%EB%B0%9C&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1C3q/btszbNzlXN8/3v5IKeeQsWteDtz8NKJQK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1C3q/btszbNzlXN8/3v5IKeeQsWteDtz8NKJQK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1C3q/btszbNzlXN8/3v5IKeeQsWteDtz8NKJQK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1C3q%2FbtszbNzlXN8%2F3v5IKeeQsWteDtz8NKJQK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;400&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쏘카에서 EV6를 대여해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;가브리엘,&lt;span&gt;&amp;nbsp;&lt;/span&gt;센트,&lt;span&gt;&amp;nbsp;&lt;/span&gt;키아라가 잠실역에서 출발하였습니다. 저녁 퇴근 이후에 남이섬을 가려고 목적지를 설정하였으나 배가 너무 고파서 가는 길에 식사를 하자고 얘기가 나왔습니다.&lt;/p&gt;
&lt;h3 id=&quot;하남-만두집&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;하남 만두집&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%ED%95%98%EB%82%A8-%EB%A7%8C%EB%91%90%EC%A7%91&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;500&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IGlfV/btsy9kEFLS9/wK4FvV9LaDFHUS9OnwiSn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IGlfV/btsy9kEFLS9/wK4FvV9LaDFHUS9OnwiSn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IGlfV/btsy9kEFLS9/wK4FvV9LaDFHUS9OnwiSn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIGlfV%2Fbtsy9kEFLS9%2FwK4FvV9LaDFHUS9OnwiSn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;667&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;식당 주변을 가기 위해 지역 검색을 처음으로 사용하여 식당과 가까운 지역을 탐색할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 식당에는 충전소가 없다는 사실을 알게되어, 근처 충전소를 찾아보기 위해서 지도를 축소했더니 1차 체험때와는 달리 더 넓은 영역을 보여줬습니다. 이전에는 마커 자체가 보이지 않아 답답하였으나, 이제는 더 넓은 영역을 조회할 수 있게 되어 편리했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 체험 이후로 피드백을 자체 수집하여 개발한 기능들이 편하다는 것을 식당에 가는 길에 느낄 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;다음-목적지-설정&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;다음 목적지 설정&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EB%8B%A4%EC%9D%8C-%EB%AA%A9%EC%A0%81%EC%A7%80-%EC%84%A4%EC%A0%95&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbcUGx/btsy8tIBSIy/cU8PBJmcxrujAllmiz0eKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbcUGx/btsy8tIBSIy/cU8PBJmcxrujAllmiz0eKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbcUGx/btsy8tIBSIy/cU8PBJmcxrujAllmiz0eKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbcUGx%2Fbtsy8tIBSIy%2FcU8PBJmcxrujAllmiz0eKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하남 만두집에서 식사를 하다가 알게된 사실은, 남이섬은 생각보다 너무 멀다는 것이었습니다. 식사를 마치고 남이섬에 가면, 충전도 제대로 못하고 돌아올 판이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;식사를 하면서 다른 목적지를 알아봤는데, 가브리엘이 예전에 가봤던 곳 중에서 남양주의 물의 정원이 시간을 떼우기 좋다는 소리를 하였습니다. 따라서 물의 정원을 검색해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;놀랍게도 물의정원은 검색결과에 없었습니다!&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어쩔 수 없이 카카오 지도로 물의 정원 위치를 확인하여 주소를 알아내었고, 이 주소를 카페인 검색창에 넣었습니다. 저희는 이 과정에서 카페인 서비스는 업체명 조회가 안된다는 것이 치명적인 단점이라고 생각했습니다. 다만, 이 기능은 검색 할 때마다 많은 비용이 청구되어 현실적으로 지금 당장 기능을 넣는 것은 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 주소 검색을 통해 물의 정원과 가장 가까운 충전소를 알아내었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;500&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JR2rw/btszbvZZQa6/ojREeqiJ8QzXuxLt3Khs50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JR2rw/btszbvZZQa6/ojREeqiJ8QzXuxLt3Khs50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JR2rw/btszbvZZQa6/ojREeqiJ8QzXuxLt3Khs50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJR2rw%2FbtszbvZZQa6%2FojREeqiJ8QzXuxLt3Khs50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;1082&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd67e0/btsy8avLOAz/RqmlL4JziukSZXgZvDI8S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd67e0/btsy8avLOAz/RqmlL4JziukSZXgZvDI8S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd67e0/btsy8avLOAz/RqmlL4JziukSZXgZvDI8S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd67e0%2Fbtsy8avLOAz%2FRqmlL4JziukSZXgZvDI8S0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;1082&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무려 걸어서 30분이나 걸리는 충전소였습니다!&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전기차 충전을 위해 왕복 1시간이나 걸리는 거리를 걸을 수 없다고 생각하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 지난 체험에서 전기차가 생각보다 배터리가 오래간다는 사실을 알고 있었지만, 만약 저희처럼 충전이 급한 사용자라면 목적지를 포기할 수 밖에 없겠구나 라는 생각이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 정한 목적지는, 의외의 결정이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;굉장히 발전된 첨단 도시로 알려진 판교였습니다!&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실은 앞으로 갈지도 모르는 판교를 미리 구경이나 해보자는게 이유였지만 비밀입니다(?)&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 판교역은 IT서비스 회사들이 많이 몰려있는 곳이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;370&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPP1o/btszbMtFyLC/yKHLUU6eW7gsN4tzlK3Or0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPP1o/btszbMtFyLC/yKHLUU6eW7gsN4tzlK3Or0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPP1o/btszbMtFyLC/yKHLUU6eW7gsN4tzlK3Or0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPP1o%2FbtszbMtFyLC%2FyKHLUU6eW7gsN4tzlK3Or0%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;370&quot; height=&quot;630&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지도를 판교역으로 이동하여 외부인 개방인 충전소를 찾았는데, 판교공영주차장이 보여서 해당 충전소를 목적지로 잡고 출발했습니다.&lt;/p&gt;
&lt;h3 id=&quot;판교&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;판교&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%ED%8C%90%EA%B5%90&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하남에서 판교를 가기 위해서는 서하남IC를 지나야했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가는 길에 우리 서비스에 나오는 정보와 실제 정보가 일치하는지 점검차 서하남 간이 휴게소를 들려봤습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;500&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVcvxA/btszcQvwjTE/tO4EE0NQTQuTu5kT5W3Z9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVcvxA/btszcQvwjTE/tO4EE0NQTQuTu5kT5W3Z9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVcvxA/btszcQvwjTE/tO4EE0NQTQuTu5kT5W3Z9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVcvxA%2FbtszcQvwjTE%2FtO4EE0NQTQuTu5kT5W3Z9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;667&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검색 당시에는 2대의 충전기가 있다고 나왔고, 둘다 사용이 가능하다고 되어있었는데 실제로 확인해보니 일치하는 것을 확인했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼 길을 달려 판교에 도착하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주차장에 들어오기 전, 카페인 서비스를 확인해보니 판교공영주차장의 충전기 총 12기 중 10기가 사용가능한 상태였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정작 들어와서 보니 입구부터 너무 많은 전기차들이 충전기를 사용중이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;500&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vcRFI/btszb2C0ma9/6lEgVpd6wzNnEWkHnMuuck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vcRFI/btszb2C0ma9/6lEgVpd6wzNnEWkHnMuuck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vcRFI/btszb2C0ma9/6lEgVpd6wzNnEWkHnMuuck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvcRFI%2Fbtszb2C0ma9%2F6lEgVpd6wzNnEWkHnMuuck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;667&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&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;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3QHBW/btszb84iMXW/S3IC5gjnkLDLAdKfAsCkm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3QHBW/btszb84iMXW/S3IC5gjnkLDLAdKfAsCkm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3QHBW/btszb84iMXW/S3IC5gjnkLDLAdKfAsCkm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3QHBW%2Fbtszb84iMXW%2FS3IC5gjnkLDLAdKfAsCkm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;충전기를 꽂고 나서 알게된 것은 카페인 서비스에 나온 충전소 회사명과 방금 꽂은 충전기 회사명이 다르다는 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;알고보니 음성 인식으로 네비에 검색한 충전소는 판교공영주차장이 아닌 판교역 환승 주차장이라 엉뚱한 곳으로 온 것이었습니다!!!&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행인 점은 우리 서비스에서 제공하는 충전기 사용 여부 정보가 잘못된 것이 아니었다는 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;500&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eoZ3Oc/btszbjFgjt7/15WmqgSgOUt7X9n0ZOSTKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eoZ3Oc/btszbjFgjt7/15WmqgSgOUt7X9n0ZOSTKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eoZ3Oc/btszbjFgjt7/15WmqgSgOUt7X9n0ZOSTKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeoZ3Oc%2FbtszbjFgjt7%2F15WmqgSgOUt7X9n0ZOSTKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;375&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&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;418&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UXtJt/btszb3WdWkq/zPHnCmWwEiHRk27fwcYrq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UXtJt/btszb3WdWkq/zPHnCmWwEiHRk27fwcYrq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UXtJt/btszb3WdWkq/zPHnCmWwEiHRk27fwcYrq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUXtJt%2Fbtszb3WdWkq%2FzPHnCmWwEiHRk27fwcYrq1%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;418&quot; height=&quot;228&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도착해보니 1층의 충전기들이 모두 공사중이었고, 서비스의 정보가 실제로도 불일치 하는 줄 알았습니다. 다시 상세 정보를 보니 3~6층에 충전기들에 대한 정보라는 것이 명시되어 있었고, 실제로도 이와 동일한 것을 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVtuJK/btszbOrtBIk/UHDosyNKRKuZlKVy61pZh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVtuJK/btszbOrtBIk/UHDosyNKRKuZlKVy61pZh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVtuJK/btszbOrtBIk/UHDosyNKRKuZlKVy61pZh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVtuJK%2FbtszbOrtBIk%2FUHDosyNKRKuZlKVy61pZh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;375&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희는 시간이 너무 흘러 다시 잠실로 돌아와 차를 반납하고 체험을 마무리 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EA%B2%B0%EB%A1%A0&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;불편했던-점&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;불편했던 점&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EB%B6%88%ED%8E%B8%ED%96%88%EB%8D%98-%EC%A0%90&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; text-align: start;&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;/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;현위치를 나타내는 핀 (1차 체험기에서도 언급했던 부분)&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;다음-목표&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;다음 목표&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/40#%EB%8B%A4%EC%9D%8C-%EB%AA%A9%ED%91%9C&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 불편했던점을 개선하기 위해 다음과 같은 기능 개선을 추가로 진행할 예정입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; 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: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카페인 서비스만으로 목적지를 찾아갈 수 있도록 하기 위해서 특정 장소 검색을 추가하고 싶지만, 해당 기능을 구현하기 위해선 검색당 비용이 많이 청구되는 장소 검색 API를 추가해야 했기에 현실적으로 지금 당장 구현하기 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이상 카페인 사용기였습니다.&lt;/p&gt;</description>
      <category>Study/일상&amp;middot;회고</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/159</guid>
      <comments>https://leirbag.tistory.com/159#entry159comment</comments>
      <pubDate>Thu, 26 Oct 2023 01:22:50 +0900</pubDate>
    </item>
    <item>
      <title>React에서 Google Maps API를 자유롭게 사용하는 방법 (@googlemaps/react-wrapper)</title>
      <link>https://leirbag.tistory.com/158</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;google_maps.png&quot; data-origin-width=&quot;2275&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dw7XX/btszXY7fCub/n4rsr7bOhFqh5K52sahIU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dw7XX/btszXY7fCub/n4rsr7bOhFqh5K52sahIU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dw7XX/btszXY7fCub/n4rsr7bOhFqh5K52sahIU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDw7XX%2FbtszXY7fCub%2Fn4rsr7bOhFqh5K52sahIU1%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;217&quot; height=&quot;195&quot; data-filename=&quot;google_maps.png&quot; data-origin-width=&quot;2275&quot; data-origin-height=&quot;2048&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;React에서 Google Maps API를 사용하기 위해 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/JustFly1984/react-google-maps-api&quot;&gt;@react-google-maps/api&lt;/a&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/tomchentw/react-google-maps&quot;&gt;react-google-maps&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 라이브러리들을 사용하는 방법이 있을 것입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 라이브러리들을 사용하면 Google Maps API에 있는 주요 기능들을 React 컴포넌트처럼 사용할 수 있지만, 여러 가지 문제가 있습니다.&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제 Google Maps API에서 제공하는 환경과 달라 제대로 된 기능을 활용하기 어렵습니다.&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;과거 React Native에 Google Maps API를 사용하기 위해 라이브러리(&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/react-native-maps/react-native-maps&quot;&gt;react-native-maps&lt;/a&gt;)를 사용하였는데, Vanilla JS 수준에서 조작하기 어려운 상황이 많았습니다.&lt;/span&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;최근 vis.gl에서도 &lt;a href=&quot;https://github.com/visgl/react-map-gl&quot;&gt;react-map-gl&lt;/a&gt;를 출시하여 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Google Maps Platform과 협업을 하고 있는 것으로 보이지만, &lt;/span&gt;Google Maps API의 모든 기능을 사용하는 것은 어려워 지금 당장 추천하기는 어렵습니다.&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;라이브러리의 유지보수가 중단될 수 있습니다.&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Google Maps API는 다른 지도 API에 비해 변화가 많은 라이브러리입니다. 주기적으로 업데이트 되고, 신기능이 출시되는 과정에서 특정 라이브러리에 의존하여 개발을 하다보면 대응하기 어려울 수 있습니다. 예를들어 r&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;eact-google-maps의 경우에는 개발자가 유지보수를 중단한 전례가 있습니다. React의 버전이 더이상 맞지 않는 것은 물론이고, Google Maps API의 버전도 맞지 않았습니다. 다른 라이브러리들도 마찬가지로 개발자가 대응하지 않으면 사용하지 못하는 기능들이 있습니다. 물론 라이브러리 개발자들이 잘 대응해주겠지만, 최근에 추가된 AdvancedMarker 객체의 경우에도, 컴포넌트가 미리 구현되어 있지 않다면 적용하기 어려워 구버전의 Marker를 계속 사용해야 할 것입니다.&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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;@googlemaps/react-wrapper&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Maps Platform 팀에서는 공식 라이브러리를 몇 가지 제공해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #ffffff; color: #cb3837; text-align: start;&quot; href=&quot;https://www.npmjs.com/package/@googlemaps/js-api-loader&quot;&gt;@googlemaps/js-api-loader&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하는 것이 가장 저수준의 개발을 할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 라이브러리는 Google Maps API를 React 환경으로 끌어오는 역할을 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 npm 설치와 달리, 지도 라이브러리를 동적으로 받아옵니다.&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;라이브러리를 동적으로 호출하려면 비동기적인 처리가 필요한데, 이를 Wrapper 컴포넌트 형태로 제공하는 라이브러리도 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/googlemaps/react-wrapper&quot;&gt;https://github.com/googlemaps/react-wrapper&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699325780802&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bks8UC/hyUrvRN3So/JceugDkIfz8s2FqqAvYvrK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot; data-og-url=&quot;https://github.com/googlemaps/react-wrapper&quot; data-og-source-url=&quot;https://github.com/googlemaps/react-wrapper&quot; data-og-host=&quot;github.com&quot; data-og-description=&quot;Wrap React components with this libary to load the Google Maps JavaScript API. - GitHub - googlemaps/react-wrapper: Wrap React components with this libary to load the Google Maps JavaScript API.&quot; data-og-title=&quot;GitHub - googlemaps/react-wrapper: Wrap React components with this libary to load the Google Maps JavaScript API.&quot; data-og-type=&quot;object&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/googlemaps/react-wrapper&quot; data-source-url=&quot;https://github.com/googlemaps/react-wrapper&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bks8UC/hyUrvRN3So/JceugDkIfz8s2FqqAvYvrK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - googlemaps/react-wrapper: Wrap React components with this libary to load the Google Maps JavaScript API.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Wrap React components with this libary to load the Google Maps JavaScript API. - GitHub - googlemaps/react-wrapper: Wrap React components with this libary to load the Google Maps JavaScript API.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@googlemaps/react-wrapper는 js-api-loader를 활용하여 지도를 호출하고, 로딩/에러/성공 처리를 분기처리 해주는 Wrapper 컴포넌트를 제공해주는 라이브러리입니다. 이 역시 Google Maps Platform 팀에서 제공하는 공식 라이브러리 입니다.&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;b&gt;Google Maps API Key 발급 받기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://console.cloud.google.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699333237404&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;Google 클라우드 플랫폼&quot; data-og-description=&quot;로그인 Google 클라우드 플랫폼으로 이동&quot; data-og-host=&quot;accounts.google.com&quot; data-og-source-url=&quot;https://console.cloud.google.com&quot; data-og-url=&quot;https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fconsole.cloud.google.com%2F&amp;amp;followup=https%3A%2F%2Fconsole.cloud.google.com%2F&amp;amp;ifkv=AVQVeyxCJkan3lFZF9e5Hgrp8qYqqGBukdjsROb-xeTXPcom6l1tBfR7E72Ra9scPHDO704isj-ZCQ&amp;amp;osid=1&amp;amp;passive=1209600&amp;amp;service=cloudconsole&amp;amp;flowName=WebLiteSignIn&amp;amp;flowEntry=ServiceLogin&amp;amp;dsh=S117506186%3A1699333235865164&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://console.cloud.google.com&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;Google 클라우드 플랫폼&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;로그인 Google 클라우드 플랫폼으로 이동&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;accounts.google.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;/p&gt;
&lt;p 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;&lt;b&gt;@googlemaps/react-wrapper 설치 하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1699333443961&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add @googlemaps/react-wrapper&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1699333456267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add -D @types/google.maps&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm으로 설치해도 괜찮습니다.&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;Wrapper를 활용하여 Google Maps API 호출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 환경을 다음과 같이 수정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699341540234&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {Status, Wrapper} from &quot;@googlemaps/react-wrapper&quot;;

const render = (status: Status) =&amp;gt; {
  switch (status) {
    case Status.LOADING:
      return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
    case Status.FAILURE:
      return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
    case Status.SUCCESS:
      return &amp;lt;&amp;gt;로드 성공&amp;lt;/&amp;gt;;
  }
};

function App() {

  return (
    &amp;lt;Wrapper apiKey=&quot;구글에서 받은 api key&quot; render={render}/&amp;gt;
  )
}

export default App&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apiKey에는 키를 직접 주입하는 방식도 있지만, 만일 github에 그대로 push하는 경우 보안 경고 메시지가 올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 시에는 google cloud 플랫폼에서 api key를 제한 걸 수 있어서 접속한 url에 따라 자동으로 차단되거나 로드가 허용 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 키 제한을 걸지 않았다면 누구나 사용할 수 있게 되므로 &lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;다음과 같이 적용하는 것을 권장합니다.&lt;/span&gt;&lt;/b&gt;&lt;/i&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. url 보호&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo8Hiq/btszSLBzSPv/fn1wjHgXEMM8k3BrRUtfY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo8Hiq/btszSLBzSPv/fn1wjHgXEMM8k3BrRUtfY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo8Hiq/btszSLBzSPv/fn1wjHgXEMM8k3BrRUtfY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo8Hiq%2FbtszSLBzSPv%2Ffn1wjHgXEMM8k3BrRUtfY0%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;1186&quot; height=&quot;1070&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속을 허용하는 url을 걸어두게 되면, 허용하는 주소에서만 접속이 가능합니다.&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. .env를 활용한 키 보호&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xtbqy/btszXpqDNnf/i4v40wYnILMJttUTSshJt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xtbqy/btszXpqDNnf/i4v40wYnILMJttUTSshJt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xtbqy/btszXpqDNnf/i4v40wYnILMJttUTSshJt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxtbqy%2FbtszXpqDNnf%2Fi4v40wYnILMJttUTSshJt0%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;1134&quot; height=&quot;754&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키값을 외부에 노출시키지 않도록 .env를 활용하여 보호합니다.&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;3. 할당량 제한&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2900&quot; data-origin-height=&quot;1988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l52lw/btszVvEXcl2/vrKvPUAkb2gXIDtgJftUeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l52lw/btszVvEXcl2/vrKvPUAkb2gXIDtgJftUeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l52lw/btszVvEXcl2/vrKvPUAkb2gXIDtgJftUeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl52lw%2FbtszVvEXcl2%2FvrKvPUAkb2gXIDtgJftUeK%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;2900&quot; height=&quot;1988&quot; data-origin-width=&quot;2900&quot; data-origin-height=&quot;1988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 키 접근 한도를 제한하여 여러분의 지갑을 지키는 방법도 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-07-2023 16-34-13.gif&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pP1CB/btszUd5BP3E/0qIxzcYq3oEu8GzzfhXkk1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pP1CB/btszUd5BP3E/0qIxzcYq3oEu8GzzfhXkk1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pP1CB/btszUd5BP3E/0qIxzcYq3oEu8GzzfhXkk1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/pP1CB/btszUd5BP3E/0qIxzcYq3oEu8GzzfhXkk1/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;368&quot; height=&quot;206&quot; data-filename=&quot;Nov-07-2023 16-34-13.gif&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 라이브러리가 로드되면 위와 같이 &lt;span style=&quot;background-color: #f6e199; color: #ee2323;&quot;&gt;로드 성공&lt;/span&gt;이 출력됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;지도 객체 생성 및 할당하기&amp;nbsp;&lt;/b&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;위 작업만을 담당할 컴포넌트를 만들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 StrictMode로 인하여 지도 객체를 일반적인 방법으로 생성하는 것은 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도를 생성하는 방식을 두 가지 버전으로 소개하겠습니다. Case1과 Case2로 나눠서 예제를 제시하겠습니다.&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;StrictMode는 애플리케이션의 컴포넌트를 두 번 렌더링하여 잠재적인 부작용을 식별하는 데 도움을 줍니다. 이중 렌더링은 컴포넌트의 순수성과 예측 가능한 동작을 확인하는 데 유용합니다. 다만, 이중 렌더링 현상으로 인해 development모드에서는 지도도 두 번 부착되게 됩니다. 따라서 다음과 같이 잠재적인 문제를 회피할 수 있습니다.&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;GoogleMap.tsx (Case1)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699343528335&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useEffect, useRef, useState} from &quot;react&quot;;

function GoogleMap(){

  const ref = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const [googleMap, setGoogleMap] = useState&amp;lt;google.maps.Map&amp;gt;();

  useEffect(() =&amp;gt; {
    if (ref.current) {
      const initialMap = new window.google.maps.Map(ref.current, {
        center: {
          lat: 37.5,
          lng: 127.0,
        },
        zoom: 16,
      });

      setGoogleMap(initialMap);
    }
  }, []);

  return &amp;lt;div ref={ref} id=&quot;map&quot; style={{ minHeight: '100vh' }} /&amp;gt;
}

export default GoogleMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식은 useRef를 활용하여 지도가 단 한번 부착되는 방식입니다.&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;GoogleMap.tsx (Case2)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699369239709&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useEffect, useState} from &quot;react&quot;;

function GoogleMap(){

  const [googleMap, setGoogleMap] = useState&amp;lt;google.maps.Map&amp;gt;();

  useEffect(() =&amp;gt; {

    const container = document.createElement('div');

    container.id = 'map';
    container.style.minHeight = '100vh';

    document.body.appendChild(container);

    const instance = new window.google.maps.Map(container, {
      center: {
        lat: 37.5,
        lng: 127.0,
      },
      zoom: 16,
    } );

    setGoogleMap(instance);

    return () =&amp;gt; {
      document.body.removeChild(container);
    }

  } ,[])

  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
}

export default GoogleMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식은 useEffect의 clean up 함수를 사용하여 강제로 지도 conatainer를 제거하여 이중 렌더링 현상을 방지하는 현상입니다.&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;App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699343585460&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {Status, Wrapper} from &quot;@googlemaps/react-wrapper&quot;;
import GoogleMap from &quot;./GoogleMap.tsx&quot;;

const render = (status: Status) =&amp;gt; {
  switch (status) {
    case Status.LOADING:
      return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
    case Status.FAILURE:
      return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
    case Status.SUCCESS:
      return &amp;lt;GoogleMap /&amp;gt;;
  }
};

function App() {

  return (
    &amp;lt;Wrapper apiKey=&quot;구글에서 받은 api key&quot; render={render}/&amp;gt;
  )
}

export default App&lt;/code&gt;&lt;/pre&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;3420&quot; data-origin-height=&quot;2224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qbnnP/btszOLIOy11/OkfZAuZFQQM8h4y1bDXfi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qbnnP/btszOLIOy11/OkfZAuZFQQM8h4y1bDXfi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qbnnP/btszOLIOy11/OkfZAuZFQQM8h4y1bDXfi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqbnnP%2FbtszOLIOy11%2FOkfZAuZFQQM8h4y1bDXfi1%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;3420&quot; height=&quot;2224&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;2224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Maps API의 기능들을 사용하기 위해서는 반드시 google.maps.Map 객체(GoogleMap.tsx에서 생성한 googleMap 상태)에 자유롭게 접근할 수 있어야 합니다.&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;따라서 해당 상태를 클라이언트 전역 상태로 두거나 context api 혹은 useSyncExternalStore에 두는 방식으로도 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 useState에 할당했지만, &lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;여러분이 편한대로 접근하면 됩니다.&lt;/span&gt; &lt;/b&gt;&lt;/i&gt;(저는 지도 객체를 외부 시스템이라고 생각해서 useSyncExternalStore에서 접근 하는 것을 선호합니다.)&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;1790&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccmkOh/btszXDWQHil/mnxXJReyZKykFfKCT4sQUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccmkOh/btszXDWQHil/mnxXJReyZKykFfKCT4sQUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccmkOh/btszXDWQHil/mnxXJReyZKykFfKCT4sQUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccmkOh%2FbtszXDWQHil%2FmnxXJReyZKykFfKCT4sQUK%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;1790&quot; height=&quot;710&quot; data-origin-width=&quot;1790&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;벡터 지도 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-08-2023 00-28-50.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgXtgu/btszU2QroFI/duktncg2B2fefe0snFyF50/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgXtgu/btszU2QroFI/duktncg2B2fefe0snFyF50/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgXtgu/btszU2QroFI/duktncg2B2fefe0snFyF50/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bgXtgu/btszU2QroFI/duktncg2B2fefe0snFyF50/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;320&quot; height=&quot;272&quot; data-filename=&quot;Nov-08-2023 00-28-50.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도를 스크롤 해보면 래스터 지도임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Maps API는 벡터 지도를 지원하며, 이를 적용하면 좀 더 나은 사용자 경험을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(여담이지만 제가 Google Maps API를 선호했던 이유가 벡터 지도였는데, 최근 네이버 지도에서도 벡터 지도를 지원하기 시작했습니다.)&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;1752&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTLFjB/btszSLWbdL8/EMcgU7jcjKqNNBxb9auDgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTLFjB/btszSLWbdL8/EMcgU7jcjKqNNBxb9auDgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTLFjB/btszSLWbdL8/EMcgU7jcjKqNNBxb9auDgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTLFjB%2FbtszSLWbdL8%2FEMcgU7jcjKqNNBxb9auDgk%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;1752&quot; height=&quot;928&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 클라우드 플랫폼에 접속해서 지도 ID를 발급 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아이디는 api key가 아닌, 지도의 설정을 식별하는 역할을 합니다.&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-origin-width=&quot;2188&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ai9vs/btszTCkv7IN/ukbrajqKr0iKqPPbaNcIC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ai9vs/btszTCkv7IN/ukbrajqKr0iKqPPbaNcIC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ai9vs/btszTCkv7IN/ukbrajqKr0iKqPPbaNcIC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAi9vs%2FbtszTCkv7IN%2FukbrajqKr0iKqPPbaNcIC0%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;2188&quot; height=&quot;768&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 지도 아이디를 복사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKa4Dp/btszXZZGvgP/dsai2V09a1fErS5WGsad50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKa4Dp/btszXZZGvgP/dsai2V09a1fErS5WGsad50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKa4Dp/btszXZZGvgP/dsai2V09a1fErS5WGsad50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKa4Dp%2FbtszXZZGvgP%2Fdsai2V09a1fErS5WGsad50%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;1722&quot; height=&quot;566&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복사한 아이디를 mapId에 전달합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Nov-08-2023 00-45-24.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uwS2I/btszXoS57vq/jwyrBnUuXPQBzO7brCpnPK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uwS2I/btszXoS57vq/jwyrBnUuXPQBzO7brCpnPK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uwS2I/btszXoS57vq/jwyrBnUuXPQBzO7brCpnPK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/uwS2I/btszXoS57vq/jwyrBnUuXPQBzO7brCpnPK/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;320&quot; height=&quot;272&quot; data-filename=&quot;Nov-08-2023 00-45-24.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 지도로 전환 된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWO3gv/btszRRWwu0u/bzKOQ9xEgAz9WNOz8qJQNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWO3gv/btszRRWwu0u/bzKOQ9xEgAz9WNOz8qJQNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWO3gv/btszRRWwu0u/bzKOQ9xEgAz9WNOz8qJQNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWO3gv%2FbtszRRWwu0u%2FbzKOQ9xEgAz9WNOz8qJQNk%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;1642&quot; height=&quot;1096&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCnyNX/btszYEudICp/QnmVxi88kV6mMPkycwM2lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCnyNX/btszYEudICp/QnmVxi88kV6mMPkycwM2lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCnyNX/btszYEudICp/QnmVxi88kV6mMPkycwM2lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCnyNX%2FbtszYEudICp%2FQnmVxi88kV6mMPkycwM2lk%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;1860&quot; height=&quot;1416&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담이지만 발급받은 지도 아이디에 지도 스타일을 연동하면 지도 스타일링도 커스텀 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;1220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTZKaQ/btszXHLDHg0/VUY2mNiueHLVuq0nUlvEV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTZKaQ/btszXHLDHg0/VUY2mNiueHLVuq0nUlvEV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTZKaQ/btszXHLDHg0/VUY2mNiueHLVuq0nUlvEV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTZKaQ%2FbtszXHLDHg0%2FVUY2mNiueHLVuq0nUlvEV0%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;1482&quot; height=&quot;1220&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;1220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고급 마커(Advanced Marker)란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 말, Google Maps API의 Marker 기능이 새롭게 출시되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 출시된 Advanced Marker는 현 시점에도 이 기능은 베타이지만, 기존 Marker 보다 훨씬 자유로운 사용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 베타임에도 불구하고 Google Maps API의 강력한 주요 기능으로 떠오를 가능성이 있다고 생각하는데, 그 이유는 다음과 같습니다. (이 객체도 제가 Google Maps API를 선호하는 이유 중 하나입니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPhHdC/btszZjJ7k4Q/ZO3C4VQKzFx8oidQGbS5L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPhHdC/btszZjJ7k4Q/ZO3C4VQKzFx8oidQGbS5L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPhHdC/btszZjJ7k4Q/ZO3C4VQKzFx8oidQGbS5L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPhHdC%2FbtszZjJ7k4Q%2FZO3C4VQKzFx8oidQGbS5L1%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;696&quot; height=&quot;900&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼, 이미지 적용 없이도 기존 마커를 개량하여 사용할 수 있습니다. (위 기능은 PinView를 활용한 예제입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉, 굳이 마커용 이미지를 지도 위에 띄우지 않아도 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;코드에서 직접 기본 빨간색 핀의 색상, 배경, 아이콘 및 윤곽선을 변경할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;1220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RQMEf/btszQCrGcn7/Oz1Vj8gv4BBsEkS0HbZ7k0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RQMEf/btszQCrGcn7/Oz1Vj8gv4BBsEkS0HbZ7k0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RQMEf/btszQCrGcn7/Oz1Vj8gv4BBsEkS0HbZ7k0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/RQMEf/btszQCrGcn7/Oz1Vj8gv4BBsEkS0HbZ7k0/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;928&quot; height=&quot;1220&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;1220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5f6368; text-align: start;&quot;&gt;위 예제처럼 CSS를 사용하여 크기 조정, 불투명도, 위치, 색상 등을 변경하는 등 고급 마커의 스타일을 동적으로 지정하고 애니메이션을 적용할 수 있습니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c219ft/btszX14et6n/MScLwGCqEJdHi62hKdunX1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c219ft/btszX14et6n/MScLwGCqEJdHi62hKdunX1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c219ft/btszX14et6n/MScLwGCqEJdHi62hKdunX1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/c219ft/btszX14et6n/MScLwGCqEJdHi62hKdunX1/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;860&quot; height=&quot;1140&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제처럼 HTML 엘리먼트를 마커에 적용하는 기능도 지원하게 되었습니다. 즉, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;AdvancedMarker 객체를 사용하면 아주 자유로운 Marker 사용&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699373384535&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;Advanced Markers - Google Maps Platform&quot; data-og-description=&quot;More performant, more customizable, more feature rich.&quot; data-og-host=&quot;storage.googleapis.com&quot; data-og-source-url=&quot;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&quot; data-og-url=&quot;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/blNjGY/hyUrE9jQlW/9ezS8byoNWhE9rUNGv6JK1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/hhQ4j/hyUrs8VDCF/5oF6YJ3rFljEBqTtOwK961/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://storage.googleapis.com/gmp-maps-demos/advanced-markers/index.html#intro-page&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/blNjGY/hyUrE9jQlW/9ezS8byoNWhE9rUNGv6JK1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/hhQ4j/hyUrs8VDCF/5oF6YJ3rFljEBqTtOwK961/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;Advanced Markers - Google Maps Platform&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;More performant, more customizable, more feature rich.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;storage.googleapis.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크에 접속하면 Advanced Markers의 활용 사례를 확인할 수 있으니 참고 바랍니다.&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;Nov-08-2023 01-12-56.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OXGmj/btszUu7qe2r/8A1L9iMMNwa4ezokJoERAk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OXGmj/btszUu7qe2r/8A1L9iMMNwa4ezokJoERAk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OXGmj/btszUu7qe2r/8A1L9iMMNwa4ezokJoERAk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/OXGmj/btszUu7qe2r/8A1L9iMMNwa4ezokJoERAk/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;320&quot; height=&quot;254&quot; data-filename=&quot;Nov-08-2023 01-12-56.gif&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 영상은 실제로 마커에 적용해본 애니메이션 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Advanced Marker는 기존보다 66%의 성능 개선이 있으며, 더 많은 마커를 동시에 핸들링 할 수 있습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Advanced Marker 렌더링 하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699375942824&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {Status, Wrapper} from &quot;@googlemaps/react-wrapper&quot;;
import GoogleMap from &quot;./GoogleMap.tsx&quot;;

const render = (status: Status) =&amp;gt; {
  switch (status) {
    case Status.LOADING:
      return &amp;lt;&amp;gt;로딩중...&amp;lt;/&amp;gt;;
    case Status.FAILURE:
      return &amp;lt;&amp;gt;에러 발생&amp;lt;/&amp;gt;;
    case Status.SUCCESS:
      return &amp;lt;GoogleMap /&amp;gt;;
  }
};

function App() {

  return (
    &amp;lt;Wrapper apiKey=&quot;구글에서 받은 api key&quot; render={render} libraries={['marker']}/&amp;gt;
  )
}

export default App&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리로 marker를 추가합니다. (베타 버전 종료 이후에는 삭제될 수도 있습니다.)&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;GoogleMap.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699375973983&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useEffect, useState} from &quot;react&quot;;
import {createRoot} from &quot;react-dom/client&quot;;

function GoogleMap(){

  const [googleMap, setGoogleMap] = useState&amp;lt;google.maps.Map&amp;gt;();

  useEffect(() =&amp;gt; {

    const mapContainer = document.createElement('div');

    mapContainer.id = 'map';
    mapContainer.style.minHeight = '100vh';

    document.body.appendChild(mapContainer);

    const instance = new window.google.maps.Map(mapContainer, {
      center: {
        lat: 37.5,
        lng: 127.0,
      },
      zoom: 16,
      mapId: '92cb7201b7d43b21',
      disableDefaultUI: true,
      clickableIcons: false,
      minZoom: 10,
      maxZoom: 18,
      gestureHandling: 'greedy',
      restriction: {
        latLngBounds: {
          north: 39,
          south: 32,
          east: 132,
          west: 124,
        },
        strictBounds: true,
      },
    } );

    setGoogleMap(instance);


    return () =&amp;gt; {
      document.body.removeChild(mapContainer);
    }

  } ,[])

  useEffect(() =&amp;gt; {
    const markerContainer = document.createElement('div');
    const markerInstance = new google.maps.marker.AdvancedMarkerElement({
      position: {
        lat: 37.5,
        lng: 127.0,
      },
      map: googleMap,
      title: '마커',
      content: markerContainer,
    });
    createRoot(markerContainer).render(&amp;lt;div style={{backgroundColor:'yellow', padding:'10px'}}&amp;gt;마커&amp;lt;/div&amp;gt;);
    markerInstance.addListener('click', () =&amp;gt; {
      alert('마커 클릭')
    });

    return () =&amp;gt; {
      markerInstance.map = null;
    }
  }, [googleMap])

  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;
}

export default GoogleMap;&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;위 예제는 정말 억지로 useEffect를 동작시켜 마커 1개를 정적으로 등록해 본 것이지만, 만약 googleMap 상태를 전역으로 관리하고 마커 데이터를 서버에서 받아온다면,  별도의 컴포넌트에서도 마커를 동적으로도 그리고 지울 수 있게 됩니다. (이 글은 이후에 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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;markerContainer로 엘리먼트를 하나 생성하고, 해당 엘리먼트를 AdvancedMarkerElement에 지도 객체와 함께 부착합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createRoot로 markerContainer에 ReactDOM을 동적으로 생성하고, 해당 위치에 React Component을 그리는 방식입니다.&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;1632&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAKflf/btszQA1GDK3/KKS7VK6QYRSXL4AtdYjK61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAKflf/btszQA1GDK3/KKS7VK6QYRSXL4AtdYjK61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAKflf/btszQA1GDK3/KKS7VK6QYRSXL4AtdYjK61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAKflf%2FbtszQA1GDK3%2FKKS7VK6QYRSXL4AtdYjK61%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;1632&quot; height=&quot;1164&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제는 정말 대충 디자인 한 것이지만, 원하시면 더 다양한 디자인을 할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 마커를 생성하는 부분을 별도의 컴포넌트로 이전하면 좀 더 효율적인 구조가 탄생할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 초반 부에도 강조했지만, 이와 같은 방식으로 Google Maps API를 React와 결합하게 된다면&amp;nbsp;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Google Maps API의 공식문서에 나온 모든 기능을 아무런 제한 없이 사용할 수 있게 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이렇게 의존성이 강한 라이브러리를 사용하지 않고, 동적 호출에만 활용하면 Google Maps Plaform 팀에서도 권장하는 라이브러리 로드 방식에도 부합하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이제 여러분의 의도에 따라 지도를 호출하는 컴포넌트, 마커를 생성하는 컴포넌트(이 글에서는 미구현) 등을 적절하게 커스터마이징하여 재 렌더링을 피하는 설계도 진행할 수 있게 될 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;useMemo와 useCallback을 사용한다거나, &lt;a href=&quot;https://leirbag.tistory.com/151&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useSyncExternalStore를 사용&lt;/a&gt;한다거나, 클라이언트 상태 관리 라이브러리를 사용한다거나 등등 여러 아이디어를 통해 지도 환경과 React UI의 환경을 최대한 분리하려는 시도를 할 수 있으니 다양한 아이디어로 지도 기능을 활용하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;⚡️&lt;/b&gt;&lt;b&gt;⚡️&lt;/b&gt;&lt;b&gt;⚡️&lt;/b&gt;TanStack Query와 함께 동적으로 마커 렌더링하기&lt;b&gt;⚡️&lt;/b&gt;&lt;b&gt;⚡️&lt;/b&gt;&lt;b&gt;⚡️&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/160&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leirbag.tistory.com/160&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699468546873&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;React에서 Google Maps API, Tanstack Query로 대용량 마커 데이터를 관리하는 방법&quot; data-og-description=&quot;최근 진행한 프로젝트에서는 다음과 같은 핵심 기능이 필요했습니다. 1. 대량의 데이터(전국 약 6만여 건)를 사용자의 화면에 해당하는 영역만 적당하게 마커로 렌더링 해줄 것 2. 사용자가 움직&quot; data-og-host=&quot;leirbag.tistory.com&quot; data-og-source-url=&quot;https://leirbag.tistory.com/160&quot; data-og-url=&quot;https://leirbag.tistory.com/160&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ylelZ/hyUrAsCK77/gogVBcFVS1Z1tz4fExBbW1/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/K5Hg3/hyUuYSUWyV/nmNKwsxJgl9ikodOzhVnmk/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/bTdBCB/hyUrzAvEVT/GMvhZAmGJDS9Pt9ZDI3gE1/img.png?width=2140&amp;amp;height=1880&amp;amp;face=0_0_2140_1880&quot;&gt;&lt;a href=&quot;https://leirbag.tistory.com/160&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leirbag.tistory.com/160&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ylelZ/hyUrAsCK77/gogVBcFVS1Z1tz4fExBbW1/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/K5Hg3/hyUuYSUWyV/nmNKwsxJgl9ikodOzhVnmk/img.png?width=800&amp;amp;height=720&amp;amp;face=0_0_800_720,https://scrap.kakaocdn.net/dn/bTdBCB/hyUrzAvEVT/GMvhZAmGJDS9Pt9ZDI3gE1/img.png?width=2140&amp;amp;height=1880&amp;amp;face=0_0_2140_1880');&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에서 Google Maps API, Tanstack Query로 대용량 마커 데이터를 관리하는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최근 진행한 프로젝트에서는 다음과 같은 핵심 기능이 필요했습니다. 1. 대량의 데이터(전국 약 6만여 건)를 사용자의 화면에 해당하는 영역만 적당하게 마커로 렌더링 해줄 것 2. 사용자가 움직&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leirbag.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;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>@googlemaps/react-wrapper</category>
      <category>AdvancedMarker</category>
      <category>AdvancedMarkerElement</category>
      <category>Google Maps API</category>
      <category>google maps platform</category>
      <category>React</category>
      <category>react wrapper</category>
      <category>구글 지도</category>
      <category>리액트</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/158</guid>
      <comments>https://leirbag.tistory.com/158#entry158comment</comments>
      <pubDate>Tue, 24 Oct 2023 15:39:30 +0900</pubDate>
    </item>
    <item>
      <title>카페인 서비스와 함께하는 전기차 여행 - 1</title>
      <link>https://leirbag.tistory.com/157</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은 &lt;a href=&quot;https://car-ffeine.github.io/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카페인 서비스 팀 블로그&lt;/a&gt;에도 작성되어있는 글 입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카페인 서비스를 개발하면서 가장 많이 받은 피드백 중 하나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용자 경험이 반드시 필요하다는 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아무래도 전기자동차를 보유한 팀원들이 아무도 없다보니 실제 사용자들이 겪는 어려움을 예상할 수 밖에 없었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 전기 자동차 운전자들을 찾아내어 수차례 인터뷰를 진행하였는데 실제 차주들이 원하는 기능이 무엇인지, 어떤 어려움을 겪는지를 확인하여 이를 바탕으로 서비스를 개발하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스를 처음 개발하였을 때 가장 많이 받았던 피드백은 앱 로드 속도가 너무 느리다는 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스 초기에는 로딩 속도가 너무 느려서 사용자들에게 서비스 사용을 권장하기 미안한 상태였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 실사용자를 모집하는 것 보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;서비스 안정화에 집중하는 것이 최우선이라는 목표 아래에 서비스를 개선하는 시간을 가졌고, 지금은 로딩 속도가 빠르다는 피드백을 받고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 한 달간 서비스 안정화에 집중을 했다면, 이제는 사용자 경험을 개선하는데 집중을 해야할 때가 왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 사용자 유치를 위해 전기차 동호회 카페, 카카오톡 오픈채팅, 자동차 커뮤니티 등을 돌면서 홍보를 진행하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행히도 불특정 다수의 익명 사용자들을 손쉽게 구할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자들로부터 많은 피드백을 받았고, 해당 피드백을 저희 서비스에 최대한 반영하고자 노력했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만, 대부분의 사용자들이 저희 서비스에 단순히 방문하여 피드백을 준 것일 뿐&lt;span&gt;&amp;nbsp;&lt;/span&gt;실제로 사용하면서 피드백을 준 것 같지는 않다는 느낌을 받았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자들이 앱에 머무른 시간을 GA4를 통해 확인 하였을 때 평균 3분 이상이라는 긴 시간을 머물러서 앱을 꼼꼼하게 사용했을 것이라고 기대는 하였으나, 사용 중에 피드백을 준다거나 사용 후에 피드백을 준 것이 맞는지 확신하기 어려웠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다고 주변에서 전기차주들을 찾자니 문제가 발생하였습니다. 일단 전기자동차 보급률이 굉장히 낮았으며, 40~50대에 편중되어 있어 저희에게 협조해 줄 차주분들을 주변에서 찾기 어려웠습니다. (대부분 생업으로 인해 바쁘십니다 ㅠㅠ)&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 저희는 그냥 직접 서비스를 사용하면서 사용자 경험을 하기로 하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;카페인-서비스에서는요&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;카페인 서비스에서는요&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EC%B9%B4%ED%8E%98%EC%9D%B8-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C%EB%8A%94%EC%9A%94&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희 서비스에서 지원하는 핵심 기능은 다음과 같았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #1c1e21; text-align: start;&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;/ul&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;/li&gt;
&lt;li&gt;충전소 별 리뷰 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이외에도 많은 기능들이 있었지만, 위의 기능들이 사용자들이 가장 주력으로 사용할 것 같은 기능들이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;계획을-세워보자&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;계획을 세워보자&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EA%B3%84%ED%9A%8D%EC%9D%84-%EC%84%B8%EC%9B%8C%EB%B3%B4%EC%9E%90&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전기자동차 렌트에 앞서 어디에 방문할 지 부터 정해야 했습니다. 저희는 몇 가지 원칙을 가지고 방문지를 정하기로 했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;잘 모르는 지역일 것&lt;/li&gt;
&lt;li&gt;도착지에 충전소가 반드시 있을 것&lt;/li&gt;
&lt;li&gt;타사 앱을 전혀 사용하지 말 것&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단, 제가 처음 정했던 목표는 경상남도 진주시였습니다. 진주시에서 복귀해야하는 팀원이 있던 점, 방문해 본 적이 없는 도시인 점, 장거리라서 충전기 사용이 필연적인 점 등 여러 가지 이유로 진주시를 방문하기로 결정했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카페인 서비스를 킨 순간 눈앞이 캄캄해졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;진주시가 어디에 있지?&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crJF0f/btsx2wE3eZJ/rZcLZUkDMv5kXkeEU28DW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crJF0f/btsx2wE3eZJ/rZcLZUkDMv5kXkeEU28DW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crJF0f/btsx2wE3eZJ/rZcLZUkDMv5kXkeEU28DW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrJF0f%2Fbtsx2wE3eZJ%2FrZcLZUkDMv5kXkeEU28DW1%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;392&quot; height=&quot;225&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행히 진주시를 검색하니 주소 기반으로 검색이 되었습니다! 진주시를 검색한 것은 아니지만 간접적이라도 검색이 되는 것을 보고 안심했습니다. 아무 충전소를 눌러서 진주시로 이동하는 것은 가능했습니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;여기에서 저는 이 과정에서 도시나 지역 검색 기능이 반드시 필요하다고 생각했습니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 너무 멀었습니다. 왕복 700km를 생각해야하여 1박 2일이 필수였고, 팀원들 간에 일정을 조정하기가 너무 어려웠습니다. 따라서 다른 도시를 찾아보기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sCWHk/btsxZABfHvJ/XTkOU0QFYwrK3HBgw5vYH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sCWHk/btsxZABfHvJ/XTkOU0QFYwrK3HBgw5vYH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sCWHk/btsxZABfHvJ/XTkOU0QFYwrK3HBgw5vYH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsCWHk%2FbtsxZABfHvJ%2FXTkOU0QFYwrK3HBgw5vYH0%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;341&quot; height=&quot;569&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러던 중, 제가 전에 방문했던 파주시의&lt;span&gt;&amp;nbsp;&lt;/span&gt;마장호수가 생각났습니다. 서울에서 꽤나 먼 거리(약 50km)에 있었고, 적당히 시간을 보낼만한 장소였습니다. 다행히도 충전소의 이름이&lt;span&gt;&amp;nbsp;&lt;/span&gt;마장호수관리사무소여서 카페인 서비스를 통해 바로 찾을 수 있었습니다. 심지어 마장호수 주변에는 충전소가 많지 않은 편이었고, 초급속 충전기가 있어 저희 앱을 실험하기에 딱 좋았습니다.&lt;/p&gt;
&lt;h2 id=&quot;마장호수로-출발&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마장호수로 출발&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EB%A7%88%EC%9E%A5%ED%98%B8%EC%88%98%EB%A1%9C-%EC%B6%9C%EB%B0%9C&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저&lt;span&gt;&amp;nbsp;&lt;/span&gt;가브리엘과&lt;span&gt;&amp;nbsp;&lt;/span&gt;제이,&lt;span&gt;&amp;nbsp;&lt;/span&gt;박스터는 서울 선정릉역에서 아이오닉5를 렌트하고 마장호수로 출발했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJRoZt/btsxsnwMVDT/jNA3GiBHccN66bMgcX7R3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJRoZt/btsxsnwMVDT/jNA3GiBHccN66bMgcX7R3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJRoZt/btsxsnwMVDT/jNA3GiBHccN66bMgcX7R3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJRoZt%2FbtsxsnwMVDT%2FjNA3GiBHccN66bMgcX7R3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzkT0/btsx1QDL3ea/KwBD3VhW3TV3VUhtypP7Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzkT0/btsx1QDL3ea/KwBD3VhW3TV3VUhtypP7Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzkT0/btsx1QDL3ea/KwBD3VhW3TV3VUhtypP7Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzkT0%2Fbtsx1QDL3ea%2FKwBD3VhW3TV3VUhtypP7Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQi2if/btsxtnXG26r/CeNUJZdCXbwcz4q4yGOPk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQi2if/btsxtnXG26r/CeNUJZdCXbwcz4q4yGOPk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQi2if/btsxtnXG26r/CeNUJZdCXbwcz4q4yGOPk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQi2if%2FbtsxtnXG26r%2FCeNUJZdCXbwcz4q4yGOPk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbj6FX/btsxsp2pwSb/ShfOGguWmgox3O4QisARVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbj6FX/btsxsp2pwSb/ShfOGguWmgox3O4QisARVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbj6FX/btsxsp2pwSb/ShfOGguWmgox3O4QisARVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbj6FX%2Fbtsxsp2pwSb%2FShfOGguWmgox3O4QisARVk%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;396&quot; height=&quot;602&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qIGzc/btsxHZaF1Vq/FuVmVVLuIfKISdCskxhKv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qIGzc/btsxHZaF1Vq/FuVmVVLuIfKISdCskxhKv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qIGzc/btsxHZaF1Vq/FuVmVVLuIfKISdCskxhKv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqIGzc%2FbtsxHZaF1Vq%2FFuVmVVLuIfKISdCskxhKv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마장 호수까지 20분 거리를 남기고, 갑자기 배가 고파진 저희는 목적지를 틀어&lt;span&gt;&amp;nbsp;&lt;/span&gt;파주닭국수 본점을 가기로 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;파주닭국수가-어디에-있지&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;파주닭국수가 어디에 있지?&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%ED%8C%8C%EC%A3%BC%EB%8B%AD%EA%B5%AD%EC%88%98%EA%B0%80-%EC%96%B4%EB%94%94%EC%97%90-%EC%9E%88%EC%A7%80&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coNzuR/btsx1UsDE4B/QV9lmlMvVV8CYabiczpjH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coNzuR/btsx1UsDE4B/QV9lmlMvVV8CYabiczpjH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coNzuR/btsx1UsDE4B/QV9lmlMvVV8CYabiczpjH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoNzuR%2Fbtsx1UsDE4B%2FQV9lmlMvVV8CYabiczpjH0%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;1170&quot; height=&quot;2532&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 저희는 자동차 내비게이션에 있는 도로명 주소를 검색하여 위치를 파악하려고 하였고, 다소 부정확 하지만 동네에 있는 인근 충전소를 찾을 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;휴게소에-들리다&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;휴게소에 들리다&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%ED%9C%B4%EA%B2%8C%EC%86%8C%EC%97%90-%EB%93%A4%EB%A6%AC%EB%8B%A4&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;424&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8FC74/btsxsREGtWi/qkKl29JrA4HYkUBeVaplo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8FC74/btsxsREGtWi/qkKl29JrA4HYkUBeVaplo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8FC74/btsxsREGtWi/qkKl29JrA4HYkUBeVaplo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8FC74%2FbtsxsREGtWi%2FqkKl29JrA4HYkUBeVaplo0%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;424&quot; height=&quot;342&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;589&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mSev4/btsx14WmqHq/LpiH1dOkm55kK1WbshUpUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mSev4/btsx14WmqHq/LpiH1dOkm55kK1WbshUpUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mSev4/btsx14WmqHq/LpiH1dOkm55kK1WbshUpUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmSev4%2Fbtsx14WmqHq%2FLpiH1dOkm55kK1WbshUpUk%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;589&quot; height=&quot;541&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9f9L1/btsx14IQ0vz/zNFJqqx0GP6xSpANSmUOP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9f9L1/btsx14IQ0vz/zNFJqqx0GP6xSpANSmUOP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9f9L1/btsx14IQ0vz/zNFJqqx0GP6xSpANSmUOP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9f9L1%2Fbtsx14IQ0vz%2FzNFJqqx0GP6xSpANSmUOP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtppgZ/btsxH2SNTbb/Oxutuh2t7FyeK243dp6sl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtppgZ/btsxH2SNTbb/Oxutuh2t7FyeK243dp6sl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtppgZ/btsxH2SNTbb/Oxutuh2t7FyeK243dp6sl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtppgZ%2FbtsxH2SNTbb%2FOxutuh2t7FyeK243dp6sl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;말로만 듣던 고속도로 휴게소의 전기차 충전소 대기줄을 직접 확인할 수 있었습니다. 차주 분과 인터뷰 하고 싶었지만, 차 내부에서 너무 바빠보이셔서 그럴 수 없었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;391&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6bLv/btsxsS4FCMP/7o5GjQSkmqWzY4uhEZAc6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6bLv/btsxsS4FCMP/7o5GjQSkmqWzY4uhEZAc6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6bLv/btsxsS4FCMP/7o5GjQSkmqWzY4uhEZAc6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6bLv%2FbtsxsS4FCMP%2F7o5GjQSkmqWzY4uhEZAc6k%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;391&quot; height=&quot;330&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;휴게소에는 충전소가 하나 더 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 곳은 사용중이지만, 다른 한 곳은 사용할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;488&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ErhpV/btsxzXqN3kD/ZKjKLcLF4PRTn0n3oaqNBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ErhpV/btsxzXqN3kD/ZKjKLcLF4PRTn0n3oaqNBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ErhpV/btsxzXqN3kD/ZKjKLcLF4PRTn0n3oaqNBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FErhpV%2FbtsxzXqN3kD%2FZKjKLcLF4PRTn0n3oaqNBK%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;488&quot; height=&quot;453&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용할 수 있으니깐 들어가봐야지! 하고 도착한 순간 아차 싶었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;아, 충전소가 외부인 사용 금지일 수 있었지?&quot;&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희는 분명히 서비스를 직접 개발했으니깐 다 알고 있던 사항이었지만, 전혀 생각치 못했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스를 개발하는 내내 외부인 개방 충전소에 대한 중요성을 간파하였고, 이 기능을 넣었으면서도 사용하지 않고 충전소를 방문한 것이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로 앞에 있어서 다행이었지만, 어찌됐든 이 충전소를 사용할 수 없었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 저희는 휴게소를 떠나는 내내 이 문제에 대해서 토론을 할 수 밖에 없었습니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;분명 우리가 만든 서비스인데 왜 놓쳤을까?
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;맛있는-점심&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;맛있는 점심&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EB%A7%9B%EC%9E%88%EB%8A%94-%EC%A0%90%EC%8B%AC&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7YPaB/btsxvH2yArS/mw1ezh1oxm9oLvcK0UDTG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7YPaB/btsxvH2yArS/mw1ezh1oxm9oLvcK0UDTG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7YPaB/btsxvH2yArS/mw1ezh1oxm9oLvcK0UDTG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7YPaB%2FbtsxvH2yArS%2Fmw1ezh1oxm9oLvcK0UDTG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파주닭국수 본점에서 맛있는 식사를 했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비록 식당에는 전기차 충전소가 없었지만, 인근에 충전소가 있어 실험을 하나 해볼 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인근 충전소와 식당의 거리가 가까워 보이는데, 과연 걸어갈 수 있을까?&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 걷지는 않았습니다만 차 타면서 지나가면서 확인해본 결과 직접 걸을 수 없는 거리였습니다. (굉장히 걷기 싫은 수준의 먼 거리였습니다.)&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;집에 있는 PHEV를 탈 기회가 많아 전기차 충전소를 자주 방문했던 저는 이런 점을 잘 알고 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SohZM/btsxsUIbdWl/A8Z93700Sj0oaYyldEqsg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SohZM/btsxsUIbdWl/A8Z93700Sj0oaYyldEqsg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SohZM/btsxsUIbdWl/A8Z93700Sj0oaYyldEqsg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSohZM%2FbtsxsUIbdWl%2FA8Z93700Sj0oaYyldEqsg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;식사가 끝나고 드디어 마장호수로 출발하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;마장호수-도착&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마장호수 도착&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EB%A7%88%EC%9E%A5%ED%98%B8%EC%88%98-%EB%8F%84%EC%B0%A9&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dldAmn/btsx2u1xBgr/uCt5M3gYvKcSqUohlkQDbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dldAmn/btsx2u1xBgr/uCt5M3gYvKcSqUohlkQDbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dldAmn/btsx2u1xBgr/uCt5M3gYvKcSqUohlkQDbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdldAmn%2Fbtsx2u1xBgr%2FuCt5M3gYvKcSqUohlkQDbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boU7N4/btsx2qrihkM/e1L0HPRcs7NJio1TLKH4kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boU7N4/btsx2qrihkM/e1L0HPRcs7NJio1TLKH4kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boU7N4/btsx2qrihkM/e1L0HPRcs7NJio1TLKH4kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboU7N4%2Fbtsx2qrihkM%2Fe1L0HPRcs7NJio1TLKH4kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdiaqU/btsxzYb9J6w/TYjzQPB6vMYkf0kMR1LLG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdiaqU/btsxzYb9J6w/TYjzQPB6vMYkf0kMR1LLG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdiaqU/btsxzYb9J6w/TYjzQPB6vMYkf0kMR1LLG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdiaqU%2FbtsxzYb9J6w%2FTYjzQPB6vMYkf0kMR1LLG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2기 중 1곳을 저희가 사용하였고, 마장호수를 돌았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s5tAZ/btsx3OyFed5/Rtxb3qqlaHHcZw1AQxiYiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s5tAZ/btsx3OyFed5/Rtxb3qqlaHHcZw1AQxiYiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s5tAZ/btsx3OyFed5/Rtxb3qqlaHHcZw1AQxiYiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs5tAZ%2Fbtsx3OyFed5%2FRtxb3qqlaHHcZw1AQxiYiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;800&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약 50분 간 산책을 하고, 돌아와보니 충전기 다 되어있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 마장호수 까지 오는 내내 든 생각이었지만, 전기차의 배터리가 생각보다 오래 간다는 생각이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일부러 회생제동 기능도 끄고, 에어컨을 강하게 틀어서 배터리를 소진하려고 하였으나, 85km를 주행하는 동안 겨우 20%를 소모하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;충전기를 꽂을 때 50%였으나, 호수를 한바퀴 돌고 오니 이미 100%가 되어있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여담이지만, 저희가 돌아왔을 때 옆 자리에는 전기 화물차가 있어 충전소가 가득 찼습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; 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;800&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CWCtX/btsxUbPkvbX/gtFssn3sddnyy1Tc7CPBO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CWCtX/btsxUbPkvbX/gtFssn3sddnyy1Tc7CPBO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CWCtX/btsxUbPkvbX/gtFssn3sddnyy1Tc7CPBO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCWCtX%2FbtsxUbPkvbX%2FgtFssn3sddnyy1Tc7CPBO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;1067&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배터리 성능에는 좋지 않고 가격도 비싸서 이를 자주 사용하는 것은 좋지 않겠지만, 급한 사람들은 급속 충전기를 사용하면 되겠구나 싶었습니다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot; style=&quot;background-color: #000000; color: #000000;&quot;&gt;&lt;code&gt;따라서 급속과 완속은 더더욱 다른 개념으로 봐야겠다는 생각이 들었습니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제가 그동안 경험했던 전기차 충전소는 완속 기준이었기에 신선한 경험이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;선릉으로-돌아오다&quot; style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;선릉으로 돌아오다&lt;a style=&quot;color: #000000;&quot; href=&quot;https://car-ffeine.github.io/39#%EC%84%A0%EB%A6%89%EC%9C%BC%EB%A1%9C-%EB%8F%8C%EC%95%84%EC%98%A4%EB%8B%A4&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pcjst/btsxZxLhpx6/yuNIi6KML1bhKh8KZoxbSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pcjst/btsxZxLhpx6/yuNIi6KML1bhKh8KZoxbSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcjst/btsxZxLhpx6/yuNIi6KML1bhKh8KZoxbSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpcjst%2FbtsxZxLhpx6%2FyuNIi6KML1bhKh8KZoxbSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;524&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;선릉으로 돌아와서 차량을 반납하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저희는 이번 여정을 통해 카페인 서비스에서 어떤 점을 개선해야할지 좀 더 명확하게 알게되었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #1c1e21; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 서비스에서 제공하는 기능들로 충전소를 검색하는 것은 가능하며, 충전소의 위치를 정확하게 파악하는 것도 가능하다.&lt;/li&gt;
&lt;li&gt;하지만 충전소가 없는 목적지는 검색할 수 없고, 현 위치가 어디인지 가늠하기가 어려워진다.&lt;/li&gt;
&lt;li&gt;충전소를 사용할 수 있다고 표기되어 있더라도 외부인 개방이 아닐 수 있다. 정보가 정확히 제공됨에도 불구하고 이를 단번에 눈치채기 어렵다.&lt;/li&gt;
&lt;li&gt;이러한 문제를 예상하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;외부인 개방 여부를 필터링 할 수 있는 기능을 제공하고 있음에도 불구하고 사용하지 않았다.&lt;/li&gt;
&lt;li&gt;충전소의 통계 자료의 적중률은 높았으나, 좀 더 많은 충전소를 들려 확인해봐야 할 것 같았다.&lt;/li&gt;
&lt;li&gt;전기자동차는 생각보다 오래가고 상품성이 있었다. 주행 능력도 충분하고, 인프라가 잘 되어있다. 이걸 왜 욕하지? 라는 생각이 들었다.&lt;/li&gt;
&lt;li&gt;지도 확대 허용 범위가 너무 좁아서 사용하는데 불편한건 실제 상황에서 더 불편했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #1c1e21; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이상 카페인 사용기였습니다.&lt;/p&gt;</description>
      <category>Study/일상&amp;middot;회고</category>
      <category>우아한테크코스</category>
      <category>우테코</category>
      <category>전기차충전소</category>
      <category>전기차충전소지도</category>
      <category>카페인</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/157</guid>
      <comments>https://leirbag.tistory.com/157#entry157comment</comments>
      <pubDate>Wed, 11 Oct 2023 00:18:10 +0900</pubDate>
    </item>
    <item>
      <title>고밀도 지도 데이터 관리를 위한 효과적인 전략: 지도 확대 및 마커 제어</title>
      <link>https://leirbag.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지도 기능에서 가장 핵심인 기능 두 가지를 뽑자면, 지도 그 자체와 지도 위에 그려지는 마커를 뽑을 수 있을 것입니다. 지도 위에 마커를 그리는 일은 그다지 어렵지 않고, documents 에 있는 예제들을 잘 따라하면 누구나 충분히 구현할 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Irhid/btsr4i1qUBL/k2781lM4lTYNS26TJPUuMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Irhid/btsr4i1qUBL/k2781lM4lTYNS26TJPUuMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Irhid/btsr4i1qUBL/k2781lM4lTYNS26TJPUuMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIrhid%2Fbtsr4i1qUBL%2Fk2781lM4lTYNS26TJPUuMK%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;518&quot; height=&quot;432&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&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;최근 진행한 서비스에서는요 ...&lt;/b&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;어떤 전략을 펼쳤는지 소개하기에 앞서 미리 말씀드리지만, 저희 팀에서 취한 지도 관리 전략은 모든 프로젝트에 유효하지 않을 것입니다. 지도 위에 한번에 표현할 마커의 갯수가 수백 개 이하라면, 서버에 데이터가 과도하게 많은 것이 아니라면 오히려 이러한 전략이 사용자 경험을 해칠 수 있을 것입니다. (환경이 원활하다면 데이터를 가능한 많이 보여주는 것이 좋을테니깐요.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 이 글에서는 Google Maps API를 기준으로 설명하고 있지만, 지원하는 기능이 일부 다르더라도 대부분의 지도 API에서 사용이 가능한 전략일 것입니다. 참고로 개인적으로 사용 해본 여러 벤더 사의 지도 API들은 모두 이와 유사한 기능을 제공했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;좌표란 무엇일까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 어린 시절부터 우리나라에는 특별히 38선이라는 것이 존재한다는 사실을 교육받기에 &lt;code&gt;좌표계라는 것이 있다는 사실&lt;/code&gt;은 누구나 알 것입니다. 하지만 당장 위도와 경도를 구분지으라고 하면 어떤 선이 위선이고 경선인지 헷갈리기에 찍어야 할 것입니다. 따라서 이 선이 어떤 선인지, 어떤 값을 얘기하려는 것인지 사진과 함께 간단히 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC9APY/btssc7RhlP2/rDPnt5g4B3mAfWk2PVkMaK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC9APY/btssc7RhlP2/rDPnt5g4B3mAfWk2PVkMaK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC9APY/btssc7RhlP2/rDPnt5g4B3mAfWk2PVkMaK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC9APY%2Fbtssc7RhlP2%2FrDPnt5g4B3mAfWk2PVkMaK%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;259&quot; height=&quot;194&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진을 보시면 아시겠지만 위도란, 남북의 위치를 나타내는 데 사용됩니다. 경도는 동서의 위치를 나타내는 데 사용됩니다. 대부분의 공식 문서가 영어로 작성되어있고, 코드에서도 이를 나타내는 것이 중요하기에 영문 표기법까지 소개를 하자면 위도는 Latitude, 경도는 Longitude로 표기합니다. 이유는 모르겠지만 제공되는 변수나 메서드 명으로 lat, lng라고 줄여서 표기하기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o6M6J/btsr4jst9BE/CIiKsanUp63Kfl3LzgsIwk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o6M6J/btsr4jst9BE/CIiKsanUp63Kfl3LzgsIwk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o6M6J/btsr4jst9BE/CIiKsanUp63Kfl3LzgsIwk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/o6M6J/btsr4jst9BE/CIiKsanUp63Kfl3LzgsIwk/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;435&quot; height=&quot;340&quot; data-origin-width=&quot;435&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위도와 경도만 알면, 지구 위의 어떤 위치를 나타낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 어떤 마커를 어떤 위치에 찍을 것인지는 위도와 경도 값으로 결정할 수 있게 되겠죠?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용자가 어딜 보고 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어느 위치를 보고 있는지 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;let map = /* 어디선가 생성된 구글 맵 객체 */
const center = map.getCenter();
console.log(center.lng()); // 디바이스 중심의 longitude
console.log(center.lat()); // 디바이스 중심의 latitude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 객체로 부터 중심점을 알게되면 해당 디바이스의 중심의 좌표를 알아낼 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;1015&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qZmaC/btsr3dsJ8Ka/5Ru0IqGI41pANwAlfqHKAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qZmaC/btsr3dsJ8Ka/5Ru0IqGI41pANwAlfqHKAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZmaC/btsr3dsJ8Ka/5Ru0IqGI41pANwAlfqHKAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqZmaC%2Fbtsr3dsJ8Ka%2F5Ru0IqGI41pANwAlfqHKAk%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;1627&quot; height=&quot;1015&quot; data-origin-width=&quot;1627&quot; data-origin-height=&quot;1015&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용자의 디바이스는 얼마나 넓게 보고 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 api에서 제공해주는 메서드를 활용하면 사용자의 디바이스가 어떤 영역을 보고 있는지도 알게 됩니다. 지도 api 마다 제공하는 스펙이 다르지만, 대부분은 어떤 식으로든 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google maps API에서는 디스플레이의 북동쪽 끝 점의 좌표와, 남서쪽 끝 점의 좌표를 제공해줍니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;const map = /* 어디선가 생성된 구글 맵 객체 */
const bounds = map.getBounds();
console.log(bounds.getNorthEast().lng(), bounds.getNorthEast().lat()); // 디바이스 1사분면 끝 점의 longitude와 latitude
console.log(bounds.getSouthWest().lng(), bounds.getSouthWest().lat()); // 디바이스 3사분면 끝 점의 longitude와 latitude&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ycaoa/btsr3buUKJY/7duNnpkPqsKmEYDN83tjs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ycaoa/btsr3buUKJY/7duNnpkPqsKmEYDN83tjs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ycaoa/btsr3buUKJY/7duNnpkPqsKmEYDN83tjs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fycaoa%2Fbtsr3buUKJY%2F7duNnpkPqsKmEYDN83tjs0%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;2162&quot; height=&quot;1177&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1177&quot;/&gt;&lt;/span&gt;&lt;/figure&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;중심 점 p0: (x0, y0)&lt;/li&gt;
&lt;li&gt;디바이스의 제 1사분면 끝점 p2: (x2, y2)&lt;/li&gt;
&lt;li&gt;디바이스의 제 3사분면 끝점 p1: (x1, y1)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&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;저희 팀에서는 이 값을 좀 더 효율적으로 다루기 위해 delta 개념을 도입했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;화면에서 보고 있는 영역을 확대/축소 하면 어떤 특징을 보일까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delta 설명을 앞서, 사용자의 디바이스 영역과 확대 수준에 따른 실제 좌표에 대해 알아보려고 합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2685&quot; data-origin-height=&quot;1648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFXRR/btssbk4rTVR/8tTfBQHn0l2x0wAiuKrS6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFXRR/btssbk4rTVR/8tTfBQHn0l2x0wAiuKrS6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFXRR/btssbk4rTVR/8tTfBQHn0l2x0wAiuKrS6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFXRR%2Fbtssbk4rTVR%2F8tTfBQHn0l2x0wAiuKrS6K%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;2685&quot; height=&quot;1648&quot; data-origin-width=&quot;2685&quot; data-origin-height=&quot;1648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 지도를 많이 축소한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심 점 p0으로 부터 멀어질 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 사용자가 지도를 많이 확대한 경우에는 중심 점 p0은 그대로지만 양 끝점 p1, p2의 위치가 점점 중심점과 가까워질 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;2028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9mh42/btsr5VY6SGg/Cex2byEsJBXBEKtWDwsZJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9mh42/btsr5VY6SGg/Cex2byEsJBXBEKtWDwsZJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9mh42/btsr5VY6SGg/Cex2byEsJBXBEKtWDwsZJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9mh42%2Fbtsr5VY6SGg%2FCex2byEsJBXBEKtWDwsZJ1%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;2694&quot; height=&quot;2028&quot; data-origin-width=&quot;2694&quot; data-origin-height=&quot;2028&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양 사진 모두 중심 점 p0는 그대로지만, 디바이스의 확대 수준으로 인해 양 끝점인 p1과 p2가 달라진 모습을 보인 것입니다.&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;양 끝점 p1, p2가 중심 점 p0으로 부터 멀어질 수록 지도를 축소한 것이다.&lt;/li&gt;
&lt;li&gt;양 끝점 p1, p2가 중심 점 p0으로 부터 가까워 수록 지도를 확대한 것이다.&lt;/li&gt;
&lt;/ol&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;확대 수준을 수치화 할 수 없을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 디스플레이의 중심 점 p0을 기준으로 하여 양 끝점 p1, p2이 얼마나 멀어져있는지에 따라 지도의 영역 뿐만 아니라 얼마나 많이 확대 되었는지 여부를 알게 됐습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bulOLQ/btsr62pXuVu/AXfuIWnQEuzmuOQbZ6eaAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bulOLQ/btsr62pXuVu/AXfuIWnQEuzmuOQbZ6eaAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bulOLQ/btsr62pXuVu/AXfuIWnQEuzmuOQbZ6eaAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbulOLQ%2Fbtsr62pXuVu%2FAXfuIWnQEuzmuOQbZ6eaAk%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;2162&quot; height=&quot;1494&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중학교 시절 배웠던 좌표 평면계를 떠올려보면 화면에서 얻을 수 있는 좌표들은 위와 같습니다. 여기에서 각 점의 수직/수평의 변화량인 delta를 알아보면 어떨까요?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;경도 델타 (longitudeDelta)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p2와 p0의 경도 거리, 그리고 p1과 p0의 경도 거리는 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;x2 - x0 === x0 - x1&lt;/code&gt; 이라는 결론을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 longitudeDelta로 정의하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;위도 델타 (latitudeDelta)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p2와 p0의 위도 거리, 그리고 p1과 p0의 위도 거리는 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;y2 - y0 === y0 - y1&lt;/code&gt; 이라는 결론을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 latitudeDelta로 정의하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;1580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zCAuH/btsr5WKtp92/RQGlxlhM8KMd2K9RUPFXYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zCAuH/btsr5WKtp92/RQGlxlhM8KMd2K9RUPFXYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zCAuH/btsr5WKtp92/RQGlxlhM8KMd2K9RUPFXYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzCAuH%2Fbtsr5WKtp92%2FRQGlxlhM8KMd2K9RUPFXYK%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;2294&quot; height=&quot;1580&quot; data-origin-width=&quot;2294&quot; data-origin-height=&quot;1580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 알아보면 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const map = /* 어디선가 생성된 구글 맵 객체 */
const bounds = map.getBounds();
const longitudeDelta = (bounds.getNorthEast().lng() - bounds.getSouthWest().lng()) / 2; // 경도 변화량
const latitudeDelta = (bounds.getNorthEast().lat() - bounds.getSouthWest().lat()) / 2; // 위도 변화량&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;delta의 유용한 점 1: 원래 의도한 값을 복원하기 쉽다.&lt;/b&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;예를 들어 클라이언트에서 서버로 다음과 같은 파라미터를 넘겨줬다고 가정해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;{
    &quot;longitude&quot;: 127,
    &quot;latitude&quot;: 37,
    &quot;longitudeDelta&quot;: 0.1,
    &quot;longitudeDelta&quot;: 0.2,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 서버에서는 다음과 같이 해석할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const maxLongitude = longitude + longitudeDelta;
const minLongitude = longitude - longitudeDelta;
const maxLatitude = latitude + latitudeDelta;
const minLatitude = latitude - latitudeDelta;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(javascript 기준으로 작성했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 알아낸 경계 값을 가지고 다음과 같은 sql문을 작성할 수 있게 될 것입니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SELECT * FROM stations WHERE latitude &amp;gt;= :minLatitude AND latitude &amp;lt;= :maxLatitude AND longitude &amp;gt;= :minLongitude AND longitude &amp;lt;= :maxLongitude;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3068&quot; data-origin-height=&quot;1866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7RCoP/btsr9OZsYZh/UkkAZpjFDj4LCyKhuMPtkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7RCoP/btsr9OZsYZh/UkkAZpjFDj4LCyKhuMPtkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7RCoP/btsr9OZsYZh/UkkAZpjFDj4LCyKhuMPtkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7RCoP%2Fbtsr9OZsYZh%2FUkkAZpjFDj4LCyKhuMPtkK%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;3068&quot; height=&quot;1866&quot; data-origin-width=&quot;3068&quot; data-origin-height=&quot;1866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위 그림처럼, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;b&gt;원하는 영역만큼만 정확하게 데이터를 호출할 수 있게 됩니다.&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;delta의 유용한 점 2: 델타가 무분별하게 커지는 것을 막기 쉽다.&lt;/b&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;이러한 행위를 막는 가장 쉬운 방법은 지도 api에서 지원하는 줌 레벨을 제한 하는 것입니다. 후술하겠지만 &lt;i&gt;줌 레벨은 디스플레이의 해상도를 고려하지 못합니다.&lt;/i&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;예를 들어 다음과 같이 델타 값을 고정하여 요청 영역을 제한할(요청을 보내지 않거나 고정된 사이즈로만 요청을 보낼) 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;{
    longitude,
    latitude,
    longitudeDelta: longitudeDelta &amp;lt; 0.008 ? longitudeDelta : 0.008,
    latitudeDelta: latitudeDelta &amp;lt; 0.004 ? latitudeDelta : 0.004,
}&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;분명히 같은 줌 레벨이지만, 디바이스의 크기나 해상도에 따라 지도가 보여지는 정도가 다릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;787&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sshZt/btsr4Nm0Zcz/yfecjcMjD4zv6kqVTTN3VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sshZt/btsr4Nm0Zcz/yfecjcMjD4zv6kqVTTN3VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sshZt/btsr4Nm0Zcz/yfecjcMjD4zv6kqVTTN3VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsshZt%2Fbtsr4Nm0Zcz%2FyfecjcMjD4zv6kqVTTN3VK%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;1710&quot; height=&quot;787&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;787&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 구글에서 제공하는 zoom 레벨을 동일하게 맞춘 후, 여러 디바이스에서 호출한 것입니다.&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;1432&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lF8VI/btsr3dffgHv/ZtXS0zxMZVNjm83HYttYKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lF8VI/btsr3dffgHv/ZtXS0zxMZVNjm83HYttYKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lF8VI/btsr3dffgHv/ZtXS0zxMZVNjm83HYttYKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlF8VI%2Fbtsr3dffgHv%2FZtXS0zxMZVNjm83HYttYKK%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;1432&quot; height=&quot;882&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;882&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 저희 팀에서는 고해상도 모니터를 대응하기 위해 델타 값이 너무 크게 되면 요청의 제한을 하고 있습니다. 사진에서 보시다시피 고해상도 모니터의 경우, 너무 넓은 범위를 요청한다 싶으면 중심점으로 부터 일정 거리만 보여주도록 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 줌 레벨에 따른 요청도 덤으로 제한하고 있어서 멀리서 호출하는 행위도 금지하고 있습니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;delta의 유용한 점 3: 적당한 범위를 정해주기 편하다&lt;/b&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;1246&quot; data-origin-height=&quot;1344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sXaRn/btssbk4rU44/RUNnjVNjsBEAtN0M1EGfFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sXaRn/btssbk4rU44/RUNnjVNjsBEAtN0M1EGfFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sXaRn/btssbk4rU44/RUNnjVNjsBEAtN0M1EGfFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsXaRn%2Fbtssbk4rU44%2FRUNnjVNjsBEAtN0M1EGfFK%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;1246&quot; height=&quot;1344&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;1344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 현재 사용자의 디바이스 크기보다 살짝 큰 범위의 데이터를 미리 로드해 놓으면 사용자가 좁은 움직임을 보일 때 불필요한 재 렌더링을 줄여서 더 빠른 렌더링이 가능하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 기법은 프로젝트마다 다르겠지만, 저희 팀에서는 한번 불러온 마커를 매번 해제 하지 않고 &lt;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;마커를 상태에 연동하여 정확하게 메모리에서 탈부착 시키는 전략에 대한 글은 이후에 작성할 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Frontend/JavaScript</category>
      <category>Delta</category>
      <category>Google Maps API</category>
      <category>marker</category>
      <category>구글 맵</category>
      <category>델타</category>
      <category>마커</category>
      <category>지도</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/154</guid>
      <comments>https://leirbag.tistory.com/154#entry154comment</comments>
      <pubDate>Fri, 25 Aug 2023 00:22:30 +0900</pubDate>
    </item>
    <item>
      <title>Github Actions를 활용한 Jest, React Testing Library 테스트 자동화</title>
      <link>https://leirbag.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 소프트웨어 테스트란 백엔드에서 그 중요성이 강조되곤 하지만, 프론트엔드에서도 그에 못지 않게 중요한 부분을 차지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 툴 중에서 jest 기반의 두 라이브러리를 자동으로 테스트 하는 방법에 대해 작성해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Jest&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jest는 JavaScript의 테스트를 위한 대표적인 라이브러리입니다.&lt;br /&gt;기본 설정이 간편하고, 빠르게 테스트를 실행할 때 굉장히 유용합니다.&lt;br /&gt;함수를 mocking하여 의존성이 강한 함수를 제거하여 원하는 테스트를 쉽게 구성할 수 있다는 특징이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React Testing Library&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Testing Library는 리액트 애플리케이션의 UI를 테스트하기 위한 라이브러리입니다.&lt;br /&gt;React 컴포넌트를 호출하여, 사용자의 의도대로 조작할 수 있는 행위를 정의할 수 있습니다.&lt;br /&gt;사용자 입장에서 상호작용 할 수 있는 부분을 스크립트로 작성하여 컴포넌트가 어떻게 변화하는지를 테스트 할 수 있게 됩니다.&lt;br /&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;따라서 테스트를 PR이 열릴 때 마다 자동으로 진행하기 위해 테스트 자동화를 구축해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** 이 글에서는 테스트 코드 작성 방법에 대해서 다루지 않습니다. **&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** yarn test 를 매번 치는 것이 귀찮은 사람들을 위해 작성합니다. **&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;b&gt;Jest와 React Testing Library 테스트 자동화&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;name: frontend-test

on:
  pull_request:
    branches:
      - main
      - develop
    paths:
      - '*'

permissions:
  contents: read

jobs:
  test:
    name: test-when-pull-request
    runs-on: ubuntu-latest
    environment: test
    defaults:
      run:
        working-directory: .
    steps:
      - name: Checkout PR
        uses: actions/checkout@v2
      - name: Install dependencies
        run: npm install
      - name: Test
        run: npm run test&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 트리거 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pull_request 이벤트가 발생하였을 때, 해당 이벤트가 main 브랜치와 develop 브랜치에서만 동작합니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;권한 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;permissions은 읽기 권한만 설정되어 있어 코드나 파일을 변경을 방지합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업(Job) 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test라는 이름의 작업을 정의하였고, 이 작업에서는 Ubuntu 환경에서 테스트를 실행합니다. test라는 이름의 환경 변수를 사용합니다. 테스트는 (카페인 팀 레포지토리의) frontend 디렉토리에서 작업하도록 하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스텝(Step) 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 체크아웃하고, 의존성을 설치하며, 테스트를 실행하는 세 가지 단계로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 설정을 통해 PR에 코드가 올라올 때 자동으로 프론트엔드 테스트가 실행됩니다.&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;위 코드를 .github/workflows 폴더에 foo.yml 파일을 추가해준 다음, 붙여넣기 해주면 PR을 열 때마다 테스트가 동작 할 것입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/JavaScript</category>
      <category>FE 테스트 자동화</category>
      <category>github actions</category>
      <category>Jest</category>
      <category>react testing library</category>
      <category>테스트 자동화</category>
      <category>프론트엔드 테스트</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/153</guid>
      <comments>https://leirbag.tistory.com/153#entry153comment</comments>
      <pubDate>Fri, 25 Aug 2023 00:12:06 +0900</pubDate>
    </item>
    <item>
      <title>useSyncExternalStore로 만들어보는 전역상태관리 라이브러리</title>
      <link>https://leirbag.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;몇 달 전부터 전역상태관리 라이브러리를 직접 만들어보고 싶었는데, 우테코 방학 기간에 심심해서 만들어 봤습니다.&lt;br /&gt;혹시 useSyncExternalStore가 무엇인지 처음 들어보신다면 &lt;a href=&quot;https://leirbag.tistory.com/144&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;이 문서를 확인&lt;/span&gt;&lt;/a&gt;해보시는 것도 좋습니다.&lt;br /&gt;제가 사용해 본 라이브러리의 폭이 넓지는 않지만 RTK, recoil, zustand를 사용해 본 결과 여러 가지 불만스러운 점이 많았습니다.&lt;br /&gt;라이브러리는 아니지만 context API까지의 비교를 해보겠습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 87px;&quot; border=&quot;1&quot; data-ke-style=&quot;style2&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 40.5426%; height: 19px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 44.3798%; height: 19px;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%; height: 17px;&quot;&gt;RTK&lt;/td&gt;
&lt;td style=&quot;width: 40.5426%; height: 17px;&quot;&gt;주요 비즈니스 로직을 한 곳에 모 으도록 하는 탑 다운 방식의 중앙 집중형 상태 관리를 강제한다.&lt;/td&gt;
&lt;td style=&quot;width: 44.3798%; height: 17px;&quot;&gt;보일러 플레이트 코드의 양이 상당하고, 러닝 커브가 큰 편이다. react에서 해야할 일을 정해진 함수 내에서 억지로 사용하게 하는 느낌이 든다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%; height: 17px;&quot;&gt;recoil&lt;/td&gt;
&lt;td style=&quot;width: 40.5426%; height: 17px;&quot;&gt;바텀업 방식의 아톰 기반 상태관리를 지원한다. react의 useState훅과 비슷한 사용 방법을 지원한다.&lt;/td&gt;
&lt;td style=&quot;width: 44.3798%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;재사용이 가능한 비즈니스 로직을 한 곳에 모으려면&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://leirbag.tistory.com/148&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;여러 가지 작업&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;이 필요하다. 제대로 다루지 못하면 최적화가 무용지물이 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%; height: 17px;&quot;&gt;zustand&lt;/td&gt;
&lt;td style=&quot;width: 40.5426%; height: 17px;&quot;&gt;쉽다. 누구나 금방 배울 수 있는 난이도와 분량이다.&lt;/td&gt;
&lt;td style=&quot;width: 44.3798%; height: 17px;&quot;&gt;개인적으로 다소 납득하기 어려운 사용법(setter)이 존재한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.0775%; height: 17px;&quot;&gt;context API&lt;/td&gt;
&lt;td style=&quot;width: 40.5426%; height: 17px;&quot;&gt;별도의 라이브러리 설치 없이 react 내장 기능으로만 구현이 가능하다&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 44.3798%; height: 17px;&quot;&gt;보일러 플레이트 코드가 큰 편이고, 최적화 문제가 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;특히 tanstack-query가 메인스트림으로 부상하고 있는 시점에서 대부분의 상태를 tanstack-query 혹은 SWR이&amp;nbsp;&lt;i&gt;&lt;b&gt;서버 상태&lt;/b&gt;&lt;/i&gt;로 관리하게 되어 기존의&amp;nbsp;&lt;a href=&quot;https://tkdodo.eu/blog/practical-react-query#client-state-vs-server-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;상태관리 라이브러리가 더이상 복잡한 상태 관리를 하지 않아도 될 가능성&lt;/span&gt;&lt;/a&gt;이 커졌다고 생각합니다. 그렇다고 해서 기존의 상태관리 라이브러리들이 완전히 사라지는 것이 아닌 가벼운 클라이언트 상태 관리 정도만 해줘도 된다고 생각하는데요, 이에 대한 좋은 예시가 다음 영상에 나와있으니 참고하시면 좋을 것 같습니다.&lt;br /&gt;&lt;a href=&quot;https://youtu.be/HcVCb36WZZk&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://youtu.be/HcVCb36WZZk&lt;/span&gt;&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=HcVCb36WZZk&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bkl0tm/hyTeebTBWa/r2Mgr0crX5KcRkKa3mTfTK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=864_192_1040_384&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-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/HcVCb36WZZk&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;br /&gt;이런 상황이다 보니 간단한 상태 관리를 위해 보일러플레이트가 큰 상태관리 라이브러리들을 사용하기보다는 정말 간단한 필수 기능만 있는 간단한 라이브러리들을 선호하게 됐다고 생각합니다.&lt;br /&gt;단순한 상태 관리를 위해 redux를 쓰는 것은 다소 복잡하고 귀찮은 작업이 될테니깐요&lt;br /&gt;&amp;nbsp;&lt;br /&gt;더군다나 저처럼 상태관리 라이브러리를 컴포넌트 간의 단순한 상태 공유를 목적으로만 사용한다면 더더욱 기존의 복잡한 기능들이나 코드들이 불필요하게 느껴지게 됩니다. 상태를 과거의 스냅샷으로 복원한다거나 디버깅하는 것을 잘 쓰지 않는다면 말이죠.&lt;br /&gt;그래서 recoil, zustand, jotai, context api 등 좀 더 가벼운 기술들이 점점 선호되는 것이 아닌가 생각합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저는 이 중에서도 &lt;b&gt;zustand의 간결함&lt;/b&gt;과 &lt;b&gt;recoil의 상태 훅&lt;/b&gt;이 굉장히 마음에 들었고, &lt;a href=&quot;https://leirbag.tistory.com/144&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;useSyncExternalStore&lt;/span&gt;&lt;/a&gt; 훅을 사용하면 이를 적절히 섞어서 훨씬 간단한 코드로 구현할 수 있을 것이라고 생각했습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;useSyncExternalStore을 좀 더 우아하게 쓸 수 없을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;해보진 않았지만 분명히 될 것 같은데?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그동안은 훅을 &lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;공식 문서에 나와있는 방법&lt;/span&gt;&lt;/a&gt;으로만 사용했었습니다.&lt;br /&gt;하지만 예제를  보면 볼수록 추상화가 가능할 것 같았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;1674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceh4U0/btsmnP42sqG/xG1MGw8Ei5aBwv8ltTp2s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceh4U0/btsmnP42sqG/xG1MGw8Ei5aBwv8ltTp2s1/img.png&quot; data-alt=&quot;addTodo() 메서드를 제외하면 나머지 코드들은 유틸의 냄새가 강하게 난다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceh4U0/btsmnP42sqG/xG1MGw8Ei5aBwv8ltTp2s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fceh4U0%2FbtsmnP42sqG%2FxG1MGw8Ei5aBwv8ltTp2s1%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;445&quot; height=&quot;714&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;1674&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;addTodo() 메서드를 제외하면 나머지 코드들은 유틸의 냄새가 강하게 난다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;안 그래도 최근 진행하고 있는 프로젝트에서 바닐라 JS로 작성된 google maps api 인스턴스와 리액트의 강한 결합이 필요한 상황이었는데 리액트와 외부 JS인스턴스를 적절하게 통합시켜줄 적절한 기술이기도 했습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본적으로 설계한 아이디어&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기본 훅 컨셉은 recoil의 아이디어를 따오기로 생각했습니다.&lt;br /&gt;어떤 atom역할을 하는 변수가 존재하고, useRecoilState, useRecoilValue, useSetRecoilState 같은 친숙한 훅을 사용할 수 있어야 합니다.&lt;br /&gt;그리고 &lt;a href=&quot;https://recoiljs.org/docs/api-reference/core/useRecoilCallback&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;useCallback&lt;/span&gt;&lt;/a&gt;이나 &lt;a href=&quot;https://recoiljs.org/docs/api-reference/core/Snapshot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Snapshot객체&lt;/span&gt;&lt;/a&gt;는 굉장히 유용하지만 문법 자체가 너무 지저분하다고 생각하여 과감하게 포기하기로 했습니다.&lt;br /&gt;참고로 제가 recoil에 가진 &lt;a href=&quot;https://github.com/woowacourse/react-shopping-cart/pull/161&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;불만은&lt;/span&gt;&lt;/a&gt;... &lt;a href=&quot;https://github.com/woowacourse/react-shopping-cart/pull/231&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;여러 곳에&lt;/span&gt;&lt;/a&gt; 잘 나타나있습니다 ,,, (하지만 원자성을 지닌 컨셉 자체에 대해서는 만족합니다)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;두 번째로는 &lt;a href=&quot;https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;zustand에 있는 컴포넌트 바깥의 상태 관리 기능&lt;/span&gt;&lt;/a&gt;이 필요했습니다.&lt;br /&gt;비즈니스 로직을 컴포넌트 바깥에 두고 &lt;u&gt;&lt;i&gt;&lt;b&gt;모아서 쓰려면&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;u&gt;&lt;i&gt; &lt;/i&gt;&lt;/u&gt;이 기능이 반드시 필요했습니다.(개인적으로 생각하는 zustand의 킬러 기능이라고 생각했기 때문입니다.)&lt;br /&gt;또, 리액트 환경이 아닌 &lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore#subscribing-to-a-browser-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;어떤 인스턴스가 리액트 UI를 외부의 저장소와 강제로 동기화시키는 작업이 필요한 경우에도 유용하게 쓰일 수 있을 것이라고 생각&lt;/span&gt;&lt;/a&gt;했습니다. 공식 문서에서는 콕 집어서 예제로 browser API를 언급했지만, 제 생각에는 그냥 non-react인 모든 상황에서 굉장히 유용한 기능입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MsT4M/btsmuqPTLtQ/r5CNzPV9MfmvzHMhKVa421/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MsT4M/btsmuqPTLtQ/r5CNzPV9MfmvzHMhKVa421/img.png&quot; data-alt=&quot;실제로 그러라고 나온 훅이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MsT4M/btsmuqPTLtQ/r5CNzPV9MfmvzHMhKVa421/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMsT4M%2FbtsmuqPTLtQ%2Fr5CNzPV9MfmvzHMhKVa421%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;555&quot; height=&quot;129&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;430&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;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;external-state를 소개합니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름을 뭘로 지을까 하다가 외부(바닐라 JS영역)에 두는 상태를 강조하고 싶어서 external-state라고 지었습니다.&lt;br /&gt;external-state이 어떤 방식으로 동작하는지는 추후에 설명하겠습니다.&lt;br /&gt;사실 별거 없는데 이름만 거창한 이 라이브러리는 그저 useSyncExternalStore를 유틸화 하고 recoil의 껍데기를 씌운 라이브러리입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install external-store

// or 

yarn add external-store&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;store(상태 저장소) 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { store } from &quot;external-state&quot;;

export const countStore = store&amp;lt;number&amp;gt;(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 어떤 store를 생성하면 바닐라 JS 환경에서도 상태에 접근할 수 있게 됩니다.&lt;br /&gt;store.getState()로 store에 저장되어 있는 상태를 읽어낼 수 있으며&lt;br /&gt;store.setState()로 store에 있는 상태를 갱신할 수 있습니다. 이때, 상태를 갱신하면 store를 구독 중인 모든 컴포넌트들이 재 렌더링 될 것입니다.&lt;br /&gt;이런 식의 접근과 갱신 방법은 &lt;a href=&quot;https://github.com/pmndrs/zustand#readingwriting-state-and-reacting-to-changes-outside-of-components&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;zustand의 기능과도 거의 유사&lt;/span&gt;&lt;/a&gt;합니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;컴포넌트의 store 구독&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트는 마치 recoil의 hook처럼 상태를 사용할 수 있습니다.&lt;br /&gt;&lt;b&gt;- useExternalState()&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useExternalState } from &quot;external-state&quot;;

function Count() {
&amp;nbsp;&amp;nbsp;const [count, setCount] = useExternalState(countStore);

&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;{count}&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;increase
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;)
}

export default Count;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;컴포넌트에서 구독과 상태 업데이트합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;- useSetExternalState&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useSetExternalState } from &quot;external-state&quot;;

function Count() {
&amp;nbsp;&amp;nbsp;const setCount = useSetExternalState(countStore);

&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;increase
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;)
}

export default Count;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에서 직접 상태를 업데이트합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;- useExternalValue&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useExternalValue } from &quot;external-state&quot;;

function Count() {
&amp;nbsp;&amp;nbsp;const count = useExternalValue(countStore);

&amp;nbsp;&amp;nbsp;return (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{count}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;)
}

export default Count;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에서 상태를 구독합니다.&lt;br /&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;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export const countActions = {
&amp;nbsp;&amp;nbsp;increase: () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const prevCount = countStore.getState();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;countStore.setState(prevCount + 1);
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;decrease: () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const prevCount = countStore.getState();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;countStore.setState(prevCount - 1);
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;increaseIfOdd: () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const prevCount = countStore.getState();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (prevCount % 2 === 1) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;countStore.setState(prevCount + 1);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;increaseAsync: async () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const prevCount = countStore.getState();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const response = await fetchCount(1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const amount = response.data;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;countStore.setState(prevCount + amount)
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 서술했던 것처럼 store.setState()로 store에 있는 상태를 갱신할 수 있습니다.&lt;br /&gt;상태를 갱신하면 store를 구독 중인 모든 컴포넌트들이 정확하게 재 렌더링됩니다.&lt;br /&gt;특히 상태 값을 항상 구독하지 않고 특정 시점에 정확하게 불러오므로 상태 데이터가 반드시 최신 상태로 호출된다는 특징 때문에 불필요한 재 렌더링 방지에도 큰 도움이 됩니다.&lt;br /&gt;이 기능은 바닐라 환경에서도 사용이 가능하다는 엄청난 장점을 가지고 있습니다...!&lt;br /&gt;심지어 async/await도 아무런 제약 없이 사용이 가능합니다.&lt;br /&gt;이 뜻은 리액트와 강하게 결합해야 하는 어떤 외부 저장소나 라이브러리와의 소통 과정에서 굉장히 유리할 것입니다.&lt;br /&gt;거의 모든 비즈니스 로직을 리액트 바깥으로 분리할 수 있다는 장점도 가지구요&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://github.com/gabrielyoon7/external-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;자세한 사용법과 설명은 제가 작성해 놓은 공식문서&lt;/span&gt;&lt;/a&gt;에서 확인할 수 있고&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - gabrielyoon7/external-state: Easy and Lightweight State Management for React&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Easy and Lightweight State Management for React. Contribute to gabrielyoon7/external-state development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gabrielyoon7/external-state&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bUGiUV/hyTcKja4Qv/N8jKnYVYGVdbHyGSgSL90K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot; data-og-url=&quot;https://github.com/gabrielyoon7/external-state&quot;&gt;&lt;a href=&quot;https://github.com/gabrielyoon7/external-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gabrielyoon7/external-state&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bUGiUV/hyTcKja4Qv/N8jKnYVYGVdbHyGSgSL90K/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 - gabrielyoon7/external-state: Easy and Lightweight State Management for React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Easy and Lightweight State Management for React. Contribute to gabrielyoon7/external-state 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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gabrielyoon7.github.io/external-state/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;라이브러리를 테스트해볼 수 있는 데모 페이지&lt;/span&gt;&lt;/a&gt;는 다음과 같습니다.&lt;br /&gt;&lt;a href=&quot;https://www.npmjs.com/package/external-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;npmjs 문서입니다.&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;라이브러리 구성 및 동작 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Store는 상태 관리 인스턴스를 생성한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥에서 주어진 초기 상태 값은 StateManager라는 클래스에 전달됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9m8Lq/btsmqS1wH3w/GHkNPD1wsaCYZfB6lv5qc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9m8Lq/btsmqS1wH3w/GHkNPD1wsaCYZfB6lv5qc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9m8Lq/btsmqS1wH3w/GHkNPD1wsaCYZfB6lv5qc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9m8Lq%2FbtsmqS1wH3w%2FGHkNPD1wsaCYZfB6lv5qc1%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;572&quot; height=&quot;151&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export const store = &amp;lt;T&amp;gt;(initialState: T) =&amp;gt; {
&amp;nbsp;&amp;nbsp;const stateManager = new StateManager&amp;lt;T&amp;gt;(initialState);
&amp;nbsp;&amp;nbsp;return stateManager;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 상태 값을 전달받은 store 함수는 StateManager라는 어떤 상태 관리 인스턴스를 생성합니다.&lt;br /&gt;생성된 StateManager 인스턴스가 반환되어 store가 곧 초기 값을 가지는 StateManager가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1399&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OMLof/btsmsfuSIG1/XyZg88ygdn5I69dkHz674K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OMLof/btsmsfuSIG1/XyZg88ygdn5I69dkHz674K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OMLof/btsmsfuSIG1/XyZg88ygdn5I69dkHz674K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOMLof%2FbtsmsfuSIG1%2FXyZg88ygdn5I69dkHz674K%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;1399&quot; height=&quot;307&quot; data-origin-width=&quot;1399&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;예를 들어, 다음과 같은 코드가 있다고 할 때&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { store } from &quot;external-state&quot;;

export const countStore = store&amp;lt;number&amp;gt;(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;countStore는 곧 0을 초기값으로 가지는 StateManager 인스턴스이기도 하게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그러면 StateManager에 대해서 알아보겠습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;StateManager는 react 바깥에 있는 어떤 저장소이다. 근데 이게 그냥 저장소는 아니고 좀 특별한 저장소다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export type SetStateCallbackType&amp;lt;T&amp;gt; = (prevState: T) =&amp;gt; T;

export interface DataObserver&amp;lt;T&amp;gt; {
  setState: (param: SetStateCallbackType&amp;lt;T&amp;gt; | T) =&amp;gt; void;
  getState: () =&amp;gt; T;
  subscribe: (listener: () =&amp;gt; void) =&amp;gt; () =&amp;gt; void;
  emitChange: () =&amp;gt; void;
}

class StateManager&amp;lt;T&amp;gt; implements DataObserver&amp;lt;T&amp;gt; {
  public state: T;
  private listeners: Array&amp;lt;() =&amp;gt; void&amp;gt; = [];

  constructor(initialState: T) {
    this.state = initialState;
  }

  setState = (param: SetStateCallbackType&amp;lt;T&amp;gt; | T) =&amp;gt; {
    if (param instanceof Function) {
      const newState = param(this.state);
      this.state = newState;
    } else {
      this.state = param;
    }

    this.emitChange();
  };

  getState = () =&amp;gt; {
    return this.state;
  };

  subscribe = (listener: () =&amp;gt; void) =&amp;gt; {
    this.listeners = [...this.listeners, listener];

    return () =&amp;gt; {
      this.listeners = this.listeners.filter((l) =&amp;gt; l !== listener);
    };
  };

  emitChange = () =&amp;gt; {
    for (const listener of this.listeners) {
      listener();
    }
  };
}

export default StateManager;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StateManager 클래스는 외부에서 받아온 초기값을 상태로 가집니다.&lt;br /&gt;setState, getState, subscribe, emitChange를 메서드로 가집니다.&lt;br /&gt;여기서 작성된 코드들은 react에서 외부 저장소와 소통하기 위한 &lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;최소한의 규격&lt;/span&gt;&lt;/a&gt;입니다.&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- subscribe: 단일 콜백 인수를 사용하여 스토어에 구독하는 함수입니다. 스토어가 변경되면 제공된 콜백을 호출해야 합니다. 그러면 구성 요소가 다시 렌더링 됩니다. 구독 기능은 구독을 정리하는 기능을 반환해야 합니다. (구독에 관련된 데이터는 리스너 배열 필드에 넣어서 관리합니다.)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- &lt;/span&gt;&lt;/span&gt;emitChange: 리스너 배열 필드에 담겨있는 모든 리스너를 실행합니다. 즉, 구독된 어떤 것을 순차적으로 실행하게 합니다. 이는 리액트 DOM을 강제로 일깨워주는 옵저버 패턴의 역할을 하게 됩니다. 이 과정 때문에 react DOM이 정확한 재 렌더링 지점을 파악할 수 있게됩니다. (최적화 문제에서 자유로워짐)&lt;br /&gt;- setState: 상태를 업데이트합니다. 다만 상태가 업데이트 됐음을 알려야 하므로 emitChange를 실행시켜 react DOM을 강제로 동기화시킵니다.&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;-&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;getState: 호출되는 순간 현재 상태 값을 읽습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;좀 어렵지만 리액트에서 이런 규격을 가져야 useSyncExternalStore훅을 쓸 수 있게 해 줍니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 예제에서는 단순한 자바스크립트 객체로 짜여있었지만 인스턴스를 자유롭게 찍어낼 수 있는 class 구조로 개선하고 추상화하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 여기까지만 구현해도 useSyncExternalStore를 사용하는데 지장이 없습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 선언한 store객체에서 subscribe와 getState를 꺼내서 직접 전달해 주면 그만이기 때문이죠.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 결국 이 과정 자체가 반복된 작업을 요구하게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;리액트 컴포넌트에서 쉽게 접근하도록 출구를 열어주자!&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액트 컴포넌트에서는 바닐라 JS로 상태를 업데이트하는 것보다는 useState와 비슷한 형태로 훅을 사용하는 것이 훨씬 보기 깔끔할 것입니다. &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매번 스토어에서 무언가를 직접 꺼내지 않도록 하는 중간 커스텀 훅이 필요합니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export const useExternalState = &amp;lt;T&amp;gt;(
  store: DataObserver&amp;lt;T&amp;gt;
): [T, (param: SetStateCallbackType&amp;lt;T&amp;gt; | T) =&amp;gt; void] =&amp;gt; {
  const { subscribe, getState, setState } = store;
  const state = useSyncExternalStore(subscribe, getState);

  return [state, setState];
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfd;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 훅은, 바깥에서 받아온 store를 활용하여 구독/업데이트 기능을 배열로 반환합니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;모식도를 그려보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;1008&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FdTt1/btsmtDIJdtm/58czLfpeixSSD27xY3Vdn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FdTt1/btsmtDIJdtm/58czLfpeixSSD27xY3Vdn0/img.png&quot; data-alt=&quot;이 코드들을 추상화하게 만든 핵심 구조도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FdTt1/btsmtDIJdtm/58czLfpeixSSD27xY3Vdn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFdTt1%2FbtsmtDIJdtm%2F58czLfpeixSSD27xY3Vdn0%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;1770&quot; height=&quot;1008&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;1008&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;React 컴포넌트는 어디선가 생성된 store() 객체를 useExternalStore에 넘겨주고, [상태, 상태업데이트함수]를 받게 됩니다.&lt;br /&gt;마치 기존의 useState나 useRecoilState처럼 말이죠.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;정리하면 다음과 같습니다.&lt;br /&gt;푸른 영역은 React DOM&lt;br /&gt;녹색 영역은 직접 호출해야 하는 라이브러리의 영역 (하지만 최대한 단순한 형태로 구성해서 개발자의 부담을 덜어주는 형태)&lt;br /&gt;빨간색은 개발자가 직접 건들지 못하지만 간접적으로 사용할 수 있는 영역&lt;br /&gt;노란색은 React 18 엔진의 영역입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이외에 제공되는 다른 커스텀 훅들도 거의 비슷한 구조를 띄고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692793766053&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 추가로 구현할 수 있는 함수들

export const useSetExternalState = &amp;lt;T&amp;gt;(store: DataObserver&amp;lt;T&amp;gt;) =&amp;gt; {
  const { setState } = store;

  return setState;
};

export const useExternalValue = &amp;lt;T&amp;gt;(store: DataObserver&amp;lt;T&amp;gt;) =&amp;gt; {
  const { subscribe, getState } = store;
  const state = useSyncExternalStore(subscribe, getState);

  return state;
};

// 바닐라JS 영역에서 자연스러운 읽기를 지원하는 함수

export const getStoreSnapshot = &amp;lt;T&amp;gt;(store: DataObserver&amp;lt;T&amp;gt;) =&amp;gt; {
  return store.getState();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;더 다양한 예제는 &lt;a href=&quot;https://github.com/gabrielyoon7/external-state/tree/main/src/examples&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;여기에서 확인&lt;/span&gt;&lt;/a&gt;할 수 있고&lt;br /&gt;작성한 라이브러리 코드 전문은 &lt;a href=&quot;https://github.com/gabrielyoon7/external-state/tree/main/src/lib/external-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;여기에서 확인&lt;/span&gt;&lt;/a&gt;할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이후에 시간을 내서 persist 기능을 추가할 예정입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;전역상태 관리 라이브러리를 다운 받기 싫다면&lt;br /&gt;이 글처럼 직접 만들어 써보는 것은 어떨까요?&lt;br /&gt;겨우 파일 수십 줄로 만든 초경량 상태관리 라이브러리였습니다&lt;/p&gt;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>external-state</category>
      <category>useExternalState</category>
      <category>useSyncExternalStore</category>
      <category>리액트 상태관리</category>
      <category>상태관리 라이브러리</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/151</guid>
      <comments>https://leirbag.tistory.com/151#entry151comment</comments>
      <pubDate>Wed, 5 Jul 2023 01:55:13 +0900</pubDate>
    </item>
    <item>
      <title>Recoil 상태 관리를 위한 강력한 도구 : useRecoilCallback(), Snapshot 객체를 활용한 recoil 상태 관리</title>
      <link>https://leirbag.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://recoiljs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Recoil&lt;/a&gt;은 React 애플리케이션의 상태 관리를 위한 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서의 &lt;a href=&quot;https://recoiljs.org/docs/basic-tutorial/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Basic Tutorial&lt;/a&gt;를 참고해 보면 atom과 selector를 활용한 상태 관리가 그렇게 어려워 보이지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 useRecoilState으로 대부분의 로직을 리액트의 영역으로 끌고 온 다음 가공한 다음 다시 recoil로 돌려줘서 상태를 업데이트하곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이게 과연 괜찮은 걸까요? 그렇다면 recoil은 하는 일이 뭘까요?&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;u&gt;&lt;i&gt;어? 이게 뭐지?&lt;/i&gt;&lt;/u&gt; 스러운 장면들이 나올 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 이 글을 다 읽을 때 쯤에는 recoil을 recoil 답게 쓸 수 있을 것 같다는 생각이 들 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 쪽의 &lt;b&gt;1. react스러운 recoil 상태 관리&lt;/b&gt;는 다 아는 내용일 것이므로, 대충 훑어보고 후반부의 &lt;b&gt;2.  recoil다운 recoil 상태 관리&lt;/b&gt;로 건너 뛰어도 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;todoListState.js&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;먼저 일단 앞으로 사용될 todoListState atom을 선언해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685716437274&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {atom} from &quot;recoil&quot;;

export const todoListState = atom({
  key: 'TodoList',
  default: []
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;todoListState는 보시다시피 단순한 배열을 가지는 atom입니다.&lt;br /&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;화면-기록-2023-06-03-01.09.35.gif&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cx6DbP/btsivRcKvYX/KlQ5UFXgQEetPanpHzKN2k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cx6DbP/btsivRcKvYX/KlQ5UFXgQEetPanpHzKN2k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cx6DbP/btsivRcKvYX/KlQ5UFXgQEetPanpHzKN2k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cx6DbP/btsivRcKvYX/KlQ5UFXgQEetPanpHzKN2k/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;583&quot; height=&quot;582&quot; data-filename=&quot;화면-기록-2023-06-03-01.09.35.gif&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;1568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. react 스러운 recoil 상태 관리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1-1. React에서 recoil 상태를 관리하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 공식문서의&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://recoiljs.org/docs/basic-tutorial/intro&quot;&gt;Basic Tutorial&lt;/a&gt; 에서는 다음과 같이 상태를 관리하도록 소개하곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. useRecoilState로 react에서 [사용할 상태, 상태 업데이트 함수]를 끌어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 마치 useState와 같은 방법으로 상태를 구독하거나 업데이트해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TodoPage.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685718549508&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useRecoilState} from &quot;recoil&quot;;
import {todoListState} from &quot;../atoms/todo.jsx&quot;;

function TodoPage() {
  const [todoList, setTodoList] = useRecoilState(todoListState);

  const loadTodoList = async () =&amp;gt; {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos')
    const data = await response.json();
    setTodoList(data);
  }

  const handleCheckboxChange = (todoId) =&amp;gt; {
    const newTodoList = todoList.map((todo) =&amp;gt; todo.id === todoId ? {...todo, completed: !todo.completed} : todo);
    setTodoList(newTodoList);
  };

  const handleDelete = (todoId) =&amp;gt; {
    const newTodoList = todoList.filter((todo) =&amp;gt; todo.id !== todoId);
    setTodoList(newTodoList);
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={loadTodoList}&amp;gt;할 일 불러오기&amp;lt;/button&amp;gt;
      {
        todoList.map((todo) =&amp;gt; (
          &amp;lt;div key={todo.id} style={{display: 'flex'}}&amp;gt;
            &amp;lt;input
              type=&quot;checkbox&quot;
              checked={todo.completed}
              onChange={() =&amp;gt; handleCheckboxChange(todo.id)}
            /&amp;gt;
            &amp;lt;div&amp;gt;{todo.title}&amp;lt;/div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; handleDelete(todo.id)}&amp;gt;삭제&amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
    &amp;lt;/&amp;gt;
  )
}

export default TodoPage;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wgUpS/btsisvW2fLF/LX0rKxeCAe01gvAmKrtYe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wgUpS/btsisvW2fLF/LX0rKxeCAe01gvAmKrtYe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wgUpS/btsisvW2fLF/LX0rKxeCAe01gvAmKrtYe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwgUpS%2FbtsisvW2fLF%2FLX0rKxeCAe01gvAmKrtYe0%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;940&quot; height=&quot;487&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시다시피 todoList와 setTodoList로 useRecoilState에게 접근이 가능토록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 recoil이 무엇인지 모르는 사람도 해석할 수 있을 만큼 react 스러운 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;atom상태를 react로 끌고 와서 작업을 해주는 코드이기 때문이죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 로직을 바깥으로 분리해주고 싶다면 어떻게 할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRecoilState도 hook이니깐 커스텀 훅으로 관리할 수 있지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1-2.  custom hook에서 recoil 상태를 관리하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 recoil 훅을 커스텀 훅에 넣어서 관리해 보겠습니다.&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;useTodo.js&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685719159177&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useRecoilState} from &quot;recoil&quot;;
import {todoListState} from &quot;../../atoms/todo.jsx&quot;;

export const useTodo = () =&amp;gt; {

  const [todoList, setTodoList] = useRecoilState(todoListState);

  const loadTodoList = async () =&amp;gt; {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos')
    const data = await response.json();
    setTodoList(data);
  }

  const changeCheckbox = (todoId) =&amp;gt; {
    const newTodoList = todoList.map((todo) =&amp;gt; todo.id === todoId ? {...todo, completed: !todo.completed} : todo)
    setTodoList(newTodoList);
  };

  const deleteTodo = (todoId) =&amp;gt; {
    const newTodoList = todoList.filter((todo) =&amp;gt; todo.id !== todoId);
    setTodoList(newTodoList);
  }

  return {
    todoList,
    loadTodoList,
    changeCheckbox,
    deleteTodo
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TodoWithCustomHookPage.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685719140119&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useTodo} from &quot;./useTodo.js&quot;;

function TodoWithCustomHookPage() {
  const {
    todoList, loadTodoList, changeCheckbox, deleteTodo
  } = useTodo();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={loadTodoList}&amp;gt;할 일 불러오기&amp;lt;/button&amp;gt;
      {
        todoList.map((todo) =&amp;gt; (
          &amp;lt;div key={todo.id} style={{display: 'flex'}}&amp;gt;
            &amp;lt;input
              type=&quot;checkbox&quot;
              checked={todo.completed}
              onChange={() =&amp;gt; changeCheckbox(todo.id)}
            /&amp;gt;
            &amp;lt;div&amp;gt;{todo.title}&amp;lt;/div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; deleteTodo(todo.id)}&amp;gt;삭제&amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
    &amp;lt;/&amp;gt;
  )
}

export default TodoWithCustomHookPage;&lt;/code&gt;&lt;/pre&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;941&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GsBND/btsiu17K2w0/TGIhWcdKikoakZ1fW8slGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GsBND/btsiu17K2w0/TGIhWcdKikoakZ1fW8slGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GsBND/btsiu17K2w0/TGIhWcdKikoakZ1fW8slGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGsBND%2Fbtsiu17K2w0%2FTGIhWcdKikoakZ1fW8slGK%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;941&quot; height=&quot;487&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-1 예제와 달리 대부분의 상태 관리 로직이 커스텀 훅으로 분리되었네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 recoil이 할 수 있는 역할은 무엇일까요? 그저 상태를 전역으로만 관리하는 것이 다일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-1과 1-2 예제에서는 recoil 상태를 하나만 구독하고 있지만, 만약 여러 상태가 구독되는 경우에는 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상하셨겠지만 커스텀 훅에서 여러 상태 값을 호출하고 활용하는 순간 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;수많은 컴포넌트에 영향&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;을 끼칠 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;recoil상태는 전역으로 관리되고 있기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 recoil의 상태를 다른 방법으로 관리해 보는 것은 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. recoil 다운 recoil 상태 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. useRecoilCallback()과 snapshot 객체를 활용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에는 &lt;a href=&quot;https://recoiljs.org/docs/api-reference/core/useRecoilCallback/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useRecoilCallback()&lt;/a&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;이 훅은 useCallback()과 사용하는 방법이 비슷하지만, 추가로 Recoil 상태에서 작동할 수 있는 API도 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 정말 중요한 것은 Recoil 상태에서 작동할 수 있는 API도 제공한다는 것인데요, React 내에서 Recoil 기능을 쓸 수 있다는 것은 &lt;span style=&quot;color: #ee2323; background-color: #f6e199;&quot;&gt;React로 어떤 기능을 끌고 오지 않고도 Recoil 내부 기능만으로도 콜백을 동작시킬 수 있다&lt;/span&gt;는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서 볼 수 있듯이 useRecoilCallback()에 의존성 배열이 존재하는 이유는 useCallback()처럼 사용할 수도 있기 때문입니다. 하지만 이 글에서는 해당 목적으로 사용하려는 것이 아닌, recoil 기능을 직접 사용하려는 목적에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 앞선 1-1, 1-2 예제와 달리 recoil 내부의 동작만으로도 상태를 업데이트할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 1-1 코드를 useRecoilCallback() 버전으로 개선해 본 예제입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TodoWithUseRecoilCallbackPage.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685720402846&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useRecoilCallback, useRecoilValue} from &quot;recoil&quot;;
import {todoListState} from &quot;../../atoms/todo.jsx&quot;;

function TodoWithUseRecoilCallbackPage() {
  const todoList = useRecoilValue(todoListState);

  const loadTodoList = useRecoilCallback(({set}) =&amp;gt; async () =&amp;gt; {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos')
    const data = await response.json();
    set(todoListState, data);
  }, []);

  const handleCheckboxChange = useRecoilCallback(({set, snapshot}) =&amp;gt; async (todoId) =&amp;gt; {
    const todos = await snapshot.getPromise(todoListState);
    set(todoListState, todos.map((todo) =&amp;gt; todo.id === todoId ? {...todo, completed: !todo.completed} : todo));
  }, []);

  const handleDelete = useRecoilCallback(({set, snapshot}) =&amp;gt; async (todoId) =&amp;gt; {
    const todos = await snapshot.getPromise(todoListState);
    set(todoListState, todos.filter((todo) =&amp;gt; todo.id !== todoId))
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={loadTodoList}&amp;gt;할 일 불러오기&amp;lt;/button&amp;gt;
      {
        todoList.map((todo) =&amp;gt; (
          &amp;lt;div key={todo.id} style={{display: 'flex'}}&amp;gt;
            &amp;lt;input
              type=&quot;checkbox&quot;
              checked={todo.completed}
              onChange={() =&amp;gt; handleCheckboxChange(todo.id)}
            /&amp;gt;
            &amp;lt;div&amp;gt;{todo.title}&amp;lt;/div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; handleDelete(todo.id)}&amp;gt;삭제&amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
    &amp;lt;/&amp;gt;
  )
}

export default TodoWithUseRecoilCallbackPage;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBFOD0/btsitTQa3Z2/uGUgPHt8RQF6I84dLm1NS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBFOD0/btsitTQa3Z2/uGUgPHt8RQF6I84dLm1NS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBFOD0/btsitTQa3Z2/uGUgPHt8RQF6I84dLm1NS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBFOD0%2FbtsitTQa3Z2%2FuGUgPHt8RQF6I84dLm1NS0%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;992&quot; height=&quot;487&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시다시피 기존의 예제와는 달리 useRecoilState()의 사용이 필요하지 않습니다. &lt;br /&gt;useRecoilCallback() 내부에서 &lt;span style=&quot;background-color: #f6e199; color: #ee2323;&quot;&gt;recoil의 상태로 직접 접근이 가능한 setter함수가 사용이 가능&lt;/span&gt;하기 때문입니다.&lt;br /&gt;즉, 정리하면 useRecoilCallback() 내부의 코드는 react가 아닌 recoil의 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 처음 보는 snapshot이 등장하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 설명해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;useRecoilCallback()은 비동기 작업이나 외부 API 호출과 같은 부작용을 관리하는 데 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;2. Snapshot은 Recoil 상태를 안정적으로 읽고 쓰는 데 도움을 줍니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRecoilCallback()을&amp;nbsp;사용하여&amp;nbsp;비동기&amp;nbsp;작업을&amp;nbsp;처리하고,&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;Snapshot을&amp;nbsp;사용하여&amp;nbsp;상태의&amp;nbsp;일관성을&amp;nbsp;유지하면서&amp;nbsp;작업을&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;await snapshot.getPromise(todoListState);&lt;/span&gt;은 useRecoilCallback()의 평가 시점과 관계없이 &lt;span style=&quot;color: #ee2323; background-color: #f6e199;&quot;&gt;최신 상태의 recoil 상태&lt;/span&gt;(atom 혹은 selector)를 읽을 수 있도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 평범하게 getter를 사용하여 접근한다면, recoil 상태를 읽는 것 은 가능하지만 해당 상태가 다른 컴포넌트에 의해 변화하는 경우 과거의 상태 값을 읽어 낼 가능성이 있습니다. (useRecoilCallback()에 의존성 배열을 부여하면 snapshot 없이도 getter 사용이 가능하기는 하지만, 추천되지 않습니다.)&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;즉, 상태를 안정적으로 호출하기 위해서는 Snapshot의 사용이 반드시 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 조합을 통해 복잡한 상태 관리 로직을 깔끔하게 구성하고 Recoil의 장점을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;안정적으로 &lt;/span&gt;최대한 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. getCallback()을 활용한 repository selector 만들기&lt;/b&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;앞선 useRecoilCallback()은 결국 hook이기에 컴포넌트에 의존하여 호출될 수밖에 없고, 외부로 분리하기 위해서는 결국 커스텀 훅이 사용될 수 밖에 없는데요, 다시 말하면 재사용성이 오히려 떨어집니다. (useRecoilCallback 내부에 쓰인 함수만 바깥으로 빼는 방법이 있겠지만 유지보수에 방해가 될 수 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 useRecoilCallback() 훅을 리액트 바깥에서도 쓸 수 있다면 얼마나 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 훅을 바깥에서 쓰는 것은 아니지만, 콜백을 사용하는 기능인 getCallback()을 selector 내부에서 지원하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 훅 버전의 1-2와 useRecoilCallback 버전의 2-1 예제를 적절하게 섞은 듯한 모습의 예제를 소개합니다.&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;todoRepository.js&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685721758825&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {selector} from &quot;recoil&quot;;
import {todoListState} from &quot;../../atoms/todo.jsx&quot;;

export const todoRepository = selector({
  key: 'todoRepository',
  get: ({getCallback}) =&amp;gt; {

    const loadTodoList = getCallback(({set}) =&amp;gt; async () =&amp;gt; {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos')
      const data = await response.json();
      set(todoListState, data);
    });

    const updateCheckboxChange = getCallback(({set, snapshot}) =&amp;gt; async (todoId) =&amp;gt; {
      const todos = await snapshot.getPromise(todoListState);
      set(todoListState, todos.map((todo) =&amp;gt; todo.id === todoId ? {...todo, completed: !todo.completed} : todo));
    });

    const deleteTodo = getCallback(({set, snapshot}) =&amp;gt; async (todoId) =&amp;gt; {
      const todos = await snapshot.getPromise(todoListState);
      set(todoListState, todos.filter((todo) =&amp;gt; todo.id !== todoId))
    });

    return {
      loadTodoList,
      updateCheckboxChange,
      deleteTodo
    }
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TodoWithGetCallbackPage.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685721801904&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useRecoilValue} from &quot;recoil&quot;;
import {todoListState} from &quot;../../atoms/todo.jsx&quot;;
import {todoRepository} from &quot;./todoRepository.js&quot;;

function TodoWithGetCallbackPage() {
  const todoList = useRecoilValue(todoListState);
  const {
    loadTodoList, updateCheckboxChange, deleteTodo
  } = useRecoilValue(todoRepository);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={loadTodoList}&amp;gt;할 일 불러오기&amp;lt;/button&amp;gt;
      {
        todoList.map((todo) =&amp;gt; (
          &amp;lt;div key={todo.id} style={{display: 'flex'}}&amp;gt;
            &amp;lt;input
              type=&quot;checkbox&quot;
              checked={todo.completed}
              onChange={() =&amp;gt; updateCheckboxChange(todo.id)}
            /&amp;gt;
            &amp;lt;div&amp;gt;{todo.title}&amp;lt;/div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; deleteTodo(todo.id)}&amp;gt;삭제&amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        ))
      }
    &amp;lt;/&amp;gt;
  )
}

export default TodoWithGetCallbackPage;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kU9Gt/btsit0uNv7L/X90hIYZuou9zHAxxXbX1Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kU9Gt/btsit0uNv7L/X90hIYZuou9zHAxxXbX1Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kU9Gt/btsit0uNv7L/X90hIYZuou9zHAxxXbX1Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkU9Gt%2Fbtsit0uNv7L%2FX90hIYZuou9zHAxxXbX1Kk%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;1404&quot; height=&quot;488&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시다시피 사실상 모든 상태관리를 recoil (정확히 말하면 selector) 스스로 할 수 있게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 이 예제에서는 단순히 하나의 recoil 상태만을 snapshot으로부터 읽고 있기에 그 효과가 다소 체감되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여러 recoil 상태를 읽어내야 할 때 위 방법의 장점이 극대화될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 snapshot으로부터만 알아내고, 상태 그 자체를 구독하여 읽는 행위를 근본적으로 하지 않고 있으므로 최적화 이슈에서도 자유롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;또, 이 repository는 어떠한 상태도 구독하고 있지 않기에 불필요한 재 렌더링을 유도하지 않으면서 수 많은 컴포넌트에서 사용될 수 있습니다.&lt;/span&gt;&lt;/i&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;(사실 위 예제처럼 snapshot을 읽는 것이 아닌 setter함수 내부의 콜백으로 이전 상태를 호출하는 것이 더 나았을 수도 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유의할 사항으로는 어떤 상태를 구독하는 메서드는 따로 제작하지 않고 컴포넌트에서 곧바로 useRecoilValue()를 활용하는 것이 더 낫습니다.&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/GeoffCox/recoil-examples/blob/master/dispatcher-tutorial/src/dispatcher.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useRecoilCallback()을  특정 컴포넌트에 넣어두고 dispatcher처럼 사용하는 예제도 존재&lt;/a&gt;하나, 결국 어떤 컴포넌트에 의존하는 방식보다는 2-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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://recoiljs.org/docs/api-reference/core/useRecoilCallback#callback-interface&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;recoil callback&lt;/a&gt;을 올바르게 사용하면 ① recoil의 로직을 한 군데 모으면서, ② recoil 본연의 기능을 활용하면서, ③ 여러 상태를 참조하더라도, ④ 잠재적인 성능 이슈까지 고려할 수 있게 됩니다.&lt;/p&gt;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>recoil</category>
      <category>recoil getCallback</category>
      <category>recoil useRecoilCallback</category>
      <category>useRecoilCallback</category>
      <category>리코일</category>
      <category>리코일 상태관리</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/148</guid>
      <comments>https://leirbag.tistory.com/148#entry148comment</comments>
      <pubDate>Sat, 3 Jun 2023 02:01:20 +0900</pubDate>
    </item>
    <item>
      <title>실험으로 확인하는 React 최적화  (memo, useMemo, useCallback)</title>
      <link>https://leirbag.tistory.com/147</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React로 작성 된 프로젝트가 규모가 커지면 커질수록 관리해야하는 컴포넌트의 갯수가 많아지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 규모의 애플리케이션에서는 React 컴포넌트의 렌더링 성능이 곧 사용자 경험에 영향을 미치게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 렌더링을 최소화 하여 React의 성능을 극대화할 필요가 있는데요, 그 중에서도 리액트의 메모이제이션을 소개하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션이나 memo, useMemo, useCallback같은 경우에는 리액트 공식 문서에도 좋은 예제와 함께 설명도 잘 되어있지만&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;b&gt;React에서는 재렌더링(re-rendering)이 언제 일어나는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 상태가 바뀌면 본인과 그 자식들이 모두 재렌더링 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684246171914&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;


function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;34&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/syoiE/btsgaLAFXoj/gkR4AD3KgvlmvpTKFZgFRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/syoiE/btsgaLAFXoj/gkR4AD3KgvlmvpTKFZgFRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/syoiE/btsgaLAFXoj/gkR4AD3KgvlmvpTKFZgFRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsyoiE%2FbtsgaLAFXoj%2FgkR4AD3KgvlmvpTKFZgFRk%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;158&quot; height=&quot;34&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;34&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 여러번 눌러보면 렌더링이 될 때마다 출력문이 찍히는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-13.31.39.gif&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwMFFS/btsgexvdGPY/LPyytLXLMbUjMTTKCs3PKK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwMFFS/btsgexvdGPY/LPyytLXLMbUjMTTKCs3PKK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwMFFS/btsgexvdGPY/LPyytLXLMbUjMTTKCs3PKK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cwMFFS/btsgexvdGPY/LPyytLXLMbUjMTTKCs3PKK/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;1232&quot; height=&quot;838&quot; data-filename=&quot;화면-기록-2023-05-17-13.31.39.gif&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하지만 이 코드에서는 버튼을 누를 때 마다 자신의 상태를 업데이트 하고 있으므로 재 렌더링이 일어나면서 그에 해당하는 모든 코드들도 다시 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 콘솔에 계속 값이 찍히게 되는 것이겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재렌더링(re-rendering)이 일어날 때 자식 컴포넌트들은 어떻게 될까?&lt;/b&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;이번에는 Box라는 자식 컴포넌트를 추가하여 한번 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684246250924&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

function Box() {
  console.log('Box 렌더링 됨')
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: 'red' }} /&amp;gt;
  )
}


function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Box /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 버튼을 누를 때 마다 Box가 다시 그려지는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-13.37.37.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCuXlD/btsggL0QzWV/byMC95F7QmrMTzPjVzZiQk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCuXlD/btsggL0QzWV/byMC95F7QmrMTzPjVzZiQk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCuXlD/btsggL0QzWV/byMC95F7QmrMTzPjVzZiQk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bCuXlD/btsggL0QzWV/byMC95F7QmrMTzPjVzZiQk/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-13.37.37.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;React.memo() 를 사용하여 자식 컴포넌트의 재렌더링 방지하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션을 사용하면 React 컴포넌트의 재 렌더링을 방지할 수 있다고들 하는데 실제로 그럴까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서를 확인해보면 다음과 같이 말합니다.&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;memo&amp;nbsp;lets&amp;nbsp;you&amp;nbsp;skip&amp;nbsp;re-rendering&amp;nbsp;a&amp;nbsp;component&amp;nbsp;when&amp;nbsp;its&amp;nbsp;props&amp;nbsp;are&amp;nbsp;unchanged.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props가 변하지 않았을 때에만 재렌더링을 건너 뛴다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box의 경우에는 현재 넘겨주는 prop 자체가 없으므로 memo를 적용하면 재 렌더링이 방지되지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1684246292766&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo, useState } from &quot;react&quot;;

function Box() {
  console.log('Box 렌더링 됨')
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: 'red' }} /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 마구마구 눌러보면 다음과 같은 결과가 나오는데요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekSnR0/btsgcO4u0iy/zeaAnc1tahKUlTJoBjtRM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekSnR0/btsgcO4u0iy/zeaAnc1tahKUlTJoBjtRM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekSnR0/btsgcO4u0iy/zeaAnc1tahKUlTJoBjtRM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekSnR0%2FbtsgcO4u0iy%2FzeaAnc1tahKUlTJoBjtRM0%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;161&quot; height=&quot;145&quot; data-origin-width=&quot;161&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-13.43.37.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cefd0C/btsgcMGhFki/shwJk8X8DkiZ5LRqdLKQQK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cefd0C/btsgcMGhFki/shwJk8X8DkiZ5LRqdLKQQK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cefd0C/btsgcMGhFki/shwJk8X8DkiZ5LRqdLKQQK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cefd0C/btsgcMGhFki/shwJk8X8DkiZ5LRqdLKQQK/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-13.43.37.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo 처리된 컴포넌트의 props의 변화가 없으므로 재렌더링을 건너 뛰게 됨을 확인할 수 있습니다.&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메모이제이션 처리된 컴포넌트의 props 상태가 바뀌면 React.memo() 는 어떤 의미가 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 앞선 예제 상태에서 자식(Box)의 props가 바뀐다면 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box 컴포넌트가 memo 처리 되어있는데요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684246722750&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo, useState } from &quot;react&quot;;

function Box({ color }: { color: string }) {
  console.log('Box 렌더링 됨')
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: color }} /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox color={color} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 버튼이 두 가지가 있는데요, Box의 prop을 업데이트 하는 버튼과 부모 컴포넌트를 업데이트하는 버튼입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 눌러보면 Box의 재렌더링 여부가 갈리게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-13.51.38.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/snjOq/btsgdBx7jfU/T0U1Ew63lWqKZUfbxwkIk1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/snjOq/btsgdBx7jfU/T0U1Ew63lWqKZUfbxwkIk1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/snjOq/btsgdBx7jfU/T0U1Ew63lWqKZUfbxwkIk1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/snjOq/btsgdBx7jfU/T0U1Ew63lWqKZUfbxwkIk1/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-13.51.38.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시다시피 색상을 바꾸는 행위는 Box의 props를 업데이트 하는 행위이므로 Box가 memo처리 되어있어도 재렌더링을 진행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 부모의 상태를 바꾸는 행위는 Box의 props를 변하게 하지 않기 때문에 부모 컴포넌트만 재렌더링을 진행하고 Box 컴포넌트는 건너 뛰게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;+  보너스&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box 컴포넌트 하나를 더 넣어 보겠습니다. 그래도 같은 결과일까요?&lt;/p&gt;
&lt;pre id=&quot;code_1684247364631&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo, useState } from &quot;react&quot;;

function Box({ color }: { color: string }) {
  console.log(`Box 렌더링 됨 : ${color}`)
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: color }} /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox color={color} /&amp;gt;
      &amp;lt;MemoedBox color={color === 'red' ? 'blue' : 'red'} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도 React.memo()가 잘 동작하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-13.58.53.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qWmFx/btsgkhZq8AE/bwxmur2q7EfL98rLjW2og0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qWmFx/btsgkhZq8AE/bwxmur2q7EfL98rLjW2og0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qWmFx/btsgkhZq8AE/bwxmur2q7EfL98rLjW2og0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/qWmFx/btsgkhZq8AE/bwxmur2q7EfL98rLjW2og0/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-13.58.53.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식의 props가 바뀐 경우에만 재렌더링을 하고, 그 이외의 상황(props의 변화 없이 부모만 재렌더링)에는 최적화가 일어나서 다시 렌더링을 하지 않고 건너뜁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;props가 객체일 때에는 어쩔 때에는 안되는 것 같은데요?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제들은 색상 props을 string 형태로 넘겨주고 있었는데요. 이번에는 props를 객체로 바꿔보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 React에서 작업하다보면 props에 객체를 넘기는 행위는 매우 빈번하게 일어나죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색상을 넘기는 props을 객체로 수정하면 memo 처리가 갑자기 안되는 것을 알 수 있답니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684248761090&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo, useState } from &quot;react&quot;;

function Box({ params }: {
  params: { color: string }
}) {
  console.log(`Box 렌더링 됨 : ${params.color}`)
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: params.color }} /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox params={{ color }} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-14.07.42.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ToOzG/btsge6R4CJp/rghYyNjUEm0bgwA37PheWk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ToOzG/btsge6R4CJp/rghYyNjUEm0bgwA37PheWk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ToOzG/btsge6R4CJp/rghYyNjUEm0bgwA37PheWk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ToOzG/btsge6R4CJp/rghYyNjUEm0bgwA37PheWk/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-14.07.42.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 Box는 메모이제이션이 적용되어 있고, 부모가 재렌더링 되더라도 자식 prop은 변하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 자식인 Box 컴포넌트가 재렌더링이 된 이유가 무엇일까요?&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;위 예제는 이전 예제와 달리 params.color와 같이 객체의 prop을 참조하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 memo의 경우에는 prop의 얕은 비교(&lt;a href=&quot;https://legacy.reactjs.org/docs/shallow-compare.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Shallow Comparison&lt;/a&gt;)를 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재렌더링 과정에서 { color } 와 같이 객체를 새로 생성하게 되면 다른 주소 값을 참조하게 되므로 다르다고 판단하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 원시 값이 아니라면, 객체를 생성하는 과정이 있다면 memo 입장에서는 props가 변경되었다고 보게 될 것이므로 메모이제이션이 일어나지 않습니다.&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;한가지 대안이 있다면 props를 넘길 때 { color } 와 같은 형태로 객체를 생성해서 넘기는 것이 아닌&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1684301240970&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const [params, setParams] = useState({ color: 'red' });&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1684301276002&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;MemoedBox params={params} /&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;다만, 앞선 예제 처럼 React에서는 컴포넌트에게 props를 넘길 때 객체를 즉시 생성해서 넘겨주는 행위가 빈번하므로 좋은 대안은 아니겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재렌더링으로 본의 아니게 새로 생성된 객체를 이전의 객체와 같은 것 처럼 취급하기 with useMemo()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화를 하고 싶은 (재렌더링으로 인해 잠재적으로 새로 생성될 것 같은) 객체에 useMemo를 걸어주면 재 렌더링 시 새로 생성하지 않고 같은 객체로 취급하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684252236144&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { memo, useMemo, useState } from &quot;react&quot;;

function Box({ params }: {
  params: { color: string }
}) {
  console.log(`Box 렌더링 됨 : ${params.color}`)
  return (
    &amp;lt;div style={{ width: &quot;100px&quot;, height: &quot;100px&quot;, margin: '3px', backgroundColor: params.color }} /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimization() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  const params = useMemo(() =&amp;gt; ({ color }), [color]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox params={params} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimization;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-14.30.44.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6QaC0/btsgjlnNDWe/lnvG4YktqU3sJJMHPUtkw1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6QaC0/btsgjlnNDWe/lnvG4YktqU3sJJMHPUtkw1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6QaC0/btsgjlnNDWe/lnvG4YktqU3sJJMHPUtkw1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/6QaC0/btsgjlnNDWe/lnvG4YktqU3sJJMHPUtkw1/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-14.30.44.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 예제에서는 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;params={ {color} }&lt;/span&gt; 와 같이 매번 생성해줬지만, 이제는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;const params = useMemo(() =&amp;gt; ({ color }), [color]);&lt;/span&gt; 와 같이 params 자체를 최적화 할 수 있게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;props에 함수를 넘겨줬더니 메모이제이션이 안됩니다!&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 예제에 이어서 이번에는 함수도 props로 넘겨주겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제와 같이 코드를 수정해보면 메모이제이션이 또 다시 되지 않는 것을 확인할 수 있는데요&lt;/p&gt;
&lt;pre id=&quot;code_1684254072251&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* eslint-disable @typescript-eslint/no-empty-function */
import { memo, useState } from &quot;react&quot;;

function Box({ params, onClick }: {
  params: { color: string };
  onClick: () =&amp;gt; void;
}) {
  console.log(`Box 렌더링 됨 : ${params.color}`)
  return (
    &amp;lt;div
      style={{
        width: &quot;100px&quot;,
        height: &quot;100px&quot;,
        margin: '3px',
        backgroundColor: params.color
      }}
      onClick={onClick}
    /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function NonOptimizationByCallback() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox params={{ color }} onClick={() =&amp;gt; { }} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default NonOptimizationByCallback;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-14.30.44.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdIBip/btsgkh6ymPD/TcG7mqCo6HK7NK4at9lQEk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdIBip/btsgkh6ymPD/TcG7mqCo6HK7NK4at9lQEk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdIBip/btsgkh6ymPD/TcG7mqCo6HK7NK4at9lQEk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bdIBip/btsgkh6ymPD/TcG7mqCo6HK7NK4at9lQEk/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-14.30.44.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 객체 생성과 동시에 props으로 넘겨주는 예제에서 확인해보셨듯이 함수를 넘겨주는 행위도 비슷한 이유로 memo 에서 같은 함수라고 판단하지 않습니다.&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;재렌더링으로 본의 아니게 새로 생성된 함수를 이전의 함수와 같은 것 처럼 취급하기 with useCallback()&lt;/b&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;전달할 함수를 useCallback으로 감싸면 함수가 재생성 되지 않아서 최적화 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1684254274355&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* eslint-disable @typescript-eslint/no-empty-function */
import { memo, useCallback, useMemo, useState } from &quot;react&quot;;

function Box({ params, onClick }: {
  params: { color: string };
  onClick: () =&amp;gt; void;
}) {
  console.log(`Box 렌더링 됨 : ${params.color}`)
  return (
    &amp;lt;div
      style={{
        width: &quot;100px&quot;,
        height: &quot;100px&quot;,
        margin: '3px',
        backgroundColor: params.color
      }}
      onClick={onClick}
    /&amp;gt;
  )
}

const MemoedBox = memo(Box);

function OptimizationByCallback() {
  const [appRenderCount, setAppRenderCount] = useState(0);
  const [color, setColor] = useState('red');

  console.log(`랜더링 횟수 : ${appRenderCount}`);

  const params = useMemo(() =&amp;gt; ({ color }), [color]);
  const onClick = useCallback(() =&amp;gt; { }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MemoedBox params={params} onClick={onClick} /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setAppRenderCount(appRenderCount + 1)}
      &amp;gt;
        앱 다시 렌더링 하기
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; setColor(color === 'red' ? 'blue' : 'red')}
      &amp;gt;
        색상 바꾸기
      &amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export default OptimizationByCallback;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면-기록-2023-05-17-14.45.33.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/up0Ob/btsggL8fFoZ/5sxSnOfKabPw09stUTAJCk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/up0Ob/btsggL8fFoZ/5sxSnOfKabPw09stUTAJCk/img.gif&quot; data-alt=&quot;화면 하단이 잘려서 콘솔 창이 일부 일치하지 않는 것 처럼 보일 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/up0Ob/btsggL8fFoZ/5sxSnOfKabPw09stUTAJCk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/up0Ob/btsggL8fFoZ/5sxSnOfKabPw09stUTAJCk/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;1712&quot; height=&quot;1074&quot; data-filename=&quot;화면-기록-2023-05-17-14.45.33.gif&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1074&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;</description>
      <category>Frontend/React&amp;middot;React Native</category>
      <category>react memo</category>
      <category>react useCallback</category>
      <category>react useMemo</category>
      <category>react 메모</category>
      <category>react 메모이제이션</category>
      <category>react 최적화</category>
      <author>엘리브가</author>
      <guid isPermaLink="true">https://leirbag.tistory.com/147</guid>
      <comments>https://leirbag.tistory.com/147#entry147comment</comments>
      <pubDate>Wed, 17 May 2023 14:56:13 +0900</pubDate>
    </item>
  </channel>
</rss>