<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>defaultK</title>
    <link>https://kwonik2304.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 16:08:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kwoss2341</managingEditor>
    <item>
      <title>[Node.js] RabbitMQ,Socket.io 실시간 웹소켓 서버 구축기 (2)</title>
      <link>https://kwonik2304.tistory.com/61</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;본격적인 구조 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;975&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tu0Y9/btsbSezuvW4/w0rXwTcYWmtesR2tKlJAk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tu0Y9/btsbSezuvW4/w0rXwTcYWmtesR2tKlJAk0/img.png&quot; data-alt=&quot;설계1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tu0Y9/btsbSezuvW4/w0rXwTcYWmtesR2tKlJAk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftu0Y9%2FbtsbSezuvW4%2Fw0rXwTcYWmtesR2tKlJAk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1502&quot; height=&quot;975&quot; data-filename=&quot;실시간 웹소켓 서버 구성.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;975&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설계1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 구상한 RabbitMQ를 이용한 실시간 서버의 모습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 웹소켓 서버가 아닌 REST-API서버였고, 클라이언트 프로그램은 외부 RabbitMQ의 큐를 소비하는 시스템이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 역할은 그저 요청이 오면 메시지를 큐에 발행하는 일이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측도 받은 메시지에 따라 실시간으로 동작을 수행하는 간단한 시스템이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 몇 가지 문제점이 있었습니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;1. 연결 여부 및 접속 시간 확인&lt;/u&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POS시스템을 운영하면서 가장 중요한 부분은 연결 여부와 접속 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클라이언트가 연결되어 있는지, 연결한 시간과 연결이 끊긴 시간은 POS시스템 운영에 가장 중요하다 판단했습니다. RabbitMQ에서도 연결 여부를 알 수 있겠지만 서비스에 활용하기 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;2. RabbitMQ Conne&lt;/u&gt;&lt;u&gt;ction 관리&lt;/u&gt;&lt;/b&gt;&lt;u&gt;&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 AWS의 &lt;span style=&quot;background-color: #ffffff; color: #16191f; text-align: start;&quot;&gt;RabbitMQ용 &lt;/span&gt;AmazonMQ를 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 유형은 m5.large를 선택했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C1C9n/btscjjO3x9j/6GZ0dhIdKrGtC2uJ1Pk150/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C1C9n/btscjjO3x9j/6GZ0dhIdKrGtC2uJ1Pk150/img.png&quot; data-alt=&quot;AmazonMQ 브로커 기본값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C1C9n/btscjjO3x9j/6GZ0dhIdKrGtC2uJ1Pk150/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC1C9n%2FbtscjjO3x9j%2F6GZ0dhIdKrGtC2uJ1Pk150%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;858&quot; height=&quot;369&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AmazonMQ 브로커 기본값&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;m5.large 인스턴스 유형의 max-connections 값은 4000개 이고, 이는 수동으로 변경이 가능했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 많은 커넥션은 RabbitMQ서버에도 비효율적이라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;3. React-natve RabbitMQ 라이브러리&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 프로그램은 안드로이드 OS기반의 프로그램들로 react-native로 개발 중이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;react-native 앱에서 rabbitMQ를 사용하기 위해 react-native-rabbitmq 라이브러리를 이용하려 했지만 최신 버전 안드로이드에서 제대로 빌드가 되지 않았습니다. 해당 라이브러리는 최근 커밋이 2019년이 마지막이었고 react-native-rabbitmq를 제외한 쓸만한 라이브러리를 찾지 못했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 다양한 이유 때문에 RabbitMQ를 이용한 실시간 웹소켓 서버 설계를 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 실시간 웹소켓 서버 설계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (4).png&quot; data-origin-width=&quot;2015&quot; data-origin-height=&quot;975&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UXYCz/btsctMJUj75/CvvnREccjOxUvmyJ1lYre1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UXYCz/btsctMJUj75/CvvnREccjOxUvmyJ1lYre1/img.png&quot; data-alt=&quot;실시간 웹소켓 서버&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UXYCz/btsctMJUj75/CvvnREccjOxUvmyJ1lYre1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUXYCz%2FbtsctMJUj75%2FCvvnREccjOxUvmyJ1lYre1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2015&quot; height=&quot;975&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (4).png&quot; data-origin-width=&quot;2015&quot; data-origin-height=&quot;975&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실시간 웹소켓 서버&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실시간 웹소켓 서버와 RabbitMQ&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 웹소켓 서버와 RabbitMQ 사이 Connection입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 서버 2대에서 각각 하나의 Connection을 맺습니다. (실제로는 pm2를 이용하여 각각의 서버에서 4개의 인스턴스를 생성해서 한 서버에 프로세스 4개 , 즉 커넥션 4개씩 2개의 서버 총 8개의 커넥션만을 이용했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 서버와 클라이언트 프로그램사이 WebSocket Connection이 맺어지면 RabbitMQ Connection에서 Channel을 하나 생성해서 클라이언트 프로그램 1개 당 메시지 큐 하나를 생성하고 클라이언트 프로그램에 맞게 큐에 바인딩한 후 채널을 통해 해당 큐를 소비합니다. 큐의 Message Consume 이벤트는 socket을 통해 클라이언트 프로그램에 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 웹소켓이 끊어지면 해당 채널을 통해 큐 소비를 취소하고 채널을 닫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. RabbitMQ Connection 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 실시간 웹소켓서버 포트 listening&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 클라이언트 프로그램 웹소켓 connect&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Channel 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Channel =&amp;gt; 큐 생성, 바인딩, 큐 소비&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 웹소켓 연결 끊어질 시 큐 소비 취소, Channel close&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 동작이지만 구현하면서 많은 난관이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자체적으로 10000개의 웹소켓 커넥션을 맺어가며 부하테스트를 진행하면서 겪었던&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어려웠던 점에 대해 소개하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;부하테스트 난관 1. DB Connection&lt;/u&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 부분이었습니다. Knex.js를 이용해서 DB를 이용 중, 웹소켓 접속 시 헤더의 토큰을 보고 유효한 토큰에 한해 DB 조회를 해서 간단한 기기 정보를 불러오는 로직에서 DB에서 'Too many connection' 에러가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 DB의 Connection이었습니다. 하지만 knex.js의 connection pool을 이용하고 있어 의아했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 connection pool 옵션이었습니다. connection pool의 max connection을 너무 높게 잡아 서버 2대의 8개의 프로세스에서 DB Connection보다 많은 connection이 문제였습니다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;오히려 connection 수를 줄여 db connection 문제를 해결했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;부하테스트 난관 2.&amp;nbsp; RABBITMQ_MEMORY_ALARM&lt;/u&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llbJX/btscoF5hQu2/aqFBr7kcgMSsmmdg0Y9xSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llbJX/btscoF5hQu2/aqFBr7kcgMSsmmdg0Y9xSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llbJX/btscoF5hQu2/aqFBr7kcgMSsmmdg0Y9xSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FllbJX%2FbtscoF5hQu2%2FaqFBr7kcgMSsmmdg0Y9xSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;197&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10000개의 클라이언트 웹소켓이 한 번에 서버에 붙으면 RabbitMQ 서버에 10000개의 채널이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 하나의 채널에 하나의 웹소켓만 해당 큐를 소비하는 건 비효율적입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ 서버에 한 번에 10000개의 채널을 동시에 요청하니 서버에 메모리 알람이 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부득이하게 실시간 웹소켓 서버를 수정하고 다시 배포하면 한번에 많은 웹소켓이 연결될 것이고, RabbitMQ서버에 많은 채널을 요청할 것입니다. 이는 RabbitMQ 서버의 큰 부하를 일으켜 문제 해결이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio.png&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uh1eK/btscTjlCSDo/hLyRZGzcj38WJUHIZ69BcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uh1eK/btscTjlCSDo/hLyRZGzcj38WJUHIZ69BcK/img.png&quot; data-alt=&quot;channel 동시 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uh1eK/btscTjlCSDo/hLyRZGzcj38WJUHIZ69BcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuh1eK%2FbtscTjlCSDo%2FhLyRZGzcj38WJUHIZ69BcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1069&quot; height=&quot;261&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio.png&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;channel 동시 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 해결을 위해 async queue를 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 큐에 RabbitMQ 요청을 push 하고 큐에서 순서대로 채널을 생성해 공급하는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async queue의 옵션을 통해 최대 10개까지 동시 처리 가능하게 설정을 해서 10000개의 웹소켓이 한 번에 접속해도 큐에 최대 동시 10개씩 순차적으로 채널을 공급해 서버의 부하를 줄였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio (2).png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tOiEj/btscPR5uFBT/zmxLCOzLesHw2QjagMKYf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tOiEj/btscPR5uFBT/zmxLCOzLesHw2QjagMKYf1/img.png&quot; data-alt=&quot;Queue를 이용한 Channel 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tOiEj/btscPR5uFBT/zmxLCOzLesHw2QjagMKYf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtOiEj%2FbtscPR5uFBT%2FzmxLCOzLesHw2QjagMKYf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1332&quot; height=&quot;235&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio (2).png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Queue를 이용한 Channel 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 가장 좋은 방법은 가급적이면 채널을 적절히 생성하는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크리티컬 하지 않는 경우 채널을 재사용해서 서버의 부하를 줄여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;부하테스트 난관 3.&amp;nbsp; &quot;No channels left to allocate&quot;&lt;/u&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;amqplib라이브러리를 이용해서 부하테스트 중 해당 에러가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당에러는 할당할 채널이 남아 있지 않았을 때 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 RabbitMQ Management 웹 콘솔에서 설정을 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caCQ7n/btscNREHfIZ/1ySMkf40O6ey4XqmuirbRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caCQ7n/btscNREHfIZ/1ySMkf40O6ey4XqmuirbRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caCQ7n/btscNREHfIZ/1ySMkf40O6ey4XqmuirbRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaCQ7n%2FbtscNREHfIZ%2F1ySMkf40O6ey4XqmuirbRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;279&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max-channels의 설정값을 확인해서 부족하면 늘려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232629; text-align: left;&quot;&gt;두 번째는 amqplib/lib/connection.js를&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #232629; text-align: left;&quot;&gt; 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232629; text-align: left;&quot;&gt;실제로 에러발생하는 코드에서 channelMax의 값은 2047입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232629; text-align: left;&quot;&gt;connection을 통한 channel의 최대 개수는 2047개였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #232629; text-align: left;&quot;&gt;각 서버의 실시간 웹소켓 서버의 프로세스 수를 늘려 최대한 분산하는 방식으로 각 커넥션당 2047개를 넘지 않게 조절했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;u&gt;부하테스트 난관 4.&amp;nbsp; Exchages&lt;/u&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10000개의 클라이언트 프로그램의 실시간 웹소켓 서버에 연결되고, 채널과 큐가 생성되면 큐에 각각의 클라이언트의 성격에 맞게 routing key에 맞게 바인딩해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯이 Topic Exchage를 이용해서 라우팅키를 바인딩합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 하나의 Exchange에 10000개의 클라이언트 프로그램의 라우팅키를 여러 개 바인딩하는 것은 비효율적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 매장단위로 Exchange를 생성해서 해당매장의 속한 클라이언트 프로그램은 해당 매장 Exchage로만 바인딩을 해서 각각의 Exchage에 대한 부하를 최대한 분산했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 실시간 웹소켓 서버와 RabbitMQ&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio (3).png&quot; data-origin-width=&quot;2373&quot; data-origin-height=&quot;975&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNHOrK/btscRKD7m2a/gzDmNdCGJYkz6A8pk7KOPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNHOrK/btscRKD7m2a/gzDmNdCGJYkz6A8pk7KOPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNHOrK/btscRKD7m2a/gzDmNdCGJYkz6A8pk7KOPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNHOrK%2FbtscRKD7m2a%2FgzDmNdCGJYkz6A8pk7KOPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;933&quot; height=&quot;383&quot; data-filename=&quot;실시간 웹소켓 서버 구성.drawio (3).png&quot; data-origin-width=&quot;2373&quot; data-origin-height=&quot;975&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ와 Socket.io를 이용한 실시간 웹소켓 서버를 구축한 최종 모습입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버에서 필요한 메시지를 Exchange를 통해 발행하면 클라이언트 프로그램까지 도달되는 과정을 그린 그림입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;봐주셔서 감사합니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Nodejs</category>
      <category>amqplib</category>
      <category>Node</category>
      <category>nodejs</category>
      <category>rabbitmq</category>
      <category>socket.io</category>
      <category>실시간 웹소켓 서버</category>
      <category>웹소켓</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/61</guid>
      <comments>https://kwonik2304.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 26 Apr 2023 23:05:15 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] RabbitMQ,Socket.io 실시간 웹소켓 서버 구축기 (1)</title>
      <link>https://kwonik2304.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가기 앞서...&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 새로운 프로젝트를 시작하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외식업의 포스시스템 구축 프로젝트로 안드로이드 클라이언트 프로그램(POS, KIOSK, 태블릿 오더) 간의 실시간 웹소켓 통신 서버 구축이 과제였습니다. 해당 글은 Node.js로 실시간 웹소켓 통신 서버 구축하는 경험기를 소개하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(정답은 아니고 미흡한 부분이 많습니다...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개발배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외식업의 포스시스템 프로젝트를 시작하면서 가장 걱정한 부분은 네트워크 상태에따른 주문 누락에 대한 부분이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇몇 포스시스템은 불안정한 네트워크상황과 여러 변수들을 고려해 같은 로컬 네트워크를 클라이언트 프로그램에서 사용한다고 하지만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 시스템은 초기 설정도 힘들고 전문가가 아니면 네트워크 구축하기도 힘듭니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 인터넷만 되면 연결이 가능한 중앙 웹소켓 통신 서버 구축을 하기로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개발하기 앞서 기존 서비스 중인 웹소켓 서버&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 드라이브 스루 앱 서비스를 제공하는 회사에 재직 중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이브 스루 앱은 다른 배달앱과 같이 고객이 직접 앱을 통해 주문할 메뉴를 고르고 결제한뒤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차를 타고 가면 가맹점에서 주문한 메뉴를 전달해 주는 서비스를 제공해 주는 위치기반 주문 앱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램.png&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0DAgj/btsa7s6lnfp/dhTXDmJkblmNjNxYt0rvKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0DAgj/btsa7s6lnfp/dhTXDmJkblmNjNxYt0rvKk/img.png&quot; data-alt=&quot;실시간 웹소켓 서버를 이용한 주문 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0DAgj/btsa7s6lnfp/dhTXDmJkblmNjNxYt0rvKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0DAgj%2Fbtsa7s6lnfp%2FdhTXDmJkblmNjNxYt0rvKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;481&quot; data-filename=&quot;제목 없는 다이어그램.png&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실시간 웹소켓 서버를 이용한 주문 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진과 같이 드라이브 스루 앱은 실시간 웹소켓 서버와 커넥션을 맺고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가맹점에서 마찬가지로 가맹점 전용 주문 접수 앱을 실시간 웹소켓 서버와 커넥션을 맺고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객이 앱을 통해 결제와 주문을 하면 가맹점에서 실시간으로 주문 알림 발생해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램 (1).png&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWphE6/btsa8hXTiEs/J0napxtHdQA4NrNnXsBKkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWphE6/btsa8hXTiEs/J0napxtHdQA4NrNnXsBKkk/img.png&quot; data-alt=&quot;실시간 웹소켓 서버를 이용한 주문 취소 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWphE6/btsa8hXTiEs/J0napxtHdQA4NrNnXsBKkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWphE6%2Fbtsa8hXTiEs%2FJ0napxtHdQA4NrNnXsBKkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;481&quot; data-filename=&quot;제목 없는 다이어그램 (1).png&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실시간 웹소켓 서버를 이용한 주문 취소 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 가맹점 앱(STORE5)에서 해당 주문을 취소하면&amp;nbsp; APP5에서 취소 알림을 받을 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그림의 시스템은 웹소켓 서버가 하나일 경우에만 유효합니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 서비스는 안정성을 위해 분산 서버 시스템으로 구축합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 서버 시스템에서는 그림처럼 실시간 웹소켓 서버를 구축할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 사진은 분산 서버 시스템에서의 실시간 웹소켓 서버입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램 (2).png&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt0tLB/btsa9xGbpzG/eJ2IcQ9V5YpN5puJ1SRZbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt0tLB/btsa9xGbpzG/eJ2IcQ9V5YpN5puJ1SRZbK/img.png&quot; data-alt=&quot;분산 서버 시스템에서의 실시간 웹소켓 서버&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt0tLB/btsa9xGbpzG/eJ2IcQ9V5YpN5puJ1SRZbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt0tLB%2Fbtsa9xGbpzG%2FeJ2IcQ9V5YpN5puJ1SRZbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;612&quot; data-filename=&quot;제목 없는 다이어그램 (2).png&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분산 서버 시스템에서의 실시간 웹소켓 서버&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 서비스는 로드 밸런싱을 이용해서 분산 서버 시스템을 구축해 서버의 부하를 줄입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 서버 시스템에서는 각기 다른 서버에서 커넥션을 맺어서 알림을 보낼 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 캐시 메모리를 이용하는 서버로 다양한 자료구조와 서비스를 제공해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서도 Redis Queue를 이용해 일종의 실시간 메시지 큐를 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 다이어그램 (3).png&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;771&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EHBXr/btsa8gLscWd/YLtEdKVscZYk5i5Q0kDBBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EHBXr/btsa8gLscWd/YLtEdKVscZYk5i5Q0kDBBK/img.png&quot; data-alt=&quot;기존의 Redis큐를 이용한 실시간 웹소켓 서버&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EHBXr/btsa8gLscWd/YLtEdKVscZYk5i5Q0kDBBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEHBXr%2Fbtsa8gLscWd%2FYLtEdKVscZYk5i5Q0kDBBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;901&quot; height=&quot;771&quot; data-filename=&quot;제목 없는 다이어그램 (3).png&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;771&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기존의 Redis큐를 이용한 실시간 웹소켓 서버&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 웹소켓 서버의 역할이 커졌습니다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 웹소켓 서버는 가맹점앱(THROO STORE)과 웹소켓 커넥션을 맺으면 해당 가맹점의 Redis 큐를 바라봐야 합니다. 실시간 웹소켓 서버 내에서 구현한 일종의 Worker는 가맹점의&amp;nbsp; Redis 큐를 바라보고 있다가 메시지가 큐에 들어오면 즉시 소비 해서 해당 소켓으로 메시지를 전송하는 역할을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객앱(THROO APP)에서 메시지를 생성할 때 역시 일종의 Producer가 실시간 웹소켓 서버 내에서 Redis 큐에 메시지를 넣어주는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이로써 기존의 실시간 웹소켓 서버는 Redis를 통해 분산 서버 시스템에서의 웹소켓 커넥션 유지와 메시지 유실방지 기능을 수행하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기존의 Redis 가 아닌 RabbitMQ를 사용하기로 결정한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Redis 실시간 웹소켓 서버 역시 큰 문제없이 안정적인 서비스를 제공 중이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 새로운 프로젝트에서 기존의 웹소켓 서버를 이용하기에는 부족함이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 RabbitMQ를 사용하기로 결정한 이유를 소개해드리겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;1, 복잡한 라우팅 기능 지원&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis와 달리 RabbitMQ는 복잡한 라우팅 기능을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외식업 포스시스템 구축 프로젝트에는 다양한 클라이언트 프로그램 ( POS, KIOSK, 태블릿오더, 기존 자사 드라이브스루앱 등..)&amp;nbsp; 이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (1).png&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mX0jd/btsbmgrsbkb/orvY8KEmcWt0knJ6VXhst1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mX0jd/btsbmgrsbkb/orvY8KEmcWt0knJ6VXhst1/img.png&quot; data-alt=&quot;복잡한 라우팅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mX0jd/btsbmgrsbkb/orvY8KEmcWt0knJ6VXhst1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmX0jd%2Fbtsbmgrsbkb%2ForvY8KEmcWt0knJ6VXhst1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;479&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (1).png&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;복잡한 라우팅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job의 성격에 따라 메시지는 POS 그룹에, 특정 태블릿에게만, 가맹점 전체 클라이언트 프로그램에게 전달될 수 있어야 합니다. 메시지의 성격에 따라 라우팅방식이 복잡해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 pub/sub 기능을 제공하지만 해당 기능은 클라이언트가 sub 하지 않으면 메시지 유실이 발생합니다. 메시지 유실은 시스템 전체의 신뢰성을 하락시켜 치명적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실시간 웹소켓 서버에서 자체적으로 메시지 성격에 따라 Redis 큐에 메시지를 넣는 방법도 있지만 이는 기능 구현하기 번거롭고 유지보수 측면에서도 굉장히 힘들어질 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서는 메시지가 생성되면 Exchange에서 바인딩 정보에 따라 해당 메시지를 큐에 넣어 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange의 다양한 타입 중에서 저희에게 필요한 Topic을 이용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1681910311819&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;RabbitMQ tutorial - Topics 
 &amp;mdash; RabbitMQ&quot; data-og-description=&quot;Topics (using the amqp.node client) Prerequisites This tutorial assumes RabbitMQ is installed and running on localhost on the standard port (5672). In case you use a different host, port or credentials, connections settings would require adjusting. Where t&quot; data-og-host=&quot;www.rabbitmq.com&quot; data-og-source-url=&quot;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&quot; data-og-url=&quot;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.rabbitmq.com/tutorials/tutorial-five-javascript.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;RabbitMQ tutorial - Topics &amp;mdash; RabbitMQ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Topics (using the amqp.node client) Prerequisites This tutorial assumes RabbitMQ is installed and running on localhost on the standard port (5672). In case you use a different host, port or credentials, connections settings would require adjusting. Where t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.rabbitmq.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyUOA0/btsblDtFK0H/sXYdMw8CTefTyzylqFHLk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyUOA0/btsblDtFK0H/sXYdMw8CTefTyzylqFHLk1/img.png&quot; data-alt=&quot;TOPIC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyUOA0/btsblDtFK0H/sXYdMw8CTefTyzylqFHLk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyUOA0%2FbtsblDtFK0H%2FsXYdMw8CTefTyzylqFHLk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;424&quot; height=&quot;171&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TOPIC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Topic을 이용해서 메시지를 라우팅 하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*, # 으로 큐는 자기가 원하는 메시지만 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (3).png&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d76kQ3/btsbk6v4VHk/UB6s1aXSgrJKPtLkhndb0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d76kQ3/btsbk6v4VHk/UB6s1aXSgrJKPtLkhndb0k/img.png&quot; data-alt=&quot;TOPIC을 이용한 메시지 라우팅 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d76kQ3/btsbk6v4VHk/UB6s1aXSgrJKPtLkhndb0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd76kQ3%2Fbtsbk6v4VHk%2FUB6s1aXSgrJKPtLkhndb0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1189&quot; height=&quot;961&quot; data-filename=&quot;실시간 웹소켓 서버 구성 (3).png&quot; data-origin-width=&quot;1189&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TOPIC을 이용한 메시지 라우팅 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TOPIC의 라우팅 키에 따라 메시지를 라우팅하는 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 큐에 라우팅 규칙을 적용해서 1번 가맹점 전체 (STORE_1) , 1번 가맹점의 POS 전체(STORE_1.POS), 1번 가맹점의 POS 1번만 (STORE_1.POS_1) 메시지를 보낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;2. 실시간 모니터링&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;래빗엠큐에서는 기본적으로 15672 포트에서 웹브라우저를 통해 &lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;management UI를 제공해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;큐 생성뿐만 아니라 커넥션, 커넥션을 통해 생성된 채널, Exchange의 바인딩 정보, Queue 옵션, Queue 메시지 수 등등 한눈에 파악할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;기존의 Redis 큐 역시 모니터링할 수 있는 툴이 존재했지만 RabbitMQ의 &lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;management UI 수준의 모니터링은 할 수 없었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;뿐만 아니라 서버 측에서 활용할 수 있는 api 도 제공해주고 있어 유지 보수 측면에서 유리하다고 판단했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;3. 다양한 옵션&lt;/u&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;메시지 큐를 이용하면 다양한 문제를 만날 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 메시지 유실 방지를 위해 생성한 메시지 큐에 메시지를 언제까지 유지시킬 것인가?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 한번 생성된 큐에 Consumer 가 없어 계속해서 메시지가 쌓이면 이는 RabbitMQ 성능에 큰 문제를 야기시킬 수 있습니다. 또한 장기간 사용하지 않아 메시지가 잔뜩 쌓인 큐를 클라이언트 측이 한 번에 다 소비하려면 클라이언트 프로그램 역시 많은 무리가 갑니다. 이를 메시지 옵션을 통해 타임아웃을 걸어 자동 삭제가 가능하고, 큐 옵션을 통해 일정 시간 동안 Consumer가 없으면 큐를 자동 삭제해 줄 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;4. 검증된 시스템, 방대한 자료, 쉬운 구현&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;RabbitMQ는 오랜 기간 사용되어 왔습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;많은 사용자가 사용한 만큼 검증된 오픈소스이고&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;자료 역시 방대하고 구현도 쉬웠습니다. 이해하기 쉽게 정리된 &lt;/span&gt;공식문서 역시 마음에 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 '실시간 웹소켓 서버 구축기 (1)'&amp;nbsp; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'실시간 웹소켓 서버 구축기 (2)' 에서는 구현하면서 겪었던 어려움에 대해서 적어보겠습니다...&lt;/p&gt;</description>
      <category>Nodejs</category>
      <category>amqplib</category>
      <category>kwoss2341</category>
      <category>node.js</category>
      <category>rabbitmq</category>
      <category>socket.io</category>
      <category>실시간</category>
      <category>웹소켓 서버</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/60</guid>
      <comments>https://kwonik2304.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 19 Apr 2023 23:30:46 +0900</pubDate>
    </item>
    <item>
      <title>[React] Hook 훅</title>
      <link>https://kwonik2304.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.reactjs.org/docs/hooks-intro.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.reactjs.org/docs/hooks-intro.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1658035547043&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Hook의 개요 &amp;ndash; React&quot; data-og-description=&quot;A JavaScript library for building user interfaces&quot; data-og-host=&quot;ko.reactjs.org&quot; data-og-source-url=&quot;https://ko.reactjs.org/docs/hooks-intro.html&quot; data-og-url=&quot;https://ko.reactjs.org/docs/hooks-intro.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ta5Bo/hyO7Aje3lz/2HaU71tqrvaINgoN6duFHK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://ko.reactjs.org/docs/hooks-intro.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.reactjs.org/docs/hooks-intro.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ta5Bo/hyO7Aje3lz/2HaU71tqrvaINgoN6duFHK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Hook의 개요 &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A JavaScript library for building user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.reactjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 13px solid green; padding: 0 15px 5px 10px; font-weight: bold; border-bottom: green 1px solid;&quot; data-ke-size=&quot;size26&quot;&gt;Hook&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ Funtion component에서 리액트의 state와 생명주기 기능을 지원&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 13px solid green; padding: 0 15px 5px 10px; font-weight: bold; border-bottom: green 1px solid;&quot; data-ke-size=&quot;size26&quot;&gt;State Hook&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ state를 사용하기 위한 훅&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1658035805118&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

