<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://gratus907.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://gratus907.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-02-11T14:35:33+00:00</updated><id>https://gratus907.com/feed.xml</id><title type="html">Wonseok Shin</title><subtitle>Wonseok Shin, SNU CSE
</subtitle><entry><title type="html">마르코프 결정과정으로 문어 (알파카) 키우기</title><link href="https://gratus907.com/blog/maplestory-mdp/" rel="alternate" type="text/html" title="마르코프 결정과정으로 문어 (알파카) 키우기" /><published>2025-08-07T00:00:00+00:00</published><updated>2025-08-07T00:00:00+00:00</updated><id>https://gratus907.com/blog/maplestory-mdp</id><content type="html" xml:base="https://gratus907.com/blog/maplestory-mdp/"><![CDATA[<p>최근에 여름방학 챌린저스 서버가 열리면서 메이플스토리를 다시 플레이하고 있습니다. 지난 겨울에 하다가, 챌린저스 서버가 닫히면서 접었었는데 이번 이벤트는 뉴비로 시작해도 꽤 재밌게 플레이할만 한 것 같습니다.</p>

<p>지난 2월에 메이플스토리에는 “황금문어” 라는 이벤트가 추가되었고, 호응이 좋았는지 이번 여름에 “에버니아 무역왕” 이라고 이름을 바꾸어 재출시되었습니다. 
메이플스토리가 항상 그랬지만, 이 이벤트는 확률을 절묘하게 이용해서 떄로는 피가 거꾸로 솟게 만들고, 때로는 그 어느 이벤트보다 도파민 터지게 하는 경향이 있는 것 같습니다.</p>

<p>저로써는 이런 이벤트를 보면, 최적 전략을 생각해보지 않을 수 없습니다. 
특히, 이 게임은 강화학습의 바탕이 되기도 하는 마르코프 결정 과정 (Markov Decision Process) 의 굉장히 좋은 예제이기 때문에, 
이 포스팅에서는 문어 게임을 따라가면서 MDP에 대해서 알아보고자 합니다.</p>

<h3 id="게임의-규칙">게임의 규칙</h3>

<p>게임의 룰을 먼저 간략히 정리해 보면 아래와 같습니다. (문어와 알파카 이벤트는 본질적으로 똑같기 때문에, 아래에는 일반적인 단어들로 바꾸어 서술합니다)</p>

<ul>
  <li>게임은 총 100번의 “라운드” 로 구성되어 있습니다.</li>
  <li>플레이어는 1레벨에서 시작해서, 최대 9레벨까지 레벨을 올리는 것이 목표입니다.</li>
  <li>각 레벨에는 정의된 <strong>보상</strong> 값이 있습니다. 당연히, 레벨이 높을수록 많은 보상이 할당되어 있습니다.</li>
  <li>각 라운드에서, 플레이어는 다음 두 가지중 하나의 선택을 할 수 있습니다.
    <ul>
      <li><strong>레벨업 시도 (이하, “도전” 이라 부르겠습니다)</strong>: $l$ 레벨에서 레벨업을 시도합니다. <br />
이때, 가능한 경우의 수로는 성공, 실패, 조난 이 있어서,
        <ul>
          <li>성공 시에는 레벨이 1만큼 상승하고</li>
          <li>실패 시에는 레벨이 1만큼 하락하고 (단, 2레벨에서는 1레벨로 하락하지 않고, 2레벨을 유지합니다)</li>
          <li>조난 시에는 <strong>보상 없이</strong> 게임이 즉시 종료됩니다.</li>
        </ul>
      </li>
      <li><strong>보상 획득 (이하, “만족” 이라 부르겠습니다)</strong>: $l$ 레벨에 해당하는 보상을 얻고 게임을 종료합니다.</li>
    </ul>
  </li>
  <li>100라운드가 끝날때까지 만족을 택하지 않았다면, 100라운드가 종료되는 시점에서 만족한 것으로 판정합니다.</li>
</ul>

<p>실제 메이플스토리에는 다음의 두 가지 요소가 더 있으나, 분석에서 크게 중요한 요소는 아닙니다.</p>
<ul>
  <li>실제 게임에서는 $l$에서 낮은 확률로 성공 시에 $l+2$로 2레벨 상승합니다. 이 확률은 분석에서 일단 무시하겠습니다.</li>
  <li>위 서술은 마치 라운드 시도에 비용이 들지 않는 것처럼 쓰여 있지만, 실제 게임에서는 각 라운드를 시도할 때마다 300마리의 몬스터를 더 사냥해야 합니다. 
다만, 이 게임을 이렇게 최적전략을 생각하면서 플레이하시는 분들에게 100라운드를 도전하기 위한 3만 1천 마리의 레벨 범위 몬스터 사냥은 가능한 범주에 있기 때문에, 이것도 무시하겠습니다. 
그렇게 많은 몬스터를 사냥할수 없다면 100라운드에서 라운드를 줄여서 생각하면 됩니다.</li>
</ul>

<h3 id="마르코프-결정-과정">마르코프 결정 과정</h3>
<p>이 게임의 최적 전략을 분석하는 것이 자명하지 않은 이유는, 내 행동에 따라 상태가 <strong>확률적으로</strong> 변화하며, 그 상태의 가치는 <strong>내가 미래에 취할 전략에 따라 달라진다</strong>는 데 있습니다.</p>

<p>예를 들어, 이 게임을 무조건 100번 도전하는 식으로 플레이한다고 가정해 보겠습니다. 이때 최종 상태를 계산하는 것은 일반적인 <strong>마르코프 체인</strong> 으로 가능합니다. 상태들 간의 전이확률을 행렬 $P$ 로 나타내고 (시작 상태와 이미 조난당한 상태를 포함하여), 시작 상태의 벡터를 $\mathbf{x} = [1, 0, \dots, 0]$로 쓴 다음, $P^{100} \mathbf{x}$ 를 계산하면 마지막 순간 각 레벨에 있을 확률을 바로 알 수 있습니다. 이를 통해 기댓값도 쉽게 계산할 수 있을 것입니다.</p>

<p>그러나, 내가 위험을 감수할 수 없는 성격이라서 6레벨에서 반드시 만족하는 사람과, 9레벨까지 용맹정진하는 사람에게 있어서는 현재 상황 5레벨이 갖는 가치(보상에 대한 기대)가 다릅니다.</p>

<p>이러한 불확실성 하에서의 의사결정을 모델링하는 방법이 바로 마르코프 결정 과정 (Markov Decision Process) 입니다.</p>

<h3 id="정의">정의</h3>
<p>편의 상, 100번의 게임은 항상 끝까지 진행하되, 조난당한 상태에서는 무엇을 하든 빠져나올 수 없다고 생각하면 분석을 쉽게 할 수 있습니다. 
MDP는 다음과 같이 정의합니다.</p>

\[MDP = (\mathcal{S}, \mathcal{A}, P, R, \gamma)\]

