<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Joo's Story</title>
    <link>https://blog.joostory.net/</link>
    <description>개인적인 관심사에 관해서 이런저런 이야기를 하고 있습니다.
#집밥 #개발자 #Ubuntu</description>
    <language>ko</language>
    <pubDate>Sat, 10 Oct 2026 02:17:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Joo</managingEditor>
    <image>
      <title>Joo's Story</title>
      <url>https://tistory1.daumcdn.net/tistory/22084/attach/b99d6461ad6441cdad8f21ccbd483ba6</url>
      <link>https://blog.joostory.net</link>
    </image>
    <item>
      <title>서버개발자를 위한 운영툴 개발</title>
      <link>https://blog.joostory.net/629</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;(2022/6/30 사내 테크톡에서 발표한 내용을 블로그에도 작성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 카카오 N TF에서 서버 개발을 담당하고 있는 Joo 입니다.&lt;br /&gt;저는 다음에서 한때 한메일 익스프레스였던 다음메일 개발을 시작으로 브런치, 티스토리, My구독을 거쳐 최근에는 N TF에서 새로운 서비스를 개발하고 있는데요. 서비스 개발을 10여년, 운영툴 개발도 10여년 한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;서버 개발자를 위한 운영툴 개발&amp;rdquo;이라는 주제로 그동안 운영툴 개발을 하면서 고민했던 부분들에 대한 이야기를 나누어 보고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영툴 어떻게 만들고 계신가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇달을 고생해서 드디어 서비스를 오픈합니다. 서비스를 오픈했다는 기쁨도 잠시, 바로 운영이슈가 쏟아지기 시작합니다. &amp;ldquo;공지사항 수정해주세요.&amp;rdquo; &amp;ldquo;배너 다른 이미지로 교체해주세요.&amp;rdquo; &amp;ldquo;OOO 통계 뽑아주실 수 있나요?&amp;rdquo;, &amp;ldquo;이 사용자 정보 확인해주세요.&amp;rdquo; 운영 이슈만 있는 것도 아닙니다. 미처 발견하지 못했던 자잘한 버그들도 수정해야하고 서비스 로직 수정이 바로 필요한 경우도 생기게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer01.png&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTfun7/dJMcajqhiLd/ymLrBcwGFJXJtm9HW1WhAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTfun7/dJMcajqhiLd/ymLrBcwGFJXJtm9HW1WhAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTfun7/dJMcajqhiLd/ymLrBcwGFJXJtm9HW1WhAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTfun7%2FdJMcajqhiLd%2FymLrBcwGFJXJtm9HW1WhAk%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;1846&quot; height=&quot;348&quot; data-filename=&quot;admin-for-server-developer01.png&quot; data-origin-width=&quot;1846&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 잘 만든 운영툴이 있다면 수많은 운영이슈를 개발자가 직접 처리하지 않아도 됩니다. 운영자(보통은 기획자)도 운영이슈를 직접 처리할 수 있어 빠르게 해결할 수 있습니다. 운영툴은 운영-개발 사이의 커뮤니케이션 비용을 줄이는 데 큰 역할을 합니다. 운영 속도는 빨라지고, 개발자는 운영이슈를 직접 처리하지 않아도 되어 서비스 개선 이슈에 집중할 수 있습니다. 잘만들면 잘 만들수록 커뮤니케이션 비용은 낮아지고 개발자의 시간은 늘어납니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 운영툴이 좋은 운영툴일까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 서비스 기획단계에서 만들어진 운영툴 기획안의 요구사항을 충족해야합니다. 운영툴 기획안이 없다면 운영 요구사항을 파악하고 추가되는 요구사항을 충족하도록 해야합니다. 거기다 사용하기 편한 UX, UI를 가진다면 더 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 마치 하나의 서비스를 만드는 것과 비슷합니다. 운영자를 위한 서비스를 개발하는 것입니다. 조금 너그러운 사용자를 위한 풀스택 개발자가 되었다는 생각으로 운영툴을 만든다면 좋은 운영툴이 만들어지리라 생각합니다. 여기서 말하는 풀스택은 기획, 디자인, 프론트엔드 개발, 백엔드 개발 모두를 포함합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 프론트엔드 개발, 백엔드 개발, 사용하기 편리한 UX를 만드는 과정으로 나눠 저의 경험을 토대로 이야기를 나눠보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트엔드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프론트엔드 프레임워크의 선택과 UI 디자인에 대해 이야기 해보려고 합니다. 운영툴을 잘 만들자고 했지만 운영툴에 많은 시간을 투자할 수는 없습니다. 최소한의 노력으로 최대의 효과를 얻는 방법을 찾아봐야합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프론트엔드 프레임워크 선택&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer04.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfPGII/dJMcabTrsra/t1pXyAZJ4CRfwEYFOQTFG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfPGII/dJMcabTrsra/t1pXyAZJ4CRfwEYFOQTFG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfPGII/dJMcabTrsra/t1pXyAZJ4CRfwEYFOQTFG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfPGII%2FdJMcabTrsra%2Ft1pXyAZJ4CRfwEYFOQTFG0%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;1948&quot; height=&quot;792&quot; data-filename=&quot;admin-for-server-developer04.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영툴 만들때 많이 사용하는 방법이 운영툴 서버를 mvc로 만드는 것입니다. 템플릿엔진을 통해서 값을 전달하고 jQuery로 브라우저에 표시된 UI를 조작하고 css로 UI를 디자인합니다. 여기서 주목해볼 점은 jQuery인데요. jQuery는 파편화된 브라우저 API를 추상화해서 제공하기 위해 만들어져 한때 프론트엔드 개발의 표준언어처럼 사용되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer05.png&quot; data-origin-width=&quot;2430&quot; data-origin-height=&quot;1436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/voSCz/dJMcab0l9a9/CD6efBMKooxNSRKbwixMk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/voSCz/dJMcab0l9a9/CD6efBMKooxNSRKbwixMk1/img.png&quot; data-alt=&quot;( https://gs.statcounter.com/browser-market-share/all/south-korea )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/voSCz/dJMcab0l9a9/CD6efBMKooxNSRKbwixMk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvoSCz%2FdJMcab0l9a9%2FCD6efBMKooxNSRKbwixMk1%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;2430&quot; height=&quot;1436&quot; data-filename=&quot;admin-for-server-developer05.png&quot; data-origin-width=&quot;2430&quot; data-origin-height=&quot;1436&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;( https://gs.statcounter.com/browser-market-share/all/south-korea )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제는 브라우저의 파편화는 거의 사라졌다고 할 정도로 다양한 브라우저가 표준화된 API를 지원하고 있습니다. 또한 대부분 webkit 엔진, 크롬을 베이스로 하고 있어서 거의 차이가 없다고 볼 수 있습니다. 파편화의 주범인 IE의 점유율이 1% 미만으로 떨어졌습니다. 더이상 jQuery를 사용할 필요가 없어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 어떤 프론트엔드 프레임워크를 사용해야할까요? 사용하지 않아도 된다지만 그래도 편리한 jQuery? 아니면 브라우저가 지원하는 표준화된 Javascript를 사용할 수도 있겠죠. 이런 것은 어떨까요? UI를 컴포넌트 단위로 관리하는 &lt;a href=&quot;https://reactjs.org/&quot;&gt;React&lt;/a&gt;, &lt;a href=&quot;https://vuejs.org/&quot;&gt;Vuejs&lt;/a&gt;, &lt;a href=&quot;https://angularjs.org/&quot;&gt;Angular&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt;와 같은 프레임워크가 있습니다. 먼저 프론트엔드 프레임워크가 얼마나 사용되고 있는지 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/posts/admin-for-server-developer/admin-for-server-developer06.png&quot; alt=&quot;&quot; /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer06.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqYGrh/dJMb9987APp/CYAQQyhgpR5XzOx3KxU7dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqYGrh/dJMb9987APp/CYAQQyhgpR5XzOx3KxU7dk/img.png&quot; data-alt=&quot;( https://www.npmtrends.com/react-vs-jquery-vs-vue-vs-@angular/core-vs-svelte )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqYGrh/dJMb9987APp/CYAQQyhgpR5XzOx3KxU7dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqYGrh%2FdJMb9987APp%2FCYAQQyhgpR5XzOx3KxU7dk%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;2586&quot; height=&quot;906&quot; data-filename=&quot;admin-for-server-developer06.png&quot; data-origin-width=&quot;2586&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;( https://www.npmtrends.com/react-vs-jquery-vs-vue-vs-@angular/core-vs-svelte )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jQuery가 아직도 사용되고 있긴하지만 React가 압도적으로 가장 많이 사용되고 있습니다. 이것을 근거로 저는 React를 선택했습니다. 가장 많이 사용된다는 것은 가장 많은 정보가 있다는 것이고 가장 많은 정보가 있다는 것은 문제가 생기면 도움을 얻기 쉽다는 것이기도 하기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React를 사용한 운영툴 개발환경&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer07.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HGUjl/dJMcablIV3R/9cbCKAXVV6wwlrKy7HoXik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HGUjl/dJMcablIV3R/9cbCKAXVV6wwlrKy7HoXik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HGUjl/dJMcablIV3R/9cbCKAXVV6wwlrKy7HoXik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHGUjl%2FdJMcablIV3R%2F9cbCKAXVV6wwlrKy7HoXik%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;1570&quot; height=&quot;626&quot; data-filename=&quot;admin-for-server-developer07.png&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 React를 사용한 운영툴 서버 구성에 대해서 살펴보겠습니다. 먼저 운영툴 서버는 API만 제공하고 static 리소스를 제공하는 프론트엔드 서버에서 이 API를 사용하도록 구성합니다. 이런 구성을 쉽게 할 수 있도록 react는 create-react-app이라는 툴체인을 제공합니다. create-react-app으로 앱을 생성한 후에 start를 실행하면 3000포트로 node 서버가 react 앱을 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;&amp;gt; brew install node # node 설치

