<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>지수와 블로그</title>
    <link>https://jisu-log.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 14 Aug 2026 09:13:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>이제곱</managingEditor>
    <item>
      <title>[PostgreSQL] Sequence 값이 이상하다?! - Caching과 Gapless Assignment</title>
      <link>https://jisu-log.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL Docker image를 사용하여 개발하던 도중 이상현상을 발견했다.&lt;br /&gt;&lt;code&gt;Serial&lt;/code&gt; (auto increment integer) 필드 값이 내가 넣은 레코드 수보다 분명히 더 큰 값으로 설정되고 있었다.&lt;br /&gt;처음에는 잘못봤겠거니.. 했는데 반복적으로 일어나는 현상이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인 파악을 위해 sequence 를 살펴봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 비정상적인 종료 시 &lt;code&gt;log_cnt&lt;/code&gt;만큼 &lt;code&gt;last_value&lt;/code&gt;가 올라간다.&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npT6N/btsM9AntIYM/96RUS4QqRI4c0pDyJfOBCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npT6N/btsM9AntIYM/96RUS4QqRI4c0pDyJfOBCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npT6N/btsM9AntIYM/96RUS4QqRI4c0pDyJfOBCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpT6N%2FbtsM9AntIYM%2F96RUS4QqRI4c0pDyJfOBCk%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;1288&quot; height=&quot;610&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;db가 비정상 종료되었다가 재실행 되면 &lt;code&gt;last_value&lt;/code&gt;가 &lt;code&gt;log_cnt&lt;/code&gt;만큼 업데이트 된다는 것을 확인했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;new_keep=# insert into users(email, nickname, login_type) values('dsaaaaaaaaadsaa', 'nickname', 1);
INSERT 0 1
new_keep=# select * from users_user_id_seq;
 last_value | log_cnt | is_called
------------+---------+-----------
        170 |       0 | t
(1 row)

new_keep=# insert into users(email, nickname, login_type) values('dsaaaaaaaaaddsaa', 'nickname', 1);
INSERT 0 1
new_keep=# select * from users_user_id_seq;
 last_value | log_cnt | is_called
------------+---------+-----------
        171 |      32 | t
(1 row)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Insert를 실패해도 sequence 값이 올라간다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;insert를 실험하다 또 다른 사실을 발견했다. insert의 성공/실패 유무와 관계없이 sequence 값은 증가하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HCHtb/btsNbyOuMct/0Fvk5fArPsoB8EKiGebHo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HCHtb/btsNbyOuMct/0Fvk5fArPsoB8EKiGebHo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HCHtb/btsNbyOuMct/0Fvk5fArPsoB8EKiGebHo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHCHtb%2FbtsNbyOuMct%2F0Fvk5fArPsoB8EKiGebHo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1722&quot; height=&quot;650&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;unique 조건 위배&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tgjCO/btsNbgge5TQ/N5KqIOJ2ErZAMEXyIGxg5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tgjCO/btsNbgge5TQ/N5KqIOJ2ErZAMEXyIGxg5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tgjCO/btsNbgge5TQ/N5KqIOJ2ErZAMEXyIGxg5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtgjCO%2FbtsNbgge5TQ%2FN5KqIOJ2ErZAMEXyIGxg5K%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;1596&quot; height=&quot;338&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;not null 조건 위배&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unique, not null 제약조건에 위배되어 insert가 실패하는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 insert에 실패하면 sequence도 증가하지 않을것이라고 생각했는데, 예상과 달랐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니 이건 PostgreSQL의 정상 작동 방식이었다.&lt;br /&gt;그리고 발생 원인이 정말 정직하게 PostgreSQL 문서에 적혀있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sequence는 gapless 할당을 보장할 수 없다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/docs/16/sql-createsequence.html&quot;&gt;https://www.postgresql.org/docs/16/sql-createsequence.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;v16 사용중이므로 16 문서를 참고하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Because nextval and setval calls are never rolled back, sequence objects cannot be used if &amp;ldquo;gapless&amp;rdquo; assignment of sequence numbers is needed. It is possible to build gapless assignment by using exclusive locking of a table containing a counter; but this solution is much more expensive than sequence objects, especially if many transactions need sequence numbers concurrently.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;gapless&amp;rdquo; assignment를 보장해야 할 때는 sequence object를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;counter를 포함한 테이블을 lock 해서 gapless (순서 사이에 빈칸 없음)을 보장할 수 있지만 굉장히 고비용, 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;nextval()&lt;/code&gt; 호출은 절대 롤백되지 않아서 sequence 객체는 연속적인(gapless) 번호 할당을 보장할 수 없다는 것이다.&lt;br /&gt;대안인 테이블 lock이 있긴 하지만 성능이 많이 떨어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Session이 종료되면 Cache를 잃는다.&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unexpected results might be obtained if a *cache* setting greater than one is used for a sequence object that will be used concurrently by multiple sessions. Each session will allocate and cache successive sequence values during one access to the sequence object and increase the sequence object's last_value accordingly. Then, the next *cache*-1 uses of nextval within that session simply return the preallocated values without touching the sequence object. So, any numbers allocated but not used within a session will be lost when that session ends, resulting in &amp;ldquo;holes&amp;rdquo; in the sequence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 세션은 sequence에 접근할 때 여러 값을 미리 할당받아 캐싱한다.&lt;/li&gt;
&lt;li&gt;세션 종료 시 사용하지 않은 캐시들은 모두 소멸된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 종합해서 보면 sequence는 연속성을 보장할 수 없고, 세션 종료 시 캐싱된 sequnce 값이 소멸된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 어떤 경우에 Cache를 잃을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Graceful Stop에 실패했을 때가 cache를 잃을 때라고 생각하고 가설을 검증하기 위해 테스트를 해봤다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 정상 종료 케이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;종료 후 재실행해도 &lt;code&gt;last_value&lt;/code&gt;는 종료 전과 동일하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2KC9F/btsNbrhAt21/NfU0KnNLaltneH3jgpErek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2KC9F/btsNbrhAt21/NfU0KnNLaltneH3jgpErek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2KC9F/btsNbrhAt21/NfU0KnNLaltneH3jgpErek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2KC9F%2FbtsNbrhAt21%2FNfU0KnNLaltneH3jgpErek%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;2254&quot; height=&quot;594&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;code&gt;docker compose stop&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUBQY5/btsNa8CLuCf/bPC28BN4XyqcJDXI6DvIqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUBQY5/btsNa8CLuCf/bPC28BN4XyqcJDXI6DvIqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUBQY5/btsNa8CLuCf/bPC28BN4XyqcJDXI6DvIqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUBQY5%2FbtsNa8CLuCf%2FbPC28BN4XyqcJDXI6DvIqk%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;1306&quot; height=&quot;774&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ctrl+C (SIGINT - graceful stop)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2626&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6OB41/btsNaEoCVQX/qrYeFKd951KbydYK2UQmS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6OB41/btsNaEoCVQX/qrYeFKd951KbydYK2UQmS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6OB41/btsNaEoCVQX/qrYeFKd951KbydYK2UQmS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6OB41%2FbtsNaEoCVQX%2FqrYeFKd951KbydYK2UQmS1%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;2626&quot; height=&quot;540&quot; data-origin-width=&quot;2626&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 비정상 종료 케이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;last_value&lt;/code&gt;가 &lt;code&gt;log_cnt&lt;/code&gt;만큼 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;code&gt;docker compose kill&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mVlTf/btsNaBZK7UN/uEEDm9DLjWuUHdKN9GMCkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mVlTf/btsNaBZK7UN/uEEDm9DLjWuUHdKN9GMCkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mVlTf/btsNaBZK7UN/uEEDm9DLjWuUHdKN9GMCkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmVlTf%2FbtsNaBZK7UN%2FuEEDm9DLjWuUHdKN9GMCkk%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;1326&quot; height=&quot;630&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X7A7H/btsNabHfU0Z/KbuWmgd3irqcXYzk5WAGfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X7A7H/btsNabHfU0Z/KbuWmgd3irqcXYzk5WAGfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X7A7H/btsNabHfU0Z/KbuWmgd3irqcXYzk5WAGfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX7A7H%2FbtsNabHfU0Z%2FKbuWmgd3irqcXYzk5WAGfk%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;2596&quot; height=&quot;582&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;docker kill&lt;/code&gt; 커맨드는 SIGKILL을 보내므로 (Forces running containers to stop by sending a SIGKILL signal. - docker 공식 문서) gracefully stopping이 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ctrl+C를 누르고 종료되고 있는 와중에 ctrl+C를 또 누르는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맨 처음 이 현상을 발견하게 된 계기로 성격 급한 한국인의 면모를 보여준다.&lt;/li&gt;
&lt;li&gt;ctrl+C는 graceful stop하지만 한 번 더 누르면 graceful 하지 못한 stop이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uGHIg/btsM98qhPhE/EmhCule0f9l4rcmJr8qo8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uGHIg/btsM98qhPhE/EmhCule0f9l4rcmJr8qo8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uGHIg/btsM98qhPhE/EmhCule0f9l4rcmJr8qo8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuGHIg%2FbtsM98qhPhE%2FEmhCule0f9l4rcmJr8qo8K%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;2596&quot; height=&quot;582&quot; data-origin-width=&quot;2596&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;log_cnt&lt;/code&gt;는 뭘까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 &lt;code&gt;last_value&lt;/code&gt;가 &lt;code&gt;log_cnt&lt;/code&gt;만큼 증가했다는 것이다. 그럼 &lt;code&gt;log_cnt&lt;/code&gt;는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서 sequence 테이블을 조회하면 볼 수 있는 열이다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;new_keep=# select * from users_user_id_seq;
 last_value | log_cnt | is_called
------------+---------+-----------
        171 |      32 | t
