<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>망각의 코딩러 흐성진</title>
    <link>https://sj-d.tistory.com/</link>
    <description>개발자 흐성진의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 27 Aug 2026 10:35:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>흐성진</managingEditor>
    <image>
      <title>망각의 코딩러 흐성진</title>
      <url>https://tistory1.daumcdn.net/tistory/3221824/attach/483b5b49da0e43b192821d25f2745819</url>
      <link>https://sj-d.tistory.com</link>
    </image>
    <item>
      <title>React + Vite 프로젝트에 GA4(Google Analytics) 설정하기</title>
      <link>https://sj-d.tistory.com/81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;react.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqW1i5/dJMcahXihRw/kvezy2PTFvy9KQlHyXXsR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqW1i5/dJMcahXihRw/kvezy2PTFvy9KQlHyXXsR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqW1i5/dJMcahXihRw/kvezy2PTFvy9KQlHyXXsR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqW1i5%2FdJMcahXihRw%2Fkvezy2PTFvy9KQlHyXXsR0%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;420&quot; data-filename=&quot;react.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주제 선정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 개인 프로젝트나 사이드 프로젝트를 진행하다 보면&lt;b&gt; &quot;사용자가 어떻게 서비스를 사용하고 있는가?&quot;&amp;nbsp;&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;b&gt;Google Analytics 4(GA4)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GA4는 기존 Universal Analytics와 달리 이벤트 중 심의 분석 구조를 가지고 있고, 사용자 행동을 더 유연하고 세밀하게 추적할 수 있다는 장점이 있다.&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;React + Vite로 구성된 프로젝트 이며 FSD 관점을&lt;/b&gt;&amp;nbsp;기준으로 GA4를 어덯게 연동하고 기본적인 설정을 진행할 수 있는지 정리해 보려고한다.&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;GA4 이벤트 중심 모델 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GA4의 가증 큰 특징은 모든 데이터가 이벤트 기반이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Universal Analytics 에서는&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;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;반면 GA4에서는 페이지뷰 조차 하나의 이벤트로 취급된다.&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;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 data-ke-size=&quot;size16&quot;&gt;그러므로 프론트엔트 코드 레벨에서 이벤트 구조를 적절하게 통제하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Google Analytics 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 GA4를 사용하기위해서는 Google Analytics 에 접속해서 속성을 생성해주오야한다.&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;1. 속성 이름 설정&lt;/h4&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;스크린샷 2025-12-17 오후 4.57.32.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wKZEC/dJMcaaRoing/1VT2zVAOUe45pl6G2awKlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wKZEC/dJMcaaRoing/1VT2zVAOUe45pl6G2awKlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wKZEC/dJMcaaRoing/1VT2zVAOUe45pl6G2awKlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwKZEC%2FdJMcaaRoing%2F1VT2zVAOUe45pl6G2awKlK%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;444&quot; data-filename=&quot;스크린샷 2025-12-17 오후 4.57.32.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 업종 및 카테고리, 규모 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업종 및 카테고리와&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스의 규모를 목적에 맞게 설정해준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 오후 4.58.17.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDHFUF/dJMcabQh735/qUfx9dbEgJcN1vqmwZPxUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDHFUF/dJMcabQh735/qUfx9dbEgJcN1vqmwZPxUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDHFUF/dJMcabQh735/qUfx9dbEgJcN1vqmwZPxUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDHFUF%2FdJMcabQh735%2FqUfx9dbEgJcN1vqmwZPxUk%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;444&quot; data-filename=&quot;스크린샷 2025-12-17 오후 4.58.17.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 비즈니스 목표 설정&lt;/h4&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;스크린샷 2025-12-17 오후 4.58.30.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds4psb/dJMcadtLvPx/WeeuDuhYKSkLKRlh46OIYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds4psb/dJMcadtLvPx/WeeuDuhYKSkLKRlh46OIYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds4psb/dJMcadtLvPx/WeeuDuhYKSkLKRlh46OIYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds4psb%2FdJMcadtLvPx%2FWeeuDuhYKSkLKRlh46OIYk%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;444&quot; data-filename=&quot;스크린샷 2025-12-17 오후 4.58.30.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 데이터 스트림 설정&lt;/h4&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;스크린샷 2025-12-17 오후 4.58.38.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu9a9c/dJMcacn8jzk/MkC53nqjWgHEuksBuNrtfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu9a9c/dJMcacn8jzk/MkC53nqjWgHEuksBuNrtfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu9a9c/dJMcacn8jzk/MkC53nqjWgHEuksBuNrtfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu9a9c%2FdJMcacn8jzk%2FMkC53nqjWgHEuksBuNrtfk%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;444&quot; data-filename=&quot;스크린샷 2025-12-17 오후 4.58.38.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 스트림 ID, 측정 ID 확인&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;b&gt;스트림 ID, 측정 ID&lt;/b&gt;를 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 ID값들을 가지고 이제 React와 연결해주도록한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 오후 5.02.15.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7srUM/dJMcacn8jzo/uTKe19vc4WJeO0tKkPNLUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7srUM/dJMcacn8jzo/uTKe19vc4WJeO0tKkPNLUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7srUM/dJMcacn8jzo/uTKe19vc4WJeO0tKkPNLUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7srUM%2FdJMcacn8jzo%2FuTKe19vc4WJeO0tKkPNLUk%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;444&quot; data-filename=&quot;스크린샷 2025-12-17 오후 5.02.15.png&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;2754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;왜 GA4를 코드 레벨에서 관리해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GA4는 단순히 붙여두면 알아서 수집되는 분석 도구가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Google Analytics에 접속해서 속성을 만들게 되면 코드를 index.html 에 붙여넣으라고 안내를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇게 하면 실제로 어떤 이벤트에 대한 세세한 설정이 어렵다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순하게 GA4 코드 붙여넣기가 아니라 FSD 구조에서 GA4를 어디에 두는게 적절한지&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;h2 data-ke-size=&quot;size26&quot;&gt;React와 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 FSD 아키텍쳐 구조에 맞게 파일을 구성해보려고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID값 같은 경우 노출에 대해서는 최대한 피하는 방향으로 .env파일 안에 위치시키려고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env 파일 안에는 이전에 받아온 G로 시작하는 측정 ID를 넣어주면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1765960992263&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX&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;GA4와 관련된 내용은 Shared 레이어로 src/shared/analytics 의 폴더를 만들고 그 하단에 구성해보려고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 왜 Shared 레이어에 넣는지 의문이 들수도 있다.&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;p data-ke-size=&quot;size16&quot;&gt;API에 대한 에러를 추적할때 (각 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;그래서 Shared 레이어에 위치 시켰다.&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;react-ga4 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-ga4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.npmjs.com/package/react-ga4&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;정확한 측정을 위해서 react-ga4라이 브러리를 설치한다&lt;/p&gt;
&lt;pre id=&quot;code_1765960662062&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add react-ga4

or

npm i react-ga4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 타입 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 : src/shared/analytics/types.ts&lt;/p&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;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.39.18.png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ek5Ar/dJMcadtLwaq/Pek9j5ZKR1buuGSk6oPx00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ek5Ar/dJMcadtLwaq/Pek9j5ZKR1buuGSk6oPx00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ek5Ar/dJMcadtLwaq/Pek9j5ZKR1buuGSk6oPx00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEk5Ar%2FdJMcadtLwaq%2FPek9j5ZKR1buuGSk6oPx00%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;426&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.39.18.png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;1134&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;pre id=&quot;code_1765960802376&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이벤트 파라미터 타입 정의
export interface GAEvent {
  category: string;
  action: string;
  label?: string;
  value?: number;
}

