<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.8.7">Jekyll</generator><link href="https://ohyecloudy.com/pnotes/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ohyecloudy.com/pnotes/" rel="alternate" type="text/html" /><updated>2026-04-19T14:50:33+09:00</updated><id>https://ohyecloudy.com/pnotes/feed.xml</id><title type="html">ohyecloudy’s pnotes</title><subtitle>An amazing website.</subtitle><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><entry><title type="html">블로그 테마를 minimal mistakes로 바꾸다 - 네 개 전부</title><link href="https://ohyecloudy.com/pnotes/archives/jekyll-minimal-mistakes/" rel="alternate" type="text/html" title="블로그 테마를 minimal mistakes로 바꾸다 - 네 개 전부" /><published>2026-04-04T00:00:00+09:00</published><updated>2026-04-04T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/jekyll-minimal-mistakes</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/jekyll-minimal-mistakes/"><![CDATA[<p>포스트 옆에 목차가 나오는 글을 보고 나도 해보고 싶다. 직접 만들진 못하겠고 목차를 지원하는 Jekyll 템플릿을 찾았다.</p>

<p>목차를 비롯해 블로그 포스트 읽기를 도와주는 기능, 컬러 베리에이션, 유지 보수 걱정이 없게 많이 사용하는 테마. 블로그가 4개라서 컬러 베리에이션은 반드시 지원해야 한다. 이런 요구 사항을 만족하는 <a href="https://github.com/mmistakes/minimal-mistakes">Minimal Mistakes</a> 테마를 찾았다.</p>

<h1 id="org-ruby를-사용한-게-발목을-잡았다">org-ruby를 사용한 게 발목을 잡았다</h1>

<p>Emacs <a href="https://orgmode.org/">Org-mode</a>가 메인 마크업 언어다. markdown으로 바로 글쓰기가 싫다. markdown이 필요할 때는 Org-mode로 글을 쓰고 markdown으로 익스포트한다.</p>

<p>markdown을 지원하는 <a href="https://jekyllrb-ko.github.io/">Jekyll</a>로 글을 쓸 때도 마찬가지다. markdown으로 익스포트하는 과정을 없애보려고 org-ruby를 사용해서 바로 HTML을 뽑은 게 문제였다. minimal mistakes가 toc와 같은 기능을 지원하려면 markdown을 처리할 수 있어야 하는데, HTML을 뽑는 파이프라인을 따로 타고 있었기 때문이다.</p>

<p>org-ruby를 고치거나 Org-mode로 글을 쓰고 markdown을 익스포트해서 Jekyll 정규 파이프라인을 그대로 타게 해야 한다. <a href="https://ohyecloudy.com/emacsian/2026/03/14/writing-jekyll-blog-posts-in-org-mode/">org-ruby를 계속 수정하며 Jekyll 업데이트를 따라갈 에너지가 없어서 markdown으로 익스포트하게 고쳤다.</a></p>

<h1 id="설치">설치</h1>

<p>설치 방법은 <a href="https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/">Quick-Start Guide - Minimal Mistakes</a> 글을 참고하면 된다. 난 테마를 gem으로 설치하고 있다.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>

<span class="n">gem</span> <span class="s2">"minimal-mistakes-jekyll"</span>
</code></pre></div></div>

<p>블로그 빌드에 필요한 ruby는 <a href="https://ohyecloudy.com/pnotes/archives/asdf/">Asdf</a>로 설치해서 실행한다.</p>

<h1 id="커스터마이즈">커스터마이즈</h1>

<h2 id="검색-디폴트값인-lunr-대신-google-사용">검색 디폴트값인 lunr 대신 google 사용</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">search_provider          </span><span class="pi">:</span> <span class="s">google</span> <span class="c1"># lunr (default), algolia, google</span>
</code></pre></div></div>

<p><a href="https://lunrjs.com/">lunr</a>이 예쁘고 좋은데 크롤링에 너무 취약하다. 파일 용량이 너무 커서 google로 변경했다.</p>

<h2 id="google-website-analytics">google website, analytics</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">google</span><span class="pi">:</span>
  <span class="na">search_engine_id       </span><span class="pi">:</span> <span class="c1"># YOUR_SEARCH_ENGINE_ID</span>

<span class="na">analytics</span><span class="pi">:</span>
  <span class="na">provider               </span><span class="pi">:</span> <span class="s2">"</span><span class="s">google-gtag"</span> <span class="c1"># false (default), "google", "google-universal", "google-gtag", "custom"</span>
  <span class="na">google</span><span class="pi">:</span>
    <span class="na">tracking_id          </span><span class="pi">:</span> <span class="s2">"</span><span class="s">MY_TRACKING_ID"</span>
</code></pre></div></div>

<p>잘 나가는 테마를 쓰는 이유 중 하나다. 편하게 설정할 수 있다.</p>

<h2 id="asseturl-변수-사용">asseturl 변수 사용</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">asseturl </span><span class="pi">:</span> <span class="s2">"</span><span class="s">/pnotes/assets"</span>
</code></pre></div></div>

<p>블로그가 루트에 없어서 생기는 불편함이랄까?</p>

<p>둘 중 하나를 선택하면 된다.</p>

<ol>
  <li><code class="highlighter-rouge">{{ "/assets/AWESOME_IMAGE.jpg" | relative_url }}</code></li>
  <li><code class="highlighter-rouge">{{ site.asseturl }}/AWESOME_IMAGE.jpg</code></li>
</ol>

<p>난 2번이 편하다. 그래서 설정해서 사용하고 있다. 초창기부터 이렇게 사용해서 지금은 더 편하게 설정할 수 있는지는 모르겠다.</p>

<h2 id="포스트-defaults-설정">포스트 defaults 설정</h2>

<p>왠만한 건 여기서 다 설정할 수 있다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Defaults</span>
<span class="na">defaults</span><span class="pi">:</span>
  <span class="c1"># _posts</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">posts</span>
      <span class="na">values</span><span class="pi">:</span>
        <span class="na">layout</span><span class="pi">:</span> <span class="s">single</span>
        <span class="na">author_profile</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">read_time</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">share</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">related</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">mathjax</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">mermaid</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">adsense</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">page"</span>
      <span class="na">values</span><span class="pi">:</span>
        <span class="na">sitemap</span><span class="pi">:</span> <span class="no">false</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">tag</span>
      <span class="na">values</span><span class="pi">:</span>
        <span class="na">author_profile</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">sitemap</span><span class="pi">:</span> <span class="no">false</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">category</span>
      <span class="na">values</span><span class="pi">:</span>
        <span class="na">author_profile</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">sitemap</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<h2 id="google-adsense-수동-삽입">Google Adsense 수동 삽입</h2>

<p>자동 삽입으로 했는데, 읽는데 너무 방해된다. 광고는 붙이고 싶지만 읽는데 방해되는 건 싫다. 그래서 본문을 다 읽은 후 나오는 관련된 글 앞에 Google Adsense 삽입했다. <code class="highlighter-rouge">_includes/before-related.html</code> 파일을 만들어서 작업.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% if page.adsense %}
<span class="nt">&lt;script </span><span class="na">async</span> <span class="na">src=</span><span class="s">"GOOGLE ADSENSE"</span>
    <span class="na">crossorigin=</span><span class="s">"anonymous"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="c">&lt;!-- blog - pnotes --&gt;</span>
<span class="nt">&lt;ins</span> <span class="na">class=</span><span class="s">"adsbygoogle"</span> <span class="na">style=</span><span class="s">"display:block"</span> <span class="na">data-ad-client=</span><span class="s">"DATA-AD-CLIENT"</span> <span class="na">data-ad-slot=</span><span class="s">"DATA-AD-SLOT"</span>
    <span class="na">data-ad-format=</span><span class="s">"auto"</span> <span class="na">data-full-width-responsive=</span><span class="s">"true"</span><span class="nt">&gt;&lt;/ins&gt;</span>
<span class="nt">&lt;script&gt;</span>
    <span class="p">(</span><span class="nx">adsbygoogle</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">adsbygoogle</span> <span class="o">||</span> <span class="p">[]).</span><span class="nx">push</span><span class="p">({});</span>
<span class="nt">&lt;/script&gt;</span>
{% endif %}
</code></pre></div></div>

<h2 id="prev-next-버튼-사이에-random-버튼-추가">prev, next 버튼 사이에 random 버튼 추가</h2>

<p><a href="https://ohyecloudy.com/pnotes/archives/blogging-jekyll-a-random-post/">랜덤 글로 이동하는 기능</a>을 빼먹을 수 없지. 나도 가끔 누른다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{% if page.previous or page.next %}
<span class="nt">&lt;nav</span> <span class="na">class=</span><span class="s">"pagination"</span><span class="nt">&gt;</span>
    {% if page.previous %}
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"{{ page.previous.url | relative_url }}"</span> <span class="na">class=</span><span class="s">"pagination--pager"</span>
        <span class="na">title=</span><span class="s">"{{ page.previous.title | markdownify | strip_html }}"</span><span class="nt">&gt;</span>{{
        site.data.ui-text[site.locale].pagination_previous | default: "Previous" }}<span class="nt">&lt;/a&gt;</span>
    {% else %}
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"pagination--pager disabled"</span><span class="nt">&gt;</span>{{ site.data.ui-text[site.locale].pagination_previous | default:
        "Previous" }}<span class="nt">&lt;/a&gt;</span>
    {% endif %}
    <span class="c">&lt;!-- Random Page --&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"{{ "</span><span class="err">/</span><span class="na">random</span><span class="err">/"</span> <span class="err">|</span> <span class="na">relative_url</span> <span class="err">}}"</span> <span class="na">class=</span><span class="s">"pagination--pager"</span> <span class="na">title=</span><span class="s">"Random"</span><span class="nt">&gt;</span>Random<span class="nt">&lt;/a&gt;</span>
    {% if page.next %}
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"{{ page.next.url | relative_url }}"</span> <span class="na">class=</span><span class="s">"pagination--pager"</span>
        <span class="na">title=</span><span class="s">"{{ page.next.title | markdownify | strip_html }}"</span><span class="nt">&gt;</span>{{ site.data.ui-text[site.locale].pagination_next |
        default: "Next" }}<span class="nt">&lt;/a&gt;</span>
    {% else %}
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"pagination--pager disabled"</span><span class="nt">&gt;</span>{{ site.data.ui-text[site.locale].pagination_next | default: "Next"
        }}<span class="nt">&lt;/a&gt;</span>
    {% endif %}
<span class="nt">&lt;/nav&gt;</span>
{% endif %}
</code></pre></div></div>

<p><code class="highlighter-rouge">assets/css/main.scss</code> 를 복사하고 import 후 커스터마이징했다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
# Only the main Sass file needs front matter (the dashes are enough)
search: false
---

@charset "utf-8";

@import "minimal-mistakes/skins/{{ site.minimal_mistakes_skin | default: 'default' }}"; // skin
@import "minimal-mistakes"; // main partials

/*
Post pagination navigation links
========================================================================== */

/* next/previous buttons customize */
.pagination--pager { width: 33%; }
</code></pre></div></div>

<p>50%를 33%로 고쳤다.</p>

<h2 id="latex-랜더링-기능">LaTeX 랜더링 기능</h2>

<p>이건 자주 사용을 안 하더라도 붙여 놓으면 뿌듯하다.</p>

<p><a href="https://ohyecloudy.com/pnotes/archives/jekyll-mathjax-latex/">MathJax를 사용해서 붙였다.</a></p>

<h1 id="이전-이후-테마">이전 이후 테마</h1>

<h2 id="lifelog"><a href="https://ohyecloudy.com/lifelog/">lifelog</a></h2>

<h3 id="이전">이전</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-01.jpg" alt="nil" /></p>

<h3 id="이후">이후</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-02.jpg" alt="nil" /></p>

<h2 id="programming-notes">programming notes</h2>

<h3 id="이전-1">이전</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-03.jpg" alt="nil" /></p>

<h3 id="이후-1">이후</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-04.jpg" alt="nil" /></p>

<h2 id="emacsian"><a href="https://ohyecloudy.com/emacsian/">emacsian</a></h2>

<h3 id="이전-2">이전</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-05.jpg" alt="nil" /></p>

<h3 id="이후-2">이후</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-06.jpg" alt="nil" /></p>

<h2 id="dev-diary"><a href="https://ohyecloudy.com/ddiary/">dev-diary</a></h2>

<h3 id="이전-3">이전</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-07.jpg" alt="nil" /></p>

<h3 id="이후-3">이후</h3>

<p><img src="/pnotes/assets/2026-04-04-jekyll-minimal-mistakes-08.jpg" alt="nil" /></p>

<h1 id="마치며">마치며</h1>

<p><a href="https://ohyecloudy.com/emacsian/2026/03/14/writing-jekyll-blog-posts-in-org-mode/">Jekyll 표준 파이프라인을 타게 하는 작업</a>이 가장 크다. 이걸 해놔서 다음 테마를 바꿀 때는 이번처럼 힘들지는 않을 것 같다.</p>

<p>블로그마다 비주얼 개성이 있었다. 하지만 이제는 색깔만 다르다. 개성이 유지보수 효율화에 무릎을 꿇었다. 블로그 아이콘을 만들어서 좀 더 개성을 줄까 싶다.</p>

<p>테마가 마음에 들어 한동안 신경 쓰지 않고 쓸 것 같다.</p>

<h1 id="링크">링크</h1>

<ul>
  <li><a href="https://github.com/mmistakes/minimal-mistakes">mmistakes/minimal-mistakes - github.com</a></li>
  <li><a href="https://jekyllrb-ko.github.io/">Jekyll • 심플한, 블로그 지향적, 정적 사이트 - 평범한 텍스트 파일을 정적 웹사이트 또는 블로그로 변신시켜 보세요. - jekyl…</a>(<a href="http://web.archive.org/web/20260404031640/https://jekyllrb-ko.github.io/">archive</a>)</li>
  <li><a href="https://lunrjs.com/">Lunr: A bit like Solr, but much smaller and not as bright - lunrjs.com</a>(<a href="http://web.archive.org/web/20260404031648/https://lunrjs.com/">archive</a>)</li>
  <li><a href="https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/">Quick-Start Guide - Minimal Mistakes - mmistakes.github.io</a>(<a href="http://web.archive.org/web/20260404031651/https://mmistakes.github.io/minimal-mistakes/docs/quick-start-guide/">archive</a>)</li>
  <li><a href="https://ohyecloudy.com/emacsian/2026/03/14/writing-jekyll-blog-posts-in-org-mode/">Jekyll 블로그 포스트를 Org-mode로 쓰기 - org-ruby, ox-jekyll-lite - (emacsian ohyecloud…</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/asdf/">asdf 툴 버전 매니저로 프로젝트별 elixir 버전 관리 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/blogging-jekyll-a-random-post/">#blogging #jekyll 블로그에 랜덤 글 링크를 추가하기 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/jekyll-mathjax-latex/">MathJax로 LaTeX 수식을 블로그에서 그리자 - Jekyll Minimal Mistakes 테마에 적용 - ohyecloudy’s …</a></li>
  <li><a href="https://orgmode.org/">Org mode for GNU Emacs - orgmode.org</a></li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="blogging" /><category term="jekyll" /><category term="emacs" /><summary type="html"><![CDATA[포스트 옆에 목차가 나오는 글을 보고 나도 해보고 싶다. 직접 만들진 못하겠고 목차를 지원하는 Jekyll 템플릿을 찾았다.]]></summary></entry><entry><title type="html">Seven Concurrency Models in Seven Weeks (Paul Butcher, 2014) 독후감 - 복습하고 교양쌓고 배우기</title><link href="https://ohyecloudy.com/pnotes/archives/book-seven-concurrency-models-in-seven-weeks-2014/" rel="alternate" type="text/html" title="Seven Concurrency Models in Seven Weeks (Paul Butcher, 2014) 독후감 - 복습하고 교양쌓고 배우기" /><published>2026-03-22T00:00:00+09:00</published><updated>2026-03-22T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/book-seven-concurrency-models-in-seven-weeks-2014</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/book-seven-concurrency-models-in-seven-weeks-2014/"><![CDATA[<p>두 번째로 읽은 Seven Weeks 시리즈 책이다. ’<a href="https://ohyecloudy.com/pnotes/archives/book-seven-databases-in-seven-weeks/">Seven Databases in Seven Weeks (Luc Perkins et al., 2018)</a>’ 책을 재미있게 읽어서 블랙프라이데이 할인을 받아 이 책을 사서 읽었다.</p>

<h1 id="간단한-내용-소개">간단한 내용 소개</h1>

<p>Threads and Locks, Functional Programming, Clojure, Elixir, Go Channel, OpenCL, Hadoop의 Concurrency(동시성) 모델을 소개한다. 함수형 프로그래밍과 Elixir 챕터는 편하게 읽었다. 저자가 Clojure를 엄청 좋아하는 것 같다. Go Channel을 Go 언어가 아니라 Clojure로 설명한다.</p>

<h1 id="재미있었던-cspcommunicating-sequential-processes">재미있었던 CSP(communicating sequential processes)</h1>

<p>Threads and Locks은 복습한다는 기분으로 읽었고 OpenCL과 Hadoop은 교양 삼아 읽었다. 제일 재미있게 읽었던 챕터는 CSP(communicating sequential processes) 설명 챕터였다.</p>

<blockquote>
  <p>A program using the communicating sequential processes model similarly consists of independent, concurrently executing entities that communicate by sending each other messages. The difference is one of emphasis - instead of focusing on the entities sending the messages, CSP focuses on the channels over which they are sent. Channels are first class- instead of each process being tightly coupled to a single mailbox, channels can be independently created, written to, read from, and passed between processes.</p>

  <p>통신 순차 프로세스 모델을 사용하는 프로그램은 마찬가지로 독립적이고 동시에 실행되는 엔티티들로 구성되며, 이들은 서로 메시지를 전송하여 통신합니다. 차이점은 강조점의 차이입니다 - 메시지를 전송하는 엔티티에 초점을 맞추는 대신, CSP는 메시지가 전송되는 채널에 초점을 맞춥니다. 채널은 첫 번째 클래스입니다 - 각 프로세스가 단일 메일박스와 밀접하게 연결되는 대신, 채널은 독립적으로 생성되고, 쓰이며, 읽히며, 프로세스 간에 전달될 수 있습니다.</p>
</blockquote>

<p>Actor Model을 사용하는 Erlang과 Elixir에서는 채널이라고 부를 수 있는 Mailbox가 Actor과 강결합이 되어 있다. 하지만 CSP에서는 Go channel 처럼 채널 자체를 독립적으로 쓸 수 있다. 채널이 일급 객체다.</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">start</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nf">go</span><span class="w">
    </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">wizard</span><span class="w"> </span><span class="p">(</span><span class="nf">dom/getElement</span><span class="w"> </span><span class="s">"wizard"</span><span class="p">)</span><span class="w">
          </span><span class="n">step1</span><span class="w"> </span><span class="p">(</span><span class="nf">dom/getElement</span><span class="w"> </span><span class="s">"step1"</span><span class="p">)</span><span class="w">
          </span><span class="n">step2</span><span class="w"> </span><span class="p">(</span><span class="nf">dom/getElement</span><span class="w"> </span><span class="s">"step2"</span><span class="p">)</span><span class="w">
          </span><span class="n">step3</span><span class="w"> </span><span class="p">(</span><span class="nf">dom/getElement</span><span class="w"> </span><span class="s">"step3"</span><span class="p">)</span><span class="w">
          </span><span class="n">next-button</span><span class="w"> </span><span class="p">(</span><span class="nf">dom/getElement</span><span class="w"> </span><span class="s">"next"</span><span class="p">)</span><span class="w">
          </span><span class="n">next-clicks</span><span class="w"> </span><span class="p">(</span><span class="nf">get-events</span><span class="w"> </span><span class="n">next-button</span><span class="w"> </span><span class="s">"click"</span><span class="p">)]</span><span class="w">
      </span><span class="p">(</span><span class="nf">show</span><span class="w"> </span><span class="n">step1</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">&lt;!</span><span class="w"> </span><span class="n">next-clicks</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">hide</span><span class="w"> </span><span class="n">step1</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">show</span><span class="w"> </span><span class="n">step2</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">&lt;!</span><span class="w"> </span><span class="n">next-clicks</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">set-value</span><span class="w"> </span><span class="n">next-button</span><span class="w"> </span><span class="s">"Finish"</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">hide</span><span class="w"> </span><span class="n">step2</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">show</span><span class="w"> </span><span class="n">step3</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">&lt;!</span><span class="w"> </span><span class="n">next-clicks</span><span class="p">)</span><span class="w">
      </span><span class="p">(</span><span class="nf">.submit</span><span class="w"> </span><span class="n">wizard</span><span class="p">))))</span><span class="w">

