<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.3">Jekyll</generator><link href="https://trenbe.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://trenbe.github.io/" rel="alternate" type="text/html" /><updated>2023-12-19T04:17:11+00:00</updated><id>https://trenbe.github.io/feed.xml</id><title type="html">트렌비 기술블로그</title><subtitle>Tren:be Tech Blog</subtitle><author><name>Trenbe Tech Team</name></author><entry><title type="html">AWS 가상환경에서의 테스트 자동화 실행기</title><link href="https://trenbe.github.io/2023/07/23/AWS-Device-Farm%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%B1%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%A4%ED%96%89%EA%B8%B0.html" rel="alternate" type="text/html" title="AWS 가상환경에서의 테스트 자동화 실행기" /><published>2023-07-23T15:00:00+00:00</published><updated>2023-07-23T15:00:00+00:00</updated><id>https://trenbe.github.io/2023/07/23/AWS%20Device%20Farm%EC%9D%84%20%ED%99%9C%EC%9A%A9%ED%95%9C%20%EC%95%B1%EC%9E%90%EB%8F%99%ED%99%94%20%EC%8B%A4%ED%96%89%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2023/07/23/AWS-Device-Farm%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%B1%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%A4%ED%96%89%EA%B8%B0.html">&lt;h2 id=&quot;들어가며&quot;&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요. 트렌비에서 QA 업무를 맡고 있는 리타입니다.&lt;/p&gt;

&lt;p&gt;트렌비의 QA(Quality Assurance)는 트렌비 서비스의 품질을 보증하기 위해 기획 단계부터 최종 딜리버리까지 개발의 모든 부분에 참여하며 다양한 업무를 수행하고 있습니다.&lt;/p&gt;

&lt;p&gt;그 중에서도 테스트가 차지하는 비중이 상당히 클 수 밖에 없는데요. 신규 서비스 런칭을 위해 테스트케이스를 설계하고 테스트 계획을 수립한 후 최종적으로 테스트를 수행하는 업무는 항상 많은 노력을 요구합니다.&lt;/p&gt;

&lt;p&gt;개발이 끝나고 통합 테스트를 진행하면서 기존 기능에 영향이 없는지 확인하는 과정은 가장 많이 신경쓰이는 부분이기도 하고, 이 부분에서 혹여라도 문제가 생길 경우 서비스 장애로 이어지기 때문에 가장 높은 집중력을 필요로 하기도 합니다.&lt;/p&gt;

&lt;p&gt;하지만 QA도 사람인지라 항상 모든 변경에 대한 영향도를 파악하기는 어려운 부분이 있습니다. 이런 약점을 보완하기 위해 트렌비에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Regression Test&lt;/code&gt;를 자동화하고, 필요할 때마다 실행되도록 하고 있습니다.&lt;/p&gt;

&lt;p&gt;테스트 자동화를 위한 기술에는 대표적으로 Selenium, Appium 등이 있있는데요. 트렌비에서 이 기술들을 사용하여 주요 기능들을 검수하는 테스트 자동화를 구현하고 있습니다.&lt;/p&gt;

&lt;p&gt;다양한 실수를 유발할 수 있는 반복적인 검증 과정에서의 실패를 줄이고 테스트 커버리지를 확보하기 위해서 자동화 기술은 필수적인 선택이었습니다.&lt;/p&gt;

&lt;p&gt;그중에서 이번에는 &lt;strong&gt;가상환경에서의 테스트 자동화&lt;/strong&gt;를 구현해 본 경험을 공유하고자 합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/qa-process.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;구현-배경&quot;&gt;구현 배경&lt;br /&gt;&lt;/h2&gt;

&lt;h4 id=&quot;자동화의-시작&quot;&gt;자동화의 시작&lt;/h4&gt;
&lt;p&gt;트렌비에서 ‘테스트 자동화’를 시작하게 된 계기는 점검해야 하는 서비스는 많은데 비해 &lt;strong&gt;QA 리소스는 부족&lt;/strong&gt;했기 때문입니다.&lt;/p&gt;

&lt;p&gt;간단해 보이는 변경일지라도 프로덕션에 배포하면 예상치 못한 문제가 발생하는 경우가 종종 있기 때문에 작은 변경 사항이더라도 구매와 관련된 핵심 기능들(&lt;em&gt;로그인부터 시작하여 회원가입, 주문, 상품목록, 장바구니 등&lt;/em&gt;)이 정상적으로 동작하는지는 항상 확인해야 합니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;하지만 부족한 리소스로 기존 기능 검증과 신규 기능 검증을 모두 하려면 많은 시간이 소요될 수밖에 없었습니다.&lt;/p&gt;

&lt;p&gt;그래서 반복적으로 확인하는 기능들은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Selenium&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Python&lt;/code&gt;을 사용하여 &lt;strong&gt;Web 테스트 자동화를 구현&lt;/strong&gt;했습니다.&lt;/p&gt;

&lt;p&gt;그동안 로그인, 회원가입 등 수동으로 확인했던 케이스들을 자동화로 전환하면서 QA 리소스를 절감 할 수 있었습니다.&lt;/p&gt;

&lt;h4 id=&quot;app-테스트-자동화&quot;&gt;APP 테스트 자동화&lt;/h4&gt;
&lt;p&gt;트렌비 앱은 Hybrid 기반으로 만들어졌습니다. 따라서 &lt;em&gt;Hybrid APP 특성상 Web과 App간의 차이가 많이 없기 때문에 Web 테스트 자동화로 Regression Test를 수행&lt;/em&gt;하고 있었습니다.&lt;/p&gt;

&lt;p&gt;그러나 &lt;strong&gt;앱에서만 다르게 동작하는 부분들&lt;/strong&gt;이 있었고 실제로 앱과 관련된 배포가 아니었음에도 앱이 실행이 안 되는 장애가 발생하기도 했습니다.&lt;/p&gt;

&lt;p&gt;앱에서만 발생하는 장애를 체크하기 위해서 여러 가지 방법을 조사했고 그 중에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Appium&lt;/code&gt;을 사용하게 되었습니다.&lt;/p&gt;

&lt;p&gt;Appium을 통해서 앱 테스트 때마다 반복적으로 수행하는 테스트 케이스를 자동화할 수 있었습니다.&lt;/p&gt;

&lt;h4 id=&quot;보유하고-있는-단말의-한계&quot;&gt;보유하고 있는 단말의 한계&lt;/h4&gt;
&lt;p&gt;앱에 대한 테스트 커버리지를 확보하기 위해서는 제조사, 해상도, OS 버전에 따른 다양한 단말에서 확인이 필요합니다.&lt;/p&gt;

&lt;p&gt;하지만 QA팀에서 &lt;strong&gt;보유하고 있는 단말은 한정적&lt;/strong&gt;이고 저희가 보유하고 있지 않은 단말에서의 이슈사항 문의가 들어오면 확인이 어려웠습니다.&lt;/p&gt;

&lt;p&gt;모든 단말을 갖출 수 없기 때문에 &lt;strong&gt;실제 단말과 동일한 가상환경에서 테스트할 수 있는 방법&lt;/strong&gt;을 찾아보게 되었고 그렇게 알아본 것이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AWS Device Farm&lt;/code&gt;이었습니다.&lt;/p&gt;

&lt;h2 id=&quot;appium을-이용한-app자동화&quot;&gt;Appium을 이용한 APP자동화&lt;/h2&gt;

&lt;h3 id=&quot;appium이란&quot;&gt;Appium이란?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Appium&lt;/code&gt;은 Native, Hybrid, Mobile Web 등 모든 Mobile 환경에서 테스트 자동화를 가능하게 해주는 오픈소스 도구입니다.&lt;/p&gt;

&lt;p&gt;대부분의 Mobile App은 iOS, Android 두 가지 플랫폼 상에서 작성되고 있습니다. 같은 기능의 APP이지만 서로 다른 플랫폼을 기반으로 작성되어있기 때문에 테스트를 진행할 때 몇 배의 커버리지가 필요로 합니다.&lt;/p&gt;

&lt;p&gt;이러한 환경에서 Appium은 &lt;strong&gt;하나의 테스트 코드로&lt;/strong&gt; 서로 다른 플랫폼(Android, iOS, Windows, FirefoxOS)의 APP을 테스트할 수 있도록 하여 리소스 확보는 물론 테스트 커버리지를 확대할 수 있습니다.&lt;/p&gt;