export interface GAPageView {
  path: string;
  title?: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GA4 초기화 및 핵심 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 : src/shared/analytics/ga4.ts&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입을 정의했다면 이제 GA4를 사용하기 위해 초기화를 시키고 그 안에서 사용할 핵심 함수에 대해서 정의를 해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1765961101154&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ReactGA from &quot;react-ga4&quot;;
import { GAEvent } from &quot;./types&quot;;

// GA4 측정 ID (환경변수로 관리)
const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID || &quot;&quot;;

/**
 * GA4 초기화
 */
export const initGA = () =&amp;gt; {
  if (!GA_MEASUREMENT_ID) {
    console.warn(&quot;GA4 Measurement ID가 설정되지 않았습니다.&quot;);
    return;
  }

  ReactGA.initialize(GA_MEASUREMENT_ID, {
    gaOptions: {
      // 디버그 모드 (개발 환경에서만)
      debug_mode: import.meta.env.DEV,
    },
    gtagOptions: {
      // 쿠키 설정
      cookie_flags: &quot;SameSite=None;Secure&quot;,
    },
  });

  console.log(&quot;GA4 초기화 완료&quot;);
};

/**
 * 페이지뷰 추적
 */
export const trackPageView = (path: string, title?: string) =&amp;gt; {
  if (!GA_MEASUREMENT_ID) return;

  ReactGA.send({ 
    hitType: &quot;pageview&quot;, 
    page: path,
    title: title || document.title,
  });
  
  console.log(&quot;GA4 PageView:&quot;, path, title);
};

/**
 * 이벤트 추적
 */
export const trackEvent = (event: GAEvent) =&amp;gt; {
  if (!GA_MEASUREMENT_ID) return;

  ReactGA.event({
    category: event.category,
    action: event.action,
    label: event.label,
    value: event.value,
  });

  console.log(&quot;GA4 Event:&quot;, event);
};

/**
 * 사용자 속성 설정
 */
export const setUserProperties = (properties: Record&amp;lt;string, string | number&amp;gt;) =&amp;gt; {
  if (!GA_MEASUREMENT_ID) return;

  ReactGA.gtag(&quot;set&quot;, &quot;user_properties&quot;, properties);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;App.tsx에서 GA4 초기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.tsx에서 이전에 만들었던 ga4.ts 의 initGa를 호출해주어 초기화를 시켜주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765961517266&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect } from &quot;react&quot;;
import { AppRouter } from &quot;./router&quot;;
import { ToastProvider, AlertProvider } from &quot;@shared/ui&quot;;
import { initGA } from &quot;@shared/analytics&quot;;

export function App() {

  useEffect(() =&amp;gt; {
    initGA();
  }, []);

  useEffect(() =&amp;gt; {
    const handleContextMenu = (e: MouseEvent) =&amp;gt; {
      e.preventDefault();
    };

    document.addEventListener(&quot;contextmenu&quot;, handleContextMenu);

    return () =&amp;gt; {
      document.removeEventListener(&quot;contextmenu&quot;, handleContextMenu);
    };
  }, []);

  return (
    &amp;lt;ToastProvider&amp;gt;
      &amp;lt;AlertProvider&amp;gt;
        &amp;lt;AppRouter /&amp;gt;
      &amp;lt;/AlertProvider&amp;gt;
    &amp;lt;/ToastProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 호출이 완료되었다면 다음과 같이 초기화 완료 메시지가 뜨고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.52.46.png&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dafCpw/dJMcaaDQCyq/OM6iPVGkh1xUDhgglxc4H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dafCpw/dJMcaaDQCyq/OM6iPVGkh1xUDhgglxc4H0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dafCpw/dJMcaaDQCyq/OM6iPVGkh1xUDhgglxc4H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdafCpw%2FdJMcaaDQCyq%2FOM6iPVGkh1xUDhgglxc4H0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;298&quot; height=&quot;52&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.52.46.png&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;52&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;Google Analytics에 들어가게되면 다음 이미지와 같이 활성 사용자가 잡히게된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.53.18.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKCaaJ/dJMcadtLwpG/TU739ZfpPxb2XRv9PqkK5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKCaaJ/dJMcadtLwpG/TU739ZfpPxb2XRv9PqkK5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKCaaJ/dJMcadtLwpG/TU739ZfpPxb2XRv9PqkK5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKCaaJ%2FdJMcadtLwpG%2FTU739ZfpPxb2XRv9PqkK5k%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;321&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.53.18.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Router에서 페이지뷰 추적&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;p data-ke-size=&quot;size16&quot;&gt;그러기 위해선 Router 내부에서 어떤 페이지에 접근하는지 알려주도록한다.&lt;/p&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;AppRouter 의 BrowserRouter의 하단에 작성하도록 한다&lt;/p&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;Router 레벨에서 페이지뷰를 추적하는 이유&lt;/b&gt;는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA환경에서는 페이지가 바꿔도 브라우저의 새로고침이 발생하지 않기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GA입장에서는 사용자가 페이지를 이동했다는 사실을 명시적으로 알려줘야 하기 떄문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1765961841619&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { trackPageView } from &quot;@shared/analytics&quot;;

// 페이지뷰 추적 훅
function usePageTracking() {
  const location = useLocation();

  useEffect(() =&amp;gt; {
    trackPageView(location.pathname + location.search);
  }, [location]);
}

// 페이지 추적 컴포넌트
function PageTracker() {
  usePageTracking();
  return null;
}


export function AppRouter() {
  return (
    &amp;lt;BrowserRouter&amp;gt;
      &amp;lt;PageTracker /&amp;gt;
    &amp;lt;/BrowserRouter&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 페이지 뷰를 추적하도록 했다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trackingPageView에 따라서 어떤 페이지에 접근했는지 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.58.59.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYpcmG/dJMcahbT1Dm/eUeduCJFXEuVptDor8xki1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYpcmG/dJMcahbT1Dm/eUeduCJFXEuVptDor8xki1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYpcmG/dJMcahbT1Dm/eUeduCJFXEuVptDor8xki1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYpcmG%2FdJMcahbT1Dm%2FeUeduCJFXEuVptDor8xki1%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;295&quot; data-filename=&quot;스크린샷 2025-12-17 시간: 17.58.59.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1250&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;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 추적하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 특정 버튼을 누른경우, 특정 폼을 제출한 경우, 또는 에러를 추적하는 경우에 다음과 같이 추적할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1765962870764&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { trackEvent } from &quot;@shared/analytics&quot;;

// 버튼 클릭 추적
const handleLoginClick = () =&amp;gt; {
  trackEvent({
    category: &quot;User&quot;,
    action: &quot;Login&quot;,
    label: &quot;Login Button&quot;,
  });
  // 로그인 로직...
};

// 폼 제출 추적
const handleFormSubmit = () =&amp;gt; {
  trackEvent({
    category: &quot;Form&quot;,
    action: &quot;Submit&quot;,
    label: &quot;Timetable Creation&quot;,
  });
  // 폼 제출 로직...
};

// 에러 추적
const handleError = (error: Error) =&amp;gt; {
  trackEvent({
    category: &quot;Error&quot;,
    action: &quot;API Error&quot;,
    label: error.message,
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 여기서 이벤트 추적은 언제, 어디까지 할까 의문이 들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무조건 많은 이벤트를 추적한다고 해서 좋은 코드는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 핵심 CTA 클릭&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;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;하지만 모든 버튼, 단순 UI 인터렉션, 분석 목적이 불분명한 이벤트는 피하는 방향이 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이벤트 하나하나가 나중에 이 데이터를 보고 어떤 판단을 할 수 있는가? 에 대해서 답변이 가능한 이벤트 들에만 사용하는 것이 좋은 방식이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GA4는 단순히 방문자 수를 확인하는 도구가 아니라,&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 React + Vite 환경에서 Ga4를 단순히 연동하는 방법을 FSD 구조를 기준으로 분석 로직을 어떻게 분리하고 관리할 수 있는지 초점을 맞췄다.&lt;/p&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;shared/analytics&lt;/b&gt; 레이어에 GA4관련 로직을 모아두면서 페이지뷰, 이벤트, 사용자 속성을 공통 인터페이스로 관리하면 프로젝트가 커지더라고 추적 구조가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Router 레벨에서 페이지 뷰를 추적하고 필요한 지점에서만 명확한 의미를 가진 이벤트를 정의함 으로써 불필요한 로그를 줄이고 실제로 판단에 도움이 되는 데이터에 집줄할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세팅을 시작으로 전환 이벤트, 커스텀 디멘션, 퍼널 분석을 위한 이벤트 설계의 단계까지 확장해 나아간다면 단순한 로그 수집을 넘어 의사결정을 돕는분석 환경을 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/codler/react-ga4/blob/HEAD/src/ga4.test.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/codler/react-ga4/blob/HEAD/src/ga4.test.js&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765963145848&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;react-ga4/src/ga4.test.js at d50684824f5d155c772721d93ff2be7ea65767bf &amp;middot; codler/react-ga4&quot; data-og-description=&quot;React Google Analytics 4. Contribute to codler/react-ga4 development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/codler/react-ga4/blob/HEAD/src/ga4.test.js&quot; data-og-url=&quot;https://github.com/codler/react-ga4/blob/d50684824f5d155c772721d93ff2be7ea65767bf/src/ga4.test.js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bo7tjJ/hyZPt2Bng3/toJ3fJ5ZAlkGi8Wn1t7uHK/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_161_1034_262,https://scrap.kakaocdn.net/dn/bsK0GM/hyZP4UsCdM/dExHhxgNk4aRvXelqJKsrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_161_1034_262&quot;&gt;&lt;a href=&quot;https://github.com/codler/react-ga4/blob/HEAD/src/ga4.test.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/codler/react-ga4/blob/HEAD/src/ga4.test.js&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bo7tjJ/hyZPt2Bng3/toJ3fJ5ZAlkGi8Wn1t7uHK/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_161_1034_262,https://scrap.kakaocdn.net/dn/bsK0GM/hyZP4UsCdM/dExHhxgNk4aRvXelqJKsrk/img.png?width=1200&amp;amp;height=600&amp;amp;face=941_161_1034_262');&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-ga4/src/ga4.test.js at d50684824f5d155c772721d93ff2be7ea65767bf &amp;middot; codler/react-ga4&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React Google Analytics 4. Contribute to codler/react-ga4 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/react-ga4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/react-ga4&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;&lt;a href=&quot;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765963183204&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.js] GA4(Google Analytics) - ②API 호출하기&quot; data-og-description=&quot;React에서 GA4(Google Analytics) 보고서 API를 다루는 방법을 알아보고자 한다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-호출하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rm2jx/hyZP7czZMF/0TtOe6rgEwr45rcHwDKs0K/img.png?width=1080&amp;amp;height=565&amp;amp;face=0_0_1080_565,https://scrap.kakaocdn.net/dn/Zfv5o/hyZPTMatni/6MR3P71MUAHCGcfRcalfZK/img.png?width=1080&amp;amp;height=565&amp;amp;face=0_0_1080_565,https://scrap.kakaocdn.net/dn/HI26Q/hyZP7czZOY/11kVfKN3spoFpWdiRmU8Gk/img.png?width=1725&amp;amp;height=2813&amp;amp;face=0_0_1725_2813&quot;&gt;&lt;a href=&quot;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@slight-snow/React.js-GA4Google-Analytics-API-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rm2jx/hyZP7czZMF/0TtOe6rgEwr45rcHwDKs0K/img.png?width=1080&amp;amp;height=565&amp;amp;face=0_0_1080_565,https://scrap.kakaocdn.net/dn/Zfv5o/hyZPTMatni/6MR3P71MUAHCGcfRcalfZK/img.png?width=1080&amp;amp;height=565&amp;amp;face=0_0_1080_565,https://scrap.kakaocdn.net/dn/HI26Q/hyZP7czZOY/11kVfKN3spoFpWdiRmU8Gk/img.png?width=1725&amp;amp;height=2813&amp;amp;face=0_0_1725_2813');&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.js] GA4(Google Analytics) - ②API 호출하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React에서 GA4(Google Analytics) 보고서 API를 다루는 방법을 알아보고자 한다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.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;</description>
      <category>Web/React</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/81</guid>
      <comments>https://sj-d.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 17 Dec 2025 18:38:02 +0900</pubDate>
    </item>
    <item>
      <title>Flutter Supabase 연결하기 (Feat. 공식문서)</title>
      <link>https://sj-d.tistory.com/80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;주제선정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 주제는 &lt;b&gt;Flutter와 Supabase의 연동&lt;/b&gt;이다.&lt;br /&gt;최근 진행 중인 새로운 사이드 프로젝트에 이 기술 스택을 직접 적용해보면서, 보다 효율적인 개발 환경을 구축하고자 하는 목적에서 이 주제를 선택했다.&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;128&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 Firebase나 자체 서버를 활용한 경험이 있었지만, Supabase는 오픈소스 기반으로 백엔드 기능을 간편하게 구현할 수 있다는 점에서 흥미로웠다.&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;128&quot; data-ke-size=&quot;size16&quot;&gt;특히 Flutter와의 궁합이 좋아 실무 수준의 연동 과정을 익히기에 적합하다고 판단했다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이번 포스팅을 통해 공식문서를 보면서&amp;nbsp;&lt;b&gt;Flutter와 Supabase를 실제 프로젝트에 적용하면서 배우는 과정&lt;/b&gt;을 정리하고, 같은 주제를 공부하려는 개발자들에게 도움이 되는 자료를 만들어보고자 한다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;아래의 공식문서를 따라서 작성할 예정이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;공식문서&quot; href=&quot;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762502286933&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;Use Supabase with Flutter | Supabase Docs&quot; data-og-description=&quot;Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.&quot; data-og-host=&quot;supabase.com&quot; data-og-source-url=&quot;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&quot; data-og-url=&quot;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/czYDae/hyZMGna1Pm/mxc5kBDDyDk6JWNndkZgtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dT1mk9/hyZMCE5Cap/36dSExJpKQ1Zsu8qOvjMk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://supabase.com/docs/guides/getting-started/quickstarts/flutter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/czYDae/hyZMGna1Pm/mxc5kBDDyDk6JWNndkZgtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dT1mk9/hyZMCE5Cap/36dSExJpKQ1Zsu8qOvjMk1/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;Use Supabase with Flutter | Supabase Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;supabase.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;380&quot; data-start=&quot;272&quot; data-ke-size=&quot;size26&quot;&gt;Supabase&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://supabase.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Supabase 홈페이지&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762502243954&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;Supabase | The Postgres Development Platform.&quot; data-og-description=&quot;Build production-grade applications with a Postgres database, Authentication, instant APIs, Realtime, Functions, Storage and Vector embeddings. Start for free.&quot; data-og-host=&quot;supabase.com&quot; data-og-source-url=&quot;https://supabase.com/&quot; data-og-url=&quot;https://supabase.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5fRK4/hyZNc7IgfS/UiajGGSSEX2BId53wfkk80/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://supabase.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://supabase.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5fRK4/hyZNc7IgfS/UiajGGSSEX2BId53wfkk80/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;Supabase | The Postgres Development Platform.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Build production-grade applications with a Postgres database, Authentication, instant APIs, Realtime, Functions, Storage and Vector embeddings. Start for free.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;supabase.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식홈페이지에 접속해서 회원가입을 진행해 준 다음, 프로젝트를 만들어 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-07 오후 4.56.02.png&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;3546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l9CWI/dJMcac9glHd/AtxNMKaRsV3mwJpqWpJqwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l9CWI/dJMcac9glHd/AtxNMKaRsV3mwJpqWpJqwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9CWI/dJMcac9glHd/AtxNMKaRsV3mwJpqWpJqwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl9CWI%2FdJMcac9glHd%2FAtxNMKaRsV3mwJpqWpJqwK%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;3232&quot; height=&quot;3546&quot; data-filename=&quot;스크린샷 2025-11-07 오후 4.56.02.png&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;3546&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;공식문서를 따라 들어가보면 다음과 같이 테이블을 만들라고 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 탭에보면 &lt;b&gt;Table Editor&lt;/b&gt; 와 &lt;b&gt;SQL Editor&lt;/b&gt; 를 볼 수 있는데 UI적으로 만드냐, 코드로 만드냐의 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는는 SQL 코드를 제공해 주기 때문에 그대로 사용해보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1762502504029&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Create the table
create table instruments (
  id bigint primary key generated always as identity,
  name text not null
);
-- Insert some sample data into the table
insert into instruments (name)
values
  ('violin'),
  ('viola'),
  ('cello');

alter table instruments enable row level security;&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;다음 이미지와 같이 SQL 코드를 넣어주게 되면 테이블이 정상적으로 들어가 있는걸 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgAeBX/dJMcagDPgp1/pKUT204glsEi7YhH9jnXv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgAeBX/dJMcagDPgp1/pKUT204glsEi7YhH9jnXv0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;3546&quot; data-filename=&quot;스크린샷 2025-11-07 오후 4.59.02.png&quot; width=&quot;400&quot; height=&quot;439&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgAeBX/dJMcagDPgp1/pKUT204glsEi7YhH9jnXv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgAeBX%2FdJMcagDPgp1%2FpKUT204glsEi7YhH9jnXv0%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;3232&quot; height=&quot;3546&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yakvz/dJMcafEUSDb/em5Ep7mShqiWFrZ2k8sJ00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yakvz/dJMcafEUSDb/em5Ep7mShqiWFrZ2k8sJ00/img.png&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;3546&quot; data-is-animation=&quot;false&quot; data-filename=&quot;스크린샷 2025-11-07 오후 4.59.37.png&quot; width=&quot;300&quot; height=&quot;329&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yakvz/dJMcafEUSDb/em5Ep7mShqiWFrZ2k8sJ00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyakvz%2FdJMcafEUSDb%2Fem5Ep7mShqiWFrZ2k8sJ00%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;3232&quot; height=&quot;3546&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RLS 정책을 추가해서 테이블의 데이터를 공개적으로 읽을 수 있도록 설정또한 &lt;b&gt;SQL Editor&amp;nbsp;&lt;/b&gt;를 통해서 진행해 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1762502685419&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create policy &quot;public can read instruments&quot;
on public.instruments
for select to anon
using (true);&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-filename=&quot;스크린샷 2025-11-07 시간: 17.03.41.png&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XdMFv/dJMcacVI88o/ExAMzZrd09nyOeFchzgC6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XdMFv/dJMcacVI88o/ExAMzZrd09nyOeFchzgC6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XdMFv/dJMcacVI88o/ExAMzZrd09nyOeFchzgC6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXdMFv%2FdJMcacVI88o%2FExAMzZrd09nyOeFchzgC6k%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;1918&quot; height=&quot;1080&quot; data-filename=&quot;스크린샷 2025-11-07 시간: 17.03.41.png&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flutter 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flutter&lt;/b&gt; 에서 &lt;b&gt;Supabase&amp;nbsp;&lt;/b&gt;를 사용하려면 라이브러리의 설치가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/supabase_flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pub.dev/packages/supabase_flutter&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762502776354&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;supabase_flutter | Flutter package&quot; data-og-description=&quot;Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.&quot; data-og-host=&quot;pub.dev&quot; data-og-source-url=&quot;https://pub.dev/packages/supabase_flutter&quot; data-og-url=&quot;https://pub.dev/packages/supabase_flutter&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MPGMn/hyZMxqe2qt/Slt6eSXPBLshk5LJvVXEok/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bc3C61/hyZNuMrhXr/B0hHVMK5Fd6nVPZbHlfwD0/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/clmv6a/hyZM9b02yc/qVRfYQstLFkracYpJrxpdK/img.png?width=1756&amp;amp;height=1148&amp;amp;face=0_0_1756_1148&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/supabase_flutter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pub.dev/packages/supabase_flutter&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MPGMn/hyZMxqe2qt/Slt6eSXPBLshk5LJvVXEok/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bc3C61/hyZNuMrhXr/B0hHVMK5Fd6nVPZbHlfwD0/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/clmv6a/hyZM9b02yc/qVRfYQstLFkracYpJrxpdK/img.png?width=1756&amp;amp;height=1148&amp;amp;face=0_0_1756_1148');&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;supabase_flutter | Flutter package&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pub.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라이브러리를 설치해주도록 하자&lt;/p&gt;
&lt;pre id=&quot;code_1762502790401&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flutter pub add supabase_flutter&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;설치가 완료되었으면 이제 main 부분에 url, annoKey 를 사용해줘야 되는데&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 11.30.51.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BqG60/dJMcahpc8Fa/09tO7WyXfRBsRTXTfZqQX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BqG60/dJMcahpc8Fa/09tO7WyXfRBsRTXTfZqQX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqG60/dJMcahpc8Fa/09tO7WyXfRBsRTXTfZqQX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBqG60%2FdJMcahpc8Fa%2F09tO7WyXfRBsRTXTfZqQX0%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;1060&quot; height=&quot;186&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 11.30.51.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;186&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;main 부분에서 초기화를 진행 한 다음, URL과 키값을 불러와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762741999776&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await dotenv.load(fileName: &quot;.env&quot;);

  await Supabase.initialize(
    url: dotenv.env['SUPABASE_URL']!,
    anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
  );
  runApp(const MyApp());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 등록되었다면 다음과 같은 메시지가 출력된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 11.36.04.png&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SmFe6/dJMcajN591A/JUm5RScLPxtMDRkX9m0m9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SmFe6/dJMcajN591A/JUm5RScLPxtMDRkX9m0m9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SmFe6/dJMcajN591A/JUm5RScLPxtMDRkX9m0m9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSmFe6%2FdJMcajN591A%2FJUm5RScLPxtMDRkX9m0m9K%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;1160&quot; height=&quot;36&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 11.36.04.png&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;36&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드와 같이 FutureBuilder를 사용해서 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 가져올때 기존에 가져온 테이블 명을 명시해 주고 그 뒤에는 여러 옵션들이 이 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요하게 사용할건 select, stream 이 주요하게 사용 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762743652398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class HomePage extends StatefulWidget {
  const HomePage({super.key});
  @override
  State&amp;lt;HomePage&amp;gt; createState() =&amp;gt; _HomePageState();
}

class _HomePageState extends State&amp;lt;HomePage&amp;gt; {
  final _future = Supabase.instance.client.from('instruments').select();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder(
        future: _future,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const Center(child: CircularProgressIndicator());
          }
          final instruments = snapshot.data!;
          return ListView.builder(
            itemCount: instruments.length,
            itemBuilder: ((context, index) {
              final instrument = instruments[index];
              return ListTile(
                title: Text(instrument['name']),
              );
            }),
          );
        },
      ),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 실행시켜보면 다음과 같이 학기의 이름들을 가져올 수있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-10 오후 12.03.04.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;1890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bthius/dJMcac2vvfq/Gv3kkMGYu9lQM5y6D4KUik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bthius/dJMcac2vvfq/Gv3kkMGYu9lQM5y6D4KUik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bthius/dJMcac2vvfq/Gv3kkMGYu9lQM5y6D4KUik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbthius%2FdJMcac2vvfq%2FGv3kkMGYu9lQM5y6D4KUik%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;300&quot; height=&quot;569&quot; data-filename=&quot;스크린샷 2025-11-10 오후 12.03.04.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;1890&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 12.04.50.png&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;2154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mciq1/dJMcabP33lN/KPFyoJrk4185kg4PZb30M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mciq1/dJMcabP33lN/KPFyoJrk4185kg4PZb30M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mciq1/dJMcabP33lN/KPFyoJrk4185kg4PZb30M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmciq1%2FdJMcabP33lN%2FKPFyoJrk4185kg4PZb30M0%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;619&quot; data-filename=&quot;스크린샷 2025-11-10 시간: 12.04.50.png&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;2154&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 기초적인 &lt;b&gt;Supabase&amp;nbsp;&lt;/b&gt;연결하는 방법에 대해서 공식문서를 보고 한번 따라해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 &lt;b&gt;Supabase&lt;/b&gt; 의 기능을 제대로 쓰지는 않았지만, 앞으로 사이드 프로젝트를 진행하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가되는 내용에 대해서는 별도로 공부 후 작성할 예정이다.&lt;/p&gt;</description>
      <category>Mobile/Flutter</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/80</guid>
      <comments>https://sj-d.tistory.com/80#entry80comment</comments>
      <pubDate>Fri, 7 Nov 2025 16:54:13 +0900</pubDate>
    </item>
    <item>
      <title>React에서 이미지와 파일 관리 public vs. src/assets</title>
      <link>https://sj-d.tistory.com/79</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;react.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/er3p8g/btsQcquRqeu/KaHSo1aYefK3Krj3gS0nD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/er3p8g/btsQcquRqeu/KaHSo1aYefK3Krj3gS0nD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/er3p8g/btsQcquRqeu/KaHSo1aYefK3Krj3gS0nD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fer3p8g%2FbtsQcquRqeu%2FKaHSo1aYefK3Krj3gS0nD0%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;420&quot; data-filename=&quot;react.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React를 처음 공부하면서 프로젝트를 만들었을때 생긴 의문점이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite.svg 파일은 public에 들어가있고 react.svg 파일은 src/assets 에 위치해 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 해당 내용에 대해서 개념적으로 공부하면서 정리해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;public 폴더의 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public 에서는 정적 파일을 다루게 된다.&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;직접 URL 접근&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;App.tsx 파일에서 다음과 같이 불러오게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1756463457883&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 실제 코드에선 import 로 파일을 넣어준 다음 src={viteLogo} 를 사용
// 아래 코드에선 풀어 쓴 내용

&amp;lt;img src=&quot;/vite.svg&quot; className=&quot;logo&quot; alt=&quot;Vite logo&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 사용하게 되면 다음과 같이 직접적인 URL을 통해서 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로는 프로젝트 루트 / 를 기준으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 후에 /vite.svg 같은 형태로 접근이 가능하다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-29 오후 7.36.39.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;3478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDcdzK/btsQeT3Cq4a/1KB7dmRaSlKcbXKpqDnjrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDcdzK/btsQeT3Cq4a/1KB7dmRaSlKcbXKpqDnjrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDcdzK/btsQeT3Cq4a/1KB7dmRaSlKcbXKpqDnjrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDcdzK%2FbtsQeT3Cq4a%2F1KB7dmRaSlKcbXKpqDnjrK%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;2230&quot; height=&quot;3478&quot; data-filename=&quot;스크린샷 2025-08-29 오후 7.36.39.png&quot; data-origin-width=&quot;2230&quot; data-origin-height=&quot;3478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드 최적화와 무관&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 빌드를 해보면 dist 폴더 안에 public 폴더에서의 vite.svg파일은 다음과 같이 나타내게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-29 시간: 19.34.16.png&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r3Jfz/btsQdWNA635/CPDHIRxTkt6NP0Os50KAO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r3Jfz/btsQdWNA635/CPDHIRxTkt6NP0Os50KAO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r3Jfz/btsQdWNA635/CPDHIRxTkt6NP0Os50KAO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr3Jfz%2FbtsQdWNA635%2FCPDHIRxTkt6NP0Os50KAO0%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;598&quot; height=&quot;324&quot; data-filename=&quot;스크린샷 2025-08-29 시간: 19.34.16.png&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;324&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;이렇게 직접적으로 파일이 적용되는 이유는 public 폴더에 넣은 파일은 Webpack/Vite 에 의해 관리 되지 않고 원본 자체가 복사되기 때문이다.&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;압축/ 최적화/ 해싱 이 적용되지 않는다.&lt;/li&gt;
&lt;li&gt;파일이 수정되면 캐싱 문제를 피하기 위해 직접 파일명을 바꾸거나 쿼리스트링을 붙여야 한다.&lt;/li&gt;
&lt;li&gt;경로가 잘못되거나 파일이 없는 경우 컴파일 에러는 발생하지 않고 404 에러만 발생하게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필수 파일의 집합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;favicon, mainfest, robots.txt, 오픈그래프 이미지 등 애플리케이션 동작이나 SEO에 꼭 필요한 파일을 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드를 할때 코드에서 import 하지 않아도 되며 단순하게 경로만 명시하면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안에 주의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 봤듯이 URL을 통해서 접근이 가능하다.&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;민감한 문서, 인증서, API key 등은 절대 두지 않는다.&lt;/li&gt;
&lt;li&gt;외부에 노출되어도 상관없는 파일만 넣는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;src/assets 폴더의 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;import 방식의 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/assets 폴더의 파일은 JS/TS 코드에서 import 형식으로 불러오게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1756464136089&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import reactLogo from &quot;./assets/react.svg&quot;;

&amp;lt;img src={reactLogo} className=&quot;logo react&quot; alt=&quot;React logo&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 직접 URL 로 접근은 불가능하고 반드시 import를 거쳐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 도구(Webpack/VIte) 등 다음과 같은 최적화를 자동으로 적용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 해싱(content hash) -&amp;gt; 캐싱 문제 방지&lt;/li&gt;
&lt;li&gt;압축 및 용량 최적화&lt;/li&gt;
&lt;li&gt;중복 제거&lt;/li&gt;
&lt;li&gt;사용하지 않는 리소스 제거(Tree Shaking)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-29 시간: 19.59.21.png&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tHfIu/btsQc1uOb6S/LXk9knE9KraGrZp95Dnaa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tHfIu/btsQc1uOb6S/LXk9knE9KraGrZp95Dnaa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tHfIu/btsQc1uOb6S/LXk9knE9KraGrZp95Dnaa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtHfIu%2FbtsQc1uOb6S%2FLXk9knE9KraGrZp95Dnaa1%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;574&quot; height=&quot;294&quot; data-filename=&quot;스크린샷 2025-08-29 시간: 19.59.21.png&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 URL접근의 불가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public과는 다르게 직접적인 URL을 통해서 접근이 불가능하고, 코드 내에서 import를 거쳐야 사용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안 및 역할 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비공개 리소스, 외부에 노출되면 안되는 파일 등도 안전하게 관리 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조화된 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 맞춰 assets/images/, assets/icons/ 등 세분화 하여 관리하면 유지보수성이 높아지고 재사용성이 높아지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제 뭘 써야되는걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public은 외부 접근이 필요하고, 최적화가 불필요한 정적 파일(favicon, robots.txt, 큰 동영상, 3rd-paty 스크립트 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;assets는 앱 내부에서 참조하며 최적화가 필요한 리소스 (아이콘, UI 이미지, svg 컴포넌트 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 85px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;public&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;src/assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;접근 방식&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;직접 URL(/vite.svg)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;import 이후 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;빌드 최적화&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;⛔ 없음 (원본 복사)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;✅ 있음(해싱, 압축, 최적화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;캐싱&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;파일명 고정&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;content hash 자동 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;보안&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;누구나 URL을 통해 접근 가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;코드 내부에서만 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;권장 사용&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;favicon, mainfest, robots.txt&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;UI리소스, 아이콘, 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://whales.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://whales.tistory.com/95&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756465591261&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에서 이미지(images) 경로 설정 방법&quot; data-og-description=&quot;React에서 이미지를 사용해야 할 때 해당 이미지가 public폴더에 있는지 src폴더에 있는지, 또는 jsx파일에서 사용하는지 css에서 사용하는지에 따라 경로 설정하는 방법이 달라 처음 경로를 설정할 &quot; data-og-host=&quot;whales.tistory.com&quot; data-og-source-url=&quot;https://whales.tistory.com/95&quot; data-og-url=&quot;https://whales.tistory.com/95&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/u6ngt/hyZG5eVWF3/oMtq5Ai2Geey9T135W79M1/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/83EWH/hyZG4G5YQZ/7ybx3864ny2T0YsePF0Tw1/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/gMAbq/hyZDO6Zvfw/CWJBmFFuaM1psaoq01ybgk/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://whales.tistory.com/95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://whales.tistory.com/95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/u6ngt/hyZG5eVWF3/oMtq5Ai2Geey9T135W79M1/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/83EWH/hyZG4G5YQZ/7ybx3864ny2T0YsePF0Tw1/img.png?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/gMAbq/hyZDO6Zvfw/CWJBmFFuaM1psaoq01ybgk/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;React에서 이미지(images) 경로 설정 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React에서 이미지를 사용해야 할 때 해당 이미지가 public폴더에 있는지 src폴더에 있는지, 또는 jsx파일에서 사용하는지 css에서 사용하는지에 따라 경로 설정하는 방법이 달라 처음 경로를 설정할&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;whales.tistory.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://ttaerrim.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ttaerrim.tistory.com/62&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756465597853&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;정적 에셋은 왜 src에서 관리하는 것이 좋을까?&quot; data-og-description=&quot;이번 포스팅에서는 React + Vite 환경에서 정적 에셋을 public과 src 디렉토리에서 관리한 경험을 살펴보고 스스로 도출한 결론을 설명해 보려고 합니다. 6주 동안 진행된 morak 프로젝트에서는 정적 에&quot; data-og-host=&quot;ttaerrim.tistory.com&quot; data-og-source-url=&quot;https://ttaerrim.tistory.com/62&quot; data-og-url=&quot;https://ttaerrim.tistory.com/62&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/C004r/hyZG2oYQaj/WGNanOVDm4sjf5IlnoW3Ok/img.png?width=760&amp;amp;height=400&amp;amp;face=0_0_760_400,https://scrap.kakaocdn.net/dn/ezyNw8/hyZD0fhOv4/MDSWLCsKRpznvEypvKykSk/img.png?width=760&amp;amp;height=400&amp;amp;face=0_0_760_400,https://scrap.kakaocdn.net/dn/cMvix7/hyZCYPEEln/PnZoS1MfJSNB9bOvB1EFC0/img.png?width=1280&amp;amp;height=962&amp;amp;face=0_0_1280_962&quot;&gt;&lt;a href=&quot;https://ttaerrim.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ttaerrim.tistory.com/62&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/C004r/hyZG2oYQaj/WGNanOVDm4sjf5IlnoW3Ok/img.png?width=760&amp;amp;height=400&amp;amp;face=0_0_760_400,https://scrap.kakaocdn.net/dn/ezyNw8/hyZD0fhOv4/MDSWLCsKRpznvEypvKykSk/img.png?width=760&amp;amp;height=400&amp;amp;face=0_0_760_400,https://scrap.kakaocdn.net/dn/cMvix7/hyZCYPEEln/PnZoS1MfJSNB9bOvB1EFC0/img.png?width=1280&amp;amp;height=962&amp;amp;face=0_0_1280_962');&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;정적 에셋은 왜 src에서 관리하는 것이 좋을까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 React + Vite 환경에서 정적 에셋을 public과 src 디렉토리에서 관리한 경험을 살펴보고 스스로 도출한 결론을 설명해 보려고 합니다. 6주 동안 진행된 morak 프로젝트에서는 정적 에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ttaerrim.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>Web/React</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/79</guid>
      <comments>https://sj-d.tistory.com/79#entry79comment</comments>
      <pubDate>Fri, 29 Aug 2025 20:04:02 +0900</pubDate>
    </item>
    <item>
      <title>Vite 기반 React 프로젝트에서 Path Alias 설정하기</title>
      <link>https://sj-d.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;패스트 캠퍼스 강의를 보면서 Vite 기반 React 프로젝트에서 Path Alias 설정하는 부분에 대해서 조금 다른 부분이 있어 해당 부분에 대해서 작성해보려고 한다.&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;Path Alias 는 왜 사용하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Path Alias&lt;/b&gt; 는 프로젝트 내부에서 특정 경로를 별칭(alias) 를 설정하여 import 하게 해주는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 내가 import 해야되는 파일이 src/pages/main/lui/MainPage.tsx 파일이라고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 기존의 방법론으론 경우에 따라 다르지만 상대경로 또는 절대경로를 사용하게 된다 그러면 다음과 같이 가독성이 떨어지는 문제가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1751788339731&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { MainPage } from &quot;../pages/main-page/ui/MainPage&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;근데 Path Alias 를 사용하게 되면 다음과 같이 사용이 가능하다&lt;/p&gt;
&lt;pre id=&quot;code_1751788483835&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { MainPage } from &quot;@pages/main-page/ui/MainPage&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;물론 Path Alias 를 어떻게 설정하느냐에 따라서 다음과 같이 간결하게 만들수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1751788521795&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { MainPage } from &quot;@ui/MainPage&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vite에서 Path Alias 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Path Alias 를 하기위해서는 vite.config.ts, tsconfig.app.json 에서 수정을 진행해줘야한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vite.config.ts 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript 상에서 path 부분에서 타입 오류가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 다음 패키지를 설치해 주자&lt;/p&gt;
&lt;pre id=&quot;code_1751788859040&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# npm 을 사용하는 경우
npm install -D @types/node

# yarn 을 사용하는경우
yarn add -D @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;그 후 다음과 같이 vite.config.ts 파일을 수정해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우에는 fsd 를 적용하고 각 레이어들을 설정해 주 었다.&lt;/p&gt;
&lt;pre id=&quot;code_1751788790653&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import path from &quot;path&quot;;

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      &quot;@app&quot;: path.resolve(__dirname, &quot;./src/app&quot;),
      &quot;@pages&quot;: path.resolve(__dirname, &quot;./src/pages&quot;),
      &quot;@widgets&quot;: path.resolve(__dirname, &quot;./src/widgets&quot;),
      &quot;@features&quot;: path.resolve(__dirname, &quot;./src/features&quot;),
      &quot;@entities&quot;: path.resolve(__dirname, &quot;./src/entities&quot;),
      &quot;@shared&quot;: path.resolve(__dirname, &quot;./src/shared&quot;),
    },
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tsconfig.app.json 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 tsconfig.app.json 파일도 설정을 해주어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;baseUrl을 설정해주고 paths 에 각각의 레이어를 넣어주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1751788968696&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {

    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@app/*&quot;: [&quot;src/app/*&quot;],
      &quot;@pages/*&quot;: [&quot;src/pages/*&quot;],
      &quot;@widgets/*&quot;: [&quot;src/widgets/*&quot;],
      &quot;@features/*&quot;: [&quot;src/features/*&quot;],
      &quot;@entities/*&quot;: [&quot;src/entities/*&quot;],
      &quot;@shared/*&quot;: [&quot;src/shared/*&quot;]
    },
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Path Alias 사용하기&lt;/h2&gt;
&lt;pre id=&quot;code_1751789266770&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Path Alias 설정 전
import { MainPage } from &quot;../pages/main-page&quot;;
import { cn } from &quot;../../shared/lib/utils&quot;;

// Path Alias 설정 후
import { MainPage } from &quot;@pages/main-page&quot;
import { cn } from &quot;@shared/lib/utils&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;편하고 쉬운 방법(vite-tsconfig-paths)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 설정해도 되지만 Path Alias 에 추가를 할때마다 vite.config, tsconfig.app 두가지 파일을 수정해 줘야 하는 귀찮음이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 vite 패키지 중에 vite-tsconfig-paths 를 사용하면 tsconfig만 수정해도 vite 번들에도 동시에 설정되도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1751789492890&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# npm 사용시
npm i vite-tsconfig-paths

# yarn 사용시
yarn add vite-tsconfig-paths&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vite.config.ts 설정하기&lt;/h3&gt;
&lt;pre id=&quot;code_1751789592838&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import tsconfigPaths from 'vite-tsconfig-paths';

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss(), tsconfigPaths()],
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bluemiv.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bluemiv.tistory.com/114&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751789650023&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 프로젝트에서 Path Aliasing 설정하기&quot; data-og-description=&quot;React 프로젝트에서 Path Aliasing을 설정하면, 복잡하고 긴 상대 경로 대신 짧고 명확한 경로로 모듈을 import할 수 있습니다. 본 글에서는 Path Aliasing을 설정하는 방법에 대해 설명합니다.&amp;nbsp;Path Aliasing&quot; data-og-host=&quot;bluemiv.tistory.com&quot; data-og-source-url=&quot;https://bluemiv.tistory.com/114&quot; data-og-url=&quot;https://bluemiv.tistory.com/114&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LEmz0/hyZjtHkt1K/qwPnnyytnCfON8vR6kbK7k/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/bGclRa/hyZjt1Duib/UzavqKSh8DHpL2O6QWj6nk/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/cdD8cv/hyZjwKOW8B/W8Qh5cLKC7uG5qNCnAL3KK/img.png?width=2358&amp;amp;height=1320&amp;amp;face=0_0_2358_1320&quot;&gt;&lt;a href=&quot;https://bluemiv.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bluemiv.tistory.com/114&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LEmz0/hyZjtHkt1K/qwPnnyytnCfON8vR6kbK7k/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/bGclRa/hyZjt1Duib/UzavqKSh8DHpL2O6QWj6nk/img.png?width=800&amp;amp;height=447&amp;amp;face=0_0_800_447,https://scrap.kakaocdn.net/dn/cdD8cv/hyZjwKOW8B/W8Qh5cLKC7uG5qNCnAL3KK/img.png?width=2358&amp;amp;height=1320&amp;amp;face=0_0_2358_1320');&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 프로젝트에서 Path Aliasing 설정하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React 프로젝트에서 Path Aliasing을 설정하면, 복잡하고 긴 상대 경로 대신 짧고 명확한 경로로 모듈을 import할 수 있습니다. 본 글에서는 Path Aliasing을 설정하는 방법에 대해 설명합니다.&amp;nbsp;Path Aliasing&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bluemiv.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>Web/React</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/78</guid>
      <comments>https://sj-d.tistory.com/78#entry78comment</comments>
      <pubDate>Wed, 9 Jul 2025 18:14:14 +0900</pubDate>
    </item>
    <item>
      <title>React + Vite + TypeScript + Tailwind CSS(4.0 이상) 설정 방법</title>
      <link>https://sj-d.tistory.com/77</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;초보 React 개발자의 세팅 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 기존에 정리된 글은 많은데 Tawilwind CSS 가 4.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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. React + Vite + TypeScript 프로젝트 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1751620898791&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm create vite@latest react-templete&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 명령어를 치고 아래 이미지와 같이 따라하면 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-04 오후 6.22.05.png&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;1298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebl0W1/btsO5LsvAkj/nJYrNBjr7A5jvC07rxHOmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebl0W1/btsO5LsvAkj/nJYrNBjr7A5jvC07rxHOmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebl0W1/btsO5LsvAkj/nJYrNBjr7A5jvC07rxHOmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Febl0W1%2FbtsO5LsvAkj%2FnJYrNBjr7A5jvC07rxHOmk%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;2170&quot; height=&quot;1298&quot; data-filename=&quot;스크린샷 2025-07-04 오후 6.22.05.png&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;1298&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;React를 사용하므로 React 설정&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;스크린샷 2025-07-04 오후 6.22.33.png&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;1298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8r06W/btsO6pCvo5Y/CsGgEQKZkd6yK54KSJfV90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8r06W/btsO6pCvo5Y/CsGgEQKZkd6yK54KSJfV90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8r06W/btsO6pCvo5Y/CsGgEQKZkd6yK54KSJfV90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8r06W%2FbtsO6pCvo5Y%2FCsGgEQKZkd6yK54KSJfV90%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;2170&quot; height=&quot;1298&quot; data-filename=&quot;스크린샷 2025-07-04 오후 6.22.33.png&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;1298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 React + Vite + TypeScript 의 설정은 끝이다.&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;2. Tailwind CSS 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말했듯이 해당 내용은 4.0 이상 버전의 Tailwind CSS 설치에 대해서 다룬다.&lt;/p&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;pre id=&quot;code_1751621119600&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -D tailwindcss@latest @tailwindcss/vite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Tailwind CSS 적용을 위한 CSS파일 생성 및 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'src/index.css' 파일에서 해당 내용을 import 해주면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1751621222912&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@import &quot;tailwindcss&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;기존에는 @tailwind base; @tailwind components; @tailwind utilities 까지 사용해줘야 됐지만 해당 내용들이 사라지고 한줄로 import하면 된다.&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;4. Vite 설정 파일(vite.config.ts) 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드와 같이 Tailwind 플러그인을 추가 해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1751621351931&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import tailwindcss from &quot;@tailwindcss/vite&quot;;

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
});&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;여기서 주의해야 될 점은 플러그인 배열안에 tailwindcss() 를 반드시 추가해야 정상 작동한다.&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;5. Tailwind CSS 클래스 적용 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'src/main.tsx' 에서 방금 적용한 'index.css' 를 import한다&lt;/p&gt;
&lt;pre id=&quot;code_1751621465939&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import './index.css'&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;그다음 'App.tsx' 파일에서 Tailwind CSS 가 정상 작동되는지 확인하려면&lt;/p&gt;
&lt;pre id=&quot;code_1751621614158&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import &quot;./App.css&quot;;

function App() {
  return (
    &amp;lt;h1 className=&quot;text-4xl font-bold text-blue-600&quot;&amp;gt;Hello Tailwind CSS&amp;lt;/h1&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;다음과 같이 Tailwind CSS 문법을 적용해보면 된다.&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;6. 개발 서버 실행&lt;/h3&gt;
&lt;pre id=&quot;code_1751621694239&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run 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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-04 오후 6.35.17.png&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;3478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/svW4j/btsO4UcWloz/Enc86XB8RP9ohEcMCux2VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/svW4j/btsO4UcWloz/Enc86XB8RP9ohEcMCux2VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/svW4j/btsO4UcWloz/Enc86XB8RP9ohEcMCux2VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsvW4j%2FbtsO4UcWloz%2FEnc86XB8RP9ohEcMCux2VK%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;460&quot; height=&quot;646&quot; data-filename=&quot;스크린샷 2025-07-04 오후 6.35.17.png&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;3478&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;다음과 같은 결과를 얻을 수 있다 text-blue-600 의 컬러의 텍스트를 확인할 수 있다.&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;7. 추가 적용 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS 4.0 이상의 버전부터는 기존의 tailwind.config.js 와 postcss.config.js 파일이 필수가 아니라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 해당 명령어를 통해서 파일을 생성했지만&lt;/p&gt;
&lt;pre id=&quot;code_1751622051279&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npx tailwindcss init -p&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 사용할 필요없이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.css 파일 내부에서 직접 커스텀이 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1751622100991&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@import &quot;tailwindcss&quot;;

@theme {
  --color-primary: hsl(220, 90%, 56%);
  --color-secondary: hsl(340, 82%, 52%);
  --font-sans: &quot;Inter&quot;, sans-serif;
}

/* 필요에 따라 @layer로 커스텀 유틸리티 추가 가능 */
@layer utilities {
  .btn-primary {
    @apply bg-primary text-white font-bold py-2 px-4 rounded;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751783884037&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;React/personal/react-ts-tailwind-templete at main &amp;middot; Hsungjin/React&quot; data-og-description=&quot;Contribute to Hsungjin/React development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&quot; data-og-url=&quot;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkZfJG/hyZjsazc1k/SPhBXnq8QySKgVrKAnXqiK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfxPiW/hyZjpSr0Q3/0ImbHiQRs3rQygE36KG770/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Hsungjin/React/tree/main/personal/react-ts-tailwind-templete&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkZfJG/hyZjsazc1k/SPhBXnq8QySKgVrKAnXqiK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bfxPiW/hyZjpSr0Q3/0ImbHiQRs3rQygE36KG770/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;React/personal/react-ts-tailwind-templete at main &amp;middot; Hsungjin/React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to Hsungjin/React 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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751622157667&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;Installation of tailwind 4.0 + vite (react + javascript or typescript)&quot; data-og-description=&quot;in cmd, navigate to the folder where you want to start the project and enter the command&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&quot; data-og-url=&quot;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/D99Cm/hyZf8kcHwr/dfh2aFdZbxarpYuCJHIyw1/img.jpg?width=1200&amp;amp;height=511&amp;amp;face=0_0_1200_511,https://scrap.kakaocdn.net/dn/egt2D7/hyZfXXfZZv/fcCV1yYBU8fcH9hKSDrFik/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/cv0Ejn/hyZf5gHTL6/Ot0BKkFImgQzL1BQ7CSTE1/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742&quot;&gt;&lt;a href=&quot;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@npguapo/installation-of-tailwind-vite-react-javascript-or-typescript-ec1abdfa56b2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/D99Cm/hyZf8kcHwr/dfh2aFdZbxarpYuCJHIyw1/img.jpg?width=1200&amp;amp;height=511&amp;amp;face=0_0_1200_511,https://scrap.kakaocdn.net/dn/egt2D7/hyZfXXfZZv/fcCV1yYBU8fcH9hKSDrFik/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742,https://scrap.kakaocdn.net/dn/cv0Ejn/hyZf5gHTL6/Ot0BKkFImgQzL1BQ7CSTE1/img.png?width=989&amp;amp;height=742&amp;amp;face=0_0_989_742');&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;Installation of tailwind 4.0 + vite (react + javascript or typescript)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;in cmd, navigate to the folder where you want to start the project and enter the command&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/React</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/77</guid>
      <comments>https://sj-d.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 4 Jul 2025 18:44:52 +0900</pubDate>
    </item>
    <item>
      <title>[JS] async/await VS Promise VS Callback</title>
      <link>https://sj-d.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;강의를 들으면서 어떨때는 Callback 또 어떨때는 Promise 또 어떨때는 async/await을 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter 를 사용하면서는 Future, async, await, Stream 만 사용해왔었는데 JavaScript 에서의 비동기 처리를 하는 방법들이 각각 어떤 상황에서 사용해야 효율적인 프로그래밍이 될지에 대해서 알아볼 필요가 있다고 생각해서 이에 대해 정리해보려고 한다.&lt;/p&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;&lt;a href=&quot;https://sj-d.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.01.31 - [ Flutter] - Flutter 에서의 비동기 프로그래밍 Future, async/await&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747047159676&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;Flutter 에서의 비동기 프로그래밍 Future, async/await&quot; data-og-description=&quot;맨 처음 iOS를 공부하면서 가장 어렵게 느껴졌던 개념이 비동기 프로그래밍이었다.내용만 듣고 이걸 왜 쓰는 거지?라는 의문이 들어서 대충 흘려보냈다가 프로젝트에 들어가면서 그 중요성에 대&quot; data-og-host=&quot;sj-d.tistory.com&quot; data-og-source-url=&quot;https://sj-d.tistory.com/66&quot; data-og-url=&quot;https://sj-d.tistory.com/66&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/g8QTS/hyYRwlKwSc/5sNyJ1uCezRSe9JmQq0oe0/img.png?width=800&amp;amp;height=510&amp;amp;face=0_0_800_510,https://scrap.kakaocdn.net/dn/bJQ5YH/hyYVeqb3lu/EtiLfzcyee56feLuxbu33k/img.png?width=800&amp;amp;height=510&amp;amp;face=0_0_800_510,https://scrap.kakaocdn.net/dn/pnsj3/hyYRyw3Qf5/zMLTPYfK4d9H8VJ3qbBZOk/img.png?width=1280&amp;amp;height=817&amp;amp;face=0_0_1280_817&quot;&gt;&lt;a href=&quot;https://sj-d.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sj-d.tistory.com/66&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/g8QTS/hyYRwlKwSc/5sNyJ1uCezRSe9JmQq0oe0/img.png?width=800&amp;amp;height=510&amp;amp;face=0_0_800_510,https://scrap.kakaocdn.net/dn/bJQ5YH/hyYVeqb3lu/EtiLfzcyee56feLuxbu33k/img.png?width=800&amp;amp;height=510&amp;amp;face=0_0_800_510,https://scrap.kakaocdn.net/dn/pnsj3/hyYRyw3Qf5/zMLTPYfK4d9H8VJ3qbBZOk/img.png?width=1280&amp;amp;height=817&amp;amp;face=0_0_1280_817');&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;Flutter 에서의 비동기 프로그래밍 Future, async/await&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;맨 처음 iOS를 공부하면서 가장 어렵게 느껴졌던 개념이 비동기 프로그래밍이었다.내용만 듣고 이걸 왜 쓰는 거지?라는 의문이 들어서 대충 흘려보냈다가 프로젝트에 들어가면서 그 중요성에 대&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sj-d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자바스크립트의 비동기 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 비동기 처리는 웹 개발에서의 필수적인 요소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 데이터를 가져오거나, 파일을 읽거나, 타이머를 설정하는 등의 작업은 모두 비동기적으로 처리하게 된다. 이렇게 비동기 작업을 효율적으로 다루기 위해서 자바스크립트에서는 세가지 주요 방식 (Callback, Promise, async/await)을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 자바스크립트는 싱글 스레드 언어로 한번에 하나의 작업만 수행할 수 있다. 하지만 웹 애플리케이션은 여러 작업을 동시에 처리해야 하는 경우가 많다. 예를들어 사용자의 입력을 받으면서 동시에 서버에서 데이터를 가져와야 할 때 비동기 처리가 필요하게 된다. 이러한 비동기 처리 작업을 효율적으로 관리하기 위해 Callback, Promise, async/awit이 등장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Callback(콜백 함수)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수는 다른 함수에 인자로 전달되어 특정 작업이 완료 된 후에 호출되는 함수이다. 결과적으로 &quot;너는 나중에 호출할게&quot; 라고 의미할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747047604374&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function greet(name, callback) {
  console.log(`Hello, ${name}!`);
  callback();
}

function sayGoodbye() {
  console.log(&quot;Goodbye!&quot;);
}

greet(&quot;황성진&quot;, sayGoodbye);

// 출력
// Hello, 황성진!
// Goodbye!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비동기 처리 예제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수는 비동지 작업에서 특히 유용하다&lt;/p&gt;
&lt;pre id=&quot;code_1747047678718&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function fetchData(callback) {
  setTimeout(function() {
    const data = { id: 1, message: &quot;Hello, world!&quot; };
    callback(data);
  }, 2000);
}

function displayData(data) {
  console.log(&quot;데이터 받기 완료 :&quot;, data);
}

fetchData(displayData);
// 2초 후 출력: 데이터 받기 완료 : { id: 1, message: 'Hello, world!' }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단하고 이해하기가 쉽다&lt;/li&gt;
&lt;li&gt;모든 브라우저에서 지원된다&lt;/li&gt;
&lt;li&gt;특정 이벤트가 발생했을때 원하는 기능을 실행항 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무한 콜백 지옥의 발생 가능성&lt;/li&gt;
&lt;li&gt;에러처리가 복잡하다&lt;/li&gt;
&lt;li&gt;가독성이 떨어진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Callback Hell(콜백 지옥)&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRvrz1/btsNTCYC3EF/kx1AC3ksA6proKbLhIn0BK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRvrz1/btsNTCYC3EF/kx1AC3ksA6proKbLhIn0BK/img.png&quot; data-alt=&quot;무한 콜백지옥&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRvrz1/btsNTCYC3EF/kx1AC3ksA6proKbLhIn0BK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRvrz1%2FbtsNTCYC3EF%2Fkx1AC3ksA6proKbLhIn0BK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;791&quot; height=&quot;465&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;465&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Promise(프로미스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise는 비동기 작업의 최종 완료 혹은 실패와 그 결과값을 나타내는 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise가 생설될때 꼭 결과를 알 수 없는 작업의 최종 결과값을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise는 세가지 상태중 하나를 가진다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대기(Pending) : 초기 상태, 이행되거나 거부되지 않은 상태&lt;/li&gt;
&lt;li&gt;이행(Fulfilled) : 연산이 성공적으로 완료됨&lt;/li&gt;
&lt;li&gt;거부(Rejected) : 연산이 실패함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747048054696&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let myPromise = new Promise(function(resolve, reject) {
  // 비동기 작업 수행
  setTimeout(() =&amp;gt; {
    const success = true;
    if (success) {
      resolve(&quot;성공했습니다!&quot;); // 성공 시 resolve 호출
    } else {
      reject(&quot;실패했습니다.&quot;); // 실패 시 reject 호출
    }
  }, 2000);
});

// 프로미스 소비
myPromise
  .then(result =&amp;gt; console.log(result))   // 성공 시 실행
  .catch(error =&amp;gt; console.log(error));  // 실패 시 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Promise Chaining(프로미스 체이닝)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise의 큰 장점 중 하나는 여러 비동기 작업을 연속적으로 처리할 수 있는 체이닝 기능이다.&lt;/p&gt;
&lt;pre id=&quot;code_1747048139160&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function stepOne(value) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(value);
      resolve();
    }, 3000);
  });
}

function stepTwo(value) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(value);
      resolve();
    }, 2000);
  });
}

function stepThree(value) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(value);
      resolve();
    }, 1000);
  });
}