<ul>
  <li>상태 $\mathcal{S}$ 는 이 시스템이 존재하는 모든 상태의 집합입니다. 여기서는 레벨 1부터 9까지 아홉개에, 조난당한 상태 (0레벨) 까지 총 10개가 있습니다.</li>
  <li>행동 $\mathcal{A}$ 는 우리가 취할 수 있는 행동의 집합입니다. 여기서는 도전과 만족이 있습니다.</li>
  <li>전이 함수 $P: \mathcal{S} \times \mathcal{A} \times \mathcal{S} \to [0, 1]$ 은 상태 $s$ 에서 행동 $a$ 를 취했을 때, 상태 $s’$ 로 전이할 확률 $P[s’ \mid s, a]$ 를 의미합니다.</li>
  <li>보상 함수 $R: \mathcal{S} \times \mathcal{A} \to \mathbb{R}$ 은 상태 $s$ 에서 행동 $a$ 를 취했을 때 획득하는 보상입니다. 우리는 여기서, <strong>획득하는 도파민의 양</strong> 같은 변수에 현혹되지 않고 엄격하게 게임이 제공하는 보상 (솔 에르다 조각 등) 을 기준으로 합니다. 도전은 보상값이 항상 0이고, 각 상태에서 만족이 주는 보상값만 정의할 수 있습니다.</li>
  <li>할인 상수 $\gamma \in [0, 1]$는 미래에 얻을 보상을 현재 보상으로 환산하기 위해 곱하는 상수값이나, 우리는 이 게임에서 이 상수를 1로 고정하고 무시할 것입니다.</li>
</ul>

<p>분석을 쉽게 하기 위해서는, $A$가 두개밖에 없으므로, $P_{도전}$ 과 $P_{만족}$ 이 각각 행렬이라고 생각하면 편합니다. 
또한, $R$ 함수는 도전할 때에는 항상 0이고, 만족할 때에만 정의되어 있다고 생각하면 됩니다. 그렇게 하면, <strong>만족</strong> 행동은 사실 어떤 보상을 받고 조난 상태 (0레벨) 로 이동하는 것과 동치임을 알 수 있습니다.</p>

<p>각 스텝 $t$ 에서의 상태를 $S_t$, 그때 취하는 행동을 $A_t$ 로 쓰기로 하겠습니다. 이 프로세스의 이름에 <strong>마르코프</strong> 가 들어가는 것은, 아래 성질을 만족하는 상태를 다루기 때문입니다.</p>
<div class="math-block math-block-red">
  <div class="title">정의: 마르코프 성질</div>
  <p>시스템의 미래가 현재 상태와 행동에만 의존하며, 과거에 지나온 경로에 의존하지 않을 때, 즉</p>

\[P[S_{t+1} = s' \mid S_t = s, A_t = a, S_{t-1}, A_{t-1} \dots] = P[S_{t+1} = s' \bar S_t = s, A_t = a]\]

  <p>을 만족할 때, 마르코프 성질을 만족한다고 부른다.</p>
</div>

<p>이는, 30번째 라운드에서 내가 4레벨인 상태에서 도전을 누른다면, 그 4레벨이 방금 29번째 라운드에서 5레벨에서 떨어져서 온건지, 3레벨에서 올라와서 온건지가 확률에 영향을 미치지 않아야 한다는 것입니다. <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<h3 id="정책과-가치">정책과 가치</h3>
<p>마르코프 결정 과정에서 우리가 최적화하는 대상은 <strong>정책</strong> (Policy) 이라고 부릅니다. Formal하게 쓰면</p>

\[\pi: \mathcal{S} \times \mathcal{A} \to [0, 1]\]

<p>정책 $\pi$ 는 현재 상태 $s$ 에서 어떤 행동 $a$ 를 취할 확률로 정의합니다. 다만, 우리는 이 게임을 확률적으로 플레이하고 싶지는 않으므로, 결정론적인 정책 ($\pi(s, *)$ 가 $(1, 0, 0, \dots)$ 와 같은 one-hot vector) 을 생각할 것입니다. 편의상, 앞으로 이 정책함수를 $\pi(a \mid s)$ 와 같이, $s$ 상태에서 각 행동 $a$ 의 조건부확률로 기술하겠습니다.</p>

<p>우리가 원하는 것은, $\pi$를 <strong>적절히</strong> 선택하여, 기대되는 보상을 최대화하는 것입니다. 그러기 위해서는 $\pi$ 가 게임에 미치는 영향을 정확히 이해할 필요가 있습니다.</p>

<p>정책 $\pi$ 가 정해져 있다면, 현재 상태 $s$ 에 대해, 다음 상태 $s’$ 의 확률은 다음과 같이 정해집니다.</p>