</span><span class="p">(</span><span class="nf">set!</span><span class="w"> </span><span class="p">(</span><span class="nf">.-onload</span><span class="w"> </span><span class="n">js/window</span><span class="p">)</span><span class="w"> </span><span class="n">start</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>채널로 콜백을 풀어내는 코드가 인상적이었다.</p>

<h1 id="마치며">마치며</h1>

<p>모아서 정리하면 얽힌 개념이 명료하게 정리되는 기분을 느끼곤 한다. 그런 즐거움을 주는 책이다.</p>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="book" /><category term="concurrency" /><category term="seven-weeks" /><summary type="html"><![CDATA[두 번째로 읽은 Seven Weeks 시리즈 책이다. ’Seven Databases in Seven Weeks (Luc Perkins et al., 2018)’ 책을 재미있게 읽어서 블랙프라이데이 할인을 받아 이 책을 사서 읽었다.]]></summary></entry><entry><title type="html">Quick Iteration in Elixir (Jason Axelson, ElixirConf 2022) 발표 리뷰 - 도구 벼리기 자극</title><link href="https://ohyecloudy.com/pnotes/archives/quick-iteration-in-elixir-jason-axelson-elixirconf-2022/" rel="alternate" type="text/html" title="Quick Iteration in Elixir (Jason Axelson, ElixirConf 2022) 발표 리뷰 - 도구 벼리기 자극" /><published>2026-03-01T00:00:00+09:00</published><updated>2026-03-01T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/quick-iteration-in-elixir-jason-axelson-elixirconf-2022</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/quick-iteration-in-elixir-jason-axelson-elixirconf-2022/"><![CDATA[<p>’<a href="https://www.youtube.com/watch?v=BotVs6TXR-c">ElixirConf 2022 - Jason Axelson - Quick Iteration in Elixir - Tips from 6 Yrs of Elixir Development</a>’ 발표를 재미있게 봤다. 팁을 가볍게 정리하고 넘기려다가 발표자의 매일 자신의 도구를 익히는 자세를 기억하고 싶어서 글로 남긴다.</p>

<h1 id="elixir-interactive-shell인-iex-팁">Elixir interactive shell인 IEx 팁</h1>

<p>IEx는 Elixir의 interactive shell이다. 코드를 짜고 실행해서 동작을 확인할 수 있다. 몰입되게 하는 빠른 루프를 제공한다. user switch command는 처음 알았다.</p>

<h2 id="c-auto-bracket"><code class="highlighter-rouge">C+]</code> auto bracket</h2>

<p>자동으로 짝을 맞춰준다. 도움말에도 안 보이던데 이런 키는 어떻게 알았데?</p>

<h2 id="recompile"><code class="highlighter-rouge">recompile</code></h2>

<p>전체를 재컴파일하는 함수다. 특정 모듈을 재컴파일하는 <code class="highlighter-rouge">r/1</code> 함수도 있는데 이건 귀찮아서 잘 사용을 안 하게 된다.</p>

<h2 id="h0-h1"><code class="highlighter-rouge">h/0</code>, <code class="highlighter-rouge">h/1</code></h2>

<p>인자 없이 <code class="highlighter-rouge">h</code> 함수를 실행하면 전체 명령을 볼 수 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex&gt; h

                                  IEx.Helpers

Welcome to Interactive Elixir. You are currently seeing the documentation for
the module IEx.Helpers which provides many helpers to make Elixir's shell more
joyful to work with.

This message was triggered by invoking the helper h(), usually referred to as
h/0 (since it expects 0 arguments).
...
</code></pre></div></div>

<p>함수나 모듈을 인자로 넘겨</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(1)&gt; h Enum.map

                            def map(enumerable, fun)

  @spec map(t(), (element() -&gt; any())) :: list()
  ...
</code></pre></div></div>

<h2 id="i1"><code class="highlighter-rouge">i/1</code></h2>

<p>데이터 타입 정보를 출력한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex&gt; i {:ok, %{a: :b}}
Term
  {:ok, %{a: :b}}
Data type
  Tuple
Reference modules
  Tuple
Implemented protocols
  IEx.Info, Inspect
</code></pre></div></div>

<h2 id="b1"><code class="highlighter-rouge">b/1</code></h2>

<p>콜백 함수 정보를 출력한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex&gt; b GenServer
@callback code_change(old_vsn, state :: term(), extra :: term()) ::
            {:ok, new_state :: term()} | {:error, reason :: term()}
          when old_vsn: term() | {:down, term()}

@callback format_status(reason, pdict_and_state :: list()) :: term()
          when reason: :normal | :terminate

...
</code></pre></div></div>

<h2 id="open1"><code class="highlighter-rouge">open/1</code></h2>

<p>파일을 <code class="highlighter-rouge">ELIXIR_EDITOR</code> 환경변수로 정의한 에디터로 열어준다. Emacs로 열고 싶으면 아래와 같이 세팅한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ~export ELIXIR_EDITOR="emacsclient --no-wait +__LINE__ __FILE__"
</code></pre></div></div>

<p>IEx에서 소스 코드를 보고 싶으면 함수나 모듈 앞에 open을 붙인다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex&gt; open Enum.map
</code></pre></div></div>

<h2 id="iexexs"><code class="highlighter-rouge">~/.iex.exs</code></h2>

<p>IEx 기본 설정을 <code class="highlighter-rouge">~/.iex.exs</code> 파일에 정의한다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"Loaded ~/.iex.exs"</span><span class="p">)</span>

<span class="no">IEx</span><span class="o">.</span><span class="n">configure</span><span class="p">(</span><span class="ss">inspect:</span> <span class="p">[</span><span class="ss">charlists:</span> <span class="ss">:as_lists</span><span class="p">])</span>
</code></pre></div></div>

<p>Elixir 프로젝트 루트에 <code class="highlighter-rouge">.iex.exs</code> 파일을 만들고 아래 코드를 추가하면 공통 설정을 손쉽게 사용할 수 있다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="no">Path</span><span class="o">.</span><span class="n">expand</span><span class="p">(</span><span class="s2">"~/.iex.exs"</span><span class="p">))</span> <span class="o">&amp;&amp;</span> <span class="n">import_file</span><span class="p">(</span><span class="s2">"~/.iex.exs"</span><span class="p">)</span>
</code></pre></div></div>

<p>있으면 편하다. 발표를 보고 바로 설정했다. 개인적으로 사용하는 <a href="https://github.com/ohyecloudy/template-elixir/blob/main/.iex.exs">Elixir 프로젝트 template</a>에도 추가했다.</p>

<h2 id="user-switch-command">user switch command</h2>

<p>IEx와 shell은 1:1 관계가 아니다. 이번에 처음 알게 됐다. shell을 관리할 수 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(1)&gt; Enum.each(1..10_000_000, fn _ -&gt; :timer.sleep(1000) end)
</code></pre></div></div>

<p>위처럼 무한루프가 걸렸다면? <code class="highlighter-rouge">C-c C-c</code> 로 종료할 수 있다. <code class="highlighter-rouge">C-g</code> 키를 눌러 user switch command를 사용하면 IEx를 종료하지 않고 interrupt 할 수 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(1)&gt; Enum.each(1..10_000_000, fn _ -&gt; :timer.sleep(1000) end)
# 여기서 C-g 입력
User switch command
 --&gt; h
  c [nn]            - connect to job
  i [nn]            - interrupt job
  k [nn]            - kill job
  j                 - list all jobs
  s [shell]         - start local shell
  r [node [shell]]  - start remote shell
  q                 - quit erlang
  ? | h             - this message
 --&gt; j
   1* {'Elixir.IEx',start,[[{dot_iex_path,nil},{on_eof,halt}],{elixir,start_cli,[]}]}
 --&gt; i 1
 --&gt; c 1
** (EXIT) interrupted

