<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>명왕성의 공전기록</title>
    <link>https://zynoob.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 6 Jul 2026 18:29:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>자전하는명왕성</managingEditor>
    <image>
      <title>명왕성의 공전기록</title>
      <url>https://tistory1.daumcdn.net/tistory/5867075/attach/85bcad55e4ab40eaa5eef1ce1fe5149b</url>
      <link>https://zynoob.tistory.com</link>
    </image>
    <item>
      <title>모두가 자신의 무가치함과 싸우고 있다 2화 감상</title>
      <link>https://zynoob.tistory.com/327</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baIY69/dJMcabc8C0H/AMKg1pkfZ7s26RCxk4Zfm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baIY69/dJMcabc8C0H/AMKg1pkfZ7s26RCxk4Zfm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baIY69/dJMcabc8C0H/AMKg1pkfZ7s26RCxk4Zfm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaIY69%2FdJMcabc8C0H%2FAMKg1pkfZ7s26RCxk4Zfm0%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;1190&quot; height=&quot;970&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2화에서는&amp;nbsp;동만과&amp;nbsp;은하에&amp;nbsp;대한&amp;nbsp;연결을&amp;nbsp;다룬다. &lt;br /&gt;&lt;br /&gt;먼저,&amp;nbsp;은하라는&amp;nbsp;인물에&amp;nbsp;대해&amp;nbsp;얘기해본다. &lt;br /&gt;이전&amp;nbsp;1화에서,&amp;nbsp;동만이라는&amp;nbsp;인물에&amp;nbsp;대해&amp;nbsp;&amp;lsquo;명왕성&amp;rsquo;과&amp;nbsp;닮았다고&amp;nbsp;얘기한&amp;nbsp;적&amp;nbsp;있다. &lt;br /&gt;태양계에&amp;nbsp;잠시&amp;nbsp;속했다가&amp;nbsp;퇴출당한&amp;nbsp;명왕성. &lt;br /&gt;은하라는&amp;nbsp;인물은&amp;nbsp;행성으로&amp;nbsp;비유하자면,&amp;nbsp;어느&amp;nbsp;이름&amp;nbsp;없는&amp;nbsp;행성과&amp;nbsp;같다. &lt;br /&gt;어느곳에도&amp;nbsp;속하지&amp;nbsp;못하고&amp;nbsp;그저&amp;nbsp;자신만의&amp;nbsp;박자로&amp;nbsp;공전이&amp;nbsp;아닌&amp;nbsp;자전만&amp;nbsp;하고&amp;nbsp;있는&amp;nbsp;행성. &lt;br /&gt;&lt;br /&gt;스크린에서&amp;nbsp;보여지는&amp;nbsp;은하의&amp;nbsp;표정은&amp;nbsp;대체로&amp;nbsp;무표정에&amp;nbsp;가깝다. &lt;br /&gt;무슨&amp;nbsp;생각을&amp;nbsp;하는지도&amp;nbsp;알&amp;nbsp;수&amp;nbsp;없고,&amp;nbsp; &lt;br /&gt;이따금&amp;nbsp;감정을&amp;nbsp;느낄&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;장면은&amp;nbsp;그녀가&amp;nbsp;스트레스를&amp;nbsp;받을&amp;nbsp;때&amp;nbsp;코피를&amp;nbsp;흘리는&amp;nbsp;것&amp;nbsp;정도. &lt;br /&gt;여기서의&amp;nbsp;코피는&amp;nbsp;마치&amp;nbsp;소리&amp;nbsp;없는&amp;nbsp;아우성에&amp;nbsp;가까워,&amp;nbsp;동만의&amp;nbsp;직접적인&amp;nbsp;발화를&amp;nbsp;통한&amp;nbsp;감정&amp;nbsp;표출과&amp;nbsp;대비된다. &lt;br /&gt;다만,&amp;nbsp;작품에서&amp;nbsp;보여진&amp;nbsp;감정&amp;nbsp;워치라는&amp;nbsp;소품에서&amp;nbsp;&amp;lsquo;알&amp;nbsp;수&amp;nbsp;없음&amp;rsquo;이라고&amp;nbsp;뜨는&amp;nbsp;것으로&amp;nbsp;봐서는 &lt;br /&gt;그&amp;nbsp;감정이&amp;nbsp;단순&amp;nbsp;부정적인&amp;nbsp;감정일&amp;nbsp;뿐&amp;nbsp;어떤&amp;nbsp;감정인지&amp;nbsp;특정할&amp;nbsp;수는&amp;nbsp;없다는&amp;nbsp;건&amp;nbsp; &lt;br /&gt;은하의&amp;nbsp;정체성&amp;nbsp;자체를&amp;nbsp;대변하는&amp;nbsp;것으로&amp;nbsp;보여&amp;nbsp;보는&amp;nbsp;입장에서&amp;nbsp;꽤&amp;nbsp;재밌다. &lt;br /&gt;&lt;br /&gt;그녀의&amp;nbsp;외모는&amp;nbsp;차분하다. &lt;br /&gt;눈에&amp;nbsp;튀지&amp;nbsp;않는&amp;nbsp;옷과&amp;nbsp;헤어스타일,&amp;nbsp;그리고&amp;nbsp;화장. &lt;br /&gt;내가&amp;nbsp;생각하는&amp;nbsp;&amp;lsquo;외모는&amp;nbsp;무의식에서&amp;nbsp;행하는&amp;nbsp;자신의&amp;nbsp;감정&amp;nbsp;표현의&amp;nbsp;한&amp;nbsp;가지&amp;nbsp;방식이기&amp;nbsp;때문에, &lt;br /&gt;아마&amp;nbsp;그녀는&amp;nbsp;무의식에서&amp;nbsp;조차&amp;nbsp;자신의&amp;nbsp;감정을&amp;nbsp;드러내지&amp;nbsp;않으려고&amp;nbsp;하는&amp;nbsp;것처럼&amp;nbsp;보인다. &lt;br /&gt;&lt;br /&gt;때문에&amp;nbsp;그녀가&amp;nbsp;하는&amp;nbsp;말은&amp;nbsp;힘이&amp;nbsp;있으면서도&amp;nbsp;냉소적이다. &lt;br /&gt;드러내는&amp;nbsp;감정이&amp;nbsp;없음에도&amp;nbsp;말로써&amp;nbsp;타인에게&amp;nbsp;흥분을&amp;nbsp;일으키거나&amp;nbsp;공격성을&amp;nbsp;보이게&amp;nbsp;한다. &lt;br /&gt;은하는&amp;nbsp;동만과&amp;nbsp;달리&amp;nbsp;능력을&amp;nbsp;가지고&amp;nbsp;있지만,&amp;nbsp; &lt;br /&gt;그&amp;nbsp;능력&amp;nbsp;때문에&amp;nbsp;집단의&amp;nbsp;울타리&amp;nbsp;안에&amp;nbsp;속할&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;인물이다. &lt;br /&gt;&lt;br /&gt;- &lt;br /&gt;&lt;br /&gt;2화까지의&amp;nbsp;동만과&amp;nbsp;은하의&amp;nbsp;만남은&amp;nbsp;기차가&amp;nbsp;없는&amp;nbsp;기찻길에서&amp;nbsp;이루어진다. &lt;br /&gt;기찻길은&amp;nbsp;그&amp;nbsp;말마따나&amp;nbsp;&amp;lsquo;길&amp;rsquo;이라는&amp;nbsp;측면에서,&amp;nbsp;이&amp;nbsp;둘이&amp;nbsp;함께&amp;nbsp;길을&amp;nbsp;걸을&amp;nbsp;것이라는&amp;nbsp;것을&amp;nbsp;암시한다. &lt;br /&gt;어쩌면&amp;nbsp;명왕성과&amp;nbsp;이름&amp;nbsp;없는&amp;nbsp;행성의&amp;nbsp;공전&amp;nbsp;궤도가&amp;nbsp;맞닿아&amp;nbsp;있다고도&amp;nbsp;말할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;다음화는&amp;nbsp;동만과&amp;nbsp;은하가&amp;nbsp;규합하여&amp;nbsp;어떤&amp;nbsp;일들을&amp;nbsp;시작하는&amp;nbsp;장면을&amp;nbsp;보여줄&amp;nbsp;듯하다.&lt;/p&gt;</description>
      <category>감상문/Drama</category>
      <category>리뷰</category>
      <category>모자무싸</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/327</guid>
      <comments>https://zynoob.tistory.com/327#entry327comment</comments>
      <pubDate>Mon, 18 May 2026 21:21:30 +0900</pubDate>
    </item>
    <item>
      <title>모두가 자신의 무가치함과 싸우고 있다 1화 감상</title>
      <link>https://zynoob.tistory.com/326</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIL4gY/dJMcagFrAYt/HnukrzUeNDjMh6pWWYPEx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIL4gY/dJMcagFrAYt/HnukrzUeNDjMh6pWWYPEx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIL4gY/dJMcagFrAYt/HnukrzUeNDjMh6pWWYPEx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIL4gY%2FdJMcagFrAYt%2FHnukrzUeNDjMh6pWWYPEx0%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;1190&quot; height=&quot;970&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당&lt;span&gt; &lt;/span&gt;드라마는&lt;span&gt; 20&lt;/span&gt;년째&lt;span&gt; &lt;/span&gt;시나리오를&lt;span&gt; &lt;/span&gt;쓰고&lt;span&gt; &lt;/span&gt;있음에도&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;영화감독으로&lt;span&gt; &lt;/span&gt;데뷔조차&lt;span&gt; &lt;/span&gt;하지&lt;span&gt; &lt;/span&gt;못한&lt;span&gt; &lt;/span&gt;인물을&lt;span&gt; &lt;/span&gt;주인공으로&lt;span&gt; &lt;/span&gt;다룬다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;주인공은&lt;span&gt; &lt;/span&gt;황동만&lt;span&gt;(&lt;/span&gt;구교환&lt;span&gt;)&lt;/span&gt;이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;아무래도&lt;span&gt; 1&lt;/span&gt;화는&lt;span&gt; &lt;/span&gt;주인공이&lt;span&gt; &lt;/span&gt;어떤&lt;span&gt; &lt;/span&gt;인물인지에&lt;span&gt; &lt;/span&gt;집중하고&lt;span&gt; &lt;/span&gt;있기&lt;span&gt; &lt;/span&gt;때문에&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;이번&lt;span&gt; &lt;/span&gt;감상에서도&lt;span&gt; &lt;/span&gt;나름의&lt;span&gt; &lt;/span&gt;인물&lt;span&gt; &lt;/span&gt;분석을&lt;span&gt; &lt;/span&gt;중심으로&lt;span&gt; &lt;/span&gt;이야기해보고자&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;먼저&lt;span&gt;, &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;인물을&lt;span&gt; &lt;/span&gt;한마디로&lt;span&gt; &lt;/span&gt;표현하자면&lt;span&gt; &amp;lsquo;&lt;/span&gt;명왕성&lt;span&gt;&amp;rsquo; &lt;/span&gt;같다고&lt;span&gt; &lt;/span&gt;말하고&lt;span&gt; &lt;/span&gt;싶다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;황동만을&lt;span&gt; &lt;/span&gt;보며&lt;span&gt; &lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;년&lt;span&gt; &lt;/span&gt;전의&lt;span&gt; &lt;/span&gt;내&lt;span&gt; &lt;/span&gt;모습이&lt;span&gt; &lt;/span&gt;떠올랐다&lt;span&gt;. &lt;/span&gt;일&lt;span&gt; &lt;/span&gt;년&lt;span&gt; &lt;/span&gt;만에&lt;span&gt; &lt;/span&gt;시인으로&lt;span&gt; &lt;/span&gt;등단했음에도&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;어린&lt;span&gt; &lt;/span&gt;나이&lt;span&gt; &lt;/span&gt;때문이었는지&lt;span&gt;, &lt;/span&gt;혹은&lt;span&gt; &lt;/span&gt;내지&lt;span&gt; &lt;/span&gt;못했던&lt;span&gt; 30&lt;/span&gt;만&lt;span&gt; &lt;/span&gt;원의&lt;span&gt; &lt;/span&gt;회비&lt;span&gt; &lt;/span&gt;때문이었는지&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;시인협회라는&lt;span&gt; &lt;/span&gt;소속에&lt;span&gt; &lt;/span&gt;끝내&lt;span&gt; &lt;/span&gt;녹아들지&lt;span&gt; &lt;/span&gt;못했던&lt;span&gt; &lt;/span&gt;나&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;나는&lt;span&gt; &lt;/span&gt;당시의&lt;span&gt; &lt;/span&gt;나를&lt;span&gt; &amp;lsquo;&lt;/span&gt;명왕성&lt;span&gt;&amp;rsquo;&lt;/span&gt;이라&lt;span&gt; &lt;/span&gt;비유하곤&lt;span&gt; &lt;/span&gt;했다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;명왕성은&lt;span&gt; &lt;/span&gt;한때&lt;span&gt; &lt;/span&gt;태양계의&lt;span&gt; &lt;/span&gt;행성으로&lt;span&gt; &lt;/span&gt;인정받았지만&lt;span&gt;, &lt;/span&gt;얼마&lt;span&gt; &lt;/span&gt;지나지&lt;span&gt; &lt;/span&gt;않아&lt;span&gt; &lt;/span&gt;그&lt;span&gt; &lt;/span&gt;지위를&lt;span&gt; &lt;/span&gt;잃어버린&lt;span&gt; &lt;/span&gt;존재이기&lt;span&gt; &lt;/span&gt;때문이다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;황동만&lt;span&gt; &lt;/span&gt;역시&lt;span&gt; &lt;/span&gt;나와는&lt;span&gt; &lt;/span&gt;다른&lt;span&gt; &lt;/span&gt;우주에&lt;span&gt; &lt;/span&gt;놓인&lt;span&gt; &lt;/span&gt;또&lt;span&gt; &lt;/span&gt;하나의&lt;span&gt; &lt;/span&gt;명왕성처럼&lt;span&gt; &lt;/span&gt;보였다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;모두가&lt;span&gt; &lt;/span&gt;비슷한&lt;span&gt; &lt;/span&gt;행성처럼&lt;span&gt; &lt;/span&gt;보이는데&lt;span&gt;, &lt;/span&gt;왜&lt;span&gt; &lt;/span&gt;나만&lt;span&gt; &lt;/span&gt;이곳에&lt;span&gt; &lt;/span&gt;속하지&lt;span&gt; &lt;/span&gt;못하는지에&lt;span&gt; &lt;/span&gt;대해&lt;span&gt; &lt;/span&gt;홀로&lt;span&gt; &lt;/span&gt;지독한&lt;span&gt; &lt;/span&gt;외로움을&lt;span&gt; &lt;/span&gt;견디는&lt;span&gt; &lt;/span&gt;인물이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그는&lt;span&gt; &lt;/span&gt;곰팡이로&lt;span&gt; &lt;/span&gt;얼룩진&lt;span&gt; &lt;/span&gt;듯&lt;span&gt; &lt;/span&gt;누리끼리한&lt;span&gt; &lt;/span&gt;원룸에서&lt;span&gt; &lt;/span&gt;살며&lt;span&gt;, &lt;/span&gt;관리비조차&lt;span&gt; &lt;/span&gt;제대로&lt;span&gt; &lt;/span&gt;내지&lt;span&gt; &lt;/span&gt;못하는&lt;span&gt; &lt;/span&gt;삶을&lt;span&gt; &lt;/span&gt;이어간다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그는&lt;span&gt; &lt;/span&gt;가난한&lt;span&gt; &lt;/span&gt;사람일수록&lt;span&gt; &lt;/span&gt;더&lt;span&gt; &lt;/span&gt;좋은&lt;span&gt; &lt;/span&gt;글을&lt;span&gt; &lt;/span&gt;쓸&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다고&lt;span&gt; &lt;/span&gt;말하지만&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;스스로를&lt;span&gt; &amp;lsquo;&lt;/span&gt;퍼펙트하게&lt;span&gt; &lt;/span&gt;가난하다&lt;span&gt;&amp;rsquo;&lt;/span&gt;고&lt;span&gt; &lt;/span&gt;표현하는&lt;span&gt; &lt;/span&gt;그는&lt;span&gt; &lt;/span&gt;아무것도&lt;span&gt; &lt;/span&gt;증명하지&lt;span&gt; &lt;/span&gt;못한&lt;span&gt; &lt;/span&gt;채&lt;span&gt; &lt;/span&gt;억울함만을&lt;span&gt; &lt;/span&gt;느낀다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;밖을&lt;span&gt; &lt;/span&gt;나설&lt;span&gt; &lt;/span&gt;때면&lt;span&gt; &lt;/span&gt;마치&lt;span&gt; &lt;/span&gt;독일군이&lt;span&gt; &lt;/span&gt;입었을&lt;span&gt; &lt;/span&gt;법한&lt;span&gt; &lt;/span&gt;총알&lt;span&gt; &lt;/span&gt;구멍&lt;span&gt; &lt;/span&gt;난&lt;span&gt; &lt;/span&gt;코트를&lt;span&gt; &lt;/span&gt;걸치고&lt;span&gt;, &lt;/span&gt;검은&lt;span&gt; &lt;/span&gt;가방&lt;span&gt; &lt;/span&gt;위에&lt;span&gt; &lt;/span&gt;헤드셋을&lt;span&gt; &lt;/span&gt;묶어&lt;span&gt; &lt;/span&gt;다닌다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그&lt;span&gt; &lt;/span&gt;헤드셋은&lt;span&gt; &lt;/span&gt;외로운&lt;span&gt; &lt;/span&gt;술자리&lt;span&gt; &lt;/span&gt;이후의&lt;span&gt; &lt;/span&gt;귀갓길&lt;span&gt;, &lt;/span&gt;그리고&lt;span&gt; &lt;/span&gt;혼자&lt;span&gt; &lt;/span&gt;영화를&lt;span&gt; &lt;/span&gt;볼&lt;span&gt; &lt;/span&gt;때&lt;span&gt; &lt;/span&gt;착용되는데&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;이때의&lt;span&gt; &lt;/span&gt;헤드셋은&lt;span&gt; &lt;/span&gt;외부로부터&lt;span&gt; &lt;/span&gt;자신을&lt;span&gt; &lt;/span&gt;보호하는&lt;span&gt; &lt;/span&gt;방패에&lt;span&gt; &lt;/span&gt;가깝다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;결국&lt;span&gt; &lt;/span&gt;그는&lt;span&gt; &lt;/span&gt;전투복과&lt;span&gt; &lt;/span&gt;방패로&lt;span&gt; &lt;/span&gt;스스로를&lt;span&gt; &lt;/span&gt;무장한&lt;span&gt; &lt;/span&gt;채&lt;span&gt; &lt;/span&gt;세상&lt;span&gt; &lt;/span&gt;밖으로&lt;span&gt; &lt;/span&gt;나서는&lt;span&gt; &lt;/span&gt;셈이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그는&lt;span&gt; &lt;/span&gt;말로&lt;span&gt; &lt;/span&gt;주변&lt;span&gt; &lt;/span&gt;사람들을&lt;span&gt; &lt;/span&gt;찌르는&lt;span&gt; &lt;/span&gt;인물이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;lsquo;&lt;/span&gt;나는&lt;span&gt; &lt;/span&gt;안&lt;span&gt; &lt;/span&gt;되었는데&lt;span&gt; &lt;/span&gt;왜&lt;span&gt; &lt;/span&gt;너는&lt;span&gt; &lt;/span&gt;되느냐&lt;span&gt;&amp;rsquo;&lt;/span&gt;라는&lt;span&gt; &lt;/span&gt;마음으로&lt;span&gt;, &lt;/span&gt;때로는&lt;span&gt; &lt;/span&gt;눈치&lt;span&gt; &lt;/span&gt;없이&lt;span&gt; &lt;/span&gt;타인을&lt;span&gt; &lt;/span&gt;향해&lt;span&gt; &lt;/span&gt;모진&lt;span&gt; &lt;/span&gt;말을&lt;span&gt; &lt;/span&gt;내뱉는다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;하지만&lt;span&gt; &lt;/span&gt;그&lt;span&gt; &lt;/span&gt;역시&lt;span&gt; &lt;/span&gt;그것이&lt;span&gt; &lt;/span&gt;옳은&lt;span&gt; &lt;/span&gt;방식이&lt;span&gt; &lt;/span&gt;아니라는&lt;span&gt; &lt;/span&gt;것을&lt;span&gt; &lt;/span&gt;알고&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;듯하다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;자아실현의&lt;span&gt; &lt;/span&gt;결핍을&lt;span&gt; &lt;/span&gt;느낄&lt;span&gt; &lt;/span&gt;때마다&lt;span&gt; &lt;/span&gt;말을&lt;span&gt; &lt;/span&gt;삼키는&lt;span&gt; &lt;/span&gt;대신&lt;span&gt; &lt;/span&gt;음식을&lt;span&gt; &lt;/span&gt;입에&lt;span&gt; &lt;/span&gt;넣거나&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그것마저&lt;span&gt; &lt;/span&gt;어려울&lt;span&gt; &lt;/span&gt;때는&lt;span&gt; &lt;/span&gt;늘&lt;span&gt; &lt;/span&gt;해오던&lt;span&gt; &lt;/span&gt;것처럼&lt;span&gt; &lt;/span&gt;남들에게&lt;span&gt; &lt;/span&gt;혓바늘을&lt;span&gt; &lt;/span&gt;세우기&lt;span&gt; &lt;/span&gt;때문이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;또한&lt;span&gt; &lt;/span&gt;그가&lt;span&gt; &lt;/span&gt;쓰는&lt;span&gt; &lt;/span&gt;작품은&lt;span&gt; &amp;lsquo;&lt;/span&gt;날씨가&lt;span&gt; &lt;/span&gt;없는&lt;span&gt; &lt;/span&gt;세상에서&lt;span&gt; &lt;/span&gt;날씨를&lt;span&gt; &lt;/span&gt;만들어내는&lt;span&gt; &lt;/span&gt;주인공의&lt;span&gt; &lt;/span&gt;이야기&lt;span&gt;&amp;rsquo;&lt;/span&gt;라고&lt;span&gt; &lt;/span&gt;한다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;span&gt; &lt;/span&gt;날씨는&lt;span&gt; &lt;/span&gt;감정과&lt;span&gt; &lt;/span&gt;맞닿아&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그는&lt;span&gt; &amp;lsquo;&lt;/span&gt;감정&lt;span&gt; &lt;/span&gt;워치&lt;span&gt;&amp;rsquo;&lt;/span&gt;라는&lt;span&gt; &lt;/span&gt;장치를&lt;span&gt; &lt;/span&gt;착용하고&lt;span&gt; &lt;/span&gt;다니는데&lt;span&gt;, &lt;/span&gt;이는&lt;span&gt; &lt;/span&gt;자신의&lt;span&gt; &lt;/span&gt;감정을&lt;span&gt; &lt;/span&gt;시각적으로&lt;span&gt; &lt;/span&gt;드러내는&lt;span&gt; &lt;/span&gt;물건이다&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그리고&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;물건은&lt;span&gt; &lt;/span&gt;오히려&lt;span&gt; &lt;/span&gt;황동만을&lt;span&gt; &lt;/span&gt;지배한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;결국&lt;span&gt; &lt;/span&gt;그가&lt;span&gt; &lt;/span&gt;쓰는&lt;span&gt; &lt;/span&gt;이야기는&lt;span&gt;, &lt;/span&gt;감정조차&lt;span&gt; &lt;/span&gt;스스로&lt;span&gt; &lt;/span&gt;통제하지&lt;span&gt; &lt;/span&gt;못하는&lt;span&gt; &lt;/span&gt;현재의&lt;span&gt; &lt;/span&gt;자신을&lt;span&gt; &lt;/span&gt;벗어나고자&lt;span&gt; &lt;/span&gt;하는&lt;span&gt; &lt;/span&gt;자아실현의&lt;span&gt; &lt;/span&gt;또&lt;span&gt; &lt;/span&gt;다른&lt;span&gt; &lt;/span&gt;표현일지도&lt;span&gt; &lt;/span&gt;모른다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1&lt;/span&gt;화의&lt;span&gt; &lt;/span&gt;감상을&lt;span&gt; &lt;/span&gt;마쳤다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로&lt;span&gt; &lt;/span&gt;황동만의&lt;span&gt; &lt;/span&gt;날씨는&lt;span&gt; &lt;/span&gt;어떨지&lt;span&gt;, &lt;/span&gt;그는&lt;span&gt; &lt;/span&gt;작품&lt;span&gt; &lt;/span&gt;제목처럼&lt;span&gt; &lt;/span&gt;어떻게&lt;span&gt; &lt;/span&gt;자신의&lt;span&gt; &lt;/span&gt;무가치함과&lt;span&gt; &lt;/span&gt;싸울지&lt;span&gt; &lt;/span&gt;궁금하다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;아마&lt;span&gt; 2&lt;/span&gt;화의&lt;span&gt; &lt;/span&gt;이야기는&lt;span&gt; &lt;/span&gt;여주인공처럼&lt;span&gt; &lt;/span&gt;비중있게&lt;span&gt; &lt;/span&gt;다루는&lt;span&gt; &lt;/span&gt;고윤정에&lt;span&gt; &lt;/span&gt;대한&lt;span&gt; &lt;/span&gt;인물&lt;span&gt; &lt;/span&gt;분석을&lt;span&gt; &lt;/span&gt;하게되지&lt;span&gt; &lt;/span&gt;않을까&lt;span&gt; &lt;/span&gt;싶다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>감상문/Drama</category>
      <category>감상</category>
      <category>리뷰</category>
      <category>모두가 자신의 무가치함과 싸우고 있다</category>
      <category>모자무싸</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/326</guid>
      <comments>https://zynoob.tistory.com/326#entry326comment</comments>
      <pubDate>Thu, 7 May 2026 04:13:17 +0900</pubDate>
    </item>
    <item>
      <title>Next.js - zustand 를 사용한 상태 관리</title>
      <link>https://zynoob.tistory.com/325</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 next.js로 프론트엔드 개발하던 중 사용한 zustand 라이브러리에 대해 포스팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 zustand를 쓸 생각이 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 recoil을 쓰다보니 아래와 같은 에러가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-28 오후 6.20.25.png&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdffCB/btsOgODPSkZ/LL5k5CbpasjxHz2qsnWub1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdffCB/btsOgODPSkZ/LL5k5CbpasjxHz2qsnWub1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdffCB/btsOgODPSkZ/LL5k5CbpasjxHz2qsnWub1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdffCB%2FbtsOgODPSkZ%2FLL5k5CbpasjxHz2qsnWub1%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;2038&quot; height=&quot;1400&quot; data-filename=&quot;스크린샷 2025-05-28 오후 6.20.25.png&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 찾아보니, react 15버전 이상에서는 recoil이 호환성 에러가 발생한다는 거.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 원인을 모른 채 gpt를 붙잡고 한 시간 가까이 씨름하다 구글링으로 알게 되었다. 하참.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 나는 라이브러리 하나 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 프로젝트의 버전을 기존 19버전에서 14버전까지 내릴 생각이 없었기 때문에 zustand를 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zustand 를 쓰는 방법은 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 먼저 해당 라이브러리를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1748426325410&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn add zustand&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;2. 이후, 상태 관리 스토어를 설정한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 경우, 유저 정보를 설정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1748426381658&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/common/store/user.ts