// 프로미스 체이닝으로 순차적 실행
stepOne(1)
  .then(() =&amp;gt; stepTwo(2))
  .then(() =&amp;gt; stepThree(3))
  .then(() =&amp;gt; {
    console.log(&quot;모든 단계 완료&quot;);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;콜백 지옥을 해결하여 코드의 가독성을 향상 시킨다&lt;/li&gt;
&lt;li&gt;체이닝을 통해 연속적인 비동기 처리가 용이함&lt;/li&gt;
&lt;li&gt;catch 메서드를 통해서 오류에 대한 처리가 쉬움&lt;/li&gt;
&lt;li&gt;여러 비동기 작업을 병렬로 처리 가능 (Promise.all, Promise.race 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES6&amp;nbsp; 이전 브라우저에서는 polyfill 이 필요&lt;/li&gt;
&lt;li&gt;코드가 여전히 복잡할 수 있음&lt;/li&gt;
&lt;li&gt;조건부 로직 처리가 복잡함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Async/Await&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 친숙한 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Async/Await은 ES2017(ES8)에서 도입된 문법으로, Promise를 더욱 쉽게 사용할 수 있게 해주는 특별한 문법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Async/Awit을 사용하면 비동기 코드를 마치 동기 코드처럼 작성 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;async 함수는 항상 Promise를 반환한다&lt;/li&gt;
&lt;li&gt;await 은 Promise 가 처리될때 까지 함수 실행을 일시 중단한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747048405892&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function fetchUserData() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
    const userData = await response.json();
    console.log(userData);
    return userData;
  } catch (error) {
    console.error('유저정보 받아오기 오류', error);
    throw error;
  }
}

// 함수 호출
fetchUserData()
  .then(data =&amp;gt; console.log('유저정보:', data))
  .catch(error =&amp;gt; console.error('에러발생:', error));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prmoise 와 Async/Await 의 비교&lt;/h4&gt;
&lt;pre id=&quot;code_1747048462967&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Promise
fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response =&amp;gt; response.json())
  .then(json =&amp;gt; console.log(json))
  .catch(error =&amp;gt; console.log(error));