\[\begin{align*}
  P[S_{t+1} = s' \mid S_{t} = s]
  &amp;= \sum_{a \in \mathcal{A}} \textcolor{red}{P[S_{t+1} = s' \mid A_{t} = a, S_t = s]} \times \textcolor{blue}{P[A_t = a \mid S_t = s]} \\ 
  &amp;= \sum_{a \in \mathcal{A}} \textcolor{red}{P[s' \mid s, a]} \times \textcolor{blue}{\pi(a \mid s)}
\end{align*}\]

<p>또한, 우리가 상태와 행동에 따른 보상을 정의했으므로, 상태에 대한 보상도 비슷하게 정의할 수 있습니다.</p>

\[R(s) = \sum_{a \in \mathcal{A}} \pi(a \mid s) R(s, a)\]

<h3 id="벨만-방정식-무한-라운드">벨만 방정식: 무한 라운드</h3>
<p>잠시 편의상, 라운드가 100번이 아닌, 무한히 많이 할 수 있다고 가정해 보겠습니다.</p>

<p>무한히 많은 라운드가 있을 때, 우리가 궁금한 것은 “그래서 내가 지금 5레벨이면, 앞으로 얼마 정도 기대해 볼 수 있는가?” 일 것입니다. 무한히 많은 라운드를 앞으로 플레이한다면 이전에 무슨일이 있었는지는 별로 알 필요가 없고, 상태 $s$의 가치는 여기서 출발해서 정책 $\pi$ 를 따르면 앞으로 얼마의 보상을 기대할 수 있는지에 따라 정해질 것입니다.</p>

\[\begin{align*}
  V^{\pi}(s) &amp;= \mathbb{E}_\pi \left[\sum_{t = 0}^{\infty} R(S_t, A_t) \mid S_0 = s\right]
\end{align*}\]

<p>그런데, $t$ 상태에서의 $S_t$ 가 무엇일지는 $S_0$에서 출발해서 $t$번이나 행동을 거친 이후의 상태이므로 그 확률을 알기 어렵습니다. 
다만 이때는 라운드가 <strong>무한히 많기</strong> 때문에, $V$ 에 대해서 다음의 재귀적 관계가 성립합니다.</p>

\[\begin{align*}
  V^{\pi}(s) &amp;= \sum_{a \in \mathcal{A}} \pi(a \mid s) \textcolor{blue}{\left(R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] V^{\pi}(s')\right)}
\end{align*}\]

<p>즉, $s$에서의 가치는, 이 시점에서 어떤 행동 $a$를 취하는 데 있는데, 행동 $a$를 취하고 나면 소정의 보상 $R(a, s)$ 을 획득함과 동시에 상태가 바뀌며, 그때부터는 또 바뀐 상태 $V^{\pi}(s’)$ 의 보상값이 (얼마인지는 모르지만 정해져 있는) 기대값에 반영된다는 것입니다.</p>

<p>그런데, 저 기댓값 식에서 파란색으로 표시된 부분은 우리가 상태 $s$ 에서 $a$ 라는 행동을 취했을 때의 기대되는 가치라고 해석해 볼 수 있습니다. 그러니까, $s$라는 <strong>상태의 가치</strong>는 사실 그 상태에서 $a$ 라는 행동을 취할 확률로 그 상태-행동 조합이 갖는 가치를 가중합한, <strong>상태-행동-가치의 기댓값</strong> 인 것이죠. 즉,</p>

\[Q^{\pi}(s, a) = R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] V^{\pi}(s')\]

\[V^{\pi}(s) = \sum_{a \in \mathcal{A}} \pi(a \mid s) Q^{\pi}(s, a)\]

<p>라고 쓸 수 있고, 이 식을 <strong>벨만 방정식 (Bellman Equation)</strong> 이라고 부릅니다.</p>

<p>그러면 이제 우리가 원하는 것은 이 상태에서 최적의 가치를 뽑아낼 수 있는 전략 $\pi$를 찾고 싶고, 또 그때 최적의 가치를 얻는 전략 $\pi$ 를 찾고 싶습니다. 이 과정을 최적화하는 여러 알고리즘들이 알려져 있으나, 우리의 현재 관심사는 일단은 아닙니다. 
그래도 짧게 생각해보면, 어떤 정책 $\pi$를 하나 잡고, 그 상태에서 $V^{\pi}$ 를 일단 계산해보고, 
그렇게 계산된 $V$ 값을 이용해서 $\pi’(a \mid s)$를 최대한 잘 설정하는 $\pi’(a \mid s) = \argmax_a Q^{\pi}(s, a)$ 를 반복하면 뭔가 약간 될 것 같이 느껴집니다.</p>

<p>이 알고리즘을 Policy Iteration 이라고 부릅니다.</p>

<h4 id="벨만-최적-방정식">벨만 최적 방정식</h4>

<p>어떠한 경로로든, 최적의 전략 $\pi^\ast$ 를 이미 알고 있다고 가정해 보겠습니다. 그렇다면, 그 $\pi^\ast$ 하에서 $V$ 나 $Q$의 값은 상태마다 ($Q$의 경우 $(s, a)$ 순서쌍마다) 이제 하나로 정해지게 됩니다. 즉,</p>

\[V^{\ast}(s) = \sum_{a \in \mathcal{A}} \pi^\ast(a \mid s) Q^{\ast}(s, a)\]

<p>그런데 생각해보면, 이때 $\pi^\ast(a \mid s)$ 는 당연히 최대한 가치가 높은 $a$ 하나에 몰아주는것이 최적일 것입니다.</p>

<p>즉, 최적 상태에서는 다음이 만족되어야 합니다.</p>

\[V^{\ast}(s) = \max_{a \in \mathcal{A}} Q^{\ast}(s, a) = \max_{a \in \mathcal{A}} R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] V^{\ast}(s')\]

\[\begin{align*}
  Q^{\ast}(s, a) &amp;= R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] V^{\ast}(s') \\ 
  &amp;= R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] \max_{a' \in \mathcal{A}} Q^\ast(s', a')
\end{align*}\]

<p>이 식을 벨만 최적 방정식 (Bellman Optimality Equation) 이라고도 부릅니다. <sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<h3 id="유한-라운드">유한 라운드</h3>
<p>유한한 라운드에서는 생각을 달리해야 하는 요소들이 있습니다. 가령, 100번만 할 수 있는 문어 키우기를 처음 시작할 때는 용감하게 5레벨에서 6레벨로 나아가 볼 수 있겠지만, 97번째에서도 똑같이 누를 수 있을까요? 97번째에서는 “도전했다가는 4레벨로 떨어져서, 다시 올라오지 못할” 가능성을 고려히지 않을 수 없을 것입니다.</p>

<p>그렇기 때문에, 유한 시간 MDP는 조금 다르게 접근해야 합니다. 
우리는 $t$ 만큼 <strong>시간이 남아 있는</strong> 상황에서의 가치함수 $V_t^{\pi}$, 그때의 전략 $\pi_t$ 와 같은 것들을 생각할 것입니다. 여기서, $t$가 현재 시간이 아닌, 남은 시간임에 주의하여야 합니다.</p>

<p>일단 가치가 시간에 따라 변한다는 것을 인정하고 나면, 앞서 본 Bellman equation과 비슷하게 재귀적으로 가치를 산정해 볼 수 있습니다. 먼저, 시간에 따라 전략이 바뀌지 않는다면,</p>

\[\begin{align*}
  \textcolor{red}{V_{t}^{\pi}(s)} &amp;= \sum_{a \in \mathcal{A}} \pi(a \mid s) \left(R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] \textcolor{red}{V_{t-1}^{\pi}(s')}\right)
\end{align*}\]

<p>이렇게 쓸 수 있습니다. 그런데 이 문제의 경우 시간에 따라 당연히 전략이 바뀌어야 하므로, 
최적 전략을 찾기 위해 우리는 맨 뒤부터 거꾸로 접근합니다.</p>

<p>더이상 할 수 있는 행동이 없을 때 ($t = 0$) 의 최적전략은 자명합니다. 이걸 이용해서 위 벨만 최적방정식처럼 써보면,</p>

\[\begin{align*}
  \textcolor{red}{V_{t}^{\ast}(s)} &amp;= \max_{a \in \mathcal{A}} \left(R(a, s) + \sum_{s' \in \mathcal{S}} P[s' \mid s, a] \textcolor{red}{V_{t-1}^{\ast}(s')}\right)
\end{align*}\]

<p>이 식을 만족하는 $V$ 가 최적일 것입니다. 이렇게 최적의 $V$를 구했다면, $Q$를 구하는 것은 자명한 계산이며, $\pi_t^\ast$ 도 위 식을 최적화하는 $a$를 고르는 것이 항상 최적일 것입니다.</p>

<p>그렇다면, 이 문제는</p>
<ul>
  <li>경계값 ($t = 0$) 이 주어져 있고,</li>
  <li>모든 계산이 $t$를 하나씩 늘려가면서 이전 상태만 참고하여 할 수 있으므로</li>
</ul>

<p>알고리즘적으로 <strong>동적 계획법 (다이나믹 프로그래밍)</strong> 을 이용하여 풀 수 있습니다!<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> 
위 식을 $t = 0$에서 역방향으로 귀납적으로 적용하면, $O(T \abs{\mathcal{S}}^2 \abs{\mathcal{A}})$ 시간에 정확히 풀 수 있습니다.</p>

<h3 id="문어-키우기">문어 키우기</h3>
<p>다시 문어 (알파카) 키우기로 돌아와 보겠습니다. 편의상 조난상태를 레벨 0이라고 정의합니다. 
이하, 확률과 보상값은 <a href="https://maplestory.nexon.com/News/Event/Ongoing/1175">2025년 7월 에버니아 이벤트</a>를 기준으로 하겠습니다.<br />
1레벨부터 9레벨까지 각 레벨에서 게임이 종료되었을 때 획득하는 솔 에르다 조각은 다음과 같습니다. <sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<p><code class="language-plaintext highlighter-rouge">(0, 1, 3, 6, 10, 15, 25, 150, 500)</code></p>

<p>$t = 0$ 은 100라운드가 모두 끝난 상황을 의미합니다. 아무것도 할 수 없으므로, 
이 값이 곧 $V_0(1)$ 부터 $V_0(9)$ 까지가 됩니다. (조난당한 상태에는 보상이 없으므로 $V_0(0) = 0$까지 생각하면 됩니다)</p>

<p>이제, $t = 1$에서 (이는 99번째 라운드가 끝나고, 100번째 라운드를 시작하기 전을 의미합니다!) 6레벨에서 7레벨을 눌러야 할지 생각해 보겠습니다. 
만약 이때 <strong>만족</strong> 을 선택한다면, 안전하게 15의 보상을 받습니다. 이는 위 재귀식에서 $Q_1(6, 만족) = 15$ 임을 의미하며, 아래와 같이 계산합니다.</p>
<ul>
  <li>$R(6, 만족) = 15$ 이고,</li>
  <li>$P[0 \mid 6, 만족] = 1$ 이며 (만족 전략은 보상을 받고 조난당하는 것과 동치)</li>
  <li>$V_0(0) = 0$ 이기 때문에, $15 + 1 \times 0$ (그러면 더이상 보상을 기대할 수 없음)</li>
</ul>

<p>도전의 보상을 계산해 보겠습니다. 
$V_0(7) = 25$, $V_0(6) = 15$, $V_0(5) = 10$ 이며, 6레벨에서 올라갈 확률은 20.5%, 실패확률은 76.5%, 조난 확률은 3%입니다. 이는 즉,</p>
<ul>
  <li>$R(6, 도전) = 0$ 이고 (도전은 즉시 보상을 수령하지 않음)</li>
  <li>$P[0 \mid 6, 도전] = 0.03, P[5 \mid 6, 도전] = 0.765, P[7 \mid 6, 도전] = 0.205$ 입니다.</li>
  <li>따라서, 기대 보상값 $Q_{1}(6, 도전)$ 은 $25 * 0.205 + 10 * 0.765 + 0 * 0.03 = 12.775$ 입니다.</li>
</ul>

<p>즉, 기회가 한 번 남았는데 6레벨에서 7레벨을 도전해서는 안 된다 라는 것을 알 수 있습니다.</p>

<p>여기서 주목해야 할 점은, 이제 $t = 2$ 를 계산하는 데 있어서는 $V_1(6) = 15$ 를 취한다는 것입니다. 
이미 $t = 1$에서 6레벨에 도달한다면 만족하는 것이 옳음을 알기 때문에, 그 뒤의 최적전략은 항상 정해져 있습니다. 이제 “두번 남았는데 5레벨에서 눌러야 하나” 를 계산할 때는, 
“한번 남고 6레벨에 도달한다면 멈출 것이므로 그 가치가 15” 로 계산하면 된다는 것입니다. 이 부분이 단순 마르코프 체인 계산과 달리 (비록 여기서는 만족이 더 나은 전략이었지만), 
MDP에서는 “미래에 내가 적절히 잘 판단해서 누른다면” 어떻게 보상을 최적화하는지 알 수 있는 대목입니다.</p>

<h3 id="그래서-눌러요-말아요">그래서 눌러요, 말아요?</h3>
<p><strong>다음 포스팅에 계속 알아보겠습니다.</strong></p>

<hr />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>속칭 액땜이니, 제물이니 하는 것을 믿는다면 이 성질을 믿지 않는다는 뜻이므로, 이하 모든 논의는 아무런 의미가 없습니다. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>용어는 자료마다 조금씩 다른것 같습니다. 이 식을 벨만 방정식이라고 부르기도 합니다. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3">
      <p>이 용어도 Bellman 이 명명하였습니다. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4">
      <p>솔 에르다 기운이나 EXP 쿠폰도 똑같이 풀면 되고, 보상의 배율이 일치하기 때문에 어느 것으로 계산하든 상관없이 동일한 결과를 얻습니다 <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="mathematics" /><category term="mathematics" /><category term="probability theory" /><category term="korean" /><summary type="html"><![CDATA[최근에 여름방학 챌린저스 서버가 열리면서 메이플스토리를 다시 플레이하고 있습니다. 지난 겨울에 하다가, 챌린저스 서버가 닫히면서 접었었는데 이번 이벤트는 뉴비로 시작해도 꽤 재밌게 플레이할만 한 것 같습니다.]]></summary></entry><entry><title type="html">Approximate Counting: Morris Counter</title><link href="https://gratus907.com/blog/morris-counter/" rel="alternate" type="text/html" title="Approximate Counting: Morris Counter" /><published>2025-05-05T00:00:00+00:00</published><updated>2025-05-05T00:00:00+00:00</updated><id>https://gratus907.com/blog/morris-counter</id><content type="html" xml:base="https://gratus907.com/blog/morris-counter/"><![CDATA[<p>이 포스팅을 시작으로, 몇 번에 걸쳐 <strong>Approximate Counting</strong> 및 몇가지 관련된 알고리즘들을 공부해 보려고 합니다.</p>

<h3 id="문제">문제</h3>
<p>문제 자체는 정말 간단합니다.</p>
<div class="math-block math-block-blue">
  <div class="title">문제: Counting</div>
  <p>다음의 두 쿼리를 처리하고자 한다.</p>
  <ol>
    <li>카운터를 1만큼 업데이트한다.</li>
    <li>현재 카운터의 값 (지금까지 1번 쿼리가 입력된 횟수)를 출력한다.</li>
  </ol>
</div>
<p>이 문제는 당연히 카운터 변수 하나를 관리함으로써 자명하게 해결할 수 있고, 이때 $n$ 까지의 수를 헤아리기 위해서는 $O(\log n)$ 비트가 필요합니다.</p>

<p>오늘 다루고자 하는 문제는 여기서 나아가서, <strong>정확한 횟수 대신 근삿값을 출력하도록 허용함으로써, 위 문제를 더 작은 공간만으로 해결할 수 있는가?</strong> 입니다.</p>

<hr />

<h3 id="approximate-counting-morris-counter">Approximate Counting: Morris Counter</h3>
<p>알고리즘이 단순하기 때문에, 바로 먼저 알고리즘을 서술하고, 분석을 진행하겠습니다. 이 알고리즘은 Robert Morris가 1978년에 <a class="citation" href="#MorrisCounter">[1]</a> 제안하여, 보통 Morris Counter 라는 이름으로 불립니다.</p>
<div class="math-block math-block-red">
  <div class="title">알고리즘: Morris Counter</div>
  <p>변수 $X$를 관리하며, 쿼리를 다음과 같이 처리한다.</p>
  <ul>
    <li>1번 쿼리 (increment) 가 입력되면, 현재 변수 $X$의 값 $k$에 따라, $1/2^k$의 확률로 $X$의 값을 1만큼 증가시킨다.</li>
    <li>2번 쿼리가 입력되면, $2^X - 1$ 을 출력한다.</li>
  </ul>
</div>
<p>2번 쿼리의 답을 $2^X - 1$ 로 제출하겠다는 것은, $X$를 $\log_2(n + 1)$ 의 근사값으로 사용하겠다는 의미입니다. 
$n$을 저장하는 대신 $O(\log n)$ 크기의 변수를 하나 저장하고 있기 때문에, 필요한 공간의 기댓값은 $O(\log \log n)$ 가 됩니다.</p>

<p>업데이트가 확률적인 과정에 따라 이루어지기 때문에, $n$번의 업데이트가 이루어진 시점의 $X$의 값을 확률변수 $X_n$ 으로 쓸 수 있습니다. 
이제, 이 알고리즘을 분석하기 위해서는, randomized algorithm을 분석하는 일반적인 방법에 따라 다음 두 가지를 보여야 합니다.</p>
<ul>
  <li>Unbiased: 기댓값 $\E[X_n]$의 값이 정말 $\log_2(n + 1)$ 가 맞는가?</li>
  <li>Accuracy: 그떄의 분산, 또는 적절한 Concentration bound를 줄 수 있는가?</li>
</ul>

<hr />

<h3 id="analysis-unbiasedness">Analysis: Unbiasedness</h3>
<p>첫번째 명제 - Unbiased - 를 보이기 위해서는, $\E[2^{X_n}] = n + 1$ 을 확인하면 충분합니다. 수학적 귀납법에 따라 증명합니다. $n = 0$ 일 때는 $X_0$의 값이 0이므로 성립합니다.</p>

<p>$\E[2^{X_n}] = n + 1$ 이라고 할 때, $\E[2^{X_{n + 1}}]$ 을 분석하면, 기댓값의 정의에 의해</p>

\[\E[2^{X_{n + 1}}] = \sum_{k = 0}^{\infty} 2^{k} \cdot \P[X_{n + 1} = k]\]

<p>이고, 이때 $\P[X_{n + 1} = k]$ 는 $X_n$의 값에 따라 확률적으로 다음과 같이 계산됩니다.</p>

\[\P[X_{n + 1} = k] = \P[X_{n} = k-1] \cdot \frac{1}{2^{k-1}} + \P[X_{n} = k] \cdot \left(1 - \frac{1}{2^k}\right)\]

<p>앞 항은 $X_n$ 이 $k-1$이었는데 1번에서 확률 $1 / 2^{k-1}$ 에 의해 업데이트에 성공한 경우를, 뒤 항은 $X_n$이 원래 $k$였고 업데이트가 실패한 경우를 의미합니다. 
이제, 이 값을 대입하고 적절히 정리하여,</p>

\[\begin{align*}
\E[2^{X_{n + 1}}] &amp;= \sum_{k = 0}^{\infty} 2^{k} \left(\P[X_{n} = k-1] \cdot \frac{1}{2^{k-1}} +  \P[X_{n} = k] \cdot \left(1 - \frac{1}{2^k}\right)\right)\\ 
&amp;=  \sum_{k = 0}^{\infty} 2 \cdot \P[X_{n} = k-1] + 2^k \cdot \P[X_{n} = k]  - \P[X_{n} = k]\\ 
&amp;= \E[2^{X_{n}}] + 1
\end{align*}\]

<p>이와 같이, $\E[2^{X_{n + 1}}] = \E[2^{X_n}] + 1$ 이므로, $\E[2^{X_n}] = n + 1$ 이 성립합니다.</p>

<hr />

<h3 id="analysis-concentration-bound">Analysis: Concentration Bound</h3>
<p>Variance를 보이는 것은 위와 거의 비슷한 (조금 더 귀찮은) 계산으로 충분합니다.</p>
<div class="math-block math-block-red">
  <div class="title">정리: Morris Counter - Variance</div>
  <p>위 알고리즘에서, $\Var[2^{X_n} - 1] \leq n^2 / 2$ 이 성립한다.</p>
</div>
<p>Variance가 $\E[X]^2$ 와 같은 order로 잡힌다면, Chebyshev’s inequality를 이용하여 적절한 tail bound도 줄 수 있습니다.</p>

<p>상수 -1은 분산에 영향을 주지 않으므로, 위 분산을 계산하기 위해서는, $\E[(2^{X_n})^2]$ 을 계산하는 것으로 충분합니다. 위 계산과 같은 방법으로,</p>

\[\begin{align*}
\E[2^{2X_{n + 1}}] &amp;= \sum_{k = 0}^{\infty} 2^{2k} \cdot \P[X_{n + 1} = k]\\
&amp;= 2^{2k} \left(\P[X_{n} = k-1] \cdot \frac{1}{2^{k-1}} +  \P[X_{n} = k] \cdot \left(1 - \frac{1}{2^k}\right)\right)\\ 
&amp;= \sum_{k = 0}^{\infty} 4 \cdot 2^{k - 1} \P[X_{n} = k-1] + 2^{2k} \cdot \P[X_{n} = k]  - 2^k \P[X_{n} = k] \\ 
&amp;= 4 \E[2^{X_n}] + \E[2^{2X_{n}}] - \E[2^{X_n}]\\ 
&amp;= \E[2^{2X_{n}}] + 3(n + 1)
\end{align*}\]

<p>$\E[2^{2X_{0}}] = 1$ 이므로, $\E[2^{2X_{n}}] = 3n(n + 1) / 2 + 1$ 가 됩니다. 따라서,</p>

\[\Var[2^{X_n} - 1] = \frac{3n(n + 1)}{2} + 1 - (n + 1)^2 = \frac{(n^2 - n)}{2} \leq n^2 / 2\]

<p>임을 알 수 있습니다.</p>

<p>이제, Chebyshev’s inequality로부터,</p>

\[\P[|2^{X_n} - 1 - n| &gt; \epsilon] &lt; \frac{\Var[2^{X_n}]}{\epsilon^2} \leq \frac{n^2}{2\epsilon^2}\]

<p>이므로, $\epsilon = k \sqrt{n}$ 을 취하면, 오차가 $k \sqrt{n}$ 이상 발생할 확률은 $1 / 2k^2$ 이하임을 확인할 수 있습니다.</p>

<hr />

<h3 id="extensions-mean--median-trick">Extensions: Mean / Median trick</h3>
<p><strong>이 문단의 내용들은 다양한 확률적 알고리즘들에 동일하게 적용이 가능하므로, [별도 포스팅(작성중)]에서 더 자세히 다루고 있습니다.</strong></p>

<p>Morris counter는 가장 간단한 형태의 approximate counting 알고리즘들이기 때문에, 관련하여 다양한 확장들이 있습니다.</p>

<p>우리는 보통 확률적 알고리즘들을 분석할 때에, 작은 상수 $\epsilon, \delta$를 설정해 두고, 
$\P[\text{error} &gt; \epsilon] &lt; \delta$ 로 만들기 위한 최소한의 실행횟수 (시간) / 메모리에 관심을 갖습니다.</p>

<ul>
  <li>단순히 여러 개를 parallel하게 작동시켜 그 평균을 사용하는 경우, $1 / (\epsilon^2 \delta)$ 개의 estimator들을 사용함으로써, 
$O\left(\frac{\log\log{n}}{\epsilon^2\delta}\right)$ 공간 복잡도로 위 bound에 부합하는 알고리즘을 얻습니다.</li>
  <li>별도 포스팅에서 다룰 Median Trick을 이용하여 알고리즘을 개선할 수 있고, 그 경우 
$O\left(\frac{\log\log{n}}{\epsilon^2} \cdot \log (1 / \delta)\right)$ 공간 복잡도로 위 bound에 부합하는 알고리즘을 얻습니다.</li>
</ul>

<hr />

<h3 id="extensions-parametrized-version">Extensions: Parametrized Version</h3>
<p>위 알고리즘의 내용에서, $2^{X_n}$ 을 사용하는 대신, 임의의 수 $(1 + a)$ ($a &gt; 0$) 로도 비슷하게 생각할 수 있습니다.</p>

<p>이경우 $((1 + a)^{X_n} - 1) / a$ 를 estimation으로 사용하고, 업데이트도 $X_n = k$ 일 때는 $1 / (1 + a)^{k}$ 확률로 1을 더해 주는 식으로 해주면 됩니다. 
분석도 거의 똑같이 할 수 있어서, 위 계산에서 필요한 부분을 (조금씩 더 귀찮아지지만) 바꾸어 계산해보면 expectation은 똑같이 성립하며 variance는 $\frac{an(n - 1)}{2} \leq an^2 / 2$ 가 됨을 알 수 있습니다. 
따라서, $a = 1 / (\epsilon^2 \delta)$ 를 잡아주면, variance가 바로 $\frac{n^2}{2\epsilon^2 \delta}$ 로 줄어들기 때문에 Chebyshev 부등식만 적용해도 바로 $(\epsilon, \delta)$ - estimation이 성립하게 됩니다.</p>

<p>이렇게 얻어지는 estimator는 위에 논의한, randomized algorithm들 대개에 적용되는 일반적인 방법보다 훨씬 더 효과적입니다. 
우리가 사용하는 expected space는 $\log X_n$ bit이며,
위 세팅에서 이는 대략 (asymptotic하게 계산하고 있으므로, $X_n \approx \log_{1 + a}(an)$ 으로 생각하여)</p>

\[\begin{align*}
  \log X_n \approx \log_2 \log_{1 + a} (an) &amp;= \log \log (an) - \log \log (1 + a) \\ 
  &amp; = \log \log (an) + \log 1 / (\log (1 + a))\\
  &amp;\leq \log \log (an) + \log (1 / a)
\end{align*}\]

<p>이와 같이 유도됩니다 (마지막 부등식에서는 $\log (1 + a) \leq a$ 임을 사용하였습니다). 따라서, Space complexity는</p>

\[\log \log (n / \epsilon^2 \delta) + \log (1 / \epsilon^2\delta) = O(\log \log n + \log (1 / \epsilon) + \log(1 / \delta))\]

<p>임을 얻게 됩니다.</p>

<hr />]]></content><author><name></name></author><category term="computer-science" /><category term="algorithms" /><category term="randomized algorithms" /><category term="streaming algorithms" /><category term="korean" /><summary type="html"><![CDATA[이 포스팅을 시작으로, 몇 번에 걸쳐 Approximate Counting 및 몇가지 관련된 알고리즘들을 공부해 보려고 합니다.]]></summary></entry><entry><title type="html">Cloudflare Pages로 블로그 이전하기</title><link href="https://gratus907.com/blog/migrating-to-cloudflare-pages/" rel="alternate" type="text/html" title="Cloudflare Pages로 블로그 이전하기" /><published>2025-03-03T00:00:00+00:00</published><updated>2025-03-03T00:00:00+00:00</updated><id>https://gratus907.com/blog/migrating-to-cloudflare-pages</id><content type="html" xml:base="https://gratus907.com/blog/migrating-to-cloudflare-pages/"><![CDATA[<p>예전부터 뭔가 공부했던 내용을 글로 정리해서 쓰다보면 좀더 체계적으로 정리할수도 있고, 대강 알고 있었던 부분들을 좀더 정확하게 찾아보게 되는 좋은 효과들이 있다고 생각해서 
블로그에 공부했던 내용들, 특히 한참 Competitive programming을 하던 때에는 대회 / 문제풀이 관련해서 정리하는 글을 많이 작성해 왔습니다.</p>

<p>기존에는 Github Pages 에 개인 블로그 (웹사이트?) 를 호스팅해서 사용하고 있었는데, 이번에 Cloudflare Pages로 넘어오면서 기존의 글들을 모두 정리하고, 새롭게 시작해 보기로 마음먹게 되었습니다. 
새로운 블로그의 첫 포스팅으로는 이 “이사” 작업을 하기로 생각하게 된 계기와, 
Github pages라는 흔한(?) 방법 대신 Cloudflare pages를 선택하게 된 이유에 대해서 공유해 보려고 합니다.</p>

<h3 id="jekyll">Jekyll</h3>
<p>Jekyll은 Ruby로 작성된 툴로, markdown을 이용해 작성한 포스트를 HTML 형태의 웹페이지로 바꾸어 주는 역할을 합니다. 원하는 만큼 직접 HTML을 손댈 수 있으면서도, 글 쓰는 것 자체는 markdown으로 편하게 할 수 있습니다.</p>

<p>무엇보다 큰 강점은 Github Pages라는 강력한 호스팅 솔루션이 native하게 지원하는 방법이라는 점입니다. 누구나 무료로 <code class="language-plaintext highlighter-rouge">github.io</code> 형태의 웹페이지를 만들 수 있고, 비교적 간편해서 기술적인 내용을 다루는 블로그에 널리 사용되고 있습니다.</p>

<p>이런 일을 하려고 할 때 우리가 생각할 수 있는 자연스러운 솔루션(?) 은 대략 아래 네 가지가 있습니다.</p>

<ol>
  <li>네이버 블로그</li>
  <li>티스토리</li>
  <li>Jekyll + Github Pages</li>
  <li>아예 처음부터 웹사이트 구축</li>
</ol>

<p>이 네 가지는 1 &lt; 2 &lt; 3 &lt; 4 순으로 세팅할 것이 많은 대신, 더 많은 기능과 customizability를 제공합니다 (사실 이 두가지가 서로 상반되는 것은 당연한 일입니다)</p>

<p>Jekyll을 선택한 배경에는 몇가지 이유가 있습니다. 아래 기준들은 모두 제 개인적인 것이기는 합니다만, 비슷한 취향이신 분들께는 도움이 될것 같습니다.</p>

<ul>
  <li>수식 작성: 수식은 반드시 <strong>LaTeX 문법으로</strong> 작성 가능해야 하며, <strong>이미지</strong> 형태로 들어가서는 안 됩니다. 즉, MathJax나 KaTeX를 사용 가능해야 합니다.
    <ul>
      <li>네이버 블로그는 이 부분에서 약간 불편함이 있습니다. 네이버 자체 수식 편집기를 사용하거나 외부에서 렌더링해서 이미지로 가져와야 하는데, 이미지로 가져오는 방법은 당연히 아름답지 못하고, 자체 수식 편집기가 렌더링하는 결과물의 폰트도 LaTeX의 아름다운 수식 폰트와 틀립니다.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> 결정적으로 인라인 수식을 사용할 수 없어서, 작은 수식도 모두 display 형태로 들어가야 합니다.</li>
      <li>티스토리나 Jekyll 모두 적절히 설정하면 이부분은 충분히 가능합니다.</li>
    </ul>
  </li>
  <li>코드 작성: 코드 하이라이트가 가능해야 하며, 최대한 configurable해야 합니다.
    <ul>
      <li>네이버 블로그를 제외한 나머지 세 솔루션은 이부분을 적당히 잘 지원합니다.</li>
    </ul>
  </li>
  <li>포스트 정리: 태그 (tag) 등을 이용하여 포스트를 체계적으로 정리할 수 있어야 하며, 가능하다면 트리 형태로 정리할 수 있는 것이 바람직하다고 생각합니다. 정확히는 
<code class="language-plaintext highlighter-rouge">Collection: dict[str, Collection] | list[Collection] | post</code> 정도로 정의되어 있으면 행복한것 같습니다.
    <ul>
      <li>네이버 블로그는 이게 굉장히 잘 되어 있습니다. 이외에는 적절한 설정을 잡아주면 잘 되는것으로 알고 있습니다.</li>
    </ul>
  </li>
  <li>검색: 구글, 네이버 등에서 검색 가능했으면 좋겠고, 특히 구글 검색엔진이 잘 작동했으면 좋겠습니다.
    <ul>
      <li>네이버/티스토리로는 이것이 비교적 쉽게 가능하고, Jekyll이나 웹사이트 구축 시에는 약간의 작업을 해야 합니다.</li>
    </ul>
  </li>
  <li>확장성: 차후에 웹 상에서 제가 뭔가를 하고싶을때, <strong>기술적으로 불가능</strong> 한 것을 최소화하고 싶었습니다. 가령 <code class="language-plaintext highlighter-rouge">/[some-sub-website]</code> 에 제가 새로운 웹사이트같은걸 추가로 구축한다거나…
    <ul>
      <li>이런 것을 하려면 4번이 ideal하고, 최소한 3번을 써야 합니다. 외부 플랫폼에 의존적이라면 그 플랫폼이 허락한것만 사용할 수 있습니다.</li>
    </ul>
  </li>
</ul>

<p>이런 요소들을 고려해 볼 때, Jekyll은 상당히 좋은 솔루션입니다. 물론 모든 부분이 만족스럽지는 않지만, 큰 불만 없이 사용할만 한것 같습니다.</p>

<h3 id="al-folio-theme">Al-folio Theme</h3>
<p>제가 컴퓨터공학 학부를 졸업하면서 가장 힘들었던 과목은 <strong>웹 서비스 개발</strong> 을 실제로 해보는 과목이었습니다. 
체감상 저는 이 과목이 바로 그 전 학기에 들었던 <strong>(위상수학 + 복소해석 + 현대대수 + 알고리즘 + 양자컴퓨팅 + 데이터베이스) 여섯 과목의 합보다 더 힘들었다</strong>고 기억합니다.</p>

<p>전체적으로 개발할 양이 많았던것도 사실이지만 결정적으로 <strong>프론트엔드</strong> 가 제게 너무 고통스러웠습니다. 
Web browser에 올라가있는 웹사이트는 어떤 알수 없는 이유로 생각대로 동작하지 않고, 분명 표준적인 프레임워크를 사용하고 있는데도 어딘가 반쯤 나사가 풀려있는 컴포넌트들과, 분명 종이와 펜으로 쓸때는 명확했던 동작이 올려보면 미묘하게 어긋나 있고, 결정적으로 다 만들고 나면 제가 만든건 뭐가되었든 쓰기 불편하고 기분이 나쁩니다. 
돌이켜서 왜 그런지를 생각해보니, 근본적으로는 UI/UX와 디자인 같은 human한 요소들에 제가 별로 재능이 없다는 생각이 듭니다.</p>

<p>그래서 Jekyll을 쓰면서도 최대한 모든것이 잘 갖추어진 포맷을 가져오고 싶었고, 최대한 디자인에 제 개입을 줄이기로 했습니다. 그래서 찾은 것이 <a href="https://github.com/alshedivat/al-folio">Al-folio</a> 입니다. Al-folio는 적당히 아카데믹한 느낌의 웹페이지를 잘 만들어 주고, 제가 보기에는 디자인이 괜찮아 보였습니다<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<p>Al-folio는 제가 꼭 쓰고 싶었던 포스트 내 인용 (Jekyll-scholar 기반), 프로젝트 등에 기반한 적당히 깔끔한 포스트 정리, (제가 보이게) 적당히 예쁜 시작 페이지 등은 물론이고, 한번도 써보지 않을것같은 수많은 소셜 네트워크들과의 연동, disqus / giscus 기반의 댓글, related posts, 여러 외부 API 지원 등이 굉장히 잘 되어 있습니다. 비슷한 느낌의 블로그를 계획하신다면 적극 추천드립니다.</p>

<p>한가지 단점은 너무 많은 기능들이 들어있어서 무겁고, 무엇이 어디에 있는지 알기 어렵다는 점입니다. 후자는 쓰다보면 익숙해지고, 전자는 어차피 블로그는 한번 빌드해두면 며칠이상은 그대로 돌아가며, 빌드는 내 머신이 아니라 호스팅 쪽에서 해줄것이니까 별로 상관이 없습니다. 지금은 10여 초 정도가 걸리는데, 나중에 블로그가 엄청나게 커지거나 추가 기능을 쓰게된다면 그때 다시 고민해 보겠습니다.</p>

<h3 id="cloudflare-pages">Cloudflare Pages</h3>
<p>Jekyll에 기반한 웹사이트는 Github Pages를 이용, .github.io 에 무료로 호스팅할 수 있습니다.</p>

<p>일단 무료라는 점에서 엄청난 어드밴티지가 있고, github 에서 운영하다보니 다른 세팅을 아무것도 안 해도 git push 하면 바로 빌드가 된다는 편리함이 있지만, 그럼에도 불구하고 제가 다른 솔루션을 찾아 떠난 이유는 크게 두 가지입니다.</p>

<ul>
  <li>
    <p>확장성: Github Pages는 static website만을 지원하기 때문에, 백엔드 API가 필요한 일을 하려면 외부 솔루션을 사용해야 합니다. Cloudflare Pages도 이것은 마찬가지지만, Cloudflare에서는 KV storage, cloudflare worker 등의 솔루션도 같이 제공하기 때문에, 확장 시에 약간 더 편하게 작업할 수 있습니다.</p>
  </li>
  <li>
    <p>Jekyll 사용과정의 불편함: Github pages는 github action을 이용하기 떄문에, 빌드 프로세스가 아무래도 덜 customizable합니다. 이것때문에 Jekyll에서도 어떤 패키지를 사용하게 되면 대신에 github의 자동 빌드를 쓰지 못하게 되는 것들이 있는데, 대표적으로 Jekyll-Scholar (citation) 이 그렇습니다.</p>
  </li>
  <li>
    <p>Analytics: Github Pages도 외부에서 analytics를 달아줄수는 있는데 (구글 등), 뭔가 매우 불편했습니다. 특히 Google analytics가 저는 매우 불편하던데, 이부분은 아마 취향 차이일것 같습니다.</p>
  </li>
</ul>

<p>이외에도 속도와 보안 등의 요소들이 있다고 하는데, 제가 작업할 내용의 특성상 그런게 별로 필요하지는 않습니다. 
크게 위 내용들 때문에 Cloudflare Pages를 선택하게 되었다고 보면 될 것 같습니다. 
특히 google analytics가 잘 작동하지 않는다는 점이 크리티컬했고, 도대체 왜인지 모르겠지만 google search console에서 아무리 다시 등록을 해봐도 sitemap을 잘 읽지 못한다거나… 하는 부분들이 가장 컸던 것 같습니다.</p>

<p>그래서 마치 이사를 하면서 과감히 물건을 버리듯이, 포스트들도 정리하고, 새롭게 시작하기로 결심했습니다.</p>

<p>사실 기술적으로는 기존 블로그를 그대로 유지하면서 Cloudflare Pages에 올리는 것도 가능했겠지만, 
그렇게 하면 아마 영원히 정리도 하지 않았을 거고, 제가 이것저것 건드려놓은 (그러면서 망가진) HTML/CSS도 영원히 수정할 수 없었을것 같아서이기도 합니다.</p>

<h3 id="타조-디버깅법">타조 디버깅법</h3>
<p>여전히 해결하지 못하고 대충 덮어놓은 기술적인 문제들이 몇가지 있습니다. 
당연히 처음에는 심플하게 al-folio로 시작하고 빌드 커맨드로 <code class="language-plaintext highlighter-rouge">bundle install &amp;&amp; jekyll build</code> 해 보았는데,</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Liquid Exception: invalid byte sequence in US-ASCII
</code></pre></div></div>

<p>이 에러와 몇시간정도 싸웠던것 같습니다. 당연히 US-ASCII가 아닌 바이트시퀀스가 있겠죠. 한국어를 썼으니까요. 저는 분명히 <strong>UTF-8</strong> 로 인코딩되어있기를 기대하고 있었습니다.</p>

<p>Liquid에 대해 잘 모르기때문에, 제가 컴퓨터공학을 전공했더라도 이런 상황에서 할수있는건 구글과 GPT-4o, GPT-4.5, GPT-o3-mini-high를 오가며 분노-타협-애원하는 수밖에 없습니다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export LANG=en_US.UTF-8 &amp;&amp; export LC_ALL=en_US.UTF-8 &amp;&amp; bundle install &amp;&amp; jekyll build
</code></pre></div></div>
<p>이렇게 하라고 했는데, 전혀 안 되더군요. <code class="language-plaintext highlighter-rouge">iconv</code>를 이용해서 글 전체를 UTF-8로 바꾸라느니 (이미 UTF-8인데요),</p>

<p>결국 반복적으로 GPT 모델들을 바꿔가며 분노한 끝에 (ㅋㅋ)
<code class="language-plaintext highlighter-rouge">LANG</code> 과 <code class="language-plaintext highlighter-rouge">LC_ALL</code> 을 <code class="language-plaintext highlighter-rouge">C.UTF-8</code> 로 바꾸고 나니 빌드가 되었습니다. <code class="language-plaintext highlighter-rouge">export</code> 로 하면 안 되고, cloudflare 빌드 세팅에서 직접 variable로 설정해 줬을 때는 되었는데, 이 두개 사이에 어떤 차이가 있는지 잘 모르겠습니다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle install &amp;&amp; jekyll build
</code></pre></div></div>

<p>그 외에도 <strong>분명히 안 쓰는 것 같은데</strong> 지우면 뭔가가 작동을 안하는 <code class="language-plaintext highlighter-rouge">.js</code> 파일이라던가, 
저는 도저히 어떤 원리로 작동하는건지 알수도 없고 그닥 알고싶지도 않은 al-folio 테마 구석구석 숨어있는 <code class="language-plaintext highlighter-rouge">liquid</code> 흑마법 등… 
사실 언젠가 이 문제들이 반드시 발목을 잡을것만 같지만, 
<strong>일단 웹사이트가 웹에 올라간다면 만족하기</strong>로 했습니다.</p>

<p>마치 머리를 풀숲에 숨기고 사냥당하지 않기를 기대하는 타조 같은 자세가 아닐 수 없습니다.</p>

<hr />

<p>앞으로도 블로그 운영하면서 기존에 하던대로 공부했던 내용 관련 정리, 기술적인 글들 (여러 디버깅 이슈라던가…), 
개인적인 이야기들 여러가지를 기록해보려고 합니다.</p>

<p>특히 기술적인 글들에 대해서는, 혹시 더 좋은 방법을 아시는분은 댓글 달아주시면 감사하겠습니다 ㅎㅎ;;</p>

<hr />
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>“다름”과 “틀림”의 국어적 의미에 대해서는 알고 있으며, 제 개인적 호오를 담은 의도적인 사용입니다. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>지금까지 살아오면서, 제 디자인적 심미안이 전체 평균과 어긋나있음이 여러 차례 드러났는데, 만약 그렇다면 이게 좋은게 맞는지 잘 모르겠습니다. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="personal" /><category term="blog" /><category term="personal" /><category term="tech" /><category term="korean" /><summary type="html"><![CDATA[예전부터 뭔가 공부했던 내용을 글로 정리해서 쓰다보면 좀더 체계적으로 정리할수도 있고, 대강 알고 있었던 부분들을 좀더 정확하게 찾아보게 되는 좋은 효과들이 있다고 생각해서 블로그에 공부했던 내용들, 특히 한참 Competitive programming을 하던 때에는 대회 / 문제풀이 관련해서 정리하는 글을 많이 작성해 왔습니다.]]></summary></entry></feed>