Interactive Elixir (1.16.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&gt;
</code></pre></div></div>

<p>이건 사실 <code class="highlighter-rouge">C-c C-c</code> 를 눌러 종료하고 다시 IEx를 시작하는 것과 별로 달라 보이지 않는다.</p>

<p>새로운 shell을 만들어서 넘어가고 전환하며 작업할수도 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>iex(1)&gt; hello = :iex
:iex
User switch command
 --&gt; s 'Elixir.IEx'
 --&gt; j
   1  {'Elixir.IEx',start,[[{dot_iex_path,nil},{on_eof,halt}],{elixir,start_cli,[]}]}
   2* {'Elixir.IEx',start,[]}
 --&gt; c 2
Interactive Elixir (1.16.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&gt; hello
error: undefined variable "hello"
└─ iex:1

** (CompileError) cannot compile code (errors have been logged)

User switch command
 --&gt; c 1

iex(3)&gt; hello
:iex
</code></pre></div></div>

<p>IEx가 내부적으론 Erlang의 <code class="highlighter-rouge">erl</code> 셸을 사용하기 때문에 새로운 IEx 시작을 <code class="highlighter-rouge">s 'Elixir.IEx'</code> 로 했다.</p>

<p>그래서 어떻게 활용하면 좋을까? 그건 잘 모르겠다. 언젠가는 유용한 도구로 사용할 상황이 오지 않을까 기대한다.</p>

<h2 id="shell-history">shell history</h2>

<p>IEx를 종료해서 입력한 명령 히스토리를 유지할 수 있다. <code class="highlighter-rouge">iex --erl "-kernel shell_history enabled"</code> 처럼 인자로 넣어도 되는데, 난 환경 변수에 세팅했다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export ERL_AFLAGS="-kernel shell_history enabled"
</code></pre></div></div>

<h1 id="의존성-라이브러리를-수정해서-테스트하려면">의존성 라이브러리를 수정해서 테스트하려면</h1>

<p>우선 받아놓고 버전이 아니라 path로 경로를 지정하면 된다.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">defp deps do
</span><span class="err">[</span>
-    {:ecto, "~&gt; 3.12"}
<span class="gi">+    {:ecto, path: "deps/ecto"}
</span><span class="err">]</span>
end
</code></pre></div></div>

<h1 id="exsync---코드-수정을-감지하고-자동-컴파일">ExSync - 코드 수정을 감지하고 자동 컴파일</h1>

<p><a href="https://hexdocs.pm/exsync/readme.html">ExSync</a> 라이브러리를 의존성에 추가하면 된다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">deps</span> <span class="k">do</span>
  <span class="p">[</span>
    <span class="p">{</span><span class="ss">:exsync</span><span class="p">,</span> <span class="s2">"~&gt; 0.4"</span><span class="p">,</span> <span class="ss">only:</span> <span class="ss">:dev</span><span class="p">},</span>
  <span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<h1 id="적극적으로-활용하는-snippets">적극적으로 활용하는 snippets</h1>

<p>반복되는 코드 패턴을 snippet으로 등록해서 자잘한 실수를 줄이고 효율도 높인다.</p>

<p>발표에서는 이런 snippet들을 소개한다.</p>

<ul>
  <li><code class="highlighter-rouge">lin</code> -&gt; <code class="highlighter-rouge">IO.inspect($1, label: $1)</code></li>
  <li><code class="highlighter-rouge">pin</code> -&gt; <code class="highlighter-rouge">|&gt; IO.inpsect(label: "$1")</code></li>
  <li><code class="highlighter-rouge">logi</code> -&gt; <code class="highlighter-rouge">Logger.info("$1: #{inspect($1, pretty: true)}")</code></li>
  <li><code class="highlighter-rouge">k</code> -&gt; <code class="highlighter-rouge">$1: $1</code></li>
  <li><code class="highlighter-rouge">label</code> -&gt; <code class="highlighter-rouge">IO.inspect($1, label: "$1 (hello_pehoenix.ex:15)")</code></li>
</ul>

<p>나는 일일이 다 쳤던 것 같다. 발표 보고 반성을 좀 했다. snippet? GitHub Copilot이 이런 건 다 해결해 주지 않나? 아니다. 내 의도를 추론 없이 표현하는 방법이 필요하다. 게다가 이렇게 세팅한 snippet이 Coding agent(코딩 에이전트)를 더 부스팅해준다.</p>

<h1 id="도구-벼리기sharpening-tools">도구 벼리기(sharpening tools)</h1>

<blockquote>
  <ul>
    <li>5-10 minutes of getting to know my tools</li>
    <li>daily-ish practice</li>
    <li>it’s all about continual learning</li>
    <li>i keep a doc to capture ideas and annoyances in the moment
      <ul>
        <li>review them later</li>
      </ul>
    </li>
  </ul>
</blockquote>

<p>발표를 한 번에 만들었을 것 같지 않다. 도구 벼리기 챕터에서 설명하는 것처럼 매일 조금씩 배우고 발전시키며 남겨놓은 기록을 모으니 이런 발표자료가 만들어졌을 것이다. 배움은 증분이다. 멋지다.</p>

<h1 id="마치며">마치며</h1>

<p>몰랐던 IEx 기능과 ExSync를 배웠다. snippet 활용에 대한 자극을 받았다.</p>

<p>매일 조금씩 자신의 도구를 배우고 벼리는 시간은 복리 보상으로 찾아온다.</p>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="uncategorized" /><category term="elixir" /><category term="tool" /><category term="sharpening-tools" /><summary type="html"><![CDATA[’ElixirConf 2022 - Jason Axelson - Quick Iteration in Elixir - Tips from 6 Yrs of Elixir Development’ 발표를 재미있게 봤다. 팁을 가볍게 정리하고 넘기려다가 발표자의 매일 자신의 도구를 익히는 자세를 기억하고 싶어서 글로 남긴다.]]></summary></entry><entry><title type="html">#retrospective 2025년 회고</title><link href="https://ohyecloudy.com/pnotes/archives/retrospective-2025/" rel="alternate" type="text/html" title="#retrospective 2025년 회고" /><published>2026-02-07T00:00:00+09:00</published><updated>2026-02-07T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/retrospective-2025</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/retrospective-2025/"><![CDATA[<p><a href="https://ohyecloudy.com/pnotes/archives/work-20th-anniversary/">게임 프로그래머 20주년</a>이 됐고 <a href="https://ohyecloudy.com/pnotes/archives/402/">2009년</a>부터 시작한 연간회고는 올해로 17번째가 됐다.</p>

<h1 id="뒤를-돌아보니-20년-앞에는-얼마나-남았을까">뒤를 돌아보니 20년, 앞에는 얼마나 남았을까?</h1>

<p>게임 개발자가 된 지 20년이 지났다. 배운 걸 파먹고 살 수 있을 거로 생각하진 않았지만 이토록 급격하게 바뀔 거라고 예상하진 못했다. Coding agent(코딩 에이전트)가 계속해서 묻는다. 이래도 프로그래밍이 수단이 아니고 목적이야? 다이나믹하다.</p>

<p>호기심과 숙달욕을 유지해야 한다. 코딩 에이전트가 코드를 짜는 것에만 영향을 주는 건 아니다. 예전에는 엄두가 안 나는 방대한 양의 소스를 적극적으로 볼 수 있다. 이제 기반 코드를 블랙박스로 취급할 것이냐는 호기심과 숙달욕이 결정한다.</p>

<p>비판적인 사고가 중요하다. 내 의견을 정리해서 개진할 수 있어야 한다. 새로운 도구가 나와도 사용하는 사람은 바뀌지 않는다. 웹사이트 링크만 던지고 코멘트를 구걸하던 사람은 LLM의 대답을 캡처해서 그대로 붙일 뿐이다. 확률적으로 다음 말을 생성할 뿐 아무런 의견도 책임도 없다. 내 주장을 만들기 위한 비판적인 사고가 더 중요해지고 있다.</p>

<p>리프레시 시간을 자주 가져야 한다. 좀 길게 쉬니깐 장래에 대한 불안감이 생긴다. 좋은 걱정인 것 같다. 하루하루 치여서 땅만 보고 걷다가 갑자기 종점에 도달하는 것만큼 무서운 게 없다. 휴가를 붙여서 길게 쉬는 기간을 더 많이 가져야겠다.</p>

<p>다행히 배우는 게 즐겁다. 이 직업을 선택해서 다행이다.</p>

<h1 id="러닝-출퇴근">러닝 출퇴근</h1>

<p><a href="https://ohyecloudy.com/lifelog/archives/my-one-year-boxing-experience/">Boxing</a>을 쉬고 있다. 1년 정도 다녔다. 어깨충돌증후군 회전근개 염증을 진단받았다. 안 아프고 운동하는 게 이렇게 힘들다. 맞아서 다친 게 아니라 때리다가 다쳐서 원망할 곳도 없다.</p>

<p>자리 잡은 운동 루틴에 먼지가 쌓이도록 방치할 수 없었다. 이런 루틴을 어떻게 만들었는데. 출근 전에 달리기를 시작했다. 아내가 말했다. 왜 달리고 씻고 자전거를 타고 출근하냐. 그냥 뛰어서 출근하면 되는 건 아니냐. 맞다! 뛰어서 출근하면 된다.</p>

<p>Running 출퇴근하고 있다. 코로나19 시절에 무리하게 뛰었다가 족저근막염이 걸린 적이 있다. 그래서 격일로 충분히 쉬면서 달리고 있다. 뛰어서 출근하면 하루를 1승으로 연 것 같아서 뿌듯하다. 회사에 샤워실이 있어서 고맙다.</p>

<p><a href="https://ohyecloudy.com/lifelog/archives/givenrace-marathon-2020/">기브앤레이스 가상 마라톤 (2020)</a> 할 때가 기억난다. 오프라인에서 떼로 달리고 싶었다. 평소 6km 정도 달려서 10km도 달릴 만했다. 올해 <a href="https://ohyecloudy.com/lifelog/archives/marathon-run-seoul-run-2025/">런 서울 런 2025</a>과 <a href="https://ohyecloudy.com/lifelog/archives/marathon-2025-go-free-run/">Go Free Run 2025</a>에 참가했다. 10km 달리기가 그리 힘들지 않았다. 좀 더 연습해서 하프 마라톤에도 참가해 보고 싶다.</p>

<p>달리기랑 자전거로 번갈아 가며 출퇴근하고 있다. 쾌적하다. 버스 타고 출퇴근할 때, 삶의 질이 확 떨어진다.</p>

<h1 id="네오위즈로-이직">네오위즈로 이직</h1>

<p>네오위즈 ROUND8 스튜디오로 소속이 바뀌었다. 패키지 게임을 만들고 있다. 경력 내내 서버가 있는 온라인 게임을 만들었다. 서버가 없는 첫 게임이다.</p>

<p>딸기부엉이와 Taek이 해볼 수 있는 게임을 만든다. 전체이용가 게임을 만드는 건 아니다. 아마도 청소년 이용 불가를 받지 않을까? 라이브 서비스 게임이 아니라서 가능하다. 잘만 만들면 어른이 돼서 지금 만들고 있는 게임을 할 수 있을지도 모른다. 패키지 게임이 주는 완결성의 보상이다.</p>

<p>이전에 다니는 회사와 같은 건물이다. 심지어 같은 엘리베이터를 탄다. 이전 직장 동료를 보면 무안하지 않을까요? 이런 질문을 받은 적이 있다. 아마도 처음에는 무안하다. 하지만 내가 이직한 게 엄청난 사건인가? 아니다. 사소한 일일 뿐이다. 금세 잊히고 예전에 알고 있던 사람이니 인사를 하는 정도이다. 짧게 줄여서 답한다면 ’사소한 일이다’.</p>

<p>이제 다시 Unreal Engine을 사용하는 클라이언트에 집중할 차례다.</p>

<h1 id="wheelyday-야구-여행">WheelyDay, 야구, 여행</h1>

<p><a href="https://ohyecloudy.com/lifelog/archives/family-wheely-day-2025/">주말 가족 라이딩을 WheelyDay</a>라고 부르고 있다. 주말에 자전거를 타고 맥모닝을 먹고 좀 쉬다가 다시 돌아온다. 편도 6km 정도 거리를 달린다. 조금씩 거리를 늘려보고 있다.</p>

<p>딸기부엉이와 야구를 보러 갔다. 아빠가 줄 수 있는 경험이라 생각했다. <a href="https://ohyecloudy.com/lifelog/archives/baseball-2025/">맛만 보려고 했는데 광팬이 되어 버렸다</a>. 2026년에도 야구 경기를 같이 보러 갈 것 같다.</p>

<p>1박 이상 가족 여행을 6번 갔다. 6월 이전에 여행을 많이 갔다. 6월부터 WheelyDay 때문에 주말이 바빠졌다. 매주 자전거를 타니 여행을 가고 싶단 생각이 안 들었나 보다.</p>

<ul>
  <li><a href="https://ohyecloudy.com/lifelog/archives/place-phoenix-park-2025-01/">강원도 휘닉스 파크 2025년 1월</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/family-trip-uljin-dukgu-hot-spring/">경상북도 덕구온천 2025년 2월</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-mongsanpo-beach-may-2025/">충청남도 몽산포 해수욕장 2025년 5월</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-forest-resom-2025-06/">충청북도 포레스트 리솜 2025년 6월</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-gangchon-grape-pension-auto-camping-site/">강원도 강촌 포도펜션 오토캠핑장 2025년 6월</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-gunsan-2025/">전북 군산 2025년 12월</a></li>
</ul>

<p>난 뜨끈하게 지졌던 덕구온천이 가장 좋았다.</p>

<p>내가 딸기부엉이 수학을 가르쳐 주고 있다. 역시 자기 자식을 가르치는 건 보통 일이 아니다. 과외 선생을 해 본 경험이 아무런 도움이 되지 않는다. 태도와 수준에 대한 기대치가 높기 때문이다. 항상 선서하고 시작한다. 나는 화내지 않겠다고 선서한다. 딸기부엉이는 집중하고 글씨를 똑바로 쓰겠다고 한다. 기다려주는 게 가장 힘들다. 내 속도와 딸기부엉이 속도가 다르다는 걸 인정해야 한다. 내가 더 어른이 되어야 한다.</p>

<h1 id="personal-code삶의-실행-규칙">Personal code(삶의 실행 규칙)</h1>

<p>가장 중요한 걸 먼저 한다. 모든 걸 다 할 수는 없다. 삶도 일도 마찬가지다. 태깅과 정렬에 시간을 항상 할애한다. 최소 5분 정도만 해도 만족도가 다르다. 태깅 시간이 아까운 작은 일은 중요도에 상관없이 바로 처리한다. 못하는 일에 관대해지자. 중요한 걸 먼저 하다 보면 모든 일을 못 하고 다음 날로 넘기는 일이 생길 수밖에 없다.</p>

<p>백언이 불여일행. 한 번의 실행이 백번의 말을 압도한다. AI 때문에 행동의 비용이 비약적으로 낮아졌다. 그래도 사람은 달라지지 않는다. 여전히 말만 하는 사람이 있고 해보는 사람이 있다. 벌어지는 차이가 더 벌어질 뿐이다. 예전에는 실행하는 데 1년이 걸렸다면 이제는 1달이면 된다. 1년 동안 말만 하는 사람과 실행하는 사람의 차이가 12배가 넘게 나는 시대다. 실행하는 근육을 가져야 한다.</p>

<p>하기 전보다 하고 난 뒤에 기분이 좋은 걸 해야 한다. 운동 가기 정말 싫다. 바로 일어나서 씻지 않고 앉아서 운동을 가면 안 되는 이유 백만 가지를 생각한다. 이걸 이기고 운동하러 간다면 돌아오는 길이 뿌듯하다. 이렇게 하고 난 뒤에 기분이 좋은 걸 해야 한다.</p>

<p>환경에 영향받으니 환경에 신경을 써야 한다. TV를 치우고 책장으로 거실 한쪽을 채웠다. TV는 스탠바이미로 바꿨다. 책상을 정리했다. 모니터 받침대도 두고 멀티탭도 정리했다. 책을 많이 볼 수 있는 거실 환경을 만들고 집중에 도움 줄 수 있게 깔끔하게 책상 위를 정리했다. 요즘은 가구 구경하는 것도 재미있다.</p>

<p>무례하지 않고 손해가 없는 건 물어야 한다. 거추장스럽고 불필요하다고 지레짐작하면 남는 건 손해뿐이다. 어쩌면 거절당하는 것을 두려워했기 때문이다. 그에 따른 방어기제로 아마 안 될 거라고 생각했다. 내 행동이 정당화된다. <a href="https://ohyecloudy.com/lifelog/archives/cool-things-office-lens/">오피스 렌즈</a>가 좋은 사례이다. 기능성 렌즈를 써보고 안 맞으면 어떡하지? 반품이 가능한지 물어봤던 것 같다. 일반 렌즈보다 비싼데 안 맞는다고 돈을 낭비할 수는 없다. 추가금을 내고 더 비싼 오피스렌즈로 교체할 수 있었다.</p>

<p>직장 동료와의 적절한 온도를 유지할 수 있어야 한다. 모두와 친할 수는 없다. 세계관 자체가 다른 동료와도 비즈니스 관계를 유지할 수 있어야 한다. 개인적인 감정이 업무에 영향을 주지 않도록 훈련해야 한다. 나는 평균 온도를 약간 올리는 태도로 이제까지 일해왔다. 이 정도로도 충분하다. 그렇다고 비즈니스 관계라고 단정 지을 필요는 없다. 직장에서 많은 시간을 보내고 여러 사람을 만나게 된다. 그중 마음이 맞는 사람도 찾을 수 있다. 그런 기회까지 놓치기엔 직장에서 보내는 시간이 아깝다. 적정한 온도를 유지하되 마음이 맞는 사람을 찾을 수 있는 열린 태도도 유지해야 한다.</p>

<p>AI 오마카세를 피해야 한다. YouTube의 숏츠가 대표적인 예다. 추천 목록은 도움이 될 때가 있다. 이건 괜찮은데 AI가 골라주는 영상을 아무 생각 없이 다음 영상으로 보는 걸 피해야 한다. 컨텐츠를 보는 건 내가 결정해야 한다. YouTube 숏츠는 보기 전보다 보고 난 후에 기분이 안 좋은 대표적인 콘텐트다.</p>

<h1 id="사이드-프로젝트-실행-근육-단련">사이드 프로젝트 실행 근육 단련</h1>

<p>매일 자기 전에 30분 이상 하는 걸 목표로 뭔가를 만들어보고 있다. 2025년에는 48% 달성으로 기대보다 저조하다. 사이드 프로젝트를 안 하고 잠에 들면 몸이 근질근질할 정도로 근육이 만들어지진 않았다. 근육통을 겪고 있는 단계이다.</p>

<p>내가 Genie라고 부르는 투자 프로젝트는 2025년부터 실행되고 있다. 새로운 전략을 계속 추가하고 유지 보수하느라 이것 하나만 했다. 지금 구현 중인 전략 하나를 마무리 짓고 다른 사이드 프로젝트도 해 볼 계획이다. Elxir로만 사이드 프로젝트를 진행했는데 서버가 아닌 앱이나 Unreal Engine 플러그인도 만들어보고 싶다.</p>

<p>Claude code는 내 실행 근육을 더 자극하고 있다. 사이드 프로젝트를 진행하기 위한 최소 멘탈 에너지를 획기적으로 낮췄다. 내 의지를 시험한다. 피곤해도 어느 정도 할 수 있는데, 그냥 잘래?</p>

<h1 id="bodyops---노안-근력-운동">BodyOps - 노안, 근력 운동</h1>

<p>바디빌딩이 아니라 바디옵스를 해야 한다. 과시나 자기만족을 위한 게 아니라 유지하기 위해 운용해야 한다. 그래서 근력 운동을 시작했다. 나이를 먹어 근육이 빠지는 디퍼브가 걸렸다. 나중에 다시 Boxing을 하던 수영하든 병행해야 한다. 바디빌딩을 하는 곳이 피트니스 센터라면 빌드 머신이라 불러야 하는 건가?</p>

<p>회사에서 쓸 오피스 렌즈와 일상생활에서 쓸 다초점 렌즈를 맞췄다. 누진 렌즈 하나로 퉁쳐보려고 했지만 모니터를 보는 시간이 길어서 눈의 피로를 줄일 수 있는 오피스 렌즈를 하나 더 맞췄다. 이제 좀 노안 스트레스가 줄어들었다. 이도류가 돼서 귀찮아졌지만 잊어먹는 일 없이 잘 적응하고 있다.</p>

<h1 id="독서---기술-서적-2권-교양서적-1권-투자책-1권-중단-2권">독서 - 기술 서적 2권, 교양서적 1권, 투자책 1권, 중단 2권</h1>

<p>기술 서적은 두 권을 읽고 한 권을 포기했다.</p>

<ul>
  <li><a href="https://ohyecloudy.com/pnotes/archives/book-the-programmer-brain/">프로그래머의 뇌 (펠리너 헤르만스, 2022)</a>
    <ul>
      <li>LTM(long-term memory)에 저장을 더 열심히 해야겠다. 플래시 카드 앱을 적극적으로 사용하려고 한다.</li>
    </ul>
  </li>
  <li>Seven Concurrency Models in Seven Weeks (Paul Butcher, 2014)
    <ul>
      <li>복습하는 느낌으로 7가지 모델을 공부했다. 내가 좋아하는 Seven Weeks 시리즈다.</li>
    </ul>
  </li>
  <li>유연한 소프트웨어를 만드는 설계 원칙 (크리스 핸슨, 제럴드 제이 서스먼, 2022)
    <ul>
      <li>예제 코드를 실행하기도 어렵고 책 내용도 어렵다. 지금은 때가 아닌 것 같아서 포기했다.</li>
    </ul>
  </li>
</ul>

<p>교양서적은 한 권을 읽었다. 한 권은 중단했다. 독후감은 둘 다 썼다.</p>

<ul>
  <li>틀리지 않는 법 (조던 엘렌버그, 2016)
    <ul>
      <li>교양 수학은 이렇게 재미있는 것이다. 저자의 다음 책도 읽어보고 싶다. 확률이 가장 이해하기가 어려웠다. 확률과 통계는 어렵다.</li>
    </ul>
  </li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/book-the-mind-is-flat-2019/">생각한다는 착각 (닉 채터, 2021)</a>
    <ul>
      <li>찾아 헤매던 우리 마음의 깊이 같은 건 없다. 이런 접근이 인공지능의 급격한 발전을 가져왔다. LLM(large language model) 동작 원리를 설명하는 책 같기도 하다.</li>
    </ul>
  </li>
</ul>

<p>투자 책은 한 권 읽었다.</p>

<ul>
  <li>터틀의 방식 (커티스 페이스, 2010)
    <ul>
      <li>비밀스러운 트레이딩 전략이 있는 게 아니다. 필요한 건 일관성과 자제력이다. 이걸 갖기 위해서는 반드시 이론적인 바탕과 믿음이 있어야 한다.</li>
    </ul>
  </li>
</ul>

<p>종이가 아니라 PDF로 읽거나 E-Reader(이북리더기)로 책을 읽고 있다. 이게 아니었으면 책을 더 못 읽었을 것 같다. 종이책 고유의 촉각적 경험을 포기하기로 했다.</p>

<h1 id="게임---게임-3개">게임 - 게임 3개</h1>

<p>’<a href="https://ohyecloudy.com/lifelog/archives/game-it-takes-two-hazelight-2022/">It Takes Two (Hazelight, 2022)</a>’ 딸기부엉이와 즐겁게 했다. 딸기부엉이는 다음 게임으로 동물의 숲을 혼자 즐기고 있다. 스플릿 픽션을 사서 다시 같이해 볼 예정이다.</p>

<p>’엘든 링 (Elden Ring, From Software, Windows, 2022)’ 게임 개발자로서 벽을 느끼게 해준다. 85시간 플레이를 했다. 먼저 해야 하는 게임이 있어서 엔딩은 못 봤다. 2026에는 힘들 것 같고 2027년에 다시 돌아와 엔딩을 보고 싶다.</p>

<p>’Lies of P (NEOWIZ, 2023)’ 라운드8 스튜디오 작품이라 우선순위를 높여 플레이하고 있다.</p>

<h1 id="블로깅---총-142개의-블로그-포스트">블로깅 - 총 142개의 블로그 포스트</h1>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>pnotes</th>
      <th>exp cabinet</th>
      <th>emacsian</th>
      <th>ddiary</th>
      <th>project M</th>
      <th>total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2025</td>
      <td>15</td>
      <td>111</td>
      <td>6</td>
      <td>9</td>
      <td>1</td>
      <td>142</td>
    </tr>
    <tr>
      <td>2024</td>
      <td>18</td>
      <td>82</td>
      <td>11</td>
      <td>9</td>
      <td>4</td>
      <td>124</td>
    </tr>
    <tr>
      <td>2023</td>
      <td>15</td>
      <td>60</td>
      <td>16</td>
      <td>8</td>
      <td>6</td>
      <td>105</td>
    </tr>
    <tr>
      <td>2022</td>
      <td>12</td>
      <td>64</td>
      <td>15</td>
      <td>10</td>
      <td>8</td>
      <td>109</td>
    </tr>
    <tr>
      <td>2021</td>
      <td>8</td>
      <td>62</td>
      <td>3</td>
      <td>11</td>
      <td>13</td>
      <td>97</td>
    </tr>
    <tr>
      <td>2020</td>
      <td>20</td>
      <td>98</td>
      <td>8</td>
      <td>18</td>
      <td> </td>
      <td>144</td>
    </tr>
    <tr>
      <td>2019</td>
      <td>8</td>
      <td>47</td>
      <td>10</td>
      <td>27</td>
      <td> </td>
      <td>92</td>
    </tr>
    <tr>
      <td>2018</td>
      <td>18</td>
      <td>50</td>
      <td>22</td>
      <td>39</td>
      <td> </td>
      <td>129</td>
    </tr>
    <tr>
      <td>2017</td>
      <td>19</td>
      <td>112</td>
      <td>34</td>
      <td>35</td>
      <td> </td>
      <td>200</td>
    </tr>
    <tr>
      <td>2016</td>
      <td>79</td>
      <td>57</td>
      <td>9</td>
      <td>27</td>
      <td> </td>
      <td>172</td>
    </tr>
    <tr>
      <td>2015</td>
      <td>40</td>
      <td>51</td>
      <td>6</td>
      <td>0</td>
      <td> </td>
      <td>97</td>
    </tr>
    <tr>
      <td>2014</td>
      <td>20</td>
      <td>21</td>
      <td>8</td>
      <td>4</td>
      <td> </td>
      <td>53</td>
    </tr>
    <tr>
      <td>2013</td>
      <td>32</td>
      <td>43</td>
      <td>7</td>
      <td>18</td>
      <td> </td>
      <td>100</td>
    </tr>
    <tr>
      <td>2012</td>
      <td>33</td>
      <td>77</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>110</td>
    </tr>
    <tr>
      <td>2011</td>
      <td>24</td>
      <td>59</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>83</td>
    </tr>
    <tr>
      <td>2010</td>
      <td>41</td>
      <td>66</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>107</td>
    </tr>
    <tr>
      <td>2009</td>
      <td>71</td>
      <td>80</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>151</td>
    </tr>
    <tr>
      <td>2008</td>
      <td>21</td>
      <td>47</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>68</td>
    </tr>
    <tr>
      <td>2007</td>
      <td>7</td>
      <td>21</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>28</td>
    </tr>
    <tr>
      <td>2006</td>
      <td> </td>
      <td>11</td>
      <td> </td>
      <td> </td>
      <td> </td>
      <td>11</td>
    </tr>
    <tr>
      <td>total</td>
      <td>501</td>
      <td>1219</td>
      <td>155</td>
      <td>215</td>
      <td>32</td>
      <td>2122</td>
    </tr>
  </tbody>
</table>

<p>계속 잘 쓰고 있다. 블로깅은 내 취미 생활이다. 올해 142개를 포스팅했다. 이제까지 쓴 포스팅 개수는 2,122개이다.</p>

<p>2025년에 쓴 포스팅을 몇 개 꼽아보자면 아래와 같다.</p>

<ul>
  <li>pnotes
    <ul>
      <li><a href="https://ohyecloudy.com/pnotes/archives/work-20th-anniversary/">게임 개발 20주년, 비트코인 빼고 과거의 나에게 던지고 싶은 조언들</a>
        <ul>
          <li>벌써 20년이라니 시간 참 빠르다. 글을 쓰면서 나 자신을 돌아볼 수 있어서 좋았다.</li>
        </ul>
      </li>
      <li><a href="https://ohyecloudy.com/pnotes/archives/application-layering/">Application Layering 아키텍처 - ports and adpaters로 layer를 만든 아키텍처</a>
        <ul>
          <li>사이드 프로젝트에 적용하고 있는 아키텍처다. 글을 적으며 모호한 것들을 정리할 수 있었다. 맞다. 글은 생각하는 행위다.</li>
        </ul>
      </li>
      <li><a href="https://ohyecloudy.com/pnotes/archives/the-power-of-composition-scott-wlaschin-ndc-oslo-2018/">The Power of Composition (Scott Wlaschin) 보고 느낀 점 - 합성하려다보니 Currying과 Monad</a>
        <ul>
          <li>Currying과 Monad 모두 합성에 관한 얘기다. 훌륭한 발표다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>exp cabinet
    <ul>
      <li><a href="https://ohyecloudy.com/lifelog/archives/my-one-year-boxing-experience/">복싱 1년 후기 - 하기 전과 하고 난 뒤가 달라진 고마운 운동</a>
        <ul>
          <li>복싱은 매력 넘치는 스포츠다</li>
        </ul>
      </li>
      <li><a href="https://ohyecloudy.com/lifelog/archives/baseball-2025/">삼성 라이온즈 팬이 되다 - 2025년</a>
        <ul>
          <li>가족 이벤트가 됐다. 야구를 안 하는 겨울이 심심해졌다.</li>
        </ul>
      </li>
      <li><a href="https://ohyecloudy.com/lifelog/archives/book-delicious-in-dungeon/">던전밥 (쿠이 료코, 2016-2024) 독후감</a>
        <ul>
          <li>설정으로 넘어가던 던전에서의 끼니를 재미있게 풀어냈다. 유머 코드가 완전 내 스타일이다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>(emacsian ohyecloudy)
    <ul>
      <li><a href="https://ohyecloudy.com/emacsian/2025/06/21/evil-snipe-hangul-f-t-motion/">f/t 모션으로 한글도 검색하는 evil-snipe 설정</a>
        <ul>
          <li>emacs로 vim 키바인딩을 더 편하게 쓴다</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>dev diary, TIL
    <ul>
      <li><a href="https://ohyecloudy.com/ddiary/2025/03/22/systemd-journald/">#TIL systemd로 프로세스를 관리하고 systemd-journald로 로그를 본다</a>
        <ul>
          <li>elixir로 만든 앱을 iex로 띄우곤 했는데, 사양이 낮은 VM에 셋방살이하다 보니 최적화에 신경 쓰게 된다.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h1 id="도구">도구</h1>

<h2 id="매일-가지고-다니는-도구">매일 가지고 다니는 도구</h2>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- iPhone 12 Pro (2020)
</span><span class="gi">+ iPhone 14 (2022)
</span><span class="gd">- Aer Day Pack 2 (2020)
</span><span class="gi">+ On speed pack 18L lite (2025)
</span><span class="p">Apple Watch 7 (2021)
</span><span class="err">벨킨</span> 애플워치 무선 충전 보조배터리 F8J233bt (2020)
<span class="err">맥세이프(MagSafe)</span> 카드 지갑
<span class="p">AirPods 4 액티브 노이즈 캔슬링 (2024)
ANKER 321 맥고 무선충전 보조배터리 A1616 5000mAh (2023)
</span><span class="gd">- UA 컨테인 듀오 스몰 백팩 더플 (언더아머, 2024)
</span></code></pre></div></div>

<ul>
  <li>iPhone 14 (2022)
    <ul>
      <li>최신을 따라가지 못하는 내 여유</li>
    </ul>
  </li>
  <li>On speed pack 18L lite (2025)
    <ul>
      <li>러닝 출퇴근에 최적화한 가방이다</li>
      <li><a href="https://ohyecloudy.com/lifelog/archives/aer-day-pack-2-2020/">Aer Day Pack 2 (2020)</a> 대신 들고 다니고 있다</li>
    </ul>
  </li>
</ul>

<h2 id="개인-개발-도구">개인 개발 도구</h2>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">Mac mini (M2, 2023)
MacBook Air (M1, 2020)
Happy Hacking Professional 2 (2006)
Happy Hacking Professional 2 Type-S (2011)
Magic Trackpad 2 (2015)
</span></code></pre></div></div>

<p>달라진 게 없다. 요즘 맥북으로 Unreal Engine을 돌려보고 싶어서 MacBook Pro를 기웃거리고 있다.</p>

<h2 id="구독-서비스">구독 서비스</h2>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- Netflix Premium
</span><span class="gi">+ Netflix 광고형 스탠다드
+ 네이버 플러스 멤버십
</span><span class="p">1Password Personal
Dropbox Plus
</span><span class="gd">- Inoreader Supporter
</span><span class="gi">+ Inoreader Pro
</span><span class="p">Apple Music 가족 구독
</span><span class="gd">- ChatGPT Plus
</span><span class="p">Google One 2TB
Duolingo 프리미엄 패밀리
iCloud+ 50GB
</span><span class="gd">- GitHub Copilot
</span><span class="gi">+ Claude Code Pro
+ Ridi Select
</span></code></pre></div></div>

<ul>
  <li>네이버 플러스 멤버십
    <ul>
      <li>Netflix도 이걸로 보고 있다</li>
    </ul>
  </li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/app-inoreader/">Inoreader</a> Pro
    <ul>
      <li>AI 요약 때문에 40% 할인할 때, 결제했다</li>
      <li>AI 요약은 RSS 서비스의 필수인 듯</li>
    </ul>
  </li>
  <li>ChatGPT Plus 해지
    <ul>
      <li>AI 서비스를 여러 개 구독하고 싶지는 않으니 여러 개를 돌려가며 써 볼 생각이다</li>
      <li>AI 서비스를 하나만 쓰는 건 위험하다. 특히 1년 결재하는 건 위험하다</li>
    </ul>
  </li>
  <li>Claude Code Pro
    <ul>
      <li>사이드 프로젝트에 쓰려고 결제했다</li>
      <li>아직 Pro로 충분한 걸 보니 사이드 프로젝트에 시간을 많이 쓰지 않는 것 같아 반성한다</li>
    </ul>
  </li>
  <li>Ridi Select
    <ul>
      <li>통신사 서비스로 사용하고 있다. 교양서적은 이걸로 읽고 있다</li>
      <li><a href="id:fc20ee87-25cb-48de-a1b2-76ddda73bace">리디북스 페이퍼 라이트 (리디, 2015)</a> 수명이 연장됐다</li>
    </ul>
  </li>
</ul>

<h1 id="계속-해야-할-것">계속 해야 할 것</h1>

<p>사이드 프로젝트. 매일 사이드 프로젝트를 짧은 시간이라도 한다. 소비자가 아니라 생산자의 마인드로 살아가고 싶다. 2026년에는 60% 달성을 목표로 하고 있다.</p>

<p>블로깅. 글쓰기는 곧 생각하기. 누가 취미를 물어보면 블로깅이라고 답한다. 블로깅이 아직 재미있다.</p>

<p>자전거 출퇴근, 러닝 출퇴근. 번갈아 가며 한다. 러닝 출퇴근만 하고 싶지만 연속으로 달려보니 회복이 잘 안 된다. 욕심내지 않고 꾸준히 할 수 있는 템포를 찾아가고 있다.</p>

<p>근력 운동. 어떤 운동을 하든지 근력 운동을 베이스로 깔고 가야 한다. 좀 더 일찍 했으면 좋았을 걸.</p>

<p>주간 회고, 분기 회고, 연간 회고. 회고는 짜내는 게 아니라 많은 양을 줄이는데 골머리를 앓아야 한다. 회고를 계속한다. 지금보다는 늘리지 말자.</p>

<p>Duolingo. 영어 공부 의무감의 최소 방어선이다. 좀 더 늘릴 수 있을까? 이거라도 계속하자.</p>

<p>요리. 주말에는 가족 식사를 한 끼 책임지고 싶다. 늙어서도 생존에 필요한 스킬이다. 간단하면서도 생색낼 수 있는 요리 위주로 선정한다. 솥 밥을 파보고 싶다.</p>

<p>Technology Radar. <a href="https://www.thoughtworks.com/radar">Technology Radar</a>를 참고삼아 개인 테크 레이더도 적고 회사에서도 적고 있다. 트렌드에 관심을 기울이고 따라가지 않았을 때 리스크 관리에 도움이 되는 것 같다.</p>

<p>분산 투자. 종목을 공부하고 투자해서 성공하는 건 불가능하다. 분산 투자로 억울하지 않을 만큼의 수익과 리스크 관리를 하고 있다. 열심히 일해서 투자 금액을 늘리는 데 집중하자.</p>

<p>오찬 모임. 저녁 약속을 잡기 힘들다고 보고 싶은 사람과의 만남을 놓치지 말자. 명절이라도 아침 약속은 가능하더라. 시간이 되면 술 한잔 하는 게 좋겠지만 안 된다면 포기하지 말고 오찬 모임이라도 가지자.</p>

<h1 id="멀리할-것">멀리할 것</h1>

<p>약속을 지키지 않는 사람을 멀리할 것. 나도 다른 사람의 시간을 존중하려고 노력해야 한다.</p>

<p>기대를 너무 많이 가지면 실망도 더 커질 수밖에 없다. 시니컬해지지 않으면서 기대를 낮추고 관대해질 필요가 있다. 내 자신에게나 엄격하자.</p>

<p>너무 자세한 설명을 피한다. 간결하게 설명하는 것으로 충분할 때가 많다. 더 설명이 필요하면 그때 자세히 설명해도 된다. 특히 인터넷 세상에는 답변을 바라고 남긴다기보다는 글을 남기는 행위가 완결된 행동일 때도 많다.</p>

<p>패배자 대화를 피해야 한다. 나도 그런 말을 하지 말아야 하고 그런 대화를 하는 사람을 멀리해야 한다. 성공을 깎아내리는 건 자신도 모르게 전염된다. 신경 써서 멀리해야 함</p>

<h1 id="시도할-것">시도할 것</h1>

<p>수영. 근련 운동을 베이스로 하고 수영을 다시 시작해보자. 마스터해서 간지나게 수영해 보고 싶다.</p>

<p>이른 기상. 다시 게을러졌다. 일찍 일어나면 방해받지 않는 시간을 확보할 수 있다.</p>

<p>멘토링. AI는 시니어가 누군가를 멘토링하며 성장하는 기회도 뺏어갔다. 멘토링할 기회를 만들어서 해보고 싶다.</p>

<p>무작정 해보는 행동력. AI 때문에 행동이 값싸지고 있다. 행동의 트리거를 당기는 건 아직 휴먼에게 있다. 더 자주 더 적극적으로 트리거를 당겨야 한다.</p>

<h1 id="링크">링크</h1>

<ul>
  <li><a href="https://ohyecloudy.com/ddiary/2025/03/22/systemd-journald/">#TIL systemd로 프로세스를 관리하고 systemd-journald로 로그를 본다 - dev diary, TIL - ohyeclou…</a></li>
  <li><a href="https://ohyecloudy.com/emacsian/2025/06/21/evil-snipe-hangul-f-t-motion/">f/t 모션으로 한글도 검색하는 evil-snipe 설정 - (emacsian ohyecloudy) - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/aer-day-pack-2-2020/">에이어 데이팩 2(Aer Day Pack 2) 백팩 사용 후기 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/app-inoreader/">Inoreader RSS 리더 사용기 2019-2024 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/baseball-2025/">삼성 라이온즈 팬이 되다 - 2025년 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/book-delicious-in-dungeon/">던전밥 (쿠이 료코, 2016-2024) 독후감 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/book-the-mind-is-flat-2019/">생각한다는 착각 (닉 채터, 2021) 중단 독후감 - 재미있는 주장 지루한 전개 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/cool-things-office-lens/">업무용 안경으로 구매한 자이스 오피스 렌즈 사용 후기 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/family-trip-uljin-dukgu-hot-spring/">경상북도 울진 덕구온천 가족여행 후기 (feat. 국립해양과학관, 독도횟집) - 2025년 2월 - exp cabinet - ohyeclo…</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/family-wheely-day-2025/">가족 라이딩 후기 - 2025년 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/game-it-takes-two-hazelight-2022/">It Takes Two (Hazelight, Nintendo Switch, 2022) 플레이 후기 - exp cabinet - ohyecl…</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/givenrace-marathon-2020/">기브앤레이스 가상 마라톤 대회 참가 후기 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/marathon-2025-go-free-run/">Go Free Run 2025 마라톤 10km 후기 - 서강대교가 포함된 코스가 좋다 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/marathon-run-seoul-run-2025/">제19회 일간스포츠 서울마라톤 10km 후기 - 첫 10km 달리기 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/my-one-year-boxing-experience/">복싱 1년 후기 - 하기 전과 하고 난 뒤가 달라진 고마운 운동 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/place-phoenix-park-2025-01/">강원도 휘닉스 파크 가족여행 후기 (feat. 다키닥팜, 블루캐니언, 스노우 빌리지) - 2025년 1월 - exp cabinet - oh…</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-forest-resom-2025-06/">포레스트 리솜 가족여행 후기 2025-06 - 힐링은 산속에서 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-gangchon-grape-pension-auto-camping-site/">강원도 강촌 포도펜션 오토캠핑장 2025년 6월 - 두 번째 가족 캠핑 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/lifelog/archives/trip-mongsanpo-beach-may-2025/">몽산포 해수욕장 2025년 5월 가족여행 후기 - exp cabinet - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/402/">#retrospective 2009년 돌아보기 - 내가 읽은 프로그래밍 관련 서적 - ohyecloudy’s pnotes - ohyeclo…</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/application-layering/">Application Layering 아키텍처 - ports and adpaters로 layer를 만든 아키텍처 - ohyecloudy’s…</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/book-the-programmer-brain/">프로그래머의 뇌 (펠리너 헤르만스, 2022) 독후감 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/the-power-of-composition-scott-wlaschin-ndc-oslo-2018/">The Power of Composition (Scott Wlaschin) 보고 느낀 점 - 합성하려다보니 Currying과 Monad -…</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/work-20th-anniversary/">게임 개발 20주년, 비트코인 빼고 과거의 나에게 던지고 싶은 조언들 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://www.thoughtworks.com/radar">Technology Radar - Guide to technology landscape - Thoughtworks - thoughtwork…</a></li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="retrospective" /><summary type="html"><![CDATA[게임 프로그래머 20주년이 됐고 2009년부터 시작한 연간회고는 올해로 17번째가 됐다.]]></summary></entry><entry><title type="html">오라클 무료 VM을 알차게 사용하려면? - 트위터(X) 봇을 Heroku에서 OCI로 이사</title><link href="https://ohyecloudy.com/pnotes/archives/free-hosting-optimization/" rel="alternate" type="text/html" title="오라클 무료 VM을 알차게 사용하려면? - 트위터(X) 봇을 Heroku에서 OCI로 이사" /><published>2025-10-26T00:00:00+09:00</published><updated>2025-10-26T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/free-hosting-optimization</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/free-hosting-optimization/"><![CDATA[<p>잘 쓰고 있던 <a href="https://ohyecloudy.com/pnotes/archives/deploying-elixir-project-to-heroku/">Heroku</a> 무료 dyno가 중지됐다. 그동안 무료로 잘 쓴 의리로 Heroku를 좀 쓰다가 클라우드 무료 VM으로 옮겼다. 주기적으로 트윗하는 게 전부인 <a href="https://ohyecloudy.com/pnotes/archives/tbot-800-ex/">트위터 인용봇</a>은 클라우드 무료 인스턴스에서 돌려도 충분하다.</p>

<h1 id="오라클-클라우드-무료-vm">오라클 클라우드 무료 VM</h1>

<p><a href="https://www.oracle.com/kr/cloud/">오라클 클라우드</a>를 선택했다. 처음엔 <a href="https://cloud.google.com/">구글 클라우드</a>를 사용할 계획이었다. 오라클 클라우드 무료 티어가 더 사양이 좋다는 얘기를 듣고 찾아보니 훨씬 더 빠방하게 제공한다. 역시 경쟁은 아름답다. 후발주자 화이팅이다.</p>

<blockquote>
  <ol>
    <li>Arm 기반 Ampere A1 코어 및 24GB 메모리(매월 OCPU 3,000시간 및 18,000GB-시간을 제공하며 VM 1개 또는 4개로 사용 가능)</li>
    <li>AMD 기반 컴퓨트 VM 2개(각각 OCPU 1/8개 및 1GB 메모리) 사용</li>
  </ol>
</blockquote>

<p>트위터 인용봇은 2번으로 충분하다. 내가 끌렸던 건 1번이다. 오라클 클라우드에서는 무료로 ARM 아키텍처 VM을 제공한다. ARM 아키텍처에서 Elixir 프로젝트가 잘 돌아가는지 확인도 하고 싶었다. 잘 <a href="https://ohyecloudy.com/pnotes/archives/github-actions-arm64-build-deploy/">돌아가는 걸 확인</a>하고 원래 계획했던 저사양의 AMD 기반 VM으로 옮겼다. 초반에 ARM 아키텍처 VM 생성이 안 돼서 몇 번을 시도했는지 모른다. 결제 카드를 등록하면 잘 생성된다는 팁을 보고 등록하고 생성했더니 한 번에 성공했다. 카드 등록 여부로 생성할 수 있는 풀을 다르게 선택하는 것 같았다.</p>

<h1 id="ssh-접속-편의를-위한-alias-설정">SSH 접속 편의를 위한 alias 설정</h1>

<p>SSH 키는 VM을 만들 때 다운로드했다. 매번 접속 IP 주소와 개인키 경로를 인자로 넘길 필요가 없게 별칭을 만든다.</p>

<p><code class="highlighter-rouge">~/.ssh/config</code> 파일을 수정하면 된다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host app
  HostName 123.123.123.123
  User ubuntu
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile ~/.ssh/private-key.key
  IdentitiesOnly yes
  LogLevel FATAL
</code></pre></div></div>

<p>별칭을 만들어놔서 <code class="highlighter-rouge">ssh app</code> 을 입력하면 접속한다.</p>

<h1 id="swap-space-할당">Swap space 할당</h1>

<p>무료 인스턴스 메모리가 1GB로 작아서 필수적으로 세팅해야 한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo fallocate -l 2G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
</code></pre></div></div>

<p>실제 메모리를 다 사용하면 사용할 <a href="https://ohyecloudy.com/ddiary/2025/08/09/til-linux-swap-space/">Swap space</a>를 세팅한다.</p>

<h1 id="systemd로-프로세스-관리하기">Systemd로 프로세스 관리하기</h1>

<p>크래시로 프로그램이 종료되면 다른 조치하지 않는 한 다시 실행되게 한다. 아예 실행조차 안 되는 상황이 아니고 특정 조건에만 크래시가 난다면 <a href="https://ohyecloudy.com/pnotes/archives/sentry-error-tracking-software/">Sentry</a>로 보고된 정보를 보고 고칠 때까지 시간을 벌 수 있다.</p>

<p>service unit 정의를 <code class="highlighter-rouge">/etc/systemd/system/myapp.service</code> 파일에 한 후</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
</code></pre></div></div>

<p>파일을 리로드하고 자동실행을 활성화하고 바로 시작한다.</p>

<h1 id="systemd-journald로-로그-파일-설정">Systemd-journald로 로그 파일 설정</h1>

<p>Systemd로 실행한 프로세스 로그를 <code class="highlighter-rouge">journalctl</code> 프로그램으로 볼 수 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>journalctl -u -f myapp
</code></pre></div></div>

<p>좁은 집 셋방살이에 맞게 <code class="highlighter-rouge">/etc/systemd/journald.conf</code> 파일을 수정해 남길 로그 양을 설정한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Journal]
Storage=persistent
SystemMaxUse=500M
SystemKeepFree=1G
MaxRetentionSec=7day

sudo systemctl daemon-reload
sudo systemctl restart systemd-journald
</code></pre></div></div>

<p>로그를 저장하다가 디스크 공간이 부족하지 않게 적당히 설정한다.</p>

<h1 id="배포-전용-user-설정">배포 전용 user 설정</h1>

<p><a href="https://docs.github.com/ko/actions">GitHub Actions</a>을 사용해서 배포할 계획이다. 모든 권한이 있는 <code class="highlighter-rouge">root</code> 유저를 배포용으로 사용하지 말고 배포할 디렉터리에만 권한이 있는 유저를 만든다.</p>

<p><code class="highlighter-rouge">deployuser</code> 계정을 만든다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo useradd -m -s /bin/bash deployuser
</code></pre></div></div>

<p><code class="highlighter-rouge">appgroup</code> 그룹을 만들고 배포 계정뿐만 아니라 주로 사용하는 계정도 포함시킨다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo groupadd appgroup

sudo usermod -aG appgroup ubuntu
sudo usermod -aG appgroup deployuser
</code></pre></div></div>

<p>외부에서 빌드한 파일을 복사할 디렉터리를 만들고 권한을 설정한다. 아래 예제에서는 <code class="highlighter-rouge">/app</code> 디렉터리를 사용했다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo mkdir /app
sudo chown deployuser:appgroup /app

sudo chmod g+s /app
</code></pre></div></div>

<p><code class="highlighter-rouge">/app</code> 디렉터리에서 새로 생성되는 파일이나 하위 디렉터리가 그룹 소유권을 그대로 상속하게 한다.</p>

<p>VM 설정은 끝났다. 이제 VM으로 편하게 배포하는 방법을 궁리해야 한다.</p>

<h1 id="github-actions가-사용할-ssh-key-생성">GitHub actions가 사용할 ssh key 생성</h1>

<p>GitHub actions에서 VM으로 파일을 복사해야 한다. 즉 ssh key를 생성해서 public key를 VM에 인증된 키로 등록하고 private key는 GitHub actions가 소유해야 한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen -t ed25519 -C "github-actions-deploy"
</code></pre></div></div>

<p><code class="highlighter-rouge">deployuser</code> 권한을 사용하게 인증된 키로 등록한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo su deployuser
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

cat ~/.ssh/id_ed25519.pub &gt;&gt; ~/.ssh/authorized_keys
</code></pre></div></div>

<p>public key를 인증된 키에 추가하고 비밀키를 <a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub이 제공하는 저장소 환경 secrets</a>에 저장하고 쓴다.</p>

<h1 id="scp-action-github-actions으로-배포">scp-action GitHub actions으로 배포</h1>

<p>Systemd로 프로세스를 관리하기 때문에 scp로 파일을 복사하기 전에 종료하고 복사 후에 다시 시작한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Stop service</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">appleboy/ssh-action@v1.2.2</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">APP_NAME</span><span class="pi">:</span> <span class="s">${{ secrets.APP_NAME }}</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">host</span><span class="pi">:</span> <span class="s">${{ secrets.HOST }}</span>
      <span class="na">username</span><span class="pi">:</span> <span class="s">${{ secrets.USER }}</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">${{ secrets.KEY }}</span>
      <span class="na">envs</span><span class="pi">:</span> <span class="s">APP_NAME</span>
      <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">sudo systemctl stop $APP_NAME</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy via scp</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">appleboy/scp-action@v1.0.0</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">${{ secrets.USER }}</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">${{ secrets.HOST }}</span>
    <span class="na">key</span><span class="pi">:</span> <span class="s">${{ secrets.KEY }}</span>
    <span class="na">source</span><span class="pi">:</span> <span class="s">_build/prod/rel/tbot800/*</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s">${{ secrets.APP_DIR }}</span>
    <span class="na">strip_components</span><span class="pi">:</span> <span class="m">4</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Start service</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">appleboy/ssh-action@v1.2.2</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">APP_NAME</span><span class="pi">:</span> <span class="s">${{ secrets.APP_NAME }}</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">host</span><span class="pi">:</span> <span class="s">${{ secrets.HOST }}</span>
      <span class="na">username</span><span class="pi">:</span> <span class="s">${{ secrets.USER }}</span>
      <span class="na">key</span><span class="pi">:</span> <span class="s">${{ secrets.KEY }}</span>
      <span class="na">envs</span><span class="pi">:</span> <span class="s">APP_NAME</span>
      <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">sudo systemctl start $APP_NAME</span>
</code></pre></div></div>

<p><a href="https://ohyecloudy.com/pnotes/archives/tbot-800-ex/">Tbot-800.ex</a>에서 사용 중인 스크립트를 가져왔다. 빌드 결과물은 <code class="highlighter-rouge">_build/prod/rel/tbot800/*</code> 디렉터리에 저장된다.</p>

<p><a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub이 제공하는 저장소 환경 secrets</a>을 <code class="highlighter-rouge">secrets</code> 환경 변수로 접근한다. 위에서 배포용으로 만든 ssh key의 private 키를 <code class="highlighter-rouge">KEY</code> 키의 값으로 저장했다. <code class="highlighter-rouge">secrets.KEY</code> 코드로 접근한다.</p>

<p>전체 GitHub actions workflows는 <a href="https://github.com/ohyecloudy/tbot-800.ex/blob/main/.github/workflows/elixir.yaml">ohyecloudy/tbot-800.ex/blob/main/.github/workflows/elixir.yaml - github.com</a> 에서 볼 수 있다.</p>

<h1 id="마치며">마치며</h1>

<p>Heroku에서 돌리던 Tbot-800.ex를 오라클 무료 VM으로 옮겼다. 그동안 무료로 오래 써서 의리로 Heroku를 사용했다고 하지만 항상 우선순위에서 밀려 질질 끌었던 것 같다. 자동으로 재시작할 수 있게 System로 프로세스를 관리하고 배포 전용 user를 만들어서 GitHub actions로 배포하는 개발 파이프라인을 구성했다. DB를 사용하지 않는 프로젝트라서 DB를 따로 세팅하지는 않았다.</p>

<p>알림이 아쉬운데, 이건 Telegram 같은 메신저를 사용해 보면 어떨까 생각하고 있다. 지금은 가끔 Twitter(X)에 들어가 인용봇이 잘 동작하고 있는지 확인하고 있다.</p>

<h1 id="링크">링크</h1>

<ul>
  <li><a href="https://cloud.google.com/">Cloud Computing Services - Google Cloud - cloud.google.com</a></li>
  <li><a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub Actions에서 비밀 사용 - GitHub Docs - docs.github.com</a></li>
  <li><a href="https://docs.github.com/ko/actions">GitHub Actions 설명서 - GitHub Docs - docs.github.com</a></li>
  <li><a href="https://github.com/ohyecloudy/tbot-800.ex/blob/main/.github/workflows/elixir.yaml">ohyecloudy/tbot-800.ex/blob/main/.github/workflows/elixir.yaml - github.com</a></li>
  <li><a href="https://ohyecloudy.com/ddiary/2025/03/22/systemd-journald/">#TIL systemd로 프로세스를 관리하고 systemd-journald로 로그를 본다 - dev diary, TIL - ohyeclou…</a></li>
  <li><a href="https://ohyecloudy.com/ddiary/2025/08/09/til-linux-swap-space/">#TIL Linux Swap space(스왑 공간) 설정 - dev diary, TIL - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/deploying-elixir-project-to-heroku/">Elixir 프로젝트를 Heroku에 배포하기 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/github-actions-arm64-build-deploy/">GitHub Actions로 ARM64 플랫폼 빌드 및 배포 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/linux-deploy-user-github-actions/">GitHub Actions에서 사용할 배포용 Linux 유저 생성 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/sentry-error-tracking-software/">크래시 리포트, 에러 트래킹이 필요하면 sentry - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/tbot-800-ex/">10년 전에 Clojure로 짠 트위터 인용봇을 Elixir로 재작성한 후기 - ohyecloudy’s pnotes - ohyecloudy…</a></li>
  <li><a href="https://www.oracle.com/kr/cloud/">클라우드 인프라 - Oracle 대한민국 - oracle.com</a></li>
  <li><a href="https://damonvjanis.medium.com/optimizing-for-free-hosting-elixir-deployments-6bfc119a1f44">Optimizing for Free Hosting — Elixir Deployments - by Damon Janis - Medium - …</a></li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="uncategorized" /><category term="tbot800" /><category term="heroku" /><category term="oci" /><category term="github" /><category term="swap-space" /><category term="systemd" /><category term="linux" /><category term="ssh" /><category term="scp" /><category term="side-project" /><category term="bot" /><summary type="html"><![CDATA[잘 쓰고 있던 Heroku 무료 dyno가 중지됐다. 그동안 무료로 잘 쓴 의리로 Heroku를 좀 쓰다가 클라우드 무료 VM으로 옮겼다. 주기적으로 트윗하는 게 전부인 트위터 인용봇은 클라우드 무료 인스턴스에서 돌려도 충분하다.]]></summary></entry><entry><title type="html">게임 개발 20주년, 비트코인 빼고 과거의 나에게 던지고 싶은 조언들</title><link href="https://ohyecloudy.com/pnotes/archives/work-20th-anniversary/" rel="alternate" type="text/html" title="게임 개발 20주년, 비트코인 빼고 과거의 나에게 던지고 싶은 조언들" /><published>2025-10-04T00:00:00+09:00</published><updated>2025-10-04T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/work-20th-anniversary</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/work-20th-anniversary/"><![CDATA[<p>2005년에 넥슨 공채로 시작한 게임 프로그래머 경력이 벌써 20주년을 맞이했다. 학교에서 보낸 시간보다 게임 프로그래머로 돈을 번 시간이 더 길다. 20주년이 된 기념으로 소박한 회고를 남겨본다.</p>

<p>게임 프로그래머로 입사했을 때는 멤버 모두가 어렸다. ’자녀 대학교 등록금 지원’ 같은 회사 복지를 상상하기 어려웠다. 왜냐하면 자녀가 대학생인 직원이 없었기 때문이다. 이래서였을까? 나이를 먹으면 게임 프로그래머를 못하는 거 아니야? 이런 질문들이 간간이 나오곤 했다.</p>

<blockquote>
  <p>“프로그래머를 40세 넘어서까지 할 수 있어요?”</p>
</blockquote>

<p>우선 이 질문에 대한 답부터 해야겠다.</p>

<p>“네. 물론입니다.”</p>

<h1 id="요약-by-human">요약 by human</h1>

<ul>
  <li>과거의 나를 만나면 해주고 싶은 얘기</li>
  <li>20년 전 내가 지금 봤으면 놀랐을 것
    <ul>
      <li>“풀파티”</li>
      <li>“부지런하다”</li>
      <li>“엔지니어링 매니저”</li>
      <li>운동을 꾸준히 하고 있다</li>
    </ul>
  </li>
  <li>20년 전 내가 나를 봤을 때 실망할 것
    <ul>
      <li>구체적으로 바라는 게 없으니 크게 실망할 것도 없다</li>
    </ul>
  </li>
  <li>20년 전 나를 만나면 조언할 것들
    <ul>
      <li>스스로 계획을 세워라</li>
      <li>글을 써라 - 업무 일지, 중간보고, 블로깅, 회고</li>
      <li>한 걸음의 힘 - 냉소적이고 비관적인 태도를 벗어나는 마법의 주문</li>
      <li>운동은 훌륭한 GC(Garbage Collection) 메서드</li>
      <li>일을 100%로 완료하는 습관을 가져라</li>
      <li>Engineering manager(엔지니어링 매니저)의 기회가 있다면 잡아라</li>
      <li>경력에 걸맞는 시야를 가져야 하고 기여를 해야 한다</li>
      <li>순수 엔지니어가 아니라 게임 프로그래머</li>
      <li>미워하지 말고 기대치를 낮춰라</li>
      <li>변화를 일으키고자 하는 열정의 상한선</li>
      <li>라이브 프로젝트 경험을 쌓아라</li>
      <li>홈그라운드를 넓힐 수 있는 도구를 벼리자</li>
      <li>스터디에 참여하라</li>
      <li>과거 프로젝트 멤버와의 연결 고리</li>
    </ul>
  </li>
</ul>

<h1 id="남의-향한-조언이라기보다는-내-자신의-회고">남의 향한 조언이라기보다는 내 자신의 회고</h1>

<p>이 글이 뻔할 수도 있다. 특별한 경험으로 가득 찬 경력은 아니기 때문이다. 그리고 한 번쯤은 들어 본 말일 수도 있다. 형님 누님들이 좋은 얘기를 정제하고 멋진 비유를 곁들여 이미 다 말해 놓았다. 훌륭한 조언을 찾아보면 ChatGPT에 요약을 부탁해야 할 정도로 많은 양에 압도당할 것이다.</p>

<p>그래서 다른 사람이 아니라 내가 20년 전 나를 만나면 비트코인을 기억했다가 사라는 얘기 말고 어떤 얘기를 해줄 수 있을까? 이런 질문에서 20주년 회고를 시작했다.</p>

<h1 id="간단한-경력-요약">간단한 경력 요약</h1>

<p>2005년 넥슨 공채로 게임 프로그래머 경력을 시작했다. ’프로젝트 뫼비우스’, ’마비노기’, ’프로젝트 XR’, ’허스키 익스프레스’ 프로젝트에 참여했다. 엔씨소프트에서는 ’연구개발실 엔진팀’, ’리니지 3’, ’리니지 이터널’ 프로젝트에 참여했다. 넥슨게임즈에서는 ’프로젝트 DX’ 프로젝트에 참여했다. 엑스엘게임즈에서는 ’문명온라인’, ’프로젝트 X4’, ’달빛조각사’, ’아키에이지 크로니클’ 프로젝트에 참여했다.</p>

<p>대부분이 PC MMORPG 게임이다. 드랍 된 프로젝트도 많다. 게임을 보는 눈이 없었을까? 이렇게 생각이 들다가도 자신의 프로젝트 합류를 망설이던 내게 게임 보는 눈을 운운했던 PD의 프로젝트가 접힌 걸 떠올리면 운도 따른다고 생각한다. 확실하다고 생각했던 ’리니지 3’ 타이틀을 부여받은 프로젝트도 접혔으니 말이다.</p>

<p>그렇다고 모든 걸 운에만 맡겨야 할까? 아니다. 적어도 회사의 절실함과 PD를 포함한 구성원의 절실함은 있어야 나머지를 운에 맡길 수 있다.</p>

<h1 id="20년-전-내가-지금-봤으면-놀랐을-것">20년 전 내가 지금 봤으면 놀랐을 것</h1>

<h2 id="풀파티">“풀파티”</h2>

<p>몇 명을 낳아야지. 자녀 계획이 딱히 있진 않았다. 첫째 딸기부엉이를 낳고 신기하게도 모든 일이 잘 풀렸다. 더 큰 책임감 때문이었을까? 더 열심히 살았던 것 같다. 둘째 Taek은 날 살린 것 같다. 몸도 마음도 힘들었던 시기에 지속 가능성을 생각하게 됐다. 이대로 계속할 수 있을까? 내가 통제할 수 있었던 걸 조금씩 변경해 부담을 낮추는 계기가 되어 주었다.</p>

<p>하나만 있으면 내가 직접 참가자가 되어야 하지만 둘이니 매니징을 주로 하고 가끔 참가만 하면 된다. 역시 애들끼리 노는 게 최고다. 많이 싸우지만.</p>

<p>여행지에서 집으로 돌아오는 길. 내가 운전하는 차 안에서 모두 피곤해서 잠든 모습을 보면 이런 게 가장이라고 생각하게 된다.</p>

<h2 id="부지런하다">“부지런하다”</h2>

<p>이런 얘기를 듣는다면 많이 놀랄 것 같다. 스스로 너무 게으르다고 생각했기 때문이다. 내일의 나를 믿고 내일로 미루는 일도 비일비재했다. 대부분 내가 가장 게으른 그룹에 속해있었던 것 같다. 새벽 6시에 수영을 다닐 때도 수영반에서 내가 가장 게을렀다. 아슬아슬하거나 약간 늦게 도착해서 물에 입수하곤 했다. 6시에 맞추는 것도 힘든데 10분~20분씩 일찍 와서 몸을 푸는 사람들을 보며 정말 부지런하다 생각했다. 집에서는 부지런한 아내를 보며 항상 나는 정말 게으르다고 생각했다. 부지런해 보이는 건 루틴 때문인 것 같다. 아침 일찍 출근하고 매주 정기적으로 블로그 포스팅을 발행하는 루틴이 나를 부지런한 사람으로 보이게 한다. 실제로도 조금은 더 부지런해진 것 같다. 꾸준한 루틴이 조금씩 변화시켰다.</p>

<h2 id="엔지니어링-매니저">“엔지니어링 매니저”</h2>

<p>IC(Individual Contributor, 실무자)로 계속 남고 싶었다. 매니저가 되면 프로그래밍 실력이 정체되거나 퇴화할 것으로 생각했다. 해본 적이 없으니 주변에서 들은 말로 그렇게 상상했던 것 같다. 기득권 같은 실무자들이 매니저를 떠밀듯이 시키는 것도 본 것 같다.</p>

<p>프로그램팀장이 없던 조직이었다. PD의 권유로 프로그램팀장을 맡게 됐고 런칭까지 했다. 시야가 달라지는 걸 느꼈다. 디테일보다는 아키텍처에 더 신경 쓰게 된다. 구현 세부 사항보다는 인터페이스를 먼저 보게 된다.</p>

<p>프로그래밍 실력이 정체되진 않았다. IC로만 일했을 때보단 실력의 직선이 완만하지만 그래도 기울기는 양수다. 시야가 바뀐 긍정적인 영향이다. 매니징만 백퍼센트 할 수 있는 환경이 아니었다. 다행히 실무를 놓지 않아야 하고 매니징과 프로그래밍을 적절히 배분해야 한다고 생각하고 있어서 그런 환경이 불만족스럽진 않았다. 일손이 모자라 Bottleneck(병목) 지점을 맡는 경우도 많아서 항상 넘치게 일했었다. 엄청 힘들었는지 뇌가 나를 보호하려고 그 기간의 기억을 지운 것 같다. 그때가 잘 기억나지 않는다. 채용 전략이 너무 초보였다. 더 뽑아야 했다.</p>

<h2 id="운동을-꾸준히-하고-있다">운동을 꾸준히 하고 있다</h2>

<p>운동을 하긴 해야 하는데, 지금 하지 못하는 백만 가지 이유를 가지고 있던 나였다. 어떤 운동이든 하나를 꾸준히 하고 있는 게 신기하다. 운동에 재미를 붙였다기보다는 살기 위해 하는 것에 가깝지만 말이다. 그렇게 시작한 운동 습관 스노우볼링이 되니 뭔가를 하지 않으면 이상하다. 습관이란 게 이런 거구나.</p>

<p>수영, <a href="https://ohyecloudy.com/lifelog/archives/my-one-year-boxing-experience/">복싱</a>을 했고 지금은 달리기를 하고 있다.</p>

<h1 id="20년-전-내가-나를-봤을-때-실망할-것">20년 전 내가 나를 봤을 때 실망할 것</h1>

<p>크게 실망할 게 없을 것 같다. 구체적으로 바라는 게 없었기 때문이다. 기대가 있어야 실망할 것도 생긴다. 미래에 대한 내 모습을 그리지 않았다. 땅을 보고 살았던 것 같다. 한 번씩은 고개를 들어 풍경도 바라봐야 했었다.</p>

<p>막연히 게임 하나 대박쳐서 은퇴하는 모습을 상상했던 것 같다. 만약 은퇴했으면 무엇을 하고 있을까? 경제적인 걱정 없이 프로그래밍하고 있지 않을까? 아직까진 이게 제일 재미있다.</p>

<p>내 이름이 저자로 적힌 책이 있는 걸 상상했었다. 블로깅을 시작한 것도 이런 욕구에서부터 시작했던 것 같다. 결과물을 정리해 개발자 컨퍼런스에 발표를 몇 번 하긴 했지만 책을 쓸 볼륨은 만들지 못했다. 아직 책을 쓰고 싶다는 욕구는 있다.</p>

<p>실망을 제대로 못 해서 아쉽다. 기대해야 실망할 수 있다.</p>

<h1 id="20년-전-나를-만나면-조언할-것들">20년 전 나를 만나면 조언할 것들</h1>

<p>우선 비트코인을 사라는 조언을 하고 아래 조언을 할 것 같다.</p>

<h2 id="스스로-계획을-세워라">스스로 계획을 세워라</h2>

<blockquote>
  <p>If you don’t design your own life plan, chances are you’ll fall into someone else’s plan. And guess what they have planned for you? Not much.</p>

  <p>당신이 스스로 인생을 설계하지 않으면, 다른 사람의 설계에 따라 살게 될 가능성이 큽니다. 그런데 그들이 당신을 위해 준비한 건요? 별로 없습니다. - 짐 론(Jim Rohn)</p>
</blockquote>

<p>등대 역할을 할 열린 계획을 세울 수 있어야 한다. 통제할 수 없는 요인들 때문에 변하지 않는 닫힌 계획을 세우는 건 불가능하다. 그렇다고 핑계를 대면서 계획을 세우는 걸 미루기만 할 수는 없다.</p>

<p>이제까지 게임 개발을 하면서 마일스톤 시작 전에 게임 기획서가 다 준비된 적은 한 번도 없다. 게임 디자인팀(기획팀)을 욕하기엔 좋은 상황이다. 그런데 정말 이 핑계를 대면서 계획을 세우지 말아야 할까? 정말 모든 게 다 준비된 상태에서 시작하는 이상적인 상황이 오기나 하는 걸까?</p>

<p>외부 요인이 하나도 없다면 다음 마일스톤에는 어떤 일들을 해야 할까? 여기서부터 시작해야 한다. ’내부 주도 업무’로만 다음 마일스톤 목표를 세워두는 것이다. 50% 정도로만 채워서 진행하고 변경이 가능한 열린 계획임을 명심해야 한다. 뒤에 추가되는 ’외부 요청 업무’를 우선순위에 맞게 처리하며 균형을 잡으면 된다. 하나의 팀에서 시작한 이런 계획은 전체에 영향을 준다.</p>

<p>이런 계획이 주는 장점은 뭘까? 등대 역할을 할 수 있다. 단거리를 뛰는 게 아니라 마라톤을 뛰는 거라고 생각해야 한다. 디테일에만 매몰되면 해야 할 일을 허겁지겁 해치우기에 바빠서 정작 중요한 일에 힘을 제대로 쓰지 못하거나 임시방편으로 떼어놓고 뒤처리에 괴로워하게 된다.</p>

<p>매니저가 되면 계획을 세워봐야지? 일간 계획부터 시작해 주간 계획으로 넓히면서 조금씩 범위를 넓혀보는 게 좋다. IC(Individual Contributor)일 때는 주간 계획까지가 적당했다. 일간 계획을 세워보니 하루 만에 끝나는 일의 비율이 낮아서 컨텍스트를 유지하기에 주간 계획이 잘 맞았다. 실무자일 때 석 달 정도로 묶는 마일스톤 계획까지는 못 세워봤다. 이렇게 하기엔 변수가 너무 많아서 힘들 것 같다.</p>

<p>업무 계획을 세우다 보니 자연스럽게 이런 생각이 들었다. 더 중요한 내 인생에 대한 계획은 세우고 있는가? 그때부터 일상에서도 계획을 세우기 시작했다. 조금이라도 더 일찍 시작했더라면 더 나은 사람이 됐을텐데. 이런 생각을 한다.</p>

<h2 id="글을-써라---업무-일지-중간보고-블로깅-회고">글을 써라 - 업무 일지, 중간보고, 블로깅, 회고</h2>

<p>글을 쓰는 건 생각하는 행위다. 예전에는 쓸 게 왜 그리도 없었는지 모른다. 지나고 보니 글솜씨에 문제가 있었던 게 아니라 생각이 부족한 탓이었다. 유려한 문장은 글솜씨에서 나오겠지만 알아듣게 쓰는 건 생각에 의존한다.</p>

<p>업무 일지부터 시작해 보는 게 좋다. <a href="https://ohyecloudy.com/pnotes/archives/great-habit-interim-report/">중간보고</a>로 자연스럽게 이어진다. 무언가를 쓴다는 것은 정신 모델을 덤프하는 행위이기도 하다. slack 같은 인터럽트 툴들이 협업 도구가 된 시대에 인터럽트에 맞설 무기를 얻게 된다.</p>

<p>업무 일지를 쓰다 보면 상반기와 하반기에 있는 자기 평가에서 이전과는 다른 고민을 하게 된다. 쓸 게 없어서 찾아다니는 게 아니라 너무 많아서 중요한 업적을 추려내야 한다. 주간 업무 일지를 모으고 거기서 중요한 걸 추려내는 작업이다. 최근에 자기 평가에 쓸 내용 정리를 ai에게 도움받는 팁을 들은 적이 있다. ai를 사용해 자신이 작업한 jira 이슈나 코드를 바탕으로 한 일들을 뽑아낸다. 좋은 접근일 수도 있지만 나는 필요성을 전혀 느끼지 못했다. 내가 ai를 사용한다면 뽑아내는 게 아니라 이미 써 놓은 주간 업무 일지를 바탕으로 요약하는 용도로 사용할 것 같다.</p>

<p>블로깅을 하자. 글을 잘 쓰는 사람들이 부러워서 블로깅을 시작했다. 처음부터 책 같은 걸 쓸 능력이 없다는 자기 객관화가 된 덕분이다. 짧은 글들을 쓰다 보면 책을 한 권 쓸 수 있는 능력이 생기지 않을까? 이런 막연한 목표로 시작했던 것 같다. 기대하지 않은 걸 배운 것 같다. ’왜?’라는 질문을 뇌에 하드코딩한 것이다. 뇌의 입장에서 ’왜?’라고 묻는 건 에너지를 많이 써서 괴로운 행위다. 뭔가 깨달음을 얻어 한 번에 바꾸기 힘들다는 뜻이기도 하다. 꾸준히 글을 쓰다 보면 뇌가 포기하는 것 같다. ’왜?’라고 묻는 건 에너지를 많이 써서 괴롭지만 이번 생에는 어쩔 수 없군. 항상 ’왜?’라는 질문을 하게 된다.</p>

<h2 id="한-걸음의-힘---냉소적이고-비관적인-태도를-벗어나는-마법의-주문">한 걸음의 힘 - 냉소적이고 비관적인 태도를 벗어나는 마법의 주문</h2>

<p>냉소적이고 비관적인 말과 태도를 가진 사람이 왜 항상 보이는 걸까? 비율의 문제이지 항상 있는 것 같다. 이런 태도를 지니는 사람은 어떤 걸 얻는 걸까? 감정을 발산하는 행동이니 스트레스 관리에 도움이 될 것 같다. 스스로를 더 똑똑한 사람이라고 착각하게 만들어서 자기만족에 도움을 주기도 한다.</p>

<p>이런 사람과는 최선을 다해 거리를 둬야 한다. 술자리에서 같은 테이블에 앉으면 온갖 숨겨진 에피소드를 듣는 재미에 잠깐 빠질 수도 있지만 그런 재미는 금방 수명을 다한다. 냉소적이고 비관적인 말을 듣는 족족 흘려보낼 수 있을까? 결국 쌓이게 된다. 말하는 사람은 스트레스 수치가 낮아지겠지만 주변 사람들은 스트레스 수치가 점점 높아진다.</p>

<p>다른 사람은 그렇다고 치자. 내가 이런 태도를 가지지 않으려면 어떻게 해야 할까? ’한 걸음의 힘’을 믿어야 한다. 냉소적이고 비관적인 태도는 변화를 불러오기 힘들겠다는 믿음에서 나온다. 긍정적인 변화를 불러 올 수 있는 계단을 하나씩 발견해서 올라가다 보면 비관적인 태도를 가질 여유조차 없어진다. 팀 간의 의견 교환이 활발해지길 원하면 정기적인 티타임을 만들자. 게임 최적화에 아무도 신경을 안 쓰는 것 같다면 모두가 볼 수 있는 위키에 필요한 최적화 리스트를 작성하자. 자신이 할 수 있는 한 걸음을 내디디면 된다.</p>

<p>냉소적이고 비관적인 사람을 멀리해야 한다. 힘이 되는 좋은 버프를 달고 게임을 만들어도 힘들다. 그런데 이런 디버프까지 달고 극복하며 개발할 여력이 있을까? 나 스스로도 이런 태도를 가지지 않게 주의해야 한다. ’한 걸음의 힘’을 믿고 할 수 있는 가장 작은 목표를 찾아서 해보자.</p>

<h2 id="운동은-훌륭한-gcgarbage-collection-메서드">운동은 훌륭한 GC(Garbage Collection) 메서드</h2>

<p>나이가 들면 자연스럽게 운동을 시작한다. 건강 검진에서 빨간색 수치가 하나둘씩 보이게 되고 운동만이 살 길이라는 걸 깨닫기 때문이다. 빨간색 스탯이 뜨기 전에 운동을 시작했으면 더 좋은 삶을 보냈을 것 같다.</p>

<p>체력은 여유와 자상함을 만든다. 체력이 없는데 주변 사람들에게 자상할 수 있는 정신력은 탈인간급이라고 생각한다. 멘탈 버퍼는 결국 체력인 것 같다. 가까운 사람들에게 더 친절해지고 싶으면 운동화를 신고 달리러 가면 된다.</p>

<p>운동은 훌륭한 GC(Garbage Collection) 수단이다. 임시 객체가 가득한 머릿속을 정리한다. 운동하기 전에 가득한 잡생각이 숨이 차오르는 운동이 끝날 즈음엔 거의 살아남지 못한다. 게임 런칭후 발생한 각종 버그에 정신적, 육체적으로 힘들었을 때 걸어서 퇴근한 게 도움이 됐다. 버스가 끊긴 늦은 시간이었는데도 걸어갔다. 퇴근하며 GC를 해주고 다음 날 출근해서 잡생각을 머릿속에 쌓는 생활의 반복이었다. 지금 돌아보면 그때 걸어서 퇴근한 게 나를 지켜준 것 같다.</p>

<p>어떤 운동이라고 시작하자. 주짓수 같은 운동은 한 살이라도 젊을 때 하는 게 좋은 것 같다. 나이가 들수록 관절기 부상으로 시작하기 어려운 운동이다. 주짓수를 배워보고 싶었지만 나이 때문이 타격기인 복싱을 배웠다. 달리기도 좋다. 달려서 출근할 수 있는 거리에 산다면 출근과 운동을 한 번에 해치울 수 있다. 미혼이라면 새벽 수영을 강력하게 추천한다.</p>

<p>GC도 하고 몸도 건강해지고 멘탈 버퍼도 만드는 운동이 최고다.</p>

<h2 id="일을-100로-완료하는-습관을-가져라">일을 100%로 완료하는 습관을 가져라</h2>

<p>Issue tracking system을 통해 받은 이슈를 완료하기 전 100%로 완료하기까지 남은 일이 있는지 확인하자. 공유할 거리가 있다면 문서를 써서 팀원에게 발표하자. 거창하지 않아도 괜찮다. 정기적으로 열리는 주간 회의 같은 곳에서 같은 팀 동료에게만 공유해도 충분하다.</p>

<p>덩치가 큰 일이면 잘 정리해서 개발자 컨퍼런스에서 발표를 해보자. 90%로 끝냈을 일이 발표 자료를 만들면서 <a href="https://news.hada.io/topic?id=10156">100%로 만들어지</a>는 경험을 할 수 있다. 나는 ’<a href="https://ohyecloudy.com/pnotes/archives/ndc22/">NDC22 달빛조각사에서 서버 테스트 코드를 작성하는 방법 발표 후기</a>’를 준비하면서 그런 경험을 했다. 만약 발표하지 않았다면 90% 정도에서 스스로 만족했을 것이다.</p>

<h2 id="engineering-manager엔지니어링-매니저의-기회가-있다면-잡아라">Engineering manager(엔지니어링 매니저)의 기회가 있다면 잡아라</h2>

<p>IC(Individual Contributor)면 절대 배우지 못할 것들을 배우는 매니저를 할 수 있는 기회가 있다면 하라고 얘기하고 싶다. 프로젝트를 보는 시야와 프로젝트에 대한 기여를 한 번에 점프시킬 기회다. 한 번 매니저를 하면 끝까지 매니저를 하는 것도 아니다. 다시 IC로 내려가 프로젝트에 기여하기도 한다. 팀장의 고충을 아는 팀원의 배려는 고맙다.</p>

<p>프로그램팀장을 하는 사람들의 태도가 변해가는 걸 곁에서 보곤 한다. 프로그램팀 입장만 고수하는 사람에서 더 큰 시야로 프로젝트에 도움이 되려면 어떻게 해야 하는지를 생각하고 행동하게 된다. 커뮤니케이션도 좀 더 부드러워지는 것 같다. 나만 잘하면 되는 직책에서 프로그램팀이 잘해야 하고 더 크게는 프로젝트가 잘 돼야 하는 넓고 높아진 목표를 경험하면 변하게 된다.</p>

<p>팀원과 연봉 협상을 하고 사인받는 시기가 오면 소화가 잘되지 않아서 점심을 건너뛰곤 했다. 소화가 잘되지 않아서 식사를 건너뛴 건 딱 이때뿐이었다. 그만큼 엄청난 부담이었다. 매번 아쉬운 소리를 들으며 사인 여부를 결정하는 처지에서 아쉬운 소리를 하며 사인받아야 하는 처지가 되었다. 이 정도는 되겠지 하는 암묵적인 추가적인 협상 금액을 융통하며 사인받아야 했다. 어떤 협상 카드를 내면 내가 PD에게 조금이라도 더 추가로 타내려고 노력하겠다는 걸 연봉 협상을 하는 입장이 되어보니 알겠더라. 지금은 전자서명으로 모든 게 바뀌었다. 바뀌기 전 연봉 협상을 직접 하는 소중한 경험을 하게 됐다. 물론 지금 다시 하라면 점심을 또 굶을지도 모르겠다.</p>

<p>실무를 놓아서는 안 된다. 처음에는 매니징을 100% 가까이하면서 시스템을 만들고 안정적으로 굴러가기 시작하면 50% 정도로 낮추면 이상적일 것 같다. 매니저를 맡은 후로 계속해서 맡을 수도 있지만 IC(individual contributor)로 기여할 수 있는 좋은 프로젝트에 참여할 기회를 얻기도 한다. 매니저를 맡았을 때의 경험을 바탕으로 매니저를 도우며 충분히 IC로서 프로젝트 참여도 할 수 있다. ’<a href="https://ohyecloudy.com/pnotes/archives/book-fundamentals-of-software-architecture/">소프트웨어 아키텍처 101 (마크 리처즈, 닐 포드, 2021)</a>’ 책에서 도움을 얻을 수 있다. 우선 순위가 떨어지는 일 위주로 해야 한다. 병목이 되는 일을 하면 디테일에 매몰돼서 정작 해야할 매니징을 소홀히 하게 되니 피할 수 있으면 피해야 한다. 나는 몇 번 어쩔 수 없이 병목을 맡게 돼서 디테일에 매몰되면 어떻게 되는지를 몸으로 체험했다. <a href="https://ko.wikipedia.org/wiki/%EA%B0%9C%EB%85%90_%EC%A6%9D%EB%AA%85">PoC(Proof of Concept)</a>를 해보는 것도 좋다. Technical Debt을 갚는 일도 천천히 진행해 보자. 정리해 보면 개발 스케쥴의 크리티컬 패스에 놓이지 않은 일이라면 무엇이든 괜찮다.</p>

<p>AI-assisted software development(AI 보조 소프트웨어 개발)이 점점 더 발전하면서 매니징 경험이 AI를 보조 도구로 사용하는 데 도움이 되고 있다. 특히 모호한 일을 명확한 일로 가공해서 팀원에게 나눠주는 일이 도움이 된다. 미래에는 모두가 팀장 역할을 해야 하지 않을까? 모든 팀원이 AI 에이전트를 몇 개씩 굴릴지도 모르니 말이다.</p>

<h2 id="경력에-걸맞는-시야를-가져야-하고-기여를-해야-한다">경력에 걸맞는 시야를 가져야 하고 기여를 해야 한다</h2>

<p>매니징을 하는 건 골치 아픈 일이고 피할 수 있으면 좋다는 얘기를 예전에 업계 선배에게 들은 적이 있다. 정말 개인에게 좋은 것일까?</p>

<p>경력에 따라 팀 퍼포먼스에 더하기 또는 곱하기를 한다. 목표가 잘 정리된 일을 빠르고 정확하게 하는 일은 더하기다. 모호한 문제를 풀 수 있는 문제를 재정의할 수 있는 순간부터 곱하기를 하기 시작한다.</p>

<p>자신이 곱하기 영향력을 끼친다고 자각한 순간부터는 돋보기를 들고 코드 한 줄에 집중하기보단 높은 곳으로 올라가 코드 전체를 보기 시작해야 한다. 중요한 일은 직접 처리하지만 목표와 수단이 명확한 일은 매니저와 상의해서 다른 사람에게 넘기는 일을 시작해야 한다.</p>

<p>퍼포먼스 뿐만 아니라 문화에도 곱하기 역할을 한다. 시니어의 나쁜 업무 태도는 팀 전체에 강한 디버프를 걸게 한다. 실력이 엄청 뛰어나면 감수할 수 있지 않을까? 그런 경우를 겪으면 한 번 생각이 정리될 것 같다. 나는 나쁜 업무 태도를 커버할 만큼 출중한 실력을 갖춘 사람을 아직 한 명도 보지 못했다.</p>

<p>나는 어떤 기여를 해야 하는 걸까? 매니저에게 상담하기에도 좋은 주제다. 한 번쯤은 물어보자.</p>

<h2 id="순수-엔지니어가-아니라-게임-프로그래머">순수 엔지니어가 아니라 게임 프로그래머</h2>

<p>프로그래머로서 기본 역량이 중요하다. 이건 프로그래머라는 직업을 갖는 데 필요한 기본 소양이다. 새로운 기술을 따라가기도 바쁘지만 여기에서 끝나지 않는다. 도메인 지식 또한 필요하다.</p>

<p>게임 개발에 관심이 아주 많고 도메인 지식도 풍부하지만 프로그래밍 실력이 형편없는 프로그래머. 프로그래밍 실력은 출중하지만 게임에는 아예 관심이 없는 게임 프로그래머. 각자 적절한 균형점을 찾아야 한다. 모두 다 자기 역량을 발휘할 수 있는 적절한 위치가 있지만 기본적인 도메인 지식은 꼭 필요하다.</p>

<p>왜 기본적인 도메인 지식이 필요한가? 순수 엔지니어로 가는 걸 막아준다고 생각하기 때문이다. 문제 해결에 더 집중하고 비즈니스 퍼스트로 생각해야 하기 때문이다. 더 나아가 PD에게 게임 프로그래머가 줄 수 있는 옵션도 제공할 수 있기 때문이다. 예를 들면 어떤 게임 영상을 최근에 봤는데 R&amp;D를 해보니 우리도 가능할 것 같다는 프로그래머가 먼저 줄 수 있는 그런 옵션들이다.</p>

<p>도메인 지식은 게임을 많이 해보면서 쌓는다. 컨텐츠를 구성하는 트렌드를 봐야 한다. 다른 게임과 구별 짓는 엣지(edge)를 어떻게 만들어 내는지도 배운다. 어디까지 해야 할까? 프로그래밍 공부에도 벅찬데 게임 트랜드까지 따라가야 한다. 어디까지 따라갈 수 있을까? 답은 없다. 답이 없다고 아무런 생각을 안 해도 되는 건 아니다. 답이 여러 개이니 자신의 답을 찾아야 한다.</p>

<p>나는 식사 자리나 회의 자리에서 소재가 되는 게임을 따라 해 보겠다고 생각했다가 포기한 적이 있었다. 게임 디자이너가 게임하는 양을 보니 도저히 따라가지 못하겠다. 내가 게임에 할애할 수 있는 시간이 초라해 보인다. 내가 내린 절충안은 레퍼런스 게임 플레이다. 레퍼런스 게임부터 다 해보고 시간이 더 확보되면 트렌드를 주도하는 게임을 해본다.</p>

<h2 id="미워하지-말고-기대치를-낮춰라">미워하지 말고 기대치를 낮춰라</h2>

<blockquote>
  <p>판단하지 말고 호기심을 가져라 - 월트 휘트먼 via <a href="https://ohyecloudy.com/lifelog/archives/video-ted-lasso-season-1/">테드 래소 시즌 1 (Apple TV+, 2020)</a></p>
</blockquote>

<p>엄청난 말이다. 이내 시큰둥해진다. 이렇게 할 수 있는 사람이 과연 있기나 한 것일까?</p>

<p>따뜻한 차를 옆에 두고 말을 곱씹어본다. 기대하지 말라는 말과도 통하는 것 같다. 판단하려면 기대가 있어야 한다. 내 기대에 얼마나 부응하는지 못 미치는지를 판단하는 것이다.</p>

<p>일을 못 한다고 미워할 필요가 있을까? 모두 자신의 정글에서 열심히 살고 있다. 기대를 너무 해서 미워하게 되는 것이다. 어떤 기대를 하는지 명료하게 글로 옮기지 못할 정도로 추상적이고 감정에 따라 바뀐다. 이런 기대에 부응하는 사람은 애초에 존재하지 않는지도 모른다.</p>

<p>미워하지 말고 기대를 낮춰라. 어쩌면 호기심이 생길지도 모르겠다.</p>

<h2 id="변화를-일으키고자-하는-열정의-상한선">변화를 일으키고자 하는 열정의 상한선</h2>

<p>“내가 이렇게까지 해야 하는가?”</p>

<p>열심히 일하다 보면 이런 생각이 들 수 있다. 더 많은 시간을 프로젝트에 쏟는 것이 꼭 헌신이 아니다. 똑같은 시간을 일하더라도 더 많은 관심의 에너지를 쏟을 때도 헌신이 필요하다고 생각한다. 내가 생각하는 대로 바꾸면 더 효율적으로 게임을 만들 수 있다고 생각하는 데 적극적으로 따라주지 않는다. 이런 걸 게임에 넣으면 정말 재미있을 텐데 디자인팀에서는 별 반응이 없다. 모두 게임을 많이 하지 않는다고 허공에 대고 모두를 비난한다.</p>

<p>“모르겠고 그냥 남들이 하는 만큼만 해야겠다”</p>

<p>아무도 모르게 삐지게 된다. 어느 순간부터는 다른 열정적인 멤버의 장애물이 된다. 삐진 걸 드러내고 싶어 한다. “내가 예전에 얘기했는데”를 달고 살며 변화를 시키고자 하는 사람들의 의욕을 꺾는 일을 하게 된다.</p>

<p>상한선이 있는 것 같다. 남을 원망하지 않는 한도 내에서 지속적으로 변화를 일으키고자 하는 열정이 꺾이지 않는 한도 내에서만 한다. 현재 내가 내린 답은 ’한걸음의 힘’이다. 변화의 시작은 사소하며 그렇게 혼자서 계단을 쌓아가다 보면 자연스럽게 설득이 돼서 같이 계단을 쌓는 동료가 생기게 된다.</p>

<p>한 번에 거대한 변화를 동료에게 쌓은 믿음의 마일리지 하나도 없이 시도했다가 실패해서 삐지지 않아야 한다. 다른 동료의 열정을 꺾지 않아야 한다. 자신이 가지고 있는 열정의 상한선을 기억하자.</p>

<h2 id="라이브-프로젝트-경험을-쌓아라">라이브 프로젝트 경험을 쌓아라</h2>

<p>주니어로 신규 프로젝트에 참여해서 런칭하고 라이브 경험까지 쌓는 게 베스트다. 하지만 게임 런칭이란 내가 통제할 수 없는 것들에 좌지우지되는 게 많아서 운도 따라줘야 한다. 그래서 주니어일 때, 라이브 중인 게임에 입사 지원하는 게 좋지 않을까 생각한다.</p>

<p>라이브 개발팀에 들어가면 기획부터 시작해 유저에게 배포해서 피드백을 받는 전체 프로세스를 경험할 수 있다. 단계마다 배우는 게 다르다. 게임 컨셉을 증명해야 하는 프리프로덕션 단계에서는 기술적으로 모호한 것들을 해결해야 한다. 컨텐츠 대량 생산을 하는 프로덕션 단계에서는 프리프로덕션 단계에서 미처 준비하지 못했던 생산성에 집중해야 하며 대량 생산에 맞지 않은 구조를 발견하고 고쳐야 한다. 라이브 단계에서는 앞에서 한 잘못된 결정에 대해 대처해야 하며 달리는 차 안에서 차 바퀴를 서서히 교체해야 한다. 이런 일련의 경험을 반복해서 차례로 하면 좋겠지만 거듭 말하는바 런칭은 힘들다.</p>

<p>그래서 마지막 단계라고 볼 수 있는 라이브 서비스 단계를 주니어 때 경험하면 좋지 않을까 생각한다. 내 첫 프로젝트는 프로덕션 단계까지 가지 못하고 종료했다. 종료 후 라이브 서비스 중인 프로젝트로 이동했다. 내 코드가 QA를 통과하고 유저에게 배포되는 것까지 경험하니 시야가 넓어진 느낌이었다. 이전에 잘못한 결정으로 고통받으며 다음 게임에서는 어떻게 코드를 짜야 할지 영감도 얻었다. 첫 프로젝트에서 좋은 사람들을 많이 만나서 후회는 없지만 주니어일 때 라이브 서비스를 하는 프로젝트에 들어갔으면 어땠을까 생각하게 됐다.</p>

<h2 id="홈그라운드를-넓힐-수-있는-도구를-벼리자">홈그라운드를 넓힐 수 있는 도구를 벼리자</h2>

<p>홈그라운드 역할을 할 수 있는 에디터를 하나 선택해서 꾸준히 사용하며 익혔던 게 도움이 됐다. 이왕이면 고인 물이 많은 도구를 선택하는 게 좋다. 내가 앞서가지 못하더라도 고인 물이 앞서서 뛰어가며 내 멱살을 잡아서 끌어주기 때문이다. AI를 붙인 에디터가 많이 나오네. 이렇게 감탄만 하고 있다 보면 내가 쓰는 에디터에도 관련 패키지가 추가된다. 이 글을 쓰는 지금 Visual Studio Code가 다 싸잡아 먹고 있기 때문에 이걸 선택하는 것도 좋을 것 같다. 하지만 평생 주력으로 삼을 에디터 하나만 선택하고 싶다면 <a href="https://ohyecloudy.com/emacsian/">Emacs</a>나 Vim을 추천한다.</p>

<p>확장이 쉬워야 한다. 주력 에디터로는 Visual Studio 밖에 없던 시절 내 입맛에 맞는 플러그인 하나 만들기가 왜 그리 어려웠던지 용두사미로 끝나곤 했다. 열정이 부족한 탓도 있겠지만 간단한 기능도 손쉽게 확장 기능으로 넣지 못하는 에디터 설계 자체에도 문제가 있었다. Emacs는 다르다. 내 입맛에 맞게 함수도 추가하고 패키지도 수정하며 점점 더 내 수족이 되어가고 있다. Emacs가 아니더라도 이런 에디터를 골라야 한다.</p>

<p>IDE로는 JetBrains Rider나 Visual Studio를 사용하더라도 에디터 하나쯤은 오래 사용할 걸 하나 선택해서 공부해 보는 걸 추천한다. AI 시대에 더욱더 글을 쓰고 편집하고 확인하는 게 중요해지고 있다. IDE 말고 잘 다루는 에디터 하나 배워서 손해 볼 게 없다.</p>

<h2 id="스터디에-참여하라">스터디에 참여하라</h2>

<p>’<a href="https://cafe.naver.com/architect1">아꿈사</a>’와 ’<a href="https://cafe.naver.com/shader">Shader Study</a>’ 모임에 꾸준히 나갔었다. 발표 준비를 하면서 평소 공부하는 것보다 더 완성도를 높이는 경험도 하게 된다. 그러면서 더 배움의 너비와 깊이를 더할 수 있다. 작은 인원이지만 여러 사람 앞에서 발표하는 경험을 많이 할 수 있다. 게임 개발뿐만 아니라 다른 도메인을 가진 프로그래머를 만나 같이 공부하며 연결된 경험을 나누며 영감을 얻을 수 있다.</p>

<p>가장 도움이 되는 건 뭐였을까? 자극이다. 쉬고 싶은 개인 시간 중 일부를 할당해 공부하고 스터디 모임에 나오는 사람들이다. 새로운 시각을 배우고 열심히 사는 모습에 자극받는다. 다양한 경험에서 많은 영감도 얻는다.</p>

<p>풀파티를 구성하다 보니 이런 모임에 할당할 수 있는 시간의 틈을 찾기가 힘들다. 하지만 애들이 좀 커서 힘든 구간이 지났으니 다시 평일에 열리는 스터디 모임을 찾아보고 싶다.</p>

<h2 id="과거-프로젝트-멤버와의-연결-고리">과거 프로젝트 멤버와의 연결 고리</h2>

<p>간만에 연락하면 이직 상담인 줄 알까 봐 단순 안부 인사라는 걸 알리곤 한다. 과거에 참여했던 프로젝트를 돌이켜보면 좋은 멤버들이 많았는데, 연락이 많이 끊겼다. 메신저 친구로는 남아있지만, 안부를 나는 뒤 이어갈 화제가 마땅치 않다.</p>

<p>업계 소식이나 트렌드에 대한 얘기를 나누지 않더라도 각자의 정글에서 살아남고 있는 무용담을 서로 나누며 위로받는 것만으로도 정신 건강에 도움이 된다.</p>

<p>그래도 지금은 꾸준히 술자리나 점심 식사 자리를 만들고 있다. 과거에 같이 게임을 만들던 좋은 사람이 많았는데, 연락이 많이 끊겨서 아쉽다.</p>

<h1 id="다음-10년을-생각하며">다음 10년을 생각하며</h1>

<p>요즘은 경력 지속가능성에 대해 많은 생각을 하게 된다. 큰돈을 벌어서 은퇴하게 된다면 어떤 걸 하게 될까? 결국 뭔가를 만들 것이다. 시간을 온전히 내가 소유한다는 게 달라질 뿐이다. 은퇴는 요원하고 이래 되나 저래 되나 뭔가를 만드는 걸 계속할 텐데, 이왕이면 더 뿌듯한 걸 만들고 싶다.</p>

<p>이제는 매니저급이 아니면 이직하기 힘들어지는 경력이 됐다. 위에 선배들이 많은 큰 회사에 들어가면 매니징을 하지 않을 수 있을까? 그렇게 실무로 입사한다고 해도 매니저를 할 수 있는 인력으로 분류돼서 조직개편이든 차기 신작이든 결국 매니저를 맡게 될 것이다. 매니저를 하면서도 일정 비율을 프로그래밍에 할당할 수 있는 잘 굴러가는 시스템을 만드는 능력을 길러야 한다. 크리티컬 패스에 놓여있지 않은 업무에도 재미있는 게 많아서 다행이다.</p>

<p>다시 코로나19처럼 재택 업무가 가능해질까? 코로나19 전후로 업무 환경이 많이 바뀔 것으로 예상했지만 몇몇 회사를 빼놓고는 출근을 강제한다. 재택근무를 하면 일을 안 하고 논다고? 일과 일상이 구분을 잘 못해서 하루 종일 일을 하는 것 같았다. 오히려 일상을 지키려고 노력했던 것 같다. 업무와 일상을 분리하는 게 자리를 잡아갈 때즘 재택근무가 끝나서 아쉽다. 재택근무 좋았는데. 재택근무에 대한 미련이 아직 남아있다. 기회가 오기 전까지는 비동기(asynchronous)로 일하는 방법을 강화하며 기다릴 것 같다. 동기(synchronous)로 일하는 건 모두가 할 수 있다. 비동기로 일하는 게 어렵다. 하지만 효율적이다. 출근 근무를 하더라도 효율적이다.</p>

<p>이제는 AI라는 단어가 어떤 문장에 들어가도 더 그럴듯하고 더 나아질 것 같은 시대다. AI가 텍스트를 다루는 프로그래머에게 가장 먼저 스며드는 게 당연하다. 텍스트는 AI가 다룰 수 있는 primitive이기 때문이다. GitHub Copilot 같은 AI 보조 도구는 써야 한다와 안 써야 한다로 토론하는 시기는 지났다고 생각한다. 어떤 보조 도구가 괜찮은지. 어떻게 사용하면 더 효율적으로 사용할 수 있는지. 잘 쓰는 방법을 궁리해야 할 단계이다.</p>

<p>AI 위임은 허들을 하나씩 넘으며 현실로 다가오고 있다. AI 보조 도구처럼 당연시되는 날이 올지도 모른다. 근본은 달라지지 않는다. 문제 해결을 위한 도구만 달라지는 것이다. 뒤처지지 않고 AI 보조 도구 및 위임 같은 걸 부지런히 따라갈 계획이다. 너무 빠르지도 않고 너무 느리지도 않게 속도를 잘 조절하고 싶다. 시장을 지배하는 유력한 프로그램이 나올 때쯤이 아닐까? 여러 제품을 사용해 보며 평가할 만큼 시간이 많지 않다. 시장을 지배하는 제품이 나오면 그걸 사용한다. 초기 제품 평가에 웬만하면 시간을 쓰지 말고 큐레이션을 적극 활용한다. AI 도구에 잡아먹히지 않고 실용적으로 사용하려는 내 절충안이다.</p>

<h1 id="마치며">마치며</h1>

<p>자부심이든 후회든 적을 게 있어서 다행이다. 누군가에겐 자극을 누군가에겐 위로를 줬다면 충분하다. 다음 10년이 기대된다.</p>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="retrospective" /><summary type="html"><![CDATA[2005년에 넥슨 공채로 시작한 게임 프로그래머 경력이 벌써 20주년을 맞이했다. 학교에서 보낸 시간보다 게임 프로그래머로 돈을 번 시간이 더 길다. 20주년이 된 기념으로 소박한 회고를 남겨본다.]]></summary></entry><entry><title type="html">Elixir 빌드 결과물인 릴리즈의 런타임 설정은 어떻게 관리해야 할까?</title><link href="https://ohyecloudy.com/pnotes/archives/elixir-release-runtime-configuration/" rel="alternate" type="text/html" title="Elixir 빌드 결과물인 릴리즈의 런타임 설정은 어떻게 관리해야 할까?" /><published>2025-09-07T00:00:00+09:00</published><updated>2025-09-07T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/elixir-release-runtime-configuration</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/elixir-release-runtime-configuration/"><![CDATA[<p>Elixir에서는 두 종류의 설정 파일이 있다. 빌드 타임 설정에 사용하는 <code class="highlighter-rouge">config.exs</code> 파일과 런타임 설정에 사용하는 <code class="highlighter-rouge">runtime.exs</code> 파일이다. 두 파일 모두 사용할 수밖에 없다. 빌드 타임에 모든 걸 결정할 수 없기 때문이다. DB 접속 정보가 대표적인 예다.</p>

<p>런타임 설정을 어떻게 관리하면 좋을까? <code class="highlighter-rouge">runtime.exs</code> 파일을 직접 수정하는 건 좋은 생각이 아니다. <code class="highlighter-rouge">runtime.exs</code> 파일은 배포를 위해 릴리즈(release)를 만들면 같이 포함되기 때문이다. 내가 편집한 설정이 릴리즈에 포함된 파일에 덮어 씌여져서 사라지는 걸 방지해야 한다.</p>

<h1 id="환경-변수가-있으면-읽어서-사용">환경 변수가 있으면 읽어서 사용</h1>

<p>환경 변수가 있으면 읽어서 사용하는 방법을 생각할 수 있다. <a href="https://ohyecloudy.com/pnotes/archives/deploying-elixir-project-to-heroku/">Heroku</a> 같은 <a href="https://azure.microsoft.com/ko-kr/resources/cloud-computing-dictionary/what-is-paas/">PaaS(platform as a service)</a>를 사용하면 앱마다 환경 변수를 웹에서 손쉽게 관리할 수 있어서 설정하기 편하다.</p>

<p>예를 들어 개발 환경이 아닌 배포 환경에서만 <a href="https://ohyecloudy.com/pnotes/archives/sentry-error-tracking-software/">Sentry</a>를 사용한다면 <code class="highlighter-rouge">runtime.exs</code> 파일을 통해 설정할 수 있어야 한다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># runtime.exs</span>

<span class="kn">import</span> <span class="no">Config</span>

<span class="k">if</span> <span class="n">sentry_dsn</span> <span class="o">=</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">"SENTRY_DSN"</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">config</span> <span class="ss">:sentry</span><span class="p">,</span> <span class="ss">dsn:</span> <span class="n">sentry_dsn</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">System.get_env/1</code> 함수로 환경 변수를 읽어 있다면 세팅하게 구성한다. <code class="highlighter-rouge">release.exs</code> 파일에 <a href="https://ohyecloudy.com/pnotes/archives/read-settings-from-environment-variables/">elixir 코드를 사용할 수 있어서 N 개의 설정을 읽는 복잡한 작업도 가능</a>하다.</p>

<p><a href="https://ohyecloudy.com/ddiary/2025/03/22/systemd-journald/">Systemd</a>로 elixir 앱 실행을 관리한다면 서비스 파일로 환경 변수를 세팅한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Service]
...

Environment="ACCOUNT1_INTERVAL_MINUTE=60"
Environment="ACCOUNT1_KEY=[SECRET]"
Environment="ACCOUNT1_SECRET=[SECRET]"
Environment="ACCOUNT1_TOKEN=[SECRET]"
Environment="ACCOUNT1_TOKEN_SECRET=[SECRET]"
...
</code></pre></div></div>

<p>대부분은 환경 변수만으로 충분하다. 특히 사이드 프로젝트 규모라면. 만약 복잡한 계층 구조를 가진 설정이라서 환경 변수들로 평면적으로 풀 수 없다면 어떻게 하면 될까? 설정 파일을 써야 한다.</p>

<h1 id="외부-설정-파일을-사용">외부 설정 파일을 사용</h1>

<p><code class="highlighter-rouge">runtime.exs</code> 파일을 직접 수정하는 건 피하고 싶다. 릴리즈를 만들어 배포할 때, <code class="highlighter-rouge">runtime.exs</code> 파일이 포함되기 때문이다. 디폴트 설정을 <code class="highlighter-rouge">runtime.exs</code> 파일에 하고 사용자가 바꿀 수 있는 설정은 환경 변수나 외부 설정 파일로 빼면 배포할 때마다 설정이 덮어 씌여지는 걸 걱정하지 않아도 된다.</p>

<p><a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS(Filesystem Hierarchy Standard)</a>를 준수한다면 외부 설정 파일 경로는 <code class="highlighter-rouge">/etc/opt/my_app/runtime.exs</code> 정도가 좋겠다. <code class="highlighter-rouge">/etc/my_app/runtime.exs</code> 정도로 해도 무난하다. 만약 Docker 라면 <code class="highlighter-rouge">/config/runtime.exs</code> 경로를 사용해도 된다.</p>

<p>아래와 같은 외부 설정 파일을 만든다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># /etc/opt/my_app/runtime.exs</span>
<span class="kn">import</span> <span class="no">Config</span>

<span class="n">config</span> <span class="ss">:my_app</span><span class="p">,</span> <span class="no">MyApp</span><span class="o">.</span><span class="no">Repo</span><span class="p">,</span>
  <span class="ss">database:</span> <span class="s2">"ecto_simple"</span><span class="p">,</span>
  <span class="ss">username:</span> <span class="s2">"postgres"</span><span class="p">,</span>
  <span class="ss">password:</span> <span class="s2">"postgres"</span><span class="p">,</span>
  <span class="ss">hostname:</span> <span class="s2">"localhost"</span>
</code></pre></div></div>

<p>디폴트 설정을 모아두는 <code class="highlighter-rouge">runtime.exs</code> 파일에서는 외부 설정 파일이 있는지 검사하고 로드한다. 기본 설정을 덮어쓸 수 있게 제일 마지막에 검사하고 적용해야 한다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="no">Config</span>

<span class="c1"># 설정 마지막에 외부 설정 파일 로드</span>
<span class="n">external_config_path</span> <span class="o">=</span> <span class="s2">"/etc/opt/my_app/runtime.exs"</span>
<span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">external_config_path</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"Loading external config from </span><span class="si">#{</span><span class="n">external_config_path</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="no">Code</span><span class="o">.</span><span class="n">eval_file</span><span class="p">(</span><span class="n">external_config_path</span><span class="p">)</span>
<span class="k">else</span>
  <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"No external config found at </span><span class="si">#{</span><span class="n">external_config_path</span><span class="si">}</span><span class="s2">, using default settings"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<h1 id="마치며">마치며</h1>

<p><code class="highlighter-rouge">release.exs</code> 파일을 직접 수정하는 건 피해야 한다. 릴리즈에 포함돼서 덮어쓰는 걸 주의해야 하기 때문이다. <code class="highlighter-rouge">release.exs</code> 파일에서 환경 변수를 로드하거나 외부 설정 파일을 읽도록 지원해야 한다.</p>

<p>사이드 프로젝트는 외부 설정 파일까지는 필요 없고 환경 변수만으로 충분하다. 그래도 또 모른다. 프로젝트가 오래 진행돼서 커진다면 환경 변수를 사용해 복잡한 설정을 1차원으로 풀어내는 게 버거워진다. 그때 외부 설정 파일을 고려하면 된다.</p>

<h1 id="링크">링크</h1>

<ul>
  <li><a href="https://azure.microsoft.com/ko-kr/resources/cloud-computing-dictionary/what-is-paas/">PaaS(Platform as a Service)란 무엇인가요?- Microsoft Azure - azure.microsoft.com</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">Filesystem Hierarchy Standard - en.wikipedia.org</a></li>
  <li><a href="https://ohyecloudy.com/ddiary/2025/03/22/systemd-journald/">#TIL systemd로 프로세스를 관리하고 systemd-journald로 로그를 본다 - dev diary, TIL - ohyeclou…</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/deploying-elixir-project-to-heroku/">Elixir 프로젝트를 Heroku에 배포하기 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/read-settings-from-environment-variables/">#elixir 환경 변수로부터 N개의 설정을 읽기 - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
  <li><a href="https://ohyecloudy.com/pnotes/archives/sentry-error-tracking-software/">크래시 리포트, 에러 트래킹이 필요하면 sentry - ohyecloudy’s pnotes - ohyecloudy.com</a></li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="uncategorized" /><category term="elixir" /><category term="config" /><category term="environment-variables" /><category term="release" /><summary type="html"><![CDATA[Elixir에서는 두 종류의 설정 파일이 있다. 빌드 타임 설정에 사용하는 config.exs 파일과 런타임 설정에 사용하는 runtime.exs 파일이다. 두 파일 모두 사용할 수밖에 없다. 빌드 타임에 모든 걸 결정할 수 없기 때문이다. DB 접속 정보가 대표적인 예다.]]></summary></entry><entry><title type="html">Elixir key-value 자료구조 함수 네이밍 규칙 - get, fetch, fetch!</title><link href="https://ohyecloudy.com/pnotes/archives/naming-elixir-get-fetch/" rel="alternate" type="text/html" title="Elixir key-value 자료구조 함수 네이밍 규칙 - get, fetch, fetch!" /><published>2025-08-24T00:00:00+09:00</published><updated>2025-08-24T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/naming-elixir-get-fetch</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/naming-elixir-get-fetch/"><![CDATA[<p>Elixir 표준 라이브러리 중 key-value 자료구조에 규칙을 가진 것처럼 보이는 함수가 있다. <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">fetch</code>, <code class="highlighter-rouge">fetch!</code> 와 같은 값을 가져오는 함수이다. 자료 구조에서 값을 가져오는 함수로 <code class="highlighter-rouge">fetch</code> 를 사용하는 게 낯설었다. 어떤 규칙으로 설계했는지 궁금해졌다.</p>

<h1 id="요약-by-human">요약 by human</h1>

<ul>
  <li>Elixir에서 key-value 자료구조에서 값을 가져오는 함수가 3개
    <ul>
      <li><code class="highlighter-rouge">get</code> - 없으면 default 값</li>
      <li><code class="highlighter-rouge">fetch</code> - {:ok, value} 형식으로 패턴매칭하기 좋게 리턴</li>
      <li><code class="highlighter-rouge">fetch!</code> - 없으면 에러를 내는 터프한 함수</li>
    </ul>
  </li>
  <li>Erlang은 <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">find</code> 이름을 사용</li>
  <li>Erlang 네이밍 규칙이 더 마음에 든다
    <ul>
      <li><code class="highlighter-rouge">get</code> 은 빠르게 값을 가져오고 <code class="highlighter-rouge">fetch</code> 는 DB나 웹에서 가져오는 식으로 이름을 붙여왔음</li>
    </ul>
  </li>
</ul>

<h1 id="get-fetch-fetch-구분"><code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">fetch</code>, <code class="highlighter-rouge">fetch!</code> 구분</h1>

<blockquote>
  <p>When you see the functions <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">fetch</code>, and <code class="highlighter-rouge">fetch!</code> for key-value data structures, you can expect the following behaviours:</p>

  <ul>
    <li><code class="highlighter-rouge">get</code> returns a default value (which itself defaults to nil) if the key is not present, or returns the requested value.</li>
    <li><code class="highlighter-rouge">fetch</code> returns :error if the key is not present, or returns {:ok, value} if it is.</li>
    <li><code class="highlighter-rouge">fetch!</code> raises if the key is not present, or returns the requested value.</li>
  </ul>

  <p><a href="https://hexdocs.pm/elixir/1.18.4/naming-conventions.html#get-fetch-fetch">Naming conventions — Elixir v1.18.4 - hexdocs.pm</a></p>
</blockquote>

<p>예전에는 이런 네이밍 규칙(naming convention) 문서가 없었던 것 같다. 표준 라이브러리 문서 보강을 많이 했다더니 정말이다.</p>

<p>’없을 수도 있지’의 <code class="highlighter-rouge">get</code> 함수. 없을 때는 디폴트 값을 리턴한다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">([],</span> <span class="ss">:a</span><span class="p">)</span>
<span class="no">nil</span>
<span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:a</span><span class="p">)</span>
<span class="mi">1</span>
<span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:b</span><span class="p">)</span>
<span class="no">nil</span>
<span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:b</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="mi">3</span>
</code></pre></div></div>

<p>세 번째 인자로 디폴트 값을 넘긴다.</p>

<p>’패턴 매칭 편하게 해줄께’의 <code class="highlighter-rouge">fetch</code> 함수.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:a</span><span class="p">)</span>
<span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="mi">1</span><span class="p">}</span>
<span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:b</span><span class="p">)</span>
<span class="ss">:error</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">{:ok, value}</code> 를 리턴해서 패턴 매칭을 편하게 할 수 있다.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:a</span><span class="p">)</span> <span class="k">do</span>
  <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">val</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"찾았다! </span><span class="si">#{</span><span class="n">val</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="ss">:error</span> <span class="o">-&gt;</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="s2">"없다!"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">fetch</code> 함수가 아니라 <code class="highlighter-rouge">get</code> 함수를 호출한다면 디폴트 값을 리턴한 것인지 찾은 key의 값인지 구분할 수가 없다. <code class="highlighter-rouge">has_key?</code> 함수와 같이 써야 확신할 수 있는데, <code class="highlighter-rouge">fetch</code> 함수를 쓰면 한 번에 가능하다.</p>

<p>’없으면 큰일난다’의 <code class="highlighter-rouge">fetch!</code> 함수</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:a</span><span class="p">)</span>
<span class="mi">1</span>
<span class="n">iex</span><span class="o">&gt;</span> <span class="no">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p">([</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">],</span> <span class="ss">:b</span><span class="p">)</span>
<span class="o">**</span> <span class="p">(</span><span class="no">KeyError</span><span class="p">)</span> <span class="n">key</span> <span class="ss">:b</span> <span class="ow">not</span> <span class="n">found</span> <span class="ow">in</span><span class="p">:</span> <span class="p">[</span><span class="ss">a:</span> <span class="mi">1</span><span class="p">]</span>
</code></pre></div></div>

<h1 id="get-과-fetch-의-사전적-의미"><code class="highlighter-rouge">get</code> 과 <code class="highlighter-rouge">fetch</code> 의 사전적 의미</h1>

<blockquote>
  <p>to obtain (something)
(무언가를) 얻다</p>

  <p><a href="https://english.dict.naver.com/english-dictionary/#/entry/enen/f02d052ac06d478681ec0c37b12cd12c">dict.naver.com</a></p>
</blockquote>

<blockquote>
  <p>to go after and bring back (someone or something)
누군가나 어떤 것을 찾아가서 다시 데려오거나 가지고 돌아오다</p>

  <p><a href="https://english.dict.naver.com/english-dictionary/#/entry/enen/fbf2d840805746f1ae34552bdcce574e">dict.naver.com</a></p>
</blockquote>

<p><code class="highlighter-rouge">get</code> 과 다르게 <code class="highlighter-rouge">fetch</code> 는 찾아가서 가지고 돌아온다는 뜻이다. 그래서 더 시간이 걸리는 뉘앙스를 전달할 수 있다. 뭔가 찾아서 가져온다는 뜻이니 반드시 가져온다는 뉘앙스도 전달할 수 있을 것 같다.</p>

<h1 id="erlang은-어떤-함수-이름을-사용하나---get-find">Erlang은 어떤 함수 이름을 사용하나? - <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">find</code></h1>

<p><code class="highlighter-rouge">fetch</code> 라는 함수 이름을 Erlang에서 가져왔다고 생각했으나 보기 좋게 빗나갔다.</p>

<p>Elixir의 <code class="highlighter-rouge">fetch</code> 는 Erlang의 <code class="highlighter-rouge">find</code> 와 비슷하다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1&gt; Map = #{foo =&gt; 42}.
2&gt; maps:find(foo, Map).
{ok,42}
3&gt; maps:find(bar, Map).
error
</code></pre></div></div>

<p>Elixir의 <code class="highlighter-rouge">get</code> 은 디폴트 인자를 명시적으로 넘긴 <code class="highlighter-rouge">get</code> 과 비슷하다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1&gt; Map = #{foo =&gt; 42}.
2&gt; maps:get(foo, Map, 0).
42
3&gt; maps:get(bar, Map, 0).
0
</code></pre></div></div>

<p>Elixir의 <code class="highlighter-rouge">fetch!</code> 는 디폴트 인자가 없는 <code class="highlighter-rouge">get</code> 과 비슷하다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1&gt; Map = #{foo =&gt; 42}.
2&gt; maps:get(foo, Map).
42
3&gt; maps:get(bar, Map).
** exception error: {badkey,bar}
</code></pre></div></div>

<h1 id="erlang의-get-find-규칙이-더-마음에-든다">Erlang의 <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">find</code> 규칙이 더 마음에 든다</h1>

<p>없는 영어 살림에 뭔가 찾아서 가져와서 오래 걸릴 수 있다는 <code class="highlighter-rouge">fetch</code> 라는 용어를 허탈하게 써버려서 아깝다는 생각이 들었다. <code class="highlighter-rouge">get</code> 은 캐시가 있어서 바로 리턴할 수 있고 <code class="highlighter-rouge">fetch</code> 는 웹이나 DB 등에서 가져오지만 느리다는 뜻으로 사용하고 있었기 때문이다.</p>

<p><code class="highlighter-rouge">find</code> 는 찾으니깐 없을 수도 있지. 이런 뉘앙스를 전달해서 Erlang의 네이밍 규칙이 더 마음에 든다.</p>

<p>Erlang에도 <code class="highlighter-rouge">fetch</code> 를 안 쓰는데 왜 이런 단어를 사용했을까? Ruby와 관련이 있을 수 있겠다고 생각했다. Elixir는 Ruby의 영향을 많이 받았다. Elixir를 만든 Jose Valim이 Rails 코어 팀 멤버였기 때문이다. Ruby hash에서 사용하는 <code class="highlighter-rouge">fetch</code> 함수에서 가져오지 않았을까?</p>

<h1 id="마치며">마치며</h1>

<p>Elixir에서 사용하는 <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">fetch</code>, <code class="highlighter-rouge">fetch!</code> 함수가 처음에는 혼란스러웠다. 값을 가져오는 함수가 왜 이렇게 많아? 패턴매칭에 유용해서 <code class="highlighter-rouge">fetch</code> 함수의 진가를 알아봤고 그 외에는 <code class="highlighter-rouge">get</code> 함수를 편하게 사용하고 있다. <code class="highlighter-rouge">fetch!</code> 함수는 쓸 일이 없는데, 언젠가는 쓸 일이 있을 것 같다.</p>

<p>Erlang의 <code class="highlighter-rouge">get</code>, <code class="highlighter-rouge">find</code> 규칙이 더 마음에 든다. <code class="highlighter-rouge">fetch</code> 단어를 빠르게 값을 가져오는 <code class="highlighter-rouge">get</code> 과는 달리 시간이 오래 걸리는 작업을 나타내는 데 쓰고 있기 때문이다.</p>

<p>뭐 그래도 규칙을 잘 정하고 지키기만 한다면 잘 따르다 보면 금방 익숙해진다. Jose Valim도 처음엔 Ruby hash의 fetch 함수 이름이 마음에 들지 않았을 것 같다는 재미있는 생각이 들었다.</p>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="uncategorized" /><category term="naming" /><category term="elixir" /><category term="erlang" /><summary type="html"><![CDATA[Elixir 표준 라이브러리 중 key-value 자료구조에 규칙을 가진 것처럼 보이는 함수가 있다. get, fetch, fetch! 와 같은 값을 가져오는 함수이다. 자료 구조에서 값을 가져오는 함수로 fetch 를 사용하는 게 낯설었다. 어떤 규칙으로 설계했는지 궁금해졌다.]]></summary></entry><entry><title type="html">GitHub Actions에서 사용할 배포용 Linux 유저 생성</title><link href="https://ohyecloudy.com/pnotes/archives/linux-deploy-user-github-actions/" rel="alternate" type="text/html" title="GitHub Actions에서 사용할 배포용 Linux 유저 생성" /><published>2025-08-01T00:00:00+09:00</published><updated>2025-08-01T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/linux-deploy-user-github-actions</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/linux-deploy-user-github-actions/"><![CDATA[<p><a href="https://ohyecloudy.com/pnotes/archives/github-actions-arm64-build-deploy/">GitHub Actions로 ARM64 플랫폼 빌드 및 배포</a> 용도의 Linux 유저를 만들자. GitHub Actions로 빌드한 결과물을 Linux 인스턴스로 복사한다. 빌드 결과물 저장할 디렉터리의 쓰기 권한이 필요하다. 딱 필요한 권한만 세팅한 Linux 유저를 만들자. 비밀키를 <a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub이 제공하는 저장소 환경 secrets</a>에 저장해서 내가 관리하는 것보다 훨씬 더 안전하다고 해서 필요 이상의 막대한 권한을 가진 유저 비밀키를 동네방네 뿌리는 잘못된 습관을 만들지 말자.</p>

<h1 id="배포용-유저-생성---deployuser">배포용 유저 생성 - <code class="highlighter-rouge">deployuser</code></h1>

<p><code class="highlighter-rouge">deployuser</code> 로 직관적인 이름의 유저를 만든다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo useradd -m -s /bin/bash deployuser
</code></pre></div></div>

<p><code class="highlighter-rouge">-m</code> 옵션으로 홈 디렉터리를 만들고 <code class="highlighter-rouge">bash</code> 를 기본 셸로 지정했다.</p>

<h1 id="보조-그룹supplementary-groups-멤버로-추가">보조 그룹(supplementary groups) 멤버로 추가</h1>

<p><code class="highlighter-rouge">deployuser</code> 는 배포할때만 사용한다. 나는 평소에 <code class="highlighter-rouge">ubuntu</code> 유저를 사용한다. 평소에 사용하는 유저로 <code class="highlighter-rouge">deployuser</code> 가 배포한 바이너리를 똑같은 권한으로 접근하고 싶다. 이럴 때 사용하라고 group이 있다. 특정 디렉터리 접근을 위해서는 유저당 여러 개를 가질 수 있는 보조 그룹(supplementary groups)을 추가한다.</p>

<p><code class="highlighter-rouge">appgroup</code> 그룹을 추가하고 <code class="highlighter-rouge">ubuntu</code> 와 <code class="highlighter-rouge">deployuser</code> 유저를 그룹에 추가한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo groupadd appgroup

sudo usermod -aG appgroup ubuntu
sudo usermod -aG appgroup deployuser
</code></pre></div></div>

<p>그룹에 멤버로 추가가 잘 됐는지 확인해본다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grep ^appgroup /etc/group

appgroup:x:1003:ubuntu,deployuser
</code></pre></div></div>

<h1 id="appgroup-그룹이-권한을-가진-디렉터리-생성"><code class="highlighter-rouge">appgroup</code> 그룹이 권한을 가진 디렉터리 생성</h1>

<p><code class="highlighter-rouge">/app</code> 디렉터리를 만들고 소유자는 <code class="highlighter-rouge">deployuser</code>, 그룹은 <code class="highlighter-rouge">appgroup</code> 으로 설정한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo mkdir /app
sudo chown deployuser:appgroup /app

drwxr-xr-x   2 deployuser appgroup  4096 Jun 15 07:07 app
</code></pre></div></div>

<p>유저 뿐만 아니라 그룹에게도 똑같은 권한을 세팅한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo chmod 775 /app

drwxrwxr-x   2 deployuser appgroup  4096 Jun 15 07:07 app
</code></pre></div></div>

<p>GitHub Actions로 <code class="highlighter-rouge">/app</code> 디렉터리로 파일을 복사한다. group이 <code class="highlighter-rouge">appgroup</code> 으로 세팅되게 한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo chmod g+s /app

drwxrwsr-x   2 deployuser appgroup  4096 Jun 15 07:07 app
</code></pre></div></div>

<p><code class="highlighter-rouge">x</code> 가 <code class="highlighter-rouge">s</code> 로 바뀌었다.</p>

<h1 id="ssh-키-생성-및-github에-등록">ssh 키 생성 및 GitHub에 등록</h1>

<p>반대라서 헷갈릴 수 있다. GitHub에서 Linux 인스턴스로 접속한다. 즉 비밀키가 GitHub에 저장되어야 한다.</p>

<p>ssh 키를 생성한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo su deployuser
ssh-keygen -t ed25519 -C "github-actions-deploy"
</code></pre></div></div>

<p><code class="highlighter-rouge">authorized_keys</code> 파일을 만들고 방금 만든 ssh 키의 공개키를 추가한다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

cat ~/.ssh/id_ed25519.pub &gt;&gt; ~/.ssh/authorized_keys
</code></pre></div></div>

<p>이제 비밀키를 클립보드에 복사한다. pbcopy는 클립보드에 저장하는 MacOS 명령어다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh my-app 'sudo su - deployuser -c "cat ~/.ssh/id_ed25519"' | pbcopy
</code></pre></div></div>

<p>복사한 비밀키를 <a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub이 제공하는 저장소 환경 secrets</a>에 저장하고 쓴다.</p>

<h1 id="links">links</h1>

<ul>
  <li><a href="https://ohyecloudy.com/pnotes/archives/github-actions-arm64-build-deploy/">GitHub Actions로 ARM64 플랫폼 빌드 및 배포 - ohyecloudy’s pnotes - ohyecloudy.com</a>(<a href="http://web.archive.org/web/20250801023819/https://ohyecloudy.com/pnotes/archives/github-actions-arm64-build-deploy/">archive</a>)</li>
  <li><a href="https://docs.github.com/ko/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions">GitHub Actions에서 비밀 사용 - GitHub Docs - docs.github.com</a>(<a href="http://web.archive.org/web/20250801023817/https://docs.github.com/ko/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets">archive</a>)</li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="uncategorized" /><category term="linux" /><category term="user" /><category term="group" /><summary type="html"><![CDATA[GitHub Actions로 ARM64 플랫폼 빌드 및 배포 용도의 Linux 유저를 만들자. GitHub Actions로 빌드한 결과물을 Linux 인스턴스로 복사한다. 빌드 결과물 저장할 디렉터리의 쓰기 권한이 필요하다. 딱 필요한 권한만 세팅한 Linux 유저를 만들자. 비밀키를 GitHub이 제공하는 저장소 환경 secrets에 저장해서 내가 관리하는 것보다 훨씬 더 안전하다고 해서 필요 이상의 막대한 권한을 가진 유저 비밀키를 동네방네 뿌리는 잘못된 습관을 만들지 말자.]]></summary></entry><entry><title type="html">MathJax로 LaTeX 수식을 블로그에서 그리자 - Jekyll Minimal Mistakes 테마에 적용</title><link href="https://ohyecloudy.com/pnotes/archives/jekyll-mathjax-latex/" rel="alternate" type="text/html" title="MathJax로 LaTeX 수식을 블로그에서 그리자 - Jekyll Minimal Mistakes 테마에 적용" /><published>2025-07-19T00:00:00+09:00</published><updated>2025-07-19T00:00:00+09:00</updated><id>https://ohyecloudy.com/pnotes/archives/jekyll-mathjax-latex</id><content type="html" xml:base="https://ohyecloudy.com/pnotes/archives/jekyll-mathjax-latex/"><![CDATA[<p>\begin{equation}
M = (Q, \Sigma, \Gamma, \delta, q_0, q_{\text{accept}}, q_{\text{reject}})
\end{equation}</p>

<p><a href="https://ohyecloudy.com/pnotes/archives/1704/">Modulo operation</a> 글을 쓰면서 <a href="https://www.latex-project.org/">LaTeX</a> 수식을 블로그 포스트에 쓰고 싶단 생각을 했다. 텍스트에서 수식을 자동으로 만들어내는 것이 아니라 다른 곳에서 수식을 예쁘게 그리고 이미지로 삽입하는 건 되도록 피하고 싶었다. 귀찮고 소모적인 작업이다.</p>

<p><a href="https://jekyllrb-ko.github.io/">Jekyll</a> 정적 사이트 생성기에서 LaTeX 수식을 렌더링하는 <a href="https://www.mathjax.org/">MathJax</a> JavaScript 라이브러리를 사용한다. <a href="https://github.com/mmistakes/minimal-mistakes">Minimal mistakes</a> 테마를 기준으로 적었지만 Jekyll을 사용하고 있다면 어렵지 않게 적용할 수 있다.</p>

<h1 id="블로그에-mathjax-라이브러리-추가">블로그에 <code class="highlighter-rouge">MathJax</code> 라이브러리 추가</h1>

<p><a href="https://www.mathjax.org/#gettingstarted">아래처럼 한 줄만 head에 추가</a>하면 된다. 허탈할 정도로 간단한다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">id=</span><span class="s">"MathJax-script"</span> <span class="na">async</span> <span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Minimal mistakes에서는 MathJax를 지원할까? 간단하게 추가할 수 있어서 <a href="https://github.com/mmistakes/minimal-mistakes/discussions/314">테마에서는 지원할 계획이 없어 보인다</a>. 어디에 추가하면 될까? 테마 소스를 변경하는 방법은 피하고 싶다.</p>

<blockquote>
  <ul>
    <li><code class="highlighter-rouge">_includes/head/custom.html</code> is included at the end of the &lt;head&gt; tag.</li>
    <li><code class="highlighter-rouge">_includes/footer/custom.html</code> is included at the beginning of the &lt;footer&gt; tag.</li>
  </ul>

  <p><a href="https://mmistakes.github.io/minimal-mistakes/docs/layouts/#custom-head-and-footer">Layouts - Minimal Mistakes - mmistakes.github.io</a></p>
</blockquote>

<p>커스터마이징할 수 있는 방법을 만들어놨다. <code class="highlighter-rouge">_includes/head/custom.html</code> 파일을 편집하면 된다. <a href="https://mmistakes.github.io/minimal-mistakes/docs/configuration/#site-scripts">혹은 head_scripts 에 추가해도 된다</a>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">head_scripts</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">https://code.jquery.com/jquery-3.2.1.min.js</span>
  <span class="pi">-</span> <span class="s">/assets/js/your-custom-head-script.js</span>
</code></pre></div></div>

<p>모든 글에 로드할 필요가 없는 라이브러리라서 <code class="highlighter-rouge">_includes/head/custom.html</code> 파일을 사용했다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- _includes/head/custom.html --&gt;</span>
<span class="nt">&lt;script </span><span class="na">id=</span><span class="s">"MathJax-script"</span> <span class="na">async</span> <span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>모든 글에서 MathJax를 쓰는 건 아니니 좀 더 최적화를 해보자</p>

<h1 id="pagemathjax-변수로-mathjax-라이브러리-로딩-여부-결정"><code class="highlighter-rouge">page.mathjax</code> 변수로 <code class="highlighter-rouge">MathJax</code> 라이브러리 로딩 여부 결정</h1>

<p>수학 블로그가 아니다. <code class="highlighter-rouge">MathJax</code> 라이브러리를 붙인 게 오버엔지니어링이란 생각이 들 정도로 수학 수식을 쓴 포스트는 극히 일부다. 그래서 페이지에 <code class="highlighter-rouge">mathjax</code> 변수를 추가해 이걸로 로딩 여부를 결정하게 했다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">defaults</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">scope</span><span class="pi">:</span>
      <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">posts</span>
      <span class="na">values</span><span class="pi">:</span>
        <span class="na">layout</span><span class="pi">:</span> <span class="s">single</span>
        <span class="na">mathjax</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">_config.yml</code> 파일에 <code class="highlighter-rouge">page.mathjax</code> 변수 디폴트 값에 <code class="highlighter-rouge">false</code> 를 할당했다. 다음으로 로딩 여부를 결정하는 코드를 추가한다.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c">&lt;!-- _includes/head/custom.html --&gt;</span>
{% if page.mathjax %}
<span class="nt">&lt;script </span><span class="na">id=</span><span class="s">"MathJax-script"</span> <span class="na">async</span> <span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
{% endif %}

</code></pre></div></div>

<p>이제 <code class="highlighter-rouge">page.mathjax</code> 값이 true일 때만 MathJax 라이브러리를 로드한다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">#math</span><span class="nv"> </span><span class="s">부호가</span><span class="nv"> </span><span class="s">다른</span><span class="nv"> </span><span class="s">두</span><span class="nv"> </span><span class="s">수를</span><span class="nv"> </span><span class="s">모듈로</span><span class="nv"> </span><span class="s">연산(modulo</span><span class="nv"> </span><span class="s">operation)할</span><span class="nv"> </span><span class="s">때,</span><span class="nv"> </span><span class="s">결괏값</span><span class="nv"> </span><span class="s">부호는?"</span>
<span class="na">mathjax</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">categories</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">...</span>
  <span class="na">tags</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">...</span>
    <span class="pi">-</span> <span class="s">...</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Jekyll 글마다 있는 <a href="https://jekyllrb.com/docs/front-matter/">Front matter(전문)</a>에 <code class="highlighter-rouge">mathjax: true</code> 를 추가하면 LaTeX 문법을 MathJax가 예쁘게 보여준다.</p>

<p>포스트에서는 어떻게 LaTeX 수식을 쓰면 되는가? <a href="https://docs.mathjax.org/en/latest/input/tex/delimiters.html">LaTeX 수식 구분자(delimiter)를 $ 문자 빼고는 그대로 사용할 수 있다</a>. <code class="highlighter-rouge">$$..$$</code>, <code class="highlighter-rouge">\(..\)</code>, <code class="highlighter-rouge">\begin{equation}...\end{equation}</code> 등을 사용할 수 있다.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\\(a = n \times q + r\\) 에서 \\(q\\) 를 어떻게 구하느냐에 따라 부호가 피제수 혹은 제수(divisor)를 따라가게 된다.
</code></pre></div></div>

<p>\(a = n \times q + r\) 에서 \(q\) 를 어떻게 구하느냐에 따라 부호가 피제수 혹은 제수(divisor)를 따라가게 된다.</p>

<p>이렇게 수식이 예쁘게 렌더링된다.</p>

<h1 id="마치며">마치며</h1>

<p>허탈할 정도로 쉽게 로드하고 사용할 수 있는 MathJax 라이브러리 덕분에 블로그 포스트에 LaTeX 수식을 쉽게 렌더링할 수 있다. LaTeX 외에 다른 문법도 지원하지만 호환성이 가장 좋고 오래 살아남을 LaTeX 수식을 사용하고 있다. 수학 수식을 많이 쓰지 않지만 가끔 LaTeX 문법으로 써서 예쁘게 렌더링 되는 걸 보면 흐뭇하다.</p>

<h1 id="링크">링크</h1>

<ul>
  <li><a href="https://ohyecloudy.com/pnotes/archives/1704/">#math 부호가 다른 두 수를 모듈로 연산(modulo operation)할 때, 결과 값 부호는? - ohyecloudy’s pnote…</a>(<a href="http://web.archive.org/web/20250719063729/https://ohyecloudy.com/pnotes/archives/1704/">archive</a>)</li>
  <li><a href="https://www.mathjax.org/#gettingstarted">MathJax - Beautiful math in all browsers. - mathjax.org</a>(<a href="http://web.archive.org/web/20250719063733/https://www.mathjax.org/">archive</a>)</li>
  <li><a href="https://mmistakes.github.io/minimal-mistakes/docs/layouts/#custom-head-and-footer">Layouts - Minimal Mistakes - mmistakes.github.io</a>(<a href="http://web.archive.org/web/20250719063728/https://mmistakes.github.io/minimal-mistakes/docs/layouts/">archive</a>)</li>
  <li><a href="https://www.latex-project.org/">LaTeX - A document preparation system - latex-project.org</a>(<a href="http://web.archive.org/web/20250719063845/https://www.latex-project.org/">archive</a>)</li>
  <li><a href="https://jekyllrb-ko.github.io/">Jekyll • 심플한, 블로그 지향적, 정적 사이트 - 평범한 텍스트 파일을 정적 웹사이트 또는 블로그로 변신시켜 보세요. - jekyl…</a>(<a href="http://web.archive.org/web/20250719063453/https://jekyllrb-ko.github.io/">archive</a>)</li>
  <li><a href="https://jekyllrb.com/docs/front-matter/">Front Matter - Jekyll • Simple, blog-aware, static sites - jekyllrb.com</a>(<a href="http://web.archive.org/web/20250719063453/https://jekyllrb.com/docs/front-matter/">archive</a>)</li>
  <li><a href="https://github.com/mmistakes/minimal-mistakes/discussions/314">mmistakes/minimal-mistakes/discussions/314 - github.com</a>(<a href="http://web.archive.org/web/20250719063453/https://github.com/mmistakes/minimal-mistakes/discussions/314">archive</a>)</li>
  <li><a href="https://github.com/mmistakes/minimal-mistakes">mmistakes/minimal-mistakes - github.com</a>(<a href="http://web.archive.org/web/20250719063453/https://github.com/mmistakes/minimal-mistakes">archive</a>)</li>
  <li><a href="https://docs.mathjax.org/en/latest/input/tex/delimiters.html">TeX and LaTeX math delimiters — MathJax 3.2 documentation - docs.mathjax.org</a>(<a href="http://web.archive.org/web/20250719063454/https://docs.mathjax.org/en/latest/input/tex/delimiters.html">archive</a>)</li>
  <li><a href="https://mmistakes.github.io/minimal-mistakes/docs/configuration/#site-scripts">Configuration - Minimal Mistakes - mmistakes.github.io</a>(<a href="http://web.archive.org/web/20250719063619/https://mmistakes.github.io/minimal-mistakes/docs/configuration/">archive</a>)</li>
</ul>

<!----- Footnotes ----->]]></content><author><name>Jongbin Oh</name><email>ohyecloudy@gmail.com</email></author><category term="blogging" /><category term="blogging" /><category term="jekyll" /><category term="latex" /><category term="mathjax" /><category term="minimal-mistakes" /><summary type="html"><![CDATA[\begin{equation} M = (Q, \Sigma, \Gamma, \delta, q_0, q_{\text{accept}}, q_{\text{reject}}) \end{equation}]]></summary></entry></feed>