&amp;gt; npx create-react-app admin-ui # ui앱 생성
&amp;gt; cd admin-ui
&amp;gt; npm run start # 개발환경 앱 실행
localhost:3000...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱은 hot reload가 적용되어 있어서 코드를 수정하면 즉시 앱에 반영해줍니다. 운영툴 서버 API에 접근할 수 있도록 운영툴 서버에 CORS 설정만 적용해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이대로 운영툴 서버를 운영해도 물론 문제는 없겠지만 그래도 프로덕션으로 배포를 하는 방법도 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer08.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba6p4s/dJMb99OKnTs/Qv7mkpCnedKEMKxqCL5wbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba6p4s/dJMb99OKnTs/Qv7mkpCnedKEMKxqCL5wbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba6p4s/dJMb99OKnTs/Qv7mkpCnedKEMKxqCL5wbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba6p4s%2FdJMb99OKnTs%2FQv7mkpCnedKEMKxqCL5wbk%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;1598&quot; height=&quot;548&quot; data-filename=&quot;admin-for-server-developer08.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;gt; npm run build
&amp;gt; cp -R build ../src/main/resource/static&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create-react-app에서 미리 만들어둔 빌드스크립트를 사용하면 되는데요. 빌드 스크립트를 수행하면 모든 static 리소스가 build 디렉토리에 만들어집니다. 이 디렉토리를 운영툴의 static 리소스에 그대로 복사하면 완료입니다.&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;// index.js
import React from 'react'
import { createRoot } from 'react-dom
import App from './container/App'

const root = createRoot(document.getElementById('root'))
root.render(
    &amp;lt;App /&amp;gt;
)