(1 row)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log_cnt는 현재 세션에서 캐시한 시퀀스 값의 수를 나타낸다. 이 값들은 WAL(Write-Ahead Logging)에 로깅된 값들이다.&lt;br /&gt;PostgreSQL 소스 코드를 살펴보면 log_cnt의 정확한 역할을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;postgres/src/backend/commands/sequence.c&lt;/code&gt;의 &lt;code&gt;int64 nextval_internal(Oid relid, bool check_permissions)&lt;/code&gt; 함수 일부이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c#L623&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c#L623&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743869048591&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;postgres/src/backend/commands/sequence.c at 5db3bf7391d77ae86bc9b5f580141e022803b744 &amp;middot; postgres/postgres&quot; data-og-description=&quot;Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c#L623&quot; data-og-url=&quot;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/blH2GI/hyYBdzeXFP/Ogz6E5HWjE3XdGRKUNWgi0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bkY53E/hyYyOtLDJe/gssme4Z9LDsyvwoqa8kgJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c#L623&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/postgres/postgres/blob/5db3bf7391d77ae86bc9b5f580141e022803b744/src/backend/commands/sequence.c#L623&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/blH2GI/hyYBdzeXFP/Ogz6E5HWjE3XdGRKUNWgi0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bkY53E/hyYyOtLDJe/gssme4Z9LDsyvwoqa8kgJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;postgres/src/backend/commands/sequence.c at 5db3bf7391d77ae86bc9b5f580141e022803b744 &amp;middot; postgres/postgres&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Mirror of the official PostgreSQL GIT repository. Note that this is just a *mirror* - we don't work with pull requests on github. To contribute, please see https://wiki.postgresql.org/wiki/Subm...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;/* Decide whether we should emit a WAL log record. */
if (log &amp;lt; fetch || !seq-&amp;gt;is_called)
{
    /* forced log to satisfy local demand for values */
    fetch = log = fetch + SEQ_LOG_VALS;
    logit = true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 log는 시퀀스 튜플의 &lt;code&gt;log_cnt&lt;/code&gt; 값이다. (이전에 &lt;code&gt;log = seq-&amp;gt;log_cnt;&lt;/code&gt;로 초기화된다.)&lt;br /&gt;필요한 값보다 적을 경우 &lt;code&gt;SEQ_LOG_VALS&lt;/code&gt;(32)만큼 추가로 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 트랜잭션이 종료될 때 다음과 같이 최종 상태를 업데이트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;/* Now update sequence tuple to the intended final state */
seq-&amp;gt;last_value = last;     /* last fetched number */
seq-&amp;gt;is_called = true;
seq-&amp;gt;log_cnt = log;         /* how much is logged */&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 log_cnt는 실제로 WAL(Write ahead logging)에 로깅된 시퀀스 값의 수를 추적하며, 이 값들이 캐시되어 있다가 비정상 종료 시 손실된다는 것을 알 수 있다. 소스 코드의 주석 &lt;code&gt;/* how much is logged */&lt;/code&gt; 에서도 이 값이 로깅된 값의 수를 나타냄을 명시하고 있다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>Caching</category>
      <category>gapless assignment</category>
      <category>PostgreSQL</category>
      <category>너에게닿기를</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/18</guid>
      <comments>https://jisu-log.tistory.com/18#entry18comment</comments>
      <pubDate>Sun, 6 Apr 2025 01:02:31 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 이미지 크기 줄이다 chown한테 맞은 이야기</title>
      <link>https://jisu-log.tistory.com/17</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;때는 바야흐로 너에게 닿기를 프로젝트에 합류하기로 결정하고 처음으로 코드를 살펴볼 때였다.&lt;br /&gt;기존 백엔드 개발자 분들이 개인 사정으로 모두 떠난 프로젝트에서 백엔드 개발을 혼자 맡게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낯선 코드 속에서 정신이 혼미해졌지만, 우선 기존 코드와 설정을 파악하며 프로젝트의 흐름을 정리해 나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중 Dockerfile에서 개선할 수 있는 부분을 발견했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 Dockerfile&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Dockerfile은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM node:22-slim

WORKDIR /app

RUN npm install -g pnpm

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    git \
    vim \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

COPY package.json pnpm-lock.yaml ./

RUN pnpm install

COPY . .

EXPOSE 3000

ENTRYPOINT [&quot;pnpm&quot;, &quot;run&quot;, &quot;start:dev&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile을 살펴보니 pnpm을 사용한 설치 과정이 있었고, 실행 시 &lt;code&gt;pnpm run start:dev&lt;/code&gt;를 사용하고 있었다.&lt;br /&gt;해당 커맨드는 개발 환경에서 파일 변경을 지속적으로 모니터링하여 불필요한 리소스를 소모하는 &lt;code&gt;nest start --watch&lt;/code&gt;로 alias 되어있으므로 운영환경에서는 부적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경이 아닌 운영 환경에 적합하도록 entrypoint를 &lt;code&gt;node dist/main&lt;/code&gt;으로 변경하고, multi-stage build를 적용하여 이미지 크기를 최적화하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-stage build 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entrypoint를 &lt;code&gt;node dist/main&lt;/code&gt;으로 바꾸고 multi-stage build를 적용하였다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM node:22-slim AS base
WORKDIR /app
ENV PNPM_HOME=&quot;/pnpm&quot;
ENV PATH=&quot;$PNPM_HOME:$PATH&quot;
RUN corepack enable

FROM base AS prod-deps
COPY package.json .
COPY pnpm-lock.yaml .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile

FROM base AS build
COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build

FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    git \
    vim \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
# 애플리케이션 디렉토리의 소유권을 node 사용자로 변경
RUN chown -R node:node /app

# node 사용자로 전환
USER node
EXPOSE 3000
# 애플리케이션 실행
CMD [&quot;node&quot;, &quot;dist/main&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 이미지 크기를 줄이고 빌드 단계와 실행 환경을 분리하여 최적화하기 위해 stage를 여러 단계로 나눴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prod-deps에서 production 환경에 필요한 의존성만 설치 후 node_modules 디렉토리를 COPY&lt;/li&gt;
&lt;li&gt;build에서 build에 필요한 의존성을 설치하여 build 후 실제 실행 코드가 있는 dist 디렉토리를 COPY&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 있던 불필요한 패키지와 빌드 전 코드를 제외하고 필요한 부분만 production 이미지에 포함시켜 최적화하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 파일이 많이 제거되었으므로 예상대로라면 이미지 크기가 줄어들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 이미지 크기가 커졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;기존 이미지 크기 (649MB)&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;keep-in-touch-be-v2-app   latest        6a192eebdc4c   3 hours ago         649MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바꾼 방식 이미지 크기 (692MB)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
keep-what    latest    8a2db73409ad   3 hours ago   692MB&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;예상과 다르게 43MB 증가하는 결과가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당황한 나는 .. 여러 가설과 검증을 통해 문제를 파악해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드 과정 단순화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 prod, dev 의존성을 나누는 것에 문제가 있나 싶어 두 개로 나뉘어 있던 build 레이어를 하나로 통합해 보았다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM node:22-slim AS base
WORKDIR /app
ENV PNPM_HOME=&quot;/pnpm&quot;
ENV PATH=&quot;$PNPM_HOME:$PATH&quot;
RUN corepack enable

FROM base AS builder

COPY package.json pnpm-lock.yaml ./

RUN pnpm install --frozen-lockfile

COPY . .

RUN pnpm build

FROM base
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/dist /app/dist

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    git \
    vim \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

USER node
EXPOSE 3000
CMD [&quot;node&quot;, &quot;dist/main&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;builder&lt;/code&gt; 이미지에서 빌드 후 dist와 node_modules만 빼오는 방식이다.&lt;br /&gt;여긴 dev dependency도 포함되어 있으므로 이전 방식보다 node_modules의 크기가 클 것이고 당연히 이미지 크기도 더 커야 한다.&lt;/p&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;그러나 결과는 612MB로 오히려 줄어들었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;keep-prod-v3              latest        dfe462f4390a   31 minutes ago      612MB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체 왜,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해할 수 없는 결과가 나와서 두 방식의 빌드 결과물을 비교해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디렉토리 크기 분석&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;node_modules, dist 따로 만들어 결과만 추출하는 첫 번째 이미지&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;1M dist  
204M node\_modules&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;dev dependency 포함하여 빌드하여 같이 추출하는 두 번째 이미지&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;988K    dist
318M    node_modules&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;예상대로 빌드 과정을 하나로 통합한 두 번째 이미지의 node_modules가 114MB 더 컸다.&lt;br /&gt;근데 최종 이미지 결과는 통합된 첫 번째 이미지가 더 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해할 수 없는 결과가 나와서 &lt;code&gt;docker history&lt;/code&gt; 로 레이어 크기를 조사했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker history로 진짜 원인 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker history [image] &amp;mdash;no-trunc&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IMAGE                                CREATED     CREATED BY                          SIZE      
sha256:8a2db73409ad4febb2c514671f... 3 hours ago CMD [&quot;node&quot; &quot;dist/main&quot;]            0B        
&amp;lt;missing&amp;gt;                            3 hours ago EXPOSE map[3000/tcp:{}]             0B        
&amp;lt;missing&amp;gt;                            3 hours ago USER node                           0B        
&amp;lt;missing&amp;gt;                            3 hours ago RUN chown -R node:node /app         152MB     
&amp;lt;missing&amp;gt;                            3 hours ago RUN apt-get update &amp;amp;&amp;amp; apt-get i...  149MB     
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살펴보니 수상한 커맨드가 있었다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;&amp;lt;missing&amp;gt;  3 hours ago   RUN /bin/sh -c chown -R node:node /app # buildkit 152MB     buildkit.dockerfile.v0&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;code&gt;RUN chown -R node:node /app&lt;/code&gt;이 무려 152MB나 차지하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;xczqFVSQ72KthbGhe7D2ikMLlkvTR7YRQR1obkTTZYm1y4U4PVAEcMAJkyi8q0C0mj1WbITp8vuKYqqJLrkJChvMshs-cvBxv2vkGqT7ofhFLgCc1Qt-VBAlHPxbrzDqM0Puj-mY-ks4IIDFYMKL8Q.webp&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXizW/btsM139VGta/SbaOdSTRNdYFBJyE37HyFk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXizW/btsM139VGta/SbaOdSTRNdYFBJyE37HyFk/img.webp&quot; data-alt=&quot;완전.. 정형돈됨..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXizW/btsM139VGta/SbaOdSTRNdYFBJyE37HyFk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXizW%2FbtsM139VGta%2FSbaOdSTRNdYFBJyE37HyFk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;305&quot; data-filename=&quot;xczqFVSQ72KthbGhe7D2ikMLlkvTR7YRQR1obkTTZYm1y4U4PVAEcMAJkyi8q0C0mj1WbITp8vuKYqqJLrkJChvMshs-cvBxv2vkGqT7ofhFLgCc1Qt-VBAlHPxbrzDqM0Puj-mY-ks4IIDFYMKL8Q.webp&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완전.. 정형돈됨..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 이미지에서는 이게 없어서 (비교한다더니 대체 왜 없앴을까? 맘대로 작성했는갑다...) 크기가 줄어든 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image)%5C&quot;&gt;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743342248209&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;Why does chown increase size of docker image?&quot; data-og-description=&quot;I can't understand why the 'chown' command should increase the size of my docker image? The following Dockerfile creates an image of size 5.3MB: FROM alpine:edge RUN adduser example -D -h /exampl...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image)%5C&quot; data-og-url=&quot;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jry5a/hyYvnXvwhx/FSenLJeVxmcL2tVKsCbfDK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image)%5C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/30085621/why-does-chown-increase-size-of-docker-image)%5C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jry5a/hyYvnXvwhx/FSenLJeVxmcL2tVKsCbfDK/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Why does chown increase size of docker image?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I can't understand why the 'chown' command should increase the size of my docker image? The following Dockerfile creates an image of size 5.3MB: FROM alpine:edge RUN adduser example -D -h /exampl...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고보니 Docker의 레이어 시스템이 &lt;code&gt;chown&lt;/code&gt;과 같은 파일 메타데이터 변경도 새 레이어를 생성하기 때문에 발생하는 문제였다.&lt;br /&gt;Docker의 Copy-on-Write 특성으로 인해 &lt;code&gt;chown&lt;/code&gt; 명령으로 변경된 모든 파일이 새 레이어에 다시 복사되어 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 파일 내용은 변하지 않았더라도 메타데이터가 변경된 모든 파일의 용량만큼 이미지 크기가 증가하게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Note that changing the metadata of files, for example, changing file permissions or ownership of a file, can also result in a copy_up operation, therefore duplicating the file to the writable layer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/storage/drivers/#copying-makes-containers-efficient&quot;&gt;https://docs.docker.com/engine/storage/drivers/#copying-makes-containers-efficient&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743342265423&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;Storage drivers&quot; data-og-description=&quot;Learn the technologies that support storage drivers.&quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/engine/storage/drivers/#copying-makes-containers-efficient&quot; data-og-url=&quot;https://docs.docker.com/engine/storage/drivers/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/29L26/hyYvkfeo4C/2zgRYEATylfj7WVM5bvUQk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/storage/drivers/#copying-makes-containers-efficient&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/engine/storage/drivers/#copying-makes-containers-efficient&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/29L26/hyYvkfeo4C/2zgRYEATylfj7WVM5bvUQk/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260');&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;Storage drivers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn the technologies that support storage drivers.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것에 대한 대안으로 &lt;code&gt;ADD&lt;/code&gt;나 &lt;code&gt;COPY&lt;/code&gt; 커맨드에서 &lt;code&gt;--chown&lt;/code&gt; 옵션을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/reference/dockerfile/#copy---chown---chmod)%5Bhttps://docs.docker.com/reference/dockerfile/#copy---chown---chmod%5D&quot;&gt;https://docs.docker.com/reference/dockerfile/#copy---chown---chmod]&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743342277445&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;Dockerfile reference&quot; data-og-description=&quot;Find all the available commands you can use in a Dockerfile and learn how to use them, including COPY, ARG, ENTRYPOINT, and more.&quot; data-og-host=&quot;docs.docker.com&quot; data-og-source-url=&quot;https://docs.docker.com/reference/dockerfile/#copy---chown---chmod)%5Bhttps://docs.docker.com/reference/dockerfile/#copy---chown---chmod%5D&quot; data-og-url=&quot;https://docs.docker.com/reference/dockerfile/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cguNII/hyYyL3wePU/k29yaxhN39x9d2QYYzzyoK/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260&quot;&gt;&lt;a href=&quot;https://docs.docker.com/reference/dockerfile/#copy---chown---chmod)%5Bhttps://docs.docker.com/reference/dockerfile/#copy---chown---chmod%5D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.docker.com/reference/dockerfile/#copy---chown---chmod)%5Bhttps://docs.docker.com/reference/dockerfile/#copy---chown---chmod%5D&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cguNII/hyYyL3wePU/k29yaxhN39x9d2QYYzzyoK/img.jpg?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260');&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;Dockerfile reference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Find all the available commands you can use in a Dockerfile and learn how to use them, including COPY, ARG, ENTRYPOINT, and more.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;chown&lt;/code&gt; 커맨드를 제거하고 빌드한 결과 540MB의 이미지를 얻을 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;keep-what-2               latest        9a00e97a8e2f   3 hours ago      540MB&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;multi-stage 빌드 적용과 불필요한 chown 명령 제거로 기존 이미지(649MB)에서 109MB를 줄였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 Dockerfile&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;FROM node:22-slim AS base
WORKDIR /app

ENV PNPM_HOME=&quot;/pnpm&quot;
ENV PATH=&quot;$PNPM_HOME:$PATH&quot;
RUN corepack enable

FROM base AS prod-deps
COPY package.json .
COPY pnpm-lock.yaml .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile

FROM base AS build
COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build

FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist

# node 사용자로 전환
USER node
EXPOSE 3000
# 애플리케이션 실행
CMD [&quot;node&quot;, &quot;dist/main&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부록&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;난 애초에 왜 chown을 썼을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 공식 이미지가 그렇듯 &lt;code&gt;node&lt;/code&gt; 이미지에서 컨테이너 프로세스는 &lt;code&gt;node&lt;/code&gt;사용자를 이용하여 실행하는 것이 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user)%5C&quot;&gt;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743342309274&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;docker-node/docs/BestPractices.md at main &amp;middot; nodejs/docker-node&quot; data-og-description=&quot;Official Docker Image for Node.js :whale: :turtle: :rocket: - nodejs/docker-node&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user)%5C&quot; data-og-url=&quot;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/J34Mm/hyYxMIUP5i/rmMbQ1uS9ZlXjlsKoSyHz0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/G9huE/hyYxF33zUu/D6S1BFMhnHEWsVvuy8yTJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user)%5C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user)%5C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/J34Mm/hyYxMIUP5i/rmMbQ1uS9ZlXjlsKoSyHz0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/G9huE/hyYxF33zUu/D6S1BFMhnHEWsVvuy8yTJK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;docker-node/docs/BestPractices.md at main &amp;middot; nodejs/docker-node&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Official Docker Image for Node.js :whale: :turtle: :rocket: - nodejs/docker-node&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 &quot;node 사용자에게 소유권을 옮겨야 실행이 되겠지&quot; 하고 단순하게 생각해서 &lt;code&gt;chown&lt;/code&gt;을 사용했다.&lt;br /&gt;하지만 node 이미지에서 node 사용자는 &lt;code&gt;chown&lt;/code&gt; 없이도 &lt;code&gt;USER node&lt;/code&gt;로 사용자 전환만 하면 해당 디렉토리를 실행할 수 있었다..&lt;/p&gt;</description>
      <category>개발</category>
      <category>chown</category>
      <category>docker</category>
      <category>dockerfile</category>
      <category>Linux</category>
      <category>multi-stage</category>
      <category>Node</category>
      <category>node.js</category>
      <category>너에게닿기를</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/17</guid>
      <comments>https://jisu-log.tistory.com/17#entry17comment</comments>
      <pubDate>Sun, 30 Mar 2025 22:35:14 +0900</pubDate>
    </item>
    <item>
      <title>[TypeORM] Entity 관계 설정에 eager, lazy 옵션을 명시해야 할까?</title>
      <link>https://jisu-log.tistory.com/16</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드래빗을 이용해 열심히 리뷰받고 있던 어느 날.. 코드래빗이 다음과 같은 제안을 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계 설정 시 eager 혹은 lazy 옵션을 명시적으로 설정하라는 내용이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ️ Refactor suggestion&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관계 설정에 대한 추가 구성이 필요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 사항들을 고려해주세요:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @OneToOne 데코레이터에 eager 또는 lazy 로딩 옵션을 명시적으로 설정하는 것이 좋습니다.&lt;br /&gt;2. 사용자 삭제 시 통계 데이터 처리를 위한 cascade 옵션 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 수정하는 것을 제안드립니다:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PrimaryColumn({ name: 'user_id', type: 'bigint' })
- @OneToOne(() =&amp;gt; User)
+ @OneToOne(() =&amp;gt; User, { eager: true, onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  userId: number;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17#discussion_r1849672849&quot;&gt;https://github.com/pudding-keep-in-touch/keep-in-touch&lt;/a&gt;&lt;a href=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17#discussion_r1849672849&quot;&gt;-be-v2/pull/17#discussion_r1849672849&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742128132883&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;✨ [Feature] v1 to v2 API 이관 by jis-kim &amp;middot; Pull Request #17 &amp;middot; pudding-keep-in-touch/keep-in-touch-be-v2&quot; data-og-description=&quot;Summary v1 files 삭제 entity file 생성 v1과 로직이 똑같은 /auth/google/login, /auth/google/callback , /users/:userId/nickname 구현 Describe your changes auth, user module 생성 사용자 관련 작업을 관리하는 UsersService 및 U...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17#discussion_r1849672849&quot; data-og-url=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBfs4b/hyYr0NQpdn/CBBDJwLJQBr4HlKxRFCs5K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cCME4z/hyYqWFh7hx/LJkywKSXuTKPuNDHQKYrTK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17#discussion_r1849672849&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/pudding-keep-in-touch/keep-in-touch-be-v2/pull/17#discussion_r1849672849&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBfs4b/hyYr0NQpdn/CBBDJwLJQBr4HlKxRFCs5K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cCME4z/hyYqWFh7hx/LJkywKSXuTKPuNDHQKYrTK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;✨ [Feature] v1 to v2 API 이관 by jis-kim &amp;middot; Pull Request #17 &amp;middot; pudding-keep-in-touch/keep-in-touch-be-v2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary v1 files 삭제 entity file 생성 v1과 로직이 똑같은 /auth/google/login, /auth/google/callback , /users/:userId/nickname 구현 Describe your changes auth, user module 생성 사용자 관련 작업을 관리하는 UsersService 및 U...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 그렇지만.. AI의 조언은 곧이 곧대로 들으면 안되고 맞는 말인지 확인할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eager, lazy 옵션은 잘 사용한 적이 없었으므로 리뷰 받은 김에 둘이 어떤 옵션인지, 이 경우 정말 명시해야 하는지 알아보았다.&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;eager와 lazy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeORM 공식문서를 참고하였을 때, 두 가지 옵션은 각각 다음을 뜻한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;eager: 항상 자동 로드&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;With eager loading enabled on a relation, you don't have to specify relations in the find command as it will&lt;br /&gt;ALWAYS be loaded automatically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eager: true&lt;/code&gt; 로 설정하면 &lt;code&gt;findOne&lt;/code&gt;이나 &lt;code&gt;find&lt;/code&gt; 를 호출할 때 자동으로 관계 데이터를 가져온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;relations&lt;/code&gt; 옵션을 별도로 지정하지 않아도 알아서 join해서 데이터를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;QueryBuilder&lt;/code&gt;를 사용할 때는 eager relation이 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 쪽 관계에서만 사용할 수 있고, 양쪽에서 사용되는 것은 허용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;lazy: 필요할 때 로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lazy: true&lt;/code&gt;로 설정하면 관계 데이터가 즉시 로드되지 않고, 실제로 접근할 때 로드된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 필드 타입이 반드시 &lt;code&gt;Promise&lt;/code&gt; 여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 비동기 방식으로 작동하므로 동기적으로 접근하여 프로퍼티를 가져오는 것이 불가능하기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable,
} from &quot;typeorm&quot;
import { Category } from &quot;./Category&quot;

@Entity()
export class Question {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column()
    text: string

    @ManyToMany((type) =&amp;gt; Category, (category) =&amp;gt; category.questions)
    @JoinTable()
    categories: Promise&amp;lt;Category[]&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;이런 relation을 &lt;code&gt;save&lt;/code&gt; 하려면 다음과 같이 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const category1 = new Category()
category1.name = &quot;animals&quot;
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = &quot;zoo&quot;
await dataSource.manager.save(category2)

const question = new Question()
question.categories = Promise.resolve([category1, category2])
await dataSource.manager.save(question)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 하려면 &lt;code&gt;await&lt;/code&gt; 으로 비동기 처리를 해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;const [question] = await dataSource.getRepository(Question).find()
const categories = await question.categories
// you'll have all question's categories inside &quot;categories&quot; variable now&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;TypeORM 문서에서는 lazy loding이 비표준적인 방법이라고 알려주고 있다.&lt;br /&gt;다른 언어의 ORM은 (대표적으로 Java의 JPA) 관계 설정에서 lazy loading 이 기본 값인 경우가 있지만, TypeORM에서는 lazy loading을 권장하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Note: if you came from other languages (Java, PHP, etc.) and are used to use lazy relations everywhere - be careful. Those languages aren't asynchronous and lazy loading is achieved different way, that's why you don't work with promises there. In JavaScript and Node.JS you have to use promises if you want to have lazy-loaded relations. This is non-standard technique and considered experimental in TypeORM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://typeorm.io/eager-and-lazy-relations#eager-and-lazy-relations&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typeorm.io/eager-and-lazy-relations#eager-and-lazy-relations&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;TypeORM 관계 설정 시 기본값은 eager일까 lazy일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쯤 알아보고 나니 자연스레 의문이 생겼다. TypeORM은 기본 설정으로 대체 뭘 쓸까? eager와 lazy 둘 다 아닌 것 같은데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&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;TypeORM은 &lt;code&gt;findOne&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt; 등에서 &lt;code&gt;relations&lt;/code&gt; 옵션을 명시적으로 지정하는 것을 기본으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시의 categories를 가져오려면 &lt;code&gt;relations&lt;/code&gt;에 명시해 주면 된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const question = await questionRepository.findOne({
  where: { id: 1 },
  relations: { categories: true } // v0.3 이상부터 적용
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;relations&lt;/code&gt;에 이렇게 설정해주면 실제 쿼리 실행 시 &lt;code&gt;JOIN&lt;/code&gt; 이 포함되어 &lt;code&gt;SELECT&lt;/code&gt; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 필요한 경우에만 관계 데이터를 가져오는 방식으로 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서 이 PR에서 수정이 필요한지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 제안이 온 &lt;code&gt;MessageStatistics&lt;/code&gt; 를 사용할 때는 애초에 &lt;code&gt;User&lt;/code&gt; 정보가 필요할 일이 없고, 필요하다면 join을 이용하면 될 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정은 필요 없다고 결론을 내렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;eager: true&lt;/code&gt;는 관계된 데이터를 자동으로 로드하는 방식이며, 한쪽 관계에서만 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lazy: true&lt;/code&gt; 는 관계된 데이터를 실제 접근 시 비동기적으로 로드하는 방식이다.&lt;/li&gt;
&lt;li&gt;TypeORM의 기본 설정은 eager나 lazy 옵션을 사용하지 않으며 &lt;code&gt;relations&lt;/code&gt;를 통해 명시적으로 관계 데이터를 불러온다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>Devlog</category>
      <category>Eager</category>
      <category>Lazy</category>
      <category>typeORM</category>
      <category>너에게닿기를</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/16</guid>
      <comments>https://jisu-log.tistory.com/16#entry16comment</comments>
      <pubDate>Sun, 16 Mar 2025 21:29:44 +0900</pubDate>
    </item>
    <item>
      <title>Notion API로 원하는 날짜에 일정 생성하기</title>
      <link>https://jisu-log.tistory.com/13</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8~9월동안 원티드 프리온보딩 인턴쉽 챌린지를 진행하며 좋은 인연을 맺게 되었다. 팀으로 활동하는 과정은 끝났지만, 데일리 스크럼을 통해 매일 할 일을 정리하며 취업 활동을 같이 이어나가기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 팀 공용으로 쓰는 노션 페이지의 캘린더 뷰 데이터베이스에 팀원들의 리마인드를 위해 직접 블록을 생성해오고 있었다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-10-07 at 9.56.51 PM.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTllvp/btsJW87YC2D/eRbxU5l2ubwabcjyi4IOpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTllvp/btsJW87YC2D/eRbxU5l2ubwabcjyi4IOpK/img.png&quot; data-alt=&quot;이런 식으로.. 다 직접 생성해서 관리했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTllvp/btsJW87YC2D/eRbxU5l2ubwabcjyi4IOpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTllvp%2FbtsJW87YC2D%2FeRbxU5l2ubwabcjyi4IOpK%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;1414&quot; height=&quot;800&quot; data-filename=&quot;Screenshot 2024-10-07 at 9.56.51 PM.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;800&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;과정을 진행하며 공휴일을 제외한 평일 오전 10시 30분에 정기적으로 진행하기로 정해졌으므로 자동화 방법을 찾고있었다. 매번 페이지 생성 하는 것은 너무 번거롭기 때문에...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Notion 페이지 반복 기능 이용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-10-07 at 10.10.50 PM.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv4ZXr/btsJW4drwfZ/UBb16kOEBCl3jog4tOLS20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv4ZXr/btsJW4drwfZ/UBb16kOEBCl3jog4tOLS20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv4ZXr/btsJW4drwfZ/UBb16kOEBCl3jog4tOLS20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv4ZXr%2FbtsJW4drwfZ%2FUBb16kOEBCl3jog4tOLS20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;446&quot; data-filename=&quot;Screenshot 2024-10-07 at 10.10.50 PM.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;446&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;데이터베이스의 템플릿에 ... 버튼을 누르면 Repeat 설정을 할 수 있다.&lt;/p&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;Screenshot 2024-10-07 at 10.12.16 PM.png&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v9ayM/btsJYEdvz8i/l0L7zo3qrQezKmlWNBAtyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v9ayM/btsJYEdvz8i/l0L7zo3qrQezKmlWNBAtyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v9ayM/btsJYEdvz8i/l0L7zo3qrQezKmlWNBAtyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv9ayM%2FbtsJYEdvz8i%2Fl0L7zo3qrQezKmlWNBAtyK%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;734&quot; height=&quot;648&quot; data-filename=&quot;Screenshot 2024-10-07 at 10.12.16 PM.png&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;648&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;여기서 weekly 를 선택하면 요일 지정은 가능하지만... 더 섬세한 작업은 불가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 원하는 것은 &lt;b&gt;&quot;공휴일과 주말을 제외한 모든 날에 자동생성되는 페이지&quot;&lt;/b&gt; 였기 때문에 다른 방법을 찾기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Notion API와 공공 API 이용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공공 데이터 포털에서 제공해주는 특일 API를 이용해서 공휴일을 판별하고, Notion API를 이용해서 페이지 생성 요청을 보내면 정확히 내가 원하는 작업을 할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특일 API&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.data.go.kr/data/15012690/openapi.do&quot;&gt;https://www.data.go.kr/data/15012690/openapi.do&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457103447&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;(천문우주정보)국경일정보, 공휴일정보, 기념일정보, 24절기정보, 잡절정보를 조회하는 서비스 입니다. 활용시 날짜, 순번, 특일정보의 분류, 공공기관 휴일 여부, 명칭을 확인할 수 있습니다.&quot; data-og-host=&quot;www.data.go.kr&quot; data-og-source-url=&quot;https://www.data.go.kr/data/15012690/openapi.do&quot; data-og-url=&quot;https://www.data.go.kr/data/15012690/openapi.do&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b6KwrM/hyXd8zOoxR/iVXIAe75ayPkQffla3M4Xk/img.png?width=390&amp;amp;height=158&amp;amp;face=0_0_390_158&quot;&gt;&lt;a href=&quot;https://www.data.go.kr/data/15012690/openapi.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.data.go.kr/data/15012690/openapi.do&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b6KwrM/hyXd8zOoxR/iVXIAe75ayPkQffla3M4Xk/img.png?width=390&amp;amp;height=158&amp;amp;face=0_0_390_158');&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;(천문우주정보)국경일정보, 공휴일정보, 기념일정보, 24절기정보, 잡절정보를 조회하는 서비스 입니다. 활용시 날짜, 순번, 특일정보의 분류, 공공기관 휴일 여부, 명칭을 확인할 수 있습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.data.go.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;짧게 찾아본 바로는 공휴일을 제공해주는 API가 이것 뿐이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 정보 중 &quot;공휴일 정보&quot;를 받아오면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 URL은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService/getRestDeInfo&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 필요한 query parameter를 붙여서 요청하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 10월의 공휴일 데이터는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;yes&quot;?&amp;gt;&amp;lt;response&amp;gt;&amp;lt;header&amp;gt;&amp;lt;resultCode&amp;gt;00&amp;lt;/resultCode&amp;gt;&amp;lt;resultMsg&amp;gt;NORMAL SERVICE.&amp;lt;/resultMsg&amp;gt;&amp;lt;/header&amp;gt;&amp;lt;body&amp;gt;&amp;lt;items&amp;gt;&amp;lt;item&amp;gt;&amp;lt;dateKind&amp;gt;01&amp;lt;/dateKind&amp;gt;&amp;lt;dateName&amp;gt;임시공휴일&amp;lt;/dateName&amp;gt;&amp;lt;isHoliday&amp;gt;Y&amp;lt;/isHoliday&amp;gt;&amp;lt;locdate&amp;gt;20241001&amp;lt;/locdate&amp;gt;&amp;lt;seq&amp;gt;2&amp;lt;/seq&amp;gt;&amp;lt;/item&amp;gt;&amp;lt;item&amp;gt;&amp;lt;dateKind&amp;gt;01&amp;lt;/dateKind&amp;gt;&amp;lt;dateName&amp;gt;개천절&amp;lt;/dateName&amp;gt;&amp;lt;isHoliday&amp;gt;Y&amp;lt;/isHoliday&amp;gt;&amp;lt;locdate&amp;gt;20241003&amp;lt;/locdate&amp;gt;&amp;lt;seq&amp;gt;1&amp;lt;/seq&amp;gt;&amp;lt;/item&amp;gt;&amp;lt;item&amp;gt;&amp;lt;dateKind&amp;gt;01&amp;lt;/dateKind&amp;gt;&amp;lt;dateName&amp;gt;한글날&amp;lt;/dateName&amp;gt;&amp;lt;isHoliday&amp;gt;Y&amp;lt;/isHoliday&amp;gt;&amp;lt;locdate&amp;gt;20241009&amp;lt;/locdate&amp;gt;&amp;lt;seq&amp;gt;1&amp;lt;/seq&amp;gt;&amp;lt;/item&amp;gt;&amp;lt;/items&amp;gt;&amp;lt;numOfRows&amp;gt;10&amp;lt;/numOfRows&amp;gt;&amp;lt;pageNo&amp;gt;1&amp;lt;/pageNo&amp;gt;&amp;lt;totalCount&amp;gt;3&amp;lt;/totalCount&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/response&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Notion API&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.notion.com/reference/post-page&quot;&gt;https://developers.notion.com/reference/post-page&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457088450&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;Create a page&quot; data-og-description=&quot;Creates a new page that is a child of an existing page or database. If the new page is a child of an existing page, title is the only valid property in the properties body param. If the new page is a child of an existing database, the keys of the propertie&quot; data-og-host=&quot;developers.notion.com&quot; data-og-source-url=&quot;https://developers.notion.com/reference/post-page&quot; data-og-url=&quot;https://developers.notion.com/reference/post-page&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/M2lCY/hyXechTNXS/aLzjC8dK7ul5qu9ytAVIV1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dF6bVh/hyXd9ZMYiZ/KbiVmhW06DHsU9k6p0vKpK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.notion.com/reference/post-page&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.notion.com/reference/post-page&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/M2lCY/hyXechTNXS/aLzjC8dK7ul5qu9ytAVIV1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dF6bVh/hyXd9ZMYiZ/KbiVmhW06DHsU9k6p0vKpK/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;Create a page&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Creates a new page that is a child of an existing page or database. If the new page is a child of an existing page, title is the only valid property in the properties body param. If the new page is a child of an existing database, the keys of the propertie&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.notion.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;굉장히 다양한 기능을 API로 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Block을 생성해야 한다고 생각했는데, 캘린더 일정은 Database에 소속된 Page 형태여서 Page를 생성하는 요청을 보내야 한다.&lt;/p&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;사용하기 전에 &quot;API Integration&quot;으로 앱과 노션 데이터베이스를 연결하는 작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.notion.com/docs/authorization&quot;&gt;https://developers.notion.com/docs/authorization&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457076604&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;Authorization&quot; data-og-description=&quot;This guide describes the authorization flows for internal and public Notion integrations.&quot; data-og-host=&quot;developers.notion.com&quot; data-og-source-url=&quot;https://developers.notion.com/docs/authorization&quot; data-og-url=&quot;https://developers.notion.com/docs/authorization&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qwPaI/hyXd3FiAly/YSfraCuP556K46wcYgpFtk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/y4zMZ/hyXd5QBVuF/7gAfNjVx8JOBS4BhgGUfA0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.notion.com/docs/authorization&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.notion.com/docs/authorization&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qwPaI/hyXd3FiAly/YSfraCuP556K46wcYgpFtk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/y4zMZ/hyXd5QBVuF/7gAfNjVx8JOBS4BhgGUfA0/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;Authorization&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This guide describes the authorization flows for internal and public Notion integrations.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.notion.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/profile/integrations&quot;&gt;Integration 설정 페이지&lt;/a&gt;로 들어가서 새 Integration (새 API 통합)을 생성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 생성한 Integration을 사용할 데이터베이스에 꼭 연결해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 생성만 하고 연결은 안 한 상태에서 왜 안되지를 외치고 있었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api#add-connections-to-pages&quot;&gt;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api#add-connections-to-pages&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457083198&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;API 통합 추가와 관리 &amp;ndash; Notion (노션) 도움말 센터&quot; data-og-description=&quot;다른 소프트웨어의 API를 Notion에 연결하고, 워크스페이스 작업을 자동화하고, 다른 플랫폼의 서비스를 Notion에서 이용하세요  &quot; data-og-host=&quot;www.notion.so&quot; data-og-source-url=&quot;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api#add-connections-to-pages&quot; data-og-url=&quot;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bm7kj7/hyXeibn6wz/AStPOs4WJEIZPeYG86mEQK/img.png?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160,https://scrap.kakaocdn.net/dn/cINhTU/hyXeemvii6/l0Ww7JUYfVDJvR8zVwzIRK/img.png?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160&quot;&gt;&lt;a href=&quot;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api#add-connections-to-pages&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.notion.so/ko/help/add-and-manage-connections-with-the-api#add-connections-to-pages&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bm7kj7/hyXeibn6wz/AStPOs4WJEIZPeYG86mEQK/img.png?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160,https://scrap.kakaocdn.net/dn/cINhTU/hyXeemvii6/l0Ww7JUYfVDJvR8zVwzIRK/img.png?width=3840&amp;amp;height=2160&amp;amp;face=0_0_3840_2160');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;API 통합 추가와 관리 &amp;ndash; Notion (노션) 도움말 센터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;다른 소프트웨어의 API를 Notion에 연결하고, 워크스페이스 작업을 자동화하고, 다른 플랫폼의 서비스를 Notion에서 이용하세요  &lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.notion.so&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Om7m/btsJZMQJA80/OawMV8sxPh4O4jazDUK0o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Om7m/btsJZMQJA80/OawMV8sxPh4O4jazDUK0o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Om7m/btsJZMQJA80/OawMV8sxPh4O4jazDUK0o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Om7m%2FbtsJZMQJA80%2FOawMV8sxPh4O4jazDUK0o1%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;1362&quot; height=&quot;614&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;614&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;정상적으로 Integration을 생성했다면 점 세개 - connect to를 눌렀을 때 내 Integration이 보인다. 누르고 confirm하면 연결 완료.&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;Notion SDK&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/makenotion/notion-sdk-js&quot;&gt;https://github.com/makenotion/notion-sdk-js&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457145757&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - makenotion/notion-sdk-js: Official Notion JavaScript Client&quot; data-og-description=&quot;Official Notion JavaScript Client. Contribute to makenotion/notion-sdk-js development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/makenotion/notion-sdk-js&quot; data-og-url=&quot;https://github.com/makenotion/notion-sdk-js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dCtM06/hyXd6Wg6MW/5EvKr1lcJfU0qbXfMjWNnk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/MamNT/hyXd96wD7H/TUjMHlXLcTgLmKRbfrXakk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/makenotion/notion-sdk-js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/makenotion/notion-sdk-js&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dCtM06/hyXd6Wg6MW/5EvKr1lcJfU0qbXfMjWNnk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/MamNT/hyXd96wD7H/TUjMHlXLcTgLmKRbfrXakk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - makenotion/notion-sdk-js: Official Notion JavaScript Client&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Official Notion JavaScript Client. Contribute to makenotion/notion-sdk-js development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion팀이 동일한 기능을 제공하는 Javascript SDK도 제공하고 있다. (정말 감사합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운이 좋게도 Javascript를 이용해서 개발 중이었기에 바로 가져다 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page POST 요청을 하려면 클라이언트 객체 생성을 한 후, &lt;code&gt;pages.create()&lt;/code&gt; 메소드를 호출해주면 된다. 너무 간단해서 감동의 눈물이..&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;  const notion = new Client({
    auth: process.env.NOTION_SECRET,
  });

  await notion.pages.create(page);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단 플로우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공휴일 API 요청 및 응답 데이터 처리 -&amp;gt; 공휴일이 아닌 날짜만 Page 타입 객체로 생성 -&amp;gt; Notion SDK 사용하여 Notion API 요청&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 공휴일 데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공휴일 정보는 xml형태로 오므로 편한 처리를 위해 &lt;code&gt;fast-xml-parser&lt;/code&gt;를 이용하여 js object 형태로 변환하여 사용하였다. 많은 패키지 중 &lt;code&gt;fast-xml-parser&lt;/code&gt;를 선택한 이유는 최근 다운수가 많았고, 업데이트 날짜가 최근(한 달 전)이었기 때문이다. 패키지 크기도 작았던 것으로 기억한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;response.body.items.item&lt;/code&gt;에 원하는 값의 배열이 들어있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  item: [
    {
      dateKind: '01',
      dateName: '임시공휴일',
      isHoliday: 'Y',
      locdate: '20241001',
      seq: '2'
    },
    ...
  ]
}&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;locdate를 YYYY-MM-DD 형태로 변형한 후 새 &lt;code&gt;Date&lt;/code&gt; 객체를 생성해주었다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;  return items.map((item) =&amp;gt; {
    const date = item.locdate;
    return new Date(
      `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}`
    );
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Page 객체 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Notion Client의 &lt;code&gt;page.create()&lt;/code&gt;의 인자로는 생성할 Database의 속성에 맞는 객체가 들어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 확인을 위해 &lt;code&gt;database.retrive()&lt;/code&gt;를 호출해서 상세 속성을 확인하였고, 다음 형태로 객체를 생성해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  parent: { database_id },
  icon: {
    emoji: ' ',
  },
  properties: {
    // database의 속성과 일치
    Name: {
      title: [
        {
          text: {
            content: '데일리 스크럼',
          },
        },
      ],
    },
    Date: {
      date: {
        start: date, // Date type
        time_zone: 'Asia/Seoul',
      },
    },
  },
  children: [
    {
      object: 'block',
      type: 'paragraph',
      paragraph: {
        rich_text: [
          {
            text: {
              content: 'notion API로 만들어진 페이지입니다.',
            },
          },
        ],
      },
    },
  ],
}&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;&lt;code&gt;Page&lt;/code&gt;와 &lt;code&gt;PageBuilder&lt;/code&gt;를 사용하여 쉽게 알맞은 타입의 객체가 생성되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;export class Page {
  constructor(databaseId) {
    this.parent = { database_id: databaseId };
    this.properties = {};
    this.children = [];
    this.icon = null;
  }

  addProperty(name, value) {
    this.properties[name] = value;
    return this;
  }

  addChildBlock(block) {
    this.children.push(block);
    return this;
  }

  setIcon(emoji) {
    this.icon = { emoji };
    return this;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { Page } from './page.js';

export class PageBuilder {
  constructor(databaseId) {
    this.page = new Page(databaseId);
  }

  setTitle(title) {
    this.page.addProperty('Name', {
      title: [{ text: { content: title } }],
    });
    return this;
  }

  setDate(date) {
    this.page.addProperty('Date', {
      date: { start: date.toISOString(), time_zone: 'Asia/Seoul' },
    });
    return this;
  }

  setIcon(emoji) {
    this.page.setIcon(emoji);
    return this;
  }

  addParagraph(content) {
    this.page.addChildBlock({
      object: 'block',
      type: 'paragraph',
      paragraph: {
        rich_text: [{ text: { content } }],
      },
    });
    return this;
  }

  build() {
    return this.page;
  }
}&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;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;pageArray.push(
  new PageBuilder(databaseId)
    .setTitle('데일리 스크럼')
    .setDate(date)
    .setIcon(' ')
    .addParagraph('notion API로 만들어진 페이지입니다.')
    .build()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Notion API 요청 보내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 다른 방법을 발견하지 못한 걸 수도 있지만.. 안타깝게도 &lt;code&gt;page.create()&lt;/code&gt;로는 한 요청에 한 페이지밖에 생성할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;PageBuilder&lt;/code&gt;로 &lt;code&gt;Page&lt;/code&gt; 배열을 생성하고 &lt;code&gt;map()&lt;/code&gt;을 사용하여 각 페이지를 생성하는 &lt;code&gt;page.create()&lt;/code&gt; 요청의 &lt;code&gt;Promise&lt;/code&gt; 배열을 만든 다음, &lt;code&gt;Promise.all()&lt;/code&gt;로 병렬 처리하였다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const createNotionPage = async (holidays) =&amp;gt; {
  const databaseId = process.env.NOTION_DATABASE_ID;
  const pageArray = createPageArray(holidays, databaseId);

  const notion = new Client({
    auth: process.env.NOTION_SECRET,
  });

  const promises = pageArray.map((page) =&amp;gt; {
    notion.pages.create(page);
    console.log('page created at: ', page.properties.Date.date.start);
  });

  await Promise.all(promises);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 배포할 수 있는 Railway를 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 배포 서비스 중 Railway를 선택한 이유는 무료 플랜이 있고 배포 과정이 간단해 보였기 때문이다. 무료 플랜은 램 500MB, 디스크 1GB, vCPU 2개가 제공된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github repository로도 할 수 있고, Docker image로도 할 수 있는데 간단한 프로그램이라 repository를 직접 연결해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;1974&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnFKm/btsJ0fEO0g0/jWrjO9n0umSv5RmvQw31q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnFKm/btsJ0fEO0g0/jWrjO9n0umSv5RmvQw31q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnFKm/btsJ0fEO0g0/jWrjO9n0umSv5RmvQw31q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqnFKm%2FbtsJ0fEO0g0%2FjWrjO9n0umSv5RmvQw31q0%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;3104&quot; height=&quot;1974&quot; data-origin-width=&quot;3104&quot; data-origin-height=&quot;1974&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lKjTz/btsJYcpvboJ/thtw0MDE38CL8huOydKBh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lKjTz/btsJYcpvboJ/thtw0MDE38CL8huOydKBh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lKjTz/btsJYcpvboJ/thtw0MDE38CL8huOydKBh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlKjTz%2FbtsJYcpvboJ%2Fthtw0MDE38CL8huOydKBh1%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;594&quot; height=&quot;437&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;762&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;create 누르고 원하는 github repo 누르면 그대로 &lt;code&gt;package.json&lt;/code&gt; 읽어서 실행해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scripts에&amp;nbsp; &quot;start&quot;가 없으면 실행이 안된다. 간단하게 node src/main.js를 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 railway github app 설치해도 되냐고도 물어보는데 허용해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그보니까 Docker image 생성해서 하는 것 같던데 repository 연결보단 Docker image파일 이용하는 쪽이 더 빠를 듯 싶다.&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;Screenshot 2024-10-09 at 3.52.21 PM.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/909WC/btsJYBJrwiC/TK3NoNX8N4Vo2xgMKv7pFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/909WC/btsJYBJrwiC/TK3NoNX8N4Vo2xgMKv7pFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/909WC/btsJYBJrwiC/TK3NoNX8N4Vo2xgMKv7pFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F909WC%2FbtsJYBJrwiC%2FTK3NoNX8N4Vo2xgMKv7pFk%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;662&quot; height=&quot;344&quot; data-filename=&quot;Screenshot 2024-10-09 at 3.52.21 PM.png&quot; data-origin-width=&quot;1716&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-10-09 at 3.52.41 PM.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1050&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8B62I/btsJ0cg6C9v/0TKoygf723NaLGXZwLvMek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8B62I/btsJ0cg6C9v/0TKoygf723NaLGXZwLvMek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8B62I/btsJ0cg6C9v/0TKoygf723NaLGXZwLvMek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8B62I%2FbtsJ0cg6C9v%2F0TKoygf723NaLGXZwLvMek%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;640&quot; height=&quot;389&quot; data-filename=&quot;Screenshot 2024-10-09 at 3.52.41 PM.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1050&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 후 앱에 맞게 설정할 것 설정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;variable과 cron schedule을 설정해주었다.&lt;/p&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;blob&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;1083&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmHorf/btsJY2zWuat/nF5dzkKApMFsnJzyx9owM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmHorf/btsJY2zWuat/nF5dzkKApMFsnJzyx9owM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmHorf/btsJY2zWuat/nF5dzkKApMFsnJzyx9owM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmHorf%2FbtsJY2zWuat%2FnF5dzkKApMFsnJzyx9owM0%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;439&quot; height=&quot;460&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;1083&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;이런 변경사항이 있으면 꼭.. Apply change 해주어야 한다... variable 설정 했는데도 자꾸 undefined 문제가 발생해서 뭔가 했더니 휴먼에러였던...........&lt;/p&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-10-09 at 4.05.18 PM.png&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QetGk/btsJYbqHxFK/yckLz4HhoIx7D5j61atD61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QetGk/btsJYbqHxFK/yckLz4HhoIx7D5j61atD61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QetGk/btsJYbqHxFK/yckLz4HhoIx7D5j61atD61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQetGk%2FbtsJYbqHxFK%2FyckLz4HhoIx7D5j61atD61%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;2572&quot; height=&quot;986&quot; data-filename=&quot;Screenshot 2024-10-09 at 4.05.18 PM.png&quot; data-origin-width=&quot;2572&quot; data-origin-height=&quot;986&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-filename=&quot;edited_Screenshot 2024-10-09 at 4.05.29 PM.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baPAnW/btsJY9ZZJ1F/9yr9QjUeSG4BWGNvC6ogX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baPAnW/btsJY9ZZJ1F/9yr9QjUeSG4BWGNvC6ogX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baPAnW/btsJY9ZZJ1F/9yr9QjUeSG4BWGNvC6ogX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaPAnW%2FbtsJY9ZZJ1F%2F9yr9QjUeSG4BWGNvC6ogX0%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;498&quot; height=&quot;459&quot; data-filename=&quot;edited_Screenshot 2024-10-09 at 4.05.29 PM.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;978&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jis-kim/make-some-scrum&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jis-kim/make-some-scrum&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1728457648906&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - jis-kim/make-some-scrum: 스크럼 벗고 소리질러&quot; data-og-description=&quot;스크럼 벗고 소리질러. Contribute to jis-kim/make-some-scrum development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/jis-kim/make-some-scrum&quot; data-og-url=&quot;https://github.com/jis-kim/make-some-scrum&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boaqU4/hyXef6MO1o/vRGVXgHG46dBg4h3e1tSW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b13rpQ/hyXebcgYri/f9LbltP8JUg1oHLkKi0SB1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/jis-kim/make-some-scrum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/jis-kim/make-some-scrum&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boaqU4/hyXef6MO1o/vRGVXgHG46dBg4h3e1tSW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b13rpQ/hyXebcgYri/f9LbltP8JUg1oHLkKi0SB1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - jis-kim/make-some-scrum: 스크럼 벗고 소리질러&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스크럼 벗고 소리질러. Contribute to jis-kim/make-some-scrum development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 거 없지만 깃헙은 이쪽입니다,,&lt;/p&gt;</description>
      <category>개발</category>
      <category>JavaScript</category>
      <category>node.js</category>
      <category>notion</category>
      <category>NotionAPI</category>
      <category>notionsdk</category>
      <category>Railway</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/13</guid>
      <comments>https://jisu-log.tistory.com/13#entry13comment</comments>
      <pubDate>Wed, 9 Oct 2024 16:10:59 +0900</pubDate>
    </item>
    <item>
      <title>[Passport.js] passport 에서 로그인과 callback route 를 나눠야 할까?</title>
      <link>https://jisu-log.tistory.com/12</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2.0 을 이용하여 로그인을 개발하던 중..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Nest 가 먹기 좋게 감싸놓은 &lt;code&gt;@nestjs/passport&lt;/code&gt; 패키지와 &lt;code&gt;@UseGuard(AuthGuard(`${strategy명}`))&lt;/code&gt; 을 이용하여 편안하게 개발하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때 동료가 저에게 로그인 프롬프트를 띄우는 경로와 콜백 경로가 같아도 인증이 잘 작동한다는 놀라운 사실을 알려주고 말았습니다. 같은 Guard를 사용하는데도 다른 동작을 한다니??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 외부 로그인 프롬프트를 띄워야하고 하나는 인증된 정보를 받아 처리해야하니 당연히 경로가 달라야겠지 하고 별 생각없이 '/login' 과 '/login/callback' 이라는 두 가지 경로를 만들던 저는 정말 하나로 통일해도 잘 작동한다는 사실을 알고 왜 그런지 궁금해졌습니다..&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;009&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/009.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/009.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AuthGuard에서는 무슨 일이 일어나는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;route handler 에 Guard 를 지정하게 되면 경로에 들어올 때마다 해당 Guard의 &lt;code&gt;canActive()&lt;/code&gt; 를 실행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@nestjs/passport&lt;/code&gt; 패키지의 &lt;code&gt;AuthGuard&lt;/code&gt; 의 &lt;code&gt;canActive&lt;/code&gt; 는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1698999331978&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
  const options = {
    ...defaultOptions,
    ...this.options,
    ...(await this.getAuthenticateOptions(context))
  };
  const [request, response] = [
    this.getRequest(context),
    this.getResponse(context)
  ];
  const passportFn = createPassportContext(request, response);
  const user = await passportFn(
    type || this.options.defaultStrategy,
    options,
    (err, user, info, status) =&amp;gt;
      this.handleRequest(err, user, info, context, status)
  );
  request[options.property || defaultOptions.property] = user;
  return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;createPassportContext()&lt;/code&gt;를 들어가보면?&lt;/p&gt;
&lt;pre id=&quot;code_1698999383890&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const createPassportContext =
  (request, response) =&amp;gt; (type, options, callback: Function) =&amp;gt;
    new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt;
      passport.authenticate(type, options, (err, user, info, status) =&amp;gt; {
        try {
          request.authInfo = info;
          return resolve(callback(err, user, info, status));
        } catch (err) {
          reject(err);
        }
      })(request, response, (err) =&amp;gt; (err ? reject(err) : resolve()))
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;passport.authenticate()&lt;/code&gt; 를 Promise 로 감싼 객체를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 express 와 passport 를 이용해서도 로그인을 구현했었는데, 그 때의 코드와 상당히 비슷합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;express 와 42 passport strategy를 사용했을 때의 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1698999518222&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  router.get('/42', passport.authenticate('42', { session: false }));

  // return으로 redirect와 동시에 종료
  router.get('/42/return', (req, res) =&amp;gt; {
    passport.authenticate('42', { session: false }, async (err, user) =&amp;gt; {
      try {
        if (err || !user) throw err;

        await setTokens(res, user);
        res.redirect('/');
      } catch (err) {
        logger.warn(`passport authenticate return :  ${err.stack}`);
        res.redirect('/login');
      }
    })(req, res);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 해당 코드를 쓰기 쉽게 래핑해놓은 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;AuthGuard&lt;/code&gt; 가 두 개의 동작을 다르게 해주는 줄 알았는데, 그게 아니고 passport 의 &lt;code&gt;authenticate()&lt;/code&gt; 가 옵션에 따라 다르게 처리해주는 것 같아 보입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Passport.authenticate() 에서는 무슨 일이 일어나는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 authenticate 는 대체 뭘 하는거지..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금해서 42 passport 의 &lt;code&gt;authenticate()&lt;/code&gt; 를 까봤습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pandark/passport-42/blob/d458a4386e35ec1ffb7345efd8b54d2cda45533d/lib/strategy.js#L66&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/pandark/passport-42/blob/d458a4386e35ec1ffb7345efd8b54d2cda45533d/lib/strategy.js#L66&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699015076447&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var OAuth2Strategy = require('passport-oauth2');
var util = require('util');
var Profile = require('./profile');
var InternalOAuthError = require('passport-oauth2').InternalOAuthError;

function Strategy(options, verify) {
  options = options || {};
  options.authorizationURL = options.authorizationURL ||
    'https://api.intra.42.fr/oauth/authorize';
  options.tokenURL = options.tokenURL || 'https://api.intra.42.fr/oauth/token';
  options.customHeaders = options.customHeaders || {};

  if (!options.customHeaders['User-Agent']) {
    options.customHeaders['User-Agent'] = options.userAgent || 'passport-42';
  }

  OAuth2Strategy.call(this, options, verify);
  this.name = '42';
  this._profileURL = options.profileURL || 'https://api.intra.42.fr/v2/me';
  this._profileFields = options.profileFields || null;
  this._oauth2.useAuthorizationHeaderforGET(true);
}

// Inherit from `OAuth2Strategy`.
util.inherits(Strategy, OAuth2Strategy);
...&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;까봤더니 authenticate 는 없고 &lt;code&gt;Strategy&lt;/code&gt; 함수의 정의와 &lt;code&gt;util.inherits(Strategy, OAuth2Strategy);&lt;/code&gt; 만 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보는 함수였기 때문에 구글링을 해보니 prototype 을 상속받는 함수였습니다. class와 extend 문법이 생기면서 deprecated 되었습니다. 그렇다면 &lt;code&gt;util.inherits(Strategy, OAuth2Strategy);&lt;/code&gt; 의 의미는 42-passport 의 &lt;code&gt;Strategy&lt;/code&gt; 가 &lt;code&gt;OAuth2Strategy&lt;/code&gt;를 상속받는다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699009456447&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;Util | Node.js v21.1.0 Documentation&quot; data-og-description=&quot;Util# Source Code: lib/util.js The node:util module supports the needs of Node.js internal APIs. Many of the utilities are useful for application and module developers as well. To access it: const util = require('node:util'); copy util.callbackify(original&quot; data-og-host=&quot;nodejs.org&quot; data-og-source-url=&quot;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&quot; data-og-url=&quot;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nodejs.org/docs/latest/api/util.html#utilinheritsconstructor-superconstructor&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;Util | Node.js v21.1.0 Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Util# Source Code: lib/util.js The node:util module supports the needs of Node.js internal APIs. Many of the utilities are useful for application and module developers as well. To access it: const util = require('node:util'); copy util.callbackify(original&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nodejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699008957135&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;Educative Answers - Trusted Answers to Developer Questions&quot; data-og-description=&quot;Level up your coding skills. No more passive learning. Interactive in-browser environments keep you engaged and test your progress as you go.&quot; data-og-host=&quot;www.educative.io&quot; data-og-source-url=&quot;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&quot; data-og-url=&quot;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/f3cvP/hyUnS7W4K9/E3DuPcdIptonsxRd8gw6s0/img.png?width=1667&amp;amp;height=1667&amp;amp;face=0_0_1667_1667&quot;&gt;&lt;a href=&quot;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.educative.io/answers/what-is-utilinherits-in-nodejs&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/f3cvP/hyUnS7W4K9/E3DuPcdIptonsxRd8gw6s0/img.png?width=1667&amp;amp;height=1667&amp;amp;face=0_0_1667_1667');&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;Educative Answers - Trusted Answers to Developer Questions&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Level up your coding skills. No more passive learning. Interactive in-browser environments keep you engaged and test your progress as you go.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.educative.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2Strategy 는 passport-oauth2 모듈이었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699014746934&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var OAuth2Strategy = require('passport-oauth2');&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;그렇다면 42-passport 의 authenticate 는 passport-oauth2 의 authenticate 와 동일할 것으로 생각됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 passport-oauth2 의 authenticate 를 살펴봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/jaredhanson/passport-oauth2/blob/ea9e99adda82dff67502654347589866fea80eb2/lib/strategy.js#L130&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jaredhanson/passport-oauth2/blob/ea9e99adda82dff67502654347589866fea80eb2/lib/strategy.js#L130&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1699017899163&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OAuth2Strategy.prototype.authenticate = function(req, options) {
  options = options || {};
  var self = this;

  ...
  
  if ((req.query &amp;amp;&amp;amp; req.query.code) || (req.body &amp;amp;&amp;amp; req.body.code)) {
    function loaded(err, ok, state) {
      if (err) { return self.error(err); }
      if (!ok) {
        return self.fail(state, 403);
      }

      var code = (req.query &amp;amp;&amp;amp; req.query.code) || (req.body &amp;amp;&amp;amp; req.body.code);

      var params = self.tokenParams(options);
      params.grant_type = 'authorization_code';
      if (callbackURL) { params.redirect_uri = callbackURL; }
      if (typeof ok == 'string') { // PKCE
        params.code_verifier = ok;
      }
      self._oauth2.getOAuthAccessToken(code, params,
        function(err, accessToken, refreshToken, params) {
        ...
      });
    });
     ...
    var state = (req.query &amp;amp;&amp;amp; req.query.state) || (req.body &amp;amp;&amp;amp; req.body.state);
    try {
      var arity = this._stateStore.verify.length;
      if (arity == 4) {
        this._stateStore.verify(req, state, meta, loaded);
      } else { // arity == 3
        this._stateStore.verify(req, state, loaded);
      }
    } catch (ex) {
      return this.error(ex);
    }
 } else {
    var params = this.authorizationParams(options);
    params.response_type = 'code';
    if (callbackURL) { params.redirect_uri = callbackURL; }

	...
    
	var state = options.state;
    if (state &amp;amp;&amp;amp; typeof state == 'string') {      
      params.state = state;
      
      var parsed = url.parse(this._oauth2._authorizeUrl, true);
      utils.merge(parsed.query, params);
      parsed.query['client_id'] = this._oauth2._clientId;
      delete parsed.search;
      var location = url.format(parsed);
      this.redirect(location);   
    } else {
      function stored(err, state) {
        if (err) { return self.error(err); }

        if (state) { params.state = state; }
        var parsed = url.parse(self._oauth2._authorizeUrl, true);
        utils.merge(parsed.query, params);
        parsed.query['client_id'] = self._oauth2._clientId;
        delete parsed.search;
        var location = url.format(parsed);
        self.redirect(location);
      }

      try {
        var arity = this._stateStore.store.length;
        if (arity == 5) {
          this._stateStore.store(req, verifier, state, meta, stored);
        } else if (arity == 4) {
          this._stateStore.store(req, state, meta, stored);
        } else if (arity == 3) {
          this._stateStore.store(req, meta, stored);
        } else { // arity == 2
          this._stateStore.store(req, stored);
        }
      } catch (ex) {
        return this.error(ex);
      }
    }
  }
}
 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 일부만 빼오느라 괄호 갯수나 indent 가 안맞을 수 있습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authenticate 에서 가장 큰 분기는 &lt;code&gt;if ((req.query &amp;amp;&amp;amp; req.query.code) || (req.body &amp;amp;&amp;amp; req.body.code)) { ... }&lt;/code&gt; 인데, &lt;code&gt;req.query.code&lt;/code&gt; 나 &lt;code&gt;req.body.code&lt;/code&gt; 값이 있다면 code 값이 조건문에 들어가므로 if문이 실행됩니다. 반대로 둘 다 없다면 undefined 가 되므로 else 로 넘어갑니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-11-03 at 10.28.44 PM.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biRM57/btszHUyQ5NA/v1pzyHIHkEFHM115FVUdg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biRM57/btszHUyQ5NA/v1pzyHIHkEFHM115FVUdg1/img.png&quot; data-alt=&quot;간단테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biRM57/btszHUyQ5NA/v1pzyHIHkEFHM115FVUdg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiRM57%2FbtszHUyQ5NA%2Fv1pzyHIHkEFHM115FVUdg1%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;1242&quot; height=&quot;464&quot; data-filename=&quot;Screenshot 2023-11-03 at 10.28.44 PM.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;464&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;if문 안에서는 code 를 추출한 후 loaded 를 선언해 마지막에는 &lt;code&gt;this._stateStore.verify()&lt;/code&gt; 를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 _stateStore 는 Strategy 를 만들 때 매개변수의 &lt;code&gt;options.store&lt;/code&gt; 값에 따라 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 하나인 &lt;a href=&quot;https://github.com/jaredhanson/passport-oauth2/blob/ea9e99adda82dff67502654347589866fea80eb2/lib/state/pkcesession.js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PCKESessionStore&lt;/a&gt; 에 가보면 &lt;code&gt;verify()&lt;/code&gt; 도 있고 &lt;code&gt;store()&lt;/code&gt; 도 있습니다. 아무튼 code 값이 있으면 &lt;code&gt;verify()&lt;/code&gt; 로직을 호출하는 것 같네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code 값이 존재하지 않는 else 에서는 &lt;code&gt;self.redirect(location)&lt;/code&gt; 를 호출하게 됩니다. location 은 잘 찾아보면 Strategy의 &lt;code&gt;options.autorizationURL&lt;/code&gt; 을 가공한 값인데, autorizationURL 은 passport 의 Strategy 마다 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 passport-google-oauth2 의 값은 &lt;code&gt;options.authorizationURL=options.authorizationURL || 'https://accounts.google.com/o/oauth2/v2/auth';&lt;/code&gt;이고, passport-42 의 값은 &lt;code&gt;options.authorizationURL=options.authorizationURL || 'https://api.intra.42.fr/oauth/authorize';&lt;/code&gt; 입니다. 즉 대상 인증서버의 로그인 프롬프트 url 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오.. 어디서 많이 봤습니다.......&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어라? 익숙한데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oauth 로그인을 테스트 하다보면 자주 보는 형태가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-12 at 3.46.17 PM.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv91rc/btszLpxdF98/67K7g0v5zt7OVCWg3JOf90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv91rc/btszLpxdF98/67K7g0v5zt7OVCWg3JOf90/img.png&quot; data-alt=&quot;42login callback 의 request 객체&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv91rc/btszLpxdF98/67K7g0v5zt7OVCWg3JOf90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv91rc%2FbtszLpxdF98%2F67K7g0v5zt7OVCWg3JOf90%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;1026&quot; height=&quot;180&quot; data-filename=&quot;Screenshot 2023-08-12 at 3.46.17 PM.png&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;42login callback 의 request 객체&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;인증 서버에서 유저가 로그인을 완료하면 Strategy option 으로 지정한 callbackURL 으로 요청이 오게 되는데 이 때 query string 으로 code 값이 붙어옵니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;passport.authenticate()&lt;/code&gt; 에서 추출되는 code 값이 이 값과 동일해 보입니다.&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-11-03 at 11.48.01 PM.png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s6TXM/btszHSgJ66n/FH3zQziqsksKvrxwgYuytk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s6TXM/btszHSgJ66n/FH3zQziqsksKvrxwgYuytk/img.png&quot; data-alt=&quot;출처: 근-본 RFC 6749&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s6TXM/btszHSgJ66n/FH3zQziqsksKvrxwgYuytk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs6TXM%2FbtszHSgJ66n%2FFH3zQziqsksKvrxwgYuytk%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;1274&quot; height=&quot;1134&quot; data-filename=&quot;Screenshot 2023-11-03 at 11.48.01 PM.png&quot; data-origin-width=&quot;1274&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 근-본 RFC 6749&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 verity() 내부가 D~E 에 해당하는 것 같습니다.&lt;/p&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.passportjs.org/concepts/oauth2/authorization/&quot;&gt;https://www.passportjs.org/concepts/oauth2/authorization/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699023154824&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;Understanding OAuth 2.0: Authorization&quot; data-og-description=&quot;Requesting Authorization The OAuth 2.0 protocol flow begins when the application requests authorization from the user. The authorization request is typically triggered based on the user taking an action. In the case of Facebook, such an action would be the&quot; data-og-host=&quot;www.passportjs.org&quot; data-og-source-url=&quot;https://www.passportjs.org/concepts/oauth2/authorization/&quot; data-og-url=&quot;https://www.passportjs.org/concepts/oauth2/authorization/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dMlBZU/hyUnVwShAX/NnQL6FXBlEFFaQod530oHk/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/l1Eea/hyUnTsiuqk/X7OonVnKST4FdhAvBnMkx1/img.png?width=3010&amp;amp;height=1612&amp;amp;face=0_0_3010_1612&quot;&gt;&lt;a href=&quot;https://www.passportjs.org/concepts/oauth2/authorization/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.passportjs.org/concepts/oauth2/authorization/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dMlBZU/hyUnVwShAX/NnQL6FXBlEFFaQod530oHk/img.png?width=2400&amp;amp;height=1260&amp;amp;face=0_0_2400_1260,https://scrap.kakaocdn.net/dn/l1Eea/hyUnTsiuqk/X7OonVnKST4FdhAvBnMkx1/img.png?width=3010&amp;amp;height=1612&amp;amp;face=0_0_3010_1612');&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;Understanding OAuth 2.0: Authorization&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Requesting Authorization The OAuth 2.0 protocol flow begins when the application requests authorization from the user. The authorization request is typically triggered based on the user taking an action. In the case of Facebook, such an action would be the&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.passportjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 작성하다 발견한 passport 의 oauth2 문서가 생각보다 친절합니다.....!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;passport 문서와 oauth rfc 를 참고했을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 서버로 redirect -&amp;gt; 유저의 인증 -&amp;gt; code 를 어플리케이션에 반환 -&amp;gt; code 로 다시 인증 서버에 token 요청 -&amp;gt; 토큰 발급 -&amp;gt; 발급한 토큰으로 필요한 정보 요청 (profile 에 들어오는 정보들) 까지 passport 로 간편하게 처리할 수 있다는 것을 알 수 있었습니다.&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;Screenshot 2023-11-04 at 12.29.28 AM.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw9TwU/btszKCw6t6E/mZ5xjQgkdDVC4TOOXaQYV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw9TwU/btszKCw6t6E/mZ5xjQgkdDVC4TOOXaQYV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw9TwU/btszKCw6t6E/mZ5xjQgkdDVC4TOOXaQYV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw9TwU%2FbtszKCw6t6E%2FmZ5xjQgkdDVC4TOOXaQYV1%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;540&quot; height=&quot;1268&quot; data-filename=&quot;Screenshot 2023-11-04 at 12.29.28 AM.png&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 맨 처음 redirect 되는 로그인 프롬프트의 url 을 살펴보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-11-03 at 11.30.20 PM.png&quot; data-origin-width=&quot;1548&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o6BQW/btszHu1rHYO/IVENVRZKYvDGmKW8ryJk7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o6BQW/btszHu1rHYO/IVENVRZKYvDGmKW8ryJk7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o6BQW/btszHu1rHYO/IVENVRZKYvDGmKW8ryJk7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo6BQW%2FbtszHu1rHYO%2FIVENVRZKYvDGmKW8ryJk7K%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;1548&quot; height=&quot;258&quot; data-filename=&quot;Screenshot 2023-11-03 at 11.30.20 PM.png&quot; data-origin-width=&quot;1548&quot; data-origin-height=&quot;258&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;아까 else 문에서 redirect했던 주소와 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 하나로 처리 되나 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;express 와 passport 로 간단한 앱을 하나 만들어 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;express init 후 &lt;code&gt;index.route&lt;/code&gt; 에 다음 코드를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699025613072&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var express = require('express');
var router = express.Router();
var passport = require('passport');
var GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(
  new GoogleStrategy(
    {
      clientID: ... ,
      clientSecret: ... ,
      callbackURL: 'http://localhost:3000/api/v1/auth/callback/google',
    },
    function (accessToken, refreshToken, profile, cb) {
      console.log(profile);
      return cb(null, profile);
    }
  )
);

router.get(
  '/api/v1/auth/callback/google/',
  passport.authenticate('google', { scope: ['profile'] }, () =&amp;gt; {
    console.log('callback');
  })
);

module.exports = router;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전에 사용하던 프로젝트를 재사용해서 URL이 깁니다ㅎㅎ... 다시 설정하기 귀찮아서 그만.....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 path 로 요청 결과 예상대로 리다이렉션 -&amp;gt; 콜백 모두 한 route 에서 이루어 지는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-11-04 at 12.52.04 AM.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6itP2/btszJHMt3oL/JqP6B9UM2zIN2LKLfTwme0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6itP2/btszJHMt3oL/JqP6B9UM2zIN2LKLfTwme0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6itP2/btszJHMt3oL/JqP6B9UM2zIN2LKLfTwme0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6itP2%2FbtszJHMt3oL%2FJqP6B9UM2zIN2LKLfTwme0%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;534&quot; height=&quot;615&quot; data-filename=&quot;Screenshot 2023-11-04 at 12.52.04 AM.png&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;776&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;code 로만 구분이 되는지 테스트 하기 위해 이번에는 다른 Nest 앱에서 @UseGuard(AuthGuard) 가 붙는 route 에 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;curl 로 요청을 날려보았습니다.&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;1. code가 없는 get 요청&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Screenshot 2023-08-16 at 5.32.06 PM.png&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epNyAT/btszJKJb23M/QNViSAhZDB3k04CPAFxN7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epNyAT/btszJKJb23M/QNViSAhZDB3k04CPAFxN7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epNyAT/btszJKJb23M/QNViSAhZDB3k04CPAFxN7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepNyAT%2FbtszJKJb23M%2FQNViSAhZDB3k04CPAFxN7k%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;1556&quot; height=&quot;802&quot; data-filename=&quot;edited_Screenshot 2023-08-16 at 5.32.06 PM.png&quot; data-origin-width=&quot;1556&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 서버로 리다이렉션 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. code가 있으면서 유효하지 않은 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부러 code 를 아무값이나 줘봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-08-16 at 5.31.56 PM.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsWFK9/btszLurNT9C/QKabup1K93CiSkXgZDKIck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsWFK9/btszLurNT9C/QKabup1K93CiSkXgZDKIck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsWFK9/btszLurNT9C/QKabup1K93CiSkXgZDKIck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsWFK9%2FbtszLurNT9C%2FQKabup1K93CiSkXgZDKIck%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;1462&quot; height=&quot;824&quot; data-filename=&quot;Screenshot 2023-08-16 at 5.31.56 PM.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;500 에러의 response 가 옵니다. code 값을 검증하러 갔다가 에러가 난 모양입니다.&lt;/p&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://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1699027726275&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;RFC 6749: The OAuth 2.0 Authorization Framework&quot; data-og-description=&quot;The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowi&quot; data-og-host=&quot;datatracker.ietf.org&quot; data-og-source-url=&quot;https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2&quot; data-og-url=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nVpHB/hyUrrHEsjl/77XpJttmZTw65TkOsDkTqK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nVpHB/hyUrrHEsjl/77XpJttmZTw65TkOsDkTqK/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;RFC 6749: The OAuth 2.0 Authorization Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;datatracker.ietf.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0 RFC 에서 인증 서버의 응답 중 code가 필수였기 때문에 code의 유무로 클라이언트의 인증 요청인지, 인증 서버의 code 포함 응답인지가 구분될 수 있던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개를 구분짓지 않아도 passport 가 잘 구분지어서 요청해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 예시 코드도 나뉘어 있고 세상 많은 사람들이 둘을 구분지어 쓰기 때문에 실제로는 구분하여 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;passport 가 많은 처리를 해주는 편한 라이브러리라는 것이 잘 느껴지는 여정(?)이었습니다. 정보성 글이라기보다는 호기심 해결을 위한 글이지만... 재밌었으면 된 거 아닐까요? 전 재밌었습니다.....!  &lt;/p&gt;</description>
      <category>개발</category>
      <category>GhostPong</category>
      <category>OAuth</category>
      <category>OAuth2.0</category>
      <category>passport.js</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/12</guid>
      <comments>https://jisu-log.tistory.com/12#entry12comment</comments>
      <pubDate>Sat, 4 Nov 2023 01:28:03 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] 파일 업로드 구현하다 FileInterceptor 를 커스텀 한 사람이 있다?</title>
      <link>https://jisu-log.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Ghostpong 에서는 유저의 프로필 사진 업로드 기능을 지원하고 있습니다. 서버에 직접 static file 들을 업로드하고 serving 하는 기능을 구현했던 과정을 소개합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. serve static&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/recipes/serve-static&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.nestjs.com/recipes/serve-static&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695280114312&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;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/recipes/serve-static&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vw2kM/hyTY0R1uQi/kK2WSK3K95Fn2fvxoGeX2K/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/recipes/serve-static&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/recipes/serve-static&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vw2kM/hyTY0R1uQi/kK2WSK3K95Fn2fvxoGeX2K/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest 문서가 너무 잘 되어있어서 5초만에 따라할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695280750299&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add @nestjs/serve-static&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 설치 후 &lt;code&gt;AppModule&lt;/code&gt; 에 &lt;code&gt;ServeStaticModule&lt;/code&gt; 을 설정해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app.module.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1695280834728&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module({
  imports: [
	...
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', '..', '..', 'public'),
      renderPath: '/img',
      serveStaticOptions: { index: false, redirect: false },
    }),
    ...
  ],
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nestjs/serve-static/blob/master/lib/interfaces/serve-static-options.interface.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/nestjs/serve-static/blob/master/lib/interfaces/serve-static-options.interface.ts&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 옵션을 참고하여 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static file 이 이미지 파일 뿐이었기 때문에 directory 로 요청이 오면 &lt;code&gt;index.html&lt;/code&gt; 을 보여주는 &lt;code&gt;index&lt;/code&gt; 와 path 가 directory 이면 '/' 을 붙여주는 &lt;code&gt;redirect&lt;/code&gt; 설정은 &lt;code&gt;false&lt;/code&gt; 로 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. file upload&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/techniques/file-upload#basic-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.nestjs.com/techniques/file-upload#basic-example&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695282564555&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;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/techniques/file-upload#basic-example&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bd66R4/hyTY9VMx8k/8nukczdrEot8ntK23mOqY0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/techniques/file-upload#basic-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/techniques/file-upload#basic-example&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bd66R4/hyTY9VMx8k/8nukczdrEot8ntK23mOqY0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest 는 express 패키지인 multer 를 사용해서 &lt;code&gt;multipart/form-data&lt;/code&gt; 로 오는 파일을 처리합니다. multer 는 &lt;code&gt;multipart/form-data&lt;/code&gt; 에서 지원하지 않는 형식의 데이터는 처리할 수 없습니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;왜 multipart/form-data 밖에 안되는가?&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 업로드하려면 request body 에 string 으로 담아줘야하는데 이거 format 이 &lt;code&gt;multipart/formdata&lt;/code&gt; 임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7578.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.rfc-editor.org/rfc/rfc7578.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695282895187&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;RFC 7578: Returning Values from Forms: multipart/form-data&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.rfc-editor.org&quot; data-og-source-url=&quot;https://www.rfc-editor.org/rfc/rfc7578.html&quot; data-og-url=&quot;https://www.rfc-editor.org/rfc/rfc7578.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7578.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.rfc-editor.org/rfc/rfc7578.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;RFC 7578: Returning Values from Forms: multipart/form-data&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.rfc-editor.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 파일을 업로드하려면 &lt;code&gt;FileInterceptor()&lt;/code&gt; 인터셉터를 라우트 핸들러에 연결하고 &lt;code&gt;@UploadedFile()&lt;/code&gt; 데코레이터를 사용하여 request에서 파일을 추출하기만 하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695283687523&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('image')
@UseInterceptors(FileInterceptor('image'))
uploadImage(@UploadedFile() file: Express.Multer.File) {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;@UploadedFile&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우트 핸들러 매개변수 데코레이터로 file object 를 추출하고 데코레이터가 달린 매개변수에 추출한 파일을 넣어줍니다. Express 기반 애플리케이션을 위한 다중 미들웨어와 함께 사용됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FileInterceptor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 인자를 받습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fieldName: HTML form 에서 파일을 hold 할 이름 -&amp;gt; client 에서 &lt;code&gt;multipart/form-data&lt;/code&gt; 형태로 파일을 보내줄 때 name 을 해당 인자로 설정해줘야 합니다.&lt;/li&gt;
&lt;li&gt;options : MulterOptions 타입의 옵셔널 오브젝트. multer constructor 에서 &lt;a href=&quot;https://github.com/expressjs/multer#multeropts&quot;&gt;쓰이는 것&lt;/a&gt;과 동일함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의 : 파이어베이스같은 써드파티와 호환이 안될 수도 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;File validation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 크기나 mime type 을 체크하는 유효성 검사를 위해서는 pipe 를 만들어서 &lt;code&gt;UploadedFile&lt;/code&gt; 데코레이터에 바인딩하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@nestjs/common&lt;/code&gt; 에서 file validation 을 위한 내장 파이프 (ParseFilePipe) 도 제공하고 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ParseFilePipe&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;validators 인자에 &lt;code&gt;FileValidator&lt;/code&gt; 타입의 validator 를 여러 개 설정해서 사용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695285324885&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Post('file')
uploadFileAndPassValidation(
  @Body() body: SampleDto,
  @UploadedFile(
    new ParseFilePipe({
      validators: [
        // ... Set of file validator instances here
      ]
    })
  )
  file: Express.Multer.File,
) { ... };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;validators 외에 다음 추가 옵션도 설정할 수 있습니다. (optional)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;errorHttpStatusCode&lt;/code&gt; : 모든 validator fail 시 throw 될 HTTP 상태코드 (기본 400)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exceptionFactory&lt;/code&gt; : 에러메세지를 만들고 에러 리턴할 Factory&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FileValidator 는 커스텀으로 만들 수도 있고, 빌트인으로 제공되는 것을 사용해도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀하려면 다음 형태로 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export abstract class FileValidator&amp;lt;TValidationOptions = Record&amp;lt;string, any&amp;gt;&amp;gt; {
  constructor(protected readonly validationOptions: TValidationOptions) {}

  /**
   * Indicates if this file should be considered valid, according to the options passed in the constructor.
   * @param file the file from the request object
   */
  abstract isValid(file?: any): boolean | Promise&amp;lt;boolean&amp;gt;;

  /**
   * Builds an error message in case the validation fails.
   * @param file the file from the request object
   */
  abstract buildErrorMessage(file: any): string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isValid 에서는 비동기 validation 도 지원합니다. file 을 Express.Multer.File 타입으로 지정해서 타입안정성도 보장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌트인으로 제공되는 FileValidator 는 두 가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MaxFileSizeValidator&lt;/code&gt; : 파일 사이즈가 주어진 값 (byte) 보다 작은지 체크&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileTypeValidator&lt;/code&gt; : 주어진 값과 매치되는 마임타입인지 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File validation 을 ParseFilePipe 로 하는 경우 다음과 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;@UploadedFile(
  new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: MAX_IMAGE_SIZE }),
      new FileTypeValidator({ fileType: /image\\/(png|jpeg|gif)/ }),
    ],
  }),
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAX_IMAGE_SIZE 보다 큰 파일이나 image/png, image/jpeg, image/gif 형태가 아니면 400 에러코드를 리턴합니다.&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;ParseFilePIpeBuilder&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;builder 를 이용해서 validator 객체를 생성하지 않고 설정할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695288655833&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UploadedFile(
  new ParseFilePipeBuilder()
    .addFileTypeValidator({
      fileType: 'jpeg',
    })
    .addMaxSizeValidator({
      maxSize: 1000
    })
    .build({
      errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
    }),
)
file: Express.Multer.File,&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 단일 파일 처리만 구현했지만 다중 파일 업로드도 가능하니 공식 문서를 참조하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/techniques/file-upload#array-of-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.nestjs.com/techniques/file-upload#array-of-files&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1695288361597&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;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/techniques/file-upload#array-of-files&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/j3Hsw/hyTY7Re01c/Cnfm1c0dcLfWDKjWMXKpe1/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/techniques/file-upload#array-of-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/techniques/file-upload#array-of-files&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/j3Hsw/hyTY7Re01c/Cnfm1c0dcLfWDKjWMXKpe1/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Validation 하는 위치에 대한 고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드할 파일의 limit, MIME type 을 체크해야 했는데, option을 넘겨서 multer 에서 직접할 수도 있고 nest의 &lt;code&gt;ParseFilePipe&lt;/code&gt; 에서도 할 수 있었기 때문에 고민이 되었습니다. pipe 의 역할을 생각해보면 pipe가 맞았지만 multer 기능을 (&lt;code&gt;FileInterceptor&lt;/code&gt;) 놔두고 굳이 인터셉터보다 뒤에 실행되는 파이프에서...? 하는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민하던 와중 확실히 인터셉터로 정한 계기가 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 저장 api 에서 사용하던 &lt;code&gt;FileInterceptor&lt;/code&gt; 의 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1696753181725&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UseInterceptors(
  FileInterceptor('image', {
    fileFilter: (req, file, cb) =&amp;gt; {
      if (!file.mimetype.match(/image\/(gif|jpeg|png)/)) {
        cb(new UnsupportedMediaTypeException('gif, jpeg, png 형식의 파일만 업로드 가능합니다.'), false);
      }
      if (file.size &amp;gt; MAX_IMAGE_SIZE) {
        cb(new PayloadTooLargeException('이미지 파일은 4MB 이하로 업로드 가능합니다.'), false);
      }
      cb(null, true);
    },
    storage: diskStorage({
      destination: 'public/asset',
      filename: (req, file, cb) =&amp;gt; {
        // FIXME : auth guard 적용 후 req.user.id로 변경
        const myId = req.headers['x-my-id'];
        const extArray = file.mimetype.split('/');
        cb(null, 'profile-' + myId + '.' + extArray[extArray.length - 1]);
      },
     }),
  }),
 )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;storage 옵션을 통해 파일을 저장하는데 만약 fileFilter 에서 적용하는 파일 validation 부분이 &lt;code&gt;ParseFilePipe&lt;/code&gt; 에서 적용된다면 nest 의 생명 주기에 따라 전위 인터셉터 -&amp;gt; 파이프 순으로 실행되기 때문에 파일이 먼저 생성되고 validation 이 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 따로 pipe 에서 unlink 를 실행해야 합니다. 정말..... 아름답지 않습니다....................&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 데이터 상에 multer 에서 읽은 file 을 저장해놨다가 pipe 에서 save 하는 방법도 있을 듯 합니다만 이것도....아름답지 않습니다.............&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트로 Ghostpong 에서 지원하지 않는 sql 형식의 파일을 업로드 해 봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-04-23 at 11.25.27 AM.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CiiXE/btsxrk6Lyca/H2pDXe0kCrSaryG10kDlQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CiiXE/btsxrk6Lyca/H2pDXe0kCrSaryG10kDlQ0/img.png&quot; data-alt=&quot;확장자가 octet-stream?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CiiXE/btsxrk6Lyca/H2pDXe0kCrSaryG10kDlQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCiiXE%2Fbtsxrk6Lyca%2FH2pDXe0kCrSaryG10kDlQ0%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;1546&quot; height=&quot;832&quot; data-filename=&quot;Screenshot 2023-04-23 at 11.25.27 AM.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;확장자가 octet-stream?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와! 정말 생각한대로 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 pipe 는 버리고 &lt;code&gt;FileInterceptor&lt;/code&gt; 로만 구현하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재앙의 시작&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_Screenshot 2023-04-23 at 6.48.33 PM.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boUzjy/btsxrYWUWvK/eLQyUGUiggA3ev2k5RSPmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boUzjy/btsxrYWUWvK/eLQyUGUiggA3ev2k5RSPmk/img.png&quot; data-alt=&quot; &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boUzjy/btsxrYWUWvK/eLQyUGUiggA3ev2k5RSPmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboUzjy%2FbtsxrYWUWvK%2FeLQyUGUiggA3ev2k5RSPmk%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;667&quot; height=&quot;548&quot; data-filename=&quot;edited_edited_Screenshot 2023-04-23 at 6.48.33 PM.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;548&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;controller 에 decorator 로 너무 많은 코드가 들어가버린 나머지 동료분이 분리를 요청해주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 썩 마음에 들지 않았기 때문에 &lt;s&gt;아름답지 않습니다&lt;/s&gt; 리팩토링을 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분리를 위해 &lt;code&gt;FileInterceptor&lt;/code&gt; 코드를 따로 작성하여 제가 직접 multer 를 연결하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FileInterceptor&lt;/code&gt; 는 사실상 multer 를 nest 의 Interceptor 로 감싼 형태기 때문에 리팩토링이 쉽진 않았습니다. 이 글은 10월에 적고 있지만 4월에 겪은 고난이라 힘들었다는 것 밖에 기억이 안나는 게 아쉽습니다. 좀 더 생생하게 기록해둬야 했는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 지금 생각해보니까 그냥 &lt;code&gt;FileInterceptor&lt;/code&gt; 를 상속받아서 옵션만 넘기거나 했어도 될 것 같네요.. 아니면 multer 옵션만 다른 파일에 저장하는 방법도 있었을 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;FileInterceptor&lt;/code&gt; 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest 에서 제공하는 &lt;code&gt;FileInterceptor&lt;/code&gt; 코드입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1696754293603&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function FileInterceptor(
  fieldName: string,
  localOptions?: MulterOptions,
): Type&amp;lt;NestInterceptor&amp;gt; {
  class MixinInterceptor implements NestInterceptor {
    protected multer: MulterInstance;

    constructor(
      @Optional()
      @Inject(MULTER_MODULE_OPTIONS)
      options: MulterModuleOptions = {},
    ) {
      this.multer = (multer as any)({
        ...options,
        ...localOptions,
      });
    }

    async intercept(
      context: ExecutionContext,
      next: CallHandler,
    ): Promise&amp;lt;Observable&amp;lt;any&amp;gt;&amp;gt; {
      const ctx = context.switchToHttp();

      await new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt;
        this.multer.single(fieldName)(
          ctx.getRequest(),
          ctx.getResponse(),
          (err: any) =&amp;gt; {
            if (err) {
              const error = transformException(err);
              return reject(error);
            }
            resolve();
          },
        ),
      );
      return next.handle();
    }
  }
  const Interceptor = mixin(MixinInterceptor);
  return Interceptor;
}&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;rxjs 가 마구 쓰여서 겁나 어렵습니다.&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;code&gt;intercept()&lt;/code&gt; 로 &lt;code&gt;this.multer.single()&lt;/code&gt; 에서 파일을 읽어와 Promise를 생성해줍니다. 파일 I/O 는 동기 처리가 되어야 하므로 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;intercept()&lt;/code&gt; 의 끝에서 &lt;code&gt;next.handle()&lt;/code&gt; 을 해주는데 이걸 해줘야 이후에 실행되어야 할 route handler 를 부를 수 있습니다. Nest 의 Interceptor 문서와 rxjs 의 Observable 문서를 보면 조금 이해할 수 있습니다.&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;code&gt;FileUploadInterceptor&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1696754158553&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  UnsupportedMediaTypeException,
  PayloadTooLargeException,
  CallHandler,
} from '@nestjs/common';
import { HttpArgumentsHost } from '@nestjs/common/interfaces';
import * as multer from 'multer';
import { FileFilterCallback, diskStorage } from 'multer';
import { Observable } from 'rxjs';