&lt;p&gt;Appium의 소프트웨어 구조가 여러 플랫폼을 제공해주는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Selenium WebDriver API&lt;/code&gt;를 사용하기 때문에 &lt;strong&gt;iOS 및 Android에서 상호작용이 가능합니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;작동 방식에 대해여 간단히 설명드리자면 Appium은 Node.js로 작성된 HTTP 서버입니다. 테스트 스크립트의 API들은 WebDriver JSON 유선 프로토콜을 사용하여 Appium 서버와 통신하여 iOS 및 Android 세션을 구동할 수 있습니다. 아키텍처 및 더욱 구체적인 내용은 아래 링크 참고하시길 바랍니다.&lt;br /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://domich.wordpress.com/2016/01/11/appium-%EC%95%A0%ED%94%BC%EC%9B%80-%ED%94%84%EB%A1%9C%ED%8C%8C%EC%9D%BC%EB%A7%81-%EA%B8%B0%EB%B0%98-ui-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EB%8F%84%EA%B5%AC/&quot;&gt;안드로이드 테스팅의 효자손 Appium&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/appium1.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
[출처: &lt;a href=&quot;https://www.edureka.co/blog/appium-architecture/&quot;&gt;edureka Blog&lt;/a&gt;]&lt;/p&gt;

&lt;h3 id=&quot;appium-실행-방법&quot;&gt;Appium 실행 방법&lt;br /&gt;&lt;/h3&gt;

&lt;h4 id=&quot;step-1-appium-서버-구성&quot;&gt;STEP 1. Appium 서버 구성&lt;/h4&gt;
&lt;p&gt;Appium은 Client/Server 구조로 되어 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Server&lt;/code&gt;: Appium을 실행하며 REST API를 제공해 Client로부터 요청을 받습니다. 그 명령어들을 모바일 기기에서 실행하고 다시 그 결과를 Client에게 전달해 주는 역할을 합니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Client&lt;/code&gt;: 테스트 코드를 작성해서 Server에 요청을 보냅니다. 실질적으로 Client는 요청을 하고 응답에 대한 처리만 하며 자동화나 에뮬레이팅은 Server에서 진행됩니다.&lt;/p&gt;

&lt;p&gt;이렇게 구조를 분리함으로써 Client는 더 다양한 환경에서 테스트 코드를 작성할 수 있고 Server는 클라우드와 같은 여러 환경에서 돌려 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/appium-server.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
[출처: &lt;a href=&quot;https://geeksfortesting.com/what-is-appium-appium-setup-for-mobile-automation/&quot;&gt;geeksfortestin Blog&lt;/a&gt;]&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;이 중 제가 구현해본 것은 Window기반의 Android 환경이었는데 이를 위해서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JDK, Node, Android SDK, Appium&lt;/code&gt; 설치가 필요합니다. 해당 관련된 자료들이 많았음에도 환경을 구축하는 과정에서 어려움이 있었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;예를 들어 Android Studio에서 필요한 SDK, Tool, API들을 누락없이 전부 다운받아야 하는데 여기서 하나라도 설치가 안되면 이후 Appium Server가 실행이 안됩니다. 여기서 나온 오류 로그를 일일히 검색해 가면서 원인을 찾는 데에도 많은 시간이 들었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;

&lt;/blockquote&gt;

&lt;p&gt;Appium 설치는 오히려 간단합니다. Appium 홈페이지에 접속하여 파일만 다운받으면 됩니다.&lt;/p&gt;

&lt;p&gt;위의 과정에서 JDK, Node, Android SDK 설치와 환경변수 설정만 문제없다면 아래와 같이 Appium 서버가 정상적으로 실행됩니다. 자세한 서버 구성법은 정말 도움이 많이 됐던 블로그 링크 공유드립니다.&lt;br /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://dejavuqa.tistory.com/222&quot;&gt;Appium 서버 구성 (on Windows)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/appium2-3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;step-2-appium-inspector를-통한-트렌비앱-실행&quot;&gt;STEP 2. Appium Inspector를 통한 트렌비앱 실행&lt;/h4&gt;
&lt;p&gt;이어서 테스트 스크립트를 작성하기 위해서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Appium Inspector&lt;/code&gt;가 필요합니다.&lt;/p&gt;

&lt;p&gt;테스트 대상이 되는 객체에 대한 정보를 식별하는 것이 중요한데 &lt;strong&gt;앱 화면을 미러링하여 Element 값을 식별&lt;/strong&gt;해 주는 Appium Inspector가 그 역할을 해줍니다.&lt;/p&gt;

&lt;p&gt;Appium Inspector와의 연결은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json&lt;/code&gt; 형태로 이루어진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Desired Capabilities&lt;/code&gt; 정보에 의해 이루어집니다.&lt;/p&gt;

&lt;p&gt;단말 버전, 설치된 앱 경로, udid 등 형식에 맞게 설정하고 실행하면 아래와 같이 앱 화면이 미러링 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;platformName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Android&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;platformVersion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;12.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;deviceName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Z Flip3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;app&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;C:&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Downloads&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;app-1.3.77-prod.apk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;automationName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Appium&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;newCommandTimeout&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;udid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;R5CR8050S5K&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;noReset&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;appPackage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;com.trenbe.trenbehybrid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;appActivity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;com.trenbe.trenbehybrid.view.splash.SplashActivity&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/appium4.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/imgs/posts/202211/appium5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;step-3-appium-자동화-실행-결과&quot;&gt;STEP 3. Appium 자동화 실행 결과&lt;/h4&gt;
&lt;p&gt;이제 Appium Inspector를 사용하여 테스트 스크립트를 작성합니다.&lt;/p&gt;

&lt;p&gt;Appium 작동 방식에서도 말씀드렸듯이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Selenium WebDriver API&lt;/code&gt;는 여러 플랫폼에서 사용이 가능하고, 기존에 구현해놨던 자동화 코드와 크게 다르지 않아서 스크립트를 작성하는 데에는 큰 공수가 들지 않았습니다.&lt;/p&gt;

&lt;p&gt;Appium Server가 실행되고 있는 상태에서 테스트 스크립트를 실행하면 아래와 같이 동작합니다.&lt;/p&gt;

&lt;p&gt;예시로 보여드릴 케이스는 트렌비앱을 실행한 후 로그인하는 동작입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/appium6.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/imgs/posts/202211/appium-login.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;
지금까지 로컬에서 Appium을 통한 앱 테스트 자동화를 실행하는 방법을 알아봤습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;다음으로 이어갈 내용은 가상환경인 AWS Device Farm에서 Appium을 실행하는 내용입니다.&lt;/p&gt;

&lt;h2 id=&quot;aws-device-farm-가상환경&quot;&gt;AWS Device Farm 가상환경&lt;br /&gt;&lt;/h2&gt;

&lt;h3 id=&quot;aws-device-farm-이란&quot;&gt;AWS Device Farm 이란?&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AWS Device Farm&lt;/code&gt;은 AWS 클라우드에서 실제 Android 및 iOS 디바이스를 테스트하고 상호 작용할 수 있는 앱 테스트 서비스입니다.&lt;/p&gt;

&lt;p&gt;이 서비스의 장점은 아래와 같습니다.&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;첫째, 실제 단말이 없어도 실제 환경과 동일하게 시물레이션이 가능하다.&lt;br /&gt;
둘째, 다양한 디바이스 및 OS 버전과 테스트 프레임워크를 지원해준다.&lt;br /&gt;
셋째, 웹 브라우저에서 디바이스 밀기, 제스처 및 상호 작용을 통해 고객 문제를 재현하고 디버깅 할 수 있다.&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;무엇보다 가장 중요한 장점은 &lt;strong&gt;AWS Device Farm 환경 안에서 Appium을 연동하여 앱 테스트 자동화가 가능&lt;/strong&gt;했다는 점입니다.&lt;/p&gt;

&lt;p&gt;구현 배경에서도 말씀드렸듯이 모든 단말을 구비할 수 없는 어려운 환경에서 AWS Device Farm을 사용하는게 적합했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;aws-device-farm-활용법&quot;&gt;AWS Device Farm 활용법&lt;/h3&gt;
&lt;p&gt;AWS에서 말하는 디바이스팜을 활용하는 방법 &lt;strong&gt;첫번째는 실시간으로 앱을 로드, 실행 및 상호 작용할 수 있는 장치에 대한 원격 액세스&lt;/strong&gt; 입니다.&lt;/p&gt;

&lt;p&gt;사용 방법은 간단합니다. 원하는 디바이스를 선택한 다음 .apk 또는 .ipa 파일을 업로드 합니다. 업로드가 완료되면 아래 이미지와 같이 가상환경 디바이스에 앱설치가 되고 실제 단말과 동일하게 동작이 됩니다.&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;실제로 지난번 AWS Device Farm을 사용하여 안드로이드 업데이트 대응 테스트를 진행했습니다. 안드로이드 단말 OS 13버전이 필요했지만 이미 다른 테스트로 사용중이어서 AWS Device Farm을 활용하게 되었습니다.&lt;/em&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;아래와 같이 간편하게 새로 빌드된 앱을 업로드 하기만 하면 쉽게 테스트가 가능합니다.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;두번째는 다양한 테스트 프레임워크를 사용하여 앱 테스트 자동화가 가능&lt;/strong&gt;합니다.&lt;br /&gt;
지원되는 다양한 테스트 프레임워크 중 하나가 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Appium&lt;/code&gt; 입니다.&lt;/p&gt;

&lt;p&gt;수백개의 실제 디바이스에서 자동화된 테스트를 동시에 실행하고 몇 분 만에 결과, 스크린샷, 동영상 및 성능 데이터를 얻을 수 있습니다.&lt;/p&gt;

&lt;p&gt;자세한 내용은 다음으로 이어갈 AWS Device Farm에서의 Appium 구성 방법에서 설명드리겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;aws-device-farm에서의-appium-실행-방법&quot;&gt;AWS Device Farm에서의 Appium 실행 방법&lt;br /&gt;&lt;/h3&gt;

&lt;h4 id=&quot;step-1-appium-테스트-패키지-구성&quot;&gt;STEP 1. Appium 테스트 패키지 구성&lt;/h4&gt;
&lt;p&gt;AWS Device Farm에서 Appium을 실행시키기 위해서는 테스트 패키지 구성이 필요합니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;먼저 로컬에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Virtualenv&lt;/code&gt; 라는 가상환경 셋팅이 필요하고 이 가상환경 안에서 Python, Appium Client 등 필요한 요소들을 설치해줍니다.&lt;/p&gt;

&lt;p&gt;그다음 AWS Device Farm에서 실행할 수 있는 압축된 테스트 파일을 생성해야 합니다. 자세한 가이드는 AWS 공식 페이지가 제일 설명이 잘되어 있으며 아래 링크 참고하시길 바랍니다.&lt;br /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://hcnoh.github.io/2019-06-19-windows-python-virtualenv&quot;&gt;Virtualenv 설치법&lt;/a&gt;, &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/devicefarm/latest/developerguide/test-types-appium.html&quot;&gt;AWS Device Farm 공식 가이드&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;이번에는 Window, Mac 환경에서 둘 다 진행해 봤지만 역시나 구축하는데 어려움이 있었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;그 이유는, &lt;strong&gt;Device Farm과 Appium을 연동하는 가이드 자체가 많이 없었을뿐더러&lt;/strong&gt; 설치하는 과정에서 Python이나 Appium 버전 문제를 해결하는 데에도 시간을 많이 쏟았습니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;특히 대부분의 가이드가 Mac 환경 기준으로 설명이 되어있다 보니 Window 환경 기준으로 명령어를 변환하는 과정도 번거로웠습니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;구현을 완료하고 나서는 간단한 작업처럼 보였지만 작업 과정에서 여러 어려움들이 있었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;

&lt;/blockquote&gt;

&lt;p&gt;하지만 결론적으론 테스트 패키지를 구성하는데 성공했습니다.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pip&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;virtualenv&lt;/span&gt; 
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;virtualenv&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;workspace&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;workspace&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activate&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;#가상환경 활성화
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pip&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pytest&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pip&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Appium&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Python&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Client&lt;/span&gt;

&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#tests 폴더 안에 테스트 스크립트 생성
#테스트 파일 이름은 test_로 시작해야함
&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;only&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#테스트파일 실행되는지 확인
&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pip&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;freeze&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requirements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#요구사항 파일
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;zip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;test_bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;zip&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requirements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;txt&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#최종 zip 파일로 패키징
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;step-2aws에-테스트-패키지-업로드&quot;&gt;STEP 2. AWS에 테스트 패키지 업로드&lt;/h4&gt;
&lt;p&gt;이제 위에서 만든 테스트 패키지 파일을 AWS에 업로드를 합니다.&lt;br /&gt;
업로드하는 과정은 어렵지 않으며 자세한 방식은 위에 공유한 AWS 공식 가이드에 나와있습니다.&lt;/p&gt;

&lt;p&gt;패키지 업로드 후 테스트 자동화가 실행이 완료되면 아래와 같이 보고서가 생성됩니다. 실제 테스트 진행시 안드로이드 단말 2대로 실행했던 결과이고 Pass 비율을 한눈에 보여줍니다.&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;사실 여기서도 실행이 완료되기까지 어려움이 있었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;가장 큰 원인은 &lt;strong&gt;AWS Device Farm 내에서 앱이 실행되는 환경이 느렸습니다.&lt;/strong&gt;&lt;/em&gt;&lt;br /&gt;
&lt;em&gt;그래서 화면이 넘어가는 부분에서 지연이 자주 발생하다 보니 실제 로컬에서 Appium을 실행하는 것과 다르게 에러가 많이 났습니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;한 번에 테스트가 매끄럽게 실행되면 좋겠지만 Pass율 100%를 만들기 위해 추가 작업이 필요했습니다. 기존 구현해놨던 스크립트에서 보정 작업이 필요했고 지속적인 유지 보수가 필요한 작업이었습니다.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm_13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;step-3가상환경에서의-실행-결과&quot;&gt;STEP 3. 가상환경에서의 실행 결과&lt;/h4&gt;
&lt;p&gt;이러한 노력 끝에 각 단말별로 실행결과를 보면 녹화영상, 로그 등 자동으로 수집된 것을 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;CPU, Memory 등 퍼포먼스 그래프도 수집해주며 테스트 로그는 보기가 편리해서 만약 실패가 발생했을 때 어디에서 에러가 났는지 바로 찾을 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;아래 영상은 실제 AWS Device Farm 가상환경에서 테스트가 실행되는 과정을 첨부했습니다. 해당 케이스는 트렌비에서 주문부터 주문 취소까지 동작하는 플로우입니다.&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;strong&gt;&lt;em&gt;재현경로&lt;/em&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;트렌비앱 실행 &amp;gt; 로그인 &amp;gt; 상품 상세페이지 진입 &amp;gt; 주문하기 클릭 &amp;gt; 주문서 페이지 진입 &amp;gt; 결제수단 가상계좌 선택 &amp;gt; 필수정보 입력 &amp;gt; 주문 완료 &amp;gt; 주문내역 확인 &amp;gt; 주문 취소 진행&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/devicefarm-order.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;마치며-트렌비에서의-활용법&quot;&gt;마치며, 트렌비에서의 활용법&lt;/h2&gt;
&lt;p&gt;AWS Device Farm 서비스 자체는 유료이지만 요금 방식이 여러 가지가 있고 실제 단말을 구매하는 것보다 오히려 저렴하게 관리가 가능합니다.&lt;/p&gt;

&lt;p&gt;테스트 자동화뿐만 아니라 디버깅에도 유용하게 사용하실 수 있는데요. 실제 단말이 없어도 시뮬레이션을 통해 고객 문제를 디버깅할 수 있고 그 결과를 자동으로 로그 및 동영상을 제공해주기 때문에 이슈사항을 빠르게 파악할 수 있습니다.&lt;/p&gt;

&lt;p&gt;최근 안드로이드 앱 업데이트 대응을 위해 활용해 보니 테스트 시간을 절감할 수도 있고 실제 보유하고 있지 않은 단말도 테스트가 가능해서 유용했습니다.&lt;/p&gt;

&lt;p&gt;트렌비 서비스가 더욱 많아지고 규모도 커진다면 이러한 가상환경 툴을 사용함으로써 품질을 더 높일 수 있는 발판이 될 것 같습니다.&lt;/p&gt;

&lt;h5 id=&quot;정말-마치며&quot;&gt;정말 마치며,&lt;/h5&gt;
&lt;p&gt;트렌비에 와서 처음으로 자동화 업무를 시작했을 때 정말 힘들었던 기억이 납니다. 개발자가 아니었기에 코드 짜는 기술도 부족했고 막히는 부분도 참 많았습니다.&lt;/p&gt;

&lt;p&gt;그럼에도 이렇게까지 테스트 자동화를 구현할 수 있었던 건 많은 도움을 주신 동료 개발자분들 덕분이 아닐까 싶습니다.&lt;/p&gt;

&lt;p&gt;이 글을 빌려 다시 한번 감사드립니다!&lt;/p&gt;

&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;

&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://appium.io/&quot;&gt;Appium 공식 페이지&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://domich.wordpress.com/2016/01/11/appium-%EC%95%A0%ED%94%BC%EC%9B%80-%ED%94%84%EB%A1%9C%ED%8C%8C%EC%9D%BC%EB%A7%81-%EA%B8%B0%EB%B0%98-ui-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EB%8F%84%EA%B5%AC/&quot;&gt;안드로이드 테스팅의 효자손 Appium&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://twpower.github.io/94-set-appium-test-environment-for-android-using-python-client-in-local&quot;&gt;Appium 구조&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://easytesting.tistory.com/15&quot;&gt;Appium 작동 방식&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dejavuqa.tistory.com/222&quot;&gt;Appium 서버 구성 (on Windows)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/device-farm/&quot;&gt;AWS Device Farm 공식 페이지&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/devicefarm/latest/developerguide/test-types-appium.html&quot;&gt;AWS Device Farm에서의 Appium 사용법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>리타</name></author><category term="QA" /><category term="Appium" /><category term="Selenium" /><category term="Automation" /><summary type="html">들어가며 안녕하세요. 트렌비에서 QA 업무를 맡고 있는 리타입니다.</summary></entry><entry><title type="html">마이크로 서비스 환경에서 통합된 API 문서 서버 구축하기</title><link href="https://trenbe.github.io/2023/01/30/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%ED%86%B5%ED%95%A9%EB%90%9C-API-%EB%AC%B8%EC%84%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0.html" rel="alternate" type="text/html" title="마이크로 서비스 환경에서 통합된 API 문서 서버 구축하기" /><published>2023-01-30T01:00:00+00:00</published><updated>2023-01-30T01:00:00+00:00</updated><id>https://trenbe.github.io/2023/01/30/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%20%EC%84%9C%EB%B9%84%EC%8A%A4%20%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%20%ED%86%B5%ED%95%A9%EB%90%9C%20API%20%EB%AC%B8%EC%84%9C%20%EC%84%9C%EB%B2%84%20%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2023/01/30/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%ED%86%B5%ED%95%A9%EB%90%9C-API-%EB%AC%B8%EC%84%9C-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0.html">&lt;h2 id=&quot;들어가며&quot;&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요. 트렌비 스토어팀에서 백엔드 개발을 맡고 있는 오언입니다.&lt;/p&gt;

&lt;p&gt;최근 마이크로 서비스 환경에서 API 문서를 효율적으로 관리할 수 있는 공용 API 문서 서버를 구축하였고 이에 대한 내용을 공유하고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;1-api-문서를-효율적으로-관리할-수-없을까&quot;&gt;1. API 문서를 효율적으로 관리할 수 없을까?&lt;/h2&gt;
&lt;p&gt;API를 제공하거나 사용하는 개발자들에게 있어서 API 명세를 정리하고 공유하는 것은 주요한 업무 중 하나입니다.&lt;/p&gt;

&lt;p&gt;백엔드 개발자가 생성한 API를 프론트엔드 개발자에게 전달해줄 때, 다른 도메인 팀과 API 명세를 주고받을 때 등 개발자 간의 효율적인 소통은 잘 정리된 API 문서를 통해서 이루어지게 됩니다.&lt;/p&gt;

&lt;p&gt;트렌비에서는 API 문서를 관리하는 데 있어서 정해진 규칙이 존재하지 않았습니다. 서비스에 따라 Swagger나 Spring Rest Docs와 같은 API 문서화 도구를 통해 문서를 관리하고 있거나 노션에 API 명세를 정리를 해두는 등 각양각색의 방법이 혼재되어 있었습니다. 이로 인해 API 문서를 공유하는데 혼란이 있었고 비효율적인 커뮤니케이션이 추가로 발생하였습니다.&lt;/p&gt;

&lt;p&gt;API 문서를 효율적으로 관리하는 방법을 고민하던 중에 2020년에 &lt;a href=&quot;https://youtu.be/qguXHW0s8RY&quot;&gt;NHN에서 진행되었던 한 컨퍼런스 영상&lt;/a&gt;에서 이에 대한 좋은 아이디어를 얻을 수 있었습니다.
NHN에서도 마찬가지로 MSA 환경에서 API 문서 관리 방법에 대해 고민하였고, 이에 대한 해결책으로 Swagger와 Spring RestDocs로 만들어진 API 문서 형식을 하나의 OpenAPI 형식으로 통일하여 한 곳에서 관리하도록 처리했다는 것을 확인할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이 아이디어를 기반으로 하여 트렌비에서도 API 문서가 통합되고 자동으로 관리될 수 있는 공용 API 문서 서버를 구성하였습니다.&lt;/p&gt;

&lt;h2 id=&quot;2-api-문서-형식-통일하기&quot;&gt;2. API 문서 형식 통일하기&lt;/h2&gt;
&lt;p&gt;API 문서를 효율적으로 관리하기 위해서는 우선 다양한 형식의 문서를 통일된 형식으로 맞출 필요가 있었습니다.&lt;/p&gt;

&lt;p&gt;트렌비의 많은 서비스는 Swagger나 Spring RestDocs로 문서 관리를 하고 있었기 때문에 NHN 컨퍼런스에서 소개한 방법으로 많은 서비스들을 하나의 OpenAPI 형식으로 통합할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;또한 기존에 Swagger가 적용되어 있는 프로젝트에서는 OpenAPI 문서 형식이 기본 포맷이므로 별도의 작업이 필요 없고, Spring Rest Docs 코드에서도 오픈소스를 이용하면 적은 비용으로 OpenAPI 문서를 생성할 수가 있어 쉽게 마이그레이션이 가능하다는 장점이 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/generate_openapi.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;spring-rest-docs에서-openapi-문서-생성하기&quot;&gt;Spring Rest Docs에서 OpenAPI 문서 생성하기&lt;/h4&gt;
&lt;p&gt;Spring RestDocs에서는 AsciiDoc이나 Markdown 문법을 지원하고 이를 통해 쉽게 HTML 기반의 문서 파일을 만들어낼 수 있습니다. 기본적으로 OpenAPI 형식의 문서 파일을 만들어 낼 수는 없지만 &lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot;&gt;restdocs-api-spec&lt;/a&gt; 오픈 소스로 손쉽게 OpenAPI 형식의 문서 파일을 생성할 수 있습니다.&lt;/p&gt;

&lt;p&gt;이 오픈 소스를 활용하면 Spring Rest Docs 기반의 테스트 코드와 거의 동일하게 작성할 수 있고, 기존에 작성되어 있는 Spring Rest Docs 기반의 테스트 코드에서도 import 문을 변경하는 최소한의 코드 수정으로 손쉽게 OpenAPI 스펙으로 마이그레이션이 가능합니다.&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;epages&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;restdocs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;apispec&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;MockMvcRestDocumentationWrapper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;document&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;resultActions&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;andDo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operationName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;requestFields&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fieldDescriptors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFieldDescriptors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;responseFields&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fieldWithPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;the comment&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fieldWithPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flag&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;the flag&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fieldWithPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;count&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;the count&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fieldWithPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;fieldWithPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;_links&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ignored&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;links&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linkWithRel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;self&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;some&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이후 다음과 같은 Gradle 플러그인 명령어를 실행하면 Spring Rest Docs 기반의 테스트 코드로부터 OpenAPI 문서를 생성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./gradlew openapi3 &lt;span class=&quot;nt&quot;&gt;--no-build-cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;3-통합된-api-문서를-자동으로-관리하기&quot;&gt;3. 통합된 API 문서를 자동으로 관리하기&lt;/h2&gt;
&lt;p&gt;트렌비와 같은 MSA 환경에서는 도메인별로 서버가 분산되어 있기 때문에 API 문서도 서버별로 분산되어 있습니다.
API 문서를 공유하기 위해서는 각 도메인에 있는 문서를 하나하나 URL로 전달해야 한다는 불편함이 있고 개별 문서를 찾아보거나 관리하는 것도 쉽지 않습니다.&lt;/p&gt;

&lt;p&gt;API 문서를 한 곳에서 조회하고 관리한다면 이러한 불편함이 줄어들지 않을까요?&lt;/p&gt;

&lt;p&gt;앞서 API 문서 형식을 OpenAPI 형식으로 통일했기 때문에 Swagger UI와 함께 사용한다면 통합된 API 문서를 관리 할 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;swagger-ui&quot;&gt;Swagger UI&lt;/h4&gt;
&lt;p&gt;Swagger UI를 이용하면 OpenAPI 문서를 UI로 시각화할 수 있고 API 요청을 직접 테스트해볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;Swagger UI는 복수 개의 OpenAPI 문서 조회가 가능하므로 모든 OpenAPI 문서를 하나의 Swagger UI 서버에서 조회하게 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;Swagger UI 화면에서 셀렉트 박스를 클릭하면 각 도메인별로 API 문서 조회가 가능한 것을 보실 수 있을 것입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/swagger_ui_server.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 API 문서를 한 곳에서 관리하기 위해서는 한 저장소로 업로드하는 과정이 필요하고 API 문서가 업데이트될 때마다 재업로드하는 과정이 필요하기 때문에 이를 수작업으로 하는 데는 많은 번거로움이 요구됩니다.&lt;/p&gt;

&lt;p&gt;그렇기 때문에 API에 변경이 생겼을 때 업데이트된 API 문서를 자동으로 반영해줄 수 있는 CI/CD를 구성하였습니다.&lt;/p&gt;

&lt;h4 id=&quot;cicd를-통해-자동으로-api-문서-관리하기&quot;&gt;CI/CD를 통해 자동으로 API 문서 관리하기&lt;/h4&gt;
&lt;p&gt;트렌비에서는 마이크로 서비스를 빌드, 배포할 때 Github Action을 주로 활용하고 있기에 Github Action으로 API 문서 관리를 위한 CI/CD 파이프라인을 구성하였습니다.&lt;/p&gt;

&lt;p&gt;CI/CD 파이프라인에 의해 서비스를 개발 환경에 배포할 때마다 API 문서 파일을 생성하고 AWS S3 버킷에 업로드 하는 로직을 추가하여 자동으로 문서가 업데이트될 수 있도록 처리하였습니다.&lt;/p&gt;

&lt;p&gt;저희의 경우 개발 환경에만 API 문서 서버를 구축하였는데, Swagger UI에서 쉽게 API 테스트를 할 수 있다 보니 운영 환경에서 API를 테스트할 수 있게 하면 실제 상용 데이터가 변경되는 이슈가 발생할 수 있기 때문이었습니다.
또, 개발 중인 API의 문서를 주로 공유한다는 점에서 개발 환경에서만 구축하는 것으로 충분하다고 판단했습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Github Action 배포 파이프라인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;build-deploy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Upload OpenApi Specification to S3&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;# OpenAPI 형식의 yaml 파일 생성&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;./gradlew openapi3 --no-build-cache&lt;/span&gt;

  &lt;span class=&quot;s&quot;&gt;# Amazone S3에 OpenAPI 문서 파일 업로드&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;aws s3 cp openapi3.yaml s3://{bucket}/(문서파일 이름}.yam&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;$&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;AWS_REGION&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ap-northeast-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;
&lt;strong&gt;전체적인 구성도&lt;/strong&gt;
&lt;img src=&quot;/imgs/posts/202301/build_and_deploy.jpg&quot; alt=&quot;&quot; /&gt;
사용자가 개발 환경에 서비스 배포 요청을 하면 Github Action의 workflow 파이프라인이 수행되어 어플리케이션의 이미지가 빌드되고 배포되는 동시에 최신 API 문서도 S3로 업로드됩니다.&lt;/p&gt;

&lt;p&gt;API 문서 서버에서는 S3에 있는 API 문서 파일을 조회해서 UI 화면에 렌더링해서 최신의 API 문서를 시각화하여 보여주게 됩니다.&lt;/p&gt;

&lt;h2 id=&quot;4-후기&quot;&gt;4. 후기&lt;/h2&gt;
&lt;p&gt;Spring RestDocs의 장점인 테스트 코드로 검증된 API 문서와 Swagger의 장점인 UI 화면에서 손쉽게 API 테스트가 가능하다는 이 두 가지 장점을 모두 취해 보다 편리하고 검증된 API 문서 서버를 구축할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;또, 통합된 API 문서 서버를 구축하고 여러 도메인 팀의 개발자들이 API 문서 표준화에 힘써준 덕분에 트렌비의 분산된 많은 도메인 API를 한 곳에서 확인할 수 있게 되었습니다.
API 명세를 공유하기 위해 하나의 API 문서 서버 주소를 사용하게 되면서 제공하는 입장이나 사용하는 입장에서 불필요한 의사소통을 줄일 수 있게 되었으며, 손쉽게 사이트에서 API 테스트를 할 수 있어서 개발 편의성이 증가하였습니다.&lt;/p&gt;

&lt;p&gt;트렌비와 비슷한 MSA 환경에서 API 문서를 효율적으로 관리하는 것을 고민하고 있다면 위와 같은 방법을 고려해보는 것도 좋을 것 같습니다.&lt;/p&gt;

&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://youtu.be/qguXHW0s8RY&quot;&gt;[NHN FORWARD 2020] MSA 환경에서 API 문서 관리하기: 생성부터 배포까지&lt;/a&gt; &lt;br /&gt;
&lt;a href=&quot;https://github.com/aws-actions/configure-aws-credentials&quot;&gt;https://github.com/aws-actions/configure-aws-credentials&lt;/a&gt; &lt;br /&gt;
&lt;a href=&quot;https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md&quot;&gt;https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content><author><name>오언</name></author><category term="API 문서" /><category term="Swagger" /><category term="OpenAPI Specification" /><category term="Spring Rest Docs" /><summary type="html">들어가며 안녕하세요. 트렌비 스토어팀에서 백엔드 개발을 맡고 있는 오언입니다.</summary></entry><entry><title type="html">트렌비 서비스 품질 향상을 위한 API 테스트 적용기</title><link href="https://trenbe.github.io/2023/01/23/API-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EC%A0%81%EC%9A%A9%EA%B8%B0.html" rel="alternate" type="text/html" title="트렌비 서비스 품질 향상을 위한 API 테스트 적용기" /><published>2023-01-23T15:00:00+00:00</published><updated>2023-01-23T15:00:00+00:00</updated><id>https://trenbe.github.io/2023/01/23/API%20%ED%85%8C%EC%8A%A4%ED%8A%B8%20%EC%9E%90%EB%8F%99%ED%99%94%20%EC%A0%81%EC%9A%A9%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2023/01/23/API-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EC%A0%81%EC%9A%A9%EA%B8%B0.html">&lt;h1 id=&quot;들어가며&quot;&gt;들어가며&lt;/h1&gt;
&lt;p&gt;안녕하세요. QA Engineer 미키입니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 트렌비 프론트 서비스의 더 나은 &lt;strong&gt;서비스 품질보증&lt;/strong&gt;을 위해 도입한 &lt;strong&gt;API 테스트 자동화&lt;/strong&gt;에 대해서 이야기 해보고자 합니다.&lt;/p&gt;

&lt;h1 id=&quot;배경-및-목적&quot;&gt;배경 및 목적&lt;/h1&gt;
&lt;p&gt;트렌비는 Selenium을 통한 UI 테스트 자동화를 업무에 적용시켜 회원가입, 로그인, 결제, 장바구니, 전시영역에 이르는 대부분의 주요 구매 플로우(flow) 영역을 자동화하여 테스트하고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 UI 테스트 자동화를 서비스의 모든 영역으로 확장하여 적용하기에는 몇가지 어려움이 있었고, 이 문제를 보완하고자 다른 방안을 생각해보게 되었습니다.&lt;/p&gt;

&lt;p&gt;저희가 파악한 UI 테스트 자동화의 단점으로는 &lt;strong&gt;전수 테스트를 하는데 시간이 오래 걸리는 문제&lt;/strong&gt;와 &lt;strong&gt;UI의 잦은 변화로 인한 유지보수 리소스 문제&lt;/strong&gt; 였습니다.&lt;/p&gt;

&lt;p&gt;먼저, &lt;strong&gt;전수 테스트시에 시간이 오래 걸리는 문제&lt;/strong&gt; 에 대해 살펴보면 트렌비는 지속적인 배포(Continuous Delivery)를 지향하고, 이를 위해 모든 CI/CD 파이프라인은 빠른 빌드와 배포에 초점을 맞추고 있는 상황이었습니다.&lt;/p&gt;

&lt;p&gt;따라서, &lt;u&gt;빠른 테스트 수행&lt;/u&gt;은 이를 위한 필수적인 요소 중 하나였습니다. 전수 테스트를 하는데 시간이 오래 걸리면 그만큼 배포도 늦어 질 수 있기 때문입니다.&lt;/p&gt;

&lt;p&gt;또 다른 문제는 &lt;strong&gt;UI의 잦은 변화로 인한 유지보수 리소스 문제&lt;/strong&gt; 였습니다. 여러 기능을 지속적으로 배포하다 보니 이에 따른 UI 변경도 자주 발생했는데요. 이러한 UI 변경 시마다 테스트 코드도 함께 수정해야 하는 어려움이 있었습니다.&lt;/p&gt;

&lt;p&gt;변경된 UI에 대응하는 테스트 코드 수정을 배포 전에 미리 하지 않을 경우 테스트가 실패하여 오류가 발생했고 이는 불필요한 알람을 야기했습니다.&lt;/p&gt;

&lt;p&gt;지금까지 살펴본 단점을 보완하면서 자동화된 QA &lt;u&gt;테스트 영역을 확장&lt;/u&gt;하기 위해서 여러가지 방안들을 고민하게 되었고 &lt;strong&gt;API 테스트 자동화&lt;/strong&gt;가 하나의 대안이 될 수 있다고 판단 했습니다.&lt;/p&gt;

&lt;p&gt;화면보다는 데이터가 중요한 부분에 API 테스트 자동화를 병행하면 전수 테스트를 하는데 &lt;strong&gt;시간을 절약&lt;/strong&gt; 할 수 있고, &lt;strong&gt;잦은 UI변경에도 대처&lt;/strong&gt; 할 수 있기 때문입니다.&lt;/p&gt;

&lt;h1 id=&quot;api-테스트-자동화의-접근&quot;&gt;API 테스트 자동화의 접근&lt;/h1&gt;
&lt;p&gt;API 테스트 자동화를 도입하기로 결정하고 나니 API 테스트를 어떻게 해야할지(?) 고민하게 되었고 대략 두가지 방법을 생각하게 되었습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Postman과 같은 잘 만들어진 툴을 사용하여 API 테스트를 한다.&lt;/li&gt;
  &lt;li&gt;Python에서 HTTP 요청을 보내는 모듈인 Requests를 사용한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;두 가지 방안에 대해 조사하고 POC를 진행하였는데 현재 트렌비의 경우 자동화 스크립트를 Python으로 관리하고 있기에 유지보수 측면에서 두 번째 방법이 더 효율적이라 판단되어 &lt;strong&gt;Python Requests&lt;/strong&gt;를 이용하기로 했습니다.&lt;/p&gt;

&lt;h1 id=&quot;목표&quot;&gt;목표&lt;/h1&gt;
&lt;p&gt;API가 정상적으로 응답하는지 테스트 하려면 어떻게 해야 할까!?
이를 검증하기 위해서 3가지 목표를 설정했습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;API &lt;strong&gt;상태코드&lt;/strong&gt;를 체크하자!&lt;/li&gt;
  &lt;li&gt;API가 정상동작 할 때 &lt;strong&gt;적절한 응답 및 데이터&lt;/strong&gt;가 오는지 체크하자!&lt;/li&gt;
  &lt;li&gt;유저시나리오(로그인부터 주문까지) 기반 &lt;strong&gt;핵심 API가 정상동작&lt;/strong&gt; 하는지 체크하자!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;첫-번째-get방식requestsget을-통한-상태코드-체크&quot;&gt;첫 번째. GET방식(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests.get()&lt;/code&gt;)을 통한 상태코드 체크!&lt;/h2&gt;
&lt;p&gt;API 테스트를 하기 위해서는 원하는 URL 주소로 &lt;strong&gt;요청(requests(get/post/put/delete))&lt;/strong&gt; 을 보내고, 서버에서는 그 요청을 받아 처리한 후 요청자에게 &lt;strong&gt;응답(response)&lt;/strong&gt; 을 줍니다.&lt;/p&gt;

&lt;p&gt;아래에 보이는 트렌비 이벤트 페이지를 예로 들어 API 상태를 체크해 보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;API를 Request로 호출하면 위와 같은 상태코드를 응답 받을 수 있습니다.&lt;/p&gt;

&lt;p&gt;이 상태 코드를 보고 요청이 잘 처리되었는지 문제가 있는지 알 수가 있습니다.&lt;/p&gt;

&lt;p&gt;상태 코드는 응답 객체의 &lt;strong&gt;‘status_code’&lt;/strong&gt; 를 통해 간단하게 얻을 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;상태 코드(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status_code&lt;/code&gt;)&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;1xx (정보): 요청을 받았으며 프로세스를 계속한다.&lt;/li&gt;
    &lt;li&gt;2xx (성공): 요청을 성공적으로 받았으며 인식했고 수용하였다.&lt;/li&gt;
    &lt;li&gt;3xx (리다이렉션): 요청 완료를 위해 추가 작업 조치가 필요하다.&lt;/li&gt;
    &lt;li&gt;4xx (클라이언트 오류): 요청의 문법이 잘못되었거나 요청을 처리할 수 없다.&lt;/li&gt;
    &lt;li&gt;5xx (서버 오류): 서버가 명백히 유효한 요청에 대해 충족을 실패했다.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;requests&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 이벤트 API
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'https://www.trenbe.com/event_url/****'&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# event_url 요청 및 응답
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# status_code 출력
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;정상&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;일&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;경우&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xx&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;실패&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;할&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;경우&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xx&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 간단한 코드로 API 상태를 체크 할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이벤트 페이지 기준으로 &lt;strong&gt;UI를 포함한 테스트 시 약 40초&lt;/strong&gt;가 소요 되었고, &lt;strong&gt;API만 테스트할 경우는 약 18초&lt;/strong&gt; 정도로 절반 이상 빨랐습니다.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(실제 코드 실행 기준만 본다면 2~3초면 검수가 완료됩니다.)&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;두-번째-상태-코드가-2xx이면-데이터들도-정상일까&quot;&gt;두 번째. 상태 코드가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2xx&lt;/code&gt;이면 데이터들도 정상일까!??&lt;/h2&gt;
&lt;p&gt;첫 번째 이야기에서 상태 코드가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2xx&lt;/code&gt;일 경우 정상적으로 응답 받은 것을 알 수 있습니다.&lt;/p&gt;

&lt;p&gt;하지만 응답은 정상적으로 왔지만 그 안에 포함되어 있는 데이터 들도 정상일까(?) 라는 의문이 들었고, 실제로 상태 코드가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2xx&lt;/code&gt;인 경우에도 데이터들이 정상적으로 오는지, 유효한 데이터 인지 확인 할 필요가 있었습니다.&lt;/p&gt;

&lt;p&gt;트렌비 상품 페이지에서는 화면에 보이는 정보를 구성하기 위해 여러 API를 호출하고 있습니다. 아래 화면을 보면 보기에는 아무 문제가 없어 보이지만 상품 페이지가 정상적으로 표시됐다고 해서 모든 기능이 정상 동작 하고 있을까요??&lt;/p&gt;

&lt;p&gt;보기에는 문제가 없어 보이더라도 만약 데이터를 제대로 불러오지 못했다면 일부 기능은 동작 시 오류가 발생할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;상품 페이지 내에 중요 API 중 하나인 쿠폰 데이터를 예로 들겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 쿠폰 데이터 조회
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_product_coupon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;product_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/coupon*****/*********'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'application/json'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Authorization'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;device&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;brandId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16963&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;categoryId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;finalPrice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;*******&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;*******&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;*********&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;****&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;********&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;***&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;먼저, 쿠폰 데이터를 검증하기 위해서 위와 같이 코드 작성을 하고 API를 호출합니다.&lt;/p&gt;

&lt;p&gt;API가 정상적으로 호출 될 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON&lt;/code&gt; 형태의 응답을 받을 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON&lt;/code&gt; 형태로 받은 응답 값에서 &lt;strong&gt;호출한 값과 기대한 값을 비교&lt;/strong&gt;하여 정상적으로 응답이 들어오는지 확인 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;쿠폰 사용 가능 일 수, 쿠폰 명, 쿠폰 타입 등&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;추가로 API의 &lt;strong&gt;응답 시간(속도)&lt;/strong&gt; 을 검증하는 것도 중요했습니다.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;예를들어 음료수 자판기에서 돈을 투입하고 음료를 선택했는데 10분뒤에 음료가 나온다고 생각해보면 다시는 그 자판기를 이용하지 않을 겁니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;응답 시간을 확인하는 것은 생각보다 간단했습니다.&lt;/p&gt;

&lt;p&gt;Requests에 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeout=N&lt;/code&gt;&lt;/strong&gt; 값을 설정해서 제한시간 내에 응답이 오는지 확인 할 수 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_04.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;여기까지 API의 상태와 데이터들까지 확인하고 응답 속도까지 확인 할 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;세-번째-유저시나리오-기반의-api-테스트를-적용하자&quot;&gt;세 번째. 유저시나리오 기반의 API 테스트를 적용하자!&lt;/h2&gt;
&lt;p&gt;테스트를 자동화하려 할 때 &lt;strong&gt;어떤 케이스를 자동화하여 수행하면 효과적인지 판단&lt;/strong&gt;하는 것도 매우 중요한 부분이라고 생각합니다.&lt;/p&gt;

&lt;p&gt;저희는 로그인부터 주문까지의 유저 시나리오에 필요한 주요 API를 테스트에 먼저 적용해 보았습니다.&lt;/p&gt;

&lt;p&gt;코드를 작성하기에 앞서 &lt;strong&gt;개발자들과 협의하여 API 목록을 식별&lt;/strong&gt;하였고, &lt;strong&gt;어떤식으로 자동화를 하면 좋을 지 함께 검토&lt;/strong&gt;하였습니다.&lt;/p&gt;

&lt;p&gt;운영중인 상품으로 테스트를 할 경우 데이터가 바뀌어 에러를 발생 시킬 수 있기 때문에, 지속적이고 반복적인 테스트를 위해 별도의 테스트 전용 상품을 생성하였습니다.&lt;/p&gt;

&lt;p&gt;아래는 유저시나리오 기반의 핵심 API 리스트 중 일부입니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;trenbe.com/login&lt;/li&gt;
  &lt;li&gt;trenbe.com/product&lt;/li&gt;
  &lt;li&gt;trenbe.com/order&lt;/li&gt;
  &lt;li&gt;trenbe.com/payment-init&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;가장 먼저 주문 정보를 가져오기 위해서는 로그인 정보 즉, 토큰 값을 필요로 하기에 아래와 같이 코드를 작성하였습니다.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 로그인 정보 
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;usr_token_get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;login_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/login'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'application/json'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'appInfo'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'email'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'password'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'provider'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'trenbe'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'*********'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'***'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'********'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;****&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;login_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'Token'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;로그인 정보를 가지고 오는데 성공하였다면, 상품 데이터를 조회합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 상품 데이터 조회
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;goodsno&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;product_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/product/**********/******/****'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'application/json'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Authorization'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위에서 조회 한 로그인 정보와 상품 데이터를 이용하여 주문서로 이동하고 주문서 ID를 조회 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;주문서의 경우 주문서가 생성 될 때 마다 주문번호가 달라지기에 주문번호를 가져오는 코드가 필요 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 주문서 ID 조회
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_order_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;get_order_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/order'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'application/json'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Authorization'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;product_data&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_order_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;주문서에 필요한 각각의 API가 호출되고 데이터가 정상적으로 내려 오는지 확인을 하고 최종적으로 모든 결제 정보가 정상적으로 넘어 왔는지 확인을 합니다.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 결제 정보 확인 (토스 가상계좌를 통한 주문 결제)
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;order_payment_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;order_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;base_url&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/order/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/******'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Content-Type'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'application/json'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'Authorization'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'paymentMethod'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'ta'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;'pg'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'TOSS_PAYMENTS'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dumps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;아래와 같이 각각 API의 응답은 정상인지, 데이터들은 잘 넘어 오는지, 제한시간내에 정상적으로 응답 하는지 확인 할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로그인부터 주문까지 유저 시나리오에 필요한 API들을 테스트 해보았습니다.&lt;/p&gt;

&lt;h2 id=&quot;네-번째-github-actions를-통하여-api-테스트-자동화-하기&quot;&gt;네 번째. Github Actions를 통하여 API 테스트 자동화 하기!&lt;/h2&gt;
&lt;p&gt;저희 트렌비는 Selenium을 통한 UI 테스트 자동화가 적용되어 있고, Github Actions를 통해 주기적으로 테스트를 실행시켜 확인 할 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;마찬가지로 API 테스트도 Github Actions를 통해 테스트 자동화를 적용시켜 보았습니다.&lt;/p&gt;

&lt;p&gt;정해진 저장소에 작성한 API 테스트 코드를 commit 하고, 워크플로우 파일을 생성합니다.
워크플로우는 yaml 파일을 통해 설정을 하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows&lt;/code&gt; 폴더 아래에 yaml 파일을 위치시킵니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trenbe_api_test.yml&lt;/code&gt; 을 통해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt; 될 때 마다 해당 워크플로우가 자동으로 실행되게 설정했고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workflow_dispatch&lt;/code&gt;를 통해서 수동 테스트도 가능하게 설정했습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cronjob&lt;/code&gt;을 통해서 원하는 시간에 반복해서 테스트를 수행 할 수도 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;워크플로우가 정상적으로 실행 되면 위와 같이 리스트에 노출 되는 것을 확인 할 수 있습니다.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Run workflow&lt;/code&gt;를 통해서 테스트를 실행 할 수 있고, 가장 최근에 실행 된 작업을 보시면 테스트가 28초만에 완료 된 걸 볼 수 있습니다.&lt;/p&gt;

&lt;p&gt;테스트 로그는 아래와 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;테스트 실패 시 슬랙 메신저와 연동하여 메세지를 통한 알림을 받을 수도 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202211/api_test_automation_15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;마치며&quot;&gt;마치며&lt;/h1&gt;
&lt;p&gt;API 테스트를 활용할 줄 알면 단순히 화면을 보고, 테스트 시나리오대로 검증하는 역할을 넘어서 테스트 수행 시간 단축 및 리소스 감소를 통해 더욱 단순하고 빠르게 일을 해결 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;매뉴얼 테스트, UI 테스트 자동화, API 테스트 자동화를 적절히 분배하여 프로젝트를 검증하거나 배포 전 최종 검증 단계에서 테스트를 진행 할 경우 서비스의 품질을 좀 더 높일 수 있는 기회가 될 것으로 생각합니다. 👊&lt;/p&gt;

&lt;p&gt;앞으로도 더 나은 서비스 품질을 위해 고민하고 노력하겠습니다.&lt;/p&gt;

&lt;p&gt;감사합니다.&lt;/p&gt;</content><author><name>미키</name></author><category term="Python" /><category term="Selenium" /><category term="API" /><category term="Automation" /><summary type="html">들어가며 안녕하세요. QA Engineer 미키입니다.</summary></entry><entry><title type="html">드디어 리뉴얼 하는 날이 오는구나</title><link href="https://trenbe.github.io/2023/01/15/%ED%8A%B8%EB%A0%8C%EB%B9%84-%EB%A6%AC%EB%89%B4%EC%96%BC-%EA%B0%9C%ED%8E%B8%EA%B8%B0.html" rel="alternate" type="text/html" title="드디어 리뉴얼 하는 날이 오는구나" /><published>2023-01-15T15:00:00+00:00</published><updated>2023-01-15T15:00:00+00:00</updated><id>https://trenbe.github.io/2023/01/15/%ED%8A%B8%EB%A0%8C%EB%B9%84%20%EB%A6%AC%EB%89%B4%EC%96%BC%20%EA%B0%9C%ED%8E%B8%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2023/01/15/%ED%8A%B8%EB%A0%8C%EB%B9%84-%EB%A6%AC%EB%89%B4%EC%96%BC-%EA%B0%9C%ED%8E%B8%EA%B8%B0.html">&lt;h2 id=&quot;개편-배경&quot;&gt;개편 배경&lt;/h2&gt;
&lt;p&gt;서비스를 리뉴얼을 하는 작업은 생각보다 많은 수고와 인내가 필요한 작업입니다. 단순 심미적인 만족감뿐 아니라 사용자 고객여정을 파악하여 더 편리한 구매와 탐색 경험을 서비스에 녹여 반영해야 하기 때문이죠.&lt;/p&gt;

&lt;p&gt;그런데도 불구하고 왜 수많은 서비스들은 리뉴얼을 하는 걸까요? 여러 가지 이유가 있겠지만 대부분은 다음과 비슷한 이유를 포함하고 있을 것입니다.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;‘디자인이 오래되었다거나 구성이 복잡하거나, 모바일에 최적화되어있지 않거나, 지표 성과가 좋지 않거나, 사이트가 무겁거나’ …&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;리뉴얼을 하기전에 해야 하는 가장 중요한 작업은 우리 서비스의 현재 상황 분석입니다. 문제점을 명확히 파악해야 개선 목표를 올바르게 설정할 수 있고 의미 있는 결과를 가져올 수 있기 때문입니다.&lt;/p&gt;

&lt;h2 id=&quot;문제-정의&quot;&gt;문제 정의&lt;/h2&gt;
&lt;p&gt;리뉴얼 전에 분석한 트렌비 서비스의 문제점은 크게 4가지가 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_01.png&quot; alt=&quot;&quot; width=&quot;50%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 디자인 시스템의 부재&lt;/strong&gt;로 오랜 시간 조금씩 무너진 디자인 일관성과 비효율적인 업무 방식&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 어려운 탐색구조&lt;/strong&gt;, 뎁스(Depth)가 깊고 복잡함&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.&lt;/strong&gt; 다소&lt;strong&gt;아쉬운 브랜드 정체성(Identity)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. 모바일에 최적화&lt;/strong&gt;되지 않음&lt;/p&gt;

&lt;p&gt;(리뉴얼을 진행하며 함께 작업했던 디자인 시스템 제작기는 할 이야기가 많으니 따로 글을 쓰도록 하겠습니다.😇)&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;서비스-리뉴얼-목표-설정&quot;&gt;서비스 리뉴얼 목표 설정&lt;/h2&gt;
&lt;dl&gt;
  &lt;dt&gt;&lt;br /&gt;&lt;/dt&gt;
  &lt;dt&gt;&lt;strong&gt;디자인 시스템 제작&lt;/strong&gt;&lt;/dt&gt;
  &lt;dd&gt;
    &lt;p&gt;컴포넌트를 만들어 반복되는 작업을 최소화 시키자
또 누가 만들어도 한 사람이 만든 것처럼 보이도록 일관성을 갖추자&lt;/p&gt;
  &lt;/dd&gt;
  &lt;dt&gt;&lt;strong&gt;탐색 고도화&lt;/strong&gt;&lt;/dt&gt;
  &lt;dd&gt;
    &lt;p&gt;편리한 탐색으로 유저가 원하는 상품을 빠르게 찾도록 돕자.
탐색 여정 자체가 즐거운 과정이 되어 유저에게 좋은 경험을 주고 재방문을 유도하자&lt;/p&gt;
  &lt;/dd&gt;
  &lt;dt&gt;&lt;strong&gt;정품에 대한 신뢰도 상승&lt;/strong&gt;&lt;/dt&gt;
  &lt;dd&gt;서비스 디자인과 탐색, 구매 과정에서 트렌비 정품 신뢰도를 높여주는 요소 제공&lt;br /&gt;&lt;br /&gt;&lt;/dd&gt;
  &lt;dt&gt;&lt;strong&gt;데이터 기반의 작업&lt;/strong&gt;&lt;/dt&gt;
  &lt;dd&gt;데이터 활용을 적극적으로 해보자.
본격적인 데이터 수집과 데이터 기반의 설계 하나씩 시작&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;실행&quot;&gt;실행&lt;/h2&gt;
&lt;h3 id=&quot;step-01-가장-많이-쓰이는-개선점이-가장-필요한-부분-먼저&quot;&gt;STEP 01. 가장 많이 쓰이는, 개선점이 가장 필요한 부분 먼저&lt;/h3&gt;
&lt;p&gt;본격적인 문제 해결에 앞서 UX 방법론을 활용한 사용자 조사(User Research)를 진행했습니다. 주요 화면 사용성 테스트 (Usability Test)를 통해 사용자의 특성과 경향성을 분석할 수 있었으며 이를 통해 얻은 인사이트를 토대로 작업의 우선순위를 평가하고 개발팀과 스펙과 범위를 결정하기 위해 여러번의 미팅을 진행했습니다. 몇 년 동안 쌓인 요구사항을 모아 정리하고 기능 추가와 개편 그리고 최신 디자인도 반영하려다 보니 생각보다(?) 목표가 커졌지만 만족스러운 결과를 얻기 위해 전반적인 UI/UX 전략을 재수립하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_02.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;step-02-브랜드-정체성-구체화&quot;&gt;STEP 02. 브랜드 정체성 구체화&lt;/h3&gt;
&lt;p&gt;우리 유저가 갖는 트렌비의 또 다른 아쉬움은 브랜드 정체성이 부족하다는 점이었습니다. 이를 보완하기 위해 요즘 백화점에서의 고객 경험은 어떤지부터 살펴보니 우리가 유저에게 전달하고 싶은 고객 경험과 닮아 있었습니다.&lt;/p&gt;

&lt;p&gt;단순 구매 행위만을 위한 공간이 아닌, 다양한 볼거리를 제공하여 방문 자체가 즐거움이 되는 복합 문화 공간으로써 발전한 요즘 백화점의 모습을 착안하여 쇼핑과 탐색, 재미, 커뮤니티 공간을 모두 어우르는 &lt;strong&gt;Online Luxury Playground&lt;/strong&gt; 의 모습을 지향하고자 했습니다. 오프라인 백화점에서의 친근한 쇼핑 경험을 온라인 서비스에서도 느낄 수 있도록 부드러운 어조와 디자인 요소를 찾아 반영하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_03.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;
&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_04.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;step-03-내외부-전면-개선&quot;&gt;STEP 03. 내외부 전면 개선&lt;/h3&gt;
&lt;p&gt;기존 틀을 유지하면서 디자인을 개선하고 쌓여있던 레거시(Legacy)를 잘 정리하는 것까지 이번 리뉴얼의 핵심 목표로 정했습니다. 정리하고 보니 내/외부적으로 다음과 같은 작업이 필요했습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;내부적 개선:&lt;/strong&gt; 데이터 기반의 UX 기획, 디자인 시스템 작업, 이전 작업물 정리와 피그마 파일링&lt;br /&gt;
&lt;strong&gt;외부적 개선:&lt;/strong&gt; 콘텐츠 정돈, UI 디자인, 디자인 QA, 전후 데이터 평가 및 비교&lt;/p&gt;

&lt;p&gt;이번 리뉴얼이 단편적인 개선 프로젝트로 끝나는 것이 아닌 장기적인 서비스 성장에 보탬이 되는 유의미한 작업이 될 수 있도록 뼈대 작업부터 새로 진행하고자 했습니다.&lt;/p&gt;

&lt;h3 id=&quot;step-04-구현&quot;&gt;STEP 04. 구현&lt;/h3&gt;
&lt;p&gt;비즈니스팀(운영)과 개발팀 그리고 UX팀과 여러번의 미팅을 거쳐 나온 기획을 바탕으로 드디어 구현을 시작했습니다. 구현은 아래와 같은 단계로 진행되었습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(1) 와이어프레임(Wireframe)&lt;/strong&gt;&lt;br /&gt;
초기에는 UX 리서치와 타사/동종업계 서비스의 자료를 수집하고 분석한 후 서비스 배치 구성을 보여주는 와이어프레임(Wireframe)을 작업하여 여러 차례 싱크업 미팅을 거칩니다. 와이어프레임(Wireframe)의 장점은 구체적인 UI 디자인이 들어가기 이전에 서비스의 전반적인 플로우를 빠르게 확인할 수 있고 수정 작업도 빠르게 이루어질 수 있기 때문에 UI/UX 디자이너에게 필수적인 작업이기도 합니다. (그러나 간혹 와이어프레임만을 보고 GUI를 논하는 분들이 있어 골치 아플 때도 있습니다.🥲) 연관 부서와 끊임없이 소통하며 문제가 될 부분, 어색한 부분 등을 초기에 찾아 해결하며 더 나은 산출물을 위해 많은 고민을 하는 단계입니다. 기획 의도를 잘 어필하여 설득하고 납득시키는 부분 또한 대부분 이 단계에서 이루어집니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(2) 디자인&lt;/strong&gt;&lt;br /&gt;
디자인을 하며 중요한 포인트로 잡았던 것은 1. 목적에 맞는 디자인인가 2. 사용하기 쉬운가 이 두 가지였습니다. UT에서 얻은 니즈를 기반으로 우리 서비스 사용자의 편의를 고려하여 설계하였으며 개발 작업 속도와 디자인 일관성 유지를 위해 최대한 디자인 시스템에서 활용 가능한 컴포넌트를 사용하였습니다. GUI는 온라인 백화점으로써의 아이덴티티(Identity) 전달을 위해 오프라인 백화점에서 볼 수 있는 인포메이션 아이콘과 디자인 요소들을 반영하고 최대한 상품이 눈에 잘 띄도록 정제되고 세련된 레이아웃을 구현하는 데 힘을 쏟았습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(3) 개발&lt;/strong&gt;&lt;br /&gt;
디자인을 완료하고 개발 단계로 넘어가면 테스트 서버에 올라온 작업물을 틈틈이 확인하며 작업한 대로 디자인이 잘 구현되었는지를 체크합니다. 제대로 구현되지 않은 부분들이 발견되면 개발팀과 많은 소통을 하며 해결해 나갔습니다. 개발에 대해 잘 알지 못하지만 이해하기 쉽도록 설명해주시는 개발팀 덕분에 작업이 수월했습니다.&lt;/p&gt;

&lt;p&gt;위의 절차대로 기능이 잘 구현되었다면 다음은 숲을 보는 단계입니다. 가능하면 모든 니즈가 한 번에 반영된 결과물이 나오면 좋겠지만 이것은 사실상 불가능에 가깝죠. 특히나 ‘우선 진행하고 평가받은 후에 다시 개선하는’ 애자일(agile) 환경에서는 빠른 실행 과정을 거쳐야하기 때문입니다. 때문에 첫발(First-Step)을 잘 내딛고 나서의 이후 과정도 무척이나 중요합니다. &lt;em&gt;‘다음으로 개선해야 할 것은 무엇인지, 어떻게 해야 좋은 경험을 제공할 수 있는지・・・’&lt;/em&gt; 이렇다 보니 작업자는 기록을 잘 남기는 것 또한 필수적인 역량이라고 생각합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(4) 검수&lt;/strong&gt;&lt;br /&gt;
마무리 단계이기에 더욱 긴장해서 체크해야 하는 검수 단계. 본인이 생각한 대로 잘 구현이 되고 오류가 없는지를 냉정하게 평가하는 시간입니다. 트렌비에서의 검수는 크게 ‘기능 개발적 이슈, 디자인적 이슈, 더 좋은 아이디어 제안, 이슈 검증 완료, 나중에 작업으로 넘기는 후 작업’ 티켓으로 나누어 진행합니다. 불과 몇년 전 신입 때는 사실 검수에 자신이 없어 디자인이 내 손에서 떠나면 그 이후는 개발자들의 역량에 맡기는 면도 없지 않았는데(진실고백) 이제는 디자인 단계만큼 꼼꼼하게 체크하고 간격 하나하나 검수하는 자신을 보니 저와 함께 일하는 개발자분들이 피곤할 수도 있었을 것 같습니다.🙏 (늘 감사하며..)&lt;/p&gt;

&lt;p&gt;유저가 접하게 될 최종 화면을 일차적으로 직접 써보고 테스트하는 과정이다 보니 최대한 냉정하게 바라보려 노력합니다. 검수 단계에 진심이어야 유저의 입장을 간접적으로나마 공감할 수 있는 좋은 깨달음을 얻는 것 같습니다.&lt;/p&gt;

&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;메인홈에서 투데이와 랭킹, 검색, 중고명품이 현재 시점으로 배포가 되었고 데이터로 살펴보자면&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 투데이 메인홈&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_05.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_06.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;상단은 개선 이전, 하단은 개선 이후 시안입니다. 이전의 트렌비 홈화면은 상품 타임딜, 큐레이션, 최저가스캐너, 상품랭킹, 매거진 등 트렌비의 거의 모든 기능을 한 화면에 배치하여 볼거리는 많아보이지만 정돈되지 않아 다소 산만했습니다. 또한 복잡한 레이아웃으로 온라인 명품 플랫폼에 대한 신뢰도가 떨어진다는 VOC도 있었죠.&lt;/p&gt;

&lt;p&gt;개선을 하면서 재정립한 브랜드 아이덴티티를 반영하기 위해 힘썼고 최초 메인 홈에는 기능적인 면보다 상품에 좀 더 포커스 하여 다양한 상품을 충분히 둘러본 후, 관심 있는 메뉴로 넘어가 자연스럽게 탐색할 수 있는 경험을 제공하고자 했습니다. 이전에 혼란스러웠던 시선을 편안하게 해주는 것 역시 하나의 목표이기도 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_07.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;뷰저블 데이터로 살펴본 투데이(메인홈) 화면의 스크롤 및 체류시간입니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;사용자의 도달률 100% 비율이 과거 (CMS 전,후)대 800px 선에서 6,800px로 약 8.5배 이상 증가&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;콘텐츠의 총 길이 이전 대비(5,400px) 이후(23,700px) 약 4.4배 이상 길어진 화면에도 불구하고 최종 콘텐츠까지의 도달 비율 61%에서 66% 약 5%가 증가&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;체류시간 역시 전반적인 화면에서 이전보다 훨씬 증가한 것을 확인할 수 있습니다.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;2. 검색&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_08.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_09.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 역시 상단은 개선 이전, 하단은 개선 이후의 시안입니다. 검색은 이전부터 트렌비에서 가장 클릭율이 높았던 UX로 기존에 최근 검색어, 추천 검색어, 인기 검색어를 활용하되 UI를 정돈하고 불편했던 점을 찾아 개선과 동시에 검색 활용도를 더 높일 수 있도록 기능을 추가하였습니다. 이전에는 최근 검색어 내역을 낱개로 일일이 지워야 하는 불편함이 있었기에 모두 삭제 버튼을 추가하여 여러 번 클릭의 노고를 덜어주었고 기간 내 인기가 급상승한 검색어에 N(New)배지를 추가하여 최신 동향을 살펴볼 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;검색 경험은 자동 완성 검색어를 유지했으나 이전에 브랜드와 연관검색어의 UI가 동일해 구분이 어려웠던 점을 해결하고자 다른 UI 스타일로 구현하여 함께 노출이 되더라도 구분이 가능하도록 수정하였습니다. 추가로 브랜드에는 찜하기 아이콘을 배치하여 관심 브랜드를 빠르게 찜 리스트에 담아 볼 수 있도록 하고 찜하기를 유도하는 UX를 제공했습니다.&lt;/p&gt;

&lt;p&gt;기존에 브랜드 검색은 웹 기반의 UI였기 때문에 모바일에서의 사용이 상당히 불편했습니다. 터치 영역의 확장과 상하 스크롤 변경으로 모바일 경험에 최적화된 UIUX로 개선하고 ABC순/가나순 필터링으로 유저가 찾고자 하는 브랜드를 빠르게 찾을 수 있도록 도왔습니다. 또 브랜드 검색을 검색바 영역으로 재배치하여 ‘검색’이라는 타이틀 내 자연스러운 탐색 경험이 연결될 수 있도록 변경하였습니다. 더불어 인기 브랜드를 제공해 브랜드에 대해 무지한 유저도 길을 잃지 않고 최신 인기 브랜드를 살펴 정보 검색의 폭을 넓힐 수 있도록 하였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202301/trenbe_renewal_10.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;11월쯤 검색 개편 기능이 배포 되었습니다. 검색 개편 직후 전반적인 지표는 좋아졌으나 아이러니하게도 검색에 대한 클릭수는 감소하였습니다. 우리는 이것을 검색바(Search Bar)가 노출이 안되고 아이콘으로 노출되었기 때문이라는 가설을 세워 위의 As-is 검색 아이콘에서 To-be 고정형 서치바로 바꾸어 다시 재배포 했습니다.&lt;/p&gt;

&lt;p&gt;변경 후 수치는 개선 전보다 평균 클릭률이 1만 건 이상 증가하였고, 검색 결과 PV의 평균 값 또한 소폭 상승하였습니다. 그러나 일정하지 않은 PV 폭으로 앞으로의 동향을 좀 더 살펴볼 필요가 있다고 판단하였습니다.&lt;/p&gt;

&lt;p&gt;이처럼 눈에 띄는 좋은 성과로 만족스러운 결과를 얻은 것도 있는 반면 크지 않은 변화에 애매한 성과를 얻은 것도 있습니다. 그러나 이번 검색 개편은 기본적으로 트렌비에서 가장 높은 이용률을 자랑했던 UX였기 때문에 지표가 떨어지지 않고 유지된 것만으로도 꽤 만족스러운 개선 과정을 거쳤다고 생각합니다.&lt;/p&gt;

&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;그렇게 리뉴얼은 시작이 되었고 배포가 하나씩 이루어지고 있습니다. 황무지에서 시작하다 보니 디자인 시스템 제작부터 서비스 리뉴얼까지 이것저것 준비 기간만 약 1년이 걸렸고 비즈니스의 요구사항에 따라 우선순위에 밀리기도 했지만 우여곡절 끝에 프로젝트가 진행되었기 때문인지 남다른 애정이 갑니다. 다양한 시행착오를 겪으며 배운 점과 느낀 점 또한 참 많았습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;사실 이제 시작이다. 2차 요건을 잘 정리하고 적용해서 서비스를 더 개선시키자.&lt;/li&gt;
  &lt;li&gt;히스토리를 잘 남기자 (미팅 내용, 추후 기능 개선건, 변경 사항 등)&lt;/li&gt;
  &lt;li&gt;추진력을 강화해서 팀원에게 좋은 영향력과 시너지를 주자&lt;/li&gt;
  &lt;li&gt;그때 그때 잘 정리하자 (이전 작업 레거시만 정리하는데 꽤 많은 시간을 할애했다. 최신 버전으로 잘 업데이트하고 디자인 시스템 관리도 꾸준히 하자)&lt;/li&gt;
  &lt;li&gt;데이터와 친해지자 (UI/UX 디자이너에게 데이터는 든든한 빽이다.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이번 경험을 계기로 다른 프로젝트를 수행할 때는 분명 이전보다 순조로울 것이라는 기대를 가지며 리뉴얼 개편기 이야기를 마치려 합니다. 지치지 않고 함께해 준 개발팀과 무엇보다 처음부터 끝까지 함께 걸어준 UX팀에게 깊은 감사의 인사를 드리고 싶습니다. 트렌비의 히어로- 스토어팀이 있어서 참 든든합니다! 앞으로도 남은 리뉴얼 같이 힘내봅시다.💪&lt;/p&gt;</content><author><name>유하</name></author><category term="uxui" /><category term="renewal" /><category term="designsystem" /><category term="component" /><summary type="html">개편 배경 서비스를 리뉴얼을 하는 작업은 생각보다 많은 수고와 인내가 필요한 작업입니다. 단순 심미적인 만족감뿐 아니라 사용자 고객여정을 파악하여 더 편리한 구매와 탐색 경험을 서비스에 녹여 반영해야 하기 때문이죠.</summary></entry><entry><title type="html">트렌비 리뷰 서비스의 성능 개선기</title><link href="https://trenbe.github.io/2022/09/14/%ED%8A%B8%EB%A0%8C%EB%B9%84-%EB%A6%AC%EB%B7%B0-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0.html" rel="alternate" type="text/html" title="트렌비 리뷰 서비스의 성능 개선기" /><published>2022-09-14T15:00:00+00:00</published><updated>2022-09-14T15:00:00+00:00</updated><id>https://trenbe.github.io/2022/09/14/%ED%8A%B8%EB%A0%8C%EB%B9%84%20%EB%A6%AC%EB%B7%B0%20%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98%20%EC%84%B1%EB%8A%A5%20%EA%B0%9C%EC%84%A0%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2022/09/14/%ED%8A%B8%EB%A0%8C%EB%B9%84-%EB%A6%AC%EB%B7%B0-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0.html">&lt;h1 id=&quot;들어가며&quot;&gt;들어가며&lt;/h1&gt;
&lt;p&gt;안녕하세요, 트렌비 백엔드 개발자 도현입니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 트렌비 &lt;strong&gt;리뷰 서비스 (review-service)&lt;/strong&gt; 의 성능을 개선하게 된 이야기를 해보고자 합니다.&lt;/p&gt;

&lt;h1 id=&quot;문제-상황&quot;&gt;문제 상황&lt;/h1&gt;
&lt;p&gt;작년 11월경 리뷰 서비스가 런칭되고 시간이 지남에 따라 비즈니스가 점점 고도화되었고, 사용자와 트래픽이 증가함에 따라 아래와 같은 시스템 알럿이 발생하기 시작했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_01.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;발생하는 원인은 다양했는데, 다음 2가지 유형이 가장 빈번했습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;DB 커넥션풀 고갈 이슈&lt;/li&gt;
  &lt;li&gt;API 타임아웃 이슈&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;보통 이런 알럿들은 5분 이내로 안정화가 되었지만 &lt;strong&gt;반복적으로 발생하며&lt;/strong&gt; 사용자 경험을 저해시켜 개선이 필요했습니다.&lt;/p&gt;

&lt;p&gt;게다가, 리뷰 서비스의 API 응답 속도도 그닥 빠르지 않았는데 p90 API 응답 속도는 약 &lt;strong&gt;200ms ~ 250ms&lt;/strong&gt; 을 보여주고 있었습니다. (아래 그림 참조)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_02.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;앱푸시 등으로 인해 트래픽이 증가하면 더 느려지기도 했습니다.&lt;/p&gt;

&lt;p&gt;200ms ~ 250ms 의 응답속도가 적절한 수준인지 판단하기 위해 “&lt;em&gt;Acceptable API Response Time&lt;/em&gt;” 라는 키워드로 조사를 해봤습니다.&lt;/p&gt;

&lt;p&gt;아래 문서에 따르면 &lt;strong&gt;0.1초 (100ms) 이내의&lt;/strong&gt; 응답 속도가 즉각적인 응답으로 인식이 된다고 합니다. 이를 기준으로 리뷰 서비스는 즉각적인 응답을 주지 못하고 있던 상태라고 판단할 수 있었습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;&lt;strong&gt;A response time of about 0.1 seconds offers users an “instant” response, with no interruption.&lt;/strong&gt;
A one-second response time is generally the maximum acceptable limit, as users still likely won’t notice a delay.
Anything more than one second is problematic…
&lt;a href=&quot;https://www.dnsstuff.com/response-time-monitoring&quot;&gt;https://www.dnsstuff.com/response-time-monitoring&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;h1 id=&quot;목표&quot;&gt;목표&lt;/h1&gt;
&lt;p&gt;위와 같은 문제를 해결하기 위해 2가지 목표를 설정했습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;API 응답 속도를 &lt;strong&gt;100ms&lt;/strong&gt; 이하로 줄이자!&lt;/li&gt;
  &lt;li&gt;리뷰 서비스에서 발생하는 &lt;strong&gt;알럿을 해결&lt;/strong&gt;하자!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;설정한 2가지 목표를 달성하기 위해 진행했던 다섯가지 이야기를 해보고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;이야기-1-n번의-api-호출을-1번으로-줄일-수-있을까&quot;&gt;이야기 1. n번의 API 호출을 1번으로 줄일 수 있을까?&lt;/h2&gt;

&lt;p&gt;리뷰 서비스에는 다음과 같은 로직이 있었습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPurchaseOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderItemIds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderItemIds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getOption&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Collectors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;n 개의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 ID&lt;/code&gt; (orderItemIds) 에 대한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; (purchaseOption) 을 조회하기 위해 주문 서비스 (order-service) 를 n 번 호출하는 로직입니다.&lt;/p&gt;

&lt;p&gt;한번의 API 호출은 약 1 ~ 2ms 정도의 통신 비용이 소요되었는데, 조회하고자 하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 ID&lt;/code&gt; 가 많아지면 많아질수록 통신 비용이 증가하게 되었습니다.&lt;/p&gt;

&lt;p&gt;보통 10개정도의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 ID&lt;/code&gt; 에 대한 조회를 하고 있었고 따라서 기본적으로 &lt;strong&gt;10 ~ 20ms&lt;/strong&gt; 의 통신 비용이 발생했습니다.&lt;/p&gt;

&lt;p&gt;API 응답 속도를 100ms 이하로 줄이고자 하는 목표에서 20ms 은 작지 않은 비중이었습니다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 주문 서비스에는 n 개의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 ID&lt;/code&gt; 를 파라미터로 받는 별도 API 를 만들고, 리뷰 서비스에서는 한번의 API 호출만 하도록 변경했습니다.&lt;/p&gt;

&lt;p&gt;변경된 로직은 아래와 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getPurchaseOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderItemIds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderItemIds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 n 번의 호출이 발생하던 부분을 1번으로 개선함으로써 &lt;strong&gt;200ms ~ 250ms&lt;/strong&gt; 을 보이던 p90 응답 속도는 &lt;strong&gt;100ms&lt;/strong&gt; 정도로 빨라졌습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_03.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;이야기-2-굳이-order-service-를-호출해야-할까&quot;&gt;이야기 2. 굳이 order-service 를 호출해야 할까?&lt;/h2&gt;
&lt;p&gt;이야기 1 에서 공유했던 성능 개선을 하고 나서 이런 생각이 들었습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“&lt;em&gt;주문 서비스로부터 어떤 데이터를 호출하는걸까? 리뷰 서비스 내부적으로 관리할 수 있는 데이터라면 굳이 호출을 하지 않아도 될 것 같은데…&lt;/em&gt;”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;확인 결과, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;주문 ID&lt;/code&gt; 에 맵핑된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; 데이터만을 조회하고 있었습니다. (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; 데이터는 리뷰 도메인에서 관리하는 데이터가 아니었습니다.)&lt;/p&gt;

&lt;p&gt;하지만 이 데이터만 조회하기 위해 주문 서비스를 호출하는 것은 불필요하다고 판단했습니다.&lt;/p&gt;

&lt;p&gt;그 이유는 하나의 리뷰가 작성될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; 데이터를 파라미터로 함께 전달받게 되는데, 이 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; 데이터를 리뷰 도메인에 저장할 수 있기 때문입니다.&lt;/p&gt;

&lt;p&gt;따라서 리뷰 서비스 내부에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;구매 옵션&lt;/code&gt; 데이터도 저장 및 관리함으로써 주문 서비스로의 API 호출을 줄일 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이 개선을 통해 &lt;strong&gt;100ms&lt;/strong&gt; 을 보이던 p90 응답 속도는 &lt;strong&gt;30ms ~ 40ms&lt;/strong&gt; 까지 빨라졌습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_04.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;외부 서비스와의 의존성을 끊음으로써 API 의 응답 속도를 빠르게 개선할 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;이야기-3-could-not-open-jpa-entitymanager-에러-해결하기&quot;&gt;이야기 3. “Could not open JPA EntityManager” 에러 해결하기&lt;/h2&gt;
&lt;p&gt;앱푸시나 카톡 채널 메시지 등으로 인해 짧은 시간 내에 많은 트래픽이 발생하는 상황에서 리뷰 서비스 역시 부하를 받게 되어 아래와 같이 많은 시스템 에러 알럿이 발생했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_05.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;보통 시스템 에러 알럿이 발생하게 되면 &lt;strong&gt;로그&lt;/strong&gt;를 가장 먼저 확인해보는데요.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_06.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;대부분의 로그는 아래와 같이 &lt;strong&gt;“Could not open JPA EntityManager for transaction”&lt;/strong&gt; 라는 에러였습니다.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Unknown Exception.
message=**Could not open JPA EntityManager for transaction;**
nested exception is org.hibernate.exception.**JDBCConnectionException: Unable to acquire JDBC Connection**
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;동시간대의 시스템 메트릭도 같이 확인을 해보았는데, 아래 그림과 같이 &lt;strong&gt;“DB 커넥션 풀”&lt;/strong&gt; 에 대한 시스템 메트릭이 튀는 것을 확인할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_07.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;에러 로그와 시스템 메트릭을 함께 살펴보았을 때, &lt;strong&gt;DB 와 관련된 트랜잭션 이슈&lt;/strong&gt;라는 것을 인지할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;확인 결과, 리뷰 서비스에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt; 이 걸린 하나의 메소드 내에서 &lt;strong&gt;외부 API 호출과 내부 DB 접근&lt;/strong&gt;이 동시에 수행되고 있었습니다.&lt;/p&gt;

&lt;p&gt;아래는 예제 코드입니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 1. 외부 API 호출&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;externalId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;externalService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getExternalDataById&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 2. 내부 DB 접근&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;internalId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;internalRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getInternalDataById&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;externalId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 3. 기타 비즈니스 로직 수행&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;internalId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt; 어노테이션은 메소드가 수행될 때 DB 커넥션 풀로부터 커넥션을 하나 가져오게 되고 완료되면 커넥션을 반납을 하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt; 이 걸린 메소드가 빠르게 수행된다면 문제가 되지 않지만, 메소드 내에서 수행 시간이 오래 걸린다면 가져온 커넥션을 반납하지 못하는 상황이 됩니다.&lt;/p&gt;

&lt;p&gt;따라서 커넥션이 고갈될 수 있고 커넥션을 필요로하는 그 다음 요청부터는 실패하게 됩니다.&lt;/p&gt;

&lt;p&gt;리뷰 서비스에서도 외부 API 의 지연이 종종 발생하고 있었습니다.&lt;/p&gt;

&lt;p&gt;커넥션을 가져온 상태에서 외부 API 의 응답을 기다리는 상황이 발생했고, 결국 커넥션이 고갈되어 그 다음 요청부터는 위와 같은 &lt;strong&gt;“Unable to acquire JDBC connection”&lt;/strong&gt; 과 같은 에러가 발생하게 되었습니다.&lt;/p&gt;

&lt;p&gt;관련 내용에 대해서 알아보던 중 아래와 같은 가이드 글을 찾을 수 있었습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;하나의 트랜잭션 내에서 DB I/O 와 기타 I/O 가 함께 있는 경우는 bad smell 이다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Mixing the database I/O with other types of I/O in a transactional context is a bad smell. So, the first solution for these sorts of problems is to separate these types of I/O altogether&lt;/strong&gt;
. If for whatever reason we can’t separate them, we can still use Spring APIs to manage transactions manually.&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://www.baeldung.com/spring-programmatic-transaction-management&quot;&gt;&lt;em&gt;https://www.baeldung.com/spring-programmatic-transaction-management&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;가이드 글에서 안내된 것처럼 가장 먼저 시도해볼 수 있는 해결책은 &lt;strong&gt;트랜잭션을 분리하는 것&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;p&gt;따라서 아래와 같이 트랜잭션을 분리하였고 외부 API 에서 지연이 발생하더라도 DB 커넥션이 고갈되지 않도록 개선했습니다.&lt;/p&gt;

&lt;p&gt;트랜잭션의 범위를 명확히 구분하기 위해 &lt;strong&gt;“Transaction Template”&lt;/strong&gt; 을 활용했습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 1. 외부 API 호출&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;externalId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getExternalData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 2. 내부 DB 접근&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;internalId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transactionTemplate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getInternalData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;externalId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 3. 기타 비즈니스 로직 수행&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;internalId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getExternalData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;externalService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getExternalDataById&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getInternalData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;internalRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getInternalDataById&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같이 트랜잭션을 분리했더니 트래픽이 많이 증가해도 DB 커넥션 풀 고갈은 더 이상 발생되지 않았습니다.&lt;/p&gt;

&lt;p&gt;이를 통해 시스템을 좀 더 안정적으로 유지할 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;이야기-4-성능이-안-좋은-외부-api-에-의존하고-있으면-어떻게-해야-할까&quot;&gt;이야기 4. 성능이 안 좋은 외부 API 에 의존하고 있으면 어떻게 해야 할까?&lt;/h2&gt;

&lt;p&gt;리뷰 서비스는 중고 상품에 대한 데이터를 조회하기 위해 상품 서비스를 지속적으로 호출하고 있었습니다.&lt;/p&gt;

&lt;p&gt;상품 서비스는 종종 API 타임아웃이 발생했고 리뷰 서비스는 응답을 제대로 받지 못했습니다.&lt;/p&gt;

&lt;p&gt;리뷰 서비스에서는 별다른 정책이 설정되어 있지 않아 이런 경우엔 똑같이 API 타임아웃을 발생시키고 있었고 프론트까지 영향을 받았습니다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 저희는 &lt;strong&gt;캐시&lt;/strong&gt;를 적용해보기로 했습니다.&lt;/p&gt;

&lt;p&gt;그 이유는 &lt;strong&gt;리뷰 서비스에서는 거의 동일한 중고 상품에 대한 데이터를 필요로 했고, 그 상품 데이터는 빈번하게 변경되는 데이터가 아니었습니다&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;따라서 리뷰 서비스에서 중고 상품에 대한 캐시 레이어를 설정하고 불안정한 상품 서비스로의 호출을 최소화하는 방향으로 개선했습니다.&lt;/p&gt;

&lt;p&gt;아래처럼 레디스 캐시를 적용하여 매번 상품 서비스를 호출하는 것이 아니라 캐시 데이터를 참조하도록 변경했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_08.jpeg&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;아래 그림은 캐시를 적용하기 전/후의 모습인데, &lt;strong&gt;노란색&lt;/strong&gt; 선으로 표시된 부분이 상품 서비스의 API 응답 속도입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_09.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;적용하기 전에는 노란색 선의 Spike 가 종종 발생했고 이로 인해 리뷰 서비스에서도 API 타임아웃 에러가 발생했습니다.&lt;/p&gt;

&lt;p&gt;적용 후에는 기존에 발생하던 Spike 는 거의 발생하지 않게 되었고 보다 안정적으로 시스템을 운영할 수 있게 되었습니다.&lt;/p&gt;

&lt;h2 id=&quot;이야기-5-간단한-쿼리들이-왜-느리지&quot;&gt;이야기 5. 간단한 쿼리들이 왜 느리지?&lt;/h2&gt;
&lt;p&gt;트렌비에서는 시스템의 메트릭을 모니터링하는 툴 중 하나로 “핀포인트”를 활용하고 있습니다.&lt;/p&gt;

&lt;p&gt;자세한 내용은 아래 링크를 참고해주세요.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://tech.trenbe.com/2022/02/22/pinpoint.html&quot;&gt;&lt;em&gt;https://tech.trenbe.com/2022/02/22/pinpoint.html&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;핀포인트를 활용하면 하나의 API 요청이 어떻게 구성되어 있는지 확인할 수 있고 각각의 구성 요소에 대한 응답 속도를 파악할 수 있습니다.&lt;/p&gt;

&lt;p&gt;아래 그림은 핀포인트 화면 중 일부분을 캡쳐한 화면인데, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getProductDetailBy(Long goodsno)&lt;/code&gt; 라는 메소드가 &lt;strong&gt;28ms&lt;/strong&gt; 이 소요된 점을 확인할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/review_api_performance_10.png&quot; alt=&quot;&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;핀포인트를 활용하여 간단한 역할을 하는 API 요청이 생각보다 느린 경우를 발견할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;간단한 조건에 대한 조회 쿼리였는데도 불구하고 조회 속도가 약 600ms 의 쿼리 수행 시간이 소요되고 있었습니다.&lt;/p&gt;

&lt;p&gt;문제의 쿼리는 다음과 같았습니다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;review&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order_item_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 쿼리는 리뷰를 작성하는 API 에서 &lt;strong&gt;주문 상품에 대해 이미 리뷰가 작성되었는지를 판단하기 위해&lt;/strong&gt; 활용하는 쿼리입니다.&lt;/p&gt;

&lt;p&gt;주문 상품에 대해 이미 리뷰가 작성되었다면, 다시 작성할 수 없기 때문입니다.&lt;/p&gt;

&lt;p&gt;원인을 파악해본 결과 &lt;strong&gt;order_item_id&lt;/strong&gt; 컬럼에 &lt;strong&gt;인덱스가 걸려있지 않아서 조회 속도가 느렸습니다&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;보통 데이터를 DB 로부터 조회할 때, 조회 조건에 대한 인덱스를 걸게 되는데 이 경우에는 누락되어 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;order_item_id 컬럼에 대한 인덱스를 추가함으로써&lt;/strong&gt; 간단하게 성능을 개선할 수 있었습니다.&lt;/p&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며&lt;/h1&gt;

&lt;p&gt;지금까지 트렌비 리뷰 서비스의 API 응답 속도를 빠르게 개선하고 시스템 알럿을 줄이는 방법들에 대해서 알아보았습니다.&lt;/p&gt;

&lt;p&gt;200ms 에서 250ms 를 보이던 p90 응답 속도는 현재 30ms 에서 40ms 수준에서 안정적으로 유지되고 있습니다.&lt;/p&gt;

&lt;p&gt;또한, DB 커넥션 풀 고갈로 인한 시스템 알럿은 더 이상 발생하고 있지 않습니다.&lt;/p&gt;

&lt;p&gt;일반적인 방법은 아닐 수 있지만 위에서 소개했던 방법들을 참고해서 시스템 성능 개선의 첫 단계로 활용할 수 있으면 좋겠습니다.&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.dnsstuff.com/response-time-monitoring&quot;&gt;Acceptable Response Time&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-programmatic-transaction-management&quot;&gt;Programmatic Transaction Management&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://tech.trenbe.com/2022/02/22/pinpoint.html&quot;&gt;핀포인트 소개&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>도현</name></author><category term="performance" /><category term="engineering" /><category term="review_service" /><category term="성능개선" /><summary type="html">들어가며 안녕하세요, 트렌비 백엔드 개발자 도현입니다.</summary></entry><entry><title type="html">구글 시트(GoogleSheet) Apps Script 소개</title><link href="https://trenbe.github.io/2022/08/16/%EA%B5%AC%EA%B8%80%EC%8B%9C%ED%8A%B8(GoogleSheet)-Apps-Script-%EC%86%8C%EA%B0%9C.html" rel="alternate" type="text/html" title="구글 시트(GoogleSheet) Apps Script 소개" /><published>2022-08-16T01:00:00+00:00</published><updated>2022-08-16T01:00:00+00:00</updated><id>https://trenbe.github.io/2022/08/16/%EA%B5%AC%EA%B8%80%EC%8B%9C%ED%8A%B8(GoogleSheet)%20Apps%20Script%20%EC%86%8C%EA%B0%9C</id><content type="html" xml:base="https://trenbe.github.io/2022/08/16/%EA%B5%AC%EA%B8%80%EC%8B%9C%ED%8A%B8(GoogleSheet)-Apps-Script-%EC%86%8C%EA%B0%9C.html">&lt;h2 id=&quot;들어가며&quot;&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요. 트렌봇 개발팀 타노스입니다.&lt;/p&gt;

&lt;p&gt;트렌봇 개발팀은 세계 곳곳에서 상품 정보를 수집하고 정제하여 트렌비 서비스나 운영 페이지에 제공하는 역할을 담당하고 있습니다.&lt;/p&gt;

&lt;p&gt;각 상품의 브랜드, 색상, 구매옵션, 가격, 세일정보 등 여러가지 정보들을 관리하고 이와 관련된 다양한 요청을 받아 이를 운영에 반영해 드리고 있습니다.&lt;/p&gt;

&lt;p&gt;예를 들면 특정 판매처의 상품들은 10%의 세일가를 적용하거나 혹은 특정 상품들을 ‘아울렛’ 같은 특수한 카테고리에 노출시키는 등의 요구사항입니다.&lt;/p&gt;

&lt;p&gt;이러한 다양한 요구사항을 모두 수용할 수 있는 우주최강 관리툴을 제공해드리고 싶지만, 아쉽게도 우주최강 관리툴은 아직 개발중입니다…..&lt;/p&gt;

&lt;p&gt;오늘은 위와 같은 상황에서 간편하게 데이터를 관리할 수 있게 하는 구글 시트(GoogleSheet)의 Apps Script를 소개하고자 합니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;빠르게-적용해주세요&quot;&gt;빠르게 적용해주세요!&lt;/h2&gt;
&lt;p&gt;상품 정보는 대부분 다 중요하지만 그 중에서도 타이밍이 중요한 것 들이 있습니다.&lt;/p&gt;

&lt;p&gt;세일 같은 경우 특별한 기간에만 적용되거나 할인율이 변하기 때문에 이런 정보들의 경우 적용이 너무 늦을 경우 의미가 없어지게 됩니다.&lt;/p&gt;

&lt;p&gt;운영팀에서는 빠른 처리를 위해 세일 등 중요한 정보들을 구글 시트로 정리해서 전달해 주시곤 합니다.&lt;/p&gt;

&lt;p&gt;사실 구글 시트에 정리된 자료들은 그 자체로 데이터베이스와 다를 바 없기 때문에 이 구글 시트에 Apps Script를 적용하면 입력된 데이터들을 곧바로 활용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;Apps Script를 구글 시트에 적용하면 ‘Custom menu’, ‘Dialog’, ‘Sidebar’등을 만들어 붙일 수 있고, 직접 작성한 자바스크립트 함수들을 실행할 수 있습니다.&lt;/p&gt;

&lt;p&gt;구글 시트에서 제공하는 대부분의 기능들은 Apps Script에서 제어가 가능합니다.&lt;/p&gt;

&lt;p&gt;Google 계정을 통한 구글 시트파일의 권한 관리나 히스토리 관리가 가능하다는 점, 사용자가 익숙한 엑셀UI를 사용할 수 있다는 것도 상당한 이점입니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;구글-시트--준비-및-apps-script-생성&quot;&gt;구글 시트  준비 및 Apps Script 생성&lt;/h2&gt;

&lt;p&gt;예를 들어 상품번호별로 별도의 할인율을 기록하고 상품 시스템의 API를 통해 기록한 할인율을 반영하는 상황을 가정 해 보겠습니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;먼저 구글 시트에 접속해서 새 파일을 만들고 데이터도 입력합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript2.png&quot; alt=&quot;새 파일 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;여기까지가 일반적으로 구글 시트를 사용하는 단계입니다.&lt;/p&gt;

&lt;p&gt;이제부터 여기에 Apps Script를 사용하여 커스텀 기능을 추가해보겠습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;상단 메뉴의 Extensions -&amp;gt; Apps Script 를 통해 Apps Script 를 생성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript3.png&quot; alt=&quot;앱 스크립트 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript5.png&quot; alt=&quot;앱 스크립트 메뉴&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Apps Script 탭이 자동으로 열리게 되는데 좌측 메뉴를 살펴보면&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Overview : 전반적인 상태를 살펴볼 수 있습니다. 기본정보 및 배포현황, 현재 적용된 권한 등을 확인할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;Editor : 포함된 파일들의 관리, 수정이 가능하고 기본적인 테스트 등을 수행할 수 있습니다. 배포된 타 Apps Script를 라이브러리 형태로 불러오거나 Google에서 제공하는 다른 서비스와의 연결도 관리합니다.&lt;/li&gt;
  &lt;li&gt;Triggers : Apps Script내의 특정 함수를 호출하는 Trigger를 추가 할 수 있습니다. 시트 내에서 발생하는 이벤트, 반복적인 이벤트, Calendar를 통한 이벤트 등을 설정할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;Executions : 실제 Apps Script에서 실행된 함수들의 로그를 확인할 수 있습니다. 디버깅을 위한 용도로 사용할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;Project Settings : 프로젝트에 대한 설정이 가능합니다. 기본 셋팅 몇가지와 Apps Script의 ID, Google Cloud Platform(GCP)프로젝트 설정 등을 볼 수있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;생성된 Apps Script는 현재 오픈되어있는 구글 시트와 자동으로 연결됩니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;기본적으로 현재 시트와 1:1 관계로 연결되지만 배포(Deploy)를 수행하면 다른 시트에서 배포된 Apps Script를 사용할 수도 있습니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;그럼 이제 코딩 작업을 진행해보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;sidebar-만들기&quot;&gt;SideBar 만들기&lt;/h2&gt;
&lt;p&gt;좌측 Editor 메뉴에서 기본적으로 제공하는 Code.gs파일을 수정해보겠습니다.&lt;/p&gt;

&lt;p&gt;[Code.gs]&lt;/p&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onOpen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;addMenu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;addMenu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createMenu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;트렌비&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;할인율&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;makeSideBar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addToUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;makeSideBar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sidebar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;HtmlService&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createTemplateFromFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;sidebar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    
  &lt;span class=&quot;nx&quot;&gt;sidebar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;상품 할인율 예제&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      
  &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Or DocumentApp or SlidesApp or FormApp.&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showSidebar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sidebar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onOpen&lt;/code&gt; 함수가 이 Apps Script의 도입부입니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onOpen&lt;/code&gt; 함수는 별도의 설정 없이도 자동으로 실행되지만 Triggers 메뉴에서 시트 로드 시점에 지정한 함수를 실행하도록 추가 할 수도 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addMenu&lt;/code&gt;함수에서는 커스텀 메뉴를 추가하고 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpreadsheetApp&lt;/code&gt;객체를 사용해 상단 메뉴에 ‘트렌비’를 추가 하고 ‘할인율’ 하위 메뉴를 만들어줍니다.&lt;/p&gt;

&lt;p&gt;‘할인율’ 메뉴를 선택하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;makeSideBar&lt;/code&gt; 함수가 실행되도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;makeSideBar&lt;/code&gt;를 입력하고 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;makeSideBar&lt;/code&gt; 함수에서는 사이드 바를 생성하고 UI에 추가해줍니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HtmlService&lt;/code&gt;를 사용해 ‘sidebar’ 파일로부터 사이드바 UI 객체를 생성하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpreadsheetApp&lt;/code&gt;를 이용해 화면에 사이드바를 표시해줍니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createTemplateFromFile(&quot;sidebar&quot;)&lt;/code&gt;에서 참조하는 파일이 아직 없으니 이번에는 파일을 추가해줍니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Files 우측에 있는 ‘+’ 버튼을 누르고 html파일을 추가해주세요.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript6.png&quot; alt=&quot;sidebar 파일 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;파일 이름을 sidebar 로 수정해주고 내용을 추가해줍니다.&lt;/p&gt;

&lt;p&gt;[sidebar.html]&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;base&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;_top&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;applyDiscountRate&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;할인율 적용&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;사이드바의 파일 내용은 Html 형식입니다. Html에는 나중에 시트의 데이터를 추출하여 적용하기 위한 버튼을 하나 추가합니다.&lt;/p&gt;

&lt;p&gt;이제 사이드바를 표시하기 위한 작업은 마무리 되었으니 저장하고 (단축키는 Ctrl+s) 시트 탭으로 돌아가서 새로고침을 한번 눌러보죠.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript7.png&quot; alt=&quot;트렌비 메뉴가 추가된 시트&quot; /&gt;&lt;/p&gt;

&lt;p&gt;상단 메뉴에 ‘트렌비’가 추가되었습니다. 추가한 메뉴를 한번 눌러보면…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript8.png&quot; alt=&quot;권한요청&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이렇게 사용자에게 권한을 요청하는 팝업이 표시됩니다. 현재 로그인한 계정으로 처음 실행했기 때문에 권한을 요청하게 됩니다.&lt;/p&gt;

&lt;p&gt;‘Continue’ 버튼을 누르면 구글 계정연결 과정이 안내되고 권한을 승인하는 프로세스가 진행됩니다.&lt;/p&gt;

&lt;p&gt;모든 프로세스가 완료된 후 ‘트렌비 -&amp;gt; 할인율’ 메뉴를 다시 누르면 이제 우측에 사이드 바가 표시됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript9.png&quot; alt=&quot;사이드바&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;몇가지 주의사항들&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onOpen&lt;/code&gt;함수에서 곧바로 사이드메뉴를 추가하지 않고 메뉴를 생성하는 과정을 거친 이유는 조금전의 권한요청을 위함입니다. 사용자의 직접적인 행위를 통하지 않으면 권한을 추가하라는 팝업이 뜨지 않고 ‘권한없음’ 오류만 발생하게 됩니다.&lt;/li&gt;
    &lt;li&gt;간혹 권한요청을 통해 권한을 부여했음에도 동작이 실행되지 않고 ‘권한없음’오류가 발생하는 경우가 있는데 이건 현재 웹 브라우저에서 여러개의 구글 아이디가 동시에 로그인 되어있는 상태일 확률이 큽니다. Gmail은 A계정, 구글 시트는 B계정 등… 이미 알려진 이슈이며 별도의 해결책은 아직 없습니다. 한 개의 계정으로 로그인 해야만 정상적으로 동작합니다.&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://developers.google.com/apps-script/guides/support/troubleshooting#issues_with_multiple_google_accounts&quot;&gt;권한 관련 알려진 문제점 확인 링크 : https://developers.google.com/apps-script/guides/support/troubleshooting#issues_with_multiple_google_accounts&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;사이드 메뉴를 확인 했으니 이제 ‘할인율 적용’ 버튼에 이벤트를 연결해보겠습니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;event-연결&quot;&gt;Event 연결&lt;/h2&gt;
&lt;p&gt;사이드바가 html형식으로 되어있으니 sidebar.html 안에 버튼 이벤트를 추가해줍니다.&lt;/p&gt;

&lt;p&gt;[sidebar.html]&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;base&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;_top&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;applyDiscountRate&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;할인율 적용&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onclick=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;applyDiscountRate();&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;applyDiscountRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;google&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withSuccessHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendDiscountResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendDiscountRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sendDiscountResult&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;‘할인율 적용’ 버튼에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onclick&lt;/code&gt; 을 추가해주고 스크립트도 추가 해줍니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google.script.run&lt;/code&gt; 을 통해 Code.gs 내부에 있는 함수를 호출할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.withSuccessHandler(sendDiscountResult)&lt;/code&gt; 를 통해 성공시 이벤트 핸들러를 추가할 수 있습니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;withFailureHandler(function)&lt;/code&gt; 는 생략되어 있지만 실패시 핸들러도 추가할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sendDiscountRate()&lt;/code&gt;가 Code.gs에서 호출될 함수입니다. 아직 작성되지 않았으니 미리 추가해줍니다.&lt;/p&gt;

&lt;p&gt;[Code.gs]&lt;/p&gt;
&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onOpen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;addMenu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;addMenu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sendDiscountRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sheetName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getActiveSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sheetId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;userEmail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getEffectiveUser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserLoginId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

  
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getActiveSheet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getSheetValues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;22&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;


  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;updateApi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://api-example.trenbe.com/product/discountrate&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Basic &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Utilities&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;base64Encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;USERNAME&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PASSWORD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;updateOptions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;muteHttpExceptions&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;worksheetName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sheetName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;sheetId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;userEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; 
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UrlFetchApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;updateOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;  

  &lt;span class=&quot;nx&quot;&gt;SpreadsheetApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`할인율이 적용되었습니다.`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendDiscountRate&lt;/code&gt; 함수롤 보면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpreadsheetApp&lt;/code&gt;를 통해 시트에 대한 각종 정보를 가져옵니다.&lt;/p&gt;

&lt;p&gt;예시에는 없지만 읽어오기 외에 쓰기에 대한 각종 기능들을 사용할 수도 있으며 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UrlFetchApp&lt;/code&gt;을 통해 외부 rest api 호출도 가능합니다.&lt;/p&gt;

&lt;p&gt;이를 통해 원하는 api와 연계하여 사용이 가능하며 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpreadsheetApp&lt;/code&gt;을 통해 화면에 안내 다이얼로그를 띄울 수도 있습니다.&lt;/p&gt;

&lt;p&gt;마지막으로 시트에서 읽은 값들을 반환하고 있는데 이 반환값이 [sidebar.html]에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;withSuccessHandler&lt;/code&gt;를 통해 설정한 핸들러의 파라미터로 넘어가게 됩니다.&lt;/p&gt;

&lt;p&gt;예시에서는 javascript의 alert을 통해 값을 표시했습니다.&lt;/p&gt;

&lt;p&gt;이제 프로젝트를 저장한 후 사이드바에서 ‘할인율 적용’ 버튼을 눌러보면&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202202/appscript10.png&quot; alt=&quot;실행결과&quot; /&gt;&lt;/p&gt;

&lt;p&gt;앱이 실행되고 결과 창이 보입니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;Apps Script 를 활용하여 커스텀 메뉴 및 사이드 바를 구성하고 외부 api 호출하는 과정을 소개해 봤습니다.&lt;/p&gt;

&lt;p&gt;비슷한 과정을 통해 간단한 UI구성 및 API 연계 작업이 가능하고 이를 이용해 ‘엑셀파일을 PDF로 만들어 이메일로 전송하기’ 같은 작업 자동화 툴도 개발이 가능합니다.&lt;/p&gt;

&lt;p&gt;이번에는 간단한 예제를 소개했지만 활용하기에 따라 상당히 복잡한 작업도 구현이 가능한데 Apps Script는 사실 구글 시트만을 위한 기능은 아닙니다.&lt;/p&gt;

&lt;p&gt;Google에서 제공하는 다양한 서비스들에 적용하여 활용이 가능하므로 다양한 목적으로 유용하게 사용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;Gmail 자동 응답기, 나의 Gmail 사용통계보기(메일 쓰는 빈도나 보내는 메일의 용량 등..), 한번 읽으면 스스로 폭파되는 메시지 보내기 같은 재미있는 예시들이 많으니 한번 찾아보는 것도 좋을 것 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://developers.google.com/apps-script&quot;&gt;Apps Script 공식 페이지 : https://developers.google.com/apps-script&lt;/a&gt;&lt;/p&gt;</content><author><name>타노스</name></author><category term="JavaScript" /><category term="GoogleSheet" /><category term="Apps Script" /><summary type="html">들어가며 안녕하세요. 트렌봇 개발팀 타노스입니다.</summary></entry><entry><title type="html">찜으로 찜해보는 react-query</title><link href="https://trenbe.github.io/2022/08/08/%EC%B0%9C%EC%9C%BC%EB%A1%9C%EC%B0%9C%ED%95%B4%EB%B3%B4%EB%8A%94react-query.html" rel="alternate" type="text/html" title="찜으로 찜해보는 react-query" /><published>2022-08-08T08:35:00+00:00</published><updated>2022-08-08T08:35:00+00:00</updated><id>https://trenbe.github.io/2022/08/08/%EC%B0%9C%EC%9C%BC%EB%A1%9C%EC%B0%9C%ED%95%B4%EB%B3%B4%EB%8A%94react-query</id><content type="html" xml:base="https://trenbe.github.io/2022/08/08/%EC%B0%9C%EC%9C%BC%EB%A1%9C%EC%B0%9C%ED%95%B4%EB%B3%B4%EB%8A%94react-query.html">&lt;h1 id=&quot;도입배경&quot;&gt;도입배경&lt;/h1&gt;

&lt;p&gt;트렌비 웹페이지는 &lt;a href=&quot;https://github.com/react-boilerplate/react-boilerplate&quot;&gt;react-boilerplate&lt;/a&gt; 기반으로 작성되었습니다. 그래서 상태관리에 대해서도 보일러플레이트에서 사용한 리덕스(redux)를 그대로 사용해 왔습니다. 리덕스를 사용하는데 있어 큰 문제는 없었으나, 개발과정에서 아래와 같은 몇 가지 불편함이 있었습니다.&lt;/p&gt;

&lt;p&gt;첫째, API를 호출하고 그 결과를 화면에 그리기 위해서 해야할 게 너무 많았습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;컴포넌트에서 API를 호출하기 위한 액션(action)을 정의해 줘야 합니다.&lt;/li&gt;
  &lt;li&gt;액션과 연결되는 제너레이터(generator)를 만들어서 API를 호출해야 합니다.&lt;/li&gt;
  &lt;li&gt;API결과를 처리하기 위해 다시 성공, 실패에 대한 액션을 만들어 줍니다. 데이터 캐시가 필요하다면 직접 여기서 구현해야 합니다.&lt;/li&gt;
  &lt;li&gt;스토어(store)에서 이 결과를 저장할 수 있도록 처리해줘야 합니다.&lt;/li&gt;
  &lt;li&gt;스토어에 저장된 값을 가져오기 위해 셀렉터(selctor)를 만들어 줘야 합니다.&lt;/li&gt;
  &lt;li&gt;마지막으로 컴포넌트는 셀렉터를 이용해 API 결과를 화면에 그려줍니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;둘째, 트렌비 프론트엔드 구조의 문제일텐데 공통적인 부분을 공통적으로 처리 못하고 있었습니다. 컨테이너/컴포넌트 구조를 계속 확장하다 보니 사가(saga)와 스토어가 개별 컨테이너에 속해 있었습니다. 그러나 웹페이지 규모가 커지면서 하나의 컨테이너가 다른 컨테이너에 속한 API를 호출해야 하는 상황이 빈번히 발생하기 시작했습니다. 예를 들면 트렌비 매거진 목차를 가져오는 API는 매거진 컨테이너에서 호출하지만 홈화면에서도 호출해야 했습니다. 홈에서 매거진 컨테이너에 포함된 제너레이터를 호출할 수 있으나, 홈화면 컨테이너가 매거진 목차를 가져오는 API 하나로 인해 매거진 컨테이너에 대한 종속성이 생기게 됩니다. 따라서 트렌비는 새로운 구조를 만들어서 이 문제를 해결해야 했습니다.&lt;/p&gt;

&lt;p&gt;셋째, 우리는 정말 상태를 관리하고 있는가 라는 근본적인 질문이 생겼습니다. 리덕스를 단순히 사용한다는 것만으로 상태를 관리하고 있다고 할 수 있을까요?&lt;/p&gt;

&lt;p&gt;트렌비 &lt;a href=&quot;#프론트엔드-커미티&quot;&gt;프론트엔드 커미티&lt;/a&gt;에서는 비대해지면서 비효율적인 리덕스를 이용한 상태관리를 개선해보기 위해 조사를 시작했습니다. 리덕스 사용에 대한 불편함은 리덕스팀에서도 알고 있는지 리덕스툴킷(redux-toolkit)이라는 패키지도 존재했습니다. 리코일(Recoil)과 몹엑스(Mobx)도 있었으나 리액트 쿼리(react-query)와 SWR을 최종 후보로 선택했습니다. 가장 큰 이유는 현재 사용하고 있는 리덕스에 대한 수정 없이 리액트 쿼리와 SWR을 도입해서 사용할 수 있다는 것이었습니다.&lt;/p&gt;

&lt;h2 id=&quot;리액트-쿼리-vs-swr&quot;&gt;리액트 쿼리 vs SWR&lt;/h2&gt;
&lt;p&gt;리액트 쿼리의 장점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;뮤테이션(mutation)하기 위한 함수가 따로 존재함.&lt;/li&gt;
  &lt;li&gt;리액트 쿼리용 디버깅 도구가 포함되어 있어 디버깅 편의성 제공&lt;/li&gt;
  &lt;li&gt;캐시 시간 설정을 통해 캐시삭제 가능. 따라서 캐시 삭제를 위한 구현을 하지 않아도 됨.&lt;/li&gt;
  &lt;li&gt;SWR보다 많은 레퍼런스&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SWR의 장점&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;15.7kB로 리액트 쿼리의 1/3 크기&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;라이브러리 크기가 3배 크긴 하지만 한 번 정한 라이브러리를 바꾸는 것은 어려운 일이며, 라이브러리를 바꿔야 하는 상황이 크기로 인한 문제보다는 원하는 기능이 없어서일 확률이 더 클 것이라는 결론을 내리고 리액트 쿼리를 점진적으로 사용해 보기로 하였습니다.&lt;/p&gt;

&lt;p&gt;트렌비의 다양한 부분에서 도입해서 사용하고 있지만, 이 글에서는 리액트 쿼리로 CRUD를 간단하게 적용해 볼 수 있었던 찜 페이지를 예로 들어 설명해 보겠습니다.&lt;/p&gt;

&lt;h1 id=&quot;찜-페이지를-이용해-찜-해보자&quot;&gt;찜 페이지를 이용해 찜 해보자&lt;/h1&gt;
&lt;p&gt;리액트 쿼리는 상태 관리영역 중, 서버 상태 관리에 초점을 맞추고 있는 라이브러리 입니다. 프론트 개발에서 상태관리는 클라이언트 상태 관리와 서버 상태 관리로 나눌 수 있습니다. 그 중 서버 상태 관리는 말 그대로 CRUD를 통해 서버와 데이터 싱크를 맞추는 부분이라 할 수 있습니다. 찜 페이지를 예로 들자면, 찜한 개수, 상품 찜하기, 찜한 상품 삭제와 같이 클라이언트에서 발생한 동작이 서버에 영향을 주는 기능들입니다. 반면 클라이언트 상태 관리 영역은 서버에 영향을 주지 않습니다. 어떤 모달창이 열려 있는지, 사용자가 스크롤한 위치가 어딘지, 어떤 아코디언 메뉴가 열려 있는지와 같은 것입니다.&lt;/p&gt;

&lt;p&gt;찜 페이지에서는 사용자가 현재 찜한 개수가 몇 개인지 표시되는 부분이 있습니다. 그리고 상품 화면에서 하트 버튼을 클릭하면 상품을 찜하게 되고 이 때 상품 찜 개수도 변경됩니다.&lt;/p&gt;
&lt;center&gt;
&lt;img src=&quot;/imgs/posts/202206/1.add-to-wish.gif&quot; width=&quot;50%&quot; alt=&quot;찜 하기&quot; /&gt;
&lt;/center&gt;

&lt;p&gt;이제 리액트 쿼리에서 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;을 사용해 이 과정을 구현해 봅시다.&lt;/p&gt;

&lt;h2 id=&quot;1-usequery&quot;&gt;1. useQuery&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;는 이름 그대로 쿼리(query), 즉 CRUD에서 READ에 해당하는 동작을 위한 함수입니다. 무한 스크롤이나 페이지네이션이 필요한 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt; 대신 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useInfiniteQuery&lt;/code&gt; 함수를 이용할 수 있습니다. 두 함수의 차이점은 페이지에 대한 설정이 추가되고 캐시도 페이지별로 가능하다는 점입니다.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;dataUpdatedAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;errorUpdatedAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;failureCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isFetched&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isFetchedAfterMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isFetching&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isPaused&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isLoading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isLoadingError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isPlaceholderData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isPreviousData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isRefetchError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isRefetching&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isStale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;fetchStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;queryKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;queryFn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;cacheTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;networkMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;initialData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;initialDataUpdatedAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;isDataEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;keepPreviousData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;notifyOnChangeProps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;onSettled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;placeholderData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;queryKeyHashFn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetchInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetchIntervalInBackground&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetchOnMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetchOnReconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;refetchOnWindowFocus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;retry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;retryOnMount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;retryDelay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;staleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;structuralSharing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;suspense&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useErrorBoundary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이 훅(hook)을 이용해 찜 개수를 가져와 봅시다.&lt;/p&gt;

&lt;h3 id=&quot;usequery를-이용한-찜한-개수-가져오기&quot;&gt;useQuery를 이용한 찜한 개수 가져오기&lt;/h3&gt;
&lt;p&gt;트렌비에서 찜한 개수는 찜한 상품 개수와 찜한 브랜드 개수의 합으로 표시됩니다.&lt;/p&gt;
&lt;center&gt;
&lt;img src=&quot;/imgs/posts/202206/2.react-query-count.png&quot; width=&quot;50%&quot; alt=&quot;찜 개수&quot; /&gt;
&lt;/center&gt;

&lt;p&gt;API 응답 예제입니다.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Request /api/wish/count

Response
{ totalCount: 2, product: 1, brand: 1}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;를 컴포넌트에서 바로 사용할 수도 있습니다. 그러나 우리는 API가 특정 컴포넌트에 종속되는 실수를 반복하고 싶지 않았습니다. API와 컴포넌트를 분리하고 어디서든 호출하기 위해 함수를 하나 만들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;를 감싸줬습니다.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishGetCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;                   &lt;span class=&quot;c1&quot;&gt;// &amp;lt;-- 키를 세팅하는 부분&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WishCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// &amp;lt;-- query 함수 &lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;wish_get_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;리액트 쿼리에서 서버 상태를 관리한다고 했는데, 그 핵심 중 하나는 키(key) 사용입니다. 키를 이용해 결과값을 캐시하며, 캐시한 데이터를 수정/삭제할 수 있습니다. 당연히 이 키는 다른 키와 중복되면 안됩니다.&lt;/p&gt;

&lt;p&gt;키 설정하는 부분을 보시면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[WISH_GET_COUNT]&lt;/code&gt; 처럼 배열을 인자로 받고 있습니다. 즉 상황에 따라 얼마든지 키를 더 세분화 해서 추가할 수 있습니다. 예를 들어 전체 찜한 개수를 가져오는 API가 있고, 브랜드와 상품 찜 수를 가져오는 API가 따로 있다고 가정해 봅시다. 그러면 아래와 같이 상품 찜에 대한 키를 세팅할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishGetCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
     &lt;span class=&quot;c1&quot;&gt;// type 키를 추가해서 서로 다른 값을 캐싱할 수 있도록 함.&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WishCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;배열을 이용해서 키를 설정했기 때문에 캐시에 접근하기 위한 두 가지 방법이 생겼습니다. 첫째는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WISH_GET_COUNT&lt;/code&gt;를 키로 가지는 모든 데이터에 접근하는 방법이고 두 번째는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WISH_GET_COUNT&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type&lt;/code&gt;을 이용해서 데이터에 접근하는 방법입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brand&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;product&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total&lt;/code&gt; 세 가지 타입으로 저장된 모든 데이터를 삭제하기 위해서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WISH_GET_COUNT&lt;/code&gt; 키만 이용하면 됩니다. 자세한 내용은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt; 부분에서 좀 더 다루겠습니다.&lt;/p&gt;

&lt;h3 id=&quot;usequery를-이용해-찜한-개수를-컴포넌트에-표시하기&quot;&gt;useQuery를 이용해 찜한 개수를 컴포넌트에 표시하기&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;을 구현할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt; 훅에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;return response.data&lt;/code&gt;로 돌려주는 값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; 필드를 통해 접근할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;HeaderMobileIconMenus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// data 필드 접근.&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishGetCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
 
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;99&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;99+&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;리덕스에서 거의 동일한 동작을 하는 코드는 아래처럼 많은 작업을 해야 합니다. 리액트 쿼리 도입으로 얼마나 많은 개발 시간을 단출할 수 있었는지 알 수 있습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;리덕스로 작업한 코드 보기&lt;/summary&gt;
&lt;div&gt;

    &lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// constants&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;REQUEST_WISH_COUNT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;request_wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_WISH_COUNT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ressponse_success_wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_WISH_COUNT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;response_success_Wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// actions&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getWishCountRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;REQUEST_WISH_COUNT&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getWishCountSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_WISH_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getWishCountError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_WISH_COUNT&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// saga&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getWishCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;opt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;opt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getWishCountSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getWishCountError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// reducer&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_WISH_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;wishListCount&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_WISH_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// selector&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;makeSelectWishCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;selectWish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;wish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;wishCount&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;HeaderMobileIconMenus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;makeSelectWishCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
 
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;99&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;99+&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishListCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;li&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

  &lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;의 아주 기본적인 사용법을 알아보았습니다. 몇 가지 팁을 통해 조금 더 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;를 잘 사용하는 방법을 알아봅시다.&lt;/p&gt;

&lt;h3 id=&quot;tip-1-wishlistcount-초기값을-undefined-대신-원하는-초기값으로-수정하기&quot;&gt;TIP 1. wishListCount 초기값을 undefined 대신 원하는 초기값으로 수정하기&lt;/h3&gt;
&lt;p&gt;최초 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount()&lt;/code&gt;를 호출할 때, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt;에서 초기값으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;undefined&lt;/code&gt;가 대신 0이 전달되도록 해 봅시다. 초기값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;의 마지막 인자인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Option&lt;/code&gt;에서 설정할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishGetCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WishCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Option 부분&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// 초기값.&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;initialData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; 
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;tip-2-staletime-vs-cachetime&quot;&gt;TIP 2. staleTime vs cacheTime&lt;/h3&gt;
&lt;p&gt;찜 리스트는 자주 변경되는 데이터가 아니기 때문에 한 번 데이터를 받아오면 새롭게 갱신될 때까지 데이터를 받아오지 않아도 됩니다. 리액트 쿼리는 캐시를 해준다고 하였으나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 사용할 때 마다 API도 호출하고 있었습니다. 이것은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 구현할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Option&lt;/code&gt;에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;을 설정하지 않았기 때문입니다. 리액트 쿼리는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt; 5분, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt; 0이 기본값입니다. 이제 staleTime을 5분으로 설정하게 되면 5분 동안 API를 호출하지 않게 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishGetCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WishCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// staleTime 추가&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;staleTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;initialData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그렇다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;은 무엇이고 어떻게 다를까요?&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;return&lt;/code&gt;으로 전달한 값이 얼마나 유효한가에 대한 시간입니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;에 설정된 시간만큼 유효하기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt; 동안 API를 댜시 호출하지 않고 캐시되어 있는 데이터를 사용합니다.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;return&lt;/code&gt;으로 전달한 값을 얼마나 오랫동안 가지고 있을지에 대한 시간입니다. 캐시한 데이터가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;을 초과해 유효하지 않다고 하더라도 캐시에서 지우지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;동안 보관합니다.&lt;/p&gt;

&lt;p&gt;이해를 돕기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTiem&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;을 극단적으로 설정해 봅시다.&lt;/p&gt;

&lt;h4 id=&quot;cachetime-5min-staletime-0&quot;&gt;CacheTime 5min, StaleTime 0&lt;/h4&gt;
&lt;center&gt;
&lt;img src=&quot;/imgs/posts/202206/3.cacheTime5min staleTime0min.png&quot; alt=&quot;cacheTime 5min, staleTime 0min&quot; /&gt;
&lt;/center&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출하였을 때는 캐시된 데이터가 없기 때문에 초기값을 반환합니다. 그러나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출한 시점에는 캐시된 데이터(이미지에서 노란색 부분)가 있기 때문에 캐시된 데이터를 전달합니다. 다만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;이 0이기 때문에 다시 API를 호출하고 있습니다. API 응답값이 캐시된 데이터와 다른 경우 캐시를 업데이트하고 변경된 데이터를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B Component&lt;/code&gt;로 전달합니다.&lt;/p&gt;

&lt;h4 id=&quot;cachetime-5min-staletime-5min&quot;&gt;CacheTime 5min, StaleTime 5min&lt;/h4&gt;
&lt;center&gt;
&lt;img src=&quot;/imgs/posts/202206/4.cacheTime5min staleTime5min.png&quot; alt=&quot;cacheTime 5min, staleTime 5min&quot; /&gt;
&lt;/center&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출하였을 때는 캐시된 데이터가 없기 때문에 초기값을 반환합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출한 시점에는 캐시된 데이터(이미지에서 노란색 부분)도 있고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;(이미지에서 주황색 부분)도 유효하기 때문에 캐시된 데이터를 전달하고 API를 호출하지 않습니다. 그러나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;이 모두 종료된 시점에는 처음 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출할 때와 동일하게 동작합니다.&lt;/p&gt;

&lt;h4 id=&quot;cachetime-0-staletime-5min&quot;&gt;CacheTime 0, StaleTime 5min&lt;/h4&gt;
&lt;center&gt;
&lt;img src=&quot;/imgs/posts/202206/5.cacheTime0min staleTime5min.png&quot; alt=&quot;cacheTime 0min, staleTime 5min&quot; /&gt;
&lt;/center&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 처음 호출할때는 동일합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B Component&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;를 호출한 시점에는 캐시된 데이터는 없고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;(이미지에서 주황색 부분)만 유효한 상태입니다. 리액트 쿼리는 이 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;을 무시하고 다시 API 호출을 한 후 결과를 다시 전달해 줍니다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;를 통해 찜 개수를 가지고 왔고 초기값도 설정했으며 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt;을 통해 적절히 캐시도 할 수 있게 되었습니다. 이제는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;을 통해 서버 상태를 업데이트 해 봅시다.&lt;/p&gt;

&lt;h2 id=&quot;2-usemutation이란&quot;&gt;2. useMutation이란?&lt;/h2&gt;
&lt;p&gt;CRUD에서 READ를 제외한 CUD를 위해 사용할 수 있습니다. &lt;a href=&quot;https://tanstack.com/query/v4/docs/reference/useMutation&quot;&gt;공식문서&lt;/a&gt;를 통해 자세한 내용을 확인하실 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;const {
  data,
  error,
  isError,
  isIdle,
  isLoading,
  isPaused,
  isSuccess,
  mutate,
  mutateAsync,
  reset,
  status,
} = useMutation(mutationFn, {
  cacheTime,
  mutationKey,
  networkMode,
  onError,
  onMutate,
  onSettled,
  onSuccess,
  retry,
  retryDelay,
  useErrorBoundary,
  meta
})

mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;usemutation을-이용해-상품-찜하기&quot;&gt;useMutation을 이용해 상품 찜하기&lt;/h3&gt;
&lt;p&gt;이해를 돕기 위해 API와 응답값은 아래와 같습니다.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Request https://host/api/wish/product
Method: POST
Body: { &quot;productId&quot; : number }
 
Response
{ 
  success: true, 
  data: {
    id: wishId
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;와 동일하게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;도 함수로 한 번 감싼 후 다른 컴포넌트에서도 언제든 호출할 수 있도록 합니다.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishAddProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddWishProductReq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AddWishProductData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/product&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
          &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;캐시를 사용하지 않기 때문에 키, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cacheTime&lt;/code&gt; 같은 설정없이 상대적으로 단순한 형태를 가지고 있습니다. 이제 찜하기 버튼을 클릭했을 때 이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishAddProduct&lt;/code&gt; 훅을 사용하면 됩니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;은 훅을 사용하는 시점에 API 호출이 발생하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutate&lt;/code&gt;를 사용하는 시점에 API 호출이 발생합니다. 그래서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutate&lt;/code&gt;를 사용할 때 인자값으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddWishProductReq&lt;/code&gt; 타입을 넘겨야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BtnWish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({...}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductWishBtnProps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// 이름을 재정의해서 가독성을 높입니다.&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;mutate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onAddToWish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishAddProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClickWish&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// mutate 함수를 호출합니다.&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;onAddToWish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;buttn&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onClickWish&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;찜하기&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;리덕스로 동일한 기능을 구현할 때와 코드양은 비교 불가입니다.&lt;/p&gt;

&lt;details style=&quot;margin-bottom: 50px&quot;&gt;
&lt;summary&gt; 리덕스로 작업한 코드 보기 &lt;/summary&gt;
&lt;div&gt;

    &lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// constants&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;REQUEST_ADD_WISH_PRODUCT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;request_add_wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_ADD_WISH_PRODUCT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ressponse_success_add_wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_ADD_WISH_PRODUCT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;response_success_add_wish_count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// actions&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;postAddWishProductRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;REQUEST_ADD_WISH_PRODUCT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;postAddWishProductSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_ADD_WISH_PRODUCT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;postAddWishProductError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_ADD_WISH_PRODUCT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// saga&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;postAddWishProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;opt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;application/json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/product&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;opt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postAddWishProductSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postAddWishProductError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// reducer&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;wishStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_SUCCESS_ADD_WISH_PRODUCT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;addWishProduct&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RESPONSE_ERROR_ADD_WISH_PRODUCT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// component&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BtnWish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({...}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProductWishBtnProps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useDispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onClickWish&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useCallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postAddWishProductRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;buttn&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onClickWish&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;찜하기&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/div&gt;


&lt;/details&gt;

&lt;hr /&gt;
&lt;p&gt;서버에 찜한 상품이 하나 추가 되었습니다. 그러자 서버에서 받아와 가지고 있던 기존 데이터들은 서버와 상태가 맞지 않게 되었습니다. 찜한 상품 개수도 맞지 않고, 찜 목록 페이지에 가면 방금 찜한 상품도 보이지 않았습니다. 이제 이 문제들을 풀어 봅시다.&lt;/p&gt;

&lt;h3 id=&quot;문제1-찜한-상품이-추가되었으나-찜한-개수는-변경되지-않음&quot;&gt;문제1. 찜한 상품이 추가되었으나 찜한 개수는 변경되지 않음&lt;/h3&gt;
&lt;p&gt;앞에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;를 이용해 상품 개수를 가지고 오고 캐시를 하고 있었습니다. 이제 여기서 상품을 추가했으니 자동으로 개수가 1 늘어나면 좋겠지만 그런 일이 발생하지는 않습니다. 특히 상품 찜 개수는 캐시되어 있기 때문에 다른 페이지로 이동하더라도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt;동안 API가 다시 호출되지 않고 개수가 그대로 유지됩니다. 이 상황을 해결할 수 있는 두 가지 방법이 있습니다.&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useQuery&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refetch&lt;/code&gt;라는 함수도 응답값에 전달해 줍니다. 이 함수를 이용해서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;staleTime&lt;/code&gt; 시간과 관계없이 API를 호출할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;API 호출 없이 저장된 캐시값만 업데이트 합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;여기서는 두 번째 방법을 사용해 보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishGetCount&lt;/code&gt;에서 캐시에 저장될 때 사용하는 키 값으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WISH_GET_COUNT&lt;/code&gt;를 설정했었습니다. 따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WISH_GET_COUNT&lt;/code&gt; 키 값을 이용해 캐시 데이터를 불러온 후 값을 업데이트하고 다시 동일한 키 값으로 저장하도록 하겠습니다. 물론 이 모든 동작은 찜하기가 성공했을 때 발생해야 하기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMutation&lt;/code&gt;에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onSuccess&lt;/code&gt;콜백에서 작업해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishAddProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddWishProductReq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AddWishProductData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/product&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// WISH_GET_COUNT로 캐시되어 있던 데이터를 가지고 옵니다.&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WishCountDto&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getQueryData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WishCountDto&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// query client를 통해 WISH_GET_COUNT 키에 저장된 값을 다시 세팅해 줍니다.&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setQueryData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;// brandCount는 변경이 없기 때문에 변경하지 않습니다.&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;brandCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;brandCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;// productCount는 1을 추가합니다.&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;productCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;productCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;// 전체 개수도 1을 추가합니다.&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;totalCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 찜하기를 누르게 되면 API 요청이 성공했을 때 추가적인 API 호출 없이 찜 개수가 1 증가되는 것을 확인할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;문제2-찜-목록-페이지로-이동하면-새롭게-찜한-상품이-보이지-않음&quot;&gt;문제2. 찜 목록 페이지로 이동하면 새롭게 찜한 상품이 보이지 않음&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useWishAddProduct&lt;/code&gt;를 호출하고, 찜 페이지에 가보면 사용자가 찜한 상품이 보이지 않습니다. 새로고침하게 되면 잘 보입니다. 즉 서버는 업데이트 되었으나 클라이언트 상태가 변경되지 않은 것입니다. 그러면 찜 개수 변경할 때 처럼 캐시를 업데이트 하거나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refetch&lt;/code&gt; 함수를 부르는 방법이 있습니다.
그러나 상황이 조금 다릅니다. 찜 개수는 페이지 상단에 있는 헤더에서 항상 숫자가 보이고 있습니다. 찜하기 동작이 완료되었을 때 변경 사항이 바로 적용되어야 합니다. 하지만 찜한 목록은 그렇지 않습니다. 당장 업데이트 하지 않고 기다렸다가 찜 목록 페이지로 진입할 때 새롭게 변경된 데이터가 보이면 됩니다. 이 때 사용할 수 있는 함수가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;invalidateQueries&lt;/code&gt; 입니다. 캐시 데이터를 무효화 시키기 때문에 캐시 데이터를 사용하지 않고 API를 호출하게 됩니다. 인자 값으로 무효화 시킬 키 값을 넘겨주면 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useWishAddProduct&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useMutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddWishProductReq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AddWishProductData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;qs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringifyUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://host/api/wish/product&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;productId&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// 찜 목록을 캐시하는 키 값을 전달해서 연결된 캐시를 무효화.&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;invalidateQueries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_PRODUCT_LIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; 
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WishCountDto&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getQueryData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WishCountDto&lt;/span&gt; 
        &lt;span class=&quot;nx&quot;&gt;queryClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setQueryData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WISH_GET_COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
          &lt;span class=&quot;na&quot;&gt;brandCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;brandCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;productCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;productCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;totalCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;totalCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이렇게 두 가지 방법을 통해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mutation&lt;/code&gt;이 발생했을 때 상태 관리하는 방법을 알아보았습니다.&lt;/p&gt;

&lt;h1 id=&quot;결론&quot;&gt;결론&lt;/h1&gt;
&lt;p&gt;우리가 고민하던 문제가 3개 있었습니다.&lt;/p&gt;

&lt;p&gt;첫번째, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;간단한 작업을 위해서도 해야할 일이 너무 많다.&lt;/code&gt; 리액트 쿼리는 현재 사용중인 리덕스를 걷어내거나 수정하는 추가적인 노력없이 셀렉터, 액션, 스토어와 같은 보일러플레이트 작성에 들여야 하는 시간을 상당히 줄여 주었습니다. 리덕스에서 5개의 파일을 만들고 작업해야 했던 부분은 두 개의 파일로 줄었습니다. 그 만큼 구현도 단순해지고 디버깅 난이도도 낮아졌습니다.&lt;/p&gt;

&lt;p&gt;두번째, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;공통적인 부분을 공통으로 처리할 수 없었습니다.&lt;/code&gt; 구조적인 문제를 수정한다는 것은 쉬운일이 아니지만 리액트 쿼리를 사용하면서 우리가 생각하는 이상적인 그림에 조금 더 가깝게 구조를 변경할 수 있었습니다. 여전히 많은 부분에서 리덕스 사가를 사용하고 있으나 여러 페이지에서 자주 사용되는 찜, IT-EM 부분들을 리액트 쿼리로 전환함으로써 약 3,000라인의 중복된 코드를 제거할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;세번째, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;우리는 정말 리덕스를 이용해 상태를 관리하고 있는가?&lt;/code&gt; 리액트 쿼리는 그 목적에 맞게 서버 상태를 관리하는 부분에 집중하면서, 리덕스는 클라이언트 상태를 관리하도록 사용하고 있습니다. 이로 인해 리덕스에 대한 의존성이 낮아지고 있으며, 장기적으로 리덕스 툴킷이나 몹엑스(Mobx)와 같은 다른 상태 관리 도구로 대체할 계획도 세울 수 있게 되었습니다.&lt;/p&gt;

&lt;hr /&gt;
&lt;h4 id=&quot;각주&quot;&gt;각주&lt;/h4&gt;
&lt;h5 id=&quot;프론트엔드-커미티&quot;&gt;프론트엔드 커미티&lt;/h5&gt;
&lt;p&gt;트렌비 프론트엔드 개발자는 같은 팀이 아니라 다른 팀에 속해 있습니다. 그렇지만 하나의 코드 베이스를 공유하고 있어 동일한 방향성을 가지고 함께 일을 하기 위해 커미티를 조직해 소통하고 있습니다. 개발 과정상에서 발생하는 어려움, 혹은 어려움을 해결한 좋은 사례를 공유하기도 하며, 여기서 소개된 리액트 쿼리처럼 라이브러리에 대해 알아보고 내용을 공유하기도 합니다. 코드리뷰도 프론트엔드 커미티 기준으로 진행하고 있습니다.&lt;/p&gt;

&lt;h5 id=&quot;external-link&quot;&gt;External link&lt;/h5&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://tanstack.com/query/v4/docs/overview&quot;&gt;리액트 쿼리 공식 페이지&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://velog.io/@velopert/using-redux-in-2021&quot;&gt;Redux 어떻게 써야 잘 썼다고 소문이 날까?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://javascript.plainenglish.io/react-query-vs-swr-36743c14ba7e&quot;&gt;React Query vs SWR&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>티라노</name></author><category term="react-query" /><category term="리액트쿼리" /><category term="상태관리" /><category term="redux" /><summary type="html">도입배경</summary></entry><entry><title type="html">백오피스 엑셀 다운로드 속도 개선하기</title><link href="https://trenbe.github.io/2022/08/05/%EB%B0%B1%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%97%91%EC%85%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0.html" rel="alternate" type="text/html" title="백오피스 엑셀 다운로드 속도 개선하기" /><published>2022-08-05T04:00:00+00:00</published><updated>2022-08-05T04:00:00+00:00</updated><id>https://trenbe.github.io/2022/08/05/%EB%B0%B1%EC%98%A4%ED%94%BC%EC%8A%A4%20%EC%97%91%EC%85%80%20%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C%20%EC%86%8D%EB%8F%84%20%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2022/08/05/%EB%B0%B1%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%97%91%EC%85%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0.html">&lt;h1 id=&quot;백오피스-엑셀-다운로드-속도-개선하기&quot;&gt;백오피스 엑셀 다운로드 속도 개선하기&lt;/h1&gt;

&lt;p&gt;안녕하세요, 트렌비 가든 개발팀의 도리입니다.&lt;/p&gt;

&lt;p&gt;저희 가든 팀에서는 내부 직원들이 사용하는 백오피스(Back Office)를 개발하고 있습니다.&lt;/p&gt;

&lt;p&gt;백오피스를 개발하다 보면 엑셀 다운로드 기능을 제공해야 하는 경우가 굉장히 많은데 최근 기존에 존재하던 엑셀 다운로드 기능이 정상적으로 동작하지 않아 이를 개선한 과정을 공유해 드리고 자 합니다.&lt;/p&gt;

&lt;h4 id=&quot;문제-상황&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p&gt;평화로웠던 어느 날 갑자기 사내 메신저(슬랙)을 통해 가든의 ‘구매확정’이라는 메뉴에서 엑셀 다운로드 시&lt;br /&gt;
504(Gateway Timeout) 상태 코드가 응답으로 반환된다는 신고가 들어왔습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_1.png&quot; alt=&quot;backoffice_excel_1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;504 Gateway Timeout은 클라이언트에서 서버로 요청을 보낸 뒤 일정 시간 동안 서버에서 응답을 주지 않아 발생하는 오류이기 때문에
타임아웃의 원인을 파악하고자 타임아웃의 로컬 환경에서 엑셀 다운로드 시 걸리는 시간을 측정해 봤습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[측정환경]&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;서버 : Local&lt;/li&gt;
  &lt;li&gt;대상 데이터 개수 : 1,473건&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;[측정결과]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_2.png&quot; alt=&quot;backoffice_excel_2&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;약 1,000건의 데이터를 다운로드하는데 2분이 넘는 시간이 소요되었습니다 😱&lt;br /&gt;
운영 DB와 개발 DB의 스펙 차이와 로그 설정 차이 등으로 인해 실제 운영환경에서는 위 측정 결과보다 나은 성능을 보이겠지만 도저히 그대로 방치할 수 없는 결과가 나왔기 때문에 이를 개선하기로 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h4 id=&quot;왜-이렇게-오래걸리는-것인가---원인-파악&quot;&gt;왜 이렇게 오래걸리는 것인가? - 원인 파악&lt;/h4&gt;
&lt;p&gt;문제가 되는 부분은 로컬 테스트에서 바로 파악할 수 있었습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_3.gif&quot; alt=&quot;backoffice_excel_3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;엑셀 다운로드 버튼 클릭 한 번에 무수히 많은 쿼리가 발생하고 있는 모습을 볼 수 있습니다…🥲&lt;/p&gt;

&lt;p&gt;문제가 되는 메뉴에서 엑셀 다운로드 시도 시 필요한 데이터를 JpaSpecificationExecutor&amp;lt;T&amp;gt;의 findAll 메소드를 통해 가져오고 있었는데 해당 메소드가 호출될 때마다 N+1 쿼리가 발생했습니다.&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderItemRepository&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getOrderItemsBy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Specification&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;specification&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderItemRepository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findAll&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;specification&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h4 id=&quot;문제-해결&quot;&gt;문제 해결&lt;/h4&gt;
&lt;p&gt;원인으로 파악된 N+1 문제를 해결하기 위해 두 가지를 변경하게 되었습니다.&lt;/p&gt;

&lt;p&gt;첫 번째 변경으로 JpaSpecticifationExecutor&amp;lt;T&amp;gt;제거와 QueryDSL 사용입니다.&lt;/p&gt;

&lt;p&gt;문제 해결을 위해 fetchJoin 사용을 고려해야 했었는데 JpaSpecificationExecutor&amp;lt;T&amp;gt;는 개발자가 자유롭게 JPQL을 작성하는데 어려움이 있기 때문입니다.&lt;br /&gt;
또한, JpaSpecificationExecutor&amp;lt;T&amp;gt;은 실제로 호출되는 시점에 어떤 쿼리가 나갈지 개발자가 알기 힘들기 때문에 유지 보수에 걸림돌이 된다고 판단했습니다.&lt;/p&gt;

&lt;p&gt;두 번째 변경으로 Projection을 사용한 데이터 조회입니다.&lt;/p&gt;

&lt;p&gt;다른 주문 관련 메뉴에서도 엑셀 다운로드에 필요한 데이터를 조회하기 위해 동일한 메소드를 호출하고 있었기 때문에 List&amp;lt;OrderItem&amp;gt;을 반환하는 메소드를 구현하는 것이 가장 많은 범위에 걸쳐 개선 효과를 볼 수 있었지만 Projection을 선택한 이유는 다음과 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[OrderItem과 관계를 맺고있는 객체가 너무 많이 존재합니다.]&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderItem&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderPartnerId&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderPartner&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderPartner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToOne&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderHistory&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderHistory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToOne&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderHistoryEstimate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderHistoryEstimate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToOne&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;MERGE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderDeliveryClientGuide&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderDeliveryClientGuide&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderDeliveryClientGuideLog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderDeliveryClientGuideLogs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToOne&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderItem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderCancel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderCancel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;기존에는 OrderItem 엔티티를 가져와서 엑셀에 필요한 데이터를 채워주었는데, OrderItem은 너무 많은 객체들과 관계를 맺고 있고 그 모든 객체들의 필드가 필요한 게 아니기 때문에 필요한 데이터만 가져와서 채워주기 위해 Projection을 사용했습니다.
(위 코드는 OrderItem과 관계를 맺고 있는 객체들의 일부분이며 실제로는 훨씬 더 많은 엔티티 객체들과 연관관계를 맺고 있습니다.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[OrderItem과 관계를 맺고 있는 많은 객체들이 또 다른 객체들과 관계를 맺고 있습니다.]&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Entity&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderPartner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Id&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@ManyToOne&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;order_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@OneToMany&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cascade&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CascadeType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ALL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mappedBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;orderPartner&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderItems&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;nd&quot;&gt;@OneToOne&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@JoinColumn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;partner_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PartnerCompany&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;partnerCompany&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;OrderItem과 관계를 맺고 있는 OrderPartner를 보면 PartnerCompany와 관계를 맺고 있는 것을 볼 수 있습니다.&lt;br /&gt;
이 상태로 OrderItem을 조회하게 되면 OrderPartner의 PartnerCompany에 값을 채워주기 위해 N 개의 쿼리가 더 발생하게 됩니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;이를 해결하려면 OrderItem을 Root로 객체들을 탐색하며 여러 Depth에 걸쳐 관계를 가진 모든 객체들을 찾아내 fetchJoin을 걸어주어야 하는데 이는 너무 많은 불필요한 조인을 유발합니다.&lt;/p&gt;

&lt;p&gt;QueryDSL을 사용해 아래와 같이 엑셀 다운로드에 필요한 데이터만을 조회하는 새로운 메소드를 작성해 호출해 보니 드디어 N+1 문제가 해결되었습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItemExcelDownloadDto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;findOrderItemExcelDownloadDtosBySearchCondition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItemExcelDownloadSearchCondition&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;searchCondition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jpaQueryFactory&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Projections&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;bean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;OrderItemExcelDownloadDto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;orderItemId&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;orderItemName&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;goodsno&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;...))&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orderPartner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orderPartner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderPartner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expression&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;searchCondition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orderBy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;orderItem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;())&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;N+1 문제가 해결되어 처음 시도했던 동일한 환경과 데이터로 다시 속도를 측정해 봤습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_4.png&quot; alt=&quot;backoffice_excel_4&quot; width=&quot;100%&quot; /&gt;&lt;/p&gt;

&lt;p&gt;2분 40초에서 6초로 다운로드 소요 시간이 감소했습니다.&lt;br /&gt;
(N+1 문제 해결 후부터는 DB 조회 시간과 엑셀 다운로드 시간을 분리해서 측정했습니다.)&lt;/p&gt;

&lt;p&gt;N+1 문제 해결로 많은 개선이 이루어지긴 했지만 만족스럽지 못한 부분이 있습니다.&lt;br /&gt;
DB에서 데이터를 조회하는 시간은 조인이 걸려있는 테이블의 개수가 너무 많아 어쩔 수 없다지만 겨우 1,000건 정도의 데이터를 가지고 엑셀을 생성하는 데 2초나 걸렸습니다.&lt;/p&gt;

&lt;p&gt;만약 데이터의 수가 몇 만 건 혹은 몇 십만 건이 되었을 경우에도 원활하게 다운로드가 가능할 것인가에 대해 자신 있게 YES라고 대답할 수 있는 결과는 아니었습니다.&lt;/p&gt;

&lt;p&gt;이걸 어떻게 더 개선할 수 없을까 고민하며 코드를 살펴보다 기존에 사용하고 있던 엑셀 다운로드 유틸에 작성되어 있는 Apache POI 라이브러리의 Workbook 구현체가 XSFFWorkbook으로 되어있는 것을 발견했습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;orderDownload&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fileName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Integer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;useType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Workbook&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;workbook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;XSSFWorkbook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;XSFFWorkbook은 데이터를 메모리에 모두 올려놓고 쓰기 작업을 수행하기 때문에 굉장히 많은 메모리 공간을 필요로 하고 데이터가 많을 경우 OOM(Out Of Memory)가 발생할 확률이 높습니다.&lt;/p&gt;

&lt;p&gt;그러면 XSFFWorkbook이 아닌 어떤 구현체를 사용해야 할까요? 바로 SXSSFWorkbook입니다.&lt;/p&gt;

&lt;p&gt;아래 화면은 Apache의 POI 라이브러리에 대한 공식 문서 일부입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_5.png&quot; alt=&quot;backoffice_excel_5&quot; /&gt;&lt;/p&gt;

&lt;p&gt;SXSSF라는 것에 대해 소개하는 부분이 있는데, 밑줄 친 부분을 보면 아래와 같이 설명하고 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;SXSSF is an API-compatible streaming extension of XSSF to be used when very large spreadsheets have to be produced, and heap space is limited. &lt;br /&gt;
SXSSF는 XSSF를 확장한 API로 힙 공간이 제한적이고 매우 큰 스프레드시트를 생성해야 할 때 사용된다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이제 XSSF 대신 SXSSF를 사용해야 쓰기 작업 시 유리하다는 것을 알았으니 XSSFWorkbook을 SXSSFWorkbook으로 변경한 다음 재측정해 봤습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_6.png&quot; alt=&quot;backoffice_excel_6&quot; /&gt;&lt;/p&gt;

&lt;p&gt;2초에서 400밀리 초로 엑셀을 생성하는 시간이 줄어들었습니다.&lt;/p&gt;

&lt;p&gt;어떻게 이런 차이가 발생한 것일까요?&lt;br /&gt;
그 이유는 SXSSFWorkbook이 “BigGridDemo”라는 전략을 구현하고 있기 때문입니다.&lt;br /&gt;
위 전략을 구현한 SXSSFWorkbook은 설정된 WINDOW_SIZE(default=100)만큼만 데이터를 메모리에 올려 작업을 수행하기 때문에 모든 데이터를 메모리에 올린 뒤 작업을 수행하는
XSSFWorkbook에 비해 적은 메모리 공간을 사용해 효율적으로 쓰기 작업을 수행할 수 있게 됩니다.&lt;/p&gt;

&lt;p&gt;Workbook 구현체까지 변경한 뒤 처음 2분 40초와 비교하면 &lt;strong&gt;97%&lt;/strong&gt;정도의 개선이 이루어졌습니다.&lt;br /&gt;
사실 2초 정도의 차이로는 크게 개선이 이루어진 것으로 보이지 않을 수 있습니다.&lt;br /&gt;
하지만 위 결과는 천 개 정도의 적은 데이터로 측정한 것이고 실제로 데이터 수가 늘어날수록 차이가 커지게 됩니다.&lt;/p&gt;

&lt;p&gt;데이터가 많아질수록 차이가 커지는 것을 보기 위해 30,000개의 데이터를 가지고 구현체가 XSSFWorkbook 일 때와 SXSSFWorkbook 일 때를 비교해 다시 측정을 해보겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[XSFFWorkbook]&lt;/strong&gt;
&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_7.png&quot; alt=&quot;backoffice_excel_7&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[SXSFFWorkbook]&lt;/strong&gt;
&lt;img src=&quot;/imgs/posts/202208/backoffice_excel_8.png&quot; alt=&quot;backoffice_excel_8&quot; /&gt;&lt;/p&gt;

&lt;p&gt;무려 16초의 차이가 발생하는 것을 볼 수 있습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;여기까지 N+1 문제 해결과 더불어 엑셀을 생성하는 시간까지 개선하는 과정을 보여드렸습니다.&lt;br /&gt;
현재 가든에서는 백오피스의 많은 메뉴들을 관리하고 있고 대부분의 메뉴에서 엑셀 다운로드 기능을 제공하고 있기 때문에 아직 더 많은 개선이 필요하다고 생각합니다.&lt;/p&gt;

&lt;h4 id=&quot;마치며&quot;&gt;마치며&lt;/h4&gt;
&lt;p&gt;JPA를 사용해 엑셀 다운로드에 필요한 데이터를 조회하는 경우 Projection을 통해 필요한 데이터만을 조회하는 것을 고려해 보면 좋을 것 같습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;DB 조회 속도 개선도 중요하지만 데이터 수가 많아질수록 서버에서 엑셀을 생성하는데 많은 시간이 소요되기 때문에 Apache POI 라이브러리를 사용하는 경우 Workbook의 구현체로 SXSSFWorkbook을 사용하는 것이 좋습니다.&lt;/p&gt;</content><author><name>도리</name></author><category term="JPA" /><category term="N+1" /><category term="Apache POI" /><category term="XSSFWorkbook" /><category term="SXSSFWorkbook" /><summary type="html">백오피스 엑셀 다운로드 속도 개선하기</summary></entry><entry><title type="html">FE Lazy Loading 적용기</title><link href="https://trenbe.github.io/2022/07/20/FE-Lazy-Loading-%EC%A0%81%EC%9A%A9%EA%B8%B0.html" rel="alternate" type="text/html" title="FE Lazy Loading 적용기" /><published>2022-07-20T03:46:41+00:00</published><updated>2022-07-20T03:46:41+00:00</updated><id>https://trenbe.github.io/2022/07/20/FE%20Lazy%20Loading%20%EC%A0%81%EC%9A%A9%EA%B8%B0</id><content type="html" xml:base="https://trenbe.github.io/2022/07/20/FE-Lazy-Loading-%EC%A0%81%EC%9A%A9%EA%B8%B0.html">&lt;h1 id=&quot;fe-lazy-loading-적용기&quot;&gt;FE Lazy Loading 적용기&lt;/h1&gt;

&lt;p&gt;안녕하세요! 트렌비 Growth Marketing 개발팀의 FE개발자 이리입니다!&lt;br /&gt;
자세한 제 소개는 &lt;strong&gt;&lt;a href=&quot;https://trenbersday.com/Diary/?idx=9293853&amp;amp;bmode=view&quot;&gt;인터뷰&lt;/a&gt;&lt;/strong&gt; 내용을 참고해주세요!&lt;/p&gt;

&lt;h2 id=&quot;목차&quot;&gt;목차&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;개요&lt;/li&gt;
  &lt;li&gt;문제가 되는 부분은 어딜까? - 원인 분석과 해결책 찾기&lt;/li&gt;
  &lt;li&gt;Lazy Loading…?&lt;/li&gt;
  &lt;li&gt;Lazy Loading을 적용하는 방법!&lt;/li&gt;
  &lt;li&gt;개선 후 결과&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;개요&quot;&gt;개요&lt;/h3&gt;

&lt;p&gt;트렌비의 &lt;strong&gt;&lt;a href=&quot;https://www.trenbe.com/salescanner&quot;&gt;세일스캐너&lt;/a&gt;&lt;/strong&gt; 부분은 만족스러운 사용자 경험을 위해 다양한 변화와 실험을 하고 있는 페이지입니다.&lt;br /&gt;
참고로, 현재는 좀 더 매력적인 페이지를 구성하기 위해 준비 중입니다.&lt;/p&gt;

&lt;blockquote&gt;

  &lt;p&gt;&lt;em&gt;어떤 제품을 구매하면 좋을까?&lt;br /&gt;
지금 할인 중인 제품이 뭐가 있을까?&lt;br /&gt;
지금 T(Time).P(Place).O(Occasion)에 적합한 제품이 어떤 게 있을까?&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;이번 변화는 위와 같은 다양한 사용자의 니즈를 해소하기 위해,&lt;br /&gt;
트렌비의 MD가 직접 엄선한 카테고리와 상품들을 모두 진열했습니다.&lt;br /&gt;
&lt;br /&gt;
그러다 보니 &lt;strong&gt;정말 많은 상품이 진열됐고, 그로 인해 증가한 상품 이미지들은 예상치 못한 이슈&lt;/strong&gt;를 남겨주었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202203/before_lazy_loading.gif&quot; alt=&quot;로딩이 한——참 걸리는 페이지... (중간의 흰 페이지는 아직 컴포넌트조차도 못 불러온 상황입니다)&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로딩이 한——참 걸리는 페이지… (중간의 흰 페이지는 아직 컴포넌트조차도 못 불러온 상황입니다)&lt;/p&gt;

&lt;h2 id=&quot;문제가-되는-부분은-어딜까---원인-분석과-해결책-찾기&quot;&gt;문제가 되는 부분은 어딜까? - 원인 분석과 해결책 찾기&lt;/h2&gt;

&lt;p&gt;데스크톱에서는 어떻게든 기다려서 로딩을 할 순 있었지만,&lt;br /&gt;
모바일에서는 &lt;strong&gt;한참 걸리는 로딩&lt;/strong&gt;과 &lt;strong&gt;어마어마한 리소스 요청&lt;/strong&gt; 덕분에 아예 앱이 뻗어버리는 일이 생겼습니다.&lt;/p&gt;

&lt;p&gt;위와 같은 문제에 직면해서 원인을 분석하고 해결책을 찾기 시작했습니다.&lt;/p&gt;

&lt;h3 id=&quot;원인-분석을-위해-시도했던-방법&quot;&gt;원인 분석을 위해 시도했던 방법&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 동시에 로딩하는 콘텐츠의 개수가 문제가 될까?&lt;/strong&gt;&lt;br /&gt;
API 응답에서 내려오는 콘텐츠의 카테고리 개수를 제한해 보기&lt;br /&gt;
→ 카테고리별로 상품을 6~12개까지 표시하는데, 안정적으로 로딩할 수 있는 카테고리는 30개가 전부였습니다.&lt;br /&gt;
&lt;br /&gt;
&lt;strong&gt;한 번에 너무 많은 콘텐츠를 로딩하면 문제가 발생했습니다.&lt;/strong&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;strong&gt;2. 모바일에서만 안 되는 걸 보면, 모바일 네트워크의 속도가 느려서 그런 건 아닐까?&lt;/strong&gt;&lt;br /&gt;
데스크톱에서 인터넷의 속도를 제한시켜 해당 부분 로딩을 테스트하기&lt;br /&gt;
→ 데스크톱에서는 &lt;strong&gt;정말 오래 걸렸지만&lt;/strong&gt;, 어쨌든 로딩이 되기는 했습니다.&lt;br /&gt;
&lt;br /&gt;
&lt;strong&gt;네트워크의 속도는 근본적인 원인이 아니었습니다.&lt;/strong&gt;&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;결과적으로, &lt;strong&gt;한 번에 많은 콘텐츠를 로딩하지 않도록 개선&lt;/strong&gt;하는 작업이 필요합니다.&lt;/p&gt;

&lt;h3 id=&quot;고려했던-해결책&quot;&gt;고려했던 해결책&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;모든 리소스를 가져와서, 20개 정도의 리스트만 보여준 뒤 더 보기를 구현하는 방법&lt;br /&gt;
→ &lt;strong&gt;많은 양의 이미지를 최초에 로딩하는 것 자체가 문제&lt;/strong&gt;였기 때문에, 증상은 나아질 바 없었습니다.&lt;/li&gt;
  &lt;li&gt;API 자체에 Pagination(“이전/다음 페이지”를 끊어 보여주는 기능)을 걸어서 특정 스크롤 위치에 도달하는 경우 지속적으로 API 요청&lt;br /&gt;
→ &lt;strong&gt;프론트단에서 해결해야 할 문제&lt;/strong&gt;라고 생각했기에, 백엔드 공수가 필요한 작업은 최후의 단계로 생각했습니다.&lt;/li&gt;
  &lt;li&gt;이미지를 최초엔 처음 필요한 만큼만 로딩하고, 나머지는 필요한 타이밍에 로딩&lt;br /&gt;
→ &lt;strong&gt;가장 자연스럽고 전체적인 공수를 적게 사용하여 해결되었고, 부가적으로 리소스 이슈도 줄일 수 있었습니다!&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;우리는 해결책 3번의 방법을 사용하기 위해 &lt;strong&gt;Lazy Loading&lt;/strong&gt;을 적용했습니다.&lt;/p&gt;

&lt;h2 id=&quot;lazy-loading&quot;&gt;Lazy Loading…?&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;웹 페이지에 접근하면 그 안에 있는 내용이 모두 다운로드 됩니다.&lt;br /&gt;
하지만, 사용자가 접근한 첫 화면 이후에 다운로드 된 모든 리소스를 살펴본다는 보장은 없습니다.&lt;br /&gt;
이럴 경우, 사용자는 실제로는 필요하지 않은 리소스까지 다운로드하느라 시간과 메모리 낭비가 발생하게 됩니다.&lt;br /&gt;
&lt;strong&gt;Lazy Loading&lt;/strong&gt;을 적용한다면 위와 같은 상황을 방지할 수 있습니다.&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;페이지에 접근할 때 필요한 섹션만 로드하고, 나머지는 사용자가 필요할 때까지 로딩을 지연시키는 개념입니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Lazy Loading을 적용했을 때, 고려해야 할 부분은 다음과 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;이미지를 로딩하는 부분에만 Lazy Loading을 적용하면 될까?&lt;/li&gt;
  &lt;li&gt;Lazy Loading이 적용된 부분이 로딩 될 때, 이미지만 빈 공간으로 나오는 그 잠깐의 타이밍을 해소할 수 없을까?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202203/image1.png&quot; alt=&quot;만약 이미지 컴포넌트에 직접 적용을 시켰다면, 잠깐 동안은 다음과 같이 보일 것입니다.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 이미지 컴포넌트에 직접 적용을 시켰다면, 잠깐 동안은 위와 같이 보이게 됩니다.&lt;/p&gt;

&lt;p&gt;이런 문제를 해결하기 위해 생각한 방법은, 이미지를 로딩 해주는 컴포넌트 자체를 Lazy Loading 시키는 것입니다.&lt;br /&gt;
이제 직접 적용해 보겠습니다!&lt;/p&gt;

&lt;h2 id=&quot;lazy-loading을-적용하는-방법&quot;&gt;Lazy Loading을 적용하는 방법!&lt;/h2&gt;

&lt;p&gt;먼저 이번에 제가 적용할 Lazy Loading 기술은 Intersection Observer입니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API&quot;&gt;Intersection Observer&lt;/a&gt;란?
 → 대상 요소와 상위 요소 또는 최상위 document의 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/Viewport&quot;&gt;viewport&lt;/a&gt; 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법&lt;br /&gt;
 &lt;br /&gt;
 &lt;strong&gt;쉽게 얘기해서, 화면에 내가 지정한 대상이 보이는지를 관찰하는 API입니다.&lt;/strong&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;위 기술을 활용하여 Lazy Loading을 적용하는 원리는 다음과 같습니다.&lt;/p&gt;

&lt;p&gt;각 리스트를 뿌려주는 컴포넌트에 Intersection Observer를 적용시켜 &lt;strong&gt;현재 보이고 있는 부분은 해당하는 리스트 컴포넌트를 로딩하고, 그렇지 않은 경우는 공용으로 사용되는 Loading 컴포넌트&lt;/strong&gt;를 대신 보여줍니다.&lt;/p&gt;

&lt;p&gt;예제 코드는 다음과 같습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;자바스크립트의 IntersectionObserver를 직접 사용하여 작성한 예제&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaggedProductsList&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setShowList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;observer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;
	
  &lt;span class=&quot;nx&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;observer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;IntersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(([&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isIntersecting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 현재 observerRef로 지정한 대상이 보여지고 있는지 확인&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;setShowList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

      &lt;span class=&quot;nx&quot;&gt;observer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;observer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
	
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observerRef&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// showList의 값에 따라 로딩스크린을 표시하거나 상품을 보여준다.&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeListContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tagProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeProductCard&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tagProduct&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;))&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeListContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LoadingScreen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위와 같은 자바스크립트 코드 대신, React 환경에서는 아래와 같은 React Hook을 사용할 수 있습니다.&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;‘react-intersection-observer’ NPM 모듈을 사용하여 작성한 예제&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useInView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react-intersection-observer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaggedProductsList&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setShowList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;inView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useInView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// inView를 통해 보여지는지 구분한다.&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 각종 option을 추가해서 IntersectionObserver처럼 사용할 수 있다.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;triggerOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
	
  &lt;span class=&quot;nx&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;inView&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;setShowList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;inView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
	
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showList&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// showList의 값에 따라 로딩스크린을 표시하거나 상품을 보여준다.&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeListContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tagProduct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeProductCard&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tagProduct&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;))&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TrenbeListContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;LoadingScreen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;상황과 기호에 맞춰서 원하는 방식으로 작성하여 사용하면 됩니다.&lt;/p&gt;

&lt;h2 id=&quot;개선-후-결과&quot;&gt;개선 후 결과&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202203/after_lazy_loading.gif&quot; alt=&quot;로딩 스크린이 나오지만, 필요한 부분만 로딩되기 때문에 빠른 속도로 로딩되는 걸 확인 가능합니다.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로딩 스크린이 나오지만, 필요한 부분만 로딩되기 때문에 빠른 속도로 로딩되는 걸 볼 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;최초에 로딩되는 순간에 호출되는 리소스: 120mb (이미지 전체) → 평균 &lt;strong&gt;9.6mb&lt;/strong&gt; (필요한 이미지)&lt;br /&gt;
첫 페이지 로딩 완료 속도: 약 30초 → 평균 약 &lt;strong&gt;5.5&lt;/strong&gt;초&lt;/em&gt;&lt;/p&gt;

&lt;/blockquote&gt;

&lt;p&gt;시간으로 비교했을 때, &lt;strong&gt;500%&lt;/strong&gt; 정도의 성능 개선이 있었습니다.&lt;/p&gt;

&lt;p&gt;물론, 극단적으로 이미지가 많이 호출되는 페이지라서 더 눈에 띄는 결과를 보여준 것도 있지만,&lt;br /&gt;
리소스를 아끼고 로딩 속도를 합리적으로 개선할 수 있는 방법임에는 분명합니다.&lt;br /&gt;
필요한 경우 사용자 경험을 위해 적재적소에 활용하면 좋을 것 같습니다.&lt;/p&gt;</content><author><name>이리</name></author><category term="Lazy Loading" /><category term="FrontEnd" /><category term="React" /><category term="Javascript" /><summary type="html">FE Lazy Loading 적용기</summary></entry><entry><title type="html">리덕스 사가(Redux Saga)란 무엇인가?</title><link href="https://trenbe.github.io/2022/05/25/Redux-Saga.html" rel="alternate" type="text/html" title="리덕스 사가(Redux Saga)란 무엇인가?" /><published>2022-05-25T15:00:00+00:00</published><updated>2022-05-25T15:00:00+00:00</updated><id>https://trenbe.github.io/2022/05/25/Redux%20Saga</id><content type="html" xml:base="https://trenbe.github.io/2022/05/25/Redux-Saga.html">&lt;h2 id=&quot;리덕스redux&quot;&gt;리덕스(Redux)&lt;/h2&gt;
&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_0.png&quot; alt=&quot;redux-logo.png&quot; /&gt;&lt;/p&gt;

&lt;h5 id=&quot;리덕스redux의-필요성&quot;&gt;리덕스(Redux)의 필요성&lt;/h5&gt;

&lt;p&gt;리액트(React)는 상태(State)를 가지고 어떻게 돔(DOM)으로 잘 변형할지에 대해 다루고 있습니다. 여기서 상태란 애플리케이션(Application)이 기본적으로 가지고 있는 아주 중요한 요소인데요. 애플리케이션의 규모가 커질수록 상태 즉 데이터를 관리하기가 매우 까다롭기 때문에 상태를 잘 다루고 실제 돔(DOM)으로 업데이트하기까지 이를 도와주는 라이브러리가 필요합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_1.png&quot; alt=&quot;0.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;리액트(React) 애플리케이션(Application)을 이루는 각각의 컴포넌트(Component) 요소들은 외부 상태를 공유받고 싶을 때 프롭스(Props)를 통해서만 전달받을 수 있습니다.
이러한 형태적인 특징 때문에 여러 개의 컴포넌트를 중첩시키고 조합시키면서 복잡한 계층 구조를 가지게 되는 상황에서는 상태(State)에 대한 흐름을 추적하기 어렵고
UI가 변경될 때마다 상태에 대한 의존성을 가진 컴포넌트들에 영향이 없는지 전달 구조를 일일이 확인해야 하는 등 관리하는 데 큰 어려움을 겪게 됩니다.
이러한 문제점을 극복하기 위해 상태를 전역적으로 관리하고 가공해 UI로 쉽게 변형할 수 있도록 도와주는 라이브러리인 리덕스(Redux)를 사용하게 되었습니다.&lt;/p&gt;

&lt;p&gt;그렇다면 리덕스는 어떤 방식으로 상태를 관리하는지에 대해 간단하게 설명해드리겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_2.png&quot; alt=&quot;flux-simple-f8-diagram-1300w.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;단방향으로 데이터가 흐르게 하는 Flux 아키텍처 아이디어를 잘 구현한 리덕스의 구조는 상태(State)를 저장하는 스토어(Store), 상태를 조작하는 리듀서(Reducer), 액션(Action)을 전달하는 디스패처(Dispatcher) 함수로 이뤄져 있습니다.&lt;/p&gt;

&lt;p&gt;*Flux에 대한 자세한 소개는 &lt;a href=&quot;https://facebook.github.io/flux/docs/in-depth-overview&quot;&gt;Flux 공식 사이트-개요&lt;/a&gt; 페이지를 참고해주세요&lt;/p&gt;
&lt;h5 id=&quot;상태값을-저장하는-저장소--스토어store&quot;&gt;상태값을 저장하는 저장소 : 스토어(Store)&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;스토어(Store)는 모든 상태 값을 저장하며 상태 값을 조작할 리듀서(Reducer) 함수를 인자로 받습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;상태를-조작하는-함수--리듀서reducer&quot;&gt;상태를 조작하는 함수 : 리듀서(Reducer)&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;리듀서(Reducer) 함수는 초기 상태 값과 액션(action)을 인자로 받아 액션에 조작할 상태(State)를 지정하는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;액션을-전달하는-함수--디스패처-함수dispatcher&quot;&gt;액션을 전달하는 함수 : 디스패처 함수(Dispatcher)&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;디스패처(Dispatcher) 함수는 액션 값과 상태에 관한 데이터를 리듀서(Reducer) 함수에 전달하는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;어떤-변화가-필요할-때-발생시키는-신호-객체--액션action&quot;&gt;어떤 변화가 필요할 때 발생시키는 신호 객체 : 액션(Action)&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;어떤 데이터를 어떻게 바꿀 것이냐에 대한 일종의 신호를 의미합니다. 어떤 동작에 대해 선언된 액션(Action) 객체는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type&lt;/code&gt;이라는 필드를 변화가 필요한 상황을 인지하기위해 사용하며 이 객체에 변화할 때 필요한 데이터를 담을 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;다음 이미지처럼 사용자에게 보이는 리액트(React) 컴포넌트(Component)에서 특정 이벤트 즉 액션(Action)이 발생하면 액션이 스토어(Store)에 디스패치(Dispatch) 됩니다. 스토어에서는 다시 리듀서(Reducer)에 액션을 전달하고 리듀서는 상태(State)를 가공해서 새로운 상태를 스토어로 전달하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_3.png&quot; alt=&quot;redux흐름.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;리덕스(Redux) 사용 시에는 따라야 할 세 가지 원칙이 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;리덕스를 사용하는 어플리케이션엔 하나의 스토어(Store)만이 존재할 것&lt;/li&gt;
  &lt;li&gt;상태(State)를 직접 변경할 수 없음, 상태를 변경하기 위해서는 액션(Action)이 디스패치(dispatch)되어야 할 것&lt;/li&gt;
  &lt;li&gt;리듀서(Reducer)는 ‘순수 함수’로 작성할 것&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;여기서 “스토어(Store)의 상태 변화를 다루는 리듀서(Reducer)는 순수 함수로 작성해야 한다”라는 원칙 때문에 리덕스 미들웨어(Redux Middleware)의 필요성이 생기게되었는데요.&lt;/p&gt;

&lt;p&gt;리듀서에서 부수 효과(Side Effect)를 가진 함수를 실행할 경우 순수 함수 사용 원칙을 위배하게 됩니다. 그로 인한 문제를 사전에 방지하기 위해 리덕스는 리덕스 미들웨어를 통해 부수 효과를 처리하기를 원합니다.&lt;/p&gt;

&lt;h2 id=&quot;리덕스-미들웨어redux-middleware&quot;&gt;리덕스 미들웨어(Redux Middleware)&lt;/h2&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_4.png&quot; alt=&quot;다운로드.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;미들웨어(Middleware)는 리덕스(Redux)의 기능을 확장하기 위한 수단으로, 디스패치(dispatch)함수를 감싸는 역할을 수행합니다. 여러 미들웨어는 서로 체이닝이 되고 최종적으로 체이닝된 함수가 스토어의 디스패치 함수로 주입됩니다. 이 디스패치 함수에 액션(Action) 객체를 담아 호출하게 되면 맨 마지막에 감싸진 미들웨어가 액션을 전달받아 작업을 수행하고 작업을 마치면 다음 감싸진 미들웨어를 실행하는 동작을 반복합니다. 이 모든 미들웨어를 지나면 리듀서(Reducer)로 액션이 전달되어 상태가 업데이트될 것입니다.&lt;/p&gt;

&lt;h5 id=&quot;참고-미들웨어를-리덕스-스토어에-연결하는-코드&quot;&gt;참고. 미들웨어를 리덕스 스토어에 연결하는 코드&lt;/h5&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;applyMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;middlewares&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;middlewares&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;store&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_dispatch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Dispatching while constructing your middleware is not allowed. &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Other middleware would not be applied to this dispatch.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;middlewareAPI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;getState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;middlewares&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;middleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;middlewareAPI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;_dispatch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;compose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_objectSpread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({},&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_dispatch&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;리덕스-미들웨어-redux-middleware-소개&quot;&gt;리덕스 미들웨어 (Redux Middleware) 소개&lt;/h3&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;1리덕스-프로미스-미들웨어redux-promise-middleware&quot;&gt;1.리덕스 프로미스 미들웨어(Redux Promise Middleware)&lt;/h5&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;redux&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;middleware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 미들웨어는 프로미스(Promise) 기반의 비동기 작업을 조금 더 편하게 해주는 미들웨어입니다. 
리덕스 프로미스(Redux promise)를 사용하면 페이로드(Payload)로 전달된 객체가 만약 프로미스(Promise) 객체라면 통신에 대한 응답이 올 때까지 기다린 이후 결과값을 리듀서(Reducer)에게 전달합니다.
비교적 코드가 단순하기 때문에 기본적인 비동기 또는 조건부로 작업을 수행해야 하는 경우 유용합니다. 
해당 미들웨어를 사용하지 않으면 액션(Action) 생성함수는 페이로드로 프로미스(Promise) 객체 자체를 실어 보내므로 리듀서에 상태(State)를 정상적으로 전달할 수 없습니다.&lt;/p&gt;

&lt;h5 id=&quot;2리덕스-썽크redux-thunk&quot;&gt;2.리덕스 썽크(Redux-Thunk)&lt;/h5&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;redux&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;thunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;리덕스 창시자인 &lt;a href=&quot;https://golden.com/wiki/Dan_Abramov_(software_engineer)-99B8RJM&quot;&gt;Dan Abramov&lt;/a&gt;(댄 아브라모프)가 만든 가장 많이 사용되는 비동기 작업 미들웨어입니다. 리덕스 썽크(Redux Thunk)는 객체가 아닌, 동기 또는 비동기 작업을 수행할 수 있는 함수를 말합니다.&lt;/p&gt;

&lt;p&gt;리덕스 썽크(Redux Thunk)를 사용하면 액션 생성자가 반환하는 객체로는 처리하지 못했던 작업을 함수를 반환할 수 있게 되면서 반환받은 함수를 통해 다양한 작업이 가능해졌다는 점이 장점입니다.&lt;/p&gt;

&lt;h5 id=&quot;3리덕스-로거redux-logger&quot;&gt;3.리덕스 로거(Redux logger)&lt;/h5&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;redux&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;리덕스 로거(Redux Logger)를 사용하면 리덕스 미들웨어를 사용해 개발을 진행할 때, 리듀서(Reducer)가 실행되기 전과 실행된 후를 로그(log)로 편리하게 확인하여 비교할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;리덕스-사가&quot;&gt;리덕스 사가&lt;/h2&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;리덕스-사가redux-saga란&quot;&gt;리덕스 사가(Redux Saga)란?&lt;/h5&gt;

&lt;p&gt;리덕스 썽크(Redux Thunk) 다음으로 가장 많이 사용되고 있는 리덕스 사가(Redux Saga)는 리액트/리덕스 애플리케이션에서 비동기적으로 API를 호출하여 데이터를 가져오는 일과 같은 부수 효과(Side Effect)를 쉽게 처리하기 위해 사용하는 라이브러리입니다.
때에 따라 기존 요청을 취소 처리해야 한다거나 여러 개의 API를 순차적으로 호출해야 하는 등의 좀 더 까다로운 비동기 작업을 다루는 상황에 유용합니다.&lt;/p&gt;

&lt;p&gt;리덕스 사가(Redux Saga)는 애플리케이션에서 전적으로 부수 효과(Side Effect)만을 담당하여 처리합니다.
비동기 함수 호출 결과 데이터를 통해 성공, 실패 여부를 판단하고 상태를 업데이트시키는 등의 작업(Task)을 제어할 수 있으며,
스토어에 접근하거나 특정 액션(Action)을 디스패치(Dispatch) 하여 다른 사가함수를 실행시킬 수 있습니다.&lt;/p&gt;

&lt;p&gt;리덕스 사가(Redux Saga)는 부수 효과들을 처리하기 위해 제너레이터(Generator)라는 ES6 기능을 사용합니다. 제너레이터를 사용하게되면 비동기의 흐름을 표준 동기 코드처럼 보이게 하여 비동기 흐름을 쉽게 읽고 쓰고 테스트할 수 있어 복잡한 워크플로를 관리하는 데 매우 효과적입니다.&lt;/p&gt;

&lt;h5 id=&quot;제너레이터generator란&quot;&gt;제너레이터(Generator)란?&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;제너레이터(Generator)에 대한 간단한 설명&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;function*&lt;/code&gt; 키워드를 사용하여 만든 제너레이터 함수를 호출했을 때는 한 객체가 반환되는데요, 이를 제너레이터(Generator) 또는 이터레이터(iterator) 객체라고도 부릅니다. 제너레이터의 내부 코드를 실행하기 위해서는 이 객체가 가지고 있는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드를 호출 해야만 하는데요. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드의 호출 시마다 순차적으로 원소들을 탐색하고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt;키워드를 반환 포인트로 여기며 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; 와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;done&lt;/code&gt; 프로퍼티를 가진 새로운 객체를 반환합니다.&lt;/p&gt;

    &lt;p&gt;다시 말해서 제너레이터 안에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 키워드를 사용하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 키워드를 포인트로 코드의 흐름이 멈추게 되고 멈춘 코드의 흐름을 이어 나가기 위해서는 다시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드를 호출하는 방식으로 일시 정지와 재시작 기능을 사용할 수 있습니다.&lt;/p&gt;

    &lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/generator_1.gif&quot; alt=&quot;화면-기록-2022-03-09-오후-4.09.13.gif&quot; /&gt;&lt;/p&gt;

    &lt;p&gt;제너레이터(Generator)의 이러한 흐름제어를 이용하여 리덕스 사가(Redux saga)에서는 액션(Action)을 모니터링하고 있다가 구독하고 있는 특정 액션이 발생했을 때 원하는 비동기 함수를 실행시킬 수 있습니다.&lt;/p&gt;

    &lt;p&gt;다음은 사가함수가 어떻게 액션(Action)의 발생여부를 알아내어 원하는 함수를 실행할 수 있는지에 대해 설명하기 위해 간단히 구현한 예시입니다.&lt;/p&gt;

    &lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/generator_2.gif&quot; alt=&quot;화면-기록-2022-03-09-오후-8.04.00 (1).gif&quot; /&gt;&lt;/p&gt;

    &lt;p&gt;간단히 설명하자면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch&lt;/code&gt;라는 사가 함수는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watchSaga&lt;/code&gt;로부터 제너레이터를 전달받고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드를 통해 내부 코드를 실행시킵니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watchSaga&lt;/code&gt; 내부에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while문&lt;/code&gt;을 통해 액션을 기다리고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드의 인자를 통해 전달받은 액션을 계속해서 모니터링하고 있다가 특정 액션이 디스패치된 경우 실행해야 하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker&lt;/code&gt; 함수를 호출할 수 있습니다.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;리덕스-사가-api&quot;&gt;리덕스 사가 API&lt;/h3&gt;
&lt;hr /&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;applyMiddleware&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;redux&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createSagaMiddleware&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;redux-saga&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reducer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./reducers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mySaga&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./sagas&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// saga 미들웨어를 생성합니다.&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sagaMiddleware&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createSagaMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 스토어에 적용시킵니다.&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;store&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createStore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;reducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;applyMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sagaMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 그리고 saga를 실행합니다.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;sagaMiddleware&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mySaga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;1-createsagamiddleware&quot;&gt;1. createSagaMiddleware&lt;/h5&gt;

&lt;p&gt;리덕스 사가(Redux Saga)에서 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createSagaMiddleware&lt;/code&gt; API를 사용하면 사가 미들웨어(Saga Middleware)를 생성할 수 있습니다. 그리고 생성된 사가 미들웨어는 리덕스(Redux)에서 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applyMiddleware&lt;/code&gt; API를 호출할 때 인자로 넘겨 리덕스 미들웨어(Redux Middleware)로 추가할 수 있습니다.&lt;/p&gt;

&lt;h5 id=&quot;2middlewarerunsaga-args&quot;&gt;2.middleware.run(saga, …args)&lt;/h5&gt;

&lt;p&gt;사가 미들웨어(Saga Middleware)의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run&lt;/code&gt; 메서드를 통해 사가(Saga)를 실행할 수 있습니다. 사가가 여러 개가 존재하는 경우 진입점(Entry Point)에 해당하는 루트 사가(Root Saga)를 만들고 이 루트 사가를 실행시켜줄 수 있습니다.&lt;/p&gt;

&lt;p&gt;사가(Saga)가 실행되면 해당 제너레이터(Generator) 함수를 호출하여 반복 가능한 제네레이터를 획득하게 되는데 해당 제네레이터의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; 메서드를 통해 이펙트(Effect) 타입을 확인하고 해당 이펙트에 대해 지시된 동작을 수행하는 작업을 반복하게 됩니다.&lt;/p&gt;

&lt;h3 id=&quot;리덕스-사가-이펙트effect-함수&quot;&gt;리덕스 사가 이펙트(Effect) 함수&lt;/h3&gt;
&lt;hr /&gt;
&lt;p&gt;앞서 언급한 이펙트(Effect)라고 하는 것은 어떤 기능을 수행하기 위해 주어진 함수와 인자들을 담은 명령 객체를 미들웨어에게 전달하는데 이러한 명령 객체를 말합니다. 이펙트를 전달받은 미들웨어는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 된 이펙트들을 확인하며 정확한 명령이 포함되었는지 검사하고, 이펙트 타입에 따라 어떻게 이펙트를 수행할지 결정합니다.&lt;/p&gt;

&lt;p&gt;다음은 이펙트를 설명하기 위한 단순한 예제입니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeEvery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;redux-saga/effects&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Our worker Saga: will perform the async increment task&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;incrementAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;INCREMENT&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watchIncrementAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeEvery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;INCREMENT_ASYNC&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;incrementAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다음은 위 코드의 흐름을 이미지화 한것입니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_5.png&quot; alt=&quot;redux_saga_flow.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사가(Saga)는 INCREMENT_ASYNC 액션(Action)을 모니터링하고 있다가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delay&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;put&lt;/code&gt;이라는 이펙트(Effect)를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt;하고 자바스크립트 객체를 반환합니다. 따라서 INCREMENT_ASYNC 액션이 디스패치 되면 처음 1초를 기다렸다가 1초가 지나면 { type: ‘INCREMENT’ } 액션을 디스패치(Dispatch) 하라는 각각의 이펙트 객체가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 되고 미들웨어에서는 해당 명령에 대한 수행이 이루어지게 됩니다.&lt;/p&gt;

&lt;p&gt;이러한 이펙트를 생성할 때는 redux-saga/effects 패키지에 있는 라이브러리들이 제공하는 함수들을 사용하며 주요하게 사용하는 이펙트 함수 타입은 다음과 같습니다.&lt;/p&gt;

&lt;h4 id=&quot;이펙트effect-함수-타입&quot;&gt;이펙트(Effect) 함수 타입&lt;/h4&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;1forkfn-args&quot;&gt;1.fork(fn, …args)&lt;/h5&gt;

&lt;p&gt;매개변수로 전달된 함수를 비동기적으로 실행합니다. 비동기적으로 실행되기 때문에 블로킹(Blocking)이 발생하지 않는 새로운 맥락의 사가(Saga) 작업(Task) 생성하게 됩니다.&lt;/p&gt;

&lt;h5 id=&quot;2callfn-args&quot;&gt;2.call(fn, …args)&lt;/h5&gt;

&lt;p&gt;매개변수로 전달된 동기 혹은 비동기 함수를 실행합니다. 전달받은 함수가 비동기 함수인 경우 해당 함수가 수행(resolve)될 때까지 기다렸다가 결과값을 반환하므로 블로킹(Blocking)이 발생하게 됩니다.&lt;/p&gt;

&lt;h5 id=&quot;3putaction&quot;&gt;3.put(action)&lt;/h5&gt;

&lt;p&gt;액션(Action)을 디스패치(dispatch) 합니다. 일반적으로 워커 사가(Worker Saga)에서 API 성공/실패 여부에 따라 상태를 반영하기 위해 리듀서에 액션을 디스패치 하는 용도로 많이 사용합니다.&lt;/p&gt;

&lt;h5 id=&quot;4takeeverypattern-saga-args&quot;&gt;4.takeEvery(pattern, saga, …args)&lt;/h5&gt;

&lt;p&gt;액션(Action)이 디스패치(dispatch) 될 때마다 새로운 작업(Task) 분기(fork)합니다. 작업이 동시에 중복으로 발생해도 문제가 없는 경우에 사용해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeEvery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fork&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;take&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fork&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;5takelatestpattern-saga-args&quot;&gt;5.takeLatest(pattern, saga, …args)&lt;/h5&gt;

&lt;p&gt;액션(Action)이 디스패치(을)될 때 이전에 실행 중인 작업(Task)이 있다면 취소하고 새로운 작업을 분기(fork)합니다. 이전 API 요청를 무시하고 최신데이터를 받아올 수 있도록 합니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fork&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lastTask&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;take&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;cancel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// cancel is no-op if the task has already terminated&lt;/span&gt;

      &lt;span class=&quot;nx&quot;&gt;lastTask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fork&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;6selectselector-args&quot;&gt;6.select(selector, …args)&lt;/h5&gt;

&lt;p&gt;리듀서(Reducer)에 있는 특정 상태를 리덕스 사가(Redux)로 가져와서 사용할 수 있습니다. 블로킹이 발생하여 셀렉터(Selector) 함수가 정보를 가져온 이후에 다음 작업(Task)을 수행할 수 있습니다.&lt;/p&gt;

&lt;h5 id=&quot;7canceltask&quot;&gt;7.cancel(task)&lt;/h5&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield fork()&lt;/code&gt;는 실행되는 작업에 대한 오브젝트를 반환합니다. 이 오브젝트를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cancel&lt;/code&gt; 메서드의 인자로 넘기면 이미 비동기로 실행 중인 작업을 취소할 수 있습니다. 또한 작업을 취소한 경우라도 해당 워커 사가(worker Saga) 내의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;finally&lt;/code&gt; 구간 안에서는 취소된 작업에 대해 처리할 수 있습니다.&lt;/p&gt;

&lt;h5 id=&quot;8추가-설명&quot;&gt;8.추가 설명&lt;/h5&gt;

&lt;p&gt;모든 이펙트(Effect)는 반드시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 키워드와 함께 사용해야 합니다. 리덕스 사가의 이펙트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blocking effect&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Non-Blocking effect&lt;/code&gt;로 구분됩니다.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blocking Effect&lt;/code&gt;는 처리가 완료될 때까지 기다리며 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Non-Blocking Effect&lt;/code&gt;는 완료를 기다리지 않고 진행합니다. 대표적인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blocking Effect&lt;/code&gt;로는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;call&lt;/code&gt;이 있고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Non-blocking Effect&lt;/code&gt;에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fork&lt;/code&gt;가 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;웹앱에서-사용하는-리덕스-사가-패턴-예시&quot;&gt;웹앱에서 사용하는 리덕스 사가 패턴 예시&lt;/h2&gt;
&lt;hr /&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
├── container
│   ├── action.js
│   ├── constants.js
│   ├── index.js
│   ├── reducer.js
│   ├── selectors.js
    └── saga.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_6.png&quot; alt=&quot;workflow.png&quot; /&gt;
출처:&lt;a href=&quot;https://github.com/react-boilerplate/react-boilerplate&quot;&gt;https://github.com/react-boilerplate/react-boilerplate&lt;/a&gt;&lt;/p&gt;

&lt;h5 id=&quot;1-useinjectreducer-useinjectsaga-그리고-key&quot;&gt;1. useInjectReducer, useInjectSaga 그리고 key&lt;/h5&gt;

&lt;p&gt;현재 트렌비 프론트에서는 보일러 플레이트(Boilerplate)를 사용하여 컨테이너 컴포넌트에서 고유한 키(key)를 생성하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useInjectSaga&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useInjectReducer&lt;/code&gt; 유틸함수를 통해 스토어(Store)에 사가(Saga)와 리듀서(Reducer)를 주입하고 있습니다.&lt;/p&gt;

&lt;p&gt;이러한 구조를 사용하여 루트 사가(Root Saga)를 별도로 만들지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useInjectReducer&lt;/code&gt; 내부에서 모니터링용 사가(Watcher Saga)를 실행시켜주며 특정 액션(Action)이 발생하면 상태(State)를 갱신하여 스토어(Store)에 전달할 수 있고 고유한 키(key)를 통해 특정한 상태를 가져올 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Signup&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./Signup&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reducer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./reducer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./saga&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useInjectReducer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;utils/injectReducer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useInjectSaga&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;utils/injectSaga&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupRequest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./actions&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserPage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useInjectReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;reducer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;useInjectSaga&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;saga&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onSignUp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onSignUpRequest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;sms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;mailing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;socialId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;tempUserId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;onSignUp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SignUpButton&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;onClick&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onSignUpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;가입하기&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SignUpButton&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;



&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;2-actionjs&quot;&gt;2. action.js&lt;/h5&gt;

&lt;p&gt;특정 액션(Action)을 생성하는 함수만을 모아놓은 파일입니다. 디스패치(dispatch) 함수에 이 액션 생성 함수를 호출하여 전달하면 미들웨어를 지나 리듀서(Reducer)에게 액션이 전달됩니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_REQUEST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_SUCCESS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupFailure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_FAILURE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;3-reducerjs&quot;&gt;3. reducer.js&lt;/h5&gt;

&lt;p&gt;액션(Action)이 발생할 때 상태(State)에 대한 업데이트를 구현하는 리듀서(Reducer) 함수가 있는 파일입니다. 현재 상태와 전달받은 액션을 참고하여 특정 액션 타입에 따라 새롭게 상태를 만들고 이를 반환하여 업데이트합니다. 상태가 전달되지 않은 경우에 초깃값(InitialState)를 기본값으로 지정하고 있습니다. 미들웨어를 사용하는 경우 해당 미들웨어를 지나며 데이터를 추가로 전달받을 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fromJS&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;immutable&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;SIGNUP_REQUEST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;SIGNUP_FAILURE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;AUTH_VERIFY_STATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;SIGNUP_SUCCESS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./constants&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initialState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fromJS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;facebookConnects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;processing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;authState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AUTH_VERIFY_STATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;STANDBY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;signUpSuccessRedirectData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;userReducer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initialState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_REQUEST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt;
									&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;facebookConnects&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt;
									&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;processing&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_SUCCESS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;signUpSuccessRedirectData&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
									&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;processing&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SIGNUP_FAILURE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupErrorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
									&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;processing&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;userReducer&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;4selectorjs&quot;&gt;4.selector.js&lt;/h5&gt;

&lt;p&gt;셀렉터(Selector)는 스토어에서 필요한 데이터를 가져오거나, 계산을 수행해서 원하는 형태의 데이터를 가져오는 역할을 합니다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reselect&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createSelector&lt;/code&gt; 함수에 선택자 함수를 전달하고 그 선택자 함수가 전달한 값이 이전과 같다면 캐싱을 통해 동일한 계산을 방지하는 메모이제이션(memoization) 기능을 동작시킬 수 있습니다&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createSelector&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;reselect&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;selectRoute&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;router&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toJS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;makeSelectLocation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;createSelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;selectRoute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;makeSelectLocation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;5-sagajs&quot;&gt;5. Saga.js&lt;/h5&gt;

&lt;p&gt;일반적으로 사가(Saga)는 액션을 구독하는 모니터링 사가(Watcher Saga)와 실제 작업을 수행하는 워커 사가(Worker Saga)의 구성으로 만듭니다.
모니터링하는 사가는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useInjectSaga&lt;/code&gt; 유틸 함수에 키와 함께 전달되어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sagaMiddleware.run&lt;/code&gt;에 인자로 담겨 실행되고 이펙트(Effect) 타입에 따라 
사가를 실행하는 실행부에게 어떠한 동작을 수행해야 할지 전달합니다. 예를 들어 아래와 같이 이펙트 타입이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;takeLatest&lt;/code&gt;인 경우 특정 액션을 감시하고 있다가 
액션이 발생할 때 인자로 전달된 워커 사가를 분기(fork)합니다.&lt;/p&gt;

&lt;p&gt;워커 사가(Worker Saga)가 호출되면 내부 이펙트(Effect) 타입에 따라 어떤 동작을 수행해야 할지 판단하여 실행 부에 전달하고 수행 중에 발생한 에러는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;try... catch&lt;/code&gt;문을 통해 처리할 수 있습니다. 보통 API를 정상적으로 수행한 경우 리듀서에 해당 데이터를 액션과 함께 실어 보내 상태 업데이트를 진행합니다. 
실패한 경우에도 실패한 상태에 대해 업데이트하는 액션을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;put&lt;/code&gt; 이펙트 함수를 통해 디스패치(dispatch) 할 수 있습니다.&lt;/p&gt;

&lt;p&gt;추가적인 설명으로 signup Saga 함수 내에 작성한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt; 표현식은 다른 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;제너레이터(generator)&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;이터러블(iterable)&lt;/code&gt; 객체에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; 를 위임할 때 사용됩니다.
여기서 ‘위임’이라는 단어는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt; 키워드가 붙은 제너레이터를 대상으로 반복을 수행하고, 산출 값들을 바깥으로 전달한다는 것을 의미합니다.&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeLatest&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;redux-saga/effects&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getResource&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;containers/saga&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;SIGNUP_REQUEST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./constants&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

	&lt;span class=&quot;nx&quot;&gt;signupFailure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;signupSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./actions&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signupInfo&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;makeSelectLocation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
     &lt;span class=&quot;c1&quot;&gt;// ...생략&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;signup&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;applyUserResource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;TLogger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fieldError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ERROR_CODE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CONFLICTED_DATA&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fieldError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;conflict&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fieldError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;해당 이메일은 이미 가입되어 있습니다.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupFailure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;userRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;takeLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SIGNUP_REQUEST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 보통 리덕스 사가 패턴(Redux Saga Pattern)에서 모니터링 역할을 하는 사가(Watcher Saga)와 작업을 수행하는 사가(Worker Saga)를 분리해서 작성하는데 그 이유는
분리해서 작성했을 때 문제가 된 부분이 어딘지 더 빠르게 파악할 수 있고 요구 사항이 변경될 때도 해당되는 한 곳에서만 변경하면 되기 때문입니다.&lt;/p&gt;

&lt;p&gt;지금까지 리덕스 사가(Redux Saga)에 대해서 말씀드렸습니다.&lt;/p&gt;

&lt;p&gt;저희 팀에서 리덕스 사가를 통해 비동기 통신이 많은 복잡한 구조의 애플리케이션을 관리하면서 느낀 장점은
비동기의 흐름을 동기적으로 읽을 수 있게 해주어 흐름제어를 쉽게 할 수 있다는 점과 다른 라이브러리에서 제공하는 기능보다 다양한 기능들을 제공받아 사용할 수 있다는 점이었습니다.&lt;/p&gt;

&lt;p&gt;하지만 한편으로는 리덕스 사가(Redux Saga)를 사용할 때 늘어나는 보일러 플레이트(boilerplate) 코드들과 사가 함수 재사용에 대한 제약들로 인해 쓸데없이 추가되는 코드들이 많아지는 것을 보면서 이 또한 유지보수를 어렵게 하는 것이 아닌가 하는 생각도 하게 되었습니다.
그래서 앞으로의 애플리케이션의 유지보수를 생각했을 때 지금처럼 비동기 API 호출 로직들을 리덕스(Redux) 구조에 의존하기보다는 따로 분리해서 관리할 방법이 있다면 그 방법을 한번 시도해보면 좋을 거 같다고 팀원들과 입을 모으게 되었습니다.&lt;/p&gt;

&lt;p&gt;스토어팀은 앞서 말씀드린 리덕스 사가의 단점들을 보완하기 위한 방법으로 비동기 함수를 스토어와 분리할 수 있도록 도움을 주는 SWR과 리액트 쿼리(React Query)라고하는 라이브러리의 존재를 알게 되었습니다.
그리고 다음에 이어지는 내용으로 그 라이브러리들을 간단히 소개해드리고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;비동기-함수를-스토어와-분리하기-위한-방법&quot;&gt;비동기 함수를 스토어와 분리하기 위한 방법&lt;/h2&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;1-swr-stale-while-revalidate&quot;&gt;1. SWR (Stale While Revalidate)&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_7.png&quot; alt=&quot;스크린샷 2022-03-07 오후 6.02.25.png&quot; /&gt;&lt;/p&gt;
&lt;h5 id=&quot;swr이란&quot;&gt;SWR이란?&lt;/h5&gt;

&lt;p&gt;SWR 은 리액트(React) 프레임워크인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Next.js&lt;/code&gt;를 만든 팀에서 개발한 라이브러리로 비동기 요청을 위한 훅(Hook)을 제공합니다.&lt;/p&gt;

&lt;p&gt;데이터 재검증을 하는 동안 의도적으로 캐시(Cache) 된 데이터를 보여주고 최종적으로 재검증된 최신화된 데이터를 보여주는 전략을 쓰고 있습니다.&lt;/p&gt;

&lt;p&gt;또한 네트워크 재연결 또는 사용자 이벤트가 잡힐 때마다 컴포넌트는 지속적, 반응적으로 데이터 업데이트 스트림을 받게 되어 최신화된 데이터로 빠르게 UI 업데이트 구현이 가능하다는 점이 장점입니다.&lt;/p&gt;

&lt;p&gt;SWR에서 제공하는 useSWR이라는 훅(Hook)을 사용하면 데이터가 필요한 컴포넌트 안에서 단 한 줄의 코드로 데이터를 가져오고 
이 데이터를 통해 알아낸 현재 요청 상태(error, isLoading, ready)에 따라 해당하는 UI를 반환할 수 있습니다.&lt;/p&gt;

&lt;p&gt;또한 데이터를 재사용해야 할 때도 위와 같이 컴포넌트에서 필요한 데이터에 대해서 명시적으로 불러오기만 하면 되기 때문에 
독립적인 컴포넌트에서도 쉽고 간단하게 데이터를 불러올 수 있으며 그 요청이 자동으로 중복 제거, 캐시, 공유되므로, 단 한 번의 요청으로 데이터를 받아서 UI를 업데이트시킬 수 있습니다.&lt;/p&gt;

&lt;h4 id=&quot;2-리액트-쿼리react-query&quot;&gt;2. 리액트 쿼리(React Query)&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/imgs/posts/202205/redux_saga_8.png&quot; alt=&quot;스크린샷 2022-03-10 오후 12.26.27.png&quot; /&gt;&lt;/p&gt;

&lt;h5 id=&quot;리액트-쿼리react-query란&quot;&gt;리액트 쿼리(React Query)란?&lt;/h5&gt;

&lt;p&gt;리액트 쿼리(React Query)는 React 앱에서 비동기 로직(서버 상태 가져오기, 캐싱, 동기화 및 업데이트)을 쉽게 다루게 해주는 라이브러리입니다. 패칭(Fetching) 후 성공과 실패에 대한 처리를
리덕스 사가(Redux Saga)에서는 직접 사가(Saga) 안에서 try catch 문을 통해 처리를 해주었다면 리액트 쿼리에서는 성공 여부를 판단할 수 있는 필드값을 전달해 주어 조건부 렌더링을 쉽게 처리할 수 있습니다.&lt;/p&gt;

&lt;p&gt;또한 리액트 쿼리는 라이브러리가 알아서 캐싱 및 리페칭(Re-Fetching)을 해주기 때문에 필요할 때마다 뷰(View)에서 최신 데이터를 보여줄 수 있고 무수히 많은 보일러 플레이트(Boilerplate) 코드를 생성했던 
리덕스 사가와는 달리 고유한 키값 설정으로 데이터 페칭 요청을 구분하고 있어 작은 보일러 플레이트 코드만으로도 전역적으로 데이터를 가져와 페이지를 빠르게 갱신할 수 있다는 장점이 있는 라이브러리입니다&lt;/p&gt;

&lt;h4 id=&quot;마치며&quot;&gt;마치며&lt;/h4&gt;
&lt;hr /&gt;
&lt;p&gt;두 라이브러리 모두 비동기 통신 후 상태관리가 필요할 때 리덕스(Redux) 구조에 얽매이지 않고 쉽고 빠르게 상태를 업데이트해서 뷰를 완성할 수 있도록 도와주는데요.
저희 팀에서는 리덕스 사가(Redux Saga)처럼 좀 더 복잡한 애플리케이션에 적합한 라이브러리로 SWR보다는 다양한 기능을 제공하는 리액트 쿼리에 관심을 갖게 되었습니다.
그리고 더 나아가 진입장벽이 낮은 리액트 쿼리를 웹앱에 빠르게 도입해 사용해보니 리덕스의 구조를 더 이상 따르지 않고도 상태 업데이트를 편하고 쉽게 할 수 있다는 것을 직접 느끼게 되었고 유지보수 측면에서도 긍정적인 효과를 기대해볼 수 있었습니다.&lt;/p&gt;

&lt;p&gt;하지만 여전히 트렌비 웹앱에는 리덕스와 리덕스 사가의 조합으로 상태를 관리하는 페이지들이 많이 있고 당장 모든 페이지에 리액트 쿼리를 도입하는 것은 구조적인 문제가 있기에 쉽지 않은 일일 것입니다. 그래서 저희 스토어팀에서는 리덕스 사가와 리액트 쿼리 두 개를 동시에 사용하면서도
최대한 리덕스 구조에 대한 의존도를 줄일 수 있도록 그 둘을 적재적소에 각각 사용하는 방식으로 더 이상의 유지보수를 어렵게 하는 복잡한 구조를 만들지 않기 위해 노력하고 있습니다.&lt;/p&gt;

&lt;p&gt;앞으로도 저희 팀에서는 리액트 쿼리 도입을 시작으로 더 최선이 될 수 있는 코드와 구조 개선을 위해 계속해서 고민하며 좋은 서비스를 제공하기 위해 노력해나갈 예정입니다.&lt;/p&gt;

&lt;p&gt;긴 글을 읽어주셔서 감사합니다.&lt;/p&gt;

&lt;h4 id=&quot;참고한-사이트&quot;&gt;참고한 사이트&lt;/h4&gt;
&lt;hr /&gt;
&lt;ul&gt;
  &lt;li&gt;리덕스(Redux) : &lt;a href=&quot;https://ko.redux.js.org/&quot;&gt;https://ko.redux.js.org/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;리덕스 사가(Redux Saga):&lt;a href=&quot;https://github.com/redux-saga/redux-saga&quot;&gt;https://github.com/redux-saga/redux-saga&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;SWR : &lt;a href=&quot;https://swr.vercel.app/ko&quot;&gt;https://swr.vercel.app/ko&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;리액트 쿼리 (React Query): &lt;a href=&quot;https://react-query-v3.tanstack.com/&quot;&gt;https://react-query-v3.tanstack.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>[&quot;웬디&quot;]</name></author><category term="react" /><category term="redux" /><category term="redux-saga" /><summary type="html">리덕스(Redux)</summary></entry></feed>