import { create } from &quot;zustand&quot;;

// 사용자 정보 인터페이스 정의
interface User {
  id: string;
  email: string;
  nickname: string;
  role?: string;
 // 중략
}

// Zustand 스토어 인터페이스 정의
interface UserState {
  user: User | null;
  setUser: (user: User | null) =&amp;gt; void;
}

// Zustand 스토어 생성
export const useUserStore = create&amp;lt;UserState&amp;gt;((set) =&amp;gt; ({
  user: null,
  setUser: (user) =&amp;gt; set({ user }),
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 컴포넌트에서 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1748426435770&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const useLogin = (
  onSuccess: () =&amp;gt; void,
  onFail?: (errorMessage: string) =&amp;gt; void
) =&amp;gt; {
  const setUser = useUserStore((state) =&amp;gt; state.setUser);
  const router = useRouter();&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;zustand의 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 설정 방법이 매우 단순하다. (Recoil과 달리, 별도의 provider 설정이 필요 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- typescript와 완벽히 통합된다.&lt;/p&gt;</description>
      <category>개발/react next.js</category>
      <category>Next.js</category>
      <category>react</category>
      <category>zustand</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/325</guid>
      <comments>https://zynoob.tistory.com/325#entry325comment</comments>
      <pubDate>Wed, 28 May 2025 20:02:50 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot - GraphQL 적용하기</title>
      <link>https://zynoob.tistory.com/324</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 포스팅에서는 graphQL을 스프링 부트에 적용하는 방법에 대해 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아래를 참고하여 의존성을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746266972436&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle

// graphql 스타터 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-graphql' 
// GraphQL Java에서 제공하지 않는 추가 스칼라 타입들을 사용할 수 있도록 해주는 라이브러리
implementation 'com.graphql-java:graphql-java-extended-scalars:21.0'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1746267178324&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml

	graphql:
        schema:
            printer: // 서버 구동 시 실제 graphQL 스키마 파일 출력
                enabled: true
                location: classpath:graphql/schema.graphqls
        graphiql: // 웹 UI
            enabled: true
            path: /graphiql
        schema-location-pattern: &quot;**/*.graphqls&quot;
        tools: // 스키마 정보조회 허용
            introspection:
                enabled: true
            schema-location-pattern: &quot;**/*.graphqls&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;/p&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를 정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746267253550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// schema.graphqls
type Query {
    fetchUser(userId: ID): User
    fetchUsers: [User]
}

type Mutation {
    createUser(createUserDTO: CreateUserDTO!): User
}

input CreateUserDTO {
    nickName: String!
    password: String!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746267417870&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CreateUserDTO.java

@Data
public class CreateUserDTO {
    private String nickName;
    private String password;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest.js와 굉장히 흡사하다.&lt;/p&gt;
&lt;pre id=&quot;code_1746267545444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserResolver

package com.chessy.resolver;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

import com.chessy.domain.Users;
import com.chessy.dto.userDTO.UserDTO;
import com.chessy.dto.userDTO.CreateUserDTO;
import com.chessy.service.UserService;

@Controller
public class UserResolver {

  @Autowired
  private UserService userService;

  @MutationMapping
  public UserDTO createUser(
      @Argument(&quot;createUserDTO&quot;) CreateUserDTO createUserDTO) {
    Users user = userService.createUser(createUserDTO);
    return convertToDTO(user);
  }
 
// 중략 
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1746267593986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UserService

  public Users createUser(CreateUserDTO createUserDTO) {
    if (createUserDTO == null) {
      throw new CustomException(ErrorCode.INVALID_INPUT_VALUE, &quot;사용자 정보는 필수입니다.&quot;);
    }

    Users user = new Users();
    user.setNickName(createUserDTO.getNickName());
    user.setPassword(passwordEncoder.encode(createUserDTO.getPassword()));
    user.setRole(Role.USER);
    return userRepository.save(user);
  }&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;이러면 graphQL 사용을 테스트 해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost:8080/graphiql 로 접속하면, 아래와 같은 화면이 뜨는데, 내게는 익숙치 않은 편.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1065&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odjot/btsNJHlwVtb/XVrHTPEoUbvfWptIw00Rd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odjot/btsNJHlwVtb/XVrHTPEoUbvfWptIw00Rd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odjot/btsNJHlwVtb/XVrHTPEoUbvfWptIw00Rd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fodjot%2FbtsNJHlwVtb%2FXVrHTPEoUbvfWptIw00Rd1%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;1920&quot; height=&quot;1065&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1065&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;그래서, 익숙한 Sandbox의 플레이그라운드를 사용해주기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 페이지를 만들어서 접속할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1746267724950&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// resources/static/sandbox.html

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
        &amp;lt;title&amp;gt;GraphQL Sandbox&amp;lt;/title&amp;gt;
        &amp;lt;style&amp;gt;
            html,
            body {
                margin: 0;
                padding: 0;
                width: 100vw;
                height: 100vh;
                overflow: hidden;
            }
            #sandbox {
                width: 100vw;
                height: 100vh;
            }
        &amp;lt;/style&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;script src=&quot;https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
        &amp;lt;div id=&quot;sandbox&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;script&amp;gt;
            new window.EmbeddedSandbox({
                target: &quot;#sandbox&quot;,
                initialEndpoint: window.location.origin + &quot;/graphql&quot;,
                includeCookies: true,
                initialState: {
                    document: `query {
                        fetchUser(id: 1) {
                            id
                            role
                            nickName
                            createdAt
                            updatedAt
                        }
                    }`,
                },
            });
        &amp;lt;/script&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&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;다만, 매번 sandbox.html 을 입력하는 건 번거로우니까,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 swagger 를 사용했던 방식을 응용하여 아래와 같은 방식을 채택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 localhost:8080/sandbox로 플레이그라운드에 접근이 가능해졌다.&lt;/p&gt;
&lt;pre id=&quot;code_1746267810145&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.chessy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DocsController {

  @GetMapping(&quot;/api&quot;)
  public String redirectSwagger() {
    return &quot;redirect:/swagger-ui/index.html&quot;;
  }

  @GetMapping(&quot;/sandbox&quot;)
  public String redirectSandbox() {
    return &quot;forward:/sandbox.html&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1010&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IxUGi/btsNJJQ9ZNA/TJ3TKeVRrZqGQdq58Tbyg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IxUGi/btsNJJQ9ZNA/TJ3TKeVRrZqGQdq58Tbyg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IxUGi/btsNJJQ9ZNA/TJ3TKeVRrZqGQdq58Tbyg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIxUGi%2FbtsNJJQ9ZNA%2FTJ3TKeVRrZqGQdq58Tbyg0%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;1920&quot; height=&quot;1010&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1010&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/Spring boot</category>
      <category>graphql</category>
      <category>Java</category>
      <category>spring boot</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/324</guid>
      <comments>https://zynoob.tistory.com/324#entry324comment</comments>
      <pubDate>Fri, 16 May 2025 20:26:19 +0900</pubDate>
    </item>
    <item>
      <title>node.js - npm 라이브러리 배포하기</title>
      <link>https://zynoob.tistory.com/323</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 npm에 라이브러리를 배포해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nest.js를 쓰면서 class-validator의 한계로 인해 매번 커스텀을 해야한다는 게 불편하기도 했고,&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;때문에, class-validator를 계승하는 방식으로,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 데코레이터를 제작하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이왕 만드는 김에, 타입스크립를 사용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cjs(CommonJS)와 ESM(ES Module) 환경에서 모두 사용할 수 있도록 설계하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* CJS와 ESM을 모두 지원하는 이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Node.js나 Nest.js는 여전히(CommonJS) 형식을 주로 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Next.js 등은 ESM을 기본으로 사용하기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지를 다양한 프로젝트에서 사용할 수 있도록 하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 형식을 모두 배포하는 게 중요하다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 폴더 구조 예시&lt;/p&gt;
&lt;pre id=&quot;code_1746110977253&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;zyno-validator/
├── src/
│   └── validators/
│       └── is-english.validator.ts
│   └── index.ts
├── dist/
│   ├── cjs/   &amp;larr; CommonJS 빌드 결과
│   └── esm/   &amp;larr; ESM 빌드 결과
├── tsconfig.cjs.json
├── tsconfig.esm.json
└── package.json&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 작업할 프로젝트의 폴더를 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111091852&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;mkdir zyno-validator &amp;amp;&amp;amp; cd zyno-validator
yarn init&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 패키지 개발에 필요한 의존성 설치&lt;/p&gt;
&lt;pre id=&quot;code_1746111091852&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;yarn add class-validator class-transformer reflect-metadata
yarn add -D typescript ts-node jest ts-jest @types/jest&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- tsconfig.json 설정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후, src/index.ts 에 다음과 같은 설정을 해줘야 정상 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 tsconfig의 설정은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111062562&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// tsconfig.cjs.json
{
  &quot;extends&quot;: &quot;./tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;dist/cjs&quot;,
    &quot;module&quot;: &quot;CommonJS&quot;
  }
}


// tsconfig.esm.json
{
  &quot;extends&quot;: &quot;./tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;dist/esm&quot;,
    &quot;module&quot;: &quot;ES2020&quot;
  }
}

// tsconfig.json
{
  &quot;name&quot;: &quot;@zynoobb/validator&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;, // ESM을 기본으로 지정
  &quot;main&quot;: &quot;./dist/cjs/index.js&quot;,      // CommonJS 진입점
  &quot;module&quot;: &quot;./dist/esm/index.js&quot;,    // ESM 진입점
  &quot;types&quot;: &quot;./dist/esm/index.d.ts&quot;,
  &quot;exports&quot;: {
    &quot;import&quot;: &quot;./dist/esm/index.js&quot;,  // ESM
    &quot;require&quot;: &quot;./dist/cjs/index.js&quot;  // CJS
  },
  &quot;scripts&quot;: {
    &quot;build:cjs&quot;: &quot;tsc -p tsconfig.cjs.json&quot;,
    &quot;build:esm&quot;: &quot;tsc -p tsconfig.esm.json&quot;,
    &quot;build&quot;: &quot;npm run build:cjs &amp;amp;&amp;amp; npm run build:esm&quot;,
    &quot;prepare&quot;: &quot;npm run build&quot;
  }
}

// src.index.ts
import &quot;reflect-metadata&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;/p&gt;
&lt;pre id=&quot;code_1746111303752&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/decorator/is-english.validator.ts

import {
  registerDecorator,
  ValidationOptions,
  ValidationArguments,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from &quot;class-validator&quot;;
import { Transform } from &quot;class-transformer&quot;;

/**
 * 실제로 영어 검사 로직을 수행하는 Constraint 클래스
 */
@ValidatorConstraint({ name: &quot;isEnglish&quot;, async: false })
export class IsEnglishConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments): boolean {
    if (value === null || value === undefined || value === &quot;&quot;) return true; // 빈 값은 스킵
    return /^[a-zA-Z\s-]+$/.test(value.trim());
  }

  defaultMessage(args: ValidationArguments): string {
    // custom message 가 들어오면 validationOptions.message 를 우선, 아니면 기본 메시지
    return `${args.property} must contain only English letters`;
  }
}

/**
 * 영어로만 구성된 값인지 검증하는 데코레이터
 */
export function IsEnglish(
  validationOptions?: ValidationOptions
): PropertyDecorator {
  return (object: Object, propertyName: string | symbol) =&amp;gt; {
    // 1) 먼저 Transform 으로 트리밍
    Transform(({ value }) =&amp;gt;
      typeof value === &quot;string&quot; ? value.trim() : value
    )(object, propertyName);
    // 2) registerDecorator 로 class-validator 에 등록
    registerDecorator({
      name: &quot;isEnglish&quot;,
      target: object.constructor,
      propertyName: propertyName as string,
      options: validationOptions,
      validator: IsEnglishConstraint,
    });
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼, 앞서 받아놓은 class-transformer, class-validator 라이브러리를 활용하여 검증 로직을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이후 index.ts 에 해당 기능을 임포트하고 내보내면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111640186&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.ts

import &quot;reflect-metadata&quot;;
import { IsKorean, IsKoreanConstraint } from &quot;./validators/is-korean.validator&quot;;
import {
  IsEnglish,
  IsEnglishConstraint,
} from &quot;./validators/is-english.validator&quot;;

export { IsKorean, IsEnglish, IsKoreanConstraint, IsEnglishConstraint };&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;배포하기 전, package.json을 자신의 취향대로 수정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111837345&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@zynoobb/validator&quot;, // 패키지 이름
  &quot;version&quot;: &quot;1.0.0&quot;, // 버전
  &quot;description&quot;: &quot;한글과 영어 입력을 위한 검증 라이브러리&quot;,
  &quot;main&quot;: &quot;./dist/index.js&quot;,
  &quot;module&quot;: &quot;./dist/index.mjs&quot;,
  &quot;types&quot;: &quot;./dist/index.d.ts&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;require&quot;: &quot;./dist/index.js&quot;,
      &quot;import&quot;: &quot;./dist/index.mjs&quot;,
      &quot;types&quot;: &quot;./dist/index.d.ts&quot;
    },
    &quot;./validators&quot;: {
      &quot;require&quot;: &quot;./dist/validators/index.js&quot;,
      &quot;import&quot;: &quot;./dist/validators/index.mjs&quot;,
      &quot;types&quot;: &quot;./dist/validators/index.d.ts&quot;
    }
  },
  &quot;files&quot;: [
    &quot;dist&quot;, // 빌드된 내용
    &quot;README.md&quot;, // 라이브러리 설명
    &quot;LICENSE&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 npm 계정이 필요하다. 아래 명령어를 입력하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 계정을 생성하는 페이지로 이동하거나, 로그인 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111863938&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm login&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 아래와 같은 명령어를 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746111938573&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run build // 라이브러리 빌드
npm pack // 라이브러리 압축&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이떄 npm pack 명령어를 실행하면, .tgz 압축 파일이 생성되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 안에 dist, readme.md 파일이 있는지 확인하면 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1746112001441&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm publish --access public // 공개 배포&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;여기까지 마치면, npm.js에 자신이 만든 라이브러리가 배포된다.&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.npmjs.com/package/@zynoobb/validator&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/@zynoobb/validator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bae22d/btsNHeQKbFj/LydM2MfoyxuaajlkKMEG10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bae22d/btsNHeQKbFj/LydM2MfoyxuaajlkKMEG10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bae22d/btsNHeQKbFj/LydM2MfoyxuaajlkKMEG10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbae22d%2FbtsNHeQKbFj%2FLydM2MfoyxuaajlkKMEG10%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;1195&quot; height=&quot;890&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 다운 받아서 테스트 해보도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트하기 위한 코드는 아래와 같이 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1746112190304&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { IsEnglish } from '@zynoobb/validator';
import { validate } from 'class-validator';
import express, { Request, Response } from 'express';

class TestDTO {
  @IsEnglish()
  name!: string;
}

const app = express();

app.use(express.json());

app.post('/', async (req: Request, res: Response) =&amp;gt; {
  const testDTO = new TestDTO();
  testDTO.name = (req.body as { name: string }).name;

  const errors = await validate(testDTO);
  if (errors.length &amp;gt; 0) {
    return res.status(400).json({ errors });
  }

  return res.status(200).json({ message: 'successful' });
});

app.listen(3000, () =&amp;gt; {
  console.log('Server running');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;1320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XrhUq/btsNHTk3G8I/qulXKxjLsMMDxxZTm44hM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XrhUq/btsNHTk3G8I/qulXKxjLsMMDxxZTm44hM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XrhUq/btsNHTk3G8I/qulXKxjLsMMDxxZTm44hM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXrhUq%2FbtsNHTk3G8I%2FqulXKxjLsMMDxxZTm44hM0%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;2174&quot; height=&quot;1320&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;1320&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;</description>
      <category>개발/node.js</category>
      <category>npmjs</category>
      <category>npmjs 라이브러리</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/323</guid>
      <comments>https://zynoob.tistory.com/323#entry323comment</comments>
      <pubDate>Mon, 12 May 2025 10:09:06 +0900</pubDate>
    </item>
    <item>
      <title>Docker-compose - .sh 파일과 DB 백업</title>
      <link>https://zynoob.tistory.com/321</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 Docker-compose 를 사용하며, 데이터베이스를 백업하는 방식에 대해 다룬다.&lt;/p&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;- Nest.js 를 Docker-compose 를 활용해 개발 중이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그리고, Docker 컨테이너 안에서는 docker exec 명령어 자체가 불가능한 상황이었으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 따라서 DB 백업 명령어를 서버 안에서 직접 실행할 수 없는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Docker 컨테이너 내부가 아닌 외부에서 백업 명령어를 수행시키면 될 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방식은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1745931387392&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- Nest.js는 백업 요청(.todo 파일)만 생성한다.

- 호스트(내 맥북)에서 .sh 파일(backup-watch.sh)이 백그라운드로 실행되게 하고

- .todo 파일이 생겼는지 감지하며

- 해당 파일이 감지된 경우, 진짜 docker exec 명령어로 백업을 수행한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 서버 컨테이너가 직접 docker 명령어를 사용하지 못하니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 맥북에서 감시하고 명령을 실행할 수 있도록 자동화된 Shell script(.sh)파일을 작성했다고 보면 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1745931379234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.sh 파일이란, shell script 파일로, 여러 터미널 명령어를 모아놓고, 한번에 실행할 수 있게 만든 스크립트.&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 id=&quot;code_1745932284478&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 폴더 구조
root/
├── backup/
│   ├── db/                   # 실제 백업된 .sql.gz 파일 저장 폴더
│   └── tasks/                 # .todo 파일 저장 폴더 (백업 요청용)
│
├── backup-watcher.sh          # 호스트에서 돌아가는 감시 스크립트
│
└── src/
    └── data-backup/
        ├── data-backup.service.ts    # 백업 요청(create-backup.todo 파일 생성)
        └── data-backup.resolver.ts   # graphQL 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;.sh 작성 요령에 대해서는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1745931536147&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# backup-watcher.sh  

#!/bin/bash # 선언 명시

echo &quot;[Watcher] 백업 감시&quot; # echo 문자열 터미널에 출력

while true; do # 무한 루프 시작
  if [ -f backup/tasks/create-backup.todo ]; then # -f 파일 경로에 파일 존재 확인
    echo &quot;[Watcher] 백업 요청 감지&quot; # 존재한다면 문자열 출력 

    timestamp=$(date +&quot;%Y-%m-%dT%H-%M-%S&quot;) # 파일 이름으로 쓸 시간 변수 저장
    backup_dir=&quot;backup/db&quot; # 저장 위치
    backup_file=&quot;$backup_dir/neodb_${timestamp}.sql.gz&quot; # 파일 이름

    mkdir -p $backup_dir # 해당 디렉토리가 있으면 새로 만들고 있으면 무시
    # 즉, backup/db 폴더가 없으면 새로 만듬

    echo &quot;[Watcher] 백업 파일 생성 중: $backup_file&quot; 

    docker exec {db_container_name} sh -c 'mysqldump -u {user_id} -p&quot;{user_password}&quot; {db_name}' | gzip &amp;gt; $backup_file
    # {} 안에는 해당되는 내용의 이름을 넣어주면 된다
    # docker exec docker 컨테이너 안에서 명령어 실행
    # mysqldump mysql db 백업 명령어
    # gzip 결과를 바로 압축
    # 해당 결과를 back_file 이름으로 저장

    if [ -s &quot;$backup_file&quot; ]; then # 파일 존재하고, 크기가 0보다 크면 true
      echo &quot;[Watcher] 백업 성공: $backup_file&quot;
    else
      echo &quot;[Watcher] 백업 실패: 파일이 비어있음&quot;
      rm -f &quot;$backup_file&quot; # 백업 파일 강제 삭제
    fi

    # todo 파일 삭제
    rm -f backup/tasks/create-backup.todo # 트리거로써 역할 완수했으니 todo파일 삭제
    echo &quot;[Watcher] 백업 요청 완료 및 .todo 파일 삭제&quot;

  fi
  sleep 3  # 3초마다 체크
done&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;백업 파일 생성 기능이 실행되면 .todo 파일을 생성하기만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1745932073005&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// data-backup.service.ts

taskDir = path.resolve('backup/tasks'); // .todo 파일 위치

async createBackup(): Promise&amp;lt;boolean&amp;gt; {
  try {
    // tasks 폴더가 없으면 생성
    if (!fs.existsSync(this.taskDir)) {
      fs.mkdirSync(this.taskDir, { recursive: true });
    }

    // create-backup.todo 파일 경로 지정
    const todoPath = path.join(this.taskDir, 'create-backup.todo');

    // .todo 파일 생성 (백업 요청)
    fs.writeFileSync(todoPath, 'CREATE_BACKUP');

    return true;
  } catch (error) {
    throw new CusException();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker-compose.jpeg&quot; data-origin-width=&quot;215&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mL9J6/btsND2Q0PsK/Y3uiJkKegpn2fmKCQbWbdK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mL9J6/btsND2Q0PsK/Y3uiJkKegpn2fmKCQbWbdK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mL9J6/btsND2Q0PsK/Y3uiJkKegpn2fmKCQbWbdK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmL9J6%2FbtsND2Q0PsK%2FY3uiJkKegpn2fmKCQbWbdK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;215&quot; height=&quot;234&quot; data-filename=&quot;docker-compose.jpeg&quot; data-origin-width=&quot;215&quot; data-origin-height=&quot;234&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;</description>
      <category>개발/알쓸코잡</category>
      <category>DB BackUp</category>
      <category>docker</category>
      <category>docker-compose</category>
      <category>MySQL</category>
      <category>백업</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/321</guid>
      <comments>https://zynoob.tistory.com/321#entry321comment</comments>
      <pubDate>Fri, 9 May 2025 23:14:19 +0900</pubDate>
    </item>
    <item>
      <title>라즈베리파이 - OS 설치 방법과 원격 접속 방법(VNC)</title>
      <link>https://zynoob.tistory.com/322</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;홈서버를 구축해보고자, '일단' 라즈베리파이5를 구매했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 해당 설치 방식은 SD카드가 필요하니 참고할 것.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1YA8d/btsNHqQctYl/zIB5L9H48u6UsG4tNpkBTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1YA8d/btsNHqQctYl/zIB5L9H48u6UsG4tNpkBTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1YA8d/btsNHqQctYl/zIB5L9H48u6UsG4tNpkBTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1YA8d%2FbtsNHqQctYl%2FzIB5L9H48u6UsG4tNpkBTk%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;698&quot; height=&quot;355&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 해당 홈페이지에서 OS를 설치할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.raspberrypi.com/software/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.raspberrypi.com/software/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FMjm5/btsNHsAcb8j/9RkK1t133r9TcLu1X7gEx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FMjm5/btsNHsAcb8j/9RkK1t133r9TcLu1X7gEx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FMjm5/btsNHsAcb8j/9RkK1t133r9TcLu1X7gEx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFMjm5%2FbtsNHsAcb8j%2F9RkK1t133r9TcLu1X7gEx0%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;1394&quot; height=&quot;961&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;961&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;현재 자신의 PC에 맞는 운영체제를 선택 후, Raspberry Pi imager를 설치해준다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wmqUA/btsNGZSO6UY/D1Tluk3vKjraJ7s0jlmcM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wmqUA/btsNGZSO6UY/D1Tluk3vKjraJ7s0jlmcM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wmqUA/btsNGZSO6UY/D1Tluk3vKjraJ7s0jlmcM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwmqUA%2FbtsNGZSO6UY%2FD1Tluk3vKjraJ7s0jlmcM0%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;658&quot; height=&quot;421&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;421&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;자신의 라즈베리파이 디바이스 모델, 원하는 OS(추천하길래 64Bit OS로 설치)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 설치를 원하는 SD 카드를 선택한다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7VMIO/btsNGyPbWFM/hkUdwS2oLRbDvgNRByyQY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7VMIO/btsNGyPbWFM/hkUdwS2oLRbDvgNRByyQY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7VMIO/btsNGyPbWFM/hkUdwS2oLRbDvgNRByyQY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7VMIO%2FbtsNGyPbWFM%2FhkUdwS2oLRbDvgNRByyQY0%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;679&quot; height=&quot;474&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;474&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;883&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Se39H/btsNF9JaHdk/1SF7hzI5KrDbufySYzTNJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Se39H/btsNF9JaHdk/1SF7hzI5KrDbufySYzTNJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Se39H/btsNF9JaHdk/1SF7hzI5KrDbufySYzTNJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSe39H%2FbtsNF9JaHdk%2F1SF7hzI5KrDbufySYzTNJk%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;1284&quot; height=&quot;883&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;883&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;여자저차 세팅을 완료했는데, 키보드며 마우스, hdmi 선까지 온통 테이블 위에 올라가 있으니, 불편하다 느껴졌고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 타파하고자 원격으로 접속할 수 있는 방법을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이에 대해 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. VNC&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, VNC란, 원격 컴퓨터의 데스크탑 화면을 캡쳐하여 네트워크로 전송 &amp;amp; 입력(키보드, 마우스) 원격 컴퓨터로 전달하는 프로토콜을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, VNC 서버는 화면을 캡처하고 입력 이벤트를 수신하는 쪽으로 여기서는 라즈베리 파이.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 VNC 뷰어는 화면을 렌더링하고 키보드, 마우스 입력을 전송하는 쪽으로 여기서는 내 PC를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 작동 원리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동원리는 간략히 말하면 다음과 같은데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면의 경우 서버는 화면 출력을 일정 주기로 캡처하고, 이를 이미지로 압축한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 압축된 화면 데이터를 프레임 단위로 뷰어에게 전송하고, 뷰어는 이를 받아 디코딩해 화면으로 출력하는 순이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입출력도 비슷한데, 키보드 마우스 이벤트를 패킹해 서버로 전송하고, 서버는 이를 ㅂ다아 로컬 입력으로 주입한다.(가상 입력장치 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 필요 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 뷰어가 같은 네트워크에 존재해야 하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VNC포트가 열려있고, 방화벽에서 허용된 상태여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 적용 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 라즈베리파이에서 터미널을 열고 아래와 같은 명령어를 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746006983101&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install realvnc-vnc-server // vnc-server 다운
sudo raspi-config // 라즈베리파이 설정 창 열기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciNeb1/btsNGV4eUE0/ip5gQvqfo8jNJiftkeKaK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciNeb1/btsNGV4eUE0/ip5gQvqfo8jNJiftkeKaK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciNeb1/btsNGV4eUE0/ip5gQvqfo8jNJiftkeKaK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciNeb1%2FbtsNGV4eUE0%2Fip5gQvqfo8jNJiftkeKaK1%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;774&quot; height=&quot;374&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널은 좌측 상단에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5DcjF/btsNHfagXhW/9OYFkIqNkk1hvsIxCDTDJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5DcjF/btsNHfagXhW/9OYFkIqNkk1hvsIxCDTDJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5DcjF/btsNHfagXhW/9OYFkIqNkk1hvsIxCDTDJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5DcjF%2FbtsNHfagXhW%2F9OYFkIqNkk1hvsIxCDTDJ1%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;774&quot; height=&quot;374&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config 관련 명령어까지 입력했으면 위와 같은 창이 뜨는데, 여기서 Interface Option 탭으로 들어가,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VNC설정을 Enable로 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 터미널에서 아래의 명령어를 입력하면 라즈베리파이의 IP가 나타나는데 이를 기록해두자.&lt;/p&gt;
&lt;pre id=&quot;code_1746007174088&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hostname -I&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;다음 원격을 하기 위한 PC에서는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.realvnc.com/en/connect/download/viewer/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.realvnc.com/en/connect/download/viewer/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 홈페이지에서 자신의 OS에 맞는 VNC뷰어를 설치하고 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 위에 언급한 라즈베리파이의 IP를 VNC뷰어에 입력해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DDhig/btsNFRIMW2M/dZF0ZdEzeTfasWWtkzYdT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DDhig/btsNFRIMW2M/dZF0ZdEzeTfasWWtkzYdT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DDhig/btsNFRIMW2M/dZF0ZdEzeTfasWWtkzYdT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDDhig%2FbtsNFRIMW2M%2FdZF0ZdEzeTfasWWtkzYdT0%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;1920&quot; height=&quot;1074&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이와 같이 연결된 화면을 띄울 수가 있다.&lt;/p&gt;</description>
      <category>개발/알쓸코잡</category>
      <category>VNC</category>
      <category>라즈베리파이5</category>
      <category>라즈베리파이5 os</category>
      <category>라즈베리파이5 세팅</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/322</guid>
      <comments>https://zynoob.tistory.com/322#entry322comment</comments>
      <pubDate>Tue, 6 May 2025 20:10:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot - Custom Global exception Handler 적용</title>
      <link>https://zynoob.tistory.com/319</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring boot를 사용 중에 있어, 내 서버의 에러가 곧이 곧대로 반환되는 게 마음에 들지 않았다.&lt;/p&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;먼저, global exception handler를 적용하는 이유는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 만들다보면 어떤 이유에서든 예상하지 못한 예외가 발생할 수 있는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대해 매번 try-catch 로 직접 처리하는 것은 비효율적이고 지저분하며, 잊어버릴 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 서버 전역에서 예외를 통합 관리하는 구조가 필요한데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 기능을 해주는 것이 해당 핸들러다.&lt;/p&gt;
&lt;pre id=&quot;code_1745593031786&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CustomException.java
package com.chessy.exception;

public class CustomException extends RuntimeException {
  private final ErrorCode errorCode;

  public CustomException(ErrorCode errorCode) {
    super(errorCode.getMessage());
    this.errorCode = errorCode;
  }

  public CustomException(ErrorCode errorCode, String message) {
    super(message);
    this.errorCode = errorCode;
  }

  public ErrorCode getErrorCode() {
    return errorCode;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 에러를 어떻게 다룰 것인지에 대한 핸들러다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 코드 이름=NOT_FOUND_USER&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 코드 넘버=200 이라고 한다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 서비스 로직에서 에러 코드 이름을 CustomException에 전달하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스에서 에러 코드 이름에 대응하는 에러 코드 넘버를 가져와 반환토록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 코드를 매핑한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1745593256157&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.chessy.exception;

import lombok.Getter;

@Getter
public enum ErrorCode {
  USER_NOT_FOUND(200, &quot;사용자를 찾을 수 없습니다.&quot;),

  private final int code;
  private final String message;

  ErrorCode(int code, String message) {
    this.code = code;
    this.message = message;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 GlobalExceptionHandler를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745593335186&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.chessy.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
  // 의도된 에러
  @ExceptionHandler(CustomException.class)
  public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleCustomException(CustomException ex) {
    log.error(&quot;[CustomException] {} - cause: {}&quot;, ex.getMessage(), ex.getCause() != null ? ex.getCause() : &quot;-&quot;);

    return ResponseEntity
        .status(HttpStatus.INTERNAL_SERVER_ERROR) // HTTP Status는 500 고정
        .body(ErrorResponse.of(ex.getErrorCode().getCode()));
  }

  // 의도되지 않은 에러
  @ExceptionHandler(Exception.class)
  public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handleAllExceptions(Exception ex) {
    log.error(&quot;[UnknownException] {} - cause: {}&quot;, ex.getMessage(), ex.getCause() != null ? ex.getCause() : &quot;-&quot;);

    return ResponseEntity
        .status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR.getCode()));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소스 코드는 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 추가설명&lt;/p&gt;
&lt;pre id=&quot;code_1745593483990&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice =&amp;gt; 모든 @Controller, @RestController 에 적용되는 전용 어노테이션
	내부적으로 @Component가 포함되어 있어, 별도 bean 등록 없이 사용이 가능하다.

@ExceptionHandler =&amp;gt; 특정 예외를 캐치해서 처리할 수 있도록 도와주는 어노테이션
	어떤 타입의 예외를 잡을지 명시할 수 있다.
    
해당 코드의 경우, http 상태 코드는 에러 시 무조건 internal_server 에러로 반환토록 하고,
	내가 의도한 에러와 의도되지 않은 에러를 같은 방식으로 반환할 수 있게 작성했다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nDoAt/btsNBBkbful/BkH3kXEmnRZ3CNYBB0mhp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nDoAt/btsNBBkbful/BkH3kXEmnRZ3CNYBB0mhp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nDoAt/btsNBBkbful/BkH3kXEmnRZ3CNYBB0mhp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnDoAt%2FbtsNBBkbful%2FBkH3kXEmnRZ3CNYBB0mhp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;225&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/Spring boot</category>
      <category>exception</category>
      <category>Interceptor</category>
      <category>Java</category>
      <category>spring boot</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/319</guid>
      <comments>https://zynoob.tistory.com/319#entry319comment</comments>
      <pubDate>Sun, 4 May 2025 11:05:35 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot - Api docs 설정 (Swagger + OpenAPI)</title>
      <link>https://zynoob.tistory.com/320</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 스프링부트에서 Api docs를 설정하는 방식에 대해 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아마 다음 포스팅은 GraphQL 설정 방식에 대해 다룰 듯 싶다.)&lt;/p&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 docs를 사용하기 있어, 먼저 해당 기능 사용을 돕는 라이브러리를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745841653464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // Swagger 
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
    implementation 'org.springdoc:springdoc-openapi-starter-common:2.5.0'
    implementation 'io.swagger.core.v3:swagger-annotations:2.2.20'
    
    // 중략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, org.springdoc의 버전은 1.8.0 | 2.3.0 | 2.5.0 이 존재하니 자신이 원하는 버전으로 설치하면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Swagger UI 기본 경로 및 문서 경로를 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745841829157&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml
springdoc:
    swagger-ui:
        enabled: true
        path: /swagger-ui.html // Swagger ui 접근 경로
        operationsSorter: method // 메소드 정렬
        tagsSorter: alpha // 태그 알파벳순 정렬
        tryItOutEnabled: true
        filter: true // 필터
        urls:
            - url: /v3/api-docs 
              name: Chessy API
    api-docs:
        enabled: true
        path: /v3/api-docs // open api 스펙 json 경로
    default-produces-media-type: application/json
    default-consumes-media-type: application/json&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;@Tag, @Operation 어노테이션으로 Swagger가 적용됨을 명시하며, 설명을 덧붙일 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1745842296555&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AppController.java

package com.chessy.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.chessy.exception.CustomException;
import com.chessy.exception.ErrorCode;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = &quot;AppController&quot;, description = &quot;기본 API&quot;)
@RestController
@RequestMapping(&quot;/api/&quot;)
public class AppController {
  @Operation(summary = &quot;헬스 체커&quot;, description = &quot;애플리케이션 상태 확인 API&quot;)
  @GetMapping(&quot;/&quot;)

  public String healthCheck() {
    return &quot;ok&quot;;
  }

  @Operation(summary = &quot;테스트 API&quot;, description = &quot;예외 처리 테스트용 API&quot;)
  @GetMapping(&quot;/test&quot;)
  public String test() {
    throw new CustomException(ErrorCode.USER_NOT_FOUND);
  }

}&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 id=&quot;code_1745841867632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/swagger-ui.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 언제든 api 버전 관리는 필요하기 때문에 커스텀 하는 방법도 소개한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745841956369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Spring.Config.java

@Configuration
public class SpringConfig implements WebMvcConfigurer {

  // 중략

  @Bean
  public OpenAPI customOpenAPI() {
    return new OpenAPI()
        .info(new Info()
            .title(&quot;Test API&quot;) // 제목
            .version(&quot;1.0.0&quot;) // 버전
            .description(&quot;API 문서&quot;) // 설명
            .contact(new Contact()
                .name(&quot;zynoobb&quot;) // 작성자
                .email(&quot;jw9491@gmail.com&quot;)) // 연락처
            .license(new License()
                .name(&quot;Apache 2.0&quot;)
                .url(&quot;http://springdoc.org&quot;)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 방식으로 해당 내용을 추가해줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, 위 html 주소를 일일히 외워쓰기 번거롭다면 아래와 같은 방법을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1745842034597&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SwaggerController.java

package com.**.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SwaggerController {

  @GetMapping(&quot;/api&quot;)
  public String redirectSwagger() {
    return &quot;redirect:/swagger-ui/index.html&quot;;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/api 로 접속 시, 내 swagger 주소로 리디렉션하는 방식으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URI 뒤에 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;ps. 자바스크립트 기반 프레임워크 같은 경우는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swagger를 작성함에 있어 api에서 사용하는 인풋값의 타입을 직접 지정해주어야 하는 불편함이 있었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트의 경우 아직까지는 그럴 필요가 없어서 꽤 만족스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPG2y/btsNBzuQS5u/qLVkdKyy0PKVZTl4KCPK00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPG2y/btsNBzuQS5u/qLVkdKyy0PKVZTl4KCPK00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPG2y/btsNBzuQS5u/qLVkdKyy0PKVZTl4KCPK00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPG2y%2FbtsNBzuQS5u%2FqLVkdKyy0PKVZTl4KCPK00%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;1958&quot; height=&quot;1564&quot; data-origin-width=&quot;1958&quot; data-origin-height=&quot;1564&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYwcVq/btsNDvkm8so/otwZxngKOkjhlHD8xPE6i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYwcVq/btsNDvkm8so/otwZxngKOkjhlHD8xPE6i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYwcVq/btsNDvkm8so/otwZxngKOkjhlHD8xPE6i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYwcVq%2FbtsNDvkm8so%2FotwZxngKOkjhlHD8xPE6i1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;225&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>개발/Spring boot</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/320</guid>
      <comments>https://zynoob.tistory.com/320#entry320comment</comments>
      <pubDate>Thu, 1 May 2025 22:13:02 +0900</pubDate>
    </item>
    <item>
      <title>Spring boot - Docker-compose hot loading 환경 설정(with, vscode)</title>
      <link>https://zynoob.tistory.com/318</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 몇날 며칠 숙고했던 Spring boot 에서의 Docker 환경 설정에 대해 다룬다.&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재로딩 자체는 의도했던 것이었지만, 변경된 소스 코드가 적용되지 않은 채 변경되었다는 게 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 마침내 승리한 기록을 남기고자, 미래에 나에게 전하고자 중점 소스 코드 위주로 해당 포스팅을 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. build.gradle 설정을 아래와 같이 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745589209721&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Spring Boot DevTools 의존성 추가
implementation 'org.springframework.boot:spring-boot-devtools'

// bootRun 태스크 설정
bootRun {
    dependsOn 'classes'
    sourceResources sourceSets.main
}

// 소스 코드 변경 감지 태스크
tasks.register('watch') {
    doLast {
        println '소스코드 변경 감지 중'
        exec {
            executable = 'gradle'
            args = ['build', '--continuous']
        }
    }
}&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;2. Dockerfile 을 아래와 같이 수정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745589242358&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 베이스 이미지 (Gradle + JDK 17 포함)
FROM gradle:8.6-jdk17 as builder 

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 설치를 위해 gradle 관련 파일만 먼저 복사
COPY build.gradle settings.gradle gradlew gradlew.bat ./
COPY gradle ./gradle

# 의존성만 미리 다운 (캐시 최적화)
RUN ./gradlew dependencies --no-daemon || return 0

# 소스 복사
COPY src ./src

# 실행 포트 오픈
EXPOSE 8080

# gradlew 실행권한 부여
RUN chmod +x ./gradlew

# 환경변수 설정 (optional, devtools)
ENV SPRING_DEVTOOLS_RESTART_ENABLED=true
ENV SPRING_DEVTOOLS_LIVERELOAD_ENABLED=true

# 어플리케이션 실행
CMD [&quot;./gradlew&quot;, &quot;bootRun&quot;, &quot;--continuous&quot;, &quot;--no-daemon&quot;]

# --continuous =&amp;gt; 소스 코드를 지속적으로 감지
# --no-daemon =&amp;gt; grdale 데몬을 사용하지 않고 직접 실행
# (빌드 속도는 빠르나, Docker 컨테이너 자체가 일회성이 잦기에 데몬 유지가 의미 없을 뿐더러 메모리 누수 방지)&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;3. docker-compose.yaml 파일 설정&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745589418588&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.8'

services:
  app:
    container_name: ${container_name}
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - &quot;8080:8080&quot;
    environment:
      SPRING_PROFILES_ACTIVE: dev
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/${DB_NAME}
      SPRING_DATASOURCE_USERNAME: ${DB_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./src:/app/src
      - ./gradle:/app/gradle
      - ./build.gradle:/app/build.gradle
      - ./settings.gradle:/app/settings.gradle
      - ./gradlew:/app/gradlew
      - ./gradlew.bat:/app/gradlew.bat
      - ./build:/app/build
      - ~/.gradle:/root/.gradle
    env_file:
      - .env
    depends_on:
      - db
    command: sh -c &quot;./gradlew classes --continuous --no-daemon &amp;amp; ./gradlew bootRun --no-daemon&quot;

// 이하 중략&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일에 대해 설명을 하자면,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745589509488&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 볼륨
./src:/app/src =&amp;gt; 소스 코드 변경을 실시간으로 반영토록 하는 볼륨
~/.gradle:/root/.gradle =&amp;gt; gradle 캐시 공유
기타 =&amp;gt; gradle 관련 파일 &amp;amp; 설정 유지

# 명령어
./gradlew classes --continuous --no-daemon: 소스 코드 변경 감지
./gradlew bootRun --no-daemon: 애플리케이션 실행&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;추신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- nest.js할 때가 좋았다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ywvia/btsNBaAm3Vo/OdXKi6irs6A2rk7TOKXwtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ywvia/btsNBaAm3Vo/OdXKi6irs6A2rk7TOKXwtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ywvia/btsNBaAm3Vo/OdXKi6irs6A2rk7TOKXwtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fywvia%2FbtsNBaAm3Vo%2FOdXKi6irs6A2rk7TOKXwtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;225&quot; data-filename=&quot;srpig boot.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Spring boot</category>
      <category>docker</category>
      <category>docker-compose</category>
      <category>hot loading</category>
      <category>Java</category>
      <category>spring boot</category>
      <category>VSCode</category>
      <author>자전하는명왕성</author>
      <guid isPermaLink="true">https://zynoob.tistory.com/318</guid>
      <comments>https://zynoob.tistory.com/318#entry318comment</comments>
      <pubDate>Fri, 25 Apr 2025 23:03:00 +0900</pubDate>
    </item>
  </channel>
</rss>