import { MAX_IMAGE_SIZE } from '../../common/constant';

@Injectable()
export class FileUploadInterceptor implements NestInterceptor&amp;lt;void, void&amp;gt; {
  async intercept(context: ExecutionContext, next: CallHandler): Promise&amp;lt;Observable&amp;lt;void&amp;gt;&amp;gt; {
    const ctx: HttpArgumentsHost = context.switchToHttp();

    const multerOptions: multer.Options = {
      fileFilter: (_req, file: Express.Multer.File, cb: FileFilterCallback) =&amp;gt; {
        if (!file.mimetype.match(/image\/(gif|jpeg|png)/)) {
          cb(new UnsupportedMediaTypeException('gif, jpeg, png 형식의 파일만 업로드 가능합니다.'));
        }
        cb(null, true);
      },
      storage: diskStorage({
        destination: 'public/img',
        filename: (req, file, cb) =&amp;gt; {
          const myId = req.headers['x-my-id'];
          const extArray = file.mimetype.split('/');
          cb(null, 'profile-' + myId + '.' + extArray[extArray.length - 1]);
        },
      }),
      limits: { fileSize: MAX_IMAGE_SIZE },
    };

    await new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt;
      multer(multerOptions).single('image')(ctx.getRequest(), ctx.getResponse(), (error) =&amp;gt; {
        error
          ? reject(
              error.code === 'LIMIT_FILE_SIZE'
                ? new PayloadTooLargeException('이미지 파일은 4MB 이하로 업로드 가능합니다.')
                : error,
            )
          : resolve();
      }),
    );
    return next.handle();
  }
}&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;기존에 fileFilter 에서 size 검사하던 것을 limits 로 변경한 것 이외에는 같은 로직 입니다. file limit 에러를 따로 처리하기 위해 &lt;code&gt;single()&lt;/code&gt; 의 return 값에서 error code 를 따로 체크해줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fileFilter 에서 size 를 체크하게 되면 아무리 큰 파일이어도 다 올라간 후 체크를 해야 해서 limits 로 업로드 시점에 (파일 객체가 생성되기 전에) 체크를 해주는 것이 성능상 이점이 있어 채택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 부분을 참고하며 핵심 기능만 추가했습니다. 하지만 재사용이 어려워 보여서 multer option 만 따로 빼는 등의 선택이 나았을 것 같네요..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multer 에 대해 깊이 파보고 rxjs 도 살짝 찍먹해봐서 좋은 경험이긴 했지만 아쉬운 부분입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드가 적용된 pr 을 보고 싶다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/GhostPangPang/GhostPong/pull/122&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/GhostPangPang/GhostPong/pull/122&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1696755412497&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;✨ [Feature] image upload api &amp;amp; serve static by jis-kim &amp;middot; Pull Request #122 &amp;middot; GhostPangPang/GhostPong&quot; data-og-description=&quot;Summary image 를 POST /user/image 로 업로드하는 기능 업로드 시 backend/public/asset 에 &amp;quot;profile-${userId}.${ext}&amp;quot; 형태로 저장되어 유저당 최대 3개 저장됨 backend/public/asset 에 있는 이미지 파일을 get 할 수 있게 se&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/GhostPangPang/GhostPong/pull/122&quot; data-og-url=&quot;https://github.com/GhostPangPang/GhostPong/pull/122&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ohoG8/hyT5YucNBX/Zblk3d2ImeF1VdGJbd3DqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/GhostPangPang/GhostPong/pull/122&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/GhostPangPang/GhostPong/pull/122&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ohoG8/hyT5YucNBX/Zblk3d2ImeF1VdGJbd3DqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;✨ [Feature] image upload api &amp;amp; serve static by jis-kim &amp;middot; Pull Request #122 &amp;middot; GhostPangPang/GhostPong&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary image 를 POST /user/image 로 업로드하는 기능 업로드 시 backend/public/asset 에 &quot;profile-${userId}.${ext}&quot; 형태로 저장되어 유저당 최대 3개 저장됨 backend/public/asset 에 있는 이미지 파일을 get 할 수 있게 se&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <category>FileInterceptor</category>
      <category>GhostPong</category>
      <category>multer</category>
      <category>nestjs</category>
      <category>TypeScript</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/11</guid>
      <comments>https://jisu-log.tistory.com/11#entry11comment</comments>
      <pubDate>Sun, 8 Oct 2023 18:00:36 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] 프로젝트에서 사용한 swagger 태그</title>
      <link>https://jisu-log.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/GhostPangPang/GhostPong&quot;&gt;프로젝트&lt;/a&gt; 진행 중 작성한 wiki 를 옮겨와 편집했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들 고생하지 말라고 필수적인 태그만 정리한 거라 부족한 점이 많습니다.&lt;br /&gt;Nest 와 Swagger 공식문서를 보면서 태그 쇼핑하시는 것을 추천합니다.&lt;br /&gt;하지만 제가 프로젝트 시작할 때는 다른 사람들이 대체 무슨 태그를 쓰는 지 궁금했어서 기록용으로 남깁니다...ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ApiOperation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Api에 대한 설명을 적는 태그입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;@ApiOperation({ summary: '친구 신청하기 (닉네임)' })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 스크린샷&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-12_at_5.01.20_PM.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt52ai/btst5l2fVq0/NKU5K1v3ekFotW0yrAzGnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt52ai/btst5l2fVq0/NKU5K1v3ekFotW0yrAzGnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt52ai/btst5l2fVq0/NKU5K1v3ekFotW0yrAzGnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt52ai%2Fbtst5l2fVq0%2FNKU5K1v3ekFotW0yrAzGnK%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;714&quot; height=&quot;130&quot; data-filename=&quot;Screenshot_2023-04-12_at_5.01.20_PM.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Path 옆에 summary 의 내용이 나타납니다.&lt;br /&gt;메소드 위에 주석 (&lt;code&gt;/**/&lt;/code&gt;) 을 쓰면 더 상세한 표기가 가능합니다. (&lt;a href=&quot;https://jisu-log.tistory.com/9#CLI%20%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nest 의 swagger plugin 을 적용했을 경우&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Api{StatusCode}Response&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;status code 는 &quot;NotFound&quot;(404) 나 &quot;BadRequest&quot;(400) 등의 형태로 들어갑니다.&lt;br /&gt;저는 에러 코드 응답 중에서도 서버 로직에서 나는 에러는 제외하고 클라이언트에서 날 수 있는 것만 표시했습니다.&lt;br /&gt;모든 경우의 수를 다 적으면 지저분해지기 때문에 어느정도만 쓰시는 것을 추천합니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;@ApiConflictResponse({
  type: ErrorResponseDto,
  description: '신청 정원 초과, 이미 친구 상태, 이미 친구 신청 상태, 자기 자신에게 신청',
})
@ApiNotFoundResponse({ type: ErrorResponseDto, description: '유저 없음' })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 스크린샷&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-13_at_4.06.37_PM.png&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpibua/btstY5Tf8mM/Phw7NS9acNu0uhgGyPR1JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpibua/btstY5Tf8mM/Phw7NS9acNu0uhgGyPR1JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpibua/btstY5Tf8mM/Phw7NS9acNu0uhgGyPR1JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpibua%2FbtstY5Tf8mM%2FPhw7NS9acNu0uhgGyPR1JK%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;2298&quot; height=&quot;1164&quot; data-filename=&quot;Screenshot_2023-04-13_at_4.06.37_PM.png&quot; data-origin-width=&quot;2298&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Status code 별로 다 있습니다.&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;APIQuery, ApiHeaders, ApiParam&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 에 header, param, querystring이 필요해!!!! 할 때 사용합니다.&lt;br /&gt;제가 진행했던 프로젝트에서는 로그인 기능 적용 전 임시 헤더로 user id 를 주고받아서 모든 api에 &lt;code&gt;@ApiHeaders&lt;/code&gt; 를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;@ApiQuery({ name: 'nickname', description: '친구 신청할 유저의 닉네임' })
@ApiHeaders([{ name: 'x-my-id', description: '내 아이디 (임시값)' }])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 스크린샷&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.54.36_PM.png&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8QAO4/btst5kh5wQ2/XvTBMPTXMJjqvgvnnTKFpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8QAO4/btst5kh5wQ2/XvTBMPTXMJjqvgvnnTKFpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8QAO4/btst5kh5wQ2/XvTBMPTXMJjqvgvnnTKFpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8QAO4%2Fbtst5kh5wQ2%2FXvTBMPTXMJjqvgvnnTKFpK%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;1122&quot; height=&quot;756&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.54.36_PM.png&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;@ApiParam({ name: 'userId', description: '친구 신청할 유저의 아이디' })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 스크린샷&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.56.36_PM.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5D2jn/btst6mmavpn/eAdUSSkQEU11tHlykd52GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5D2jn/btst6mmavpn/eAdUSSkQEU11tHlykd52GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5D2jn/btst6mmavpn/eAdUSSkQEU11tHlykd52GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5D2jn%2Fbtst6mmavpn%2FeAdUSSkQEU11tHlykd52GK%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;1156&quot; height=&quot;438&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.56.36_PM.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 태그들을 달면 테스트 시 값 입력을 요구하는 데, &lt;code&gt;required: false&lt;/code&gt; 를 추가하면 값을 넣지 않아도 테스트 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DTO 는 어쩌죠?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; (&lt;a href=&quot;https://jisu-log.tistory.com/9#CLI%20%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nest 의 swagger plugin 을 적용했을 경우&lt;/a&gt;) 파일명이 &lt;code&gt;.dto.ts&lt;/code&gt; 이면 알아서 detect 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석으로 상세 설명이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export class SuccessResponseDto {
  constructor(msg: string) {
    this.message = msg;
  }
  /**
   * 성공 응답 메세지
   * @example '요청이 처리되었습니다.'
   */
  message: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로퍼티 위 주석 정보를 읽어서 swagger 에 심어줍니다.&lt;br /&gt;&lt;code&gt;@example&lt;/code&gt; 로 예시를 넣을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 스크린샷&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.52.15_PM.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EVhcW/btstXxbESXc/aeFliKw6amZiIvkB565Gn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EVhcW/btstXxbESXc/aeFliKw6amZiIvkB565Gn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EVhcW/btstXxbESXc/aeFliKw6amZiIvkB565Gn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEVhcW%2FbtstXxbESXc%2FaeFliKw6amZiIvkB565Gn1%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;990&quot; height=&quot;330&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.52.15_PM.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;330&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;api 응답에서도 리턴되는 dto type 의 @example 값을 읽어 example value 로 넣어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.52.53_PM.png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIt6c9/btst0dp3WWz/RUEnGQSjsgd1PKrKo97rIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIt6c9/btst0dp3WWz/RUEnGQSjsgd1PKrKo97rIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIt6c9/btst0dp3WWz/RUEnGQSjsgd1PKrKo97rIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIt6c9%2Fbtst0dp3WWz%2FRUEnGQSjsgd1PKrKo97rIK%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;892&quot; height=&quot;464&quot; data-filename=&quot;Screenshot_2023-04-13_at_7.52.53_PM.png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzHvzt/btst6oK1HfS/eDj2XChb5nQjnii7ynUe9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzHvzt/btst6oK1HfS/eDj2XChb5nQjnii7ynUe9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzHvzt/btst6oK1HfS/eDj2XChb5nQjnii7ynUe9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzHvzt%2Fbtst6oK1HfS%2FeDj2XChb5nQjnii7ynUe9K%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;642&quot; height=&quot;438&quot; data-filename=&quot;Untitled.png&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object literal 의 배열은 인식하지 못합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2023-04-27_at_12.39.31_PM.png&quot; data-origin-width=&quot;2276&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8nJ1X/btst89GBFP2/l82PBHaMJytxev8SkKq0I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8nJ1X/btst89GBFP2/l82PBHaMJytxev8SkKq0I0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8nJ1X/btst89GBFP2/l82PBHaMJytxev8SkKq0I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8nJ1X%2Fbtst89GBFP2%2Fl82PBHaMJytxev8SkKq0I0%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;2276&quot; height=&quot;738&quot; data-filename=&quot;Screenshot_2023-04-27_at_12.39.31_PM.png&quot; data-origin-width=&quot;2276&quot; data-origin-height=&quot;738&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;뭔가 참조 문제가 나는데 swagger cli 에서 나는 문제라 디버깅이 어려워서 그냥 타입을 따로 선언하고 &lt;code&gt;type[]&lt;/code&gt; 하는 식으로 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 어떻게 썼는데?&lt;/h2&gt;
&lt;pre id=&quot;code_1694747256856&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @ApiOperation({ summary: '로그인 2단계 인증' })
  @ApiHeaders([{ name: 'x-my-id', description: '내 아이디 (임시값)' }])
  @ApiForbiddenResponse({ type: ErrorResponseDto, description: '유효하지 않은 인증 코드' })
  @ApiBadRequestResponse({ type: ErrorResponseDto, description: '잘못된 인증 코드' })
  @SkipUserGuard()
  @UseGuards(TwoFaGuard)
  @HttpCode(HttpStatus.OK)
  @Post('login/2fa')
  async twoFactorAuthLogin(
    @ExtractUserId() myId: number,
    @Body() { code }: CodeVerificationRequestDto,
    @Res() res: Response,
  ): Promise&amp;lt;void&amp;gt; {
	...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-15 at 12.11.47 PM.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NqicG/btst89Nns0F/McssCPCpMBab22nQGYHjDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NqicG/btst89Nns0F/McssCPCpMBab22nQGYHjDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NqicG/btst89Nns0F/McssCPCpMBab22nQGYHjDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNqicG%2Fbtst89Nns0F%2FMcssCPCpMBab22nQGYHjDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1432&quot; height=&quot;1294&quot; data-filename=&quot;Screenshot 2023-09-15 at 12.11.47 PM.png&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많은 예시는 &lt;a href=&quot;https://github.com/GhostPangPang/GhostPong/blob/develop/backend/src/auth/auth.controller.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt; 을 참고해 주세요&lt;/p&gt;</description>
      <category>개발</category>
      <category>GhostPong</category>
      <category>nestjs</category>
      <category>SWAGGER</category>
      <category>TypeScript</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/10</guid>
      <comments>https://jisu-log.tistory.com/10#entry10comment</comments>
      <pubDate>Fri, 15 Sep 2023 12:16:51 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] NestJS 에 swagger 달기</title>
      <link>https://jisu-log.tistory.com/9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r5UMb/btstY2gs03l/5hbR3sLSGiV0nsumDLPT80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r5UMb/btstY2gs03l/5hbR3sLSGiV0nsumDLPT80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r5UMb/btstY2gs03l/5hbR3sLSGiV0nsumDLPT80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr5UMb%2FbtstY2gs03l%2F5hbR3sLSGiV0nsumDLPT80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;405&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/GhostPangPang/GhostPong&quot;&gt;프로젝트&lt;/a&gt; 진행 중 작성한 wiki 를 옮겨와 편집했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;swagger&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAPI Speciation (OAS) 를 활용할 수 있는 도구.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenAPI&lt;/b&gt; 란 RESTful 인터페이스를 정의하기 위한 세계 표준이다. OAS 로 technology-agnostic API 를 설하고 개발, 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Use in nest!&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;yarn add @nestjs/swagger&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest 는 OpenAPI 를 데코레이터로 만들 수 있는 모듈을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DocumentBuilder&lt;/code&gt; 로 OpenAPI 사양을 준수하는 기본 문서를 구축할 수 있다. 전체문서 (all HTTP routes defined) 를 생성하려면 &lt;code&gt;SwaggerModule&lt;/code&gt; 의 &lt;code&gt;createDocument()&lt;/code&gt; 를 사용한다. 순서대로 application, Swagger option, SwaggerDocumentOptions 객체를 인자로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 만들고 &lt;code&gt;setup()&lt;/code&gt; 메소드를 호출할 수 있다. 인자 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;The path to mount the Swagger UI&lt;/li&gt;
&lt;li&gt;An application instance&lt;/li&gt;
&lt;li&gt;The document object instantiated above&lt;/li&gt;
&lt;li&gt;Optional configuration parameter (read more &lt;a href=&quot;https://docs.nestjs.com/openapi/introduction#document-options&quot;&gt;here&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;main.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const config = new DocumentBuilder()
  .setTitle('GhostPong API')
  .setDescription('The GhostPong API description')
  .setVersion('1.0')
  .addTag('GhostPong')
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정 후 start 하면 다음과 같은 페이지가 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BReqO/btstY30KNLm/qgKYfINLHGpAhk1AoVKUjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BReqO/btstY30KNLm/qgKYfINLHGpAhk1AoVKUjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BReqO/btstY30KNLm/qgKYfINLHGpAhk1AoVKUjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBReqO%2FbtstY30KNLm%2FqgKYfINLHGpAhk1AoVKUjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;315&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/api-json&lt;/code&gt;, &lt;code&gt;/api-yaml&lt;/code&gt; 으로 각각 json, yaml 형식으로 데이터를 받을 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CLI 플러그인&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;DTO를 자동으로 detect 해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 DTO 프로퍼티에 &lt;code&gt;@ApiHideProperty&lt;/code&gt;를 사용하지 않는 한 &lt;code&gt;@ApiProperty&lt;/code&gt;로 주석을 달아줌&lt;/li&gt;
&lt;li&gt;물음표에 따라 필요한 프로퍼티를 설정 (e.g. &lt;code&gt;name?: string&lt;/code&gt; will set &lt;code&gt;required: false&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;유형에 따라 &lt;code&gt;type&lt;/code&gt; or &lt;code&gt;enum&lt;/code&gt; 속성을 설정(배열도 지원).&lt;/li&gt;
&lt;li&gt;할당된 기본값에 따라 &lt;code&gt;default&lt;/code&gt; 속성을 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class-validator&lt;/code&gt; 데코레이터를 기반으로 여러 유효성 검사 규칙을 설정(&lt;code&gt;classValidatorShim&lt;/code&gt;이 &lt;code&gt;true&lt;/code&gt;로 설정된 경우).&lt;/li&gt;
&lt;li&gt;적절한 상태와 &lt;code&gt;type&lt;/code&gt;(response model)으로 모든 엔드포인트에 응답 데코레이터를 추가&lt;/li&gt;
&lt;li&gt;주석을 기반으로 속성 및 엔드포인트에 대한 설명을 생성(&lt;code&gt;introspectComments&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;로 설정된 경우).&lt;/li&gt;
&lt;li&gt;설명을 기반으로 속성에 대한 예제 값을 생성(&lt;code&gt;introspectComments&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;로 설정된 경우).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일명은 반드시 &lt;code&gt;.dto.ts&lt;/code&gt;이거나 &lt;code&gt;.entity.ts&lt;/code&gt; 여야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mappte-types 의 utilies 를 사용하는 경우 (ex - PartialType) @nestjs/mapped-type 대신 @nestjs/swagger 에서 가져와야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;temp.dto.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;export class Temp {
  /**
   * The name of Temp
   * @example 'Jule'
   */
  @IsAlphanumeric()
  @MaxLength(8)
  name: string;

  /**
   * The age of Temp
   * @example 42
   */
  @IsNumber()
  @Max(130)
  @Min(0)
  age: number;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인식한 dto 를 swagger 에서 확인할 수 있다.&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHYOFm/btstRtmDutC/yO4GByVFhn4J8IcDKkkL1k/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;api 예시에도 설정한 값이 다 들어가 있다.&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Geu0t/btstMZ0Do4h/97BKdHOoL2hqhHvSgBvTQ0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;plugin 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plugin 을 사용하려면 &lt;code&gt;nest-cli.json&lt;/code&gt; 을 편집한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;&quot;compilerOptions&quot;: {
    ...
  &quot;plugins&quot;: [
    {
      &quot;name&quot;: &quot;@nestjs/swagger/plugin&quot;,
      &quot;options&quot;: {
        &quot;classValidatorShim&quot;: true,
        &quot;introspectComments&quot;: true
      }
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 설정할 Decorators&lt;/h2&gt;
&lt;figure id=&quot;og_1694707975634&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;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/openapi/operations&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKPdQ4/hyTVPixpEH/08Yjuo3OAnXcyzDk32Uok0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/openapi/operations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/openapi/operations&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKPdQ4/hyTVPixpEH/08Yjuo3OAnXcyzDk32Uok0/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에 있는 많은 decorator 들을 살펴보면서 적절하게 값을 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;@ApiTags()&lt;/code&gt;는 각 컨트롤러 위에 달아서 태그별로 분리하면 좋을 것 같다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;figure id=&quot;og_1694708006509&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;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/openapi/introduction&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rkhHL/hyTV3VoxNI/LaKG8lEFbJ2UgEZnKwl3dk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/openapi/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/openapi/introduction&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rkhHL/hyTV3VoxNI/LaKG8lEFbJ2UgEZnKwl3dk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/nestjs/swagger&quot;&gt;GitHub - nestjs/swagger: OpenAPI (Swagger) module for Nest framework (node.js)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694708016235&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - nestjs/swagger: OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas:&quot; data-og-description=&quot;OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas: - GitHub - nestjs/swagger: OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas:&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/nestjs/swagger&quot; data-og-url=&quot;https://github.com/nestjs/swagger&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WMrHm/hyTV4GLRcv/yJuEKZhfXQUis0klWSU0R0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/nestjs/swagger&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/nestjs/swagger&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WMrHm/hyTV4GLRcv/yJuEKZhfXQUis0klWSU0R0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - nestjs/swagger: OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas:&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas: - GitHub - nestjs/swagger: OpenAPI (Swagger) module for Nest framework (node.js) :earth_americas:&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>개발</category>
      <category>GhostPong</category>
      <category>nestjs</category>
      <category>SWAGGER</category>
      <category>TypeScript</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/9</guid>
      <comments>https://jisu-log.tistory.com/9#entry9comment</comments>
      <pubDate>Wed, 13 Sep 2023 17:57:34 +0900</pubDate>
    </item>
    <item>
      <title>[php-fpm] nginx(웹 서버)와 php-fpm 을 연결했을 때 502 (Bad Gateway) 에러 해결</title>
      <link>https://jisu-log.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php-fpm 을 사용하는 도커 컨테이너 하나와 웹 서버로 nginx 를 사용하는 도커 컨테이너 하나를 띄우고 둘 사이에 통신할 수 있게 세팅하는 와중에 자꾸 502 에러나 connection reset by peer 가 발생하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발생한 에러&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl -v localhost:1234
*   Trying ::1:1234...
* Connected to localhost (::1) port 1234 (#0)
&amp;gt; GET / HTTP/1.1
&amp;gt; Host: localhost:1234
&amp;gt; User-Agent: curl/7.74.0
&amp;gt; Accept: */*
&amp;gt; 
* Mark bundle as not supporting multiuse
&amp;lt; HTTP/1.1 502 Bad Gateway
&amp;lt; Server: nginx/1.20.2
&amp;lt; Date: Sun, 05 Mar 2023 11:09:50 GMT
&amp;lt; Content-Type: text/html
&amp;lt; Content-Length: 157
&amp;lt; Connection: keep-alive
&amp;lt; 
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;502 Bad Gateway&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;center&amp;gt;&amp;lt;h1&amp;gt;502 Bad Gateway&amp;lt;/h1&amp;gt;&amp;lt;/center&amp;gt;
&amp;lt;hr&amp;gt;&amp;lt;center&amp;gt;nginx/1.20.2&amp;lt;/center&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
* Connection #0 to host localhost left intact&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php-fpm 측에서 에러 로그가 안떠서 원인 파악이 힘들었는데 그냥 설정 파일을 잘못 건드려서 문제가 생긴 것이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 설정 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt; wordpress:
    container_name: wordpress
    image: wordpress
    ...
    networks:
      - inception
    expose:
      - &quot;9000&quot;
    init: true
    restart: on-failure
  nginx:
    container_name: nginx
    image: nginx
    ...
    networks:
      - inception
    ports:
      - &quot;443:443&quot;
    init: true
    restart: on-failure
networks:
  inception:
    name: inception
    driver: bridge&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;동일한 docker bridge 네트워크에 넣고 컨테이너명:포트로 nginx -&amp;gt; wordpress(php-fpm running) 으로 통신 시도했는데도 안됐다..&lt;/p&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;code&gt;default.conf&lt;/code&gt; (nginx config file)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;server {
    listen 443 ssl;
    listen [::]:443 ssl;

    index       index.php index.html index.htm;
    ...

    error_page  404  /404.html;
    error_page  500 502 503 504/50x.html;

    location ~ \.php$ {
        include /etc/nginx/fastcgi_params;
        fastcgi_pass wordpress:9000; # .php 요청은 wordpress:9000 으로 보낸다.
        fastcgi_param SCRIPT_FILENAME /wordpress/$fastcgi_script_name;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;www.conf&lt;/code&gt; (php-fpm config file)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]

user = nobody

listen = 0.0.0.0:9000

; Default Value: any
listen.allowed_clients = 0.0.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php-fpm 설정 파일의 access 설정이 잘못되어서 문제였다.&lt;br /&gt;&lt;code&gt;listen.allowed_clients&lt;/code&gt; 의 기본 설정은 이미 any 인데 굳이 0.0.0.0 으로 모든 주소로부터 요청을 받으려고 시도했다..&lt;br /&gt;심지어 예시 컨피그 파일에 아주 친절하게 써져있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.&lt;br /&gt;; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original&lt;br /&gt;; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address&lt;br /&gt;; must be separated by a comma. If this value is left blank, connections will be&lt;br /&gt;; accepted from any ip address.&lt;br /&gt;; Default Value: any&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultValue.. ANY!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 줄을 삭제해주면 아주 잘 해결이 된다.&lt;br /&gt;&lt;code&gt;listen&lt;/code&gt; 도 port 만 주어졌을 때 모든 ip 를 listen 하는 것이 기본값이므로 9000만 작성해 주었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 후 config file&lt;/h3&gt;
&lt;pre class=&quot;php&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]

user = nobody

listen = 9000

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

access.log = /var/log/php8/access.logs&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Errors</category>
      <category>docker</category>
      <category>nginx</category>
      <category>php-fpm</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/8</guid>
      <comments>https://jisu-log.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 12 Mar 2023 23:46:20 +0900</pubDate>
    </item>
    <item>
      <title>[Chrome] 모든 웹페이지에 커서가 나타나는 현상 해결 및 Vimium 소개</title>
      <link>https://jisu-log.tistory.com/7</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.31.48 AM.png&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzgve/btrYTVMIbEM/d7c98QZBzzd63kQ1ujDGVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzgve/btrYTVMIbEM/d7c98QZBzzd63kQ1ujDGVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzgve/btrYTVMIbEM/d7c98QZBzzd63kQ1ujDGVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxzgve%2FbtrYTVMIbEM%2Fd7c98QZBzzd63kQ1ujDGVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;262&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.31.48 AM.png&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 순간부터 모든 웹페이지에 커서가 나타나기 시작했다.&lt;/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;크롬 브라우저 세팅 (맥 단축키 : cmd + ,) 에서 &quot;Navigate pages with a text cursor.&quot; 라는 기능을 찾아서 꺼주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;F7 으로도 on/off 할 수 있다.&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;Screenshot 2023-02-11 at 10.28.31 AM.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk8VRE/btrYTg4Oiee/XaB8NBk6fi3Xa3SSsAizrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk8VRE/btrYTg4Oiee/XaB8NBk6fi3Xa3SSsAizrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk8VRE/btrYTg4Oiee/XaB8NBk6fi3Xa3SSsAizrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk8VRE%2FbtrYTg4Oiee%2FXaB8NBk6fi3Xa3SSsAizrK%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;860&quot; height=&quot;627&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.28.31 AM.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;1036&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;정식 명칭은 caret browing 이다. Firefox 와 Internet Explorer 에서도 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스나 트랙패드를 사용할 수 없을 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 Vimium extension 을 기본으로 사용하고 있기 때문에 도움이 되지 않았다...&lt;/p&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;이 글을 보시는 분들께도 Vimium을 추천하고 싶다! 마우스 없이 브라우징이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?hl=en&quot;&gt;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?hl=en&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1676079908848&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;Vimium&quot; data-og-description=&quot;The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.&quot; data-og-host=&quot;chrome.google.com&quot; data-og-source-url=&quot;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?hl=en&quot; data-og-url=&quot;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RXJLz/hyRAhabDJd/ZC2Ku32X6iw7JY4hbxjUz0/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?hl=en&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RXJLz/hyRAhabDJd/ZC2Ku32X6iw7JY4hbxjUz0/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&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;Vimium&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chrome.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f 를 누르면 다음과 같이 모든 링크를 키보드로 이동할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.47.53 AM.png&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQrK1Q/btrYT8Zt2ty/WVswuuggZPSCbcnNwLUGY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQrK1Q/btrYT8Zt2ty/WVswuuggZPSCbcnNwLUGY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQrK1Q/btrYT8Zt2ty/WVswuuggZPSCbcnNwLUGY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQrK1Q%2FbtrYT8Zt2ty%2FWVswuuggZPSCbcnNwLUGY0%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;860&quot; height=&quot;532&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.47.53 AM.png&quot; data-origin-width=&quot;2216&quot; data-origin-height=&quot;1372&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;Vimium 의 전체 기능은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim 사용법을 안다면 더욱 유용하게 사용할 수 있다.&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;Screenshot 2023-02-11 at 10.46.24 AM.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhvFGD/btrYSkT70VN/5Ok3CEetOzLsmGs6LpoXk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhvFGD/btrYSkT70VN/5Ok3CEetOzLsmGs6LpoXk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhvFGD/btrYSkT70VN/5Ok3CEetOzLsmGs6LpoXk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhvFGD%2FbtrYSkT70VN%2F5Ok3CEetOzLsmGs6LpoXk1%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;860&quot; height=&quot;662&quot; data-filename=&quot;Screenshot 2023-02-11 at 10.46.24 AM.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1288&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;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://support.google.com/chrome/answer/10129654?hl=en&quot;&gt;https://support.google.com/chrome/answer/10129654?hl=en&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1676079874570&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;Use a text cursor to navigate &amp;amp; select text - Google Chrome Help&quot; data-og-description=&quot;&quot; data-og-host=&quot;support.google.com&quot; data-og-source-url=&quot;https://support.google.com/chrome/answer/10129654?hl=en&quot; data-og-url=&quot;https://support.google.com/chrome/answer/10129654?hl=en&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://support.google.com/chrome/answer/10129654?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://support.google.com/chrome/answer/10129654?hl=en&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;Use a text cursor to navigate &amp;amp; select text - Google Chrome Help&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;support.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1676079882251&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;What is Caret Browsing and How to Use It&quot; data-og-description=&quot;You&amp;rsquo;re probably scrolling through this page with your touchpad, external mouse, stylus, or your fingers. But what if we told you there&amp;rsquo;s another interesting way to go through web pages [...]&quot; data-og-host=&quot;www.online-tech-tips.com&quot; data-og-source-url=&quot;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&quot; data-og-url=&quot;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/o9XLV/hyRAof407X/KIzW9tE3CizHOjcnoARGu0/img.jpg?width=680&amp;amp;height=383&amp;amp;face=0_0_680_383,https://scrap.kakaocdn.net/dn/byu8xQ/hyRAfXKrp8/zby6h3axjUvflx2m5A4xyk/img.png?width=618&amp;amp;height=335&amp;amp;face=0_0_618_335,https://scrap.kakaocdn.net/dn/f6rel/hyRAeYPyQx/7HKh9JM5RmoGdI58kg2UQ1/img.png?width=680&amp;amp;height=289&amp;amp;face=0_0_680_289&quot;&gt;&lt;a href=&quot;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.online-tech-tips.com/computer-tips/what-is-caret-browsing-and-how-to-use-it/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/o9XLV/hyRAof407X/KIzW9tE3CizHOjcnoARGu0/img.jpg?width=680&amp;amp;height=383&amp;amp;face=0_0_680_383,https://scrap.kakaocdn.net/dn/byu8xQ/hyRAfXKrp8/zby6h3axjUvflx2m5A4xyk/img.png?width=618&amp;amp;height=335&amp;amp;face=0_0_618_335,https://scrap.kakaocdn.net/dn/f6rel/hyRAeYPyQx/7HKh9JM5RmoGdI58kg2UQ1/img.png?width=680&amp;amp;height=289&amp;amp;face=0_0_680_289');&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;What is Caret Browsing and How to Use It&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;You&amp;rsquo;re probably scrolling through this page with your touchpad, external mouse, stylus, or your fingers. But what if we told you there&amp;rsquo;s another interesting way to go through web pages [...]&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.online-tech-tips.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>tools</category>
      <category>chrome</category>
      <category>Vimium</category>
      <author>이제곱</author>
      <guid isPermaLink="true">https://jisu-log.tistory.com/7</guid>
      <comments>https://jisu-log.tistory.com/7#entry7comment</comments>
      <pubDate>Sat, 11 Feb 2023 10:52:14 +0900</pubDate>
    </item>
  </channel>
</rss>