// Async/Await
async function fetchTodo() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
    const json = await response.json();
    console.log(json);
  } catch (error) {
    console.log(error);
  }
}

fetchTodo();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드의 가독성 향상&lt;/li&gt;
&lt;li&gt;직관적인 오류 처리(try-catch 구문 사용)&lt;/li&gt;
&lt;li&gt;디버깅이 용이함&lt;/li&gt;
&lt;li&gt;조건부 로직 처리가 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES2017 이전의 브라우저에서는 트랜스파일러 필요&lt;/li&gt;
&lt;li&gt;병렬 처리 시 추가 작업 필요&lt;/li&gt;
&lt;li&gt;await은 반드시 async 함수 구문내에서만 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 비동기 처리는 Callback, Promise, Async/Await 세 가지 방식으로 구현할 수 있다.&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;Callback&lt;/b&gt;은 가장 기본적인 방식이지만 코드가 복잡해질 수록 가독성이 떨어지는 콜백 지옥 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Promise&lt;/b&gt;는 콜백 지옥 문제를 해결하고 체이닝을 통해 연속적인 비동기 작업이 가능하지만 여전히 코드가 복잡해 질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Async/Await&lt;/b&gt;은 Promise 기반으로 동작하면서 동기 코드와 유사한 구조로 작성할 수 있어 가독성이 좋고 오류 처리가 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 현재 자바스크립트 개발에서는 Async/Await, Promise 가 주로 사용되며, 특히 Async/Await은 가독성과 오류 처리의 용이성으로 인해 많이선호된다(나도 제일 편해서 좋아한다) 하지만 모든 상황에 맞는 &lt;b&gt;최고의 방식&amp;nbsp;&lt;/b&gt;은 존재할 수 없듯이 여러 방법론에 따라 다방면에서 고민하면서 항상 차선책을 찾으려 노력해야 된다고 생각한다.&lt;/p&gt;</description>
      <category>Web/&amp;zwnj;JavaScript || TypeScript</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/76</guid>
      <comments>https://sj-d.tistory.com/76#entry76comment</comments>
      <pubDate>Mon, 26 May 2025 21:20:39 +0900</pubDate>
    </item>
    <item>
      <title>이것은 UI 오류인가요? 아닙니다!(Feat. 서브픽셀 렌더링)</title>
      <link>https://sj-d.tistory.com/75</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 사용하는 업무툴중에 네이버 웍스 가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문득 웍스의 UI 를 바라보았는데&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 12.46.44.png&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DRRK0/btsNUHExaqO/8zgvTbkj7FHjfkz3IzpOUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DRRK0/btsNUHExaqO/8zgvTbkj7FHjfkz3IzpOUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DRRK0/btsNUHExaqO/8zgvTbkj7FHjfkz3IzpOUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDRRK0%2FbtsNUHExaqO%2F8zgvTbkj7FHjfkz3IzpOUk%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;291&quot; height=&quot;43&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 12.46.44.png&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;43&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;1:1 에서 왼쪽의 1과 오른쪽의 1이 다르다는 느낌을 받았다.(지금 보니 티스토리 에디터도 동일하게 미세하게 다르다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 캡쳐를 해서 확대를 해보았다&lt;/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;스크린샷 2025-05-12 시간: 12.43.53.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F2cAN/btsNTJpvx02/nPop4ggqFDmMEqq4042EY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F2cAN/btsNTJpvx02/nPop4ggqFDmMEqq4042EY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F2cAN/btsNTJpvx02/nPop4ggqFDmMEqq4042EY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF2cAN%2FbtsNTJpvx02%2FnPop4ggqFDmMEqq4042EY1%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;272&quot; height=&quot;146&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 12.43.53.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 12.47.02.png&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xbVwZ/btsNVjJV5hG/FPkMTp1hwMXukbmO7gaIh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xbVwZ/btsNVjJV5hG/FPkMTp1hwMXukbmO7gaIh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xbVwZ/btsNVjJV5hG/FPkMTp1hwMXukbmO7gaIh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxbVwZ%2FbtsNVjJV5hG%2FFPkMTp1hwMXukbmO7gaIh0%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;299&quot; height=&quot;169&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 12.47.02.png&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;169&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;나는 당연히 이건 UI 적으로 오류가 발생했다고 생각했다.&lt;/p&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-filename=&quot;스크린샷 2025-05-12 시간: 18.38.14.png&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/da8MAw/btsNS7xDypQ/onjHyF5fT0DgerZRnSGjt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/da8MAw/btsNS7xDypQ/onjHyF5fT0DgerZRnSGjt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/da8MAw/btsNS7xDypQ/onjHyF5fT0DgerZRnSGjt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fda8MAw%2FbtsNS7xDypQ%2FonjHyF5fT0DgerZRnSGjt1%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;536&quot; height=&quot;484&quot; data-filename=&quot;스크린샷 2025-05-12 시간: 18.38.14.png&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;484&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;그러면 이런 현상은 왜 발생하는걸까? 라는 의문이 들었다.&lt;/p&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;/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;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlCEdH/btsNUKVwt7V/oEVeerqnBDFVpnK6tzNDwk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlCEdH/btsNUKVwt7V/oEVeerqnBDFVpnK6tzNDwk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlCEdH/btsNUKVwt7V/oEVeerqnBDFVpnK6tzNDwk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dlCEdH/btsNUKVwt7V/oEVeerqnBDFVpnK6tzNDwk/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;800&quot; height=&quot;558&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;558&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;LCD 화면에서 하나의 픽셀이 실제로는 세개의 서브픽셀(빨강, 초록, 파랑)로 구성되어 있다. 서브픽셀 렌더링은 이러한 구조를 활용하여 실질적으로 해상도를 높이는 기술이다.&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;128&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj3emE/btsNUvdAdTu/ucZhJgWk9Z3InVOz2O6kwk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj3emE/btsNUvdAdTu/ucZhJgWk9Z3InVOz2O6kwk/img.gif&quot; data-alt=&quot;https://en.wikipedia.org/wiki/Subpixel_rendering&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj3emE/btsNUvdAdTu/ucZhJgWk9Z3InVOz2O6kwk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bj3emE/btsNUvdAdTu/ucZhJgWk9Z3InVOz2O6kwk/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;128&quot; height=&quot;128&quot; data-origin-width=&quot;128&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://en.wikipedia.org/wiki/Subpixel_rendering&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 LCD 화면의 각 픽셀은 RGB(빨강, 초록, 파랑) 세 개의 서브 픽셀로 이루어진다.&lt;/li&gt;