// /container/App.js
function App() {
    return (
        &amp;lt;div&amp;gt;Hello, World!&amp;lt;/div&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 귀찮고 어려운 개발환경설정이 완료되었습니다. 이제 React Component로 운영툴 UI를 만들어가면 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UI 디자인 프레임워크 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 디자인을 해결해주진 않습니다. 단지 UI를 컴포넌트 단위로 관리하기 편하게 만들어줄 뿐이죠. 이왕 만드는거 조금 더 보기 좋게 만들고 싶은데 디자이너도 없고 마크업 개발자도 없습니다. 결국 쉽게 사용할 수 있는 디자인 템플릿을 찾아야 한다는 결론에 이르렀습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer09.png&quot; data-origin-width=&quot;2622&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pUZEF/dJMcahTwFUV/MZQI3Uua3YY879YCKERpkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pUZEF/dJMcahTwFUV/MZQI3Uua3YY879YCKERpkK/img.png&quot; data-alt=&quot;( https://www.npmtrends.com/bootstrap-vs-tailwindcss-vs-@material-ui/core-vs-@mui/material-vs-antd )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pUZEF/dJMcahTwFUV/MZQI3Uua3YY879YCKERpkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpUZEF%2FdJMcahTwFUV%2FMZQI3Uua3YY879YCKERpkK%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;2622&quot; height=&quot;910&quot; data-filename=&quot;admin-for-server-developer09.png&quot; data-origin-width=&quot;2622&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;( https://www.npmtrends.com/bootstrap-vs-tailwindcss-vs-@material-ui/core-vs-@mui/material-vs-antd )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 프레임워크와 달리 UI 디자인을 제공하는 프레임워크는 절대 강자가 존재하지 않았습니다. 물론 부트스트랩이 여전히 가장 많이 사용되긴 하지만 tailwind 의 사용자도 만만치 않습니다. 그래서 하나씩 살펴보면서 어떤 것을 사용하는게 좋을지 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer10.png&quot; data-origin-width=&quot;2322&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdrKxn/dJMcaf9flQZ/z6LQ684N0DX8TZNscxPGi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdrKxn/dJMcaf9flQZ/z6LQ684N0DX8TZNscxPGi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdrKxn/dJMcaf9flQZ/z6LQ684N0DX8TZNscxPGi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdrKxn%2FdJMcaf9flQZ%2Fz6LQ684N0DX8TZNscxPGi0%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;2322&quot; height=&quot;562&quot; data-filename=&quot;admin-for-server-developer10.png&quot; data-origin-width=&quot;2322&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://getbootstrap.com/&quot;&gt;bootstrap&lt;/a&gt;은 반응형 웹디자인을 위한 css 추상화를 목표로 개발되었습니다. 컴포넌트 단위의 class를 제공해서 html에 class만 지정하면 원하는 디자인을 만들 수 있습니다. 주로 jQuery와 같이 사용되었지만 지금은 관계가 끝어지고 있고 React를 위한 버전도 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer11.png&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNVCSh/dJMcaiZcE0x/Rk4Wtv9VSzWlXDGReHbhX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNVCSh/dJMcaiZcE0x/Rk4Wtv9VSzWlXDGReHbhX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNVCSh/dJMcaiZcE0x/Rk4Wtv9VSzWlXDGReHbhX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNVCSh%2FdJMcaiZcE0x%2FRk4Wtv9VSzWlXDGReHbhX0%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;2320&quot; height=&quot;606&quot; data-filename=&quot;admin-for-server-developer11.png&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;tailwind&lt;/a&gt;입니다. tailwind는 모던 웹디자인을 위한 css 추상화를 제공합니다. bootstrap과 유사하게 html에 class를 지정하는 것으로 디자인을 만듭니다. 다른 점은 컴포넌트 단위의 디자인을 외부에 위임했다는 것입니다. tailwind는 css 단위의 기능만 제공합니다. 그리고 빌드 결과물에 bootstrap이 모든 css파일을 포함했다면 tailwind는 사용한 css만 포함해 결과물 용량을 절약할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer12.png&quot; data-origin-width=&quot;1792&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb8UdU/dJMcaf2xXa8/5p3PDoDKqJsIYmKp2AAtbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb8UdU/dJMcaf2xXa8/5p3PDoDKqJsIYmKp2AAtbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb8UdU/dJMcaf2xXa8/5p3PDoDKqJsIYmKp2AAtbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb8UdU%2FdJMcaf2xXa8%2F5p3PDoDKqJsIYmKp2AAtbk%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;1792&quot; height=&quot;978&quot; data-filename=&quot;admin-for-server-developer12.png&quot; data-origin-width=&quot;1792&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://material.io/&quot;&gt;Material Design&lt;/a&gt;에 대해서는 들어보셨을겁니다. 구글이 만든 디자인 가이드라인인데요. 대부분의 구글 프로덕트에 사용되고 있죠. &lt;a href=&quot;https://mui.com/&quot;&gt;Material UI&lt;/a&gt;는 이 디자인의 React 구현체입니다. 컴포넌트 단위로 디자인을 제공하고 이를 배치만 하면 됩니다. html과 css는 거의 몰라도 되고 Material UI에서 제공하는 방식을 알아야합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer13.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZN4Kh/dJMcafIrNIH/xXqSjURwyBqZau3Zn0XWlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZN4Kh/dJMcafIrNIH/xXqSjURwyBqZau3Zn0XWlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZN4Kh/dJMcafIrNIH/xXqSjURwyBqZau3Zn0XWlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZN4Kh%2FdJMcafIrNIH%2FxXqSjURwyBqZau3Zn0XWlk%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;2400&quot; height=&quot;710&quot; data-filename=&quot;admin-for-server-developer13.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;a href=&quot;https://ant.design/&quot;&gt;Ant Design&lt;/a&gt;에 대해서 알아보겠습니다. Ant Design은 알리페이를 만든 중국의 앤트그룹이 만든 디자인 가이드입니다. 같은 이름으로 React 구현체를 제공하고 있습니다. Material UI와 마찬가지로 컴포넌트 단위의 디자인을 제공합니다. 특이한 점은 디자인 뿐만 아니라 데이터 처리에 대한 인터페이스도 제공을 하고 있습니다. 테이블 컴포넌트에 데이터 소스만 제공하면 테이블 UI가 만들어지는 식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer14.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ddi96/dJMcadX5wvZ/qWyYByKGIxz7t3FNsMgHm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ddi96/dJMcadX5wvZ/qWyYByKGIxz7t3FNsMgHm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ddi96/dJMcadX5wvZ/qWyYByKGIxz7t3FNsMgHm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDdi96%2FdJMcadX5wvZ%2FqWyYByKGIxz7t3FNsMgHm1%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;1836&quot; height=&quot;838&quot; data-filename=&quot;admin-for-server-developer14.png&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 어떤 UI 프레임워크를 사용해야할지 선택을 해야합니다. bootstrap, tailwind는 직접 디자인을 해야합니다. Material UI는 styled component 방식이라 학습이 어렵습니다. 최소한의 노력으로 최대의 효과를 보고자 한다면 아무래도 디자인 템플릿을 제공하고 사용이 편리한 Ant Design이 좋다는 생각입니다. 물론 서비스 프론트엔드 개발을 한다면 tailwind가 더 좋지 않을까 하는 생각이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;And Design&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ant Design의 사용에 대해 간략히 알아보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;&amp;gt; npm i antd @ant-design/icons&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;And Design을 사용하려면 antd와 icons 모듈을 설치합니다. 이제 간단한 UI를 만들어보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer15.png&quot; data-origin-width=&quot;2132&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfyhZf/dJMcaiLJwsK/lTIfHfWyKN4IzYR2kskxt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfyhZf/dJMcaiLJwsK/lTIfHfWyKN4IzYR2kskxt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfyhZf/dJMcaiLJwsK/lTIfHfWyKN4IzYR2kskxt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfyhZf%2FdJMcaiLJwsK%2FlTIfHfWyKN4IzYR2kskxt0%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;2132&quot; height=&quot;1120&quot; data-filename=&quot;admin-for-server-developer15.png&quot; data-origin-width=&quot;2132&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function AdminLayout({children}) {
  return (
    &amp;lt;Layout&amp;gt;
      &amp;lt;Layout.Header&amp;gt;
        &amp;lt;div style={{ color:'#fff' }}&amp;gt;
          운영툴
        &amp;lt;/div&amp;gt;
      &amp;lt;/Layout.Header&amp;gt;
      &amp;lt;Layout&amp;gt;
        &amp;lt;Layout.Sider&amp;gt;
          &amp;lt;AdminSideMenus /&amp;gt;
        &amp;lt;/Layout.Sider&amp;gt;
        &amp;lt;Layout&amp;gt;
          &amp;lt;Layout.Content style={{ margin: '24px 16px 0' }}&amp;gt;
            {children}
          &amp;lt;/Layout.Content&amp;gt;
          &amp;lt;Layout.Footer style={{ textAlign:'center'}}&amp;gt;
            N TF
          &amp;lt;/Layout.Footer&amp;gt;
        &amp;lt;/Layout&amp;gt;
      &amp;lt;/Layout&amp;gt;
    &amp;lt;/Layout&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 레이아웃을 만들어보았는데요. 운영툴에서 흔히 사용하게 되는 GNB, LNB, 컨텐츠, 푸터가 있는 레이아웃을 Layout이라는 컴포넌트로 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;const ADMIN_MENU = [
  {
    key: 'members-group',
    icon: &amp;lt;UserOutlined /&amp;gt;,
    label: '회원',
    children: [
      {
        key: '/members',
        label: &amp;lt;NavLink to={&quot;/members&quot;}&amp;gt;회원관리&amp;lt;/NavLink&amp;gt;
      }
    ]
  }
]

export default function AdminSideMenus() {
  const location = useLocation()
  const [selectedKey, setSelectedKey] = useState(&quot;/&quot;)

  useEffect(() =&amp;gt; {
    function handleLocationChange(pathname) {
      let names = pathname.split(&quot;/&quot;)
      setSelectedKey((names != null) ? `/${names[1]}` : '/')
    }

    handleLocationChange(location.pathname)
  }, [location])

  return (
    &amp;lt;Menu
      mode='inline' selectedKeys={[selectedKey]}
      items={ADMIN_MENU}
    /&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LNB의 메뉴도 Menu라는 컴포넌트에 메뉴 데이터와 선택된 메뉴 키를 넘겨주는 것만으로 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function MemberList() {
  const [members, setMembers] = useState([])
  useEffect(() =&amp;gt; {
    graphQlQuery(`
      query {
        members {
          id
          name
        }
      }
    `)
      .then(data =&amp;gt; setMembers(data.data.members))
  }, [])


  return (
    &amp;lt;Table dataSource={members} pagination={false} rowKey={'id'}&amp;gt;
      &amp;lt;Table.Column title={'id'} dataIndex={'id'} /&amp;gt;
      &amp;lt;Table.Column title={'name'} dataIndex={'name'} /&amp;gt;
    &amp;lt;/Table&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본문에 사용된 컴포넌트도 살펴보겠습니다. 서버로부터 회원 데이터를 요청해서 Table 컴포넌트에 그대로 넘겨주면 회원 테이블 표시도 완료됩니다. 간략하게 살펴봤지만 코드 몇줄로 그럴듯한 UI가 만들지는 것을 알 수 있습니다. 이런 쉬운 사용이 Ant Design의 장점입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 디자인은 프레임워크의 도움을 받아 해결했으니 이제 API에 대해서 알아보겠습니다. API의 선택은 서버 개발자에게는 너무나 쉬운 선택입니다. REST API를 사용하고 Gateway나 프록시 설정을 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest API로 운영툴을 개발하는 과정은 UI 컴포넌트 개발, CRUD API 개발의 반복입니다. UI 에서 표시하는 값을 바꾸면 그에 따라 API도 바꾸고 연관된 값을 표시하기 위해서 API를 추가하고, 또 UI를 추가하고 또 API를 추가하고&amp;hellip; 이렇게 계속 반복하다보니 문득 &amp;ldquo;누가 UI에 맞게 API 만들어주면 좋겠다&quot;라는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 GraphQL이 해결해줄 수 있겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GraphQL&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer16.png&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wsazG/dJMcacdJGF8/8OclhkjbMmGrlmEWvikhU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wsazG/dJMcacdJGF8/8OclhkjbMmGrlmEWvikhU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wsazG/dJMcacdJGF8/8OclhkjbMmGrlmEWvikhU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwsazG%2FdJMcacdJGF8%2F8OclhkjbMmGrlmEWvikhU1%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;1248&quot; height=&quot;402&quot; data-filename=&quot;admin-for-server-developer16.png&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://graphql.org/&quot;&gt;GraphQL&lt;/a&gt;은 Facebook이 2012년 개발해서 2015년에 공개한 query language입니다. 주요한 특징으로 필요한 데이터만 받아올 수 있다는 점과 한번에 필요한 데이터를 모두 요철할 수 있다는 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL을 운영툴에 사용해보면 어떨까요? 운영툴 기능이 추가/변경되어도 API를 추가하거나 변경하지 않아도 될 수 있습니다. 그리고 하나의 엔드포인트로 데이터 제공이 가능해서 API Gateway를 위한 여러 설정을 하지 않아도 됩니다. 게다가 때마침 GraphQL Java를 베이스로 한 &lt;a href=&quot;https://spring.io/projects/spring-graphql&quot;&gt;Spring for GraphQL의 1.0 릴리즈가 개발&lt;/a&gt;되어 도입을 해보았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GraphQL의 사용&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;type Query {
    searchProfile(q: String, size: Int, pivot: Int): SearchProfileResponse
    profile(sid: String): Profile
    profileByUserName(userName: String): Profile
    room(roomSid: String): Room
    startedRooms(profileSid: String, from: DateTime, to: DateTime): [Room]
    profileBadges: [ProfileBadge]
    profileBadge(profileBadgeNid: Int): ProfileBadge
}

type Mutation {
    attachRelatedProfile(profileBadgeNid: Int, profileSid: String): Profile
    detachRelatedProfile(profileBadgeNid: Int, profileSid: String): Profile
}

scalar DateTime

type Profile {
    profileNid: Int
    profileSid: ID
    userNid: Int
    userName: String
    displayName: String
    badges: [ProfileBadge]
    createdAt: DateTime
}

type ProfileBadge {
    profileBadgeNid: Int
    description: String
    keyword: String
    createdAt: DateTime
    profiles: [Profile]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 간단히 Spring for GraphQL로 API를 만든 과정을 소개하겠습니다. GraphQL API를 제공하기 위해서는 먼저 스키마 정의를 해야합니다. .graphqls 파일에 Query, Mutation, Scalar, Type을 정의합니다. Query, Mutation 타입은 하나만 존재해야하는데 모든 요청의 시작점이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@QueryMapping(&quot;profile&quot;)
fun profile(@Argument sid: String): InternalProfileResponse {
    val profile = profileService.getProfile(sid)
    val restriction = BanDTO(bannedUserChecker.isBannedUser(profile.userNid))
    val involvedList: List&amp;lt;Room&amp;gt; = profileScheduleService.getInvolvedRoomList(sid)
    val supportResponse = IntegrationSupportResponse(supportReceiverUtils.isSupportReceiver(profile), profile.supports)
    return InternalProfileResponse.from(profile, restriction, involvedList, supportResponse)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query와 Mutation 타입은 각각 QueryMapping, MutationMapping으로 구현과 연결합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;type Profile {
    profileNid: Int
    profileSid: ID

    badges: [ProfileBadge] # Profile Entity/DTO에는 존재하지 않는다.

    createdAt: DateTime
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SchemaMapping(typeName = &quot;Profile&quot;, field = &quot;badges&quot;)
fun profileBadgesByProfile(profile: InternalProfileResponse): List&amp;lt;ProfileBadge&amp;gt; {
    return profileBadgeService.getProfileBadgeListByProfileSid(profile.profileSid).badgeList
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API와의 비교에서 보여드렸던 것 처럼 각 스키마를 연결할 수 있는 있는데요. SchemaMapping으로 Entity변경이나 DTO 추가없이 type을 연결할 수 있습니다. 클라이언트가 해당 필드를 요청하면 연결된 메소드가 호출되게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;scalar DateTime&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;private val scalarDateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;)
private val scalarDateTimeZoneId: ZoneId = ZoneId.of(&quot;Asia/Seoul&quot;)

private class DateTimeCoercing : Coercing&amp;lt;ZonedDateTime, String&amp;gt; {
    // ZonedDateTime -&amp;gt; String
    override fun serialize(value: Any): String? = when(value) {
        is ZonedDateTime -&amp;gt; value.format(scalarDateTimeFormat)
        else -&amp;gt; null
    }

    // String -&amp;gt; ZonedDateTime
    override fun parseValue(value: Any): ZonedDateTime = parseLiteral(value)

    override fun parseLiteral(value: Any): ZonedDateTime = when(value) {
        is StringValue -&amp;gt; LocalDateTime.parse(value.value, scalarDateTimeFormat).atZone(scalarDateTimeZoneId)
        else -&amp;gt; throw CoercingParseLiteralException(&quot;Unknown Literal&quot;)
    }
}

@Component
class CustomScalarConfigurer : RuntimeWiringConfigurer {
    override fun configure(builder: RuntimeWiring.Builder) {
        builder.scalar(GraphQLScalarType.newScalar() // scalar의 coercing등록
                        .name(&quot;DateTime&quot;)
                        .coercing(DateTimeCoercing()).build())
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL에서 기본 제공하는 Scalar type은 Int, Float, String, Boolean, ID입니다. ZonedDateTime과 같은 값을 사용하기 위해서는 custom scalar로 정의해서 사용했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer17.png&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c65NN4/dJMcahF5V8B/LIfKWsGySW8qTLtObMEnU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c65NN4/dJMcahF5V8B/LIfKWsGySW8qTLtObMEnU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c65NN4/dJMcahF5V8B/LIfKWsGySW8qTLtObMEnU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc65NN4%2FdJMcahF5V8B%2FLIfKWsGySW8qTLtObMEnU1%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;1896&quot; height=&quot;1158&quot; data-filename=&quot;admin-for-server-developer17.png&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL API도 swagger와 같은 API 문서 및 테스트 도구가 있습니다. GraphiQL 이라는 GraphQL IDE 입니다. 쿼리 요청도 가능하고 schema 탐색기능은 문서화 기능으로도 활용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이렇게 좋은 GraphQL 왜 서비스에 사용하지 않나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL이 클라이언트에서 사용하기 좋고 적용하기도 어렵지 않다면서도 서비스에서 사용하지 않은 이유가 있습니다. 먼저 한번도 써본 적이 없기 때문입니다. 어떻게 구성하면 더 효율적일지 성능은 어느 정도일지에 대한 경험치가 전혀 없습니다. 그리고 Spring for GraphQL도 이제 막 1.0 버전이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영툴에 먼저 적용한 이유가 바로 이 이유입니다. 서비스에 사용하기에는 조금 불안하지만 운영툴에 적용해보면서 익숙해지는 시간을 가져보는 것이죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적인 내용은 모두 다룬 것 같습니다. 이제 UX, 그러니까 편리한 운영툴을 만들기 위해서 노력했던 부분에 대한 경험을 나눠보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영툴은 필요하긴 하지만 우선순위는 낮습니다. 그래서 디자인이나 UX에 소홀해지기 쉽습니다. 조금 불편하더라도 운영을 할 수 있기는 하다고 넘어가게 됩니다. 그리고 회원관리는 테이블로, 대화방은 카드로, 일관되지 않은 방식으로 만들어 지기도 하죠. 만들때도 어떻게 만들어야 하나 고민을 하게 되고 사용하는 분들도 어떻게 사용해야하는지를 문의하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 좀 더 일관되고 유지보수가 쉬운 운영툴을 만들기 위해 UX 가이드를 만들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;admin-for-server-developer02.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0H8Sr/dJMcahF5V8G/5XHC8x4LDrb136MOCVlDk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0H8Sr/dJMcahF5V8G/5XHC8x4LDrb136MOCVlDk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0H8Sr/dJMcahF5V8G/5XHC8x4LDrb136MOCVlDk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0H8Sr%2FdJMcahF5V8G%2F5XHC8x4LDrb136MOCVlDk0%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;1258&quot; height=&quot;766&quot; data-filename=&quot;admin-for-server-developer02.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UX 가이드에는 이런 내용을 담았습니다. 공통 레이아웃인 GNB에는 어떤 내용이 담겨야하는지, LNB의 메뉴는 어떤 기준으로 그룹핑하는지, depth는 어디까지 허용할 것인지. 컨텐츠의 Body는 1단으로 할지, 2단으로 할지. 테이블로 표시할 내용이 있다면 페이징은 어떤 식으로 구성할 것인지 각 로우를 수정할때 수정버튼은 어디에 위치할 것인지와 같은 세부적인 내용을 담았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 설계에 대한 가이드도 작성했는데요. 퍼머링크 URL을 사용해 같은 URL이면 같은 화면이 보여야 한다는, 심지어 무엇인가가 선택되어 있었다면 그런 부분도 똑같이 보여야 한다고 정해두었습니다. 이 부분은 운영 데이터에 대한 URL로 커뮤니케이션을 할때 편리하게 사용했던 것 같습니다. 또, 메뉴 이름 기반으로 url segment depth를 설계해 url만 봐도 어떤 기능인지 짐작 가능하도록 가이드를 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영툴은 서비스 발전의 동력이라고 생각합니다. 개발자와 운영자 사이에서 커뮤니케이션을 돕고 서로의 시간을 절약해줍니다. 운영자를 위한 최고의 서비스를 개발한다는 마음으로 개발한다면 그만큼 서비스 운영에 큰 힘이 될거라 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 개발자에게는 운영툴을 개발하면서 얻는 또다른 좋은 점이 있습니다. 바로 프론트엔드 기술을 익힐 수 있는 기회가 된다는 것입니다. 대부분의 서비스는 프론트엔드 개발자 서버개발자로 나누어 었어서 프론트엔드 개발이 어떤 식으로 이뤄지는지 잘 알지 못할텐데요. 프론트엔드 개발을 통해서 더 나은 API 설계에 대해 생각해볼 기회도 될 것입니다. 그리고 GraphQL과 같은 잘 써보지 않았던 기술을 적용해 검증을 해볼 수 있다는 것도 아주 좋은 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 그동안 운영툴 개발을 통해 경험했던 부분에 대한 생각을 프론트엔드, API, UX관점에서 정리해보았는데요. 이런 부분에서 고민을 했던 분들이 계시다면 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>Dev</category>
      <category>graphql</category>
      <category>react</category>
      <category>UX</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/629</guid>
      <comments>https://blog.joostory.net/629#entry629comment</comments>
      <pubDate>Sun, 4 Oct 2026 19:05:17 +0900</pubDate>
    </item>
    <item>
      <title>zsh 환경을 위한 zshrc 설정</title>
      <link>https://blog.joostory.net/628</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;zsh 설정을 할때마다 헷갈려서 정리해본다. 원래 zshrc에 때려 넣곤 했는데 AI 사용하는 대화형 쉘이나 스크립트에서는 설정이 로드가 되지 않는 일이 종종 있었다. 알아보다보니 zshenv, zshrc 등에 적절히 넣어야하는 것을 알았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zsh 로드순서&lt;/h3&gt;
&lt;table style=&quot;color: #bbbebf; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;4&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;순서&lt;/td&gt;
&lt;td&gt;파일&lt;/td&gt;
&lt;td&gt;로그인쉘&lt;br /&gt;(zsh)&lt;/td&gt;
&lt;td&gt;비로그인 대화형 쉘&lt;br /&gt;(zsh -l)&lt;/td&gt;
&lt;td&gt;비대화형 스크립트&lt;br /&gt;(zsh script.sh)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;6&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;/etc/zshenv&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;7&quot;&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;~/.zshenv&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;8&quot;&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;/etc/zprofile&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;9&quot;&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~/.zprofile&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;10&quot;&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;/etc/zshrc&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;11&quot;&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~/.zshrc&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;12&quot;&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;~/.zlogin&lt;br /&gt;(로그아웃시: ~/.zlogout)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 순서로 로드가 되고 있기 때문에 다음의 기준으로 설정을 배치해야한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;zshenv: 최소한의 필수 환경변수 (PATH 등)&lt;/li&gt;
&lt;li&gt;zprofile: 로그인 쉘 초기화 (brew shellenv 등)&lt;/li&gt;
&lt;li&gt;zshrc: 프롬프트, alias, 자동완성 등&lt;/li&gt;
&lt;li&gt;zlogin: 로그인 환영 메시지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.zshenv&lt;/h3&gt;
&lt;pre id=&quot;code_1790987054834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ==========================================
#   zsh 모듈형 설정 로더 (.zshenv)
# ==========================================

if [ -d &quot;$HOME/.zsh&quot; ]; then
  for config_file in &quot;$HOME/.zsh&quot;/zshenv/*.zsh; do
    [ -r &quot;$config_file&quot; ] &amp;amp;&amp;amp; source &quot;$config_file&quot;
  done
  unset config_file
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env에는 PATH, JAVA_HOME 등의 환경변수, secret에는 각 cli에서 사용할 token을 두었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.zprofile&lt;/h3&gt;
&lt;pre id=&quot;code_1790987103446&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ==========================================
#   zsh 모듈형 설정 로더 (.zprofile)
# ==========================================

if [ -d &quot;$HOME/.zsh&quot; ]; then
  for config_file in &quot;$HOME/.zsh&quot;/zprofile/*.zsh; do
    [ -r &quot;$config_file&quot; ] &amp;amp;&amp;amp; source &quot;$config_file&quot;
  done
  unset config_file
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macos는 /etc/zprofile에서 path_helper 같은 것이 /usr/bin 같은 것을 PATH 최상위로 변경하고 있음을 알았다. 이렇게 하면 아무리&amp;nbsp; brew로 뭔가를 설치해도 사용할 수가 없다. 그래서 여기서 brew 같은 앱의 path를 최상위로 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.zshrc&lt;/h3&gt;
&lt;pre id=&quot;code_1790987121886&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ==========================================
#   zsh 모듈형 설정 로더 (.zshrc)
# ==========================================

if [ -d &quot;$HOME/.zsh&quot; ]; then
  for config_file in &quot;$HOME/.zsh&quot;/zshrc/*.zsh; do
    [ -r &quot;$config_file&quot; ] &amp;amp;&amp;amp; source &quot;$config_file&quot;
  done
  unset config_file
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 alias, prompt, nvm 같은 필요한 것들을 모두 넣는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 헷갈리지 않고 설정 잘 할 수 있게 되기를...&lt;/p&gt;</description>
      <category>Dev</category>
      <category>zsh</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/628</guid>
      <comments>https://blog.joostory.net/628#entry628comment</comments>
      <pubDate>Sat, 3 Oct 2026 09:31:41 +0900</pubDate>
    </item>
    <item>
      <title>TypeScript 7 &amp;amp; Next.js 16 환경에서 ESLint 대신 Biome(바이옴) 도입 후기</title>
      <link>https://blog.joostory.net/627</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프로젝트에서 &lt;b&gt;TypeScript 7.0&lt;/b&gt;과 &lt;b&gt;Next.js 16.3&lt;/b&gt; 버전 업그레이드를 진행하면서 기존 정적 분석 및 포맷팅 도구였던 ESLint 생태계와의 호환성 문제와 느린 실행 속도를 해결하기 위해 Rust 기반 초고속 툴체인인 &lt;b&gt;Biome&lt;/b&gt;으로 도구를 교체했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 경험한 설정 노하우와 도입 후기를 공유합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Biome을 도입하게 된 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;최신 TypeScript 7 호환성&lt;/b&gt;: `@typescript-eslint` 패키지의 공식 지원을 기다리는 대신, 자체 파서로 TypeScript/TSX를 직접 분석하는 독립적인 도구가 필요했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합 툴체인 (Linter + Formatter)&lt;/b&gt;: ESLint와 Prettier를 조합하는 복잡한 플러그인 설정 없이 단일 바이너리로 린팅과 코드 포맷팅을 한 번에 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;압도적인 실행 속도&lt;/b&gt;: 100여 개가 넘는 프로젝트 파일의 정적 분석 및 포맷팅이 불과 &lt;b&gt;30~40ms&lt;/b&gt; 만에 완료됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 설정 방법 (`biome.json`)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`npm install -D &amp;ndash;save-exact @biomejs/biome` 설치 후 `npx biome init`을 실행하여 설정 파일을 생성합니다. Next.js 및 Tailwind CSS 프로젝트에 맞춘 실전 설정 팁입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;`biome.json` 핵심 설정&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://biomejs.dev/schemas/2.5.6/schema.json&quot;,
  &quot;vcs&quot;: {
    &quot;enabled&quot;: true,
    &quot;clientKind&quot;: &quot;git&quot;,
    &quot;useIgnoreFile&quot;: true
  },
  &quot;formatter&quot;: {
    &quot;enabled&quot;: true,
    &quot;indentStyle&quot;: &quot;space&quot;,
    &quot;indentWidth&quot;: 2
  },
  &quot;linter&quot;: {
    &quot;enabled&quot;: true,
    &quot;rules&quot;: {
      &quot;preset&quot;: &quot;recommended&quot;,
      &quot;a11y&quot;: {
        &quot;recommended&quot;: false
      },
      &quot;performance&quot;: {
        &quot;noImgElement&quot;: &quot;off&quot;
      }
    }
  },
  &quot;javascript&quot;: {
    &quot;formatter&quot;: {
      &quot;quoteStyle&quot;: &quot;double&quot;,
      &quot;semicolons&quot;: &quot;asNeeded&quot;
    }
  },
  &quot;css&quot;: {
    &quot;parser&quot;: {
      &quot;tailwindDirectives&quot;: true
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 커스텀 옵션 포인트:&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Tailwind CSS v4 지원&lt;/b&gt;: `css.parser.tailwindDirectives: true`를 추가하여 `@ theme`, `@ utility`, `@ apply` 지시어 파싱 오류를 방지했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Static Export 환경 예외 처리&lt;/b&gt;: Static Site Export(`output: &amp;lsquo;export&amp;rsquo;`)를 사용하는 경우 dynamic image optimization이 불가능하므로 `performance.noImgElement: &amp;ldquo;off&amp;rdquo;`로 설정하여 &amp;ldquo; 태그 관련 경고를 예외 처리했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세미콜론 안 붙이기 (Semi-less)&lt;/b&gt;: `javascript.formatter.semicolons: &quot;asNeeded&amp;rdquo;`로 필요 시에만 세미콜론을 붙이도록 설정했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. NPM 스크립트 및 CI 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`package.json`에 다음 스크립트를 추가하여 간편하게 실행합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`npm run lint` -&amp;gt; `biome check .`&lt;/li&gt;
&lt;li&gt;`npm run lint:fix` -&amp;gt; `biome check &amp;ndash;write .`&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 GitHub Actions(`ci.yaml`)에 `npm run lint` 및 `npx tsc &amp;ndash;noEmit` 검증 단계를 추가하여 모든 PR에 대해 정적 분석과 타입 검사를 자동으로 통과하도록 구축했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Biome으로 전환한 후 개발 환경이 훨씬 깔끔해졌고, 코드 체크 및 포맷팅 속도가 비약적으로 향상되었습니다. 최신 TypeScript/Next.js 프로젝트를 진행 중이라면 Biome 도입을 강력히 추천합니다!&lt;/p&gt;</description>
      <category>Dev</category>
      <category>BIOME</category>
      <category>nextjs</category>
      <category>TypeScript</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/627</guid>
      <comments>https://blog.joostory.net/627#entry627comment</comments>
      <pubDate>Sat, 15 Aug 2026 08:18:33 +0900</pubDate>
    </item>
    <item>
      <title>이것이 vibe 코딩인가</title>
      <link>https://blog.joostory.net/626</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 AI 서비스가 계속 발전해서 chatgpt, copilot, grok, gemini 등이 나왔고 이것이 개발도구에 들어오기 시작했다. 많은 사람들이 너무 좋다고 계속 추천을 하는데 나는 유료버전을 안써서 그런지 별로 좋은 경험은 아니었다. intelliJ, vscode에서 copilot agent, gemini agent를 사용했었는데 ide 문제인지 내가 익숙하지 않아서 그런지 모르겠지만 뭔가 모르게 불편했고 그냥 내가 원하는대로 되지가 않았다. 그래서 좋다는 사람들은 어떻게 쓰나 궁금했는데 vibe코딩이라는 말이 나오기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vibe코딩은 뭔가 정확한 지시를 하는게 아니라 기획초기 아이디어 던지듯이 요구사항을 툭툭 던지면서 하는 코딩을 말하는 것 같았다. (사람들이 하는거 보니까 그렇게 하더라) 사실 이것도 그냥 그러려니 했는데 gemini-cli를 소개하는 영상을 보면서 조금 끌리기 시작했다. 그동안의 ai 코딩은 ide와 통합이 필요했고, 그래서 뭐 cursor니 뭐니 잔뜩 나왔었다. 그런데 gemini-cli는 달랐다. ide는 상관없다. 그 안에 터미널만 열면 됐다. 아니 ide 내부 터미널이 아니라도 된다. 아무 터미널이면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;project 디렉토리에서 터미널을 열고 이것저것 요구사항을 말하면 뚝딱뚝딱 만들어서 diff를 보여주면서 코드를 막 수정한다. 아주 간단한 작업을 시켜보니까... 어라? 굉장히 깔끔하게 작업을 마무리 한다. 뭔가 원하는대로 동작을 안하고 있어서 해결해달라고 했는데 코드를 쭉 읽어보더니 문제점을 찾아서 해결해주었다. 단순히 알려주는 것만이 아니고 파일 수정까지도 한다. 무엇보다도 그 과정에서 나에게 설명해주는 부분이 좋았다. 개요, 본론, 결론 나눠서 군더더기 없는 커뮤니케이션을 한다. 마치 굉장히 똑똑한 동료와 페어코딩하는 느낌. 때마침 성경앱에 검색기능이 필요한 상황이어서 gemini-cli를 사용해서 해결해보기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색기능 해줘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 어떻게 시작해야할지 몰라서 최대한 자세히 설명했던 것 같다. (터미널 기록이 없네 ;;;)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;nextjs로 github page로 publish를 하고 있어. data/bible.json을 읽어서 페이지에 표시하고 있는데 모든 페이지에서 이 성경책을 검색하도록 하고 싶어. ctrl+k, 맥에선 cmd+k로 검색하고 header에도 검색버튼을 추가해줘. ui는 dialog로 표시하고 검색어를 입력하면 리스트가 나오도록 하면 좋겠어.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이정도 느낌이었다. 여기서 첫번째 감동을 받았다. 바로 실행하지 않고 분석을 한 후에 실행계획을 순서대로 알려주고 이후에 순서대로 수행을 하는데 이 부분이 뭔가 나에게 믿음을 주는 행동이었다. 뭐랄까 회사에서 동료에게 뭔가를 부탁했을때 이런 응답이 오기를 바랬는데 완벽한 응답이었다. 이제 계획을 읽고 나는 ok만 하면 되는거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수행과정도 아주 맘에 든다. 뭔가를 할때마다 '하겠습니다', '하고 있습니다', '했습니다' 이런식으로 알려주면서 한다. 실제 동료와 페어로 일을 하면 커뮤니케이션에 말로, 화면으로 여러가지 수단을 활용해야하는데 gemini-cli는 한 화면에서 수행과정까지 보고서 형식으로 하니 정말 편했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;723&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6YwyW/btsO5eQiqvt/3KcGSw7RfViXxZ1Xmu8fPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6YwyW/btsO5eQiqvt/3KcGSw7RfViXxZ1Xmu8fPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6YwyW/btsO5eQiqvt/3KcGSw7RfViXxZ1Xmu8fPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6YwyW%2FbtsO5eQiqvt%2F3KcGSw7RfViXxZ1Xmu8fPK%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;1764&quot; height=&quot;723&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기능을 만들어달라는 요구였기 때문에 다소 작업이 많았는데 수행과정은 자세히 설명하고 계획 설명, 수행 결과 요약은 간결했다. 이건 뭐 너무나 완벽한 동료개발자의 모습이다. 심지어 내가 직접했다면 한시간은 걸렸을 작업을 몇마디 말에 몇분만에 완료했다. 물론 100% 완벽한 것은 아니다 ui 작업이기 때문에 실제로 결과를 눈으로 확인하고 조정해야하는 것은 내 몫이었다. 개발 과정이 완벽했다는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고쳐줘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 확인해보니 오류가 발생하고 있다. 흠... 이제 두번째 요구인데 어떻게 말할까 하다가 간략하게 상황만 설명하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1705&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xmC5E/btsO7yTus6O/mmqLOJLkXUEQlMxwZp0PG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xmC5E/btsO7yTus6O/mmqLOJLkXUEQlMxwZp0PG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xmC5E/btsO7yTus6O/mmqLOJLkXUEQlMxwZp0PG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxmC5E%2FbtsO7yTus6O%2FmmqLOJLkXUEQlMxwZp0PG0%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;1705&quot; height=&quot;502&quot; data-origin-width=&quot;1705&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세상에. 원인파악, 해결계획, 수행 모두 완벽하다. 저렇게 잘 알면서 왜 처음부터 잘 만들지 않았지라는 생각은 뒤로 하고 다음 요구를 말했다. daisyui를 쓰고 있는데 radix dialog를 쓰고 있어서 이걸 바꿔달라는 말이었다. 이제 진짜 동료대하듯 친철하게 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZlQLP/btsO5x9TFHJ/xZ3DR2Xjs0Ygufh9WcuAYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZlQLP/btsO5x9TFHJ/xZ3DR2Xjs0Ygufh9WcuAYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZlQLP/btsO5x9TFHJ/xZ3DR2Xjs0Ygufh9WcuAYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZlQLP%2FbtsO5x9TFHJ%2FxZ3DR2Xjs0Ygufh9WcuAYk%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;1759&quot; height=&quot;522&quot; data-origin-width=&quot;1759&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref를 사용해서 브라우저 api를 직접 사용하는 부분은 귀찮은 작업인데 이것도 별문제 아니라는 듯 뚝딱해결해주었다. 이젠 좀 더 친근하게 다른 문제의 해결을 말하기 시작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1707&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0zez9/btsO5lWaxHz/sL6ks0z7XsN51Vo7KxNvJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0zez9/btsO5lWaxHz/sL6ks0z7XsN51Vo7KxNvJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0zez9/btsO5lWaxHz/sL6ks0z7XsN51Vo7KxNvJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0zez9%2FbtsO5lWaxHz%2FsL6ks0z7XsN51Vo7KxNvJ0%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;1707&quot; height=&quot;287&quot; data-origin-width=&quot;1707&quot; data-origin-height=&quot;287&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1733&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxJj5B/btsO5l9JpPM/YhpLYfe3766FDKPpx0tkk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxJj5B/btsO5l9JpPM/YhpLYfe3766FDKPpx0tkk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxJj5B/btsO5l9JpPM/YhpLYfe3766FDKPpx0tkk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxJj5B%2FbtsO5l9JpPM%2FYhpLYfe3766FDKPpx0tkk0%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;1733&quot; height=&quot;679&quot; data-origin-width=&quot;1733&quot; data-origin-height=&quot;679&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1695&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duZQd3/btsO55SwsJt/lp3kTtKV3wVqU6pyszme11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duZQd3/btsO55SwsJt/lp3kTtKV3wVqU6pyszme11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duZQd3/btsO55SwsJt/lp3kTtKV3wVqU6pyszme11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduZQd3%2FbtsO55SwsJt%2Flp3kTtKV3wVqU6pyszme11%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;1695&quot; height=&quot;209&quot; data-origin-width=&quot;1695&quot; data-origin-height=&quot;209&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lB1Hg/btsO688A0Z4/kcaiExGvFDNKj7HCOyaH8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lB1Hg/btsO688A0Z4/kcaiExGvFDNKj7HCOyaH8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lB1Hg/btsO688A0Z4/kcaiExGvFDNKj7HCOyaH8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlB1Hg%2FbtsO688A0Z4%2FkcaiExGvFDNKj7HCOyaH8K%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;1657&quot; height=&quot;395&quot; data-origin-width=&quot;1657&quot; data-origin-height=&quot;395&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1739&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5HsNO/btsO67u4E2l/eL8ZU7fGr3P1eRBL2YKma0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5HsNO/btsO67u4E2l/eL8ZU7fGr3P1eRBL2YKma0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5HsNO/btsO67u4E2l/eL8ZU7fGr3P1eRBL2YKma0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5HsNO%2FbtsO67u4E2l%2FeL8ZU7fGr3P1eRBL2YKma0%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;1739&quot; height=&quot;504&quot; data-origin-width=&quot;1739&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 몇몇 핑퐁을 거치긴 했지만 나는 요구사항 전달과 코드리뷰한 것만으로도 새로운 기능을 얻게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과물&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수행한 commit: &lt;a href=&quot;https://github.com/joostory/holybible/commit/2679783dd1e37a55e046652ca38a0517636f5eaf&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/joostory/holybible/commit/2679783dd1e37a55e046652ca38a0517636f5eaf&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;1878&quot; data-origin-height=&quot;587&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekESCz/btsO5dw8rGh/sJW8rU97LkdNy7RUkv9kxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekESCz/btsO5dw8rGh/sJW8rU97LkdNy7RUkv9kxk/img.png&quot; data-alt=&quot;gemini agent 사용 요약&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekESCz/btsO5dw8rGh/sJW8rU97LkdNy7RUkv9kxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekESCz%2FbtsO5dw8rGh%2FsJW8rU97LkdNy7RUkv9kxk%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;1878&quot; height=&quot;587&quot; data-origin-width=&quot;1878&quot; data-origin-height=&quot;587&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gemini agent 사용 요약&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vibe코딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 걸 vibe코딩이라고 하는거겠지? 요구사항을 툭툭 던지고 개발은 ai가 하고 나는 그저 검토만 하는 것. ai가 코딩을 한다고 해서 개발자가 할 일이 없어지는 것은 아닌 것 같다. 내가 모르는 코드에 대해서 ai가 개발을 해준다고 했을때 나는 아무런 조언을 할 수가 없었을 것이다. 이 작업은 나도 익숙하게 할 수 있는 작업이고 한눈에 작업상황 파악, 코드리뷰가 가능했기때문에 무리없이 가능했던 것 같다. (내가 모르는 분야에 대해서도 이렇게 한번 해보면 확실히 알 수 있겠지.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 이번 기회에 ai로 개발하는 것이 어떤 것인지 잘 알았고 ai는 훌륭한 개발동료가 될 수 있다는 생각이 들었다. 자주 보자. gemini.&lt;/p&gt;</description>
      <category>Dev</category>
      <category>gemini-cli</category>
      <category>vibe코딩</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/626</guid>
      <comments>https://blog.joostory.net/626#entry626comment</comments>
      <pubDate>Sun, 6 Jul 2025 10:17:04 +0900</pubDate>
    </item>
    <item>
      <title>Ubuntu grub 복구</title>
      <link>https://blog.joostory.net/625</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;갑자기 Ubuntu로 부팅이 되지 않는 문제가 생겼다. 먼저 내 상황을 설명하자면... 오래 전에 고장나버린 노트북이 고장나서 거기서 SSD를 뽑아두었다. 그걸 게임하려고 산 노트북에 넣어서 우분투를 쓰고 싶을때 사용하고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1732421517246&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/dev/nvmen1n1 - Windows
- /dev/nvmen1n1p1 : Windows Boot Manager (UEFI partition)
- /dev/nvmen1n1p2
- /dev/nvmen1n1p3 : Windows
- /dev/nvmen1n1p4
/dev/nvmen0n1 - Ubuntu
- /dev/nvmen0n1p1 : Ubuntu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상태다. Windows의 부트매니저가 window, ubuntu중 선택하도록 하고 ubuntu의 grub이 다시 뭘로 부팅할지 물어보는 두번에 걸친 과정을 거친다. 우분투는 어찌되었든 메인이 아니고 기본값을 윈도우로 두고 싶어서였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫번째 문제 - grub 진입 안됨&lt;/h3&gt;
&lt;pre id=&quot;code_1732421740321&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Failed to open \EFI\UBUNTU\grubx64.efi - Not Found  
Failed to load image \EFI\UBUNTU\grubx64.efi: Not Found    
start_image() returned Not Found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우 부트매니저에서 우분투를 선택하면 이런 메시지만 나오고 더이상 진행이 되지 않았다. 이건 윈도우 업데이트를 할 때 생길 수 있는 문제라고 했다. 그리고 EFI/UBUNTU 에 grubx64.efi 라는 것을 복사만 하면 해결된다고 했다. 실제로 이게 맞다. 하지만 나는 여기서 고생을 많이 했는데 왜냐하면 저 복사해야할 디렉토리가 어디인지를 못찾았기 때문이다. 지금은 알고 있어서 허탈하지만 몇시간을 고생한지 모르겠다.&lt;/p&gt;
&lt;figure id=&quot;og_1732421996069&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Failed to open \EFI\BOOT\grubx64.efi - Not Found (Dual Boot Ubuntu with Windows 10)&quot; data-og-description=&quot;Background I have a Windows 10 machine with a SSD where I have installed Ubuntu LTS 20.04. This setup worked wonderfully for months. When I turn my machine on I get to choose which operative system...&quot; data-og-host=&quot;askubuntu.com&quot; data-og-source-url=&quot;https://askubuntu.com/questions/1356544/failed-to-open-efi-boot-grubx64-efi-not-found-dual-boot-ubuntu-with-windows&quot; data-og-url=&quot;https://askubuntu.com/questions/1356544/failed-to-open-efi-boot-grubx64-efi-not-found-dual-boot-ubuntu-with-windows&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFEGqJ/hyXzMYjibZ/CicUOWp01eclutT52Mj5b0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://askubuntu.com/questions/1356544/failed-to-open-efi-boot-grubx64-efi-not-found-dual-boot-ubuntu-with-windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://askubuntu.com/questions/1356544/failed-to-open-efi-boot-grubx64-efi-not-found-dual-boot-ubuntu-with-windows&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFEGqJ/hyXzMYjibZ/CicUOWp01eclutT52Mj5b0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Failed to open \EFI\BOOT\grubx64.efi - Not Found (Dual Boot Ubuntu with Windows 10)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Background I have a Windows 10 machine with a SSD where I have installed Ubuntu LTS 20.04. This setup worked wonderfully for months. When I turn my machine on I get to choose which operative system...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;askubuntu.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서에 설명한 것처럼 우분투 설치 usb같은 것으로 부팅한 후에 UEFI 파티션의 디렉토리에 복사를 하면 된다. 그 UEFI 파티션은 바로 윈도우 부트매니저가 있는 곳을 말한다. 그러니까 나의 경우는 /dev/nvme1n1p1 이다. 이걸 이해 못해서 너무 고생했다. 심지어 다시 설치하려고 usb 메모리까지 구매했었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두번째 문제 - grub console&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 grub으로 진입은 했지만 문제가 있었다. grub 메뉴가 안나오고 console만 나오는거다. grub 환경설정을 정상적으로 불러오지 못하는 것 같았다. grub... 을 진작에 좀 공부해서 알고 있었으면 좋았으련만 그동안 직접 설정을 만지거나하지를 않아서 몰랐다. 이제 어떻게 해야하는지를...&lt;/p&gt;
&lt;figure id=&quot;og_1732422375658&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Classic SysAdmin: How to Rescue a Non-booting GRUB 2 on Linux - Linux Foundation&quot; data-og-description=&quot;Once upon a time we had legacy GRUB, the Grand Unified Linux Bootloader version 0.97. Learn how to rescue a non-booting GRUB 2 on Linux.&quot; data-og-host=&quot;www.linuxfoundation.org&quot; data-og-source-url=&quot;https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-rescue-a-non-booting-grub-2-on-linux&quot; data-og-url=&quot;https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-rescue-a-non-booting-grub-2-on-linux&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cEHzUc/hyXDeFzvrI/qTS7aHGCFCiv5SA842Dtf1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/buDeGP/hyXDhoLN9D/v57HRIZ7pgiTiVFnUkVFEk/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/eSaawY/hyXDdzTtSm/Gvm5jh3lQFOTR0dIqkQR60/img.jpg?width=550&amp;amp;height=282&amp;amp;face=0_0_550_282&quot;&gt;&lt;a href=&quot;https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-rescue-a-non-booting-grub-2-on-linux&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.linuxfoundation.org/blog/blog/classic-sysadmin-how-to-rescue-a-non-booting-grub-2-on-linux&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cEHzUc/hyXDeFzvrI/qTS7aHGCFCiv5SA842Dtf1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/buDeGP/hyXDhoLN9D/v57HRIZ7pgiTiVFnUkVFEk/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/eSaawY/hyXDdzTtSm/Gvm5jh3lQFOTR0dIqkQR60/img.jpg?width=550&amp;amp;height=282&amp;amp;face=0_0_550_282');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Classic SysAdmin: How to Rescue a Non-booting GRUB 2 on Linux - Linux Foundation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Once upon a time we had legacy GRUB, the Grand Unified Linux Bootloader version 0.97. Learn how to rescue a non-booting GRUB 2 on Linux.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.linuxfoundation.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 방법이 제시되는데 나에게는 이 방법으로 해결이 되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1732422466562&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; ls
(hd0) (hd0,gpt1) (hd1) (hd1,gpt1) (hd1,gpt2) (hd1,gpt3)
&amp;gt; set prefix=(hd0,gpt1)/boot/grub
&amp;gt; set root=(hd0,gpt1)
&amp;gt; insmod linux
&amp;gt; insmod normal
&amp;gt; normal&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것으로 며칠동안 고생하며 보지 못했던 우분투의 화면을 만났다. 그리고 이 설정은 저장되는 것이 아니라서 매번 이렇게 실행을 해야하는데 다시 grub의 설정을 정상적으로 복구하려면 우분투로 부팅한 후에 update-grub을 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1732422553114&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo update-grub&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투를 새로 다시 설치해야하나 고민도 하고 너무 괴로웠는데 다행히 해결되었다. ㅠㅠ 정말 다행이다.&lt;/p&gt;</description>
      <category>잡담</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/625</guid>
      <comments>https://blog.joostory.net/625#entry625comment</comments>
      <pubDate>Sun, 24 Nov 2024 13:30:29 +0900</pubDate>
    </item>
    <item>
      <title>@types/axios deprecated</title>
      <link>https://blog.joostory.net/624</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오랜동안 업데이트하지 않았던 fe 프로젝트의 라이브러리를 최신화하는 작업을 했는데 이상한게 보였다. @types/axios를 0.14.0 버전을 사용하고 있는데 0.9.36이 최신버전이라며 업데이트를 하라는거다. 업데이트 가능 목록에 계속 나오는게 싫어서 별차이 있겠나 싶어 0.9.36으로 재설치를 했다. 그랬더니 axios를 사용하는 모든 부분에서 에러가 발생하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한참을 이랬다 저랬다 하다가 npm 사이트에 들어가서야 발견했다. axios 자체에서 type을 제공하니 0.14.x 를 deprecated 시켜버렸던 것이다. 아마도 이렇게 잘못 설치하도록 해서 사용하면 안된다는 것을 알게해주려고 이렇게 한 것 같다는 생각이다. 물론 이걸 찾기 전까지는 조금 짜증났지만...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2388&quot; data-origin-height=&quot;1744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9VjiR/btsKM4PYxhD/E1RtH8v9lCQcKklYkVhENK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9VjiR/btsKM4PYxhD/E1RtH8v9lCQcKklYkVhENK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9VjiR/btsKM4PYxhD/E1RtH8v9lCQcKklYkVhENK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9VjiR%2FbtsKM4PYxhD%2FE1RtH8v9lCQcKklYkVhENK%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;2388&quot; height=&quot;1744&quot; data-origin-width=&quot;2388&quot; data-origin-height=&quot;1744&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>Dev</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/624</guid>
      <comments>https://blog.joostory.net/624#entry624comment</comments>
      <pubDate>Sun, 17 Nov 2024 09:13:44 +0900</pubDate>
    </item>
    <item>
      <title>pyenv 설정</title>
      <link>https://blog.joostory.net/623</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;python3이면 다 똑같은 줄 알았는데 3.10, 3.11, 3.12, 3.13까지 조금 다른 부분이 있었다. 이미 만들어진 어떤 모듈을 사용하려고 보니 python 3.13에서는 동작하지 않았다. 뭔가가 바뀌었다보다. 그래서 여러가지 버전의 python을 사용할 수 있도록 pyenv을 사용하려고 한다. 현재 맥OS에는 기본적으로 python3.11.1이 설치되어 있다. 기본 버전은 그대로 두고 pyenv로 원하는 python을 설치해서 사용하도록 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;a href=&quot;https://github.com/pyenv/pyenv?tab=readme-ov-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pyenv&lt;/a&gt;를 설치해야한다. 여러가지 방법이 있는데 homebrew로 설치하기로 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1731494410082&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ brew install pyenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyenv만 설치하면 설치된 python은 없는 상태다. python 3.11.10을 설치하려면 homebrew처럼 install 만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1731494532874&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ pyenv install 3.11.10

❯ pyenv versions
  system
* 3.11.10 (set by /Users/joo.yoon/.pyenv/version)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이렇게만 하면 python의 경로를 찾지 못한다. PATH에 추가를 해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1731494586299&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export PATH=&quot;/opt/homebrew/bin:$HOME/.bin:$PATH&quot;
export PATH=&quot;$(pyenv root)/shims:$PATH&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 homebrew로 설치한 pyenv를 실행해서 경로를 찾아야 하기 때문에 homebrew를 PATH에 넣은 후 다시 한번 PATH를 만들어야 한다는 것이다. 이렇게 하면 pyenv로 설정한 python을 사용할 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1731494875281&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ python --version
Python 3.11.10

❯ pyenv versions
  system
* 3.11.10 (set by /Users/joo.yoon/.pyenv/version)
  3.13.0

❯ pyenv global 3.13.0

❯ python --version
Python 3.13.0&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/623</guid>
      <comments>https://blog.joostory.net/623#entry623comment</comments>
      <pubDate>Wed, 13 Nov 2024 19:53:39 +0900</pubDate>
    </item>
    <item>
      <title>MacOS java 설치</title>
      <link>https://blog.joostory.net/622</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서는 java를 많이 쓰지만 집에선 거의 java를 쓰지 않는다. 그래도 ubuntu나 mac에는 기본으로 설치가 되어 있으니 그냥 썼는데 최근 mac에는 java가 없다. 그래서 설치하면서 설치방법을 정리해본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다운로드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 openjdk를 설치하려고 하는데 어디서 설치를 해야하는지 찾아봤다. 먼저 &lt;a href=&quot;https://openjdk.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;openjdk 웹사이트&lt;/a&gt;에서 시작했다. 다운로드 하려면 jdk.java.net으로 가라고 한다. 지금은 23이 GA 상태지만 가장 최근 버전인 것 같고 &lt;a href=&quot;https://jdk.java.net/archive/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;archive 페이지&lt;/a&gt;에 모든 버전이 다 있다. lts 버전인 21이랑 23을 써보기로 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mac에서는 /usr/libexec/java_home 을 통해서 java 위치를 결정한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731104306556&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ /usr/libexec/java_home
The operation couldn&amp;rsquo;t be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 java_home은 /Library/Java/JavaVirtualMachines 에서 사용할 jvm을 선택한다.&lt;/p&gt;
&lt;pre id=&quot;code_1731104544545&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/Library/Java/JavaVirtualMachines 
❯ ls -l
total 0
drwxr-xr-x@ 3 joo.yoon  staff  96 12  8  2021 jdk-17.0.2.jdk
drwxr-xr-x@ 3 joo.yoon  staff  96  1  6  2024 jdk-21.0.2.jdk
drwxr-xr-x@ 3 joo.yoon  staff  96  9 30 22:25 jdk-23.0.1.jdk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 편하게 디렉토리에 넣어두기만 하면 java_home이 알아서 경로를 찾는다.&lt;/p&gt;
&lt;pre id=&quot;code_1731104691068&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ /usr/libexec/java_home -v 17
/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home

❯ /usr/libexec/java_home -v 21
/Library/Java/JavaVirtualMachines/jdk-21.0.2.jdk/Contents/Home

❯ /usr/libexec/java_home -v 23
/Library/Java/JavaVirtualMachines/jdk-23.0.1.jdk/Contents/Home&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원하는 java 버전을 선택하는 일만 남았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 가장 최근 버전을 선택하나 JAVA_HOME을 지정하면 버전이 변경된다. shell에서는 .zshrc 같은 곳에서 원하는 java 버전을 선택해서 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1731104815876&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ java -version
openjdk version &quot;23.0.1&quot; 2024-10-15
OpenJDK Runtime Environment (build 23.0.1+11-39)
OpenJDK 64-Bit Server VM (build 23.0.1+11-39, mixed mode, sharing)

❯ echo $JAVA_HOME

❯ export JAVA_HOME=`/usr/libexec/java_home -v 17`

❯ echo $JAVA_HOME
/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home

❯ java -version
openjdk version &quot;17.0.2&quot; 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-86)
OpenJDK 64-Bit Server VM (build 17.0.2+8-86, mixed mode, sharing)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Dev</category>
      <category>java</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/622</guid>
      <comments>https://blog.joostory.net/622#entry622comment</comments>
      <pubDate>Sat, 9 Nov 2024 07:30:28 +0900</pubDate>
    </item>
    <item>
      <title>Ubuntu 24.10, 20주년 버전</title>
      <link>https://blog.joostory.net/621</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu의 20주년 버전인 24.10이 나왔다. 24.04 버전이 lts라서 라이브러리 몇개 업그레이드하고 넘어갈 수도 있는데 이번엔 20주년이라서 뭔가 많다.&lt;/p&gt;
&lt;figure id=&quot;og_1728692923559&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Ubuntu 24.10 is Now Available to Download&quot; data-og-description=&quot;Ubuntu 24.10 is now available for download after six months of dedicated development. The latest release offers plenty of changes and new features,&quot; data-og-host=&quot;www.omgubuntu.co.uk&quot; data-og-source-url=&quot;https://www.omgubuntu.co.uk/2024/10/ubuntu-24-10-now-available-to-download&quot; data-og-url=&quot;https://www.omgubuntu.co.uk/2024/10/ubuntu-24-10-now-available-to-download&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bJ4iDa/hyXhRwUB4g/hzXQ7x0RvLdAMTMeuV1Huk/img.jpg?width=1920&amp;amp;height=1008&amp;amp;face=0_0_1920_1008,https://scrap.kakaocdn.net/dn/ClCEb/hyXd8G52SJ/ejzkjkNbJOXSOfYNwNWum0/img.jpg?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/dyDB4q/hyXd36QYPZ/4GRPi2utp8njSgSCWaSHs0/img.jpg?width=840&amp;amp;height=473&amp;amp;face=0_0_840_473&quot;&gt;&lt;a href=&quot;https://www.omgubuntu.co.uk/2024/10/ubuntu-24-10-now-available-to-download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.omgubuntu.co.uk/2024/10/ubuntu-24-10-now-available-to-download&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bJ4iDa/hyXhRwUB4g/hzXQ7x0RvLdAMTMeuV1Huk/img.jpg?width=1920&amp;amp;height=1008&amp;amp;face=0_0_1920_1008,https://scrap.kakaocdn.net/dn/ClCEb/hyXd8G52SJ/ejzkjkNbJOXSOfYNwNWum0/img.jpg?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/dyDB4q/hyXd36QYPZ/4GRPi2utp8njSgSCWaSHs0/img.jpg?width=840&amp;amp;height=473&amp;amp;face=0_0_840_473');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Ubuntu 24.10 is Now Available to Download&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ubuntu 24.10 is now available for download after six months of dedicated development. The latest release offers plenty of changes and new features,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.omgubuntu.co.uk&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 눈에 띄는건 아무래도 20주년 기념 이미지인데 부트화면에도 이 이미지가 들어가고 20주년을 기념한 wallpaper에도 들어갔다. 10년 단위 로고를 보니 정말 감회가 새롭다. 조금씩 변해왔는데 그래도 처음의 형태를 거의 그대로 유지하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPg8a/btsJ20a3Tfm/fb0OOPA7Hkg8IzwPOZgII0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPg8a/btsJ20a3Tfm/fb0OOPA7Hkg8IzwPOZgII0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot; data-filename=&quot;Ubuntu_Legacy_by_Aaron_J_Prisk_dark.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPg8a/btsJ20a3Tfm/fb0OOPA7Hkg8IzwPOZgII0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPg8a%2FbtsJ20a3Tfm%2Ffb0OOPA7Hkg8IzwPOZgII0%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;3840&quot; height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zyNjp/btsJ4gcBHyC/dfcke0OU4HKkubMpOkMGVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zyNjp/btsJ4gcBHyC/dfcke0OU4HKkubMpOkMGVk/img.png&quot; data-origin-width=&quot;7680&quot; data-origin-height=&quot;4320&quot; data-is-animation=&quot;false&quot; data-filename=&quot;Warty_remastered_by_Romactu.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zyNjp/btsJ4gcBHyC/dfcke0OU4HKkubMpOkMGVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzyNjp%2FbtsJ4gcBHyC%2Fdfcke0OU4HKkubMpOkMGVk%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;7680&quot; height=&quot;4320&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전이 올라가면 대부분 바로 업데이트를 하는 편인데 이번엔 업데이트하고나서 너무 좋았다. 가장 좋았던 점은 디스플레이 지원이 좋아졌다는거다. 30인치 모니터를 4k로 쓰면서 125%로 쓰는게 적당하고 좋았는데 이게 제대로 동작을 안하다가 이번 버전에서는 잘 동작한다. 게다가 멀티스크린 상황에서도 문제가 없었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-12 09-18-01.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;587&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzuoxg/btsJ4pAqnAR/MRLiNTX7o7ZvcFw0dKanAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzuoxg/btsJ4pAqnAR/MRLiNTX7o7ZvcFw0dKanAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzuoxg/btsJ4pAqnAR/MRLiNTX7o7ZvcFw0dKanAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzuoxg%2FbtsJ4pAqnAR%2FMRLiNTX7o7ZvcFw0dKanAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;587&quot; data-filename=&quot;스크린샷 2024-10-12 09-18-01.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;587&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑 리눅스의 가장 큰 문제가 별거 아닌거 같지만 소소한 설정들이 제대로 동작을 안하는 경우가 있는 건데 이번엔 대부분의 기능이 별다른 문제없이 동작한다. 이런걸로 좋아해야하나 싶긴하지만 그래도 좋은 건 좋은 거니까... 한동안은 또 이걸 기본으로 써야겠다.&lt;/p&gt;</description>
      <category>Dev</category>
      <category>ubuntu</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/621</guid>
      <comments>https://blog.joostory.net/621#entry621comment</comments>
      <pubDate>Sat, 12 Oct 2024 09:42:48 +0900</pubDate>
    </item>
    <item>
      <title>오랜만에 블로그 스킨 관리</title>
      <link>https://blog.joostory.net/620</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;블로그를 한동안 안쓰다가 다시 써볼까 싶어서 들어왔더니 서비스들이 이것저것 바뀌어 있었다. 티스토리는... 크게 바뀐건 없지만 내부적으로 약간씩 디테일이 달라졌다. 텀블러는 충격적이게도 커스텀 도메인이 유료화되었다. 그리고 테마 변수에 조금 변경이 있다. NPF 가 추가되었다. 아무래도 javascript로 컨트롤하는 테마들이 많아서 추가된 것 같다. 하는김에 summary, 대표이미지 같은 것도 좀 해주지 그런건 아직도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌거나 이제 어디다가 글을 쓰나 고민을 하다가 joo.joohana.net은 그대로 살리고 예전에 개발관련 내용을 쓰려고 했던 지금은 joo-yoon.tumblr.com이 된 곳은... 버리기로 했다. 너무 황당하게도 텀블러엔 이제 코드블럭이 안된다. 모바일과의 통일성을 위해서 과감하게 버린 것 같다. 그럼 이제 변화를 따라갈 시간이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;텀블러 summary&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텀블러에는 말했듯 리스트에서 사용할 대표이미지, summary에 대한 변수를 제공하지 않는다. 그런데 이번에 NPF 변수가 생겼으니 javascript로 이걸 사용하면 되겠구나 싶었다. npf는 &lt;a href=&quot;https://github.com/tumblr/docs/blob/master/npf-spec.md&quot;&gt;docs/npf-spec.md at master &amp;middot; tumblr/docs (github.com)&lt;/a&gt; 에 설명이 되어 있는데 실제로 내려오는 값을 보니 content의 image 타입중 첫번째를 대표이미지로, content의 text 타입 중 subtype이 없는 것을 summary로 사용하면 될 것 같았다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1726581657849&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function makeSummaryFromNpf(contentId, npf) {
  let content = &quot;&quot;
  const img = npf.content.find(it =&amp;gt; it.type == 'image')
  if (img) {
    let imgSrc = img['media'][1]['url']
    content = &quot;&amp;lt;div class='npf_row'&amp;gt;&amp;lt;div class='npf_col'&amp;gt;&amp;lt;img src='&quot; + imgSrc + &quot;' /&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&quot;
  }

  const summary = npf.content.find(it =&amp;gt; it['type'] == 'text' &amp;amp;&amp;amp; !it['subtype'])
  if (summary) {
    let text = summary['text']
    if (text.length &amp;gt; 100) {
      text = text.substr(0, 100) + &quot;...&quot;
    }
    content += &quot;&amp;lt;p&amp;gt;&quot; + text + &quot;&amp;lt;/p&amp;gt;&quot;
  }

  document.getElementById(contentId).innerHTML = content
}&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_1726582053422&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{block:IndexPage}
&amp;lt;div id=&quot;summary-{PostId}&quot; class=&quot;post-text&quot; style=&quot;margin-bottom: 15px&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
    const contentId = &quot;summary-{PostId}&quot;
    makeSummaryFromNpf(contentId, {NPF});
&amp;lt;/script&amp;gt;
{/block:IndexPage}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적당히 만들어보니 그럴듯하게 만들어지는 것 같아서 요정도로만 하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;티스토리 댓글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리는 크게 달라진 점이 없어서 그냥 놔두려다가 아무래도 커스텀 도메인에서의 댓글이 너무 이상해서 이걸 좀 바꿔보고 싶었다. 일단 티스토리 댓글은 못쓴다. 그래서 다시 한번 disqus를 써볼까 했는데 disqus는 역시 무료플랜에서는 광고가 너무너무너무 많이 나와서 부담스럽다. 누가 댓글을 잘 쓰지도 않는데 혹시나 하고 달아둔 댓글에 돈을 내기도 뭣하고 그렇다고 광고가 한 화면 가득 나오는건 참을 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체재를 찾아봤는데 결국 &lt;a href=&quot;https://utteranc.es/&quot;&gt;utterances&lt;/a&gt; 와 &lt;a href=&quot;https://giscus.app/ko&quot;&gt;giscus&lt;/a&gt; 로 돌아가게 되었다. 이 둘의 문제는 github repo가 필요하다는 것인데 다행히 여러 사이트를 한 곳에서 관리할 수 있는 옵션이 가능했다. 카테고리별로 웹사이트를 관리할 수 있다. 그래서 그동안 쓰던 repo에서 사용하기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단했지만 오랜만에 또 보다보니 익숙해지는데 시간도 필요해서 약간은 시간이 걸렸다. 일단락되었으니 이제 글을 쓰는 일만 남았다. 공부한 것, 읽은 책, 영화 등등 잘 써봐야겠다.&lt;/p&gt;</description>
      <category>Dev</category>
      <category>블로그</category>
      <author>Joo</author>
      <guid isPermaLink="true">https://blog.joostory.net/620</guid>
      <comments>https://blog.joostory.net/620#entry620comment</comments>
      <pubDate>Tue, 17 Sep 2024 23:28:05 +0900</pubDate>
    </item>
  </channel>
</rss>