<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DevOps on kenji.blog</title><link>http://kenji.blog/ko/categories/devops/</link><description>Recent content in DevOps on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ko</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 09:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ko/categories/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>【Git 명령】rebase와 merge의 차이와 실무에서의 올바른 사용법</title><link>http://kenji.blog/ko/p/git-rebase-vs-merge-practical-guide/</link><pubDate>Sun, 13 Sep 2026 09:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/git-rebase-vs-merge-practical-guide/</guid><description>&lt;img src="http://kenji.blog/p/git-rebase-vs-merge-practical-guide/img/eyecatch.jpg" alt="Featured image of post 【Git 명령】rebase와 merge의 차이와 실무에서의 올바른 사용법" />&lt;h1 id="1-들어가며왜-merge인가-rebase인가는-영원한-숙제인가">1. 들어가며：왜 &amp;ldquo;merge인가 rebase인가&amp;quot;는 영원한 숙제인가
&lt;/h1>&lt;p>Git은 현대 소프트웨어 개발에 있어서 필수적인 버전 관리 시스템입니다. 여러 개발자가 동시에 코드베이스를 변경할 때, Git의 강력한 브랜치 모델이 위력을 발휘합니다. 하지만, 팀 개발에서 &amp;ldquo;&lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code> 중 어느 것을 사용해야 하는가&amp;quot;라는 논쟁은 초보자부터 숙련자까지 항상 개발자를 괴롭히는 주제 중 하나입니다.&lt;/p>
&lt;p>이 글에서는 Git의 내부 구조인 DAG(방향 비순환 그래프)와 커밋 해시의 수학적 성질을 통해, &lt;code>git merge&lt;/code>와 &lt;code>git rebase&lt;/code>의 메커니즘 차이를 깊이 파헤칩니다. 그리고 실무에서 이것들을 어떻게 구분해서 사용해야 하는지, 구체적인 워크플로우와 함께 철저하게 해설합니다. 단순한 명령어 소개에 그치지 않고, 이면에서 Git이 어떤 계산을 하고 있는지까지 이해함으로써, 충돌(Conflict)에 대한 두려움을 없애고 깔끔하고 추적 가능한 이력을 구축할 수 있게 됩니다.&lt;/p>
&lt;hr>
&lt;h1 id="2-git의-내부-구조커밋-해시와-객체-모델">2. Git의 내부 구조：커밋 해시와 객체 모델
&lt;/h1>&lt;p>Git이 이력을 어떻게 통합하는지 이해하기 위해서는 먼저 Git이 데이터를 어떻게 저장하고 있는지 알아야 합니다. Git은 단순한 파일 변경의 차이(패치)를 저장하는 것이 아니라, 특정 시점의 파일 시스템 전체의 스냅샷을 저장하고 있습니다.&lt;/p>
&lt;h2 id="21-커밋-해시의-암호학적-성질">2.1 커밋 해시의 암호학적 성질
&lt;/h2>&lt;p>Git의 각 커밋은 그 내용을 바탕으로 계산된 SHA-1(Secure Hash Algorithm 1) 해시 함수에 의한 40자리 16진수로 고유하게 식별됩니다. 커밋 객체는 다음 요소들로 구성됩니다:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Tree 객체에 대한 포인터&lt;/strong>: 그 시점의 디렉토리 구조와 파일(Blob)의 스냅샷&lt;/li>
&lt;li>&lt;strong>부모 커밋에 대한 포인터&lt;/strong>: 1개 이상의 부모 커밋의 해시값 (첫 커밋은 부모를 가지지 않으며, 병합 커밋은 2개 이상의 부모를 가집니다)&lt;/li>
&lt;li>&lt;strong>작성자 정보(Author)&lt;/strong>: 코드를 작성한 사람과 일시&lt;/li>
&lt;li>&lt;strong>커미터 정보(Committer)&lt;/strong>: 커밋을 생성하고 적용한 사람과 일시&lt;/li>
&lt;li>&lt;strong>커밋 메시지&lt;/strong>: 변경의 의도를 설명하는 텍스트&lt;/li>
&lt;/ol>
&lt;p>수학적으로 표현하면, 커밋 객체 $C$ 에 대한 해시값 $H(C)$ 는 다음과 같이 정의됩니다：&lt;/p>
$$
H(C) = \text{SHA-1}( \text{tree} \parallel \text{parent} \parallel \text{author} \parallel \text{committer} \parallel \text{message} )
$$&lt;p>여기서 $\parallel$ 은 데이터의 결합을 나타냅니다. 해시 함수의 특성에 따라, 커밋 메시지를 1글자만 바꾸거나 부모 커밋이 다르기만 해도 전혀 다른 해시값이 생성됩니다. 즉, &lt;strong>커밋은 불변(Immutable)&lt;/strong> 입니다. 후술할 &lt;code>rebase&lt;/code>가 &amp;ldquo;이력을 조작한다&amp;quot;고 표현되는 것은 실제로는 &amp;ldquo;내용은 비슷하지만 다른 해시값을 가지는 새로운 커밋을 생성하고 있기&amp;rdquo; 때문입니다.&lt;/p>
&lt;p>해시 공간의 크기는 $2^{160}$ 이며, 충돌(다른 커밋이 같은 해시값을 가지는 것)이 발생할 확률 $P$ 는 생일 문제(Birthday Paradox)의 이론을 사용하면 다음과 같이 근사할 수 있습니다 ($n$ 은 커밋 수)：&lt;/p>
$$
P(\text{collision}) \approx 1 - \exp\left(-\frac{n^2}{2 \times 2^{160}}\right)
$$&lt;p>이 확률은 극히 낮아 실용상 Git의 커밋 해시가 충돌하는 일은 거의 있을 수 없습니다.&lt;/p>
&lt;hr>
&lt;h1 id="3-그래프-이론과-daggit-이력의-수학적-모델">3. 그래프 이론과 DAG：Git 이력의 수학적 모델
&lt;/h1>&lt;p>Git의 커밋 이력은 그래프 이론의 &amp;ldquo;방향 비순환 그래프(Directed Acyclic Graph, DAG)&amp;ldquo;로 모델링됩니다.&lt;/p>
&lt;h2 id="31-dag방향-비순환-그래프란">3.1 DAG(방향 비순환 그래프)란
&lt;/h2>&lt;p>그래프 $G = (V, E)$ 에서, $V$ 는 커밋의 집합(정점), $E$ 는 커밋 간의 부모-자식 관계를 나타내는 방향 간선(Directed Edge)의 집합입니다. Git에서는 간선의 방향이 &amp;ldquo;자식 커밋에서 부모 커밋&amp;quot;으로 향합니다. 새로운 커밋은 과거의 커밋에 대한 포인터를 가지고 있기 때문입니다.&lt;/p>
&lt;pre class="mermaid">
graph BT
A[&amp;#34;Commit A (초기)&amp;#34;]
B[&amp;#34;Commit B&amp;#34;]
C[&amp;#34;Commit C (Main)&amp;#34;]
D[&amp;#34;Commit D (Feature)&amp;#34;]
E[&amp;#34;Commit E (Merge)&amp;#34;]
B --&amp;gt; A
C --&amp;gt; B
D --&amp;gt; B
E --&amp;gt; C
E --&amp;gt; D
&lt;/pre>
&lt;p>DAG의 가장 큰 특징은 &amp;ldquo;사이클(순환)이 존재하지 않는다&amp;quot;는 것입니다. 이로 인해 커밋 이력을 거슬러 올라가는 알고리즘은 무한 루프에 빠지지 않고 확실하게 종단(초기 커밋)에 도달할 수 있습니다.&lt;/p>
&lt;h2 id="32-위상-정렬topological-sort과-이력의-순서">3.2 위상 정렬(Topological Sort)과 이력의 순서
&lt;/h2>&lt;p>Git의 &lt;code>git log&lt;/code> 명령어 등으로 이력을 표시할 때, DAG는 위상 정렬(Topological Sort) 알고리즘에 의해 1차원의 리스트로 순서가 매겨집니다. DAG에서의 임의의 방향 간선 $u \to v$ ($u$ 는 $v$ 의 자식)에 대해, 리스트 내에서 $u$ 가 $v$ 보다 앞에 오도록 정렬합니다.&lt;/p>
&lt;hr>
&lt;h1 id="4-git-merge의-메커니즘과-종류">4. git merge의 메커니즘과 종류
&lt;/h1>&lt;p>브랜치의 변경을 통합하는 가장 기본적인 명령어가 &lt;code>git merge&lt;/code>입니다. 하지만 현재 상태에 따라 Git은 다른 병합 전략을 자동으로 선택합니다.&lt;/p>
&lt;h2 id="41-fast-forward-병합---ff">4.1 Fast-Forward 병합 (&amp;ndash;ff)
&lt;/h2>&lt;p>통합 대상(예: &lt;code>main&lt;/code>)의 브랜치가 통합할(예: &lt;code>feature&lt;/code>) 브랜치의 직접적인 조상일 경우, Git은 &amp;ldquo;Fast-Forward(빨리 감기)&amp;rdquo; 병합을 실행합니다. 이는 새로운 커밋을 생성하지 않고 단순히 브랜치의 포인터를 앞으로 이동시키는 작업입니다.&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
merge feature
&lt;/pre>
&lt;p>Fast-Forward 병합은 이력을 일직선으로 유지하지만, &amp;ldquo;어떤 커밋들이 하나의 기능 개발(feature)로 묶여 있었는지&amp;quot;에 대한 컨텍스트가 손실되는 단점이 있습니다.&lt;/p>
&lt;h2 id="42-non-fast-forward-병합---no-ff">4.2 Non-Fast-Forward 병합 (&amp;ndash;no-ff)
&lt;/h2>&lt;p>&lt;code>git merge --no-ff&lt;/code>를 명시적으로 지정하면, Fast-Forward가 가능한 상황이더라도 반드시 새로운 &amp;ldquo;병합 커밋&amp;quot;을 생성합니다. 병합 커밋은 2개의 부모를 가지는 특별한 커밋입니다.&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
commit id: &amp;#34;Main 작업 1&amp;#34;
merge feature type: NORMAL
&lt;/pre>
&lt;p>이 방법의 이점은 기능 브랜치의 존재와 역사가 DAG 상에 명확하게 남는다는 것입니다. 문제가 발생했을 때 &lt;code>git revert -m 1 &amp;lt;병합 커밋의 해시&amp;gt;&lt;/code>를 실행하여 기능 전체를 한 번에 안전하게 취소(리버트)할 수 있습니다.&lt;/p>
&lt;h2 id="43-3방향-병합3-way-merge-알고리즘">4.3 3방향 병합(3-Way Merge) 알고리즘
&lt;/h2>&lt;p>통합 대상과 통합할 브랜치가 각각 독자적인 커밋을 가지고 있는 경우, Git은 3방향 병합을 실행합니다. 이 때 Git은 DAG를 탐색하여 두 브랜치의 &amp;ldquo;공통 조상(Lowest Common Ancestor, LCA)&amp;ldquo;을 찾아냅니다.&lt;/p>
&lt;p>LCA를 찾기 위한 알고리즘의 시간 복잡도 $T_{\text{LCA}}$ 는 정점 수 $|V|$ 와 간선 수 $|E|$ 에 대해 선형 시간에 실행 가능합니다：&lt;/p>
$$
T_{\text{LCA}} = \mathcal{O}(|V| + |E|)
$$&lt;p>Git은 &amp;ldquo;LCA의 상태&amp;rdquo;, &amp;ldquo;현재 브랜치의 상태&amp;rdquo;, &amp;ldquo;상대 브랜치의 상태&amp;rdquo; 3가지를 비교하여 변경 사항이 충돌하지 않으면 자동으로 병합 커밋을 생성합니다.&lt;/p>
&lt;hr>
&lt;h1 id="5-git-rebase의-메커니즘과-이력의-재구성">5. git rebase의 메커니즘과 이력의 재구성
&lt;/h1>&lt;p>&lt;code>git merge&lt;/code>가 이력을 &amp;ldquo;통합&amp;quot;하는 반면, &lt;code>git rebase&lt;/code>는 이력을 &amp;ldquo;재구성(다시 붙이기)&amp;ldquo;합니다.&lt;/p>
&lt;h2 id="51-rebase의-이면에-있는-동작">5.1 Rebase의 이면에 있는 동작
&lt;/h2>&lt;p>&lt;code>feature&lt;/code> 브랜치를 &lt;code>main&lt;/code> 브랜치로 리베이스(&lt;code>git rebase main&lt;/code>)할 때의 내부 동작은 다음과 같습니다：&lt;/p>
&lt;ol>
&lt;li>&lt;code>feature&lt;/code> 브랜치와 &lt;code>main&lt;/code> 브랜치의 공통 조상(LCA)을 찾는다.&lt;/li>
&lt;li>LCA부터 &lt;code>feature&lt;/code> 브랜치 끝까지의 커밋 차이를 임시 영역에 저장한다.&lt;/li>
&lt;li>&lt;code>feature&lt;/code> 브랜치의 포인터를 &lt;code>main&lt;/code> 브랜치의 끝으로 이동시킨다.&lt;/li>
&lt;li>저장해둔 차이를 새로운 베이스(&lt;code>main&lt;/code>의 끝) 위에 하나씩 순차적으로 적용(Cherry-Pick)하여 새로운 커밋을 생성한다.&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;Commit A&amp;#34;] --&amp;gt; B[&amp;#34;Commit B&amp;#34;]
B --&amp;gt; C[&amp;#34;Commit C (Main)&amp;#34;]
B --&amp;gt; D[&amp;#34;Commit D (이전 Feature)&amp;#34;]
D -.-&amp;gt; E[&amp;#34;Commit D&amp;#39; (새로운 Feature)&amp;#34;]
C --&amp;gt; E
style D stroke-dasharray: 5 5, fill: #f9f9f9, color: #999
&lt;/pre>
&lt;p>여기서 중요한 것은 리베이스로 인해 생성된 커밋 $D'$ 는 원래의 커밋 $D$ 와 &lt;strong>부모 커밋이 다르기 때문에 완전히 다른 해시값을 가진다&lt;/strong>는 것입니다 (앞서 설명한 해시 함수의 정의 $H(C)$ 참조).&lt;/p>
&lt;h2 id="52-인터랙티브-리베이스interactive-rebase">5.2 인터랙티브 리베이스(Interactive Rebase)
&lt;/h2>&lt;p>&lt;code>git rebase -i&lt;/code> (또는 &lt;code>--interactive&lt;/code>)를 사용하면 커밋 이력을 마음대로 조작할 수 있습니다. 이는 로컬 이력을 정리하기 위한 가장 강력한 도구입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;code>pick&lt;/code> : 커밋을 그대로 적용한다.&lt;/li>
&lt;li>&lt;code>reword&lt;/code> : 커밋 메시지만 수정한다.&lt;/li>
&lt;li>&lt;code>edit&lt;/code> : 커밋 내용을 수정하기 위해 일시 정지한다.&lt;/li>
&lt;li>&lt;code>squash&lt;/code> : 이 커밋을 이전 커밋에 합치고, 메시지도 결합한다.&lt;/li>
&lt;li>&lt;code>fixup&lt;/code> : &lt;code>squash&lt;/code>와 같지만, 이 커밋의 메시지는 파기한다.&lt;/li>
&lt;li>&lt;code>drop&lt;/code> : 커밋을 완전히 삭제한다.&lt;/li>
&lt;/ul>
&lt;p>수학적으로 보면, 어떤 브랜치에 $N$ 개의 커밋이 있을 경우 리베이스의 순서 변경으로 생성 가능한 선형 이력의 변형(순열) $P$ 는 다음과 같습니다.&lt;/p>
$$
P = N!
$$&lt;p>Git은 개발자에게 $N!$ 가지의 자유를 주어 이력을 논리적이고 아름다운 상태로 유지할 수 있게 해줍니다.&lt;/p>
&lt;hr>
&lt;h1 id="6-리베이스의-황금률the-golden-rule-of-rebase">6. 리베이스의 황금률(The Golden Rule of Rebase)
&lt;/h1>&lt;p>&lt;code>rebase&lt;/code>는 매우 강력하지만 하나의 절대적인 규칙이 존재합니다.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>&amp;ldquo;공개된 퍼블릭한 이력에 대해서는 절대 리베이스를 해서는 안 된다&amp;rdquo;&lt;/strong>
&lt;em>(Never rebase public history)&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;h2 id="61-왜-퍼블릭-이력을-리베이스하면-안-되는가">6.1 왜 퍼블릭 이력을 리베이스하면 안 되는가?
&lt;/h2>&lt;p>Git은 분산형입니다. 당신이 &lt;code>origin/main&lt;/code>에 푸시한 커밋은 다른 개발자의 로컬 저장소에도 클론(복제)되어 있습니다. 만약 당신이 이미 푸시한 커밋을 리베이스하여 이력을 조작하고 &lt;code>git push --force&lt;/code>로 강제 덮어쓰기를 한다면 무슨 일이 일어날까요?&lt;/p>
&lt;p>다른 개발자의 로컬에 있는 DAG와 리모트의 DAG가 근본적으로 분기되어 버립니다. 다른 개발자가 &lt;code>git pull&lt;/code>을 실행하면 Git은 다른 역사를 가진 커밋 뭉치를 억지로 병합하려고 시도하고, 대량의 충돌이나 중복 커밋(동일한 변경 내용이지만 해시가 다른 커밋)이 발생하여 저장소가 패닉 상태에 빠집니다.&lt;/p>
&lt;p>리베이스는 **&amp;ldquo;아직 아무와도 공유하지 않은 로컬 브랜치&amp;rdquo;**에 대해서만 수행하는 것이 철칙입니다.&lt;/p>
&lt;hr>
&lt;h1 id="7-충돌conflict-해결과-git-rebase---continue">7. 충돌(Conflict) 해결과 git rebase &amp;ndash;continue
&lt;/h1>&lt;p>여러 사람이 같은 파일의 같은 부분을 변경한 경우 충돌이 발생합니다. &lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code>에서는 충돌을 해결하는 과정이 다릅니다.&lt;/p>
&lt;h2 id="71-merge에서의-충돌-해결">7.1 Merge에서의 충돌 해결
&lt;/h2>&lt;p>&lt;code>git merge&lt;/code>의 경우 충돌 해결은 &lt;strong>한 번만&lt;/strong> 발생합니다. 최종적인 병합 커밋을 생성하기 직전에 모든 충돌 지점을 한 번에 수정합니다.&lt;/p>
&lt;h2 id="72-rebase에서의-충돌-해결">7.2 Rebase에서의 충돌 해결
&lt;/h2>&lt;p>&lt;code>git rebase&lt;/code>의 경우 커밋을 하나씩 다시 적용해 나가는 특성상 &lt;strong>각 커밋마다 충돌이 발생할 가능성&lt;/strong>이 있습니다.&lt;/p>
&lt;p>리베이스 중에 충돌이 발생하면 Git은 처리를 일시 정지합니다. 해결 흐름은 다음과 같습니다：&lt;/p>
&lt;ol>
&lt;li>에디터나 IDE(VS Code 등)를 열어 충돌 마커(&lt;code>&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code> 나 &lt;code>======&lt;/code>, &lt;code>&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code>)를 수동으로 수정한다.&lt;/li>
&lt;li>수정한 파일을 인덱스에 추가한다：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add &amp;lt;수정한 파일&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>커밋은 생성하지 않고 리베이스 처리를 재개한다：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>만약 리베이스 자체를 취소하고 원래 상태로 되돌리고 싶다면 다음 명령어를 실행합니다：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --abort
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>(※ 충돌 해결이 필요 없고 해당 커밋을 통째로 건너뛰고 싶을 때는 &lt;code>git rebase --skip&lt;/code>을 사용합니다)&lt;/p>
&lt;hr>
&lt;h1 id="8-실무에서의-올바른-사용법워크플로우-실천">8. 실무에서의 올바른 사용법(워크플로우 실천)
&lt;/h1>&lt;p>그렇다면 실제 개발 현장에서는 &lt;code>merge&lt;/code>와 &lt;code>rebase&lt;/code>를 어떻게 구분해서 사용해야 할까요? 여기서는 가장 표준적이고 안전한 접근 방식을 소개합니다.&lt;/p>
&lt;h2 id="81-시나리오-1-로컬-작업-이력-정리rebase-활용">8.1 【시나리오 1】 로컬 작업 이력 정리(Rebase 활용)
&lt;/h2>&lt;p>기능 브랜치에서 개발 중, 자잘한 커밋(&amp;ldquo;오타 수정&amp;rdquo;, &amp;ldquo;임시 저장&amp;rdquo; 등)이 많이 쌓였다고 가정해 봅시다. 풀 리퀘스트(PR)를 올리기 전에, 이것들을 의미 있는 단위로 정리하기 위해 인터랙티브 리베이스를 사용합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># feature 브랜치에 있는 상태에서 실행&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># (에디터가 열리면 squash나 fixup을 사용하여 이력을 정리한다)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이를 통해 리뷰어에게 의도가 잘 전달되는 아름다운 커밋 이력을 만들 수 있습니다.&lt;/p>
&lt;h2 id="82-시나리오-2-최신-main-브랜치-따라가기rebase-활용">8.2 【시나리오 2】 최신 main 브랜치 따라가기(Rebase 활용)
&lt;/h2>&lt;p>개발이 길어지고 &lt;code>main&lt;/code> 브랜치에 다른 사람들의 변경 사항이 계속 병합되는 경우, 자신의 &lt;code>feature&lt;/code> 브랜치가 구버전이 되어 버립니다. 이 경우 &lt;code>feature&lt;/code> 브랜치를 최신 &lt;code>main&lt;/code>으로 리베이스하여 변경 사항을 쫓아갑니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># main의 최신 정보를 가져옴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># feature 브랜치를 최신 main 위에 다시 이어 붙임&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase origin/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이로 인해 이력이 일직선이 되어 나중에 병합할 때의 충돌을 방지할 수 있습니다. 또한, 불필요한 병합 커밋(&amp;ldquo;Merge branch &amp;lsquo;main&amp;rsquo; into feature&amp;rdquo;)이 생성되는 것을 막을 수 있습니다.&lt;/p>
&lt;h2 id="83-시나리오-3-완성된-기능-통합merge-활용">8.3 【시나리오 3】 완성된 기능 통합(Merge 활용)
&lt;/h2>&lt;p>&lt;code>feature&lt;/code> 브랜치에서의 개발이 완료되고 마침내 &lt;code>main&lt;/code> 브랜치에 통합하는 단계입니다. 여기서는 **&lt;code>git merge --no-ff&lt;/code>**를 사용합니다 (GitHub 등의 Pull Request에서 &amp;ldquo;Create a merge commit&amp;quot;을 선택하는 것과 동일합니다).&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge --no-ff feature -m &lt;span class="s2">&amp;#34;Merge feature: 사용자 로그인 기능 구현&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이로 인해 &lt;code>main&lt;/code> 브랜치의 DAG 상에 &amp;ldquo;여기서 하나의 기능이 병합되었다&amp;quot;는 역사의 결절점(병합 커밋)이 남습니다. 나중에 이력을 볼 때 기능 단위로 코드를 추적하기 쉬워집니다.&lt;/p>
&lt;hr>
&lt;h1 id="9-결론요약">9. 결론(요약)
&lt;/h1>&lt;p>Git을 조작할 때 &amp;ldquo;모든 것을 Merge로 해결한다&amp;quot;거나 &amp;ldquo;모든 것을 Rebase로 일직선으로 만든다&amp;quot;와 같은 극단적인 접근 방식은 각각 장단점이 있습니다.&lt;/p>
&lt;p>실무에서의 모범 사례는 **&amp;ldquo;로컬의 프라이빗한 이력은 rebase로 아름답게 정리하고, 퍼블릭한 통합 이력은 merge &amp;ndash;no-ff로 문맥을 남긴다&amp;rdquo;**는 하이브리드 접근 방식입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>로컬(개인의 작업 공간)&lt;/strong>: &lt;code>rebase&lt;/code>를 사용하여 불필요한 커밋을 배제하고, 최신 메인 라인을 따라가며 일직선의 이력을 유지한다.&lt;/li>
&lt;li>&lt;strong>글로벌(공유 작업 공간)&lt;/strong>: &lt;code>merge --no-ff&lt;/code>를 사용하여 기능 브랜치의 존재를 병합 커밋으로 DAG에 기록하고, 리버트나 트래킹을 용이하게 한다.&lt;/li>
&lt;/ul>
&lt;p>DAG의 구조나 해시 함수의 원리 등 수학적, 아키텍처적 배경을 이해함으로써, Git 명령어는 단순한 암기에서 &amp;ldquo;의도를 가진 이력 설계&amp;quot;로 승화됩니다. 리베이스의 황금률을 지키면서 상황에 맞는 최적의 명령어를 선택하여, 팀 전체가 읽기 쉽고 유지 보수하기 편한 깔끔한 커밋 이력을 구축해 나갑시다.&lt;/p></description></item><item><title>GitHub Actions를 사용한 C++ 프로젝트의 CI/CD 파이프라인 구축</title><link>http://kenji.blog/ko/p/github-actions-cpp-cicd-pipeline/</link><pubDate>Sat, 12 Sep 2026 18:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/github-actions-cpp-cicd-pipeline/</guid><description>&lt;img src="http://kenji.blog/p/github-actions-cpp-cicd-pipeline/img/eyecatch.jpg" alt="Featured image of post GitHub Actions를 사용한 C++ 프로젝트의 CI/CD 파이프라인 구축" />&lt;h1 id="github-actions를-사용한-c-프로젝트의-cicd-파이프라인-구축-완전-가이드">GitHub Actions를 사용한 C++ 프로젝트의 CI/CD 파이프라인 구축: 완전 가이드
&lt;/h1>&lt;p>현대 소프트웨어 개발 패러다임에서 지속적 통합(Continuous Integration: CI)과 지속적 제공/배포(Continuous Delivery/Deployment: CD)는 애자일한 개발 프로세스와 고품질 소프트웨어를 유지하는 데 필수적인 요소입니다. 수많은 프로그래밍 언어가 존재하지만, C++에서의 CI/CD 파이프라인 구축은 다른 언어(예: Python, JavaScript, Go 등)에 비해 독특한 어려움과 복잡성을 수반합니다.&lt;/p>
&lt;p>본 문서에서는 GitHub Actions를 활용하여 C++ 프로젝트를 위한 견고하고 실용적인 CI/CD 파이프라인을 처음부터 구축하는 방법을 매우 상세하게 설명합니다. 크로스 플랫폼(Windows, Linux, macOS)에서의 매트릭스 빌드, CMake를 이용한 빌드 시스템 통합, CTest를 사용한 자동 테스트, 정적·동적 분석 자동화, 커버리지 측정, 그리고 GitHub Releases를 통한 컴파일된 바이너리의 자동 전달까지 모든 실전 기술을 망라합니다.&lt;/p>
&lt;h2 id="1-c-프로젝트에서-cicd의-의미와-특유의-과제">1. C++ 프로젝트에서 CI/CD의 의미와 특유의 과제
&lt;/h2>&lt;p>웹 애플리케이션이나 스크립트 언어를 사용한 개발에서는 단일 Docker 컨테이너에서의 테스트 및 빌드로 충분한 경우가 대부분입니다. 그러나 C++는 네이티브로 컴파일되는 언어이며, 실행 환경의 하드웨어 아키텍처나 운영 체제에 강하게 의존합니다.&lt;/p>
&lt;p>C++ 프로젝트에 CI/CD를 도입할 때 직면하는 주요 과제는 다음과 같습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>플랫폼의 다양성&lt;/strong>: Windows, Linux, macOS와 같이 다른 OS마다 API(Windows API, POSIX 등)가 다릅니다. 개발자의 로컬 환경(예: macOS)에서 동작하더라도 Linux나 Windows에서 컴파일 오류가 발생하는 일은 일상다반사입니다.&lt;/li>
&lt;li>&lt;strong>컴파일러의 차이&lt;/strong>: Microsoft Visual C++ (MSVC), GNU Compiler Collection (GCC), Clang과 같은 주요 컴파일러는 C++ 표준(C++17, C++20, C++23)의 구현 정도나 해석, 경고의 엄격함이 다릅니다.&lt;/li>
&lt;li>&lt;strong>빌드 시간&lt;/strong>: 대규모 C++ 프로젝트에서는 빌드에 수십 분에서 수 시간이 걸리는 일도 드물지 않습니다. CI 환경에서는 제한된 컴퓨팅 리소스로 효율적으로 빌드하기 위한 캐시 전략과 병렬화가 필요합니다.&lt;/li>
&lt;li>&lt;strong>의존성 관리&lt;/strong>: C++에는 npm이나 pip 같은 절대적인 표준 패키지 관리자가 존재하지 않습니다. vcpkg, Conan 또는 CMake의 &lt;code>FetchContent&lt;/code> 등을 사용하여 CI 환경에서 매번 올바르게 라이브러리를 해결해야 합니다.&lt;/li>
&lt;li>&lt;strong>메모리 관리와 미정의 동작&lt;/strong>: 포인터 조작이나 수동 메모리 관리가 수반되므로, 단순한 로직 테스트뿐만 아니라 메모리 누수나 미정의 동작(Undefined Behavior)의 감지도 자동화해야 합니다.&lt;/li>
&lt;/ol>
&lt;p>이러한 과제를 해결하기 위해서는 다양한 OS 가상 머신을 온디맨드로 프로비저닝할 수 있고, 복잡한 워크플로우를 코드로 정의(Configuration as Code)할 수 있는 GitHub Actions가 최적의 솔루션이 됩니다.&lt;/p>
&lt;h2 id="2-cicd-파이프라인의-아키텍처-개요">2. CI/CD 파이프라인의 아키텍처 개요
&lt;/h2>&lt;p>이제 구축할 CI/CD 파이프라인의 전체적인 모습을 시각화해 보겠습니다. 아래의 Mermaid 시퀀스 다이어그램은 코드 Push부터 배포까지의 워크플로우를 보여줍니다.&lt;/p>
&lt;pre class="mermaid">
sequenceDiagram
participant Dev as &amp;#34;개발자&amp;#34;
participant Repo as &amp;#34;GitHub 저장소&amp;#34;
participant Action as &amp;#34;GitHub Actions CI/CD&amp;#34;
participant Rel as &amp;#34;GitHub Releases&amp;#34;
Dev-&amp;gt;&amp;gt;Repo: &amp;#34;브랜치 Push / PR 열기&amp;#34;
Repo-&amp;gt;&amp;gt;Action: &amp;#34;CI 워크플로우 트리거&amp;#34;
activate Action
Action-&amp;gt;&amp;gt;Action: &amp;#34;린트 및 정적 분석 (Clang-Tidy)&amp;#34;
rect rgb(200, 220, 240)
note right of Action: &amp;#34;크로스 플랫폼 매트릭스 빌드&amp;#34;
Action-&amp;gt;&amp;gt;Action: &amp;#34;Ubuntu 빌드 (GCC/Clang)&amp;#34;
Action-&amp;gt;&amp;gt;Action: &amp;#34;Windows 빌드 (MSVC)&amp;#34;
Action-&amp;gt;&amp;gt;Action: &amp;#34;macOS 빌드 (Apple Clang)&amp;#34;
end
Action-&amp;gt;&amp;gt;Action: &amp;#34;CTest 실행 (ASAN/UBSAN 포함)&amp;#34;
Action-&amp;gt;&amp;gt;Action: &amp;#34;커버리지 리포트 생성&amp;#34;
alt &amp;#34;태그가 Push된 경우 (예: v1.0.0)&amp;#34;
Action-&amp;gt;&amp;gt;Action: &amp;#34;CPack으로 바이너리 패키징&amp;#34;
Action-&amp;gt;&amp;gt;Rel: &amp;#34;ZIP/Tarball을 Release에 업로드&amp;#34;
end
deactivate Action
Repo--&amp;gt;&amp;gt;Dev: &amp;#34;CI 상태 보고 (성공/실패)&amp;#34;
&lt;/pre>
&lt;p>이 아키텍처에서는 Pull Request 단계에서는 빠른 피드백(정적 분석 및 빌드·테스트)을 제공하고, 버전 태그가 부여된 시점에서 결과물의 패키징 및 배포를 수행합니다.&lt;/p>
&lt;h2 id="3-모던-cmake를-사용한-프로젝트-설정">3. 모던 CMake를 사용한 프로젝트 설정
&lt;/h2>&lt;p>훌륭한 CI 파이프라인의 기반이 되는 것은 견고한 빌드 시스템입니다. C++의 사실상 표준인 CMake를 사용합니다. 여기에서는 &amp;ldquo;모던 CMake&amp;quot;라고 불리는 타겟 지향적인 접근 방식을 채택합니다.&lt;/p>
&lt;p>프로젝트의 디렉토리 구조를 다음과 같이 가정합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">my_cpp_project/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── CMakeLists.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── src/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── main.cpp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── calculator.cpp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── calculator.h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── tests/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── CMakeLists.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── test_calculator.cpp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>루트의 &lt;code>CMakeLists.txt&lt;/code> 설정 예시입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cmake" data-lang="cmake">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cmake_minimum_required&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">VERSION&lt;/span> &lt;span class="s">3.20&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">project&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MyCppProject&lt;/span> &lt;span class="s">VERSION&lt;/span> &lt;span class="s">1.0.0&lt;/span> &lt;span class="s">LANGUAGES&lt;/span> &lt;span class="s">CXX&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># C++ 표준 설정
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CMAKE_CXX_STANDARD&lt;/span> &lt;span class="s">20&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CMAKE_CXX_STANDARD_REQUIRED&lt;/span> &lt;span class="s">ON&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CMAKE_CXX_EXTENSIONS&lt;/span> &lt;span class="s">OFF&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c"># 컴파일러 고유의 확장을 비활성화하여 이식성을 높임
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 컴파일러 경고 엄격화
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">set_project_warnings&lt;/span> &lt;span class="s">target_name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MSVC&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">target_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">${&lt;/span>&lt;span class="nv">target_name&lt;/span>&lt;span class="o">}&lt;/span> &lt;span class="s">PRIVATE&lt;/span> &lt;span class="s">/W4&lt;/span> &lt;span class="s">/WX&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">else&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">target_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">${&lt;/span>&lt;span class="nv">target_name&lt;/span>&lt;span class="o">}&lt;/span> &lt;span class="s">PRIVATE&lt;/span> &lt;span class="s">-Wall&lt;/span> &lt;span class="s">-Wextra&lt;/span> &lt;span class="s">-Wpedantic&lt;/span> &lt;span class="s">-Werror&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">endif&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">endfunction&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 라이브러리 타겟 생성
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">add_library&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CalculatorLib&lt;/span> &lt;span class="s">src/calculator.cpp&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">target_include_directories&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CalculatorLib&lt;/span> &lt;span class="s">PUBLIC&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">CMAKE_CURRENT_SOURCE_DIR&lt;/span>&lt;span class="o">}&lt;/span>&lt;span class="s">/src&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set_project_warnings&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CalculatorLib&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 실행 파일 타겟 생성
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">add_executable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MyApplication&lt;/span> &lt;span class="s">src/main.cpp&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">target_link_libraries&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MyApplication&lt;/span> &lt;span class="s">PRIVATE&lt;/span> &lt;span class="s">CalculatorLib&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set_project_warnings&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">MyApplication&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 테스트 활성화
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">enable_testing&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">add_subdirectory&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">tests&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 설치 규칙 정의 (CPack용)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">include&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">GNUInstallDirs&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">install&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">TARGETS&lt;/span> &lt;span class="s">MyApplication&lt;/span> &lt;span class="s">CalculatorLib&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">RUNTIME&lt;/span> &lt;span class="s">DESTINATION&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">CMAKE_INSTALL_BINDIR&lt;/span>&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">LIBRARY&lt;/span> &lt;span class="s">DESTINATION&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">CMAKE_INSTALL_LIBDIR&lt;/span>&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">ARCHIVE&lt;/span> &lt;span class="s">DESTINATION&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">CMAKE_INSTALL_LIBDIR&lt;/span>&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># CPack을 통한 패키징 설정
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CPACK_PROJECT_NAME&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">PROJECT_NAME&lt;/span>&lt;span class="o">}&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CPACK_PROJECT_VERSION&lt;/span> &lt;span class="o">${&lt;/span>&lt;span class="nv">PROJECT_VERSION&lt;/span>&lt;span class="o">}&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CPACK_GENERATOR&lt;/span> &lt;span class="s2">&amp;#34;ZIP;TGZ&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">WIN32&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CPACK_GENERATOR&lt;/span> &lt;span class="s2">&amp;#34;ZIP&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">endif&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">include&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">CPack&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;strong>중요한 포인트:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>CMAKE_CXX_EXTENSIONS OFF&lt;/code>: GNU 확장 등 비표준 기능에 대한 의존성을 방지하고 크로스 플랫폼성을 보장합니다.&lt;/li>
&lt;li>&lt;strong>경고 엄격화 (&lt;code>-Werror&lt;/code> / &lt;code>/WX&lt;/code>)&lt;/strong>: CI 환경에서 컴파일러 경고를 에러로 취급하여 코드 품질을 강제로 높게 유지합니다.&lt;/li>
&lt;li>&lt;strong>GNUInstallDirs&lt;/strong>: OS별 표준 설치 경로(&lt;code>/usr/local/bin&lt;/code>이나 &lt;code>C:\Program Files&lt;/code> 등)를 자동으로 해결합니다.&lt;/li>
&lt;/ul>
&lt;h2 id="4-github-actions의-기초와-매트릭스-전략">4. GitHub Actions의 기초와 매트릭스 전략
&lt;/h2>&lt;p>GitHub Actions는 &lt;code>.github/workflows/&lt;/code> 디렉토리 내의 YAML 파일로 구성됩니다.
C++ 프로젝트에서 가장 강력한 기능이 &amp;ldquo;매트릭스 전략(Matrix Strategy)&amp;ldquo;입니다. 이를 통해 OS와 컴파일러의 조합을 동적으로 생성하고 병렬로 실행할 수 있습니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;워크플로우 트리거&amp;#34;] --&amp;gt; B[&amp;#34;매트릭스 작업 평가&amp;#34;]
B --&amp;gt; C[&amp;#34;Ubuntu 22.04 (GCC 12)&amp;#34;]
B --&amp;gt; D[&amp;#34;Ubuntu 22.04 (Clang 15)&amp;#34;]
B --&amp;gt; E[&amp;#34;Windows Server 2022 (MSVC)&amp;#34;]
B --&amp;gt; F[&amp;#34;macOS 14 (Apple Clang)&amp;#34;]
&lt;/pre>
&lt;p>아래에 매트릭스 빌드의 기본이 되는 YAML의 작업 정의를 보여줍니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build &amp;amp; Test [${{ matrix.os }} - ${{ matrix.compiler }}]&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.os }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fail-fast&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 하나의 작업이 실패해도 다른 OS의 빌드를 계속함&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matrix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gcc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gcc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">g++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">windows-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">msvc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">macos-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apple-clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>fail-fast: false&lt;/code>는 매우 중요합니다. 예를 들어 Linux 특유의 API를 잘못 사용한 경우, Ubuntu의 빌드는 실패하지만 Windows의 빌드는 성공하는지 여부도 동시에 확인하고 싶기 때문입니다.&lt;/p>
&lt;h2 id="5-빌드-비용과-암달의-법칙을-이용한-병렬-처리-최적화">5. 빌드 비용과 암달의 법칙을 이용한 병렬 처리 최적화
&lt;/h2>&lt;p>클라우드 환경에서의 CI/CD는 시간과의 싸움이며, 빌드 시간은 그대로 개발자의 대기 시간 및 운영 비용과 직결됩니다.
여기서 빌드 시간 최적화에 대해 컴퓨터 과학의 &amp;ldquo;암달의 법칙(Amdahl&amp;rsquo;s Law)&amp;ldquo;을 사용하여 수학적으로 접근해 보겠습니다.&lt;/p>
&lt;p>암달의 법칙은 프로그램 중 병렬화 가능한 부분의 비율을 $P$라고 할 때, $N$개의 프로세서를 사용했을 때의 이론상 최대 속도 향상률 $S(N)$을 다음과 같이 정의합니다.&lt;/p>
$$ S(N) = \frac{1}{(1 - P) + \frac{P}{N}} $$&lt;p>C++ 빌드 프로세스에서 소스 코드의 각 번역 단위(Translation Unit: &lt;code>.cpp&lt;/code> 파일) 컴파일은 완전히 독립적이며 병렬화가 가능합니다. 반면, CMake 구성이나 최종 바이너리 링크 단계는 기본적으로 직렬 실행(병렬화 불가)이 됩니다.&lt;/p>
&lt;p>만약 프로젝트의 전체 빌드 시간 중 80%가 컴파일 단계($P = 0.8$), 20%가 직렬 단계($1 - P = 0.2$)라고 가정합시다.
GitHub Actions의 표준 러너(Linux)는 2코어(스레드)를 제공합니다. 따라서 $N = 2$인 경우:&lt;/p>
$$ S(2) = \frac{1}{0.2 + \frac{0.8}{2}} = \frac{1}{0.2 + 0.4} = \frac{1}{0.6} \approx 1.67 $$&lt;p>2코어를 사용하는 것만으로 약 1.67배의 속도 향상을 얻을 수 있습니다. 이를 실현하기 위해서는 CMake 빌드 명령어에 &lt;code>--parallel&lt;/code> 옵션을 지정하는 것이 필수적입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build Project&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmake --build build --config Release --parallel 2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>더 나아가 비용 계산도 고려합니다. GitHub Actions의 이용 비용 $C_{total}$은 작업의 실행 시간 $T_i$와 러너의 단가 $R_i$의 곱의 총합입니다.&lt;/p>
$$ C_{total} = \sum_{i=1}^{M} \left( T_i \times R_i \right) $$&lt;p>빌드 시간을 단축하는 것은 피드백 루프를 가속화할 뿐만 아니라, 프로젝트의 운영 비용(특히 비공개 저장소의 경우)을 직접적으로 절감하는 결과로 이어집니다. 더 빠른 속도를 원한다면 &lt;code>ccache&lt;/code>를 도입하여 컴파일 결과를 캐시하는 방법이 효과적입니다.&lt;/p>
&lt;h2 id="6-자동-테스트와-새니타이저sanitizers의-통합">6. 자동 테스트와 새니타이저(Sanitizers)의 통합
&lt;/h2>&lt;p>C++에서 버그를 미연에 방지하기 위해서는 단위 테스트 외에 메모리 누수나 미정의 동작을 실행 시에 감지하는 &amp;ldquo;새니타이저&amp;quot;의 도입을 강력히 권장합니다. Google이 개발한 AddressSanitizer (ASAN) 및 UndefinedBehaviorSanitizer (UBSAN)를 사용합니다.&lt;/p>
&lt;p>CMake에서 새니타이저를 활성화하는 옵션을 추가합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cmake" data-lang="cmake">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">option&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ENABLE_SANITIZERS&lt;/span> &lt;span class="s2">&amp;#34;Enable ASAN and UBSAN&amp;#34;&lt;/span> &lt;span class="s">OFF&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ENABLE_SANITIZERS&lt;/span> &lt;span class="s">AND&lt;/span> &lt;span class="s">CMAKE_CXX_COMPILER_ID&lt;/span> &lt;span class="s">MATCHES&lt;/span> &lt;span class="s2">&amp;#34;GNU|Clang&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">-fsanitize=address,undefined&lt;/span> &lt;span class="s">-fno-omit-frame-pointer&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_link_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">-fsanitize=address,undefined&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">endif&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>CI 파이프라인의 Ubuntu 작업에서 이 옵션을 활성화하여 테스트를 실행합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Configure CMake&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CC&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.c_compiler }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CXX&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.cpp_compiler }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cmake -B build
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -DCMAKE_BUILD_TYPE=Release
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -DENABLE_SANITIZERS=${{ matrix.os == &amp;#39;ubuntu-latest&amp;#39; &amp;amp;&amp;amp; &amp;#39;ON&amp;#39; || &amp;#39;OFF&amp;#39; }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Run CTest&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ASAN_OPTIONS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;detect_leaks=1:symbolize=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">UBSAN_OPTIONS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;print_stacktrace=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ctest --build-config Release --output-on-failure --parallel 2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>테스트 실행에는 &lt;code>ctest&lt;/code> 명령어를 사용합니다. &lt;code>--output-on-failure&lt;/code>를 지정하여 실패한 테스트의 자세한 로그만 CI 출력에 표시하여 로그가 비대해지는 것을 방지합니다.&lt;/p>
&lt;h2 id="7-커버리지코드-커버리지-측정">7. 커버리지(코드 커버리지) 측정
&lt;/h2>&lt;p>테스트가 얼마나 많은 코드를 커버하고 있는지 시각화하는 것은 품질 보증에 있어 중요합니다. Linux 환경(GCC)을 이용하여 &lt;code>gcov&lt;/code> 및 &lt;code>lcov&lt;/code>로 커버리지를 측정합니다.&lt;/p>
&lt;p>먼저 CMake에서 커버리지 측정용 컴파일 플래그를 설정합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cmake" data-lang="cmake">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">option&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ENABLE_COVERAGE&lt;/span> &lt;span class="s2">&amp;#34;Enable coverage reporting&amp;#34;&lt;/span> &lt;span class="s">OFF&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">if&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">ENABLE_COVERAGE&lt;/span> &lt;span class="s">AND&lt;/span> &lt;span class="s">CMAKE_CXX_COMPILER_ID&lt;/span> &lt;span class="s">STREQUAL&lt;/span> &lt;span class="s2">&amp;#34;GNU&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_compile_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">--coverage&lt;/span> &lt;span class="s">-O0&lt;/span> &lt;span class="s">-g&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="nb">add_link_options&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">--coverage&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="nb">endif&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>커버리지 측정용 독립적인 작업을 GitHub Actions에 정의합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">coverage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Test Coverage Analysis&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Install lcov&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y lcov&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Configure CMake for Coverage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build &amp;amp; Test&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cmake --build build --parallel 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cd build &amp;amp;&amp;amp; ctest --output-on-failure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Generate lcov Report&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lcov --capture --directory . --output-file coverage.info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lcov --remove coverage.info &amp;#39;/usr/*&amp;#39; &amp;#39;*_deps/*&amp;#39; &amp;#39;*tests/*&amp;#39; --output-file coverage.info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Upload Coverage to Codecov&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codecov/codecov-action@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build/coverage.info&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>lcov --remove&lt;/code> 명령어를 사용하여 시스템 헤더나 서드파티 라이브러리, 테스트 코드 자체를 커버리지 측정 대상에서 제외합니다. 이를 통해 프로젝트 고유의 소스 코드의 순수한 커버리지를 얻을 수 있습니다.&lt;/p>
&lt;h2 id="8-github-releases를-통한-바이너리-자동-배포-cd">8. GitHub Releases를 통한 바이너리 자동 배포 (CD)
&lt;/h2>&lt;p>CI/CD의 &amp;ldquo;CD&amp;rdquo; 부분을 구축합니다. 개발자가 Git에서 버전 태그(예: &lt;code>v1.2.0&lt;/code>)를 부여하여 푸시했을 때, 자동으로 각 OS용 실행 가능한 바이너리를 컴파일하고 ZIP이나 Tarball로 패키징하여 GitHub Releases에 업로드합니다.&lt;/p>
&lt;p>이 단계에서는 CMake에 포함된 패키징 도구 &lt;code>CPack&lt;/code>을 이용합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Package Application (CPack)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith(github.ref, &amp;#39;refs/tags/v&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cpack -C Release -V&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Upload Release Assets&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith(github.ref, &amp;#39;refs/tags/v&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">softprops/action-gh-release@v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.tar.gz
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.zip
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.exe&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">GITHUB_TOKEN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.GITHUB_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 설정을 통해 &lt;code>git tag v1.0.0&lt;/code>과 &lt;code>git push origin v1.0.0&lt;/code>을 실행하는 것만으로, Windows 사용자용으로는 ZIP 파일이, Linux/macOS 사용자용으로는 Tarball이 수동 개입 없이 자동으로 릴리스 페이지에 공개됩니다. 이것은 사용자에게 소프트웨어를 제공하는 데 있어 매우 강력한 기능입니다.&lt;/p>
&lt;h2 id="9-완전한-workflow-yaml-파일">9. 완전한 Workflow YAML 파일
&lt;/h2>&lt;p>지금까지 설명한 모든 요소를 통합한, 견고하고 실용적인 &lt;code>.github/workflows/main.yml&lt;/code>의 완전한 코드를 아래에 보여줍니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt"> 10
&lt;/span>&lt;span class="lnt"> 11
&lt;/span>&lt;span class="lnt"> 12
&lt;/span>&lt;span class="lnt"> 13
&lt;/span>&lt;span class="lnt"> 14
&lt;/span>&lt;span class="lnt"> 15
&lt;/span>&lt;span class="lnt"> 16
&lt;/span>&lt;span class="lnt"> 17
&lt;/span>&lt;span class="lnt"> 18
&lt;/span>&lt;span class="lnt"> 19
&lt;/span>&lt;span class="lnt"> 20
&lt;/span>&lt;span class="lnt"> 21
&lt;/span>&lt;span class="lnt"> 22
&lt;/span>&lt;span class="lnt"> 23
&lt;/span>&lt;span class="lnt"> 24
&lt;/span>&lt;span class="lnt"> 25
&lt;/span>&lt;span class="lnt"> 26
&lt;/span>&lt;span class="lnt"> 27
&lt;/span>&lt;span class="lnt"> 28
&lt;/span>&lt;span class="lnt"> 29
&lt;/span>&lt;span class="lnt"> 30
&lt;/span>&lt;span class="lnt"> 31
&lt;/span>&lt;span class="lnt"> 32
&lt;/span>&lt;span class="lnt"> 33
&lt;/span>&lt;span class="lnt"> 34
&lt;/span>&lt;span class="lnt"> 35
&lt;/span>&lt;span class="lnt"> 36
&lt;/span>&lt;span class="lnt"> 37
&lt;/span>&lt;span class="lnt"> 38
&lt;/span>&lt;span class="lnt"> 39
&lt;/span>&lt;span class="lnt"> 40
&lt;/span>&lt;span class="lnt"> 41
&lt;/span>&lt;span class="lnt"> 42
&lt;/span>&lt;span class="lnt"> 43
&lt;/span>&lt;span class="lnt"> 44
&lt;/span>&lt;span class="lnt"> 45
&lt;/span>&lt;span class="lnt"> 46
&lt;/span>&lt;span class="lnt"> 47
&lt;/span>&lt;span class="lnt"> 48
&lt;/span>&lt;span class="lnt"> 49
&lt;/span>&lt;span class="lnt"> 50
&lt;/span>&lt;span class="lnt"> 51
&lt;/span>&lt;span class="lnt"> 52
&lt;/span>&lt;span class="lnt"> 53
&lt;/span>&lt;span class="lnt"> 54
&lt;/span>&lt;span class="lnt"> 55
&lt;/span>&lt;span class="lnt"> 56
&lt;/span>&lt;span class="lnt"> 57
&lt;/span>&lt;span class="lnt"> 58
&lt;/span>&lt;span class="lnt"> 59
&lt;/span>&lt;span class="lnt"> 60
&lt;/span>&lt;span class="lnt"> 61
&lt;/span>&lt;span class="lnt"> 62
&lt;/span>&lt;span class="lnt"> 63
&lt;/span>&lt;span class="lnt"> 64
&lt;/span>&lt;span class="lnt"> 65
&lt;/span>&lt;span class="lnt"> 66
&lt;/span>&lt;span class="lnt"> 67
&lt;/span>&lt;span class="lnt"> 68
&lt;/span>&lt;span class="lnt"> 69
&lt;/span>&lt;span class="lnt"> 70
&lt;/span>&lt;span class="lnt"> 71
&lt;/span>&lt;span class="lnt"> 72
&lt;/span>&lt;span class="lnt"> 73
&lt;/span>&lt;span class="lnt"> 74
&lt;/span>&lt;span class="lnt"> 75
&lt;/span>&lt;span class="lnt"> 76
&lt;/span>&lt;span class="lnt"> 77
&lt;/span>&lt;span class="lnt"> 78
&lt;/span>&lt;span class="lnt"> 79
&lt;/span>&lt;span class="lnt"> 80
&lt;/span>&lt;span class="lnt"> 81
&lt;/span>&lt;span class="lnt"> 82
&lt;/span>&lt;span class="lnt"> 83
&lt;/span>&lt;span class="lnt"> 84
&lt;/span>&lt;span class="lnt"> 85
&lt;/span>&lt;span class="lnt"> 86
&lt;/span>&lt;span class="lnt"> 87
&lt;/span>&lt;span class="lnt"> 88
&lt;/span>&lt;span class="lnt"> 89
&lt;/span>&lt;span class="lnt"> 90
&lt;/span>&lt;span class="lnt"> 91
&lt;/span>&lt;span class="lnt"> 92
&lt;/span>&lt;span class="lnt"> 93
&lt;/span>&lt;span class="lnt"> 94
&lt;/span>&lt;span class="lnt"> 95
&lt;/span>&lt;span class="lnt"> 96
&lt;/span>&lt;span class="lnt"> 97
&lt;/span>&lt;span class="lnt"> 98
&lt;/span>&lt;span class="lnt"> 99
&lt;/span>&lt;span class="lnt">100
&lt;/span>&lt;span class="lnt">101
&lt;/span>&lt;span class="lnt">102
&lt;/span>&lt;span class="lnt">103
&lt;/span>&lt;span class="lnt">104
&lt;/span>&lt;span class="lnt">105
&lt;/span>&lt;span class="lnt">106
&lt;/span>&lt;span class="lnt">107
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;C++ CI/CD Pipeline&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;develop&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v*.*.*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pull_request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">BUILD_TYPE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Release&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build-and-test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build [${{ matrix.os }} | ${{ matrix.compiler }}]&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.os }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fail-fast&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matrix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gcc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gcc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">g++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">windows-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">msvc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cl&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">os&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">macos-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apple-clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">c_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpp_compiler&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">clang++&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Checkout Repository&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Configure CMake&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CC&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.c_compiler }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CXX&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ matrix.cpp_compiler }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cmake -B build
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -DENABLE_SANITIZERS=${{ matrix.os == &amp;#39;ubuntu-latest&amp;#39; &amp;amp;&amp;amp; &amp;#39;ON&amp;#39; || &amp;#39;OFF&amp;#39; }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build Project&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmake --build build --config ${{ env.BUILD_TYPE }} --parallel 2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Run Unit Tests (CTest)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ASAN_OPTIONS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;detect_leaks=1:symbolize=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">UBSAN_OPTIONS&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;print_stacktrace=1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ctest --build-config ${{ env.BUILD_TYPE }} --output-on-failure --parallel 2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Package with CPack&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith(github.ref, &amp;#39;refs/tags/v&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cpack -C ${{ env.BUILD_TYPE }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Create GitHub Release and Upload Assets&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">startsWith(github.ref, &amp;#39;refs/tags/v&amp;#39;)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">softprops/action-gh-release@v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.tar.gz
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> build/*.zip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">GITHUB_TOKEN&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.GITHUB_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">coverage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Code Coverage Analysis&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ubuntu-latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">if&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github.event_name == &amp;#39;pull_request&amp;#39; || github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Checkout Repository&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">actions/checkout@v4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Install lcov&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y lcov&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Configure CMake for Coverage&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build Project&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cmake --build build --parallel 2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Run Tests&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ctest --output-on-failure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Generate lcov Report&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">working-directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lcov --capture --directory . --output-file coverage.info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> lcov --remove coverage.info &amp;#39;/usr/*&amp;#39; &amp;#39;*_deps/*&amp;#39; &amp;#39;*tests/*&amp;#39; --output-file coverage.info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Upload Coverage to Codecov&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codecov/codecov-action@v3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">files&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">build/coverage.info&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fail_ci_if_error&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="10-더-높은-수준의-cicd를-향하여-정적-분석과-포맷팅">10. 더 높은 수준의 CI/CD를 향하여 (정적 분석과 포맷팅)
&lt;/h2>&lt;p>여기서는 자세한 설명은 생략하지만, 실제 운용에 있어서는 추가적인 품질 보증 도구를 파이프라인에 통합하는 것을 권장합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Clang-Format 강제&lt;/strong>: 코드 리뷰의 부담을 줄이기 위해 &lt;code>clang-format&lt;/code>을 통한 코드 스타일 검사를 CI에 통합하고, 포맷 규칙을 위반하는 경우 파이프라인을 실패시킵니다.&lt;/li>
&lt;li>&lt;strong>정적 분석 (Clang-Tidy)&lt;/strong>: 컴파일러 경고만으로는 막을 수 없는 잠재적 버그나 비효율적인 코드(불필요한 복사 등)를 감지하기 위해, &lt;code>clang-tidy&lt;/code>를 CMake에 통합하고 CI상에서 실행합니다.&lt;/li>
&lt;li>&lt;strong>vcpkg / Conan 캐시 활용&lt;/strong>: 서드파티 라이브러리를 다수 사용하는 경우, 의존성 빌드에 엄청난 시간이 소요됩니다. GitHub Actions의 &lt;code>actions/cache&lt;/code>를 이용하여 vcpkg의 설치된 디렉토리나 Conan의 캐시를 유지함으로써 빌드 시간을 획기적으로 줄일 수 있습니다.&lt;/li>
&lt;/ol>
&lt;h2 id="결론">결론
&lt;/h2>&lt;p>C++ 프로젝트에서 CI/CD 파이프라인 구축은 플랫폼 의존성이나 빌드 도구의 복잡성 때문에 언뜻 보기에 진입 장벽이 높게 느껴집니다. 하지만 GitHub Actions, 모던 CMake, 그리고 CTest/CPack 생태계를 올바르게 조합함으로써, 매우 강력하고 자동화된 개발 흐름을 얻을 수 있습니다.&lt;/p>
&lt;p>본 문서에서 설명한 매트릭스 전략을 이용한 크로스 플랫폼 검증, 새니타이저를 이용한 런타임 버그 감지, 커버리지 측정, 그리고 GitHub Releases로의 자동 배포는 상용 수준의 오픈 소스 프로젝트에서도 널리 채택하고 있는 모범 사례입니다.&lt;/p>
&lt;p>자동화된 CI/CD 파이프라인은 개발자가 &amp;ldquo;버그 찾기&amp;quot;이나 &amp;ldquo;수동 빌드 및 릴리스 작업&amp;quot;에 쏟는 시간을 최소화하고, 본연의 창조적인 코딩 활동에 집중하기 위한 최강의 무기가 됩니다. 여러분의 C++ 프로젝트에도 적극 도입하여, 애자일하고 안심할 수 있는 개발 라이프를 실현하시기 바랍니다.&lt;/p></description></item><item><title>Cloudflare Pages나 GitHub Pages로 Hugo 블로그를 무료로, 초고속으로 공개하는 방법</title><link>http://kenji.blog/ko/p/hugo-hosting-cloudflare-vs-github-pages/</link><pubDate>Sat, 12 Sep 2026 12:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/hugo-hosting-cloudflare-vs-github-pages/</guid><description>&lt;img src="http://kenji.blog/p/hugo-hosting-cloudflare-vs-github-pages/img/eyecatch.jpg" alt="Featured image of post Cloudflare Pages나 GitHub Pages로 Hugo 블로그를 무료로, 초고속으로 공개하는 방법" />&lt;p>웹사이트나 블로그를 운영하는 데 있어서 표시 속도(퍼포먼스), 운영 비용, 그리고 보안은 매우 중요한 요소입니다. 예전에는 WordPress와 같은 동적 CMS(Content Management System)와 렌탈 서버의 조합이 주류를 이루었지만, 현재는 &amp;lsquo;Jamstack&amp;rsquo;이라고 불리는 아키텍처가 큰 주목을 받고 있습니다. 그 중에서도 Go 언어로 만들어진 초고속 정적 사이트 생성기(SSG)인 &amp;lsquo;Hugo&amp;rsquo;와, Cloudflare Pages나 GitHub Pages 같은 모던 호스팅 서비스를 조합하면 &lt;strong>완전 무료이면서 초고속&lt;/strong>인 블로그 환경을 구축할 수 있습니다.&lt;/p>
&lt;p>이 글에서는 Hugo를 사용한 정적 사이트를 Cloudflare Pages나 GitHub Pages에 공개하기 위한 구체적인 절차, 각 플랫폼의 아키텍처 차이, GitHub Actions를 이용한 CI/CD(지속적 통합/지속적 배포) 구축, DNS 최적화, 캐시 전략, 그리고 프라이버시를 고려한 접속 분석 도입에 이르기까지 기술적인 관점에서 매우 깊이 있게 설명합니다.&lt;/p>
&lt;hr>
&lt;h2 id="1-정적-사이트-생성기ssg와-jamstack의-기초">1. 정적 사이트 생성기(SSG)와 Jamstack의 기초
&lt;/h2>&lt;h3 id="11-왜-정적-사이트인가">1.1 왜 정적 사이트인가?
&lt;/h3>&lt;p>기존의 동적 CMS(예: WordPress)는 사용자로부터 요청이 있을 때마다 데이터베이스(MySQL 등)에 쿼리를 실행하고, 서버 사이드(PHP 등)에서 HTML을 동적으로 생성하여 반환합니다. 이 방식은 유연성이 높은 반면, 트래픽의 급증(이른바 화제성 집중이나 DDoS 공격)에 대한 내성이 낮아 캐시 서버(Redis나 Varnish)를 전면에 두는 등 인프라 구성이 복잡해지기 쉽습니다.&lt;/p>
&lt;p>반면, Jamstack(JavaScript, APIs, and Markup) 아키텍처를 채택한 정적 사이트 생성기(SSG)에서는 사전에(빌드 시에) 모든 HTML 파일, CSS, JavaScript를 생성해 둡니다. 사용자의 요청에 대해서는 이미 생성된 정적 파일을 웹 서버(또는 CDN)가 그대로 반환하기만 하므로, 압도적인 고속성과 견고한 보안을 실현할 수 있습니다.&lt;/p>
&lt;h3 id="12-hugo의-우위성">1.2 Hugo의 우위성
&lt;/h3>&lt;p>SSG에는 Next.js, Gatsby, Jekyll, Astro 등 다양한 선택지가 있지만, Hugo의 가장 큰 특징은 바로 &lt;strong>빌드 속도&lt;/strong>입니다. Go 언어의 병행 처리 혜택 덕분에, 수천에서 수만 페이지의 사이트라도 불과 몇 초 만에 빌드가 완료됩니다. 이는 CI/CD 파이프라인에서의 대기 시간을 대폭 줄여주며, 개발자 경험(DX: Developer Experience) 향상과 직결됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-호스팅-서비스의-아키텍처-비교">2. 호스팅 서비스의 아키텍처 비교
&lt;/h2>&lt;p>Hugo로 생성한 정적 파일을 어디에 호스팅할 것인지가 다음 과제입니다. 대표적인 선택지로 Cloudflare Pages, GitHub Pages, 그리고 Netlify를 들 수 있는데, 각각 배후에 있는 네트워크 아키텍처가 다릅니다.&lt;/p>
&lt;h3 id="21-cdn과-엣지-컴퓨팅">2.1 CDN과 엣지 컴퓨팅
&lt;/h3>&lt;p>이러한 플랫폼들은 모두 글로벌하게 분산된 CDN(Content Delivery Network)을 이용하여 콘텐츠를 전송합니다. 하지만 단순한 정적 파일의 캐싱뿐만 아니라, &amp;lsquo;엣지 컴퓨팅&amp;rsquo;을 통해 요청 라우팅이나 헤더 수정을 사용자와 가장 가까운 PoP(Point of Presence)에서 실행할 수 있는지가 차별화 요소가 되고 있습니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;사용자 (도쿄)&amp;#34;] --&amp;gt;|Low Latency| B[&amp;#34;엣지 노드 (NRT)&amp;#34;]
C[&amp;#34;사용자 (뉴욕)&amp;#34;] --&amp;gt;|Low Latency| D[&amp;#34;엣지 노드 (EWR)&amp;#34;]
E[&amp;#34;사용자 (런던)&amp;#34;] --&amp;gt;|Low Latency| F[&amp;#34;엣지 노드 (LHR)&amp;#34;]
B --&amp;gt; G[&amp;#34;오리진 스토리지 / 오브젝트 스토어&amp;#34;]
D --&amp;gt; G
F --&amp;gt; G
&lt;/pre>
&lt;h3 id="22-github-pages">2.2 GitHub Pages
&lt;/h3>&lt;p>GitHub Pages는 GitHub 저장소에서 직접 HTML, CSS, JavaScript 파일을 공개할 수 있는 서비스입니다. 그 배후에는 Fastly 같은 CDN이 이용되고 있어 충분한 퍼포먼스를 발휘합니다. 단, 헤더의 커스터마이즈(예: &lt;code>Cache-Control&lt;/code>이나 보안 헤더 설정)에 제약이 있는 데다, 리다이렉트 설정에는 HTML의 meta refresh나 Jekyll의 플러그인에 의존하는 등 순수한 인프라로서의 기능은 약간 부족한 편입니다.&lt;/p>
&lt;h3 id="23-cloudflare-pages">2.3 Cloudflare Pages
&lt;/h3>&lt;p>Cloudflare Pages는 Cloudflare가 자랑하는 세계 최대 규모의 Anycast 네트워크(275개 이상의 도시에 전개) 위에 구축된 정적 사이트 호스팅 서비스입니다.
HTTP/3(QUIC)의 기본 지원, 이미지 최적화, 엣지 함수(Cloudflare Workers)의 통합 등 압도적인 퍼포먼스 튜닝이 가능합니다. 또한 대역폭에 대한 과금이 없어, 아무리 트래픽이 급증해도 무료로 운영할 수 있다는 점이 큰 장점입니다.&lt;/p>
&lt;h3 id="24-netlify">2.4 Netlify
&lt;/h3>&lt;p>Netlify는 Jamstack의 선구자적인 존재로, 폼 기능, 인증(Identity), 서버리스 함수 등을 통합한 올인원 DX를 제공합니다. 그러나 무료 제공량의 대역폭(월간 100GB)을 초과하면 고액의 종량제 과금이 발생하므로, 이미지나 동영상을 많이 사용하는 블로그에서는 비용 관리에 주의가 필요합니다.&lt;/p>
&lt;hr>
&lt;h2 id="3-퍼포먼스와-지연-시간의-이론적-계산-latex에-의한-수리-모델">3. 퍼포먼스와 지연 시간의 이론적 계산 (LaTeX에 의한 수리 모델)
&lt;/h2>&lt;p>웹 퍼포먼스를 평가하는 데 있어서 지연 시간(Latency)의 감소는 가장 중요한 지표입니다. CDN(엣지)을 이용함으로써 오리진 서버에 직접 접속할 때와 비교하여 지연 시간이 얼마나 줄어드는지를 모델화해 봅시다.&lt;/p>
&lt;p>사용자의 요청이 캐시에 적중할 확률을 &amp;lsquo;캐시 적중률(Cache Hit Ratio)&amp;lsquo;이라 하고, $C$ 로 둡니다. $0 \le C \le 1$ 입니다.
오리진 서버까지의 지연 시간을 $L_{origin}$, 가장 가까운 엣지 노드까지의 지연 시간을 $L_{edge}$ 라고 합니다.&lt;/p>
&lt;p>새로운 평균 지연 시간 $L_{new}$ 는 다음의 기댓값으로 계산됩니다.&lt;/p>
$$ L_{new} = C \times L_{edge} + (1 - C) \times (L_{edge} + L_{origin}) $$&lt;p>이 식을 간략화하면 다음과 같습니다.&lt;/p>
$$ L_{new} = L_{edge} + (1 - C) \times L_{origin} $$&lt;p>예를 들어, 도쿄의 사용자가 미국 동부 해안(뉴욕)에 있는 오리진 서버에 접속할 경우, 광섬유의 물리적인 거리와 라우터에서의 처리 지연을 고려하면 $L_{origin}$ 은 약 200 ms 정도가 됩니다. 반면, Cloudflare와 같은 CDN을 이용하면 도쿄의 엣지 노드에 연결할 수 있으므로 $L_{edge}$ 는 약 10 ms 정도로 단축됩니다.&lt;/p>
&lt;p>만약 캐시 적중률이 $C = 0.95$ (95%)라고 가정하면,&lt;/p>
$$ L_{new} = 10 + (1 - 0.95) \times 200 = 10 + 0.05 \times 200 = 10 + 10 = 20 \text{ ms} $$&lt;p>이처럼 CDN 도입을 통해 평균 지연 시간을 210 ms에서 20 ms로 극적으로(약 90%) 줄이는 것이 가능해집니다.&lt;/p>
&lt;hr>
&lt;h2 id="4-github-actions를-이용한-cicd-파이프라인-구축">4. GitHub Actions를 이용한 CI/CD 파이프라인 구축
&lt;/h2>&lt;p>Hugo 블로그의 업데이트 프로세스를 자동화하기 위해, GitHub Actions를 이용한 CI/CD 파이프라인을 구축합니다. 이를 통해 로컬에서 Markdown 글을 작성하고 &lt;code>git push&lt;/code> 하기만 하면, 자동으로 빌드가 실행되어 Cloudflare Pages나 GitHub Pages에 배포됩니다.&lt;/p>
&lt;p>다음의 시퀀스 다이어그램은 글을 Push한 후 사용자에게 전송될 때까지의 전체 흐름을 보여줍니다.&lt;/p>
&lt;pre class="mermaid">
sequenceDiagram
participant U as &amp;#34;사용자 (작성자)&amp;#34;
participant G as &amp;#34;GitHub 저장소&amp;#34;
participant A as &amp;#34;GitHub Actions&amp;#34;
participant C as &amp;#34;Cloudflare Pages&amp;#34;
participant V as &amp;#34;방문자&amp;#34;
U-&amp;gt;&amp;gt;G: &amp;#34;git push origin main&amp;#34;
G-&amp;gt;&amp;gt;A: &amp;#34;Push 이벤트 트리거&amp;#34;
A-&amp;gt;&amp;gt;A: &amp;#34;체크아웃 및 Hugo 설정&amp;#34;
A-&amp;gt;&amp;gt;A: &amp;#34;hugo --minify (빌드)&amp;#34;
A-&amp;gt;&amp;gt;C: &amp;#34;Cloudflare Pages에 배포 (직접 업로드)&amp;#34;
C--&amp;gt;&amp;gt;A: &amp;#34;배포 URL (성공)&amp;#34;
V-&amp;gt;&amp;gt;C: &amp;#34;웹 페이지 요청&amp;#34;
C--&amp;gt;&amp;gt;V: &amp;#34;캐시된 HTML 반환 (엣지)&amp;#34;
&lt;/pre>
&lt;h3 id="41-cloudflare-pages를-위한-배포-설정-direct-upload">4.1 Cloudflare Pages를 위한 배포 설정 (Direct Upload)
&lt;/h3>&lt;p>Cloudflare Pages에는 GitHub 저장소를 연동시켜 Cloudflare의 인프라 위에서 빌드하는 방법과, GitHub Actions에서 빌드한 정적 파일을 &amp;lsquo;Direct Upload(직접 업로드)&amp;lsquo;하는 방법이 있습니다. Hugo의 버전 관리를 더 엄격하게 하고 다른 작업(테스트나 이미지 최적화 등)과 연동하고 싶다면, GitHub Actions에서 빌드하고 Direct Upload 하는 방식을 추천합니다.&lt;/p>
&lt;p>다음은 Cloudflare Pages에 배포하기 위한 &lt;code>.github/workflows/deploy.yml&lt;/code> 의 실전 예시입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Deploy Hugo site to Cloudflare Pages&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">push&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workflow_dispatch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">jobs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build-and-deploy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runs-on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ubuntu-latest&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Checkout repository&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;actions/checkout@v4&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">submodules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;recursive&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fetch-depth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Setup Hugo&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;peaceiris/actions-hugo@v3&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hugo-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.125.0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extended&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Build Hugo Site&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">run&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;hugo --minify --gc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">HUGO_ENVIRONMENT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;production&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Deploy to Cloudflare Pages&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">uses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;cloudflare/pages-action@v1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">with&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accountId&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">projectName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;your-project-name&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;public&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gitHubToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${{ secrets.GITHUB_TOKEN }}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">branch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 파이프라인에서는 &lt;code>--minify&lt;/code> 옵션으로 HTML/CSS/JS를 축소하고, &lt;code>--gc&lt;/code> 로 불필요한 파일을 삭제하고 있습니다. 이것들은 퍼포먼스 최적화의 기본입니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-dns-설정-깊게-파고들기-커스텀-도메인과-cname--alias-레코드">5. DNS 설정 깊게 파고들기: 커스텀 도메인과 CNAME / ALIAS 레코드
&lt;/h2>&lt;p>독자 도메인(예: &lt;code>kenji.blog&lt;/code>)을 이용할 경우, DNS(Domain Name System)의 적절한 설정이 필수적입니다.&lt;/p>
&lt;h3 id="51-cname-레코드의-제약과-zone-apex">5.1 CNAME 레코드의 제약과 Zone Apex
&lt;/h3>&lt;p>보통 서브 도메인(예: &lt;code>www.kenji.blog&lt;/code>)을 외부 서비스로 연결할 경우에는 &lt;code>CNAME&lt;/code> 레코드를 이용합니다. 그러나 DNS의 사양(RFC 1034)에 따라 루트 도메인(Zone Apex, 네이키드 도메인이라고도 불림. 예: &lt;code>kenji.blog&lt;/code>)에는 &lt;code>CNAME&lt;/code> 레코드를 설정할 수 없습니다. 이는 Zone Apex에는 SOA(Start of Authority) 레코드나 NS(Name Server) 레코드, MX(Mail Exchange) 레코드가 반드시 존재해야 하며, CNAME은 다른 리소스 레코드와 공존할 수 없다는 규칙이 있기 때문입니다.&lt;/p>
&lt;h3 id="52-해결책-alias--aname--cname-flattening">5.2 해결책: ALIAS / ANAME / CNAME Flattening
&lt;/h3>&lt;p>이 문제를 해결하기 위해, 모던 DNS 제공업체들은 자체적인 확장 기능을 제공하고 있습니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>ALIAS / ANAME 레코드&lt;/strong>: DNS 서버 측에서 동적으로 이름 확인을 수행하여, 최종적인 A 레코드(IP 주소)를 클라이언트에 반환합니다. Amazon Route 53 등이 지원하고 있습니다.&lt;/li>
&lt;li>&lt;strong>CNAME Flattening&lt;/strong>: Cloudflare가 제공하는 기능입니다. Zone Apex에 CNAME을 설정한 것처럼 작동하면서, Cloudflare의 권한(Authoritative) DNS 서버가 자동으로 확인한 IP 주소들(A 레코드 및 AAAA 레코드)을 클라이언트에게 투명하게 반환합니다.&lt;/li>
&lt;/ul>
&lt;p>Cloudflare Pages를 이용할 경우, 도메인의 네임 서버를 Cloudflare에 위임하고, 이 &amp;lsquo;CNAME Flattening&amp;rsquo;을 활용하는 것이 가장 매끄럽고 고성능인 구성이 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="6-캐시-전략과-http-헤더-제어">6. 캐시 전략과 HTTP 헤더 제어
&lt;/h2>&lt;p>정적 사이트의 고속화에 있어서 또 하나의 핵심이 되는 것은 &amp;lsquo;캐시 전략&amp;rsquo;입니다. Cloudflare Pages에서는 생성된 파일(&lt;code>_headers&lt;/code> 파일)을 이용하여, HTTP 응답 헤더를 세밀하게 제어할 수 있습니다.&lt;/p>
&lt;h3 id="61-엣지-캐시edge-cache-vs-브라우저-캐시browser-cache">6.1 엣지 캐시(Edge Cache) vs 브라우저 캐시(Browser Cache)
&lt;/h3>&lt;p>캐시에는 크게 나누어 CDN 측에서 유지되는 &amp;lsquo;엣지 캐시&amp;rsquo;와 사용자의 브라우저에 저장되는 &amp;lsquo;브라우저 캐시&amp;rsquo; 2종류가 있습니다.&lt;/p>
&lt;p>정적 파일(이미지, CSS, JS 등 파일명에 해시가 포함된 것)은 브라우저 측에 장기간 캐싱시키는 것이 이상적입니다. 반면, HTML 파일은 업데이트를 즉시 반영하기 위해 브라우저 캐시를 짧게(혹은 무효화) 하고, 엣지 캐시로 처리하는 구성이 일반적입니다.&lt;/p>
&lt;p>Cloudflare Pages에서의 &lt;code>_headers&lt;/code> 설정 예시:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl"># HTML 파일은 브라우저 캐시를 하지 않고, 매번 검증한다
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/*.html
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Cache-Control: public, max-age=0, must-revalidate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"># 에셋 파일(CSS/JS/이미지)은 1년간 브라우저에 캐시한다
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/assets/*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Cache-Control: public, max-age=31536000, immutable
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/img/*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Cache-Control: public, max-age=31536000, immutable
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="62-대역폭-비용-절감-계산식">6.2 대역폭 비용 절감 계산식
&lt;/h3>&lt;p>적절한 캐시 헤더를 설정함으로써 서버(엣지)로부터의 데이터 전송량을 대폭 줄일 수 있습니다. 월간 대역폭 비용 $Cost$ 는 각 리소스의 전송량 $B_i$, 캐시 적중률 $C_i$, 그리고 대역폭 단가 $R$ 에 의해 다음과 같은 모델로 나타낼 수 있습니다.&lt;/p>
$$ Cost = \sum_{i=1}^{n} \left( B_i \times (1 - C_i) \times R \right) $$&lt;p>Cloudflare는 다운로드 전송량이 무료($R = 0$)이므로 직접적인 금전적 비용은 $0$ 이 됩니다. 그러나 GitHub Pages 등 다른 인프라를 병용하는 경우나 AWS 구성을 백엔드로 삼는 경우에는, 이 캐시 적중률 $C_i$ 를 극대화하는 것이 인프라 비용 절감의 핵심이 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="7-프라이버시와-퍼포먼스를-양립하는-접속-분석">7. 프라이버시와 퍼포먼스를 양립하는 접속 분석
&lt;/h2>&lt;p>블로그를 운영하는 데 있어서, 얼마나 많은 사용자가 방문하고 있는지를 알기 위한 접속 분석(Web Analytics)은 필수적입니다. 오랫동안 Google Analytics(GA4)가 사실상의 표준(De facto standard)이었지만, 최근의 프라이버시 보호 흐름(GDPR, CCPA)이나 서드파티 쿠키 폐지에 따라 상황이 변하고 있습니다.&lt;/p>
&lt;h3 id="71-웹-퍼포먼스에-미치는-영향">7.1 웹 퍼포먼스에 미치는 영향
&lt;/h3>&lt;p>Google Analytics(구체적으로는 &lt;code>gtag.js&lt;/code> 나 Google Tag Manager)를 도입하면, 수많은 외부 스크립트의 로딩과 실행이 발생하여 퍼포먼스(특히 TTFB나 메인 스레드 차단 시간)에 악영향을 미칩니다.&lt;/p>
&lt;p>사이트의 로드 시간을 다음과 같이 분해해서 생각해 봅시다.&lt;/p>
&lt;pre class="mermaid">
pie title &amp;#34;일반적인 사이트 로딩 시간 분석&amp;#34;
&amp;#34;TTFB (첫 바이트 도달 시간)&amp;#34; : 15
&amp;#34;콘텐츠 다운로드 (HTML/CSS)&amp;#34; : 20
&amp;#34;DOM 파싱 및 렌더링&amp;#34; : 45
&amp;#34;분석 및 서드파티 JS&amp;#34; : 20
&lt;/pre>
&lt;p>서드파티의 JS 분석 도구는 전체 로딩 시간의 약 20%~30%를 차지하는 일도 드물지 않습니다.&lt;/p>
&lt;h3 id="72-cloudflare-web-analytics의-도입">7.2 Cloudflare Web Analytics의 도입
&lt;/h3>&lt;p>그래서 주목받고 있는 것이 Cloudflare Web Analytics나 Plausible Analytics 같은, 쿠키를 사용하지 않는(Cookieless) 프라이버시 퍼스트 접속 분석입니다.&lt;/p>
&lt;p>Cloudflare Web Analytics는 매우 가벼운 JavaScript 스니펫을 삽입하기만 하면 작동하며, 쿠키를 발행하지 않기 때문에 번거로운 쿠키 동의 배너(Cookie Consent Banner)를 설치할 필요가 없습니다.&lt;/p>
&lt;p>Hugo에서의 구현도 매우 간단합니다. &lt;code>layouts/partials/head.html&lt;/code> 이나 &lt;code>layouts/partials/analytics.html&lt;/code> 에 제공받은 스니펫을 추가하기만 하면 됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">{{ if eq hugo.Environment &amp;#34;production&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Cloudflare Web Analytics --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">defer&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;https://static.cloudflareinsights.com/beacon.min.js&amp;#39;&lt;/span> &lt;span class="na">data-cf-beacon&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#39;{&amp;#34;token&amp;#34;: &amp;#34;YOUR_CLOUDFLARE_BEACON_TOKEN&amp;#34;}&amp;#39;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c">&amp;lt;!-- End Cloudflare Web Analytics --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{ end }}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>defer&lt;/code> 속성을 부여함으로써 HTML 파싱을 차단하지 않고 스크립트를 비동기적으로 불러와, DOM 구축 후에 실행시킬 수 있습니다. 이를 통해 초기 표시 속도(LCP: Largest Contentful Paint 나 FCP: First Contentful Paint)에 미치는 영향을 최소화할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="8-요약-및-베스트-프랙티스">8. 요약 및 베스트 프랙티스
&lt;/h2>&lt;p>Hugo를 이용한 정적 사이트 운영에 있어, Cloudflare Pages나 GitHub Pages와 같은 모던 호스팅 플랫폼을 채택하는 것은 가성비, 표시 속도, 보안의 모든 측면에서 압도적인 장점이 있습니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>초고속 빌드&lt;/strong>: Hugo의 고속성을 살려 CI/CD 파이프라인(GitHub Actions)의 실행 시간을 최소화한다.&lt;/li>
&lt;li>&lt;strong>엣지 전송&lt;/strong>: Cloudflare의 엣지 네트워크를 이용하여 전 세계 사용자에게 밀리초 단위의 지연 시간으로 콘텐츠를 전달한다.&lt;/li>
&lt;li>&lt;strong>적절한 DNS 구성&lt;/strong>: CNAME Flattening을 활용하여 Zone Apex(독자 도메인)를 안전하고 빠르게 운영한다.&lt;/li>
&lt;li>&lt;strong>캐시 전략 최적화&lt;/strong>: &lt;code>_headers&lt;/code> 를 사용하여 브라우저 캐시와 엣지 캐시를 리소스 종류에 따라 적절하게 분리한다.&lt;/li>
&lt;li>&lt;strong>가벼운 애널리틱스&lt;/strong>: 프라이버시를 배려하면서 퍼포먼스를 손상시키지 않는 Cloudflare Web Analytics 등을 도입한다.&lt;/li>
&lt;/ol>
&lt;p>이것들을 조합함으로써 월간 수백만 PV 클래스의 대규모 트래픽도 견뎌낼 수 있는, 확장 가능하고 견고한 블로그 시스템을 무료로 구축할 수 있습니다. 기술 블로그나 기업 사이트, 포트폴리오 사이트 개설을 검토하고 계신 분들은 꼭 이 Jamstack + Hugo + Cloudflare Pages 구성을 시도해 보시기 바랍니다.&lt;/p></description></item></channel></rss>