&lt;li&gt;서브픽셀들은 서로 나란히 배치되어 있으며 대부분의 RGB 순서로 정렬되어 있다(일부는 BGR 도 있다고 한다)&lt;/li&gt;
&lt;li&gt;인간의 눈은 이 세가지 색상이 섞여 하나의 색으로 보이도록 통합한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브픽셀 렌더링은 이 개별 서브픽셀을 활용하여 더 정밀한 텍스트 렌더링 기능을 하게 한다. 일반적인 픽셀 단위 렌더링 보다 최대 3배 더 정밀하게 수평해상도를 제공할 수 있다고 하낟.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 왜 생겨?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;텍스트의 분수 픽셀 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 에서 보이듯 차이는 분수 픽셀(fractional pixel)의 처리 방식 때문일 수도 있다고 한다&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. 어떤 브라우저는 33.5px을 33px로 내림을 하는 경우도 있고&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. 일부 브라우저는 올바르게 반올림 하여 34px로 처리하는 경우도 있다.&lt;/p&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;분스 픽셀을 사용할 때 Edge와 Firefox는 너비를 가장 가까운 전체 픽셀로 반올림 하는 반면, 다른 브라우저들은 정수 값만 사용하는 경향이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안티앨리어싱과 텍스트 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 가설은 안티앨리어싱 적용 여부와 방식의 차이 일 수 도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;124&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRS1BI/btsNUJoNbkq/3ml2OH8PH0nm94OxQm7jU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRS1BI/btsNUJoNbkq/3ml2OH8PH0nm94OxQm7jU1/img.png&quot; data-alt=&quot;https://ko.wikipedia.org/wiki/%EC%9C%84%EC%8B%A0%ED%98%B8_%EC%A0%9C%EA%B1%B0&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRS1BI/btsNUJoNbkq/3ml2OH8PH0nm94OxQm7jU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRS1BI%2FbtsNUJoNbkq%2F3ml2OH8PH0nm94OxQm7jU1%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;124&quot; height=&quot;127&quot; data-origin-width=&quot;124&quot; data-origin-height=&quot;127&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://ko.wikipedia.org/wiki/%EC%9C%84%EC%8B%A0%ED%98%B8_%EC%A0%9C%EA%B1%B0&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;안티앨리어싱 기법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안티앨리어싱은 텍스트나 그래픽의 가장자리에 나타난 계단 현상을 감소시키는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브픽셀 안티엘리어싱&lt;/b&gt; - 서브픽셀 단위로 정밀하게 처리하여 텍스트를 더 선명하게 표현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 안티앨리어싱&lt;/b&gt; - 픽셀 단위로 가장자리를 부드럽게 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS에서는 -webkit-font-smoothing: antialiaseed 와 같은 속성을 사용해서 폰트 렌더링 방식을 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 이유 (가설) - 사실 나는 다 틀림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 다른 렌더링 엔진의 사용 : 각 텍스트가 다른 렌더링 엔진을 사용하거나 스프트웨어에서 생성 되었을 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디지털 디스플레이에서 텍스트 렌더링은 서브픽셀 렌더링, 안티앨리어싱, 폰트 힝팅, 분수 픽셀 처리 등 다양한 방법론이 있고 어떻게 적용하는지에 따라서 유저에게 보여지는 부분이 미세하게 변동되는 경우도 있는 상황을 알게되었다.&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>CS</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/75</guid>
      <comments>https://sj-d.tistory.com/75#entry75comment</comments>
      <pubDate>Mon, 12 May 2025 19:11:52 +0900</pubDate>
    </item>
    <item>
      <title>Flutter 클린아키텍처를 공부하면서 생긴 의문점 ~ ing</title>
      <link>https://sj-d.tistory.com/74</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iry3D/btsM3X9mRpD/bkaB0iOWCq92pB6YKCOG80/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iry3D/btsM3X9mRpD/bkaB0iOWCq92pB6YKCOG80/img.webp&quot; data-alt=&quot;클린아키텍처 이해했어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iry3D/btsM3X9mRpD/bkaB0iOWCq92pB6YKCOG80/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIry3D%2FbtsM3X9mRpD%2FbkaB0iOWCq92pB6YKCOG80%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;182&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;182&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;