function Example() {

  const [count, setCount] = useState(0);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;You clicked {count} times&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Click me
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 13px solid green; padding: 0 15px 5px 10px; font-weight: bold; border-bottom: green 1px solid;&quot; data-ke-size=&quot;size26&quot;&gt;Effect Hook&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ 생명주기 함수 기능을 제공&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1658068770467&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() =&amp;gt; {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/59</guid>
      <comments>https://kwonik2304.tistory.com/59#entry59comment</comments>
      <pubDate>Sun, 17 Jul 2022 23:40:57 +0900</pubDate>
    </item>
    <item>
      <title>[React] State 와 LifeCycle</title>
      <link>https://kwonik2304.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.reactjs.org/docs/state-and-lifecycle.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.reactjs.org/docs/state-and-lifecycle.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1658034902084&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;State and Lifecycle &amp;ndash; React&quot; data-og-description=&quot;A JavaScript library for building user interfaces&quot; data-og-host=&quot;ko.reactjs.org&quot; data-og-source-url=&quot;https://ko.reactjs.org/docs/state-and-lifecycle.html&quot; data-og-url=&quot;https://ko.reactjs.org/docs/state-and-lifecycle.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzdt09/hyO5PoOEQ9/ndBCkgleXqkrU4b67fjcj1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://ko.reactjs.org/docs/state-and-lifecycle.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.reactjs.org/docs/state-and-lifecycle.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzdt09/hyO5PoOEQ9/ndBCkgleXqkrU4b67fjcj1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;State and Lifecycle &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A JavaScript library for building user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.reactjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 13px solid green; padding: 0 15px 5px 10px; font-weight: bold; border-bottom: green 1px solid;&quot; data-ke-size=&quot;size26&quot;&gt;State&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ 리액트 컴포넌트의 변경 가능 데이터&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ state가 변경될 경우 재랜더링.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ 변경시 재랜더링이 일어나므로 성능을 고려하여 state 값 설정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ state값을 변경시 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;-Class component : setState()&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;-Function component : useState() 훅에서 정의한 set함수&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;border-left: 13px solid green; padding: 0 15px 5px 10px; font-weight: bold; border-bottom: green 1px solid;&quot; data-ke-size=&quot;size26&quot;&gt;LifeCycle&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ componentDidMount()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;- 컴포넌트가 생성될 때&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ componentDidUpdate()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;- component props가 변경될 때&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;- state값 변경시(setState())&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- forceUpdate() 강제 업데이트 함수 호출 시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;✔ componentWillUnmount()&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;- 컴포넌트가 언마운트될때 호출&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1658035122688&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () =&amp;gt; this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;Hello, world!&amp;lt;/h1&amp;gt;
        &amp;lt;h2&amp;gt;It is {this.state.date.toLocaleTimeString()}.&amp;lt;/h2&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

ReactDOM.render(
  &amp;lt;Clock /&amp;gt;,
  document.getElementById('root')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/58</guid>
      <comments>https://kwonik2304.tistory.com/58#entry58comment</comments>
      <pubDate>Sun, 17 Jul 2022 14:19:45 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Selenium 네이버 블로그 서로이웃추가 자동화 프로그램</title>
      <link>https://kwonik2304.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 블로그 서로이웃추가 자동화 프로그램을 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 개발환경을 세팅하는 방법은 여기서...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kwonik2304.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kwonik2304.tistory.com/52&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1655604063046&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JAVA] Selenium - 개발 환경 세팅&quot; data-og-description=&quot;1. pom.xml 추가 (최신버전: 4.1.4 추가. 해당 링크에서 원하는 버전 조회하여 추가) org.seleniumhq.selenium selenium-java 4.1.4 2. 크롬 드라이버 다운로드 https://chromedriver.chromium.org/downloads Chro..&quot; data-og-host=&quot;kwonik2304.tistory.com&quot; data-og-source-url=&quot;https://kwonik2304.tistory.com/52&quot; data-og-url=&quot;https://kwonik2304.tistory.com/52&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bx8qEe/hyOOsS1Dgg/hQkLC4AFyitKZ2F9ukfRi0/img.png?width=800&amp;amp;height=627&amp;amp;face=15_167_102_254,https://scrap.kakaocdn.net/dn/ES4w0/hyONKHIGF4/hRqoRRwXkqmO43G3QmRqzk/img.png?width=800&amp;amp;height=627&amp;amp;face=15_167_102_254,https://scrap.kakaocdn.net/dn/kCvbB/hyOOs6yeJH/1cyX3mIbNSR2SWf58Aju50/img.png?width=1255&amp;amp;height=985&amp;amp;face=0_0_1255_985&quot;&gt;&lt;a href=&quot;https://kwonik2304.tistory.com/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kwonik2304.tistory.com/52&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bx8qEe/hyOOsS1Dgg/hQkLC4AFyitKZ2F9ukfRi0/img.png?width=800&amp;amp;height=627&amp;amp;face=15_167_102_254,https://scrap.kakaocdn.net/dn/ES4w0/hyONKHIGF4/hRqoRRwXkqmO43G3QmRqzk/img.png?width=800&amp;amp;height=627&amp;amp;face=15_167_102_254,https://scrap.kakaocdn.net/dn/kCvbB/hyOOs6yeJH/1cyX3mIbNSR2SWf58Aju50/img.png?width=1255&amp;amp;height=985&amp;amp;face=0_0_1255_985');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[JAVA] Selenium - 개발 환경 세팅&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. pom.xml 추가 (최신버전: 4.1.4 추가. 해당 링크에서 원하는 버전 조회하여 추가) org.seleniumhq.selenium selenium-java 4.1.4 2. 크롬 드라이버 다운로드 https://chromedriver.chromium.org/downloads Chro..&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kwonik2304.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Selenium 을 이용하기에 앞서 웹 분석이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석은 크게 프로그램 동작과&amp;nbsp; 동작에 필요한 각각의 요소에 대한 분석이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 사용자의 event ( click, input, wait, get 등)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 요소분석은 event가 이루어질 웹 요소를 분석한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 네이버 로딩 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 클릭 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정시간동안 사용자가 로그인하기를 대기한 후 블로그 페이지 까지 이동하는 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1655604553744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String url = &quot;https://www.naver.com/&quot;;

//1. 네이버 페이지 로딩
driver.get(url);
Thread.sleep(delay);

//2. 로그인 클릭
element = driver.findElement(By.cssSelector(&quot;#account &amp;gt; a&quot;));
element.click();
Thread.sleep(delay*4);

//3. 블로그 페이지 이동
driver.get(&quot;https://section.blog.naver.com/BlogHome.naver?directoryNo=0&amp;amp;currentPage=1&amp;amp;groupId=0&quot;);
Thread.sleep(delay);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 click 이벤트를 수행하는 요소를 찾기 위해서 css 선택자를 이용한 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소를 찾기위한 방법은 xpath, css selector, id, class name 등 많은 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 요소를 개발자 모드를 이용하여 분석한 후 가장 적합한 방법을 이용하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be5HPK/btrE9RpgFkE/l6dIwO0HlDZmx90hpdR5I1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be5HPK/btrE9RpgFkE/l6dIwO0HlDZmx90hpdR5I1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be5HPK/btrE9RpgFkE/l6dIwO0HlDZmx90hpdR5I1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe5HPK%2FbtrE9RpgFkE%2Fl6dIwO0HlDZmx90hpdR5I1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1021&quot; height=&quot;390&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 페이지로 이동했다면 이웃추가 버튼을 클릭하여 서로이웃추가 하는 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7zUd9/btrE4cuRacx/eLCdxuHXKnGrZDwLk5zDsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7zUd9/btrE4cuRacx/eLCdxuHXKnGrZDwLk5zDsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7zUd9/btrE4cuRacx/eLCdxuHXKnGrZDwLk5zDsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7zUd9%2FbtrE4cuRacx%2FeLCdxuHXKnGrZDwLk5zDsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;532&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqRSS6/btrE6xrNKQu/59oj5eGiJIUHUhkALgWod1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqRSS6/btrE6xrNKQu/59oj5eGiJIUHUhkALgWod1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqRSS6/btrE6xrNKQu/59oj5eGiJIUHUhkALgWod1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqRSS6%2FbtrE6xrNKQu%2F59oj5eGiJIUHUhkALgWod1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;61&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 반복 수행에 대한 문제가 생긴다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp; 해당 페이지 내의 모든글에 대해 이웃추가 버튼을 클릭 반복 수행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp; 1번 반복 완료 후 하단 1~10페이지 클릭 반복수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp; 2번 반복 완료 후 다음버튼 클릭&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복을 위해서는 해당 요소를 찾아 그 요소만큼 반복적으로 수행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서는 ChromeDriver 의 findElements 메소드를 이용하여 공통된 특징을 가진 요소드를 찾은 후 반복한다.&lt;/p&gt;
&lt;pre id=&quot;code_1655606292560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
List&amp;lt;WebElement&amp;gt; pageButtons = driver.findElements(By.cssSelector(&quot;#content &amp;gt; section &amp;gt; div:nth-child(3) &amp;gt; div &amp;gt; span &amp;gt; a&quot;)) ;
for(int i=1; i&amp;lt;10; i++)
{
	getList(driver);
	element=pageButtons.get(i);
	element.click();
	Thread.sleep(delay);
}
	
element = driver.findElement(By.cssSelector(&quot;#content &amp;gt; section &amp;gt; div:nth-child(3) &amp;gt; div &amp;gt; a&quot;));
element.click();
Thread.sleep(delay);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 반복적으로 서로이웃신청이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 이웃추가 버튼을 클릭하게 되면....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bomW2S/btrFcg2XZym/jfrgHPYztzEnj1k4mrsuL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bomW2S/btrFcg2XZym/jfrgHPYztzEnj1k4mrsuL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bomW2S/btrFcg2XZym/jfrgHPYztzEnj1k4mrsuL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbomW2S%2FbtrFcg2XZym%2FjfrgHPYztzEnj1k4mrsuL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;447&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 팝업창이 나온다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 팝업창에서 driver 동작이 이루어 져야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서 driver의 switchTo 메소드를 이용하여 해당 팝업으로 전환 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작들이 이루어 져야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1655606958017&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static void clickAddNeighbor(ChromeDriver driver) throws InterruptedException
	{
		String MainWindow = driver.getWindowHandle();
		Set&amp;lt;String&amp;gt; s1= driver.getWindowHandles();
		Iterator&amp;lt;String&amp;gt; i1= s1.iterator();
		
		while(i1.hasNext()){
			String ChildWindow = i1.next();
			
			if(!MainWindow.equalsIgnoreCase(ChildWindow)){
				driver.switchTo().window(ChildWindow);
				//팝업창에서의 동작 구현...
			}
		}
		
		
		driver.switchTo().window(MainWindow);

		
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드와 같이 MainWindow 를 저장 후 하위 window로 이동하여 동작을 수행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작이 끝나면 다시 MainWindow 로 돌아와서 이웃추가를 반복 수행하여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업을 수행하는 전체 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1655607142599&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SeleniumMain {

	public static String WEB_DRIVER_ID = &quot;webdriver.chrome.driver&quot;;
	public static String WEB_DRIVER_PATH = &quot;chromedriver.exe&quot;;
	public static int delay = 3;
	public static WebElement element;
	

	public static void main(String[] args) throws InterruptedException, IOException {
		
		
		// TODO Auto-generated method stub
		System.setProperty(WEB_DRIVER_ID, WEB_DRIVER_PATH);
		
		
		ChromeOptions options = new ChromeOptions();
		options.addArguments(&quot;--disable-popup-blocking&quot;);
		options.addArguments(&quot;--start-maximized&quot;);
		options.addArguments(&quot;--window-size=1920,1080&quot;);
		
		ChromeDriver driver = new ChromeDriver(options);
		
		try {
			start(driver);
			
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		finally{
			driver.quit();
		}
		
		
		
		
		
		
		
		

	}
	
	
	
	static void start(ChromeDriver driver) throws InterruptedException
	{
		String url = &quot;https://www.naver.com/&quot;;
		
		
		driver.get(url);
		Thread.sleep(delay);
	
		element = driver.findElement(By.cssSelector(&quot;#account &amp;gt; a&quot;));
		element.click();
		Thread.sleep(delay*4);

		
		driver.get(&quot;https://section.blog.naver.com/BlogHome.naver?directoryNo=0&amp;amp;currentPage=1&amp;amp;groupId=0&quot;);
		Thread.sleep(delay);
		
		
		
		
		while(true)
		{
			List&amp;lt;WebElement&amp;gt; pageButtons = driver.findElements(By.cssSelector(&quot;#content &amp;gt; section &amp;gt; div:nth-child(3) &amp;gt; div &amp;gt; span &amp;gt; a&quot;)) ;
			for(int i=1; i&amp;lt;10; i++)
			{
				getList(driver);
				element=pageButtons.get(i);
				element.click();
				Thread.sleep(delay);
			}
			
			element = driver.findElement(By.cssSelector(&quot;#content &amp;gt; section &amp;gt; div:nth-child(3) &amp;gt; div &amp;gt; a&quot;));
			element.click();
			Thread.sleep(delay);
		}
		
		
		

		
		
	}
	
	static void getList(ChromeDriver driver) throws InterruptedException
	{
		WebElement element;
		
		//목록찾기
		List&amp;lt;WebElement&amp;gt; elements = driver.findElements(By.cssSelector(&quot;a.button_buddy.button_buddy_add&quot;));
		Thread.sleep(delay);
		
		for(WebElement el : elements)
		{
			el.click();
			Thread.sleep(delay);
			
			clickAddNeighbor(driver);
			Thread.sleep(delay);
		}

	}

	
	
	static void clickAddNeighbor(ChromeDriver driver) throws InterruptedException
	{
		String MainWindow = driver.getWindowHandle();
		Set&amp;lt;String&amp;gt; s1= driver.getWindowHandles();
		Iterator&amp;lt;String&amp;gt; i1= s1.iterator();
		
		while(i1.hasNext()){
			String ChildWindow = i1.next();
			
			if(!MainWindow.equalsIgnoreCase(ChildWindow)){
				driver.switchTo().window(ChildWindow);
				String name=null;
				try {
					element = driver.findElement(By.cssSelector(&quot;#content &amp;gt; div &amp;gt; form &amp;gt; fieldset &amp;gt; div.popup_text &amp;gt; div.buddy_state &amp;gt; p.text_buddy_add &amp;gt; strong&quot;));
					name=element.getAttribute(&quot;innerHTML&quot;);
					
					element = driver.findElement(By.cssSelector(&quot;#content &amp;gt; div &amp;gt; form &amp;gt; fieldset &amp;gt; div.popup_text &amp;gt; div.buddy_state &amp;gt; p &amp;gt; span.wrap_radio.radio_bothbuddy &amp;gt; label&quot;));
					element.click();
					Thread.sleep(delay);
					
					element = driver.findElement(By.cssSelector(&quot;#content &amp;gt; div &amp;gt; form &amp;gt; fieldset &amp;gt; div.area_button &amp;gt; a.button_next._buddyAddNext&quot;));
					element.click();
					Thread.sleep(delay);
					
					element = driver.findElement(By.cssSelector(&quot;#message&quot;));
					if(name!=null)
					{
						element.sendKeys(name+&quot;님! 글 재밌게봤습니다^^ 서로이웃 추가 해요 ㅎㅎ&quot;);
					}
					else
					{
						element.sendKeys(&quot;글 재밌게봤습니다^^ 서로이웃 추가 해요 ㅎㅎ&quot;);
					}
					Thread.sleep(delay);
					
					
					element = driver.findElement(By.cssSelector(&quot;body &amp;gt; form &amp;gt; div &amp;gt; div &amp;gt; fieldset &amp;gt; div.area_button &amp;gt; a.button_next._addBothBuddy&quot;));
					element.click();
					Thread.sleep(delay);
					
					System.out.println(&quot;[&quot;+name+&quot;] 서로이웃추가 신청완료&quot;);
					driver.close();
				}
				catch(UnhandledAlertException e)
				{
					System.out.println(&quot;[&quot;+name+&quot;] 이미 서로이웃추가신청된 계정.&quot;);
				}
				catch(Exception e)
				{
					System.out.println(&quot;[&quot;+name+&quot;] 서로이웃추가 비활성 계정&quot;);	
					driver.close();
				}
				finally
				{
					
				}

				
				
			}
		}
		
		
		driver.switchTo().window(MainWindow);

		
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Selenium</category>
      <category>Selenium 네이버 블로그</category>
      <category>네이버 블로그</category>
      <category>네이버 블로그 이웃추가 자동</category>
      <category>서로이웃추가 매크로</category>
      <category>서로이웃추가 자동</category>
      <category>서이추 매크로</category>
      <category>서이추 자동</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/53</guid>
      <comments>https://kwonik2304.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 19 Jun 2022 11:53:41 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Selenium - 개발 환경 세팅</title>
      <link>https://kwonik2304.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. pom.xml 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(최신버전: 4.1.4 추가.&amp;nbsp; 해당 링크에서 원하는 버전 조회하여 추가)&lt;/p&gt;
&lt;pre id=&quot;code_1651383833099&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	&amp;lt;!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java --&amp;gt;
	&amp;lt;dependency&amp;gt;
	    &amp;lt;groupId&amp;gt;org.seleniumhq.selenium&amp;lt;/groupId&amp;gt;
	    &amp;lt;artifactId&amp;gt;selenium-java&amp;lt;/artifactId&amp;gt;
	    &amp;lt;version&amp;gt;4.1.4&amp;lt;/version&amp;gt;
	&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 크롬 드라이버 다운로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chromedriver.chromium.org/downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://chromedriver.chromium.org/downloads&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1651383887805&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ChromeDriver - WebDriver for Chrome - Downloads&quot; data-og-description=&quot;Current Releases If you are using Chrome version 101, please download ChromeDriver 101.0.4951.41 If you are using Chrome version 100, please download ChromeDriver 100.0.4896.60 If you are using Chrome version 99, please download ChromeDriver 99.0.4844.51 F&quot; data-og-host=&quot;chromedriver.chromium.org&quot; data-og-source-url=&quot;https://chromedriver.chromium.org/downloads&quot; data-og-url=&quot;https://chromedriver.chromium.org/downloads&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bouJue/hyOffmf8U6/D2jyTGl6UOYEk2AtUA9zd0/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000&quot;&gt;&lt;a href=&quot;https://chromedriver.chromium.org/downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chromedriver.chromium.org/downloads&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bouJue/hyOffmf8U6/D2jyTGl6UOYEk2AtUA9zd0/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ChromeDriver - WebDriver for Chrome - Downloads&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Current Releases If you are using Chrome version 101, please download ChromeDriver 101.0.4951.41 If you are using Chrome version 100, please download ChromeDriver 100.0.4896.60 If you are using Chrome version 99, please download ChromeDriver 99.0.4844.51 F&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chromedriver.chromium.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬 브라우저 버전과 맞는 드라이버를 다운로드.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(크롬 브라우저:&lt;span style=&quot;color: #5f6368;&quot;&gt;101.0.4951.41,&amp;nbsp; &amp;nbsp;크롬 드라이버:&lt;a href=&quot;https://chromedriver.storage.googleapis.com/index.html?path=101.0.4951.41/&quot;&gt;101.0.4951.41&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. TEST&lt;/p&gt;
&lt;pre id=&quot;code_1651384064216&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SeleniumMain {
	
	public static String WEB_DRIVER_ID = &quot;webdriver.chrome.driver&quot;;
	public static String WEB_DRIVER_PATH = &quot;chromedriver.exe&quot;;
	
	public static String url = &quot;https://www.hometax.go.kr/&quot;;
	
	
	public static void main(String[] args) throws InterruptedException {
		
		
		System.setProperty(WEB_DRIVER_ID, WEB_DRIVER_PATH);
		ChromeDriver driver = new ChromeDriver();
		driver.get(url);

		Thread.sleep(10000);
		driver.quit();

	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #5f6368;&quot;&gt;다운로드 받은 드라이버는 WEB_DRIVER_PATH에서 경로를 설정.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;985&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oKTrQ/btrAYuqLKRm/nMVNEsWskmg122mdH8yOL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oKTrQ/btrAYuqLKRm/nMVNEsWskmg122mdH8yOL1/img.png&quot; data-alt=&quot;국세청 홈텍스 접속 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oKTrQ/btrAYuqLKRm/nMVNEsWskmg122mdH8yOL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoKTrQ%2FbtrAYuqLKRm%2FnMVNEsWskmg122mdH8yOL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1255&quot; height=&quot;985&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;985&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;국세청 홈텍스 접속 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Selenium</category>
      <category>Java</category>
      <category>Selenium</category>
      <category>국세청</category>
      <category>셀레니움</category>
      <category>스크래퍼</category>
      <category>크롤러</category>
      <category>크롤링</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/52</guid>
      <comments>https://kwonik2304.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 1 May 2022 14:49:36 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스][BFS/DFS] 단어 변환 c++</title>
      <link>https://kwonik2304.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/43163#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://programmers.co.kr/learn/courses/30/lessons/43163#&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1631021572127&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코딩테스트 연습 - 단어 변환&quot; data-og-description=&quot;두 개의 단어 begin, target과 단어의 집합 words가 있습니다. 아래와 같은 규칙을 이용하여 begin에서 target으로 변환하는 가장 짧은 변환 과정을 찾으려고 합니다. 1. 한 번에 한 개의 알파벳만 바꿀 수&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/43163#&quot; data-og-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/43163&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eYumu/hyLxy9OG2E/zkztuiQkEAXd41p6TksZB0/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/WNb6H/hyLxGfHKpH/hWVBosjvhIrJjDhYlOZoYk/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/43163#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/43163#&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eYumu/hyLxy9OG2E/zkztuiQkEAXd41p6TksZB0/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/WNb6H/hyLxGfHKpH/hWVBosjvhIrJjDhYlOZoYk/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 연습 - 단어 변환&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;두 개의 단어 begin, target과 단어의 집합 words가 있습니다. 아래와 같은 규칙을 이용하여 begin에서 target으로 변환하는 가장 짧은 변환 과정을 찾으려고 합니다. 1. 한 번에 한 개의 알파벳만 바꿀 수&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;기본적인 아이디어.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 단어에 한 문자만 틀리면 엣지를 연결하는 그래프를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 해당 그래프를 BFS(DFS역시 가능)를 이용하여 begin 과 target 사이 최단 거리(엣지 개수)를 구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631021805898&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
using namespace std;

bool onecheck(string a, string b)
{
    int x=0;
    for(int i=0; i&amp;lt;a.size(); i++)
    {
        if(a[i]!=b[i])
        {
            x++;
        }
        
        if(x&amp;gt;1) break;
    }
    
    if(x&amp;gt;1) return 0;
    else if(x==1) return 1;
}

int solution(string begin, string target, vector&amp;lt;string&amp;gt; words) {
    int answer = 0;
    int n=begin.size();
    
    int zz=0;

    
    
    words.insert(words.begin(), target);
    words.insert(words.begin(), begin);
    int size=words.size();
    
    queue &amp;lt;int&amp;gt; q;
    vector &amp;lt;int&amp;gt; w[size];
    int visit[size];
    int d[size];
    
    
    
    
    for(int i=0; i&amp;lt;size; i++)
    {
        for(int j=0; j&amp;lt;size; j++)
        {
            if(i==j) continue;
            
            if(onecheck(words[i] , words[j]))
            {
                w[i].push_back(j);
            }
        }
    }
    
    for(int i=0; i&amp;lt;size; i++)
    { 
        visit[i]=0;
        d[i]=0;
    }

        int start;
        int i=0;

        start=i;
        while(1) 
        {
            
            visit[start]=1;
            for(int j=0; j&amp;lt;w[start].size(); j++)
            {
                if(visit[w[start][j]]==1) continue;
                
                
                                
                q.push(w[start][j]);
                d[w[start][j]]=d[start]+1;
                
                if(w[start][j]==1) return d[start]+1;
            }
            
            if(q.empty())
            {
                answer = 0;
                break;    
            }
            
            
            
            start=q.front();
            q.pop();
        }
        
        
        
    
    
    
    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;139&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjluL9/btreoUlqKC3/umbMFpXvK4wdFdj5LikE0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjluL9/btreoUlqKC3/umbMFpXvK4wdFdj5LikE0k/img.png&quot; data-alt=&quot;프로그래머스 단어 변환 실행시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjluL9/btreoUlqKC3/umbMFpXvK4wdFdj5LikE0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjluL9%2FbtreoUlqKC3%2FumbMFpXvK4wdFdj5LikE0k%2Fimg.png&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;139&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로그래머스 단어 변환 실행시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>알고리즘 ( C++ )/프로그래머스</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/50</guid>
      <comments>https://kwonik2304.tistory.com/50#entry50comment</comments>
      <pubDate>Tue, 7 Sep 2021 22:37:38 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 카카오 순위 검색 c++</title>
      <link>https://kwonik2304.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://programmers.co.kr/learn/courses/30/lessons/72412&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1630843977416&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;코딩테스트 연습 - 순위 검색&quot; data-og-description=&quot;[&amp;quot;java backend junior pizza 150&amp;quot;,&amp;quot;python frontend senior chicken 210&amp;quot;,&amp;quot;python frontend senior chicken 150&amp;quot;,&amp;quot;cpp backend senior pizza 260&amp;quot;,&amp;quot;java backend junior chicken 80&amp;quot;,&amp;quot;python backend senior chicken 50&amp;quot;] [&amp;quot;java and backend and junior and pizza 100&amp;quot;,&amp;quot;pyt&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; data-og-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chJCoJ/hyLuQYwbNl/6PBsIxnk7Vk13ZIVimKzrK/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/lDfp1/hyLwnHarEL/e6kKXABK52otKveRnJ14M0/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://programmers.co.kr/learn/courses/30/lessons/72412&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chJCoJ/hyLuQYwbNl/6PBsIxnk7Vk13ZIVimKzrK/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626,https://scrap.kakaocdn.net/dn/lDfp1/hyLwnHarEL/e6kKXABK52otKveRnJ14M0/img.jpg?width=626&amp;amp;height=626&amp;amp;face=0_0_626_626');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 연습 - 순위 검색&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[&quot;java backend junior pizza 150&quot;,&quot;python frontend senior chicken 210&quot;,&quot;python frontend senior chicken 150&quot;,&quot;cpp backend senior pizza 260&quot;,&quot;java backend junior chicken 80&quot;,&quot;python backend senior chicken 50&quot;] [&quot;java and backend and junior and pizza 100&quot;,&quot;pyt&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;기본적인 아이디어.&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 나올수 있는 모든경우의수 4(java,cpp,python,-) *3(back,front,-) *3(chicken, pizza, -) 벡터 배열을 만들어 점수를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;push_back하여 해당 조건을 검색한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 점수를 push_back한 배열을 정렬한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 정렬한 배열을 이분탐색으로&amp;nbsp; 조건의 점수 이상인 사람을 탐색하여 시간을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(하지만 정렬하지않고 이분탐색을 하지 않아도 500ms시간안으로 통과 된다.. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1630853266284&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;
int as;
vector&amp;lt;int&amp;gt; solution(vector&amp;lt;string&amp;gt; info, vector&amp;lt;string&amp;gt; query) {
    vector&amp;lt;int&amp;gt; answer;
    
    vector&amp;lt;int&amp;gt; countn[5][4][4][4];
    
    
    
    for(int i=0; i&amp;lt;info.size(); i++)
    {
        int j;
        int a=0;
        int b,c,d;
        int an,bn,cn,dn;
        for(j=0; j&amp;lt;info[i].size(); j++)
        {
            if(info[i][j]==' ')
            {
                break;
            }
        }
        
        
        if(info[i][a]=='c')
        {
            an=1;
        }
        else if(info [i][a]=='j')
        {
            an=2;
        }
        else
        {
            an=3;
        }
        
        
        b=j+1;
        for(j=b; j&amp;lt;info[i].size(); j++)
        {
            if(info[i][j]==' ')
            {
                break;
            }
        }
        
        if(info[i][b]=='b')
        {
            bn=1;
        }
        else
        {
            bn=2;
        }
        
        
        c=j+1;
        for(j=c; j&amp;lt;info[i].size(); j++)
        {
            if(info[i][j]==' ')
            {
                break;
            }
        }
        
        if(info[i][c]=='j')
        {
            cn=1;
        }
        else
        {
            cn=2;
        }
        
        
        d=j+1;
        for(j=d; j&amp;lt;info[i].size(); j++)
        {
            if(info[i][j]==' ')
            {
                break;
            }
        }
        
        if(info[i][d]=='c')
        {
            dn=1;
        }
        else
        {
            dn=2;
        }
        
        int score = stoi(info[i].substr(j+1));
        
        
        //무지성구간
        //몸이 나쁘면 머리가 고생한다 &amp;lt;안개특유&amp;gt;
        countn[an][bn][cn][dn].push_back(score);
        countn[0][bn][cn][dn].push_back(score);
        countn[an][0][cn][dn].push_back(score);
        countn[an][bn][0][dn].push_back(score);
        countn[an][bn][cn][0].push_back(score);
        countn[0][0][cn][dn].push_back(score);
        countn[0][bn][0][dn].push_back(score);
        countn[0][bn][cn][0].push_back(score);
        countn[an][0][0][dn].push_back(score);
        countn[an][0][cn][0].push_back(score);
        countn[an][bn][0][0].push_back(score);
        countn[0][0][0][dn].push_back(score);
        countn[0][bn][0][0].push_back(score);
        countn[0][0][cn][0].push_back(score);
        countn[an][0][0][0].push_back(score);
        countn[0][0][0][0].push_back(score);
        
      
    }
    
    
 
    for(int a=0; a&amp;lt;4; a++)
    {
        for(int b=0; b&amp;lt;3; b++)
        {
            for(int c=0; c&amp;lt;3; c++)
            {
                for(int d=0; d&amp;lt;3; d++)
                {
                    sort(countn[a][b][c][d].begin(),countn[a][b][c][d].end());
                }
            }
        }
    }
    
    
    
    
    for(int i=0; i&amp;lt;query.size(); i++)
    {
        int j;
        int a=0;
        int b,c,d;
        int an,bn,cn,dn;
        for(j=0; j&amp;lt;query[i].size(); j++)
        {
            if(query[i][j]==' ')
            {
                break;
            }
        }
        
        
        if(query[i][a]=='c')
        {
            an=1;
        }
        else if(query[i][a]=='j')
        {
            an=2;
        }
        else if(query[i][a]=='p')
        {
            an=3;
        }
        else
        {
            an=0;
        }
        
        
        b=j+5;
        for(j=b; j&amp;lt;query[i].size(); j++)
        {
            if(query[i][j]==' ')
            {
                break;
            }
        }
        
        if(query[i][b]=='b')
        {
            bn=1;
        }
        else if(query[i][b]=='f')
        {
            bn=2;
        }
        else
        {
            bn=0;
        }
        
        c=j+5;
        for(j=c; j&amp;lt;query[i].size(); j++)
        {
            if(query[i][j]==' ')
            {
                break;
            }
        }
        
        if(query[i][c]=='j')
        {
            cn=1;
        }
        else if(query[i][c]=='s')
        {
            cn=2;
        }
        else
        {
            cn=0;
        }
        
        
        d=j+5;
        for(j=d; j&amp;lt;query[i].size(); j++)
        {
            if(query[i][j]==' ')
            {
                break;
            }
        }
        
        if(query[i][d]=='c')
        {
            dn=1;
        }
        else if(query[i][d]=='p')
        {
            dn=2;
        }
        else
        {
            dn=0;
        }
        
        int score = stoi(query[i].substr(j+1));
        
        int start = 0;
        int end = countn[an][bn][cn][dn].size()-1;
        int mid = 0;
        while (start &amp;lt;= end || start&amp;lt;0 )
        {
            mid = (start+end) / 2;
            if (countn[an][bn][cn][dn][mid] == score)
                break;
            else {
                if (countn[an][bn][cn][dn][mid] &amp;lt; score)
                    start = mid+1;
                else if (countn[an][bn][cn][dn][mid] &amp;gt; score)
                    end = mid-1;
            }
        }
        
        
        
        if(countn[an][bn][cn][dn].size()==0)
        {
            answer.push_back(0);
            continue;
        }
        
        
        if(countn[an][bn][cn][dn][mid]&amp;gt;=score)
        {
           
            
            for(mid; mid&amp;gt;=0; mid--)
            {
                if(countn[an][bn][cn][dn][mid]&amp;lt;score)
                {                    
                    break;
                }
            }
            
            
            if(mid&amp;lt;0)
            {
                 answer.push_back(countn[an][bn][cn][dn].size());
            }
            else
            {
                answer.push_back(countn[an][bn][cn][dn].size()-(mid+1));
            }
            
        }
        else
        {
            int sw=0;
            for(mid; mid&amp;lt;countn[an][bn][cn][dn].size(); mid++)
            {
                if(countn[an][bn][cn][dn][mid]&amp;gt;=score)
                {    
                    sw=1;
                    break;
                }
            }
            
            if(sw==1)
            {
                answer.push_back(countn[an][bn][cn][dn].size()-mid);
            }
            else
            {
                answer.push_back(0);
            }
            
        }
        
        
    }
    

    
    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;537&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drrkHO/btrec4gMut4/sxiPsMKTda1qA5eqYkGMB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drrkHO/btrec4gMut4/sxiPsMKTda1qA5eqYkGMB0/img.png&quot; data-alt=&quot;카카오 순위 검색 실행시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drrkHO/btrec4gMut4/sxiPsMKTda1qA5eqYkGMB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrrkHO%2Fbtrec4gMut4%2FsxiPsMKTda1qA5eqYkGMB0%2Fimg.png&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;537&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카카오 순위 검색 실행시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 ( C++ )/프로그래머스</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/49</guid>
      <comments>https://kwonik2304.tistory.com/49#entry49comment</comments>
      <pubDate>Sun, 5 Sep 2021 23:49:21 +0900</pubDate>
    </item>
    <item>
      <title>[디자인 패턴 C++] 추상 팩토리 패턴</title>
      <link>https://kwonik2304.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;의도&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;276&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYr9I/btrbAIhlCIW/QAPVcmTKj2WwYybhY4Yjgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYr9I/btrbAIhlCIW/QAPVcmTKj2WwYybhY4Yjgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYr9I/btrbAIhlCIW/QAPVcmTKj2WwYybhY4Yjgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYr9I%2FbtrbAIhlCIW%2FQAPVcmTKj2WwYybhY4Yjgk%2Fimg.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;276&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;참여자&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-AbstractFactory&amp;nbsp; :&amp;nbsp; 객체(제품)을 생성하는 연산을 추상화 시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-ConcreteFactory :&amp;nbsp; virtual로 추상화 되어있는 CreateProductA(),CreateProductB() 메소드를 구체화하여 객체를 생성하는 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-AbstractProduct :&amp;nbsp; 제품을 추상화 시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( Client 코드에서 &lt;span style=&quot;color: #000000;&quot;&gt;const&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;AbstractProductA&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;product_a&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;factory&lt;/span&gt;.&lt;span style=&quot;color: #000000;&quot;&gt;CreateProductA&lt;/span&gt;();&amp;nbsp; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ConcreteProduct : AbstractProduct가 정의하는 인터페이스 구현. 구체으로 팩토리가 생성할 객체.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Client : AbstractFactory, AbstractProduct 클래스에 선언된 인터페이스를 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( ConcreteFactory 클래스를 생성해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractFactory의 인터페이스 CreatProductA() 메소드 사용,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractProduct를 이용 일관성 유지 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/u&gt;&lt;u&gt;&lt;b&gt;&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(추상 팩토리 패턴 이해가 어려워 많은 코드를 찾아보던 중 가장 정리가 잘된 코드를 찾았다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 &lt;a href=&quot;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1628520078467&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Design Patterns: Abstract Factory in C++&quot; data-og-description=&quot;/ Design Patterns / Abstract Factory / C++ Abstract Factory in C++ Abstract Factory is a creational design pattern, which solves the problem of creating entire product families without specifying their concrete classes. Abstract Factory defines an interfac&quot; data-og-host=&quot;refactoring.guru&quot; data-og-source-url=&quot;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&quot; data-og-url=&quot;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tAQrg/hyLa2dNjdA/Xv1WKb1P9vuAszpI0tKRmK/img.png?width=476&amp;amp;height=249&amp;amp;face=0_0_476_249&quot;&gt;&lt;a href=&quot;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://refactoring.guru/design-patterns/abstract-factory/cpp/example&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tAQrg/hyLa2dNjdA/Xv1WKb1P9vuAszpI0tKRmK/img.png?width=476&amp;amp;height=249&amp;amp;face=0_0_476_249');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Design Patterns: Abstract Factory in C++&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;/ Design Patterns / Abstract Factory / C++ Abstract Factory in C++ Abstract Factory is a creational design pattern, which solves the problem of creating entire product families without specifying their concrete classes. Abstract Factory defines an interfac&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;refactoring.guru&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1628519970159&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Each distinct product of a product family should have a base interface. All
 * variants of the product must implement this interface.
 */
class AbstractProductA {
 public:
  virtual ~AbstractProductA(){};
  virtual std::string UsefulFunctionA() const = 0;
};

/**
 * Concrete Products are created by corresponding Concrete Factories.
 */
class ConcreteProductA1 : public AbstractProductA {
 public:
  std::string UsefulFunctionA() const override {
    return &quot;The result of the product A1.&quot;;
  }
};

class ConcreteProductA2 : public AbstractProductA {
  std::string UsefulFunctionA() const override {
    return &quot;The result of the product A2.&quot;;
  }
};

/**
 * Here's the the base interface of another product. All products can interact
 * with each other, but proper interaction is possible only between products of
 * the same concrete variant.
 */
class AbstractProductB {
  /**
   * Product B is able to do its own thing...
   */
 public:
  virtual ~AbstractProductB(){};
  virtual std::string UsefulFunctionB() const = 0;
  /**
   * ...but it also can collaborate with the ProductA.
   *
   * The Abstract Factory makes sure that all products it creates are of the
   * same variant and thus, compatible.
   */
  virtual std::string AnotherUsefulFunctionB(const AbstractProductA &amp;amp;collaborator) const = 0;
};

/**
 * Concrete Products are created by corresponding Concrete Factories.
 */
class ConcreteProductB1 : public AbstractProductB {
 public:
  std::string UsefulFunctionB() const override {
    return &quot;The result of the product B1.&quot;;
  }
  /**
   * The variant, Product B1, is only able to work correctly with the variant,
   * Product A1. Nevertheless, it accepts any instance of AbstractProductA as an
   * argument.
   */
  std::string AnotherUsefulFunctionB(const AbstractProductA &amp;amp;collaborator) const override {
    const std::string result = collaborator.UsefulFunctionA();
    return &quot;The result of the B1 collaborating with ( &quot; + result + &quot; )&quot;;
  }
};

class ConcreteProductB2 : public AbstractProductB {
 public:
  std::string UsefulFunctionB() const override {
    return &quot;The result of the product B2.&quot;;
  }
  /**
   * The variant, Product B2, is only able to work correctly with the variant,
   * Product A2. Nevertheless, it accepts any instance of AbstractProductA as an
   * argument.
   */
  std::string AnotherUsefulFunctionB(const AbstractProductA &amp;amp;collaborator) const override {
    const std::string result = collaborator.UsefulFunctionA();
    return &quot;The result of the B2 collaborating with ( &quot; + result + &quot; )&quot;;
  }
};

/**
 * The Abstract Factory interface declares a set of methods that return
 * different abstract products. These products are called a family and are
 * related by a high-level theme or concept. Products of one family are usually
 * able to collaborate among themselves. A family of products may have several
 * variants, but the products of one variant are incompatible with products of
 * another.
 */
class AbstractFactory {
 public:
  virtual AbstractProductA *CreateProductA() const = 0;
  virtual AbstractProductB *CreateProductB() const = 0;
};

/**
 * Concrete Factories produce a family of products that belong to a single
 * variant. The factory guarantees that resulting products are compatible. Note
 * that signatures of the Concrete Factory's methods return an abstract product,
 * while inside the method a concrete product is instantiated.
 */
class ConcreteFactory1 : public AbstractFactory {
 public:
  AbstractProductA *CreateProductA() const override {
    return new ConcreteProductA1();
  }
  AbstractProductB *CreateProductB() const override {
    return new ConcreteProductB1();
  }
};

/**
 * Each Concrete Factory has a corresponding product variant.
 */
class ConcreteFactory2 : public AbstractFactory {
 public:
  AbstractProductA *CreateProductA() const override {
    return new ConcreteProductA2();
  }
  AbstractProductB *CreateProductB() const override {
    return new ConcreteProductB2();
  }
};

/**
 * The client code works with factories and products only through abstract
 * types: AbstractFactory and AbstractProduct. This lets you pass any factory or
 * product subclass to the client code without breaking it.
 */

void ClientCode(const AbstractFactory &amp;amp;factory) {
  const AbstractProductA *product_a = factory.CreateProductA();
  const AbstractProductB *product_b = factory.CreateProductB();
  std::cout &amp;lt;&amp;lt; product_b-&amp;gt;UsefulFunctionB() &amp;lt;&amp;lt; &quot;\n&quot;;
  std::cout &amp;lt;&amp;lt; product_b-&amp;gt;AnotherUsefulFunctionB(*product_a) &amp;lt;&amp;lt; &quot;\n&quot;;
  delete product_a;
  delete product_b;
}

int main() {
  std::cout &amp;lt;&amp;lt; &quot;Client: Testing client code with the first factory type:\n&quot;;
  ConcreteFactory1 *f1 = new ConcreteFactory1();
  ClientCode(*f1);
  delete f1;
  std::cout &amp;lt;&amp;lt; std::endl;
  std::cout &amp;lt;&amp;lt; &quot;Client: Testing the same client code with the second factory type:\n&quot;;
  ConcreteFactory2 *f2 = new ConcreteFactory2();
  ClientCode(*f2);
  delete f2;
  return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 말그대로 팩토리는 공장이다. 객체를 생성하는 공장이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AbstractFactory가 제시한 인터페이스를 ConcreteFactory가 구체화 하여 어떤 A제품을 createProductA()할건지 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AbstractProduct가 정의한 인터페이스는 제품의 특성에따라 ConcreteProduct에서 구체화 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Client는 ClientCode() 함수를 통해 실제 팩토리에서 어떤 제품이 생성되는지 관여하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>디자인패턴/생성 패턴</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/48</guid>
      <comments>https://kwonik2304.tistory.com/48#entry48comment</comments>
      <pubDate>Mon, 9 Aug 2021 23:57:02 +0900</pubDate>
    </item>
    <item>
      <title>[Qt 프로그래밍] [크로스컴파일]  Linux Sqlite db 조회</title>
      <link>https://kwonik2304.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;pro 파일에 sql 추가해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #55ff55;&quot;&gt;QT&lt;/span&gt;&lt;span style=&quot;color: #c0c0c0;&quot;&gt; &lt;/span&gt;+=&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sql&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QtSql inlcude 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#include &amp;lt;QtSql&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1622880311626&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;

    QSqlDatabase db;
    db = QSqlDatabase::addDatabase(&quot;QSQLITE&quot;);
    db.setDatabaseName(&quot;/usr/local/app/my.db&quot;); //db 경로

    QString strQry = &quot;PRAGMA synchronous = OFF;&quot;; // 디스크 동기화 사용여부

    if(!db.open()) {
            qDebug() &amp;lt;&amp;lt; QString(&quot;&amp;lt;FAILED&amp;gt; Connect to DB: %1&quot;) .arg(&quot;/usr/local/app/my.db&quot;);
            abort();
        }


    QSqlQuery qry(db);
    qry.clear();
    qry.prepare(strQry);
    if(!qry.exec()) {
        qDebug() &amp;lt;&amp;lt; QString(&quot;&amp;lt;SQL FAILED&amp;gt; %1&quot;) .arg(strQry);
        qDebug() &amp;lt;&amp;lt; QString(&quot;&amp;lt;SQL FAILED&amp;gt; %1&quot;) .arg(qry.lastError().text());
    }
    else {
        qDebug() &amp;lt;&amp;lt; QString(&quot;&amp;lt;SQL SUCCEED&amp;gt; %1&quot;) .arg(strQry);
    }

	
    
    
   
    QString szQry;
    QString szVal, szType, szTgtIP, szVarNm, szValOld, szDec;
    
    szQry = QString(&quot;select * from tb_evm_mst where set_target_ip='%1';&quot;) .arg(&quot;&quot;); //조회쿼리


    qry.prepare(szQry);


    if(!qry.exec()) {
            qDebug() &amp;lt;&amp;lt; QString(&quot;&amp;lt;SQL FAILED&amp;gt; %1&quot;) .arg(szQry);
            qWarning() &amp;lt;&amp;lt; QString(&quot;&amp;lt;SQL FAILED&amp;gt; %1&quot;) .arg(qry.lastError().text());
            return false;
        }

    
    
    
    while(qry.next()) {
            szVal = qry.record().value(&quot;set_val&quot;).toString();
            szValOld = qry.record().value(&quot;set_val_old&quot;).toString();
            szVarNm = qry.record().value(&quot;set_var_nm&quot;).toString();

            szType = qry.record().value(&quot;set_type&quot;).toString();
            szDec = qry.record().value(&quot;set_dec&quot;).toString();
            szTgtIP = qry.record().value(&quot;set_target_ip&quot;).toString();

          
            qDebug()&amp;lt;&amp;lt;szVal&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;szValOld&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;szVarNm&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;szType&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;szDec&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;szTgtIP;
        }

   
   


&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Qt 프로그래밍/Basic Qt</category>
      <author>kwoss2341</author>
      <guid isPermaLink="true">https://kwonik2304.tistory.com/47</guid>
      <comments>https://kwonik2304.tistory.com/47#entry47comment</comments>
      <pubDate>Sat, 5 Jun 2021 17:05:30 +0900</pubDate>
    </item>
  </channel>
</rss>