&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;
&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;1. DTO ~ Mapper ~ Model 이 효율적인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 지금까지의 내가 해오던 방식은 Model만 정의해서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 사용한 방법은 아래와 같이 옵셔널 하게 미리 정의해 놓고 필요한 값만 꺼내서 쓰는 방식이었다.&lt;/p&gt;
&lt;pre id=&quot;code_1743437212864&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class User {
	String? id;
    String? name;
    String? email;
    String? phone;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 클린아키텍처를 공부하면서 지금까지의 개념과는 다르게 작용한 부분이라고 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 가 서버에서 받아온 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mapper 를 통해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model 이 된다 라는 개념이였다.&lt;/p&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;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;클린 아키텍처에서 DTO와 Model을 나누는 접근법에 대한 질문이시군요.&lt;br /&gt;&lt;br /&gt;DTO와 Model을 분리하는 대신 Model에서 optional 값으로 처리하는 것이 더 편할 수 있다는 생각은 이해가 됩니다. 실제로 많은 개발자들이 이런 방식도 고려합니다. 각 접근법의 장단점을 살펴보겠습니다:&lt;br /&gt;&lt;br /&gt;DTO와 Model을 분리했을 때의 장점:&lt;br /&gt;&lt;br /&gt;관심사 분리: 네트워크 데이터 구조(DTO)와 앱 내부 로직 데이터 구조(Model)가 명확히 분리됩니다. API 변경 대응: 서버 API가 변경되더라도 내부 Model은 변경할 필요가 없고 Mapper만 수정하면 됩니다. 테스트 용이성: 비즈니스 로직을 테스트할 때 서버 응답 구조와 독립적으로 테스트할 수 있습니다. 도메인 중심 설계: 앱의 비즈니스 요구사항에 맞는 Model 설계가 가능합니다.&lt;br /&gt;&lt;br /&gt;단일 Model에 optional 값으로 처리할 때 장점:&lt;br /&gt;&lt;br /&gt;코드 간결성: 추가적인 매핑 코드가 필요 없어 코드량이 줄어듭니다. 개발 속도: 빠른 개발이 가능하고 DTO-Model 간 매핑 오류가 발생하지 않습니다. 유지보수 단순화: 변경 시 한 곳만 수정하면 되어 동기화 문제가 없습니다.&lt;br /&gt;&lt;br /&gt;실제로는 프로젝트의 규모와 요구사항에 따라 적절한 접근법을 선택하는 것이 중요합니다:&lt;br /&gt;&lt;br /&gt;작은 프로젝트나 빠른 프로토타이핑에서는 단일 Model에 optional 값을 사용하는 것이 개발 속도와 간결성 측면에서 더 효율적일 수 있습니다. 대규모 프로젝트나 장기 유지보수가 필요한 앱에서는 DTO-Model 분리가 확장성과 유지보수 측면에서 더 유리할 수 있습니다.&lt;br /&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 data-ke-size=&quot;size16&quot;&gt;일단은 정석의 방식대로 구현하고자 했을 때는 DTO, Model 을 분리해서 사용하는 것이 맞다고 생각된다.&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>Mobile/Flutter</category>
      <category>Flutter</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/74</guid>
      <comments>https://sj-d.tistory.com/74#entry74comment</comments>
      <pubDate>Fri, 18 Apr 2025 09:34:25 +0900</pubDate>
    </item>
    <item>
      <title>Flutter Debounce 와 Throttle</title>
      <link>https://sj-d.tistory.com/72</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blOEZA/btsMPv0fLzA/vWfSPQ72El4heACOKYZGZK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blOEZA/btsMPv0fLzA/vWfSPQ72El4heACOKYZGZK/img.gif&quot; data-alt=&quot;오류를 고치는 나&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blOEZA/btsMPv0fLzA/vWfSPQ72El4heACOKYZGZK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/blOEZA/btsMPv0fLzA/vWfSPQ72El4heACOKYZGZK/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;420&quot; height=&quot;236&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오류를 고치는 나&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주제선정&lt;/h2&gt;
&lt;p 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-filename=&quot;스크린샷 2025-03-19 시간: 21.06.28.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5wuhO/btsMP2QVnqo/LzFjSl4WVQuk79kxh8fVRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5wuhO/btsMP2QVnqo/LzFjSl4WVQuk79kxh8fVRK/img.png&quot; data-alt=&quot;당시 요청사항&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5wuhO/btsMP2QVnqo/LzFjSl4WVQuk79kxh8fVRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5wuhO%2FbtsMP2QVnqo%2FLzFjSl4WVQuk79kxh8fVRK%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;676&quot; height=&quot;23&quot; data-filename=&quot;스크린샷 2025-03-19 시간: 21.06.28.png&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;23&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;특히 내 도메인이 금융쪽이였으면 엄청난 큰 문제가 발생했을 것이다.&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;처음에 내가 생각했던 방법은 bool 값을 넣어서 try catch final 로 진입자체를 막는방법 이였지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 검색하다보니 Debounce 와 Throttle 이라는 라이브러리가 존재하는것 알았고 해당 라이브러리를 사용해서 고쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 해당 개념에 대해서 조금 더 자세히 공부하고, 라이브러리를 사용하는 것과 직접 구현하는 방법, 원래 생각했던 try catch finally의 사용 예제를 공유하고 비교해보려고 한다.&lt;/p&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://pub.dev/packages/easy_debounce&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pub.dev/packages/easy_debounce&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742548060112&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;easy_debounce | Dart package&quot; data-og-description=&quot;An extremely easy-to-use method call debouncer package for Dart/Flutter.&quot; data-og-host=&quot;pub.dev&quot; data-og-source-url=&quot;https://pub.dev/packages/easy_debounce&quot; data-og-url=&quot;https://pub.dev/packages/easy_debounce&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dlcTJl/hyYvof93Ao/8lOzLvX1MraLaNeMWwxLuK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bV496o/hyYup00wIy/K1PSCdXmL0ivVB04AML761/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/easy_debounce&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pub.dev/packages/easy_debounce&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dlcTJl/hyYvof93Ao/8lOzLvX1MraLaNeMWwxLuK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/bV496o/hyYup00wIy/K1PSCdXmL0ivVB04AML761/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; data-ke-size=&quot;size16&quot;&gt;easy_debounce | Dart package&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An extremely easy-to-use method call debouncer package for Dart/Flutter.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pub.dev&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;우선 해당 에러를 가정하기 위해 간단하게 Hello Flutter 를 받아오는 API 를 만들어서 간단하게 dio 를 통해서 get 방식으로 가져와서 사용했다. &lt;s&gt;별거 아니긴 하지만&amp;nbsp;이래서 풀스택이 좋은건가 싶다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Debounce&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Debounce 의 개념&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUDemZ/btsMTIjYI4J/kRnOuRoTumyTJamwOkNHtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUDemZ/btsMTIjYI4J/kRnOuRoTumyTJamwOkNHtk/img.png&quot; data-alt=&quot;출처 : https://llu.is/throttle-and-debounce-visualized/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUDemZ/btsMTIjYI4J/kRnOuRoTumyTJamwOkNHtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUDemZ%2FbtsMTIjYI4J%2FkRnOuRoTumyTJamwOkNHtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;387&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://llu.is/throttle-and-debounce-visualized/&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;Debounce는 반복적인 이벤트의 실행을 지정된 이후 시간이 지난 후 이벤트가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debounce 시간을 400ms 로 지정했다고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음 버튼을 정상적으로 한번 눌렀을 경우 400ms 이후 함수가 호출된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 빠르게 버튼을 10번 눌렀다고 가정해보자 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 버튼이 입력되고 다음 버튼의 입력을 지켜보고있다가 다음 입력이 일어나면 다시 400ms가 초기화 되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력이 일어나지 않는 400ms 가 되면 함수를 한번 실행시켜준다.&lt;/p&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 유저가 버튼을 누를때마다 서버로 통신을하고 DB에 반영된다고 하면 상당히 비효율적일것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기위해 등장했다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또다른 예시로 검색 기능을 호출한다고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 글자를 하나하나 입력할때마다 API 를 호출하면 &quot;제주&quot; 이 단어를 검색하는동안 &quot;ㅈ&quot; 에서 한번 &quot;제&quot; 에서 한번 이런식으로 호출이 증가하게 된다. 이것 방지하기위해서 디바운스를 적용한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debounce 는 주로 검색창 입력, 유저 타이핑, 상품 구매 버튼 등에서 사용된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debounce가 적용된 원을 직접 움직일 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rxmarbles.com/#debounce&quot;&gt;https://rxmarbles.com/#debounce&lt;/a&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;Debounce 사용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 라이브러리 사용이라 별로 어려운건 없다&lt;/p&gt;
&lt;pre id=&quot;code_1742553672332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  void _incrementDebounceCounter() {
    EasyDebounce.debounce('debounce', const Duration(milliseconds: 400), () {
      setState(() {
        _debouncecounter++;
      });
      callAPI('debounce');
    });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Throttle&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Throttle 의 개념&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1201&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRn92/btsMTjdKALg/Nehl6PMo52mVCWkqKiPNpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRn92/btsMTjdKALg/Nehl6PMo52mVCWkqKiPNpK/img.png&quot; data-alt=&quot;출처 : https://llu.is/throttle-and-debounce-visualized/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRn92/btsMTjdKALg/Nehl6PMo52mVCWkqKiPNpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRn92%2FbtsMTjdKALg%2FNehl6PMo52mVCWkqKiPNpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;387&quot; data-origin-width=&quot;1201&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://llu.is/throttle-and-debounce-visualized/&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;Throttle은 이벤트가 연속적으로 발생하면 일정한 시간동안 일어나게 되고 그 사이에 발생하는 다른 이벤트들은 무시하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Debounce 와 다른점은 Throttle은 연속된 이벤트가 발생하더라도 타이머를 초기화하지 않는 것이다&lt;br /&gt;&lt;br /&gt;만약 임계 시간을 400ms로 설정하고 390ms 에 버튼을 누르면 Debounce 와 같은경우는 타이머가 다시 400ms로 초기화 되겠지만 Throttle 은 타이머가 초기화 되지않고 발생한 이벤트에 대해서 무시하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 스크롤 이벤트에 400ms의 Throttle 을 적용했다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Throttle을 통해서 스크롤의 끝에 도달하면 한번만 호출하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Debounce 가 해당 기능에 적용되었다고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번만 이벤트를 수행하게되면 큰 문제는 없이 작동된다 하지만 여러번 스크롤 이벤트를 호출한다면? 계속 400ms 의 타이머가 초기화되어 결국 함수가 호출되지 않을것이다.&lt;/p&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;Throttle 이 적용된 원을 직접 움직일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rxmarbles.com/#throttle&quot;&gt;https://rxmarbles.com/#throttle&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Throttle 사용 예제&lt;/h3&gt;
&lt;pre id=&quot;code_1742553683604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  void _incrementThrottleCounter() {
    EasyThrottle.throttle('throttle', const Duration(milliseconds: 400), () {
      setState(() {
        _throttlecounter++;
      });
      callAPI('throttle');
    });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;호출 결과 비교&lt;/h2&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 결과를 놓고보니 비교가 편한것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈에 잘 보이기 위해서 호출 관련 부분의 호출 횟수를 같이 넣어봤다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Debounce와 Throttle 직접 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Debounce와 Throttle을 찾아보다가 직접 구현하신 분도 봤다 해당 내용을 참고해서 작성해봤다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742554089489&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;[flutter]throttle, debounce 만들기, 구현 예제 by Dart&quot; data-og-description=&quot;다른 언어로 프론트 개발을 해봤다면 이미 사용해봤거나 개념 정도는 익히고 있겠지만 flutter 그리고 Dart가 첫 개발 언어이신 분들을 위해 간단히 요약하자면throttle ➡️ 지정된 시간동안 연속 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; data-og-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-만들기-구현-예제-by-Dart&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf80JY/hyYvsQoSsM/HNqzYVWu8GV6KYFSWQKrck/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bGy54j/hyYvkria5j/ykM9LYKMhOhm4LrVIqlT8k/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bjjWLq/hyYul5oz3M/yGXs3UoapyZbfHgqP9QE31/img.png?width=1920&amp;amp;height=1634&amp;amp;face=684_378_1254_948&quot;&gt;&lt;a href=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf80JY/hyYvsQoSsM/HNqzYVWu8GV6KYFSWQKrck/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bGy54j/hyYvkria5j/ykM9LYKMhOhm4LrVIqlT8k/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bjjWLq/hyYul5oz3M/yGXs3UoapyZbfHgqP9QE31/img.png?width=1920&amp;amp;height=1634&amp;amp;face=684_378_1254_948');&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;[flutter]throttle, debounce 만들기, 구현 예제 by Dart&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다른 언어로 프론트 개발을 해봤다면 이미 사용해봤거나 개념 정도는 익히고 있겠지만 flutter 그리고 Dart가 첫 개발 언어이신 분들을 위해 간단히 요약하자면throttle ➡️ 지정된 시간동안 연속&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Debounce&lt;/h3&gt;
&lt;pre id=&quot;code_1742554290862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Debounce {
  final Duration delay;
  Timer? _timer;
  Debounce({required this.delay});

  run(action) {
    // 이미 기다리고 있었다면 타이머를 초기화
    if (_timer != null) {
      _timer!.cancel();
    }
    // 지정된 시간이 지나면 action을 실행
    _timer = Timer(delay, action);
  }
}

Debounce debounce = Debounce(delay: const Duration(milliseconds: 400));

debounce.run(() {
  callAPI('Handmade debounce');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Throttle&lt;/h3&gt;
&lt;pre id=&quot;code_1742554280162&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Throttle {
  final Duration delay;
  bool waiting = false;
  Timer? _timer;
  Throttle({required this.delay});

  void run(action) {
    // 타이머를 기다리지 않을 때만
    if (!waiting) {
      // action을 실행하고 이벤트를 기다림
      action();
      waiting = true;

      // 지정된 시간이 지나면 waiting을 풀어줌
      _timer = Timer(delay, () {
        waiting = false;
      });
    }
  }
}

Throttle throttle = Throttle(delay: const Duration(milliseconds: 400));

debounce.run(() {
  callAPI('Handmade debounce');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;try catch finally 사용해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실살 내 기준에서 제일 간편한 방법이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bool 값 하나 넣어주고 try catch final 로 상태관리하기 이렇게하면 이전 이벤트가 종료 직전에는 api가 호출이 안되니까 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 지금 공부하면서 보니까 Debounce 나 Throttle 을 상황에 맞게 쓰는게 더 안전하게 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742554676260&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  bool isDoing = false;

  void _tryCatchFinally() async {
    try {
      if (isDoing) {
        return;
      }
      isDoing = true;
      await callAPI('try');
    } catch (e) {
      if (kDebugMode) {
        print(e);
      }
    } finally {
      isDoing = false;
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체코드는 깃허브에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742555217883&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Flutter/personal_study/debounce_throttle at main &amp;middot; Hsungjin/Flutter&quot; data-og-description=&quot;Contribute to Hsungjin/Flutter development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&quot; data-og-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bhqFyp/hyYvf4ATKq/tIRqMOwdZt8SpxFuv5xQu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/byhKWS/hyYufREQ6m/gBlhP3sKtmUaLGwGKurWoK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/debounce_throttle&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bhqFyp/hyYvf4ATKq/tIRqMOwdZt8SpxFuv5xQu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/byhKWS/hyYufREQ6m/gBlhP3sKtmUaLGwGKurWoK/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;Flutter/personal_study/debounce_throttle at main &amp;middot; Hsungjin/Flutter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to Hsungjin/Flutter 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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일하게 함수의 수행을 막는 방법이라고해도 참 여러가지 방법이 있는것 같다.&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;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742555312088&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;[flutter]throttle, debounce 만들기, 구현 예제 by Dart&quot; data-og-description=&quot;다른 언어로 프론트 개발을 해봤다면 이미 사용해봤거나 개념 정도는 익히고 있겠지만 flutter 그리고 Dart가 첫 개발 언어이신 분들을 위해 간단히 요약하자면throttle ➡️ 지정된 시간동안 연속 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; data-og-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-만들기-구현-예제-by-Dart&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf80JY/hyYvsQoSsM/HNqzYVWu8GV6KYFSWQKrck/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bGy54j/hyYvkria5j/ykM9LYKMhOhm4LrVIqlT8k/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bjjWLq/hyYul5oz3M/yGXs3UoapyZbfHgqP9QE31/img.png?width=1920&amp;amp;height=1634&amp;amp;face=684_378_1254_948&quot;&gt;&lt;a href=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@iwytbbtss/flutterthrottle-debounce-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%98%88%EC%A0%9C-by-Dart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf80JY/hyYvsQoSsM/HNqzYVWu8GV6KYFSWQKrck/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bGy54j/hyYvkria5j/ykM9LYKMhOhm4LrVIqlT8k/img.png?width=880&amp;amp;height=370&amp;amp;face=0_0_880_370,https://scrap.kakaocdn.net/dn/bjjWLq/hyYul5oz3M/yGXs3UoapyZbfHgqP9QE31/img.png?width=1920&amp;amp;height=1634&amp;amp;face=684_378_1254_948');&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;[flutter]throttle, debounce 만들기, 구현 예제 by Dart&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다른 언어로 프론트 개발을 해봤다면 이미 사용해봤거나 개념 정도는 익히고 있겠지만 flutter 그리고 Dart가 첫 개발 언어이신 분들을 위해 간단히 요약하자면throttle ➡️ 지정된 시간동안 연속&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.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://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&quot;&gt;https://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742555346567&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;Debouncing and Throttling in Flutter using flutter_debouncer 2.0.0&quot; data-og-description=&quot;Ever encountered moments when your app&amp;rsquo;s responsiveness didn&amp;rsquo;t quite match your expectations? Imagine you&amp;rsquo;re typing a message or clicking a&amp;hellip;&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&quot; data-og-url=&quot;https://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/juBmB/hyYufjOse5/Yrcwh5vrqXr4fuoK1siJS0/img.png?width=1200&amp;amp;height=644&amp;amp;face=0_0_1200_644,https://scrap.kakaocdn.net/dn/dtjYk1/hyYuoOBTXa/lOIUMb4OhhvKJU2anW3PW0/img.png?width=1358&amp;amp;height=849&amp;amp;face=0_0_1358_849&quot;&gt;&lt;a href=&quot;https://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@syaifakmal/debouncing-and-throttling-on-flutter-265d42eee8d9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/juBmB/hyYufjOse5/Yrcwh5vrqXr4fuoK1siJS0/img.png?width=1200&amp;amp;height=644&amp;amp;face=0_0_1200_644,https://scrap.kakaocdn.net/dn/dtjYk1/hyYuoOBTXa/lOIUMb4OhhvKJU2anW3PW0/img.png?width=1358&amp;amp;height=849&amp;amp;face=0_0_1358_849');&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;Debouncing and Throttling in Flutter using flutter_debouncer 2.0.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ever encountered moments when your app&amp;rsquo;s responsiveness didn&amp;rsquo;t quite match your expectations? Imagine you&amp;rsquo;re typing a message or clicking a&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Mobile/Flutter</category>
      <category>Flutter</category>
      <category>flutter debounce</category>
      <category>flutter throttle</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/72</guid>
      <comments>https://sj-d.tistory.com/72#entry72comment</comments>
      <pubDate>Fri, 21 Mar 2025 20:09:43 +0900</pubDate>
    </item>
    <item>
      <title>Flutter 신용카드 예제 Like 카카오페이 2편</title>
      <link>https://sj-d.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sj-d.tistory.com/69&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2025.02.07 - [Flutter] - Flutter 신용카드 예제 Like 카카오페이 1편&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739958162501&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;Flutter 신용카드 예제 Like 카카오페이 1편&quot; data-og-description=&quot;주제 선정&amp;nbsp;[Flutter] 그라데이션으로 만들어보는 광원 효과빛이 반사되는 효과는 어떻게 만들까?velog.io처음에 위의 글을 보고 애니메이션이 기깔난다고 생각을 하고있었다.문득 카카오페이를 들&quot; data-og-host=&quot;sj-d.tistory.com&quot; data-og-source-url=&quot;https://sj-d.tistory.com/69&quot; data-og-url=&quot;https://sj-d.tistory.com/69&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bllwul/hyYjh82bDJ/3kxI97eN6ot6D76tGWa2B0/img.jpg?width=800&amp;amp;height=1739&amp;amp;face=0_0_800_1739,https://scrap.kakaocdn.net/dn/hleKd/hyYjyCUzqW/fXoQb8atiemNxswEudAV9K/img.jpg?width=800&amp;amp;height=1739&amp;amp;face=0_0_800_1739,https://scrap.kakaocdn.net/dn/OoTI9/hyYfTaYiRX/tH5iFXbKjhUKOuuGcCuyP0/img.png?width=1179&amp;amp;height=2556&amp;amp;face=0_0_1179_2556&quot;&gt;&lt;a href=&quot;https://sj-d.tistory.com/69&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sj-d.tistory.com/69&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bllwul/hyYjh82bDJ/3kxI97eN6ot6D76tGWa2B0/img.jpg?width=800&amp;amp;height=1739&amp;amp;face=0_0_800_1739,https://scrap.kakaocdn.net/dn/hleKd/hyYjyCUzqW/fXoQb8atiemNxswEudAV9K/img.jpg?width=800&amp;amp;height=1739&amp;amp;face=0_0_800_1739,https://scrap.kakaocdn.net/dn/OoTI9/hyYfTaYiRX/tH5iFXbKjhUKOuuGcCuyP0/img.png?width=1179&amp;amp;height=2556&amp;amp;face=0_0_1179_2556');&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;Flutter 신용카드 예제 Like 카카오페이 1편&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;주제 선정&amp;nbsp;[Flutter] 그라데이션으로 만들어보는 광원 효과빛이 반사되는 효과는 어떻게 만들까?velog.io처음에 위의 글을 보고 애니메이션이 기깔난다고 생각을 하고있었다.문득 카카오페이를 들&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sj-d.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글에 이어서 작성하는 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 이전편에서 카드 생성, 토스카드 모양으로 그리기, 기타 애니메이션 등을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DB에 저장된 데이터를 받아와서 하단에서 카드를 보여주고, 상단영역에서는 카드 상세정보를 보여주면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 제스쳐에 따라서 카드의 각도가 변하면서 광원효과를 넣어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;카드 목록 보여주기&lt;/h2&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;Carousel로 만들어서 상단에서 보여주려고 하려고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/carousel_slider&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pub.dev/packages/carousel_slider&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740568785709&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;carousel_slider | Flutter package&quot; data-og-description=&quot;A carousel slider widget, support infinite scroll and custom child widget.&quot; data-og-host=&quot;pub.dev&quot; data-og-source-url=&quot;https://pub.dev/packages/carousel_slider&quot; data-og-url=&quot;https://pub.dev/packages/carousel_slider&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btSANJ/hyYjoIyOAb/z9GmBKrCkVRTPSK4yGTEIk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/TRGOu/hyYmNUaPYJ/rb1Y11KSY0Ynlw2K5uXQ7k/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://pub.dev/packages/carousel_slider&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pub.dev/packages/carousel_slider&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btSANJ/hyYjoIyOAb/z9GmBKrCkVRTPSK4yGTEIk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/TRGOu/hyYmNUaPYJ/rb1Y11KSY0Ynlw2K5uXQ7k/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; data-ke-size=&quot;size16&quot;&gt;carousel_slider | Flutter package&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A carousel slider widget, support infinite scroll and custom child widget.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pub.dev&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;해당 라이브러리르 사용하면 carousel 을 쉽게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 공식지원하는 CarouselView 도 있고&lt;br /&gt;&lt;a href=&quot;https://api.flutter.dev/flutter/material/CarouselView-class.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://api.flutter.dev/flutter/material/CarouselView-class.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740568711819&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;CarouselView class - material library - Dart API&quot; data-og-description=&quot;A Material Design carousel widget. The CarouselView presents a scrollable list of items, each of which can dynamically change size based on the chosen layout. Material Design 3 introduced 4 carousel layouts: Multi-browse: This layout shows at least one lar&quot; data-og-host=&quot;api.flutter.dev&quot; data-og-source-url=&quot;https://api.flutter.dev/flutter/material/CarouselView-class.html&quot; data-og-url=&quot;https://api.flutter.dev/flutter/material/CarouselView-class.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://api.flutter.dev/flutter/material/CarouselView-class.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://api.flutter.dev/flutter/material/CarouselView-class.html&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;CarouselView class - material library - Dart API&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Material Design carousel widget. The CarouselView presents a scrollable list of items, each of which can dynamically change size based on the chosen layout. Material Design 3 introduced 4 carousel layouts: Multi-browse: This layout shows at least one lar&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;api.flutter.dev&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;이에 대응하는 방식인 PageView 도 있지만 세가지의 방법으로 구현해 봤을때 carousel_slider 가 가장 완성도가 높다고 판단하여 해당 라이브러리를 사용해서 진행하였다.&lt;/p&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;carousel_slider 도 마찬가지로 컨트롤러를 만들어 주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1740568909807&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final CarouselController controller = CarouselController(initialItem: 1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 CarouselSlider.builder 를 통해서 해당 _cards 의 리스트를 그려주도록 한다.&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;CarouselSlider&lt;/h3&gt;
&lt;pre id=&quot;code_1740568959409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Stack(
  alignment: Alignment.center,
  children: [
    // Transform.translate를 사용하여 위치 조정
    Transform.translate(
      offset: const Offset(-75, 0), // x축으로 -75 픽셀 이동
      child: Container(
        height: 120,
        padding: const EdgeInsets.only(bottom: 20),
        child: CarouselSlider.builder(
          carouselController: _carouselController,
          itemCount: _cards.length,
          options: CarouselOptions(
            height: 100,
            viewportFraction: 0.8,
            enlargeCenterPage: true,
            enableInfiniteScroll: _cards.length &amp;gt; 1,
            initialPage: currentCardIndex,
            autoPlay: false,
            onPageChanged: (index, reason) {
              if (currentCardIndex != index) {
                setState(() {
                  currentCardIndex = index;
                });
              }
            },
            clipBehavior: Clip.none,
            padEnds: true,
            pageSnapping: true,
            disableCenter: false,
            enlargeStrategy: CenterPageEnlargeStrategy.scale,
          ),
          itemBuilder: (context, index, realIndex) {
            // 카드 색상 가져오기
            final cardColor = _getColorFromDynamic(_cards[index]['card_color']);
            
            return GestureDetector(
              onTap: () =&amp;gt; _selectCard(index),
              child: CustomPaint(
                painter: CardPainter(
                  cardWidth: 150,
                  cardHeight: 90,
                  cardColor: currentCardIndex == index
                      ? cardColor
                      : cardColor.withValues(alpha: 0.5),
                ),
              ),
            );
          },
        ),
      ),
    ),
  ],
),&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;생각보다 구현하면서 CarouselOption 에서 많은걸 지정할 수 있었는데&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://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741160494597&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;flutter_carousel_slider/lib/carousel_options.dart at master &amp;middot; serenader2014/flutter_carousel_slider&quot; data-og-description=&quot;A flutter carousel widget, support infinite scroll, and custom child widget. - serenader2014/flutter_carousel_slider&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&quot; data-og-url=&quot;https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4F4cY/hyYna3208Q/Azjll9LfKxDSjtsWGVZCu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cesPyB/hyYm6AASgN/VlKfVAk7zwtuWFFBkQN8tK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/serenader2014/flutter_carousel_slider/blob/master/lib/carousel_options.dart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4F4cY/hyYna3208Q/Azjll9LfKxDSjtsWGVZCu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cesPyB/hyYm6AASgN/VlKfVAk7zwtuWFFBkQN8tK/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;flutter_carousel_slider/lib/carousel_options.dart at master &amp;middot; serenader2014/flutter_carousel_slider&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A flutter carousel widget, support infinite scroll, and custom child widget. - serenader2014/flutter_carousel_slider&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;height
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;높이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;viewportFraction
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 페이지가 사용하는 비율(기본값이 0.8 이라 사실 선언 안해줘도됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enlargeCenterPage
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;현재 페이지를 더 크게 보여줌(기본값 false)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enableInfiniteScroll
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;캐러셀 자체 슬라이드가 무한 반복 되어야 하는지 혹은 길이가 제한되어야 하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;initialPage
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;캐러셀이 생성될때 최초에 보여지는 화면&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;autoPlay
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자동 재생 여부(만약 true로 사용시 autoPlayInterval, autoPlayAnimationDuration, autoPlayCurve 등과 같이 사용 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;onPageChanged
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;페이지가 변경되었을때의 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;clipBehavior
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;페이지에 대한 clipBehavior(기본값 Clip.hardEdge)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;padEnds
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목록의 양끝에 패팅을 추가할지에 대한 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pageSnapping
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;페이지에 대해서 스냅을 사용할지의 여부 false로 놓게되면 스크롤을 자유롭게 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;disableCenter
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Center 위젯을 비활성화 할지에 대한 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;enlargeStrategy
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;현재 위치한 인덱스의 확대 방법으로 CenterPageEnlargeStrategy.scale , .height, .zoom 을 사용가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 왜 Transform.translate 로 이동시켰는지 의문이 들 수 있는데 이상하게 나는 카드를 가운대로 보내고 싶은데 시작점이 가운데가 되는 현상이 있었고 이걸 해결하고자 위치를 강제적으로 이동시켜서 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 카드의 색상 같은 경우 SQFlite 에서는 별도의 색상관련 데이터를 지정할수 없어 TEXT 형식으로 받았기 때문에 변환해서 카드의 색상을 넣어주어야 했다.&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;Carousel Indicator&lt;/h3&gt;
&lt;pre id=&quot;code_1740569197613&quot; class=&quot;lisp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;SizedBox(
  height: 50,
  child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: _cards.asMap().entries.map((entry) {
      return GestureDetector(
        onTap: () =&amp;gt; _selectCard(entry.key),
        child: Container(
          width: 8,
          height: 8,
          margin: const EdgeInsets.symmetric(horizontal: 4),
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: currentCardIndex == entry.key
                ? Colors.white
                : Colors.white.withValues(alpha: 0.5),
          ),
        ),
      );
    }).toList(),
  ),
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 현재 탭의 위치에 대해서 인지를 시켜주기위해서 만들어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카드의 값에 따른 UI 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1740569369688&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;child: CustomPaint(
  painter: CardPainter(
    cardWidth: 300,
    cardHeight: 200,
    cardColor: _getColorFromDynamic(_cards[currentCardIndex]['card_color']),
  ),
  child: SizedBox(
    width: 300,
    height: 200,
    child: Stack(
      children: [
        // 카드 번호
        Positioned(
          top: 120,
          left: 20,
          child: Text(
            _cards[currentCardIndex]['card_number'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        // 카드 소유자 이름
        Positioned(
          top: 160,
          left: 20,
          child: Text(
            _cards[currentCardIndex]['card_name'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        // 카드 만료일
        Positioned(
          top: 160,
          right: 20,
          child: Text(
            _cards[currentCardIndex]['card_expiration_date'] ?? '',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ],
    ),
  ),
),&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;현재 선택한 카드의 내용을 보여주기 위한 UI 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 1편에서 다뤘던 카드를 추가하는 부분이랑 큰 차이는 없으니 별다른 설명없이 넘어가겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 결과는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;카드 이동.gif&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qc24B/btsMB0eG0PR/hhaklvJYuSGaa83SAcjE81/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qc24B/btsMB0eG0PR/hhaklvJYuSGaa83SAcjE81/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qc24B/btsMB0eG0PR/hhaklvJYuSGaa83SAcjE81/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/qc24B/btsMB0eG0PR/hhaklvJYuSGaa83SAcjE81/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;273&quot; height=&quot;586&quot; data-filename=&quot;카드 이동.gif&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;1932&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;드래그를 통해 움직이는 효과 주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 카드를 슬라이드를 통해서 움직이게 만들어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 필요한건 내가 터치이벤트를 발생시켜서 드래그를 하면 그 방향으로 움직이는 부분을 만들어야 한다.&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;gt; 카드가 터치되었다는 신호를 받는다 -&amp;gt; 드래그를 하면 카드가 움직인다 -&amp;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;크게 어려운건 없고 카드 위젯 자체에 GestureDetector 로 감싸주고&lt;/p&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;onPanStart&lt;/b&gt;, &lt;b&gt;onPanUpdate&lt;/b&gt;, &lt;b&gt;onPanEnd&lt;/b&gt; 를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능은 조이스틱과 같은 상황에서 많이 사용된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;onPanStart&lt;/b&gt; 를 통해서 드래그 이벤트가 일어날 준비를 한다&lt;/p&gt;
&lt;pre id=&quot;code_1741161969629&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onPanStart: (_) {
	isCardTilting = true;
},&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;onPanUpdate&lt;/b&gt; 를 통해서 사용자의 드래그에 대해 업데이트 한다&lt;/p&gt;
&lt;pre id=&quot;code_1741162247663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onPanUpdate: (details) {
  _onPointerMove(details.localPosition, const Size(300, 200));
},

// 사용자의 터치 위치에 따라 기울기 값 조절
void _onPointerMove(Offset position, Size cardSize) {
  if (!isCardTilting) return;
  
  setState(() {
    // 카드 중심을 기준으로 기울기 계산
    final centerX = cardSize.width / 2;
    final centerY = cardSize.height / 2;
    
    // 터치 위치와 중심 간의 차이를 기반으로 기울기 계산
    tiltX = ((position.dy - centerY) / 10).clamp(-15.0, 15.0);
    tiltY = (-(position.dx - centerX) / 10).clamp(-15.0, 15.0);
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기 계산 로직은 사용자의 터치 위치와 카드 중심점 간의 거리를 기반으로 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 카드의 중심점을 계산해서 centerX, centerY 를 잡아주고,&lt;/p&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;tiltX&lt;/b&gt; 는 Y 축 방향의 터치 위치(position.dy)와 중심점 의 차이를 기반으로 계산된다. 여기서 -15도에서 15도로 제한한 이유는 사용자가 카드 위쪽을 터치하면 tiltX는 음즈가 되어 카드가 앞으로 기울어지고 아래쪽을 터치하면 양수가 뒤에 반대로 기울임을 주기 위함이다.&lt;/p&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;tiltY&lt;/b&gt; 는 X 축 방향의 터치 위치(position.dx)와 줌심점의 차이를 기반으로 계산된다. 여기서 Y는 - 를 사용한 이유는 직관적인 움직임을 위해서이다. 사용자가 카드의 오른쪽을 터치하면 음수가 되어 카드가 오른쪽으로 기울어지고 왼쪽을 터치하게되면 양수가 되어 카드가 왼쪽으로 기울어 지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 계산된 tiltX 와 tiltY 값은 밑에 Matrix4에서 3D 회전에서 다시 사용하게 된다.&lt;/p&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;onPanEnd&lt;/b&gt; 를 통해서 다시 원래의 상태로 되돌린다.&lt;/p&gt;
&lt;pre id=&quot;code_1741162411706&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onPanEnd: (_) =&amp;gt; _resetTilt(),

// 손을 떼면 원래 위치로 돌아가는 애니메이션 적용
void _resetTilt() {
  isCardTilting = false;
  _animationController.forward(from: 0.0);
}&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;이전에 사용했던 Transform의 Martrix4 를 통해서 이동에 따른 원근감을 주면서 회전을 시켜준다.&lt;/p&gt;
&lt;pre id=&quot;code_1741162573737&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;child: Transform(
  transform: Matrix4.identity()
    ..setEntry(3, 2, 0.001) // 원근감 추가
    ..rotateX(tiltX * (pi / 180))
    ..rotateY(tiltY * (pi / 180)),&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 사용하게 되면 사용자의 터치 드래그 이벤트를 받아오면서 카드를 회전시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원금감을 주기위해 Z축 방향으로 원근감을 추가해주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rotateX와 rotateY 메서드는 각각 X축과 Y축을 중심으로 회전 변환을 적용하기 위해서 pi / 180 을 곱해주게 되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Martrix4는 라디안을 단위로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재는 각도(degree)를 사용하기 떄문에 라디안(radian)으로 변환을 하기 위함이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;움직임에 따라 광원 효과 주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 해야되는건 움직임에 따라서 카드의 빛 반사를 만들어 줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱히 어려운건없고 Stack 에 Positioned 을 통해서 카드 위젯 위해 쌓아서 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 기울기를 통해 얻어오는 tilitX, tiltY 의 값을 통해서 opacity 를 조정하면서 만들어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opacity 값은 X축과 Y축 기울기의 절대값의 합(tiltX.abs() + tiltY.abs())를 30으로 나눈 값으로 설정했다.&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;pre id=&quot;code_1741162825821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 빛 반사 효과 (기울기에 따라 투명도 변경)
Positioned.fill(
  child: Opacity(
    opacity: (tiltX.abs() + tiltY.abs()) / 30,
    child: Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [
            Colors.white.withValues(alpha: 0.7),
            Colors.transparent,
          ],
        ),
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
),&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 사용하게되면 사용자의 드래그에 따라서 카드가 회전하면서 광원효과를 받을 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;카드 드래그 광원효과.gif&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjv4uR/btsMAWjZx9f/0pfDKBc6NaIqS1U1kNZik1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjv4uR/btsMAWjZx9f/0pfDKBc6NaIqS1U1kNZik1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjv4uR/btsMAWjZx9f/0pfDKBc6NaIqS1U1kNZik1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cjv4uR/btsMAWjZx9f/0pfDKBc6NaIqS1U1kNZik1/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;517&quot; height=&quot;418&quot; data-filename=&quot;카드 드래그 광원효과.gif&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 회사에서는 크게 애니메이션에 신경을 쓰고있지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 자주 사용하던 GestureDetector 에서 onPanStart 와 같이 조이스틱을 사용할 수 있는 부분과,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Martrix4를 사용해서 원금감을 주고 회전을 시키고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기를 이용해서 카드 자체에 광원효과를 주고 좋은 학습의 기회가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RiverPod은 이제야 공부해서 돌아와야해서 조금 오래걸릴거같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소스 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741162926219&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Flutter/personal_study/creadit_card_example at main &amp;middot; Hsungjin/Flutter&quot; data-og-description=&quot;Contribute to Hsungjin/Flutter development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&quot; data-og-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnQEme/hyYnb9JrUR/gV2vkfVprd3KZm8ljxXs10/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ccb7ih/hyYmPZ1w6z/8RzSrxh0dqrFxskxrzjAo0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Hsungjin/Flutter/tree/main/personal_study/creadit_card_example&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnQEme/hyYnb9JrUR/gV2vkfVprd3KZm8ljxXs10/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ccb7ih/hyYmPZ1w6z/8RzSrxh0dqrFxskxrzjAo0/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;Flutter/personal_study/creadit_card_example at main &amp;middot; Hsungjin/Flutter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to Hsungjin/Flutter 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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741162995761&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;[Flutter] 그라데이션으로 만들어보는 광원 효과&quot; data-og-description=&quot;빛이 반사되는 효과는 어떻게 만들까?&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC&quot; data-og-url=&quot;https://velog.io/@locked/Flutter-그라데이션으로-만들어보는-광원-효과&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpkzrV/hyYm2ybEEX/aUbbuZ0RPKL13Fxy4itpX1/img.gif?width=328&amp;amp;height=190&amp;amp;face=0_0_328_190,https://scrap.kakaocdn.net/dn/dP3F1C/hyYm86fNWM/jEN3ousRui4CIbV7bAHKjK/img.gif?width=328&amp;amp;height=190&amp;amp;face=0_0_328_190,https://scrap.kakaocdn.net/dn/KOJFa/hyYnd0K4nL/66xCcUh9VnwFceQxZrB8xK/img.png?width=670&amp;amp;height=990&amp;amp;face=0_0_670_990&quot;&gt;&lt;a href=&quot;https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@locked/Flutter-%EA%B7%B8%EB%9D%BC%EB%8D%B0%EC%9D%B4%EC%85%98%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EB%8A%94-%EA%B4%91%EC%9B%90-%ED%9A%A8%EA%B3%BC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpkzrV/hyYm2ybEEX/aUbbuZ0RPKL13Fxy4itpX1/img.gif?width=328&amp;amp;height=190&amp;amp;face=0_0_328_190,https://scrap.kakaocdn.net/dn/dP3F1C/hyYm86fNWM/jEN3ousRui4CIbV7bAHKjK/img.gif?width=328&amp;amp;height=190&amp;amp;face=0_0_328_190,https://scrap.kakaocdn.net/dn/KOJFa/hyYnd0K4nL/66xCcUh9VnwFceQxZrB8xK/img.png?width=670&amp;amp;height=990&amp;amp;face=0_0_670_990');&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;[Flutter] 그라데이션으로 만들어보는 광원 효과&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;빛이 반사되는 효과는 어떻게 만들까?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.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;&lt;a href=&quot;https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741162940135&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;[Flutter] 조이스틱 만들어보기&quot; data-og-description=&quot; &amp;zwj;  플러터로 조이스틱을 구현해보자&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@gongd/Flutter-조이스틱-만들어보기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ckYl0M/hyYm3Rn5kX/kOpGhg9k45ek11u9qVALC1/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502,https://scrap.kakaocdn.net/dn/da9tKm/hyYmMoGyNy/LvO9KeOLiyB8VFQjxhKlRK/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502,https://scrap.kakaocdn.net/dn/ZH2SG/hyYndT1tyg/APYNEMTwrU3ILFnHxi14MK/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502&quot;&gt;&lt;a href=&quot;https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@gongd/Flutter-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ckYl0M/hyYm3Rn5kX/kOpGhg9k45ek11u9qVALC1/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502,https://scrap.kakaocdn.net/dn/da9tKm/hyYmMoGyNy/LvO9KeOLiyB8VFQjxhKlRK/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502,https://scrap.kakaocdn.net/dn/ZH2SG/hyYndT1tyg/APYNEMTwrU3ILFnHxi14MK/img.png?width=844&amp;amp;height=502&amp;amp;face=0_0_844_502');&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;[Flutter] 조이스틱 만들어보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt; &amp;zwj;  플러터로 조이스틱을 구현해보자&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.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;&lt;a href=&quot;https://developer0524.tistory.com/77&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer0524.tistory.com/77&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741162971160&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;기울어지는 카드 UI 만들기 ( Flutter )&quot; data-og-description=&quot;Flutter에서는 다양한 애니메이션과 인터랙션을 쉽게 구현할 수 있습니다. 이번에는 사용자의 움직임(터치 또는 드래그)에 따라 이미지가 기울어지는 카드 효과를 만들어보겠습니다.&amp;nbsp;이 효과는 &quot; data-og-host=&quot;developer0524.tistory.com&quot; data-og-source-url=&quot;https://developer0524.tistory.com/77&quot; data-og-url=&quot;https://developer0524.tistory.com/77&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tIs0D/hyYmOUkIFV/kd8sMk0uaY53dZV7DlEEu0/img.png?width=800&amp;amp;height=1734&amp;amp;face=0_0_800_1734,https://scrap.kakaocdn.net/dn/dv0uk7/hyYmSh7fnl/Hs1g7TPSzVzIu9qqRL39oK/img.png?width=800&amp;amp;height=1734&amp;amp;face=0_0_800_1734&quot;&gt;&lt;a href=&quot;https://developer0524.tistory.com/77&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer0524.tistory.com/77&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tIs0D/hyYmOUkIFV/kd8sMk0uaY53dZV7DlEEu0/img.png?width=800&amp;amp;height=1734&amp;amp;face=0_0_800_1734,https://scrap.kakaocdn.net/dn/dv0uk7/hyYmSh7fnl/Hs1g7TPSzVzIu9qqRL39oK/img.png?width=800&amp;amp;height=1734&amp;amp;face=0_0_800_1734');&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;기울어지는 카드 UI 만들기 ( Flutter )&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Flutter에서는 다양한 애니메이션과 인터랙션을 쉽게 구현할 수 있습니다. 이번에는 사용자의 움직임(터치 또는 드래그)에 따라 이미지가 기울어지는 카드 효과를 만들어보겠습니다.&amp;nbsp;이 효과는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer0524.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Mobile/Flutter</category>
      <category>Flutter</category>
      <author>흐성진</author>
      <guid isPermaLink="true">https://sj-d.tistory.com/70</guid>
      <comments>https://sj-d.tistory.com/70#entry70comment</comments>
      <pubDate>Wed, 5 Mar 2025 17:23:24 +0900</pubDate>
    </item>
  </channel>
</rss>